Rework fighter calculations to use cycle parameters

This commit is contained in:
DarkPhoenix
2019-05-12 02:18:44 +03:00
parent cb8f76c582
commit 56d9a8b626
6 changed files with 95 additions and 59 deletions

View File

@@ -153,7 +153,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
volley = self.getVolley(targetResists=targetResists)
if not volley:
return DmgTypes(0, 0, 0, 0)
dpsFactor = 1 / (self.cycleParameters.averageTime / 1000)
dpsFactor = 1 / (self.getCycleParameters().averageTime / 1000)
dps = DmgTypes(
em=volley.em * dpsFactor,
thermal=volley.thermal * dpsFactor,
@@ -161,8 +161,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
explosive=volley.explosive * dpsFactor)
return dps
@property
def cycleParameters(self):
def getCycleParameters(self):
return CycleInfo(self.cycleTime, 0, math.inf)
def getRemoteReps(self, ignoreState=False):
@@ -186,7 +185,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
rrAmount = 0
if rrAmount:
droneAmount = self.amount if ignoreState else self.amountActive
rrAmount *= droneAmount / (self.cycleParameters.averageTime / 1000)
rrAmount *= droneAmount / (self.getCycleParameters().averageTime / 1000)
self.__baseRemoteReps = (rrType, rrAmount)
return self.__baseRemoteReps
@@ -195,7 +194,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if self.__miningyield is None:
if self.mines is True and self.amountActive > 0:
getter = self.getModifiedItemAttr
cycleTime = self.cycleParameters.averageTime
cycleTime = self.getCycleParameters().averageTime
volley = sum([getter(d) for d in self.MINING_ATTRIBUTES]) * self.amountActive
self.__miningyield = volley / (cycleTime / 1000.0)
else:

View File

@@ -17,16 +17,19 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
import math
from logbook import Logger
from sqlalchemy.orm import validates, reconstructor
from sqlalchemy.orm import reconstructor, validates
import eos.db
from eos.effectHandlerHelpers import HandledItem, HandledCharge
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut
from eos.saveddata.fighterAbility import FighterAbility
from eos.utils.stats import DmgTypes
from eos.const import FittingSlot
from eos.effectHandlerHelpers import HandledCharge, HandledItem
from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict
from eos.saveddata.fighterAbility import FighterAbility
from eos.utils.cycles import CycleInfo, CycleSequence
from eos.utils.stats import DmgTypes
from eos.utils.float import floatUnerr
pyfalog = Logger(__name__)
@@ -207,44 +210,71 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
explosive += dps.explosive
return DmgTypes(em=em, thermal=thermal, kinetic=kinetic, explosive=explosive)
def getUptime(self):
if not self.owner.factorReload:
return 1
activeTimes = []
reloadTimes = []
for ability in self.abilities:
if ability.numShots > 0:
activeTimes.append(ability.numShots * ability.cycleTime)
reloadTimes.append(ability.reloadTime)
if not activeTimes:
return 1
shortestActive = sorted(activeTimes)[0]
longestReload = sorted(reloadTimes, reverse=True)[0]
uptime = shortestActive / (shortestActive + longestReload)
return uptime
def getDpsPerEffect(self, targetResists=None):
if not self.active or self.amountActive <= 0:
return {}
uptime = self.getUptime()
if uptime == 1:
return {a.effectID: a.getDps(targetResists=targetResists) for a in self.abilities}
cycleParamsInfinite = self.getCycleParametersPerEffectInfinite()
cycleParamsReload = self.getCycleParametersPerEffect()
dpsMapOnlyInfinite = {}
dpsMapAllWithReloads = {}
# Decide if it's better to keep steady dps up and never reload or reload from time to time
dpsMapSteady = {}
dpsMapPeakAdjusted = {}
for ability in self.abilities:
abilityDps = ability.getDps(targetResists=targetResists)
dpsMapPeakAdjusted[ability.effectID] = DmgTypes(
em=abilityDps.em * uptime,
thermal=abilityDps.thermal * uptime,
kinetic=abilityDps.kinetic * uptime,
explosive=abilityDps.explosive * uptime)
# Infinite use - add to steady dps
if ability.numShots == 0:
dpsMapSteady[ability.effectID] = abilityDps
totalSteady = sum(i.total for i in dpsMapSteady.values())
totalPeakAdjusted = sum(i.total for i in dpsMapPeakAdjusted.values())
return dpsMapSteady if totalSteady >= totalPeakAdjusted else dpsMapPeakAdjusted
if ability.effectID in cycleParamsInfinite:
cycleTime = cycleParamsInfinite[ability.effectID].averageTime
dpsMapOnlyInfinite[ability.effectID] = ability.getDps(targetResists=targetResists, cycleTimeOverride=cycleTime)
if ability.effectID in cycleParamsReload:
cycleTime = cycleParamsReload[ability.effectID].averageTime
dpsMapAllWithReloads[ability.effectID] = ability.getDps(targetResists=targetResists, cycleTimeOverride=cycleTime)
totalOnlyInfinite = sum(i.total for i in dpsMapOnlyInfinite.values())
totalAllWithReloads = sum(i.total for i in dpsMapAllWithReloads.values())
return dpsMapOnlyInfinite if totalOnlyInfinite >= totalAllWithReloads else dpsMapAllWithReloads
def getCycleParametersPerEffectInfinite(self):
return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities if a.numShots == 0}
def getCycleParametersPerEffect(self):
# Assume it can cycle infinitely
if not self.owner.factorReload:
return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities}
limitedAbilities = [a for a in self.abilities if a.numShots > 0]
if len(limitedAbilities) == 0:
return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities}
validAbilities = [a for a in self.abilities if a.cycleTime > 0]
if len(validAbilities) == 0:
return {}
mostLimitedAbility = min(limitedAbilities, key=lambda a: a.cycleTime * a.numShots)
durationToRefuel = mostLimitedAbility.cycleTime * mostLimitedAbility.numShots
# find out how many shots various abilities will do until reload, and how much time
# "extra" cycle will last (None for no extra cycle)
cyclesUntilRefuel = {mostLimitedAbility.effectID: (mostLimitedAbility.numShots, None)}
for ability in (a for a in validAbilities if a is not mostLimitedAbility):
fullCycles = int(floatUnerr(durationToRefuel / ability.cycleTime))
extraShotTime = floatUnerr(durationToRefuel - (fullCycles * ability.cycleTime))
if extraShotTime == 0:
extraShotTime = None
cyclesUntilRefuel[ability.effectID] = (fullCycles, extraShotTime)
refuelTimes = {}
for ability in validAbilities:
spentShots, extraShotTime = cyclesUntilRefuel[ability.effectID]
if extraShotTime is not None:
spentShots += 1
refuelTimes[ability.effectID] = ability.getReloadTime(spentShots)
refuelTime = max(refuelTimes.values())
cycleParams = {}
for ability in validAbilities:
regularShots, extraShotTime = cyclesUntilRefuel[ability.effectID]
sequence = []
if extraShotTime is not None:
if regularShots > 0:
sequence.append(CycleInfo(ability.cycleTime, 0, regularShots))
sequence.append(CycleInfo(extraShotTime, refuelTime, 1))
else:
regularShotsNonReload = regularShots - 1
if regularShotsNonReload > 0:
sequence.append(CycleInfo(ability.cycleTime, 0, regularShotsNonReload))
sequence.append(CycleInfo(ability.cycleTime, refuelTime, 1))
cycleParams[ability.effectID] = CycleSequence(sequence, math.inf)
return cycleParams
@property
def maxRange(self):

