├── .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 |
--------------------------------------------------------------------------------