NovaBryan/EVE_Dump
Log | Files | 📦 下载最新源码 (.zip) | Refs | README

combattargets.py (39673B)


      1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\packages\behaviors\actions\combattargets.py
      2 import logging
      3 import random
      4 from collections import defaultdict
      5 import evetypes
      6 from ballpark.messenger.const import MESSAGE_TARGET_ADDED
      7 from behaviors.blackboards import BlackboardDeletedError
      8 from behaviors.const.combat import COMBAT_MAX_EFFECTIVE_FALLOFF_DISTANCE_FACTOR, COMBAT_MAX_EFFECTIVE_DISTANCE_FACTOR
      9 from behaviors.utility.ballparks import is_ball_in_park, get_slim_item, is_ball_in_range, is_invulnerable, get_ball_type_id
     10 from behaviors.utility.ballparks import get_specific_globals
     11 from behaviors.utility.ballparks import is_target_valid, is_ball_cloaked, get_distance_between
     12 from behaviors.utility.dogmatic import get_default_effect_id_for_type, get_my_attribute_value, type_has_effect
     13 from behaviors.utility.dogmatic import get_entity_max_missile_range
     14 from behaviors.utility.dogmatic import remove_my_target
     15 from behaviors.utility.dogmatic import get_my_locked_targets, try_stop_effect
     16 from behaviors.utility.inventory import get_inventory_item
     17 from behaviors.utility.math import get_normalized_dict
     18 from behaviors.utility.standings import is_hostile_towards
     19 from dogma.const import attributeFalloff
     20 from dogma.const import effectTargetAttack
     21 from dogma.const import attributeMaxRange, effectMissileLaunchingForEntity
     22 from dogma.effects.restricted.util import get_effect_range_with_falloff_multiplier
     23 from dogma.effects.restricted.util import get_effect_range_and_falloff_values_for_item
     24 from inventorycommon.const import categoryShip, categoryStructure
     25 import uthread2
     26 from behaviors.const.behaviorroles import COMBAT_DISTRIBUTE_EFFECTS_BY_ROLE, COMBAT_IGNORE_PRIMARY_BY_ROLE
     27 from behaviors.const.blackboardchannels import MY_COMBAT_TARGET
     28 from behaviors.groups.mixin import GroupTaskMixin
     29 from behaviors.targetEvaluation import targetEvaluationRegistry, ThreatTargetEvaluator
     30 from behaviors.tasks import Task
     31 from behaviors.trees.role import ROLE_ADDRESS
     32 from eve.common.script.sys import idCheckers
     33 from evemath import weighted_choice
     34 from ccpProfile import TimedFunction
     35 from eveexceptions import UserError
     36 logger = logging.getLogger(__name__)
     37 
     38 class FindTarget(Task, GroupTaskMixin):
     39 
     40     def __init__(self, attributes = None):
     41         super(FindTarget, self).__init__(attributes)
     42         self.filterFunctions = None
     43 
     44     @TimedFunction('behaviors::actions::combattargets::FindTarget::OnEnter')
     45     def OnEnter(self):
     46         self.SetTargetEvaluator()
     47         targetList = self._GetPotentialTargets()
     48         selectedTargetId = self.GetLastBlackboardValue(self.attributes.selectedTargetAddress)
     49         self.filterFunctions = self._GetFilterFunctions()
     50         targetList = self.FilterValidTargets(targetList)
     51         if self.ShouldPickNewTarget(selectedTargetId, targetList):
     52             pickedTarget = self.PickTarget(targetList)
     53             self._update_picked_target(pickedTarget)
     54             self.SendBlackboardValue(self.attributes.selectedTargetAddress, pickedTarget)
     55             self.SetStatusToSuccess()
     56         else:
     57             if selectedTargetId is not None:
     58                 self.SendBlackboardValue(self.attributes.selectedTargetAddress, None)
     59             self.SetStatusToFailed()
     60 
     61     def SetTargetEvaluator(self):
     62         if not self.HasContextValue('targetEvaluator'):
     63             evaluatorClass = self._GetTargetEvaluatorClass()
     64             targetEvaluator = evaluatorClass(self.context, self.attributes)
     65             self.SetContextValue('targetEvaluator', targetEvaluator)
     66 
     67     def _GetTargetEvaluatorClass(self):
     68         return targetEvaluationRegistry[self.attributes.targetEvaluationFunction]
     69 
     70     def _GetPotentialTargets(self):
     71         return self.GetLastBlackboardValue(self.attributes.potentialTargetListAddress)
     72 
     73     def _GetFilterFunctions(self):
     74         return [self._GetFilterBubbleFunction(),
     75          self._FilterTargetCategory,
     76          self._FilterCloaked,
     77          self._FilterProtected,
     78          self._FilterMaxRangeFromGuardObject]
     79 
     80     def PickTarget(self, targetList):
     81         evaluateTarget = self.GetTargetEvaluator().EvaluateTarget
     82         sortedList = sorted(targetList, key=evaluateTarget)
     83         return sortedList[-1]
     84 
     85     def GetTargetEvaluator(self):
     86         return self.GetContextValue('targetEvaluator')
     87 
     88     @TimedFunction('behaviors::actions::combattargets::FindTarget::FilterValidTargets')
     89     def FilterValidTargets(self, targetList):
     90         if targetList is None:
     91             return []
     92         validTargets = self._GetValidTargets(targetList)
     93         validTargets = self._filter_valid_targets_for_fleet_spread(validTargets)
     94         validTargets = self._FilterValidTargetsForBehaviorRole(validTargets)
     95         return validTargets
     96 
     97     @TimedFunction('behaviors::actions::combattargets::FindTarget::_GetValidTargets')
     98     def _GetValidTargets(self, targetList):
     99         validTargets = []
    100         for targetId in targetList:
    101             if self._IsTargetValid(targetId):
    102                 validTargets.append(targetId)
    103 
    104         return validTargets
    105 
    106     def _GetBubbleIdAndBubbleCheckRestriction(self):
    107         bubbleId = None
    108         if getattr(self.attributes, 'bubbleIdAddress', None) is not None:
    109             bubbleId = self.GetLastBlackboardValue(self.attributes.bubbleIdAddress)
    110             onlyCheckLocalBubble = True
    111         else:
    112             onlyCheckLocalBubble = self.attributes.onlyCheckLocalBubble
    113             if onlyCheckLocalBubble:
    114                 bubbleId = self.context.myBall.newBubbleId
    115         return (bubbleId, onlyCheckLocalBubble)
    116 
    117     def _IsTargetValid(self, targetId):
    118         ballpark = self.context.ballpark
    119         ball = ballpark.balls.get(targetId)
    120         if ball is None:
    121             return False
    122         return all((filterfunction(ballpark, ball) for filterfunction in self.filterFunctions))
    123 
    124     def _GetFilterBubbleFunction(self):
    125         bubbleId, onlyCheckLocalBubble = self._GetBubbleIdAndBubbleCheckRestriction()
    126 
    127         def _FilterBubble(_, ball):
    128             if onlyCheckLocalBubble and ball.newBubbleId != bubbleId:
    129                 return False
    130             return True
    131 
    132         return _FilterBubble
    133 
    134     def _FilterTargetCategory(self, ballpark, ball):
    135         if self.attributes.includedCategories:
    136             slimItem = ballpark.slims.get(ball.id)
    137             if slimItem is None:
    138                 return False
    139             if slimItem.categoryID not in self.attributes.includedCategories:
    140                 return False
    141         return True
    142 
    143     def _FilterCloaked(self, ballpark, ball):
    144         if ball.isCloaked:
    145             return False
    146         return not ballpark.HasPendingCloak(ball.id)
    147 
    148     def _FilterProtected(self, ballpark, ball):
    149         return not ballpark.IsBallProtected(ball.id)
    150 
    151     def _FilterMaxRangeFromGuardObject(self, ballpark, ball):
    152         if not self.HasAttribute('guardObjectIdAddress'):
    153             return True
    154         guardObjectMaxRange = getattr(self.attributes, 'maxDistanceFromGuardObject', None)
    155         if guardObjectMaxRange is not None:
    156             guardObjectId = self.GetLastBlackboardValue(self.attributes.guardObjectIdAddress)
    157             distanceBetween = get_distance_between(self, ball.id, guardObjectId)
    158             if distanceBetween is not None and distanceBetween >= guardObjectMaxRange:
    159                 return False
    160         return True
    161 
    162     def ShouldPickNewTarget(self, selectedTarget, targetList):
    163         if not targetList:
    164             shouldPickNewTarget = False
    165         elif selectedTarget is not None and selectedTarget in targetList:
    166             shouldPickNewTarget = False
    167         else:
    168             shouldPickNewTarget = True
    169         return shouldPickNewTarget
    170 
    171     @TimedFunction('behaviors::actions::combattargets::FindTarget::_FilterValidTargetsForBehaviorRole')
    172     def _FilterValidTargetsForBehaviorRole(self, validTargets):
    173         myRoleAddress = getattr(self.attributes, 'roleAddress', None)
    174         if not myRoleAddress:
    175             return validTargets
    176         myRole = self.GetLastBlackboardValue(self.attributes.roleAddress)
    177         if not myRole:
    178             return validTargets
    179         validTargets = self._FilterPrimary(myRole, validTargets)
    180         validTargets = self._FilterValidTargetsForEffectDistribution(myRole, validTargets)
    181         return validTargets
    182 
    183     @TimedFunction('behaviors::actions::combattargets::FindTarget::_FilterPrimary')
    184     def _FilterPrimary(self, myRole, validTargets):
    185         if len(validTargets) <= 1:
    186             return validTargets
    187         if self._ShouldIgnorePrimary(myRole):
    188             if getattr(self.attributes, 'primaryTargetIdAddress', None):
    189                 primaryTargetId = self.GetLastBlackboardValue(self.attributes.primaryTargetIdAddress)
    190                 if primaryTargetId and primaryTargetId in validTargets:
    191                     validTargets.remove(primaryTargetId)
    192         return validTargets
    193 
    194     def _ShouldIgnorePrimary(self, myRole):
    195         return myRole in COMBAT_IGNORE_PRIMARY_BY_ROLE
    196 
    197     @TimedFunction('behaviors::actions::combattargets::FindTarget::_FilterValidTargetsForEffectDistribution')
    198     def _FilterValidTargetsForEffectDistribution(self, myRole, validTargets):
    199         if not self._ShouldDistributeEffects(myRole):
    200             return validTargets
    201         alreadyEffectedTargets = self._GetAllTargetsForGroupMembersWithRole(myRole)
    202         availableTargets = list(set(validTargets) - alreadyEffectedTargets)
    203         return availableTargets or validTargets
    204 
    205     def _ShouldDistributeEffects(self, myRole):
    206         return myRole in COMBAT_DISTRIBUTE_EFFECTS_BY_ROLE
    207 
    208     def _GetAllTargetsForGroupMembersWithRole(self, role):
    209         memberIds = self.GetMemberIdList()
    210         alreadyEffectedTargets = set()
    211         for groupMemberId in memberIds:
    212             if groupMemberId == self.context.myItemId:
    213                 continue
    214             targetId = self._GetTargetIdForGroupMemberWithRole(groupMemberId, role)
    215             if targetId:
    216                 alreadyEffectedTargets.add(targetId)
    217 
    218         return alreadyEffectedTargets
    219 
    220     def _GetTargetIdForGroupMemberWithRole(self, groupMemberId, role):
    221         try:
    222             memberRole = self._GetChannelValueForGroupMemberId(groupMemberId, ROLE_ADDRESS[1])
    223             if memberRole == role:
    224                 return self._GetChannelValueForGroupMemberId(groupMemberId, MY_COMBAT_TARGET[1])
    225         except BlackboardDeletedError:
    226             return None
    227 
    228     def _GetChannelValueForGroupMemberId(self, groupMemberId, channelAddressName):
    229         return self.GetMessageChannelForItemId(groupMemberId, channelAddressName).GetLastMessageValue()
    230 
    231     @TimedFunction('behaviors::actions::combattargets::FindTarget::_filter_valid_targets_for_fleet_spread')
    232     def _filter_valid_targets_for_fleet_spread(self, valid_targets):
    233         if not self._should_spread_targets_in_fleet():
    234             return valid_targets
    235         available_targets = valid_targets[:]
    236         fleet_targets_by_group = self.GetLastBlackboardValue(self.attributes.fleetTargetsByGroupAddress) or {}
    237         for entity_group_id, fleet_target_id in fleet_targets_by_group.iteritems():
    238             if entity_group_id == self.context.myEntityGroupId:
    239                 continue
    240             if fleet_target_id in available_targets:
    241                 available_targets.remove(fleet_target_id)
    242 
    243         return available_targets or valid_targets
    244 
    245     def _should_spread_targets_in_fleet(self):
    246         return self.HasAttribute('shouldSpreadTargetsInFleet') and self.attributes.shouldSpreadTargetsInFleet is True and self.HasAttribute('fleetTargetsByGroupAddress')
    247 
    248     def _update_picked_target(self, picked_target_id):
    249         self.SendBlackboardValue(self.attributes.selectedTargetAddress, picked_target_id)
    250         if self.HasAttribute('fleetTargetsByGroupAddress'):
    251             fleet_targets_by_group = self.GetLastBlackboardValue(self.attributes.fleetTargetsByGroupAddress) or {}
    252             fleet_targets_by_group[self.context.myEntityGroupId] = picked_target_id
    253             self.SendBlackboardValue(self.attributes.fleetTargetsByGroupAddress, fleet_targets_by_group)
    254 
    255 
    256 class FindTargetForModule(FindTarget):
    257 
    258     def __init__(self, attributes = None):
    259         FindTarget.__init__(self, attributes)
    260 
    261     def _GetPotentialTargets(self):
    262         if self.HasAttribute('potentialTargetDictionaryAddress'):
    263             primary_targets_by_entity = self.GetLastBlackboardValue(self.attributes.potentialTargetDictionaryAddress)
    264             if primary_targets_by_entity:
    265                 return [ primary_target_id for primary_target_id in primary_targets_by_entity.itervalues() if primary_target_id is not None ]
    266         return super(FindTargetForModule, self)._GetPotentialTargets()
    267 
    268     def _GetFilterFunctions(self):
    269         return [self._GetFilterBubbleFunction(),
    270          self._FilterTargetCategory,
    271          self._FilterCloaked,
    272          self._FilterMaxRangeFromGuardObject,
    273          self._get_module_range_filter_function()]
    274 
    275     @TimedFunction('behaviors::actions::combattargets::FindTargetForModule::FilterValidTargets')
    276     def FilterValidTargets(self, targetList):
    277         if not targetList:
    278             return []
    279         validTargets = self._GetValidTargets(targetList)
    280         validTargets = self._FilterValidTargetsForBehaviorRole(validTargets)
    281         validTargets = self._filter_valid_targets_for_module_distribution(validTargets)
    282         return validTargets
    283 
    284     def _get_module_range_filter_function(self):
    285         module_item_id = self.GetLastBlackboardValue(self.attributes.moduleAddress)
    286         module_type_id = get_inventory_item(self, module_item_id).typeID
    287         effect_id = get_default_effect_id_for_type(self, module_type_id)
    288         minimum_range = get_effect_range_with_falloff_multiplier(self.context.dogmaLM, effect_id, module_item_id, COMBAT_MAX_EFFECTIVE_FALLOFF_DISTANCE_FACTOR)
    289 
    290         def _filter_module_range(_, ball):
    291             return is_ball_in_range(self, self.context.myBall.id, ball.id, minimum_range)
    292 
    293         return _filter_module_range
    294 
    295     @TimedFunction('behaviors::actions::combattargets::FindTargetForModule::_filter_valid_targets_for_module_distribution')
    296     def _filter_valid_targets_for_module_distribution(self, valid_targets):
    297         targets_already_affected = self._get_my_already_affected_targets(valid_targets)
    298         if not targets_already_affected:
    299             return valid_targets
    300         not_affected_targets = list(set(valid_targets).difference(targets_already_affected))
    301         if not_affected_targets:
    302             return not_affected_targets
    303         return [random.choice([min(targets_already_affected.keys(), key=targets_already_affected.get)])]
    304 
    305     def _get_my_already_affected_targets(self, valid_targets):
    306         targets_already_affected = defaultdict(int)
    307         non_stackable_module_list = self._get_my_non_stackable_module_list()
    308         if not non_stackable_module_list:
    309             return targets_already_affected
    310         my_targets_by_modules = self._get_my_targets_by_modules() or {}
    311         for module_id, target_id in my_targets_by_modules.iteritems():
    312             if target_id is None or target_id not in valid_targets:
    313                 continue
    314             module_type_id = get_inventory_item(self, module_id).typeID
    315             if module_type_id in non_stackable_module_list:
    316                 targets_already_affected[target_id] += 1
    317 
    318         return targets_already_affected
    319 
    320     def _get_my_non_stackable_module_list(self):
    321         eve_type_list_id = self.attributes.nonStackableEveTypeListId
    322         if not eve_type_list_id:
    323             return None
    324         return evetypes.GetTypeIDsByListID(eve_type_list_id)
    325 
    326     def _get_my_targets_by_modules(self):
    327         return self.GetLastBlackboardValue(self.attributes.targetsByModulesAddress)
    328 
    329 
    330 class AcquireTargetLock(Task):
    331 
    332     def __init__(self, *args):
    333         super(AcquireTargetLock, self).__init__(*args)
    334         self._targetingThread = None
    335 
    336     @TimedFunction('behaviors::actions::combattargets::AcquireTargetLock::OnEnter')
    337     def OnEnter(self):
    338         pass
    339 
    340     @TimedFunction('behaviors::actions::combattargets::AcquireTargetLock::Update')
    341     def Update(self):
    342         self._AttemptToAcquireTargetLock()
    343 
    344     def _AttemptToAcquireTargetLock(self):
    345         selectedTarget = self.GetLastBlackboardValue(self.attributes.selectedTargetAddress)
    346         if not self._IsTargetValid(selectedTarget):
    347             self.SetStatusToFailed()
    348         elif is_invulnerable(self, self.context.myItemId):
    349             self._SetStatusWhileLockPending()
    350         elif self.context.dogmaLM.IsTargeting(self.context.myItemId, selectedTarget):
    351             self.SetStatusToSuccess()
    352         elif self.context.dogmaLM.HasPendingTarget(self.context.myItemId, selectedTarget):
    353             if self._targetingThread is None:
    354                 self._SetStatusWhileLockPending()
    355             else:
    356                 self._SetStatusWhileWaitingForLock()
    357         else:
    358             self._SetStatusWhileWaitingForLock()
    359             self._targetingThread = uthread2.start_tasklet(self._TryTargetLock, selectedTarget)
    360 
    361     def _IsTargetValid(self, selectedTarget):
    362         if selectedTarget is None:
    363             return False
    364         if not is_ball_in_park(self, selectedTarget):
    365             return False
    366         if is_ball_cloaked(self, selectedTarget):
    367             return False
    368         return True
    369 
    370     def _SetStatusWhileWaitingForLock(self):
    371         self.SubscribeItem(self.context.myItemId, MESSAGE_TARGET_ADDED, self.OnTargetAdded)
    372         self.SetStatusToSuspended()
    373 
    374     def _SetStatusWhileLockPending(self):
    375         self.SubscribeItem(self.context.myItemId, MESSAGE_TARGET_ADDED, self.OnTargetAdded)
    376         self.SetStatusToRunning()
    377 
    378     @TimedFunction('behaviors::actions::combattargets::AcquireTargetLock::_TryTargetLock')
    379     def _TryTargetLock(self, selectedTarget):
    380         try:
    381             self._LockTarget(selectedTarget)
    382         except (UserError, RuntimeError) as e:
    383             logger.debug('unable to acquire target lock: %s', e)
    384         except TaskletExit:
    385             logger.debug('The target locking thread got killed prematurely.')
    386         finally:
    387             if self.IsSuspended():
    388                 self.SetStatusToRunning()
    389                 self.status.OnUpdated(self)
    390             self._targetingThread = None
    391 
    392     def _LockTarget(self, selectedTarget):
    393         self.context.dogmaLM.AddTargetEx(self.context.myItemId, selectedTarget)
    394 
    395     def OnTargetAdded(self, target_id):
    396         self.SetStatusToSuccess()
    397         self.status.OnUpdated(self)
    398 
    399     def CleanUp(self):
    400         if not self.IsInvalid():
    401             self.UnsubscribeItem(self.context.myItemId, MESSAGE_TARGET_ADDED, self.OnTargetAdded)
    402         super(AcquireTargetLock, self).CleanUp()
    403 
    404 
    405 class AcquireTargetLockNonBlocking(AcquireTargetLock):
    406 
    407     def _SetStatusWhileWaitingForLock(self):
    408         self.SetStatusToSuccess()
    409 
    410     def _SetStatusWhileLockPending(self):
    411         self.SetStatusToSuccess()
    412 
    413 
    414 class UnlockTarget(Task):
    415 
    416     @TimedFunction('behaviors::actions::combattargets::UnlockTarget::OnEnter')
    417     def OnEnter(self):
    418         targetList = self.GetTargetList()
    419         if targetList:
    420             targetId = targetList[0]
    421             self.UnlockTarget(targetId)
    422         self.SetStatusToSuccess()
    423 
    424     def GetTargetList(self):
    425         return self.context.dogmaLM.GetTargetsEx(self.context.myItemId)
    426 
    427     def UnlockTarget(self, targetId):
    428         self.context.dogmaLM.RemoveTarget(self.context.myItemId, targetId)
    429 
    430 
    431 class UnlockModuleTarget(UnlockTarget):
    432 
    433     @TimedFunction('behaviors::actions::combattargets::UnlockTarget::OnEnter')
    434     def OnEnter(self):
    435         target_list = self.GetTargetList()
    436         target_id = self._get_target_id()
    437         if target_list and target_id and target_id in target_list:
    438             self.UnlockTarget(target_id)
    439         self.SetStatusToSuccess()
    440 
    441     def _get_target_id(self):
    442         module_id = self.GetLastBlackboardValue(self.attributes.moduleAddress)
    443         targets_by_modules = self.GetLastBlackboardValue(self.attributes.targetsByModulesAddress)
    444         if targets_by_modules is None:
    445             return
    446         if module_id not in targets_by_modules:
    447             return
    448         target_id = targets_by_modules[module_id]
    449         if target_id is None or self._is_also_targeted_by_other_module(module_id, target_id, targets_by_modules):
    450             return
    451         return target_id
    452 
    453     def _is_also_targeted_by_other_module(self, module_id, target_id, targets_by_modules):
    454         for other_module_id, other_target_id in targets_by_modules.iteritems():
    455             if other_module_id == module_id:
    456                 continue
    457             if other_target_id == target_id:
    458                 return True
    459 
    460         return False
    461 
    462 
    463 class PruneNpcsFromTargetSet(Task, GroupTaskMixin):
    464 
    465     def OnEnter(self):
    466         self.SetStatusToSuccess()
    467         targetSet = self.GetLastBlackboardValue(self.attributes.targetSetAddress)
    468         if not targetSet:
    469             return
    470         removedSet = set()
    471         destinyBalls = self.context.ballpark.balls
    472         for targetId in targetSet:
    473             if idCheckers.IsFakeItem(targetId) and targetId not in destinyBalls:
    474                 removedSet.add(targetId)
    475 
    476         if removedSet:
    477             targetSet.difference_update(removedSet)
    478             self.SendBlackboardValue(self.attributes.targetSetAddress, targetSet)
    479 
    480 
    481 class ClearTargetSet(Task, GroupTaskMixin):
    482 
    483     def OnEnter(self):
    484         self.SetStatusToSuccess()
    485         targetSet = self.GetLastBlackboardValue(self.attributes.targetSetAddress)
    486         if not targetSet:
    487             return
    488         self._UpdateTargetSet(set())
    489         entityGroupMembers = self.GetMemberIdList()
    490         uthread2.start_tasklet(self._LogKilledAndFledTargets, targetSet, entityGroupMembers)
    491 
    492     def _LogKilledAndFledTargets(self, targetSet, entityGroupMembers):
    493         self.context.eventLogger.log_killed_and_fled_players(self.GetGroupOwnerId(), self.behaviorTree.GetBehaviorId(), targetSet, entityGroupMembers)
    494 
    495     def _UpdateTargetSet(self, targetSet):
    496         self.SendBlackboardValue(self.attributes.targetSetAddress, targetSet)
    497 
    498 
    499 class SelectPilotedShips(Task):
    500 
    501     @TimedFunction('behaviors::actions::combattargets::SelectPilotedShips::OnEnter')
    502     def OnEnter(self):
    503         self.SetStatusToSuccess()
    504         target_ids = self.GetLastBlackboardValue(self.attributes.sourceSetAddress)
    505         if not target_ids:
    506             return
    507         piloted_ship_set = self.get_piloted_ship_set(target_ids)
    508         self.SendBlackboardValue(self.attributes.targetSetAddress, piloted_ship_set)
    509 
    510     def get_piloted_ship_set(self, target_ids):
    511         result_set = set()
    512         for target_id in target_ids:
    513             if self.is_valid(target_id):
    514                 result_set.add(target_id)
    515 
    516         return result_set
    517 
    518     def is_valid(self, target_id):
    519         if not is_ball_in_park(self, target_id):
    520             return False
    521         slim_item = get_slim_item(self, target_id)
    522         if slim_item is None:
    523             return False
    524         if slim_item.categoryID != categoryShip:
    525             return False
    526         if not self.context.dogmaLM.IsShipPiloted(target_id):
    527             return False
    528         return True
    529 
    530 
    531 class GetMyMinOptimalAndFalloff(Task):
    532 
    533     @TimedFunction('behaviors::actions::combattargets::GetMyMinOptimalAndFalloff::OnEnter')
    534     def OnEnter(self):
    535         self.SetStatusToSuccess()
    536         min_optimal, min_falloff = self._get_my_min_optimal_and_falloff()
    537         self.SendBlackboardValue(self.attributes.minOptimalAddress, min_optimal)
    538         self.SendBlackboardValue(self.attributes.minFalloffAddress, min_falloff)
    539 
    540     def _get_my_min_optimal_and_falloff(self):
    541         effect_optimal, effect_falloff = self._get_my_min_effect_range()
    542         weapon_optimal, weapon_falloff = self._get_my_min_weapon_range()
    543         if not weapon_optimal or effect_optimal and (effect_optimal < weapon_optimal or effect_optimal == weapon_optimal and effect_falloff < weapon_falloff):
    544             logger.debug('Behavior=%s for entity_type=%s setting min optimal=%s and falloff=%s from combat effect', self.behaviorTree.GetBehaviorId(), self.context.mySlimItem.typeID, effect_optimal, effect_falloff)
    545             return (effect_optimal, effect_falloff)
    546         logger.debug('Behavior=%s for entity_type=%s setting min optimal=%s and falloff=%s from weapons', self.behaviorTree.GetBehaviorId(), self.context.mySlimItem.typeID, weapon_optimal, weapon_falloff)
    547         return (weapon_optimal, weapon_falloff)
    548 
    549     def _get_my_min_effect_range(self):
    550         minimum_optimal = 0.0
    551         minimum_falloff = 0.0
    552         my_combat_effects = self._get_my_combat_effects()
    553         for effect_id in my_combat_effects:
    554             if type_has_effect(self, self.context.mySlimItem.typeID, effect_id):
    555                 effect_optimal, effect_falloff = self._get_effect_optimal_and_falloff(effect_id)
    556                 if effect_optimal == 0.0:
    557                     logger.error('Badly authored effect: %s for behavior NPC range picks - no range assigned to it.', effect_id)
    558                     continue
    559                 if minimum_optimal == 0.0 or effect_optimal < minimum_optimal or effect_optimal == minimum_optimal and effect_falloff < minimum_falloff:
    560                     minimum_optimal = effect_optimal
    561                     minimum_falloff = effect_falloff
    562 
    563         return (minimum_optimal, minimum_falloff)
    564 
    565     def _get_my_combat_effects(self):
    566         return self.GetLastBlackboardValue(self.attributes.combatEffectsAddress) or []
    567 
    568     def _get_effect_optimal_and_falloff(self, effect_id):
    569         effect_optimal, effect_falloff = get_effect_range_and_falloff_values_for_item(self.context.dogmaLM, self.context.myItemId, effect_id)
    570         return (effect_optimal or 0.0, effect_falloff or 0.0)
    571 
    572     def _get_my_min_weapon_range(self):
    573         weapon_optimal, weapon_falloff = turret_optimal, turret_falloff = self._get_turret_range()
    574         if type_has_effect(self, self.context.mySlimItem.typeID, effectMissileLaunchingForEntity):
    575             missile_range = self._get_missile_range()
    576             if not turret_optimal or missile_range < turret_optimal:
    577                 weapon_optimal = missile_range
    578                 weapon_falloff = 0.0
    579         return (weapon_optimal or 0.0, weapon_falloff or 0.0)
    580 
    581     def _get_turret_range(self):
    582         weapon_optimal = get_my_attribute_value(self, attributeMaxRange)
    583         weapon_falloff = get_my_attribute_value(self, attributeFalloff)
    584         if weapon_falloff == 1.0:
    585             weapon_falloff = 0.0
    586         return (weapon_optimal, weapon_falloff)
    587 
    588     def _get_missile_range(self):
    589         return get_entity_max_missile_range(self, self.context.mySlimItem.typeID)
    590 
    591 
    592 class GetMaxEffectiveRange(Task):
    593 
    594     @TimedFunction('behaviors::actions::combattargets::GetMaxEffectiveRange::OnEnter')
    595     def OnEnter(self):
    596         self.SetStatusToSuccess()
    597         max_effective_range = self._get_max_effective_range()
    598         self.SendBlackboardValue(self.attributes.maxEffectiveRangeAddress, max_effective_range)
    599 
    600     def _get_max_effective_range(self):
    601         optimal_range = self.GetLastBlackboardValue(self.attributes.minOptimalAddress)
    602         falloff_range = self.GetLastBlackboardValue(self.attributes.minFalloffAddress) or 0
    603         if falloff_range:
    604             max_effective_range = optimal_range + falloff_range * COMBAT_MAX_EFFECTIVE_FALLOFF_DISTANCE_FACTOR
    605             return max_effective_range * COMBAT_MAX_EFFECTIVE_DISTANCE_FACTOR
    606         else:
    607             return optimal_range
    608 
    609 
    610 class GenerateTargetSetsByCombatRangesInGroup(Task):
    611 
    612     @TimedFunction('behaviors::actions::combattargets::GenerateTargetSetsByCombatRangesInGroup::OnEnter')
    613     def OnEnter(self):
    614         self.SetStatusToSuccess()
    615         targets_by_entity_type = self._get_targets_by_entity_type()
    616         logger.debug('Behavior=%s for task=%s generated target_set=%s', self.behaviorTree.GetBehaviorId(), self.GetTaskName(), targets_by_entity_type)
    617         self.SendBlackboardValue(self.attributes.targetsByEntityTypeAddress, targets_by_entity_type)
    618 
    619     def _get_targets_by_entity_type(self):
    620         range_by_entity_type = self._get_range_by_entity_type()
    621         balls_in_range = self._get_ball_ids_and_distance_in_range(range_by_entity_type)
    622         targets_by_entity_type = defaultdict(set)
    623         threat_target_set = self._get_threat_target_set()
    624         for distance_squared, ball_id in balls_in_range:
    625             if not is_target_valid(self, ball_id, target_list=threat_target_set, required_categories=[categoryShip]):
    626                 continue
    627             self._add_target(targets_by_entity_type, range_by_entity_type, ball_id, distance_squared)
    628 
    629         return targets_by_entity_type
    630 
    631     def _get_range_by_entity_type(self):
    632         return self.GetLastBlackboardValue(self.attributes.rangeByEntityTypeAddress)
    633 
    634     def _get_ball_ids_and_distance_in_range(self, range_by_entity_type):
    635         if range_by_entity_type is None:
    636             return []
    637         commander = self._get_commander()
    638         if commander is None or not is_ball_in_park(self, commander):
    639             logger.warn('Behavior=%s for task=% is missing commander', self.behaviorTree.GetBehaviorId(), self.GetTaskName())
    640             return []
    641         max_effective_range_of_group = max(range_by_entity_type.values())
    642         return self.context.ballpark.GetBallIdsAndDistInRange(commander, max_effective_range_of_group)
    643 
    644     def _get_commander(self):
    645         return self.GetLastBlackboardValue(self.attributes.commanderAddress)
    646 
    647     def _get_threat_target_set(self):
    648         if self.HasAttribute('threatTargetSetAddres'):
    649             return self.GetLastBlackboardValue(self.attributes.threatTargetSetAddress)
    650 
    651     def _add_target(self, targets_by_entity_type, range_by_entity_type, ball_id, distance_squared):
    652         for entity_type_id, max_effective_range in range_by_entity_type.iteritems():
    653             if max_effective_range ** 2 > distance_squared:
    654                 targets_by_entity_type[entity_type_id].add(ball_id)
    655 
    656 
    657 class GetPrimaryAndNonPrimaryTargetsByEntityTypes(Task):
    658 
    659     @TimedFunction('behaviors::actions::combattargets::GetTargetsByRanges::OnEnter')
    660     def OnEnter(self):
    661         self.SetStatusToSuccess()
    662         primary_targets, non_primary_targets = self._get_targets_by_entity_type()
    663         self.SetStatusToSuccess()
    664         self._post_targets_by_entity_type(primary_targets, non_primary_targets)
    665 
    666     def _get_targets_by_entity_type(self):
    667         all_targets = self._get_all_targets()
    668         if not all_targets:
    669             return (None, None)
    670         primary_evaluator = self._get_primary_evaluator()
    671         primary_targets = {}
    672         non_primary_evaluator = self._get_non_primary_evaluator()
    673         non_primary_targets = {}
    674         for entity_type, targets_set in all_targets.iteritems():
    675             self._get_primary_target(primary_targets, primary_evaluator, entity_type, targets_set)
    676             primary = primary_targets[entity_type]
    677             self._get_non_primary_targets(non_primary_targets, non_primary_evaluator, entity_type, targets_set, primary)
    678 
    679         return (primary_targets, non_primary_targets)
    680 
    681     def _get_all_targets(self):
    682         return self.GetLastBlackboardValue(self.attributes.targetsAddress)
    683 
    684     def _get_primary_evaluator(self):
    685         evaluator_class = targetEvaluationRegistry[self.attributes.primaryTargetEvaluationFunction]
    686         return evaluator_class(self.context, self.attributes)
    687 
    688     def _get_non_primary_evaluator(self):
    689         evaluator_class = targetEvaluationRegistry[self.attributes.nonPrimaryTargetEvaluationFunction]
    690         return evaluator_class(self.context, self.attributes)
    691 
    692     def _get_primary_target(self, primary_targets, primary_target_evaluator, entity_type, targets_set):
    693         type_list_id = getattr(self.attributes, 'primaryTypeListId', None)
    694         filtered_targets_set = self._filter_targets_set_by_type_list(type_list_id, targets_set)
    695         if filtered_targets_set:
    696             primary_targets[entity_type] = sorted(filtered_targets_set, key=primary_target_evaluator.EvaluateTarget)[-1]
    697         else:
    698             primary_targets[entity_type] = None
    699 
    700     def _get_non_primary_targets(self, non_primary_targets, non_primary_evaluator, entity_type, targets_set, primary):
    701         type_list_id = getattr(self.attributes, 'nonPrimaryTypeListId', None)
    702         filtered_targets_set = self._filter_targets_set_by_type_list(type_list_id, targets_set)
    703         if primary and primary in filtered_targets_set:
    704             filtered_targets_set.remove(primary)
    705         non_primary_targets[entity_type] = set()
    706         for target_id in filtered_targets_set:
    707             weight = non_primary_evaluator.EvaluateTarget(target_id)
    708             non_primary_targets[entity_type].add((target_id, weight))
    709 
    710     def _filter_targets_set_by_type_list(self, type_list_id, targets_set):
    711         if type_list_id is None:
    712             return targets_set
    713         types_of_interest = evetypes.GetTypeIDsByListID(type_list_id)
    714         filtered_targets_set = set()
    715         for target_id in targets_set:
    716             if get_slim_item(self, target_id).typeID in types_of_interest:
    717                 filtered_targets_set.add(target_id)
    718 
    719         return filtered_targets_set
    720 
    721     def _post_targets_by_entity_type(self, primary_targets, non_primary_targets):
    722         logger.debug('Behavior=%s for task=%s found and posting primary_targets=%s and non_primary=%s', self.behaviorTree.GetBehaviorId(), self.GetTaskName(), primary_targets, non_primary_targets)
    723         self.SendBlackboardValue(self.attributes.primaryTargetsByEntityTypesAddress, primary_targets)
    724         self.SendBlackboardValue(self.attributes.nonPrimaryTargetsByEntityTypesAddress, non_primary_targets)
    725 
    726 
    727 class GetWeightedTargetFromTargetsSet(Task):
    728 
    729     @TimedFunction('behaviors::actions::combattargets::GetWeightedTargetFromTargetsSet::OnEnter')
    730     def OnEnter(self):
    731         target_weight_set = self._get_target_weight_set()
    732         target_id = self._get_target(target_weight_set)
    733         if target_id is None:
    734             self.SetStatusToFailed()
    735             return
    736         self.SetStatusToSuccess()
    737         logger.debug('Behavior=%s for task=%s and entity=%s received new target=%s', self.behaviorTree.GetBehaviorId(), self.GetTaskName(), self.context.myItemId, target_id)
    738         self._post_target(target_id)
    739 
    740     def _get_target(self, target_weight_set):
    741         if not target_weight_set:
    742             return None
    743         return weighted_choice(target_weight_set)
    744 
    745     def _get_target_weight_set(self):
    746         return self.GetLastBlackboardValue(self.attributes.targetWeightSetAddress)
    747 
    748     def _post_target(self, target_id):
    749         self.SendBlackboardValue(self.attributes.targetIdAddress, target_id)
    750 
    751 
    752 class DisengageObsoleteTargets(Task):
    753 
    754     @TimedFunction('behaviors::actions::combattargets::DisengageObsoleteTargets::OnEnter')
    755     def OnEnter(self):
    756         self.SetStatusToSuccess()
    757         my_target_is = self.GetLastBlackboardValue(self.attributes.myTargetAddress)
    758         my_locked_targets = get_my_locked_targets(self)
    759         my_combat_effects = self._get_my_combat_effects()
    760         for target_id in my_locked_targets:
    761             if target_id != my_target_is:
    762                 self._stop_combat_effects_on_target(my_combat_effects)
    763                 remove_my_target(self, target_id)
    764 
    765     def _get_my_combat_effects(self):
    766         my_combat_effects = [effectTargetAttack, effectMissileLaunchingForEntity]
    767         my_combat_effects_from_address = self.GetLastBlackboardValue(self.attributes.myCombatEffectsAddress) or []
    768         my_combat_effects.extend(my_combat_effects_from_address)
    769         return my_combat_effects
    770 
    771     def _stop_combat_effects_on_target(self, my_combat_effects):
    772         for effect_id in my_combat_effects:
    773             try_stop_effect(self, effect_id)
    774 
    775 
    776 class FindHostileStructureInRange(Task):
    777 
    778     @TimedFunction('behaviors::actions::combattargets::FindHostileStructureInRange::OnEnter')
    779     def OnEnter(self):
    780         self.SetStatusToFailed()
    781         hostile_global_structures = self._get_hostile_structures()
    782         if hostile_global_structures:
    783             current_hostiles = self.GetLastBlackboardValue(self.attributes.targetsAddress) or set()
    784             current_hostiles = current_hostiles.union(hostile_global_structures)
    785             self.SendBlackboardValue(self.attributes.targetsAddress, current_hostiles)
    786             self.SetStatusToSuccess()
    787             logger.debug('Behavior=%s found hostile_structures=%s', self.context.myItemId, hostile_global_structures)
    788 
    789     def _get_hostile_structures(self):
    790         hostile_global_structures = set()
    791         for global_ball_id, global_ball in get_specific_globals(self, category_id=categoryStructure).iteritems():
    792             if self._is_valid(global_ball_id, global_ball):
    793                 hostile_global_structures.add(global_ball_id)
    794 
    795         return hostile_global_structures
    796 
    797     def _is_valid(self, global_ball_id, global_ball):
    798         if not global_ball.IsVulnerable():
    799             return False
    800         if not is_hostile_towards(self, global_ball.ownerID):
    801             return False
    802         return is_ball_in_range(self, self.context.myItemId, global_ball_id, self.attributes.minDistance)
    803 
    804 
    805 class FindTargetWithPolarizedPreference(FindTarget):
    806 
    807     def __init__(self, attributes = None):
    808         super(FindTargetWithPolarizedPreference, self).__init__(attributes)
    809 
    810     def OnEnter(self):
    811         super(FindTargetWithPolarizedPreference, self).OnEnter()
    812 
    813     def _GetTargetEvaluatorClass(self):
    814         return ThreatTargetEvaluator
    815 
    816     def PickTarget(self, target_list):
    817         weightByItemId = {itemId:self._EvaluateTarget(itemId) for itemId in target_list}
    818         if self._IsPreferenceEnabled():
    819             weightByItemId = get_normalized_dict(weightByItemId)
    820             biasAmount = self._GetPreferredTypeBiasAmount()
    821             if self._IsPreferringType():
    822                 self._UpdateWeights(weightByItemId, biasAmount)
    823             else:
    824                 self._UpdateWeights(weightByItemId, -biasAmount)
    825         sortedTargetIds = sorted(weightByItemId.keys(), key=lambda itemId: weightByItemId[itemId])
    826         return sortedTargetIds[-1]
    827 
    828     def _UpdateWeights(self, weightByItemId, biasAmount):
    829         preferredTypeList = self._GetPreferredTypeList()
    830         for itemId in weightByItemId:
    831             if self._GetBallTypeId(itemId) in preferredTypeList:
    832                 weightByItemId[itemId] += biasAmount
    833 
    834     def _GetBallTypeId(self, itemId):
    835         return get_ball_type_id(self, itemId)
    836 
    837     def _GetPreferredTypeList(self):
    838         preferredTypeList = evetypes.GetTypeIDsByListID(self.attributes.preferredTypeListID)
    839         return preferredTypeList
    840 
    841     def _IsPreferenceEnabled(self):
    842         return self.GetLastBlackboardValue(self.attributes.isPreferenceEnabledAddress)
    843 
    844     def _IsPreferringType(self):
    845         isPreferringType = self._GetIsPreferringType()
    846         if isPreferringType is None:
    847             isPreferringType = self._GetChanceOfPreferringType() >= random.random() * 100
    848             self._SendIsPreferringType(isPreferringType)
    849         return isPreferringType
    850 
    851     def _GetChanceOfPreferringType(self):
    852         return self.GetLastBlackboardValue(self.attributes.chanceOfPreferringTypesAddress)
    853 
    854     def _SendIsPreferringType(self, isPreferringType):
    855         self.SendBlackboardValue(self.attributes.isPreferringTypeAddress, isPreferringType)
    856 
    857     def _GetIsPreferringType(self):
    858         return self.GetLastBlackboardValue(self.attributes.isPreferringTypeAddress)
    859 
    860     def _EvaluateTarget(self, itemId):
    861         return self.GetTargetEvaluator().EvaluateTarget(itemId)
    862 
    863     def _GetPreferredTypeBiasAmount(self):
    864         return self.GetLastBlackboardValue(self.attributes.preferredTypeBiasAmountAddress)