├── .gitignore ├── Configurations.txt ├── README.md └── Revolution Rotation Calculator.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/macos,python,windows,jetbrains+all 3 | 4 | ### JetBrains+all ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff: 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/dictionaries 12 | 13 | # Sensitive or high-churn files: 14 | .idea/**/dataSources/ 15 | .idea/**/dataSources.ids 16 | .idea/**/dataSources.xml 17 | .idea/**/dataSources.local.xml 18 | .idea/**/sqlDataSources.xml 19 | .idea/**/dynamic.xml 20 | .idea/**/uiDesigner.xml 21 | 22 | # Gradle: 23 | .idea/**/gradle.xml 24 | .idea/**/libraries 25 | 26 | # CMake 27 | cmake-build-debug/ 28 | 29 | # Mongo Explorer plugin: 30 | .idea/**/mongoSettings.xml 31 | 32 | ## File-based project format: 33 | *.iws 34 | 35 | ## Plugin-specific files: 36 | 37 | # IntelliJ 38 | /out/ 39 | 40 | # mpeltonen/sbt-idea plugin 41 | .idea_modules/ 42 | 43 | # JIRA plugin 44 | atlassian-ide-plugin.xml 45 | 46 | # Cursive Clojure plugin 47 | .idea/replstate.xml 48 | 49 | # Ruby plugin and RubyMine 50 | /.rakeTasks 51 | 52 | # Crashlytics plugin (for Android Studio and IntelliJ) 53 | com_crashlytics_export_strings.xml 54 | crashlytics.properties 55 | crashlytics-build.properties 56 | fabric.properties 57 | 58 | ### JetBrains+all Patch ### 59 | # Ignores the whole idea folder 60 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 61 | 62 | .idea/ 63 | 64 | ### macOS ### 65 | *.DS_Store 66 | .AppleDouble 67 | .LSOverride 68 | 69 | # Icon must end with two \r 70 | Icon 71 | 72 | # Thumbnails 73 | ._* 74 | 75 | # Files that might appear in the root of a volume 76 | .DocumentRevisions-V100 77 | .fseventsd 78 | .Spotlight-V100 79 | .TemporaryItems 80 | .Trashes 81 | .VolumeIcon.icns 82 | .com.apple.timemachine.donotpresent 83 | 84 | # Directories potentially created on remote AFP share 85 | .AppleDB 86 | .AppleDesktop 87 | Network Trash Folder 88 | Temporary Items 89 | .apdisk 90 | 91 | ### Python ### 92 | # Byte-compiled / optimized / DLL files 93 | __pycache__/ 94 | *.py[cod] 95 | *$py.class 96 | 97 | # C extensions 98 | *.so 99 | 100 | # Distribution / packaging 101 | .Python 102 | build/ 103 | develop-eggs/ 104 | dist/ 105 | downloads/ 106 | eggs/ 107 | .eggs/ 108 | lib/ 109 | lib64/ 110 | parts/ 111 | sdist/ 112 | var/ 113 | wheels/ 114 | *.egg-info/ 115 | .installed.cfg 116 | *.egg 117 | 118 | # PyInstaller 119 | # Usually these files are written by a python script from a template 120 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 121 | *.manifest 122 | *.spec 123 | 124 | # Installer logs 125 | pip-log.txt 126 | pip-delete-this-directory.txt 127 | 128 | # Unit test / coverage reports 129 | htmlcov/ 130 | .tox/ 131 | .coverage 132 | .coverage.* 133 | .cache 134 | nosetests.xml 135 | coverage.xml 136 | *.cover 137 | .hypothesis/ 138 | 139 | # Translations 140 | *.mo 141 | *.pot 142 | 143 | # Django stuff: 144 | *.log 145 | local_settings.py 146 | 147 | # Flask stuff: 148 | instance/ 149 | .webassets-cache 150 | 151 | # Scrapy stuff: 152 | .scrapy 153 | 154 | # Sphinx documentation 155 | docs/_build/ 156 | 157 | # PyBuilder 158 | target/ 159 | 160 | # Jupyter Notebook 161 | .ipynb_checkpoints 162 | 163 | # pyenv 164 | .python-version 165 | 166 | # celery beat schedule file 167 | celerybeat-schedule 168 | 169 | # SageMath parsed files 170 | *.sage.py 171 | 172 | # Environments 173 | .env 174 | .venv 175 | env/ 176 | venv/ 177 | ENV/ 178 | env.bak/ 179 | venv.bak/ 180 | 181 | # Spyder project settings 182 | .spyderproject 183 | .spyproject 184 | 185 | # Rope project settings 186 | .ropeproject 187 | 188 | # mkdocs documentation 189 | /site 190 | 191 | # mypy 192 | .mypy_cache/ 193 | 194 | ### Windows ### 195 | # Windows thumbnail cache files 196 | Thumbs.db 197 | ehthumbs.db 198 | ehthumbs_vista.db 199 | 200 | # Folder config file 201 | Desktop.ini 202 | 203 | # Recycle Bin used on file shares 204 | $RECYCLE.BIN/ 205 | 206 | # Windows Installer files 207 | *.cab 208 | *.msi 209 | *.msm 210 | *.msp 211 | 212 | # Windows shortcuts 213 | *.lnk 214 | 215 | # End of https://www.gitignore.io/api/macos,python,windows,jetbrains+all -------------------------------------------------------------------------------- /Configurations.txt: -------------------------------------------------------------------------------- 1 | # Rotation Parameters 2 | 3 | Adrenaline: 4 | Gain: 5 | AttackSpeed: 6 | Bleeds: 7 | Stuns: 8 | Abilities: [,,,] 9 | Style: (,) 10 | Time: 11 | 12 | # Mode 13 | 14 | units: seconds 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Revolution-Rotation-Calculator- 2 | 3 | A Python commandline application for generating RuneScape Ability Bars for use with Revolution++ 4 | 5 | ## Getting Started 6 | 7 | Refer to the *Installing* section on getting the project setup locally. 8 | 9 | Start by filling out required configurations in configurations.txt and appropriate abilities to be included. 10 | 11 | Once the project has been downloaded locally, navigate into the download directory and execute the command `python3 Revolution Rotation Calculator.py` to start the program. Follow the prompts to generate an ability bar. 12 | 13 | ```bash 14 | $ cd Revolution-Rotation-Calculator- 15 | $ python3 Revolution Rotation Calculator.py 16 | Startup Complete! Warning, the more abilities and the higher the time entered, higher wait times will be reached. A better processor will improve this speed. 17 | Start Calculations? (Y/N) Y 18 | ``` 19 | 20 | ### Prerequisites 21 | 22 | - This project uses [Python 3] 23 | 24 | ### Installing 25 | 26 | - Install [Python 3] by following the instructions on the Python website. 27 | - Clone this repository to your local machine `git clone https://github.com/NightShadeI/Revolution-Rotation-Calculator-.git` 28 | 29 | ## Authors 30 | 31 | * [NightShadeI](https://github.com/NightShadeI) 32 | 33 | See also the list of [contributors](https://github.com/NightShadeI/Revolution-Rotation-Calculator-/graphs/contributors) who participated in this project. 34 | 35 | ## License 36 | 37 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 38 | 39 | [Python 3]: https://www.python.org/ 40 | -------------------------------------------------------------------------------- /Revolution Rotation Calculator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import itertools 3 | import math 4 | import sys 5 | import time 6 | from typing import List, Dict, Tuple 7 | 8 | abilities: List[str] = ["ASPHYXIATE", "ASSAULT", "BACKHAND", "BARGE", "BERSERK", "BINDING SHOT", "BLOOD TENDRILS", 9 | "BOMBARDMENT", "CHAIN", "CLEAVE", "COMBUST", "CONCENTRATED BLAST", "CORRUPTION BLAST", 10 | "CORRUPTION SHOT", "DAZING SHOT", "DEADSHOT", "DEATH'S SWIFTNESS", "DEBILITATE", "DECIMATE", 11 | "DEEP IMPACT", "DESTROY", "DETONATE", "DISMEMBER", "DRAGON BREATH", "FLURRY", 12 | "FORCEFUL BACKHAND", "FRAGMENTATION SHOT", "FRENZY", "FURY", "HAVOC", "HURRICANE", "IMPACT", 13 | "KICK", "MASSACRE", "METAMORPHOSIS", "NEEDLE STRIKE", "OMNIPOWER", "ONSLAUGHT", "OVERPOWER", 14 | "PIERCING SHOT", "PULVERISE", "PUNISH", "QUAKE", "RAPID FIRE", "RICOCHET", "SACRIFICE", "SEVER", 15 | "SHADOW TENDRILS", "SHATTER", "SLAUGHTER", "SLICE", "SMASH", "SMOKE TENDRILS", "SNAP SHOT", 16 | "SNIPE", "SONIC WAVE", "STOMP", "STORM SHARDS", "SUNSHINE", "TIGHT BINDINGS", "TSUNAMI", 17 | "TUSKA'S WRATH", "UNLOAD", "WILD MAGIC", "WRACK"] 18 | 19 | # --- Defining how abilities work --- # 20 | 21 | # Define cooldowns for cases where no abilities may be used 22 | attack_speed_cooldowns: Dict[str, float] = {"FASTEST": 2.4, "FAST": 3.0, "AVERAGE": 3.6, "SLOW": 4.2, "SLOWEST": 7.2} 23 | 24 | # Define ability damage for every ability 25 | ability_damage: Dict[str, float] = {"ASPHYXIATE": 451.2, "ASSAULT": 525.6, "BACKHAND": 60, "BARGE": 75, "BERSERK": 0, 26 | "BINDING SHOT": 60, "BLOOD TENDRILS": 324, "BOMBARDMENT": 131.4, "CHAIN": 60, 27 | "CLEAVE": 112.8, "COMBUST": 241.2, "CONCENTRATED BLAST": 152.8, 28 | "CORRUPTION BLAST": 200, "CORRUPTION SHOT": 200, "DAZING SHOT": 94.2, 29 | "DEADSHOT": 426.13, "DEATH'S SWIFTNESS": 0, "DEBILITATE": 60, "DECIMATE": 112.8, 30 | "DEEP IMPACT": 120, "DESTROY": 451.2, "DETONATE": 225, "DISMEMBER": 120.6, 31 | "DRAGON BREATH": 112.8, "FLURRY": 204, "FORCEFUL BACKHAND": 120, 32 | "FRAGMENTATION SHOT": 120.6, "FRENZY": 610, "FURY": 152.8, "HAVOC": 94.2, 33 | "HURRICANE": 265, "IMPACT": 60, "KICK": 60, "MASSACRE": 426.13, "METAMORPHOSIS": 0, 34 | "NEEDLE STRIKE": 94.2, "OMNIPOWER": 300, "ONSLAUGHT": 532, "OVERPOWER": 300, 35 | "PIERCING SHOT": 56.4, "PULVERISE": 300, "PUNISH": 56.4, "QUAKE": 131.4, 36 | "RAPID FIRE": 451.2, "RICOCHET": 60, "SACRIFICE": 60, "SEVER": 112.8, 37 | "SHADOW TENDRILS": 283, "SHATTER": 0, "SLAUGHTER": 145, "SLICE": 75, "SMASH": 94.2, 38 | "SMOKE TENDRILS": 345, "SNAP SHOT": 265, "SNIPE": 172, "SONIC WAVE": 94.2, 39 | "STOMP": 120, "STORM SHARDS": 0, "SUNSHINE": 0, "TIGHT BINDINGS": 120, 40 | "TSUNAMI": 250, "TUSKA'S WRATH": 5940, "UNLOAD": 610, "WILD MAGIC": 265, 41 | "WRACK": 56.4} 42 | 43 | # Define the cooldown of abilities (in seconds) 44 | ability_cooldown: Dict[str, float] = {"ASPHYXIATE": 20.4, "ASSAULT": 30, "BACKHAND": 15, "BARGE": 20.4, "BERSERK": 60, 45 | "BINDING SHOT": 15, "BLOOD TENDRILS": 45, "BOMBARDMENT": 30, "CHAIN": 10.2, 46 | "CLEAVE": 7.2, "COMBUST": 15, "CONCENTRATED BLAST": 5.4, "CORRUPTION BLAST": 15, 47 | "CORRUPTION SHOT": 15, "DAZING SHOT": 5.4, "DEADSHOT": 30, 48 | "DEATH'S SWIFTNESS": 60, "DEBILITATE": 30, "DECIMATE": 7.2, "DEEP IMPACT": 15, 49 | "DESTROY": 20.4, "DETONATE": 30, "DISMEMBER": 15, "DRAGON BREATH": 10.2, 50 | "FLURRY": 20.4, "FORCEFUL BACKHAND": 15, "FRAGMENTATION SHOT": 15, "FRENZY": 60, 51 | "FURY": 5.4, "HAVOC": 10.2, "HURRICANE": 20.4, "IMPACT": 15, "KICK": 15, 52 | "MASSACRE": 60, "METAMORPHOSIS": 60, "NEEDLE STRIKE": 5.4, "OMNIPOWER": 30, 53 | "ONSLAUGHT": 120, "OVERPOWER": 60, "PIERCING SHOT": 3, "PULVERISE": 60, 54 | "PUNISH": 3, "QUAKE": 20.4, "RAPID FIRE": 20.4, "RICOCHET": 10.2, "SACRIFICE": 30, 55 | "SEVER": 15, "SHADOW TENDRILS": 45, "SHATTER": 120, "SLAUGHTER": 30, "SLICE": 3, 56 | "SMASH": 10.2, "SMOKE TENDRILS": 45, "SNAP SHOT": 20.4, "SNIPE": 10.2, 57 | "SONIC WAVE": 5.4, "STOMP": 15, "STORM SHARDS": 30, "SUNSHINE": 60, 58 | "TIGHT BINDINGS": 15, "TSUNAMI": 60, "TUSKA'S WRATH": 120, "UNLOAD": 60, 59 | "WILD MAGIC": 20.4, "WRACK": 3} 60 | 61 | # How long it takes to use each ability 62 | ability_time: Dict[str, float] = {"ASPHYXIATE": 5.4, "ASSAULT": 5.4, "BACKHAND": 1.8, "BARGE": 1.8, "BERSERK": 1.8, 63 | "BINDING SHOT": 1.8, "BLOOD TENDRILS": 1.8, "BOMBARDMENT": 1.8, "CHAIN": 1.8, 64 | "CLEAVE": 1.8, "COMBUST": 1.8, "CONCENTRATED BLAST": 3.6, "CORRUPTION BLAST": 1.8, 65 | "CORRUPTION SHOT": 1.8, "DAZING SHOT": 1.8, "DEADSHOT": 1.8, "DEATH'S SWIFTNESS": 1.8, 66 | "DEBILITATE": 1.8, "DECIMATE": 1.8, "DEEP IMPACT": 1.8, "DESTROY": 4.2, 67 | "DETONATE": 3.6, "DISMEMBER": 1.8, "DRAGON BREATH": 1.8, "FLURRY": 5.4, 68 | "FORCEFUL BACKHAND": 1.8, "FRAGMENTATION SHOT": 1.8, "FRENZY": 4.2, "FURY": 3.6, 69 | "HAVOC": 1.8, "HURRICANE": 1.8, "IMPACT": 1.8, "KICK": 1.8, "MASSACRE": 1.8, 70 | "METAMORPHOSIS": 1.8, "NEEDLE STRIKE": 1.8, "OMNIPOWER": 1.8, "ONSLAUGHT": 4.8, 71 | "OVERPOWER": 1.8, "PIERCING SHOT": 1.8, "PULVERISE": 1.8, "PUNISH": 1.8, "QUAKE": 1.8, 72 | "RAPID FIRE": 5.4, "RICOCHET": 1.8, "SACRIFICE": 1.8, "SEVER": 1.8, 73 | "SHADOW TENDRILS": 1.8, "SHATTER": 1.8, "SLAUGHTER": 1.8, "SLICE": 1.8, "SMASH": 1.8, 74 | "SMOKE TENDRILS": 5.4, "SNAP SHOT": 1.8, "SNIPE": 3.6, "SONIC WAVE": 1.8, 75 | "STOMP": 1.8, "STORM SHARDS": 1.8, "SUNSHINE": 1.8, "TIGHT BINDINGS": 1.8, 76 | "TSUNAMI": 1.8, "TUSKA'S WRATH": 1.8, "UNLOAD": 4.2, "WILD MAGIC": 4.8, "WRACK": 1.8} 77 | 78 | # Define the type of abilities (B = basic, T = threshold, U = ultimate) 79 | ability_type: Dict[str, str] = {"ASPHYXIATE": "T", "ASSAULT": "T", "BACKHAND": "B", "BARGE": "B", "BERSERK": "U", 80 | "BINDING SHOT": "B", "BLOOD TENDRILS": "T", "BOMBARDMENT": "T", "CHAIN": "B", 81 | "CLEAVE": "B", "COMBUST": "B", "CONCENTRATED BLAST": "B", "CORRUPTION BLAST": "B", 82 | "CORRUPTION SHOT": "B", "DAZING SHOT": "B", "DEADSHOT": "U", "DEATH'S SWIFTNESS": "U", 83 | "DEBILITATE": "T", "DECIMATE": "B", "DEEP IMPACT": "T", "DESTROY": "T", "DETONATE": "T", 84 | "DISMEMBER": "B", "DRAGON BREATH": "B", "FLURRY": "T", "FORCEFUL BACKHAND": "T", 85 | "FRAGMENTATION SHOT": "B", "FRENZY": "U", "FURY": "B", "HAVOC": "B", "HURRICANE": "U", 86 | "IMPACT": "B", "KICK": "B", "MASSACRE": "U", "METAMORPHOSIS": "U", "NEEDLE STRIKE": "B", 87 | "OMNIPOWER": "U", "ONSLAUGHT": "U", "OVERPOWER": "U", "PIERCING SHOT": "B", 88 | "PULVERISE": "U", "PUNISH": "B", "QUAKE": "T", "RAPID FIRE": "T", "RICOCHET": "B", 89 | "SACRIFICE": "B", "SEVER": "B", "SHADOW TENDRILS": "T", "SHATTER": "T", 90 | "SLAUGHTER": "T", "SLICE": "B", "SMASH": "B", "SMOKE TENDRILS": "T", "SNAP SHOT": "T", 91 | "SNIPE": "B", "SONIC WAVE": "B", "STOMP": "T", "STORM SHARDS": "B", "SUNSHINE": "U", 92 | "TIGHT BINDINGS": "T", "TSUNAMI": "U", "TUSKA'S WRATH": "B", "UNLOAD": "U", 93 | "WILD MAGIC": "T", "WRACK": "B"} 94 | 95 | # Define a flag to decide if you can use abilities (based on adrenaline) 96 | ability_ready: Dict[str, bool] = {"ASPHYXIATE": False, "ASSAULT": False, "BACKHAND": True, "BARGE": True, 97 | "BERSERK": False, "BINDING SHOT": True, "BLOOD TENDRILS": False, "BOMBARDMENT": False, 98 | "CHAIN": True, "CLEAVE": True, "COMBUST": True, "CONCENTRATED BLAST": True, 99 | "CORRUPTION BLAST": True, "CORRUPTION SHOT": True, "DAZING SHOT": True, 100 | "DEADSHOT": False, "DEATH'S SWIFTNESS": False, "DEBILITATE": False, "DECIMATE": True, 101 | "DEEP IMPACT": False, "DESTROY": False, "DETONATE": False, "DISMEMBER": True, 102 | "DRAGON BREATH": True, "FLURRY": False, "FORCEFUL BACKHAND": False, 103 | "FRAGMENTATION SHOT": True, "FRENZY": True, "FURY": True, "HAVOC": True, 104 | "HURRICANE": False, "IMPACT": True, "KICK": True, "MASSACRE": False, 105 | "METAMORPHOSIS": False, "NEEDLE STRIKE": True, "OMNIPOWER": False, "ONSLAUGHT": False, 106 | "OVERPOWER": False, "PIERCING SHOT": True, "PULVERISE": False, "PUNISH": True, 107 | "QUAKE": False, "RAPID FIRE": False, "RICOCHET": True, "SACRIFICE": True, 108 | "SEVER": True, "SHADOW TENDRILS": False, "SHATTER": False, "SLAUGHTER": False, 109 | "SLICE": True, "SMASH": True, "SMOKE TENDRILS": False, "SNAP SHOT": False, 110 | "SNIPE": True, "SONIC WAVE": True, "STOMP": False, "STORM SHARDS": True, 111 | "SUNSHINE": False, "TIGHT BINDINGS": False, "TSUNAMI": False, "TUSKA'S WRATH": True, 112 | "UNLOAD": False, "WILD MAGIC": False, "WRACK": True} 113 | 114 | # Define the time DOT abilities last (in seconds) 115 | bleeds: Dict[str, float] = {"BLOOD TENDRILS": 4.8, "COMBUST": 6, "CORRUPTION BLAST": 6, "CORRUPTION SHOT": 6, 116 | "DEADSHOT": 6, "DISMEMBER": 6, "FRAGMENTATION SHOT": 6, "MASSACRE": 6, 117 | "SHADOW TENDRILS": 1.8, "SLAUGHTER": 6, "SMOKE TENDRILS": 5.4} 118 | 119 | # Define damage multiplier of walking bleeds 120 | walking_bleeds: Dict[str, float] = {"COMBUST": 1, "FRAGMENTATION SHOT": 1, "SLAUGHTER": 1.5} 121 | 122 | # Define bleed abilities that have their first hit affected by damage modifying abilities 123 | special_bleeds: List[str] = ["DEADSHOT", "MASSACRE", "SMOKE TENDRILS"] 124 | 125 | # Define abilities that take longer than 1.8 seconds to use but will still have full impact from abilities in the 126 | # crit_boost list 127 | special_abilities: List[str] = ["DETONATE", "SNIPE"] 128 | 129 | # How long stuns, DPS increases .. etc last 130 | buff_time: Dict[str, float] = {"BARGE": 6.6, "BERSERK": 19.8, "BINDING SHOT": 9.6, "CONCENTRATED BLAST": 5.4, 131 | "DEATH'S SWIFTNESS": 30, "DEEP IMPACT": 3.6, "FORCEFUL BACKHAND": 3.6, "FURY": 5.4, 132 | "METAMORPHOSIS": 15, "NEEDLE STRIKE": 3.6, "RAPID FIRE": 6, "STOMP": 3.6, "SUNSHINE": 30, 133 | "TIGHT BINDINGS": 9.6} 134 | 135 | # Define the Multiplier for boosted damage 136 | buff_effect: Dict[str, float] = {"BERSERK": 2, "CONCENTRATED BLAST": 1.1, "DEATH'S SWIFTNESS": 1.5, "FURY": 1.1, 137 | "METAMORPHOSIS": 1.625, "NEEDLE STRIKE": 1.07, "PIERCING SHOT": 2, "PUNISH": 2, 138 | "SLICE": 1.506, "SUNSHINE": 1.5, "WRACK": 2} 139 | 140 | # Define crit-boosting abilities 141 | crit_boost: List[str] = ["BERSERK", "CONCENTRATED BLAST", "DEATH'S SWIFTNESS", "FURY", "METAMORPHOSIS", "NEEDLE STRIKE", 142 | "SUNSHINE"] 143 | 144 | # Define the abilities that do extra damage when the target is stun or bound 145 | punishing: List[str] = ["PIERCING SHOT", "PUNISH", "SLICE", "WRACK"] 146 | 147 | # Define abilities that can stun or bind the target 148 | debilitating: List[str] = ["BARGE", "BINDING SHOT", "DEEP IMPACT", "FORCEFUL BACKHAND", "RAPID FIRE", "STOMP", 149 | "TIGHT BINDINGS"] 150 | 151 | # Define abilities that bind the target 152 | binds: List[str] = ["BARGE", "BINDING SHOT", "DEEP IMPACT", "TIGHT BINDINGS"] 153 | 154 | # Define area of effect abilities 155 | aoe: List[str] = ["BOMBARDMENT", "CHAIN", "CLEAVE", "CORRUPTION BLAST", "CORRUPTION SHOT", "DRAGON BREATH", "FLURRY", 156 | "HURRICANE", "QUAKE", "RICOCHET", "TSUNAMI"] 157 | 158 | start_adrenaline: int 159 | gain: int 160 | attack_speed: str 161 | activate_bleeds: bool 162 | my_abilities: List[str] 163 | auto_adrenaline: int 164 | cycle_duration: float 165 | 166 | 167 | # Will return how much damage an ability bar will do over a given time 168 | def ability_rotation(permutation: List[str]) -> float: 169 | # Will check if an auto attack is needed to be used 170 | def auto_available() -> bool: 171 | for ability in track_cooldown: 172 | if (ability_cooldown[ability] - track_cooldown[ability]) < attack_speed_cooldowns[attack_speed]: 173 | return False 174 | return True 175 | 176 | # Decreases Cooldowns of abilities and buffs as well as modifying and damage multipliers 177 | def adjust_cooldowns(current_buff: float, adrenaline: int, cooldown_time: float) -> float: 178 | for ability in track_cooldown: 179 | track_cooldown[ability] += cooldown_time 180 | track_cooldown[ability] = round(track_cooldown[ability], 1) 181 | if track_cooldown[ability] >= ability_cooldown[ability]: 182 | track_cooldown[ability] = 0 183 | if ability in threshold_ability_list: 184 | if adrenaline >= 50: 185 | ability_ready[ability] = True 186 | elif ability in ultimate_ability_list: 187 | if adrenaline == 100: 188 | ability_ready[ability] = True 189 | else: 190 | ability_ready[ability] = True 191 | for ability in permutation: 192 | if ability in track_cooldown and track_cooldown[ability] == 0: 193 | del track_cooldown[ability] 194 | for ability in track_buff: 195 | track_buff[ability] += cooldown_time 196 | track_buff[ability] = round(track_buff[ability], 1) 197 | if track_buff[ability] >= buff_time[ability]: 198 | track_buff[ability] = 0 199 | for ability in permutation: 200 | if ability in track_buff and track_buff[ability] == 0: 201 | del track_buff[ability] 202 | if (ability not in punishing) and (ability in buff_effect): 203 | current_buff = current_buff / buff_effect[ability] 204 | return current_buff 205 | 206 | # Determines if enemy vulnerable to extra damage due to stuns or binds 207 | def buff_available() -> bool: 208 | for ability in debilitating: 209 | if ability in track_buff: 210 | return True 211 | return False 212 | 213 | def modify_time(cycle_duration: float, time_elapsed: float, ability: str) -> float: 214 | if (ability in bleeds) and (ability != "SHADOW TENDRILS") and ( 215 | (cycle_duration - time_elapsed) < bleeds[ability]): 216 | return (cycle_duration - time_elapsed) / bleeds[ability] 217 | else: 218 | if (ability not in special_abilities) and (ability_time[ability] > 1.8) and ( 219 | (cycle_duration - time_elapsed) < ability_time[ability]): 220 | return (cycle_duration - time_elapsed) / ability_time[ability] 221 | return 1 222 | 223 | # --- Defining Variables --- # 224 | damage_dealt: float = 0 225 | current_buff: float = 1 226 | time_elapsed: float = 0 227 | shards: float = 0 228 | adrenaline = start_adrenaline 229 | # --- Calculations begin here --- # 230 | ability_path.append(f"AUTO D: {round(damage_dealt, 1)} T: {round(time_elapsed, 1)} A: {adrenaline}") 231 | damage_dealt += 50 232 | adrenaline += auto_adrenaline 233 | if adrenaline >= 100: 234 | adrenaline = 100 235 | for tracked_ability in ultimate_ability_list: 236 | ability_ready[tracked_ability] = True 237 | for tracked_ability in threshold_ability_list: 238 | ability_ready[tracked_ability] = True 239 | elif adrenaline >= 50: 240 | for tracked_ability in threshold_ability_list: 241 | ability_ready[tracked_ability] = True 242 | elif adrenaline < 0: 243 | adrenaline = 0 244 | time_elapsed += 0.6 245 | time_elapsed = round(time_elapsed, 1) 246 | while time_elapsed < cycle_duration: 247 | for ability in permutation: 248 | # Checks if ability can be used TODO: Check if this is necessary 249 | if time_elapsed < cycle_duration and ability_ready[ability] is True: 250 | ability_ready[ability] = False 251 | # --- Modifying adrenaline as required --- # 252 | ability_path.append(f"{ability} D: {round(damage_dealt, 1)} T: {round(time_elapsed, 1)}" 253 | f" A: {adrenaline}") 254 | if ability in basic_ability_list: 255 | adrenaline += 8 256 | elif ability in threshold_ability_list: 257 | adrenaline -= 15 258 | else: 259 | adrenaline = gain 260 | if adrenaline > 100: 261 | adrenaline = 100 262 | # --- Adding shards if they are used, or using them if activated --- # 263 | if ability == "STORM SHARDS": 264 | if shards < 10: 265 | shards += 1 266 | elif ability == "SHATTER": 267 | damage_dealt += round(shards * 85, 1) 268 | shards = 0 269 | # --- Calculating how much damage abilities should do --- # 270 | more_binds: bool = False 271 | altered_bleeds: bool = False 272 | modified_damage: bool = False 273 | damage_multiplier: float = 1 # Multiplier for damage due to damage boosting abilities 274 | bleed_multiplier: float = 1 # Multiplier in case target is bound (and bind about to run out) 275 | for tracked_ability in track_buff: 276 | if tracked_ability in crit_boost: 277 | if ((buff_time[tracked_ability] - track_buff[tracked_ability]) < ability_time[ability]) and ( 278 | (ability not in special_abilities) and (ability_time[ability] > 1.8)): 279 | damage_multiplier *= ( 280 | (((buff_time[tracked_ability] - track_buff[tracked_ability]) / ability_time[ability]) * 281 | (buff_effect[tracked_ability] - 1)) + 1) 282 | else: 283 | damage_multiplier *= buff_effect[tracked_ability] 284 | elif (tracked_ability in binds) and (activate_bleeds is True) and (ability in walking_bleeds) and ( 285 | len(debilitating) > 0): 286 | if (more_binds is False) and ( 287 | buff_time[tracked_ability] - track_buff[tracked_ability] < bleeds[ability]): 288 | bleed_multiplier = walking_bleeds[ability] * ( 289 | 1 + (buff_time[tracked_ability] - track_buff[tracked_ability]) / bleeds[ability]) 290 | else: 291 | bleed_multiplier = 1 292 | more_binds = True 293 | altered_bleeds = True 294 | if (activate_bleeds is True) and (ability in walking_bleeds) and (altered_bleeds is False): 295 | bleed_multiplier = walking_bleeds[ability] * 2 296 | time_multiplier = modify_time(cycle_duration, time_elapsed, ability) 297 | if ability in bleeds: 298 | if ability in special_bleeds: 299 | if ability == "SMOKE TENDRILS": 300 | damage_dealt += (ability_damage[ability] * damage_multiplier) 301 | else: 302 | ability_damage[ability] = ((112.8 * damage_multiplier) + 313.33) 303 | modified_damage = True 304 | damage_dealt += round(ability_damage[ability] * bleed_multiplier * time_multiplier, 1) 305 | if modified_damage is True: 306 | ability_damage[ability] = 426.13 307 | elif (ability in punishing) and (buff_available() is True): 308 | damage_dealt += round(ability_damage[ability] * buff_effect[ability] * damage_multiplier * 309 | time_multiplier, 1) 310 | else: 311 | damage_dealt += round(ability_damage[ability] * damage_multiplier * time_multiplier, 1) 312 | # --- Increasing rotation duration and managing cooldowns --- # 313 | time_elapsed += ability_time[ability] 314 | time_elapsed = round(time_elapsed, 1) 315 | track_cooldown[ability] = float(0) 316 | if ability in buff_time and ability not in punishing: 317 | track_buff[ability] = 0 318 | if ability in buff_effect: 319 | current_buff = current_buff * buff_effect[ability] 320 | # Will also manage cooldowns 321 | current_buff = adjust_cooldowns(current_buff, adrenaline, ability_time[ability]) 322 | break 323 | # --- Determines whether thresholds or ultimates may be used --- # 324 | if time_elapsed < cycle_duration: 325 | if adrenaline == 100: 326 | for tracked_ability in (a for a in ultimate_ability_list if a not in track_cooldown): 327 | ability_ready[tracked_ability] = True 328 | for tracked_ability in (a for a in threshold_ability_list if a not in track_cooldown): 329 | ability_ready[tracked_ability] = True 330 | elif adrenaline >= 50: 331 | for tracked_ability in (a for a in threshold_ability_list if a not in track_cooldown): 332 | ability_ready[tracked_ability] = True 333 | elif adrenaline < 50: 334 | for tracked_ability in threshold_ability_list: 335 | ability_ready[tracked_ability] = False 336 | if adrenaline != 100: 337 | for tracked_ability in ultimate_ability_list: 338 | ability_ready[tracked_ability] = False 339 | # --- Determines if any abilities available/ whether auto attacks must be used --- # 340 | if time_elapsed < cycle_duration: 341 | ability_available = False 342 | for _ in (a for a in permutation if ability_ready[a]): 343 | ability_available = True 344 | break 345 | if ability_available is False: 346 | if auto_available() is True: 347 | if (time_elapsed + attack_speed_cooldowns[attack_speed]) <= cycle_duration: 348 | time_elapsed += attack_speed_cooldowns[attack_speed] 349 | else: 350 | time_elapsed += (cycle_duration - time_elapsed) 351 | break 352 | ability_path.append(f"AUTO D: {round(damage_dealt, 1)} T: {round(time_elapsed, 1)} A: {adrenaline}") 353 | if float(cycle_duration - time_elapsed) >= 0.6: 354 | damage_dealt += round(50 * current_buff, 1) 355 | else: 356 | damage_dealt += round(float(50 * round(float((cycle_duration - time_elapsed) / 0.6), 1)) * 357 | current_buff, 1) 358 | adrenaline += auto_adrenaline 359 | time_elapsed += 0.6 360 | if adrenaline > 100: 361 | adrenaline = 100 362 | # Will also manage cooldowns 363 | current_buff = adjust_cooldowns(current_buff, adrenaline, (attack_speed_cooldowns[attack_speed] + 364 | 0.6)) 365 | else: 366 | time_elapsed += 0.6 367 | current_buff = adjust_cooldowns(current_buff, adrenaline, 0.6) 368 | time_elapsed = round(time_elapsed, 1) 369 | return damage_dealt 370 | 371 | 372 | def setup_config() -> None: 373 | global start_adrenaline, gain, attack_speed, activate_bleeds, debilitating, my_abilities, auto_adrenaline, cycle_duration 374 | 375 | def compare(lines) -> bool: 376 | # configuration followed by line number 377 | correct_data: Dict[str, int] = {"Adrenaline": 2, "Gain": 3, "AttackSpeed": 4, "Bleeds": 5, "Stuns": 6, 378 | "Abilities": 7, "Style": 8, "Time": 9, "units": 13} 379 | for setting in correct_data: 380 | if setting != lines[correct_data[setting]]: 381 | return False 382 | return True 383 | 384 | def validate(configurations) -> List[str]: 385 | error_log: List[str] = [] 386 | empty_field: bool = False 387 | for config in configurations: 388 | if config == "": 389 | empty_field = True 390 | if empty_field is True: 391 | error_log.append("One or more settings have been left empty.") 392 | 393 | try: 394 | setting: int = int(configurations[0]) 395 | if not (0 <= setting <= 100): 396 | error_log.append("Adrenaline must be between 0 and 100 inclusive.") 397 | except ValueError: 398 | error_log.append("Adrenaline must be an integer.") 399 | 400 | try: 401 | setting: int = int(configurations[1]) 402 | if not (0 <= setting <= 100): 403 | error_log.append("Gain must be a positive integer between 0 and 100 inclusive.") 404 | except ValueError: 405 | error_log.append("Gain must be an integer.") 406 | 407 | if configurations[2].upper() not in ("SLOWEST", "SLOW", "AVERAGE", "FAST", "FASTEST"): 408 | error_log.append("AttackSpeed must either be one of the following options: ('slowest, slow, average, fast," 409 | " fastest').") 410 | 411 | setting: str = configurations[3] 412 | if not ((setting.lower() == "false") or (setting.lower() == "true")): 413 | error_log.append("Bleeds must be true or false.") 414 | 415 | setting: str = configurations[4] 416 | if not ((setting.lower() == "false") or (setting.lower() == "true")): 417 | error_log.append("Stuns must be true or false.") 418 | 419 | setting: str = configurations[5] 420 | if setting[0] == "[" and setting[-1] == "]": 421 | setting = setting[1:-1].split(",") 422 | counter: Dict[str, int] = {} 423 | if len(setting) > 0: 424 | for ability in setting: 425 | ability = ability.upper().strip() 426 | if (ability not in abilities) and (ability not in counter): 427 | error_log.append(f"{ability.strip()} is not a recognised ability, or is not included in this " 428 | f"calculator.") 429 | if ability in counter: 430 | counter[ability] += 1 431 | if counter[ability] == 2: 432 | error_log.append(f"{(ability.strip())} is referenced 2 or more times within array. Ensure " 433 | f"it is only referenced once.") 434 | else: 435 | counter[ability] = 1 436 | else: 437 | error_log.append("No abilities were added") 438 | else: 439 | error_log.append("Abilities must be surrounded by square brackets [], and separated by comma's (,).") 440 | 441 | setting: str = configurations[6] 442 | if setting[0] == "(" and setting[-1] == ")": 443 | setting = setting[1:-1].split(",") 444 | if setting[0].upper() not in ("MAGIC", "RANGED", "MELEE"): 445 | error_log.append("First style option must be 'magic', 'ranged' or 'melee' (without quotes).") 446 | if setting[1] not in ("1", "2"): 447 | error_log.append("Second style option must be 1 or 2 (for 1 handed / 2 handed weapon)3") 448 | else: 449 | error_log.append("Style must start and end with round brackets (), with each option separated by a single " 450 | "comma (,).") 451 | 452 | try: 453 | setting: float = float(configurations[7]) 454 | if not (setting > 0): 455 | error_log.append("Time must be a number greater than zero.") 456 | except ValueError: 457 | error_log.append("Time must be a number.") 458 | 459 | if configurations[8].upper() not in ("SECONDS", "TICKS"): 460 | error_log.append("Units must be either 'seconds' or 'ticks' (without quotes).") 461 | 462 | return error_log 463 | 464 | def repair_config_file() -> None: 465 | repair: str = input("Configurations.txt has been modified, perform repair? (Y/N).\n>> ").upper() 466 | if (repair == "Y") or (repair == "YES"): 467 | import os 468 | correct_data: List[str] = ["# Rotation Parameters", "", "Adrenaline: ", "Gain: ", "AttackSpeed: ", 469 | "Bleeds: ", "Stuns: ", "Abilities: [,,,]", "Style: (,)", "Time: ", "", "# Mode", 470 | "", "units: seconds"] 471 | if os.path.exists("Configurations.txt"): 472 | os.remove("Configurations.txt") 473 | with open("Configurations.txt", "w") as settings: 474 | for line in correct_data: 475 | settings.write(line + str("\n")) 476 | input("Repair successful! fill out settings in Configurations.txt before running calculator again. " 477 | "Press enter to exit.\n>> ") 478 | sys.exit() 479 | 480 | # --- Gets data for setup --- # 481 | filedata: List[str] = [] 482 | configurations: List[str] = [] 483 | try: 484 | with open("Configurations.txt", "r") as settings: 485 | for line in settings: 486 | filedata.append(line.split(":")[0]) 487 | if ":" in line: 488 | configurations.append(line.split(":")[1].strip()) 489 | if compare(filedata) is False: 490 | repair_config_file() 491 | except: 492 | repair_config_file() 493 | error_log = validate(configurations) 494 | if len(error_log) > 0: 495 | print("Errors were found!!!\n") 496 | for error in error_log: 497 | print(error) 498 | input("\nCould not complete setup, please change fields accordingly and run the calculator again. " 499 | "Press enter to exit.\n>> ") 500 | sys.exit() 501 | start_adrenaline = int(configurations[0]) 502 | gain = int(configurations[1]) 503 | attack_speed = configurations[2].upper() 504 | activate_bleeds = configurations[3] 505 | bound: str = configurations[4] 506 | if bound == "False": 507 | debilitating = [] 508 | my_abilities = [] 509 | for ability in configurations[5][1:-1].split(","): 510 | my_abilities.append(ability.strip().upper()) 511 | # --- Different styles of combat tree give varying amounts of adrenaline from auto attacks --- # 512 | style: Tuple[str, str] = tuple(configurations[6][1:-1].split(",")) 513 | if style[0] == "MAGIC": 514 | auto_adrenaline = 2 515 | else: 516 | if style[1] != "2": 517 | auto_adrenaline = 2 518 | else: 519 | auto_adrenaline = 3 520 | cycle_duration = float(configurations[7]) 521 | units: str = configurations[8] 522 | if units == "ticks": 523 | cycle_duration *= 0.6 524 | 525 | 526 | def main() -> None: 527 | global basic_ability_list, threshold_ability_list, ultimate_ability_list, track_cooldown, track_buff, ability_path, ability_ready 528 | 529 | # Converts raw seconds into Years, Weeks, etc... 530 | def get_time(seconds: int) -> str: 531 | years: int = int(seconds / 31449600) 532 | seconds -= years * 31449600 533 | weeks: int = int(seconds / 604800) 534 | seconds -= weeks * 604800 535 | days: int = int(seconds / 86400) 536 | seconds -= days * 86400 537 | hours: int = int(seconds / 3600) 538 | seconds -= hours * 3600 539 | minutes: int = int(seconds / 60) 540 | seconds -= minutes * 60 541 | eta: str = f"{years} years, {weeks} weeks, {days} days, {hours} hours, {minutes} minutes and {seconds} seconds." 542 | return eta 543 | 544 | # Removes abilities from lists and dictionaries not being used to save runtime and memory 545 | def remove() -> Dict[str, bool]: 546 | for ability in abilities: 547 | if not (ability in my_abilities): 548 | del ability_damage[ability] 549 | del ability_cooldown[ability] 550 | del ability_type[ability] 551 | del ability_time[ability] 552 | del ability_ready[ability] 553 | if ability in walking_bleeds: 554 | del walking_bleeds[ability] 555 | if ability in bleeds: 556 | del bleeds[ability] 557 | if ability in buff_time: 558 | del buff_time[ability] 559 | if ability in buff_effect: 560 | del buff_effect[ability] 561 | if ability in punishing: 562 | punishing.remove(ability) 563 | if ability in debilitating: 564 | debilitating.remove(ability) 565 | if ability in crit_boost: 566 | crit_boost.remove(ability) 567 | if ability in special_bleeds: 568 | special_bleeds.remove(ability) 569 | if ability in special_abilities: 570 | special_abilities.remove(ability) 571 | if ability in aoe: 572 | aoe.remove(ability) 573 | return dict(ability_ready) 574 | 575 | setup_config() 576 | # --- Dictionaries, lists and other data types laid out here --- # 577 | print("Starting process ...") 578 | copy_of_ready = remove() 579 | basic_ability_list = [a for a in ability_type if ability_type[a] == "B"] 580 | threshold_ability_list = [a for a in ability_type if ability_type[a] == "T"] 581 | ultimate_ability_list = [a for a in ability_type if ability_type[a] == "U"] 582 | track_cooldown = {} 583 | track_buff = {} 584 | ability_path = [] 585 | best_rotation: List[str] = [] 586 | worst_rotation: List[str] = [] 587 | # --- Calculations for estimation of time remaining --- # 588 | permutation_count: int = math.factorial(len(my_abilities)) 589 | time_remaining_calculation: int = permutation_count / 10000 590 | runthrough: int = 0 591 | # --- Tracking of highest and lowest damaging ability bars --- # 592 | current_highest: float = 0 593 | current_lowest: float = float("inf") 594 | # Define the amount of targets affected by area of effect attacks 595 | aoe_average_targets_hit: float = 2.5 596 | # --- Gets rotation length --- # 597 | while True: 598 | try: 599 | if len(aoe) > 0: # Only ask if AoE abilities are in my_abilities 600 | aoe_average_targets_hit = float(input("How many targets on average will your AoE abilities hit? ")) 601 | if aoe_average_targets_hit < 1: 602 | print("Area of effect abilities should hit at least 1 target per use.") 603 | continue 604 | break 605 | except: 606 | print("Invalid Input.") 607 | if aoe_average_targets_hit > 1: 608 | for ability in my_abilities: 609 | if ability in aoe: 610 | ability_damage[ability] = ability_damage[ability] * aoe_average_targets_hit 611 | print("Startup Complete! Warning, the more the abilities, and the higher the cycle time, the more time it will take" 612 | " to process. A better processor will improve this speed.") 613 | choice: str = input("Start Calculations? (Y/N) ").upper() 614 | if (choice != "Y") and (choice != "YES"): 615 | sys.exit() 616 | # --- Calculations start here --- # 617 | start: int = int(time.time()) # Record time since epoch (UTC) (in seconds) 618 | try: # Will keep running until Control C (or other) is pressed to end process 619 | for permutation in itertools.permutations(my_abilities): 620 | damage_dealt: float = ability_rotation(permutation) 621 | # --- Reset data ready for next ability bar to be tested 622 | # and check if any better/worse bars have been found --- # 623 | ability_ready = dict(copy_of_ready) 624 | track_cooldown = {} 625 | track_buff = {} 626 | if round(damage_dealt, 1) > current_highest: 627 | current_highest = round(damage_dealt, 1) 628 | best_rotation = [] 629 | best_rotation = list(ability_path) 630 | best_bar: List[str] = list(permutation) 631 | print(f"New best bar with damage {current_highest}: {best_bar}") 632 | if round(damage_dealt, 1) < current_lowest: 633 | current_lowest = round(damage_dealt, 1) 634 | worst_rotation: List[str] = [] 635 | worst_rotation: List[str] = list(ability_path) 636 | worst_bar = list(permutation) 637 | ability_path = [] 638 | runthrough += 1 639 | # --- Time Remaining estimation calculations every 10,000 bars analysed --- # 640 | if runthrough == 10000: 641 | end_estimation = int(time_remaining_calculation * (time.time() - start)) 642 | if runthrough % 10000 == 0: 643 | print(f"\r===== {round(float(runthrough / permutation_count) * 100, 3)}" 644 | f"% ===== Estimated time remaining: {get_time(int(end_estimation - (time.time() - start)))}" 645 | f"; Best found: {current_highest}%" + (" " * 22), end="") 646 | time_remaining_calculation -= 1 647 | end_estimation = int(time_remaining_calculation * (time.time() - start)) 648 | start = time.time() 649 | except KeyboardInterrupt: 650 | print("\nProcess terminated!") 651 | # --- Display results --- # 652 | print(f"\n\nHighest ability damage: {current_highest}%") 653 | print(f"Best ability bar found: {best_bar}") 654 | print(f"{best_rotation}\n") 655 | print(f"Lowest ability damage: {current_lowest}%") 656 | print(f"Worst ability bar found: {worst_bar}") 657 | print(worst_rotation) 658 | input("\nPress enter to exit\n") 659 | 660 | 661 | # Execute main() function 662 | if __name__ == "__main__": 663 | main() 664 | --------------------------------------------------------------------------------