group.py (13921B)
1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\packages\behaviors\actions\group.py 2 from collections import defaultdict 3 import uthread2 4 from behaviors.blackboards import BlackboardDeletedError 5 from behaviors.const.behaviorroles import COMBAT_ROLES 6 from behaviors.const.combat import COMBAT_MAX_EFFECTIVE_FALLOFF_DISTANCE_FACTOR 7 from behaviors.const.combat import COMBAT_MIN_EFFECTIVE_COMBAT_RANGE_FACTOR 8 from behaviors.const.loot import EXTRA_LOOT_TABLES_BY_OWNER_ID_AND_ENTITY_GROUP_ID 9 from behaviors.groups.mixin import GroupTaskMixin 10 from behaviors.tasks import Task 11 from behaviors.trees.combat import ROLE_ADDRESS, COMBAT_ORBIT_RANGE, COMBAT_WARP_AT_DISTANCE 12 from behaviors.utility.ballparks import get_ship_balls_in_bubble, get_distance_between, get_ball, is_target_valid, get_slim_item, is_ball_cloaked 13 from behaviors.utility.dogmatic import type_has_effect, get_entity_max_missile_range 14 from behaviors.utility.inventory import get_type_id_by_item_id 15 from ccpProfile import TimedFunction 16 from dogma.const import effectMissileLaunchingForEntity, effectTargetAttack 17 from dogma.effects.restricted.util import get_effect_range_with_falloff_multiplier 18 from lootutils.grouploot import AssignAdditionalLootToGroupEntities 19 from random import choice 20 import logging 21 logger = logging.getLogger(__name__) 22 UNSPAWN_GROUP_SLEEP_DELAY_SEC = 0.2 23 24 class UpdateMaxMemberCount(Task, GroupTaskMixin): 25 26 @TimedFunction('behaviors::actions::group::UpdateMaxMemberCount::OnEnter') 27 def OnEnter(self): 28 current_count = self.GetEntityGroup().Count() 29 last_count = self.GetLastBlackboardValue(self.attributes.maxMemberCountAddress) 30 if last_count is None: 31 self.SendBlackboardValue(self.attributes.maxMemberCountAddress, current_count) 32 elif current_count > last_count: 33 self.SendBlackboardValue(self.attributes.maxMemberCountAddress, current_count) 34 self.SetStatusToSuccess() 35 36 37 class UpdateCurrentMemberCount(Task, GroupTaskMixin): 38 39 @TimedFunction('behaviors::actions::group::UpdateCurrentMemberCount::OnEnter') 40 def OnEnter(self): 41 current_count = self.GetEntityGroup().Count() 42 last_count = self.GetLastBlackboardValue(self.attributes.currentMemberCountAddress) 43 if current_count != last_count: 44 self.SendBlackboardValue(self.attributes.currentMemberCountAddress, current_count) 45 self.SetStatusToSuccess() 46 47 48 class ChooseCommander(Task, GroupTaskMixin): 49 50 @TimedFunction('behaviors::actions::group::ChooseCommander::OnEnter') 51 def OnEnter(self): 52 self.SetStatusToSuccess() 53 requiredRole = getattr(self.attributes, 'requiredCommanderRole', None) 54 existingCommanderItemId = self.GetLastBlackboardValue(self.attributes.commanderAddress) 55 chosenGroupMemberId = self._ChooseCommanderFromGroup(requiredRole, existingCommanderItemId) 56 if chosenGroupMemberId == existingCommanderItemId: 57 return 58 self.SendBlackboardValue(self.attributes.commanderAddress, chosenGroupMemberId) 59 if chosenGroupMemberId: 60 self._ResetCommanderNavigationRangesAndBehaviorTree(chosenGroupMemberId) 61 62 def _ChooseCommanderFromGroup(self, requiredRole, existingCommanderItemId): 63 membersWithCombatRoles = self._GetMembersWithCombatRoles() 64 self._RemoveInvalidCommanderRoles(membersWithCombatRoles) 65 if not membersWithCombatRoles: 66 return None 67 if self._IsCurrentCommanderValid(requiredRole, existingCommanderItemId, membersWithCombatRoles): 68 return existingCommanderItemId 69 if requiredRole and requiredRole in membersWithCombatRoles: 70 return choice(membersWithCombatRoles[requiredRole]) 71 randomRole = choice(membersWithCombatRoles.keys()) 72 return choice(membersWithCombatRoles[randomRole]) 73 74 def _RemoveInvalidCommanderRoles(self, membersWithCombatRoles): 75 for invalidRole in self.attributes.invalidCommanderRoles: 76 if invalidRole in membersWithCombatRoles: 77 del membersWithCombatRoles[invalidRole] 78 79 def _IsCurrentCommanderValid(self, requiredRole, existingCommanderItemId, membersWithCombatRoles): 80 if not existingCommanderItemId or not self.context.ballpark.HasBall(existingCommanderItemId): 81 return False 82 if not self._IsCurrentCommanderRoleValid(requiredRole, existingCommanderItemId, membersWithCombatRoles): 83 return False 84 return True 85 86 def _IsCurrentCommanderRoleValid(self, requiredRole, existingCommanderItemId, membersWithCombatRoles): 87 if not requiredRole or requiredRole not in membersWithCombatRoles: 88 return True 89 try: 90 commandersRole = self._GetRoleByGroupMemberId(existingCommanderItemId) 91 if commandersRole == requiredRole: 92 return True 93 except BlackboardDeletedError: 94 return False 95 96 return False 97 98 def _GetRoleByGroupMemberId(self, groupMemberId): 99 _, channelAddressName = ROLE_ADDRESS 100 return self.GetMessageChannelForItemId(groupMemberId, channelAddressName).GetLastMessageValue() 101 102 def _GetMembersWithCombatRoles(self): 103 membersWithCombatRoles = defaultdict(list) 104 memberIds = self.GetMemberIdList() 105 for groupMemberId in memberIds: 106 try: 107 memberRole = self._GetRoleByGroupMemberId(groupMemberId) 108 if memberRole in COMBAT_ROLES: 109 membersWithCombatRoles[memberRole].append(groupMemberId) 110 except BlackboardDeletedError: 111 continue 112 113 return membersWithCombatRoles 114 115 def _ResetCommanderNavigationRangesAndBehaviorTree(self, chosenGroupMemberId): 116 _, combatOrbitAddress = COMBAT_ORBIT_RANGE 117 _, combatWarpAtDistanceAddress = COMBAT_WARP_AT_DISTANCE 118 self.GetMessageChannelForItemId(chosenGroupMemberId, combatOrbitAddress).SendMessage(None) 119 self.GetMessageChannelForItemId(chosenGroupMemberId, combatWarpAtDistanceAddress).SendMessage(None) 120 commanderBehaviorTree = self.context.entityLocation.GetBehaviorTreeManager().GetEntryByKey(chosenGroupMemberId) 121 commanderBehaviorTree.ResetTree() 122 123 124 class GetMemberList(Task, GroupTaskMixin): 125 126 @TimedFunction('behaviors::actions::group::GetMemberList::OnEnter') 127 def OnEnter(self): 128 member_id_list = self.GetMemberIdList() 129 self.SendBlackboardValue(self.attributes.memberListAddress, member_id_list) 130 self.SetStatusToSuccess() 131 132 133 class AssignExtraLootTableByOwnerIdAndEntityGroupId(Task, GroupTaskMixin): 134 135 @TimedFunction('behaviors::actions::group::AssignExtraLootTableByOwnerIdAndEntityGroupId::OnEnter') 136 def OnEnter(self): 137 self.SetStatusToSuccess() 138 newExtraLoot = self._GetExtraLootDataByOwnerId() 139 if not newExtraLoot: 140 return 141 extraLoot = self.GetLastBlackboardValue(self.attributes.additionalLootTablesAddress) or defaultdict(set) 142 for entityGroupId, lootTableIds in newExtraLoot.iteritems(): 143 extraLoot[entityGroupId].update(lootTableIds) 144 145 self.SendBlackboardValue(self.attributes.additionalLootTablesAddress, extraLoot) 146 AssignAdditionalLootToGroupEntities(self.context.entityLocation, self.context.myEntityGroupId) 147 148 def _GetExtraLootDataByOwnerId(self): 149 return EXTRA_LOOT_TABLES_BY_OWNER_ID_AND_ENTITY_GROUP_ID.get(self.GetGroupOwnerId()) 150 151 152 class UnspawnGroup(Task, GroupTaskMixin): 153 154 @TimedFunction('behaviors::actions::group::UnspawnGroup::OnEnter') 155 def OnEnter(self): 156 self.SetStatusToSuccess() 157 uthread2.StartTasklet(self._UnspawnGroup) 158 159 def _UnspawnGroup(self): 160 uthread2.sleep(UNSPAWN_GROUP_SLEEP_DELAY_SEC) 161 self.context.entityLocation.UnspawnBehaviorEntityGroupWithRespawnEnabled(self.context.myEntityGroupId) 162 163 164 class PostWeaponRangesForGroup(Task, GroupTaskMixin): 165 166 @TimedFunction('behaviors::actions::group::PostWeaponRangesForGroup::OnEnter') 167 def OnEnter(self): 168 self.SetStatusToSuccess() 169 max_effective_weapon_range = self._get_max_effective_weapon_range() 170 self.SendBlackboardValue(self.attributes.maxEffectiveWeaponRangeAddress, max_effective_weapon_range) 171 min_effective_weapon_range = max_effective_weapon_range * COMBAT_MIN_EFFECTIVE_COMBAT_RANGE_FACTOR 172 self.SendBlackboardValue(self.attributes.minEffectiveWeaponRangeAddress, min_effective_weapon_range) 173 174 def _get_max_effective_weapon_range(self): 175 max_effective_weapon_range_for_group = [] 176 entities_to_consider = self._get_entities_to_consider() 177 if not entities_to_consider: 178 return 0 179 for entity_id in entities_to_consider: 180 entity_type_id = get_type_id_by_item_id(self, entity_id) 181 if entity_type_id is None: 182 continue 183 max_effective_weapon_range_for_entity = self._get_max_effective_weapon_range_for_entity(entity_id, entity_type_id) 184 if max_effective_weapon_range_for_entity == 0: 185 continue 186 max_effective_weapon_range_for_group.append(max_effective_weapon_range_for_entity) 187 logger.debug('Behavior: max effective weaponRange for entityType=%s set to=%s', entity_type_id, max_effective_weapon_range_for_entity) 188 189 if not max_effective_weapon_range_for_group: 190 return 0 191 max_effective_weapon_range_for_group.sort() 192 return max_effective_weapon_range_for_group[len(max_effective_weapon_range_for_group) / 2] 193 194 def _get_entities_to_consider(self): 195 roles_to_consider = self._get_roles_to_consider() 196 if not roles_to_consider: 197 return self.GetMemberIdList() 198 entities_by_role = self._get_entities_by_role() 199 entities_to_consider = [] 200 if entities_by_role is None: 201 logger.debug('Behavior: no entities by role posted yet for PostWeaponRangesForGroup, most likely pending initial spawns') 202 return entities_to_consider 203 for role in roles_to_consider: 204 if role in entities_by_role: 205 entities_to_consider.extend(entities_by_role[role]) 206 207 return entities_to_consider 208 209 def _get_roles_to_consider(self): 210 return self.attributes.rolesToConsider 211 212 def _get_entities_by_role(self): 213 return self.GetLastBlackboardValue(self.attributes.entitiesByRole) 214 215 def _get_max_effective_weapon_range_for_entity(self, entity_id, entity_type_id): 216 turret_range = self._get_max_effective_turret_range_for_entity(entity_id, entity_type_id) 217 missile_range = self._get_max_effective_missile_range_for_entity(entity_type_id) 218 if turret_range == 0 and missile_range == 0: 219 return 0 220 if missile_range == 0 or turret_range < missile_range: 221 return turret_range 222 return missile_range 223 224 def _get_max_effective_turret_range_for_entity(self, entity_id, entity_type_id): 225 if not type_has_effect(self, entity_type_id, effectTargetAttack): 226 return 0 227 return get_effect_range_with_falloff_multiplier(self.context.dogmaLM, effectTargetAttack, entity_id, COMBAT_MAX_EFFECTIVE_FALLOFF_DISTANCE_FACTOR) 228 229 def _get_max_effective_missile_range_for_entity(self, entity_type_id): 230 if not type_has_effect(self, entity_type_id, effectMissileLaunchingForEntity): 231 return 0 232 return get_entity_max_missile_range(self, entity_type_id) 233 234 235 class GetClosestPlayerBallToGroupLeader(Task, GroupTaskMixin): 236 237 def OnEnter(self): 238 self.SetStatusToSuccess() 239 closest_player_ball_id = self._get_closest_player_ball_id_to_group() 240 self.SendBlackboardValue(self.attributes.ballIdAddress, closest_player_ball_id) 241 242 def _get_closest_player_ball_id_to_group(self): 243 selected_group_member_id = self._select_group_member() 244 bubble_id = get_ball(self, selected_group_member_id).newBubbleId 245 return self._get_closest_player_to_member(selected_group_member_id, bubble_id) 246 247 def _get_closest_player_to_member(self, selected_group_member_id, bubble_id): 248 closest_player_ball_id = None 249 shortest_distance = None 250 for player_ship_ball_id in get_ship_balls_in_bubble(self, bubble_id): 251 if not is_target_valid(self, player_ship_ball_id): 252 continue 253 distance = get_distance_between(self, selected_group_member_id, player_ship_ball_id) 254 if shortest_distance is None or distance < shortest_distance: 255 closest_player_ball_id = player_ship_ball_id 256 shortest_distance = distance 257 258 return closest_player_ball_id 259 260 def _select_group_member(self): 261 if not self.HasAttribute('commanderAddress'): 262 return self.GetMemberIdList()[0] 263 commander_id = self.GetLastBlackboardValue(self.attributes.commanderAddress) 264 return commander_id or self.GetMemberIdList()[0] 265 266 267 class IsAnyPlayerShipOnGridWithGroupMember(Task, GroupTaskMixin): 268 269 def OnEnter(self): 270 self.SetStatusToSuccess() 271 bubbles = self.GetMemberBubbleSet() 272 for bubble_id in bubbles: 273 if self._bubble_contains_player(bubble_id): 274 self.SendBlackboardValue(self.attributes.resultAddress, True) 275 return 276 277 self.SendBlackboardValue(self.attributes.resultAddress, False) 278 279 def _is_target_valid(self, target_id): 280 if target_id is None: 281 return False 282 slim_item = get_slim_item(self, target_id) 283 if slim_item is None: 284 return False 285 if not self.attributes.includeCloaked and is_ball_cloaked(self, target_id): 286 return False 287 return True 288 289 def _bubble_contains_player(self, bubble_id): 290 for player_ship_ball_id in get_ship_balls_in_bubble(self, bubble_id): 291 if self._is_target_valid(player_ship_ball_id): 292 return True 293 294 return False