diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ed4527a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +root = true +charset = utf-8 +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitignore b/.gitignore index 49aac70..5e48c1a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,11 @@ item_db.db /.project /.pydevproject +*.sublime-workspace +*.sublime-project +/ShadowCraft-Engine.pyproj +/ShadowCraft-Engine.pyproj.user +/ShadowCraft-Engine.sln +/.vs/ShadowCraft-Engine/v14/.suo +/.vscode +/TestResults diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 4666151..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include license.txt -recursive-include shadowcraft/core/locale * diff --git a/README b/README deleted file mode 100644 index 4cbfdc2..0000000 --- a/README +++ /dev/null @@ -1,27 +0,0 @@ -ShadowCraft - Engine --------------------- -This repository contains the calculations piece of ShadowCraft, a WoW -theorycraft project. Initially, this is focused on rogues (hence the name), -but the framework is designed such that if other classes wish to make use of it -in the future, they can do so in a sensible and reasonable way. All rogue -specific functionality is currently contained in directories named "rogue" - -for instance, the objects/ directory contains objects of general use for -theorycrafting calculations, while objects/rogue contains objects specifically -for use in rogue theorycraft. - -If you would like to contribute to this project, either to add your own -calculations module (for rogues or otherwise) or to improve what's already here -(bugfixes, new features, etc.) by all means do so; however, I will be -maintaining reasonably tight control over the architecture and *extremely* -tight control over my calculations module (currently located in -calcs/rogue/Aldriana). This doesn't mean you can't contribute stuff; it just -means that you should be aware that I may not accept your changes. - -If you have any questions/comments/suggestions, you can email me at aldriana at -elitistjerks dot com. Additionally, if your question is of a more general -nature, there is a discussion thread for this project on the EJ forums. - -NOTE: Please read style.txt if you intend to submit code to this project. - - -- Aldriana - Oct 28 2010 diff --git a/README.md b/README.md new file mode 100644 index 0000000..a509a23 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +ShadowCraft-Engine +================== + +This repository contains the calculations engine behind the ShadowCraft +theorycrafting webapp for the Rogue class in World of Warcraft. For the +web application including the UI see +[shadowcraft-ui](https://github.com/cheald/shadowcraft-ui). + +ShadowCraft-Engine is written in Python and supports both Python 2 and +3. The calculation modules can be found in shadowcraft/calcs. Objects +used for those calculations are defined in shadowcraft/objects. + + +How To +------ + +In order to support both Python 2 and 3, ShadowCraft-Engine depends on +the *future* library. You can install it by running: + +``` +pip install future +``` + +To run a simple calculation for the rogue spec of your choice you can +look at the examples in the scripts folder. Feel free to play around +and edit those files as you see fit, when testing. E.g. to run a DPS +calculation for Subtlety, type: + +``` +python scripts/subtlety.py +``` + + +Tests +----- + +Although the tests currently do not provide good number testing for the +different specialization models, they can be used to ensure that nothing +major is broken. Run the tests using the following command: + +``` +python tests/runtests.py +``` + +Of course, we appreciate any help in extending the test coverage for the +engine. + + +Contributing +------------ + +The ShadowCraft team is always looking for help. If you would like to +contribute to the engine, you can always contact the active developers +or open a pull request on GitHub. There is also a #shadowcraft channel +on the [Ravenholdt Discord Server](https://discord.gg/DdPahJ9) that can +be used for discussion about the project. + +Before writing code and submitting for review, please have a look at the +code style guidelines in style.md. diff --git a/scripts/assassination.py b/scripts/assassination.py index fad5f0f..327a890 100644 --- a/scripts/assassination.py +++ b/scripts/assassination.py @@ -1,6 +1,10 @@ +from __future__ import division +from __future__ import print_function # Simple test program to debug + play with assassination models. +from builtins import str from os import path import sys +from pprint import pprint sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator @@ -11,149 +15,148 @@ from shadowcraft.objects import stats from shadowcraft.objects import procs from shadowcraft.objects import talents -from shadowcraft.objects import glyphs +from shadowcraft.objects import artifact from shadowcraft.core import i18n -import time - # Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. test_language = 'local' i18n.set_language(test_language) -start = time.time() - # Set up level/class/race -test_level = 100 -test_race = race.Race('none') +test_level = 110 +test_race = race.Race('blood_elf', 'rogue', 110) test_class = 'rogue' +test_spec = 'assassination' # Set up buffs. test_buffs = buffs.Buffs( - #'short_term_haste_buff', - 'stat_multiplier_buff', - 'crit_chance_buff', - 'mastery_buff', - 'haste_buff', - 'multistrike_buff', - 'versatility_buff', - 'attack_power_buff', - 'physical_vulnerability_debuff', - 'spell_damage_debuff', - 'agi_flask_mop', - 'food_mop_agi' - ) + 'short_term_haste_buff', + 'flask_legion_agi', + 'food_legion_mastery_375' +) # Set up weapons. -test_mh = stats.Weapon(812.0, 1.8, 'dagger', 'mark_of_the_shattered_hand') -test_oh = stats.Weapon(812.0, 1.8, 'dagger', 'mark_of_the_frostwolf') +test_mh = stats.Weapon(7063.0, 1.8, 'dagger', None) +test_oh = stats.Weapon(7063.0, 1.8, 'dagger', None) # Set up procs. -test_procs = procs.ProcsList(('scales_of_doom', 691), ('beating_heart_of_the_mountain', 701), - 'draenic_agi_pot', 'draenic_agi_prepot', 'archmages_greater_incandescence') +#test_procs = procs.ProcsList(('scales_of_doom', 691), ('beating_heart_of_the_mountain', 701), +# 'draenic_agi_pot', 'draenic_agi_prepot', 'archmages_greater_incandescence') +test_procs = procs.ProcsList('old_war_pot', 'old_war_prepot', + #'convergence_of_fates', + ('draught_of_souls', 910), + #('chaos_talisman', 890), + ('nightblooming_frond', 910), +) # Set up gear buffs. -test_gear_buffs = stats.GearBuffs('gear_specialization') +test_gear_buffs = stats.GearBuffs('gear_specialization', +#'the_dreadlords_deceit', +'rogue_t19_2pc', +'rogue_t19_4pc', +'zoldyck_family_training_shackles', +'mantle_of_the_master_assassin', +#'duskwalkers_footpads', +#'cinidaria_the_symbiote', +) # Set up a calcs object.. test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, - agi=3650, - stam=2426, - crit=1539, - haste=0, - mastery=1615, - readiness=0, - versatility=122, - multistrike=1034,) - + agi=21964, + stam=32801, + crit=8749, + haste=1935, + mastery=9729, + versatility=4382,) # Initialize talents.. -test_talents = talents.Talents('3322122', test_class, test_level) - -# Set up glyphs. -glyph_list = ['disappearance', 'sprint', 'vendetta'] #just to have something -test_glyphs = glyphs.Glyphs(test_class, *glyph_list) +#test_talents = talents.Talents('2110031', test_spec, test_class, level=test_level) +test_talents = talents.Talents('1230011', test_spec, test_class, level=test_level) + +#initialize artifact traits.. +test_traits = artifact.Artifact(test_spec, test_class, trait_dict={ + 'kingsbane': 1, + 'assassins_blades': 1, + 'toxic_blades': 3, + 'poison_knives': 3, + 'urge_to_kill': 1, + 'balanced_blades ': 3, + 'surge_of_toxins': 1, + 'shadow_walker': 3, + 'master_assassin': 3+2, + 'shadow_swiftness': 1, + 'serrated_edge': 3, + 'bag_of_tricks': 1, + 'master_alchemist': 3, + 'gushing_wounds': 3+1, + 'fade_into_shadows': 3, + 'from_the_shadows': 1, + 'blood_of_the_assassinated': 1, + 'slayers_precision': 1, + 'silence_of_the_uncrowned': 1, + 'strangler': 4, + 'dense_concoction': 0, + 'sinister_circulation': 0, + 'concordance_of_the_legionfall': 0, +}) # Set up settings. -test_cycle = settings.AssassinationCycle(min_envenom_size_non_execute=4, min_envenom_size_execute=5) -test_settings = settings.Settings(test_cycle, response_time=.5, duration=360, dmg_poison='dp', utl_poison='lp', is_pvp=False, - use_opener='always', opener_name='mutilate') +test_cycle = settings.AssassinationCycle() +test_settings = settings.Settings(test_cycle, response_time=.5, duration=300, + finisher_threshold=4) # Build a DPS object. -calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) +calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_traits, test_buffs, test_race, test_spec, test_settings, test_level) + +print(str(calculator.stats.get_character_stats(calculator.race, test_traits))) # Compute DPS Breakdown. dps_breakdown = calculator.get_dps_breakdown() -total_dps = sum(entry[1] for entry in dps_breakdown.items()) -calculator.init_assassination() -non_execute_breakdown = calculator.assassination_dps_breakdown_non_execute() -non_execute_total = sum(entry[1] for entry in non_execute_breakdown.items()) -calculator.init_assassination() -execute_breakdown = calculator.assassination_dps_breakdown_execute() -execute_total = sum(entry[1] for entry in execute_breakdown.items()) +total_dps = sum(entry[1] for entry in list(dps_breakdown.items())) # Compute EP values. ep_values = calculator.get_ep(baseline_dps=total_dps) -#tier_ep_values = calculator.get_other_ep(['rogue_t14_4pc', 'rogue_t14_2pc', 'rogue_t15_4pc', 'rogue_t15_2pc', 'rogue_t16_2pc', 'rogue_t16_4pc']) -#mh_enchants_and_dps_ep_values, oh_enchants_and_dps_ep_values = calculator.get_weapon_ep(dps=True, enchants=True) - -trinkets_list = [ - #5.4 - 'assurance_of_consequence', - 'haromms_talisman', - 'sigil_of_rampage', - 'ticking_ebon_detonator', - 'thoks_tail_tip', - 'discipline_of_xuen', - 'fury_of_xuen', -] -# trinkets_ep_value = calculator.get_upgrades_ep_fast(trinkets_list) +tier_ep_values = calculator.get_other_ep(['rogue_t19_4pc', 'rogue_t19_2pc', +'duskwalkers_footpads', 'zoldyck_family_training_shackles', 'mantle_of_the_master_assassin' +]) + #talent_ranks = calculator.get_talents_ranking() +#trait_ranks = calculator.get_trait_ranking() def max_length(dict_list): max_len = 0 for i in dict_list: - dict_values = i.items() + dict_values = list(i.items()) if max_len < max(len(entry[0]) for entry in dict_values): max_len = max(len(entry[0]) for entry in dict_values) return max_len -def pretty_print(dict_list, total_sum = 1., show_percent=False): +def pretty_print(dict_list): max_len = max_length(dict_list) for i in dict_list: - dict_values = i.items() + dict_values = list(i.items()) dict_values.sort(key=lambda entry: entry[1], reverse=True) for value in dict_values: - #print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) - if show_percent and ("{0:.2f}".format(float(value[1])/total_sum)) != '0.00': - print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' ('+str( "{0:.2f}".format(100*float(value[1])/total_sum) )+'%)' + if ("{0:.2f}".format(value[1] / total_dps)) != '0.00': + print(value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' ('+str( "{0:.2f}".format(100*float(value[1])/total_dps) )+'%)') else: - print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) - print '-' * (max_len + 15) + print(value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1])) + print('-' * (max_len + 15)) dicts_for_pretty_print = [ ep_values, - #tier_ep_values, - #mh_enchants_and_dps_ep_values, - #oh_enchants_and_dps_ep_values, - #trinkets_ep_value, - #glyph_values, + tier_ep_values, #talent_ranks, + #trinkets_ep_value, + dps_breakdown, + #trait_ranks ] pretty_print(dicts_for_pretty_print) -pretty_print([dps_breakdown], total_sum=total_dps, show_percent=True) -print ' ' * (max_length([dps_breakdown]) + 1), total_dps, _("total damage per second.") -print '' -print "Request time: %s sec" % (time.time() - start) - - -print 'non-execute breakdown: ' -pretty_print([non_execute_breakdown], total_sum=non_execute_total, show_percent=True) -print ' ' * (max_length([non_execute_breakdown]) + 1), non_execute_total, _("total damage per second.") +#pretty_print([dps_breakdown], total_sum=total_dps, show_percent=True) +print(' ' * (max_length([dps_breakdown]) + 1), total_dps, ("total damage per second.")) -print 'execute breakdown: ' -pretty_print([execute_breakdown], total_sum=execute_total, show_percent=True) -print ' ' * (max_length([execute_breakdown]) + 1), execute_total, _("total damage per second.") +#pprint(talent_ranks) diff --git a/scripts/assassination_import.py b/scripts/assassination_import.py deleted file mode 100755 index 7845b92..0000000 --- a/scripts/assassination_import.py +++ /dev/null @@ -1,147 +0,0 @@ -# Simple test program to debug + play with assassination models. -from os import path -import sys -from import_character import CharacterData -from char_info import charInfo - - -#sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) - -from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator -from shadowcraft.calcs.rogue.Aldriana import settings - -from shadowcraft.objects import buffs -from shadowcraft.objects import race -from shadowcraft.objects import stats -from shadowcraft.objects import procs -from shadowcraft.objects import proc_data -from shadowcraft.objects import talents -from shadowcraft.objects import glyphs - -from shadowcraft.core import i18n - -# Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. -test_language = 'local' -i18n.set_language(test_language) - - -key = 1 -while key < len(sys.argv): - terms = sys.argv[key].split(':') - charInfo[ terms[0] ] = terms[1] - key += 1 - -print "Loading " + charInfo['name'] + " of " + charInfo['region'] + "-" + charInfo['realm'] + "\n" -character_data = CharacterData(charInfo['region'], charInfo['realm'], charInfo['name'], verbose=charInfo['verbose']) -character_data.do_import() - - -# Set up level/class/race -test_level = 90 -test_race = race.Race(character_data.get_race()) -test_class = 'rogue' - -# Set up buffs. -test_buffs = buffs.Buffs( - 'short_term_haste_buff', - 'stat_multiplier_buff', - 'crit_chance_buff', - 'mastery_buff', - 'melee_haste_buff', - 'attack_power_buff', - 'armor_debuff', - 'physical_vulnerability_debuff', - 'spell_damage_debuff', - 'agi_flask_mop', - 'food_300_agi' - ) - -# Set up weapons. -test_mh = stats.Weapon(*character_data.get_mh()) -test_oh = stats.Weapon(*character_data.get_oh()) - -# Set up procs. -character_procs = character_data.get_procs() -character_procs_allowed = filter(lambda p: p in proc_data.allowed_procs, character_procs) - -#not_allowed_procs = set(character_procs) - set(character_procs_allowed) -#print not_allowed_procs - -test_procs = procs.ProcsList(*character_procs_allowed) - -# Set up a calcs object.. -lst = character_data.get_gear_stats() - -# Set up gear buffs. -character_gear_buffs = character_data.get_gear_buffs() + ['leather_specialization', 'virmens_bite', 'virmens_bite_prepot'] -if character_data.has_chaotic_metagem(): - character_gear_buffs.append('chaotic_metagem') -test_gear_buffs = stats.GearBuffs(*character_gear_buffs) - -test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, **lst) - -# Initialize talents.. -if charInfo['talents'] == None: - charInfo['talents'] = character_data.get_talents() -test_talents = talents.Talents(charInfo['talents'], test_class, test_level) - -# Set up glyphs. -glyph_list = character_data.get_glyphs() -test_glyphs = glyphs.Glyphs(test_class, *glyph_list) - -# Set up settings. -test_cycle = settings.AssassinationCycle(min_envenom_size_non_execute=4, min_envenom_size_execute=5, - prioritize_rupture_uptime_non_execute=True, prioritize_rupture_uptime_execute=True) -test_settings = settings.Settings(test_cycle, response_time=.5, duration=360, dmg_poison='dp', utl_poison='lp', is_pvp=charInfo['pvp'], - stormlash=charInfo['stormlash'], shiv_interval=charInfo['shiv']) - -# Build a DPS object. -#calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level, char_class=test_class) -calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) - -# Compute EP values. -ep_values = calculator.get_ep() - -# Compute DPS Breakdown. -dps_breakdown = calculator.get_dps_breakdown() -non_execute_breakdown = calculator.assassination_dps_breakdown_non_execute() -total_dps = sum(entry[1] for entry in dps_breakdown.items()) -non_execute_total = sum(entry[1] for entry in non_execute_breakdown.items()) -talent_ranks = calculator.get_talents_ranking() -heal_sum, heal_table = calculator.get_self_healing(dps_breakdown=dps_breakdown) - -def max_length(dict_list): - max_len = 0 - for i in dict_list: - dict_values = i.items() - if max_len < max(len(entry[0]) for entry in dict_values): - max_len = max(len(entry[0]) for entry in dict_values) - - return max_len - -def pretty_print(dict_list, total_sum = 1.): - max_len = max_length(dict_list) - - for i in dict_list: - dict_values = i.items() - dict_values.sort(key=lambda entry: entry[1], reverse=True) - for value in dict_values: - #print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) - if ("{0:.2f}".format(10*float(value[1])/total_dps)) != '0.00': - print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' ('+str( "{0:.2f}".format(100*float(value[1])/total_sum) )+'%)' - else: - print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) - print '-' * (max_len + 15) - -dicts_for_pretty_print = [ - ep_values, - talent_ranks, - heal_table, - dps_breakdown -] -pretty_print(dicts_for_pretty_print, total_sum=total_dps) -print ' ' * (max_length(dicts_for_pretty_print) + 1), total_dps, _("total damage per second.") -print '' -print 'non-execute breakdown: ' -pretty_print([non_execute_breakdown], total_sum=non_execute_total) -print ' ' * (max_length([non_execute_breakdown]) + 1), non_execute_total, _("total damage per second.") diff --git a/scripts/char_info.py b/scripts/char_info.py deleted file mode 100644 index eeb1369..0000000 --- a/scripts/char_info.py +++ /dev/null @@ -1,2 +0,0 @@ -charInfo = {'region':'us', 'realm':'Doomhammer', 'name':'Pins', 'talents':None, - 'stormlash':False, 'pvp':False, 'shiv':0, 'verbose':False, 'blade_flurry':False} diff --git a/scripts/combat.py b/scripts/combat.py deleted file mode 100644 index 3086072..0000000 --- a/scripts/combat.py +++ /dev/null @@ -1,145 +0,0 @@ -# Simple test program to debug + play with assassination models. -from os import path -import sys -sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) - -from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator -from shadowcraft.calcs.rogue.Aldriana import settings - -from shadowcraft.objects import buffs -from shadowcraft.objects import race -from shadowcraft.objects import stats -from shadowcraft.objects import procs -from shadowcraft.objects import talents -from shadowcraft.objects import glyphs - -from shadowcraft.core import i18n - -import time - -# Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. -test_language = 'local' -i18n.set_language(test_language) - -start = time.time() - -# Set up level/class/race -test_level = 100 -test_race = race.Race('troll') -test_class = 'rogue' - -# Set up buffs. -test_buffs = buffs.Buffs( - 'short_term_haste_buff', - 'stat_multiplier_buff', - 'crit_chance_buff', - 'mastery_buff', - 'haste_buff', - 'multistrike_buff', - 'versatility_buff', - 'attack_power_buff', - 'physical_vulnerability_debuff', - 'spell_damage_debuff', - 'flask_wod_agi', - 'food_mop_agi' - ) - -# Set up weapons: dancing_steel mark_of_the_shattered_hand mark_of_warsong -test_mh = stats.Weapon(410., 2.6, 'sword', 'dancing_steel') -#test_mh = stats.Weapon(420.5, 1.8, 'dagger', 'mark_of_the_shattered_hand') -test_oh = stats.Weapon(410., 2.6, 'sword', 'dancing_steel') - -# Set up procs. -test_procs = procs.ProcsList(('assurance_of_consequence', 588), ('draenic_philosophers_stone', 620), 'virmens_bite', 'virmens_bite_prepot', 'archmages_incandescence') #trinkets, other things (legendary procs) - -# Set up gear buffs. -test_gear_buffs = stats.GearBuffs('gear_specialization', 'rogue_t17_2pc', 'rogue_t17_4pc') #tier buffs located here - -# Set up a calcs object.. -test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, - agi=3650, - stam=2426, - crit=1039, - haste=1100, - mastery=1015, - readiness=0, - versatility=122, - multistrike=1034,) - -# Initialize talents.. -test_talents = talents.Talents('3111121', test_class, test_level) - -# Set up glyphs. -glyph_list = ['energy', 'disappearance'] -test_glyphs = glyphs.Glyphs(test_class, *glyph_list) - -# Set up settings. -test_cycle = settings.CombatCycle(revealing_strike_pooling=True, blade_flurry=False, dfa_during_ar=True) -test_settings = settings.Settings(test_cycle, response_time=.5, duration=360, dmg_poison='dp', utl_poison='lp', - latency=.03, merge_damage=True, use_opener='always', opener_name='ambush', - num_boss_adds=0.0, adv_params="") # 0.2 = 20% of the fight is an add present - -# Build a DPS object. -calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) - -# Compute DPS Breakdown. -dps_breakdown = calculator.get_dps_breakdown() -total_dps = sum(entry[1] for entry in dps_breakdown.items()) - -# Compute EP values. -ep_values = calculator.get_ep(baseline_dps=total_dps) -#tier_ep_values = calculator.get_other_ep(['rogue_t16_2pc', 'rogue_t16_4pc']) -#mh_enchants_and_dps_ep_values, oh_enchants_and_dps_ep_values = calculator.get_weapon_ep(dps=True, enchants=True) - -trinkets_list = { - #5.4 - 'assurance_of_consequence': [(528,532,536),(540,544,548),(553,557,561),(559,563,567),(566,570,574),(572,576,580)], - 'haromms_talisman': [(528,532,536),(540,544,548),(553,557,561),(559,563,567),(566,570,574),(572,576,580)], - 'sigil_of_rampage': [(528,532,536),(540,544,548),(553,557,561),(559,563,567),(566,570,574),(572,576,580)], - 'ticking_ebon_detonator': [(528,532,536),(540,544,548),(553,557,561),(559,563,567),(566,570,574),(572,576,580)], - 'thoks_tail_tip': [(528,532,536),(540,544,548),(553,557,561),(559,563,567),(566,570,574),(572,576,580)], - 'discipline_of_xuen': [(496,500,504),(535,539,543)], -} -#trinkets_ep_value = calculator.get_upgrades_ep_fast(trinkets_list) -#glyph_values = calculator.get_glyphs_ranking() - -# Compute weapon type modifier. -#weapon_type_mod = calculator.get_oh_weapon_modifier() -talent_ranks = calculator.get_talents_ranking() - -def max_length(dict_list): - max_len = 0 - for i in dict_list: - dict_values = i.items() - if max_len < max(len(entry[0]) for entry in dict_values): - max_len = max(len(entry[0]) for entry in dict_values) - - return max_len - -def pretty_print(dict_list, total_sum = 1., show_percent=False): - max_len = max_length(dict_list) - - for i in dict_list: - dict_values = i.items() - dict_values.sort(key=lambda entry: entry[1], reverse=True) - for value in dict_values: - #print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) - if show_percent and ("{0:.2f}".format(float(value[1])/total_dps)) != '0.00': - print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' ('+str( "{0:.2f}".format(100*float(value[1])/total_sum) )+'%)' - else: - print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) - print '-' * (max_len + 15) - -dicts_for_pretty_print = [ - ep_values, - #tier_ep_values, - #mh_enchants_and_dps_ep_values, - #oh_enchants_and_dps_ep_values, - #trinkets_ep_value, - #glyph_values, - talent_ranks, -] -pretty_print(dicts_for_pretty_print) -pretty_print([dps_breakdown], total_sum=total_dps, show_percent=True) -print ' ' * (max_length(dicts_for_pretty_print) + 1), total_dps, _("total damage per second.") -print "Request time: %s sec" % (time.time() - start) diff --git a/scripts/combat_import.py b/scripts/combat_import.py deleted file mode 100755 index fdf8dc8..0000000 --- a/scripts/combat_import.py +++ /dev/null @@ -1,146 +0,0 @@ -# Simple test program to debug + play with combat models. -from os import path -import sys -from import_character import CharacterData -from char_info import charInfo -#sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) - -from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator -from shadowcraft.calcs.rogue.Aldriana import settings - -from shadowcraft.objects import buffs -from shadowcraft.objects import race -from shadowcraft.objects import stats -from shadowcraft.objects import procs -from shadowcraft.objects import proc_data -from shadowcraft.objects import talents -from shadowcraft.objects import glyphs - -from shadowcraft.core import i18n - -# Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. -test_language = 'local' -i18n.set_language(test_language) - - -key = 1 -while key < len(sys.argv): - terms = sys.argv[key].split(':') - if terms[0] in ['stormlash', 'shiv']: - charInfo[ terms[0] ] = float(terms[1]) - else: - charInfo[ terms[0] ] = terms[1] - key += 1 - -print "Loading " + charInfo['name'] + " of " + charInfo['region'] + "-" + charInfo['realm'] + "\n" -character_data = CharacterData(charInfo['region'], charInfo['realm'], charInfo['name'], verbose=charInfo['verbose']) -character_data.do_import() - - -# Set up level/class/race -test_level = 90 -test_race = race.Race(character_data.get_race()) -test_class = 'rogue' - -# Set up buffs. -test_buffs = buffs.Buffs( - 'short_term_haste_buff', - 'stat_multiplier_buff', - 'crit_chance_buff', - 'mastery_buff', - 'melee_haste_buff', - 'spell_haste_buff', - 'attack_power_buff', - 'armor_debuff', - 'physical_vulnerability_debuff', - 'spell_damage_debuff', - 'agi_flask_mop', - 'food_300_agi' - ) - -# Set up weapons. -test_mh = stats.Weapon(*character_data.get_mh()) -test_oh = stats.Weapon(*character_data.get_oh()) - -# Set up procs. -character_procs = character_data.get_procs() -character_procs_allowed = filter(lambda p: p in proc_data.allowed_procs, character_procs) - -#not_allowed_procs = set(character_procs) - set(character_procs_allowed) -#print not_allowed_procs - -test_procs = procs.ProcsList(*character_procs_allowed) - -# Set up a calcs object.. -lst = character_data.get_gear_stats() - -# Set up gear buffs. -character_gear_buffs = character_data.get_gear_buffs() + ['leather_specialization', 'virmens_bite', 'virmens_bite_prepot'] -if character_data.has_chaotic_metagem(): - character_gear_buffs.append('chaotic_metagem') -test_gear_buffs = stats.GearBuffs(*character_gear_buffs) - -test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, **lst) - -# Initialize talents.. -if charInfo['talents'] == None: - charInfo['talents'] = character_data.get_talents() -test_talents = talents.Talents(charInfo['talents'], test_class, test_level) - -# Set up glyphs. -glyph_list = character_data.get_glyphs() -test_glyphs = glyphs.Glyphs(test_class, *glyph_list) - -# Set up settings. -if character_data.get_mh_type() == 'dagger': - print "\nALERT: Dagger found. Playing combat with a dagger should be a last resort, and is not recommended. \n\n" -test_cycle = settings.CombatCycle(use_rupture=True, ksp_immediately=True, revealing_strike_pooling=True, blade_flurry=charInfo['blade_flurry']) -test_settings = settings.Settings(test_cycle, response_time=.5, duration=360, dmg_poison='dp', utl_poison='lp', is_pvp=charInfo['pvp'], - stormlash=charInfo['stormlash'], shiv_interval=charInfo['shiv']) - -# Build a DPS object. -calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) - -# Compute EP values. -ep_values = calculator.get_ep() - -# Compute DPS Breakdown. -dps_breakdown = calculator.get_dps_breakdown() -total_dps = sum(entry[1] for entry in dps_breakdown.items()) -talent_ranks = calculator.get_talents_ranking() -heal_sum, heal_table = calculator.get_self_healing(dps_breakdown=dps_breakdown) - -# Compute weapon type modifier. -weapon_type_mod = calculator.get_oh_weapon_modifier() - -def max_length(dict_list): - max_len = 0 - for i in dict_list: - dict_values = i.items() - if max_len < max(len(entry[0]) for entry in dict_values): - max_len = max(len(entry[0]) for entry in dict_values) - - return max_len - -def pretty_print(dict_list): - max_len = max_length(dict_list) - - for i in dict_list: - dict_values = i.items() - dict_values.sort(key=lambda entry: entry[1], reverse=True) - for value in dict_values: - if ("{0:.2f}".format(float(value[1])/total_dps)) != '0.00': - print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' ('+str( "{0:.2f}".format(100*float(value[1])/total_dps) )+'%)' - else: - print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) - print '-' * (max_len + 15) - -dicts_for_pretty_print = [ - weapon_type_mod, - ep_values, - talent_ranks, - heal_table, - dps_breakdown -] -pretty_print(dicts_for_pretty_print) -print ' ' * (max_length(dicts_for_pretty_print) + 1), total_dps, _("total damage per second.") diff --git a/scripts/dm_combat.py b/scripts/dm_combat.py deleted file mode 100644 index d5b7160..0000000 --- a/scripts/dm_combat.py +++ /dev/null @@ -1,115 +0,0 @@ -# Simple test program to debug + play with assassination models. -from os import path -import sys -sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) - -from shadowcraft.calcs.darkmantle import DarkmantleCalculator -from shadowcraft.calcs.darkmantle.rogue import RogueDarkmantleCalculator -from shadowcraft.calcs.darkmantle import settings - -from shadowcraft.objects import buffs -from shadowcraft.objects import race -from shadowcraft.objects import stats -from shadowcraft.objects import procs -from shadowcraft.objects import talents -from shadowcraft.objects import glyphs -from shadowcraft.objects import priority_list - -from shadowcraft.core import i18n - -import time - -# Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. -test_language = 'local' -i18n.set_language(test_language) - -start = time.time() - -# Set up level/class/race -test_level = 90 -test_race = race.Race('pandaren') -test_class = 'rogue' - -# Set up buffs. -test_buffs = buffs.Buffs( - 'short_term_haste_buff', - 'stat_multiplier_buff', - 'crit_chance_buff', - 'mastery_buff', - 'haste_buff', - 'multistrike_buff', - 'attack_power_buff', - 'armor_debuff', - 'physical_vulnerability_debuff', - 'spell_damage_debuff', - ) - -# Set up weapons. -test_mh = stats.Weapon(571.0, 2.6, 'axe', 'dancing_steel') -test_oh = stats.Weapon(571.0, 2.6, 'axe', 'dancing_steel') - -# Set up procs. -test_procs = procs.ProcsList(('assurance_of_consequence', 580), ('haromms_talisman', 580), 'legendary_capacitive_meta', 'fury_of_xuen') - -# Set up gear buffs. -test_gear_buffs = stats.GearBuffs('rogue_t16_2pc', 'rogue_t16_4pc', 'leather_specialization') - -# Set up a calcs object.. -test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, - agi=862, - stam=1000, - crit=87, - haste=553, - mastery=200, - versatility=160, - multistrike=120,) - -# Initialize talents.. -test_talents = talents.Talents('332213', test_class, test_level) - -# Just a priority list to define the course of actions -#priority_list = PriorityList()#'prepot = prefight,!buff.stealth', - #'stealth = prefight,!buff.stealth', - #'ambush = buff.stealth') - -# Set up glyphs. -glyph_list = ['recuperate'] -test_glyphs = glyphs.Glyphs(test_class, *glyph_list) - -# Set up settings. -test_cycle = settings.CombatCycle() -test_settings = settings.Settings(test_cycle, response_time=.5, latency=.03, merge_damage=True, style='time', limit=10) - -# Build a DPS object. -calculator = RogueDarkmantleCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) - -# Compute DPS Breakdown. -dps_breakdown = calculator.get_dps_breakdown() -total_dps = sum(entry[1] for entry in dps_breakdown.items()) - -def max_length(dict_list): - max_len = 0 - for i in dict_list: - dict_values = i.items() - if max_len < max(len(entry[0]) for entry in dict_values): - max_len = max(len(entry[0]) for entry in dict_values) - - return max_len - -def pretty_print(dict_list, total_sum = 1., show_percent=False): - max_len = max_length(dict_list) - - for i in dict_list: - dict_values = i.items() - dict_values.sort(key=lambda entry: entry[1], reverse=True) - for value in dict_values: - #print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) - if show_percent and ("{0:.2f}".format(float(value[1])/total_dps)) != '0.00': - print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' ('+str( "{0:.2f}".format(100*float(value[1])/total_sum) )+'%)' - else: - print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) - print '-' * (max_len + 15) - -pretty_print([dps_breakdown], total_sum=total_dps, show_percent=True) -print ' ' * (max_length([dps_breakdown]) + 1), total_dps, _("total damage per second.") -print "Request time: %s sec" % (time.time() - start) \ No newline at end of file diff --git a/scripts/import_character.py b/scripts/import_character.py deleted file mode 100755 index ce92449..0000000 --- a/scripts/import_character.py +++ /dev/null @@ -1,458 +0,0 @@ -# Original Code by by Ayliex @ EJ ( https://github.com/postrov/sc-character-import ) -# -*- coding: utf-8 -*- -from os import path -from types import * -import sys -import pprint -import shelve -import math - -sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) - -from wowapi.api import WoWApi - -wowapi = WoWApi() -pp = pprint.PrettyPrinter(indent=4) - - -class ItemDB: - def __init__(self): - pass - - def get_item(self, id): - return None - - def add_item(self, id, item): - pass - - def close(self): - pass - - - -class SimpleItemDB(ItemDB): - def __init__(self, path): - self.path = path - self.db = shelve.open(path, writeback = True) - - def get_item(self, id): - str_id = str(id) - if str_id in self.db: - return self.db[str_id] - else: - return None - - def add_item(self, id, item): - str_id = str(id) - self.db[str_id] = item - self.sync() # FIXME - - def close(self): - self.db.close() - - def sync(self): - self.db.sync() - - -item_db = SimpleItemDB('item_db') - - -def get_item_cached(region, id): - cached_item = item_db.get_item(id) - if cached_item: - return cached_item - else: - item = wowapi.get_item(region, id) - item_db.add_item(id, item) - return item - -class CharacterData: - races = {1 : 'human', - 2 : 'orc', - 3 : 'dwarf', - 4 : 'night_elf', - 5 : 'undead', - 6 : 'tauren', - 7 : 'gnome', - 8 : 'troll', - 9 : 'goblin', - 10 : 'blood_elf', - 11 : 'draenei', - 22 : 'worgen', - 24 : 'pandaren', #Neutral - 25 : 'pandaren', #Alliance - 26 : 'pandaren', #Horde - } - statMap = {3:'agi', 4:'str', 5:'int', 6:'spirit', 7:'stam', 31:'hit', 32:'crit', 36:'haste', 37:'exp', 49:'mastery', - 35:'pvp_resil', 57:'pvp_power'} - - enchants = {4441 : 'windsong', - 4443 : 'elemental_force', - 4444 : 'dancing_steel', - 4416 : [{'stat':'agi', 'value':170}], # Enchant Bracer - Greater Agility - 4359 : [{'stat':'agi', 'value':180}], #Enchanting Perk - 4411 : [{'stat':'mastery', 'value':170}], - 4416 : [{'stat':'agi', 'value':170}], - 4419 : [{'stat':'agi', 'value':80}, {'stat':'str', 'value':80}, {'stat':'stam', 'value':80}], - 4421 : [{'stat':'hit', 'value':180}], - 4424 : [{'stat':'crit', 'value':180}], - 4426 : [{'stat':'haste', 'value':175}], # Enchant Boots - Greater Haste - 4428 : [{'stat':'agi', 'value':140}], #Speed Boost - 4430 : [{'stat':'haste', 'value':170}], - 4431 : [{'stat':'exp', 'value':170}], - 4433 : [{'stat':'mastery', 'value':170}], - 4429 : [{'stat':'mastery', 'value':140}], # Pandaren's Step - 4804 : [{'stat':'agi', 'value':200}, {'stat':'crit', 'value':100}], - 4822 : [{'stat':'agi', 'value':285}, {'stat':'crit', 'value':165}], - 4875 : [{'stat':'agi', 'value':500}], #Leatherworking Perk - 4871 : [{'stat':'agi', 'value':170}, {'stat':'crit', 'value':100}], - 4880 : [{'stat':'agi', 'value':285}, {'stat':'crit', 'value':165}], - 4822 : [{'stat':'agi', 'value':285}, {'stat':'crit', 'value':165}], # Shadowleather Leg Armor - 4411 : [{'stat':'mastery', 'value':170}], - 4871 : [{'stat':'agi', 'value':170}, {'stat':'crit', 'value':100}], - 4427 : [{'stat':'hit', 'value':175}], - 4908 : [{'stat':'agi', 'value':120}, {'stat':'crit', 'value':80}], # Tiger Claw Inscrption - } - - trinkets = {87057 : 'heroic_bottle_of_infinite_stars', - 86132 : 'bottle_of_infinite_stars', - 87167 : 'heroic_terror_in_the_mists', - 79328 : 'relic_of_xuen', - 86791 : 'lfr_bottle_of_infinite_stars', - 86332 : 'terror_in_the_mists', - 87079 : 'heroic_jade_bandit_figurine', - 75274 : 'zen_alchemist_stone', - 86890 : 'lfr_terror_in_the_mists', - 89082 : 'hawkmasters_talon', - 86043 : 'jade_bandit_figurine', - 81267 : 'searing_words', - 81265 : 'flashing_steel_talisman', - 81125 : 'windswept_pages', - 86772 : 'lfr_jade_bandit_figurine', - 87574 : 'corens_cold_chromium_coaster'} - - sets = {'t14' : {'pieces': {u'head' : 'Helmet of the Thousandfold Blades', - u'shoulder' : 'Spaulders of the Thousandfold Blades', - u'chest' : 'Tunic of the Thousandfold Blades', - u'hands' : 'Gloves of the Thousandfold Blades', - u'legs' : 'Legguards of the Thousandfold Blades'}, - 'set_bonus' : {2 : 'rogue_t14_2pc', 4 : 'rogue_t14_4pc'}}} - - glyphs = {# Major - u'Glyph of Adrenaline Rush' : 'adrenaline_rush', - u'Glyph of Ambush' : 'ambush', - u'Glyph of Blade Flurry' : 'blade_flurry', - u'Glyph of Blind' : 'blind', - u'Glyph of Cheap Shot' : 'cheap_shot', - u'Glyph of Cloak of Shadows' : 'cloak_of_shadows', - u'Glyph of Crippling Poison' : 'crippling_poison', - u'Glyph of Deadly Momentum' : 'deadly_momentum', - u'Glyph of Debilitation' : 'debilitation', - u'Glyph of Evasion' : 'evasion', - u'Glyph of Expose Armor' : 'expose_armor', - u'Glyph of Feint' : 'feint', - u'Glyph of Garrote' : 'garrote', - u'Glyph of Gouge' : 'gouge', - u'Glyph of Kick' : 'kick', - u'Glyph of Recuperate' : 'recuperate', - u'Glyph of Sap' : 'sap', - u'Glyph of Shadow Walk' : 'shadow_walk', - u'Glyph of Shiv' : 'shiv', - u'Glyph of Smoke Bomb' : 'smoke_bomb', - u'Glyph of Sprint' : 'sprint', - u'Glyph of Stealth' : 'stealth', - u'Glyph of Vanish' : 'vanish', - u'Glyph of Vendetta' : 'vendetta', - # Minor - u'Glyph of Blurred Speed' : 'blurred_speed', - u'Glyph of Decoy' : 'decoy', - u'Glyph of Detection' : 'detection', - u'Glyph of Disguise' : 'disguise', - u'Glyph of Distract' : 'distract', - u'Glyph of Hemorrhage' : 'hemorrhage', - u'Glyph of Killing Spree' : 'killing_spree', - u'Glyph of Pick Lock' : 'pick_lock', - u'Glyph of Pick Pocket' : 'pick_pocket', - u'Glyph of Poisons' : 'poisons', - u'Glyph of Safe Fall' : 'safe_fall', - u'Glyph of Tricks of the Trade' : 'tricks_of_the_trade'} - - reforgeMap = {113: ('spirit', 'dodge_rating'), - 114: ('spirit','parry_rating'), - 115: ('spirit','hit'), - 116: ('spirit','crit'), - 117: ('spirit','haste'), - 118: ('spirit','exp'), - 119: ('spirit','mastery'), - 120: ('dodge_rating','spirit'), - 121: ('dodge_rating','parry_rating'), - 122: ('dodge_rating','hit'), - 123: ('dodge_rating','crit'), - 124: ('dodge_rating','haste'), - 125: ('dodge_rating','exp'), - 126: ('dodge_rating','mastery'), - 127: ('parry_rating','spirit'), - 128: ('parry_rating','dodge_rating'), - 129: ('parry_rating','hit'), - 130: ('parry_rating','crit'), - 131: ('parry_rating','haste'), - 132: ('parry_rating','exp'), - 133: ('parry_rating','mastery'), - 134: ('hit','spirit'), - 135: ('hit','dodge_rating'), - 136: ('hit','parry_rating'), - 137: ('hit','crit'), - 138: ('hit','haste'), - 139: ('hit','exp'), - 140: ('hit','mastery'), - 141: ('crit','spirit'), - 142: ('crit','dodge_rating'), - 143: ('crit','parry_rating'), - 144: ('crit','hit'), - 145: ('crit','haste'), - 146: ('crit','exp'), - 147: ('crit','mastery'), - 148: ('haste','spirit'), - 149: ('haste','dodge_rating'), - 150: ('haste','parry_rating'), - 151: ('haste','hit'), - 152: ('haste','crit'), - 153: ('haste','exp'), - 154: ('haste','mastery'), - 155: ('exp','spirit'), - 156: ('exp','dodge_rating'), - 157: ('exp','parry_rating'), - 158: ('exp','hit'), - 159: ('exp','crit'), - 160: ('exp','haste'), - 161: ('exp','mastery'), - 162: ('mastery','spirit'), - 163: ('mastery','dodge_rating'), - 164: ('mastery','parry_rating'), - 165: ('mastery','hit'), - 166: ('mastery','crit'), - 167: ('mastery','haste'), - 168: ('mastery','exp'), - } - - - def __init__(self, region, realm, name, verbose=False): - self.region = region - self.realm = realm - self.name = name - self.verbose = verbose - self.raw_data = None - self.chaotic_metagem = False - - def do_import(self): - self.raw_data = wowapi.get_character(self.region , self.realm, self.name, ['talents', 'items', 'stats']) - - def get_race(self): - return CharacterData.races[self.raw_data[u'data'][u'race']] - - def get_weapon(self, weapon_data, item_data): - weapon_info = weapon_data['data'][u'weaponInfo'] - weaponMap = {0:'axe', 1:'axe', 2:'2h_axe', 3:'bow', 4:'rifle',5:'mace', 6:'2h_mace', 7:'polearm', 8:'sword', 9:'2H_sword', 10:'staff', - 11:'exotic', 12:'2h_exotic', 13:'fist', 14:'misc', 15:'dagger', 16:'thrown', 17:'spear', 18:'xbow', 19:'wand', - 20:'fishing_pole'} - tmpItem = get_item_cached(self.region, item_data[u'id']) - damage_info = weapon_info[u'damage'] - damage = (damage_info[u'max'] + damage_info[u'min']) / 2 - speed = weapon_info[u'weaponSpeed'] - type = weaponMap[ tmpItem['data'][u'itemSubClass'] ] - enchant = CharacterData.enchants[item_data[u'tooltipParams'][u'enchant']] - return [damage, speed, type, enchant] - - def get_mh(self): - item_data = self.raw_data['data'][u'items'][u'mainHand'] - weapon_data = get_item_cached(self.region, item_data[u'id']) -# weapon_data = get_item_cached(self.region, 85924) - return self.get_weapon(weapon_data, item_data) - - def get_oh(self): - item_data = self.raw_data['data'][u'items'][u'offHand'] - weapon_data = get_item_cached(self.region, item_data[u'id']) - return self.get_weapon(weapon_data, item_data) - - def get_mh_type(self): - return self.get_mh()[2] - - def get_trinket_proc(self, item_data): - id = item_data[u'id'] - if id in CharacterData.trinkets: - return CharacterData.trinkets[id] - else: - return item_data[u'name'] # fallback, this will most likely be rejected by shadowcraft - - def get_trinket_procs(self): - trinket1 = self.raw_data['data'][u'items'][u'trinket1'] - trinket2 = self.raw_data['data'][u'items'][u'trinket2'] - return [self.get_trinket_proc(trinket1), self.get_trinket_proc(trinket2)] - - def get_procs(self): - procs = [] - procs += self.get_trinket_procs() - return procs - - def get_set_bonuses(self): - set_bonuses = [] - for set_name in CharacterData.sets: - s = CharacterData.sets[set_name] - pieces = s['pieces'] - pieces_found = 0 - for p in pieces: - if self.raw_data['data'][u'items'][p][u'name'] == pieces[p]: - pieces_found += 1 -# print 'found set piece, set: %s, pieces so far: %d' % (set_name, pieces_found) - if pieces_found in s['set_bonus']: - set_bonuses.append(s['set_bonus'][pieces_found]) - return set_bonuses - - def get_gear_buffs(self): - gear_buffs = [] - gear_buffs += self.get_set_bonuses() - return gear_buffs - - def get_stats(self): - stats_data = self.raw_data['data'][u'stats'] - agi = stats_data[u'agi'] - str = stats_data[u'str'] - ap = stats_data[u'attackPower'] - crit = stats_data[u'critRating'] - hit = stats_data[u'hitRating'] - exp = stats_data[u'expertiseRating'] - haste = stats_data[u'hasteRating'] - mast = stats_data[u'masteryRating'] -# ret = [str, agi + 956, 250, crit, hit, exp, haste, mast] -# pp.pprint(ret) - return [str, agi, ap - 2 * agi, crit, hit, exp, haste, mast] - - def get_gear_stats(self): - # - lst = {'agi': 0, 'str':0, 'int':0, 'spirit':0, 'stam':0, 'crit':0, 'hit':0, 'exp':0, 'haste':0, 'mastery':0, 'ap':0, 'pvp_power':0, 'pvp_resil':0} - reforge = ('none', 'none') - reforgeID = None - gemColorToSocketColors = {u'RED': (u'RED'), u'YELLOW':(u'YELLOW'), u'BLUE':(u'BLUE'), u'META':(u'META'), u'COGWHEEL':(u'COGWHEEL'), u'HYDRAULIC':(u'HYDRAULIC'), - u'ORANGE':(u'RED', u'YELLOW'), u'PURPLE':(u'RED', u'BLUE'), u'GREEN':(u'YELLOW', u'BLUE')} - verboseStatMap = {'Agility':'agi', 'Strength':'str', 'Stamina':'stam', 'Critical Strike':'crit', 'Hit':'hit', - 'Expertise':'exp', 'Haste':'haste', 'Mastery':'mastery', 'Increased Critical Effect':'chaotic_metagem', - 'PvP Resilience':'pvp_resil', 'PvP Power':'pvp_power'} - #Loops over every item - for p in self.raw_data['data'][u'items']: - try: - #ilvl is included in the gear array for some unknown reason, lets ignore it - if p != 'averageItemLevelEquipped' and p != 'averageItemLevel': - tmpItem = get_item_cached(self.region, self.raw_data['data'][u'items'][p][u'id']) - self.verbose_print('\n' + p + ': ' + self.raw_data['data'][u'items'][p][u'name']) - params = self.raw_data['data'][u'items'][p][u'tooltipParams'] - #grab the reforge if it exists - if u'reforge' in self.raw_data['data'][u'items'][p][u'tooltipParams']: - reforgeID = self.raw_data['data'][u'items'][p][u'tooltipParams'][u'reforge'] - #if we have data on the reforge - if reforgeID in CharacterData.reforgeMap.keys(): - reforge = CharacterData.reforgeMap[reforgeID] - #for each stat on the gear - for key in tmpItem[u'data'][u'bonusStats']: - if key[u'stat'] in CharacterData.statMap and CharacterData.statMap[key[u'stat']] == reforge[0]: - #if a reforge was found - tmpVal = math.ceil(key[u'amount'] * .6) - lst[ CharacterData.statMap[key[u'stat']] ] += tmpVal - lst[ reforge[1] ] += key[u'amount'] - tmpVal - self.verbose_print('Reforge found: +' + str(tmpVal) + ' ' + reforge[0] + ', +' + str(key[u'amount'] - tmpVal) + ' ' + reforge[1]) - else: - #otherwise, no reforge - lst[ CharacterData.statMap[key[u'stat']] ] += key[u'amount'] - self.verbose_print('+' + str(key[u'amount']) + ' ' + CharacterData.statMap[key[u'stat']]) - #prevents cached reforges from affecting subsequent items - reforge = ('none', 'none') - #add stats from gems, check if socket colors are matched along the way - if u'socketInfo' in tmpItem['data']: - socketInfo = tmpItem['data'][u'socketInfo'] - socketBonusActivated = True # we'll find out if this is not true as we process each gem - else: - socketInfo = None - socketBonusActivated = False - gemCount = 0 - for gemNumber in range(3): - gemId = 'gem' + str(gemNumber) - if gemId in params.keys(): - gemCount += 1 - tmpGem = get_item_cached(self.region, params[gemId]) - if not socketInfo == None: - sockets = socketInfo[u'sockets'] - if gemNumber < len(sockets): - if not sockets[gemNumber][u'type'] in gemColorToSocketColors[tmpGem['data'][u'gemInfo'][u'type'][u'type']]: - socketBonusActivated = False - self.verbose_print(tmpGem['data'][u'name'] + ' does not match socket of color ' + sockets[gemNumber][u'type'] + ', socket bonus not activated!') - for entry in tmpGem['data'][u'gemInfo'][u'bonus'][u'name'].split(' and '): - tmpLst = entry.split(' ') - if not '%' in tmpLst[0]: - tmpVal = int(tmpLst[0][1:]) - tmpStat = verboseStatMap[' '.join(tmpLst[1:])] - lst[tmpStat] += tmpVal - self.verbose_print(tmpGem['data'][u'name'] + ': +' + str(tmpVal) + ' ' + tmpStat) - else: - self.chaotic_metagem = True - self.verbose_print(tmpGem['data'][u'name'] + ' is a meta gem') - #add stats from socket bonuses - if socketBonusActivated == True and gemCount >= len(socketInfo[u'sockets']): - if u'socketBonus' in socketInfo: - for entry in socketInfo[u'socketBonus'].split(' and '): #similar to gem treatment... is there ever a socket bonus that gives multiple stats? - tmpLst = entry.split(' ') - tmpVal = int(tmpLst[0][1:]) - tmpStat = verboseStatMap[ ' '.join(tmpLst[1:]) ] - lst[ tmpStat ] += tmpVal - self.verbose_print('Socket bonus +' + str(tmpVal) + ' ' + tmpStat) - #add stats from enchants - if u'enchant' in params.keys(): - if not type( CharacterData.enchants[ params[u'enchant'] ] ) == type(''): - for key in CharacterData.enchants[ params[u'enchant'] ]: - lst[ key['stat'] ] += key['value'] - self.verbose_print('Enchant +' + str(key['value']) + ' ' + key['stat']) - else: - self.verbose_print(CharacterData.enchants[params[u'enchant']]) - else: - self.verbose_print('Unenchanted') - except Exception as inst: - #it's okay, we can keep going, just so long as we pretend to handle the exception - print "\n" - print "Error at slot: ", p - print "Error type: ", type(inst) - raise - if self.verbose: - pp.pprint(lst) - return lst - #return [lst['str'], lst['agi'], lst['int'], lst['spirit'], lst['stam'], lst['ap'], lst['crit'], lst['hit'], lst['exp'], lst['haste'], lst['mastery']] - - def has_chaotic_metagem(self): - return self.chaotic_metagem - - def get_current_spec_data(self): - specs_data = self.raw_data['data'][u'talents'] - if u'selected' in specs_data[0]: - current_spec_data = specs_data[0] - else: - current_spec_data = specs_data[1] - - return current_spec_data - - def get_talents(self): - spec_data = self.get_current_spec_data() - talents = [""] * 6 - for t in spec_data[u'talents']: - talents[t[u'tier']] = str(t[u'column'] + 1) - return "".join(talents) - - def get_glyphs(self): - glyphs = [] - spec_data = self.get_current_spec_data() - glyphs_data = spec_data[u'glyphs'] - for g in (glyphs_data[u'major'] + glyphs_data[u'minor']): - glyph_name = g[u'name'] - if glyph_name in CharacterData.glyphs: - glyphs.append(CharacterData.glyphs[glyph_name]) - return glyphs - - def verbose_print(self, str): - if self.verbose: - print str diff --git a/scripts/outlaw.py b/scripts/outlaw.py new file mode 100644 index 0000000..385e752 --- /dev/null +++ b/scripts/outlaw.py @@ -0,0 +1,169 @@ +from __future__ import division +from __future__ import print_function +# Simple test program to debug + play with assassination models. +from builtins import str +from os import path +import sys +from pprint import pprint +sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) + +from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator +from shadowcraft.calcs.rogue.Aldriana import settings + +from shadowcraft.objects import buffs +from shadowcraft.objects import race +from shadowcraft.objects import stats +from shadowcraft.objects import procs +from shadowcraft.objects import talents +from shadowcraft.objects import artifact + +from shadowcraft.core import i18n + +# Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. +test_language = 'local' +i18n.set_language(test_language) + +# Set up level/class/race +test_level = 110 +test_race = race.Race('pandaren', 'rogue', 110) +test_class = 'rogue' +test_spec = 'outlaw' + +# Set up buffs. +test_buffs = buffs.Buffs( + 'short_term_haste_buff', + 'flask_legion_agi', + 'food_legion_versatility_375' +) + +# Set up weapons. mark_of_the_frostwolf mark_of_the_shattered_hand +test_mh = stats.Weapon(4821.0, 2.6, 'sword', None) +test_oh = stats.Weapon(4821.0, 2.6, 'sword', None) + +# Set up procs. +#test_procs = procs.ProcsList(('assurance_of_consequence', 588), +#('draenic_philosophers_stone', 620), 'virmens_bite', 'virmens_bite_prepot', +#'archmages_incandescence') #trinkets, other things (legendary procs) +test_procs = procs.ProcsList( + 'mark_of_the_hidden_satyr', + 'old_war_pot', + 'old_war_prepot', + ('nightblooming_frond', 895), + ('memento_of_angerboda', 885) +) + +# Set up gear buffs. +test_gear_buffs = stats.GearBuffs( + 'gear_specialization', + 'rogue_t19_2pc', + 'rogue_t19_4pc', + 'mantle_of_the_master_assassin', + 'greenskins_waterlogged_wristcuffs' +) + +# Set up a calcs object.. +test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, + agi=round(35872 * 0.95238 - test_race.racial_agi), + stam=28367, + crit=9070, + haste=2476, + mastery=6254, + versatility=5511,) + +# Initialize talents.. +test_talents = talents.Talents('3213122', test_spec, test_class, level=test_level) + +#initialize artifact traits.. +test_traits = artifact.Artifact(test_spec, test_class, trait_dict={ + 'curse_of_the_dreadblades': 1, + 'cursed_edges': 1, + 'fates_thirst': 4, + 'blade_dancer': 3, + 'fatebringer': 4, + 'gunslinger': 3, + 'hidden_blade': 1, + 'fortune_strikes': 3, + 'ghostly_shell': 3, + 'deception': 1, + 'black_powder': 4, + 'greed': 1, + 'blurred_time': 1, + 'fortunes_boon': 3, + 'fortunes_strike': 3, + 'blademaster': 1, + 'blunderbuss': 1, + 'cursed_steel': 1, + 'bravado_of_the_uncrowned': 1, + 'sabermetrics': 0, + 'dreadblades_vigor': 0, + 'loaded_dice': 0, + 'concordance_of_the_legionfall': 0, +}) + +# Set up settings. +test_cycle = settings.OutlawCycle(blade_flurry=False, + jolly_roger_reroll=2, + grand_melee_reroll=2, + shark_reroll=2, + true_bearing_reroll=0, + buried_treasure_reroll=2, + broadsides_reroll=2, + between_the_eyes_policy='never' + ) +test_settings = settings.Settings(test_cycle, response_time=.5, duration=300, + adv_params="", is_demon=True, num_boss_adds=0, + finisher_threshold=5) + +# Build a DPS object. +calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_traits, test_buffs, test_race, test_spec, test_settings, test_level) + +print(str(test_stats.get_character_stats(test_race, test_traits))) + +# Compute DPS Breakdown. +dps_breakdown = calculator.get_dps_breakdown() +total_dps = sum(entry[1] for entry in list(dps_breakdown.items())) + +# Compute EP values. +ep_values = calculator.get_ep(baseline_dps=total_dps) +tier_ep_values = calculator.get_other_ep(['rogue_t16_2pc', 'rogue_t16_4pc', 'mantle_of_the_master_assassin']) +#mh_enchants_and_dps_ep_values, oh_enchants_and_dps_ep_values = +#calculator.get_weapon_ep(dps=True, enchants=True) + +#talent_ranks = calculator.get_talents_ranking() +#trait_ranks = calculator.get_trait_ranking() + +def max_length(dict_list): + max_len = 0 + for i in dict_list: + dict_values = list(i.items()) + if max_len < max(len(entry[0]) for entry in dict_values): + max_len = max(len(entry[0]) for entry in dict_values) + + return max_len + +def pretty_print(dict_list, total_sum=1., show_percent=False): + max_len = max_length(dict_list) + + for i in dict_list: + dict_values = list(i.items()) + dict_values.sort(key=lambda entry: entry[1], reverse=True) + for value in dict_values: + #print value[0] + ':' + ' ' * (max_len - len(value[0])), + #str(value[1]) + if show_percent and ("{0:.2f}".format(value[1] / total_dps)) != '0.00': + print(value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' (' + str("{0:.2f}".format(100 * float(value[1]) / total_sum)) + '%)') + else: + print(value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1])) + print('-' * (max_len + 15)) + +dicts_for_pretty_print = [ep_values, + tier_ep_values, + #talent_ranks, + #trinkets_ep_value, + dps_breakdown, + #trait_ranks +] +pretty_print(dicts_for_pretty_print) +print(' ' * (max_length(dicts_for_pretty_print) + 1), total_dps, ("total damage per second.")) + +#pprint(talent_ranks) diff --git a/scripts/reinstall.bat b/scripts/reinstall.bat deleted file mode 100755 index d04ce2e..0000000 --- a/scripts/reinstall.bat +++ /dev/null @@ -1,12 +0,0 @@ -cd wowapi - -python setup.py build - -python setup.py install - -cd ../../ - -python setup.py build - -python setup.py install -cd scripts \ No newline at end of file diff --git a/scripts/reinstall.sh b/scripts/reinstall.sh deleted file mode 100755 index 1d07470..0000000 --- a/scripts/reinstall.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -cd wowapi -python setup.py build -python setup.py install -cd ../../ -python setup.py build -python setup.py install \ No newline at end of file diff --git a/scripts/reinstall_dm.sh b/scripts/reinstall_dm.sh deleted file mode 100755 index 3ad485e..0000000 --- a/scripts/reinstall_dm.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -cd wowapi -python setup.py build -python setup.py install -cd ../../ -python setup_dm.py build -python setup_dm.py install \ No newline at end of file diff --git a/scripts/subtlety.py b/scripts/subtlety.py index 6c06cb2..8e63e69 100644 --- a/scripts/subtlety.py +++ b/scripts/subtlety.py @@ -1,17 +1,22 @@ +from __future__ import division +from __future__ import print_function # Simple test program to debug + play with subtlety models. +from builtins import str from os import path import sys -#sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) +from pprint import pprint +sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator from shadowcraft.calcs.rogue.Aldriana import settings +from shadowcraft.calcs.rogue.Aldriana import settings_data from shadowcraft.objects import buffs from shadowcraft.objects import race from shadowcraft.objects import stats from shadowcraft.objects import procs from shadowcraft.objects import talents -from shadowcraft.objects import glyphs +from shadowcraft.objects import artifact from shadowcraft.core import i18n @@ -20,78 +25,125 @@ i18n.set_language(test_language) # Set up level/class/race -test_level = 100 -test_race = race.Race('night_elf') +test_level = 110 +test_race = race.Race('blood_elf', level=110) test_class = 'rogue' +test_spec = 'subtlety' # Set up buffs. test_buffs = buffs.Buffs( 'short_term_haste_buff', - 'stat_multiplier_buff', - 'crit_chance_buff', - 'mastery_buff', - 'haste_buff', - 'multistrike_buff', - 'versatility_buff', - 'attack_power_buff', - 'physical_vulnerability_debuff', - 'spell_damage_debuff', - 'flask_wod_agi', - 'food_mop_agi' + 'flask_legion_agi', + 'food_legion_mastery_375', + #'food_legion_feast_150' ) -# Set up weapons. mark_of_the_frostwolf mark_of_the_shattered_hand -test_mh = stats.Weapon(812.0, 1.8, 'dagger', 'mark_of_the_shattered_hand') -test_oh = stats.Weapon(812.0, 1.8, 'dagger', 'mark_of_the_frostwolf') +# Set up weapons. +test_mh = stats.Weapon(5442.0, 1.8, 'dagger', None) +test_oh = stats.Weapon(5442.0, 1.8, 'dagger', None) # Set up procs. - trinkets, other things (legendary procs) -test_procs = procs.ProcsList(('scales_of_doom', 691), ('beating_heart_of_the_mountain', 701), - 'draenic_agi_pot', 'draenic_agi_prepot', 'archmages_greater_incandescence') +test_procs = procs.ProcsList( + 'mark_of_the_hidden_satyr', + ('convergence_of_fates', 890), + ('nightblooming_frond', 905), + #('kiljaedens_burning_wish', 940) + #'old_war_pot', + #'old_war_prepot', + 'prolonged_power_pot', + 'prolonged_power_prepot', +) + +""" +# test all procs +from shadowcraft.objects import proc_data +test_procs = procs.ProcsList() +for key in proc_data.allowed_procs.keys(): + test_procs.set_proc(key) + + +# Debug prints for scaled trinket values +for proc in test_procs.get_all_procs_for_stat(): + if proc.scaling: + print proc.proc_name + " - " + str(proc.item_level) + " - " + str(proc.value) +""" # Set up gear buffs. -test_gear_buffs = stats.GearBuffs('gear_specialization') #tier buffs located here +test_gear_buffs = stats.GearBuffs('gear_specialization', +'the_first_of_the_dead', +'rogue_t20_2pc', +'rogue_t20_4pc', +#'insignia_of_ravenholdt', +'mantle_of_the_master_assassin', +) #tier buffs located here # Set up a calcs object.. test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, - agi=3650, - stam=2426, - crit=1039, - haste=0, - mastery=1315, - readiness=0, - versatility=122, - multistrike=1834,) + agi=round(31794 * 0.95238 - test_race.racial_agi), #gear spec and racial agi are added during calc again + stam=54585, + crit=7010, + haste=4209, + mastery=6481, + versatility=5428,) # Initialize talents.. -test_talents = talents.Talents('2000002', test_class, test_level) - -# Set up glyphs. -glyph_list = [] -test_glyphs = glyphs.Glyphs(test_class, *glyph_list) +test_talents = talents.Talents('1113213', test_spec, test_class, level=test_level) + +#initialize artifact traits.. +test_traits = artifact.Artifact(test_spec, test_class, trait_dict={ + 'goremaws_bite': 1, + 'shadow_fangs': 1, + 'gutripper': 4, + 'fortunes_bite': 4, + 'catlike_reflexes': 4, + 'embrace_of_darkness': 1, + 'ghost_armor': 4, + 'precision_strike': 4, + 'energetic_stabbing': 4+3, + 'flickering_shadows': 1, + 'second_shuriken': 1, + 'demons_kiss': 4, + 'finality': 1, + 'the_quiet_knife': 4, + 'akarris_soul': 1, + 'soul_shadows': 4, + 'shadow_nova': 1, + 'legionblade': 1, + 'shadows_of_the_uncrowned': 1, + 'weak_point': 4, + 'shadows_whisper': 1, + 'feeding_frenzy': 1, + 'concordance_of_the_legionfall': 24, + #crucible + #'master_of_shadows': 3, +}) # Set up settings. -test_cycle = settings.SubtletyCycle(5, use_hemorrhage='never', clip_fw=False) -test_settings = settings.Settings(test_cycle, response_time=.5, duration=360, dmg_poison='dp', utl_poison='lp', is_pvp=False, - adv_params="") +test_cycle = settings.SubtletyCycle() +test_settings = settings.Settings(test_cycle) # Build a DPS object. -calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) +calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_traits, test_buffs, test_race, test_spec, test_settings, test_level) + +print(str(test_stats.get_character_stats(test_race, test_traits))) # Compute DPS Breakdown. dps_breakdown = calculator.get_dps_breakdown() -total_dps = sum(entry[1] for entry in dps_breakdown.items()) +total_dps = sum(entry[1] for entry in list(dps_breakdown.items())) # Compute EP values. ep_values = calculator.get_ep(baseline_dps=total_dps) #ep_values = calculator.get_ep() -#tier_ep_values = calculator.get_other_ep(['rogue_t17_2pc', 'rogue_t17_4pc', 'rogue_t17_4pc_lfr']) +tier_ep_values = calculator.get_other_ep(['rogue_t19_2pc', 'rogue_t19_4pc', 'denial_of_the_half_giants', 'insignia_of_ravenholdt', +'shadow_satyrs_walk', 'convergence_of_fates', 'mantle_of_the_master_assassin']) -talent_ranks = calculator.get_talents_ranking() +#talent_ranks = calculator.get_talents_ranking() +#trait_ranks = calculator.get_trait_ranking() def max_length(dict_list): max_len = 0 for i in dict_list: - dict_values = i.items() + dict_values = list(i.items()) if max_len < max(len(entry[0]) for entry in dict_values): max_len = max(len(entry[0]) for entry in dict_values) @@ -101,21 +153,32 @@ def pretty_print(dict_list): max_len = max_length(dict_list) for i in dict_list: - dict_values = i.items() + dict_values = list(i.items()) dict_values.sort(key=lambda entry: entry[1], reverse=True) for value in dict_values: - if ("{0:.2f}".format(float(value[1])/total_dps)) != '0.00': - print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' ('+str( "{0:.2f}".format(100*float(value[1])/total_dps) )+'%)' + if ("{0:.2f}".format(value[1] / total_dps)) != '0.00': + print(value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' ('+str( "{0:.2f}".format(100*float(value[1])/total_dps) )+'%)') else: - print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) - print '-' * (max_len + 15) + print(value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1])) + print('-' * (max_len + 15)) dicts_for_pretty_print = [ ep_values, - #tier_ep_values, - talent_ranks, + tier_ep_values, #trinkets_ep_value, - dps_breakdown + dps_breakdown, + #trait_ranks ] pretty_print(dicts_for_pretty_print) -print ' ' * (max_length(dicts_for_pretty_print) + 1), total_dps, _("total damage per second.") +print(' ' * (max_length(dicts_for_pretty_print) + 1), total_dps, ("total damage per second.")) + +""" +for value in list(aps.items()): + if type(value[1]) is float: + val = value[1] * 300. + else: + val = sum(value[1]) * 300. + print(str(value[0]) + ' - ' + str(val)) +""" + +#pprint(trait_ranks) diff --git a/scripts/subtlety_import.py b/scripts/subtlety_import.py deleted file mode 100755 index d12f230..0000000 --- a/scripts/subtlety_import.py +++ /dev/null @@ -1,143 +0,0 @@ -# Simple test program to debug + play with subtlety models. -from os import path -import sys -from import_character import CharacterData -from char_info import charInfo -#sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) - -from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator -from shadowcraft.calcs.rogue.Aldriana import settings - -from shadowcraft.objects import buffs -from shadowcraft.objects import race -from shadowcraft.objects import stats -from shadowcraft.objects import procs -from shadowcraft.objects import proc_data -from shadowcraft.objects import talents -from shadowcraft.objects import glyphs - -from shadowcraft.core import i18n - -# Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. -test_language = 'local' -i18n.set_language(test_language) - -key = 1 -while key < len(sys.argv): - terms = sys.argv[key].split(':') - charInfo[ terms[0] ] = terms[1] - key += 1 - -print "Loading " + charInfo['name'] + " of " + charInfo['region'] + "-" + charInfo['realm'] + "\n" -character_data = CharacterData(charInfo['region'], charInfo['realm'], charInfo['name'], verbose=charInfo['verbose']) -character_data.do_import() - - -# Set up level/class/race -test_level = 90 -test_race = race.Race(character_data.get_race()) -test_class = 'rogue' - -# Set up buffs. -test_buffs = buffs.Buffs( - 'short_term_haste_buff', - 'stat_multiplier_buff', - 'crit_chance_buff', - 'mastery_buff', - 'melee_haste_buff', - 'attack_power_buff', - 'armor_debuff', - 'physical_vulnerability_debuff', - 'spell_damage_debuff', - 'agi_flask_mop', - 'food_300_agi' - ) - -# Set up weapons. - -test_mh = stats.Weapon(*character_data.get_mh()) -test_oh = stats.Weapon(*character_data.get_oh()) - -# Set up procs. -character_procs = character_data.get_procs() -character_procs_allowed = filter(lambda p: p in proc_data.allowed_procs, character_procs) - -#not_allowed_procs = set(character_procs) - set(character_procs_allowed) -#print not_allowed_procs - -test_procs = procs.ProcsList(*character_procs_allowed) - -# Set up a calcs object.. -lst = character_data.get_gear_stats() - -# Set up gear buffs. -character_gear_buffs = character_data.get_gear_buffs() + ['leather_specialization', 'virmens_bite', 'virmens_bite_prepot'] -if character_data.has_chaotic_metagem(): - character_gear_buffs.append('chaotic_metagem') -test_gear_buffs = stats.GearBuffs(*character_gear_buffs) - -test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, **lst) - -# Initialize talents.. -if charInfo['talents'] == None: - charInfo['talents'] = character_data.get_talents() -test_talents = talents.Talents(charInfo['talents'], test_class, test_level) - -# Set up glyphs. -glyph_list = character_data.get_glyphs() -test_glyphs = glyphs.Glyphs(test_class, *glyph_list) - -# Set up settings. -raid_crits_per_second = 5 -hemo_interval = 24 #'always', 'never', 24, 25, 26... -if not character_data.get_mh_type() == 'dagger' and not test_talents.shuriken_toss: - if not hemo_interval == 'always': - print "\nALERT: Viable dagger cycle not found, forced rotation to strictly Hemo \n" - hemo_interval = 'always' -test_cycle = settings.SubtletyCycle(raid_crits_per_second, use_hemorrhage=hemo_interval) -test_settings = settings.Settings(test_cycle, response_time=.5, duration=360, dmg_poison='dp', utl_poison='lp', is_pvp=charInfo['pvp'], - stormlash=charInfo['stormlash'], shiv_interval=charInfo['shiv']) - -# Build a DPS object. -calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) - -# Compute EP values. -ep_values = calculator.get_ep() - -# Compute DPS Breakdown. -dps_breakdown = calculator.get_dps_breakdown() -total_dps = sum(entry[1] for entry in dps_breakdown.items()) -talent_ranks = calculator.get_talents_ranking() -heal_sum, heal_table = calculator.get_self_healing(dps_breakdown=dps_breakdown) - - -def max_length(dict_list): - max_len = 0 - for i in dict_list: - dict_values = i.items() - if max_len < max(len(entry[0]) for entry in dict_values): - max_len = max(len(entry[0]) for entry in dict_values) - - return max_len - -def pretty_print(dict_list): - max_len = max_length(dict_list) - - for i in dict_list: - dict_values = i.items() - dict_values.sort(key=lambda entry: entry[1], reverse=True) - for value in dict_values: - if ("{0:.2f}".format(float(value[1])/total_dps)) != '0.00': - print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' ('+str( "{0:.2f}".format(100*float(value[1])/total_dps) )+'%)' - else: - print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) - print '-' * (max_len + 15) - -dicts_for_pretty_print = [ - ep_values, - talent_ranks, - heal_table, - dps_breakdown -] -pretty_print(dicts_for_pretty_print) -print ' ' * (max_length(dicts_for_pretty_print) + 1), total_dps, _("total damage per second.") diff --git a/scripts/wowapi/LICENSE b/scripts/wowapi/LICENSE deleted file mode 100755 index ea2235b..0000000 --- a/scripts/wowapi/LICENSE +++ /dev/null @@ -1,17 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2011 Thorsten Sanders - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software -and associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/scripts/wowapi/README.rst b/scripts/wowapi/README.rst deleted file mode 100755 index 9786f81..0000000 --- a/scripts/wowapi/README.rst +++ /dev/null @@ -1,14 +0,0 @@ -About -====== -I am using a python framework for my website and due the current python modules for the WoW Api are not updated very often, -still missing features and I prefer to get raw data, I wrote my own little module. - -| It supports: gzip compression, If-Modified-Since header, authorization, SSL - - - -Documentation -============= - -Full documentation can be found at: -http://wowapi.wowuse.com/ \ No newline at end of file diff --git a/scripts/wowapi/setup.py b/scripts/wowapi/setup.py deleted file mode 100755 index 934e579..0000000 --- a/scripts/wowapi/setup.py +++ /dev/null @@ -1,11 +0,0 @@ -from distutils.core import setup - -setup(name='wowapi', - version='0.3.0', - description='Python module to access the WoW Api', - author='Dorwido', - author_email='darkz@gmx.de', - url='https://github.com/Dorwido/wowapi', - license='MIT', - packages=['wowapi'] -) \ No newline at end of file diff --git a/scripts/wowapi/tests/__init__.py b/scripts/wowapi/tests/__init__.py deleted file mode 100755 index 3d8b4e2..0000000 --- a/scripts/wowapi/tests/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/scripts/wowapi/tests/test_data.py b/scripts/wowapi/tests/test_data.py deleted file mode 100755 index 7c38d3e..0000000 --- a/scripts/wowapi/tests/test_data.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- -from wowapi.api import WoWApi - -try: - import unittest2 as unittest -except ImportError: - import unittest as unittest - -wowapi = WoWApi() -class Test_GetData(unittest.TestCase): - - def test_get_character(self): - character = wowapi.get_character('eu','Doomhammer','Thetotemlord') - self.assertEqual(character['data']['name'],'Thetotemlord') - - - def test_get_item(self): - item = wowapi.get_item('eu',25) - self.assertEqual(item['data']['name'],'Worn Shortsword') - - def test_get_guild(self): - guild = wowapi.get_guild('eu','Doomhammer','Dawn Of Osiris') - self.assertEqual(guild['data']['name'],'Dawn Of Osiris') - - def test_get_realm(self): - realm = wowapi.get_realm('eu') - self.assertGreater(len(realm['data']['realms']),1) - - - def test_get_auctions(self): - auctions = wowapi.get_auctions('eu','Defias Brotherhood') - self.assertEqual(len(auctions['data']),4) - - def test_get_arena_ladder_team(self): - arena_ladder = wowapi.get_arena_ladder('eu','Blackout','2v2',1) - self.assertEqual(len(arena_ladder['data']['arenateam']),1) - arena_team = wowapi.get_arena_team('eu',arena_ladder['data']['arenateam'][0]['realm'],'2v2',arena_ladder['data']['arenateam'][0]['name']) - self.assertEqual(arena_team['data']['name'],arena_ladder['data']['arenateam'][0]['name']) - - - def test_get_character_races (self): - character_races = wowapi.get_character_races('eu') - self.assertGreater(len(character_races['data']['races']),1) - - def test_get_character_classes (self): - character_classes = wowapi.get_character_classes('eu') - self.assertGreater(len(character_classes['data']['classes']),1) - - def test_get_guild_rewards (self): - guild_rewards = wowapi.get_guild_rewards('eu') - self.assertGreater(len(guild_rewards['data']['rewards']),1) - - def test_get_guild_perks (self): - guild_perks = wowapi.get_guild_perks('eu') - self.assertGreater(len(guild_perks['data']['perks']),1) - - def test_get_item_classes (self): - item_classes = wowapi.get_item_classes('eu') - self.assertGreater(len(item_classes['data']['classes']),1) - - def test_get_quest(self): - quest_info = wowapi.get_quest('eu',25) - self.assertEqual(quest_info['data']['id'],25) - - def test_get_recipe (self): - recipe_info = wowapi.get_recipe('us',33994) - self.assertEqual(recipe_info['data']['id'],33994) - - def test_get_achievements_character(self): - char_achievements = wowapi.get_achievements_character('eu') - self.assertGreater(len(char_achievements['data']['achievements']),1) - - def test_get_achievements_guild(self): - guild_achievements = wowapi.get_achievements_guild('eu') - self.assertGreater(len(guild_achievements['data']['achievements']),1) diff --git a/scripts/wowapi/tests/test_locales.py b/scripts/wowapi/tests/test_locales.py deleted file mode 100755 index e7a5797..0000000 --- a/scripts/wowapi/tests/test_locales.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -from wowapi.api import WoWApi - -try: - import unittest2 as unittest -except ImportError: - import unittest as unittest - -wowapi = WoWApi() -class Test_Locales(unittest.TestCase): - - def test_locale_en_US(self): - item = wowapi.get_item('us',25,None,'en_US') - self.assertEqual(item['data']['name'],'Worn Shortsword') - - def test_locale_es_MX(self): - item = wowapi.get_item('us',25,None,'es_MX') - self.assertEqual(item['data']['name'],'Espada corta desgastada') - - def test_locale_en_GB(self): - item = wowapi.get_item('eu',25,None,'en_GB') - self.assertEqual(item['data']['name'],'Worn Shortsword') - - def test_locale_es_ES(self): - item = wowapi.get_item('eu',25,None,'es_ES') - self.assertEqual(item['data']['name'],'Espada corta desgastada') - - def test_locale_fr_FR(self): - item = wowapi.get_item('eu',25,None,'fr_FR') - self.assertEqual(item['data']['name'],u'Epée courte usée') - - def test_locale_ru_RU(self): - item = wowapi.get_item('eu',25,None,'ru_RU') - self.assertEqual(item['data']['name'],u'Иссеченный короткий меч') - - def test_locale_de_DE(self): - item = wowapi.get_item('eu',25,None,'de_DE') - self.assertEqual(item['data']['name'],'Abgenutztes Kurzschwert') - - def test_locale_ko_KR(self): - item = wowapi.get_item('kr',25,None,'ko_KR') - self.assertEqual(item['data']['name'],u'낡은 쇼트소드') - - def test_locale_zh_TW(self): - item = wowapi.get_item('tw',25,None,'zh_TW') - self.assertEqual(item['data']['name'],u'破損的短劍') - - def test_locale_zh_CN(self): - item = wowapi.get_item('cn',25,None,'zh_CN') - self.assertEqual(item['data']['name'],u'破损的短剑') - diff --git a/scripts/wowapi/tests/test_regions.py b/scripts/wowapi/tests/test_regions.py deleted file mode 100755 index a8d6798..0000000 --- a/scripts/wowapi/tests/test_regions.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -from wowapi.api import WoWApi - -try: - import unittest2 as unittest -except ImportError: - import unittest as unittest - -wowapi = WoWApi() -class Test_Regions(unittest.TestCase): - def test_region_us(self): - realm = wowapi.get_realm('us') - self.assertGreater(len(realm['data']['realms']),1) - - def test_region_eu(self): - realm = wowapi.get_realm('eu') - self.assertGreater(len(realm['data']['realms']),1) - - def test_region_kr(self): - realm = wowapi.get_realm('kr') - self.assertGreater(len(realm['data']['realms']),1) - - def test_region_tw(self): - realm = wowapi.get_realm('tw') - self.assertGreater(len(realm['data']['realms']),1) - - def test_region_cn(self): - realm = wowapi.get_realm('cn') - self.assertGreater(len(realm['data']['realms']),1) \ No newline at end of file diff --git a/scripts/wowapi/wowapi/__init__.py b/scripts/wowapi/wowapi/__init__.py deleted file mode 100755 index e69de29..0000000 diff --git a/scripts/wowapi/wowapi/api.py b/scripts/wowapi/wowapi/api.py deleted file mode 100755 index 43e3bd7..0000000 --- a/scripts/wowapi/wowapi/api.py +++ /dev/null @@ -1,399 +0,0 @@ -from urllib2 import Request, urlopen, URLError,quote -import gzip -import StringIO -try: - import simplejson as json -except ImportError: - import json -import datetime -import base64 -import hmac -import hashlib -from .exceptions import APIError,NotModified,NotFound -from .utilities import parse_http_datetime,http_datetime - -regions = { - 'us' : { - 'domain':'us.battle.net', - 'locales' : [ - 'en_US', - 'es_MX' - ] - }, - 'eu' : { - 'domain':'eu.battle.net', - 'locales' : [ - 'en_GB', - 'es_ES', - 'fr_FR', - 'ru_RU', - 'de_DE' - ] - }, - 'kr' : { - 'domain':'kr.battle.net', - 'locales' : [ - 'ko_KR' - ] - }, - 'tw' : { - 'domain':'tw.battle.net', - 'locales' : [ - 'zh_TW' - ] - }, - 'cn' : { - 'domain':'www.battlenet.com.cn', - 'locales' : [ - 'zh_CN' - ] - }, -} - -datatypes = { - 'character' : { - 'path':'character/%s/%s', - 'param':'fields' - }, - 'guild' : { - 'path':'guild/%s/%s', - 'param':'fields' - }, - 'realm' : { - 'path': 'realm/status', - 'param':'realms' - }, - 'auction' : { - 'path' : 'auction/data/%s' - }, - 'item' : { - 'path' : 'item/%d' - }, - 'arena_team' : { - 'path' : 'arena/%s/%s/%s' - }, - 'arena_ladder' : { - 'path' : 'pvp/arena/%s/%s', - 'param' : 'size' - }, - 'character_races':{ - 'path' : 'data/character/races' - }, - 'character_classes':{ - 'path' : 'data/character/classes' - }, - 'guild_rewards':{ - 'path' : 'data/guild/rewards' - }, - 'guild_perks':{ - 'path':'data/guild/perks' - }, - 'item_classes':{ - 'path':'data/item/classes' - }, - 'achievements_character':{ - 'path':'data/character/achievements' - }, - 'achievements_guild':{ - 'path':'data/guild/achievements' - }, - 'quest':{ - 'path':'quest/%d' - }, - 'recipe':{ - 'path':'recipe/%d' - } -} - -class WoWApi(): - - - def __init__(self,privatekey=None,publickey=None,ssl=None): - self.privkey = privatekey - self.pubkey = publickey - if ssl is None: - if self.privkey and self.pubkey: - self.ssl = True - else: - self.ssl = False - else: - self.ssl = ssl - - - - - - def _decode_response(self,response): - - if 'content-encoding' in response.info() and response.info()['content-encoding'] == 'gzip': - response = gzip.GzipFile(fileobj=StringIO.StringIO(response.read())) - try: - data = json.loads(unicode(response.read(),'UTF-8')) - except json.JSONDecodeError: - raise APIError('Non-JSON Response') - return data - - - def _do_request(self,request): - try: - response = urlopen(request) - except URLError, e: - if hasattr(e, 'reason'): - raise APIError(e.reason,request.get_full_url()) - elif hasattr(e, 'code'): - if e.code == 304: - raise NotModified(request.get_full_url()) - elif e.code == 404: - raise NotFound(request.get_full_url()) - else: - error_response = self._decode_response(e) - if error_response['reason']: - raise APIError(e.code,error_response['reason'],request.get_full_url()) - else: - raise APIError(e.code,None,request.get_full_url()) - else: - return response - - def _sign_request(self,path,date): - stringtosign = "GET\n"+date+"\n"+path+"\n" - hash = hmac.new(self.privkey, stringtosign, hashlib.sha1).digest() - return base64.encodestring(hash) - - def _get_data(self,region,data,params=None,lastmodified=None,lang=None,datatype=None): - if region not in regions: - raise ValueError('Region not found') - if lang and lang not in regions[region]['locales']: - raise ValueError('Locales not valid for current region') - httpdate = http_datetime(datetime.datetime.utcnow()) - signature = None - if self.privkey and self.pubkey: - signature = self._sign_request('/api/wow/'+data,httpdate) - - if params: - data += '?'+datatypes[datatype]['param']+'='+','.join(map(str, params)) - - if lang and params: - data+='&locale='+lang - elif lang: - data+='?locale='+lang - if self.ssl: - url = 'https://' - else: - url = 'http://' - - url += regions[region]['domain']+'/api/wow/'+data - header = { - 'Accept-Encoding': 'gzip', - 'Date' : httpdate - } - if signature: - header['Authorization'] = 'BNET '+self.pubkey+':'+signature - - - if lastmodified: - header['If-Modified-Since'] = http_datetime(lastmodified) - - request = Request(url, None, header) - - response = self._do_request(request) - - rlastmodified = None - if 'Last-Modified' in response.info(): - rlastmodified = parse_http_datetime(response.info()['Last-Modified']) - - return {'lastmodified':(rlastmodified),'data':self._decode_response(response)} - - def get_item(self,region,itemid,lastmodified=None,lang=None): - """ - Get infos about an item - - | ``Example:`` - :: - - get_item('eu',25) - """ - if not int(itemid): - raise ValueError('Itemid must be a integer') - return self._get_data(region,datatypes['item']['path'] % (itemid),None,lastmodified,lang) - - - def get_character(self,region,realm,character,params=None,lastmodified=None,lang=None): - """ - Get infos about an character, params is a array taking optional fields to look up infos like achievements,talents etc - - | ``Example:`` - :: - - get_character('eu','Doomhammer','Thetotemlord',['talents']) - """ - return self._get_data(region,datatypes['character']['path'] % (quote(realm),quote(character)),params,lastmodified,lang,'character') - - def get_guild(self,region,realm,guild,params=None,lastmodified=None,lang=None): - """ - Get infos about an guild, params is a array taking optional fields to look up infos like achievements,members etc - - | ``Example:`` - :: - - get_guild('eu','Doomhammer','Dawn Of Osiris') - """ - return self._get_data(region,datatypes['guild']['path'] % (quote(realm),quote(guild)),params,lastmodified,lang,'guild') - - def get_realm(self,region,params=None,lastmodified=None,lang=None): - """ - Get infos about realm(s), params is a array taking optional which realms to look up otherwise returning all realms of an region - - | ``Example:`` - :: - - get_realm('eu',['Doomhammer']) - """ - return self._get_data(region,datatypes['realm']['path'],params,lastmodified,lang,'realm') - - def get_auctions(self,region,realm,lastmodified=None,lang=None): - """ - Returns all auctions of a realms - - | ``Example:`` - :: - - get_auction('eu','Doomhammer') - """ - data = self._get_data(region,datatypes['auction']['path'] % (quote(realm)),None,lastmodified,lang) - request = Request(data['data']['files'][0]['url'], None, {'Accept-Encoding': 'gzip'}) - - return {'lastmodified': data['lastmodified'],'data':self._decode_response(self._do_request(request))} - - def get_arena_team(self,region,realm,teamsize,teamname,lastmodified=None,lang=None): - """ - Get infos about a arena team - - | ``Example:`` - :: - - get_arenea_team('eu','Doomhammer','2v2','We win') - """ - return self._get_data(region,datatypes['arena_team']['path'] % (quote(realm),teamsize,quote(teamname)),None,lastmodified,lang) - - def get_arena_ladder(self,region,battlegroup,teamsize,howmany=None,lastmodified=None,lang=None): - """ - Get the arena ladder of the specified battlegroup, optional with howmany you can define how many teams should be included - - | ``Example:`` - :: - - get_arena_ladder('eu','Blackout','5v5',100) - """ - return self._get_data(region,datatypes['arena_ladder']['path'] % (quote(battlegroup),teamsize),[howmany],lastmodified,lang,'arena_ladder') - - def get_character_races(self,region,lastmodified=None,lang=None): - """ - Get infos about all character races - - | ``Example:`` - :: - - get_character_races('us',None,'es_MX') - """ - return self._get_data(region,datatypes['character_races']['path'],None,lastmodified,lang) - - def get_character_classes(self,region,lastmodified=None,lang=None): - """ - Get infos about all character classes - - | ``Example:`` - :: - - get_character_class('eu') - """ - return self._get_data(region,datatypes['character_classes']['path'],None,lastmodified,lang) - - def get_guild_rewards(self,region,lastmodified=None,lang=None): - """ - Get infos about all guild rewards - - | ``Example:`` - :: - - get_guild_rewards('cn') - """ - return self._get_data(region,datatypes['guild_rewards']['path'],None,lastmodified,lang) - - def get_guild_perks(self,region,lastmodified=None,lang=None): - """ - Get infos about all guild perks - - | ``Example:`` - :: - - get_guild_perks('tw') - """ - return self._get_data(region,datatypes['guild_perks']['path'],None,lastmodified,lang) - - def get_item_classes(self,region,lastmodified=None,lang=None): - """ - Get infos about all item classes - - | ``Example:`` - :: - - get_item_classes('eu',None,'fr_FR') - """ - return self._get_data(region,datatypes['item_classes']['path'],None,lastmodified,lang) - - - def get_quest(self,region,questid,lastmodified=None,lang=None): - """ - .. versionadded:: 0.2.3 - - Get infos about an quest - - | ``Example:`` - :: - - get_quest('eu',25) - """ - if not int(questid): - raise ValueError('Quest id must be a integer') - return self._get_data(region,datatypes['quest']['path'] % (questid),None,lastmodified,lang) - - def get_recipe(self,region,recipeid,lastmodified=None,lang=None): - """ - .. versionadded:: 0.3.0 - - Get infos about an recipe - - | ``Example:`` - :: - - get_recipe('eu',33994) - """ - if not int(recipeid): - raise ValueError('Recipe id must be a integer') - return self._get_data(region,datatypes['recipe']['path'] % (recipeid),None,lastmodified,lang) - - def get_achievements_character(self,region,lastmodified=None,lang=None): - """ - .. versionadded:: 0.2.3 - - Get all character achievements which exists with name,description etc - - | ``Example:`` - :: - - get_achievements_character('eu',None,'en_GB') - """ - return self._get_data(region,datatypes['achievements_character']['path'],None,lastmodified,lang) - - def get_achievements_guild(self,region,lastmodified=None,lang=None): - """ - .. versionadded:: 0.2.3 - - Get all guild achievements which exists with name,description etc - - | ``Example:`` - :: - - get_achievements_guild('eu',None,'fr_FR') - """ - return self._get_data(region,datatypes['achievements_guild']['path'],None,lastmodified,lang) \ No newline at end of file diff --git a/scripts/wowapi/wowapi/exceptions.py b/scripts/wowapi/wowapi/exceptions.py deleted file mode 100755 index 8f20170..0000000 --- a/scripts/wowapi/wowapi/exceptions.py +++ /dev/null @@ -1,21 +0,0 @@ -class APIError(Exception): - """ - .. versionadded:: 0.2.5 - - - This is raised on all other http errors and will always return http error code, reason for fail - (if a reason is given otherwise None), url which failed - """ - pass - -class NotModified(APIError): - """ - This is raised when using the last modified option and nothing changed, since last request - """ - pass - -class NotFound(APIError): - """ - This is raised on 404 Errors - """ - pass \ No newline at end of file diff --git a/scripts/wowapi/wowapi/utilities.py b/scripts/wowapi/wowapi/utilities.py deleted file mode 100755 index c0412d7..0000000 --- a/scripts/wowapi/wowapi/utilities.py +++ /dev/null @@ -1,58 +0,0 @@ -def http_datetime( dt=None ): - - if not dt: - import datetime - dt = datetime.datetime.utcnow() - else: - try: - dt = dt - dt.utcoffset() - except: - pass # no timezone offset, just assume already in UTC - - s = dt.strftime('%a, %d %b %Y %H:%M:%S GMT') - return s - - -def parse_http_datetime( datestring, utc_tzinfo=None, strict=False ): - - import re, datetime - m = re.match(r'(?P[a-z]+), (?P\d+) (?P[a-z]+) (?P\d+) (?P\d+):(?P\d+):(?P\d+(\.\d+)?) (?P\w+)$', - datestring, re.IGNORECASE) - if not m and not strict: - m = re.match(r'(?P[a-z]+) (?P[a-z]+) (?P\d+) (?P\d+):(?P\d+):(?P\d+) (?P\d+)$', - datestring, re.IGNORECASE) - if not m: - m = re.match(r'(?P[a-z]+), (?P\d+)-(?P[a-z]+)-(?P\d+) (?P\d+):(?P\d+):(?P\d+(\.\d+)?) (?P\w+)$', - datestring, re.IGNORECASE) - if not m: - raise ValueError('HTTP date is not correctly formatted') - - try: - tz = m.group('TZ').upper() - except: - tz = 'GMT' - if tz not in ('GMT','UTC','0000','00:00'): - raise ValueError('HTTP date is not in GMT timezone') - - monname = m.group('MON').upper() - mdict = {'JAN':1, 'FEB':2, 'MAR':3, 'APR':4, 'MAY':5, 'JUN':6, - 'JUL':7, 'AUG':8, 'SEP':9, 'OCT':10, 'NOV':11, 'DEC':12} - month = mdict.get(monname) - if not month: - raise ValueError('HTTP date has an unrecognizable month') - y = int(m.group('Y')) - if y < 100: - century = datetime.datetime.utcnow().year / 100 - if y < 50: - y = century * 100 + y - else: - y = (century - 1) * 100 + y - d = int(m.group('D')) - hour = int(m.group('H')) - minute = int(m.group('M')) - try: - second = int(m.group('S')) - except: - second = float(m.group('S')) - dt = datetime.datetime( y, month, d, hour, minute, second, tzinfo=utc_tzinfo ) - return dt \ No newline at end of file diff --git a/setup.py b/setup.py index 1868c51..291cd8b 100644 --- a/setup.py +++ b/setup.py @@ -2,12 +2,16 @@ setup( name='ShadowCraft-Engine', - url='http://github.com/Aldriana/ShadowCraft-Engine/', - version='0.1', - packages=['shadowcraft', - 'shadowcraft.calcs', 'shadowcraft.calcs.rogue', 'shadowcraft.calcs.rogue.Aldriana', + url='http://github.com/ShadowCraft/ShadowCraft-Engine/', + version='7.3.0', + packages=[ + 'shadowcraft', + 'shadowcraft.calcs', + 'shadowcraft.calcs.rogue', + 'shadowcraft.calcs.rogue.Aldriana', 'shadowcraft.core', - 'shadowcraft.objects'], + 'shadowcraft.objects' + ], license='LGPL', - long_description=open('README').read(), + long_description=open('README.md').read(), ) diff --git a/setup_dm.py b/setup_dm.py deleted file mode 100644 index 7527b0a..0000000 --- a/setup_dm.py +++ /dev/null @@ -1,14 +0,0 @@ -from distutils.core import setup - -setup( - name='ShadowCraft-Engine', - url='http://github.com/dazer/ShadowCraft-Engine/', - version='0.1', - packages=['shadowcraft', - 'shadowcraft.calcs', 'shadowcraft.calcs.rogue', 'shadowcraft.calcs.rogue.Aldriana', - 'shadowcraft.calcs.darkmantle', 'shadowcraft.calcs.darkmantle.rogue', - 'shadowcraft.core', - 'shadowcraft.objects'], - license='LGPL', - long_description=open('README').read(), -) diff --git a/shadowcraft/__init__.py b/shadowcraft/__init__.py index 4974700..d133f1c 100644 --- a/shadowcraft/__init__.py +++ b/shadowcraft/__init__.py @@ -1,4 +1,6 @@ +from future import standard_library +standard_library.install_aliases() import gettext -import __builtin__ +import builtins -__builtin__._ = gettext.gettext +_ = gettext.gettext diff --git a/shadowcraft/calcs/__init__.py b/shadowcraft/calcs/__init__.py index c8ba8ad..c078ea2 100755 --- a/shadowcraft/calcs/__init__.py +++ b/shadowcraft/calcs/__init__.py @@ -1,13 +1,21 @@ +from __future__ import division +from future import standard_library +standard_library.install_aliases() +from builtins import zip +from builtins import str +from builtins import object import gettext -import __builtin__ +import builtins import math +import os +import subprocess -__builtin__._ = gettext.gettext +_ = 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 artifact from shadowcraft.objects import procs from shadowcraft.objects.procs import InvalidProcException @@ -20,46 +28,46 @@ class DamageCalculator(object): # calcs..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.1.0' # should reflect the game patch being targetted - self.SHADOWCRAFT_BUILD = '1.0' # <1 for beta builds, 1.00 is GM, >1 for any bug fixes, reset for each warcraft patch + def __init__(self, stats, talents, traits, buffs, race, spec, settings=None, level=110, target_level=None, char_class='rogue'): + self.WOW_BUILD_TARGET = '7.3.0' # should reflect the game patch being targetted + self.SHADOWCRAFT_BUILD = self.get_version_string() self.tools = class_data.Util() self.stats = stats self.talents = talents - self.glyphs = glyphs + self.traits = traits self.buffs = buffs self.race = race self.char_class = char_class + self.spec = spec self.settings = settings - self.target_level = [target_level, level + 3][target_level is None] #assumes 3 levels higher if not explicit + self.target_level = target_level if target_level else level+3 #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') - + 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.dw_miss_penalty = .19 self._set_constants_for_class() self.level = level - + self.recalculate_hit_constants() self.base_block_chance = .03 + .015 * self.level_difference + self.pantheon_empowerment_uptime = self.get_pantheon_empowerment_uptime(self.settings.pantheon_trinket_users) + def __setattr__(self, name, value): object.__setattr__(self, name, value) if name == 'level': @@ -77,25 +85,34 @@ def _set_constants_for_level(self): 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) + self.attacker_k_value = self.tools.get_k_value(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 + self.target_base_armor = self.tools.get_base_armor(self.target_level) + + #Crit suppression removed in Legion + #Source: http://blue.mmo-champion.com/topic/409203-theorycrafting-questions/#post274 + self.crit_reduction = 0 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 get_version_string(self): + try: + thisdir = os.path.dirname(os.path.abspath(__file__)) + build = subprocess.check_output(['git', 'rev-list', '--count', 'HEAD'], cwd=thisdir).strip() + commit = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'], cwd=thisdir).strip() + if build.isdigit() and commit: + return '{0} ({1})'.format(build, commit) + except: + pass + return 'UNKNOWN' + 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 ) @@ -104,32 +121,32 @@ def get_adv_param(self, type, default_val, min_bound=-10000, max_bound=10000, ig 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 + haste *= self.stats.get_haste_multiplier_from_rating(self.base_stats['haste'] * self.stat_multipliers['haste']) * self.true_haste_mod if proc.att_spd_scales: haste *= 1.4 #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)) + proc.uptime = 1.1307 * (1 - math.e ** (-1 * haste * proc.get_rppm_proc_rate(spec=self.spec) * proc.duration / 60)) else: - lambd = haste * proc.get_rppm_proc_rate() * proc.duration / 60 + lambd = haste * proc.get_rppm_proc_rate(spec=self.spec) * 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) + mean_proc_time = 60 / (haste * proc.get_rppm_proc_rate(spec=self.spec)) + 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) @@ -137,7 +154,7 @@ def set_uptime(self, proc, attacks_per_second, crit_rates): 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) + 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) @@ -151,7 +168,7 @@ def set_uptime(self, proc, attacks_per_second, crit_rates): 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 @@ -161,11 +178,11 @@ def average_damage_breakdowns(self, aps_dict, denom=180): 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) + 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) + 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() @@ -177,10 +194,10 @@ def get_ep(self, ep_stats=None, normalize_ep_stat=None, baseline_dps=None): normalize_ep_stat = self.get_adv_param('normalize_stat', self.settings.default_ep_stat, ignore_bounds=True) 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: @@ -188,7 +205,7 @@ def get_ep(self, ep_stats=None, normalize_ep_stat=None, baseline_dps=None): 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 @@ -395,14 +412,14 @@ def get_other_ep(self, list, normalize_ep_stat=None): 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 = {} @@ -459,20 +476,23 @@ def get_upgrades_ep(self, _list, normalize_ep_stat=None): # 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): + def get_upgrades_ep_fast(self, _list, normalize_ep_stat=None, exclude_list=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 = [] + + active_procs_cache = [] #procs removed by ranker, all procs if no exclude_list provided + procs_list = [] #holds all procs to consider 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): + #if an excludelist is provided only add values on exclude_list to active_proc_cache + #if no exclude_list add all procs to active_proc_cache + if (exclude_list and i in exclude_list) or (not exclude_list and getattr(self.stats.procs, i)): active_procs_cache.append((i, getattr(self.stats.procs, i).item_level)) delattr(self.stats.procs, i) else: @@ -503,20 +523,14 @@ def get_upgrades_ep_fast(self, _list, normalize_ep_stat=None): 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) + 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) + upgraded_scale_factor = self.tools.get_random_prop_point(l) + ep *= upgraded_scale_factor / scale_factor ep_values[proc_name][l] = ep if old_proc: self.stats.procs.set_proc(proc_name) @@ -533,49 +547,59 @@ def get_upgrades_ep_fast(self, _list, normalize_ep_stat=None): 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 + def get_talents_ranking(self, list=None): + talents_ranking = {} + existing_talents = self.talents.get_talent_string() - 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)) + tier_levels = [15, 30, 45, 60, 75, 90, 100] #list of levels for our tiers, because it is not in the talent data + allowed_talent_list = self.talents.get_allowed_talents_for_level() if list == None else list #cache - return glyphs_ranking + for tier, level in zip(self.talents.class_talents, tier_levels): + tier_ranking = {} #reinitialized to clear dict for each new tier + self.talents.initialize_talents(existing_talents) + self.talents.set_talent(tier[0], False) # Wipes the row + baseline_dps = self.get_dps() + for talent in tier: + if talent in allowed_talent_list: + try: + self.talents.set_talent(talent, True) + new_dps = self.get_dps() + if new_dps != baseline_dps: + tier_ranking[talent] = abs(new_dps - baseline_dps) + else: + tier_ranking[talent] = 'not implemented' #unique error: no dps delta for this talent + except: + tier_ranking[talent] = 'implementation error' #unique error: error attempting to calc dps with this talent + talents_ranking[level] = tier_ranking #place each tier into the talent tree + self.talents.initialize_talents(existing_talents) + return talents_ranking - def get_talents_ranking(self, list=None): - talents_ranking = {} + def get_trait_ranking(self, list=None): + trait_ranking = {} baseline_dps = self.get_dps() - talent_list = [] + trait_list = [] - if list is None: - talent_list = self.talents.get_allowed_talents_for_level() + if not list: + trait_list = self.traits.get_trait_list() else: - talent_list = list + trait_list = list + + single_rank = self.traits.get_single_rank_trait_list() - for talent in talent_list: - setattr(self.talents, talent, not getattr(self.talents, talent)) + for trait in trait_list: + base_trait_rank = getattr(self.traits, trait) + if trait in single_rank and base_trait_rank: + setattr(self.traits, trait, 0) + else: + setattr(self.traits, trait, base_trait_rank+1) try: new_dps = self.get_dps() if new_dps != baseline_dps: - talents_ranking[talent] = abs(new_dps - baseline_dps) + trait_ranking[trait] = abs(new_dps-baseline_dps) except: - talents_ranking[talent] = _('not implemented') - setattr(self.talents, talent, not getattr(self.talents, talent)) - - return talents_ranking + trait_ranking[trait] = _('not_implemented') + setattr(self.traits, trait, base_trait_rank) + return trait_ranking def get_engine_info(self): data = { @@ -589,93 +613,72 @@ def get_dps(self): # 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_mitigation_multiplier(self, armor=None): + if not armor: + armor = self.target_base_armor + return self.attacker_k_value / (self.attacker_k_value + armor) 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): + def melee_hit_chance(self, base_miss_chance, dodgeable, parryable, blockable=False): 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) + parry_chance = self.base_parry_chance else: parry_chance = 0 - block_chance = self.base_block_chance * blockable + if blockable: + block_chance = self.base_block_chance + else: + block_chance = 0 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) + hit_chance = self.melee_hit_chance(self.base_one_hand_miss_rate, dodgeable=False, parryable=False) return hit_chance - def one_hand_melee_hit_chance(self, dodgeable=False, parryable=False, weapon=None, bonus_hit=0): + def one_hand_melee_hit_chance(self, dodgeable=False, parryable=False, blockable=False): # 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) + hit_chance = self.melee_hit_chance(self.base_one_hand_miss_rate, dodgeable, parryable, blockable) return hit_chance - def off_hand_melee_hit_chance(self, dodgeable=False, parryable=False, weapon=None, bonus_hit=0): + def off_hand_melee_hit_chance(self, dodgeable=False, parryable=False, 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) + hit_chance = self.melee_hit_chance(self.base_one_hand_miss_rate, dodgeable, parryable) 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) + hit_chance = self.dual_wield_hit_chance(dodgeable, parryable, 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) + hit_chance = self.dual_wield_hit_chance(dodgeable, parryable, dw_miss=dw_miss) return hit_chance - def dual_wield_hit_chance(self, dodgeable, parryable, weapon_type, dw_miss=None): + def dual_wield_hit_chance(self, dodgeable, parryable, 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) + hit_chance = self.melee_hit_chance(dw_miss, dodgeable, parryable) return hit_chance def buff_melee_crit(self): @@ -697,16 +700,31 @@ def target_armor(self, armor=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. - 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() - elif attack_kind == 'bleed': - return self.buffs.bleed_damage_multiplier() - elif attack_kind == 'physical': - armor_override = self.target_armor(armor) - return self.buffs.physical_damage_multiplier() * self.armor_mitigation_multiplier(armor_override) + # Antorus 7.3 Raid Pantheon Trinket Empowerment, precomputed uptimes (thanks to seriallos from raidbots) + # Updated 2017/11/24 + def get_pantheon_empowerment_uptime(self, wearers): + uptimes = { + 4: 0.023, + 5: 0.061, + 6: 0.103, + 7: 0.141, + 8: 0.176, + 9: 0.205, + 10: 0.228, + 11: 0.248, + 12: 0.264, + 13: 0.276, + 14: 0.287, + 15: 0.295, + 16: 0.302, + 17: 0.306, + 18: 0.311, + 19: 0.313, + 20: 0.316 + } + if wearers in uptimes: + return uptimes[wearers] + elif wearers > 20: + return 0.316 + else: + return 0 diff --git a/shadowcraft/calcs/armor_mitigation.py b/shadowcraft/calcs/armor_mitigation.py deleted file mode 100644 index 81ff521..0000000 --- a/shadowcraft/calcs/armor_mitigation.py +++ /dev/null @@ -1,38 +0,0 @@ -from shadowcraft.core.exceptions import InvalidLevelException - -# tiered parameters for use in armor mitigation calculations. first tuple -# element is the minimum level of the tier. the tuples must be in descending -# order of minimum level for the lookup to work. parameters taken from -# http://code.google.com/p/simulationcraft/source/browse/branches/mop/engine/sc_player.cpp#1365 -PARAMETERS = [ (91, 1938, 3610.0), #lvl 100 371830 3610.0 - (86, 1044, 1945.0), #lvl 90 - (81, 8000, 1047.0), #lvl 85 - (60, 4000, 747.0), - ( 1, 85.0, 157.0) ] - -def _get_appropriate_level_for_armor_table(level): - for i in xrange(0, len(PARAMETERS)): - if level >= PARAMETERS[i][0]: - return i - -def lookup_parameters(level): - for parameters in PARAMETERS: - if level >= parameters[0]: - return parameters - raise InvalidLevelException(_('No armor mitigation parameters available for level {level}').format(level=level)) - -def parameter(level=100): - parameters = lookup_parameters(level) - return level * parameters[1] - parameters[2] - -# this is the fraction of damage reduced by the armor -def mitigation(armor, level=100, cached_parameter=None): - if cached_parameter == None: - cached_parameter = parameter(level) - #cached_parameter = lookup_parameters(level) - return armor / (armor + cached_parameter) - -# this is the fraction of damage retained despite the armor, 1 - mitigation. -def multiplier(armor, level=100, cached_parameter=None): - table_level = _get_appropriate_level_for_armor_table(level) - return PARAMETERS[table_level][2] / (PARAMETERS[table_level][1] + PARAMETERS[table_level][2]) \ No newline at end of file diff --git a/shadowcraft/calcs/darkmantle/__init__.py b/shadowcraft/calcs/darkmantle/__init__.py deleted file mode 100644 index 414de8a..0000000 --- a/shadowcraft/calcs/darkmantle/__init__.py +++ /dev/null @@ -1,126 +0,0 @@ -import copy -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.procs import InvalidProcException - -class InputNotModeledException(exceptions.InvalidInputException): - pass - -class DarkmantleCalculator(object): - - def __init__(self, stats, talents, glyphs, buffs, race, settings=None, level=100, target_level=103, char_class='rogue'): - #load stats, class, procs, etc to main content - 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 - self.level = level - - 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) - - #setup global variables, these get deep-copy and passed to new objects - self.state_values = {} - self.state_values['damage_multiplier'] = 1.0 - self.state_values['gcd_size'] = 1.0 - self.state_values['trinket_1'] = {'name':'null', 'last_proc_time': -600} - self.state_values['trinket_2'] = {'name':'null', 'last_proc_time': -600} - self.state_values['weapon_proc_1'] = {'name':'null', 'last_proc_time': -600} - self.state_values['weapon_proc_2'] = {'name':'null', 'last_proc_time': -600} - self.state_values['cooldown'] = {} - self.state_values['stat_multipliers'] = { - 'primary':self.stats.gear_buffs.gear_specialization_multiplier(), #armor specialization - 'ap':self.buffs.attack_power_multiplier(), - 'haste':1.0, - 'crit':1.0, - 'mastery':1.0, - 'versatility':1.0, - 'readiness':1.0, - 'multistrike':1.0, - } - self.state_values['current_stats'] = { - 'str': (self.stats.str), #useless for rogues now - 'agi': (self.stats.agi + self.race.racial_agi), #+ self.buffs.buff_agi() - 'int': (self.stats.int), #useless for rogues now - 'ap': (self.stats.ap), - 'crit': (self.stats.crit), - 'haste': (self.stats.haste), - 'mastery': (self.stats.mastery), # + self.buffs.buff_mast() - 'readiness': (self.stats.readiness), - 'multistrike': (self.stats.multistrike), - 'versatility': (self.stats.versatility), - } - self.calculate_effective_ap() - - self.state_values['auras'] = [] #handles permanent and temporary - for e in self.buffs.buffs_debuffs: - self.state_values['auras'].append((e, 'inf')) # ('name', time), time = 'inf' or number - - #change stats to match buffs - - #combat tables - self.base_one_hand_miss_rate = 0 - self.base_parry_chance = .03 - self.base_dodge_chance = 0 - self.base_spell_miss_rate = 0 - self.base_dw_miss_rate = .17 - self.base_block_chance = .075 - self.crit_reduction = .01 * (self.target_level - self.level) - - #load class module data - class_variables = self._get_values_for_class() - for key in class_variables: - self.state_values[key] = class_variables[key] - - def calculate_effective_ap(self): - self.state_values['effective_ap'] = (self.state_values['current_stats']['agi'] * self.state_values['stat_multipliers']['primary'] + self.stats.ap) - self.state_values['effective_ap'] *= self.state_values['stat_multipliers']['ap'] - - def end_calc_branch(self, current_time, total_damage_done): - if self.settings.style == 'time' and current_time >= self.settings.limit: - print 'Stopping calculations at: ', current_time, ' seconds' - print '--------' - return True - if self.settings.style == 'health' and total_damage_done >= self.settings.limit: - print 'Stopping calculations at: ', total_damage_done, ' damage' - print '--------' - return True - return False - - def shallow_copy_table(self, base): - #need a deep copy variant - table = {} - for key in base: - table[key] = base[key] - return table - - def shallow_copy_array(self, base): - lst = [] - for e in base: - lst.append(e) - return lst - - def _class_bonus_crit(self): - return 0 #should be overwritten by individual class modules if the crit rate needs to be shifted - - def calculate_crit_rate(self): - crit = self.stats.get_crit_from_rating(rating=self.state_values['current_stats']['crit']) - crit += self._class_bonus_crit() + self.buffs.buff_all_crit() - return crit - \ No newline at end of file diff --git a/shadowcraft/calcs/darkmantle/generic_attack.py b/shadowcraft/calcs/darkmantle/generic_attack.py deleted file mode 100644 index 659e33a..0000000 --- a/shadowcraft/calcs/darkmantle/generic_attack.py +++ /dev/null @@ -1,51 +0,0 @@ -import copy -import gettext -import __builtin__ -import math - -__builtin__._ = gettext.gettext - -from shadowcraft.calcs.rogue import RogueDamageCalculator -from shadowcraft.calcs.darkmantle.generic_event import GenericEvent -from shadowcraft.core import exceptions -from shadowcraft.objects import procs -from shadowcraft.objects import proc_data - -class GenericAttack(GenericEvent): - _name = 'generic_attack' - _hand = 'mh' - _cost = 0 - _cast_time = 0.0 - - def calculate_damage(self): - return 1 #to be overwritten by actual actions - - def secondary_effects(self): - return - - def calculate_breakdown(self): - if self.engine.end_calc_branch(self.time, self.total_damage): - return self.breakdown - normal_damage = self.calculate_damage() - a = self.secondary_effects() - self.state_values['current_power'] -= self._cost - crit_rate = 0 - crit_damage = 0 - if self.can_crit: - crit_rate = self.engine.calculate_crit_rate() - #deal with crits at a later time - if self._name in self.breakdown: - self.breakdown[self._name] += normal_damage - else: - self.breakdown[self._name] = normal_damage - #spawn child objects - self.timeline = self.timeline[1:] - self.setup_queues(self.timeline, self.state_values['auras']) - next_attack_constructor = self.engine.get_next_attack(self.timeline[0][1]) - # engine, breakdown, time, timeline, - # total_damage, state_values - next = next_attack_constructor(self.engine, self.breakdown, self.timeline[0][0], self.engine.shallow_copy_array(self.timeline), - self.total_damage, self.state_values) - next.calculate_breakdown() - average_breakdown = self.breakdown - return average_breakdown diff --git a/shadowcraft/calcs/darkmantle/generic_event.py b/shadowcraft/calcs/darkmantle/generic_event.py deleted file mode 100644 index 1f96e60..0000000 --- a/shadowcraft/calcs/darkmantle/generic_event.py +++ /dev/null @@ -1,25 +0,0 @@ -import copy -import gettext -import __builtin__ -import math - -__builtin__._ = gettext.gettext - -from shadowcraft.core import exceptions -from shadowcraft.objects import procs -from shadowcraft.objects import proc_data - -class GenericEvent(object): - - def __init__(self, engine, breakdown, time, timeline, total_damage, state_values): - self.engine = engine - self.breakdown = breakdown - self.time = time - self.timeline = timeline - self.total_damage = total_damage - self.state_values = state_values - - self.can_crit = True - - def setup_queues(self, timeline, buffs): - pass #to be overwritten by actual actions diff --git a/shadowcraft/calcs/darkmantle/rogue/__init__.py b/shadowcraft/calcs/darkmantle/rogue/__init__.py deleted file mode 100644 index 26b5df2..0000000 --- a/shadowcraft/calcs/darkmantle/rogue/__init__.py +++ /dev/null @@ -1,162 +0,0 @@ -import copy -import gettext -import __builtin__ -import math - -__builtin__._ = gettext.gettext - -import shadowcraft -from shadowcraft.calcs.darkmantle import DarkmantleCalculator -from shadowcraft.calcs.darkmantle.rogue import mh_attack -from shadowcraft.calcs.darkmantle.rogue import oh_attack -from shadowcraft.calcs.darkmantle.rogue import instant_poison -from shadowcraft.core import exceptions -from shadowcraft.objects import procs -from shadowcraft.objects import proc_data - - -class InputNotModeledException(exceptions.InvalidInputException): - # I'll return these when inputs don't make sense to the model. - pass - -class RogueDarkmantleCalculator(DarkmantleCalculator): - abilities_list = { - 'mh_autoattack': mh_attack, - 'oh_autoattack': oh_attack, - 'instant_poison': instant_poison, - } - ability_constructors = { - 'mh_autoattack': mh_attack.MHAttack, - 'oh_autoattack': oh_attack.OHAttack, - 'instant_poison': instant_poison.InstantPoison, - } - - def get_next_attack(self, name): - #pulls the constructor, not the module - if name not in self.ability_constructors: - raise InputNotModeledException(_('Can\'t locate action: {action}').format(action=str(name))) - return self.ability_constructors[name] - - def can_cast_ability(self, name): - if abilities_list._cost < self.state_values['current_power'] and self.state_values['cooldown'][name] < self.time: - return True - return False - - def _get_values_for_class(self): - #override global states if necessary - if self.settings.is_combat_rogue(): - self.base_dw_miss_rate = 0 - - #initialize variables into a table that won't disappear throughout the calculations - #additionally, set up data structures (like combo points) - class_table = {} - class_table['current_second_power'] = 0 #combot points - class_table['max_second_power'] = 5 #can only get to 5 CP (for now?) - - class_table['max_power'] = 100 #energy - if self.settings.is_assassination_rogue(): - class_table['max_power'] += 20 - if self.glyphs.energy: - class_table['max_power'] += 20 - if self.talents.lemon_zest: - class_table['max_power'] += 15 - if self.stats.gear_buffs.rogue_pvp_4pc_extra_energy(): - class_table['max_power'] += 30 - class_table['current_power'] = class_table['max_power'] - class_table['base_power_regen'] = 10 - if self.settings.is_combat_rogue(): - class_table['base_power_regen'] *= 1.2 - - if self.talents.anticipation: - class_table['anticipation'] = 0 - class_table['anticipation_max'] = 5 - if self.settings.is_combat_rogue(): - class_table['bg_counter'] = 0 - - return class_table - - def _class_bonus_crit(self): - return .05 #rogues get a "free" 5% extra crit - - def get_dps(self): - if self.settings.is_assassination_rogue(): - return self.assassination_dps_estimate() - elif self.settings.is_combat_rogue(): - return self.combat_dps_estimate() - elif self.settings.is_subtlety_rogue(): - return self.subtlety_dps_estimate() - else: - raise InputNotModeledException(_('You must specify a spec.')) - - def get_dps_breakdown(self): - if self.settings.is_assassination_rogue(): - return self.assassination_dps_breakdown() - elif self.settings.is_combat_rogue(): - return self.combat_dps_breakdown() - elif self.settings.is_subtlety_rogue(): - return self.subtlety_dps_breakdown() - else: - raise InputNotModeledException(_('You must specify a spec.')) - - def assassination_dps_estimate(self): - return sum(self.assassination_dps_breakdown().values()) - def assassination_dps_breakdown(self): - #determine pre-fight sequence, establish baseline event_queue and auras - #read priority list, determine first action - #load event_state object with event_queue - return {'none':1.} - - def combat_dps_estimate(self): - return sum(self.combat_dps_breakdown().values()) - def combat_dps_breakdown(self): - print 'Calculating Combat Breakdown...' - breakdown = {} - event_queue = [] - #determine pre-fight sequence, establish baseline event_queue and auras - #read priority list, determine first action - #load event_state object with event_queue - event_queue = [(0.0, 'mh_autoattack'), (0.01, 'oh_autoattack')] #temporary for development purposes - #self.combat_priority_list() #should determine opener, as well as handle normal rotational decisions - root_event = mh_attack.MHAttack(self, breakdown, 0, event_queue, 0, self.state_values) #timer always starts at 0, prefight has no bearing - root_event.calculate_breakdown() - return breakdown - def combat_priority_list(self, cost): - action = 'wait' - if self.state_values['current_energy'] > cost and self.state_values['combo_points'] < self.state_values['max_second_power']: - action = 'sinister_strike' - if self.state_values['current_energy'] > cost and self.state_values['combo_points'] == self.state_values['max_second_power']: - action = 'eviscerate' - if self.state_values: - return - return action - - def subtlety_dps_estimate(self): - return sum(self.subtlety_dps_breakdown().values()) - def subtlety_dps_breakdown(self): - #determine pre-fight sequence, establish baseline event_queue and auras - #read priority list, determine first action - #load event_state object with event_queue - return {'none':1.} - - def reset_bandits_guile(self): - self.state_values['bg_counter'] = 0 - self.state_values['damage_multiplier'] *= 1.0 / 1.5 #BG30 is now 50% - - def set_bandits_guile_level(self): - c = self.state_values['bg_counter'] - level = math.min(c // 4, 3) #BG30/50 is highest level - if level == 3: - self.state_values['damage_multiplier'] *= 1.5 / 1.2 #would be 1.3/1.2 if under level 100 - else: - self.state_values['damage_multiplier'] *= (1 + .1 * level) / (1 + .1 * (level-1)) - - def restless_blades_impact(self, cp): - self.state_values['cooldown']['killing_spree'] -= 2 * cp - self.state_values['cooldown']['adrenaline_rush'] -= 2 * cp - - def set_sanguinary_veins(self, enabled=True): - if enabled: - self.state_values['damage_multiplier'] *= 1.2 - else: - self.state_values['damage_multiplier'] *= 1.0/1.2 - \ No newline at end of file diff --git a/shadowcraft/calcs/darkmantle/rogue/eviscerate.py b/shadowcraft/calcs/darkmantle/rogue/eviscerate.py deleted file mode 100644 index e69cab1..0000000 --- a/shadowcraft/calcs/darkmantle/rogue/eviscerate.py +++ /dev/null @@ -1,27 +0,0 @@ -import copy -import gettext -import __builtin__ -import math - -__builtin__._ = gettext.gettext - -from shadowcraft.calcs.darkmantle.generic_attack import GenericAttack - -class Eviscerate(GenericAttack): - _name = 'Eviscerate' - _cost = 35 - - def calculate_damage(self): - # non-normalized weapon strike => (mh_weapon_damage + ap / 3.5 * weapon_speed) * weapon_damage_percentage - print 'Eviscerate: ', self.state_values['current_second_power'] - return .3 * self.state_values['current_second_power'] * self.state_values['effective_ap'] - - def secondary_effects(self): - self.engine.restless_blades_impact(self.state_values['current_second_power']) - #shift combo points, clean up residuals - self.state_values['current_second_power'] = self.state_values['anticipation'] - self.state_values['anticipation'] = 0 - - def setup_queues(self, timeline, buffs): - #enable_autoattacks() - timeline.append((self.time + self.state_values['gcd_size'], 'priority_queue')) diff --git a/shadowcraft/calcs/darkmantle/rogue/instant_poison.py b/shadowcraft/calcs/darkmantle/rogue/instant_poison.py deleted file mode 100644 index d15ed85..0000000 --- a/shadowcraft/calcs/darkmantle/rogue/instant_poison.py +++ /dev/null @@ -1,25 +0,0 @@ -import copy -import gettext -import __builtin__ -import math - -__builtin__._ = gettext.gettext - -from shadowcraft.calcs.darkmantle.generic_attack import GenericAttack -from shadowcraft.core import exceptions -from shadowcraft.objects import procs -from shadowcraft.objects import proc_data - -class InstantPoison(GenericAttack): - _name = 'instant_poison' - _cost = 0 - _cast_time = 0.0 - - def calculate_damage(self): - # non-normalized weapon strike => (mh_weapon_damage + ap / 3.5 * weapon_speed) * weapon_damage_percentage - print 'Instant Poison: ', self.state_values['effective_ap'] * .20 - return self.state_values['effective_ap'] * .20 #??? - - def setup_queues(self, timeline, buffs): - return #nothing else to trigger for now - \ No newline at end of file diff --git a/shadowcraft/calcs/darkmantle/rogue/mh_attack.py b/shadowcraft/calcs/darkmantle/rogue/mh_attack.py deleted file mode 100644 index ef6bc42..0000000 --- a/shadowcraft/calcs/darkmantle/rogue/mh_attack.py +++ /dev/null @@ -1,19 +0,0 @@ -import copy -import gettext -import __builtin__ -import math - -__builtin__._ = gettext.gettext - -from shadowcraft.calcs.darkmantle.generic_attack import GenericAttack - -class MHAttack(GenericAttack): - _name = 'mh_autoattack' - - def calculate_damage(self): - # non-normalized weapon strike => (mh_weapon_damage + ap / 3.5 * weapon_speed) * weapon_damage_percentage - print 'MH Attack: ', self.engine.stats.mh.speed * (self.engine.stats.mh.weapon_dps + self.state_values['effective_ap'] / 3.5) - return self.engine.stats.mh.speed * (self.engine.stats.mh.weapon_dps + self.state_values['effective_ap'] / 3.5) - - def setup_queues(self, timeline, buffs): - timeline.append((self.time + self.engine.stats.mh.speed, 'mh_autoattack')) diff --git a/shadowcraft/calcs/darkmantle/rogue/oh_attack.py b/shadowcraft/calcs/darkmantle/rogue/oh_attack.py deleted file mode 100644 index 02d76d8..0000000 --- a/shadowcraft/calcs/darkmantle/rogue/oh_attack.py +++ /dev/null @@ -1,20 +0,0 @@ -import copy -import gettext -import __builtin__ -import math - -__builtin__._ = gettext.gettext - -from shadowcraft.calcs.darkmantle.generic_attack import GenericAttack - -class OHAttack(GenericAttack): - _name = 'oh_autoattack' - - def calculate_damage(self): - # non-normalized weapon strike => (oh_weapon_damage + ap / 3.5 * weapon_speed) * weapon_damage_percentage - print 'OH Attack: ', self.engine.stats.oh.speed * (self.engine.stats.oh.weapon_dps + self.state_values['effective_ap'] / 3.5) * .5 - return self.engine.stats.oh.speed * (self.engine.stats.oh.weapon_dps + self.state_values['effective_ap'] / 3.5) * .5 - - def setup_queues(self, timeline, buffs): - timeline.append((self.time + self.engine.stats.oh.speed, 'oh_autoattack')) - \ No newline at end of file diff --git a/shadowcraft/calcs/darkmantle/rogue/sinister_strike.py b/shadowcraft/calcs/darkmantle/rogue/sinister_strike.py deleted file mode 100644 index e727706..0000000 --- a/shadowcraft/calcs/darkmantle/rogue/sinister_strike.py +++ /dev/null @@ -1,28 +0,0 @@ -import copy -import gettext -import __builtin__ -import math - -__builtin__._ = gettext.gettext - -from shadowcraft.calcs.darkmantle.generic_attack import GenericAttack - -class SinisterStrike(GenericAttack): - _name = 'Sinister Strike' - _cost = 50 - - def calculate_damage(self): - # non-normalized weapon strike => (mh_weapon_damage + ap / 3.5 * weapon_speed) * weapon_damage_percentage - print 'Sinister Strike: ', 1.2 * .85 * self.engine.stats.mh.speed * (self.engine.stats.mh.weapon_dps + self.state_values['effective_ap'] / 3.5) - return 1.2 * .85 * self.engine.stats.mh.speed * (self.engine.stats.mh.weapon_dps + self.state_values['effective_ap'] / 3.5) - - def secondary_effects(self): - # +1 CP - if self.state_values['current_second_power'] < self.state_values['max_second_power']: - self.state_values['current_second_power'] = math.min(self.state_values['current_second_power']+1, self.state_values['max_second_power']) - if self.engine.talents.anticipation and self.state_values['current_second_power'] == self.state_values['max_second_power']: - self.state_values['anticipation'] += math.min(self.state_values['anticipation']+1, self.state_values['anticipation_max']) - - def setup_queues(self, timeline, buffs): - #enable_autoattacks() - timeline.append((self.time + self.state_values['gcd_size'], 'priority_queue')) diff --git a/shadowcraft/calcs/darkmantle/settings.py b/shadowcraft/calcs/darkmantle/settings.py deleted file mode 100755 index c94663e..0000000 --- a/shadowcraft/calcs/darkmantle/settings.py +++ /dev/null @@ -1,53 +0,0 @@ -from shadowcraft.core import exceptions - -class Settings(object): - - def __init__(self, cycle, response_time=.5, latency=.03, merge_damage=True, style='time', limit=10): - self.cycle = cycle #for the spec - self.response_time = response_time #general player reaction time - self.latency = latency #used sparingly - self.merge_damage = merge_damage #combines mh and oh attacks to a single source - self.style = style #determines end conditions, limited by health or time - self.limit = limit #end condition for the style, if time then in seconds - - def get_spec(self): - return self.cycle._cycle_type - - def is_assassination_rogue(self): - return self.get_spec() == 'assassination' - - def is_combat_rogue(self): - return self.get_spec() == 'combat' - - def is_subtlety_rogue(self): - return self.get_spec() == 'subtlety' - -class Cycle(object): - # Base class for cycle objects. Can't think of anything that particularly - # needs to go here yet, but it seems worth keeping options open in that - # respect. - - # When subclassing, define _cycle_type to be one of 'assassination', - # 'combat', or 'subtlety' - this is how the damage calculator makes sure - # you have an appropriate cycle object to go with your talent trees, etc. - _cycle_type = '' - - -class AssassinationCycle(Cycle): - _cycle_type = 'assassination' - - def __init__(self): - return - - -class CombatCycle(Cycle): - _cycle_type = 'combat' - - def __init__(self): - return - -class SubtletyCycle(Cycle): - _cycle_type = 'subtlety' - - def __init__(self): - return diff --git a/shadowcraft/calcs/rogue/Aldriana/__init__.py b/shadowcraft/calcs/rogue/Aldriana/__init__.py index 5083964..a21368f 100644 --- a/shadowcraft/calcs/rogue/Aldriana/__init__.py +++ b/shadowcraft/calcs/rogue/Aldriana/__init__.py @@ -1,14 +1,20 @@ +from __future__ import division #import copy +from future import standard_library +standard_library.install_aliases() +from builtins import map +from builtins import range import gettext -import __builtin__ +import builtins import math from operator import add from copy import copy -__builtin__._ = gettext.gettext +_ = gettext.gettext from shadowcraft.calcs.rogue import RogueDamageCalculator from shadowcraft.core import exceptions +from shadowcraft.objects import modifiers from shadowcraft.objects import procs from shadowcraft.objects import proc_data @@ -17,6 +23,10 @@ class InputNotModeledException(exceptions.InvalidInputException): # I'll return these when inputs don't make sense to the model. pass +class ConvergenceErrorException(exceptions.InvalidInputException): + # Return this if a convergence loop goes too long + pass + class AldrianasRogueDamageCalculator(RogueDamageCalculator): ########################################################################### @@ -26,23 +36,23 @@ class AldrianasRogueDamageCalculator(RogueDamageCalculator): def get_dps(self): super(AldrianasRogueDamageCalculator, self).get_dps() - if self.settings.is_assassination_rogue(): - self.init_assassination() + if self.spec == 'assassination': return self.assassination_dps_estimate() - elif self.settings.is_combat_rogue(): - return self.combat_dps_estimate() - elif self.settings.is_subtlety_rogue(): + elif self.spec == 'outlaw': + raise InputNotModeledException(_('Outlaw model not supported, at the moment.')) + return self.outlaw_dps_estimate() + elif self.spec == 'subtlety': return self.subtlety_dps_estimate() else: raise InputNotModeledException(_('You must specify a spec.')) def get_dps_breakdown(self): - if self.settings.is_assassination_rogue(): - self.init_assassination() + if self.spec == 'assassination': return self.assassination_dps_breakdown() - elif self.settings.is_combat_rogue(): - return self.combat_dps_breakdown() - elif self.settings.is_subtlety_rogue(): + elif self.spec == 'outlaw': + raise InputNotModeledException(_('Outlaw model not supported, at the moment.')) + return self.outlaw_dps_breakdown() + elif self.spec == 'subtlety': return self.subtlety_dps_breakdown() else: raise InputNotModeledException(_('You must specify a spec.')) @@ -65,35 +75,27 @@ def are_close_enough(self, old_dist, new_dist, precision=PRECISION_REQUIRED): if abs(new_dist[item][index] - old_dist[item][index]) > precision: return False return True - + ########################################################################### # Overrides: these make the ep methods default to glyphs/talents or weapon # setups that we are really modeling. ########################################################################### - def get_glyphs_ranking(self, list=None): - if list is None: - list = [ - 'vendetta', - 'energy', - 'disappearance', - ] - return super(AldrianasRogueDamageCalculator, self).get_glyphs_ranking(list) - - def get_talents_ranking(self, list=None): - if list is None: - list = [ - 'nightstalker', - 'subterfuge', - 'shadow_focus', - #'shuriken_toss', - 'marked_for_death', - 'anticipation', - 'lemon_zest', - 'death_from_above', - 'shadow_reflection', - ] - return super(AldrianasRogueDamageCalculator, self).get_talents_ranking(list) + #i don't know why this is overridden, but I disabled it to fix talent ranking -aeriwen + #def get_talents_ranking(self, list=None): + # if list is None: + # list = [ + # 'nightstalker', + # 'subterfuge', + # 'shadow_focus', + # #'shuriken_toss', + # 'marked_for_death', + # 'anticipation', + # 'lemon_zest', + # 'death_from_above', + # 'shadow_reflection', + # ] + # return super(AldrianasRogueDamageCalculator, self).get_talents_ranking(list) def get_oh_weapon_modifier(self, setups=None): if setups is None: @@ -124,60 +126,6 @@ def get_heroism_haste_multiplier(self): # Just average-casing for now. Should fix that at some point. return 1 + .3 * self.heroism_uptime_per_fight() - def get_cp_distribution_for_cycle(self, cp_distribution_per_move, target_cp_quantity): - avg_cp_per_cpg = sum([key * cp_distribution_per_move[key] for key in cp_distribution_per_move]) - - time_spent_at_cp = [0, 0, 0, 0, 0, 0] - cur_min_cp = 0 - cur_dist = {(0, 0): 1} - while cur_min_cp < target_cp_quantity: - cur_min_cp += 1 - - new_dist = {} - for (cps, moves), prob in cur_dist.items(): - if cps >= cur_min_cp: - if (cps, moves) in new_dist: - new_dist[(cps, moves)] += prob - else: - new_dist[(cps, moves)] = prob - else: - for (move_cp, move_prob) in cp_distribution_per_move.items(): - total_cps = cps + move_cp - if total_cps > 5: - total_cps = 5 - dist_entry = (total_cps, moves + 1) - time_spent_at_cp[total_cps] += move_prob * prob - if dist_entry in new_dist: - new_dist[dist_entry] += move_prob * prob - else: - new_dist[dist_entry] = move_prob * prob - cur_dist = new_dist - - for (cps, moves), prob in cur_dist.items(): - time_spent_at_cp[cps] += prob - - total_weight = sum(time_spent_at_cp) - for i in xrange(6): - time_spent_at_cp[i] /= total_weight - - return cur_dist, time_spent_at_cp, avg_cp_per_cpg - - def get_cp_per_cpg(self, base_cp_per_cpg=1, *probs): - # Computes the combined probabilites of getting an additional cp from - # each of the items in probs. - cp_per_cpg = {base_cp_per_cpg: 1} - for prob in probs: - if prob == 0: - continue - new_cp_per_cpg = {} - for cp in cp_per_cpg: - new_cp_per_cpg.setdefault(cp, 0) - new_cp_per_cpg.setdefault(cp + 1, 0) - new_cp_per_cpg[cp] += cp_per_cpg[cp] * (1 - prob) - new_cp_per_cpg[cp + 1] += cp_per_cpg[cp] * prob - cp_per_cpg = new_cp_per_cpg - return cp_per_cpg - def get_crit_rates(self, stats): base_melee_crit_rate = self.crit_rate(crit=stats['crit']) crit_rates = { @@ -187,39 +135,68 @@ def get_crit_rates(self, stats): for attack in ('rupture_ticks', 'shuriken_toss'): crit_rates[attack] = base_melee_crit_rate - if self.settings.is_assassination_rogue(): - spec_attacks = ('mutilate', 'dispatch', 'envenom', 'venomous_wounds') - elif self.settings.is_combat_rogue(): - spec_attacks = ('main_gauche', 'sinister_strike', 'revealing_strike', 'eviscerate', 'killing_spree', 'oh_killing_spree', 'mh_killing_spree') - elif self.settings.is_subtlety_rogue(): - spec_attacks = ('eviscerate', 'backstab', 'ambush', 'hemorrhage') - - if self.settings.dmg_poison == 'dp': - poisons = ('deadly_instant_poison', 'deadly_poison') - elif self.settings.dmg_poison == 'wp': - poisons = ('wound_poison', ) - elif self.settings.dmg_poison == 'sp': - poisons = ('swift_poison', ) - - talent_attacks = () - if self.talents.death_from_above: - talent_attacks = ('death_from_above', 'death_from_above_strike', 'death_from_above_pulse') - - openers = tuple([self.settings.opener_name]) + if self.spec == 'assassination': + spec_attacks = self.assassination_damage_sources + elif self.spec == 'outlaw': + spec_attacks = self.outlaw_damage_sources + elif self.spec == 'subtlety': + spec_attacks = self.subtlety_damage_sources - for attack in spec_attacks + poisons + openers + talent_attacks: + for attack in spec_attacks: #for handling odd crit rates - if attack in ('eviscerate', 'envenom') and self.stats.gear_buffs.rogue_t15_4pc: - crit_rates[attack] = base_melee_crit_rate + .2 + if attack == 'mutilate' and self.traits.balanced_blades: + crit_rates[attack] = base_melee_crit_rate + (0.02 * self.traits.balanced_blades) + elif attack == 'rupture_ticks' and self.traits.serrated_edge: + crit_rates[attack] = base_melee_crit_rate + (0.03333 * self.traits.serrated_edge) + elif attack in ('pistol_shot', 'blunderbuss') and self.traits.gunslinger: + crit_rates[attack] = base_melee_crit_rate + (0.06 * self.traits.gunslinger) + elif attack == 'eviscerate' and self.traits.gutripper: + crit_rates[attack] = base_melee_crit_rate + (0.05 * self.traits.gutripper) else: crit_rates[attack] = base_melee_crit_rate - for attack, crit_rate in crit_rates.items(): + for attack, crit_rate in list(crit_rates.items()): if crit_rate > 1: crit_rates[attack] = 1 return crit_rates + + def get_haste_multiplier(self, current_stats): + return self.stats.get_haste_multiplier_from_rating(current_stats['haste']) * self.true_haste_mod + + + def get_energy_regen(self, current_stats, buried=False, ar=False, alacrity_stacks=0, snd=False): + regen = 10. + if self.spec == "outlaw": + regen = 12. + if self.settings.cycle.blade_flurry: + regen *= .8 + (0.03333 * self.traits.blade_dancer) + if buried: + regen *= 1.25 + if ar: + regen *= 2.0 + if snd: + regen *= 1.195 if ar and self.traits.loaded_dice else 1.15 + else: + alacrity_stacks = 0 + if self.talents.vigor or self.stats.gear_buffs.soul_of_the_shadowblade: + regen *= 1.1 + regen *= self.get_haste_multiplier(current_stats) + 0.01 * alacrity_stacks + return regen + + + def get_attack_speed_multiplier(self, current_stats, snd=False, melee=False, ar=False, alacrity_stacks=0): + attack_speed_multiplier = self.get_haste_multiplier(current_stats) + 0.01 * alacrity_stacks + if melee: + attack_speed_multiplier *= 1.5 + elif snd: + attack_speed_multiplier *= 2.3 if ar and self.traits.loaded_dice else 2 + if ar: + attack_speed_multiplier *= 1.2 + return attack_speed_multiplier + + def set_constants(self): # General setup that we'll use in all 3 cycles. self.load_from_advanced_parameters() @@ -227,119 +204,184 @@ def set_constants(self): self.spec_needs_converge = False #racials if self.race.arcane_torrent: - self.bonus_energy_regen += 15. / (120 + self.settings.response_time) + self.bonus_energy_regen += 15 / (120 + self.settings.response_time) #auxiliary rotational effects - if self.settings.shiv_interval != 0: - self.bonus_energy_regen -= self.get_spell_stats('shiv')[0] / self.settings.shiv_interval if self.settings.feint_interval != 0: self.bonus_energy_regen -= self.get_spell_stats('feint')[0] / self.settings.feint_interval - - self.set_openers() - - #only include if general multiplier applies to spec calculations + + + #only include if general multiplier applies to spec calculations self.true_haste_mod *= self.get_heroism_haste_multiplier() - self.base_stats = { - 'agi': (self.stats.agi + self.buffs.buff_agi(race=self.race.epicurean) + self.race.racial_agi), - 'ap': (self.stats.ap), - 'crit': (self.stats.crit + self.buffs.buff_crit(race=self.race.epicurean)), - 'haste': (self.stats.haste + self.buffs.buff_haste(race=self.race.epicurean)), - 'mastery': (self.stats.mastery + self.buffs.buff_mast(race=self.race.epicurean)), - 'readiness': (self.stats.readiness + self.buffs.buff_readiness(race=self.race.epicurean)), - 'multistrike': (self.stats.multistrike + self.buffs.buff_multistrike(race=self.race.epicurean)), - 'versatility': (self.stats.versatility + self.buffs.buff_versatility(race=self.race.epicurean)), - } - self.stat_multipliers = { - 'str': 1., - 'agi': self.buffs.stat_multiplier() * self.stats.gear_buffs.gear_specialization_multiplier(), - 'ap': self.buffs.attack_power_multiplier(), - 'crit': 1., - 'haste': 1., - 'mastery': 1., - 'readiness': 1., - 'multistrike': 1., - 'versatility': 1., - } - - if self.race.human_spirit: - self.base_stats['versatility'] += self.race.versatility_bonuses[self.level] - + self.base_stats = self.stats.get_character_base_stats(self.race, self.traits, self.buffs) + self.stat_multipliers = self.stats.get_character_stat_multipliers(self.race) + for boost in self.race.get_racial_stat_boosts(): if boost['stat'] in self.base_stats: self.base_stats[boost['stat']] += boost['value'] * boost['duration'] * 1.0 / (boost['cooldown'] + self.settings.response_time) - - if self.stats.procs.virmens_bite: - getattr(self.stats.procs, 'virmens_bite').icd = self.settings.duration - if self.stats.procs.virmens_bite_prepot: - getattr(self.stats.procs, 'virmens_bite_prepot').icd = self.settings.duration - if self.stats.procs.draenic_agi_pot: - getattr(self.stats.procs, 'draenic_agi_pot').icd = self.settings.duration - if self.stats.procs.draenic_agi_prepot: - getattr(self.stats.procs, 'draenic_agi_prepot').icd = self.settings.duration - - self.base_strength = self.stats.str + self.buffs.buff_str() + self.race.racial_str - self.base_strength *= self.buffs.stat_multiplier() - self.base_intellect = self.stats.int + self.race.racial_int - self.base_intellect *= self.buffs.stat_multiplier() - - self.relentless_strikes_energy_return_per_cp = 5 #.20 * 25 - + + if self.stats.procs.prolonged_power_pot: + self.stats.procs.prolonged_power_pot.icd = self.settings.duration + if self.stats.procs.prolonged_power_prepot: + self.stats.procs.prolonged_power_prepot.icd = self.settings.duration + if self.stats.procs.old_war_pot: + self.stats.procs.old_war_pot.icd = self.settings.duration + if self.stats.procs.old_war_prepot: + self.stats.procs.old_war_prepot.icd = self.settings.duration + + self.relentless_strikes_energy_return_per_cp = 6 + #should only include bloodlust if the spec can average it in, deal with this later - self.base_speed_multiplier = 1.4 if self.race.berserking: self.true_haste_mod *= (1 + .15 * 10. / (180 + self.settings.response_time)) self.true_haste_mod *= 1 + self.race.get_racial_haste() #doesn't include Berserking - self.true_haste_mod *= self.buffs.haste_multiplier() if self.stats.gear_buffs.rogue_t14_4pc: self.true_haste_mod *= 1.05 - + if self.stats.gear_buffs.sephuzs_secret: + self.true_haste_mod *= 1.02 + + #The procs are set within the stats object, which is global. + #This means they will still be active, even if gear/traits of + #origin changed due to rankings. + #Because of that, remove these manually added procs before setting them. + #For other procs, the specific EP ranking function is responsible. + + #set additional procs + self.stats.procs.del_proc('felmouth_frenzy') + if self.buffs.felmouth_food(): + self.stats.procs.set_proc('felmouth_frenzy') + self.stats.procs.del_proc('jacins_ruse_2pc') + if self.stats.gear_buffs.jacins_ruse_2pc: + self.stats.procs.set_proc('jacins_ruse_2pc') + self.stats.procs.del_proc('march_of_the_legion_2pc') + if self.stats.gear_buffs.march_of_the_legion_2pc and self.settings.is_demon: + self.stats.procs.set_proc('march_of_the_legion_2pc') + self.stats.procs.del_proc('rogue_orderhall_8pc') + if self.stats.gear_buffs.rogue_orderhall_8pc: + self.stats.procs.set_proc('rogue_orderhall_8pc') + if self.stats.gear_buffs.journey_through_time_2pc and self.stats.procs.chrono_shard: + self.stats.procs.chrono_shard.update_proc_value() + self.stats.procs.chrono_shard.value['haste'] += 1000 + if self.stats.gear_buffs.kara_empowered_2pc: + if self.stats.procs.bloodstained_handkerchief: + self.stats.procs.bloodstained_handkerchief.update_proc_value() + self.stats.procs.bloodstained_handkerchief.value *= 1.3 + if self.stats.procs.eye_of_command: + self.stats.procs.eye_of_command.update_proc_value() + self.stats.procs.eye_of_command.value['crit'] *= 1.3 + if self.stats.procs.toe_knees_promise: + self.stats.procs.toe_knees_promise.update_proc_value() + self.stats.procs.toe_knees_promise.value *= 1.3 + + self.stats.procs.del_proc('concordance_of_the_legionfall') + if self.traits.concordance_of_the_legionfall: + self.stats.procs.set_proc('concordance_of_the_legionfall') + self.stats.procs.concordance_of_the_legionfall.value['agi'] = 4000 + (self.traits.concordance_of_the_legionfall - 1) * 300 + if self.traits.murderous_intent: + self.stats.procs.concordance_of_the_legionfall.value['versatility'] = 1500 * self.traits.murderous_intent + if self.traits.shocklight: + self.stats.procs.concordance_of_the_legionfall.value['crit'] = 1500 * self.traits.shocklight + + # Pantheon Empowerment proc setup + self.stats.procs.del_proc('amanthuls_vision_empowered') + if self.stats.procs.amanthuls_vision and self.settings.pantheon_trinket_users >= 4: + self.stats.procs.set_proc('amanthuls_vision_empowered') + self.stats.procs.amanthuls_vision_empowered.duration = self.pantheon_empowerment_uptime * self.stats.procs.amanthuls_vision_empowered.icd + self.stats.procs.del_proc('golganneths_vitality_empowered') + if self.stats.procs.golganneths_vitality and self.settings.pantheon_trinket_users >= 4: + self.stats.procs.set_proc('golganneths_vitality_empowered') + + #netherlight crucible t2 procs + insigniaMod = 1.5 if self.stats.gear_buffs.insignia_of_the_grand_army else 1 + self.stats.procs.del_proc('chaotic_darkness') + if self.traits.chaotic_darkness: + self.stats.procs.set_proc('chaotic_darkness') + self.stats.procs.chaotic_darkness.value *= self.traits.chaotic_darkness * insigniaMod + self.stats.procs.del_proc('dark_sorrows') + if self.traits.dark_sorrows: + self.stats.procs.set_proc('dark_sorrows') + self.stats.procs.dark_sorrows.value *= self.traits.dark_sorrows * insigniaMod + self.stats.procs.del_proc('infusion_of_light') + if self.traits.infusion_of_light: + self.stats.procs.set_proc('infusion_of_light') + self.stats.procs.infusion_of_light.value *= self.traits.infusion_of_light * insigniaMod + self.stats.procs.del_proc('secure_in_the_light') + if self.traits.secure_in_the_light: + self.stats.procs.set_proc('secure_in_the_light') + self.stats.procs.secure_in_the_light.value *= self.traits.secure_in_the_light * insigniaMod + self.stats.procs.del_proc('shadowbind') + if self.traits.shadowbind: + self.stats.procs.set_proc('shadowbind') + self.stats.procs.shadowbind.value *= self.traits.shadowbind * insigniaMod + self.stats.procs.del_proc('torment_the_weak') + if self.traits.torment_the_weak: + self.stats.procs.set_proc('torment_the_weak') + self.stats.procs.torment_the_weak.value *= self.traits.torment_the_weak * insigniaMod + #hit chances self.dw_mh_hit_chance = self.dual_wield_mh_hit_chance() self.dw_oh_hit_chance = self.dual_wield_oh_hit_chance() - + return self + def load_from_advanced_parameters(self): self.true_haste_mod = self.get_adv_param('haste_buff', 1., min_bound=.1, max_bound=3.) - + self.major_cd_delay = self.get_adv_param('major_cd_delay', 0, min_bound=0, max_bound=600) self.settings.feint_interval = self.get_adv_param('feint_interval', self.settings.feint_interval, min_bound=0, max_bound=600) - + self.settings.is_day = self.get_adv_param('is_day', self.settings.is_day, ignore_bounds=True) self.get_version_number = self.get_adv_param('print_version', False, ignore_bounds=True) - - def get_proc_damage_contribution(self, proc, proc_count, current_stats, average_ap, damage_breakdown): + + def get_proc_damage_contribution(self, proc, proc_count, current_stats, average_ap, modifier_dict): crit_multiplier = self.crit_damage_modifiers() crit_rate = self.crit_rate(crit=current_stats['crit']) - - if proc.stat == 'spell_damage': - multiplier = self.get_modifiers(current_stats, damage_type='spell') - elif proc.stat == 'physical_damage': - multiplier = self.get_modifiers(current_stats, damage_type='physical') - elif proc.stat == 'physical_dot': - multiplier = self.get_modifiers(current_stats, damage_type='bleed') - elif proc.stat == 'bleed_damage': - multiplier = self.get_modifiers(current_stats, damage_type='bleed') + + if proc.proc_name in modifier_dict: + multiplier = modifier_dict[proc.proc_name] + elif proc.dmg_school is not None and 'school_' + proc.dmg_school in modifier_dict: + multiplier = modifier_dict['school_' + proc.dmg_school] else: - return 0 + multiplier = modifier_dict['all_damage'] if proc.can_crit == False: crit_rate = 0 + elif self.stats.gear_buffs.mantle_of_the_master_assassin: + crit_rate = min(crit_rate * (1. - self.mantle_uptime) + self.mantle_uptime, 1) proc_value = proc.value #280+75% AP if proc is getattr(self.stats.procs, 'legendary_capacitive_meta'): crit_rate = self.crit_rate(crit=current_stats['crit']) proc_value = average_ap * 1.5 + 50 - + if proc is getattr(self.stats.procs, 'fury_of_xuen'): crit_rate = self.crit_rate(crit=current_stats['crit']) proc_value = (average_ap * .40 + 1) * 10 * (1 + min(4., self.settings.num_boss_adds)) - average_hit = proc_value * multiplier - average_damage = average_hit * (1 + crit_rate * (crit_multiplier - 1)) * proc_count - #print proc.proc_name, average_hit, multiplier - - if proc.stat == 'physical_dot': - average_damage *= proc.uptime / proc_count - + if proc is getattr(self.stats.procs, 'mirror_of_the_blademaster'): + crit_rate = self.crit_rate(crit=current_stats['crit']) + # Each mirror produces 10 swings scaling with haste + # There are 4 mirrors, 2 spawn in front of the get and are parryable + # Each mirror swings a weapon with weapon damage based on 100% of AP + haste_mult = self.stats.get_haste_multiplier_from_rating(current_stats['haste']) + swings_per_mirror = 20 / (2 / haste_mult) + total_swings = 2 * swings_per_mirror + 2 * (1 - self.base_parry_chance) * swings_per_mirror + proc_value = total_swings*(average_ap / 3.5) * (1 + self.settings.num_boss_adds) + + #.424*max(AP, SP) + if proc is getattr(self.stats.procs, 'felmouth_frenzy'): + proc_value = average_ap * 0.424 * 5 + + average_hit = proc_value + proc.ap_coefficient * average_ap + average_damage = average_hit * (1 + crit_rate * (crit_multiplier - 1)) * proc_count * multiplier + + if proc.stat in ['physical_dot', 'spell_dot']: + initial_tick = 1. if proc.dot_initial_tick else 0. + ticks_per_second = (proc.dot_ticks - initial_tick) / proc.duration + average_damage *= initial_tick + ticks_per_second * proc.uptime / proc_count + + if proc.aoe: + average_damage *= 1 + self.settings.num_boss_adds + return average_damage def set_openers(self): @@ -349,14 +391,14 @@ def set_openers(self): opener_cd = 30 if self.settings.use_opener == 'always': opener_spacing = (self.get_spell_cd('vanish') + self.settings.response_time) - total_openers_per_second = (1. + math.floor((self.settings.duration - opener_cd) / opener_spacing)) / self.settings.duration + total_openers_per_second = (1 + math.floor((self.settings.duration - opener_cd) / opener_spacing)) / self.settings.duration elif self.settings.use_opener == 'opener': - total_openers_per_second = 1. / self.settings.duration + total_openers_per_second = 1 / self.settings.duration opener_spacing = None else: total_openers_per_second = 0 opener_spacing = None - + self.total_openers_per_second = total_openers_per_second self.swing_reset_spacing = opener_spacing @@ -371,7 +413,7 @@ def get_bonus_energy_from_openers(self, *cycle_abilities): # else, it's a rotational ability and we have SF, so we should add energy # this lets us save computational time in the aps methods return self.get_net_energy_cost(self.settings.opener_name) * (1 - self.get_shadow_focus_multiplier()) * self.total_openers_per_second - + def get_net_energy_cost(self, ability): return self.get_spell_stats(ability)[0] @@ -384,14 +426,16 @@ def get_mh_procs_per_second(self, proc, attacks_per_second, crit_rates): else: if 'mh_autoattack_hits' in attacks_per_second: triggers_per_second += attacks_per_second['mh_autoattack_hits'] + elif 'mh_autoattacks' in attacks_per_second: + triggers_per_second += attacks_per_second['mh_autoattacks'] * self.dw_mh_hit_chance if proc.procs_off_strikes(): - for ability in ('mutilate', 'dispatch', 'backstab', 'revealing_strike', 'sinister_strike', 'ambush', 'hemorrhage', 'mh_killing_spree', 'shuriken_toss'): + for ability in ('mutilate', 'dispatch', 'backstab', 'pistol_shot', 'saber_slash', 'ambush', 'hemorrhage', 'mh_killing_spree', 'shuriken_toss'): if ability in attacks_per_second: if proc.procs_off_crit_only(): triggers_per_second += attacks_per_second[ability] * crit_rates[ability] else: triggers_per_second += attacks_per_second[ability] - for ability in ('envenom', 'eviscerate'): + for ability in ('envenom', 'eviscerate', 'run_through'): if ability in attacks_per_second: if proc.procs_off_crit_only(): triggers_per_second += sum(attacks_per_second[ability]) * crit_rates[ability] @@ -399,12 +443,12 @@ def get_mh_procs_per_second(self, proc, attacks_per_second, crit_rates): triggers_per_second += sum(attacks_per_second[ability]) if proc.procs_off_apply_debuff() and not proc.procs_off_crit_only(): if 'rupture' in attacks_per_second: - triggers_per_second += attacks_per_second['rupture'] + triggers_per_second += sum(attacks_per_second['rupture']) if 'garrote' in attacks_per_second: triggers_per_second += attacks_per_second['garrote'] if 'hemorrhage_ticks' in attacks_per_second: triggers_per_second += attacks_per_second['hemorrhage'] - return triggers_per_second * proc.get_proc_rate(self.stats.mh.speed) + return triggers_per_second * proc.get_proc_rate(self.stats.mh.speed, spec=self.spec) def get_oh_procs_per_second(self, proc, attacks_per_second, crit_rates): triggers_per_second = 0 @@ -415,6 +459,8 @@ def get_oh_procs_per_second(self, proc, attacks_per_second, crit_rates): else: if 'oh_autoattack_hits' in attacks_per_second: triggers_per_second += attacks_per_second['oh_autoattack_hits'] + elif 'oh_autoattacks' in attacks_per_second: + triggers_per_second += attacks_per_second['oh_autoattacks'] * self.dw_oh_hit_chance if proc.procs_off_strikes(): for ability in ('mutilate', 'oh_killing_spree'): if ability in attacks_per_second: @@ -422,7 +468,7 @@ def get_oh_procs_per_second(self, proc, attacks_per_second, crit_rates): triggers_per_second += attacks_per_second[ability] * crit_rates[ability] else: triggers_per_second += attacks_per_second[ability] - return triggers_per_second * proc.get_proc_rate(self.stats.oh.speed) + return triggers_per_second * proc.get_proc_rate(self.stats.oh.speed, spec=self.spec) def get_other_procs_per_second(self, proc, attacks_per_second, crit_rates): triggers_per_second = 0 @@ -458,7 +504,7 @@ def get_other_procs_per_second(self, proc, attacks_per_second, crit_rates): else: raise InputNotModeledException(_('PPMs that also proc off spells are not yet modeled.')) else: - return triggers_per_second * proc.get_proc_rate() + return triggers_per_second * proc.get_proc_rate(spec=self.spec) def get_procs_per_second(self, proc, attacks_per_second, crit_rates): # TODO: Include damaging proc hits in figuring out how often everything else procs. @@ -471,25 +517,24 @@ def get_procs_per_second(self, proc, attacks_per_second, crit_rates): procs_per_second += self.get_oh_procs_per_second(proc, attacks_per_second, crit_rates) procs_per_second += self.get_other_procs_per_second(proc, attacks_per_second, crit_rates) return procs_per_second - + def lost_swings_from_swing_delay(self, delay, swing_timer): # delay = swing delay = s (see: graphs) # swing timer = x (see: graphs) delay_remainder = delay % .5 #m num_sum = min(swing_timer, delay) #n - + #TODO: Wiki Documentation explaining swing delay calculations #OLD SWING DELAY METHODS: delay//swing_timer + (delay%swing_timer)/swing_timer # : delay/swing_timer # : OH is the same value but 1 lower - - t0 = max(min( delay_remainder/swing_timer*1.5, 1.5 ), 0) - t1 = max(min( num_sum - delay_remainder, .5 )/swing_timer, 0) - t2 = max(min( num_sum - delay_remainder - .5, .5 )/swing_timer * .5, 0) - - #print "total delay: ", t0, t1, t2, (t0+t1+t2) - return (t0+t1+t2)/swing_timer - + + t0 = max(min(delay_remainder / swing_timer * 1.5, 1.5), 0) + t1 = max(min(num_sum - delay_remainder, .5) / swing_timer, 0) + t2 = max(min(num_sum - delay_remainder - .5, .5 ) / swing_timer * .5, 0) + + return (t0 + t1 + t2) / swing_timer + def set_uptime_for_ramping_proc(self, proc, procs_per_second): time_for_one_stack = 1 / procs_per_second if time_for_one_stack * proc.max_stacks > self.settings.duration: @@ -505,23 +550,23 @@ def update_with_damaging_proc(self, proc, attacks_per_second, crit_rates): #http://us.battle.net/wow/en/forum/topic/8197741003?page=4#79 haste = 1. if proc.haste_scales: - haste *= self.true_haste_mod * self.stats.get_haste_multiplier_from_rating(self.base_stats['haste']) + haste *= self.true_haste_mod * self.stats.get_haste_multiplier_from_rating(self.base_stats['haste'] * self.stat_multipliers['haste']) if proc.att_spd_scales: haste *= 1.4 #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: - frequency = haste * 1.1307 * proc.get_rppm_proc_rate() / 60 + frequency = haste * 1.1307 * proc.get_rppm_proc_rate(spec=self.spec) / 60 else: - mean_proc_time = 60. / (haste * proc.get_rppm_proc_rate()) + proc.icd - min(proc.icd, 10) + mean_proc_time = 60 / (haste * proc.get_rppm_proc_rate(spec=self.spec)) + proc.icd - min(proc.icd, 10) if proc.max_stacks > 1: # just correct if you only do damage on max_stacks, e.g. legendary_capacitive_meta mean_proc_time *= proc.max_stacks frequency = 1.1307 / mean_proc_time else: if proc.icd: - frequency = 1. / (proc.icd + 0.5 / self.get_procs_per_second(proc, attacks_per_second, crit_rates)) + frequency = 1 / (proc.icd + 0.5 / self.get_procs_per_second(proc, attacks_per_second, crit_rates)) else: frequency = self.get_procs_per_second(proc, attacks_per_second, crit_rates) - + if proc.proc_name in attacks_per_second: attacks_per_second[proc.proc_name] += frequency else: @@ -534,7 +579,7 @@ def get_shadow_focus_multiplier(self): def setup_unique_procs(self, current_stats, average_ap): if self.stats.procs.rocket_barrage: - getattr(self.stats.procs, 'rocket_barrage').value = 0.42900 * self.base_intellect + .5 * average_ap + 1 + self.level * 2 #need to update + getattr(self.stats.procs, 'rocket_barrage').value = 0.42900 * current_stats['int'] + .5 * average_ap + 1 + self.level * 2 #need to update if self.stats.procs.touch_of_the_grave: getattr(self.stats.procs, 'touch_of_the_grave').value = 8 * self.tools.get_constant_scaling_point(self.level) # +/- 15% spread @@ -545,51 +590,52 @@ def get_poison_counts(self, attacks_per_second, current_stats): mh_hits_per_second = self.get_mh_procs_per_second(poison, attacks_per_second, None) oh_hits_per_second = self.get_oh_procs_per_second(poison, attacks_per_second, None) total_hits_per_second = mh_hits_per_second + oh_hits_per_second - if poison: - poison_base_proc_rate = .3 - else: + if not poison: return - proc_multiplier = 1 - if self.settings.is_combat_rogue(): - if self.settings.cycle.blade_flurry: - ms_value = 1 + min(2 * (self.stats.get_multistrike_chance_from_rating(rating=current_stats['multistrike']) + self.buffs.multistrike_bonus()), 2) - proc_multiplier += min(self.settings.num_boss_adds, [4, 999][self.level==100]) * ms_value - - if self.settings.is_assassination_rogue(): - poison_base_proc_rate += .2 - poison_envenom_proc_rate = poison_base_proc_rate + .3 - aps_envenom = attacks_per_second['envenom'] - if self.talents.death_from_above: - aps_envenom = map(add, attacks_per_second['death_from_above_strike'], attacks_per_second['envenom']) - envenom_uptime = min(sum([(1 + cps + self.stats.gear_buffs.rogue_t15_2pc_bonus_cp()) * aps_envenom[cps] for cps in xrange(1, 6)]), 1) - avg_poison_proc_rate = poison_base_proc_rate * (1 - envenom_uptime) + poison_envenom_proc_rate * envenom_uptime - else: - avg_poison_proc_rate = poison_base_proc_rate - - if self.settings.dmg_poison == 'sp': - poison_procs = avg_poison_proc_rate * total_hits_per_second * proc_multiplier - 1 / self.settings.duration - attacks_per_second['swift_poison'] = poison_procs - elif self.settings.dmg_poison == 'dp': - poison_procs = avg_poison_proc_rate * total_hits_per_second * proc_multiplier - 1 / self.settings.duration + + poison_base_proc_rate = 0.5 #Improved Poisons passive for Deadly and Wound Poison + poison_envenom_proc_rate = poison_base_proc_rate + 0.3 + aps_envenom = attacks_per_second['envenom'] + if self.talents.death_from_above: + aps_envenom = list(map(add, attacks_per_second['death_from_above_strike'], attacks_per_second['envenom'])) + envenom_uptime = min(sum([(1 + cps) * aps_envenom[cps] for cps in range(1, 6)]), 1) + avg_poison_proc_rate = poison_base_proc_rate * (1 - envenom_uptime) + poison_envenom_proc_rate * envenom_uptime + + poison_procs = avg_poison_proc_rate * total_hits_per_second - 1 / self.settings.duration + if self.settings.cycle.lethal_poison == 'dp': attacks_per_second['deadly_instant_poison'] = poison_procs - attacks_per_second['deadly_poison'] = 1. / 3 * proc_multiplier - elif self.settings.dmg_poison == 'wp': - attacks_per_second['wound_poison'] = total_hits_per_second * avg_poison_proc_rate + attacks_per_second['deadly_poison'] = 1 / 3 + elif self.settings.cycle.lethal_poison == 'wp': + attacks_per_second['wound_poison'] = poison_procs + + def get_average_alacrity(self, attacks_per_second): + stacks_per_second = 0.0 + for finisher in self.finisher_damage_sources: + #Don't double count DfA + if finisher in attacks_per_second and finisher != 'death_from_above_pulse': + for cp in range(7): + stacks_per_second += 0.2 * cp * attacks_per_second[finisher][cp] + stack_time = 10 / stacks_per_second + if stack_time > self.settings.duration: + max_stacks = self.settings.duration * stacks_per_second + return max_stacks / 2 + else: + max_time = self.settings.duration - stack_time + return (max_time / self.settings.duration) * 10 + (stack_time / self.settings.duration) * 5 def determine_stats(self, attack_counts_function): current_stats = { - 'str': self.base_strength, + 'str': self.base_stats['str'] * self.stat_multipliers['str'], + 'int': self.base_stats['int'] * self.stat_multipliers['int'], 'agi': self.base_stats['agi'] * self.stat_multipliers['agi'], 'ap': self.base_stats['ap'] * self.stat_multipliers['ap'], 'crit': self.base_stats['crit'] * self.stat_multipliers['crit'], 'haste': self.base_stats['haste'] * self.stat_multipliers['haste'], 'mastery': self.base_stats['mastery'] * self.stat_multipliers['mastery'], - 'readiness': self.base_stats['readiness'] * self.stat_multipliers['readiness'], - 'multistrike': self.base_stats['multistrike'] * self.stat_multipliers['multistrike'], 'versatility': self.base_stats['versatility'] * self.stat_multipliers['versatility'], } self.current_variables = {} - + #arrys to store different types of procs active_procs_rppm_stat_mods = [] active_procs_rppm = [] @@ -597,21 +643,7 @@ def determine_stats(self, attack_counts_function): active_procs_no_icd = [] damage_procs = [] weapon_damage_procs = [] - - shatt_hand = 0 - for hand in ('mh', 'oh'): - if getattr(getattr(self.stats, hand), 'mark_of_the_shattered_hand'): - self.stats.procs.set_proc('mark_of_the_shattered_hand_dot') #this enables the proc if it's not active, doesn't duplicate - shatt_hand += 1 - if shatt_hand > 0: - if shatt_hand > 1: - getattr(self.stats.procs, 'mark_of_the_shattered_hand_dot').proc_rate = 5 - else: - getattr(self.stats.procs, 'mark_of_the_shattered_hand_dot').proc_rate = 2.5 - self.set_rppm_uptime(getattr(self.stats.procs, 'mark_of_the_shattered_hand_dot')) - if not shatt_hand: - self.stats.procs.del_proc('mark_of_the_shattered_hand_dot') - + #sort the procs into groups for proc in self.stats.procs.get_all_procs_for_stat(): if (proc.stat == 'stats'): @@ -624,30 +656,11 @@ def determine_stats(self, attack_counts_function): active_procs_no_icd.append(proc) elif proc.stat == 'stats_modifier': active_procs_rppm_stat_mods.append(proc) - elif proc.stat in ('spell_damage', 'physical_damage', 'physical_dot'): + elif proc.stat in ('spell_damage', 'physical_damage', 'physical_dot', 'spell_dot'): damage_procs.append(proc) elif proc.stat == 'extra_weapon_damage': weapon_damage_procs.append(proc) - - #calculate weapon procs - weapon_enchants = set([]) - for hand, enchant in [(x, y) for x in ('mh', 'oh') for y in ('dancing_steel', 'mark_of_the_frostwolf', - 'mark_of_the_shattered_hand', 'mark_of_the_thunderlord', - 'mark_of_the_bleeding_hollow', 'mark_of_warsong')]: - proc = getattr(getattr(self.stats, hand), enchant) - if proc: - setattr(proc, '_'.join((hand, 'only')), True) - if (proc.stat in current_stats or proc.stat == 'stats'): - if proc.is_real_ppm(): - active_procs_rppm.append(proc) - else: - if proc.icd: - active_procs_icd.append(proc) - else: - active_procs_no_icd.append(proc) - elif enchant in ('mark_of_the_shattered_hand', ): - damage_procs.append(proc) - + static_proc_stats = { 'str': 0, 'agi': 0, @@ -655,11 +668,9 @@ def determine_stats(self, attack_counts_function): 'crit': 0, 'haste': 0, 'mastery': 0, - 'readiness': 0, - 'multistrike': 0, 'versatility': 0, } - + for proc in active_procs_rppm_stat_mods: self.set_rppm_uptime(proc) for e in proc.value: @@ -671,13 +682,18 @@ def determine_stats(self, attack_counts_function): self.set_rppm_uptime(proc) for e in proc.value: static_proc_stats[ e ] += proc.uptime * proc.value[e] * self.stat_multipliers[e] - + + # Cradle of Anguish, special hanling + if self.stats.procs.cradle_of_anguish: + static_proc_stats['agi'] += self.stats.procs.cradle_of_anguish.value['agi'] * self.stats.procs.cradle_of_anguish.max_stacks + for k in static_proc_stats: current_stats[k] += static_proc_stats[ k ] - + attacks_per_second, crit_rates, additional_info = attack_counts_function(current_stats) + self.add_special_aps_penalties(attacks_per_second) recalculate_crit = False - + #check need to converge need_converge = False convergence_stats = False @@ -685,897 +701,1251 @@ def determine_stats(self, attack_counts_function): need_converge = True while (need_converge or self.spec_needs_converge): current_stats = { - 'str': self.base_strength, + 'str': self.base_stats['str'] * self.stat_multipliers['str'], + 'int': self.base_stats['int'] * self.stat_multipliers['int'], 'agi': self.base_stats['agi'] * self.stat_multipliers['agi'], 'ap': self.base_stats['ap'] * self.stat_multipliers['ap'], 'crit': self.base_stats['crit'] * self.stat_multipliers['crit'], 'haste': self.base_stats['haste'] * self.stat_multipliers['haste'], 'mastery': self.base_stats['mastery'] * self.stat_multipliers['mastery'], - 'readiness': self.base_stats['readiness'] * self.stat_multipliers['readiness'], - 'multistrike': self.base_stats['multistrike'] * self.stat_multipliers['multistrike'], 'versatility': self.base_stats['versatility'] * self.stat_multipliers['versatility'], } for k in static_proc_stats: current_stats[k] += static_proc_stats[k] - + for proc in active_procs_no_icd: self.set_uptime(proc, attacks_per_second, crit_rates) for e in proc.value: - if e in self.spec_convergence_stats: - convergence_stats = True + convergence_stats = True if e == 'crit': recalculate_crit = True current_stats[ e ] += proc.uptime * proc.value[e] * self.stat_multipliers[e] - + #only have to converge with specific procs - #check if... assassination:crit/haste, combat:mastery/haste, sub:haste/mastery + #check if... assassination:crit/haste, outlaw:mastery/haste, sub:haste/mastery if not convergence_stats and not self.spec_needs_converge: break - + old_attacks_per_second = attacks_per_second if recalculate_crit: crit_rates = None recalculate_crit = False attacks_per_second, crit_rates, additional_info = attack_counts_function(current_stats, crit_rates=crit_rates) - + self.add_special_aps_penalties(attacks_per_second) + if self.are_close_enough(old_attacks_per_second, attacks_per_second): break - + for proc in active_procs_icd: self.set_uptime(proc, attacks_per_second, crit_rates) for e in proc.value: if e == 'crit': recalculate_crit = True current_stats[ e ] += proc.uptime * proc.value[e] * self.stat_multipliers[e] - + #if no new stats are added, skip this step if len(active_procs_icd) > 0 or self.spec_needs_converge: if recalculate_crit: crit_rates = None attacks_per_second, crit_rates, additional_info = attack_counts_function(current_stats, crit_rates=crit_rates) + self.add_special_aps_penalties(attacks_per_second) - # the t16 4pc do not need to be in the main loop because mastery for assa is just increased damage - # and has no impact on the cycle - if self.stats.gear_buffs.rogue_t16_4pc_bonus() and self.settings.is_assassination_rogue(): - #20 stacks of 250 mastery, lasts 5 seconds - mas_per_stack = 38. - max_stacks = 20. - buff_duration = 5. - extra_duration = buff_duration - self.settings.response_time - ability_aps = 0 - mutilate_aps = 0 - for key in ('mutilate', 'dispatch', 'envenom'): - if key in attacks_per_second: - if key in ('envenom'): - ability_aps += sum(attacks_per_second[key]) - elif key == 'mutilate': - ability_aps += attacks_per_second[key] - mutilate_aps += attacks_per_second[key] - else: - ability_aps += attacks_per_second[key] - attack_spacing = 1 / ability_aps - # mutilate gives 2 stacks, so it needs to be included - avg_stacks_per_attack = 1 + mutilate_aps / ability_aps - res = 0. - if attack_spacing < 5: - time_to_max = max_stacks * attack_spacing / avg_stacks_per_attack - time_at_max = max(0., self.vendetta_duration - time_to_max) - max_stacks_able_to_reach = min(self.vendetta_duration / attack_spacing, max_stacks) - avg_stacks = max_stacks_able_to_reach / 2 - avg = time_to_max * avg_stacks + time_at_max * max_stacks + extra_duration * max_stacks_able_to_reach - res = avg * mas_per_stack / self.get_spell_cd('vendetta') - else: - uptime = buff_duration / attack_spacing - res = self.vendetta_duration * uptime * mas_per_stack * avg_stacks_per_attack / self.get_spell_cd('vendetta') - current_stats['mastery'] += res - #some procs need specific prep, think RoRO/VoS self.setup_unique_procs(current_stats, current_stats['agi']+current_stats['ap']) - + for proc in damage_procs: self.update_with_damaging_proc(proc, attacks_per_second, crit_rates) - + for proc in weapon_damage_procs: self.set_uptime(proc, attacks_per_second, crit_rates) + + #Mantle of the Master Assassin Legendary + if self.stats.gear_buffs.mantle_of_the_master_assassin: + mantle_triggers = 1 #Opener + if attacks_per_second['vanish']: + mantle_triggers += attacks_per_second['vanish'] * self.settings.duration + mantle_seconds = mantle_triggers * 5 + self.mantle_uptime = mantle_seconds / self.settings.duration + for attack in crit_rates: + crit_rates[attack] = min(crit_rates[attack] * (1. - self.mantle_uptime) + self.mantle_uptime, 1) + return current_stats, attacks_per_second, crit_rates, damage_procs, additional_info - + + def add_special_aps_penalties(self, attacks_per_second): + #Draught of Souls Trinket, 3s ability downtime per use + dos = self.stats.procs.draught_of_souls + if dos: + lost_seconds = self.settings.duration * float(dos.duration) / float(dos.icd) + loss_ratio = (self.settings.duration - lost_seconds) / self.settings.duration + for attack in attacks_per_second: + if attack not in ['mh_autoattacks', 'oh_autoattacks', 'shadow_blades', 'nightblade_ticks', + 'rupture_ticks', 'from_the_shadows', 'kingsbane_ticks', 'garrote_ticks', 'deadly_poison']: + if isinstance(attacks_per_second[attack], list): + for i in range(len(attacks_per_second[attack])): + attacks_per_second[attack][i] *= loss_ratio + else: + attacks_per_second[attack] *= loss_ratio + def compute_damage_from_aps(self, current_stats, attacks_per_second, crit_rates, damage_procs, additional_info): # this method exists solely to let us use cached values you would get from determine stats - # really only useful for combat calculations (restless blades calculations) + # really only useful for outlaw calculations (restless blades calculations) damage_breakdown, additional_info = self.get_damage_breakdown(current_stats, attacks_per_second, crit_rates, damage_procs, additional_info) return damage_breakdown, additional_info - + def compute_damage(self, attack_counts_function): current_stats, attacks_per_second, crit_rates, damage_procs, additional_info = self.determine_stats(attack_counts_function) damage_breakdown, additional_info = self.get_damage_breakdown(current_stats, attacks_per_second, crit_rates, damage_procs, additional_info) #damage_breakdown, additional_info = self.get_damage_breakdown(self.determine_stats(attack_counts_function)) return damage_breakdown, additional_info - + + def compute_insignia_of_ravenholdt_damage(self, stats, damage_breakdown): + # Insignia of Ravenholdt, 30% (Assassination) / 15% generator damage with crit chance + insignia_base_dmg = 0 + insignia_dmg_factor = 0.3 if self.spec == 'assassination' else 0.15 + # Ignores Vendetta and Nightblade modifiers + if self.spec == 'assassination': + insignia_dmg_factor /= 1 + self.vendetta_multiplier + if self.spec == 'subtlety': + insignia_dmg_factor /= 1.15 + for ability in damage_breakdown: + if ability in ['mutilate', 'hemorrhage', + 'ambush', 'blunderbuss', 'pistol_shot', 'saber_slash', + 'backstab', 'gloomblade', 'shadowstrike']: + # For physical generators we assume an additional 4.38% damage. (See https://github.com/Ravenholdt-TC/Rogue/issues/50) + physical_mod = 1.0438 if ability in self.physical_damage_sources else 1 + insignia_base_dmg += insignia_dmg_factor * physical_mod * damage_breakdown[ability] + crit_rate = self.crit_rate(crit=stats['crit']) + crit_mod = self.crit_damage_modifiers() + insignia_dmg = insignia_base_dmg * (1 - crit_rate) + insignia_base_dmg * crit_rate * crit_mod + + # Also hits adds within 15yd in front + if self.settings.num_boss_adds: + insignia_dmg *= 1 + self.settings.num_boss_adds + return insignia_dmg + + def compute_symbiote_strike_damage(self, damage_breakdown): + # Cinidaria's Symbiote Strike is plain 30% of all damage we actually do + # Assume it's up for 10% of the fight + return sum(damage_breakdown.values()) * 0.03 + ########################################################################### # Assassination DPS functions ########################################################################### - def init_assassination(self): - # Call this before calling any of the assassination_dps functions - # directly. If you're just calling get_dps, you can ignore this as it - # happens automatically; however, if you're going to pull a damage - # breakdown or other sub-result, make sure to call this, as it - # initializes many values that are needed to perform the calculations. - - if not self.settings.is_assassination_rogue(): - raise InputNotModeledException(_('You must specify an assassination cycle to match your assassination spec.')) - if self.stats.mh.type != 'dagger' or self.stats.oh.type != 'dagger': - raise InputNotModeledException(_('Assassination modeling requires daggers in both hands')) - - #set readiness coefficient - self.readiness_spec_conversion = self.assassination_readiness_conversion - self.spec_convergence_stats = ['haste', 'crit', 'readiness'] - - # Assassasins's Resolve - self.damage_modifier_cache = 1.17 - - #update spec specific proc rates - if getattr(self.stats.procs, 'legendary_capacitive_meta'): - getattr(self.stats.procs, 'legendary_capacitive_meta').proc_rate_modifier = 1.789 - if getattr(self.stats.procs, 'fury_of_xuen'): - getattr(self.stats.procs, 'fury_of_xuen').proc_rate_modifier = 1.55 - - #spec specific glyph behaviour - if self.glyphs.disappearance: - self.ability_cds['vanish'] = 60 - else: - self.ability_cds['vanish'] = 120 - - self.base_energy_regen = 10 - self.max_energy = 120. - if self.stats.gear_buffs.rogue_pvp_4pc_extra_energy(): - self.max_energy += 30 - if self.talents.lemon_zest: - self.base_energy_regen *= 1 + .05 * (1 + min(self.settings.num_boss_adds, 2)) - self.max_energy += 15 - if self.glyphs.energy: - self.max_energy += 20 - if self.race.expansive_mind: - self.max_energy = round(self.max_energy * 1.05, 0) - - self.set_constants() - self.stat_multipliers['mastery'] *= 1.05 + #Legion TODO: - self.vendetta_duration = 20 + 10 * self.glyphs.vendetta - self.vendetta_uptime = self.vendetta_duration / (self.get_spell_cd('vendetta') + self.settings.response_time + self.major_cd_delay) - self.vendetta_multiplier = .3 - .05 * self.glyphs.vendetta - self.vendetta_mult = 1 + self.vendetta_multiplier * self.vendetta_uptime + #Artifact: + # 'poison_knives' def assassination_dps_estimate(self): - non_execute_dps = self.assassination_dps_estimate_non_execute() * (1 - self.settings.time_in_execute_range) - execute_dps = self.assassination_dps_estimate_execute() * self.settings.time_in_execute_range - return non_execute_dps + execute_dps + return sum(self.assassination_dps_breakdown().values()) - def assassination_dps_estimate_execute(self): - return sum(self.assassination_dps_breakdown_execute().values()) + def assassination_dps_breakdown(self): + if not self.spec == 'assassination': + raise InputNotModeledException(_('You must specify a assassination cycle to match your assassination spec.')) + + #assassination specific constants + #set up damage modifier list and all relevant modifiers, use None for placeholder values + self.damage_modifiers = modifiers.ModifierList(self.assassination_damage_sources + ['autoattacks']) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('versatility', None, [], all_damage=True)) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('armor', self.armor_mitigation_multiplier(), ['death_from_above_pulse', + 'fan_of_knives', 'hemorrhage', 'mutilate', 'poisoned_knife', 'autoattacks'], dmg_schools=['physical'])) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('potent_poisons', None, ['deadly_poison', + 'deadly_instant_poison', 'wound_poison', 'envenom', 'poison_bomb', 'kingsbane', 'kingsbane_ticks', 'toxic_blade'])) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('assassins_resolve', 1.17, [], all_damage=True)) + + #Generic tuning aura + self.damage_modifiers.register_modifier(modifiers.DamageModifier('assassination_aura', 1.28, ['death_from_above_pulse', 'death_from_above_strike', + 'deadly_poison', 'deadly_instant_poison', 'envenom', 'fan_of_knives', 'garrote_ticks', 'hemorrhage', + 'kingsbane', 'kingsbane_ticks', 'mutilate', 'poisoned_knife', 'rupture_ticks', 'toxic_blade'])) + + #time averaged vendetta modifier used for most things + self.damage_modifiers.register_modifier(modifiers.DamageModifier('vendetta_time_average', None, ['garrote_ticks', 'mutilate', 'deadly_poison', 'deadly_instant_poison', + 'wound_poison', 'hemorrhage', 'envenom', 'fan_of_knives', 'death_from_above_pulse', 'poisoned_knife', 'from_the_shadows', 'poison_bomb', 'toxic_blade'])) + + self.damage_modifiers.register_modifier(modifiers.DamageModifier('vendetta_exsang', None, ['rupture_ticks'])) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('vendetta_kb', None, ['kingsbane', 'kingsbane_ticks'])) + + if self.talents.toxic_blade: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('toxic_blade', None, ['deadly_poison', 'deadly_instant_poison','wound_poison', 'envenom', + 'from_the_shadows', 'poison_bomb', 'kingsbane', 'kingsbane_ticks', 'toxic_blade'])) + + #talent specific modifiers + if self.talents.elaborate_planning: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('elaborate_planning', None, [], all_damage=True)) + if self.talents.hemorrhage: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('hemorrhage', 1.25, ['rupture_ticks', 'garrote_ticks'])) + if self.talents.nightstalker: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('nightstalker', None, ['rupture_ticks'])) + if self.talents.subterfuge: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('subterfuge_garrote', None, ['garrote_ticks'])) + if self.talents.deeper_stratagem: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('deeper_stratagem', 1.05, ['rupture_ticks', 'envenom', 'death_from_above_pulse', 'death_from_above_strike'])) + + #trait specific modifiers + if self.traits.kingsbane: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('kingsbane_tick_increase', None, ['kingsbane_ticks'])) + if self.traits.blood_of_the_assassinated: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('blood_of_the_assassinated', None, ['rupture_ticks'])) + if self.traits.surge_of_toxins: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('surge_of_toxins', None, ['deadly_poison', + 'deadly_instant_poison', 'wound_poison', 'envenom', 'poison_bomb'], dmg_schools=['poison'])) + + if self.traits.slayers_precision: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('slayers_precision', + 1.05 + (0.005 * (self.traits.slayers_precision - 1)), [], all_damage=True)) + + if self.traits.silence_of_the_uncrowned: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('silence_of_the_uncrowned', 1.1, [], all_damage=True)) + + #gear specific modifiers + if self.stats.gear_buffs.the_dreadlords_deceit: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('the_dreadlords_deceit', None, ['fan_of_knives'])) + + if self.stats.gear_buffs.zoldyck_family_training_shackles: + #Assume spend 30% of the time sub 30% health, imperfect but good enough + self.damage_modifiers.register_modifier(modifiers.DamageModifier('zoldyck_family_training_shackles', 1.09, ['deadly_poison', 'deadly_instant_poison', + 'garrote_ticks', 'kingsbane', 'kingsbane_ticks', 'rupture_ticks', 'poison_bomb', 'wound_poison'], dmg_schools=['poison', 'bleed'])) + + if self.stats.gear_buffs.jeweled_signet_of_melandrus: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('jeweled_signet_of_melandrus', 1.1, ['autoattacks'])) + + if self.stats.gear_buffs.gnawed_thumb_ring: + gtr_mod = 1 + 0.05 * 12 / 180 + self.damage_modifiers.register_modifier(modifiers.DamageModifier('gnawed_thumb_ring', gtr_mod, + ['deadly_poison', 'deadly_instant_poison', 'envenom', 'kingsbane', 'kingsbane_ticks', + 'poison_bomb', 'from_the_shadows', 'wound_poison'], + dmg_schools=['arcane', 'fire', 'frost', 'holy', 'nature', 'shadow'])) + + if self.stats.gear_buffs.rogue_t20_2pc: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('t20_2pc', 1.4, ['garrote_ticks'])) + + #Assume 100% uptime of Rupture, Garrote and Mutilated Flesh (2pc bleed) + if self.stats.gear_buffs.rogue_t19_4pc: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('t19_4pc', 1.21, ['envenom'])) - def assassination_dps_estimate_non_execute(self): - return sum(self.assassination_dps_breakdown_non_execute().values()) + self.set_constants() - def assassination_dps_breakdown(self): - non_execute_dps_breakdown = self.assassination_dps_breakdown_non_execute() - execute_dps_breakdown = self.assassination_dps_breakdown_execute() + stats, aps, crits, procs, additional_info = self.determine_stats(self.assassination_attack_counts) - non_execute_weight = 1 - self.settings.time_in_execute_range - execute_weight = self.settings.time_in_execute_range + self.vendetta_multiplier = 0.3 * (20 / self.vendetta_cd) + if self.settings.cycle.kingsbane_with_vendetta == 'just': + kb_venn_uptime = min(1, self.kingsbane_cd / self.vendetta_cd) + else: + kb_venn_uptime = 1.0 + if self.settings.cycle.exsang_with_vendetta == 'just': + exsang_venn_uptime = min(1, self.exsang_cd / self.vendetta_cd) + else: + exsang_venn_uptime = 1.0 + self.damage_modifiers.update_modifier_value('vendetta_time_average', 1 + self.vendetta_multiplier) + self.damage_modifiers.update_modifier_value('vendetta_exsang', 1 + (self.vendetta_multiplier * exsang_venn_uptime)) + self.damage_modifiers.update_modifier_value('vendetta_kb', 1 + (self.vendetta_multiplier * kb_venn_uptime)) + + if self.talents.toxic_blade: + tb_debuff_multiplier = 0.35 * (9 / self.get_spell_cd('toxic_blade')) + self.damage_modifiers.update_modifier_value('toxic_blade', 1 + tb_debuff_multiplier) + + self.damage_modifiers.update_modifier_value('versatility', self.stats.get_versatility_multiplier_from_rating(rating=stats['versatility'])) + self.damage_modifiers.update_modifier_value('potent_poisons', (1 + self.assassination_mastery_conversion * self.stats.get_mastery_from_rating(stats['mastery']))) + + #Lethal poison applications increase kingsbane damage by 15% each, KB ticks 7 times every 2 sec + if self.traits.kingsbane: + poison_aps = 0 + if self.settings.cycle.lethal_poison == 'dp': + poison_aps = aps['deadly_instant_poison'] + elif self.settings.cycle.lethal_poison == 'wp': + poison_aps = aps['wound_poison'] + applications_per_tick = 2 * poison_aps + average_kb_stacks = (applications_per_tick + applications_per_tick * 7) / 2 + self.damage_modifiers.update_modifier_value('kingsbane_tick_increase', 1 + (average_kb_stacks * 0.15)) + + if self.traits.blood_of_the_assassinated: + bota_uptime = 0.35 * sum(aps['rupture']) * 10 # procs/ability * ability/second * seconds/proc gives unit-less uptime + bota_multiplier = 1 + 2 * bota_uptime + self.damage_modifiers.update_modifier_value('blood_of_the_assassinated', bota_multiplier) + + finisher_aps = 0.0 + for ability in aps: + if ability in self.finisher_damage_sources and 'ticks' not in ability: + finisher_aps += sum(aps[ability]) + + #actually 2% per cp up to max of 5 + surge_of_toxins_multiplier = 1. + surge_of_toxins_ap_multiplier = 1 + if self.traits.surge_of_toxins: + finisher_cpps = 0.0 #finisher cps per second + for ability in aps: + if ability in self.finisher_damage_sources and 'ticks' not in ability: + finisher_cpps += sum([min(cp, 5) * aps[ability][cp] for cp in range(len(aps[ability]))]) + surge_uptime = finisher_aps * 5 #attacks/second * seconds/attack + surge_of_toxins_multiplier = 1. + ((0.02 * finisher_cpps) * surge_uptime) + surge_of_toxins_ap_multiplier = 1. + ((0.01 * finisher_cpps) * surge_uptime) + self.damage_modifiers.update_modifier_value('surge_of_toxins', surge_of_toxins_multiplier) + + if self.talents.elaborate_planning: + ep_uptime = finisher_aps * 5 #attacks/second * seconds/attack + self.damage_modifiers.update_modifier_value('elaborate_planning', 1 + (0.12 * ep_uptime)) + + if self.talents.nightstalker: + #Assume we use nightstalker for snapshotting Rupture + ns_rupture_uptime = aps['vanish'] / sum(aps['rupture']) + self.damage_modifiers.update_modifier_value('nightstalker', 1 + (0.5 * ns_rupture_uptime)) - dps_breakdown = {} - for source, quantity in non_execute_dps_breakdown.items(): - dps_breakdown[source] = quantity * non_execute_weight + if self.talents.subterfuge: + #Get modifier for buffed garrotes from Subterfuge, including opener + subterfuge_garrote_uptime = (1 / self.settings.duration + aps['vanish']) / aps['garrote'] + self.damage_modifiers.update_modifier_value('subterfuge_garrote', 1 + (1.25 * subterfuge_garrote_uptime)) - for source, quantity in execute_dps_breakdown.items(): - if source in dps_breakdown: - dps_breakdown[source] += quantity * execute_weight - else: - dps_breakdown[source] = quantity * execute_weight - - return dps_breakdown - - def update_assassination_breakdown_with_modifiers(self, damage_breakdown, current_stats): - #calculate multistrike here for Sub and Assassination, really cheap to calculate - #turns out the 2 chance system yields a very basic linear pattern, the damage modifier is 30% of the multistrike %! - multistrike_multiplier = .3 * 2 * (self.stats.get_multistrike_chance_from_rating(rating=current_stats['multistrike']) + self.buffs.multistrike_bonus()) - multistrike_multiplier = min(.6, multistrike_multiplier) - - for key in damage_breakdown: - damage_breakdown[key] *= 1 + multistrike_multiplier - if ('sr_' not in key): - damage_breakdown[key] *= self.vendetta_mult - elif 'sr_' in key: - damage_breakdown[key] *= 1 + self.vendetta_multiplier - if self.level == 100 and key in ('mutilate', 'dispatch', 'sr_mutilate', 'sr_mh_mutilate', 'sr_oh_mutilate', 'sr_dispatch'): - damage_breakdown[key] *= self.emp_envenom_percentage - - def assassination_dps_breakdown_non_execute(self): - #damage_breakdown, additional_info = self.compute_damage(self.assassination_attack_counts_non_execute) - current_stats, attacks_per_second, crit_rates, damage_procs, additional_info = self.determine_stats(self.assassination_attack_counts_non_execute) - damage_breakdown, additional_info = self.get_damage_breakdown(current_stats, attacks_per_second, crit_rates, damage_procs, additional_info) - - self.update_assassination_breakdown_with_modifiers(damage_breakdown, current_stats) - return damage_breakdown + if self.stats.gear_buffs.the_dreadlords_deceit: + avg_dreadlord_stacks = 0.5 / aps['fan_of_knives'] + self.damage_modifiers.update_modifier_value('the_dreadlords_deceit', 1 + (0.25 * avg_dreadlord_stacks)) + + if self.stats.gear_buffs.rogue_t19_4pc: + if aps['mutilate'] < 0.125: + t19_4pc_multiplier = 0.07 * (aps['mutilate'] / 0.125) + self.damage_modifiers.update_modifier_value('t19_4pc', 1.14 + t19_4pc_multiplier) + + damage_breakdown, additional_info = self.compute_damage_from_aps(stats, aps, crits, procs, additional_info) + + if self.stats.gear_buffs.rogue_t19_2pc: + # To prevent double dipping this is based on actual Mutilate damage. + # There's no pandemic and it does not respect other modifiers. + # Remaining damage is added on refresh. + damage_breakdown['t19_2pc'] = damage_breakdown['mutilate'] * 0.2 + + if self.stats.gear_buffs.insignia_of_ravenholdt: + damage_breakdown['insignia_of_ravenholdt'] = self.compute_insignia_of_ravenholdt_damage(stats, damage_breakdown) + + if self.stats.gear_buffs.cinidaria_the_symbiote: + damage_breakdown['symbiote_strike'] = self.compute_symbiote_strike_damage(damage_breakdown) - def assassination_dps_breakdown_execute(self): - #damage_breakdown, additional_info = self.compute_damage(self.assassination_attack_counts_execute) - current_stats, attacks_per_second, crit_rates, damage_procs, additional_info = self.determine_stats(self.assassination_attack_counts_execute) - damage_breakdown, additional_info = self.get_damage_breakdown(current_stats, attacks_per_second, crit_rates, damage_procs, additional_info) - - self.update_assassination_breakdown_with_modifiers(damage_breakdown, current_stats) return damage_breakdown - - def assassination_cp_distribution_for_finisher(self, current_cp, crit_rates, ability_count, size_breakdown, cp_limit=4, blindside_proc=0, execute=False): - current_sizes = copy(size_breakdown) - if (current_cp >= cp_limit and not blindside_proc and not execute) or current_cp >= 5: - final_cp = min(current_cp, 5) - current_sizes[final_cp] += 1 - return final_cp, blindside_proc, ability_count, current_sizes - avg_count = {'mutilate':0, 'dispatch':0} - avg_breakdown = [0,0,0,0,0,0] - new_count = copy(ability_count) - - if blindside_proc or execute: - new_count['dispatch'] += 1 - - n_chance = 1 - crit_rates['dispatch'] - n_value, n_proc, n_count, n_breakdown = self.assassination_cp_distribution_for_finisher(current_cp+1, crit_rates, new_count, current_sizes, cp_limit=cp_limit, execute=execute) - c_chance = crit_rates['dispatch'] - c_value, c_proc, c_count, c_breakdown = self.assassination_cp_distribution_for_finisher(current_cp+2, crit_rates, new_count, current_sizes, cp_limit=cp_limit, execute=execute) - - avg_cp = n_chance*n_value + c_chance*c_value - avg_bs_afterwards = n_chance*n_proc + c_chance*c_proc - for key in new_count: - avg_count[key] = n_chance*n_count[key] + c_chance*c_count[key] - for i in xrange(1, 6): - avg_breakdown[i] = n_chance*n_breakdown[i] + c_chance*c_breakdown[i] - return avg_cp, avg_bs_afterwards, avg_count, avg_breakdown - else: - bs_proc_rate = .3 - new_count['mutilate'] += 1 - - n_chance = ((1 - crit_rates['mutilate']) ** 2) * (1-bs_proc_rate) - n_value, n_proc, n_count, n_breakdown = self.assassination_cp_distribution_for_finisher(current_cp+2, crit_rates, new_count, current_sizes, cp_limit=cp_limit) - n_bs_chance = ((1 - crit_rates['mutilate']) ** 2) * bs_proc_rate - n_bs_value, n_bs_proc, n_bs_count, n_bs_breakdown = self.assassination_cp_distribution_for_finisher(current_cp+2, crit_rates, new_count, current_sizes, cp_limit=cp_limit, blindside_proc=1.) - - c_chance = (1 - (1 - crit_rates['mutilate']) ** 2) * (1-bs_proc_rate) - c_value, c_proc, c_count, c_breakdown = self.assassination_cp_distribution_for_finisher(current_cp+3, crit_rates, new_count, current_sizes, cp_limit=cp_limit) - c_bs_chance = (1 - (1 - crit_rates['mutilate']) ** 2) * bs_proc_rate - c_bs_value, c_bs_proc, c_bs_count, c_bs_breakdown = self.assassination_cp_distribution_for_finisher(current_cp+3, crit_rates, new_count, current_sizes, cp_limit=cp_limit, blindside_proc=1.) - - avg_cp = n_chance*n_value + n_bs_chance*n_bs_value + c_chance*c_value + c_bs_chance*c_bs_value - avg_bs_afterwards = n_chance*n_proc + n_bs_chance*n_bs_proc + c_chance*c_proc + c_bs_chance*c_bs_proc - for key in new_count: - avg_count[key] = n_chance*n_count[key] + n_bs_chance*n_bs_count[key] + c_chance*c_count[key] + c_bs_chance*c_bs_count[key] - for i in xrange(1, 6): - avg_breakdown[i] = n_chance*n_breakdown[i] + n_bs_chance*n_bs_breakdown[i] + c_chance*c_breakdown[i] + c_bs_chance*c_bs_breakdown[i] - return avg_cp, avg_bs_afterwards, avg_count, avg_breakdown - - def assassination_attack_counts(self, current_stats, cpg, finisher_size, crit_rates=None): + + def assassination_attack_counts(self, current_stats, crit_rates=None): attacks_per_second = {} additional_info = {} - #can't rely on a cache, due to the Cold Blood perk - crit_rates = self.get_crit_rates(current_stats) - - haste_multiplier = self.stats.get_haste_multiplier_from_rating(current_stats['haste']) * self.true_haste_mod - ability_cost_modifier = self.stats.gear_buffs.rogue_t15_4pc_reduced_cost() - - energy_regen = self.base_energy_regen * haste_multiplier - if self.stats.gear_buffs.rogue_t17_4pc_lfr: - #http://www.wolframalpha.com/input/?i=1.1307+*+%281+-+e+**+%28-1+*+1.1+*+6%2F+60%29%29 - #https://twitter.com/Celestalon/status/525350819856535552 - energy_regen *= 1 + (.11778034322021550695 * .3) #11% uptime on 30% boost) - energy_regen += self.bonus_energy_regen - if cpg == 'dispatch': - #this is for the effects of pooling going into execute phase - energy_regen += (self.max_energy - 10) / (self.settings.duration * self.settings.time_in_execute_range) - - vw_energy_return = 10 - vw_energy_per_bleed_tick = vw_energy_return - - blindside_proc_rate = [0, .3][cpg == 'mutilate'] - attacks_per_second['envenom'] = [0,0,0,0,0,0] - attacks_per_second['dispatch'] = 0 - - if self.talents.marked_for_death: - energy_regen -= 10. / self.get_spell_cd('marked_for_death') # 35-25 - - attack_speed_multiplier = self.base_speed_multiplier * haste_multiplier - self.attack_speed_increase = attack_speed_multiplier - - seal_fate_proc_rate = crit_rates['dispatch'] - if cpg == 'mutilate': - seal_fate_proc_rate *= blindside_proc_rate - seal_fate_proc_rate += 1 - (1 - crit_rates['mutilate']) ** 2 - - mutilate_cps = 3 - (1 - crit_rates['mutilate']) ** 2 # 1 - (1 - crit_rates['mutilate']) ** 2 is the Seal Fate CP - dispatch_cps = 1 + crit_rates['dispatch'] - if self.talents.anticipation: - avg_finisher_size = 5 - avg_size_breakdown = [0,0,0,0,0,1.] #this is for determining the % likelyhood of sizes, not frequency of the sizes - cp_needed_per_finisher = 5 - if self.stats.gear_buffs.rogue_t17_4pc: - cp_needed_per_finisher -= 1 - - if cpg == 'mutilate': - avg_cp_per_cpg = mutilate_cps + dispatch_cps * blindside_proc_rate + + if crit_rates == None: + crit_rates = self.get_crit_rates(current_stats) + + #Vendetta cd, modified by Duskwalker Legendary, used for damage modifier + self.vendetta_cd = self.get_spell_cd('vendetta') + + self.kingsbane_cd = self.get_spell_cd('kingsbane') + self.exsang_cd = self.get_spell_cd('exsanguinate') + + #convergence loop + old_aps = {} + for assa_loop in range(6): + if assa_loop >= 5: + raise ConvergenceErrorException(_('Assassination aps failed to converge.')) + + #cd stacking handlers + if self.settings.cycle.kingsbane_with_vendetta == 'only': + self.kingsbane_cd = max(self.vendetta_cd, self.kingsbane_cd) + if self.settings.cycle.exsang_with_vendetta == 'only': + self.exsang_cd = max(self.vendetta_cd, self.exsang_cd) + + #Vanish on cooldown + attacks_per_second['vanish'] = 1 / self.get_spell_cd('vanish') + + # set up our finisher distributions + #unlike outlaw these depend on gear (crit) so they cannot be precomputed + self.cp_builder = self.settings.cycle.cp_builder + cp_builder_crit = crit_rates[self.cp_builder] + if self.cp_builder == 'mutilate': + cpg_cps = {2: (1 - cp_builder_crit) ** 2, + 3: 2 * (1 - cp_builder_crit) * cp_builder_crit, + 4: cp_builder_crit ** 2} + elif self.cp_builder == 'fan_of_knives': + raise InputNotModeledException(_('Fan of Knives cp builder unimplemented')) else: - avg_cp_per_cpg = dispatch_cps - - avg_cpgs_per_finisher = cp_needed_per_finisher / avg_cp_per_cpg - else: - ability_count = {'mutilate':0, 'dispatch':0} - finisher_size_breakdown = [0,0,0,0,0,0] - - #This is incredibly verbose, but functional. It exhaustively calculates the potential finisher size outcomes using recursion. - #avg_finisher_size - measures average finisher size - #avg_bs_afterwards - likelyhood of finishing with a blindside proc active - #avg_count - number of ability casts per finisher (dictionary of both Mutilate and Dispatch) - #avg_breakdown - frequency of finisher sizes (should sum to 100% or 1) - execute = False - base_cp = 0 - min_finisher_size = self.settings.cycle.min_envenom_size_non_execute - if cpg == 'dispatch': - min_finisher_size = self.settings.cycle.min_envenom_size_execute - execute = True - if self.stats.gear_buffs.rogue_t17_4pc: - base_cp = 1 - avg_finisher_size, avg_bs, avg_count, avg_size_breakdown = self.assassination_cp_distribution_for_finisher(base_cp, crit_rates, - ability_count, finisher_size_breakdown, cp_limit=min_finisher_size, execute=execute) - if avg_bs > 0: - mut_start_chance = 1/(1+avg_bs) - bs_start_chance = 1 - mut_start_chance - extra_tuple = self.assassination_cp_distribution_for_finisher(base_cp, crit_rates, ability_count, finisher_size_breakdown, cp_limit=4, blindside_proc=1) - - avg_finisher_size = avg_finisher_size*mut_start_chance + extra_tuple[0]*bs_start_chance - for key in ability_count: - avg_count[key] = avg_count[key]*mut_start_chance + extra_tuple[2][key]*bs_start_chance - for i in xrange(1,6): - avg_size_breakdown[i] = avg_size_breakdown[i]*mut_start_chance + extra_tuple[3][i]*bs_start_chance - - avg_cpgs_per_finisher = avg_count[cpg] - avg_cp_per_cpg = avg_finisher_size / avg_cpgs_per_finisher - - cpg_energy_cost = self.get_spell_stats(cpg, cost_mod=ability_cost_modifier)[0] - cpg_cost_reduction = 0 - if self.stats.gear_buffs.rogue_t17_2pc: - cpg_cost_reduction = 14 * crit_rates['mutilate'] #7 per hand, double crit is 2 procs - if self.stats.gear_buffs.rogue_t16_2pc_bonus(): - cpg_cost_reduction = 6 * seal_fate_proc_rate - cpg_energy_cost -= cpg_cost_reduction - - current_opener_name = self.settings.opener_name - if self.settings.opener_name == 'cpg': - current_opener_name = cpg - - cp_generated = 0 - if current_opener_name == 'envenom': - opener_net_cost = self.get_spell_stats('envenom', cost_mod=ability_cost_modifier*(1-self.get_shadow_focus_multiplier()))[0] - energy_regen += opener_net_cost * self.total_openers_per_second - elif current_opener_name == cpg: - opener_net_cost = self.get_spell_stats(current_opener_name, cost_mod=(1-self.get_shadow_focus_multiplier()))[0] - opener_net_cost += cpg_cost_reduction - cp_generated = avg_cp_per_cpg - energy_regen += opener_net_cost * self.total_openers_per_second - else: - opener_net_cost = self.get_spell_stats(current_opener_name, cost_mod=self.get_shadow_focus_multiplier())[0] - attacks_per_second[current_opener_name] = self.total_openers_per_second - if current_opener_name == 'mutilate': - attacks_per_second['dispatch'] += self.total_openers_per_second * blindside_proc_rate - if current_opener_name in ('mutilate', 'dispatch', 'cpg'): - cp_generated = mutilate_cps + dispatch_cps * blindside_proc_rate - elif current_opener_name == 'ambush': - cp_generated = 2 + crit_rates['ambush'] - energy_regen -= opener_net_cost * self.total_openers_per_second - for i in xrange(1,6): - attacks_per_second['envenom'][i] = self.total_openers_per_second * cp_generated / i * avg_size_breakdown[i] - - attacks_per_second['venomous_wounds'] = .5 - energy_regen_with_rupture = energy_regen + .5 * vw_energy_return - - avg_cycle_length = 4. * (1 + avg_finisher_size + self.stats.gear_buffs.rogue_t15_2pc_bonus_cp()) - - energy_for_rupture = avg_cpgs_per_finisher * cpg_energy_cost + self.get_spell_stats('rupture', cost_mod=ability_cost_modifier)[0] - energy_for_rupture -= avg_finisher_size * self.relentless_strikes_energy_return_per_cp - - attacks_per_second['rupture'] = 1. / avg_cycle_length - energy_per_cycle = avg_cycle_length * energy_regen_with_rupture - - energy_for_dfa = 0 - if self.talents.death_from_above: - dfa_cd = self.get_spell_cd('death_from_above') + self.settings.response_time - dfa_cd += energy_for_rupture / (4 * energy_regen_with_rupture) - dfa_interval = 1./dfa_cd - energy_for_dfa = avg_cpgs_per_finisher * cpg_energy_cost + self.get_spell_stats('death_from_above', cost_mod=ability_cost_modifier)[0] - energy_for_dfa -= avg_finisher_size * self.relentless_strikes_energy_return_per_cp - - attacks_per_second['death_from_above'] = dfa_interval - attacks_per_second['death_from_above_strike'] = [finisher_chance * dfa_interval for finisher_chance in avg_size_breakdown] - attacks_per_second['death_from_above_pulse'] = [finisher_chance * dfa_interval * self.settings.num_boss_adds for finisher_chance in avg_size_breakdown] - - #Normalize DfA energy intervals to rupture intervals - energy_for_dfa *= (avg_cycle_length)/(1./dfa_interval) - - energy_for_envenoms = energy_per_cycle - energy_for_rupture - energy_for_dfa - - envenom_energy_cost = avg_cpgs_per_finisher * cpg_energy_cost + self.get_spell_stats('envenom', cost_mod=ability_cost_modifier)[0] - envenom_energy_cost -= avg_finisher_size * self.relentless_strikes_energy_return_per_cp - envenoms_per_cycle = energy_for_envenoms / envenom_energy_cost - - envenoms_per_second = envenoms_per_cycle / avg_cycle_length - finishers_per_second = envenoms_per_second + attacks_per_second['rupture'] - if self.talents.death_from_above: - finishers_per_second += attacks_per_second['death_from_above'] - cpgs_per_second = avg_cpgs_per_finisher * finishers_per_second - if cpg in attacks_per_second: - attacks_per_second[cpg] += cpgs_per_second - else: - attacks_per_second[cpg] = cpgs_per_second - if cpg == 'mutilate': - attacks_per_second['dispatch'] += cpgs_per_second * blindside_proc_rate - - attacks_per_second['rupture_ticks'] = [0,0,0,0,0,.5] - if self.talents.anticipation: - attacks_per_second['envenom'][5] += envenoms_per_second - else: - for i in xrange(1,6): - attacks_per_second['envenom'][i] = envenoms_per_second * avg_size_breakdown[i] - #attacks_per_second['envenom'] = [finisher_chance * envenoms_per_second for finisher_chance in avg_size_breakdown] - for i in xrange(1, 6): - ticks_per_rupture = 2 * (1 + i + self.stats.gear_buffs.rogue_t15_2pc_bonus_cp()) - attacks_per_second['rupture_ticks'][i] = ticks_per_rupture * attacks_per_second['rupture'] * avg_size_breakdown[i] - - if self.talents.marked_for_death: - attacks_per_second['envenom'][5] += 1. / self.get_spell_cd('marked_for_death') - - if 'garrote' in attacks_per_second: - attacks_per_second['garrote_ticks'] = 6 * attacks_per_second['garrote'] - attacks_per_second['envenom'][5] += 1. / 180 - - if self.level == 100: - finisher_per_second = sum(attacks_per_second['envenom']) + attacks_per_second['rupture'] - if self.talents.death_from_above: - finisher_per_second += sum(attacks_per_second['death_from_above_strike']) - self.emp_envenom_percentage = 1 + .3 * (1 - attacks_per_second['rupture']/finisher_per_second) - - if self.talents.shadow_reflection: - sr_uptime = 8. / self.get_spell_cd('shadow_reflection') - for ability in ('rupture_ticks', 'dispatch'): - if type(attacks_per_second[ability]) in (tuple, list): - attacks_per_second['sr_'+ability] = [0,0,0,0,0,0] - for i in xrange(1, 6): - attacks_per_second['sr_'+ability][i] = sr_uptime * attacks_per_second[ability][i] + raise InputNotModeledException(_('Cp builder must be \'mutilate\' or \'fan_of_knives\'')) + + #if anticipation we can just assume no waste + if self.talents.anticipation: + avg_cp_per_builder = sum([cp * cpg_cps[cp] for cp in cpg_cps]) + builders_per_finisher = self.settings.finisher_threshold / avg_cp_per_builder + avg_finisher_size = self.settings.finisher_threshold + finisher_list = [0, 0, 0, 0, 0, 0, 0] + finisher_list[self.settings.finisher_threshold] = 1.0 + #otherwise we need to enumerate paths to determine amount of waste given cp threshold + else: + #TODO: Super hackish, do this right + finisher_list = [0, 0, 0, 0, 0, 0, 0] + if self.settings.finisher_threshold == 4: + paths = [(2, 2), (2, 3), (2, 4), (3, 2), (3, 3), (3, 4), (4,)] + elif self.settings.finisher_threshold == 5: + paths = [(2, 2, 2), (2, 2, 3), (2, 2, 4), (2, 3), (2, 4), (3, 2), (3, 3), (3, 4), (4, 2), (4, 3), (4, 4)] + elif self.settings.finisher_threshold == 6: + paths = [(2, 2, 2), (2, 2, 3), (2, 2, 4), (2, 3, 2), (2, 3, 3), (2, 3, 4), (2, 4), + (3, 2, 2), (3, 2, 3), (3, 2, 4), (3, 3), (3, 4), (4, 2), (4, 3), (4, 4)] else: - attacks_per_second['sr_'+ability] = sr_uptime * attacks_per_second[ability] - envenom_per_sr = 1.5 * sum(attacks_per_second['envenom']) - attacks_per_second['sr_envenom'] = [finisher_chance * envenom_per_sr / self.get_spell_cd('shadow_reflection') for finisher_chance in avg_size_breakdown] - crit_rates['sr_envenom'] = 1./envenom_per_sr + (1-envenom_per_sr)/envenom_per_sr * crit_rates['envenom'] - if 'mutilate' in attacks_per_second: - attacks_per_second['sr_mh_mutilate'] = 2 * sr_uptime * attacks_per_second['mutilate'] - attacks_per_second['sr_oh_mutilate'] = 2 * sr_uptime * attacks_per_second['mutilate'] - - white_swing_downtime = 0 - if self.swing_reset_spacing is not None: - white_swing_downtime += .5 / self.swing_reset_spacing - attacks_per_second['mh_autoattacks'] = self.attack_speed_increase / self.stats.mh.speed * (1 - white_swing_downtime) - attacks_per_second['oh_autoattacks'] = self.attack_speed_increase / self.stats.oh.speed * (1 - white_swing_downtime) - - if self.talents.death_from_above: - lost_swings_mh = self.lost_swings_from_swing_delay(1.3, self.stats.mh.speed / self.attack_speed_increase) - lost_swings_oh = self.lost_swings_from_swing_delay(1.3, self.stats.oh.speed / self.attack_speed_increase) - - attacks_per_second['mh_autoattacks'] -= lost_swings_mh / dfa_cd - attacks_per_second['oh_autoattacks'] -= lost_swings_oh / dfa_cd - - attacks_per_second['mh_autoattack_hits'] = attacks_per_second['mh_autoattacks'] * self.dw_mh_hit_chance - attacks_per_second['oh_autoattack_hits'] = attacks_per_second['oh_autoattacks'] * self.dw_oh_hit_chance - - self.get_poison_counts(attacks_per_second, current_stats) - - if self.level == 100: - #this is to update the crit rate for envenom due to the 'crit on Vendetta cast' perk, unlikely to ever be another ability - crit_uptime = (1./(self.get_spell_cd('vendetta') + self.settings.response_time + self.major_cd_delay)) / sum(attacks_per_second['envenom']) - #this takes the difference between normal and guaranteed crits (1 - crit_rate), and multiplies it by the "uptime" across all envenoms - #it's then added back to the original crit rate - crit_rates['envenom'] += crit_uptime * (1 - crit_rates['envenom']) + raise InputNotModeledException(_('Finisher thresholds less than 4 unimplemented')) + max_cps = 5 + if self.talents.deeper_stratagem: + max_cps = 6 + builders_per_finisher = 0.0 + avg_finisher_size = 0.0 + finisher_list = [0., 0., 0., 0., 0., 0., 0.] + + for path in paths: + chance = 1.0 + for step in path: + chance *= cpg_cps[step] + builders_per_finisher += chance * len(path) + size = min(max_cps, sum(path)) + avg_finisher_size += chance * size + finisher_list[size] += chance + + cp_builder_energy_per_finisher = builders_per_finisher * self.get_spell_cost(self.cp_builder) + + #set up our energy budget + haste_multiplier = self.get_haste_multiplier(current_stats) + energy_regen = self.get_energy_regen(current_stats) + + #set up rupture + attacks_per_second['rupture'] = [0, 0, 0, 0, 0, 0, 0] + attacks_per_second['rupture_ticks'] = [0, 0, 0, 0, 0, 0, 0] + base_rupture_duration = 4 * (1 + avg_finisher_size) + if self.talents.exsanguinate: + #assume full pandemic on exsanged ruptures + exsang_rupture_duration = (1.3 * base_rupture_duration) / 2.5 + #rupture we're pandemicing from + exsang_from_duration = 0.7 * base_rupture_duration + normal_ruptures_per_exsang_cd = (self.exsang_cd - exsang_from_duration - exsang_rupture_duration) / base_rupture_duration + ruptures_per_second = (2. + normal_ruptures_per_exsang_cd) / self.exsang_cd + rupture_ticks_per_second = 1. * float(exsang_rupture_duration)/ self.exsang_cd + \ + 0.5 * float(self.exsang_cd - exsang_rupture_duration)/self.exsang_cd + else: + ruptures_per_second = 1 / base_rupture_duration + rupture_ticks_per_second = 0.5 + + for cp in range(7): + attacks_per_second['rupture'][cp] = ruptures_per_second * finisher_list[cp] + attacks_per_second['rupture_ticks'][cp] = rupture_ticks_per_second * finisher_list[cp] + rupture_cost_per_second = self.get_spell_cost('rupture') * ruptures_per_second + rupture_cost_per_second += cp_builder_energy_per_finisher * ruptures_per_second + attacks_per_second[self.cp_builder] = ruptures_per_second * builders_per_finisher + + #set up garrote: + base_garrote_duration = 18. + garrote_cooldown = self.get_spell_cd('garrote') + if self.talents.exsanguinate: + exsang_garrote_duration = base_garrote_duration / 2.5 + exsang_downtime = max(0, garrote_cooldown - exsang_garrote_duration) + normal_garrote_per_exsang = (self.exsang_cd - garrote_cooldown) / base_garrote_duration + attacks_per_second['garrote'] = (1 + normal_garrote_per_exsang) / self.exsang_cd + attacks_per_second['garrote_ticks'] = 2/3 * float(exsang_garrote_duration) / self.exsang_cd + \ + 1/3 * float(self.exsang_cd - exsang_garrote_duration - exsang_downtime) / self.exsang_cd + else: + attacks_per_second['garrote'] = 1 / base_garrote_duration + attacks_per_second['garrote_ticks'] = 1 / 3 + + cp_budget = attacks_per_second['garrote'] * self.settings.duration + garrote_cost_per_second = self.get_spell_cost('garrote') * attacks_per_second['garrote'] + + #Now that ticks are done, we can compute VW regen + vw_energy_per_tick = 7 + 3 * self.talents.venom_rush + vw_regen_per_second = vw_energy_per_tick * (sum(attacks_per_second['rupture_ticks']) + attacks_per_second['garrote_ticks']) + + net_energy_per_second = energy_regen + vw_regen_per_second + net_energy_per_second -= rupture_cost_per_second + garrote_cost_per_second + duskwalker_expended_energy = rupture_cost_per_second + garrote_cost_per_second + + #compute cooldowned talents: + if self.talents.marked_for_death: + mfd_base_count = 1 + self.settings.duration / self.get_spell_cd('marked_for_death') + mfd_cps = (5. + self.talents.deeper_stratagem) * (mfd_base_count + self.settings.marked_for_death_resets) + cp_budget += mfd_cps + + if self.stats.gear_buffs.the_dreadlords_deceit: + fok_interval = 1 / 60 + attacks_per_second['fan_of_knives'] = fok_interval + cp_budget += self.settings.duration * fok_interval * (1 + crit_rates['fan_of_knives']) + net_energy_per_second -= fok_interval * 35 + duskwalker_expended_energy += fok_interval * 35 + + if self.traits.kingsbane: + attacks_per_second['kingsbane'] = 1 / self.kingsbane_cd + attacks_per_second['kingsbane_ticks'] = 7 / self.kingsbane_cd + kb_crit = crit_rates['kingsbane'] + cpg_cps = {1: (1 - kb_crit) ** 2, + 2: 2 * (1 - kb_crit) * kb_crit, + 3: kb_crit ** 2} + avg_cp_per_kb = sum([cp * cpg_cps[cp] for cp in cpg_cps]) + cp_budget += avg_cp_per_kb * attacks_per_second['kingsbane'] * self.settings.duration + net_energy_per_second -= self.get_spell_cost('kingsbane') * attacks_per_second['kingsbane'] + duskwalker_expended_energy += self.get_spell_cost('kingsbane') * attacks_per_second['kingsbane'] + if self.stats.gear_buffs.the_empty_crown: + net_energy_per_second += 40 * attacks_per_second['kingsbane'] + + if self.talents.hemorrhage: + hemos_per_second = 1 / 20 + attacks_per_second['hemorrhage'] = hemos_per_second + hemo_cps = (1 + crit_rates['hemorrhage']) * (self.settings.duration * hemos_per_second) + cp_budget += hemo_cps + net_energy_per_second -= self.get_spell_cost('hemorrhage') * hemos_per_second + duskwalker_expended_energy += self.get_spell_cost('hemorrhage') * hemos_per_second - return attacks_per_second, crit_rates, additional_info - - def assassination_attack_counts_non_execute(self, current_stats, crit_rates=None): - return self.assassination_attack_counts(current_stats, 'mutilate', self.settings.cycle.min_envenom_size_non_execute, crit_rates=crit_rates) + if self.talents.death_from_above: + dfa_cd = self.get_spell_cd('death_from_above') + self.settings.response_time + dfa_per_second = 1 / dfa_cd + attacks_per_second['death_from_above_strike'] = [0, 0, 0, 0, 0, 0, 0] + attacks_per_second['death_from_above_pulse'] = [0, 0, 0, 0, 0, 0, 0] + for cp in range(7): + attacks_per_second['death_from_above_pulse'][cp] = dfa_per_second * finisher_list[cp] + attacks_per_second['death_from_above_strike'][cp] = dfa_per_second * finisher_list[cp] + attacks_per_second[self.cp_builder] += dfa_per_second * builders_per_finisher + dfa_cost_per_second = self.get_spell_cost('death_from_above') * dfa_per_second + dfa_cost_per_second += cp_builder_energy_per_finisher * dfa_per_second + net_energy_per_second -= dfa_cost_per_second + duskwalker_expended_energy += dfa_cost_per_second + + if self.talents.toxic_blade: + attacks_per_second['toxic_blade'] = 1 / self.get_spell_cd('toxic_blade') + tb_cps = (1 + crit_rates['toxic_blade']) * (self.settings.duration * attacks_per_second['toxic_blade']) + cp_budget += tb_cps + tb_cost_per_second = self.get_spell_cost('toxic_blade') * attacks_per_second['toxic_blade'] + net_energy_per_second -= tb_cost_per_second + duskwalker_expended_energy += tb_cost_per_second + + if self.talents.exsanguinate: + exsg_cost_per_second = self.get_spell_cost('exsanguinate') / self.exsang_cd + net_energy_per_second -= exsg_cost_per_second + duskwalker_expended_energy += exsg_cost_per_second + + #form whats left into a budget + duskwalker_expended_energy *= self.settings.duration + energy_budget = self.settings.duration * net_energy_per_second + max_energy = 120 + if self.talents.vigor or self.stats.gear_buffs.soul_of_the_shadowblade: + max_energy += 50 + energy_budget += max_energy + #As of Patch 7.2 we get 60 energy + 60 over 2s, assume no loss + if self.traits.urge_to_kill: + energy_budget += (self.settings.duration / self.vendetta_cd) * 120 + #If we have Shadow Focus, use it as a builder cost reducer after vanish + if self.talents.shadow_focus: + energy_budget += 0.75 * self.get_spell_cost('garrote') #Opener + energy_budget += 0.75 * self.get_spell_cost(self.cp_builder) * self.settings.duration / self.get_spell_cd('vanish') + + attacks_per_second['envenom'] = [0, 0, 0, 0, 0, 0, 0] + #spend those extra cps + if cp_budget > 0: + extra_envenom = cp_budget / avg_finisher_size + energy_budget -= self.get_spell_cost('envenom') * extra_envenom + duskwalker_expended_energy += self.get_spell_cost('envenom') * extra_envenom + extra_envenom_per_second = extra_envenom / self.settings.duration + for cp in range(7): + attacks_per_second['envenom'][cp] = extra_envenom_per_second * finisher_list[cp] + + #now burn whats left in a minicycle + mini_cycle_energy = self.get_spell_cost('envenom') + cp_builder_energy_per_finisher + loop_counter = 0 + + alacrity_stacks = 0 + while energy_budget > 0.1: + if loop_counter > 20: + raise ConvergenceErrorException(_('Mini-cycles failed to converge.')) + loop_counter += 1 + + total_minicycles = energy_budget / mini_cycle_energy + attacks_per_second[self.cp_builder] += total_minicycles * builders_per_finisher / self.settings.duration + finishers_per_second = total_minicycles / self.settings.duration + for cp in range(7): + attacks_per_second['envenom'][cp] += finisher_list[cp] * finishers_per_second + energy_budget -= total_minicycles * mini_cycle_energy + duskwalker_expended_energy += total_minicycles * mini_cycle_energy + + if self.talents.alacrity: + old_alacrity_regen = energy_regen * (1 + (alacrity_stacks *0.02)) + new_alacrity_stacks = self.get_average_alacrity(attacks_per_second) + new_alacrity_regen = energy_regen * (1 + (new_alacrity_stacks *0.02)) + energy_budget += (new_alacrity_regen - old_alacrity_regen) * self.settings.duration + alacrity_stacks = new_alacrity_stacks + + #swing timer + white_swing_downtime = 0 + self.swing_reset_spacing = self.get_spell_cd('vanish') + if self.swing_reset_spacing is not None: + white_swing_downtime += self.settings.response_time / self.swing_reset_spacing + attacks_per_second['mh_autoattacks'] = (haste_multiplier * (1 + (alacrity_stacks * 0.01))) / self.stats.mh.speed * (1 - white_swing_downtime) + attacks_per_second['oh_autoattacks'] = attacks_per_second['mh_autoattacks'] + + if self.traits.bag_of_tricks: + #2.5% chance per cp on envenom and rupture + attacks_per_second['poison_bomb'] = 0 + for i in range(7): + attacks_per_second['poison_bomb'] += attacks_per_second['envenom'][i] * i * 0.025 + attacks_per_second['poison_bomb'] += attacks_per_second['rupture'][i] * i * 0.025 + + if self.stats.gear_buffs.duskwalkers_footpads: + #Recalculate Vendetta cooldown + self.vendetta_cd = self.get_spell_cd('vendetta') / (1 + (duskwalker_expended_energy / 65) / self.settings.duration) + + #poison computations, use old function for now + self.get_poison_counts(attacks_per_second, current_stats) + + #Sinister Circulation + if self.traits.sinister_circulation: + poisons_per_second = 0 + if self.settings.cycle.lethal_poison == 'dp': + poisons_per_second = attacks_per_second['deadly_instant_poison'] + elif self.settings.cycle.lethal_poison == 'wp': + poisons_per_second = attacks_per_second['wound_poison'] + #Recalculate KB cooldown, Sinister Circulation has a 0.5s icd + kb_cdr_per_sec = min(poisons_per_second, 2) * 0.5 + self.kingsbane_cd = self.get_spell_cd('kingsbane') + if self.settings.cycle.kingsbane_with_vendetta == 'only': + self.kingsbane_cd = max(self.vendetta_cd, self.kingsbane_cd) + self.kingsbane_cd /= 1 + kb_cdr_per_sec + + if self.traits.from_the_shadows: + attacks_per_second['from_the_shadows'] = 1 / self.vendetta_cd + + #Break convergence loop when it's not needed + if not self.traits.sinister_circulation and not self.stats.gear_buffs.duskwalkers_footpads: + break + if self.are_close_enough(old_aps, attacks_per_second): + break + + old_aps = attacks_per_second + + # for a in attacks_per_second: + # if isinstance(attacks_per_second[a], list): + # print a, 1./sum(attacks_per_second[a]) + # else: + # print a, 1./attacks_per_second[a] + # print "--------" - def assassination_attack_counts_execute(self, current_stats, crit_rates=None): - return self.assassination_attack_counts(current_stats, 'dispatch', self.settings.cycle.min_envenom_size_execute, crit_rates=crit_rates) + return attacks_per_second, crit_rates, additional_info ########################################################################### - # Combat DPS functions + # Outlaw DPS functions ########################################################################### - def combat_dps_estimate(self): - return sum(self.combat_dps_breakdown().values()) - - def combat_dps_breakdown(self): - if not self.settings.is_combat_rogue(): - raise InputNotModeledException(_('You must specify a combat cycle to match your combat spec.')) - - #set readiness coefficient - self.readiness_spec_conversion = self.combat_readiness_conversion - self.spec_convergence_stats = ['haste', 'mastery', 'readiness'] - - #spec specific glyph behaviour - if self.glyphs.disappearance: - self.ability_cds['vanish'] = 60 - else: - self.ability_cds['vanish'] = 120 - - #update spec specific proc rates - if getattr(self.stats.procs, 'legendary_capacitive_meta'): - getattr(self.stats.procs, 'legendary_capacitive_meta').proc_rate_modifier = 1.136 - if getattr(self.stats.procs, 'fury_of_xuen'): - getattr(self.stats.procs, 'fury_of_xuen').proc_rate_modifier = 1.15 - - #combat specific constants - self.max_bandits_guile_buff = 1.3 - self.combat_cd_delay = 0 #this is for DFA convergence, mostly - if self.level == 100: - self.max_bandits_guile_buff += .2 - self.dw_miss_penalty = 0 - self.recalculate_hit_constants() - self.max_energy = 100. - if self.stats.gear_buffs.rogue_pvp_4pc_extra_energy(): - self.max_energy += 30 - if self.talents.lemon_zest: - self.max_energy += 15 - if self.glyphs.energy: - self.max_energy += 20 - if self.race.expansive_mind: - self.max_energy = round(self.max_energy * 1.05, 0) + #Legion TODO: + + #Talents: + #T3:Anticipation + + #Artifact: + # 'hidden_blade' (ambush proc weirdness) + # 'blurred_time' + # 'loaded_dice' (for RtB) + + #Items: + #Tier bonus + #Legendaries + + #Rotation details: + + def outlaw_dps_estimate(self): + return sum(self.outlaw_dps_breakdown().values()) + + def outlaw_dps_breakdown(self): + if not self.spec == 'outlaw': + raise InputNotModeledException(_('You must specify a outlaw cycle to match your outlaw spec.')) + + #outlaw specific constants + self.outlaw_cd_delay = 0 #this is for DFA convergence, mostly + self.ar_duration = 15 - self.revealing_strike_multiplier = 1.35 - self.extra_cp_chance = .25 # Assume all casts during RvS - if self.stats.gear_buffs.rogue_t17_2pc: - self.extra_cp_chance += 0.2 - self.rvs_duration = 24 - if self.settings.dmg_poison == 'dp' and self.level == 100: - self.settings.dmg_poison = 'sp' - + self.ar_cd = self.get_spell_cd('adrenaline_rush') + self.cotd_cd = self.get_spell_cd('curse_of_the_dreadblades') + self.set_constants() - self.stat_multipliers['haste'] *= 1.05 - self.stat_multipliers['ap'] *= 1.50 - - if self.talents.death_from_above: - self.spec_needs_converge = True - - cds = {'ar':self.get_spell_cd('adrenaline_rush'), - 'ks':self.get_spell_cd('killing_spree')} - - # actual damage calculations here - phases = {} - #AR phase - stats, aps, crits, procs, additional_info = self.determine_stats(self.combat_attack_counts_ar) - ar_tuple = self.compute_damage_from_aps(stats, aps, crits, procs, additional_info) - phases['ar'] = (self.ar_duration, self.update_with_bandits_guile(ar_tuple[0], ar_tuple[1])) - for e in cds: - cds[e] -= self.ar_duration / self.rb_cd_modifier(aps) - - #none - self.tmp_ks_cd = cds['ks'] - self.tmp_phase_length = cds['ar'] #This is to approximate the value of a full energy bar to be used when not during AR or SB - stats, aps, crits, procs, additional_info = self.determine_stats(self.combat_attack_counts_none) - none_tuple = self.compute_damage_from_aps(stats, aps, crits, procs, additional_info) - phases['none'] = (self.rb_actual_cds(aps, cds)['ar'] + self.settings.response_time + self.major_cd_delay, - self.update_with_bandits_guile(none_tuple[0], none_tuple[1]) ) - - total_duration = phases['ar'][0] + phases['none'][0] - #average it together - damage_breakdown = self.average_damage_breakdowns(phases, denom = total_duration) - + + #table of minicycle ability amounts + #indexed by (min_spend_cps, deeper_strat, quick_draw, swordmaster, broadside, jollyroger) + #values are (ss_per_min_cycle, ps_per_min_cycle, finisher_cp_list) + #TODO: 60 element table is probably a bit much, should probably be condensed + self.minicycle_table = { + (4, True, True, False, True, True) : (0.92778015, 0.5566681, [0, 0, 0, 0, 0.46230870485305786, 0.40208783745765686, 0.13560345768928528]) , + (4, True, True, False, True, False) : (1.2831669, 0.44910839, [0, 0, 0, 0, 0.35908344388008118, 0.49529376626014709, 0.14562278985977173]) , + (4, True, True, False, False, True) : (1.3207548, 0.79245281, [0, 0, 0, 0, 0.37735849618911743, 0.62264150381088257, 0.0]) , + (4, True, True, False, False, False) : (1.7271835, 0.60451424, [0, 0, 0, 0, 0.57409226894378662, 0.42590776085853577, 0.0]) , + (4, True, False, True, True, True) : (1.7995313, 1.2596719, [0, 0, 0, 0, 0.19270744919776917, 0.39063876867294312, 0.41665378212928772]) , + (4, True, False, True, True, False) : (1.759297, 0.79168367, [0, 0, 0, 0, 0.13849352300167084, 0.56256377696990967, 0.29894271492958069]) , + (4, True, False, True, False, True) : (1.3918972, 0.97432804, [0, 0, 0, 0, 0.82430845499038696, 0.17569157481193542, 0.0]) , + (4, True, False, True, False, False) : (1.7689608, 0.79603237, [0, 0, 0, 0, 0.7987181544303894, 0.20128187537193298, 0.0]) , + (4, True, False, False, True, True) : (1.7663901, 1.059834, [0, 0, 0, 0, 0.17100141942501068, 0.45841407775878906, 0.37058448791503906]) , + (4, True, False, False, True, False) : (1.7791812, 0.62271339, [0, 0, 0, 0, 0.11556066572666168, 0.63698828220367432, 0.24745103716850281]) , + (4, True, False, False, False, True) : (1.5257645, 0.91545868, [0, 0, 0, 0, 0.80414772033691406, 0.19585229456424713, 0.0]) , + (4, True, False, False, False, False) : (1.9706308, 0.68972075, [0, 0, 0, 0, 0.81240963935852051, 0.1875903457403183, 0.0]) , + (4, False, True, False, True, True) : (0.90085906, 0.54051542, [0, 0, 0, 0, 0.46230870485305786, 0.53769129514694214, 0]) , + (4, False, True, False, True, False) : (1.2441286, 0.43544501, [0, 0, 0, 0, 0.35908344388008118, 0.64091658592224121, 0]) , + (4, False, True, False, False, True) : (1.3207548, 0.79245281, [0, 0, 0, 0, 0.37735849618911743, 0.62264150381088257, 0]) , + (4, False, True, False, False, False) : (1.7271835, 0.60451424, [0, 0, 0, 0, 0.57409226894378662, 0.42590776085853577, 0]) , + (4, False, False, True, True, True) : (1.6560036, 1.1592025, [0, 0, 0, 0, 0.19270744919776917, 0.80729258060455322, 0]) , + (4, False, False, True, True, False) : (1.6573817, 0.74582177, [0, 0, 0, 0, 0.13849352300167084, 0.86150646209716797, 0]) , + (4, False, False, True, False, True) : (1.3918972, 0.97432804, [0, 0, 0, 0, 0.82430845499038696, 0.17569157481193542, 0]) , + (4, False, False, True, False, False) : (1.7689608, 0.79603237, [0, 0, 0, 0, 0.7987181544303894, 0.20128187537193298, 0]) , + (4, False, False, False, True, True) : (1.640496, 0.98429757, [0, 0, 0, 0, 0.17100141942501068, 0.82899856567382812, 0]) , + (4, False, False, False, True, False) : (1.693392, 0.59268725, [0, 0, 0, 0, 0.11556066572666168, 0.88443934917449951, 0]) , + (4, False, False, False, False, True) : (1.5257645, 0.91545868, [0, 0, 0, 0, 0.80414772033691406, 0.19585229456424713, 0]) , + (4, False, False, False, False, False) : (1.9706308, 0.68972075, [0, 0, 0, 0, 0.81240963935852051, 0.1875903457403183, 0]) , + (5, True, True, False, True, True) : (1.5440897, 0.92645377, [0, 0, 0, 0, 0, 0.47792428731918335, 0.52207571268081665]) , + (5, True, True, False, True, False) : (1.6837471, 0.58931148, [0, 0, 0, 0, 0, 0.52392536401748657, 0.47607460618019104]) , + (5, True, True, False, False, True) : (1.509434, 0.90566039, [0, 0, 0, 0, 0, 0.71698111295700073, 0.28301885724067688]) , + (5, True, True, False, False, False) : (2.0673864, 0.72358519, [0, 0, 0, 0, 0, 0.70232254266738892, 0.29767745733261108]) , + (5, True, False, True, True, True) : (2.7676663, 1.9373665, [0, 0, 0, 0, 0, 0.32654938101768494, 0.67345058917999268]) , + (5, True, False, True, True, False) : (2.0575211, 0.92588449, [0, 0, 0, 0, 0, 0.53625214099884033, 0.46374788880348206]) , + (5, True, False, True, False, True) : (1.7693849, 1.2385694, [0, 0, 0, 0, 0, 0.69184529781341553, 0.30815470218658447]) , + (5, True, False, True, False, False) : (2.1994596, 0.98975676, [0, 0, 0, 0, 0, 0.7762836217880249, 0.22371639311313629]) , + (5, True, False, False, True, True) : (2.3502514, 1.4101509, [0, 0, 0, 0, 0, 0.41270622611045837, 0.58729374408721924]) , + (5, True, False, False, True, False) : (1.9709414, 0.68982947, [0, 0, 0, 0, 0, 0.62002801895141602, 0.37997198104858398]) , + (5, True, False, False, False, True) : (1.9163667, 1.14982, [0, 0, 0, 0, 0, 0.72999167442321777, 0.27000829577445984]) , + (5, True, False, False, False, False) : (2.4447069, 0.85564739, [0, 0, 0, 0, 0, 0.80499798059463501, 0.19500201940536499]) , + (5, False, True, False, True, True) : (1.475865, 0.88551903, [0, 0, 0, 0, 0, 1.0, 0]) , + (5, False, True, False, True, False) : (1.6334157, 0.57169551, [0, 0, 0, 0, 0, 1.0, 0]) , + (5, False, True, False, False, True) : (1.509434, 0.90566039, [0, 0, 0, 0, 0, 1.0, 0]) , + (5, False, True, False, False, False) : (2.0673864, 0.72358519, [0, 0, 0, 0, 0, 1.0, 0]) , + (5, False, False, True, True, True) : (2.5490196, 1.7843137, [0, 0, 0, 0, 0, 1.0, 0]) , + (5, False, False, True, True, False) : (1.9435737, 0.87460816, [0, 0, 0, 0, 0, 1.0, 0]) , + (5, False, False, True, False, True) : (1.7693849, 1.2385694, [0, 0, 0, 0, 0, 1.0, 0]) , + (5, False, False, True, False, False) : (2.1994596, 0.98975676, [0, 0, 0, 0, 0, 1.0, 0]) , + (5, False, False, False, True, True) : (2.1875, 1.3125, [0, 0, 0, 0, 0, 1.0, 0]) , + (5, False, False, False, True, False) : (1.8803419, 0.65811968, [0, 0, 0, 0, 0, 1.0, 0]) , + (5, False, False, False, False, True) : (1.9163667, 1.14982, [0, 0, 0, 0, 0, 1.0, 0]) , + (5, False, False, False, False, False) : (2.4447069, 0.85564739, [0, 0, 0, 0, 0, 1.0, 0]) , + (6, True, True, False, True, True) : (2.7550187, 1.6530112, [0, 0, 0, 0, 0, 0, 1.0]) , + (6, True, True, False, True, False) : (2.4767113, 0.86684889, [0, 0, 0, 0, 0, 0, 1.0]) , + (6, True, True, False, False, True) : (1.8489302, 1.1093582, [0, 0, 0, 0, 0, 0, 1.0]) , + (6, True, True, False, False, False) : (2.4813204, 0.86846215, [0, 0, 0, 0, 0, 0, 1.0]) , + (6, True, False, True, True, True) : (1.8811882, 1.3168317, [0, 0, 0, 0, 0, 0, 1.0]) , + (6, True, False, True, True, False) : (2.0423892, 0.91907513, [0, 0, 0, 0, 0, 0, 1.0]) , + (6, True, False, True, False, True) : (2.1186955, 1.4830868, [0, 0, 0, 0, 0, 0, 1.0]) , + (6, True, False, True, False, False) : (2.6321666, 1.1844751, [0, 0, 0, 0, 0, 0, 1.0]) , + (6, True, False, False, True, True) : (1.9298246, 1.1578947, [0, 0, 0, 0, 0, 0, 1.0]) , + (6, True, False, False, True, False) : (2.1415608, 0.74954629, [0, 0, 0, 0, 0, 0, 1.0]) , + (6, True, False, False, False, True) : (2.2952538, 1.3771522, [0, 0, 0, 0, 0, 0, 1.0]) , + (6, True, False, False, False, False) : (2.9230175, 1.0230561, [0, 0, 0, 0, 0, 0, 1.0]) , + } + + self.damage_modifiers = modifiers.ModifierList(self.outlaw_damage_sources + ['autoattacks']) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('versatility', None, [], all_damage=True)) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('armor', self.armor_mitigation_multiplier(), ['death_from_above_pulse', + 'death_from_above_strike', 'ambush', 'between_the_eyes', 'blunderbuss', 'cannonball_barrage', + 'ghostly_strike', 'greed', 'killing_spree', 'main_gauche', + 'pistol_shot', 'run_through', 'saber_slash', 'autoattacks'], dmg_schools=['physical'])) + + # Generic tuning aura + self.damage_modifiers.register_modifier(modifiers.DamageModifier('outlaw_aura', 1.06, ['death_from_above_pulse', 'death_from_above_strike', + 'ambush', 'between_the_eyes', 'blunderbuss', 'cannonball_barrage', 'ghostly_strike', 'killing_spree', + 'pistol_shot', 'run_through', 'saber_slash'])) + + # Talent specific modifiers + if self.talents.deeper_stratagem: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('deeper_stratagem', 1.05, ['between_the_eyes', 'run_through', 'death_from_above_pulse', 'death_from_above_strike'])) + + # Trait specific modifiers + if self.traits.cursed_steel: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('cursed_steel', + 1.05 + (0.005 * (self.traits.legionblade - 1)), [], all_damage=True)) + + if self.traits.bravado_of_the_uncrowned: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('bravado_of_the_uncrowned', 1.1, [], all_damage=True)) + + if self.traits.dreadblades_vigor: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('dreadblades_vigor', None, [], all_damage=True)) + + #Gear specific + if self.stats.gear_buffs.jeweled_signet_of_melandrus: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('jeweled_signet_of_melandrus', 1.1, ['autoattacks'])) + + if self.stats.gear_buffs.gnawed_thumb_ring: + gtr_mod = 1 + 0.05 * 12 / 180 + self.damage_modifiers.register_modifier(modifiers.DamageModifier('gnawed_thumb_ring', gtr_mod, [], + dmg_schools=['arcane', 'fire', 'frost', 'holy', 'nature', 'shadow'])) + + stats, aps, crits, procs, additional_info = self.determine_stats(self.outlaw_attack_counts) + + self.damage_modifiers.update_modifier_value('versatility', self.stats.get_versatility_multiplier_from_rating(rating=stats['versatility'])) + + if self.traits.dreadblades_vigor: + self.damage_modifiers.update_modifier_value('dreadblades_vigor', 1 + (0.1 * 12 / self.cotd_cd)) + + damage_breakdown, additional_info = self.compute_damage_from_aps(stats, aps, crits, procs, additional_info) + + if self.stats.gear_buffs.insignia_of_ravenholdt: + damage_breakdown['insignia_of_ravenholdt'] = self.compute_insignia_of_ravenholdt_damage(stats, damage_breakdown) + bf_mod = .35 - bf_max_targets = 4 - if self.level == 100: - bf_max_targets = 999 #this is the "no more target cap" limit, screw extra if statements if self.settings.cycle.blade_flurry: damage_breakdown['blade_flurry'] = 0 for key in damage_breakdown: - if key in self.melee_attacks: - damage_breakdown['blade_flurry'] += bf_mod * damage_breakdown[key] * min(self.settings.num_boss_adds, bf_max_targets) - - #combat gets it's own MS calculation due to BF mechanics - #calculate multistrike here, really cheap to calculate - #turns out the 2 chance system yields a very basic linear pattern, the damage modifier is 30% of the multistrike %! - multistrike_multiplier = .3 * 2 * (self.stats.get_multistrike_chance_from_rating(rating=stats['multistrike']) + self.buffs.multistrike_bonus()) - multistrike_multiplier = min(.6, multistrike_multiplier) + if key in self.blade_flurry_damage_sources: + damage_breakdown['blade_flurry'] += bf_mod * damage_breakdown[key] * self.settings.num_boss_adds + + infallible_trinket_mod = 1.0 + if self.settings.is_demon: + if getattr(self.stats.procs, 'infallible_tracking_charm_mod'): + ift = getattr(self.stats.procs, 'infallible_tracking_charm_mod') + self.set_rppm_uptime(ift) + infallible_trinket_mod = 1+(ift.uptime *0.10) + for ability in damage_breakdown: - damage_breakdown[ability] *= (1 + multistrike_multiplier) - - return damage_breakdown - - def update_with_bandits_guile(self, damage_breakdown, additional_info): - for key in damage_breakdown: - if key in ('killing_spree', 'mh_killing_spree', 'oh_killing_spree'): - if self.settings.cycle.ksp_immediately: - damage_breakdown[key] *= self.bandits_guile_multiplier - else: - damage_breakdown[key] *= self.max_bandits_guile_buff - if self.stats.gear_buffs.rogue_t16_4pc_bonus(): - #http://elitistjerks.com/f78/t132793-5_4_changes_discussion/p2/#post2301780 - #http://www.wolframalpha.com/input/?i=%28sum+of+1.5*1.1%5Ex+from+x%3D1+to+7%29+%2F+%281.5*7%29 - # No need to use anything other than a constant. Yay for convenience! - damage_breakdown[key] *= 1.49084 - elif key in ('sinister_strike', 'revealing_strike'): - damage_breakdown[key] *= self.bandits_guile_multiplier - elif key in ('eviscerate', ): - damage_breakdown[key] *= self.bandits_guile_multiplier * self.revealing_strike_multiplier - else: - damage_breakdown[key] *= self.bandits_guile_multiplier #* self.ksp_multiplier - + damage_breakdown[ability] *= infallible_trinket_mod + + if self.stats.gear_buffs.cinidaria_the_symbiote: + damage_breakdown['symbiote_strike'] = self.compute_symbiote_strike_damage(damage_breakdown) + return damage_breakdown - - def combat_cpg_per_finisher(self, current_cp, ability_count): - if current_cp >= 5: - return ability_count - new_count = copy(ability_count) - new_count += 1 - - normal = self.combat_cpg_per_finisher(current_cp+1, new_count) - rvs_proc = self.combat_cpg_per_finisher(current_cp+2, new_count) - - return (1 - self.extra_cp_chance)*normal + self.extra_cp_chance*rvs_proc - - def combat_attack_counts(self, current_stats, ar=False, crit_rates=None): + + def outlaw_attack_counts(self, current_stats, crit_rates=None): attacks_per_second = {} additional_info = {} - # base_energy_regen needs to be reset here due to determine_stats method - self.base_energy_regen = 12. - if self.settings.cycle.blade_flurry: - self.base_energy_regen *= .8 if crit_rates == None: crit_rates = self.get_crit_rates(current_stats) - haste_multiplier = self.stats.get_haste_multiplier_from_rating(current_stats['haste']) * self.true_haste_mod + combat_potency_proc_energy = 15 + (1 * self.traits.fortune_strikes) + self.combat_potency_regen_per_oh = combat_potency_proc_energy * 0.3 * self.stats.oh.speed / 1.4 # the new "normalized" formula + self.combat_potency_from_mg = combat_potency_proc_energy * 0.3 + + self.main_gauche_proc_rate = self.outlaw_mastery_conversion * self.stats.get_mastery_from_rating(current_stats['mastery']) + cost_reducer = self.main_gauche_proc_rate * self.combat_potency_from_mg + + # Compute Main Gauche lumped ability costs + self.run_through_energy_cost = self.get_spell_cost('run_through') - (1 * self.traits.fatebringer) - cost_reducer + self.between_the_eyes_energy_cost = self.get_spell_cost('between_the_eyes') - (1 * self.traits.fatebringer) - cost_reducer + self.pistol_shot_energy_cost = self.get_spell_cost('run_through') - (1 * self.traits.fatebringer) - cost_reducer + self.saber_slash_energy_cost = self.get_spell_cost('saber_slash') - cost_reducer + self.death_from_above_energy_cost = max(0, self.get_spell_cost('death_from_above') - (1 * self.traits.fatebringer) - cost_reducer * (1 + self.settings.num_boss_adds)) + if self.talents.slice_and_dice: + self.slice_and_dice_cost = self.get_spell_cost('slice_and_dice') - (1 * self.traits.fatebringer) + else: + self.roll_the_bones_cost = self.get_spell_cost('roll_the_bones') - (1 * self.traits.fatebringer) + if self.talents.ghostly_strike: + self.ghostly_strike_cost = self.get_spell_cost('ghostly_strike') - cost_reducer - self.attack_speed_increase = self.base_speed_multiplier * haste_multiplier + self.white_swing_downtime = self.settings.response_time / self.get_spell_cd('vanish') - main_gauche_proc_rate = self.combat_mastery_conversion * self.stats.get_mastery_from_rating(current_stats['mastery']) + # Compute dps phases each non-rerolling RtB buff combo AR and not + phases = {} + ar_phases = {} + + keep_chance = 0.0 + keep_tb_chance = 0.0 + keep_shark_chance = 0.0 + keep_gm_chance = 0.0 + maintainence_buff_duration = 6 * (1 + self.settings.finisher_threshold) + + if self.talents.slice_and_dice: + aps_normal = self.outlaw_attack_counts_mincycle(current_stats, snd=True, duration=maintainence_buff_duration) + aps_ar = self.outlaw_attack_counts_mincycle(current_stats, snd=True, ar=True, duration=self.ar_duration) + else: + for phase in self.settings.cycle.keep_list: + jolly = 'jr' in phase + melee = 'gm' in phase + buried = 'bt' in phase + broadsides = 'b' in phase + true_bearing = 'tb' in phase + shark = 's' in phase + + chance = self.rtb_probabilities[len(phase)] / self.rtb_buff_count[len(phase)] + aps = self.outlaw_attack_counts_mincycle(current_stats, jolly=jolly, + melee=melee, buried=buried, broadsides=broadsides, shark=shark, true_bearing=true_bearing, + duration=maintainence_buff_duration) + aps_ar = self.outlaw_attack_counts_mincycle(current_stats, ar=True, jolly=jolly, + melee=melee, buried=buried, broadsides=broadsides, shark=shark, true_bearing=true_bearing, + duration=self.ar_duration) + phases[phase] = (chance, aps) + ar_phases[phase] = (chance, aps_ar) + keep_chance += chance + if melee: + keep_gm_chance += chance + if true_bearing: + keep_tb_chance += chance + if shark: + keep_shark_chance += chance + keep_gm_uptime = keep_gm_chance / keep_chance + keep_tb_uptime = keep_tb_chance / keep_chance + keep_shark_uptime = keep_shark_chance / keep_chance + # Merge AR and non-AR into single phases + aps_keep = self.merge_attacks_per_second(phases, total_time=keep_chance) + aps_keep_ar = self.merge_attacks_per_second(ar_phases, total_time=keep_chance) + # Technically there is a convergence relationship here but ignoring it + if self.talents.alacrity: + alacrity_stacks = self.get_average_alacrity(aps_keep) + alacrity_stacks_ar = self.get_average_alacrity(aps_keep_ar) + else: + alacrity_stacks = 0 + alacrity_stacks_ar = 0 + # Now compute the average time for each reroll + phases = {} + ar_phases = {} + net_reroll_time = 0.0 + net_reroll_time_ar = 0.0 + reroll_tb_time = 0.0 + reroll_shark_time = 0.0 + reroll_gm_time = 0.0 + for phase in self.settings.cycle.reroll_list: + jolly = 'jr' in phase + melee = 'gm' in phase + buried = 'bt' in phase + broadsides = 'b' in phase + true_bearing = 'tb' in phase + shark = 's' in phase + + chance = self.rtb_probabilities[len(phase)] / self.rtb_buff_count[len(phase)] + aps, reroll_time = self.outlaw_attack_counts_reroll(current_stats, jolly=jolly, + melee=melee, buried=buried, broadsides=broadsides, alacrity_stacks=alacrity_stacks) + aps_ar, reroll_time_ar = self.outlaw_attack_counts_reroll(current_stats, ar=True, jolly=jolly, + melee=melee, buried=buried, broadsides=broadsides, alacrity_stacks=alacrity_stacks_ar) + phases[phase] = (chance * reroll_time, aps) + ar_phases[phase] = (chance * reroll_time_ar, aps_ar) + net_reroll_time += chance * reroll_time + net_reroll_time_ar += chance * reroll_time_ar + if true_bearing: + reroll_tb_time += chance * reroll_time + if shark: + reroll_shark_time += chance * reroll_time + if melee: + reroll_gm_time += chance * reroll_time + + # Check for reroll time, to protect from divide by zero + if net_reroll_time: + reroll_tb_uptime = reroll_tb_time / net_reroll_time + reroll_shark_uptime = reroll_shark_time / net_reroll_time + reroll_gm_uptime = reroll_gm_time / net_reroll_time + else: + reroll_tb_uptime = 0 + reroll_shark_uptime = 0 + reroll_gm_uptime = 0 + + aps_reroll = self.merge_attacks_per_second(phases, total_time=net_reroll_time) + aps_reroll_ar = self.merge_attacks_per_second(phases, total_time=net_reroll_time_ar) + # Now combine the reroll and keep dicts + rtb_keep_duration = 6 * (1+ self.settings.finisher_threshold) + # Will pandemic into RtB based on keep_chance + rtb_keep_duration *= 1 + (0.3 * keep_chance) + reroll_duration = net_reroll_time * len(self.settings.cycle.reroll_list) + + ar_reroll_duration = net_reroll_time_ar + + phases = {'keep': (rtb_keep_duration, aps_keep), + 'reroll': (reroll_duration, aps_reroll)} + aps_normal = self.merge_attacks_per_second(phases, rtb_keep_duration + reroll_duration) + phases = {'keep': (rtb_keep_duration, aps_keep_ar), + 'reroll': (ar_reroll_duration, aps_reroll_ar)} + aps_ar = self.merge_attacks_per_second(phases, rtb_keep_duration + ar_reroll_duration) + + keep_uptime = rtb_keep_duration / (rtb_keep_duration + reroll_duration) + tb_uptime = (keep_uptime * keep_tb_uptime) + (1 - keep_uptime) * reroll_tb_uptime + gm_uptime = (keep_uptime * keep_gm_uptime) + (1 - keep_uptime) * reroll_gm_uptime + shark_uptime = (keep_uptime * keep_shark_uptime) + (1 - keep_uptime) * reroll_shark_uptime + + # Determine AR uptime and merge the two distributions + attacks_per_second = self.merge_attacks_per_second({'normal': (self.ar_cd - self.ar_duration, aps_normal), + 'ar': (self.ar_duration, aps_ar)}, total_time=self.ar_cd) + ar_uptime = self.ar_duration / self.ar_cd + tb_seconds_per_second = 0 + + ar_cd_modifier = 1 + # If RtB loop on AR cooldown + if not self.talents.slice_and_dice: + loop_counter = 0 + while (loop_counter < 20): + loop_counter +=1 + ar_cd = self.ar_cd * ar_cd_modifier + cp_spend_per_second = 0 + for ability in attacks_per_second: + if ability in self.finisher_damage_sources: + for cp in range(7): + cp_spend_per_second += attacks_per_second[ability][cp] * cp + #tb_seconds_per_second = 2 * cp_spend_per_second * tb_uptime + ar_cd_modifier = (1 - (2 * tb_uptime) / (1 / cp_spend_per_second + 2 * tb_uptime)) + new_ar_cd = self.ar_cd * ar_cd_modifier + attacks_per_second = self.merge_attacks_per_second({'normal': (new_ar_cd - self.ar_duration, aps_normal), + 'ar': (self.ar_duration, aps_ar)}, total_time=new_ar_cd) + if ar_cd - new_ar_cd < 0.1: + break + #else: + #old_ar_cd = new_ar_cd + + ar_uptime = self.ar_duration / ar_cd + + #Vanish on cooldown + attacks_per_second['vanish'] = 1 / self.get_spell_cd('vanish') + + # Add in Cannonball and Killing Spree + if self.talents.killing_spree: + ksp_cd = self.get_spell_cd('killing_spree') / (1. + tb_seconds_per_second) + #ksp is 7 hits per hand + attacks_per_second['killing_spree'] = 7 / ksp_cd + if self.talents.cannonball_barrage: + cannonball_barrage_cd = self.get_spell_cd('cannonball_barrage') / (1. + tb_seconds_per_second) + attacks_per_second['cannonball_barrage'] = 1 / cannonball_barrage_cd + + # Figure swing timer and add Main Gauche + attack_speed_multiplier = self.get_attack_speed_multiplier(current_stats, snd=self.talents.slice_and_dice) + attack_speed_multiplier *= (1 + (0.2 * ar_uptime)) + if not self.talents.slice_and_dice: + attack_speed_multiplier *= (1 + (0.5 * gm_uptime)) + elif self.talents.slice_and_dice and self.traits.loaded_dice: + buffed_snd_uptime = (self.settings.finisher_threshold + 1) * 6 / self.ar_cd + attack_speed_multiplier *= 1 + (0.3 * buffed_snd_uptime) + swing_timer = self.stats.mh.speed / (attack_speed_multiplier * (1 - self.white_swing_downtime)) + attacks_per_second['mh_autoattacks'] = 1 / swing_timer + attacks_per_second['oh_autoattacks'] = 1 / swing_timer + attacks_per_second['main_gauche'] = self.main_gauche_proc_rate * attacks_per_second['mh_autoattacks'] * self.dual_wield_mh_hit_chance() + + # Add in Main Gauche + for ability in attacks_per_second: + if ability in ['ambush', 'ghostly_strike', 'killing_spree', 'saber_slash']: + attacks_per_second['main_gauche'] += self.main_gauche_proc_rate * attacks_per_second[ability] + elif ability in ['death_from_above_pulse', 'death_from_above_strike','run_through',]: + attacks_per_second['main_gauche'] += sum(attacks_per_second[ability]) * self.main_gauche_proc_rate + + if not self.talents.slice_and_dice: + crit_mod = 1 + (0.25 * shark_uptime) + for ability in crit_rates: + if ability == 'between_the_eyes' and self.settings.cycle.between_the_eyes_policy == 'shark': + crit_rates[ability] += 0.25 + else: + crit_rates[ability] += crit_mod + + if self.traits.greed: + attacks_per_second['greed'] = 0.35 * sum(attacks_per_second['run_through']) + + if self.traits.blunderbuss: + attacks_per_second['blunderbuss'] = 0.33 * attacks_per_second['pistol_shot'] + attacks_per_second['pistol_shot'] -= attacks_per_second['blunderbuss'] + + return attacks_per_second, crit_rates, additional_info + + # Probably don't actually need Shark or True Bearing here but simpler + def outlaw_attack_counts_mincycle(self, current_stats, snd=False, ar=False, jolly=False, melee=False, + buried=False, broadsides=False, duration=30, shark=False, true_bearing=True): + + maintainence_buff = 'slice_and_dice' if snd else 'roll_the_bones' + attack_speed_multiplier = self.get_attack_speed_multiplier(current_stats, snd, melee, ar) + energy_regen = self.get_energy_regen(current_stats, buried, ar, snd) - combat_potency_regen_per_oh = 15 * .2 * self.stats.oh.speed / 1.4 # the new "normalized" formula - combat_potency_from_mg = 15 * .2 - FINISHER_SIZE = 5 - ruthlessness_value = 1 # 1CP gained at 20% chance per CP spent (5CP spent means 1 is always added) - - if ar: - self.attack_speed_increase *= 1.2 - self.base_energy_regen *= 2.0 - if self.talents.lemon_zest: - self.base_energy_regen *= 1 + .05 * (1 + min(self.settings.num_boss_adds, 2)) gcd_size = 1.0 + self.settings.latency if ar: gcd_size -= .2 - cp_per_cpg = 1. - dfa_cd = self.get_spell_cd('death_from_above') + self.settings.response_time - - # Combine energy cost scalers to reduce function calls (ie, 40% reduced energy cost). Assume multiplicative. - cost_modifier = self.stats.gear_buffs.rogue_t15_4pc_modifier() - # Turn the cost of the ability into the net loss of energy by reducing it by the energy gained from MG - cost_reducer = main_gauche_proc_rate * combat_potency_from_mg - - eviscerate_energy_cost = self.get_spell_stats('eviscerate', cost_mod=cost_modifier)[0] - eviscerate_energy_cost -= cost_reducer - eviscerate_energy_cost -= FINISHER_SIZE * self.relentless_strikes_energy_return_per_cp - revealing_strike_energy_cost = self.get_spell_stats('revealing_strike', cost_mod=cost_modifier)[0] - revealing_strike_energy_cost -= cost_reducer - sinister_strike_energy_cost = self.get_spell_stats('sinister_strike', cost_mod=cost_modifier)[0] - sinister_strike_energy_cost -= cost_reducer - death_from_above_energy_cost = self.get_spell_stats('death_from_above', cost_mod=cost_modifier)[0] - death_from_above_energy_cost -= cost_reducer * (2 + self.settings.num_boss_adds) - #need to reduce the cost of DFA by the strike's MG proc ... - #but also the MG procs from the AOE which hits the main target plus each additional add (strike + aoe) - if self.stats.gear_buffs.rogue_t16_2pc_bonus(): - sinister_strike_energy_cost -= 15 * self.extra_cp_chance - - ## Base CPs and Attacks - #Autoattacks - white_swing_downtime = 0 - if self.swing_reset_spacing is not None and not ar: - white_swing_downtime += self.settings.response_time / self.swing_reset_spacing #from vanish - swing_timer_mh = self.stats.mh.speed / self.attack_speed_increase - swing_timer_mh = self.stats.oh.speed / self.attack_speed_increase - - attacks_per_second['mh_autoattacks'] = self.attack_speed_increase / self.stats.mh.speed * (1 - white_swing_downtime) - attacks_per_second['oh_autoattacks'] = self.attack_speed_increase / self.stats.oh.speed * (1 - white_swing_downtime) - #swing delays should be handled here - if self.talents.death_from_above and not ar: - lost_swings_mh = self.lost_swings_from_swing_delay(1.3, self.stats.mh.speed / self.attack_speed_increase) - lost_swings_oh = self.lost_swings_from_swing_delay(1.3, self.stats.oh.speed / self.attack_speed_increase) - - attacks_per_second['mh_autoattacks'] -= lost_swings_mh / dfa_cd - attacks_per_second['oh_autoattacks'] -= lost_swings_oh / dfa_cd - - attacks_per_second['mh_autoattack_hits'] = attacks_per_second['mh_autoattacks'] * self.dw_mh_hit_chance - attacks_per_second['oh_autoattack_hits'] = attacks_per_second['oh_autoattacks'] * self.dw_oh_hit_chance - attacks_per_second['main_gauche'] = attacks_per_second['mh_autoattack_hits'] * main_gauche_proc_rate - combat_potency_regen = attacks_per_second['oh_autoattack_hits'] * combat_potency_regen_per_oh - - #Base energy - bonus_energy_from_openers = self.get_bonus_energy_from_openers('sinister_strike', 'revealing_strike') - if self.settings.opener_name in ('ambush', 'garrote'): - attacks_per_second[self.settings.opener_name] = self.total_openers_per_second - attacks_per_second['main_gauche'] += self.total_openers_per_second * main_gauche_proc_rate - if self.talents.death_from_above and not ar: - attacks_per_second['main_gauche'] += (1 + self.settings.num_boss_adds) * main_gauche_proc_rate / dfa_cd - combat_potency_regen += combat_potency_from_mg * attacks_per_second['main_gauche'] - energy_regen = self.base_energy_regen * haste_multiplier - if self.stats.gear_buffs.rogue_t17_4pc_lfr: - #http://www.wolframalpha.com/input/?i=1.1307+*+%281+-+e+**+%28-1+*+1.1+*+6%2F+60%29%29 - #https://twitter.com/Celestalon/status/525350819856535552 - energy_regen *= 1 + (.11778034322021550695 * .3) #11% uptime on 30% boost) - energy_regen += self.bonus_energy_regen + combat_potency_regen + bonus_energy_from_openers - #Rough idea to factor in a full energy bar - if not ar: - energy_regen += self.max_energy / self.settings.duration - - #Base actions - rvs_interval = self.rvs_duration - if self.settings.cycle.revealing_strike_pooling: - min_energy_while_pooling = energy_regen * gcd_size - max_energy_while_pooling = self.max_energy - 20 - rvs_interval += (max_energy_while_pooling - min_energy_while_pooling) / energy_regen - - #Minicycle sizes and cpg_per_finisher stats - if self.talents.anticipation: - ss_per_finisher = (FINISHER_SIZE - ruthlessness_value) / (cp_per_cpg + self.extra_cp_chance) - else: - ss_per_finisher = self.combat_cpg_per_finisher(1, 0) - cp_per_finisher = FINISHER_SIZE - energy_cost_for_cpgs = ss_per_finisher * sinister_strike_energy_cost - total_eviscerate_cost = energy_cost_for_cpgs + eviscerate_energy_cost - - ss_per_snd = ss_per_finisher - snd_size = FINISHER_SIZE - snd_base_cost = 25 - snd_cost = ss_per_snd * sinister_strike_energy_cost + snd_base_cost - snd_size * self.relentless_strikes_energy_return_per_cp - snd_duration = 6 + 6 * (snd_size + self.stats.gear_buffs.rogue_t15_2pc_bonus_cp()) - energy_spent_on_snd = snd_cost / snd_duration - - #Base Actions - #marked for death CD - self.combat_cd_delay = (.5 * total_eviscerate_cost) / (2 * energy_regen) - marked_for_death_cd = self.get_spell_cd('marked_for_death') + self.combat_cd_delay + self.settings.response_time - if self.talents.marked_for_death: - energy_regen -= 10. / marked_for_death_cd - energy_regen -= revealing_strike_energy_cost / rvs_interval - - energy_for_dfa = 0 - if self.talents.death_from_above and not ar: - #dfa_gap probably should be handled more accurately especially in the non-anticipation case - dfa_interval = 1./(dfa_cd) - energy_for_dfa = energy_cost_for_cpgs + death_from_above_energy_cost - energy_for_dfa -= cp_per_finisher * self.relentless_strikes_energy_return_per_cp - energy_for_dfa *= dfa_interval - - attacks_per_second['death_from_above'] = dfa_interval - attacks_per_second['death_from_above_strike'] = [0, 0, 0, 0, 0, dfa_interval] - attacks_per_second['death_from_above_pulse'] = [0, 0, 0, 0, 0, dfa_interval * (self.settings.num_boss_adds+1)] - - #Base CPGs - attacks_per_second['sinister_strike_base'] = ss_per_snd / snd_duration - if self.talents.death_from_above and not ar: - attacks_per_second['sinister_strike_base'] += ss_per_finisher / (1/dfa_interval) - attacks_per_second['revealing_strike'] = 1. / rvs_interval - extra_finishers_per_second = attacks_per_second['revealing_strike'] / 5. - #Scaling CPGs - free_gcd = 1./gcd_size - free_gcd -= 1./snd_duration + (attacks_per_second['sinister_strike_base'] + attacks_per_second['revealing_strike'] + extra_finishers_per_second) - if self.talents.marked_for_death: - free_gcd -= (1. / marked_for_death_cd) - #2 seconds is an approximation of GCD loss while in air + max_cps = 5 + if self.talents.deeper_stratagem: + max_cps += 1 + + #fetch minicycle value + minicycle_key = (self.settings.finisher_threshold, bool(self.talents.deeper_stratagem), bool(self.talents.quick_draw), + bool(self.talents.swordmaster), broadsides, jolly) + ss_count, ps_count, finisher_list = self.minicycle_table[minicycle_key] + + # set up our initial budgets + energy_budget = duration * energy_regen + gcd_budget = duration / gcd_size + + #since artifacts we'll just compute a one handed swing timer if self.talents.death_from_above and not ar: - free_gcd -= dfa_interval * (2. / gcd_size) #wowhead claims a 2s GCD - energy_available_for_evis = energy_regen - energy_spent_on_snd - energy_for_dfa - total_evis_per_second = energy_available_for_evis / total_eviscerate_cost - evisc_actions_per_second = (total_evis_per_second * ss_per_finisher + total_evis_per_second) - if self.stats.gear_buffs.rogue_t17_4pc: - #http://www.wolframalpha.com/input/?i=sum+of+.2%5Ex+from+x%3D1+to+inf - #This increases the frequency of Eviscerates by 25% for every Evisc cast - evisc_actions_per_second += total_evis_per_second * .25 - attacks_per_second['sinister_strike'] = total_evis_per_second * ss_per_finisher - # If GCD capped - if evisc_actions_per_second > free_gcd: - gcd_cap_mod = evisc_actions_per_second / free_gcd - attacks_per_second['sinister_strike'] = attacks_per_second['sinister_strike'] / gcd_cap_mod - total_evis_per_second = total_evis_per_second / gcd_cap_mod - # Reintroduce flat gcds - attacks_per_second['sinister_strike'] += attacks_per_second['sinister_strike_base'] - attacks_per_second['main_gauche'] += (attacks_per_second['sinister_strike'] + attacks_per_second['revealing_strike'] + - total_evis_per_second) * main_gauche_proc_rate + dfa_cd = self.get_spell_cd('death_from_above') + self.settings.response_time - (10 * true_bearing) + dfa_count = duration / dfa_cd + dfa_lost_swings = self.lost_swings_from_swing_delay(1.5, self.stats.mh.speed / attack_speed_multiplier) + dfa_energy_lost = dfa_lost_swings * (self.main_gauche_proc_rate * self.combat_potency_from_mg + self.combat_potency_regen_per_oh) + energy_budget -= dfa_energy_lost + + mg_cp_energy = self.get_mg_cp_regen_from_haste(attack_speed_multiplier) * (1 - self.white_swing_downtime) + energy_budget += mg_cp_energy + + attacks_per_second = {} + + #consider the cost of building to max cps and using rtb + energy_budget -= ss_count * self.saber_slash_energy_cost + #don't account for ps energy becuase ps is free + if snd: + energy_budget -= self.slice_and_dice_cost + else: + energy_budget -= self.roll_the_bones_cost + gcd_budget -= (ss_count + ps_count + 1) + attacks_per_second['saber_slash'] = (ss_count + ps_count) / duration + attacks_per_second['pistol_shot'] = ps_count / duration + + attacks_per_second[maintainence_buff] = [v / duration for v in finisher_list] + + if (shark and self.settings.cycle.between_the_eyes_policy == 'shark') or self.settings.cycle.between_the_eyes_policy == 'always': + bte_count = duration / (20 + self.settings.response_time - (10 * true_bearing)) + attacks_per_second['between_the_eyes'] = [v * bte_count / duration for v in finisher_list] + attacks_per_second['pistol_shot'] += bte_count * ps_count / duration + attacks_per_second['saber_slash'] += bte_count * (ss_count + ps_count) / duration + energy_budget -= (bte_count * ss_count) * self.saber_slash_energy_cost + energy_budget -= bte_count * self.between_the_eyes_energy_cost + gcd_budget -= bte_count * (ss_count + ps_count + 1) + + #consider DfA if self.talents.death_from_above and not ar: - attacks_per_second['main_gauche'] += attacks_per_second['death_from_above_strike'][5] * main_gauche_proc_rate - - #attacks_per_second['eviscerate'] = [finisher_chance * total_evis_per_second for finisher_chance in finisher_size_breakdown] - attacks_per_second['eviscerate'] = [0,0,0,0,0,total_evis_per_second] - for opener, cps in [('ambush', 2), ('garrote', 1)]: - if opener in attacks_per_second: - extra_finishers_per_second += attacks_per_second[opener] * cps / 5 - attacks_per_second['eviscerate'][5] += extra_finishers_per_second + energy_budget -= ss_count * dfa_count * self.saber_slash_energy_cost + energy_budget -= dfa_count * self.death_from_above_energy_cost + attacks_per_second['saber_slash'] += (ss_count + ps_count) * dfa_count / duration + attacks_per_second['pistol_shot'] += ps_count * dfa_count / duration + attacks_per_second['death_from_above_strike'] = [v * dfa_count / duration for v in finisher_list] + attacks_per_second['death_from_above_pulse'] = [v * dfa_count / duration for v in finisher_list] + #DfA forces a 2 second GCD + gcd_budget -= dfa_count * (ss_count + ps_count + 2) + + bonus_cps = 0 + attacks_per_second['run_through'] = [0] * 7 + + #consider ghostly strike + if self.talents.ghostly_strike: + gs_count = duration / 15 + bonus_cps += gs_count * (1 + broadsides) + gs_energy = self.ghostly_strike_cost * gs_count + energy_budget -= gs_energy + gcd_budget -= gs_count + attacks_per_second['ghostly_strike'] = gs_count / duration + + #consider MfD if self.talents.marked_for_death: - attacks_per_second['eviscerate'][5] += 1. / marked_for_death_cd - if self.stats.gear_buffs.rogue_t17_4pc: - attacks_per_second['eviscerate'][5] *= 1.25 - - #self.current_variables['cp_spent_on_damage_finishers_per_second'] = (total_evis_per_second) * cp_per_finisher - if 'garrote' in attacks_per_second: - attacks_per_second['garrote_ticks'] = 6 * attacks_per_second['garrote'] - - time_at_level = 4 / attacks_per_second['sinister_strike'] - cycle_duration = 3 * time_at_level + 15 - if self.level == 100: - self.bandits_guile_multiplier = 1 + (0*time_at_level + .1*time_at_level + .2*time_at_level + .5 * 15) / cycle_duration - else: - avg_stacks = (3 * time_at_level + 45) / cycle_duration #45 is the duration (15s) multiplied by the stack power (30% BG) - self.bandits_guile_multiplier = 1 + .1 * avg_stacks - - if not ar: - ks_duration = 3 - if self.stats.gear_buffs.rogue_pvp_wod_4pc: - ks_duration += 1 - final_ks_cd = self.rb_actual_cd(attacks_per_second, self.tmp_ks_cd) + self.major_cd_delay + self.ar_duration/2. - if not self.settings.cycle.ksp_immediately: - final_ks_cd += (3 * time_at_level)/2 * (3 * time_at_level)/cycle_duration - attacks_per_second['mh_killing_spree'] = (1 + 2*ks_duration) / (final_ks_cd + self.settings.response_time) - attacks_per_second['oh_killing_spree'] = (1 + 2*ks_duration) / (final_ks_cd + self.settings.response_time) - attacks_per_second['main_gauche'] += attacks_per_second['mh_killing_spree'] * main_gauche_proc_rate - - if self.talents.shadow_reflection: - sr_uptime = 8. / self.get_spell_cd('shadow_reflection') - lst = ('sinister_strike', 'eviscerate', 'revealing_strike') - if not ar: - lst += ('mh_killing_spree', 'oh_killing_spree') - for ability in lst: - if type(attacks_per_second[ability]) in (tuple, list): - attacks_per_second['sr_'+ability] = [0,0,0,0,0,0] - for i in xrange(1, 6): - attacks_per_second['sr_'+ability][i] = sr_uptime * attacks_per_second[ability][i] + mfd_base_count = 1 + self.settings.duration / self.get_spell_cd('marked_for_death') + mfd_cps = (5. + self.talents.deeper_stratagem) * (mfd_base_count + self.settings.marked_for_death_resets) + bonus_cps += mfd_cps + + #consider Curse of the Dreadblades + if self.traits.curse_of_the_dreadblades: + curse_cd_multiplier = duration / self.cotd_cd + #curse lasts 12 seconds, half to RT, half to CP builders + curse_gcds = (12 / gcd_size) * curse_cd_multiplier + rt_count = curse_gcds / 2 + ps_per_ss = 0.35 + if self.talents.swordmaster: + ps_per_ss += 0.1 + if jolly: + ps_per_ss += 0.25 + + ss_count = (curse_gcds / 2) * (1 / (ps_per_ss + 1)) + ps_count = (curse_gcds / 2) * (ps_per_ss / (ps_per_ss + 1)) + + attacks_per_second['saber_slash'] += ss_count / self.cotd_cd + attacks_per_second['pistol_shot'] += ps_count / self.cotd_cd + attacks_per_second['run_through'][max_cps] += rt_count / self.cotd_cd + gcd_budget -= curse_gcds + energy_budget -= (ss_count * self.saber_slash_energy_cost) + (rt_count * self.run_through_energy_cost) + + #Curse gives 10 cps with anticipation so 5 left over + if self.talents.anticipation: + bonus_cps += 5 * curse_cd_multiplier + + #spend bonus cps for max cp RTs + + extra_rt = (bonus_cps / max_cps) / duration + gcd_budget -= extra_rt + energy_budget -= extra_rt * self.run_through_energy_cost + attacks_per_second['run_through'][max_cps] += extra_rt + + #Burn the rest of our energy until you run out of energy or gcds + gcds_per_minicycle = ss_count + ps_count + 1 + energy_per_minicycle = ss_count * self.saber_slash_energy_cost + self.run_through_energy_cost + + alacrity_stacks = 0 + loop_counter = 0 + while energy_budget > 0.1 and gcd_budget > 0.1: + if loop_counter > 20: + raise ConvergenceErrorException(_('Mini-cycles failed to converge.')) + + loop_counter += 1 + minicycle_count = min(gcd_budget / gcds_per_minicycle, energy_budget / energy_per_minicycle) + attacks_per_second['saber_slash'] += minicycle_count * (ss_count + ps_count) / duration + attacks_per_second['pistol_shot'] += minicycle_count * ps_count / duration + for i, v in enumerate(finisher_list): + attacks_per_second['run_through'][i] += minicycle_count * v / duration + + #Don't need to converge if we don't have alacrity + if not self.talents.alacrity: + break + else: + energy_budget -= minicycle_count * energy_per_minicycle + gcd_budget -= minicycle_count * gcds_per_minicycle + + #ar doubles the effect of alacrity while up + old_alacrity_regen = energy_regen * (1 + (alacrity_stacks *0.02)) * (1 + int(ar)) + new_alacrity_stacks = self.get_average_alacrity(attacks_per_second) + new_alacrity_regen = energy_regen * (1 + (new_alacrity_stacks *0.02)) * (1 + int(ar)) + energy_budget += (new_alacrity_regen - old_alacrity_regen) * duration + #compute new CP/MG regen + old_cp_mg = self.get_mg_cp_regen_from_haste(attack_speed_multiplier * 1 + (0.02 * alacrity_stacks)) + new_cp_mg = self.get_mg_cp_regen_from_haste(attack_speed_multiplier * 1 + (0.02 * new_alacrity_stacks)) + energy_budget += new_cp_mg - old_cp_mg + alacrity_stacks = new_alacrity_stacks + + #skip white swings and mg procs because we can do those later + return attacks_per_second + + def outlaw_attack_counts_reroll(self, current_stats, ar=False, + jolly=False, melee=False, buried=False, broadsides=False, alacrity_stacks=0): + #fetch minicycle value + minicycle_key = (self.settings.finisher_threshold, bool(self.talents.deeper_stratagem), bool(self.talents.quick_draw), + bool(self.talents.swordmaster), broadsides, jolly) + ss_count, ps_count, finisher_list = self.minicycle_table[minicycle_key] + reroll_energy_cost = (ss_count * self.saber_slash_energy_cost) + self.roll_the_bones_cost + + energy_regen = self.get_energy_regen(current_stats, buried, ar, alacrity_stacks) + attack_speed_multiplier = self.get_attack_speed_multiplier(current_stats, False, melee, ar, alacrity_stacks) + mg_cp_energy = self.get_mg_cp_regen_from_haste(attack_speed_multiplier) + + total_regen = energy_regen + mg_cp_energy + reroll_time = reroll_energy_cost / total_regen + attacks_per_second = {} + attacks_per_second['saber_slash'] = (ss_count + ps_count) / reroll_time + attacks_per_second['pistol_shot'] = ps_count / reroll_time + attacks_per_second['roll_the_bones'] = [v / reroll_time for v in finisher_list] + return attacks_per_second, reroll_time + + #dict of (probability, aps) pairs + def merge_attacks_per_second(self, aps_dicts, total_time=1.0): + attacks_per_second = {} + for key in aps_dicts: + proportion, aps = aps_dicts[key] + uptime = proportion / total_time + for ability in aps: + if ability in attacks_per_second: + if isinstance(attacks_per_second[ability], list): + for cp in range(7): + attacks_per_second[ability][cp] += uptime * aps[ability][cp] + else: + attacks_per_second[ability] += uptime * aps[ability] else: - attacks_per_second['sr_'+ability] = sr_uptime * attacks_per_second[ability] - - self.get_poison_counts(attacks_per_second, current_stats) - - #print attacks_per_second - return attacks_per_second, crit_rates, additional_info - - def rb_actual_cds(self, attacks_per_second, base_cds, avg_rb_effect=10): - final_cds = {} - # If it's best to always use 5CP finishers as combat now, it should continue to be so, this is simpler and faster - for cd_name in base_cds: - final_cds[cd_name] = base_cds[cd_name] * self.rb_cd_modifier(attacks_per_second) - return final_cds - - def rb_actual_cd(self, attacks_per_second, base_cd, avg_rb_effect=10): - # If it's best to always use 5CP finishers as combat now, it should continue to be so, this is simpler and faster - return base_cd * self.rb_cd_modifier(attacks_per_second) - - def rb_cd_modifier(self, attacks_per_second, avg_rb_effect=10): - # If it's best to always use 5CP finishers as combat now, it should continue to be so, this is simpler and faster - offensive_finisher_rate = attacks_per_second['eviscerate'][5] - if 'death_from_above' in attacks_per_second: - offensive_finisher_rate += attacks_per_second['death_from_above'] - return (1./avg_rb_effect) / (offensive_finisher_rate + (1./avg_rb_effect)) - - def combat_attack_counts_ar(self, current_stats, crit_rates=None): - return self.combat_attack_counts(current_stats, ar=True, crit_rates=crit_rates) - - def combat_attack_counts_none(self, current_stats, crit_rates=None): - return self.combat_attack_counts(current_stats, crit_rates=crit_rates) + if isinstance(aps[ability], list): + attacks_per_second[ability] = copy(aps[ability]) + for cp in range(7): + attacks_per_second[ability][cp] *= uptime + else: + attacks_per_second[ability] = uptime * aps[ability] + return attacks_per_second + + def get_mg_cp_regen_from_haste(self, haste_multiplier): + swing_per_second = self.stats.mh.speed * self.dw_mh_hit_chance / haste_multiplier + mg_regen = self.main_gauche_proc_rate * self.combat_potency_from_mg * swing_per_second + cp_regen = self.combat_potency_regen_per_oh * swing_per_second + return mg_regen + cp_regen + + def get_max_energy(self): + self.max_energy = 100 + if self.talents.vigor or self.stats.gear_buffs.soul_of_the_shadowblade: + self.max_energy += 50 + if self.race.expansive_mind: + self.max_energy = round(self.max_energy * 1.05, 0) + return self.max_energy ########################################################################### # Subtlety DPS functions ########################################################################### + #Legion TODO: + + #Artifact: + # 'flickering_shadows' + + #Rotation details: + #Combo Point loss + #Shuriken storm dances details + #weaponmaster bonus cp gen + def subtlety_dps_estimate(self): return sum(self.subtlety_dps_breakdown().values()) @@ -1583,80 +1953,220 @@ def subtlety_dps_breakdown(self): if not self.settings.is_subtlety_rogue(): raise InputNotModeledException(_('You must specify a subtlety cycle to match your subtlety spec.')) - if self.stats.mh.type != 'dagger' and self.settings.cycle.use_hemorrhage != 'always': - raise InputNotModeledException(_('Subtlety modeling requires a MH dagger if Hemorrhage is not the main combo point builder')) - - if self.settings.cycle.use_hemorrhage not in ('always', 'never', 'uptime'): - if float(self.settings.cycle.use_hemorrhage) <= 0: - raise InputNotModeledException(_('Hemorrhage usage must be set to always, never or a positive number')) - if float(self.settings.cycle.use_hemorrhage) > self.settings.duration: - raise InputNotModeledException(_('Interval between Hemorrhages cannot be higher than the fight duration')) - - #set readiness coefficient - self.readiness_spec_conversion = self.subtlety_readiness_conversion - self.spec_convergence_stats = ['haste', 'multistrike'] - - #overrides setting, using Ambush + Vanish on CD is critical - self.settings.use_opener = 'always' - self.settings.opener_name = 'ambush' - # Sanguinary Vein - self.damage_modifier_cache = 1.25 - - self.sc_trigger_rate = 0 - mos_value = .1 - # leveling perks - if self.level == 100: - mos_value += .05 - self.ability_cds['vanish'] = 90 - - #update spec specific proc rates - if getattr(self.stats.procs, 'legendary_capacitive_meta'): - getattr(self.stats.procs, 'legendary_capacitive_meta').proc_rate_modifier = 1.114 - + self.cp_builder = self.settings.cycle.cp_builder + if self.cp_builder == 'shuriken_storm': + self.dance_cp_builder = 'shuriken_storm' + elif self.cp_builder == 'backstab': + self.dance_cp_builder = 'shadowstrike' + else: + raise InputNotModeledException(_("{} is not a valid cp_builder").format(self.cp_builder)) + + if self.cp_builder == 'backstab' and self.talents.gloomblade: + self.cp_builder = 'gloomblade' + + self.max_spend_cps = 5 + if self.talents.deeper_stratagem: + self.max_spend_cps += 1 + self.max_store_cps = self.max_spend_cps + if self.talents.anticipation: + self.max_store_cps += 5 + self.set_constants() - self.stat_multipliers['multistrike'] *= 1.05 - self.stat_multipliers['agi'] *= 1.15 - #sinister calling requires convergence to calculate (for now?) - self.spec_needs_converge = True - - self.settings.cycle.raid_crits_per_second = self.get_adv_param('hat_triggers_per_second', self.settings.cycle.raid_crits_per_second, min_bound=0, max_bound=600) - self.settings.cycle.clip_fw = self.get_adv_param('clip_fw', self.settings.cycle.clip_fw, ignore_bounds=True) - - self.vanish_rate = 1. / (self.get_spell_cd('vanish') + self.settings.response_time) + 1. / (self.get_spell_cd('preparation') + self.settings.response_time * 3) #vanish CD + Prep CD - mos_multiplier = 1. + mos_value * (6 + 3 * self.talents.subterfuge * [1, 2][self.glyphs.vanish]) * self.vanish_rate - + + #set up damage modifier list and all relevant modifiers, use None for placeholder values + self.damage_modifiers = modifiers.ModifierList(self.subtlety_damage_sources + ['autoattacks']) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('versatility', None, [], all_damage=True)) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('armor', self.armor_mitigation_multiplier(), ['death_from_above_pulse', + 'death_from_above_strike', 'shuriken_storm', 'eviscerate', 'backstab', 'shadowstrike', 'shuriken_toss', 'autoattacks'], dmg_schools=['physical'])) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('executioner', None, ['eviscerate', 'nightblade_ticks', 'death_from_above_strike', 'death_from_above_pulse'])) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('symbols_of_death', None, [], all_damage=True)) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('stealth_shuriken_storm', None, ['shuriken_storm', 'second_shuriken'])) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('backstab_positional', 1 + 0.2 * self.settings.cycle.positional_uptime, ['backstab'])) + + #Assume 100% Nightblade uptime, TODO: AoE handling + self.damage_modifiers.register_modifier(modifiers.DamageModifier('nightblade', 1.15, [], all_damage=True)) + + #Shadowstrike (Rank 2) deals 25% more damage from stealth + self.damage_modifiers.register_modifier(modifiers.DamageModifier('shadowstrike_rank_2', None, ['shadowstrike'])) + + #Shuriken Combo + self.damage_modifiers.register_modifier(modifiers.DamageModifier('focused_shurikens', None, ['eviscerate', 'death_from_above_strike'])) + + #Generic tuning aura + self.damage_modifiers.register_modifier(modifiers.DamageModifier('subtlety_aura', 1.27, ['death_from_above_pulse', 'death_from_above_strike', + 'backstab', 'eviscerate', 'gloomblade', 'nightblade', 'shadowstrike', 'shuriken_storm', 'shuriken_toss', 'nightblade_ticks', 'shadow_blades', + 'second_shuriken', 'shadow_nova', 'goremaws_bite', 'soul_rip'])) + + #talent specific modifiers + if self.talents.nightstalker: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('nightstalker_full', None, ['shadowstrike', 'shadow_nova'])) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('nightstalker_shuriken_storm', None, ['shuriken_storm'])) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('nightstalker_evis', None, ['eviscerate'])) + # The following creates a blacklist so only AA abilities and procs are affected + other_whitelist = ['shadow_blades', 'soul_rip'] + self.damage_modifiers.register_modifier(modifiers.DamageModifier('nightstalker_other', None, + [item for item in self.subtlety_damage_sources if item not in other_whitelist], blacklist=True, all_damage=True)) + + if self.talents.master_of_subtlety: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('mos_ssk', None, ['shadowstrike'])) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('mos_shuriken_storm', None, ['shuriken_storm'])) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('mos_evis', None, ['eviscerate'])) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('mos_other', None, ['shadowstrike', 'eviscerate', 'shuriken_storm', 'death_from_above_strike'], blacklist=True, all_damage=True)) + + if self.talents.deeper_stratagem: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('deeper_stratagem', 1.05, ['nightblade_ticks', 'eviscerate', 'death_from_above_strike', 'death_from_above_pulse'])) + + if self.talents.dark_shadow: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('dark_shadow_ssk', None, ['shadowstrike'])) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('dark_shadow_storm', None, ['shuriken_storm'])) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('dark_shadow_evis', None, ['eviscerate'])) + self.damage_modifiers.register_modifier(modifiers.DamageModifier('dark_shadow_other', None, ['shadowstrike', 'shuriken_storm', 'eviscerate', + 'backstab', 'goremaws_bite', 'death_from_above_strike'], blacklist=True, all_damage=True)) + + if self.talents.death_from_above: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('dfa_mods', None, ['death_from_above_strike'])) + + #trait specific modifiers + if self.traits.shadow_fangs: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('shadow_fangs', 1.04, [], blacklist=True, dmg_schools=['physical', 'shadow'])) + + if self.traits.finality: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('finality', None, ['nightblade_ticks', 'eviscerate', 'death_from_above_strike'])) + + if self.traits.legionblade: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('legionblade', + 1.05 + (0.005 * (self.traits.legionblade - 1)), [], all_damage=True)) + + if self.traits.shadows_of_the_uncrowned: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('shadows_of_the_uncrowned', 1.1, [], all_damage=True)) + + #gear specific modifiers + if self.stats.gear_buffs.the_dreadlords_deceit: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('the_dreadlords_deceit', None, ['shuriken_storm'])) + + if self.stats.gear_buffs.jeweled_signet_of_melandrus: + self.damage_modifiers.register_modifier(modifiers.DamageModifier('jeweled_signet_of_melandrus', 1.1, ['autoattacks', 'shadow_blades'])) + + if self.stats.gear_buffs.gnawed_thumb_ring: + gtr_mod = 1 + 0.05 * 12 / 180 + self.damage_modifiers.register_modifier(modifiers.DamageModifier('gnawed_thumb_ring', gtr_mod, + ['gloomblade', 'goremaws_bite', 'shadow_blades', 'nightblade_ticks', 'soul_rip', 'shadow_nova'], + dmg_schools=['arcane', 'fire', 'frost', 'holy', 'nature', 'shadow'])) + stats, aps, crits, procs, additional_info = self.determine_stats(self.subtlety_attack_counts) + + self.damage_modifiers.update_modifier_value('executioner', (1 + self.subtlety_mastery_conversion * self.stats.get_mastery_from_rating(stats['mastery']))) + self.damage_modifiers.update_modifier_value('versatility', self.stats.get_versatility_multiplier_from_rating(rating=stats['versatility'])) + self.damage_modifiers.update_modifier_value('stealth_shuriken_storm', 1 + self.stealth_shuriken_uptime * 3) + + infallible_trinket_mod = 1.0 + if self.settings.is_demon: + if getattr(self.stats.procs, 'infallible_tracking_charm_mod'): + ift = getattr(self.stats.procs, 'infallible_tracking_charm_mod') + self.set_rppm_uptime(ift) + infallible_trinket_mod = 1+(ift.uptime *0.10) + + #Symbols of Death + sod_uptime = 10 / self.get_spell_cd('symbols_of_death') + sod_modifier = 0.25 if self.stats.gear_buffs.rogue_t20_2pc else 0.15 + self.damage_modifiers.update_modifier_value('symbols_of_death', 1 + sod_modifier * sod_uptime) + + #Shadowstrike (Rank 2) deals 25% more damage from stealth + #Atm, we assume one Shadowstrike per Vanish + Opener unless Nightstalker was chosen + if 'shadowstrike' in aps: + buffed_shadowstrikes = 1 / self.settings.duration + if not self.talents.nightstalker and aps['vanish']: + buffed_shadowstrikes += aps['vanish'] / aps['shadowstrike'] + self.damage_modifiers.update_modifier_value('shadowstrike_rank_2', 1 + (0.25 * buffed_shadowstrikes)) + else: + self.damage_modifiers.update_modifier_value('shadowstrike_rank_2', 1) + + #Focused Shurikens gets one stack up to 5 per additional enemy hit and increases Evi dmg by 10% per stack + if 'shuriken_storm' in aps: + if self.talents.death_from_above: + storms_per_evis = aps['shuriken_storm'] / (sum(aps['eviscerate']) + sum(aps['death_from_above_strike'])) + else: + storms_per_evis = aps['shuriken_storm'] / sum(aps['eviscerate']) + stacks_per_evis = min(5, storms_per_evis * self.settings.num_boss_adds) + print(stacks_per_evis) + self.damage_modifiers.update_modifier_value('focused_shurikens', 1 + (0.1 * stacks_per_evis)) + else: + self.damage_modifiers.update_modifier_value('focused_shurikens', 1) + + + #nightstalker + if self.talents.nightstalker: + ns_full_multiplier = 0.12 + self.damage_modifiers.update_modifier_value('nightstalker_full', 1 + ns_full_multiplier) + self.damage_modifiers.update_modifier_value('nightstalker_shuriken_storm', 1 + (0.12 * self.stealth_shuriken_uptime)) + self.damage_modifiers.update_modifier_value('nightstalker_evis', 1 + (0.12 * self.stealth_evis_uptime)) + self.damage_modifiers.update_modifier_value('nightstalker_other', 1 + (0.12 * self.stealthed_uptime)) + + #master of subtlety + if self.talents.master_of_subtlety: + mos_full_multiplier = 1.1 + mos_uptime_multipler = 1. + (0.1 * self.mos_uptime) + self.damage_modifiers.update_modifier_value('mos_ssk', mos_full_multiplier) + self.damage_modifiers.update_modifier_value('mos_shuriken_storm', 1 + (0.1 * self.stealth_shuriken_uptime)) + self.damage_modifiers.update_modifier_value('mos_evis', 1 + (0.1 * self.stealth_evis_uptime)) + self.damage_modifiers.update_modifier_value('mos_other', mos_uptime_multipler) + + if self.talents.dark_shadow: + dsh_uptime = aps['shadow_dance'] * (5 if self.talents.subterfuge else 4) + dsh_ssk_uptime = 0 + dsh_storm_uptime = 0 + dsh_evis_uptime = 0 + if 'shadowstrike' in self.dark_shadow_attacks_per_dance and 'shadowstrike' in aps: + dsh_ssk_uptime = self.dark_shadow_attacks_per_dance['shadowstrike'] * aps['shadow_dance'] / aps['shadowstrike'] + if 'shuriken_storm' in self.dark_shadow_attacks_per_dance and 'shuriken_storm' in aps: + dsh_storm_uptime = self.dark_shadow_attacks_per_dance['shuriken_storm'] * aps['shadow_dance'] / aps['shuriken_storm'] + if 'eviscerate' in self.dark_shadow_attacks_per_dance and 'eviscerate' in aps: + dsh_evis_uptime = sum(self.dark_shadow_attacks_per_dance['eviscerate']) * aps['shadow_dance'] / sum(aps['eviscerate']) + self.damage_modifiers.update_modifier_value('dark_shadow_ssk', 1 + (0.3 * dsh_ssk_uptime)) + self.damage_modifiers.update_modifier_value('dark_shadow_storm', 1 + (0.3 * dsh_storm_uptime)) + self.damage_modifiers.update_modifier_value('dark_shadow_evis', 1 + (0.3 * dsh_evis_uptime)) + self.damage_modifiers.update_modifier_value('dark_shadow_other', 1 + (0.3 * dsh_uptime)) + + # Special DfA mod handling + if self.talents.death_from_above: + dfa_mod = 1 + if self.talents.dark_shadow: + dfa_mod *= 1.3 + if self.talents.nightstalker: + dfa_mod *= 1.12 + if self.talents.master_of_subtlety: + dfa_mod *= 1.1 + else: + if self.talents.master_of_subtlety: + dfa_mod *= mos_uptime_multipler + self.damage_modifiers.update_modifier_value('dfa_mods', dfa_mod) + + if self.traits.finality: + #4% increase per cp applied every to every other + finality_damage_boost = 1 + 0.02 * self.settings.finisher_threshold + self.damage_modifiers.update_modifier_value('finality', finality_damage_boost) + + if self.stats.gear_buffs.the_dreadlords_deceit: + avg_dreadlord_stacks = 0.5 / aps['shuriken_storm'] + self.damage_modifiers.update_modifier_value('the_dreadlords_deceit', 1 + (0.25 * avg_dreadlord_stacks)) + damage_breakdown, additional_info = self.compute_damage_from_aps(stats, aps, crits, procs, additional_info) - armor_value = self.target_armor() - find_weakness_damage_boost = 1. / self.max_level_armor_multiplier() - find_weakness_multiplier = 1 + (find_weakness_damage_boost - 1) * additional_info['fw_uptime'] - - #calculate multistrike here for Sub and Assassination, really cheap to calculate - #turns out the 2 chance system yields a very basic linear pattern, the damage modifier is 30% of the multistrike %! - multistrike_multiplier = .3 * 2 * (self.stats.get_multistrike_chance_from_rating(rating=stats['multistrike']) + self.buffs.multistrike_bonus()) - multistrike_multiplier = min(.6, multistrike_multiplier) - + if self.stats.gear_buffs.insignia_of_ravenholdt: + damage_breakdown['insignia_of_ravenholdt'] = self.compute_insignia_of_ravenholdt_damage(stats, damage_breakdown) + for key in damage_breakdown: - if key in ('eviscerate', 'hemorrhage', 'shuriken_toss', 'hemorrhage_dot', 'autoattack'): #'burning_wounds' - damage_breakdown[key] *= find_weakness_multiplier - if key == 'ambush': - #damage_breakdown[key] *= find_weakness_multiplier - damage_breakdown[key] *= 1 + ((1 - additional_info['ambush_no_fw_rate']) * (find_weakness_damage_boost - 1)) - if key == 'backstab': - #damage_breakdown[key] *= find_weakness_multiplier - damage_breakdown[key] *= 1 + additional_info['backstab_fw_rate'] * (find_weakness_damage_boost - 1) - if key in ('rupture', 'sr_rupture', 'rupture_sc'): - damage_breakdown[key] *= 1.1 - if key is not 'rupture_sc': - damage_breakdown[key] *= (1 + multistrike_multiplier) - damage_breakdown[key] *= mos_multiplier - - #discard the loose rupture component to clean up the breakdown - if 'rupture_sc' in damage_breakdown and self.settings.merge_damage: - damage_breakdown['rupture'] += damage_breakdown['rupture_sc'] - del damage_breakdown['rupture_sc'] - + damage_breakdown[key] *= infallible_trinket_mod + + #add AoE damage sources: + if self.settings.num_boss_adds: + for key in damage_breakdown: + if key in ['shuriken_storm', 'second_shuriken', 'shadow_nova']: + damage_breakdown[key] *= 1 + self.settings.num_boss_adds + + if self.stats.gear_buffs.cinidaria_the_symbiote: + damage_breakdown['symbiote_strike'] = self.compute_symbiote_strike_damage(damage_breakdown) + return damage_breakdown def subtlety_attack_counts(self, current_stats, crit_rates=None): @@ -1664,239 +2174,432 @@ def subtlety_attack_counts(self, current_stats, crit_rates=None): additional_info = {} if crit_rates == None: crit_rates = self.get_crit_rates(current_stats) - - base_energy_regen = 10. - max_energy = 100. - if self.stats.gear_buffs.rogue_pvp_4pc_extra_energy(): - max_energy += 30 - if self.talents.lemon_zest: - base_energy_regen *= 1 + .05 * (1 + min(self.settings.num_boss_adds, 2)) - max_energy += 15 - if self.glyphs.energy: - max_energy += 20 - if self.race.expansive_mind: - max_energy = round(max_energy * 1.05, 0) - shd_duration = 8 - if self.level == 100: - shd_duration += 2 - shd_cd = self.get_spell_cd('shadow_dance') + self.settings.response_time + self.major_cd_delay - - cost_modifier = self.stats.gear_buffs.rogue_t15_4pc_reduced_cost() - shd_ambush_cost_modifier = 1. - backstab_cost_mod = cost_modifier - base_eviscerate_cost = self.get_spell_stats('eviscerate', cost_mod=cost_modifier)[0] - base_rupture_cost = self.get_spell_stats('rupture', cost_mod=cost_modifier)[0] - base_hemo_cost = self.get_spell_stats('hemorrhage', cost_mod=cost_modifier)[0] - base_backstab_energy_cost = self.get_spell_stats('backstab', cost_mod=backstab_cost_mod)[0] - sd_ambush_cost = self.get_spell_stats('ambush', cost_mod=shd_ambush_cost_modifier)[0] - 20 - normal_ambush_cost = self.get_spell_stats('ambush')[0] - if self.talents.death_from_above: - self.dfa_cost = self.get_spell_stats('death_from_above', cost_mod=cost_modifier)[0] - - #haste and attack speed - haste_multiplier = self.stats.get_haste_multiplier_from_rating(current_stats['haste']) * self.true_haste_mod - mastery_snd_speed = 1 + .4 * (1 + self.subtlety_mastery_conversion * self.stats.get_mastery_from_rating(current_stats['mastery'])) - attack_speed_multiplier = self.base_speed_multiplier * haste_multiplier * mastery_snd_speed / 1.4 - - cpg_name = 'backstab' - if self.settings.cycle.use_hemorrhage == 'always': - cpg_name = 'hemorrhage' - - #constant and base values - hat_triggers_per_second = self.settings.cycle.raid_crits_per_second - hat_cp_per_second = 1. / (2 + 1. / hat_triggers_per_second) - er_energy = 8. / 2 #8 energy every 2 seconds, assumed full SnD uptime - fw_duration = 10. #17.5s - if self.settings.cycle.clip_fw: - fw_duration -= .5 - attacks_per_second['eviscerate'] = [0,0,0,0,0,0] - attacks_per_second['rupture_ticks'] = [0,0,0,0,0,0] - attacks_per_second['ambush'] = self.total_openers_per_second - attacks_per_second['backstab'] = 0 - attacks_per_second['hemorrhage'] = 0 - cp_per_ambush = 2 - vanish_bonus_stealth = 0 + 3 * self.talents.subterfuge * [1, 2][self.glyphs.vanish] - rupture_ticks_per_cast = 12. - rupture_cd = 24. - hemo_cd = 24. - snd_cd = 36. - base_cp_per_second = hat_cp_per_second * (shd_cd-8.)/shd_cd + self.total_openers_per_second * 2 - if self.stats.gear_buffs.rogue_t15_2pc: - rupture_ticks_per_cast += 2 - rupture_cd += 4 - snd_cd += 6 - - #sinister calling mechanic - sc_scaler = .5 / (.5 + self.sc_trigger_rate) - rupture_cd *= sc_scaler - hemo_cd *= sc_scaler - - #passive energy regen - energy_regen = base_energy_regen * haste_multiplier - if self.stats.gear_buffs.rogue_t17_4pc_lfr: - #http://www.wolframalpha.com/input/?i=1.1307+*+%281+-+e+**+%28-1+*+1.1+*+6%2F+60%29%29 - #https://twitter.com/Celestalon/status/525350819856535552 - energy_regen *= 1 + (.11778034322021550695 * .3) #11% uptime on 30% boost) - energy_regen += self.bonus_energy_regen + max_energy / self.settings.duration + er_energy - energy_regen += self.get_bonus_energy_from_openers() - if self.stats.gear_buffs.rogue_t16_2pc_bonus(): - energy_regen += 2 * hat_cp_per_second - if self.stats.gear_buffs.rogue_t17_2pc: - energy_regen += 60. / shd_cd - if self.stats.gear_buffs.rogue_t17_4pc: - energy_regen -= (base_eviscerate_cost - 25) / shd_cd - - #deal with extra subterfuge ambushes - if self.talents.subterfuge: - attacks_per_second['ambush'] += (1. / self.get_spell_cd('vanish')) * [1., 2.][self.glyphs.vanish] - energy_regen -= (normal_ambush_cost / self.get_spell_cd('vanish')) * [1., 2.][self.glyphs.vanish] - base_cp_per_second += (2. / self.get_spell_cd('vanish')) * [1., 2.][self.glyphs.vanish] - - ##calculations dependent on energy regen - cpg_costs_for_cycle = base_backstab_energy_cost * 5 - if self.settings.cycle.use_hemorrhage == 'always': - cpg_costs_for_cycle = base_hemo_cost * 5 - typical_cycle_size = cpg_costs_for_cycle + (base_eviscerate_cost - 25) - + + #Set up initial energy budget + haste_multiplier = self.get_haste_multiplier(current_stats) + self.energy_regen = self.get_energy_regen(current_stats) + + self.max_energy = 100. + if self.talents.vigor or self.stats.gear_buffs.soul_of_the_shadowblade: + self.max_energy += 50 + self.energy_budget = self.settings.duration * self.energy_regen + self.max_energy + + #Symbols of Death + sod_casts = 1 + self.settings.duration / self.get_spell_cd('symbols_of_death') + self.energy_budget += 40 * sod_casts + if self.stats.gear_buffs.rogue_t20_4pc: + self.energy_budget += 20 * sod_casts + + #set initial dance budget + self.dance_budget = 2 + self.settings.duration / 60 + if self.talents.enveloping_shadows: + self.dance_budget += 1 + deepening_shadows_cdr_per_cp = 2.5 if self.talents.enveloping_shadows else 1.5 + + shadow_blades_duration = 15. + (3.3333 * self.traits.soul_shadows) + self.shadow_blades_uptime = shadow_blades_duration / self.get_spell_cd('shadow_blades') + #swing timer white_swing_downtime = 0 + self.swing_reset_spacing = self.get_spell_cd('vanish') if self.swing_reset_spacing is not None: - white_swing_downtime += .5 / self.swing_reset_spacing - attacks_per_second['mh_autoattacks'] = attack_speed_multiplier / self.stats.mh.speed * (1 - white_swing_downtime) - attacks_per_second['oh_autoattacks'] = attack_speed_multiplier / self.stats.oh.speed * (1 - white_swing_downtime) + white_swing_downtime += self.settings.response_time / self.swing_reset_spacing + attacks_per_second['mh_autoattacks'] = haste_multiplier / self.stats.mh.speed * (1 - white_swing_downtime) + attacks_per_second['oh_autoattacks'] = haste_multiplier / self.stats.oh.speed * (1 - white_swing_downtime) + + #Set up initial combo point budget + self.cp_budget = 0 + if self.talents.marked_for_death: + mfd_base_count = 1 + self.settings.duration / self.get_spell_cd('marked_for_death') + mfd_cps = (6 if self.talents.deeper_stratagem else 5) * (mfd_base_count + self.settings.marked_for_death_resets) + self.cp_budget += mfd_cps + + #Very VERY simple implementation for The First of the Dead legendary (this should be handled better) + if self.stats.gear_buffs.the_first_of_the_dead: + self.cp_budget += (6 if self.talents.anticipation else 3) * sod_casts + + #setup timelines + nightblade_duration = 6 + (2 * self.settings.finisher_threshold) + if self.stats.gear_buffs.rogue_t19_2pc: + nightblade_duration = 6 + (4 * self.settings.finisher_threshold) + + #Add attacks that could occur during first pass to aps + attacks_per_second[self.dance_cp_builder] = 0 + attacks_per_second['shadow_dance'] = 0 + attacks_per_second['vanish'] = 0 + + nightblade_timeline = list(range(nightblade_duration, self.settings.duration, nightblade_duration)) + for finisher in ['nightblade', 'eviscerate']: + attacks_per_second[finisher] = [0, 0, 0, 0, 0, 0, 0] + nightblade_count = len(nightblade_timeline) + attacks_per_second['nightblade'][self.settings.finisher_threshold] += nightblade_count / self.settings.duration + self.cp_budget -= self.settings.finisher_threshold * nightblade_count + self.energy_budget += (self.relentless_strikes_energy_return_per_cp * self.settings.finisher_threshold - self.get_spell_cost('nightblade')) * nightblade_count + self.dance_budget += (deepening_shadows_cdr_per_cp * self.settings.finisher_threshold * nightblade_count) / self.get_spell_cd('shadow_dance') + + #Add in various cooldown abilities + #This could be made better with timelining but for now simple time average will do + if self.traits.goremaws_bite: + goremaws_bite_cd = self.get_spell_cd('goremaws_bite') + self.settings.response_time + attacks_per_second['goremaws_bite'] = 1 / goremaws_bite_cd + self.cp_budget += (3 + self.shadow_blades_uptime) * (self.settings.duration / goremaws_bite_cd) + self.energy_budget += 30 * (self.settings.duration / goremaws_bite_cd) + if self.traits.feeding_frenzy: + #assume we time it so we can get three free eviscerates + self.energy_budget += self.get_spell_cost('eviscerate') * (self.settings.duration / goremaws_bite_cd) + if self.talents.death_from_above: dfa_cd = self.get_spell_cd('death_from_above') + self.settings.response_time - - lost_swings_mh = self.lost_swings_from_swing_delay(1.3, self.stats.mh.speed / attack_speed_multiplier) - lost_swings_oh = self.lost_swings_from_swing_delay(1.3, self.stats.oh.speed / attack_speed_multiplier) - + if self.talents.dark_shadow: + dfa_cd = self.get_spell_cd('symbols_of_death') + self.settings.response_time + dfa_count = self.settings.duration / dfa_cd + + lost_swings_mh = self.lost_swings_from_swing_delay(1.475, self.stats.mh.speed / haste_multiplier) + lost_swings_oh = self.lost_swings_from_swing_delay(1.475, self.stats.oh.speed / haste_multiplier) + attacks_per_second['mh_autoattacks'] -= lost_swings_mh / dfa_cd attacks_per_second['oh_autoattacks'] -= lost_swings_oh / dfa_cd - + + attacks_per_second['death_from_above_strike'] = [0, 0, 0, 0, 0, 0, 0] + attacks_per_second['death_from_above_strike'][self.settings.finisher_threshold] += 1 / dfa_cd + attacks_per_second['death_from_above_pulse'] = [0, 0, 0, 0, 0, 0, 0] + attacks_per_second['death_from_above_pulse'][self.settings.finisher_threshold] += 1 / dfa_cd + + self.cp_budget -= self.settings.finisher_threshold * dfa_count + self.energy_budget += (self.relentless_strikes_energy_return_per_cp * self.settings.finisher_threshold - self.get_spell_cost('death_from_above')) * dfa_count + self.dance_budget += (deepening_shadows_cdr_per_cp * self.settings.finisher_threshold * dfa_count) / self.get_spell_cd('shadow_dance') + + #Need to handle shadow techniques now to account for swing timer loss attacks_per_second['mh_autoattack_hits'] = attacks_per_second['mh_autoattacks'] * self.dw_mh_hit_chance attacks_per_second['oh_autoattack_hits'] = attacks_per_second['oh_autoattacks'] * self.dw_oh_hit_chance - - ##start consuming energy - #base energy reductions - marked_for_death_cd = self.get_spell_cd('marked_for_death') + (.5 * typical_cycle_size / energy_regen) + self.settings.response_time - if self.talents.marked_for_death: - energy_regen -= (base_eviscerate_cost - 25) / marked_for_death_cd - attacks_per_second['eviscerate'][5] += 1. / marked_for_death_cd - shadowmeld_ambushes = 0. - if self.race.shadowmeld: - shadowmeld_ambushes = 1. / (self.get_spell_cd('shadowmeld') + self.settings.response_time) - shadowmeld_ambushes *= ((self.settings.duration - fw_duration * 3 - 8) / self.settings.duration) - attacks_per_second['ambush'] += shadowmeld_ambushes - energy_regen -= normal_ambush_cost * shadowmeld_ambushes - base_cp_per_second += shadowmeld_ambushes * 2 - - #base CPs, CPGs, and finishers - if self.settings.cycle.use_hemorrhage != 'always' and self.settings.cycle.use_hemorrhage != 'never': - if self.settings.cycle.use_hemorrhage == 'uptime': - hemo_per_second = 1. / hemo_cd + + # Shadow Techniques have a 50% chance to proc on fourth autohit and are guaranteed on fifth + shadow_techniques_cps_per_proc = 1 + (0.05 * self.traits.fortunes_bite) + shadow_techniques_procs = self.settings.duration * (attacks_per_second['mh_autoattack_hits'] + attacks_per_second['oh_autoattack_hits']) / 4.5 + shadow_techniques_cps = shadow_techniques_procs * shadow_techniques_cps_per_proc + self.cp_budget += shadow_techniques_cps + if self.traits.shadows_whisper: + self.energy_budget += 8 * shadow_techniques_procs + + # Init stealth evis counter + stealth_evis_per_vanish = 0 + stealth_evis_per_dance = 0 + + #vanish handling + vanish_count = self.settings.duration / self.get_spell_cd('vanish') + #Treat subterfuge as a mini-dance + if self.talents.subterfuge or self.talents.nightstalker: + net_energy, net_cps, spent_cps, attack_counts = self.get_dance_resources(finisher='eviscerate', vanish=True) + stealth_evis_per_vanish += sum(attack_counts['eviscerate']) + else: + net_energy, net_cps, spent_cps, attack_counts = self.get_dance_resources(finisher=None, vanish=True) + self.energy_budget += vanish_count * net_energy + self.cp_budget += vanish_count * net_cps + self.dance_budget += (deepening_shadows_cdr_per_cp * spent_cps * vanish_count) / self.get_spell_cd('shadow_dance') + self.rotation_merge(attacks_per_second, attack_counts, vanish_count) + + #Generate one final dance templates + if self.settings.cycle.dance_finishers_allowed: + net_energy, net_cps, spent_cps, attack_counts = self.get_dance_resources(finisher='eviscerate') + stealth_evis_per_dance += sum(attack_counts['eviscerate']) + else: + net_energy, net_cps, spent_cps, attack_counts = self.get_dance_resources(finisher=None) + + #Remember dark shadow buffed abilties per one dance + self.dark_shadow_attacks_per_dance = {} + if self.talents.dark_shadow: + self.dark_shadow_attacks_per_dance = dict(attack_counts) + + #Now lets make sure all our budgets are positive + cp_per_builder = 1 + self.shadow_blades_uptime + if self.cp_builder == 'shuriken_storm': + cp_per_builder += self.settings.num_boss_adds + cp_per_builder = min(self.max_store_cps, cp_per_builder) + energy_per_cp = self.get_spell_cost(self.cp_builder) / cp_per_builder + + extra_evis = 0 + extra_builders = 0 + #Not enough dances, generate some more + cps_per_dance = self.get_spell_cd('shadow_dance') / deepening_shadows_cdr_per_cp + net_evis_cost = self.relentless_strikes_energy_return_per_cp * self.settings.finisher_threshold - self.get_spell_cost('eviscerate') + if self.dance_budget<0: + cps_required = abs(self.dance_budget) * cps_per_dance + extra_evis += cps_required / self.settings.finisher_threshold + self.energy_budget += net_evis_cost + #just subtract the cps because we'll fix those next + self.cp_budget -= cps_required + self.dance_budget = 0 + #If we have too many dances just spend them now + elif self.dance_budget > 0: + #quick convergence loop + loop_counter = 0 + while self.dance_budget > 0.0001: + if loop_counter > 100: + raise ConvergenceErrorException(_('Dance fixup failed to converge.')) + dance_count = abs(self.dance_budget) + self.energy_budget += dance_count * net_energy + self.cp_budget += dance_count * net_cps + self.dance_budget += (deepening_shadows_cdr_per_cp * spent_cps * dance_count / self.get_spell_cd('shadow_dance')) - dance_count + #merge attack counts into attacks_per_second + self.rotation_merge(attacks_per_second, attack_counts, dance_count) + loop_counter += 1 + + #if we don't have enough cps lets build some + if self.cp_budget <0: + #can add since we know cp_budget is negative + self.energy_budget += self.cp_budget * energy_per_cp + extra_builders += abs(self.cp_budget) / cp_per_builder + self.cp_budget = 0 + + if self.cp_builder == 'shuriken_storm': + attacks_per_second['shuriken_storm-no-dance'] = extra_builders / self.settings.duration + else: + attacks_per_second[self.cp_builder] = extra_builders / self.settings.duration + attacks_per_second['eviscerate'][self.settings.finisher_threshold] += extra_evis + + #Calculate Shadow Blades extension so far + if self.stats.gear_buffs.denial_of_the_half_giants: + sb_uptime = self.shadow_blades_uptime + sb_extension_converged = False + sb_extension = 0 + counter = 0 + while not sb_extension_converged: + if counter > 50: + raise ConvergenceErrorException(_('Denial Shadow Blades extension failed to converge.')) + finisher_cps = 0 + for i in range(0, 7): + if self.talents.death_from_above: + finisher_cps += attacks_per_second['death_from_above_strike'][i] * i + finisher_cps += attacks_per_second['eviscerate'][i] * i + finisher_cps += attacks_per_second['nightblade'][i] * i + new_sb_extension = finisher_cps * sb_uptime * 0.2 + sb_extension_converged = (new_sb_extension - sb_extension) < 10 ** -5 + sb_uptime = self.shadow_blades_uptime + new_sb_extension + sb_extension = new_sb_extension + counter += 1 + #Account for extra cps + generators = ['shadowstrike', 'shuriken_storm', 'backstab', 'goremaws_bite', 'gloomblade', 'shuriken_toss'] + denial_extra_cps = sb_extension * sum((attacks_per_second[gen] if gen in attacks_per_second else 0) for gen in generators) + self.cp_budget += denial_extra_cps * self.settings.duration + self.shadow_blades_uptime = sb_uptime + cp_per_builder += sb_extension + + #Hopefully energy budget here isn't negative, if it is we're in trouble + #Now we convert all the energy we have left into mini-cycles + #Each mini-cycle contains enough 1 dance and generators+finishers for one dance + finishers_per_minicycle = cps_per_dance / self.settings.finisher_threshold + + attack_counts_mini_cycle = attack_counts + if 'eviscerate' in attack_counts_mini_cycle: + attack_counts_mini_cycle['eviscerate'][self.settings.finisher_threshold] += finishers_per_minicycle + else: + attack_counts_mini_cycle['eviscerate'][self.settings.finisher_threshold] = finishers_per_minicycle + loop_counter = 0 + + alacrity_stacks = 0 + while self.energy_budget > 0.1: + if loop_counter > 50: + raise ConvergenceErrorException(_('Mini-cycles failed to converge.')) + loop_counter += 1 + cps_to_generate = max(cps_per_dance - self.cp_budget, 0) + builders_per_minicycle = cps_to_generate / cp_per_builder + mini_cycle_energy = net_evis_cost * finishers_per_minicycle - (cps_to_generate * energy_per_cp) + #add in dance energy + mini_cycle_energy += net_energy + if cps_to_generate: + mini_cycle_count = self.energy_budget / abs(mini_cycle_energy) else: - hemo_per_second = 1. / float(self.settings.cycle.use_hemorrhage) - energy_regen -= hemo_per_second * base_hemo_cost - base_cp_per_second += hemo_per_second - attacks_per_second['hemorrhage'] += hemo_per_second - #premed - base_cp_per_second += 2. / self.settings.duration #start of the fight - base_cp_per_second += 2. / shd_cd * (self.settings.duration-25.)/self.settings.duration - base_cp_per_second += 2. / self.get_spell_cd('vanish') * (self.settings.duration-50.)/self.settings.duration - #rupture - attacks_per_second['rupture'] = 1. / rupture_cd - attacks_per_second['rupture_ticks'][5] = rupture_ticks_per_cast / rupture_cd - #attacks_per_second['rupture_ticks_sc'] = [0,0,0,0,0, (1 - sc_scaler) * rupture_ticks_per_cast / rupture_cd] - base_cp_per_second -= 5. / rupture_cd - energy_regen -= (base_rupture_cost - 25) / rupture_cd - #no need to add slice and dice to attacks per second - base_cp_per_second -= 5. / snd_cd - - energy_for_dfa = 0 - if self.talents.death_from_above: - #dfa_gap probably should be handled more accurately especially in the non-anticipation case - dfa_interval = 1./(dfa_cd) - energy_for_dfa = typical_cycle_size + self.dfa_cost - base_eviscerate_cost - - attacks_per_second['death_from_above'] = dfa_interval - attacks_per_second['death_from_above_strike'] = [0, 0, 0, 0, 0, dfa_interval] - attacks_per_second['death_from_above_pulse'] = [0, 0, 0, 0, 0, dfa_interval * (self.settings.num_boss_adds+1)] - attacks_per_second[cpg_name] += dfa_interval * 5 - energy_regen -= energy_for_dfa / dfa_cd - - base_cp_per_second += self.vanish_rate * 2 - #if we've consumed more CP's than we have for base functionality, lets generate some more CPs - if base_cp_per_second < 0: - cpg_per_second = math.fabs(base_cp_per_second) - base_cp_per_second += cpg_per_second - attacks_per_second[cpg_name] += cpg_per_second - if cpg_name == 'backstab': - energy_regen -= base_backstab_energy_cost * cpg_per_second - elif cpg_name == 'hemorrhage': - energy_regen -= base_hemo_cost * cpg_per_second - extra_evisc = base_cp_per_second / 5 - energy_regen -= (base_eviscerate_cost - 25) * extra_evisc - attacks_per_second['eviscerate'][5] += extra_evisc - if energy_regen < 0: - raise InputNotModeledException(_('Catastrophic failure: cycle not sustainable.')) - - #calculate shd ambush cycles - shd_energy = (max_energy - self.get_adv_param('max_pool_reduct', 10, min_bound=0, max_bound=50)) + energy_regen * shd_duration #lasts 8s, assume we pool to ~10 energy below max - shd_cycle_cost = 2 * sd_ambush_cost + (base_eviscerate_cost - 25) - shd_eviscerates = min(shd_energy / shd_cycle_cost, 8./3) #8/3 is the max GCDs - shd_ambushes = shd_eviscerates * 2 - attacks_per_second['ambush'] += (shd_ambushes / shd_cd) * ((self.settings.duration - fw_duration) / self.settings.duration) - attacks_per_second['eviscerate'][5] += (shd_eviscerates / shd_cd) * ((self.settings.duration - fw_duration) / self.settings.duration) - energy_regen -= (shd_cycle_cost * shd_eviscerates) / shd_cd * ((self.settings.duration - fw_duration) / self.settings.duration) - - #calculate percentage of ambushes with FW - ambush_no_fw = shadowmeld_ambushes + 1. / shd_cd - 1. / self.settings.duration - if not self.settings.cycle.clip_fw: - ambush_no_fw += self.total_openers_per_second + 1. / self.settings.duration - additional_info['ambush_no_fw_rate'] = ambush_no_fw / attacks_per_second['ambush'] - #calculate percentage of backstabs with FW - additional_info['backstab_fw_rate'] = (fw_duration - 1) / self.settings.duration #start of fight - additional_info['backstab_fw_rate'] += (fw_duration - 1) / shd_cd * (1. - fw_duration / self.settings.duration) - additional_info['backstab_fw_rate'] += (fw_duration + vanish_bonus_stealth - 1) / self.get_spell_cd('vanish') * ((self.settings.duration - fw_duration * 2 - 8) / self.settings.duration) - if self.race.shadowmeld: - additional_info['backstab_fw_rate'] += (fw_duration - 1) / self.get_spell_cd('shadowmeld') * ((self.settings.duration - fw_duration * 3 - 8) / self.settings.duration) - #accounts for the fact that backstab isn't evenly distributed - additional_info['backstab_fw_rate'] = additional_info['backstab_fw_rate'] / ((shd_cd - 8.) / shd_cd) - #calculate FW uptime overall - additional_info['fw_uptime'] = fw_duration / self.settings.duration #start of fight - additional_info['fw_uptime'] += (fw_duration + 7.5) / shd_cd * ((self.settings.duration - fw_duration) / self.settings.duration) - additional_info['fw_uptime'] += (fw_duration + vanish_bonus_stealth) / self.get_spell_cd('vanish') * ((self.settings.duration - fw_duration * 2 - 8) / self.settings.duration) - if self.race.shadowmeld: - additional_info['fw_uptime'] += fw_duration / self.get_spell_cd('shadowmeld') * ((self.settings.duration - fw_duration * 3 - 8) / self.settings.duration) - #allocate the remaining energy - filler_cycles_per_second = energy_regen / typical_cycle_size - attacks_per_second[cpg_name] += filler_cycles_per_second * 5 - attacks_per_second['eviscerate'][5] += filler_cycles_per_second - if self.stats.gear_buffs.rogue_t17_4pc: - attacks_per_second['eviscerate'][5] += 1. / shd_cd - - #Hemo ticks - if 'hemorrhage' in attacks_per_second and self.settings.cycle.use_hemorrhage != 'never': - if self.settings.cycle.use_hemorrhage == 'always': - hemo_gap = 1 / attacks_per_second['hemorrhage'] + mini_cycle_count = 1 + + mini_cycle_count = min(mini_cycle_count, 1) + #print loop_counter, mini_cycle_count + #mini_cycle_count = 1 + #build the minicycle attack_counts + if self.cp_builder == 'shuriken_storm': + attack_counts_mini_cycle['shuriken_storm-no-dance'] = builders_per_minicycle else: - hemo_gap = hemo_cd - ticks_per_second = min(1. / (3 * sc_scaler), 8. / hemo_gap) - attacks_per_second['hemorrhage_ticks'] = ticks_per_second - - sc_ms_chance = min(2 * (self.stats.get_multistrike_chance_from_rating(rating=current_stats['multistrike']) + self.buffs.multistrike_bonus()), 2) - #this is a cache for convergence - self.sc_trigger_rate = attacks_per_second['ambush'] * sc_ms_chance - if 'backstab' in attacks_per_second: - self.sc_trigger_rate += attacks_per_second['backstab'] * sc_ms_chance - self.sc_trigger_rate = min(self.sc_trigger_rate, 2) - - if self.talents.shadow_reflection: - sr_cd = self.get_spell_cd('shadow_reflection') - attacks_per_second['sr_eviscerate'] = [0,0,0,0,0, shd_eviscerates / sr_cd] - attacks_per_second['sr_rupture_ticks'] = [0,0,0,0,0, 12. / sr_cd] - attacks_per_second['sr_ambush'] = shd_ambushes / sr_cd - - self.get_poison_counts(attacks_per_second, current_stats) - + attack_counts_mini_cycle[self.cp_builder] = builders_per_minicycle + self.rotation_merge(attacks_per_second, attack_counts_mini_cycle, mini_cycle_count) + self.energy_budget += mini_cycle_energy * mini_cycle_count + self.cp_budget += (net_cps - cps_per_dance + cps_to_generate) * mini_cycle_count + #Update energy budget with alacrity and haste procs + if self.talents.alacrity: + old_alacrity_regen = self.energy_regen * (1 + (alacrity_stacks *0.02)) + new_alacrity_stacks = self.get_average_alacrity(attacks_per_second) + new_alacrity_regen = self.energy_regen * (1 + (new_alacrity_stacks *0.02)) + self.energy_budget += (new_alacrity_regen - old_alacrity_regen) * self.settings.duration + alacrity_stacks = new_alacrity_stacks + #Update Shadow Blades extension from Denial + if self.stats.gear_buffs.denial_of_the_half_giants: + new_sb_extension = mini_cycle_count * sum(attack_counts_mini_cycle['eviscerate']) * self.settings.finisher_threshold * self.shadow_blades_uptime * 0.2 / self.settings.duration + generators = ['shadowstrike', 'shuriken_storm', 'backstab', 'goremaws_bite', 'gloomblade', 'shuriken_toss'] + denial_extra_cps = new_sb_extension * sum((attacks_per_second[gen] if gen in attacks_per_second else 0) for gen in generators) + self.shadow_blades_uptime += new_sb_extension + self.cp_budget += denial_extra_cps * self.settings.duration + cp_per_builder += new_sb_extension + + #Now fixup attacks_per_second + #convert nightblade casts into nightblade ticks + if 'nightblade' in attacks_per_second: + attacks_per_second['nightblade_ticks'] = [0, 0, 0, 0, 0, 0, 0] + for cp in range(7): + attacks_per_second['nightblade_ticks'][cp] = (3 + cp) * attacks_per_second['nightblade'][cp] + if self.stats.gear_buffs.rogue_t19_2pc: + attacks_per_second['nightblade_ticks'][cp] = (3 + (2 * cp)) * attacks_per_second['nightblade'][cp] + del attacks_per_second['nightblade'] + + #convert some white swings into shadowblades + #since weapon speeds are now fixed just handle a single shadowblades + attacks_per_second['shadow_blades'] = self.shadow_blades_uptime * attacks_per_second['mh_autoattacks'] + attacks_per_second['mh_autoattacks'] -= attacks_per_second['shadow_blades'] + attacks_per_second['oh_autoattacks'] -= attacks_per_second['shadow_blades'] + + if self.traits.akarris_soul and 'shadowstrike' in attacks_per_second: + attacks_per_second['soul_rip'] = attacks_per_second['shadowstrike'] + if self.traits.shadow_nova: + attacks_per_second['shadow_nova'] = min(attacks_per_second['shadow_dance'], 1 / 5) + + #FIXME: Kinda hackish, better approach would be to compute a seperate dance rotation + if self.stats.gear_buffs.the_dreadlords_deceit and (self.cp_builder =='backstab' or self.cp_builder == 'gloomblade'): + shuriken_interval = 1 / 60 + attacks_per_second['shadowstrike'] -= shuriken_interval + attacks_per_second['shuriken_storm'] = shuriken_interval + self.stealth_shuriken_uptime = 1. + + self.stealth_shuriken_uptime = 0. + if self.cp_builder == 'shuriken_storm': + self.stealth_shuriken_uptime = attacks_per_second['shuriken_storm'] / (attacks_per_second['shuriken_storm'] + attacks_per_second['shuriken_storm-no-dance']) + attacks_per_second['shuriken_storm'] = attacks_per_second['shuriken_storm'] + attacks_per_second['shuriken_storm-no-dance'] + del attacks_per_second['shuriken_storm-no-dance'] + + self.stealthed_uptime = 4 * attacks_per_second['shadow_dance'] + if self.talents.subterfuge: + self.stealthed_uptime += 1 * attacks_per_second['shadow_dance'] + 3 * attacks_per_second['vanish'] + + #Full additive assumption for now + if self.talents.master_of_subtlety: + self.mos_uptime = self.stealthed_uptime + 5 * attacks_per_second['shadow_dance'] + 5 * attacks_per_second['vanish'] + + for ability in list(attacks_per_second.keys()): + if not attacks_per_second[ability]: + del attacks_per_second[ability] + elif isinstance(attacks_per_second[ability], list) and not any(attacks_per_second[ability]): + del attacks_per_second[ability] + + #determine how many evis used during stealth + if self.settings.cycle.dance_finishers_allowed: + stealth_evis = stealth_evis_per_dance * attacks_per_second['shadow_dance'] + if self.talents.subterfuge: + stealth_evis += stealth_evis_per_vanish * attacks_per_second['vanish'] + else: + stealth_evis = 0 + self.stealth_evis_uptime = stealth_evis / sum(attacks_per_second['eviscerate']) + + if self.traits.second_shuriken and 'shuriken_toss' in attacks_per_second: + attacks_per_second['second_shuriken'] = 0.1 * attacks_per_second['shuriken_toss'] + + if self.talents.weaponmaster: + for ability in attacks_per_second: + if isinstance(attacks_per_second[ability], list): + for cp in range(7): + attacks_per_second[ability][cp] *= 1.06 + else: + attacks_per_second[ability] *= 1.06 + + #for a in attacks_per_second: + # if isinstance(attacks_per_second[a], list): + # print a, 1./sum(attacks_per_second[a]) + # else: + # print a, 1./attacks_per_second[a] return attacks_per_second, crit_rates, additional_info + + #Computes the net energy and combo points from a shadow dance rotation + #Returns net_energy, net_cps, spent_cps, dict of attack counts + def get_dance_resources(self, finisher=None, vanish=False): + net_energy = 0 + net_cps = 0 + spent_cps = 0 + + attack_counts = {} + + if self.talents.master_of_shadows: + net_energy += 25 + + cost_mod = 1.0 + if self.talents.shadow_focus: + cost_mod = 0.75 + + dance_gcds = 1 + if vanish and self.talents.subterfuge: + dance_gcds += 3 + elif not vanish: + dance_gcds = 4 + if self.talents.subterfuge: + dance_gcds += 1 + + max_dance_energy = dance_gcds * self.energy_regen + self.max_energy + + if vanish: + attack_counts['vanish'] = 1 + else: + attack_counts['shadow_dance'] = 1 + + cp_builder_cost = self.get_spell_cost(self.dance_cp_builder, cost_mod=cost_mod) + attack_counts[self.dance_cp_builder] = 0 + if finisher: + finisher_cost = self.get_spell_cost(finisher, cost_mod=cost_mod) + attack_counts[finisher] = [0, 0, 0, 0, 0, 0, 0] + + while dance_gcds > 0: + remaining_energy = (net_energy + max_dance_energy) + if finisher and net_cps >= self.settings.finisher_threshold and remaining_energy >= finisher_cost: + use_cps = min(int(net_cps), self.max_spend_cps) + net_energy += self.relentless_strikes_energy_return_per_cp * use_cps - finisher_cost + attack_counts[finisher][use_cps] += 1 + spent_cps += use_cps + net_cps -= use_cps + elif remaining_energy >= cp_builder_cost: + attack_counts[self.dance_cp_builder] += 1 + net_energy -= cp_builder_cost + if self.dance_cp_builder == 'shadowstrike': + net_cps += 2 + self.shadow_blades_uptime + if self.stats.gear_buffs.rogue_t19_4pc: + net_cps += 0.3 + elif self.dance_cp_builder == 'shuriken_storm': + net_cps += min(1 + self.settings.num_boss_adds + self.shadow_blades_uptime, self.max_store_cps) + net_cps = min(net_cps, self.max_store_cps) + dance_gcds -= 1 + + return net_energy, net_cps, spent_cps, attack_counts + + #Performs fuzzy matching, with specified delta on two lists. + #Returns 3 lists, match, and a and b with matches removed + #Only works for negative deltas for now. + def timeline_overlap(self, timeline_a, timeline_b, match_delta): + match_list = [] + #index of matches for removal + no_match_a = [] + for a in range(len(timeline_a)): + match = False + for b in range(len(timeline_b)): + #early termination for impossible matches + if timeline_b[b] > timeline_a[a]: + break + if timeline_b[b] > timeline_a[a] + match_delta and timeline_b[b] < timeline_a[a]: + match_list.append(timeline_b[b]) + match = True + if not match: + no_match_a.append(timeline_a[a]) + + return match_list, no_match_a, [x for x in timeline_b if x not in match_list] + + #Takes in the full attacks per second dict and a raw attack counts dict + #adds attack countes into the rotation at global scope + def rotation_merge (self, attacks_per_second, attack_counts, count): + rotations_per_second = count / self.settings.duration + for ability in attack_counts: + if ability in self.finisher_damage_sources: + for cp in range(7): + attacks_per_second[ability][cp] += rotations_per_second * attack_counts[ability][cp] + else: + attacks_per_second[ability] += rotations_per_second * attack_counts[ability] diff --git a/shadowcraft/calcs/rogue/Aldriana/settings.py b/shadowcraft/calcs/rogue/Aldriana/settings.py index 0b08a6b..f9eea80 100755 --- a/shadowcraft/calcs/rogue/Aldriana/settings.py +++ b/shadowcraft/calcs/rogue/Aldriana/settings.py @@ -1,52 +1,30 @@ +from builtins import object from shadowcraft.core import exceptions +from shadowcraft.calcs.rogue.Aldriana import settings_data class Settings(object): # Settings object for AldrianasRogueDamageCalculator. - def __init__(self, cycle, time_in_execute_range=.35, response_time=.5, latency=.03, dmg_poison='dp', utl_poison=None, - duration=300, use_opener='always', opener_name='default', is_pvp=False, shiv_interval=0, adv_params=None, - merge_damage=True, num_boss_adds=0, feint_interval=0, default_ep_stat='ap', is_day=False): + def __init__(self, cycle, **kwargs): self.cycle = cycle - self.time_in_execute_range = time_in_execute_range - self.response_time = response_time - self.latency = latency - self.dmg_poison = dmg_poison - self.utl_poison = utl_poison - self.duration = duration - self.use_opener = use_opener # Allowed values are 'always' (vanish/shadowmeld on cooldown), 'opener' (once per fight) and 'never' - self.opener_name = opener_name - self.is_pvp = is_pvp - self.feint_interval = feint_interval - self.merge_damage = merge_damage - self.is_day = is_day - self.num_boss_adds = max(num_boss_adds, 0) - self.shiv_interval = float(shiv_interval) - self.adv_params = self.interpret_adv_params(adv_params) - self.default_ep_stat = default_ep_stat - if self.shiv_interval < 10 and not self.shiv_interval == 0: - self.shiv_interval = 10 - allowed_openers_per_spec = { - 'assassination': ('mutilate', 'dispatch', 'envenom'), - 'combat': ('sinister_strike', 'revealing_strike', 'eviscerate'), - 'subtlety': () - } - allowed_openers = allowed_openers_per_spec[self.cycle._cycle_type] + ('ambush', 'garrote', 'default', 'cpg') - if opener_name not in allowed_openers: - raise exceptions.InvalidInputException(_('Opener {opener} is not allowed in {cycle} cycles.').format(opener=opener_name, cycle=self.cycle._cycle_type)) - if opener_name == 'default': - default_openers = { - 'assassination': 'mutilate', - 'combat': 'ambush', - 'subtlety': 'ambush'} - self.opener_name = default_openers[self.cycle._cycle_type] - if dmg_poison not in (None, 'dp', 'wp'): - raise exceptions.InvalidInputException(_('You can only choose Deadly(dp) or Wound(wp) as a damage poison')) - if utl_poison not in (None, 'cp', 'mnp', 'lp', 'pp'): - raise exceptions.InvalidInputException(_('You can only choose Crippling(cp), Mind-Numbing(mnp), Leeching(lp) or Paralytic(pp) as a non-lethal poison')) - - def get_spec(self): - return self.cycle._cycle_type - + self.default_ep_stat = kwargs.get('default_ep_stat', 'agi') + self.feint_interval = int(kwargs.get('feint_interval', 0)) + + #Get defaults from settings_data + defaults = settings_data.get_default_settings(settings_data.rogue_settings) + settings_data.process_overrides(defaults, kwargs, self.cycle._cycle_type) + + self.response_time = float(kwargs.get('response_time', defaults['response_time'])) + self.latency = float(kwargs.get('latency', defaults['latency'])) + self.duration = int(kwargs.get('duration', defaults['duration'])) + self.is_day = kwargs.get('is_day', defaults['is_day']) + self.is_demon = kwargs.get('is_demon', defaults['is_demon']) + self.num_boss_adds = max(int(kwargs.get('num_boss_adds', defaults['num_boss_adds'])), 0) + self.adv_params = self.interpret_adv_params(kwargs.get('adv_params', defaults['adv_params'])) + self.marked_for_death_resets = int(kwargs.get('marked_for_death_resets', defaults['marked_for_death_resets'])) + self.finisher_threshold = int(kwargs.get('finisher_threshold', defaults['finisher_threshold'])) + self.pantheon_trinket_users = int(kwargs.get('pantheon_trinket_users', defaults['pantheon_trinket_users'])) + def interpret_adv_params(self, s=""): data = {} max_effects = 8 @@ -63,12 +41,12 @@ def interpret_adv_params(self, s=""): except: raise exceptions.InvalidInputException(_('Advanced Parameter ' + e + ' found corrupt. Properly structure params and try again.')) return data - + def is_assassination_rogue(self): return self.cycle._cycle_type == 'assassination' - def is_combat_rogue(self): - return self.cycle._cycle_type == 'combat' + def is_outlaw_rogue(self): + return self.cycle._cycle_type == 'outlaw' def is_subtlety_rogue(self): return self.cycle._cycle_type == 'subtlety' @@ -79,7 +57,7 @@ class Cycle(object): # respect. # When subclassing, define _cycle_type to be one of 'assassination', - # 'combat', or 'subtlety' - this is how the damage calculator makes sure + # 'outlaw', or 'subtlety' - this is how the damage calculator makes sure # you have an appropriate cycle object to go with your talent trees, etc. _cycle_type = '' @@ -87,31 +65,96 @@ class Cycle(object): class AssassinationCycle(Cycle): _cycle_type = 'assassination' - allowed_values = (1, 2, 3, 4, 5) - - def __init__(self, min_envenom_size_non_execute=4, min_envenom_size_execute=5): - assert min_envenom_size_non_execute in self.allowed_values - self.min_envenom_size_non_execute = min_envenom_size_non_execute - - assert min_envenom_size_execute in self.allowed_values - self.min_envenom_size_execute = min_envenom_size_execute - -class CombatCycle(Cycle): - _cycle_type = 'combat' - - def __init__(self, ksp_immediately=True, revealing_strike_pooling=True, blade_flurry=False, dfa_during_ar=False): - self.blade_flurry = bool(blade_flurry) - self.ksp_immediately = bool(ksp_immediately) # Determines whether to KSp the instant it comes off cool or wait until Bandit's Guile stacks up. - self.revealing_strike_pooling = bool(revealing_strike_pooling) - self.dfa_during_ar = bool(dfa_during_ar) + def __init__(self, **kwargs): + defaults = settings_data.get_default_settings(settings_data.rogue_settings) + settings_data.process_overrides(defaults, kwargs, self._cycle_type) + + self.cp_builder = kwargs.get('cp_builder', defaults['cp_builder']) + self.kingsbane_with_vendetta = kwargs.get('kingsbane', defaults['kingsbane']) + self.exsang_with_vendetta = kwargs.get('exsang', defaults['exsang']) + self.lethal_poison = kwargs.get('lethal_poison', defaults['lethal_poison']) + +class OutlawCycle(Cycle): + _cycle_type = 'outlaw' + #Generated by: + #from itertools import combinations + #single = ['jr', 'gm', 's', 'tb', 'bt', 'b'] + #list(combinations(single, 6)) + list(combinations(single, 3)) + list(combinations(single, 2)) + list(combinations(single, 1)) + rtb_combos = [('jr', 'gm', 's', 'tb', 'bt', 'b'), + #3 buffs + ('jr', 'gm', 's'), ('jr', 'gm', 'tb'), + ('jr', 'gm', 'bt'), ('jr', 'gm', 'b'), + ('jr', 's', 'tb'), ('jr', 's', 'bt'), + ('jr', 's', 'b'), ('jr', 'tb', 'bt'), + ('jr', 'tb', 'b'), ('jr', 'bt', 'b'), + ('gm', 's', 'tb'), ('gm', 's', 'bt'), + ('gm', 's', 'b'), ('gm', 'tb', 'bt'), + ('gm', 'tb', 'b'), ('gm', 'bt', 'b'), + ('s', 'tb', 'bt'), ('s', 'tb', 'b'), + ('s', 'bt', 'b'), ('tb', 'bt', 'b'), + #2 buffs + ('jr', 'gm'), ('jr', 's'), ('jr', 'tb'), + ('jr', 'bt'), ('jr', 'b'), ('gm', 's'), + ('gm', 'tb'), ('gm', 'bt'), ('gm', 'b'), + ('s', 'tb'), ('s', 'bt'), ('s', 'b'), + ('tb', 'bt'), ('tb', 'b'), ('bt', 'b'), + #single buffs + ('jr',), ('gm',), ('s',), ('tb',), ('bt',), ('b',)] + + def __init__(self, **kwargs): + defaults = settings_data.get_default_settings(settings_data.rogue_settings) + settings_data.process_overrides(defaults, kwargs, self._cycle_type) + + self.blade_flurry = kwargs.get('blade_flurry', defaults['blade_flurry']) + self.between_the_eyes_policy = kwargs.get('between_the_eyes_policy', defaults['between_the_eyes_policy']) + self.jolly_roger_reroll = int(kwargs.get('jolly_roger_reroll', defaults['jolly_roger_reroll'])) + self.grand_melee_reroll = int(kwargs.get('grand_melee_reroll', defaults['grand_melee_reroll'])) + self.shark_reroll = int(kwargs.get('shark_reroll', defaults['shark_reroll'])) + self.true_bearing_reroll = int(kwargs.get('true_bearing_reroll', defaults['true_bearing_reroll'])) + self.buried_treasure_reroll = int(kwargs.get('buried_treasure_reroll', defaults['buried_treasure_reroll'])) + self.broadsides_reroll = int(kwargs.get('broadsides_reroll', defaults['broadsides_reroll'])) + + # RtB reroll thresholds, 0, 1, 2, 3 + # 0 means never reroll combos with this buff + # 1 means reroll singles of buff + # 2 means reroll doubles containing this buff + # 3 means reroll triples containing this buff + #build reroll lists here + self.reroll_list = [] + self.keep_list = [] + for combo in self.rtb_combos: + buffs = len(combo) + if 'jr' in combo and buffs <= self.jolly_roger_reroll: + self.reroll_list.append(combo) + continue + if 'gm' in combo and buffs <= self.grand_melee_reroll: + self.reroll_list.append(combo) + continue + if 's' in combo and buffs <= self.shark_reroll: + self.reroll_list.append(combo) + continue + if 'tb' in combo and buffs <= self.true_bearing_reroll: + self.reroll_list.append(combo) + continue + if 'bt' in combo and buffs <= self.buried_treasure_reroll: + self.reroll_list.append(combo) + continue + if 'b' in combo and buffs <= self.broadsides_reroll: + self.reroll_list.append(combo) + continue + self.keep_list.append(combo) class SubtletyCycle(Cycle): _cycle_type = 'subtlety' - def __init__(self, raid_crits_per_second, use_hemorrhage='uptime', clip_fw=False): - self.clip_fw = clip_fw #reduces fw uptime, but increases ambush damage - self.raid_crits_per_second = raid_crits_per_second #used to calculate HAT procs per second. - self.use_hemorrhage = use_hemorrhage # Allowed values are 'always' (main CP generator), - #'never' (default to backstab), - #'uptime' (cast when hemo is down), - + def __init__(self, **kwargs): + defaults = settings_data.get_default_settings(settings_data.rogue_settings) + settings_data.process_overrides(defaults, kwargs, self._cycle_type) + + self.cp_builder = kwargs.get('cp_builder', defaults['cp_builder']) + self.dance_finishers_allowed = kwargs.get('dance_finishers_allowed', defaults['dance_finishers_allowed']) + self.compute_cp_waste = kwargs.get('compute_cp_waste', defaults['compute_cp_waste']) + + #Handle percent vs float automatically + self.positional_uptime = int(kwargs.get('positional_uptime_percent', defaults['positional_uptime_percent'])) / 100 + self.positional_uptime = float(kwargs.get('positional_uptime', self.positional_uptime)) diff --git a/shadowcraft/calcs/rogue/Aldriana/settings_data.py b/shadowcraft/calcs/rogue/Aldriana/settings_data.py new file mode 100644 index 0000000..8913ba9 --- /dev/null +++ b/shadowcraft/calcs/rogue/Aldriana/settings_data.py @@ -0,0 +1,450 @@ +#This file contains all rogue settings that are advertised to the react UI and later passed +#into the settings object before calculation. +#Variable names are supposed to be unique and currently passed into the settings and cycle +#constructors all together. +#Options ending in "_spec" will set shared settings with the spec stripped. + +def get_default_settings(settings_data): + defaults = {} + for category in settings_data: + for option in category['items']: + defaults[option['name']] = option['default'] + return defaults + +def process_overrides(defaults_dict, params_dict, spec): + suffix = '_' + spec + #Spec overrides from defaults + for setting in list(defaults_dict.keys()): + if setting.endswith(suffix): + override_key = setting.replace(suffix, '') + defaults_dict[override_key] = defaults_dict[setting] + #Spec overrides from params + for setting in params_dict: + if setting.endswith(suffix): + override_key = setting.replace(suffix, '') + defaults_dict[override_key] = params_dict[setting] + +rogue_settings = [ + { + 'spec': 'a', + 'heading': 'Assassination Rotation Settings', + 'name': 'rotation.assassination', + 'items': [ + { + 'name': 'kingsbane', + 'label': 'Kingsbane w/ Vendetta', + 'description': '', + 'type': 'dropdown', + 'default': 'just', + 'options': { + 'just': "Use cooldown if it aligns, but don't delay usage", + 'only': 'Only use cooldown with Vendetta' + } + }, + { + 'name': 'exsang', + 'label': 'Exsang w/ Vendetta', + 'description': '', + 'type': 'dropdown', + 'default': 'just', + 'options': { + 'just': "Use cooldown if it aligns, but don't delay usage", + 'only': 'Only use cooldown with Vendetta' + } + }, + { + 'name': 'cp_builder_assassination', + 'label': 'CP Builder', + 'description': '', + 'type': 'dropdown', + 'default': 'mutilate', + 'options': { + 'mutilate': 'Mutilate', + 'fan_of_knives': 'Fan of Knives' + } + }, + { + 'name': 'lethal_poison', + 'label': 'Lethal Poison', + 'description': '', + 'type': 'dropdown', + 'default': 'dp', + 'options': { + 'dp': 'Deadly Poison', + 'wp': 'Wound Poison' + } + }, + { + 'name': 'finisher_threshold_assassination', + 'label': 'Finisher Threshold', + 'description': 'Minimum CPs to use finisher', + 'type': 'dropdown', + 'default': '4', + 'options': { + '4': '4', + '5': '5', + '6': '6' + } + }, + ] + }, + { + 'spec': 'Z', + 'heading': 'Outlaw Rotation Settings', + 'name': 'rotation.outlaw', + 'items': [ + { + 'name': 'blade_flurry', + 'label': 'Blade Flurry', + 'description': 'Use Blade Flurry', + 'type': 'checkbox', + 'default': False + }, + { + 'name': 'between_the_eyes_policy', + 'label': 'BtE Policy', + 'description': '', + 'type': 'dropdown', + 'default': 'never', + 'options': { + 'shark': 'Only use with Shark', + 'always': 'Use BtE on cooldown', + 'never': 'Never use BtE', + } + }, + { + 'name': 'reroll_policy', + 'label': 'RtB Reroll Policy', + 'description': '', + 'type': 'dropdown', + 'default': 'custom', + 'options': { + '1': 'Reroll single buffs', + '2': 'Reroll two or fewer buffs', + '3': 'Reroll three or fewer buffs', + 'custom': 'Custom setup per buff (see below)', + } + }, + { + 'name': 'jolly_roger_reroll', + 'label': 'Jolly Roger', + 'description': '', + 'type': 'dropdown', + 'default': '2', + 'options': { + '0': '0 - Never reroll combos with this buff', + '1': '1 - Reroll single buff rolls of this buff', + '2': '2 - Reroll double-buff rolls containing this buff', + '3': '3 - Reroll triple-buff rolls containing this buff' + } + }, + { + 'name': 'grand_melee_reroll', + 'label': 'Grand Melee', + 'description': '', + 'type': 'dropdown', + 'default': '2', + 'options': { + '0': '0 - Never reroll combos with this buff', + '1': '1 - Reroll single buff rolls of this buff', + '2': '2 - Reroll double-buff rolls containing this buff', + '3': '3 - Reroll triple-buff rolls containing this buff' + } + }, + { + 'name': 'shark_reroll', + 'label': 'Shark-Infested Waters', + 'description': '', + 'type': 'dropdown', + 'default': '2', + 'options': { + '0': '0 - Never reroll combos with this buff', + '1': '1 - Reroll single buff rolls of this buff', + '2': '2 - Reroll double-buff rolls containing this buff', + '3': '3 - Reroll triple-buff rolls containing this buff' + } + }, + { + 'name': 'true_bearing_reroll', + 'label': 'True Bearing', + 'description': '', + 'type': 'dropdown', + 'default': '0', + 'options': { + '0': '0 - Never reroll combos with this buff', + '1': '1 - Reroll single buff rolls of this buff', + '2': '2 - Reroll double-buff rolls containing this buff', + '3': '3 - Reroll triple-buff rolls containing this buff' + } + }, + { + 'name': 'buried_treasure_reroll', + 'label': 'Buried Treasure', + 'description': '', + 'type': 'dropdown', + 'default': '2', + 'options': { + '0': '0 - Never reroll combos with this buff', + '1': '1 - Reroll single buff rolls of this buff', + '2': '2 - Reroll double-buff rolls containing this buff', + '3': '3 - Reroll triple-buff rolls containing this buff' + } + }, + { + 'name': 'broadsides_reroll', + 'label': 'Broadsides', + 'description': '', + 'type': 'dropdown', + 'default': '2', + 'options': { + '0': '0 - Never reroll combos with this buff', + '1': '1 - Reroll single buff rolls of this buff', + '2': '2 - Reroll double-buff rolls containing this buff', + '3': '3 - Reroll triple-buff rolls containing this buff' + } + }, + { + 'name': 'finisher_threshold_outlaw', + 'label': 'Finisher Threshold', + 'description': 'Minimum CPs to use finisher', + 'type': 'dropdown', + 'default': '5', + 'options': { + '4': '4', + '5': '5', + '6': '6' + } + }, + ] + }, + { + 'spec': 'b', + 'heading': 'Subtlety Rotation Settings', + 'name': 'rotation.subtlety', + 'items': [ + { + 'name': 'cp_builder_subtlety', + 'label': 'CP Builder', + 'description': '', + 'type': 'dropdown', + 'default': 'backstab', + 'options': { + 'backstab': 'Backstab', + 'shuriken_storm': 'Shuriken Storm', + } + }, + { + 'name': 'dance_finishers_allowed', + 'label': 'Use Finishers during Dance', + 'description': '', + 'type': 'checkbox', + 'default': True + }, + { + 'name': 'positional_uptime_percent', + 'label': 'Backstab uptime', + 'description': 'Percentage of the fight you are behind the target (0-100). This has no effect if Gloomblade is selected as a talent.', + 'type': 'text', + 'default': '100' + }, + { + 'name': 'compute_cp_waste', + 'label': 'Compute CP Waste', + 'description': 'EXPERIMENTAL FEATURE: Compute combo point waste', + 'type': 'checkbox', + 'default': False + }, + { + 'name': 'finisher_threshold_subtlety', + 'label': 'Finisher Threshold', + 'description': 'Minimum CPs to use finisher', + 'type': 'dropdown', + 'default': '5', + 'options': { + '4': '4', + '5': '5', + '6': '6' + } + }, + ] + }, + { + 'spec': 'All', + 'heading': 'Raid Buffs', + 'name': 'buffs', + 'items': [ + { + 'name': 'flask_legion_agi', + 'label': 'Legion Agility Flask', + 'description': 'Flask of the Seventh Demon (1300 Agility)', + 'type': 'checkbox', + 'default': False + }, + { + 'name': 'short_term_haste_buff', + 'label': '+30% Haste/40 sec', + 'description': 'Heroism/Bloodlust/Time Warp', + 'type': 'checkbox', + 'default': False + }, + { + 'name': 'food_buff', + 'label': 'Food', + 'description': '', + 'type': 'dropdown', + 'default': 'food_legion_feast_500', + 'options': { + 'food_legion_crit_375': 'The Hungry Magister (375 Crit)', + 'food_legion_haste_375': 'Azshari Salad (375 Haste)', + 'food_legion_mastery_375': 'Nightborne Delicacy Platter (375 Mastery)', + 'food_legion_versatility_375': 'Seed-Battered Fish Plate (375 Versatility)', + 'food_legion_feast_500': 'Lavish Suramar Feast (500 Agility)', + 'food_legion_damage_3': 'Fishbrul Special (High Fire Proc)', + } + }, + { + 'name': 'prepot', + 'label': 'Pre-pot', + 'description': '', + 'type': 'dropdown', + 'default': 'prolonged_power_pot', + 'options': { + 'old_war_pot': 'Potion of the Old War', + 'prolonged_power_pot': 'Potion of Prolonged Power', + 'potion_none': 'None', + } + }, + { + 'name': 'potion', + 'label': 'Combat Potion', + 'description': '', + 'type': 'dropdown', + 'default': 'prolonged_power_prepot', + 'options': { + 'old_war_prepot': 'Potion of the Old War', + 'prolonged_power_prepot': 'Potion of Prolonged Power', + 'potion_none': 'None', + } + } + ] + }, + { + 'spec': 'All', + 'heading': 'General Settings', + 'name': 'general.settings', + 'items': [ + { + 'name': 'is_demon', + 'label': 'Enemy is Demon', + 'description': 'Enables damage buff from heirloom trinket against demons', + 'type': 'checkbox', + 'default': False + }, + { + 'name': 'patch', + 'label': 'Patch/Engine', + 'description': '', + 'type': 'dropdown', + 'default': '7.0', + 'options': { + '7.0': '7.0', + 'fierys_strange_voodoo': 'fierys strange voodoo', + } + }, + { + 'name': 'race', + 'label': 'Race', + 'description': '', + 'type': 'dropdown', + 'default': 'human', + 'options': { + 'human': 'Human', + 'dwarf': 'Dwarf', + 'orc': 'Orc', + 'blood_elf': 'Blood Elf', + 'gnome': 'Gnome', + 'worgen': 'Worgen', + 'troll': 'Troll', + 'night_elf': 'Night Elf', + 'undead': 'Undead', + 'goblin': 'Goblin', + 'pandren': 'Pandaren', + } + }, + { + 'name': 'is_day', + 'label': 'Night Elf Racial', + 'description': '', + 'type': 'dropdown', + 'default': False, + 'options': { + False: 'Night', + True: 'Day', + } + }, + { + 'name': 'level', + 'label': 'Level', + 'description': '', + 'type': 'text', + 'default': '110' + }, + { + 'name': 'duration', + 'label': 'Fight Duration', + 'description': '', + 'type': 'text', + 'default': '300' + }, + { + 'name': 'response_time', + 'label': 'Response Time', + 'description': '', + 'type': 'text', + 'default': '0.5', + }, + { + 'name': 'num_boss_adds', + 'label': 'Number of Boss Adds', + 'description': '', + 'type': 'text', + 'default': '0', + }, + { + 'name': 'marked_for_death_resets', + 'label': 'Total number of additional MfD Resets', + 'description': '', + 'type': 'text', + 'default': '0', + }, + { + 'name': 'pantheon_trinket_users', + 'label': 'Number of players with Pantheon trinkets', + 'description': '', + 'type': 'text', + 'default': '0', + } + ] + }, + { + 'spec': 'All', + 'heading': 'Other', + 'name': 'other', + 'items': [ + { + 'name': 'latency', + 'label': 'Latency', + 'description': '', + 'type': 'text', + 'default': '0.03' + }, + { + 'name': 'adv_params', + 'label': 'Advanced Parameters', + 'description': '', + 'type': 'text', + 'default': '' + } + ] + } +] diff --git a/shadowcraft/calcs/rogue/__init__.py b/shadowcraft/calcs/rogue/__init__.py index 25c0a02..6c5a373 100755 --- a/shadowcraft/calcs/rogue/__init__.py +++ b/shadowcraft/calcs/rogue/__init__.py @@ -1,92 +1,192 @@ +from __future__ import division +from future import standard_library +standard_library.install_aliases() +from builtins import range import gettext -import __builtin__ +import builtins -__builtin__._ = gettext.gettext +_ = gettext.gettext from shadowcraft.calcs import DamageCalculator from shadowcraft.core import exceptions class RogueDamageCalculator(DamageCalculator): - # Functions of general use to rogue damage calculation go here. If a + # Functions of general use to rogue damage calculation go here. If a # calculation will reasonably used for multiple classes, it should go in - # calcs.DamageCalculator instead. If its a specific intermediate + # calcs.DamageCalculator instead. If its a specific intermediate # value useful to only your calculations, when you extend this you should - # put the calculations in your object. But there are things - like + # put the calculations in your object. But there are things - like # backstab damage as a function of AP - that (almost) any rogue damage # calculator will need to know, so things like that go here. - - default_ep_stats = ['agi', 'haste', 'crit', 'mastery', 'ap', 'multistrike', 'versatility'] #'readiness' - melee_attacks = ['mh_autoattack_hits', 'oh_autoattack_hits', 'autoattack', - 'eviscerate', 'envenom', 'ambush', 'garrote', - 'sinister_strike', 'revealing_strike', 'main_gauche', 'mh_killing_spree', 'oh_killing_spree', - 'backstab', 'hemorrhage', - 'mutilate', 'mh_mutilate', 'oh_mutilate', 'dispatch', "death_from_above_strike"] - other_attacks = ['deadly_instant_poison', 'swift_poison'] - aoe_attacks = ['fan_of_knives', 'crimson_tempest', "death_from_above_pulse"] - dot_ticks = ['rupture_ticks', 'garrote_ticks', 'deadly_poison', 'hemorrhage_dot'] - ranged_attacks = ['shuriken_toss', 'throw'] - non_dot_attacks = melee_attacks + ranged_attacks + aoe_attacks - all_attacks = melee_attacks + ranged_attacks + dot_ticks + aoe_attacks + other_attacks - - assassination_mastery_conversion = .035 - combat_mastery_conversion = .02 - subtlety_mastery_conversion = .03 - assassination_readiness_conversion = 1.0 - combat_readiness_conversion = 1.0 - subtlety_readiness_conversion = 1.0 - - raid_modifiers_cache = {'physical':None, - 'bleed':None, - 'spell':None} - + + default_ep_stats = ['agi', 'haste', 'crit', 'mastery', 'ap', 'versatility'] + + assassination_damage_sources = ['death_from_above_pulse', 'death_from_above_strike', + 'deadly_poison', 'deadly_instant_poison', 'envenom', + 'fan_of_knives', 'garrote_ticks', 'hemorrhage', + 'kingsbane', 'kingsbane_ticks', 'mutilate', + 'poisoned_knife', 'poison_bomb', 'rupture_ticks', 'from_the_shadows', + 'wound_poison', 'toxic_blade'] + outlaw_damage_sources = ['death_from_above_pulse', 'death_from_above_strike', + 'ambush', 'between_the_eyes', 'blunderbuss', 'cannonball_barrage', + 'ghostly_strike', 'greed', 'killing_spree', 'main_gauche', + 'pistol_shot', 'run_through', 'saber_slash'] + subtlety_damage_sources = ['death_from_above_pulse', 'death_from_above_strike', + 'backstab', 'eviscerate', 'gloomblade', + 'goremaws_bite', 'nightblade', 'shadowstrike', + 'shadow_blades', 'shuriken_storm', 'shuriken_toss', + 'nightblade_ticks', 'soul_rip', 'shadow_nova', 'second_shuriken'] + #All damage sources mitigated by armor + physical_damage_sources = ['death_from_above_pulse', 'death_from_above_strike', + 'fan_of_knives', 'hemorrhage', 'mutilate', 'poisoned_knife', + 'ambush', 'between_the_eyes', 'blunderbuss', 'cannonball_barrage', + 'ghostly_strike', 'greed', 'killing_spree', 'main_gauche', + 'pistol_shot', 'run_through', 'saber_slash', 'backstab', + 'eviscerate', 'shadowstrike', 'shuriken_storm', 'shuriken_toss'] + #All damage sources that deal damage with both hands + dual_wield_damage_sources = ['kingsbane', 'mutilate', 'greed', 'killing_spree', + 'goremaws_bite', 'shadow_blades'] + #All damage sources that scale with cps + finisher_damage_sources = ['death_from_above_pulse', 'death_from_above_strike', + 'envenom', 'rupture_ticks', 'between_the_eyes', + 'run_through', 'eviscerate', + 'nightblade', 'nightblade_ticks', + 'roll_the_bones', 'slice_and_dice'] + #All damage source that are replicated by Blade Flurry + blade_flurry_damage_sources = ['death_from_above_pulse', 'death_from_above_strike', + 'ambush', 'between_the_eyes', 'blunderbuss', 'ghostly_strike', 'greed', 'killing_spree', + 'main_gauche','pistol_shot', 'run_through', 'saber_slash'] + + #probability of getting X buffs with rtb + rtb_probabilities = { + 1: 0.5923, + 2: 0.3537, + 3: 0.0386, + 6: 0.0154, + } + + #probabilities of getting X buffs from RtB with loaded dice + #assume we reroll/blacklist one buff rolls instead of adding a second buff to one rolls + #so far this assumption could neither be confirmed nor disproved + #TODO: actually plug these into the model :/ + rtb_loaded_dice_probabilities = { + 1: 0, + 2: 0.8675, + 3: 0.0946, + 6: 0.0379, + } + + #number of unique rtb buffs of each amount + rtb_buff_count = { + 1: 6, + 2: 15, + 3: 20, + 6: 1, + } + + assassination_mastery_conversion = .04 + outlaw_mastery_conversion = .022 + subtlety_mastery_conversion = .0276 + ability_info = { - 'ambush': (60., 'strike'), - 'backstab': (35., 'strike'), - 'dispatch': (30., 'strike'), + #general + 'crimson_vial': (30., 'buff'), + 'death_from_above': (25., 'strike'), + 'feint': (35., 'buff'), + 'kick': (15., 'strike'), + #assassination 'envenom': (35., 'strike'), - 'eviscerate': (35., 'strike'), + 'fan_of_knives': (35., 'strike'), 'garrote': (45., 'strike'), 'hemorrhage': (30., 'strike'), + 'kingsbane': (35., 'strike'), 'mutilate': (55., 'strike'), - 'recuperate': (30., 'buff'), - 'revealing_strike': (40., 'strike'), + 'poisoned_knife': (40., 'strike'), 'rupture': (25., 'strike'), - 'sinister_strike': (50., 'strike'), + 'toxic_blade': (20., 'strike'), + 'exsanguinate': (25., 'buff'), + #outlaw + 'ambush': (60., 'strike'), + 'between_the_eyes': (35., 'strike'), + 'blunderbuss': (40., 'strike'), + 'ghostly_strike': (30., 'strike'), + 'pistol_shot': (40., 'strike'), + 'roll_the_bones': (25., 'buff'), + 'run_through': (35., 'strike'), + 'saber_slash': (50., 'strike'), 'slice_and_dice': (25., 'buff'), - 'tricks_of_the_trade': (0, 'buff'), + #subtlety + 'backstab': (35., 'strike'), + 'eviscerate': (35., 'strike'), + 'gloomblade': (35., 'strike'), + 'nightblade': (25., 'strike'), + 'shadowstrike': (40., 'strike'), + 'shuriken_storm': (35., 'strike'), 'shuriken_toss': (40., 'strike'), - 'shiv': (20., 'strike'), - 'feint': (20., 'buff'), - 'death_from_above': (50., 'strike'), } ability_cds = { - 'tricks_of_the_trade': 30, - 'kick': 15, - 'shiv': 8, - 'vanish': 120, - 'vendetta': 120, - 'adrenaline_rush': 180, - 'killing_spree': 120, - 'shadow_dance': 60, - 'shadowmeld': 120, - 'marked_for_death': 60, - 'preparation': 300, - 'death_from_above': 20, - 'shadow_reflection': 120, + #general + 'crimson_vial': 30., + 'death_from_above': 20., + 'kick': 15., + 'marked_for_death': 60., + 'sprint': 60., + 'tricks_of_the_trade': 30., + 'vanish': 120., + #assassination + 'exsanguinate': 45., + 'garrote': 15., + 'kingsbane': 45., + 'vendetta': 120., + 'toxic_blade': 25., + #outlaw + 'adrenaline_rush': 180., + 'cannonball_barrage': 60., + 'curse_of_the_dreadblades': 90., + 'killing_spree': 120., + #subtlety + 'goremaws_bite': 60., + 'shadow_dance': 60., + 'shadow_blades': 180., + 'symbols_of_death': 30., } - cd_reduction_table = {'assassination': ['vanish', 'vendetta'], - 'combat': ['adrenaline_rush', 'killing_spree'], - 'subtlety': ['shadow_dance'] - } - + + # Vendetta CDR for number of points in trait + master_assassin_cdr = { + 0: 0, + 1: 10, + 2: 20, + 3: 30, + 4: 38, + 5: 44, + 6: 48, + 7: 52, + 8: 56 + } + + # Adrenaline Rush CDR for number of points in trait + fortunes_boon_cdr = { + 0: 0, + 1: 10, + 2: 18, + 3: 25, + 4: 31, + 5: 37, + 6: 42, + 7: 46, + 8: 49 + } + def __setattr__(self, name, value): object.__setattr__(self, name, value) if name == 'level': self._set_constants_for_level() def _set_constants_for_level(self): - # this calls _set_constants_for_level() in calcs/__init__.py because this supercedes it, this is how inheretence in python works - # any modules that expand on rogue/__init__.py and use this should do the same + # this calls _set_constants_for_level() in calcs/__init__.py because + # this supercedes it, this is how inheretence in python works + # any modules that expand on rogue/__init__.py and use this should do + # the same super(RogueDamageCalculator, self)._set_constants_for_level() self.normalize_ep_stat = self.get_adv_param('norm_ep_stat', self.settings.default_ep_stat, ignore_bounds=True) self.damage_modifier_cache = 1 @@ -102,415 +202,474 @@ def get_weapon_damage(self, hand, ap, is_normalized=True): def oh_penalty(self): return .5 - def get_modifiers(self, current_stats, damage_type='physical', armor=None, executioner_modifier=1., potent_poisons_modifier=1.): - # self.damage_modifier_cache stores common modifiers like Assassin's Resolve that won't change between calculations - # this cuts down on repetitive if statements + def get_base_modifier(self, current_stats): base_modifier = self.damage_modifier_cache - - # Raid modifiers - if not self.raid_modifiers_cache[damage_type]: - self.raid_modifiers_cache[damage_type] = self.raid_settings_modifiers(attack_kind=damage_type, armor=armor) - base_modifier *= self.raid_modifiers_cache[damage_type] - - # potent poisons and executioner should be calculated outside, and passed in, no need to recalculate the % each time - base_modifier *= executioner_modifier - base_modifier *= potent_poisons_modifier - - #versatility is a generic damage modifier - base_modifier *= (self.stats.get_versatility_multiplier_from_rating(rating=current_stats['versatility']) + self.buffs.versatility_bonus()) - + base_modifier *= self.stats.get_versatility_multiplier_from_rating(rating=current_stats['versatility']) return base_modifier - + def get_dps_contribution(self, base_damage, crit_rate, frequency, crit_modifier): average_hit = base_damage * (1 - crit_rate) + base_damage * crit_rate * crit_modifier return average_hit * frequency - + + + #Computes a merged dps contribution for an ability + def get_ability_dps(self, ap, ability, attacks_per_second, crit_rate, modifier, crit_modifier, both_hands=False, cps=0): + if both_hands: + ability_list = [hand + ability for hand in ['mh_', 'oh_']] + else: + ability_list = [ability] + + dps = 0 + if not cps: + for a in ability_list: + base_damage = self.get_formula(a)(ap) * modifier + dps += self.get_dps_contribution(base_damage, crit_rate, attacks_per_second, crit_modifier) + else: + for i in range(1, cps + 1): + for a in ability_list: + base_damage = self.get_formula(a)(ap, i) * modifier + dps += self.get_dps_contribution(base_damage, crit_rate, attacks_per_second[i], crit_modifier) + return dps + def get_damage_breakdown(self, current_stats, attacks_per_second, crit_rates, damage_procs, additional_info): average_ap = current_stats['ap'] + current_stats['agi'] * self.stat_multipliers['ap'] - + max_cps = 5 + int(self.talents.deeper_stratagem) + self.setup_unique_procs(current_stats, average_ap) damage_breakdown = {} - - # we calculate mastery here to reduce redundant calls - # can't rely on spec init thread because stats change afterwards - executioner_mod = 1. - potent_poisons_mod = 1. - if self.settings.is_subtlety_rogue(): - executioner_mod = 1 + self.subtlety_mastery_conversion * self.stats.get_mastery_from_rating(current_stats['mastery']) - if self.settings.is_assassination_rogue(): - potent_poisons_mod = 1 + self.assassination_mastery_conversion * self.stats.get_mastery_from_rating(current_stats['mastery']) - - # these return the tuple (damage_modifier, crit_multiplier) + crit_damage_modifier = self.crit_damage_modifiers() - physical_modifier = self.get_modifiers(current_stats, damage_type='physical') - spell_modifier = self.get_modifiers(current_stats, damage_type='spell') - bleed_modifier = self.get_modifiers(current_stats, damage_type='bleed') - + + modifier_dict = self.damage_modifiers.compile_modifier_dict() + + # this removes keys with empty values, prevents errors from: + # attacks_per_second['sinister_strike'] = None + for key in list(attacks_per_second.keys()): + if not attacks_per_second[key]: + del attacks_per_second[key] + if 'mh_autoattacks' in attacks_per_second: - # Assumes mh and oh attacks are both active at the same time. As they should always be. + # Assumes mh and oh attacks are both active at the same time. As + # they should always be. # Friends don't let friends raid without gear. - mh_base_damage = self.mh_damage(average_ap) * physical_modifier + mh_base_damage = self.mh_damage(average_ap) * modifier_dict['autoattacks'] mh_hit_rate = self.dw_mh_hit_chance - crit_rates['mh_autoattacks'] average_mh_hit = mh_hit_rate * mh_base_damage + crit_rates['mh_autoattacks'] * mh_base_damage * crit_damage_modifier mh_dps_tuple = average_mh_hit * attacks_per_second['mh_autoattacks'] - - oh_base_damage = self.oh_damage(average_ap) * physical_modifier + + oh_base_damage = self.oh_damage(average_ap) * modifier_dict['autoattacks'] oh_hit_rate = self.dw_oh_hit_chance - crit_rates['oh_autoattacks'] average_oh_hit = oh_hit_rate * oh_base_damage + crit_rates['oh_autoattacks'] * oh_base_damage * crit_damage_modifier oh_dps_tuple = average_oh_hit * attacks_per_second['oh_autoattacks'] - if self.settings.merge_damage: - damage_breakdown['autoattack'] = mh_dps_tuple + oh_dps_tuple - else: - damage_breakdown['mh_autoattack'] = mh_dps_tuple - damage_breakdown['oh_autoattack'] = oh_dps_tuple - - # this removes keys with empty values, prevents errors from: attacks_per_second['sinister_strike'] = None - for key in attacks_per_second.keys(): - if not attacks_per_second[key]: - del attacks_per_second[key] - - if 'mutilate' in attacks_per_second: - mh_dmg = self.mh_mutilate_damage(average_ap) * physical_modifier - oh_dmg = self.oh_mutilate_damage(average_ap) * physical_modifier - mh_mutilate_dps = self.get_dps_contribution(mh_dmg, crit_rates['mutilate'], attacks_per_second['mutilate'], crit_damage_modifier) - oh_mutilate_dps = self.get_dps_contribution(oh_dmg, crit_rates['mutilate'], attacks_per_second['mutilate'], crit_damage_modifier) - if self.settings.merge_damage: - damage_breakdown['mutilate'] = mh_mutilate_dps + oh_mutilate_dps - else: - damage_breakdown['mh_mutilate'] = mh_mutilate_dps - damage_breakdown['oh_mutilate'] = oh_mutilate_dps - - for strike in ('hemorrhage', 'backstab', 'sinister_strike', 'revealing_strike', 'main_gauche', 'ambush', 'dispatch', 'shuriken_toss'): - if strike in attacks_per_second: - dps = self.get_formula(strike)(average_ap) * physical_modifier - dps = self.get_dps_contribution(dps, crit_rates[strike], attacks_per_second[strike], crit_damage_modifier) - if strike in ('sinister_strike', 'backstab'): - dps *= self.stats.gear_buffs.rogue_t14_2pc_damage_bonus(strike) - damage_breakdown[strike] = dps - - for poison in ('venomous_wounds', 'deadly_poison', 'wound_poison', 'deadly_instant_poison', 'swift_poison'): - if poison in attacks_per_second: - damage = self.get_formula(poison)(average_ap) * spell_modifier * potent_poisons_mod - damage = self.get_dps_contribution(damage, crit_rates[poison], attacks_per_second[poison], crit_damage_modifier) - if poison == 'venomous_wounds': - damage *= self.stats.gear_buffs.rogue_t14_2pc_damage_bonus('venomous_wounds') - damage_breakdown[poison] = damage - - if 'mh_killing_spree' in attacks_per_second: - mh_dmg = self.mh_killing_spree_damage(average_ap) * physical_modifier - oh_dmg = self.oh_killing_spree_damage(average_ap) * physical_modifier - mh_killing_spree_dps = self.get_dps_contribution(mh_dmg, crit_rates['killing_spree'], attacks_per_second['mh_killing_spree'], crit_damage_modifier) - oh_killing_spree_dps = self.get_dps_contribution(oh_dmg, crit_rates['killing_spree'], attacks_per_second['oh_killing_spree'], crit_damage_modifier) - if self.settings.merge_damage: - damage_breakdown['killing_spree'] = mh_killing_spree_dps + oh_killing_spree_dps - else: - damage_breakdown['mh_killing_spree'] = mh_killing_spree_dps - damage_breakdown['oh_killing_spree'] = oh_killing_spree_dps - - if 'garrote_ticks' in attacks_per_second: - dps_tuple = self.garrote_tick_damage(average_ap) * bleed_modifier - damage_breakdown['garrote'] = self.get_dps_contribution(dps_tuple, crit_rates['garrote'], attacks_per_second['garrote_ticks'], crit_damage_modifier) - - if 'hemorrhage_ticks' in attacks_per_second: - hemo_hit = self.hemorrhage_tick_damage(average_ap) * bleed_modifier - dps_from_hit_hemo = self.get_dps_contribution(hemo_hit, crit_rates['hemorrhage'], attacks_per_second['hemorrhage_ticks'], crit_damage_modifier) - damage_breakdown['hemorrhage_dot'] = dps_from_hit_hemo - - if 'rupture_ticks' in attacks_per_second: - average_dps = 0 - for i in xrange(1, 6): - dps_tuple = self.rupture_tick_damage(average_ap, i) * bleed_modifier * executioner_mod - dps_tuple = self.get_dps_contribution(dps_tuple, crit_rates['rupture_ticks'], attacks_per_second['rupture_ticks'][i], crit_damage_modifier) - average_dps += dps_tuple - damage_breakdown['rupture'] = average_dps - if 'rupture_ticks_sc' in attacks_per_second: - average_dps = 0 - for i in xrange(1, 6): - dps_tuple = self.rupture_tick_damage(average_ap, i) * bleed_modifier * executioner_mod - dps_tuple = self.get_dps_contribution(dps_tuple, 0, attacks_per_second['rupture_ticks_sc'][i], crit_damage_modifier) - average_dps += dps_tuple - damage_breakdown['rupture_sc'] = average_dps - - if 'envenom' in attacks_per_second: - average_dps = 0 - for i in xrange(1, 6): - dps_tuple = self.envenom_damage(average_ap, i) * potent_poisons_mod * spell_modifier - dps_tuple = self.get_dps_contribution(dps_tuple, crit_rates['envenom'], attacks_per_second['envenom'][i], crit_damage_modifier) - average_dps += dps_tuple - damage_breakdown['envenom'] = average_dps - - if 'eviscerate' in attacks_per_second: - average_dps = 0 - for i in xrange(1, 6): - dps_tuple = self.eviscerate_damage(average_ap, i) * physical_modifier * executioner_mod - dps_tuple = self.get_dps_contribution(dps_tuple, crit_rates['eviscerate'], attacks_per_second['eviscerate'][i], crit_damage_modifier) - average_dps += dps_tuple - damage_breakdown['eviscerate'] = average_dps - - if 'death_from_above_strike' in attacks_per_second: - if self.settings.get_spec() == 'assassination': - average_dps = 0 - for i in xrange(1, 6): - dps_tuple = self.envenom_damage(average_ap, i) * potent_poisons_mod * spell_modifier * 1.5 - dps_tuple = self.get_dps_contribution(dps_tuple, crit_rates['death_from_above_strike'], attacks_per_second['death_from_above_strike'][i], crit_damage_modifier) - average_dps += dps_tuple - damage_breakdown['death_from_above_strike'] = average_dps - else: - average_dps = 0 - for i in xrange(1, 6): - dps_tuple = self.eviscerate_damage(average_ap, i) * physical_modifier * executioner_mod * 1.5 - dps_tuple = self.get_dps_contribution(dps_tuple, crit_rates['death_from_above_strike'], attacks_per_second['death_from_above_strike'][i], crit_damage_modifier) - average_dps += dps_tuple - damage_breakdown['death_from_above_strike'] = average_dps - - if 'death_from_above_pulse' in attacks_per_second: - average_dps = 0 - for i in xrange(1, 6): - dps_tuple = self.death_from_above_pulse_damage(average_ap, i) * physical_modifier * executioner_mod - dps_tuple = self.get_dps_contribution(dps_tuple, crit_rates['death_from_above_pulse'], attacks_per_second['death_from_above_pulse'][i], crit_damage_modifier) - average_dps += dps_tuple - damage_breakdown['death_from_above_pulse'] = average_dps - - #shadow reflection code block - if self.talents.shadow_reflection: - for ability in attacks_per_second: - if 'sr_' in ability: - modifier = 1. - if ability[3:] in ('envenom'): - modifier *= spell_modifier * potent_poisons_mod - elif ability == 'sr_rupture_ticks': - modifier *= bleed_modifier - else: - if ability == 'sr_eviscerate': - modifier *= executioner_mod - modifier *= physical_modifier - crit_name = ability - if ability not in crit_rates: - crit_name = ability[3:] - if 'mh_' in crit_name or 'oh_' in crit_name: - crit_name = crit_name[3:] - if type(attacks_per_second[ability]) in (tuple, list): - average_dps = 0 - for i in xrange(1, 6): - dps_tuple = self.get_formula(ability)(average_ap, i) * modifier - dps_tuple = self.get_dps_contribution(dps_tuple, crit_rates[crit_name], attacks_per_second[ability][i], crit_damage_modifier) - average_dps += dps_tuple - damage_breakdown[ability] = average_dps - else: - average_dps = self.get_formula(ability)(average_ap) * modifier - average_dps = self.get_dps_contribution(average_dps, crit_rates[crit_name], attacks_per_second[ability], crit_damage_modifier) - damage_breakdown[ability] = average_dps - + damage_breakdown['autoattack'] = mh_dps_tuple + oh_dps_tuple + for proc in damage_procs: if proc.proc_name not in damage_breakdown: # Toss multiple damage procs with the same name (Avalanche): # attacks_per_second is already being updated with that key. - damage_breakdown[proc.proc_name] = self.get_proc_damage_contribution(proc, attacks_per_second[proc.proc_name], current_stats, average_ap, damage_breakdown) - - if self.talents.nightstalker: - nightstalker_mod = .50 - if self.settings.opener_name in ('eviscerate', 'envenom'): - ability = attacks_per_second[self.settings.opener_name][5] - elif self.settings.opener_name in attacks_per_second: - ability = attacks_per_second[self.settings.opener_name] - nightstalker_percent = self.total_openers_per_second / (ability) - modifier = 1 + nightstalker_mod * nightstalker_percent - damage_breakdown[self.settings.opener_name] *= modifier - - + if proc.stat in ['physical_dot', 'spell_dot']: + self.set_uptime(proc, attacks_per_second, crit_rates) + damage_breakdown[proc.proc_name] = self.get_proc_damage_contribution(proc, attacks_per_second[proc.proc_name], current_stats, average_ap, modifier_dict) + + self.add_special_procs_damage(current_stats, attacks_per_second, crit_rates, modifier_dict, damage_breakdown) + + #compute damage breakdown for each spec + if self.spec == 'assassination': + + for ability in self.assassination_damage_sources: + if ability not in attacks_per_second: + continue + aps = attacks_per_second[ability] + crits = crit_rates[ability] + crit_mod = crit_damage_modifier + modifier = modifier_dict[ability] + both_hands = ability in self.dual_wield_damage_sources + cps = max_cps if ability in self.finisher_damage_sources else 0 + + #override for "weird" abilities + #death from above strike is actually an envenom with 1.5 + #modifier + #manually add in base modifier because DfA strike is in + #physical sources + if ability == 'death_from_above_strike': + modifier *= 1.5 + ability = 'envenom' + if ability in damage_breakdown: + damage_breakdown[ability] += self.get_ability_dps(average_ap, ability, aps, crits, modifier, crit_mod, both_hands, cps) + else: + damage_breakdown[ability] = self.get_ability_dps(average_ap, ability, aps, crits, modifier, crit_mod, both_hands, cps) + + if self.spec == 'outlaw': + for ability in self.outlaw_damage_sources: + if ability not in attacks_per_second: + continue + aps = attacks_per_second[ability] + crits = crit_rates[ability] + crit_mod = crit_damage_modifier + modifier = modifier_dict[ability] + both_hands = ability in self.dual_wield_damage_sources + cps = max_cps if ability in self.finisher_damage_sources else 0 + + #override for "weird" abilities + #death from above strike is actually an evis with 1.5 modifier + if ability == 'death_from_above_strike': + modifier *= 1.5 + ability = 'run_through' + #between the eyes has additional crit damage + #Damage modifier 3 explained here: + #http://beta.askmrrobot.com/wow/simulator/docs/critdamage + if ability == 'between_the_eyes': + crit_mod = self.crit_damage_modifiers(3) + if ability == 'saber_slash' and self.traits.sabermetrics: + crit_mod = self.crit_damage_modifiers(1 + self.traits.sabermetrics * 0.05) + if ability in damage_breakdown: + damage_breakdown[ability] += self.get_ability_dps(average_ap, ability, aps, crits, modifier, crit_mod, both_hands, cps) + else: + damage_breakdown[ability] = self.get_ability_dps(average_ap, ability, aps, crits, modifier, crit_mod, both_hands, cps) + + if self.spec == 'subtlety': + + for ability in self.subtlety_damage_sources: + if ability not in attacks_per_second: + continue + aps = attacks_per_second[ability] + crits = crit_rates[ability] + crit_mod = crit_damage_modifier + modifier = modifier_dict[ability] + both_hands = ability in self.dual_wield_damage_sources + cps = max_cps if ability in self.finisher_damage_sources else 0 + + #override for "weird" abilities + #death from above strike is actually an evis with 1.5 modifier + #and dfa pulse needs mastery + if ability == 'death_from_above_strike': + modifier *= 1.5 + ability = 'eviscerate' + if ability in ['shadowstrike', 'backstab']: + crit_mod *= 1.0 + (0.08 * self.traits.weak_point) + + if ability in damage_breakdown: + damage_breakdown[ability] += self.get_ability_dps(average_ap, ability, aps, crits, modifier, crit_mod, both_hands, cps) + else: + damage_breakdown[ability] = self.get_ability_dps(average_ap, ability, aps, crits, modifier, crit_mod, both_hands, cps) + + #DfA AoE + if 'death_from_above_pulse' in damage_breakdown: + damage_breakdown['death_from_above_pulse'] *= 1 + self.settings.num_boss_adds + return damage_breakdown, additional_info - + #autoattacks def mh_damage(self, ap): return self.get_weapon_damage('mh', ap, is_normalized=False) def oh_damage(self, ap): return self.oh_penalty() * self.get_weapon_damage('oh', ap, is_normalized=False) - - def mh_shuriken(self, ap): - return .75 * mh_damage(ap) #update? - - def oh_shuriken(self, ap): - return .75 * oh_damage(ap) #update? - - #abilities - def ambush_damage(self, ap): - return 3.10 * [1., 1.4][self.stats.mh.type == 'dagger'] * self.get_weapon_damage('mh', ap) - def ambush_sr_damage(self, ap): - return 3.10 * 1.4 * 1.8 * 0.924 * ap / 3.5 - - def backstab_damage(self, ap): - return 2.10 * self.get_weapon_damage('mh', ap) - def backstab_sr_damage(self, ap): - return 2.10 * 1.8 * 0.924 * ap / 3.5 + #general abilities def death_from_above_pulse_damage(self, ap, cp): - return 0.266 * cp * ap - def death_from_above_pulse_sr_damage(self, ap, cp): - return 0.266 * cp * 0.924 * ap - - def dispatch_damage(self, ap): - return 3.30 * self.get_weapon_damage('mh', ap) - def dispatch_sr_damage(self, ap): - return 3.30 * 1.8 * 0.924 * ap / 3.5 - + return 0.8784 * cp * ap #20% buff in 7.1.5 + + #assassination + def deadly_poison_tick_damage(self, ap): + return 0.3575 * ap * (1 + (0.05 * self.traits.master_alchemist)) * (1 + (0.3 * self.talents.master_poisoner)) + + def deadly_instant_poison_damage(self, ap): + return 0.221 * ap * (1 + (0.05 * self.traits.master_alchemist)) * (1 + (0.3 * self.talents.master_poisoner)) + + #Maybe add better handling for 'rule of three' for artifact traits def envenom_damage(self, ap, cp): - return .321 * cp * ap - def envenom_sr_damage(self, ap, cp): - return .321 * cp * 0.924 * ap - - def eviscerate_damage(self, ap, cp): - return .508 * cp * ap - def eviscerate_sr_damage(self, ap, cp): - return .508 * cp * 0.924 * ap - + return .6 * cp * ap * (1 + (0.0333 * self.traits.toxic_blades)) + + def fan_of_knives_damage(self, ap): + return 1.5 * ap + + #Lumping 40 ticks together for simplicity + def from_the_shadows_damage(self, ap): + return 40 * 0.35 * ap + def garrote_tick_damage(self, ap): - return .2241 * ap - def garrote_tick_sr_damage(self, ap): - return .2241 * 0.924 * ap - - #20% damage more hotfix + return .9 * ap * (1 + 0.04 * self.traits.strangler) + def hemorrhage_damage(self, ap): - return 1.3 * 1.2 * .40 * [1., 1.4][self.stats.mh.type == 'dagger'] * self.get_weapon_damage('mh', ap) - def hemorrhage_sr_damage(self, ap): - return 1.3 * 1.2 * .4 * 1.4 * 1.8 * 0.924 * ap / 3.5 + return 1 * self.get_weapon_damage('mh', ap) - def hemorrhage_tick_damage(self, ap): - return 1.3 * 1.2 * .035 * ap - def hemorrhage_tick_sr_damage(self, ap): - return 1.3 * 1.2 * .035 * 0.924 * ap + def mh_kingsbane_damage(self, ap): + return 2.4 * self.get_weapon_damage('mh', ap) * (1 + (0.3 * self.talents.master_poisoner)) + def oh_kingsbane_damage(self, ap): + return 2.4 * self.oh_penalty() * self.get_weapon_damage('oh', ap) * (1 + (0.3 * self.talents.master_poisoner)) + def kingsbane_tick_damage(self, ap): + return 0.36 * ap * (1 + (0.3 * self.talents.master_poisoner)) + def mh_mutilate_damage(self, ap): + return 3.6 * self.get_weapon_damage('mh', ap) * (1 + (0.15 * self.traits.assassins_blades)) + + def oh_mutilate_damage(self, ap): + return 3.6 * self.oh_penalty() * self.get_weapon_damage('oh', ap) * (1 + (0.15 * self.traits.assassins_blades)) + + def poisoned_knife_damage(self, ap): + return 0.6 * ap + + def toxic_blade_damage(self, ap): + return 6 * self.get_weapon_damage('mh', ap) + + #Lumping 6 ticks together for simplicity + def poison_bomb_damage(self, ap): + return 6 * 1.2 * ap * (1 + (0.3 * self.talents.master_poisoner)) + + def rupture_tick_damage(self, ap, cp): + return 1.5 * ap * (1 + (0.0333 * self.traits.gushing_wounds)) + def wound_poison_damage(self, ap): + return 0.13 * ap * (1 + (0.05 * self.traits.master_alchemist)) * (1 + (0.3 * self.talents.master_poisoner)) + + #outlaw + def ambush_damage(self, ap): + return 4.5 * self.get_weapon_damage('mh', ap) + + def between_the_eyes_damage(self, ap, cp): + return .85 * cp * ap * (1 + (0.06 * self.traits.black_powder)) + + #7*121% AP + def blunderbuss_damage(self, ap): + return 8.47 * ap + + #Ignoring that this behaves as a dot for simplicity, 6*150% + def cannonball_barrage_damage(self, ap): + return 9 * ap + + def ghostly_strike_damage(self, ap): + return 1.94 * self.get_weapon_damage('mh', ap) + + def mh_greed_damage(self, ap): + return 3.5 * self.get_weapon_damage('mh', ap) + + def oh_greed_damage(self, ap): + return 3.5 * self.oh_penalty() * self.get_weapon_damage('oh', ap) + + #For KsP treat each hit individually def mh_killing_spree_damage(self, ap): - return 1.0 * self.get_weapon_damage('mh', ap) - def mh_killing_spree_sr_damage(self, ap): - return 1.0 * 1.8 * 0.924 * ap / 3.5 + return 2.6 * self.get_weapon_damage('mh', ap) def oh_killing_spree_damage(self, ap): - return 1.0 * self.oh_penalty() * self.get_weapon_damage('oh', ap) - def oh_killing_spree_sr_damage(self, ap): - return 1.0 * 1.8 * 0.924 * ap / 3.5 * 0.5 - + return 2.6 * self.oh_penalty() * self.get_weapon_damage('oh', ap) + def main_gauche_damage(self, ap): - return 1.4 * self.oh_penalty() * self.get_weapon_damage('oh', ap) - def main_gauche_sr_damage(self, ap): - return 1.4 * 1.8 * 0.924 * ap / 3.5 - - def mh_mutilate_damage(self, ap): - return 2.1 * self.get_weapon_damage('mh', ap) - def mh_mutilate_sr_damage(self, ap): - return 2.1 * 1.8 * 0.924 * ap / 3.5 + return 2.1 * self.oh_penalty() * self.get_weapon_damage('oh', ap) * (1 + (0.1 * self.traits.fortunes_strike)) - def oh_mutilate_damage(self, ap): - return 2.1 * self.oh_penalty() * self.get_weapon_damage('oh', ap) - def oh_mutilate_sr_damage(self, ap): - return 2.1 * 1.8 * 0.924 * ap / 3.5 * 0.5 - - def revealing_strike_damage(self, ap): - return 1.2 * self.get_weapon_damage('mh', ap) - def revealing_strike_sr_damage(self, ap): - return 1.2 * 1.8 * 0.924 * ap / 3.5 - - def rupture_tick_damage(self, ap, cp): - return .0685 * cp * ap - def rupture_tick_sr_damage(self, ap, cp): - return .0685 * 0.924 * ap - - def sinister_strike_damage(self, ap): - return 1.6 * self.get_weapon_damage('mh', ap) - def sinister_strike_sr_damage(self, ap): - return 1.6 * 1.8 * 0.924 * ap / 3.5 - - def venomous_wounds_damage(self, ap): - return 1.2 * .320 * ap - def venomous_wounds_sr_damage(self, ap): - return 1.2 * .320 * 0.924 * ap - - #poisons - def deadly_poison_tick_damage(self, ap): - return .25014 * ap + def pistol_shot_damage(self, ap): + return 1.65 * ap - def deadly_instant_poison_damage(self, ap): - return .1287000030 * ap + def run_through_damage(self, ap, cp): + return 1.42 * ap * cp * (1 + (0.04 * self.traits.fates_thirst)) - def swift_poison_damage(self, ap): - return .264 * ap + def saber_slash_damage(self, ap): + return 3.02 * self.get_weapon_damage('mh', ap) * (1 + (0.15 * self.traits.cursed_edges)) - def wound_poison_damage(self, ap): - return .6 * .2179999948 * ap #40% reduction hotfix - - #unused - def fan_of_knives_damage(self, ap): - return .231 * ap + #subtlety + #Ignore positional modifier for now + def backstab_damage(self, ap): + return 5 * self.get_weapon_damage('mh', ap) * (1 + (0.05 * self.traits.the_quiet_knife)) + + #has two ranks + def eviscerate_damage(self, ap, cp): + return 1.472 * cp * ap + + def gloomblade_damage(self, ap): + return 5.75 * self.get_weapon_damage('mh', ap) * (1 + (0.05 * self.traits.the_quiet_knife)) - def crimson_tempest_damage(self, ap, cp): - return .0903 * cp * ap + def mh_goremaws_bite_damage(self, ap): + return 10 * self.get_weapon_damage('mh', ap) - def crimson_tempest_tick_damage(self, ap, cp): - return self.crimson_tempest_damage(ap, cp) * (2.4 / 6) + def oh_goremaws_bite_damage(self, ap): + return 10 * self.oh_penalty() * self.get_weapon_damage('oh', ap) - def shiv_damage(self, ap): - return .10 * self.oh_penalty() * self.get_weapon_damage('oh', ap, is_normalized=False) + #Nightblade doesn't actually scale with cps but passing cps for simplicity + def nightblade_tick_damage(self, ap, cp): + return 0.9 * ap * (1 + (0.05 * self.traits.demons_kiss)) - def throw_damage(self, ap): - return .05 * ap + def shadowstrike_damage(self, ap): + return 8.5 * self.get_weapon_damage('mh', ap) * (1 + (0.05 * self.traits.precision_strike)) + + def mh_shadow_blades_damage(self, ap): + return self.get_weapon_damage('mh', ap, is_normalized=False) + + def oh_shadow_blades_damage(self, ap): + return self.oh_penalty() * self.get_weapon_damage('oh', ap, is_normalized=False) + + def second_shuriken_damage(self, ap): + return 1.0 * ap + + def shuriken_storm_damage(self, ap): + return 0.7215 * ap def shuriken_toss_damage(self, ap): return 1.2 * ap - + + def soul_rip_damage(self, ap): + return 1.5 * ap + + def shadow_nova_damage(self, ap): + return 2.5 * ap + def get_formula(self, name): formulas = { - 'backstab': self.backstab_damage, - 'hemorrhage': self.hemorrhage_damage, - 'sinister_strike': self.sinister_strike_damage, - 'revealing_strike': self.revealing_strike_damage, - 'main_gauche': self.main_gauche_damage, - 'ambush': self.ambush_damage, - 'eviscerate': self.eviscerate_damage, - 'dispatch': self.dispatch_damage, - 'mh_mutilate': self.mh_mutilate_damage, - 'oh_mutilate': self.oh_mutilate_damage, - 'venomous_wounds': self.venomous_wounds_damage, - 'deadly_poison': self.deadly_poison_tick_damage, - 'wound_poison': self.wound_poison_damage, - 'deadly_instant_poison': self.deadly_instant_poison_damage, - 'swift_poison': self.swift_poison_damage, - 'shuriken_toss': self.shuriken_toss_damage, - 'death_from_above_pulse':self.death_from_above_pulse_damage, - #shadow reflection abilities - 'sr_backstab': self.backstab_sr_damage, - 'sr_hemorrhage': self.hemorrhage_sr_damage, - 'sr_sinister_strike': self.sinister_strike_sr_damage, - 'sr_revealing_strike': self.revealing_strike_sr_damage, - 'sr_main_gauche': self.main_gauche_sr_damage, - 'sr_ambush': self.ambush_sr_damage, - 'sr_eviscerate': self.eviscerate_sr_damage, - 'sr_envenom': self.envenom_sr_damage, - 'sr_dispatch': self.dispatch_sr_damage, - 'sr_mh_mutilate': self.mh_mutilate_sr_damage, - 'sr_oh_mutilate': self.oh_mutilate_sr_damage, - 'sr_mh_killing_spree': self.mh_killing_spree_sr_damage, - 'sr_oh_killing_spree': self.oh_killing_spree_sr_damage, - 'sr_venomous_wounds': self.venomous_wounds_sr_damage, - 'sr_rupture_ticks': self.rupture_tick_sr_damage, + #general + 'mh_autoattack': self.mh_damage, + 'oh_autoattack': self.oh_damage, + 'death_from_above_pulse': self.death_from_above_pulse_damage, + #assassination + 'deadly_poison': self.deadly_poison_tick_damage, + 'deadly_instant_poison': self.deadly_instant_poison_damage, + 'envenom': self.envenom_damage, + 'fan_of_knives': self.fan_of_knives_damage, + 'from_the_shadows': self.from_the_shadows_damage, + 'garrote_ticks': self.garrote_tick_damage, + 'hemorrhage': self.hemorrhage_damage, + 'mh_kingsbane': self.mh_kingsbane_damage, + 'oh_kingsbane': self.oh_kingsbane_damage, + 'kingsbane_ticks': self.kingsbane_tick_damage, + 'mh_mutilate': self.mh_mutilate_damage, + 'oh_mutilate': self.oh_mutilate_damage, + 'poisoned_knife': self.poisoned_knife_damage, + 'poison_bomb': self.poison_bomb_damage, + 'rupture_ticks': self.rupture_tick_damage, + 'wound_poison': self.wound_poison_damage, + 'toxic_blade': self.toxic_blade_damage, + #outlaw + 'ambush': self.ambush_damage, + 'between_the_eyes': self.between_the_eyes_damage, + 'blunderbuss': self.blunderbuss_damage, + 'cannonball_barrage': self.cannonball_barrage_damage, + 'ghostly_strike': self.ghostly_strike_damage, + 'mh_greed': self.mh_greed_damage, + 'oh_greed': self.oh_greed_damage, + 'mh_killing_spree': self.mh_killing_spree_damage, + 'oh_killing_spree': self.oh_killing_spree_damage, + 'main_gauche': self.main_gauche_damage, + 'pistol_shot': self.pistol_shot_damage, + 'run_through': self.run_through_damage, + 'saber_slash': self.saber_slash_damage, + #subtlety + 'backstab': self.backstab_damage, + 'eviscerate': self.eviscerate_damage, + 'gloomblade': self.gloomblade_damage, + 'mh_goremaws_bite': self.mh_goremaws_bite_damage, + 'oh_goremaws_bite': self.oh_goremaws_bite_damage, + 'nightblade_ticks': self.nightblade_tick_damage, + 'shadowstrike': self.shadowstrike_damage, + 'mh_shadow_blades': self.mh_shadow_blades_damage, + 'oh_shadow_blades': self.oh_shadow_blades_damage, + 'second_shuriken': self.second_shuriken_damage, + 'shuriken_storm': self.shuriken_storm_damage, + 'shuriken_toss': self.shuriken_toss_damage, + 'soul_rip': self.soul_rip_damage, + 'shadow_nova': self.shadow_nova_damage, } return formulas[name] - def get_spell_stats(self, ability, cost_mod=1.0): + def get_spell_cost(self, ability, cost_mod=1.0): cost = self.ability_info[ability][0] * cost_mod - return (cost, self.ability_info[ability][1]) - + if ability == 'shadowstrike': + cost -= 0.5 * (2 * self.traits.energetic_stabbing) + #Assume 5 yards away so 3 + 5/3 + if self.stats.gear_buffs.shadow_satyrs_walk: + cost -= 4.67 + elif ability == 'backstab': + cost -= 0.5 * (2 * self.traits.energetic_stabbing) + elif ability == 'garrote' and self.stats.gear_buffs.rogue_t20_4pc: + cost -= 25 + return cost + def get_spell_cd(self, ability): - #need to update list of affected abilities - if ability in self.cd_reduction_table[self.settings.get_spec()]: - #self.stats.get_readiness_multiplier_from_rating(readiness_conversion=self.readiness_spec_conversion) - return self.ability_cds[ability] * self.get_trinket_cd_reducer() - else: - return self.ability_cds[ability] + cd = self.ability_cds[ability] + if ability == 'adrenaline_rush': + cd -= self.fortunes_boon_cdr[self.traits.fortunes_boon] + elif ability == 'vendetta': + cd -= self.master_assassin_cdr[self.traits.master_assassin] + elif ability == 'vanish' and self.traits.flickering_shadows: + cd -= 30 + elif self.spec == 'subtlety' and ability == 'marked_for_death': + cd = 40 + elif ability == 'garrote' and self.stats.gear_buffs.rogue_t20_4pc: + cd -= 12 + elif ability == 'symbols_of_death' and self.stats.gear_buffs.rogue_t20_4pc: + cd -= 5 + + #Convergence of Fates Trinket + cof = self.stats.procs.convergence_of_fates + if cof and ability in ['vendetta', 'adrenaline_rush', 'shadow_blades']: + #We want time t in sec when CD is ready. CD goes down by 1 every sec plus value * proc_chance. + #That gives us: 0 = cd - t(1 + value * proc_chance) <=> t = cd / (1 + value * proc_chance) + cd /= 1 + cof.value * cof.get_proc_rate(spec=self.spec) + + return cd def crit_rate(self, crit=None): - # all rogues get 10% bonus crit, .05 of base crit for everyone + # all rogues have 10% base crit # should be coded better? - base_crit = .15 + base_crit = .10 base_crit += self.stats.get_crit_from_rating(crit) - return base_crit + self.buffs.buff_all_crit() + self.race.get_racial_crit(is_day=self.settings.is_day) - self.crit_reduction + return base_crit + self.race.get_racial_crit(is_day=self.settings.is_day) - self.crit_reduction + + def add_special_procs_damage(self, current_stats, attacks_per_second, crit_rates, modifier_dict, damage_breakdown): + ap = current_stats['ap'] + current_stats['agi'] * self.stat_multipliers['ap'] + + # Nightblooming Frond + frond = self.stats.procs.nightblooming_frond + if frond: + autoattacks_per_second = attacks_per_second['mh_autoattacks'] * self.dual_wield_mh_hit_chance() + autoattacks_per_second += attacks_per_second['oh_autoattacks'] * self.dual_wield_oh_hit_chance() + if 'shadow_blades' in attacks_per_second: + autoattacks_per_second += attacks_per_second['shadow_blades'] * 2 #both hands + + # calculate stacks for each second and accumulate bonus damage per proc + stack_list = [] + for second in range(1, frond.duration + 1): + stack_list.append(min(second * autoattacks_per_second, frond.max_stacks)) + stack_damage = self.get_proc_damage_contribution(frond, 1, current_stats, ap, modifier_dict) + proc_damage = 0 + for stack_count in stack_list: + proc_damage += stack_count * stack_damage * autoattacks_per_second + + damage_breakdown[frond.proc_name] = proc_damage * frond.get_proc_rate(spec=self.spec) * 1.1307 #BLP + + # Tiny Oozeling in a Jar + oozeling = self.stats.procs.tiny_oozeling_in_a_jar + if oozeling: + haste = self.get_haste_multiplier(current_stats) + stacks_per_use = min(oozeling.icd * haste * 1.1307 * 3 / 60, 6) #3 rppm, capped at 6 stacks, 1.1307 bad luck protection + damage_per_use = self.get_proc_damage_contribution(oozeling, stacks_per_use, current_stats, ap, modifier_dict) + damage_breakdown[oozeling.proc_name] = damage_per_use / oozeling.icd + + # Potions: Potion of the Old War + # Trinkets: Tirathon's Betrayal and Faulty Countermeasure + for proc in [self.stats.procs.old_war_pot, self.stats.procs.old_war_prepot, + self.stats.procs.tirathons_betrayal, self.stats.procs.faulty_countermeasure]: + if proc: + # all 20 RPPM with haste mod + rppm = 20 + haste_mod = self.get_haste_multiplier(current_stats) if proc.haste_scales else 1 + procs_per_use = proc.duration * rppm * 1.1307 * haste_mod / 60 + damage_per_use = self.get_proc_damage_contribution(proc, procs_per_use, current_stats, ap, modifier_dict) + if proc.proc_name in damage_breakdown: + damage_breakdown[proc.proc_name] += damage_per_use / proc.icd + else: + damage_breakdown[proc.proc_name] = damage_per_use / proc.icd + + # Specter of Betrayal + specter_of_betrayal = self.stats.procs.specter_of_betrayal + if specter_of_betrayal: + num_torrents = (1 + 2 * (self.settings.duration / specter_of_betrayal.icd)) / self.settings.duration + damage_breakdown[specter_of_betrayal.proc_name] = self.get_proc_damage_contribution(specter_of_betrayal, num_torrents, current_stats, ap, modifier_dict) + + # Golganneth's Thunderous Wrath, use pantheon empowerment uptime value + golganneths_vitality_empowered = self.stats.procs.golganneths_vitality_empowered + if golganneths_vitality_empowered: + autoattacks_per_second = attacks_per_second['mh_autoattacks'] * self.dual_wield_mh_hit_chance() + autoattacks_per_second += attacks_per_second['oh_autoattacks'] * self.dual_wield_oh_hit_chance() + if 'shadow_blades' in attacks_per_second: + autoattacks_per_second += attacks_per_second['shadow_blades'] * 2 #both hands + dmg_per_aa = self.stats.mh.speed * self.get_proc_damage_contribution(golganneths_vitality_empowered, 1, current_stats, ap, modifier_dict) + proc_damage = dmg_per_aa * autoattacks_per_second * (1 + self.settings.num_boss_adds) * self.pantheon_empowerment_uptime + damage_breakdown[golganneths_vitality_empowered.proc_name] = proc_damage diff --git a/shadowcraft/core/__init__.py b/shadowcraft/core/__init__.py index 4974700..d133f1c 100644 --- a/shadowcraft/core/__init__.py +++ b/shadowcraft/core/__init__.py @@ -1,4 +1,6 @@ +from future import standard_library +standard_library.install_aliases() import gettext -import __builtin__ +import builtins -__builtin__._ = gettext.gettext +_ = gettext.gettext diff --git a/shadowcraft/core/exceptions.py b/shadowcraft/core/exceptions.py index 6407656..d69af65 100644 --- a/shadowcraft/core/exceptions.py +++ b/shadowcraft/core/exceptions.py @@ -1,3 +1,4 @@ +from builtins import str class InvalidInputException(Exception): # Base class for all our exceptions. All exceptions we generate should # either use or subclass this. diff --git a/shadowcraft/core/i18n.py b/shadowcraft/core/i18n.py index ba60d53..baa7d04 100644 --- a/shadowcraft/core/i18n.py +++ b/shadowcraft/core/i18n.py @@ -1,9 +1,13 @@ +from __future__ import absolute_import +from future import standard_library +standard_library.install_aliases() import gettext import os.path import locale -import __builtin__ +import builtins +import sys -__builtin__._ = gettext.gettext +_ = gettext.gettext # Domain: this needs to be the name of our .mo files TRANSLATION_DOMAIN = 'SCE' @@ -14,6 +18,9 @@ def set_language(language): # language specified. It will fall back to code strings if given a not supported # language. Note that the 'local' value only makes sense when not running from # the hosted online version. + install_args = { } + if sys.api_version < 3: + install_args['str'] = True if language == 'local': # Setting up a list of locales in your machine and asign them to the _() function languages_list = [] @@ -26,7 +33,7 @@ def set_language(language): if (gnu_lang): languages_list += gnu_lang.split(":") - gettext.translation(TRANSLATION_DOMAIN, LOCALE_DIR, fallback=True, languages=languages_list).install(unicode=True) + gettext.translation(TRANSLATION_DOMAIN, LOCALE_DIR, fallback=True, languages=languages_list).install(**install_args) else: - gettext.translation(TRANSLATION_DOMAIN, LOCALE_DIR, fallback=True, languages=[language]).install(unicode=True) + gettext.translation(TRANSLATION_DOMAIN, LOCALE_DIR, fallback=True, languages=[language]).install(**install_args) diff --git a/shadowcraft/core/jsoninput.py b/shadowcraft/core/jsoninput.py index b506c17..e8889b5 100644 --- a/shadowcraft/core/jsoninput.py +++ b/shadowcraft/core/jsoninput.py @@ -1,3 +1,5 @@ +from __future__ import print_function +from builtins import str import json from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator from shadowcraft.calcs.rogue.Aldriana import settings @@ -14,10 +16,10 @@ class InvalidJSONException(exceptions.InvalidInputException): def from_json(json_string, character_class='rogue'): j = json.loads(json_string) - try: + try: race_object = race.Race(str(j['race']), character_class=character_class) level = int(j['level']) - + s = j['settings'] settings_type = s['type'] if settings_type == 'assassination': @@ -28,7 +30,7 @@ def from_json(json_string, character_class='rogue'): elif settings_type == 'combat': # CombatCycle(self, use_rupture=True, use_revealing_strike='sometimes', ksp_immediately=False) c = s.get('cycle', {}) - cycle = settings.CombatCycle(c.get('use_rupture', True), c.get('use_revealing_strike', 'sometimes'), c.get('ksp_immediately', False)) + cycle = settings.OutlawCycle(c.get('use_rupture', True), c.get('use_revealing_strike', 'sometimes'), c.get('ksp_immediately', False)) elif settings_type == 'subtlety': # SubletySycle(raid_crits_per_second, clip_recuperate=False) c = s['cycle'] @@ -39,7 +41,7 @@ def from_json(json_string, character_class='rogue'): # Settings(cycle, time_in_execute_range=.35, tricks_on_cooldown=True, response_time=.5, mh_poison='ip', oh_poison='dp', duration=300): settings_object = settings.Settings(cycle, s.get('time_in_execute_range', .35), s.get('tricks_on_cooldown', True), s.get('response_time', .5), s.get('mh_poison', 'ip'), s.get('oh_poison', 'dp'), s.get('duration', 300)) - + stats_dict = j['stats'] # Weapon(damage, speed, weapon_type, enchant=None): mh_dict = stats_dict['mh'] @@ -53,8 +55,9 @@ def from_json(json_string, character_class='rogue'): # Stats(str, agi, ap, crit, hit, exp, haste, mastery, mh, oh, ranged, procs, gear_buffs, level=85): def s(stat): return int(stats_dict[stat]) - stats_object = stats.Stats(s('str'), s('agi'), s('ap'), s('crit'), s('hit'), s('exp'), s('haste'), s('mastery'), - mh, oh, ranged, procs_list, gear_buffs, level) + stats_object = stats.Stats( + mh, oh, procs_list, gear_buffs, + str=s('str'), agi=s('agi'), crit=s('crit'), haste=s('haste'), mastery=s('mastery')) glyphs = rogue_glyphs.RogueGlyphs(*j['glyphs']) talents = rogue_talents.RogueTalents(*j['talents']) buffs_object = buffs.Buffs(*j['buffs']) @@ -66,74 +69,74 @@ def s(stat): if __name__ == '__main__': json_string = """{ - "level": 85, + "level": 85, "stats": { "str": 20, - "agi": 4756, - "ap": 190, - "crit": 1022, - "hit": 1329, - "exp": 159, - "haste": 1291, - "mastery": 1713, + "agi": 4756, + "ap": 190, + "crit": 1022, + "hit": 1329, + "exp": 159, + "haste": 1291, + "mastery": 1713, "gear_buffs": [ - "rogue_t11_2pc", - "leather_specialization", - "potion_of_the_tolvir", + "rogue_t11_2pc", + "leather_specialization", + "potion_of_the_tolvir", "chaotic_metagem" - ], + ], "procs": [ - "heroic_prestors_talisman_of_machination", - "fluid_death", + "heroic_prestors_talisman_of_machination", + "fluid_death", "rogue_t11_4pc" - ], + ], "mh": { - "type": "dagger", - "speed": 1.8, - "damage": 939.5, + "type": "dagger", + "speed": 1.8, + "damage": 939.5, "enchant": "landslide" }, "oh": { - "type": "dagger", - "speed": 1.4, - "damage": 730.5, + "type": "dagger", + "speed": 1.4, + "damage": 730.5, "enchant": "landslide" - }, + }, "ranged": { - "type": "thrown", - "speed": 2.2, + "type": "thrown", + "speed": 2.2, "damage": 1371.5 } }, "buffs": [ - "short_term_haste_buff", - "stat_multiplier_buff", - "crit_chance_buff", - "all_damage_buff", - "melee_haste_buff", - "attack_power_buff", - "str_and_agi_buff", - "armor_debuff", - "physical_vulnerability_debuff", - "spell_damage_debuff", - "spell_crit_debuff", - "bleed_damage_debuff", - "agi_flask", + "short_term_haste_buff", + "stat_multiplier_buff", + "crit_chance_buff", + "all_damage_buff", + "melee_haste_buff", + "attack_power_buff", + "str_and_agi_buff", + "armor_debuff", + "physical_vulnerability_debuff", + "spell_damage_debuff", + "spell_crit_debuff", + "bleed_damage_debuff", + "agi_flask", "guild_feast" - ], + ], "settings": { - "type": "assassination", + "type": "assassination", "response_time": 1 - }, + }, "talents": [ - "0333230113022110321", - "0020000000000000000", + "0333230113022110321", + "0020000000000000000", "2030030000000000000" - ], - "race": "night_elf", + ], + "race": "night_elf", "glyphs": [ - "backstab", - "mutilate", + "backstab", + "mutilate", "rupture" ] }""" @@ -141,23 +144,23 @@ def s(stat): calculator = from_json(json_string) # Compute EP values. - ep_values = calculator.get_ep().items() + ep_values = list(calculator.get_ep().items()) ep_values.sort(key=lambda entry: entry[1], reverse=True) max_len = max(len(entry[0]) for entry in ep_values) for value in ep_values: - print value[0] + ':' + ' ' * (max_len - len(value[0])), value[1] + print(value[0] + ':' + ' ' * (max_len - len(value[0])), value[1]) - print '---------' + print('---------') # Compute DPS Breakdown. - dps_breakdown = calculator.get_dps_breakdown().items() + dps_breakdown = list(calculator.get_dps_breakdown().items()) dps_breakdown.sort(key=lambda entry: entry[1], reverse=True) max_len = max(len(entry[0]) for entry in dps_breakdown) total_dps = sum(entry[1] for entry in dps_breakdown) for entry in dps_breakdown: - print entry[0] + ':' + ' ' * (max_len - len(entry[0])), entry[1] + print(entry[0] + ':' + ' ' * (max_len - len(entry[0])), entry[1]) - print '-' * (max_len + 15) + print('-' * (max_len + 15)) - print ' ' * (max_len + 1), total_dps, _("total damage per second.") + print(' ' * (max_len + 1), total_dps, _("total damage per second.")) diff --git a/shadowcraft/objects/__init__.py b/shadowcraft/objects/__init__.py index 4974700..d133f1c 100644 --- a/shadowcraft/objects/__init__.py +++ b/shadowcraft/objects/__init__.py @@ -1,4 +1,6 @@ +from future import standard_library +standard_library.install_aliases() import gettext -import __builtin__ +import builtins -__builtin__._ = gettext.gettext +_ = gettext.gettext diff --git a/shadowcraft/objects/artifact.py b/shadowcraft/objects/artifact.py new file mode 100644 index 0000000..d35d086 --- /dev/null +++ b/shadowcraft/objects/artifact.py @@ -0,0 +1,51 @@ +from builtins import range +from builtins import object +from shadowcraft.core import exceptions +from shadowcraft.objects import artifact_data + +class InvalidTraitException(exceptions.InvalidInputException): + pass + +class Artifact(object): + def __init__(self, class_spec, game_class, trait_string='', trait_dict= {}): + self.allowed_traits = artifact_data.traits[(game_class, class_spec)]+artifact_data.traits[('all','netherlight')] + self.single_rank_traits = artifact_data.single_rank[(game_class, class_spec)] + + if trait_string: + self.initialize_traits(trait_string) + + else: + self.traits = {} + for trait in self.allowed_traits: + if trait in trait_dict: + self.traits[trait] = trait_dict[trait] + else: + self.traits[trait] = 0 + + + def __getattr__(self, attr): + if attr in self.traits: + return self.traits[attr] + return False + + def set_trait(self, trait, value): + if trait not in self.allowed_traits: + raise InvalidTraitException(_('Invalid trait name {trait}').format(trait=trait)) + self.traits[trait] = value + + def initialize_traits(self, trait_string): + if len(trait_string) != len(self.allowed_traits) and len(trait_string) != len(self.allowed_traits) + 1: + raise InvalidTraitException(_('Trait strings must be {traits} (or {traits} + 1) characters long').format(traits=len(self.allowed_traits))) + self.traits = {} + for trait in range(len(self.allowed_traits)): + #grab all charcters for final trait + if trait == len(self.allowed_traits) - 1: + self.set_trait(self.allowed_traits[trait], int(trait_string[trait:])) + else: + self.set_trait(self.allowed_traits[trait], int(trait_string[trait])) + + def get_trait_list(self): + return list(self.allowed_traits) + + def get_single_rank_trait_list(self): + return list(self.single_rank_traits) diff --git a/shadowcraft/objects/artifact_data.py b/shadowcraft/objects/artifact_data.py new file mode 100644 index 0000000..4c520a9 --- /dev/null +++ b/shadowcraft/objects/artifact_data.py @@ -0,0 +1,138 @@ +traits = { + ('rogue', 'assassination'): ( + 'kingsbane', + 'assassins_blades', + 'toxic_blades', + 'poison_knives', + 'urge_to_kill', + 'balanced_blades', + 'surge_of_toxins', + 'shadow_walker', + 'master_assassin', + 'shadow_swiftness', + 'serrated_edge', + 'bag_of_tricks', + 'master_alchemist', + 'gushing_wounds', + 'fade_into_shadows', + 'from_the_shadows', + 'blood_of_the_assassinated', + 'slayers_precision', + 'silence_of_the_uncrowned', + 'strangler', + 'dense_concoction', + 'sinister_circulation', + 'concordance_of_the_legionfall', + ), + ('rogue', 'outlaw'): ( + 'curse_of_the_dreadblades', + 'cursed_edges', + 'fates_thirst', + 'blade_dancer', + 'fatebringer', + 'gunslinger', + 'hidden_blade', + 'fortune_strikes', + 'ghostly_shell', + 'deception', + 'black_powder', + 'greed', + 'blurred_time', + 'fortunes_boon', + 'fortunes_strike', + 'blademaster', + 'blunderbuss', + 'cursed_steel', + 'bravado_of_the_uncrowned', + 'sabermetrics', + 'dreadblades_vigor', + 'loaded_dice', + 'concordance_of_the_legionfall', + ), + ('rogue', 'subtlety'): ( + 'goremaws_bite', + 'shadow_fangs', + 'gutripper', + 'fortunes_bite', + 'catlike_reflexes', + 'embrace_of_darkness', + 'ghost_armor', + 'precision_strike', + 'energetic_stabbing', + 'flickering_shadows', + 'second_shuriken', + 'demons_kiss', + 'finality', + 'the_quiet_knife', + 'akarris_soul', + 'soul_shadows', + 'shadow_nova', + 'legionblade', + 'shadows_of_the_uncrowned', + 'weak_point', + 'shadows_whisper', + 'feeding_frenzy', + 'concordance_of_the_legionfall', + ), + ('all','netherlight'): ( + 'chaotic_darkness', + 'dark_sorrows', + 'infusion_of_light', + 'light_speed', + 'lights_embrace', + 'master_of_shadows', + 'murderous_intent', + 'refractive_shell', + 'secure_in_the_light', + 'shadowbind', + 'shocklight', + 'torment_the_weak', + ) +} + +#Single Rank Traits for each spec +#Used for binary trait ranking +single_rank = { + ('rogue', 'assassination'): ( + 'kingsbane', + 'assassins_blades', + 'urge_to_kill', + 'surge_of_toxins', + 'shadow_swiftness', + 'bag_of_tricks', + 'from_the_shadows', + 'blood_of_the_assassinated', + 'slayers_precision', + 'silence_of_the_uncrowned', + 'dense_concoction', + 'sinister_circulation', + ), + ('rogue', 'outlaw'): ( + 'curse_of_the_dreadblades', + 'cursed_edges', + 'hidden_blade', + 'deception', + 'greed', + 'blurred_time', + 'blademaster', + 'blunderbuss', + 'cursed_steel', + 'bravado_of_the_uncrowned', + 'dreadblades_vigor', + 'loaded_dice', + ), + ('rogue', 'subtlety'): ( + 'goremaws_bite', + 'shadow_fangs', + 'embrace_of_darkness', + 'flickering_shadows', + 'second_shuriken', + 'finality', + 'akarris_soul', + 'shadow_nova', + 'legionblade', + 'shadows_of_the_uncrowned', + 'shadows_whisper', + 'feeding_frenzy', + ), +} diff --git a/shadowcraft/objects/buffs.py b/shadowcraft/objects/buffs.py index cfe5ed5..ba0ea81 100644 --- a/shadowcraft/objects/buffs.py +++ b/shadowcraft/objects/buffs.py @@ -1,30 +1,31 @@ +from builtins import object from shadowcraft.core import exceptions +import gettext +_ = gettext.gettext + class InvalidBuffException(exceptions.InvalidInputException): pass - class Buffs(object): allowed_buffs = frozenset([ 'short_term_haste_buff', # Heroism/Blood Lust, Time Warp - 'stat_multiplier_buff', # Mark of the Wild, Blessing of Kings, Legacy of the Emperor - 'crit_chance_buff', # Leader of the Pack, Legacy of the White Tiger, Arcane Brillance - 'haste_buff', # Swiftblade's Cunning, Unholy Aura - 'multistrike_buff', # Swiftblade's Cunning, ... - 'attack_power_buff', # Horn of Winter, Trueshot Aura, Battle Shout - 'mastery_buff', # Blessing of Might, Grace of Air - 'stamina_buff', # PW: Fortitude, Blood Pact, Commanding Shout - 'versatility_buff', # - 'spell_power_buff', # Dark Intent, Arcane Brillance + #'stat_multiplier_buff', # Mark of the Wild, Blessing of Kings, Legacy of the Emperor + #'crit_chance_buff', # Leader of the Pack, Legacy of the White Tiger, Arcane Brillance + #'haste_buff', # Swiftblade's Cunning, Unholy Aura + #'multistrike_buff', # Swiftblade's Cunning, ... + #'attack_power_buff', # Horn of Winter, Trueshot Aura, Battle Shout + #'mastery_buff', # Blessing of Might, Grace of Air + #'stamina_buff', # PW: Fortitude, Blood Pact, Commanding Shout + #'versatility_buff', # + #'spell_power_buff', # Dark Intent, Arcane Brillance #'armor_debuff', # Sunder, Expose Armor, Faerie Fire - 'physical_vulnerability_debuff', # Brittle Bones, Ebon Plaguebringer, Judgments of the Bold, Colossus Smash - 'spell_damage_debuff', # Master Poisoner, Curse of Elements + #'physical_vulnerability_debuff', # Brittle Bones, Ebon Plaguebringer, Judgments of the Bold, Colossus Smash + #'spell_damage_debuff', # Master Poisoner, Curse of Elements #'slow_casting_debuff', 'mortal_wounds_debuff', # consumables - 'agi_flask_mop', # Flask of Spring Blossoms - 'food_mop_agi', # Sea Mist Rice Noodles 'flask_wod_agi_200', # 'flask_wod_agi', # 250 'food_wod_mastery', # 100 @@ -39,35 +40,49 @@ class Buffs(object): 'food_wod_versatility', # 'food_wod_versatility_75', # 'food_wod_versatility_125', # - 'food_wod_multistrike', # - 'food_wod_multistrike_75', # - 'food_wod_multistrike_125', # + 'food_felmouth_frenzy', # Felmouth frenzy, 2 haste scaling RPPM dealing 0.424 AP in damage + ###LEGION### + 'flask_legion_agi', # Flask of the Seventh Demon + 'food_legion_mastery_225', # Pickeled Stormray + 'food_legion_crit_225', # Salt & Pepper Shank + 'food_legion_haste_225', # Deep-Fried Mossgill + 'food_legion_versatility_225', # Faronaar Fizz + 'food_legion_mastery_300', # Barracude Mrglgagh + 'food_legion_crit_300', # Leybeque Ribs + 'food_legion_haste_300', # Suramar Surf and Turf + 'food_legion_versatility_300', # Koi-Scented Stormray + 'food_legion_mastery_375', # Nightborne Delicacy Platter + 'food_legion_crit_375', # The Hungry Magister + 'food_legion_haste_375', # Azshari Salad + 'food_legion_versatility_375', # Seed-Battered Fish Plate + 'food_legion_damage_1', # Spiced Rib Roast + 'food_legion_damage_2', # Drogbar-Style Salmon + 'food_legion_damage_3', # Fishbrul Special + 'food_legion_feast_400', + 'food_legion_feast_500', ]) - + buffs_debuffs = frozenset([ 'short_term_haste_buff', # Heroism/Blood Lust, Time Warp - 'stat_multiplier_buff', # Mark of the Wild, Blessing of Kings, Legacy of the Emperor - 'crit_chance_buff', # Leader of the Pack, Legacy of the White Tiger, Arcane Brillance - 'haste_buff', # Swiftblade's Cunning, Unholy Aura - 'multistrike_buff', # Swiftblade's Cunning, ... - 'attack_power_buff', # Horn of Winter, Trueshot Aura, Battle Shout - 'mastery_buff', # Blessing of Might, Grace of Air - 'spell_power_buff', # Dark Intent, Arcane Brillance - 'versatility_buff', - 'stamina_buff', # PW: Fortitude, Blood Pact, Commanding Shout - 'physical_vulnerability_debuff', # Brittle Bones, Ebon Plaguebringer, Judgments of the Bold, Colossus Smash - 'spell_damage_debuff', # Master Poisoner, Curse of Elements + #'stat_multiplier_buff', # Mark of the Wild, Blessing of Kings, Legacy of the Emperor + #'crit_chance_buff', # Leader of the Pack, Legacy of the White Tiger, Arcane Brillance + #'haste_buff', # Swiftblade's Cunning, Unholy Aura + #'multistrike_buff', # Swiftblade's Cunning, ... + #'attack_power_buff', # Horn of Winter, Trueshot Aura, Battle Shout + #'mastery_buff', # Blessing of Might, Grace of Air + #'spell_power_buff', # Dark Intent, Arcane Brillance + #'versatility_buff', + #'stamina_buff', # PW: Fortitude, Blood Pact, Commanding Shout + #'physical_vulnerability_debuff', # Brittle Bones, Ebon Plaguebringer, Judgments of the Bold, Colossus Smash + #'spell_damage_debuff', # Master Poisoner, Curse of Elements 'mortal_wounds_debuff', ]) - buff_scaling = {80: 91, 85: 122, 90: 141, 100: 550} - def __init__(self, *args, **kwargs): for buff in args: if buff not in self.allowed_buffs: raise InvalidBuffException(_('Invalid buff {buff}').format(buff=buff)) setattr(self, buff, True) - self.level = kwargs.get('level', 100) def __getattr__(self, name): # Any buff we haven't assigned a value to, we don't have. @@ -77,92 +92,76 @@ def __getattr__(self, name): def __setattr__(self, name, value): object.__setattr__(self, name, value) - if name == 'level': - self._set_constants_for_level() - - def _set_constants_for_level(self): - try: - self.mast_buff_bonus = self.buff_scaling[self.level] - except KeyError as e: - raise exceptions.InvalidLevelException(_('No conversion factor available for level {level}').format(level=self.level)) - - def get_max_buffs(self): - return frozenset(buffs_debuffs + ['food_wod_multistrike_125', 'flask_wod_agi']) - - def stat_multiplier(self): - return [1, 1.05][self.stat_multiplier_buff] - - def spell_damage_multiplier(self): - return [1, 1.08][self.spell_damage_debuff] - - def physical_damage_multiplier(self): - return [1, 1.05][self.physical_vulnerability_debuff] - - def bleed_damage_multiplier(self): - return self.physical_damage_multiplier() - - def attack_power_multiplier(self): - return [1, 1.1][self.attack_power_buff] - - def haste_multiplier(self): - return [1, 1.05][self.haste_buff] - - def versatility_bonus(self): - return [0, 0.03][self.versatility_buff] - - def buff_all_crit(self): - return [0, .05][self.crit_chance_buff] - - def multistrike_bonus(self): - return [0, 0.05][self.multistrike_buff] - - #stat buffs - def buff_str(self, race=False): - return 0 + + def get_stat_bonuses(self, epicurean=False): + bonuses = { + 'agi': self.buff_agi(epicurean), + 'crit': self.buff_crit(epicurean), + 'haste': self.buff_haste(epicurean), + 'mastery': self.buff_mast(epicurean), + 'versatility': self.buff_versatility(epicurean), + } + return bonuses def buff_agi(self, race=False): bonus_agi = 0 - bonus_agi += 114 * self.agi_flask_mop bonus_agi += 200 * self.flask_wod_agi_200 bonus_agi += 250 * self.flask_wod_agi - bonus_agi += 34 * self.food_mop_agi * [1, 2][race] + bonus_agi += 1300 * self.flask_legion_agi + bonus_agi += 400 * self.food_legion_feast_400 * [1, 2][race] + bonus_agi += 500 * self.food_legion_feast_500 * [1, 2][race] return bonus_agi - + def buff_haste(self, race=False): bonus_haste = 0 bonus_haste += 125 * self.food_wod_haste_125 * [1, 2][race] bonus_haste += 100 * self.food_wod_haste * [1, 2][race] bonus_haste += 75 * self.food_wod_haste_75 * [1, 2][race] + bonus_haste += 225 * self.food_legion_haste_225 * [1, 2][race] + bonus_haste += 300 * self.food_legion_haste_300 * [1, 2][race] + bonus_haste += 375 * self.food_legion_haste_375 * [1, 2][race] return bonus_haste - + def buff_crit(self, race=False): bonus_crit = 0 bonus_crit += 125 * self.food_wod_crit_125 * [1, 2][race] bonus_crit += 100 * self.food_wod_crit * [1, 2][race] bonus_crit += 75 * self.food_wod_crit_75 * [1, 2][race] + bonus_crit += 225 * self.food_legion_crit_225 * [1, 2][race] + bonus_crit += 300 * self.food_legion_crit_300 * [1, 2][race] + bonus_crit += 375 * self.food_legion_crit_375 * [1, 2][race] return bonus_crit - + def buff_mast(self, race=False): bonus_mastery = 0 - bonus_mastery += [0, self.mast_buff_bonus][self.mastery_buff] bonus_mastery += 125 * self.food_wod_mastery_125 * [1, 2][race] bonus_mastery += 100 * self.food_wod_mastery * [1, 2][race] bonus_mastery += 75 * self.food_wod_mastery_75 * [1, 2][race] + bonus_mastery += 225 * self.food_legion_mastery_225 * [1, 2][race] + bonus_mastery += 300 * self.food_legion_mastery_300 * [1, 2][race] + bonus_mastery += 375 * self.food_legion_mastery_375 * [1, 2][race] return bonus_mastery - + def buff_versatility(self, race=False): bonus_versatility = 0 bonus_versatility += 125 * self.food_wod_versatility_125 * [1, 2][race] bonus_versatility += 100 * self.food_wod_versatility * [1, 2][race] bonus_versatility += 75 * self.food_wod_versatility_75 * [1, 2][race] + bonus_versatility += 225 * self.food_legion_versatility_225 * [1, 2][race] + bonus_versatility += 300 * self.food_legion_versatility_300 * [1, 2][race] + bonus_versatility += 375 * self.food_legion_versatility_375 * [1, 2][race] return bonus_versatility - - def buff_multistrike(self, race=False): - bonus_multistrike = 0 - bonus_multistrike += 125 * self.food_wod_multistrike_125 * [1, 2][race] - bonus_multistrike += 100 * self.food_wod_multistrike * [1, 2][race] - bonus_multistrike += 75 * self.food_wod_multistrike_75 * [1, 2][race] - return bonus_multistrike - - def buff_readiness(self, race=False): + + def felmouth_food(self): + if self.food_felmouth_frenzy : + return True + return False + + def damage_food(self): + if self.food_legion_damage_1: + return 1 + if self.food_legion_damage_2: + return 2 + if self.food_legion_damage_3: + return 3 return 0 diff --git a/shadowcraft/objects/class_data.py b/shadowcraft/objects/class_data.py index c78f7e0..8e757e2 100644 --- a/shadowcraft/objects/class_data.py +++ b/shadowcraft/objects/class_data.py @@ -1,3 +1,5 @@ +from builtins import str +from builtins import object from shadowcraft.core import exceptions class Util(object): @@ -6,9 +8,9 @@ class Util(object): GAME_CLASS_NUMBER = { 1:"warrior", 2:"paladin", 3:"hunter", 4:"rogue", 5:"priest", 6:"death_knight", 7:"shaman", 8:"mage", 9:"warlock", - 10:"monk", 11:"druid" + 10:"monk", 11:"druid", 12:"demon_hunter" } - + #constant scaling (for level based calculations and the like) CONSTANT_SCALING = [ 3.000000000000000, 3.000000000000000, 4.000000000000000, 4.000000000000000, 5.000000000000000, @@ -31,1016 +33,1319 @@ class Util(object): 60.000000000000000, 63.000000000000000, 65.000000000000000, 66.000000000000000, 67.000000000000000, 101.000000000000000, 118.000000000000000, 139.000000000000000, 162.000000000000000, 190.000000000000000, 225.000000000000000, 234.000000000000000, 242.000000000000000, 252.000000000000000, 261.000000000000000, + 261.000000000000000, 261.000000000000000, 261.000000000000000, 261.000000000000000, 261.000000000000000, + 261.000000000000000, 261.000000000000000, 261.000000000000000, 261.000000000000000, 261.000000000000000, ] - #SimC keeps their enchant scale data here: https://code.google.com/p/simulationcraft/source/browse/engine/dbc/sc_scale_data.inc#342 - #these, however, are the trinket scale data + #Base armor values for enemy level + #Source: http://blue.mmo-champion.com/topic/409203-theorycrafting-questions/#post109 + BASE_ARMOR = {100: 1536., + 101: 2313., + 102: 2388., + 103: 2467., + 104: 2550., + 105: 2638., + 106: 2729., + 107: 2826., + 108: 2927., + 109: 3035., + 110: 3144., + 111: 3254., + 112: 3364., + 113: 3474., + } + + #K value for armor mitigation computation + #Source: http://blue.mmo-champion.com/topic/409203-theorycrafting-questions/#post46 + ATTACKER_K_VALUE = {100: 3610., + 101: 3973., + 102: 4373., + 103: 4812., + 104: 5296., + 105: 5829., + 106: 6415., + 107: 6642., + 108: 6880., + 109: 7132., + 110: 7390., + 111: 7648., + 112: 7906., + 113: 8164., + } + + #SimC keeps random prop points in engine/dbc/generated/sc_item_data2.inc + #Trinket random property points for item levels 1-1000, wow build 23360, updated 2017-02-24 RANDOM_PROP_POINTS = [ - [0,0], - [1,0], - [2,0], - [3,0], - [4,0], - [5,0], - [6,0], - [7,0], - [8,0], - [9,0], - [10,9], - [11,10], - [12,10], - [13,11], - [14,11], - [15,12], - [16,12], - [17,13], - [18,13], - [19,14], - [20,14], - [21,15], - [22,15], - [23,16], - [24,16], - [25,17], - [26,17], - [27,18], - [28,18], - [29,19], - [30,19], - [31,20], - [32,20], - [33,21], - [34,21], - [35,22], - [36,22], - [37,23], - [38,23], - [39,24], - [40,24], - [41,25], - [42,25], - [43,26], - [44,26], - [45,27], - [46,27], - [47,28], - [48,28], - [49,29], - [50,29], - [51,30], - [52,30], - [53,31], - [54,31], - [55,32], - [56,32], - [57,33], - [58,33], - [59,34], - [60,34], - [61,35], - [62,35], - [63,36], - [64,36], - [65,37], - [66,37], - [67,37], - [68,37], - [69,37], - [70,37], - [71,37], - [72,37], - [73,37], - [74,37], - [75,37], - [76,37], - [77,37], - [78,37], - [79,37], - [80,37], - [81,37], - [82,37], - [83,37], - [84,37], - [85,37], - [86,37], - [87,37], - [88,37], - [89,37], - [90,37], - [91,37], - [92,37], - [93,37], - [94,38], - [95,38], - [96,39], - [97,39], - [98,40], - [99,40], - [100,40], - [101,41], - [102,41], - [103,42], - [104,42], - [105,43], - [106,43], - [107,44], - [108,44], - [109,44], - [110,45], - [111,45], - [112,46], - [113,46], - [114,47], - [115,47], - [116,47], - [117,47], - [118,47], - [119,47], - [120,47], - [121,47], - [122,47], - [123,47], - [124,47], - [125,47], - [126,47], - [127,47], - [128,47], - [129,47], - [130,47], - [131,47], - [132,47], - [133,47], - [134,47], - [135,47], - [136,47], - [137,47], - [138,47], - [139,47], - [140,47], - [141,47], - [142,47], - [143,47], - [144,47], - [145,47], - [146,47], - [147,47], - [148,47], - [149,47], - [150,47], - [151,47], - [152,47], - [153,47], - [154,47], - [155,47], - [156,47], - [157,47], - [158,47], - [159,47], - [160,47], - [161,47], - [162,47], - [163,47], - [164,47], - [165,47], - [166,48], - [167,48], - [168,48], - [169,48], - [170,48], - [171,48], - [172,48], - [173,49], - [174,49], - [175,49], - [176,49], - [177,49], - [178,49], - [179,49], - [180,50], - [181,50], - [182,50], - [183,50], - [184,50], - [185,50], - [186,50], - [187,51], - [188,51], - [189,51], - [190,51], - [191,51], - [192,51], - [193,51], - [194,52], - [195,52], - [196,52], - [197,52], - [198,52], - [199,52], - [200,52], - [201,53], - [202,53], - [203,53], - [204,53], - [205,53], - [206,53], - [207,54], - [208,54], - [209,54], - [210,54], - [211,54], - [212,54], - [213,55], - [214,55], - [215,55], - [216,55], - [217,55], - [218,55], - [219,55], - [220,55], - [221,55], - [222,55], - [223,55], - [224,55], - [225,55], - [226,55], - [227,55], - [228,55], - [229,55], - [230,55], - [231,55], - [232,55], - [233,55], - [234,55], - [235,55], - [236,55], - [237,55], - [238,55], - [239,55], - [240,55], - [241,55], - [242,55], - [243,55], - [244,55], - [245,55], - [246,55], - [247,55], - [248,55], - [249,55], - [250,55], - [251,55], - [252,55], - [253,55], - [254,55], - [255,55], - [256,55], - [257,55], - [258,55], - [259,55], - [260,55], - [261,55], - [262,55], - [263,55], - [264,55], - [265,55], - [266,55], - [267,55], - [268,55], - [269,55], - [270,55], - [271,55], - [272,55], - [273,55], - [274,55], - [275,55], - [276,55], - [277,55], - [278,55], - [279,55], - [280,55], - [281,55], - [282,55], - [283,55], - [284,55], - [285,55], - [286,55], - [287,55], - [288,55], - [289,55], - [290,55], - [291,56], - [292,56], - [293,56], - [294,56], - [295,56], - [296,56], - [297,56], - [298,56], - [299,56], - [300,56], - [301,56], - [302,56], - [303,56], - [304,56], - [305,57], - [306,57], - [307,57], - [308,57], - [309,57], - [310,57], - [311,57], - [312,57], - [313,57], - [314,57], - [315,57], - [316,57], - [317,57], - [318,57], - [319,58], - [320,58], - [321,58], - [322,58], - [323,58], - [324,58], - [325,58], - [326,58], - [327,58], - [328,58], - [329,58], - [330,58], - [331,58], - [332,59], - [333,59], - [334,59], - [335,59], - [336,59], - [337,59], - [338,59], - [339,59], - [340,59], - [341,59], - [342,59], - [343,59], - [344,59], - [345,59], - [346,60], - [347,60], - [348,60], - [349,60], - [350,60], - [351,60], - [352,60], - [353,60], - [354,60], - [355,60], - [356,60], - [357,60], - [358,60], - [359,61], - [360,61], - [361,61], - [362,61], - [363,61], - [364,61], - [365,61], - [366,61], - [367,61], - [368,61], - [369,61], - [370,61], - [371,61], - [372,61], - [373,61], - [374,61], - [375,61], - [376,61], - [377,61], - [378,61], - [379,61], - [380,61], - [381,61], - [382,61], - [383,61], - [384,61], - [385,61], - [386,61], - [387,61], - [388,61], - [389,61], - [390,61], - [391,61], - [392,61], - [393,61], - [394,61], - [395,61], - [396,61], - [397,61], - [398,61], - [399,61], - [400,61], - [401,61], - [402,61], - [403,61], - [404,61], - [405,61], - [406,61], - [407,61], - [408,61], - [409,61], - [410,61], - [411,61], - [412,61], - [413,61], - [414,61], - [415,61], - [416,61], - [417,61], - [418,62], - [419,62], - [420,62], - [421,62], - [422,62], - [423,62], - [424,62], - [425,62], - [426,62], - [427,62], - [428,62], - [429,62], - [430,62], - [431,62], - [432,62], - [433,62], - [434,62], - [435,63], - [436,63], - [437,63], - [438,63], - [439,63], - [440,63], - [441,63], - [442,63], - [443,63], - [444,64], - [445,64], - [446,64], - [447,64], - [448,64], - [449,64], - [450,65], - [451,65], - [452,65], - [453,65], - [454,65], - [455,65], - [456,65], - [457,66], - [458,66], - [459,66], - [460,66], - [461,66], - [462,66], - [463,67], - [464,68], - [465,68], - [466,69], - [467,70], - [468,70], - [469,71], - [470,72], - [471,72], - [472,73], - [473,74], - [474,74], - [475,75], - [476,76], - [477,76], - [478,77], - [479,78], - [480,78], - [481,79], - [482,80], - [483,81], - [484,81], - [485,82], - [486,83], - [487,84], - [488,85], - [489,85], - [490,86], - [491,87], - [492,88], - [493,89], - [494,89], - [495,90], - [496,91], - [497,92], - [498,93], - [499,94], - [500,95], - [501,95], - [502,96], - [503,97], - [504,98], - [505,99], - [506,100], - [507,101], - [508,102], - [509,103], - [510,104], - [511,105], - [512,106], - [513,107], - [514,108], - [515,109], - [516,110], - [517,111], - [518,112], - [519,113], - [520,114], - [521,115], - [522,116], - [523,117], - [524,118], - [525,119], - [526,121], - [527,122], - [528,123], - [529,124], - [530,125], - [531,126], - [532,127], - [533,129], - [534,130], - [535,131], - [536,132], - [537,134], - [538,135], - [539,136], - [540,137], - [541,139], - [542,140], - [543,141], - [544,143], - [545,144], - [546,145], - [547,147], - [548,148], - [549,149], - [550,151], - [551,152], - [552,154], - [553,155], - [554,156], - [555,158], - [556,159], - [557,161], - [558,162], - [559,164], - [560,165], - [561,167], - [562,169], - [563,170], - [564,172], - [565,173], - [566,175], - [567,177], - [568,178], - [569,180], - [570,182], - [571,183], - [572,185], - [573,187], - [574,188], - [575,190], - [576,192], - [577,194], - [578,196], - [579,197], - [580,199], - [581,201], - [582,203], - [583,205], - [584,207], - [585,209], - [586,211], - [587,213], - [588,215], - [589,217], - [590,219], - [591,221], - [592,223], - [593,225], - [594,227], - [595,229], - [596,231], - [597,234], - [598,236], - [599,238], - [600,240], - [601,242], - [602,245], - [603,247], - [604,249], - [605,252], - [606,254], - [607,256], - [608,259], - [609,261], - [610,264], - [611,266], - [612,269], - [613,271], - [614,274], - [615,276], - [616,279], - [617,281], - [618,284], - [619,287], - [620,289], - [621,292], - [622,295], - [623,298], - [624,300], - [625,303], - [626,306], - [627,309], - [628,312], - [629,315], - [630,318], - [631,321], - [632,324], - [633,327], - [634,330], - [635,333], - [636,336], - [637,339], - [638,342], - [639,345], - [640,349], - [641,352], - [642,355], - [643,358], - [644,362], - [645,365], - [646,369], - [647,372], - [648,376], - [649,379], - [650,383], - [651,386], - [652,390], - [653,393], - [654,397], - [655,401], - [656,405], - [657,408], - [658,412], - [659,416], - [660,420], - [661,424], - [662,428], - [663,432], - [664,436], - [665,440], - [666,444], - [667,448], - [668,452], - [669,457], - [670,461], - [671,465], - [672,470], - [673,474], - [674,479], - [675,483], - [676,488], - [677,492], - [678,497], - [679,501], - [680,506], - [681,511], - [682,516], - [683,520], - [684,525], - [685,530], - [686,535], - [687,540], - [688,545], - [689,550], - [690,555], - [691,561], - [692,566], - [693,571], - [694,577], - [695,582], - [696,587], - [697,593], - [698,598], - [699,604], - [700,610], - [701,615], - [702,621], - [703,627], - [704,633], - [705,639], - [706,645], - [707,651], - [708,657], - [709,663], - [710,669], - [711,675], - [712,682], - [713,688], - [714,695], - [715,701], - [716,708], - [717,714], - [718,721], - [719,728], - [720,735], - [721,741], - [722,748], - [723,755], - [724,762], - [725,770], - [726,777], - [727,784], - [728,791], - [729,799], - [730,806], - [731,814], - [732,821], - [733,829], - [734,837], - [735,845], - [736,853], - [737,861], - [738,869], - [739,877], - [740,885], - [741,893], - [742,902], - [743,910], - [744,919], - [745,927], - [746,936], - [747,945], - [748,954], - [749,962], - [750,971], - [751,981], - [752,990], - [753,999], - [754,1008], - [755,1018], - [756,1027], - [757,1037], - [758,1047], - [759,1056], - [760,1066], - [761,1076], - [762,1086], - [763,1097], - [764,1107], - [765,1117], - [766,1128], - [767,1138], - [768,1149], - [769,1160], - [770,1170], - [771,1181], - [772,1192], - [773,1204], - [774,1215], - [775,1226], - [776,1238], - [777,1249], - [778,1261], - [779,1273], - [780,1285], - [781,1297], - [782,1309], - [783,1321], - [784,1334], - [785,1346], - [786,1359], - [787,1371], - [788,1384], - [789,1397], - [790,1410], - [791,1423], - [792,1437], - [793,1450], - [794,1464], - [795,1477], - [796,1491], - [797,1505], - [798,1519], - [799,1534], - [800,1548], - [801,1562], - [802,1577], - [803,1592], - [804,1607], - [805,1622], - [806,1637], - [807,1652], - [808,1668], - [809,1683], - [810,1699], - [811,1715], - [812,1731], - [813,1747], - [814,1764], - [815,1780], - [816,1797], - [817,1814], - [818,1831], - [819,1848], - [820,1865], - [821,1882], - [822,1900], - [823,1918], - [824,1936], - [825,1954], - [826,1972], - [827,1991], - [828,2009], - [829,2028], - [830,2047], - [831,2066], - [832,2086], - [833,2105], - [834,2125], - [835,2145], - [836,2165], - [837,2185], - [838,2206], - [839,2226], - [840,2247], - [841,2268], - [842,2289], - [843,2311], - [844,2332], - [845,2354], - [846,2376], - [847,2398], - [848,2421], - [849,2444], - [850,2466], - [851,2490], - [852,2513], - [853,2536], - [854,2560], - [855,2584], - [856,2608], - [857,2633], - [858,2657], - [859,2682], - [860,2707], - [861,2733], - [862,2758], - [863,2784], - [864,2810], - [865,2836], - [866,2863], - [867,2890], - [868,2917], - [869,2944], - [870,2972], - [871,3000], - [872,3028], - [873,3056], - [874,3085], - [875,3113], - [876,3143], - [877,3172], - [878,3202], - [879,3232], - [880,3262], - [881,3292], - [882,3323], - [883,3354], - [884,3386], - [885,3417], - [886,3449], - [887,3482], - [888,3514], - [889,3547], - [890,3580], - [891,3614], - [892,3648], - [893,3682], - [894,3716], - [895,3751], - [896,3786], - [897,3822], - [898,3858], - [899,3894], - [900,3930], - [901,3967], - [902,4004], - [903,4042], - [904,4079], - [905,4118], - [906,4156], - [907,4195], - [908,4234], - [909,4274], - [910,4314], - [911,4354], - [912,4395], - [913,4436], - [914,4478], - [915,4520], - [916,4562], - [917,4605], - [918,4648], - [919,4691], - [920,4735], - [921,4779], - [922,4824], - [923,4869], - [924,4915], - [925,4961], - [926,5007], - [927,5054], - [928,5102], - [929,5149], - [930,5198], - [931,5246], - [932,5295], - [933,5345], - [934,5395], - [935,5445], - [936,5496], - [937,5548], - [938,5600], - [939,5652], - [940,5705], - [941,5759], - [942,5812], - [943,5867], - [944,5922], - [945,5977], - [946,6033], - [947,6090], - [948,6147], - [949,6204], - [950,6262], - [951,6321], - [952,6380], - [953,6440], - [954,6500], - [955,6561], - [956,6622], - [957,6684], - [958,6747], - [959,6810], - [960,6874], - [961,6938], - [962,7003], - [963,7069], - [964,7135], - [965,7202], - [966,7269], - [967,7337], - [968,7406], - [969,7475], - [970,7545], - [971,7616], - [972,7687], - [973,7759], - [974,7832], - [975,7905], - [976,7979], - [977,8054], - [978,8129], - [979,8205], - [980,8282], - [981,8359], - [982,8438], - [983,8517], - [984,8596], - [985,8677], - [986,8758], - [987,8840], - [988,8923], - [989,9006], - [990,9091], - [991,9176], - [992,9262], - [993,9348], - [994,9436], - [995,9524], - [996,9613], - [997,9703], - [998,9794], - [999,9886], - [1000,9978], + [0,0], + [1,0], + [2,0], + [3,0], + [4,0], + [5,0], + [6,0], + [7,0], + [8,0], + [9,0], + [10,9], + [11,10], + [12,10], + [13,11], + [14,11], + [15,12], + [16,12], + [17,13], + [18,13], + [19,14], + [20,14], + [21,15], + [22,15], + [23,16], + [24,16], + [25,17], + [26,17], + [27,18], + [28,18], + [29,19], + [30,19], + [31,20], + [32,20], + [33,21], + [34,21], + [35,22], + [36,22], + [37,23], + [38,23], + [39,24], + [40,24], + [41,25], + [42,25], + [43,26], + [44,26], + [45,27], + [46,27], + [47,28], + [48,28], + [49,29], + [50,29], + [51,30], + [52,30], + [53,31], + [54,31], + [55,32], + [56,32], + [57,33], + [58,33], + [59,34], + [60,34], + [61,35], + [62,35], + [63,36], + [64,36], + [65,37], + [66,37], + [67,37], + [68,37], + [69,37], + [70,37], + [71,37], + [72,37], + [73,37], + [74,37], + [75,37], + [76,37], + [77,37], + [78,37], + [79,37], + [80,37], + [81,37], + [82,37], + [83,37], + [84,37], + [85,37], + [86,37], + [87,37], + [88,37], + [89,37], + [90,37], + [91,37], + [92,37], + [93,37], + [94,38], + [95,38], + [96,39], + [97,39], + [98,40], + [99,40], + [100,40], + [101,41], + [102,41], + [103,42], + [104,42], + [105,43], + [106,43], + [107,44], + [108,44], + [109,44], + [110,45], + [111,45], + [112,46], + [113,46], + [114,47], + [115,47], + [116,47], + [117,47], + [118,47], + [119,47], + [120,47], + [121,47], + [122,47], + [123,47], + [124,47], + [125,47], + [126,47], + [127,47], + [128,47], + [129,47], + [130,47], + [131,47], + [132,47], + [133,47], + [134,47], + [135,47], + [136,47], + [137,47], + [138,47], + [139,47], + [140,47], + [141,47], + [142,47], + [143,47], + [144,47], + [145,47], + [146,47], + [147,47], + [148,47], + [149,47], + [150,47], + [151,47], + [152,47], + [153,47], + [154,47], + [155,47], + [156,47], + [157,47], + [158,47], + [159,47], + [160,47], + [161,47], + [162,47], + [163,47], + [164,47], + [165,47], + [166,48], + [167,48], + [168,48], + [169,48], + [170,48], + [171,48], + [172,48], + [173,49], + [174,49], + [175,49], + [176,49], + [177,49], + [178,49], + [179,49], + [180,50], + [181,50], + [182,50], + [183,50], + [184,50], + [185,50], + [186,50], + [187,51], + [188,51], + [189,51], + [190,51], + [191,51], + [192,51], + [193,51], + [194,52], + [195,52], + [196,52], + [197,52], + [198,52], + [199,52], + [200,52], + [201,53], + [202,53], + [203,53], + [204,53], + [205,53], + [206,53], + [207,54], + [208,54], + [209,54], + [210,54], + [211,54], + [212,54], + [213,55], + [214,55], + [215,55], + [216,55], + [217,55], + [218,55], + [219,55], + [220,55], + [221,55], + [222,55], + [223,55], + [224,55], + [225,55], + [226,55], + [227,55], + [228,55], + [229,55], + [230,55], + [231,55], + [232,55], + [233,55], + [234,55], + [235,55], + [236,55], + [237,55], + [238,55], + [239,55], + [240,55], + [241,55], + [242,55], + [243,55], + [244,55], + [245,55], + [246,55], + [247,55], + [248,55], + [249,55], + [250,55], + [251,55], + [252,55], + [253,55], + [254,55], + [255,55], + [256,55], + [257,55], + [258,55], + [259,55], + [260,55], + [261,55], + [262,55], + [263,55], + [264,55], + [265,55], + [266,55], + [267,55], + [268,55], + [269,55], + [270,55], + [271,55], + [272,55], + [273,55], + [274,55], + [275,55], + [276,55], + [277,55], + [278,55], + [279,55], + [280,55], + [281,55], + [282,55], + [283,55], + [284,55], + [285,55], + [286,55], + [287,55], + [288,55], + [289,55], + [290,55], + [291,56], + [292,56], + [293,56], + [294,56], + [295,56], + [296,56], + [297,56], + [298,56], + [299,56], + [300,56], + [301,56], + [302,56], + [303,56], + [304,56], + [305,57], + [306,57], + [307,57], + [308,57], + [309,57], + [310,57], + [311,57], + [312,57], + [313,57], + [314,57], + [315,57], + [316,57], + [317,57], + [318,57], + [319,58], + [320,58], + [321,58], + [322,58], + [323,58], + [324,58], + [325,58], + [326,58], + [327,58], + [328,58], + [329,58], + [330,58], + [331,58], + [332,59], + [333,59], + [334,59], + [335,59], + [336,59], + [337,59], + [338,59], + [339,59], + [340,59], + [341,59], + [342,59], + [343,59], + [344,59], + [345,59], + [346,60], + [347,60], + [348,60], + [349,60], + [350,60], + [351,60], + [352,60], + [353,60], + [354,60], + [355,60], + [356,60], + [357,60], + [358,60], + [359,61], + [360,61], + [361,61], + [362,61], + [363,61], + [364,61], + [365,61], + [366,61], + [367,61], + [368,61], + [369,61], + [370,61], + [371,61], + [372,61], + [373,61], + [374,61], + [375,61], + [376,61], + [377,61], + [378,61], + [379,61], + [380,61], + [381,61], + [382,61], + [383,61], + [384,61], + [385,61], + [386,61], + [387,61], + [388,61], + [389,61], + [390,61], + [391,61], + [392,61], + [393,61], + [394,61], + [395,61], + [396,61], + [397,61], + [398,61], + [399,61], + [400,61], + [401,61], + [402,61], + [403,61], + [404,61], + [405,61], + [406,61], + [407,61], + [408,61], + [409,61], + [410,61], + [411,61], + [412,61], + [413,61], + [414,61], + [415,61], + [416,61], + [417,61], + [418,62], + [419,62], + [420,62], + [421,62], + [422,62], + [423,62], + [424,62], + [425,62], + [426,62], + [427,62], + [428,62], + [429,62], + [430,62], + [431,62], + [432,62], + [433,62], + [434,62], + [435,63], + [436,63], + [437,63], + [438,63], + [439,63], + [440,63], + [441,63], + [442,63], + [443,63], + [444,64], + [445,64], + [446,64], + [447,64], + [448,64], + [449,64], + [450,65], + [451,65], + [452,65], + [453,65], + [454,65], + [455,65], + [456,65], + [457,66], + [458,66], + [459,66], + [460,66], + [461,66], + [462,66], + [463,67], + [464,68], + [465,68], + [466,69], + [467,70], + [468,70], + [469,71], + [470,72], + [471,72], + [472,73], + [473,74], + [474,74], + [475,75], + [476,76], + [477,76], + [478,77], + [479,78], + [480,78], + [481,79], + [482,80], + [483,81], + [484,81], + [485,82], + [486,83], + [487,84], + [488,85], + [489,85], + [490,86], + [491,87], + [492,88], + [493,89], + [494,89], + [495,90], + [496,91], + [497,92], + [498,93], + [499,94], + [500,95], + [501,95], + [502,96], + [503,97], + [504,98], + [505,99], + [506,100], + [507,101], + [508,102], + [509,103], + [510,104], + [511,105], + [512,106], + [513,107], + [514,108], + [515,109], + [516,110], + [517,111], + [518,112], + [519,113], + [520,114], + [521,115], + [522,116], + [523,117], + [524,118], + [525,119], + [526,121], + [527,122], + [528,123], + [529,124], + [530,125], + [531,126], + [532,127], + [533,129], + [534,130], + [535,131], + [536,132], + [537,134], + [538,135], + [539,136], + [540,137], + [541,139], + [542,140], + [543,141], + [544,143], + [545,144], + [546,145], + [547,147], + [548,148], + [549,149], + [550,151], + [551,152], + [552,154], + [553,155], + [554,156], + [555,158], + [556,159], + [557,161], + [558,162], + [559,164], + [560,165], + [561,167], + [562,169], + [563,170], + [564,172], + [565,173], + [566,175], + [567,177], + [568,178], + [569,180], + [570,182], + [571,183], + [572,185], + [573,187], + [574,188], + [575,190], + [576,192], + [577,194], + [578,196], + [579,197], + [580,199], + [581,201], + [582,203], + [583,205], + [584,207], + [585,209], + [586,211], + [587,213], + [588,215], + [589,217], + [590,219], + [591,221], + [592,223], + [593,225], + [594,227], + [595,229], + [596,231], + [597,234], + [598,236], + [599,238], + [600,240], + [601,242], + [602,245], + [603,247], + [604,249], + [605,252], + [606,254], + [607,256], + [608,259], + [609,261], + [610,264], + [611,266], + [612,269], + [613,271], + [614,274], + [615,276], + [616,279], + [617,281], + [618,284], + [619,287], + [620,289], + [621,292], + [622,295], + [623,298], + [624,300], + [625,303], + [626,306], + [627,309], + [628,312], + [629,315], + [630,318], + [631,321], + [632,324], + [633,327], + [634,330], + [635,333], + [636,336], + [637,339], + [638,342], + [639,345], + [640,349], + [641,352], + [642,355], + [643,358], + [644,362], + [645,365], + [646,369], + [647,372], + [648,376], + [649,379], + [650,383], + [651,386], + [652,390], + [653,393], + [654,397], + [655,401], + [656,405], + [657,408], + [658,412], + [659,416], + [660,420], + [661,424], + [662,428], + [663,432], + [664,436], + [665,440], + [666,444], + [667,448], + [668,452], + [669,457], + [670,461], + [671,465], + [672,470], + [673,474], + [674,479], + [675,483], + [676,488], + [677,492], + [678,497], + [679,501], + [680,506], + [681,511], + [682,516], + [683,520], + [684,525], + [685,530], + [686,535], + [687,540], + [688,545], + [689,550], + [690,555], + [691,561], + [692,566], + [693,571], + [694,577], + [695,582], + [696,587], + [697,593], + [698,598], + [699,604], + [700,610], + [701,615], + [702,621], + [703,627], + [704,633], + [705,639], + [706,645], + [707,651], + [708,657], + [709,663], + [710,669], + [711,675], + [712,682], + [713,688], + [714,695], + [715,701], + [716,708], + [717,714], + [718,721], + [719,728], + [720,735], + [721,741], + [722,748], + [723,755], + [724,762], + [725,770], + [726,777], + [727,784], + [728,791], + [729,799], + [730,806], + [731,814], + [732,821], + [733,829], + [734,837], + [735,845], + [736,853], + [737,861], + [738,869], + [739,877], + [740,885], + [741,893], + [742,902], + [743,910], + [744,919], + [745,927], + [746,936], + [747,945], + [748,954], + [749,962], + [750,971], + [751,981], + [752,990], + [753,999], + [754,1008], + [755,1018], + [756,1027], + [757,1037], + [758,1047], + [759,1056], + [760,1066], + [761,1076], + [762,1086], + [763,1097], + [764,1107], + [765,1117], + [766,1128], + [767,1138], + [768,1149], + [769,1160], + [770,1170], + [771,1181], + [772,1192], + [773,1204], + [774,1215], + [775,1226], + [776,1238], + [777,1249], + [778,1261], + [779,1273], + [780,1285], + [781,1297], + [782,1309], + [783,1321], + [784,1334], + [785,1346], + [786,1359], + [787,1371], + [788,1384], + [789,1397], + [790,1410], + [791,1423], + [792,1437], + [793,1450], + [794,1464], + [795,1477], + [796,1491], + [797,1505], + [798,1519], + [799,1534], + [800,1551], + [801,1568], + [802,1586], + [803,1604], + [804,1623], + [805,1641], + [806,1659], + [807,1678], + [808,1698], + [809,1716], + [810,1736], + [811,1756], + [812,1776], + [813,1795], + [814,1816], + [815,1836], + [816,1858], + [817,1879], + [818,1900], + [819,1921], + [820,1943], + [821,1964], + [822,1987], + [823,2010], + [824,2032], + [825,2051], + [826,2070], + [827,2090], + [828,2109], + [829,2129], + [830,2149], + [831,2169], + [832,2190], + [833,2210], + [834,2231], + [835,2252], + [836,2273], + [837,2294], + [838,2316], + [839,2337], + [840,2359], + [841,2381], + [842,2403], + [843,2426], + [844,2448], + [845,2471], + [846,2494], + [847,2517], + [848,2542], + [849,2566], + [850,2589], + [851,2614], + [852,2638], + [853,2662], + [854,2688], + [855,2713], + [856,2738], + [857,2764], + [858,2789], + [859,2816], + [860,2842], + [861,2869], + [862,2895], + [863,2923], + [864,2950], + [865,2977], + [866,3006], + [867,3034], + [868,3062], + [869,3091], + [870,3120], + [871,3150], + [872,3179], + [873,3208], + [874,3239], + [875,3268], + [876,3300], + [877,3330], + [878,3362], + [879,3393], + [880,3425], + [881,3456], + [882,3489], + [883,3521], + [884,3555], + [885,3587], + [886,3621], + [887,3656], + [888,3689], + [889,3724], + [890,3759], + [891,3794], + [892,3830], + [893,3866], + [894,3901], + [895,3938], + [896,3975], + [897,4013], + [898,4050], + [899,4088], + [900,4126], + [901,4165], + [902,4204], + [903,4244], + [904,4282], + [905,4323], + [906,4363], + [907,4404], + [908,4445], + [909,4487], + [910,4529], + [911,4571], + [912,4614], + [913,4657], + [914,4701], + [915,4746], + [916,4790], + [917,4835], + [918,4880], + [919,4925], + [920,4971], + [921,5017], + [922,5065], + [923,5112], + [924,5160], + [925,5209], + [926,5257], + [927,5306], + [928,5357], + [929,5406], + [930,5457], + [931,5508], + [932,5559], + [933,5612], + [934,5664], + [935,5717], + [936,5770], + [937,5825], + [938,5880], + [939,5934], + [940,5990], + [941,6046], + [942,6102], + [943,6160], + [944,6218], + [945,6275], + [946,6334], + [947,6394], + [948,6454], + [949,6514], + [950,6575], + [951,6637], + [952,6699], + [953,6762], + [954,6825], + [955,6889], + [956,6953], + [957,7018], + [958,7084], + [959,7150], + [960,7217], + [961,7284], + [962,7353], + [963,7422], + [964,7491], + [965,7562], + [966,7632], + [967,7703], + [968,7776], + [969,7848], + [970,7922], + [971,7996], + [972,8071], + [973,8146], + [974,8223], + [975,8300], + [976,8377], + [977,8456], + [978,8535], + [979,8615], + [980,8696], + [981,8776], + [982,8859], + [983,8942], + [984,9025], + [985,9110], + [986,9195], + [987,9282], + [988,9369], + [989,9456], + [990,9545], + [991,9634], + [992,9725], + [993,9815], + [994,9907], + [995,10000], + [996,10093], + [997,10188], + [998,10283], + [999,10380], + [1000,10476], + ] + + #SimC keeps combat rating multipliers in engine/dbc/generated/sc_scale_data.inc + #Trinket combat rating multipliers for item level 1 - 1000, wow build 23420, updated 2017-02-25 + COMBAT_RATING_MULTIPLIERS = [ + 1, 1, 1, 1, 1, # 5 + 1, 1, 1, 1, 1, # 10 + 1, 1, 1, 1, 1, # 15 + 1, 1, 1, 1, 1, # 20 + 1, 1, 1, 1, 1, # 25 + 1, 1, 1, 1, 1, # 30 + 1, 1, 1, 1, 1, # 35 + 1, 1, 1, 1, 1, # 40 + 1, 1, 1, 1, 1, # 45 + 1, 1, 1, 1, 1, # 50 + 1, 1, 1, 1, 1, # 55 + 1, 1, 1, 1, 1, # 60 + 1, 1, 1, 1, 1, # 65 + 1, 1, 1, 1, 1, # 70 + 1, 1, 1, 1, 1, # 75 + 1, 1, 1, 1, 1, # 80 + 1, 1, 1, 1, 1, # 85 + 1, 1, 1, 1, 1, # 90 + 1, 1, 1, 1, 1, # 95 + 1, 1, 1, 1, 1, # 100 + 1, 1, 1, 1, 1, # 105 + 1, 1, 1, 1, 1, # 110 + 1, 1, 1, 1, 1, # 115 + 1, 1, 1, 1, 1, # 120 + 1, 1, 1, 1, 1, # 125 + 1, 1, 1, 1, 1, # 130 + 1, 1, 1, 1, 1, # 135 + 1, 1, 1, 1, 1, # 140 + 1, 1, 1, 1, 1, # 145 + 1, 1, 1, 1, 1, # 150 + 1, 1, 1, 1, 1, # 155 + 1, 1, 1, 1, 1, # 160 + 1, 1, 1, 1, 1, # 165 + 1, 1, 1, 1, 1, # 170 + 1, 1, 1, 1, 1, # 175 + 1, 1, 1, 1, 1, # 180 + 1, 1, 1, 1, 1, # 185 + 1, 1, 1, 1, 1, # 190 + 1, 1, 1, 1, 1, # 195 + 1, 1, 1, 1, 1, # 200 + 1, 1, 1, 1, 1, # 205 + 1, 1, 1, 1, 1, # 210 + 1, 1, 1, 1, 1, # 215 + 1, 1, 1, 1, 1, # 220 + 1, 1, 1, 1, 1, # 225 + 1, 1, 1, 1, 1, # 230 + 1, 1, 1, 1, 1, # 235 + 1, 1, 1, 1, 1, # 240 + 1, 1, 1, 1, 1, # 245 + 1, 1, 1, 1, 1, # 250 + 1, 1, 1, 1, 1, # 255 + 1, 1, 1, 1, 1, # 260 + 1, 1, 1, 1, 1, # 265 + 1, 1, 1, 1, 1, # 270 + 1, 1, 1, 1, 1, # 275 + 1, 1, 1, 1, 1, # 280 + 1, 1, 1, 1, 1, # 285 + 1, 1, 1, 1, 1, # 290 + 1, 1, 1, 1, 1, # 295 + 1, 1, 1, 1, 1, # 300 + 1, 1, 1, 1, 1, # 305 + 1, 1, 1, 1, 1, # 310 + 1, 1, 1, 1, 1, # 315 + 1, 1, 1, 1, 1, # 320 + 1, 1, 1, 1, 1, # 325 + 1, 1, 1, 1, 1, # 330 + 1, 1, 1, 1, 1, # 335 + 1, 1, 1, 1, 1, # 340 + 1, 1, 1, 1, 1, # 345 + 1, 1, 1, 1, 1, # 350 + 1, 1, 1, 1, 1, # 355 + 1, 1, 1, 1, 1, # 360 + 1, 1, 1, 1, 1, # 365 + 1, 1, 1, 1, 1, # 370 + 1, 1, 1, 1, 1, # 375 + 1, 1, 1, 1, 1, # 380 + 1, 1, 1, 1, 1, # 385 + 1, 1, 1, 1, 1, # 390 + 1, 1, 1, 1, 1, # 395 + 1, 1, 1, 1, 1, # 400 + 1, 1, 1, 1, 1, # 405 + 1, 1, 1, 1, 1, # 410 + 1, 1, 1, 1, 1, # 415 + 1, 1, 1, 1, 1, # 420 + 1, 1, 1, 1, 1, # 425 + 1, 1, 1, 1, 1, # 430 + 1, 1, 1, 1, 1, # 435 + 1, 1, 1, 1, 1, # 440 + 1, 1, 1, 1, 1, # 445 + 1, 1, 1, 1, 1, # 450 + 1, 1, 1, 1, 1, # 455 + 1, 1, 1, 1, 1, # 460 + 1, 1, 1, 1, 1, # 465 + 1, 1, 1, 1, 1, # 470 + 1, 1, 1, 1, 1, # 475 + 1, 1, 1, 1, 1, # 480 + 1, 1, 1, 1, 1, # 485 + 1, 1, 1, 1, 1, # 490 + 1, 1, 1, 1, 1, # 495 + 1, 1, 1, 1, 1, # 500 + 1, 1, 1, 1, 1, # 505 + 1, 1, 1, 1, 1, # 510 + 1, 1, 1, 1, 1, # 515 + 1, 1, 1, 1, 1, # 520 + 1, 1, 1, 1, 1, # 525 + 1, 1, 1, 1, 1, # 530 + 1, 1, 1, 1, 1, # 535 + 1, 1, 1, 1, 1, # 540 + 1, 1, 1, 1, 1, # 545 + 1, 1, 1, 1, 1, # 550 + 1, 1, 1, 1, 1, # 555 + 1, 1, 1, 1, 1, # 560 + 1, 1, 1, 1, 1, # 565 + 1, 1, 1, 1, 1, # 570 + 1, 1, 1, 1, 1, # 575 + 1, 1, 1, 1, 1, # 580 + 1, 1, 1, 1, 1, # 585 + 1, 1, 1, 1, 1, # 590 + 1, 1, 1, 1, 1, # 595 + 1, 1, 1, 1, 1, # 600 + 1, 1, 1, 1, 1, # 605 + 1, 1, 1, 1, 1, # 610 + 1, 1, 1, 1, 1, # 615 + 1, 1, 1, 1, 1, # 620 + 1, 1, 1, 1, 1, # 625 + 1, 1, 1, 1, 1, # 630 + 1, 1, 1, 1, 1, # 635 + 1, 1, 1, 1, 1, # 640 + 1, 1, 1, 1, 1, # 645 + 1, 1, 1, 1, 1, # 650 + 1, 1, 1, 1, 1, # 655 + 1, 1, 1, 1, 1, # 660 + 1, 1, 1, 1, 1, # 665 + 1, 1, 1, 1, 1, # 670 + 1, 1, 1, 1, 1, # 675 + 1, 1, 1, 1, 1, # 680 + 1, 1, 1, 1, 1, # 685 + 1, 1, 1, 1, 1, # 690 + 1, 1, 1, 1, 1, # 695 + 1, 1, 1, 1, 1, # 700 + 1, 1, 1, 1, 1, # 705 + 1, 1, 1, 1, 1, # 710 + 1, 1, 1, 1, 1, # 715 + 1, 1, 1, 1, 1, # 720 + 1, 1, 1, 1, 1, # 725 + 1, 1, 1, 1, 1, # 730 + 1, 1, 1, 1, 1, # 735 + 1, 1, 1, 1, 1, # 740 + 1, 1, 1, 1, 1, # 745 + 1, 1, 1, 1, 1, # 750 + 1, 1, 1, 1, 1, # 755 + 1, 1, 1, 1, 1, # 760 + 1, 1, 1, 1, 1, # 765 + 1, 1, 1, 1, 1, # 770 + 1, 1, 1, 1, 1, # 775 + 1, 1, 1, 1, 1, # 780 + 1, 1, 1, 1, 1, # 785 + 1, 1, 1, 1, 1, # 790 + 1, 1, 1, 1, 1, # 795 + 1, 1, 1, 1, 1, # 800 + 0.994435486, 0.988901936, 0.983399178, 0.977927039, 0.972485351, # 805 + 0.967073942, 0.961692646, 0.956341294, 0.95101972, 0.945727757, # 810 + 0.940465242, 0.93523201, 0.930027899, 0.924852745, 0.91970639, # 815 + 0.914588671, 0.909499429, 0.904438507, 0.899405746, 0.894400991, # 820 + 0.889424084, 0.884474871, 0.879553199, 0.874658913, 0.869791861, # 825 + 0.864951892, 0.860138855, 0.855352601, 0.850592979, 0.845859843, # 830 + 0.841153044, 0.836472436, 0.831817874, 0.827189212, 0.822586306, # 835 + 0.818009013, 0.813457191, 0.808930697, 0.804429391, 0.799953132, # 840 + 0.795501782, 0.791075201, 0.786673252, 0.782295798, 0.777942702, # 845 + 0.773613829, 0.769309044, 0.765028214, 0.760771204, 0.756537882, # 850 + 0.752328116, 0.748141776, 0.743978731, 0.739838851, 0.735722007, # 855 + 0.731628072, 0.727556917, 0.723508417, 0.719482444, 0.715478874, # 860 + 0.711497582, 0.707538444, 0.703601336, 0.699686137, 0.695792724, # 865 + 0.691920975, 0.688070772, 0.684241992, 0.680434518, 0.676648231, # 870 + 0.672883012, 0.669138746, 0.665415314, 0.661712601, 0.658030492, # 875 + 0.654368872, 0.650727628, 0.647106645, 0.643505811, 0.639925014, # 880 + 0.636364142, 0.632823085, 0.629301732, 0.625799974, 0.622317701, # 885 + 0.618854806, 0.61541118, 0.611986716, 0.608581307, 0.605194848, # 890 + 0.601827233, 0.598478357, 0.595148116, 0.591836406, 0.588543124, # 895 + 0.585268168, 0.582011435, 0.578772824, 0.575552235, 0.572349566, # 900 + 0.569164719, 0.565997594, 0.562848093, 0.559716117, 0.556601569, # 905 + 0.553504352, 0.550424369, 0.547361525, 0.544315724, 0.541286872, # 910 + 0.538274873, 0.535279635, 0.532301064, 0.529339068, 0.526393553, # 915 + 0.523464429, 0.520551604, 0.517654987, 0.514774489, 0.511910019, # 920 + 0.509061489, 0.506228809, 0.503411892, 0.500610649, 0.497824994, # 925 + 0.49505484, 0.492300101, 0.48956069, 0.486836523, 0.484127514, # 930 + 0.48143358, 0.478754636, 0.476090599, 0.473441387, 0.470806916, # 935 + 0.468187104, 0.46558187, 0.462991133, 0.460414813, 0.457852828, # 940 + 0.4553051, 0.452771548, 0.450252095, 0.447746661, 0.445255168, # 945 + 0.44277754, 0.440313698, 0.437863566, 0.435427068, 0.433004128, # 950 + 0.430594671, 0.428198621, 0.425815904, 0.423446445, 0.421090172, # 955 + 0.41874701, 0.416416886, 0.414099729, 0.411795465, 0.409504023, # 960 + 0.407225332, 0.404959321, 0.40270592, 0.400465057, 0.398236664, # 965 + 0.39602067, 0.393817008, 0.391625607, 0.389446401, 0.387279321, # 970 + 0.3851243, 0.382981271, 0.380850166, 0.37873092, 0.376623467, # 975 + 0.37452774, 0.372443675, 0.370371207, 0.368310272, 0.366260804, # 980 + 0.364222741, 0.362196018, 0.360180574, 0.358176344, 0.356183267, # 985 + 0.35420128, 0.352230322, 0.350270331, 0.348321247, 0.346383009, # 990 + 0.344455556, 0.342538828, 0.340632766, 0.33873731, 0.336852402, # 995 + 0.334977982, 0.333113992, 0.331260375, 0.329417072, 0.327584026, # 1000 + 0.32576118, 0.323948478, 0.322145862, 0.320353277, 0.318570666, # 1005 + 0.316797976, 0.315035149, 0.313282131, 0.311538869, 0.309805306, # 1010 + 0.30808139, 0.306367067, 0.304662283, 0.302966986, 0.301281122, # 1015 + 0.299604639, 0.297937485, 0.296279607, 0.294630955, 0.292991477, # 1020 + 0.291361122, 0.289739839, 0.288127578, 0.286524288, 0.28492992, # 1025 + 0.283344423, 0.281767749, 0.280199849, 0.278640673, 0.277090173, # 1030 + 0.275548301, 0.274015008, 0.272490248, 0.270973972, 0.269466134, # 1035 + 0.267966686, 0.266475582, 0.264992774, 0.263518218, 0.262051868, # 1040 + 0.260593676, 0.259143599, 0.257701591, 0.256267607, 0.254841602, # 1045 + 0.253423533, 0.252013354, 0.250611022, 0.249216494, 0.247829725, # 1050 + 0.246450673, 0.245079295, 0.243715548, 0.242359389, 0.241010777, # 1055 + 0.239669669, 0.238336024, 0.2370098, 0.235690956, 0.23437945, # 1060 + 0.233075242, 0.231778292, 0.230488558, 0.229206002, 0.227930582, # 1065 + 0.226662259, 0.225400994, 0.224146747, 0.222899479, 0.221659152, # 1070 + 0.220425726, 0.219199164, 0.217979427, 0.216766478, 0.215560278, # 1075 + 0.21436079, 0.213167976, 0.2119818, 0.210802224, 0.209629212, # 1080 + 0.208462728, 0.207302734, 0.206149195, 0.205002075, 0.203861338, # 1085 + 0.202726949, 0.201598872, 0.200477072, 0.199361515, 0.198252165, # 1090 + 0.197148988, 0.19605195, 0.194961016, 0.193876153, 0.192797326, # 1095 + 0.191724503, 0.190657649, 0.189596732, 0.188541718, 0.187492575, # 1100 + 0.18644927, 0.185411771, 0.184380044, 0.183354059, 0.182333783, # 1105 + 0.181319184, 0.180310231, 0.179306892, 0.178309136, 0.177316933, # 1110 + 0.17633025, 0.175349058, 0.174373326, 0.173403023, 0.172438119, # 1115 + 0.171478585, 0.17052439, 0.169575505, 0.1686319, 0.167693545, # 1120 + 0.166760412, 0.165832471, 0.164909694, 0.163992052, 0.163079516, # 1125 + 0.162172058, 0.161269649, 0.160372262, 0.159479868, 0.15859244, # 1130 + 0.15770995, 0.156832371, 0.155959675, 0.155091836, 0.154228825, # 1135 + 0.153370616, 0.152517184, 0.1516685, 0.150824538, 0.149985273, # 1140 + 0.149150678, 0.148320727, 0.147495394, 0.146674654, 0.145858481, # 1145 + 0.145046849, 0.144239734, 0.14343711, 0.142638952, 0.141845236, # 1150 + 0.141055936, 0.140271028, 0.139490488, 0.138714291, 0.137942414, # 1155 + 0.137174831, 0.13641152, 0.135652456, 0.134897616, 0.134146977, # 1160 + 0.133400514, 0.132658205, 0.131920026, 0.131185956, 0.13045597, # 1165 + 0.129730046, 0.129008161, 0.128290293, 0.12757642, 0.126866519, # 1170 + 0.126160569, 0.125458547, 0.124760431, 0.1240662, 0.123375832, # 1175 + 0.122689305, 0.122006599, 0.121327691, 0.120652562, 0.119981189, # 1180 + 0.119313552, 0.11864963, 0.117989402, 0.117332849, 0.116679948, # 1185 + 0.116030681, 0.115385027, 0.114742965, 0.114104477, 0.113469541, # 1190 + 0.112838138, 0.112210248, 0.111585853, 0.110964932, 0.110347466, # 1195 + 0.109733436, 0.109122823, 0.108515607, 0.107911771, 0.107311294, # 1200 + 0.106714159, 0.106120347, 0.105529838, 0.104942616, 0.104358662, # 1205 + 0.103777956, 0.103200482, 0.102626222, 0.102055157, 0.10148727, # 1210 + 0.100922542, 0.100360957, 0.099802497, 0.099247145, 0.098694883, # 1215 + 0.098145694, 0.097599561, 0.097056467, 0.096516395, 0.095979328, # 1220 + 0.095445249, 0.094914143, 0.094385992, 0.09386078, 0.09333849, # 1225 + 0.092819107, 0.092302614, 0.091788995, 0.091278233, 0.090770314, # 1230 + 0.090265222, 0.08976294, 0.089263453, 0.088766745, 0.088272801, # 1235 + 0.087781606, 0.087293144, 0.0868074, 0.086324359, 0.085844006, # 1240 + 0.085366326, 0.084891304, 0.084418925, 0.083949174, 0.083482038, # 1245 + 0.083017501, 0.082555549, 0.082096168, 0.081639342, 0.081185059, # 1250 + 0.080733304, 0.080284062, 0.07983732, 0.079393065, 0.078951281, # 1255 + 0.078511955, 0.078075074, 0.077640625, 0.077208592, 0.076778964, # 1260 + 0.076351726, 0.075926866, 0.07550437, 0.075084225, 0.074666418, # 1265 + 0.074250935, 0.073837765, 0.073426894, 0.073018309, 0.072611997, # 1270 + 0.072207947, 0.071806145, 0.071406578, 0.071009236, 0.070614104, # 1275 + 0.070221171, 0.069830424, 0.069441851, 0.069055441, 0.068671181, # 1280 + 0.06828906, 0.067909064, 0.067531183, 0.067155405, 0.066781718, # 1285 + 0.06641011, 0.06604057, 0.065673086, 0.065307648, 0.064944242, # 1290 + 0.064582859, 0.064223487, 0.063866115, 0.063510731, 0.063157324, # 1295 + 0.062805884, 0.0624564, 0.062108861, 0.061763255, 0.061419573, # 1300 ] - + def get_class_number(self, game_class): - for i in self.GAME_CLASS_NUMBER.keys(): + for i in list(self.GAME_CLASS_NUMBER.keys()): if self.GAME_CLASS_NUMBER[i] == game_class: return i raise exceptions.InvalidInputException(_('{game_class} is not a supported game class').format(game_class=game_class)) @@ -1055,7 +1360,21 @@ def get_random_prop_point(self, item_level): if item_level < 1: raise exceptions.InvalidInputException(_('item_level={item_level} need to be >= 1').format(item_level=item_level)) return self.RANDOM_PROP_POINTS[item_level][1] - + + def get_combat_rating_multiplier(self, item_level): + if item_level < 1: + raise exceptions.InvalidInputException(_('item_level={item_level} need to be >= 1').format(item_level=item_level)) + return self.COMBAT_RATING_MULTIPLIERS[item_level - 1] + def get_constant_scaling_point(self, level): return self.CONSTANT_SCALING[level-1] - + + def get_base_armor(self, level): + if level < 100: + raise exceptions.InvalidInputException(_('There\'s no armor value for a target level {level}').format(level=str(level))) + return self.BASE_ARMOR[level] + + def get_k_value(self, level): + if level < 100: + raise exceptions.InvalidInputException(_('There\'s no k value for a level {level} player').format(level=str(level))) + return self.ATTACKER_K_VALUE[level] \ No newline at end of file diff --git a/shadowcraft/objects/glyphs.py b/shadowcraft/objects/glyphs.py deleted file mode 100644 index fa5e172..0000000 --- a/shadowcraft/objects/glyphs.py +++ /dev/null @@ -1,16 +0,0 @@ -from shadowcraft.objects import glyphs_data - -class Glyphs(object): - - def __init__(self, game_class='rogue', *args): - self.game_class = game_class - self.allowed_glyphs = glyphs_data.glyphs[game_class] - for arg in args: - if arg in self.allowed_glyphs: - setattr(self, arg, True) - - def __getattr__(self, name): - # Any glyph we haven't assigned a value to, we don't have. - if name in self.allowed_glyphs: - return False - object.__getattribute__(self, name) diff --git a/shadowcraft/objects/glyphs_data.py b/shadowcraft/objects/glyphs_data.py deleted file mode 100644 index d3ff283..0000000 --- a/shadowcraft/objects/glyphs_data.py +++ /dev/null @@ -1,56 +0,0 @@ -glyphs = { - 'death_knight': frozenset([]), - 'druid': frozenset([]), - 'hunter': frozenset([]), - 'mage': frozenset([]), - 'monk': frozenset([]), - 'paladin': frozenset([]), - 'priest': frozenset([]), - 'rogue': frozenset([ - # Major - 'adrenaline_rush', - 'ambush', - 'blade_flurry', - 'blind', - 'cheap_shot', - 'cloak_of_shadows', - 'crippling_poison', - 'deadly_momentum', - 'debilitation', - 'evasion', - 'expose_armor', - 'feint', - 'garrote', - 'gouge', - 'kick', - 'recuperate', - 'sap', - 'shadow_walk', - 'shiv', - 'smoke_bomb', - 'sprint', - 'stealth', - 'vanish', - 'vendetta', - 'disappearance', - 'energy', - 'elusiveness', - 'energy_flows', - # Minor - 'blurred_speed', - 'decoy', - 'detection', - 'disguise', - 'distract', - 'hemorrhage', - 'killing_spree', - 'pick_lock', - 'pick_pocket', - 'poisons', - 'safe_fall', - 'tricks_of_the_trade', - ]), - 'shaman': frozenset([]), - 'warlock': frozenset([]), - 'warrior': frozenset([]), -} diff --git a/shadowcraft/objects/modifiers.py b/shadowcraft/objects/modifiers.py new file mode 100644 index 0000000..e8c048a --- /dev/null +++ b/shadowcraft/objects/modifiers.py @@ -0,0 +1,76 @@ +from builtins import object +from shadowcraft.core import exceptions + +#ModifierList contains all modifiers needed for dps computation. +#ModifierList is used to compile all modifiers into a single lumped modifier per damage source. +#Typical use case registers all modifiers needed at the beginning of the computation with a +#None value and then updates later. +#Before final damage computation a dict is compiled and used for damage computation +class ModifierList(object): + def __init__(self, sources): + self.sources = sources + self.modifiers = {} + + def register_modifier(self, modifier): + self.modifiers[modifier.name] = modifier + for ability in modifier.ability_list: + if ability not in self.sources: + raise exceptions.InvalidInputException(_('Unknown source {source} in damage modifier {mod}').format(source=ability, mod=modifier.name)) + + def update_modifier_value(self, modifier_name, value): + self.modifiers[modifier_name].value = value + + def compile_modifier_dict(self): + lumped_modifier = {s:1 for s in self.sources} + + # mods for all damage + lumped_modifier['all_damage'] = 1 + for mod in list(self.modifiers.values()): + if mod.value is None: + raise exceptions.InvalidInputException(_('Modifier {mod} is uninitialized').format(mod=mod.name)) + if mod.all_damage: + lumped_modifier['all_damage'] *= mod.value + + # mods for damage schools + for mod in list(self.modifiers.values()): + if mod.value is None: + raise exceptions.InvalidInputException(_('Modifier {mod} is uninitialized').format(mod=mod.name)) + if mod.dmg_schools: + for school in mod.dmg_schools: + modname = 'school_' + school + if modname in lumped_modifier: + lumped_modifier[modname] *= mod.value + else: + lumped_modifier[modname] = lumped_modifier['all_damage'] * mod.value + + # mods for source abilities + for mod in list(self.modifiers.values()): + if mod.value is None: + raise exceptions.InvalidInputException(_('Modifier {mod} is uninitialized').format(mod=mod.name)) + for ability in self.sources: + if mod.blacklist: + if ability in mod.ability_list: + continue + else: + lumped_modifier[ability] *= mod.value + elif mod.all_damage or ability in mod.ability_list: + lumped_modifier[ability] *= mod.value + + return lumped_modifier + +#DamageModifier specifies any type of modifier applied to ability damage. +#Each modifier is specified as a value applied to either a whitelist or blacklist of abilities. +#Whitelist is default since it is more compact for most modifiers +#but all damage modifiers can be represented either way +class DamageModifier(object): + def __init__(self, name, value, ability_list, blacklist=False, all_damage=False, dmg_schools=None): + self.name = name + self.value = value + self.ability_list = ability_list + self.blacklist = blacklist + self.all_damage = all_damage + self.dmg_schools = dmg_schools + + if self.all_damage and self.dmg_schools is not None: + raise exceptions.InvalidInputException(_('Modifier {mod} should only specify either all_damage or dmg_schools').format(mod=mod.name)) + diff --git a/shadowcraft/objects/priority_list.py b/shadowcraft/objects/priority_list.py index 6501b0f..4d7897b 100644 --- a/shadowcraft/objects/priority_list.py +++ b/shadowcraft/objects/priority_list.py @@ -1,9 +1,11 @@ +from __future__ import print_function +from builtins import object class PriorityList(object): def __init__(self, *args): #for each arg (string), read conditionals and determine checks for a in args: - print a #to implement later + print(a) #to implement later return def __getattr__(self, name): diff --git a/shadowcraft/objects/proc_data.py b/shadowcraft/objects/proc_data.py index e9dfad0..732ec8b 100755 --- a/shadowcraft/objects/proc_data.py +++ b/shadowcraft/objects/proc_data.py @@ -1,3 +1,4 @@ +from __future__ import division # None should be used to indicate unknown values # The Proc class takes these parameters: # stat, value, duration, proc_name, default_behaviour, max_stacks=1, can_crit=True, spell_behaviour=None @@ -16,69 +17,56 @@ 'trigger': 'all_attacks' }, #potions - 'draenic_agi_pot': { - 'stat': 'stats', - 'value': {'agi':1000}, + 'old_war_pot': { + 'stat': 'special_model', #rppm, modeled in add_special_procs_damage + 'value': 169900, #level 110 assumed for simplicity + 'dmg_school': 'physical', 'duration': 25, - 'proc_name': 'Draenic Agi Potion', - 'item_level': 100, + 'proc_name': 'Potion of the Old War', 'type': 'icd', 'source': 'unique', 'icd': 0, 'proc_rate': 1.0, + 'can_crit': True, + 'haste_scales': True, 'trigger': 'all_attacks' }, - 'draenic_agi_prepot': { - 'stat': 'stats', - 'value': {'agi':1000}, - 'duration': 23, - 'proc_name': 'Draenic Agi Prepot', - 'item_level': 100, + 'old_war_prepot': { + 'stat': 'special_model', #rppm, modeled in add_special_procs_damage + 'value': 169900, #level 110 assumed for simplicity + 'dmg_school': 'physical', + 'duration': 25, + 'proc_name': 'Potion of the Old War', 'type': 'icd', 'source': 'unique', 'icd': 0, 'proc_rate': 1.0, + 'can_crit': True, + 'haste_scales': True, 'trigger': 'all_attacks' }, - 'virmens_bite': { + 'prolonged_power_pot': { 'stat': 'stats', - 'value': {'agi':456}, - 'duration': 25, - 'proc_name': 'Virmens Bite', - 'item_level': 90, + 'value': {'agi': 2500, 'str': 2500, 'int': 2500}, + 'duration': 60, + 'proc_name': 'Potion of Prolonged Power', 'type': 'icd', 'source': 'unique', 'icd': 0, 'proc_rate': 1.0, 'trigger': 'all_attacks' }, - 'virmens_bite_prepot': { + 'prolonged_power_prepot': { 'stat': 'stats', - 'value': {'agi':456}, - 'duration': 23, - 'proc_name': 'Virmens Bite', - 'item_level': 90, + 'value': {'agi': 2500, 'str': 2500, 'int': 2500}, + 'duration': 60, + 'proc_name': 'Potion of Prolonged Power', 'type': 'icd', 'source': 'unique', 'icd': 0, 'proc_rate': 1.0, 'trigger': 'all_attacks' }, - #weapon enchant support - 'mark_of_the_shattered_hand_dot': { - 'stat': 'physical_dot', - 'value': 750, - 'duration': 6, - 'proc_name': 'Mark of the Shattered Hand DOT', - 'type': 'rppm', - 'source': 'weapon', - 'item_level': 100, - 'icd': 0, - 'proc_rate': 2.5, - 'can_crit': False, - 'haste_scales': True, - 'trigger': 'all_attacks', - }, #racials 'touch_of_the_grave': { 'stat': 'spell_damage', @@ -102,6 +90,90 @@ 'proc_rate': 1.0, 'trigger': 'all_attacks' }, + #netherlight crucible + 'chaotic_darkness': { + 'stat': 'spell_damage', + 'dmg_school': 'shadow', + 'value': 180000, #avg of range 60000 to 5*60000 + 'proc_name': 'Chaotic Darkness', + 'duration': 0, + 'type': 'rppm', + 'source': 'crucible', + 'proc_rate': 2, + 'haste_scales': True, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + 'dark_sorrows': { + 'stat': 'spell_damage', + 'dmg_school': 'shadow', + 'aoe': True, + 'value': 186350, + 'proc_name': 'Dark Sorrows', + 'duration': 0, + 'type': 'rppm', + 'source': 'crucible', + 'icd': 8, + 'proc_rate': 1, + 'haste_scales': True, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + 'infusion_of_light': { + 'stat': 'spell_damage', + 'dmg_school': 'holy', + 'value': 101000, + 'proc_name': 'Infusion of Light', + 'duration': 0, + 'type': 'rppm', + 'source': 'crucible', + 'icd': 1, + 'proc_rate': 4, + 'haste_scales': True, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + 'secure_in_the_light': { + 'stat': 'spell_damage', + 'dmg_school': 'holy', + 'value': 135000, + 'proc_name': 'Secure in the Light', + 'duration': 0, + 'type': 'rppm', + 'source': 'crucible', + 'icd': 1, + 'proc_rate': 3, + 'haste_scales': True, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + 'shadowbind': { + 'stat': 'spell_damage', + 'dmg_school': 'shadow', + 'value': 200000, + 'proc_name': 'Shadowbind', + 'duration': 0, + 'type': 'rppm', + 'source': 'crucible', + 'proc_rate': 2, + 'haste_scales': True, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + 'torment_the_weak': { + 'stat': 'spell_dot', + 'dmg_school': 'shadow', + 'dot_ticks': 5, + 'can_crit': True, + 'value': 16000, + 'duration': 15, + 'max_stacks': 3, + 'proc_name': 'Torment the Weak', + 'type': 'rppm', + 'proc_rate': 4, + 'source': 'crucible', + 'trigger': 'all_attacks' + }, #gear procs 'fury_of_xuen': { 'stat':'physical_damage', @@ -145,6 +217,7 @@ 'proc_rate': 0.92, 'trigger': 'all_attacks' }, + 'archmages_greater_incandescence': { 'stat':'stats_modifier', 'value': {'agi':.15}, @@ -158,54 +231,885 @@ 'proc_rate': 0.92, 'trigger': 'all_attacks' }, - #6.1 procs (alch only) - 'stone_of_wind': { + + #7.0 neck enchants + 'mark_of_the_hidden_satyr': { + 'stat':'spell_damage', + 'value': 0, # AP based + 'ap_coefficient': 2.5, # server-side, not in dbc + 'dmg_school': 'fire', + 'duration': 0, + 'proc_name': 'Mark of the Hidden Satyr', + 'type': 'rppm', + 'source': 'neck', + 'proc_rate': 2.5, + 'haste_scales': True, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + + 'mark_of_the_distant_army': { #A distant army fires a volley of arrows, dealing 3 ticks of damage over 1.5 sec. + 'stat':'physical_dot', + 'value': 0, # AP based + 'aoe': True, + 'ap_coefficient': 2.5 / 3, # server-side, not in dbc, per tick is 2.5 / 3 + 'duration': 1.5, + 'dot_ticks': 3, + 'proc_name': 'Mark of the Distant Army', + 'type': 'rppm', + 'source': 'neck', + 'proc_rate': 2.5, + 'haste_scales': True, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + + 'mark_of_the_claw': { #Permanently enchants a necklace to sometimes increase critical strike and haste by 1000 for 6 sec. + 'stat':'stats', + 'value': {'haste': 1000, 'crit': 1000}, + 'duration': 6, + 'proc_name': 'Mark of the Claw', + 'source': 'neck', + 'type': 'rppm', + 'proc_rate': 3, + 'trigger': 'all_attacks', + }, + + #Legion trinket procs + 'arcanogolem_digit': { #Equip: Your attacks have a chance to rake all enemies in front of you for X Arcane damage. + 'stat':'spell_damage', + 'value': 0, #rpp-scaled + 'aoe': True, + 'duration': 0, + 'proc_name': 'Arcane Swipe', + 'dmg_school': 'arcane', + 'scaling': 14.21082, #hotfixed value + 'item_level': 870, + 'type': 'rppm', + 'source': 'trinket', + 'proc_rate': 5, + 'icd': 1, + 'haste_scales': True, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + + 'bloodstained_handkerchief': { #Use: Garrote your target from behind, causing them to bleed for X Physical damage every 3 sec until they die. (1 Min Cooldown) + 'stat':'physical_damage', #modeled as icd because it's active FOREVER + 'value': 0, #rpp-scaled, TODO: could be applied to adds as well, after CD + 'duration': 0, + 'proc_name': 'Cruel Garrote', + 'scaling': 3.474452, + 'item_level': 855, + 'type': 'icd', + 'source': 'trinket', + 'proc_rate': 1, + 'icd': 3, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + + 'bloodthirsty_instinct': { #Equip: Your melee attacks have a chance to increase your Haste by X for 10 sec. This effect occurs more often against targets at low health. + 'stat':'stats', + 'value': {'haste': 0}, #rpp-scaled + 'duration': 10, + 'proc_name': 'Bloodthirsty Instinct', + 'scaling': 1.470561, + 'crm_scales': True, + 'item_level': 850, + 'source': 'trinket', + 'type': 'rppm', + 'proc_rate': 3, + 'trigger': 'all_attacks', + }, + + 'chaos_talisman': { #Equip: Your melee autoattacks grant you Chaotic Energy, increasing your Strength or Agility by X, stacking up to 20 times. If you do not autoattack an enemy for 4 sec, this effect will decrease by 1 stack every sec. + 'stat':'stats', + 'value': {'agi': 0}, #rpp-scaled + 'duration': 23, #decays by 1 stack after 4s without autoattacks (assume we can ignore decay) + 'max_stacks': 20, + 'proc_name': 'Chaotic Energy', + 'scaling': 0.029595, + 'item_level': 805, + 'source': 'trinket', + 'type': 'icd', + 'icd': 0, # stacks with every autohit + 'proc_rate': 1, + 'trigger': 'auto_attacks', + }, + + 'chrono_shard': { #Equip: Your spells and abilities have a chance to grant you X Haste and 15% movement speed for 10 sec. + 'stat':'stats', + 'value': {'haste': 0}, #rpp-scaled + 'duration': 10, + 'proc_name': 'Acceleration', + 'scaling': 2.741159, + 'crm_scales': True, + 'item_level': 805, + 'source': 'trinket', + 'type': 'rppm', + 'proc_rate': 1, + 'trigger': 'all_attacks', + }, + + 'convergence_of_fates': { #Equip: Your attacks have a chance to reduce the remaining cooldown on one of your powerful abilities by 5 sec. + 'stat':'ability_modifier', + 'value': 5, #5 sec decrease, modeled in get_spell_cd + 'duration': 0, + 'proc_name': 'Prescience', # reduce cd of shadow blades, vendetta, adrenaline rush + 'item_level': 875, + 'source': 'trinket', + 'type': 'rppm', + 'proc_rate': {'assassination': 3.51, 'outlaw': 8.4, 'subtlety': 9}, + 'trigger': 'all_attacks', + }, + + 'cradle_of_anguish': { #Equip: While you are above 80% health you gain X Strength or Agility per second, based on your specialization, stacking up to 10 times. If you fall below 50% health, this effect is lost. + 'stat': 'special_model', #handled in determine stats, assume 10 stacks all the time + 'value': {'agi':0}, #rpp-scaled + 'proc_name': 'Strength of Will', + 'item_level': 900, + 'scaling': 0.05585, + 'duration': 1, + 'max_stacks': 10, + 'type': 'icd', + 'icd': 1, + 'proc_rate': 1, + 'source': 'trinket', + }, + + #removed the ":" not sure which way it should be + 'darkmoon_deck_dominion': { #Equip: Increase critical strike by X-Y. The amount of critical strike depends on the topmost card in the deck. Equip: Periodically shuffle the deck while in combat. + 'stat': 'stats', + 'value': {'crit':0}, #rpp-scaled, TODO: not accurate, it should be shuffled every 20s + 'duration': 20, + 'proc_name': 'Dominion Deck', #this does some wierd shuffling crit values /wrists + 'scaling': 0.5627245, #use average for now, min 0.375134, max 0.750315 + 'item_level': 815, + 'type': 'icd', + 'icd': 20, #slight loss? should change value instantly, not on attack trigger + 'source': 'trinket', + 'proc_rate': 1, + 'trigger': 'all_attacks' + }, + + 'draught_of_souls': { #Use: Enter a fel-crazed rage, dealing X damage to a random nearby enemy every 0.25sec for 3 sec. You cannot move or use abilities during your rage. (1 Min, 20 Sec Cooldown) + 'stat':'spell_damage', + 'value': 0, #rpp-scaled + 'duration': 3, #3sec ability downtime modeled in add_special_aps_penalties + 'proc_name': 'Fel-Crazed Rage', + 'dmg_school': 'shadow', + 'scaling': 33. * 13., #13 hits total + 'item_level': 880, + 'source': 'trinket', + 'type': 'icd', + 'icd': 80, + 'proc_rate': 1, + 'can_crit': True, + 'trigger': 'all_attacks', + }, + + 'engine_of_eradication': { #Equip: Your auto attacks have a chance to increase your Strength or Agility, based on your specialization, by 5424 for 12 sec, and expel orbs of fel energy. Collecting an orb increases the duration of this effect by 3 sec. + 'stat':'stats', + 'value': {'agi': 0}, + 'duration': 24, # 12 + 4*3 + 'proc_name': 'Demonic Vigor', + 'scaling': 1.314659, + 'crm_scales': False, + 'item_level': 900, + 'source': 'trinket', + 'type': 'rppm', + 'proc_rate': 1, + 'trigger': 'all_attacks', + }, + + 'entwined_elemental_foci': { #Equip: Your attacks have a chance to grant you a Fiery, Frost, or Arcane enchants for 20 sec. + 'stat':'stats', + 'value': {'haste': 0, 'crit': 0, 'mastery': 0}, #TODO: needs special modeling, you get only one stat per proc, but can have multiple at the same time + 'duration': 20, + 'proc_name': 'Triumvirate', + 'scaling': 2.069368 / 3, #FIXME: for now using 1/3 for each stat / assume we get all 3 for 1/3 each + 'crm_scales': True, + 'item_level': 875, + 'source': 'trinket', + 'type': 'rppm', + 'proc_rate': 0.7, + 'trigger': 'all_attacks', + }, + + 'eye_of_command': { #Equip: Your melee auto attacks increase your Critical Strike by 148 for 10 sec, stacking up to 10 times. This effect is reset if you auto attack a different target. + 'stat':'stats', + 'value': {'crit': 0}, #rpp-scaled + 'duration': 10, #decays when autoattacking different target, assume we can ignore + 'max_stacks': 10, + 'proc_name': "Legion's Gaze", + 'scaling': 0.072857, + 'crm_scales': True, + 'item_level': 860, + 'source': 'trinket', + 'type': 'icd', + 'icd': 0, # stacks with every autohit + 'proc_rate': 1, + 'trigger': 'auto_attacks', + }, + + 'faulty_countermeasure': { #Use: Sheathe your weapons in ice for 30 sec, giving your attacks a chance to cause X additional Frost damage and slow the target's movement speed by 30% for 8 sec. (2 Min Cooldown) + 'stat':'ability_modifier', #modeled in add_special_procs_damage + 'value': 0, #rpp-scaled + 'duration': 30, + 'proc_name': 'Sheathed in Frost', + 'dmg_school': 'frost', + 'scaling': 17.47413, + 'item_level': 805, + 'type': 'icd', + 'source': 'trinket', + 'proc_rate': 1, + 'icd': 120, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + + 'giant_ornamental_pearl': { #Use: Become enveloped by a Gaseous Bubble that absorbs up to X damage for 8 sec. When the bubble is consumed or expires, it explodes and deals Y Frost damage to all nearby enemies within 10 yards. (1 Min Cooldown) + 'stat':'spell_damage', + 'dmg_school': 'frost', + 'value': 0, #rpp-scaled + 'aoe': True, + 'duration': 0, + 'proc_name': 'Gaseous Bubble', + 'dmg_school': 'frost', + 'scaling': 55.83131, + 'item_level': 805, + 'type': 'icd', + 'source': 'trinket', + 'icd': 60, + 'proc_rate': 1, + 'can_crit': True, + 'trigger': 'all_attacks', + }, + + 'horn_of_valor': { #Use: Sound the horn, increasing your primary stat by X for 30 sec. (2 Min Cooldown) + 'stat':'stats', + 'value': {'agi': 0}, #rpp-scaled + 'duration': 30, + 'proc_name': "Valarjar's Path", + 'scaling': 1.2, + 'item_level': 805, + 'type': 'icd', + 'source': 'trinket', + 'icd': 120, + 'proc_rate': 1, + 'trigger': 'all_attacks', + }, + + 'infernal_alchemist_stone': { #Equip: When you heal or deal damage you have a chance to increase your Strength, Agility, or Intellect by X for 15 sec. Your highest stat is always chosen. 'stat': 'stats', - 'value': {'agi':931}, + 'value': {'agi': 0}, #rpp-scaled 'duration': 15, - 'proc_name': 'Stone of Wind', - 'upgradable': True, - 'scaling': 2.6670000553, - 'item_level': 640, + 'proc_name': 'Infernal Alchemist Stone', + 'scaling': 1.839772, + 'item_level': 815, + 'type': 'rppm', #yes, it is rppm now + 'source': 'trinket', + 'proc_rate': 1, + 'trigger': 'all_attacks' + }, + + 'infernal_cinders': { #Your melee attacks have a chance to deal an additional 82910 Fire damage. The critical strike chance of this damage is increased by 10% for each ally within 10 yds who bears this item. + 'stat':'spell_damage', + 'value': 0, #rpp-scaled + 'duration': 0, + 'proc_name': 'Infernal Cinders', + 'dmg_school': 'fire', + 'scaling': 20.09453, + 'item_level': 900, + 'type': 'rppm', + 'source': 'trinket', + 'proc_rate': 10, + 'haste_scales': True, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + + 'kiljaedens_burning_wish': { #Use: Launch a vortex of destruction that seeks your current enemy. When it reaches the target, it explodes, dealing a critical strike to all enemies within 10 yds for X Fire damage. (1 Min, 15 Sec Cooldown) + 'stat':'spell_damage', + 'dmg_school': 'fire', + 'value': 0, #rpp-scaled + 'aoe': True, + 'duration': 0, + 'proc_name': "Kil'jaeden's Burning Wish", + 'scaling': 70 * 2, #always crits, hotfixed value + 'item_level': 910, 'type': 'icd', 'source': 'trinket', - 'icd': 55, - 'proc_rate': 0.35, + 'proc_rate': 1, + 'icd': 75, 'trigger': 'all_attacks' }, - 'stone_of_the_earth': { + + 'mark_of_dargrul': { #Equip: Your melee attacks have a chance to trigger a Landslide, dealing X Physical damage to all enemies directly in front of you. + 'stat':'physical_damage', + 'value': 0, #rpp-scaled + 'aoe': True, + 'duration': 0, + 'proc_name': 'Landslide', + 'scaling': 21.66943, + 'item_level': 805, + 'type': 'rppm', + 'source': 'trinket', + 'proc_rate': 4, + 'icd': 2, + 'haste_scales': True, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + + 'memento_of_angerboda': { #Equip: Your melee attacks have a chance to activate Screams of the Dead, granting you a random combat enhancement for 8 sec. + 'stat':'stats', + 'value': {'mastery': 0, 'crit': 0, 'haste': 0}, #TODO: actually 1-3 stat buffs each time + 'duration': 8, + 'proc_name': 'Screams of the Dead', + 'scaling': 2.297781 / 3, #FIXME: for now using 1/3 for each stat, similar to entwined elemental foci + 'crm_scales': True, + 'item_level': 805, + 'source': 'trinket', + 'type': 'rppm', + 'proc_rate': 1.5, + 'trigger': 'all_attacks', + }, + + 'natures_call': { #Equip: Your melee attacks have a chance to grant you a blessing of one of the Allies of Nature for 10 sec. + 'stat':'stats', + 'value': {'mastery': 0, 'crit': 0, 'haste': 0}, #rpp-scaled, TODO: needs special modeling, you get only one stat per proc, but can have multiple at the same time + 'duration': 10, + 'proc_name': 'Allies of Nature', + 'scaling': 1.378778 / 3, #FIXME: for now using 1/3 for each stat, similar to entwined elemental foci + 'crm_scales': True, + 'item_level': 850, + 'source': 'trinket', + 'type': 'rppm', + 'proc_rate': 2, + 'can_crit': True, #TODO: according to wowhead this can also proc Cleansed Drake's Breath for (scale factor 48.72993) damage + 'trigger': 'all_attacks', + }, + + 'nightblooming_frond': { #Equip: Your attacks have a chance to grant Recursive Strikes for 15 sec, causing your auto attacks to deal an additional X damage and increase the intensity of Recursive Strikes. + 'stat':'ability_modifier', + 'value': 0, #rpp-scaled, modeled in add_special_procs_damage + 'duration': 15, + 'max_stacks': 15, + 'dmg_school': 'physical', + 'proc_name': 'Recursive Strikes', + 'scaling': 2.12, + 'item_level': 875, + 'source': 'trinket', + 'type': 'rppm', + 'proc_rate': 1, + 'can_crit': True, + 'trigger': 'all_attacks', + }, + + 'nightmare_egg_shell': { #Equip: Your melee attacks have a chance to grant you X Haste every 1 sec for 20 sec. + 'stat':'stats', + 'value': {'haste': 0}, #rpp-scaled + 'duration': 20, + 'proc_name': 'Down Draft', + 'scaling': 0.187677 * 10.5, # avg should be 10.5 stacks for 20 sec, melee attacks only needed to proc, not for stacks + 'item_level': 805, + 'source': 'trinket', + 'type': 'rppm', + 'icd': 20, + 'proc_rate': .7, + 'trigger': 'all_attacks', + }, + + 'ravaged_seed_pod': { #Use: Contaminate the ground beneath your feet for 10 sec, dealing X Shadow damage to enemies in the area each second. While you remain in this area, you gain Y Leech. (1 Min Cooldown) + 'stat':'spell_damage', + 'value': 0, #rpp-scaled + 'aoe': True, + 'dmg_school': 'shadow', + 'duration': 10, + 'proc_name': 'Infested Ground', + 'scaling': 6.624573, + 'item_level': 850, + 'type': 'icd', + 'icd': 60, + 'source': 'trinket', + 'proc_rate': 1, + }, + + 'six_feather_fan': { #Equip: Your attacks have a chance to launch a volley of 6 Wind Bolts, each dealing X Nature damage and slowing your target by 30% for 6 sec. + 'stat':'spell_dot', + 'dmg_school': 'nature', + 'value': 0, #rpp-scaled + 'duration': 5, + 'dot_ticks': 6, + 'dot_initial_tick': True, + 'proc_name': 'Wind Bolt', + 'scaling': 19.01865, #6 bolts, one every second + 'item_level': 810, + 'type': 'rppm', + 'source': 'trinket', + 'proc_rate': 1, + 'haste_scales': True, + 'can_crit': True + }, + + 'specter_of_betrayal': { #Use: Create a Dread Reflection at your location for 1 min and cause each of your Dread Reflections to unleash a torrent of magic that deals (111484 * 4) Shadow damage over 3 sec, split evenly among nearby enemies. (45 Sec Cooldown) + 'stat':'spell_damage', + 'dmg_school': 'shadow', + 'value': 0, #rpp-scaled + 'duration': 0, #modeled all-in-one + 'proc_name': 'Dread Torrent', + 'scaling': 4 * 24.6155, + 'item_level': 900, + 'type': 'icd', + 'source': 'trinket', + 'proc_rate': 1, + 'icd': 45, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + + 'spiked_counterweight': { #Your melee attacks have a chance to deal X Physical damage and increase all damage the target takes from you by 15% for 15 sec, up to Y extra damage dealt. + 'stat':'physical_damage', + 'value': 0, #rpp-scaled + 'duration': 0, + 'proc_name': 'Brutal Haymaker', + 'scaling': 49.4631 + 185.487, #scaling for initial + extra damage. can we just add full extra dmg? what about crits? + 'item_level': 805, + 'type': 'rppm', + 'source': 'trinket', + 'proc_rate': .92, + }, + + 'splinters_of_agronax': { #Equip: Your attacks have a chance to imbed Fel Barbs into your target, dealing X Fire damage over 6 sec. + 'stat': 'spell_dot', + 'dmg_school': 'fire', + 'dot_ticks': 6, + 'can_crit': True, + 'value': 0, #rpp-scaled + 'duration': 6, + 'proc_name': 'Fel Barbs', + 'scaling': 5.075319, + 'item_level': 845, + 'type': 'rppm', + 'haste_scales': True, + 'proc_rate': 3.5, + 'source': 'trinket', + }, + + 'spontaneous_appendages': { #Equip: Your melee attacks have a chance to generate extra appendages for 12 sec that attack nearby enemies for X Physical damage every 0.75 sec. + 'stat':'physical_dot', + 'value': 0, #rpp-scaled + 'aoe': True, + 'duration': 12, + 'proc_name': 'Horrific Slam', #not the proc name but the dmg + 'can_crit': True, + 'scaling': 10.1246, #hotfixed value + 'dot_ticks': 16, + 'item_level': 850, + 'type': 'rppm', + 'source': 'trinket', + 'proc_rate': .7, + 'haste_scales': True, + }, + + 'tempered_egg_of_serpentrix': { #Equip: Your attacks have a chance to summon a Spawn of Serpentrix to assist you. + 'stat':'spell_dot', + 'dmg_school': 'fire', + 'value': 0, #rpp-scaled + 'duration': 15, + 'dot_ticks': 8, #pet might be scaling with haste, but most logs have 8 magma spits, assume that for now + 'proc_name': 'Magma Spit', #not the proc name but the dmg of the add + 'scaling': 8.235604, + 'item_level': 805, + 'source': 'trinket', + 'type': 'rppm', + 'proc_rate': 1, + 'can_crit': True, + 'haste_scales': True, + 'trigger': 'all_attacks', + }, + + 'terrorbound_nexus': { #Equip: Your melee attacks have a chance to unleash 4 Shadow Waves that deal X Shadow damage to enemies in their path. The waves travel 15 yards away from you, and then return. + 'stat':'spell_damage', + 'dmg_school': 'shadow', + 'value': 0, #rpp-scaled + 'aoe': True, + 'duration': 0, + 'proc_name': 'Shadow Wave', + 'scaling': 46.22871 * 2., #judging from logs wave damage only occurs twice (forth and back), 4 waves seem to be visual + 'item_level': 805, + 'type': 'rppm', + 'source': 'trinket', + 'proc_rate': 1, + 'icd': 10, + 'haste_scales': True, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + + 'the_devilsaurs_bite': { #Equip: Your attacks have a chance to inflict X Physical damage and stun the target for 1 sec. + 'stat':'physical_damage', + 'value': 0, #rpp-scaled + 'duration': 0, + 'proc_name': "Devilsaur's Bite", + 'scaling': 65., + 'item_level': 805, + 'type': 'rppm', + 'source': 'trinket', + 'proc_rate': 2, + 'haste_scales': True, + 'can_crit': True + }, + + 'tiny_oozeling_in_a_jar': { #Equip: Your melee attacks have a chance to grant you Congealing Goo, stacking up to 6 times. Use: Consume all Congealing Goo to vomit on enemies in front of you for 3 sec, inflicting X Nature damage per Goo consumed. (20 Sec Cooldown) + 'stat':'special_model', + 'dmg_school': 'nature', + 'value': 0, #rpp-scaled, trinket damage modeled in add_special_procs_damage + 'aoe': True, + 'duration': 0, + 'proc_name': 'Fetid Regurgitation', + 'scaling': 2.853606 * 6, #hit per stack, hitting for scaled value every 0.5 for 3 seconds = 6 ticks + 'item_level': 805, + 'type': 'icd', + 'source': 'trinket', + 'proc_rate': 1, #for on use, rppm for stacks is 3 (w/ haste), max is 6 stacks + 'icd': 20, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + + 'tirathons_betrayal': { #Use: Empower yourself with dark energy, causing your attacks to have a chance to inflict 38847 additional Shadow damage and grant you a shield for 38847. Lasts 15 sec. (1 Min, 15 Sec Cooldown) + 'stat':'ability_modifier', #modeled in add_special_procs_damage + 'value': 0, + 'duration': 15, + 'proc_name': 'Darkstrikes', + 'dmg_school': 'shadow', + 'scaling': 16.11315, + 'item_level': 805, + 'type': 'icd', + 'source': 'trinket', + 'proc_rate': 1, + 'icd': 75, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + + 'toe_knees_promise': { #Use: Create a Flame Gale at an enemy's location, dealing X Fire damage over 8 sec. If Flame Gale strikes an enemy affected by Thunder Ritual, Flame Gale's damage is increased by 30%, and its radius by 50%. (1 Min Cooldown) + 'stat':'spell_damage', + 'dmg_school': 'fire', + 'value': 0, #rpp-scaled, TODO: only modeled base damage without Thunder Ritual + 'duration': 0, + 'proc_name': 'Flame Gale', + 'scaling': 9.768856 * 8., + 'item_level': 855, + 'source': 'trinket', + 'type': 'icd', + 'icd': 60, + 'proc_rate': 1, + 'can_crit': True, + 'trigger': 'all_attacks', + }, + + 'umbral_moonglaives': { #Use: Conjure a storm of glaives at your location, causing 125220 Arcane damage every 1 sec to nearby enemies. After 8 sec the glaives shatter, causing another 313052 Arcane damage to enemies in the area. (1 Min, 30 Sec Cooldown) + 'stat':'spell_damage', + 'dmg_school': 'arcane', + 'value': 0, #rpp-scaled + 'aoe': True, + 'duration': 0, #modeled all-in-one + 'proc_name': 'Umbral Glaive Storm', + 'scaling': 8 * 30.34914 + 75.87286, + 'item_level': 900, + 'type': 'icd', + 'source': 'trinket', + 'proc_rate': 1, + 'icd': 90, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + + 'vial_of_ceaseless_toxins': { #Use: Inflict 225700 Shadow damage to an enemy in melee range, plus 366752 damage over 20 sec. If they die while this effect is active, the cooldown of this ability is reduced by 45 sec. (1 Min Cooldown) + 'stat':'spell_damage', + 'dmg_school': 'shadow', + 'value': 0, #rpp-scaled + 'duration': 0, #modeled all-in-one + 'proc_name': 'Ceaseless Toxin', + 'scaling': 10 * 8.888798 + 54.70188, + 'item_level': 900, + 'type': 'icd', + 'source': 'trinket', + 'proc_rate': 1, + 'icd': 60, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + + 'void_stalkers_contract': { #Use: Call upon two Void Stalkers to strike your target from two directions inflicting up to (209877 * 2) Physical damage to all enemies in their paths. (1 Min, 30 Sec Cooldown) + 'stat':'physical_damage', + 'value': 0, #rpp-scaled + 'aoe': True, + 'duration': 0, + 'proc_name': 'Void Slash', + 'scaling': 84.93605, + 'item_level': 845, + 'type': 'icd', + 'source': 'trinket', + 'icd': 90, + 'proc_rate': 1, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + + 'windscar_whetstone': { #Use: A Slicing Maelstrom surrounds you, inflicting X Physical damage to nearby enemies over 6 sec. (2 Min Cooldown) + 'stat':'physical_damage', + 'value': 0, #rpp-scaled + 'aoe': True, + 'duration': 0, + 'proc_name': 'Slicing Maelstrom', + 'scaling': 19.93953 * 7., # 7 hits + 'item_level': 805, + 'type': 'icd', + 'source': 'trinket', + 'icd': 120, + 'proc_rate': 1, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + + ### Antorus Procs ### + 'amanthuls_vision': { #PROXY PROC, only used to set empowerment + 'stat': 'special_model', + 'value': 0, + 'duration': 0, + 'proc_name': 'Aman\'thul Proxy' + }, + 'amanthuls_vision_empowered': { #When empowered by the Pantheon, your primary stat is increased by X for 15 sec. + 'stat':'stats', + 'value': {'agi': 0}, #rpp-scaled + 'duration': 15, #Ignored, use precomputed uptime values, set in set_constants + 'proc_name': 'Aman\'thul\'s Grandeur', + 'scaling': 0.639601, + 'item_level': 1000, + 'source': 'trinket', + 'type': 'icd', + 'icd': 100, #modeled as icd, duration will be set to uptime % and icd 100 + 'proc_rate': 1, #Ignore RPPM + 'trigger': 'all_attacks' + }, + + 'golganneths_vitality': { #Your damaging abilities have a chance to create a Ravaging Storm at your target's location, inflicting Nature damage split among all enemies within 6 yds over 6 sec. + 'stat':'spell_damage', + 'dmg_school': 'nature', + 'value': 0, #rpp-scaled + 'duration': 6, + 'proc_name': 'Ravaging Storm', + 'scaling': 14.58708 * 6, # 6 hits + 'item_level': 940, + 'type': 'rppm', + 'source': 'trinket', + 'proc_rate': 1.8, + 'can_crit': True, + 'haste_scales': True, + 'trigger': 'all_attacks' + }, + 'golganneths_vitality_empowered': { #When empowered by the Pantheon, your autoattacks cause an explosion of lightning dealing [(Mainhand weapon base speed) * X] Nature damage to all enemies within 8 yds of the target. Lasts 15 sec. + 'stat':'special_model', + 'dmg_school': 'nature', + 'aoe': True, + 'value': 0, #rpp-scaled + 'duration': 15, #Ignored, use precomputed uptime values + 'proc_name': 'Golganneth\'s Thunderous Wrath', + 'scaling': 7.98956, + 'item_level': 940, + 'type': 'rppm', + 'source': 'trinket', + 'proc_rate': 0, #Ignore RPPM, special pantheon formula in add_special_procs_damage + 'can_crit': True, + 'haste_scales': True, + 'trigger': 'all_attacks' + }, + + #Other Legion procs + 'jacins_ruse_2pc': { #Equip: Your spells and attacks have a chance to increase your Mastery by 3000 for 15 sec. + 'stat':'stats', + 'value':{'mastery':3000}, + 'duration':15, + 'proc_name': "Jacin's Ruse", + 'item_level': 820, + 'type': 'rppm', + 'source': 'unique', + 'icd': 0, + 'proc_rate': 1, + 'can_crit': False, + 'trigger': 'all_attacks' + }, + + 'march_of_the_legion_2pc': { #Equip: Your spells and attacks against Demons have a chance to deal an additional 27200 to 36800 Fire damage. + 'stat':'spell_damage', + 'value': 35000, + 'duration':0, + 'proc_name': "March of the Legion", + 'item_level': 820, + 'type': 'rppm', + 'source': 'unique', + 'icd': 0, + 'proc_rate': 6, + 'haste_scales': True, + 'can_crit': True, + 'trigger': 'all_attacks' + }, + + 'rogue_orderhall_8pc': { #Your finishing moves have a chance to increase your Haste by 2000 for 12 sec. 'stat': 'stats', - 'value': {'agi':1069}, + 'value': {'haste': 2000}, + 'duration': 12, + 'proc_name': "Jacin's Ruse", + 'type': 'rppm', + 'source': 'unique', + 'proc_rate': 2, + 'trigger': 'all_attacks' #should be only finishing moves, but since it's rppm that doesn't matter + }, + + 'concordance_of_the_legionfall': { + 'stat': 'stats', + 'value': {}, #set depending on traits in determine_stats + 'duration': 10, + 'proc_name': 'Concordance of the Legionfall', + 'type': 'rppm', + 'source': 'trait', + 'proc_rate': 1.37, + 'icd': 10, + 'trigger': 'all_attacks' + }, + + #6.2.3 procs + 'infallible_tracking_charm': { + 'stat':'spell_damage', + 'value': 42872, + 'duration': 0, + 'proc_name': "Cleansing Flame", + #'scaling': 61.1583452211, + 'item_level': 715, + 'type': 'rppm', + 'source': 'trinket', + 'proc_rate': 3, + 'haste_scales': False, + 'can_crit': False, + 'trigger': 'all_attacks' + }, + + 'infallible_tracking_charm_mod': { + 'stat':'damage_modifier', + 'value': {'damage_mod': 10}, + 'proc_name': "Cleansing Flame", + 'scaling': 0.0, + 'item_level': 715, + 'type': 'rppm', + 'source': 'trinket', + 'duration': 5, + 'proc_rate': 3, + 'haste_scales': False, + 'trigger': 'all_attacks' + }, + #6.2 procs + 'maalus': { + 'stat': 'damage_modifier', + 'value': {'damage_mod': 2500}, 'duration': 15, - 'proc_name': 'Stone of the Earth', + 'proc_name': 'Maalus', 'upgradable': True, - 'scaling': 2.6670000553, - 'item_level': 655, + 'scaling': 2.95857988166, + 'item_level': 735, 'type': 'icd', 'source': 'trinket', - 'icd': 55, - 'proc_rate': 0.35, + 'icd': 120, + 'proc_rate': 1.0, 'trigger': 'all_attacks' }, - 'stone_of_the_waters': { + + 'felmouth_frenzy': { + 'stat':'spell_damage', + 'value': 1, + 'duration': 0, + 'proc_name': 'Fel Lash', + 'scaling': 0.0, + 'type': 'rppm', + 'source': 'unique', + 'icd': 0, + 'proc_rate': 2, + 'haste_scales': True, + 'can_crit': False, + 'trigger': 'all_attacks' + }, + + 'malicious_censer': { 'stat': 'stats', - 'value': {'agi':1229}, - 'duration': 15, - 'proc_name': 'Stone of the Waters', + 'value': {'agi':1093}, + 'duration': 20, + 'proc_name': 'Malicious Censer', 'upgradable': True, - 'scaling': 2.6670000553, - 'item_level': 670, + 'scaling': 1.79180327869, + 'item_level': 700, + 'type': 'rppm', + 'source': 'trinket', + 'icd': 0, + 'proc_rate': 1.0, + 'trigger': 'all_attacks' + }, + + 'soul_capacitor': { + 'stat': 'damage_modifier', + 'value': {'damage_mod': 2677}, + 'duration': 10, + 'proc_name': 'Soul Capacitor', + 'upgradable': True, + 'scaling': 4.59965635, + 'item_level': 695, + 'type': 'rppm', + 'source': 'trinket', + 'icd': 0, + 'proc_rate': 1.0, + 'trigger': 'all_attacks' + }, + + 'mirror_of_the_blademaster': { + 'stat': 'physical_damage', + 'value': {'damage': 1}, + 'duration': 20, + 'proc_name': 'Mirror of the Blademaster', + 'upgradable': True, + 'scaling': 1.0, + 'item_level': 695, 'type': 'icd', 'source': 'trinket', - 'icd': 55, - 'proc_rate': 0.35, + 'icd': 60, + 'proc_rate': 1.0, 'trigger': 'all_attacks' }, - 'stone_of_fire': { + + 'bleeding_hollow_toxin_vessel': { + 'stat': 'ability_modifer', + 'value': {'ability_mod':5149}, + 'duration': 0, + 'proc_name': 'Bleeding Hollow Toxin Vessel', + 'upgradable': True, + 'scaling': 8.05790297, + 'item_level': 705, + 'type': 'perk', + 'source': 'trinket', + 'icd': 0, + 'proc_rate': 0.0, + 'trigger': 'all_attacks' + }, + + #all alchemy trinket upgrades are just scales + #with different names, collapsed into single proc + 'alchemy_stone': { 'stat': 'stats', 'value': {'agi':1350}, 'duration': 15, - 'proc_name': 'Stone of Fire', + 'proc_name': 'Alchemy Trinket Proc', 'upgradable': True, 'scaling': 2.6670000553, 'item_level': 680, @@ -260,7 +1164,7 @@ }, 'scales_of_doom': { 'stat': 'stats', - 'value': {'multistrike':1743}, + 'value': {'mastery':1743}, 'duration': 10, 'proc_name': 'Scales of Doom', 'upgradable': True, @@ -274,7 +1178,7 @@ }, 'blackheart_enforcers_medallion': { 'stat': 'stats', - 'value': {'multistrike':1665}, + 'value': {'haste':1665}, 'duration': 10, 'proc_name': 'Blackheart Enforcers Medallion', 'upgradable': True, @@ -302,7 +1206,7 @@ }, 'beating_heart_of_the_mountain': { 'stat': 'stats', - 'value': {'multistrike':1467}, + 'value': {'crit':1467}, 'duration': 20, 'proc_name': 'Beating Heart of the Mountain', 'upgradable': True, @@ -358,7 +1262,7 @@ }, 'gorashans_lodestone_spike': { 'stat': 'stats', - 'value': {'multistrike':1060}, + 'value': {'crit':1060}, 'duration': 15, 'proc_name': 'Gorashans Lodestone Spike', 'upgradable': True, @@ -524,28 +1428,13 @@ 'proc_rate': 1.0, 'trigger': 'all_attacks' }, - #5.4 procs - 'assurance_of_consequence': { #DBC - 197491 - 'stat': 'stats', - 'value': {'agi':268}, - 'duration': 20, - 'proc_name': 'Assurance of Consequence', - 'upgradable': True, - 'scaling': 4.000, - 'item_level': 572, - 'type': 'icd', - 'source': 'trinket', - 'icd': 115, - 'proc_rate': 0.15, - 'trigger': 'all_attacks' - }, } allowed_melee_enchants = { #6.0 'mark_of_the_frostwolf': { 'stat': 'stats', - 'value': {'multistrike':500}, + 'value': {'crit':500}, 'duration': 6, 'max_stacks': 2, 'proc_name': 'Mark of the Frostwolf', @@ -595,7 +1484,7 @@ }, 'mark_of_warsong': { 'stat': 'stats', - 'value': {'haste':5.5*100}, + 'value': {'haste':5.5 * 100}, 'duration': 20, 'proc_name': 'Mark of the Bleeding Hollow', 'type': 'rppm', diff --git a/shadowcraft/objects/procs.py b/shadowcraft/objects/procs.py index 078b6cf..f03b9b5 100755 --- a/shadowcraft/objects/procs.py +++ b/shadowcraft/objects/procs.py @@ -1,9 +1,13 @@ +from builtins import object from shadowcraft.core import exceptions from shadowcraft.objects import proc_data from shadowcraft.objects import class_data import sys, traceback +import gettext +_ = gettext.gettext + class InvalidProcException(exceptions.InvalidInputException): pass @@ -11,7 +15,8 @@ class InvalidProcException(exceptions.InvalidInputException): class Proc(object): def __init__(self, stat, value, duration, proc_name, max_stacks=1, can_crit=True, stats=None, upgradable=False, scaling=None, buffs=None, base_value=0, type='rppm', icd=0, proc_rate=1.0, trigger='all_attacks', haste_scales=False, item_level=1, - on_crit=False, on_procced_strikes=True, proc_rate_modifier=1., source='generic', att_spd_scales=False,): + on_crit=False, on_procced_strikes=True, proc_rate_modifier=1., source='generic', att_spd_scales=False, + ap_coefficient=0., dmg_school=None, crm_scales=False, aoe=False, dot_ticks=1, dot_initial_tick=False): self.stat = stat if stats is not None: self.stats = set(stats) @@ -36,17 +41,34 @@ def __init__(self, stat, value, duration, proc_name, max_stacks=1, can_crit=True self.on_crit = on_crit self.on_procced_strikes = on_procced_strikes self.proc_rate_modifier = proc_rate_modifier - + self.ap_coefficient = ap_coefficient + self.dmg_school = dmg_school + self.crm_scales = crm_scales + self.aoe = aoe + self.dot_ticks = dot_ticks + self.dot_initial_tick = dot_initial_tick + + if self.dmg_school is None and stat in ['physical_damage', 'physical_dot']: + self.dmg_school = 'physical' + #separate method just to keep the constructor clean self.update_proc_value() - + def update_proc_value(self): tools = class_data.Util() #http://forums.elitistjerks.com/topic/130561-shadowcraft-for-mists-of-pandaria/page-3 #see above for stat value initialization - if self.source in ('trinket',): - for e in self.value: - self.value[e] = round(self.scaling * tools.get_random_prop_point(self.item_level)) + #not sure if this is the correct way to handle damage procs. Most seem to have both disabled scaling and raw value or both enabled scaling and an {object:value}, + #they should probably all use the same notation either way. If we want both, the scaling property should probably be set by value property instead of manually configured. + #the other option is to always handle it deeper into the calc module, but that is coupling object responsibilities and not ideal. + if self.scaling and self.source in ('trinket',): + crm = tools.get_combat_rating_multiplier(self.item_level) if self.crm_scales else 1. # apply combat rating modifier + scaled_value = round(self.scaling * tools.get_random_prop_point(self.item_level) * crm) + if hasattr(self.value,'__iter__'): #handle object value + for e in self.value: + self.value[e] = scaled_value + else: #handle raw value + self.value = scaled_value def procs_off_auto_attacks(self): if self.trigger in ('all_attacks', 'auto_attacks', 'all_spells_and_attacks', 'all_melee_attacks'): @@ -107,22 +129,39 @@ def procs_off_procced_strikes(self): return True else: return False - - def get_rppm_proc_rate(self, haste=1.): + + def get_base_proc_rate_for_spec(self, spec): + proc_rate = self.proc_rate + if hasattr(self.proc_rate,'__iter__'): # list of proc rates by spec + if not spec: + raise InvalidProcException(_('Spec expected for the proc rate of {proc}').format(proc=self.proc_name)) + if not spec in self.proc_rate: + if 'other' in self.proc_rate: + spec = 'other' + else: + raise InvalidProcException(_('Proc rate of {proc} not found for current spec').format(proc=self.proc_name)) + proc_rate = self.proc_rate[spec] + return proc_rate + + def get_rppm_proc_rate(self, haste=1., spec=None): + if not self.haste_scales: #Failsafe to allow passing haste for non-scling procs + haste = 1. if self.is_real_ppm(): - return haste * self.proc_rate * self.proc_rate_modifier + proc_rate = self.get_base_proc_rate_for_spec(spec) + return haste * proc_rate * self.proc_rate_modifier raise InvalidProcException(_('Invalid proc handling for proc {proc}').format(proc=self.proc_name)) - - def get_proc_rate(self, speed=None, haste=1.0): + + def get_proc_rate(self, speed=None, haste=1.0, spec=None): + proc_rate = self.get_base_proc_rate_for_spec(spec) if self.is_ppm(): if speed is None: raise InvalidProcException(_('Weapon speed needed to calculate the proc rate of {proc}').format(proc=self.proc_name)) else: - return self.proc_rate * speed / 60. + return proc_rate * speed / 60. elif self.is_real_ppm(): - return haste * self.proc_rate / 60. + return haste * proc_rate / 60. else: - return self.proc_rate + return proc_rate def is_ppm(self): if self.type == 'ppm': @@ -131,7 +170,7 @@ def is_ppm(self): return False # probably should configure this somehow, but type check is probably enough raise InvalidProcException(_('Invalid data for proc {proc}').format(proc=self.proc_name)) - + def is_rppm(self): return self.is_real_ppm() def is_real_ppm(self): @@ -158,7 +197,7 @@ def __init__(self, *args): def set_proc(self, proc): setattr(self, proc, Proc(**self.allowed_procs[proc])) - + def del_proc(self, proc): setattr(self, proc, False) @@ -167,7 +206,7 @@ def __getattr__(self, proc): if proc in self.allowed_procs: return False object.__getattribute__(self, proc) - + def get_all_procs_for_stat(self, stat=None): procs = [] for proc_name in self.allowed_procs: @@ -177,7 +216,6 @@ def get_all_procs_for_stat(self, stat=None): procs.append(proc) elif proc.stat in ('stats', 'highest', 'random') and stat in proc.value: procs.append(proc) - return procs def get_all_damage_procs(self): @@ -185,7 +223,7 @@ def get_all_damage_procs(self): for proc_name in self.allowed_procs: proc = getattr(self, proc_name) if proc: - if proc.stat in ('spell_damage', 'physical_damage'): + if proc.stat in ('spell_damage', 'physical_damage', 'physical_dot', 'spell_dot'): procs.append(proc) return procs diff --git a/shadowcraft/objects/race.py b/shadowcraft/objects/race.py index 8613ead..097a175 100755 --- a/shadowcraft/objects/race.py +++ b/shadowcraft/objects/race.py @@ -1,5 +1,11 @@ +from builtins import map +from builtins import zip +from builtins import object from shadowcraft.core import exceptions +import gettext +_ = gettext.gettext + class InvalidRaceException(exceptions.InvalidInputException): pass @@ -10,6 +16,7 @@ class Race(object): 85: ( 288, 306, 212, 169, 127), 90: ( 339, 361, 250, 200, 150), 100:(1206, 1284, 890, 711, 533), + 110:(8481, 9030, 6259, 5000, 0), } #(ap,sp) @@ -18,16 +25,14 @@ class Race(object): 85: {'ap': 30, 'sp': 30}, 90: {'ap': 50, 'sp': 50}, 100:{'ap': 120, 'sp': 120}, + 110:{'ap':2243, 'sp': 2243}, } touch_of_the_grave_bonuses = { 80: {'spell_damage': 200}, 90: {'spell_damage': 400}, 100:{'spell_damage': 1000}, - } - versatility_bonuses = { - 0: 1, - 90: 26, - 100: 100, + #TODO: CHECK + 110:{'spell_damage': 1000}, } #Arguments are ap, spellpower:fire, and int @@ -106,7 +111,7 @@ def calculate_rocket_barrage(self, ap, spfi, int): def __init__(self, race, character_class="rogue", level=85): self.character_class = str.lower(character_class) self.race_name = race - if self.race_name not in Race.racial_stat_offset.keys(): + if self.race_name not in list(Race.racial_stat_offset.keys()): raise InvalidRaceException(_('Unsupported race {race}').format(race=self.race_name)) if self.character_class == "rogue": self.stat_set = Race.rogue_base_stats @@ -117,7 +122,7 @@ def __init__(self, race, character_class="rogue", level=85): def set_racials(self): # Set all racials, so we don't invoke __getattr__ all the time - for race, racials in Race.racials_by_race.items(): + for race, racials in list(Race.racials_by_race.items()): for racial in racials: setattr(self, racial, False) for racial in Race.racials_by_race[self.race_name]: @@ -132,14 +137,15 @@ def __setattr__(self, name, value): object.__setattr__(self, name, value) if name == 'level': self._set_constants_for_level() - + def _set_constants_for_level(self): try: self.stats = self.stat_set[self.level] self.activated_racial_data["blood_fury_physical"]["value"] = self.blood_fury_bonuses[self.level]["ap"] self.activated_racial_data["blood_fury_spell"]["value"] = self.blood_fury_bonuses[self.level]["sp"] # this merges racial stats with class stats (ie, racial_stat_offset and rogue_base_stats) - self.stats = map(sum, zip(self.stats, Race.racial_stat_offset[self.race_name])) + self.stats = list(map(sum, list(zip(self.stats, Race.racial_stat_offset[self.race_name])))) + self.set_racials() except KeyError as e: raise InvalidRaceException(_('Unsupported class/level combination {character_class}/{level}').format(character_class=self.character_class, level=self.level)) @@ -149,17 +155,6 @@ def __getattr__(self, name): return False else: object.__getattribute__(self, name) - - def get_stats_from_race(self, level, secondaries=False): - str = Race.rogue_base_stats[level][0] + Race.racial_stat_offset[self.race_name][0] - agi = Race.rogue_base_stats[level][1] + Race.racial_stat_offset[self.race_name][1] - sta = Race.rogue_base_stats[level][2] + Race.racial_stat_offset[self.race_name][2] - int = Race.rogue_base_stats[level][3] + Race.racial_stat_offset[self.race_name][3] - spi = Race.rogue_base_stats[level][4] + Race.racial_stat_offset[self.race_name][4] - if secondaries: - return {'agi':agi, 'str':str, 'sta':sta, 'int':int, 'spi':spi, - 'readiness':0, 'multistrike':0, 'versatility':0, 'haste':0, 'crit':0, 'mastery':0} - return {'agi':agi, 'str':str, 'sta':sta, 'int':int, 'spi':spi} def get_racial_crit(self, is_day=False): crit_bonus = 0 diff --git a/shadowcraft/objects/stats.py b/shadowcraft/objects/stats.py index 6ed96b6..53c17b0 100755 --- a/shadowcraft/objects/stats.py +++ b/shadowcraft/objects/stats.py @@ -1,60 +1,51 @@ +from __future__ import division +from builtins import object +from shadowcraft.objects import buffs from shadowcraft.objects import procs from shadowcraft.objects import proc_data +from shadowcraft.objects import race from shadowcraft.core import exceptions +import gettext +_ = gettext.gettext + class Stats(object): - # For the moment, lets define this as raw stats from gear + race; AP is - # only AP bonuses from gear and level. Do not include multipliers like - # Vitality and Sinister Calling; this is just raw stats. See calcs page - # rows 1-9 from my WotLK spreadsheets to see how these are typically - # defined, though the numbers will need to updated for level 85. - - crit_rating_conversion_values = {60:13.0, 70:14.0, 80:15.0, 85:17.0, 90:23.0, 100:110.0} - haste_rating_conversion_values = {60:9.00, 70:10.0, 80:12.0, 85:14.0, 90:20.0, 100:90.0} - mastery_rating_conversion_values = {60:13.0, 70:14.0, 80:15.0, 85:17.0, 90:23.0, 100:110.0} - multistrike_rating_conversion_values = {60:3.00, 70:4.00, 80:5.00, 85:6.00, 90:14.0, 100:66.0} - readiness_rating_conversion_values = {60:13.0, 70:14.0, 80:15.0, 85:17.0, 90:23.0, 100:110.0} - versatility_rating_conversion_values = {60:13.0, 70:14.0, 80:15.0, 85:17.0, 90:27.0, 100:130.0} - pvp_power_rating_conversion_values = {60:5.00, 70:7.00, 80:8.00, 85:9.00, 90:10.0, 100:49.0} - pvp_resil_rating_conversion_values = {60:9.29, 70:14.65, 80:30.46, 85:92.31, 90:310.0, 100:600.0} - - def __init__(self, mh, oh, procs, gear_buffs, str=0, agi=0, int=0, spirit=0, stam=0, ap=0, crit=0, haste=0, mastery=0, - readiness=0, multistrike=0, versatility=0, level=None, pvp_power=0, pvp_resil=0, pvp_target_armor=None): + # For the moment, lets define this as raw stats from gear + # AP is only AP bonuses from gear (as of Legion usually 0) + # Other base stat bonuses are added in get_character_base_stats + # Multipliers are added in get_character_stat_multipliers + + crit_rating_conversion_values = {60:13.0, 70:14.0, 80:15.0, 85:17.0, 90:23.0, 100:110.0, 110:400.0} + haste_rating_conversion_values = {60:9.00, 70:10.0, 80:12.0, 85:14.0, 90:20.0, 100:100.0, 110:375.0} + mastery_rating_conversion_values = {60:13.0, 70:14.0, 80:15.0, 85:17.0, 90:23.0, 100:110.0, 110:400.0} + versatility_rating_conversion_values = {60:13.0, 70:14.0, 80:15.0, 85:17.0, 90:27.0, 100:130.0, 110:475.0} + + def __init__(self, mh, oh, procs, gear_buffs, str=0, agi=0, int=0, stam=0, ap=0, crit=0, haste=0, mastery=0, + versatility=0, level=None): # This will need to be adjusted if at any point we want to support # other classes, but this is probably the easiest way to do it for # the moment. self.str = str self.agi = agi self.int = int - self.spirit = spirit self.stam = stam self.ap = ap self.crit = crit self.haste = haste self.mastery = mastery - self.readiness = readiness - self.multistrike = multistrike self.versatility = versatility self.mh = mh self.oh = oh - self.procs = procs self.gear_buffs = gear_buffs self.level = level - self.pvp_power = pvp_power - self.pvp_resil = pvp_resil - self.pvp_target_armor = pvp_target_armor + self.procs = procs def _set_constants_for_level(self): - self.procs.level = self.level try: self.crit_rating_conversion = self.crit_rating_conversion_values[self.level] self.haste_rating_conversion = self.haste_rating_conversion_values[self.level] self.mastery_rating_conversion = self.mastery_rating_conversion_values[self.level] - self.multistrike_rating_conversion = self.multistrike_rating_conversion_values[self.level] - self.readiness_rating_conversion = self.readiness_rating_conversion_values[self.level] self.versatility_rating_conversion = self.versatility_rating_conversion_values[self.level] - self.pvp_power_rating_conversion = self.pvp_power_rating_conversion_values[self.level] - self.pvp_resil_rating_conversion = self.pvp_resil_rating_conversion_values[self.level] except KeyError: raise exceptions.InvalidLevelException(_('No conversion factor available for level {level}').format(level=self.level)) @@ -62,7 +53,59 @@ def __setattr__(self, name, value): object.__setattr__(self, name, value) if name == 'level' and value is not None: self._set_constants_for_level() - + + def get_character_base_stats(self, race, traits=None, buffs=None): + base_stats = { + 'str': self.str + race.racial_str, + 'int': self.int + race.racial_int, + 'agi': self.agi + race.racial_agi, + 'ap': self.ap, + 'crit': self.crit, + 'haste': self.haste, + 'mastery': self.mastery, + 'versatility': self.versatility, + } + if buffs is not None: + buff_bonuses = buffs.get_stat_bonuses(race.epicurean) + for bonus in buff_bonuses: + base_stats[bonus] += buff_bonuses[bonus] + + #netherlight crucible t2 + if traits is not None: + insigniaMod = 1.5 if self.gear_buffs.insignia_of_the_grand_army else 1 + if traits.light_speed: + base_stats['haste'] += 500 * traits.light_speed * insigniaMod + if traits.master_of_shadows: + base_stats['mastery'] += 500 * traits.master_of_shadows * insigniaMod + + # Other bonuses + if self.gear_buffs.rogue_orderhall_6pc: + base_stats['agi'] += 500 + + return base_stats + + def get_character_stat_multipliers(self, race): + # assume rogue for gear spec + stat_multipliers = { + 'str': 1., + 'int': 1., + 'agi': self.gear_buffs.gear_specialization_multiplier(), + 'ap': 1, + 'crit': 1. + (0.02 * race.human_spirit), + 'haste': 1. + (0.02 * race.human_spirit), + 'mastery': 1. + (0.02 * race.human_spirit), + 'versatility': 1. + (0.02 * race.human_spirit), + } + return stat_multipliers + + def get_character_stats(self, race, traits=None, buffs=None): + base = self.get_character_base_stats(race, traits, buffs) + mult = self.get_character_stat_multipliers(race) + stats = { } + for stat in base: + stats[stat] = base[stat] * mult[stat] + return stats + def get_mastery_from_rating(self, rating=None): if rating is None: rating = self.mastery @@ -77,31 +120,11 @@ def get_haste_multiplier_from_rating(self, rating=None): if rating is None: rating = self.haste return 1 + rating / (100. * self.haste_rating_conversion) - - def get_readiness_multiplier_from_rating(self, rating=None, readiness_conversion=1): - if rating is None: - rating = self.readiness - return 1. / (1 + (readiness_conversion * rating) / (self.readiness_rating_conversion * 100.)) - - def get_multistrike_chance_from_rating(self, rating=None): - if rating is None: - rating = self.multistrike - return rating / (100. * self.multistrike_rating_conversion) - + def get_versatility_multiplier_from_rating(self, rating=None): if rating is None: rating = self.versatility return 1. + rating / (100. * self.versatility_rating_conversion) - - def get_pvp_power_multiplier_from_rating(self, rating=None): - if rating is None: - rating = self.pvp_power - return 1. + rating / (100. * self.pvp_power_rating_conversion) - - def get_pvp_resil_multiplier_from_rating(self, rating=None): - if rating is None: - rating = self.pvp_resil - return 0.6*(rating/(rating+11727)) # 0% base resil now class Weapon(object): allowed_melee_enchants = proc_data.allowed_melee_enchants @@ -124,7 +147,7 @@ def set_normalization_speed(self): #elif self.type in ['2h_sword', '2h_mace', '2h_axe', 'polearm']: # self._normalization_speed = 3.3 #elif - + # commented out for micro performance's sake # should be re-enabled if other classes ever make use of Shadowcraft if self.type == 'dagger': @@ -186,17 +209,51 @@ class GearBuffs(object): 'rogue_t17_2pc', # Mut and Dispatch crits generate 7 energy, RvS has 20% higher chance to generate a CP, generate 60e when casting ShD 'rogue_t17_4pc', # Envenom generates 1 CP, finishers have a 20% chance to generate 5CP and next Evisc costs 0, 5 CP at the end of ShD 'rogue_t17_4pc_lfr', # 1.1 RPPM, 30% energy generation for 6s + 'rogue_t18_2pc', # Dispatch deals 25% additional damage as Nature damage, SnD internal ticks have 8% change to proc ARfor 4 sec, Vanish awards 5cps and increases all damage done by 30% for 10 sec + 'rogue_t18_4pc', # Dispatch generates +2cps, AR increased damage by 15%, Evis and Rupture reduce the CD of vanish by 1 seconds per CP + 'rogue_t18_4pc_lfr', # Energy increased by 20, 5% increase in energy regen + 'rogue_t19_2pc', # Mutilate causes 30% bleed over 8 seconds, Nightblades lasts additional 2 seconds per CP + 'rogue_t19_4pc', # 10% envenom damage per bleed, 30% SSk generates additional CP if nightblade up + 'rogue_t20_2pc', # Garrote deals 40% increased damage, Symbols of Death increases your damage done by an additional 10%. + 'rogue_t20_4pc', # Garrote's cost is reduced by 25 Energy and cooldown is reduced by 12 sec, Symbols of Death has 5 sec reduced cooldown and generates 2 Energy per sec while active. + 'jacins_ruse_2pc', # Proc 3000 mastery for 15s, 1 rppm + 'march_of_the_legion_2pc', # Proc 35K damage when fighting demons, 6+Haste RPPM + 'journey_through_time_2pc', # The effect from Chrono Shard now increases your movement speed by 30%, and grants an additional 1000 Haste. + 'kara_empowered_2pc', # 30% increase to paired trinkets + 'rogue_orderhall_6pc', # Agility increased by 500 + 'rogue_orderhall_8pc', # Your finishing moves have a chance to increase your Haste by 2000 for 12 sec. + #Legendaries + 'the_dreadlords_deceit', #fok/ssk damage increased by 35% per 2 seconds up to 1 minute + 'duskwalkers_footpads', #Vendetta CD reduced by 1 second for each 65 energy spent + 'thraxis_tricksy_treads', # + 'shadow_satyrs_walk', #3+1/3yd energy refund on ssk + 'insignia_of_ravenholdt', #15% damage as shadow on cp generators + 'zoldyck_family_training_shackles', #Poisons and Bleeds deal 30% additional damage below 30% health + 'greenskins_waterlogged_wristcuffs', # + 'denial_of_the_half_giants', # Finishers extend ShB by 0.3 seconds per cp spent + 'shivarran_symmetry', # + 'mantle_of_the_master_assassin', #100% crit during stealth and for 6 seconds after + 'cinidaria_the_symbiote', #30% additional damage to enemies above 90% health + 'sephuzs_secret', #2% haste + 'the_empty_crown', #Kingsbane generates 40 Energy over 5 sec. + 'the_first_of_the_dead', #For 2 sec after activating Symbols of Death, Shadowstrike generates 3 additional combo points and Backstab generates 4 additional combo points. + 'the_curse_of_restlessness', #NYI + 'soul_of_the_shadowblade', #Gain the Vigor talent. + 'insignia_of_the_grand_army', #Increase the effects of Light and Shadow powers granted by the Netherlight Crucible by 50%. + #Other + 'jeweled_signet_of_melandrus', #Increases your autoattack damage by 10%. + 'gnawed_thumb_ring', #Use: Have a nibble, increasing your healing and magic damage done by 5% for 12 sec. (3 Min Cooldown) ] allowed_buffs = frozenset(other_gear_buffs) - + def __init__(self, *args): for arg in args: if not isinstance(arg, (list,tuple)): arg = (arg,0) if arg[0] in self.allowed_buffs: setattr(self, arg[0], True) - + def __getattr__(self, name): # Any gear buff we haven't assigned a value to, we don't have. @@ -212,12 +269,12 @@ def metagem_crit_multiplier(self): return 1.03 else: return 1 - + def rogue_pvp_4pc_extra_energy(self): if self.rogue_pvp_4pc: return 30 return 0 - + def rogue_t14_2pc_damage_bonus(self, spell): if self.rogue_t14_2pc: bonus = { @@ -225,54 +282,34 @@ def rogue_t14_2pc_damage_bonus(self, spell): ('combat', 'ss', 'sinister_strike'): 0.15, ('subtlety', 'bs', 'backstab'): 0.1 } - for spells in bonus.keys(): + for spells in list(bonus.keys()): if spell in spells: return 1 + bonus[spells] return 1 - + def rogue_t14_4pc_extra_time(self, is_combat=False): if is_combat: return self.rogue_t14_4pc * 6 return self.rogue_t14_4pc * 12 - + def rogue_t15_2pc_bonus_cp(self): if self.rogue_t15_2pc: return 1 return 0 - - def rogue_t15_4pc_reduced_cost(self, uptime= 12. / 180.): #This is for Mut calcs + + def rogue_t15_4pc_reduced_cost(self, uptime=12/180): #This is for Mut calcs cost_reduction = .15 if self.rogue_t15_4pc: return 1. - (cost_reduction * uptime) return 1. - + def rogue_t15_4pc_modifier(self, is_sb=False): #This is for Combat/Sub calcs if self.rogue_t15_4pc and is_sb: return .85 # 1 - .15 return 1. - - def rogue_t16_2pc_bonus(self): - if self.rogue_t16_2pc: - return True - return False - - def rogue_t16_4pc_bonus(self): - if self.rogue_t16_4pc: - return True - return False - - def rogue_t17_2pc_bonus(self): - if self.rogue_t17_2pc: - return True - return False - - def rogue_t17_4pc_bonus(self): - if self.rogue_t17_4pc: - return True - return False - + def gear_specialization_multiplier(self): if self.gear_specialization: return 1.05 else: - return 1 \ No newline at end of file + return 1 diff --git a/shadowcraft/objects/talents.py b/shadowcraft/objects/talents.py index 8535d7f..3315d43 100755 --- a/shadowcraft/objects/talents.py +++ b/shadowcraft/objects/talents.py @@ -1,3 +1,6 @@ +from builtins import str +from builtins import range +from builtins import object from shadowcraft.core import exceptions from shadowcraft.objects import talents_data @@ -7,9 +10,10 @@ class InvalidTalentException(exceptions.InvalidInputException): class Talents(object): - def __init__(self, talent_string, game_class='rogue', level='100'): + def __init__(self, talent_string, class_spec, game_class, level=110): self.game_class = game_class - self.class_talents = talents_data.talents[game_class] + self.class_spec = class_spec + self.class_talents = talents_data.talents[(game_class,class_spec)] self.level = level self.max_rows = 7 self.allowed_talents = [talent for tier in self.class_talents for talent in tier] @@ -28,7 +32,7 @@ def __getattr__(self, name): def get_allowed_talents_for_level(self): allowed_talents_for_level = [] - for i in xrange(self.get_top_tier()): + for i in range(self.get_top_tier()): for talent in self.class_talents[i]: allowed_talents_for_level.append(talent) return allowed_talents_for_level @@ -51,8 +55,9 @@ def initialize_talents(self, talent_string): if len(talent_string) > self.max_rows: raise InvalidTalentException(_('Talent strings must be 7 or less characters long')) j = 0 + self.reset_talents() for i in talent_string: - if int(i) not in range(4): + if int(i) not in list(range(4)): raise InvalidTalentException(_('Values in the talent string must be 0, 1, 2, 3, or sometimes 4')) if int(i) == 0 or i == '.': pass @@ -60,6 +65,20 @@ def initialize_talents(self, talent_string): setattr(self, self.class_talents[j][int(i) - 1], True) j += 1 + def get_talent_string(self): + talent_str = "" + for row in self.class_talents: + got_talent = False + for index, talent in enumerate(row): + if getattr(self, talent): + got_talent = True + talent_str += str(index + 1) + break + if not got_talent: + talent_str += "0" + return talent_str + + def reset_talents(self): for talent in self.allowed_talents: setattr(self, talent, False) @@ -68,22 +87,25 @@ def get_tier_for_talent(self, name): if name not in self.allowed_talents: return None tier = 0 - for i in xrange(self.max_rows): + for i in range(self.max_rows): if name in self.class_talents[i]: return i - def set_talent(self, name): + def set_talent(self, name, value=True): # Clears talents in the tier and sets the new one if name not in self.allowed_talents: - return False + raise InvalidTalentException("Invalid talent") for talent in self.class_talents[self.get_tier_for_talent(name)]: setattr(self, talent, False) - setattr(self, name, True) - + setattr(self, name, value) + + def get_talent(self, name): + return getattr(self, name) + def get_active_talents(self): active_talents = [] for row in self.class_talents: for talent in row: if getattr(self, talent): active_talents.append(talent) - return active_talents \ No newline at end of file + return active_talents diff --git a/shadowcraft/objects/talents_data.py b/shadowcraft/objects/talents_data.py index 87e55bb..372cf7a 100644 --- a/shadowcraft/objects/talents_data.py +++ b/shadowcraft/objects/talents_data.py @@ -1,91 +1,127 @@ talents = { - 'death_knight': ( - ('roiling_blood', 'plague_leech', 'unholy_blight'), - ('lichborne', 'anti-magic_zone', 'purgatory'), - ('deaths_advance', 'chilblains', 'asphyxiate'), - ('death_pact', 'death_siphon', 'conversion'), - ('blood_tap', 'runic_empowerment', 'runic_corruption'), - ('gorefiends_grasp', 'remorseless_winter', 'desecrated_ground') + ('death_knight', 'blood'): ( + ('bloodworms', 'heart_strike', 'consume_vitality'), + ('bloody_reprisal', 'bloodbolt', 'ossuary'), + ('rapid_decomposition', 'red_thirst', 'anti-magic_barrier'), + ('rune_tap', 'purgatory', 'mark_of_blood'), + ('tightening_grasp', 'tremble_before_me', 'march_of_the_damned'), + ('will_of_the_necropolis', 'exhume', 'foul_bulwark'), + ('bonestorm', 'blood_mirror', 'blood_beasts') ), - 'druid': ( - ('feline_swiftness', 'displacer_beast', 'wild_charge'), - ('natures_swiftness', 'renewal', 'cenarion_ward'), - ('faerie_swarm', 'mass_entanglement', 'typhoon'), - ('soul_of_the_forest', 'incarnation', 'force_of_nature'), - ('disorienting_roar', 'ursols_vortex', 'mighty_bash'), - ('heart_of_the_wild', 'dream_of_cenarius', 'natures_vigil') + ('demon_hunter', 'havoc'): ( + ('fel_mastery', 'first_blood', 'blind_fury'), + ('prepared', 'demon_blades', 'master_of_the_glaive'), + ('demon_reborn', 'bloodlet', 'feed_the_demon'), + ('desperate_instincts', 'netherwalk', 'soul_rending'), + ('nemesis', 'chaos_cleave', 'momentum'), + ('improved_chaos_nova', "ill_swallow_your_soul", 'cull_the_weak'), + ('place_holder1', 'place_holder2', 'place_holder3') ), - 'hunter': ( - ('posthaste', 'narrow_escape', 'crouching_tiger_hidden_chimera'), - ('silencing_shot', 'wyvern_sting', 'binding_shot'), - ('exhilaration', 'aspect_of_the_iron_hawk', 'spirit_bond'), - ('fervor', 'readiness', 'thrill_of_the_hunt'), - ('a_murder_of_crows', 'dire_beast', 'lynx_rush'), - ('glaive_toss', 'powershot', 'barrage') + ('druid', 'balance'): ( + ('force_of_nature', 'warrior_of_elune', 'starlord'), + ('renewal', 'displacer_beast', 'wild_charge'), + ('feral_affinity', 'guardian_affinity', 'restoration_affinity'), + ('mighty_bash', 'mass_entanglement', 'typhoon'), + ('soul_of_the_forest', 'incarnation_chosen_of_elune', 'stellar_flare'), + ('shooting_starts', 'astral_communion', 'blessing_of_the_ancients'), + ('collapsing_stars', 'stellar_drift', 'natures_balance') ), - 'mage': ( - ('presence_of_mind', 'scorch', 'ice_floes'), - ('temporal_shield', 'blazing_speed', 'ice_barrier'), - ('ring_of_frost', 'ice_ward', 'frostjaw'), - ('greater_invisibility', 'cauterize', 'cold_snap'), - ('nether_tempest', 'living_bomb', 'frost_bomb'), - ('invocation', 'rune_of_power', 'incanters_ward') + ('hunter', 'beast_mastery'): ( + ('one_with_the_pack', 'way_of_the_cobra', 'dire_stable'), + ('posthaste', 'farstrider', 'dash'), + ('stomp', 'exptic_munitions', 'chimaera_shot'), + ('binding_shot', 'wyvern_sting', 'intimidation'), + ('big_game_hunter', 'bestial_fury', 'blink_strikes'), + ('a_murder_of_crows', 'barrage', 'volley'), + ('stampede', 'killer_cobra', 'aspect_of_the_beast') ), - 'monk': ( - ('celerity', 'tigers_lust', 'momentum'), - ('chi_wave', 'zen_sphere', 'chi_burst'), - ('power_strikes', 'ascension', 'chi_brew'), - ('deadly_reach', 'charging_ox_wave', 'leg_sweep'), - ('healing_elixirs', 'dampen_harm', 'diffuse_magic'), - ('rushing_jade_wind', 'invoke_xuen,_the_white_tiger', 'chi_torpedo') + ('mage', 'arcane'): ( + ('arcane_familiar', 'presence_of_mind', 'torrent'), + ('shimmer', 'cauterize', 'ice_block'), + ('mirror_image', 'rune_of_power', 'incanters_flow'), + ('supernova', 'charged_up', 'words_of_power'), + ('ice_floes', 'ring_of_frost', 'ice_ward'), + ('nether_tempest', 'unstable_magic', 'erosion'), + ('overpowered', 'quickening', 'arcane_orb') ), - 'paladin': ( - ('speed_of_light', 'long_arm_of_the_law', 'pursuit_of_justice'), - ('fist_of_justice', 'repentance', 'burden_of_guilt'), - ('selfless_healer', 'eternal_flame', 'sacred_shield'), - ('hand_of_purity', 'unbreakable_spirit', 'clemency'), - ('holy_avenger', 'sanctified_wrath', 'divine_purpose'), - ('holy_prism', 'lights_hammer', 'execution_sentence') + ('monk', 'windwalker'): ( + ('chi_burst', 'eye_of_the_tiger', 'chi_wave'), + ('chi_torpedo', 'tiger_lust', 'celerity'), + ('energizing_elixer', 'ascension', 'power_strikes'), + ('ring_of_peace', 'dizzying_kicks', 'leg_sweep'), + ('healing_elixirs', 'diffuse_magic', 'dampen_harm'), + ('rushing_jade_wind', 'invoke_xuen_the_white_tiger', 'hit_combo'), + ('chi_orbit', 'spinning_dragon_strike', 'serenity') ), - 'priest': ( - ('void_tendrils', 'psyfiend', 'dominate_mind'), - ('body_and_soul', 'angelic_feather', 'phantasm'), - ('from_darkness_comes_light', 'mindbender', 'power_word:_solace'), + ('paladin', 'retribution'): ( + ('execution_sentence', 'turalyons_might', 'consecration'), + ('the_fires_of_justice', 'crusaders_flurry', 'zeal'), + ('fist_of_justice', 'repentance', 'blinding_light'), + ('virtues_blade', 'blade_of_wrath', 'divine_hammer'), + ('judgements_of_the_bold', 'might_of_the_virtue', 'mass_judgement'), + ('blaze_of_light', 'divine_speed', 'eye_for_an_eye'), + ('final_verdict', 'seal_of_light', 'holy_wrath') + ), + ('priest', 'shadow'): ( + ('twist_of_fate', 'fortress_of_the_mind', 'shadow_word_void'), + ('mania', 'body_and_soul', 'masochism'), + ('mind_bomb', 'psychic_voice', 'dominate_mind'), ('desperate_prayer', 'spectral_guise', 'angelic_bulwark'), ('twist_of_fate', 'power_infusion', 'divine_insight'), ('cascade', 'divine_star', 'halo') ), - 'rogue': ( + ('rogue', 'assassination'): ( + ('master_poisoner', 'elaborate_planning', 'hemorrhage'), + ('nightstalker', 'subterfuge', 'shadow_focus'), + ('deeper_stratagem', 'anticipation', 'vigor'), + ('leeching_poison', 'elusiveness', 'cheat_death'), + ('thuggee', 'prey_on_the_weak', 'internal_bleeding'), + ('toxic_blade', 'alacrity', 'exsanguinate'), + ('venom_rush', 'marked_for_death', 'death_from_above') + ), + ('rogue', 'outlaw'): ( + ('ghostly_strike', 'swordmaster', 'quick_draw'), + ('grappling_hook', 'acrobatic_strikes', 'hit_and_run'), + ('deeper_stratagem', 'anticipation', 'vigor'), + ('iron_stomach', 'elusiveness', 'cheat_death'), + ('parley', 'prey_on_the_weak', 'dirty_tricks'), + ('cannonball_barrage', 'alacrity', 'killing_spree'), + ('slice_and_dice', 'marked_for_death', 'death_from_above') + ), + ('rogue', 'subtlety'): ( + ('master_of_subtlety', 'weaponmaster', 'gloomblade'), ('nightstalker', 'subterfuge', 'shadow_focus'), - ('deadly_throw', 'nerve_strike', 'combat_readiness'), - ('cheat_death', 'leeching_poison', 'elusiveness'), - ('cloak_and_dagger', 'shadowstep', 'burst_of_speed'), - ('prey_on_the_weak', 'internal_bleeding', 'dirty_tricks'), - ('shuriken_toss', 'marked_for_death', 'anticipation'), - ('lemon_zest', 'shadow_reflection', 'death_from_above') + ('deeper_stratagem', 'anticipation', 'vigor'), + ('soothing_darkness', 'elusiveness', 'cheat_death'), + ('strike_from_the_shadows', 'prey_on_the_weak', 'tangled_shadow'), + ('dark_shadow', 'alacrity', 'enveloping_shadows'), + ('master_of_shadows', 'marked_for_death', 'death_from_above') ), - 'shaman': ( - ('natures_guardian', 'stone_bulwark_totem', 'astral_shift'), - ('frozen_power', 'earthgrab_totem', 'windwalk_totem'), - ('call_of_the_elements', 'totemic_restoration', 'totemic_projection'), - ('elemental_mastery', 'ancestral_swiftness', 'echo_of_the_elements'), - ('healing_tide_totem', 'ancestral_guidance', 'conductivity'), - ('unleashed_fury', 'primal_elementalist', 'elemental_blast') + ('shaman', 'elemental'): ( + ('path_of_flame', 'path_of_elements', 'maelstrom_totem'), + ('gust_of_wind', 'fleet_of_foot', 'wind_rush_totem'), + ('lightening_surge_totem', 'earthgrab_totem', 'voodoo_totem'), + ('elemental_blast', 'ancestral_swiftness', 'echo_of_the_elements'), + ('elemental_fusion', 'sons_of_flame', 'magnitude'), + ('lightning_rod', 'storm_elemental', 'liquid_magma_totem'), + ('ascendance', 'primal_elementalist', 'totemic_fury') ), - 'warlock': ( - ('dark_regeneration', 'soul_leech', 'harvest_life'), - ('howl_of_terror', 'mortal_coil', 'shadowfury'), - ('soul_link', 'sacrificial_pact', 'dark_bargain'), - ('blood_fear', 'burning_rush', 'unbound_will'), - ('grimoire_of_supremacy', 'grimoire_of_service', 'grimoire_of_sacrifice'), - ('archimondes_vengeance', 'kiljaedens_cunning', 'mannoroths_fury') + ('affliction', 'warlock'): ( + ('haunt', 'writhe_in_agony', 'drain_soul'), + ('contagion', 'absolute_corruption', 'mana_tap'), + ('soul_leech', 'mortal_coil', 'howl_of_terror'), + ('siphon_life', 'sow_the_seeds', 'soul_harvest'), + ('demonic_circle', 'burning_rush', 'dark_pact'), + ('grimore_of_supremacy', 'grimore_of_service', 'grimore_of_sacrifce'), + ('soul_effigy', 'phantom_singularity', 'demonic_servitude') ), - 'warrior': ( - ('juggernaut', 'double_time', 'warbringer'), - ('enraged_regeneration', 'second_wind', 'impending_victory'), - ('staggering_shout', 'piercing_howl', 'disrupting_shout'), - ('bladestorm', 'shockwave', 'dragon_roar'), - ('mass_spell_reflection', 'safeguard', 'vigilance'), - ('avatar', 'bloodbath', 'storm_bolt') + ('warrior', 'arms'): ( + ('shockwave', 'storm_bolt', 'sweeping_strikes'), + ('impending_victory', 'bounding_stride', 'die_by_the_sword'), + ('dauntless', 'overpower', 'avatar'), + ('second_wind', 'double_time', 'imposing_roar'), + ('fervor_of_battle', 'rend', 'bladestorm'), + ('heroic_strike', 'mortal_combo', 'titanic_might'), + ('anger_management', 'opportunity_strikes', 'ravager') ), } diff --git a/style.md b/style.md new file mode 100644 index 0000000..0961544 --- /dev/null +++ b/style.md @@ -0,0 +1,84 @@ +Style Guideines for ShadowCraft-Engine +====================================== + +These are the code style guidelines that were defined by Aldriana when the +project started. Although, they may or may not have been heeded consistently +throughout the code base, try to keep them in mind when writing new code. + +1. Indents are 4 spaces. Tabs are strictly forbidden. + +2. Avoid trailing whitespace in all cases. And I do mean all cases. + +3. Line length: Try to keep comments to 80 characters. For general code I'm + not going to enforce a strict limit, but if you're going over 120 characters + or so you should think about whether there's a natural way to break it. If + there's not, that's fine, but if there is, that's better. + +4. List comprehensions, lambda functions, map(), reduce(), filter(), etc. are + all fine if they're simple and generally aid code clarity. If you're doing + some hairy nested thing, it's probably better to split it up. + +5. For binary operators (+, -, *, /, %, etc.) put a space around the operator: + + Correct: `a = 1 + 2 * 3` + + Wrong: `a=1+2*3` + + Exception: When assigning a default value for a function parameter, do not + use spaces: + + Correct: `def foo(bar=1):` + + Wrong: `def foo(bar = 1):` + +6. Imports: With the exception of importing something that's in `__init__`, + import the module, not the class. + + Correct: `from calcs import gylphs` + + Slightly Wrong: `import calcs.glyphs` + + Wrong: `from calcs.glyphs import Glyph` + + Very Wrong: `from calcs.glyph import *` + + Imports should also generally be done in alphabetical order. + +7. Try to keep module names distinct to the extent that it's possible to do so + and still have them make sense. It helps if you use descriptive module + names. + +8. Modules names should be lowercase_and_underscores. + +9. Function names should be lowercase_and_underscores. + +10. Class names should be CamelCase. + +11. If a module primarily consists of a single class definition, the module + name and the class name should match. + +12. Any string where there is even the slightest chance it will be shown to an + external user should use named introspection for variables. This is to + make translation, um, possible. + + Correct: `"%(character_name)s is level %(character_level)d" % {'character_name': name, 'character_level': level}` + + Wrong: `"%s is level %d" % (name, level)` + + Very Wrong: `name + ' is level ' + str(level)` + + To explain: in various languages the sentence syntax may require the + variables to be in a different order. Giving them good descriptive names + lets the translators properly rearrange them as needed to convey the proper + meaning. + +13. Comments are a good thing. If what you're doing isn't immediately obvious + from a quick readthrough, add a comment to explain it. Recommended practise + are comments without a space after the #. + +In general: please try to write code that's as readable and maintainable as +possible. You only write the code once, but it will be read many many times. +Hence it's worth spending an extra couple of minutes writing it if it saves the +readers even a few seconds in understanding it. As the saying goes: always +write code as though the person who has to maintain it is a dangerous +psychopath that knows where you live. diff --git a/style.txt b/style.txt deleted file mode 100644 index 5ffd676..0000000 --- a/style.txt +++ /dev/null @@ -1,63 +0,0 @@ -Style guideines for ShadowCraft-Engine - -While we're getting started I'm going to be somewhat lenient about these, as -its pretty important to get the framework roughed out so people can start -building off it; however, once the initial flurry settles down, I will start -enforcing these a bit more strenuously. - -0) Assume for the moment that we're using Python 2.6. If there's some 2.7 - feature that you feel would be a real asset for something you're doing, - let me know and we can discuss it. We are not using 3.x. -1) Indents are 4 spaces. Tabs are strictly forbidden. -2) Avoid trailing whitespace in all cases. And I do mean all cases. -3) Line length: Try to keep comments to 80 characters. For general code I'm - not going to enforce a strict limit, but if you're going over 120 characters - or so you should think about whether there's a natural way to break it. If - there's not, that's fine, but if there is, that's better. -4) List comprehensions, lambda functions, map(), reduce(), filter(), etc. are - all fine if they're simple and generally aid code clarity. If you're doing - some hairy nested thing, it's probably better to split it up. -5) For binary operators (+, -, *, /, %, etc.) put a space around the operator: - Correct: a = 1 + 2 * 3 - Wrong: a=1+2*3 - - Exception: When assigning a default value for a function parameter, do not - use spaces: - Correct: def foo(bar=1): - Wrong: def foo(bar = 1): -6) Imports: With the exception of importing something that's in __init__, - import the module, not the class. - Correct: from calcs import gylphs - Slightly Wrong: import calcs.glyphs - Wrong: from calcs.glyphs import Glyph - Very Wrong: from calcs.glyph import * - - Imports should also generally be done in alphabetical order. -7) Try to keep module names distinct to the extent that it's possible to do so - and still have them make sense. It helps if you use descriptive module - names -8) Modules names should be lowercase_and_underscores. -9) Function names should be lowercase_and_underscores. -10) Class names should be CamelCase. -11) If a module primarily consists of a single class definition, the module - name and the class name should match. -12) Any string where there is even the slightest chance it will be shown to an - external user should use named introspection for variables. This is to - make translation, um, possible. - Correct: "%(character_name)s is level %(character_level)d" % {'character_name': name, 'character_level': level} - Wrong: "%s is level %d" % (name, level) - Very Wrong: name + ' is level ' + str(level). - - To explain: in various languages the sentence syntax may require the - variables to be in a different order. Giving them good descriptive names - lets the translators properly rearrange them as needed to convey the proper - meaning. -13) Comments are a good thing. If what you're doing isn't immediately obvious - from a quick readthrough, add a comment to explain it. - -In general: please try to write code that's as readable and maintainable as -possible. You only write the code once, but it will be read many many times. -Hence its worth spending an extra couple of minutes writing it if it saves the -readers even a few seconds in understanding it. As the saying goes: always -write code as though the person who has to maintain it is a dangerous -psychopath that knows where you live. diff --git a/test_ui/old_items.py b/test_ui/old_items.py deleted file mode 100644 index 0c5ab3f..0000000 --- a/test_ui/old_items.py +++ /dev/null @@ -1,328 +0,0 @@ -head = { - # ----- 4.2 ----- - 'Hood of Rampant Disdain': {'id': 71003, 'agi': 348, 'exp': 172, 'haste': 295, 'sockets': ['red', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 30}, - '(H)Hood of Rampant Disdain': {'id': 71416, 'agi': 400, 'exp': 202, 'haste': 333, 'sockets': ['red', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 30}, - 'Dark Phoenix Helmet': {'id': 71047, 'agi': 348, 'hit': 227, 'haste': 249, 'sockets': ['yellow', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 30, 'gear_buff': 'tier_12'}, # Tier 12 - '(H)Dark Phoenix Helmet': {'id': 71539, 'agi': 400, 'hit': 259, 'haste': 285, 'sockets': ['yellow', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 30, 'gear_buff': 'tier_12'}, # Tier 12 - # ----- 4.1 ----- - "The Savager's Mask": {'id': 69564, 'agi': 263, 'crit': 175, 'exp': 192, 'sockets': ['red', 'meta'], 'bonus_stat': 'crit', 'bonus_value': 30}, - # ----- 4.0 ----- - #'Agile Bio-Optic Killshades': {'id': 59455, 'agi': 301, 'sockets': ['meta'], 'bonus_stat': 'agi', 'bonus_value': 20}, # missing cogwheels - "(H)Membrane of C'Thun": {'id': 65129, 'agi': 325, 'exp': 197, 'haste': 257, 'sockets': ['yellow', 'meta'], 'bonus_stat': 'haste', 'bonus_value': 30}, - "Membrane of C'Thun": {'id': 59490, 'agi': 281, 'exp': 168, 'haste': 228, 'sockets': ['yellow', 'meta'], 'bonus_stat': 'haste', 'bonus_value': 30}, - "Tsanga's Helm": {'id': 60202, 'agi': 281, 'crit': 168, 'mastery': 228, 'sockets': ['blue', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 30}, - "(H)Wind Dancer's Helmet": {'id': 65241, 'agi': 325, 'crit': 257, 'hit': 197, 'sockets': ['blue', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 30, 'gear_buff': 'tier_11'}, # Tier 11 - "Wind Dancer's Helmet": {'id': 60299, 'agi': 281, 'crit': 228, 'hit': 168, 'sockets': ['blue', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 30, 'gear_buff': 'tier_11'}, # Tier 11 - 'Dunwald Winged Helm': {'id': 63833, 'agi': 268, 'haste': 178, 'mastery': 178}, - '(H)Helm of Numberless Shadows': {'id': 56344, 'agi': 242, 'crit': 162, 'hit': 182, 'sockets': ['blue', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 30}, - 'Helm of Secret Knowledge': {'id': 66936, 'agi': 208, 'crit': 117, 'haste': 171, 'sockets': ['yellow', 'meta'], 'bonus_stat': 'mastery', 'bonus_value': 30}, - 'Hood of the Crying Rogue': {'id': 66975, 'agi': 208, 'crit': 117, 'haste': 171, 'sockets': ['yellow', 'meta'], 'bonus_stat': 'mastery', 'bonus_value': 30}, - 'Mask of Vines': {'id': 58133, 'agi': 242, 'crit': 182, 'haste': 162, 'sockets': ['blue', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 30}, - 'Shocktrooper Hood': {'id': 63829, 'agi': 268, 'haste': 178, 'mastery': 178}, -} -neck = { - # ----- 4.2 ----- - 'Necklace of Smoke Signals': {'id': 71129, 'agi': 227, 'hit': 144, 'crit': 156}, - '(H)Necklace of Smoke Signals': {'id': 71565, 'agi': 256, 'hit': 162, 'crit': 176}, - 'Choker of Vanquished Lord': {'id': 71354, 'agi': 240, 'haste': 162, 'mastery': 156}, - '(H)Choker of the Vanquished Lord': {'id': 71610, 'agi': 271, 'haste': 183, 'mastery': 176}, - # ----- 4.1 ----- - 'Amulet of the Watcher': {'id': 69605, 'agi': 180, 'crit': 125, 'mastery': 111}, - # ----- 4.0 ----- - '(H)Necklace of Strife': {'id': 65107, 'agi': 215, 'haste': 143, 'mastery': 143}, - 'Necklace of Strife': {'id': 59517, 'agi': 190, 'haste': 127, 'mastery': 127}, - 'Acorn of the Daughter Tree': {'id': 62378, 'agi': 168, 'crit': 112, 'haste': 112}, - 'Amulet of Dull Dreaming': {'id': 57931, 'agi': 168, 'crit': 112, 'haste': 112}, - '(H)Barnacle Pendant': {'id': 56292, 'agi': 168, 'exp': 120, 'haste': 98}, - 'Brazen Elementium Medallion': {'id': 52350, 'agi': 138, 'crit': 112, 'haste': 102, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 10}, - 'Entwined Elementium Choker': {'id': 52321, 'agi': 148, 'crit': 65, 'haste': 128, 'sockets': ['yellow'], 'bonus_stat': 'mastery', 'bonus_value': 10}, - '(H)Mouth of the Earth': {'id': 56422, 'agi': 168, 'hit': 112, 'exp': 112}, - 'Mouth of the Earth': {'id': 56095, 'agi': 149, 'hit': 100, 'exp': 100}, - 'Nightrend Choker': {'id': 66974, 'agi': 149, 'crit': 100, 'haste': 100}, - '(H)Pendant of the Lightless Grotto': {'id': 56338, 'agi': 168, 'crit': 112, 'mastery': 112}, - 'Pendant of Victorious Fury': {'id': 63762, 'agi': 149, 'haste': 105, 'mastery': 90}, - 'Sweet Perfume Broach': {'id': 68174, 'agi': 168, 'crit': 101, 'haste': 119}, -} -shoulders = { - # ----- 4.2 ----- - 'Dark Phoenix Spaulders': {'id': 71049, 'agi': 282, 'haste': 185, 'mastery': 197, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 10, 'gear_buff': 'tier_12'}, # Tier 12 - '(H)Dark Phoenix Spaulders': {'id': 71541, 'agi': 322, 'haste': 211, 'mastery': 222, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 10, 'gear_buff': 'tier_12'}, # Tier 12 - 'Shoulderpads of the Forgotten Gate': {'id': 71345, 'agi': 282, 'hit': 210, 'crit': 153, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 10}, - '(H)Shoulderpads of the Forgotten Gate': {'id': 71456, 'agi': 322, 'hit': 240, 'crit': 173, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 10}, - # ----- 4.1 ----- - 'Tusked Shoulderpads': {'id': 69574, 'agi': 220, 'crit': 153, 'haste': 147, 'sockets': ['blue'], 'bonus_stat': 'agi', 'bonus_value': 10}, - # ----- 4.0 ----- - '(H)Poison Protocol Pauldrons': {'id': 65083, 'agi': 226, 'crit': 171, 'mastery': 191, 'sockets': ['red'], 'bonus_stat': 'mastery', 'bonus_value': 10}, - 'Poison Protocol Pauldrons': {'id': 59120, 'agi': 233, 'crit': 149, 'mastery': 169, 'sockets': ['red'], 'bonus_stat': 'mastery', 'bonus_value': 10}, - "(H)Wind Dancer's Spaulders": {'id': 65243, 'agi': 266, 'crit': 171, 'haste': 191, 'sockets': ['blue'], 'bonus_stat': 'agi', 'bonus_value': 10, 'gear_buff': 'tier_11'}, # Tier 11 - "Wind Dancer's Spaulders": {'id': 60302, 'agi': 233, 'crit': 149, 'haste': 169, 'sockets': ['blue'], 'bonus_stat': 'agi', 'bonus_value': 10, 'gear_buff': 'tier_11'}, # Tier 11 - '(H)Caridean Epaulettes': {'id': 56273, 'agi': 205, 'exp': 150, 'haste': 130, 'sockets': ['red'], 'bonus_stat': 'haste', 'bonus_value': 10}, - 'Clandestine Spaulders': {'id': 66905, 'agi': 199, 'crit': 142, 'haste': 116}, - 'Embrace of the Night': {'id': 58134, 'agi': 205, 'crit': 150, 'hit': 130, 'sockets': ['blue'], 'bonus_stat': 'agi', 'bonus_value': 10}, - '(H)Thieving Spaulders': {'id': 63449, 'agi': 205, 'crit': 130, 'haste': 150, 'sockets': ['yellow'], 'bonus_stat': 'haste', 'bonus_value': 20}, -} -back = { - # ----- 4.2 ----- - 'Dreadfire Drape': {'id': 70992, 'agi': 212, 'hit': 138, 'mastery': 95, 'sockets': ['red', 'red'], 'bonus_stat': 'agi', 'bonus_value': 20}, - '(H)Dreadfire Drape': {'id': 71415, 'agi': 241, 'hit': 158, 'mastery': 113, 'sockets': ['red', 'red'], 'bonus_stat': 'agi', 'bonus_value': 20}, - 'Sleek Flamewrath Cloak': {'id': 71228, 'agi': 227, 'hit': 169, 'crit': 122}, - '(H)Sleek Flamewrath Cloak': {'id': 71388, 'agi': 256, 'hit': 190, 'crit': 138}, - 'Mantle of Doubt': {'id': 71268, 'agi': 201, 'hit': 134, 'mastery': 134}, # 'elemental bonds' quest reward - # ----- 4.1 ----- - 'Recovered Cloak of Frostheim': {'id': 69584, 'agi': 180, 'hit': 117, 'haste': 122}, - "The Frost Lord's War Cloak": {'id': 69766, 'agi': 180, 'crit': 137, 'haste': 91}, # seasonal - # ----- 4.0 ----- - '(H)Cloak of Biting Chill': {'id': 65035, 'agi': 215, 'crit': 143, 'mastery': 143}, - 'Cloak of Biting Chill': {'id': 59348, 'agi': 190, 'crit': 127, 'mastery': 127}, - 'Viewless Wings': {'id': 58191, 'agi': 190, 'crit': 127, 'hit': 127}, - '(H)Cape of the Brotherhood': {'id': 65177, 'agi': 168, 'hit': 112, 'haste': 112}, - 'Cloak of Beasts': {'id': 56518, 'agi': 149, 'hit': 114, 'mastery': 76}, - '(H)Cloak of Thredd': {'id': 63473, 'agi': 168, 'crit': 112, 'mastery': 112}, - '(H)Kaleki Cloak': {'id': 56379, 'agi': 168, 'hit': 85, 'mastery': 128}, - 'Kaleki Cloak': {'id': 55858, 'agi': 149, 'hit': 76, 'mastery': 114}, - 'Razor-Edged Cloak': {'id': 56548, 'agi': 168, 'crit': 125, 'mastery': 90}, - 'Softwind Cape': {'id': 62361, 'agi': 168, 'hit': 112, 'haste': 112}, - '(H)Twitching Shadows': {'id': 56315, 'agi': 168, 'crit': 112, 'haste': 112}, -} -chest = { - # ----- 4.2 ----- - 'Dark Phoenix Tunic': {'id': 71045, 'agi': 368, 'crit': 230, 'exp': 263, 'sockets': ['red', 'blue'], 'bonus_stat': 'agi', 'bonus_value': 20, 'gear_buff': 'tier_12'}, # Tier 12 - '(H)Dark Phoenix Tunic': {'id': 71537, 'agi': 420, 'crit': 261, 'exp': 299, 'sockets': ['red', 'blue'], 'bonus_stat': 'agi', 'bonus_value': 20, 'gear_buff': 'tier_12'}, # Tier 12 - 'Breastplate of the Incendiary Soul': {'id': 71314, 'agi': 368, 'haste': 231, 'mastery': 267, 'sockets': ['red', 'red'], 'bonus_stat': 'agi', 'bonus_value': 20}, - '(H)Breastplate of the Incendiary Soul': {'id': 71455, 'agi': 420, 'haste': 264, 'mastery': 303, 'sockets': ['red', 'red'], 'bonus_stat': 'agi', 'bonus_value': 20}, - # ----- 4.1 ----- - 'Shadowtooth Trollskin Breastplate': {'id': 69569, 'agi': 283, 'crit': 164, 'haste': 216, 'sockets': ['yellow', 'yellow'], 'bonus_stat': 'agi', 'bonus_value': 20}, - # ----- 4.0 ----- - "Assassin's Chestplate": {'id': 56562, 'agi': 341, 'crit': 253, 'hit': 183, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 10}, - "Morrie's Waywalker Wrap": {'id': 67135, 'agi': 301, 'crit': 198, 'mastery': 218, 'sockets': ['red', 'yellow'], 'bonus_stat': 'mastery', 'bonus_value': 20}, - '(H)Sark of the Unwatched': {'id': 65060, 'agi': 345, 'crit': 227, 'mastery': 247,'sockets': ['red', 'yellow'], 'bonus_stat': 'mastery', 'bonus_value': 20}, - 'Sark of the Unwatched': {'id': 59318, 'agi': 301, 'crit': 198, 'mastery': 218, 'sockets': ['red', 'yellow'], 'bonus_stat': 'mastery', 'bonus_value': 20}, - "(H)Wind Dancer's Tunic": {'id': 65239, 'agi': 345, 'exp': 217, 'haste': 257, 'sockets': ['red', 'blue'], 'bonus_stat': 'agi', 'bonus_value': 20, 'gear_buff': 'tier_11'}, # Tier 11 - "Wind Dancer's Tunic": {'id': 60301, 'agi': 301, 'exp': 188, 'haste': 228, 'sockets': ['red', 'blue'], 'bonus_stat': 'agi', 'bonus_value': 20, 'gear_buff': 'tier_11'}, # Tier 11 - '(H)Defias Brotherhood Vest': {'id': 63468, 'agi': 262, 'haste': 182, 'mastery': 182, 'sockets': ['red', 'yellow'], 'bonus_stat': 'mastery', 'bonus_value': 20}, - '(H)Hieroglyphic Vest': {'id': 57874, 'agi': 262, 'crit': 182, 'haste': 182, 'sockets': ['yellow', 'yellow'], 'bonus_stat': 'agi', 'bonus_value': 20}, - 'Hieroglyphic Vest': {'id': 57863, 'agi': 228, 'crit': 158, 'haste': 158, 'sockets': ['yellow', 'yellow'], 'bonus_stat': 'agi', 'bonus_value': 20}, - 'Sly Fox Jerkin': {'id': 62374, 'agi': 228, 'crit': 178, 'mastery': 138, 'sockets': ['red', 'blue'], 'bonus_stat': 'mastery', 'bonus_value': 20}, - 'Tunic of Sinking Envy': {'id': 58131, 'agi': 262, 'crit': 202, 'hit': 162, 'sockets': ['red', 'blue'], 'bonus_stat': 'crit', 'bonus_value': 20}, - '(H)Vest of Misshapen Hides': {'id': 56455, 'agi': 262, 'crit': 162, 'mastery': 202, 'sockets': ['red', 'blue'], 'bonus_stat': 'mastery', 'bonus_value': 20}, - 'Vest of Misshapen Hides': {'id': 56128, 'agi': 268, 'crit': 178, 'mastery': 178}, -} -wrist = { - # ----- 4.2 ----- - 'Flamebinder Bracers': {'id': 71130, 'agi': 227, 'crit': 148, 'exp': 154}, - '(H)Flamebinder Bracers': {'id': 71569, 'agi': 256, 'crit': 166, 'exp': 173}, - # ----- 4.1 ----- - "Amani'shi Bracers": {'id': 69559, 'agi': 180, 'haste': 120, 'exp': 120}, - # ----- 4.0 ----- - '(H)Parasitic Bands': {'id': 65050, 'agi': 215, 'crit': 143, 'mastery': 143}, - 'Parasitic Bands': {'id': 59329, 'agi': 190, 'crit': 127, 'mastery': 127}, - '(H)Double Dealing Bracers': {'id': 63454, 'agi': 168, 'crit': 112, 'mastery': 112}, - '(H)Poison Fang Bracers': {'id': 56409, 'agi': 168, 'hit': 112, 'haste': 112}, - 'Poison Fang Bracers': {'id': 55886, 'agi': 149, 'hit': 100, 'haste': 100}, -} -hands = { - # ----- 4.2 ----- - 'Gloves of Dissolving Smoke': {'id': 71020, 'agi': 282, 'crit': 172, 'mastery': 208, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 10}, - '(H)Gloves of Dissolving Smoke': {'id': 71440, 'agi': 322, 'crit': 197, 'mastery': 235, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 10}, - 'Dark Phoenix Gloves': {'id': 71046, 'agi': 282, 'crit': 133, 'haste': 230, 'sockets': ['red'], 'bonus_stat': 'haste', 'bonus_value': 10, 'gear_buff': 'tier_12'}, # Tier 12 - '(H)Dark Phoenix Gloves': {'id': 71538, 'agi': 322, 'crit': 153, 'haste': 260, 'sockets': ['red'], 'bonus_stat': 'haste', 'bonus_value': 10, 'gear_buff': 'tier_12'}, # Tier 12 - 'Clutches of Evil': {'id': 69942, 'agi': 282, 'haste': 198, 'mastery': 186, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 10}, - "Aviana's Grips": {'id': 70122, 'agi': 248, 'haste': 203, 'mastery': 117, 'sockets': ['yellow'], 'bonus_stat': 'agi', 'bonus_value': 10}, - # ----- 4.1 ----- - 'Knotted Handwraps': {'id': 69798, 'agi': 240, 'haste': 182, 'exp': 121}, - # ----- 4.0 ----- - '(H)Double Attack Handguards': {'id': 65073, 'agi': 266, 'exp': 171, 'mastery': 191, 'sockets': ['red'], 'bonus_stat': 'mastery', 'bonus_value': 10}, - 'Double Attack Handguards': {'id': 59223, 'agi': 233, 'exp': 149, 'mastery': 169, 'sockets': ['red'], 'bonus_stat': 'mastery', 'bonus_value': 10}, - "Liar's Handwraps": {'id': 62417, 'agi': 233, 'crit': 149, 'haste': 169, 'sockets': ['yellow'], 'bonus_stat': 'haste', 'bonus_value': 10}, - 'Stormbolt Gloves': {'id': 62433, 'agi': 233, 'crit': 149, 'haste': 169, 'sockets': ['yellow'], 'bonus_stat': 'haste', 'bonus_value': 10}, - "(H)Wind Dancer's Gloves": {'id': 65240, 'agi': 266, 'hit': 171, 'haste': 191, 'sockets': ['red'], 'bonus_stat': 'haste', 'bonus_value': 10, 'gear_buff': 'tier_11'}, # Tier 11 - "Wind Dancer's Gloves": {'id': 60298, 'agi': 233, 'hit': 149, 'haste': 169, 'sockets': ['red'], 'bonus_stat': 'haste', 'bonus_value': 10, 'gear_buff': 'tier_11'}, # Tier 11 - '(H)Gloves of Haze': {'id': 56368, 'agi': 205, 'crit': 150, 'mastery': 130, 'sockets': ['blue'], 'bonus_stat': 'crit', 'bonus_value': 10}, - 'Sticky Fingers': {'id': 58138, 'agi': 205, 'haste': 130, 'mastery': 150, 'sockets': ['yellow'], 'bonus_stat': 'agi', 'bonus_value': 10}, -} -waist = { - # ----- 4.2 ----- - 'Flamebinding Girdle': {'id': 71131, 'agi': 282, 'hit': 167, 'haste': 211, 'sockets': ['blue', 'prismatic'], 'bonus_stat': 'agi', 'bonus_value': 10}, - '(H)Flamebinding Girdle': {'id': 71394, 'agi': 322, 'hit': 191, 'haste': 238, 'sockets': ['blue', 'prismatic'], 'bonus_stat': 'agi', 'bonus_value': 10}, - "Riplimb's Lost Collar": {'id': 71640, 'agi': 282, 'crit': 216, 'exp': 157, 'sockets': ['blue', 'prismatic'], 'bonus_stat': 'agi', 'bonus_value': 10}, - "(H)Riplimb's Lost Collar": {'id': 71641, 'agi': 322, 'crit': 244, 'exp': 180, 'sockets': ['blue', 'prismatic'], 'bonus_stat': 'agi', 'bonus_value': 10}, - # ----- 4.1 ----- - 'Belt of Slithering Serpents': {'id': 69600, 'agi': 220, 'haste': 142, 'mastery': 155, 'sockets': ['blue', 'prismatic'], 'bonus_stat': 'exp', 'bonus_value': 10}, - # ----- 4.0 ----- - 'Belt of Nefarious Whispers': {'id': 56537, 'agi': 253, 'hit': 184, 'mastery': 144, 'sockets': ['red', 'prismatic'], 'bonus_stat': 'agi', 'bonus_value': 10}, - '(H)Dispersing Belt': {'id': 65122, 'agi': 266, 'crit': 171, 'haste': 191, 'sockets': ['blue', 'prismatic'], 'bonus_stat': 'agi', 'bonus_value': 10}, - 'Dispersing Belt': {'id': 59502, 'agi': 233, 'crit': 149, 'haste': 169, 'sockets': ['blue', 'prismatic'], 'bonus_stat': 'agi', 'bonus_value': 10}, - 'Belt of a Thousand Mouths': {'id': 67240, 'agi': 225, 'crit': 150, 'haste': 150, 'sockets': ['prismatic']}, - 'Quicksand Belt': {'id': 62446, 'agi': 205, 'crit': 130, 'hit': 150, 'sockets': ['blue', 'prismatic'], 'bonus_stat': 'agi', 'bonus_value': 10}, - '(H)Red Beam Cord': {'id': 56429, 'agi': 205, 'crit': 130, 'hast': 150, 'sockets': ['blue', 'prismatic'], 'bonus_stat': 'haste', 'bonus_value': 10}, - 'Red Beam Cord': {'id': 56098, 'agi': 199, 'crit': 133, 'haste': 133, 'sockets': ['blue', 'prismatic'], 'bonus_stat': 'haste', 'bonus_value': 10}, - 'Sash of Musing': {'id': 57918, 'agi': 205, 'exp': 130, 'mastery': 150, 'sockets': ['red', 'prismatic'], 'bonus_stat': 'mastery', 'bonus_value': 10}, -} -legs = { - # ----- 4.2 ----- - 'Cinderweb Leggings': {'id': 71031, 'agi': 368, 'haste': 212, 'mastery': 284, 'sockets': ['red', 'yellow'], 'bonus_stat': 'agi', 'bonus_value': 20}, - '(H)Cinderweb Leggings': {'id': 71402, 'agi': 420, 'haste': 244, 'mastery': 320, 'sockets': ['red', 'yellow'], 'bonus_stat': 'agi', 'bonus_value': 20}, - 'Dark Phoenix Legguards': {'id': 71048, 'agi': 368, 'hit': 280, 'crit': 218, 'sockets': ['red', 'blue'], 'bonus_stat': 'agi', 'bonus_value': 20, 'gear_buff': 'tier_12'}, # Tier 12 - '(H)Dark Phoenix Legguards': {'id': 71540, 'agi': 420, 'hit': 316, 'crit': 251, 'sockets': ['red', 'blue'], 'bonus_stat': 'agi', 'bonus_value': 20, 'gear_buff': 'tier_12'}, # Tier 12 - # ----- 4.1 ----- - 'Leggings of Dancing Blades': {'id': 69589, 'agi': 283, 'crit': 174, 'exp': 206, 'sockets': ['red', 'blue'], 'bonus_stat': 'exp', 'bonus_value': 20}, - # ----- 4.0 ----- - "(H)Aberration's Leggings": {'id': 65039, 'agi': 345, 'crit': 257, 'haste': 217, 'sockets': ['yellow', 'yellow'], 'bonus_stat': 'agi', 'bonus_value': 20}, - "Aberration's Leggings": {'id': 59343, 'agi': 301, 'crit': 228, 'haste': 188, 'sockets': ['yellow', 'yellow'], 'bonus_stat': 'agi', 'bonus_value': 20}, - "(H)Wind Dancer's Legguards": {'id': 65242, 'agi': 345, 'crit': 217, 'mastery': 257, 'sockets': ['yellow', 'blue'], 'bonus_stat': 'agi', 'bonus_value': 20, 'gear_buff': 'tier_11'}, # Tier 11 - "Wind Dancer's Legguards": {'id': 60300, 'agi': 301, 'crit': 188, 'mastery': 228, 'sockets': ['yellow', 'blue'], 'bonus_stat': 'agi', 'bonus_value': 20, 'gear_buff': 'tier_11'}, # Tier 11 - "(H)Beauty's Chew Toy": {'id': 56309, 'agi': 262, 'hit': 162, 'haste': 202, 'sockets': ['red', 'blue'], 'bonus_stat': 'haste', 'bonus_value': 20}, - "Garona's Finest Leggings": {'id': 63703, 'agi': 268, 'crit': 191, 'exp': 157}, - 'Leggings of the Burrowing Mole': {'id': 58132, 'agi': 262, 'exp': 162, 'mastery': 202, 'sockets': ['red', 'blue'], 'bonus_stat': 'agi', 'bonus_value': 20}, - 'Leggings of the Impenitent': {'id': 62405, 'agi': 228, 'crit': 168, 'haste': 148, 'sockets': ['red', 'yellow'], 'bonus_stat': 'crit', 'bonus_value': 20}, - "Shaw's Finest Leggings": {'id': 63707, 'agi': 268, 'crit': 191, 'exp': 157}, - 'Swiftflight Leggings': {'id': 62425, 'agi': 228, 'crit': 168, 'haste': 148, 'sockets': ['red', 'yellow'], 'bonus_stat': 'crit', 'bonus_value': 20}, -} -feet = { - # ----- 4.2 ----- - 'Sandals of Leaping Coals': {'id': 71313, 'agi': 282, 'crit': 133, 'mastery': 230, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 10}, - '(H)Sandals of Leaping Coals': {'id': 71467, 'agi': 322, 'crit': 153, 'mastery': 260, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 10}, - 'Treads of the Craft': {'id': 69951, 'agi': 282, 'haste': 197, 'mastery': 187, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 10}, - # ----- 4.1 ----- - "Fasc's Preserved Boots": {'id': 69634, 'agi': 220, 'exp': 157, 'mastery': 135, 'sockets': ['red'], 'bonus_stat': 'crit', 'bonus_value': 10}, - # ----- 4.0 ----- - "(H)Storm Rider's Boots": {'id': 65144, 'agi': 266, 'haste': 171, 'mastery': 191, 'sockets': ['yellow'], 'bonus_stat': 'mastery', 'bonus_value': 10}, - "Storm Rider's Boots": {'id': 59469, 'agi': 233, 'haste': 149, 'mastery': 169, 'sockets': ['yellow'], 'bonus_stat': 'mastery', 'bonus_value': 10}, - 'Treads of Fleeting Joy': {'id': 58482, 'agi': 233, 'crit': 149, 'haste': 169, 'sockets': ['blue'], 'bonus_stat': 'agi', 'bonus_value': 10}, - 'Boots of the Hard Way': {'id': 66914, 'agi': 199, 'crit': 116, 'haste': 142}, - '(H)Boots of the Predator': {'id': 63435, 'agi': 205, 'crit': 150, 'hit': 130, 'sockets': ['yellow'], 'bonus_stat': 'crit', 'bonus_value': 10}, - "(H)Crafty's Gaiters": {'id': 56395, 'agi': 205, 'haste': 130, 'mastery': 150, 'sockets': ['blue'], 'bonus_stat': 'mastery', 'bonus_value': 10}, - "Crafty's Gaiters": {'id': 55871, 'agi': 199, 'haste': 133, 'mastery': 133}, - "(H)VanCleef's Boots": {'id': 65178, 'agi': 205, 'haste': 150, 'mastery': 130, 'sockets': ['yellow'], 'bonus_stat': 'agi', 'bonus_value': 10}, -} -rings = { - # ----- 4.2 ----- - 'Viridian Signet of the Avengers': {'id': 71216, 'agi': 236, 'haste': 181, 'mastery': 134, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 10}, - 'Splintered Brimstone Seal': {'id': 71209, 'agi': 227, 'crit': 140, 'mastery': 158}, - '(H)Splintered Brimstone Seal': {'id': 71566, 'agi': 256, 'crit': 158, 'mastery': 178}, - "Widow's Kiss": {'id': 71032, 'agi': 227, 'haste': 167, 'mastery': 126}, - "(H)Widow's Kiss": {'id': 71401, 'agi': 256, 'haste': 188, 'mastery': 142}, - 'Band of Glittering Lights': {'id': 70110, 'agi': 201, 'crit': 131, 'haste': 136}, - "Matoclaw's Band": {'id': 70105, 'agi': 201, 'hit': 121, 'crit': 142}, - 'Band of Ghoulish Glee': {'id': 71327, 'agi': 201, 'hit': 118, 'crit': 144}, # seasonal - # ----- 4.1 ----- - "Arlokk's Signet": {'id': 69610, 'agi': 180, 'crit': 122, 'mastery': 117}, - 'Quickfinger Ring': {'id': 69799, 'agi': 180, 'haste': 135, 'exp': 94}, - # ----- 4.0 ----- - 'Gilnean Ring of Ruination': {'id': 67136, 'agi': 190, 'hit': 108, 'haste': 138}, - '(H)Lightning Conductor Band': {'id': 65082, 'agi': 215, 'crit': 143, 'hit': 143}, - 'Lightning Conductor Band': {'id': 59121, 'agi': 190, 'crit': 127, 'hit': 127}, - 'Signet of the Elder Council': {'id': 62362, 'agi': 190, 'haste': 127, 'mastery': 127}, - 'Band of Blades': {'id': 52318, 'agi': 138, 'crit': 116, 'hit': 97, 'sockets': ['yellow'], 'bonus_stat': 'hit', 'bonus_value': 10}, - "Elementium Destroyer's Ring": {'id': 52348, 'agi': 148, 'crit': 89, 'mastery': 114, 'sockets': ['red'], 'bonus_stat': 'haste', 'bonus_value': 10}, - '(H)Mirage Ring': {'id': 56404, 'agi': 168, 'hit': 85, 'haste': 128}, - 'Mirage Ring': {'id': 55884, 'agi': 149, 'hit': 76, 'haste': 114}, - '(H)Nautilus Ring': {'id': 56282, 'agi': 168, 'crit': 112, 'haste': 112}, - '(H)Ring of Blinding Stars': {'id': 56412, 'agi': 168, 'haste': 112, 'mastery': 112}, - 'Ring of Blinding Stars': {'id': 55994, 'agi': 149, 'haste': 100, 'mastery': 100}, - '(H)Ring of Dun Algaz': {'id': 56445, 'agi': 168, 'crit': 120, 'hit': 98}, - 'Ring of Dun Algaz': {'id': 56120, 'agi': 149, 'crit': 107, 'hit': 87}, - '(H)Skullcracker Ring': {'id': 58186, 'agi': 168, 'crit': 112, 'haste': 112}, - '(H)Skullcracker Ring': {'id': 56310, 'agi': 168, 'crit': 112, 'mastery': 112}, - "Terrath's Signet of Balance": {'id': 62348, 'agi': 168, 'hit': 112, 'mastery': 112}, -} -trinkets = { - # ----- 4.2 ----- - # "Aella's Bottle": {'id': 71633, 'agi': 340, 'gear_buff': 'rickets_magnetic_fireball'}, # unobtainable - "Ricket's Magnetic Fireball": {'id': 70144, 'agi': 340, 'gear_buff': 'rickets_magnetic_fireball', 'proc': 'rickets_magnetic_fireball_proc'}, - '(H)Matrix Restabilizer': {'id': 69150, 'agi': 433, 'proc': 'heroic_matrix_restabilizer'}, - 'Matrix Restabilizer': {'id': 68994, 'agi': 406, 'proc': 'matrix_restabilizer'}, - '(H)The Hungerer': {'id': 69112, 'agi': 458, 'proc': 'heroic_the_hungerer'}, - 'The Hungerer': {'id': 68927, 'agi': 383, 'proc': 'the_hungerer'}, - '(H)Ancient Petrified Seed': {'id': 69199, 'mastery': 433, 'gear_buff': 'heroic_ancient_petrified_seed'}, - 'Ancient Petrified Seed': {'id': 69001, 'mastery': 383, 'gear_buff': 'ancient_petrified_seed'}, - "Coren's Chilled Chromium Coaster": {'id': 71335, 'crit': 340, 'proc': 'corens_chilled_chromium_coaster'}, # seasonal - # ----- 4.1 ----- - # ----- 4.0 ----- - '(H)Grace of the Herald': {'id': 56295, 'agi': 285, 'proc': 'heroic_grace_of_the_herald'}, - 'Grace of the Herald': {'id': 55266, 'agi': 153, 'proc': 'grace_of_the_herald'}, - '(H)Key to the Endless Chamber': {'id': 56328, 'hit': 285, 'proc': 'heroic_key_to_the_endless_chamber'}, - 'Key to the Endless Chamber': {'id': 55795, 'hit': 215, 'proc': 'key_to_the_endless_chamber'}, - '(H)Left Eye of Rajh': {'id': 56427, 'exp': 285, 'proc': 'heroic_left_eye_of_rajh'}, - 'Left Eye of Rajh': {'id': 56102, 'exp': 252, 'proc': 'left_eye_of_rajh'}, - "(H)Prestor's Talisman of Machination": {'id': 65026, 'agi': 363, 'proc': 'heroic_prestors_talisman_of_machination'}, - "Prestor's Talisman of Machination": {'id': 59441, 'agi': 321, 'proc': 'prestors_talisman_of_machination'}, - "(H)Tia's Grace": {'id': 56394, 'mastery': 285, 'proc': 'heroic_tias_grace'}, - "Tia's Grace": {'id': 55874, 'mastery': 252, 'proc': 'tias_grace'}, - 'Darkmoon Card: Hurricane ': {'id': 62051, 'agi': 321, 'proc': 'darkmoon_card_hurricane'}, - 'Essence of the Cyclone': {'id': 59473, 'agi': 321, 'proc': 'essence_of_the_cyclone'}, - 'Fluid Death ': {'id': 58181, 'hit': 321, 'proc': 'fluid_death'}, - 'Heart of the Vile': {'id': 66969, 'agi': 234, 'proc': 'heart_of_the_vile'}, - 'Unheeded Warning ': {'id': 59520, 'agi': 321, 'proc': 'unheeded_warning'}, - 'Unsolvable Riddle': {'id': 62463, 'mastery': 321, 'gear_buff': 'unsolvable_riddle'}, - 'Figurine - Demon Panther ': {'id': 52199, 'hit': 285, 'gear_buff': 'demon_panther '}, -} -weapons = { - # ----- 4.2 ----- - # "1.8d Avool's Incendiary Shanker": {'id': 71779, 'agi': 175, 'hit': 125, 'crit': 102, 'damage': 993.5, 'speed': 1.8, 'type': 'dagger'}, # throws error when included - # "1.8d (H)Avool's Incendiary Shanker": {'id': 71778, 'agi': 197, 'hit': 141, 'crit': 115, 'damage': 1121, 'speed': 1.8, 'type': 'dagger'}, # throws error when included - # '1.4d Entrail Disgorger': {'id': 71787, 'agi': 175, 'crit': 97, 'mastery': 128, 'damage': 773, 'speed': 1.4, 'type': 'dagger'}, # throws error when included - # '1.4d (H) Entrail Disgorger': {'id': 71786, 'agi': 197, 'crit': 109, 'mastery': 145, 'damage': 872, 'speed': 1.4, 'type': 'dagger'}, # throws error when included - "1.4d Alysra's Razor": {'id': 70733, 'agi': 155, 'haste': 113, 'exp': 98, 'sockets': ['yellow'], 'bonus_stat': 'agi', 'bonus_value': 10, 'damage': 772.5, 'speed': 1.4, 'type': 'dagger'}, - # "1.4d (H)Alysra's Razor": {'id': 71427, 'agi': 177, 'haste': 128, 'exp': 113, 'sockets': ['yellow'], 'bonus_stat': 'agi', 'bonus_value': 10, 'damage': 872, 'speed': 1.4, 'type': 'dagger'}, # throws error when included - '1.8d Feeding Frenzy': {'id': 71013, 'agi': 175, 'crit': 88, 'haste': 133, 'damage': 993, 'speed': 1.8, 'type': 'dagger'}, - #'1.8d (H)Feeding Frenzy': {'id': 71441, 'agi': 197, 'crit': 100, 'haste': 150, 'damage': 1121, 'speed': 1.8, 'type': 'dagger'}, # throws error when included - '1.8d Brainsplinter': {'id': 70155, 'agi': 155, 'hit': 103, 'haste': 103, 'damage': 880.5, 'speed': 1.8, 'type': 'dagger'}, - # "2.0d Direbrew's Bloodied Shanker": {'id': 71331, 'agi': 155, 'hit': 90, 'crit': 111, 'damage': 978, 'speed': 2.0, 'type': 'dagger'}, # seasonal - throws error when included - # '2.6a Gatecrasher': {'id': 71312, 'agi': 152, 'crit': 120, 'exp': 111, 'damage': 1435, 'speed': 2.6, 'type': 'axe'}, # throws error when included - # '2.6a (H)Gatecrasher': {'id': 71454, 'agi': 197, 'crit': 135, 'exp': 125, 'damage': 1619.5, 'speed': 2.6, 'type': 'axe'}, # throws error when included - # '2.6m Shatterskull Bonecrusher': {'id': 71782, 'agi': 175, 'crit': 123, 'haste': 105, 'damage': 1435, 'speed': 2.6, 'type': 'mace'}, # throws error when included - # '2.6m (H)Shatterskull Bonecrusher': {'id': 71783, 'agi': 197, 'crit': 139, 'haste': 119, 'damage': 1619.5, 'speed': 2.6, 'type': 'mace'}, # throws error when included - # "2.6m Tremendous Tankard O' Terror": {'id': 71332, 'agi': 153, 'crit': 99, 'haste': 97, 'damage': 1271, 'speed': 2.6, 'type': 'mace'}, # seasonal, throws error when included - '2.6s Pyrium Spellward': {'id': 70162, 'agi': 155, 'hit': 103, 'mastery': 103, 'damage': 1271, 'speed': 2.6, 'type': 'sword'}, - # "2.6s The Horseman's Sinister Saber": {'id': 71325, 'agi': 103, 'hit': 103, 'exp': 103, 'damage': 1271, 'speed': 2.6, 'type': 'sword'}, # main hand - seasonal, not modeled gear_buff, throws error when included - # ----- 4.1 ----- - # '1.8d Twinblade of the Hakkari': {'id': 69621, 'agi': 138, 'crit': 92, 'haste': 92, 'damage': 786.5, 'speed': 1.8, 'type': 'dagger'}, # throws error when included - # '1.4d Twinblade of the Hakkari': {'id': 69620, 'agi': 138, 'hit': 92, 'exp': 92, 'damage': 612, 'speed': 1.4, 'type': 'dagger'}, # throws error when included - # "2.6f Thekal's Claws": {'id': 69636, 'agi': 138, 'crit': 96, 'mastery': 85, 'damage': 1136.5, 'speed': 2.6, 'type': 'fist'}, # main hand, throws error when included - # "2.6f Arlokk's Claws": {'id': 69638, 'agi': 138, 'hit': 94, 'haste': 90, 'damage': 1136.5, 'speed': 2.6, 'type': 'fist'}, # off hand, throws error when included - # '2.6m Mace of the Sacrificed': {'id': 69575, 'agi': 138, 'hit': 85, 'haste': 96, 'damage': 1136.5, 'speed': 2.6, 'type': 'mace'}, # throws error when included - # ----- 4.0 ----- - '1.8d (H)Organic Lifeform Inverter': {'id': 65081, 'agi': 165, 'exp': 110, 'mastery': 110, 'damage': 939.5, 'speed': 1.8, 'type': 'dagger'}, - '1.8d Organic Lifeform Inverter': {'id': 59122, 'agi': 146, 'exp': 97, 'mastery': 97, 'damage': 832, 'speed': 1.8, 'type': 'dagger'}, - '1.4d Scaleslicer': {'id': 68601, 'agi': 146, 'hit': 97, 'exp': 97, 'damage': 647.5, 'speed': 1.4, 'type': 'dagger'}, - '1.8d The Twilight Blade': {'id': 68163, 'proc': 'the_twilight_blade', 'damage': 832, 'speed': 1.8, 'type': 'dagger'}, - "1.4d (H)Uhn'agh Fash, the Darkest Betrayal": {'id': 68600, 'agi': 165, 'crit': 110, 'haste': 110, 'damage': 750.5, 'speed': 1.4, 'type': 'dagger'}, - "1.4d Uhn'agh Fash, the Darkest Betrayal": {'id': 59494, 'agi': 146, 'crit': 97, 'haste': 97, 'damage': 647.5, 'speed': 1.4, 'type': 'dagger'}, - "1.4d (H)Barim's Main Gauche": {'id': 56390, 'agi': 129, 'crit': 86, 'mastery': 86, 'damage': 573.5, 'speed': 1.4, 'type': 'dagger'}, - "1.4d Barim's Main Gauche": {'id': 55870, 'agi': 115, 'crit': 76, 'mastery': 76, 'damage': 508, 'speed': 1.4, 'type': 'dagger'}, - '1.4d (H)Buzzer Blade': {'id': 65163, 'agi': 129, 'crit': 86, 'haste': 86, 'damage': 573.5, 'speed': 1.4, 'type': 'dagger'}, - '1.8d Dagger of Restless Nights': {'id': 62456, 'agi': 129, 'crit': 86, 'hit': 86, 'damage': 737, 'speed': 1.8, 'type': 'dagger'}, - '1.8d Elementium Shank': {'id': 55068, 'agi': 129, 'hit': 86, 'haste': 86, 'damage': 737.5, 'speed': 1.8, 'type': 'dagger'}, - '1.8d Laquered Lung-Leak Longknife': {'id': 63792, 'agi': 115, 'crit': 75, 'mastery': 78, 'damage': 653.5, 'speed': 1.8, 'type': 'dagger'}, - #'1.8d (H)Meteor Shard': {'id': 63456, 'agi': 129, 'damage': 737.5, 'speed': 1.8, 'type': 'dagger'}, # not modeled proc - '1.4d (H)Quicksilver Blade': {'id': 56335, 'agi': 129, 'haste': 86, 'mastery': 86, 'damage': 573.5, 'speed': 1.4, 'type': 'dagger'}, - "1.8d (H)Steelbender's Masterpiece": {'id': 56302, 'agi': 129, 'hit': 93, 'mastery': 76, 'damage': 737, 'speed': 1.8, 'type': 'dagger'}, - '1.4d Throat Slasher': {'id': 57927, 'agi': 129, 'crit': 86, 'hit': 86, 'damage': 573.5, 'speed': 1.4, 'type': 'dagger'}, # off hand - '1.4d (H)Toxidunk Dagger': {'id': 56326, 'agi': 129, 'hit': 86, 'exp': 86, 'damage': 573.5, 'speed': 1.4, 'type': 'dagger'}, - '1.8d (H)Wicked Dagger': {'id': 63477, 'agi': 129, 'crit': 86, 'exp': 86, 'damage': 737.5, 'speed': 1.8, 'type': 'dagger'}, - '1.8d (H)Windwalker Blade': {'id': 56454, 'agi': 129, 'crit': 86, 'exp': 86, 'damage': 737, 'speed': 1.8, 'type': 'dagger'}, - '1.8d Windwalker Blade': {'id': 56127, 'agi': 115, 'crit': 76, 'exp': 76, 'damage': 653, 'speed': 1.8, 'type': 'dagger'}, - '2.6f (H)Claws of Torment': {'id': 65006, 'agi': 165, 'crit': 110, 'haste': 110, 'damage': 1356.5, 'speed': 2.6, 'type': 'fist'}, # main hand - '2.6f Claws of Torment': {'id': 63537, 'agi': 146, 'crit': 97, 'haste': 97, 'damage': 1202, 'speed': 2.6, 'type': 'fist'}, # main hand - '2.6f Crystalline Geoknife': {'id': 66972, 'agi': 115, 'crit': 76, 'haste': 76, 'damage': 943.5, 'speed': 2.6, 'type': 'fist'}, # main hand - '2.6f (H)Fist of Pained Senses': {'id': 56329, 'agi': 129, 'crit': 86, 'haste': 86, 'damage': 1065, 'speed': 2.6, 'type': 'fist'}, # main hand - '2.6f The Perforator': {'id': 52493, 'agi': 95, 'crit': 87, 'mastery': 38, 'sockets': ['red'], 'bonus_stat': 'mastery', 'bonus_value': 10, 'damage': 943.5, 'speed': 2.6, 'type': 'fist'}, - "2.6a (H)Crul'korak, the Lightning's Arc": {'id': 65024, 'agi': 165, 'crit': 110, 'haste': 110, 'damage': 1356.5, 'speed': 2.6, 'type': 'axe'}, - "2.6a Crul'korak, the Lightning's Arc": {'id': 59443, 'agi': 146, 'crit': 97, 'haste': 97, 'damage': 1202, 'speed': 2.6, 'type': 'axe'}, - # "2.6a (H)Maimgor's Bite": {'id': 65014, 'agi': 165, 'hit': 110, 'mastery': 110, 'damage': 1356.5, 'speed': 2.6, 'type': 'axe'}, # off hand - # "2.6a Maimgor's Bite": {'id': 59462, 'agi': 146, 'hit': 97, 'mastery': 97, 'damage': 1202, 'speed': 2.6, 'type': 'axe'}, # off hand - "2.6a Calder's Coated Carrion Carver": {'id': 63788, 'agi': 115, 'crit': 84, 'haste': 63, 'damage': 943.5, 'speed': 2.6, 'type': 'axe'}, - '2.6a Elementium Gutslicer': {'id': 67602, 'agi': 129, 'hit': 86, 'mastery': 86, 'damage': 1065, 'speed': 2.6, 'type': 'axe'}, - '2.6a (H)Lightning Whelk Axe': {'id': 56266, 'agi': 129, 'crit': 86, 'hit': 86, 'damage': 1065, 'speed': 2.6, 'type': 'axe'}, - '2.6a Ravening Slicer': {'id': 62457, 'agi': 129, 'haste': 86, 'mastery': 86, 'damage': 1065, 'speed': 2.6, 'type': 'axe'}, - # '2.6a Windslicer': {'id': Windslicer, 'agi': 129, 'crit': 86, 'mastery': 86, 'damage': 1065, 'speed': 2.6, 'type': 'axe'}, # off hand - '2.6m (H)Hammer of Sparks': {'id': 56396, 'agi': 129, 'crit': 86, 'hit': 86, 'damage': 1065, 'speed': 2.6, 'type': 'mace'}, - '2.6m Hammer of Sparks': {'id': 55875, 'agi': 115, 'crit': 76, 'hit': 76, 'damage': 943.5, 'speed': 2.6, 'type': 'mace'}, - '2.6m (H)Heavy Geode Mace': {'id': 56353, 'agi': 129, 'hit': 86, 'exp': 86, 'damage': 1065, 'speed': 2.6, 'type': 'mace'}, - '2.6s (H)Fang of Twilight': {'id': 65094, 'agi': 165, 'crit': 110, 'mastery': 110, 'damage': 1356.5, 'speed': 2.6, 'type': 'sword'}, - '2.6s Fang of Twilight': {'id': 63533, 'agi': 146, 'crit': 97, 'mastery': 97, 'damage': 1202, 'speed': 2.6, 'type': 'sword'}, - '2.6s Krol Decapitator': {'id': 68161, 'agi': 146, 'hit': 86, 'haste': 105, 'damage': 1202, 'speed': 2.6, 'type': 'sword'}, - '2.7s (H)Cruel Barb': {'id': 65164, 'agi': 129, 'crit': 86, 'hit': 86, 'damage': 1106, 'speed': 2.7, 'type': 'sword'}, - "2.6s (H)Thief's Blade": {'id': 65173, 'agi': 129, 'haste': 86, 'mastery': 86, 'damage': 1065, 'speed': 2.6, 'type': 'sword'}, -} \ No newline at end of file diff --git a/test_ui/testing_ui.py b/test_ui/testing_ui.py deleted file mode 100644 index 4698b1e..0000000 --- a/test_ui/testing_ui.py +++ /dev/null @@ -1,709 +0,0 @@ -# All the imports here are either base python or shadowcraft files with the exception of wx, -# which can be downloaded from http://www.wxpython.org/download.php (I worked with windows 2.6/64) -from os import path -import sys -sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) - -from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator -from shadowcraft.calcs.rogue.Aldriana import settings - -from shadowcraft.core import exceptions -from shadowcraft.core import i18n - -from shadowcraft.objects import buffs -from shadowcraft.objects import race -from shadowcraft.objects import stats -from shadowcraft.objects import procs -from shadowcraft.objects import talents -from shadowcraft.objects import glyphs - -import ui_data -import os -import string -import wx - -class GearPage(wx.Panel): - gear_slots = [ - "head", - "neck", - "shoulders", - "back", - "chest", - "wrists", - "hands", - "waist", - "legs", - "feet", - "ring1", - "ring2", - "trinket1", - "trinket2", - "mainhand", - "offhand", - ] - current_gear = { - "head": 0, - "neck": 0, - "shoulders": 0, - "back": 0, - "chest": 0, - "wrists": 0, - "hands": 0, - "waist": 0, - "legs": 0, - "feet": 0, - "ring1": 0, - "ring2": 0, - "trinket1": 0, - "trinket2": 0, - "mainhand": 0, - "offhand": 0, - } - stats = [ - 'str', - 'agi', - 'ap', - 'crit', - 'hit', - 'exp', - 'haste', - 'mastery', - ] - enchants = {} - gems = {} - reforges = {} - - def __init__(self, parent, calculator): - wx.Panel.__init__(self, parent) - self.calculator = calculator - - grid_sizer = wx.FlexGridSizer(cols = 6) - for slot in self.gear_slots: - self.create_ui_for_slot(grid_sizer, slot) - self.SetSizer(grid_sizer) - self.Fit() - - def create_ui_for_slot(self, sizer, slot): - label = wx.StaticText(self, -1, label = string.capwords(slot)) - sizer.Add(label, flag = wx.ALIGN_RIGHT) - item_cb = self.create_item_ui_for_slot(slot) - sizer.Add(item_cb) - if not slot in ('trinket1', 'trinket2', 'neck', 'waist'): - ench_label = wx.StaticText(self, -1, label = "Enchant") - sizer.Add(ench_label, flag = wx.ALIGN_RIGHT) - enchant_cb = self.create_enchant_ui_for_slot(self, slot) - if not enchant_cb == None: - sizer.Add(enchant_cb) - else: - sizer.Add((10, 10)) - else: - sizer.Add((2, 2)) - sizer.Add((2, 2)) - - gem_selecter = self.create_gem_ui_for_slot(slot) - #In order to get gems set for initial items, have to do update here - self.update_item_for_slot(self.get_items_for_slot(slot)[0], slot) - self.update_gems_for_slot(slot) - sizer.Add(gem_selecter) - - reforging_panel = self.create_reforging_ui_for_slot(slot) - sizer.Add(reforging_panel) - - def create_item_ui_for_slot(self, slot): - cb = None - cb = wx.ComboBox(self, -1, style = wx.CB_READONLY, name = slot) - cb.SetItems(self.get_items_for_slot(slot)) - cb.SetSelection(0) - cb.Bind(wx.EVT_COMBOBOX, lambda evt, slot=slot:self.on_item_selected(evt, slot)) - return cb - - def create_gem_ui_for_slot(self, slot): - vbox = wx.BoxSizer(wx.VERTICAL) - self.gems[slot] = {} - for color in ('meta', 'red', 'yellow', 'blue', 'prismatic'): - panel = wx.Panel(self, -1) - gem_sizer = wx.BoxSizer(wx.HORIZONTAL) - panel.SetSizer(gem_sizer) - color_block = wx.StaticText(panel, -1, label = " ") - gem_sizer.Add(color_block) - if color == 'meta': - color_block.SetBackgroundColour('WHITE') - elif color == 'prismatic': - color_block.SetBackgroundColour('GREY') - else: - color_block.SetBackgroundColour(color.upper()) - cb = wx.ComboBox(panel, -1, style = wx.CB_READONLY) - cb.Bind(wx.EVT_COMBOBOX, self.on_gem_selected) - cb.SetItems([''] + ui_data.gems.keys()) - cb.SetSelection(0) - panel.Hide() - - gem_sizer.Add(cb, 0, wx.EXPAND) - self.gems[slot][color] = cb - vbox.Add(panel, 0, wx.EXPAND) - return vbox - - def create_enchant_ui_for_slot(self, master, slot): - cb = None - enchants = [''] + self.get_enchants_for_slot(slot).keys() - cb = wx.ComboBox(master, -1, style = wx.CB_READONLY) - cb.SetItems(enchants) - cb.Bind(wx.EVT_COMBOBOX, self.on_enchant_selected) - self.enchants[slot] = cb - return cb - - def create_reforging_ui_for_slot(self, slot): - sizer = wx.BoxSizer(wx.HORIZONTAL) - reforge_from = self.current_gear[slot].reforgable_from() - reforge_to = self.current_gear[slot].reforgable_to() - if len(reforge_from) > 0: - cb_from = wx.ComboBox(self, -1, style = wx.CB_READONLY) - cb_from.SetItems([''] + reforge_from) - sizer.Add(cb_from, 2, wx.EXPAND) - cb_to = wx.ComboBox(self, -1, style = wx.CB_READONLY) - cb_to.SetItems([''] + reforge_to) - sizer.Add(cb_to, 2, wx.EXPAND) - btn_reforge = wx.Button(self, -1, label = "Reforge") - btn_reforge.Bind(wx.EVT_BUTTON, lambda evt, slot=slot: self.on_reforge(evt, slot)) - sizer.Add(btn_reforge, 2, wx.EXPAND) - btn_restore = wx.Button(self, -1, label = "Restore") - btn_restore.Bind(wx.EVT_BUTTON, lambda evt, slot=slot: self.on_restore(evt, slot)) - btn_restore.Hide() - sizer.Add(btn_restore, 2, wx.EXPAND) - self.reforges[slot] = {'from': cb_from, 'to': cb_to, 'reforge': btn_reforge, 'restore': btn_restore} - return sizer - - def populate_combobox_for_slot(self, combobox, slot): - options = self.get_items_for_slot(slot) - combobox.SetItems(options) - combobox.SetStringSelection(options[0]) - - def get_items_for_slot(self, slot): - item_names = [] - items_dict = getattr(ui_data, slot) - item_names = items_dict.keys() - return item_names - - def get_gems(self): - return ui_data.gems.keys() - - def get_enchants_for_slot(self, slot): - enchants = [] - if slot in ('mainhand', 'offhand'): - enchants = ui_data.enchants['melee_weapons'] - elif slot in ('ring1', 'ring2'): - enchants = ui_data.enchants['rings'] - else: - enchants = ui_data.enchants[slot] - return enchants - - def update_gems_for_slot(self, slot): - for color in self.gems[slot].keys(): - self.gems[slot][color].SetSelection(0) - self.gems[slot][color].GetParent().Hide() - item = self.current_gear[slot] - for color in item.sockets: - self.gems[slot][color].GetParent().Show() - self.Layout() - - def update_item_for_slot(self, item_name, slot): - items_dict = getattr(ui_data, slot) - item = None - if slot in ('mainhand', 'offhand'): - item = ui_data.Weapon(item_name, **items_dict[item_name]) - else: - item = ui_data.Item(item_name, **items_dict[item_name]) - self.current_gear[slot] = item - - #Event handler for selecting a combo box entry - def on_item_selected(self, e, slot): - item_name = e.GetString() - self.update_item_for_slot(item_name, slot) - self.update_gems_for_slot(slot) - self.calculator.calculate() - #Clear the (no longer true) reforge settings - self.reset_reforging_ui_for_slot(slot) - - def on_reforge(self, e, slot): - reforge_from = self.reforges[slot]['from'].GetValue() - reforge_to = self.reforges[slot]['to'].GetValue() - if len(reforge_from) > 0 and len(reforge_to) > 0: - self.current_gear[slot].reforge(reforge_from, reforge_to) - self.reforges[slot]['reforge'].Hide() - self.reforges[slot]['restore'].Show() - self.Layout() - self.calculator.calculate() - - def on_restore(self, e, slot): - #Restoring the item to its dictionary definition - print self.current_gear[slot].name - self.update_item_for_slot(self.current_gear[slot].name, slot) - self.reset_reforging_ui_for_slot(slot) - self.calculator.calculate() - - def reset_reforging_ui_for_slot(self, slot): - reforge_from = self.current_gear[slot].reforgable_from() - reforge_to = self.current_gear[slot].reforgable_to() - if len(reforge_from) > 0: - self.reforges[slot]['from'].SetItems([''] + reforge_from) - self.reforges[slot]['from'].SetSelection(0) - self.reforges[slot]['to'].SetItems([''] + reforge_to) - self.reforges[slot]['to'].SetSelection(0) - self.reforges[slot]['restore'].Hide() - self.reforges[slot]['reforge'].Show() - self.Layout() - - def on_gem_selected(self, e): - self.calculator.calculate() - - def on_enchant_selected(self, e): - self.calculator.calculate() - - def get_stats(self): - current_stats = {'str': 0, 'agi': 0, 'ap': 0, 'crit': 0, 'hit': 0, 'exp': 0, 'haste': 0, 'mastery': 0, 'procs': [], 'gear_buffs': []} - current_stats['procs'] = [] - current_stats['gear_buffs'] = ['leather_specialization'] #Assuming this rather than give equipment an armor type - enchant_slots = self.enchants.keys() - - tier11_count = 0 - tier12_count = 0 - tier14_count = 0 - for slot in self.gear_slots: - for stat in self.stats: - current_stats[stat] += getattr(self.current_gear[slot], stat) - gear_buff = self.current_gear[slot].gear_buff - if 'tier_11' == gear_buff: - tier11_count += 1 - elif 'tier_12' == gear_buff: - tier12_count += 1 - elif 'tier_14' == gear_buff: - tier14_count += 1 - elif len(gear_buff) > 0: - current_stats['gear_buffs'].append(gear_buff) - if len(self.current_gear[slot].proc) > 0: - current_stats['procs'].append(self.current_gear[slot].proc) - get_bonus = True - for slot_color in self.current_gear[slot].sockets: - gem_name = self.gems[slot][slot_color].GetValue() - if len(gem_name) > 0: - gem = ui_data.gems[gem_name] - for stat in gem[1]: - if stat == 'proc': - current_stats['procs'] += gem[1][stat] - elif stat == 'gear_buff': - current_stats['gear_buffs'] += gem[1][stat] - else: - current_stats[stat] += gem[1][stat] - if not slot_color in gem[0] and slot_color != 'prismatic': - get_bonus = False - if get_bonus and len(self.current_gear[slot].bonus_stat) > 0: - current_stats[self.current_gear[slot].bonus_stat] += self.current_gear[slot].bonus_value - if slot in enchant_slots and slot not in ('mainhand', 'offhand'): - #bugged - enchant_name = self.enchants[slot].GetValue() - if len(enchant_name) > 0: - enchant_data = ui_data.enchants[slot][enchant_name] - for stat in enchant_data.keys(): - current_stats[stat] += enchant_data[stat] - if tier11_count >= 2: - current_stats['gear_buffs'].append('rogue_t11_2pc') - if tier11_count >= 4: - current_stats['procs'].append('rogue_t11_4pc') - if tier12_count >= 2: - current_stats['gear_buffs'].append('rogue_t12_2pc') - if tier12_count >= 4: - current_stats['gear_buffs'].append('rogue_t12_4pc') - if tier14_count >= 2: - current_stats['gear_buffs'].append('rogue_t14_2pc') - if tier14_count >= 4: - current_stats['gear_buffs'].append('rogue_t14_4pc') - - mh = self.current_gear['mainhand'] - enchant = None - #bugged - if len(self.enchants['mainhand'].GetValue()) > 0: - enchant = ui_data.enchants['melee_weapons'][self.enchants['mainhand'].GetValue()] - mainhand = stats.Weapon(mh.damage, mh.speed, mh.type, enchant) - current_stats['mh'] = mainhand - - oh = self.current_gear['offhand'] - enchant = None - if len(self.enchants['offhand'].GetValue()) > 0: - enchant = ui_data.enchants['melee_weapons'][self.enchants['offhand'].GetValue()] - offhand = stats.Weapon(oh.damage, oh.speed, oh.type, enchant) - current_stats['oh'] = offhand - - current_stats['procs'] = procs.ProcsList(*set(current_stats['procs'])) - - current_stats['gear_buffs'] = stats.GearBuffs(*set(current_stats['gear_buffs'])) - - return current_stats - -class TalentsPage(wx.Panel): - rogue_talents = [ - ['nightstalker', 'subterfuge', 'shadow_focus'], - ['deadly_throw', 'nerve_strike', 'combat_readiness'], - ['cheat_death', 'leeching_poison', 'elusiveness'], - ['preparation', 'shadowstep', 'burst_of_speed'], - ['prey_on_the_weak', 'paralytic_poison', 'dirty_tricks'], - ['shuriken_toss', 'versatility', 'anticipation'] - ] - major_glyphs = [ - 'ambush', - 'blade_flurry', - 'blind', - 'cloak_of_shadows', - 'crippling_poison', - 'deadly_throw', - 'evasion', - 'expose_armor', - 'fan_of_knives', - 'feint', - 'garrote', - 'gouge', - 'kick', - 'preparation', - 'sap', - 'sprint', - 'tricks_of_the_trade', - 'vanish', - ] - - minor_glyphs = [ - 'blurred_speed', - 'distract', - 'pick_lock', - 'pick_pocket', - 'poisons', - 'safe_fall' - ] - - current_glyphs = [] - talents = {} - MAX_TALENTS_PER_TIER = 4 - - def __init__(self, parent, calculator): - wx.Panel.__init__(self, parent) - self.calculator = calculator - - hbox = wx.BoxSizer(wx.HORIZONTAL) - hbox.Add(self.add_talents(), 0, wx.EXPAND) - hbox.Add(self.add_glyphs()) - self.SetSizer(hbox) - - def add_talents(self): - talents_box = wx.BoxSizer(wx.VERTICAL) - talents_box.Add(wx.StaticText(self, -1, label = "Talents")) - talents_box.Add(wx.StaticLine(self), 0, wx.ALL|wx.EXPAND, 5) - talents_box.Add(wx.StaticText(self, -1, label = "Assassination")) - talents_box.Add(self.add_talents_for_spec('assass'), 0, wx.EXPAND) - talents_box.Add(wx.StaticLine(self), 0, wx.ALL|wx.EXPAND, 5) - talents_box.Add(wx.StaticText(self, -1, label = "Combat")) - talents_box.Add(self.add_talents_for_spec('combat'), 0, wx.EXPAND) - talents_box.Add(wx.StaticLine(self), 0, wx.ALL|wx.EXPAND, 5) - talents_box.Add(wx.StaticText(self, -1, label = "Subtlety")) - talents_box.Add(self.add_talents_for_spec('subtlety'), 0, wx.EXPAND) - - return talents_box - - def add_talents_for_spec(self, spec): - spec_box = wx.FlexGridSizer(cols = 2 * self.MAX_TALENTS_PER_TIER) - talents = [] - talent_data = {} - current_tier = 1 - talents_this_tier = 0 - - for talent in talents: - #Funky ui stuff to get reasonable columns - if talent_data[talent][1] != current_tier: - while(talents_this_tier < self.MAX_TALENTS_PER_TIER): - spec_box.Add((10, 10)) - spec_box.Add((10, 10)) - talents_this_tier += 1 - current_tier += 1 - talents_this_tier = 0 - talents_this_tier += 1 - spec_box.Add(wx.StaticText(self, -1, label = talent), flag = wx.ALIGN_RIGHT) - combo = self.create_combo_with_max(talent_data[talent][0]) - if ui_data.default_talents.has_key(talent): - combo.SetSelection(ui_data.default_talents[talent]) - self.talents[talent] = combo - spec_box.Add(combo) - - return spec_box - - def create_combo_with_max(self, max_value): - cb = wx.ComboBox(self, -1, style = wx.CB_READONLY) - cb.Bind(wx.EVT_COMBOBOX, self.on_selection) - for value in range(0, max_value + 1): - cb.Append(str(value)) - cb.SetSelection(0) - return cb - - def add_glyphs(self): - vbox = wx.BoxSizer(wx.VERTICAL) - vbox.Add(wx.StaticText(self, -1, label = "Glyphs")) - vbox.Add(wx.StaticLine(self), 0, wx.ALL|wx.EXPAND, 5) - sizer = wx.FlexGridSizer(cols = 4) - vbox.Add(sizer) - - sizer.Add(wx.StaticText(self, -1, label = "Major: ")) - sizer.Add(self.add_glyph(self.major_glyphs)) - sizer.Add(self.add_glyph(self.major_glyphs)) - sizer.Add(self.add_glyph(self.major_glyphs)) - - sizer.Add(wx.StaticText(self, -1, label = "Minor: ")) - sizer.Add(self.add_glyph(self.minor_glyphs)) - sizer.Add(self.add_glyph(self.minor_glyphs)) - sizer.Add(self.add_glyph(self.minor_glyphs)) - - return vbox - - def add_glyph(self, options): - cb = wx.ComboBox(self, -1, style = wx.CB_READONLY) - cb.Bind(wx.EVT_COMBOBOX, self.on_selection) - cb.SetItems([''] + options) - self.current_glyphs.append(cb) - return cb - - def get_talents(self): - # TODO - talents = '000000' - #for each row - - return (talents) - - def get_glyphs(self): - glyphs_list = [] - for glyph in self.current_glyphs: - glyph_name = glyph.GetValue() - if len(glyph_name) > 0 and glyph_name not in glyphs_list: - glyphs_list.append(glyph_name) - return glyphs_list - - def on_selection(self, e): - self.calculator.calculate() - -class BuffsPage(wx.Panel): - current_buffs = [] - - def __init__(self, parent, calculator): - wx.Panel.__init__(self, parent) - self.calculator = calculator - - self.create_buff_checkboxes() - - def create_buff_checkboxes(self): - vbox = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(vbox) - - for buff in buffs.Buffs.allowed_buffs: - chk_box = wx.CheckBox(self, -1, buff, name = buff) - chk_box.SetValue(False) - vbox.Add(chk_box, 2, wx.BOTTOM) - chk_box.Bind(wx.EVT_CHECKBOX, lambda event, name = buff: self.on_check_changed(event, name)) - - self.current_buffs = list(buffs.Buffs.allowed_buffs) - - def on_check_changed(self, e, name): - chk_box = e.GetEventObject() - if (name in self.current_buffs) and (not chk_box.GetValue()): - self.current_buffs.remove(name) - elif not (name in self.current_buffs) and (chk_box.GetValue()): - self.current_buffs.append(name) - self.calculator.calculate() - - - def get_buff_string(self): - return ", ".join(self.current_buffs) - -class SettingsPage(wx.Panel): - def __init__(self, parent, calculator): - wx.Panel.__init__(self, parent) - self.calculator = calculator - sizer = wx.FlexGridSizer(cols = 2) - - sizer.Add(wx.StaticText(self, -1, label = "Race: ")) - sizer.Add(self.create_race_selector()) - sizer.Add(wx.StaticText(self, -1, label = "Cycle: ")) - sizer.Add(self.create_cycle_selector()) - sizer.Add(wx.StaticText(self, -1, label = "Response Time: ")) - sizer.Add(self.create_response_time_entry()) - - self.SetSizer(sizer) - - def create_race_selector(self): - races = race.Race.racial_stat_offset.keys() - cb = self.create_combobox_with_options(races) - self.race = cb - return cb - - def create_combobox_with_options(self, options): - cb = wx.ComboBox(self, -1, style = wx.CB_READONLY) - cb.Bind(wx.EVT_COMBOBOX, self.on_selection) - cb.SetItems(options) - cb.SetSelection(0) - return cb - - def create_cycle_selector(self): - cycles = ["Assassination", "Combat", "Subtlety"] - cb = self.create_combobox_with_options(cycles) - self.cycle = cb - test_cycle = settings.AssassinationCycle() - test_settings = settings.Settings(test_cycle, response_time=1) - return cb - - def create_response_time_entry(self): - tc = wx.TextCtrl(self, -1, value = '1') - self.response_time = tc - return tc - - def get_race(self): - return self.race.GetValue() - - def get_cycle(self): - cycle = '' - cur_cycle = self.cycle.GetValue() - if cur_cycle == "Assassination": - cycle = settings.AssassinationCycle() - elif cur_cycle == "Combat": - cycle = settings.CombatCycle() - elif cur_cycle == "Subtlety": - cycle = settings.SubtletyCycle() - return cycle - - def get_response_time(self): - response_time = 1 - if len(self.response_time.GetValue()) > 0: - reponse_time = float(self.response_time.GetValue()) - return response_time - - def on_selection(self, e): - self.calculator.calculate() - -class TestGUI(wx.Frame): - ep_stats = [ - 'white_hit', - 'yellow_hit', - 'str', - 'agi', - 'haste', - 'crit', - 'mastery', - 'dodge_exp', - 'parry_exp' - ] - - def __init__(self): - wx.Frame.__init__(self, None, title = "ShadowCraft") - self.initializing = True - vbox = wx.BoxSizer(wx.VERTICAL) - nb = wx.Notebook(self) - - self.gear_page = GearPage(nb, self) - self.talents_page = TalentsPage(nb, self) - self.buffs_page = BuffsPage(nb, self) - self.settings_page = SettingsPage(nb, self) - - nb.AddPage(self.gear_page, "Gear") - nb.AddPage(self.talents_page, "Talents") - nb.AddPage(self.buffs_page, "Buffs") - nb.AddPage(self.settings_page, "Settings") - vbox.Add(nb, 0, wx.EXPAND) - - self.error_area = wx.StaticText(self, -1) - self.error_area.SetForegroundColour("Red") - error_font = self.error_area.GetFont() - error_font.SetPointSize(16) - self.error_area.SetFont(error_font) - vbox.Add(self.error_area) - - results_area = self.create_results_area() - vbox.Add(results_area, 2, wx.EXPAND | wx.BOTTOM) - - self.SetSizer(vbox) - self.Fit() - self.initializing = False - self.calculate() - - def no_edit_text_box(self): - panel = wx.Panel(self, -1) - tb = wx.TextCtrl(self, -1, style = wx.TE_READONLY) - tb.SetBackgroundColour(panel.GetBackgroundColour()) - return tb - - def create_multiline_with_label(self, label, key): - vbox = wx.BoxSizer(wx.VERTICAL) - vbox.Add(wx.StaticText(self, -1, label = label)) - multi = wx.TextCtrl(self, -1, style = wx.TE_MULTILINE | wx.TE_READONLY) - vbox.Add(multi, 2, wx.EXPAND | wx.ALL) - setattr(self, key, multi) - return vbox - - def create_results_area(self): - hbox = wx.BoxSizer(wx.HORIZONTAL) - - dps_box = wx.FlexGridSizer(cols = 2) - dps_box.Add(wx.StaticText(self, -1, style = wx.ALIGN_RIGHT, label = "DPS: ")) - self.dps = self.no_edit_text_box() - dps_box.Add(self.dps, 2, wx.BOTTOM) - hbox.Add(dps_box, 2, wx.BOTTOM | wx.EXPAND) - - sizer = wx.FlexGridSizer(cols = 2) - for stat in GearPage.stats: - sizer.Add(wx.StaticText(self, -1, label = stat)) - stat_box = wx.TextCtrl(self, -1, style = wx.TE_READONLY) - setattr(self, stat, stat_box) - sizer.Add(stat_box) - hbox.Add(sizer, 2, wx.EXPAND) - - hbox.Add(self.create_multiline_with_label("EP Values", 'ep_box'), 2, wx.EXPAND | wx.ALL) - #TODO: add talents comparions here - hbox.Add(self.create_multiline_with_label("DPS Breakdown", 'dps_breakdown'), 2, wx.EXPAND | wx.ALL) - - return hbox - - def calculate(self): - # bugged - if not self.initializing: - gear_stats = self.gear_page.get_stats() - my_stats = stats.Stats(**gear_stats) - my_talents = talents.Talents(talent_string='311113') #(*self.talents_page.get_talents() ) - #my_talents = '311113' - my_glyphs = glyphs.Glyphs('rogue', *self.talents_page.get_glyphs()) #.RogueGlyphs(*self.talents_page.get_glyphs()) - my_buffs = buffs.Buffs(*self.buffs_page.current_buffs) - my_race = race.Race(self.settings_page.get_race()) - test_settings = settings.Settings(self.settings_page.get_cycle(), response_time = self.settings_page.get_response_time()) - - self.error_area.SetLabel("") - try: - calculator = AldrianasRogueDamageCalculator(my_stats, my_talents, my_glyphs, my_buffs, my_race, test_settings) - dps = calculator.get_dps() - ep_values = calculator.get_ep() - dps_breakdown = calculator.get_dps_breakdown() - - except exceptions.InvalidInputException as e: - self.error_area.SetLabel(str(e)) - - self.dps.SetValue(str(dps)) - self.ep_box.SetValue(self.pretty_print(ep_values)) - self.dps_breakdown.SetValue(self.pretty_print(dps_breakdown)) - for stat in GearPage.stats: - tc = getattr(self, stat) - tc.SetValue(str(gear_stats[stat])) - - def pretty_print(self, my_dict): - ret_str = '' - max_len = max(len(entry[0]) for entry in my_dict.items()) - dict_values = my_dict.items() - dict_values.sort(key=lambda entry: entry[1], reverse=True) - for value in dict_values: - ret_str += value[0] + ':' + ' ' * (max_len - len(value[0])) + str(value[1]) + os.linesep - - return ret_str - -if __name__ == "__main__": - app = wx.App(False) - gui = TestGUI(); - gui.Show() - app.MainLoop() diff --git a/test_ui/ui_data.py b/test_ui/ui_data.py deleted file mode 100644 index ee35a67..0000000 --- a/test_ui/ui_data.py +++ /dev/null @@ -1,319 +0,0 @@ -# the following items had incorrect stats and should be corrected now -# necklace of strife -# wind dancer tunic -# dispersing belt -# storm rider's boots -# wind dancer gloves -# Uhn'agh Fash -# Poison Protocol Pauldrons -# Wind Dancer's Spaulders -# lots of necks/heads - -import math - -class Item(object): - reforgable_stats = frozenset([ - 'crit', - 'hit', - 'exp', - 'haste', - 'mastery' - ]) - - def __init__(self, name, id=0, str=0, agi=0, ap=0, crit=0, hit=0, exp=0, haste=0, mastery=0, sockets=[], bonus_stat='', bonus_value=0, proc='', gear_buff=''): - self.name = name - self.id = id - self.str = str - self.agi = agi - self.ap = ap - self.crit = crit - self.hit = hit - self.exp = exp - self.haste = haste - self.mastery = mastery - self.sockets = sockets - self.bonus_stat = bonus_stat - self.bonus_value = bonus_value - self.proc = proc - self.gear_buff = gear_buff - - def reforgable_from(self): - reforgable = [] - for stat in self.reforgable_stats: - if getattr(self, stat) > 0: - reforgable.append(stat) - return reforgable - - def reforgable_to(self): - reforgable = [] - for stat in self.reforgable_stats: - if getattr(self, stat) == 0: - reforgable.append(stat) - return reforgable - - def reforge(self, from_stat, to_stat): - print "before: " + from_stat + " = " + str(getattr(self, from_stat)) - print " " + to_stat + " = " + str(getattr(self, to_stat)) - reforged_value = math.floor(getattr(self, from_stat) * 0.4) - setattr(self, from_stat, getattr(self, from_stat) - reforged_value) - setattr(self, to_stat, reforged_value) - print "after: " + from_stat + " = " + str(getattr(self, from_stat)) - print " " + to_stat + " = " + str(getattr(self, to_stat)) - -class Weapon(Item): - def __init__(self, name, id=0, str=0, agi=0, ap=0, crit=0, hit=0, exp=0, haste=0, mastery=0, sockets=[], bonus_stat='', bonus_value=0, proc='', gear_buff='', damage=0, speed=0, type=''): - super(Weapon, self).__init__(name, id, str, agi, ap, crit, hit, exp, haste, mastery, sockets, bonus_stat, bonus_value, proc, gear_buff) - self.damage = damage - self.speed = speed - self.type = type - -head = { - # ----- 5.0 ----- - '(H)Helmet of the Thousandfold Blades': {'id': 87126, 'agi': 1140, 'exp': 755, 'mastery': 828, 'sockets': ['blue', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 180, 'gear_buff': 'tier_14'}, - '(H)Crown of Opportunistic Strikes': {'id': 87070, 'agi': 1054, 'crit': 702, 'haste': 782, 'sockets': ['red', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 180}, - 'Helmet of the Thousandfold Blades': {'id': 85301, 'agi': 983, 'exp': 655, 'mastery': 720, 'sockets': ['blue', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 180, 'gear_buff': 'tier_14'}, - 'Crown of Opportunistic Strikes': {'id': 86146, 'agi': 906, 'crit': 604, 'haste': 684, 'sockets': ['red', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 180}, - 'Red Smoke Bandana': {'id': 89300, 'agi': 906, 'hit': 665, 'mastery': 616, 'sockets': ['blue', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 180}, - '(L)Helmet of the Thousandfold Blades': {'id': 86641, 'agi': 844, 'exp': 567, 'mastery': 624, 'sockets': ['blue', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 180, 'gear_buff': 'tier_14'}, - '(L)Crown of Opportunistic Strikes': {'id': 86804, 'agi': 775, 'crit': 517, 'haste': 597, 'sockets': ['red', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 180}, - 'Windblast Helm': {'id': 81283, 'agi': 659, 'haste': 520, 'mastery': 440, 'sockets': ['red', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 180}, - 'Soulburner Crown': {'id': 82853, 'agi': 659, 'hit': 537, 'crit': 410, 'sockets': ['red', 'meta'], 'bonus_stat': 'agi', 'bonus_value': 180}, -} -neck = { - # ----- 5.0 ----- - '(H)Choker of the Unleashed Storm': {'id': 86953, 'agi': 769, 'crit': 564, 'mastery': 426}, - '(H)Amulet of the Hidden Kings': {'id': 87045, 'agi': 720, 'haste': 502, 'exp': 445}, - 'Choker of the Unleashed Storm': {'id': 86166, 'agi': 682, 'crit': 500, 'mastery': 377}, - 'Amulet of the Hidden Kings': {'id': 86047, 'agi': 638, 'haste': 444, 'exp': 394}, - 'Choker of the Klaxxi\'va': {'id': 89065, 'agi': 638, 'hit': 363, 'crit': 463}, - 'Delicate Necklace of the Golden Lotus': {'id': 90593, 'agi': 638, 'crit': 426, 'mastery': 426}, - '(L)Choker of the Unleashed Storm': {'id': 86824, 'agi': 604, 'crit': 443, 'mastery': 334}, - '(L)Amulet of the Hidden Kings': {'id': 86776, 'agi': 566, 'haste': 394, 'exp': 349}, - 'Don Guerrero\'s Glorious Choker': {'id': 90583, 'agi': 566, 'haste': 287, 'mastery': 430}, - 'Scorched Scarlet Key': {'id': 81564, 'agi': 501, 'hit': 334, 'exp': 334}, - 'Engraved Amber Pendant': {'id': 81271, 'agi': 501, 'crit': 344, 'haste': 318}, -} -shoulders = { - # ----- 5.0 ----- - '(H)Spaulders of the Thousandfold Blades': {'id': 87128, 'agi': 946, 'haste': 520, 'exp': 733, 'sockets': ['blue'], 'bonus_stat': 'agi', 'bonus_value': 60, 'gear_buff': 'tier_14'}, - '(H)Netherrealm Shoulderpads': {'id': 87033, 'agi': 881, 'exp': 690, 'mastery': 447, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 60}, - 'Spaulders of the Thousandfold Blades': {'id': 85299, 'agi': 829, 'haste': 452, 'exp': 650, 'sockets': ['blue'], 'bonus_stat': 'agi', 'bonus_value': 60, 'gear_buff': 'tier_14'}, - 'Netherrealm Shoulderpads': {'id': 85995, 'agi': 771, 'exp': 607, 'mastery': 391, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 60}, - 'Imperion Spaulders': {'id': 89341, 'agi': 771, 'hit': 513, 'crit': 536, 'sockets': ['yellow'], 'bonus_stat': 'agi', 'bonus_value': 60}, - '(L)Spaulders of the Thousandfold Blades': {'id': 86639, 'agi': 725, 'haste': 391, 'exp': 576, 'sockets': ['blue'], 'bonus_stat': 'agi', 'bonus_value': 60, 'gear_buff': 'tier_14'}, - '(L)Netherrealm Shoulderpads': {'id': 86763, 'agi': 674, 'exp': 533, 'mastery': 342, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 60}, - 'Doubtridden Shoulderpads': {'id': 81071, 'agi': 668, 'hit': 434, 'exp': 452}, - 'Fizzy Spaulders': {'id': 81068, 'agi': 668, 'crit': 412, 'haste': 465}, -} -back = { - # ----- 5.0 ----- - '(H)Legbreaker Greatcloak': {'id': 86963, 'agi': 769, 'crit': 513, 'mastery': 513}, - '(H)Arrow Breaking Windcloak': {'id': 87044, 'agi': 720, 'haste': 399, 'exp': 529}, - 'Legbreaker Greatcloak': {'id': 86173, 'agi': 682, 'crit': 454, 'mastery': 454}, - 'Arrow Breaking Windcloak': {'id': 86082, 'agi': 638, 'haste': 353, 'exp': 468}, - 'Blackguard Cape': {'id': 89076, 'agi': 638, 'hit': 444, 'mastery': 394}, - '(L)Legbreaker Greatcloak': {'id': 86831, 'agi': 604, 'crit': 402, 'mastery': 402}, - '(L)Arrow Breaking Windcloak': {'id': 86782, 'agi': 566, 'haste': 313, 'exp': 415}, - 'Dory\'s Pageantry': {'id': 86782, 'agi': 566, 'hit': 377, 'crit': 377}, - 'Aerial Bombardment Cloak': {'id': 81282, 'agi': 501, 'hit': 254, 'crit': 381}, - 'Wind-soaked Drape': {'id': 81123, 'agi': 501, 'crit': 358, 'mastery': 293}, -} -chest = { - # ----- 5.0 ----- - '(H)Tunic of the Thousandfold Blades': {'id': 87124, 'agi': 1220, 'crit': 880, 'mastery': 800, 'sockets': ['yellow', 'yellow'], 'bonus_stat': 'mastery', 'bonus_value': 120, 'gear_buff': 'tier_14'}, - '(H)Chestguard of Total Annihilation': {'id': 87058, 'agi': 1134, 'crit': 698, 'haste': 833, 'sockets': ['red', 'yellow'], 'bonus_stat': 'agi', 'bonus_value': 120}, - 'Tunic of the Thousandfold Blades': {'id': 85303, 'agi': 1063, 'crit': 775, 'mastery': 695, 'sockets': ['yellow', 'yellow'], 'bonus_stat': 'mastery', 'bonus_value': 120, 'gear_buff': 'tier_14'}, - 'Chestguard of Total Annihilation': {'id': 86136, 'agi': 986, 'crit': 609, 'haste': 729, 'sockets': ['red', 'yellow'], 'bonus_stat': 'agi', 'bonus_value': 120}, - 'Softfoot Silentwrap': {'id': 89431, 'agi': 986, 'hit': 554, 'exp': 761, 'sockets': ['blue', 'yellow'], 'bonus_stat': 'agi', 'bonus_value': 120}, - 'Chestguard of Nemeses': {'id': 85788, 'agi': 1223, 'crit': 755, 'mastery': 852}, - '(L)Tunic of the Thousandfold Blades': {'id': 86643, 'agi': 924, 'crit': 683, 'mastery': 603, 'sockets': ['yellow', 'yellow'], 'bonus_stat': 'mastery', 'bonus_value': 120, 'gear_buff': 'tier_14'}, - 'Greyshadow Chestguard': {'id': 85823, 'agi': 1015, 'crit': 660, 'mastery': 687}, - 'Vulajin\'s Vicious Breastplate': {'id': 90585, 'agi': 855, 'crit': 557, 'mastery': 637, 'sockets': ['red', 'yellow'], 'bonus_stat': 'mastery', 'bonus_value': 20}, #bug? seems like bonus should be 120 - '(L)Chestguard of Total Annihilation': {'id': 86795, 'agi': 855, 'crit': 530, 'haste': 636, 'sockets': ['red', 'yellow'], 'bonus_stat': 'agi', 'bonus_value': 120}, - 'Korloff\'s Raiment': {'id': 81573, 'agi': 899, 'hit': 456, 'crit': 683}, - 'Nimbletoe Chestguard': {'id': 81080, 'agi': 899, 'crit': 609, 'haste': 584}, - 'Delicate Chestguard of the Golden Lotus': {'id': 90597, 'agi': 899, 'crit': 660, 'mastery': 497}, - 'Refurbished Zandalari Vestment': {'id': 89667, 'agi': 797, 'haste': 555, 'exp': 492}, -} -wrists = { - # ----- 5.0 ----- - '(H)Bracers of Unseen Strikes': {'id': 86954, 'agi': 769, 'crit': 487, 'haste': 528}, - '(H)Smooth Bettle Wristbands': {'id': 86995, 'agi': 769, 'exp': 414, 'mastery': 571}, - 'Bracers of Unseen Strikes': {'id': 86163, 'agi': 682, 'crit': 432, 'haste': 468}, - 'Smooth Bettle Wristbands': {'id': 86185, 'agi': 682, 'exp': 366, 'mastery': 506}, - 'Quillpaw Family Bracers': {'id': 88884, 'agi': 638, 'hit': 426, 'crit': 426}, - '(L)Bracers of Unseen Strikes': {'id': 86821, 'agi': 604, 'crit': 383, 'haste': 414}, - '(L)Smooth Bettle Wristbands': {'id': 86843, 'agi': 604, 'exp': 325, 'mastery': 448}, - 'Lightblade Bracer': {'id': 81700, 'agi': 501, 'crit': 363, 'mastery': 285}, - 'Saboteur\'s Stabilizing Bracers': {'id': 81090, 'agi': 501, 'haste': 339, 'exp': 326}, -} -hands = { - # ----- 5.0 ----- - '(H)Gloves of the Thousandfold Blades': {'id': 87125, 'agi': 1026, 'hit': 724, 'crit': 616, 'gear_buff': 'tier_14'}, - '(H)Bonebreaker Gauntlets': {'id': 86964, 'agi': 946, 'hit': 495, 'haste': 730, 'sockets': ['blue'], 'bonus_stat': 'haste', 'bonus_value': 60}, - 'Murderer\'s Gloves': {'id': 85828, 'agi': 909, 'crit': 615, 'mastery': 591}, - 'Gloves of the Thousandfold Blades': {'id': 85302, 'agi': 909, 'hit': 641, 'crit': 546, 'gear_buff': 'tier_14'}, - 'Bonebreaker Gauntlets': {'id': 86176, 'agi': 829, 'hit': 434, 'haste': 643, 'sockets': ['blue'], 'bonus_stat': 'haste', 'bonus_value': 60}, - 'Fingers of the Loneliest Monk': {'id': 88744, 'agi': 851, 'exp': 593, 'mastery': 526}, - '(L)Bonebreaker Gauntlets': {'id': 86834, 'agi': 725, 'hit': 380, 'haste': 565, 'sockets': ['blue'], 'bonus_stat': 'haste', 'bonus_value': 60}, - '(L)Gloves of the Thousandfold Blades': {'id': 86642, 'agi': 805, 'hit': 568, 'crit': 484, 'gear_buff': 'tier_14'}, - 'Greyshadow Gloves': {'id': 85824, 'agi': 754, 'crit': 490, 'mastery': 510}, - 'Tombstone Gauntlets': {'id': 82858, 'agi': 668, 'hit': 349, 'haste': 502}, - 'Hound Trainer\'s Gloves': {'id': 81695, 'agi': 668, 'exp': 391, 'mastery': 478}, -} -waist = { - # ----- 5.0 ----- - '(H)Stalker\'s Cord of Eternal Autumn': {'id': 87180, 'agi': 946, 'hit': 593, 'crit': 674, 'sockets': ['blue', 'prismatic'], 'bonus_stat': 'crit', 'bonus_value': 60}, - '(H)Tomb Raider\'s Girdle': {'id': 87022, 'agi': 801, 'haste': 598, 'exp': 498, 'sockets': ['yellow', 'blue', 'prismatic'], 'bonus_stat': 'exp', 'bonus_value': 120}, - 'Stalker\'s Cord of Eternal Autumn': {'id': 86341, 'agi': 829, 'hit': 521, 'crit': 593, 'sockets': ['blue', 'prismatic'], 'bonus_stat': 'crit', 'bonus_value': 60}, - 'Tomb Raider\'s Girdle': {'id': 85982, 'agi': 691, 'haste': 521, 'exp': 432, 'sockets': ['yellow', 'blue', 'prismatic'], 'bonus_stat': 'exp', 'bonus_value': 120}, - 'Klaxxi Lash of the Borrower': {'id': 89060, 'agi': 771, 'crit': 391, 'mastery': 607, 'sockets': ['blue', 'prismatic'], 'bonus_stat': 'crit', 'bonus_value': 60}, - '(L)Stalker\'s Cord of Eternal Autumn': {'id': 86899, 'agi': 725, 'hit': 457, 'crit': 520, 'sockets': ['blue', 'prismatic'], 'bonus_stat': 'crit', 'bonus_value': 60}, - '(L)Tomb Raider\'s Girdle': {'id': 86750, 'agi': 594, 'haste': 452, 'exp': 373, 'sockets': ['yellow', 'blue', 'prismatic'], 'bonus_stat': 'exp', 'bonus_value': 120}, - 'Icewrath Belt': {'id': 82823, 'agi': 668, 'crit': 401, 'mastery': 471}, - 'Belt of Brazen Inebriation': {'id': 81135, 'agi': 668, 'hit': 412, 'exp': 465}, -} -legs = { - # ----- 5.0 ----- - '(H)Legguards of the Thousandfold Blades': {'id': 87127, 'agi': 1300, 'hit': 659, 'haste': 1009, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 60, 'gear_buff': 'tier_14'}, - '(H)Stoneflesh Leggings': {'id': 87013, 'agi': 1134, 'crit': 845, 'exp': 677, 'sockets': ['red', 'blue'], 'bonus_stat': 'agi', 'bonus_value':120}, - 'Legguards of the Thousandfold Blades': {'id': 85300, 'agi': 1143, 'hit': 580, 'haste': 890, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 60, 'gear_buff': 'tier_14'}, - 'Stoneflesh Leggings': {'id': 85926, 'agi': 986, 'crit': 740, 'exp': 590, 'sockets': ['red', 'blue'], 'bonus_stat': 'agi', 'bonus_value':120}, - 'Dreadsworn Slayer Legs': {'id': 89090, 'agi': 1066, 'crit': 801, 'mastery': 594, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value':60}, - '(L)Legguards of the Thousandfold Blades': {'id': 86640, 'agi': 1004, 'hit': 509, 'haste': 784, 'sockets': ['red'], 'bonus_stat': 'agi', 'bonus_value': 60, 'gear_buff': 'tier_14'}, - '(L)Stoneflesh Leggings': {'id': 86743, 'agi': 855, 'crit': 646, 'exp': 514, 'sockets': ['red', 'blue'], 'bonus_stat': 'agi', 'bonus_value':120}, - 'Wall-Breaker Legguards': {'id': 81091, 'agi': 899, 'exp': 584, 'mastery': 609}, - 'Ghostwoven Legguards': {'id': 82851, 'agi': 899, 'crit': 497, 'haste': 660}, -} -feet = { - # ----- 5.0 ----- - '(H)Boots of the Still Breath': {'id': 86943, 'agi': 946, 'crit': 551, 'haste': 695, 'sockets': ['yellow'], 'bonus_stat': 'crit', 'bonus_value': 60}, - '(H)Treads of Deadly Secretions': {'id': 86984, 'agi': 946, 'exp': 568, 'mastery': 685, 'sockets': ['yellow'], 'bonus_stat': 'exp', 'bonus_value': 60}, - 'Boots of the Still Breath': {'id': 86153, 'agi': 829, 'crit': 485, 'haste': 610, 'sockets': ['yellow'], 'bonus_stat': 'crit', 'bonus_value': 60}, - 'Treads of Deadly Secretions': {'id': 86984, 'agi': 829, 'exp': 500, 'mastery': 602, 'sockets': ['yellow'], 'bonus_stat': 'exp', 'bonus_value': 60}, - 'Tukka-Tuk\'s Hairy Boots': {'id': 88868, 'agi': 851, 'hit': 601, 'haste': 512}, - '(L)Boots of the Still Breath': {'id': 86811, 'agi': 725, 'crit': 426, 'haste': 535, 'sockets': ['yellow'], 'bonus_stat': 'crit', 'bonus_value': 60}, - '(L)Treads of Deadly Secretions': {'id': 86859, 'agi': 725, 'exp': 439, 'mastery': 528, 'sockets': ['yellow'], 'bonus_stat': 'exp', 'bonus_value': 60}, - 'Dashing Strike Treads': {'id': 81688, 'agi': 668, 'crit': 391, 'mastery': 478}, - 'Boots of Plummeting Death': {'id': 81249, 'agi': 668, 'haste': 434, 'exp': 452}, -} -rings = { - # ----- 5.0 ----- - '(HE)Regail\'s Band of the Endless': {'id': 90503, 'agi': 821, 'crit': 571, 'haste': 507}, - '(H)Painful Thorned Ring': {'id': 86974, 'agi': 769, 'exp': 438, 'mastery': 557}, - '(H)Regail\'s Band of the Endless': {'id': 87144, 'agi': 769, 'crit': 536, 'haste': 475}, - '(E)Regail\'s Band of the Endless': {'id': 90517, 'agi': 727, 'crit': 506, 'haste': 449}, - 'Painful Thorned Ring': {'id': 86200, 'agi': 682, 'exp': 388, 'mastery': 494}, - 'Regail\'s Band of the Endless': {'id': 86231, 'agi': 682, 'crit': 474, 'haste': 421}, - 'Anji\'s Keepsake': {'id': 89070, 'agi': 638, 'hit': 323, 'haste': 485}, - '(L)Painful Thorned Ring': {'id': 86851, 'agi': 604, 'exp': 343, 'mastery': 437}, - '(L)Regail\'s Band of the Endless': {'id': 86869, 'agi': 604, 'crit': 420, 'haste': 373}, - 'Perculia\'s Peculiar Signet': {'id': 90584, 'agi': 566, 'hit': 322, 'haste': 410}, - 'Seal of Ghoulish Glee': {'id': 88168, 'agi': 535, 'hit': 313, 'crit': 382}, - 'Pulled Grenade Pin': {'id': 81191, 'agi': 501, 'crit': 349, 'mastery': 309}, - 'Signet of Dancing Jade': {'id': 81128, 'agi': 501, 'crit': 309, 'exp': 349}, - 'Seal of Hateful Meditation': {'id': 81186, 'agi': 501, 'hit': 254, 'haste': 381}, -} -ring1 = rings -ring2 = rings -trinkets = { - # ----- 5.0 ----- - '(H)Terror in the Mists': {'id': 87167, 'agi': 1300, 'proc': 'heroic_terror_in_the_mists'}, - '(H)Jade Bandit Figurine': {'id': 87079, 'agi': 1218, 'proc': 'heroic_jade_bandit_figurine'}, - '(H)Bottle of Infinite Stars': {'id': 87057, 'mastery': 1218, 'proc': 'heroic_bottle_of_infinite_stars'}, - 'Terror in the Mists': {'id': 86332, 'agi': 1152, 'proc': 'terror_in_the_mists'}, - 'Jade Bandit Figurine': {'id': 86043, 'agi': 1079, 'proc': 'jade_bandit_figurine'}, - 'Bottle of Infinite Stars': {'id': 86132, 'mastery': 1079, 'proc': 'bottle_of_infinite_stars'}, - "Hawkmaster's Talon": {'id': 89082, 'agi': 1079, 'proc': 'hawkmasters_talon'}, - '(L)Terror in the Mists': {'id': 86890, 'agi': 1021, 'proc': 'lfr_terror_in_the_mists'}, - '(L)Jade Bandit Figurine': {'id': 87079, 'agi': 956, 'proc': 'lfr_jade_bandit_figurine'}, - '(L)Bottle of Infinite Stars': {'id': 87057, 'mastery': 956, 'proc': 'lfr_bottle_of_infinite_stars'}, - 'Relic of Xuen': {'id': 79328, 'agi': 956, 'proc': 'relic_of_xuen'}, - "Coren's Cold Chromium Coaster": {'id': 87574, 'crit': 904, 'proc': 'corens_cold_chromium_coaster'}, - 'Flashing Steel Talisman' : {'id': 81265, 'hit': 847, 'proc': 'flashing_steel_talisman'}, - 'Searing Words' : {'id': 81267, 'crit': 847, 'proc': 'searing_words'}, - 'Windswept Pages' : {'id': 81125, 'agi': 847, 'proc': 'windswept_pages'}, - #'Shadow-Pan Dragon Gun' : {'id': 88995, 'proc': 'shadow_pan_dragon_gun'}, #worth modelling? - 'Zen Alchemist Stone': {'id': 75274, 'mastery': 751, 'proc': 'zen_alchemist_stone'}, -} -trinket1 = trinkets -trinket2 = trinkets -melee_weapons = { - # ----- 5.0 ----- - #daggers - 'd-(H)Spiritsever': {'id': 87166, 'agi': 592, 'exp': 337, 'mastery': 429, 'damage': 6733, 'speed': 1.8, 'type': 'dagger','sockets': ['sha'], 'bonus_stat': 'agi', 'bonus_value': 0}, - 'd-(H)Dagger of the Seven Stars': {'id': 87012, 'agi': 554, 'hit': 324, 'haste': 396, 'damage': 6308, 'speed': 1.8, 'type': 'dagger'}, - 'd-Spiritsever': {'id': 86391, 'agi': 524, 'exp': 298, 'mastery': 380, 'damage': 5965, 'speed': 1.8, 'type': 'dagger','sockets': ['sha'], 'bonus_stat': 'agi', 'bonus_value': 0}, - 'd-Dagger of the Seven Stars': {'id': 85924, 'agi': 491, 'hit': 287, 'haste': 351, 'damage': 5588, 'speed': 1.8, 'type': 'dagger'}, - 'd-(L)Spiritsever': {'id': 86910, 'agi': 464, 'exp': 264, 'mastery': 336, 'damage': 5284.5, 'speed': 1.8, 'type': 'dagger','sockets': ['sha'], 'bonus_stat': 'agi', 'bonus_value': 0}, - 'd-(L)Dagger of the Seven Stars': {'id': 86741, 'agi': 435, 'hit': 254, 'haste': 311, 'damage': 4951, 'speed': 1.8, 'type': 'dagger'}, - 'd-Tolakesh, Horn of the Black Ox': {'id': 87547, 'agi': 435, 'hit': 294, 'exp': 283, 'damage': 4951, 'speed': 1.8, 'type': 'dagger'}, - "d-Amber Slicer of Klaxxi'vess": {'id': 89393, 'agi': 385, 'haste': 261, 'mastery': 251, 'damage': 4386, 'speed': 1.8, 'type': 'dagger'}, - "d-Koegler's Ritual Knife": {'id': 82813, 'agi': 385, 'hit': 201, 'mastery': 290, 'damage': 4386, 'speed': 1.8, 'type': 'dagger'}, - 'd-Mantid Trochanter': {'id': 81088, 'agi': 385, 'crit': 251, 'haste': 261, 'damage': 4386, 'speed': 1.8, 'type': 'dagger'}, - 'd-Masterwork Ghost Shard': {'id': 82974, 'agi': 385, 'crit': 283, 'mastery': 213, 'damage': 4386, 'speed': 1.8, 'type': 'dagger'}, - #swords, maces, fists, axes - "f-(H)Claws of Shek'zeer": {'id': 86988, 'agi': 592, 'crit': 444, 'exp': 309, 'damage': 9725.5, 'speed': 2.6, 'type': 'fist','sockets': ['sha'], 'bonus_stat': 'agi', 'bonus_value': 0}, - "f-(H)Gara'kal, Fist of the Spiritbinder": {'id': 87032, 'agi': 554, 'haste': 391, 'mastery': 333, 'damage': 9111.5, 'speed': 2.6, 'type': 'fist'}, - "f-Claws of Shek'zeer": {'id': 86226, 'agi': 524, 'crit': 394, 'exp': 274, 'damage': 8616, 'speed': 2.6, 'type': 'fist','sockets': ['sha'], 'bonus_stat': 'agi', 'bonus_value': 0}, - "f-Gara'kal, Fist of the Spiritbinder": {'id': 85994, 'agi': 491, 'haste': 347, 'mastery': 295, 'damage': 8072, 'speed': 2.6, 'type': 'fist'}, - "f-(L)Claws of Shek'zeer": {'id': 86864, 'agi': 464, 'crit': 349, 'exp': 242, 'damage': 7633.5, 'speed': 2.6, 'type': 'fist','sockets': ['sha'], 'bonus_stat': 'agi', 'bonus_value': 0}, - "f-(L)Gara'kal, Fist of the Spiritbinder": {'id': 86762, 'agi': 435, 'haste': 307, 'mastery': 261, 'damage': 7151, 'speed': 2.6, 'type': 'fist'}, - "f-Ka'eng, Breath of the Shadow": {'id': 87543, 'agi': 355, 'crit': 287, 'exp': 187, 'damage': 7151, 'speed': 2.6, 'type': 'fist', 'sockets': ['blue'], 'bonus_stat': 'exp', 'bonus_value': 60}, - "f-Claws of Gekkan": {'id': 81245, 'agi': 385, 'crit': 272, 'haste': 232, 'damage': 6335, 'speed': 2.6, 'type': 'fist'}, - "f-Ner'onok's Razor Katar": {'id': 81286, 'agi': 385, 'hit': 257, 'exp': 257, 'damage': 6335, 'speed': 2.6, 'type': 'fist'}, -} -mainhand = melee_weapons -offhand = melee_weapons - -default_talents = { - # -} - -enchants = { - 'head': { - #'Arcanum of the Ramkahen':{'agi': 60, 'haste': 35} - }, - 'shoulders': { - #'Greater Inscription of Shattered Crystal': {'agi': 50, 'mastery': 25}, - #'Lesser Inscription of Shattered Crystal': {'agi': 30, 'mastery': 20} - }, - 'back': { - 'Greater Critical Strike': {'crit': 65}, - 'Major Agility': {'agi': 22} - }, - 'chest': {'Peerless Stats': {'agi': 20, 'str': 20}}, - 'wrists': { - 'Greater Speed': {'haste': 50}, - 'Greater Expertise': {'exp': 50}, - 'Precision': {'hit': 50}, - '(LW)Draconic Embossment':{'agi': 130} - }, - 'hands': { - 'Greater Mastery':{'mastery': 65}, - 'Greater Expertise': {'exp': 50}, - 'Haste': {'haste': 50} - }, - 'legs': {'Dragonbone': {'ap': 190, 'crit': 55}}, - 'feet': { - 'Major Agility': {'agi': 35}, - 'Mastery':{'mastery': 50}, - 'Precision': {'hit': 50}, - 'Haste': {'haste': 50} - }, - 'rings': {'dummy1': {}}, - 'melee_weapons': { - 'Landslide': 'landslide', - 'Hurricane': 'hurricane' - } -} - -gems = { - "Fleet Mists Metagem": (['meta'], {'mastery': 432}), - "Agile Mists Metagem": (['meta'], {'agi': 216, 'gear_buff': ['chaotic_metagem']}), - "Delicate Mists Gem": (['red'], {'agi': 160}), - "Adept Mists Gem": (['red', 'yellow'], {'agi': 80, 'mastery': 160}), - "Deft Mists Gem": (['red', 'yellow'], {'agi': 80, 'haste': 160}), - "Glinting Mists Gem": (['red', 'blue'], {'agi': 80, 'hit': 160}), - "Rigid Mists Gem": (['blue'], {'hit': 320}) -} \ No newline at end of file diff --git a/tests/calcs_tests/__init__.py b/tests/calcs_tests/__init__.py index 6d0190c..b3fcb6a 100644 --- a/tests/calcs_tests/__init__.py +++ b/tests/calcs_tests/__init__.py @@ -5,21 +5,28 @@ from shadowcraft.objects import race from shadowcraft.objects import stats from shadowcraft.objects import procs -from shadowcraft.objects import glyphs +from shadowcraft.objects import talents +from shadowcraft.objects import artifact class TestDamageCalculator(unittest.TestCase): - def make_calculator(self, buffs_list=[], gear_buffs_list=[], race_name='night_elf'): + def make_calculator(self, buffs_list=[], gear_buffs_list=[], race_name='night_elf', test_spec='outlaw'): test_buffs = buffs.Buffs(*buffs_list) test_gear_buffs = stats.GearBuffs(*gear_buffs_list) test_procs = procs.ProcsList() - test_mh = stats.Weapon(737, 1.8, 'dagger', 'hurricane') - test_oh = stats.Weapon(573, 1.4, 'dagger', 'hurricane') + test_mh = stats.Weapon(737, 1.8, 'dagger') + test_oh = stats.Weapon(573, 1.4, 'dagger') test_ranged = stats.Weapon(1104, 2.0, 'thrown') - test_stats = stats.Stats(20, 3485, 190, 1517, 1086, 641, 899, 666, test_mh, test_oh, test_ranged, test_procs, test_gear_buffs) + test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, + agi=20909, + stam=19566, + crit=4402, + haste=5150, + mastery=5999, + versatility=1515) test_race = race.Race(race_name) - test_talents = None - test_glyphs = glyphs.Glyphs() - return calcs.DamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race) + test_talents = talents.Talents('1000000', test_spec, 'rogue', level=110) + test_traits = artifact.Artifact(test_spec, 'rogue', '000000000000000000') + return calcs.DamageCalculator(test_stats, test_talents, test_traits, test_buffs, test_race, 'outlaw') def setUp(self): self.calculator = self.make_calculator() @@ -29,84 +36,16 @@ def test_melee_hit_chance(self): def test_one_hand_melee_hit_chance(self): self.assertAlmostEqual( - self.calculator.one_hand_melee_hit_chance(dodgeable=False, parryable=False), - 1.0) + self.calculator.one_hand_melee_hit_chance(dodgeable=False, parryable=False), 1.0) self.assertAlmostEqual( - self.calculator.one_hand_melee_hit_chance(dodgeable=True, parryable=False), - 1.0 - (0.065 - (641 / (30.027200698852539 * 4)) * 0.01)) - self.calculator.stats.exp = 0 + self.calculator.one_hand_melee_hit_chance(dodgeable=True, parryable=False), 1.0) self.assertAlmostEqual( - self.calculator.one_hand_melee_hit_chance(dodgeable=True, parryable=False), - 1.0 - 0.065) + self.calculator.one_hand_melee_hit_chance(dodgeable=True, parryable=True), 1.0 - 0.03) self.assertAlmostEqual( - self.calculator.one_hand_melee_hit_chance(dodgeable=True, parryable=True), - 1.0 - 0.14 - 0.065) - self.assertAlmostEqual( - self.calculator.one_hand_melee_hit_chance(dodgeable=False, parryable=True), - 1.0 - 0.14) - self.calculator.stats.hit = 0 - self.assertAlmostEqual( - self.calculator.one_hand_melee_hit_chance(dodgeable=True, parryable=False), - 1.0 - 0.065 - 0.08) + self.calculator.one_hand_melee_hit_chance(dodgeable=False, parryable=True), 1.0 - 0.03) def test_dual_wield_mh_hit_chance(self): - self.assertAlmostEqual( - self.calculator.dual_wield_mh_hit_chance(dodgeable=False, parryable=False), - 1.0 - (0.27 - 0.01 * (1086 / 120.109001159667969))) - self.calculator.stats.hit = 0 - self.calculator.stats.exp = 0 - self.assertAlmostEqual( - self.calculator.dual_wield_mh_hit_chance(dodgeable=False, parryable=False), - 1.0 - 0.27) - self.assertAlmostEqual( - self.calculator.dual_wield_mh_hit_chance(dodgeable=True, parryable=False), - 1.0 - 0.27 - 0.065) - self.assertAlmostEqual( - self.calculator.dual_wield_mh_hit_chance(dodgeable=True, parryable=True), - 1.0 - 0.27 - 0.065 - 0.14) - self.assertAlmostEqual( - self.calculator.dual_wield_mh_hit_chance(dodgeable=False, parryable=True), - 1.0 - 0.27 - 0.14) - - def test_dual_wield_oh_hit_chance(self): - pass - - def test_spell_hit_chance(self): - self.assertAlmostEqual(self.calculator.spell_hit_chance(), - 1.0 - (0.17 - 0.01 * (1086 / 102.445999145507812))) - - def test_buff_melee_crit(self): - pass - - def test_buff_spell_crit(self): - pass - - def test_target_armor(self): - pass - - def test_raid_settings_modifiers(self): - self.assertRaises(exceptions.InvalidInputException, self.calculator.raid_settings_modifiers, '') - - def test_mixology_no_flask(self): - test_calculator = self.make_calculator(gear_buffs_list=['mixology']) - self.assertEqual(test_calculator.stats.agi, self.calculator.stats.agi) - - def test_mixology(self): - test_calculator = self.make_calculator(buffs_list=['agi_flask'], gear_buffs_list=['mixology']) - self.assertEqual(test_calculator.stats.agi, self.calculator.stats.agi + 80) - - def test_master_of_anatomy(self): - test_calculator = self.make_calculator(gear_buffs_list=['master_of_anatomy']) - self.assertEqual(test_calculator.stats.crit, self.calculator.stats.crit + 80) - - def test_get_all_activated_stat_boosts(self): - calculator = self.make_calculator(gear_buffs_list=['leather_specialization', 'potion_of_the_tolvir'], race_name='orc') - boosts = calculator.get_all_activated_stat_boosts() - self.assertEqual(len(boosts), 3) # blood fury sp, blood fury ap, potion of the tolvir - for boost in boosts: - if boost['stat'] == 'ap': - self.assertEqual(boost['value'], 1170) - elif boost['stat'] == 'sp': - self.assertEqual(boost['value'], 585) - elif boost['stat'] == 'agi': - self.assertEqual(boost['value'], 1200) + self.assertAlmostEqual(self.calculator.dual_wield_mh_hit_chance(dodgeable=False, parryable=False), 1.0 - 0.19) + self.assertAlmostEqual(self.calculator.dual_wield_mh_hit_chance(dodgeable=True, parryable=False), 1.0 - 0.19) + self.assertAlmostEqual(self.calculator.dual_wield_mh_hit_chance(dodgeable=False, parryable=True), 1.0 - 0.19 - 0.03) + self.assertAlmostEqual(self.calculator.dual_wield_mh_hit_chance(dodgeable=True, parryable=True), 1.0 - 0.19 - 0.03) diff --git a/tests/calcs_tests/armor_mitigation_tests.py b/tests/calcs_tests/armor_mitigation_tests.py deleted file mode 100644 index 627d587..0000000 --- a/tests/calcs_tests/armor_mitigation_tests.py +++ /dev/null @@ -1,43 +0,0 @@ -from shadowcraft import calcs -import unittest -from shadowcraft.core import exceptions -from shadowcraft.calcs import armor_mitigation - -class TestArmorMitigation(unittest.TestCase): - def test_thresholds(self): - self.assertRaises(exceptions.InvalidLevelException, armor_mitigation.lookup_parameters, 0) - self.assertEqual(1, armor_mitigation.lookup_parameters(1)[0]) - self.assertEqual(1, armor_mitigation.lookup_parameters(59)[0]) - self.assertEqual(60, armor_mitigation.lookup_parameters(60)[0]) - self.assertEqual(60, armor_mitigation.lookup_parameters(80)[0]) - self.assertEqual(81, armor_mitigation.lookup_parameters(81)[0]) - - def test_parameter_spot_checks(self): - self.assertAlmostEqual( 5882.5, armor_mitigation.parameter(60)) - self.assertAlmostEqual(10557.5, armor_mitigation.parameter(70)) - self.assertAlmostEqual(15232.5, armor_mitigation.parameter(80)) - self.assertAlmostEqual(26070.0, armor_mitigation.parameter(85)) - - def test_mitigation_spot_checks(self): - self.assertAlmostEqual(0.4441, armor_mitigation.mitigation(4700, 60), 4) - self.assertAlmostEqual(0.4217, armor_mitigation.mitigation(7700, 70), 4) - self.assertAlmostEqual(0.4109, armor_mitigation.mitigation(10623, 80), 4) - self.assertAlmostEqual(0.3148, armor_mitigation.mitigation(11977, 85), 4) - - def test_mitigation_cached_parameter(self): - self.assertAlmostEqual(0.4441, armor_mitigation.mitigation(4700, 60, 5882.5), 4) - self.assertAlmostEqual(0.4217, armor_mitigation.mitigation(7700, 70, 10557.5), 4) - self.assertAlmostEqual(0.4109, armor_mitigation.mitigation(10623, 80, 15232.5), 4) - self.assertAlmostEqual(0.3148, armor_mitigation.mitigation(11977, 85, 26070.0), 4) - - def test_multiplier_spot_checks(self): - self.assertAlmostEqual(1 - 0.4441, armor_mitigation.multiplier(4700, 60), 4) - self.assertAlmostEqual(1 - 0.4217, armor_mitigation.multiplier(7700, 70), 4) - self.assertAlmostEqual(1 - 0.4109, armor_mitigation.multiplier(10623, 80), 4) - self.assertAlmostEqual(1 - 0.3148, armor_mitigation.multiplier(11977, 85), 4) - - def test_multiplier_cached_paramter(self): - self.assertAlmostEqual(1 - 0.4441, armor_mitigation.multiplier(4700, 60, 5882.5), 4) - self.assertAlmostEqual(1 - 0.4217, armor_mitigation.multiplier(7700, 70, 10557.5), 4) - self.assertAlmostEqual(1 - 0.4109, armor_mitigation.multiplier(10623, 80, 15232.5), 4) - self.assertAlmostEqual(1 - 0.3148, armor_mitigation.multiplier(11977, 85, 26070.0), 4) diff --git a/tests/calcs_tests/rogue_tests/Aldriana_tests/__init__.py b/tests/calcs_tests/rogue_tests/Aldriana_tests/__init__.py deleted file mode 100644 index 97a7cbf..0000000 --- a/tests/calcs_tests/rogue_tests/Aldriana_tests/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -import unittest -from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator -from shadowcraft.calcs.rogue.Aldriana import settings - -from shadowcraft.objects import buffs -from shadowcraft.objects import race -from shadowcraft.objects import stats -from shadowcraft.objects import procs -from shadowcraft.objects.rogue import rogue_talents -from shadowcraft.objects.rogue import rogue_glyphs - -class TestAldrianasRogueDamageCalculator(unittest.TestCase): - def test_get_ep(self): - test_buffs = buffs.Buffs() - test_mh = stats.Weapon(939.5, 1.8, 'dagger', 'landslide') - test_oh = stats.Weapon(730.5, 1.4, 'dagger', 'landslide') - test_ranged = stats.Weapon(1371.5, 2.2, 'thrown') - test_procs = procs.ProcsList('heroic_prestors_talisman_of_machination', 'fluid_death', 'rogue_t11_4pc') - test_gear_buffs = stats.GearBuffs('rogue_t11_2pc', 'leather_specialization', 'potion_of_the_tolvir', 'chaotic_metagem') - test_stats = stats.Stats(20, 4756, 190, 1022, 1329, 597, 1189, 1377, test_mh, test_oh, test_ranged, test_procs, test_gear_buffs) - test_talents = rogue_talents.RogueTalents('0333230113022110321', '0020000000000000000', '2030030000000000000') - glyph_list = ['backstab', 'mutilate', 'rupture'] - test_glyphs = rogue_glyphs.RogueGlyphs(*glyph_list) - test_race = race.Race('night_elf') - test_cycle = settings.AssassinationCycle() - test_settings = settings.Settings(test_cycle, response_time=1) - test_level = 85 - calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) - ep_values = calculator.get_ep() - self.assertTrue(ep_values['agi'] < 4.0) - self.assertTrue(ep_values['agi'] > 2.0) - self.assertTrue(ep_values['yellow_hit'] < 4.0) - self.assertTrue(ep_values['yellow_hit'] > 1.0) - self.assertTrue(ep_values['crit'] < 2.0) - self.assertTrue(ep_values['crit'] > 0.0) diff --git a/tests/calcs_tests/rogue_tests/__init__.py b/tests/calcs_tests/rogue_tests/__init__.py index ba44c8a..a532593 100644 --- a/tests/calcs_tests/rogue_tests/__init__.py +++ b/tests/calcs_tests/rogue_tests/__init__.py @@ -1,165 +1,387 @@ +from builtins import object import unittest -from shadowcraft.calcs.rogue import RogueDamageCalculator +from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator from shadowcraft.core import exceptions -from shadowcraft.objects import buffs -from shadowcraft.objects import race -from shadowcraft.objects import stats -from shadowcraft.objects import procs -from shadowcraft.objects.rogue import rogue_talents - -class TestRogueDamageCalculator(unittest.TestCase): - def setUp(self): - test_buffs = buffs.Buffs( - 'stat_multiplier_buff', - 'crit_chance_buff', - 'melee_haste_buff', - 'attack_power_buff', - 'str_and_agi_buff', - 'armor_debuff', - 'spell_damage_debuff', - 'spell_crit_debuff' - ) - test_mh = stats.Weapon(737, 1.8, 'dagger', 'hurricane') - test_oh = stats.Weapon(573, 1.4, 'dagger', 'hurricane') - test_ranged = stats.Weapon(1104, 2.0, 'thrown') - test_procs = procs.ProcsList('darkmoon_card_hurricane') - test_gear_buffs = stats.GearBuffs('chaotic_metagem') - test_stats = stats.Stats(20, 3485, 190, 1517, 1086, 641, 899, 666, test_mh, test_oh, test_ranged, test_procs, test_gear_buffs) - test_race = race.Race('night_elf') - test_talents = rogue_talents.RogueTalents('0333230113022110321', '0020000000000000000', '0030030000000000000') - self.calculator = RogueDamageCalculator(test_stats, test_talents, None, test_buffs, test_race) - - def test_get_spell_hit_from_talents(self): - self.assertAlmostEqual(self.calculator.get_spell_hit_from_talents(), .04) - self.calculator.talents.precision = 0 - self.assertAlmostEqual(self.calculator.get_spell_hit_from_talents(), .0) - - def test_get_melee_hit_from_talents(self): - self.assertAlmostEqual(self.calculator.get_melee_hit_from_talents(), .04) - self.calculator.talents.precision = 3 - self.assertAlmostEqual(self.calculator.get_melee_hit_from_talents(), .06) +from shadowcraft.objects import buffs as _buffs +from shadowcraft.objects import race as _race +from shadowcraft.objects import stats as _stats +from shadowcraft.objects import procs as _procs +from shadowcraft.objects import talents as _talents +from shadowcraft.objects import artifact as _artifact +from shadowcraft.objects import artifact_data as artifact_data +from shadowcraft.calcs.rogue.Aldriana import settings as _settings + +class RogueDamageCalculatorFactory(object): + def __init__(self, spec, **kwargs): + self.class_name = 'rogue' + self.talent_str = '0000000' + self.buffs = _buffs.Buffs('short_term_haste_buff', 'flask_wod_agi', 'food_wod_versatility') + self.procs = _procs.ProcsList() + self.gear_buffs = _stats.GearBuffs('gear_specialization') + self.traits = '000000000000000000' + self.level = 110 + self.race = 'pandaren' + self.agi = 21122 + self.stam = 28367 + self.crit = 6306 + self.haste = 3260 + self.mastery = 3706 + self.versatility = 3486 + self.response_time = 0.5 + self.duration = 360 + self.is_demon = False + self.num_boss_adds = 0 + self.adv_params = 0 + self.finisher_threshold = 5 + self.buildSpecDefaults(spec) + self.__dict__.update(kwargs) + + def buildSpecDefaults(self, spec, weapon_dps=2100): + self.spec = spec + if spec == "outlaw": + self.talent_str = '1010022' + self.mh = _stats.Weapon(weapon_dps * 2.6, 2.6, 'sword', None) + self.oh = _stats.Weapon(weapon_dps * 2.6, 2.6, 'sword', None) + self.cycle = _settings.OutlawCycle(blade_flurry=False, + jolly_roger_reroll=1, + grand_melee_reroll=1, + shark_reroll=1, + true_bearing_reroll=1, + buried_treasure_reroll=1, + broadsides_reroll=1, + between_the_eyes_policy='never') + elif spec == "assassination": + self.talent_str = '2101220' + self.mh = _stats.Weapon(weapon_dps * 1.8, 1.8, 'dagger', None) + self.oh = _stats.Weapon(weapon_dps * 1.8, 1.8, 'dagger', None) + self.cycle = _settings.AssassinationCycle() + elif spec == "subtlety": + self.talent_str = '2100120' + self.mh = _stats.Weapon(weapon_dps * 1.8, 1.8, 'dagger', None) + self.oh = _stats.Weapon(weapon_dps * 1.8, 1.8, 'dagger', None) + self.cycle = _settings.SubtletyCycle(cp_builder='backstab', dance_finishers_allowed=True, positional_uptime=0.9) + else: + raise "Invalid spec: %s" % spec + + + def build(self, **kwargs): + self.__dict__.update(kwargs) + + test_race = _race.Race(self.race) + + # Set up a calcs object.. + test_stats = _stats.Stats(self.mh, self.oh, self.procs, + self.gear_buffs, + agi=self.agi, + stam=self.stam, + crit=self.crit, + haste=self.haste, + mastery=self.mastery, + versatility=self.versatility) + + # Initialize talents.. + test_talents = _talents.Talents(self.talent_str, self.spec, self.class_name, level=self.level) + + #initialize artifact traits.. + test_traits = _artifact.Artifact(self.spec, self.class_name, self.traits) + + # Set up settings. + test_settings = _settings.Settings(self.cycle, response_time=self.response_time, duration=self.duration, + adv_params=self.adv_params, is_demon=self.is_demon, num_boss_adds=self.num_boss_adds, finisher_threshold=self.finisher_threshold) + + self.calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_traits, self.buffs, test_race, self.spec, test_settings, self.level) + self.calculator.level = 110 + return self.calculator + +class RogueDamageCalculatorTestBase(object): + def compare_dps(self, a, b): + return self.compare(a, b, "get_dps") + + def compare(self, a, b, method=None): + calc_a = self.factory.build(**a) + calc_b = self.factory.build(**b) + if method is not None: + return (getattr(calc_a, method)(), getattr(calc_b, method)()) + else: + return (calc_a, calc_b) def test_oh_penalty(self): self.assertAlmostEqual(self.calculator.oh_penalty(), 0.5) - def test_talents_modifiers_assassins_resolve(self): - self.assertAlmostEqual(self.calculator.talents_modifiers([]), 1.0) - self.assertAlmostEqual(self.calculator.talents_modifiers(['assassins_resolve']), 1.2) - self.calculator.stats.mh.type = '1h_axe' - self.assertAlmostEqual(self.calculator.talents_modifiers(['assassins_resolve']), 1.0) + def test_crit_damage_modifiers(self): + self.assertAlmostEqual(self.calculator.crit_damage_modifiers(), 1 + (2 * 1. - 1) * 1) + + def test_dps_breakdowns(self): + # TODO: Add assertions. This at least runs it though. + self.calculator.get_dps_breakdown() + + def test_get_talents_ranking(self): + # TODO: Add assertions. This at least runs it though. + self.calculator.get_talents_ranking() + + def test_get_trait_ranking(self): + # TODO: Add assertions. This at least runs it though. + self.calculator.get_trait_ranking() + + def test_ep(self): + ep_values = self.calculator.get_ep() + + self.assertTrue(ep_values['agi'] < 1.5) + self.assertTrue(ep_values['agi'] > 1.0) + self.assertTrue(ep_values['mastery'] < 1.0) + self.assertTrue(ep_values['mastery'] > 0.0) + self.assertTrue(ep_values['haste'] < 1.0) + self.assertTrue(ep_values['haste'] > 0.0) + self.assertTrue(ep_values['versatility'] < 1.0) + self.assertTrue(ep_values['versatility'] > 0.0) + self.assertTrue(ep_values['crit'] < 1.0) + self.assertTrue(ep_values['crit'] > 0.0) - def test_talents_modifiers(self): - self.assertAlmostEqual(self.calculator.talents_modifiers(['opportunity', 'assassins_resolve']), 1.2 * 1.3) + def test_set_constants_for_level(self): + self.assertRaises(exceptions.InvalidLevelException, self.calculator.__setattr__, 'level', 111) + + + def test_get_talents_ranking_does_not_change_talents(self): + active_talents = self.calculator.talents.get_active_talents() + self.assertEqual(self.calculator.talents.get_active_talents(), active_talents) + +## Single target + +class TestOutlawRogueDamageCalculator(RogueDamageCalculatorTestBase, unittest.TestCase): + def setUp(self): + self.factory = RogueDamageCalculatorFactory('outlaw') + self.calculator = self.factory.build() + + + # This is a dumb test but illustrates how we can test changes in a calculator + def test_mastery_helps_dps(self): + a, b = self.compare({"mastery": 3706}, {"mastery": 4706}, 'get_dps_breakdown') + self.assertGreater(b["main_gauche"], a["main_gauche"]) + + + def test_energy_regen(self): + self.calculator.set_constants() + # This is 12.4 base because the calculator currently averages Heroism out over the course of the fight. + # Should fix at some point. + self.assertAlmostEqual(self.calculator.get_energy_regen({'haste': 0}), 12.4) + self.assertAlmostEqual(self.calculator.get_energy_regen({'haste': 5000}), 14.3076923) + self.assertAlmostEqual(self.calculator.get_energy_regen({'haste': 5000}, buried=True), 17.8846153846) + self.assertAlmostEqual(self.calculator.get_energy_regen({'haste': 5000}, ar=True), 28.615384615384613) + self.assertAlmostEqual(self.calculator.get_energy_regen({'haste': 5000}, buried=True, ar=True), 35.76923076923077) + self.assertAlmostEqual(self.calculator.get_energy_regen({'haste': 5000}, alacrity_stacks=10), 15.50769230769231) + cycle = _settings.OutlawCycle(blade_flurry=True) + self.assertEqual(self.factory.build(cycle=cycle).set_constants().get_energy_regen({'haste': 0}), 12.4 * 0.8) + + + def test_energy_regen_blade_dancer(self): + cycle = _settings.OutlawCycle(blade_flurry=True) + self.assertEqual(self.factory.build(cycle=cycle,traits='000200000000000000').set_constants().get_energy_regen({'haste': 0}), 12.4 * (0.8 + 0.03333 * 2)) + + + def test_cursed_edges_dps(self): + base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '010000000000000000'}) + self.assertGreater(rank1, base) + rank2 = self.factory.build(traits='020000000000000000', num_boss_adds=3).get_dps() + rank3 = self.factory.build(traits='030000000000000000', num_boss_adds=3).get_dps() + self.assertGreater(rank2, rank1) + self.assertGreater(rank3, rank2) + + + def test_fates_thirst_dps(self): + base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '001000000000000000'}) + self.assertGreater(rank1, base) + rank2 = self.factory.build(traits='002000000000000000', num_boss_adds=3).get_dps() + rank3 = self.factory.build(traits='003000000000000000', num_boss_adds=3).get_dps() + self.assertGreater(rank2, rank1) + self.assertGreater(rank3, rank2) + + + def test_blade_flurry_hurts_single_target_dps(self): + cycle = _settings.OutlawCycle(blade_flurry=True) + base, rank1 = self.compare_dps({}, {'num_boss_adds': 0, 'cycle': cycle}) + self.assertLess(rank1, base) + + + def test_blade_dancer_improves_blade_flurry_penalty(self): + cycle = _settings.OutlawCycle(blade_flurry=True) + base, rank1 = self.compare_dps({'traits': '000000000000000000', 'cycle': cycle}, {'traits': '000100000000000000', 'cycle': cycle}) + self.assertGreater(rank1, base) + rank2 = self.factory.build(traits='000200000000000000').get_dps() + rank3 = self.factory.build(traits='000300000000000000').get_dps() + self.assertGreater(rank2, rank1) + self.assertGreater(rank3, rank2) + + + def test_blade_dancer_multi_target_dps(self): + cycle = _settings.OutlawCycle(blade_flurry=True) + base = self.factory.build(traits='000000000000000000', cycle=cycle).get_dps() + rank1 = self.factory.build(traits='000100000000000000', cycle=cycle, num_boss_adds=3).get_dps() + rank2 = self.factory.build(traits='000200000000000000', cycle=cycle, num_boss_adds=3).get_dps() + rank3 = self.factory.build(traits='000300000000000000', cycle=cycle, num_boss_adds=3).get_dps() + self.assertGreater(rank1, base) + self.assertGreater(rank2, rank1) + self.assertGreater(rank3, rank2) - def test_crit_damage_modifiers(self): - self.assertAlmostEqual(self.calculator.crit_damage_modifiers(), 1 + (2 * 1.03 - 1) * 1) - self.assertAlmostEqual(self.calculator.crit_damage_modifiers(is_spell=True), 1 + (1.5 * 1.03 - 1) * 1) - self.assertAlmostEqual(self.calculator.crit_damage_modifiers(lethality=True), 1 + (2 * 1.03 - 1) * 1.3) - # Just do some basic checks for the individual abilities, increasing AP - # should increase damage and similar for combo points. - # The optional armor argument isn't tested for now. - # Should probably compare damage and crit_damage individually so it can - # catch some error where crit_damage is lower even with higher AP. + def test_fatebringer_dps(self): + base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000010000000000000'}) + self.assertGreater(rank1, base) + rank2 = self.factory.build(traits='000020000000000000').get_dps() + rank3 = self.factory.build(traits='000030000000000000').get_dps() + self.assertGreater(rank2, rank1) + self.assertGreater(rank3, rank2) - def test_mh_damage(self): - self.assertTrue(self.calculator.mh_damage(0) < self.calculator.mh_damage(1)) - def test_oh_damage(self): - self.assertTrue(self.calculator.oh_damage(0) < self.calculator.oh_damage(1)) + def test_gunslinger_dps(self): + base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000001000000000000'}) + self.assertGreater(rank1, base) - def test_backstab_damage(self): - self.assertTrue(self.calculator.backstab_damage(0) < self.calculator.backstab_damage(1)) - def test_mh_mutilate_damage(self): - self.assertTrue(self.calculator.mh_mutilate_damage(0) < self.calculator.mh_mutilate_damage(1)) - not_poisoned = self.calculator.mh_mutilate_damage(1, is_poisoned=False) - poisoned = self.calculator.mh_mutilate_damage(1) - self.assertAlmostEqual(not_poisoned[0] * 1.2, poisoned[0]) - self.assertAlmostEqual(not_poisoned[1] * 1.2, poisoned[1]) + def test_hidden_blade_dps(self): + base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000100000000000'}) + self.assertGreater(rank1, base) - def test_oh_mutilate_damage(self): - self.assertTrue(self.calculator.oh_mutilate_damage(0) < self.calculator.oh_mutilate_damage(1)) - not_poisoned = self.calculator.oh_mutilate_damage(1, is_poisoned=False) - poisoned = self.calculator.oh_mutilate_damage(1) - self.assertAlmostEqual(not_poisoned[0] * 1.2, poisoned[0]) - self.assertAlmostEqual(not_poisoned[1] * 1.2, poisoned[1]) - def test_sinister_strike_damage(self): - self.assertTrue(self.calculator.sinister_strike_damage(0) < self.calculator.sinister_strike_damage(1)) + def test_fortune_strikes_dps(self): + base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000010000000000'}) + self.assertGreater(rank1, base) + rank2 = self.factory.build(traits='000000020000000000').get_dps() + rank3 = self.factory.build(traits='000000030000000000').get_dps() + self.assertGreater(rank2, rank1) + self.assertGreater(rank3, rank2) - def test_hemorrhage_damage(self): - self.assertTrue(self.calculator.hemorrhage_damage(0) < self.calculator.hemorrhage_damage(1)) - def test_hemorrhage_tick_damage(self): - self.assertTrue(self.calculator.hemorrhage_tick_damage(0) < self.calculator.hemorrhage_tick_damage(1)) - self.assertTrue(self.calculator.hemorrhage_tick_damage(0, from_crit_hemo=False) < self.calculator.hemorrhage_tick_damage(0, from_crit_hemo=True)) + def test_ghostly_shell_dps(self): + base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000001000000000'}) + self.assertEqual(base, rank1) - def test_ambush_damage(self): - self.assertTrue(self.calculator.ambush_damage(0) < self.calculator.ambush_damage(1)) - def test_revealing_strike_damage(self): - self.assertTrue(self.calculator.revealing_strike_damage(0) < self.calculator.revealing_strike_damage(1)) + def test_deception_dps(self): + base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000000100000000'}) + self.assertEqual(base, rank1) - def test_venomous_wounds_damage(self): - self.assertTrue(self.calculator.venomous_wounds_damage(0) < self.calculator.venomous_wounds_damage(1)) - def test_main_gauche_damage(self): - self.assertTrue(self.calculator.main_gauche_damage(0) < self.calculator.main_gauche_damage(1)) + def test_black_powder_dps(self): + cycle = _settings.OutlawCycle(between_the_eyes_policy='shark') + base, rank1 = self.compare_dps({'traits': '000000000000000000', 'cycle': cycle}, {'traits': '000000000010000000', 'cycle': cycle}) + self.assertGreater(rank1, base) - def test_mh_killing_spree_damage(self): - self.assertTrue(self.calculator.mh_killing_spree_damage(0) < self.calculator.mh_killing_spree_damage(1)) - def test_oh_killing_spree_damage(self): - self.assertTrue(self.calculator.oh_killing_spree_damage(0) < self.calculator.oh_killing_spree_damage(1)) + def test_greed_dps(self): + base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000000001000000'}) + self.assertGreater(rank1, base) - def test_instant_poison_damage(self): - self.assertTrue(self.calculator.instant_poison_damage(0) < self.calculator.instant_poison_damage(1)) - self.assertTrue(self.calculator.instant_poison_damage(0, mastery=0) < self.calculator.instant_poison_damage(0, mastery=1)) - def test_deadly_poison_tick_damage(self): - # test mastery - self.assertTrue(self.calculator.deadly_poison_tick_damage(0) < self.calculator.deadly_poison_tick_damage(1)) - self.assertTrue(self.calculator.deadly_poison_tick_damage(0, dp_stacks=1) < self.calculator.deadly_poison_tick_damage(0, dp_stacks=2)) + def test_blurred_time_dps(self): + base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000000000100000'}) + self.assertGreater(rank1, base) - def test_wound_poison_damage(self): - self.assertTrue(self.calculator.wound_poison_damage(0) < self.calculator.wound_poison_damage(1)) - self.assertTrue(self.calculator.wound_poison_damage(0, mastery=0) < self.calculator.wound_poison_damage(0, mastery=1)) - def test_garrote_tick_damage(self): - self.assertTrue(self.calculator.garrote_tick_damage(0) < self.calculator.garrote_tick_damage(1)) + def test_fortunes_boon_dps(self): + base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000000000010000'}) + self.assertGreater(rank1, base) + rank2 = self.factory.build(traits='000000000000020000').get_dps() + rank3 = self.factory.build(traits='000000000000030000').get_dps() + self.assertGreater(rank2, rank1) + self.assertGreater(rank3, rank2) - def test_rupture_tick_damage(self): - self.assertTrue(self.calculator.rupture_tick_damage(0, 1) < self.calculator.rupture_tick_damage(1, 1)) - self.assertTrue(self.calculator.rupture_tick_damage(0, 1) < self.calculator.rupture_tick_damage(0, 2)) - self.assertRaises(IndexError, self.calculator.rupture_tick_damage, 0, 6) - def test_eviscerate_damage(self): - self.assertTrue(self.calculator.eviscerate_damage(0, 1) < self.calculator.eviscerate_damage(1, 1)) - self.assertTrue(self.calculator.eviscerate_damage(0, 1) < self.calculator.eviscerate_damage(0, 2)) + def test_fortunes_strike_dps(self): + base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000000000001000'}) + self.assertGreater(rank1, base) - def test_envenom_damage(self): - self.assertTrue(self.calculator.envenom_damage(0, 1) < self.calculator.envenom_damage(1, 1)) - self.assertTrue(self.calculator.envenom_damage(0, 1) < self.calculator.envenom_damage(0, 2)) + rank2 = self.factory.build(traits='000000000000002000').get_dps() + rank3 = self.factory.build(traits='000000000000003000').get_dps() + self.assertGreater(rank2, rank1) + self.assertGreater(rank3, rank2) - def test_melee_crit_rate(self): - agi_per_crit = self.calculator.level == 80 and 83.15 or 324.72 - crit_rating_per_crit = self.calculator.level == 80 and 45.906 or 179.279998779296875 - self.assertAlmostEqual(self.calculator.melee_crit_rate(agi=1000), - 0.01 * (1000 / agi_per_crit - 0.295) + 0.01 * (1517 / crit_rating_per_crit) + 0.05 - 0.048) - self.assertTrue(self.calculator.spell_crit_rate(0) < self.calculator.spell_crit_rate(1)) - def test_spell_crit_rate(self): - self.assertTrue(self.calculator.melee_crit_rate(0) < self.calculator.melee_crit_rate(1)) + def test_blademaster_dps(self): + base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000000000000100'}) + self.assertEqual(base, rank1) - def test_crit_cap(self): - pass + def test_blunderbuss_dps(self): + base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000000000000010'}) + self.assertGreater(rank1, base) -class TestRogueDamageCalculatorLevels(TestRogueDamageCalculator): + + def test_cursed_steel_dps(self): + base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000000000000001'}) + self.assertGreater(rank1, base) + + + # These are sanity checks; they are what is expected from current modeling, so they're included to help + # guard against regressions in the calculator. + def test_best_rank_1(self): + base = self.factory.build(talent_str='0000000').get_dps() + gstrike = self.factory.build(talent_str='1000000').get_dps() + swords = self.factory.build(talent_str='2000000').get_dps() + quick = self.factory.build(talent_str='3000000').get_dps() + self.assertGreater(gstrike, base, 'No Talents %s >= Ghostly Strike %s' % (base, gstrike)) + self.assertGreater(gstrike, swords, 'Swordmaster %s >= Ghostly Strike %s' % (swords, gstrike)) + self.assertGreater(gstrike, quick, 'Quick Draw %s >= Ghostly Strike %s' % (quick, gstrike)) + + + def test_best_rank_3(self): + base = self.factory.build(talent_str='0010000').get_dps() + stratagem = self.factory.build(talent_str='0010000').get_dps() + anticipation = self.factory.build(talent_str='0020000').get_dps() + vigor = self.factory.build(talent_str='0030000').get_dps() + self.assertGreater(stratagem, base, 'No Talents %s >= Stratagem %s' % (base, stratagem)) + self.assertGreater(stratagem, anticipation, 'Anticipation %s >= Stratagem %s' % (anticipation, stratagem)) + self.assertGreater(stratagem, vigor, 'Vigor %s >= Stratagem %s' % (vigor, stratagem)) + + + def test_best_rank_6(self): + base = self.factory.build(talent_str='0000000').get_dps() + cannons = self.factory.build(talent_str='0000010').get_dps() + alacrity = self.factory.build(talent_str='0000020').get_dps() + kspree = self.factory.build(talent_str='0000030').get_dps() + self.assertGreater(alacrity, base, 'No Talents %s >= Alacrity %s' % (base, alacrity)) + self.assertGreater(alacrity, kspree, 'KSpree %s >= Alacrity %s' % (kspree, alacrity)) + self.assertGreater(alacrity, cannons, 'Cannons %s >= Alacrity %s' % (cannons, alacrity)) + + + def test_best_rank_7(self): + base = self.factory.build(talent_str='0000000').get_dps() + snd = self.factory.build(talent_str='0000001').get_dps() + mfd = self.factory.build(talent_str='0000002').get_dps() + dfa = self.factory.build(talent_str='0000003').get_dps() + self.assertGreater(mfd, base, 'No Talents %s >= Marked for Death %s' % (base, mfd)) + self.assertGreater(mfd, snd, 'SnD %s >= Marked for Death %s' % (snd, mfd)) + self.assertGreater(mfd, dfa, 'Death From Above %s >= mfd %s' % (dfa, mfd)) + + def test_get_talents_ranking_does_not_persist_talents_in_same_row(self): + self.calculator.talents.initialize_talents("0000000") + ranking_without_existing = self.calculator.get_talents_ranking() + + self.calculator.talents.initialize_talents("1000000") + ranking_with_existing = self.calculator.get_talents_ranking() + + self.assertEqual(ranking_without_existing[15], ranking_with_existing[15]) + + +class TestAssassinationRogueDamageCalculator(RogueDamageCalculatorTestBase, unittest.TestCase): def setUp(self): - super(TestRogueDamageCalculatorLevels, self).setUp() - self.calculator.level = 80 + self.calculator = RogueDamageCalculatorFactory('assassination').build() - def test_set_constants_for_level(self): - self.assertRaises(exceptions.InvalidLevelException, self.calculator.__setattr__, 'level', 86) +class TestSubtletyRogueDamageCalculator(RogueDamageCalculatorTestBase, unittest.TestCase): + def setUp(self): + self.calculator = RogueDamageCalculatorFactory('subtlety').build() + +## Multi-target + +class TestAOEOutlawRogueDamageCalculator(RogueDamageCalculatorTestBase, unittest.TestCase): + def setUp(self): + self.calculator = RogueDamageCalculatorFactory('outlaw').build(num_boss_adds=3) + + +class TestAOEAssassinationRogueDamageCalculator(RogueDamageCalculatorTestBase, unittest.TestCase): + def setUp(self): + self.calculator = RogueDamageCalculatorFactory('assassination').build(num_boss_adds=3) + + +class TestAOESubtletyRogueDamageCalculator(RogueDamageCalculatorTestBase, unittest.TestCase): + def setUp(self): + self.calculator = RogueDamageCalculatorFactory('subtlety').build(num_boss_adds=3) \ No newline at end of file diff --git a/tests/core_tests/exceptions_tests.py b/tests/core_tests/exceptions_tests.py index 0423025..640afc1 100644 --- a/tests/core_tests/exceptions_tests.py +++ b/tests/core_tests/exceptions_tests.py @@ -1,3 +1,4 @@ +from builtins import str import unittest from shadowcraft.core.exceptions import InvalidInputException diff --git a/tests/objects_tests/buffs_tests.py b/tests/objects_tests/buffs_tests.py index c302af1..f69080e 100644 --- a/tests/objects_tests/buffs_tests.py +++ b/tests/objects_tests/buffs_tests.py @@ -1,109 +1,80 @@ import unittest from shadowcraft.core import exceptions from shadowcraft.objects import buffs - + class TestBuffsTrue(unittest.TestCase): def setUp(self): self.buffs = buffs.Buffs(*buffs.Buffs.allowed_buffs) - + def test_exception(self): self.assertRaises(buffs.InvalidBuffException, buffs.Buffs, 'fake_buff') - + def test__getattr__(self): self.assertRaises(AttributeError, self.buffs.__getattr__, 'fake_buff') - self.assertTrue(self.buffs.crit_chance_buff) - - def test_stat_multiplier(self): - self.assertEqual(self.buffs.stat_multiplier(), 1.05) - - def test_all_damage_multiplier(self): - self.assertEqual(self.buffs.all_damage_multiplier(), 1.03) - - def test_spell_damage_multiplier(self): - self.assertEqual(self.buffs.spell_damage_multiplier(), 1.08 * 1.03) - - def test_physical_damage_multiplier(self): - self.assertEqual(self.buffs.physical_damage_multiplier(), 1.04 * 1.03) - - def test_bleed_damage_multiploer(self): - self.assertEqual(self.buffs.bleed_damage_multiplier(), 1.30 * 1.03 * 1.04) - - def test_attack_power_multiplier(self): - self.assertEqual(self.buffs.attack_power_multiplier(), 1.1) - - def test_melee_haste_multiplier(self): - self.assertEqual(self.buffs.melee_haste_multiplier(), 1.1) - - def test_buff_str(self): - self.assertEqual(self.buffs.buff_str(), 549) + self.assertTrue(self.buffs.flask_wod_agi_200) def test_buff_agi(self): - self.assertEqual(self.buffs.buff_agi(), 549 + 90 + 300) - - def test_buff_all_crit(self): - self.assertEqual(self.buffs.buff_all_crit(), 0.05) - - def test_buff_spell_crit(self): - self.assertEqual(self.buffs.buff_spell_crit(), 0.05) - - def test_armor_reduction_multiplier(self): - self.assertEqual(self.buffs.armor_reduction_multiplier(), 0.88) + self.assertEqual(self.buffs.buff_agi(), 2100) class TestBuffsFalse(unittest.TestCase): def setUp(self): self.buffs = buffs.Buffs() - + def test__getattr__(self): self.assertRaises(AttributeError, self.buffs.__getattr__, 'fake_buff') - self.assertFalse(self.buffs.bleed_damage_debuff) - - def test_stat_multiplier(self): - self.assertEqual(self.buffs.stat_multiplier(), 1.0) - - def test_all_damage_multiplier(self): - self.assertEqual(self.buffs.all_damage_multiplier(), 1.0) - - def test_spell_damage_multiplier(self): - self.assertEqual(self.buffs.spell_damage_multiplier(), 1.0) - - def test_physical_damage_multiplier(self): - self.assertEqual(self.buffs.physical_damage_multiplier(), 1.0) - - def test_bleed_damage_multiploer(self): - self.assertEqual(self.buffs.bleed_damage_multiplier(), 1.0) - - def test_attack_power_multiplier(self): - self.assertEqual(self.buffs.attack_power_multiplier(), 1.0) - - def test_melee_haste_multiplier(self): - self.assertEqual(self.buffs.melee_haste_multiplier(), 1.0) - - def test_buff_str(self): - self.assertEqual(self.buffs.buff_str(), 0) + self.assertFalse(self.buffs.flask_legion_agi) - def test_buff_agi(self): - self.assertEqual(self.buffs.buff_agi(), 0) - - def test_buff_all_crit(self): - self.assertEqual(self.buffs.buff_all_crit(), 0.0) - - def test_buff_spell_crit(self): - self.assertEqual(self.buffs.buff_spell_crit(), 0.0) - - def test_armor_reduction_multiplier(self): - self.assertEqual(self.buffs.armor_reduction_multiplier(), 1.0) - -class TestBuffsLevel(unittest.TestCase): - def setUp(self): - self.buffs = buffs.Buffs('str_and_agi_buff') - - def test(self): - self.assertEqual(self.buffs.buff_agi(), 549) - self.assertEqual(self.buffs.buff_str(), 549) - self.buffs.level = 80 - self.assertEqual(self.buffs.buff_agi(), 155) - self.assertEqual(self.buffs.buff_str(), 155) + def test_flask_legion_agi(self): + self.assertEqual(self.buffs.flask_legion_agi, False) - def test_exception(self): - self.assertRaises(exceptions.InvalidLevelException, self.buffs.__setattr__, 'level', 86) + def test_food_legion_mastery_225(self): + self.assertEqual(self.buffs.food_legion_mastery_225, False) + + def test_food_legion_crit_225(self): + self.assertEqual(self.buffs.food_legion_crit_225, False) + + def test_food_legion_haste_225(self): + self.assertEqual(self.buffs.food_legion_haste_225, False) + + def test_food_legion_versatility_225(self): + self.assertEqual(self.buffs.food_legion_versatility_225, False) + + def test_food_legion_mastery_300(self): + self.assertEqual(self.buffs.food_legion_mastery_300, False) + + def test_food_legion_crit_300(self): + self.assertEqual(self.buffs.food_legion_crit_300, False) + + def test_food_legion_haste_300(self): + self.assertEqual(self.buffs.food_legion_haste_300, False) + + def test_food_legion_versatility_300(self): + self.assertEqual(self.buffs.food_legion_versatility_300, False) + + def test_food_legion_mastery_375(self): + self.assertEqual(self.buffs.food_legion_mastery_375, False) + + def test_food_legion_crit_375(self): + self.assertEqual(self.buffs.food_legion_crit_375, False) + + def test_food_legion_haste_375(self): + self.assertEqual(self.buffs.food_legion_haste_375, False) + + def test_food_legion_versatility_375(self): + self.assertEqual(self.buffs.food_legion_versatility_375, False) + + def test_food_legion_damage_1(self): + self.assertEqual(self.buffs.food_legion_damage_1, False) + + def test_food_legion_damage_2(self): + self.assertEqual(self.buffs.food_legion_damage_2, False) + + def test_food_legion_damage_3(self): + self.assertEqual(self.buffs.food_legion_damage_3, False) + + def test_food_legion_feast_150(self): + self.assertEqual(self.buffs.food_legion_feast_150, False) + + def test_food_legion_feast_200(self): + self.assertEqual(self.buffs.food_legion_feast_200, False) \ No newline at end of file diff --git a/tests/objects_tests/procs_tests.py b/tests/objects_tests/procs_tests.py index 4375ea8..0423ddb 100644 --- a/tests/objects_tests/procs_tests.py +++ b/tests/objects_tests/procs_tests.py @@ -1,76 +1,72 @@ import unittest from shadowcraft.objects import procs - + class TestProcsList(unittest.TestCase): def setUp(self): - self.procsList = procs.ProcsList('darkmoon_card_hurricane','heroic_left_eye_of_rajh') - + self.procsList = procs.ProcsList('fury_of_xuen','legendary_capacitive_meta') + def test__init__(self): self.assertRaises(procs.InvalidProcException, procs.ProcsList, 'fake_proc') - self.procsList = procs.ProcsList('darkmoon_card_hurricane') + self.procsList = procs.ProcsList('fury_of_xuen') self.assertEqual(len(self.procsList.get_all_procs_for_stat(stat=None)), 1) - + def test__getattr__(self): self.assertRaises(AttributeError, self.procsList.__getattr__, 'fake_proc') - self.assertTrue(self.procsList.darkmoon_card_hurricane) - self.assertFalse(self.procsList.fluid_death) - + self.assertTrue(self.procsList.fury_of_xuen) + self.assertFalse(self.procsList.touch_of_the_grave) + def test_get_all_procs_for_stat(self): self.assertEqual(len(self.procsList.get_all_procs_for_stat(stat=None)), 2) self.procsList = procs.ProcsList() self.assertEqual(len(self.procsList.get_all_procs_for_stat(stat=None)), 0) - + def test_get_all_damage_procs(self): - self.assertEqual(len(self.procsList.get_all_damage_procs()), 1) + self.assertEqual(len(self.procsList.get_all_damage_procs()), 2) self.procsList = procs.ProcsList() self.assertEqual(len(self.procsList.get_all_damage_procs()), 0) class TestProc(unittest.TestCase): def setUp(self): - self.proc = procs.Proc(**procs.ProcsList.allowed_procs['prestors_talisman_of_machination']) - + self.proc = procs.Proc(**procs.ProcsList.allowed_procs['bloodthirsty_instinct']) + def test__init__(self): - self.assertEqual(self.proc.stat, 'haste') - self.assertEqual(self.proc.value, 1926) - self.assertEqual(self.proc.duration, 15) - self.assertEqual(self.proc.proc_chance, .1) + self.assertEqual(self.proc.stat, 'stats') + self.assertEqual(self.proc.value['haste'], 2880) + self.assertEqual(self.proc.duration, 10) + self.assertEqual(self.proc.proc_rate, 3) self.assertEqual(self.proc.trigger, 'all_attacks') - self.assertEqual(self.proc.icd, 75) + self.assertEqual(self.proc.icd, 0) self.assertEqual(self.proc.max_stacks, 1) self.assertEqual(self.proc.on_crit, False) - self.assertEqual(self.proc.proc_name, 'Nefarious Plot') - self.assertEqual(self.proc.ppm, False) - + self.assertEqual(self.proc.proc_name, 'Bloodthirsty Instinct') + def test_procs_off_auto_attacks(self): self.assertTrue(self.proc.procs_off_auto_attacks()) - + def test_procs_off_strikes(self): self.assertTrue(self.proc.procs_off_strikes()) - + def test_procs_off_harmful_spells(self): self.assertFalse(self.proc.procs_off_harmful_spells()) - + def test_procs_off_heals(self): self.assertFalse(self.proc.procs_off_heals()) - + def test_procs_off_periodic_spell_damage(self): self.assertFalse(self.proc.procs_off_periodic_spell_damage()) - + def test_procs_off_periodic_heals(self): self.assertFalse(self.proc.procs_off_periodic_heals()) - + def test_procs_off_apply_debuff(self): self.assertTrue(self.proc.procs_off_apply_debuff()) - + def test_procs_off_bleeds(self): self.assertFalse(self.proc.procs_off_bleeds()) - + def test_procs_off_crit_only(self): self.assertFalse(self.proc.procs_off_crit_only()) def test_is_ppm(self): self.assertFalse(self.proc.is_ppm()) - - def test_proc_rate(self): - self.assertEqual(self.proc.proc_rate(), self.proc.proc_chance) diff --git a/tests/objects_tests/race_tests.py b/tests/objects_tests/race_tests.py index 18fefc3..529fc28 100644 --- a/tests/objects_tests/race_tests.py +++ b/tests/objects_tests/race_tests.py @@ -1,54 +1,42 @@ import unittest from shadowcraft.objects import race - + class TestRace(unittest.TestCase): def setUp(self): self.race = race.Race('human') - + def test__init__(self): self.assertEqual(self.race.race_name, 'human') self.assertEqual(self.race.character_class, 'rogue') def test_set_racials(self): - self.assertTrue(self.race.sword_1h_specialization) + self.assertTrue(self.race.human_spirit) self.assertFalse(self.race.blood_fury_physical) def test_exceptions(self): - self.assertRaises(race.InvalidRaceException, self.race.__setattr__, 'level', 81) + self.assertRaises(race.InvalidRaceException, self.race.__setattr__, 'level', 111) self.assertRaises(race.InvalidRaceException, self.race.__init__, 'murloc') self.assertRaises(race.InvalidRaceException, self.race.__init__, 'undead', 'demon_hunter') - + def test__getattr__(self): - racial_stats = (122, 206, 114, 46, 73) + racial_stats = (288, 306, 212, 169, 127) for i, stat in enumerate(['racial_str', 'racial_agi', 'racial_sta', 'racial_int', 'racial_spi']): self.assertEqual(getattr(self.race, stat), racial_stats[i]) - racial_stats = (122 - 4, 206 + 4, 114, 46, 73) + racial_stats = (288 - 4, 306 + 4, 212, 169, 127) night_elf = race.Race('night_elf') for i, stat in enumerate(['racial_str', 'racial_agi', 'racial_sta', 'racial_int', 'racial_spi']): self.assertEqual(getattr(night_elf, stat), racial_stats[i]) - def test_get_racial_expertise(self): - self.assertTrue(abs(self.race.get_racial_expertise('1h_sword') - 0.0075) < 0.00000001) - self.assertEqual(self.race.get_racial_expertise('1h_axe'), 0) - def test_get_racial_crit(self): for weapon in ('thrown', 'gun', 'bow'): self.assertEqual(self.race.get_racial_crit(weapon), 0) - troll = race.Race('troll') - self.assertEqual(troll.get_racial_crit('thrown'), 0.01) - self.assertEqual(troll.get_racial_crit('bow'), 0.01) - self.assertEqual(troll.get_racial_crit('gun'), 0) + troll = race.Race('troll') self.assertEqual(troll.get_racial_crit(), 0) worgen = race.Race('worgen') self.assertEqual(worgen.get_racial_crit(), 0.01) self.assertEqual(worgen.get_racial_crit('gun'), 0.01) self.assertEqual(worgen.get_racial_crit('axe'), 0.01) - def test_get_racial_hit(self): - self.assertEqual(self.race.get_racial_hit(), 0) - draenei = race.Race('draenei') - self.assertEqual(draenei.get_racial_hit(), 0.01) - def test_get_racial_haste(self): self.assertEqual(self.race.get_racial_haste(), 0) goblin = race.Race('goblin') @@ -57,14 +45,14 @@ def test_get_racial_haste(self): def test_get_racial_stat_boosts(self): self.assertEqual(len(self.race.get_racial_stat_boosts()), 0) orc = race.Race('orc') - orc.level = 85; + orc.level = 110; abilities = orc.get_racial_stat_boosts() self.assertEqual(len(abilities), 2) self.assertEqual(abilities[0]['duration'], 15) self.assertTrue(abilities[1]['stat'] in ('ap', 'sp')) self.assertNotEqual(abilities[0]['stat'],abilities[1]['stat']) if (abilities[0]['stat'] == 'ap'): - self.assertEqual(abilities[0]['value'], 1170) + self.assertEqual(abilities[0]['value'], 2243) else: self.assertEqual(abilities[0]['value'], 585) diff --git a/tests/objects_tests/rogue_tests/rogue_glyphs_tests.py b/tests/objects_tests/rogue_tests/rogue_glyphs_tests.py deleted file mode 100644 index 7bb7235..0000000 --- a/tests/objects_tests/rogue_tests/rogue_glyphs_tests.py +++ /dev/null @@ -1,11 +0,0 @@ -import unittest -from shadowcraft.objects.rogue import rogue_glyphs - -class TestRogueGlyphs(unittest.TestCase): - def setUp(self): - self.glyphs = rogue_glyphs.RogueGlyphs('backstab', 'mutilate', 'rupture') - - def test__getattr__(self): - self.assertRaises(AttributeError, self.glyphs.__getattr__, 'fake_glyph') - self.assertTrue(self.glyphs.backstab) - self.assertFalse(self.glyphs.slice_and_dice) diff --git a/tests/objects_tests/rogue_tests/rogue_talents_tests.py b/tests/objects_tests/rogue_tests/rogue_talents_tests.py index b06be64..56c6db5 100644 --- a/tests/objects_tests/rogue_tests/rogue_talents_tests.py +++ b/tests/objects_tests/rogue_tests/rogue_talents_tests.py @@ -1,30 +1,21 @@ import unittest -from shadowcraft.objects import talents -from shadowcraft.objects.rogue import rogue_talents +from shadowcraft.objects.talents import Talents +from shadowcraft.objects.talents import InvalidTalentException class TestAssassinationTalents(unittest.TestCase): # Tests for the abstract class objects.talents.TalentTree def setUp(self): - self.talents = rogue_talents.Assassination('0333230113022110321') + self.talents = Talents('1231231', 'assassination', 'rogue') def test__getattr__(self): self.assertRaises(AttributeError, self.talents.__getattr__, 'fake_talent') - talents = rogue_talents.Assassination() - self.assertEqual(talents.vendetta, 0) - - def test__init__kwargs(self): - talents = rogue_talents.Assassination(vendetta=1) - self.assertEqual(talents.vendetta, 1) - self.assertEqual(talents.cold_blood, 0) - self.assertRaises(AttributeError, talents.__getattr__, 'fake_talent') - + self.assertEqual(self.talents.master_poisoner, True) + self.assertEqual(self.talents.nightstalker, False) + def test_set_talent(self): - self.assertRaises(talents.InvalidTalentException, self.talents.set_talent, 'fake_talent', 2) - self.assertRaises(talents.InvalidTalentException, self.talents.set_talent, 'vendetta', -1) - self.assertRaises(talents.InvalidTalentException, self.talents.set_talent, 'vendetta', -2) - - def test_exceptions(self): - self.assertRaises(talents.InvalidTalentException, rogue_talents.Assassination, '10333230113022110321') + self.assertRaises(InvalidTalentException, self.talents.set_talent, 'fake_talent') + self.assertRaises(InvalidTalentException, self.talents.set_talent, 'vendetta') + self.assertRaises(InvalidTalentException, self.talents.set_talent, 'vendetta') class TestCombatTalents(unittest.TestCase): pass @@ -36,20 +27,11 @@ class TestSubtletyTalents(unittest.TestCase): class TestRogueTalents(unittest.TestCase): def setUp(self): - self.talents = rogue_talents.RogueTalents('0333230113022110321', '0020000000000000000', '2030030000000000000') + self.talents = Talents('1231231', 'assassination', 'rogue') def test(self): - self.assertEqual(self.talents.vendetta, 1) - self.assertEqual(self.talents.cold_blood, 1) - self.assertEqual(self.talents.relentless_strikes, 3) - self.assertEqual(self.talents.precision, 2) - self.assertEqual(self.talents.killing_spree, 0) - - def test_is_assassination_rogue(self): - self.assertTrue(self.talents.is_assassination_rogue()) - - def test_is_combat_rogue(self): - self.assertFalse(self.talents.is_combat_rogue()) - - def test_is_subtlety_rogue(self): - self.assertFalse(self.talents.is_subtlety_rogue()) + self.assertEqual(self.talents.master_poisoner, 1) + self.assertEqual(self.talents.subterfuge, 1) + self.assertEqual(self.talents.vigor, 1) + self.assertEqual(self.talents.cheat_death, 0) + self.assertEqual(self.talents.thuggee, 0) \ No newline at end of file diff --git a/tests/objects_tests/stats_tests.py b/tests/objects_tests/stats_tests.py index 0e30e3e..2d7713d 100644 --- a/tests/objects_tests/stats_tests.py +++ b/tests/objects_tests/stats_tests.py @@ -1,3 +1,4 @@ +from __future__ import division import unittest from shadowcraft.core import exceptions from shadowcraft.objects import stats @@ -5,129 +6,43 @@ class TestStats(unittest.TestCase): def setUp(self): - self.stats = stats.Stats(20, 3485, 190, 1517, 1086, 641, 899, 666, None, None, None, None, None) + mainhand = stats.Weapon(1234, 2.6, "sword") + offhand = stats.Weapon(777, 2.6, "sword") + self.stats = stats.Stats(mainhand, offhand, procs.ProcsList(), None, str=20, agi=3485, int=190, stam=1086, crit=899, haste=666, mastery=1234, versatility=1222, level=110) def test_stats(self): self.assertEqual(self.stats.agi, 3485) def test_set_constants_for_level(self): - self.assertRaises(exceptions.InvalidLevelException, self.stats.__setattr__, 'level', 86) + self.assertRaises(exceptions.InvalidLevelException, self.stats.__setattr__, 'level', 111) def test_get_mastery_from_rating(self): - self.assertAlmostEqual(self.stats.get_mastery_from_rating(), 8 + 666 / 179.279998779296875) - self.assertAlmostEqual(self.stats.get_mastery_from_rating(100), 8 + 100 / 179.279998779296875) - self.stats.level = 80 - self.assertAlmostEqual(self.stats.get_mastery_from_rating(), 8 + 666 / 45.906) - self.assertAlmostEqual(self.stats.get_mastery_from_rating(100), 8 + 100 / 45.906) + self.assertAlmostEqual(self.stats.get_mastery_from_rating(), 8 + 1234 / 400) + self.assertAlmostEqual(self.stats.get_mastery_from_rating(100), 8 + 100 / 400) - def test_get_melee_hit_from_rating(self): - self.assertAlmostEqual(self.stats.get_melee_hit_from_rating(), .01 * 1086 / 120.109001159667969) - self.assertAlmostEqual(self.stats.get_melee_hit_from_rating(100), .01 * 100 / 120.109001159667969) - - def test_get_expertise_from_rating(self): - self.assertAlmostEqual(self.stats.get_expertise_from_rating(), .01 * 641 / (30.027200698852539 * 4)) - self.assertAlmostEqual(self.stats.get_expertise_from_rating(100), .01 * 100 / (30.027200698852539 * 4)) - - def test_get_spell_hit_from_rating(self): - self.assertAlmostEqual(self.stats.get_spell_hit_from_rating(), .01 * 1086 / 102.445999145507812) - self.assertAlmostEqual(self.stats.get_spell_hit_from_rating(100, 0), .01 * 100 / 102.445999145507812) + def test_get_versatility_multiplier_from_rating(self): + self.assertAlmostEqual(self.stats.get_versatility_multiplier_from_rating(), 1 + 1222 / 47500) + self.assertAlmostEqual(self.stats.get_versatility_multiplier_from_rating(100), 1 + 100 / 47500) def test_get_crit_from_rating(self): - self.assertAlmostEqual(self.stats.get_crit_from_rating(), .01 * 1517 / 179.279998779296875) - self.assertAlmostEqual(self.stats.get_crit_from_rating(100), .01 * 100 / 179.279998779296875) + self.assertAlmostEqual(self.stats.get_crit_from_rating(), 899 / 40000) + self.assertAlmostEqual(self.stats.get_crit_from_rating(100), 100 / 40000) def test_get_haste_multiplier_from_rating(self): - self.assertAlmostEqual(self.stats.get_haste_multiplier_from_rating(), 1 + .01 * 899 / 128.057006835937500) - self.assertAlmostEqual(self.stats.get_haste_multiplier_from_rating(100), 1 + .01 * 100 / 128.057006835937500) - - -class TestWeapon(unittest.TestCase): - def setUp(self): - self.mh = stats.Weapon(1000, 2.0, 'dagger', 'hurricane') - self.ranged = stats.Weapon(1104, 2.0, 'thrown') - - def test___init__(self): - self.assertAlmostEqual(self.mh._normalization_speed, 1.7) - self.assertAlmostEqual(self.mh.speed, 2.0) - self.assertAlmostEqual(self.mh.weapon_dps, 1000 / 2.0) - self.assertEqual(self.mh.type, 'dagger') - self.assertRaises(exceptions.InvalidInputException, stats.Weapon, 1000, 2.0, 'thrown', 'fake_enchant') - self.assertAlmostEqual(self.ranged._normalization_speed, 2.1) - mh = stats.Weapon(1000, 1.8, 'dagger', 'hurricane') - self.assertAlmostEqual(mh.hurricane.proc_rate(speed=1.8), 1 * 1.8 / 60) - oh = stats.Weapon(1000, 1.4, 'dagger', 'hurricane') - self.assertAlmostEqual(mh.hurricane.proc_rate(speed=1.8), 1 * 1.8 / 60) - self.assertAlmostEqual(oh.hurricane.proc_rate(speed=1.4), 1 * 1.4 / 60) - - def test__getattr__(self): - self.assertTrue(self.mh.hurricane) - self.assertFalse(self.mh.landslide) - self.assertRaises(AttributeError, self.mh.__getattr__, 'fake_enchant') - - def test_is_melee(self): - self.assertTrue(self.mh.is_melee()) - self.assertFalse(self.ranged.is_melee()) - - def test_damage(self): - self.assertAlmostEqual(self.mh.damage(1000), 2.0 * (1000 / 2.0 + 1000 / 14.0)) - - def test_normalized_damage(self): - self.assertAlmostEqual(self.mh.normalized_damage(1000), 1000 + (1.7 * 1000 / 14.0)) - - def test_weapon_enchant_proc_rate_exception(self): - self.assertRaises(procs.InvalidProcException, self.mh.hurricane.proc_rate) + self.assertAlmostEqual(self.stats.get_haste_multiplier_from_rating(), 1 + 666 / 37500) + self.assertAlmostEqual(self.stats.get_haste_multiplier_from_rating(100), 1 + 100 / 37500) class TestGearBuffs(unittest.TestCase): def setUp(self): - self.gear = stats.GearBuffs('chaotic_metagem', 'leather_specialization', 'rogue_t11_2pc', 'potion_of_the_tolvir', 'engineer_glove_enchant', 'lifeblood') + self.gear = stats.GearBuffs('gear_specialization') self.gear_none = stats.GearBuffs() def test__getattr__(self): - self.assertTrue(self.gear.chaotic_metagem) - self.assertTrue(self.gear.leather_specialization) - self.assertFalse(self.gear.unsolvable_riddle) + self.assertTrue(self.gear.gear_specialization) + self.assertFalse(self.gear.rogue_t19_2pc) self.assertRaises(AttributeError, self.gear.__getattr__, 'fake_gear_buff') - def test_metagem_crit_multiplier(self): - self.assertAlmostEqual(self.gear.metagem_crit_multiplier(), 1.03) - self.assertAlmostEqual(self.gear_none.metagem_crit_multiplier(), 1.0) - - def test_rogue_t11_2pc_crit_bonus(self): - self.assertAlmostEqual(self.gear.rogue_t11_2pc_crit_bonus(), 0.05) - self.assertAlmostEqual(self.gear_none.rogue_t11_2pc_crit_bonus(), 0.0) - - def test_leather_specialization_multiplier(self): - self.assertAlmostEqual(self.gear.leather_specialization_multiplier(), 1.05) - self.assertAlmostEqual(self.gear_none.leather_specialization_multiplier(), 1.0) - - def test_get_all_activated_agi_boosts(self): - self.assertEqual(len(self.gear.get_all_activated_agi_boosts()), 1) - self.assertEqual(len(self.gear_none.get_all_activated_agi_boosts()), 0) - - def test_get_all_activated_boosts_for_stat(self): - self.assertEqual(len(self.gear.get_all_activated_boosts_for_stat('agi')), 1) - self.assertEqual(len(self.gear.get_all_activated_boosts_for_stat('haste')), 2) - self.assertEqual(len(self.gear.get_all_activated_boosts_for_stat('crit')), 0) - - def test_get_all_activated_haste_rating_boosts(self): - self.assertEqual(len(self.gear.get_all_activated_haste_rating_boosts()), 2) - self.assertEqual(len(self.gear_none.get_all_activated_haste_rating_boosts()), 0) - - def test_engineer_glove_enchant(self): - test_gear = stats.GearBuffs('engineer_glove_enchant') - haste_boost = test_gear.get_all_activated_haste_rating_boosts()[0] - self.assertEqual(haste_boost['value'], 340) - self.assertEqual(haste_boost['duration'], 12) - self.assertEqual(haste_boost['cooldown'], 60) - - def test_lifeblood(self): - test_gear = stats.GearBuffs('lifeblood') - haste_boost = test_gear.get_all_activated_haste_rating_boosts()[0] - self.assertEqual(haste_boost['value'], 480) - self.assertEqual(haste_boost['duration'], 20) - self.assertEqual(haste_boost['cooldown'], 120) - - def test_get_all_activated_boosts(self): - self.assertEqual(len(self.gear.get_all_activated_boosts()), 3) - self.assertEqual(len(self.gear_none.get_all_activated_boosts()), 0) + def test_gear_specialization_multiplier(self): + self.assertAlmostEqual(self.gear.gear_specialization_multiplier(), 1.05) + self.assertAlmostEqual(self.gear_none.gear_specialization_multiplier(), 1.0) diff --git a/tests/runtests.py b/tests/runtests.py index d6db385..349dc6b 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import unittest from os import path import sys @@ -5,16 +6,17 @@ sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) from calcs_tests import TestDamageCalculator -from calcs_tests.armor_mitigation_tests import TestArmorMitigation -from calcs_tests.rogue_tests import TestRogueDamageCalculator -from calcs_tests.rogue_tests import TestRogueDamageCalculatorLevels -from calcs_tests.rogue_tests.Aldriana_tests import TestAldrianasRogueDamageCalculator +from calcs_tests.rogue_tests import TestOutlawRogueDamageCalculator +from calcs_tests.rogue_tests import TestAssassinationRogueDamageCalculator +from calcs_tests.rogue_tests import TestSubtletyRogueDamageCalculator +from calcs_tests.rogue_tests import TestAOEOutlawRogueDamageCalculator +from calcs_tests.rogue_tests import TestAOEAssassinationRogueDamageCalculator +from calcs_tests.rogue_tests import TestAOESubtletyRogueDamageCalculator from core_tests.exceptions_tests import TestInvalidInputException -from objects_tests.buffs_tests import TestBuffsTrue, TestBuffsFalse, TestBuffsLevel -from objects_tests.stats_tests import TestStats, TestWeapon, TestGearBuffs +from objects_tests.buffs_tests import TestBuffsTrue, TestBuffsFalse +from objects_tests.stats_tests import TestStats, TestGearBuffs from objects_tests.procs_tests import TestProcsList, TestProc from objects_tests.race_tests import TestRace -from objects_tests.rogue_tests.rogue_glyphs_tests import TestRogueGlyphs from objects_tests.rogue_tests.rogue_talents_tests import TestAssassinationTalents from objects_tests.rogue_tests.rogue_talents_tests import TestCombatTalents from objects_tests.rogue_tests.rogue_talents_tests import TestSubtletyTalents