Merge pull request #168 from blitzmann/161-effDps

Implement effective DPS
This commit is contained in:
Ryan Holmes
2014-09-19 19:18:20 -04:00
28 changed files with 1075 additions and 151 deletions

View File

@@ -66,7 +66,7 @@ from eos.db.saveddata.queries import getUser, getCharacter, getFit, getFitsWithS
getCharacterList, getPrice, getDamagePatternList, getDamagePattern, \
getFitList, getFleetList, getFleet, save, remove, commit, add, \
getCharactersForUser, getMiscData, getSquadsIDsWithFitID, getWing, \
getSquad, getBoosterFits, getProjectedFits
getSquad, getBoosterFits, getProjectedFits, getTargetResistsList, getTargetResists
#If using in memory saveddata, you'll want to reflect it so the data structure is good.
if config.saveddata_connectionstring == "sqlite:///:memory:":

View File

@@ -4,6 +4,7 @@ def update(saveddata_engine):
checkPriceFailures(saveddata_engine)
checkApiDefaultChar(saveddata_engine)
checkFitBooster(saveddata_engine)
checktargetResists(saveddata_engine)
def checkPriceFailures(saveddata_engine):
# Check if we have 'failed' column
@@ -57,3 +58,19 @@ def checkFitBooster(saveddata_engine):
saveddata_engine.execute("ALTER TABLE fits ADD COLUMN booster BOOLEAN;")
# Set NULL data to 0 (needed in case of downgrade, see GH issue #62
saveddata_engine.execute("UPDATE fits SET booster = 0 WHERE booster IS NULL;")
def checktargetResists(saveddata_engine):
try:
saveddata_engine.execute("SELECT * FROM fits LIMIT 1")
# If table doesn't exist, it means we're doing everything from scratch
# and sqlalchemy will process everything as needed
except sqlalchemy.exc.DatabaseError:
pass
# If not, we're running on top of existing DB
else:
# Check that we have columns
try:
saveddata_engine.execute("SELECT targetResistsID FROM fits LIMIT 1")
# If we don't, create them
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE fits ADD COLUMN targetResistsID INTEGER;")

View File

@@ -1,3 +1,3 @@
__all__ = ["character", "fit", "module", "user", "skill", "price",
"booster", "drone", "implant", "fleet", "damagePattern",
"miscData"]
"miscData", "targetResists"]

View File

