diff --git a/eos/mathUtils.py b/eos/mathUtils.py
deleted file mode 100644
index 845bb4844..000000000
--- a/eos/mathUtils.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# ===============================================================================
-# Copyright (C) 2010 Anton Vorobyov
-#
-# This file is part of eos.
-#
-# eos is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by
-# the Free Software Foundation, either version 2 of the License, or
-# (at your option) any later version.
-#
-# eos is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with eos. If not, see .
-# ===============================================================================
-
-from math import floor
-
-
-def floorFloat(value):
- result = int(floor(value))
- return result
diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py
index e332bc14f..aa92ff21a 100644
--- a/eos/saveddata/fit.py
+++ b/eos/saveddata/fit.py
@@ -127,6 +127,12 @@ class Fit(object):
self.__capUsed = None
self.__capRecharge = None
self.__calculatedTargets = []
+ self.__remoteReps = {
+ "Armor": None,
+ "Shield": None,
+ "Hull": None,
+ "Capacitor": None,
+ }
self.factorReload = False
self.boostsFits = set()
self.gangBoosts = None
@@ -392,6 +398,9 @@ class Fit(object):
self.ecmProjectedStr = 1
self.commandBonuses = {}
+ for remoterep_type in self.__remoteReps:
+ self.__remoteReps[remoterep_type] = None
+
del self.__calculatedTargets[:]
del self.__extraDrains[:]
@@ -1151,6 +1160,67 @@ class Fit(object):
self.__capStable = True
self.__capState = 100
+ @property
+ def remoteReps(self):
+ force_recalc = False
+ for remote_type in self.__remoteReps:
+ if self.__remoteReps[remote_type] is None:
+ force_recalc = True
+ break
+
+ if force_recalc is False:
+ return self.__remoteReps
+
+ # We are rerunning the recalcs. Explicitly set to 0 to make sure we don't duplicate anything and correctly set all values to 0.
+ for remote_type in self.__remoteReps:
+ self.__remoteReps[remote_type] = 0
+
+ for module in self.modules:
+ # Skip empty and non-Active modules
+ if module.isEmpty or module.state < State.ACTIVE:
+ continue
+
+ # Covert cycleTime to seconds
+ duration = module.cycleTime / 1000
+
+ # Skip modules with no duration.
+ if not duration:
+ continue
+
+ fueledMultiplier = module.getModifiedItemAttr("chargedArmorDamageMultiplier", 1)
+
+ remote_module_groups = {
+ "Remote Armor Repairer" : "Armor",
+ "Ancillary Remote Armor Repairer": "Armor",
+ "Remote Hull Repairer" : "Hull",
+ "Remote Shield Booster" : "Shield",
+ "Ancillary Remote Shield Booster": "Shield",
+ "Remote Capacitor Transmitter" : "Capacitor",
+ }
+
+ module_group = module.item.group.name
+
+ if module_group in remote_module_groups:
+ remote_type = remote_module_groups[module_group]
+ else:
+ # Module isn't in our list of remote rep modules, bail
+ continue
+
+ if remote_type == "Hull":
+ hp = module.getModifiedItemAttr("structureDamageAmount", 0)
+ elif remote_type == "Armor":
+ hp = module.getModifiedItemAttr("armorDamageAmount", 0)
+ elif remote_type == "Shield":
+ hp = module.getModifiedItemAttr("shieldBonus", 0)
+ elif remote_type == "Capacitor":
+ hp = module.getModifiedItemAttr("powerTransferAmount", 0)
+ else:
+ hp = 0
+
+ self.__remoteReps[remote_type] += (hp * fueledMultiplier) / duration
+
+ return self.__remoteReps
+
@property
def hp(self):
hp = {}
diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py
index 9ea524067..4442cb9b8 100644
--- a/eos/saveddata/module.py
+++ b/eos/saveddata/module.py
@@ -20,11 +20,11 @@
from logbook import Logger
from sqlalchemy.orm import validates, reconstructor
+from math import floor
import eos.db
from eos.effectHandlerHelpers import HandledItem, HandledCharge
from eos.enum import Enum
-from eos.mathUtils import floorFloat
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut
from eos.saveddata.citadel import Citadel
@@ -172,7 +172,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if chargeVolume is None or containerCapacity is None:
charges = 0
else:
- charges = floorFloat(float(containerCapacity) / chargeVolume)
+ charges = floor(containerCapacity / chargeVolume)
return charges
@property
@@ -216,9 +216,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def __calculateAmmoShots(self):
if self.charge is not None:
# Set number of cycles before reload is needed
+ # numcycles = math.floor(module_capacity / (module_volume * module_chargerate))
chargeRate = self.getModifiedItemAttr("chargeRate")
numCharges = self.numCharges
- numShots = floorFloat(float(numCharges) / chargeRate)
+ numShots = floor(numCharges / chargeRate)
else:
numShots = None
return numShots
@@ -231,7 +232,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
chance = self.getModifiedChargeAttr("crystalVolatilityChance")
damage = self.getModifiedChargeAttr("crystalVolatilityDamage")
crystals = self.numCharges
- numShots = floorFloat(float(crystals * hp) / (damage * chance))
+ numShots = floor((crystals * hp) / (damage * chance))
else:
# Set 0 (infinite) for permanent crystals like t1 laser crystals
numShots = 0
@@ -665,32 +666,68 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
@property
def cycleTime(self):
- reactivation = (self.getModifiedItemAttr("moduleReactivationDelay") or 0)
- # Reactivation time starts counting after end of module cycle
- speed = self.rawCycleTime + reactivation
- if self.charge:
- reload = self.reloadTime
- else:
- reload = 0.0
# Determine if we'll take into account reload time or not
factorReload = self.owner.factorReload if self.forceReload is None else self.forceReload
- # If reactivation is longer than 10 seconds then module can be reloaded
- # during reactivation time, thus we may ignore reload
- if factorReload and reactivation < reload:
- numShots = self.numShots
- # Time it takes to reload module after end of reactivation time,
- # given that we started when module cycle has just over
- additionalReloadTime = (reload - reactivation)
- # Speed here already takes into consideration reactivation time
- speed = (speed * numShots + additionalReloadTime) / numShots if numShots > 0 else speed
+
+ numShots = self.numShots
+ speed = self.rawCycleTime
+
+ if factorReload and self.charge:
+ raw_reload_time = self.reloadTime
+ else:
+ raw_reload_time = 0.0
+
+ # Module can only fire one shot at a time, think bomb launchers or defender launchers
+ if self.disallowRepeatingAction:
+ if numShots > 1:
+ """
+ The actual mechanics behind this is complex. Behavior will be (for 3 ammo):
+ fire, reactivation delay, fire, reactivation delay, fire, max(reactivation delay, reload)
+ so your effective reload time depends on where you are at in the cycle.
+
+ We can't do that, so instead we'll average it out.
+
+ Currently would apply to bomb launchers and defender missiles
+ """
+ effective_reload_time = ((self.reactivationDelay * numShots) + raw_reload_time) / numShots
+ else:
+ """
+ Applies to MJD/MJFG
+ """
+ effective_reload_time = max(raw_reload_time, self.reactivationDelay, 0)
+ else:
+ """
+ Currently no other modules would have a reactivation delay, so for sanities sake don't try and account for it.
+ Okay, technically cloaks do, but they also have 0 cycle time and cap usage so why do you care?
+ """
+ effective_reload_time = raw_reload_time
+
+ if numShots > 0 and self.charge:
+ speed = (speed * numShots + effective_reload_time) / numShots
return speed
@property
def rawCycleTime(self):
- speed = self.getModifiedItemAttr("speed") or self.getModifiedItemAttr("duration")
+ speed = max(
+ self.getModifiedItemAttr("speed"), # Most weapons
+ self.getModifiedItemAttr("duration"), # Most average modules
+ self.getModifiedItemAttr("durationSensorDampeningBurstProjector"),
+ self.getModifiedItemAttr("durationTargetIlluminationBurstProjector"),
+ self.getModifiedItemAttr("durationECMJammerBurstProjector"),
+ self.getModifiedItemAttr("durationWeaponDisruptionBurstProjector"),
+ 0, # Return 0 if none of the above are valid
+ )
return speed
+ @property
+ def disallowRepeatingAction(self):
+ return self.getModifiedItemAttr("disallowRepeatingAction", 0)
+
+ @property
+ def reactivationDelay(self):
+ return self.getModifiedItemAttr("moduleReactivationDelay", 0)
+
@property
def capUse(self):
capNeed = self.getModifiedItemAttr("capacitorNeed")
diff --git a/gui/builtinStatsViews/__init__.py b/gui/builtinStatsViews/__init__.py
index 5f2ca646e..7161d1c38 100644
--- a/gui/builtinStatsViews/__init__.py
+++ b/gui/builtinStatsViews/__init__.py
@@ -1,3 +1,3 @@
__all__ = ["resourcesViewFull", "resistancesViewFull",
- "rechargeViewFull", "firepowerViewFull", "capacitorViewFull",
+ "rechargeViewFull", "firepowerViewFull", "capacitorViewFull", "outgoingViewFull",
"targetingMiscViewFull", "priceViewFull", "miningyieldViewFull"]
diff --git a/gui/builtinStatsViews/outgoingViewFull.py b/gui/builtinStatsViews/outgoingViewFull.py
new file mode 100644
index 000000000..346aacc7a
--- /dev/null
+++ b/gui/builtinStatsViews/outgoingViewFull.py
@@ -0,0 +1,106 @@
+# ===============================================================================
+# Copyright (C) 2014 Alexandros Kosiaris
+#
+# This file is part of pyfa.
+#
+# pyfa is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# pyfa is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with pyfa. If not, see .
+# ===============================================================================
+
+# noinspection PyPackageRequirements
+import wx
+from gui.statsView import StatsView
+from gui.bitmapLoader import BitmapLoader
+from gui.utils.numberFormatter import formatAmount
+
+
+class OutgoingViewFull(StatsView):
+ name = "outgoingViewFull"
+
+ def __init__(self, parent):
+ StatsView.__init__(self)
+ self.parent = parent
+ self._cachedValues = []
+
+ def getHeaderText(self, fit):
+ return "Remote Reps"
+
+ def getTextExtentW(self, text):
+ width, height = self.parent.GetTextExtent(text)
+ return width
+
+ def populatePanel(self, contentPanel, headerPanel):
+ contentSizer = contentPanel.GetSizer()
+ parent = self.panel = contentPanel
+ self.headerPanel = headerPanel
+
+ sizerOutgoing = wx.GridSizer(1, 4)
+
+ contentSizer.Add(sizerOutgoing, 0, wx.EXPAND, 0)
+
+ counter = 0
+
+ rr_list = [
+ ("RemoteCapacitor", "Capacitor:", "capacitorInfo", "Capacitor GJ/s per second transferred remotely."),
+ ("RemoteShield", "Shield:", "shieldActive", "Shield hitpoints per second repaired remotely."),
+ ("RemoteArmor", "Armor:", "armorActive", "Armor hitpoints per second repaired remotely."),
+ ("RemoteHull", "Hull:", "hullActive", "Hull hitpoints per second repaired remotely."),
+ ]
+
+ for outgoingType, label, image, tooltip in rr_list:
+ baseBox = wx.BoxSizer(wx.VERTICAL)
+
+ baseBox.Add(BitmapLoader.getStaticBitmap("%s_big" % image, parent, "gui"), 0, wx.ALIGN_CENTER)
+
+ if "Capacitor" in outgoingType:
+ lbl = wx.StaticText(parent, wx.ID_ANY, u"0 GJ/s")
+ else:
+ lbl = wx.StaticText(parent, wx.ID_ANY, u"0 HP/s")
+
+ lbl.SetToolTip(wx.ToolTip(tooltip))
+
+ setattr(self, "label%s" % outgoingType, lbl)
+
+ baseBox.Add(lbl, 0, wx.ALIGN_CENTER)
+ self._cachedValues.append(0)
+ counter += 1
+
+ sizerOutgoing.Add(baseBox, 1, wx.ALIGN_LEFT)
+
+ def refreshPanel(self, fit):
+ # If we did anything intresting, we'd update our labels to reflect the new fit's stats here
+
+ stats = [
+ ("labelRemoteArmor", lambda: fit.remoteReps["Armor"], 3, 0, 0, u"%s HP/s", None),
+ ("labelRemoteShield", lambda: fit.remoteReps["Shield"], 3, 0, 0, u"%s HP/s", None),
+ ("labelRemoteHull", lambda: fit.remoteReps["Hull"], 3, 0, 0, u"%s HP/s", None),
+ ("labelRemoteCapacitor", lambda: fit.remoteReps["Capacitor"], 3, 0, 0, u"%s GJ/s", None),
+ ]
+
+ counter = 0
+ for labelName, value, prec, lowest, highest, valueFormat, altFormat in stats:
+ label = getattr(self, labelName)
+ value = value() if fit is not None else 0
+ value = value if value is not None else 0
+ if self._cachedValues[counter] != value:
+ valueStr = formatAmount(value, prec, lowest, highest)
+ label.SetLabel(valueFormat % valueStr)
+ tipStr = valueFormat % valueStr if altFormat is None else altFormat % value
+ label.SetToolTip(wx.ToolTip(tipStr))
+ self._cachedValues[counter] = value
+ counter += 1
+ self.panel.Layout()
+ self.headerPanel.Layout()
+
+
+OutgoingViewFull.register()
diff --git a/gui/builtinStatsViews/outgoingViewMinimal.py b/gui/builtinStatsViews/outgoingViewMinimal.py
new file mode 100644
index 000000000..346aacc7a
--- /dev/null
+++ b/gui/builtinStatsViews/outgoingViewMinimal.py
@@ -0,0 +1,106 @@
+# ===============================================================================
+# Copyright (C) 2014 Alexandros Kosiaris
+#
+# This file is part of pyfa.
+#
+# pyfa is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# pyfa is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with pyfa. If not, see .
+# ===============================================================================
+
+# noinspection PyPackageRequirements
+import wx
+from gui.statsView import StatsView
+from gui.bitmapLoader import BitmapLoader
+from gui.utils.numberFormatter import formatAmount
+
+
+class OutgoingViewFull(StatsView):
+ name = "outgoingViewFull"
+
+ def __init__(self, parent):
+ StatsView.__init__(self)
+ self.parent = parent
+ self._cachedValues = []
+
+ def getHeaderText(self, fit):
+ return "Remote Reps"
+
+ def getTextExtentW(self, text):
+ width, height = self.parent.GetTextExtent(text)
+ return width
+
+ def populatePanel(self, contentPanel, headerPanel):
+ contentSizer = contentPanel.GetSizer()
+ parent = self.panel = contentPanel
+ self.headerPanel = headerPanel
+
+ sizerOutgoing = wx.GridSizer(1, 4)
+
+ contentSizer.Add(sizerOutgoing, 0, wx.EXPAND, 0)
+
+ counter = 0
+
+ rr_list = [
+ ("RemoteCapacitor", "Capacitor:", "capacitorInfo", "Capacitor GJ/s per second transferred remotely."),
+ ("RemoteShield", "Shield:", "shieldActive", "Shield hitpoints per second repaired remotely."),
+ ("RemoteArmor", "Armor:", "armorActive", "Armor hitpoints per second repaired remotely."),
+ ("RemoteHull", "Hull:", "hullActive", "Hull hitpoints per second repaired remotely."),
+ ]
+
+ for outgoingType, label, image, tooltip in rr_list:
+ baseBox = wx.BoxSizer(wx.VERTICAL)
+
+ baseBox.Add(BitmapLoader.getStaticBitmap("%s_big" % image, parent, "gui"), 0, wx.ALIGN_CENTER)
+
+ if "Capacitor" in outgoingType:
+ lbl = wx.StaticText(parent, wx.ID_ANY, u"0 GJ/s")
+ else:
+ lbl = wx.StaticText(parent, wx.ID_ANY, u"0 HP/s")
+
+ lbl.SetToolTip(wx.ToolTip(tooltip))
+
+ setattr(self, "label%s" % outgoingType, lbl)
+
+ baseBox.Add(lbl, 0, wx.ALIGN_CENTER)
+ self._cachedValues.append(0)
+ counter += 1
+
+ sizerOutgoing.Add(baseBox, 1, wx.ALIGN_LEFT)
+
+ def refreshPanel(self, fit):
+ # If we did anything intresting, we'd update our labels to reflect the new fit's stats here
+
+ stats = [
+ ("labelRemoteArmor", lambda: fit.remoteReps["Armor"], 3, 0, 0, u"%s HP/s", None),
+ ("labelRemoteShield", lambda: fit.remoteReps["Shield"], 3, 0, 0, u"%s HP/s", None),
+ ("labelRemoteHull", lambda: fit.remoteReps["Hull"], 3, 0, 0, u"%s HP/s", None),
+ ("labelRemoteCapacitor", lambda: fit.remoteReps["Capacitor"], 3, 0, 0, u"%s GJ/s", None),
+ ]
+
+ counter = 0
+ for labelName, value, prec, lowest, highest, valueFormat, altFormat in stats:
+ label = getattr(self, labelName)
+ value = value() if fit is not None else 0
+ value = value if value is not None else 0
+ if self._cachedValues[counter] != value:
+ valueStr = formatAmount(value, prec, lowest, highest)
+ label.SetLabel(valueFormat % valueStr)
+ tipStr = valueFormat % valueStr if altFormat is None else altFormat % value
+ label.SetToolTip(wx.ToolTip(tipStr))
+ self._cachedValues[counter] = value
+ counter += 1
+ self.panel.Layout()
+ self.headerPanel.Layout()
+
+
+OutgoingViewFull.register()
diff --git a/gui/statsPane.py b/gui/statsPane.py
index 6aaf277f5..c86786f97 100644
--- a/gui/statsPane.py
+++ b/gui/statsPane.py
@@ -33,7 +33,7 @@ from gui.pyfatogglepanel import TogglePanel
class StatsPane(wx.Panel):
DEFAULT_VIEWS = ["resourcesViewFull", "resistancesViewFull", "rechargeViewFull", "firepowerViewFull",
"capacitorViewFull", "targetingmiscViewFull",
- "priceViewFull"]
+ "priceViewFull", "outgoingViewFull"]
def fitChanged(self, event):
sFit = Fit.getInstance()
diff --git a/gui/statsView.py b/gui/statsView.py
index 773063ca4..dbdb6da3c 100644
--- a/gui/statsView.py
+++ b/gui/statsView.py
@@ -52,4 +52,5 @@ from gui.builtinStatsViews import ( # noqa: E402, F401
rechargeViewFull,
targetingMiscViewFull,
priceViewFull,
+ outgoingViewFull,
)
diff --git a/tests/test_modules/eos/test_mathUtils.py b/tests/test_modules/eos/test_mathUtils.py
deleted file mode 100644
index c5cf2b0bb..000000000
--- a/tests/test_modules/eos/test_mathUtils.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from eos.mathUtils import floorFloat
-
-
-def test_floorFloat():
- assert type(floorFloat(1)) is not float
- assert type(floorFloat(1)) is int
- assert type(floorFloat(1.1)) is not float
- assert type(floorFloat(1.1)) is int
- assert floorFloat(1.1) == 1
- assert floorFloat(1.9) == 1
- assert floorFloat(1.5) == 1
- assert floorFloat(-1.5) == -2