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

ballparks.py (60438B)


      1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\packages\behaviors\actions\ballparks.py
      2 import logging
      3 import math
      4 from random import choice, random, randint
      5 import geo2
      6 import evetypes
      7 import gametime
      8 import uthread2
      9 from ballpark.const import DESTINY_MODE_WARP
     10 from ballpark.locationgenerator import GetRandomDeepSpaceLocation, GetRandomPlacementNearRandomPlanet
     11 from ballpark.messenger.const import MESSAGE_ON_BALL_MODE_CHANGED
     12 from ballpark.warpDisruptionBalls import GetWarpBubbleItemIdsWithinRangeOfBall
     13 from behaviors import status
     14 from behaviors.behaviortree import UnrecoverableBehaviorError
     15 from behaviors.blackboards import scopes
     16 from behaviors.const.combat import COMBAT_ORBIT_MWD_DISTANCE_FACTOR_SHORT_DISTANCE, COMBAT_ORBIT_MWD_DISTANCE_THRESHOLD
     17 from behaviors.const.combat import COMBAT_ORBIT_MWD_DISTANCE_MAXIMUM, COMBAT_ORBIT_COMMANDER_VELOCITY_FACTOR
     18 from behaviors.const.combat import COMBAT_ORBIT_MWD_DISTANCE_MINIMUM, COMBAT_ORBIT_MWD_DISTANCE_FACTOR_LONG_DISTANCE
     19 from behaviors.entity import NPCWarpError, NpcWarpScrambledError, NpcWarpBubbledError, NpcWarpPathBubbledError
     20 from behaviors.exceptions import BehaviorAuthoringException
     21 from behaviors.tasks import Task, BlockingTask
     22 from behaviors.utility.ballparks import check_ball_released, get_balls_in_bubble, get_ball, is_ball_in_range
     23 from behaviors.utility.ballparks import get_ball_position, is_ball_in_park, get_ball_warp_to_position
     24 from behaviors.utility.ballparks import get_id_and_types_in_bubble_by_typelist
     25 from behaviors.utility.ballparks import get_slim_item
     26 from behaviors.utility.dogmatic import get_type_attribute_value
     27 from behaviors.utility.inventory import get_inventory_item
     28 from behaviors.utility.roles import is_commander, has_combat_role
     29 from brennivin.itertoolsext import Bundle
     30 from carbon.common.lib.const import SEC
     31 from carbon.common.script.util import mathCommon
     32 from carbon.common.script.util.mathCommon import RandomVector
     33 from ccpProfile import TimedFunction
     34 from dogma.const import attributeEntityCruiseSpeed, attributeMaxVelocity
     35 from dogma.const import attributeWarpSpeedMultiplier
     36 from eve.common.lib.appConst import AU
     37 from eve.common.script.sys.idCheckers import IsEvePlayerCharacter
     38 from eveexceptions import UserError
     39 from eveuniverse.security import SecurityClassFromLevel, securityClassZeroSec
     40 from inventorycommon.const import categoryCharge, groupStargate, groupStation
     41 from spacecomponents.common.componentConst import NPC_WARP_BEACON, STORE_SLIM_ITEM_FIELD_IN_ITEM_SETTINGS
     42 from spacecomponents.common.helper import HasStoreSlimItemFieldInItemSettingsComponent
     43 from spacecomponents.server.components.storeslimitemfieldinitemsettings import StoreSlimItemFieldInItemSettings
     44 logger = logging.getLogger(__name__)
     45 ADDITIONAL_EXPLORATION_GROUPS = (scopes.ScopeTypes.EntityGroup, 'ADDITIONAL_EXPLORATION_GROUPS')
     46 ADDITIONAL_EXPLORATION_ACTION = (scopes.ScopeTypes.EntityGroup, 'ADDITIONAL_EXPLORATION_ACTION')
     47 ALIGN_START_TIME = (scopes.ScopeTypes.Item, 'ALIGN_START_TIME')
     48 ALIGN_DIRECTION_NORMAL = (scopes.ScopeTypes.Item, 'ALIGN_DIRECTION_NORMAL')
     49 UNSAFE_EXPLORATION_GROUPS = [groupStargate, groupStation]
     50 
     51 class WarpToNewLocation(BlockingTask):
     52 
     53     @TimedFunction('behaviors::actions::ballparks::WarpToNewLocation::OnEnter')
     54     def OnEnter(self):
     55         super(WarpToNewLocation, self).OnEnter()
     56         self.SetStatusToFailed()
     57         coordinate = self.GetDestinationCoordinate()
     58         jitterRadius = self.GetJitterRadius()
     59         warpAtRange = self.GetWarpinDistance()
     60         warpSpeed = self.GetWarpSpeed()
     61         if coordinate:
     62             try:
     63                 logger.debug('Behavior entity=%s is starting warp', self.context.myItemId)
     64                 self.context.myBall.NPCWarpTo(coordinate, jitterRadius=jitterRadius, warpAtRange=warpAtRange, warpSpeed=warpSpeed)
     65                 self.SubscribeItem(self.context.myItemId, MESSAGE_ON_BALL_MODE_CHANGED, self.DestinyModeChange)
     66                 self.SendBlackboardValue(self.attributes.warpScrambledAddress, False)
     67                 self.SetStatusToBlocked()
     68             except NpcWarpBubbledError:
     69                 logger.debug('Behavior entity=%s is warp bubbled and cant warp anywhere', self.context.myItemId)
     70                 self.SendBlackboardValue(self.attributes.warpScrambledAddress, True)
     71                 self.context.myBall.NPCEndWarp()
     72             except NpcWarpScrambledError:
     73                 logger.debug('Behavior entity=%s is warp scrambled and cant warp anywhere', self.context.myItemId)
     74                 self.SendBlackboardValue(self.attributes.warpScrambledAddress, True)
     75                 self.context.myBall.NPCEndWarp()
     76             except NpcWarpPathBubbledError:
     77                 logger.debug('Behavior entity=%s path is warp bubbled', self.context.myItemId)
     78                 self.context.myBall.NPCEndWarp()
     79             except NPCWarpError as e:
     80                 logger.debug('Unable to warp: %s', e)
     81                 try:
     82                     self.context.myBall.NPCEndWarp()
     83                 except (UserError, NPCWarpError):
     84                     raise UnrecoverableBehaviorError('Unable to end after a failed warp.  Entity is probably dead.')
     85 
     86     def GetWarpSpeed(self):
     87         warpSpeed = self.context.ballpark.dogmaLM.GetAttributeValue(self.context.myItemId, attributeWarpSpeedMultiplier)
     88         if not warpSpeed or warpSpeed < 0.01:
     89             return 6.0
     90         return warpSpeed
     91 
     92     def GetDestinationCoordinate(self):
     93         return self.GetLastBlackboardValue(self.attributes.locationMessage)
     94 
     95     def DestinyModeChange(self, oldMode, newMode):
     96         if oldMode == DESTINY_MODE_WARP and self.context.ballpark is not None:
     97             self.UnsubscribeItem(self.context.myItemId, MESSAGE_ON_BALL_MODE_CHANGED, self.DestinyModeChange)
     98             self.SetStatusToSuccess()
     99             logger.debug('Behavior entity=%s is exiting warp', self.context.myItemId)
    100 
    101     def CleanUp(self):
    102         if not self.IsInvalid():
    103             self.UnsubscribeItem(self.context.myItemId, MESSAGE_ON_BALL_MODE_CHANGED, self.DestinyModeChange)
    104             self.SetStatusToInvalid()
    105 
    106     def GetJitterRadius(self):
    107         if hasattr(self.attributes, 'jitterRadius') and self.attributes.jitterRadius is not None:
    108             return self.attributes.jitterRadius
    109         else:
    110             return 20000
    111 
    112     def GetWarpinDistance(self):
    113         if self.HasAttribute('warpAtDistanceAddress'):
    114             valueFromAddress = self.GetLastBlackboardValue(self.attributes.warpAtDistanceAddress)
    115             if valueFromAddress is None:
    116                 logger.debug('Behavior entity=%s has a warp distance address, but the value is None. Defaulting to 0.0', self.context.myItemId)
    117             else:
    118                 return valueFromAddress
    119         return 0.0
    120 
    121 
    122 class WarpToItem(WarpToNewLocation):
    123 
    124     def GetDestinationCoordinate(self):
    125         item_id = self.GetLastBlackboardValue(self.attributes.warpToItemIdAddress)
    126         if item_id is None:
    127             return
    128         if not is_ball_in_park(self, item_id):
    129             return
    130         coordinate = self.context.ballpark.GetWarpDestinationPoint(self.context.myItemId, item_id)
    131         return coordinate
    132 
    133 
    134 class SelectCoordinateNearLocationAction(Task):
    135 
    136     @TimedFunction('behaviors::actions::ballparks::SelectCoordinateNearLocationAction::OnEnter')
    137     def OnEnter(self):
    138         locationID = self.GetLastBlackboardValue(self.attributes.locationAddress)
    139         referenceCoordinates = self.GetReferenceCoordinates(locationID)
    140         selectedCoordinates = self.ComputeRandomCoordinate(referenceCoordinates)
    141         self.SendBlackboardValue(self.attributes.coordinateAddress, selectedCoordinates)
    142         self.status = status.TaskSuccessStatus
    143 
    144     def GetReferenceCoordinates(self, locationID):
    145         if locationID is None:
    146             loc = (self.context.myBall.x, self.context.myBall.y, self.context.myBall.z)
    147         else:
    148             loc = self.GetLocation(locationID)
    149         return loc
    150 
    151     def ComputeRandomCoordinate(self, locationCoordinates):
    152         offset = mathCommon.RandomVector(self.attributes.maxDistance, self.attributes.maxDistance - self.attributes.minDistance)
    153         selectedCoordinates = geo2.Vec3AddD(locationCoordinates, offset)
    154         return selectedCoordinates
    155 
    156     def GetLocation(self, locationID):
    157         loc = cfg.evelocations.Get(locationID)
    158         return (loc.x, loc.y, loc.z)
    159 
    160 
    161 class SelectCoordinateNearCoordinateAction(Task):
    162 
    163     @TimedFunction('behaviors::actions::ballparks::SelectCoordinateNearCoordinateAction::OnEnter')
    164     def OnEnter(self):
    165         sourcePoint = self._GetSourceCoordinates()
    166         selectedCoordinates = self.ComputeRandomCoordinate(sourcePoint or self.context.myBall.GetPosition())
    167         self.SendBlackboardValue(self.attributes.destinationCoordinateAddress, selectedCoordinates)
    168         self.status = status.TaskSuccessStatus
    169 
    170     def _GetSourceCoordinates(self):
    171         return self.GetLastBlackboardValue(self.attributes.sourceCoordinateAddress)
    172 
    173     def ComputeRandomCoordinate(self, locationCoordinates):
    174         offset = mathCommon.RandomVector(self.attributes.maxDistance, self.attributes.maxDistance - self.attributes.minDistance)
    175         selectedCoordinates = geo2.Vec3AddD(locationCoordinates, offset)
    176         return selectedCoordinates
    177 
    178 
    179 class SelectCoordinateNearBall(SelectCoordinateNearCoordinateAction):
    180 
    181     def _GetSourceCoordinates(self):
    182         ball_id = self.GetLastBlackboardValue(self.attributes.targetBallAddress)
    183         ball = get_ball(self, ball_id)
    184         return (ball.x, ball.y, ball.z)
    185 
    186 
    187 class ApproachCoordinatesAction(Task):
    188 
    189     @TimedFunction('behaviors::actions::ballparks::ApproachCoordinatesAction::OnEnter')
    190     def OnEnter(self):
    191         coordinate = self.GetLastBlackboardValue(self.attributes.coordinateAddress)
    192         if coordinate is None:
    193             self.status = status.TaskFailureStatus
    194             return
    195         if self.attributes.blocking:
    196             self.context.myBall.GotoPoint(coordinate, notificationRange=self.attributes.notifyRange, callback=(self.OnApproachNotify, ()))
    197             self.status = status.TaskSuspendedStatus
    198         else:
    199             self.context.myBall.GotoPoint(coordinate)
    200             self.status = status.TaskSuccessStatus
    201 
    202     def OnApproachNotify(self, entered, *args):
    203         logger.debug('OnApproachNotify(%s)', (entered, self.behaviorTree, self.context))
    204         self.context.myBall.StopMovementEvents()
    205         if self.status is not status.TaskInvalidStatus:
    206             self.status = status.TaskSuccessStatus
    207             self.status.OnUpdated(self)
    208 
    209 
    210 def clampBetween(value, low, high):
    211     return max(min(value, high), low)
    212 
    213 
    214 class AlignToCoordinatesAction(Task):
    215 
    216     @TimedFunction('behaviors::actions::ballparks::AlignToCoordinatesAction::OnEnter')
    217     def OnEnter(self):
    218         self.SendBlackboardValue(ALIGN_START_TIME, gametime.GetSimTime())
    219         self._MakeSureTargetPointIsFarAway()
    220         self.SetStatusToRunning()
    221 
    222     def _MakeSureTargetPointIsFarAway(self):
    223         minDist = 30000
    224         coordinate = self.GetLastBlackboardValue(self.attributes.coordinateAddress)
    225         myPos = self.context.myBall.GetPosition()
    226         direction = geo2.Vec3SubtractD(coordinate, myPos)
    227         if geo2.Vec3LengthSqD(direction) < minDist * minDist:
    228             normalizedDirection = geo2.Vec3NormalizeD(direction)
    229             scaledDirection = geo2.Vec3ScaleD(normalizedDirection, minDist)
    230             coordinate = geo2.Vec3AddD(myPos, scaledDirection)
    231             self.SendBlackboardValue(self.attributes.coordinateAddress, coordinate)
    232 
    233     def _HasTimedOut(self):
    234         timeOutSec = getattr(self.attributes, 'timeoutSeconds', 60.0)
    235         startTime = self.GetLastBlackboardValue(ALIGN_START_TIME)
    236         current_time = gametime.GetSimTime()
    237         return current_time - startTime > timeOutSec * SEC
    238 
    239     def _HasSuccessfullyAligned(self, alignDirectionNormal):
    240         acceptableDeviation = getattr(self.attributes, 'acceptableAngleDeviationDegrees', 1.0)
    241         fighterDirectionNormal = geo2.Vec3NormalizeD((self.context.myBall.vx, self.context.myBall.vy, self.context.myBall.vz))
    242         dotProduct = geo2.Vec3DotD(alignDirectionNormal, fighterDirectionNormal)
    243         dotProduct = clampBetween(dotProduct, -1.0, 1.0)
    244         angle = math.acos(dotProduct)
    245         return math.degrees(angle) < acceptableDeviation
    246 
    247     def _StopMovement(self):
    248         self.context.myBall.StopMovement()
    249 
    250     def _UpdateGotoPointAndReturnNormalizedDirection(self):
    251         coordinate = self.GetLastBlackboardValue(self.attributes.coordinateAddress)
    252         if coordinate is None:
    253             self.SetStatusToFailed()
    254             return
    255         myPos = self.context.myBall.GetPosition()
    256         direction = geo2.Vec3SubtractD(coordinate, myPos)
    257         normalizedDirection = geo2.Vec3NormalizeD(direction)
    258         gotoDistance = 5000.0
    259         gotoVector = geo2.Vec3ScaleD(normalizedDirection, gotoDistance)
    260         gotoPoint = geo2.Vec3AddD(myPos, gotoVector)
    261         self.context.myBall.GotoPoint(gotoPoint)
    262         return normalizedDirection
    263 
    264     def Update(self):
    265         normalizedDirection = self._UpdateGotoPointAndReturnNormalizedDirection()
    266         if self._HasSuccessfullyAligned(normalizedDirection):
    267             self.SetStatusToSuccess()
    268             self._StopMovement()
    269         elif self._HasTimedOut():
    270             self.SetStatusToFailed()
    271             self._StopMovement()
    272 
    273     def CleanUp(self):
    274         if not self.IsRunning():
    275             self.SetStatusToInvalid()
    276 
    277 
    278 class ApproachObject(Task):
    279 
    280     @TimedFunction('behaviors::actions::ballparks::ApproachObject::OnEnter')
    281     def OnEnter(self):
    282         self.SetStatusToFailed()
    283         itemId = self.GetLastBlackboardValue(self.attributes.itemIdAddress)
    284         if itemId is None:
    285             return
    286         if not self.context.ballpark.HasBall(itemId):
    287             return
    288         approachRange = self._GetApproachRange()
    289         if self.attributes.blocking:
    290             notifyRange = max(self.attributes.notifyRange, approachRange)
    291             self.context.myBall.ApproachObject(itemId, notificationRange=notifyRange, approachRange=approachRange, callback=(self.OnApproachNotify, ()))
    292             self.SetStatusToSuspended()
    293         else:
    294             self.context.myBall.ApproachObject(itemId, approachRange=approachRange)
    295             self.SetStatusToSuccess()
    296 
    297     def _GetApproachRange(self):
    298         if getattr(self.attributes, 'approachRangeAddress', None):
    299             return self.GetLastBlackboardValue(self.attributes.approachRangeAddress)
    300         return self.attributes.approachRange
    301 
    302     def OnApproachNotify(self, entered, *args):
    303         logger.debug('OnApproachNotify(%s)', (entered, self.behaviorTree, self.context))
    304         self.context.myBall.StopMovementEvents()
    305         if not self.IsInvalid():
    306             self.SetStatusToSuccess()
    307             self.status.OnUpdated(self)
    308 
    309 
    310 class OrbitAtDistanceAction(Task):
    311 
    312     def OrbitNonBlocking(self, orbitTargetId, orbitRange):
    313         self.context.myBall.OrbitObject(orbitTargetId, approachRange=orbitRange)
    314         self.status = status.TaskSuccessStatus
    315 
    316     @TimedFunction('behaviors::actions::ballparks::OrbitAtDistanceAction::OnEnter')
    317     def OnEnter(self):
    318         check_ball_released(self.context.myBall, raises=UnrecoverableBehaviorError)
    319         orbitTargetId = self.GetLastBlackboardValue(self.attributes.orbitTargetAddress)
    320         if orbitTargetId is None:
    321             self.SetStatusToFailed()
    322             return
    323         if not self.context.ballpark.HasBall(orbitTargetId):
    324             self.SetStatusToFailed()
    325             return
    326         try:
    327             orbitRange = self._GetOrbitRange()
    328             if self.attributes.blocking:
    329                 self.OrbitBlocking(orbitTargetId, orbitRange)
    330             else:
    331                 self.OrbitNonBlocking(orbitTargetId, orbitRange)
    332             self._set_velocity_fraction()
    333         except UserError:
    334             self.SetStatusToFailed()
    335 
    336     def _GetOrbitRange(self):
    337         if getattr(self.attributes, 'orbitRangeAddress', None):
    338             return self.GetLastBlackboardValue(self.attributes.orbitRangeAddress)
    339         if getattr(self.attributes, 'orbitRange', None):
    340             return self.attributes.orbitRange
    341         raise BehaviorAuthoringException('BehaviorID: %s has no orbit range defined for task: %s', self.behaviorTree.GetBehaviorId(), type(self).__name__)
    342 
    343     def OrbitBlocking(self, orbitTargetId, orbitRange):
    344         if getattr(self.attributes, 'blockUntilRangeAddress', None):
    345             blockUntilRange = self.GetLastBlackboardValue(self.attributes.blockUntilRangeAddress)
    346         else:
    347             blockUntilRange = self.attributes.blockUntilRange or 1000.0
    348         self.context.myBall.OrbitObject(orbitTargetId, approachRange=orbitRange, notificationRange=blockUntilRange, callback=(self.OnInOrbitNotify, ()))
    349         self.status = status.TaskSuspendedStatus
    350 
    351     def OnInOrbitNotify(self, entered, *args):
    352         logger.debug('OnInOrbitNotify(%s)', (entered, self.behaviorTree, self.context))
    353         self.context.myBall.StopMovementEvents()
    354         if not self.IsInvalid():
    355             self.SetStatusToSuccess()
    356             self.status.OnUpdated(self)
    357 
    358     def _set_velocity_fraction(self):
    359         if self.HasAttribute('velocityFraction'):
    360             self.context.ballpark.SetSpeedFraction(self.context.myItemId, self.attributes.velocityFraction)
    361 
    362     def CleanUp(self):
    363         if not self.IsInvalid() and self.HasAttribute('velocityFraction'):
    364             self.context.ballpark.SetSpeedFraction(self.context.myItemId, 1.0)
    365         self.SetStatusToInvalid()
    366 
    367 
    368 class FullStopAction(Task):
    369 
    370     @TimedFunction('behaviors::actions::ballparks::FullStopAction::OnEnter')
    371     def OnEnter(self):
    372         self.context.myBall.StopMovement()
    373         self.status = status.TaskSuccessStatus
    374 
    375 
    376 class SelectTargetToAnalyzeAction(Task):
    377 
    378     @TimedFunction('behaviors::actions::ballparks::SelectTargetToAnalyzeAction::OnEnter')
    379     def OnEnter(self):
    380         logger.debug('SelectTargetToAnalyzeAction starting')
    381         ballsInBubble = self.context.ballpark.bubbles.get(self.context.myBall.newBubbleId)
    382         self.status = status.TaskFailureStatus
    383         if not ballsInBubble:
    384             logger.error('No bubbles to pick from')
    385             return
    386         targetCandidates = self.GetTargetCandidates(ballsInBubble)
    387         logger.debug('Target Candidates for Analysis: %s' % targetCandidates)
    388         targetId = self.FindSuitableTarget(targetCandidates)
    389         if targetId is not None:
    390             self.SendBlackboardValue(self.attributes.selectedTargetAddress, targetId)
    391             self.status = status.TaskSuccessStatus
    392         else:
    393             logger.debug('no suitable target found')
    394 
    395     def GetTargetCandidates(self, ballsInBubble):
    396         targetCandidates = []
    397         for ballId in ballsInBubble.keys():
    398             if ballId <= 0:
    399                 continue
    400             if self.context.myItemId == ballId:
    401                 continue
    402             targetCandidates.append(ballId)
    403 
    404         return targetCandidates
    405 
    406     def FindSuitableTarget(self, targetCandidates):
    407         targetId = None
    408         maxDistance = self.attributes.maxDistance
    409         GetSurfaceDist = self.context.ballpark.GetSurfaceDist
    410         getSlim = self.context.ballpark.slims.get
    411         ignoredCategories = self.attributes.ignoredCategories
    412         ignoredGroups = self.attributes.ignoredGroups
    413         ignoredTypes = self.attributes.ignoredTypes
    414         shouldIgnoreMobileTargets = self.attributes.shouldIgnoreMobileTargets
    415         while targetId is None and targetCandidates:
    416             index = randint(0, len(targetCandidates) - 1)
    417             ballId = targetCandidates.pop(index)
    418             slimItem = getSlim(ballId)
    419             if slimItem is None:
    420                 continue
    421             if slimItem.categoryID in ignoredCategories:
    422                 continue
    423             if slimItem.groupID in ignoredGroups:
    424                 continue
    425             if slimItem.typeID in ignoredTypes:
    426                 continue
    427             ball = self.context.ballpark.GetBall(ballId)
    428             if ball.isCloaked:
    429                 continue
    430             if shouldIgnoreMobileTargets and ball.isFree:
    431                 continue
    432             distance = GetSurfaceDist(self.context.myItemId, ballId)
    433             if distance and distance <= maxDistance:
    434                 targetId = ballId
    435 
    436         return targetId
    437 
    438 
    439 class GetBallWarpToLocation(Task):
    440 
    441     @TimedFunction('behaviors::actions::ballparks::GetBallWarpToLocation::OnEnter')
    442     def OnEnter(self):
    443         self.SetStatusToFailed()
    444         ballId = self.GetLastBlackboardValue(self.attributes.ballIdAddress)
    445         if ballId is None:
    446             return
    447         try:
    448             self.context.ballpark.CheckBall(self.context.myItemId)
    449         except (UserError, RuntimeError):
    450             raise UnrecoverableBehaviorError('Entity no longer in ballpark. Probably dead.')
    451 
    452         position = get_ball_warp_to_position(self, ballId)
    453         self.SendBlackboardValue(self.attributes.locationAddress, position)
    454         self.SetStatusToSuccess()
    455 
    456 
    457 class GetItemIdByTypeId(Task):
    458 
    459     @TimedFunction('behaviors::actions::ballparks::GetItemIdByTypeId::OnEnter')
    460     def OnEnter(self):
    461         self.SetStatusToFailed()
    462         if not self.attributes.typeIds:
    463             return
    464         itemId = self.GetItemIdFromBallpark(self.context.myBall.id, self.attributes.typeIds)
    465         if itemId:
    466             self.SendBlackboardValue(self.attributes.itemIdAddress, itemId)
    467             self.SetStatusToSuccess()
    468 
    469     def GetItemIdFromBallpark(self, myBallId, typeIds):
    470         itemId = None
    471         getSlim = self.context.ballpark.slims.get
    472         itemsInBubble = self.context.ballpark.GetItemsInBubble(None, itemID=myBallId)
    473         while itemId is None and itemsInBubble:
    474             index = randint(0, len(itemsInBubble) - 1)
    475             ballId = itemsInBubble.pop(index)
    476             slimItem = getSlim(ballId)
    477             if slimItem is None:
    478                 continue
    479             if slimItem.typeID not in typeIds:
    480                 continue
    481             itemId = ballId
    482 
    483         return itemId
    484 
    485 
    486 class GetItemIdByTypeIdsFromBlackboard(GetItemIdByTypeId):
    487 
    488     @TimedFunction('behaviors::actions::ballparks::GetItemIdByTypeId::OnEnter')
    489     def OnEnter(self):
    490         self.SetStatusToFailed()
    491         if self.attributes.typeIdsAddress is None:
    492             return
    493         typeIds = self.GetLastBlackboardValue(self.attributes.typeIdsAddress)
    494         if not typeIds:
    495             return
    496         itemId = self.GetItemIdFromBallpark(self.context.myBall.id, typeIds)
    497         if itemId:
    498             self.SendBlackboardValue(self.attributes.itemIdAddress, itemId)
    499             self.SetStatusToSuccess()
    500 
    501 
    502 class GetRandomDeepSpacePosition(Task):
    503 
    504     @TimedFunction('behaviors::actions::ballparks::GetRandomDeepSpacePosition::OnEnter')
    505     def OnEnter(self):
    506         self.SetStatusToFailed()
    507         location = GetRandomDeepSpaceLocation(self.context.ballpark.solarsystemID, minPlacementDistance=500)
    508         if location:
    509             self.SendBlackboardValue(self.attributes.locationAddress, location)
    510             self.SetStatusToSuccess()
    511 
    512 
    513 class GetRandomPositionNearRandomPlanet(Task):
    514 
    515     @TimedFunction('behaviors::actions::ballparks::GetRandomPositionNearRandomPlanet::OnEnter')
    516     def OnEnter(self):
    517         self.SetStatusToFailed()
    518         location = GetRandomPlacementNearRandomPlanet(self.context.ballpark.solarsystemID, self.attributes.minDistanceAU * AU, self.attributes.maxDistanceAU * AU)
    519         if location:
    520             self.SendBlackboardValue(self.attributes.locationAddress, location)
    521             self.SetStatusToSuccess()
    522 
    523 
    524 class TransferToLocation(Task):
    525 
    526     @TimedFunction('behaviors::actions::ballparks::TransferToLocation::OnEnter')
    527     def OnEnter(self):
    528         self.status = status.TaskFailureStatus
    529         location = self.GetLastBlackboardValue(self.attributes.locationAddress)
    530         if location:
    531             self.context.ballpark.ChangeBallPosition(self.context.myBall.id, *location)
    532             self.status = status.TaskSuccessStatus
    533 
    534 
    535 class GetObjectSpaceLocation(Task):
    536 
    537     @TimedFunction('behaviors::actions::ballparks::GetObjectSpaceLocation::OnEnter')
    538     def OnEnter(self):
    539         itemId = self.GetLastBlackboardValue(self.attributes.itemIdAddress)
    540         if not itemId:
    541             self.status = status.TaskFailureStatus
    542             return
    543         location = self.context.ballpark.GetBallPosition(itemId)
    544         self.SendBlackboardValue(self.attributes.locationAddress, location)
    545         self.status = status.TaskSuccessStatus
    546 
    547 
    548 class MakeSelfInvulnerable(Task):
    549 
    550     @TimedFunction('behaviors::actions::ballparks::MakeSelfInvulnerable::OnEnter')
    551     def OnEnter(self):
    552         self.context.ballpark.MakeInvulnerablePermanently(self.context.myBall.id, self.attributes.reason)
    553         self.status = status.TaskSuccessStatus
    554 
    555 
    556 class CancelInvulnerabilityForSelf(Task):
    557 
    558     @TimedFunction('behaviors::actions::ballparks::CancelInvulnerabilityForSelf::OnEnter')
    559     def OnEnter(self):
    560         self.context.ballpark.CancelCurrentInvulnerability(self.context.myBall.id, self.attributes.reason)
    561         self.status = status.TaskSuccessStatus
    562 
    563 
    564 class Cloak(Task):
    565 
    566     @TimedFunction('behaviors::actions::ballparks::Cloak::OnEnter')
    567     def OnEnter(self):
    568         self.context.ballpark.CloakShip(self.context.myItemId)
    569         self.status = status.TaskSuccessStatus
    570 
    571 
    572 class Uncloak(Task):
    573 
    574     @TimedFunction('behaviors::actions::ballparks::Uncloak::OnEnter')
    575     def OnEnter(self):
    576         self.context.ballpark.UncloakShip(self.context.myItemId)
    577         self.status = status.TaskSuccessStatus
    578 
    579 
    580 class SelectItemByTypes(Task):
    581 
    582     @TimedFunction('behaviors::actions::ballparks::SelectItemByTypes::OnEnter')
    583     def OnEnter(self):
    584         ballpark = self.context.ballpark
    585         bubbleId = self.context.myBall.newBubbleId
    586         ballIdsInBubble = ballpark.bubbles.get(bubbleId)
    587         self.status = status.TaskFailureStatus
    588         if ballIdsInBubble:
    589             selectedItem = self.FindItemByType(ballIdsInBubble, ballpark.slims)
    590             if selectedItem is not None:
    591                 self.SendBlackboardValue(self.attributes.selectedItemAddress, selectedItem)
    592                 self.status = status.TaskSuccessStatus
    593 
    594     def FindItemByType(self, ballIdsInBubble, slims):
    595         for ballId in ballIdsInBubble:
    596             slim = slims.get(ballId)
    597             if slim and slim.typeID in self.attributes.typeIds:
    598                 return ballId
    599 
    600 
    601 class SelectItemByGroups(Task):
    602 
    603     @TimedFunction('behaviors::actions::ballparks::SelectItemByTypes::OnEnter')
    604     def OnEnter(self):
    605         ballpark = self.context.ballpark
    606         bubbleId = self.context.myBall.newBubbleId
    607         ballIdsInBubble = ballpark.bubbles.get(bubbleId)
    608         self.status = status.TaskFailureStatus
    609         if ballIdsInBubble:
    610             selectedItem = self.FindItemByGroup(ballIdsInBubble, ballpark.slims)
    611             if selectedItem is not None:
    612                 self.SendBlackboardValue(self.attributes.selectedItemAddress, selectedItem)
    613                 self.status = status.TaskSuccessStatus
    614 
    615     def FindItemByGroup(self, ballIdsInBubble, slims):
    616         for ballId in ballIdsInBubble:
    617             slim = slims.get(ballId)
    618             if slim and slim.groupID in self.attributes.groupIds:
    619                 return ballId
    620 
    621 
    622 class StoreSunToBlackboardValue(Task):
    623 
    624     @TimedFunction('behaviors::actions::ballparks::SelectItemByTypes::OnEnter')
    625     def OnEnter(self):
    626         self.SetStatusToFailed()
    627         sunID = self.context.ballpark.sunID
    628         if sunID:
    629             self.SendBlackboardValue(self.attributes.targetAddress, sunID)
    630             self.status = status.TaskSuccessStatus
    631             self.SetStatusToSuccess()
    632 
    633 
    634 class BallNotInParkError(Exception):
    635     pass
    636 
    637 
    638 class SelectClosestPointOnObject(Task):
    639 
    640     @TimedFunction('behaviors::actions::ballparks::SelectClosestPointOnObject::OnEnter')
    641     def OnEnter(self):
    642         objectId = self.GetLastBlackboardValue(self.attributes.objectIdAddress)
    643         try:
    644             closestSphere = self.GetClosestSphere(objectId)
    645             closestPoint = self.GetClosestSurfacePoint(closestSphere)
    646             self.SendBlackboardValue(self.attributes.closestPointAddress, closestPoint)
    647             self.status = status.TaskSuccessStatus
    648         except KeyError:
    649             self.status = status.TaskFailureStatus
    650 
    651     def GetClosestSphere(self, objectId):
    652         ball = self.context.ballpark.balls[objectId]
    653         pos = (ball.x, ball.y, ball.z)
    654         myPos = (self.context.myBall.x, self.context.myBall.y, self.context.myBall.z)
    655         if len(ball.miniBalls) == 0:
    656             return Bundle(pos=(ball.x, ball.y, ball.z), radius=ball.radius)
    657 
    658         def GetDistance(m):
    659             miniPos = (m.x, m.y, m.z)
    660             return geo2.Vec3DistanceD(myPos, geo2.Vec3AddD(pos, miniPos))
    661 
    662         miniBall = sorted(ball.miniBalls, key=GetDistance)[0]
    663         return Bundle(pos=geo2.Vec3AddD(pos, (miniBall.x, miniBall.y, miniBall.z)), radius=miniBall.radius)
    664 
    665     def GetClosestSurfacePoint(self, otherSphere):
    666         myPos = (self.context.myBall.x, self.context.myBall.y, self.context.myBall.z)
    667         vecToOther = geo2.Vec3SubtractD(otherSphere.pos, myPos)
    668         distanceToOther = geo2.Vec3LengthD(vecToOther)
    669         vecToPoint = geo2.Vec3ScaleD(vecToOther, 1.0 - otherSphere.radius / distanceToOther)
    670         return geo2.Vec3AddD(myPos, vecToPoint)
    671 
    672 
    673 class GetBallsInBubbleByOwnerIds(Task):
    674 
    675     @TimedFunction('behaviors::actions::ballparks::GetBallsInBubbleByOwnerIds::OnEnter')
    676     def OnEnter(self):
    677         ballIds = self.GetBallsInBubbleForOwnerIds()
    678         self.UpdateObjectList(ballIds)
    679         self.status = status.TaskSuccessStatus
    680 
    681     def GetBallsInBubbleForOwnerIds(self):
    682         shipBallsInBubble = self.context.ballpark.GetShipsInSameBubble(self.context.myItemId)
    683         ownerIds = self.GetLastBlackboardValue(self.attributes.ownerListAddress)
    684         ballIds = set()
    685         for shipBallId in shipBallsInBubble:
    686             slim = self.context.ballpark.slims.get(shipBallId)
    687             if slim.ownerID in ownerIds:
    688                 ballIds.add(shipBallId)
    689 
    690         return ballIds
    691 
    692     def UpdateObjectList(self, ballIds):
    693         if not ballIds:
    694             return
    695         existingBallIds = self.GetExistingBallIds()
    696         if ballIds.issubset(existingBallIds):
    697             return
    698         existingBallIds.update(ballIds)
    699         self.SendBlackboardValue(self.attributes.objectListAddress, existingBallIds)
    700 
    701     def GetExistingBallIds(self):
    702         return self.GetLastBlackboardValue(self.attributes.objectListAddress) or set()
    703 
    704 
    705 class EntityLootItem(Task):
    706 
    707     @TimedFunction('behaviors::actions::ballparks::EntityLootItem::OnEnter')
    708     def OnEnter(self):
    709         itemId = self.GetItemForCollecting()
    710         if itemId:
    711             item = self.context.ballpark.inventory2.GetItem(itemId)
    712             self.context.ballpark.RemoveItemFromPark(item)
    713             self.AddObjectToLoot(item)
    714             self.status = status.TaskSuccessStatus
    715         else:
    716             self.status = status.TaskFailureStatus
    717 
    718     def GetItemForCollecting(self):
    719         return self.GetLastBlackboardValue(self.attributes.itemIdAddress)
    720 
    721     def AddObjectToLoot(self, item):
    722         logger.debug('Entity: %s looting typeId: %s & itemId: %s from park.' % (self.context.myBall.id, item.typeID, item.itemID))
    723         self.context.myBall.AddTypeAndAmountToAdditionalLoot(item.typeID)
    724 
    725     def SetChannel(self, context):
    726         self.channel = scopes.GetChannelFromAddress(context, self.attributes.itemIdAddress)
    727 
    728 
    729 class StoreSlimItemFieldInItemSettingsAction(Task):
    730 
    731     @TimedFunction('behaviors::actions::ballparks::StoreSlimItemFieldInItemSettingsAction::OnEnter')
    732     def OnEnter(self):
    733         targetId = self.GetLastBlackboardValue(self.attributes.targetAddress)
    734         if targetId is None:
    735             logger.info('StoreSlimItemFieldInItemSettingsAction::Failed to get targetID from blackboard')
    736             self.SetStatusToFailed()
    737             return
    738         targetSlimItem = self.context.ballpark.slims.get(targetId, None)
    739         if targetSlimItem is None:
    740             logger.info('StoreSlimItemFieldInItemSettingsAction::Ball not in park')
    741             self.SetStatusToFailed()
    742             return
    743         if not HasStoreSlimItemFieldInItemSettingsComponent(targetSlimItem.typeID):
    744             logger.error('StoreSlimItemFieldInItemSettingsAction::Trying to set persistent pose for type %s which does not have the persistent pose spacecomponent attached.', targetSlimItem.typeID)
    745             self.SetStatusToFailed()
    746             return
    747         slimItemFieldName = self.GetLastBlackboardValue(self.attributes.slimItemFieldName)
    748         if slimItemFieldName is None:
    749             logger.info('StoreSlimItemFieldInItemSettingsAction::Failed to get slim item field name from blackboard')
    750             self.SetStatusToFailed()
    751             return
    752         if slimItemFieldName not in StoreSlimItemFieldInItemSettings.GetPersistentSlimItemAttributesForType(targetSlimItem.typeID):
    753             logger.info("StoreSlimItemFieldInItemSettingsAction::Can't modify field '%s' as it is not listed as one of the persistent fields on the space component.", slimItemFieldName)
    754             self.SetStatusToFailed()
    755             return
    756         slimItemFieldValue = self.GetLastBlackboardValue(self.attributes.slimItemFieldValue)
    757         if slimItemFieldValue is None:
    758             logger.info('StoreSlimItemFieldInItemSettingsAction::Failed to get slim item field value from blackboard')
    759             self.SetStatusToFailed()
    760             return
    761         persistentPoseComponent = self.context.ballpark.componentRegistry.GetComponentForItem(targetId, STORE_SLIM_ITEM_FIELD_IN_ITEM_SETTINGS)
    762         persistentPoseComponent.SaveSlimItemSetting(self.context.ballpark, slimItemFieldName, slimItemFieldValue)
    763         self.SetStatusToSuccess()
    764 
    765 
    766 class StoreBlackboardToSlimItemAttribute(Task):
    767 
    768     @TimedFunction('behaviors::actions::ballparks::StoreBlackboardToSlimItemAttribute::OnEnter')
    769     def OnEnter(self):
    770         targetId = self.GetLastBlackboardValue(self.attributes.targetAddress)
    771         message = self.GetLastBlackboardValue(self.attributes.messageAddress)
    772         self.context.ballpark.UpdateSlimItemField(targetId, self.attributes.attributeName, message)
    773         self.SetStatusToSuccess()
    774 
    775 
    776 class StoreSlimItemAttributeToBlackboard(Task):
    777 
    778     @TimedFunction('behaviors::actions::ballparks::StoreSlimItemAttributeToBlackboard::OnEnter')
    779     def OnEnter(self):
    780         targetId = self.GetLastBlackboardValue(self.attributes.targetAddress)
    781         slimItem = self.context.ballpark.slims.get(targetId)
    782         attributeValue = getattr(slimItem, self.attributes.attributeName, None)
    783         self.SendBlackboardValue(self.attributes.messageAddress, attributeValue)
    784         self.SetStatusToSuccess()
    785 
    786 
    787 class FindPointToEscapeWarpTo(Task):
    788 
    789     @TimedFunction('behaviors::actions::ballparks::FindPointToEscapeWarpTo::OnEnter')
    790     def OnEnter(self):
    791         location = self.SelectLocation()
    792         if location:
    793             self.SendBlackboardValue(self.attributes.locationMessage, location)
    794             self.SetStatusToSuccess()
    795         else:
    796             self.SetStatusToFailed()
    797 
    798     def SelectLocation(self):
    799         return GetRandomPlacementNearRandomPlanet(self.context.ballpark.solarsystemID, 1000, 10000)
    800 
    801 
    802 class FindPointToExplore(Task):
    803 
    804     @TimedFunction('behaviors::actions::ballparks::FindPointToExplore::OnEnter')
    805     def OnEnter(self):
    806         location = self.SelectLocation()
    807         if location:
    808             self.SendBlackboardValue(self.attributes.locationMessage, location)
    809             self.status = status.TaskSuccessStatus
    810         else:
    811             self.status = status.TaskFailureStatus
    812 
    813     def SelectLocation(self):
    814         generalList = self.GetGeneralList()
    815         specificList = self._GetSpecificDungeonEntryPointsOfInterest()
    816         if len(specificList) > 0 and (len(generalList) == 0 or random() < self.attributes.pickSpecificChance):
    817             result = self._GetPointToExplore(specificList)
    818         else:
    819             result = self._GetPointToExplore(generalList)
    820         return result
    821 
    822     def GetGeneralList(self):
    823         distributionOverwrite = self.GetLastBlackboardValue(ADDITIONAL_EXPLORATION_ACTION)
    824         if distributionOverwrite:
    825             return self._GetExplorationBallsFromDistribution()
    826         behavioralList = self._GetBallsOfInterestFromGlobals(self.attributes.groupsOfInterest)
    827         distributionList = self._GetExplorationBallsFromDistribution()
    828         warpBeaconsInSystem = self._GetWarpBeaconsInSystem()
    829         dungeonArchetypeList = self._GetDungeonArchetypeEntryPointsOfInterest()
    830         return list(set(behavioralList + distributionList + warpBeaconsInSystem + dungeonArchetypeList))
    831 
    832     def _GetPointToExplore(self, warpObjectList):
    833         if len(warpObjectList) > 0:
    834             hasNoValidLocation = True
    835             count = 0
    836             objectToWarpToo = None
    837             while hasNoValidLocation and count < 10:
    838                 objectToWarpToo = choice(warpObjectList)
    839                 hasNoValidLocation = self.context.ballpark.InSameBubble(objectToWarpToo.id, self.context.myBall.id)
    840                 count += 1
    841 
    842             if objectToWarpToo:
    843                 position = self.context.ballpark.GetWarpDestinationPoint(self.context.myBall.id, objectToWarpToo.id)
    844                 return position
    845             else:
    846                 return
    847         else:
    848             return
    849 
    850     def _GetExplorationBallsFromDistribution(self):
    851         additionalExplorationGroupIDs = self.GetLastBlackboardValue(ADDITIONAL_EXPLORATION_GROUPS)
    852         return self._GetBallsOfInterestFromGlobals(additionalExplorationGroupIDs)
    853 
    854     def _GetBallsOfInterestFromGlobals(self, groups):
    855         groups = groups or []
    856         categories = self._get_categories_of_interest()
    857         if not groups and not categories:
    858             return []
    859         safe_balls_of_interest = []
    860         for ballID, ball in self._get_all_global_balls():
    861             item = get_inventory_item(self, ballID)
    862             if self._is_ball_of_interest(item, groups, categories) and self._is_safe_to_visit(item):
    863                 safe_balls_of_interest.append(ball)
    864 
    865         return safe_balls_of_interest
    866 
    867     def _GetWarpBeaconsInSystem(self):
    868         warpBeacons = self.context.ballpark.componentRegistry.GetInstancesWithComponentClass(NPC_WARP_BEACON)
    869         warpBeaconBalls = []
    870         for warpBeacon in warpBeacons:
    871             warpBeaconBalls.append(self.context.ballpark.GetBall(warpBeacon.itemID))
    872 
    873         return warpBeaconBalls
    874 
    875     def _GetDungeonArchetypeEntryPointsOfInterest(self):
    876         archetypes_of_interest = self._get_archetypes_of_interest()
    877         if archetypes_of_interest:
    878             if self.attributes.shouldConsiderUnspawnedDungeons:
    879                 return self.context.keeper.GetInstalledDungeonEntryObjectsOfArchetype(self.context.solarSystemId, archetypes_of_interest)
    880             else:
    881                 return self.context.keeper.GetDungeonEntryObjectsOfArchetype(self.context.solarSystemId, archetypes_of_interest)
    882         return []
    883 
    884     def _GetSpecificDungeonEntryPointsOfInterest(self):
    885         dungeons_interest = self._get_dungeons_of_interest()
    886         if dungeons_interest:
    887             if self.attributes.shouldConsiderUnspawnedDungeons:
    888                 return self.context.keeper.GetInstalledDungeonEntryObjectsOfDungeonID(self.context.solarSystemId, dungeons_interest)
    889             else:
    890                 return self.context.keeper.GetDungeonEntryObjectsOfDungeonID(self.context.solarSystemId, dungeons_interest)
    891         return []
    892 
    893     def _get_categories_of_interest(self):
    894         if self.HasAttribute('categoriesOfInterest'):
    895             return self.attributes.categoriesOfInterest
    896         return []
    897 
    898     def _get_archetypes_of_interest(self):
    899         if self.HasAttribute('dungeonArchetypesOfInterest'):
    900             return self.attributes.dungeonArchetypesOfInterest
    901         return []
    902 
    903     def _get_dungeons_of_interest(self):
    904         if self.HasAttribute('dungeonsOfInterest'):
    905             return self.attributes.dungeonsOfInterest
    906         return []
    907 
    908     def _get_all_global_balls(self):
    909         return self.context.ballpark.globals.iteritems()
    910 
    911     def _is_ball_of_interest(self, item, groups, categories):
    912         return item.groupID in groups or item.categoryID in categories
    913 
    914     def _is_safe_to_visit(self, item):
    915         if not self._should_avoid_unsafe_locations_in_empire_space():
    916             return True
    917         if item.groupID not in UNSAFE_EXPLORATION_GROUPS:
    918             return True
    919         if IsEvePlayerCharacter(item.ownerID):
    920             return True
    921         return SecurityClassFromLevel(self.context.ballpark.security) == securityClassZeroSec
    922 
    923     def _should_avoid_unsafe_locations_in_empire_space(self):
    924         if not self.HasAttribute('avoidUnsafeLocationsInEmpireSpace'):
    925             return False
    926         return self.attributes.avoidUnsafeLocationsInEmpireSpace
    927 
    928 
    929 class GetBallGotoPointAction(Task):
    930 
    931     @TimedFunction('behaviors::actions::ballparks::GetBallGotoPointAction::OnEnter')
    932     def OnEnter(self):
    933         ballId = self.GetLastBlackboardValue(self.attributes.ballIdAddress)
    934         ball = self.context.ballpark.GetBall(ballId)
    935         gotoPoint = (ball.gotoX, ball.gotoY, ball.gotoZ)
    936         self.SendBlackboardValue(self.attributes.gotoPointAddress, gotoPoint)
    937         self.SetStatusToSuccess()
    938 
    939 
    940 class ManageMicroWarpDriveToTarget(Task):
    941 
    942     def __init__(self, attributes):
    943         super(ManageMicroWarpDriveToTarget, self).__init__(attributes)
    944         self.microWarpDriveThread = None
    945         self.isActive = False
    946 
    947     @TimedFunction('behaviors::actions::ballparks::ManageMicroWarpDriveToTarget::OnEnter')
    948     def OnEnter(self):
    949         self.SetStatusToFailed()
    950         self.targetId = self.GetLastBlackboardValue(self.attributes.targetAddress)
    951         if self.targetId is None or not self._IsTargetInSameBubble():
    952             return
    953         if self.microWarpDriveThread is None:
    954             self.microWarpDriveThread = uthread2.start_tasklet(self._ManageMicroWarpDrive)
    955         self.SetStatusToSuccess()
    956 
    957     def _IsTargetInSameBubble(self):
    958         return self.context.ballpark.InSameBubble(self.context.myItemId, self.targetId)
    959 
    960     def _ManageMicroWarpDrive(self):
    961         while not self.IsInvalid() and self._AreBallsStillThere():
    962             shouldMicroWarpDrive = self._ShouldMicroWarpDrive()
    963             if self.isActive != shouldMicroWarpDrive:
    964                 self._SetMovementSpeed(shouldMicroWarpDrive)
    965             uthread2.SleepSim(self.attributes.microWarpDriveDuration / 1000)
    966 
    967         self.microWarpDriveThread = None
    968 
    969     def _AreBallsStillThere(self):
    970         if not self.context.ballpark.HasBall(self.targetId):
    971             return False
    972         if not self.context.ballpark.HasBall(self.context.myItemId):
    973             return False
    974         if self.context.ballpark.IsCloaked(self.targetId):
    975             return False
    976         return True
    977 
    978     def _ShouldMicroWarpDrive(self):
    979         if not self._IsOutsideMicroWarpDriveDistance():
    980             return False
    981         return random() < self.attributes.microWarpDriveChance
    982 
    983     def _IsOutsideMicroWarpDriveDistance(self):
    984         distance = self.context.ballpark.DistanceBetween(self.context.myItemId, self.targetId)
    985         microWarpdriveDistance = self._GetMicroWarpDriveDistance()
    986         return distance >= microWarpdriveDistance
    987 
    988     def _GetMicroWarpDriveDistance(self):
    989         if self.HasAttribute('microwarpdriveRangeAddress'):
    990             return self.GetLastBlackboardValue(self.attributes.microwarpdriveRangeAddress)
    991         else:
    992             orbitDistance = self.GetLastBlackboardValue(self.attributes.orbitRangeAddress)
    993             if orbitDistance < COMBAT_ORBIT_MWD_DISTANCE_THRESHOLD:
    994                 microWarpdriveDistance = orbitDistance * COMBAT_ORBIT_MWD_DISTANCE_FACTOR_SHORT_DISTANCE
    995                 return max(microWarpdriveDistance, COMBAT_ORBIT_MWD_DISTANCE_MINIMUM)
    996             microWarpdriveDistance = orbitDistance * COMBAT_ORBIT_MWD_DISTANCE_FACTOR_LONG_DISTANCE
    997             return min(microWarpdriveDistance, COMBAT_ORBIT_MWD_DISTANCE_MAXIMUM)
    998 
    999     def _SetMovementSpeed(self, shouldMicroWarpDrive):
   1000         if shouldMicroWarpDrive:
   1001             self.context.myBall.SetMovementSpeed(cruise=False)
   1002         else:
   1003             self.context.myBall.SetMovementSpeed(cruise=True)
   1004         self.isActive = shouldMicroWarpDrive
   1005 
   1006     def CleanUp(self):
   1007         if not self.IsInvalid():
   1008             self.isActive = False
   1009             self._SetMovementSpeed(False)
   1010             self.SetStatusToInvalid()
   1011 
   1012 
   1013 class ManageMicroWarpDriveToCoordinates(ManageMicroWarpDriveToTarget):
   1014 
   1015     @TimedFunction('behaviors::actions::ballparks::ManageMicroWarpDriveToCoordinates::OnEnter')
   1016     def OnEnter(self):
   1017         self.SetStatusToFailed()
   1018         self.coordinates = self.GetLastBlackboardValue(self.attributes.coordinateAddress)
   1019         if self.coordinates is None or not self._IsTargetInSameBubble():
   1020             return
   1021         if self.microWarpDriveThread is None:
   1022             self.microWarpDriveThread = uthread2.start_tasklet(self._ManageMicroWarpDrive)
   1023         self.SetStatusToSuccess()
   1024 
   1025     def _IsTargetInSameBubble(self):
   1026         myBubbleId = self.context.myBall.newBubbleId
   1027         bubbleIdAtCoordinates = self.context.ballpark.GetBubbleAtCoordinates(*self.coordinates)
   1028         return myBubbleId == bubbleIdAtCoordinates
   1029 
   1030     def _AreBallsStillThere(self):
   1031         return self.context.ballpark.HasBall(self.context.myItemId)
   1032 
   1033     def _IsOutsideMicroWarpDriveDistance(self):
   1034         ballPosition = (self.context.myBall.x, self.context.myBall.y, self.context.myBall.z)
   1035         distance = geo2.Vec3DistanceD(ballPosition, self.coordinates)
   1036         microWarpdriveDistance = self._GetMicroWarpDriveDistance()
   1037         return distance >= microWarpdriveDistance
   1038 
   1039 
   1040 class ModifyOrbitVelocity(Task):
   1041 
   1042     @TimedFunction('behaviors::actions::ballparks::ModifyOrbitVelocity::OnEnter')
   1043     def OnEnter(self):
   1044         self.SetStatusToFailed()
   1045         self.orbitVelocity = self.attributes.orbitVelocity
   1046         customVelocity = self._GetCustomOrbitVelocity()
   1047         if not customVelocity:
   1048             entityVelocity = self.context.myBall.GetEntityVelocity()
   1049             entityVelocity.set_custom_orbit_velocity(customVelocity)
   1050         self.SetStatusToSuccess()
   1051 
   1052     def _GetCustomOrbitVelocity(self):
   1053         if is_commander(self, self.context.myItemId):
   1054             slowestVelocityOfGroup = self._GetSlowestOrbitVelocityOfGroup()
   1055             if slowestVelocityOfGroup:
   1056                 return slowestVelocityOfGroup * COMBAT_ORBIT_COMMANDER_VELOCITY_FACTOR
   1057             raise BehaviorAuthoringException('Behavior %d received None or 0 as slowest combat velocity' % self.behaviorTree.GetBehaviorId())
   1058         return self.attributes.orbitVelocity
   1059 
   1060     def _GetSlowestOrbitVelocityOfGroup(self):
   1061         slowestVelocityForGroup = None
   1062         group = self.context.entityLocation.GetGroupManager().GetGroup(self.context.myEntityGroupId)
   1063         for entityId in group.GetGroupMembers():
   1064             entityItem = self.context.ballpark.inventory2.GetItem(entityId)
   1065             if not has_combat_role(self, entityItem.itemID):
   1066                 continue
   1067             entityTypeVelocity = self._GetOrbitVelocityForType(entityItem)
   1068             if entityTypeVelocity and (slowestVelocityForGroup is None or entityTypeVelocity < slowestVelocityForGroup):
   1069                 slowestVelocityForGroup = entityTypeVelocity
   1070 
   1071         return slowestVelocityForGroup
   1072 
   1073     def _GetOrbitVelocityForType(self, entityItem):
   1074         entityTypeCruiseSpeed = get_type_attribute_value(self, entityItem.typeID, attributeEntityCruiseSpeed)
   1075         entityTypeMaxVelocity = get_type_attribute_value(self, entityItem.typeID, attributeMaxVelocity)
   1076         return entityTypeCruiseSpeed or entityTypeMaxVelocity
   1077 
   1078     def CleanUp(self):
   1079         if not self.IsInvalid():
   1080             if self.orbitVelocity is not None:
   1081                 entityVelocity = self.context.myBall.GetEntityVelocity()
   1082                 entityVelocity.remove_custom_orbit_velocity()
   1083             self.SetStatusToInvalid()
   1084 
   1085 
   1086 class SelectClosestItemAndTypeByEveTypeList(Task):
   1087 
   1088     @TimedFunction('behaviors::actions::ballparks::SelectClosestItemAndTypeByEveTypeList::OnEnter')
   1089     def OnEnter(self):
   1090         self.SetStatusToFailed()
   1091         logger.debug('SelectClosestItemAndTypeByEveTypeList starting')
   1092         if self.HasValidTarget():
   1093             self.SetStatusToSuccess()
   1094             return
   1095         closestItemId, closestTypeId = self.GetClosestItemAndTypeOfInterest()
   1096         if closestItemId is not None:
   1097             self._update_blackboards(closestItemId, closestTypeId)
   1098             self.SetStatusToSuccess()
   1099         else:
   1100             logger.debug('no suitable target found')
   1101 
   1102     def GetClosestItemAndTypeOfInterest(self):
   1103         ballsOfInterest = get_id_and_types_in_bubble_by_typelist(self.context.ballpark, self.context.myBall.newBubbleId, self.attributes.eveTypeListId)
   1104         closestItemId = None
   1105         closestTypeId = None
   1106         closestDistance = 0
   1107         for ballId, typeId in ballsOfInterest:
   1108             distanceToBall = self.GetDistanceToBall(ballId)
   1109             if not closestItemId or distanceToBall < closestDistance:
   1110                 closestItemId = ballId
   1111                 closestTypeId = typeId
   1112                 closestDistance = distanceToBall
   1113 
   1114         return (closestItemId, closestTypeId)
   1115 
   1116     def GetDistanceToBall(self, ballId):
   1117         return self.context.ballpark.DistanceBetween(self.context.myBall.id, ballId)
   1118 
   1119     def HasValidTarget(self):
   1120         targetId = self.GetLastBlackboardValue(self.attributes.selectedTargetAddress)
   1121         if not targetId or not self.context.ballpark.HasBall(targetId):
   1122             return False
   1123         targetBall = self.context.ballpark.GetBall(targetId)
   1124         return targetBall.newBubbleId == self.context.myBall.newBubbleId
   1125 
   1126     def _update_blackboards(self, closestItemId, closestTypeId):
   1127         self.SendBlackboardValue(self.attributes.selectedTargetAddress, closestItemId)
   1128         self.SendBlackboardValue(self.attributes.selectedTypeAddress, closestTypeId)
   1129 
   1130 
   1131 class SelectClosestItemByEveTypeList(SelectClosestItemAndTypeByEveTypeList):
   1132 
   1133     def _update_blackboards(self, closestItemId, _):
   1134         self.SendBlackboardValue(self.attributes.selectedTargetAddress, closestItemId)
   1135 
   1136 
   1137 class GetBallLocation(Task):
   1138 
   1139     @TimedFunction('behaviors::actions::ballparks::GetBallLocation::OnEnter')
   1140     def OnEnter(self):
   1141         self.SetStatusToFailed()
   1142         targetBallId = self.GetLastBlackboardValue(self.attributes.ballIdAddress)
   1143         if targetBallId and is_ball_in_park(self, targetBallId):
   1144             targetBall = get_ball(self, targetBallId)
   1145             targetCoordinates = (targetBall.x, targetBall.y, targetBall.z)
   1146             self.SendBlackboardValue(self.attributes.coordinateAddress, targetCoordinates)
   1147             self.SetStatusToSuccess()
   1148 
   1149 
   1150 class PostOwnPosition(Task):
   1151 
   1152     @TimedFunction('behaviors::actions::ballparks::PostOwnPosition::OnEnter')
   1153     def OnEnter(self):
   1154         position = (self.context.myBall.x, self.context.myBall.y, self.context.myBall.z)
   1155         self.SendBlackboardValue(self.attributes.coordinateAddress, position)
   1156         self.SetStatusToSuccess()
   1157 
   1158 
   1159 class GotoPosition(Task):
   1160 
   1161     @TimedFunction('behaviors::actions::ballparks::GotoPosition::OnEnter')
   1162     def OnEnter(self):
   1163         position = self.GetLastBlackboardValue(self.attributes.coordinateAddress)
   1164         self.context.ballpark.GotoPoint(self.context.myItemId, *position)
   1165         self.SetStatusToSuccess()
   1166 
   1167 
   1168 class GetItemIdsInBubbleWithinRange(Task):
   1169 
   1170     @TimedFunction('behaviors::actions::ballparks::GetItemIdsInBubbleWithinRange::OnEnter')
   1171     def OnEnter(self):
   1172         self.SetStatusToSuccess()
   1173         center_ball_id = self._get_center_ball_id()
   1174         if center_ball_id is None:
   1175             self.SetStatusToFailed()
   1176             return
   1177         center_ball = get_ball(self, center_ball_id)
   1178         balls_in_bubble = get_balls_in_bubble(self, center_ball.newBubbleId)
   1179         balls_of_interest = self._get_balls_of_interest(center_ball_id, balls_in_bubble)
   1180         self.SendBlackboardValue(self.attributes.ballsWithinRangeAddress, balls_of_interest)
   1181 
   1182     def _get_center_ball_id(self):
   1183         if self.HasAttribute('centerBallAddress'):
   1184             return self.GetLastBlackboardValue(self.attributes.centerBallAddress)
   1185         return self.context.myItemId
   1186 
   1187     def _get_balls_of_interest(self, center_ball_id, balls_in_bubble):
   1188         balls_of_interest = []
   1189         for ball_id in balls_in_bubble.keys():
   1190             slim_item = get_slim_item(self, ball_id)
   1191             if not slim_item:
   1192                 continue
   1193             if any((filter_function(slim_item) for filter_function in self._get_filter_functions())):
   1194                 if self._is_ball_within_range(center_ball_id, ball_id):
   1195                     balls_of_interest.append(ball_id)
   1196 
   1197         return balls_of_interest
   1198 
   1199     def _get_filter_functions(self):
   1200         return [self._is_ball_of_category_interest, self._is_ball_of_group_interest]
   1201 
   1202     def _is_ball_of_category_interest(self, slim_item):
   1203         categories_of_interest = getattr(self.attributes, 'categoriesOfInterest', [])
   1204         return slim_item.categoryID in categories_of_interest
   1205 
   1206     def _is_ball_of_group_interest(self, slim_item):
   1207         groups_of_interest = getattr(self.attributes, 'groupsOfInterest', [])
   1208         return slim_item.groupID in groups_of_interest
   1209 
   1210     def _is_ball_within_range(self, center_ball_id, target_ball_id):
   1211         if self.HasAttribute('maxDistanceAddress'):
   1212             max_distance = self.GetLastBlackboardValue(self.attributes.maxDistanceAddress)
   1213         elif self.HasAttribute('maxDistance'):
   1214             max_distance = self.attributes.maxDistance
   1215         else:
   1216             raise BehaviorAuthoringException('Behavior Task: GetItemIdsInBubbleWithinRange is missing maxDistance for behavior=%s', self.behaviorTree.GetBehaviorId())
   1217         return is_ball_in_range(self, center_ball_id, target_ball_id, max_distance)
   1218 
   1219 
   1220 class GetWarpBubbleItemIdsBlockingMeFromWarp(Task):
   1221 
   1222     @TimedFunction('behaviors::actions::ballparks::GetWarpBubbleItemIdsBlockingMeFromWarp::OnEnter')
   1223     def OnEnter(self):
   1224         self.SetStatusToSuccess()
   1225         ship_ids = self._get_warp_bubble_owner_ship_ids()
   1226         logger.debug('Behavior: GetWarpBubbleItemIdsBlockingMeFromWarp found targets=%s', ship_ids)
   1227         self.SendBlackboardValue(self.attributes.itemIdSetAddress, ship_ids)
   1228 
   1229     def _get_warp_bubble_owner_ship_ids(self):
   1230         warp_disrupt_probe_ids, warp_disrupt_ship_ids = self._get_blocking_warp_bubble_item_ids()
   1231         warp_disrupt_ship_ids.extend(self._get_targets_for_warp_disrupt_probes(warp_disrupt_probe_ids))
   1232         return warp_disrupt_ship_ids
   1233 
   1234     def _get_blocking_warp_bubble_item_ids(self):
   1235         return GetWarpBubbleItemIdsWithinRangeOfBall(self.context.ballpark, self.context.myBall.id)
   1236 
   1237     def _get_targets_for_warp_disrupt_probes(self, warp_disrupt_probe_ids):
   1238         target_ids_of_interest = []
   1239         for probe_id in warp_disrupt_probe_ids:
   1240             source_id = self._get_targetable_source_for_probe(probe_id)
   1241             if source_id is not None:
   1242                 target_ids_of_interest.append(source_id)
   1243 
   1244         return target_ids_of_interest
   1245 
   1246     def _get_targetable_source_for_probe(self, probe_id):
   1247         probe_category_id = get_inventory_item(self, probe_id).categoryID
   1248         if probe_category_id != categoryCharge:
   1249             return probe_id
   1250         return get_slim_item(self, probe_id).sourceShipID
   1251 
   1252 
   1253 class SelectCoordinatesFromDirectionIntoDistance(Task):
   1254 
   1255     def OnEnter(self):
   1256         self.SetStatusToSuccess()
   1257         coordinates = self._get_coordinates()
   1258         self.SendBlackboardValue(self.attributes.coordinatesAddress, coordinates)
   1259 
   1260     def _get_coordinates(self):
   1261         direction = self._get_direction()
   1262         coordinates = self._get_coordinates_based_on_direction_into_distance(direction)
   1263         return coordinates
   1264 
   1265     def _get_direction(self):
   1266         direction = self.GetLastBlackboardValue(self.attributes.directionAddress)
   1267         return geo2.Vec3NormalizeD(direction)
   1268 
   1269     def _get_coordinates_based_on_direction_into_distance(self, direction):
   1270         my_ball = get_ball(self, self.context.myItemId)
   1271         coordinates = geo2.Vec3ScaleD(direction, self.attributes.distance)
   1272         return [my_ball.x + coordinates[0], my_ball.y + coordinates[1], my_ball.z + coordinates[2]]
   1273 
   1274 
   1275 class GetCoordinatesReducedByDistance(Task):
   1276 
   1277     def OnEnter(self):
   1278         self.SetStatusToFailed()
   1279         coordinates = self.GetLastBlackboardValue(self.attributes.sourceCoordinateAddress)
   1280         my_position = get_ball_position(self, self.context.myItemId)
   1281         if self.HasAttribute('distanceAddress'):
   1282             distance = self.GetLastBlackboardValue(self.attributes.distanceAddress)
   1283             if distance:
   1284                 direction = geo2.Vec3DirectionD(my_position, coordinates)
   1285                 coordinates = geo2.Vec3AddD(coordinates, geo2.Vec3ScaleD(direction, distance))
   1286         self.SendBlackboardValue(self.attributes.destinationCoordinateAddress, coordinates)
   1287         self.SetStatusToSuccess()
   1288 
   1289 
   1290 class FlyIntoRandomDirection(Task):
   1291 
   1292     def OnEnter(self):
   1293         self.context.ballpark.GotoDirection(self.context.myItemId, *RandomVector())
   1294         self.context.ballpark.SetSpeedFraction(self.context.myItemId, self.attributes.velocityFraction)
   1295         self.SetStatusToSuspended()
   1296 
   1297     def CleanUp(self):
   1298         if not self.IsInvalid():
   1299             self.context.ballpark.SetSpeedFraction(self.context.myItemId, 1.0)
   1300         self.SetStatusToInvalid()
   1301 
   1302 
   1303 class FindSpecificItemLocation(Task):
   1304 
   1305     def OnEnter(self):
   1306         location = self.select_location()
   1307         if location:
   1308             self.SendBlackboardValue(self.attributes.locationMessageAddress, location)
   1309             self.SetStatusToSuccess()
   1310         else:
   1311             self.SetStatusToFailed()
   1312 
   1313     def select_location(self):
   1314         specific_slims = self._get_slims_by_typelist()
   1315         if len(specific_slims) > 0:
   1316             chosen_slim_item = choice(specific_slims)
   1317             self.SendBlackboardValue(self.attributes.chosenItemOfInterestAddress, chosen_slim_item)
   1318             coordinates = self.context.ballpark.GetWarpDestinationPoint(self.context.myItemId, chosen_slim_item)
   1319             return coordinates
   1320         else:
   1321             return None
   1322 
   1323     def _get_slims_by_typelist(self):
   1324         slims_list = []
   1325         if self.attributes.specificInterestTypelist is not None:
   1326             type_ids = evetypes.GetTypeIDsByListID(self.attributes.specificInterestTypelist)
   1327             for slim_item in self.context.ballpark.slims.itervalues():
   1328                 if slim_item.typeID in type_ids:
   1329                     slims_list.append(slim_item.itemID)
   1330 
   1331         self.SendBlackboardValue(self.attributes.itemsOfInterestAddress, slims_list)
   1332         return slims_list
   1333 
   1334 
   1335 class SelectAllItemIdsInBubbleByTypeList(Task):
   1336 
   1337     @TimedFunction('behaviors::actions::ballparks::SelectAllItemIdInBubbleByEveTypeList::OnEnter')
   1338     def OnEnter(self):
   1339         balls_of_interest = get_id_and_types_in_bubble_by_typelist(self.context.ballpark, self.context.myBall.newBubbleId, self.attributes.typeListId)
   1340         item_ids = [ item_id for item_id, type_id in balls_of_interest ]
   1341         self.SendBlackboardValue(self.attributes.selectedTargetsAddress, item_ids)
   1342         self.SetStatusToSuccess()
   1343 
   1344 
   1345 class CountItemsInBubbleByTypeList(Task):
   1346 
   1347     @TimedFunction('behaviors::actions::ballparks::SelectAllItemIdInBubbleByEveTypeList::OnEnter')
   1348     def OnEnter(self):
   1349         balls_of_interest = get_id_and_types_in_bubble_by_typelist(self.context.ballpark, self.context.myBall.newBubbleId, self.attributes.typeListId)
   1350         self.SendBlackboardValue(self.attributes.itemCountAddress, len(balls_of_interest))
   1351         self.SetStatusToSuccess()
   1352 
   1353 
   1354 class TransferItemToLocation(Task):
   1355 
   1356     @TimedFunction('behaviors::actions::ballparks::TransferItemToLocation::OnEnter')
   1357     def OnEnter(self):
   1358         self.status = status.TaskFailureStatus
   1359         item_id = self.GetLastBlackboardValue(self.attributes.itemIdAddress)
   1360         location = self.GetLastBlackboardValue(self.attributes.locationAddress)
   1361         if location and item_id:
   1362             self.context.ballpark.ChangeBallPosition(item_id, *location)
   1363             self.status = status.TaskSuccessStatus