@@ -26,10 +26,11 @@ from eos.db.saveddata.module import modules_table
from eos.db.saveddata.drone import drones_table
from eos.db.saveddata.cargo import cargo_table
from eos.db.saveddata.implant import fitImplants_table
from eos.types import Fit, Module, User, Booster, Drone, Cargo, Implant, Character, DamagePattern
from eos.types import Fit, Module, User, Booster, Drone, Cargo, Implant, Character, DamagePattern, TargetResists
from eos.effectHandlerHelpers import HandledModuleList, HandledDroneList, \
HandledImplantBoosterList, HandledProjectedModList, HandledProjectedDroneList, \
HandledProjectedFitList, HandledCargoList
fits_table = Table("fits", saveddata_meta,
Column("ID", Integer, primary_key = True),
Column("ownerID", ForeignKey("users.ID"), nullable = True, index = True),
@@ -38,7 +39,8 @@ fits_table = Table("fits", saveddata_meta,
Column("timestamp", Integer, nullable = False),
Column("characterID", ForeignKey("characters.ID"), nullable = True),
Column("damagePatternID", ForeignKey("damagePatterns.ID"), nullable=True),
Column("booster", Boolean, nullable = False, index = True, default = 0))
Column("booster", Boolean, nullable = False, index = True, default = 0),
Column("targetResistsID", ForeignKey("targetResists.ID"), nullable=True))
projectedFits_table = Table("projectedFits", saveddata_meta,
Column("sourceID", ForeignKey("fits.ID"), primary_key = True),
@@ -64,6 +66,7 @@ mapper(Fit, fits_table,
secondary = fitImplants_table),
"_Fit__character" : relation(Character, backref = "fits"),
"_Fit__damagePattern" : relation(DamagePattern),
"_Fit__targetResists" : relation(TargetResists),
"_Fit__projectedFits" : relation(Fit,
primaryjoin = projectedFits_table.c.victimID == fits_table.c.ID,
secondaryjoin = fits_table.c.ID == projectedFits_table.c.sourceID,

View File

@@ -19,7 +19,7 @@
from eos.db.util import processEager, processWhere
from eos.db import saveddata_session, sd_lock
from eos.types import User, Character, Fit, Price, DamagePattern, Fleet, MiscData, Wing, Squad
from eos.types import User, Character, Fit, Price, DamagePattern, Fleet, MiscData, Wing, Squad, TargetResists
from eos.db.saveddata.fleet import squadmembers_table
from eos.db.saveddata.fit import projectedFits_table
from sqlalchemy.sql import and_
@@ -322,6 +322,12 @@ def getDamagePatternList(eager=None):
patterns = saveddata_session.query(DamagePattern).options(*eager).all()
return patterns
def getTargetResistsList(eager=None):
eager = processEager(eager)
with sd_lock:
patterns = saveddata_session.query(TargetResists).options(*eager).all()
return patterns
@cachedQuery(DamagePattern, 1, "lookfor")
def getDamagePattern(lookfor, eager=None):
if isinstance(lookfor, int):
@@ -340,6 +346,24 @@ def getDamagePattern(lookfor, eager=None):
raise TypeError("Need integer or string as argument")
return pattern
@cachedQuery(TargetResists, 1, "lookfor")
def getTargetResists(lookfor, eager=None):
if isinstance(lookfor, int):
if eager is None:
with sd_lock:
pattern = saveddata_session.query(TargetResists).get(lookfor)
else:
eager = processEager(eager)
with sd_lock:
pattern = saveddata_session.query(TargetResists).options(*eager).filter(TargetResists.ID == lookfor).first()
elif isinstance(lookfor, basestring):
eager = processEager(eager)
with sd_lock:
pattern = saveddata_session.query(TargetResists).options(*eager).filter(TargetResists.name == lookfor).first()
else:
raise TypeError("Need integer or string as argument")
return pattern
def searchFits(nameLike, where=None, eager=None):
if not isinstance(nameLike, basestring):
raise TypeError("Need string as argument")
@@ -361,7 +385,7 @@ def getSquadsIDsWithFitID(fitID):
return squads
else:
raise TypeError("Need integer as argument")
def getProjectedFits(fitID):
if isinstance(fitID, int):
with sd_lock:
@@ -369,7 +393,7 @@ def getProjectedFits(fitID):
fits = saveddata_session.query(Fit).filter(filter).all()
return fits
else:
raise TypeError("Need integer as argument")
raise TypeError("Need integer as argument")
def add(stuff):
with sd_lock:

View File

@@ -0,0 +1,35 @@
#===============================================================================
# Copyright (C) 2014 Ryan Holmes
#
# 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 <http://www.gnu.org/licenses/>.
#===============================================================================
from sqlalchemy import Table, Column, Integer, Float, ForeignKey, String
from sqlalchemy.orm import mapper
from eos.db import saveddata_meta
from eos.types import TargetResists
targetResists_table = Table("targetResists", saveddata_meta,
Column("ID", Integer, primary_key = True),
Column("name", String),
Column("emAmount", Float),
Column("thermalAmount", Float),
Column("kineticAmount", Float),
Column("explosiveAmount", Float),
Column("ownerID", ForeignKey("users.ID"), nullable=True))
mapper(TargetResists, targetResists_table)

View File

@@ -69,26 +69,44 @@ class DamagePattern(object):
def importPatterns(cls, text):
lines = re.split('[\n\r]+', text)
patterns = []
numPatterns = 0
for line in lines:
line = line.split('#',1)[0] # allows for comments
name, data = line.rsplit('=',1)
name, data = name.strip(), ''.join(data.split()) # whitespace
try:
if line.strip()[0] == "#": # comments
continue
line = line.split('#',1)[0] # allows for comments
type, data = line.rsplit('=',1)
type, data = type.strip(), data.split(',')
except:
# Data isn't in correct format, continue to next line
continue
if type != "DamageProfile":
continue
numPatterns += 1
name, data = data[0], data[1:5]
fields = {}
for entry in data.split(','):
key, val = entry.split(':')
fields["%sAmount" % cls.importMap[key.lower()]] = float(val)
if len(fields) > 0: # Avoid possible blank lines
for index, val in enumerate(data):
try:
fields["%sAmount" % cls.DAMAGE_TYPES[index]] = int(val)
except:
continue
if len(fields) == 4: # Avoid possible blank lines
pattern = DamagePattern(**fields)
pattern.name = name
pattern.name = name.strip()
patterns.append(pattern)
return patterns
EXPORT_FORMAT = "%s = EM:%d, Therm:%d, Kin:%d, Exp:%d\n"
return patterns, numPatterns
EXPORT_FORMAT = "DamageProfile = %s,%d,%d,%d,%d\n"
@classmethod
def exportPatterns(cls, *patterns):
out = ""
out = "# Exported from pyfa\n#\n"
out += "# Values are in following format:\n"
out += "# DamageProfile = [name],[EM amount],[Thermal amount],[Kinetic amount],[Explosive amount]\n\n"
for dp in patterns:
out += cls.EXPORT_FORMAT % (dp.name, dp.emAmount, dp.thermalAmount, dp.kineticAmount, dp.explosiveAmount)

View File

@@ -22,7 +22,7 @@ from eos.effectHandlerHelpers import HandledItem, HandledCharge
from sqlalchemy.orm import validates, reconstructor
class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
DAMAGE_ATTRIBUTES = ("emDamage", "kineticDamage", "explosiveDamage", "thermalDamage")
DAMAGE_TYPES = ("em", "kinetic", "explosive", "thermal")
MINING_ATTRIBUTES = ("miningAmount",)
def __init__(self, item):
@@ -111,6 +111,9 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
@property
def dps(self):
return self.damageStats()
def damageStats(self, targetResists = None):
if self.__dps == None:
if self.dealsDamage is True and self.amountActive > 0:
if self.hasAmmo:
@@ -121,7 +124,9 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
getter = self.getModifiedItemAttr
cycleTime = self.getModifiedItemAttr(attr)
volley = sum(map(lambda d: getter(d), self.DAMAGE_ATTRIBUTES)) * self.amountActive
volley = sum(map(lambda d: (getter("%sDamage"%d) or 0) * (1-getattr(targetResists, "%sAmount"%d, 0)), self.DAMAGE_TYPES))
volley *= self.amountActive
volley *= self.getModifiedItemAttr("damageMultiplier") or 1
self.__dps = volley / (cycleTime / 1000.0)
else:

View File

@@ -99,6 +99,17 @@ class Fit(object):
self.extraAttributes.original = self.EXTRA_ATTRIBUTES
self.ship = Ship(db.getItem(self.shipID)) if self.shipID is not None else None
@property
def targetResists(self):
return self.__targetResists
@targetResists.setter
def targetResists(self, targetResists):
self.__targetResists = targetResists
self.__weaponDPS = None
self.__weaponVolley = None
self.__droneDPS = None
@property
def damagePattern(self):
return self.__damagePattern
@@ -811,12 +822,12 @@ class Fit(object):
weaponVolley = 0
for mod in self.modules:
dps, volley = mod.damageStats
dps, volley = mod.damageStats(self.targetResists)
weaponDPS += dps
weaponVolley += volley
for drone in self.drones:
droneDPS += drone.dps
droneDPS += drone.damageStats(self.targetResists)
self.__weaponDPS = weaponDPS
self.__weaponVolley = weaponVolley
@@ -838,6 +849,7 @@ class Fit(object):
copy.ship = deepcopy(self.ship, memo)
copy.name = "%s copy" % self.name
copy.damagePattern = self.damagePattern
copy.targetResists = self.targetResists
toCopy = ("modules", "drones", "cargo", "implants", "boosters", "projectedModules", "projectedDrones")
for name in toCopy:

View File

@@ -44,7 +44,7 @@ class Hardpoint(Enum):
class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
"""An instance of this class represents a module together with its charge and modified attributes"""
DAMAGE_ATTRIBUTES = ("emDamage", "kineticDamage", "explosiveDamage", "thermalDamage")
DAMAGE_TYPES = ("em", "thermal", "kinetic", "explosive")
MINING_ATTRIBUTES = ("miningAmount", )
def __init__(self, item):
@@ -308,29 +308,23 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.__itemModifiedAttributes.clear()
@property
def damageStats(self):
def damageStats(self, targetResists):
if self.__dps == None:
if self.isEmpty:
self.__dps = 0
self.__volley = 0
else:
if self.state >= State.ACTIVE:
if self.charge:
volley = sum(map(lambda attr: self.getModifiedChargeAttr(attr) or 0, self.DAMAGE_ATTRIBUTES))
else:
volley = sum(map(lambda attr: self.getModifiedItemAttr(attr) or 0, self.DAMAGE_ATTRIBUTES))
volley *= self.getModifiedItemAttr("damageMultiplier") or 1
if volley:
cycleTime = self.cycleTime
self.__volley = volley
self.__dps = volley / (cycleTime / 1000.0)
else:
self.__volley = 0
self.__dps = 0
self.__dps = 0
self.__volley = 0
if not self.isEmpty and self.state >= State.ACTIVE:
if self.charge:
func = self.getModifiedChargeAttr
else:
self.__volley = 0
self.__dps = 0
func = self.getModifiedItemAttr
volley = sum(map(lambda attr: (func("%sDamage"%attr) or 0) * (1-getattr(targetResists, "%sAmount"%attr, 0)), self.DAMAGE_TYPES))
volley *= self.getModifiedItemAttr("damageMultiplier") or 1
if volley:
cycleTime = self.cycleTime
self.__volley = volley
self.__dps = volley / (cycleTime / 1000.0)
return self.__dps, self.__volley
@@ -354,11 +348,11 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
@property
def dps(self):
return self.damageStats[0]
return self.damageStats(None)[0]
@property
def volley(self):
return self.damageStats[1]
return self.damageStats(None)[1]
@property
def reloadTime(self):

View File

@@ -0,0 +1,84 @@
#===============================================================================
# Copyright (C) 2014 Ryan Holmes
#
# 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 <http://www.gnu.org/licenses/>.
#===============================================================================
import re
class TargetResists(object):
# also determined import/export order - VERY IMPORTANT
DAMAGE_TYPES = ("em", "thermal", "kinetic", "explosive")
def __init__(self, emAmount = 0, thermalAmount = 0, kineticAmount = 0, explosiveAmount = 0):
self.emAmount = emAmount
self.thermalAmount = thermalAmount
self.kineticAmount = kineticAmount
self.explosiveAmount = explosiveAmount
@classmethod
def importPatterns(cls, text):
lines = re.split('[\n\r]+', text)
patterns = []
numPatterns = 0
for line in lines:
try:
if line.strip()[0] == "#": # comments
continue
line = line.split('#',1)[0] # allows for comments
type, data = line.rsplit('=',1)
type, data = type.strip(), data.split(',')
except:
# Data isn't in correct format, continue to next line
continue
if type != "TargetResists":
continue
numPatterns += 1
name, data = data[0], data[1:5]
fields = {}
for index, val in enumerate(data):
val = float(val)
try:
assert 0 <= val <= 100
fields["%sAmount" % cls.DAMAGE_TYPES[index]] = val/100
except:
continue
if len(fields) == 4: # Avoid possible blank lines
pattern = TargetResists(**fields)
pattern.name = name.strip()
patterns.append(pattern)
return patterns, numPatterns
EXPORT_FORMAT = "TargetResists = %s,%.1f,%.1f,%.1f,%.1f\n"
@classmethod
def exportPatterns(cls, *patterns):
out = "# Exported from pyfa\n#\n"
out += "# Values are in following format:\n"
out += "# TargetResists = [name],[EM %],[Thermal %],[Kinetic %],[Explosive %]\n\n"
for dp in patterns:
out += cls.EXPORT_FORMAT % (dp.name, dp.emAmount*100, dp.thermalAmount*100, dp.kineticAmount*100, dp.explosiveAmount*100)
return out.strip()
def __deepcopy__(self, memo):
p = TargetResists(self.emAmount, self.thermalAmount, self.kineticAmount, self.explosiveAmount)
p.name = "%s copy" % self.name
return p

View File

@@ -22,6 +22,7 @@ MetaGroup, AttributeInfo, Unit, EffectInfo, MetaType, MetaData, Traits
from eos.saveddata.price import Price
from eos.saveddata.user import User
from eos.saveddata.damagePattern import DamagePattern
from eos.saveddata.targetResists import TargetResists
from eos.saveddata.character import Character, Skill
from eos.saveddata.module import Module, State, Slot, Hardpoint, Rack
from eos.saveddata.drone import Drone