View File

@@ -95,8 +95,15 @@ class FighterAbility(object):
@property
def reloadTime(self):
return self.getReloadTime()
def getReloadTime(self, spentShots=None):
if spentShots is not None:
spentShots = max(self.numShots, spentShots)
else:
spentShots = self.numShots
rearm_time = (self.REARM_TIME_MAPPING[self.fighter.getModifiedItemAttr("fighterSquadronRole")] or 0 if self.hasCharges else 0)
return self.fighter.getModifiedItemAttr("fighterRefuelingTime") + rearm_time * self.numShots
return self.fighter.getModifiedItemAttr("fighterRefuelingTime") + rearm_time * spentShots
@property
def numShots(self):
@@ -128,11 +135,12 @@ class FighterAbility(object):
explosive=exp * dmgMult * (1 - getattr(targetResists, "explosiveAmount", 0)))
return volley
def getDps(self, targetResists=None):
def getDps(self, targetResists=None, cycleTimeOverride=None):
volley = self.getVolley(targetResists=targetResists)
if not volley:
return DmgTypes(0, 0, 0, 0)
dpsFactor = 1 / (self.cycleTime / 1000)
cycleTime = cycleTimeOverride if cycleTimeOverride is not None else self.cycleTime
dpsFactor = 1 / (cycleTime / 1000)
dps = DmgTypes(
em=volley.em * dpsFactor,
thermal=volley.thermal * dpsFactor,

View File

@@ -401,7 +401,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
volley = self.getModifiedItemAttr("specialtyMiningAmount") or self.getModifiedItemAttr(
"miningAmount") or 0
if volley:
cycleTime = self.cycleParameters.averageTime
cycleTime = self.getCycleParameters().averageTime
self.__miningyield = volley / (cycleTime / 1000.0)
else:
self.__miningyield = 0
@@ -440,7 +440,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return DmgTypes(0, 0, 0, 0)
# Some weapons repeat multiple times in one cycle (bosonic doomsdays). Get the number of times it fires off
volleysPerCycle = max(self.getModifiedItemAttr("doomsdayDamageDuration", 1) / self.getModifiedItemAttr("doomsdayDamageCycleTime", 1), 1)
dpsFactor = volleysPerCycle / (self.cycleParameters.averageTime / 1000)
dpsFactor = volleysPerCycle / (self.getCycleParameters().averageTime / 1000)
dps = DmgTypes(
em=volley.em * dpsFactor,
thermal=volley.thermal * dpsFactor,
@@ -475,7 +475,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
else:
return None, 0
if rrAmount:
rrAmount *= 1 / (self.cycleParameters.averageTime / 1000)
rrAmount *= 1 / (self.getCycleParameters().averageTime / 1000)
if module.item.group.name == "Ancillary Remote Armor Repairer" and module.charge:
rrAmount *= module.getModifiedItemAttr("chargedArmorDamageMultiplier", 1)
@@ -820,8 +820,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
except:
effect.handler(fit, self, context)
@property
def cycleParameters(self):
def getCycleParameters(self):
"""Copied from new eos as well"""
# Determine if we'll take into account reload time or not
factorReload = self.owner.factorReload if self.forceReload is None else self.forceReload
@@ -896,7 +895,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def capUse(self):
capNeed = self.getModifiedItemAttr("capacitorNeed")
if capNeed and self.state >= FittingModuleState.ACTIVE:
cycleTime = self.cycleParameters.averageTime
cycleTime = self.getCycleParameters().averageTime
if cycleTime > 0:
capUsed = capNeed / (cycleTime / 1000.0)
return capUsed

View File

@@ -140,7 +140,7 @@ class Miscellanea(ViewColumn):
return "+ " + ", ".join(info), "Slot Modifiers"
elif itemGroup == "Energy Neutralizer":
neutAmount = stuff.getModifiedItemAttr("energyNeutralizerAmount")
cycleTime = stuff.cycleParameters.averageTime
cycleTime = stuff.getCycleParameters().averageTime
if not neutAmount or not cycleTime:
return "", None
capPerSec = float(-neutAmount) * 1000 / cycleTime
@@ -149,7 +149,7 @@ class Miscellanea(ViewColumn):
return text, tooltip
elif itemGroup == "Energy Nosferatu":
neutAmount = stuff.getModifiedItemAttr("powerTransferAmount")
cycleTime = stuff.cycleParameters.averageTime
cycleTime = stuff.getCycleParameters().averageTime
if not neutAmount or not cycleTime:
return "", None
capPerSec = float(-neutAmount) * 1000 / cycleTime

View File

@@ -356,7 +356,7 @@ class EfsPort:
"dps": stats.getDps(spoolOptions=spoolOptions).total * n, "capUse": stats.capUse * n, "falloff": stats.falloff,
"type": typeing, "name": name, "optimal": maxRange,
"numCharges": stats.numCharges, "numShots": stats.numShots, "reloadTime": stats.reloadTime,
"cycleTime": stats.cycleParameters.averageTime, "volley": stats.getVolley(spoolOptions=spoolOptions).total * n, "tracking": tracking,
"cycleTime": stats.getCycleParameters().averageTime, "volley": stats.getVolley(spoolOptions=spoolOptions).total * n, "tracking": tracking,
"maxVelocity": maxVelocity, "explosionDelay": explosionDelay, "damageReductionFactor": damageReductionFactor,
"explosionRadius": explosionRadius, "explosionVelocity": explosionVelocity, "aoeFieldRange": aoeFieldRange,
"damageMultiplierBonusMax": stats.getModifiedItemAttr("damageMultiplierBonusMax"),
@@ -369,7 +369,7 @@ class EfsPort:
# Drones are using the old tracking formula for trackingSpeed. This updates it to match turrets.
newTracking = droneAttr("trackingSpeed") / (droneAttr("optimalSigRadius") / 40000)
statDict = {
"dps": drone.getDps().total, "cycleTime": drone.cycleParameters.averageTime, "type": "Drone",
"dps": drone.getDps().total, "cycleTime": drone.getCycleParameters().averageTime, "type": "Drone",
"optimal": drone.maxRange, "name": drone.item.name, "falloff": drone.falloff,
"maxSpeed": droneAttr("maxVelocity"), "tracking": newTracking,
"volley": drone.getVolley().total
@@ -498,11 +498,11 @@ class EfsPort:
fitMultipliers["drones"] = list(map(getDroneMulti, tf.drones))
getFitTurrets = lambda f: filter(lambda mod: mod.hardpoint == FittingHardpoint.TURRET, f.modules)
getTurretMulti = lambda mod: mod.getModifiedItemAttr("damageMultiplier") / mod.cycleParameters.averageTime
getTurretMulti = lambda mod: mod.getModifiedItemAttr("damageMultiplier") / mod.getCycleParameters().averageTime
fitMultipliers["turrets"] = list(map(getTurretMulti, getFitTurrets(tf)))
getFitLaunchers = lambda f: filter(lambda mod: mod.hardpoint == FittingHardpoint.MISSILE, f.modules)
getLauncherMulti = lambda mod: sumDamage(mod.getModifiedChargeAttr) / mod.cycleParameters.averageTime
getLauncherMulti = lambda mod: sumDamage(mod.getModifiedChargeAttr) / mod.getCycleParameters().averageTime
fitMultipliers["launchers"] = list(map(getLauncherMulti, getFitLaunchers(tf)))
return fitMultipliers