├── tests ├── __init__.py ├── core_tests │ ├── __init__.py │ └── exceptions_tests.py ├── objects_tests │ ├── __init__.py │ ├── rogue_tests │ │ ├── __init__.py │ │ ├── rogue_glyphs_tests.py │ │ └── rogue_talents_tests.py │ ├── procs_tests.py │ ├── race_tests.py │ ├── buffs_tests.py │ └── stats_tests.py ├── runtests.py └── calcs_tests │ ├── rogue_tests │ ├── Aldriana_tests │ │ └── __init__.py │ └── __init__.py │ ├── armor_mitigation_tests.py │ └── __init__.py ├── scripts ├── wowapi │ ├── wowapi │ │ ├── __init__.py │ │ ├── exceptions.py │ │ ├── utilities.py │ │ └── api.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_regions.py │ │ ├── test_locales.py │ │ └── test_data.py │ ├── setup.py │ ├── README.rst │ └── LICENSE ├── char_info.py ├── reinstall.sh ├── reinstall.bat ├── subtlety.py ├── combat.py ├── mop_subtlety.py ├── assassination.py ├── mop_subtlety_import.py ├── mop_combat_import.py ├── mop_assassination_import.py ├── mop_combat.py └── mop_assassination.py ├── MANIFEST.in ├── shadowcraft ├── __init__.py ├── core │ ├── __init__.py │ ├── locale │ │ ├── en │ │ │ ├── LC_MESSAGES │ │ │ │ └── SCE.mo │ │ │ └── en.po │ │ ├── es_ES │ │ │ ├── LC_MESSAGES │ │ │ │ └── SCE.mo │ │ │ └── es_ES.po │ │ └── SCE.pot │ ├── exceptions.py │ ├── files.in │ ├── i18n.py │ └── jsoninput.py ├── objects │ ├── __init__.py │ ├── glyphs.py │ ├── glyphs_data.py │ ├── talents.py │ ├── talents_data.py │ ├── buffs.py │ ├── class_data.py │ ├── procs.py │ ├── old_proc_data.py │ └── race.py └── calcs │ ├── armor_mitigation.py │ └── rogue │ └── Aldriana │ └── settings.py ├── .gitignore ├── setup.py ├── README ├── style.txt └── license.txt /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/core_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/wowapi/wowapi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/objects_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/wowapi/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/objects_tests/rogue_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include license.txt 2 | recursive-include shadowcraft/core/locale * 3 | -------------------------------------------------------------------------------- /shadowcraft/__init__.py: -------------------------------------------------------------------------------- 1 | import gettext 2 | import __builtin__ 3 | 4 | __builtin__._ = gettext.gettext 5 | -------------------------------------------------------------------------------- /shadowcraft/core/__init__.py: -------------------------------------------------------------------------------- 1 | import gettext 2 | import __builtin__ 3 | 4 | __builtin__._ = gettext.gettext 5 | -------------------------------------------------------------------------------- /shadowcraft/objects/__init__.py: -------------------------------------------------------------------------------- 1 | import gettext 2 | import __builtin__ 3 | 4 | __builtin__._ = gettext.gettext 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /shadowcraft/core/locale/en/LC_MESSAGES/SCE.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldriana/ShadowCraft-Engine/HEAD/shadowcraft/core/locale/en/LC_MESSAGES/SCE.mo -------------------------------------------------------------------------------- /scripts/char_info.py: -------------------------------------------------------------------------------- 1 | charInfo = {'region':'us', 'realm':'Doomhammer', 'name':'Pins', 'talents':None, 'stormlash':False, 'pvp':False, 'shiv':0, 'verbose':False} 2 | -------------------------------------------------------------------------------- /shadowcraft/core/locale/es_ES/LC_MESSAGES/SCE.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldriana/ShadowCraft-Engine/HEAD/shadowcraft/core/locale/es_ES/LC_MESSAGES/SCE.mo -------------------------------------------------------------------------------- /scripts/reinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd wowapi 4 | python setup.py build 5 | python setup.py install 6 | cd ../../ 7 | python setup.py build 8 | python setup.py install -------------------------------------------------------------------------------- /scripts/reinstall.bat: -------------------------------------------------------------------------------- 1 | cd wowapi 2 | 3 | python setup.py build 4 | 5 | python setup.py install 6 | 7 | cd ../../ 8 | 9 | python setup.py build 10 | 11 | python setup.py install 12 | cd scripts -------------------------------------------------------------------------------- /scripts/wowapi/setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup(name='wowapi', 4 | version='0.3.0', 5 | description='Python module to access the WoW Api', 6 | author='Dorwido', 7 | author_email='darkz@gmx.de', 8 | url='https://github.com/Dorwido/wowapi', 9 | license='MIT', 10 | packages=['wowapi'] 11 | ) -------------------------------------------------------------------------------- /tests/core_tests/exceptions_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from shadowcraft.core.exceptions import InvalidInputException 3 | 4 | class TestInvalidInputException(unittest.TestCase): 5 | def test(self): 6 | try: 7 | raise InvalidInputException("test") 8 | except InvalidInputException as e: 9 | self.assertEqual(str(e), "test") 10 | self.assertEqual(e.error_msg, "test") 11 | -------------------------------------------------------------------------------- /shadowcraft/core/exceptions.py: -------------------------------------------------------------------------------- 1 | class InvalidInputException(Exception): 2 | # Base class for all our exceptions. All exceptions we generate should 3 | # either use or subclass this. 4 | 5 | def __init__(self, error_msg): 6 | self.error_msg = error_msg 7 | 8 | def __str__(self): 9 | return str(self.error_msg) 10 | 11 | 12 | class InvalidLevelException(InvalidInputException): 13 | pass 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name='ShadowCraft-Engine', 5 | url='http://github.com/Aldriana/ShadowCraft-Engine/', 6 | version='0.1', 7 | packages=['shadowcraft', 8 | 'shadowcraft.calcs', 'shadowcraft.calcs.rogue', 'shadowcraft.calcs.rogue.Aldriana', 9 | 'shadowcraft.core', 10 | 'shadowcraft.objects'], 11 | license='LGPL', 12 | long_description=open('README').read(), 13 | ) 14 | -------------------------------------------------------------------------------- /scripts/wowapi/README.rst: -------------------------------------------------------------------------------- 1 | About 2 | ====== 3 | I am using a python framework for my website and due the current python modules for the WoW Api are not updated very often, 4 | still missing features and I prefer to get raw data, I wrote my own little module. 5 | 6 | | It supports: gzip compression, If-Modified-Since header, authorization, SSL 7 | 8 | 9 | 10 | Documentation 11 | ============= 12 | 13 | Full documentation can be found at: 14 | http://wowapi.wowuse.com/ -------------------------------------------------------------------------------- /tests/objects_tests/rogue_tests/rogue_glyphs_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from shadowcraft.objects.rogue import rogue_glyphs 3 | 4 | class TestRogueGlyphs(unittest.TestCase): 5 | def setUp(self): 6 | self.glyphs = rogue_glyphs.RogueGlyphs('backstab', 'mutilate', 'rupture') 7 | 8 | def test__getattr__(self): 9 | self.assertRaises(AttributeError, self.glyphs.__getattr__, 'fake_glyph') 10 | self.assertTrue(self.glyphs.backstab) 11 | self.assertFalse(self.glyphs.slice_and_dice) 12 | -------------------------------------------------------------------------------- /scripts/wowapi/wowapi/exceptions.py: -------------------------------------------------------------------------------- 1 | class APIError(Exception): 2 | """ 3 | .. versionadded:: 0.2.5 4 | 5 | 6 | This is raised on all other http errors and will always return http error code, reason for fail 7 | (if a reason is given otherwise None), url which failed 8 | """ 9 | pass 10 | 11 | class NotModified(APIError): 12 | """ 13 | This is raised when using the last modified option and nothing changed, since last request 14 | """ 15 | pass 16 | 17 | class NotFound(APIError): 18 | """ 19 | This is raised on 404 Errors 20 | """ 21 | pass -------------------------------------------------------------------------------- /shadowcraft/objects/glyphs.py: -------------------------------------------------------------------------------- 1 | from shadowcraft.objects import glyphs_data 2 | 3 | class Glyphs(object): 4 | 5 | def __init__(self, game_class='rogue', *args): 6 | self.game_class = game_class 7 | self.allowed_glyphs = glyphs_data.glyphs[game_class] 8 | for arg in args: 9 | if arg in self.allowed_glyphs: 10 | setattr(self, arg, True) 11 | 12 | def __getattr__(self, name): 13 | # Any glyph we haven't assigned a value to, we don't have. 14 | if name in self.allowed_glyphs: 15 | return False 16 | object.__getattribute__(self, name) 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/wowapi/tests/test_regions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from wowapi.api import WoWApi 3 | 4 | try: 5 | import unittest2 as unittest 6 | except ImportError: 7 | import unittest as unittest 8 | 9 | wowapi = WoWApi() 10 | class Test_Regions(unittest.TestCase): 11 | def test_region_us(self): 12 | realm = wowapi.get_realm('us') 13 | self.assertGreater(len(realm['data']['realms']),1) 14 | 15 | def test_region_eu(self): 16 | realm = wowapi.get_realm('eu') 17 | self.assertGreater(len(realm['data']['realms']),1) 18 | 19 | def test_region_kr(self): 20 | realm = wowapi.get_realm('kr') 21 | self.assertGreater(len(realm['data']['realms']),1) 22 | 23 | def test_region_tw(self): 24 | realm = wowapi.get_realm('tw') 25 | self.assertGreater(len(realm['data']['realms']),1) 26 | 27 | def test_region_cn(self): 28 | realm = wowapi.get_realm('cn') 29 | self.assertGreater(len(realm['data']['realms']),1) -------------------------------------------------------------------------------- /scripts/wowapi/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011 Thorsten Sanders 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software 6 | and associated documentation files (the "Software"), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 15 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 16 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /tests/runtests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from os import path 3 | import sys 4 | sys.path.append(path.abspath(path.dirname(__file__))) 5 | sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) 6 | 7 | from calcs_tests import TestDamageCalculator 8 | from calcs_tests.armor_mitigation_tests import TestArmorMitigation 9 | from calcs_tests.rogue_tests import TestRogueDamageCalculator 10 | from calcs_tests.rogue_tests import TestRogueDamageCalculatorLevels 11 | from calcs_tests.rogue_tests.Aldriana_tests import TestAldrianasRogueDamageCalculator 12 | from core_tests.exceptions_tests import TestInvalidInputException 13 | from objects_tests.buffs_tests import TestBuffsTrue, TestBuffsFalse, TestBuffsLevel 14 | from objects_tests.stats_tests import TestStats, TestWeapon, TestGearBuffs 15 | from objects_tests.procs_tests import TestProcsList, TestProc 16 | from objects_tests.race_tests import TestRace 17 | from objects_tests.rogue_tests.rogue_glyphs_tests import TestRogueGlyphs 18 | from objects_tests.rogue_tests.rogue_talents_tests import TestAssassinationTalents 19 | from objects_tests.rogue_tests.rogue_talents_tests import TestCombatTalents 20 | from objects_tests.rogue_tests.rogue_talents_tests import TestSubtletyTalents 21 | from objects_tests.rogue_tests.rogue_talents_tests import TestRogueTalents 22 | 23 | if __name__ == "__main__": 24 | unittest.main() 25 | -------------------------------------------------------------------------------- /shadowcraft/core/i18n.py: -------------------------------------------------------------------------------- 1 | import gettext 2 | import os.path 3 | import locale 4 | import __builtin__ 5 | 6 | __builtin__._ = gettext.gettext 7 | 8 | # Domain: this needs to be the name of our .mo files 9 | TRANSLATION_DOMAIN = 'SCE' 10 | LOCALE_DIR = os.path.join(os.path.dirname(__file__), "locale") 11 | 12 | def set_language(language): 13 | # This function will install gettext as the _() function and use the 14 | # language specified. It will fall back to code strings if given a not supported 15 | # language. Note that the 'local' value only makes sense when not running from 16 | # the hosted online version. 17 | if language == 'local': 18 | # Setting up a list of locales in your machine and asign them to the _() function 19 | languages_list = [] 20 | 21 | default_local_language, encoding = locale.getdefaultlocale() 22 | if (default_local_language): 23 | languages_list = [default_local_language] 24 | 25 | gnu_lang = os.environ.get('LANGUAGE', None) 26 | if (gnu_lang): 27 | languages_list += gnu_lang.split(":") 28 | 29 | gettext.translation(TRANSLATION_DOMAIN, LOCALE_DIR, fallback=True, languages=languages_list).install(unicode=True) 30 | 31 | else: 32 | gettext.translation(TRANSLATION_DOMAIN, LOCALE_DIR, fallback=True, languages=[language]).install(unicode=True) 33 | -------------------------------------------------------------------------------- /shadowcraft/objects/glyphs_data.py: -------------------------------------------------------------------------------- 1 | glyphs = { 2 | 'death_knight': frozenset([]), 3 | 'druid': frozenset([]), 4 | 'hunter': frozenset([]), 5 | 'mage': frozenset([]), 6 | 'monk': frozenset([]), 7 | 'paladin': frozenset([]), 8 | 'priest': frozenset([]), 9 | 'rogue': frozenset([ 10 | # Major 11 | 'adrenaline_rush', 12 | 'ambush', 13 | 'blade_flurry', 14 | 'blind', 15 | 'cheap_shot', 16 | 'cloak_of_shadows', 17 | 'crippling_poison', 18 | 'deadly_momentum', 19 | 'debilitation', 20 | 'evasion', 21 | 'expose_armor', 22 | 'feint', 23 | 'garrote', 24 | 'gouge', 25 | 'kick', 26 | 'recuperate', 27 | 'sap', 28 | 'shadow_walk', 29 | 'shiv', 30 | 'smoke_bomb', 31 | 'sprint', 32 | 'stealth', 33 | 'vanish', 34 | 'vendetta', 35 | # Minor 36 | 'blurred_speed', 37 | 'decoy', 38 | 'detection', 39 | 'disguise', 40 | 'distract', 41 | 'hemorrhage', 42 | 'killing_spree', 43 | 'pick_lock', 44 | 'pick_pocket', 45 | 'poisons', 46 | 'safe_fall', 47 | 'tricks_of_the_trade', 48 | ]), 49 | 'shaman': frozenset([]), 50 | 'warlock': frozenset([]), 51 | 'warrior': frozenset([]), 52 | } 53 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | ShadowCraft - Engine 2 | -------------------- 3 | This repository contains the calculations piece of ShadowCraft, a WoW 4 | theorycraft project. Initially, this is focused on rogues (hence the name), 5 | but the framework is designed such that if other classes wish to make use of it 6 | in the future, they can do so in a sensible and reasonable way. All rogue 7 | specific functionality is currently contained in directories named "rogue" - 8 | for instance, the objects/ directory contains objects of general use for 9 | theorycrafting calculations, while objects/rogue contains objects specifically 10 | for use in rogue theorycraft. 11 | 12 | If you would like to contribute to this project, either to add your own 13 | calculations module (for rogues or otherwise) or to improve what's already here 14 | (bugfixes, new features, etc.) by all means do so; however, I will be 15 | maintaining reasonably tight control over the architecture and *extremely* 16 | tight control over my calculations module (currently located in 17 | calcs/rogue/Aldriana). This doesn't mean you can't contribute stuff; it just 18 | means that you should be aware that I may not accept your changes. 19 | 20 | If you have any questions/comments/suggestions, you can email me at aldriana at 21 | elitistjerks dot com. Additionally, if your question is of a more general 22 | nature, there is a discussion thread for this project on the EJ forums. 23 | 24 | NOTE: Please read style.txt if you intend to submit code to this project. 25 | 26 | -- Aldriana 27 | Oct 28 2010 28 | -------------------------------------------------------------------------------- /shadowcraft/calcs/armor_mitigation.py: -------------------------------------------------------------------------------- 1 | from shadowcraft.core.exceptions import InvalidLevelException 2 | 3 | # tiered parameters for use in armor mitigation calculations. first tuple 4 | # element is the minimum level of the tier. the tuples must be in descending 5 | # order of minimum level for the lookup to work. parameters taken from 6 | # http://code.google.com/p/simulationcraft/source/browse/branches/mop/engine/sc_player.cpp#1365 7 | PARAMETERS = [ (86, 4037.5, 317117.5), 8 | (81, 2167.5, 158167.5), 9 | (60, 467.5, 22167.5), 10 | ( 1, 85.0, -400.0) ] # yes, negative 400 11 | 12 | def lookup_parameters(level): 13 | for parameters in PARAMETERS: 14 | if level >= parameters[0]: 15 | return parameters 16 | raise InvalidLevelException(_('No armor mitigation parameters available for level {level}').format(level=level)) 17 | 18 | def parameter(level=90): 19 | parameters = lookup_parameters(level) 20 | return level * parameters[1] - parameters[2] 21 | 22 | # this is the fraction of damage reduced by the armor 23 | def mitigation(armor, level=90, cached_parameter=None): 24 | if cached_parameter == None: 25 | cached_parameter = parameter(level) 26 | return armor / (armor + cached_parameter) 27 | 28 | # this is the fraction of damage retained despite the armor, 1 - mitigation. 29 | def multiplier(armor, level=90, cached_parameter=None): 30 | if cached_parameter == None: 31 | cached_parameter = parameter(level) 32 | return cached_parameter / (armor + cached_parameter) 33 | -------------------------------------------------------------------------------- /scripts/wowapi/tests/test_locales.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from wowapi.api import WoWApi 3 | 4 | try: 5 | import unittest2 as unittest 6 | except ImportError: 7 | import unittest as unittest 8 | 9 | wowapi = WoWApi() 10 | class Test_Locales(unittest.TestCase): 11 | 12 | def test_locale_en_US(self): 13 | item = wowapi.get_item('us',25,None,'en_US') 14 | self.assertEqual(item['data']['name'],'Worn Shortsword') 15 | 16 | def test_locale_es_MX(self): 17 | item = wowapi.get_item('us',25,None,'es_MX') 18 | self.assertEqual(item['data']['name'],'Espada corta desgastada') 19 | 20 | def test_locale_en_GB(self): 21 | item = wowapi.get_item('eu',25,None,'en_GB') 22 | self.assertEqual(item['data']['name'],'Worn Shortsword') 23 | 24 | def test_locale_es_ES(self): 25 | item = wowapi.get_item('eu',25,None,'es_ES') 26 | self.assertEqual(item['data']['name'],'Espada corta desgastada') 27 | 28 | def test_locale_fr_FR(self): 29 | item = wowapi.get_item('eu',25,None,'fr_FR') 30 | self.assertEqual(item['data']['name'],u'Epée courte usée') 31 | 32 | def test_locale_ru_RU(self): 33 | item = wowapi.get_item('eu',25,None,'ru_RU') 34 | self.assertEqual(item['data']['name'],u'Иссеченный короткий меч') 35 | 36 | def test_locale_de_DE(self): 37 | item = wowapi.get_item('eu',25,None,'de_DE') 38 | self.assertEqual(item['data']['name'],'Abgenutztes Kurzschwert') 39 | 40 | def test_locale_ko_KR(self): 41 | item = wowapi.get_item('kr',25,None,'ko_KR') 42 | self.assertEqual(item['data']['name'],u'낡은 쇼트소드') 43 | 44 | def test_locale_zh_TW(self): 45 | item = wowapi.get_item('tw',25,None,'zh_TW') 46 | self.assertEqual(item['data']['name'],u'破損的短劍') 47 | 48 | def test_locale_zh_CN(self): 49 | item = wowapi.get_item('cn',25,None,'zh_CN') 50 | self.assertEqual(item['data']['name'],u'破损的短剑') 51 | 52 | -------------------------------------------------------------------------------- /tests/calcs_tests/rogue_tests/Aldriana_tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator 3 | from shadowcraft.calcs.rogue.Aldriana import settings 4 | 5 | from shadowcraft.objects import buffs 6 | from shadowcraft.objects import race 7 | from shadowcraft.objects import stats 8 | from shadowcraft.objects import procs 9 | from shadowcraft.objects.rogue import rogue_talents 10 | from shadowcraft.objects.rogue import rogue_glyphs 11 | 12 | class TestAldrianasRogueDamageCalculator(unittest.TestCase): 13 | def test_get_ep(self): 14 | test_buffs = buffs.Buffs() 15 | test_mh = stats.Weapon(939.5, 1.8, 'dagger', 'landslide') 16 | test_oh = stats.Weapon(730.5, 1.4, 'dagger', 'landslide') 17 | test_ranged = stats.Weapon(1371.5, 2.2, 'thrown') 18 | test_procs = procs.ProcsList('heroic_prestors_talisman_of_machination', 'fluid_death', 'rogue_t11_4pc') 19 | test_gear_buffs = stats.GearBuffs('rogue_t11_2pc', 'leather_specialization', 'potion_of_the_tolvir', 'chaotic_metagem') 20 | test_stats = stats.Stats(20, 4756, 190, 1022, 1329, 597, 1189, 1377, test_mh, test_oh, test_ranged, test_procs, test_gear_buffs) 21 | test_talents = rogue_talents.RogueTalents('0333230113022110321', '0020000000000000000', '2030030000000000000') 22 | glyph_list = ['backstab', 'mutilate', 'rupture'] 23 | test_glyphs = rogue_glyphs.RogueGlyphs(*glyph_list) 24 | test_race = race.Race('night_elf') 25 | test_cycle = settings.AssassinationCycle() 26 | test_settings = settings.Settings(test_cycle, response_time=1) 27 | test_level = 85 28 | calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) 29 | ep_values = calculator.get_ep() 30 | self.assertTrue(ep_values['agi'] < 4.0) 31 | self.assertTrue(ep_values['agi'] > 2.0) 32 | self.assertTrue(ep_values['yellow_hit'] < 4.0) 33 | self.assertTrue(ep_values['yellow_hit'] > 1.0) 34 | self.assertTrue(ep_values['crit'] < 2.0) 35 | self.assertTrue(ep_values['crit'] > 0.0) 36 | -------------------------------------------------------------------------------- /scripts/wowapi/wowapi/utilities.py: -------------------------------------------------------------------------------- 1 | def http_datetime( dt=None ): 2 | 3 | if not dt: 4 | import datetime 5 | dt = datetime.datetime.utcnow() 6 | else: 7 | try: 8 | dt = dt - dt.utcoffset() 9 | except: 10 | pass # no timezone offset, just assume already in UTC 11 | 12 | s = dt.strftime('%a, %d %b %Y %H:%M:%S GMT') 13 | return s 14 | 15 | 16 | def parse_http_datetime( datestring, utc_tzinfo=None, strict=False ): 17 | 18 | import re, datetime 19 | m = re.match(r'(?P[a-z]+), (?P\d+) (?P[a-z]+) (?P\d+) (?P\d+):(?P\d+):(?P\d+(\.\d+)?) (?P\w+)$', 20 | datestring, re.IGNORECASE) 21 | if not m and not strict: 22 | m = re.match(r'(?P[a-z]+) (?P[a-z]+) (?P\d+) (?P\d+):(?P\d+):(?P\d+) (?P\d+)$', 23 | datestring, re.IGNORECASE) 24 | if not m: 25 | m = re.match(r'(?P[a-z]+), (?P\d+)-(?P[a-z]+)-(?P\d+) (?P\d+):(?P\d+):(?P\d+(\.\d+)?) (?P\w+)$', 26 | datestring, re.IGNORECASE) 27 | if not m: 28 | raise ValueError('HTTP date is not correctly formatted') 29 | 30 | try: 31 | tz = m.group('TZ').upper() 32 | except: 33 | tz = 'GMT' 34 | if tz not in ('GMT','UTC','0000','00:00'): 35 | raise ValueError('HTTP date is not in GMT timezone') 36 | 37 | monname = m.group('MON').upper() 38 | mdict = {'JAN':1, 'FEB':2, 'MAR':3, 'APR':4, 'MAY':5, 'JUN':6, 39 | 'JUL':7, 'AUG':8, 'SEP':9, 'OCT':10, 'NOV':11, 'DEC':12} 40 | month = mdict.get(monname) 41 | if not month: 42 | raise ValueError('HTTP date has an unrecognizable month') 43 | y = int(m.group('Y')) 44 | if y < 100: 45 | century = datetime.datetime.utcnow().year / 100 46 | if y < 50: 47 | y = century * 100 + y 48 | else: 49 | y = (century - 1) * 100 + y 50 | d = int(m.group('D')) 51 | hour = int(m.group('H')) 52 | minute = int(m.group('M')) 53 | try: 54 | second = int(m.group('S')) 55 | except: 56 | second = float(m.group('S')) 57 | dt = datetime.datetime( y, month, d, hour, minute, second, tzinfo=utc_tzinfo ) 58 | return dt -------------------------------------------------------------------------------- /tests/objects_tests/rogue_tests/rogue_talents_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from shadowcraft.objects import talents 3 | from shadowcraft.objects.rogue import rogue_talents 4 | 5 | class TestAssassinationTalents(unittest.TestCase): 6 | # Tests for the abstract class objects.talents.TalentTree 7 | def setUp(self): 8 | self.talents = rogue_talents.Assassination('0333230113022110321') 9 | 10 | def test__getattr__(self): 11 | self.assertRaises(AttributeError, self.talents.__getattr__, 'fake_talent') 12 | talents = rogue_talents.Assassination() 13 | self.assertEqual(talents.vendetta, 0) 14 | 15 | def test__init__kwargs(self): 16 | talents = rogue_talents.Assassination(vendetta=1) 17 | self.assertEqual(talents.vendetta, 1) 18 | self.assertEqual(talents.cold_blood, 0) 19 | self.assertRaises(AttributeError, talents.__getattr__, 'fake_talent') 20 | 21 | def test_set_talent(self): 22 | self.assertRaises(talents.InvalidTalentException, self.talents.set_talent, 'fake_talent', 2) 23 | self.assertRaises(talents.InvalidTalentException, self.talents.set_talent, 'vendetta', -1) 24 | self.assertRaises(talents.InvalidTalentException, self.talents.set_talent, 'vendetta', -2) 25 | 26 | def test_exceptions(self): 27 | self.assertRaises(talents.InvalidTalentException, rogue_talents.Assassination, '10333230113022110321') 28 | 29 | class TestCombatTalents(unittest.TestCase): 30 | pass 31 | 32 | 33 | class TestSubtletyTalents(unittest.TestCase): 34 | pass 35 | 36 | 37 | class TestRogueTalents(unittest.TestCase): 38 | def setUp(self): 39 | self.talents = rogue_talents.RogueTalents('0333230113022110321', '0020000000000000000', '2030030000000000000') 40 | 41 | def test(self): 42 | self.assertEqual(self.talents.vendetta, 1) 43 | self.assertEqual(self.talents.cold_blood, 1) 44 | self.assertEqual(self.talents.relentless_strikes, 3) 45 | self.assertEqual(self.talents.precision, 2) 46 | self.assertEqual(self.talents.killing_spree, 0) 47 | 48 | def test_is_assassination_rogue(self): 49 | self.assertTrue(self.talents.is_assassination_rogue()) 50 | 51 | def test_is_combat_rogue(self): 52 | self.assertFalse(self.talents.is_combat_rogue()) 53 | 54 | def test_is_subtlety_rogue(self): 55 | self.assertFalse(self.talents.is_subtlety_rogue()) 56 | -------------------------------------------------------------------------------- /tests/calcs_tests/armor_mitigation_tests.py: -------------------------------------------------------------------------------- 1 | from shadowcraft import calcs 2 | import unittest 3 | from shadowcraft.core import exceptions 4 | from shadowcraft.calcs import armor_mitigation 5 | 6 | class TestArmorMitigation(unittest.TestCase): 7 | def test_thresholds(self): 8 | self.assertRaises(exceptions.InvalidLevelException, armor_mitigation.lookup_parameters, 0) 9 | self.assertEqual(1, armor_mitigation.lookup_parameters(1)[0]) 10 | self.assertEqual(1, armor_mitigation.lookup_parameters(59)[0]) 11 | self.assertEqual(60, armor_mitigation.lookup_parameters(60)[0]) 12 | self.assertEqual(60, armor_mitigation.lookup_parameters(80)[0]) 13 | self.assertEqual(81, armor_mitigation.lookup_parameters(81)[0]) 14 | 15 | def test_parameter_spot_checks(self): 16 | self.assertAlmostEqual( 5882.5, armor_mitigation.parameter(60)) 17 | self.assertAlmostEqual(10557.5, armor_mitigation.parameter(70)) 18 | self.assertAlmostEqual(15232.5, armor_mitigation.parameter(80)) 19 | self.assertAlmostEqual(26070.0, armor_mitigation.parameter(85)) 20 | 21 | def test_mitigation_spot_checks(self): 22 | self.assertAlmostEqual(0.4441, armor_mitigation.mitigation(4700, 60), 4) 23 | self.assertAlmostEqual(0.4217, armor_mitigation.mitigation(7700, 70), 4) 24 | self.assertAlmostEqual(0.4109, armor_mitigation.mitigation(10623, 80), 4) 25 | self.assertAlmostEqual(0.3148, armor_mitigation.mitigation(11977, 85), 4) 26 | 27 | def test_mitigation_cached_parameter(self): 28 | self.assertAlmostEqual(0.4441, armor_mitigation.mitigation(4700, 60, 5882.5), 4) 29 | self.assertAlmostEqual(0.4217, armor_mitigation.mitigation(7700, 70, 10557.5), 4) 30 | self.assertAlmostEqual(0.4109, armor_mitigation.mitigation(10623, 80, 15232.5), 4) 31 | self.assertAlmostEqual(0.3148, armor_mitigation.mitigation(11977, 85, 26070.0), 4) 32 | 33 | def test_multiplier_spot_checks(self): 34 | self.assertAlmostEqual(1 - 0.4441, armor_mitigation.multiplier(4700, 60), 4) 35 | self.assertAlmostEqual(1 - 0.4217, armor_mitigation.multiplier(7700, 70), 4) 36 | self.assertAlmostEqual(1 - 0.4109, armor_mitigation.multiplier(10623, 80), 4) 37 | self.assertAlmostEqual(1 - 0.3148, armor_mitigation.multiplier(11977, 85), 4) 38 | 39 | def test_multiplier_cached_paramter(self): 40 | self.assertAlmostEqual(1 - 0.4441, armor_mitigation.multiplier(4700, 60, 5882.5), 4) 41 | self.assertAlmostEqual(1 - 0.4217, armor_mitigation.multiplier(7700, 70, 10557.5), 4) 42 | self.assertAlmostEqual(1 - 0.4109, armor_mitigation.multiplier(10623, 80, 15232.5), 4) 43 | self.assertAlmostEqual(1 - 0.3148, armor_mitigation.multiplier(11977, 85, 26070.0), 4) 44 | -------------------------------------------------------------------------------- /shadowcraft/objects/talents.py: -------------------------------------------------------------------------------- 1 | from shadowcraft.core import exceptions 2 | from shadowcraft.objects import talents_data 3 | 4 | class InvalidTalentException(exceptions.InvalidInputException): 5 | pass 6 | 7 | 8 | class Talents(object): 9 | 10 | def __init__(self, talent_string, game_class='rogue', level='90'): 11 | self.game_class = game_class 12 | self.class_talents = talents_data.talents[game_class] 13 | self.level = level 14 | self.allowed_talents = [talent for tier in self.class_talents for talent in tier] 15 | self.allowed_talents_for_level = self.get_allowed_talents_for_level() 16 | self.initialize_talents(talent_string) 17 | 18 | def __setattr__(self, name, value): 19 | object.__setattr__(self, name, value) 20 | 21 | def __getattr__(self, name): 22 | # If someone tries to access a talent not initialized (the talent 23 | # string was shorter than 6) we return False 24 | if name in self.allowed_talents: 25 | return False 26 | object.__getattribute__(self, name) 27 | 28 | def get_allowed_talents_for_level(self): 29 | allowed_talents_for_level = [] 30 | for i in xrange(self.get_top_tier()): 31 | for talent in self.class_talents[i]: 32 | allowed_talents_for_level.append(talent) 33 | return allowed_talents_for_level 34 | 35 | def is_allowed_talent(self, name, check_level=False): 36 | if check_level: 37 | return name in self.allowed_talents_for_level 38 | else: 39 | return name in self.allowed_talents 40 | 41 | def get_top_tier(self): 42 | levels = (15, 30, 45, 60, 75, 90) 43 | top_tier = 0 44 | for i in levels: 45 | if self.level >= i: 46 | top_tier += 1 47 | return top_tier 48 | 49 | def initialize_talents(self, talent_string): 50 | if len(talent_string) > 6: 51 | raise InvalidTalentException(_('Talent strings must be 6 or less characters long')) 52 | j = 0 53 | for i in talent_string: 54 | if int(i) not in range(4): 55 | raise InvalidTalentException(_('Values in the talent string must be 0, 1, 2, 3, or sometimes 4')) 56 | if int(i) == 0 or i == '.': 57 | pass 58 | else: 59 | setattr(self, self.class_talents[j][int(i) - 1], True) 60 | j += 1 61 | 62 | def reset_talents(self): 63 | for talent in self.allowed_talents: 64 | setattr(self, talent, False) 65 | 66 | def get_tier_for_talent(self, name): 67 | if name not in self.allowed_talents: 68 | return None 69 | tier = 0 70 | for i in xrange(6): 71 | if name in self.class_talents[i]: 72 | return i 73 | 74 | def set_talent(self, name): 75 | # Clears talents in the tier and sets the new one 76 | if name not in self.allowed_talents: 77 | return False 78 | for talent in self.class_talents[self.get_tier_for_talent(name)]: 79 | setattr(self, talent, False) 80 | setattr(self, name, True) 81 | -------------------------------------------------------------------------------- /scripts/wowapi/tests/test_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from wowapi.api import WoWApi 3 | 4 | try: 5 | import unittest2 as unittest 6 | except ImportError: 7 | import unittest as unittest 8 | 9 | wowapi = WoWApi() 10 | class Test_GetData(unittest.TestCase): 11 | 12 | def test_get_character(self): 13 | character = wowapi.get_character('eu','Doomhammer','Thetotemlord') 14 | self.assertEqual(character['data']['name'],'Thetotemlord') 15 | 16 | 17 | def test_get_item(self): 18 | item = wowapi.get_item('eu',25) 19 | self.assertEqual(item['data']['name'],'Worn Shortsword') 20 | 21 | def test_get_guild(self): 22 | guild = wowapi.get_guild('eu','Doomhammer','Dawn Of Osiris') 23 | self.assertEqual(guild['data']['name'],'Dawn Of Osiris') 24 | 25 | def test_get_realm(self): 26 | realm = wowapi.get_realm('eu') 27 | self.assertGreater(len(realm['data']['realms']),1) 28 | 29 | 30 | def test_get_auctions(self): 31 | auctions = wowapi.get_auctions('eu','Defias Brotherhood') 32 | self.assertEqual(len(auctions['data']),4) 33 | 34 | def test_get_arena_ladder_team(self): 35 | arena_ladder = wowapi.get_arena_ladder('eu','Blackout','2v2',1) 36 | self.assertEqual(len(arena_ladder['data']['arenateam']),1) 37 | arena_team = wowapi.get_arena_team('eu',arena_ladder['data']['arenateam'][0]['realm'],'2v2',arena_ladder['data']['arenateam'][0]['name']) 38 | self.assertEqual(arena_team['data']['name'],arena_ladder['data']['arenateam'][0]['name']) 39 | 40 | 41 | def test_get_character_races (self): 42 | character_races = wowapi.get_character_races('eu') 43 | self.assertGreater(len(character_races['data']['races']),1) 44 | 45 | def test_get_character_classes (self): 46 | character_classes = wowapi.get_character_classes('eu') 47 | self.assertGreater(len(character_classes['data']['classes']),1) 48 | 49 | def test_get_guild_rewards (self): 50 | guild_rewards = wowapi.get_guild_rewards('eu') 51 | self.assertGreater(len(guild_rewards['data']['rewards']),1) 52 | 53 | def test_get_guild_perks (self): 54 | guild_perks = wowapi.get_guild_perks('eu') 55 | self.assertGreater(len(guild_perks['data']['perks']),1) 56 | 57 | def test_get_item_classes (self): 58 | item_classes = wowapi.get_item_classes('eu') 59 | self.assertGreater(len(item_classes['data']['classes']),1) 60 | 61 | def test_get_quest(self): 62 | quest_info = wowapi.get_quest('eu',25) 63 | self.assertEqual(quest_info['data']['id'],25) 64 | 65 | def test_get_recipe (self): 66 | recipe_info = wowapi.get_recipe('us',33994) 67 | self.assertEqual(recipe_info['data']['id'],33994) 68 | 69 | def test_get_achievements_character(self): 70 | char_achievements = wowapi.get_achievements_character('eu') 71 | self.assertGreater(len(char_achievements['data']['achievements']),1) 72 | 73 | def test_get_achievements_guild(self): 74 | guild_achievements = wowapi.get_achievements_guild('eu') 75 | self.assertGreater(len(guild_achievements['data']['achievements']),1) 76 | -------------------------------------------------------------------------------- /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('darkmoon_card_hurricane','heroic_left_eye_of_rajh') 7 | 8 | def test__init__(self): 9 | self.assertRaises(procs.InvalidProcException, procs.ProcsList, 'fake_proc') 10 | self.procsList = procs.ProcsList('darkmoon_card_hurricane') 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.darkmoon_card_hurricane) 16 | self.assertFalse(self.procsList.fluid_death) 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()), 1) 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['prestors_talisman_of_machination']) 32 | 33 | def test__init__(self): 34 | self.assertEqual(self.proc.stat, 'haste') 35 | self.assertEqual(self.proc.value, 1926) 36 | self.assertEqual(self.proc.duration, 15) 37 | self.assertEqual(self.proc.proc_chance, .1) 38 | self.assertEqual(self.proc.trigger, 'all_attacks') 39 | self.assertEqual(self.proc.icd, 75) 40 | self.assertEqual(self.proc.max_stacks, 1) 41 | self.assertEqual(self.proc.on_crit, False) 42 | self.assertEqual(self.proc.proc_name, 'Nefarious Plot') 43 | self.assertEqual(self.proc.ppm, False) 44 | 45 | def test_procs_off_auto_attacks(self): 46 | self.assertTrue(self.proc.procs_off_auto_attacks()) 47 | 48 | def test_procs_off_strikes(self): 49 | self.assertTrue(self.proc.procs_off_strikes()) 50 | 51 | def test_procs_off_harmful_spells(self): 52 | self.assertFalse(self.proc.procs_off_harmful_spells()) 53 | 54 | def test_procs_off_heals(self): 55 | self.assertFalse(self.proc.procs_off_heals()) 56 | 57 | def test_procs_off_periodic_spell_damage(self): 58 | self.assertFalse(self.proc.procs_off_periodic_spell_damage()) 59 | 60 | def test_procs_off_periodic_heals(self): 61 | self.assertFalse(self.proc.procs_off_periodic_heals()) 62 | 63 | def test_procs_off_apply_debuff(self): 64 | self.assertTrue(self.proc.procs_off_apply_debuff()) 65 | 66 | def test_procs_off_bleeds(self): 67 | self.assertFalse(self.proc.procs_off_bleeds()) 68 | 69 | def test_procs_off_crit_only(self): 70 | self.assertFalse(self.proc.procs_off_crit_only()) 71 | 72 | def test_is_ppm(self): 73 | self.assertFalse(self.proc.is_ppm()) 74 | 75 | def test_proc_rate(self): 76 | self.assertEqual(self.proc.proc_rate(), self.proc.proc_chance) 77 | -------------------------------------------------------------------------------- /style.txt: -------------------------------------------------------------------------------- 1 | Style guideines for ShadowCraft-Engine 2 | 3 | While we're getting started I'm going to be somewhat lenient about these, as 4 | its pretty important to get the framework roughed out so people can start 5 | building off it; however, once the initial flurry settles down, I will start 6 | enforcing these a bit more strenuously. 7 | 8 | 0) Assume for the moment that we're using Python 2.6. If there's some 2.7 9 | feature that you feel would be a real asset for something you're doing, 10 | let me know and we can discuss it. We are not using 3.x. 11 | 1) Indents are 4 spaces. Tabs are strictly forbidden. 12 | 2) Avoid trailing whitespace in all cases. And I do mean all cases. 13 | 3) Line length: Try to keep comments to 80 characters. For general code I'm 14 | not going to enforce a strict limit, but if you're going over 120 characters 15 | or so you should think about whether there's a natural way to break it. If 16 | there's not, that's fine, but if there is, that's better. 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 | 5) For binary operators (+, -, *, /, %, etc.) put a space around the operator: 21 | Correct: a = 1 + 2 * 3 22 | Wrong: a=1+2*3 23 | 24 | Exception: When assigning a default value for a function parameter, do not 25 | use spaces: 26 | Correct: def foo(bar=1): 27 | Wrong: def foo(bar = 1): 28 | 6) Imports: With the exception of importing something that's in __init__, 29 | import the module, not the class. 30 | Correct: from calcs import gylphs 31 | Slightly Wrong: import calcs.glyphs 32 | Wrong: from calcs.glyphs import Glyph 33 | Very Wrong: from calcs.glyph import * 34 | 35 | Imports should also generally be done in alphabetical order. 36 | 7) Try to keep module names distinct to the extent that it's possible to do so 37 | and still have them make sense. It helps if you use descriptive module 38 | names 39 | 8) Modules names should be lowercase_and_underscores. 40 | 9) Function names should be lowercase_and_underscores. 41 | 10) Class names should be CamelCase. 42 | 11) If a module primarily consists of a single class definition, the module 43 | name and the class name should match. 44 | 12) Any string where there is even the slightest chance it will be shown to an 45 | external user should use named introspection for variables. This is to 46 | make translation, um, possible. 47 | Correct: "%(character_name)s is level %(character_level)d" % {'character_name': name, 'character_level': level} 48 | Wrong: "%s is level %d" % (name, level) 49 | Very Wrong: name + ' is level ' + str(level). 50 | 51 | To explain: in various languages the sentence syntax may require the 52 | variables to be in a different order. Giving them good descriptive names 53 | lets the translators properly rearrange them as needed to convey the proper 54 | meaning. 55 | 13) Comments are a good thing. If what you're doing isn't immediately obvious 56 | from a quick readthrough, add a comment to explain it. 57 | 58 | In general: please try to write code that's as readable and maintainable as 59 | possible. You only write the code once, but it will be read many many times. 60 | Hence its worth spending an extra couple of minutes writing it if it saves the 61 | readers even a few seconds in understanding it. As the saying goes: always 62 | write code as though the person who has to maintain it is a dangerous 63 | psychopath that knows where you live. 64 | -------------------------------------------------------------------------------- /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.sword_1h_specialization) 14 | self.assertFalse(self.race.blood_fury_physical) 15 | 16 | def test_exceptions(self): 17 | self.assertRaises(race.InvalidRaceException, self.race.__setattr__, 'level', 81) 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 = (122, 206, 114, 46, 73) 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 = (122 - 4, 206 + 4, 114, 46, 73) 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_expertise(self): 31 | self.assertTrue(abs(self.race.get_racial_expertise('1h_sword') - 0.0075) < 0.00000001) 32 | self.assertEqual(self.race.get_racial_expertise('1h_axe'), 0) 33 | 34 | def test_get_racial_crit(self): 35 | for weapon in ('thrown', 'gun', 'bow'): 36 | self.assertEqual(self.race.get_racial_crit(weapon), 0) 37 | troll = race.Race('troll') 38 | self.assertEqual(troll.get_racial_crit('thrown'), 0.01) 39 | self.assertEqual(troll.get_racial_crit('bow'), 0.01) 40 | self.assertEqual(troll.get_racial_crit('gun'), 0) 41 | self.assertEqual(troll.get_racial_crit(), 0) 42 | worgen = race.Race('worgen') 43 | self.assertEqual(worgen.get_racial_crit(), 0.01) 44 | self.assertEqual(worgen.get_racial_crit('gun'), 0.01) 45 | self.assertEqual(worgen.get_racial_crit('axe'), 0.01) 46 | 47 | def test_get_racial_hit(self): 48 | self.assertEqual(self.race.get_racial_hit(), 0) 49 | draenei = race.Race('draenei') 50 | self.assertEqual(draenei.get_racial_hit(), 0.01) 51 | 52 | def test_get_racial_haste(self): 53 | self.assertEqual(self.race.get_racial_haste(), 0) 54 | goblin = race.Race('goblin') 55 | self.assertEqual(goblin.get_racial_haste(), 0.01) 56 | 57 | def test_get_racial_stat_boosts(self): 58 | self.assertEqual(len(self.race.get_racial_stat_boosts()), 0) 59 | orc = race.Race('orc') 60 | orc.level = 85; 61 | abilities = orc.get_racial_stat_boosts() 62 | self.assertEqual(len(abilities), 2) 63 | self.assertEqual(abilities[0]['duration'], 15) 64 | self.assertTrue(abilities[1]['stat'] in ('ap', 'sp')) 65 | self.assertNotEqual(abilities[0]['stat'],abilities[1]['stat']) 66 | if (abilities[0]['stat'] == 'ap'): 67 | self.assertEqual(abilities[0]['value'], 1170) 68 | else: 69 | self.assertEqual(abilities[0]['value'], 585) 70 | 71 | def test_goblin_racial(self): 72 | goblin = race.Race('goblin') 73 | goblin.level = 80 74 | self.assertTrue(goblin.rocket_barrage) 75 | self.assertAlmostEqual(goblin.activated_racial_data['rocket_barrage']['value'](goblin, 10, 10, 10), 172.8093) 76 | -------------------------------------------------------------------------------- /scripts/subtlety.py: -------------------------------------------------------------------------------- 1 | # Simple test program to debug + play with subtlety models. 2 | from os import path 3 | import sys 4 | sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) 5 | 6 | from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator 7 | from shadowcraft.calcs.rogue.Aldriana import settings 8 | 9 | from shadowcraft.objects import buffs 10 | from shadowcraft.objects import race 11 | from shadowcraft.objects import stats 12 | from shadowcraft.objects import procs 13 | from shadowcraft.objects.rogue import rogue_talents 14 | from shadowcraft.objects.rogue import rogue_glyphs 15 | 16 | from shadowcraft.core import i18n 17 | 18 | # Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. 19 | test_language = 'local' 20 | i18n.set_language(test_language) 21 | 22 | # Set up buffs. 23 | test_buffs = buffs.Buffs( 24 | 'short_term_haste_buff', 25 | 'stat_multiplier_buff', 26 | 'crit_chance_buff', 27 | 'all_damage_buff', 28 | 'melee_haste_buff', 29 | 'attack_power_buff', 30 | 'str_and_agi_buff', 31 | 'armor_debuff', 32 | 'physical_vulnerability_debuff', 33 | 'spell_damage_debuff', 34 | 'spell_crit_debuff', 35 | 'bleed_damage_debuff', 36 | 'agi_flask', 37 | 'guild_feast' 38 | ) 39 | 40 | # Set up weapons. 41 | test_mh = stats.Weapon(939.5, 1.8, 'dagger', 'landslide') 42 | test_oh = stats.Weapon(730.5, 1.4, 'dagger', 'landslide') 43 | test_ranged = stats.Weapon(1371.5, 2.2, 'thrown') 44 | 45 | # Set up procs. 46 | test_procs = procs.ProcsList('heroic_prestors_talisman_of_machination', 'fluid_death') 47 | 48 | # Set up gear buffs. 49 | test_gear_buffs = stats.GearBuffs('rogue_t11_2pc', 'leather_specialization', 'potion_of_the_tolvir') 50 | 51 | # Set up a calcs object.. 52 | test_stats = stats.Stats(20, 4788, 190, 1399, 752, 574, 1705, 964, test_mh, test_oh, test_ranged, test_procs, test_gear_buffs) 53 | 54 | # Initialize talents.. 55 | test_talents = rogue_talents.RogueTalents('0230030000000000000', '0020000000000000000', '0332031321310012321') 56 | 57 | # Set up glyphs. 58 | glyph_list = ['backstab', 'slice_and_dice', 'shadow_dance', 'tricks_of_the_trade'] 59 | test_glyphs = rogue_glyphs.RogueGlyphs(*glyph_list) 60 | 61 | # Set up race. 62 | test_race = race.Race('night_elf') 63 | 64 | # Set up settings. 65 | test_cycle = settings.SubtletyCycle(5) 66 | test_settings = settings.Settings(test_cycle, response_time=1) 67 | 68 | # Set up level 69 | test_level = 85 70 | 71 | # Build a DPS object. 72 | calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) 73 | 74 | # Compute EP values. 75 | ep_values = calculator.get_ep() 76 | 77 | # Compute talents ranking 78 | main_tree_talents_ranking, off_tree_talents_ranking = calculator.get_talents_ranking() 79 | 80 | # Compute DPS Breakdown. 81 | dps_breakdown = calculator.get_dps_breakdown() 82 | total_dps = sum(entry[1] for entry in dps_breakdown.items()) 83 | 84 | def pretty_print(dict_list): 85 | max_len = 0 86 | for i in dict_list: 87 | dict_values = i.items() 88 | if max_len < max(len(entry[0]) for entry in dict_values): 89 | max_len = max(len(entry[0]) for entry in dict_values) 90 | 91 | print '-' * (max_len + 15) 92 | for i in dict_list: 93 | dict_values = i.items() 94 | dict_values.sort(key=lambda entry: entry[1], reverse=True) 95 | for value in dict_values: 96 | print value[0] + ':' + ' ' * (max_len - len(value[0])), value[1] 97 | print '-' * (max_len + 15) 98 | 99 | return max_len 100 | 101 | dicts_for_pretty_print = [ 102 | main_tree_talents_ranking, 103 | off_tree_talents_ranking, 104 | ep_values, 105 | dps_breakdown 106 | ] 107 | print ' ' * (pretty_print(dicts_for_pretty_print) + 1), total_dps, _("total damage per second.") 108 | -------------------------------------------------------------------------------- /scripts/combat.py: -------------------------------------------------------------------------------- 1 | # Simple test program to debug + play with combat models. 2 | from os import path 3 | import sys 4 | sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) 5 | 6 | from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator 7 | from shadowcraft.calcs.rogue.Aldriana import settings 8 | 9 | from shadowcraft.objects import buffs 10 | from shadowcraft.objects import race 11 | from shadowcraft.objects import stats 12 | from shadowcraft.objects import procs 13 | from shadowcraft.objects.rogue import rogue_talents 14 | from shadowcraft.objects.rogue import rogue_glyphs 15 | 16 | from shadowcraft.core import i18n 17 | 18 | # Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. 19 | test_language = 'local' 20 | i18n.set_language(test_language) 21 | 22 | # Set up buffs. 23 | test_buffs = buffs.Buffs( 24 | 'short_term_haste_buff', 25 | 'stat_multiplier_buff', 26 | 'crit_chance_buff', 27 | 'all_damage_buff', 28 | 'melee_haste_buff', 29 | 'attack_power_buff', 30 | 'str_and_agi_buff', 31 | 'armor_debuff', 32 | 'physical_vulnerability_debuff', 33 | 'spell_damage_debuff', 34 | 'spell_crit_debuff', 35 | 'bleed_damage_debuff', 36 | 'agi_flask', 37 | 'guild_feast' 38 | ) 39 | 40 | # Set up weapons. 41 | test_mh = stats.Weapon(1356.5, 2.6, '1h_axe', 'landslide') 42 | test_oh = stats.Weapon(730.5, 1.4, 'dagger', 'landslide') 43 | test_ranged = stats.Weapon(1371.5, 2.2, 'thrown') 44 | 45 | # Set up procs. 46 | test_procs = procs.ProcsList('heroic_prestors_talisman_of_machination', 'fluid_death', 'rogue_t11_4pc') 47 | 48 | # Set up gear buffs. 49 | test_gear_buffs = stats.GearBuffs('rogue_t11_2pc', 'leather_specialization', 'potion_of_the_tolvir', 'chaotic_metagem') 50 | 51 | # Set up a calcs object.. 52 | test_stats = stats.Stats(20, 4746, 190, 1290, 716, 779, 1761, 963, test_mh, test_oh, test_ranged, test_procs, test_gear_buffs) 53 | 54 | # Initialize talents.. 55 | test_talents = rogue_talents.RogueTalents('0232000000000000000', '0332230310032012321', '0030000000000000000') 56 | 57 | # Set up glyphs. 58 | glyph_list = ['sinister_strike', 'adrenaline_rush', 'slice_and_dice', 'tricks_of_the_trade'] 59 | test_glyphs = rogue_glyphs.RogueGlyphs(*glyph_list) 60 | 61 | # Set up race. 62 | test_race = race.Race('night_elf') 63 | 64 | # Set up settings. 65 | test_cycle = settings.CombatCycle() 66 | test_settings = settings.Settings(test_cycle, response_time=1) 67 | 68 | # Set up level 69 | test_level = 85 70 | 71 | # Build a DPS object. 72 | calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) 73 | 74 | # Compute EP values. 75 | ep_values = calculator.get_ep() 76 | 77 | # Compute talents ranking 78 | main_tree_talents_ranking, off_tree_talents_ranking = calculator.get_talents_ranking() 79 | 80 | # Compute DPS Breakdown. 81 | dps_breakdown = calculator.get_dps_breakdown() 82 | total_dps = sum(entry[1] for entry in dps_breakdown.items()) 83 | 84 | def pretty_print(dict_list): 85 | max_len = 0 86 | for i in dict_list: 87 | dict_values = i.items() 88 | if max_len < max(len(entry[0]) for entry in dict_values): 89 | max_len = max(len(entry[0]) for entry in dict_values) 90 | 91 | print '-' * (max_len + 15) 92 | for i in dict_list: 93 | dict_values = i.items() 94 | dict_values.sort(key=lambda entry: entry[1], reverse=True) 95 | for value in dict_values: 96 | print value[0] + ':' + ' ' * (max_len - len(value[0])), value[1] 97 | print '-' * (max_len + 15) 98 | 99 | return max_len 100 | 101 | dicts_for_pretty_print = [ 102 | off_tree_talents_ranking, 103 | ep_values, 104 | dps_breakdown 105 | ] 106 | print ' ' * (pretty_print(dicts_for_pretty_print) + 1), total_dps, _("total damage per second.") 107 | -------------------------------------------------------------------------------- /scripts/mop_subtlety.py: -------------------------------------------------------------------------------- 1 | # Simple test program to debug + play with assassination models. 2 | from os import path 3 | import sys 4 | #sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) 5 | 6 | from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator 7 | from shadowcraft.calcs.rogue.Aldriana import settings 8 | 9 | from shadowcraft.objects import buffs 10 | from shadowcraft.objects import race 11 | from shadowcraft.objects import stats 12 | from shadowcraft.objects import procs 13 | from shadowcraft.objects import talents 14 | from shadowcraft.objects import glyphs 15 | 16 | from shadowcraft.core import i18n 17 | 18 | # Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. 19 | test_language = 'local' 20 | i18n.set_language(test_language) 21 | 22 | # Set up level/class/race 23 | test_level = 90 24 | test_race = race.Race('night_elf') 25 | test_class = 'rogue' 26 | 27 | # Set up buffs. 28 | test_buffs = buffs.Buffs( 29 | 'short_term_haste_buff', 30 | 'stat_multiplier_buff', 31 | 'crit_chance_buff', 32 | 'mastery_buff', 33 | 'melee_haste_buff', 34 | 'attack_power_buff', 35 | 'armor_debuff', 36 | 'physical_vulnerability_debuff', 37 | 'spell_damage_debuff', 38 | 'agi_flask', 39 | 'guild_feast' 40 | ) 41 | 42 | # Set up weapons. 43 | test_mh = stats.Weapon(6733, 1.8, 'dagger', 'dancing_steel') 44 | test_oh = stats.Weapon(6733, 1.8, 'dagger', 'dancing_steel') 45 | 46 | # Set up procs. 47 | test_procs = procs.ProcsList('heroic_bottle_of_infinite_stars', 'relic_of_xuen') 48 | 49 | # Set up gear buffs. 50 | test_gear_buffs = stats.GearBuffs('rogue_t14_2pc', 'rogue_t14_4pc', 'leather_specialization', 'virmens_bite', 'virmens_bite_prepot', 'chaotic_metagem') 51 | 52 | # Set up a calcs object.. 53 | # str, agi, int, spirit, stam, ap, crit, hit, exp,haste, mast, mh, oh, procs, gear_buffs 54 | test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, 55 | str=80, 56 | agi=19000, 57 | crit=4800, 58 | hit=2550, 59 | exp=2550, 60 | haste=3000, 61 | mastery=5000) 62 | 63 | # Initialize talents.. 64 | test_talents = talents.Talents('022211', test_class, test_level) 65 | 66 | # Set up glyphs. 67 | glyph_list = [] 68 | test_glyphs = glyphs.Glyphs(test_class, *glyph_list) 69 | 70 | # Set up settings. 71 | test_cycle = settings.SubtletyCycle(5) 72 | test_settings = settings.Settings(test_cycle, response_time=.5, duration=360, dmg_poison='dp', utl_poison='lp', is_pvp=False) 73 | 74 | # Build a DPS object. 75 | calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) 76 | 77 | # Compute EP values. 78 | ep_values = calculator.get_ep() 79 | 80 | # Compute DPS Breakdown. 81 | dps_breakdown = calculator.get_dps_breakdown() 82 | total_dps = sum(entry[1] for entry in dps_breakdown.items()) 83 | talent_ranks = calculator.get_talents_ranking() 84 | 85 | def max_length(dict_list): 86 | max_len = 0 87 | for i in dict_list: 88 | dict_values = i.items() 89 | if max_len < max(len(entry[0]) for entry in dict_values): 90 | max_len = max(len(entry[0]) for entry in dict_values) 91 | 92 | return max_len 93 | 94 | def pretty_print(dict_list): 95 | max_len = max_length(dict_list) 96 | 97 | for i in dict_list: 98 | dict_values = i.items() 99 | dict_values.sort(key=lambda entry: entry[1], reverse=True) 100 | for value in dict_values: 101 | if ("{0:.2f}".format(float(value[1])/total_dps)) != '0.00': 102 | print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' ('+str( "{0:.2f}".format(100*float(value[1])/total_dps) )+'%)' 103 | else: 104 | print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) 105 | print '-' * (max_len + 15) 106 | 107 | dicts_for_pretty_print = [ 108 | ep_values, 109 | talent_ranks, 110 | dps_breakdown 111 | ] 112 | pretty_print(dicts_for_pretty_print) 113 | print ' ' * (max_length(dicts_for_pretty_print) + 1), total_dps, _("total damage per second.") 114 | -------------------------------------------------------------------------------- /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.crit_chance_buff) 15 | 16 | def test_stat_multiplier(self): 17 | self.assertEqual(self.buffs.stat_multiplier(), 1.05) 18 | 19 | def test_all_damage_multiplier(self): 20 | self.assertEqual(self.buffs.all_damage_multiplier(), 1.03) 21 | 22 | def test_spell_damage_multiplier(self): 23 | self.assertEqual(self.buffs.spell_damage_multiplier(), 1.08 * 1.03) 24 | 25 | def test_physical_damage_multiplier(self): 26 | self.assertEqual(self.buffs.physical_damage_multiplier(), 1.04 * 1.03) 27 | 28 | def test_bleed_damage_multiploer(self): 29 | self.assertEqual(self.buffs.bleed_damage_multiplier(), 1.30 * 1.03 * 1.04) 30 | 31 | def test_attack_power_multiplier(self): 32 | self.assertEqual(self.buffs.attack_power_multiplier(), 1.1) 33 | 34 | def test_melee_haste_multiplier(self): 35 | self.assertEqual(self.buffs.melee_haste_multiplier(), 1.1) 36 | 37 | def test_buff_str(self): 38 | self.assertEqual(self.buffs.buff_str(), 549) 39 | 40 | def test_buff_agi(self): 41 | self.assertEqual(self.buffs.buff_agi(), 549 + 90 + 300) 42 | 43 | def test_buff_all_crit(self): 44 | self.assertEqual(self.buffs.buff_all_crit(), 0.05) 45 | 46 | def test_buff_spell_crit(self): 47 | self.assertEqual(self.buffs.buff_spell_crit(), 0.05) 48 | 49 | def test_armor_reduction_multiplier(self): 50 | self.assertEqual(self.buffs.armor_reduction_multiplier(), 0.88) 51 | 52 | 53 | class TestBuffsFalse(unittest.TestCase): 54 | def setUp(self): 55 | self.buffs = buffs.Buffs() 56 | 57 | def test__getattr__(self): 58 | self.assertRaises(AttributeError, self.buffs.__getattr__, 'fake_buff') 59 | self.assertFalse(self.buffs.bleed_damage_debuff) 60 | 61 | def test_stat_multiplier(self): 62 | self.assertEqual(self.buffs.stat_multiplier(), 1.0) 63 | 64 | def test_all_damage_multiplier(self): 65 | self.assertEqual(self.buffs.all_damage_multiplier(), 1.0) 66 | 67 | def test_spell_damage_multiplier(self): 68 | self.assertEqual(self.buffs.spell_damage_multiplier(), 1.0) 69 | 70 | def test_physical_damage_multiplier(self): 71 | self.assertEqual(self.buffs.physical_damage_multiplier(), 1.0) 72 | 73 | def test_bleed_damage_multiploer(self): 74 | self.assertEqual(self.buffs.bleed_damage_multiplier(), 1.0) 75 | 76 | def test_attack_power_multiplier(self): 77 | self.assertEqual(self.buffs.attack_power_multiplier(), 1.0) 78 | 79 | def test_melee_haste_multiplier(self): 80 | self.assertEqual(self.buffs.melee_haste_multiplier(), 1.0) 81 | 82 | def test_buff_str(self): 83 | self.assertEqual(self.buffs.buff_str(), 0) 84 | 85 | def test_buff_agi(self): 86 | self.assertEqual(self.buffs.buff_agi(), 0) 87 | 88 | def test_buff_all_crit(self): 89 | self.assertEqual(self.buffs.buff_all_crit(), 0.0) 90 | 91 | def test_buff_spell_crit(self): 92 | self.assertEqual(self.buffs.buff_spell_crit(), 0.0) 93 | 94 | def test_armor_reduction_multiplier(self): 95 | self.assertEqual(self.buffs.armor_reduction_multiplier(), 1.0) 96 | 97 | class TestBuffsLevel(unittest.TestCase): 98 | def setUp(self): 99 | self.buffs = buffs.Buffs('str_and_agi_buff') 100 | 101 | def test(self): 102 | self.assertEqual(self.buffs.buff_agi(), 549) 103 | self.assertEqual(self.buffs.buff_str(), 549) 104 | self.buffs.level = 80 105 | self.assertEqual(self.buffs.buff_agi(), 155) 106 | self.assertEqual(self.buffs.buff_str(), 155) 107 | 108 | def test_exception(self): 109 | self.assertRaises(exceptions.InvalidLevelException, self.buffs.__setattr__, 'level', 86) 110 | -------------------------------------------------------------------------------- /shadowcraft/objects/talents_data.py: -------------------------------------------------------------------------------- 1 | talents = { 2 | 'death_knight': ( 3 | ('roiling_blood', 'plague_leech', 'unholy_blight'), 4 | ('lichborne', 'anti-magic_zone', 'purgatory'), 5 | ('deaths_advance', 'chilblains', 'asphyxiate'), 6 | ('death_pact', 'death_siphon', 'conversion'), 7 | ('blood_tap', 'runic_empowerment', 'runic_corruption'), 8 | ('gorefiends_grasp', 'remorseless_winter', 'desecrated_ground') 9 | ), 10 | 'druid': ( 11 | ('feline_swiftness', 'displacer_beast', 'wild_charge'), 12 | ('natures_swiftness', 'renewal', 'cenarion_ward'), 13 | ('faerie_swarm', 'mass_entanglement', 'typhoon'), 14 | ('soul_of_the_forest', 'incarnation', 'force_of_nature'), 15 | ('disorienting_roar', 'ursols_vortex', 'mighty_bash'), 16 | ('heart_of_the_wild', 'dream_of_cenarius', 'natures_vigil') 17 | ), 18 | 'hunter': ( 19 | ('posthaste', 'narrow_escape', 'crouching_tiger_hidden_chimera'), 20 | ('silencing_shot', 'wyvern_sting', 'binding_shot'), 21 | ('exhilaration', 'aspect_of_the_iron_hawk', 'spirit_bond'), 22 | ('fervor', 'readiness', 'thrill_of_the_hunt'), 23 | ('a_murder_of_crows', 'dire_beast', 'lynx_rush'), 24 | ('glaive_toss', 'powershot', 'barrage') 25 | ), 26 | 'mage': ( 27 | ('presence_of_mind', 'scorch', 'ice_floes'), 28 | ('temporal_shield', 'blazing_speed', 'ice_barrier'), 29 | ('ring_of_frost', 'ice_ward', 'frostjaw'), 30 | ('greater_invisibility', 'cauterize', 'cold_snap'), 31 | ('nether_tempest', 'living_bomb', 'frost_bomb'), 32 | ('invocation', 'rune_of_power', 'incanters_ward') 33 | ), 34 | 'monk': ( 35 | ('celerity', 'tigers_lust', 'momentum'), 36 | ('chi_wave', 'zen_sphere', 'chi_burst'), 37 | ('power_strikes', 'ascension', 'chi_brew'), 38 | ('deadly_reach', 'charging_ox_wave', 'leg_sweep'), 39 | ('healing_elixirs', 'dampen_harm', 'diffuse_magic'), 40 | ('rushing_jade_wind', 'invoke_xuen,_the_white_tiger', 'chi_torpedo') 41 | ), 42 | 'paladin': ( 43 | ('speed_of_light', 'long_arm_of_the_law', 'pursuit_of_justice'), 44 | ('fist_of_justice', 'repentance', 'burden_of_guilt'), 45 | ('selfless_healer', 'eternal_flame', 'sacred_shield'), 46 | ('hand_of_purity', 'unbreakable_spirit', 'clemency'), 47 | ('holy_avenger', 'sanctified_wrath', 'divine_purpose'), 48 | ('holy_prism', 'lights_hammer', 'execution_sentence') 49 | ), 50 | 'priest': ( 51 | ('void_tendrils', 'psyfiend', 'dominate_mind'), 52 | ('body_and_soul', 'angelic_feather', 'phantasm'), 53 | ('from_darkness_comes_light', 'mindbender', 'power_word:_solace'), 54 | ('desperate_prayer', 'spectral_guise', 'angelic_bulwark'), 55 | ('twist_of_fate', 'power_infusion', 'divine_insight'), 56 | ('cascade', 'divine_star', 'halo') 57 | ), 58 | 'rogue': ( 59 | ('nightstalker', 'subterfuge', 'shadow_focus'), 60 | ('deadly_throw', 'nerve_strike', 'combat_readiness'), 61 | ('cheat_death', 'leeching_poison', 'elusiveness'), 62 | ('preparation', 'shadowstep', 'burst_of_speed'), 63 | ('prey_on_the_weak', 'paralytic_poison', 'dirty_tricks'), 64 | ('shuriken_toss', 'versatility', 'anticipation') 65 | ), 66 | 'shaman': ( 67 | ('natures_guardian', 'stone_bulwark_totem', 'astral_shift'), 68 | ('frozen_power', 'earthgrab_totem', 'windwalk_totem'), 69 | ('call_of_the_elements', 'totemic_restoration', 'totemic_projection'), 70 | ('elemental_mastery', 'ancestral_swiftness', 'echo_of_the_elements'), 71 | ('healing_tide_totem', 'ancestral_guidance', 'conductivity'), 72 | ('unleashed_fury', 'primal_elementalist', 'elemental_blast') 73 | ), 74 | 'warlock': ( 75 | ('dark_regeneration', 'soul_leech', 'harvest_life'), 76 | ('howl_of_terror', 'mortal_coil', 'shadowfury'), 77 | ('soul_link', 'sacrificial_pact', 'dark_bargain'), 78 | ('blood_fear', 'burning_rush', 'unbound_will'), 79 | ('grimoire_of_supremacy', 'grimoire_of_service', 'grimoire_of_sacrifice'), 80 | ('archimondes_vengeance', 'kiljaedens_cunning', 'mannoroths_fury') 81 | ), 82 | 'warrior': ( 83 | ('juggernaut', 'double_time', 'warbringer'), 84 | ('enraged_regeneration', 'second_wind', 'impending_victory'), 85 | ('staggering_shout', 'piercing_howl', 'disrupting_shout'), 86 | ('bladestorm', 'shockwave', 'dragon_roar'), 87 | ('mass_spell_reflection', 'safeguard', 'vigilance'), 88 | ('avatar', 'bloodbath', 'storm_bolt') 89 | ), 90 | } 91 | -------------------------------------------------------------------------------- /scripts/assassination.py: -------------------------------------------------------------------------------- 1 | # Simple test program to debug + play with assassination models. 2 | from os import path 3 | import sys 4 | sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) 5 | 6 | from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator 7 | from shadowcraft.calcs.rogue.Aldriana import settings 8 | 9 | from shadowcraft.objects import buffs 10 | from shadowcraft.objects import race 11 | from shadowcraft.objects import stats 12 | from shadowcraft.objects import procs 13 | from shadowcraft.objects.rogue import rogue_talents 14 | from shadowcraft.objects.rogue import rogue_glyphs 15 | 16 | from shadowcraft.core import i18n 17 | 18 | # Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. 19 | test_language = 'local' 20 | i18n.set_language(test_language) 21 | 22 | # Set up buffs. 23 | test_buffs = buffs.Buffs( 24 | 'short_term_haste_buff', 25 | 'stat_multiplier_buff', 26 | 'crit_chance_buff', 27 | 'all_damage_buff', 28 | 'melee_haste_buff', 29 | 'attack_power_buff', 30 | 'str_and_agi_buff', 31 | 'armor_debuff', 32 | 'physical_vulnerability_debuff', 33 | 'spell_damage_debuff', 34 | 'spell_crit_debuff', 35 | 'bleed_damage_debuff', 36 | 'agi_flask', 37 | 'guild_feast' 38 | ) 39 | 40 | # Set up weapons. 41 | test_mh = stats.Weapon(1121, 1.8, 'dagger', 'landslide') 42 | test_oh = stats.Weapon(872, 1.4, 'dagger', 'landslide') 43 | test_ranged = stats.Weapon(1679.5, 2.0, 'thrown') 44 | 45 | # Set up procs. 46 | test_procs = procs.ProcsList('heroic_the_hungerer', 'heroic_matrix_restabilizer') 47 | 48 | # Set up gear buffs. 49 | test_gear_buffs = stats.GearBuffs('rogue_t12_2pc', 'rogue_t12_4pc', 'leather_specialization', 'potion_of_the_tolvir', 'chaotic_metagem') 50 | 51 | # Set up a calcs object.. 52 | test_stats = stats.Stats(20, 6248, 190, 624, 1331, 297, 1719, 2032, test_mh, test_oh, test_ranged, test_procs, test_gear_buffs) 53 | 54 | # Initialize talents.. 55 | test_talents = rogue_talents.RogueTalents('0333230113022110321', '0020000000000000000', '2030030000000000000') 56 | 57 | # Set up glyphs. 58 | glyph_list = ['backstab', 'mutilate', 'rupture', 'tricks_of_the_trade'] 59 | test_glyphs = rogue_glyphs.RogueGlyphs(*glyph_list) 60 | 61 | # Set up race. 62 | test_race = race.Race('night_elf') 63 | 64 | # Set up settings. 65 | test_cycle = settings.AssassinationCycle() 66 | test_settings = settings.Settings(test_cycle, response_time=1, duration=360) 67 | 68 | # Set up level 69 | test_level = 85 70 | 71 | # Build a DPS object. 72 | calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) 73 | 74 | # Compute EP values. 75 | ep_values = calculator.get_ep() 76 | 77 | # Compute talents ranking 78 | main_tree_talents_ranking, off_tree_talents_ranking = calculator.get_talents_ranking() 79 | 80 | # Compute glyphs ranking 81 | glyps_ranking = calculator.get_glyphs_ranking(['vendetta', 'backstab']) 82 | 83 | # Compute EP values for procs and gear buffs 84 | tier_ep_values = calculator.get_other_ep(['rogue_t11_4pc', 'rogue_t11_2pc', 'rogue_t12_4pc', 'rogue_t12_2pc']) 85 | metagem_ep_value = calculator.get_other_ep(['chaotic_metagem']) 86 | trinkets_list = [ 87 | 'the_hungerer', 88 | 'matrix_restabilizer', 89 | 'rickets_magnetic_fireball_proc', 90 | 'heroic_the_hungerer', 91 | 'heroic_matrix_restabilizer' 92 | ] 93 | trinkets_ep_value = calculator.get_other_ep(trinkets_list) 94 | 95 | # Compute weapon ep 96 | mh_enchants_and_dps_ep_values, oh_enchants_and_dps_ep_values = calculator.get_weapon_ep(dps=True, enchants=True) 97 | mh_speed_ep_values, oh_speed_ep_values = calculator.get_weapon_ep([1.4, 1.8]) 98 | 99 | # Compute DPS Breakdown. 100 | dps_breakdown = calculator.get_dps_breakdown() 101 | total_dps = sum(entry[1] for entry in dps_breakdown.items()) 102 | 103 | def max_length(dict_list): 104 | max_len = 0 105 | for i in dict_list: 106 | dict_values = 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 = i.items() 117 | dict_values.sort(key=lambda entry: entry[1], reverse=True) 118 | for value in dict_values: 119 | print value[0] + ':' + ' ' * (max_len - len(value[0])), value[1] 120 | print '-' * (max_len + 15) 121 | 122 | print '-' * (max_length([trinkets_ep_value]) + 15) 123 | pretty_print([trinkets_ep_value]) 124 | 125 | dicts_for_pretty_print = [ 126 | glyps_ranking, 127 | off_tree_talents_ranking, 128 | mh_enchants_and_dps_ep_values, 129 | oh_enchants_and_dps_ep_values, 130 | mh_speed_ep_values, 131 | oh_speed_ep_values, 132 | tier_ep_values, 133 | metagem_ep_value, 134 | ep_values, 135 | dps_breakdown 136 | ] 137 | pretty_print(dicts_for_pretty_print) 138 | print ' ' * (max_length(dicts_for_pretty_print) + 1), total_dps, _("total damage per second.") 139 | -------------------------------------------------------------------------------- /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 glyphs 9 | 10 | class TestDamageCalculator(unittest.TestCase): 11 | def make_calculator(self, buffs_list=[], gear_buffs_list=[], race_name='night_elf'): 12 | test_buffs = buffs.Buffs(*buffs_list) 13 | test_gear_buffs = stats.GearBuffs(*gear_buffs_list) 14 | test_procs = procs.ProcsList() 15 | test_mh = stats.Weapon(737, 1.8, 'dagger', 'hurricane') 16 | test_oh = stats.Weapon(573, 1.4, 'dagger', 'hurricane') 17 | test_ranged = stats.Weapon(1104, 2.0, 'thrown') 18 | test_stats = stats.Stats(20, 3485, 190, 1517, 1086, 641, 899, 666, test_mh, test_oh, test_ranged, test_procs, test_gear_buffs) 19 | test_race = race.Race(race_name) 20 | test_talents = None 21 | test_glyphs = glyphs.Glyphs() 22 | return calcs.DamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race) 23 | 24 | def setUp(self): 25 | self.calculator = self.make_calculator() 26 | 27 | def test_melee_hit_chance(self): 28 | pass 29 | 30 | def test_one_hand_melee_hit_chance(self): 31 | self.assertAlmostEqual( 32 | self.calculator.one_hand_melee_hit_chance(dodgeable=False, parryable=False), 33 | 1.0) 34 | self.assertAlmostEqual( 35 | self.calculator.one_hand_melee_hit_chance(dodgeable=True, parryable=False), 36 | 1.0 - (0.065 - (641 / (30.027200698852539 * 4)) * 0.01)) 37 | self.calculator.stats.exp = 0 38 | self.assertAlmostEqual( 39 | self.calculator.one_hand_melee_hit_chance(dodgeable=True, parryable=False), 40 | 1.0 - 0.065) 41 | self.assertAlmostEqual( 42 | self.calculator.one_hand_melee_hit_chance(dodgeable=True, parryable=True), 43 | 1.0 - 0.14 - 0.065) 44 | self.assertAlmostEqual( 45 | self.calculator.one_hand_melee_hit_chance(dodgeable=False, parryable=True), 46 | 1.0 - 0.14) 47 | self.calculator.stats.hit = 0 48 | self.assertAlmostEqual( 49 | self.calculator.one_hand_melee_hit_chance(dodgeable=True, parryable=False), 50 | 1.0 - 0.065 - 0.08) 51 | 52 | def test_dual_wield_mh_hit_chance(self): 53 | self.assertAlmostEqual( 54 | self.calculator.dual_wield_mh_hit_chance(dodgeable=False, parryable=False), 55 | 1.0 - (0.27 - 0.01 * (1086 / 120.109001159667969))) 56 | self.calculator.stats.hit = 0 57 | self.calculator.stats.exp = 0 58 | self.assertAlmostEqual( 59 | self.calculator.dual_wield_mh_hit_chance(dodgeable=False, parryable=False), 60 | 1.0 - 0.27) 61 | self.assertAlmostEqual( 62 | self.calculator.dual_wield_mh_hit_chance(dodgeable=True, parryable=False), 63 | 1.0 - 0.27 - 0.065) 64 | self.assertAlmostEqual( 65 | self.calculator.dual_wield_mh_hit_chance(dodgeable=True, parryable=True), 66 | 1.0 - 0.27 - 0.065 - 0.14) 67 | self.assertAlmostEqual( 68 | self.calculator.dual_wield_mh_hit_chance(dodgeable=False, parryable=True), 69 | 1.0 - 0.27 - 0.14) 70 | 71 | def test_dual_wield_oh_hit_chance(self): 72 | pass 73 | 74 | def test_spell_hit_chance(self): 75 | self.assertAlmostEqual(self.calculator.spell_hit_chance(), 76 | 1.0 - (0.17 - 0.01 * (1086 / 102.445999145507812))) 77 | 78 | def test_buff_melee_crit(self): 79 | pass 80 | 81 | def test_buff_spell_crit(self): 82 | pass 83 | 84 | def test_target_armor(self): 85 | pass 86 | 87 | def test_raid_settings_modifiers(self): 88 | self.assertRaises(exceptions.InvalidInputException, self.calculator.raid_settings_modifiers, '') 89 | 90 | def test_mixology_no_flask(self): 91 | test_calculator = self.make_calculator(gear_buffs_list=['mixology']) 92 | self.assertEqual(test_calculator.stats.agi, self.calculator.stats.agi) 93 | 94 | def test_mixology(self): 95 | test_calculator = self.make_calculator(buffs_list=['agi_flask'], gear_buffs_list=['mixology']) 96 | self.assertEqual(test_calculator.stats.agi, self.calculator.stats.agi + 80) 97 | 98 | def test_master_of_anatomy(self): 99 | test_calculator = self.make_calculator(gear_buffs_list=['master_of_anatomy']) 100 | self.assertEqual(test_calculator.stats.crit, self.calculator.stats.crit + 80) 101 | 102 | def test_get_all_activated_stat_boosts(self): 103 | calculator = self.make_calculator(gear_buffs_list=['leather_specialization', 'potion_of_the_tolvir'], race_name='orc') 104 | boosts = calculator.get_all_activated_stat_boosts() 105 | self.assertEqual(len(boosts), 3) # blood fury sp, blood fury ap, potion of the tolvir 106 | for boost in boosts: 107 | if boost['stat'] == 'ap': 108 | self.assertEqual(boost['value'], 1170) 109 | elif boost['stat'] == 'sp': 110 | self.assertEqual(boost['value'], 585) 111 | elif boost['stat'] == 'agi': 112 | self.assertEqual(boost['value'], 1200) 113 | -------------------------------------------------------------------------------- /scripts/mop_subtlety_import.py: -------------------------------------------------------------------------------- 1 | # Simple test program to debug + play with subtlety models. 2 | from os import path 3 | import sys 4 | from import_character import CharacterData 5 | from char_info import charInfo 6 | #sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) 7 | 8 | from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator 9 | from shadowcraft.calcs.rogue.Aldriana import settings 10 | 11 | from shadowcraft.objects import buffs 12 | from shadowcraft.objects import race 13 | from shadowcraft.objects import stats 14 | from shadowcraft.objects import procs 15 | from shadowcraft.objects import proc_data 16 | from shadowcraft.objects import talents 17 | from shadowcraft.objects import glyphs 18 | 19 | from shadowcraft.core import i18n 20 | 21 | # Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. 22 | test_language = 'local' 23 | i18n.set_language(test_language) 24 | 25 | key = 1 26 | while key < len(sys.argv): 27 | terms = sys.argv[key].split(':') 28 | charInfo[ terms[0] ] = terms[1] 29 | key += 1 30 | 31 | print "Loading " + charInfo['name'] + " of " + charInfo['region'] + "-" + charInfo['realm'] + "\n" 32 | character_data = CharacterData(charInfo['region'], charInfo['realm'], charInfo['name'], verbose=charInfo['verbose']) 33 | character_data.do_import() 34 | 35 | 36 | # Set up level/class/race 37 | test_level = 90 38 | test_race = race.Race(character_data.get_race()) 39 | test_class = 'rogue' 40 | 41 | # Set up buffs. 42 | test_buffs = buffs.Buffs( 43 | 'short_term_haste_buff', 44 | 'stat_multiplier_buff', 45 | 'crit_chance_buff', 46 | 'mastery_buff', 47 | 'melee_haste_buff', 48 | 'attack_power_buff', 49 | 'armor_debuff', 50 | 'physical_vulnerability_debuff', 51 | 'spell_damage_debuff', 52 | 'agi_flask_mop', 53 | 'food_300_agi' 54 | ) 55 | 56 | # Set up weapons. 57 | 58 | test_mh = stats.Weapon(*character_data.get_mh()) 59 | test_oh = stats.Weapon(*character_data.get_oh()) 60 | 61 | # Set up procs. 62 | character_procs = character_data.get_procs() 63 | character_procs_allowed = filter(lambda p: p in proc_data.allowed_procs, character_procs) 64 | 65 | #not_allowed_procs = set(character_procs) - set(character_procs_allowed) 66 | #print not_allowed_procs 67 | 68 | test_procs = procs.ProcsList(*character_procs_allowed) 69 | 70 | # Set up a calcs object.. 71 | lst = character_data.get_gear_stats() 72 | 73 | # Set up gear buffs. 74 | character_gear_buffs = character_data.get_gear_buffs() + ['leather_specialization', 'virmens_bite', 'virmens_bite_prepot'] 75 | if character_data.has_chaotic_metagem(): 76 | character_gear_buffs.append('chaotic_metagem') 77 | test_gear_buffs = stats.GearBuffs(*character_gear_buffs) 78 | 79 | test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, **lst) 80 | 81 | # Initialize talents.. 82 | if charInfo['talents'] == None: 83 | charInfo['talents'] = character_data.get_talents() 84 | test_talents = talents.Talents(charInfo['talents'], test_class, test_level) 85 | 86 | # Set up glyphs. 87 | glyph_list = character_data.get_glyphs() 88 | test_glyphs = glyphs.Glyphs(test_class, *glyph_list) 89 | 90 | # Set up settings. 91 | raid_crits_per_second = 5 92 | hemo_interval = 24 #'always', 'never', 24, 25, 26... 93 | if not character_data.get_mh_type() == 'dagger' and not test_talents.shuriken_toss: 94 | if not hemo_interval == 'always': 95 | print "\nALERT: Viable dagger cycle not found, forced rotation to strictly Hemo \n" 96 | hemo_interval = 'always' 97 | test_cycle = settings.SubtletyCycle(raid_crits_per_second, use_hemorrhage=hemo_interval) 98 | test_settings = settings.Settings(test_cycle, response_time=.5, duration=360, dmg_poison='dp', utl_poison='lp', is_pvp=charInfo['pvp'], 99 | stormlash=charInfo['stormlash'], shiv_interval=charInfo['shiv']) 100 | 101 | # Build a DPS object. 102 | calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) 103 | 104 | # Compute EP values. 105 | ep_values = calculator.get_ep() 106 | 107 | # Compute DPS Breakdown. 108 | dps_breakdown = calculator.get_dps_breakdown() 109 | total_dps = sum(entry[1] for entry in dps_breakdown.items()) 110 | talent_ranks = calculator.get_talents_ranking() 111 | heal_sum, heal_table = calculator.get_self_healing(dps_breakdown=dps_breakdown) 112 | 113 | 114 | def max_length(dict_list): 115 | max_len = 0 116 | for i in dict_list: 117 | dict_values = i.items() 118 | if max_len < max(len(entry[0]) for entry in dict_values): 119 | max_len = max(len(entry[0]) for entry in dict_values) 120 | 121 | return max_len 122 | 123 | def pretty_print(dict_list): 124 | max_len = max_length(dict_list) 125 | 126 | for i in dict_list: 127 | dict_values = i.items() 128 | dict_values.sort(key=lambda entry: entry[1], reverse=True) 129 | for value in dict_values: 130 | if ("{0:.2f}".format(float(value[1])/total_dps)) != '0.00': 131 | print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' ('+str( "{0:.2f}".format(100*float(value[1])/total_dps) )+'%)' 132 | else: 133 | print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) 134 | print '-' * (max_len + 15) 135 | 136 | dicts_for_pretty_print = [ 137 | ep_values, 138 | talent_ranks, 139 | heal_table, 140 | dps_breakdown 141 | ] 142 | pretty_print(dicts_for_pretty_print) 143 | print ' ' * (max_length(dicts_for_pretty_print) + 1), total_dps, _("total damage per second.") 144 | -------------------------------------------------------------------------------- /scripts/mop_combat_import.py: -------------------------------------------------------------------------------- 1 | # Simple test program to debug + play with combat models. 2 | from os import path 3 | import sys 4 | from import_character import CharacterData 5 | from char_info import charInfo 6 | #sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) 7 | 8 | from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator 9 | from shadowcraft.calcs.rogue.Aldriana import settings 10 | 11 | from shadowcraft.objects import buffs 12 | from shadowcraft.objects import race 13 | from shadowcraft.objects import stats 14 | from shadowcraft.objects import procs 15 | from shadowcraft.objects import proc_data 16 | from shadowcraft.objects import talents 17 | from shadowcraft.objects import glyphs 18 | 19 | from shadowcraft.core import i18n 20 | 21 | # Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. 22 | test_language = 'local' 23 | i18n.set_language(test_language) 24 | 25 | 26 | key = 1 27 | while key < len(sys.argv): 28 | terms = sys.argv[key].split(':') 29 | if terms[0] in ['stormlash', 'shiv']: 30 | charInfo[ terms[0] ] = float(terms[1]) 31 | else: 32 | charInfo[ terms[0] ] = terms[1] 33 | key += 1 34 | 35 | print "Loading " + charInfo['name'] + " of " + charInfo['region'] + "-" + charInfo['realm'] + "\n" 36 | character_data = CharacterData(charInfo['region'], charInfo['realm'], charInfo['name'], verbose=charInfo['verbose']) 37 | character_data.do_import() 38 | 39 | 40 | # Set up level/class/race 41 | test_level = 90 42 | test_race = race.Race(character_data.get_race()) 43 | test_class = 'rogue' 44 | 45 | # Set up buffs. 46 | test_buffs = buffs.Buffs( 47 | 'short_term_haste_buff', 48 | 'stat_multiplier_buff', 49 | 'crit_chance_buff', 50 | 'mastery_buff', 51 | 'melee_haste_buff', 52 | 'attack_power_buff', 53 | 'armor_debuff', 54 | 'physical_vulnerability_debuff', 55 | 'spell_damage_debuff', 56 | 'agi_flask_mop', 57 | 'food_300_agi' 58 | ) 59 | 60 | # Set up weapons. 61 | test_mh = stats.Weapon(*character_data.get_mh()) 62 | test_oh = stats.Weapon(*character_data.get_oh()) 63 | 64 | # Set up procs. 65 | character_procs = character_data.get_procs() 66 | character_procs_allowed = filter(lambda p: p in proc_data.allowed_procs, character_procs) 67 | 68 | #not_allowed_procs = set(character_procs) - set(character_procs_allowed) 69 | #print not_allowed_procs 70 | 71 | test_procs = procs.ProcsList(*character_procs_allowed) 72 | 73 | # Set up a calcs object.. 74 | lst = character_data.get_gear_stats() 75 | 76 | # Set up gear buffs. 77 | character_gear_buffs = character_data.get_gear_buffs() + ['leather_specialization', 'virmens_bite', 'virmens_bite_prepot'] 78 | if character_data.has_chaotic_metagem(): 79 | character_gear_buffs.append('chaotic_metagem') 80 | test_gear_buffs = stats.GearBuffs(*character_gear_buffs) 81 | 82 | test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, **lst) 83 | 84 | # Initialize talents.. 85 | if charInfo['talents'] == None: 86 | charInfo['talents'] = character_data.get_talents() 87 | test_talents = talents.Talents(charInfo['talents'], test_class, test_level) 88 | 89 | # Set up glyphs. 90 | glyph_list = character_data.get_glyphs() 91 | test_glyphs = glyphs.Glyphs(test_class, *glyph_list) 92 | 93 | # Set up settings. 94 | if character_data.get_mh_type() == 'dagger': 95 | print "\nALERT: Dagger found. Playing combat with a dagger should be a last resort, and is not recommended. \n" 96 | test_cycle = settings.CombatCycle(use_rupture=True, ksp_immediately=True, revealing_strike_pooling=True, blade_flurry=False) 97 | test_settings = settings.Settings(test_cycle, response_time=.5, duration=360, dmg_poison='dp', utl_poison='lp', is_pvp=charInfo['pvp'], 98 | stormlash=charInfo['stormlash'], shiv_interval=charInfo['shiv']) 99 | 100 | # Build a DPS object. 101 | calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) 102 | 103 | # Compute EP values. 104 | ep_values = calculator.get_ep() 105 | 106 | # Compute DPS Breakdown. 107 | dps_breakdown = calculator.get_dps_breakdown() 108 | total_dps = sum(entry[1] for entry in dps_breakdown.items()) 109 | talent_ranks = calculator.get_talents_ranking() 110 | heal_sum, heal_table = calculator.get_self_healing(dps_breakdown=dps_breakdown) 111 | 112 | # Compute weapon type modifier. 113 | weapon_type_mod = calculator.get_oh_weapon_modifier() 114 | 115 | def max_length(dict_list): 116 | max_len = 0 117 | for i in dict_list: 118 | dict_values = i.items() 119 | if max_len < max(len(entry[0]) for entry in dict_values): 120 | max_len = max(len(entry[0]) for entry in dict_values) 121 | 122 | return max_len 123 | 124 | def pretty_print(dict_list): 125 | max_len = max_length(dict_list) 126 | 127 | for i in dict_list: 128 | dict_values = i.items() 129 | dict_values.sort(key=lambda entry: entry[1], reverse=True) 130 | for value in dict_values: 131 | if ("{0:.2f}".format(float(value[1])/total_dps)) != '0.00': 132 | print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' ('+str( "{0:.2f}".format(100*float(value[1])/total_dps) )+'%)' 133 | else: 134 | print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) 135 | print '-' * (max_len + 15) 136 | 137 | dicts_for_pretty_print = [ 138 | weapon_type_mod, 139 | ep_values, 140 | talent_ranks, 141 | heal_table, 142 | dps_breakdown 143 | ] 144 | pretty_print(dicts_for_pretty_print) 145 | print ' ' * (max_length(dicts_for_pretty_print) + 1), total_dps, _("total damage per second.") 146 | -------------------------------------------------------------------------------- /shadowcraft/calcs/rogue/Aldriana/settings.py: -------------------------------------------------------------------------------- 1 | from shadowcraft.core import exceptions 2 | 3 | class Settings(object): 4 | # Settings object for AldrianasRogueDamageCalculator. 5 | 6 | def __init__(self, cycle, time_in_execute_range=.35, tricks_on_cooldown=True, response_time=.5, dmg_poison='dp', utl_poison=None, 7 | duration=300, use_opener='always', opener_name='default', is_pvp=False, stormlash=False, shiv_interval=0): 8 | self.cycle = cycle 9 | self.time_in_execute_range = time_in_execute_range 10 | self.tricks_on_cooldown = tricks_on_cooldown 11 | self.response_time = response_time 12 | self.dmg_poison = dmg_poison 13 | self.utl_poison = utl_poison 14 | self.duration = duration 15 | self.use_opener = use_opener # Allowed values are 'always' (vanish/shadowmeld on cooldown), 'opener' (once per fight) and 'never' 16 | self.opener_name = opener_name 17 | self.is_pvp = is_pvp 18 | self.use_stormlash = stormlash 19 | self.shiv_interval = int(shiv_interval) 20 | if self.shiv_interval < 10 and not self.shiv_interval == 0: 21 | self.shiv_interval = 10 22 | allowed_openers_per_spec = { 23 | 'assassination': tuple(['mutilate']), 24 | 'combat': ('sinister_strike', 'revealing_strike'), 25 | 'subtlety': () 26 | } 27 | allowed_openers = allowed_openers_per_spec[self.get_spec()] + ('ambush', 'garrote', 'default', 'cpg') 28 | if opener_name not in allowed_openers: 29 | raise exceptions.InvalidInputException(_('Opener {opener} is not allowed in {cycle} cycles.').format(opener=opener_name, cycle=self.get_spec())) 30 | if opener_name == 'default': 31 | default_openers = { 32 | 'assassination': 'ambush', 33 | 'combat': 'sinister_strike', 34 | 'subtlety': 'ambush'} 35 | self.opener_name = default_openers[self.get_spec()] 36 | if dmg_poison not in (None, 'dp', 'wp'): 37 | raise exceptions.InvalidInputException(_('You can only choose Deadly(dp) or Wound(wp) as a damage poison')) 38 | if utl_poison not in (None, 'cp', 'mnp', 'lp', 'pp'): 39 | raise exceptions.InvalidInputException(_('You can only choose Crippling(cp), Mind-Numbing(mnp), Leeching(lp) or Paralytic(pp) as a non-lethal poison')) 40 | 41 | def get_spec(self): 42 | return self.cycle._cycle_type 43 | 44 | def is_assassination_rogue(self): 45 | return self.get_spec() == 'assassination' 46 | 47 | def is_combat_rogue(self): 48 | return self.get_spec() == 'combat' 49 | 50 | def is_subtlety_rogue(self): 51 | return self.get_spec() == 'subtlety' 52 | 53 | class Cycle(object): 54 | # Base class for cycle objects. Can't think of anything that particularly 55 | # needs to go here yet, but it seems worth keeping options open in that 56 | # respect. 57 | 58 | # When subclassing, define _cycle_type to be one of 'assassination', 59 | # 'combat', or 'subtlety' - this is how the damage calculator makes sure 60 | # you have an appropriate cycle object to go with your talent trees, etc. 61 | _cycle_type = '' 62 | 63 | 64 | class AssassinationCycle(Cycle): 65 | _cycle_type = 'assassination' 66 | 67 | allowed_values = (1, 2, 3, 4, 5) 68 | 69 | def __init__(self, min_envenom_size_non_execute=4, min_envenom_size_execute=5, prioritize_rupture_uptime_non_execute=True, prioritize_rupture_uptime_execute=True): 70 | assert min_envenom_size_non_execute in self.allowed_values 71 | self.min_envenom_size_non_execute = min_envenom_size_non_execute 72 | 73 | assert min_envenom_size_execute in self.allowed_values 74 | self.min_envenom_size_execute = min_envenom_size_execute 75 | 76 | # There are two fundamental ways you can manage rupture; one is to 77 | # reapply with whatever CP you have as soon as you can after the old 78 | # rupture drops; we will call this priorotizing uptime over size. 79 | # The second is to use ruptures that are the same size as your 80 | # envenoms, which we will call prioritizing size over uptime. True 81 | # means the first of these options; False means the second. 82 | # There are theoretically other things you can do (say, 4+ envenom and 83 | # 5+ ruptures) but such things are significantly harder to model so I'm 84 | # not going to worry about them until we have reason to believe they're 85 | # actually better. 86 | self.prioritize_rupture_uptime_non_execute = prioritize_rupture_uptime_non_execute 87 | self.prioritize_rupture_uptime_execute = prioritize_rupture_uptime_execute 88 | 89 | 90 | class CombatCycle(Cycle): 91 | _cycle_type = 'combat' 92 | 93 | def __init__(self, use_rupture=True, ksp_immediately=True, revealing_strike_pooling=True, blade_flurry=False): 94 | self.blade_flurry = bool(blade_flurry) 95 | self.use_rupture = bool(use_rupture) 96 | self.ksp_immediately = bool(ksp_immediately) # Determines whether to KSp the instant it comes off cool or wait until Bandit's Guile stacks up.' 97 | self.revealing_strike_pooling = revealing_strike_pooling 98 | 99 | class SubtletyCycle(Cycle): 100 | _cycle_type = 'subtlety' 101 | 102 | def __init__(self, raid_crits_per_second, clip_recuperate=False, use_hemorrhage='24'): 103 | self.raid_crits_per_second = raid_crits_per_second 104 | self.clip_recuperate = clip_recuperate # Determines if you clip the previous recuperate or wait for it to drop before reapplying. 105 | self.use_hemorrhage = use_hemorrhage # Allowed values are 'always' (main CP generator), 'never' (default to backstab), or a number denoting the interval in seconds between applications. 106 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/mop_assassination_import.py: -------------------------------------------------------------------------------- 1 | # Simple test program to debug + play with assassination models. 2 | from os import path 3 | import sys 4 | from import_character import CharacterData 5 | from char_info import charInfo 6 | 7 | 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 proc_data 18 | from shadowcraft.objects import talents 19 | from shadowcraft.objects import glyphs 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 | 28 | key = 1 29 | while key < len(sys.argv): 30 | terms = sys.argv[key].split(':') 31 | charInfo[ terms[0] ] = terms[1] 32 | key += 1 33 | 34 | print "Loading " + charInfo['name'] + " of " + charInfo['region'] + "-" + charInfo['realm'] + "\n" 35 | character_data = CharacterData(charInfo['region'], charInfo['realm'], charInfo['name'], verbose=charInfo['verbose']) 36 | character_data.do_import() 37 | 38 | 39 | # Set up level/class/race 40 | test_level = 90 41 | test_race = race.Race(character_data.get_race()) 42 | test_class = 'rogue' 43 | 44 | # Set up buffs. 45 | test_buffs = buffs.Buffs( 46 | 'short_term_haste_buff', 47 | 'stat_multiplier_buff', 48 | 'crit_chance_buff', 49 | 'mastery_buff', 50 | 'melee_haste_buff', 51 | 'attack_power_buff', 52 | 'armor_debuff', 53 | 'physical_vulnerability_debuff', 54 | 'spell_damage_debuff', 55 | 'agi_flask_mop', 56 | 'food_300_agi' 57 | ) 58 | 59 | # Set up weapons. 60 | test_mh = stats.Weapon(*character_data.get_mh()) 61 | test_oh = stats.Weapon(*character_data.get_oh()) 62 | 63 | # Set up procs. 64 | character_procs = character_data.get_procs() 65 | character_procs_allowed = filter(lambda p: p in proc_data.allowed_procs, character_procs) 66 | 67 | #not_allowed_procs = set(character_procs) - set(character_procs_allowed) 68 | #print not_allowed_procs 69 | 70 | test_procs = procs.ProcsList(*character_procs_allowed) 71 | 72 | # Set up a calcs object.. 73 | lst = character_data.get_gear_stats() 74 | 75 | # Set up gear buffs. 76 | character_gear_buffs = character_data.get_gear_buffs() + ['leather_specialization', 'virmens_bite', 'virmens_bite_prepot'] 77 | if character_data.has_chaotic_metagem(): 78 | character_gear_buffs.append('chaotic_metagem') 79 | test_gear_buffs = stats.GearBuffs(*character_gear_buffs) 80 | 81 | test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, **lst) 82 | 83 | # Initialize talents.. 84 | if charInfo['talents'] == None: 85 | charInfo['talents'] = character_data.get_talents() 86 | test_talents = talents.Talents(charInfo['talents'], test_class, test_level) 87 | 88 | # Set up glyphs. 89 | glyph_list = character_data.get_glyphs() 90 | test_glyphs = glyphs.Glyphs(test_class, *glyph_list) 91 | 92 | # Set up settings. 93 | test_cycle = settings.AssassinationCycle(min_envenom_size_non_execute=4, min_envenom_size_execute=5, 94 | prioritize_rupture_uptime_non_execute=True, prioritize_rupture_uptime_execute=True) 95 | test_settings = settings.Settings(test_cycle, response_time=.5, duration=360, dmg_poison='dp', utl_poison='lp', is_pvp=charInfo['pvp'], 96 | stormlash=charInfo['stormlash'], shiv_interval=charInfo['shiv']) 97 | 98 | # Build a DPS object. 99 | #calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level, char_class=test_class) 100 | calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) 101 | 102 | # Compute EP values. 103 | ep_values = calculator.get_ep() 104 | 105 | # Compute DPS Breakdown. 106 | dps_breakdown = calculator.get_dps_breakdown() 107 | non_execute_breakdown = calculator.assassination_dps_breakdown_non_execute() 108 | total_dps = sum(entry[1] for entry in dps_breakdown.items()) 109 | non_execute_total = sum(entry[1] for entry in non_execute_breakdown.items()) 110 | talent_ranks = calculator.get_talents_ranking() 111 | heal_sum, heal_table = calculator.get_self_healing(dps_breakdown=dps_breakdown) 112 | 113 | def max_length(dict_list): 114 | max_len = 0 115 | for i in dict_list: 116 | dict_values = i.items() 117 | if max_len < max(len(entry[0]) for entry in dict_values): 118 | max_len = max(len(entry[0]) for entry in dict_values) 119 | 120 | return max_len 121 | 122 | def pretty_print(dict_list, total_sum = 1.): 123 | max_len = max_length(dict_list) 124 | 125 | for i in dict_list: 126 | dict_values = i.items() 127 | dict_values.sort(key=lambda entry: entry[1], reverse=True) 128 | for value in dict_values: 129 | #print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) 130 | if ("{0:.2f}".format(10*float(value[1])/total_dps)) != '0.00': 131 | print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' ('+str( "{0:.2f}".format(100*float(value[1])/total_sum) )+'%)' 132 | else: 133 | print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) 134 | print '-' * (max_len + 15) 135 | 136 | dicts_for_pretty_print = [ 137 | ep_values, 138 | talent_ranks, 139 | heal_table, 140 | dps_breakdown 141 | ] 142 | pretty_print(dicts_for_pretty_print, total_sum=total_dps) 143 | print ' ' * (max_length(dicts_for_pretty_print) + 1), total_dps, _("total damage per second.") 144 | print '' 145 | print 'non-execute breakdown: ' 146 | pretty_print([non_execute_breakdown], total_sum=non_execute_total) 147 | print ' ' * (max_length([non_execute_breakdown]) + 1), non_execute_total, _("total damage per second.") 148 | -------------------------------------------------------------------------------- /shadowcraft/objects/buffs.py: -------------------------------------------------------------------------------- 1 | from shadowcraft.core import exceptions 2 | 3 | class InvalidBuffException(exceptions.InvalidInputException): 4 | pass 5 | 6 | 7 | class Buffs(object): 8 | 9 | allowed_buffs = frozenset([ 10 | 'short_term_haste_buff', # Heroism/Blood Lust, Time Warp 11 | 'stat_multiplier_buff', # Mark of the Wild, Blessing of Kings, Legacy of the Emperor 12 | 'crit_chance_buff', # Leader of the Pack, Legacy of the White Tiger, Arcane Brillance 13 | 'melee_haste_buff', # Swiftblade's Cunning, Unholy Aura 14 | 'attack_power_buff', # Horn of Winter, Trueshot Aura, Battle Shout 15 | 'mastery_buff', # Blessing of Might, Grace of Air 16 | 'spell_haste_buff', # Moonkin Form, Shadowform 17 | 'spell_power_buff', # Dark Intent, Arcane Brillance 18 | 'stamina_buff', # PW: Fortitude, Blood Pact, Commanding Shout 19 | 'armor_debuff', # Sunder, Expose Armor, Faerie Fire 20 | 'physical_vulnerability_debuff', # Brittle Bones, Ebon Plaguebringer, Judgments of the Bold, Colossus Smash 21 | 'spell_damage_debuff', # Master Poisoner, Curse of Elements 22 | 'weakened_blows_debuff', 23 | 'slow_casting_debuff', 24 | 'mortal_wounds_debuff', 25 | 'agi_flask', # Flask of the Winds (cata) 26 | 'guild_feast', # Seafood Magnifique Feast (cata) 27 | 'agi_flask_mop', # Flask of Spring Blossoms 28 | 'food_250', # Pandaren Banquet 29 | 'food_275', # Pandaren Banquet 30 | 'food_300_agi' # Sea Mist Rice Noodles 31 | ]) 32 | 33 | buffs_debuffs = frozenset([ 34 | 'short_term_haste_buff', # Heroism/Blood Lust, Time Warp 35 | 'stat_multiplier_buff', # Mark of the Wild, Blessing of Kings, Legacy of the Emperor 36 | 'crit_chance_buff', # Leader of the Pack, Legacy of the White Tiger, Arcane Brillance 37 | 'melee_haste_buff', # Swiftblade's Cunning, Unholy Aura 38 | 'attack_power_buff', # Horn of Winter, Trueshot Aura, Battle Shout 39 | 'mastery_buff', # Blessing of Might, Grace of Air 40 | 'spell_haste_buff', # Moonkin Form, Shadowform 41 | 'spell_power_buff', # Dark Intent, Arcane Brillance 42 | 'stamina_buff', # PW: Fortitude, Blood Pact, Commanding Shout 43 | 'armor_debuff', # Sunder, Expose Armor, Faerie Fire 44 | 'physical_vulnerability_debuff', # Brittle Bones, Ebon Plaguebringer, Judgments of the Bold, Colossus Smash 45 | 'spell_damage_debuff', # Master Poisoner, Curse of Elements 46 | 'weakened_blows_debuff', 47 | 'slow_casting_debuff', 48 | 'mortal_wounds_debuff', 49 | ]) 50 | 51 | buff_scaling = {80: 131, 85: 509, 90: 1710} 52 | 53 | def __init__(self, *args, **kwargs): 54 | for buff in args: 55 | if buff not in self.allowed_buffs: 56 | raise InvalidBuffException(_('Invalid buff {buff}').format(buff=buff)) 57 | setattr(self, buff, True) 58 | self.level = kwargs.get('level', 85) 59 | 60 | def __getattr__(self, name): 61 | # Any buff we haven't assigned a value to, we don't have. 62 | if name in self.allowed_buffs: 63 | return False 64 | object.__getattribute__(self, name) 65 | 66 | def __setattr__(self, name, value): 67 | object.__setattr__(self, name, value) 68 | if name == 'level': 69 | self._set_constants_for_level() 70 | 71 | def _set_constants_for_level(self): 72 | try: 73 | self.mast_buff_bonus = round(1.7545000315 * self.buff_scaling[self.level]) 74 | except KeyError as e: 75 | raise exceptions.InvalidLevelException(_('No conversion factor available for level {level}').format(level=self.level)) 76 | 77 | def get_max_buffs(self): 78 | return frozenset(buffs_debuffs + ['food_300_agi', 'agi_flask_mop']) 79 | 80 | def stat_multiplier(self): 81 | return [1, 1.05][self.stat_multiplier_buff] 82 | 83 | def spell_damage_multiplier(self): 84 | return [1, 1.08][self.spell_damage_debuff] 85 | 86 | def physical_damage_multiplier(self): 87 | return [1, 1.04][self.physical_vulnerability_debuff] 88 | 89 | def bleed_damage_multiplier(self): 90 | return self.physical_damage_multiplier() 91 | 92 | def attack_power_multiplier(self): 93 | return [1, 1.1][self.attack_power_buff] 94 | 95 | def melee_haste_multiplier(self): 96 | return [1, 1.1][self.melee_haste_buff] 97 | 98 | def buff_str(self): 99 | return 0 100 | 101 | def buff_agi(self, just_food=False): 102 | flask_agi = 0 103 | if not just_food: 104 | if self.agi_flask_mop + self.agi_flask > 1: 105 | raise InvalidBuffException(_('You can only have one type of Flask active')) 106 | flask_agi += [0, 300][self.agi_flask] 107 | flask_agi += [0, 1000][self.agi_flask_mop] 108 | 109 | if self.guild_feast + self.food_250 + self.food_275 + self.food_300_agi > 1: 110 | raise InvalidBuffException(_('You can only have one type of Well Fed buff active')) 111 | food_agi = 0 112 | food_agi += 90 * self.guild_feast 113 | food_agi += 250 * self.food_250 114 | food_agi += 275 * self.food_275 115 | food_agi += 300 * self.food_300_agi 116 | 117 | return food_agi + flask_agi 118 | 119 | def buff_all_crit(self): 120 | return [0, .05][self.crit_chance_buff] 121 | 122 | def buff_spell_crit(self): 123 | return 0 124 | 125 | def armor_reduction_multiplier(self): 126 | return [1, .88][self.armor_debuff] 127 | 128 | def buff_mast(self): 129 | return [0, self.mast_buff_bonus][self.mastery_buff] 130 | -------------------------------------------------------------------------------- /scripts/mop_combat.py: -------------------------------------------------------------------------------- 1 | # Simple test program to debug + play with assassination models. 2 | from os import path 3 | import sys 4 | sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) 5 | 6 | from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator 7 | from shadowcraft.calcs.rogue.Aldriana import settings 8 | 9 | from shadowcraft.objects import buffs 10 | from shadowcraft.objects import race 11 | from shadowcraft.objects import stats 12 | from shadowcraft.objects import procs 13 | from shadowcraft.objects import talents 14 | from shadowcraft.objects import glyphs 15 | 16 | from shadowcraft.core import i18n 17 | 18 | # Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. 19 | test_language = 'local' 20 | i18n.set_language(test_language) 21 | 22 | # Set up level/class/race 23 | test_level = 90 24 | test_race = race.Race('night_elf') 25 | test_class = 'rogue' 26 | 27 | # Set up buffs. 28 | test_buffs = buffs.Buffs( 29 | 'short_term_haste_buff', 30 | 'stat_multiplier_buff', 31 | 'crit_chance_buff', 32 | 'mastery_buff', 33 | 'melee_haste_buff', 34 | 'attack_power_buff', 35 | 'armor_debuff', 36 | 'physical_vulnerability_debuff', 37 | 'spell_damage_debuff', 38 | 'agi_flask_mop', 39 | 'food_300_agi' 40 | ) 41 | 42 | # Set up weapons. 43 | test_mh = stats.Weapon(9725.5, 2.6, 'fist', 'dancing_steel') 44 | test_oh = stats.Weapon(9725.5, 2.6, 'fist', 'dancing_steel') 45 | 46 | # Set up procs. 47 | test_procs = procs.ProcsList('heroic_bottle_of_infinite_stars', 'heroic_terror_in_the_mists') 48 | 49 | # Set up gear buffs. 50 | test_gear_buffs = stats.GearBuffs('rogue_t14_2pc', 'rogue_t14_4pc', 'leather_specialization', 'virmens_bite', 'virmens_bite_prepot', 'chaotic_metagem') 51 | 52 | # Set up a calcs object.. 53 | test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, 54 | str=80, 55 | agi=16695, 56 | crit=3209, 57 | hit=2570, 58 | exp=2547, 59 | haste=7721, 60 | mastery=5220) 61 | 62 | # Initialize talents.. 63 | test_talents = talents.Talents('322213', test_class, test_level) 64 | 65 | # Set up glyphs. 66 | glyph_list = ['recuperate'] 67 | test_glyphs = glyphs.Glyphs(test_class, *glyph_list) 68 | 69 | # Set up settings. 70 | test_cycle = settings.CombatCycle() 71 | test_settings = settings.Settings(test_cycle, response_time=.5, duration=360, dmg_poison='dp', utl_poison='lp', is_pvp=False) 72 | 73 | # Build a DPS object. 74 | calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) 75 | 76 | # Compute EP values. 77 | ep_values = calculator.get_ep() 78 | tier_ep_values = calculator.get_other_ep(['rogue_t14_4pc', 'rogue_t14_2pc']) 79 | mh_enchants_and_dps_ep_values, oh_enchants_and_dps_ep_values = calculator.get_weapon_ep(dps=True, enchants=True) 80 | 81 | trinkets_list = [ 82 | 'heroic_bottle_of_infinite_stars', 83 | 'bottle_of_infinite_stars', 84 | 'lfr_bottle_of_infinite_stars', 85 | 'heroic_terror_in_the_mists', 86 | 'terror_in_the_mists', 87 | 'lfr_terror_in_the_mists', 88 | 'relic_of_xuen', 89 | 'windswept_pages', 90 | 'jade_bandit_figurine', 91 | 'hawkmasters_talon', 92 | 'windswept_pages', 93 | 'searing_words', 94 | 'flashing_steel_talisman' 95 | ] 96 | trinkets_ep_value = calculator.get_other_ep(trinkets_list) 97 | 98 | trinkets_ep_value['heroic_bottle_of_infinite_stars'] += 731 * ep_values['mastery'] + 487 * ep_values['haste'] 99 | trinkets_ep_value['bottle_of_infinite_stars'] += 648 * ep_values['mastery'] + 431 * ep_values['haste'] 100 | trinkets_ep_value['lfr_bottle_of_infinite_stars'] += 574 * ep_values['mastery'] + 382 * ep_values['haste'] 101 | trinkets_ep_value['heroic_terror_in_the_mists'] += 1300 * ep_values['agi'] 102 | trinkets_ep_value['terror_in_the_mists'] += 1152 * ep_values['agi'] 103 | trinkets_ep_value['lfr_terror_in_the_mists'] += 1021 * ep_values['agi'] 104 | trinkets_ep_value['relic_of_xuen'] += 956 * ep_values['agi'] 105 | trinkets_ep_value['windswept_pages'] += 847 * ep_values['agi'] 106 | trinkets_ep_value['jade_bandit_figurine'] += 1079 * ep_values['agi'] 107 | trinkets_ep_value['hawkmasters_talon'] += 1079 * ep_values['agi'] 108 | trinkets_ep_value['searing_words'] += 509 * ep_values['crit'] + 338 * ep_values['haste'] 109 | trinkets_ep_value['flashing_steel_talisman'] += 847 * ep_values['haste'] 110 | 111 | # Compute DPS Breakdown. 112 | dps_breakdown = calculator.get_dps_breakdown() 113 | total_dps = sum(entry[1] for entry in dps_breakdown.items()) 114 | 115 | # Compute weapon type modifier. 116 | weapon_type_mod = calculator.get_oh_weapon_modifier() 117 | 118 | def max_length(dict_list): 119 | max_len = 0 120 | for i in dict_list: 121 | dict_values = i.items() 122 | if max_len < max(len(entry[0]) for entry in dict_values): 123 | max_len = max(len(entry[0]) for entry in dict_values) 124 | 125 | return max_len 126 | 127 | def pretty_print(dict_list, total_sum = 1., show_percent=False): 128 | max_len = max_length(dict_list) 129 | 130 | for i in dict_list: 131 | dict_values = i.items() 132 | dict_values.sort(key=lambda entry: entry[1], reverse=True) 133 | for value in dict_values: 134 | #print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) 135 | if show_percent and ("{0:.2f}".format(float(value[1])/total_dps)) != '0.00': 136 | print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' ('+str( "{0:.2f}".format(100*float(value[1])/total_sum) )+'%)' 137 | else: 138 | print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) 139 | print '-' * (max_len + 15) 140 | 141 | dicts_for_pretty_print = [ 142 | ep_values, 143 | tier_ep_values, 144 | mh_enchants_and_dps_ep_values, 145 | oh_enchants_and_dps_ep_values, 146 | trinkets_ep_value, 147 | ] 148 | pretty_print(dicts_for_pretty_print) 149 | pretty_print([dps_breakdown], total_sum=total_dps, show_percent=True) 150 | print ' ' * (max_length(dicts_for_pretty_print) + 1), total_dps, _("total damage per second.") -------------------------------------------------------------------------------- /shadowcraft/objects/class_data.py: -------------------------------------------------------------------------------- 1 | from shadowcraft.core import exceptions 2 | 3 | class Util(object): 4 | # This should have its own GameClass(object) architecture at some point. 5 | 6 | GAME_CLASS_NUMBER = { 7 | 1:"warrior", 2:"paladin", 3:"hunter", 4:"rogue", 8 | 5:"priest", 6:"death_knight", 7:"shaman", 8:"mage", 9:"warlock", 9 | 10:"monk", 11:"druid" 10 | } 11 | 12 | AGI_CRIT_INTERCEPT_VALUES = [ 13 | 0.000000000000000, 0.050000000745058, 0.050000000745058, -0.015300000086427, -0.003000000026077, 14 | 0.031800001859665, 0.050000000745058, 0.029200000688434, 0.034499999135733, 0.026200000196695, 15 | 0.074799999594688, 0.074799999594688, 16 | ] 17 | 18 | SPELL_SCALING_VALUES = [ 19 | [], 20 | [], 21 | [], 22 | [], 23 | [ 24 | 37.297119140625000, 38.664085388183594, 39.575397491455078, 40.182941436767578, 40.587966918945312, 25 | 41.398025512695312, 43.448474884033203, 45.498928070068359, 47.549381256103516, 49.599834442138672, 26 | 51.650287628173828, 53.700740814208984, 55.751190185546875, 59.506370544433594, 63.464359283447266, 27 | 67.625152587890625, 71.988761901855469, 76.555175781250000, 81.324394226074219, 86.296417236328125, 28 | 90.327865600585938, 94.427680969238281, 98.588249206542969, 102.801979064941406, 107.061256408691406, 29 | 111.358489990234375, 115.686058044433594, 120.036361694335938, 124.401786804199219, 128.774749755859375, 30 | 133.147628784179688, 137.512817382812500, 141.862716674804688, 146.189712524414062, 150.486206054687500, 31 | 154.744598388671875, 158.957290649414062, 163.116653442382812, 167.215087890625000, 171.244995117187500, 32 | 182.127532958984375, 193.476638793945312, 205.302459716796875, 217.615112304687500, 230.424774169921875, 33 | 243.741546630859375, 257.575592041015625, 271.937042236328125, 286.836029052734375, 302.282745361328125, 34 | 318.287261962890625, 334.859741210937500, 352.010345458984375, 369.749206542968750, 388.086425781250000, 35 | 407.032226562500000, 426.596649169921875, 446.789916992187500, 510.141021728515625, 532.945800781250000, 36 | 552.214721679687500, 571.938354492187500, 592.121704101562500, 612.769897460937500, 633.887939453125000, 37 | 655.481018066406250, 677.554077148437500, 700.112243652343750, 723.160583496093750, 746.704162597656250, 38 | 770.748107910156250, 795.297363281250000, 820.357116699218750, 845.932373046875000, 872.028198242187500, 39 | 898.649719238281250, 925.802001953125000, 953.490051269531250, 981.718994140625000, 1010.493896484375000, 40 | 1032.933593750000000, 1055.626708984375000, 1078.573486328125000, 1101.773681640625000, 1125.227416992187500, 41 | 1148.934570312500000, 1172.895385742187500, 1197.109619140625000, 1221.577270507812500, 1246.298583984375000, 42 | ], 43 | [], 44 | [], 45 | [], 46 | [], 47 | [], 48 | [], 49 | [], 50 | ] 51 | 52 | AGI_PER_CRIT_VALUES = [ 53 | [], 54 | [], 55 | [], 56 | [], 57 | [ 58 | 2.234094142913818, 2.331228971481323, 2.428363323211670, 2.622632265090942, 2.719766616821289, 59 | 2.816901445388794, 3.011170148849487, 3.108304500579834, 3.205439567565918, 3.399708032608032, 60 | 3.788246154785156, 4.176784992218018, 4.662456989288330, 5.050994873046875, 5.633802890777588, 61 | 6.022340297698975, 6.410876750946045, 6.896551609039307, 7.382225990295410, 7.867895126342773, 62 | 8.353569984436035, 8.742107391357422, 9.227782249450684, 9.616318702697754, 10.199128150939941, 63 | 10.684799194335938, 11.073339462280273, 11.559009552001953, 12.044680595397949, 12.627490043640137, 64 | 13.016019821166992, 13.501690864562988, 13.987363815307617, 14.473036766052246, 15.055842399597168, 65 | 15.541511535644531, 15.930060386657715, 16.415725708007812, 16.901399612426758, 17.484230041503906, 66 | 17.969871520996094, 18.455564498901367, 18.941223144531250, 19.524042129516602, 20.106847763061523, 67 | 20.592529296875000, 21.078191757202148, 21.563854217529297, 22.049549102783203, 22.729494094848633, 68 | 23.215160369873047, 23.700838088989258, 24.283632278442383, 24.769273757934570, 25.352140426635742, 69 | 25.934886932373047, 26.420568466186523, 27.003381729125977, 27.489067077636719, 28.169013977050781, 70 | 29.917457580566406, 31.083024978637695, 32.637184143066406, 33.802856445312500, 34.968460083007812, 71 | 36.134086608886719, 37.299655914306641, 38.173912048339844, 39.048007965087891, 40.019371032714844, 72 | 43.030555725097656, 46.333190917968750, 49.830078125000000, 53.618152618408203, 57.697746276855469, 73 | 62.069007873535156, 66.828392028808594, 71.879356384277344, 77.319190979003906, 83.146942138671875, 74 | 108.695663452148438, 142.857147216796875, 188.679244995117188, 250.000000000000000, 322.580657958984375, 75 | 422.838226318359375, 555.987243652343750, 730.520507812500000, 959.033020019531250, 1259.518066406250000, 76 | ], 77 | [], 78 | [], 79 | [], 80 | [], 81 | [], 82 | [], 83 | [], 84 | ] 85 | 86 | def get_class_number(self, game_class): 87 | for i in self.GAME_CLASS_NUMBER.keys(): 88 | if self.GAME_CLASS_NUMBER[i] == game_class: 89 | return i 90 | raise exceptions.InvalidInputException(_('{game_class} is not a supported game class').format(game_class=game_class)) 91 | 92 | def get_spell_scaling(self, game_class, level): 93 | return self.SPELL_SCALING_VALUES[self.get_class_number(game_class)][level - 1] 94 | 95 | def get_agi_per_crit(self, game_class, level): 96 | return self.AGI_PER_CRIT_VALUES[self.get_class_number(game_class)][level - 1] 97 | 98 | def get_agi_intercept(self, game_class): 99 | return self.AGI_CRIT_INTERCEPT_VALUES[self.get_class_number(game_class)] 100 | -------------------------------------------------------------------------------- /tests/objects_tests/stats_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from shadowcraft.core import exceptions 3 | from shadowcraft.objects import stats 4 | from shadowcraft.objects import procs 5 | 6 | class TestStats(unittest.TestCase): 7 | def setUp(self): 8 | self.stats = stats.Stats(20, 3485, 190, 1517, 1086, 641, 899, 666, None, None, None, None, None) 9 | 10 | def test_stats(self): 11 | self.assertEqual(self.stats.agi, 3485) 12 | 13 | def test_set_constants_for_level(self): 14 | self.assertRaises(exceptions.InvalidLevelException, self.stats.__setattr__, 'level', 86) 15 | 16 | def test_get_mastery_from_rating(self): 17 | self.assertAlmostEqual(self.stats.get_mastery_from_rating(), 8 + 666 / 179.279998779296875) 18 | self.assertAlmostEqual(self.stats.get_mastery_from_rating(100), 8 + 100 / 179.279998779296875) 19 | self.stats.level = 80 20 | self.assertAlmostEqual(self.stats.get_mastery_from_rating(), 8 + 666 / 45.906) 21 | self.assertAlmostEqual(self.stats.get_mastery_from_rating(100), 8 + 100 / 45.906) 22 | 23 | def test_get_melee_hit_from_rating(self): 24 | self.assertAlmostEqual(self.stats.get_melee_hit_from_rating(), .01 * 1086 / 120.109001159667969) 25 | self.assertAlmostEqual(self.stats.get_melee_hit_from_rating(100), .01 * 100 / 120.109001159667969) 26 | 27 | def test_get_expertise_from_rating(self): 28 | self.assertAlmostEqual(self.stats.get_expertise_from_rating(), .01 * 641 / (30.027200698852539 * 4)) 29 | self.assertAlmostEqual(self.stats.get_expertise_from_rating(100), .01 * 100 / (30.027200698852539 * 4)) 30 | 31 | def test_get_spell_hit_from_rating(self): 32 | self.assertAlmostEqual(self.stats.get_spell_hit_from_rating(), .01 * 1086 / 102.445999145507812) 33 | self.assertAlmostEqual(self.stats.get_spell_hit_from_rating(100, 0), .01 * 100 / 102.445999145507812) 34 | 35 | def test_get_crit_from_rating(self): 36 | self.assertAlmostEqual(self.stats.get_crit_from_rating(), .01 * 1517 / 179.279998779296875) 37 | self.assertAlmostEqual(self.stats.get_crit_from_rating(100), .01 * 100 / 179.279998779296875) 38 | 39 | def test_get_haste_multiplier_from_rating(self): 40 | self.assertAlmostEqual(self.stats.get_haste_multiplier_from_rating(), 1 + .01 * 899 / 128.057006835937500) 41 | self.assertAlmostEqual(self.stats.get_haste_multiplier_from_rating(100), 1 + .01 * 100 / 128.057006835937500) 42 | 43 | 44 | class TestWeapon(unittest.TestCase): 45 | def setUp(self): 46 | self.mh = stats.Weapon(1000, 2.0, 'dagger', 'hurricane') 47 | self.ranged = stats.Weapon(1104, 2.0, 'thrown') 48 | 49 | def test___init__(self): 50 | self.assertAlmostEqual(self.mh._normalization_speed, 1.7) 51 | self.assertAlmostEqual(self.mh.speed, 2.0) 52 | self.assertAlmostEqual(self.mh.weapon_dps, 1000 / 2.0) 53 | self.assertEqual(self.mh.type, 'dagger') 54 | self.assertRaises(exceptions.InvalidInputException, stats.Weapon, 1000, 2.0, 'thrown', 'fake_enchant') 55 | self.assertAlmostEqual(self.ranged._normalization_speed, 2.1) 56 | mh = stats.Weapon(1000, 1.8, 'dagger', 'hurricane') 57 | self.assertAlmostEqual(mh.hurricane.proc_rate(speed=1.8), 1 * 1.8 / 60) 58 | oh = stats.Weapon(1000, 1.4, 'dagger', 'hurricane') 59 | self.assertAlmostEqual(mh.hurricane.proc_rate(speed=1.8), 1 * 1.8 / 60) 60 | self.assertAlmostEqual(oh.hurricane.proc_rate(speed=1.4), 1 * 1.4 / 60) 61 | 62 | def test__getattr__(self): 63 | self.assertTrue(self.mh.hurricane) 64 | self.assertFalse(self.mh.landslide) 65 | self.assertRaises(AttributeError, self.mh.__getattr__, 'fake_enchant') 66 | 67 | def test_is_melee(self): 68 | self.assertTrue(self.mh.is_melee()) 69 | self.assertFalse(self.ranged.is_melee()) 70 | 71 | def test_damage(self): 72 | self.assertAlmostEqual(self.mh.damage(1000), 2.0 * (1000 / 2.0 + 1000 / 14.0)) 73 | 74 | def test_normalized_damage(self): 75 | self.assertAlmostEqual(self.mh.normalized_damage(1000), 1000 + (1.7 * 1000 / 14.0)) 76 | 77 | def test_weapon_enchant_proc_rate_exception(self): 78 | self.assertRaises(procs.InvalidProcException, self.mh.hurricane.proc_rate) 79 | 80 | 81 | class TestGearBuffs(unittest.TestCase): 82 | def setUp(self): 83 | self.gear = stats.GearBuffs('chaotic_metagem', 'leather_specialization', 'rogue_t11_2pc', 'potion_of_the_tolvir', 'engineer_glove_enchant', 'lifeblood') 84 | self.gear_none = stats.GearBuffs() 85 | 86 | def test__getattr__(self): 87 | self.assertTrue(self.gear.chaotic_metagem) 88 | self.assertTrue(self.gear.leather_specialization) 89 | self.assertFalse(self.gear.unsolvable_riddle) 90 | self.assertRaises(AttributeError, self.gear.__getattr__, 'fake_gear_buff') 91 | 92 | def test_metagem_crit_multiplier(self): 93 | self.assertAlmostEqual(self.gear.metagem_crit_multiplier(), 1.03) 94 | self.assertAlmostEqual(self.gear_none.metagem_crit_multiplier(), 1.0) 95 | 96 | def test_rogue_t11_2pc_crit_bonus(self): 97 | self.assertAlmostEqual(self.gear.rogue_t11_2pc_crit_bonus(), 0.05) 98 | self.assertAlmostEqual(self.gear_none.rogue_t11_2pc_crit_bonus(), 0.0) 99 | 100 | def test_leather_specialization_multiplier(self): 101 | self.assertAlmostEqual(self.gear.leather_specialization_multiplier(), 1.05) 102 | self.assertAlmostEqual(self.gear_none.leather_specialization_multiplier(), 1.0) 103 | 104 | def test_get_all_activated_agi_boosts(self): 105 | self.assertEqual(len(self.gear.get_all_activated_agi_boosts()), 1) 106 | self.assertEqual(len(self.gear_none.get_all_activated_agi_boosts()), 0) 107 | 108 | def test_get_all_activated_boosts_for_stat(self): 109 | self.assertEqual(len(self.gear.get_all_activated_boosts_for_stat('agi')), 1) 110 | self.assertEqual(len(self.gear.get_all_activated_boosts_for_stat('haste')), 2) 111 | self.assertEqual(len(self.gear.get_all_activated_boosts_for_stat('crit')), 0) 112 | 113 | def test_get_all_activated_haste_rating_boosts(self): 114 | self.assertEqual(len(self.gear.get_all_activated_haste_rating_boosts()), 2) 115 | self.assertEqual(len(self.gear_none.get_all_activated_haste_rating_boosts()), 0) 116 | 117 | def test_engineer_glove_enchant(self): 118 | test_gear = stats.GearBuffs('engineer_glove_enchant') 119 | haste_boost = test_gear.get_all_activated_haste_rating_boosts()[0] 120 | self.assertEqual(haste_boost['value'], 340) 121 | self.assertEqual(haste_boost['duration'], 12) 122 | self.assertEqual(haste_boost['cooldown'], 60) 123 | 124 | def test_lifeblood(self): 125 | test_gear = stats.GearBuffs('lifeblood') 126 | haste_boost = test_gear.get_all_activated_haste_rating_boosts()[0] 127 | self.assertEqual(haste_boost['value'], 480) 128 | self.assertEqual(haste_boost['duration'], 20) 129 | self.assertEqual(haste_boost['cooldown'], 120) 130 | 131 | def test_get_all_activated_boosts(self): 132 | self.assertEqual(len(self.gear.get_all_activated_boosts()), 3) 133 | self.assertEqual(len(self.gear_none.get_all_activated_boosts()), 0) 134 | -------------------------------------------------------------------------------- /scripts/mop_assassination.py: -------------------------------------------------------------------------------- 1 | # Simple test program to debug + play with assassination models. 2 | from os import path 3 | import sys 4 | sys.path.append(path.abspath(path.join(path.dirname(__file__), '..'))) 5 | 6 | from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator 7 | from shadowcraft.calcs.rogue.Aldriana import settings 8 | 9 | from shadowcraft.objects import buffs 10 | from shadowcraft.objects import race 11 | from shadowcraft.objects import stats 12 | from shadowcraft.objects import procs 13 | from shadowcraft.objects import talents 14 | from shadowcraft.objects import glyphs 15 | 16 | from shadowcraft.core import i18n 17 | 18 | # Set up language. Use 'en_US', 'es_ES', 'fr' for specific languages. 19 | test_language = 'local' 20 | i18n.set_language(test_language) 21 | 22 | # Set up level/class/race 23 | test_level = 90 24 | test_race = race.Race('night_elf') 25 | test_class = 'rogue' 26 | 27 | # Set up buffs. 28 | test_buffs = buffs.Buffs( 29 | 'short_term_haste_buff', 30 | 'stat_multiplier_buff', 31 | 'crit_chance_buff', 32 | 'mastery_buff', 33 | 'melee_haste_buff', 34 | 'attack_power_buff', 35 | 'armor_debuff', 36 | 'physical_vulnerability_debuff', 37 | 'spell_damage_debuff', 38 | 'agi_flask_mop', 39 | 'food_300_agi' 40 | ) 41 | 42 | # Set up weapons. 43 | test_mh = stats.Weapon(6733, 1.8, 'dagger', 'dancing_steel') 44 | test_oh = stats.Weapon(6733, 1.8, 'dagger', 'dancing_steel') 45 | 46 | # Set up procs. 47 | test_procs = procs.ProcsList('heroic_bottle_of_infinite_stars', 'heroic_terror_in_the_mists') 48 | 49 | # Set up gear buffs. 50 | test_gear_buffs = stats.GearBuffs('rogue_t14_2pc', 'rogue_t14_4pc', 'leather_specialization', 'virmens_bite', 'virmens_bite_prepot', 'chaotic_metagem') 51 | 52 | # Set up a calcs object.. 53 | # str, agi, int, spirit, stam, ap, crit, hit, exp, haste, mast, mh, oh, procs, gear_buffs 54 | test_stats = stats.Stats(test_mh, test_oh, test_procs, test_gear_buffs, 55 | str=80, 56 | agi=17400, 57 | crit=2818, 58 | hit=2559, 59 | exp=2553, 60 | haste=2713, 61 | mastery=9328) 62 | 63 | # Initialize talents.. 64 | test_talents = talents.Talents('322213', test_class, test_level) 65 | 66 | # Set up glyphs. 67 | glyph_list = ['recuperate', 'sprint'] #just to have something 68 | test_glyphs = glyphs.Glyphs(test_class, *glyph_list) 69 | 70 | # Set up settings. 71 | test_cycle = settings.AssassinationCycle(min_envenom_size_non_execute=4, min_envenom_size_execute=5, 72 | prioritize_rupture_uptime_non_execute=True, prioritize_rupture_uptime_execute=True) 73 | test_settings = settings.Settings(test_cycle, response_time=.5, duration=360, dmg_poison='dp', utl_poison='lp', is_pvp=False) 74 | 75 | # Build a DPS object. 76 | calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, test_glyphs, test_buffs, test_race, test_settings, test_level) 77 | 78 | # Compute EP values. 79 | ep_values = calculator.get_ep() 80 | tier_ep_values = calculator.get_other_ep(['rogue_t14_4pc', 'rogue_t14_2pc']) 81 | mh_enchants_and_dps_ep_values, oh_enchants_and_dps_ep_values = calculator.get_weapon_ep(dps=True, enchants=True) 82 | 83 | trinkets_list = [ 84 | 'heroic_bottle_of_infinite_stars', 85 | 'bottle_of_infinite_stars', 86 | 'lfr_bottle_of_infinite_stars', 87 | 'heroic_terror_in_the_mists', 88 | 'terror_in_the_mists', 89 | 'lfr_terror_in_the_mists', 90 | 'relic_of_xuen', 91 | 'windswept_pages', 92 | 'jade_bandit_figurine', 93 | 'hawkmasters_talon', 94 | 'windswept_pages', 95 | 'searing_words', 96 | 'flashing_steel_talisman' 97 | ] 98 | trinkets_ep_value = calculator.get_other_ep(trinkets_list) 99 | 100 | trinkets_ep_value['heroic_bottle_of_infinite_stars'] += 1218 * ep_values['mastery'] 101 | trinkets_ep_value['bottle_of_infinite_stars'] += 1079 * ep_values['mastery'] 102 | trinkets_ep_value['lfr_bottle_of_infinite_stars'] += 956 * ep_values['mastery'] 103 | trinkets_ep_value['heroic_terror_in_the_mists'] += 1300 * ep_values['agi'] 104 | trinkets_ep_value['terror_in_the_mists'] += 1152 * ep_values['agi'] 105 | trinkets_ep_value['lfr_terror_in_the_mists'] += 1021 * ep_values['agi'] 106 | trinkets_ep_value['relic_of_xuen'] += 956 * ep_values['agi'] 107 | trinkets_ep_value['windswept_pages'] += 847 * ep_values['agi'] 108 | trinkets_ep_value['jade_bandit_figurine'] += 1079 * ep_values['agi'] 109 | trinkets_ep_value['hawkmasters_talon'] += 1079 * ep_values['agi'] 110 | trinkets_ep_value['searing_words'] += 509 * ep_values['crit'] + 338 * ep_values['mastery'] 111 | trinkets_ep_value['flashing_steel_talisman'] += 509 * ep_values['haste'] + 338 * ep_values['mastery'] 112 | 113 | # Compute DPS Breakdown. 114 | dps_breakdown = calculator.get_dps_breakdown() 115 | total_dps = sum(entry[1] for entry in dps_breakdown.items()) 116 | 117 | def max_length(dict_list): 118 | max_len = 0 119 | for i in dict_list: 120 | dict_values = i.items() 121 | if max_len < max(len(entry[0]) for entry in dict_values): 122 | max_len = max(len(entry[0]) for entry in dict_values) 123 | 124 | return max_len 125 | 126 | def pretty_print(dict_list, total_sum = 1., show_percent=False): 127 | max_len = max_length(dict_list) 128 | 129 | for i in dict_list: 130 | dict_values = i.items() 131 | dict_values.sort(key=lambda entry: entry[1], reverse=True) 132 | for value in dict_values: 133 | #print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) 134 | if show_percent and ("{0:.2f}".format(float(value[1])/total_dps)) != '0.00': 135 | print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) + ' ('+str( "{0:.2f}".format(100*float(value[1])/total_sum) )+'%)' 136 | else: 137 | print value[0] + ':' + ' ' * (max_len - len(value[0])), str(value[1]) 138 | print '-' * (max_len + 15) 139 | 140 | dicts_for_pretty_print = [ 141 | ep_values, 142 | tier_ep_values, 143 | mh_enchants_and_dps_ep_values, 144 | oh_enchants_and_dps_ep_values, 145 | trinkets_ep_value, 146 | ] 147 | pretty_print(dicts_for_pretty_print) 148 | 149 | pretty_print([dps_breakdown], total_sum=total_dps, show_percent=True) 150 | print ' ' * (max_length([dps_breakdown]) + 1), total_dps, _("total damage per second.") 151 | print '' 152 | 153 | 154 | # non_execute_breakdown = calculator.assassination_dps_breakdown_non_execute() 155 | # non_execute_total = sum(entry[1] for entry in non_execute_breakdown.items()) 156 | # print 'non-execute breakdown: ' 157 | # pretty_print([non_execute_breakdown], total_sum=non_execute_total, show_percent=True) 158 | # print ' ' * (max_length([non_execute_breakdown]) + 1), non_execute_total, _("total damage per second.") 159 | 160 | # execute_breakdown = calculator.assassination_dps_breakdown_execute() 161 | # execute_total = sum(entry[1] for entry in execute_breakdown.items()) 162 | # print 'execute breakdown: ' 163 | # pretty_print([execute_breakdown], total_sum=execute_total, show_percent=True) 164 | # print ' ' * (max_length([execute_breakdown]) + 1), execute_total, _("total damage per second.") 165 | -------------------------------------------------------------------------------- /shadowcraft/core/jsoninput.py: -------------------------------------------------------------------------------- 1 | import json 2 | from shadowcraft.calcs.rogue.Aldriana import AldrianasRogueDamageCalculator 3 | from shadowcraft.calcs.rogue.Aldriana import settings 4 | from shadowcraft.core import exceptions 5 | from shadowcraft.objects import buffs 6 | from shadowcraft.objects import procs 7 | from shadowcraft.objects import race 8 | from shadowcraft.objects import stats 9 | from shadowcraft.objects.rogue import rogue_glyphs 10 | from shadowcraft.objects.rogue import rogue_talents 11 | 12 | class InvalidJSONException(exceptions.InvalidInputException): 13 | pass 14 | 15 | def from_json(json_string, character_class='rogue'): 16 | j = json.loads(json_string) 17 | try: 18 | race_object = race.Race(str(j['race']), character_class=character_class) 19 | level = int(j['level']) 20 | 21 | s = j['settings'] 22 | settings_type = s['type'] 23 | if settings_type == 'assassination': 24 | # AssassinationCycle(self, min_envenom_size_mutilate=4, min_envenom_size_backstab=5, prioritize_rupture_uptime_mutilate=True, prioritize_rupture_uptime_backstab=True) 25 | c = s.get('cycle', {}) 26 | cycle = settings.AssassinationCycle(c.get('min_envenom_size_mutilate', 4), c.get('min_envenom_size_backstab', 5), 27 | c.get('prioritize_rupture_uptime_mutilate', True), c.get('prioritize_rupture_uptime_backstab', True)) 28 | elif settings_type == 'combat': 29 | # CombatCycle(self, use_rupture=True, use_revealing_strike='sometimes', ksp_immediately=False) 30 | c = s.get('cycle', {}) 31 | cycle = settings.CombatCycle(c.get('use_rupture', True), c.get('use_revealing_strike', 'sometimes'), c.get('ksp_immediately', False)) 32 | elif settings_type == 'subtlety': 33 | # SubletySycle(raid_crits_per_second, clip_recuperate=False) 34 | c = s['cycle'] 35 | cycle = settings.SubtletyCycle(c['raid_crits_per_second'], c.get('clip_recuperate', False)) 36 | else: 37 | raise InvalidJSONException(_("Missing settings")) 38 | 39 | # Settings(cycle, time_in_execute_range=.35, tricks_on_cooldown=True, response_time=.5, mh_poison='ip', oh_poison='dp', duration=300): 40 | settings_object = settings.Settings(cycle, s.get('time_in_execute_range', .35), s.get('tricks_on_cooldown', True), 41 | s.get('response_time', .5), s.get('mh_poison', 'ip'), s.get('oh_poison', 'dp'), s.get('duration', 300)) 42 | 43 | stats_dict = j['stats'] 44 | # Weapon(damage, speed, weapon_type, enchant=None): 45 | mh_dict = stats_dict['mh'] 46 | mh = stats.Weapon(mh_dict['damage'], mh_dict['speed'], mh_dict['type'], mh_dict.get('enchant')) 47 | oh_dict = stats_dict['oh'] 48 | oh = stats.Weapon(oh_dict['damage'], oh_dict['speed'], oh_dict['type'], oh_dict.get('enchant')) 49 | ranged_dict = stats_dict['ranged'] 50 | ranged = stats.Weapon(ranged_dict['damage'], ranged_dict['speed'], ranged_dict['type'], ranged_dict.get('enchant')) 51 | procs_list = procs.ProcsList(*stats_dict['procs']) 52 | gear_buffs = stats.GearBuffs(*stats_dict['gear_buffs']) 53 | # Stats(str, agi, ap, crit, hit, exp, haste, mastery, mh, oh, ranged, procs, gear_buffs, level=85): 54 | def s(stat): 55 | return int(stats_dict[stat]) 56 | stats_object = stats.Stats(s('str'), s('agi'), s('ap'), s('crit'), s('hit'), s('exp'), s('haste'), s('mastery'), 57 | mh, oh, ranged, procs_list, gear_buffs, level) 58 | glyphs = rogue_glyphs.RogueGlyphs(*j['glyphs']) 59 | talents = rogue_talents.RogueTalents(*j['talents']) 60 | buffs_object = buffs.Buffs(*j['buffs']) 61 | except KeyError as e: 62 | raise InvalidJSONException(_("Missing required input {key}").format(key=str(e))) 63 | # Calculator(stats, talents, glyphs, buffs, race, settings=None, level=85): 64 | return AldrianasRogueDamageCalculator(stats_object, talents, glyphs, buffs_object, race_object, settings=settings_object, level=level) 65 | 66 | 67 | if __name__ == '__main__': 68 | json_string = """{ 69 | "level": 85, 70 | "stats": { 71 | "str": 20, 72 | "agi": 4756, 73 | "ap": 190, 74 | "crit": 1022, 75 | "hit": 1329, 76 | "exp": 159, 77 | "haste": 1291, 78 | "mastery": 1713, 79 | "gear_buffs": [ 80 | "rogue_t11_2pc", 81 | "leather_specialization", 82 | "potion_of_the_tolvir", 83 | "chaotic_metagem" 84 | ], 85 | "procs": [ 86 | "heroic_prestors_talisman_of_machination", 87 | "fluid_death", 88 | "rogue_t11_4pc" 89 | ], 90 | "mh": { 91 | "type": "dagger", 92 | "speed": 1.8, 93 | "damage": 939.5, 94 | "enchant": "landslide" 95 | }, 96 | "oh": { 97 | "type": "dagger", 98 | "speed": 1.4, 99 | "damage": 730.5, 100 | "enchant": "landslide" 101 | }, 102 | "ranged": { 103 | "type": "thrown", 104 | "speed": 2.2, 105 | "damage": 1371.5 106 | } 107 | }, 108 | "buffs": [ 109 | "short_term_haste_buff", 110 | "stat_multiplier_buff", 111 | "crit_chance_buff", 112 | "all_damage_buff", 113 | "melee_haste_buff", 114 | "attack_power_buff", 115 | "str_and_agi_buff", 116 | "armor_debuff", 117 | "physical_vulnerability_debuff", 118 | "spell_damage_debuff", 119 | "spell_crit_debuff", 120 | "bleed_damage_debuff", 121 | "agi_flask", 122 | "guild_feast" 123 | ], 124 | "settings": { 125 | "type": "assassination", 126 | "response_time": 1 127 | }, 128 | "talents": [ 129 | "0333230113022110321", 130 | "0020000000000000000", 131 | "2030030000000000000" 132 | ], 133 | "race": "night_elf", 134 | "glyphs": [ 135 | "backstab", 136 | "mutilate", 137 | "rupture" 138 | ] 139 | }""" 140 | 141 | 142 | calculator = from_json(json_string) 143 | # Compute EP values. 144 | ep_values = calculator.get_ep().items() 145 | ep_values.sort(key=lambda entry: entry[1], reverse=True) 146 | max_len = max(len(entry[0]) for entry in ep_values) 147 | for value in ep_values: 148 | print value[0] + ':' + ' ' * (max_len - len(value[0])), value[1] 149 | 150 | print '---------' 151 | 152 | # Compute DPS Breakdown. 153 | dps_breakdown = calculator.get_dps_breakdown().items() 154 | dps_breakdown.sort(key=lambda entry: entry[1], reverse=True) 155 | max_len = max(len(entry[0]) for entry in dps_breakdown) 156 | total_dps = sum(entry[1] for entry in dps_breakdown) 157 | for entry in dps_breakdown: 158 | print entry[0] + ':' + ' ' * (max_len - len(entry[0])), entry[1] 159 | 160 | print '-' * (max_len + 15) 161 | 162 | print ' ' * (max_len + 1), total_dps, _("total damage per second.") 163 | 164 | -------------------------------------------------------------------------------- /shadowcraft/objects/procs.py: -------------------------------------------------------------------------------- 1 | from shadowcraft.core import exceptions 2 | from shadowcraft.objects import proc_data 3 | 4 | class InvalidProcException(exceptions.InvalidInputException): 5 | pass 6 | 7 | 8 | class Proc(object): 9 | allowed_behaviours = proc_data.behaviours 10 | 11 | def __init__(self, stat, value, duration, proc_name, behaviours, max_stacks=1, can_crit=True, stats=None): 12 | self.stat = stat 13 | if stats is not None: 14 | self.stats = set(stats) 15 | self.value = value 16 | self.can_crit = can_crit 17 | self.duration = duration 18 | self.max_stacks = max_stacks 19 | self.proc_name = proc_name 20 | self.proc_behaviours = {} 21 | for i in behaviours: 22 | if behaviours[i] in self.allowed_behaviours: 23 | self.proc_behaviours[i] = self.allowed_behaviours[behaviours[i]] 24 | else: 25 | raise InvalidProcException(_('Behaviour {behaviour}:{behaviour_name} is not allowed').format(behaviour=i, behaviour_name=behaviours[i])) 26 | self.behaviour_toggle = 'default' 27 | 28 | def __setattr__(self, name, value): 29 | object.__setattr__(self, name, value) 30 | if name == 'behaviour_toggle': 31 | # Set behaviour attributes when this is modified. 32 | if value in self.proc_behaviours: 33 | self._set_behaviour(**self.proc_behaviours[value]) 34 | else: 35 | raise InvalidProcException(_('Behaviour \'{behaviour}\' is not defined for {proc}').format(proc=self.proc_name, behaviour=value)) 36 | 37 | def _set_behaviour(self, icd, trigger, proc_chance=False, ppm=False, on_crit=False, on_procced_strikes=True, real_ppm=False): 38 | # This could be merged with __setattr__; its sole purpose is 39 | # to clearly surface the parameters passed with the behaviours. 40 | self.proc_chance = proc_chance 41 | self.trigger = trigger 42 | self.icd = icd 43 | self.on_crit = on_crit 44 | self.ppm = ppm 45 | self.real_ppm = real_ppm 46 | self.on_procced_strikes = on_procced_strikes # Main Gauche and its kin 47 | 48 | def procs_off_auto_attacks(self): 49 | if self.trigger in ('all_attacks', 'auto_attacks', 'all_spells_and_attacks', 'all_melee_attacks'): 50 | return True 51 | else: 52 | return False 53 | 54 | def procs_off_strikes(self): 55 | if self.trigger in ('all_attacks', 'strikes', 'all_spells_and_attacks', 'all_melee_attacks'): 56 | return True 57 | else: 58 | return False 59 | 60 | def procs_off_harmful_spells(self): 61 | if self.trigger in ('all_spells', 'damaging_spells', 'all_spells_and_attacks'): 62 | return True 63 | else: 64 | return False 65 | 66 | def procs_off_heals(self): 67 | if self.trigger in ('all_spells', 'healing_spells', 'all_spells_and_attacks'): 68 | return True 69 | else: 70 | return False 71 | 72 | def procs_off_periodic_spell_damage(self): 73 | if self.trigger in ('all_periodic_damage', 'periodic_spell_damage'): 74 | return True 75 | else: 76 | return False 77 | 78 | def procs_off_periodic_heals(self): 79 | if self.trigger == 'hots': 80 | return True 81 | else: 82 | return False 83 | 84 | def procs_off_bleeds(self): 85 | if self.trigger in ('all_periodic_damage', 'bleeds'): 86 | return True 87 | else: 88 | return False 89 | 90 | def procs_off_crit_only(self): 91 | if self.on_crit: 92 | return True 93 | else: 94 | return False 95 | 96 | def procs_off_apply_debuff(self): 97 | if self.trigger in ('all_spells_and_attacks', 'all_attacks', 'all_melee_attacks'): 98 | return True 99 | else: 100 | return False 101 | 102 | def procs_off_procced_strikes(self): 103 | if self.on_procced_strikes: 104 | return True 105 | else: 106 | return False 107 | 108 | def proc_rate(self, speed=None, haste=1.0): 109 | if self.is_ppm(): 110 | if speed is None: 111 | raise InvalidProcException(_('Weapon speed needed to calculate the proc rate of {proc}').format(proc=self.proc_name)) 112 | else: 113 | return self.ppm * speed / 60. 114 | elif self.is_real_ppm(): 115 | return haste * self.ppm / 60 116 | else: 117 | return self.proc_chance 118 | 119 | def is_ppm(self): 120 | if self.proc_chance not in (False, None) and self.ppm == False: 121 | return False 122 | elif self.real_ppm == True: 123 | return False 124 | elif self.ppm not in (False, None) and self.proc_chance == False: 125 | return True 126 | else: 127 | raise InvalidProcException(_('Invalid data for proc {proc}').format(proc=self.proc_name)) 128 | 129 | def is_real_ppm(self): 130 | if self.real_ppm == True and (self.ppm not in (False, None)): 131 | return True 132 | elif self.real_ppm in (False, None): 133 | return False 134 | else: 135 | raise InvalidProcException(_('Invalid data for proc {proc}').format(proc=self.proc_name)) 136 | 137 | class ProcsList(object): 138 | allowed_procs = proc_data.allowed_procs 139 | 140 | def __init__(self, *args): 141 | for arg in args: 142 | if arg in self.allowed_procs: 143 | setattr(self, arg, Proc(**self.allowed_procs[arg])) 144 | else: 145 | raise InvalidProcException(_('No data for proc {proc}').format(proc=arg)) 146 | 147 | def set_proc(self, proc): 148 | setattr(self, proc, Proc(**self.allowed_procs[proc])) 149 | 150 | def __getattr__(self, proc): 151 | # Any proc we haven't assigned a value to, we don't have. 152 | if proc in self.allowed_procs: 153 | return False 154 | object.__getattribute__(self, proc) 155 | 156 | def __setattr__(self, name, value): 157 | object.__setattr__(self, name, value) 158 | if name == 'level': 159 | self._set_constants_for_level() 160 | 161 | def _set_constants_for_level(self): 162 | self.set_swordguard_embroidery_value() 163 | 164 | def set_swordguard_embroidery_value(self): 165 | proc = getattr(self, 'swordguard_embroidery') 166 | values = [ 167 | (90, 4000), 168 | (85, 1000), 169 | (80, 400), 170 | (1, 0) 171 | ] 172 | for level, value in values: 173 | if self.level >= level: 174 | self.allowed_procs['swordguard_embroidery']['value'] = value 175 | if proc: 176 | proc.value = value 177 | break 178 | 179 | def get_all_procs_for_stat(self, stat=None): 180 | procs = [] 181 | for proc_name in self.allowed_procs: 182 | proc = getattr(self, proc_name) 183 | if proc: 184 | if stat is None or proc.stat == stat: 185 | procs.append(proc) 186 | 187 | return procs 188 | 189 | def get_all_damage_procs(self): 190 | procs = [] 191 | for proc_name in self.allowed_procs: 192 | proc = getattr(self, proc_name) 193 | if proc: 194 | if proc.stat in ('spell_damage', 'physical_damage'): 195 | procs.append(proc) 196 | 197 | return procs 198 | -------------------------------------------------------------------------------- /shadowcraft/objects/old_proc_data.py: -------------------------------------------------------------------------------- 1 | allowed_procs = { 2 | 'heroic_grace_of_the_herald': { 3 | 'stat': 'crit', 4 | 'value': 1710, 5 | 'duration': 10, 6 | 'proc_name': 'Herald of Doom', 7 | 'behaviours': {'default': 'grace_of_the_herald'} 8 | }, 9 | 'heroic_key_to_the_endless_chamber': { 10 | 'stat': 'agi', 11 | 'value': 1710, 12 | 'duration': 15, 13 | 'proc_name': 'Final Key', 14 | 'behaviours': {'default': 'key_to_the_endless_chamber'} 15 | }, 16 | 'heroic_left_eye_of_rajh': { 17 | 'stat': 'agi', 18 | 'value': 1710, 19 | 'duration': 10, 20 | 'proc_name': 'Eye of Vengeance', 21 | 'behaviours': {'default': 'left_eye_of_rajh'} 22 | }, 23 | 'heroic_prestors_talisman_of_machination': { 24 | 'stat': 'haste', 25 | 'value': 2178, 26 | 'duration': 15, 27 | 'proc_name': 'Nefarious Plot', 28 | 'behaviours': {'default': 'prestors_talisman_of_machination'} 29 | }, 30 | 'heroic_the_hungerer': { 31 | 'stat': 'haste', 32 | 'value': 1730, 33 | 'duration': 15, 34 | 'proc_name': 'Devour', 35 | 'behaviours': {'default': 'the_hungerer'} 36 | }, 37 | 'heroic_tias_grace': { 38 | 'stat': 'agi', 39 | 'value': 34, 40 | 'duration': 15, 41 | 'max_stacks': 10, 42 | 'proc_name': 'Grace', 43 | 'behaviours': {'default': 'tias_grace'} 44 | }, 45 | 'arrow_of_time': { 46 | 'stat': 'haste', 47 | 'value': 1149, 48 | 'duration': 20, 49 | 'proc_name': 'Arrow of Time', 50 | 'behaviours': {'default': 'arrow_of_time'} 51 | }, 52 | 'darkmoon_card_hurricane': { 53 | 'stat': 'spell_damage', 54 | 'value': 7000, 55 | 'can_crit': False, 56 | 'duration': 0, 57 | 'max_stacks': 0, 58 | 'proc_name': 'Lightning Strike', 59 | 'behaviours': {'default': 'darkmoon_card_hurricane'} 60 | }, 61 | 'corens_chilled_chromium_coaster': { 62 | 'stat': 'ap', 63 | 'value': 4000, 64 | 'duration': 10, 65 | 'max_stacks': 0, 66 | 'proc_name': 'Reflection of Torment', 67 | 'behaviours': {'default': 'corens_chilled_chromium_coaster'} 68 | }, 69 | 'essence_of_the_cyclone': { 70 | 'stat': 'crit', 71 | 'value': 1926, 72 | 'duration': 10, 73 | 'proc_name': 'Twisted', 74 | 'behaviours': {'default': 'essence_of_the_cyclone'} 75 | }, 76 | 'heroic_essence_of_the_cyclone': { 77 | 'stat': 'crit', 78 | 'value': 2178, 79 | 'duration': 10, 80 | 'proc_name': 'Twisted', 81 | 'behaviours': {'default': 'essence_of_the_cyclone'} 82 | }, 83 | 'fluid_death': { 84 | 'stat': 'agi', 85 | 'value': 38, 86 | 'duration': 15, 87 | 'max_stacks': 10, 88 | 'proc_name': 'River of Death', 89 | 'behaviours': {'default': 'fluid_death'} 90 | }, 91 | 'grace_of_the_herald': { 92 | 'stat': 'crit', 93 | 'value': 924, 94 | 'duration': 10, 95 | 'proc_name': 'Herald of Doom', 96 | 'behaviours': {'default': 'grace_of_the_herald'} 97 | }, 98 | 'heart_of_the_vile': { 99 | 'stat': 'crit', 100 | 'value': 924, 101 | 'duration': 10, 102 | 'proc_name': 'Herald of Doom', 103 | 'behaviours': {'default': 'heart_of_the_vile'} 104 | }, 105 | 'key_to_the_endless_chamber': { 106 | 'stat': 'agi', 107 | 'value': 1290, 108 | 'duration': 15, 109 | 'proc_name': 'Final Key', 110 | 'behaviours': {'default': 'key_to_the_endless_chamber'} 111 | }, 112 | 'left_eye_of_rajh': { 113 | 'stat': 'agi', 114 | 'value': 1512, 115 | 'duration': 10, 116 | 'proc_name': 'Eye of Vengeance', 117 | 'behaviours': {'default': 'left_eye_of_rajh'} 118 | }, 119 | 'prestors_talisman_of_machination': { 120 | 'stat': 'haste', 121 | 'value': 1926, 122 | 'duration': 15, 123 | 'proc_name': 'Nefarious Plot', 124 | 'behaviours': {'default': 'prestors_talisman_of_machination'} 125 | }, 126 | 'rickets_magnetic_fireball_proc': { # ICD should be verified. 127 | 'stat': 'physical_damage', 128 | 'value': 500, 129 | 'duration': 0, 130 | 'max_stacks': 0, 131 | 'proc_name': 'Magnetic Fireball', 132 | 'behaviours': {'default': 'rickets_magnetic_fireball'} 133 | }, 134 | 'schnottz_medallion_of_command': { 135 | 'stat': 'mastery', 136 | 'value': 918, 137 | 'duration': 20, 138 | 'proc_name': 'Hardened Shell', 139 | 'behaviours': {'default': 'schnottz_medallion_of_command'} 140 | }, 141 | 'the_hungerer': { 142 | 'stat': 'haste', 143 | 'value': 1532, 144 | 'duration': 15, 145 | 'proc_name': 'Devour', 146 | 'behaviours': {'default': 'the_hungerer'} 147 | }, 148 | 'the_twilight_blade': { 149 | 'stat': 'crit', 150 | 'value': 185, 151 | 'duration': 10, 152 | 'max_stacks': 3, 153 | 'proc_name': 'The Deepest Night', 154 | 'behaviours': {'default': 'the_twilight_blade'} 155 | }, 156 | 'tias_grace': { 157 | 'stat': 'agi', 158 | 'value': 30, 159 | 'duration': 15, 160 | 'max_stacks': 10, 161 | 'proc_name': 'Grace', 162 | 'behaviours': {'default': 'tias_grace'} 163 | }, 164 | 'unheeded_warning': { 165 | 'stat': 'ap', 166 | 'value': 1926, 167 | 'duration': 10, 168 | 'proc_name': 'Heedless Carnage', 169 | 'behaviours': {'default': 'unheeded_warning'} 170 | }, 171 | } 172 | 173 | behaviours = { 174 | 'arrow_of_time': { 175 | 'icd': 50, 176 | 'proc_chance': .2, 177 | 'trigger': 'all_attacks' 178 | }, 179 | 'corens_chilled_chromium_coaster': { # ICD is a guess and should be verified. 180 | 'icd': 50, 181 | 'proc_chance': .1, 182 | 'trigger': 'all_attacks', 183 | 'on_crit': True 184 | }, 185 | 'darkmoon_card_hurricane': { 186 | 'icd': 0, 187 | 'ppm': 1, 188 | 'trigger': 'all_attacks' 189 | }, 190 | 'essence_of_the_cyclone': { 191 | 'icd': 50, 192 | 'proc_chance': .1, 193 | 'trigger': 'all_attacks' 194 | }, 195 | 'fluid_death': { 196 | 'icd': None, 197 | 'proc_chance': 1, 198 | 'trigger': 'all_attacks' 199 | }, 200 | 'grace_of_the_herald': { 201 | 'icd': 50, 202 | 'proc_chance': .1, 203 | 'trigger': 'all_attacks' 204 | }, 205 | 'heart_of_the_vile': { 206 | 'icd': 50, 207 | 'proc_chance': .1, 208 | 'trigger': 'all_attacks' 209 | }, 210 | 'key_to_the_endless_chamber': { 211 | 'icd': 75, 212 | 'proc_chance': .1, 213 | 'trigger': 'all_attacks' 214 | }, 215 | 'left_eye_of_rajh': { 216 | 'icd': 50, 217 | 'proc_chance': .3, 218 | 'trigger': 'all_attacks', 219 | 'on_crit': True 220 | }, 221 | 'prestors_talisman_of_machination': { 222 | 'icd': 75, 223 | 'proc_chance': .1, 224 | 'trigger': 'all_attacks' 225 | }, 226 | 'rickets_magnetic_fireball': { # ICD should be verified. 227 | 'icd': 120, 228 | 'proc_chance': .2, 229 | 'trigger': 'all_attacks' 230 | }, 231 | 'schnottz_medallion_of_command': { 232 | 'icd': 100, 233 | 'proc_chance': .1, 234 | 'trigger': 'all_attacks' 235 | }, 236 | 'the_hungerer': { 237 | 'icd': 60, 238 | 'proc_chance': 1., 239 | 'trigger': 'all_attacks' 240 | }, 241 | 'the_twilight_blade': { # PPM/ICD is a guess and should be verified. 242 | 'icd': 0, 243 | 'ppm': 1, 244 | 'trigger': 'all_attacks' 245 | }, 246 | 'tias_grace': { 247 | 'icd': None, 248 | 'proc_chance': 1, 249 | 'trigger': 'all_attacks' 250 | }, 251 | 'unheeded_warning': { 252 | 'icd': 50, 253 | 'proc_chance': .1, 254 | 'trigger': 'all_attacks' 255 | }, 256 | } 257 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/race.py: -------------------------------------------------------------------------------- 1 | from shadowcraft.core import exceptions 2 | 3 | class InvalidRaceException(exceptions.InvalidInputException): 4 | pass 5 | 6 | class Race(object): 7 | rogue_base_stats = { 8 | 80: (113, 189, 105, 43, 67), 9 | 85: (122, 206, 114, 46, 73), 10 | 90: (132, 225, 123, 48, 77) 11 | } 12 | 13 | #(ap,sp) 14 | blood_fury_bonuses = { 15 | 80: {'ap': 346, 'sp': 173}, 16 | 85: {'ap': 1344, 'sp': 672}, 17 | 90: {'ap': 4514, 'sp': 2257} 18 | } 19 | touch_of_the_grave_bonuses = { 20 | 80: {'spell_damage': 1000}, 21 | 90: {'spell_damage': 16000} 22 | } 23 | 24 | #Arguments are ap, spellpower:fire, and int 25 | #This is the formula according to wowhead, with a probable typo corrected 26 | def calculate_rocket_barrage(self, ap, spfi, int): 27 | return 1 + 0.25 * ap + .429 * spfi + self.level * 2 + int * 0.50193 28 | 29 | racial_stat_offset = { 30 | "human": (0, 0, 0, 0, 0), 31 | "night_elf": (-4, 4, 0, 0, 0), 32 | "dwarf": (5, -4, 1, -1, -1), 33 | "gnome": (-5, 2, 0, 3, 0), 34 | "draenei": (1, -3, 0, 0, 2), 35 | "worgen": (3, 2, 0, -4, -1), 36 | "pandaren": (0, -2, 1, -1, 2), 37 | "orc": (3, -3, 1, -3, 2), 38 | "undead": (-1, -2, 0, -2, 5), 39 | "tauren": (5, -4, 1, -4, 2), 40 | "troll": (1, 2, 0, -4, 1), 41 | "blood_elf": (-3, 2, 0, 3, -2), 42 | "goblin": (-3, 2, 0, 3, -2), 43 | } 44 | 45 | allowed_racials = frozenset([ 46 | "axe_specialization", #Orc 47 | "fist_specialization", #Orc 48 | "mace_specialization", #Human, Dwarf 49 | "stoneform", #Dwarf 50 | "expansive_mind", #Gnome 51 | "dagger_specialization", #Gnome 52 | "sword_1h_specialization", #Gnome, Human 53 | "human_spirit", #Human 54 | "sword_2h_specialization", #Human 55 | "quickness", #Night Elf 56 | "shadowmeld", #Night Elf 57 | "heroic_presence", #Draenei 58 | "viciousness", #Worgen 59 | "blood_fury_physical", #Orc 60 | "blood_fury_spell", #Orc 61 | "command", #Orc 62 | "endurance", #Tauren 63 | "berserking", #Troll 64 | "regeneration", #Troll 65 | "beast_slaying", #Troll 66 | "ranged_specialization", #Troll, Dwarf 67 | "arcane_torrent", #Blood Elf 68 | "rocket_barrage", #Goblin 69 | "time_is_money", #Goblin 70 | "epicurean", #Pandaren 71 | "touch_of_the_grave", #Undead 72 | "cannibalize", #Undead 73 | ]) 74 | 75 | activated_racial_data = { 76 | #Blood fury values are set when level is set 77 | 'blood_fury_physical': {'stat': "ap", 'value': 0, 'duration': 15, 'cooldown': 120}, #level-based ap increase 78 | 'blood_fury_spell': {'stat': "sp", 'value': 0, 'duration': 15, 'cooldown': 120}, #level-based sp increase 79 | 'berserking': {'stat': "haste_multiplier", 'value': 1.2, 'duration': 10, 'cooldown': 180}, #20% haste increase for 10 seconds, 3 minute cd 80 | 'arcane_torrent': {'stat': "energy", 'value': 15, 'duration': 0, 'cooldown': 120}, #gain 15 energy (or 15 runic power or 6% mana), 2 minute cd 81 | 'rocket_barrage': {'stat': "damage", 'value': calculate_rocket_barrage, 'duration': 0, 'cooldown': 120}, #deal formula-based damage, 2 min cd 82 | } 83 | 84 | racials_by_race = { 85 | "human": ["mace_specialization", "sword_1h_specialization", "sword_2h_specialization", "human_spirit"], 86 | "night_elf": ["quickness", "shadowmeld"], 87 | "dwarf": ["stoneform", "ranged_specialization", "mace_specialization"], 88 | "gnome": ["expansive_mind", "dagger_specialization", "sword_1h_specialization"], 89 | "draenei": ["heroic_presence"], 90 | "worgen": ["viciousness"], 91 | "orc": ["blood_fury_physical", "blood_fury_spell", "fist_specialization", "axe_specialization"], 92 | "undead": ["touch_of_the_grave", "cannibalize"], 93 | "tauren": ["endurance"], 94 | "troll": ["regeneration", "beast_slaying", "ranged_specialization", "berserking"], 95 | "blood_elf": ["arcane_torrent"], 96 | "goblin": ["rocket_barrage", "time_is_money"], 97 | "pandaren": ["epicurean"] 98 | } 99 | 100 | #Note this allows invalid class-race combos 101 | def __init__(self, race, character_class="rogue", level=85): 102 | self.character_class = str.lower(character_class) 103 | self.race_name = race 104 | if self.race_name not in Race.racial_stat_offset.keys(): 105 | raise InvalidRaceException(_('Unsupported race {race}').format(race=self.race_name)) 106 | if self.character_class == "rogue": 107 | self.stat_set = Race.rogue_base_stats 108 | else: 109 | raise InvalidRaceException(_('Unsupported class {character_class}').format(character_class=self.character_class)) 110 | self.level = level 111 | self.set_racials() 112 | 113 | def set_racials(self): 114 | # Set all racials, so we don't invoke __getattr__ all the time 115 | for race, racials in Race.racials_by_race.items(): 116 | for racial in racials: 117 | setattr(self, racial, False) 118 | for racial in Race.racials_by_race[self.race_name]: 119 | setattr(self, racial, True) 120 | setattr(self, "racial_str", self.stats[0]) 121 | setattr(self, "racial_agi", self.stats[1]) 122 | setattr(self, "racial_sta", self.stats[2]) 123 | setattr(self, "racial_int", self.stats[3]) 124 | setattr(self, "racial_spi", self.stats[4]) 125 | 126 | def __setattr__(self, name, value): 127 | object.__setattr__(self, name, value) 128 | if name == 'level': 129 | self._set_constants_for_level() 130 | 131 | def _set_constants_for_level(self): 132 | try: 133 | self.stats = self.stat_set[self.level] 134 | self.activated_racial_data["blood_fury_physical"]["value"] = self.blood_fury_bonuses[self.level]["ap"] 135 | self.activated_racial_data["blood_fury_spell"]["value"] = self.blood_fury_bonuses[self.level]["sp"] 136 | self.stats = map(sum, zip(self.stats, Race.racial_stat_offset[self.race_name])) 137 | except KeyError as e: 138 | raise InvalidRaceException(_('Unsupported class/level combination {character_class}/{level}').format(character_class=self.character_class, level=self.level)) 139 | 140 | def __getattr__(self, name): 141 | # Any racial we haven't assigned a value to, we don't have. 142 | if name in self.allowed_racials: 143 | return False 144 | else: 145 | object.__getattribute__(self, name) 146 | 147 | def get_racial_expertise(self, weapon_type): 148 | #print weapon_type 149 | if weapon_type in ['axe', '1h_axe', '2h_axe']: 150 | if self.axe_specialization: 151 | return .01 152 | elif weapon_type == 'fist': 153 | if self.fist_specialization: 154 | return .01 155 | elif weapon_type in ['sword', '1h_sword']: 156 | if self.sword_1h_specialization: 157 | return .01 158 | elif weapon_type == '2h_sword': 159 | if self.sword_2h_specialization: 160 | return .01 161 | elif weapon_type in ['mace', '1h_mace', '2h_mace']: 162 | if self.mace_specialization: 163 | return .01 164 | elif weapon_type == 'dagger': 165 | if self.dagger_specialization: 166 | return .01 167 | elif weapon_type in ['bow', 'crossbow', 'gun']: 168 | if self.ranged_specialization: 169 | return .01 170 | 171 | return 0 172 | 173 | def get_racial_crit(self, weapon_type=None): 174 | crit_bonus = 0 175 | if self.viciousness: 176 | crit_bonus = .01 177 | 178 | return crit_bonus 179 | 180 | def get_racial_hit(self): 181 | hit_bonus = 0 182 | if self.heroic_presence: 183 | hit_bonus = .01 184 | 185 | return hit_bonus 186 | 187 | def get_racial_haste(self): 188 | haste_bonus = 0 189 | if self.time_is_money: 190 | haste_bonus = .01 191 | 192 | return haste_bonus 193 | 194 | def get_racial_stat_boosts(self): 195 | racial_boosts = [] 196 | #Only the orc racial is a straight stat boost 197 | if getattr(self, "blood_fury_physical"): 198 | racial_boosts += [self.activated_racial_data["blood_fury_physical"], self.activated_racial_data["blood_fury_spell"]] 199 | return racial_boosts 200 | -------------------------------------------------------------------------------- /tests/calcs_tests/rogue_tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from shadowcraft.calcs.rogue import RogueDamageCalculator 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.rogue import rogue_talents 9 | 10 | class TestRogueDamageCalculator(unittest.TestCase): 11 | def setUp(self): 12 | test_buffs = buffs.Buffs( 13 | 'stat_multiplier_buff', 14 | 'crit_chance_buff', 15 | 'melee_haste_buff', 16 | 'attack_power_buff', 17 | 'str_and_agi_buff', 18 | 'armor_debuff', 19 | 'spell_damage_debuff', 20 | 'spell_crit_debuff' 21 | ) 22 | test_mh = stats.Weapon(737, 1.8, 'dagger', 'hurricane') 23 | test_oh = stats.Weapon(573, 1.4, 'dagger', 'hurricane') 24 | test_ranged = stats.Weapon(1104, 2.0, 'thrown') 25 | test_procs = procs.ProcsList('darkmoon_card_hurricane') 26 | test_gear_buffs = stats.GearBuffs('chaotic_metagem') 27 | test_stats = stats.Stats(20, 3485, 190, 1517, 1086, 641, 899, 666, test_mh, test_oh, test_ranged, test_procs, test_gear_buffs) 28 | test_race = race.Race('night_elf') 29 | test_talents = rogue_talents.RogueTalents('0333230113022110321', '0020000000000000000', '0030030000000000000') 30 | self.calculator = RogueDamageCalculator(test_stats, test_talents, None, test_buffs, test_race) 31 | 32 | def test_get_spell_hit_from_talents(self): 33 | self.assertAlmostEqual(self.calculator.get_spell_hit_from_talents(), .04) 34 | self.calculator.talents.precision = 0 35 | self.assertAlmostEqual(self.calculator.get_spell_hit_from_talents(), .0) 36 | 37 | def test_get_melee_hit_from_talents(self): 38 | self.assertAlmostEqual(self.calculator.get_melee_hit_from_talents(), .04) 39 | self.calculator.talents.precision = 3 40 | self.assertAlmostEqual(self.calculator.get_melee_hit_from_talents(), .06) 41 | 42 | def test_oh_penalty(self): 43 | self.assertAlmostEqual(self.calculator.oh_penalty(), 0.5) 44 | 45 | def test_talents_modifiers_assassins_resolve(self): 46 | self.assertAlmostEqual(self.calculator.talents_modifiers([]), 1.0) 47 | self.assertAlmostEqual(self.calculator.talents_modifiers(['assassins_resolve']), 1.2) 48 | self.calculator.stats.mh.type = '1h_axe' 49 | self.assertAlmostEqual(self.calculator.talents_modifiers(['assassins_resolve']), 1.0) 50 | 51 | def test_talents_modifiers(self): 52 | self.assertAlmostEqual(self.calculator.talents_modifiers(['opportunity', 'assassins_resolve']), 1.2 * 1.3) 53 | 54 | def test_crit_damage_modifiers(self): 55 | self.assertAlmostEqual(self.calculator.crit_damage_modifiers(), 1 + (2 * 1.03 - 1) * 1) 56 | self.assertAlmostEqual(self.calculator.crit_damage_modifiers(is_spell=True), 1 + (1.5 * 1.03 - 1) * 1) 57 | self.assertAlmostEqual(self.calculator.crit_damage_modifiers(lethality=True), 1 + (2 * 1.03 - 1) * 1.3) 58 | 59 | # Just do some basic checks for the individual abilities, increasing AP 60 | # should increase damage and similar for combo points. 61 | # The optional armor argument isn't tested for now. 62 | # Should probably compare damage and crit_damage individually so it can 63 | # catch some error where crit_damage is lower even with higher AP. 64 | 65 | def test_mh_damage(self): 66 | self.assertTrue(self.calculator.mh_damage(0) < self.calculator.mh_damage(1)) 67 | 68 | def test_oh_damage(self): 69 | self.assertTrue(self.calculator.oh_damage(0) < self.calculator.oh_damage(1)) 70 | 71 | def test_backstab_damage(self): 72 | self.assertTrue(self.calculator.backstab_damage(0) < self.calculator.backstab_damage(1)) 73 | 74 | def test_mh_mutilate_damage(self): 75 | self.assertTrue(self.calculator.mh_mutilate_damage(0) < self.calculator.mh_mutilate_damage(1)) 76 | not_poisoned = self.calculator.mh_mutilate_damage(1, is_poisoned=False) 77 | poisoned = self.calculator.mh_mutilate_damage(1) 78 | self.assertAlmostEqual(not_poisoned[0] * 1.2, poisoned[0]) 79 | self.assertAlmostEqual(not_poisoned[1] * 1.2, poisoned[1]) 80 | 81 | def test_oh_mutilate_damage(self): 82 | self.assertTrue(self.calculator.oh_mutilate_damage(0) < self.calculator.oh_mutilate_damage(1)) 83 | not_poisoned = self.calculator.oh_mutilate_damage(1, is_poisoned=False) 84 | poisoned = self.calculator.oh_mutilate_damage(1) 85 | self.assertAlmostEqual(not_poisoned[0] * 1.2, poisoned[0]) 86 | self.assertAlmostEqual(not_poisoned[1] * 1.2, poisoned[1]) 87 | 88 | def test_sinister_strike_damage(self): 89 | self.assertTrue(self.calculator.sinister_strike_damage(0) < self.calculator.sinister_strike_damage(1)) 90 | 91 | def test_hemorrhage_damage(self): 92 | self.assertTrue(self.calculator.hemorrhage_damage(0) < self.calculator.hemorrhage_damage(1)) 93 | 94 | def test_hemorrhage_tick_damage(self): 95 | self.assertTrue(self.calculator.hemorrhage_tick_damage(0) < self.calculator.hemorrhage_tick_damage(1)) 96 | self.assertTrue(self.calculator.hemorrhage_tick_damage(0, from_crit_hemo=False) < self.calculator.hemorrhage_tick_damage(0, from_crit_hemo=True)) 97 | 98 | def test_ambush_damage(self): 99 | self.assertTrue(self.calculator.ambush_damage(0) < self.calculator.ambush_damage(1)) 100 | 101 | def test_revealing_strike_damage(self): 102 | self.assertTrue(self.calculator.revealing_strike_damage(0) < self.calculator.revealing_strike_damage(1)) 103 | 104 | def test_venomous_wounds_damage(self): 105 | self.assertTrue(self.calculator.venomous_wounds_damage(0) < self.calculator.venomous_wounds_damage(1)) 106 | 107 | def test_main_gauche_damage(self): 108 | self.assertTrue(self.calculator.main_gauche_damage(0) < self.calculator.main_gauche_damage(1)) 109 | 110 | def test_mh_killing_spree_damage(self): 111 | self.assertTrue(self.calculator.mh_killing_spree_damage(0) < self.calculator.mh_killing_spree_damage(1)) 112 | 113 | def test_oh_killing_spree_damage(self): 114 | self.assertTrue(self.calculator.oh_killing_spree_damage(0) < self.calculator.oh_killing_spree_damage(1)) 115 | 116 | def test_instant_poison_damage(self): 117 | self.assertTrue(self.calculator.instant_poison_damage(0) < self.calculator.instant_poison_damage(1)) 118 | self.assertTrue(self.calculator.instant_poison_damage(0, mastery=0) < self.calculator.instant_poison_damage(0, mastery=1)) 119 | 120 | def test_deadly_poison_tick_damage(self): 121 | # test mastery 122 | self.assertTrue(self.calculator.deadly_poison_tick_damage(0) < self.calculator.deadly_poison_tick_damage(1)) 123 | self.assertTrue(self.calculator.deadly_poison_tick_damage(0, dp_stacks=1) < self.calculator.deadly_poison_tick_damage(0, dp_stacks=2)) 124 | 125 | def test_wound_poison_damage(self): 126 | self.assertTrue(self.calculator.wound_poison_damage(0) < self.calculator.wound_poison_damage(1)) 127 | self.assertTrue(self.calculator.wound_poison_damage(0, mastery=0) < self.calculator.wound_poison_damage(0, mastery=1)) 128 | 129 | def test_garrote_tick_damage(self): 130 | self.assertTrue(self.calculator.garrote_tick_damage(0) < self.calculator.garrote_tick_damage(1)) 131 | 132 | def test_rupture_tick_damage(self): 133 | self.assertTrue(self.calculator.rupture_tick_damage(0, 1) < self.calculator.rupture_tick_damage(1, 1)) 134 | self.assertTrue(self.calculator.rupture_tick_damage(0, 1) < self.calculator.rupture_tick_damage(0, 2)) 135 | self.assertRaises(IndexError, self.calculator.rupture_tick_damage, 0, 6) 136 | 137 | def test_eviscerate_damage(self): 138 | self.assertTrue(self.calculator.eviscerate_damage(0, 1) < self.calculator.eviscerate_damage(1, 1)) 139 | self.assertTrue(self.calculator.eviscerate_damage(0, 1) < self.calculator.eviscerate_damage(0, 2)) 140 | 141 | def test_envenom_damage(self): 142 | self.assertTrue(self.calculator.envenom_damage(0, 1) < self.calculator.envenom_damage(1, 1)) 143 | self.assertTrue(self.calculator.envenom_damage(0, 1) < self.calculator.envenom_damage(0, 2)) 144 | 145 | def test_melee_crit_rate(self): 146 | agi_per_crit = self.calculator.level == 80 and 83.15 or 324.72 147 | crit_rating_per_crit = self.calculator.level == 80 and 45.906 or 179.279998779296875 148 | self.assertAlmostEqual(self.calculator.melee_crit_rate(agi=1000), 149 | 0.01 * (1000 / agi_per_crit - 0.295) + 0.01 * (1517 / crit_rating_per_crit) + 0.05 - 0.048) 150 | self.assertTrue(self.calculator.spell_crit_rate(0) < self.calculator.spell_crit_rate(1)) 151 | 152 | def test_spell_crit_rate(self): 153 | self.assertTrue(self.calculator.melee_crit_rate(0) < self.calculator.melee_crit_rate(1)) 154 | 155 | def test_crit_cap(self): 156 | pass 157 | 158 | 159 | class TestRogueDamageCalculatorLevels(TestRogueDamageCalculator): 160 | def setUp(self): 161 | super(TestRogueDamageCalculatorLevels, self).setUp() 162 | self.calculator.level = 80 163 | 164 | def test_set_constants_for_level(self): 165 | self.assertRaises(exceptions.InvalidLevelException, self.calculator.__setattr__, 'level', 86) 166 | -------------------------------------------------------------------------------- /scripts/wowapi/wowapi/api.py: -------------------------------------------------------------------------------- 1 | from urllib2 import Request, urlopen, URLError,quote 2 | import gzip 3 | import StringIO 4 | try: 5 | import simplejson as json 6 | except ImportError: 7 | import json 8 | import datetime 9 | import base64 10 | import hmac 11 | import hashlib 12 | from .exceptions import APIError,NotModified,NotFound 13 | from .utilities import parse_http_datetime,http_datetime 14 | 15 | regions = { 16 | 'us' : { 17 | 'domain':'us.battle.net', 18 | 'locales' : [ 19 | 'en_US', 20 | 'es_MX' 21 | ] 22 | }, 23 | 'eu' : { 24 | 'domain':'eu.battle.net', 25 | 'locales' : [ 26 | 'en_GB', 27 | 'es_ES', 28 | 'fr_FR', 29 | 'ru_RU', 30 | 'de_DE' 31 | ] 32 | }, 33 | 'kr' : { 34 | 'domain':'kr.battle.net', 35 | 'locales' : [ 36 | 'ko_KR' 37 | ] 38 | }, 39 | 'tw' : { 40 | 'domain':'tw.battle.net', 41 | 'locales' : [ 42 | 'zh_TW' 43 | ] 44 | }, 45 | 'cn' : { 46 | 'domain':'www.battlenet.com.cn', 47 | 'locales' : [ 48 | 'zh_CN' 49 | ] 50 | }, 51 | } 52 | 53 | datatypes = { 54 | 'character' : { 55 | 'path':'character/%s/%s', 56 | 'param':'fields' 57 | }, 58 | 'guild' : { 59 | 'path':'guild/%s/%s', 60 | 'param':'fields' 61 | }, 62 | 'realm' : { 63 | 'path': 'realm/status', 64 | 'param':'realms' 65 | }, 66 | 'auction' : { 67 | 'path' : 'auction/data/%s' 68 | }, 69 | 'item' : { 70 | 'path' : 'item/%d' 71 | }, 72 | 'arena_team' : { 73 | 'path' : 'arena/%s/%s/%s' 74 | }, 75 | 'arena_ladder' : { 76 | 'path' : 'pvp/arena/%s/%s', 77 | 'param' : 'size' 78 | }, 79 | 'character_races':{ 80 | 'path' : 'data/character/races' 81 | }, 82 | 'character_classes':{ 83 | 'path' : 'data/character/classes' 84 | }, 85 | 'guild_rewards':{ 86 | 'path' : 'data/guild/rewards' 87 | }, 88 | 'guild_perks':{ 89 | 'path':'data/guild/perks' 90 | }, 91 | 'item_classes':{ 92 | 'path':'data/item/classes' 93 | }, 94 | 'achievements_character':{ 95 | 'path':'data/character/achievements' 96 | }, 97 | 'achievements_guild':{ 98 | 'path':'data/guild/achievements' 99 | }, 100 | 'quest':{ 101 | 'path':'quest/%d' 102 | }, 103 | 'recipe':{ 104 | 'path':'recipe/%d' 105 | } 106 | } 107 | 108 | class WoWApi(): 109 | 110 | 111 | def __init__(self,privatekey=None,publickey=None,ssl=None): 112 | self.privkey = privatekey 113 | self.pubkey = publickey 114 | if ssl is None: 115 | if self.privkey and self.pubkey: 116 | self.ssl = True 117 | else: 118 | self.ssl = False 119 | else: 120 | self.ssl = ssl 121 | 122 | 123 | 124 | 125 | 126 | def _decode_response(self,response): 127 | 128 | if 'content-encoding' in response.info() and response.info()['content-encoding'] == 'gzip': 129 | response = gzip.GzipFile(fileobj=StringIO.StringIO(response.read())) 130 | try: 131 | data = json.loads(unicode(response.read(),'UTF-8')) 132 | except json.JSONDecodeError: 133 | raise APIError('Non-JSON Response') 134 | return data 135 | 136 | 137 | def _do_request(self,request): 138 | try: 139 | response = urlopen(request) 140 | except URLError, e: 141 | if hasattr(e, 'reason'): 142 | raise APIError(e.reason,request.get_full_url()) 143 | elif hasattr(e, 'code'): 144 | if e.code == 304: 145 | raise NotModified(request.get_full_url()) 146 | elif e.code == 404: 147 | raise NotFound(request.get_full_url()) 148 | else: 149 | error_response = self._decode_response(e) 150 | if error_response['reason']: 151 | raise APIError(e.code,error_response['reason'],request.get_full_url()) 152 | else: 153 | raise APIError(e.code,None,request.get_full_url()) 154 | else: 155 | return response 156 | 157 | def _sign_request(self,path,date): 158 | stringtosign = "GET\n"+date+"\n"+path+"\n" 159 | hash = hmac.new(self.privkey, stringtosign, hashlib.sha1).digest() 160 | return base64.encodestring(hash) 161 | 162 | def _get_data(self,region,data,params=None,lastmodified=None,lang=None,datatype=None): 163 | if region not in regions: 164 | raise ValueError('Region not found') 165 | if lang and lang not in regions[region]['locales']: 166 | raise ValueError('Locales not valid for current region') 167 | httpdate = http_datetime(datetime.datetime.utcnow()) 168 | signature = None 169 | if self.privkey and self.pubkey: 170 | signature = self._sign_request('/api/wow/'+data,httpdate) 171 | 172 | if params: 173 | data += '?'+datatypes[datatype]['param']+'='+','.join(map(str, params)) 174 | 175 | if lang and params: 176 | data+='&locale='+lang 177 | elif lang: 178 | data+='?locale='+lang 179 | if self.ssl: 180 | url = 'https://' 181 | else: 182 | url = 'http://' 183 | 184 | url += regions[region]['domain']+'/api/wow/'+data 185 | header = { 186 | 'Accept-Encoding': 'gzip', 187 | 'Date' : httpdate 188 | } 189 | if signature: 190 | header['Authorization'] = 'BNET '+self.pubkey+':'+signature 191 | 192 | 193 | if lastmodified: 194 | header['If-Modified-Since'] = http_datetime(lastmodified) 195 | 196 | request = Request(url, None, header) 197 | 198 | response = self._do_request(request) 199 | 200 | rlastmodified = None 201 | if 'Last-Modified' in response.info(): 202 | rlastmodified = parse_http_datetime(response.info()['Last-Modified']) 203 | 204 | return {'lastmodified':(rlastmodified),'data':self._decode_response(response)} 205 | 206 | def get_item(self,region,itemid,lastmodified=None,lang=None): 207 | """ 208 | Get infos about an item 209 | 210 | | ``Example:`` 211 | :: 212 | 213 | get_item('eu',25) 214 | """ 215 | if not int(itemid): 216 | raise ValueError('Itemid must be a integer') 217 | return self._get_data(region,datatypes['item']['path'] % (itemid),None,lastmodified,lang) 218 | 219 | 220 | def get_character(self,region,realm,character,params=None,lastmodified=None,lang=None): 221 | """ 222 | Get infos about an character, params is a array taking optional fields to look up infos like achievements,talents etc 223 | 224 | | ``Example:`` 225 | :: 226 | 227 | get_character('eu','Doomhammer','Thetotemlord',['talents']) 228 | """ 229 | return self._get_data(region,datatypes['character']['path'] % (quote(realm),quote(character)),params,lastmodified,lang,'character') 230 | 231 | def get_guild(self,region,realm,guild,params=None,lastmodified=None,lang=None): 232 | """ 233 | Get infos about an guild, params is a array taking optional fields to look up infos like achievements,members etc 234 | 235 | | ``Example:`` 236 | :: 237 | 238 | get_guild('eu','Doomhammer','Dawn Of Osiris') 239 | """ 240 | return self._get_data(region,datatypes['guild']['path'] % (quote(realm),quote(guild)),params,lastmodified,lang,'guild') 241 | 242 | def get_realm(self,region,params=None,lastmodified=None,lang=None): 243 | """ 244 | Get infos about realm(s), params is a array taking optional which realms to look up otherwise returning all realms of an region 245 | 246 | | ``Example:`` 247 | :: 248 | 249 | get_realm('eu',['Doomhammer']) 250 | """ 251 | return self._get_data(region,datatypes['realm']['path'],params,lastmodified,lang,'realm') 252 | 253 | def get_auctions(self,region,realm,lastmodified=None,lang=None): 254 | """ 255 | Returns all auctions of a realms 256 | 257 | | ``Example:`` 258 | :: 259 | 260 | get_auction('eu','Doomhammer') 261 | """ 262 | data = self._get_data(region,datatypes['auction']['path'] % (quote(realm)),None,lastmodified,lang) 263 | request = Request(data['data']['files'][0]['url'], None, {'Accept-Encoding': 'gzip'}) 264 | 265 | return {'lastmodified': data['lastmodified'],'data':self._decode_response(self._do_request(request))} 266 | 267 | def get_arena_team(self,region,realm,teamsize,teamname,lastmodified=None,lang=None): 268 | """ 269 | Get infos about a arena team 270 | 271 | | ``Example:`` 272 | :: 273 | 274 | get_arenea_team('eu','Doomhammer','2v2','We win') 275 | """ 276 | return self._get_data(region,datatypes['arena_team']['path'] % (quote(realm),teamsize,quote(teamname)),None,lastmodified,lang) 277 | 278 | def get_arena_ladder(self,region,battlegroup,teamsize,howmany=None,lastmodified=None,lang=None): 279 | """ 280 | Get the arena ladder of the specified battlegroup, optional with howmany you can define how many teams should be included 281 | 282 | | ``Example:`` 283 | :: 284 | 285 | get_arena_ladder('eu','Blackout','5v5',100) 286 | """ 287 | return self._get_data(region,datatypes['arena_ladder']['path'] % (quote(battlegroup),teamsize),[howmany],lastmodified,lang,'arena_ladder') 288 | 289 | def get_character_races(self,region,lastmodified=None,lang=None): 290 | """ 291 | Get infos about all character races 292 | 293 | | ``Example:`` 294 | :: 295 | 296 | get_character_races('us',None,'es_MX') 297 | """ 298 | return self._get_data(region,datatypes['character_races']['path'],None,lastmodified,lang) 299 | 300 | def get_character_classes(self,region,lastmodified=None,lang=None): 301 | """ 302 | Get infos about all character classes 303 | 304 | | ``Example:`` 305 | :: 306 | 307 | get_character_class('eu') 308 | """ 309 | return self._get_data(region,datatypes['character_classes']['path'],None,lastmodified,lang) 310 | 311 | def get_guild_rewards(self,region,lastmodified=None,lang=None): 312 | """ 313 | Get infos about all guild rewards 314 | 315 | | ``Example:`` 316 | :: 317 | 318 | get_guild_rewards('cn') 319 | """ 320 | return self._get_data(region,datatypes['guild_rewards']['path'],None,lastmodified,lang) 321 | 322 | def get_guild_perks(self,region,lastmodified=None,lang=None): 323 | """ 324 | Get infos about all guild perks 325 | 326 | | ``Example:`` 327 | :: 328 | 329 | get_guild_perks('tw') 330 | """ 331 | return self._get_data(region,datatypes['guild_perks']['path'],None,lastmodified,lang) 332 | 333 | def get_item_classes(self,region,lastmodified=None,lang=None): 334 | """ 335 | Get infos about all item classes 336 | 337 | | ``Example:`` 338 | :: 339 | 340 | get_item_classes('eu',None,'fr_FR') 341 | """ 342 | return self._get_data(region,datatypes['item_classes']['path'],None,lastmodified,lang) 343 | 344 | 345 | def get_quest(self,region,questid,lastmodified=None,lang=None): 346 | """ 347 | .. versionadded:: 0.2.3 348 | 349 | Get infos about an quest 350 | 351 | | ``Example:`` 352 | :: 353 | 354 | get_quest('eu',25) 355 | """ 356 | if not int(questid): 357 | raise ValueError('Quest id must be a integer') 358 | return self._get_data(region,datatypes['quest']['path'] % (questid),None,lastmodified,lang) 359 | 360 | def get_recipe(self,region,recipeid,lastmodified=None,lang=None): 361 | """ 362 | .. versionadded:: 0.3.0 363 | 364 | Get infos about an recipe 365 | 366 | | ``Example:`` 367 | :: 368 | 369 | get_recipe('eu',33994) 370 | """ 371 | if not int(recipeid): 372 | raise ValueError('Recipe id must be a integer') 373 | return self._get_data(region,datatypes['recipe']['path'] % (recipeid),None,lastmodified,lang) 374 | 375 | def get_achievements_character(self,region,lastmodified=None,lang=None): 376 | """ 377 | .. versionadded:: 0.2.3 378 | 379 | Get all character achievements which exists with name,description etc 380 | 381 | | ``Example:`` 382 | :: 383 | 384 | get_achievements_character('eu',None,'en_GB') 385 | """ 386 | return self._get_data(region,datatypes['achievements_character']['path'],None,lastmodified,lang) 387 | 388 | def get_achievements_guild(self,region,lastmodified=None,lang=None): 389 | """ 390 | .. versionadded:: 0.2.3 391 | 392 | Get all guild achievements which exists with name,description etc 393 | 394 | | ``Example:`` 395 | :: 396 | 397 | get_achievements_guild('eu',None,'fr_FR') 398 | """ 399 | return self._get_data(region,datatypes['achievements_guild']['path'],None,lastmodified,lang) --------------------------------------------------------------------------------