diff --git a/eos/const.py b/eos/const.py index e1348c5b8..12998c0fc 100644 --- a/eos/const.py +++ b/eos/const.py @@ -101,3 +101,12 @@ class FitSystemSecurity(IntEnum): LOWSEC = 1 NULLSEC = 2 WSPACE = 3 + + +@unique +class Operator(IntEnum): + PREASSIGN = 0 + PREINCREASE = 1 + MULTIPLY = 2 + POSTINCREASE = 3 + FORCE = 4 diff --git a/eos/modifiedAttributeDict.py b/eos/modifiedAttributeDict.py index 47dd28cc7..1162446dd 100644 --- a/eos/modifiedAttributeDict.py +++ b/eos/modifiedAttributeDict.py @@ -20,10 +20,13 @@ import collections from copy import copy from math import exp + +from eos.const import Operator # TODO: This needs to be moved out, we shouldn't have *ANY* dependencies back to other modules/methods inside eos. # This also breaks writing any tests. :( from eos.db.gamedata.queries import getAttributeInfo + defaultValuesCache = {} cappingAttrKeyCache = {} @@ -55,6 +58,11 @@ class ItemAttrShortcut: return_value = self.itemModifiedAttributes.getWithExtraMods(key, extraMultipliers=extraMultipliers) return return_value or default + def getModifiedItemAttrWithoutAfflictor(self, key, afflictor, default=0): + """Returns attribute value with passed afflictor modification removed.""" + return_value = self.itemModifiedAttributes.getWithoutAfflictor(key, afflictor) + return return_value or default + def getItemBaseAttrValue(self, key, default=0): return_value = self.itemModifiedAttributes.getOriginal(key) return return_value or default @@ -68,7 +76,12 @@ class ChargeAttrShortcut: def getModifiedChargeAttrWithExtraMods(self, key, extraMultipliers=None, default=0): """Returns attribute value with passed modifiers applied to it.""" - return_value = self.itemModifiedAttributes.getWithExtraMods(key, extraMultipliers=extraMultipliers) + return_value = self.chargeModifiedAttributes.getWithExtraMods(key, extraMultipliers=extraMultipliers) + return return_value or default + + def getModifiedChargeAttrWithoutAfflictor(self, key, afflictor, default=0): + """Returns attribute value with passed modifiers applied to it.""" + return_value = self.chargeModifiedAttributes.getWithoutAfflictor(key, afflictor) return return_value or default def getChargeBaseAttrValue(self, key, default=0): @@ -93,6 +106,10 @@ class ModifiedAttributeDict(collections.MutableMapping): # Final modified values self.__modified = {} # Affected by entities + # Format: + # {attr name: {modifying fit: ( + # modifying item, operation, stacking group, pre-resist amount, + # post-resist amount, affects result or not)}} self.__affectedBy = {} # Overrides (per item) self.__overrides = {} @@ -205,6 +222,20 @@ class ModifiedAttributeDict(collections.MutableMapping): # Passed in default value return default + def getWithoutAfflictor(self, key, afflictor, default=0): + # Here we do not have support for preAssigns/forceds, as doing them would + # mean that we have to store all of them in a list which increases memory use, + # and we do not actually need those operators atm + preIncreaseAdjustment = 0 + multiplierAdjustment = 1 + ignoredPenalizedMultipliers = None + postIncreaseAdjustment = 0 + # for fit, afflictors in self.getAfflictions(key).items(): + # for afflictor1, operator, stackingGroup, preResAmount, postResAmount, used in afflictors: + # if afflictor1 is afflictor: + # print(afflictor.item.name, operator, stackingGroup, preResAmount, postResAmount, used) + return self[key] + def __delitem__(self, key): if key in self.__modified: del self.__modified[key] @@ -373,7 +404,7 @@ class ModifiedAttributeDict(collections.MutableMapping): def iterAfflictions(self): return self.__affectedBy.__iter__() - def __afflict(self, attributeName, operation, bonus, used=True): + def __afflict(self, attributeName, operator, stackingGroup, preResAmount, postResAmount, used=True): """Add modifier to list of things affecting current item""" # Do nothing if no fit is assigned fit = self.fit @@ -399,13 +430,13 @@ class ModifiedAttributeDict(collections.MutableMapping): modifier = fit.getModifier() # Add current affliction to list - affs.append((modifier, operation, bonus, used)) + affs.append((modifier, operator, stackingGroup, preResAmount, postResAmount, used)) def preAssign(self, attributeName, value, **kwargs): """Overwrites original value of the entity with given one, allowing further modification""" self.__preAssigns[attributeName] = value self.__placehold(attributeName) - self.__afflict(attributeName, "=", value, value != self.getOriginal(attributeName)) + self.__afflict(attributeName, Operator.PREASSIGN, None, value, value, value != self.getOriginal(attributeName)) def increase(self, attributeName, increase, position="pre", skill=None, **kwargs): """Increase value of given attribute by given number""" @@ -418,8 +449,10 @@ class ModifiedAttributeDict(collections.MutableMapping): # Increases applied before multiplications and after them are # written in separate maps if position == "pre": + operator = Operator.PREINCREASE tbl = self.__preIncreases elif position == "post": + operator = Operator.POSTINCREASE tbl = self.__postIncreases else: raise ValueError("position should be either pre or post") @@ -427,7 +460,7 @@ class ModifiedAttributeDict(collections.MutableMapping): tbl[attributeName] = 0 tbl[attributeName] += increase self.__placehold(attributeName) - self.__afflict(attributeName, "+", increase, increase != 0) + self.__afflict(attributeName, operator, None, increase, increase, increase != 0) def multiply(self, attributeName, multiplier, stackingPenalties=False, penaltyGroup="default", skill=None, **kwargs): """Multiply value of given attribute by given factor""" @@ -437,6 +470,7 @@ class ModifiedAttributeDict(collections.MutableMapping): if skill: multiplier *= self.__handleSkill(skill) + preResMultiplier = multiplier resisted = False # Goddammit CCP, make up your mind where you want this information >.< See #1139 if 'effect' in kwargs: @@ -468,7 +502,9 @@ class ModifiedAttributeDict(collections.MutableMapping): if resisted: afflictPenal += "r" - self.__afflict(attributeName, "%s*" % afflictPenal, multiplier, multiplier != 1) + self.__afflict( + attributeName, Operator.MULTIPLY, penaltyGroup if stackingPenalties else None, + preResMultiplier, multiplier, multiplier != 1) def boost(self, attributeName, boostFactor, skill=None, **kwargs): """Boost value by some percentage""" @@ -482,7 +518,7 @@ class ModifiedAttributeDict(collections.MutableMapping): """Force value to attribute and prohibit any changes to it""" self.__forced[attributeName] = value self.__placehold(attributeName) - self.__afflict(attributeName, "\u2263", value) + self.__afflict(attributeName, Operator.FORCE, None, value, value) @staticmethod def getResistance(fit, effect): diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index 21807541b..555041e62 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -1198,9 +1198,11 @@ class Fit: def capDelta(self): return (self.__capRecharge or 0) - (self.__capUsed or 0) - def calculateCapRecharge(self, percent=PEAK_RECHARGE): - capacity = self.ship.getModifiedItemAttr("capacitorCapacity") - rechargeRate = self.ship.getModifiedItemAttr("rechargeRate") / 1000.0 + def calculateCapRecharge(self, percent=PEAK_RECHARGE, capacity=None, rechargeRate=None): + if capacity is None: + capacity = self.ship.getModifiedItemAttr("capacitorCapacity") + if rechargeRate is None: + rechargeRate = self.ship.getModifiedItemAttr("rechargeRate") / 1000.0 return 10 / rechargeRate * sqrt(percent) * (1 - sqrt(percent)) * capacity def calculateShieldRecharge(self, percent=PEAK_RECHARGE): @@ -1314,6 +1316,14 @@ class Fit: self.__savedCapSimData[startingCap] = [] return None + def getCapGainFromMod(self, mod): + """Return how much cap regen do we gain from having this module""" + currentRegen = self.calculateCapRecharge() + nomodRegen = self.calculateCapRecharge( + capacity=self.ship.getModifiedItemAttrWithoutAfflictor("capacitorCapacity", mod), + rechargeRate=self.ship.getModifiedItemAttrWithoutAfflictor("rechargeRate", mod) / 1000.0) + return currentRegen - nomodRegen + def getRemoteReps(self, spoolOptions=None): if spoolOptions not in self.__remoteRepMap: remoteReps = RRTypes(0, 0, 0, 0) @@ -1417,47 +1427,47 @@ class Fit: for tankType in localAdjustment: dict = self.extraAttributes.getAfflictions(tankType) if self in dict: - for mod, _, amount, used in dict[self]: + for afflictor, operator, stackingGroup, preResAmount, postResAmount, used in dict[self]: if not used: continue - if mod.projected: + if afflictor.projected: continue - if mod.item.group.name not in groupAttrMap: + if afflictor.item.group.name not in groupAttrMap: continue usesCap = True try: - if mod.capUse: - capUsed -= mod.capUse + if afflictor.capUse: + capUsed -= afflictor.capUse else: usesCap = False except AttributeError: usesCap = False # Normal Repairers - if usesCap and not mod.charge: - cycleTime = mod.rawCycleTime - amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name]) + if usesCap and not afflictor.charge: + cycleTime = afflictor.rawCycleTime + amount = afflictor.getModifiedItemAttr(groupAttrMap[afflictor.item.group.name]) localAdjustment[tankType] -= amount / (cycleTime / 1000.0) - repairers.append(mod) + repairers.append(afflictor) # Ancillary Armor reps etc - elif usesCap and mod.charge: - cycleTime = mod.rawCycleTime - amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name]) - if mod.charge.name == "Nanite Repair Paste": - multiplier = mod.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1 + elif usesCap and afflictor.charge: + cycleTime = afflictor.rawCycleTime + amount = afflictor.getModifiedItemAttr(groupAttrMap[afflictor.item.group.name]) + if afflictor.charge.name == "Nanite Repair Paste": + multiplier = afflictor.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1 else: multiplier = 1 localAdjustment[tankType] -= amount * multiplier / (cycleTime / 1000.0) - repairers.append(mod) + repairers.append(afflictor) # Ancillary Shield boosters etc - elif not usesCap and mod.item.group.name in ("Ancillary Shield Booster", "Ancillary Remote Shield Booster"): - cycleTime = mod.rawCycleTime - amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name]) - if self.factorReload and mod.charge: - reloadtime = mod.reloadTime + elif not usesCap and afflictor.item.group.name in ("Ancillary Shield Booster", "Ancillary Remote Shield Booster"): + cycleTime = afflictor.rawCycleTime + amount = afflictor.getModifiedItemAttr(groupAttrMap[afflictor.item.group.name]) + if self.factorReload and afflictor.charge: + reloadtime = afflictor.reloadTime else: reloadtime = 0.0 - offdutycycle = reloadtime / ((max(mod.numShots, 1) * cycleTime) + reloadtime) + offdutycycle = reloadtime / ((max(afflictor.numShots, 1) * cycleTime) + reloadtime) localAdjustment[tankType] -= amount * offdutycycle / (cycleTime / 1000.0) # Sort repairers by efficiency. We want to use the most efficient repairers first @@ -1469,35 +1479,35 @@ class Fit: # Most efficient first, as we sorted earlier. # calculate how much the repper can rep stability & add to total totalPeakRecharge = self.capRecharge - for mod in repairers: + for afflictor in repairers: if capUsed > totalPeakRecharge: break - if self.factorReload and mod.charge: - reloadtime = mod.reloadTime + if self.factorReload and afflictor.charge: + reloadtime = afflictor.reloadTime else: reloadtime = 0.0 - cycleTime = mod.rawCycleTime - capPerSec = mod.capUse + cycleTime = afflictor.rawCycleTime + capPerSec = afflictor.capUse if capPerSec is not None and cycleTime is not None: # Check how much this repper can work sustainability = min(1, (totalPeakRecharge - capUsed) / capPerSec) - amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name]) + amount = afflictor.getModifiedItemAttr(groupAttrMap[afflictor.item.group.name]) # Add the sustainable amount - if not mod.charge: - localAdjustment[groupStoreMap[mod.item.group.name]] += sustainability * amount / ( + if not afflictor.charge: + localAdjustment[groupStoreMap[afflictor.item.group.name]] += sustainability * amount / ( cycleTime / 1000.0) else: - if mod.charge.name == "Nanite Repair Paste": - multiplier = mod.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1 + if afflictor.charge.name == "Nanite Repair Paste": + multiplier = afflictor.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1 else: multiplier = 1 - ondutycycle = (max(mod.numShots, 1) * cycleTime) / ( - (max(mod.numShots, 1) * cycleTime) + reloadtime) + ondutycycle = (max(afflictor.numShots, 1) * cycleTime) / ( + (max(afflictor.numShots, 1) * cycleTime) + reloadtime) localAdjustment[groupStoreMap[ - mod.item.group.name]] += sustainability * amount * ondutycycle * multiplier / ( + afflictor.item.group.name]] += sustainability * amount * ondutycycle * multiplier / ( cycleTime / 1000.0) capUsed += capPerSec diff --git a/gui/builtinContextMenus/skillAffectors.py b/gui/builtinContextMenus/skillAffectors.py index d72f07a78..84f276aba 100644 --- a/gui/builtinContextMenus/skillAffectors.py +++ b/gui/builtinContextMenus/skillAffectors.py @@ -60,7 +60,7 @@ class ChangeAffectingSkills(ContextMenuSingle): continue for fit, afflictors in cont.getAfflictions(attrName).items(): - for afflictor, modifier, amount, used in afflictors: + for afflictor, operator, stackingGroup, preResAmount, postResAmount, used in afflictors: # only add Skills if not isinstance(afflictor, Skill): continue diff --git a/gui/builtinItemStatsViews/itemAffectedBy.py b/gui/builtinItemStatsViews/itemAffectedBy.py index ac4d15048..7d32faf97 100644 --- a/gui/builtinItemStatsViews/itemAffectedBy.py +++ b/gui/builtinItemStatsViews/itemAffectedBy.py @@ -1,6 +1,7 @@ # noinspection PyPackageRequirements import wx +from eos.const import Operator from eos.saveddata.mode import Mode from eos.saveddata.character import Skill from eos.saveddata.implant import Implant @@ -17,6 +18,21 @@ from gui.contextMenu import ContextMenu from gui.bitmap_loader import BitmapLoader +def formatOperator(operator, stackingGroup, preResAmount, postResAmount): + opMap = { + Operator.PREASSIGN: '=', + Operator.PREINCREASE: '+', + Operator.MULTIPLY: '*', + Operator.POSTINCREASE: '+', + Operator.FORCE: '\u2263'} + prefix = '' + if stackingGroup is not None: + prefix += 's' + if preResAmount != postResAmount: + prefix += 'r' + return '{}{}'.format(prefix, opMap[operator]) + + class ItemAffectedBy(wx.Panel): ORDER = [Fit, Ship, Citadel, Mode, Module, Drone, Fighter, Implant, Booster, Skill] @@ -183,7 +199,7 @@ class ItemAffectedBy(wx.Panel): continue for fit, afflictors in attributes.getAfflictions(attrName).items(): - for afflictor, modifier, amount, used in afflictors: + for afflictor, operator, stackingGroup, preResAmount, postResAmount, used in afflictors: if not used or afflictor.item is None: continue @@ -209,8 +225,10 @@ class ItemAffectedBy(wx.Panel): else: item = afflictor.item - items[attrName].append( - (type(afflictor), afflictor, item, modifier, amount, getattr(afflictor, "projected", False))) + items[attrName].append(( + type(afflictor), afflictor, item, + formatOperator(operator, stackingGroup, preResAmount, postResAmount), + postResAmount, getattr(afflictor, "projected", False))) # Make sure projected fits are on top rootOrder = list(container.keys()) @@ -316,7 +334,7 @@ class ItemAffectedBy(wx.Panel): continue for fit, afflictors in attributes.getAfflictions(attrName).items(): - for afflictor, modifier, amount, used in afflictors: + for afflictor, operator, stackingGroup, preResAmount, postResAmount, used in afflictors: if not used or getattr(afflictor, 'item', None) is None: continue @@ -343,12 +361,13 @@ class ItemAffectedBy(wx.Panel): info = items[item.name] info[1].add(afflictor) + operatorStr = formatOperator(operator, stackingGroup, preResAmount, postResAmount) # If info[1] > 1, there are two separate modules working. # Check to make sure we only include the modifier once # See GH issue 154 - if len(info[1]) > 1 and (attrName, modifier, amount) in info[2]: + if len(info[1]) > 1 and (attrName, operatorStr, postResAmount) in info[2]: continue - info[2].append((attrName, modifier, amount)) + info[2].append((attrName, operatorStr, postResAmount)) # Make sure projected fits are on top rootOrder = list(container.keys()) diff --git a/gui/builtinViewColumns/misc.py b/gui/builtinViewColumns/misc.py index e56bc5fe8..8f1f0e357 100644 --- a/gui/builtinViewColumns/misc.py +++ b/gui/builtinViewColumns/misc.py @@ -444,6 +444,14 @@ class Miscellanea(ViewColumn): text = "{0}/s".format(formatAmount(rps, 3, 0, 3, forceSign=True)) tooltip = "Structure repaired per second" return text, tooltip + elif itemGroup in ("Capacitor Recharger", "Capacitor Power Relay", "Capacitor Battery"): + fit = Fit.getInstance().getFit(self.fittingView.getActiveFit()) + capGain = fit.getCapGainFromMod(stuff) + if not capGain: + return "", None + text = formatAmount(capGain, 3, 0, 3, forceSign=True) + tooltip = "Peak capacitor regeneration gain" + return text, tooltip elif itemGroup == "Gang Coordinator": command = stuff.getModifiedItemAttr("commandBonus") or stuff.getModifiedItemAttr("commandBonusHidden") if not command: