diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 647a8b3a6..1becb0b0a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Virtual environment will be created in *PyfaEnv* folder. Project will be cloned ## Setting up the project manually -Clone the repo +Clone the repository ``` git clone PyfaDEV ``` @@ -36,7 +36,7 @@ pip install -r PyfaDEV\requirements.txt ``` > For some Linux distributions, you may need to install separate wxPython bindings, such as `python-matplotlib-wx` -Check that libs from *requirements.txt* are installed +Check that the libs from *requirements.txt* are installed ``` pip list ``` diff --git a/eos/saveddata/damagePattern.py b/eos/saveddata/damagePattern.py index f518be127..bd0e1cd0e 100644 --- a/eos/saveddata/damagePattern.py +++ b/eos/saveddata/damagePattern.py @@ -78,6 +78,15 @@ class DamagePattern: "exp" : "explosive" } + @classmethod + def oneType(cls, damageType, amount=100): + pattern = DamagePattern() + pattern.update(amount if damageType == "em" else 0, + amount if damageType == "thermal" else 0, + amount if damageType == "kinetic" else 0, + amount if damageType == "explosive" else 0) + return pattern + @classmethod def importPatterns(cls, text): lines = re.split('[\n\r]+', text) diff --git a/gui/copySelectDialog.py b/gui/copySelectDialog.py index 7700c79cd..6ea13ead9 100644 --- a/gui/copySelectDialog.py +++ b/gui/copySelectDialog.py @@ -40,6 +40,7 @@ class CopySelectDialog(wx.Dialog): copyFormatEsi = 3 copyFormatMultiBuy = 4 copyFormatEfs = 5 + copyFormatFitStats = 6 def __init__(self, parent): super().__init__(parent, id=wx.ID_ANY, title="Select a format", size=(-1, -1), style=wx.DEFAULT_DIALOG_STYLE) @@ -50,7 +51,8 @@ class CopySelectDialog(wx.Dialog): CopySelectDialog.copyFormatDna : self.exportDna, CopySelectDialog.copyFormatEsi : self.exportEsi, CopySelectDialog.copyFormatMultiBuy: self.exportMultiBuy, - CopySelectDialog.copyFormatEfs : self.exportEfs + CopySelectDialog.copyFormatEfs : self.exportEfs, + CopySelectDialog.copyFormatFitStats: self.exportFitStats } self.mainFrame = parent @@ -62,6 +64,7 @@ class CopySelectDialog(wx.Dialog): ("ESI", (CopySelectDialog.copyFormatEsi, None)), ("DNA", (CopySelectDialog.copyFormatDna, DNA_OPTIONS)), ("EFS", (CopySelectDialog.copyFormatEfs, None)), + ("Fit stats", (CopySelectDialog.copyFormatFitStats, None)), # ("XML", (CopySelectDialog.copyFormatXml, None)), )) @@ -117,7 +120,8 @@ class CopySelectDialog(wx.Dialog): self.Center() def Validate(self): - # Since this dialog is shown through aa ShowModal(), we hook into the Validate function to veto the closing of the dialog until we're ready. + # Since this dialog is shown through as ShowModal(), + # we hook into the Validate function to veto the closing of the dialog until we're ready. # This always returns False, and when we're ready will EndModal() selected = self.GetSelected() options = self.GetOptions() @@ -185,3 +189,10 @@ class CopySelectDialog(wx.Dialog): def exportEfs(self, options, callback): fit = getFit(self.mainFrame.getActiveFit()) EfsPort.exportEfs(fit, 0, callback) + + # noinspection PyUnusedLocal + def exportFitStats(self, options, callback): + """ Puts fit stats in textual format into the clipboard """ + fit = getFit(self.mainFrame.getActiveFit()) + Port.exportFitStats(fit, callback) + diff --git a/service/port/port.py b/service/port/port.py index 2db627bbc..2fc4d879a 100644 --- a/service/port/port.py +++ b/service/port/port.py @@ -39,6 +39,7 @@ from service.port.eft import ( from service.port.esi import exportESI, importESI from service.port.multibuy import exportMultiBuy from service.port.shared import IPortUser, UserCancelException, processing_notify +from service.port.shipstats import exportFitStats from service.port.xml import importXml, exportXml from service.port.muta import parseMutant @@ -317,3 +318,7 @@ class Port: @staticmethod def exportMultiBuy(fit, options, callback=None): return exportMultiBuy(fit, options, callback=callback) + + @staticmethod + def exportFitStats(fit, callback=None): + return exportFitStats(fit, callback=callback) \ No newline at end of file diff --git a/service/port/shipstats.py b/service/port/shipstats.py new file mode 100644 index 000000000..0ab42c30a --- /dev/null +++ b/service/port/shipstats.py @@ -0,0 +1,191 @@ +from functools import reduce +from eos.saveddata.damagePattern import DamagePattern +from gui.utils.numberFormatter import formatAmount + +tankTypes = ("shield", "armor", "hull") +damageTypes = ("em", "thermal", "kinetic", "explosive") +damagePatterns = [DamagePattern.oneType(damageType) for damageType in damageTypes] +damageTypeResonanceNames = [damageType.capitalize() + "DamageResonance" for damageType in damageTypes] +resonanceNames = {"shield": ["shield" + s for s in damageTypeResonanceNames], + "armor": ["armor" + s for s in damageTypeResonanceNames], + "hull": [s[0].lower() + s[1:] for s in damageTypeResonanceNames]} + + +def firepowerSection(fit): + """ Returns the text of the firepower section""" + totalDps = fit.getTotalDps().total + weaponDps = fit.getWeaponDps().total + droneDps = fit.getDroneDps().total + totalVolley = fit.getTotalVolley().total + firepower = [totalDps, weaponDps, droneDps, totalVolley] + + firepowerStr = [formatAmount(dps, 3, 0, 0) for dps in firepower] + # showWeaponAndDroneDps = (weaponDps > 0) and (droneDps > 0) + if sum(firepower) == 0: + return "" + + return "DPS: {} (".format(firepowerStr[0]) + \ + ("Weapon: {}, Drone: {}, ".format(*firepowerStr[1:3])) + \ + ("Volley: {})\n".format(firepowerStr[3])) + + +def tankSection(fit): + """ Returns the text of the tank section""" + ehp = [fit.ehp[tank] for tank in tankTypes] if fit.ehp is not None else [0, 0, 0] + ehp.append(sum(ehp)) + ehpStr = [formatAmount(ehpVal, 3, 0, 9) for ehpVal in ehp] + resists = {tankType: [1 - fit.ship.getModifiedItemAttr(s) for s in resonanceNames[tankType]] for tankType in tankTypes} + ehpAgainstDamageType = [sum(pattern.calculateEhp(fit).values()) for pattern in damagePatterns] + ehpAgainstDamageTypeStr = [formatAmount(ehpVal, 3, 0, 9) for ehpVal in ehpAgainstDamageType] + + # not used for now. maybe will be improved later + # def formattedOutput(): + # return \ + # " {:>7} {:>7} {:>7} {:>7} {:>7}\n".format("TOTAL", "EM", "THERM", "KIN", "EXP") + \ + # "EHP {:>7} {:>7} {:>7} {:>7} {:>7}\n".format(ehpStr[3], *ehpAgainstDamageTypeStr) + \ + # "Shield {:>7} {:>7.0%} {:>7.0%} {:>7.0%} {:>7.0%}\n".format(ehpStr[0], *resists["shield"]) + \ + # "Armor {:>7} {:>7.0%} {:>7.0%} {:>7.0%} {:>7.0%}\n".format(ehpStr[1], *resists["armor"]) + \ + # "Hull {:>7} {:>7.0%} {:>7.0%} {:>7.0%} {:>7.0%}\n".format(ehpStr[2], *resists["hull"]) + + def generalOutput(): + return \ + "EHP: {:>} (Em: {:>}, Th: {:>}, Kin: {:>}, Exp: {:>}\n".format(ehpStr[3], *ehpAgainstDamageTypeStr) + \ + "Shield: {:>} (Em: {:.0%}, Th: {:.0%}, Kin: {:.0%}, Exp: {:.0%}\n".format(ehpStr[0], *resists["shield"]) + \ + "Armor: {:>} (Em: {:.0%}, Th: {:.0%}, Kin: {:.0%}, Exp: {:.0%}\n".format(ehpStr[1], *resists["armor"]) + \ + "Hull: {:>} (Em: {:.0%}, Th: {:.0%}, Kin: {:.0%}, Exp: {:.0%}\n".format(ehpStr[2], *resists["hull"]) + + return generalOutput() + + +def repsSection(fit): + """ Returns the text of the repairs section""" + selfRep = [fit.effectiveTank[tankType + "Repair"] for tankType in tankTypes] + sustainRep = [fit.effectiveSustainableTank[tankType + "Repair"] for tankType in tankTypes] + remoteRepObj = fit.getRemoteReps() + remoteRep = [remoteRepObj.shield, remoteRepObj.armor, remoteRepObj.hull] + shieldRegen = [fit.effectiveSustainableTank["passiveShield"], 0, 0] + shieldRechargeModuleMultipliers = [module.item.attributes["shieldRechargeRateMultiplier"].value for module in + fit.modules if + module.item and "shieldRechargeRateMultiplier" in module.item.attributes] + shieldRechargeMultiplierByModules = reduce(lambda x, y: x * y, shieldRechargeModuleMultipliers, 1) + if shieldRechargeMultiplierByModules >= 0.9: # If the total affect of modules on the shield recharge is negative or insignificant, we don't care about it + shieldRegen[0] = 0 + totalRep = list(zip(selfRep, remoteRep, shieldRegen)) + totalRep = list(map(sum, totalRep)) + + selfRep.append(sum(selfRep)) + sustainRep.append(sum(sustainRep)) + remoteRep.append(sum(remoteRep)) + shieldRegen.append(sum(shieldRegen)) + totalRep.append(sum(totalRep)) + + totalSelfRep = selfRep[-1] + totalRemoteRep = remoteRep[-1] + totalShieldRegen = shieldRegen[-1] + + text = "" + + if sum(totalRep) > 0: # Most commonly, there are no reps at all; then we skip this section + singleTypeRep = None + singleTypeRepName = None + if totalRemoteRep == 0 and totalShieldRegen == 0: # Only self rep + singleTypeRep = selfRep[:-1] + singleTypeRepName = "Self" + if totalSelfRep == 0 and totalShieldRegen == 0: # Only remote rep + singleTypeRep = remoteRep[:-1] + singleTypeRepName = "Remote" + if totalSelfRep == 0 and totalRemoteRep == 0: # Only shield regen + singleTypeRep = shieldRegen[:-1] + singleTypeRepName = "Regen" + if singleTypeRep and sum( + x > 0 for x in singleTypeRep) == 1: # Only one type of reps and only one tank type is repaired + index = next(i for i, v in enumerate(singleTypeRep) if v > 0) + if singleTypeRepName == "Regen": + text += "Shield regeneration: {} EHP/s".format(formatAmount(singleTypeRep[index], 3, 0, 9)) + else: + text += "{} {} repair: {} EHP/s".format(singleTypeRepName, tankTypes[index], + formatAmount(singleTypeRep[index], 3, 0, 9)) + if (singleTypeRepName == "Self") and (sustainRep[index] != singleTypeRep[index]): + text += " (Sustained: {} EHP/s)".format(formatAmount(sustainRep[index], 3, 0, 9)) + text += "\n" + else: # Otherwise show a table + selfRepStr = [formatAmount(rep, 3, 0, 9) for rep in selfRep] + sustainRepStr = [formatAmount(rep, 3, 0, 9) for rep in sustainRep] + remoteRepStr = [formatAmount(rep, 3, 0, 9) for rep in remoteRep] + shieldRegenStr = [formatAmount(rep, 3, 0, 9) if rep != 0 else "" for rep in shieldRegen] + totalRepStr = [formatAmount(rep, 3, 0, 9) for rep in totalRep] + + header = "REPS " + lines = [ + "Shield ", + "Armor ", + "Hull ", + "Total " + ] + + showSelfRepColumn = totalSelfRep > 0 + showSustainRepColumn = sustainRep != selfRep + showRemoteRepColumn = totalRemoteRep > 0 + showShieldRegenColumn = totalShieldRegen > 0 + + if showSelfRepColumn + showSustainRepColumn + showRemoteRepColumn + showShieldRegenColumn > 1: + header += "{:>7} ".format("TOTAL") + lines = [line + "{:>7} ".format(rep) for line, rep in zip(lines, totalRepStr)] + if showSelfRepColumn: + header += "{:>7} ".format("SELF") + lines = [line + "{:>7} ".format(rep) for line, rep in zip(lines, selfRepStr)] + if showSustainRepColumn: + header += "{:>7} ".format("SUST") + lines = [line + "{:>7} ".format(rep) for line, rep in zip(lines, sustainRepStr)] + if showRemoteRepColumn: + header += "{:>7} ".format("REMOTE") + lines = [line + "{:>7} ".format(rep) for line, rep in zip(lines, remoteRepStr)] + if showShieldRegenColumn: + header += "{:>7} ".format("REGEN") + lines = [line + "{:>7} ".format(rep) for line, rep in zip(lines, shieldRegenStr)] + + text += header + "\n" + repsByTank = zip(totalRep, selfRep, sustainRep, remoteRep, shieldRegen) + for line in lines: + reps = next(repsByTank) + if sum(reps) > 0: + text += line + "\n" + return text + + +def miscSection(fit): + text = "" + text += "Speed: {} m/s\n".format(formatAmount(fit.maxSpeed, 3, 0, 0)) + text += "Signature: {} m\n".format(formatAmount(fit.ship.getModifiedItemAttr("signatureRadius"), 3, 0, 9)) + + text += "Capacitor: {} GJ".format(formatAmount(fit.ship.getModifiedItemAttr("capacitorCapacity"), 3, 0, 9)) + capState = fit.capState + if fit.capStable: + text += " (Stable at {0:.0f}%)".format(capState) + else: + text += " (Lasts {})".format("%ds" % capState if capState <= 60 else "%dm%ds" % divmod(capState, 60)) + text += "\n" + + text += "Targeting range: {} km\n".format(formatAmount(fit.maxTargetRange / 1000, 3, 0, 0)) + text += "Scan resolution: {0:.0f} mm\n".format(fit.ship.getModifiedItemAttr("scanResolution")) + text += "Sensor strength: {}\n".format(formatAmount(fit.scanStrength, 3, 0, 0)) + + return text + + +def exportFitStats(fit, callback): + """ + Returns the text of the stats export of the given fit + """ + sections = filter(None, (firepowerSection(fit), # Prune empty sections + tankSection(fit), + repsSection(fit), + miscSection(fit))) + + text = "{} ({})\n".format(fit.name, fit.ship.name) + "\n" + text += "\n".join(sections) + + if callback: + callback(text) + else: + return text