Files
pyfa/eos/modifiedAttributeDict.py
DarkPhoenix 15087a290a Add proper support of attribute value limit
This fixes issue with blighted/polarized weapons getting resistances down to negative values
2014-11-07 12:49:34 +03:00

286 lines
12 KiB
Python

#===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# 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 math import exp
import collections
defaultValuesCache = {}
cappingAttrKeyCache = {}
class ItemAttrShortcut(object):
def getModifiedItemAttr(self, key):
if key in self.itemModifiedAttributes:
return self.itemModifiedAttributes[key]
else:
return None
class ChargeAttrShortcut(object):
def getModifiedChargeAttr(self, key):
if key in self.chargeModifiedAttributes:
return self.chargeModifiedAttributes[key]
else:
return None
class ModifiedAttributeDict(collections.MutableMapping):
class CalculationPlaceholder():
pass
def __init__(self, fit = None):
self.fit = fit
# Stores original values of the entity
self.__original = None
# Modified values during calculations
self.__intermediary = {}
# Final modified values
self.__modified = {}
# Affected by entities
self.__affectedBy = {}
# Dictionaries for various value modification types
self.__forced = {}
self.__preAssigns = {}
self.__preIncreases = {}
self.__multipliers = {}
self.__penalizedMultipliers = {}
self.__postIncreases = {}
def clear(self):
self.__intermediary.clear()
self.__modified.clear()
self.__affectedBy.clear()
self.__forced.clear()
self.__preAssigns.clear()
self.__preIncreases.clear()
self.__multipliers.clear()
self.__penalizedMultipliers.clear()
self.__postIncreases.clear()
@property
def original(self):
return self.__original
@original.setter
def original(self, val):
self.__original = val
self.__modified.clear()
def __getitem__(self, key):
# Check if we have final calculated value
if key in self.__modified:
if self.__modified[key] == self.CalculationPlaceholder:
self.__modified[key] = self.__calculateValue(key)
return self.__modified[key]
# Then in values which are not yet calculated
elif key in self.__intermediary:
return self.__intermediary[key]
# Original value is the least priority
else:
return self.getOriginal(key)
def __delitem__(self, key):
if key in self.__modified:
del self.__modified[key]
if key in self.__intermediary:
del self.__intermediary[key]
def getOriginal(self, key):
val = self.__original.get(key)
if val is None:
return None
return val.value if hasattr(val, "value") else val
def __setitem__(self, key, val):
self.__intermediary[key] = val
def __iter__(self):
all = dict(self.__original, **self.__modified)
return (key for key in all)
def __contains__(self, key):
return (self.__original is not None and key in self.__original) or key in self.__modified or key in self.__intermediary
def __placehold(self, key):
"""Create calculation placeholder in item's modified attribute dict"""
self.__modified[key] = self.CalculationPlaceholder
def __len__(self):
keys = set()
keys.update(self.__original.iterkeys())
keys.update(self.__modified.iterkeys())
keys.update(self.__intermediary.iterkeys())
return len(keys)
def __calculateValue(self, key):
# It's possible that various attributes are capped by other attributes,
# it's defined by reference maxAttributeID
try:
cappingKey = cappingAttrKeyCache[key]
except KeyError:
from eos.db.gamedata.queries import getAttributeInfo
attrInfo = getAttributeInfo(key)
if attrInfo is None:
cappingId = cappingAttrKeyCache[key] = None
else:
cappingId = cappingAttrKeyCache[key] = attrInfo.maxAttributeID
if cappingId is None:
cappingKey = None
else:
cappingAttrInfo = getAttributeInfo(cappingId)
cappingKey = None if cappingAttrInfo is None else cappingAttrInfo.name
cappingValue = self.__calculateValue(cappingKey) if cappingKey is not None else None
# If value is forced, we don't have to calculate anything,
# just return forced value instead
force = self.__forced[key] if key in self.__forced else None
if force is not None:
if cappingValue is not None:
force = min(force, cappingValue)
return force
# Grab our values if they're there, otherwise we'll take default values
preIncrease = self.__preIncreases[key] if key in self.__preIncreases else 0
multiplier = self.__multipliers[key] if key in self.__multipliers else 1
penalizedMultiplierGroups = self.__penalizedMultipliers[key] if key in self.__penalizedMultipliers else {}
postIncrease = self.__postIncreases[key] if key in self.__postIncreases else 0
# Grab initial value, priorities are:
# Results of ongoing calculation > preAssign > original > 0
try:
default = defaultValuesCache[key]
except KeyError:
from eos.db.gamedata.queries import getAttributeInfo
attrInfo = getAttributeInfo(key)
if attrInfo is None:
default = defaultValuesCache[key] = 0.0
else:
dv = attrInfo.defaultValue
default = defaultValuesCache[key] = dv if dv is not None else 0.0
val = self.__intermediary[key] if key in self.__intermediary else self.__preAssigns[key] if key in self.__preAssigns else self.getOriginal(key) if key in self.__original else default
# We'll do stuff in the following order:
# preIncrease > multiplier > stacking penalized multipliers > postIncrease
val += preIncrease
val *= multiplier
# Each group is penalized independently
# Things in different groups will not be stack penalized between each other
for penalizedMultipliers in penalizedMultiplierGroups.itervalues():
# A quick explanation of how this works:
# 1: Bonuses and penalties are calculated seperately, so we'll have to filter each of them
l1 = filter(lambda val: val > 1, penalizedMultipliers)
l2 = filter(lambda val: val < 1, penalizedMultipliers)
# 2: The most significant bonuses take the smallest penalty,
# This means we'll have to sort
abssort = lambda val: -abs(val - 1)
l1.sort(key=abssort)
l2.sort(key=abssort)
# 3: The first module doesn't get penalized at all
# Any module after the first takes penalties according to:
# 1 + (multiplier - 1) * math.exp(- math.pow(i, 2) / 7.1289)
for l in (l1, l2):
for i in xrange(len(l)):
bonus = l[i]
val *= 1 + (bonus - 1) * exp(- i ** 2 / 7.1289)
val += postIncrease
# Cap value if we have cap defined
if cappingValue is not None:
val = min(val, cappingValue)
return val
def getAfflictions(self, key):
return self.__affectedBy[key] if key in self.__affectedBy else {}
def iterAfflictions(self):
return self.__affectedBy.__iter__()
def __afflict(self, attributeName, operation, bonus, used=True):
"""Add modifier to list of things affecting current item"""
# Do nothing if no fit is assigned
if self.fit is None:
return
# Create dictionary for given attribute and give it alias
if attributeName not in self.__affectedBy:
self.__affectedBy[attributeName] = {}
affs = self.__affectedBy[attributeName]
# If there's no set for current fit in dictionary, create it
if self.fit not in affs:
affs[self.fit] = []
# Reassign alias to list
affs = affs[self.fit]
# Get modifier which helps to compose 'Affected by' map
modifier = self.fit.getModifier()
# Add current affliction to list
affs.append((modifier, operation, bonus, used))
def preAssign(self, attributeName, value):
"""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))
def increase(self, attributeName, increase, position="pre"):
"""Increase value of given attribute by given number"""
# Increases applied before multiplications and after them are
# written in separate maps
if position == "pre":
tbl = self.__preIncreases
elif position == "post":
tbl = self.__postIncreases
else:
raise ValueError("position should be either pre or post")
if not attributeName in tbl:
tbl[attributeName] = 0
tbl[attributeName] += increase
self.__placehold(attributeName)
self.__afflict(attributeName, "+", increase, increase != 0)
def multiply(self, attributeName, multiplier, stackingPenalties=False, penaltyGroup="default"):
"""Multiply value of given attribute by given factor"""
# If we're asked to do stacking penalized multiplication, append values
# to per penalty group lists
if stackingPenalties:
if not attributeName in self.__penalizedMultipliers:
self.__penalizedMultipliers[attributeName] = {}
if not penaltyGroup in self.__penalizedMultipliers[attributeName]:
self.__penalizedMultipliers[attributeName][penaltyGroup] = []
tbl = self.__penalizedMultipliers[attributeName][penaltyGroup]
tbl.append(multiplier)
# Non-penalized multiplication factors go to the single list
else:
if not attributeName in self.__multipliers:
self.__multipliers[attributeName] = 1
self.__multipliers[attributeName] *= multiplier
self.__placehold(attributeName)
self.__afflict(attributeName, "%s*" % ("s" if stackingPenalties else ""), multiplier, multiplier != 1)
def boost(self, attributeName, boostFactor, *args, **kwargs):
"""Boost value by some percentage"""
# We just transform percentage boost into multiplication factor
self.multiply(attributeName, 1 + boostFactor / 100.0, *args, **kwargs)
def force(self, attributeName, value):
"""Force value to attribute and prohibit any changes to it"""
self.__forced[attributeName] = value
self.__placehold(attributeName)
self.__afflict(attributeName, u"\u2263", value)
class Affliction():
def __init__(self, type, amount):
self.type = type
self.amount = amount