class SlotIndexRegister(RestrictionRegister): """ Class which implements common functionality for all registers, which track indices of occupied slots and disallow multiple holders reside within slot with the same index. """ def __init__(self, slotIndexAttr, restrictionType): # This attribute's value on holder # represents their index of slot self.__slotIndexAttr = slotIndexAttr self.__restrictionType = restrictionType # All holders which possess index of slot # are stored in this container # Format: {slot index: {holders}} self.__slottedHolders = KeyedSet() def registerHolder(self, holder): # Skip items which don't have index specifier try: slotIndex = holder.item.attributes[self.__slotIndexAttr] except KeyError: return self.__slottedHolders.addData(slotIndex, holder) def unregisterHolder(self, holder): try: slotIndex = holder.item.attributes[self.__slotIndexAttr] except KeyError: return self.__slottedHolders.rmData(slotIndex, holder) def validate(self): taintedHolders = {} for slotIndex in self.__slottedHolders: slotIndexHolders = self.__slottedHolders[slotIndex] # If more than one item occupies the same slot, all # holders in this slot are tainted if len(slotIndexHolders) > 1: for holder in slotIndexHolders: taintedHolders[holder] = SlotIndexErrorData(holderSlotIndex=slotIndex) if taintedHolders: raise RegisterValidationError(taintedHolders) @property def restrictionType(self): return self.__restrictionType
class SlotIndexRegister(RestrictionRegister): """ Class which implements common functionality for all registers, which track indices of occupied slots and disallow multiple holders reside within slot with the same index. """ def __init__(self, slotIndexAttr, restrictionType): # This attribute's value on holder # represents their index of slot self.__slotIndexAttr = slotIndexAttr self.__restrictionType = restrictionType # All holders which possess index of slot # are stored in this container # Format: {slot index: {holders}} self.__slottedHolders = KeyedSet() def registerHolder(self, holder): # Skip items which don't have index specifier try: slotIndex = holder.item.attributes[self.__slotIndexAttr] except KeyError: return self.__slottedHolders.addData(slotIndex, holder) def unregisterHolder(self, holder): try: slotIndex = holder.item.attributes[self.__slotIndexAttr] except KeyError: return self.__slottedHolders.rmData(slotIndex, holder) def validate(self): taintedHolders = {} for slotIndex in self.__slottedHolders: slotIndexHolders = self.__slottedHolders[slotIndex] # If more than one item occupies the same slot, all # holders in this slot are tainted if len(slotIndexHolders) > 1: for holder in slotIndexHolders: taintedHolders[holder] = SlotIndexErrorData( holderSlotIndex=slotIndex) if taintedHolders: raise RegisterValidationError(taintedHolders) @property def restrictionType(self): return self.__restrictionType
class SkillUniquenessRegister(RestrictionRegister): """ Implements restriction: Fit can't have more than one skill based on the same type. Details: Only holders having level attribute and item typeID other than None are tracked. """ def __init__(self): # Container for skill holders # Format: {holder id: {holders}} self.__skillHolders = KeyedSet() def registerHolder(self, holder): # Only holders which have level attribute are tracked as skills if hasattr(holder, "level") is True and holder.item.id is not None: self.__skillHolders.addData(holder.item.id, holder) def unregisterHolder(self, holder): self.__skillHolders.rmData(holder.item.id, holder) def validate(self): taintedHolders = {} # Go through all skill IDs for skillId in self.__skillHolders: skillHolders = self.__skillHolders[skillId] # If there's at least two skills with the same ID, # taint these holders if len(skillHolders) > 1: for holder in skillHolders: taintedHolders[holder] = SkillUniquenessErrorData(skill=skillId) if taintedHolders: raise RegisterValidationError(taintedHolders) @property def restrictionType(self): return Restriction.skillUniqueness
class SkillUniquenessRegister(RestrictionRegister): """ Implements restriction: Fit can't have more than one skill based on the same type. Details: Only holders having level attribute and item typeID other than None are tracked. """ def __init__(self): # Container for skill holders # Format: {holder id: {holders}} self.__skillHolders = KeyedSet() def registerHolder(self, holder): # Only holders which have level attribute are tracked as skills if hasattr(holder, 'level') is True and holder.item.id is not None: self.__skillHolders.addData(holder.item.id, holder) def unregisterHolder(self, holder): self.__skillHolders.rmData(holder.item.id, holder) def validate(self): taintedHolders = {} # Go through all skill IDs for skillId in self.__skillHolders: skillHolders = self.__skillHolders[skillId] # If there's at least two skills with the same ID, # taint these holders if len(skillHolders) > 1: for holder in skillHolders: taintedHolders[holder] = SkillUniquenessErrorData(skill=skillId) if taintedHolders: raise RegisterValidationError(taintedHolders) @property def restrictionType(self): return Restriction.skillUniqueness
class MaxGroupRegister(RestrictionRegister): """ Class which implements common functionality for all registers, which track maximum number of belonging to ship holders in certain state on per-group basis. """ def __init__(self, maxGroupAttr, restrictionType): # Attribute ID whose value contains group restriction # of holder self.__maxGroupAttr = maxGroupAttr self.__restrictionType = restrictionType # Container for all tracked holders, keyed # by their group ID # Format: {group ID: {holders}} self.__groupAll = KeyedSet() # Container for holders, which have max group # restriction to become operational # Format: {holders} self.__maxGroupRestricted = set() def registerHolder(self, holder): # Ignore holders which do not belong to ship if holder._location != Location.ship: return groupId = holder.item.groupId # Ignore holders, whose item isn't assigned # to any group if groupId is None: return # Having group ID is enough condition # to enter container of all fitted holders self.__groupAll.addData(groupId, holder) # To enter restriction container, original # item must have restriction attribute if self.__maxGroupAttr not in holder.item.attributes: return self.__maxGroupRestricted.add(holder) def unregisterHolder(self, holder): # Just clear data containers groupId = holder.item.groupId self.__groupAll.rmData(groupId, holder) self.__maxGroupRestricted.discard(holder) def validate(self): # Container for tainted holders taintedHolders = {} # Go through all restricted holders for holder in self.__maxGroupRestricted: # Get number of registered holders, assigned to group of current # restricted holder, and holder's restriction value groupId = holder.item.groupId groupHolders = len(self.__groupAll.get(groupId) or ()) maxGroupRestriction = holder.item.attributes[self.__maxGroupAttr] # If number of registered holders from this group is bigger, # then current holder is tainted if groupHolders > maxGroupRestriction: taintedHolders[holder] = MaxGroupErrorData(maxGroup=maxGroupRestriction, holderGroup=groupId, groupHolders=groupHolders) # Raise error if we detected any tainted holders if taintedHolders: raise RegisterValidationError(taintedHolders) @property def restrictionType(self): return self.__restrictionType
class MaxGroupRegister(RestrictionRegister): """ Class which implements common functionality for all registers, which track maximum number of belonging to ship holders in certain state on per-group basis. """ def __init__(self, maxGroupAttr, restrictionType): # Attribute ID whose value contains group restriction # of holder self.__maxGroupAttr = maxGroupAttr self.__restrictionType = restrictionType # Container for all tracked holders, keyed # by their group ID # Format: {group ID: {holders}} self.__groupAll = KeyedSet() # Container for holders, which have max group # restriction to become operational # Format: {holders} self.__maxGroupRestricted = set() def registerHolder(self, holder): # Ignore holders which do not belong to ship if holder._location != Location.ship: return groupId = holder.item.groupId # Ignore holders, whose item isn't assigned # to any group if groupId is None: return # Having group ID is enough condition # to enter container of all fitted holders self.__groupAll.addData(groupId, holder) # To enter restriction container, original # item must have restriction attribute if self.__maxGroupAttr not in holder.item.attributes: return self.__maxGroupRestricted.add(holder) def unregisterHolder(self, holder): # Just clear data containers groupId = holder.item.groupId self.__groupAll.rmData(groupId, holder) self.__maxGroupRestricted.discard(holder) def validate(self): # Container for tainted holders taintedHolders = {} # Go through all restricted holders for holder in self.__maxGroupRestricted: # Get number of registered holders, assigned to group of current # restricted holder, and holder's restriction value groupId = holder.item.groupId groupHolders = len(self.__groupAll.get(groupId) or ()) maxGroupRestriction = holder.item.attributes[self.__maxGroupAttr] # If number of registered holders from this group is bigger, # then current holder is tainted if groupHolders > maxGroupRestriction: taintedHolders[holder] = MaxGroupErrorData( maxGroup=maxGroupRestriction, holderGroup=groupId, groupHolders=groupHolders) # Raise error if we detected any tainted holders if taintedHolders: raise RegisterValidationError(taintedHolders) @property def restrictionType(self): return self.__restrictionType
class MutableAttributeMap: """ Calculate, store and provide access to modified attribute values. Positional arguments: holder -- holder, to which this map is assigned """ __slots__ = ("__holder", "__modifiedAttributes", "_capMap") def __init__(self, holder): # Reference to holder for internal needs self.__holder = holder # Actual container of calculated attributes # Format: {attribute ID: value} self.__modifiedAttributes = {} # This variable stores map of attributes which cap # something, and attributes capped by them. Initialized # to None to not waste memory, will be changed to dict # when needed. # Format {capping attribute ID: {capped attribute IDs}} self._capMap = None def __getitem__(self, attrId): # Special handling for skill level attribute if attrId == Attribute.skillLevel: # Attempt to return level attribute of holder try: val = self.__holder.level # Try regular way of getting attribute, if accessing # level attribute failed except AttributeError: pass else: return val # If carrier holder isn't assigned to any fit, then # we can use just item's original attributes if self.__holder.fit is None: val = self.__holder.item.attributes[attrId] return val # If value is stored, it's considered valid try: val = self.__modifiedAttributes[attrId] # Else, we have to run full calculation process except KeyError: try: val = self.__modifiedAttributes[attrId] = self.__calculate(attrId) except BaseValueError as e: msg = "unable to find base value for attribute {} on item {}".format(e.args[0], self.__holder.item.id) signature = (type(e), self.__holder.item.id, e.args[0]) self.__holder.fit._eos._logger.warning(msg, childName="attributeCalculator", signature=signature) raise KeyError(attrId) from e except AttributeMetaError as e: msg = "unable to fetch metadata for attribute {}, requested for item {}".format(e.args[0], self.__holder.item.id) signature = (type(e), self.__holder.item.id, e.args[0]) self.__holder.fit._eos._logger.error(msg, childName="attributeCalculator", signature=signature) raise KeyError(attrId) from e self.__holder.fit._linkTracker.clearHolderAttributeDependents(self.__holder, attrId) return val def __len__(self): return len(self.keys()) def __contains__(self, attrId): # Seek for attribute in both modified attribute container # and original item attributes result = attrId in self.__modifiedAttributes or attrId in self.__holder.item.attributes return result def __iter__(self): for k in self.keys(): yield k def __delitem__(self, attrId): # Clear the value in our calculated attributes dictionary try: del self.__modifiedAttributes[attrId] # Do nothing if it wasn't calculated except KeyError: pass # And make sure all other attributes relying on it # are cleared too else: self.__holder.fit._linkTracker.clearHolderAttributeDependents(self.__holder, attrId) def __setitem__(self, attrId, value): # Write value and clear all attributes relying on it self.__modifiedAttributes[attrId] = value self.__holder.fit._linkTracker.clearHolderAttributeDependents(self.__holder, attrId) def get(self, attrId, default=None): try: return self[attrId] except KeyError: return default def keys(self): # Return union of both keys which are already calculated in return self.__modifiedAttributes.keys() | self.__holder.item.attributes.keys() def clear(self): self.__modifiedAttributes.clear() def __calculate(self, attrId): """ Run calculations to find the actual value of attribute. Positional arguments: attrId -- ID of attribute to be calculated Return value: Calculated attribute value Possible exceptions: BaseValueError -- attribute cannot be calculated, as its base value is not available """ # Attribute object for attribute being calculated try: attrMeta = self.__holder.item._cacheHandler.getAttribute(attrId) # Raise error if we can't get to getAttribute method # or it can't find requested attribute except (AttributeError, AttributeFetchError) as e: raise AttributeMetaError(attrId) from e # Base attribute value which we'll use for modification try: result = self.__holder.item.attributes[attrId] # If attribute isn't available on base item, # base off its default value except KeyError: result = attrMeta.defaultValue # If original attribute is not specified and default # value isn't available, raise error - without valid # base we can't go on if result is None: raise BaseValueError(attrId) # Container for non-penalized modifiers # Format: {operator: [values]} normalMods = {} # Container for penalized modifiers # Format: {operator: [values]} penalizedMods = {} # Now, go through all affectors affecting our holder for affector in self.__holder.fit._linkTracker.getAffectors(self.__holder, attrId=attrId): try: sourceHolder, modifier = affector operator = modifier.operator # Decide if it should be stacking penalized or not, based on stackable property, # source item category and operator penalize = (attrMeta.stackable is False and not sourceHolder.item.categoryId in penaltyImmuneCategories and operator in penalizableOperators) try: modValue = sourceHolder.attributes[modifier.sourceAttributeId] # Silently skip current affector: error should already # be logged by map before it raised KeyError except KeyError: continue # Normalize operations to just three types: # assignments, additions, multiplications try: normalizationFunc = normalizationMap[operator] # Raise error on any unknown operator types except KeyError as e: raise OperatorError(operator) from e modValue = normalizationFunc(modValue) # Add value to appropriate dictionary if penalize is True: try: modList = penalizedMods[operator] except KeyError: modList = penalizedMods[operator] = [] else: try: modList = normalMods[operator] except KeyError: modList = normalMods[operator] = [] modList.append(modValue) # Handle operator type failure except OperatorError as e: msg = "malformed modifier on item {}: unknown operator {}".format(sourceHolder.item.id, e.args[0]) signature = (type(e), sourceHolder.item.id, e.args[0]) self.__holder.fit._eos._logger.warning(msg, childName="attributeCalculator", signature=signature) continue # When data gathering is complete, process penalized modifiers # They are penalized on per-operator basis for operator, modList in penalizedMods.items(): penalizedValue = self.__penalizeValues(modList) try: modList = normalMods[operator] except KeyError: modList = normalMods[operator] = [] modList.append(penalizedValue) # Calculate result of normal dictionary, according to operator order for operator in sorted(normalMods): modList = normalMods[operator] # Pick best modifier for assignments, based on highIsGood value if operator in assignments: result = max(modList) if attrMeta.highIsGood is True else min(modList) elif operator in additions: for modVal in modList: result += modVal elif operator in multiplications: for modVal in modList: result *= modVal # If attribute has upper cap, do not let # its value to grow above it if attrMeta.maxAttributeId is not None: try: maxValue = self[attrMeta.maxAttributeId] # If max value isn't available, don't # cap anything except KeyError: pass else: result = min(result, maxValue) # Let map know that capping attribute # restricts current attribute if self._capMap is None: self._capMap = KeyedSet() # Fill cap map with data: capping attribute and capped attribute self._capMap.addData(attrMeta.maxAttributeId, attrId) return result def __penalizeValues(self, modList): """ Calculate aggregated factor of passed factors, taking into consideration stacking penalty. Positional argument: modList -- list of factors Return value: Final aggregated factor of passed modList """ # Gather positive modifiers into one chain, negative # into another chainPositive = [] chainNegative = [] for modVal in modList: # Transform value into form of multiplier - 1 for ease of # stacking chain calculation modVal -= 1 if modVal >= 0: chainPositive.append(modVal) else: chainNegative.append(modVal) # Strongest modifiers always go first chainPositive.sort(reverse=True) chainNegative.sort() # Base final multiplier on 1 listResult = 1 for chain in (chainPositive, chainNegative): # Same for intermediate per-chain result chainResult = 1 for position, modifier in enumerate(chain): # Ignore 12th modifier and further as non-significant if position > 10: break # Apply stacking penalty based on modifier position chainResult *= 1 + modifier * penaltyBase ** (position ** 2) listResult *= chainResult return listResult
class SkillRequirementRegister(RestrictionRegister): """ Implements restriction: To use holder, all its skill requirements must be met. Details: Only holders having level attribute are tracked. Original item attributes are taken to determine skill and skill level requirements. If corresponding skill is found, but its skill level is None, check for holder is failed. """ def __init__(self): # Container for skill holders, for ease of # access # Format: {holder id: {holders}} self.__skillHolders = KeyedSet() # Set with holders which have any skill requirements # Format: {holders} self.__restrictedHolders = set() def registerHolder(self, holder): # Only holders which belong to character and have # level attribute are tracked as skills if hasattr(holder, "level") is True: self.__skillHolders.addData(holder.item.id, holder) # Holders which have any skill requirement are tracked if holder.item.requiredSkills: self.__restrictedHolders.add(holder) def unregisterHolder(self, holder): self.__skillHolders.rmData(holder.item.id, holder) self.__restrictedHolders.discard(holder) def validate(self): taintedHolders = {} # Go through restricted holders for holder in self.__restrictedHolders: # Container for skill requirement errors skillRequirementErrors = [] # Check each skill requirement for requiredSkillId in holder.item.requiredSkills: requiredSkillLevel = holder.item.requiredSkills[requiredSkillId] skillHolders = self.__skillHolders.get(requiredSkillId) or () # Pick max level of all skill holders, absence of skill # is considered as skill level set to None skillLevel = None for skillHolder in skillHolders: skillHolderLevel = skillHolder.level if skillLevel is None: skillLevel = skillHolderLevel elif skillHolderLevel is not None: skillLevel = max(skillLevel, skillHolderLevel) # Last check - if skill level is lower than expected, current holder # is tainted; mark it so and move to the next one if skillLevel is None or skillLevel < requiredSkillLevel: skillRequirementError = SkillRequirementErrorData(skill=requiredSkillId, level=skillLevel, requiredLevel=requiredSkillLevel) skillRequirementErrors.append(skillRequirementError) if skillRequirementErrors: taintedHolders[holder] = tuple(skillRequirementErrors) if taintedHolders: raise RegisterValidationError(taintedHolders) @property def restrictionType(self): return Restriction.skillRequirement
class SkillRequirementRegister(RestrictionRegister): """ Implements restriction: To use holder, all its skill requirements must be met. Details: Only holders having level attribute are tracked. Original item attributes are taken to determine skill and skill level requirements. If corresponding skill is found, but its skill level is None, check for holder is failed. """ def __init__(self): # Container for skill holders, for ease of # access # Format: {holder id: {holders}} self.__skillHolders = KeyedSet() # Set with holders which have any skill requirements # Format: {holders} self.__restrictedHolders = set() def registerHolder(self, holder): # Only holders which belong to character and have # level attribute are tracked as skills if hasattr(holder, 'level') is True: self.__skillHolders.addData(holder.item.id, holder) # Holders which have any skill requirement are tracked if holder.item.requiredSkills: self.__restrictedHolders.add(holder) def unregisterHolder(self, holder): self.__skillHolders.rmData(holder.item.id, holder) self.__restrictedHolders.discard(holder) def validate(self): taintedHolders = {} # Go through restricted holders for holder in self.__restrictedHolders: # Container for skill requirement errors skillRequirementErrors = [] # Check each skill requirement for requiredSkillId in holder.item.requiredSkills: requiredSkillLevel = holder.item.requiredSkills[ requiredSkillId] skillHolders = self.__skillHolders.get(requiredSkillId) or () # Pick max level of all skill holders, absence of skill # is considered as skill level set to None skillLevel = None for skillHolder in skillHolders: skillHolderLevel = skillHolder.level if skillLevel is None: skillLevel = skillHolderLevel elif skillHolderLevel is not None: skillLevel = max(skillLevel, skillHolderLevel) # Last check - if skill level is lower than expected, current holder # is tainted; mark it so and move to the next one if skillLevel is None or skillLevel < requiredSkillLevel: skillRequirementError = SkillRequirementErrorData( skill=requiredSkillId, level=skillLevel, requiredLevel=requiredSkillLevel) skillRequirementErrors.append(skillRequirementError) if skillRequirementErrors: taintedHolders[holder] = tuple(skillRequirementErrors) if taintedHolders: raise RegisterValidationError(taintedHolders) @property def restrictionType(self): return Restriction.skillRequirement
class MutableAttributeMap: """ Calculate, store and provide access to modified attribute values. Positional arguments: holder -- holder, to which this map is assigned """ __slots__ = ('__holder', '__modifiedAttributes', '_capMap') def __init__(self, holder): # Reference to holder for internal needs self.__holder = holder # Actual container of calculated attributes # Format: {attribute ID: value} self.__modifiedAttributes = {} # This variable stores map of attributes which cap # something, and attributes capped by them. Initialized # to None to not waste memory, will be changed to dict # when needed. # Format {capping attribute ID: {capped attribute IDs}} self._capMap = None def __getitem__(self, attrId): # Special handling for skill level attribute if attrId == Attribute.skillLevel: # Attempt to return level attribute of holder try: val = self.__holder.level # Try regular way of getting attribute, if accessing # level attribute failed except AttributeError: pass else: return val # If carrier holder isn't assigned to any fit, then # we can use just item's original attributes if self.__holder._fit is None: val = self.__holder.item.attributes[attrId] return val # If value is stored, it's considered valid try: val = self.__modifiedAttributes[attrId] # Else, we have to run full calculation process except KeyError: try: val = self.__modifiedAttributes[attrId] = self.__calculate( attrId) except BaseValueError as e: msg = 'unable to find base value for attribute {} on item {}'.format( e.args[0], self.__holder.item.id) signature = (type(e), self.__holder.item.id, e.args[0]) self.__holder._fit.eos._logger.warning( msg, childName='attributeCalculator', signature=signature) raise KeyError(attrId) from e except AttributeMetaError as e: msg = 'unable to fetch metadata for attribute {}, requested for item {}'.format( e.args[0], self.__holder.item.id) signature = (type(e), self.__holder.item.id, e.args[0]) self.__holder._fit.eos._logger.error( msg, childName='attributeCalculator', signature=signature) raise KeyError(attrId) from e self.__holder._fit._linkTracker.clearHolderAttributeDependents( self.__holder, attrId) return val def __len__(self): return len(self.keys()) def __contains__(self, attrId): # Seek for attribute in both modified attribute container # and original item attributes result = attrId in self.__modifiedAttributes or attrId in self.__holder.item.attributes return result def __iter__(self): for k in self.keys(): yield k def __delitem__(self, attrId): # Clear the value in our calculated attributes dictionary try: del self.__modifiedAttributes[attrId] # Do nothing if it wasn't calculated except KeyError: pass # And make sure all other attributes relying on it # are cleared too else: self.__holder._fit._linkTracker.clearHolderAttributeDependents( self.__holder, attrId) def __setitem__(self, attrId, value): # Write value and clear all attributes relying on it self.__modifiedAttributes[attrId] = value self.__holder._fit._linkTracker.clearHolderAttributeDependents( self.__holder, attrId) def get(self, attrId, default=None): try: return self[attrId] except KeyError: return default def keys(self): # Return union of both keys which are already calculated in return self.__modifiedAttributes.keys( ) | self.__holder.item.attributes.keys() def clear(self): """Reset map to its initial state.""" self.__modifiedAttributes.clear() self._capMap = None def __calculate(self, attrId): """ Run calculations to find the actual value of attribute. Positional arguments: attrId -- ID of attribute to be calculated Return value: Calculated attribute value Possible exceptions: BaseValueError -- attribute cannot be calculated, as its base value is not available """ # Attribute object for attribute being calculated try: attrMeta = self.__holder._fit.eos._cacheHandler.getAttribute( attrId) # Raise error if we can't get to getAttribute method # or it can't find requested attribute except (AttributeError, AttributeFetchError) as e: raise AttributeMetaError(attrId) from e # Base attribute value which we'll use for modification try: result = self.__holder.item.attributes[attrId] # If attribute isn't available on base item, # base off its default value except KeyError: result = attrMeta.defaultValue # If original attribute is not specified and default # value isn't available, raise error - without valid # base we can't go on if result is None: raise BaseValueError(attrId) # Container for non-penalized modifiers # Format: {operator: [values]} normalMods = {} # Container for penalized modifiers # Format: {operator: [values]} penalizedMods = {} # Now, go through all affectors affecting our holder for affector in self.__holder._fit._linkTracker.getAffectors( self.__holder, attrId=attrId): try: sourceHolder, modifier = affector operator = modifier.operator # Decide if it should be stacking penalized or not, based on stackable property, # source item category and operator penalize = (attrMeta.stackable is False and sourceHolder.item.categoryId not in penaltyImmuneCategories and operator in penalizableOperators) try: modValue = sourceHolder.attributes[ modifier.sourceAttributeId] # Silently skip current affector: error should already # be logged by map before it raised KeyError except KeyError: continue # Normalize operations to just three types: # assignments, additions, multiplications try: normalizationFunc = normalizationMap[operator] # Raise error on any unknown operator types except KeyError as e: raise OperatorError(operator) from e modValue = normalizationFunc(modValue) # Add value to appropriate dictionary if penalize is True: modList = penalizedMods.setdefault(operator, []) else: modList = normalMods.setdefault(operator, []) modList.append(modValue) # Handle operator type failure except OperatorError as e: msg = 'malformed modifier on item {}: unknown operator {}'.format( sourceHolder.item.id, e.args[0]) signature = (type(e), sourceHolder.item.id, e.args[0]) self.__holder._fit.eos._logger.warning( msg, childName='attributeCalculator', signature=signature) continue # When data gathering is complete, process penalized modifiers # They are penalized on per-operator basis for operator, modList in penalizedMods.items(): penalizedValue = self.__penalizeValues(modList) modList = normalMods.setdefault(operator, []) modList.append(penalizedValue) # Calculate result of normal dictionary, according to operator order for operator in sorted(normalMods): modList = normalMods[operator] # Pick best modifier for assignments, based on highIsGood value if operator in assignments: result = max(modList) if attrMeta.highIsGood is True else min( modList) elif operator in additions: for modVal in modList: result += modVal elif operator in multiplications: for modVal in modList: result *= modVal # If attribute has upper cap, do not let # its value to grow above it if attrMeta.maxAttributeId is not None: try: maxValue = self[attrMeta.maxAttributeId] # If max value isn't available, don't # cap anything except KeyError: pass else: result = min(result, maxValue) # Let map know that capping attribute # restricts current attribute if self._capMap is None: self._capMap = KeyedSet() # Fill cap map with data: capping attribute and capped attribute self._capMap.addData(attrMeta.maxAttributeId, attrId) return result def __penalizeValues(self, modList): """ Calculate aggregated factor of passed factors, taking into consideration stacking penalty. Positional argument: modList -- list of factors Return value: Final aggregated factor of passed modList """ # Gather positive modifiers into one chain, negative # into another chainPositive = [] chainNegative = [] for modVal in modList: # Transform value into form of multiplier - 1 for ease of # stacking chain calculation modVal -= 1 if modVal >= 0: chainPositive.append(modVal) else: chainNegative.append(modVal) # Strongest modifiers always go first chainPositive.sort(reverse=True) chainNegative.sort() # Base final multiplier on 1 listResult = 1 for chain in (chainPositive, chainNegative): # Same for intermediate per-chain result chainResult = 1 for position, modifier in enumerate(chain): # Ignore 12th modifier and further as non-significant if position > 10: break # Apply stacking penalty based on modifier position chainResult *= 1 + modifier * penaltyBase**(position**2) listResult *= chainResult return listResult