├── .gitignore ├── LICENSE ├── README.md ├── data ├── characters │ ├── two_mages.json │ ├── undead-lf-bis-no-udc-no-atiesh.json │ ├── undead-lf-bis-no-udc.json │ └── undead-lf-bis-udc.json ├── icons │ ├── ability_creature_poison_05.jpg │ ├── icon.png │ ├── icon2.png │ ├── inv_chest_cloth_04.jpg │ ├── inv_jewelry_necklace_13.jpg │ ├── inv_misc_food_63.jpg │ ├── inv_misc_gift_03.jpg │ ├── inv_misc_head_dragon_01.jpg │ ├── inv_misc_orb_02.jpg │ ├── inv_misc_stonetablet_11.jpg │ ├── inv_potion_105.jpg │ ├── inv_potion_25.jpg │ ├── inv_potion_26.jpg │ ├── inv_potion_41.jpg │ ├── inv_potion_60.jpg │ ├── inv_trinket_naxxramas06.jpg │ ├── inv_valentineschocolate02.jpg │ ├── spell_fire_fireball.jpg │ ├── spell_holy_elunesgrace.jpg │ ├── spell_holy_lesserheal02.jpg │ ├── spell_holy_magicalsentry.jpg │ ├── spell_holy_mindvision.jpg │ ├── spell_holy_powerinfusion.jpg │ ├── spell_ice_lament.jpg │ ├── spell_magic_greaterblessingofkings.jpg │ ├── spell_nature_regeneration.jpg │ ├── spell_nature_timestop.jpg │ └── spell_nature_wispheal.jpg ├── items.dat ├── pictures │ ├── anaconda_prompt.png │ ├── scenario_comparison.png │ ├── scenario_editor.png │ ├── scenario_selection.png │ └── stats_distribution.png ├── samples │ ├── compare.png │ └── distribution.png └── scenarios │ ├── default.json │ ├── mqg-mark-fireball-mts.json │ ├── presapp-mqg-fireball-cbi.json │ ├── presapp-mqg-fireball-mts.json │ ├── sapp-mark-fireball-wep-cbi.json │ ├── two_mages.json │ └── weight_mage.json └── src ├── gui ├── __init__.py ├── config_select.py ├── main.py ├── scenario_editor │ ├── __init__.py │ ├── buffs.py │ ├── character.py │ ├── mages.py │ ├── rotation.py │ └── scenario.py ├── simulation_interface │ ├── __init__.py │ ├── output.py │ ├── settings.py │ └── simulation.py └── utils │ ├── __init__.py │ ├── guard_lineedit.py │ ├── icon_edit.py │ └── worker.py ├── items ├── __init__.py ├── build_group.py └── items.py └── sim ├── config.py ├── constants.py ├── decisions.py ├── mechanics.py └── old_scripts ├── ce_damage.py ├── ce_equiv.py ├── ce_equiv2_fast.py ├── ce_equiv_2pi.py ├── ce_rotation_mark_vs_sapp.py ├── fire_mage_simulator.py ├── fixed_equiv.py ├── player_fight.py ├── regression.py ├── som_damage.py ├── som_equiv.py └── upgrade_lists.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Emacs droppings 2 | *~ 3 | \#* 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Ronkuby 2 | 3 | Source code is licensed under the Apache License, Version 2.0 4 | (the "License"); you may not use this file except in compliance with 5 | the License. You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | Icons included are ©2004 Blizzard Entertainment, Inc. All rights reserved. 16 | World of Warcraft, Warcraft and Blizzard Entertainment are trademarks 17 | or registered trademarks of Blizzard Entertainment, Inc. in the U.S. 18 | and/or other countries. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vanilla Fire Mage Simulation 2 | 3 | This application simulates a team of fire mages casting against a single boss level target within the framework of Classic Era mechanics. 4 | 5 | ### Installation 6 | 7 | Here are the steps to install and run the application on Windows. 8 | 1. Under the green "Code" link above, select "download ZIP" 9 | 2. Extract the ZIP file into a directory such as "C:\sims\" 10 | 3. Download and install the Anaconda package: [tested windows version](https://repo.anaconda.com/archive/Anaconda3-2023.07-2-Windows-x86_64.exe) 11 | 4. From the Start menu, open an Anaconda Prompt (see below) 12 | 5. Go to the directory you extracted the code to in Step 2 using the "cd" command. For the above example, ```cd \sims\fire-mage-simulation``` 13 | 6. Run the application with ```python -m src.gui.main``` 14 | 15 | ![](./data/pictures/anaconda_prompt.png) 16 | 17 | ## Walkthrough 18 | ### Scenario Editor 19 | Start by selecting the Scenario editor tab on the top left. 20 | ![](./data/pictures/scenario_editor.png) 21 | 22 | #### Directly Editing Stats 23 | Within the **Mages** section you can change the stats and other attributes of each mage. *From left to right*: The stats should be entered as from gear/enchant only for spell power, hit, and crit. Int should be entered as base value + gear, with no buffs. The trinket section informs the sim which active trinkets are available. Next are indicators for the UDC set bonus and whether the mage can receive PI. For now, only one PI per mage can be available. "Target" indicates whether that mage's personal DPS and their share of the ignite are included in the output. The expected output for only one mage on the team can be analyzed by checking only a single *target* box. Finally there are three aura indicators for Mage Atiesh, Warlock Atiesh, and Boomkin Crit. Only auras from other in-party members are counted here, self auras are not. Buttons on the far right (for the bottom mage) increase or decrease the number of mages. 24 | 25 | #### Loading Sixty Upgrades 26 | As an alterative to directly editing stats, they can be automatically populated by loading a character export from the excellent website [sixtyupgrades](https://sixtyupgrades.com). To do so, make a character on the website, and export it to file. Save the exported character to the directory ```.\data\characters\``` within the simulation installation. Then from the scenario editor, if you click "Mage 1", the exported charater should be available to load within the simulator. Importing a sixtyupagrades character automatically sets stats, trinkets, and set bonuses. These attributes can no longer be adjusted. Target, PI, and the auras are still adjustable as they are independent of gear. To release a mage to be adjusted manually again, click the name again. 27 | 28 | ### Rotation 29 | The **Rotation** section has an initial fixed set of rotation commands that each mage attempts to cast when the fight starts. Abilities that are not available to a mage are skipped and do not expend any time. For example if "mqg" is on the list but a mage doesn't have Mind Quickening Gem, when they get to that cast in the sequence it is ignored. The *Special* section assigns one or more mage to a continuing rotation in which usually scorch is cast given a set of conditions. Other mages spam the default spell once the initial rotation is complete. Details about the *special* rotations are given in the tooltip. After the initial rotation, and unless contradicted by a special ruleset, abilities that are on cooldown (combustion, trinkets, etc...) will be recast when they become available. 30 | 31 | ### Stat Weights/Distribution Run 32 | In the bottom right scenario selection panel, type in "weight_mage" for Scenario 1, and then press "Load". Switch to the Simulation tab and run the sim with default paramters. The output should look like this: 33 | 34 | ![](./data/pictures/stats_distribution.png) 35 | 36 | Lety's play around with a few of the scenario and simulation settings. 37 | * Switch back to the Scenario tab. Notice that for the weight_mage scenario, there are two tabs under rotation: "No PI" and "PI". Mages that have PI will follow the PI rotation, and those that don't will follow the "No PI" rotation. The purpose of having two rotations is to increase the chances of a PI buffed mage initiating the ignite by frost buffering the non-PI mages. 38 | * Change the "Special 1" rotation to "cobimf" for mage 3. Rerun the Stat weights. You should see a slight increase in DPS. 39 | * In the **Simualtion Settings** panel, deselect "Vary All Mages" and then click on all the numbers except 1. This looks at the effect of changing only the stats for Mage 1 to calculate the stat weights. So the **DPS** output should not be affected, but **Crit** and **Hit** will be. You should see the crit valuation go up a few SP points. Crit is more important for the well geared Mage 1 than it is for the team aggregate. 40 | * Return to the Scenario tab and deselect *target* for all mages but Mage 1. Rerun the simulation and you should see much higher DPS values. 41 | 42 | ### Setting up Multiple Scenarios 43 | The scenario selection panel is on the bottom right of the application. Here you can load, save, add, and remove multiple scenarios. The selection button on the left indicates which scenario is shown and edited in the scenario editor as well as the scenario that is run for stat weight/distribution type simulations. 44 | 45 | ![](./data/pictures/scenario_selection.png) 46 | 47 | ### Scenario Comparison Run 48 | A Scenario Comparison Run plots all scenarios in the selection panel as a function of encounter time. In the comparison plot below, BiS three mage teams with different trinkets and rotations are compared in encounters from 30s to 90s. Each trinket+rotation combination is the top DPS for an interval. The scenarios shown are based on the "undead-lf-bis-no-udc" character with only trinkets changed. Both the character and the scenarios shown are included in this package. 49 | 50 | ![](./data/pictures/scenario_comparison.png) 51 | 52 | ### Crit equivalency comparisons 53 | Here are some results from other simulations: 54 | * [Quasexort](https://docs.google.com/spreadsheets/d/1dqFuQeNVa403ulrmuW_8Ww-5UszOde0RPMBe2g7t1g4) 55 | * [elio](https://github.com/ignitelio/ignite/blob/master/magus2.ipynb) 56 | 57 | ### Acknowledgement 58 | *Thanks to elio for tracking down the error in ignite timing and providing parallel code sample!* 59 | *Thanks to alzy for the sim result comparison, which helped pin down a bug in the scorch refresh logic!* 60 | -------------------------------------------------------------------------------- /data/characters/two_mages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Two Mages", 3 | "phase": 6, 4 | "links": { 5 | "set": "https://sixtyupgrades.com/set/dmwuwJiwyirVVpMHdun8PT" 6 | }, 7 | "character": { 8 | "name": "Ronkuby", 9 | "level": 60, 10 | "gameClass": "MAGE", 11 | "race": "HUMAN", 12 | "faction": "ALLIANCE" 13 | }, 14 | "items": [ 15 | { 16 | "name": "Frostfire Circlet", 17 | "id": 22498, 18 | "enchant": { 19 | "name": "Presence of Sight", 20 | "id": 2588, 21 | "itemId": 19787 22 | }, 23 | "slot": "HEAD" 24 | }, 25 | { 26 | "name": "Gem of Trapped Innocents", 27 | "id": 23057, 28 | "slot": "NECK" 29 | }, 30 | { 31 | "name": "Rime Covered Mantle", 32 | "id": 22983, 33 | "enchant": { 34 | "name": "Power of the Scourge", 35 | "id": 2721, 36 | "itemId": 23545 37 | }, 38 | "slot": "SHOULDERS" 39 | }, 40 | { 41 | "name": "Frostfire Robe", 42 | "id": 22496, 43 | "enchant": { 44 | "name": "Enchant Chest - Greater Stats", 45 | "id": 1891, 46 | "spellId": 20025 47 | }, 48 | "slot": "CHEST" 49 | }, 50 | { 51 | "name": "Eyestalk Waist Cord", 52 | "id": 22730, 53 | "slot": "WAIST" 54 | }, 55 | { 56 | "name": "Leggings of Polarity", 57 | "id": 23070, 58 | "enchant": { 59 | "name": "Presence of Sight", 60 | "id": 2588, 61 | "itemId": 19787 62 | }, 63 | "slot": "LEGS" 64 | }, 65 | { 66 | "name": "Frostfire Sandals", 67 | "id": 22500, 68 | "enchant": { 69 | "name": "Enchant Boots - Minor Speed", 70 | "id": 911, 71 | "spellId": 13890 72 | }, 73 | "slot": "FEET" 74 | }, 75 | { 76 | "name": "Rockfury Bracers", 77 | "id": 21186, 78 | "enchant": { 79 | "name": "Enchant Bracer - Greater Intellect", 80 | "id": 1883, 81 | "spellId": 20008 82 | }, 83 | "slot": "WRISTS" 84 | }, 85 | { 86 | "name": "Dark Storm Gauntlets", 87 | "id": 21585, 88 | "enchant": { 89 | "name": "Enchant Gloves - Fire Power", 90 | "id": 2616, 91 | "spellId": 25078 92 | }, 93 | "slot": "HANDS" 94 | }, 95 | { 96 | "name": "Ring of the Eternal Flame", 97 | "id": 23237, 98 | "slot": "FINGER_1" 99 | }, 100 | { 101 | "name": "Seal of the Damned", 102 | "id": 23025, 103 | "slot": "FINGER_2" 104 | }, 105 | { 106 | "name": "Mark of the Champion", 107 | "id": 23207, 108 | "slot": "TRINKET_1" 109 | }, 110 | { 111 | "name": "The Restrained Essence of Sapphiron", 112 | "id": 23046, 113 | "slot": "TRINKET_2" 114 | }, 115 | { 116 | "name": "Cloak of the Necropolis", 117 | "id": 23050, 118 | "enchant": { 119 | "name": "Enchant Cloak - Subtlety", 120 | "id": 2621, 121 | "spellId": 25084 122 | }, 123 | "slot": "BACK" 124 | }, 125 | { 126 | "name": "Atiesh, Greatstaff of the Guardian", 127 | "id": 22589, 128 | "enchant": { 129 | "name": "Enchant Weapon - Spell Power", 130 | "id": 2504, 131 | "spellId": 22749 132 | }, 133 | "slot": "MAIN_HAND" 134 | }, 135 | { 136 | "name": "Doomfinger", 137 | "id": 22821, 138 | "slot": "RANGED" 139 | } 140 | ], 141 | "wishlist": [ 142 | { 143 | "name": "Band of Forced Concentration", 144 | "id": 19403, 145 | "type": "ARMOR" 146 | }, 147 | { 148 | "name": "Bracers of Arcane Accuracy", 149 | "id": 19374, 150 | "type": "ARMOR" 151 | }, 152 | { 153 | "name": "Neltharion's Tear", 154 | "id": 19379, 155 | "type": "ARMOR" 156 | } 157 | ], 158 | "points": [ 159 | { 160 | "name": "ball3", 161 | "stats": { 162 | "spellDamage": 1, 163 | "fireDamage": 1, 164 | "spellCrit": 18.5, 165 | "intellect": 0.39, 166 | "spellHit": 20 167 | } 168 | } 169 | ], 170 | "stats": { 171 | "agility": 39, 172 | "arcaneDamage": 647, 173 | "armor": 1025, 174 | "attackPower": 58, 175 | "crit": 5.41, 176 | "defense": 300, 177 | "dodge": 5.41, 178 | "fireDamage": 701, 179 | "frostDamage": 647, 180 | "healing": 647, 181 | "health": 3650, 182 | "holyDamage": 647, 183 | "intellect": 314, 184 | "mainHandSpeed": 2.9, 185 | "mana": 5643, 186 | "natureDamage": 647, 187 | "parry": 5, 188 | "rangedSpeed": 1.5, 189 | "shadowDamage": 647, 190 | "spellCrit": 22.48, 191 | "spellDamage": 647, 192 | "spellHit": 10, 193 | "spellPen": 13, 194 | "spirit": 172, 195 | "stamina": 246, 196 | "strength": 34 197 | }, 198 | "exportOptions": { 199 | "buffs": false, 200 | "talents": false 201 | } 202 | } -------------------------------------------------------------------------------- /data/characters/undead-lf-bis-no-udc-no-atiesh.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BIS no UDC no Atiesh", 3 | "phase": 6, 4 | "links": { 5 | "talents": "https://sixtyupgrades.com/talents/mage/0Ab3F?g" 6 | }, 7 | "character": { 8 | "name": "Everyday Mage", 9 | "level": 60, 10 | "gameClass": "MAGE", 11 | "race": "GNOME", 12 | "faction": "ALLIANCE" 13 | }, 14 | "items": [ 15 | { 16 | "name": "Frostfire Circlet", 17 | "id": 22498, 18 | "enchant": { 19 | "name": "Presence of Sight", 20 | "id": 2588, 21 | "itemId": 19787 22 | }, 23 | "slot": "HEAD" 24 | }, 25 | { 26 | "name": "Amulet of Vek'nilash", 27 | "id": 21608, 28 | "slot": "NECK" 29 | }, 30 | { 31 | "name": "Rime Covered Mantle", 32 | "id": 22983, 33 | "enchant": { 34 | "name": "Power of the Scourge", 35 | "id": 2721, 36 | "itemId": 23545 37 | }, 38 | "slot": "SHOULDERS" 39 | }, 40 | { 41 | "name": "Frostfire Robe", 42 | "id": 22496, 43 | "enchant": { 44 | "name": "Enchant Chest - Greater Stats", 45 | "id": 1891, 46 | "spellId": 20025 47 | }, 48 | "slot": "CHEST" 49 | }, 50 | { 51 | "name": "Eyestalk Waist Cord", 52 | "id": 22730, 53 | "slot": "WAIST" 54 | }, 55 | { 56 | "name": "Leggings of Polarity", 57 | "id": 23070, 58 | "enchant": { 59 | "name": "Presence of Sight", 60 | "id": 2588, 61 | "itemId": 19787 62 | }, 63 | "slot": "LEGS" 64 | }, 65 | { 66 | "name": "Frostfire Sandals", 67 | "id": 22500, 68 | "enchant": { 69 | "name": "Enchant Boots - Minor Speed", 70 | "id": 911, 71 | "spellId": 13890 72 | }, 73 | "slot": "FEET" 74 | }, 75 | { 76 | "name": "Rockfury Bracers", 77 | "id": 21186, 78 | "enchant": { 79 | "name": "Enchant Bracer - Greater Intellect", 80 | "id": 1883, 81 | "spellId": 20008 82 | }, 83 | "slot": "WRISTS" 84 | }, 85 | { 86 | "name": "Dark Storm Gauntlets", 87 | "id": 21585, 88 | "enchant": { 89 | "name": "Enchant Gloves - Fire Power", 90 | "id": 2616, 91 | "spellId": 25078 92 | }, 93 | "slot": "HANDS" 94 | }, 95 | { 96 | "name": "Ring of the Fallen God", 97 | "id": 21709, 98 | "slot": "FINGER_1" 99 | }, 100 | { 101 | "name": "Ring of the Eternal Flame", 102 | "id": 23237, 103 | "slot": "FINGER_2" 104 | }, 105 | { 106 | "name": "The Restrained Essence of Sapphiron", 107 | "id": 23046, 108 | "slot": "TRINKET_1" 109 | }, 110 | { 111 | "name": "Mark of the Champion", 112 | "id": 23207, 113 | "slot": "TRINKET_2" 114 | }, 115 | { 116 | "name": "Cloak of the Necropolis", 117 | "id": 23050, 118 | "enchant": { 119 | "name": "Enchant Cloak - Subtlety", 120 | "id": 2621, 121 | "spellId": 25084 122 | }, 123 | "slot": "BACK" 124 | }, 125 | { 126 | "name": "Wraith Blade", 127 | "id": 22807, 128 | "enchant": { 129 | "name": "Enchant Weapon - Spell Power", 130 | "id": 2504, 131 | "spellId": 22749 132 | }, 133 | "slot": "MAIN_HAND" 134 | }, 135 | { 136 | "name": "Sapphiron's Left Eye", 137 | "id": 23049, 138 | "slot": "OFF_HAND" 139 | }, 140 | { 141 | "name": "Doomfinger", 142 | "id": 22821, 143 | "slot": "RANGED" 144 | } 145 | ], 146 | "wishlist": [ 147 | { 148 | "name": "Band of Forced Concentration", 149 | "id": 19403, 150 | "type": "ARMOR" 151 | }, 152 | { 153 | "name": "Bracers of Arcane Accuracy", 154 | "id": 19374, 155 | "type": "ARMOR" 156 | }, 157 | { 158 | "name": "Neltharion's Tear", 159 | "id": 19379, 160 | "type": "ARMOR" 161 | } 162 | ], 163 | "consumables": [], 164 | "buffs": [], 165 | "talents": [ 166 | { 167 | "name": "Arcane Subtlety", 168 | "id": 74, 169 | "rank": 2, 170 | "spellId": 12592 171 | }, 172 | { 173 | "name": "Arcane Concentration", 174 | "id": 75, 175 | "rank": 5, 176 | "spellId": 12577 177 | }, 178 | { 179 | "name": "Arcane Focus", 180 | "id": 76, 181 | "rank": 3, 182 | "spellId": 12840 183 | } 184 | ], 185 | "points": [ 186 | { 187 | "name": "Frenzy (6 mages) WB", 188 | "stats": { 189 | "spellDamage": 1, 190 | "fireDamage": 1, 191 | "spellCrit": 9.6, 192 | "intellect": 0.2 193 | } 194 | }, 195 | { 196 | "name": "Frenzy (6 mages) no WB", 197 | "stats": { 198 | "spellDamage": 1, 199 | "fireDamage": 1, 200 | "spellCrit": 12, 201 | "intellect": 0.33 202 | } 203 | } 204 | ], 205 | "stats": { 206 | "agility": 42, 207 | "arcaneDamage": 646, 208 | "arcaneResist": 10, 209 | "armor": 1031, 210 | "attackPower": 48, 211 | "crit": 5.56, 212 | "defense": 300, 213 | "dodge": 5.56, 214 | "fireDamage": 700, 215 | "frostDamage": 646, 216 | "healing": 646, 217 | "health": 3430, 218 | "holyDamage": 646, 219 | "intellect": 325, 220 | "mainHandSpeed": 1.8, 221 | "mana": 5808, 222 | "natureDamage": 646, 223 | "parry": 5, 224 | "rangedSpeed": 1.5, 225 | "shadowDamage": 646, 226 | "spellCrit": 20.66, 227 | "spellDamage": 646, 228 | "spellHit": 10, 229 | "spellPen": 23, 230 | "spirit": 134, 231 | "stamina": 224, 232 | "strength": 29 233 | }, 234 | "exportOptions": { 235 | "buffs": true, 236 | "talents": true 237 | } 238 | } -------------------------------------------------------------------------------- /data/characters/undead-lf-bis-no-udc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BIS no UDC", 3 | "phase": 6, 4 | "links": { 5 | "set": "https://sixtyupgrades.com/set/wScp34LkVJrrz3YrJrh9AX", 6 | "talents": "https://sixtyupgrades.com/talents/mage/0Ab3F" 7 | }, 8 | "character": { 9 | "name": "Ronkuby", 10 | "level": 60, 11 | "gameClass": "MAGE", 12 | "race": "HUMAN", 13 | "faction": "ALLIANCE" 14 | }, 15 | "items": [ 16 | { 17 | "name": "Frostfire Circlet", 18 | "id": 22498, 19 | "enchant": { 20 | "name": "Presence of Sight", 21 | "id": 2588, 22 | "itemId": 19787 23 | }, 24 | "slot": "HEAD" 25 | }, 26 | { 27 | "name": "Amulet of Vek'nilash", 28 | "id": 21608, 29 | "slot": "NECK" 30 | }, 31 | { 32 | "name": "Rime Covered Mantle", 33 | "id": 22983, 34 | "enchant": { 35 | "name": "Power of the Scourge", 36 | "id": 2721, 37 | "itemId": 23545 38 | }, 39 | "slot": "SHOULDERS" 40 | }, 41 | { 42 | "name": "Frostfire Robe", 43 | "id": 22496, 44 | "enchant": { 45 | "name": "Enchant Chest - Greater Stats", 46 | "id": 1891, 47 | "spellId": 20025 48 | }, 49 | "slot": "CHEST" 50 | }, 51 | { 52 | "name": "Eyestalk Waist Cord", 53 | "id": 22730, 54 | "slot": "WAIST" 55 | }, 56 | { 57 | "name": "Leggings of Polarity", 58 | "id": 23070, 59 | "enchant": { 60 | "name": "Presence of Sight", 61 | "id": 2588, 62 | "itemId": 19787 63 | }, 64 | "slot": "LEGS" 65 | }, 66 | { 67 | "name": "Frostfire Sandals", 68 | "id": 22500, 69 | "enchant": { 70 | "name": "Enchant Boots - Minor Speed", 71 | "id": 911, 72 | "spellId": 13890 73 | }, 74 | "slot": "FEET" 75 | }, 76 | { 77 | "name": "Rockfury Bracers", 78 | "id": 21186, 79 | "enchant": { 80 | "name": "Enchant Bracer - Greater Intellect", 81 | "id": 1883, 82 | "spellId": 20008 83 | }, 84 | "slot": "WRISTS" 85 | }, 86 | { 87 | "name": "Dark Storm Gauntlets", 88 | "id": 21585, 89 | "enchant": { 90 | "name": "Enchant Gloves - Fire Power", 91 | "id": 2616, 92 | "spellId": 25078 93 | }, 94 | "slot": "HANDS" 95 | }, 96 | { 97 | "name": "Ring of the Fallen God", 98 | "id": 21709, 99 | "slot": "FINGER_1" 100 | }, 101 | { 102 | "name": "Ring of the Eternal Flame", 103 | "id": 23237, 104 | "slot": "FINGER_2" 105 | }, 106 | { 107 | "name": "The Restrained Essence of Sapphiron", 108 | "id": 23046, 109 | "slot": "TRINKET_1" 110 | }, 111 | { 112 | "name": "Mark of the Champion", 113 | "id": 23207, 114 | "slot": "TRINKET_2" 115 | }, 116 | { 117 | "name": "Cloak of the Necropolis", 118 | "id": 23050, 119 | "enchant": { 120 | "name": "Enchant Cloak - Subtlety", 121 | "id": 2621, 122 | "spellId": 25084 123 | }, 124 | "slot": "BACK" 125 | }, 126 | { 127 | "name": "Atiesh, Greatstaff of the Guardian", 128 | "id": 22589, 129 | "enchant": { 130 | "name": "Enchant Weapon - Spell Power", 131 | "id": 2504, 132 | "spellId": 22749 133 | }, 134 | "slot": "MAIN_HAND" 135 | }, 136 | { 137 | "name": "Doomfinger", 138 | "id": 22821, 139 | "slot": "RANGED" 140 | } 141 | ], 142 | "wishlist": [ 143 | { 144 | "name": "Band of Forced Concentration", 145 | "id": 19403, 146 | "type": "ARMOR" 147 | }, 148 | { 149 | "name": "Bracers of Arcane Accuracy", 150 | "id": 19374, 151 | "type": "ARMOR" 152 | }, 153 | { 154 | "name": "Neltharion's Tear", 155 | "id": 19379, 156 | "type": "ARMOR" 157 | } 158 | ], 159 | "consumables": [], 160 | "buffs": [], 161 | "talents": [ 162 | { 163 | "name": "Arcane Subtlety", 164 | "id": 74, 165 | "rank": 2, 166 | "spellId": 12592 167 | }, 168 | { 169 | "name": "Arcane Concentration", 170 | "id": 75, 171 | "rank": 5, 172 | "spellId": 12577 173 | }, 174 | { 175 | "name": "Arcane Focus", 176 | "id": 76, 177 | "rank": 3, 178 | "spellId": 12840 179 | } 180 | ], 181 | "points": [ 182 | { 183 | "name": "Frenzy (6 mages) WB", 184 | "stats": { 185 | "spellDamage": 1, 186 | "fireDamage": 1, 187 | "spellCrit": 9.6, 188 | "intellect": 0.2 189 | } 190 | }, 191 | { 192 | "name": "Frenzy (6 mages) no WB", 193 | "stats": { 194 | "spellDamage": 1, 195 | "fireDamage": 1, 196 | "spellCrit": 12, 197 | "intellect": 0.33 198 | } 199 | } 200 | ], 201 | "stats": { 202 | "agility": 39, 203 | "arcaneDamage": 675, 204 | "armor": 1025, 205 | "attackPower": 58, 206 | "crit": 5.41, 207 | "defense": 300, 208 | "dodge": 5.41, 209 | "fireDamage": 729, 210 | "frostDamage": 675, 211 | "healing": 675, 212 | "health": 3530, 213 | "holyDamage": 675, 214 | "intellect": 318, 215 | "mainHandSpeed": 2.9, 216 | "mana": 5703, 217 | "natureDamage": 675, 218 | "parry": 5, 219 | "rangedSpeed": 1.5, 220 | "shadowDamage": 675, 221 | "spellCrit": 20.54, 222 | "spellDamage": 675, 223 | "spellHit": 10, 224 | "spellPen": 23, 225 | "spirit": 172, 226 | "stamina": 234, 227 | "strength": 34 228 | }, 229 | "exportOptions": { 230 | "buffs": true, 231 | "talents": true 232 | } 233 | } -------------------------------------------------------------------------------- /data/characters/undead-lf-bis-udc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BIS UDC", 3 | "phase": 6, 4 | "links": { 5 | "set": "https://sixtyupgrades.com/set/8Jz5DAYveFydS9Zue1UUcm", 6 | "talents": "https://sixtyupgrades.com/talents/mage/1ACD2C0A1FGJILMo3Poo" 7 | }, 8 | "character": { 9 | "name": "Ronkuby", 10 | "level": 60, 11 | "gameClass": "MAGE", 12 | "race": "HUMAN", 13 | "faction": "ALLIANCE" 14 | }, 15 | "items": [ 16 | { 17 | "name": "Frostfire Circlet", 18 | "id": 22498, 19 | "enchant": { 20 | "name": "Presence of Sight", 21 | "id": 2588, 22 | "itemId": 19787 23 | }, 24 | "slot": "HEAD" 25 | }, 26 | { 27 | "name": "Amulet of Vek'nilash", 28 | "id": 21608, 29 | "slot": "NECK" 30 | }, 31 | { 32 | "name": "Rime Covered Mantle", 33 | "id": 22983, 34 | "enchant": { 35 | "name": "Power of the Scourge", 36 | "id": 2721, 37 | "itemId": 23545 38 | }, 39 | "slot": "SHOULDERS" 40 | }, 41 | { 42 | "name": "Robe of Undead Cleansing", 43 | "id": 23085, 44 | "enchant": { 45 | "name": "Enchant Chest - Greater Stats", 46 | "id": 1891, 47 | "spellId": 20025 48 | }, 49 | "slot": "CHEST" 50 | }, 51 | { 52 | "name": "Eyestalk Waist Cord", 53 | "id": 22730, 54 | "slot": "WAIST" 55 | }, 56 | { 57 | "name": "Leggings of Polarity", 58 | "id": 23070, 59 | "enchant": { 60 | "name": "Presence of Sight", 61 | "id": 2588, 62 | "itemId": 19787 63 | }, 64 | "slot": "LEGS" 65 | }, 66 | { 67 | "name": "Enigma Boots", 68 | "id": 21344, 69 | "enchant": { 70 | "name": "Enchant Boots - Minor Speed", 71 | "id": 911, 72 | "spellId": 13890 73 | }, 74 | "slot": "FEET" 75 | }, 76 | { 77 | "name": "Bracers of Undead Cleansing", 78 | "id": 23091, 79 | "enchant": { 80 | "name": "Enchant Bracer - Greater Intellect", 81 | "id": 1883, 82 | "spellId": 20008 83 | }, 84 | "slot": "WRISTS" 85 | }, 86 | { 87 | "name": "Gloves of Undead Cleansing", 88 | "id": 23084, 89 | "enchant": { 90 | "name": "Enchant Gloves - Fire Power", 91 | "id": 2616, 92 | "spellId": 25078 93 | }, 94 | "slot": "HANDS" 95 | }, 96 | { 97 | "name": "Ring of the Fallen God", 98 | "id": 21709, 99 | "slot": "FINGER_1" 100 | }, 101 | { 102 | "name": "Band of the Inevitable", 103 | "id": 23031, 104 | "slot": "FINGER_2" 105 | }, 106 | { 107 | "name": "The Restrained Essence of Sapphiron", 108 | "id": 23046, 109 | "slot": "TRINKET_1" 110 | }, 111 | { 112 | "name": "Mark of the Champion", 113 | "id": 23207, 114 | "slot": "TRINKET_2" 115 | }, 116 | { 117 | "name": "Cloak of the Necropolis", 118 | "id": 23050, 119 | "enchant": { 120 | "name": "Enchant Cloak - Subtlety", 121 | "id": 2621, 122 | "spellId": 25084 123 | }, 124 | "slot": "BACK" 125 | }, 126 | { 127 | "name": "Atiesh, Greatstaff of the Guardian", 128 | "id": 22589, 129 | "enchant": { 130 | "name": "Enchant Weapon - Spell Power", 131 | "id": 2504, 132 | "spellId": 22749 133 | }, 134 | "slot": "MAIN_HAND" 135 | }, 136 | { 137 | "name": "Wand of Fates", 138 | "id": 22820, 139 | "slot": "RANGED" 140 | } 141 | ], 142 | "wishlist": [ 143 | { 144 | "name": "Band of Forced Concentration", 145 | "id": 19403, 146 | "type": "ARMOR" 147 | }, 148 | { 149 | "name": "Bracers of Arcane Accuracy", 150 | "id": 19374, 151 | "type": "ARMOR" 152 | }, 153 | { 154 | "name": "Neltharion's Tear", 155 | "id": 19379, 156 | "type": "ARMOR" 157 | } 158 | ], 159 | "consumables": [], 160 | "buffs": [], 161 | "talents": [ 162 | { 163 | "name": "Burning Soul", 164 | "id": 23, 165 | "rank": 2, 166 | "spellId": 12351 167 | }, 168 | { 169 | "name": "Improved Scorch", 170 | "id": 25, 171 | "rank": 3, 172 | "spellId": 12873 173 | }, 174 | { 175 | "name": "Improved Fireball", 176 | "id": 26, 177 | "rank": 5, 178 | "spellId": 12341 179 | }, 180 | { 181 | "name": "Flame Throwing", 182 | "id": 28, 183 | "rank": 2, 184 | "spellId": 12353 185 | }, 186 | { 187 | "name": "Improved Flamestrike", 188 | "id": 31, 189 | "rank": 3, 190 | "spellId": 12350 191 | }, 192 | { 193 | "name": "Critical Mass", 194 | "id": 33, 195 | "rank": 3, 196 | "spellId": 11368 197 | }, 198 | { 199 | "name": "Ignite", 200 | "id": 34, 201 | "rank": 5, 202 | "spellId": 12848 203 | }, 204 | { 205 | "name": "Fire Power", 206 | "id": 35, 207 | "rank": 5, 208 | "spellId": 12400 209 | }, 210 | { 211 | "name": "Combustion", 212 | "id": 36, 213 | "rank": 1, 214 | "spellId": 11129 215 | }, 216 | { 217 | "name": "Arcane Subtlety", 218 | "id": 74, 219 | "rank": 2, 220 | "spellId": 12592 221 | }, 222 | { 223 | "name": "Incinerate", 224 | "id": 1141, 225 | "rank": 2, 226 | "spellId": 18460 227 | }, 228 | { 229 | "name": "Master of Elements", 230 | "id": 1639, 231 | "rank": 3, 232 | "spellId": 29076 233 | }, 234 | { 235 | "name": "Elemental Precision", 236 | "id": 1649, 237 | "rank": 3, 238 | "spellId": 29440 239 | } 240 | ], 241 | "points": [ 242 | { 243 | "name": "Frenzy (6 mages) WB", 244 | "stats": { 245 | "spellDamage": 1, 246 | "fireDamage": 1, 247 | "spellCrit": 9.6, 248 | "intellect": 0.2 249 | } 250 | }, 251 | { 252 | "name": "Frenzy (6 mages) no WB", 253 | "stats": { 254 | "spellDamage": 1, 255 | "fireDamage": 1, 256 | "spellCrit": 12, 257 | "intellect": 0.33 258 | } 259 | } 260 | ], 261 | "stats": { 262 | "agility": 39, 263 | "arcaneDamage": 596, 264 | "armor": 801, 265 | "attackPower": 58, 266 | "crit": 5.41, 267 | "defense": 300, 268 | "dodge": 5.41, 269 | "fireDamage": 616, 270 | "frostDamage": 596, 271 | "healing": 596, 272 | "health": 3390, 273 | "holyDamage": 596, 274 | "intellect": 300, 275 | "mainHandSpeed": 2.9, 276 | "mana": 5433, 277 | "mp5": 4, 278 | "natureDamage": 596, 279 | "parry": 5, 280 | "rangedSpeed": 1.5, 281 | "shadowDamage": 596, 282 | "spellCrit": 22.24, 283 | "spellDamage": 596, 284 | "spellHit": 16, 285 | "spellPen": 10, 286 | "spirit": 168, 287 | "stamina": 220, 288 | "strength": 34 289 | }, 290 | "exportOptions": { 291 | "buffs": true, 292 | "talents": true 293 | } 294 | } -------------------------------------------------------------------------------- /data/icons/ability_creature_poison_05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/ability_creature_poison_05.jpg -------------------------------------------------------------------------------- /data/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/icon.png -------------------------------------------------------------------------------- /data/icons/icon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/icon2.png -------------------------------------------------------------------------------- /data/icons/inv_chest_cloth_04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/inv_chest_cloth_04.jpg -------------------------------------------------------------------------------- /data/icons/inv_jewelry_necklace_13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/inv_jewelry_necklace_13.jpg -------------------------------------------------------------------------------- /data/icons/inv_misc_food_63.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/inv_misc_food_63.jpg -------------------------------------------------------------------------------- /data/icons/inv_misc_gift_03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/inv_misc_gift_03.jpg -------------------------------------------------------------------------------- /data/icons/inv_misc_head_dragon_01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/inv_misc_head_dragon_01.jpg -------------------------------------------------------------------------------- /data/icons/inv_misc_orb_02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/inv_misc_orb_02.jpg -------------------------------------------------------------------------------- /data/icons/inv_misc_stonetablet_11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/inv_misc_stonetablet_11.jpg -------------------------------------------------------------------------------- /data/icons/inv_potion_105.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/inv_potion_105.jpg -------------------------------------------------------------------------------- /data/icons/inv_potion_25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/inv_potion_25.jpg -------------------------------------------------------------------------------- /data/icons/inv_potion_26.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/inv_potion_26.jpg -------------------------------------------------------------------------------- /data/icons/inv_potion_41.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/inv_potion_41.jpg -------------------------------------------------------------------------------- /data/icons/inv_potion_60.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/inv_potion_60.jpg -------------------------------------------------------------------------------- /data/icons/inv_trinket_naxxramas06.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/inv_trinket_naxxramas06.jpg -------------------------------------------------------------------------------- /data/icons/inv_valentineschocolate02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/inv_valentineschocolate02.jpg -------------------------------------------------------------------------------- /data/icons/spell_fire_fireball.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/spell_fire_fireball.jpg -------------------------------------------------------------------------------- /data/icons/spell_holy_elunesgrace.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/spell_holy_elunesgrace.jpg -------------------------------------------------------------------------------- /data/icons/spell_holy_lesserheal02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/spell_holy_lesserheal02.jpg -------------------------------------------------------------------------------- /data/icons/spell_holy_magicalsentry.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/spell_holy_magicalsentry.jpg -------------------------------------------------------------------------------- /data/icons/spell_holy_mindvision.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/spell_holy_mindvision.jpg -------------------------------------------------------------------------------- /data/icons/spell_holy_powerinfusion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/spell_holy_powerinfusion.jpg -------------------------------------------------------------------------------- /data/icons/spell_ice_lament.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/spell_ice_lament.jpg -------------------------------------------------------------------------------- /data/icons/spell_magic_greaterblessingofkings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/spell_magic_greaterblessingofkings.jpg -------------------------------------------------------------------------------- /data/icons/spell_nature_regeneration.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/spell_nature_regeneration.jpg -------------------------------------------------------------------------------- /data/icons/spell_nature_timestop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/spell_nature_timestop.jpg -------------------------------------------------------------------------------- /data/icons/spell_nature_wispheal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/icons/spell_nature_wispheal.jpg -------------------------------------------------------------------------------- /data/items.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/items.dat -------------------------------------------------------------------------------- /data/pictures/anaconda_prompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/pictures/anaconda_prompt.png -------------------------------------------------------------------------------- /data/pictures/scenario_comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/pictures/scenario_comparison.png -------------------------------------------------------------------------------- /data/pictures/scenario_editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/pictures/scenario_editor.png -------------------------------------------------------------------------------- /data/pictures/scenario_selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/pictures/scenario_selection.png -------------------------------------------------------------------------------- /data/pictures/stats_distribution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/pictures/stats_distribution.png -------------------------------------------------------------------------------- /data/samples/compare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/samples/compare.png -------------------------------------------------------------------------------- /data/samples/distribution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronkuby-mage/fire-mage-simulation/d377ea2123a89ac77bd6e9100036aa52302373e7/data/samples/distribution.png -------------------------------------------------------------------------------- /data/scenarios/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "stats": { 3 | "spell_power": [ 4 | 810, 5 | 810, 6 | 810, 7 | 785 8 | ], 9 | "hit_chance": [ 10 | 0.1, 11 | 0.1, 12 | 0.1, 13 | 0.1 14 | ], 15 | "crit_chance": [ 16 | 0.11, 17 | 0.11, 18 | 0.11, 19 | 0.15 20 | ], 21 | "intellect": [ 22 | 300, 23 | 300, 24 | 300, 25 | 316 26 | ] 27 | }, 28 | "buffs": { 29 | "raid": [ 30 | "blessing_of_kings", 31 | "improved_mark", 32 | "arcane_intellect" 33 | ], 34 | "consumes": [ 35 | "very_berry_cream", 36 | "flask_of_supreme_power", 37 | "greater_arcane_elixir", 38 | "brilliant_wizard_oil", 39 | "elixir_of_greater_firepower" 40 | ], 41 | "world": [ 42 | "dire_maul_tribute", 43 | "songflower_serenade", 44 | "rallying_cry_of_the_dragonslayer", 45 | "sayges_dark_fortune_of_damage", 46 | "spirit_of_zandalar" 47 | ], 48 | "racial": [ 49 | "human", 50 | "human", 51 | "human", 52 | "human" 53 | ], 54 | "boss": "", 55 | "auras": { 56 | "mage_atiesh": [ 57 | 2, 58 | 2, 59 | 2, 60 | 3 61 | ], 62 | "lock_atiesh": [ 63 | 1, 64 | 1, 65 | 1, 66 | 1 67 | ], 68 | "boomkin": [ 69 | 0, 70 | 0, 71 | 0, 72 | 0 73 | ] 74 | } 75 | }, 76 | "configuration": { 77 | "num_mages": 4, 78 | "target": [ 79 | 0, 80 | 1, 81 | 2, 82 | 3 83 | ], 84 | "sapp": [ 85 | 0, 86 | 1, 87 | 2, 88 | 3 89 | ], 90 | "toep": [], 91 | "zhc": [], 92 | "mqg": [], 93 | "pi": [ 94 | 0, 95 | 1, 96 | 2, 97 | 3 98 | ], 99 | "udc": [ 100 | 0, 101 | 1, 102 | 2 103 | ], 104 | "name": [ 105 | "undead-lf-bis-udc", 106 | "undead-lf-bis-udc", 107 | "undead-lf-bis-udc", 108 | "undead-lf-bis-no-udc-no-atiesh" 109 | ] 110 | }, 111 | "rotation": { 112 | "initial": { 113 | "common": [], 114 | "other": [ 115 | "scorch", 116 | "scorch", 117 | "sapp", 118 | "combustion", 119 | "pyroblast", 120 | "pi", 121 | "fireball" 122 | ] 123 | }, 124 | "continuing": { 125 | "default": "fireball", 126 | "special1": { 127 | "slot": [ 128 | 3 129 | ], 130 | "value": "cobimf", 131 | "cast_point_remain": 0.5 132 | } 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /data/scenarios/mqg-mark-fireball-mts.json: -------------------------------------------------------------------------------- 1 | { 2 | "stats": { 3 | "spell_power": [ 4 | 767, 5 | 767, 6 | 767 7 | ], 8 | "hit_chance": [ 9 | 0.1, 10 | 0.1, 11 | 0.1 12 | ], 13 | "crit_chance": [ 14 | 0.15, 15 | 0.15, 16 | 0.15 17 | ], 18 | "intellect": [ 19 | 318, 20 | 318, 21 | 318 22 | ] 23 | }, 24 | "buffs": { 25 | "raid": [ 26 | "blessing_of_kings", 27 | "improved_mark", 28 | "arcane_intellect" 29 | ], 30 | "consumes": [ 31 | "very_berry_cream", 32 | "flask_of_supreme_power", 33 | "greater_arcane_elixir", 34 | "brilliant_wizard_oil", 35 | "elixir_of_greater_firepower" 36 | ], 37 | "world": [ 38 | "dire_maul_tribute", 39 | "songflower_serenade", 40 | "rallying_cry_of_the_dragonslayer", 41 | "sayges_dark_fortune_of_damage", 42 | "spirit_of_zandalar" 43 | ], 44 | "racial": [ 45 | "human", 46 | "human", 47 | "human" 48 | ], 49 | "boss": "", 50 | "auras": { 51 | "mage_atiesh": [ 52 | 2, 53 | 2, 54 | 2 55 | ], 56 | "lock_atiesh": [ 57 | 1, 58 | 1, 59 | 1 60 | ], 61 | "boomkin": [ 62 | 0, 63 | 0, 64 | 0 65 | ] 66 | } 67 | }, 68 | "configuration": { 69 | "num_mages": 3, 70 | "target": [ 71 | 0, 72 | 1, 73 | 2 74 | ], 75 | "sapp": [], 76 | "toep": [], 77 | "zhc": [], 78 | "mqg": [ 79 | 0, 80 | 1, 81 | 2 82 | ], 83 | "pi": [ 84 | 0, 85 | 1, 86 | 2 87 | ], 88 | "udc": [], 89 | "name": [ 90 | "", 91 | "", 92 | "", 93 | "" 94 | ] 95 | }, 96 | "rotation": { 97 | "initial": { 98 | "common": [], 99 | "other": [ 100 | "scorch", 101 | "scorch", 102 | "sapp", 103 | "combustion", 104 | "gcd", 105 | "fireball", 106 | "pi", 107 | "mqg" 108 | ] 109 | }, 110 | "continuing": { 111 | "default": "fireball", 112 | "special1": { 113 | "slot": [ 114 | 2 115 | ], 116 | "value": "maintain_scorch" 117 | } 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /data/scenarios/presapp-mqg-fireball-cbi.json: -------------------------------------------------------------------------------- 1 | { 2 | "stats": { 3 | "spell_power": [ 4 | 727, 5 | 727, 6 | 727 7 | ], 8 | "hit_chance": [ 9 | 0.1, 10 | 0.1, 11 | 0.1 12 | ], 13 | "crit_chance": [ 14 | 0.19, 15 | 0.19, 16 | 0.19 17 | ], 18 | "intellect": [ 19 | 318, 20 | 318, 21 | 318 22 | ] 23 | }, 24 | "buffs": { 25 | "raid": [ 26 | "blessing_of_kings", 27 | "improved_mark", 28 | "arcane_intellect" 29 | ], 30 | "consumes": [ 31 | "very_berry_cream", 32 | "flask_of_supreme_power", 33 | "greater_arcane_elixir", 34 | "brilliant_wizard_oil", 35 | "elixir_of_greater_firepower" 36 | ], 37 | "world": [ 38 | "dire_maul_tribute", 39 | "songflower_serenade", 40 | "rallying_cry_of_the_dragonslayer", 41 | "sayges_dark_fortune_of_damage", 42 | "spirit_of_zandalar" 43 | ], 44 | "racial": [ 45 | "human", 46 | "human", 47 | "human" 48 | ], 49 | "boss": "", 50 | "auras": { 51 | "mage_atiesh": [ 52 | 2, 53 | 2, 54 | 2 55 | ], 56 | "lock_atiesh": [ 57 | 1, 58 | 1, 59 | 1 60 | ], 61 | "boomkin": [ 62 | 0, 63 | 0, 64 | 0 65 | ] 66 | } 67 | }, 68 | "configuration": { 69 | "num_mages": 3, 70 | "target": [ 71 | 0, 72 | 1, 73 | 2 74 | ], 75 | "sapp": [ 76 | 0, 77 | 1, 78 | 2 79 | ], 80 | "toep": [], 81 | "zhc": [], 82 | "mqg": [ 83 | 0, 84 | 1, 85 | 2 86 | ], 87 | "pi": [ 88 | 0, 89 | 1, 90 | 2 91 | ], 92 | "udc": [], 93 | "name": [ 94 | "", 95 | "", 96 | "", 97 | "" 98 | ] 99 | }, 100 | "rotation": { 101 | "initial": { 102 | "common": [], 103 | "other": [ 104 | "sapp", 105 | "scorch", 106 | "scorch", 107 | "combustion", 108 | "gcd", 109 | "fireball", 110 | "pi", 111 | "mqg" 112 | ] 113 | }, 114 | "continuing": { 115 | "default": "fireball", 116 | "special1": { 117 | "slot": [ 118 | 2 119 | ], 120 | "value": "cobimf", 121 | "cast_point_remain": 0.5 122 | } 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /data/scenarios/presapp-mqg-fireball-mts.json: -------------------------------------------------------------------------------- 1 | { 2 | "stats": { 3 | "spell_power": [ 4 | 727, 5 | 727, 6 | 727 7 | ], 8 | "hit_chance": [ 9 | 0.1, 10 | 0.1, 11 | 0.1 12 | ], 13 | "crit_chance": [ 14 | 0.19, 15 | 0.19, 16 | 0.19 17 | ], 18 | "intellect": [ 19 | 318, 20 | 318, 21 | 318 22 | ] 23 | }, 24 | "buffs": { 25 | "raid": [ 26 | "blessing_of_kings", 27 | "improved_mark", 28 | "arcane_intellect" 29 | ], 30 | "consumes": [ 31 | "very_berry_cream", 32 | "flask_of_supreme_power", 33 | "greater_arcane_elixir", 34 | "brilliant_wizard_oil", 35 | "elixir_of_greater_firepower" 36 | ], 37 | "world": [ 38 | "dire_maul_tribute", 39 | "songflower_serenade", 40 | "rallying_cry_of_the_dragonslayer", 41 | "sayges_dark_fortune_of_damage", 42 | "spirit_of_zandalar" 43 | ], 44 | "racial": [ 45 | "human", 46 | "human", 47 | "human" 48 | ], 49 | "boss": "", 50 | "auras": { 51 | "mage_atiesh": [ 52 | 2, 53 | 2, 54 | 2 55 | ], 56 | "lock_atiesh": [ 57 | 1, 58 | 1, 59 | 1 60 | ], 61 | "boomkin": [ 62 | 0, 63 | 0, 64 | 0 65 | ] 66 | } 67 | }, 68 | "configuration": { 69 | "num_mages": 3, 70 | "target": [ 71 | 0, 72 | 1, 73 | 2 74 | ], 75 | "sapp": [ 76 | 0, 77 | 1, 78 | 2 79 | ], 80 | "toep": [], 81 | "zhc": [], 82 | "mqg": [ 83 | 0, 84 | 1, 85 | 2 86 | ], 87 | "pi": [ 88 | 0, 89 | 1, 90 | 2 91 | ], 92 | "udc": [], 93 | "name": [ 94 | "", 95 | "", 96 | "", 97 | "" 98 | ] 99 | }, 100 | "rotation": { 101 | "initial": { 102 | "common": [], 103 | "other": [ 104 | "sapp", 105 | "scorch", 106 | "scorch", 107 | "combustion", 108 | "gcd", 109 | "fireball", 110 | "pi", 111 | "mqg" 112 | ] 113 | }, 114 | "continuing": { 115 | "default": "fireball", 116 | "special1": { 117 | "slot": [ 118 | 2 119 | ], 120 | "value": "maintain_scorch" 121 | } 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /data/scenarios/sapp-mark-fireball-wep-cbi.json: -------------------------------------------------------------------------------- 1 | { 2 | "stats": { 3 | "spell_power": [ 4 | 814, 5 | 814, 6 | 814 7 | ], 8 | "hit_chance": [ 9 | 0.1, 10 | 0.1, 11 | 0.1 12 | ], 13 | "crit_chance": [ 14 | 0.15, 15 | 0.15, 16 | 0.15 17 | ], 18 | "intellect": [ 19 | 318, 20 | 318, 21 | 318 22 | ] 23 | }, 24 | "buffs": { 25 | "raid": [ 26 | "blessing_of_kings", 27 | "improved_mark", 28 | "arcane_intellect" 29 | ], 30 | "consumes": [ 31 | "very_berry_cream", 32 | "flask_of_supreme_power", 33 | "greater_arcane_elixir", 34 | "brilliant_wizard_oil", 35 | "elixir_of_greater_firepower" 36 | ], 37 | "world": [ 38 | "dire_maul_tribute", 39 | "songflower_serenade", 40 | "rallying_cry_of_the_dragonslayer", 41 | "sayges_dark_fortune_of_damage", 42 | "spirit_of_zandalar" 43 | ], 44 | "racial": [ 45 | "human", 46 | "human", 47 | "human" 48 | ], 49 | "boss": "", 50 | "auras": { 51 | "mage_atiesh": [ 52 | 2, 53 | 2, 54 | 2 55 | ], 56 | "lock_atiesh": [ 57 | 1, 58 | 1, 59 | 1 60 | ], 61 | "boomkin": [ 62 | 0, 63 | 0, 64 | 0 65 | ] 66 | } 67 | }, 68 | "configuration": { 69 | "num_mages": 3, 70 | "target": [ 71 | 0, 72 | 1, 73 | 2 74 | ], 75 | "sapp": [ 76 | 0, 77 | 1, 78 | 2 79 | ], 80 | "toep": [], 81 | "zhc": [], 82 | "mqg": [], 83 | "pi": [ 84 | 0, 85 | 1, 86 | 2 87 | ], 88 | "udc": [], 89 | "name": [ 90 | "", 91 | "", 92 | "", 93 | "" 94 | ] 95 | }, 96 | "rotation": { 97 | "initial": { 98 | "common": [], 99 | "other": [ 100 | "scorch", 101 | "scorch", 102 | "sapp", 103 | "combustion", 104 | "gcd", 105 | "fireball", 106 | "pi", 107 | "mqg" 108 | ] 109 | }, 110 | "continuing": { 111 | "default": "fireball", 112 | "special1": { 113 | "slot": [ 114 | 2 115 | ], 116 | "value": "cobimf", 117 | "cast_point_remain": 0.5 118 | }, 119 | "special2": { 120 | "value": "scorch_wep", 121 | "slot": [ 122 | 0 123 | ] 124 | } 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /data/scenarios/two_mages.json: -------------------------------------------------------------------------------- 1 | { 2 | "stats": { 3 | "spell_power": [ 4 | 786, 5 | 786 6 | ], 7 | "hit_chance": [ 8 | 0.1, 9 | 0.1 10 | ], 11 | "crit_chance": [ 12 | 0.17, 13 | 0.17 14 | ], 15 | "intellect": [ 16 | 314, 17 | 314 18 | ] 19 | }, 20 | "buffs": { 21 | "raid": [ 22 | "blessing_of_kings", 23 | "improved_mark", 24 | "arcane_intellect" 25 | ], 26 | "consumes": [ 27 | "very_berry_cream", 28 | "flask_of_supreme_power", 29 | "greater_arcane_elixir", 30 | "brilliant_wizard_oil", 31 | "elixir_of_greater_firepower", 32 | "stormwind_gift_of_friendship", 33 | "runn_tum_tuber_surprise" 34 | ], 35 | "world": [ 36 | "dire_maul_tribute", 37 | "songflower_serenade", 38 | "rallying_cry_of_the_dragonslayer", 39 | "sayges_dark_fortune_of_damage", 40 | "spirit_of_zandalar" 41 | ], 42 | "racial": [ 43 | "gnome", 44 | "gnome" 45 | ], 46 | "boss": "", 47 | "auras": { 48 | "mage_atiesh": [ 49 | 2, 50 | 2 51 | ], 52 | "lock_atiesh": [ 53 | 1, 54 | 1 55 | ], 56 | "boomkin": [ 57 | 0, 58 | 0 59 | ] 60 | } 61 | }, 62 | "configuration": { 63 | "num_mages": 2, 64 | "target": [ 65 | 0, 66 | 1 67 | ], 68 | "sapp": [ 69 | 0, 70 | 1 71 | ], 72 | "toep": [], 73 | "zhc": [], 74 | "mqg": [], 75 | "pi": [ 76 | 0, 77 | 1 78 | ], 79 | "udc": [], 80 | "name": [ 81 | "two_mages", 82 | "two_mages" 83 | ] 84 | }, 85 | "rotation": { 86 | "initial": { 87 | "common": [], 88 | "other": [ 89 | "scorch", 90 | "scorch", 91 | "scorch", 92 | "sapp", 93 | "combustion", 94 | "pyroblast", 95 | "pi" 96 | ] 97 | }, 98 | "continuing": { 99 | "default": "fireball", 100 | "special1": { 101 | "slot": [ 102 | 0 103 | ], 104 | "value": "cobimf", 105 | "cast_point_remain": 0.5 106 | }, 107 | "special2": { 108 | "value": "scorch_wep", 109 | "slot": [ 110 | 1 111 | ] 112 | } 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /data/scenarios/weight_mage.json: -------------------------------------------------------------------------------- 1 | { 2 | "stats": { 3 | "spell_power": [ 4 | 810, 5 | 500, 6 | 500, 7 | 500, 8 | 500 9 | ], 10 | "hit_chance": [ 11 | 0.1, 12 | 0.08, 13 | 0.07, 14 | 0.08, 15 | 0.09 16 | ], 17 | "crit_chance": [ 18 | 0.11, 19 | 0.06, 20 | 0.07, 21 | 0.06, 22 | 0.05 23 | ], 24 | "intellect": [ 25 | 300, 26 | 318, 27 | 318, 28 | 302, 29 | 302 30 | ] 31 | }, 32 | "buffs": { 33 | "raid": [ 34 | "blessing_of_kings", 35 | "improved_mark", 36 | "arcane_intellect" 37 | ], 38 | "consumes": [ 39 | "flask_of_supreme_power", 40 | "greater_arcane_elixir", 41 | "brilliant_wizard_oil", 42 | "elixir_of_greater_firepower" 43 | ], 44 | "world": [ 45 | "dire_maul_tribute", 46 | "rallying_cry_of_the_dragonslayer", 47 | "sayges_dark_fortune_of_damage", 48 | "spirit_of_zandalar" 49 | ], 50 | "racial": [ 51 | "human", 52 | "gnome", 53 | "gnome", 54 | "human", 55 | "human" 56 | ], 57 | "boss": "", 58 | "auras": { 59 | "mage_atiesh": [ 60 | 0, 61 | 1, 62 | 1, 63 | 1, 64 | 1 65 | ], 66 | "lock_atiesh": [ 67 | 0, 68 | 0, 69 | 0, 70 | 0, 71 | 0 72 | ], 73 | "boomkin": [ 74 | 0, 75 | 0, 76 | 0, 77 | 0, 78 | 0 79 | ] 80 | } 81 | }, 82 | "configuration": { 83 | "num_mages": 5, 84 | "target": [ 85 | 0, 86 | 1, 87 | 2, 88 | 3, 89 | 4 90 | ], 91 | "sapp": [ 92 | 0 93 | ], 94 | "toep": [ 95 | 1, 96 | 2, 97 | 3 98 | ], 99 | "zhc": [ 100 | 2, 101 | 4 102 | ], 103 | "mqg": [], 104 | "pi": [ 105 | 0 106 | ], 107 | "udc": [ 108 | 0 109 | ], 110 | "name": [ 111 | "", 112 | "", 113 | "", 114 | "", 115 | "" 116 | ] 117 | }, 118 | "rotation": { 119 | "initial": { 120 | "common": [], 121 | "other": [ 122 | "scorch", 123 | "scorch", 124 | "frostbolt", 125 | "combustion", 126 | "frostbolt", 127 | "frostbolt", 128 | "zhc", 129 | "toep", 130 | "fireball" 131 | ], 132 | "have_pi": [ 133 | "scorch", 134 | "sapp", 135 | "combustion", 136 | "pyroblast", 137 | "pi", 138 | "fireball" 139 | ] 140 | }, 141 | "continuing": { 142 | "default": "fireball", 143 | "special1": { 144 | "slot": [ 145 | 2 146 | ], 147 | "value": "maintain_scorch" 148 | } 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /src/gui/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Wed Feb 23 18:15:34 2022 4 | 5 | @author: jongo 6 | """ 7 | 8 | -------------------------------------------------------------------------------- /src/gui/config_select.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PyQt5.QtWidgets import QApplication, QWidget 3 | from PyQt5.QtWidgets import ( 4 | QPushButton, 5 | QVBoxLayout, 6 | QHBoxLayout, 7 | QTabWidget, 8 | QCheckBox, 9 | QRadioButton, 10 | QLineEdit, 11 | QMessageBox, 12 | QWidget, 13 | QLabel 14 | ) 15 | 16 | class ConfigListWidget(QWidget): 17 | 18 | _MAX_CONFIGS = 5 19 | 20 | def __init__(self, config_list, update_trigger, expand=True): 21 | super().__init__() 22 | self._update_trigger = update_trigger 23 | self._settings_refresh = None 24 | self.layout = QVBoxLayout() 25 | self.setLayout(self.layout) 26 | self._items = [ConfigWidget(0, self.item_signal, self.update, self.select_trigger, config=config_list.current())] 27 | self.layout.addWidget(self._items[0]) 28 | self._expand = expand 29 | self._set_size() 30 | self._config_list = config_list 31 | self._index = 0 32 | 33 | def _set_size(self): 34 | for item in self._items[:-1]: 35 | item.set_last(False) 36 | self._items[-1].set_last(self._expand, max_configs=self._MAX_CONFIGS) 37 | 38 | # to add or remove last config 39 | def item_signal(self, stype: int): 40 | if not stype: 41 | self.layout.removeWidget(self._items[-1]) 42 | if self._index == len(self._items) - 1: 43 | self.set_index(self._index - 1) 44 | self._items = self._items[:-1] 45 | self._config_list.pop() 46 | else: 47 | filename = self._items[-1].filename() + "_copy" 48 | self._items.append(ConfigWidget(len(self._items), 49 | self.item_signal, 50 | self.update, 51 | self.select_trigger, 52 | config=self._config_list.copy(filename))) 53 | self.layout.addWidget(self._items[-1]) 54 | self.set_index(len(self._items) - 1) 55 | self.changed_trigger() 56 | self._items[-1].settings_refresh(self._settings_refresh) 57 | self._set_size() 58 | if self._settings_refresh is not None: 59 | self._settings_refresh() 60 | 61 | def select_trigger(self, index: int): 62 | if index == self._index: #do nothing 63 | self._items[self._index].select(True) 64 | else: 65 | self.set_index(index) 66 | 67 | def set_index(self, index): 68 | self._items[self._index].select(False) 69 | self._index = index 70 | self._config_list.set_index(index) 71 | self._update_trigger() 72 | self._items[self._index].select(True) 73 | if self._settings_refresh is not None: 74 | self._settings_refresh() 75 | 76 | def changed_trigger(self): 77 | self._items[self._index].modify() 78 | 79 | def update(self, index): 80 | if index == self._index: 81 | self._update_trigger() 82 | 83 | def filenames(self, current=False): 84 | if current: 85 | return self._items[self._index].filename() 86 | else: 87 | return [item.filename() for item in self._items] 88 | 89 | def settings_refresh(self, refresh): 90 | for item in self._items: 91 | item.settings_refresh(refresh) 92 | self._settings_refresh = refresh 93 | 94 | class ConfigWidget(QWidget): 95 | 96 | def __init__(self, index, item_signal, update_trigger, select_trigger, config=None): 97 | super().__init__() 98 | self._index = index 99 | self._config = config 100 | self._update_trigger = update_trigger 101 | self._settings_refresh = None 102 | sidelayout = QHBoxLayout() 103 | sidelayout.setContentsMargins(3, 5, 3, 5) 104 | 105 | self._select = QRadioButton(f"Scenario {index + 1:d}") 106 | self._select.setChecked(True) 107 | self._select.pressed.connect(lambda x=self._index: select_trigger(x)) 108 | self._select.released.connect(lambda x=self._index: select_trigger(x)) 109 | 110 | self._filename = QLineEdit() 111 | self._filename.textChanged.connect(self.modify) 112 | self._filename.returnPressed.connect(self._load) 113 | self._filename.setMaximumWidth(250) 114 | self._filename.setMinimumWidth(200) 115 | self._filename.setMaxLength(26) 116 | self._filename.setContentsMargins(0, 0, 0, 0) 117 | self._load_button = QPushButton("Load", self) 118 | self._save_button = QPushButton("Save", self) 119 | self._load_button.setMaximumWidth(130) 120 | self._save_button.setMaximumWidth(130) 121 | self._load_button.clicked.connect(self._load) 122 | self._save_button.clicked.connect(self._save) 123 | 124 | self._add_button = QPushButton("+", self) 125 | self._del_button = QPushButton("X", self) 126 | self._add_button.setMaximumWidth(20) 127 | self._del_button.setMaximumWidth(20) 128 | self._add_button.clicked.connect(lambda: item_signal(1)) 129 | self._del_button.clicked.connect(lambda: item_signal(0)) 130 | 131 | sidelayout.addStretch() 132 | sidelayout.addWidget(self._select) 133 | sidelayout.addWidget(self._filename) 134 | sidelayout.addWidget(self._load_button) 135 | sidelayout.addWidget(self._save_button) 136 | sidelayout.addWidget(self._add_button) 137 | sidelayout.addWidget(self._del_button) 138 | self.setLayout(sidelayout) 139 | 140 | # default is initially loaded 141 | self._filename.setText(self._user_filename()) 142 | self._filename.setStyleSheet("border:1px solid rgb(0, 255, 0); ") 143 | 144 | def select(self, enabled: bool): 145 | self._select.setChecked(enabled) 146 | 147 | def _load(self): 148 | try: 149 | self._config.load(self._filename.text()) 150 | self._update_trigger(self._index) 151 | self._filename.setStyleSheet("border:1px solid rgb(0, 255, 0); ") 152 | self._filename.setToolTip("") 153 | except FileNotFoundError: 154 | msg = QMessageBox() 155 | msg.setIcon(QMessageBox.Critical) 156 | msg.setText("File not found.") 157 | msg.setWindowTitle("Load Error") 158 | msg.exec() 159 | if self._settings_refresh is not None: 160 | self._settings_refresh() 161 | 162 | def _save(self): 163 | self._config.save(self._filename.text()) 164 | self._filename.setStyleSheet("border:1px solid rgb(0, 255, 0); ") 165 | self._filename.setToolTip("") 166 | 167 | def modify(self): 168 | self._filename.setStyleSheet("border:1px solid rgb(255, 0, 0); ") 169 | self._filename.setToolTip("Scenario is not saved") 170 | if self._settings_refresh is not None: 171 | self._settings_refresh() 172 | 173 | def _user_filename(self): 174 | if self._config is not None: 175 | filename = self._config.filename() 176 | base, ext = os.path.splitext(filename) 177 | return base 178 | 179 | return "" 180 | 181 | def set_last(self, enabled, max_configs=100): 182 | if not enabled: 183 | self._add_button.setEnabled(False) 184 | self._del_button.setEnabled(False) 185 | else: 186 | self._add_button.setEnabled(True if self._index + 1 < max_configs else False) 187 | self._del_button.setEnabled(True if self._index else False) 188 | 189 | def filename(self): 190 | return self._filename.text() 191 | 192 | def settings_refresh(self, refresh): 193 | self._settings_refresh = refresh 194 | -------------------------------------------------------------------------------- /src/gui/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from PyQt5.QtGui import QIcon 4 | from PyQt5.QtCore import Qt 5 | from PyQt5.QtWidgets import ( 6 | QApplication, 7 | QVBoxLayout, 8 | QTabWidget, 9 | QWidget 10 | ) 11 | import ctypes 12 | import qdarkstyle 13 | from .config_select import ConfigListWidget 14 | from .scenario_editor.scenario import Scenario 15 | from ..sim.config import ConfigList 16 | from .simulation_interface.simulation import Simulation 17 | 18 | class Window(QWidget): 19 | 20 | if os.name == "nt": 21 | _DEFAULT_WIDTH = 1200 22 | _DEFAULT_HEIGHT = 800 23 | else: 24 | _DEFAULT_WIDTH = 1536 25 | _DEFAULT_HEIGHT = 1024 26 | _CONFIG_DIRECTORY = "./data/scenarios/" 27 | _CONFIG_DEFAULT = "default.json" 28 | 29 | def __init__(self): 30 | super().__init__() 31 | 32 | self.config_list = ConfigList(self._CONFIG_DIRECTORY, self._CONFIG_DEFAULT) 33 | 34 | self.setWindowTitle("Fire Mage Simulation") 35 | 36 | self.setWindowIcon(QIcon("./data/icons/icon2.png")) 37 | if os.name == "nt": 38 | myappid = 'mycompany.myproduct.subproduct.version' # arbitrary string 39 | ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) 40 | 41 | self.resize(self._DEFAULT_WIDTH, self._DEFAULT_HEIGHT) 42 | # Create a top-level layout 43 | self.toplayout = QVBoxLayout() 44 | self.toplayout.setContentsMargins(5, 5, 5, 5) 45 | # Create the tab widget with two tabs 46 | self.tabs = QTabWidget() 47 | self._scenario = Scenario(self.config_list) 48 | self._config_widget = ConfigListWidget(self.config_list, self._scenario.update) 49 | self._simulation = Simulation(self.config_list, self._config_widget.filenames) 50 | self._config_widget.settings_refresh(self._simulation.refresh) 51 | self._scenario.settings_refresh(self._simulation.refresh) 52 | self.tabs.addTab(self._simulation, "Simulation") 53 | self.tabs.addTab(self._scenario, "Scenario") 54 | 55 | self.toplayout.addWidget(self.tabs) 56 | self.toplayout.addWidget(self._config_widget) 57 | self.setLayout(self.toplayout) 58 | self._last_height = None 59 | 60 | # enable custom window hint 61 | self.setWindowFlags(self.windowFlags() | Qt.MSWindowsFixedSizeDialogHint) 62 | self.setWindowFlags(self.windowFlags() | Qt.CustomizeWindowHint) 63 | 64 | # disable (but not hide) close button 65 | self.setWindowFlags(self.windowFlags() & ~Qt.WindowMaximizeButtonHint) 66 | #self.setWindowFlag(Qt.WindowMaximizeButtonHint, False) 67 | 68 | def show(self): 69 | super().show() 70 | self._zero_height = self._config_widget.geometry().height() 71 | self._last_height = self._zero_height 72 | self._scenario.update() 73 | self._scenario.set_changed_trigger(self._config_widget.changed_trigger) 74 | 75 | def paintEvent(self, event): 76 | super().paintEvent(event) 77 | if self._last_height is not None: 78 | current_height = self._config_widget.geometry().height() 79 | if current_height != self._last_height: 80 | new_height = self._DEFAULT_HEIGHT - self._zero_height + current_height 81 | self.resize(self._DEFAULT_WIDTH, new_height) 82 | self._last_height = current_height 83 | 84 | if __name__ == "__main__": 85 | app = QApplication([]) 86 | window = Window() 87 | app.setStyleSheet(qdarkstyle.load_stylesheet()) 88 | window.show() 89 | sys.exit(app.exec_()) 90 | -------------------------------------------------------------------------------- /src/gui/scenario_editor/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Wed Feb 23 18:15:34 2022 4 | 5 | @author: jongo 6 | """ 7 | 8 | -------------------------------------------------------------------------------- /src/gui/scenario_editor/buffs.py: -------------------------------------------------------------------------------- 1 | import json 2 | from PyQt5.QtWidgets import ( 3 | QPushButton, 4 | QGridLayout, 5 | QVBoxLayout, 6 | QHBoxLayout, 7 | QTableWidget, 8 | QComboBox, 9 | QCheckBox, 10 | QWidget, 11 | QStackedWidget, 12 | QLabel, 13 | QHeaderView, 14 | QGroupBox 15 | ) 16 | from PyQt5.QtGui import QIcon 17 | from PyQt5.QtCore import Qt, QSize 18 | from ..utils.icon_edit import get_pixmap 19 | from ..utils.guard_lineedit import GuardLineEdit 20 | 21 | class Buffs(QGroupBox): 22 | _VALUES = ["name", 23 | "row", 24 | "col", 25 | "tooltip", 26 | "icon_fn", 27 | "enabled"] 28 | 29 | _BUFFS = { 30 | "world": [ 31 | ("rallying_cry_of_the_dragonslayer", 0, 0, "Ony", "inv_misc_head_dragon_01.jpg", True), 32 | ("spirit_of_zandalar", 0, 1, "Heart", "ability_creature_poison_05.jpg", True), 33 | ("dire_maul_tribute", 1, 0, "DMT", "spell_holy_lesserheal02.jpg", True), 34 | ("songflower_serenade", 1, 1, "Songflower", "spell_holy_mindvision.jpg", True), 35 | ("sayges_dark_fortune_of_damage", 2, 0, "DMF", "inv_misc_orb_02.jpg", True), 36 | ("traces_of_silithus", 2, 1, "Silithus", "spell_nature_timestop.jpg", False)], 37 | "consumes": [ 38 | ("greater_arcane_elixir", 0, 0, "GAE", "inv_potion_25.jpg", True), 39 | ("elixir_of_greater_firepower", 0, 1, "Greater Firepower", "inv_potion_60.jpg", True), 40 | ("flask_of_supreme_power", 0, 2, "FoSP", "inv_potion_41.jpg", True), 41 | ("brilliant_wizard_oil", 1, 0, "Brilliant Oil", "inv_potion_105.jpg", True), 42 | ("blessed_wizard_oil", 1, 1, "Blessed Oil", "inv_potion_26.jpg", True), 43 | ("very_berry_cream", 1, 2, "VDay Candy", "inv_valentineschocolate02.jpg", True), 44 | ("infallible_mind", 2, 0, "BL Int", "spell_ice_lament.jpg", True), 45 | ("stormwind_gift_of_friendship", 2, 1, "VDay Int", "inv_misc_gift_03.jpg", True), 46 | ("runn_tum_tuber_surprise", 2, 2, "Tuber", "inv_misc_food_63.jpg", True)], 47 | "raid": [ 48 | ("arcane_intellect", 0, 0, "Int", "spell_holy_magicalsentry.jpg", True), 49 | ("blessing_of_kings", 1, 0, "BoK", "spell_magic_greaterblessingofkings.jpg", True), 50 | ("improved_mark", 2, 0, "Mark", "spell_nature_regeneration.jpg", True)] 51 | } 52 | _BOSSES = ["", "loatheb", "thaddius"] 53 | _DRAGONLING_DEFAULT = 30.0 54 | _DRAGONLING_ICON_FN = "spell_fire_fireball.jpg" 55 | _NIGHTFALL_DEFAULT = 2.55 56 | _NIGHTFALL_ICON_FN = "spell_holy_elunesgrace.jpg" 57 | _MAX_NIGHTFALL = 5 58 | 59 | def __init__(self, config_list): 60 | super().__init__() 61 | self.setTitle("Buffs") 62 | self._changed_trigger = None 63 | self._config = config_list 64 | self._buffs = {} 65 | 66 | layout = QHBoxLayout() 67 | 68 | misc = QGroupBox("Misc") 69 | misc_layout = QGridLayout() 70 | misc_layout.setSpacing(0) 71 | misc_layout.addWidget(QLabel(f"boss"), 0, 1) 72 | self._buffs["boss"] = QComboBox() 73 | self._buffs["boss"].addItems(self._BOSSES) 74 | self._buffs["boss"].currentIndexChanged.connect(self.modify_boss) 75 | misc_layout.addWidget(self._buffs["boss"], 0, 2) 76 | self._buffs["dragonling"] = QPushButton(self) 77 | self._buffs["dragonling"].setStyleSheet("QPushButton { background-color: transparent; border: 0px }") 78 | self._buffs["dragonling"].setCheckable(True) 79 | self._buffs["dragonling"].setToolTip("Arcanite Dragonling") 80 | icon = QIcon() 81 | icon.addPixmap(get_pixmap(self._DRAGONLING_ICON_FN)) 82 | self._buffs["dragonling"].setIcon(icon) 83 | self._buffs["dragonling"].setIconSize(QSize(25, 25)) 84 | misc_layout.addWidget(self._buffs["dragonling"], 1, 0) 85 | misc_layout.addWidget(QLabel(f"Dragonling | Stack Time"), 1, 1) 86 | self._buffs["dragonling_time"] = GuardLineEdit("float", 1000.0) 87 | self._buffs["dragonling_time"].textChanged.connect(self.time_dragon) 88 | misc_layout.addWidget(self._buffs["dragonling_time"], 1, 2) 89 | 90 | self._buffs["nightfall"] = [] 91 | self._buffs["nightfall_timer"] = [] 92 | for index in range(self._MAX_NIGHTFALL): 93 | nightfall = QPushButton(self) 94 | nightfall.setStyleSheet("QPushButton { background-color: transparent; border: 0px }") 95 | nightfall.setCheckable(True) 96 | nightfall.setToolTip("Nightfall") 97 | icon = QIcon() 98 | icon.addPixmap(get_pixmap(self._NIGHTFALL_ICON_FN)) 99 | nightfall.setIcon(icon) 100 | nightfall.setIconSize(QSize(25, 25)) 101 | misc_layout.addWidget(nightfall, index + 2, 0) 102 | self._buffs["nightfall"].append(nightfall) 103 | misc_layout.addWidget(QLabel(f"Swing Timer {index + 1:d}"), index + 2, 1) 104 | swing = GuardLineEdit("float", 100.0) 105 | swing.textChanged.connect(lambda state, x=index: self.timer_nightfall(x)) 106 | misc_layout.addWidget(swing, index + 2, 2) 107 | self._buffs["nightfall_timer"].append(swing) 108 | misc.setLayout(misc_layout) 109 | layout.addWidget(misc) 110 | 111 | layout.addStretch() 112 | for buff_type in self._BUFFS: 113 | grid = QGroupBox(buff_type.capitalize()) 114 | grid_layout = QGridLayout() 115 | grid_layout.setSpacing(0) 116 | for index, vals in enumerate(self._BUFFS[buff_type]): 117 | name = vals[self._VALUES.index("name")] 118 | tooltip = vals[self._VALUES.index("tooltip")] 119 | icon_fn = vals[self._VALUES.index("icon_fn")] 120 | row = vals[self._VALUES.index("row")] 121 | col = vals[self._VALUES.index("col")] 122 | 123 | self._buffs[name] = QPushButton(self) 124 | self._buffs[name].setStyleSheet("QPushButton { background-color: transparent; border: 0px }") 125 | self._buffs[name].setCheckable(True) 126 | self._buffs[name].setToolTip(tooltip) 127 | icon = QIcon() 128 | icon.addPixmap(get_pixmap(icon_fn)) 129 | self._buffs[name].setIcon(icon) 130 | self._buffs[name].setIconSize(QSize(40, 40)) 131 | self._buffs[name].clicked.connect(lambda state, x=buff_type, y=index: self.modify_button(x, y)) 132 | grid_layout.addWidget(self._buffs[name], row, col) 133 | grid.setLayout(grid_layout) 134 | layout.addWidget(grid) 135 | layout.addStretch() 136 | 137 | self.setLayout(layout) 138 | 139 | def fill(self, top=False): 140 | config = self._config.current().config() 141 | self._buffs["boss"].setCurrentIndex(self._BOSSES.index(config["buffs"]["boss"])) 142 | clear_dragonling = False 143 | clear_nightfall = False 144 | if "proc" in config["buffs"]: 145 | if "dragonling" in config["buffs"]["proc"]: 146 | icon = QIcon() 147 | icon.addPixmap(get_pixmap(self._DRAGONLING_ICON_FN, fade=False)) 148 | self._buffs["dragonling"].setIcon(icon) 149 | self._buffs["dragonling_time"].setText(str(config["buffs"]["proc"]["dragonling"])) 150 | if top: 151 | self._buffs["dragonling_time"].set_text(str(config["buffs"]["proc"]["dragonling"])) 152 | else: 153 | clear_dragonling = True 154 | if "nightfall" in config["buffs"]["proc"]: 155 | for index, val in enumerate(config["buffs"]["proc"]["nightfall"]): 156 | icon = QIcon() 157 | icon.addPixmap(get_pixmap(self._NIGHTFALL_ICON_FN, fade=False)) 158 | self._buffs["nightfall"][index].setIcon(icon) 159 | self._buffs["nightfall"][index].setChecked(True) 160 | self._buffs["nightfall_timer"][index].setText(str(val)) 161 | if top: 162 | self._buffs["nightfall_timer"][index].set_text(str(val)) 163 | self._buffs["nightfall_timer"][index].setEnabled(False) 164 | self._buffs["nightfall_timer"][index].setEnabled(True) 165 | for index2 in range(index + 1, self._MAX_NIGHTFALL): 166 | icon = QIcon() 167 | icon.addPixmap(get_pixmap(self._NIGHTFALL_ICON_FN, fade=True)) 168 | self._buffs["nightfall"][index2].setIcon(icon) 169 | self._buffs["nightfall_timer"][index2].setText("") 170 | if top: 171 | self._buffs["nightfall_timer"][index2].set_text("") 172 | enabled = index2 == index + 1 173 | self._buffs["nightfall_timer"][index2].setEnabled(enabled) 174 | else: 175 | clear_nightfall = True 176 | else: 177 | clear_nightfall = True 178 | clear_dragonling = True 179 | if clear_dragonling: 180 | icon = QIcon() 181 | icon.addPixmap(get_pixmap(self._DRAGONLING_ICON_FN, fade=True)) 182 | self._buffs["dragonling"].setIcon(icon) 183 | self._buffs["dragonling_time"].setText("") 184 | if top: 185 | self._buffs["dragonling_time"].set_text("") 186 | if clear_nightfall: 187 | for index in range(self._MAX_NIGHTFALL): 188 | icon = QIcon() 189 | icon.addPixmap(get_pixmap(self._NIGHTFALL_ICON_FN, fade=True)) 190 | self._buffs["nightfall"][index].setIcon(icon) 191 | enabled = True if not index else False 192 | self._buffs["nightfall_timer"][index].setEnabled(enabled) 193 | self._buffs["nightfall_timer"][index].setText("") 194 | if top: 195 | self._buffs["nightfall_timer"][index].set_text("") 196 | 197 | for btype in self._BUFFS: 198 | for vals in self._BUFFS[btype]: 199 | name = vals[self._VALUES.index("name")] 200 | icon_fn = vals[self._VALUES.index("icon_fn")] 201 | enabled = vals[self._VALUES.index("enabled")] 202 | 203 | state = 1 if name in config["buffs"][btype] else 0 204 | self._buffs[name].setChecked(state) 205 | icon = QIcon() 206 | icon.addPixmap(get_pixmap(icon_fn, fade=not state)) 207 | self._buffs[name].setIcon(icon) 208 | self._buffs[name].setEnabled(enabled) 209 | 210 | def modify_boss(self): 211 | config = self._config.current().config() 212 | boss = self._buffs["boss"].currentText() 213 | config["buffs"]["boss"] = boss 214 | if self._changed_trigger is not None: 215 | self._changed_trigger() 216 | 217 | def time_dragon(self): 218 | config = self._config.current().config() 219 | value = self._buffs["dragonling_time"].text() 220 | icon = QIcon() 221 | if value and value != ".": 222 | if "proc" not in config["buffs"]: 223 | config["buffs"]["proc"] = {"dragonling": float(value)} 224 | else: 225 | config["buffs"]["proc"]["dragonling"] = float(value) 226 | icon.addPixmap(get_pixmap(self._DRAGONLING_ICON_FN, fade=False)) 227 | else: 228 | icon.addPixmap(get_pixmap(self._DRAGONLING_ICON_FN, fade=True)) 229 | if "proc" in config["buffs"]: 230 | if "dragonling" in config["buffs"]["proc"]: 231 | config["buffs"]["proc"].pop("dragonling") 232 | if not config["buffs"]["proc"]: 233 | config["buffs"].pop("proc") 234 | self._buffs["dragonling"].setIcon(icon) 235 | 236 | if self._changed_trigger is not None: 237 | self._changed_trigger() 238 | 239 | 240 | def timer_nightfall(self, index): 241 | config = self._config.current().config() 242 | value = self._buffs["nightfall_timer"][index].text() 243 | icon = QIcon() 244 | icon.addPixmap(get_pixmap(self._NIGHTFALL_ICON_FN, fade=not value)) 245 | self._buffs["nightfall"][index].setIcon(icon) 246 | if value and value != ".": 247 | if "proc" not in config["buffs"]: 248 | config["buffs"]["proc"] = {"nightfall": [float(value)]} 249 | elif "nightfall" not in config["buffs"]["proc"]: 250 | config["buffs"]["proc"]["nightfall"] = [float(value)] 251 | else: 252 | if index >= len(config["buffs"]["proc"]["nightfall"]): 253 | config["buffs"]["proc"]["nightfall"].append(float(value)) 254 | self._buffs["nightfall_timer"][index - 1].setEnabled(False) 255 | else: 256 | config["buffs"]["proc"]["nightfall"][index] = float(value) 257 | if index + 1 < self._MAX_NIGHTFALL: 258 | self._buffs["nightfall_timer"][index + 1].setEnabled(True) 259 | elif value != ".": 260 | if "proc" in config["buffs"]: 261 | if "nightfall" in config["buffs"]["proc"]: 262 | config["buffs"]["proc"]["nightfall"].pop() 263 | if not config["buffs"]["proc"]["nightfall"]: 264 | config["buffs"]["proc"].pop("nightfall") 265 | if not config["buffs"]["proc"]: 266 | config["buffs"].pop("proc") 267 | if index > 0: 268 | self._buffs["nightfall_timer"][index - 1].setEnabled(True) 269 | if index + 1 < self._MAX_NIGHTFALL: 270 | self._buffs["nightfall_timer"][index + 1].setEnabled(False) 271 | if self._changed_trigger is not None: 272 | self._changed_trigger() 273 | 274 | def modify_button(self, btype, index): 275 | config = self._config.current().config() 276 | 277 | vals = self._BUFFS[btype][index] 278 | name = vals[self._VALUES.index("name")] 279 | icon_fn = vals[self._VALUES.index("icon_fn")] 280 | 281 | state = self._buffs[name].isChecked() 282 | icon = QIcon() 283 | icon.addPixmap(get_pixmap(icon_fn, fade=not state)) 284 | self._buffs[name].setIcon(icon) 285 | atm = config["buffs"][btype] 286 | if state: 287 | atm.append(name) 288 | elif name in atm: 289 | atm.remove(name) 290 | config["buffs"][btype] = atm 291 | if self._changed_trigger is not None: 292 | self._changed_trigger() 293 | 294 | def set_changed_trigger(self, changed_trigger): 295 | self._changed_trigger = changed_trigger 296 | -------------------------------------------------------------------------------- /src/gui/scenario_editor/character.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import ( 2 | QApplication, 3 | QPushButton, 4 | QVBoxLayout, 5 | QHBoxLayout, 6 | QTabWidget, 7 | QCheckBox, 8 | QWidget, 9 | QStackedWidget, 10 | QLabel 11 | ) 12 | 13 | class Character(QWidget): 14 | 15 | def __init__(self): 16 | super().__init__() -------------------------------------------------------------------------------- /src/gui/scenario_editor/scenario.py: -------------------------------------------------------------------------------- 1 | import json 2 | from PyQt5.QtWidgets import ( 3 | QPushButton, 4 | QGridLayout, 5 | QVBoxLayout, 6 | QHBoxLayout, 7 | QTableWidget, 8 | QComboBox, 9 | QCheckBox, 10 | QWidget, 11 | QStackedWidget, 12 | QLabel, 13 | QHeaderView, 14 | QGroupBox 15 | ) 16 | from .character import Character 17 | from .mages import Group 18 | from .rotation import Rotation 19 | from .buffs import Buffs 20 | 21 | class Scenario(QStackedWidget): 22 | 23 | def __init__(self, config_list): 24 | 25 | super().__init__() 26 | self._settings_refresh = None 27 | 28 | self._group = Group(config_list, self.mod_mages) 29 | self._buffs = Buffs(config_list) 30 | self._rotation = Rotation(config_list) 31 | 32 | 33 | self._character = Character() 34 | 35 | scen = QWidget() 36 | gbs = QWidget() 37 | layout1 = QHBoxLayout() 38 | layout2 = QVBoxLayout() 39 | 40 | layout2.addWidget(self._group) 41 | layout2.addWidget(self._buffs) 42 | gbs.setLayout(layout2) 43 | gbs.setFixedWidth(950) 44 | 45 | layout1.addWidget(gbs) 46 | layout1.addWidget(self._rotation) 47 | 48 | scen.setLayout(layout1) 49 | 50 | self.addWidget(scen) 51 | self.addWidget(self._character) 52 | self._changed_trigger = None 53 | 54 | def update(self): 55 | temp_ct = self._changed_trigger 56 | self.set_changed_trigger(None) 57 | self._group.fill(top=True) 58 | self._buffs.fill(top=True) 59 | self._rotation.fill(top=True) 60 | self.set_changed_trigger(temp_ct) 61 | 62 | def set_changed_trigger(self, changed_trigger): 63 | self._changed_trigger = changed_trigger 64 | self._group.set_changed_trigger(changed_trigger) 65 | self._buffs.set_changed_trigger(changed_trigger) 66 | self._rotation.set_changed_trigger(changed_trigger) 67 | 68 | def mod_mages(self, stype: int): 69 | self._rotation.mod_mages(stype) 70 | 71 | def settings_refresh(self, refresh): 72 | self._settings_refresh = refresh 73 | self._group.settings_refresh(refresh) 74 | -------------------------------------------------------------------------------- /src/gui/simulation_interface/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Wed Feb 23 18:15:34 2022 4 | 5 | @author: jongo 6 | """ 7 | 8 | -------------------------------------------------------------------------------- /src/gui/simulation_interface/output.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | from PyQt5.QtWidgets import ( 4 | QPushButton, 5 | QGridLayout, 6 | QVBoxLayout, 7 | QHBoxLayout, 8 | QTableWidget, 9 | QComboBox, 10 | QCheckBox, 11 | QLineEdit, 12 | QWidget, 13 | QStackedWidget, 14 | QLabel, 15 | QHeaderView, 16 | QButtonGroup, 17 | QGroupBox, 18 | QFrame 19 | ) 20 | from PyQt5.QtCore import Qt, QSize 21 | import matplotlib as mpl 22 | mpl.use('Agg') 23 | import matplotlib.pyplot as plt 24 | from ..utils.icon_edit import numpy_to_qt_pixmap 25 | from skimage import io 26 | 27 | class StatOutput(QWidget): 28 | 29 | _OUTPUT_SIZE = 780 30 | _HIST_BINS = 512 31 | _EDGE_CHOP = (50, 20, 85, 75) 32 | _DPS_TYPES_ROW = ["Mean", "Min", "Max", "Median", "90%"] 33 | _EP_TYPES_BOX = ["SP per Crit %", "SP per Hit %"] 34 | _EP_TYPES = ["crit", "hit"] 35 | _EP_METRICS = ["Mean", "90%"] 36 | 37 | def __init__(self): 38 | super().__init__() 39 | self._config = None 40 | self._filename = None 41 | self._runs = None 42 | self.setFixedWidth(self._OUTPUT_SIZE) 43 | 44 | layout = QVBoxLayout() 45 | layout.setContentsMargins(5, 0, 5, 0) 46 | self._dist_plot = QLabel() 47 | layout.addWidget(self._dist_plot) 48 | 49 | style = "QLabel{font-size: 16px;}" 50 | 51 | self._dps = {} 52 | dps_frame = QGroupBox("DPS") 53 | dps_layout = QHBoxLayout() 54 | dps_layout.setContentsMargins(5, 0, 5, 0) 55 | dps_layout.setSpacing(0) 56 | dps_layout.setAlignment(Qt.AlignCenter) 57 | dps_layout.addStretch() 58 | for value in self._DPS_TYPES_ROW: 59 | self._dps[value] = QLabel("") 60 | self._dps[value].setStyleSheet(style) 61 | dps_layout.addWidget(self._dps[value]) 62 | dps_layout.addStretch() 63 | dps_frame.setLayout(dps_layout) 64 | layout.addWidget(dps_frame) 65 | dps_frame.setVisible(False) 66 | self._frames = [dps_frame] 67 | 68 | self._ep = {} 69 | ep_row = QWidget() 70 | ep_layout = QHBoxLayout() 71 | frame = QGroupBox("DPS per SP") 72 | frame_layout = QHBoxLayout() 73 | frame_layout.setContentsMargins(5, 0, 5, 0) 74 | frame_layout.setSpacing(0) 75 | frame_layout.setAlignment(Qt.AlignCenter) 76 | self._ep["SP"] = QLabel("") 77 | self._ep["SP"].setStyleSheet(style) 78 | frame_layout.addWidget(self._ep["SP"]) 79 | frame.setLayout(frame_layout) 80 | ep_layout.addWidget(frame) 81 | frame.setVisible(False) 82 | self._frames.append(frame) 83 | 84 | for ep_type, ep_box in zip(self._EP_TYPES, self._EP_TYPES_BOX): 85 | frame = QGroupBox(ep_box) 86 | frame_layout = QHBoxLayout() 87 | frame_layout.setContentsMargins(5, 0, 5, 0) 88 | frame_layout.setSpacing(0) 89 | frame_layout.setAlignment(Qt.AlignCenter) 90 | frame_layout.addStretch() 91 | for metric in self._EP_METRICS: 92 | value = "_".join([ep_type, metric]) 93 | self._ep[value] = QLabel("") 94 | self._ep[value].setStyleSheet(style) 95 | frame_layout.addWidget(self._ep[value]) 96 | frame_layout.addStretch() 97 | frame.setLayout(frame_layout) 98 | ep_layout.addWidget(frame) 99 | frame.setVisible(False) 100 | self._frames.append(frame) 101 | ep_row.setLayout(ep_layout) 102 | layout.addWidget(ep_row) 103 | 104 | self.setLayout(layout) 105 | 106 | def prepare_output(self, config, filename): 107 | self._config = config 108 | self._filename = filename 109 | 110 | def set_runs(self, runs): 111 | self._runs = runs 112 | self._received = {} 113 | for value in self._DPS_TYPES_ROW: 114 | self._dps[value].setText("") 115 | self._ep["SP"].setText("") 116 | for ep_type in self._EP_TYPES: 117 | for metric in self._EP_METRICS: 118 | value = "_".join([ep_type, metric]) 119 | self._ep[value].setText("") 120 | 121 | def handle_output(self, result): 122 | run_id, output = result 123 | output /= len(self._config["configuration"]["target"]) 124 | self._received[run_id] = output 125 | if run_id == "dps": 126 | tmp_filename = "temp.png" 127 | hist, edges = np.histogram(output, self._HIST_BINS) 128 | centers = (edges[:-1] + edges[1:])/2.0 129 | 130 | plt.close('all') 131 | plt.figure(figsize=(9.0, 6.25), dpi=100) 132 | plt.style.use('dark_background') 133 | plt.title(f"{self._filename:s} Damage Distribution") 134 | plt.plot(centers, hist) 135 | plt.xlabel('DPS per Mage') 136 | plt.yticks([]) 137 | plt.savefig(tmp_filename) 138 | data = io.imread(tmp_filename) 139 | os.unlink(tmp_filename) 140 | data = np.array(data[self._EDGE_CHOP[0]:(data.shape[0] - self._EDGE_CHOP[1]), 141 | self._EDGE_CHOP[2]:(data.shape[1] - self._EDGE_CHOP[3]), :3]) 142 | pixmap = numpy_to_qt_pixmap(data) 143 | self._dist_plot.setPixmap(pixmap) 144 | 145 | value = output.mean() 146 | self._dps["Mean"].setText(f"Mean: {value:.0f}") 147 | value = output.min() 148 | self._dps["Min"].setText(f"Min: {value:.0f}") 149 | value = output.max() 150 | self._dps["Max"].setText(f"Max: {value:.0f}") 151 | value = output[int(len(output)/2 + 0.5)] 152 | self._dps["Median"].setText(f"Median: {value:.0f}") 153 | value = output[int(0.9*len(output) + 0.5)] 154 | self._dps["90%"].setText(f"90%: {value:.0f}") 155 | 156 | if all([run_id2 in self._received for run_id2 in ["dps", "sp"]]): 157 | value_0 = self._received["dps"].mean() 158 | value_sp = self._received["sp"].mean() 159 | metric = (value_sp - value_0)/15.0 160 | self._ep["SP"].setText(f"Mean: {metric:.2f}") 161 | 162 | for run_id3 in ["crit", "hit"]: 163 | if all([run_id2 in self._received for run_id2 in ["dps", "sp", run_id3]]): 164 | for metric in self._EP_METRICS: 165 | if metric == "Mean": 166 | value_cr = self._received[run_id3].mean() 167 | value_0 = self._received["dps"].mean() 168 | value_sp = self._received["sp"].mean() 169 | elif metric == "90%": 170 | size = self._received["dps"].size 171 | level_90 = int(0.9*size) 172 | value_cr = self._received[run_id3][level_90] 173 | value_0 = self._received["dps"][level_90] 174 | value_sp = self._received["sp"][level_90] 175 | value = 10.0*(value_cr - value_0)/(value_sp - value_0) 176 | if run_id3 == "hit": 177 | value = -value 178 | str_value = "_".join([run_id3, metric]) 179 | self._ep[str_value].setText(f"{metric:s}: {value:.1f}") 180 | if len(self._runs) == len(self._received): 181 | for frame in self._frames: 182 | frame.setVisible(True) 183 | 184 | class CompareOutput(QWidget): 185 | _OUTPUT_SIZE = 780 186 | _EDGE_CHOP = (30, 10, 20, 30) 187 | _COLORS = ["#FF0000", "#FFFF00", "#00FF00", "#007FFF", "#FF00FF"] 188 | 189 | def __init__(self): 190 | super().__init__() 191 | self._num_mages = None 192 | self._runs = None 193 | self.setFixedWidth(self._OUTPUT_SIZE) 194 | 195 | layout = QVBoxLayout() 196 | layout.setContentsMargins(5, 0, 5, 0) 197 | self._plot = QLabel() 198 | layout.addWidget(self._plot) 199 | 200 | self.setLayout(layout) 201 | 202 | def prepare_output(self, num_mages, min_time, max_time, etimes): 203 | self._num_mages = num_mages 204 | self._min_time = min_time 205 | self._max_time = max_time 206 | self._etimes = etimes 207 | 208 | def set_runs(self, runs): 209 | self._runs = runs 210 | self._received = [None for dummy in range(len(runs))] 211 | 212 | def handle_output(self, result): 213 | run_id, output = result 214 | index = self._runs.index(run_id) 215 | output = np.array(output)/self._num_mages[index] 216 | self._received[index] = output 217 | if all([rec is not None for rec in self._received]): 218 | ymin = 99999.9 219 | ymax = 0.0 220 | cmin = self._min_time 221 | cmax = self._max_time 222 | for yvals in self._received: 223 | for ctime, yval in zip(self._etimes, yvals): 224 | if ctime >= cmin and ctime <= cmax: 225 | if yval < ymin: 226 | ymin = yval 227 | if yval > ymax: 228 | ymax = yval 229 | 230 | tmp_filename = "temp.png" 231 | plt.close('all') 232 | plt.figure(figsize=(8.0, 6.75), dpi=100) 233 | plt.style.use('dark_background') 234 | plt.title("Scenario Comparison") 235 | for idx, (run_id, values) in enumerate(zip(self._runs, self._received)): 236 | plt.plot(self._etimes, values, label=run_id, color=self._COLORS[idx]) 237 | plt.xlabel('Encounter Duration (seconds)') 238 | plt.ylabel('Damage per mage') 239 | plt.grid() 240 | plt.xlim(cmin, cmax) 241 | plt.ylim(ymin, ymax + 0.02*(ymax - ymin)) 242 | plt.legend() 243 | plt.savefig(tmp_filename) 244 | data = io.imread(tmp_filename) 245 | os.unlink(tmp_filename) 246 | data = np.array(data[self._EDGE_CHOP[0]:(data.shape[0] - self._EDGE_CHOP[1]), 247 | self._EDGE_CHOP[2]:(data.shape[1] - self._EDGE_CHOP[3]), :3]) 248 | pixmap = numpy_to_qt_pixmap(data) 249 | self._plot.setPixmap(pixmap) 250 | -------------------------------------------------------------------------------- /src/gui/simulation_interface/simulation.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | from PyQt5.QtWidgets import ( 4 | QPushButton, 5 | QGridLayout, 6 | QVBoxLayout, 7 | QHBoxLayout, 8 | QTableWidget, 9 | QComboBox, 10 | QCheckBox, 11 | QLineEdit, 12 | QWidget, 13 | QStackedWidget, 14 | QLabel, 15 | QHeaderView, 16 | QButtonGroup, 17 | QGroupBox, 18 | QStatusBar, 19 | QFrame 20 | ) 21 | from PyQt5.QtGui import QPixmap 22 | from PyQt5.QtCore import Qt, QSize 23 | from .settings import StatSettings, CompareSettings 24 | from .output import StatOutput, CompareOutput 25 | from ..utils.icon_edit import numpy_to_qt_pixmap 26 | 27 | class Progress(QLabel): 28 | 29 | _SHAPE = (35, 300, 3) 30 | _FONT_COLOR = f"rgb(255, 255, 255)" 31 | _BAR_COLOR = np.array([64, 192, 32]) 32 | 33 | def __init__(self): 34 | super().__init__() 35 | self.setText("") 36 | self.setAlignment(Qt.AlignCenter | Qt.AlignVCenter) 37 | self.setFixedHeight(self._SHAPE[0]) 38 | self.setFixedWidth(self._SHAPE[1]) 39 | layout = QVBoxLayout() 40 | layout.setContentsMargins(0, 0, 0, 0) 41 | layout.setSpacing(0) 42 | self._pl = QLabel() 43 | self._pl.setFixedHeight(20) 44 | self._pl.setFixedWidth(self._SHAPE[1]) 45 | style = f"QLabel{{font-size: 18px; background: transparent; color: {self._FONT_COLOR:s};}}" 46 | self._pl.setStyleSheet(style) 47 | self._pl.setAlignment(Qt.AlignCenter | Qt.AlignVCenter) 48 | layout.addWidget(self._pl) 49 | self.setLayout(layout) 50 | 51 | def set_value(self, value: float): 52 | img = np.zeros(self._SHAPE, dtype=np.uint8) 53 | frac = int(min([value*self._SHAPE[1]/100.0, self._SHAPE[1]])) 54 | img[:,:frac, :] = self._BAR_COLOR 55 | self.setPixmap(numpy_to_qt_pixmap(img)) 56 | self._pl.setText(f"{int(value):d}%") 57 | 58 | class Simulation(QWidget): 59 | 60 | _SAMPLE_DIRECTORY = "./data/samples/" 61 | _SIMULATION_TYPES = [ 62 | ("Stat Weights/Distribution", "distribution.png", StatSettings, StatOutput), 63 | ("Scenario Comparison", "compare.png", CompareSettings, CompareOutput) 64 | ] 65 | _INDEX = ["name", "sample_file", "setting_class", "output_class"] 66 | 67 | def __init__(self, config_list, filename_func): 68 | super().__init__() 69 | self._config_list = config_list 70 | self._filenames = filename_func 71 | self._runs = None 72 | 73 | self._simtype_index = 0 74 | 75 | layout = QHBoxLayout() 76 | control_col = QWidget() 77 | cc_layout = QVBoxLayout() 78 | 79 | # sample 80 | sample_frame = QFrame() 81 | sample_layout = QHBoxLayout() 82 | self._sample = QLabel() 83 | self.fill_sample() 84 | sample_layout.setAlignment(Qt.AlignCenter | Qt.AlignVCenter) 85 | sample_layout.addWidget(self._sample) 86 | sample_frame.setLayout(sample_layout) 87 | cc_layout.addWidget(sample_frame) 88 | 89 | # selector 90 | select_frame = QGroupBox("Simulation Type") 91 | select_layout = QVBoxLayout() 92 | self._select = QButtonGroup() 93 | for index, stype in enumerate(self._SIMULATION_TYPES): 94 | sim_name = stype[self._INDEX.index("name")] 95 | button = QPushButton(sim_name) 96 | button.setCheckable(True) 97 | style = "QPushButton{font-size: 18px; color: rgb(0, 0, 0);} " 98 | style += "QPushButton:hover{font-size: 18px; color: rgb(0, 0, 0);} " 99 | style += "QPushButton:checked{font-size: 18px; color: rgb(255, 255, 255);}" 100 | button.setStyleSheet(style) 101 | if index == self._simtype_index: 102 | button.setChecked(True) 103 | select_layout.addWidget(button) 104 | self._select.addButton(button) 105 | self._select.setId(button, index) 106 | self._select.buttonClicked.connect(self.modify_type) 107 | select_frame.setLayout(select_layout) 108 | cc_layout.addWidget(select_frame) 109 | 110 | # settings 111 | settings_frame = QGroupBox("Simulation Settings") 112 | settings_layout = QVBoxLayout() 113 | self._settings_stack = QStackedWidget() 114 | self._settings = [] 115 | for stype in self._SIMULATION_TYPES: 116 | sim_class = stype[self._INDEX.index("setting_class")] 117 | setting = sim_class(config_list, filename_func) 118 | self._settings_stack.addWidget(setting) 119 | self._settings.append(setting) 120 | settings_layout.addWidget(self._settings_stack) 121 | settings_frame.setLayout(settings_layout) 122 | cc_layout.addWidget(settings_frame) 123 | 124 | # progress/number of simulations 125 | prog_frame = QWidget() 126 | prog_layout = QHBoxLayout() 127 | prog_layout.setAlignment(Qt.AlignCenter | Qt.AlignVCenter) 128 | self._progress = Progress() 129 | prog_layout.addWidget(self._progress) 130 | prog_frame.setLayout(prog_layout) 131 | cc_layout.addWidget(prog_frame) 132 | 133 | # run button -- note the reverse is being used 134 | self._run = QPushButton("Run Simulation") 135 | self._run.setCheckable(True) 136 | self._run.setChecked(False) 137 | self._run.released.connect(self.run) 138 | self._run.setMinimumHeight(50) 139 | style = "QPushButton{font-size: 24px; background-color: rgb(168, 184, 194); color: rgb(192, 0, 0);} " 140 | style += "QPushButton:hover{font-size: 24px; background-color: rgb(208, 214, 224); color: rgb(192, 0, 0);} " 141 | style += "QPushButton:checked{font-size: 24px; background-color: rgb(72, 89, 104); color: rgb(192, 0, 0);}" 142 | self._run.setStyleSheet(style) 143 | cc_layout.addWidget(self._run) 144 | 145 | # end of control panel 146 | control_col.setLayout(cc_layout) 147 | layout.addWidget(control_col) 148 | 149 | # start output 150 | self._output_stack = QStackedWidget() 151 | self._output = [] 152 | for stype in self._SIMULATION_TYPES: 153 | sim_class = stype[self._INDEX.index("output_class")] 154 | output = sim_class() 155 | self._output_stack.addWidget(output) 156 | self._output.append(output) 157 | layout.addWidget(self._output_stack) 158 | 159 | self.setLayout(layout) 160 | 161 | for setting, output in zip(self._settings, self._output): 162 | setting.set_progbar(self._progress.set_value) 163 | setting.set_output(output.prepare_output) 164 | 165 | def processing_complete(self): 166 | self._run_count += 1 167 | if self._run_count == self._runs: 168 | self._run.setEnabled(True) 169 | self._run.setChecked(False) 170 | 171 | def set_runs(self, runs): 172 | self._runs = len(runs) 173 | self._output[self._index_at_run].set_runs(runs) 174 | 175 | def run(self): 176 | self._run.setEnabled(False) 177 | self._index_at_run = self._simtype_index 178 | run_func = self._settings[self._simtype_index].run 179 | handle_output = self._output[self._simtype_index].handle_output 180 | self._run_count = 0 181 | run_func(self.processing_complete, handle_output, self.set_runs) 182 | 183 | def modify_type(self, button: QPushButton): 184 | index = self._select.id(button) 185 | if index == self._simtype_index: 186 | return 187 | self._simtype_index = index 188 | self.fill_sample() 189 | self._settings_stack.setCurrentIndex(index) 190 | self._output_stack.setCurrentIndex(index) 191 | 192 | def fill_sample(self): 193 | filename = self._SIMULATION_TYPES[self._simtype_index][self._INDEX.index("sample_file")] 194 | fullfile = os.path.join(self._SAMPLE_DIRECTORY, filename) 195 | pixmap = QPixmap(fullfile) 196 | self._sample.setPixmap(pixmap) 197 | 198 | def refresh(self): 199 | for setting in self._settings: 200 | setting.refresh() 201 | -------------------------------------------------------------------------------- /src/gui/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Wed Feb 23 18:15:34 2022 4 | 5 | @author: jongo 6 | """ 7 | 8 | -------------------------------------------------------------------------------- /src/gui/utils/guard_lineedit.py: -------------------------------------------------------------------------------- 1 | import json 2 | from PyQt5.QtWidgets import ( 3 | QPushButton, 4 | QGridLayout, 5 | QVBoxLayout, 6 | QHBoxLayout, 7 | QTableWidget, 8 | QMessageBox, 9 | QComboBox, 10 | QCheckBox, 11 | QLineEdit, 12 | QWidget, 13 | QStackedWidget, 14 | QLabel, 15 | QHeaderView, 16 | QGroupBox 17 | ) 18 | from PyQt5.QtGui import QIntValidator, QDoubleValidator, QIcon 19 | from PyQt5.QtCore import Qt, QSize, pyqtSignal 20 | 21 | class GuardLineEdit(QLineEdit): 22 | 23 | #currenttextedited = pyqtSignal(str) 24 | 25 | def __init__(self, ftype, max_val, min_val=0, required=False, parent=None): 26 | super(GuardLineEdit, self).__init__(parent) 27 | self._ftype = ftype 28 | self._min_val = min_val 29 | self._max_val = max_val 30 | self._required = required 31 | 32 | 33 | # any change to text, happens second 34 | # self.textChanged.connect(self.text_changed) 35 | 36 | # only user changes 37 | self.textEdited.connect(self.text_edited) 38 | 39 | # works! 40 | #self.Enter.connect(self.editing_finished) 41 | self.editingFinished.connect(self.editing_finished) 42 | self._last = None 43 | self._last_edit = None 44 | self._lock = False 45 | 46 | #def text_changed(self, text): 47 | # print("text_changed", text) 48 | 49 | def pop_message(self, message): 50 | if not self._lock: # prevent double popup on enter 51 | self._lock = True 52 | msg = QMessageBox() 53 | msg.setIcon(QMessageBox.Critical) 54 | msg.setText(message) 55 | msg.setWindowTitle("Invalid Input") 56 | msg.exec() 57 | self._lock = False 58 | 59 | def text_edited(self, text): 60 | if self._ftype == "float": 61 | if text not in [".", ""]: 62 | try: 63 | new_text = float(text) 64 | self._last_edit = text 65 | except ValueError: 66 | if self._last_edit is not None: 67 | self.setText(self._last_edit) 68 | else: 69 | self.setText(self._last) 70 | elif self._ftype == "int": 71 | if text not in [""]: 72 | try: 73 | new_text = int(text) 74 | self._last_edit = text 75 | except ValueError: 76 | if self._last_edit is not None: 77 | self.setText(self._last_edit) 78 | else: 79 | self.setText(self._last) 80 | 81 | def editing_finished(self): 82 | text = self.text() 83 | self._last_edit = None 84 | if not text: 85 | if not self._required: 86 | return 87 | else: 88 | self.setText(self._last) 89 | if self._ftype == "float": 90 | try: 91 | value = float(text) 92 | except ValueError: 93 | self.pop_message("Not a floating point value.") 94 | self.setText(self._last) 95 | return 96 | if value < self._min_val or value > self._max_val: 97 | self.pop_message(f"Value out of bounds ({self._min_val:.1f}, {self._max_val:.1f}).") 98 | self.setText(self._last) 99 | else: 100 | if text[0] == '.': 101 | text = "0" + text 102 | elif "." not in text: 103 | text += ".0" 104 | self._last = text 105 | self.setText(text) 106 | elif self._ftype == "int": 107 | try: 108 | value = int(text) 109 | except ValueError: 110 | self.pop_message("Not an integer value.") 111 | self.setText(self._last) 112 | return 113 | if value < self._min_val or value > self._max_val: 114 | self.pop_message(f"Value out of bounds ({self._min_val:d}, {self._max_val:d}).") 115 | self.setText(self._last) 116 | else: 117 | self._last = text 118 | self.setText(text) 119 | 120 | def set_text(self, text): 121 | self._last = text 122 | -------------------------------------------------------------------------------- /src/gui/utils/icon_edit.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | from skimage import io 4 | from PyQt5.QtGui import QPixmap, QImage 5 | 6 | _ICONS_DIR = "./data/icons" 7 | 8 | def numpy_to_qt_image(image): 9 | height, width, channel = image.shape 10 | bytesPerLine = 3 * width 11 | return QImage(image.data, width, height, bytesPerLine, QImage.Format_RGB888) 12 | 13 | def numpy_to_qt_pixmap(image): 14 | return QPixmap(numpy_to_qt_image(image)) 15 | 16 | def get_pixmap(filename, fade=True, highlight=False): 17 | fade_factor = 0.15 18 | fullfile = os.path.join(_ICONS_DIR, filename) 19 | if not fade and not highlight: 20 | return QPixmap(fullfile) 21 | img = io.imread(fullfile).astype(np.float32) 22 | 23 | if highlight: 24 | s = img.shape 25 | red = np.array([255, 0, 0]).reshape(1, 1, 3).astype(np.float32) 26 | img[0:2, :, :] = red 27 | img[:, 0:2, :] = red 28 | img[(s[0] - 2):s[0], :, :] = red 29 | img[:, (s[1] - 2):s[1], :] = red 30 | 31 | if fade: 32 | img *= fade_factor 33 | img = img.astype(np.uint8) 34 | 35 | return numpy_to_qt_pixmap(img) 36 | -------------------------------------------------------------------------------- /src/gui/utils/worker.py: -------------------------------------------------------------------------------- 1 | import traceback, sys 2 | from PyQt5.QtCore import QThreadPool, QRunnable, pyqtSlot, pyqtSignal, QObject 3 | 4 | class WorkerSignals(QObject): 5 | ''' 6 | Defines the signals available from a running worker thread. 7 | 8 | Supported signals are: 9 | 10 | finished 11 | No data 12 | 13 | error 14 | tuple (exctype, value, traceback.format_exc() ) 15 | 16 | result 17 | object data returned from processing, anything 18 | 19 | progress 20 | int indicating % progress 21 | 22 | ''' 23 | finished = pyqtSignal() 24 | error = pyqtSignal(tuple) 25 | result = pyqtSignal(object) 26 | progress = pyqtSignal(tuple) 27 | 28 | 29 | class Worker(QRunnable): 30 | ''' 31 | Worker thread 32 | 33 | Inherits from QRunnable to handler worker thread setup, signals and wrap-up. 34 | 35 | :param callback: The function callback to run on this worker thread. Supplied args and 36 | kwargs will be passed through to the runner. 37 | :type callback: function 38 | :param args: Arguments to pass to the callback function 39 | :param kwargs: Keywords to pass to the callback function 40 | 41 | ''' 42 | 43 | def __init__(self, fn, *args, **kwargs): 44 | super(Worker, self).__init__() 45 | 46 | # Store constructor arguments (re-used for processing) 47 | self.fn = fn 48 | self.args = args 49 | self.kwargs = kwargs 50 | self.signals = WorkerSignals() 51 | 52 | # Add the callback to our kwargs 53 | self.kwargs['progress_callback'] = self.signals.progress 54 | 55 | @pyqtSlot() 56 | def run(self): 57 | ''' 58 | Initialise the runner function with passed args, kwargs. 59 | ''' 60 | 61 | # Retrieve args/kwargs here; and fire processing using them 62 | try: 63 | result = self.fn(*self.args, **self.kwargs) 64 | except: 65 | traceback.print_exc() 66 | exctype, value = sys.exc_info()[:2] 67 | self.signals.error.emit((exctype, value, traceback.format_exc())) 68 | else: 69 | self.signals.result.emit(result) # Return the result of the processing 70 | finally: 71 | self.signals.finished.emit() # Done 72 | -------------------------------------------------------------------------------- /src/items/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Wed Feb 23 18:15:34 2022 4 | 5 | @author: jongo 6 | """ 7 | 8 | -------------------------------------------------------------------------------- /src/items/build_group.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import pickle 4 | from items import CharInfo 5 | 6 | __savefile = "items.dat" 7 | 8 | if __name__ == "__main__": 9 | encounter = "loatheb" 10 | encounter = "pink-team" 11 | template = "template.json" 12 | 13 | if os.path.exists(__savefile): 14 | with open(__savefile, "rb") as fid: 15 | saved = pickle.load(fid) 16 | else: 17 | saved = {} 18 | 19 | chars = [] 20 | for filename in os.listdir(encounter): 21 | fullfile = os.path.join(encounter, filename) 22 | name = filename.split('-')[-1].split('.')[0] 23 | char_info = CharInfo(fullfile, name, saved) 24 | chars.append(char_info) 25 | print(name, char_info.spd, char_info.is_udc) 26 | 27 | with open(__savefile, "wb") as fid: 28 | pickle.dump(saved, fid) 29 | 30 | with open(template, "rt") as fid: 31 | econf = json.load(fid) 32 | 33 | econf["stats"] = { 34 | "spell_power": [], 35 | "hit_chance": [], 36 | "crit_chance": [], 37 | "intellect": [] 38 | } 39 | econf["buffs"]["racial"] = [] 40 | econf["configuration"] = { 41 | "num_mages": len(chars), 42 | "target": [], 43 | "sapp": [], 44 | "toep": [], 45 | "zhc": [], 46 | "mqg": [], 47 | "pi": [], 48 | "udc": [], 49 | "name": []} 50 | 51 | for cindex, char in enumerate(chars): 52 | econf["stats"]["spell_power"].append(char.spd) 53 | econf["stats"]["hit_chance"].append(char.hit/100.0) 54 | crit = char.crt 55 | for char2 in chars: 56 | if char2.name != char.name and char2.atiesh: 57 | crit += 2 58 | 59 | econf["stats"]["crit_chance"].append(crit/100.0) 60 | econf["stats"]["intellect"].append(char.intellect) 61 | econf["buffs"]["racial"].append(char.race) 62 | 63 | for active in ["sapp", "toep", "zhc", "mqg"]: 64 | if active in char.act: 65 | econf["configuration"][active].append(cindex) 66 | if char.is_udc: 67 | econf["configuration"]["udc"].append(cindex) 68 | econf["configuration"]["name"].append(char.name) 69 | 70 | with open(encounter + ".json", "wt") as fid: 71 | json.dump(econf, fid, indent=3) 72 | -------------------------------------------------------------------------------- /src/items/items.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import xml.etree.ElementTree as ET 3 | import json 4 | 5 | _MAX_SLOTS = 27 6 | 7 | class ItemInfo(): 8 | 9 | def __init__(self, item, prev): 10 | self._enchant = None 11 | self._item = item 12 | self._new = False 13 | if item not in prev: 14 | self._new = True 15 | self._name = None 16 | self._armor = 0 17 | self._spd = 0 18 | self._int = 0 19 | self._crt = 0 20 | self._hit = 0 21 | self._slt = None 22 | self._set = None 23 | self._load_item(item) 24 | prev[item] = {"spd": self._spd, 25 | "int": self._int, 26 | "crt": self._crt, 27 | "hit": self._hit, 28 | "slt": self._slt, 29 | "set": self._set, 30 | "name": self._name, 31 | "armor": self._armor} 32 | else: 33 | self._name = prev[item]["name"] 34 | self._armor = prev[item]["armor"] 35 | self._spd = prev[item]["spd"] 36 | self._int = prev[item]["int"] 37 | self._crt = prev[item]["crt"] 38 | self._hit = prev[item]["hit"] 39 | self._slt = prev[item]["slt"] 40 | self._set = prev[item]["set"] 41 | 42 | act_lookup = { 43 | 23046: "sapp", 44 | 19339: "mqg", 45 | 18820: "toep", 46 | 19950: "zhc"} 47 | if item in act_lookup: 48 | self._act = act_lookup[item] 49 | else: 50 | self._act = None 51 | 52 | def _load_item(self, item): 53 | undead = {23207: 85, 54 | 23085: 48, 55 | 23084: 35, 56 | 23091: 26, 57 | 19812: 48} 58 | 59 | r = requests.get(f"http://classic.wowhead.com/item={item:d}&xml") 60 | tree = ET.fromstring(r.text) 61 | 62 | self._name = tree.find('./item/name').text 63 | 64 | info = tree.find('./item/jsonEquip') 65 | info = json.loads("{" + info.text + "}") 66 | if "spldmg" in info: 67 | self._spd += info["spldmg"] 68 | if "firsplpwr" in info: 69 | self._spd += info["firsplpwr"] 70 | if item in undead: 71 | self._spd += undead[item] 72 | if "int" in info: 73 | self._int += info["int"] 74 | if "splcritstrkpct" in info: 75 | self._crt += info["splcritstrkpct"] 76 | if "splhitpct" in info: 77 | self._hit += info["splhitpct"] 78 | if "slotbak" in info: 79 | self._slt = info["slotbak"] 80 | if "itemset" in info: 81 | self._set = info["itemset"] 82 | if "armor" in info: 83 | self._armor = info["armor"] 84 | 85 | def add_enchant(self, item, prev, spell=False): 86 | self._enchant = item 87 | self._enchant_spell = spell 88 | if not spell: 89 | enchant = ItemInfo(item, prev) 90 | self._spd += enchant.spd 91 | self._int += enchant.intellect 92 | self._crt += enchant.crt 93 | self._hit += enchant.hit 94 | else: 95 | if item == 20025: # greater stats chest 96 | self._int += 4 97 | elif item == 13941: # stats chest 98 | self._int += 3 99 | elif item == 13822: # int bracers 100 | self._int += 5 101 | elif item == 20008: # int bracers 102 | self._int += 7 103 | elif item == 25078: # firepower gloves 104 | self._spd += 20 105 | elif item == 22749: # spellpower weapon 106 | self._spd += 30 107 | 108 | @property 109 | def spd(self): 110 | return self._spd 111 | 112 | @property 113 | def intellect(self): 114 | return self._int 115 | 116 | @property 117 | def crt(self): 118 | return self._crt 119 | 120 | @property 121 | def hit(self): 122 | return self._hit 123 | 124 | @property 125 | def itemset(self): 126 | return self._set 127 | 128 | @property 129 | def act(self): 130 | return self._act 131 | 132 | @property 133 | def slot(self): 134 | return self._slt 135 | 136 | @property 137 | def item(self): 138 | return self._item 139 | 140 | @property 141 | def is_new(self): 142 | return self._new 143 | 144 | @property 145 | def name(self): 146 | return self._name 147 | 148 | 149 | class CharInfo(): 150 | 151 | def __init__(self, char_file, name, saved): 152 | self._name = name 153 | with open(char_file, "rt") as fid: 154 | char = json.load(fid) 155 | 156 | self._race = char["character"]["race"].lower() 157 | self._atiesh = False 158 | self._items = [] 159 | for item in char["items"]: 160 | item_info = ItemInfo(item["id"], saved) 161 | if item["id"] == 22589: 162 | self._atiesh = True 163 | if "enchant" in item: 164 | if "itemId" in item["enchant"]: 165 | item_info.add_enchant(item["enchant"]["itemId"], saved) 166 | elif "spellId" in item["enchant"]: 167 | item_info.add_enchant(item["enchant"]["spellId"], saved, spell=True) 168 | self._items.append(item_info) 169 | self._act = [item.act for item in self._items if item.act is not None] 170 | 171 | def replace(self, inc_item, saved, second=False): 172 | slot_map = [[idx] for idx in range(_MAX_SLOTS)] 173 | slot_map[5].append(20) 174 | slot_map[20].append(5) 175 | slot_map[13] += [17, 21] 176 | slot_map[17] += [13, 21, 23] 177 | slot_map[21] += [13, 17] 178 | slot_map[23] += [17] 179 | dupes = [11, 12] 180 | 181 | new_item = ItemInfo(inc_item, saved) 182 | 183 | # build up list of replacement items 184 | items_found = [] 185 | for idx, item in enumerate(self._items): 186 | if item.slot in slot_map[new_item.slot]: 187 | if item.slot in dupes: 188 | if not items_found or second: 189 | items_found = [idx] 190 | else: 191 | items_found.append(idx) 192 | 193 | # build new list 194 | new_items = [new_item] 195 | for idx, item in enumerate(self._items): 196 | if idx in items_found: 197 | if item._enchant is not None: 198 | new_items[0].add_enchant(item._enchant, saved, spell=item._enchant_spell) 199 | else: 200 | new_items.append(item) 201 | self._items = new_items 202 | self._act = [item.act for item in self._items if item.act is not None] 203 | 204 | @property 205 | def spd(self): 206 | item_spd = sum([item.spd for item in self._items]) 207 | set_spd = 0 208 | # magister 209 | set_spd += 23*int(sum([1 for item in self._items if item.itemset == 181]) >= 4) 210 | # sorcerer 211 | set_spd += 23*int(sum([1 for item in self._items if item.itemset == 517]) >= 6) 212 | # arcanist 213 | set_spd += 18*int(sum([1 for item in self._items if item.itemset == 201]) >= 3) 214 | # postmaster 215 | set_spd += 12*int(sum([1 for item in self._items if item.itemset == 81]) >= 4) 216 | # rare pvp 217 | set_spd += 23*int(sum([1 for item in self._items if item.itemset == 542]) >= 2) 218 | # epic pvp 219 | set_spd += 23*int(sum([1 for item in self._items if item.itemset == 388]) == 6) 220 | # necropile 221 | set_spd += 23*int(sum([1 for item in self._items if item.itemset == 122]) == 5) 222 | # illusionist 223 | set_spd += 12*int(sum([1 for item in self._items if item.itemset == 482]) >= 2) 224 | # zanzil 225 | set_spd += 6*int(sum([1 for item in self._items if item.itemset == 462]) == 2) 226 | 227 | return item_spd + set_spd 228 | 229 | @property 230 | def crt(self): 231 | item_crt = sum([item.crt for item in self._items]) 232 | set_crt = 0 233 | # bloodvine 234 | set_crt += 2*int(sum([1 for item in self._items if item.itemset == 421]) == 3) 235 | 236 | return item_crt + set_crt 237 | 238 | @property 239 | def hit(self): 240 | item_hit = sum([item.hit for item in self._items]) 241 | set_hit = 0 242 | 243 | #zanzil 244 | set_hit += int(sum([1 for item in self._items if item.itemset == 462]) == 2) 245 | 246 | return item_hit + set_hit 247 | 248 | @property 249 | def intellect(self): 250 | race_int = { 251 | "human": 125, 252 | "gnome": 139, 253 | "undead": 123, 254 | "troll": 121} 255 | if self._race in race_int: 256 | base_int = race_int[self._race] 257 | else: 258 | base_int = 0 259 | item_int = sum([item.intellect for item in self._items]) 260 | 261 | return base_int + item_int 262 | 263 | @property 264 | def race(self): 265 | return self._race 266 | 267 | @property 268 | def is_udc(self): 269 | return sum([1 for item in self._items if item.itemset == 536]) == 3 270 | 271 | @property 272 | def act(self): 273 | return self._act 274 | 275 | @property 276 | def atiesh(self): 277 | return self._atiesh 278 | 279 | @property 280 | def name(self): 281 | return self._name 282 | 283 | @property 284 | def is_new(self): 285 | return any([item.is_new for item in self._items]) 286 | -------------------------------------------------------------------------------- /src/sim/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from copy import deepcopy 4 | 5 | class ConfigList(): 6 | 7 | def __init__(self, directory, filename): 8 | self._configs = [Config(directory, filename)] 9 | self._directory = directory 10 | self._index = 0 11 | 12 | def configs(self): 13 | return self._configs 14 | 15 | def current(self): 16 | return self._configs[self._index] 17 | 18 | def copy(self, filename): 19 | new_config = Config.from_config(self._directory, filename, self._configs[-1].config()) 20 | self._configs.append(new_config) 21 | return new_config 22 | 23 | def pop(self): 24 | self._configs = self._configs[:-1] 25 | 26 | def set_index(self, index): 27 | self._index = index 28 | 29 | def index(self): 30 | return self._index 31 | 32 | class Config(object): 33 | 34 | def __init__(self, directory, filename): 35 | self._directory = directory 36 | self.load(filename) 37 | 38 | @classmethod 39 | def from_config(cls, directory, filename, config_to_copy): 40 | obj = cls.__new__(cls) 41 | super(Config, obj).__init__() 42 | obj._directory = directory 43 | obj._filename = obj._add_ext(filename) 44 | obj._config = deepcopy(config_to_copy) 45 | 46 | return obj 47 | 48 | def filename(self): 49 | return self._filename 50 | 51 | def save(self, filename): 52 | self._filename = self._add_ext(filename) 53 | with open(os.path.join(self._directory, self._filename), "wt") as fid: 54 | json.dump(self._config, fid, indent=3) 55 | 56 | def load(self, filename): 57 | self._filename = self._add_ext(filename) 58 | with open(os.path.join(self._directory, self._filename), "rt") as fid: 59 | self._config = json.load(fid) 60 | self._fill_back_compatibility() 61 | 62 | def _add_ext(self, filename): 63 | base, ext = os.path.splitext(filename) 64 | if ext == ".json": 65 | return base + ext 66 | else: 67 | return filename + ".json" 68 | 69 | def config(self): 70 | return self._config 71 | 72 | def _fill_back_compatibility(self): 73 | if "auras" not in self._config["buffs"]: 74 | self._config["buffs"]["auras"] = { 75 | "mage_atiesh": [0 for dummy in range(self._config["configuration"]["num_mages"])], 76 | "lock_atiesh": [0 for dummy in range(self._config["configuration"]["num_mages"])], 77 | "boomkin": [0 for dummy in range(self._config["configuration"]["num_mages"])] 78 | } 79 | -------------------------------------------------------------------------------- /src/sim/constants.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | _LOG_SIM = -1 # set to -1 for no log 4 | 5 | class Constant(): 6 | 7 | def __init__(self, double_dip, sim_size=1000, log_sim=_LOG_SIM): 8 | 9 | self._FIREBALL_RANK = 12 10 | self._FROSTBOLT_TALENTED = False 11 | self._FROSTBOLT_RANK = 11 12 | self._INCINERATE = True 13 | self._SIMPLE_SPELL = False 14 | 15 | self._ROTATION_SIMSIZE = 3000 16 | self._CRIT_SIMSIZE = 50000 17 | self._HIT_SIMSIZE = 100000 18 | 19 | ## adapt variables 20 | self._LOW_MAGE_EXTRA_SCORCH = True 21 | 22 | ## scenario variables 23 | self._NUM_MAGES = 6 24 | self._HIT_CHANCE = 0.99 25 | self._DAMAGE = 700 26 | 27 | self._SP_START = 300 28 | self._SP_END = 900 29 | self._SP_STEP = 50 30 | 31 | self._HIT_START = 0.89 32 | self._HIT_END = 0.99 33 | self._HIT_STEP = 0.02 34 | 35 | self._CRIT_START = 0.10 36 | self._CRIT_END = 0.70 37 | self._CRIT_STEP = 0.05 38 | 39 | self._MAGES_START = 1 40 | self._MAGES_END = 9 41 | 42 | self._SIM_SIZE = sim_size 43 | 44 | self._DURATION_AVERAGE = 45.0 45 | self._DURATION_SIGMA = 6.0 46 | 47 | ## strategy/performance variables 48 | # maximum seconds before scorch expires for designated mage to start casting scorch 49 | self._MAX_SCORCH_REMAIN = 5.0 50 | 51 | # cast initial/response time variation 52 | self._INITIAL_SIGMA = 1.0 53 | self._CONTINUING_SIGMA = 0.05 54 | 55 | ## constants, do not change 56 | self._GLOBAL_COOLDOWN = 1.5 57 | 58 | self._IGNITE_TIME = 4.0 59 | self._IGNITE_TICK = 2.0 60 | self._IGNITE_STACK = 5 61 | 62 | self._SCORCH_TIME = 30.0 63 | self._SCORCH_STACK = 5 64 | 65 | self._COE_MULTIPLIER = 1.1 66 | self._SCORCH_MULTIPLIER = 0.03 67 | 68 | self._FIRE_BLAST_COOLDOWN = 7.0 # two talent points 69 | 70 | self._CAST_SCORCH = 0 71 | self._CAST_PYROBLAST = 1 72 | self._CAST_FIREBALL = 2 73 | self._CAST_FIRE_BLAST = 3 74 | self._CAST_FROSTBOLT = 4 75 | self._CAST_GCD = 5 # placeholder for extra time due to GCD 76 | # below this line are instant/external source casts 77 | self._CAST_COMBUSTION = 6 78 | self._CAST_SAPP = 7 79 | self._CAST_TOEP = 8 80 | self._CAST_ZHC = 9 81 | self._CAST_MQG = 10 82 | self._CAST_POWER_INFUSION = 11 83 | self._CASTS = 12 84 | 85 | self._SP_MULTIPLIER = np.array([0.428571429, 1.0, 1.0, 0.428571429, 0.814285714]) 86 | self._DAMAGE_MULTIPLIER = np.array([1.1, 1.1, 1.1, 1.1, 1.0]) # fire power 87 | if self._SIMPLE_SPELL: 88 | self._SPELL_BASE = np.array([250, 900, 750, 500, 500]) 89 | self._SPELL_RANGE = np.array([0, 0, 0, 0, 0]) 90 | else: 91 | self._SPELL_BASE = np.array([237, 716, 596, 446, 515]) 92 | self._SPELL_RANGE = np.array([43, 174, 164, 78, 40]) 93 | self._IS_SCORCH = np.array([True, False, False, False, False]) 94 | self._IS_FIRE = np.array([1.0, 1.0, 1.0, 1.0, 0.0]) 95 | self._INCIN_BONUS = np.array([0.0, 0.0, 0.0, 0.0, 0.0]) 96 | self._CAST_TIME = np.array([1.5, 6.0, 3.0, 0.0, 3.0, self._GLOBAL_COOLDOWN]) 97 | self._SPELL_TIME = np.array([0.0, 0.875, 0.875, 0.0, 0.75]) 98 | 99 | if self._FIREBALL_RANK == 11: 100 | self._SPELL_BASE[self._CAST_FIREBALL] = 561.0 101 | self._SPELL_RANGE[self._CAST_FIREBALL] = 154.0 102 | 103 | if self._FROSTBOLT_RANK == 10: 104 | self._SPELL_BASE[self._CAST_FROSTBOLT] = 440.0 105 | self._SPELL_RANGE[self._CAST_FROSTBOLT] = 75.0 106 | elif self._FROSTBOLT_RANK == 1: 107 | self._SPELL_BASE[self._CAST_FROSTBOLT] = 20.0 108 | self._SPELL_RANGE[self._CAST_FROSTBOLT] = 2.0 109 | self._CAST_TIME[self._CAST_FROSTBOLT] = 1.5 110 | self._SP_MULTIPLIER[self._CAST_FROSTBOLT] = 0.407142857 111 | 112 | if self._FROSTBOLT_TALENTED: 113 | self._DAMAGE_MULTIPLIER[self._CAST_FROSTBOLT] = 1.06 114 | self._CAST_TIME[self._CAST_FROSTBOLT] = 2.5 115 | self._FROSTBOLT_CRIT_DAMAGE = 1.0 116 | self._FROSTBOLT_OVERALL = 1.1*1.06 117 | 118 | if self._INCINERATE: 119 | self._INCIN_BONUS = np.array([0.04, 0.0, 0.0, 0.04, 0.0]) 120 | 121 | # BUFFs have simple mechanics: limited use and a timer 122 | self._POWER_INFUSION = 0.2 123 | self._MQG = 0.33 124 | 125 | self._BUFF_SAPP = 0 126 | self._BUFF_TOEP = 1 127 | self._BUFF_ZHC = 2 128 | self._BUFF_MQG = 3 129 | self._BUFF_POWER_INFUSION = 4 130 | self._BUFFS = 5 131 | self._DAMAGE_BUFFS = 3 132 | 133 | self._BUFF_DURATION = np.array([20.0, 15.0, 20.0, 20.0, 15.0]) 134 | self._BUFF_CAST_TYPE = np.array([self._CAST_SAPP, self._CAST_TOEP, self._CAST_ZHC, self._CAST_MQG, self._CAST_POWER_INFUSION]) 135 | self._BUFF_COOLDOWN = np.array([120.0, 90.0, 120.0, 300.0, 180.0]) 136 | self._BUFF_DAMAGE = np.array([130.0, 175.0, 204.0]) 137 | self._BUFF_PER_TICK = np.array([0.0, 0.0, -17.0]) 138 | 139 | self._DEBUFFS = 0 140 | 141 | self._NORMAL_BUFF = double_dip 142 | self._CRIT_BUFF = 1.0 143 | self._IGNITE_BUFF = double_dip 144 | 145 | self._NORMAL_BUFF_C = double_dip*1.02 146 | self._CRIT_BUFF_C = 1.02 147 | self._IGNITE_BUFF_C = double_dip*1.02 148 | 149 | self._IGNITE_DAMAGE = 0.2 150 | self._ICRIT_DAMAGE = 0.5 151 | self._CRIT_DAMAGE = 1.0 if self._FROSTBOLT_TALENTED else 0.5 152 | 153 | self._COMBUSTIONS = 3 154 | self._PER_COMBUSTION = 0.1 155 | self._COMBUSTION_COOLDOWN = 180.0 156 | 157 | self._LONG_TIME = 999*self._DURATION_AVERAGE 158 | 159 | ## decision making 160 | self._SCORCHES = np.array([9000, 6, 3, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1]) 161 | self._DECIDE = { 162 | "scorch": self._CAST_SCORCH, 163 | "pyroblast": self._CAST_PYROBLAST, 164 | "fireball": self._CAST_FIREBALL, 165 | "fire_blast": self._CAST_FIRE_BLAST, 166 | "frostbolt": self._CAST_FROSTBOLT, 167 | "gcd": self._CAST_GCD, 168 | "combustion": self._CAST_COMBUSTION, 169 | "sapp": self._CAST_SAPP, 170 | "toep": self._CAST_TOEP, 171 | "zhc": self._CAST_ZHC, 172 | "mqg": self._CAST_MQG, 173 | "pi": self._CAST_POWER_INFUSION} 174 | self._BUFF_LOOKUP = { 175 | "sapp": self._BUFF_SAPP, 176 | "toep": self._BUFF_TOEP, 177 | "zhc": self._BUFF_ZHC, 178 | "mqg": self._BUFF_MQG, 179 | "pi": self._BUFF_POWER_INFUSION} 180 | 181 | ## debugging 182 | self._LOG_SPELL = ['scorch ', 'pyroblast ', 'fireball ', 'fire blast', 'frostbolt ', 'gcd ', 'combustion', 'sapp ', 'toep ', 'zhc ', 'mqg ', 'power inf '] 183 | self.log_sim = log_sim 184 | 185 | self._RES_AMOUNT = [1.0, 0.75, 0.5, 0.25] 186 | self._RES_THRESH = [0.0, 0.8303, 0.9415, 0.9905] 187 | self._RES_THRESH_UL = [0.8303, 0.9415, 0.9905, 1.0] 188 | #self._RESISTANCE_MODIFIER = 0.966975 189 | self._RESISTANCE_MODIFIER = 0.940997 190 | 191 | self._DECISION_POINT = 2.0 192 | 193 | self._DRAGONLING_DURATION = 60.0 194 | self._DRAGONLING_BUFF = 300 195 | 196 | self._NIGHTFALL_PROB = 0.15 197 | self._NIGHTFALL_BUFF = 0.15 198 | self._NIGHTFALL_DURATION = 5.0 199 | 200 | class ArrayGenerator(): 201 | 202 | def __init__(self, params): 203 | self._params = params 204 | 205 | @staticmethod 206 | def _distribution(entry, size): 207 | array = None 208 | if isinstance(entry, float): 209 | array = entry*np.ones(size) 210 | elif 'mean' in entry: 211 | array = entry['mean']*np.ones(size) 212 | if 'var' in entry: 213 | array += entry['var']*np.random.randn(size) 214 | if 'clip' in entry: 215 | array = np.maximum(entry['clip'][0], array) 216 | array = np.minimum(entry['clip'][1], array) 217 | elif 'value' in entry: 218 | array = np.tile(entry['fixed'][None, :], (size[0], 1)) 219 | 220 | return array 221 | 222 | def run(self, C, dur_dist): 223 | sim_size = self._params['sim_size'] 224 | num_mages = self._params['configuration']['num_mages'] 225 | arrays = { 226 | 'global': { 227 | 'total_damage': [[] for dummy in range(sim_size)] if dur_dist else np.zeros(sim_size), 228 | 'running_time': np.zeros(sim_size), 229 | 'decision': np.zeros(sim_size).astype(bool), 230 | 'ignite': [[] for dummy in range(sim_size)] if dur_dist else np.zeros(sim_size), 231 | 'crit': np.zeros(sim_size), 232 | 'player': np.zeros(sim_size)}, 233 | 'boss': { 234 | 'ignite_timer': np.zeros(sim_size), 235 | 'ignite_count': np.zeros(sim_size).astype(np.int32), 236 | 'ignite_value': np.zeros(sim_size), 237 | 'ignite_multiplier': np.ones(sim_size), 238 | 'tick_timer': C._LONG_TIME*np.ones(sim_size), 239 | 'scorch_timer': np.zeros(sim_size), 240 | 'scorch_count': np.zeros(sim_size).astype(np.int32), 241 | 'debuff_timer': [np.zeros(sim_size) for aa in range(C._DEBUFFS)], 242 | 'debuff_avail': [np.zeros(sim_size).astype(np.int32) for aa in range(C._DEBUFFS)], 243 | 'dragonling': -C._DRAGONLING_DURATION, 244 | 'spell_vulnerability': np.zeros((sim_size))}, 245 | 'player': { 246 | 'cast_type': C._CAST_GCD*np.ones((sim_size, num_mages)).astype(np.int32), 247 | 'spell_timer': C._LONG_TIME*np.ones((sim_size, num_mages)), 248 | 'spell_type': C._CAST_GCD*np.ones((sim_size, num_mages)).astype(np.int32), 249 | 'comb_stack': np.zeros((sim_size, num_mages)).astype(np.int32), 250 | 'comb_left': np.zeros((sim_size, num_mages)).astype(np.int32), 251 | 'comb_avail': np.ones((sim_size, num_mages)).astype(np.int32), 252 | 'comb_cooldown': np.inf*np.ones((sim_size, num_mages)).astype(np.int32), 253 | 'cast_number': -np.ones((sim_size, num_mages)).astype(np.int32), 254 | 'buff_timer': [np.zeros((sim_size, num_mages)) for aa in range(C._BUFFS)], 255 | 'buff_cooldown': [np.inf*np.ones((sim_size, num_mages)) for aa in range(C._BUFFS)], 256 | 'buff_ticks': [np.zeros((sim_size, num_mages)).astype(np.int32) for aa in range(C._DAMAGE_BUFFS)], 257 | 'fb_cooldown': np.zeros((sim_size, num_mages)), 258 | 'crit_too_late': np.zeros((sim_size, num_mages)).astype(bool), 259 | 'nightfall': np.inf*np.ones((sim_size)).reshape(sim_size, 1), 260 | 'gcd': np.zeros((sim_size, num_mages)) 261 | } 262 | } 263 | 264 | arrays['global']['duration'] = self._distribution(self._params['timing']['duration'], sim_size) 265 | arrays['player']['cast_timer'] = np.abs(self._params['timing']['delay']*np.random.randn(sim_size, num_mages)) 266 | 267 | arrays["player"]["spell_power"] = np.tile(np.array(self._params["stats"]["spell_power"])[None, :], (sim_size, 1)) 268 | arrays["player"]["spell_power"] += 35*("greater_arcane_elixir" in self._params["buffs"]["consumes"]) 269 | arrays["player"]["spell_power"] += 40*("elixir_of_greater_firepower" in self._params["buffs"]["consumes"]) 270 | arrays["player"]["spell_power"] += 150*("flask_of_supreme_power" in self._params["buffs"]["consumes"]) 271 | arrays["player"]["spell_power"] += 60*("blessed_wizard_oil" in self._params["buffs"]["consumes"]) 272 | arrays["player"]["spell_power"] += 36*("brilliant_wizard_oil" in self._params["buffs"]["consumes"]) 273 | arrays["player"]["spell_power"] += 23*("very_berry_cream" in self._params["buffs"]["consumes"]) 274 | arrays["player"]["spell_power"] += 33*np.array(self._params["buffs"]["auras"]["lock_atiesh"]) 275 | 276 | intellect = np.tile(np.array(self._params["stats"]["intellect"], dtype=np.float32)[None, :], (sim_size, 1)) 277 | intellect += 31.0*float("arcane_intellect" in self._params["buffs"]["raid"]) 278 | intellect += 1.35*12.0*float("improved_mark" in self._params["buffs"]["raid"]) 279 | intellect += 30.0*float("stormwind_gift_of_friendship" in self._params["buffs"]["consumes"]) 280 | intellect += 25.0*float("infallible_mind" in self._params["buffs"]["consumes"]) 281 | intellect += 10.0*float("runn_tum_tuber_surprise" in self._params["buffs"]["consumes"]) 282 | 283 | intellect *= (1 + 0.1*float("blessing_of_kings" in self._params["buffs"]["raid"]))*(1 + 0.15*float("spirit_of_zandalar" in self._params["buffs"]["world"])) 284 | racial = np.array([1.05 if rac == "gnome" else 1.0 for rac in self._params["buffs"]["racial"]]) 285 | intellect *= racial 286 | 287 | # 0.062 = 6% talents + 0.2% base 288 | arrays["player"]["crit_chance"] = 0.062 + np.tile(np.array(self._params["stats"]["crit_chance"])[None, :], (sim_size, 1)) 289 | arrays["player"]["crit_chance"] += 0.01*float("brilliant_wizard_oil" in self._params["buffs"]["consumes"]) 290 | arrays["player"]["crit_chance"] += 0.1*float("rallying_cry_of_the_dragonslayer" in self._params["buffs"]["world"]) 291 | arrays["player"]["crit_chance"] += 0.03*float("moonkin_aura" in self._params["buffs"]["raid"]) 292 | arrays["player"]["crit_chance"] += 0.05*float("songflower_serenade" in self._params["buffs"]["world"]) 293 | arrays["player"]["crit_chance"] += 0.03*float("dire_maul_tribute" in self._params["buffs"]["world"]) 294 | arrays["player"]["crit_chance"] += intellect/5950 295 | arrays["player"]["crit_chance"] += 0.60*float(self._params["buffs"]["boss"] == "loatheb") 296 | arrays["player"]["crit_chance"] += 0.02*np.array(self._params["buffs"]["auras"]["mage_atiesh"]) 297 | arrays["player"]["crit_chance"] += 0.03*np.array(self._params["buffs"]["auras"]["boomkin"]) 298 | arrays["player"]["crit_chance"] = np.minimum(1.00, arrays["player"]["crit_chance"]) 299 | 300 | arrays["player"]["hit_chance"] = 0.89 + np.tile(np.array(self._params["stats"]["hit_chance"])[None, :], (sim_size, 1)) 301 | arrays["player"]["hit_chance"] = np.minimum(0.99, arrays["player"]["hit_chance"]) 302 | 303 | if C.log_sim >= 0: 304 | print("Effective State Values:") 305 | for index in range(self._params["configuration"]["num_mages"]): 306 | print(f' Mage {index + 1:d}: Spell Power = {arrays["player"]["spell_power"][0][index]:.0f} Crit Chance = {100*arrays["player"]["crit_chance"][0][index]:.2f} Int = {intellect[0][index]:.1f}') 307 | 308 | for key, val in C._BUFF_LOOKUP.items(): 309 | for index in self._params["configuration"][key]: 310 | arrays["player"]["buff_cooldown"][val][:, index] = 0.0 311 | 312 | arrays["player"]["cleaner"] = np.array(self._params["configuration"]["udc"]).reshape(1, len(self._params["configuration"]["udc"])) 313 | arrays["player"]["pi"] = np.array(self._params["configuration"]["pi"]).reshape(1, len(self._params["configuration"]["pi"])) 314 | arrays["player"]["target"] = np.array(self._params["configuration"]["target"]).reshape(1, len(self._params["configuration"]["target"])) 315 | 316 | if "proc" in self._params["buffs"]: 317 | if "dragonling" in self._params["buffs"]["proc"]: 318 | arrays["boss"]["dragonling"] = self._params["buffs"]["proc"]["dragonling"] 319 | if "nightfall" in self._params["buffs"]["proc"]: 320 | nightfall = [] 321 | for period in self._params["buffs"]["proc"]["nightfall"]: 322 | nightfall.append(period*np.ones((sim_size))) 323 | arrays["player"]["nightfall"] = np.stack(nightfall, axis=1) 324 | arrays["player"]["nightfall_period"] = np.array(self._params["buffs"]["proc"]["nightfall"]) 325 | 326 | return arrays 327 | 328 | def log_message(): 329 | print('LOG:') 330 | print(' KEY:') 331 | print(' ic = ignite stack size') 332 | print(' it = ignite time remaining') 333 | print(' in = time to next ignite tick') 334 | print(' ic = ignite damage per tick') 335 | print(' sc = scorch stack size') 336 | print(' st = scorch time remaining') 337 | print(' cs = combustion stack size (ignore if cl is 0)') 338 | print(' cl = combustion remaining crits') 339 | 340 | -------------------------------------------------------------------------------- /src/sim/old_scripts/ce_damage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from copy import deepcopy 4 | from mechanics import get_damage 5 | import matplotlib as mpl 6 | mpl.use('Agg') 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | 10 | VERSION = 3 11 | 12 | def main(config, name, ret_dist=False, sim_size=250): 13 | config["sim_size"] = sim_size 14 | values = get_damage(config, ret_dist=ret_dist) 15 | print(f" {name}={values[0]:6.1f}") 16 | return values 17 | 18 | def load_config(cfn): 19 | config_file = os.path.join("./items/", cfn) 20 | with open(config_file, 'rt') as fid: 21 | config = json.load(fid) 22 | 23 | return config 24 | 25 | def time_comparison(): 26 | max_mages = 7 27 | etimes = np.array(list(range(25, 90, 5)) + list(range(90, 271, 15)) + list(range(300, 601, 30))) 28 | 29 | out = [[] for dummy in range(max_mages)] 30 | 31 | sim_sizes = [0, 1000, 707, 577, 500, 447, 408, 378] 32 | 33 | for num_mages in range(1, max_mages + 1): 34 | config = load_config(f"mages_{num_mages:d}.json") 35 | config["configuration"]["target"] = list(range(num_mages)) 36 | config["timing"]["delay"] = 3.0 37 | osim_size = 50.0*sim_sizes[num_mages] 38 | 39 | config["stats"]["spell_power"] = [745 for aa in range(num_mages)] 40 | config["stats"]["crit_chance"] = [0.15 for aa in range(num_mages)] 41 | config["stats"]["intellect"] = [302 for aa in range(num_mages)] 42 | config["configuration"]["sapp"] = list(range(num_mages)) 43 | config["buffs"]["raid"].append("blessing_of_kings") 44 | config["buffs"]["consumes"] = { 45 | "greater_arcane_elixir", 46 | "elixir_of_greater_firepower", 47 | "flask_of_supreme_power", 48 | "blessed_wizard_oil"} 49 | config["buffs"]["world"] = { 50 | "rallying_cry_of_the_dragonslayer", 51 | "spirit_of_zandalar", 52 | "dire_maul_tribute", 53 | "songflower_serenade", 54 | "sayges_dark_fortune_of_damage"} 55 | config["configuration"]["mqg"] = list(range(num_mages)) 56 | config["configuration"]["pi"] = list(range(num_mages)) 57 | 58 | 59 | 60 | #config['timing']['duration'] = { 61 | # "mean": 300.0, 62 | # "var": 3.0, 63 | # "clip": [0.0, 10000.0]} 64 | #sim_size = int(osim_size*10/120**0.5) 65 | #main(config, f"{num_mages:d}", sim_size=sim_size)[0] 66 | #dksok() 67 | for etime in etimes: 68 | sim_size = int(osim_size*10/etime**0.5) 69 | tconfig = deepcopy(config) 70 | tconfig['timing']['duration'] = { 71 | "mean": etime, 72 | "var": 3.0, 73 | "clip": [0.0, 10000.0]} 74 | value_0 = main(tconfig, f"{num_mages:d} {etime:3.0f} fb ", sim_size=sim_size)[0] 75 | tconfig["rotation"]["continuing"]["special"] = {"slot": list(range(num_mages)), "value": "scorch"} 76 | value_1 = main(tconfig, f"{num_mages:d} {etime:3.0f} sc ", sim_size=sim_size)[0] 77 | out[num_mages - 1].append((value_1 - value_0)/num_mages) 78 | 79 | plt.close('all') 80 | plt.figure(figsize=(8.0, 5.5), dpi=200) 81 | plt.title(f"SoM Damage (Shorter Encounters, WBs, Phase 6 Gear)") 82 | for num_mages, ou in enumerate(out): 83 | #plt.plot(etimes, np.array(ou), label=config["configuration"]["name"][index]) 84 | label = f"{num_mages + 1:d} mages" 85 | if num_mages < 1: 86 | label = label[:-1] 87 | plt.plot(etimes, 88 | np.array(ou), 89 | label=label) 90 | plt.xlabel('Encounter Duration (seconds)') 91 | plt.ylabel('DPS Difference per Mage (Ignite Hold - Always Fireball) Rotations') 92 | plt.grid() 93 | plt.xlim(0, 600) 94 | plt.legend() 95 | plt.savefig("ce_rotation_phase6.png") 96 | 97 | if __name__ == '__main__': 98 | time_comparison() 99 | -------------------------------------------------------------------------------- /src/sim/old_scripts/ce_equiv.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from copy import deepcopy 4 | from mechanics import get_damage 5 | import matplotlib as mpl 6 | mpl.use('Agg') 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | 10 | VERSION = 3 11 | 12 | def main(config, name, ret_dist=False, sim_size=250): 13 | config["sim_size"] = sim_size 14 | values = get_damage(config, ret_dist=ret_dist) 15 | print(f" {name}={values[0]:6.1f}") 16 | return values 17 | 18 | def load_config(cfn): 19 | config_file = os.path.join("./items/", cfn) 20 | with open(config_file, 'rt') as fid: 21 | config = json.load(fid) 22 | 23 | return config 24 | 25 | def time_comparison(): 26 | max_mages = 7 27 | etimes = np.array(list(range(25, 90, 5)) + list(range(90, 241, 15)) + list(range(300, 601, 30))) 28 | 29 | out = [[] for dummy in range(max_mages)] 30 | 31 | sim_sizes = [0, 1000, 707, 577, 500, 447, 408, 378] 32 | 33 | for num_mages in range(1, max_mages + 1): 34 | config = load_config(f"mages_{num_mages:d}.json") 35 | config["timing"]["delay"] = 3.0 36 | config["rotation"]["continuing"]["special"] = {"slot": list(range(num_mages)), "value": "scorch"} 37 | config["stats"]["spell_power"] = [745 for aa in range(num_mages)] 38 | config["stats"]["crit_chance"] = [0.15 for aa in range(num_mages)] 39 | config["stats"]["intellect"] = [302 for aa in range(num_mages)] 40 | config["buffs"]["raid"].append("blessing_of_kings") 41 | config["buffs"]["consumes"] = { 42 | "greater_arcane_elixir", 43 | "elixir_of_greater_firepower", 44 | "flask_of_supreme_power", 45 | "blessed_wizard_oil"} 46 | config["buffs"]["world"] = { 47 | "rallying_cry_of_the_dragonslayer", 48 | "spirit_of_zandalar", 49 | "dire_maul_tribute", 50 | "songflower_serenade", 51 | "sayges_dark_fortune_of_damage"} 52 | config["configuration"]["mqg"] = list(range(num_mages)) 53 | config["configuration"]["pi"] = list(range(num_mages)) 54 | osim_size = 150.0*sim_sizes[num_mages] 55 | 56 | for etime in etimes: 57 | sim_size = int(osim_size*10/etime**0.5) 58 | tconfig = deepcopy(config) 59 | tconfig['timing']['duration'] = { 60 | "mean": etime, 61 | "var": 3.0, 62 | "clip": [0.0, 10000.0]} 63 | value_0 = main(tconfig, f"{num_mages:d} {etime:3.0f} 0", sim_size=sim_size)[0] 64 | tconfig["stats"]["spell_power"][-1] += 30 65 | value_sp = main(tconfig, f"{num_mages:d} {etime:3.0f} sp", sim_size=sim_size)[0] 66 | tconfig["stats"]["spell_power"][-1] -= 30 67 | tconfig["stats"]["crit_chance"][-1] += 0.03 68 | value_cr = main(tconfig, f"{num_mages:d} {etime:3.0f} cr", sim_size=sim_size)[0] 69 | out[num_mages - 1].append(10.0*(value_cr - value_0)/(value_sp - value_0)) 70 | 71 | plt.close('all') 72 | plt.figure(figsize=(8.0, 5.5), dpi=200) 73 | plt.title(f"CE Crit Values (Shorter Encounters, WBs, Always Fireball, Phase 6 Gear)") 74 | for num_mages, ou in enumerate(out): 75 | #plt.plot(etimes, np.array(ou), label=config["configuration"]["name"][index]) 76 | label = f"{num_mages + 1:d} mages" 77 | if num_mages < 1: 78 | label = label[:-1] 79 | plt.plot(etimes, 80 | np.array(ou), 81 | label=label) 82 | plt.xlabel('Encounter Duration (seconds)') 83 | plt.ylabel('SP per 1% Crit') 84 | plt.grid() 85 | plt.xlim(0, 600) 86 | plt.legend() 87 | plt.savefig("ce_crit_phase6_scorch.png") 88 | 89 | if __name__ == '__main__': 90 | time_comparison() 91 | -------------------------------------------------------------------------------- /src/sim/old_scripts/ce_equiv2_fast.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from copy import deepcopy 4 | from mechanics import get_damage 5 | import matplotlib as mpl 6 | mpl.use('Agg') 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | import pickle 10 | 11 | VERSION = 3 12 | _TYPE = 0 13 | 14 | def main(config, name, ret_dist=False, sim_size=250, dur_dist=None): 15 | config["sim_size"] = sim_size 16 | values = get_damage(config, ret_dist=ret_dist, dur_dist=dur_dist) 17 | print(f" {name}={values[0]:6.1f}") 18 | return values 19 | 20 | def load_config(cfn): 21 | config_file = os.path.join("../config/", cfn) 22 | with open(config_file, 'rt') as fid: 23 | config = json.load(fid) 24 | 25 | return config 26 | 27 | def time_comparison(): 28 | max_mages = 7 29 | do_ep = True 30 | 31 | etimes = np.array(list(range(25, 90, 5)) + list(range(90, 181, 15))) 32 | #etimes = np.array([60, 90, 120]) 33 | print(etimes) 34 | 35 | if do_ep: 36 | out = [np.array([0.0]) for dummy in range(max_mages)] 37 | out2 = [np.array([0.0]) for dummy in range(max_mages)] 38 | 39 | sim_sizes = [0, 1000, 707, 577, 500, 447, 408, 378] 40 | scorches = [0, 6, 3, 2, 2, 2, 1, 1] 41 | 42 | for num_mages in range(3, max_mages + 1): 43 | encounter = "template_buffer" 44 | config = load_config(encounter + ".json") 45 | config["timing"]["delay"] = 2.0 46 | config["rotation"]["initial"]["common"] = ["scorch"]*scorches[num_mages] 47 | 48 | #if num_mages > 4: 49 | # config["stats"]["spell_power"] = [785 for aa in range(num_mages)] 50 | #else: 51 | # config["stats"]["spell_power"] = [745 for aa in range(num_mages)] 52 | config["stats"]["spell_power"] = [745 for aa in range(num_mages)] 53 | 54 | config["stats"]["crit_chance"] = [0.15 for aa in range(num_mages)] 55 | config["stats"]["hit_chance"] = [0.1 for aa in range(num_mages)] 56 | config["stats"]["intellect"] = [302 for aa in range(num_mages)] 57 | config["buffs"]["raid"] = [ 58 | "arcane_intellect", 59 | "improved_mark", 60 | "blessing_of_kings"] 61 | config["buffs"]["consumes"] = [ 62 | "greater_arcane_elixir", 63 | "elixir_of_greater_firepower", 64 | "flask_of_supreme_power", 65 | "blessed_wizard_oil"] 66 | config["buffs"]["world"] = [ 67 | "rallying_cry_of_the_dragonslayer", 68 | "spirit_of_zandalar", 69 | "dire_maul_tribute", 70 | "songflower_serenade", 71 | "sayges_dark_fortune_of_damage"] 72 | #config["buffs"]["world"] = [] 73 | config["buffs"]["racial"] = ["human"]*num_mages 74 | config["configuration"]["num_mages"] = num_mages 75 | config["configuration"]["target"] = list(range(num_mages)) 76 | 77 | #if num_mages > 4: 78 | # config["configuration"]["sapp"] = list(range(num_mages)) 79 | #else: 80 | # config["configuration"]["mqg"] = list(range(num_mages)) 81 | config["configuration"]["mqg"] = list(range(num_mages)) 82 | #config["configuration"]["pi"] = list(range(num_mages)) 83 | 84 | config["configuration"]["name"] = [f"mage{idx + 1:d}" for idx in range(num_mages)] 85 | if do_ep: 86 | osim_size = 1000.0*sim_sizes[num_mages] 87 | else: 88 | osim_size = 30.0*sim_sizes[num_mages] 89 | 90 | var = 3.0 91 | sim_size = int(osim_size*10/min(etimes)**0.5) 92 | config['timing']['duration'] = { 93 | "mean": max(etimes) + 5.0*var, 94 | "var": var, 95 | "clip": [0.0, 10000.0]} 96 | value_0 = main(config, f"{num_mages:d} 0", sim_size=sim_size, dur_dist=etimes) 97 | out2[num_mages - 1] = value_0 98 | if do_ep: 99 | config["stats"]["spell_power"][-1] += 30 100 | value_sp = main(config, f"{num_mages:d} sp", sim_size=sim_size, dur_dist=etimes) 101 | config["stats"]["spell_power"][-1] -= 30 102 | config["stats"]["crit_chance"][-1] += 0.03 103 | value_cr = main(config, f"{num_mages:d} cr", sim_size=sim_size, dur_dist=etimes) 104 | out[num_mages - 1] = 10.0*(value_cr - value_0)/(value_sp - value_0) 105 | with open("savestate.dat", "wb") as fid: 106 | pickle.dump(out2, fid) 107 | if do_ep: 108 | pickle.dump(out, fid) 109 | 110 | if do_ep: 111 | plt.close('all') 112 | plt.figure(figsize=(8.0, 5.5), dpi=200) 113 | #plt.title(f"CE Crit Values (Shorter Encounters, WBs, No PI, Ignite Hold, Phase 6 Gear)") 114 | for num_mages, ou in enumerate(out): 115 | if not(ou.any()): 116 | continue 117 | #plt.plot(etimes, np.array(ou), label=config["configuration"]["name"][index]) 118 | label = f"{num_mages + 1:d} mages" 119 | if num_mages < 1: 120 | label = label[:-1] 121 | plt.plot(etimes, 122 | np.array(ou), 123 | label=label) 124 | plt.xlabel('Encounter Duration (seconds)') 125 | plt.ylabel('SP per 1% Crit') 126 | plt.grid() 127 | plt.xlim(0, 180) 128 | plt.legend() 129 | plt.savefig(f"ce3_crit_{encounter:s}.png") 130 | 131 | plt.close('all') 132 | plt.figure(figsize=(8.0, 5.5), dpi=200) 133 | #plt.title(f"CE Crit Values (Shorter Encounters, WBs, No PI, Ignite Hold, Phase 6 Gear)") 134 | for num_mages, ou in enumerate(out2): 135 | if not(ou.any()): 136 | continue 137 | #plt.plot(etimes, np.array(ou), label=config["configuration"]["name"][index]) 138 | label = f"{num_mages + 1:d} mages" 139 | if num_mages < 1: 140 | label = label[:-1] 141 | plt.plot(etimes, 142 | np.array(ou)/(num_mages + 1), 143 | label=label) 144 | plt.xlabel('Encounter Duration (seconds)') 145 | plt.ylabel('Damage per mage') 146 | plt.grid() 147 | plt.xlim(0, 180) 148 | plt.legend() 149 | plt.savefig(f"ce4_{encounter:s}.png") 150 | 151 | if __name__ == '__main__': 152 | time_comparison() 153 | -------------------------------------------------------------------------------- /src/sim/old_scripts/ce_equiv_2pi.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from copy import deepcopy 4 | from mechanics import get_damage 5 | import matplotlib as mpl 6 | mpl.use('Agg') 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | import pickle 10 | 11 | VERSION = 3 12 | _TYPE = 0 13 | 14 | def main(config, name, ret_dist=False, sim_size=250, dur_dist=None): 15 | config["sim_size"] = sim_size 16 | values = get_damage(config, ret_dist=ret_dist, dur_dist=dur_dist) 17 | print(f" {name}={values[0]:6.1f}") 18 | return values 19 | 20 | def load_config(cfn): 21 | config_file = os.path.join("../config/", cfn) 22 | with open(config_file, 'rt') as fid: 23 | config = json.load(fid) 24 | 25 | return config 26 | 27 | def time_comparison(): 28 | max_mages = 7 29 | do_ep = True 30 | 31 | etimes = np.array(list(range(25, 90, 5)) + list(range(90, 181, 15))) 32 | print(etimes) 33 | 34 | if do_ep: 35 | out_pyro = [np.array([0.0]) for dummy in range(max_mages)] 36 | out_fb = [np.array([0.0]) for dummy in range(max_mages)] 37 | out_scorch = [np.array([0.0]) for dummy in range(max_mages)] 38 | out2 = [np.array([0.0]) for dummy in range(max_mages)] 39 | 40 | sim_sizes = [0, 1000, 707, 577, 500, 447, 408, 378] 41 | scorches = [0, 6, 3, 2, 2, 2, 1, 1] 42 | 43 | for num_mages in range(3, max_mages + 1): 44 | encounter = "template_pi_buffer" 45 | config = load_config(encounter + ".json") 46 | config["timing"]["delay"] = 2.0 47 | config["rotation"]["initial"]["common"] = ["scorch"]*scorches[num_mages] 48 | config["rotation"]["continuing"]["special"] = { 49 | "slot": [num_mages - 1], 50 | "value": "scorch_wep" 51 | } 52 | 53 | config["stats"]["spell_power"] = [785, 785] + [745 for aa in range(2, num_mages)] 54 | config["stats"]["crit_chance"] = [0.15 for aa in range(num_mages)] 55 | config["stats"]["hit_chance"] = [0.1 for aa in range(num_mages)] 56 | config["stats"]["intellect"] = [302 for aa in range(num_mages)] 57 | 58 | config["stats"]["spell_power"][-1] = 748 59 | config["stats"]["crit_chance"][-1] = 0.18 60 | config["stats"]["intellect"][-1] = 335 61 | 62 | config["buffs"]["raid"] = [ 63 | "arcane_intellect", 64 | "improved_mark", 65 | "blessing_of_kings"] 66 | config["buffs"]["consumes"] = [ 67 | "greater_arcane_elixir", 68 | "elixir_of_greater_firepower", 69 | "flask_of_supreme_power", 70 | "blessed_wizard_oil"] 71 | config["buffs"]["world"] = [ 72 | "rallying_cry_of_the_dragonslayer", 73 | "spirit_of_zandalar", 74 | "dire_maul_tribute", 75 | "songflower_serenade", 76 | "sayges_dark_fortune_of_damage"] 77 | config["buffs"]["racial"] = ["human"]*num_mages 78 | config["configuration"]["num_mages"] = num_mages 79 | config["configuration"]["target"] = list(range(num_mages)) 80 | 81 | config["configuration"]["sapp"] = [0, 1, num_mages - 1] 82 | config["configuration"]["mqg"] = list(range(2, num_mages - 1)) 83 | config["configuration"]["pi"] = [0, 1] 84 | 85 | config["configuration"]["name"] = [f"mage{idx + 1:d}" for idx in range(num_mages)] 86 | 87 | if do_ep: 88 | osim_size = 1000.0*sim_sizes[num_mages] 89 | else: 90 | osim_size = 30.0*sim_sizes[num_mages] 91 | 92 | var = 3.0 93 | sim_size = int(osim_size*10/min(etimes)**0.5) 94 | config['timing']['duration'] = { 95 | "mean": max(etimes) + 5.0*var, 96 | "var": var, 97 | "clip": [0.0, 10000.0]} 98 | value_0 = main(config, f" {num_mages:d} 0", sim_size=sim_size, dur_dist=etimes) 99 | out2[num_mages - 1] = value_0 100 | if do_ep: 101 | config["stats"]["spell_power"][0] += 30 102 | value_sp = main(config, f"p{num_mages:d} sp", sim_size=sim_size, dur_dist=etimes) 103 | config["stats"]["spell_power"][0] -= 30 104 | config["stats"]["crit_chance"][0] += 0.03 105 | value_cr = main(config, f"p{num_mages:d} cr", sim_size=sim_size, dur_dist=etimes) 106 | config["stats"]["spell_power"][0] += 30 107 | config["stats"]["crit_chance"][0] -= 0.03 108 | out_pyro[num_mages - 1] = 10.0*(value_cr - value_0)/(value_sp - value_0) 109 | 110 | if num_mages > 3: 111 | config["stats"]["spell_power"][2] += 30 112 | value_sp = main(config, f"f{num_mages:d} sp", sim_size=sim_size, dur_dist=etimes) 113 | config["stats"]["spell_power"][2] -= 30 114 | config["stats"]["crit_chance"][2] += 0.03 115 | value_cr = main(config, f"f{num_mages:d} cr", sim_size=sim_size, dur_dist=etimes) 116 | config["stats"]["spell_power"][2] += 30 117 | config["stats"]["crit_chance"][2] -= 0.03 118 | out_fb[num_mages - 1] = 10.0*(value_cr - value_0)/(value_sp - value_0) 119 | 120 | 121 | config["stats"]["spell_power"][-1] += 30 122 | value_sp = main(config, f"s{num_mages:d} sp", sim_size=sim_size, dur_dist=etimes) 123 | config["stats"]["spell_power"][-1] -= 30 124 | config["stats"]["crit_chance"][-1] += 0.03 125 | value_cr = main(config, f"s{num_mages:d} cr", sim_size=sim_size, dur_dist=etimes) 126 | out_scorch[num_mages - 1] = 10.0*(value_cr - value_0)/(value_sp - value_0) 127 | with open("savestate2.dat", "wb") as fid: 128 | pickle.dump(out2, fid) 129 | if do_ep: 130 | pickle.dump(out_pyro, fid) 131 | pickle.dump(out_fb, fid) 132 | pickle.dump(out_scorch, fid) 133 | 134 | 135 | if do_ep: 136 | for type_st, out in [("pyro", out_pyro), ("fb", out_fb), ("scorch", out_scorch)]: 137 | plt.close('all') 138 | plt.figure(figsize=(8.0, 5.5), dpi=200) 139 | #plt.title(f"CE Crit Values (Shorter Encounters, WBs, No PI, Ignite Hold, Phase 6 Gear)") 140 | for num_mages, ou in enumerate(out): 141 | if type_st == "fb" and num_mages + 1 <= 3: 142 | continue 143 | if not(ou.any()): 144 | continue 145 | #plt.plot(etimes, np.array(ou), label=config["configuration"]["name"][index]) 146 | label = f"{num_mages + 1:d} mages" 147 | if num_mages < 1: 148 | label = label[:-1] 149 | plt.plot(etimes, 150 | np.array(ou), 151 | label=label) 152 | plt.xlabel('Encounter Duration (seconds)') 153 | plt.ylabel('SP per 1% Crit') 154 | plt.grid() 155 | plt.xlim(0, 180) 156 | plt.legend() 157 | plt.savefig(f"ce2_2pi_crit_{type_st:s}_{encounter:s}.png") 158 | 159 | plt.close('all') 160 | plt.figure(figsize=(8.0, 5.5), dpi=200) 161 | #plt.title(f"CE Crit Values (Shorter Encounters, WBs, No PI, Ignite Hold, Phase 6 Gear)") 162 | for num_mages, ou in enumerate(out2): 163 | if not(ou.any()): 164 | continue 165 | #plt.plot(etimes, np.array(ou), label=config["configuration"]["name"][index]) 166 | label = f"{num_mages + 1:d} mages" 167 | if num_mages < 1: 168 | label = label[:-1] 169 | plt.plot(etimes, 170 | np.array(ou)/(num_mages + 1), 171 | label=label) 172 | plt.xlabel('Encounter Duration (seconds)') 173 | plt.ylabel('Damage per mage') 174 | plt.grid() 175 | plt.xlim(0, 180) 176 | plt.legend() 177 | plt.savefig(f"ce2_2pi_{encounter:s}.png") 178 | 179 | if __name__ == '__main__': 180 | time_comparison() 181 | -------------------------------------------------------------------------------- /src/sim/old_scripts/ce_rotation_mark_vs_sapp.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from copy import deepcopy 4 | from mechanics import get_damage 5 | import matplotlib as mpl 6 | mpl.use('Agg') 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | import pickle 10 | 11 | VERSION = 3 12 | _TYPE = 0 13 | 14 | def main(config, name, ret_dist=False, sim_size=250, dur_dist=None): 15 | config["sim_size"] = sim_size 16 | values = get_damage(config, ret_dist=ret_dist, dur_dist=dur_dist) 17 | return values 18 | 19 | def load_config(cfn): 20 | config_file = os.path.join("../config/", cfn) 21 | with open(config_file, 'rt') as fid: 22 | config = json.load(fid) 23 | 24 | return config 25 | 26 | def time_comparison(): 27 | is_break = False 28 | is_zoom = True 29 | num_mages = 3 30 | 31 | if is_zoom: 32 | if num_mages == 4: 33 | if is_break: 34 | rotation_names = ["mqg-mark_fireball_mts", "sapp-mqg_fireball_mts", "presapp-mqg_fireball_mts", "sapp-mark_fireball_mts"] 35 | else: 36 | rotation_names = ["mqg-mark_pyroblast_mts", "sapp-mqg_pyroblast_mts", "presapp-mqg_pyroblast_mts", "sapp-mark_pyroblast_cbi"] 37 | elif num_mages == 3: 38 | if is_break: 39 | rotation_names = ["sapp-mark_fireball_wep-cbi", "sapp-mqg_fireball_cbi", "mqg-mark_fireball_mts", "presapp-mqg_fireball_mts"] 40 | else: 41 | rotation_names = ["sapp-mark_pyroblast_wep-cbi", "sapp-mark_pyroblast_wep-wep-cbi", "presapp-mqg_pyroblast_cbi", "mqg-mark_pyroblast_mts", 42 | "presapp-mqg_pyroblast_mts"] 43 | else: 44 | if num_mages == 4: 45 | rotation_names = ["mqg-mark_fireball_mts", "sapp-mqg_fireball_mts", "presapp-mqg_fireball_mts", "sapp-mark_fireball_mts", 46 | "mqg-mark_pyroblast_mts", "sapp-mqg_pyroblast_mts", "presapp-mqg_pyroblast_mts", "sapp-mark_pyroblast_mts", 47 | "mqg-mark_pyroblast_cbi", "presapp-mqg_pyroblast_cbi", "sapp-mark_pyroblast_cbi"] 48 | elif num_mages == 3: 49 | rotation_names = ["mqg-mark_fireball_wep-wep-cbi", "sapp-mqg_fireball_wep-wep-cbi", "presapp-mqg_fireball_wep-wep-cbi", "sapp-mark_fireball_wep-wep-cbi", 50 | "mqg-mark_pyroblast_wep-wep-cbi", "sapp-mqg_pyroblast_wep-wep-cbi", "presapp-mqg_pyroblast_wep-wep-cbi", "sapp-mark_pyroblast_wep-wep-cbi"] 51 | rotations = len(rotation_names) 52 | 53 | if not is_zoom: 54 | etimes = np.array(list(range(25, 90, 5)) + list(range(90, 181, 15))) 55 | else: 56 | if num_mages == 4: 57 | if is_break: 58 | etimes = np.array(list(range(36, 69, 3))) 59 | else: 60 | etimes = np.array(list(range(35, 80, 5))) 61 | elif num_mages == 3: 62 | if is_break: 63 | etimes = np.array(list(range(35, 85, 5))) 64 | else: 65 | etimes = np.array(list(range(30, 95, 5))) 66 | 67 | out2 = [np.array([0.0]) for dummy in range(rotations)] 68 | 69 | sim_sizes = [0, 1000, 707, 577, 500, 447, 408, 378] 70 | scorches = [0, 6, 3, 2, 2, 2, 1, 1] 71 | 72 | 73 | for rotation, rname in enumerate(rotation_names): 74 | encounter = "template_pi_buffer" 75 | config = load_config(encounter + ".json") 76 | config["timing"]["delay"] = 1.5 77 | 78 | trinket, buffer, scorch = rname.split("_") 79 | 80 | if "presapp" in trinket: 81 | config["rotation"]["initial"]["common"] = ["sapp"] 82 | else: 83 | config["rotation"]["initial"]["common"] = [] 84 | config["rotation"]["initial"]["common"] += ["scorch"]*scorches[num_mages] 85 | if is_break: 86 | config["rotation"]["initial"]["common"] += ["gcd", "gcd"] 87 | 88 | config["stats"]["spell_power"] = np.array([689 for aa in range(num_mages)]) 89 | if "sapp" in trinket: 90 | config["stats"]["spell_power"] += 40 91 | if "mark" in trinket: 92 | config["stats"]["spell_power"] += 85 93 | config["stats"]["spell_power"] = config["stats"]["spell_power"].tolist() 94 | 95 | config["stats"]["crit_chance"] = [0.21 for aa in range(num_mages)] 96 | config["stats"]["hit_chance"] = [0.1 for aa in range(num_mages)] 97 | config["stats"]["intellect"] = [318 for aa in range(num_mages)] 98 | config["buffs"]["raid"] = [ 99 | "arcane_intellect", 100 | "improved_mark", 101 | "blessing_of_kings"] 102 | config["buffs"]["consumes"] = [ 103 | "greater_arcane_elixir", 104 | "elixir_of_greater_firepower", 105 | "flask_of_supreme_power", 106 | "brilliant_wizard_oil", 107 | "very_berry_cream"] 108 | config["buffs"]["world"] = [ 109 | "rallying_cry_of_the_dragonslayer", 110 | "spirit_of_zandalar", 111 | "dire_maul_tribute", 112 | "songflower_serenade", 113 | "sayges_dark_fortune_of_damage"] 114 | #config["buffs"]["world"] = [] 115 | config["buffs"]["racial"] = ["human"]*num_mages 116 | config["configuration"]["num_mages"] = num_mages 117 | if "mqg" in trinket: 118 | config["configuration"]["mqg"] = list(range(num_mages)) 119 | if "sapp" in trinket: 120 | config["configuration"]["sapp"] = list(range(num_mages)) 121 | config["configuration"]["pi"] = list(range(num_mages)) 122 | config["configuration"]["target"] = list(range(num_mages)) 123 | 124 | #if num_mages > 4: 125 | # config["configuration"]["sapp"] = list(range(num_mages)) 126 | #else: 127 | # config["configuration"]["mqg"] = list(range(num_mages)) 128 | #config["configuration"]["mqg"] = list(range(num_mages)) 129 | config["configuration"]["pi"] = list(range(num_mages)) 130 | config["configuration"]["name"] = [f"mage{idx + 1:d}" for idx in range(num_mages)] 131 | 132 | if "pyroblast" in buffer or is_break: 133 | config["rotation"]["initial"]["have_pi"] = [] 134 | else: 135 | config["rotation"]["initial"]["have_pi"] = ["gcd"] 136 | 137 | if trinket.split("-")[0] == "mqg": 138 | config["rotation"]["initial"]["have_pi"] += [] 139 | else: 140 | config["rotation"]["initial"]["have_pi"] += ["sapp"] 141 | 142 | config["rotation"]["initial"]["have_pi"] += ["combustion"] 143 | 144 | if "pyroblast" in buffer: 145 | config["rotation"]["initial"]["have_pi"] += ["pyroblast"] 146 | else: 147 | config["rotation"]["initial"]["have_pi"] += ["fireball"] 148 | config["rotation"]["initial"]["have_pi"] += [ 149 | "pi", 150 | "mqg", 151 | "fireball"] 152 | 153 | config["rotation"]["initial"]["have_pi"] += ["fireball"]*(7 - len(config["rotation"]["initial"]["have_pi"])) 154 | 155 | config["rotation"]["continuing"] = {"default": "fireball"} 156 | 157 | mapping = { 158 | "mts": "maintain_scorch", 159 | "wep": "scorch_wep", 160 | "cbi": "cobimf"} 161 | for idx, special in enumerate(scorch.split("-")): 162 | for key, values in config["rotation"]["continuing"].items(): 163 | if "value" in values: 164 | if values["value"] == mapping[special]: 165 | config["rotation"]["continuing"][key]["slot"].append(idx) 166 | break 167 | else: 168 | spec_val = len(config["rotation"]["continuing"]) 169 | config["rotation"]["continuing"][f"special{spec_val:d}"] = { 170 | "slot": [idx], 171 | "value": mapping[special]} 172 | if special == "cbi": 173 | config["rotation"]["continuing"][f"special{spec_val:d}"]["cast_point_remain"] = 0.5 174 | 175 | print(f'On {rname:s} {config["stats"]["spell_power"][0]:d}:') 176 | for idx, cline in enumerate(config["rotation"]["initial"]["common"]): 177 | print(f' 0{idx:d} {cline:s}') 178 | for idx, cline in enumerate(config["rotation"]["initial"]["have_pi"]): 179 | print(f' 1{idx:d} {cline:s}') 180 | print(" ", config["rotation"]["continuing"]) 181 | 182 | osim_size = 30.0*sim_sizes[num_mages] 183 | 184 | var = 3.0 185 | sim_size = int(osim_size*10/min(etimes)**0.5) 186 | config['timing']['duration'] = { 187 | "mean": max(etimes) + 5.0*var, 188 | "var": var, 189 | "clip": [0.0, 10000.0]} 190 | #value_0 = main(config, f"{num_mages:d} 0", sim_size=sim_size) 191 | value_0 = main(config, f"{num_mages:d} 0", sim_size=sim_size, dur_dist=etimes) 192 | out2[rotation] = value_0 193 | 194 | plt.close('all') 195 | #plt.figure(figsize=(8.0, 5.5), dpi=200) 196 | plt.figure(figsize=(6.5, 4.0), dpi=200) 197 | #plt.title(f"CE Crit Values (Shorter Encounters, WBs, No PI, Ignite Hold, Phase 6 Gear)") 198 | for rotation, rot_name in enumerate(rotation_names): 199 | #plt.plot(etimes, np.array(ou), label=config["configuration"]["name"][index]) 200 | plt.plot(etimes, np.array(out2[rotation])/num_mages, label=rot_name) 201 | plt.xlabel('Encounter Duration (seconds)') 202 | plt.ylabel('Damage per mage') 203 | plt.title(f"{num_mages:d} mages,{'' if is_break else ' no':s} break") 204 | plt.grid() 205 | if is_zoom: 206 | if num_mages == 4: 207 | if is_break: 208 | plt.xlim(40, 65) 209 | plt.ylim(1620, 1760) 210 | else: 211 | plt.xlim(35, 75) 212 | plt.ylim(1740, 1780) 213 | elif num_mages == 3: 214 | if is_break: 215 | plt.xlim(40, 80) 216 | plt.ylim(1800, 1950) 217 | else: 218 | plt.ylim(1800, 2000) 219 | plt.xlim(30, 90) 220 | else: 221 | plt.xlim(30, 120) 222 | 223 | plt.legend(loc=4) 224 | plt.savefig(f"ce4_{encounter:s}.png") 225 | 226 | if __name__ == '__main__': 227 | time_comparison() 228 | -------------------------------------------------------------------------------- /src/sim/old_scripts/fixed_equiv.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from copy import deepcopy 4 | from mechanics import get_damage 5 | import matplotlib as mpl 6 | mpl.use('Agg') 7 | 8 | VERSION = 3 9 | _TYPE = 0 10 | 11 | def output_info(values, nm): 12 | print(f"mean: {values[0]/nm:.0f} std: {values[1]/nm:.1f} low 2sig: {values[8]/nm:.0f} high 2sig: {values[2]/nm:.0f}") 13 | 14 | def main(config, name, ret_dist=False): 15 | sim_size = 300000 16 | config["sim_size"] = sim_size 17 | values = get_damage(config, ret_dist=ret_dist) 18 | print(f" {name}={values[_TYPE]:7.2f}") 19 | return values 20 | 21 | def load_config(cfn): 22 | config_file = os.path.join("../config/", cfn) 23 | with open(config_file, 'rt') as fid: 24 | config = json.load(fid) 25 | #name = os.path.split(config_file)[-1].split('.')[0] 26 | #print('starting', name) 27 | 28 | return config 29 | 30 | def last_player(): 31 | #encounter = "two_new_rotation" 32 | #encounter = "trem_eye" 33 | #encounter = "six_rnh" 34 | #encounter = "loatheb_udc1" 35 | #encounter = "3_buffer" 36 | #encounter = "two_thaddius" 37 | #encounter = "record_run_fb" 38 | #encounter = "three_thaddius" 39 | #encounter = "2pi_no_UDC" 40 | encounter = "4_mages_dragon_nightfall" 41 | #encounter = "4_mages_dragon_nightfall_max_2std" 42 | 43 | # changes 4 mage pi no UDC 44 | 45 | 46 | config = load_config(encounter + ".json") 47 | to_mod = config["configuration"]["target"] 48 | #to_mod = [1] 49 | #config["timing"]["delay"] = 3.0 50 | config["timing"]["delay"] = 2.0 51 | #config["buffs"]["boss"] = "loatheb" 52 | #config["configuration"]["target"] = [config["configuration"]["num_mages"] - 1] 53 | config = deepcopy(config) 54 | config['timing']['duration'] = { 55 | "mean": 110.0, 56 | "var": 10.0, 57 | "clip": [3.0, 10000.0]} 58 | print(config) 59 | value = main(config, "v0") 60 | if False: 61 | output_info(value, len(config["configuration"]["target"])) 62 | else: 63 | output_info(value, len(config["configuration"]["target"])) 64 | value_0 = value[_TYPE] 65 | for tt in to_mod: 66 | config["stats"]["spell_power"][tt] += 15 67 | value_sp = main(config, "v_sp")[_TYPE] 68 | for tt in to_mod: 69 | config["stats"]["spell_power"][tt] -= 15 70 | config["stats"]["crit_chance"][tt] += 0.015 71 | value_cr = main(config, "v_cr")[_TYPE] 72 | print(10.0*(value_cr - value_0)/(value_sp - value_0)) 73 | 74 | 75 | if __name__ == '__main__': 76 | last_player() 77 | -------------------------------------------------------------------------------- /src/sim/old_scripts/player_fight.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from copy import deepcopy 4 | from mechanics import get_damage 5 | import matplotlib as mpl 6 | mpl.use('Agg') 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | 10 | VERSION = 3 11 | 12 | def main(config, name, ret_dist=False): 13 | sim_size = 10000 14 | config["sim_size"] = sim_size 15 | values = get_damage(config, ret_dist=ret_dist) 16 | print(f" {name}={values[0]:6.1f}") 17 | return values 18 | 19 | def load_config(cfn): 20 | config_file = os.path.join("../config/", cfn) 21 | with open(config_file, 'rt') as fid: 22 | config = json.load(fid) 23 | #name = os.path.split(config_file)[-1].split('.')[0] 24 | #print('starting', name) 25 | 26 | return config 27 | 28 | def time_comparison(): 29 | encounter = "patchwork" 30 | config = load_config(encounter + ".json") 31 | config["timing"]["delay"] = 3.0 32 | 33 | if False: 34 | encounter += "_extra_buffs_2" 35 | ronkuby = config["configuration"]["name"].index("ronkuby") 36 | config["stats"]["spell_power"][ronkuby] += 24 37 | config["stats"]["crit_chance"][ronkuby] -= 0.01 38 | config["stats"]["spell_power"] = [a + 23 for a in config["stats"]["spell_power"]] 39 | if False: 40 | config["buffs"]["world"] = [] 41 | 42 | out = [[] for dummy in range(config["configuration"]["num_mages"])] 43 | err = [[] for dummy in range(config["configuration"]["num_mages"])] 44 | etimes = np.array(list(range(25, 76, 3)) + list(range(80, 251, 10))) 45 | #etimes = np.array([100.0]) 46 | for etime in etimes: 47 | for index in range(config["configuration"]["num_mages"]): 48 | tconfig = deepcopy(config) 49 | tconfig["configuration"]["target"] = [index] 50 | #tconfig['buffs']['world'] = [] 51 | tconfig['timing']['duration'] = { 52 | "mean": etime, 53 | "var": 3.0, 54 | "clip": [0.0, 10000.0]} 55 | value = main(tconfig, config["configuration"]["name"][index]) 56 | out[index].append(value[0]) 57 | err[index].append(value[1]) 58 | 59 | plt.close('all') 60 | plt.figure(figsize=(8.0, 5.5), dpi=200) 61 | plt.title(f"{encounter:s}") 62 | for index, (ou, er) in enumerate(zip(out, err)): 63 | #plt.plot(etimes, np.array(ou), label=config["configuration"]["name"][index]) 64 | plt.errorbar(etimes, 65 | np.array(ou), 66 | yerr=np.array(er), 67 | fmt='o', 68 | capsize=10, 69 | label=config["configuration"]["name"][index]) 70 | plt.xlabel('Encounter Duration (seconds)') 71 | plt.ylabel('Average DPS') 72 | plt.xlim(0, 250) 73 | plt.legend() 74 | plt.savefig(encounter + ".png") 75 | 76 | def time_comparison_no_err(): 77 | encounter = "patchwork" 78 | config = load_config(encounter + ".json") 79 | config["timing"]["delay"] = 3.0 80 | 81 | if False: 82 | encounter += "_extra_buffs_2" 83 | for idx, name in config["configuration"]["name"]: 84 | if "ronkuby" in name: 85 | config["stats"]["spell_power"] += 24 86 | config["stats"]["crit_chance"] -= 0.01 87 | config["stats"]["spell_power"] += 23 88 | if False: 89 | caption = "no world buffs" 90 | config["buffs"]["world"] = [] 91 | else: 92 | caption = "world buffs" 93 | 94 | out = [[] for dummy in range(config["configuration"]["num_mages"])] 95 | etimes = np.array(list(range(25, 76, 3)) + list(range(80, 251, 10))) 96 | #etimes = np.array([100.0]) 97 | for etime in etimes: 98 | for index in range(config["configuration"]["num_mages"]): 99 | tconfig = deepcopy(config) 100 | tconfig["configuration"]["target"] = [index] 101 | tconfig['timing']['duration'] = { 102 | "mean": etime, 103 | "var": 3.0, 104 | "clip": [0.0, 10000.0]} 105 | value = main(tconfig, config["configuration"]["name"][index]) 106 | out[index].append(value[0]) 107 | 108 | plt.close('all') 109 | plt.figure(figsize=(8.0, 5.5), dpi=200) 110 | plt.title(f"{encounter:s} {caption:s}") 111 | for index, ou in enumerate(out): 112 | #plt.plot(etimes, np.array(ou), label=config["configuration"]["name"][index]) 113 | plt.plot(etimes, 114 | np.array(ou), 115 | label=config["configuration"]["name"][index]) 116 | plt.xlabel('Encounter Duration (seconds)') 117 | plt.ylabel('Average DPS') 118 | plt.xlim(0, 250) 119 | plt.legend() 120 | plt.savefig(f"{encounter:s}_{caption:s}.png") 121 | 122 | if __name__ == '__main__': 123 | time_comparison_no_err() 124 | -------------------------------------------------------------------------------- /src/sim/old_scripts/regression.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from copy import deepcopy 4 | from mechanics import get_damage 5 | import matplotlib as mpl 6 | mpl.use('Agg') 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | import pickle 10 | 11 | VERSION = 3 12 | _TYPE = 0 13 | 14 | def main(config, name, ret_dist=False, sim_size=250, dur_dist=None): 15 | config["sim_size"] = sim_size 16 | values = get_damage(config, ret_dist=ret_dist, dur_dist=dur_dist) 17 | return values 18 | 19 | def load_config(cfn): 20 | config_file = os.path.join("../config/", cfn) 21 | with open(config_file, 'rt') as fid: 22 | config = json.load(fid) 23 | 24 | return config 25 | 26 | def regress(encounter): 27 | config = load_config(encounter + ".json") 28 | 29 | sim_size = 10000 30 | values = main(config, encounter, sim_size=sim_size) 31 | print(encounter, values) 32 | 33 | if __name__ == '__main__': 34 | for rdx in range(4): 35 | regress(f"regression{rdx + 1:d}") 36 | 37 | -------------------------------------------------------------------------------- /src/sim/old_scripts/som_damage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from copy import deepcopy 4 | from mechanics import get_damage 5 | import matplotlib as mpl 6 | mpl.use('Agg') 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | 10 | VERSION = 3 11 | 12 | def main(config, name, ret_dist=False, sim_size=250): 13 | config["sim_size"] = sim_size 14 | values = get_damage(config, ret_dist=ret_dist) 15 | print(f" {name}={values[0]:6.1f}") 16 | return values 17 | 18 | def load_config(cfn): 19 | config_file = os.path.join("./items/", cfn) 20 | with open(config_file, 'rt') as fid: 21 | config = json.load(fid) 22 | 23 | return config 24 | 25 | def time_comparison(): 26 | max_mages = 7 27 | etimes = np.array(list(range(25, 90, 5)) + list(range(90, 271, 15)) + list(range(300, 601, 30))) 28 | 29 | out = [[] for dummy in range(max_mages)] 30 | 31 | sim_sizes = [0, 1000, 707, 577, 500, 447, 408, 378] 32 | 33 | for num_mages in range(1, max_mages + 1): 34 | config = load_config(f"mages_{num_mages:d}.json") 35 | config["configuration"]["target"] = list(range(num_mages)) 36 | config["timing"]["delay"] = 3.0 37 | osim_size = 50.0*sim_sizes[num_mages] 38 | 39 | # uncomment for phase 6 gear 40 | config["stats"]["spell_power"] = [785 for aa in range(num_mages)] 41 | config["stats"]["crit_chance"] = [0.15 for aa in range(num_mages)] 42 | config["stats"]["intellect"] = [302 for aa in range(num_mages)] 43 | config["buffs"]["consumes"] = { 44 | "greater_arcane_elixir", 45 | "elixir_of_greater_firepower", 46 | "flask_of_supreme_power", 47 | "blessed_wizard_oil"} 48 | config["configuration"]["sapp"] = list(range(num_mages)) 49 | 50 | #config['timing']['duration'] = { 51 | # "mean": 300.0, 52 | # "var": 3.0, 53 | # "clip": [0.0, 10000.0]} 54 | #sim_size = int(osim_size*10/120**0.5) 55 | #main(config, f"{num_mages:d}", sim_size=sim_size)[0] 56 | #dksok() 57 | for etime in etimes: 58 | sim_size = int(osim_size*10/etime**0.5) 59 | tconfig = deepcopy(config) 60 | tconfig['timing']['duration'] = { 61 | "mean": etime, 62 | "var": 3.0, 63 | "clip": [0.0, 10000.0]} 64 | value_0 = main(tconfig, f"{num_mages:d} {etime:3.0f} fb ", sim_size=sim_size)[0] 65 | tconfig["rotation"]["continuing"]["special"] = {"slot": list(range(num_mages)), "value": "scorch"} 66 | value_1 = main(tconfig, f"{num_mages:d} {etime:3.0f} sc ", sim_size=sim_size)[0] 67 | out[num_mages - 1].append((value_1 - value_0)/num_mages) 68 | 69 | plt.close('all') 70 | plt.figure(figsize=(8.0, 5.5), dpi=200) 71 | plt.title(f"SoM Damage (Longer Encounters, no WBs, Phase 6 Gear)") 72 | for num_mages, ou in enumerate(out): 73 | #plt.plot(etimes, np.array(ou), label=config["configuration"]["name"][index]) 74 | label = f"{num_mages + 1:d} mages" 75 | if num_mages < 1: 76 | label = label[:-1] 77 | plt.plot(etimes, 78 | np.array(ou), 79 | label=label) 80 | plt.xlabel('Encounter Duration (seconds)') 81 | plt.ylabel('DPS Difference per Mage (Ignite Hold - Always Fireball) Rotations') 82 | plt.grid() 83 | plt.xlim(0, 600) 84 | plt.legend() 85 | plt.savefig("som_rotation_phase6.png") 86 | 87 | if __name__ == '__main__': 88 | time_comparison() 89 | -------------------------------------------------------------------------------- /src/sim/old_scripts/som_equiv.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from copy import deepcopy 4 | from mechanics import get_damage 5 | import matplotlib as mpl 6 | mpl.use('Agg') 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | 10 | VERSION = 3 11 | 12 | def main(config, name, ret_dist=False, sim_size=250): 13 | config["sim_size"] = sim_size 14 | values = get_damage(config, ret_dist=ret_dist) 15 | print(f" {name}={values[0]:6.1f}") 16 | return values 17 | 18 | def load_config(cfn): 19 | config_file = os.path.join("./items/", cfn) 20 | with open(config_file, 'rt') as fid: 21 | config = json.load(fid) 22 | 23 | return config 24 | 25 | def time_comparison(): 26 | max_mages = 7 27 | etimes = np.array(list(range(25, 90, 5)) + list(range(90, 271, 15)) + list(range(300, 601, 30))) 28 | 29 | out = [[] for dummy in range(max_mages)] 30 | 31 | sim_sizes = [0, 1000, 707, 577, 500, 447, 408, 378] 32 | 33 | for num_mages in range(1, max_mages + 1): 34 | config = load_config(f"mages_{num_mages:d}.json") 35 | config["timing"]["delay"] = 3.0 36 | config["rotation"]["continuing"]["special"] = {"slot": list(range(num_mages)), "value": "scorch"} 37 | config["stats"]["spell_power"] = [785 for aa in range(num_mages)] 38 | config["stats"]["crit_chance"] = [0.15 for aa in range(num_mages)] 39 | config["stats"]["intellect"] = [317 for aa in range(num_mages)] 40 | config["buffs"]["consumes"] = { 41 | "greater_arcane_elixir", 42 | "elixir_of_greater_firepower", 43 | "flask_of_supreme_power", 44 | "blessed_wizard_oil"} 45 | config["configuration"]["sapp"] = list(range(num_mages)) 46 | osim_size = 150.0*sim_sizes[num_mages] 47 | 48 | for etime in etimes: 49 | sim_size = int(osim_size*10/etime**0.5) 50 | tconfig = deepcopy(config) 51 | tconfig['timing']['duration'] = { 52 | "mean": etime, 53 | "var": 3.0, 54 | "clip": [0.0, 10000.0]} 55 | value_0 = main(tconfig, f"{num_mages:d} {etime:3.0f} 0", sim_size=sim_size)[0] 56 | tconfig["stats"]["spell_power"][-1] += 30 57 | value_sp = main(tconfig, f"{num_mages:d} {etime:3.0f} sp", sim_size=sim_size)[0] 58 | tconfig["stats"]["spell_power"][-1] -= 30 59 | tconfig["stats"]["crit_chance"][-1] += 0.03 60 | value_cr = main(tconfig, f"{num_mages:d} {etime:3.0f} cr", sim_size=sim_size)[0] 61 | out[num_mages - 1].append(10.0*(value_cr - value_0)/(value_sp - value_0)) 62 | 63 | plt.close('all') 64 | plt.figure(figsize=(8.0, 5.5), dpi=200) 65 | plt.title(f"SoM Crit Values (Longer Encounters, no WBs, ignite hold, Phase 6 Gear)") 66 | for num_mages, ou in enumerate(out): 67 | #plt.plot(etimes, np.array(ou), label=config["configuration"]["name"][index]) 68 | label = f"{num_mages + 1:d} mages" 69 | if num_mages < 1: 70 | label = label[:-1] 71 | plt.plot(etimes, 72 | np.array(ou), 73 | label=label) 74 | plt.xlabel('Encounter Duration (seconds)') 75 | plt.ylabel('SP per 1% Crit') 76 | plt.grid() 77 | plt.xlim(0, 600) 78 | plt.legend() 79 | plt.savefig("som_crit_phase6_scorch.png") 80 | 81 | if __name__ == '__main__': 82 | time_comparison() 83 | -------------------------------------------------------------------------------- /src/sim/old_scripts/upgrade_lists.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import pickle 4 | from copy import deepcopy 5 | from mechanics import get_damage 6 | from items.items import CharInfo 7 | 8 | __savefile = "items/items.dat" 9 | 10 | def main_dam(config, name, duration=80.0, buffs=False, ret_dist=False, sim_size=250): 11 | tconfig = deepcopy(config) 12 | tconfig['timing']['duration'] = {"mean": duration, "var": 3.0, "clip": [0.0, 10000.0]} 13 | if not buffs: 14 | tconfig["buffs"]["world"] = [] 15 | tconfig["sim_size"] = sim_size 16 | values = get_damage(tconfig, ret_dist=ret_dist) 17 | #print(f" {name}={values[0]:6.1f}") 18 | return values 19 | 20 | def compute(config, name, durations, buffs, sim_size): 21 | return sum([main_dam(config, name, sim_size=sim_size, buffs=wb, duration=dur)[0] for wb, dur in zip(buffs, durations)])/len(buffs) 22 | 23 | def get_chars(encounter, saved): 24 | chars = [] 25 | for filename in os.listdir(encounter): 26 | fullfile = os.path.join(encounter, filename) 27 | name = filename.split('-')[-1].split('.')[0] 28 | char_info = CharInfo(fullfile, name, saved) 29 | chars.append(char_info) 30 | 31 | return chars 32 | 33 | def get_conf(chars, target=None): 34 | template = "items/template.json" 35 | 36 | with open(template, "rt") as fid: 37 | econf = json.load(fid) 38 | 39 | econf["stats"] = { 40 | "spell_power": [], 41 | "hit_chance": [], 42 | "crit_chance": [], 43 | "intellect": [] 44 | } 45 | econf["buffs"]["racial"] = [] 46 | econf["configuration"] = { 47 | "num_mages": len(chars), 48 | "target": [], 49 | "sapp": [], 50 | "toep": [], 51 | "zhc": [], 52 | "mqg": [], 53 | "pi": [], 54 | "udc": [], 55 | "name": []} 56 | 57 | for cindex, char in enumerate(chars): 58 | econf["stats"]["spell_power"].append(char.spd) 59 | econf["stats"]["hit_chance"].append(char.hit/100.0) 60 | crit = char.crt 61 | for char2 in chars: 62 | if char2.name != char.name and char2.atiesh: 63 | crit += 2 64 | 65 | econf["stats"]["crit_chance"].append(crit/100.0) 66 | econf["stats"]["intellect"].append(char.intellect) 67 | econf["buffs"]["racial"].append(char.race) 68 | 69 | for active in ["sapp", "toep", "zhc", "mqg"]: 70 | if active in char.act: 71 | econf["configuration"][active].append(cindex) 72 | if char.is_udc: 73 | econf["configuration"]["udc"].append(cindex) 74 | econf["configuration"]["name"].append(char.name) 75 | if target is not None: 76 | econf["configuration"]["target"] = [target] 77 | 78 | return econf 79 | 80 | def main(): 81 | #encounter = os.path.join("items", "pinkteam") 82 | encounter = os.path.join("items", "patchwork") 83 | 84 | with open(__savefile, "rb") as fid: 85 | saved = pickle.load(fid) 86 | 87 | chars = get_chars(encounter, saved) 88 | 89 | #wbs = [True, False, True, False] 90 | #ttime = [120.0, 120.0, 200.0, 200.0] 91 | wbs = [True] 92 | ttime = [150.0] 93 | 94 | # UDC and atiesh 95 | #blacklist = [23091, 23084, 23085, 22589] 96 | #blacklist = [23091, 23084, 23085] 97 | #blacklist = list(saved.keys()) 98 | #blacklist.remove(22589) 99 | blacklist = [] 100 | grouplist = [[(23091, False), (23084, False), (23085, False), (21344, False), 101 | (21709, False), (23031, True), (19339, False), (23207, True), 102 | (22497, False)], # UDC + eniga_b + hit rings + mark/mqg + T3 pants 103 | [(23091, False), (23084, False), (23085, False), (21344, False), 104 | (21709, False), (23031, True), (19339, False), (23207, True)], # UDC + eniga_b + hit rings + mark/mqg 105 | [(23070, False), (22820, False)], # polarity + hit wand 106 | [(22807, False), (23049, False)]] # wraith + sapps eye 107 | #grouplist = [[(22500, False), (19339, True), (22807, False), (23049, False), (21585, False), (22821, False)], # non udc 108 | # [(23091, False), (23084, False), (23085, False), (21344, False), 109 | # (21709, False), (23031, True), (19339, False), (23207, True), 110 | # (22807, False), (23049, False), (22820, False)]] # wraith + sapps eye 111 | 112 | dupes = [11, 12] 113 | sim_size = 10000 114 | threshold = -2.0 115 | 116 | #for idx in range(len(chars)): 117 | for idx in [range(len(chars))[3]]: 118 | chars = get_chars(encounter, saved) 119 | config = get_conf(chars, target=idx) 120 | 121 | baseline = compute(config, config["configuration"]["name"][idx], ttime, wbs, sim_size) 122 | test_line = compute(config, config["configuration"]["name"][idx], [ttime[0]], [wbs[0]], sim_size) 123 | print(config["configuration"]["name"][idx], baseline) 124 | 125 | best_list = [] 126 | for item in saved: 127 | 128 | if saved[item]["slt"] is None: 129 | continue 130 | if item in [itm.item for itm in chars[idx]._items]: 131 | continue 132 | if item in blacklist: 133 | continue 134 | 135 | tchars = get_chars(encounter, saved) 136 | tchars[idx].replace(item, saved) 137 | 138 | config = get_conf(tchars, target=idx) 139 | rname = config["configuration"]["name"][idx] + "_" + saved[item]["name"] 140 | check = compute(config, rname, [ttime[0]], [wbs[0]], sim_size//10) 141 | print(" ", rname, check) 142 | if check > test_line + threshold: 143 | value = compute(config, rname, ttime, wbs, sim_size) 144 | best_list.append(((value - baseline)/baseline, saved[item]["name"], False)) 145 | 146 | if saved[item]["slt"] in dupes: 147 | tchars = get_chars(encounter, saved) 148 | tchars[idx].replace(item, saved, second=True) 149 | config = get_conf(tchars, target=idx) 150 | rname = config["configuration"]["name"][idx] + "_" + saved[item]["name"] 151 | check = compute(config, rname, [ttime[0]], [wbs[0]], sim_size//10) 152 | print(" ", rname, check) 153 | if check > test_line + threshold: 154 | value = compute(config, rname, ttime, wbs, sim_size) 155 | best_list.append(((value - baseline)/baseline, saved[item]["name"], True)) 156 | 157 | 158 | for group in grouplist: 159 | tchars = get_chars(encounter, saved) 160 | for item, second in group: 161 | tchars[idx].replace(item, saved, second=second) 162 | config = get_conf(tchars, target=idx) 163 | rname = config["configuration"]["name"][idx] + "_" + ''.join([saved[item]["name"][:3] for item, ss in group]) 164 | check = compute(config, rname, [ttime[0]], [wbs[0]], sim_size//10) 165 | print(" ", rname, check) 166 | if check > test_line + threshold: 167 | value = compute(config, rname, ttime, wbs, sim_size) 168 | best_list.append(((value - baseline)/baseline, ''.join([saved[item]["name"][:3] for item, ss in group]), False)) 169 | 170 | 171 | best_list.sort(reverse=True) 172 | print(config["configuration"]["name"][idx]) 173 | for value, name, sec in best_list: 174 | print(f" {name:36s} {100.0*value:5.3f} {'2nd slot' if sec else '':s}") 175 | 176 | 177 | 178 | if __name__ == "__main__": 179 | main() --------------------------------------------------------------------------------