├── .editorconfig ├── .gitignore ├── README.md ├── extra └── swing delay graphs.gcx ├── license.txt ├── scripts ├── assassination.py ├── outlaw.py └── subtlety.py ├── setup.py ├── shadowcraft ├── __init__.py ├── calcs │ ├── __init__.py │ └── rogue │ │ ├── Aldriana │ │ ├── __init__.py │ │ ├── settings.py │ │ └── settings_data.py │ │ └── __init__.py ├── core │ ├── __init__.py │ ├── exceptions.py │ ├── files.in │ ├── i18n.py │ ├── jsoninput.py │ └── locale │ │ ├── SCE.pot │ │ ├── en │ │ ├── LC_MESSAGES │ │ │ └── SCE.mo │ │ └── en.po │ │ └── es_ES │ │ ├── LC_MESSAGES │ │ └── SCE.mo │ │ └── es_ES.po └── objects │ ├── __init__.py │ ├── artifact.py │ ├── artifact_data.py │ ├── buffs.py │ ├── class_data.py │ ├── modifiers.py │ ├── priority_list.py │ ├── proc_data.py │ ├── procs.py │ ├── race.py │ ├── stats.py │ ├── talents.py │ └── talents_data.py ├── style.md └── tests ├── __init__.py ├── calcs_tests ├── __init__.py └── rogue_tests │ └── __init__.py ├── core_tests ├── __init__.py └── exceptions_tests.py ├── objects_tests ├── __init__.py ├── buffs_tests.py ├── procs_tests.py ├── race_tests.py ├── rogue_tests │ ├── __init__.py │ └── rogue_talents_tests.py └── stats_tests.py └── runtests.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | charset = utf-8 3 | indent_style = space 4 | indent_size = 4 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | MANIFEST 9 | item_db 10 | item_db.db 11 | .DS_Store 12 | 13 | /.project 14 | /.pydevproject 15 | *.sublime-workspace 16 | *.sublime-project 17 | /ShadowCraft-Engine.pyproj 18 | /ShadowCraft-Engine.pyproj.user 19 | /ShadowCraft-Engine.sln 20 | /.vs/ShadowCraft-Engine/v14/.suo 21 | /.vscode 22 | /TestResults 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ShadowCraft-Engine 2 | ================== 3 | 4 | This repository contains the calculations engine behind the ShadowCraft 5 | theorycrafting webapp for the Rogue class in World of Warcraft. For the 6 | web application including the UI see 7 | [shadowcraft-ui](https://github.com/cheald/shadowcraft-ui). 8 | 9 | ShadowCraft-Engine is written in Python and supports both Python 2 and 10 | 3. The calculation modules can be found in shadowcraft/calcs. Objects 11 | used for those calculations are defined in shadowcraft/objects. 12 | 13 | 14 | How To 15 | ------ 16 | 17 | In order to support both Python 2 and 3, ShadowCraft-Engine depends on 18 | the *future* library. You can install it by running: 19 | 20 | ``` 21 | pip install future 22 | ``` 23 | 24 | To run a simple calculation for the rogue spec of your choice you can 25 | look at the examples in the scripts folder. Feel free to play around 26 | and edit those files as you see fit, when testing. E.g. to run a DPS 27 | calculation for Subtlety, type: 28 | 29 | ``` 30 | python scripts/subtlety.py 31 | ``` 32 | 33 | 34 | Tests 35 | ----- 36 | 37 | Although the tests currently do not provide good number testing for the 38 | different specialization models, they can be used to ensure that nothing 39 | major is broken. Run the tests using the following command: 40 | 41 | ``` 42 | python tests/runtests.py 43 | ``` 44 | 45 | Of course, we appreciate any help in extending the test coverage for the 46 | engine. 47 | 48 | 49 | Contributing 50 | ------------ 51 | 52 | The ShadowCraft team is always looking for help. If you would like to 53 | contribute to the engine, you can always contact the active developers 54 | or open a pull request on GitHub. There is also a #shadowcraft channel 55 | on the [Ravenholdt Discord Server](https://discord.gg/DdPahJ9) that can 56 | be used for discussion about the project. 57 | 58 | Before writing code and submitting for review, please have a look at the 59 | code style guidelines in style.md. 60 | -------------------------------------------------------------------------------- /extra/swing delay graphs.gcx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShadowCraft/ShadowCraft-Engine/1821fcab201c7ab969c60286d2123c646161f65b/extra/swing delay graphs.gcx -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /scripts/assassination.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from __future__ import print_function 3 | # Simple test program to debug + play with assassination models. 4 | from builtins import str 5 | from os import path 6 | import sys 7 | from pprint import pprint 8 | sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) 9 | 10 | from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator 11 | from shadowcraft.calcs.rogue.Aldriana import settings 12 | 13 | from shadowcraft.objects import buffs 14 | from shadowcraft.objects import race 15 | from shadowcraft.objects import stats 16 | from shadowcraft.objects import procs 17 | from shadowcraft.objects import talents 18 | from shadowcraft.objects import artifact 19 | 20 | from shadowcraft.core import i18n 21 | 22 | # Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. 23 | test_language = 'local' 24 | i18n.set_language(test_language) 25 | 26 | # Set up level/class/race 27 | test_level = 110 28 | test_race = race.Race('blood_elf', 'rogue', 110) 29 | test_class = 'rogue' 30 | test_spec = 'assassination' 31 | 32 | # Set up buffs. 33 | test_buffs = buffs.Buffs( 34 | 'short_term_haste_buff', 35 | 'flask_legion_agi', 36 | 'food_legion_mastery_375' 37 | ) 38 | 39 | # Set up weapons. 40 | test_mh = stats.Weapon(7063.0, 1.8, 'dagger', None) 41 | test_oh = stats.Weapon(7063.0, 1.8, 'dagger', None) 42 | 43 | # Set up procs. 44 | #test_procs = procs.ProcsList(('scales_of_doom', 691), ('beating_heart_of_the_mountain', 701), 45 | # 'draenic_agi_pot', 'draenic_agi_prepot', 'archmages_greater_incandescence') 46 | test_procs = procs.ProcsList('old_war_pot', 'old_war_prepot', 47 | ('engine_of_eradication', 920), 48 | ('specter_of_betrayal', 915) 49 | ) 50 | 51 | # Set up gear buffs. 52 | test_gear_buffs = stats.GearBuffs('gear_specialization', 53 | 'rogue_t20_2pc', 54 | 'rogue_t20_4pc', 55 | 'zoldyck_family_training_shackles', 56 | 'mantle_of_the_master_assassin', 57 | ) 58 | 59 | # Set up a calcs object.. 60 | test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, 61 | agi=26271, 62 | stam=43292, 63 | crit=8206, 64 | haste=3629, 65 | mastery=11384, 66 | versatility=4237) 67 | 68 | # Initialize talents.. 69 | #test_talents = talents.Talents('2110031', test_spec, test_class, level=test_level) 70 | test_talents = talents.Talents('1230011', test_spec, test_class, level=test_level) 71 | 72 | #initialize artifact traits.. 73 | test_traits = artifact.Artifact(test_spec, test_class, trait_dict={'assassins_blades': 1, 'bag_of_tricks': 1, 'balanced_blades': 4, 'blood_of_the_assassinated': 1, 'fade_into_shadows': 4, 'from_the_shadows': 1, 'kingsbane': 1, 'gushing_wounds': 4, 'master_alchemist': 6, 'master_assassin': 5, 'poison_knives': 4, 'serrated_edge': 4, 'shadow_swiftness': 1, 'shadow_walker': 4, 'surge_of_toxins': 1, 'toxic_blades': 4, 'urge_to_kill': 1, 'slayers_precision': 1, 'silence_of_the_uncrowned': 1, 'strangler': 4, 'dense_concoction': 1, 'sinister_circulation': 1, 'concordance_of_the_legionfall': 24}) 74 | 75 | # Set up settings. 76 | test_cycle = settings.AssassinationCycle() 77 | test_settings = settings.Settings(test_cycle, response_time=.5, duration=300, 78 | finisher_threshold=4) 79 | 80 | # Build a DPS object. 81 | calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_traits, test_buffs, test_race, test_spec, test_settings, test_level) 82 | 83 | print(str(calculator.stats.get_character_stats(calculator.race, test_traits))) 84 | 85 | # Compute DPS Breakdown. 86 | dps_breakdown = calculator.get_dps_breakdown() 87 | total_dps = sum(entry[1] for entry in list(dps_breakdown.items())) 88 | 89 | # Compute EP values. 90 | ep_values = calculator.get_ep(baseline_dps=total_dps) 91 | tier_ep_values = calculator.get_other_ep(['rogue_t19_2pc', 'rogue_t19_4pc', 'rogue_orderhall_8pc', 92 | 'rogue_t20_2pc', 'rogue_t20_4pc', 93 | 'rogue_t21_2pc', 'rogue_t21_4pc', 94 | 'mark_of_the_hidden_satyr', 'mark_of_the_distant_army', 95 | 'mark_of_the_claw', 'march_of_the_legion_2pc', 96 | 'journey_through_time_2pc', 'jacins_ruse_2pc', 97 | 'kara_empowered_2pc']) 98 | 99 | 100 | #talent_ranks = calculator.get_talents_ranking() 101 | #trait_ranks = calculator.get_trait_ranking() 102 | 103 | def max_length(dict_list): 104 | max_len = 0 105 | for i in dict_list: 106 | dict_values = list(i.items()) 107 | if max_len < max(len(entry[0]) for entry in dict_values): 108 | max_len = max(len(entry[0]) for entry in dict_values) 109 | 110 | return max_len 111 | 112 | def pretty_print(dict_list): 113 | max_len = max_length(dict_list) 114 | 115 | for i in dict_list: 116 | dict_values = list(i.items()) 117 | dict_values.sort(key=lambda entry: entry[1], reverse=True) 118 | for value in dict_values: 119 | if ("{0:.2f}".format(value[1] / total_dps)) != '0.00': 120 | print(value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' ('+str( "{0:.2f}".format(100*float(value[1])/total_dps) )+'%)') 121 | else: 122 | print(value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1])) 123 | print('-' * (max_len + 15)) 124 | 125 | dicts_for_pretty_print = [ 126 | ep_values, 127 | tier_ep_values, 128 | #talent_ranks, 129 | #trinkets_ep_value, 130 | dps_breakdown, 131 | #trait_ranks 132 | ] 133 | pretty_print(dicts_for_pretty_print) 134 | 135 | #pretty_print([dps_breakdown], total_sum=total_dps, show_percent=True) 136 | print(' ' * (max_length([dps_breakdown]) + 1), total_dps, ("total damage per second.")) 137 | 138 | #pprint(talent_ranks) 139 | -------------------------------------------------------------------------------- /scripts/outlaw.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from __future__ import print_function 3 | # Simple test program to debug + play with assassination models. 4 | from builtins import str 5 | from os import path 6 | import sys 7 | from pprint import pprint 8 | sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) 9 | 10 | from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator 11 | from shadowcraft.calcs.rogue.Aldriana import settings 12 | 13 | from shadowcraft.objects import buffs 14 | from shadowcraft.objects import race 15 | from shadowcraft.objects import stats 16 | from shadowcraft.objects import procs 17 | from shadowcraft.objects import talents 18 | from shadowcraft.objects import artifact 19 | 20 | from shadowcraft.core import i18n 21 | 22 | # Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. 23 | test_language = 'local' 24 | i18n.set_language(test_language) 25 | 26 | # Set up level/class/race 27 | test_level = 110 28 | test_race = race.Race('pandaren', 'rogue', 110) 29 | test_class = 'rogue' 30 | test_spec = 'outlaw' 31 | 32 | # Set up buffs. 33 | test_buffs = buffs.Buffs( 34 | 'short_term_haste_buff', 35 | 'flask_legion_agi', 36 | 'food_legion_versatility_375' 37 | ) 38 | 39 | # Set up weapons. mark_of_the_frostwolf mark_of_the_shattered_hand 40 | test_mh = stats.Weapon(4821.0, 2.6, 'sword', None) 41 | test_oh = stats.Weapon(4821.0, 2.6, 'sword', None) 42 | 43 | # Set up procs. 44 | #test_procs = procs.ProcsList(('assurance_of_consequence', 588), 45 | #('draenic_philosophers_stone', 620), 'virmens_bite', 'virmens_bite_prepot', 46 | #'archmages_incandescence') #trinkets, other things (legendary procs) 47 | test_procs = procs.ProcsList( 48 | 'mark_of_the_hidden_satyr', 49 | 'old_war_pot', 50 | 'old_war_prepot', 51 | ('nightblooming_frond', 895), 52 | ('memento_of_angerboda', 885) 53 | ) 54 | 55 | # Set up gear buffs. 56 | test_gear_buffs = stats.GearBuffs( 57 | 'gear_specialization', 58 | 'rogue_t19_2pc', 59 | 'rogue_t19_4pc', 60 | 'mantle_of_the_master_assassin', 61 | 'greenskins_waterlogged_wristcuffs' 62 | ) 63 | 64 | # Set up a calcs object.. 65 | test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, 66 | agi=round(35872 * 0.95238 - test_race.racial_agi), 67 | stam=28367, 68 | crit=9070, 69 | haste=2476, 70 | mastery=6254, 71 | versatility=5511,) 72 | 73 | # Initialize talents.. 74 | test_talents = talents.Talents('3213122', test_spec, test_class, level=test_level) 75 | 76 | #initialize artifact traits.. 77 | test_traits = artifact.Artifact(test_spec, test_class, trait_dict={ 78 | 'curse_of_the_dreadblades': 1, 79 | 'cursed_edges': 1, 80 | 'fates_thirst': 4, 81 | 'blade_dancer': 3, 82 | 'fatebringer': 4, 83 | 'gunslinger': 3, 84 | 'hidden_blade': 1, 85 | 'fortune_strikes': 3, 86 | 'ghostly_shell': 3, 87 | 'deception': 1, 88 | 'black_powder': 4, 89 | 'greed': 1, 90 | 'blurred_time': 1, 91 | 'fortunes_boon': 3, 92 | 'fortunes_strike': 3, 93 | 'blademaster': 1, 94 | 'blunderbuss': 1, 95 | 'cursed_steel': 1, 96 | 'bravado_of_the_uncrowned': 1, 97 | 'sabermetrics': 0, 98 | 'dreadblades_vigor': 0, 99 | 'loaded_dice': 0, 100 | 'concordance_of_the_legionfall': 0, 101 | }) 102 | 103 | # Set up settings. 104 | test_cycle = settings.OutlawCycle(blade_flurry=False, 105 | jolly_roger_reroll=2, 106 | grand_melee_reroll=2, 107 | shark_reroll=2, 108 | true_bearing_reroll=0, 109 | buried_treasure_reroll=2, 110 | broadsides_reroll=2, 111 | between_the_eyes_policy='never' 112 | ) 113 | test_settings = settings.Settings(test_cycle, response_time=.5, duration=300, 114 | adv_params="", is_demon=True, num_boss_adds=0, 115 | finisher_threshold=5) 116 | 117 | # Build a DPS object. 118 | calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_traits, test_buffs, test_race, test_spec, test_settings, test_level) 119 | 120 | print(str(test_stats.get_character_stats(test_race, test_traits))) 121 | 122 | # Compute DPS Breakdown. 123 | dps_breakdown = calculator.get_dps_breakdown() 124 | total_dps = sum(entry[1] for entry in list(dps_breakdown.items())) 125 | 126 | # Compute EP values. 127 | ep_values = calculator.get_ep(baseline_dps=total_dps) 128 | tier_ep_values = calculator.get_other_ep(['rogue_t16_2pc', 'rogue_t16_4pc', 'mantle_of_the_master_assassin']) 129 | #mh_enchants_and_dps_ep_values, oh_enchants_and_dps_ep_values = 130 | #calculator.get_weapon_ep(dps=True, enchants=True) 131 | 132 | #talent_ranks = calculator.get_talents_ranking() 133 | #trait_ranks = calculator.get_trait_ranking() 134 | 135 | def max_length(dict_list): 136 | max_len = 0 137 | for i in dict_list: 138 | dict_values = list(i.items()) 139 | if max_len < max(len(entry[0]) for entry in dict_values): 140 | max_len = max(len(entry[0]) for entry in dict_values) 141 | 142 | return max_len 143 | 144 | def pretty_print(dict_list, total_sum=1., show_percent=False): 145 | max_len = max_length(dict_list) 146 | 147 | for i in dict_list: 148 | dict_values = list(i.items()) 149 | dict_values.sort(key=lambda entry: entry[1], reverse=True) 150 | for value in dict_values: 151 | #print value[0] + ':' + ' ' * (max_len - len(value[0])), 152 | #str(value[1]) 153 | if show_percent and ("{0:.2f}".format(value[1] / total_dps)) != '0.00': 154 | print(value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' (' + str("{0:.2f}".format(100 * float(value[1]) / total_sum)) + '%)') 155 | else: 156 | print(value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1])) 157 | print('-' * (max_len + 15)) 158 | 159 | dicts_for_pretty_print = [ep_values, 160 | tier_ep_values, 161 | #talent_ranks, 162 | #trinkets_ep_value, 163 | dps_breakdown, 164 | #trait_ranks 165 | ] 166 | pretty_print(dicts_for_pretty_print) 167 | print(' ' * (max_length(dicts_for_pretty_print) + 1), total_dps, ("total damage per second.")) 168 | 169 | #pprint(talent_ranks) 170 | -------------------------------------------------------------------------------- /scripts/subtlety.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from __future__ import print_function 3 | # Simple test program to debug + play with subtlety models. 4 | from builtins import str 5 | from os import path 6 | import sys 7 | from pprint import pprint 8 | sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) 9 | 10 | from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator 11 | from shadowcraft.calcs.rogue.Aldriana import settings 12 | from shadowcraft.calcs.rogue.Aldriana import settings_data 13 | 14 | from shadowcraft.objects import buffs 15 | from shadowcraft.objects import race 16 | from shadowcraft.objects import stats 17 | from shadowcraft.objects import procs 18 | from shadowcraft.objects import talents 19 | from shadowcraft.objects import artifact 20 | 21 | from shadowcraft.core import i18n 22 | 23 | # Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. 24 | test_language = 'local' 25 | i18n.set_language(test_language) 26 | 27 | # Set up level/class/race 28 | test_level = 110 29 | test_race = race.Race('blood_elf', level=110) 30 | test_class = 'rogue' 31 | test_spec = 'subtlety' 32 | 33 | # Set up buffs. 34 | test_buffs = buffs.Buffs( 35 | 'short_term_haste_buff', 36 | 'flask_legion_agi', 37 | 'food_legion_mastery_375', 38 | #'food_legion_feast_150' 39 | ) 40 | 41 | # Set up weapons. 42 | test_mh = stats.Weapon(10064.0, 1.8, 'dagger', None) 43 | test_oh = stats.Weapon(10064.0, 1.8, 'dagger', None) 44 | 45 | # Set up procs. - trinkets, other things (legendary procs) 46 | test_procs = procs.ProcsList( 47 | 'mark_of_the_hidden_satyr', 48 | ('specter_of_betrayal', 925), 49 | #('forgefiends_fabricator', 930), 50 | #('kiljaedens_burning_wish', 940) 51 | #'old_war_pot', 52 | #'old_war_prepot', 53 | 'prolonged_power_pot', 54 | 'prolonged_power_prepot', 55 | ) 56 | 57 | """ 58 | # test all procs 59 | from shadowcraft.objects import proc_data 60 | test_procs = procs.ProcsList() 61 | for key in proc_data.allowed_procs.keys(): 62 | test_procs.set_proc(key) 63 | 64 | 65 | # Debug prints for scaled trinket values 66 | for proc in test_procs.get_all_procs_for_stat(): 67 | if proc.scaling: 68 | print proc.proc_name + " - " + str(proc.item_level) + " - " + str(proc.value) 69 | """ 70 | 71 | # Set up gear buffs. 72 | test_gear_buffs = stats.GearBuffs('gear_specialization', 73 | 'insignia_of_ravenholdt', 74 | 'rogue_t20_2pc', 75 | 'rogue_t20_4pc', 76 | #'insignia_of_ravenholdt', 77 | 'mantle_of_the_master_assassin', 78 | ) #tier buffs located here 79 | 80 | # Set up a calcs object.. 81 | test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, 82 | agi=round(42158 * 0.95238 - test_race.racial_agi), #gear spec and racial agi are added during calc again 83 | stam=54585, 84 | crit=13821, 85 | haste=4139, 86 | mastery=5600, 87 | versatility=7111,) 88 | 89 | # Initialize talents.. 90 | test_talents = talents.Talents('1113213', test_spec, test_class, level=test_level) 91 | 92 | #initialize artifact traits.. 93 | test_traits = artifact.Artifact(test_spec, test_class, trait_dict={ 94 | 'goremaws_bite': 1, 95 | 'shadow_fangs': 1, 96 | 'gutripper': 4, 97 | 'fortunes_bite': 4, 98 | 'catlike_reflexes': 4, 99 | 'embrace_of_darkness': 1, 100 | 'ghost_armor': 4, 101 | 'precision_strike': 4, 102 | 'energetic_stabbing': 4+3, 103 | 'flickering_shadows': 1, 104 | 'second_shuriken': 1, 105 | 'demons_kiss': 4, 106 | 'finality': 1, 107 | 'the_quiet_knife': 4, 108 | 'akarris_soul': 1, 109 | 'soul_shadows': 4, 110 | 'shadow_nova': 1, 111 | 'legionblade': 1, 112 | 'shadows_of_the_uncrowned': 1, 113 | 'weak_point': 4+3, 114 | 'shadows_whisper': 1, 115 | 'feeding_frenzy': 1, 116 | 'concordance_of_the_legionfall': 24, 117 | #crucible 118 | 'torment_the_weak': 3, 119 | }) 120 | 121 | # Set up settings. 122 | test_cycle = settings.SubtletyCycle() 123 | test_settings = settings.Settings(test_cycle, pantheon_trinket_users=0, num_boss_adds=0) 124 | 125 | # Build a DPS object. 126 | calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_traits, test_buffs, test_race, test_spec, test_settings, test_level) 127 | 128 | print(str(test_stats.get_character_stats(test_race, test_traits))) 129 | 130 | # Compute DPS Breakdown. 131 | dps_breakdown = calculator.get_dps_breakdown() 132 | total_dps = sum(entry[1] for entry in list(dps_breakdown.items())) 133 | 134 | # Compute EP values. 135 | ep_values = calculator.get_ep(baseline_dps=total_dps) 136 | #ep_values = calculator.get_ep() 137 | tier_ep_values = calculator.get_other_ep(['rogue_t20_2pc', 'rogue_t20_4pc', 'the_first_of_the_dead', 'insignia_of_ravenholdt', 138 | 'mantle_of_the_master_assassin', 'specter_of_betrayal', 139 | 'golganneths_vitality', 'amanthuls_vision', 'shadowsinged_fang', 'seeping_scourgewing', 'terminus_signaling_beacon', 140 | 'gorshalachs_legacy', 'forgefiends_fabricator']) 141 | 142 | #talent_ranks = calculator.get_talents_ranking() 143 | trait_ranks = calculator.get_trait_ranking() 144 | 145 | def max_length(dict_list): 146 | max_len = 0 147 | for i in dict_list: 148 | dict_values = list(i.items()) 149 | if max_len < max(len(entry[0]) for entry in dict_values): 150 | max_len = max(len(entry[0]) for entry in dict_values) 151 | 152 | return max_len 153 | 154 | def pretty_print(dict_list): 155 | max_len = max_length(dict_list) 156 | 157 | for i in dict_list: 158 | dict_values = list(i.items()) 159 | dict_values.sort(key=lambda entry: entry[1], reverse=True) 160 | for value in dict_values: 161 | if ("{0:.2f}".format(value[1] / total_dps)) != '0.00': 162 | print(value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' ('+str( "{0:.2f}".format(100*float(value[1])/total_dps) )+'%)') 163 | else: 164 | print(value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1])) 165 | print('-' * (max_len + 15)) 166 | 167 | dicts_for_pretty_print = [ 168 | ep_values, 169 | tier_ep_values, 170 | #trinkets_ep_value, 171 | dps_breakdown, 172 | #trait_ranks 173 | ] 174 | pretty_print(dicts_for_pretty_print) 175 | print(' ' * (max_length(dicts_for_pretty_print) + 1), total_dps, ("total damage per second.")) 176 | 177 | """ 178 | for value in list(aps.items()): 179 | if type(value[1]) is float: 180 | val = value[1] * 300. 181 | else: 182 | val = sum(value[1]) * 300. 183 | print(str(value[0]) + ' - ' + str(val)) 184 | """ 185 | 186 | #pprint(trait_ranks) 187 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name='ShadowCraft-Engine', 5 | url='http://github.com/ShadowCraft/ShadowCraft-Engine/', 6 | version='7.3.0', 7 | packages=[ 8 | 'shadowcraft', 9 | 'shadowcraft.calcs', 10 | 'shadowcraft.calcs.rogue', 11 | 'shadowcraft.calcs.rogue.Aldriana', 12 | 'shadowcraft.core', 13 | 'shadowcraft.objects' 14 | ], 15 | license='LGPL', 16 | long_description=open('README.md').read(), 17 | ) 18 | -------------------------------------------------------------------------------- /shadowcraft/__init__.py: -------------------------------------------------------------------------------- 1 | from future import standard_library 2 | standard_library.install_aliases() 3 | import gettext 4 | import builtins 5 | 6 | _ = gettext.gettext 7 | -------------------------------------------------------------------------------- /shadowcraft/calcs/rogue/Aldriana/settings.py: -------------------------------------------------------------------------------- 1 | from builtins import object 2 | from shadowcraft.core import exceptions 3 | from shadowcraft.calcs.rogue.Aldriana import settings_data 4 | 5 | class Settings(object): 6 | # Settings object for AldrianasRogueDamageCalculator. 7 | 8 | def __init__(self, cycle, **kwargs): 9 | self.cycle = cycle 10 | self.default_ep_stat = kwargs.get('default_ep_stat', 'agi') 11 | self.feint_interval = int(kwargs.get('feint_interval', 0)) 12 | 13 | #Get defaults from settings_data 14 | defaults = settings_data.get_default_settings(settings_data.rogue_settings) 15 | settings_data.process_overrides(defaults, kwargs, self.cycle._cycle_type) 16 | 17 | self.response_time = float(kwargs.get('response_time', defaults['response_time'])) 18 | self.latency = float(kwargs.get('latency', defaults['latency'])) 19 | self.duration = int(kwargs.get('duration', defaults['duration'])) 20 | self.is_day = kwargs.get('is_day', defaults['is_day']) 21 | self.is_demon = kwargs.get('is_demon', defaults['is_demon']) 22 | self.num_boss_adds = max(int(kwargs.get('num_boss_adds', defaults['num_boss_adds'])), 0) 23 | self.adv_params = self.interpret_adv_params(kwargs.get('adv_params', defaults['adv_params'])) 24 | self.marked_for_death_resets = int(kwargs.get('marked_for_death_resets', defaults['marked_for_death_resets'])) 25 | self.finisher_threshold = int(kwargs.get('finisher_threshold', defaults['finisher_threshold'])) 26 | self.pantheon_trinket_users = int(kwargs.get('pantheon_trinket_users', defaults['pantheon_trinket_users'])) 27 | 28 | def interpret_adv_params(self, s=""): 29 | data = {} 30 | max_effects = 8 31 | current_effects = 0 32 | if s != "" and s: 33 | for e in s.split(';'): 34 | if e != "": 35 | tmp = e.split(':') 36 | try: 37 | data[tmp[0].strip().lower()] = tmp[1].strip().lower() #strip() and lower() needed so that everyone is on the same page print data[tmp[0].strip().lower()] + ' : ' + tmp[0].strip().lower() 38 | current_effects += 1 39 | if current_effects == max_effects: 40 | return data 41 | except: 42 | raise exceptions.InvalidInputException(_('Advanced Parameter ' + e + ' found corrupt. Properly structure params and try again.')) 43 | return data 44 | 45 | def is_assassination_rogue(self): 46 | return self.cycle._cycle_type == 'assassination' 47 | 48 | def is_outlaw_rogue(self): 49 | return self.cycle._cycle_type == 'outlaw' 50 | 51 | def is_subtlety_rogue(self): 52 | return self.cycle._cycle_type == 'subtlety' 53 | 54 | class Cycle(object): 55 | # Base class for cycle objects. Can't think of anything that particularly 56 | # needs to go here yet, but it seems worth keeping options open in that 57 | # respect. 58 | 59 | # When subclassing, define _cycle_type to be one of 'assassination', 60 | # 'outlaw', or 'subtlety' - this is how the damage calculator makes sure 61 | # you have an appropriate cycle object to go with your talent trees, etc. 62 | _cycle_type = '' 63 | 64 | 65 | class AssassinationCycle(Cycle): 66 | _cycle_type = 'assassination' 67 | 68 | def __init__(self, **kwargs): 69 | defaults = settings_data.get_default_settings(settings_data.rogue_settings) 70 | settings_data.process_overrides(defaults, kwargs, self._cycle_type) 71 | 72 | self.cp_builder = kwargs.get('cp_builder', defaults['cp_builder']) 73 | self.kingsbane_with_vendetta = kwargs.get('kingsbane', defaults['kingsbane']) 74 | self.exsang_with_vendetta = kwargs.get('exsang', defaults['exsang']) 75 | self.lethal_poison = kwargs.get('lethal_poison', defaults['lethal_poison']) 76 | 77 | class OutlawCycle(Cycle): 78 | _cycle_type = 'outlaw' 79 | #Generated by: 80 | #from itertools import combinations 81 | #single = ['jr', 'gm', 's', 'tb', 'bt', 'b'] 82 | #list(combinations(single, 6)) + list(combinations(single, 3)) + list(combinations(single, 2)) + list(combinations(single, 1)) 83 | rtb_combos = [('jr', 'gm', 's', 'tb', 'bt', 'b'), 84 | #3 buffs 85 | ('jr', 'gm', 's'), ('jr', 'gm', 'tb'), 86 | ('jr', 'gm', 'bt'), ('jr', 'gm', 'b'), 87 | ('jr', 's', 'tb'), ('jr', 's', 'bt'), 88 | ('jr', 's', 'b'), ('jr', 'tb', 'bt'), 89 | ('jr', 'tb', 'b'), ('jr', 'bt', 'b'), 90 | ('gm', 's', 'tb'), ('gm', 's', 'bt'), 91 | ('gm', 's', 'b'), ('gm', 'tb', 'bt'), 92 | ('gm', 'tb', 'b'), ('gm', 'bt', 'b'), 93 | ('s', 'tb', 'bt'), ('s', 'tb', 'b'), 94 | ('s', 'bt', 'b'), ('tb', 'bt', 'b'), 95 | #2 buffs 96 | ('jr', 'gm'), ('jr', 's'), ('jr', 'tb'), 97 | ('jr', 'bt'), ('jr', 'b'), ('gm', 's'), 98 | ('gm', 'tb'), ('gm', 'bt'), ('gm', 'b'), 99 | ('s', 'tb'), ('s', 'bt'), ('s', 'b'), 100 | ('tb', 'bt'), ('tb', 'b'), ('bt', 'b'), 101 | #single buffs 102 | ('jr',), ('gm',), ('s',), ('tb',), ('bt',), ('b',)] 103 | 104 | def __init__(self, **kwargs): 105 | defaults = settings_data.get_default_settings(settings_data.rogue_settings) 106 | settings_data.process_overrides(defaults, kwargs, self._cycle_type) 107 | 108 | self.blade_flurry = kwargs.get('blade_flurry', defaults['blade_flurry']) 109 | self.between_the_eyes_policy = kwargs.get('between_the_eyes_policy', defaults['between_the_eyes_policy']) 110 | self.jolly_roger_reroll = int(kwargs.get('jolly_roger_reroll', defaults['jolly_roger_reroll'])) 111 | self.grand_melee_reroll = int(kwargs.get('grand_melee_reroll', defaults['grand_melee_reroll'])) 112 | self.shark_reroll = int(kwargs.get('shark_reroll', defaults['shark_reroll'])) 113 | self.true_bearing_reroll = int(kwargs.get('true_bearing_reroll', defaults['true_bearing_reroll'])) 114 | self.buried_treasure_reroll = int(kwargs.get('buried_treasure_reroll', defaults['buried_treasure_reroll'])) 115 | self.broadsides_reroll = int(kwargs.get('broadsides_reroll', defaults['broadsides_reroll'])) 116 | 117 | # RtB reroll thresholds, 0, 1, 2, 3 118 | # 0 means never reroll combos with this buff 119 | # 1 means reroll singles of buff 120 | # 2 means reroll doubles containing this buff 121 | # 3 means reroll triples containing this buff 122 | #build reroll lists here 123 | self.reroll_list = [] 124 | self.keep_list = [] 125 | for combo in self.rtb_combos: 126 | buffs = len(combo) 127 | if 'jr' in combo and buffs <= self.jolly_roger_reroll: 128 | self.reroll_list.append(combo) 129 | continue 130 | if 'gm' in combo and buffs <= self.grand_melee_reroll: 131 | self.reroll_list.append(combo) 132 | continue 133 | if 's' in combo and buffs <= self.shark_reroll: 134 | self.reroll_list.append(combo) 135 | continue 136 | if 'tb' in combo and buffs <= self.true_bearing_reroll: 137 | self.reroll_list.append(combo) 138 | continue 139 | if 'bt' in combo and buffs <= self.buried_treasure_reroll: 140 | self.reroll_list.append(combo) 141 | continue 142 | if 'b' in combo and buffs <= self.broadsides_reroll: 143 | self.reroll_list.append(combo) 144 | continue 145 | self.keep_list.append(combo) 146 | 147 | class SubtletyCycle(Cycle): 148 | _cycle_type = 'subtlety' 149 | 150 | def __init__(self, **kwargs): 151 | defaults = settings_data.get_default_settings(settings_data.rogue_settings) 152 | settings_data.process_overrides(defaults, kwargs, self._cycle_type) 153 | 154 | self.cp_builder = kwargs.get('cp_builder', defaults['cp_builder']) 155 | self.dance_finishers_allowed = kwargs.get('dance_finishers_allowed', defaults['dance_finishers_allowed']) 156 | self.compute_cp_waste = kwargs.get('compute_cp_waste', defaults['compute_cp_waste']) 157 | 158 | #Handle percent vs float automatically 159 | self.positional_uptime = int(kwargs.get('positional_uptime_percent', defaults['positional_uptime_percent'])) / 100 160 | self.positional_uptime = float(kwargs.get('positional_uptime', self.positional_uptime)) 161 | -------------------------------------------------------------------------------- /shadowcraft/calcs/rogue/Aldriana/settings_data.py: -------------------------------------------------------------------------------- 1 | #This file contains all rogue settings that are advertised to the react UI and later passed 2 | #into the settings object before calculation. 3 | #Variable names are supposed to be unique and currently passed into the settings and cycle 4 | #constructors all together. 5 | #Options ending in "_spec" will set shared settings with the spec stripped. 6 | 7 | def get_default_settings(settings_data): 8 | defaults = {} 9 | for category in settings_data: 10 | for option in category['items']: 11 | defaults[option['name']] = option['default'] 12 | return defaults 13 | 14 | def process_overrides(defaults_dict, params_dict, spec): 15 | suffix = '_' + spec 16 | #Spec overrides from defaults 17 | for setting in list(defaults_dict.keys()): 18 | if setting.endswith(suffix): 19 | override_key = setting.replace(suffix, '') 20 | defaults_dict[override_key] = defaults_dict[setting] 21 | #Spec overrides from params 22 | for setting in params_dict: 23 | if setting.endswith(suffix): 24 | override_key = setting.replace(suffix, '') 25 | defaults_dict[override_key] = params_dict[setting] 26 | 27 | rogue_settings = [ 28 | { 29 | 'spec': 'a', 30 | 'heading': 'Assassination Rotation Settings', 31 | 'name': 'rotation.assassination', 32 | 'items': [ 33 | { 34 | 'name': 'kingsbane', 35 | 'label': 'Kingsbane w/ Vendetta', 36 | 'description': '', 37 | 'type': 'dropdown', 38 | 'default': 'just', 39 | 'options': { 40 | 'just': "Use cooldown if it aligns, but don't delay usage", 41 | 'only': 'Only use cooldown with Vendetta' 42 | } 43 | }, 44 | { 45 | 'name': 'exsang', 46 | 'label': 'Exsang w/ Vendetta', 47 | 'description': '', 48 | 'type': 'dropdown', 49 | 'default': 'just', 50 | 'options': { 51 | 'just': "Use cooldown if it aligns, but don't delay usage", 52 | 'only': 'Only use cooldown with Vendetta' 53 | } 54 | }, 55 | { 56 | 'name': 'cp_builder_assassination', 57 | 'label': 'CP Builder', 58 | 'description': '', 59 | 'type': 'dropdown', 60 | 'default': 'mutilate', 61 | 'options': { 62 | 'mutilate': 'Mutilate', 63 | 'fan_of_knives': 'Fan of Knives' 64 | } 65 | }, 66 | { 67 | 'name': 'lethal_poison', 68 | 'label': 'Lethal Poison', 69 | 'description': '', 70 | 'type': 'dropdown', 71 | 'default': 'dp', 72 | 'options': { 73 | 'dp': 'Deadly Poison', 74 | 'wp': 'Wound Poison' 75 | } 76 | }, 77 | { 78 | 'name': 'finisher_threshold_assassination', 79 | 'label': 'Finisher Threshold', 80 | 'description': 'Minimum CPs to use finisher', 81 | 'type': 'dropdown', 82 | 'default': '4', 83 | 'options': { 84 | '4': '4', 85 | '5': '5', 86 | '6': '6' 87 | } 88 | }, 89 | ] 90 | }, 91 | { 92 | 'spec': 'Z', 93 | 'heading': 'Outlaw Rotation Settings', 94 | 'name': 'rotation.outlaw', 95 | 'items': [ 96 | { 97 | 'name': 'blade_flurry', 98 | 'label': 'Blade Flurry', 99 | 'description': 'Use Blade Flurry', 100 | 'type': 'checkbox', 101 | 'default': False 102 | }, 103 | { 104 | 'name': 'between_the_eyes_policy', 105 | 'label': 'BtE Policy', 106 | 'description': '', 107 | 'type': 'dropdown', 108 | 'default': 'never', 109 | 'options': { 110 | 'shark': 'Only use with Shark', 111 | 'always': 'Use BtE on cooldown', 112 | 'never': 'Never use BtE', 113 | } 114 | }, 115 | { 116 | 'name': 'reroll_policy', 117 | 'label': 'RtB Reroll Policy', 118 | 'description': '', 119 | 'type': 'dropdown', 120 | 'default': 'custom', 121 | 'options': { 122 | '1': 'Reroll single buffs', 123 | '2': 'Reroll two or fewer buffs', 124 | '3': 'Reroll three or fewer buffs', 125 | 'custom': 'Custom setup per buff (see below)', 126 | } 127 | }, 128 | { 129 | 'name': 'jolly_roger_reroll', 130 | 'label': 'Jolly Roger', 131 | 'description': '', 132 | 'type': 'dropdown', 133 | 'default': '2', 134 | 'options': { 135 | '0': '0 - Never reroll combos with this buff', 136 | '1': '1 - Reroll single buff rolls of this buff', 137 | '2': '2 - Reroll double-buff rolls containing this buff', 138 | '3': '3 - Reroll triple-buff rolls containing this buff' 139 | } 140 | }, 141 | { 142 | 'name': 'grand_melee_reroll', 143 | 'label': 'Grand Melee', 144 | 'description': '', 145 | 'type': 'dropdown', 146 | 'default': '2', 147 | 'options': { 148 | '0': '0 - Never reroll combos with this buff', 149 | '1': '1 - Reroll single buff rolls of this buff', 150 | '2': '2 - Reroll double-buff rolls containing this buff', 151 | '3': '3 - Reroll triple-buff rolls containing this buff' 152 | } 153 | }, 154 | { 155 | 'name': 'shark_reroll', 156 | 'label': 'Shark-Infested Waters', 157 | 'description': '', 158 | 'type': 'dropdown', 159 | 'default': '2', 160 | 'options': { 161 | '0': '0 - Never reroll combos with this buff', 162 | '1': '1 - Reroll single buff rolls of this buff', 163 | '2': '2 - Reroll double-buff rolls containing this buff', 164 | '3': '3 - Reroll triple-buff rolls containing this buff' 165 | } 166 | }, 167 | { 168 | 'name': 'true_bearing_reroll', 169 | 'label': 'True Bearing', 170 | 'description': '', 171 | 'type': 'dropdown', 172 | 'default': '0', 173 | 'options': { 174 | '0': '0 - Never reroll combos with this buff', 175 | '1': '1 - Reroll single buff rolls of this buff', 176 | '2': '2 - Reroll double-buff rolls containing this buff', 177 | '3': '3 - Reroll triple-buff rolls containing this buff' 178 | } 179 | }, 180 | { 181 | 'name': 'buried_treasure_reroll', 182 | 'label': 'Buried Treasure', 183 | 'description': '', 184 | 'type': 'dropdown', 185 | 'default': '2', 186 | 'options': { 187 | '0': '0 - Never reroll combos with this buff', 188 | '1': '1 - Reroll single buff rolls of this buff', 189 | '2': '2 - Reroll double-buff rolls containing this buff', 190 | '3': '3 - Reroll triple-buff rolls containing this buff' 191 | } 192 | }, 193 | { 194 | 'name': 'broadsides_reroll', 195 | 'label': 'Broadsides', 196 | 'description': '', 197 | 'type': 'dropdown', 198 | 'default': '2', 199 | 'options': { 200 | '0': '0 - Never reroll combos with this buff', 201 | '1': '1 - Reroll single buff rolls of this buff', 202 | '2': '2 - Reroll double-buff rolls containing this buff', 203 | '3': '3 - Reroll triple-buff rolls containing this buff' 204 | } 205 | }, 206 | { 207 | 'name': 'finisher_threshold_outlaw', 208 | 'label': 'Finisher Threshold', 209 | 'description': 'Minimum CPs to use finisher', 210 | 'type': 'dropdown', 211 | 'default': '5', 212 | 'options': { 213 | '4': '4', 214 | '5': '5', 215 | '6': '6' 216 | } 217 | }, 218 | ] 219 | }, 220 | { 221 | 'spec': 'b', 222 | 'heading': 'Subtlety Rotation Settings', 223 | 'name': 'rotation.subtlety', 224 | 'items': [ 225 | { 226 | 'name': 'cp_builder_subtlety', 227 | 'label': 'CP Builder', 228 | 'description': '', 229 | 'type': 'dropdown', 230 | 'default': 'backstab', 231 | 'options': { 232 | 'backstab': 'Backstab', 233 | 'shuriken_storm': 'Shuriken Storm', 234 | } 235 | }, 236 | { 237 | 'name': 'dance_finishers_allowed', 238 | 'label': 'Use Finishers during Dance', 239 | 'description': '', 240 | 'type': 'checkbox', 241 | 'default': True 242 | }, 243 | { 244 | 'name': 'positional_uptime_percent', 245 | 'label': 'Backstab uptime', 246 | 'description': 'Percentage of the fight you are behind the target (0-100). This has no effect if Gloomblade is selected as a talent.', 247 | 'type': 'text', 248 | 'default': '100' 249 | }, 250 | { 251 | 'name': 'compute_cp_waste', 252 | 'label': 'Compute CP Waste', 253 | 'description': 'EXPERIMENTAL FEATURE: Compute combo point waste', 254 | 'type': 'checkbox', 255 | 'default': False 256 | }, 257 | { 258 | 'name': 'finisher_threshold_subtlety', 259 | 'label': 'Finisher Threshold', 260 | 'description': 'Minimum CPs to use finisher', 261 | 'type': 'dropdown', 262 | 'default': '5', 263 | 'options': { 264 | '4': '4', 265 | '5': '5', 266 | '6': '6' 267 | } 268 | }, 269 | ] 270 | }, 271 | { 272 | 'spec': 'All', 273 | 'heading': 'Raid Buffs', 274 | 'name': 'buffs', 275 | 'items': [ 276 | { 277 | 'name': 'flask_legion_agi', 278 | 'label': 'Legion Agility Flask', 279 | 'description': 'Flask of the Seventh Demon (1300 Agility)', 280 | 'type': 'checkbox', 281 | 'default': False 282 | }, 283 | { 284 | 'name': 'short_term_haste_buff', 285 | 'label': '+30% Haste/40 sec', 286 | 'description': 'Heroism/Bloodlust/Time Warp', 287 | 'type': 'checkbox', 288 | 'default': False 289 | }, 290 | { 291 | 'name': 'food_buff', 292 | 'label': 'Food', 293 | 'description': '', 294 | 'type': 'dropdown', 295 | 'default': 'food_legion_feast_500', 296 | 'options': { 297 | 'food_legion_crit_375': 'The Hungry Magister (375 Crit)', 298 | 'food_legion_haste_375': 'Azshari Salad (375 Haste)', 299 | 'food_legion_mastery_375': 'Nightborne Delicacy Platter (375 Mastery)', 300 | 'food_legion_versatility_375': 'Seed-Battered Fish Plate (375 Versatility)', 301 | 'food_legion_feast_500': 'Lavish Suramar Feast (500 Agility)', 302 | 'food_legion_damage_3': 'Fishbrul Special (High Fire Proc)', 303 | } 304 | }, 305 | { 306 | 'name': 'prepot', 307 | 'label': 'Pre-pot', 308 | 'description': '', 309 | 'type': 'dropdown', 310 | 'default': 'prolonged_power_pot', 311 | 'options': { 312 | 'old_war_pot': 'Potion of the Old War', 313 | 'prolonged_power_pot': 'Potion of Prolonged Power', 314 | 'potion_none': 'None', 315 | } 316 | }, 317 | { 318 | 'name': 'potion', 319 | 'label': 'Combat Potion', 320 | 'description': '', 321 | 'type': 'dropdown', 322 | 'default': 'prolonged_power_prepot', 323 | 'options': { 324 | 'old_war_prepot': 'Potion of the Old War', 325 | 'prolonged_power_prepot': 'Potion of Prolonged Power', 326 | 'potion_none': 'None', 327 | } 328 | } 329 | ] 330 | }, 331 | { 332 | 'spec': 'All', 333 | 'heading': 'General Settings', 334 | 'name': 'general.settings', 335 | 'items': [ 336 | { 337 | 'name': 'is_demon', 338 | 'label': 'Enemy is Demon', 339 | 'description': 'Enables damage buff from heirloom trinket against demons', 340 | 'type': 'checkbox', 341 | 'default': False 342 | }, 343 | { 344 | 'name': 'patch', 345 | 'label': 'Patch/Engine', 346 | 'description': '', 347 | 'type': 'dropdown', 348 | 'default': '7.0', 349 | 'options': { 350 | '7.0': '7.0', 351 | 'fierys_strange_voodoo': 'fierys strange voodoo', 352 | } 353 | }, 354 | { 355 | 'name': 'race', 356 | 'label': 'Race', 357 | 'description': '', 358 | 'type': 'dropdown', 359 | 'default': 'human', 360 | 'options': { 361 | 'human': 'Human', 362 | 'dwarf': 'Dwarf', 363 | 'orc': 'Orc', 364 | 'blood_elf': 'Blood Elf', 365 | 'gnome': 'Gnome', 366 | 'worgen': 'Worgen', 367 | 'troll': 'Troll', 368 | 'night_elf': 'Night Elf', 369 | 'undead': 'Undead', 370 | 'goblin': 'Goblin', 371 | 'pandaren': 'Pandaren', 372 | 'void_elf': 'Void Elf', 373 | 'nightborne': 'Nightborne' 374 | } 375 | }, 376 | { 377 | 'name': 'is_day', 378 | 'label': 'Night Elf Racial', 379 | 'description': '', 380 | 'type': 'dropdown', 381 | 'default': False, 382 | 'options': { 383 | False: 'Night', 384 | True: 'Day', 385 | } 386 | }, 387 | { 388 | 'name': 'level', 389 | 'label': 'Level', 390 | 'description': '', 391 | 'type': 'text', 392 | 'default': '110' 393 | }, 394 | { 395 | 'name': 'duration', 396 | 'label': 'Fight Duration', 397 | 'description': '', 398 | 'type': 'text', 399 | 'default': '300' 400 | }, 401 | { 402 | 'name': 'response_time', 403 | 'label': 'Response Time', 404 | 'description': '', 405 | 'type': 'text', 406 | 'default': '0.5', 407 | }, 408 | { 409 | 'name': 'num_boss_adds', 410 | 'label': 'Number of Boss Adds', 411 | 'description': '', 412 | 'type': 'text', 413 | 'default': '0', 414 | }, 415 | { 416 | 'name': 'marked_for_death_resets', 417 | 'label': 'Total number of additional MfD Resets', 418 | 'description': '', 419 | 'type': 'text', 420 | 'default': '0', 421 | }, 422 | { 423 | 'name': 'pantheon_trinket_users', 424 | 'label': 'Number of players with Pantheon trinkets', 425 | 'description': '', 426 | 'type': 'text', 427 | 'default': '0', 428 | } 429 | ] 430 | }, 431 | { 432 | 'spec': 'All', 433 | 'heading': 'Other', 434 | 'name': 'other', 435 | 'items': [ 436 | { 437 | 'name': 'latency', 438 | 'label': 'Latency', 439 | 'description': '', 440 | 'type': 'text', 441 | 'default': '0.03' 442 | }, 443 | { 444 | 'name': 'adv_params', 445 | 'label': 'Advanced Parameters', 446 | 'description': '', 447 | 'type': 'text', 448 | 'default': '' 449 | } 450 | ] 451 | } 452 | ] 453 | -------------------------------------------------------------------------------- /shadowcraft/core/__init__.py: -------------------------------------------------------------------------------- 1 | from future import standard_library 2 | standard_library.install_aliases() 3 | import gettext 4 | import builtins 5 | 6 | _ = gettext.gettext 7 | -------------------------------------------------------------------------------- /shadowcraft/core/exceptions.py: -------------------------------------------------------------------------------- 1 | from builtins import str 2 | class InvalidInputException(Exception): 3 | # Base class for all our exceptions. All exceptions we generate should 4 | # either use or subclass this. 5 | 6 | def __init__(self, error_msg): 7 | self.error_msg = error_msg 8 | 9 | def __str__(self): 10 | return str(self.error_msg) 11 | 12 | 13 | class InvalidLevelException(InvalidInputException): 14 | pass 15 | -------------------------------------------------------------------------------- /shadowcraft/core/files.in: -------------------------------------------------------------------------------- 1 | scripts/assassination.py 2 | scripts/combat.py 3 | scripts/subtlety.py 4 | shadowcraft/__init__.py 5 | shadowcraft/calcs/__init__.py 6 | shadowcraft/calcs/armor_mitigation.py 7 | shadowcraft/calcs/rogue/__init__.py 8 | shadowcraft/calcs/rogue/Aldriana/__init__.py 9 | shadowcraft/calcs/rogue/Aldriana/settings.py 10 | shadowcraft/objects/__init__.py 11 | shadowcraft/objects/buffs.py 12 | shadowcraft/objects/glyphs.py 13 | shadowcraft/objects/procs.py 14 | shadowcraft/objects/race.py 15 | shadowcraft/objects/stats.py 16 | shadowcraft/objects/talents.py 17 | shadowcraft/objects/rogue/__init__.py 18 | shadowcraft/objects/rogue/rogue_glyphs.py 19 | shadowcraft/objects/rogue/rogue_talents.py 20 | shadowcraft/core/__init__.py 21 | shadowcraft/core/exceptions.py 22 | shadowcraft/core/i18n.py 23 | shadowcraft/core/jsoninput.py 24 | -------------------------------------------------------------------------------- /shadowcraft/core/i18n.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from future import standard_library 3 | standard_library.install_aliases() 4 | import gettext 5 | import os.path 6 | import locale 7 | import builtins 8 | import sys 9 | 10 | _ = gettext.gettext 11 | 12 | # Domain: this needs to be the name of our .mo files 13 | TRANSLATION_DOMAIN = 'SCE' 14 | LOCALE_DIR = os.path.join(os.path.dirname(__file__), "locale") 15 | 16 | def set_language(language): 17 | # This function will install gettext as the _() function and use the 18 | # language specified. It will fall back to code strings if given a not supported 19 | # language. Note that the 'local' value only makes sense when not running from 20 | # the hosted online version. 21 | install_args = { } 22 | if sys.api_version < 3: 23 | install_args['str'] = True 24 | if language == 'local': 25 | # Setting up a list of locales in your machine and asign them to the _() function 26 | languages_list = [] 27 | 28 | default_local_language, encoding = locale.getdefaultlocale() 29 | if (default_local_language): 30 | languages_list = [default_local_language] 31 | 32 | gnu_lang = os.environ.get('LANGUAGE', None) 33 | if (gnu_lang): 34 | languages_list += gnu_lang.split(":") 35 | 36 | gettext.translation(TRANSLATION_DOMAIN, LOCALE_DIR, fallback=True, languages=languages_list).install(**install_args) 37 | 38 | else: 39 | gettext.translation(TRANSLATION_DOMAIN, LOCALE_DIR, fallback=True, languages=[language]).install(**install_args) 40 | -------------------------------------------------------------------------------- /shadowcraft/core/jsoninput.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from builtins import str 3 | import json 4 | from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator 5 | from shadowcraft.calcs.rogue.Aldriana import settings 6 | from shadowcraft.core import exceptions 7 | from shadowcraft.objects import buffs 8 | from shadowcraft.objects import procs 9 | from shadowcraft.objects import race 10 | from shadowcraft.objects import stats 11 | from shadowcraft.objects.rogue import rogue_glyphs 12 | from shadowcraft.objects.rogue import rogue_talents 13 | 14 | class InvalidJSONException(exceptions.InvalidInputException): 15 | pass 16 | 17 | def from_json(json_string, character_class='rogue'): 18 | j = json.loads(json_string) 19 | try: 20 | race_object = race.Race(str(j['race']), character_class=character_class) 21 | level = int(j['level']) 22 | 23 | s = j['settings'] 24 | settings_type = s['type'] 25 | if settings_type == 'assassination': 26 | # AssassinationCycle(self, min_envenom_size_mutilate=4, min_envenom_size_backstab=5, prioritize_rupture_uptime_mutilate=True, prioritize_rupture_uptime_backstab=True) 27 | c = s.get('cycle', {}) 28 | cycle = settings.AssassinationCycle(c.get('min_envenom_size_mutilate', 4), c.get('min_envenom_size_backstab', 5), 29 | c.get('prioritize_rupture_uptime_mutilate', True), c.get('prioritize_rupture_uptime_backstab', True)) 30 | elif settings_type == 'combat': 31 | # CombatCycle(self, use_rupture=True, use_revealing_strike='sometimes', ksp_immediately=False) 32 | c = s.get('cycle', {}) 33 | cycle = settings.OutlawCycle(c.get('use_rupture', True), c.get('use_revealing_strike', 'sometimes'), c.get('ksp_immediately', False)) 34 | elif settings_type == 'subtlety': 35 | # SubletySycle(raid_crits_per_second, clip_recuperate=False) 36 | c = s['cycle'] 37 | cycle = settings.SubtletyCycle(c['raid_crits_per_second'], c.get('clip_recuperate', False)) 38 | else: 39 | raise InvalidJSONException(_("Missing settings")) 40 | 41 | # Settings(cycle, time_in_execute_range=.35, tricks_on_cooldown=True, response_time=.5, mh_poison='ip', oh_poison='dp', duration=300): 42 | settings_object = settings.Settings(cycle, s.get('time_in_execute_range', .35), s.get('tricks_on_cooldown', True), 43 | s.get('response_time', .5), s.get('mh_poison', 'ip'), s.get('oh_poison', 'dp'), s.get('duration', 300)) 44 | 45 | stats_dict = j['stats'] 46 | # Weapon(damage, speed, weapon_type, enchant=None): 47 | mh_dict = stats_dict['mh'] 48 | mh = stats.Weapon(mh_dict['damage'], mh_dict['speed'], mh_dict['type'], mh_dict.get('enchant')) 49 | oh_dict = stats_dict['oh'] 50 | oh = stats.Weapon(oh_dict['damage'], oh_dict['speed'], oh_dict['type'], oh_dict.get('enchant')) 51 | ranged_dict = stats_dict['ranged'] 52 | ranged = stats.Weapon(ranged_dict['damage'], ranged_dict['speed'], ranged_dict['type'], ranged_dict.get('enchant')) 53 | procs_list = procs.ProcsList(*stats_dict['procs']) 54 | gear_buffs = stats.GearBuffs(*stats_dict['gear_buffs']) 55 | # Stats(str, agi, ap, crit, hit, exp, haste, mastery, mh, oh, ranged, procs, gear_buffs, level=85): 56 | def s(stat): 57 | return int(stats_dict[stat]) 58 | stats_object = stats.Stats( 59 | mh, oh, procs_list, gear_buffs, 60 | str=s('str'), agi=s('agi'), crit=s('crit'), haste=s('haste'), mastery=s('mastery')) 61 | glyphs = rogue_glyphs.RogueGlyphs(*j['glyphs']) 62 | talents = rogue_talents.RogueTalents(*j['talents']) 63 | buffs_object = buffs.Buffs(*j['buffs']) 64 | except KeyError as e: 65 | raise InvalidJSONException(_("Missing required input {key}").format(key=str(e))) 66 | # Calculator(stats, talents, glyphs, buffs, race, settings=None, level=85): 67 | return AldrianasRogueDamageCalculator(stats_object, talents, glyphs, buffs_object, race_object, settings=settings_object, level=level) 68 | 69 | 70 | if __name__ == '__main__': 71 | json_string = """{ 72 | "level": 85, 73 | "stats": { 74 | "str": 20, 75 | "agi": 4756, 76 | "ap": 190, 77 | "crit": 1022, 78 | "hit": 1329, 79 | "exp": 159, 80 | "haste": 1291, 81 | "mastery": 1713, 82 | "gear_buffs": [ 83 | "rogue_t11_2pc", 84 | "leather_specialization", 85 | "potion_of_the_tolvir", 86 | "chaotic_metagem" 87 | ], 88 | "procs": [ 89 | "heroic_prestors_talisman_of_machination", 90 | "fluid_death", 91 | "rogue_t11_4pc" 92 | ], 93 | "mh": { 94 | "type": "dagger", 95 | "speed": 1.8, 96 | "damage": 939.5, 97 | "enchant": "landslide" 98 | }, 99 | "oh": { 100 | "type": "dagger", 101 | "speed": 1.4, 102 | "damage": 730.5, 103 | "enchant": "landslide" 104 | }, 105 | "ranged": { 106 | "type": "thrown", 107 | "speed": 2.2, 108 | "damage": 1371.5 109 | } 110 | }, 111 | "buffs": [ 112 | "short_term_haste_buff", 113 | "stat_multiplier_buff", 114 | "crit_chance_buff", 115 | "all_damage_buff", 116 | "melee_haste_buff", 117 | "attack_power_buff", 118 | "str_and_agi_buff", 119 | "armor_debuff", 120 | "physical_vulnerability_debuff", 121 | "spell_damage_debuff", 122 | "spell_crit_debuff", 123 | "bleed_damage_debuff", 124 | "agi_flask", 125 | "guild_feast" 126 | ], 127 | "settings": { 128 | "type": "assassination", 129 | "response_time": 1 130 | }, 131 | "talents": [ 132 | "0333230113022110321", 133 | "0020000000000000000", 134 | "2030030000000000000" 135 | ], 136 | "race": "night_elf", 137 | "glyphs": [ 138 | "backstab", 139 | "mutilate", 140 | "rupture" 141 | ] 142 | }""" 143 | 144 | 145 | calculator = from_json(json_string) 146 | # Compute EP values. 147 | ep_values = list(calculator.get_ep().items()) 148 | ep_values.sort(key=lambda entry: entry[1], reverse=True) 149 | max_len = max(len(entry[0]) for entry in ep_values) 150 | for value in ep_values: 151 | print(value[0] + ':' + ' ' * (max_len - len(value[0])), value[1]) 152 | 153 | print('---------') 154 | 155 | # Compute DPS Breakdown. 156 | dps_breakdown = list(calculator.get_dps_breakdown().items()) 157 | dps_breakdown.sort(key=lambda entry: entry[1], reverse=True) 158 | max_len = max(len(entry[0]) for entry in dps_breakdown) 159 | total_dps = sum(entry[1] for entry in dps_breakdown) 160 | for entry in dps_breakdown: 161 | print(entry[0] + ':' + ' ' * (max_len - len(entry[0])), entry[1]) 162 | 163 | print('-' * (max_len + 15)) 164 | 165 | print(' ' * (max_len + 1), total_dps, _("total damage per second.")) 166 | 167 | -------------------------------------------------------------------------------- /shadowcraft/core/locale/SCE.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2010 elitistjerks 3 | # This file is distributed under the same license as the ShadowCraft-Engine package. 4 | # Dazer , 2010. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2011-06-16 16:40+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=CHARSET\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: scripts/assassination.py:137 scripts/combat.py:106 scripts/subtlety.py:107 20 | #: shadowcraft/core/jsoninput.py:162 21 | msgid "total damage per second." 22 | msgstr "" 23 | 24 | #: shadowcraft/calcs/__init__.py:171 25 | msgid "not allowed" 26 | msgstr "" 27 | 28 | #: shadowcraft/calcs/__init__.py:195 29 | msgid "not supported" 30 | msgstr "" 31 | 32 | #: shadowcraft/calcs/__init__.py:217 shadowcraft/calcs/__init__.py:251 33 | msgid "not implemented" 34 | msgstr "" 35 | 36 | #: shadowcraft/calcs/__init__.py:387 37 | msgid "Attacks must be categorized as physical, spell or bleed" 38 | msgstr "" 39 | 40 | #: shadowcraft/calcs/armor_mitigation.py:17 41 | msgid "No armor mitigation parameters available for level {level}" 42 | msgstr "" 43 | 44 | #: shadowcraft/calcs/rogue/__init__.py:71 45 | msgid "No {spell_name} formula available for level {level}" 46 | msgstr "" 47 | 48 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:31 49 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:42 50 | msgid "You must have 31 points in at least one talent tree." 51 | msgstr "" 52 | 53 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:418 54 | msgid "PPMs that also proc off spells are not yet modeled." 55 | msgstr "" 56 | 57 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:470 58 | msgid "" 59 | "Unable to model the 4pc T11 set bonus in a cycle that uses both eviscerate " 60 | "and envenom" 61 | msgstr "" 62 | 63 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:636 64 | msgid "" 65 | "You must specify an assassination cycle to match your assassination spec." 66 | msgstr "" 67 | 68 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:638 69 | msgid "Assassination modeling requires daggers in both hands" 70 | msgstr "" 71 | 72 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:641 73 | msgid "" 74 | "Assassination modeling requires instant poison on one weapon and deadly on " 75 | "the other" 76 | msgstr "" 77 | 78 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:647 79 | msgid "Assassination modeling requires one point in Master Poisoner" 80 | msgstr "" 81 | 82 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:649 83 | msgid "Assassination modeling requires three points in Cut to the Chase" 84 | msgstr "" 85 | 86 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:948 87 | msgid "You must specify a combat cycle to match your combat spec." 88 | msgstr "" 89 | 90 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:951 91 | msgid "Revealing strike usage must be set to always, sometimes, or never" 92 | msgstr "" 93 | 94 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:954 95 | msgid "" 96 | "Cannot specify revealing strike usage in cycle without taking the talent." 97 | msgstr "" 98 | 99 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1179 100 | msgid "You must specify a subtlety cycle to match your subtlety spec." 101 | msgstr "" 102 | 103 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1182 104 | msgid "" 105 | "Subtlety modeling requires a MH dagger if Hemorrhage is not the main combo " 106 | "point builder" 107 | msgstr "" 108 | 109 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1186 110 | msgid "Hemorrhage usage must be set to always, never or a positive number" 111 | msgstr "" 112 | 113 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1188 114 | msgid "Interval between Hemorrhages cannot be higher than the fight duration" 115 | msgstr "" 116 | 117 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1191 118 | msgid "Subtlety modeling currently requires 2 points in Serrated Blades" 119 | msgstr "" 120 | 121 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1286 122 | msgid "" 123 | "Interval between Hemorrhages cannot be lower than {interval} for this gearset" 124 | msgstr "" 125 | 126 | #: shadowcraft/objects/buffs.py:33 127 | msgid "Invalid buff {buff}" 128 | msgstr "" 129 | 130 | #: shadowcraft/objects/buffs.py:52 shadowcraft/objects/stats.py:46 131 | msgid "No conversion factor available for level {level}" 132 | msgstr "" 133 | 134 | #: shadowcraft/objects/procs.py:79 135 | msgid "Weapon speed needed to calculate the proc rate of {proc}" 136 | msgstr "" 137 | 138 | #: shadowcraft/objects/procs.py:91 139 | msgid "Invalid data for proc {proc}" 140 | msgstr "" 141 | 142 | #: shadowcraft/objects/procs.py:102 143 | msgid "No data for proc {proc}" 144 | msgstr "" 145 | 146 | #: shadowcraft/objects/race.py:95 147 | msgid "Unsupported race {race}" 148 | msgstr "" 149 | 150 | #: shadowcraft/objects/race.py:99 151 | msgid "Unsupported class {character_class}" 152 | msgstr "" 153 | 154 | #: shadowcraft/objects/race.py:128 155 | msgid "Unsupported class/level combination {character_class}/{level}" 156 | msgstr "" 157 | 158 | #: shadowcraft/objects/stats.py:134 159 | msgid "Enchant {enchant} is not allowed." 160 | msgstr "" 161 | 162 | #: shadowcraft/objects/stats.py:136 163 | msgid "Only melee weapons can be enchanted with {enchant}." 164 | msgstr "" 165 | 166 | #: shadowcraft/objects/talents.py:26 167 | msgid "Invalid talent name {talent_name}" 168 | msgstr "" 169 | 170 | #: shadowcraft/objects/talents.py:29 171 | msgid "Invalid value {talent_value} for talent {talent_name}" 172 | msgstr "" 173 | 174 | #: shadowcraft/objects/talents.py:39 175 | msgid "Invalid talent string {talent_string}" 176 | msgstr "" 177 | 178 | #: shadowcraft/core/jsoninput.py:37 179 | msgid "Missing settings" 180 | msgstr "" 181 | 182 | #: shadowcraft/core/jsoninput.py:62 183 | msgid "Missing required input {key}" 184 | msgstr "" 185 | -------------------------------------------------------------------------------- /shadowcraft/core/locale/en/LC_MESSAGES/SCE.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShadowCraft/ShadowCraft-Engine/1821fcab201c7ab969c60286d2123c646161f65b/shadowcraft/core/locale/en/LC_MESSAGES/SCE.mo -------------------------------------------------------------------------------- /shadowcraft/core/locale/en/en.po: -------------------------------------------------------------------------------- 1 | # This is the ShadowCraft-Engine English translation 2 | # Copyright (C) 2010 elitistjerks 3 | # This file is distributed under the same license as the ShadowCraft-Engine package. 4 | # Dazer , 2010. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: ShadowCraft-Engine\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2011-06-16 16:40+0200\n" 11 | "PO-Revision-Date: 2011-06-16 16:43+0100\n" 12 | "Last-Translator: Dazer \n" 13 | "Language-Team: \n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "X-Poedit-Language: English\n" 18 | "X-Poedit-SourceCharset: utf-8\n" 19 | 20 | #: scripts/assassination.py:137 21 | #: scripts/combat.py:106 22 | #: scripts/subtlety.py:107 23 | #: shadowcraft/core/jsoninput.py:162 24 | msgid "total damage per second." 25 | msgstr "total damage per second (translated test string)." 26 | 27 | #: shadowcraft/calcs/__init__.py:171 28 | msgid "not allowed" 29 | msgstr "not allowed" 30 | 31 | #: shadowcraft/calcs/__init__.py:195 32 | msgid "not supported" 33 | msgstr "not supported" 34 | 35 | #: shadowcraft/calcs/__init__.py:217 36 | #: shadowcraft/calcs/__init__.py:251 37 | msgid "not implemented" 38 | msgstr "not implemented" 39 | 40 | #: shadowcraft/calcs/__init__.py:387 41 | msgid "Attacks must be categorized as physical, spell or bleed" 42 | msgstr "Attacks must be categorized as physical, spell or bleed" 43 | 44 | #: shadowcraft/calcs/armor_mitigation.py:17 45 | msgid "No armor mitigation parameters available for level {level}" 46 | msgstr "No armor mitigation parameters available for level {level}" 47 | 48 | #: shadowcraft/calcs/rogue/__init__.py:71 49 | msgid "No {spell_name} formula available for level {level}" 50 | msgstr "No {spell_name} formula available for level {level}" 51 | 52 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:31 53 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:42 54 | msgid "You must have 31 points in at least one talent tree." 55 | msgstr "You must have 31 points in at least one talent tree." 56 | 57 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:418 58 | msgid "PPMs that also proc off spells are not yet modeled." 59 | msgstr "PPMs that also proc off spells are not yet modeled." 60 | 61 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:470 62 | msgid "Unable to model the 4pc T11 set bonus in a cycle that uses both eviscerate and envenom" 63 | msgstr "Unable to model the 4pc T11 set bonus in a cycle that uses both eviscerate and envenom" 64 | 65 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:636 66 | msgid "You must specify an assassination cycle to match your assassination spec." 67 | msgstr "You must specify an assassination cycle to match your assassination spec." 68 | 69 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:638 70 | msgid "Assassination modeling requires daggers in both hands" 71 | msgstr "Assassination modeling requires daggers in both hands" 72 | 73 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:641 74 | msgid "Assassination modeling requires instant poison on one weapon and deadly on the other" 75 | msgstr "Assassination modeling requires instant poison on one weapon and deadly on the other" 76 | 77 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:647 78 | msgid "Assassination modeling requires one point in Master Poisoner" 79 | msgstr "Assassination modeling requires one point in Master Poisoner" 80 | 81 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:649 82 | msgid "Assassination modeling requires three points in Cut to the Chase" 83 | msgstr "Assassination modeling requires three points in Cut to the Chase" 84 | 85 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:948 86 | msgid "You must specify a combat cycle to match your combat spec." 87 | msgstr "You must specify a combat cycle to match your combat spec." 88 | 89 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:951 90 | msgid "Revealing strike usage must be set to always, sometimes, or never" 91 | msgstr "Revealing strike usage must be set to always, sometimes, or never" 92 | 93 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:954 94 | msgid "Cannot specify revealing strike usage in cycle without taking the talent." 95 | msgstr "Cannot specify revealing strike usage in cycle without taking the talent." 96 | 97 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1179 98 | msgid "You must specify a subtlety cycle to match your subtlety spec." 99 | msgstr "You must specify a subtlety cycle to match your subtlety spec." 100 | 101 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1182 102 | msgid "Subtlety modeling requires a MH dagger if Hemorrhage is not the main combo point builder" 103 | msgstr "Subtlety modeling requires a MH dagger if Hemorrhage is not the main combo point builder" 104 | 105 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1186 106 | msgid "Hemorrhage usage must be set to always, never or a positive number" 107 | msgstr "Hemorrhage usage must be set to always, never or a positive number" 108 | 109 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1188 110 | msgid "Interval between Hemorrhages cannot be higher than the fight duration" 111 | msgstr "Interval between Hemorrhages cannot be higher than the fight duration" 112 | 113 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1191 114 | msgid "Subtlety modeling currently requires 2 points in Serrated Blades" 115 | msgstr "Subtlety modeling currently requires 2 points in Serrated Blades" 116 | 117 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1286 118 | msgid "Interval between Hemorrhages cannot be lower than {interval} for this gearset" 119 | msgstr "Interval between Hemorrhages cannot be lower than {interval} for this gearset" 120 | 121 | #: shadowcraft/objects/buffs.py:33 122 | msgid "Invalid buff {buff}" 123 | msgstr "Invalid buff {buff}" 124 | 125 | #: shadowcraft/objects/buffs.py:52 126 | #: shadowcraft/objects/stats.py:46 127 | msgid "No conversion factor available for level {level}" 128 | msgstr "No conversion factor available for level {level}" 129 | 130 | #: shadowcraft/objects/procs.py:79 131 | msgid "Weapon speed needed to calculate the proc rate of {proc}" 132 | msgstr "Weapon speed needed to calculate the proc rate of {proc}" 133 | 134 | #: shadowcraft/objects/procs.py:91 135 | msgid "Invalid data for proc {proc}" 136 | msgstr "Invalid data for proc {proc}" 137 | 138 | #: shadowcraft/objects/procs.py:102 139 | msgid "No data for proc {proc}" 140 | msgstr "No data for proc {proc}" 141 | 142 | #: shadowcraft/objects/race.py:95 143 | msgid "Unsupported race {race}" 144 | msgstr "Unsupported race {race}" 145 | 146 | #: shadowcraft/objects/race.py:99 147 | msgid "Unsupported class {character_class}" 148 | msgstr "Unsupported class {character_class}" 149 | 150 | #: shadowcraft/objects/race.py:128 151 | msgid "Unsupported class/level combination {character_class}/{level}" 152 | msgstr "Unsupported class/level combination {character_class}/{level}" 153 | 154 | #: shadowcraft/objects/stats.py:134 155 | msgid "Enchant {enchant} is not allowed." 156 | msgstr "Enchant {enchant} is not allowed." 157 | 158 | #: shadowcraft/objects/stats.py:136 159 | msgid "Only melee weapons can be enchanted with {enchant}." 160 | msgstr "Only melee weapons can be enchanted with {enchant}." 161 | 162 | #: shadowcraft/objects/talents.py:26 163 | msgid "Invalid talent name {talent_name}" 164 | msgstr "Invalid talent name {talent_name}" 165 | 166 | #: shadowcraft/objects/talents.py:29 167 | msgid "Invalid value {talent_value} for talent {talent_name}" 168 | msgstr "Invalid value {talent_value} for talent {talent_name}" 169 | 170 | #: shadowcraft/objects/talents.py:39 171 | msgid "Invalid talent string {talent_string}" 172 | msgstr "Invalid talent string {talent_string}" 173 | 174 | #: shadowcraft/core/jsoninput.py:37 175 | msgid "Missing settings" 176 | msgstr "Missing settings" 177 | 178 | #: shadowcraft/core/jsoninput.py:62 179 | msgid "Missing required input {key}" 180 | msgstr "Missing required input {key}" 181 | 182 | #~ msgid "" 183 | #~ "Attacks cannot benefit from more than one type of raid damage multiplier" 184 | #~ msgstr "" 185 | #~ "Attacks cannot benefit from more than one type of raid damage multiplier" 186 | 187 | #~ msgid "Subtlety modeling currently requires a MH dagger" 188 | #~ msgstr "Subtlety modeling currently requires a MH dagger" 189 | 190 | #~ msgid "Total number of talentpoints has to be 41 or less" 191 | #~ msgstr "Total number of talentpoints has to be 41 or less" 192 | -------------------------------------------------------------------------------- /shadowcraft/core/locale/es_ES/LC_MESSAGES/SCE.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShadowCraft/ShadowCraft-Engine/1821fcab201c7ab969c60286d2123c646161f65b/shadowcraft/core/locale/es_ES/LC_MESSAGES/SCE.mo -------------------------------------------------------------------------------- /shadowcraft/core/locale/es_ES/es_ES.po: -------------------------------------------------------------------------------- 1 | # Ésta es la traducción española de ShadowCraft-Engine. 2 | # Copyright (C) 2010 elitistjerks 3 | # This file is distributed under the same license as the ShadowCraft-Engine package. 4 | # Dazer , 2010. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: ShadowCraft-Engine\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2011-06-16 16:40+0200\n" 11 | "PO-Revision-Date: 2011-06-16 17:23+0100\n" 12 | "Last-Translator: Dazer \n" 13 | "Language-Team: \n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "X-Poedit-Language: Spanish\n" 18 | "X-Poedit-Country: SPAIN\n" 19 | "X-Poedit-SourceCharset: utf-8\n" 20 | 21 | #: scripts/assassination.py:137 22 | #: scripts/combat.py:106 23 | #: scripts/subtlety.py:107 24 | #: shadowcraft/core/jsoninput.py:162 25 | msgid "total damage per second." 26 | msgstr "daño total por segundo." 27 | 28 | #: shadowcraft/calcs/__init__.py:171 29 | msgid "not allowed" 30 | msgstr "no permitido" 31 | 32 | #: shadowcraft/calcs/__init__.py:195 33 | msgid "not supported" 34 | msgstr "sin soporte" 35 | 36 | #: shadowcraft/calcs/__init__.py:217 37 | #: shadowcraft/calcs/__init__.py:251 38 | msgid "not implemented" 39 | msgstr "no implementado" 40 | 41 | #: shadowcraft/calcs/__init__.py:387 42 | msgid "Attacks must be categorized as physical, spell or bleed" 43 | msgstr "Los ataques se han de categorizar como físico, hechizo o sangrado" 44 | 45 | #: shadowcraft/calcs/armor_mitigation.py:17 46 | msgid "No armor mitigation parameters available for level {level}" 47 | msgstr "No hay parámetro de mitigación de armadura disponible para nivel {level}" 48 | 49 | #: shadowcraft/calcs/rogue/__init__.py:71 50 | msgid "No {spell_name} formula available for level {level}" 51 | msgstr "No hay fórmula disponible para la habilidad {spell_name} a nivel {level}" 52 | 53 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:31 54 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:42 55 | msgid "You must have 31 points in at least one talent tree." 56 | msgstr "Debes tener 31 puntos en al menos una rama de talentos." 57 | 58 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:418 59 | msgid "PPMs that also proc off spells are not yet modeled." 60 | msgstr "Los efectos de proc por minuto que se activan mediante hechizos aún no están modelados" 61 | 62 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:470 63 | msgid "Unable to model the 4pc T11 set bonus in a cycle that uses both eviscerate and envenom" 64 | msgstr "No se puede modelar 4pT11 en un ciclo que usa tanto Eviscerar como Envenenar." 65 | 66 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:636 67 | msgid "You must specify an assassination cycle to match your assassination spec." 68 | msgstr "Debes especificar un ciclo de asesinato que concuerde con tu especialización de asesinato." 69 | 70 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:638 71 | msgid "Assassination modeling requires daggers in both hands" 72 | msgstr "El modelo de asesinato requiere dagas en ambas manos." 73 | 74 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:641 75 | msgid "Assassination modeling requires instant poison on one weapon and deadly on the other" 76 | msgstr "El modelo de asesinato requiere veneno instantáneo en un arma y mortal en la otra." 77 | 78 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:647 79 | msgid "Assassination modeling requires one point in Master Poisoner" 80 | msgstr "El modelo de asesinato requiere un punto en Maestro Envenenador." 81 | 82 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:649 83 | msgid "Assassination modeling requires three points in Cut to the Chase" 84 | msgstr "El modelo de asesinato requiere tres puntos en Acortar." 85 | 86 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:948 87 | msgid "You must specify a combat cycle to match your combat spec." 88 | msgstr "Debes especificar un ciclo de combate que concuerde con tu especialización de combate." 89 | 90 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:951 91 | msgid "Revealing strike usage must be set to always, sometimes, or never" 92 | msgstr "El uso de Golpe Revelador ha de ser Siempre, A Veces o Nunca." 93 | 94 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:954 95 | msgid "Cannot specify revealing strike usage in cycle without taking the talent." 96 | msgstr "No puedes especificar Golpe Revelador en un ciclo sin tener el talento." 97 | 98 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1179 99 | msgid "You must specify a subtlety cycle to match your subtlety spec." 100 | msgstr "Debes especificar un ciclo de sutileza que concuerde con tu especialización de sutileza." 101 | 102 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1182 103 | msgid "Subtlety modeling requires a MH dagger if Hemorrhage is not the main combo point builder" 104 | msgstr "El modelo de sutilieza requiere una daga en mano derecha si Hemorragia no es el movimiento de combo principal." 105 | 106 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1186 107 | msgid "Hemorrhage usage must be set to always, never or a positive number" 108 | msgstr "El uso de Hemorragia ha de ser siempre, nunca o un número positivo." 109 | 110 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1188 111 | msgid "Interval between Hemorrhages cannot be higher than the fight duration" 112 | msgstr "El intervalo entre Hemorragias no puede ser mayor que la duración del combate." 113 | 114 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1191 115 | msgid "Subtlety modeling currently requires 2 points in Serrated Blades" 116 | msgstr "El modelo de sutileza requiere dos puntos en Espadas Dentadas" 117 | 118 | #: shadowcraft/calcs/rogue/Aldriana/__init__.py:1286 119 | msgid "Interval between Hemorrhages cannot be lower than {interval} for this gearset" 120 | msgstr "El intervalo entre Hemorragias no puede ser menor que {interval} para éste conjunto de equipo" 121 | 122 | #: shadowcraft/objects/buffs.py:33 123 | msgid "Invalid buff {buff}" 124 | msgstr "{buff} no es un bufo válido." 125 | 126 | #: shadowcraft/objects/buffs.py:52 127 | #: shadowcraft/objects/stats.py:46 128 | msgid "No conversion factor available for level {level}" 129 | msgstr "No hay factor de conversión disponible para nivel {level}" 130 | 131 | #: shadowcraft/objects/procs.py:79 132 | msgid "Weapon speed needed to calculate the proc rate of {proc}" 133 | msgstr "Se necesita la velocidad del arma para calcular el proc rate de {proc}" 134 | 135 | #: shadowcraft/objects/procs.py:91 136 | msgid "Invalid data for proc {proc}" 137 | msgstr "Los datos para el proc {proc} son inválidos" 138 | 139 | #: shadowcraft/objects/procs.py:102 140 | msgid "No data for proc {proc}" 141 | msgstr "No hay datos para el proc {proc}" 142 | 143 | #: shadowcraft/objects/race.py:95 144 | msgid "Unsupported race {race}" 145 | msgstr "La raza {race} no se admite." 146 | 147 | #: shadowcraft/objects/race.py:99 148 | msgid "Unsupported class {character_class}" 149 | msgstr "La clase {character_class} no se admite." 150 | 151 | #: shadowcraft/objects/race.py:128 152 | msgid "Unsupported class/level combination {character_class}/{level}" 153 | msgstr "La combinación de clase: {character_class} / nivel: {level} no se admite." 154 | 155 | #: shadowcraft/objects/stats.py:134 156 | msgid "Enchant {enchant} is not allowed." 157 | msgstr "El encantamiento {enchant} no está permitido" 158 | 159 | #: shadowcraft/objects/stats.py:136 160 | msgid "Only melee weapons can be enchanted with {enchant}." 161 | msgstr "Sólo las armas cuerpo a cuerpo se pueden encantar con {enchant}." 162 | 163 | #: shadowcraft/objects/talents.py:26 164 | msgid "Invalid talent name {talent_name}" 165 | msgstr "{talent_name} no es un nombre de talento válido." 166 | 167 | #: shadowcraft/objects/talents.py:29 168 | msgid "Invalid value {talent_value} for talent {talent_name}" 169 | msgstr "El valor{talent_value} no es válido para {talent_name}" 170 | 171 | #: shadowcraft/objects/talents.py:39 172 | msgid "Invalid talent string {talent_string}" 173 | msgstr "{talent_string} no corresponde a ningún talento válido." 174 | 175 | #: shadowcraft/core/jsoninput.py:37 176 | msgid "Missing settings" 177 | msgstr "Falta configuración" 178 | 179 | #: shadowcraft/core/jsoninput.py:62 180 | msgid "Missing required input {key}" 181 | msgstr "Falta {key} entre los parámetros requeridos" 182 | 183 | #~ msgid "" 184 | #~ "Attacks cannot benefit from more than one type of raid damage multiplier" 185 | #~ msgstr "" 186 | #~ "Los ataques no se pueden beneficiar de más de un multiplicador de daño de " 187 | #~ "banda." 188 | 189 | #~ msgid "Subtlety modeling currently requires a MH dagger" 190 | #~ msgstr "El modelo de sutileza requiere una daga en la mano derecha." 191 | 192 | #~ msgid "Total number of talentpoints has to be 41 or less" 193 | #~ msgstr "El número de puntos de talentos ha de ser igual a 41 o menor" 194 | -------------------------------------------------------------------------------- /shadowcraft/objects/__init__.py: -------------------------------------------------------------------------------- 1 | from future import standard_library 2 | standard_library.install_aliases() 3 | import gettext 4 | import builtins 5 | 6 | _ = gettext.gettext 7 | -------------------------------------------------------------------------------- /shadowcraft/objects/artifact.py: -------------------------------------------------------------------------------- 1 | from builtins import range 2 | from builtins import object 3 | from shadowcraft.core import exceptions 4 | from shadowcraft.objects import artifact_data 5 | 6 | class InvalidTraitException(exceptions.InvalidInputException): 7 | pass 8 | 9 | class Artifact(object): 10 | def __init__(self, class_spec, game_class, trait_string='', trait_dict= {}): 11 | self.allowed_traits = artifact_data.traits[(game_class, class_spec)]+artifact_data.traits[('all','netherlight')] 12 | self.single_rank_traits = artifact_data.single_rank[(game_class, class_spec)] 13 | 14 | if trait_string: 15 | self.initialize_traits(trait_string) 16 | 17 | else: 18 | self.traits = {} 19 | for trait in self.allowed_traits: 20 | if trait in trait_dict: 21 | self.traits[trait] = trait_dict[trait] 22 | else: 23 | self.traits[trait] = 0 24 | 25 | 26 | def __getattr__(self, attr): 27 | if attr in self.traits: 28 | return self.traits[attr] 29 | return False 30 | 31 | def set_trait(self, trait, value): 32 | if trait not in self.allowed_traits: 33 | raise InvalidTraitException(_('Invalid trait name {trait}').format(trait=trait)) 34 | self.traits[trait] = value 35 | 36 | def initialize_traits(self, trait_string): 37 | if len(trait_string) != len(self.allowed_traits) and len(trait_string) != len(self.allowed_traits) + 1: 38 | raise InvalidTraitException(_('Trait strings must be {traits} (or {traits} + 1) characters long').format(traits=len(self.allowed_traits))) 39 | self.traits = {} 40 | for trait in range(len(self.allowed_traits)): 41 | #grab all charcters for final trait 42 | if trait == len(self.allowed_traits) - 1: 43 | self.set_trait(self.allowed_traits[trait], int(trait_string[trait:])) 44 | else: 45 | self.set_trait(self.allowed_traits[trait], int(trait_string[trait])) 46 | 47 | def get_trait_list(self): 48 | return list(self.allowed_traits) 49 | 50 | def get_single_rank_trait_list(self): 51 | return list(self.single_rank_traits) 52 | -------------------------------------------------------------------------------- /shadowcraft/objects/artifact_data.py: -------------------------------------------------------------------------------- 1 | traits = { 2 | ('rogue', 'assassination'): ( 3 | 'kingsbane', 4 | 'assassins_blades', 5 | 'toxic_blades', 6 | 'poison_knives', 7 | 'urge_to_kill', 8 | 'balanced_blades', 9 | 'surge_of_toxins', 10 | 'shadow_walker', 11 | 'master_assassin', 12 | 'shadow_swiftness', 13 | 'serrated_edge', 14 | 'bag_of_tricks', 15 | 'master_alchemist', 16 | 'gushing_wounds', 17 | 'fade_into_shadows', 18 | 'from_the_shadows', 19 | 'blood_of_the_assassinated', 20 | 'slayers_precision', 21 | 'silence_of_the_uncrowned', 22 | 'strangler', 23 | 'dense_concoction', 24 | 'sinister_circulation', 25 | 'concordance_of_the_legionfall', 26 | ), 27 | ('rogue', 'outlaw'): ( 28 | 'curse_of_the_dreadblades', 29 | 'cursed_edges', 30 | 'fates_thirst', 31 | 'blade_dancer', 32 | 'fatebringer', 33 | 'gunslinger', 34 | 'hidden_blade', 35 | 'fortune_strikes', 36 | 'ghostly_shell', 37 | 'deception', 38 | 'black_powder', 39 | 'greed', 40 | 'blurred_time', 41 | 'fortunes_boon', 42 | 'fortunes_strike', 43 | 'blademaster', 44 | 'blunderbuss', 45 | 'cursed_steel', 46 | 'bravado_of_the_uncrowned', 47 | 'sabermetrics', 48 | 'dreadblades_vigor', 49 | 'loaded_dice', 50 | 'concordance_of_the_legionfall', 51 | ), 52 | ('rogue', 'subtlety'): ( 53 | 'goremaws_bite', 54 | 'shadow_fangs', 55 | 'gutripper', 56 | 'fortunes_bite', 57 | 'catlike_reflexes', 58 | 'embrace_of_darkness', 59 | 'ghost_armor', 60 | 'precision_strike', 61 | 'energetic_stabbing', 62 | 'flickering_shadows', 63 | 'second_shuriken', 64 | 'demons_kiss', 65 | 'finality', 66 | 'the_quiet_knife', 67 | 'akarris_soul', 68 | 'soul_shadows', 69 | 'shadow_nova', 70 | 'legionblade', 71 | 'shadows_of_the_uncrowned', 72 | 'weak_point', 73 | 'shadows_whisper', 74 | 'feeding_frenzy', 75 | 'concordance_of_the_legionfall', 76 | ), 77 | ('all','netherlight'): ( 78 | 'chaotic_darkness', 79 | 'dark_sorrows', 80 | 'infusion_of_light', 81 | 'light_speed', 82 | 'lights_embrace', 83 | 'master_of_shadows', 84 | 'murderous_intent', 85 | 'refractive_shell', 86 | 'secure_in_the_light', 87 | 'shadowbind', 88 | 'shocklight', 89 | 'torment_the_weak', 90 | ) 91 | } 92 | 93 | #Single Rank Traits for each spec 94 | #Used for binary trait ranking 95 | single_rank = { 96 | ('rogue', 'assassination'): ( 97 | 'kingsbane', 98 | 'assassins_blades', 99 | 'urge_to_kill', 100 | 'surge_of_toxins', 101 | 'shadow_swiftness', 102 | 'bag_of_tricks', 103 | 'from_the_shadows', 104 | 'blood_of_the_assassinated', 105 | 'slayers_precision', 106 | 'silence_of_the_uncrowned', 107 | 'dense_concoction', 108 | 'sinister_circulation', 109 | ), 110 | ('rogue', 'outlaw'): ( 111 | 'curse_of_the_dreadblades', 112 | 'cursed_edges', 113 | 'hidden_blade', 114 | 'deception', 115 | 'greed', 116 | 'blurred_time', 117 | 'blademaster', 118 | 'blunderbuss', 119 | 'cursed_steel', 120 | 'bravado_of_the_uncrowned', 121 | 'dreadblades_vigor', 122 | 'loaded_dice', 123 | ), 124 | ('rogue', 'subtlety'): ( 125 | 'goremaws_bite', 126 | 'shadow_fangs', 127 | 'embrace_of_darkness', 128 | 'flickering_shadows', 129 | 'second_shuriken', 130 | 'finality', 131 | 'akarris_soul', 132 | 'shadow_nova', 133 | 'legionblade', 134 | 'shadows_of_the_uncrowned', 135 | 'shadows_whisper', 136 | 'feeding_frenzy', 137 | ), 138 | } 139 | -------------------------------------------------------------------------------- /shadowcraft/objects/buffs.py: -------------------------------------------------------------------------------- 1 | from builtins import object 2 | from shadowcraft.core import exceptions 3 | 4 | import gettext 5 | _ = gettext.gettext 6 | 7 | class InvalidBuffException(exceptions.InvalidInputException): 8 | pass 9 | 10 | class Buffs(object): 11 | 12 | allowed_buffs = frozenset([ 13 | 'short_term_haste_buff', # Heroism/Blood Lust, Time Warp 14 | #'stat_multiplier_buff', # Mark of the Wild, Blessing of Kings, Legacy of the Emperor 15 | #'crit_chance_buff', # Leader of the Pack, Legacy of the White Tiger, Arcane Brillance 16 | #'haste_buff', # Swiftblade's Cunning, Unholy Aura 17 | #'multistrike_buff', # Swiftblade's Cunning, ... 18 | #'attack_power_buff', # Horn of Winter, Trueshot Aura, Battle Shout 19 | #'mastery_buff', # Blessing of Might, Grace of Air 20 | #'stamina_buff', # PW: Fortitude, Blood Pact, Commanding Shout 21 | #'versatility_buff', # 22 | #'spell_power_buff', # Dark Intent, Arcane Brillance 23 | #'armor_debuff', # Sunder, Expose Armor, Faerie Fire 24 | #'physical_vulnerability_debuff', # Brittle Bones, Ebon Plaguebringer, Judgments of the Bold, Colossus Smash 25 | #'spell_damage_debuff', # Master Poisoner, Curse of Elements 26 | #'slow_casting_debuff', 27 | 'mortal_wounds_debuff', 28 | # consumables 29 | 'flask_wod_agi_200', # 30 | 'flask_wod_agi', # 250 31 | 'food_wod_mastery', # 100 32 | 'food_wod_mastery_75', # 75 33 | 'food_wod_mastery_125', # 125 34 | 'food_wod_crit', # 35 | 'food_wod_crit_75', # 36 | 'food_wod_crit_125', # 37 | 'food_wod_haste', # 38 | 'food_wod_haste_75', # 39 | 'food_wod_haste_125', # 40 | 'food_wod_versatility', # 41 | 'food_wod_versatility_75', # 42 | 'food_wod_versatility_125', # 43 | 'food_felmouth_frenzy', # Felmouth frenzy, 2 haste scaling RPPM dealing 0.424 AP in damage 44 | ###LEGION### 45 | 'flask_legion_agi', # Flask of the Seventh Demon 46 | 'food_legion_mastery_225', # Pickeled Stormray 47 | 'food_legion_crit_225', # Salt & Pepper Shank 48 | 'food_legion_haste_225', # Deep-Fried Mossgill 49 | 'food_legion_versatility_225', # Faronaar Fizz 50 | 'food_legion_mastery_300', # Barracude Mrglgagh 51 | 'food_legion_crit_300', # Leybeque Ribs 52 | 'food_legion_haste_300', # Suramar Surf and Turf 53 | 'food_legion_versatility_300', # Koi-Scented Stormray 54 | 'food_legion_mastery_375', # Nightborne Delicacy Platter 55 | 'food_legion_crit_375', # The Hungry Magister 56 | 'food_legion_haste_375', # Azshari Salad 57 | 'food_legion_versatility_375', # Seed-Battered Fish Plate 58 | 'food_legion_damage_1', # Spiced Rib Roast 59 | 'food_legion_damage_2', # Drogbar-Style Salmon 60 | 'food_legion_damage_3', # Fishbrul Special 61 | 'food_legion_feast_400', 62 | 'food_legion_feast_500', 63 | ]) 64 | 65 | buffs_debuffs = frozenset([ 66 | 'short_term_haste_buff', # Heroism/Blood Lust, Time Warp 67 | #'stat_multiplier_buff', # Mark of the Wild, Blessing of Kings, Legacy of the Emperor 68 | #'crit_chance_buff', # Leader of the Pack, Legacy of the White Tiger, Arcane Brillance 69 | #'haste_buff', # Swiftblade's Cunning, Unholy Aura 70 | #'multistrike_buff', # Swiftblade's Cunning, ... 71 | #'attack_power_buff', # Horn of Winter, Trueshot Aura, Battle Shout 72 | #'mastery_buff', # Blessing of Might, Grace of Air 73 | #'spell_power_buff', # Dark Intent, Arcane Brillance 74 | #'versatility_buff', 75 | #'stamina_buff', # PW: Fortitude, Blood Pact, Commanding Shout 76 | #'physical_vulnerability_debuff', # Brittle Bones, Ebon Plaguebringer, Judgments of the Bold, Colossus Smash 77 | #'spell_damage_debuff', # Master Poisoner, Curse of Elements 78 | 'mortal_wounds_debuff', 79 | ]) 80 | 81 | def __init__(self, *args, **kwargs): 82 | for buff in args: 83 | if buff not in self.allowed_buffs: 84 | raise InvalidBuffException(_('Invalid buff {buff}').format(buff=buff)) 85 | setattr(self, buff, True) 86 | 87 | def __getattr__(self, name): 88 | # Any buff we haven't assigned a value to, we don't have. 89 | if name in self.allowed_buffs: 90 | return False 91 | object.__getattribute__(self, name) 92 | 93 | def __setattr__(self, name, value): 94 | object.__setattr__(self, name, value) 95 | 96 | def get_stat_bonuses(self, epicurean=False): 97 | bonuses = { 98 | 'agi': self.buff_agi(epicurean), 99 | 'crit': self.buff_crit(epicurean), 100 | 'haste': self.buff_haste(epicurean), 101 | 'mastery': self.buff_mast(epicurean), 102 | 'versatility': self.buff_versatility(epicurean), 103 | } 104 | return bonuses 105 | 106 | def buff_agi(self, race=False): 107 | bonus_agi = 0 108 | bonus_agi += 200 * self.flask_wod_agi_200 109 | bonus_agi += 250 * self.flask_wod_agi 110 | bonus_agi += 1300 * self.flask_legion_agi 111 | bonus_agi += 400 * self.food_legion_feast_400 * [1, 2][race] 112 | bonus_agi += 500 * self.food_legion_feast_500 * [1, 2][race] 113 | return bonus_agi 114 | 115 | def buff_haste(self, race=False): 116 | bonus_haste = 0 117 | bonus_haste += 125 * self.food_wod_haste_125 * [1, 2][race] 118 | bonus_haste += 100 * self.food_wod_haste * [1, 2][race] 119 | bonus_haste += 75 * self.food_wod_haste_75 * [1, 2][race] 120 | bonus_haste += 225 * self.food_legion_haste_225 * [1, 2][race] 121 | bonus_haste += 300 * self.food_legion_haste_300 * [1, 2][race] 122 | bonus_haste += 375 * self.food_legion_haste_375 * [1, 2][race] 123 | return bonus_haste 124 | 125 | def buff_crit(self, race=False): 126 | bonus_crit = 0 127 | bonus_crit += 125 * self.food_wod_crit_125 * [1, 2][race] 128 | bonus_crit += 100 * self.food_wod_crit * [1, 2][race] 129 | bonus_crit += 75 * self.food_wod_crit_75 * [1, 2][race] 130 | bonus_crit += 225 * self.food_legion_crit_225 * [1, 2][race] 131 | bonus_crit += 300 * self.food_legion_crit_300 * [1, 2][race] 132 | bonus_crit += 375 * self.food_legion_crit_375 * [1, 2][race] 133 | return bonus_crit 134 | 135 | def buff_mast(self, race=False): 136 | bonus_mastery = 0 137 | bonus_mastery += 125 * self.food_wod_mastery_125 * [1, 2][race] 138 | bonus_mastery += 100 * self.food_wod_mastery * [1, 2][race] 139 | bonus_mastery += 75 * self.food_wod_mastery_75 * [1, 2][race] 140 | bonus_mastery += 225 * self.food_legion_mastery_225 * [1, 2][race] 141 | bonus_mastery += 300 * self.food_legion_mastery_300 * [1, 2][race] 142 | bonus_mastery += 375 * self.food_legion_mastery_375 * [1, 2][race] 143 | return bonus_mastery 144 | 145 | def buff_versatility(self, race=False): 146 | bonus_versatility = 0 147 | bonus_versatility += 125 * self.food_wod_versatility_125 * [1, 2][race] 148 | bonus_versatility += 100 * self.food_wod_versatility * [1, 2][race] 149 | bonus_versatility += 75 * self.food_wod_versatility_75 * [1, 2][race] 150 | bonus_versatility += 225 * self.food_legion_versatility_225 * [1, 2][race] 151 | bonus_versatility += 300 * self.food_legion_versatility_300 * [1, 2][race] 152 | bonus_versatility += 375 * self.food_legion_versatility_375 * [1, 2][race] 153 | return bonus_versatility 154 | 155 | def felmouth_food(self): 156 | if self.food_felmouth_frenzy : 157 | return True 158 | return False 159 | 160 | def damage_food(self): 161 | if self.food_legion_damage_1: 162 | return 1 163 | if self.food_legion_damage_2: 164 | return 2 165 | if self.food_legion_damage_3: 166 | return 3 167 | return 0 168 | -------------------------------------------------------------------------------- /shadowcraft/objects/modifiers.py: -------------------------------------------------------------------------------- 1 | from builtins import object 2 | from shadowcraft.core import exceptions 3 | 4 | #ModifierList contains all modifiers needed for dps computation. 5 | #ModifierList is used to compile all modifiers into a single lumped modifier per damage source. 6 | #Typical use case registers all modifiers needed at the beginning of the computation with a 7 | #None value and then updates later. 8 | #Before final damage computation a dict is compiled and used for damage computation 9 | class ModifierList(object): 10 | def __init__(self, sources): 11 | self.sources = sources 12 | self.modifiers = {} 13 | 14 | def register_modifier(self, modifier): 15 | self.modifiers[modifier.name] = modifier 16 | for ability in modifier.ability_list: 17 | if ability not in self.sources: 18 | raise exceptions.InvalidInputException(_('Unknown source {source} in damage modifier {mod}').format(source=ability, mod=modifier.name)) 19 | 20 | def update_modifier_value(self, modifier_name, value): 21 | self.modifiers[modifier_name].value = value 22 | 23 | def compile_modifier_dict(self): 24 | lumped_modifier = {s:1 for s in self.sources} 25 | 26 | # mods for all damage 27 | lumped_modifier['all_damage'] = 1 28 | for mod in list(self.modifiers.values()): 29 | if mod.value is None: 30 | raise exceptions.InvalidInputException(_('Modifier {mod} is uninitialized').format(mod=mod.name)) 31 | if mod.all_damage: 32 | lumped_modifier['all_damage'] *= mod.value 33 | 34 | # mods for damage schools 35 | for mod in list(self.modifiers.values()): 36 | if mod.value is None: 37 | raise exceptions.InvalidInputException(_('Modifier {mod} is uninitialized').format(mod=mod.name)) 38 | if mod.dmg_schools: 39 | for school in mod.dmg_schools: 40 | modname = 'school_' + school 41 | if modname in lumped_modifier: 42 | lumped_modifier[modname] *= mod.value 43 | else: 44 | lumped_modifier[modname] = lumped_modifier['all_damage'] * mod.value 45 | 46 | # mods for source abilities 47 | for mod in list(self.modifiers.values()): 48 | if mod.value is None: 49 | raise exceptions.InvalidInputException(_('Modifier {mod} is uninitialized').format(mod=mod.name)) 50 | for ability in self.sources: 51 | if mod.blacklist: 52 | if ability in mod.ability_list: 53 | continue 54 | else: 55 | lumped_modifier[ability] *= mod.value 56 | elif mod.all_damage or ability in mod.ability_list: 57 | lumped_modifier[ability] *= mod.value 58 | 59 | return lumped_modifier 60 | 61 | #DamageModifier specifies any type of modifier applied to ability damage. 62 | #Each modifier is specified as a value applied to either a whitelist or blacklist of abilities. 63 | #Whitelist is default since it is more compact for most modifiers 64 | #but all damage modifiers can be represented either way 65 | class DamageModifier(object): 66 | def __init__(self, name, value, ability_list, blacklist=False, all_damage=False, dmg_schools=None): 67 | self.name = name 68 | self.value = value 69 | self.ability_list = ability_list 70 | self.blacklist = blacklist 71 | self.all_damage = all_damage 72 | self.dmg_schools = dmg_schools 73 | 74 | if self.all_damage and self.dmg_schools is not None: 75 | raise exceptions.InvalidInputException(_('Modifier {mod} should only specify either all_damage or dmg_schools').format(mod=mod.name)) 76 | 77 | -------------------------------------------------------------------------------- /shadowcraft/objects/priority_list.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from builtins import object 3 | class PriorityList(object): 4 | 5 | def __init__(self, *args): 6 | #for each arg (string), read conditionals and determine checks 7 | for a in args: 8 | print(a) #to implement later 9 | return 10 | 11 | def __getattr__(self, name): 12 | object.__getattribute__(self, name) 13 | -------------------------------------------------------------------------------- /shadowcraft/objects/procs.py: -------------------------------------------------------------------------------- 1 | from builtins import object 2 | from shadowcraft.core import exceptions 3 | from shadowcraft.objects import proc_data 4 | from shadowcraft.objects import class_data 5 | 6 | import sys, traceback 7 | 8 | import gettext 9 | _ = gettext.gettext 10 | 11 | class InvalidProcException(exceptions.InvalidInputException): 12 | pass 13 | 14 | 15 | class Proc(object): 16 | def __init__(self, stat, value, duration, proc_name, max_stacks=1, can_crit=True, stats=None, upgradable=False, scaling=None, 17 | buffs=None, base_value=0, type='rppm', icd=0, proc_rate=1.0, trigger='all_attacks', haste_scales=False, item_level=1, 18 | on_crit=False, on_procced_strikes=True, proc_rate_modifier=1., source='generic', att_spd_scales=False, 19 | ap_coefficient=0., dmg_school=None, crm_scales=False, aoe=False, dot_ticks=1, dot_initial_tick=False): 20 | self.stat = stat 21 | if stats is not None: 22 | self.stats = set(stats) 23 | self.value = value 24 | self.base_value = base_value 25 | self.buffs = buffs 26 | self.can_crit = can_crit 27 | self.duration = duration 28 | self.max_stacks = max_stacks 29 | self.upgradable = upgradable 30 | self.scaling = scaling 31 | self.proc_name = proc_name 32 | self.proc_type = type 33 | self.icd = icd 34 | self.type = type 35 | self.source = source 36 | self.proc_rate = proc_rate 37 | self.trigger = trigger 38 | self.haste_scales = haste_scales 39 | self.att_spd_scales = att_spd_scales 40 | self.item_level = item_level 41 | self.on_crit = on_crit 42 | self.on_procced_strikes = on_procced_strikes 43 | self.proc_rate_modifier = proc_rate_modifier 44 | self.ap_coefficient = ap_coefficient 45 | self.dmg_school = dmg_school 46 | self.crm_scales = crm_scales 47 | self.aoe = aoe 48 | self.dot_ticks = dot_ticks 49 | self.dot_initial_tick = dot_initial_tick 50 | 51 | if self.dmg_school is None and stat in ['physical_damage', 'physical_dot']: 52 | self.dmg_school = 'physical' 53 | 54 | #separate method just to keep the constructor clean 55 | self.update_proc_value() 56 | 57 | def update_proc_value(self): 58 | tools = class_data.Util() 59 | #http://forums.elitistjerks.com/topic/130561-shadowcraft-for-mists-of-pandaria/page-3 60 | #see above for stat value initialization 61 | #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}, 62 | #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. 63 | #the other option is to always handle it deeper into the calc module, but that is coupling object responsibilities and not ideal. 64 | if self.scaling and self.source in ('trinket',): 65 | crm = tools.get_combat_rating_multiplier(self.item_level) if self.crm_scales else 1. # apply combat rating modifier 66 | scaled_value = round(self.scaling * tools.get_random_prop_point(self.item_level) * crm) 67 | if hasattr(self.value,'__iter__'): #handle object value 68 | for e in self.value: 69 | self.value[e] = scaled_value 70 | else: #handle raw value 71 | self.value = scaled_value 72 | 73 | def procs_off_auto_attacks(self): 74 | if self.trigger in ('all_attacks', 'auto_attacks', 'all_spells_and_attacks', 'all_melee_attacks'): 75 | return True 76 | else: 77 | return False 78 | 79 | def procs_off_strikes(self): 80 | if self.trigger in ('all_attacks', 'strikes', 'all_spells_and_attacks', 'all_melee_attacks'): 81 | return True 82 | else: 83 | return False 84 | 85 | def procs_off_harmful_spells(self): 86 | if self.trigger in ('all_spells', 'damaging_spells', 'all_spells_and_attacks'): 87 | return True 88 | else: 89 | return False 90 | 91 | def procs_off_heals(self): 92 | if self.trigger in ('all_spells', 'healing_spells', 'all_spells_and_attacks'): 93 | return True 94 | else: 95 | return False 96 | 97 | def procs_off_periodic_spell_damage(self): 98 | if self.trigger in ('all_periodic_damage', 'periodic_spell_damage'): 99 | return True 100 | else: 101 | return False 102 | 103 | def procs_off_periodic_heals(self): 104 | if self.trigger == 'hots': 105 | return True 106 | else: 107 | return False 108 | 109 | def procs_off_bleeds(self): 110 | if self.trigger in ('all_periodic_damage', 'bleeds'): 111 | return True 112 | else: 113 | return False 114 | 115 | def procs_off_crit_only(self): 116 | if self.on_crit: 117 | return True 118 | else: 119 | return False 120 | 121 | def procs_off_apply_debuff(self): 122 | if self.trigger in ('all_spells_and_attacks', 'all_attacks', 'all_melee_attacks'): 123 | return True 124 | else: 125 | return False 126 | 127 | def procs_off_procced_strikes(self): 128 | if self.on_procced_strikes: 129 | return True 130 | else: 131 | return False 132 | 133 | def get_base_proc_rate_for_spec(self, spec): 134 | proc_rate = self.proc_rate 135 | if hasattr(self.proc_rate,'__iter__'): # list of proc rates by spec 136 | if not spec: 137 | raise InvalidProcException(_('Spec expected for the proc rate of {proc}').format(proc=self.proc_name)) 138 | if not spec in self.proc_rate: 139 | if 'other' in self.proc_rate: 140 | spec = 'other' 141 | else: 142 | raise InvalidProcException(_('Proc rate of {proc} not found for current spec').format(proc=self.proc_name)) 143 | proc_rate = self.proc_rate[spec] 144 | return proc_rate 145 | 146 | def get_rppm_proc_rate(self, haste=1., spec=None): 147 | if not self.haste_scales: #Failsafe to allow passing haste for non-scling procs 148 | haste = 1. 149 | if self.is_real_ppm(): 150 | proc_rate = self.get_base_proc_rate_for_spec(spec) 151 | return haste * proc_rate * self.proc_rate_modifier 152 | raise InvalidProcException(_('Invalid proc handling for proc {proc}').format(proc=self.proc_name)) 153 | 154 | def get_proc_rate(self, speed=None, haste=1.0, spec=None): 155 | proc_rate = self.get_base_proc_rate_for_spec(spec) 156 | if self.is_ppm(): 157 | if speed is None: 158 | raise InvalidProcException(_('Weapon speed needed to calculate the proc rate of {proc}').format(proc=self.proc_name)) 159 | else: 160 | return proc_rate * speed / 60. 161 | elif self.is_real_ppm(): 162 | return haste * proc_rate / 60. 163 | else: 164 | return proc_rate 165 | 166 | def is_ppm(self): 167 | if self.type == 'ppm': 168 | return True 169 | else: 170 | return False 171 | # probably should configure this somehow, but type check is probably enough 172 | raise InvalidProcException(_('Invalid data for proc {proc}').format(proc=self.proc_name)) 173 | 174 | def is_rppm(self): 175 | return self.is_real_ppm() 176 | def is_real_ppm(self): 177 | if self.type == 'rppm': 178 | return True 179 | else: 180 | return False 181 | # probably should configure this somehow, but type check is probably enough 182 | raise InvalidProcException(_('Invalid data for proc {proc}').format(proc=self.proc_name)) 183 | 184 | class ProcsList(object): 185 | allowed_procs = proc_data.allowed_procs 186 | 187 | def __init__(self, *args): 188 | for arg in args: 189 | if not isinstance(arg, (list,tuple)): 190 | arg = (arg,100) 191 | if arg[0] in self.allowed_procs: 192 | proc_data = self.allowed_procs[arg[0]] 193 | proc_data['item_level'] = arg[1] 194 | setattr(self, arg[0], Proc(**proc_data)) 195 | else: 196 | raise InvalidProcException(_('No data for proc {proc}').format(proc=arg[0])) 197 | 198 | def set_proc(self, proc): 199 | setattr(self, proc, Proc(**self.allowed_procs[proc])) 200 | 201 | def del_proc(self, proc): 202 | setattr(self, proc, False) 203 | 204 | def __getattr__(self, proc): 205 | # Any proc we haven't assigned a value to, we don't have. 206 | if proc in self.allowed_procs: 207 | return False 208 | object.__getattribute__(self, proc) 209 | 210 | def get_all_procs_for_stat(self, stat=None): 211 | procs = [] 212 | for proc_name in self.allowed_procs: 213 | proc = getattr(self, proc_name) 214 | if proc: 215 | if stat is None: 216 | procs.append(proc) 217 | elif proc.stat in ('stats', 'highest', 'random') and stat in proc.value: 218 | procs.append(proc) 219 | return procs 220 | 221 | def get_all_damage_procs(self): 222 | procs = [] 223 | for proc_name in self.allowed_procs: 224 | proc = getattr(self, proc_name) 225 | if proc: 226 | if proc.stat in ('spell_damage', 'physical_damage', 'physical_dot', 'spell_dot'): 227 | procs.append(proc) 228 | 229 | return procs 230 | -------------------------------------------------------------------------------- /shadowcraft/objects/race.py: -------------------------------------------------------------------------------- 1 | from builtins import map 2 | from builtins import zip 3 | from builtins import object 4 | from shadowcraft.core import exceptions 5 | 6 | import gettext 7 | _ = gettext.gettext 8 | 9 | class InvalidRaceException(exceptions.InvalidInputException): 10 | pass 11 | 12 | class Race(object): 13 | rogue_base_stats = { 14 | 1: ( 14, 15, 11, 8, 6), 15 | 80: ( 256, 273, 189, 151, 113), 16 | 85: ( 288, 306, 212, 169, 127), 17 | 90: ( 339, 361, 250, 200, 150), 18 | 100:(1206, 1284, 890, 711, 533), 19 | 110:(8481, 9030, 6259, 5000, 0), 20 | } 21 | 22 | #(ap,sp) 23 | blood_fury_bonuses = { 24 | 80: {'ap': 20, 'sp': 20}, 25 | 85: {'ap': 30, 'sp': 30}, 26 | 90: {'ap': 50, 'sp': 50}, 27 | 100:{'ap': 120, 'sp': 120}, 28 | 110:{'ap':2243, 'sp': 2243}, 29 | } 30 | touch_of_the_grave_bonuses = { 31 | 80: {'spell_damage': 200}, 32 | 90: {'spell_damage': 400}, 33 | 100:{'spell_damage': 1000}, 34 | #TODO: CHECK 35 | 110:{'spell_damage': 1000}, 36 | } 37 | 38 | #Arguments are ap, spellpower:fire, and int 39 | #This is the formula according to wowhead, with a probable typo corrected 40 | def calculate_rocket_barrage(self, ap, spfi, int): 41 | return 1 + 0.25 * ap + .429 * spfi + self.level * 2 + int * 0.50193 42 | 43 | racial_stat_offset = { 44 | #str,agi,sta,int,spi 45 | "human": ( 0, 0, 0, 0, 0), 46 | "dwarf": ( 2, -2, 1, -1, 0), 47 | "night_elf": (-4, 4, 0, 0, 0), 48 | "gnome": (-3, 1, -1, 3, 0), 49 | "draenei": ( 1, -3, 2, 0, 0), 50 | "worgen": ( 2, 1, 0, -3, 0), 51 | "pandaren": ( 0, -2, 2, 0, 0), 52 | "orc": ( 3, -3, 1, -1, 0), 53 | "undead": ( 2, -1, 1, -2, 0), 54 | "tauren": ( 2, -2, 2, -2, 0), 55 | "troll": ( 1, 2, 0, -3, 0), 56 | "blood_elf": (-3, 1, 0, 2, 0), 57 | "goblin": (-3, 1, -1, 3, 0), 58 | "nightborne" : ( 0, -1, 1, 3, 0), 59 | "highmountain_tauren": ( 1, -2, 2, -1, 0), 60 | "void_elf": (-3, 1, 0, 2, 0), 61 | "lightforged_draenei": (-2, 1, -1, 2, 0), 62 | "none": ( 0, 0, 0, 0, 0), 63 | } 64 | 65 | allowed_racials = frozenset([ 66 | "heroic_presence", #Draenei (+x to primary stat) 67 | "might_of_the_mountain", #Dwarf (2% crit damage/healing) 68 | "expansive_mind", #Gnome (+5% Max Mana, Energy, Rage, or Runic Power) 69 | "nimble_fingers", #Gnome (1% haste) 70 | "human_spirit", #Human (+X Versatility) 71 | "quickness", #Night Elf 72 | "touch_of_elune", #Night Elf (1% haste at night, 1% crit at day) 73 | "shadowmeld", #Night Elf 74 | "viciousness", #Worgen (1% crit chance) 75 | "blood_fury_physical", #Orc (+x AP for n seconds every 2min) 76 | "blood_fury_spell", #Orc 77 | "endurance", #Tauren (+x stam) 78 | "brawn", #Tauren (+2% crit damage/healing) 79 | "berserking", #Troll (15% haste for 10s every 3min) 80 | "arcane_acuity", #Blood Elf (1% crit chance) 81 | "arcane_torrent", #Blood Elf (20 Runic Power, or 1HoPo, or 3% Mana, x chi, x energy) 82 | "rocket_barrage", #Goblin (x magic damage every n min) 83 | "time_is_money", #Goblin (1% haste) 84 | "epicurean", #Pandaren (doubles food buff) 85 | "touch_of_the_grave", #Undead (shadow damage chance to proc) 86 | "arcane_pulse", #Nightborne: 2*AP AoE Arcane dmg 87 | "magical_affinity", #Nightborne: 1% increased magic damage 88 | "entropic_embrace", #Void Elf: 60s ICD, 33% proc chance, 5% damage/healing for 12s 89 | ]) 90 | 91 | activated_racial_data = { 92 | #Blood fury values are set when level is set 93 | 'blood_fury_physical': {'stat': "ap", 'value': 0, 'duration': 15, 'cooldown': 120}, #level-based ap increase 94 | 'blood_fury_spell': {'stat': "sp", 'value': 0, 'duration': 15, 'cooldown': 120}, #level-based sp increase 95 | 'berserking': {'stat': "haste_multiplier", 'value': 1.15, 'duration': 10, 'cooldown': 180}, #15% haste increase for 10 seconds, 3 minute cd 96 | 'arcane_torrent': {'stat': "energy", 'value': 15, 'duration': 0, 'cooldown': 120}, #gain 15 energy (or 15 runic power or 6% mana), 2 minute cd 97 | 'rocket_barrage': {'stat': "damage", 'value': calculate_rocket_barrage, 'duration': 0, 'cooldown': 120}, #deal formula-based damage, 2 min cd 98 | } 99 | 100 | racials_by_race = { 101 | "human": ["human_spirit"], 102 | "night_elf": ["quickness", "touch_of_elune", "shadowmeld"], 103 | "dwarf": ["stoneform", "might_of_the_mountain"], 104 | "gnome": ["expansive_mind", "nimble_fingers"], #TODO: Expansive Mind (multiplicative? or just +5?) 105 | "draenei": ["heroic_presence"], 106 | "worgen": ["viciousness"], 107 | "orc": ["blood_fury_physical", "blood_fury_spell"], 108 | "undead": ["touch_of_the_grave", "cannibalize"], 109 | "tauren": ["endurance", "brawn"], 110 | "troll": ["berserking"], 111 | "blood_elf": ["arcane_torrent", "arcane_acuity"], 112 | "goblin": ["rocket_barrage", "time_is_money"], 113 | "pandaren": ["epicurean"], 114 | "nightborne" : ["arcane_pulse", "magical_affinity"], 115 | "highmountain_tauren": [], 116 | "void_elf": ["entropic_embrace"], 117 | "lightforged_draenei": [], 118 | "none": [], 119 | } 120 | 121 | #Note this allows invalid class-race combos 122 | def __init__(self, race, character_class="rogue", level=85): 123 | self.character_class = str.lower(character_class) 124 | self.race_name = race 125 | if self.race_name not in list(Race.racial_stat_offset.keys()): 126 | raise InvalidRaceException(_('Unsupported race {race}').format(race=self.race_name)) 127 | if self.character_class == "rogue": 128 | self.stat_set = Race.rogue_base_stats 129 | else: 130 | raise InvalidRaceException(_('Unsupported class {character_class}').format(character_class=self.character_class)) 131 | self.level = level 132 | self.set_racials() 133 | 134 | def set_racials(self): 135 | # Set all racials, so we don't invoke __getattr__ all the time 136 | for race, racials in list(Race.racials_by_race.items()): 137 | for racial in racials: 138 | setattr(self, racial, False) 139 | for racial in Race.racials_by_race[self.race_name]: 140 | setattr(self, racial, True) 141 | setattr(self, "racial_str", self.stats[0]) 142 | setattr(self, "racial_agi", self.stats[1]) 143 | setattr(self, "racial_sta", self.stats[2]) 144 | setattr(self, "racial_int", self.stats[3]) 145 | setattr(self, "racial_spi", self.stats[4]) 146 | 147 | def __setattr__(self, name, value): 148 | object.__setattr__(self, name, value) 149 | if name == 'level': 150 | self._set_constants_for_level() 151 | 152 | def _set_constants_for_level(self): 153 | try: 154 | self.stats = self.stat_set[self.level] 155 | self.activated_racial_data["blood_fury_physical"]["value"] = self.blood_fury_bonuses[self.level]["ap"] 156 | self.activated_racial_data["blood_fury_spell"]["value"] = self.blood_fury_bonuses[self.level]["sp"] 157 | # this merges racial stats with class stats (ie, racial_stat_offset and rogue_base_stats) 158 | self.stats = list(map(sum, list(zip(self.stats, Race.racial_stat_offset[self.race_name])))) 159 | self.set_racials() 160 | except KeyError as e: 161 | raise InvalidRaceException(_('Unsupported class/level combination {character_class}/{level}').format(character_class=self.character_class, level=self.level)) 162 | 163 | def __getattr__(self, name): 164 | # Any racial we haven't assigned a value to, we don't have. 165 | if name in self.allowed_racials: 166 | return False 167 | else: 168 | object.__getattribute__(self, name) 169 | 170 | def get_racial_crit(self, is_day=False): 171 | crit_bonus = 0 172 | if self.viciousness: 173 | crit_bonus = .01 174 | if self.touch_of_elune and is_day: 175 | crit_bonus = .01 176 | if self.arcane_acuity: 177 | crit_bonus = .01 178 | 179 | return crit_bonus 180 | 181 | def get_racial_haste(self, is_day=False): 182 | haste_bonus = 0 183 | if self.time_is_money: 184 | haste_bonus = .01 185 | if self.touch_of_elune and not is_day: 186 | haste_bonus = .01 187 | if self.nimble_fingers: 188 | haste_bonus = .01 189 | 190 | return haste_bonus 191 | 192 | def get_racial_stat_boosts(self): 193 | racial_boosts = [] 194 | #Only the orc racial is a straight stat boost 195 | if getattr(self, "blood_fury_physical"): 196 | racial_boosts += [self.activated_racial_data["blood_fury_physical"], self.activated_racial_data["blood_fury_spell"]] 197 | return racial_boosts 198 | -------------------------------------------------------------------------------- /shadowcraft/objects/stats.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from builtins import object 3 | from shadowcraft.objects import buffs 4 | from shadowcraft.objects import procs 5 | from shadowcraft.objects import proc_data 6 | from shadowcraft.objects import race 7 | from shadowcraft.core import exceptions 8 | 9 | import gettext 10 | _ = gettext.gettext 11 | 12 | class Stats(object): 13 | # For the moment, lets define this as raw stats from gear 14 | # AP is only AP bonuses from gear (as of Legion usually 0) 15 | # Other base stat bonuses are added in get_character_base_stats 16 | # Multipliers are added in get_character_stat_multipliers 17 | 18 | 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} 19 | 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} 20 | 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} 21 | 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} 22 | 23 | def __init__(self, mh, oh, procs, gear_buffs, str=0, agi=0, int=0, stam=0, ap=0, crit=0, haste=0, mastery=0, 24 | versatility=0, level=None): 25 | # This will need to be adjusted if at any point we want to support 26 | # other classes, but this is probably the easiest way to do it for 27 | # the moment. 28 | self.str = str 29 | self.agi = agi 30 | self.int = int 31 | self.stam = stam 32 | self.ap = ap 33 | self.crit = crit 34 | self.haste = haste 35 | self.mastery = mastery 36 | self.versatility = versatility 37 | self.mh = mh 38 | self.oh = oh 39 | self.gear_buffs = gear_buffs 40 | self.level = level 41 | self.procs = procs 42 | 43 | def _set_constants_for_level(self): 44 | try: 45 | self.crit_rating_conversion = self.crit_rating_conversion_values[self.level] 46 | self.haste_rating_conversion = self.haste_rating_conversion_values[self.level] 47 | self.mastery_rating_conversion = self.mastery_rating_conversion_values[self.level] 48 | self.versatility_rating_conversion = self.versatility_rating_conversion_values[self.level] 49 | except KeyError: 50 | raise exceptions.InvalidLevelException(_('No conversion factor available for level {level}').format(level=self.level)) 51 | 52 | def __setattr__(self, name, value): 53 | object.__setattr__(self, name, value) 54 | if name == 'level' and value is not None: 55 | self._set_constants_for_level() 56 | 57 | def get_character_base_stats(self, race, traits=None, buffs=None): 58 | base_stats = { 59 | 'str': self.str + race.racial_str, 60 | 'int': self.int + race.racial_int, 61 | 'agi': self.agi + race.racial_agi, 62 | 'ap': self.ap, 63 | 'crit': self.crit, 64 | 'haste': self.haste, 65 | 'mastery': self.mastery, 66 | 'versatility': self.versatility, 67 | } 68 | if buffs is not None: 69 | buff_bonuses = buffs.get_stat_bonuses(race.epicurean) 70 | for bonus in buff_bonuses: 71 | base_stats[bonus] += buff_bonuses[bonus] 72 | 73 | #netherlight crucible t2 74 | if traits is not None: 75 | insigniaMod = 1.5 if self.gear_buffs.insignia_of_the_grand_army else 1 76 | if traits.light_speed: 77 | base_stats['haste'] += 500 * traits.light_speed * insigniaMod 78 | if traits.master_of_shadows: 79 | base_stats['mastery'] += 500 * traits.master_of_shadows * insigniaMod 80 | 81 | # Other bonuses 82 | if self.gear_buffs.rogue_orderhall_6pc: 83 | base_stats['agi'] += 500 84 | 85 | return base_stats 86 | 87 | def get_character_stat_multipliers(self, race): 88 | # assume rogue for gear spec 89 | stat_multipliers = { 90 | 'str': 1., 91 | 'int': 1., 92 | 'agi': self.gear_buffs.gear_specialization_multiplier(), 93 | 'ap': 1, 94 | 'crit': 1. + (0.02 * race.human_spirit), 95 | 'haste': 1. + (0.02 * race.human_spirit), 96 | 'mastery': 1. + (0.02 * race.human_spirit), 97 | 'versatility': 1. + (0.02 * race.human_spirit), 98 | } 99 | return stat_multipliers 100 | 101 | def get_character_stats(self, race, traits=None, buffs=None): 102 | base = self.get_character_base_stats(race, traits, buffs) 103 | mult = self.get_character_stat_multipliers(race) 104 | stats = { } 105 | for stat in base: 106 | stats[stat] = base[stat] * mult[stat] 107 | return stats 108 | 109 | def get_mastery_from_rating(self, rating=None): 110 | if rating is None: 111 | rating = self.mastery 112 | return 8 + rating / self.mastery_rating_conversion 113 | 114 | def get_crit_from_rating(self, rating=None): 115 | if rating is None: 116 | rating = self.crit 117 | return rating / (100. * self.crit_rating_conversion) 118 | 119 | def get_haste_multiplier_from_rating(self, rating=None): 120 | if rating is None: 121 | rating = self.haste 122 | return 1 + rating / (100. * self.haste_rating_conversion) 123 | 124 | def get_versatility_multiplier_from_rating(self, rating=None): 125 | if rating is None: 126 | rating = self.versatility 127 | return 1. + rating / (100. * self.versatility_rating_conversion) 128 | 129 | class Weapon(object): 130 | allowed_melee_enchants = proc_data.allowed_melee_enchants 131 | 132 | def __init__(self, damage, speed, weapon_type, enchant=None): 133 | self.speed = speed 134 | self.weapon_dps = damage * 1.0 / speed 135 | self.type = weapon_type 136 | if enchant is not None: 137 | self.set_enchant(enchant) 138 | 139 | def __setattr__(self, name, value): 140 | object.__setattr__(self, name, value) 141 | if name == 'type': 142 | self.set_normalization_speed() 143 | 144 | def set_normalization_speed(self): 145 | #if self.type in ['gun', 'bow', 'crossbow']: 146 | # self._normalization_speed = 2.8 147 | #elif self.type in ['2h_sword', '2h_mace', '2h_axe', 'polearm']: 148 | # self._normalization_speed = 3.3 149 | #elif 150 | 151 | # commented out for micro performance's sake 152 | # should be re-enabled if other classes ever make use of Shadowcraft 153 | if self.type == 'dagger': 154 | self._normalization_speed = 1.7 155 | else: 156 | self._normalization_speed = 2.4 157 | 158 | def set_enchant(self, enchant): 159 | if enchant == None: 160 | self.del_enchant() 161 | else: 162 | if self.is_melee(): 163 | if enchant in self.allowed_melee_enchants: 164 | self.del_enchant() 165 | proc = procs.Proc(**self.allowed_melee_enchants[enchant]) 166 | setattr(self, enchant, proc) 167 | else: 168 | raise exceptions.InvalidInputException(_('Enchant {enchant} is not allowed.').format(enchant=enchant)) 169 | else: 170 | raise exceptions.InvalidInputException(_('Only melee weapons can be enchanted with {enchant}.').format(enchant=enchant)) 171 | 172 | def del_enchant(self): 173 | for i in self.allowed_melee_enchants: 174 | if getattr(self, i): 175 | delattr(self, i) 176 | 177 | def __getattr__(self, name): 178 | # Any enchant we haven't assigned a value to, we don't have. 179 | if name in self.allowed_melee_enchants: 180 | return False 181 | object.__getattribute__(self, name) 182 | 183 | def is_melee(self): 184 | return not self.type in frozenset(['gun', 'bow', 'crossbow', 'thrown']) 185 | 186 | def damage(self, ap=0, weapon_speed=None): 187 | if weapon_speed == None: 188 | weapon_speed = self.speed 189 | return weapon_speed * (self.weapon_dps + ap / 3.5) #used to be 14 190 | 191 | def normalized_damage(self, ap=0, weapon_speed=None): 192 | if weapon_speed == None: 193 | weapon_speed = self.speed 194 | return weapon_speed * self.weapon_dps + self._normalization_speed * ap / 3.5 #used to be 14 195 | 196 | # Catch-all for non-proc gear based buffs (static or activated) 197 | class GearBuffs(object): 198 | other_gear_buffs = [ 199 | 'gear_specialization', # Increase stat by 5%, previously leather specialization 200 | 'chaotic_metagem', # Increase critical damage by 3% 201 | 'rogue_pvp_4pc', # 30 Extra Energy 202 | 'rogue_pvp_wod_4pc', # +1s KSpree - Combat, 5CP and 100% crit after vanish - Assassination, 100 versatility after feint 5s- Sub 203 | 'rogue_t14_2pc', # Venom Wound damage by 20%, Sinister Strike by 15%, Backstab by 10% 204 | 'rogue_t14_4pc', # Shadow Blades by +12s 205 | 'rogue_t15_2pc', # Extra CP per finisher (max of 6) 206 | 'rogue_t15_4pc', # Abilities cast during Shadow Blades cost 40% less 207 | 'rogue_t16_2pc', # Stacking energy cost reduction (up to 5, up to 5 stacks) on NEXT RvS cast, Seal Fate Proc, or HAT proc 208 | 'rogue_t16_4pc', # 10% KS damage every time it hits, stacking mastery during vendetta (250 mastery, up to 20 stacks), % chance for backstab to become ambush 209 | 'rogue_t17_2pc', # Mut and Dispatch crits generate 7 energy, RvS has 20% higher chance to generate a CP, generate 60e when casting ShD 210 | '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 211 | 'rogue_t17_4pc_lfr', # 1.1 RPPM, 30% energy generation for 6s 212 | '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 213 | 'rogue_t18_4pc', # Dispatch generates +2cps, AR increased damage by 15%, Evis and Rupture reduce the CD of vanish by 1 seconds per CP 214 | 'rogue_t18_4pc_lfr', # Energy increased by 20, 5% increase in energy regen 215 | 'rogue_t19_2pc', # Mutilate causes 30% bleed over 8 seconds, Nightblades lasts additional 2 seconds per CP 216 | 'rogue_t19_4pc', # 10% envenom damage per bleed, 30% SSk generates additional CP if nightblade up 217 | 'rogue_t20_2pc', # Garrote deals 40% increased damage, Symbols of Death increases your damage done by an additional 10%. 218 | '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. 219 | 'rogue_t21_2pc', # After Envenom, 35% increased crit chance for DP/WP for 6s. Each CP reduces SoD cooldown by 0.2s. 220 | 'rogue_t21_4pc', # DP/WP crits give 2 Energy. BS/GB/SS have a 3% chance to grant Shadow gestures which will refund all CPs on next finisher. 221 | 'jacins_ruse_2pc', # Proc 3000 mastery for 15s, 1 rppm 222 | 'march_of_the_legion_2pc', # Proc 35K damage when fighting demons, 6+Haste RPPM 223 | 'journey_through_time_2pc', # The effect from Chrono Shard now increases your movement speed by 30%, and grants an additional 1000 Haste. 224 | 'kara_empowered_2pc', # 30% increase to paired trinkets 225 | 'rogue_orderhall_6pc', # Agility increased by 500 226 | 'rogue_orderhall_8pc', # Your finishing moves have a chance to increase your Haste by 2000 for 12 sec. 227 | #Legendaries 228 | 'the_dreadlords_deceit', #fok/ssk damage increased by 35% per 2 seconds up to 1 minute 229 | 'duskwalkers_footpads', #Vendetta CD reduced by 1 second for each 65 energy spent 230 | 'thraxis_tricksy_treads', # 231 | 'shadow_satyrs_walk', #3+1/3yd energy refund on ssk 232 | 'insignia_of_ravenholdt', #15% damage as shadow on cp generators 233 | 'zoldyck_family_training_shackles', #Poisons and Bleeds deal 30% additional damage below 30% health 234 | 'greenskins_waterlogged_wristcuffs', # 235 | 'denial_of_the_half_giants', # Finishers extend ShB by 0.3 seconds per cp spent 236 | 'shivarran_symmetry', # 237 | 'mantle_of_the_master_assassin', #100% crit during stealth and for 6 seconds after 238 | 'cinidaria_the_symbiote', #30% additional damage to enemies above 90% health 239 | 'sephuzs_secret', #2% haste 240 | 'the_empty_crown', #Kingsbane generates 40 Energy over 5 sec. 241 | '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. 242 | 'the_curse_of_restlessness', #NYI 243 | 'soul_of_the_shadowblade', #Gain the Vigor talent. 244 | 'insignia_of_the_grand_army', #Increase the effects of Light and Shadow powers granted by the Netherlight Crucible by 50%. 245 | #Other 246 | 'jeweled_signet_of_melandrus', #Increases your autoattack damage by 10%. 247 | 'gnawed_thumb_ring', #Use: Have a nibble, increasing your healing and magic damage done by 5% for 12 sec. (3 Min Cooldown) 248 | ] 249 | 250 | allowed_buffs = frozenset(other_gear_buffs) 251 | 252 | def __init__(self, *args): 253 | for arg in args: 254 | if not isinstance(arg, (list,tuple)): 255 | arg = (arg,0) 256 | if arg[0] in self.allowed_buffs: 257 | setattr(self, arg[0], True) 258 | 259 | 260 | def __getattr__(self, name): 261 | # Any gear buff we haven't assigned a value to, we don't have. 262 | if name in self.allowed_buffs: 263 | return False 264 | object.__getattribute__(self, name) 265 | 266 | def __setattr__(self, name, value): 267 | object.__setattr__(self, name, value) 268 | 269 | def metagem_crit_multiplier(self): 270 | if self.chaotic_metagem: 271 | return 1.03 272 | else: 273 | return 1 274 | 275 | def rogue_pvp_4pc_extra_energy(self): 276 | if self.rogue_pvp_4pc: 277 | return 30 278 | return 0 279 | 280 | def rogue_t14_2pc_damage_bonus(self, spell): 281 | if self.rogue_t14_2pc: 282 | bonus = { 283 | ('assassination', 'vw', 'venomous_wounds'): 0.2, 284 | ('combat', 'ss', 'sinister_strike'): 0.15, 285 | ('subtlety', 'bs', 'backstab'): 0.1 286 | } 287 | for spells in list(bonus.keys()): 288 | if spell in spells: 289 | return 1 + bonus[spells] 290 | return 1 291 | 292 | def rogue_t14_4pc_extra_time(self, is_combat=False): 293 | if is_combat: 294 | return self.rogue_t14_4pc * 6 295 | return self.rogue_t14_4pc * 12 296 | 297 | def rogue_t15_2pc_bonus_cp(self): 298 | if self.rogue_t15_2pc: 299 | return 1 300 | return 0 301 | 302 | def rogue_t15_4pc_reduced_cost(self, uptime=12/180): #This is for Mut calcs 303 | cost_reduction = .15 304 | if self.rogue_t15_4pc: 305 | return 1. - (cost_reduction * uptime) 306 | return 1. 307 | 308 | def rogue_t15_4pc_modifier(self, is_sb=False): #This is for Combat/Sub calcs 309 | if self.rogue_t15_4pc and is_sb: 310 | return .85 # 1 - .15 311 | return 1. 312 | 313 | def gear_specialization_multiplier(self): 314 | if self.gear_specialization: 315 | return 1.05 316 | else: 317 | return 1 318 | -------------------------------------------------------------------------------- /shadowcraft/objects/talents.py: -------------------------------------------------------------------------------- 1 | from builtins import str 2 | from builtins import range 3 | from builtins import object 4 | from shadowcraft.core import exceptions 5 | from shadowcraft.objects import talents_data 6 | 7 | class InvalidTalentException(exceptions.InvalidInputException): 8 | pass 9 | 10 | 11 | class Talents(object): 12 | 13 | def __init__(self, talent_string, class_spec, game_class, level=110): 14 | self.game_class = game_class 15 | self.class_spec = class_spec 16 | self.class_talents = talents_data.talents[(game_class,class_spec)] 17 | self.level = level 18 | self.max_rows = 7 19 | self.allowed_talents = [talent for tier in self.class_talents for talent in tier] 20 | self.allowed_talents_for_level = self.get_allowed_talents_for_level() 21 | self.initialize_talents(talent_string) 22 | 23 | def __setattr__(self, name, value): 24 | object.__setattr__(self, name, value) 25 | 26 | def __getattr__(self, name): 27 | # If someone tries to access a talent not initialized (the talent 28 | # string was shorter than 6) we return False 29 | if name in self.allowed_talents: 30 | return False 31 | object.__getattribute__(self, name) 32 | 33 | def get_allowed_talents_for_level(self): 34 | allowed_talents_for_level = [] 35 | for i in range(self.get_top_tier()): 36 | for talent in self.class_talents[i]: 37 | allowed_talents_for_level.append(talent) 38 | return allowed_talents_for_level 39 | 40 | def is_allowed_talent(self, name, check_level=False): 41 | if check_level: 42 | return name in self.allowed_talents_for_level 43 | else: 44 | return name in self.allowed_talents 45 | 46 | def get_top_tier(self): 47 | levels = (15, 30, 45, 60, 75, 90, 100) 48 | top_tier = 0 49 | for i in levels: 50 | if self.level >= i: 51 | top_tier += 1 52 | return top_tier 53 | 54 | def initialize_talents(self, talent_string): 55 | if len(talent_string) > self.max_rows: 56 | raise InvalidTalentException(_('Talent strings must be 7 or less characters long')) 57 | j = 0 58 | self.reset_talents() 59 | for i in talent_string: 60 | if int(i) not in list(range(4)): 61 | raise InvalidTalentException(_('Values in the talent string must be 0, 1, 2, 3, or sometimes 4')) 62 | if int(i) == 0 or i == '.': 63 | pass 64 | else: 65 | setattr(self, self.class_talents[j][int(i) - 1], True) 66 | j += 1 67 | 68 | def get_talent_string(self): 69 | talent_str = "" 70 | for row in self.class_talents: 71 | got_talent = False 72 | for index, talent in enumerate(row): 73 | if getattr(self, talent): 74 | got_talent = True 75 | talent_str += str(index + 1) 76 | break 77 | if not got_talent: 78 | talent_str += "0" 79 | return talent_str 80 | 81 | 82 | def reset_talents(self): 83 | for talent in self.allowed_talents: 84 | setattr(self, talent, False) 85 | 86 | def get_tier_for_talent(self, name): 87 | if name not in self.allowed_talents: 88 | return None 89 | tier = 0 90 | for i in range(self.max_rows): 91 | if name in self.class_talents[i]: 92 | return i 93 | 94 | def set_talent(self, name, value=True): 95 | # Clears talents in the tier and sets the new one 96 | if name not in self.allowed_talents: 97 | raise InvalidTalentException("Invalid talent") 98 | for talent in self.class_talents[self.get_tier_for_talent(name)]: 99 | setattr(self, talent, False) 100 | setattr(self, name, value) 101 | 102 | def get_talent(self, name): 103 | return getattr(self, name) 104 | 105 | def get_active_talents(self): 106 | active_talents = [] 107 | for row in self.class_talents: 108 | for talent in row: 109 | if getattr(self, talent): 110 | active_talents.append(talent) 111 | return active_talents 112 | -------------------------------------------------------------------------------- /shadowcraft/objects/talents_data.py: -------------------------------------------------------------------------------- 1 | talents = { 2 | ('death_knight', 'blood'): ( 3 | ('bloodworms', 'heart_strike', 'consume_vitality'), 4 | ('bloody_reprisal', 'bloodbolt', 'ossuary'), 5 | ('rapid_decomposition', 'red_thirst', 'anti-magic_barrier'), 6 | ('rune_tap', 'purgatory', 'mark_of_blood'), 7 | ('tightening_grasp', 'tremble_before_me', 'march_of_the_damned'), 8 | ('will_of_the_necropolis', 'exhume', 'foul_bulwark'), 9 | ('bonestorm', 'blood_mirror', 'blood_beasts') 10 | ), 11 | ('demon_hunter', 'havoc'): ( 12 | ('fel_mastery', 'first_blood', 'blind_fury'), 13 | ('prepared', 'demon_blades', 'master_of_the_glaive'), 14 | ('demon_reborn', 'bloodlet', 'feed_the_demon'), 15 | ('desperate_instincts', 'netherwalk', 'soul_rending'), 16 | ('nemesis', 'chaos_cleave', 'momentum'), 17 | ('improved_chaos_nova', "ill_swallow_your_soul", 'cull_the_weak'), 18 | ('place_holder1', 'place_holder2', 'place_holder3') 19 | ), 20 | ('druid', 'balance'): ( 21 | ('force_of_nature', 'warrior_of_elune', 'starlord'), 22 | ('renewal', 'displacer_beast', 'wild_charge'), 23 | ('feral_affinity', 'guardian_affinity', 'restoration_affinity'), 24 | ('mighty_bash', 'mass_entanglement', 'typhoon'), 25 | ('soul_of_the_forest', 'incarnation_chosen_of_elune', 'stellar_flare'), 26 | ('shooting_starts', 'astral_communion', 'blessing_of_the_ancients'), 27 | ('collapsing_stars', 'stellar_drift', 'natures_balance') 28 | ), 29 | ('hunter', 'beast_mastery'): ( 30 | ('one_with_the_pack', 'way_of_the_cobra', 'dire_stable'), 31 | ('posthaste', 'farstrider', 'dash'), 32 | ('stomp', 'exptic_munitions', 'chimaera_shot'), 33 | ('binding_shot', 'wyvern_sting', 'intimidation'), 34 | ('big_game_hunter', 'bestial_fury', 'blink_strikes'), 35 | ('a_murder_of_crows', 'barrage', 'volley'), 36 | ('stampede', 'killer_cobra', 'aspect_of_the_beast') 37 | ), 38 | ('mage', 'arcane'): ( 39 | ('arcane_familiar', 'presence_of_mind', 'torrent'), 40 | ('shimmer', 'cauterize', 'ice_block'), 41 | ('mirror_image', 'rune_of_power', 'incanters_flow'), 42 | ('supernova', 'charged_up', 'words_of_power'), 43 | ('ice_floes', 'ring_of_frost', 'ice_ward'), 44 | ('nether_tempest', 'unstable_magic', 'erosion'), 45 | ('overpowered', 'quickening', 'arcane_orb') 46 | ), 47 | ('monk', 'windwalker'): ( 48 | ('chi_burst', 'eye_of_the_tiger', 'chi_wave'), 49 | ('chi_torpedo', 'tiger_lust', 'celerity'), 50 | ('energizing_elixer', 'ascension', 'power_strikes'), 51 | ('ring_of_peace', 'dizzying_kicks', 'leg_sweep'), 52 | ('healing_elixirs', 'diffuse_magic', 'dampen_harm'), 53 | ('rushing_jade_wind', 'invoke_xuen_the_white_tiger', 'hit_combo'), 54 | ('chi_orbit', 'spinning_dragon_strike', 'serenity') 55 | ), 56 | ('paladin', 'retribution'): ( 57 | ('execution_sentence', 'turalyons_might', 'consecration'), 58 | ('the_fires_of_justice', 'crusaders_flurry', 'zeal'), 59 | ('fist_of_justice', 'repentance', 'blinding_light'), 60 | ('virtues_blade', 'blade_of_wrath', 'divine_hammer'), 61 | ('judgements_of_the_bold', 'might_of_the_virtue', 'mass_judgement'), 62 | ('blaze_of_light', 'divine_speed', 'eye_for_an_eye'), 63 | ('final_verdict', 'seal_of_light', 'holy_wrath') 64 | ), 65 | ('priest', 'shadow'): ( 66 | ('twist_of_fate', 'fortress_of_the_mind', 'shadow_word_void'), 67 | ('mania', 'body_and_soul', 'masochism'), 68 | ('mind_bomb', 'psychic_voice', 'dominate_mind'), 69 | ('desperate_prayer', 'spectral_guise', 'angelic_bulwark'), 70 | ('twist_of_fate', 'power_infusion', 'divine_insight'), 71 | ('cascade', 'divine_star', 'halo') 72 | ), 73 | ('rogue', 'assassination'): ( 74 | ('master_poisoner', 'elaborate_planning', 'hemorrhage'), 75 | ('nightstalker', 'subterfuge', 'shadow_focus'), 76 | ('deeper_stratagem', 'anticipation', 'vigor'), 77 | ('leeching_poison', 'elusiveness', 'cheat_death'), 78 | ('thuggee', 'prey_on_the_weak', 'internal_bleeding'), 79 | ('toxic_blade', 'alacrity', 'exsanguinate'), 80 | ('venom_rush', 'marked_for_death', 'death_from_above') 81 | ), 82 | ('rogue', 'outlaw'): ( 83 | ('ghostly_strike', 'swordmaster', 'quick_draw'), 84 | ('grappling_hook', 'acrobatic_strikes', 'hit_and_run'), 85 | ('deeper_stratagem', 'anticipation', 'vigor'), 86 | ('iron_stomach', 'elusiveness', 'cheat_death'), 87 | ('parley', 'prey_on_the_weak', 'dirty_tricks'), 88 | ('cannonball_barrage', 'alacrity', 'killing_spree'), 89 | ('slice_and_dice', 'marked_for_death', 'death_from_above') 90 | ), 91 | ('rogue', 'subtlety'): ( 92 | ('master_of_subtlety', 'weaponmaster', 'gloomblade'), 93 | ('nightstalker', 'subterfuge', 'shadow_focus'), 94 | ('deeper_stratagem', 'anticipation', 'vigor'), 95 | ('soothing_darkness', 'elusiveness', 'cheat_death'), 96 | ('strike_from_the_shadows', 'prey_on_the_weak', 'tangled_shadow'), 97 | ('dark_shadow', 'alacrity', 'enveloping_shadows'), 98 | ('master_of_shadows', 'marked_for_death', 'death_from_above') 99 | ), 100 | ('shaman', 'elemental'): ( 101 | ('path_of_flame', 'path_of_elements', 'maelstrom_totem'), 102 | ('gust_of_wind', 'fleet_of_foot', 'wind_rush_totem'), 103 | ('lightening_surge_totem', 'earthgrab_totem', 'voodoo_totem'), 104 | ('elemental_blast', 'ancestral_swiftness', 'echo_of_the_elements'), 105 | ('elemental_fusion', 'sons_of_flame', 'magnitude'), 106 | ('lightning_rod', 'storm_elemental', 'liquid_magma_totem'), 107 | ('ascendance', 'primal_elementalist', 'totemic_fury') 108 | ), 109 | ('affliction', 'warlock'): ( 110 | ('haunt', 'writhe_in_agony', 'drain_soul'), 111 | ('contagion', 'absolute_corruption', 'mana_tap'), 112 | ('soul_leech', 'mortal_coil', 'howl_of_terror'), 113 | ('siphon_life', 'sow_the_seeds', 'soul_harvest'), 114 | ('demonic_circle', 'burning_rush', 'dark_pact'), 115 | ('grimore_of_supremacy', 'grimore_of_service', 'grimore_of_sacrifce'), 116 | ('soul_effigy', 'phantom_singularity', 'demonic_servitude') 117 | ), 118 | ('warrior', 'arms'): ( 119 | ('shockwave', 'storm_bolt', 'sweeping_strikes'), 120 | ('impending_victory', 'bounding_stride', 'die_by_the_sword'), 121 | ('dauntless', 'overpower', 'avatar'), 122 | ('second_wind', 'double_time', 'imposing_roar'), 123 | ('fervor_of_battle', 'rend', 'bladestorm'), 124 | ('heroic_strike', 'mortal_combo', 'titanic_might'), 125 | ('anger_management', 'opportunity_strikes', 'ravager') 126 | ), 127 | } 128 | -------------------------------------------------------------------------------- /style.md: -------------------------------------------------------------------------------- 1 | Style Guideines for ShadowCraft-Engine 2 | ====================================== 3 | 4 | These are the code style guidelines that were defined by Aldriana when the 5 | project started. Although, they may or may not have been heeded consistently 6 | throughout the code base, try to keep them in mind when writing new code. 7 | 8 | 1. Indents are 4 spaces. Tabs are strictly forbidden. 9 | 10 | 2. Avoid trailing whitespace in all cases. And I do mean all cases. 11 | 12 | 3. Line length: Try to keep comments to 80 characters. For general code I'm 13 | not going to enforce a strict limit, but if you're going over 120 characters 14 | or so you should think about whether there's a natural way to break it. If 15 | there's not, that's fine, but if there is, that's better. 16 | 17 | 4. List comprehensions, lambda functions, map(), reduce(), filter(), etc. are 18 | all fine if they're simple and generally aid code clarity. If you're doing 19 | some hairy nested thing, it's probably better to split it up. 20 | 21 | 5. For binary operators (+, -, *, /, %, etc.) put a space around the operator: 22 | 23 | Correct: `a = 1 + 2 * 3` 24 | 25 | Wrong: `a=1+2*3` 26 | 27 | Exception: When assigning a default value for a function parameter, do not 28 | use spaces: 29 | 30 | Correct: `def foo(bar=1):` 31 | 32 | Wrong: `def foo(bar = 1):` 33 | 34 | 6. Imports: With the exception of importing something that's in `__init__`, 35 | import the module, not the class. 36 | 37 | Correct: `from calcs import gylphs` 38 | 39 | Slightly Wrong: `import calcs.glyphs` 40 | 41 | Wrong: `from calcs.glyphs import Glyph` 42 | 43 | Very Wrong: `from calcs.glyph import *` 44 | 45 | Imports should also generally be done in alphabetical order. 46 | 47 | 7. Try to keep module names distinct to the extent that it's possible to do so 48 | and still have them make sense. It helps if you use descriptive module 49 | names. 50 | 51 | 8. Modules names should be lowercase_and_underscores. 52 | 53 | 9. Function names should be lowercase_and_underscores. 54 | 55 | 10. Class names should be CamelCase. 56 | 57 | 11. If a module primarily consists of a single class definition, the module 58 | name and the class name should match. 59 | 60 | 12. Any string where there is even the slightest chance it will be shown to an 61 | external user should use named introspection for variables. This is to 62 | make translation, um, possible. 63 | 64 | Correct: `"%(character_name)s is level %(character_level)d" % {'character_name': name, 'character_level': level}` 65 | 66 | Wrong: `"%s is level %d" % (name, level)` 67 | 68 | Very Wrong: `name + ' is level ' + str(level)` 69 | 70 | To explain: in various languages the sentence syntax may require the 71 | variables to be in a different order. Giving them good descriptive names 72 | lets the translators properly rearrange them as needed to convey the proper 73 | meaning. 74 | 75 | 13. Comments are a good thing. If what you're doing isn't immediately obvious 76 | from a quick readthrough, add a comment to explain it. Recommended practise 77 | are comments without a space after the #. 78 | 79 | In general: please try to write code that's as readable and maintainable as 80 | possible. You only write the code once, but it will be read many many times. 81 | Hence it's worth spending an extra couple of minutes writing it if it saves the 82 | readers even a few seconds in understanding it. As the saying goes: always 83 | write code as though the person who has to maintain it is a dangerous 84 | psychopath that knows where you live. 85 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShadowCraft/ShadowCraft-Engine/1821fcab201c7ab969c60286d2123c646161f65b/tests/__init__.py -------------------------------------------------------------------------------- /tests/calcs_tests/__init__.py: -------------------------------------------------------------------------------- 1 | from shadowcraft import calcs 2 | import unittest 3 | from shadowcraft.core import exceptions 4 | from shadowcraft.objects import buffs 5 | from shadowcraft.objects import race 6 | from shadowcraft.objects import stats 7 | from shadowcraft.objects import procs 8 | from shadowcraft.objects import talents 9 | from shadowcraft.objects import artifact 10 | 11 | class TestDamageCalculator(unittest.TestCase): 12 | def make_calculator(self, buffs_list=[], gear_buffs_list=[], race_name='night_elf', test_spec='outlaw'): 13 | test_buffs = buffs.Buffs(*buffs_list) 14 | test_gear_buffs = stats.GearBuffs(*gear_buffs_list) 15 | test_procs = procs.ProcsList() 16 | test_mh = stats.Weapon(737, 1.8, 'dagger') 17 | test_oh = stats.Weapon(573, 1.4, 'dagger') 18 | test_ranged = stats.Weapon(1104, 2.0, 'thrown') 19 | test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, 20 | agi=20909, 21 | stam=19566, 22 | crit=4402, 23 | haste=5150, 24 | mastery=5999, 25 | versatility=1515) 26 | test_race = race.Race(race_name) 27 | test_talents = talents.Talents('1000000', test_spec, 'rogue', level=110) 28 | test_traits = artifact.Artifact(test_spec, 'rogue', '000000000000000000') 29 | return calcs.DamageCalculator(test_stats, test_talents, test_traits, test_buffs, test_race, 'outlaw') 30 | 31 | def setUp(self): 32 | self.calculator = self.make_calculator() 33 | 34 | def test_melee_hit_chance(self): 35 | pass 36 | 37 | def test_one_hand_melee_hit_chance(self): 38 | self.assertAlmostEqual( 39 | self.calculator.one_hand_melee_hit_chance(dodgeable=False, parryable=False), 1.0) 40 | self.assertAlmostEqual( 41 | self.calculator.one_hand_melee_hit_chance(dodgeable=True, parryable=False), 1.0) 42 | self.assertAlmostEqual( 43 | self.calculator.one_hand_melee_hit_chance(dodgeable=True, parryable=True), 1.0 - 0.03) 44 | self.assertAlmostEqual( 45 | self.calculator.one_hand_melee_hit_chance(dodgeable=False, parryable=True), 1.0 - 0.03) 46 | 47 | def test_dual_wield_mh_hit_chance(self): 48 | self.assertAlmostEqual(self.calculator.dual_wield_mh_hit_chance(dodgeable=False, parryable=False), 1.0 - 0.19) 49 | self.assertAlmostEqual(self.calculator.dual_wield_mh_hit_chance(dodgeable=True, parryable=False), 1.0 - 0.19) 50 | self.assertAlmostEqual(self.calculator.dual_wield_mh_hit_chance(dodgeable=False, parryable=True), 1.0 - 0.19 - 0.03) 51 | self.assertAlmostEqual(self.calculator.dual_wield_mh_hit_chance(dodgeable=True, parryable=True), 1.0 - 0.19 - 0.03) 52 | -------------------------------------------------------------------------------- /tests/calcs_tests/rogue_tests/__init__.py: -------------------------------------------------------------------------------- 1 | from builtins import object 2 | import unittest 3 | from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator 4 | from shadowcraft.core import exceptions 5 | from shadowcraft.objects import buffs as _buffs 6 | from shadowcraft.objects import race as _race 7 | from shadowcraft.objects import stats as _stats 8 | from shadowcraft.objects import procs as _procs 9 | from shadowcraft.objects import talents as _talents 10 | from shadowcraft.objects import artifact as _artifact 11 | from shadowcraft.objects import artifact_data as artifact_data 12 | from shadowcraft.calcs.rogue.Aldriana import settings as _settings 13 | 14 | class RogueDamageCalculatorFactory(object): 15 | def __init__(self, spec, **kwargs): 16 | self.class_name = 'rogue' 17 | self.talent_str = '0000000' 18 | self.buffs = _buffs.Buffs('short_term_haste_buff', 'flask_wod_agi', 'food_wod_versatility') 19 | self.procs = _procs.ProcsList() 20 | self.gear_buffs = _stats.GearBuffs('gear_specialization') 21 | self.traits = '000000000000000000' 22 | self.level = 110 23 | self.race = 'pandaren' 24 | self.agi = 21122 25 | self.stam = 28367 26 | self.crit = 6306 27 | self.haste = 3260 28 | self.mastery = 3706 29 | self.versatility = 3486 30 | self.response_time = 0.5 31 | self.duration = 360 32 | self.is_demon = False 33 | self.num_boss_adds = 0 34 | self.adv_params = 0 35 | self.finisher_threshold = 5 36 | self.buildSpecDefaults(spec) 37 | self.__dict__.update(kwargs) 38 | 39 | def buildSpecDefaults(self, spec, weapon_dps=2100): 40 | self.spec = spec 41 | if spec == "outlaw": 42 | self.talent_str = '1010022' 43 | self.mh = _stats.Weapon(weapon_dps * 2.6, 2.6, 'sword', None) 44 | self.oh = _stats.Weapon(weapon_dps * 2.6, 2.6, 'sword', None) 45 | self.cycle = _settings.OutlawCycle(blade_flurry=False, 46 | jolly_roger_reroll=1, 47 | grand_melee_reroll=1, 48 | shark_reroll=1, 49 | true_bearing_reroll=1, 50 | buried_treasure_reroll=1, 51 | broadsides_reroll=1, 52 | between_the_eyes_policy='never') 53 | elif spec == "assassination": 54 | self.talent_str = '2101220' 55 | self.mh = _stats.Weapon(weapon_dps * 1.8, 1.8, 'dagger', None) 56 | self.oh = _stats.Weapon(weapon_dps * 1.8, 1.8, 'dagger', None) 57 | self.cycle = _settings.AssassinationCycle() 58 | elif spec == "subtlety": 59 | self.talent_str = '2100120' 60 | self.mh = _stats.Weapon(weapon_dps * 1.8, 1.8, 'dagger', None) 61 | self.oh = _stats.Weapon(weapon_dps * 1.8, 1.8, 'dagger', None) 62 | self.cycle = _settings.SubtletyCycle(cp_builder='backstab', dance_finishers_allowed=True, positional_uptime=0.9) 63 | else: 64 | raise "Invalid spec: %s" % spec 65 | 66 | 67 | def build(self, **kwargs): 68 | self.__dict__.update(kwargs) 69 | 70 | test_race = _race.Race(self.race) 71 | 72 | # Set up a calcs object.. 73 | test_stats = _stats.Stats(self.mh, self.oh, self.procs, 74 | self.gear_buffs, 75 | agi=self.agi, 76 | stam=self.stam, 77 | crit=self.crit, 78 | haste=self.haste, 79 | mastery=self.mastery, 80 | versatility=self.versatility) 81 | 82 | # Initialize talents.. 83 | test_talents = _talents.Talents(self.talent_str, self.spec, self.class_name, level=self.level) 84 | 85 | #initialize artifact traits.. 86 | test_traits = _artifact.Artifact(self.spec, self.class_name, self.traits) 87 | 88 | # Set up settings. 89 | test_settings = _settings.Settings(self.cycle, response_time=self.response_time, duration=self.duration, 90 | adv_params=self.adv_params, is_demon=self.is_demon, num_boss_adds=self.num_boss_adds, finisher_threshold=self.finisher_threshold) 91 | 92 | self.calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_traits, self.buffs, test_race, self.spec, test_settings, self.level) 93 | self.calculator.level = 110 94 | return self.calculator 95 | 96 | class RogueDamageCalculatorTestBase(object): 97 | def compare_dps(self, a, b): 98 | return self.compare(a, b, "get_dps") 99 | 100 | def compare(self, a, b, method=None): 101 | calc_a = self.factory.build(**a) 102 | calc_b = self.factory.build(**b) 103 | if method is not None: 104 | return (getattr(calc_a, method)(), getattr(calc_b, method)()) 105 | else: 106 | return (calc_a, calc_b) 107 | 108 | def test_oh_penalty(self): 109 | self.assertAlmostEqual(self.calculator.oh_penalty(), 0.5) 110 | 111 | def test_crit_damage_modifiers(self): 112 | self.assertAlmostEqual(self.calculator.crit_damage_modifiers(), 1 + (2 * 1. - 1) * 1) 113 | 114 | def test_dps_breakdowns(self): 115 | # TODO: Add assertions. This at least runs it though. 116 | self.calculator.get_dps_breakdown() 117 | 118 | def test_get_talents_ranking(self): 119 | # TODO: Add assertions. This at least runs it though. 120 | self.calculator.get_talents_ranking() 121 | 122 | def test_get_trait_ranking(self): 123 | # TODO: Add assertions. This at least runs it though. 124 | self.calculator.get_trait_ranking() 125 | 126 | def test_ep(self): 127 | ep_values = self.calculator.get_ep() 128 | 129 | self.assertTrue(ep_values['agi'] < 1.5) 130 | self.assertTrue(ep_values['agi'] > 1.0) 131 | self.assertTrue(ep_values['mastery'] < 1.0) 132 | self.assertTrue(ep_values['mastery'] > 0.0) 133 | self.assertTrue(ep_values['haste'] < 1.0) 134 | self.assertTrue(ep_values['haste'] > 0.0) 135 | self.assertTrue(ep_values['versatility'] < 1.0) 136 | self.assertTrue(ep_values['versatility'] > 0.0) 137 | self.assertTrue(ep_values['crit'] < 1.0) 138 | self.assertTrue(ep_values['crit'] > 0.0) 139 | 140 | def test_set_constants_for_level(self): 141 | self.assertRaises(exceptions.InvalidLevelException, self.calculator.__setattr__, 'level', 111) 142 | 143 | 144 | def test_get_talents_ranking_does_not_change_talents(self): 145 | active_talents = self.calculator.talents.get_active_talents() 146 | self.assertEqual(self.calculator.talents.get_active_talents(), active_talents) 147 | 148 | ## Single target 149 | 150 | class TestOutlawRogueDamageCalculator(RogueDamageCalculatorTestBase, unittest.TestCase): 151 | def setUp(self): 152 | self.factory = RogueDamageCalculatorFactory('outlaw') 153 | self.calculator = self.factory.build() 154 | 155 | 156 | # This is a dumb test but illustrates how we can test changes in a calculator 157 | def test_mastery_helps_dps(self): 158 | a, b = self.compare({"mastery": 3706}, {"mastery": 4706}, 'get_dps_breakdown') 159 | self.assertGreater(b["main_gauche"], a["main_gauche"]) 160 | 161 | 162 | def test_energy_regen(self): 163 | self.calculator.set_constants() 164 | # This is 12.4 base because the calculator currently averages Heroism out over the course of the fight. 165 | # Should fix at some point. 166 | self.assertAlmostEqual(self.calculator.get_energy_regen({'haste': 0}), 12.4) 167 | self.assertAlmostEqual(self.calculator.get_energy_regen({'haste': 5000}), 14.3076923) 168 | self.assertAlmostEqual(self.calculator.get_energy_regen({'haste': 5000}, buried=True), 17.8846153846) 169 | self.assertAlmostEqual(self.calculator.get_energy_regen({'haste': 5000}, ar=True), 28.615384615384613) 170 | self.assertAlmostEqual(self.calculator.get_energy_regen({'haste': 5000}, buried=True, ar=True), 35.76923076923077) 171 | self.assertAlmostEqual(self.calculator.get_energy_regen({'haste': 5000}, alacrity_stacks=10), 15.50769230769231) 172 | cycle = _settings.OutlawCycle(blade_flurry=True) 173 | self.assertEqual(self.factory.build(cycle=cycle).set_constants().get_energy_regen({'haste': 0}), 12.4 * 0.8) 174 | 175 | 176 | def test_energy_regen_blade_dancer(self): 177 | cycle = _settings.OutlawCycle(blade_flurry=True) 178 | self.assertEqual(self.factory.build(cycle=cycle,traits='000200000000000000').set_constants().get_energy_regen({'haste': 0}), 12.4 * (0.8 + 0.03333 * 2)) 179 | 180 | 181 | def test_cursed_edges_dps(self): 182 | base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '010000000000000000'}) 183 | self.assertGreater(rank1, base) 184 | rank2 = self.factory.build(traits='020000000000000000', num_boss_adds=3).get_dps() 185 | rank3 = self.factory.build(traits='030000000000000000', num_boss_adds=3).get_dps() 186 | self.assertGreater(rank2, rank1) 187 | self.assertGreater(rank3, rank2) 188 | 189 | 190 | def test_fates_thirst_dps(self): 191 | base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '001000000000000000'}) 192 | self.assertGreater(rank1, base) 193 | rank2 = self.factory.build(traits='002000000000000000', num_boss_adds=3).get_dps() 194 | rank3 = self.factory.build(traits='003000000000000000', num_boss_adds=3).get_dps() 195 | self.assertGreater(rank2, rank1) 196 | self.assertGreater(rank3, rank2) 197 | 198 | 199 | def test_blade_flurry_hurts_single_target_dps(self): 200 | cycle = _settings.OutlawCycle(blade_flurry=True) 201 | base, rank1 = self.compare_dps({}, {'num_boss_adds': 0, 'cycle': cycle}) 202 | self.assertLess(rank1, base) 203 | 204 | 205 | def test_blade_dancer_improves_blade_flurry_penalty(self): 206 | cycle = _settings.OutlawCycle(blade_flurry=True) 207 | base, rank1 = self.compare_dps({'traits': '000000000000000000', 'cycle': cycle}, {'traits': '000100000000000000', 'cycle': cycle}) 208 | self.assertGreater(rank1, base) 209 | rank2 = self.factory.build(traits='000200000000000000').get_dps() 210 | rank3 = self.factory.build(traits='000300000000000000').get_dps() 211 | self.assertGreater(rank2, rank1) 212 | self.assertGreater(rank3, rank2) 213 | 214 | 215 | def test_blade_dancer_multi_target_dps(self): 216 | cycle = _settings.OutlawCycle(blade_flurry=True) 217 | base = self.factory.build(traits='000000000000000000', cycle=cycle).get_dps() 218 | rank1 = self.factory.build(traits='000100000000000000', cycle=cycle, num_boss_adds=3).get_dps() 219 | rank2 = self.factory.build(traits='000200000000000000', cycle=cycle, num_boss_adds=3).get_dps() 220 | rank3 = self.factory.build(traits='000300000000000000', cycle=cycle, num_boss_adds=3).get_dps() 221 | self.assertGreater(rank1, base) 222 | self.assertGreater(rank2, rank1) 223 | self.assertGreater(rank3, rank2) 224 | 225 | 226 | def test_fatebringer_dps(self): 227 | base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000010000000000000'}) 228 | self.assertGreater(rank1, base) 229 | rank2 = self.factory.build(traits='000020000000000000').get_dps() 230 | rank3 = self.factory.build(traits='000030000000000000').get_dps() 231 | self.assertGreater(rank2, rank1) 232 | self.assertGreater(rank3, rank2) 233 | 234 | 235 | def test_gunslinger_dps(self): 236 | base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000001000000000000'}) 237 | self.assertGreater(rank1, base) 238 | 239 | 240 | def test_hidden_blade_dps(self): 241 | base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000100000000000'}) 242 | self.assertGreater(rank1, base) 243 | 244 | 245 | def test_fortune_strikes_dps(self): 246 | base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000010000000000'}) 247 | self.assertGreater(rank1, base) 248 | rank2 = self.factory.build(traits='000000020000000000').get_dps() 249 | rank3 = self.factory.build(traits='000000030000000000').get_dps() 250 | self.assertGreater(rank2, rank1) 251 | self.assertGreater(rank3, rank2) 252 | 253 | 254 | def test_ghostly_shell_dps(self): 255 | base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000001000000000'}) 256 | self.assertEqual(base, rank1) 257 | 258 | 259 | def test_deception_dps(self): 260 | base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000000100000000'}) 261 | self.assertEqual(base, rank1) 262 | 263 | 264 | def test_black_powder_dps(self): 265 | cycle = _settings.OutlawCycle(between_the_eyes_policy='shark') 266 | base, rank1 = self.compare_dps({'traits': '000000000000000000', 'cycle': cycle}, {'traits': '000000000010000000', 'cycle': cycle}) 267 | self.assertGreater(rank1, base) 268 | 269 | 270 | def test_greed_dps(self): 271 | base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000000001000000'}) 272 | self.assertGreater(rank1, base) 273 | 274 | 275 | def test_blurred_time_dps(self): 276 | base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000000000100000'}) 277 | self.assertGreater(rank1, base) 278 | 279 | 280 | def test_fortunes_boon_dps(self): 281 | base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000000000010000'}) 282 | self.assertGreater(rank1, base) 283 | rank2 = self.factory.build(traits='000000000000020000').get_dps() 284 | rank3 = self.factory.build(traits='000000000000030000').get_dps() 285 | self.assertGreater(rank2, rank1) 286 | self.assertGreater(rank3, rank2) 287 | 288 | 289 | def test_fortunes_strike_dps(self): 290 | base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000000000001000'}) 291 | self.assertGreater(rank1, base) 292 | 293 | rank2 = self.factory.build(traits='000000000000002000').get_dps() 294 | rank3 = self.factory.build(traits='000000000000003000').get_dps() 295 | self.assertGreater(rank2, rank1) 296 | self.assertGreater(rank3, rank2) 297 | 298 | 299 | def test_blademaster_dps(self): 300 | base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000000000000100'}) 301 | self.assertEqual(base, rank1) 302 | 303 | 304 | def test_blunderbuss_dps(self): 305 | base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000000000000010'}) 306 | self.assertGreater(rank1, base) 307 | 308 | 309 | def test_cursed_steel_dps(self): 310 | base, rank1 = self.compare_dps({'traits': '000000000000000000'}, {'traits': '000000000000000001'}) 311 | self.assertGreater(rank1, base) 312 | 313 | 314 | # These are sanity checks; they are what is expected from current modeling, so they're included to help 315 | # guard against regressions in the calculator. 316 | def test_best_rank_1(self): 317 | base = self.factory.build(talent_str='0000000').get_dps() 318 | gstrike = self.factory.build(talent_str='1000000').get_dps() 319 | swords = self.factory.build(talent_str='2000000').get_dps() 320 | quick = self.factory.build(talent_str='3000000').get_dps() 321 | self.assertGreater(gstrike, base, 'No Talents %s >= Ghostly Strike %s' % (base, gstrike)) 322 | self.assertGreater(gstrike, swords, 'Swordmaster %s >= Ghostly Strike %s' % (swords, gstrike)) 323 | self.assertGreater(gstrike, quick, 'Quick Draw %s >= Ghostly Strike %s' % (quick, gstrike)) 324 | 325 | 326 | def test_best_rank_3(self): 327 | base = self.factory.build(talent_str='0010000').get_dps() 328 | stratagem = self.factory.build(talent_str='0010000').get_dps() 329 | anticipation = self.factory.build(talent_str='0020000').get_dps() 330 | vigor = self.factory.build(talent_str='0030000').get_dps() 331 | self.assertGreater(stratagem, base, 'No Talents %s >= Stratagem %s' % (base, stratagem)) 332 | self.assertGreater(stratagem, anticipation, 'Anticipation %s >= Stratagem %s' % (anticipation, stratagem)) 333 | self.assertGreater(stratagem, vigor, 'Vigor %s >= Stratagem %s' % (vigor, stratagem)) 334 | 335 | 336 | def test_best_rank_6(self): 337 | base = self.factory.build(talent_str='0000000').get_dps() 338 | cannons = self.factory.build(talent_str='0000010').get_dps() 339 | alacrity = self.factory.build(talent_str='0000020').get_dps() 340 | kspree = self.factory.build(talent_str='0000030').get_dps() 341 | self.assertGreater(alacrity, base, 'No Talents %s >= Alacrity %s' % (base, alacrity)) 342 | self.assertGreater(alacrity, kspree, 'KSpree %s >= Alacrity %s' % (kspree, alacrity)) 343 | self.assertGreater(alacrity, cannons, 'Cannons %s >= Alacrity %s' % (cannons, alacrity)) 344 | 345 | 346 | def test_best_rank_7(self): 347 | base = self.factory.build(talent_str='0000000').get_dps() 348 | snd = self.factory.build(talent_str='0000001').get_dps() 349 | mfd = self.factory.build(talent_str='0000002').get_dps() 350 | dfa = self.factory.build(talent_str='0000003').get_dps() 351 | self.assertGreater(mfd, base, 'No Talents %s >= Marked for Death %s' % (base, mfd)) 352 | self.assertGreater(mfd, snd, 'SnD %s >= Marked for Death %s' % (snd, mfd)) 353 | self.assertGreater(mfd, dfa, 'Death From Above %s >= mfd %s' % (dfa, mfd)) 354 | 355 | def test_get_talents_ranking_does_not_persist_talents_in_same_row(self): 356 | self.calculator.talents.initialize_talents("0000000") 357 | ranking_without_existing = self.calculator.get_talents_ranking() 358 | 359 | self.calculator.talents.initialize_talents("1000000") 360 | ranking_with_existing = self.calculator.get_talents_ranking() 361 | 362 | self.assertEqual(ranking_without_existing[15], ranking_with_existing[15]) 363 | 364 | 365 | class TestAssassinationRogueDamageCalculator(RogueDamageCalculatorTestBase, unittest.TestCase): 366 | def setUp(self): 367 | self.calculator = RogueDamageCalculatorFactory('assassination').build() 368 | 369 | class TestSubtletyRogueDamageCalculator(RogueDamageCalculatorTestBase, unittest.TestCase): 370 | def setUp(self): 371 | self.calculator = RogueDamageCalculatorFactory('subtlety').build() 372 | 373 | ## Multi-target 374 | 375 | class TestAOEOutlawRogueDamageCalculator(RogueDamageCalculatorTestBase, unittest.TestCase): 376 | def setUp(self): 377 | self.calculator = RogueDamageCalculatorFactory('outlaw').build(num_boss_adds=3) 378 | 379 | 380 | class TestAOEAssassinationRogueDamageCalculator(RogueDamageCalculatorTestBase, unittest.TestCase): 381 | def setUp(self): 382 | self.calculator = RogueDamageCalculatorFactory('assassination').build(num_boss_adds=3) 383 | 384 | 385 | class TestAOESubtletyRogueDamageCalculator(RogueDamageCalculatorTestBase, unittest.TestCase): 386 | def setUp(self): 387 | self.calculator = RogueDamageCalculatorFactory('subtlety').build(num_boss_adds=3) -------------------------------------------------------------------------------- /tests/core_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShadowCraft/ShadowCraft-Engine/1821fcab201c7ab969c60286d2123c646161f65b/tests/core_tests/__init__.py -------------------------------------------------------------------------------- /tests/core_tests/exceptions_tests.py: -------------------------------------------------------------------------------- 1 | from builtins import str 2 | import unittest 3 | from shadowcraft.core.exceptions import InvalidInputException 4 | 5 | class TestInvalidInputException(unittest.TestCase): 6 | def test(self): 7 | try: 8 | raise InvalidInputException("test") 9 | except InvalidInputException as e: 10 | self.assertEqual(str(e), "test") 11 | self.assertEqual(e.error_msg, "test") 12 | -------------------------------------------------------------------------------- /tests/objects_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShadowCraft/ShadowCraft-Engine/1821fcab201c7ab969c60286d2123c646161f65b/tests/objects_tests/__init__.py -------------------------------------------------------------------------------- /tests/objects_tests/buffs_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from shadowcraft.core import exceptions 3 | from shadowcraft.objects import buffs 4 | 5 | class TestBuffsTrue(unittest.TestCase): 6 | def setUp(self): 7 | self.buffs = buffs.Buffs(*buffs.Buffs.allowed_buffs) 8 | 9 | def test_exception(self): 10 | self.assertRaises(buffs.InvalidBuffException, buffs.Buffs, 'fake_buff') 11 | 12 | def test__getattr__(self): 13 | self.assertRaises(AttributeError, self.buffs.__getattr__, 'fake_buff') 14 | self.assertTrue(self.buffs.flask_wod_agi_200) 15 | 16 | def test_buff_agi(self): 17 | self.assertEqual(self.buffs.buff_agi(), 2100) 18 | 19 | 20 | class TestBuffsFalse(unittest.TestCase): 21 | def setUp(self): 22 | self.buffs = buffs.Buffs() 23 | 24 | def test__getattr__(self): 25 | self.assertRaises(AttributeError, self.buffs.__getattr__, 'fake_buff') 26 | self.assertFalse(self.buffs.flask_legion_agi) 27 | 28 | def test_flask_legion_agi(self): 29 | self.assertEqual(self.buffs.flask_legion_agi, False) 30 | 31 | def test_food_legion_mastery_225(self): 32 | self.assertEqual(self.buffs.food_legion_mastery_225, False) 33 | 34 | def test_food_legion_crit_225(self): 35 | self.assertEqual(self.buffs.food_legion_crit_225, False) 36 | 37 | def test_food_legion_haste_225(self): 38 | self.assertEqual(self.buffs.food_legion_haste_225, False) 39 | 40 | def test_food_legion_versatility_225(self): 41 | self.assertEqual(self.buffs.food_legion_versatility_225, False) 42 | 43 | def test_food_legion_mastery_300(self): 44 | self.assertEqual(self.buffs.food_legion_mastery_300, False) 45 | 46 | def test_food_legion_crit_300(self): 47 | self.assertEqual(self.buffs.food_legion_crit_300, False) 48 | 49 | def test_food_legion_haste_300(self): 50 | self.assertEqual(self.buffs.food_legion_haste_300, False) 51 | 52 | def test_food_legion_versatility_300(self): 53 | self.assertEqual(self.buffs.food_legion_versatility_300, False) 54 | 55 | def test_food_legion_mastery_375(self): 56 | self.assertEqual(self.buffs.food_legion_mastery_375, False) 57 | 58 | def test_food_legion_crit_375(self): 59 | self.assertEqual(self.buffs.food_legion_crit_375, False) 60 | 61 | def test_food_legion_haste_375(self): 62 | self.assertEqual(self.buffs.food_legion_haste_375, False) 63 | 64 | def test_food_legion_versatility_375(self): 65 | self.assertEqual(self.buffs.food_legion_versatility_375, False) 66 | 67 | def test_food_legion_damage_1(self): 68 | self.assertEqual(self.buffs.food_legion_damage_1, False) 69 | 70 | def test_food_legion_damage_2(self): 71 | self.assertEqual(self.buffs.food_legion_damage_2, False) 72 | 73 | def test_food_legion_damage_3(self): 74 | self.assertEqual(self.buffs.food_legion_damage_3, False) 75 | 76 | def test_food_legion_feast_150(self): 77 | self.assertEqual(self.buffs.food_legion_feast_150, False) 78 | 79 | def test_food_legion_feast_200(self): 80 | self.assertEqual(self.buffs.food_legion_feast_200, False) -------------------------------------------------------------------------------- /tests/objects_tests/procs_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from shadowcraft.objects import procs 3 | 4 | class TestProcsList(unittest.TestCase): 5 | def setUp(self): 6 | self.procsList = procs.ProcsList('fury_of_xuen','legendary_capacitive_meta') 7 | 8 | def test__init__(self): 9 | self.assertRaises(procs.InvalidProcException, procs.ProcsList, 'fake_proc') 10 | self.procsList = procs.ProcsList('fury_of_xuen') 11 | self.assertEqual(len(self.procsList.get_all_procs_for_stat(stat=None)), 1) 12 | 13 | def test__getattr__(self): 14 | self.assertRaises(AttributeError, self.procsList.__getattr__, 'fake_proc') 15 | self.assertTrue(self.procsList.fury_of_xuen) 16 | self.assertFalse(self.procsList.touch_of_the_grave) 17 | 18 | def test_get_all_procs_for_stat(self): 19 | self.assertEqual(len(self.procsList.get_all_procs_for_stat(stat=None)), 2) 20 | self.procsList = procs.ProcsList() 21 | self.assertEqual(len(self.procsList.get_all_procs_for_stat(stat=None)), 0) 22 | 23 | def test_get_all_damage_procs(self): 24 | self.assertEqual(len(self.procsList.get_all_damage_procs()), 2) 25 | self.procsList = procs.ProcsList() 26 | self.assertEqual(len(self.procsList.get_all_damage_procs()), 0) 27 | 28 | 29 | class TestProc(unittest.TestCase): 30 | def setUp(self): 31 | self.proc = procs.Proc(**procs.ProcsList.allowed_procs['bloodthirsty_instinct']) 32 | 33 | def test__init__(self): 34 | self.assertEqual(self.proc.stat, 'stats') 35 | self.assertEqual(self.proc.value['haste'], 2880) 36 | self.assertEqual(self.proc.duration, 10) 37 | self.assertEqual(self.proc.proc_rate, 3) 38 | self.assertEqual(self.proc.trigger, 'all_attacks') 39 | self.assertEqual(self.proc.icd, 0) 40 | self.assertEqual(self.proc.max_stacks, 1) 41 | self.assertEqual(self.proc.on_crit, False) 42 | self.assertEqual(self.proc.proc_name, 'Bloodthirsty Instinct') 43 | 44 | def test_procs_off_auto_attacks(self): 45 | self.assertTrue(self.proc.procs_off_auto_attacks()) 46 | 47 | def test_procs_off_strikes(self): 48 | self.assertTrue(self.proc.procs_off_strikes()) 49 | 50 | def test_procs_off_harmful_spells(self): 51 | self.assertFalse(self.proc.procs_off_harmful_spells()) 52 | 53 | def test_procs_off_heals(self): 54 | self.assertFalse(self.proc.procs_off_heals()) 55 | 56 | def test_procs_off_periodic_spell_damage(self): 57 | self.assertFalse(self.proc.procs_off_periodic_spell_damage()) 58 | 59 | def test_procs_off_periodic_heals(self): 60 | self.assertFalse(self.proc.procs_off_periodic_heals()) 61 | 62 | def test_procs_off_apply_debuff(self): 63 | self.assertTrue(self.proc.procs_off_apply_debuff()) 64 | 65 | def test_procs_off_bleeds(self): 66 | self.assertFalse(self.proc.procs_off_bleeds()) 67 | 68 | def test_procs_off_crit_only(self): 69 | self.assertFalse(self.proc.procs_off_crit_only()) 70 | 71 | def test_is_ppm(self): 72 | self.assertFalse(self.proc.is_ppm()) 73 | -------------------------------------------------------------------------------- /tests/objects_tests/race_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from shadowcraft.objects import race 3 | 4 | class TestRace(unittest.TestCase): 5 | def setUp(self): 6 | self.race = race.Race('human') 7 | 8 | def test__init__(self): 9 | self.assertEqual(self.race.race_name, 'human') 10 | self.assertEqual(self.race.character_class, 'rogue') 11 | 12 | def test_set_racials(self): 13 | self.assertTrue(self.race.human_spirit) 14 | self.assertFalse(self.race.blood_fury_physical) 15 | 16 | def test_exceptions(self): 17 | self.assertRaises(race.InvalidRaceException, self.race.__setattr__, 'level', 111) 18 | self.assertRaises(race.InvalidRaceException, self.race.__init__, 'murloc') 19 | self.assertRaises(race.InvalidRaceException, self.race.__init__, 'undead', 'demon_hunter') 20 | 21 | def test__getattr__(self): 22 | racial_stats = (288, 306, 212, 169, 127) 23 | for i, stat in enumerate(['racial_str', 'racial_agi', 'racial_sta', 'racial_int', 'racial_spi']): 24 | self.assertEqual(getattr(self.race, stat), racial_stats[i]) 25 | racial_stats = (288 - 4, 306 + 4, 212, 169, 127) 26 | night_elf = race.Race('night_elf') 27 | for i, stat in enumerate(['racial_str', 'racial_agi', 'racial_sta', 'racial_int', 'racial_spi']): 28 | self.assertEqual(getattr(night_elf, stat), racial_stats[i]) 29 | 30 | def test_get_racial_crit(self): 31 | for weapon in ('thrown', 'gun', 'bow'): 32 | self.assertEqual(self.race.get_racial_crit(weapon), 0) 33 | troll = race.Race('troll') 34 | self.assertEqual(troll.get_racial_crit(), 0) 35 | worgen = race.Race('worgen') 36 | self.assertEqual(worgen.get_racial_crit(), 0.01) 37 | self.assertEqual(worgen.get_racial_crit('gun'), 0.01) 38 | self.assertEqual(worgen.get_racial_crit('axe'), 0.01) 39 | 40 | def test_get_racial_haste(self): 41 | self.assertEqual(self.race.get_racial_haste(), 0) 42 | goblin = race.Race('goblin') 43 | self.assertEqual(goblin.get_racial_haste(), 0.01) 44 | 45 | def test_get_racial_stat_boosts(self): 46 | self.assertEqual(len(self.race.get_racial_stat_boosts()), 0) 47 | orc = race.Race('orc') 48 | orc.level = 110; 49 | abilities = orc.get_racial_stat_boosts() 50 | self.assertEqual(len(abilities), 2) 51 | self.assertEqual(abilities[0]['duration'], 15) 52 | self.assertTrue(abilities[1]['stat'] in ('ap', 'sp')) 53 | self.assertNotEqual(abilities[0]['stat'],abilities[1]['stat']) 54 | if (abilities[0]['stat'] == 'ap'): 55 | self.assertEqual(abilities[0]['value'], 2243) 56 | else: 57 | self.assertEqual(abilities[0]['value'], 585) 58 | 59 | def test_goblin_racial(self): 60 | goblin = race.Race('goblin') 61 | goblin.level = 80 62 | self.assertTrue(goblin.rocket_barrage) 63 | self.assertAlmostEqual(goblin.activated_racial_data['rocket_barrage']['value'](goblin, 10, 10, 10), 172.8093) 64 | -------------------------------------------------------------------------------- /tests/objects_tests/rogue_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShadowCraft/ShadowCraft-Engine/1821fcab201c7ab969c60286d2123c646161f65b/tests/objects_tests/rogue_tests/__init__.py -------------------------------------------------------------------------------- /tests/objects_tests/rogue_tests/rogue_talents_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from shadowcraft.objects.talents import Talents 3 | from shadowcraft.objects.talents import InvalidTalentException 4 | 5 | class TestAssassinationTalents(unittest.TestCase): 6 | # Tests for the abstract class objects.talents.TalentTree 7 | def setUp(self): 8 | self.talents = Talents('1231231', 'assassination', 'rogue') 9 | 10 | def test__getattr__(self): 11 | self.assertRaises(AttributeError, self.talents.__getattr__, 'fake_talent') 12 | self.assertEqual(self.talents.master_poisoner, True) 13 | self.assertEqual(self.talents.nightstalker, False) 14 | 15 | def test_set_talent(self): 16 | self.assertRaises(InvalidTalentException, self.talents.set_talent, 'fake_talent') 17 | self.assertRaises(InvalidTalentException, self.talents.set_talent, 'vendetta') 18 | self.assertRaises(InvalidTalentException, self.talents.set_talent, 'vendetta') 19 | 20 | class TestCombatTalents(unittest.TestCase): 21 | pass 22 | 23 | 24 | class TestSubtletyTalents(unittest.TestCase): 25 | pass 26 | 27 | 28 | class TestRogueTalents(unittest.TestCase): 29 | def setUp(self): 30 | self.talents = Talents('1231231', 'assassination', 'rogue') 31 | 32 | def test(self): 33 | self.assertEqual(self.talents.master_poisoner, 1) 34 | self.assertEqual(self.talents.subterfuge, 1) 35 | self.assertEqual(self.talents.vigor, 1) 36 | self.assertEqual(self.talents.cheat_death, 0) 37 | self.assertEqual(self.talents.thuggee, 0) -------------------------------------------------------------------------------- /tests/objects_tests/stats_tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import unittest 3 | from shadowcraft.core import exceptions 4 | from shadowcraft.objects import stats 5 | from shadowcraft.objects import procs 6 | 7 | class TestStats(unittest.TestCase): 8 | def setUp(self): 9 | mainhand = stats.Weapon(1234, 2.6, "sword") 10 | offhand = stats.Weapon(777, 2.6, "sword") 11 | 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) 12 | 13 | def test_stats(self): 14 | self.assertEqual(self.stats.agi, 3485) 15 | 16 | def test_set_constants_for_level(self): 17 | self.assertRaises(exceptions.InvalidLevelException, self.stats.__setattr__, 'level', 111) 18 | 19 | def test_get_mastery_from_rating(self): 20 | self.assertAlmostEqual(self.stats.get_mastery_from_rating(), 8 + 1234 / 400) 21 | self.assertAlmostEqual(self.stats.get_mastery_from_rating(100), 8 + 100 / 400) 22 | 23 | def test_get_versatility_multiplier_from_rating(self): 24 | self.assertAlmostEqual(self.stats.get_versatility_multiplier_from_rating(), 1 + 1222 / 47500) 25 | self.assertAlmostEqual(self.stats.get_versatility_multiplier_from_rating(100), 1 + 100 / 47500) 26 | 27 | def test_get_crit_from_rating(self): 28 | self.assertAlmostEqual(self.stats.get_crit_from_rating(), 899 / 40000) 29 | self.assertAlmostEqual(self.stats.get_crit_from_rating(100), 100 / 40000) 30 | 31 | def test_get_haste_multiplier_from_rating(self): 32 | self.assertAlmostEqual(self.stats.get_haste_multiplier_from_rating(), 1 + 666 / 37500) 33 | self.assertAlmostEqual(self.stats.get_haste_multiplier_from_rating(100), 1 + 100 / 37500) 34 | 35 | 36 | class TestGearBuffs(unittest.TestCase): 37 | def setUp(self): 38 | self.gear = stats.GearBuffs('gear_specialization') 39 | self.gear_none = stats.GearBuffs() 40 | 41 | def test__getattr__(self): 42 | self.assertTrue(self.gear.gear_specialization) 43 | self.assertFalse(self.gear.rogue_t19_2pc) 44 | self.assertRaises(AttributeError, self.gear.__getattr__, 'fake_gear_buff') 45 | 46 | def test_gear_specialization_multiplier(self): 47 | self.assertAlmostEqual(self.gear.gear_specialization_multiplier(), 1.05) 48 | self.assertAlmostEqual(self.gear_none.gear_specialization_multiplier(), 1.0) 49 | -------------------------------------------------------------------------------- /tests/runtests.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import unittest 3 | from os import path 4 | import sys 5 | sys.path.append(path.abspath(path.dirname(__file__))) 6 | sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) 7 | 8 | from calcs_tests import TestDamageCalculator 9 | from calcs_tests.rogue_tests import TestOutlawRogueDamageCalculator 10 | from calcs_tests.rogue_tests import TestAssassinationRogueDamageCalculator 11 | from calcs_tests.rogue_tests import TestSubtletyRogueDamageCalculator 12 | from calcs_tests.rogue_tests import TestAOEOutlawRogueDamageCalculator 13 | from calcs_tests.rogue_tests import TestAOEAssassinationRogueDamageCalculator 14 | from calcs_tests.rogue_tests import TestAOESubtletyRogueDamageCalculator 15 | from core_tests.exceptions_tests import TestInvalidInputException 16 | from objects_tests.buffs_tests import TestBuffsTrue, TestBuffsFalse 17 | from objects_tests.stats_tests import TestStats, TestGearBuffs 18 | from objects_tests.procs_tests import TestProcsList, TestProc 19 | from objects_tests.race_tests import TestRace 20 | from objects_tests.rogue_tests.rogue_talents_tests import TestAssassinationTalents 21 | from objects_tests.rogue_tests.rogue_talents_tests import TestCombatTalents 22 | from objects_tests.rogue_tests.rogue_talents_tests import TestSubtletyTalents 23 | from objects_tests.rogue_tests.rogue_talents_tests import TestRogueTalents 24 | 25 | if __name__ == "__main__": 26 | unittest.main() 27 | --------------------------------------------------------------------------------