// These utilities are meant for strictly non-player, non-team NPCs. // These functions are in their own file because they are only intended // for use with NPCs who's logic has been overriden from the original // AI code, and who's code resides in files with the AI_ prefix. // leave this line at the top of all AI_xxxx.cpp files for PCH reasons... #include "g_headers.h" #include "b_local.h" #include "g_nav.h" #include "g_navigator.h" #define MAX_RADIUS_ENTS 128 #define DEFAULT_RADIUS 45 extern CNavigator navigator; extern cvar_t *d_noGroupAI; qboolean AI_ValidateGroupMember( AIGroupInfo_t *group, gentity_t *member ); /* ------------------------- AI_GetGroupSize ------------------------- */ int AI_GetGroupSize( vec3_t origin, int radius, team_t playerTeam, gentity_t *avoid ) { gentity_t *radiusEnts[ MAX_RADIUS_ENTS ]; vec3_t mins, maxs; int numEnts, realCount = 0; //Setup the bbox to search in for ( int i = 0; i < 3; i++ ) { mins[i] = origin[i] - radius; maxs[i] = origin[i] + radius; } //Get the number of entities in a given space numEnts = gi.EntitiesInBox( mins, maxs, radiusEnts, MAX_RADIUS_ENTS ); //Cull this list for ( int j = 0; j < numEnts; j++ ) { //Validate clients if ( radiusEnts[ j ]->client == NULL ) continue; //Skip the requested avoid ent if present if ( ( avoid != NULL ) && ( radiusEnts[ j ] == avoid ) ) continue; //Must be on the same team if ( radiusEnts[ j ]->client->playerTeam != playerTeam ) continue; //Must be alive if ( radiusEnts[ j ]->health <= 0 ) continue; realCount++; } return realCount; } //Overload int AI_GetGroupSize( gentity_t *ent, int radius ) { if ( ( ent == NULL ) || ( ent->client == NULL ) ) return -1; return AI_GetGroupSize( ent->currentOrigin, radius, ent->client->playerTeam, ent ); } extern int NAV_FindClosestWaypointForPoint( gentity_t *ent, vec3_t point ); int AI_ClosestGroupEntityNumToPoint( AIGroupInfo_t &group, vec3_t point ) { int markerWP = WAYPOINT_NONE; int cost, bestCost = Q3_INFINITE; int closest = ENTITYNUM_NONE; if ( &group == NULL || group.numGroup <= 0 ) { return ENTITYNUM_NONE; } markerWP = NAV_FindClosestWaypointForPoint( &g_entities[group.member[0].number], point ); if ( markerWP == WAYPOINT_NONE ) { return ENTITYNUM_NONE; } for ( int i = 0; i < group.numGroup; i++ ) { cost = navigator.GetPathCost( group.member[i].waypoint, markerWP ); if ( cost < bestCost ) { bestCost = cost; closest = group.member[i].number; } } return closest; } void AI_SetClosestBuddy( AIGroupInfo_t *group ) { int i, j; int dist, bestDist; for ( i = 0; i < group->numGroup; i++ ) { group->member[i].closestBuddy = ENTITYNUM_NONE; bestDist = Q3_INFINITE; for ( j = 0; j < group->numGroup; j++ ) { dist = DistanceSquared( g_entities[group->member[i].number].currentOrigin, g_entities[group->member[j].number].currentOrigin ); if ( dist < bestDist ) { bestDist = dist; group->member[i].closestBuddy = group->member[j].number; } } } } void AI_SortGroupByPathCostToEnemy( AIGroupInfo_t *group ) { AIGroupMember_t bestMembers[MAX_GROUP_MEMBERS]; int i, j, k; qboolean sort = qfalse; if ( group->enemy != NULL ) {//FIXME: just use enemy->waypoint? group->enemyWP = NAV_FindClosestWaypointForEnt( group->enemy, WAYPOINT_NONE ); } else { group->enemyWP = WAYPOINT_NONE; } for ( i = 0; i < group->numGroup; i++ ) { if ( group->enemyWP == WAYPOINT_NONE ) {//FIXME: just use member->waypoint? group->member[i].waypoint = WAYPOINT_NONE; group->member[i].pathCostToEnemy = Q3_INFINITE; } else {//FIXME: just use member->waypoint? group->member[i].waypoint = NAV_FindClosestWaypointForEnt( group->enemy, WAYPOINT_NONE ); if ( group->member[i].waypoint != WAYPOINT_NONE ) { group->member[i].pathCostToEnemy = navigator.GetPathCost( group->member[i].waypoint, group->enemyWP ); //at least one of us has a path, so do sorting sort = qtrue; } else { group->member[i].pathCostToEnemy = Q3_INFINITE; } } } //Now sort if ( sort ) { //initialize bestMembers data for ( j = 0; j < group->numGroup; j++ ) { bestMembers[j].number = ENTITYNUM_NONE; } for ( i = 0; i < group->numGroup; i++ ) { for ( j = 0; j < group->numGroup; j++ ) { if ( bestMembers[j].number != ENTITYNUM_NONE ) {//slot occupied if ( group->member[i].pathCostToEnemy < bestMembers[j].pathCostToEnemy ) {//this guy has a shorter path than the one currenly in this spot, bump him and put myself in here for ( k = group->numGroup; k > j; k++ ) { memcpy( &bestMembers[k], &bestMembers[k-1], sizeof( bestMembers[k] ) ); } memcpy( &bestMembers[j], &group->member[i], sizeof( bestMembers[j] ) ); break; } } else {//slot unoccupied, reached end of list, throw self in here memcpy( &bestMembers[j], &group->member[i], sizeof( bestMembers[j] ) ); break; } } } //Okay, now bestMembers is a sorted list, just copy it into group->members for ( i = 0; i < group->numGroup; i++ ) { memcpy( &group->member[i], &bestMembers[i], sizeof( group->member[i] ) ); } } } qboolean AI_FindSelfInPreviousGroup( gentity_t *self ) {//go through other groups made this frame and see if any of those contain me already int i, j; for ( i = 0; i < MAX_FRAME_GROUPS; i++ ) { if ( level.groups[i].numGroup && level.groups[i].enemy != NULL ) {//make a new one for ( j = 0; j < level.groups[i].numGroup; j++ ) { if ( level.groups[i].member[j].number == self->s.number ) { self->NPC->group = &level.groups[i]; return qtrue; } } } } return qfalse; } void AI_InsertGroupMember( AIGroupInfo_t *group, gentity_t *member ) { //okay, you know what? Check this damn group and make sure we're not already in here! for ( int i = 0; i < group->numGroup; i++ ) { if ( group->member[i].number == member->s.number ) {//already in here break; } } if ( i < group->numGroup ) {//found him in group already } else {//add him in group->member[group->numGroup++].number = member->s.number; group->numState[member->NPC->squadState]++; } if ( !group->commander || (member->NPC->rank > group->commander->NPC->rank) ) {//keep track of highest rank group->commander = member; } member->NPC->group = group; } qboolean AI_TryJoinPreviousGroup( gentity_t *self ) {//go through other groups made this frame and see if any of those have the same enemy as me... if so, add me in! int i; for ( i = 0; i < MAX_FRAME_GROUPS; i++ ) { if ( level.groups[i].numGroup && level.groups[i].numGroup < (MAX_GROUP_MEMBERS - 1) && level.groups[i].enemy != NULL && level.groups[i].enemy == self->enemy ) {//has members, not full and has my enemy if ( AI_ValidateGroupMember( &level.groups[i], self ) ) {//I am a valid member for this group AI_InsertGroupMember( &level.groups[i], self ); return qtrue; } } } return qfalse; } qboolean AI_GetNextEmptyGroup( gentity_t *self ) { if ( AI_FindSelfInPreviousGroup( self ) ) {//already in one, no need to make a new one return qfalse; } if ( AI_TryJoinPreviousGroup( self ) ) {//try to just put us in one that already exists return qfalse; } //okay, make a whole new one, then for ( int i = 0; i < MAX_FRAME_GROUPS; i++ ) { if ( !level.groups[i].numGroup ) {//make a new one self->NPC->group = &level.groups[i]; return qtrue; } } //if ( i >= MAX_FRAME_GROUPS ) {//WTF? Out of groups! self->NPC->group = NULL; return qfalse; } } qboolean AI_ValidateGroupMember( AIGroupInfo_t *group, gentity_t *member ) { //Validate ents if ( member == NULL ) return qfalse; //Validate clients if ( member->client == NULL ) return qfalse; //Validate NPCs if ( member->NPC == NULL ) return qfalse; //must be aware if ( member->NPC->confusionTime > level.time ) return qfalse; //must be allowed to join groups if ( member->NPC->scriptFlags&SCF_NO_GROUPS ) return qfalse; //Must not be in another group if ( member->NPC->group != NULL && member->NPC->group != group ) {//FIXME: if that group's enemy is mine, why not absorb that group into mine? return qfalse; } //Must be alive if ( member->health <= 0 ) return qfalse; //can't be in an emplaced gun if( member->s.eFlags & EF_LOCKED_TO_WEAPON ) return qfalse; //Must be on the same team if ( member->client->playerTeam != group->team ) return qfalse; if ( member->client->ps.weapon == WP_SABER ||//!= self->s.weapon ) member->client->ps.weapon == WP_THERMAL || member->client->ps.weapon == WP_DISRUPTOR || member->client->ps.weapon == WP_EMPLACED_GUN || member->client->ps.weapon == WP_BOT_LASER || // Probe droid - Laser blast member->client->ps.weapon == WP_MELEE || member->client->ps.weapon == WP_TURRET || // turret guns member->client->ps.weapon == WP_ATST_MAIN || member->client->ps.weapon == WP_ATST_SIDE || member->client->ps.weapon == WP_TIE_FIGHTER ) {//not really a squad-type guy return qfalse; } if ( member->client->NPC_class == CLASS_ATST || member->client->NPC_class == CLASS_PROBE || member->client->NPC_class == CLASS_SEEKER || member->client->NPC_class == CLASS_REMOTE || member->client->NPC_class == CLASS_SENTRY || member->client->NPC_class == CLASS_INTERROGATOR || member->client->NPC_class == CLASS_MINEMONSTER || member->client->NPC_class == CLASS_HOWLER || member->client->NPC_class == CLASS_MARK1 || member->client->NPC_class == CLASS_MARK2 ) {//these kinds of enemies don't actually use this group AI return qfalse; } //should have same enemy if ( member->enemy != group->enemy ) { if ( member->enemy != NULL ) {//he's fighting someone else, leave him out return qfalse; } if ( !gi.inPVS( member->currentOrigin, group->enemy->currentOrigin ) ) {//not within PVS of the group enemy return qfalse; } } //must be actually in combat mode if ( !TIMER_Done( member, "interrogating" ) ) return qfalse; //FIXME: need to have a route to enemy and/or clear shot? return qtrue; } /* ------------------------- AI_GetGroup ------------------------- */ //#define MAX_WAITERS 128 void AI_GetGroup( gentity_t *self ) { int i; gentity_t *member;//, *waiter; //int waiters[MAX_WAITERS]; if ( !self || !self->NPC ) { return; } if ( d_noGroupAI->integer ) { self->NPC->group = NULL; return; } if ( !self->client ) { self->NPC->group = NULL; return; } if ( self->NPC->scriptFlags&SCF_NO_GROUPS ) { self->NPC->group = NULL; return; } if ( self->enemy && (!self->enemy->client || (level.time - self->NPC->enemyLastSeenTime > 7000 ))) { self->NPC->group = NULL; return; } if ( !AI_GetNextEmptyGroup( self ) ) {//either no more groups left or we're already in a group built earlier return; } //create a new one memset( self->NPC->group, 0, sizeof( AIGroupInfo_t ) ); self->NPC->group->enemy = self->enemy; self->NPC->group->team = self->client->playerTeam; self->NPC->group->processed = qfalse; self->NPC->group->commander = self; self->NPC->group->memberValidateTime = level.time + 2000; self->NPC->group->activeMemberNum = 0; if ( self->NPC->group->enemy ) { self->NPC->group->lastSeenEnemyTime = level.time; self->NPC->group->lastClearShotTime = level.time; VectorCopy( self->NPC->group->enemy->currentOrigin, self->NPC->group->enemyLastSeenPos ); } for ( i = 0, member = &g_entities[0]; i < globals.num_entities ; i++, member++) { if ( !AI_ValidateGroupMember( self->NPC->group, member ) ) {//FIXME: keep track of those who aren't angry yet and see if we should wake them after we assemble the core group continue; } //store it AI_InsertGroupMember( self->NPC->group, member ); if ( self->NPC->group->numGroup >= (MAX_GROUP_MEMBERS - 1) ) {//full break; } } /* //now go through waiters and see if any should join the group //NOTE: Some should hang back and probably not attack, so we can ambush //NOTE: only do this if calling for reinforcements? for ( i = 0; i < numWaiters; i++ ) { waiter = &g_entities[waiters[i]]; for ( j = 0; j < self->NPC->group->numGroup; j++ ) { member = &g_entities[self->NPC->group->member[j]; if ( gi.inPVS( waiter->currentOrigin, member->currentOrigin ) ) {//this waiter is within PVS of a current member } } } */ if ( self->NPC->group->numGroup <= 0 ) {//none in group self->NPC->group = NULL; return; } AI_SortGroupByPathCostToEnemy( self->NPC->group ); AI_SetClosestBuddy( self->NPC->group ); } void AI_DeleteGroupMember( AIGroupInfo_t *group, int memberNum ) { if ( group->commander && group->commander->s.number == group->member[memberNum].number ) { group->commander = NULL; } for ( int i = memberNum; i < (group->numGroup-1); i++ ) { memcpy( &group->member[i], &group->member[i+1], sizeof( group->member[i] ) ); } if ( memberNum < group->activeMemberNum ) { group->activeMemberNum--; if ( group->activeMemberNum < 0 ) { group->activeMemberNum = 0; } } group->numGroup--; if ( group->numGroup < 0 ) { group->numGroup = 0; } } void AI_DeleteSelfFromGroup( gentity_t *self ) { //FIXME: if killed, keep track of how many in group killed? To affect morale? for ( int i = 0; i < self->NPC->group->numGroup; i++ ) { if ( self->NPC->group->member[i].number == self->s.number ) { AI_DeleteGroupMember( self->NPC->group, i ); return; } } } extern void ST_AggressionAdjust( gentity_t *self, int change ); extern void ST_MarkToCover( gentity_t *self ); extern void ST_StartFlee( gentity_t *self, gentity_t *enemy, vec3_t dangerPoint, int dangerLevel, int minTime, int maxTime ); void AI_GroupMemberKilled( gentity_t *self ) { AIGroupInfo_t *group = self->NPC->group; gentity_t *member; qboolean noflee = qfalse; if ( !group ) {//what group? return; } if ( !self || !self->NPC || self->NPC->rank < RANK_ENSIGN ) {//I'm not an officer, let's not really care for now return; } //temporarily drop group morale for a few seconds group->moraleAdjust -= self->NPC->rank; //go through and drop aggression on my teammates (more cover, worse aim) for ( int i = 0; i < group->numGroup; i++ ) { member = &g_entities[group->member[i].number]; if ( member == self ) { continue; } if ( member->NPC->rank > RANK_ENSIGN ) {//officers do not panic noflee = qtrue; } else { ST_AggressionAdjust( member, -1 ); member->NPC->currentAim -= Q_irand( 0, 2);//drop their aim accuracy } } //okay, if I'm the group commander, make everyone else flee if ( group->commander != self ) {//I'm not the commander... hmm, should maybe a couple flee... maybe those near me? return; } //now see if there is another of sufficient rank to keep them from fleeing if ( !noflee ) { self->NPC->group->speechDebounceTime = 0; for ( int i = 0; i < group->numGroup; i++ ) { member = &g_entities[group->member[i].number]; if ( member == self ) { continue; } if ( member->NPC->rank < RANK_ENSIGN ) {//grunt if ( group->enemy && DistanceSquared( member->currentOrigin, group->enemy->currentOrigin ) < 65536/*256*256*/ ) {//those close to enemy run away! ST_StartFlee( member, group->enemy, member->currentOrigin, AEL_DANGER_GREAT, 3000, 5000 ); } else if ( DistanceSquared( member->currentOrigin, self->currentOrigin ) < 65536/*256*256*/ ) {//those close to me run away! ST_StartFlee( member, group->enemy, member->currentOrigin, AEL_DANGER_GREAT, 3000, 5000 ); } else {//else, maybe just a random chance if ( Q_irand( 0, self->NPC->rank ) > member->NPC->rank ) {//lower rank they are, higher rank I am, more likely they are to flee ST_StartFlee( member, group->enemy, member->currentOrigin, AEL_DANGER_GREAT, 3000, 5000 ); } else { ST_MarkToCover( member ); } } member->NPC->currentAim -= Q_irand( 1, 3 );//drop their aim accuracy even more } member->NPC->currentAim -= Q_irand( 1, 3 );//drop their aim accuracy even more } } } void AI_GroupUpdateEnemyLastSeen( AIGroupInfo_t *group, vec3_t spot ) { if ( !group ) { return; } group->lastSeenEnemyTime = level.time; VectorCopy( spot, group->enemyLastSeenPos ); } void AI_GroupUpdateClearShotTime( AIGroupInfo_t *group ) { if ( !group ) { return; } group->lastClearShotTime = level.time; } void AI_GroupUpdateSquadstates( AIGroupInfo_t *group, gentity_t *member, int newSquadState ) { if ( !group ) { member->NPC->squadState = newSquadState; return; } for ( int i = 0; i < group->numGroup; i++ ) { if ( group->member[i].number == member->s.number ) { group->numState[member->NPC->squadState]--; member->NPC->squadState = newSquadState; group->numState[member->NPC->squadState]++; return; } } } qboolean AI_RefreshGroup( AIGroupInfo_t *group ) { gentity_t *member; int i;//, j; //see if we should merge with another group for ( i = 0; i < MAX_FRAME_GROUPS; i++ ) { if ( &level.groups[i] == group ) { break; } else { if ( level.groups[i].enemy == group->enemy ) {//2 groups with same enemy if ( level.groups[i].numGroup+group->numGroup < (MAX_GROUP_MEMBERS - 1) ) {//combining the members would fit in one group //combine the members of mine into theirs for ( int j = 0; j < group->numGroup; j++ ) { member = &g_entities[group->member[j].number]; //remove this member from this group AI_DeleteGroupMember( group, j ); //keep marker at same place since we deleted this guy and shifted everyone up one j--; //add them to the earlier group AI_InsertGroupMember( &level.groups[i], member ); } //return and delete this group return qfalse; } } } } //clear numStates for ( i = 0; i < NUM_SQUAD_STATES; i++ ) { group->numState[i] = 0; } //go through group and validate each membership group->commander = NULL; for ( i = 0; i < group->numGroup; i++ ) { /* //this checks for duplicate copies of one member in a group for ( j = 0; j < group->numGroup; j++ ) { if ( i != j ) { if ( group->member[i].number == group->member[j].number ) { break; } } } if ( j < group->numGroup ) {//found a dupe! gi.Printf( S_COLOR_RED"ERROR: member %s(%d) a duplicate group member!!!\n", g_entities[group->member[i].number].targetname, group->member[i].number ); AI_DeleteGroupMember( group, i ); i--; continue; } */ member = &g_entities[group->member[i].number]; //Must be alive if ( member->health <= 0 ) { AI_DeleteGroupMember( group, i ); //keep marker at same place since we deleted this guy and shifted everyone up one i--; } else if ( group->memberValidateTime < level.time && !AI_ValidateGroupMember( group, member ) ) { //remove this one from the group AI_DeleteGroupMember( group, i ); //keep marker at same place since we deleted this guy and shifted everyone up one i--; } else {//membership is valid //keep track of squadStates group->numState[member->NPC->squadState]++; if ( !group->commander || member->NPC->rank > group->commander->NPC->rank ) {//keep track of highest rank group->commander = member; } } } if ( group->memberValidateTime < level.time ) { group->memberValidateTime = level.time + Q_irand( 500, 2500 ); } //Now add any new guys as long as we're not full /* for ( i = 0, member = &g_entities[0]; i < globals.num_entities && group->numGroup < (MAX_GROUP_MEMBERS - 1); i++, member++) { if ( !AI_ValidateGroupMember( group, member ) ) {//FIXME: keep track of those who aren't angry yet and see if we should wake them after we assemble the core group continue; } if ( member->NPC->group == group ) {//DOH, already in our group continue; } //store it AI_InsertGroupMember( group, member ); } */ //calc the morale of this group group->morale = group->moraleAdjust; for ( i = 0; i < group->numGroup; i++ ) { member = &g_entities[group->member[i].number]; if ( member->NPC->rank < RANK_ENSIGN ) {//grunts group->morale++; } else { group->morale += member->NPC->rank; } } if ( group->enemy ) {//modify morale based on enemy health and weapon if ( group->enemy->health < 10 ) { group->morale += 10; } else if ( group->enemy->health < 25 ) { group->morale += 5; } else if ( group->enemy->health < 50 ) { group->morale += 2; } switch( group->enemy->s.weapon ) { case WP_SABER: group->morale -= 5; break; case WP_BRYAR_PISTOL: group->morale += 3; break; case WP_DISRUPTOR: group->morale += 2; break; case WP_REPEATER: group->morale -= 1; break; case WP_FLECHETTE: group->morale -= 2; break; case WP_ROCKET_LAUNCHER: group->morale -= 10; break; case WP_THERMAL: group->morale -= 5; break; case WP_TRIP_MINE: group->morale -= 3; break; case WP_DET_PACK: group->morale -= 10; break; case WP_MELEE: // Any ol' melee attack group->morale += 20; break; case WP_STUN_BATON: group->morale += 10; break; case WP_EMPLACED_GUN: group->morale -= 8; break; case WP_ATST_MAIN: group->morale -= 8; break; case WP_ATST_SIDE: group->morale -= 20; break; } } if ( group->moraleDebounce < level.time ) {//slowly degrade whatever moraleAdjusters we may have if ( group->moraleAdjust > 0 ) { group->moraleAdjust--; } else if ( group->moraleAdjust < 0 ) { group->moraleAdjust++; } group->moraleDebounce = level.time + 1000;//FIXME: define? } //mark this group as not having been run this frame group->processed = qfalse; return (group->numGroup>0); } void AI_UpdateGroups( void ) { if ( d_noGroupAI->integer ) { return; } //Clear all Groups for ( int i = 0; i < MAX_FRAME_GROUPS; i++ ) { if ( !level.groups[i].numGroup || level.groups[i].enemy == NULL || AI_RefreshGroup( &level.groups[i] ) == qfalse ) { memset( &level.groups[i], 0, sizeof( level.groups[i] ) ); } } } qboolean AI_GroupContainsEntNum( AIGroupInfo_t *group, int entNum ) { if ( !group ) { return qfalse; } for ( int i = 0; i < group->numGroup; i++ ) { if ( group->member[i].number == entNum ) { return qtrue; } } return qfalse; } //Overload /* void AI_GetGroup( AIGroupInfo_t &group, gentity_t *ent, int radius ) { if ( ent->client == NULL ) return; vec3_t temp, angles; //FIXME: This is specialized code.. move? if ( ent->enemy ) { VectorSubtract( ent->enemy->currentOrigin, ent->currentOrigin, temp ); VectorNormalize( temp ); //FIXME: Needed? vectoangles( temp, angles ); } else { VectorCopy( ent->currentAngles, angles ); } AI_GetGroup( group, ent->currentOrigin, ent->currentAngles, DEFAULT_RADIUS, radius, ent->client->playerTeam, ent, ent->enemy ); } */ /* ------------------------- AI_CheckEnemyCollision ------------------------- */ qboolean AI_CheckEnemyCollision( gentity_t *ent, qboolean takeEnemy ) { if ( ent == NULL ) return qfalse; if ( ent->svFlags & SVF_LOCKEDENEMY ) return qfalse; navInfo_t info; NAV_GetLastMove( info ); //See if we've hit something if ( ( info.blocker ) && ( info.blocker != ent->enemy ) ) { if ( ( info.blocker->client ) && ( info.blocker->client->playerTeam == ent->client->enemyTeam ) ) { if ( takeEnemy ) G_SetEnemy( ent, info.blocker ); return qtrue; } } return qfalse; } /* ------------------------- AI_DistributeAttack ------------------------- */ #define MAX_RADIUS_ENTS 128 gentity_t *AI_DistributeAttack( gentity_t *attacker, gentity_t *enemy, team_t team, int threshold ) { //Don't take new targets if ( NPC->svFlags & SVF_LOCKEDENEMY ) return enemy; int numSurrounding = AI_GetGroupSize( enemy->currentOrigin, 48, team, attacker ); //First, see if we should look for the player if ( enemy != &g_entities[0] ) { int aroundPlayer = AI_GetGroupSize( g_entities[0].currentOrigin, 48, team, attacker ); //See if we're above our threshold if ( aroundPlayer < threshold ) { return &g_entities[0]; } } //See if our current enemy is still ok if ( numSurrounding < threshold ) return enemy; //Otherwise we need to take a new enemy if possible vec3_t mins, maxs; //Setup the bbox to search in for ( int i = 0; i < 3; i++ ) { mins[i] = enemy->currentOrigin[i] - 512; maxs[i] = enemy->currentOrigin[i] + 512; } //Get the number of entities in a given space gentity_t *radiusEnts[ MAX_RADIUS_ENTS ]; int numEnts = gi.EntitiesInBox( mins, maxs, radiusEnts, MAX_RADIUS_ENTS ); //Cull this list for ( int j = 0; j < numEnts; j++ ) { //Validate clients if ( radiusEnts[ j ]->client == NULL ) continue; //Skip the requested avoid ent if present if ( ( radiusEnts[ j ] == enemy ) ) continue; //Must be on the same team if ( radiusEnts[ j ]->client->playerTeam != enemy->client->playerTeam ) continue; //Must be alive if ( radiusEnts[ j ]->health <= 0 ) continue; //Must not be overwhelmed if ( AI_GetGroupSize( radiusEnts[j]->currentOrigin, 48, team, attacker ) > threshold ) continue; return radiusEnts[j]; } return NULL; }