├── ffxivcalc ├── Jobs │ ├── __init__.py │ ├── Caster │ │ ├── __init__.py │ │ ├── Blackmage │ │ │ └── __init__.py │ │ ├── Redmage │ │ │ └── __init__.py │ │ ├── Summoner │ │ │ └── __init__.py │ │ └── Caster_Spell.py │ ├── Healer │ │ ├── __init__.py │ │ ├── Sage │ │ │ ├── __init__.py │ │ │ └── Sage_Spell.py │ │ ├── Scholar │ │ │ ├── __init__.py │ │ │ └── Scholar_Spell.py │ │ ├── Whitemage │ │ │ ├── __init__.py │ │ │ └── Whitemage_Spell.py │ │ ├── Astrologian │ │ │ └── __init__.py │ │ └── Healer_Spell.py │ ├── Melee │ │ ├── __init__.py │ │ ├── Monk │ │ │ └── __init__.py │ │ ├── Ninja │ │ │ └── __init__.py │ │ ├── Dragoon │ │ │ └── __init__.py │ │ ├── Reaper │ │ │ └── __init__.py │ │ ├── Samurai │ │ │ └── __init__.py │ │ └── Melee_Spell.py │ ├── Ranged │ │ ├── __init__.py │ │ ├── Bard │ │ │ └── __init__.py │ │ ├── Dancer │ │ │ └── __init__.py │ │ ├── Machinist │ │ │ └── __init__.py │ │ └── Ranged_Spell.py │ ├── Tank │ │ ├── __init__.py │ │ ├── DarkKnight │ │ │ └── __init__.py │ │ ├── Gunbreaker │ │ │ ├── __init__.py │ │ │ └── Gunbreaker_Spell.py │ │ ├── Paladin │ │ │ └── __init__.py │ │ ├── Warrior │ │ │ └── __init__.py │ │ └── Tank_Spell.py │ └── PlayerEnum.py ├── Tester │ └── __init__.py ├── UI │ ├── __init__.py │ └── TUI.py ├── API │ ├── Model │ │ ├── __init__.py │ │ ├── ObjectOutModel.py │ │ └── ObjectInModel.py │ ├── __init__.py │ ├── API.py │ └── library.py ├── GearSolver │ ├── __init__.py │ ├── MaimingGear.json │ ├── NINGear.json │ ├── AimingGear.json │ ├── StrikingGear.json │ ├── WARTOPSet.json │ ├── BLMSet.json │ ├── CasterSet.json │ ├── BLMTOPSet.json │ ├── FendingGear.json │ ├── HealingGear.json │ ├── PaladinGear.json │ └── strikingDSR.json ├── Request │ ├── __init__.py │ ├── etro_request.py │ ├── custom_columns.py │ └── prepull.py ├── helperCode │ ├── __init__.py │ ├── helper_math.py │ ├── requirementHandler.py │ ├── Progress.py │ ├── lookUpAbilityID.py │ ├── exceptions.py │ └── Vocal.py ├── SimulationRecord │ └── __init__.py ├── codeParser │ ├── __init__.py │ └── parser.py ├── __init__.py ├── __main__.py └── Enemy.py ├── requirements.dev.txt ├── requirements.txt ├── pyproject.toml ├── setup.cfg ├── License.md ├── .gitignore └── README.md /ffxivcalc/Jobs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Tester/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/UI/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/API/Model/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/GearSolver/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Caster/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Healer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Melee/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Ranged/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Tank/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Request/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/helperCode/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Healer/Sage/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Melee/Monk/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Melee/Ninja/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Ranged/Bard/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/SimulationRecord/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Caster/Blackmage/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Caster/Redmage/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Caster/Summoner/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Healer/Scholar/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Healer/Whitemage/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Melee/Dragoon/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Melee/Reaper/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Melee/Samurai/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Ranged/Dancer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Ranged/Machinist/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Tank/DarkKnight/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Tank/Gunbreaker/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Tank/Paladin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Tank/Warrior/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Healer/Astrologian/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.dev.txt: -------------------------------------------------------------------------------- 1 | coverage[toml]==6.5.0 2 | mypy==0.982 3 | mypy-extensions==0.4.3 4 | pylint==2.15.5 5 | fastapi==0.85.1 6 | -------------------------------------------------------------------------------- /ffxivcalc/codeParser/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module has been deactivated. Please do not use it. 3 | """ 4 | 5 | from ffxivcalc.helperCode.exceptions import deactivatedModuleCodeParser 6 | raise deactivatedModuleCodeParser -------------------------------------------------------------------------------- /ffxivcalc/API/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module is outdated and should not be used. An error is thrown when importing it but if you still want to use it 3 | simply do a try/except block. 4 | """ 5 | 6 | from ffxivcalc.helperCode.exceptions import outDatedAPICode 7 | raise outDatedAPICode 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | contourpy==1.0.5 2 | cycler==0.11.0 3 | fonttools==4.37.2 4 | kiwisolver==1.4.4 5 | matplotlib==3.6.0 6 | numpy==1.23.3 7 | packaging==21.3 8 | Pillow==9.2.0 9 | pyparsing==3.0.9 10 | python-dateutil==2.8.2 11 | six==1.16.0 12 | coreapi==2.3.3 13 | pandas 14 | python_graphql_client 15 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | # See https://coverage.readthedocs.io/en/6.5.0/config.html for configuration options 6 | [tool.coverage.run] 7 | branch = true 8 | source = ["ffxivcalc"] 9 | 10 | [tool.black] 11 | line-length = 120 12 | skip-string-normalization = true 13 | -------------------------------------------------------------------------------- /ffxivcalc/helperCode/helper_math.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): # Helper function to compare float 4 | return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) 5 | 6 | def roundDown(x, precision): 7 | return math.floor(x * 10**precision)/10**precision 8 | 9 | def roundUp(x, precision): 10 | return math.ceil(x * 10**precision)/10**precision -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = ffxiv-combat-simulator 3 | version = attr: ffxivcalc.__version__ 4 | author = IAmPythagoras 5 | author_email = Discord -> Pythagoras#6312 6 | description = Environment in which the simulation of FF14 combat is possible. 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | license = MIT 10 | 11 | [options] 12 | python_requires = >= 3.10 13 | include_package_data = True 14 | packages = find: 15 | install_requires = file: requirements.txt 16 | 17 | [options.entry_points] 18 | console_scripts = 19 | ffxivcalc = ffxivcalc.__main__:main 20 | 21 | [options.packages.find] 22 | exclude = 23 | Fun Stuff if you're bored* 24 | OldStuff* 25 | saved* 26 | 27 | [options.package_data] 28 | ffxivcalc = py.typed 29 | 30 | [options.extras_require] 31 | dev = file: requirements.dev.txt 32 | -------------------------------------------------------------------------------- /ffxivcalc/helperCode/requirementHandler.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file will contain class and functions regarding handling when an action cannot be casted. 3 | """ 4 | 5 | class failedRequirementEvent: 6 | """ 7 | This class will be object created when a requirement isn't met. It will contain information regarding the failure. 8 | """ 9 | 10 | def __init__(self, timeStamp : float, playerID : int, requirementName : str, additionalInfo : str, fatal : bool): 11 | """ 12 | timeStamp : float -> time at which the requirement failed 13 | playerID : int -> ID of the player that failed the requirement 14 | requirementName : str -> name of the requirement 15 | additionalInfo : str -> additional information relating to the requirement. Might be empty 16 | fatal : bool -> true if this requirement made the simulator stop 17 | """ 18 | self.timeStamp = timeStamp 19 | self.playerID = playerID 20 | self.requirementName = requirementName 21 | self.additionalInfo = additionalInfo 22 | self.fatal = fatal -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Anthony Desrochers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ffxivcalc/API/Model/ObjectOutModel.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file will contain model of return request from the API. 3 | """ 4 | 5 | from pydantic import BaseModel 6 | from typing import List, Dict 7 | class failedRequirementEventOut(BaseModel): 8 | timeStamp : float 9 | playerID : int 10 | requirementName : str 11 | additionalInfo : str 12 | fatal : bool 13 | 14 | class fightInfoOut(BaseModel): 15 | fightDuration : float 16 | maxfightDuration : float 17 | fightname : str | None = "SimulatedFight" 18 | TeamCompositionBonus : float 19 | failedRequirementEventList : List[failedRequirementEventOut] 20 | Success : bool | str = "Unknown" 21 | 22 | class GraphInfoOut(BaseModel): 23 | value : float | int 24 | name : float | int 25 | 26 | class PlayerInfoOut(BaseModel): 27 | JobName : str 28 | ExpectedDPS : float 29 | PotencyPerSecond : float 30 | TotalDamage : float 31 | TotalPotency : float 32 | numberOfGCD : int 33 | ProcInfo : Dict 34 | GraphInfoDPS : List[GraphInfoOut] | List[None] 35 | GraphInfoPPS : List[GraphInfoOut] | List[None] 36 | 37 | class dataOut(BaseModel): 38 | fightInfo : fightInfoOut 39 | PlayerList : List[PlayerInfoOut] 40 | 41 | class SimulateFightOut(BaseModel): 42 | data : dataOut 43 | class Config: 44 | orm_mode = True 45 | 46 | 47 | -------------------------------------------------------------------------------- /ffxivcalc/API/API.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file will contain the API module fastAPI. It is the file to launch if you 3 | want to run the API. 4 | """ 5 | 6 | from fastapi import FastAPI 7 | from ffxivcalc.API.Model.ObjectInModel import SimulateFightIn 8 | from ffxivcalc.API.Model.ObjectOutModel import SimulateFightOut 9 | from ffxivcalc.API.library import SimulateFightAPIHelper 10 | 11 | # Make sure you have uvicorn installed and run the command 12 | # python -m uvicorn API:app 13 | # in the folder where API.py is located to run the API. Add --reload if you want the API to restart 14 | # for every detected changes in the code. 15 | # Then go to HOSTING_ADRESS/docs and you can tryout the different functionallities 16 | 17 | app = FastAPI() # Creating api instance 18 | 19 | @app.post("/SimulateFight", response_model=SimulateFightOut) 20 | def GetSimulateFight(info : SimulateFightIn): 21 | """ 22 | This API functionality lets someone ask for a simulation of a given fight. 23 | The API will request a JSON file with the correct format and will return the fight's 24 | results using as a schema SimulateFightOut. 25 | 26 | Args:\n 27 | info (SimulateFightIn): JSON file containing the fight's parameters. 28 | 29 | Returns:\n 30 | JSON : JSON file with the SimulateFightOut schema. 31 | """ 32 | 33 | returnData = SimulateFightAPIHelper(info.dict()) 34 | 35 | return returnData 36 | -------------------------------------------------------------------------------- /ffxivcalc/API/Model/ObjectInModel.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file will contain all BaseModel as class for the different request we can give to the API. 3 | This follows the fastAPI model : https://fastapi.tiangolo.com/tutorial/body/ 4 | """ 5 | 6 | from pydantic import BaseModel 7 | from typing import List 8 | # defining class that will be expected. This will let fastAPI do data validation 9 | 10 | class fightInfoIn(BaseModel): 11 | fightDuration : float 12 | time_unit : float 13 | ShowGraph : bool 14 | RequirementOn : bool 15 | IgnoreMana : bool 16 | 17 | class statIn(BaseModel): 18 | MainStat : int 19 | WD : int 20 | Det : int 21 | Ten : int 22 | SS : int 23 | SkS : int 24 | Crit : int 25 | DH : int 26 | 27 | class actionIn(BaseModel): 28 | actionName : str 29 | waitTime : float | None = None 30 | 31 | class PlayerInfoIn(BaseModel): 32 | JobName : str 33 | playerID : int 34 | stat : statIn 35 | etro_gearset_url : str | None = "" 36 | Auras : List[str] 37 | actionList : List[actionIn] 38 | 39 | class dataIn(BaseModel): 40 | fightInfo : fightInfoIn 41 | PlayerList : List[PlayerInfoIn] 42 | 43 | class RequestParamIn(BaseModel): 44 | GraphInfo : bool 45 | ProcInfo : bool 46 | failedRequirementEvent : bool 47 | 48 | 49 | class SimulateFightIn(BaseModel): 50 | data : dataIn # Fight's data 51 | RequestParam : RequestParamIn # Parameter of request such as if the user wants graph info, etc. 52 | class Config: 53 | orm_mode = True -------------------------------------------------------------------------------- /ffxivcalc/Jobs/PlayerEnum.py: -------------------------------------------------------------------------------- 1 | # This file will contain the enums of all jobs and class 2 | from enum import IntEnum # Importing enums 3 | 4 | class PlayerEnum(IntEnum): 5 | # Parent enum class for all other enums. Will have 6 | # the two functions. 7 | 8 | @classmethod 9 | def name_for_id(cls, id : int) -> str: 10 | # maps from id -> name 11 | if id in cls.__members__.values(): 12 | return cls(id).name 13 | return 'Unknown' 14 | 15 | @classmethod 16 | def id_for_name(cls, name : str) -> int: 17 | # maps from name -> id 18 | if name in cls.__members__.keys(): 19 | return cls[name].value 20 | return -1 # Evaluated as Unknown 21 | 22 | 23 | class RoleEnum(PlayerEnum): 24 | # Enum for all roles 25 | 26 | Caster = 1 27 | Healer = 2 28 | Melee = 3 29 | Tank = 4 30 | PhysicalRanged = 5 31 | Pet = 6 32 | 33 | 34 | class JobEnum(PlayerEnum): 35 | # Enum for all jobs 36 | 37 | # Caster 38 | BlackMage = 1 39 | Summoner = 2 40 | RedMage = 3 41 | 42 | # Healer 43 | WhiteMage = 4 44 | Astrologian = 5 45 | Sage = 6 46 | Scholar = 7 47 | 48 | # Melee 49 | Ninja = 8 50 | Samurai = 9 51 | Reaper = 10 52 | Monk = 11 53 | Dragoon = 12 54 | 55 | # Tank 56 | Gunbreaker = 13 57 | DarkKnight = 14 58 | Paladin = 15 59 | Warrior = 16 60 | 61 | # Physical ranged 62 | Machinist = 17 63 | Bard = 18 64 | Dancer = 19 65 | 66 | # Pet 67 | Pet = 20 68 | -------------------------------------------------------------------------------- /ffxivcalc/Request/etro_request.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | This code was taken from a comment in : https://github.com/IAmPythagoras/FFXIV-Combat-Simulator/issues/17 4 | and was written by https://github.com/Alex-Ueki @apollo#3810 5 | and modified to fit the needs I have. 6 | """ 7 | # Installed versions I use 8 | # coreapi==2.3.3 9 | # coreapi-cli==1.0.9 10 | import coreapi 11 | import logging 12 | 13 | main_logging = logging.getLogger("ffxivcalc") 14 | etro_logging = main_logging.getChild("etroAPI") 15 | 16 | def get_gearset_data(set_id: str) -> dict: 17 | """ 18 | Gets the gearset data (stats) via an id. Allows url too (handles it for free) 19 | set_id : str -> URL of the set from etro 20 | """ 21 | # Handles urls by checking for webpage name and splitting 22 | logging.debug("Requesting gearset info from etro.gg, set_id : " + str(set_id)) 23 | try: 24 | cleaned_set_id = set_id.split("/")[-1] if "etro.gg/gearset" in set_id else set_id # Cleans the URL 25 | client = coreapi.Client() 26 | data = client .action( 27 | client.get("https://etro.gg/api/docs/"), 28 | ["gearsets", "read"], 29 | params={ 30 | "id": cleaned_set_id, 31 | }, 32 | ) 33 | 34 | 35 | # Since the first 8 data points are the stats. That's all we are interested in. 36 | # We will not use all of them since we will filter through. But this contains all we need 37 | 38 | stats = { 39 | data["totalParams"][i]["name"] : data["totalParams"][i]["value"] 40 | for i in range(len(data["totalParams"])) 41 | } 42 | 43 | # Will now change the keys' name 44 | 45 | return { 46 | "MainStat" : data["totalParams"][0]["value"] , # Always first value 47 | "WD" : stats["Weapon Damage"] if "Weapon Damage" in stats.keys() else 0, 48 | "Det" : stats["DET"], 49 | "Ten" : stats["TEN"] if "TEN" in stats.keys() else 400, 50 | "SS" : stats["SPS"] if "SPS" in stats.keys() else 400, 51 | "SkS" : stats["SkS"] if "SkS" in stats.keys() else 400, 52 | "Crit" : stats["CRT"], 53 | "DH" : stats["DH"], 54 | "Piety" : stats["Piety"] if "Piety" in stats.keys() else 390 55 | } 56 | except: 57 | # If error during import (wrong URL, server error, etc.) 58 | # it returns None 59 | return None -------------------------------------------------------------------------------- /ffxivcalc/GearSolver/MaimingGear.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "GearType" : 0, 4 | "MateriaLimit" : 2, 5 | "Name" : "Raid", 6 | "StatList" : [ 7 | ["Crit", 306], 8 | ["Det", 214], 9 | ["MainStat", 416], 10 | ["WD", 132] 11 | ]}, 12 | { 13 | "GearType" : 2, 14 | "MateriaLimit" : 2, 15 | "Name" : "Raid", 16 | "StatList" : [ 17 | ["SkS", 184], 18 | ["DH", 129], 19 | ["MainStat", 248] 20 | ]}, 21 | { 22 | "GearType" : 2, 23 | "MateriaLimit" : 2, 24 | "Name" : "Tome", 25 | "StatList" : [ 26 | ["Crit", 184], 27 | ["DH", 129], 28 | ["MainStat", 248] 29 | ]}, 30 | { 31 | "GearType" : 3, 32 | "MateriaLimit" : 2, 33 | "Name" : "Raid", 34 | "StatList" : [ 35 | ["Crit", 292], 36 | ["Det", 204], 37 | ["MainStat", 394] 38 | ]}, 39 | { 40 | "GearType" : 3, 41 | "MateriaLimit" : 2, 42 | "Name" : "Tome", 43 | "StatList" : [ 44 | ["Det", 292], 45 | ["DH", 204], 46 | ["MainStat", 394] 47 | ]}, 48 | { 49 | "GearType" : 4, 50 | "MateriaLimit" : 2, 51 | "Name" : "Raid", 52 | "StatList" : [ 53 | ["DH", 184], 54 | ["Det", 129], 55 | ["MainStat", 248] 56 | ]}, 57 | { 58 | "GearType" : 4, 59 | "MateriaLimit" : 2, 60 | "Name" : "Tome", 61 | "StatList" : [ 62 | ["Det", 184], 63 | ["Crit", 129], 64 | ["MainStat", 248] 65 | ]}, 66 | { 67 | "GearType" : 5, 68 | "MateriaLimit" : 2, 69 | "Name" : "Raid", 70 | "StatList" : [ 71 | ["Det", 292], 72 | ["DH", 204], 73 | ["MainStat", 394] 74 | ]}, 75 | { 76 | "GearType" : 5, 77 | "MateriaLimit" : 2, 78 | "Name" : "Tome", 79 | "StatList" : [ 80 | ["Crit", 292], 81 | ["Det", 204], 82 | ["MainStat", 394] 83 | ]}, 84 | { 85 | "GearType" : 6, 86 | "MateriaLimit" : 2, 87 | "Name" : "Raid", 88 | "StatList" : [ 89 | ["Crit", 184], 90 | ["DH", 129], 91 | ["MainStat", 248] 92 | ]}, 93 | { 94 | "GearType" : 6, 95 | "MateriaLimit" : 2, 96 | "Name" : "Tome", 97 | "StatList" : [ 98 | ["SkS", 184], 99 | ["DH", 129], 100 | ["MainStat", 248] 101 | ]}, 102 | { 103 | "GearType" : 7, 104 | "MateriaLimit" : 2, 105 | "Name" : "Raid", 106 | "StatList" : [ 107 | ["Crit", 145], 108 | ["Det", 102], 109 | ["MainStat", 196] 110 | ]}, 111 | { 112 | "GearType" : 7, 113 | "MateriaLimit" : 2, 114 | "Name" : "Tome", 115 | "StatList" : [ 116 | ["SkS", 145], 117 | ["Det", 102], 118 | ["MainStat", 196] 119 | ]}, 120 | { 121 | "GearType" : 8, 122 | "MateriaLimit" : 2, 123 | "Name" : "Raid", 124 | "StatList" : [ 125 | ["SkS", 145], 126 | ["DH", 102], 127 | ["MainStat", 196] 128 | ]}, 129 | { 130 | "GearType" : 8, 131 | "MateriaLimit" : 2, 132 | "Name" : "Tome", 133 | "StatList" : [ 134 | ["Crit", 145], 135 | ["DH", 102], 136 | ["MainStat", 196] 137 | ]}, 138 | { 139 | "GearType" : 9, 140 | "MateriaLimit" : 2, 141 | "Name" : "Raid", 142 | "StatList" : [ 143 | ["Crit", 145], 144 | ["DH", 102], 145 | ["MainStat", 196] 146 | ]}, 147 | { 148 | "GearType" : 9, 149 | "MateriaLimit" : 2, 150 | "Name" : "Tome", 151 | "StatList" : [ 152 | ["DH", 145], 153 | ["Det", 102], 154 | ["MainStat", 196] 155 | ]}, 156 | { 157 | "GearType" : 10, 158 | "MateriaLimit" : 2, 159 | "Name" : "Raid", 160 | "StatList" : [ 161 | ["Det", 145], 162 | ["Crit", 102], 163 | ["MainStat", 196] 164 | ]}, 165 | { 166 | "GearType" : 11, 167 | "MateriaLimit" : 2, 168 | "Name" : "Tome", 169 | "StatList" : [ 170 | ["Crit", 145], 171 | ["DH", 102], 172 | ["MainStat", 196] 173 | ]} 174 | ] -------------------------------------------------------------------------------- /ffxivcalc/GearSolver/NINGear.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "GearType" : 0, 4 | "MateriaLimit" : 2, 5 | "Name" : "Raid", 6 | "StatList" : [ 7 | ["Crit", 306], 8 | ["Det", 214], 9 | ["MainStat", 416], 10 | ["WD", 132] 11 | ]}, 12 | { 13 | "GearType" : 2, 14 | "MateriaLimit" : 2, 15 | "Name" : "Raid", 16 | "StatList" : [ 17 | ["Det", 184], 18 | ["DH", 129], 19 | ["MainStat", 248] 20 | ]}, 21 | { 22 | "GearType" : 2, 23 | "MateriaLimit" : 2, 24 | "Name" : "Tome", 25 | "StatList" : [ 26 | ["Det", 184], 27 | ["Crit", 129], 28 | ["MainStat", 248] 29 | ]}, 30 | { 31 | "GearType" : 3, 32 | "MateriaLimit" : 2, 33 | "Name" : "Raid", 34 | "StatList" : [ 35 | ["Crit", 292], 36 | ["Det", 204], 37 | ["MainStat", 394] 38 | ]}, 39 | { 40 | "GearType" : 3, 41 | "MateriaLimit" : 2, 42 | "Name" : "Tome", 43 | "StatList" : [ 44 | ["DH", 292], 45 | ["Det", 204], 46 | ["MainStat", 394] 47 | ]}, 48 | { 49 | "GearType" : 4, 50 | "MateriaLimit" : 2, 51 | "Name" : "Raid", 52 | "StatList" : [ 53 | ["Det", 184], 54 | ["SkS", 129], 55 | ["MainStat", 248] 56 | ]}, 57 | { 58 | "GearType" : 4, 59 | "MateriaLimit" : 2, 60 | "Name" : "Tome", 61 | "StatList" : [ 62 | ["Crit", 184], 63 | ["DH", 129], 64 | ["MainStat", 248] 65 | ]}, 66 | { 67 | "GearType" : 5, 68 | "MateriaLimit" : 2, 69 | "Name" : "Raid", 70 | "StatList" : [ 71 | ["DH", 292], 72 | ["Det", 204], 73 | ["MainStat", 394] 74 | ]}, 75 | { 76 | "GearType" : 5, 77 | "MateriaLimit" : 2, 78 | "Name" : "Tome", 79 | "StatList" : [ 80 | ["DH", 292], 81 | ["Crit", 204], 82 | ["MainStat", 394] 83 | ]}, 84 | { 85 | "GearType" : 6, 86 | "MateriaLimit" : 2, 87 | "Name" : "Raid", 88 | "StatList" : [ 89 | ["DH", 184], 90 | ["Crit", 129], 91 | ["MainStat", 248] 92 | ]}, 93 | { 94 | "GearType" : 6, 95 | "MateriaLimit" : 2, 96 | "Name" : "Tome", 97 | "StatList" : [ 98 | ["Det", 184], 99 | ["SkS", 129], 100 | ["MainStat", 248] 101 | ]}, 102 | { 103 | "GearType" : 7, 104 | "MateriaLimit" : 2, 105 | "Name" : "Raid", 106 | "StatList" : [ 107 | ["DH", 145], 108 | ["Det", 102], 109 | ["MainStat", 196] 110 | ]}, 111 | { 112 | "GearType" : 7, 113 | "MateriaLimit" : 2, 114 | "Name" : "Tome", 115 | "StatList" : [ 116 | ["Det", 145], 117 | ["Crit", 102], 118 | ["MainStat", 196] 119 | ]}, 120 | { 121 | "GearType" : 8, 122 | "MateriaLimit" : 2, 123 | "Name" : "Raid", 124 | "StatList" : [ 125 | ["Crit", 145], 126 | ["Det", 102], 127 | ["MainStat", 196] 128 | ]}, 129 | { 130 | "GearType" : 8, 131 | "MateriaLimit" : 2, 132 | "Name" : "Tome", 133 | "StatList" : [ 134 | ["SKS", 145], 135 | ["DH", 102], 136 | ["MainStat", 196] 137 | ]}, 138 | { 139 | "GearType" : 9, 140 | "MateriaLimit" : 2, 141 | "Name" : "Raid", 142 | "StatList" : [ 143 | ["DH", 145], 144 | ["SkS", 102], 145 | ["MainStat", 196] 146 | ]}, 147 | { 148 | "GearType" : 9, 149 | "MateriaLimit" : 2, 150 | "Name" : "Tome", 151 | "StatList" : [ 152 | ["Det", 145], 153 | ["Crit", 102], 154 | ["MainStat", 196] 155 | ]}, 156 | { 157 | "GearType" : 10, 158 | "MateriaLimit" : 2, 159 | "Name" : "Raid", 160 | "StatList" : [ 161 | ["DH", 145], 162 | ["Crit", 102], 163 | ["MainStat", 196] 164 | ]}, 165 | { 166 | "GearType" : 11, 167 | "MateriaLimit" : 2, 168 | "Name" : "Tome", 169 | "StatList" : [ 170 | ["Crit", 145], 171 | ["Det", 102], 172 | ["MainStat", 196] 173 | ]} 174 | ] -------------------------------------------------------------------------------- /ffxivcalc/GearSolver/AimingGear.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "GearType" : 0, 4 | "MateriaLimit" : 2, 5 | "Name" : "Raid", 6 | "StatList" : [ 7 | ["Crit", 306], 8 | ["DH", 214], 9 | ["MainStat", 416], 10 | ["WD", 132] 11 | ]}, 12 | { 13 | "GearType" : 2, 14 | "MateriaLimit" : 2, 15 | "Name" : "Raid", 16 | "StatList" : [ 17 | ["Crit", 184], 18 | ["DH", 129], 19 | ["MainStat", 248] 20 | ]}, 21 | { 22 | "GearType" : 2, 23 | "MateriaLimit" : 2, 24 | "Name" : "Tome", 25 | "StatList" : [ 26 | ["SkS", 184], 27 | ["Crit", 129], 28 | ["MainStat", 248] 29 | ]}, 30 | { 31 | "GearType" : 3, 32 | "MateriaLimit" : 2, 33 | "Name" : "Raid", 34 | "StatList" : [ 35 | ["DH", 292], 36 | ["Det", 204], 37 | ["MainStat", 394] 38 | ]}, 39 | { 40 | "GearType" : 3, 41 | "MateriaLimit" : 2, 42 | "Name" : "Tome", 43 | "StatList" : [ 44 | ["Det", 292], 45 | ["Crit", 204], 46 | ["MainStat", 394] 47 | ]}, 48 | { 49 | "GearType" : 4, 50 | "MateriaLimit" : 2, 51 | "Name" : "Raid", 52 | "StatList" : [ 53 | ["Crit", 184], 54 | ["Det", 129], 55 | ["MainStat", 248] 56 | ]}, 57 | { 58 | "GearType" : 4, 59 | "MateriaLimit" : 2, 60 | "Name" : "Tome", 61 | "StatList" : [ 62 | ["Det", 184], 63 | ["DH", 129], 64 | ["MainStat", 248] 65 | ]}, 66 | { 67 | "GearType" : 5, 68 | "MateriaLimit" : 2, 69 | "Name" : "Raid", 70 | "StatList" : [ 71 | ["Det", 292], 72 | ["Crit", 204], 73 | ["MainStat", 394] 74 | ]}, 75 | { 76 | "GearType" : 5, 77 | "MateriaLimit" : 2, 78 | "Name" : "Tome", 79 | "StatList" : [ 80 | ["DH", 292], 81 | ["Det", 204], 82 | ["MainStat", 394] 83 | ]}, 84 | { 85 | "GearType" : 6, 86 | "MateriaLimit" : 2, 87 | "Name" : "Raid", 88 | "StatList" : [ 89 | ["SkS", 184], 90 | ["DH", 129], 91 | ["MainStat", 248] 92 | ]}, 93 | { 94 | "GearType" : 6, 95 | "MateriaLimit" : 2, 96 | "Name" : "Tome", 97 | "StatList" : [ 98 | ["DH", 184], 99 | ["Crit", 129], 100 | ["MainStat", 248] 101 | ]}, 102 | { 103 | "GearType" : 7, 104 | "MateriaLimit" : 2, 105 | "Name" : "Raid", 106 | "StatList" : [ 107 | ["DH", 145], 108 | ["Det", 102], 109 | ["MainStat", 196] 110 | ]}, 111 | { 112 | "GearType" : 7, 113 | "MateriaLimit" : 2, 114 | "Name" : "Tome", 115 | "StatList" : [ 116 | ["Det", 145], 117 | ["Crit", 102], 118 | ["MainStat", 196] 119 | ]}, 120 | { 121 | "GearType" : 8, 122 | "MateriaLimit" : 2, 123 | "Name" : "Raid", 124 | "StatList" : [ 125 | ["Crit", 145], 126 | ["Det", 102], 127 | ["MainStat", 196] 128 | ]}, 129 | { 130 | "GearType" : 8, 131 | "MateriaLimit" : 2, 132 | "Name" : "Tome", 133 | "StatList" : [ 134 | ["SKS", 145], 135 | ["DH", 102], 136 | ["MainStat", 196] 137 | ]}, 138 | { 139 | "GearType" : 9, 140 | "MateriaLimit" : 2, 141 | "Name" : "Raid", 142 | "StatList" : [ 143 | ["DH", 145], 144 | ["SkS", 102], 145 | ["MainStat", 196] 146 | ]}, 147 | { 148 | "GearType" : 9, 149 | "MateriaLimit" : 2, 150 | "Name" : "Tome", 151 | "StatList" : [ 152 | ["Det", 145], 153 | ["Crit", 102], 154 | ["MainStat", 196] 155 | ]}, 156 | { 157 | "GearType" : 10, 158 | "MateriaLimit" : 2, 159 | "Name" : "Raid", 160 | "StatList" : [ 161 | ["DH", 145], 162 | ["Crit", 102], 163 | ["MainStat", 196] 164 | ]}, 165 | { 166 | "GearType" : 11, 167 | "MateriaLimit" : 2, 168 | "Name" : "Tome", 169 | "StatList" : [ 170 | ["Crit", 145], 171 | ["Det", 102], 172 | ["MainStat", 196] 173 | ]} 174 | ] -------------------------------------------------------------------------------- /ffxivcalc/GearSolver/StrikingGear.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "GearType" : 0, 4 | "MateriaLimit" : 2, 5 | "Name" : "Raid", 6 | "StatList" : [ 7 | ["Crit", 306], 8 | ["Det", 214], 9 | ["MainStat", 416], 10 | ["WD", 132] 11 | ]}, 12 | { 13 | "GearType" : 2, 14 | "MateriaLimit" : 2, 15 | "Name" : "Raid", 16 | "StatList" : [ 17 | ["Crit", 184], 18 | ["SkS", 129], 19 | ["MainStat", 248] 20 | ]}, 21 | { 22 | "GearType" : 2, 23 | "MateriaLimit" : 2, 24 | "Name" : "Tome", 25 | "StatList" : [ 26 | ["DH", 184], 27 | ["Crit", 129], 28 | ["MainStat", 248] 29 | ]}, 30 | { 31 | "GearType" : 3, 32 | "MateriaLimit" : 2, 33 | "Name" : "Raid", 34 | "StatList" : [ 35 | ["DH", 292], 36 | ["Crit", 204], 37 | ["MainStat", 394] 38 | ]}, 39 | { 40 | "GearType" : 3, 41 | "MateriaLimit" : 2, 42 | "Name" : "Tome", 43 | "StatList" : [ 44 | ["Det", 292], 45 | ["DH", 204], 46 | ["MainStat", 394] 47 | ]}, 48 | { 49 | "GearType" : 4, 50 | "MateriaLimit" : 2, 51 | "Name" : "Raid", 52 | "StatList" : [ 53 | ["Det", 184], 54 | ["SkS", 129], 55 | ["MainStat", 248] 56 | ]}, 57 | { 58 | "GearType" : 4, 59 | "MateriaLimit" : 2, 60 | "Name" : "Tome", 61 | "StatList" : [ 62 | ["Crit", 184], 63 | ["Det", 129], 64 | ["MainStat", 248] 65 | ]}, 66 | { 67 | "GearType" : 5, 68 | "MateriaLimit" : 2, 69 | "Name" : "Raid", 70 | "StatList" : [ 71 | ["Det", 292], 72 | ["DH", 204], 73 | ["MainStat", 394] 74 | ]}, 75 | { 76 | "GearType" : 5, 77 | "MateriaLimit" : 2, 78 | "Name" : "Tome", 79 | "StatList" : [ 80 | ["Crit", 292], 81 | ["DH", 204], 82 | ["MainStat", 394] 83 | ]}, 84 | { 85 | "GearType" : 6, 86 | "MateriaLimit" : 2, 87 | "Name" : "Raid", 88 | "StatList" : [ 89 | ["Crit", 184], 90 | ["Det", 129], 91 | ["MainStat", 248] 92 | ]}, 93 | { 94 | "GearType" : 6, 95 | "MateriaLimit" : 2, 96 | "Name" : "Tome", 97 | "StatList" : [ 98 | ["DH", 184], 99 | ["SkS", 129], 100 | ["MainStat", 248] 101 | ]}, 102 | { 103 | "GearType" : 7, 104 | "MateriaLimit" : 2, 105 | "Name" : "Raid", 106 | "StatList" : [ 107 | ["Crit", 145], 108 | ["Det", 102], 109 | ["MainStat", 196] 110 | ]}, 111 | { 112 | "GearType" : 7, 113 | "MateriaLimit" : 2, 114 | "Name" : "Tome", 115 | "StatList" : [ 116 | ["SkS", 145], 117 | ["Det", 102], 118 | ["MainStat", 196] 119 | ]}, 120 | { 121 | "GearType" : 8, 122 | "MateriaLimit" : 2, 123 | "Name" : "Raid", 124 | "StatList" : [ 125 | ["SkS", 145], 126 | ["DH", 102], 127 | ["MainStat", 196] 128 | ]}, 129 | { 130 | "GearType" : 8, 131 | "MateriaLimit" : 2, 132 | "Name" : "Tome", 133 | "StatList" : [ 134 | ["Crit", 145], 135 | ["DH", 102], 136 | ["MainStat", 196] 137 | ]}, 138 | { 139 | "GearType" : 9, 140 | "MateriaLimit" : 2, 141 | "Name" : "Raid", 142 | "StatList" : [ 143 | ["Crit", 145], 144 | ["DH", 102], 145 | ["MainStat", 196] 146 | ]}, 147 | { 148 | "GearType" : 9, 149 | "MateriaLimit" : 2, 150 | "Name" : "Tome", 151 | "StatList" : [ 152 | ["DH", 145], 153 | ["Det", 102], 154 | ["MainStat", 196] 155 | ]}, 156 | { 157 | "GearType" : 10, 158 | "MateriaLimit" : 2, 159 | "Name" : "Raid", 160 | "StatList" : [ 161 | ["Det", 145], 162 | ["Crit", 102], 163 | ["MainStat", 196] 164 | ]}, 165 | { 166 | "GearType" : 11, 167 | "MateriaLimit" : 2, 168 | "Name" : "Tome", 169 | "StatList" : [ 170 | ["Crit", 145], 171 | ["DH", 102], 172 | ["MainStat", 196] 173 | ]} 174 | ] -------------------------------------------------------------------------------- /ffxivcalc/GearSolver/WARTOPSet.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "GearType" : 0, 4 | "MateriaLimit" : 0, 5 | "Name" : "Raid", 6 | "StatList" : [ 7 | ["Det", 287], 8 | ["Crit", 287], 9 | ["MainStat", 358], 10 | ["Ten", 72], 11 | ["WD", 126] 12 | ]}, 13 | { 14 | "GearType" : 2, 15 | "MateriaLimit" : 2, 16 | "Name" : "Tome", 17 | "StatList" : [ 18 | ["Crit", 172], 19 | ["Det", 120], 20 | ["MainStat", 211] 21 | ]}, 22 | { 23 | "GearType" : 2, 24 | "MateriaLimit" : 2, 25 | "Name" : "Dungeon", 26 | "StatList" : [ 27 | ["Ten", 174], 28 | ["Crit", 122], 29 | ["MainStat", 217] 30 | ]}, 31 | { 32 | "GearType" : 3, 33 | "MateriaLimit" : 2, 34 | "Name" : "Dungeon", 35 | "StatList" : [ 36 | ["Crit", 276], 37 | ["Det", 193], 38 | ["MainStat", 345] 39 | ]}, 40 | { 41 | "GearType" : 4, 42 | "MateriaLimit" : 2, 43 | "Name" : "Tome", 44 | "StatList" : [ 45 | ["Crit", 172], 46 | ["Ten", 120], 47 | ["MainStat", 211] 48 | ]}, 49 | { 50 | "GearType" : 5, 51 | "MateriaLimit" : 2, 52 | "Name" : "Dungeon", 53 | "StatList" : [ 54 | ["Det", 193], 55 | ["Crit", 276], 56 | ["MainStat", 345] 57 | ]}, 58 | { 59 | "GearType" : 6, 60 | "MateriaLimit" : 2, 61 | "Name" : "Raid", 62 | "StatList" : [ 63 | ["Crit", 172], 64 | ["Det", 120], 65 | ["MainStat", 211] 66 | ]}, 67 | { 68 | "GearType" : 6, 69 | "MateriaLimit" : 2, 70 | "Name" : "Dungeon", 71 | "StatList" : [ 72 | ["Ten", 174], 73 | ["Det", 122], 74 | ["MainStat", 217] 75 | ]}, 76 | { 77 | "GearType" : 7, 78 | "MateriaLimit" : 2, 79 | "Name" : "Raid", 80 | "StatList" : [ 81 | ["Crit", 95], 82 | ["Det", 136], 83 | ["MainStat", 167] 84 | ]}, 85 | { 86 | "GearType" : 8, 87 | "MateriaLimit" : 1, 88 | "Name" : "Dungeon", 89 | "StatList" : [ 90 | ["Crit", 137], 91 | ["Ten", 96], 92 | ["MainStat", 171] 93 | ]}, 94 | { 95 | "GearType" : 8, 96 | "MateriaLimit" : 2, 97 | "Name" : "Tome", 98 | "StatList" : [ 99 | ["Det", 136], 100 | ["Crit", 95], 101 | ["MainStat", 167] 102 | ]}, 103 | { 104 | "GearType" : 9, 105 | "MateriaLimit" : 2, 106 | "Name" : "Tome", 107 | "StatList" : [ 108 | ["Det", 136], 109 | ["Ten", 95], 110 | ["MainStat", 167] 111 | ]}, 112 | { 113 | "GearType" : 9, 114 | "MateriaLimit" : 2, 115 | "Name" : "Raid", 116 | "StatList" : [ 117 | ["Crit", 136], 118 | ["Ten", 95], 119 | ["MainStat", 167] 120 | ]}, 121 | { 122 | "GearType" : 10, 123 | "MateriaLimit" : 2, 124 | "Name" : "Raid", 125 | "StatList" : [ 126 | ["Det", 95], 127 | ["Ten", 136], 128 | ["MainStat", 167] 129 | ]}, 130 | { 131 | "GearType" : 10, 132 | "MateriaLimit" : 1, 133 | "Name" : "Dungeon", 134 | "StatList" : [ 135 | ["Ten", 137], 136 | ["Crit", 96], 137 | ["MainStat", 171] 138 | ]}, 139 | { 140 | "GearType" : 11, 141 | "MateriaLimit" : 2, 142 | "Name" : "Tome", 143 | "StatList" : [ 144 | ["Det", 95], 145 | ["Crit", 136], 146 | ["MainStat", 167] 147 | ]} 148 | ] -------------------------------------------------------------------------------- /ffxivcalc/GearSolver/BLMSet.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "GearType" : 0, 4 | "MateriaLimit" : 2, 5 | "Name" : "Raid", 6 | "StatList" : [ 7 | ["Crit", 306], 8 | ["SS", 214], 9 | ["MainStat", 416], 10 | ["WD", 132] 11 | ]}, 12 | { 13 | "GearType" : 0, 14 | "MateriaLimit" : 2, 15 | "Name" : "Tome", 16 | "StatList" : [ 17 | ["Crit", 212], 18 | ["SS", 303], 19 | ["MainStat", 409], 20 | ["WD", 131] 21 | ]}, 22 | { 23 | "GearType" : 2, 24 | "MateriaLimit" : 2, 25 | "Name" : "Raid", 26 | "StatList" : [ 27 | ["SS", 184], 28 | ["Det", 129], 29 | ["MainStat", 248] 30 | ]}, 31 | { 32 | "GearType" : 2, 33 | "MateriaLimit" : 2, 34 | "Name" : "Tome", 35 | "StatList" : [ 36 | ["Crit", 184], 37 | ["DH", 129], 38 | ["MainStat", 248] 39 | ]}, 40 | { 41 | "GearType" : 3, 42 | "MateriaLimit" : 2, 43 | "Name" : "Raid", 44 | "StatList" : [ 45 | ["SS", 292], 46 | ["DH", 204], 47 | ["MainStat", 394] 48 | ]}, 49 | { 50 | "GearType" : 3, 51 | "MateriaLimit" : 2, 52 | "Name" : "Tome", 53 | "StatList" : [ 54 | ["Crit", 292], 55 | ["Det", 204], 56 | ["MainStat", 394] 57 | ]}, 58 | { 59 | "GearType" : 4, 60 | "MateriaLimit" : 2, 61 | "Name" : "Raid", 62 | "StatList" : [ 63 | ["Det", 129], 64 | ["Crit", 184], 65 | ["MainStat", 248] 66 | ]}, 67 | { 68 | "GearType" : 4, 69 | "MateriaLimit" : 2, 70 | "Name" : "Tome", 71 | "StatList" : [ 72 | ["SS", 184], 73 | ["DH", 129], 74 | ["MainStat", 248] 75 | ]}, 76 | { 77 | "GearType" : 5, 78 | "MateriaLimit" : 2, 79 | "Name" : "Raid", 80 | "StatList" : [ 81 | ["Crit", 204], 82 | ["DH", 292], 83 | ["MainStat", 394] 84 | ]}, 85 | { 86 | "GearType" : 5, 87 | "MateriaLimit" : 2, 88 | "Name" : "Tome", 89 | "StatList" : [ 90 | ["Det", 292], 91 | ["SS", 204], 92 | ["MainStat", 394] 93 | ]}, 94 | { 95 | "GearType" : 6, 96 | "MateriaLimit" : 2, 97 | "Name" : "Raid", 98 | "StatList" : [ 99 | ["Det", 184], 100 | ["SS", 129], 101 | ["MainStat", 248] 102 | ]}, 103 | { 104 | "GearType" : 6, 105 | "MateriaLimit" : 2, 106 | "Name" : "Tome", 107 | "StatList" : [ 108 | ["Crit", 129], 109 | ["DH", 184], 110 | ["MainStat", 248] 111 | ]}, 112 | { 113 | "GearType" : 7, 114 | "MateriaLimit" : 2, 115 | "Name" : "Raid", 116 | "StatList" : [ 117 | ["Crit", 145], 118 | ["Det", 102], 119 | ["MainStat", 196] 120 | ]}, 121 | { 122 | "GearType" : 7, 123 | "MateriaLimit" : 2, 124 | "Name" : "Tome", 125 | "StatList" : [ 126 | ["DH", 102], 127 | ["SS", 145], 128 | ["MainStat", 196] 129 | ]}, 130 | { 131 | "GearType" : 8, 132 | "MateriaLimit" : 2, 133 | "Name" : "Raid", 134 | "StatList" : [ 135 | ["SS", 102], 136 | ["DH", 145], 137 | ["MainStat", 196] 138 | ]}, 139 | { 140 | "GearType" : 8, 141 | "MateriaLimit" : 2, 142 | "Name" : "Tome", 143 | "StatList" : [ 144 | ["Det", 145], 145 | ["Crit", 102], 146 | ["MainStat", 196] 147 | ]}, 148 | { 149 | "GearType" : 9, 150 | "MateriaLimit" : 2, 151 | "Name" : "Raid", 152 | "StatList" : [ 153 | ["Crit", 145], 154 | ["Det", 102], 155 | ["MainStat", 196] 156 | ]}, 157 | { 158 | "GearType" : 9, 159 | "MateriaLimit" : 2, 160 | "Name" : "Tome", 161 | "StatList" : [ 162 | ["Det", 145], 163 | ["SS", 102], 164 | ["MainStat", 196] 165 | ]}, 166 | { 167 | "GearType" : 10, 168 | "MateriaLimit" : 2, 169 | "Name" : "Raid", 170 | "StatList" : [ 171 | ["Det", 102], 172 | ["Crit", 145], 173 | ["MainStat", 196] 174 | ]}, 175 | { 176 | "GearType" : 11, 177 | "MateriaLimit" : 2, 178 | "Name" : "Raid", 179 | "StatList" : [ 180 | ["SS", 102], 181 | ["DH", 145], 182 | ["MainStat", 196] 183 | ]} 184 | ] -------------------------------------------------------------------------------- /ffxivcalc/GearSolver/CasterSet.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "GearType" : 0, 4 | "MateriaLimit" : 2, 5 | "Name" : "Raid", 6 | "StatList" : [ 7 | ["Crit", 306], 8 | ["DH", 214], 9 | ["MainStat", 416], 10 | ["WD", 132] 11 | ]}, 12 | { 13 | "GearType" : 0, 14 | "MateriaLimit" : 2, 15 | "Name" : "Tome", 16 | "StatList" : [ 17 | ["Crit", 212], 18 | ["Det", 303], 19 | ["MainStat", 409], 20 | ["WD", 131] 21 | ]}, 22 | { 23 | "GearType" : 2, 24 | "MateriaLimit" : 2, 25 | "Name" : "Raid", 26 | "StatList" : [ 27 | ["SS", 184], 28 | ["Det", 129], 29 | ["MainStat", 248] 30 | ]}, 31 | { 32 | "GearType" : 2, 33 | "MateriaLimit" : 2, 34 | "Name" : "Tome", 35 | "StatList" : [ 36 | ["Crit", 184], 37 | ["DH", 129], 38 | ["MainStat", 248] 39 | ]}, 40 | { 41 | "GearType" : 3, 42 | "MateriaLimit" : 2, 43 | "Name" : "Raid", 44 | "StatList" : [ 45 | ["SS", 292], 46 | ["DH", 204], 47 | ["MainStat", 394] 48 | ]}, 49 | { 50 | "GearType" : 3, 51 | "MateriaLimit" : 2, 52 | "Name" : "Tome", 53 | "StatList" : [ 54 | ["Crit", 292], 55 | ["Det", 204], 56 | ["MainStat", 394] 57 | ]}, 58 | { 59 | "GearType" : 4, 60 | "MateriaLimit" : 2, 61 | "Name" : "Raid", 62 | "StatList" : [ 63 | ["Det", 129], 64 | ["Crit", 184], 65 | ["MainStat", 248] 66 | ]}, 67 | { 68 | "GearType" : 4, 69 | "MateriaLimit" : 2, 70 | "Name" : "Tome", 71 | "StatList" : [ 72 | ["SS", 184], 73 | ["DH", 129], 74 | ["MainStat", 248] 75 | ]}, 76 | { 77 | "GearType" : 5, 78 | "MateriaLimit" : 2, 79 | "Name" : "Raid", 80 | "StatList" : [ 81 | ["Crit", 204], 82 | ["DH", 292], 83 | ["MainStat", 394] 84 | ]}, 85 | { 86 | "GearType" : 5, 87 | "MateriaLimit" : 2, 88 | "Name" : "Tome", 89 | "StatList" : [ 90 | ["Det", 292], 91 | ["SS", 204], 92 | ["MainStat", 394] 93 | ]}, 94 | { 95 | "GearType" : 6, 96 | "MateriaLimit" : 2, 97 | "Name" : "Raid", 98 | "StatList" : [ 99 | ["Det", 184], 100 | ["SS", 129], 101 | ["MainStat", 248] 102 | ]}, 103 | { 104 | "GearType" : 6, 105 | "MateriaLimit" : 2, 106 | "Name" : "Tome", 107 | "StatList" : [ 108 | ["Crit", 129], 109 | ["DH", 184], 110 | ["MainStat", 248] 111 | ]}, 112 | { 113 | "GearType" : 7, 114 | "MateriaLimit" : 2, 115 | "Name" : "Raid", 116 | "StatList" : [ 117 | ["Crit", 145], 118 | ["Det", 102], 119 | ["MainStat", 196] 120 | ]}, 121 | { 122 | "GearType" : 7, 123 | "MateriaLimit" : 2, 124 | "Name" : "Tome", 125 | "StatList" : [ 126 | ["DH", 102], 127 | ["SS", 145], 128 | ["MainStat", 196] 129 | ]}, 130 | { 131 | "GearType" : 8, 132 | "MateriaLimit" : 2, 133 | "Name" : "Raid", 134 | "StatList" : [ 135 | ["SS", 102], 136 | ["DH", 145], 137 | ["MainStat", 196] 138 | ]}, 139 | { 140 | "GearType" : 8, 141 | "MateriaLimit" : 2, 142 | "Name" : "Tome", 143 | "StatList" : [ 144 | ["Det", 145], 145 | ["Crit", 102], 146 | ["MainStat", 196] 147 | ]}, 148 | { 149 | "GearType" : 9, 150 | "MateriaLimit" : 2, 151 | "Name" : "Raid", 152 | "StatList" : [ 153 | ["Crit", 145], 154 | ["Det", 102], 155 | ["MainStat", 196] 156 | ]}, 157 | { 158 | "GearType" : 9, 159 | "MateriaLimit" : 2, 160 | "Name" : "Tome", 161 | "StatList" : [ 162 | ["Det", 145], 163 | ["SS", 102], 164 | ["MainStat", 196] 165 | ]}, 166 | { 167 | "GearType" : 10, 168 | "MateriaLimit" : 2, 169 | "Name" : "Raid", 170 | "StatList" : [ 171 | ["Det", 102], 172 | ["Crit", 145], 173 | ["MainStat", 196] 174 | ]}, 175 | { 176 | "GearType" : 11, 177 | "MateriaLimit" : 2, 178 | "Name" : "Raid", 179 | "StatList" : [ 180 | ["SS", 102], 181 | ["DH", 145], 182 | ["MainStat", 196] 183 | ]} 184 | ] -------------------------------------------------------------------------------- /ffxivcalc/GearSolver/BLMTOPSet.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "GearType" : 0, 4 | "MateriaLimit" : 0, 5 | "Name" : "Raid", 6 | "StatList" : [ 7 | ["Crit", 287], 8 | ["SS", 287], 9 | ["Det", 72], 10 | ["MainStat", 358], 11 | ["WD", 126] 12 | ]}, 13 | { 14 | "GearType" : 0, 15 | "MateriaLimit" : 0, 16 | "Name" : "Raid", 17 | "StatList" : [ 18 | ["Crit", 287], 19 | ["SS", 72], 20 | ["Det", 287], 21 | ["MainStat", 358], 22 | ["WD", 126] 23 | ]}, 24 | { 25 | "GearType" : 0, 26 | "MateriaLimit" : 0, 27 | "Name" : "Raid", 28 | "StatList" : [ 29 | ["Crit", 287], 30 | ["DH", 72], 31 | ["Det", 287], 32 | ["MainStat", 358], 33 | ["WD", 126] 34 | ]}, 35 | { 36 | "GearType" : 2, 37 | "MateriaLimit" : 2, 38 | "Name" : "Tome", 39 | "StatList" : [ 40 | ["Crit", 172], 41 | ["Det", 120], 42 | ["MainStat", 211] 43 | ]}, 44 | { 45 | "GearType" : 2, 46 | "MateriaLimit" : 2, 47 | "Name" : "Dungeon", 48 | "StatList" : [ 49 | ["Crit", 174], 50 | ["SS", 122], 51 | ["MainStat", 217] 52 | ]}, 53 | { 54 | "GearType" : 3, 55 | "MateriaLimit" : 2, 56 | "Name" : "Dungeon", 57 | "StatList" : [ 58 | ["Crit", 276], 59 | ["Det", 193], 60 | ["MainStat", 345] 61 | ]}, 62 | { 63 | "GearType" : 4, 64 | "MateriaLimit" : 2, 65 | "Name" : "Raid", 66 | "StatList" : [ 67 | ["Crit", 172], 68 | ["Det", 120], 69 | ["MainStat", 211] 70 | ]}, 71 | { 72 | "GearType" : 4, 73 | "MateriaLimit" : 2, 74 | "Name" : "Dungeon", 75 | "StatList" : [ 76 | ["SS", 174], 77 | ["Det", 122], 78 | ["MainStat", 217] 79 | ]}, 80 | { 81 | "GearType" : 5, 82 | "MateriaLimit" : 2, 83 | "Name" : "Tome", 84 | "StatList" : [ 85 | ["Crit", 273], 86 | ["Det", 191], 87 | ["MainStat", 336] 88 | ]}, 89 | { 90 | "GearType" : 5, 91 | "MateriaLimit" : 2, 92 | "Name" : "Dungeon", 93 | "StatList" : [ 94 | ["Det", 276], 95 | ["DH", 193], 96 | ["MainStat", 345] 97 | ]}, 98 | { 99 | "GearType" : 6, 100 | "MateriaLimit" : 2, 101 | "Name" : "Dungeon", 102 | "StatList" : [ 103 | ["Crit", 122], 104 | ["DH", 174], 105 | ["MainStat", 217] 106 | ]}, 107 | { 108 | "GearType" : 7, 109 | "MateriaLimit" : 2, 110 | "Name" : "Raid", 111 | "StatList" : [ 112 | ["Crit", 95], 113 | ["Det", 136], 114 | ["MainStat", 167] 115 | ]}, 116 | { 117 | "GearType" : 8, 118 | "MateriaLimit" : 2, 119 | "Name" : "Tome", 120 | "StatList" : [ 121 | ["Det", 136], 122 | ["Crit", 95], 123 | ["MainStat", 167] 124 | ]}, 125 | { 126 | "GearType" : 9, 127 | "MateriaLimit" : 2, 128 | "Name" : "Raid", 129 | "StatList" : [ 130 | ["DH", 95], 131 | ["Crit", 136], 132 | ["MainStat", 167] 133 | ]}, 134 | { 135 | "GearType" : 10, 136 | "MateriaLimit" : 2, 137 | "Name" : "Raid", 138 | "StatList" : [ 139 | ["SS", 95], 140 | ["Crit", 136], 141 | ["MainStat", 167] 142 | ]}, 143 | { 144 | "GearType" : 11, 145 | "MateriaLimit" : 2, 146 | "Name" : "Tome", 147 | "StatList" : [ 148 | ["Det", 95], 149 | ["DH", 136], 150 | ["MainStat", 167] 151 | ]} 152 | ] -------------------------------------------------------------------------------- /ffxivcalc/GearSolver/FendingGear.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "GearType" : 0, 4 | "MateriaLimit" : 2, 5 | "Name" : "Raid", 6 | "StatList" : [ 7 | ["Det", 306], 8 | ["Crit", 214], 9 | ["MainStat", 416], 10 | ["WD", 132] 11 | ]}, 12 | { 13 | "GearType" : 2, 14 | "MateriaLimit" : 2, 15 | "Name" : "Raid", 16 | "StatList" : [ 17 | ["Crit", 184], 18 | ["Det", 129], 19 | ["MainStat", 248] 20 | ]}, 21 | { 22 | "GearType" : 2, 23 | "MateriaLimit" : 2, 24 | "Name" : "Tome", 25 | "StatList" : [ 26 | ["Ten", 184], 27 | ["Crit", 129], 28 | ["MainStat", 248] 29 | ]}, 30 | { 31 | "GearType" : 3, 32 | "MateriaLimit" : 2, 33 | "Name" : "Raid", 34 | "StatList" : [ 35 | ["Ten", 292], 36 | ["Det", 204], 37 | ["MainStat", 394] 38 | ]}, 39 | { 40 | "GearType" : 3, 41 | "MateriaLimit" : 2, 42 | "Name" : "Tome", 43 | "StatList" : [ 44 | ["Crit", 292], 45 | ["Det", 204], 46 | ["MainStat", 394] 47 | ]}, 48 | { 49 | "GearType" : 4, 50 | "MateriaLimit" : 2, 51 | "Name" : "Raid", 52 | "StatList" : [ 53 | ["Crit", 184], 54 | ["Ten", 129], 55 | ["MainStat", 248] 56 | ]}, 57 | { 58 | "GearType" : 4, 59 | "MateriaLimit" : 2, 60 | "Name" : "Tome", 61 | "StatList" : [ 62 | ["SkS", 184], 63 | ["Ten", 129], 64 | ["MainStat", 248] 65 | ]}, 66 | { 67 | "GearType" : 5, 68 | "MateriaLimit" : 2, 69 | "Name" : "Raid", 70 | "StatList" : [ 71 | ["Det", 292], 72 | ["Crit", 204], 73 | ["MainStat", 394] 74 | ]}, 75 | { 76 | "GearType" : 5, 77 | "MateriaLimit" : 2, 78 | "Name" : "Tome", 79 | "StatList" : [ 80 | ["Det", 292], 81 | ["Ten", 204], 82 | ["MainStat", 394] 83 | ]}, 84 | { 85 | "GearType" : 6, 86 | "MateriaLimit" : 2, 87 | "Name" : "Raid", 88 | "StatList" : [ 89 | ["SkS", 184], 90 | ["Det", 129], 91 | ["MainStat", 248] 92 | ]}, 93 | { 94 | "GearType" : 6, 95 | "MateriaLimit" : 2, 96 | "Name" : "Tome", 97 | "StatList" : [ 98 | ["Crit", 184], 99 | ["Det", 129], 100 | ["MainStat", 248] 101 | ]}, 102 | { 103 | "GearType" : 7, 104 | "MateriaLimit" : 2, 105 | "Name" : "Raid", 106 | "StatList" : [ 107 | ["Det", 145], 108 | ["Ten", 102], 109 | ["MainStat", 196] 110 | ]}, 111 | { 112 | "GearType" : 7, 113 | "MateriaLimit" : 2, 114 | "Name" : "Tome", 115 | "StatList" : [ 116 | ["Crit", 145], 117 | ["Det", 102], 118 | ["MainStat", 196] 119 | ]}, 120 | { 121 | "GearType" : 8, 122 | "MateriaLimit" : 2, 123 | "Name" : "Raid", 124 | "StatList" : [ 125 | ["Crit", 145], 126 | ["Det", 102], 127 | ["MainStat", 196] 128 | ]}, 129 | { 130 | "GearType" : 8, 131 | "MateriaLimit" : 2, 132 | "Name" : "Tome", 133 | "StatList" : [ 134 | ["Det", 145], 135 | ["SkS", 102], 136 | ["MainStat", 196] 137 | ]}, 138 | { 139 | "GearType" : 9, 140 | "MateriaLimit" : 2, 141 | "Name" : "Raid", 142 | "StatList" : [ 143 | ["Det", 145], 144 | ["SkS", 102], 145 | ["MainStat", 196] 146 | ]}, 147 | { 148 | "GearType" : 9, 149 | "MateriaLimit" : 2, 150 | "Name" : "Tome", 151 | "StatList" : [ 152 | ["Crit", 145], 153 | ["Det", 102], 154 | ["MainStat", 196] 155 | ]}, 156 | { 157 | "GearType" : 10, 158 | "MateriaLimit" : 2, 159 | "Name" : "Raid", 160 | "StatList" : [ 161 | ["Crit", 145], 162 | ["SkS", 102], 163 | ["MainStat", 196] 164 | ]}, 165 | { 166 | "GearType" : 10, 167 | "MateriaLimit" : 2, 168 | "Name" : "Tome650", 169 | "StatList" : [ 170 | ["Det", 142], 171 | ["Crit", 99], 172 | ["MainStat", 186] 173 | ]}, 174 | { 175 | "GearType" : 11, 176 | "MateriaLimit" : 2, 177 | "Name" : "Tome", 178 | "StatList" : [ 179 | ["Det", 145], 180 | ["Crit", 102], 181 | ["MainStat", 196] 182 | ]} 183 | ] -------------------------------------------------------------------------------- /ffxivcalc/GearSolver/HealingGear.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "GearType" : 0, 4 | "MateriaLimit" : 2, 5 | "Name" : "Raid", 6 | "StatList" : [ 7 | ["Crit", 306], 8 | ["Det", 214], 9 | ["MainStat", 416], 10 | ["WD", 132] 11 | ]}, 12 | { 13 | "GearType" : 2, 14 | "MateriaLimit" : 2, 15 | "Name" : "Raid", 16 | "StatList" : [ 17 | ["Det", 184], 18 | ["SS", 129], 19 | ["MainStat", 248] 20 | ]}, 21 | { 22 | "GearType" : 2, 23 | "MateriaLimit" : 2, 24 | "Name" : "Tome", 25 | "StatList" : [ 26 | ["Det", 184], 27 | ["Crit", 129], 28 | ["MainStat", 248] 29 | ]}, 30 | { 31 | "GearType" : 3, 32 | "MateriaLimit" : 2, 33 | "Name" : "Raid", 34 | "StatList" : [ 35 | ["Crit", 292], 36 | ["Det", 204], 37 | ["MainStat", 394] 38 | ]}, 39 | { 40 | "GearType" : 3, 41 | "MateriaLimit" : 2, 42 | "Name" : "Tome", 43 | "StatList" : [ 44 | ["Piety", 292], 45 | ["Det", 204], 46 | ["MainStat", 394] 47 | ]}, 48 | { 49 | "GearType" : 4, 50 | "MateriaLimit" : 2, 51 | "Name" : "Raid", 52 | "StatList" : [ 53 | ["Det", 184], 54 | ["Piety", 129], 55 | ["MainStat", 248] 56 | ]}, 57 | { 58 | "GearType" : 4, 59 | "MateriaLimit" : 2, 60 | "Name" : "Tome", 61 | "StatList" : [ 62 | ["Crit", 184], 63 | ["Det", 129], 64 | ["MainStat", 248] 65 | ]}, 66 | { 67 | "GearType" : 5, 68 | "MateriaLimit" : 2, 69 | "Name" : "Raid", 70 | "StatList" : [ 71 | ["Piety", 292], 72 | ["Det", 204], 73 | ["MainStat", 394] 74 | ]}, 75 | { 76 | "GearType" : 5, 77 | "MateriaLimit" : 2, 78 | "Name" : "Tome", 79 | "StatList" : [ 80 | ["Crit", 292], 81 | ["Det", 204], 82 | ["MainStat", 394] 83 | ]}, 84 | { 85 | "GearType" : 6, 86 | "MateriaLimit" : 2, 87 | "Name" : "Raid", 88 | "StatList" : [ 89 | ["Crit", 184], 90 | ["Det", 129], 91 | ["MainStat", 248] 92 | ]}, 93 | { 94 | "GearType" : 6, 95 | "MateriaLimit" : 2, 96 | "Name" : "Tome", 97 | "StatList" : [ 98 | ["SS", 184], 99 | ["Piety", 129], 100 | ["MainStat", 248] 101 | ]}, 102 | { 103 | "GearType" : 7, 104 | "MateriaLimit" : 2, 105 | "Name" : "Raid", 106 | "StatList" : [ 107 | ["Crit", 145], 108 | ["Piety", 102], 109 | ["MainStat", 196] 110 | ]}, 111 | { 112 | "GearType" : 7, 113 | "MateriaLimit" : 2, 114 | "Name" : "Tome", 115 | "StatList" : [ 116 | ["Det", 145], 117 | ["SS", 102], 118 | ["MainStat", 196] 119 | ]}, 120 | { 121 | "GearType" : 8, 122 | "MateriaLimit" : 2, 123 | "Name" : "Raid", 124 | "StatList" : [ 125 | ["Det", 145], 126 | ["SS", 102], 127 | ["MainStat", 196] 128 | ]}, 129 | { 130 | "GearType" : 8, 131 | "MateriaLimit" : 2, 132 | "Name" : "Tome", 133 | "StatList" : [ 134 | ["Crit", 145], 135 | ["Piety", 102], 136 | ["MainStat", 196] 137 | ]}, 138 | { 139 | "GearType" : 9, 140 | "MateriaLimit" : 2, 141 | "Name" : "Raid", 142 | "StatList" : [ 143 | ["Crit", 145], 144 | ["Det", 102], 145 | ["MainStat", 196] 146 | ]}, 147 | { 148 | "GearType" : 9, 149 | "MateriaLimit" : 2, 150 | "Name" : "Tome", 151 | "StatList" : [ 152 | ["Det", 145], 153 | ["Piety", 102], 154 | ["MainStat", 196] 155 | ]}, 156 | { 157 | "GearType" : 10, 158 | "MateriaLimit" : 2, 159 | "Name" : "Raid", 160 | "StatList" : [ 161 | ["Piety", 145], 162 | ["Crit", 102], 163 | ["MainStat", 196] 164 | ]}, 165 | { 166 | "GearType" : 10, 167 | "MateriaLimit" : 2, 168 | "Name" : "Tome650", 169 | "StatList" : [ 170 | ["Crit", 142], 171 | ["Det", 99], 172 | ["MainStat", 186] 173 | ]}, 174 | { 175 | "GearType" : 11, 176 | "MateriaLimit" : 2, 177 | "Name" : "Tome", 178 | "StatList" : [ 179 | ["Crit", 145], 180 | ["Det", 102], 181 | ["MainStat", 196] 182 | ]} 183 | ] -------------------------------------------------------------------------------- /ffxivcalc/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.8.940' 2 | 3 | """ 4 | Update history : 5 | 6 | 0.9.940: 7 | - Oups, forgot to ever implement Army Ethos/Army Muse effect on Bard. This is now fixed. 8 | - Other bug fixes. 9 | 10 | 0.8.930: 11 | - Added Player.getPlayerPrePullTime() which returns the length of the prepull (time before first damage action). 12 | - Added Fight.getPlayerPrepullLength() which returns the prepull length of all players 13 | - added Fight.syncPlayerPrePull() which syncs all player's prepull so they all do damage at the same time. 14 | - Added test suite 'Prepull length test suite' that tests the Player.GetPlayerPrePullTime() function. 15 | - Removed access to ffxvivcalc.API, ffxivcalc.codeParser and ffxivcalc.Request.FFLogsAPIRequest modules (outdated modules). 16 | - Other minor bug fix. 17 | 18 | 0.8.925: 19 | - getSingleFightData() can take 'max_time' input. 20 | - _aura_prepull_logic() is called in getSingleFightData() instead of RestoreFightObject. So prepull are now put directly into JSON file exported. 21 | - Fixed some other issues 22 | 23 | 0.8.920: 24 | - Added code to import fights from FFlogs (big thanks to Apollo for the base code). 25 | - Added FFLogs test suite. 26 | - Discontinued FFLogsAPIRequest 27 | - Some bug fixes and other smaller improvements. 28 | 29 | 0.8.911: 30 | - Fixed Bard's Mine issues 31 | - Added functionality to filter the record under some restrictions. 32 | - Fixed technical finish issue where a double cast would make the simulation crash 33 | - Fixed GivenHealBuff issue 34 | - Graph from the simulation now save as png instead of opening up. 35 | 36 | 0.8.910: 37 | - Fixed some 'computeActionTimer()' issues 38 | - Added Limit break actions (they do no damage but should be accurate on cast time/animation lock) 39 | - Fixed some wrong Ids issue 40 | - Fixed Huraijin ninja issue 41 | - Added Limit break cast time tests for SpS/SkS interactions. 42 | 43 | 0.8.901 : 44 | - Fixed some 'computeActionTimer()' issues on Samurai with Meikyo/Gekko interaction, an issue where only having 1 GCD would return wrong estimated time stamp. 45 | 46 | 0.8.900 : 47 | - Added 'computeActionTimer()' which returns an estimate of a player's timeStamp/DOT timer/buf timer. 48 | This function can be called on a player object and will return a dictionnary containing the estimates. 49 | - Added tests to test 'computeActionTimer' functionality. 50 | - Fixed mitigation amount issue (higher than 1 values) (PR by Apollo) 51 | - Fixed Samurai Higanbana DOT potency (was 60, fixed to 45) (PR by Apollo) 52 | - Moved some gcdTiming computation to the Player object (see Fight.py) 53 | - Removed lock in various places 54 | 55 | 0.8.800 : 56 | - Made changes to work with web app 57 | - Added ways to remove pB or redirect the output 58 | - Fixed etro import issues 59 | - BiSSolver now makes a deepcopy of the gear space so it can be reused 60 | - Fixed pb issues, fixed fight import issues. 61 | - Added 'ShowProgress' option to 'SimulateFight()' that lets enables the print of the loading bar 62 | - Various other bug fixes 63 | 64 | 0.8.700 : 65 | - Made PyPi package. 66 | 67 | 0.8.60 : 68 | - Added more accurate AA, weapon delay, haste affecting delay. 69 | - Fixed DOT/AA issues 70 | - Added AA delay, GCD timer and more DOT tests. 71 | - Added cli "tests" command to run test suites. 72 | 73 | 74 | """ 75 | -------------------------------------------------------------------------------- /ffxivcalc/GearSolver/PaladinGear.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "GearType" : 0, 4 | "MateriaLimit" : 2, 5 | "Name" : "Raid", 6 | "StatList" : [ 7 | ["Det", 306], 8 | ["Crit", 214], 9 | ["MainStat", 416], 10 | ["WD", 132] 11 | ], 12 | "customStatLimit" : 250}, 13 | { 14 | "GearType" : 2, 15 | "MateriaLimit" : 2, 16 | "Name" : "Raid", 17 | "StatList" : [ 18 | ["Crit", 184], 19 | ["Det", 129], 20 | ["MainStat", 248] 21 | ]}, 22 | { 23 | "GearType" : 2, 24 | "MateriaLimit" : 2, 25 | "Name" : "Tome", 26 | "StatList" : [ 27 | ["Ten", 184], 28 | ["Crit", 129], 29 | ["MainStat", 248] 30 | ]}, 31 | { 32 | "GearType" : 3, 33 | "MateriaLimit" : 2, 34 | "Name" : "Raid", 35 | "StatList" : [ 36 | ["Ten", 292], 37 | ["Det", 204], 38 | ["MainStat", 394] 39 | ]}, 40 | { 41 | "GearType" : 3, 42 | "MateriaLimit" : 2, 43 | "Name" : "Tome", 44 | "StatList" : [ 45 | ["Crit", 292], 46 | ["Det", 204], 47 | ["MainStat", 394] 48 | ]}, 49 | { 50 | "GearType" : 4, 51 | "MateriaLimit" : 2, 52 | "Name" : "Raid", 53 | "StatList" : [ 54 | ["Crit", 184], 55 | ["Ten", 129], 56 | ["MainStat", 248] 57 | ]}, 58 | { 59 | "GearType" : 4, 60 | "MateriaLimit" : 2, 61 | "Name" : "Tome", 62 | "StatList" : [ 63 | ["SkS", 184], 64 | ["Ten", 129], 65 | ["MainStat", 248] 66 | ]}, 67 | { 68 | "GearType" : 5, 69 | "MateriaLimit" : 2, 70 | "Name" : "Raid", 71 | "StatList" : [ 72 | ["Det", 292], 73 | ["Crit", 204], 74 | ["MainStat", 394] 75 | ]}, 76 | { 77 | "GearType" : 5, 78 | "MateriaLimit" : 2, 79 | "Name" : "Tome", 80 | "StatList" : [ 81 | ["Det", 292], 82 | ["Ten", 204], 83 | ["MainStat", 394] 84 | ]}, 85 | { 86 | "GearType" : 6, 87 | "MateriaLimit" : 2, 88 | "Name" : "Raid", 89 | "StatList" : [ 90 | ["SkS", 184], 91 | ["Det", 129], 92 | ["MainStat", 248] 93 | ]}, 94 | { 95 | "GearType" : 6, 96 | "MateriaLimit" : 2, 97 | "Name" : "Tome", 98 | "StatList" : [ 99 | ["Crit", 184], 100 | ["Det", 129], 101 | ["MainStat", 248] 102 | ]}, 103 | { 104 | "GearType" : 7, 105 | "MateriaLimit" : 2, 106 | "Name" : "Raid", 107 | "StatList" : [ 108 | ["Det", 145], 109 | ["Ten", 102], 110 | ["MainStat", 196] 111 | ]}, 112 | { 113 | "GearType" : 7, 114 | "MateriaLimit" : 2, 115 | "Name" : "Tome", 116 | "StatList" : [ 117 | ["Crit", 145], 118 | ["Det", 102], 119 | ["MainStat", 196] 120 | ]}, 121 | { 122 | "GearType" : 8, 123 | "MateriaLimit" : 2, 124 | "Name" : "Raid", 125 | "StatList" : [ 126 | ["Crit", 145], 127 | ["Det", 102], 128 | ["MainStat", 196] 129 | ]}, 130 | { 131 | "GearType" : 8, 132 | "MateriaLimit" : 2, 133 | "Name" : "Tome", 134 | "StatList" : [ 135 | ["Det", 145], 136 | ["SkS", 102], 137 | ["MainStat", 196] 138 | ]}, 139 | { 140 | "GearType" : 9, 141 | "MateriaLimit" : 2, 142 | "Name" : "Raid", 143 | "StatList" : [ 144 | ["Det", 145], 145 | ["SkS", 102], 146 | ["MainStat", 196] 147 | ]}, 148 | { 149 | "GearType" : 9, 150 | "MateriaLimit" : 2, 151 | "Name" : "Tome", 152 | "StatList" : [ 153 | ["Crit", 145], 154 | ["Det", 102], 155 | ["MainStat", 196] 156 | ]}, 157 | { 158 | "GearType" : 10, 159 | "MateriaLimit" : 2, 160 | "Name" : "Raid", 161 | "StatList" : [ 162 | ["Crit", 145], 163 | ["SkS", 102], 164 | ["MainStat", 196] 165 | ]}, 166 | { 167 | "GearType" : 10, 168 | "MateriaLimit" : 2, 169 | "Name" : "Tome650", 170 | "StatList" : [ 171 | ["Det", 142], 172 | ["Crit", 99], 173 | ["MainStat", 186] 174 | ]}, 175 | { 176 | "GearType" : 11, 177 | "MateriaLimit" : 2, 178 | "Name" : "Tome", 179 | "StatList" : [ 180 | ["Det", 145], 181 | ["Crit", 102], 182 | ["MainStat", 196] 183 | ]} 184 | ] -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Healer/Healer_Spell.py: -------------------------------------------------------------------------------- 1 | from ffxivcalc.Jobs.Base_Spell import ManaRequirement, Potion, Spell, empty 2 | from ffxivcalc.Jobs.Caster.Caster_Spell import LucidDreaming, Surecast, Swiftcast 3 | Lock = 0.75 4 | class HealerSpell(Spell): 5 | 6 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = 0, AOEHeal = False, TargetHeal = False): 7 | super().__init__(id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = type, AOEHeal = AOEHeal, TargetHeal=TargetHeal) 8 | 9 | 10 | ######################################### 11 | ########## SCHOLAR SPELL ############### 12 | ######################################### 13 | 14 | class ScholarSpell(HealerSpell): 15 | 16 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = 0, AOEHeal = False, TargetHeal = False): 17 | super().__init__(id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = type, AOEHeal = AOEHeal, TargetHeal=TargetHeal) 18 | 19 | 20 | ######################################### 21 | ########## WHITEMAGE SPELL ############## 22 | ######################################### 23 | 24 | 25 | class WhitemageSpell(HealerSpell): 26 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = 0, AOEHeal = False, TargetHeal = False): 27 | super().__init__(id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = type, AOEHeal = AOEHeal, TargetHeal=TargetHeal) 28 | 29 | ######################################### 30 | ########## ASTROLOGIAN SPELL ############ 31 | ######################################### 32 | 33 | 34 | class AstrologianSpell(HealerSpell): 35 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = 0, AOEHeal = False, TargetHeal = False): 36 | super().__init__(id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = type, AOEHeal = AOEHeal, TargetHeal=TargetHeal) 37 | 38 | ######################################### 39 | ########## SAGE SPELL ########### 40 | ######################################### 41 | 42 | 43 | class SageSpell(HealerSpell): 44 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = 0, AOEHeal = False, TargetHeal = False): 45 | super().__init__(id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = type, AOEHeal = AOEHeal, TargetHeal=TargetHeal) 46 | 47 | 48 | 49 | #Class Action spell 50 | 51 | #Requirement 52 | def RescueRequirement(Player, Spell): 53 | return Player.RescueCD <= 0, Player.RescueCD 54 | 55 | #Apply 56 | 57 | def ApplyRescue(Player, Enemy): 58 | Player.RescueCD = 120 59 | 60 | #Swiftcast, Surecast and LucidDreaming are in Caster_Spell.py 61 | Repose = HealerSpell(16560, True, 2.5, 2.5, 0, 600, empty, [ManaRequirement]) 62 | Esuna = HealerSpell(7568, True, 1, 2.5, 0, 400, empty, [ManaRequirement]) 63 | Rescue = HealerSpell(7571, False, 0, 0, 0, 0, ApplyRescue, [RescueRequirement]) 64 | 65 | # Limit Break actions 66 | 67 | LB1Timer = 4.10 68 | LB2Timer = 7.13 69 | LB3Timer = 10.10 70 | 71 | HealerLB1 = HealerSpell(1111, False,LB1Timer, LB1Timer, 0, 0, empty, [], type=3) 72 | HealerLB2 = HealerSpell(1112, False,LB2Timer, LB2Timer, 0, 0, empty, [], type=3) 73 | HealerLB3 = HealerSpell(1113, False,LB3Timer, LB3Timer, 0, 0, empty, [], type=3) 74 | 75 | HealerAbility = { 76 | 16560 : Repose, 77 | 7568 : Esuna, 78 | 7571 : Rescue, 79 | 7561 : Swiftcast, 80 | 7562 : LucidDreaming, 81 | 7559 : Surecast, 82 | 34590545 : Potion, 83 | 34592399 : Potion, 84 | 34594163 : Potion, 85 | -2 : Potion, 86 | 1111 : HealerLB1, 87 | 1112 : HealerLB2, 88 | 1113 : HealerLB3 89 | } -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Ranged/Ranged_Spell.py: -------------------------------------------------------------------------------- 1 | from ffxivcalc.Jobs.Base_Spell import Potion, Spell, empty 2 | from ffxivcalc.Jobs.Melee.Melee_Spell import ArmLength, SecondWind 3 | Lock = 0 4 | class RangedSpell(Spell): 5 | 6 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = 0): 7 | super().__init__(id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = type) 8 | 9 | 10 | ######################################### 11 | ########## MACHINIST SPELL ############# 12 | ######################################### 13 | 14 | 15 | class MachinistSpell(RangedSpell): 16 | 17 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, WeaponSkill, type = 0): 18 | super().__init__(id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = type) 19 | 20 | self.Weaponskill = WeaponSkill #Boolean Variable 21 | 22 | 23 | ######################################### 24 | ####### BARD SPELL ############# 25 | ######################################### 26 | 27 | 28 | class BardSpell(RangedSpell): 29 | 30 | def __init__(self, id, GCD, RecastTime, Potency, Effect, Requirement, Weaponskill, type = 0): 31 | super().__init__(id, GCD, Lock, RecastTime, Potency, 0, Effect, Requirement, type = type) 32 | 33 | self.Weaponskill = Weaponskill 34 | 35 | ######################################### 36 | ######### DANCER SPELL ########### 37 | ######################################### 38 | 39 | 40 | class DancerSpell(RangedSpell): 41 | 42 | def __init__(self, id, GCD, RecastTime, Potency, Effect, Requirement, Weaponskill, type = 0): 43 | super().__init__(id, GCD, Lock, RecastTime, Potency, 0, Effect, Requirement, type = type) 44 | self.Weaponskill = Weaponskill 45 | 46 | 47 | #Class Action 48 | 49 | #Requirement 50 | 51 | def LegGrazeRequirement(Player, Spell): 52 | return Player.LegGrazeCD <= 0, Player.LegGrazeCD 53 | 54 | def FootGrazeRequirement(Player, Spell): 55 | return Player.FootGrazeCD <= 0, Player.FootGrazeCD 56 | 57 | def PelotonRequirement(Player, Spell): 58 | return Player.PelotonCD <= 0, Player.PelotonCD 59 | 60 | def HeadGrazeRequirement(Player, Spell): 61 | return Player.HeadGrazeCD <= 0, Player.HeadGrazeCD 62 | 63 | #Apply 64 | 65 | def ApplyLegGraze(Player, Spell): 66 | Player.LegGrazeCD = 30 67 | 68 | def ApplyFootGraze(Player, Spell): 69 | Player.FootGrazeCD = 30 70 | 71 | def ApplyPeloton(Player, Spell): 72 | Player.PelotonCD = 5 73 | 74 | def ApplyHeadGraze(Player, Spell): 75 | Player.HeadGrazeCD = 30 76 | 77 | 78 | #ArmLength and SecondWind are in Melee_Spell.py 79 | LegGraze = RangedSpell(0, False, Lock, 0, 0, 0, ApplyLegGraze, [LegGrazeRequirement]) 80 | FootGraze = RangedSpell(0, False, Lock, 0, 0, 0, ApplyFootGraze, [FootGrazeRequirement]) 81 | Peloton = RangedSpell(0, False, Lock, 0, 0, 0, ApplyPeloton, [PelotonRequirement]) 82 | HeadGraze = RangedSpell(0, False, Lock, 0, 0, 0, ApplyHeadGraze, [HeadGrazeRequirement]) 83 | 84 | # Limit Break actions 85 | 86 | LB1Timer = 5.10 87 | LB2Timer = 6.10 88 | LB3Timer = 8.20 89 | 90 | RangedLB1 = RangedSpell(1111, False,LB1Timer, LB1Timer, 0, 0, empty, [], type=3) 91 | RangedLB2 = RangedSpell(1112, False,LB2Timer, LB2Timer, 0, 0, empty, [], type=3) 92 | RangedLB3 = RangedSpell(1113, False,LB3Timer, LB3Timer, 0, 0, empty, [], type=3) 93 | 94 | RangedAbility = { 95 | 7554 : LegGraze, 96 | 7553 : FootGraze, 97 | 7551: HeadGraze, 98 | 7557 : Peloton, 99 | 7541 : SecondWind, 100 | 7548 : ArmLength, 101 | 34590542 : Potion, 102 | 34592396 : Potion, 103 | 34594160 : Potion, # DEX pot ? 104 | -2 : Potion, 105 | 1111 : RangedLB1, 106 | 1112 : RangedLB2, 107 | 1113 : RangedLB3 108 | } -------------------------------------------------------------------------------- /ffxivcalc/__main__.py: -------------------------------------------------------------------------------- 1 | """CLI entrypoint for FFXIV-Combat-Simulator""" 2 | from argparse import ArgumentParser 3 | from pathlib import Path 4 | from sys import exit 5 | import logging 6 | 7 | from ffxivcalc import __name__ as prog 8 | from ffxivcalc.UI.SimulationInput import ExecuteMemoryCode as fight_main 9 | from ffxivcalc.Tester.testImplementation import executeTests 10 | from ffxivcalc.UI.TUI import TUI_draw 11 | 12 | __logger__ = logging.getLogger("ffxivcalc") # root logger 13 | 14 | def get_parser() -> ArgumentParser: 15 | """Defines all the cli arguments to be parsed 16 | :return: An ArgumentParser object 17 | """ 18 | verbose_help = " -v (Displays on terminal logging at level ERROR), -vv (Displays on terminal logging at level WARNING), -vvv (Creates a log file that has logging level DEBUG)" 19 | parser = ArgumentParser(prog=prog) 20 | parser.add_argument('-v', '--verbose', action='count', default=0, help=verbose_help) 21 | subparsers = parser.add_subparsers(help='action to perform', dest='action') 22 | 23 | # Running in code simulation 24 | simulation_parser = subparsers.add_parser('simulate') 25 | simulation_parser.add_argument('-code_simulation', '--code-simulation', type=bool, default=False) 26 | simulation_parser.add_argument('-s', '--step-size', type=float, default=0.01) 27 | simulation_parser.add_argument('-l', '--time-limit', type=int, default=1000) 28 | 29 | # Running tester 30 | test_parser = subparsers.add_parser('tests') 31 | seed_help = "-s XXXXXX (sets the given seed for the execution of the tests. Relevant to DOT and AA test suite. Sets the seed for both the DOT and AA test suite.)" 32 | test_parser.add_argument('-s', '--seed', type=int, default=0, help=seed_help) # argument to specify seed for DOT and aa test suite. 33 | # The same seed is given to both test if specified. 34 | name_help = "-n XXXXX (Name of a specific test suite to run. If not specified runs all the test suites. If invalid name returns an error).\nA valid name is the 3 letters of the test suite's name." 35 | test_parser.add_argument('-n', '--name', type=str, default="", help=name_help) # Specifies te name of the test suites we want to run. 36 | 37 | # Running tui 38 | subparsers.add_parser('tui') 39 | 40 | return parser 41 | 42 | 43 | def main() -> int: 44 | """ 45 | Entrypoint into the cli. Can be called with ``python -m ffxivcalc`` 46 | :return: Return code, for use with cli programs to indicate success/failure 47 | """ 48 | parser = get_parser() 49 | args = parser.parse_args() 50 | level = logging.ERROR 51 | 52 | if args.verbose > 0: 53 | match args.verbose: 54 | case 1: # displays error and above 55 | level = logging.ERROR 56 | logging.basicConfig(format='[%(levelname)s] %(name)s : %(message)s',level=level) 57 | case 2: # displays warning and above 58 | level = logging.WARNING 59 | logging.basicConfig(format='[%(levelname)s] %(name)s : %(message)s',level=level) 60 | case 3: # saves everything to a save file 61 | level = logging.DEBUG 62 | logging.basicConfig(format='[%(levelname)s] %(name)s : %(message)s',filename='ffxivcalc_log.log', encoding='utf-8',level=level) 63 | case _: # more than 3 V is too much, save as case 3 64 | level = logging.DEBUG 65 | logging.basicConfig(format='[%(levelname)s] %(name)s : %(message)s',filename='ffxivcalc_log.log', encoding='utf-8',level=level) 66 | __logger__.setLevel(level=level) 67 | 68 | match args.action: 69 | case 'simulate': 70 | fight_main(False, time_unit=args.step_size, TimeLimit=args.time_limit) 71 | case 'tests': 72 | executeTests(setSeed=args.seed, testSuiteName=args.name,level=level) 73 | case 'tui': 74 | TUI_draw() 75 | case _: # default case 76 | parser.print_help() 77 | 78 | return 0 79 | 80 | 81 | if __name__ == '__main__': 82 | exit(main()) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python ### 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # Ignoring Folders 8 | saved/ 9 | OldStuff/ 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 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 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # JSON files 87 | .json 88 | 89 | # txt files 90 | .txt 91 | 92 | # IPython 93 | profile_default/ 94 | ipython_config.py 95 | 96 | # pyenv 97 | # For a library or package, you might want to ignore these files since the code is 98 | # intended to run in multiple environments; otherwise, check them in: 99 | # .python-version 100 | 101 | # pipenv 102 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 103 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 104 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 105 | # install all needed dependencies. 106 | #Pipfile.lock 107 | 108 | # poetry 109 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 110 | # This is especially recommended for binary packages to ensure reproducibility, and is more 111 | # commonly ignored for libraries. 112 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 113 | #poetry.lock 114 | 115 | # pdm 116 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 117 | #pdm.lock 118 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 119 | # in version control. 120 | # https://pdm.fming.dev/#use-with-ide 121 | .pdm.toml 122 | 123 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 124 | __pypackages__/ 125 | 126 | # Celery stuff 127 | celerybeat-schedule 128 | celerybeat.pid 129 | 130 | # SageMath parsed files 131 | *.sage.py 132 | 133 | # Environments 134 | .env 135 | .venv 136 | env/ 137 | venv/ 138 | ENV/ 139 | env.bak/ 140 | venv.bak/ 141 | 142 | # Spyder project settings 143 | .spyderproject 144 | .spyproject 145 | 146 | # Rope project settings 147 | .ropeproject 148 | 149 | # mkdocs documentation 150 | /site 151 | 152 | # mypy 153 | .mypy_cache/ 154 | .dmypy.json 155 | dmypy.json 156 | 157 | # Pyre type checker 158 | .pyre/ 159 | 160 | # pytype static type analyzer 161 | .pytype/ 162 | 163 | # Cython debug symbols 164 | cython_debug/ 165 | 166 | # PyCharm 167 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 168 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 169 | # and can be added to the global gitignore or merged into this file. For a more nuclear 170 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 171 | #.idea/ 172 | .vscode/launch.json 173 | 174 | SimulationRecord.pdf 175 | 176 | *.pdf 177 | 178 | sps.py 179 | -------------------------------------------------------------------------------- /ffxivcalc/GearSolver/strikingDSR.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "GearType" : 0, 4 | "MateriaLimit" : 0, 5 | "Name" : "Relic Crit=Det", 6 | "StatList" : [ 7 | ["Crit", 269], 8 | ["Det", 269], 9 | ["DH", 72], 10 | ["MainStat", 304], 11 | ["WD", 120] 12 | ]}, 13 | { 14 | "GearType" : 0, 15 | "MateriaLimit" : 0, 16 | "Name" : "Relic Crit = DH", 17 | "StatList" : [ 18 | ["Crit", 269], 19 | ["DH", 269], 20 | ["Det", 72], 21 | ["MainStat", 304], 22 | ["WD", 120] 23 | ]}, 24 | { 25 | "GearType" : 2, 26 | "MateriaLimit" : 2, 27 | "Name" : "Raid", 28 | "StatList" : [ 29 | ["Crit", 162], 30 | ["SkS", 113], 31 | ["MainStat", 180] 32 | ]}, 33 | { 34 | "GearType" : 3, 35 | "MateriaLimit" : 2, 36 | "Name" : "Raid", 37 | "StatList" : [ 38 | ["Det", 257], 39 | ["Crit", 180], 40 | ["MainStat", 285] 41 | ]}, 42 | { 43 | "GearType" : 3, 44 | "MateriaLimit" : 2, 45 | "Name" : "Tome", 46 | "StatList" : [ 47 | ["Det", 180], 48 | ["DH", 257], 49 | ["MainStat", 285] 50 | ]}, 51 | { 52 | "GearType" : 3, 53 | "MateriaLimit" : 2, 54 | "Name" : "Dungeon", 55 | "StatList" : [ 56 | ["Crit", 259], 57 | ["DH", 181], 58 | ["MainStat", 293] 59 | ]}, 60 | { 61 | "GearType" : 4, 62 | "MateriaLimit" : 2, 63 | "Name" : "Tome", 64 | "StatList" : [ 65 | ["Crit", 162], 66 | ["Det", 113], 67 | ["MainStat", 180] 68 | ]}, 69 | { 70 | "GearType" : 4, 71 | "MateriaLimit" : 2, 72 | "Name" : "Dungeon", 73 | "StatList" : [ 74 | ["Det", 163], 75 | ["DH", 114], 76 | ["MainStat", 185] 77 | ]}, 78 | { 79 | "GearType" : 5, 80 | "MateriaLimit" : 2, 81 | "Name" : "Raid", 82 | "StatList" : [ 83 | ["Det", 180], 84 | ["DH", 257], 85 | ["MainStat", 285] 86 | ]}, 87 | { 88 | "GearType" : 5, 89 | "MateriaLimit" : 2, 90 | "Name" : "Dungeon", 91 | "StatList" : [ 92 | ["Crit", 259], 93 | ["Det", 181], 94 | ["MainStat", 285] 95 | ]}, 96 | { 97 | "GearType" : 5, 98 | "MateriaLimit" : 2, 99 | "Name" : "Tome", 100 | "StatList" : [ 101 | ["Crit", 180], 102 | ["Det", 257], 103 | ["MainStat", 285] 104 | ]}, 105 | { 106 | "GearType" : 6, 107 | "MateriaLimit" : 2, 108 | "Name" : "Raid", 109 | "StatList" : [ 110 | ["Crit", 162], 111 | ["DH", 113], 112 | ["MainStat", 180] 113 | ]}, 114 | { 115 | "GearType" : 6, 116 | "MateriaLimit" : 2, 117 | "Name" : "Dungeon", 118 | "StatList" : [ 119 | ["Det", 163], 120 | ["SkS", 114], 121 | ["MainStat", 185] 122 | ]}, 123 | { 124 | "GearType" : 6, 125 | "MateriaLimit" : 2, 126 | "Name" : "Tome", 127 | "StatList" : [ 128 | ["DH", 113], 129 | ["SkS", 162], 130 | ["MainStat", 180] 131 | ]}, 132 | { 133 | "GearType" : 7, 134 | "MateriaLimit" : 2, 135 | "Name" : "Raid", 136 | "StatList" : [ 137 | ["Crit", 127], 138 | ["Det", 89], 139 | ["MainStat", 142] 140 | ]}, 141 | { 142 | "GearType" : 8, 143 | "MateriaLimit" : 2, 144 | "Name" : "Raid", 145 | "StatList" : [ 146 | ["SkS", 89], 147 | ["Crit", 127], 148 | ["MainStat", 142] 149 | ]}, 150 | { 151 | "GearType" : 8, 152 | "MateriaLimit" : 2, 153 | "Name" : "Tome", 154 | "StatList" : [ 155 | ["Crit", 89], 156 | ["Det", 127], 157 | ["MainStat", 142] 158 | ]}, 159 | { 160 | "GearType" : 9, 161 | "MateriaLimit" : 2, 162 | "Name" : "Raid", 163 | "StatList" : [ 164 | ["Crit", 89], 165 | ["DH", 127], 166 | ["MainStat", 142] 167 | ]}, 168 | { 169 | "GearType" : 10, 170 | "MateriaLimit" : 2, 171 | "Name" : "Raid", 172 | "StatList" : [ 173 | ["Det", 89], 174 | ["DH", 127], 175 | ["MainStat", 142] 176 | ]}, 177 | { 178 | "GearType" : 11, 179 | "MateriaLimit" : 2, 180 | "Name" : "Tome", 181 | "StatList" : [ 182 | ["Crit", 127], 183 | ["Det", 89], 184 | ["MainStat", 142] 185 | ]} 186 | ] -------------------------------------------------------------------------------- /ffxivcalc/helperCode/Progress.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | from ffxivcalc.helperCode.helper_math import roundDown 3 | 4 | class ProgressBar: 5 | """ 6 | This class can be used to represent a progress bar that the user will see. 7 | You cannot use the __init__ method of the class, you must use the class method init() 8 | in order to get an iter(ProgressBar). To increment it simply use next(ProgressBar) and it will 9 | display itself. 10 | total : int -> total number of iterations 11 | name : str -> Name of the progress bar 12 | currentState : str -> Last generated frame of the progress bar 13 | extraBuffer : -> place to write the PB too 14 | """ 15 | def __init__(self): 16 | self.total = 0 17 | self.currentProgress = 0 18 | self.name = "" 19 | self.currentState = "" 20 | self.extraBuffer = None 21 | self.showBar = True 22 | 23 | # Timing related fields 24 | self.iterationAverage = 0 25 | self.sumIterations = 0 26 | self.lastIterTime = 0 27 | self.lenLoadBar = 0 28 | 29 | def getCurrentState(self): 30 | return self.currentState 31 | 32 | def setShowBar(self, val): 33 | self.showBar = val 34 | 35 | def setExtraBuffer(self, buff): 36 | self.extraBuffer = buff 37 | 38 | def __iter__(self): 39 | self.currentProgress = -1 40 | self.name = self.name 41 | return self 42 | 43 | def __next__(self): 44 | """ 45 | This function updates the progress of the bar and updates the display of the bar. 46 | """ 47 | self.currentProgress += 1 48 | percent = int((self.currentProgress/self.total)*1000)/10 49 | 50 | 51 | curTimeIteration = time() - self.lastIterTime 52 | self.lastIterTime = time() 53 | self.sumIterations += curTimeIteration 54 | self.iterationAverage = round(self.sumIterations/self.currentProgress,1) if self.currentProgress > 0 else self.sumIterations 55 | 56 | predictedTime = round(self.iterationAverage * (self.total-self.currentProgress),1) 57 | 58 | bar = "❚" * int(percent) + "-" * (100-int(percent)) 59 | 60 | loadBar = "\r"+ self.name +" |"+bar+"| " + ((str(percent) + " %") if self.currentProgress > 0 else "") + " ETA : " + str(predictedTime) + "s" 61 | self.currentState = loadBar 62 | if self.extraBuffer != None: self.extraBuffer['pb'] = loadBar 63 | 64 | 65 | # This will remove characters that are not supposed to be there anymore 66 | if len(loadBar) > self.lenLoadBar: self.lenLoadBar = len(loadBar) 67 | for i in range(len(loadBar), self.lenLoadBar): loadBar += " " 68 | # only print on screen if showBar is true 69 | if self.showBar: print(loadBar, end="\r") 70 | if (self.total - self.currentProgress == 0) and self.showBar:print() 71 | return self 72 | 73 | def complete(self): 74 | """ 75 | This function will terminate the progress bar 76 | """ 77 | self.currentProgress = self.total-1 78 | next(self) 79 | 80 | def setName(self, newName : str): 81 | """Sets the name of the progress bar to the newly given name 82 | """ 83 | self.name = newName 84 | 85 | @classmethod 86 | def init(self, total : int, name : str, showBar : bool = True, extraBuffer = None): 87 | """ 88 | This class method returns an iterator of the progress bar. 89 | 90 | total : int -> Total amount of iterations 91 | name : string -> name of loading bar (that is displayed) 92 | showBar : bool -> If true displays the progress bar using print() 93 | extraBuffer -> Dictionnary in which the pb view will be written at the key 'pb' 94 | """ 95 | newProgressBar = ProgressBar() 96 | newProgressBar.total = total 97 | newProgressBar.name = name 98 | newProgressBar.lastIterTime = time() 99 | iterator = iter(newProgressBar) 100 | iterator.setExtraBuffer(extraBuffer) 101 | iterator.setShowBar(showBar) 102 | next(iterator) 103 | return iterator 104 | 105 | 106 | if __name__ == "__main__": 107 | pass -------------------------------------------------------------------------------- /ffxivcalc/codeParser/parser.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains the parser will translate code in a way the simulator can use. 3 | 4 | 5 | The language contains the following : 6 | 7 | SET fieldName Value -> This will set the given fieldName to a given value. Type checking is done on the value that was previously there 8 | and an error will be thrown if type does not match. 9 | FORCESET fieldName Value -> Same as SET, but does not do type check and forces a new value on the field. 10 | 11 | Every command must be delimited by a ; . Invalid text within ; will be ignored. 12 | """ 13 | 14 | from copy import deepcopy 15 | 16 | commandList = ["SET", "FORCESET"] 17 | 18 | commandExpectedInputAmount = {"SET" : 2, "FORCESET" : 2} 19 | 20 | class parser: 21 | 22 | def __init__(self): 23 | pass 24 | 25 | def cleanCommandComponent(self, commandComponent : list) -> bool: 26 | """ 27 | This command cleans up the given commandComponent and returns True if the command is valid. 28 | """ 29 | # Removing empty parts that might have been caused by missinputs 30 | commandComponent = self.filterEmpty(commandComponent) 31 | 32 | # Check if initial entry is a valid command in commandList 33 | if not (commandComponent[0] in commandList) : return [],False 34 | 35 | return commandComponent, len(commandComponent[1:]) == commandExpectedInputAmount[commandComponent[0]] 36 | 37 | def filterEmpty(self, input): 38 | """ 39 | This command filters the empty index of a list. 40 | """ 41 | 42 | def emptyInput(s : str) -> bool: 43 | return len(s) != 0 44 | 45 | newInput = [] 46 | 47 | for x in filter(emptyInput, input): newInput.append(x) 48 | 49 | return newInput 50 | 51 | def parseString(self, code : str) -> dict: 52 | """ 53 | This function returns a dictionnary with the name of every fields as key and the value we want to set 54 | this field as. This does not type check and simply reads and output. 55 | """ 56 | # This list contains all command formatted in a way the simulator can understand. 57 | commandListCompiled = [] 58 | 59 | # Seperating by semi-colons (;) 60 | commandList = code.split(";") 61 | 62 | commandList = self.filterEmpty(commandList) 63 | 64 | for command in commandList: 65 | commandComponent = command.split(" ") 66 | commandComponent, valid = self.cleanCommandComponent(commandComponent) 67 | if not valid: continue 68 | 69 | # This is a list containing all information of the command in order. 70 | commandListFormat = [] 71 | 72 | for i in range(commandExpectedInputAmount[commandComponent[0]]): 73 | commandListFormat.append(commandComponent[i+1]) 74 | 75 | commandListCompiled.append({commandComponent[0] : deepcopy(commandListFormat)}) 76 | 77 | return commandListCompiled 78 | 79 | 80 | mainParser = parser() 81 | 82 | def executeCode(sourceCode : str, targetObject): 83 | """ 84 | This function receives as input sourceCode in the style defined above and 85 | applies the code to the targetobject. 86 | """ 87 | 88 | commandList = mainParser.parseString(sourceCode) 89 | 90 | possibleFieldName = targetObject.__dict__.keys() 91 | 92 | for command in commandList: 93 | 94 | commandName = list(command.keys())[0] 95 | 96 | match commandName: 97 | case "FORCESET": 98 | fieldName = command["FORCESET"][0] 99 | if fieldName in possibleFieldName: 100 | targetObject.__dict__[fieldName] = command["FORCESET"][1] 101 | case "SET": 102 | fieldName = command["SET"][0] 103 | if fieldName in possibleFieldName: 104 | match targetObject.__dict__[fieldName]: 105 | case int() : targetObject.__dict__[fieldName] = int(command["SET"][1]) 106 | case float() : targetObject.__dict__[fieldName] = float(command["SET"][1]) 107 | case str() : targetObject.__dict__[fieldName] = str(command["SET"][1]) 108 | case bool() : targetObject.__dict__[fieldName] = bool(command["SET"][1]) 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /ffxivcalc/Request/custom_columns.py: -------------------------------------------------------------------------------- 1 | """ 2 | This code was written by Apollo (apollo.van.waddleburg on discord) and adapted to fit the needs of the sim. 3 | """ 4 | 5 | 6 | """Defines and documents all custom columns in the data.""" 7 | 8 | # Not custom, but used a lot so recorded 9 | ABILITY_ID = "abilityGameID" 10 | TYPE = "type" 11 | CAST = "cast" 12 | DAMAGE = "damage" 13 | APPLY_BUFF = "applybuff" 14 | APPLY_DEBUFF = "applydebuff" 15 | REFRESH_BUFF = "refreshbuff" 16 | REMOVE_BUFF = "removebuff" 17 | REMOVE_DEBUFF = "removedebuff" 18 | PREPARE = "calculateddamage" 19 | PLAYER = "Player" 20 | PET = "Pet" 21 | 22 | # Human readable name for abilities 23 | ABILITY_NAME = "ability_name" 24 | 25 | 26 | # Human readable source name for actors 27 | SOURCE_NAME = "source_name" 28 | SOURCE_TYPE = "source_type" 29 | SOURCE_SUBTYPE = "source_subtype" 30 | SOURCE_PET_ACTOR = "source_pet_actor" 31 | SOURCE_ID = "sourceID" 32 | 33 | # Human readable target name for actors 34 | TARGET_NAME = "target_name" 35 | TARGET_TYPE = "target_type" 36 | TARGET_SUBTYPE = "target_subtype" 37 | TARGET_PET_ACTOR = "target_pet_actor" 38 | TARGET_ID = "targetID" 39 | 40 | # Fight time, based on lb update tick to reset gauge 41 | FIGHT_TIME = "fight_time" 42 | SNAPSHOT = "snapshot" 43 | 44 | # Bonus percent, estimate the value of combos 45 | # 1 - basePotency/bonusPotency = bonusPercent 46 | BONUS_PERCENT = "bonusPercent" 47 | 48 | # Log hittype (crit, normal, etc) 49 | LOG_HIT_TYPE = "hitType" 50 | LOG_DHIT = "directHit" 51 | 52 | # Special name added in for an assumed prepull apply 53 | ESTIMATED_APPLY_BUFF = "estimatedapplybuff" 54 | 55 | # Special name for "correct" card, other buffs 56 | CARD_MATCHES_JOB = "card_matches_job" 57 | RADIANT_CODA = "radiant_codas" 58 | 59 | # Potency data and ability details 60 | POTENCY = "ability_potency" 61 | ABILITY_DAMAGE_TYPE = "ability_damage_type" # See types below 62 | DIRECT_DAMAGE = "direct_damage" 63 | DOT = "dot" 64 | AUTO = "auto" 65 | GUARANTEED_CRIT = "guaranteed_crit" 66 | GUARANTEED_DHIT = "guaranteed_dhit" 67 | POSITIONAL = "positional" 68 | SPECIAL_FORMULA = "special_formula" 69 | GROUND_BASED = "ground_based" 70 | ABILITY_TYPE = "ability_type" 71 | GCD = "GCD" 72 | OGCD = "OGCD" 73 | LOCK_05 = "0.5 Lock" 74 | LOCK_10 = "1.0 Lock" 75 | LOCK_15 = "1.5 Lock" 76 | 77 | 78 | # GCD and Weave tracking info 79 | WEAVE = "weave" 80 | GCD_ROLL = "gcd_roll" 81 | NUM_WEAVES = "num_weaves" 82 | 83 | # Buff related stuff 84 | LOG_MULTIPLIER = "multiplier" 85 | DAMAGE_MULTIPLIER = "damage_multiplier" 86 | CRIT_BONUS = "crit_bonus" 87 | DHIT_BONUS = "dhit_bonus" 88 | MAIN_STAT_BONUS = "main_stat_bonus" 89 | DOT_APPLICATION = "dot_application" 90 | CAST_TIME = "cast_time" # Defines the buff snapshot 91 | LIMITED = "limited" # a limited buff to specific types. 92 | # Defines buff multipliers in a serial format as string 93 | ACTIVE_BUFF_MULTIPLIERS = "active_buff_multipliers" 94 | COMPLETE_DAMAGE_MULTIPLIER = "complete_damage_multiplier" 95 | AVERAGED_MULTIPLIER = "averaged_multiplier" 96 | CRIT_CHANCE = "crit_rate" 97 | CRIT_MULT = "crit_multiplier" 98 | CRIT_MOD = "crit_mod" 99 | DHIT_CHANCE = "dhit_rate" 100 | DHIT_MOD = "dhit_mod" 101 | PERSONAL_DAMAGE_MULTIPLIER = "personal_damage_multiplier" # This is all self buffs, reduce to get raw damage 102 | RAW_DAMAGE_VARIANCE = "raw_damage_variance" # variance without personal buffs 103 | DAMAGE_VARIANCE = "damage_variance" # variance with personal buffs 104 | 105 | # Event types 106 | HIT_TYPE = "hit_type" 107 | CRIT = "crit" 108 | DHIT = "direct_hit" 109 | DCRIT = "direct_crit" 110 | BASE = "base_hit" 111 | AVERAGED = "averaged" 112 | TICK = "tick" # Helpful for determining tracking simulated dots 113 | SIMULATED_DOT = "simulated_dot" 114 | 115 | # Simmed Data 116 | EXPECTED_DAMAGE = "expected_damage" 117 | RAW_DAMAGE = "raw_damage" 118 | BASE_DAMAGE = "base_damage" 119 | AMOUNT = "amount" 120 | LOG_DAMAGE = "unmitigatedAmount" # amount column is affected by overkill 121 | LOG_DOT_SIMULATED_DAMAGE = "finalizedAmount" # Simmed dot damage 122 | LATENT_VALUE = "latent_value" 123 | 124 | # Damage taken types 125 | DAMAGE_TYPE = "damage_type" 126 | PHYSICAL = "physical" 127 | MAGICAL = "magical" 128 | DARKNESS = "darkness" 129 | UNKNOWN = "unknown" 130 | 131 | # Mitigation mods 132 | PHYSICAL_MITIGATION = "Physical Mitigation" 133 | MAGICAL_MITIGATION = "Magical Mitigation" 134 | ADJUSTED_LOG_DAMAGE = ( 135 | "unmitigatedBossAmount" # amount column is affected by overkill 136 | ) 137 | 138 | # Mappings (target_subtype) 139 | JOB = "Job" 140 | 141 | TARGET_SUBTYPE_TO_JOB_MELEE = { 142 | "DarkKnight": "DRK", 143 | "Gunbreaker": "GNB", 144 | "Paladin": "PLD", 145 | "Warrior": "WAR", 146 | "Ninja": "NIN", 147 | "Samurai": "SAM", 148 | "Reaper": "RPR", 149 | "Monk": "MNK", 150 | "Dragoon": "DRG", 151 | } 152 | 153 | TARGET_SUBTYPE_TO_JOB = { 154 | **TARGET_SUBTYPE_TO_JOB_MELEE, 155 | "Dancer": "DNC", 156 | "Bard": "BRD", 157 | "Machinist": "MCH", 158 | "RedMage": "RDM", 159 | "BlackMage": "BLM", 160 | "Summoner": "SMN", 161 | "Astrologian": "AST", 162 | "Scholar": "SCH", 163 | "Sage": "SGE", 164 | "WhiteMage": "WHM", 165 | } 166 | 167 | # ('type') 168 | TYPE_TO_DAMAGE_TYPE = { 169 | "1": PHYSICAL, 170 | "32": DARKNESS, 171 | "64": MAGICAL, 172 | "128": PHYSICAL, 173 | "1024": MAGICAL, 174 | } -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Melee/Melee_Spell.py: -------------------------------------------------------------------------------- 1 | from ffxivcalc.Jobs.Base_Spell import Potion, Spell, empty 2 | Lock = 0 3 | class MeleeSpell(Spell): 4 | 5 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = 0): 6 | super().__init__(id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = type) 7 | 8 | ######################################### 9 | ########## NINJA SPELL ################# 10 | ######################################### 11 | 12 | class NinjaSpell(MeleeSpell): 13 | 14 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, Effect, Requirement,Weaponskill, Ninjutsu, type = 0): 15 | super().__init__(id, GCD, CastTime, RecastTime, Potency, 0, Effect, Requirement, type = type) 16 | self.Weaponskill = Weaponskill 17 | self.Ninjutsu = Ninjutsu 18 | 19 | 20 | ######################################### 21 | ########## SAMURAI PLAYER ############### 22 | ######################################### 23 | 24 | class SamuraiSpell(MeleeSpell): 25 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, Effect, Requirement, KenkiCost, type = 0): 26 | super().__init__(id, GCD, CastTime, RecastTime, Potency, 0, Effect, Requirement, type = type) 27 | 28 | self.KenkiCost = KenkiCost 29 | self.Requirement += [KenkiRequirement] 30 | 31 | def KenkiRequirement(Player, Spell): #By default present in Samurai spell requirements 32 | #input("Current Gauge is " + str(Player.KenkiGauge - Spell.KenkiCost)) 33 | return Spell.KenkiCost <= Player.KenkiGauge, -1 34 | 35 | ######################################### 36 | ########## DRAGOON PLAYER ############### 37 | ######################################### 38 | 39 | class DragoonSpell(MeleeSpell): 40 | def __init__(self, id, GCD, RecastTime, Potency, Effect, Requirement, Weaponskill, type = 0): 41 | super().__init__(id, GCD, Lock, RecastTime, Potency, 0, Effect, Requirement, type = type) 42 | 43 | self.Weaponskill = Weaponskill 44 | 45 | ######################################### 46 | ########## MONK SPELL ############### 47 | ######################################### 48 | 49 | class MonkSpell(MeleeSpell): 50 | def __init__(self, id, GCD, RecastTime, Potency, Effect, Requirement, Weaponskill, MasterfulBlitz, type = 0): 51 | super().__init__(id, GCD, Lock, RecastTime, Potency, 0, Effect, Requirement, type = type) 52 | 53 | self.Weaponskill = Weaponskill 54 | self.MasterfulBlitz = MasterfulBlitz 55 | 56 | ######################################### 57 | ########## MONK SPELL ############### 58 | ######################################### 59 | 60 | class ReaperSpell(MeleeSpell): 61 | def __init__(self, id, GCD, CastTime,RecastTime, Potency, Effect, Requirement, Weaponskill, type = 0): 62 | super().__init__(id, GCD, CastTime, RecastTime, Potency, 0, Effect, Requirement, type = type) 63 | 64 | self.Weaponskill = Weaponskill 65 | 66 | 67 | 68 | #Class action spell 69 | 70 | #Requirement 71 | 72 | def SecondWindRequirement(Player, Spell): 73 | return Player.SecondWindCD <= 0, Player.SecondWindCD 74 | 75 | def LegSweepRequirement(Player, Spell): 76 | return Player.LegSweepCD <= 0, Player.LegSweepCD 77 | 78 | def BloodbathRequirement(Player, Spell): 79 | return Player.BloodbathCD <= 0, Player.BloodbathCD 80 | 81 | def FeintRequirement(Player, Spell): 82 | return Player.FeintCD <= 0, Player.FeintCD 83 | 84 | def ArmLengthRequirement(Player, Spell): 85 | return Player.ArmLengthCD <= 0, Player.ArmLengthCD 86 | 87 | def TrueNorthRequirement(Player, Spell): 88 | return Player.TrueNorthStack > 0, Player.TrueNorthCD 89 | 90 | #Apply 91 | def ApplySecondWind(Player, Enemy): 92 | Player.SecondWindCD = 120 93 | 94 | def ApplyLegSweep(Player, Enemy): 95 | Player.LegSweepCD = 40 96 | 97 | def ApplyBloodbath(Player, Enemy): 98 | Player.BloodbathCD = 90 99 | 100 | def ApplyFeint(Player, Enemy): 101 | Player.FeintCD = 90 102 | Enemy.Feint = True 103 | Enemy.FeintTimer = 10 104 | 105 | def ApplyArmLength(Player, Enemy): 106 | Player.ArmLengthCD = 120 107 | 108 | def ApplyTrueNorth(Player, Enemy): 109 | if Player.TrueNorthStack == 2: 110 | Player.EffectCDList.append(TrueNorthStackCheck) 111 | Player.TrueNorthCD = 45 112 | Player.TrueNorthStack -= 1 113 | 114 | 115 | #Check 116 | 117 | def TrueNorthStackCheck(Player, Enemy): 118 | if Player.TrueNorthCD <= 0: 119 | if Player.TrueNorthStack == 1: 120 | Player.EffectToRemove.append(TrueNorthStackCheck) 121 | else: 122 | Player.TrueNorthCD = 45 123 | Player.TrueNorthStack += 1 124 | #Class Action (no effect as of now) 125 | SecondWind = MeleeSpell(7541, False, Lock, 0, 0, 0, ApplySecondWind, [SecondWindRequirement]) 126 | LegSweep = MeleeSpell(7863, False, Lock, 0, 0, 0, ApplyLegSweep, [LegSweepRequirement]) 127 | Bloodbath = MeleeSpell(7542, False, Lock, 0, 0, 0, ApplyBloodbath, [BloodbathRequirement]) 128 | Feint = MeleeSpell(7549, False, Lock, 0, 0, 0, ApplyFeint, [FeintRequirement]) 129 | ArmLength = MeleeSpell(7548, False, Lock, 0, 0, 0, ApplyArmLength, [ArmLengthRequirement]) 130 | TrueNorth = MeleeSpell(7546, False, Lock, 0, 0, 0, ApplyTrueNorth, [TrueNorthRequirement]) 131 | 132 | # Limit Break actions 133 | 134 | LB1Timer = 5.86 135 | LB2Timer = 6.86 136 | LB3Timer = 8.20 137 | 138 | MeleeLB1 = MeleeSpell(1111, False,LB1Timer, LB1Timer, 0, 0, empty, [], type=3) 139 | MeleeLB2 = MeleeSpell(1112, False,LB2Timer, LB2Timer, 0, 0, empty, [], type=3) 140 | MeleeLB3 = MeleeSpell(1113, False,LB3Timer, LB3Timer, 0, 0, empty, [], type=3) 141 | 142 | 143 | MeleeAbility = { 144 | 7541 : SecondWind, 145 | 7863 : LegSweep, 146 | 7542 : Bloodbath, 147 | 7549 : Feint, 148 | 7546 : TrueNorth, 149 | 7548 : ArmLength, 150 | 34590541 : Potion, #STR 151 | 34590542 : Potion, #DEX, 152 | 34592395 : Potion, # STR 7 153 | 34592396 : Potion, # DEX 7 154 | 34594159 : Potion, # STR 8 155 | 34594160 : Potion, # DEX pot ? 156 | -2 : Potion, 157 | 1111 : MeleeLB1, 158 | 1112 : MeleeLB2, 159 | 1113 : MeleeLB3 160 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Discord](https://img.shields.io/discord/970724799464738977?color=7289da&label=Discord&logo=discord)](https://discord.gg/mZXKUNy2sw) 2 | 3 | Hi! I have stopped working on this project. If you are interested in ffxiv simulation/opti/parsing please visit my friend's website that offers xiv simulation (http://www.xivraider.com/). I am sure you will find what you might have been looking for here:)! 4 | 5 | 6 | # Desktop app 7 | 8 | You can download the desktop app to use the simulator without having to code : https://github.com/IAmPythagoras/ffxivcalcWebApp . 9 | 10 | # Install 11 | 12 | You can install this library using pip : 13 | 14 | ``` 15 | pip install ffxivcalc 16 | ``` 17 | 18 | Or can download latest unstable version using : 19 | 20 | ``` 21 | pip install ffxiv-combat-simulator@git+https://github.com/IAmPythagoras/FFXIV-Combat-Simulator.git@feature/GeneralDev 22 | ``` 23 | 24 | *Note that you must have git installed : https://git-scm.com/download/win 25 | 26 | You can find the documentation here : https://iampythagoras.github.io/index.html or join the discord linked above if you have any questions. 27 | 28 | *The unstable version can be removed using 29 | ``` 30 | pip uninstall ffxiv-combat-simulator 31 | ``` 32 | 33 | # FFXIV-Combat-Simulator (ffxivcalc) 34 | 35 | This Python library lets you simulate combat from the game Final Fantasy XIV. It allows for as many players as possible and will simulate the fight in "real time". It 36 | accurately keeps track of MP, cooldown on abilities, HP, DOTs, raid buffs, personnal buffs, team composition bonus, potions buff, which allows for a dynamic environment that portrays as accurately as possible the real game's environment. 37 | 38 | The simulator in its base state will output every player's DPS (Damage Per Second), PPS (Potency Per Second), TP (Total Potency), but it can be customized to output any other metric that could be useful. Furthermore, the simulator is able to output a distribution of the DPS which allows to see the different DPS' percentiles. 39 | 40 | Here are some examples of simulations : 41 | 42 | BlackMage doing opener and some more : 43 | 44 | ![image](https://github.com/IAmPythagoras/FFXIV-Combat-Simulator/assets/62820030/5bde764b-7e4a-4fa5-9bc5-8668e716f1d9) 45 | ![image](https://github.com/IAmPythagoras/FFXIV-Combat-Simulator/assets/62820030/8f8fd525-6b17-4582-a490-1da3ae8f0bfb) 46 | ![image](https://github.com/IAmPythagoras/FFXIV-Combat-Simulator/assets/62820030/40304b27-af47-4181-bdff-d8d02b414cfd) 47 | 48 | Here are some of the results the simulator will output : A text result, a graph of DPS over time and a graph of the distribution. 49 | 50 | BlackMage, Dancer, Dragoon, Scholar doing opener and some more : 51 | 52 | ![image](https://github.com/IAmPythagoras/FFXIV-Combat-Simulator/assets/62820030/e3e8e2c7-935d-49fa-91c8-745f134e01e1) 53 | ![image](https://github.com/IAmPythagoras/FFXIV-Combat-Simulator/assets/62820030/f351ec39-c241-4ffb-9722-a6ce7098c5b9) 54 | ![image](https://github.com/IAmPythagoras/FFXIV-Combat-Simulator/assets/62820030/9972f6aa-e0ef-4a63-a908-2b106ecff814) 55 | 56 | Note that the Blackmage's DPS is higher because it received all the buffs. 57 | 58 | This library also has a built-in BiS (best in slot) solver. The advantage of using this one is that instead of simply maximizing the DPS of a dummy rotation, the simulator allows the solver to take into account the different raid buffs and when they are in effect. This means that the solver will find a BiS that is dependant on the simulation you want it to optimize damage in. 59 | 60 | Learn more about the simulator and how to use it by going to the official documentation website : https://iampythagoras.github.io/index.html 61 | 62 | With version 0.8.10 you can now output a "Simulation record" as a pdf file that shows information about the simulation : 63 | 64 | ![image](https://github.com/IAmPythagoras/FFXIV-Combat-Simulator/assets/62820030/88069e5d-8ffa-4783-acc3-b1b02c3ce33e) 65 | 66 | This record's goal is to act as a sort of log, but which is more easily understood and contains only the most useful information on the running of the simulation. 67 | You can also export a text only version of this record. 68 | # Rotation BiS Solver 69 | 70 | As of ffxivcalc version 0.8.00, this library now has a built in rotation BiS solver. You can use the ffxivcalclayout.py in order to experiment with it or download the desktop app. 71 | 72 | You can read this PDF [BiS_Solver_Algorithm_Documentation.pdf](https://github.com/IAmPythagoras/FFXIV-Combat-Simulator/files/12580553/BiS_Solver_Algorithm_Documentation.pdf) if you are interested in its 73 | functionning. 74 | 75 | You can read about the experimental results of this solver here : https://docs.google.com/spreadsheets/d/1yVl-F1tHARYIYvz4ZPxIY6MayakYug3OEqpGnpZllqU/edit?usp=sharing 76 | 77 | # Validity of simulator 78 | 79 | As of version 0.8.940 I have started adding in depth testing of the simulator. The simulator will still have some flaws, but it is currently being checked by around 800 individual tests. More tests will be added with time. 80 | 81 | # Other simulators 82 | 83 | Make sure to check out this alternative FFXIV simulator [Amas-FF14-Combat-Sim](https://github.com/Amarantine-xiv/Amas-FF14-Combat-Sim) or join its [Discord](https://discord.gg/8GjA5uRcDX) 84 | 85 | # If you want to help 86 | 87 | If you want to help or want to ask questions to me directly, feel free to join the discord linked above:) 88 | 89 | # Update log 90 | 91 | (Recent only read __init__.py for all update logs) 92 | 93 | 0.9.940: 94 | - Oups, forgot to ever implement Army Ethos/Army Muse effect on Bard. This is now fixed. 95 | - Other bug fixes. 96 | 97 | 0.8.930: 98 | - Added Player.getPlayerPrePullTime() which returns the length of the prepull (time before first damage action). 99 | - Added Fight.getPlayerPrepullLength() which returns the prepull length of all players 100 | - added Fight.syncPlayerPrePull() which syncs all player's prepull so they all do damage at the same time. 101 | - Added test suite 'Prepull length test suite' that tests the Player.GetPlayerPrePullTime() function. 102 | - Removed access to ffxvivcalc.API, ffxivcalc.codeParser and ffxivcalc.Request.FFLogsAPIRequest modules (outdated modules). 103 | - Other minor bug fix. 104 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Caster/Caster_Spell.py: -------------------------------------------------------------------------------- 1 | from ffxivcalc.Jobs.Base_Spell import ManaRequirement, Potion, Spell, empty 2 | from ffxivcalc.Jobs.PlayerEnum import JobEnum 3 | Lock = 0 4 | class CasterSpell(Spell): 5 | 6 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = 0): 7 | super().__init__(id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = type) 8 | 9 | 10 | ######################################### 11 | ########## BLACKMAGE SPELL ############## 12 | ######################################### 13 | class BLMSpell(CasterSpell): 14 | #This class will be all BlackMage Ability 15 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, ManaCost, IsFire, IsIce, Effect, Requirement, type = 0): 16 | super().__init__(id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = type)#Calls constructor of Spell 17 | 18 | #BLM specific part 19 | 20 | self.IsFire = IsFire 21 | self.IsIce = IsIce 22 | 23 | ######################################### 24 | ########## REDMAGE SPELL ################ 25 | ######################################### 26 | 27 | 28 | class RedmageSpell(CasterSpell): 29 | 30 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, BlackCost, WhiteCost, type = 0): 31 | super().__init__(id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = type) 32 | 33 | self.BlackCost = BlackCost 34 | self.WhiteCost = WhiteCost 35 | 36 | ######################################### 37 | ########## SUMMONER SPELL ############### 38 | ######################################### 39 | 40 | 41 | class SummonerSpell(CasterSpell): 42 | 43 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = 0): 44 | super().__init__(id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = type) 45 | 46 | 47 | #Class Action 48 | 49 | #Requirement 50 | 51 | def SwiftcastRequirement(Player, Spell): 52 | return Player.SwiftcastCD <= 0, Player.SwiftcastCD 53 | 54 | def LucidDreamingRequirement(Player, Spell): 55 | #if Player.LucidDreamingCD > 0: input("LUCID IS OUT OF SYNC BY : " + str(Player.LucidDreamingCD)) 56 | #return Player.LucidDreamingCD <= 0, Player.LucidDreamingCD 57 | return True, -1 58 | 59 | def SurecastRequirement(Player, Spell): 60 | return Player.SurecastCD <= 0, Player.SurecastCD 61 | 62 | def AddleRequirement(Player, Spell): 63 | return Player.AddleCD <= 0, Player.AddleCD 64 | 65 | #Apply 66 | 67 | def ApplyLucidDreaming(Player, Enemy): 68 | Player.LucidDreamingCD = 60 69 | Player.LucidDreamingTimer = 21 70 | Player.EffectCDList.append(LucidDreamingCheck) 71 | 72 | 73 | # The order of insta cast usage for blackmage is SwiftCast -> TripleCast 74 | # The order for redmage is Acceleration -> Dualcast -> Swiftcast. 75 | # For purpose of simplicity, the order of these for RDM might not always be like that in sim. For example, 76 | # if execute Jolt -> Acc -> Swift : The usage will be Acc -> Swift -> Dual. 77 | # That is because Acc is insert(0), Swift is insert(1) and dual is index 0 at the start. 78 | # However if execute Jolt -> Swift -> Acc, then the correct use of Acc -> Dual -> Swift will happen. 79 | 80 | def ApplySwiftcast(Player, Enemy): 81 | Player.SwiftcastCD = 60 82 | # Inserting in index 1 in order to have it go after ElementalEffect for blm and dualcast effect for rdm. 83 | Player.EffectList.insert(1,SwiftcastEffect) 84 | 85 | def ApplySurecast(Player, Enemy): 86 | Player.SurecastCD = 120 87 | 88 | def ApplyAddle(Player, Enemy): 89 | Player.AddleCD = 90 90 | Enemy.Addle = True 91 | Enemy.AddleTimer = 10 92 | 93 | 94 | #Effect 95 | 96 | def SwiftcastEffect(Player, Spell): 97 | if Spell.GCD and Spell.CastTime > Lock and not (Player.JobEnum == JobEnum.BlackMage and Spell.id == 152 and Player.Fire3Proc): #If GCD and not already insta cast and not F3 Proc when casting F3 98 | Spell.CastTime = Lock 99 | Player.EffectToRemove.append(SwiftcastEffect) 100 | 101 | #Check 102 | 103 | def LucidDreamingCheck(Player, Enemy): 104 | #input("got in") 105 | if (int(Player.LucidDreamingTimer * 100)/100)%3 == 0 and Player.LucidDreamingTimer != 21: 106 | #if on a tic and not equal to 21 107 | #input("got in") 108 | 109 | if Player.JobEnum == JobEnum.BlackMage: 110 | #We have to check if in firephase, in which case no mana regen 111 | if Player.ElementalGauge <= 0: #If in ice phase 112 | #input("Adding mana") 113 | Player.Mana = min(10000, Player.Mana + 550) 114 | else: 115 | #Then any other player 116 | Player.Mana = min(10000, Player.Mana + 550) 117 | 118 | #Check if we are done 119 | if Player.LucidDreamingTimer <= 0: 120 | Player.EffectToRemove.append(LucidDreamingCheck) 121 | 122 | #Class Action 123 | Swiftcast = CasterSpell(7561, False,0, Lock, 0, 0, ApplySwiftcast, [SwiftcastRequirement]) 124 | LucidDreaming = CasterSpell(7562, False, Lock,0,0, 0, ApplyLucidDreaming, [LucidDreamingRequirement]) 125 | Surecast = CasterSpell(7559, False, Lock, 0, 0, 0, ApplySurecast, [SurecastRequirement]) 126 | Addle = CasterSpell(7560, False, Lock, 0, 0, 0, ApplyAddle, [AddleRequirement]) 127 | Sleep = CasterSpell(25880, True, 2.5, 2.5, 0, 800, empty, [ManaRequirement]) 128 | 129 | # Limit Break actions 130 | 131 | LB1Timer = 5.1 132 | LB2Timer = 8.1 133 | LB3Timer = 12.6 134 | 135 | CasterLB1 = CasterSpell(1111, False,LB1Timer, LB1Timer, 0, 0, empty, [], type=3) 136 | CasterLB2 = CasterSpell(1112, False,LB2Timer, LB2Timer, 0, 0, empty, [], type=3) 137 | CasterLB3 = CasterSpell(1113, False,LB3Timer, LB3Timer, 0, 0, empty, [], type=3) 138 | 139 | CasterAbility = { 140 | 7561 : Swiftcast, 141 | 7562 : LucidDreaming, 142 | 7559 : Surecast, 143 | 7560 : Addle, 144 | 25880 : Sleep, 145 | 34590544 : Potion, #Grade 6 146 | 34592398 : Potion, # Grade 7 147 | 34594162 : Potion, # Grade 8 148 | -2 : Potion, 149 | 1111 : CasterLB1, 150 | 1112 : CasterLB2, 151 | 1113 : CasterLB3 152 | } -------------------------------------------------------------------------------- /ffxivcalc/helperCode/lookUpAbilityID.py: -------------------------------------------------------------------------------- 1 | from ffxivcalc.Jobs.Base_Spell import WaitAbility 2 | from ffxivcalc.Jobs.ActionEnum import name_for_id 3 | #CASTER 4 | 5 | from ffxivcalc.Jobs.Caster.Caster_Spell import CasterAbility 6 | from ffxivcalc.Jobs.Caster.Blackmage.BlackMage_Spell import BlackMageAbility 7 | from ffxivcalc.Jobs.Caster.Redmage.Redmage_Spell import RedMageAbility 8 | from ffxivcalc.Jobs.Caster.Summoner.Summoner_Spell import SummonerAbility 9 | 10 | #HEALER 11 | from ffxivcalc.Jobs.Healer.Healer_Spell import HealerAbility 12 | from ffxivcalc.Jobs.Healer.Sage.Sage_Spell import SageAbility 13 | from ffxivcalc.Jobs.Healer.Astrologian.Astrologian_Spell import AstrologianAbility 14 | from ffxivcalc.Jobs.Healer.Scholar.Scholar_Spell import ScholarAbility 15 | from ffxivcalc.Jobs.Healer.Whitemage.Whitemage_Spell import WhiteMageAbility 16 | from ffxivcalc.Jobs.Melee.Monk.Monk_Spell import MonkAbility 17 | 18 | #RANGED 19 | from ffxivcalc.Jobs.Ranged.Ranged_Spell import BardSpell, RangedAbility 20 | from ffxivcalc.Jobs.Ranged.Bard.Bard_Spell import BardAbility 21 | from ffxivcalc.Jobs.Ranged.Machinist.Machinist_Spell import MachinistAbility 22 | from ffxivcalc.Jobs.Ranged.Dancer.Dancer_Spell import DancerAbility 23 | 24 | #TANK 25 | from ffxivcalc.Jobs.Tank.Tank_Spell import TankAbility 26 | from ffxivcalc.Jobs.Tank.Gunbreaker.Gunbreaker_Spell import GunbreakerAbility 27 | from ffxivcalc.Jobs.Tank.DarkKnight.DarkKnight_Spell import DarkKnightAbility 28 | from ffxivcalc.Jobs.Tank.Warrior.Warrior_Spell import WarriorAbility 29 | from ffxivcalc.Jobs.Tank.Paladin.Paladin_Spell import PaladinAbility 30 | 31 | #MELEE 32 | from ffxivcalc.Jobs.Melee.Melee_Spell import MeleeAbility 33 | from ffxivcalc.Jobs.Melee.Samurai.Samurai_Spell import SamuraiAbility 34 | from ffxivcalc.Jobs.Melee.Ninja.Ninja_Spell import NinjaAbility 35 | from ffxivcalc.Jobs.Melee.Dragoon.Dragoon_Spell import DragoonAbility 36 | from ffxivcalc.Jobs.Melee.Reaper.Reaper_Spell import ReaperAbility 37 | 38 | from ffxivcalc.helperCode.exceptions import ActionNotFound, JobNotFound, InvalidTarget 39 | 40 | import logging 41 | main_logging = logging.getLogger("ffxivcalc") 42 | lookUpAbilityId_logging = main_logging.getChild("lookUpAbilityId") 43 | 44 | def lookup_abilityID(actionID, targetID, sourceID, player_list): 45 | """ 46 | This function will translate an actionID into a Spell object of the relevant action that the simulator can use. 47 | actionID : int -> ID of the action in the game 48 | targetID : int -> ID of the target. Can be player or Enemy. 49 | sourceID : int -> ID of the player casting the action. 50 | player_list : dict -> dict of all players with some information. 51 | """ 52 | #Will first get the job of the sourceID so we know in what dictionnary to search for 53 | 54 | def lookup(JobDict, ClassDict, job_name): 55 | """ 56 | This function actually looks up the relevant dictionnary of the Job to find the Spell object. 57 | JobDict : dict -> dictionnary with keys being IDs and mapping to the Spell object (only for Job actions) 58 | ClassDict : dict -> same as JobDict, but for Class actions 59 | """ 60 | if not (int(actionID) in JobDict.keys()): #if not in, then the action is in the ClassDict 61 | if not (int(actionID) in ClassDict.keys()): 62 | log_str = "Action Not found , Job : " + job_name + " , ActionId : " + str(actionID) + " , targetID : " + str(targetID) + " , sourceID : " + str(sourceID) 63 | lookUpAbilityId_logging.warning(log_str) 64 | lookUpAbilityId_logging.warning("Since action was not found defaulting to WaitAbility(0).") 65 | return WaitAbility(0) #Currently at none so we can debug 66 | raise ActionNotFound #Did not find action 67 | if callable(ClassDict[int(actionID)]): #If the action is a function 68 | if (not (str(targetID) in player_list.keys())): 69 | player_obj = player_list[sourceID]["job_object"] 70 | raise InvalidTarget(name_for_id(actionID, player_obj.ClassAction, player_obj.JobAction), player_obj, None,True, targetID) 71 | return ClassDict[int(actionID)](player_list[str(targetID)]["job_object"]) 72 | return ClassDict[int(actionID)] #Class actions do not have the possibility to target other allies, so we assume itll target an enemy 73 | 74 | if callable(JobDict[int(actionID)]): #If the action is a function 75 | if (not (str(targetID) in player_list.keys())): 76 | player_obj = player_list[sourceID]["job_object"] 77 | raise InvalidTarget(name_for_id(actionID, player_obj.ClassAction, player_obj.JobAction), player_obj, None,True, targetID) 78 | return JobDict[int(actionID)](player_list[str(targetID)]["job_object"]) 79 | return JobDict[int(actionID)] #Else return object 80 | 81 | job_name = player_list[str(sourceID)]["job"] #getting job name 82 | 83 | #Will now go through all possible job and find what action is being used based on the ID. If the ID is not right, it will 84 | #raise an ActionNotFoundError. And if the job's name does not exist it will raise a JobNotFoundError 85 | if job_name == "BlackMage" :#Caster 86 | return lookup(BlackMageAbility, CasterAbility,job_name) 87 | elif job_name == "RedMage": 88 | return lookup(RedMageAbility, CasterAbility,job_name) 89 | elif job_name == "Summoner": 90 | return lookup(SummonerAbility, CasterAbility,job_name) 91 | elif job_name == "Dancer":#Ranged 92 | return lookup(DancerAbility, RangedAbility,job_name) 93 | elif job_name == "Machinist": 94 | return lookup(MachinistAbility, RangedAbility,job_name) 95 | elif job_name == "Bard": 96 | return lookup(BardAbility, RangedAbility,job_name) 97 | elif job_name == "Warrior":#Tank 98 | return lookup(WarriorAbility, TankAbility,job_name) 99 | elif job_name == "Gunbreaker": 100 | return lookup(GunbreakerAbility, TankAbility,job_name) 101 | elif job_name == "DarkKnight": 102 | return lookup(DarkKnightAbility, TankAbility,job_name) 103 | elif job_name == "Paladin": 104 | return lookup(PaladinAbility, TankAbility,job_name) 105 | elif job_name == "WhiteMage":#Healer 106 | return lookup(WhiteMageAbility, HealerAbility,job_name) 107 | elif job_name == "Scholar": 108 | return lookup(ScholarAbility, HealerAbility,job_name) 109 | elif job_name == "Sage": 110 | return lookup(SageAbility, HealerAbility,job_name) 111 | elif job_name == "Astrologian": 112 | return lookup(AstrologianAbility, HealerAbility,job_name) 113 | elif job_name == "Samurai":#Melee 114 | return lookup(SamuraiAbility, MeleeAbility,job_name) 115 | elif job_name == "Reaper": 116 | return lookup(ReaperAbility, MeleeAbility,job_name) 117 | elif job_name == "Ninja": 118 | return lookup(NinjaAbility, MeleeAbility,job_name) 119 | elif job_name == "Monk": 120 | return lookup(MonkAbility, MeleeAbility,job_name) 121 | elif job_name == "Dragoon": 122 | return lookup(DragoonAbility, MeleeAbility,job_name) 123 | 124 | lookUpAbilityId_logging.critical("Job name not found : " + job_name) 125 | 126 | raise JobNotFound #If we get here, then we have not found the job in question 127 | #This should not happen, and if it does it means we either have a serious problem or the names aren't correct -------------------------------------------------------------------------------- /ffxivcalc/Request/prepull.py: -------------------------------------------------------------------------------- 1 | """ 2 | This code was written by Apollo (apollo.van.waddleburg on discord) and adapted to fit the needs of the sim. 3 | """ 4 | 5 | """Defines utilities for determining important prepull buffs from fflogs.""" 6 | import logging 7 | from typing import List 8 | 9 | import pandas as pd 10 | 11 | from ffxivcalc.Request import custom_columns 12 | 13 | LOG = logging.getLogger(__name__) 14 | 15 | # TODO: export into a configurable and accessable format 16 | ALL = { 17 | "Medicated": 30, 18 | "Swiftcast": 10, 19 | } 20 | 21 | WAR = { 22 | "Inner Release": 15, 23 | } 24 | 25 | PLD = { 26 | "Fight or Flight": 20, 27 | } 28 | 29 | DRK = { 30 | "Delirium": 15, 31 | "Blood Weapon": 15, 32 | "Blackest Night": 7, 33 | } 34 | 35 | GNB = { 36 | "No Mercy": 20, 37 | } 38 | 39 | RPR = { 40 | "Arcane Circle": 20, 41 | } 42 | 43 | MNK = { 44 | "Riddle of Fire": 20, 45 | "Riddle of Wind": 15, 46 | "Brotherhood": 15, 47 | } 48 | 49 | DRG = { 50 | "Life Surge": 5, 51 | "Lance Charge": 20, 52 | "Battle Litany": 15, 53 | "Right Eye": 20, 54 | "Left Eye": 20, 55 | } 56 | 57 | NIN = { 58 | "Kassatsu": 15, 59 | } 60 | 61 | SAM = { 62 | "Meikyo Shisui": 15, 63 | } 64 | 65 | BRD = { 66 | "Battle Voice": 15, 67 | } 68 | 69 | DNC = { 70 | "Standard Finish": 30, 71 | "Technical Finish": 20, 72 | "Devilment": 20, 73 | # "Flourish": 74 | } 75 | 76 | MCH = {} 77 | 78 | RDM = { 79 | "Acceleration": 20, 80 | "Dualcast": 5, 81 | "Embolden": 20, 82 | } 83 | 84 | BLM = { 85 | "Triplecast": 10, 86 | "Sharpcast": 30, 87 | "Ley Lines": 30, 88 | } 89 | 90 | SMN = { 91 | "Searing Light": 30, 92 | } 93 | 94 | WHM = { 95 | "Presence of Mind": 15, 96 | } 97 | 98 | AST = { 99 | "the Arrow": 15, 100 | "the Balance": 15, 101 | "the Ewer": 15, 102 | "the Spire": 15, 103 | "the Spear": 15, 104 | "the Bole": 15, 105 | "Divination": 15, 106 | "Earthly Dominance": 10, 107 | "Giant Dominance": 20, 108 | } 109 | 110 | SCH = {} 111 | 112 | SGE = { 113 | "Eukrasia": 60, # has no duration, 30 is fine. 114 | } 115 | 116 | 117 | POTENTIAL_PREPULL_BUFFS = { 118 | **ALL, 119 | **AST, 120 | **BLM, 121 | **BRD, 122 | **DNC, 123 | **DRG, 124 | **DRK, 125 | **GNB, 126 | **MCH, 127 | **MNK, 128 | **NIN, 129 | **RPR, 130 | **PLD, 131 | **SAM, 132 | **SGE, 133 | **SCH, 134 | **SMN, 135 | **WAR, 136 | **WHM, 137 | } 138 | 139 | 140 | def prepull_aura_frame(fight_events: pd.DataFrame) -> pd.DataFrame: 141 | """Builds a frame depicting prepull auras.""" 142 | output = [] 143 | combatantinfo = fight_events[ 144 | fight_events[custom_columns.TYPE] == "combatantinfo" 145 | ] 146 | for ( 147 | fight_time, 148 | source_id, 149 | source_name, 150 | source_type, 151 | source_subtype, 152 | auras, 153 | ) in zip( 154 | combatantinfo[custom_columns.FIGHT_TIME], 155 | combatantinfo[custom_columns.SOURCE_ID], 156 | combatantinfo[custom_columns.SOURCE_NAME], 157 | combatantinfo[custom_columns.SOURCE_TYPE], 158 | combatantinfo[custom_columns.SOURCE_SUBTYPE], 159 | combatantinfo["auras"], 160 | ): 161 | for aura in auras: 162 | output.append( 163 | { 164 | custom_columns.FIGHT_TIME: fight_time, 165 | custom_columns.SOURCE_ID: source_id, 166 | custom_columns.SOURCE_NAME: source_name, 167 | custom_columns.SOURCE_TYPE: source_type, 168 | custom_columns.SOURCE_SUBTYPE: source_subtype, 169 | **aura, 170 | } 171 | ) 172 | 173 | return pd.DataFrame(output) 174 | 175 | 176 | def infer_prepull_buff_casts(fight_events: pd.DataFrame) -> pd.DataFrame: 177 | """Infers all prepull buffs applications into a DF with all relevant data. 178 | 179 | Specifically, does the following: 180 | - Looks at the combatant info row to get starting buffs 181 | - For each one, looks for when the buff falls off in combat 182 | - From this, inferences the original application time 183 | """ 184 | # Get the specific subframe for particular events. 185 | outputs: List[pd.DataFrame] = [] 186 | 187 | # Get a quick subframe for the first refresh or removal of a buff 188 | buff_removal_or_refresh = ( 189 | fight_events[ 190 | ( 191 | fight_events[custom_columns.TYPE].isin( 192 | [custom_columns.REMOVE_BUFF, "refreshbuff"] 193 | ) 194 | ) 195 | ] 196 | .sort_values(by=[custom_columns.FIGHT_TIME], axis=0) 197 | .dropna(how="all", axis=1) 198 | .reset_index(drop=True) 199 | ) 200 | 201 | # groupby each player, build outputs per player 202 | for player_name, info_data in fight_events[ 203 | fight_events[custom_columns.TYPE] == "combatantinfo" 204 | ].groupby(by=custom_columns.SOURCE_NAME): 205 | LOG.debug(f"Finding auras for {player_name}") 206 | # while info data should be a single row, just loop through it for safety. 207 | for auras in info_data["auras"]: 208 | for aura in auras: 209 | # Check aura-names 210 | duration = POTENTIAL_PREPULL_BUFFS.get(aura["name"]) 211 | if duration is None: 212 | LOG.debug(f"Skipping {aura}, not tracking.") 213 | continue 214 | LOG.debug(f"Processing auras {aura}.") 215 | 216 | # get the earliest removal or refresh of the buff 217 | specific_buff_removal_or_refresh = buff_removal_or_refresh[ 218 | ( 219 | buff_removal_or_refresh[custom_columns.TARGET_NAME] 220 | == player_name 221 | ) 222 | & ( 223 | buff_removal_or_refresh[custom_columns.ABILITY_NAME] 224 | == aura["name"] 225 | ) 226 | ] 227 | 228 | if specific_buff_removal_or_refresh.empty: 229 | LOG.warning(f"Unable to find removal event for {aura}") 230 | continue 231 | 232 | earliest_buff_reference = specific_buff_removal_or_refresh.head( 233 | 1 234 | ).copy(deep=True) 235 | # Inference the original usage, via subtracting the fight-time 236 | earliest_buff_reference.iloc[ 237 | 0, 238 | earliest_buff_reference.columns.get_loc( 239 | custom_columns.FIGHT_TIME 240 | ), 241 | ] -= duration 242 | earliest_buff_reference.iloc[ 243 | 0, 244 | earliest_buff_reference.columns.get_loc( 245 | custom_columns.TYPE 246 | ), 247 | ] = custom_columns.ESTIMATED_APPLY_BUFF 248 | outputs.append(earliest_buff_reference) 249 | 250 | if not outputs: 251 | return pd.DataFrame() 252 | 253 | return ( 254 | pd.concat(outputs, axis=0) 255 | .round({custom_columns.FIGHT_TIME: 3}) 256 | .sort_values(by=[custom_columns.FIGHT_TIME], axis=0) 257 | .dropna(how="all", axis=1) 258 | .reset_index(drop=True) 259 | ) -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Healer/Whitemage/Whitemage_Spell.py: -------------------------------------------------------------------------------- 1 | ######################################### 2 | ########## WHITEMAGE PLAYER ############# 3 | ######################################### 4 | from ffxivcalc.Jobs.Base_Spell import DOTSpell, empty, ManaRequirement 5 | from ffxivcalc.Jobs.Healer.Healer_Spell import WhitemageSpell 6 | import copy 7 | 8 | #Requirement 9 | 10 | def PresenceOfMindRequirement(Player, Spell): 11 | return Player.PresenceOfMindCD <= 0, Player.PresenceOfMindCD 12 | 13 | def AssizeRequirement(Player, Spell): 14 | return Player.AssizeCD <= 0, Player.AssizeCD 15 | 16 | def ThinAirRequirement(Player, Spell): 17 | return Player.ThinAirCD <= 0, Player.ThinAirCD 18 | 19 | def BellRequirement(Player, Spell): 20 | return Player.BellCD <= 0, Player.BellCD 21 | 22 | def AquaveilRequirement(Player, Spell): 23 | return Player.AquaveilCD <= 0, Player.AquaveilCD 24 | 25 | def TemperanceRequirement(Player, Spell): 26 | return Player.TemperanceCD <= 0, Player.TemperanceCD 27 | 28 | def PlenaryIndulgenceRequirement(Player, Spell): 29 | return Player.PlenaryIndulgenceCD <= 0, Player.PlenaryIndulgenceCD 30 | 31 | def DivineBenisonRequirement(Player, Spell): 32 | return Player.DivineBenisonCD <= 0, Player.DivineBenisonCD 33 | 34 | def TetragrammatonRequirement(Player, Spell): 35 | return Player.TetragrammatonCD <= 0, Player.TetragrammatonCD 36 | 37 | def BenedictionRequirement(Player, Spell): 38 | return Player.BenedictionCD <= 0, Player.BenedictionCD 39 | 40 | def AsylumRequirement(Player, Spell): 41 | return Player.AsylumCD <= 0, Player.AsylumCD 42 | 43 | def BloodLilyRequirement(Player, Spell): 44 | return Player.LilyStack > 0, Player.LilyTimer 45 | 46 | def BloomLilyRequirement(Player, Spell): 47 | return Player.BloomLily, -1 48 | 49 | 50 | 51 | #Apply 52 | 53 | def ApplyAfflatusMisery(Player, Enemy): 54 | Player.BloomLily = False 55 | Player.UsedLily = 0 #Reset counter 56 | 57 | def ApplyLily(Player, Enemy): 58 | if not Player.BloomLily : Player.UsedLily = min(3, Player.UsedLily + 1) 59 | Player.LilyStack -= 1 60 | 61 | if Player.UsedLily == 3: Player.BloomLily = True 62 | 63 | def Apply(Player, Enemy): 64 | Player.CD = 0 65 | 66 | def ApplyBell(Player, Enemy): 67 | Player.BellCD = 180 68 | 69 | def ApplyAquaveil(Player, Enemy): 70 | Player.AquaveilCD = 60 71 | 72 | def ApplyTemperance(Player, Enemy): 73 | Player.TemperanceCD = 120 74 | 75 | def ApplyPlenaryIndulgence(Player, Enemy): 76 | Player.PlenaryIndulgenceCD = 60 77 | 78 | def ApplyDivineBenison(Player, Enemy): 79 | Player.DivineBenisonCD = 30 80 | 81 | def ApplyTetragrammaton(Player, Enemy): 82 | Player.TetragrammatonCD = 60 83 | 84 | def ApplyAsylum(Player, Enemy): 85 | Player.AsylumCD = 90 86 | 87 | def ApplyBenediction(Player, Enemy): 88 | Player.BenedictionCD = 180 89 | 90 | def ApplyDia(Player, Enemy): 91 | Player.DiaTimer = 30 92 | 93 | if (Player.Dia == None) : 94 | Player.Dia = copy.deepcopy(DiaDOT) 95 | Player.EffectCDList.append(CheckDia) 96 | Player.DOTList.append(Player.Dia) 97 | else : Player.Dia.resetBuffSnapshot() # If already applied reset snapshot 98 | 99 | def ApplyAssize(Player, Enemy): 100 | Player.Mana = min(10000, Player.Mana + 500) 101 | Player.AssizeCD = 40 102 | 103 | def ApplyThinAir(Player, Enemy): 104 | Player.ThinAirCD = 60 105 | Player.EffectList.append(ThinAirEffect) 106 | 107 | def ApplyPresenceOfMind(Player, Enemy): 108 | Player.PresenceOfMindCD = 120 109 | Player.PresenceOfMindTimer = 15 110 | Player.EffectList.append(PresenceOfMindEffect) 111 | Player.Haste += 20 112 | Player.hasteChangeValue = 20 113 | Player.hasteHasChanged = True 114 | Player.EffectCDList.append(CheckPresenceOfMind) 115 | 116 | #Effect 117 | 118 | def ThinAirEffect(Player, Spell): 119 | if Spell.GCD: 120 | Spell.ManaCost = 0 121 | Player.EffectList.remove(ThinAirEffect) 122 | 123 | def PresenceOfMindEffect(Player, Spell): 124 | pass 125 | #Spell.CastTime *= 0.8 126 | #Spell.RecastTime *= 0.8 127 | 128 | #Check 129 | 130 | def CheckDia(Player, Enemy): 131 | if Player.DiaTimer <= 0: 132 | Player.DOTList.remove(Player.Dia) 133 | Player.Dia = None 134 | Player.EffectToRemove.append(CheckDia) 135 | 136 | def CheckPresenceOfMind(Player, Enemy): 137 | if Player.PresenceOfMindTimer <= 0: 138 | Player.EffectList.remove(PresenceOfMindEffect) 139 | Player.Haste -= 20 140 | Player.hasteHasChanged = True 141 | Player.hasteChangeValue = -20 142 | Player.EffectToRemove.append(CheckPresenceOfMind) 143 | 144 | 145 | 146 | #Damage GCD 147 | Glare = WhitemageSpell(25859, True, 1.5, 2.5, 310, 400, empty, [ManaRequirement], type = 1) 148 | Dia = WhitemageSpell(16532, True, 0, 2.5, 60, 400, ApplyDia, [ManaRequirement], type = 1) 149 | DiaDOT = DOTSpell(-5, 60, False) 150 | AfflatusMisery = WhitemageSpell(16535, True, 0, 2.5, 1240, 0, ApplyAfflatusMisery, [BloomLilyRequirement], type = 1) 151 | Holy = WhitemageSpell(25860, True, 2.5, 2.5, 150, 400, empty, [ManaRequirement], type = 1) 152 | #Healing GCD 153 | AfflatusRapture = WhitemageSpell(16534, True, 0, 2.5, 0, 0, ApplyLily, [BloodLilyRequirement], type = 1, AOEHeal=True) 154 | AfflatusSolace = WhitemageSpell(16531, True, 0, 0, 0, 0, ApplyLily, [BloodLilyRequirement], type = 1, TargetHeal=True) 155 | Regen = WhitemageSpell(137, True, 0, 2.5, 0, 400, empty, [ManaRequirement], type = 1) 156 | Cure = WhitemageSpell(120, True, 1.5, 2.5, 0, 400, empty, [ManaRequirement], type = 1, TargetHeal=True) 157 | Cure2 = WhitemageSpell(135, True, 2, 2.5, 0, 1000, empty, [ManaRequirement], type = 1, TargetHeal=True) 158 | Cure3 = WhitemageSpell(131, True, 2, 2.5, 0, 1500, empty, [ManaRequirement], type = 1, AOEHeal=True) 159 | Medica = WhitemageSpell(124, True, 2, 2.5, 0, 900, empty, [ManaRequirement], type = 1, AOEHeal=True) 160 | Medica2 = WhitemageSpell(133, True, 2, 2.5, 0, 1000, empty, [ManaRequirement], type = 1, AOEHeal=True) 161 | Raise = WhitemageSpell(125, True, 8, 2.5, 0, 2400, empty, [ManaRequirement]) 162 | 163 | #Damage oGCD 164 | Assize = WhitemageSpell(3571, False, 0, 0, 400, 0, ApplyAssize, [AssizeRequirement]) 165 | ThinAir = WhitemageSpell(7430, False, 0, 0, 0, 0, ApplyThinAir, [ThinAirRequirement]) 166 | PresenceOfMind = WhitemageSpell(136, False, 0, 0, 0, 0, ApplyPresenceOfMind, [PresenceOfMindRequirement]) 167 | 168 | #Healing oGCD 169 | Bell = WhitemageSpell(25862, False, 0, 0, 0, 0, ApplyBell, [BellRequirement]) #Litturgy of the bell 170 | Aquaveil = WhitemageSpell(25861, False, 0, 0, 0, 0, ApplyAquaveil, [AquaveilRequirement]) 171 | Temperance = WhitemageSpell(16536, False, 0, 0, 0, 0, ApplyTemperance, [TemperanceRequirement]) 172 | PlenaryIndulgence = WhitemageSpell(7433, False, 0, 0, 0, 0, ApplyPlenaryIndulgence, [PlenaryIndulgenceRequirement]) 173 | DivineBenison =WhitemageSpell(7432, False, 0, 0, 0, 0, ApplyDivineBenison, [DivineBenisonRequirement]) 174 | Tetragrammaton = WhitemageSpell(3570, False, 0, 0, 0, 0, ApplyTetragrammaton, [TetragrammatonRequirement], TargetHeal=True) 175 | Asylum = WhitemageSpell(3569, False, 0, 0, 0, 0, ApplyAsylum, [AsylumRequirement]) 176 | Benediction = WhitemageSpell(140, False, 0, 0, 0, 0, ApplyBenediction, [BenedictionRequirement], TargetHeal=True) 177 | 178 | WhiteMageAbility = { 179 | 25859 : Glare, 180 | 16532 : Dia, 181 | 25860 : Holy, 182 | 16535 : AfflatusMisery, 183 | 3571 : Assize, 184 | 120 : Cure, 185 | 135 : Cure2, 186 | 131 : Cure3, 187 | 124 : Medica, 188 | 133 : Medica2, 189 | 137 : Regen, 190 | 3570 : Tetragrammaton, 191 | 3569 : Asylum, 192 | 7432 : DivineBenison, 193 | 140 : Benediction, 194 | 16531 : AfflatusSolace, 195 | 16534 : AfflatusRapture, 196 | 136 : PresenceOfMind, 197 | 7430 : ThinAir, 198 | 16536 : Temperance, 199 | 25861 : Aquaveil, 200 | 25862 : Bell, 201 | 7433 : PlenaryIndulgence, 202 | 125 : Raise 203 | } -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Tank/Tank_Spell.py: -------------------------------------------------------------------------------- 1 | from ffxivcalc.Jobs.Base_Spell import Potion, Spell, empty 2 | from ffxivcalc.Jobs.Melee.Melee_Spell import ArmLength 3 | from ffxivcalc.Jobs.Player import MitBuff 4 | from ffxivcalc.Jobs.PlayerEnum import JobEnum 5 | Lock = 0.75 6 | 7 | 8 | 9 | class TankSpell(Spell): 10 | 11 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = 0): 12 | super().__init__(id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = type) 13 | 14 | 15 | ######################################### 16 | ########## WARRIOR Spell ############### 17 | ######################################### 18 | 19 | def BeastGaugeRequirement(Player, Spell): 20 | RemoveBeast(Player, Spell.Cost) 21 | return Player.BeastGauge >= 0, -1 22 | 23 | class WarriorSpell(TankSpell): 24 | 25 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, Cost, type = 0): 26 | super().__init__(id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = type) 27 | 28 | self.Requirement += [BeastGaugeRequirement] 29 | self.Cost = Cost 30 | 31 | def RemoveBeast(Player, Gauge): 32 | Player.BeastGauge -= Gauge #Caanot go under 0 cuz verify if enough gauge 33 | 34 | 35 | ######################################### 36 | ########## DARK KNIGHT SKILLS ########### 37 | ######################################### 38 | 39 | class DRKSkill(TankSpell): 40 | #A class for Dark Knight Skills containing all the relevant weaponskills/spells, cooldowns, 41 | #as well as their effects and requirements. For now does not consider out of combo actions. 42 | 43 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, ManaCost, BloodCost, Effect, Requirement, type = 0): 44 | super().__init__(id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = type) 45 | 46 | self.BloodCost = BloodCost 47 | ######################################### 48 | ########## PALADIN SKILLS ############## 49 | ######################################### 50 | 51 | class PaladinSpell(TankSpell): 52 | 53 | def __init__(self, id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, isPhysical, type = 0): 54 | super().__init__(id, GCD, CastTime, RecastTime, Potency, ManaCost, Effect, Requirement, type = type) 55 | 56 | self.isPhysical = isPhysical #To know to what ability we will give FoF 57 | 58 | 59 | 60 | ######################################### 61 | ########## GUNBREAKER SKILLS ########### 62 | ######################################### 63 | 64 | def PowderRequirement(Player, Spell): 65 | Player.PowderGauge -= Spell.PowderCost 66 | return Player.PowderGauge >=0, 0 67 | 68 | class GunbreakerSpell(TankSpell): 69 | 70 | def __init__(self, id, GCD, RecastTime, Potency, Effect, Requirement, PowderCost , type = 0): 71 | super().__init__(id, GCD, 0, RecastTime, Potency, 0, Effect, Requirement, type = type) 72 | 73 | self.PowderCost = PowderCost 74 | self.Requirement += [PowderRequirement] 75 | 76 | #Class Action 77 | 78 | #Requirement 79 | 80 | def RampartRequirement(Player, Spell): 81 | return Player.RampartCD <= 0, Player.RampartCD 82 | 83 | def LowBlowRequirement(Player, Spell): 84 | return Player.LowBlowCD <= 0, Player.LowBlowCD 85 | 86 | def ProvokeRequirement(Player, Spell): 87 | return Player.ProvokeCD <= 0, Player.ProvokeCD 88 | 89 | def InterjectRequirement(Player, Spell): 90 | return Player.InterjectCD <= 0, Player.InterjectCD 91 | 92 | def ReprisalRequirement(Player, Spell): 93 | return Player.ReprisalCD <= 0, Player.ReprisalCD 94 | 95 | def ShirkRequirement(Player, Spell): 96 | return Player.ShirkCD <= 0, Player.ShirkCD 97 | 98 | def BigMitRequirement(Player, Spell): 99 | return Player.BigMitCD <= 0, Player.BigMitCD 100 | 101 | def TankStanceRequirement(Player, Spell): 102 | return Player.TankStanceCD <= 0, Player.TankStanceCD 103 | 104 | #Apply 105 | 106 | def ApplyTankStance(Player, Enemy): 107 | if not Player.TankStanceOn: 108 | # Only goes in cooldown if the tank stance was already on 109 | Player.TankStanceOn = True 110 | else: 111 | Player.TankStanceCD = 3 112 | Player.TankStanceOn = False 113 | 114 | def ApplyTurnOffTankStance(Player, Enemy): 115 | Player.TankStanceOn = True 116 | 117 | def ApplyBigMit(Player, Enemy): 118 | Player.BigMitCD = 120 119 | BigMitBuff = MitBuff(0.7, 15, Player) 120 | Player.MitBuffList.append(BigMitBuff) # Appends buff 121 | 122 | # Keeps pointer to buff if Warrior 123 | if Player.JobEnum == JobEnum.Warrior: Player.VengeanceBuff = BigMitBuff 124 | elif Player.JobEnum == JobEnum.Paladin: Player.BigMitTimer = 15 125 | 126 | 127 | def ApplyRampart(Player, Enemy): 128 | Player.RampartCD = 90 129 | RampartBuff = MitBuff(0.8, 20, Player) 130 | Player.MitBuffList.append(RampartBuff) 131 | if Player.JobEnum == JobEnum.Paladin: Player.RampartTimer = 20 132 | 133 | def ApplyLowBlow(Player, Enemy): 134 | Player.LowBlowCD = 25 135 | 136 | def ApplyProvoke(Player, Enemy): 137 | Player.ProvokeCD = 30 138 | Player.TotalEnemity = Player.CurrentFight.GetEnemityList(1)[0].TotalEnemity + 300 139 | # Gives enemity to the tank equal to the maximum enemity + 10. 140 | # The values here are arbitrary. 10 enemity corresponds to 100'000 tank damage (with tank stance on) 141 | 142 | def ApplyInterject(Player, Enemy): 143 | Player.InterjectCD = 30 144 | 145 | def ApplyReprisal(Player, Enemy): 146 | Player.ReprisalCD = 60 147 | Enemy.Reprisal = True 148 | Enemy.ReprisalTimer = 10 149 | 150 | 151 | 152 | 153 | 154 | #ArmLength in Melee_Spell.py 155 | Rampart = TankSpell(7531, False, Lock, 0, 0, 0, ApplyRampart, [RampartRequirement]) 156 | LowBlow = TankSpell(7540, False, Lock, 0, 0, 0, ApplyLowBlow, [LowBlowRequirement]) 157 | Provoke = TankSpell(7533, False, Lock, 0, 0, 0, ApplyProvoke, [ProvokeRequirement]) 158 | Interject = TankSpell(10101010, False, Lock, 0, 0, 0, ApplyInterject, [InterjectRequirement]) 159 | Reprisal = TankSpell(7535, False, Lock, 0, 0, 0, ApplyReprisal, [ReprisalRequirement]) 160 | RoyalGuard = TankSpell(16142, False, 0, 0, 0, 0, ApplyTankStance, [TankStanceRequirement]) #Turn on Tank Stance 161 | Grit = TankSpell(3629, False, 0, 0, 0, 0, ApplyTankStance, [TankStanceRequirement]) 162 | IronWill = TankSpell(28, False, 0, 0, 0, 0, ApplyTankStance, [TankStanceRequirement]) 163 | Defiance = TankSpell(48, False, 0, 0, 0, 0, ApplyTankStance, [TankStanceRequirement]) 164 | TurnOffTankStance = TankSpell(0, False, 0, 0, 0, 0, ApplyTurnOffTankStance, [])#Turn off Tank Stance 165 | 166 | def Shirk(Target): 167 | """This function is used to generate the shirk action with a customized target. 168 | 169 | Target (Player) : Target of the shirk. 170 | 171 | """ 172 | 173 | def ApplyShirk(Player, Enemy): 174 | Player.ShirkCD = 120 175 | 176 | Target.TotalEnemity += Player.TotalEnemity * 0.25 177 | # Giving 25% of the Player's enemity to the target 178 | 179 | Player.TotalEnemity *= 0.75 180 | # Loosing 25% of the player's enemity 181 | 182 | custom_shirk = TankSpell(7537, False, Lock, 0, 0, 0, ApplyShirk, [ShirkRequirement]) 183 | custom_shirk.TargetID = Target.playerID 184 | return custom_shirk 185 | 186 | 187 | # Limit Break actions 188 | 189 | LB1Timer = 1.93 190 | LB2Timer = 3.86 191 | LB3Timer = 3.86 192 | 193 | TankLB1 = TankSpell(1111, False,LB1Timer, LB1Timer, 0, 0, empty, [], type=3) 194 | TankLB2 = TankSpell(1112, False,LB2Timer, LB2Timer, 0, 0, empty, [], type=3) 195 | TankLB3 = TankSpell(1113, False,LB3Timer, LB3Timer, 0, 0, empty, [], type=3) 196 | 197 | TankAbility = { 198 | 10101010 : Interject, 199 | 7531 : Rampart, 200 | 7535 : Reprisal, 201 | 7548 : ArmLength, 202 | 7540 : LowBlow, 203 | 7533 : Provoke, 204 | 7537 : Shirk, 205 | 16142 : RoyalGuard, #Gunbreaker Tank Stance 206 | 3629 : Grit, #DarkKnight Tank Stance 207 | 48 : Defiance, #Warrior Tank Stance 208 | 28 : IronWill, #Paladin Tank Stance 209 | 34590541 : Potion, 210 | 34592395 : Potion, 211 | 34594159 : Potion, 212 | 34594248 : Potion, # Idk what potion this is. 213 | -2 : Potion, 214 | 1111 : TankLB1, 215 | 1112 : TankLB2, 216 | 1113 : TankLB3 217 | } -------------------------------------------------------------------------------- /ffxivcalc/helperCode/exceptions.py: -------------------------------------------------------------------------------- 1 | class MateriaOverflow(Exception): 2 | """ 3 | This exception is raised when trying to add a Materia to a gear that has reached its limit. 4 | """ 5 | def __init__(self): 6 | self.message = "This gear piece cannot receive anymore Materias." 7 | 8 | super().__init__(self.message) 9 | 10 | def __str__(self) -> str: 11 | return f'{self.message}' 12 | 13 | class InvalidFunctionParameter(Exception): 14 | """ 15 | This exception is raised when a function's parameter is invalid. 16 | """ 17 | def __init__(self, funcName : str, paramName : str, info : str): 18 | self.message = "The parameter " + paramName + " in the function " + funcName + " is invalid. Info " + info 19 | 20 | super().__init__(self.message) 21 | 22 | def __str__(self) -> str: 23 | return f'{self.message}' 24 | 25 | class InvalidStatRequest(Exception): 26 | """ 27 | This exception is raised when the user does an invalid request of stat of a gear piece 28 | """ 29 | pass 30 | 31 | class InvalidGearSpace(Exception): 32 | """ 33 | This exception is raised when the GearSpace is invalid. 34 | """ 35 | def __init__(self, missingKey : str): 36 | self.message = "The GearSpace is missing gear piece of type " + missingKey 37 | 38 | super().__init__(self.message) 39 | 40 | def __str__(self) -> str: 41 | return f'{self.message}' 42 | 43 | class MultiValuedWeaponDelay(Exception): 44 | """This exception is raised when weapons in a gear space have different weapon delay value 45 | """ 46 | 47 | def __init__(self, weaponDelay : int, newWeaponDelay : int): 48 | self.message = f"The GearSpace has at least two weapons with different 'weaponDelay' value. Please make sure these values are identical : ( {weaponDelay} != {newWeaponDelay} )" 49 | 50 | super().__init__(self.message) 51 | 52 | def __str__(self) -> str: 53 | return f'{self.message}' 54 | 55 | class InvalidFoodSpace(Exception): 56 | """ 57 | This exception is raised when the FoodSpace is invalid. 58 | """ 59 | def __init__(self): 60 | self.message = "The FoodSpace cannot be empty." 61 | 62 | super().__init__(self.message) 63 | 64 | def __str__(self) -> str: 65 | return f'{self.message}' 66 | 67 | class InvalidMateriaSpace(Exception): 68 | """ 69 | This exception is raised when the MateriaSpace is invalid. 70 | """ 71 | def __init__(self): 72 | self.message = "The size of the MateriaSpace must be at least 3." 73 | 74 | super().__init__(self.message) 75 | 76 | def __str__(self) -> str: 77 | return f'{self.message}' 78 | class InvalidTankBusterTargetNumber(Exception): 79 | """ 80 | This exception is raised when an event is defined as a tank buster 81 | but has an invalid number of targets. Tank buster's number of targets 82 | must be 1 or 2. This error will be raised only if the Event has its "experimental" 83 | flag set to false (false by default). It must be, in any case, a positive value. 84 | 85 | Attributes: 86 | nTBTarget (int) : number of targets of the TB 87 | 88 | """ 89 | 90 | def __init__(self, nTBTarget : int, id : int): 91 | self.nTBTarget = nTBTarget 92 | self.message = "targets for the tank buster with id " + str(id) + " is not in the valid range of 1 or 2." 93 | 94 | super().__init__(self.message) 95 | 96 | def __str__(self) -> str: 97 | return f'{self.nTBTarget} {self.message}' 98 | 99 | class InvalidTarget(Exception): 100 | """ 101 | This exception is raised when a non valid target is given as input to an action 102 | 103 | e.g: If an action cannot target the player casting it 104 | If the targetID is not an existing player 105 | 106 | Attributes: 107 | ActionName (str) : Name of the action which is given non valid target 108 | Caster (object) : The player object of the caster of the action. 109 | Target (object) : What was passed as target. Will display it as a string. 110 | InvalidID (bool) : If the given target ID is not a valid ID. 111 | TargetID (int) : ID of the target. Only used if InvalidID is True. 112 | """ 113 | 114 | def __init__(self, ActionName : str, Caster, Target, InvalidID : bool, TargetID : int): 115 | self.Actionname = ActionName 116 | self.Target = str(Target) 117 | self.InvalidID = InvalidID 118 | self.message = "Player with ID "+str(Caster.playerID)+" has an invalid target. The given target for " + ActionName + " is not valid. Given target : " 119 | self.message_2 = "" if not InvalidID else " TargetID " + str(TargetID) + " is invalid." 120 | 121 | super().__init__(self.message) 122 | 123 | def __str__(self) -> str: 124 | return f'{self.message} {self.Target} {self.message_2}' 125 | 126 | def __repr__(self): 127 | return str(self) 128 | 129 | 130 | class InvalidMitigation(Exception): 131 | """This exception is raised when a non valid mitigation object is being constructed. 132 | Such examples would be MagicMit and PhysicalMit being both True or a MitPercent value out of the 133 | acceptable range of (0,1). 134 | 135 | Args: 136 | InvalidRange (bool): Type of the Error. True -> MitPercent OOR, False -> MagicMit and PhysicalMit == True 137 | PercentMit (float) : Value of the PercentMit given 138 | """ 139 | 140 | def __init__(self, InvalidRange = False, PercentMit : float = 0): 141 | self.PercentMit = PercentMit 142 | 143 | self.message = "A mitigation that is both only for Physical and for Magic damage was being constructed" if not InvalidRange else "The given PercentMit value " + str(self.PercentMit) + " is not within the valid range of (0,1)." 144 | super().__init__(self.message) 145 | 146 | def __str__(self) -> str: 147 | return f'{self.message}' 148 | 149 | class InvalidFileName(Exception): 150 | """This exception is raised when given an invalid file name 151 | 152 | fileName : str 153 | """ 154 | def __init__(self, fileName : str): 155 | 156 | self.message = "Invalid file name" + fileName 157 | super().__init__(self.message) 158 | 159 | def __str__(self) -> str: 160 | return f'{self.message}' 161 | 162 | class ActionNotFound(Exception):#Exception called if an action isn't found in the dictionnary 163 | pass 164 | class JobNotFound(Exception):#Exception called if a Job isn't found 165 | pass 166 | 167 | class playerIDNotFound(Exception):#Exception called if the player isn't found with the ID given by using the Fight.playerForID function. 168 | pass 169 | 170 | class emptyEventList(Exception): 171 | # Exception is called if trying to give an enemy an empty event list. 172 | pass 173 | 174 | class invalidFFLogsFightId(Exception): 175 | # Exception is called when the users asks to retrieve a fight from a fight list with a wrong id. 176 | def __init__(self, fight_id : str): 177 | 178 | self.message = "The provided fight id is invalid. Provided id : " + fight_id 179 | super().__init__(self.message) 180 | 181 | def __str__(self) -> str: 182 | return f'{self.message}' 183 | 184 | class invalidFFLogsQuery(Exception): 185 | # Exception is called when the users does an invalid query to fflogs 186 | def __init__(self, message : str): 187 | 188 | self.message = "Invalid FFLogs query. The query returned with this error message : " + message 189 | super().__init__(self.message) 190 | 191 | def __str__(self) -> str: 192 | return f'{self.message}' 193 | 194 | class outDatedFFLogsCode(Exception): 195 | # Exception is raised when the user imports the FFLogsAPIRequest module. 196 | def __init__(self): 197 | 198 | self.message = "The module 'ffxivcalc.Request.FFLogsAPIRequest' is no longer up to date. Use 'ffxivcalc.Request.FFLogs_api' instead." 199 | super().__init__(self.message) 200 | 201 | def __str__(self) -> str: 202 | return f'{self.message}' 203 | 204 | class outDatedAPICode(Exception): 205 | # Expcetion is raised when the user tries to import the API module. 206 | def __init__(self): 207 | 208 | self.message = "The module 'ffxivcalc.API' is outdated and has been deactivated, please do not import it. If you still want to use it add a try/except block (not recommend)." 209 | super().__init__(self.message) 210 | 211 | def __str__(self) -> str: 212 | return f'{self.message}' 213 | 214 | class deactivatedModuleCodeParser(Exception): 215 | # Expcetion is raised when the user tries to import the codeParser module. 216 | def __init__(self): 217 | 218 | self.message = "The module 'ffxivcalc.codeParser' is outdated and has been deactivated, please do not import it. If you still want to use it add a try/except block (not recommend)." 219 | super().__init__(self.message) 220 | 221 | def __str__(self) -> str: 222 | return f'{self.message}' 223 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Healer/Sage/Sage_Spell.py: -------------------------------------------------------------------------------- 1 | from ffxivcalc.Jobs.Base_Spell import DOTSpell, ManaRequirement, empty 2 | from ffxivcalc.Jobs.Healer.Healer_Spell import SageSpell 3 | from ffxivcalc.Jobs.Healer.Sage.Sage_Spell import SageSpell 4 | import copy 5 | 6 | Lock = 0.75 7 | 8 | 9 | #Requirement 10 | 11 | def PhysisRequirement(Player, Spell): 12 | return Player.PhysisCD <= 0, Player.PhysisCD 13 | 14 | def EukrasianDosisRequirement(Player, Spell): 15 | return Player.Eukrasia, -1 16 | 17 | def PneumaRequirement(Player, Spell): 18 | return Player.PneumaCD <= 0, Player.PneumaCD 19 | 20 | def PhlegmaRequirement(Player, Spell): 21 | return Player.PhlegmaStack > 0, Player.PhlegmaCD 22 | 23 | def AddersgallRequirement(Player, Enemy): 24 | return Player.AddersgallStack > 0, -1 25 | 26 | def KrasisRequirement(Player, Spell): 27 | return Player.KrasisCD <= 0, Player.KrasisCD 28 | 29 | def PanhaimaRequirement(Player, Spell): 30 | return Player.PanhaimaCD <= 0, Player.PanhaimaCD 31 | 32 | def HolosRequirement(Player, Spell): 33 | return Player.HolosCD <= 0, Player.HolosCD 34 | 35 | def RhizomataRequirement(Player, Spell): 36 | return Player.RhizomataCD <= 0, Player.RhizomataCD 37 | 38 | def HaimaRequirement(Player, Spell): 39 | return Player.HaimaCD <= 0, Player.HaimaCD 40 | 41 | def TaurocholeRequirement(Player, Spell): 42 | return Player.TaurocholeCD <= 0, Player.TaurocholeCD 43 | 44 | def PepsiRequirement(Player, Spell): 45 | return Player.PepsiCD <= 0, Player.PepsiCD 46 | 47 | def ZoeRequirement(Player, Spell): 48 | return Player.ZoeCD <= 0, Player.ZoeCD 49 | 50 | def IxocholeRequirement(Player, Spell): 51 | return Player.IxocholeCD <= 0, Player.IxocholeCD 52 | 53 | def KeracholeRequirement(Player, Spell): 54 | return Player.KeracholeCD <= 0, Player.KeracholeCD 55 | 56 | def IcarusRequirement(Player, Spell): 57 | return Player.IcarusCD <= 0, Player.IcarusCD 58 | 59 | def SoteriaRequirement(Player, Spell): 60 | return Player.SoteriaCD <= 0, Player.SoteriaCD 61 | 62 | def ToxikonRequirement(Player, Spell): 63 | return Player.AdderstingStack > 0, -1 64 | 65 | #Apply 66 | 67 | def ApplyPhysis(Player, Enemy): 68 | Player.PhysisCD = 60 69 | 70 | def ApplyToxikon(Player, Enemy): 71 | Player.AdderstingStack -= 1 72 | 73 | def ApplyEukrasianDiagnosis(Player, Enemy): 74 | Player.AdderstingStack = min(3, Player.AdderstingStack + 1) 75 | #We assume we can cast a Toxikon after casting this shield by assuming it breaks 76 | 77 | def ApplyDiagnosis(Player, Enemy): 78 | Player.AdderstingStack = min(3, Player.AdderstingStack + 1) #Will be assumed each time we do it gives a stack 79 | 80 | def ApplyKrasis(Player, Enemy): 81 | Player.KrasisCD = 60 82 | 83 | def ApplyPanhaima(Player, Enemy): 84 | Player.AdderstingStack = min(3, Player.AdderstingStack + 1) 85 | Player.PanhaimaCD = 120 86 | 87 | def ApplyHolos(Player, Enemy): 88 | Player.HolosCD = 120 89 | 90 | def ApplyRhizomata(Player, Enemy): 91 | Player.RhizomataCD = 90 92 | Player.AddersgallStack = min(3, Player.AddersgallStack + 1) 93 | 94 | def Apply(Player, Enemy): 95 | Player.CD = 0 96 | 97 | def ApplyHaima(Player, Enemy): 98 | Player.AdderstingStack = min(3, Player.AdderstingStack + 1) 99 | Player.HaimaCD = 120 100 | 101 | def ApplyTaurochole(Player, Enemy): 102 | Player.TaurocholeCD = 45 103 | Player.AddersgallStack -= 1 104 | 105 | def ApplyPepsi(Player, Enemy): 106 | Player.PepsiCD = 30 107 | 108 | def ApplyZoe(Player, Enemy): 109 | Player.ZoeCD = 90 110 | 111 | def ApplyKerachole(Player, Enemy): 112 | Player.KeracholeCD = 30 113 | Player.AddersgallStack -= 1 114 | 115 | def ApplyIxochole(Player, Enemy): 116 | Player.IxocholeCD = 30 117 | Player.AddersgallStack -= 1 118 | 119 | def ApplyIcarus(Player, Enemy): 120 | Player.IcarusCD = 45 121 | 122 | def ApplySoteria(Player, Enemy): 123 | Player.SoteriaCD = 90 124 | 125 | def ApplyDruochole(Player, Enemy): 126 | Player.AddersgallStack -= 1 127 | 128 | def ApplyEukrasia(Player, Enemy): 129 | Player.Eukrasia = True 130 | 131 | def ApplyPneuma(Player, Enemy): 132 | Player.PneumaCD = 120 133 | 134 | def ApplyPhlegma(Player, Enemy): 135 | if Player.PhlegmaStack == 2: 136 | Player.EffectCDList.append(PhlegmaStackCheck) 137 | Player.FlegmaCD = 40 138 | Player.PhlegmaStack -= 1 139 | 140 | def ApplyEukrasian(Player, Enemy): 141 | Player.Eukrasia = False 142 | if Player.Eukrasian == None: 143 | Player.Eukrasian = copy.deepcopy(EukrasianDOT) 144 | Player.EffectCDList.append(EukrasianDOTCheck) 145 | Player.DOTList.append(Player.Eukrasian) 146 | else : Player.Eukrasian.resetBuffSnapshot() # If already applied reset snapshot 147 | Player.EukrasianTimer = 30 148 | 149 | 150 | 151 | #Check 152 | 153 | def PhlegmaStackCheck(Player, Enemy): 154 | if Player.PhlegmaTimer <= 0: 155 | if Player.PhlegmaStack == 1: 156 | Player.EffectToRemove.append(PhlegmaStackCheck) 157 | else: 158 | Player.PhlegmaTimer = 45 159 | Player.PhlegmaStack +=1 160 | 161 | 162 | 163 | def EukrasianDOTCheck(Player, Enemy): 164 | if Player.EukrasianTimer <= 0: 165 | Player.EffectToRemove.append(EukrasianDOTCheck) 166 | Player.DOTList.remove(Player.Eukrasian) 167 | Player.Eukrasian = None 168 | 169 | 170 | #Damage GCD 171 | Dosis = SageSpell(24312, True, 1.5, 2.5, 330, 400, empty, [ManaRequirement], type = 1) 172 | EukrasianDosis = SageSpell(24314, True, 0, 1.5, 0, 400, ApplyEukrasian, [ManaRequirement, EukrasianDosisRequirement], type = 1) 173 | EukrasianDOT = DOTSpell(-12, 75, False) 174 | Phlegma = SageSpell(24313, True, Lock, 2.5, 490, 400, ApplyPhlegma, [PhlegmaRequirement], type = 1) 175 | Pneuma = SageSpell(24318, True, 1.5, 2.5, 330, 700, ApplyPneuma, [ManaRequirement, PneumaRequirement], type = 1) 176 | Toxikon = SageSpell(24316, True, 0, 2.5, 330, 0, ApplyToxikon, [ToxikonRequirement], type = 1) 177 | Dyskrasia = SageSpell(24315, True, 0, 2.5, 170, 400, empty, [ManaRequirement], type = 1) 178 | #Healing GCD 179 | Egeiro = SageSpell(24287, True, 8, 2.5, 0, 2400, empty, [ManaRequirement], type = 1) 180 | Prognosis = SageSpell(24286, True, 2, 2.5, 0, 800, empty, [ManaRequirement], type = 1, AOEHeal=True) 181 | EukrasianPrognosis = SageSpell(24292, True, 0, 1.5, 0, 900, empty, [ManaRequirement], type = 1, AOEHeal=True) 182 | Diagnosis = SageSpell(24284, True, 1.5, 2.5, 0, 400, ApplyDiagnosis, [ManaRequirement], type = 1) 183 | EukrasianDiagnosis = SageSpell(24291, True, 1.5, 1.5, 0, 900, ApplyEukrasianDiagnosis, [ManaRequirement], type = 1, TargetHeal=True) 184 | 185 | Eukrasia = SageSpell(24290, True, 1, 1, 0, 0, ApplyEukrasia, [], type = 1) #Since only 1 sec CD, no real need to put a requirement 186 | #Healing oGCD 187 | Krasis = SageSpell(24317, False, 0, 0, 0, 0, ApplyKrasis, [KrasisRequirement]) 188 | Panhaima = SageSpell(24311, False, 0, 0, 0, 0, ApplyPanhaima, [PanhaimaRequirement], AOEHeal=True) 189 | Holos = SageSpell(24310, False, 0, 0, 0, 0, ApplyHolos, [HolosRequirement], AOEHeal=True) 190 | Rhizomata = SageSpell(24309, False, 0, 0, 0, 0, ApplyRhizomata, [RhizomataRequirement]) 191 | Haima = SageSpell(24305, False, 0, 0, 0, 0, ApplyHaima, [HaimaRequirement]) 192 | Taurochole = SageSpell(24303, False, 0, 0, 0, 0, ApplyTaurochole, [TaurocholeRequirement,AddersgallRequirement], TargetHeal=True) 193 | Pepsi = SageSpell(24301, False, 0, 0, 0, 0, ApplyPepsi, [PepsiRequirement]) 194 | Zoe = SageSpell(24300, False, 0, 0, 0, 0, ApplyZoe, [ZoeRequirement]) #https://www.google.com/url?sa=i&url=https%3A%2F%2Fwww.leagueoflegends.com%2Fen-pl%2Fchampions%2Fzoe%2F&psig=AOvVaw2X9QcnXQ_CGp3xMY8MLua9&ust=1654295724885000&source=images&cd=vfe&ved=0CAwQjRxqFwoTCJjl4oPqj_gCFQAAAAAdAAAAABAD 195 | Ixochole = SageSpell(24299, False, 0, 0, 0, 0, ApplyIxochole, [IxocholeRequirement,AddersgallRequirement], AOEHeal=True) 196 | Kerachole = SageSpell(24298, False, 0, 0, 0, 0, ApplyKerachole, [KeracholeRequirement,AddersgallRequirement]) 197 | Icarus = SageSpell(24295, False, 0, 0, 0, 0, ApplyIcarus, [IcarusRequirement]) 198 | Soteria = SageSpell(24294, False, 0, 0, 0, 0, ApplySoteria, [SoteriaRequirement]) 199 | Druochole = SageSpell(24296, False, 0, 0, 0, 0, ApplyDruochole, [AddersgallRequirement], TargetHeal=True) 200 | Kardia = SageSpell(24285, False, 0, 0, 0, 0, empty, []) 201 | Physis = SageSpell(24302, False, 0, 0, 0, 0, ApplyPhysis, [PhysisRequirement]) 202 | 203 | SageAbility = { 204 | 24287 : Egeiro, 205 | 24312 : Dosis, 206 | 24314 : EukrasianDosis, 207 | 24316 : Toxikon, 208 | 24315 : Dyskrasia, 209 | 24313 : Phlegma, 210 | 24285 : Kardia, 211 | 24290 : Eukrasia, 212 | 24284 : Diagnosis, 213 | 24291 : EukrasianDiagnosis, 214 | 24286 : Prognosis, 215 | 24292 : EukrasianPrognosis, 216 | 24302 : Physis, 217 | 24294 : Soteria, 218 | 24295 : Icarus, 219 | 24296 : Druochole, 220 | 24298 : Kerachole, 221 | 24299 : Ixochole, 222 | 24300 : Zoe, 223 | 24301 : Pepsi, 224 | 24303 : Taurochole, 225 | 24305 : Haima, 226 | 24309 : Rhizomata, 227 | 24310 : Holos, 228 | 24311 : Panhaima, 229 | 24317 : Krasis, 230 | 24318 : Pneuma 231 | 232 | } -------------------------------------------------------------------------------- /ffxivcalc/Enemy.py: -------------------------------------------------------------------------------- 1 | 2 | from copy import deepcopy 3 | from ffxivcalc.helperCode.helper_math import roundDown 4 | from ffxivcalc.helperCode.exceptions import emptyEventList 5 | # Exception 6 | from ffxivcalc.helperCode.exceptions import InvalidTankBusterTargetNumber 7 | 8 | class EnemyEvent: 9 | """ 10 | This class represents an action or event the boss can take. Such events can be raidwide, a mechanic, untargetable, an enrage, etc. 11 | """ 12 | 13 | def __init__(self, id, CastTime : float, Damage : int, RaidWide=True, nTBTarget=0, IsPhysical = False, Experimental = False) -> None: 14 | """Constructor the of the EnemyEvent class 15 | 16 | Args: 17 | id (int): id of the event. 18 | CastTime (float): casting time of the event. 19 | Damage (int): Damage the players receive when the action is used. 20 | RaidWide (bool) : True if the Event is a raidwide. Default value is true 21 | nTBTarget (int) : Number of targets of a tank buster. Only needed if RaidWide is false. Must be 1 or 2 if Experimental is false 22 | IsPhysical (bool) : True if the damage of that event is physical. By default false (hence magical by default) 23 | Experimental (bool) : False if we do not wish to overrule the automatic checking for a valid action. 24 | """ 25 | 26 | if not Experimental and not RaidWide and (nTBTarget != 1 and nTBTarget != 2) or nTBTarget < 0 : 27 | # If a tankbuster but invalid number of targets and not experimental 28 | raise InvalidTankBusterTargetNumber(nTBTarget, id) 29 | 30 | self.CastTime = CastTime 31 | self.id = id 32 | self.Damage = Damage 33 | self.RaidWide = RaidWide 34 | self.nTBTarget = nTBTarget 35 | self.IsPhysical = IsPhysical 36 | self.target = [] # Empty list. The targets will be computed in begin_cast() 37 | 38 | 39 | def begin_cast(self, Enemy) -> None: 40 | """ 41 | This function will begin the casting of an action by the enemy. 42 | 43 | Enemy (Enemy) : Enemy object casting the Event 44 | 45 | """ 46 | 47 | Enemy.CastingTimer = self.CastTime 48 | Enemy.CastingAction = deepcopy(self) 49 | Enemy.IsCasting = True 50 | 51 | def cast(self, Enemy, RaidWide=True) -> None: 52 | """This function will cast and apply damage/effect on all the players 53 | 54 | Args: 55 | Enemy (Enemy): Enemy casting the action 56 | """ 57 | 58 | # Finds targets 59 | 60 | self.target = Enemy.CurrentFight.PlayerList if self.RaidWide else Enemy.CurrentFight.GetEnemityList(self.nTBTarget) 61 | # If the event is a raidwide the target is all players. 62 | # If it is not it targets the players with the most enemity up to the number of targets 63 | # Will do damage on all target 64 | 65 | # Computes new damage because mitigation on the Enemy 66 | # Does not differentiate between magical and physical for now 67 | curr_mit = 1 68 | if Enemy.Addle > 0: 69 | if self.IsPhysical : curr_mit = round(0.95 * curr_mit, 2) # 5% physical mit 70 | else : curr_mit = round(0.9 * curr_mit, 2) # 10% magic mit 71 | if Enemy.Feint > 0: 72 | if self.IsPhysical : curr_mit = round(0.9 * curr_mit, 2) # 10% physical mit 73 | else : curr_mit = round(0.95 * curr_mit, 2) # 5% magic mit 74 | if Enemy.Reprisal > 0: curr_mit = round(0.9 * curr_mit, 2) # Flat 10% mit 75 | 76 | self.Damage *= curr_mit # Updating the new damage based on global mit 77 | 78 | 79 | 80 | for player in self.target: 81 | # Going through all players 82 | # Mit according to their own personnal mit 83 | if self.IsPhysical : player_damage = self.Damage * player.PhysicalMitigation # Only applies physical mit 84 | else : player_damage = self.Damage * player.MagicMitigation # Only applies magic mit 85 | player_damage = round(player_damage, 0) # Rounding down to lowest integer 86 | player.TakeDamage(player_damage, not self.IsPhysical) # Applying the damage to the player 87 | 88 | Enemy.EventNumber += 1 # Incrementing the pointer to the next event 89 | 90 | if Enemy.EventNumber == len(Enemy.EventList): 91 | # If the enemy has reached the end of its action list 92 | Enemy.hasEventList = False 93 | 94 | class EnemyDOT(EnemyEvent): 95 | """ 96 | This class is any DOT applied by the enemy on the players. It will do damage over time on the players. 97 | """ 98 | 99 | def __init__(self, id, DOTDamage : int, DOTDuration): 100 | """ 101 | Creates a DOTDamage object. 102 | 103 | DOTDamage (int) : Damage every application of the DOT 104 | DOTDuration (float) : Duration of the DOT. 105 | """ 106 | self.id = id 107 | self.DOTDamage = DOTDamage 108 | self.DOTDuration = DOTDuration 109 | 110 | self.DOTStateTimer = 3 # The DOT will be applied every 3 seconds 111 | 112 | def updateState(self, time : float, player): 113 | """ 114 | Update the states of the DOT. Applies damag if neeeded. 115 | 116 | time (float) : time by which the DOT is updated. 117 | player (Player) : player object on which the DOT is applied. 118 | 119 | """ 120 | # Updating timers 121 | self.DOTStateTimer -= time 122 | self.DOTDuration -= time 123 | 124 | if self.DOTStateTimer <= 0: # Apply damage 125 | player.TakeDamage(self.DOTDamage) 126 | self.DOTStateTimer = 3 127 | 128 | if self.DOTDuration <= 0: 129 | # DOT is finished 130 | player.EnemyDOT.remove(self) 131 | 132 | class Enemy: 133 | 134 | """ 135 | This class is the enemy of the simulation. Contains some global effect that will apply to the entire group and keeps track of the total damage done 136 | """ 137 | 138 | def __init__(self): 139 | 140 | 141 | self.EffectList = [] # List of all effect on the boss. These are effects applied by the players 142 | 143 | self.TotalPotency = 0 144 | self.TotalDamage = 0 145 | 146 | self.buffList = [] # List of all buffs the boss gives when a player attacks them 147 | 148 | # Buff that can be applied directly on the boss 149 | self.ChainStratagem = False # +10% crit rate 150 | self.WanderingMinuet = False # +2% crit rate 151 | self.BattleVoice = False # +20% direct hit 152 | self.ArmyPaeon = False # + 3% direct hit 153 | 154 | # Mitigation buff 155 | self.Addle = False # 10% magical, 5% physical 156 | self.Feint = False # 10% physical, 5% magical 157 | self.Reprisal = False # 10% true mitigation 158 | 159 | 160 | self.EventList = [] # List of all EnemyEvent object this boss will perform through the simulation 161 | self.EventNumber = 0 # Current index of the EvenList action. 162 | self.hasEventList = False # By default an enemy has nothing to do 163 | # Timer 164 | self.CastingTimer = 0 165 | self.AddleTimer = 0 166 | self.FeintTimer = 0 167 | self.ReprisalTimer = 0 168 | 169 | # Currently Casting Action 170 | self.CastingAction = None 171 | self.IsCasting = False # If the Enemy is casting 172 | 173 | # Fight the Enemy is in 174 | self.CurrentFight = None 175 | 176 | def setEventList(self, newEventList) -> None: 177 | """ 178 | This function gives the Enemy an event list. This will result in a crash 179 | if the list is empty. 180 | 181 | Args: 182 | newEventList (List[EnemyEvent]): List of the Events to perform 183 | """ 184 | 185 | if len(newEventList) == 0 : raise emptyEventList 186 | 187 | self.hasEventList = True # Let the Fight know this Enemy has an EventList 188 | 189 | self.EventList = deepcopy(newEventList) 190 | 191 | def UpdateTimer(self, time : float) -> None: 192 | """This function updates the Timer values of the Enemy object 193 | 194 | Args: 195 | time (float): value by which we update the timer 196 | """ 197 | 198 | if self.AddleTimer > 0 : 199 | self.AddleTimer = max(0, self.AddleTimer - time) 200 | if self.AddleTimer == 0 : self.Addle = False 201 | if self.FeintTimer > 0 : 202 | self.FeintTimer = max(0, self.FeintTimer - time) 203 | if self.FeintTimer == 0 : self.Feint = False 204 | if self.ReprisalTimer > 0 : 205 | self.ReprisalTimer = max(0, self.ReprisalTimer - time) 206 | if self.ReprisalTimer == 0 : self.Reprisal = False 207 | 208 | if self.IsCasting : # Only decrements if the enemy is currently casting 209 | self.CastingTimer = max(0, self.CastingTimer - time) 210 | 211 | if self.CastingTimer <= 0 : # If reaches the end of the cast 212 | self.CastingAction.cast(self) # Cast the action 213 | self.IsCasting = False 214 | 215 | def WaitEvent(time : float) -> EnemyEvent: 216 | """ 217 | This function returns a EnemyEvent object which has no effect or damage and simply makes the Enemy way. 218 | 219 | time (float) : time in seconds we want the enemy to wait 220 | """ 221 | 222 | return EnemyEvent(-212,time, 0) 223 | 224 | MagicRaidWide = EnemyEvent(1, 2, 400) 225 | PhysicalRaidWide = EnemyEvent(3, 2, 500, IsPhysical=True) 226 | TankBuster = EnemyEvent(2, 2, 1000, RaidWide=False, nTBTarget=1) 227 | 228 | 229 | 230 | -------------------------------------------------------------------------------- /ffxivcalc/helperCode/Vocal.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains all function used to verbally show a simulation's result to the user 3 | and all other functions used to process the information offered by the library. 4 | """ 5 | import math 6 | import numpy as np 7 | import logging 8 | import matplotlib.pyplot as plt 9 | from ffxivcalc.helperCode.Progress import ProgressBar 10 | from ffxivcalc.helperCode.helper_math import roundDown 11 | logging.getLogger('matplotlib').setLevel(logging.INFO) # silencing matplotlib logger 12 | logging.getLogger('PIL').setLevel(logging.INFO) # silencing PIL logger 13 | 14 | 15 | from ffxivcalc.Jobs.PlayerEnum import JobEnum 16 | 17 | def SimulateRuns(fight, n : int, showBar : bool = True, pBNewBuffer = None): 18 | """ 19 | This function will simulate the fight with ZIPActions the given number of time and will 20 | generate the DPS distribution from it 21 | n (int) -> Number of times to run the random simulation 22 | showBar : bool -> If true shows pB. 23 | pBNewBuffer : dict -> This will receive pB's currents frame in the key 'pb' 24 | """ 25 | zipFightProgress = ProgressBar.init(n, "Computing DPS Dist") 26 | zipFightProgress.setShowBar(showBar) 27 | zipFightProgress.setExtraBuffer(pBNewBuffer) 28 | for i in range(n): 29 | fight.SimulateZIPFight() 30 | next(zipFightProgress) 31 | 32 | l = len(fight.PlayerList) 33 | fig, axs = plt.subplots((l // 4)+ (1 if l % 4 != 0 else 0), l if l < 4 else 4, constrained_layout=True) # DPS Crit distribution 34 | fig.suptitle("DPS Distribution (n = "+str(n)+" )") 35 | i = 0 # Used as coordinate for DPS distribution graph 36 | j = 0 37 | 38 | for player in fight.PlayerList: 39 | 40 | for runs in player.ZIPDPSRun: 41 | if str(runs) in player.DPSBar.keys(): 42 | player.DPSBar[str(runs)] += 1 43 | else: 44 | player.DPSBar[str(runs)] = 1 45 | 46 | # ordering dict 47 | keys = list(player.DPSBar.keys()) 48 | keys.sort() 49 | data = {i : player.DPSBar[i] for i in keys} 50 | 51 | Percent = int(n/100) 52 | percentile = [10,25,50,75,90,95,99] 53 | index = 0 54 | curTotal = 0 55 | for key in data: 56 | curTotal += data[key] 57 | if curTotal > (Percent * percentile[index]) : 58 | player.ZIPRunPercentile[str(percentile[index])] = key 59 | index += 1 60 | if index == len(percentile) : break 61 | 62 | x = [] 63 | y = [] 64 | title = "" if player.PlayerName == "" else player.PlayerName + " ID - " + str(player.playerID) 65 | for bar in data: 66 | x += [float(bar)] 67 | y += [player.DPSBar[bar]/n] 68 | if l == 1: 69 | axs.plot(x, y) 70 | axs.plot([player.TotalDamage/fight.TimeStamp,player.TotalDamage/fight.TimeStamp], [0, 0.01]) 71 | axs.set_ylim(ymin=0) 72 | axs.set_title(title) 73 | elif l <= 4: 74 | axs[i].plot(x, y) 75 | axs[i].plot([player.TotalDamage/fight.TimeStamp,player.TotalDamage/fight.TimeStamp], [0, 0.01]) 76 | axs[i].set_ylim(ymin=0) 77 | axs[i].set_title(title) 78 | else: 79 | axs[j][i].plot(x, y) 80 | axs[j][i].plot([player.TotalDamage/fight.TimeStamp,player.TotalDamage/fight.TimeStamp], [0, 0.01]) 81 | axs[j][i].set_ylim(ymin=0) 82 | axs[j][i].set_title(title) 83 | i+=1 84 | if i == 4: 85 | i = 0 86 | j+=1 87 | return fig 88 | # Functions to print out all the results and plot DPS/PPS graph 89 | 90 | def PrintResult(self, time : float, TimeStamp, PPSGraph : bool = True) -> str: 91 | """ 92 | This function puts the result in a string that it will return which can then be printed 93 | self : Fight -> Fight we want the result of to be printed 94 | time : float -> final timestamp of the simulation 95 | TimeStamp : List[float] -> list of all timestamp where the DPS was saved in memory. Used to generate the graphs 96 | PPSGraph : bool -> If want the PPS graph next to the DPS graph 97 | """ 98 | 99 | result_string = "The Fight finishes at: " + str(time) + "\n========================\n" 100 | fig, axs = plt.subplots(1, 2 if PPSGraph else 1, constrained_layout=True) # DPS and PPS graph 101 | HPFig, HPaxs = plt.subplots(1,1, constrained_layout=True) 102 | 103 | HPaxs.set_ylabel("HP") 104 | HPaxs.set_xlabel("Time (s)") 105 | HPaxs.set_title("HP over time") 106 | HPaxs.spines["top"].set_alpha(0.0) 107 | HPaxs.spines["right"].set_alpha(0.0) 108 | HPaxs.set_facecolor("lightgrey") 109 | 110 | if PPSGraph: 111 | axs[0].set_ylabel("DPS") 112 | axs[0].set_xlabel("Time (s)") 113 | axs[0].set_title("DPS over time") 114 | axs[0].spines["top"].set_alpha(0.0) 115 | axs[0].spines["right"].set_alpha(0.0) 116 | axs[0].set_facecolor("lightgrey") 117 | axs[1].set_ylabel("PPS") 118 | axs[1].set_xlabel("Time (s)") 119 | axs[1].set_title("PPS over time") 120 | axs[1].spines["top"].set_alpha(0.0) 121 | axs[1].spines["right"].set_alpha(0.0) 122 | axs[1].set_facecolor("lightgrey") 123 | else: 124 | axs.set_ylabel("DPS") 125 | axs.set_xlabel("Time (s)") 126 | axs.set_title("DPS over time") 127 | axs.spines["top"].set_alpha(0.0) 128 | axs.spines["right"].set_alpha(0.0) 129 | axs.set_facecolor("lightgrey") 130 | 131 | fig.suptitle("Damage over time.") 132 | 133 | i = 0 # Used as coordinate for DPS distribution graph 134 | j = 0 135 | 136 | for player in self.PlayerList: 137 | HPaxs.plot(player.HPGraph[0], player.HPGraph[1], color="green") 138 | HPaxs.set_ylim(ymin=0) 139 | if time == 0: time = 1 # This is only so we don't have division by 0 error 140 | 141 | if player.TotalPotency == 0: 142 | PPS = 0 143 | DPS = 0 144 | else: 145 | PPS = player.TotalPotency / time 146 | DPS = player.TotalDamage / time 147 | 148 | result_string += ( 149 | "Results for " + str(JobEnum.name_for_id(player.JobEnum)) + ("" if player.PlayerName == "" else " " + player.PlayerName) + " ID - " + str(player.playerID) + " :\n" + 150 | "DPS : " + str(round(DPS,2)) + 151 | " PPS : " + str(round(PPS,2)) + 152 | " TP : " + str(round(player.TotalPotency,2)) + 153 | " GCD : " + str(player.GCDCounter) + 154 | " TD : " + str(player.TotalDamage) + "\n" 155 | ) 156 | 157 | # DPS Percentile if applies 158 | 159 | result_string += ("DPS Percentile" + str(player.ZIPRunPercentile) + "\n") if len(player.ZIPRunPercentile.keys()) > 0 else "" 160 | 161 | 162 | # Plot part 163 | 164 | job = JobEnum.name_for_id(player.JobEnum) 165 | 166 | 167 | if player.JobEnum == JobEnum.Bard : 168 | result_string += ( 169 | "Procs/Gauge result (Used/Expecte) : \n" + 170 | "Refulgent : " + str(round(player.UsedRefulgent,2)) + "/" + str(round(player.ExpectedRefulgent,2)) + 171 | " Wanderer Repertoire : " + str(round(player.UsedTotalWandererRepertoire,2)) + "/" + str(round(player.ExpectedTotalWandererRepertoire,2)) + 172 | " RepertoireAdd : " + str(round(player.UsedRepertoireAdd,2)) + 173 | "\nSoul Voice : " + str(round(player.UsedSoulVoiceGauge,2)) + "/" + str(round(player.ExpectedSoulVoiceGauge,2)) + 174 | " Soul BloodLetter Reduction : " + str(round(player.UsedBloodLetterReduction,2)) + "/" + str(round(player.ExpectedBloodLetterReduction,2)) 175 | ) 176 | elif player.JobEnum == JobEnum.Dancer: 177 | result_string += ( 178 | "Procs/Gauge result (Used/Expected) : \n" + 179 | "Silken Symettry : " + str(round(player.UsedSilkenSymettry,2)) + "/" + str(round(player.ExpectedSilkenSymettry,2)) + 180 | " Silken Flow : " + str(round(player.UsedSilkenFlow,2)) + "/" + str(round(player.ExpectedSilkenFlow,2)) + 181 | "\nFourfold Feather : " + str(round(player.UsedFourfoldFeather,2)) + "/" + str(round(player.ExpectedFourfoldFeather,2)) + 182 | " Threefold Fan : " + str(round(player.UsedThreefoldFan,2)) + "/" + str(round(player.ExpectedThreefoldFan,2)) 183 | ) 184 | elif player.JobEnum == JobEnum.RedMage: 185 | result_string += ( 186 | "Procs/Gauge result (Used/Expected) : \n" + 187 | "Verfire : " + str(round(player.UsedVerfireProc,2)) + "/" + str(round(player.ExpectedVerfireProc,2)) + 188 | " Verstone : " + str(round(player.UsedVerstoneProc,2)) + "/" + str(round(player.ExpectedVerstoneProc,2)) 189 | ) 190 | 191 | 192 | result_string += "\n=================\n" 193 | 194 | job_label = job + ("" if player.PlayerName == "" else (" " + player.PlayerName)) + " ID - " + str(player.playerID) 195 | if PPSGraph: 196 | axs[0].plot(TimeStamp,player.DPSGraph, label=job_label) 197 | axs[1].plot(TimeStamp,player.PotencyGraph, label=job_label) 198 | else: 199 | axs.plot(TimeStamp,player.DPSGraph, label=job_label) 200 | 201 | #if len(self.PlayerList) <= 8: 202 | # if DPS != 0 : ComputeDPSDistribution(self, player, fig2, axs2[j][i], job) 203 | # i+=1 204 | # if i == 4: 205 | # i = 0 206 | # j+=1 207 | result_string += ( 208 | "Total DPS : " + str(round(self.Enemy.TotalDamage / time, 2) if time != 0 else "0" ) + "\t" + 209 | "Total PPS : " + str(round(self.Enemy.TotalPotency/time,2) if time != 0 else "0" ) + "\t" + 210 | "Total Potency : " + str(round(self.Enemy.TotalPotency,2)) 211 | ) 212 | if PPSGraph: 213 | axs[0].xaxis.grid(True) 214 | axs[1].xaxis.grid(True) 215 | else: 216 | axs.legend() 217 | 218 | if len(TimeStamp) == 0: # If for some reason Fight ended before 3 second TimeStamp is empty and we need to do an edge case to avoid a crash 219 | # Note that however the plot will be empty since no damage has been sampled 220 | if PPSGraph: 221 | axs[0].xaxis.set_ticks(np.arange(0, 4, 25)) 222 | axs[1].xaxis.set_ticks(np.arange(0, 4, 25)) 223 | else: 224 | axs.xaxis.set_ticks(np.arange(0, 4, 25)) 225 | else: 226 | if PPSGraph: 227 | axs[0].xaxis.set_ticks(np.arange(0, max(TimeStamp)+1, 25)) 228 | axs[1].xaxis.set_ticks(np.arange(0, max(TimeStamp)+1, 25)) 229 | else: 230 | axs.xaxis.set_ticks(np.arange(0, max(TimeStamp)+1, 25)) 231 | 232 | if PPSGraph: 233 | axs[0].legend() 234 | axs[1].legend() 235 | else: 236 | axs.legend() 237 | 238 | return result_string, fig 239 | 240 | -------------------------------------------------------------------------------- /ffxivcalc/UI/TUI.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ffxivcalc.UI.SimulationInput import ExecuteMemoryCode 3 | from ffxivcalc.helperCode.helper_backend import AskInput, ImportFightBackend, MergeFightBackEnd, SaveFight, SimulateFightBackend,GenerateLayoutBackend 4 | from ffxivcalc.Jobs.PlayerEnum import JobEnum 5 | 6 | def SimulateFightMemory(): 7 | os.system('CLS') #clearing HUD 8 | #This will simulate the fight in memory 9 | 10 | #We will first go through some options 11 | print( 12 | "===================== SIMULATOR =====================" 13 | ) 14 | 15 | #Looking at all fights we have saved 16 | 17 | cur_dir = os.getcwd() 18 | 19 | #the saved directory should be in that same folder 20 | 21 | saved_dir = cur_dir + "\\saved" 22 | 23 | saved_fight = os.listdir(saved_dir) 24 | number_saved_fight = len(saved_fight) 25 | selected_fight = "" 26 | 27 | if number_saved_fight == 0: #No fight saved 28 | print("No saved fights were found. Hit any button to return to the main menu.") 29 | input("...") 30 | return 31 | else: #Saved fight 32 | print( 33 | "Select one of the saved fights : "+ "\n" + 34 | "==========================================================" 35 | ) 36 | for i in range(1,number_saved_fight+1): 37 | print(str(i) + " -> " + saved_fight[i-1]) 38 | 39 | print("==========================================================") 40 | 41 | userInput = int(AskInput(number_saved_fight)) 42 | 43 | selected_fight = saved_fight[userInput-1] #Selecting fight 44 | 45 | 46 | SimulateFightBackend(saved_dir + "\\" + selected_fight) 47 | 48 | 49 | 50 | def ImportFight(): 51 | os.system('CLS') #clearing HUD 52 | print( 53 | "===================== IMPORT FIGHT FROM FFLOGS =====================" + "\n" + 54 | "THIS IS VERY EXPERIMENTAL" + "\n" + 55 | "The program does not check if the fight is valid, and will simply result in a crash if it is not." 56 | ) 57 | 58 | fightID = input("Please enter the fightID : ") 59 | fightNumber = input("Please enter the fight's number : ") 60 | 61 | print("Converting FFLogs fight into Event object...") 62 | 63 | Event = ImportFightBackend(fightID, fightNumber) 64 | 65 | userInput = input("What name do you wish to save this fight as : ") 66 | 67 | SaveFight(Event, 0, 1000, userInput) 68 | 69 | 70 | 71 | 72 | def CreateFight(): 73 | os.system('CLS') #clearing HUD 74 | 75 | def Credits(): 76 | os.system('CLS') #clearing HUD 77 | #This menu displays credits 78 | print( 79 | "=================================================== CREDITS =======================================================" + "\n" + 80 | "This program was made mainly by myself, but I was helped by a lot of friends" + "\n" + 81 | "and other people without whom this project would not have been possible." + "\n" + 82 | "I listed here all those that helped with a short description of what they did." + "\n" + 83 | "I personally thank everyone here for the help they brought :)" + "\n" + 84 | "I will put their discord IDs in case you wish to contact them." + "\n" + 85 | "=================================================================================================================" + "\n" + 86 | "Pythagoras#6312 -> That's me! Did most of it" + "\n" + 87 | "ne0dym#7837 -> DarkKnight code and other revisions" + "\n" + 88 | "Bri-kun#6539 -> Networking code and other FFLogsAPI related stuff" + "\n" + 89 | "javaJake#0001 -> Theoretical damage computation" + "\n" + 90 | "Saint Germain#0280 -> Bit of Monk code" + "\n" + 91 | "apollo#3810 -> Put me in contact with javajake and also provided some background code for working with FFLogsAPI" + "\n" + 92 | "Ikuni#2735 -> Helped by verifying and correcting some scalar values in the damage formulas" + "\n" + 93 | "redshadowhero#8631 -> Helped restructure the code in a better way and gave other advices" + "\n" + 94 | "\n" + 95 | "I also give my thanks to The Balance, which provided me with a lot of knowleadge about the different" + "\n" + 96 | "jobs so I could implement them and also gave me access to resources like the Blackmage gear comparison" + "\n" + 97 | "spreadsheet which I used a lot when trying to better understand how damage computation work in this game." + "\n" + 98 | "=================================================================================================================" 99 | ) 100 | 101 | input("Press Enter to go back to the Main Menu : ") 102 | 103 | 104 | def GenerateLayout(): 105 | os.system('CLS') #clearing HUD 106 | print( 107 | "===================== GENERATING LAYOUT =====================" + "\n" 108 | ) 109 | 110 | namefile = input("What name do you wish to save this fight as : ") 111 | 112 | player_list = [] 113 | 114 | while True: 115 | os.system('CLS') #clearing HUD 116 | print( 117 | "===================================================================" + "\n" + 118 | "Select the job you want to add or enter 20" + "\n" + 119 | "===================================================================" + "\n" + 120 | "1- BlackMage" + "\n" + 121 | "2- RedMage" + "\n" + 122 | "3- Summoner" + "\n" + 123 | "4- Ninja" + "\n" + 124 | "5- Samurai" + "\n" + 125 | "6- Monk" + "\n" + 126 | "7- Reaper" + "\n" + 127 | "8- Dragoon" + "\n" + 128 | "9- Machinist" + "\n" + 129 | "10- Bard" + "\n" + 130 | "11- Dancer" + "\n" + 131 | "12- WhiteMage" + "\n" + 132 | "13- Scholar" + "\n" + 133 | "14- Astrologian" + "\n" + 134 | "15- Sage" + "\n" + 135 | "16- DarkKnight" + "\n" + 136 | "17- Gunbreaker" + "\n" + 137 | "18- Warrior" + "\n" + 138 | "19- Paladin" + "\n" + 139 | "20- Done" + "\n" + 140 | "===================================================================" 141 | ) 142 | user_int = int(AskInput(20)) 143 | 144 | 145 | match user_int: 146 | case 1: player_list += [JobEnum.BlackMage] 147 | case 2: player_list += [JobEnum.RedMage] 148 | case 3: player_list += [JobEnum.Summoner] 149 | case 4: player_list += [JobEnum.Ninja] 150 | case 5: player_list += [JobEnum.Samurai] 151 | case 6: player_list += [JobEnum.Monk] 152 | case 7: player_list += [JobEnum.Reaper] 153 | case 8: player_list += [JobEnum.Dragoon] 154 | case 9: player_list += [JobEnum.Machinist] 155 | case 10: player_list += [JobEnum.Bard] 156 | case 11: player_list += [JobEnum.Dancer] 157 | case 12: player_list += [JobEnum.WhiteMage] 158 | case 13: player_list += [JobEnum.Scholar] 159 | case 14: player_list += [JobEnum.Astrologian] 160 | case 15: player_list += [JobEnum.Sage] 161 | case 16: player_list += [JobEnum.DarkKnight] 162 | case 17: player_list += [JobEnum.Gunbreaker] 163 | case 18: player_list += [JobEnum.Warrior] 164 | case 19: player_list += [JobEnum.Paladin] 165 | case 20: break 166 | 167 | GenerateLayoutBackend(player_list, namefile) 168 | 169 | 170 | def MergeFight(): 171 | os.system('CLS') #clearing HUD 172 | print( 173 | "===================== MERGING TWO FIGHTS =====================" + "\nWARNING : This function does not make sure every player has a different playerID. So once the merging is done\nmake sure they all have unique playerID." 174 | ) 175 | 176 | cur_dir = os.getcwd() 177 | 178 | #the saved directory should be in that same folder 179 | 180 | saved_dir = cur_dir + "\\saved" 181 | 182 | saved_fight = os.listdir(saved_dir) 183 | number_saved_fight = len(saved_fight) 184 | parent_fight = "" 185 | child_fight = "" 186 | 187 | if number_saved_fight == 0: #No fight saved 188 | print("No saved fights were found. Hit any button to return to the main menu.") 189 | input("...") 190 | return 191 | else: #Saved fight 192 | print( 193 | "Select one of the saved fights as a parent (will merge into this one) : "+ "\n" + 194 | "==========================================================" 195 | ) 196 | for i in range(1,number_saved_fight+1): 197 | print(str(i) + " -> " + saved_fight[i-1]) 198 | 199 | print("==========================================================") 200 | 201 | userInput = int(AskInput(number_saved_fight)) 202 | 203 | parent_fight = saved_fight[userInput-1] #Selecting fight 204 | 205 | 206 | print( 207 | "Select one of the saved fights as a child (this one will be merged into) : "+ "\n" + 208 | "==========================================================" 209 | ) 210 | for i in range(1,number_saved_fight+1): 211 | print(str(i) + " -> " + saved_fight[i-1]) 212 | 213 | print("==========================================================") 214 | 215 | userInput = int(AskInput(number_saved_fight)) 216 | 217 | child_fight = saved_fight[userInput-1] #Selecting fight 218 | 219 | MergeFightBackEnd(saved_dir + "\\" + child_fight, saved_dir + "\\" + parent_fight, parent_fight) 220 | 221 | 222 | 223 | 224 | def MainMenu(): 225 | os.system('CLS') #clearing HUD 226 | #Welcome Message 227 | print( 228 | "===================== FFXIV Combat Simulator ======================" + "\n" + 229 | "MAIN MENU (input what you want and press ENTER)" + "\n" + 230 | "===================================================================" + "\n" + 231 | #"1- Simulate fight in code memory" + "\n" + 232 | #"2- Save fight in code memory" + "\n" + 233 | "1- Simulate a saved fight" + "\n" + 234 | "2- Merge two saved fights" + "\n" + 235 | "3- Import fight from FFLogs (Experimental)" + "\n" + 236 | "4- Create JSON template file" + "\n" + 237 | "5- Credits" + "\n" + 238 | "6- Exit" + "\n" + 239 | "===================================================================" 240 | ) 241 | user_input = AskInput(6) 242 | 243 | #if user_input == "1" : ExecuteMemoryCode(False) #Simulating 244 | #elif user_input == "2": ExecuteMemoryCode(True) #Saving 245 | if user_input == "1" : SimulateFightMemory() 246 | elif user_input == "2" : MergeFight() 247 | elif user_input == "3" : ImportFight() 248 | elif user_input == "4" : GenerateLayout() 249 | elif user_input == "5" : Credits() #Closes program 250 | elif user_input == "6" : exit() #Closes program 251 | 252 | 253 | def TUI_draw(): 254 | while True : MainMenu() #Draws Main Menu 255 | 256 | #This python file will serve as a GUI for the time I do not have an actual GUI 257 | if __name__ == "__main__" : 258 | #working_dir = r" INSERT WORKING DIRECTORY HERE " 259 | #os.chdir(working_dir) 260 | while True : MainMenu() #Draws Main Menu 261 | 262 | 263 | -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Healer/Scholar/Scholar_Spell.py: -------------------------------------------------------------------------------- 1 | ######################################### 2 | ########## SCHOLAR PLAYER ############### 3 | ######################################### 4 | 5 | from ffxivcalc.Jobs.Healer.Healer_Spell import ScholarSpell 6 | from ffxivcalc.Jobs.Player import HealingBuff, MitBuff 7 | 8 | from ffxivcalc.Jobs.Base_Spell import DOTSpell, empty, ManaRequirement, buffHistory 9 | import copy 10 | Lock = 0.75 11 | 12 | def AetherflowRequirement(Player, Spell): 13 | return Player.AetherFlowCD <= 0, Player.AetherFlowCD 14 | 15 | def ChainStratagemRequirement(Player, Spell): 16 | return Player.ChainStratagemCD <= 0, Player.ChainStratagemCD 17 | 18 | def AetherStackRequirement(Player, Spell): 19 | return Player.AetherFlowStack > 0, -1 20 | 21 | def DissipationRequirement(Player, Spell): 22 | return Player.DissipationCD <= 0 and Player.SummonTimer <= 0, Player.DissipationCD #Cannot use while summoned 23 | 24 | def ExpedientRequirement(Player, Spell): 25 | return Player.ExpedientCD <= 0, Player.ExpedientCD 26 | 27 | def ProtractionRequirement(Player, Spell): 28 | return Player.ProtractionCD <= 0, Player.ProtractionCD 29 | 30 | def RecitationRequirement(Player, Spell): 31 | return Player.RecitationCD <= 0, Player.RecitationCD 32 | 33 | def EmergencyTacticRequirement(Player, Spell): 34 | return Player.EmergencyTacticCD <= 0, Player.EmergencyTacticCD 35 | 36 | def DeploymentTacticRequirement(Player, Spell): 37 | return Player.DeploymentTacticCD <= 0, Player.DeploymentTacticCD 38 | 39 | def IndomitabilityRequirement(Player, Spell): 40 | return Player.IndomitabilityCD <= 0, Player.IndomitabilityCD 41 | 42 | def LustrateRequirement(Player, Spell): 43 | return Player.LustrateCD <= 0, Player.LustrateCD 44 | 45 | def SacredSoilRequirement(Player, Spell): 46 | return Player.SacredSoilCD <= 0, Player.SacredSoilCD 47 | 48 | def ExcogitationRequirement(Player, Spell): 49 | return Player.ExcogitationCD <= 0, Player.ExcogitationCD 50 | 51 | def AdloquiumRequirement(Player, Spell): 52 | if Player.Recitation: 53 | return True, -1 #if we have it 54 | else: 55 | return ManaRequirement(Player, Spell) #Else return ManaRequirement 56 | 57 | def AetherHealRequirement(Player, Spell): 58 | return Player.AetherFlowStack > 0 or Player.Recitation, -1 59 | 60 | def SummonSeraphRequirement(Player, Spell): 61 | return Player.SummonSeraphCD <= 0, Player.SummonSeraphCD 62 | 63 | def ConsolationRequirement(Player, Spell): 64 | return Player.ConsolationStack > 0, -1 65 | 66 | def FeyBlessingRequirement(Player, Spell): 67 | return Player.FeyBlessingCD <= 0, Player.FeyBlessingCD 68 | 69 | def FeyIlluminationRequirement(Player, Spell): 70 | return Player.FeyIlluminationCD <= 0, Player.FeyIlluminationCD 71 | 72 | def WhisperingDawnRequirement(Player, Spell): 73 | return Player.WhisperingDawnCD <= 0, Player.WhisperingDawnCD 74 | 75 | #==================================== 76 | 77 | def ApplyFeyBlessing(Player, Enemy): 78 | Player.FeyBlessingCD = 60 79 | 80 | def ApplyFeyIllumination(Player, Enemy): 81 | Player.FeyIlluminationCD = 120 82 | 83 | for player in Player.CurrentFight.PlayerList: 84 | player.AddHealingBuff(HealingBuff(1.1, 20, player, GivenHealBuff=False, BuffName="FeyIllumination")) 85 | player.AddMitBuff(MitBuff(0.95, 20, player, MagicMit=True, BuffName="FeyIllumination")) 86 | 87 | def ApplyWhisperingDawn(Player, Enemy): 88 | Player.WhisperingDawnCD = 60 89 | 90 | def Apply(Player, Enemy): 91 | Player.CD = 0 92 | Player.AetherFlowStack -= 1 93 | 94 | def ApplyConsolation(Player, Enemy): 95 | Player.ConsolationStack -= 1 96 | 97 | def ApplySummonSeraph(Player, Enemy): 98 | Player.SummonTimer = 22 99 | Player.SummonSeraphCD = 120 100 | Player.ConsolationStack = 2 101 | 102 | def ApplyIndomitability(Player, Enemy): 103 | Player.IndomitabilityCD = 30 104 | if Player.Recitation : Player.Recitation = False 105 | else: Player.AetherFlowStack -= 1 106 | 107 | def ApplyLustrate(Player, Enemy): 108 | Player.LustrateCD = 1 109 | Player.AetherFlowStack -= 1 110 | 111 | def ApplySacredSoil(Player, Enemy): 112 | Player.SacredSoilCD = 30 113 | Player.AetherFlowStack -= 1 114 | 115 | for player in Player.CurrentFight.PlayerList: 116 | player.AddMitBuff(MitBuff(0.9, 15, player, BuffName="SacredSoil")) 117 | 118 | def ApplyExcogitation(Player, Enemy): 119 | Player.ExcogitationCD = 45 120 | if Player.Recitation : Player.Recitation = False 121 | else: Player.AetherFlowStack -= 1 122 | 123 | def ApplyAdloquium(Player, Enemy): 124 | if Player.Recitation : Player.Recitation = False 125 | 126 | def ApplyExpedient(Player, Enemy): 127 | Player.ExpedientCD = 120 128 | 129 | for player in Player.CurrentFight.PlayerList: 130 | player.AddMitBuff(MitBuff(0.9, 20, player, BuffName="Expedient")) 131 | 132 | def ApplyProtraction(Player, Enemy): 133 | Player.ProtractionCD = 60 134 | 135 | def ApplyRecitation(Player, Enemy): 136 | Player.RecitationCD = 90 137 | Player.Recitation = True 138 | 139 | def ApplyEmergencyTactic(Player, Enemy): 140 | Player.EmergencyTacticCD = 15 141 | 142 | def ApplyDeploymentTactic(Player, Enemy): 143 | Player.DeploymentTacticCD = 90 144 | 145 | def ApplyBiolysis(Player, Enemy): 146 | if (not (Player.Biolysis in Player.DOTList)): 147 | Player.Biolysis = copy.deepcopy(BiolysisDOT) 148 | Player.DOTList.append(Player.Biolysis) 149 | Player.EffectCDList.append(CheckBiolysis) 150 | else : Player.Biolysis.resetBuffSnapshot() # If already applied reset snapshot 151 | Player.BiolysisTimer = 30 152 | 153 | def ApplyAetherflow(Player, Enemy): 154 | Player.Mana = min(10000, Player.Mana + 2000) 155 | Player.AetherFlowStack = 3 156 | Player.AetherFlowCD = 60 157 | 158 | def ApplyChainStratagem(Player, Enemy): 159 | Player.ChainStratagemCD = 120 160 | Player.ChainStratagemTimer = 15 161 | Enemy.ChainStratagem = True 162 | Player.EffectCDList.append(CheckChainStratagem) 163 | 164 | # Only doing this if SavePreBakedAction is true 165 | #if Player.CurrentFight.SavePreBakedAction: 166 | # fight = Player.CurrentFight 167 | # history = buffHistory(fight.TimeStamp, fight.TimeStamp + 15) 168 | # fight.PlayerList[fight.PlayerIDSavePreBakedAction].ChainStratagemHistory.append(history) 169 | 170 | def ApplyEnergyDrain(Player, Enemy): 171 | Player.EnergyDrainCD = 1 172 | Player.AetherFlowStack -= 1 173 | 174 | def ApplyDissipation(Player, Enemy): 175 | Player.DissipationCD = 180 176 | Player.AetherFlowStack = 3 177 | Player.GivenHealBuffList.append(HealingBuff(1.2, 30, Player, BuffName="Dissipation")) 178 | 179 | #=================================== 180 | 181 | def CheckChainStratagem(Player, Enemy): 182 | if Player.ChainStratagemTimer <= 0: 183 | Player.ChainStratagemTimer = 0 184 | Enemy.ChainStratagem = False 185 | Player.EffectToRemove.append(CheckChainStratagem) 186 | 187 | def CheckBiolysis(Player, Enemy): 188 | if Player.BiolysisTimer <= 0 : 189 | Player.DOTList.remove(Player.Biolysis) 190 | Player.Biolysis = None 191 | Player.EffectToRemove.append(CheckBiolysis) 192 | 193 | 194 | 195 | 196 | #DamageGCD 197 | Broil = ScholarSpell(25865, True, 1.5, 2.5, 295, 400, empty, [ManaRequirement], type = 1) 198 | Ruin = ScholarSpell(17870, True, 0, 2.5, 220, 300, empty, [ManaRequirement], type = 1) 199 | Biolysis = ScholarSpell(16540, True, 0, 2.5, 0, 300, ApplyBiolysis, [ManaRequirement], type = 1) 200 | BiolysisDOT = DOTSpell(-4, 70, False) 201 | ArtOfWar = ScholarSpell(25866, True, 0, 2.5, 165, 400, empty, [ManaRequirement], type = 1)#AOE 202 | 203 | #HealGCD 204 | Succor = ScholarSpell(186, True, 2, 2.5, 200, 1000, ApplyAdloquium, [AdloquiumRequirement], type = 1, AOEHeal=True) ##This action has apply and requirement because of recitation. 160% shield 205 | Adloquium = ScholarSpell(185, True, 2, 2.5, 300, 1000, ApplyAdloquium, [AdloquiumRequirement], type = 1, TargetHeal=True) #This action has apply and requirement because of recitation. 180% shield 206 | Physick = ScholarSpell(190, True, 1.5, 2.5, 450, 400, empty, [ManaRequirement], type = 1, TargetHeal=True) 207 | SummonEos = ScholarSpell(29, True, 1.5, 2.5, 0, 200, empty, [ManaRequirement], type = 1) 208 | Resurrection = ScholarSpell(173, True, 8, 2.5, 0, 2400, empty, [ManaRequirement]) 209 | #Damage oGCD 210 | ChainStratagem = ScholarSpell(7436, False, 0, Lock, 0, 0, ApplyChainStratagem, [ChainStratagemRequirement]) 211 | EnergyDrain = ScholarSpell(167, False, 0, Lock, 100, 0, ApplyEnergyDrain, [AetherStackRequirement]) 212 | Aetherflow = ScholarSpell(166, False, 0, Lock, 0, 0, ApplyAetherflow, [AetherflowRequirement]) 213 | Dissipation = ScholarSpell(3587, False, 0, Lock, 0, 0, ApplyDissipation, [DissipationRequirement]) 214 | #Heal oGCD 215 | Expedient = ScholarSpell(25868, False, 0, 0, 0, 0, ApplyExpedient, [ExpedientRequirement]) 216 | Protraction = ScholarSpell(25867, False, 0, 0, 0, 0, ApplyProtraction, [ProtractionRequirement]) 217 | Recitation = ScholarSpell(16542, False, 0, 0, 0, 0, ApplyRecitation, [RecitationRequirement]) 218 | EmergencyTactic = ScholarSpell(3586, False, 0, 0, 0, 0, ApplyEmergencyTactic, [EmergencyTacticRequirement]) 219 | DeploymentTactic = ScholarSpell(3585, False, 0, 0, 0, 0, ApplyDeploymentTactic, [DeploymentTacticRequirement]) 220 | 221 | #AetherFlow Heal spell 222 | Excogitation = ScholarSpell(7434, False, 0, 0, 0, 0, ApplyExcogitation, [ExcogitationRequirement,AetherHealRequirement]) #Can be used by recitation 223 | SacredSoil = ScholarSpell(188, False, 0, 0, 0, 0, ApplySacredSoil, [SacredSoilRequirement,AetherflowRequirement]) 224 | Lustrate = ScholarSpell(189, False, 0, 0, 600, 0, ApplyLustrate, [LustrateRequirement,AetherflowRequirement], TargetHeal=True) 225 | Indomitability = ScholarSpell(3583, False, 0, 0, 400, 0, ApplyIndomitability, [IndomitabilityRequirement,AetherHealRequirement], AOEHeal=True)#Can be used by recitation 226 | 227 | #Fey Healing 228 | Consolation = ScholarSpell(16546, False, 0, 0, 0, 0, ApplyConsolation, [ConsolationRequirement]) 229 | SummonSeraph = ScholarSpell(16545, False, 0, 0, 250, 0, ApplySummonSeraph, [SummonSeraphRequirement], AOEHeal=True) # 100% shield 230 | FeyBlessing = ScholarSpell(16543, False, 0, 0, 320, 0, ApplyFeyBlessing, [FeyBlessingRequirement], AOEHeal=True) 231 | Aetherpact = ScholarSpell(7437, False, 0, 0, 0, 0, empty, []) #No requirement, since 3 sec cd, so not really worth it imo 232 | DissolveUnion = ScholarSpell(7869, False, 0, 0, 0, 0, empty, []) 233 | FeyIllumination = ScholarSpell(16538, False, 0, 0, 0, 0, ApplyFeyIllumination, [FeyIlluminationRequirement]) 234 | WhisperingDawn = ScholarSpell(16537, False, 0, 0, 0, 0, ApplyWhisperingDawn, [WhisperingDawnRequirement]) 235 | 236 | ScholarAbility = { 237 | 166 : Aetherflow, 238 | 167 : EnergyDrain, 239 | 185 : Adloquium, 240 | 186 : Succor, 241 | 188 : SacredSoil, 242 | 189 : Lustrate, 243 | 190 : Physick, 244 | 3583 : Indomitability, 245 | 3585 : DeploymentTactic, 246 | 3586 : EmergencyTactic, 247 | 3587 : Dissipation, 248 | 7434 : Excogitation, 249 | 7436 : ChainStratagem, 250 | 7437 : Aetherpact, 251 | 16537 : WhisperingDawn, 252 | 16538 : FeyIllumination, 253 | 16542 : Recitation, 254 | 16543 : FeyBlessing, 255 | 16545 : SummonSeraph, 256 | 17215 : SummonEos, 257 | 25868 : Expedient, 258 | 25867 : Protraction, 259 | 25866 : ArtOfWar, 260 | 25865 : Broil, 261 | 17870 : Ruin, 262 | 16540 : Biolysis, 263 | 16546 : Consolation, 264 | 173 : Resurrection, 265 | 7869 : DissolveUnion 266 | } -------------------------------------------------------------------------------- /ffxivcalc/Jobs/Tank/Gunbreaker/Gunbreaker_Spell.py: -------------------------------------------------------------------------------- 1 | from ffxivcalc.Jobs.Base_Spell import buff, empty, DOTSpell 2 | from ffxivcalc.Jobs.Tank.Tank_Spell import BigMitRequirement, ApplyBigMit, GunbreakerSpell 3 | from ffxivcalc.Jobs.Player import MitBuff 4 | import copy 5 | Lock = 0.75 6 | 7 | 8 | #Requirement 9 | 10 | def CamouflageRequirement(Player, Spell): 11 | return Player.CamouflageCD <= 0, Player.CamouflageCD 12 | 13 | def NoMercyRequirement(Player, Spell): 14 | return Player.NoMercyCD <= 0, Player.NoMercyCD 15 | 16 | def RoughDivideRequirement(Player, Enemy): 17 | return Player.RoughDivideStack > 0, Player.RoughDivideCD 18 | 19 | def BowShockRequirement(Player, Spell): 20 | return Player.BowShockCD <= 0, Player.BowShockCD 21 | 22 | def SonicBreakRequirement(Player, Spell): 23 | return Player.SonicBreakCD <= 0, Player.SonicBreakCD 24 | 25 | def DoubleDownRequirement(Player, Spell): 26 | return Player.DoubleDownCD <= 0, Player.DoubleDownCD 27 | 28 | def HypervelocityRequirement(Player, Spell): 29 | return Player.ReadyToBlast, -1 30 | 31 | def BloodfestRequirement(Player, Spell): 32 | return Player.BloodfestCD <= 0, Player.BloodfestCD 33 | 34 | def BlastingZoneRequirement(Player, Spell): 35 | return Player.BlastingZoneCD <= 0, Player.BlastingZoneCD 36 | 37 | def JugularRipRequirement(Player, Spell): 38 | return Player.ReadyToRip, -1 39 | 40 | def AbdomenTearRequirement(Player, Spell): 41 | return Player.ReadyToTear, -1 42 | 43 | def EyeGougeRequirement(Player, Spell): 44 | return Player.ReadyToGouge, -1 45 | 46 | def AuroraRequirement(Player, Spell): 47 | return Player.AuroraStack > 0, Player.AuroraCD 48 | 49 | def SuperbolideRequirement(Player, Spell): 50 | return Player.SuperbolideCD <= 0, Player.SuperbolideCD 51 | 52 | def HeartOfLightRequirement(Player, Spell): 53 | return Player.HeartOfLightCD <= 0, Player.HeartOfLightCD 54 | 55 | def HeartOfCorundumRequirement(Player, Spell): 56 | return Player.HeartOfCorundumCD <= 0, Player.HeartOfCorundumCD 57 | 58 | #Apply 59 | 60 | def ApplyCamouflage(Player, Enemy): 61 | Player.CamouflageCD = 90 62 | 63 | # Since its a 50% that additional 20% mit is added. We will simply 64 | # assume a 20% total mit 65 | CamouflageMit = MitBuff(0.8, 20, Player) 66 | Player.MitBuffList.append(CamouflageMit) 67 | 68 | def ApplyDemonSlice(Player, Enemy): 69 | if not (DemonSliceCombo in Player.EffectList) : Player.EffectList.append(DemonSliceCombo) 70 | 71 | def ApplyHeartOfCorundum(Player, Enemy): 72 | Player.HeartOfCorundumCD = 25 73 | 74 | def ApplyHeartOfLight(Player, Enemy): 75 | Player.HeartOfLightCD = 90 76 | 77 | # Will give every player 10% magic mit 78 | 79 | for player in Player.CurrentFight.PlayerList: 80 | player.MitBuffList.append(MitBuff(0.9, 15, player, MagicMit=True)) 81 | 82 | def ApplySuperbolide(Player, Enemy): 83 | Player.SuperbolideCD = 360 84 | 85 | Player.InvulnTimer = 10 86 | 87 | def ApplyAurora(Player, Enemy): 88 | if Player.AuroraStack == 2: 89 | Player.EffectCDList.append(AuroraStackCheck) 90 | Player.AuroraCD = 60 91 | Player.AuroraStack -= 1 92 | 93 | def ApplyNoMercy(Player, Enemy): 94 | Player.buffList.append(NoMercyBuff) 95 | Player.NoMercyTimer = 20 96 | Player.NoMercyCD = 60 97 | Player.EffectCDList.append(NoMercyCheck) 98 | 99 | def ApplyRoughDivide(Player, Enemy): 100 | if Player.RoughDivideStack == 2: 101 | Player.EffectCDList.append(RoughDivideStackCheck) 102 | Player.RoughDivideCD = 30 103 | Player.RoughDivideStack -= 1 104 | 105 | def ApplyBowShock(Player, Enemy): 106 | Player.BowShockCD = 60 107 | Player.BowShockTimer = 15 108 | if Player.BowShockDOT == None: 109 | Player.BowShockDOT = copy.deepcopy(BowShockDOT) 110 | Player.DOTList.append(Player.BowShockDOT) 111 | Player.EffectCDList.append(BowShockDOTCheck) 112 | else: Player.BowShockDOT.resetBuffSnapshot() 113 | 114 | def ApplySonicBreak(Player, Enemy): 115 | Player.SonicBreakCD = 60 116 | Player.SonicBreakTimer = 30 117 | if Player.SonicBreakDOT == None: 118 | Player.SonicBreakDOT = copy.deepcopy(SonicBreakDOT) 119 | Player.DOTList.append(Player.SonicBreakDOT) 120 | Player.EffectCDList.append(SonicBreakDOTCheck) 121 | else: Player.SonicBreakDOT.resetBuffSnapshot() 122 | 123 | def ApplyDoubleDown(Player, Enemy): 124 | Player.DoubleDownCD = 60 * Player.WeaponskillReduction 125 | 126 | def ApplyHypervelocity(Player, Enemy): 127 | Player.ReadyToBurst = False 128 | 129 | def ApplyBurstStrike(Player, Enemy): 130 | Player.ReadyToBlast = True 131 | 132 | def ApplyBloodfest(Player, Enemy): 133 | Player.PowderGauge = 3 134 | Player.BloodfestCD = 90 135 | 136 | def ApplyBlastingZone(Player, Enemy): 137 | Player.BlastingZoneCD = 30 138 | 139 | def ApplyKeenEdge(Player, Enemy): 140 | if not (KeenEdgeCombo in Player.EffectList) : Player.EffectList.append(KeenEdgeCombo) 141 | 142 | def ApplyGnashingFang(Player, Enemy): 143 | Player.ReadyToRip = True 144 | Player.GnashingFangCD = 30 * Player.WeaponskillReduction 145 | 146 | def ApplySavageClaw(Player, Enemy): 147 | Player.ReadyToRip = False 148 | Player.ReadyToTear = True 149 | 150 | def ApplyWickedTalon(Player, Enemy): 151 | Player.ReadyToTear = False 152 | Player.ReadyToGouge = True 153 | 154 | def ApplyEyeGouge(Player, Enemy): 155 | Player.ReadyToGauge = False 156 | 157 | #Combo Effect 158 | 159 | def DemonSliceCombo(Player, Spell): 160 | if Spell.id == DemonSlaughter.id: 161 | Spell.Potency += 60 162 | Player.PowderGauge = min(3, Player.PowderGauge + 1) 163 | Player.EffectToRemove.append(DemonSliceCombo) 164 | 165 | def KeenEdgeCombo(Player, Spell): 166 | if Spell.id == BrutalShell.id: 167 | Spell.Potency += 140 168 | if not (BrutalShellCombo in Player.EffectList) : Player.EffectList.append(BrutalShellCombo) 169 | Player.EffectToRemove.append(KeenEdgeCombo) 170 | 171 | def BrutalShellCombo(Player, Spell): 172 | if Spell.id == SolidBarrel.id: 173 | Spell.Potency += 220 174 | Player.PowderGauge = min(3, Player.PowderGauge + 1) 175 | Player.EffectToRemove.append(BrutalShellCombo) 176 | 177 | 178 | #Check 179 | 180 | def NoMercyCheck(Player, Enemy): 181 | if Player.NoMercyTimer <= 0: 182 | Player.buffList.remove(NoMercyBuff) 183 | Player.EffectToRemove.append(NoMercyCheck) 184 | 185 | def BowShockDOTCheck(Player, Enemy): 186 | if Player.BowShockTimer <= 0: 187 | Player.DOTList.remove(Player.BowShockDOT) 188 | Player.BowShockDOT = None 189 | Player.EffectToRemove.append(BowShockDOTCheck) 190 | 191 | def SonicBreakDOTCheck(Player, Enemy): 192 | if Player.SonicBreakTimer <= 0: 193 | Player.DOTList.remove(Player.SonicBreakDOT) 194 | Player.SonicBreakDOT = None 195 | Player.EffectToRemove.append(SonicBreakDOTCheck) 196 | 197 | def RoughDivideStackCheck(Player, Enemy): 198 | if Player.RoughDivideCD <= 0: 199 | if Player.RoughDivideStack == 1: 200 | Player.EffectToRemove.append(RoughDivideStackCheck) 201 | else: 202 | Player.RoughDivideCD = 30 203 | Player.RoughDivideStack +=1 204 | 205 | def AuroraStackCheck(Player, Enemy): 206 | if Player.AuroraCD <= 0: 207 | if Player.AuroraStack == 1: 208 | Player.EffectToRemove.append(AuroraStackCheck) 209 | else: 210 | Player.AuroraCD = 30 211 | Player.AuroraStack +=1 212 | 213 | 214 | #Combo Action 215 | 216 | KeenEdge = GunbreakerSpell(16137, True, 2.5, 200, ApplyKeenEdge, [], 0, type = 2) 217 | BrutalShell = GunbreakerSpell(16139, True, 2.5, 160, empty, [], 0, type = 2) 218 | SolidBarrel = GunbreakerSpell(16145, True, 2.5, 140, empty, [], 0, type = 2) 219 | 220 | GnashingFang = GunbreakerSpell(16146, True, 2.5, 380, ApplyGnashingFang, [], 1, type = 2) 221 | JugularRip = GunbreakerSpell(16156, False, 0, 200, empty, [JugularRipRequirement], 0) 222 | SavageClaw = GunbreakerSpell(16147, True, 2.5, 460, ApplySavageClaw, [JugularRipRequirement], 0, type = 2) 223 | AbdomenTear = GunbreakerSpell(16157, False, 0, 240, empty, [AbdomenTearRequirement], 0) 224 | WickedTalon = GunbreakerSpell(16150, True, 2.5, 540, ApplyWickedTalon, [AbdomenTearRequirement], 0, type = 2) 225 | EyeGouge = GunbreakerSpell(16158, False, 0, 280, ApplyEyeGouge, [EyeGougeRequirement], 0) 226 | 227 | BurstStrike = GunbreakerSpell(16162, True, 2.5, 380, ApplyBurstStrike, [], 1, type = 2) 228 | Hypervelocity = GunbreakerSpell(25759, False, 0, 180, ApplyHypervelocity,[HypervelocityRequirement], 0) 229 | 230 | #oGCD 231 | BlastingZone = GunbreakerSpell(16165, False, 0, 720, ApplyBlastingZone, [BlastingZoneRequirement], 0) 232 | Bloodfest = GunbreakerSpell(16164, False, 0, 0, ApplyBloodfest, [BloodfestRequirement], 0) 233 | BowShock = GunbreakerSpell(16159, False, 0, 150, ApplyBowShock, [BowShockRequirement], 0) 234 | BowShockDOT = DOTSpell(-10, 60, True) 235 | RoughDivide = GunbreakerSpell(16154, False, 0, 150, ApplyRoughDivide, [RoughDivideRequirement], 0) 236 | NoMercy = GunbreakerSpell(16138, False, 0, 0, ApplyNoMercy, [NoMercyRequirement], 0) 237 | #GCD 238 | DoubleDown = GunbreakerSpell(25760, True, 2.5, 1200, ApplyDoubleDown, [DoubleDownRequirement], 2, type = 2) 239 | SonicBreak = GunbreakerSpell(16153, True, 2.5, 300, ApplySonicBreak, [SonicBreakRequirement], 0, type = 2) 240 | SonicBreakDOT = DOTSpell(-9, 60,True) 241 | LightningShot = GunbreakerSpell(16143, True, 2.5, 150, empty, [], 0, type = 2) 242 | #AOE GCD 243 | FatedCircle = GunbreakerSpell(16163, True, 2.5, 290, empty, [], 1, type = 2) 244 | DemonSlice = GunbreakerSpell(16141, True, 2.5, 100, ApplyDemonSlice, [], 0, type = 2) 245 | DemonSlaughter = GunbreakerSpell(16149, True, 2.5, 100, empty, [], 0, type = 2) 246 | 247 | 248 | #Mit 249 | Aurora = GunbreakerSpell(16151, False, 0, 0, ApplyAurora, [AuroraRequirement], 0) 250 | Superbolide = GunbreakerSpell(16152, False, 0, 0, ApplySuperbolide, [SuperbolideRequirement], 0) 251 | HeartOfLight = GunbreakerSpell(16160, False, 0, 0, ApplyHeartOfLight, [HeartOfLightRequirement], 0) 252 | Camouflage = GunbreakerSpell(16140, False, 0, 0, ApplyCamouflage, [CamouflageRequirement], 0) 253 | Nebula = GunbreakerSpell(16148, False, 0, 0, ApplyBigMit, [BigMitRequirement], 0) 254 | 255 | def HeartOfCorundum(Target): 256 | """This function returns a GBNSpell object corresponding to HeartOfCorundum 257 | 258 | Args: 259 | Target (Player): Target of the action 260 | """ 261 | 262 | def SpellApply(Player, Enemy): 263 | ApplyHeartOfCorundum(Player, Enemy) 264 | 265 | # Gives 15% mit for 8 seconds and 15% percent for 4 seconds 266 | # 8 seconds 15% 267 | CorundumMit = MitBuff(0.85, 8, Target) 268 | # Clarity of corundum 15% for 4s 269 | ClarityMit = MitBuff(0.85, 4, Target) 270 | 271 | Target.MitBuffList.append(CorundumMit) 272 | Target.MitBuffList.append(ClarityMit) 273 | 274 | HeartOfCorundum = GunbreakerSpell(25758, False, 0, 0, SpellApply, [HeartOfCorundumRequirement], 0) 275 | HeartOfCorundum.TargetID = Target.playerID 276 | return HeartOfCorundum 277 | 278 | 279 | 280 | 281 | 282 | 283 | #buff 284 | NoMercyBuff = buff(1.2,name="No Mercy") 285 | 286 | GunbreakerAbility = { 287 | 16152 : Superbolide, 288 | 16143 : LightningShot, 289 | 16154 : RoughDivide, 290 | 16138 : NoMercy, 291 | 16164 : Bloodfest, 292 | 16137 : KeenEdge, 293 | 16139 : BrutalShell, 294 | 16145 : SolidBarrel, 295 | 16162 : BurstStrike, 296 | 16146 : GnashingFang, 297 | 16147 : SavageClaw, 298 | 16150 : WickedTalon, 299 | 16156 : JugularRip, 300 | 16157 : AbdomenTear, 301 | 16158 : EyeGouge, 302 | 25759 : Hypervelocity, 303 | 16153 : SonicBreak, 304 | 16165 : BlastingZone, 305 | 25760 : DoubleDown, 306 | 16141 : DemonSlice, 307 | 16149 : DemonSlaughter, 308 | 16163 : FatedCircle, 309 | 16148 : Nebula, 310 | 16140 : Camouflage, 311 | 25758 : HeartOfCorundum, 312 | 16151 : Aurora, 313 | 16160 : HeartOfLight, 314 | 16159 : BowShock 315 | } -------------------------------------------------------------------------------- /ffxivcalc/API/library.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file will contain basic function we will let the user call when importing the library or 3 | functions callable by the API. 4 | """ 5 | 6 | from copy import deepcopy 7 | import json 8 | import math 9 | from ffxivcalc.Jobs.PlayerEnum import JobEnum 10 | from ffxivcalc.helperCode.helper_backend import GenerateLayoutBackend, GenerateLayoutDict, RestoreFightObject 11 | from ffxivcalc.Fight import ComputeDamage 12 | from ffxivcalc.Enemy import Enemy 13 | from ffxivcalc.Jobs.Base_Spell import Spell 14 | from ffxivcalc.Jobs.PlayerEnum import RoleEnum 15 | from ffxivcalc.helperCode.helper_math import roundDown 16 | 17 | # library 18 | 19 | def AverageDamageAction(Player, Potency, MultBonus, type=0): 20 | 21 | levelMod = 1900 22 | baseMain = 390 23 | baseSub = 400# Level 90 LevelMod values 24 | 25 | JobMod = Player.JobMod # Level 90 jobmod value, specific to each job 26 | 27 | Player.f_WD = (Player.Stat["WD"]+math.floor(baseMain*JobMod/1000))/100 28 | Player.f_DET = math.floor(1000+math.floor(140*(Player.Stat["Det"]-baseMain)/levelMod))/1000# Determination damage 29 | if Player.RoleEnum == RoleEnum.Tank : Player.f_TEN = (1000+math.floor(100*(Player.Stat["Ten"]-baseSub)/levelMod))/1000 # Tenacity damage, 1 for non-tank player 30 | else : Player.f_TEN = 1 # if non-tank 31 | Player.f_SPD = (1000+math.floor(130*((Player.Stat["SS"] if Player.RoleEnum == RoleEnum.Caster or Player.RoleEnum == RoleEnum.Healer else Player.Stat["SkS"])-baseSub)/levelMod))/1000 # Used only for dots 32 | Player.CritRate = math.floor((200*(Player.Stat["Crit"]-baseSub)/levelMod+50))/1000 # Crit rate in decimal 33 | Player.CritMult = (math.floor(200*(Player.Stat["Crit"]-baseSub)/levelMod+400))/1000 # Crit Damage multiplier 34 | Player.DHRate = math.floor(550*(Player.Stat["DH"]-baseSub)/levelMod)/1000 # DH rate in decimal 35 | 36 | return ComputeDamage(Player, Potency, Enemy(), MultBonus, type, Spell(0, False, 0, 0, 0, 0, None, [])) 37 | 38 | 39 | def CreateFightDict(PlayerList : list[JobEnum]) -> dict: 40 | """ 41 | This function creates a fight dictionnary that can then be executed by the simulator. The fight's parameters will be default but 42 | it will create the fight with the given player list. 43 | PlayerList : list[JobEnum] -> Player list 44 | """ 45 | return GenerateLayoutDict(PlayerList) 46 | 47 | def CreateFightJSON(PlayerList : list[JobEnum], filename : str) -> None: 48 | """ 49 | This function creates a dictionnary that the simulator can use and saves the file in a saved folder where the file is executed under the given name 50 | PlayerList : list[JobEnum] -> Player list 51 | filename : str -> name of the saved file 52 | """ 53 | GenerateLayoutBackend(PlayerList, filename) 54 | 55 | def AddPlayer(FightDict : dict, newPlayer : JobEnum) -> None: 56 | """ 57 | This function will add a new player to the given Fight dictionnary. The new player will have no actions and have base stats. 58 | FightDict : dict -> Dictionnary with information of the fight 59 | newPlayer : JobEnum -> Job of the new player 60 | """ 61 | jobName = JobEnum.name_for_id(newPlayer) 62 | if len(FightDict["data"]["PlayerList"]) == 0: 63 | new_id = 1 64 | else: 65 | new_id = FightDict["data"]["PlayerList"][-1]["playerID"] + 1 66 | # finding id of last player and adding 1 assuming they are in id order. 67 | 68 | 69 | newPlayerDict = { 70 | "JobName" : jobName, 71 | "playerID" : new_id, 72 | "stat": { 73 | "MainStat" : 390, 74 | "WD" : 0, 75 | "Det" : 390, 76 | "Ten" : 400, 77 | "SS" : 400, 78 | "Crit" : 400, 79 | "DH" : 400 80 | }, 81 | "etro_gearset_url" : "put etro url here if you have one. Not needed", 82 | "Auras" : [], 83 | "actionLists" : [ 84 | { 85 | "actionName" : "put action name here" 86 | } 87 | ] 88 | 89 | } 90 | 91 | FightDict["data"]["PlayerList"].append(newPlayerDict) 92 | 93 | # function called by API 94 | 95 | # GET 96 | 97 | def GetSimulateFight(JSONFile): 98 | """ 99 | This function will take the JSON file given with the API request and use other helper functions to Simulate the fight. 100 | JSONFile : json -> JSON file with the simulation's parameter 101 | """ 102 | 103 | data = json.load(JSONFile) # Loading the file in a dictionnary 104 | 105 | returnData = SimulateFightAPIHelper(data) 106 | 107 | return json.dump(returnData,None, indent=4) # Returning the JSON file 108 | 109 | def SimulateFightAPIHelper(FightDict : dict) -> dict: 110 | """ 111 | This function can be called to simulate a fight. It requires as input the JSON file of the fight 112 | and it will return all the data relating to the simulation. This function will simply return the dictionnary and another function 113 | will return a JSON file. 114 | FightDict : dict -> dictionnary holding the fight's info 115 | """ 116 | 117 | Event = RestoreFightObject(FightDict) # Restoring the fight object 118 | fightInfo = FightDict["data"]["fightInfo"] #fight information 119 | Event.ShowGraph = fightInfo["ShowGraph"] #Default 120 | Event.RequirementOn = fightInfo["RequirementOn"] 121 | Event.IgnoreMana = fightInfo["IgnoreMana"] 122 | 123 | Event.SimulateFight(0.01,fightInfo["fightDuration"], vocal=False) # Simulating the fight 124 | 125 | # Now every player object has the info we need. We will simply parse is and put into a dictionnary. 126 | # To return as a JSON file. 127 | 128 | # Skeleton of the returnData 129 | returnData = { 130 | "data" : { 131 | "fightInfo" : { 132 | "fightDuration" : Event.TimeStamp, 133 | "maxfightDuration" : fightInfo["fightDuration"], 134 | "fightname" : "ayo", 135 | "TeamCompositionBonus" : Event.TeamCompositionBonus, 136 | "failedRequirementEventList" : [], 137 | "Success" : True, 138 | }, 139 | "PlayerList" : [] 140 | } 141 | } 142 | 143 | # Will go through every failedRequirementEvent and record them 144 | success = True 145 | if FightDict["RequestParam"]["failedRequirementEvent"]: # If user wants failedRequirementList 146 | for event in Event.failedRequirementList: 147 | 148 | success = success and not event.fatal # if one event was fatal success is false 149 | 150 | eventDict = { 151 | "timeStamp" : event.timeStamp, 152 | "playerID" : event.playerID, 153 | "requirementName" : event.requirementName, 154 | "additionalInfo" : event.additionalInfo, 155 | "fatal" : event.fatal 156 | } 157 | 158 | returnData["data"]["fightInfo"]["failedRequirementEventList"].append(deepcopy(eventDict)) 159 | 160 | returnData["data"]["fightInfo"]["Success"] = success 161 | 162 | 163 | for player in Event.PlayerList: 164 | # Going through every player will create the appriopriate dictionnary to return the info and put it in 165 | 166 | playerDict = { 167 | "JobName" : JobEnum.name_for_id(player.JobEnum), 168 | "ExpectedDPS" : 0 if Event.TimeStamp == 0 else round(player.TotalDamage/Event.TimeStamp,2), 169 | "PotencyPerSecond" : 0 if Event.TimeStamp == 0 else round(player.TotalPotency/Event.TimeStamp,2), 170 | "TotalDamage" : player.TotalDamage, 171 | "TotalPotency" : player.TotalPotency, 172 | "numberOfGCD" : player.GCDCounter, 173 | "ProcInfo" : {}, 174 | "GraphInfoDPS" : [], 175 | "GraphInfoPPS" : [] 176 | } # Creating new dictionnary 177 | 178 | # Going through its registered DPS and PPS points if user wants. 179 | 180 | if FightDict["RequestParam"]["GraphInfo"]: 181 | 182 | for (x,y) in zip(player.DPSGraph, Event.timeValue): 183 | # DPS 184 | 185 | point = { 186 | "value" : y, 187 | "name" : x 188 | } 189 | 190 | playerDict["GraphInfoDPS"].append(deepcopy(point)) 191 | 192 | for (x,y) in zip(player.PotencyGraph, Event.timeValue): 193 | # PPS 194 | 195 | point = { 196 | "value" : y, 197 | "name" : x 198 | } 199 | 200 | playerDict["GraphInfoPPS"].append(deepcopy(point)) 201 | 202 | # If any job has luck will return the info regarding this luck for the rotation 203 | if FightDict["RequestParam"]["ProcInfo"]: # If user wants 204 | match player.JobEnum: 205 | case JobEnum.Bard: 206 | procInfo = { 207 | "RefulgentArrow" : { 208 | "Expected" : player.ExpectedRefulgent, 209 | "Used" : player.UsedRefulgent 210 | }, 211 | "WandererRepertoire" : { 212 | "Expected" : player.ExpectedTotalWandererRepertoire, 213 | "Used" : player.UsedTotalWandererRepertoire 214 | }, 215 | "SoulVoiceGauge" : { 216 | "Expected" : player.ExpectedSoulVoiceGauge, 217 | "Used" : player.UsedSoulVoiceGauge 218 | }, 219 | "BloodLetterReduction" : { 220 | "Expected" : player.ExpectedBloodLetterReduction, 221 | "Used" : player.UsedBloodLetterReduction 222 | } 223 | } 224 | playerDict["ProcInfo"] = deepcopy(procInfo) 225 | case JobEnum.Dancer: 226 | procInfo = { 227 | "SilkenSymettry" : { 228 | "Expected" : player.ExpectedSilkenSymettry, 229 | "Used" : player.UsedSilkenSymettry 230 | }, 231 | "SilkenFlow" : { 232 | "Expected" : player.ExpectedSilkenFlow, 233 | "Used" : player.UsedSilkenFlow 234 | }, 235 | "FourfoldFeather" : { 236 | "Expected" : player.ExpectedFourfoldFeather, 237 | "Used" : player.UsedFourfoldFeather 238 | }, 239 | "ThreefoldFan" : { 240 | "Expected" : player.ExpectedThreefoldFan, 241 | "Used" : player.UsedThreefoldFan 242 | } 243 | } 244 | playerDict["ProcInfo"] = deepcopy(procInfo) 245 | case JobEnum.RedMage: 246 | procInfo = { 247 | "Verstone" : { 248 | "Expected" : player.ExpectedVerstoneProc, 249 | "Used" : player.UsedVerstoneProc 250 | }, 251 | "Verfire" : { 252 | "Expected" : player.ExpectedVerfireProc, 253 | "Used" : player.UsedVerfireProc 254 | } 255 | } 256 | playerDict["ProcInfo"] = deepcopy(procInfo) 257 | 258 | returnData["data"]["PlayerList"].append(deepcopy(playerDict)) 259 | 260 | 261 | return returnData 262 | 263 | """ 264 | cur_dir = os.getcwd() 265 | 266 | #the saved directory should be in that same folder 267 | 268 | saved_dir = cur_dir + "\\saved" 269 | 270 | saved_fight = os.listdir(saved_dir) 271 | 272 | 273 | f = open(saved_dir + "\\" + "blackmage.json") #Opening save 274 | 275 | data = json.load(f) #Loading json file 276 | 277 | APIAnswer = SimulateFightAPIHelper(data) 278 | 279 | with open(f'{"APIRequestTest"}.json', "w") as write_files: 280 | json.dump(APIAnswer,write_files, indent=4) #saving file 281 | 282 | """ 283 | # POST 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | --------------------------------------------------------------------------------