Files
Enemy-Territory/src/botai/ai_dmq3.c

7573 lines
194 KiB
C
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
===========================================================================
Wolfenstein: Enemy Territory GPL Source Code
Copyright (C) 1999-2010 id Software LLC, a ZeniMax Media company.
This file is part of the Wolfenstein: Enemy Territory GPL Source Code (“Wolf ET Source Code”).
Wolf ET Source Code is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Wolf ET Source Code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Wolf ET Source Code. If not, see <http://www.gnu.org/licenses/>.
In addition, the Wolf: ET Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Wolf ET Source Code. If not, please request a copy in writing from id Software at the address below.
If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.
===========================================================================
*/
/*****************************************************************************
* name: ai_dmq3.c
*
* desc: Wolf bot AI
*
*
*****************************************************************************/
#include "../game/g_local.h"
#include "../game/botlib.h"
#include "../game/be_aas.h"
#include "../game/be_ea.h"
#include "../game/be_ai_char.h"
#include "../game/be_ai_chat.h"
#include "../game/be_ai_gen.h"
#include "../game/be_ai_goal.h"
#include "../game/be_ai_move.h"
#include "../game/be_ai_weap.h"
#include "../botai/botai.h"
//
#include "ai_main.h"
#include "ai_dmq3.h"
#include "ai_cmd.h"
#include "ai_team.h"
#include "ai_dmnet_mp.h"
#include "ai_dmgoal_mp.h"
#include "ai_matrix.h"
#include "ai_distances.h"
//
#include "chars.h" //characteristics
#include "inv.h" //indexes into the inventory
#include "syn.h" //synonyms
#include "match.h" //string matching types and vars
#define IDEAL_ATTACKDIST 140
//#define WEAPONINDEX_MACHINEGUN 2
#define DONT_PRINT_REPEATED_AI_ERRORS
// BOT MOVEMENT AUTONOMY
float movementAutonomyRange[NUM_BMA] =
{
256, // LOW
1024, // MEDIUM
99999, // HIGH
};
// BOT MOVEMENT AUTONOMY for Single Player
float movementAutonomyRangeSP[NUM_BMA] =
{
200, // LOW
350, // MEDIUM
700, // HIGH
};
///////////////////////
//
// COMBAT CONSTANTS
//
///////////////////////
//////////////////
// from aasfile.h
//#define AREACONTENTS_MOVER 1024
//#define AREACONTENTS_MODELNUMSHIFT 24
//#define AREACONTENTS_MAXMODELNUM 0xFF
//#define AREACONTENTS_MODELNUM (AREACONTENTS_MAXMODELNUM << AREACONTENTS_MODELNUMSHIFT)
//////////////////
//
bot_waypoint_t botai_waypoints[MAX_BOTAIWAYPOINTS];
bot_waypoint_t *botai_freewaypoints;
//NOTE: not using a cvar which can be updated because the game should be reloaded anyway
int gametype; //game type
// Rafael gameskill
//int gameskill;
vmCvar_t bot_grapple;
vmCvar_t bot_rocketjump;
vmCvar_t bot_fastchat;
vmCvar_t bot_nochat;
vmCvar_t bot_testrchat;
vec3_t lastteleport_origin;
float lastteleport_time;
//true when the map changed
int max_bspmodelindex; //maximum BSP model index
//CTF flag goals
bot_goal_t ctf_redflag;
bot_goal_t ctf_blueflag;
/*
==================
BotCarryingFlag
==================
*/
qboolean BotCarryingFlag( int client ) {
if ( gametype < GT_WOLF ) {
return qfalse;
}
if ( g_entities[client].health <= 0 ) {
return qfalse;
}
if ( level.clients[client].ps.powerups[PW_REDFLAG] || level.clients[client].ps.powerups[PW_BLUEFLAG] ) {
return qtrue;
}
return qfalse;
}
extern vec3_t playerMins;
extern vec3_t playerMaxs;
byte botCheckedAreas[65536];
/*
==================
BotFirstReachabilityArea
==================
*/
int BotFirstReachabilityArea( int entnum, vec3_t origin, int *areas, int numareas, qboolean distCheck ) {
int i, best = 0;
trace_t tr;
vec3_t center;
float bestDist, dist;
vec3_t mins, maxs;
//
if ( entnum >= 0 && entnum < level.maxclients ) {
VectorCopy( playerMins, mins );
mins[2] += 18; // STEPSIZE
VectorCopy( playerMaxs, maxs );
} else {
VectorCopy( vec3_origin, mins );
VectorCopy( vec3_origin, maxs );
}
bestDist = 999999;
for ( i = 0; i < numareas; i++ ) {
if ( botCheckedAreas[areas[i]] ) {
continue;
}
botCheckedAreas[areas[i]] = 1;
if ( trap_AAS_AreaReachability( areas[i] ) ) {
// make sure this area is visible
if ( !trap_AAS_AreaWaypoint( areas[i], center ) ) {
trap_AAS_AreaCenter( areas[i], center );
}
if ( distCheck ) {
dist = VectorDistance( center, origin );
if ( center[2] > origin[2] ) {
dist += 32 * ( center[2] - origin[2] );
}
if ( dist < bestDist ) {
trap_Trace( &tr, origin, mins, maxs, center, -1, MASK_PLAYERSOLID & ~CONTENTS_BODY );
if ( tr.fraction > .99f || tr.startsolid ) { // if we start in solid, ignore trace test
best = areas[i];
bestDist = dist;
//if (dist < 128) {
// return best;
//}
}
}
} else {
trap_Trace( &tr, origin, mins, maxs, center, -1, MASK_PLAYERSOLID & ~CONTENTS_BODY );
if ( tr.fraction > .99f || tr.startsolid ) { // if we start in solid, ignore trace test
return areas[i];
}
}
}
}
//
return best;
}
/*
==================
BotFirstLadderArea
==================
*/
int BotFirstLadderArea( int entnum, int *areas, int numareas ) {
int i;
//
for ( i = 0; i < numareas; i++ ) {
if ( trap_AAS_AreaLadder( areas[i] ) ) {
return areas[i];
}
}
//
return 0;
}
/*
==================
BotPointAreaNum
==================
*/
int BotPointAreaNum( int entnum, vec3_t origin ) {
int areanum, numareas, areas[50], bestarea = 0, i;
vec3_t end, start, ofs, mins, maxs;
float f;
gentity_t *ent = NULL;
#define BOTAREA_BOX_DIST 256
#define BOTAREA_JIGGLE_DIST 32
//
if ( entnum >= 0 && VectorCompare( origin, g_entities[entnum].botAreaPos ) ) {
return g_entities[entnum].botAreaNum;
}
memset( botCheckedAreas, 0, sizeof( botCheckedAreas ) );
if ( entnum >= 0 ) {
ent = &g_entities[entnum];
}
// if this is a bot, and it's touching a ladder, do special handling
if ( ent && ent->client && ent->client->ps.pm_flags & PMF_LADDER ) {
// use the point only if its a ladder area
areanum = trap_AAS_PointAreaNum( origin );
if ( areanum && !trap_AAS_AreaLadder( areanum ) ) {
areanum = 0;
}
if ( areanum ) {
bestarea = areanum;
goto done;
}
// try a small box, and take a ladder area as preference
maxs[0] = 8;
maxs[1] = 8;
maxs[2] = 4;
VectorSubtract( origin, maxs, mins );
VectorAdd( origin, maxs, maxs );
numareas = trap_AAS_BBoxAreas( mins, maxs, areas, 50 );
if ( numareas > 0 ) {
bestarea = BotFirstLadderArea( entnum, areas, numareas );
}
if ( bestarea ) {
goto done;
}
// try the actual point
areanum = trap_AAS_PointAreaNum( origin );
if ( areanum && !trap_AAS_AreaReachability( areanum ) ) {
areanum = 0;
}
if ( areanum ) {
bestarea = areanum;
goto done;
}
} else {
//
areanum = trap_AAS_PointAreaNum( origin );
if ( areanum && !trap_AAS_AreaReachability( areanum ) ) {
areanum = 0;
}
if ( areanum ) {
bestarea = areanum;
goto done;
}
// trace a line from below us, upwards, finding the first area the line touches
VectorCopy( origin, start );
VectorCopy( origin, end );
if ( ( entnum >= 0 ) && g_entities[entnum].inuse && g_entities[entnum].client ) {
end[2] += g_entities[entnum].client->ps.viewheight;
}
start[2] -= 30;
numareas = trap_AAS_TraceAreas( start, end, areas, NULL, 50 );
if ( numareas > 0 ) {
bestarea = BotFirstReachabilityArea( entnum, origin, areas, numareas, qfalse );
}
if ( bestarea ) {
goto done;
}
// try a small box around the origin
maxs[0] = 4;
maxs[1] = 4;
maxs[2] = 4;
VectorSubtract( origin, maxs, mins );
VectorAdd( origin, maxs, maxs );
numareas = trap_AAS_BBoxAreas( mins, maxs, areas, 50 );
if ( numareas > 0 ) {
bestarea = BotFirstReachabilityArea( entnum, origin, areas, numareas, qtrue );
}
if ( bestarea ) {
goto done;
}
}
// try using the players bounding box
if ( ( entnum >= 0 ) && g_entities[entnum].inuse && g_entities[entnum].client ) {
numareas = trap_AAS_BBoxAreas( g_entities[entnum].r.absmin, g_entities[entnum].r.absmax, areas, 50 );
if ( numareas > 0 ) {
bestarea = BotFirstReachabilityArea( entnum, origin, areas, numareas, qtrue );
}
if ( bestarea ) {
goto done;
}
}
//@TODO. The following code seems to often cause bogus areanums to be returned. They are offset
// from the real areas, and this causes all sorts of bot stickiness.
// try half size first
for ( f = 0.1; f <= 1.0; f += 0.45 ) {
VectorCopy( origin, end );
end[2] += 80;
VectorCopy( origin, ofs );
ofs[2] -= 60;
for ( i = 0; i < 2; i++ ) end[i] += BOTAREA_BOX_DIST * f;
for ( i = 0; i < 2; i++ ) ofs[i] -= BOTAREA_BOX_DIST * f;
//
numareas = trap_AAS_BBoxAreas( ofs, end, areas, 50 );
if ( numareas > 0 ) {
bestarea = BotFirstReachabilityArea( entnum, origin, areas, numareas, qtrue );
}
if ( bestarea ) {
goto done;
}
}
//
done:
if ( entnum >= 0 ) {
VectorCopy( origin, g_entities[entnum].botAreaPos );
g_entities[entnum].botAreaNum = bestarea;
}
return bestarea;
}
/*
===================
BotReachableBBoxAreaNum
===================
*/
int BotReachableBBoxAreaNum( bot_state_t *bs, vec3_t absmin, vec3_t absmax ) {
int numareas, areas[64], sorted[64], bestarea = 0, i, j;
vec3_t center, v;
float dists[200], bestdist;
//
// find the area that is reachable from bs, and closest to the center of the box
numareas = trap_AAS_BBoxAreas( absmin, absmax, areas, 64 );
// sort them by distance from center
VectorAdd( absmin, absmax, center );
VectorScale( center, 0.5, center );
for ( i = 0; i < numareas; i++ ) {
trap_AAS_AreaWaypoint( areas[i], v );
dists[i] = VectorDistanceSquared( center, v );
}
for ( i = 0; i < numareas; i++ ) {
bestdist = -1;
for ( j = 0; j < numareas; j++ ) {
if ( dists[j] > 0 && ( bestdist < 0 || dists[j] < bestdist ) ) {
bestdist = dists[j];
bestarea = j;
}
}
dists[bestarea] = -1;
sorted[i] = areas[bestarea];
}
// now take the first area that we can reach
for ( i = 0; i < numareas; i++ ) {
if ( trap_AAS_AreaTravelTimeToGoalArea( bs->areanum, bs->origin, sorted[i], bs->tfl ) ) {
return sorted[i];
}
}
return 0;
}
/*
==================
BotFindNearbyGoal
==================
*/
static qboolean sDoNearbyGoalCheck( bot_state_t *bs, vec3_t loc, gentity_t *target ) {
int t, areanum;
bot_goal_t goal;
#define MAX_NEARBY_DIST 512
#define MAX_NEARBY_TIME 1500
// if it's close enough
if ( VectorDistanceSquared( bs->origin, loc ) > SQR( MAX_NEARBY_DIST ) ) {
return qfalse;
}
// if it's not within travel time
areanum = trap_AAS_PointAreaNum( loc );
if ( areanum || !trap_AAS_AreaReachability( areanum ) ) {
areanum = BotPointAreaNum( -1, loc );
}
if ( !areanum ) {
return qfalse;
}
t = trap_AAS_AreaTravelTimeToGoalArea( bs->areanum, bs->origin, areanum, bs->tfl );
if ( !t || ( t > MAX_NEARBY_TIME ) ) {
return qfalse;
}
// found one!
BotClearGoal( &goal );
goal.areanum = areanum;
goal.entitynum = target->s.number;
VectorCopy( target->r.mins, goal.mins );
VectorCopy( target->r.maxs, goal.maxs );
VectorCopy( loc, goal.origin );
// always get it if it's close
if ( t > 200 && !BotGoalWithinMovementAutonomy( bs, &goal, BGU_LOW ) ) {
return qfalse;
}
bs->nearbygoal = goal;
return qtrue;
}
// TAT 11/21/2002
// Look for ammo and health triggers
qboolean BotFindNearbyTriggerGoal( bot_state_t *bs ) {
gentity_t *trav;
vec3_t loc;
int i;
char *goalnames[] = {"trigger_ammo", "trigger_heal", NULL};
for ( i = 0; goalnames[i]; i++ )
{
// ammo
if ( i == 0 ) {
// does the bot need ammo? also checks if they need ammo and health PACKS
if ( !ClientNeedsAmmo( bs->client ) ) {
continue;
}
}
// health
else if ( i == 1 ) {
if ( BotHealthScale( bs->client ) >= 1.0 ) {
continue;
}
}
//
trav = NULL;
while ( ( trav = G_Find( trav, FOFS( classname ), goalnames[i] ) ) )
{
// triggers don't have a location, they just have bounds
// so see how far we are from the center of the trigger
loc[0] = ( trav->r.mins[0] + trav->r.maxs[0] ) / 2.0f;
loc[1] = ( trav->r.mins[1] + trav->r.maxs[1] ) / 2.0f;
loc[2] = ( ( trav->r.mins[2] + trav->r.maxs[2] ) / 2.0f ) + 30.f;
if ( sDoNearbyGoalCheck( bs, loc, trav ) ) {
return qtrue;
}
}
}
return qfalse;
}
qboolean BotFindNearbyGoal( bot_state_t *bs ) {
vec3_t org;
int i;
qboolean needAmmo;
qboolean needHealth;
if ( bs->next_nearbygoal > level.time ) {
return qfalse;
}
bs->next_nearbygoal = level.time + 500 + rand() % 500;
needAmmo = ClientNeedsAmmo( bs->client );
needHealth = BotHealthScale( bs->client ) >= 1.0 ? qfalse : qtrue;
for ( i = MAX_CLIENTS; i < level.num_entities; i++ ) {
gentity_t* ent = &g_entities[i];
switch ( ent->s.eType ) {
case ET_ITEM:
{
gitem_t* item = &bg_itemlist[ent->s.modelindex];
switch ( item->giType ) {
case IT_TEAM:
switch ( item->giType ) {
case PW_REDFLAG:
if ( bs->sess.sessionTeam == TEAM_AXIS ) {
continue;
}
break;
case PW_BLUEFLAG:
if ( bs->sess.sessionTeam == TEAM_ALLIES ) {
continue;
}
break;
default:
break;
}
break;
case IT_WEAPON:
if ( !needAmmo ) {
continue;
}
switch ( item->giType ) {
case WP_AMMO:
break;
default:
continue;
}
break;
case IT_HEALTH:
if ( !needHealth ) {
continue;
}
break;
default:
continue;
}
if ( ent->r.ownerNum == bs->client && ent->botIgnoreTime > level.time ) {
continue;
}
VectorCopy( ent->r.currentOrigin, org );
org[2] += 30;
if ( sDoNearbyGoalCheck( bs, org, ent ) ) {
return qtrue;
}
}
case ET_SUPPLIER:
case ET_HEALER:
{
vec3_t loc;
if ( ent->s.eType == ET_HEALER && !needHealth ) {
continue;
}
if ( ent->s.eType == ET_SUPPLIER && !needAmmo ) {
continue;
}
VectorAdd( ent->r.mins, ent->r.maxs, loc );
VectorScale( loc, 0.5f, loc );
loc[2] += 30.f;
if ( sDoNearbyGoalCheck( bs, loc, ent ) ) {
return qtrue;
}
}
default:
continue;
}
}
return qfalse;
}
/*
==================
ClientName
==================
*/
char *ClientName( int client, char *name, int size ) {
char buf[MAX_INFO_STRING];
if ( client < 0 || client >= MAX_CLIENTS ) {
BotAI_Print( PRT_ERROR, "ClientName: client out of range\n" );
return "[client out of range]";
}
trap_GetConfigstring( CS_PLAYERS + client, buf, sizeof( buf ) );
strncpy( name, Info_ValueForKey( buf, "n" ), size - 1 );
name[size - 1] = '\0';
Q_CleanStr( name );
return name;
}
/*
==================
ClientSkin
==================
*/
char *ClientSkin( int client, char *skin, int size ) {
char buf[MAX_INFO_STRING];
if ( client < 0 || client >= MAX_CLIENTS ) {
BotAI_Print( PRT_ERROR, "ClientSkin: client out of range\n" );
return "[client out of range]";
}
trap_GetConfigstring( CS_PLAYERS + client, buf, sizeof( buf ) );
strncpy( skin, Info_ValueForKey( buf, "model" ), size - 1 );
skin[size - 1] = '\0';
return skin;
}
/*
==================
ClientFromName
==================
*/
int ClientFromName( char *name ) {
int i;
char buf[MAX_INFO_STRING];
static int maxclients;
if ( !maxclients ) {
maxclients = trap_Cvar_VariableIntegerValue( "sv_maxclients" );
}
for ( i = 0; i < maxclients && i < MAX_CLIENTS; i++ ) {
if ( !g_entities[i].inuse ) {
continue;
}
trap_GetConfigstring( CS_PLAYERS + i, buf, sizeof( buf ) );
Q_CleanStr( buf );
if ( !Q_stricmp( Info_ValueForKey( buf, "n" ), name ) ) {
return i;
}
}
return -1;
}
/*
==================
stristr
==================
*/
char *stristr( char *str, char *charset ) {
int i;
while ( *str ) {
for ( i = 0; charset[i] && str[i]; i++ ) {
if ( toupper( charset[i] ) != toupper( str[i] ) ) {
break;
}
}
if ( !charset[i] ) {
return str;
}
str++;
}
return NULL;
}
/*
==================
EasyClientName
==================
*/
char *EasyClientName( int client, char *buf, int size ) {
int i;
char *str1, *str2, *ptr, c;
char name[128];
strcpy( name, ClientName( client, name, sizeof( name ) ) );
for ( i = 0; name[i]; i++ ) name[i] &= 127;
//remove all spaces
for ( ptr = strstr( name, " " ); ptr; ptr = strstr( name, " " ) ) {
memmove( ptr, ptr + 1, strlen( ptr + 1 ) + 1 );
}
//check for [x] and ]x[ clan names
str1 = strstr( name, "[" );
str2 = strstr( name, "]" );
if ( str1 && str2 ) {
if ( str2 > str1 ) {
memmove( str1, str2 + 1, strlen( str2 + 1 ) + 1 );
} else { memmove( str2, str1 + 1, strlen( str1 + 1 ) + 1 );}
}
//remove Mr prefix
if ( ( name[0] == 'm' || name[0] == 'M' ) &&
( name[1] == 'r' || name[1] == 'R' ) ) {
memmove( name, name + 2, strlen( name + 2 ) + 1 );
}
//only allow lower case alphabet characters
ptr = name;
while ( *ptr ) {
c = *ptr;
if ( ( c >= 'a' && c <= 'z' ) ||
( c >= '0' && c <= '9' ) || c == '_' ) {
ptr++;
} else if ( c >= 'A' && c <= 'Z' ) {
*ptr += 'a' - 'A';
ptr++;
} else {
memmove( ptr, ptr + 1, strlen( ptr + 1 ) + 1 );
}
}
strncpy( buf, name, size - 1 );
buf[size - 1] = '\0';
return buf;
}
/*
==============
BotGotEnoughAmmoForWeapon
==============
*/
qboolean BotGotEnoughAmmoForWeapon( bot_state_t *bs, int weapon ) {
int ammo, clip;
// if this is a charged weapon, check that it is ready for use (soon)
if ( !BotWeaponCharged( bs, weapon ) ) {
return qfalse;
}
ammo = bs->cur_ps.ammo[BG_FindAmmoForWeapon( weapon )];
clip = bs->cur_ps.ammoclip[BG_FindClipForWeapon( weapon )];
// TODO!! check some kind of weapon list that holds the minimum requirements for each weapon
switch ( weapon ) {
default:
return (qboolean)( ( clip >= GetAmmoTableData( weapon )->uses ) || ( ammo >= GetAmmoTableData( weapon )->uses ) ); //----(SA)
}
}
/*
==============
BotWeaponCharged
==============
*/
#define WC_WEAPON_TIME_LEFT level.time - ps->classWeaponTime
#define WC_SOLDIER_TIME level.soldierChargeTime [team - TEAM_AXIS]
#define WC_ENGINEER_TIME level.engineerChargeTime [team - TEAM_AXIS]
#define WC_FIELDOPS_TIME level.lieutenantChargeTime [team - TEAM_AXIS]
#define WC_MEDIC_TIME level.medicChargeTime [team - TEAM_AXIS]
#define WC_COVERTOPS_TIME level.covertopsChargeTime [team - TEAM_AXIS]
qboolean G_WeaponCharged( playerState_t* ps, team_t team, int weapon, int* skill ) {
switch ( weapon ) {
case WP_PANZERFAUST:
if ( ps->eFlags & EF_PRONE ) {
return qfalse;
}
if ( skill[SK_HEAVY_WEAPONS] >= 1 ) {
if ( WC_WEAPON_TIME_LEFT < WC_SOLDIER_TIME * 0.66f ) {
return qfalse;
}
} else if ( WC_WEAPON_TIME_LEFT < WC_SOLDIER_TIME ) {
return qfalse;
}
case WP_MORTAR_SET:
if ( skill[SK_HEAVY_WEAPONS] >= 1 ) {
if ( WC_WEAPON_TIME_LEFT < WC_SOLDIER_TIME * 0.33f ) {
return qfalse;
}
} else if ( WC_WEAPON_TIME_LEFT < WC_SOLDIER_TIME * 0.5f ) {
return qfalse;
}
return qtrue;
case WP_SMOKE_BOMB:
case WP_SATCHEL:
if ( skill[SK_MILITARY_INTELLIGENCE_AND_SCOPED_WEAPONS] >= 2 ) {
if ( WC_WEAPON_TIME_LEFT < WC_COVERTOPS_TIME * 0.66f ) {
return qfalse;
}
} else if ( WC_WEAPON_TIME_LEFT < WC_COVERTOPS_TIME ) {
return qfalse;
}
break;
case WP_LANDMINE:
if ( skill[SK_EXPLOSIVES_AND_CONSTRUCTION] >= 2 ) {
if ( WC_WEAPON_TIME_LEFT < ( WC_ENGINEER_TIME * 0.33f ) ) {
return qfalse;
}
} else if ( WC_WEAPON_TIME_LEFT < ( WC_ENGINEER_TIME * 0.5f ) ) {
return qfalse;
}
break;
case WP_DYNAMITE:
if ( skill[SK_EXPLOSIVES_AND_CONSTRUCTION] >= 3 ) {
if ( WC_WEAPON_TIME_LEFT < ( WC_ENGINEER_TIME * 0.66f ) ) {
return qfalse;
}
} else if ( WC_WEAPON_TIME_LEFT < WC_ENGINEER_TIME ) {
return qfalse;
}
break;
case WP_MEDKIT:
if ( skill[SK_FIRST_AID] >= 2 ) {
if ( WC_WEAPON_TIME_LEFT < WC_MEDIC_TIME * 0.15f ) {
return qfalse;
}
} else if ( WC_WEAPON_TIME_LEFT < WC_MEDIC_TIME * 0.25f ) {
return qfalse;
}
break;
case WP_AMMO:
if ( skill[SK_SIGNALS] >= 1 ) {
if ( WC_WEAPON_TIME_LEFT < WC_FIELDOPS_TIME * 0.15f ) {
return qfalse;
}
} else if ( WC_WEAPON_TIME_LEFT < WC_FIELDOPS_TIME * 0.25f ) {
return qfalse;
}
break;
case WP_SMOKE_MARKER:
if ( skill[SK_SIGNALS] >= 2 ) {
if ( WC_WEAPON_TIME_LEFT < WC_FIELDOPS_TIME * 0.66f ) {
return qfalse;
}
} else if ( WC_WEAPON_TIME_LEFT < WC_FIELDOPS_TIME ) {
return qfalse;
}
break;
case WP_MEDIC_ADRENALINE:
if ( WC_WEAPON_TIME_LEFT < WC_MEDIC_TIME ) {
return qfalse;
}
break;
case WP_BINOCULARS:
switch ( ps->stats[ STAT_PLAYER_CLASS ] ) {
case PC_FIELDOPS:
if ( skill[SK_SIGNALS] >= 2 ) {
if ( WC_WEAPON_TIME_LEFT <= WC_FIELDOPS_TIME * 0.66f ) {
return qfalse;
}
} else if ( WC_WEAPON_TIME_LEFT <= WC_FIELDOPS_TIME ) {
return qfalse;
}
default:
return qfalse;
}
break;
case WP_GPG40:
case WP_M7:
if ( WC_WEAPON_TIME_LEFT < WC_ENGINEER_TIME * 0.5f ) {
return qfalse;
}
break;
}
return qtrue;
}
qboolean BotWeaponCharged( bot_state_t *bs, int weapon ) {
return G_WeaponCharged( &bs->cur_ps, bs->sess.sessionTeam, weapon, bs->sess.skill );
}
/*
==============
BotWeaponOnlyUseIfInInRange
returns qtrue if the given weapon should only be used if the enemy is within range (binocs, panzer, etc)
==============
*/
qboolean BotWeaponOnlyUseIfInInRange( int weaponnum ) {
switch ( weaponnum ) {
case WP_BINOCULARS:
case WP_SMOKE_MARKER:
case WP_PANZERFAUST:
case WP_MOBILE_MG42:
case WP_MORTAR:
case WP_GARAND_SCOPE:
case WP_K43_SCOPE:
case WP_FG42SCOPE:
return qtrue;
}
return qfalse;
}
/*
==============
BotWeaponClosestDist
==============
*/
float BotWeaponClosestDist( int weaponnum ) {
switch ( weaponnum ) {
case WP_PANZERFAUST:
return 512.0f;
case WP_BINOCULARS:
case WP_GARAND_SCOPE:
case WP_K43_SCOPE:
case WP_FG42SCOPE:
return 1024.0f;
case WP_GRENADE_PINEAPPLE:
case WP_GRENADE_LAUNCHER:
return 128;
case WP_M7:
case WP_GPG40:
return 256;
}
return 0;
}
int BotTeamMatesNearEnemy( bot_state_t* bs ) {
int i, j;
vec_t* vec;
float range = SQR( G_GetWeaponDamage( WP_PANZERFAUST ) );
int cnt = 0;
if ( bs->enemy < 0 ) {
return 0;
}
vec = BotGetOrigin( bs->enemy );
for ( i = 0; i < level.numConnectedClients; i++ ) {
j = level.sortedClients[i];
if ( j == bs->client ) {
continue;
}
if ( !BotSameTeam( bs, j ) ) {
continue;
}
if ( BotIsDead( &botstates[j] ) ) {
continue;
}
if ( VectorDistanceSquared( vec, BotGetOrigin( j ) ) > range ) {
continue;
}
cnt++;
}
return cnt;
}
/*
==============
BotWeaponWantScale
==============
*/
float BotWeaponWantScale( bot_state_t *bs, weapon_t weapon ) {
qboolean moving;
weapon_t clip = BG_FindClipForWeapon( weapon );
weapon_t ammo = BG_FindAmmoForWeapon( weapon );
if ( !bs->cur_ps.ammo[ammo] && !bs->cur_ps.ammoclip[clip] ) {
return 0.f;
}
if ( !BotWeaponCharged( bs, weapon ) ) {
return 0.f;
}
moving = ( VectorLengthSquared( bs->cur_ps.velocity ) > SQR( 10 ) );
switch ( weapon ) {
case WP_KNIFE:
// for fun, have random bots use the knife in warmup scrumage
if ( level.warmupTime > level.time && !( ( bs->client + level.warmupTime / 100 ) % 5 ) ) {
return 2.0;
}
return 0.2;
case WP_LUGER:
case WP_COLT:
return 0.4;
case WP_SILENCER:
case WP_SILENCED_COLT:
return 0.45;
case WP_AKIMBO_COLT:
case WP_AKIMBO_LUGER:
return 0.5;
case WP_AKIMBO_SILENCEDCOLT:
case WP_AKIMBO_SILENCEDLUGER:
return 0.55;
case WP_MP40:
case WP_THOMPSON:
case WP_STEN:
return 0.6;
case WP_GPG40:
case WP_M7:
if ( bs->inventory[ENEMY_HORIZONTAL_DIST] > 512 ) {
return 1.0;
}
return 0.1;
case WP_CARBINE:
case WP_GARAND:
case WP_FG42:
case WP_KAR98:
case WP_K43:
return 0.6;
case WP_MOBILE_MG42:
if ( !moving && bs->inventory[ENEMY_HORIZONTAL_DIST] > 500 ) {
return 3.0;
}
return 0.3;
case WP_GARAND_SCOPE:
case WP_K43_SCOPE:
case WP_FG42SCOPE:
if ( ( !moving && bs->enemy > -1 ) && ( bs->inventory[ENEMY_HORIZONTAL_DIST] > 300 ) ) {
return 1.0;
}
return 0.1;
case WP_FLAMETHROWER:
if ( ( !moving || bs->enemy > -1 ) && bs->inventory[ENEMY_HORIZONTAL_DIST] < 800 ) {
return 1.0;
}
return 0.1;
case WP_PANZERFAUST:
if ( !moving || bs->enemy > -1 ) {
if ( bs->enemy >= 0 ) {
if ( BotTeamMatesNearEnemy( bs ) > 1 ) {
return 0.f;
}
}
if ( bs->inventory[INVENTORY_HEALTH] < 15 || bs->inventory[ENEMY_HORIZONTAL_DIST] > 400 ) {
return 1.0;
}
}
return 0.1;
case WP_GRENADE_LAUNCHER:
case WP_GRENADE_PINEAPPLE:
if ( bs->enemy > -1 && bs->inventory[ENEMY_HORIZONTAL_DIST] < 300 ) {
if ( BotHealthScale( bs->client ) < 0.3 && ( bs->enemy < 0 || !BotCarryingFlag( bs->enemy ) ) ) {
return 2.0 * ( 1.0 - (float)bs->inventory[INVENTORY_HEALTH] / 40.0 ); // try and get a grenade off before death
}
if ( bs->inventory[ENEMY_HORIZONTAL_DIST] > 200 ) {
return 0.5;
} else {
return 0.3;
}
} else {
return 0.1;
}
break;
case WP_SMOKE_MARKER:
if ( bs->sess.playerType == PC_FIELDOPS && bs->enemy > -1 && ( bs->inventory[ENEMY_HORIZONTAL_DIST] < 400 ) ) {
if ( BG_GetSkyHeightAtPoint( BotGetOrigin( bs->enemy ) ) == MAX_MAP_SIZE ) {
return 0.f;
}
return 1.f;
}
break;
case WP_BINOCULARS:
if ( !moving ) {
if ( bs->sess.playerType == PC_FIELDOPS ) {
if ( bs->enemy > -1 && ( bs->inventory[ENEMY_HORIZONTAL_DIST] < 400 ) ) {
if ( BG_GetSkyHeightAtPoint( BotGetOrigin( bs->enemy ) ) == MAX_MAP_SIZE ) {
return 0.f;
}
return 1.f;
}
return 0.f;
} else if ( bs->sess.playerType == PC_COVERTOPS ) {
return 1.f;
}
} else {
return 0.01;
}
break;
default:
break;
}
// anything else must be non-combat
return 0.0;
}
/*
==================
BotBestFightWeapon
==================
*/
int BotBestFightWeapon( bot_state_t *bs ) {
weapon_t bestWeapon;
int i, *ammo;
float wantScale, bestWantScale, dist, thisRange, bestRange;
qboolean inRange, bestInRange;
ammo = bs->cur_ps.ammo;
bestWantScale = 0.0;
bestRange = 0.0;
bestWeapon = bs->weaponnum; // default to current weapon
bestInRange = qfalse;
dist = -1;
if ( bs->enemy >= 0 ) {
dist = VectorDistance( bs->origin, BotGetOrigin( bs->enemy ) );
}
for ( i = 0; i < WP_NUM_WEAPONS; i++ ) {
if ( COM_BitCheck( bs->cur_ps.weapons, i ) ) {
// check that our ammo is enough
if ( !BotGotEnoughAmmoForWeapon( bs, i ) ) {
continue;
}
// if they are too close, dont use this weapon
if ( dist != -1 && BotWeaponClosestDist( i ) > dist ) {
continue;
}
// if this is a scoped weapon, only use it if we aren't moving
if ( BotScopedWeapon( i ) && VectorLengthSquared( bs->cur_ps.velocity ) > SQR( 10 ) ) {
continue;
}
// check the range
if ( ( thisRange = BotWeaponRange( bs, i ) ) >= dist ) {
inRange = qtrue;
} else {
// if this weapon should only be used if we are within range
if ( dist != -1 && BotWeaponOnlyUseIfInInRange( i ) ) {
continue;
}
inRange = qfalse;
}
if ( !inRange && bestInRange ) {
continue;
}
//
// get the wantScale for this weapon given the current circumstances (0.0 - 1.0)
wantScale = BotWeaponWantScale( bs, i );
//
if ( ( inRange && !bestInRange ) || ( inRange && wantScale >= bestWantScale ) || ( !inRange && thisRange > bestRange ) ) {
bestWeapon = i;
bestWantScale = wantScale;
bestRange = thisRange;
bestInRange = inRange;
}
}
}
//
return bestWeapon;
}
/*
==================
BotChooseWeapon
==================
*/
void BotChooseWeapon( bot_state_t *bs ) {
int newweaponnum;
if ( bs->cur_ps.weaponstate == WEAPON_RAISING ||
bs->cur_ps.weaponstate == WEAPON_DROPPING ||
bs->cur_ps.weaponDelay ) {
trap_EA_SelectWeapon( bs->client, bs->weaponnum );
} else {
if ( ( newweaponnum = BotBestFightWeapon( bs ) ) ) {
if ( bs->weaponnum != newweaponnum ) {
bs->weaponchange_time = trap_AAS_Time();
}
bs->weaponnum = newweaponnum;
//BotAI_Print(PRT_MESSAGE, "bs->weaponnum = %d\n", bs->weaponnum);
trap_EA_SelectWeapon( bs->client, bs->weaponnum );
}
}
}
/*
==================
BotCycleWeapon
==================
Bot cycles to next weapon in inventory, and will use it until told to cycle again
TAT 11/14/2002
*/
void BotCycleWeapon( bot_state_t *bs ) {
int i;
int curWeapon = bs->weaponnum;
float wantScale;
// loop through all the weapons, starting after the one we have equipped
for ( i = curWeapon + 1; i != curWeapon; i++ )
{
// if we went off the end, start at the beginning
if ( i >= WP_NUM_WEAPONS ) {
i = 0;
// we have an endless loop here, when we have no weapons, our current weapon is 0, and we never hit the ending condition in the loop
// since it's set in here
if ( curWeapon == 0 ) {
break;
}
}
// if we have this weapon
if ( COM_BitCheck( bs->cur_ps.weapons, i ) ) {
// if we don't have ammo for it, we can't choose it
if ( !BotGotEnoughAmmoForWeapon( bs, i ) ) {
continue;
}
// get the wantScale for this weapon given the current circumstances (0.0 - 1.0)
wantScale = BotWeaponWantScale( bs, i );
// if the wantscale is positive, then it is a weapon
if ( wantScale > 0 ) {
break;
}
}
}
if ( i != curWeapon ) {
// we found a new weapon
// set it as our selected weapon
bs->commandedWeapon = i;
bs->weaponnum = i;
trap_EA_SelectWeapon( bs->client, i );
}
}
/*
==================
BotSetupForMovement
==================
*/
void BotSetupForMovement( bot_state_t *bs ) {
bot_initmove_t initmove;
memset( &initmove, 0, sizeof( bot_initmove_t ) );
VectorCopy( bs->cur_ps.origin, initmove.origin );
VectorCopy( bs->cur_ps.velocity, initmove.velocity );
VectorCopy( bs->cur_ps.origin, initmove.viewoffset );
initmove.viewoffset[2] += bs->cur_ps.viewheight;
initmove.entitynum = bs->entitynum;
initmove.client = bs->client;
initmove.thinktime = bs->thinktime;
initmove.areanum = bs->areanum;
//set the onground flag
if ( bs->cur_ps.groundEntityNum != ENTITYNUM_NONE ) {
initmove.or_moveflags |= MFL_ONGROUND;
}
//set the teleported flag
if ( ( bs->cur_ps.pm_flags & PMF_TIME_KNOCKBACK ) && ( bs->cur_ps.pm_time > 0 ) ) {
initmove.or_moveflags |= MFL_TELEPORTED;
}
//set the waterjump flag
if ( ( bs->cur_ps.pm_flags & PMF_TIME_WATERJUMP ) && ( bs->cur_ps.pm_time > 0 ) ) {
initmove.or_moveflags |= MFL_WATERJUMP;
}
//set presence type
if ( bs->cur_ps.pm_flags & PMF_DUCKED ) {
initmove.presencetype = PRESENCE_CROUCH;
} else { initmove.presencetype = PRESENCE_NORMAL;}
//
if ( bs->walker > 0.5 ) {
initmove.or_moveflags |= MFL_WALK;
}
//
VectorCopy( bs->viewangles, initmove.viewangles );
//
trap_BotInitMoveState( bs->ms, &initmove );
}
/*
==================
BotUpdateInventory
==================
*/
void BotUpdateInventory( bot_state_t *bs ) {
//powerups
bs->inventory[INVENTORY_HEALTH] = bs->cur_ps.stats[STAT_HEALTH];
if ( bs->target_goal.entitynum != -1 ) {
bs->inventory[GOAL_TRAVELTIME] = trap_AAS_AreaTravelTimeToGoalArea( bs->areanum, bs->origin, bs->target_goal.areanum, bs->tfl );
} else {
bs->inventory[GOAL_TRAVELTIME] = 0;
}
/* bs->inventory[INVENTORY_TELEPORTER] = bs->cur_ps.stats[STAT_HOLDABLE_ITEM] == MODELINDEX_TELEPORTER;
bs->inventory[INVENTORY_MEDKIT] = bs->cur_ps.stats[STAT_HOLDABLE_ITEM] == MODELINDEX_MEDKIT;
bs->inventory[INVENTORY_REDFLAG] = bs->cur_ps.powerups[PW_REDFLAG] != 0;
bs->inventory[INVENTORY_BLUEFLAG] = bs->cur_ps.powerups[PW_BLUEFLAG] != 0;*/
}
/*
==================
BotUpdateBattleInventory
==================
*/
void BotUpdateBattleInventory( bot_state_t *bs, int enemy ) {
vec3_t dir;
aas_entityinfo_t entinfo;
BotEntityInfo( enemy, &entinfo );
VectorSubtract( entinfo.origin, bs->origin, dir );
bs->inventory[ENEMY_HEIGHT] = (int) dir[2];
dir[2] = 0;
bs->inventory[ENEMY_HORIZONTAL_DIST] = (int) VectorLength( dir );
//FIXME: add num visible enemies and num visible team mates to the inventory
}
/*
==================
BotBattleUseItems
==================
*/
void BotBattleUseItems( bot_state_t *bs ) {
/*
if (bs->inventory[INVENTORY_HEALTH] < 40) {
if (bs->inventory[INVENTORY_TELEPORTER] > 0) {
trap_EA_Use(bs->client);
}
if (bs->inventory[INVENTORY_MEDKIT] > 0) {
trap_EA_Use(bs->client);
}
}
*/
}
/*
==================
BotSetTeleportTime
==================
*/
void BotSetTeleportTime( bot_state_t *bs ) {
if ( ( bs->cur_ps.eFlags ^ bs->last_eFlags ) & EF_TELEPORT_BIT ) {
bs->teleport_time = trap_AAS_Time();
}
bs->last_eFlags = bs->cur_ps.eFlags;
}
/*
==================
BotIsDead
==================
*/
qboolean BotIsDead( bot_state_t *bs ) {
if ( bs->cur_ps.pm_flags & PMF_LIMBO ) {
return qtrue;
}
// RF, re-enabled these, they are required for the bots to respawn
if ( bs->cur_ps.pm_type == PM_DEAD ) {
return qtrue;
}
if ( g_entities[bs->client].health <= 0 ) {
return qtrue;
}
return qfalse;
}
// Gordon: 27/11/02: check if the bot is prisoner of war
/*
==================
BotIsPOW
==================
*/
qboolean BotIsPOW( bot_state_t *bs ) {
return bs->isPOW;
}
/*
==================
BotIsObserver
==================
*/
qboolean BotIsObserver( bot_state_t *bs ) {
char buf[MAX_INFO_STRING];
if ( bs->cur_ps.pm_type == PM_SPECTATOR ) {
return qtrue;
}
trap_GetConfigstring( CS_PLAYERS + bs->client, buf, sizeof( buf ) );
if ( atoi( Info_ValueForKey( buf, "t" ) ) == TEAM_SPECTATOR ) {
return qtrue;
}
return qfalse;
}
/*
==================
BotIntermission
==================
*/
qboolean BotIntermission( bot_state_t *bs ) {
//NOTE: we shouldn't look at the game code...
if ( level.intermissiontime ) {
return qtrue;
}
return ( bs->cur_ps.pm_type == PM_FREEZE || bs->cur_ps.pm_type == PM_INTERMISSION );
}
/*
==============
BotInLava
==============
*/
qboolean BotInLava( bot_state_t *bs ) {
vec3_t feet;
VectorCopy( bs->origin, feet );
feet[2] -= 23;
return ( trap_AAS_PointContents( feet ) & CONTENTS_LAVA );
}
/*
==============
BotInSlime
==============
*/
qboolean BotInSlime( bot_state_t *bs ) {
vec3_t feet;
VectorCopy( bs->origin, feet );
feet[2] -= 23;
return ( trap_AAS_PointContents( feet ) & CONTENTS_SLIME );
}
/*
==================
EntityIsDead
==================
*/
qboolean EntityIsDead( aas_entityinfo_t *entinfo ) {
if ( entinfo->number >= 0 && entinfo->number < MAX_CLIENTS ) {
if ( !g_entities[entinfo->number].inuse ) {
return qtrue;
}
if ( g_entities[entinfo->number].health <= 0 ) {
return qtrue;
}
}
return qfalse;
}
/*
==================
EntityInLimbo
==================
*/
qboolean EntityInLimbo( aas_entityinfo_t *entinfo ) {
if ( !g_entities[entinfo->number].client ) {
return qfalse;
}
return ( g_entities[entinfo->number].client->ps.pm_flags & PMF_LIMBO ) ? qtrue : qfalse;
}
/*
==================
EntityIsInvisible
==================
*/
qboolean EntityIsInvisible( aas_entityinfo_t *entinfo ) {
return qfalse;
}
/*
==================
EntityIsShooting
==================
*/
qboolean EntityIsShooting( aas_entityinfo_t *entinfo ) {
if ( entinfo->flags & EF_FIRING ) {
return qtrue;
}
return qfalse;
}
/*
==================
EntityIsChatting
==================
*/
qboolean EntityIsChatting( aas_entityinfo_t *entinfo ) {
if ( entinfo->flags & EF_TALK ) {
return qtrue;
}
return qfalse;
}
/*
==================
EntityHasQuad
==================
*/
qboolean EntityHasQuad( aas_entityinfo_t *entinfo ) {
return qfalse;
}
/*
==================
BotCreateWayPoint
==================
*/
bot_waypoint_t *BotCreateWayPoint( char *name, vec3_t origin, int areanum ) {
bot_waypoint_t *wp;
vec3_t waypointmins = {-8, -8, -8}, waypointmaxs = {8, 8, 8};
wp = botai_freewaypoints;
if ( !wp ) {
BotAI_Print( PRT_WARNING, "BotCreateWayPoint: Out of waypoints\n" );
return NULL;
}
botai_freewaypoints = botai_freewaypoints->next;
Q_strncpyz( wp->name, name, sizeof( wp->name ) );
VectorCopy( origin, wp->goal.origin );
VectorCopy( waypointmins, wp->goal.mins );
VectorCopy( waypointmaxs, wp->goal.maxs );
wp->goal.areanum = areanum;
wp->next = NULL;
wp->prev = NULL;
return wp;
}
/*
==================
BotFindWayPoint
==================
*/
bot_waypoint_t *BotFindWayPoint( bot_waypoint_t *waypoints, char *name ) {
bot_waypoint_t *wp;
for ( wp = waypoints; wp; wp = wp->next ) {
if ( !Q_stricmp( wp->name, name ) ) {
return wp;
}
}
return NULL;
}
/*
==================
BotFreeWaypoints
==================
*/
void BotFreeWaypoints( bot_waypoint_t *wp ) {
bot_waypoint_t *nextwp;
for (; wp; wp = nextwp ) {
nextwp = wp->next;
wp->next = botai_freewaypoints;
botai_freewaypoints = wp;
}
}
/*
==================
BotInitWaypoints
==================
*/
void BotInitWaypoints( void ) {
int i;
botai_freewaypoints = NULL;
for ( i = 0; i < MAX_BOTAIWAYPOINTS; i++ ) {
botai_waypoints[i].next = botai_freewaypoints;
botai_freewaypoints = &botai_waypoints[i];
}
}
/*
==================
TeamPlayIsOn
==================
*/
int TeamPlayIsOn( void ) {
return qtrue; //( gametype == GT_TEAM || gametype == GT_CTF );
}
/*
==================
BotAggression
FIXME: move this to external fuzzy logic
NOTE!!: I made no changes to this code for wolf weapon awareness. (SA)
==================
*/
float BotAggression( bot_state_t *bs ) {
//otherwise the bot is not feeling too good
return 0;
}
/*
==================
BotWantsToRetreat
==================
*/
int BotWantsToRetreat( bot_state_t *bs ) {
/* if (bs->engagementMatrix && (BotEngagementFunc(bs) & BOT_SEEK_COVER)) {
return qtrue;
}
if (bs->enemy > -1 && BotCarryingFlag(bs->enemy)) {
return qfalse;
}
if (BotAggression(bs) < 50) {
return qtrue;
}*/
return qfalse;
}
/*
==================
BotWantsToChase
==================
*/
int BotWantsToChase( bot_state_t *bs ) {
if ( BotCarryingFlag( bs->client ) ) {
return qfalse;
}
/* if (bs->engagementMatrix && !(BotEngagementFunc(bs) & BOT_ROE_PURSUE)) {
return qfalse;
}*/
if ( bs->enemy > -1 && BotCarryingFlag( bs->enemy ) ) {
return qtrue;
}
/* if (BotAggression(bs) > 50) {
return qtrue;
}*/
return qfalse;
}
/*
==================
BotWantsToHelp
==================
*/
int BotWantsToHelp( bot_state_t *bs ) {
return qtrue;
}
/*
==================
BotCanAndWantsToRocketJump
==================
*/
int BotCanAndWantsToRocketJump( bot_state_t *bs ) {
return qfalse;
/*
float rocketjumper;
//if rocket jumping is disabled
if (!bot_rocketjump.integer) return qfalse;
//if no rocket launcher
if (bs->inventory[INVENTORY_ROCKETLAUNCHER] <= 0) return qfalse;
//if low on rockets
if (bs->inventory[INVENTORY_ROCKETS] < 3) return qfalse;
//never rocket jump with the Quad
if (bs->inventory[INVENTORY_QUAD]) return qfalse;
//if low on health
if (bs->inventory[INVENTORY_HEALTH] < 60) return qfalse;
//if not full health
if (bs->inventory[INVENTORY_HEALTH] < 90) {
//if the bot has insufficient armor
if (bs->inventory[INVENTORY_ARMOR] < 40) return qfalse;
}
rocketjumper = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_WEAPONJUMPING, 0, 1);
if (rocketjumper < 0.5) return qfalse;
return qtrue;
*/
}
/*
==================
BotGoCamp
==================
*/
void BotGoCamp( bot_state_t *bs, bot_goal_t *goal ) {
/*
float camper;
//set message time to zero so bot will NOT show any message
bs->teammessage_time = 0;
//set the ltg type
bs->ltgtype = LTG_CAMP;
//set the team goal
memcpy(&bs->teamgoal, goal, sizeof(bot_goal_t));
//get the team goal time
camper = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_CAMPER, 0, 1);
if (camper > 0.99) bs->teamgoal_time = 99999;
else bs->teamgoal_time = 120 + 180 * camper + random() * 15;
//set the last time the bot started camping
bs->camp_time = trap_AAS_Time();
//the teammate that requested the camping
bs->teammate = 0;
//do NOT type arrive message
bs->arrive_time = 1;
*/
}
/*
==================
BotWantsToCamp
==================
*/
/*int BotWantsToCamp(bot_state_t *bs) {
float camper;
int cs, traveltime, besttraveltime;
bot_goal_t goal, bestgoal;
camper = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_CAMPER, 0, 1);
if (camper < 0.1) return qfalse;
//if the bot has a team goal
if (bs->ltgtype == LTG_TEAMHELP ||
bs->ltgtype == LTG_TEAMACCOMPANY ||
bs->ltgtype == LTG_DEFENDKEYAREA ||
bs->ltgtype == LTG_GETFLAG ||
bs->ltgtype == LTG_RUSHBASE ||
bs->ltgtype == LTG_CAMP ||
bs->ltgtype == LTG_CAMPORDER ||
bs->ltgtype == LTG_PATROL) {
return qfalse;
}
//if camped recently
if (bs->camp_time > trap_AAS_Time() - 60 + 300 * (1-camper)) return qfalse;
//
if (random() > camper) {
bs->camp_time = trap_AAS_Time();
return qfalse;
}
//if the bot isn't healthy anough
if (BotAggression(bs) < 50) return qfalse;
//the bot should have at least have the rocket launcher, the railgun or the bfg10k with some ammo
// if ((bs->inventory[INVENTORY_ROCKETLAUNCHER] <= 0 || bs->inventory[INVENTORY_ROCKETS < 10])
// && (bs->inventory[INVENTORY_RAILGUN] <= 0 || bs->inventory[INVENTORY_SLUGS] < 10)
// && (bs->inventory[INVENTORY_BFG10K] <= 0 || bs->inventory[INVENTORY_BFGAMMO] < 10)
// ){
// return qfalse;
// }
//find the closest camp spot
besttraveltime = 99999;
for (cs = trap_BotGetNextCampSpotGoal(0, &goal); cs; cs = trap_BotGetNextCampSpotGoal(cs, &goal)) {
traveltime = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, goal.areanum, bs->tfl);
if (traveltime && traveltime < besttraveltime) {
besttraveltime = traveltime;
memcpy(&bestgoal, &goal, sizeof(bot_goal_t));
}
}
if (besttraveltime > 150) return qfalse;
//ok found a camp spot, go camp there
BotGoCamp(bs, &bestgoal);
//
return qtrue;
}*/
/*
==================
BotDontAvoid
==================
*/
void BotDontAvoid( bot_state_t *bs, char *itemname ) {
bot_goal_t goal;
int num;
num = trap_BotGetLevelItemGoal( -1, itemname, &goal );
while ( num >= 0 ) {
trap_BotRemoveFromAvoidGoals( bs->gs, goal.number );
num = trap_BotGetLevelItemGoal( num, itemname, &goal );
}
}
/*
==================
BotGoForPowerups
==================
*/
void BotGoForPowerups( bot_state_t *bs ) {
//don't avoid any of the powerups anymore
BotDontAvoid( bs, "Quad Damage" );
BotDontAvoid( bs, "Regeneration" );
BotDontAvoid( bs, "Battle Suit" );
BotDontAvoid( bs, "Speed" );
BotDontAvoid( bs, "Invisibility" );
//BotDontAvoid(bs, "Flight");
//reset the long term goal time so the bot will go for the powerup
//NOTE: the long term goal type doesn't change
}
/*
==================
BotRoamGoal
==================
*/
void BotRoamGoal( bot_state_t *bs, vec3_t goal ) {
float len, r1, r2, sign, n;
int pc;
vec3_t dir, bestorg, belowbestorg;
bsp_trace_t trace;
for ( n = 0; n < 10; n++ ) {
//start at the bot origin
VectorCopy( bs->origin, bestorg );
r1 = random();
if ( r1 < 0.8 ) {
//add a random value to the x-coordinate
r2 = random();
if ( r2 < 0.5 ) {
sign = -1;
} else { sign = 1;}
bestorg[0] += sign * 700 * random() + 50;
}
if ( r1 > 0.2 ) {
//add a random value to the y-coordinate
r2 = random();
if ( r2 < 0.5 ) {
sign = -1;
} else { sign = 1;}
bestorg[1] += sign * 700 * random() + 50;
}
//add a random value to the z-coordinate (NOTE: 48 = maxjump?)
bestorg[2] += 3 * 48 * random() - 2 * 48 - 1;
//trace a line from the origin to the roam target
BotAI_Trace( &trace, bs->origin, NULL, NULL, bestorg, bs->entitynum, MASK_SOLID );
//direction and length towards the roam target
VectorSubtract( bestorg, bs->origin, dir );
len = VectorNormalize( dir );
//if the roam target is far away anough
if ( len > 200 ) {
//the roam target is in the given direction before walls
VectorScale( dir, len * trace.fraction - 40, dir );
VectorAdd( bs->origin, dir, bestorg );
//get the coordinates of the floor below the roam target
belowbestorg[0] = bestorg[0];
belowbestorg[1] = bestorg[1];
belowbestorg[2] = bestorg[2] - 800;
BotAI_Trace( &trace, bestorg, NULL, NULL, belowbestorg, bs->entitynum, MASK_SOLID );
//
if ( !trace.startsolid ) {
trace.endpos[2]++;
pc = trap_PointContents( trace.endpos,bs->entitynum );
if ( !( pc & CONTENTS_LAVA ) ) { //----(SA) modified since slime is no longer deadly
// if (!(pc & (CONTENTS_LAVA | CONTENTS_SLIME))) {
VectorCopy( bestorg, goal );
return;
}
}
}
}
VectorCopy( bestorg, goal );
}
/*
==================
BotAttackMove
==================
*/
bot_moveresult_t BotAttackMove( bot_state_t *bs, int tfl ) {
int movetype, i;
float attack_skill, jumper, croucher, dist, strafechange_time;
float attack_dist, attack_range;
vec3_t forward, backward, sideward, hordir, up = {0, 0, 1}, end;
aas_entityinfo_t entinfo;
bot_moveresult_t moveresult;
bot_goal_t goal;
trace_t tr;
aas_clientmove_t move;
if ( bs->attackchase_time > trap_AAS_Time() ) {
//create the chase goal
goal.entitynum = bs->enemy;
goal.areanum = bs->lastenemyareanum;
VectorCopy( bs->lastenemyorigin, goal.origin );
VectorSet( goal.mins, -8, -8, -8 );
VectorSet( goal.maxs, 8, 8, 8 );
//initialize the movement state
BotSetupForMovement( bs );
//move towards the goal
trap_BotMoveToGoal( &moveresult, bs->ms, &goal, tfl );
return moveresult;
}
//
memset( &moveresult, 0, sizeof( bot_moveresult_t ) );
//
attack_skill = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_ATTACK_SKILL, 0, 1 );
jumper = 0.05 * trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_JUMPER, 0, 1 );
croucher = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_CROUCHER, 0, 1 );
//if the bot is really stupid
if ( attack_skill < 0.2 ) {
return moveresult;
}
// if the bot is in the air
if ( bs->cur_ps.groundEntityNum == ENTITYNUM_NONE ) {
return moveresult;
}
//initialize the movement state
BotSetupForMovement( bs );
//get the enemy entity info
BotEntityInfo( bs->enemy, &entinfo );
//direction towards the enemy
VectorSubtract( entinfo.origin, bs->origin, forward );
//the distance towards the enemy
dist = VectorNormalize( forward );
VectorNegate( forward, backward );
// RF, removed, has it's own AI Node now
/* // if using mobile mg42, go prone if possible
if (bs->weaponnum == WP_MOBILE_MG42) {
if (dist > 1024.0f) {
int oldviewheight;
// check for obstruction at feet
oldviewheight = level.clients[bs->client].ps.viewheight;
level.clients[bs->client].ps.viewheight = PRONE_VIEWHEIGHT;
if (BotVisibleFromPos( bs->origin, bs->client, BotGetOrigin( bs->enemy ), bs->enemy, qtrue )) {
trap_EA_Prone( bs->client );
level.clients[bs->client].ps.viewheight = oldviewheight;
return moveresult;
} else {
// just crouch
trap_EA_Crouch( bs->client );
level.clients[bs->client].ps.viewheight = oldviewheight;
return moveresult;
}
}
}
*/
// if the enemy is too far away
if ( dist > 1350.0f || !BotMoveWhileFiring( bs->weaponnum ) ) {
// crouching, no movement
trap_EA_Crouch( bs->client );
return moveresult;
}
//walk, crouch or jump
movetype = MOVE_WALK;
//
if ( bs->attackcrouch_time < trap_AAS_Time() - 1 ) {
if ( ( g_gametype.integer != GT_SINGLE_PLAYER && g_gametype.integer != GT_COOP ) && ( random() < jumper ) ) {
movetype = MOVE_JUMP;
}
//wait at least one second before crouching again
else if ( bs->attackcrouch_time < trap_AAS_Time() - 1 && random() < croucher ) {
bs->attackcrouch_time = trap_AAS_Time() + croucher * 5;
}
}
if ( bs->attackcrouch_time > trap_AAS_Time() ) {
movetype = MOVE_CROUCH;
} else if ( bs->script.flags & BSFL_CROUCH ) {
movetype = MOVE_CROUCH;
}
//if the bot should jump
if ( movetype == MOVE_JUMP ) {
//if jumped last frame
if ( bs->attackjump_time > trap_AAS_Time() ) {
movetype = MOVE_WALK;
} else {
bs->attackjump_time = trap_AAS_Time() + 1;
}
}
attack_dist = IDEAL_ATTACKDIST;
attack_range = 40;
/* //if the bot is stupid
if (attack_skill <= 0.4) {
//just walk to or away from the enemy
if (dist > attack_dist + attack_range) {
if (trap_BotMoveInDirection(bs->ms, forward, 400, movetype)) return moveresult;
}
if (dist < attack_dist - attack_range) {
if (trap_BotMoveInDirection(bs->ms, backward, 400, movetype)) return moveresult;
}
return moveresult;
}
*/
if ( BotSinglePlayer() || BotCoop() ) {
if ( dist > 512 ) {
// no attack move if they are far away
return moveresult;
}
}
//increase the strafe time
bs->attackstrafe_time += bs->thinktime;
//get the strafe change time
strafechange_time = 0.7 + ( 1 - attack_skill ) * 0.2;
if ( attack_skill > 0.7 ) {
strafechange_time += crandom() * 0.2;
}
//if the strafe direction should be changed
if ( bs->attackstrafe_time > strafechange_time ) {
//some magic number :)
if ( random() > 0.945 ) {
//flip the strafe direction
bs->flags ^= BFL_STRAFERIGHT;
bs->attackstrafe_time = 0;
}
}
//
for ( i = 0; i < 2; i++ ) {
hordir[0] = forward[0];
hordir[1] = forward[1];
hordir[2] = 0;
VectorNormalize( hordir );
//get the sideward vector
CrossProduct( hordir, up, sideward );
//reverse the vector depending on the strafe direction
if ( bs->flags & BFL_STRAFERIGHT ) {
VectorNegate( sideward, sideward );
}
/* //randomly go back a little
if (random() > 0.9) {
VectorAdd(sideward, backward, sideward);
}
else {
//walk forward or backward to get at the ideal attack distance
if (dist > attack_dist + attack_range) VectorAdd(sideward, forward, sideward);
else if (dist < attack_dist - attack_range) VectorAdd(sideward, backward, sideward);
}
*/ //
// check this direction for trigger_hurt
VectorMA( bs->origin, 80, sideward, end );
trap_Trace( &tr, bs->origin, vec3_origin, vec3_origin, end, bs->client, CONTENTS_TRIGGER );
if ( tr.fraction < 1.0 && !Q_stricmp( g_entities[tr.entityNum].classname, "trigger_hurt" ) ) {
// hit something that will hurt us
bs->flags ^= BFL_STRAFERIGHT;
bs->attackstrafe_time = 0;
return moveresult;
}
// check for walking off a ledge
if ( trap_AAS_PredictClientMovement( &move, bs->client, bs->origin, -1, qtrue, sideward, vec3_origin, 10, 0, 0.1, SE_GAP | SE_LEAVEGROUND | SE_STUCK, -1, qfalse ) ) {
// if this is a bad movement, flip the direction, and wait util next frame when we can check that that move is ok
switch ( move.stopevent ) {
case SE_GAP:
case SE_LEAVEGROUND:
case SE_STUCK:
// bad movement
bs->flags ^= BFL_STRAFERIGHT;
bs->attackstrafe_time = 0;
return moveresult;
}
} else {
// dont move
return moveresult;
}
//
//perform the movement
if ( trap_BotMoveInDirection( bs->ms, sideward, 190, movetype ) ) {
return moveresult;
}
//movement failed, flip the strafe direction
bs->flags ^= BFL_STRAFERIGHT;
bs->attackstrafe_time = 0;
}
//bot couldn't do any usefull movement
// bs->attackchase_time = AAS_Time() + 6;
return moveresult;
}
/*
==================
BotSameTeam
==================
*/
int BotSameTeam( bot_state_t *bs, int entnum ) {
// Gordon: this function is way too generic for this to work properly
/* if (level.warmupTime > level.time) {
// fight everyone in warmup mode
return qfalse;
}*/
return OnSameTeam( &g_entities[bs->client], &g_entities[entnum] );
}
/*
==================
InFieldOfVision
==================
*/
qboolean InFieldOfVision( vec3_t viewangles, float fov, vec3_t angles ) {
int i;
float diff, angle;
for ( i = 0; i < 2; i++ ) {
angle = AngleMod( viewangles[i] );
angles[i] = AngleMod( angles[i] );
diff = angles[i] - angle;
if ( angles[i] > angle ) {
if ( diff > 180.0 ) {
diff -= 360.0;
}
} else {
if ( diff < -180.0 ) {
diff += 360.0;
}
}
if ( diff > 0 ) {
if ( diff > fov * 0.5 ) {
return qfalse;
}
} else {
if ( diff < -fov * 0.5 ) {
return qfalse;
}
}
}
return qtrue;
}
/*
==================
BotEntInvisibleBySmokeBomb
returns whether smoke from smoke bombs blocks vision from start to end
==================
Mad Doc xkan, 11/25/2002
*/
#define MAX_SMOKE_RADIUS 320.0
#define MAX_SMOKE_RADIUS_TIME 10000.0
#define UNAFFECTED_BY_SMOKE_DIST SQR( 100 )
qboolean BotEntInvisibleBySmokeBomb( vec3_t start, vec3_t end ) {
gentity_t *ent = NULL;
vec3_t smokeCenter;
float smokeRadius;
// if the target is close enough, vision is not affected by smoke bomb
if ( DistanceSquared( start,end ) < UNAFFECTED_BY_SMOKE_DIST ) {
return qfalse;
}
while ( ( ent = G_FindSmokeBomb( ent ) ) ) {
if ( ent->s.effect1Time == 16 ) {
// xkan, the smoke has not really started yet, see weapon_smokeBombExplode
// and CG_RenderSmokeGrenadeSmoke
continue;
}
// check the distance
VectorCopy( ent->s.pos.trBase, smokeCenter );
// raise the center to better match the position of the smoke, see
// CG_SpawnSmokeSprite().
smokeCenter[2] += 32;
// smoke sprite has a maximum radius of 640/2. and it takes a while for it to
// reach that size, so adjust the radius accordingly.
smokeRadius = MAX_SMOKE_RADIUS *
( ( level.time - ent->grenadeExplodeTime ) / MAX_SMOKE_RADIUS_TIME );
if ( smokeRadius > MAX_SMOKE_RADIUS ) {
smokeRadius = MAX_SMOKE_RADIUS;
}
// if distance from line is short enough, vision is blocked by smoke
if ( DistanceFromLineSquared( smokeCenter, start, end ) < smokeRadius * smokeRadius ) {
return qtrue;
}
}
return qfalse;
}
/*
==================
BotEntityVisible
returns visibility in the range [0, 1] taking fog and water surfaces into account
==================
*/
float BotEntityVisible( int viewer, vec3_t eye, vec3_t viewangles, float fov, int ent, vec3_t entorigin ) {
int i, contents_mask, passent, hitent, infog, inwater, otherinfog, pc;
float fogdist = 0, waterfactor, vis, bestvis;
bsp_trace_t trace;
aas_entityinfo_t entinfo;
vec3_t dir, entangles, start, end, middle;
int checkCount;
//calculate middle of bounding box
BotEntityInfo( ent, &entinfo );
VectorAdd( entinfo.mins, entinfo.maxs, middle );
VectorScale( middle, 0.5, middle );
if ( entorigin ) {
VectorAdd( entorigin, middle, middle );
} else {
VectorAdd( entinfo.origin, middle, middle );
}
// if the entity is using an mg42, then move the trace upwards to avoid the gun
if ( g_entities[ent].s.eFlags & EF_MG42_ACTIVE ) {
middle[2] += 16;
}
//check if entity is within field of vision
VectorSubtract( middle, eye, dir );
vectoangles( dir, entangles );
if ( fov < 360 && !InFieldOfVision( viewangles, fov, entangles ) ) {
return 0;
}
// RF, check PVS
if ( !trap_InPVS( eye, middle ) ) {
return 0.f;
}
// RF, if they are carrying the flag, then we can see them if they are in PVS
if ( BotCarryingFlag( ent ) ) {
return 1.f;
}
// RF, if they are far away, and we arent using a sniper rifle, then only do 1 check
checkCount = 3;
if ( ( botstates[viewer].inuse && BotCanSnipe( &botstates[viewer], qtrue ) ) && ( VectorLengthSquared( dir ) > SQR( 1024 ) ) ) {
checkCount = 1;
}
pc = trap_AAS_PointContents( eye );
infog = ( pc & CONTENTS_SOLID );
inwater = ( pc & ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ) );
bestvis = 0;
for ( i = 0; i < checkCount; i++ ) {
//
contents_mask = MASK_SHOT & ~( CONTENTS_CORPSE ); //CONTENTS_SOLID|CONTENTS_PLAYERCLIP;
passent = viewer;
hitent = ent;
VectorCopy( eye, start );
VectorCopy( middle, end );
//if the entity is in water, lava or slime
if ( trap_AAS_PointContents( middle ) & ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ) ) {
contents_mask |= ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER );
}
//if eye is in water, lava or slime
if ( inwater ) {
if ( !( contents_mask & ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ) ) ) {
passent = ent;
hitent = viewer;
VectorCopy( middle, start );
VectorCopy( eye, end );
}
contents_mask ^= ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER );
}
//trace from start to end
BotAI_Trace( &trace, start, NULL, NULL, end, passent, contents_mask );
//if water was hit
waterfactor = 1.0;
if ( trace.contents & ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ) ) {
//if the water surface is translucent
//trace through the water
contents_mask &= ~( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER );
BotAI_Trace( &trace, trace.endpos, NULL, NULL, end, passent, contents_mask );
waterfactor = 0.5;
}
//if a full trace or the hitent was hit
if ( trace.fraction >= .99f || trace.ent == hitent || ( ( entinfo.flags & EF_TAGCONNECT ) && ( g_entities[trace.ent].nextTrain == g_entities[ent].tagParent ) ) ) {
//check for fog, assuming there's only one fog brush where
//either the viewer or the entity is in or both are in
otherinfog = ( trap_AAS_PointContents( middle ) & CONTENTS_FOG );
if ( infog && otherinfog ) {
VectorSubtract( trace.endpos, eye, dir );
fogdist = VectorLength( dir );
} else if ( infog ) {
VectorCopy( trace.endpos, start );
BotAI_Trace( &trace, start, NULL, NULL, eye, viewer, CONTENTS_FOG );
VectorSubtract( eye, trace.endpos, dir );
fogdist = VectorLength( dir );
} else if ( otherinfog ) {
VectorCopy( trace.endpos, end );
BotAI_Trace( &trace, eye, NULL, NULL, end, viewer, CONTENTS_FOG );
VectorSubtract( end, trace.endpos, dir );
fogdist = VectorLength( dir );
} else {
//if the entity and the viewer are not in fog assume there's no fog in between
fogdist = 0;
} // else...
//decrease visibility with the view distance through fog
vis = 1 / ( ( fogdist * fogdist * 0.001 ) < 1 ? 1 : ( fogdist * fogdist * 0.001 ) );
//if entering water visibility is reduced
vis *= waterfactor;
// xkan 11/25/2002 - are there smoke (from smoke bombs) blocking the sight?
if ( vis > 0 && BotEntInvisibleBySmokeBomb( start, end ) ) {
vis = 0;
}
if ( vis > bestvis ) {
bestvis = vis;
}
// if pretty much no fog
if ( bestvis >= 0.95 ) {
return bestvis;
}
}
//check bottom and top of bounding box as well
if ( i == 0 ) {
middle[2] += entinfo.mins[2];
} else if ( i == 1 ) {
middle[2] += entinfo.maxs[2] - entinfo.mins[2];
}
}
return bestvis;
}
/*
================
BotVisibleFromPos
================
*/
qboolean BotVisibleFromPos( vec3_t srcorigin, int srcnum, vec3_t destorigin, int destent, qboolean dummy ) {
vec3_t eye;
//
VectorCopy( srcorigin, eye );
eye[2] += level.clients[srcnum].ps.viewheight;
//
if ( BotEntityVisible( srcnum, eye, vec3_origin, 360, destent, destorigin ) ) {
return qtrue;
}
//
return qfalse;
}
/*
================
BotCheckAttackAtPos
FIXME: do better testing here
================
*/
qboolean BotCheckAttackAtPos( int entnum, int enemy, vec3_t pos, qboolean ducking, qboolean allowHitWorld ) {
vec3_t eye;
//
VectorCopy( pos, eye );
eye[2] += level.clients[entnum].ps.viewheight;
//
if ( BotEntityVisible( entnum, eye, vec3_origin, 360, enemy, NULL ) ) {
return qtrue;
}
//
return qfalse;
}
/*
==================
BotGetMovementAutonomyLevel
==================
*/
int BotGetMovementAutonomyLevel( bot_state_t *bs ) {
return BMA_HIGH;
}
/*
=====================
BotGetMovementAutonomyPos
returns qtrue if a valid pos was returned
=====================
*/
qboolean BotGetMovementAutonomyPos( bot_state_t *bs, vec3_t pos ) {
// if we are doing a scripted move, then make the destination our autonomy pos
if ( bs->script.frameFlags & BSFFL_MOVETOTARGET ) {
if ( bs->target_goal.entitynum == bs->script.entityNum ) {
//VectorCopy( g_entities[bs->leader].s.origin, pos );
//VectorCopy( pos, bs->script.movementAutonomyPos ); // use this as the new autonomy pos
//VectorCopy( pos, bs->movementAutonomyPos ); // use this as the new autonomy pos
// TAT 10/8/2002 - set in AIEnter_SP_Script_MoveToMarker
VectorCopy( bs->movementAutonomyPos, pos );
return qtrue;
}
}
// if we have been commanded by a player, then only override that
// if scripting has issued a "force" command
if ( bs->movementAutonomy != BMA_NOVALUE ) {
if ( !( bs->script.flags & BSFL_FORCED_MOVEMENT_AUTONOMY ) ) {
// if we are following a leader
VectorCopy( bs->movementAutonomyPos, pos );
return qtrue;
}
}
// if the scripting has set the autonomy
if ( bs->script.movementAutonomy != BMA_NOVALUE ) {
// if we are following a leader
VectorCopy( bs->script.movementAutonomyPos, pos );
return qtrue;
}
return qfalse;
}
float BotGetFollowAutonomyDist( bot_state_t *bs );
/*
==================
BotGetRawMovementAutonomyRange
==================
*/
float BotGetRawMovementAutonomyRange( bot_state_t *bs ) {
float range;
int level;
//
// RF, if we are in followme mode, then use special following ranges
// TAT 12/16/2002 - Don't use this range in SP here - if we want the follow distance, we check it explicitly
if ( ( bs->leader >= 0 ) && !G_IsSinglePlayerGame() ) {
range = BotGetFollowAutonomyDist( bs );
return range;
}
//
level = BotGetMovementAutonomyLevel( bs );
if ( level > BMA_HIGH ) {
G_Printf( "BotGetMovementAutonomyRange(): autonomy exceeds BMA_HIGH\n" );
return 0; // should never happen
} else if ( level < BMA_NOVALUE ) {
G_Printf( "BotGetMovementAutonomyRange(): autonomy range less than BMA_NOVALUE\n" );
return 0; // should never happen
}
// Use a different value for SP
if ( g_gametype.integer == GT_SINGLE_PLAYER ) {
// Use the special SP table
range = movementAutonomyRangeSP[level];
return range;
}
//
range = movementAutonomyRange[level];
return range;
}
/*
==================
BotGetMovementAutonomyRange
==================
*/
float BotGetMovementAutonomyRange( bot_state_t *bs, bot_goal_t *goal ) {
float range;
// Get the basic movement autonomy range
range = BotGetRawMovementAutonomyRange( bs );
// medium urgency goals let us wander slightly outside the autonomy range
if ( goal && goal->urgency >= BGU_MEDIUM ) {
range = ( range * ( 1.0 + 0.25 * ( goal->urgency - BGU_LOW ) ) ) + 256 * ( goal->urgency - BGU_LOW );
}
//
return range;
}
// Start - TAT 9/18/2002
// What distance should a bot be from its leader during a follow order? Based on autonomy
float BotGetFollowAutonomyDist( bot_state_t *bs ) {
int level;
// The autonomy distances have nothing to do with how far a unit should be for following
// Let's make up some constants
static float followAutonomyDist[NUM_BMA] =
{
128, // LOW
256, // MEDIUM
384, // HIGH
};
level = BotGetMovementAutonomyLevel( bs );
if ( level > BMA_HIGH ) {
G_Printf( "BotGetMovementAutonomyRange(): autonomy exceeds BMA_HIGH\n" );
return 0; // should never happen
} else if ( level < BMA_NOVALUE ) {
G_Printf( "BotGetMovementAutonomyRange(): autonomy range less than BMA_NOVALUE\n" );
return 0; // should never happen
}
return followAutonomyDist[level];
}
// Is a bot within the desired distance of its leader?
qboolean BotWithinLeaderFollowDist( bot_state_t *bs ) {
float dist;
gentity_t* leader;
if ( !BotSinglePlayer() && !BotCoop() ) {
return qtrue;
}
// if there's no leader, than sure, we're close enough
if ( bs->leader == -1 ) {
return qtrue;
}
// get the leader
leader = BotGetEntity( bs->leader );
// how far is the bot from the leader?
dist = BotGetFollowAutonomyDist( bs );
dist *= dist;
// is the distance is greater than the follow dist
if ( dist > VectorDistanceSquared( bs->origin, leader->r.currentOrigin ) ) {
return qfalse;
}
return qtrue;
}
// End - TAT 9/18/2002
/*
==================
BotPointWithinMovementAutonomy
==================
*/
qboolean BotPointWithinMovementAutonomy( bot_state_t *bs, bot_goal_t *goal, vec3_t point ) {
float dist;
vec3_t pos;
//
// no autonomy in MP
if ( !BotSinglePlayer() && !BotCoop() ) {
return qtrue;
}
//
if ( !BotGetMovementAutonomyPos( bs, pos ) ) {
return qtrue; // no autonomy yet defined
}
//
dist = VectorDistance( pos, point );
//
// must be outside range of current goal origin
if ( dist > BotGetMovementAutonomyRange( bs, goal ) ) {
// also if we are following a leader, we are allowed to be near them
if ( bs->leader >= 0 ) {
dist = VectorDistance( g_entities[bs->leader].r.currentOrigin, point );
if ( dist > BotGetMovementAutonomyRange( bs, goal ) ) {
return qfalse;
}
} else {
return qfalse;
}
}
//
// RF, had to remove, alcoves were causing this to fail, when they shouldn't be
//if ( !trap_InPVS( pos, point ) ) {
// return qfalse;
//}
//
return qtrue;
}
qboolean BotPointWithinRawMovementAutonomy( bot_state_t *bs, vec3_t point ) {
float dist;
vec3_t pos;
if ( !BotGetMovementAutonomyPos( bs, pos ) ) {
return qtrue; // no autonomy yet defined
}
dist = VectorDistance( pos, point );
// must be outside range of current goal origin
if ( dist > BotGetRawMovementAutonomyRange( bs ) ) {
return qfalse;
}
return qtrue;
}
/*
==================
BotGoalWithinMovementAutonomy
==================
*/
qboolean BotGoalWithinMovementAutonomy( bot_state_t *bs, bot_goal_t *goal, int urgency ) {
int i;
botIgnoreGoal_t *ignoreTrav;
vec3_t pos;
// no autonomy in MP
if ( !BotSinglePlayer() && !BotCoop() ) {
return qtrue;
}
//
if ( !BotGetMovementAutonomyPos( bs, pos ) ) {
return qtrue; // no autonomy yet defined
}
//
// if this goal is in our ignore list, then ignore it
for ( i = 0, ignoreTrav = bs->ignoreGoals; i < MAX_IGNORE_GOALS; i++, ignoreTrav++ ) {
if ( !ignoreTrav->expireTime || ignoreTrav->expireTime <= level.time ) {
continue;
}
if ( ignoreTrav->entityNum != goal->entitynum ) {
continue;
}
if ( ignoreTrav->areanum != goal->areanum ) {
continue;
}
// has our autonomy pos changed?
if ( !VectorCompare( pos, ignoreTrav->autonomyPos ) ) {
ignoreTrav->expireTime = 0;
continue;
}
// this ignoreGoal relates to this goal, so ignore it
return qfalse;
}
//
return BotPointWithinMovementAutonomy( bs, goal, goal->origin );
}
/*
==================
BotGoalForEntity
returns qfalse if the goal is outside our movement autonomy range
if bs is NULL, qtrue is always returned
==================
*/
qboolean BotGoalForEntity( bot_state_t *bs, int entityNum, bot_goal_t *goal, int urgency ) {
vec3_t p;
gentity_t *ent = BotGetEntity( entityNum );
BotClearGoal( goal );
if ( ent ) {
goal->entitynum = entityNum;
if ( VectorLengthSquared( ent->r.absmax ) && ( /*ent->s.eType == ET_MOVER ||*/ ent->s.eType == ET_GENERAL ) ) {
VectorAdd( ent->r.absmax, ent->r.absmin, p );
VectorScale( p, 0.5, p );
if ( bs && !BotGetReachableEntityArea( bs, entityNum, goal ) ) {
return qfalse;
}
} else {
VectorCopy( ent->r.currentOrigin, p );
p[2] += 30;
VectorCopy( ent->r.mins, goal->mins );
VectorCopy( ent->r.maxs, goal->maxs );
}
if ( !goal->areanum ) {
goal->areanum = trap_AAS_PointAreaNum( p );
if ( !goal->areanum || !trap_AAS_AreaReachability( goal->areanum ) ) {
goal->areanum = BotPointAreaNum( -1, p );
}
}
// RF, drop out if no area is found
if ( !goal->areanum ) {
return qfalse;
}
} else {
// TAT - try server entity
g_serverEntity_t *serverEnt = GetServerEntity( entityNum );
if ( serverEnt ) {
goal->entitynum = entityNum;
VectorCopy( serverEnt->origin, p );
p[2] += 30;
if ( !( goal->areanum = BotGetArea( entityNum ) ) ) {
return qfalse;
}
} else {
return qfalse;
}
}
goal->number = -1;
VectorCopy( p, goal->origin );
goal->urgency = urgency;
if ( bs ) {
if ( !BotGoalWithinMovementAutonomy( bs, goal, urgency ) ) {
return qfalse;
}
return qtrue;
} else {
return qtrue;
}
}
// Start - TAT 8/26/2002
// NOTE: The following 3 funcs to create and destroy the bot indicator objects should probably live somewhere else
// but I don't have time to figure out where
#define BOTINDICATORSET_POSTDELAY_TIME 1000 // can only set a waypoint once every 1 seconds
void botindicator_think( gentity_t *ent ) {
if ( level.time - ent->lastHintCheckTime < BOTINDICATORSET_POSTDELAY_TIME ) {
ent->nextthink = level.time + FRAMETIME;
return;
}
ent->nextthink = level.time + FRAMETIME;
}
// a helper func, this returns the angle between 2 vectors
float sAngleBetweenVectors( vec3_t a, vec3_t b ) {
float val = DotProduct( a, b ) / sqrt( DotProduct( a, a ) * DotProduct( b, b ) );
if ( val <= -1.0f ) {
return (float)M_PI;
} else if ( val >= 1.0f ) {
return 0.0f;
}
return acos( val );
}
// Gordon: 25/11/02: removing alot of the statics on these funcs, need them elsewhere, rename?
// Move a point back towards the player a bit, and down to the floor
void sAdjustPointTowardsPlayer( vec3_t playerLoc, vec3_t endPos, qboolean shouldLoop, vec3_t outPos /* Gordon: vec3_t is an array, no need for the pointer (was vec3_t*) */ ) {
vec3_t diff, normalizedDiff, scaled;
trace_t trace;
vec3_t point;
// put in a max number of times we'll do this loop
int timesThrough = 0;
// amount to move back from the wall if our goal is on it
const float wallDist = 75.0f;
// Vector pointing straight up
vec3_t floorNormal;
// setup the floor normal
VectorSet( floorNormal, 0, 0, 1 );
VectorCopy( endPos, point );
do
{
// gotta move the point
// New Loc = endpos + k * normalized(playerLoc - endpos)
// diff = goal point - our start point
VectorSubtract( playerLoc, point, diff );
// normalize that vector
VectorNormalize2( diff, normalizedDiff );
// scale that vector by a constant amount, then stuff it in muzzlePoint
VectorScale( normalizedDiff, wallDist, scaled );
// and finally add back the original end point to get the final goal
VectorAdd( point, scaled, outPos );
// Drop the point to the floor, we don't want the bots trying to climb the walls or anything
// Started with some code from BotDropToFloor
// We're going to do a trace from our goal point to a new location that is way below the old one
// the trace will stop when it hits something solid
VectorSet( diff, outPos[0], outPos[1], outPos[2] - 4096 );
trap_Trace( &trace, outPos, NULL, NULL, diff, -1, MASK_PLAYERSOLID );
// now copy the result of the trace into our loc
VectorCopy( trace.endpos, outPos );
VectorCopy( trace.endpos, point );
timesThrough++;
// only loop if we're supposed to, and don't do it more than 10 times
} while ( shouldLoop && ( timesThrough < 10 ) && sAngleBetweenVectors( floorNormal, trace.plane.normal ) >= M_PI / 4 );
}
// END - TAT 9/16/2002
/*
=====================
BotCheckMovementAutonomy
if we wander outside out movement autonomy, it better be for a bloody good reason
returns qtrue if we have moved outside autonomy range, and need to get back
=====================
*/
qboolean BotCheckMovementAutonomy( bot_state_t *bs, bot_goal_t *goal ) {
// no autonomy in MP
if ( !BotSinglePlayer() && !BotCoop() ) {
return qfalse;
}
// is our current goal too important to abort?
if ( goal->urgency == BGU_MAXIMUM ) {
return qfalse;
}
// are we outside range?
if ( !BotPointWithinMovementAutonomy( bs, goal, bs->origin ) ) {
return qtrue;
}
// is our goal outside the range?
if ( !VectorCompare( vec3_origin, goal->origin ) && !BotPointWithinMovementAutonomy( bs, goal, goal->origin ) ) {
return qtrue;
}
// everything is ok
return qfalse;
}
/*
=================
BotIgnoreGoal
=================
*/
void BotIgnoreGoal( bot_state_t *bs, bot_goal_t *goal, int duration ) {
int i;
botIgnoreGoal_t *ignoreTrav, *ignoreOldest = NULL;
vec3_t pos;
// no autonomy in MP
if ( !BotSinglePlayer() && !BotCoop() ) {
return;
}
if ( !BotGetMovementAutonomyPos( bs, pos ) ) {
return; // no autonomy yet defined
}
// are we sure we aren't already ignoring this goal?
for ( i = 0, ignoreTrav = bs->ignoreGoals; i < MAX_IGNORE_GOALS; i++, ignoreTrav++ ) {
if ( goal->entitynum >= 0 && ignoreTrav->entityNum == goal->entitynum ) {
// found a match, update expiryTime and get out of here
ignoreTrav->areanum = goal->areanum;
ignoreTrav->entityNum = goal->entitynum;
VectorCopy( pos, ignoreTrav->autonomyPos );
ignoreTrav->expireTime = level.time + duration;
return;
}
}
// find a free slot
for ( i = 0, ignoreTrav = bs->ignoreGoals; i < MAX_IGNORE_GOALS; i++, ignoreTrav++ ) {
if ( ignoreTrav->expireTime >= level.time ) {
if ( ( ignoreTrav->expireTime < level.time + duration ) && ( !ignoreOldest || ( ignoreOldest->expireTime < ignoreTrav->expireTime ) ) ) {
ignoreOldest = ignoreTrav;
}
continue;
}
// this slot is free
ignoreTrav->areanum = goal->areanum;
ignoreTrav->entityNum = goal->entitynum;
VectorCopy( pos, ignoreTrav->autonomyPos );
ignoreTrav->expireTime = level.time + duration;
return;
}
}
/*
==================
BotDangerousGoal
returns qtrue if the given goal is in a dangerous area (dynamite, grenade, etc)
==================
*/
#define MAX_DANGER_ENTS 64
qboolean BotDangerousGoal( bot_state_t *bs, bot_goal_t *goal ) {
int i, j;
vec3_t bangPos;
float radius = 0.0;
gentity_t *trav;
int dangerEnts[MAX_DANGER_ENTS];
int dangerEntsCount = 0;
if ( bs->last_dangerousgoal > level.time - 300 ) {
return qfalse;
}
bs->last_dangerousgoal = level.time + rand() % 100;
if ( dangerEntsCount < MAX_DANGER_ENTS ) {
if ( level.time - level.clients[bs->client].lastConstructibleBlockingWarnTime < 5000 ) {
dangerEnts[dangerEntsCount] = level.clients[bs->client].lastConstructibleBlockingWarnEnt;
dangerEntsCount++;
}
}
trav = g_entities;
for ( j = 0; j < level.num_entities; j++, trav++ ) {
if ( !trav->inuse ) {
continue;
}
switch ( trav->s.eType ) {
case ET_FLAMETHROWER_CHUNK:
break;
case ET_PLAYER:
if ( trav->client ) {
// is this player dangerous?
if ( !( trav->client->ps.weapon == WP_PANZERFAUST && trav->client->ps.weaponDelay ) ) {
continue;
}
}
break;
case ET_MISSILE:
switch ( trav->methodOfDeath ) {
case MOD_ARTY:
break;
case MOD_DYNAMITE:
if ( trav->s.teamNum >= 4 ) {
continue; // not armed
}
break;
case MOD_LANDMINE:
if ( !G_LandmineArmed( trav ) ) {
continue;
}
if ( G_LandmineTeam( trav ) != bs->sess.sessionTeam ) {
if ( !G_LandmineSpotted( trav ) ) {
continue;
}
}
if ( goal->entitynum == trav->s.number ) {
continue;
}
case MOD_PANZERFAUST:
case MOD_GRENADE:
case MOD_GRENADE_LAUNCHER:
break;
default:
continue;
}
break;
default:
continue;
}
// save this to the cache
dangerEnts[dangerEntsCount] = j;
dangerEntsCount++;
if ( dangerEntsCount >= MAX_DANGER_ENTS ) {
break; // too many
}
}
for ( i = 0; i < dangerEntsCount; i++ ) {
trav = &g_entities[dangerEnts[i]];
switch ( trav->s.eType ) {
case ET_MISSILE:
switch ( trav->methodOfDeath ) {
case MOD_PANZERFAUST:
case MOD_GRENADE:
case MOD_GRENADE_LAUNCHER:
if ( trav->nextthink > level.time + ( 2000 + 8 * trav->splashRadius ) ) {
continue;
}
if ( !G_PredictMissile( trav, trav->nextthink - level.time, bangPos, trav->methodOfDeath == MOD_PANZERFAUST ? qfalse : qtrue ) ) {
// not dangerous
continue;
}
break;
default:
VectorCopy( trav->r.currentOrigin, bangPos );
break;
}
break;
default:
VectorCopy( trav->r.currentOrigin, bangPos );
break;
}
if ( !trap_InPVS( bs->origin, bangPos ) ) {
continue;
}
// if the bot is zoomed, they cant see it unless they are looking at it
if ( BG_IsScopedWeapon( bs->cur_ps.weapon ) ) {
vec3_t dir, ang;
VectorSubtract( trav->r.currentOrigin, bs->origin, dir );
VectorNormalize( dir );
vectoangles( dir, ang );
if ( !InFieldOfVision( bs->viewangles, 15, ang ) ) {
continue;
}
}
switch ( trav->s.eType ) {
case ET_FLAMETHROWER_CHUNK:
radius = 128;
break;
case ET_PLAYER:
radius = 512;
break;
case ET_CONSTRUCTIBLE:
radius = 32;
break;
case ET_MISSILE:
switch ( trav->methodOfDeath ) {
case MOD_ARTY:
radius = 550;
break;
case MOD_DYNAMITE:
case MOD_LANDMINE:
case MOD_PANZERFAUST:
case MOD_GRENADE:
case MOD_GRENADE_LAUNCHER:
radius = trav->splashRadius;
break;
default: // rain - default
radius = 0;
break;
}
break;
default: // rain - default
radius = 0;
break;
}
// are we far enough away from it now?
if ( VectorDistanceSquared( bs->origin, bangPos ) > SQR( radius + 500 ) ) {
continue;
}
//
// will it explode near the goal?
if ( trav->s.eType == ET_CONSTRUCTIBLE || ( VectorDistanceSquared( bangPos, goal->origin ) < SQR( radius + 90 ) ) || ( VectorDistanceSquared( bangPos, bs->origin ) < SQR( radius + 90 ) ) ) {
// we must avoid this danger
BotClearGoal( &bs->avoid_goal );
bs->avoid_goal.entitynum = trav->s.number;
bs->avoid_goal.number = radius;
if ( trav->s.eType == ET_MISSILE && trav->methodOfDeath == MOD_ARTY ) {
bs->avoid_goal.goalEndTime = level.time + 15000;
} else {
bs->avoid_goal.goalEndTime = level.time + 100;
}
VectorCopy( bangPos, bs->avoid_goal.origin );
bs->avoid_spawnCount = trav->spawnCount;
return qtrue;
}
}
return qfalse;
}
/*
===================
BotNoLeaderPenalty
===================
We can have a penalty associated with not being near a leader.
0 == no penalty, 1 == max penalty
*/
float BotNoLeaderPenalty( bot_state_t *bs ) {
//@COOPTODO. Make this look for any allied player
// Distance to nearest allied player
float distanceToPlayer = 0;
// What sort of penalty do we get for being away from a leader
float noLeaderPenalty = 0;
// How far are we from the player?
distanceToPlayer = VectorDistance( bs->origin, g_entities[0].r.currentOrigin );
// If we're really close, or we're Nazis, don't bother
if ( ( distanceToPlayer <= kBOT_NEAR_LEADER_DISTANCE )
|| ( bs->mpTeam == TEAM_AXIS )
) {
// Jsut use the entity damage ratio
return 0;
}
// If we're really far, max out the farness
if ( distanceToPlayer >= kBOT_FAR_FROM_LEADER_DISTANCE ) {
// Just use the max distance
distanceToPlayer = kBOT_FAR_FROM_LEADER_DISTANCE;
} // if (distanceToPlayer >= kBOT_FAR_FROM_LEADER_DISTANCE)...
// What's our penalty?
noLeaderPenalty = ( distanceToPlayer - kBOT_NEAR_LEADER_DISTANCE )
/ ( kBOT_FAR_FROM_LEADER_DISTANCE - kBOT_NEAR_LEADER_DISTANCE );
// Send the penalty back
return noLeaderPenalty;
}
/*
==================
BotFindLeadersEnemy
Inherit a target from the leader, if one exists.
==================
*/
/*int BotFindLeadersEnemy(bot_state_t *bs, int curenemy, aas_entityinfo_t *entinfo)
{
//
// if we dont have a curenemy, and our leader has an enemy, we should know about it
if (curenemy < 0 && bs->leader_tagent >= 0 && botstates[bs->leader_tagent].inuse) {
if (botstates[bs->leader_tagent].enemy >= 0) {
BotEntityInfo(botstates[bs->leader_tagent].enemy, entinfo);
if (!EntityIsDead(entinfo)) {
// if we can see our leader
if (BotEntityVisible( bs->client, bs->eye, bs->viewangles, 360, bs->leader_tagent, BotGetOrigin(bs->leader_tagent) ) ) {
//found an enemy
bs->enemy = botstates[bs->leader_tagent].enemy;
// if they aren't visible, and we dont want to chase, then ignore them
if (!BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy, NULL) && !BotWantsToChase(bs)) {
bs->enemy = -1;
} else {
if (curenemy >= 0)
bs->enemysight_time = trap_AAS_Time() - 2;
else
bs->enemysight_time = trap_AAS_Time();
// bs->enemysuicide = qfalse;
bs->enemydeath_time = 0;
bs->last_findenemy_enemy = bs->enemy;
if (!bs->last_enemysight || (bs->last_enemysight < level.time - 10000)) {
Bot_ScriptEvent( bs->client, "enemysight", !(g_entities[bs->enemy].r.svFlags & SVF_BOT) ? g_entities[bs->enemy].client->pers.netname : g_entities[bs->enemy].scriptName );
}
bs->last_enemysight = level.time;
return qtrue;
}
}
}
}
}
// We did not inherit a target from our leader
return qfalse;
} // int BotFindLeadersEnemy(bot_state_t *bs, int curenemy) ... */
/*=====================
change the bot's alert state
return whether the alert state was changed
=====================*/
qboolean ChangeBotAlertState( bot_state_t *bs, aistateEnum_t newAlertState, qboolean force ) {
if ( force ) { // should only be used by script action
aistateEnum_t oldState = bs->alertState;
bs->alertState = newAlertState;
bs->alertStateChangeTime = level.time;
bs->alertStateSetTime = level.time;
return ( bs->alertState != oldState );
}
if ( newAlertState != bs->alertState
&& level.time > bs->alertStateAllowChangeTime
// if we are relaxing, make sure we don't relax too quickly, this would prevent model from
// twitching in some weird situations (alert->combat->alert->combat in rapid fire fashion).
&& ( newAlertState > bs->alertState || level.time - bs->alertStateChangeTime > 2000 ) ) {
bs->alertState = newAlertState;
bs->alertStateChangeTime = level.time;
bs->alertStateSetTime = level.time;
return qtrue;
} else if ( newAlertState == bs->alertState ) {
// record the time although we simply reaffirmed our current alert state.
bs->alertStateSetTime = level.time;
}
return qfalse;
}
void BotUpdateAlertStateSquadSensingInfo( bot_state_t *bs, qboolean canSeeTarget, qboolean heardFootSteps, qboolean heardShooting ) {
}
/*
================
BotGetEntitySurfaceSoundCoefficient
Return a modifier for how loud a bot is depending on the surface they're moving on
================
*/
float BotGetEntitySurfaceSoundCoefficient( int clientNum ) {
if ( g_entities[clientNum].surfaceFlags & SURF_NOSTEPS ) {
return 0;
}
if ( g_entities[clientNum].surfaceFlags & SURF_METAL ) {
return 2.0f;
}
if ( g_entities[clientNum].surfaceFlags & SURF_WOOD ) {
return 1.5f;
}
if ( g_entities[clientNum].surfaceFlags & SURF_GRASS ) {
return 0.6f;
}
if ( g_entities[clientNum].surfaceFlags & SURF_GRAVEL ) {
return 1.2f;
}
if ( g_entities[clientNum].surfaceFlags & SURF_ROOF ) {
return 1.3f;
}
if ( g_entities[clientNum].surfaceFlags & SURF_SNOW ) {
return 1.0f;
}
if ( g_entities[clientNum].surfaceFlags & SURF_CARPET ) {
return 0.9f;
}
return 1.0f;
}
//
// BotIsValidTarget
//
// Description: Is this target at all valid?
// Written: 10/31/2002
//
qboolean BotIsValidTarget
(
bot_state_t *bs,
int target,
int curenemy
) {
// Local Variables ////////////////////////////////////////////////////////
// Info about the target
aas_entityinfo_t entinfo;
///////////////////////////////////////////////////////////////////////////
// Fill in the info for this potential target
BotEntityInfo( target, &entinfo );
// We are not our own enemy. Ever. At least in the game. In real life, it
// happens all the time.
if ( target == bs->client ) {
return qfalse;
}
// if it's the current enemy, don't do more analysis
if ( target == curenemy ) {
return qfalse;
}
// Fill in the info for this potential target
BotEntityInfo( target, &entinfo );
// Skip clients who are no longer good.
if ( !entinfo.valid ) {
return qfalse;
}
// See if the enemy has a notarget on it, and skip if so.
if ( g_entities[target].flags & FL_NOTARGET ) {
return qfalse;
}
//if on the same team
if ( BotSameTeam( bs, target ) ) {
return qfalse;
}
// xkan, 1/6/2003 - if target is civilian
if ( g_entities[target].client->isCivilian ) {
return qfalse; // don't target civilian
}
//if the enemy isn't dead and the enemy isn't the bot self
if ( EntityIsDead( &entinfo ) || entinfo.number == bs->entitynum ) {
return qfalse;
}
//if the enemy is invisible and not shooting
if ( EntityIsInvisible( &entinfo ) && !EntityIsShooting( &entinfo ) ) {
return qfalse;
}
// TAT 10/10/2002
// If it's disguised, don't target them
if ( g_entities[target].client->ps.powerups[PW_OPS_DISGUISED] ) {
return qfalse;
}
// If we are not mounted on an MG42, or sniping, then ignore clients that are not in a valid area
if ( !( bs->cur_ps.eFlags & EF_MG42_ACTIVE ) && !( bs->flags & BFL_SNIPING ) ) {
// if they are not in a valid area
if ( !BotGetArea( target ) ) {
return qfalse;
}
} // if (!(bs->script.flags & BSFL_MOUNT_MG42))...
// He's OK to use as a target
return qtrue;
}
//
// BotIsValidTarget
//
//
// BotFindEnemies
//
// Description: Set up our danger spots
// Written: 12/26/2002
//
void BotFindEnemies
(
bot_state_t *bs,
int *dangerSpots,
int *dangerSpotCount
) {
int i;
float dist;
aas_entityinfo_t entinfo;
vec3_t dir;
// Loop through all potential targets
for ( i = 0; i < level.maxclients; i++ )
{
// Fill in the info for this potential target
BotEntityInfo( i, &entinfo );
// Is it a valid target? Puts all checks in one place
if ( !BotIsValidTarget( bs, i, -1 ) ) {
continue;
}
//calculate the distance towards the enemy
VectorSubtract( entinfo.origin, bs->origin, dir );
dist = VectorLength( dir );
// if this enemy is too far, skip her
if ( dist > kBOT_MAX_RETREAT_ENEMY_DIST ) {
continue;
}
// Add this guy's area to the list
dangerSpots[*dangerSpotCount] = BotGetArea( i );
// Record that we have one more guy
( *dangerSpotCount )++;
}
}
//
// BotFindEnemies
//
float BotWeaponRange( bot_state_t *bs, int weaponnum ) {
switch ( weaponnum ) {
// dont use unless manually forced
case WP_PLIERS:
case WP_MEDKIT:
case WP_AMMO:
case WP_MEDIC_SYRINGE:
case WP_LANDMINE:
case WP_SATCHEL:
case WP_SATCHEL_DET:
case WP_TRIPMINE:
case WP_MEDIC_ADRENALINE:
case WP_DYNAMITE:
case WP_ARTY:
return -2.0f;
// short range
case WP_KNIFE:
return 256.0f;
case WP_GRENADE_PINEAPPLE:
case WP_GRENADE_LAUNCHER:
return 512.0f;
case WP_LUGER:
case WP_COLT:
case WP_AKIMBO_COLT:
case WP_AKIMBO_LUGER:
case WP_SILENCER:
case WP_SILENCED_COLT:
case WP_FLAMETHROWER:
case WP_SMOKE_MARKER:
case WP_SMOKE_BOMB:
case WP_AKIMBO_SILENCEDCOLT:
case WP_AKIMBO_SILENCEDLUGER:
return 1000.f;
// low-mid range
case WP_MP40:
case WP_THOMPSON:
case WP_STEN:
case WP_GPG40:
case WP_M7:
return 2000.f;
// mid range
case WP_KAR98:
case WP_CARBINE:
case WP_GARAND:
case WP_MOBILE_MG42:
case WP_K43:
case WP_FG42:
return 3000.f;
// long range
case WP_PANZERFAUST:
return 7000.f;
case WP_MORTAR:
case WP_MORTAR_SET:
return 4500.f;
case WP_BINOCULARS:
case WP_GARAND_SCOPE:
case WP_K43_SCOPE:
case WP_FG42SCOPE:
return 6500.f;
default:
return 999999.0;
}
}
qboolean BotHasWeaponWithRange( bot_state_t *bs, float dist ) {
int i, *ammo;
ammo = bs->cur_ps.ammo;
// if we are mounted on an MG42, always return true
if ( g_entities[bs->client].s.eFlags & EF_MG42_ACTIVE ) {
return qtrue;
}
for ( i = 0; i < WP_NUM_WEAPONS; i++ ) {
if ( COM_BitCheck( bs->cur_ps.weapons, i ) ) {
if ( !BotGotEnoughAmmoForWeapon( bs, i ) ) {
continue;
}
if ( BotWeaponRange( bs, i ) < dist ) {
continue;
}
// found one
return qtrue;
}
}
return qfalse;
}
/*
==================
BotSortClientsByDistance
==================
*/
void BotSortClientsByDistance( vec3_t srcpos, int *sorted, qboolean hasPanzer ) {
int i, j, best = 0;
float distances[MAX_CLIENTS];
int indexes[MAX_CLIENTS];
float closest;
memset( distances, 0, sizeof( distances ) );
memset( indexes, 0, sizeof( indexes ) );
//
// build the distances
for ( i = 0; i < level.numConnectedClients; i++ ) {
int k = level.sortedClients[i];
distances[i] = VectorDistanceSquared( srcpos, level.clients[k].ps.origin );
if ( hasPanzer && level.clients[k].ps.eFlags & EF_MG42_ACTIVE ) {
distances[i] /= ( 3 * 3 );
}
indexes[i] = k;
}
//
// now build the output list
for ( i = 0; i < level.numConnectedClients; i++ ) {
closest = -1;
for ( j = 0; j < level.numConnectedClients; j++ ) {
if ( indexes[j] < 0 ) {
continue;
}
if ( closest < 0 || distances[j] < closest ) {
best = j;
closest = distances[j];
}
}
sorted[i] = indexes[best];
indexes[best] = -1;
}
}
/*
==================
BotFindEnemyMP
Find a new target for bots in multiplayer
==================
*/
int BotFindEnemyMP( bot_state_t *bs, int curenemy, qboolean ignoreViewRestrictions ) {
int i, healthdecrease, j;
float fov, dist, curdist, alertness, easyfragger, vis;
aas_entityinfo_t entinfo, curenemyinfo;
vec3_t dir, ang;
int heardShooting, heardFootSteps;
int distanceSorted[MAX_CLIENTS];
int startTime = 0;
if ( bot_profile.integer == 1 ) {
startTime = trap_Milliseconds();
}
if ( bs->last_findenemy == level.time ) {
if ( bs->last_findenemy_enemy >= 0 || curenemy >= 0 ) {
bs->enemy = bs->last_findenemy_enemy;
}
if ( bot_profile.integer == 1 ) {
botTime_FindEnemy += trap_Milliseconds() - startTime;
}
return ( bs->last_findenemy_enemy > -1 );
}
bs->last_findenemy = level.time;
alertness = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_ALERTNESS, 0, 1 );
easyfragger = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_EASY_FRAGGER, 0, 1 );
//check if the health decreased
healthdecrease = bs->lasthealth > bs->inventory[INVENTORY_HEALTH];
//remember the current health value
bs->lasthealth = bs->inventory[INVENTORY_HEALTH];
//
if ( curenemy >= 0 ) {
BotEntityInfo( curenemy, &curenemyinfo );
if ( EntityIsDead( &curenemyinfo ) ) {
bs->enemy = -1;
curenemy = -1;
curdist = 0;
} else {
VectorSubtract( curenemyinfo.origin, bs->origin, dir );
curdist = VectorLength( dir );
}
} else {
curdist = 0;
}
// See if we're inheriting a target from our leader
// if (BotFindLeadersEnemy( bs, curenemy, &entinfo))
// return qtrue;
BotSortClientsByDistance( bs->origin, distanceSorted, COM_BitCheck( bs->cur_ps.weapons, WP_PANZERFAUST ) );
//
heardShooting = qfalse;
heardFootSteps = qfalse;
for ( i = 0; i < level.numConnectedClients; i++ ) {
// j = level.sortedClients[i];
j = distanceSorted[i];
if ( j == bs->client ) {
continue;
}
//if it's the current enemy
if ( j == curenemy ) {
continue;
}
BotEntityInfo( j, &entinfo );
if ( !entinfo.valid ) {
continue;
}
// See if the enemy has a notarget on it
if ( g_entities[j].flags & FL_NOTARGET ) {
continue;
}
//if on the same team
if ( BotSameTeam( bs, j ) ) {
continue;
}
//if the enemy is dead, Gordon: and not in limbo, still want to kill em totally
if ( EntityIsDead( &entinfo ) && !EntityInLimbo( &entinfo ) ) {
continue;
}
//if the enemy is invisible and not shooting
//if(EntityIsInvisible(&entinfo) && !EntityIsShooting(&entinfo)) {
// continue;
//}
// TAT 10/10/2002
// If it's disguised, don't target them
if ( g_entities[j].client->ps.powerups[PW_OPS_DISGUISED] ) {
continue;
}
//if not an easy fragger don't shoot at chatting players
if ( easyfragger < 0.5 && EntityIsChatting( &entinfo ) ) {
continue;
}
// this is a complete check, which takes into account all forms of view restriction
if ( !ignoreViewRestrictions && !BotEntityWithinView( bs, j ) ) {
continue;
}
//
//if (lastteleport_time > trap_AAS_Time() - 3) {
// VectorSubtract(entinfo.origin, lastteleport_origin, dir);
// if (VectorLength(dir) < 70) continue;
//}
//calculate the distance towards the enemy
VectorSubtract( entinfo.origin, bs->origin, dir );
dist = VectorLength( dir );
vectoangles( dir, ang );
// Gordon: if using panzerfaust, attack guys on mg42s more readily
if ( COM_BitCheck( bs->cur_ps.weapons, WP_PANZERFAUST ) && ( g_entities[j].client->ps.eFlags & EF_MG42_ACTIVE ) ) {
dist /= 3;
}
// If this is enemy is attempting to disarm dynamite/build, they take preference!
if ( g_entities[j].client->ps.weapon == WP_PLIERS && g_entities[j].client->touchingTOI ) {
dist /= 3;
}
//if this enemy is further away than the current one
if ( curenemy >= 0 && dist > curdist ) {
continue;
}
//if the bot has no
// RF, disabled, doesnt work well with snipers
//if (dist > 900 + alertness * 4000) continue;
// weapons have range limit
// if we cant get to them, or we dont want to chase them, ignore them
if ( ( !BotWantsToChase( bs ) || !trap_AAS_AreaTravelTimeToGoalArea( bs->areanum, bs->origin, BotGetArea( j ), bs->tfl ) )
&& !BotHasWeaponWithRange( bs, dist ) ) {
continue;
}
// if the bot's health decreased or the enemy is shooting
if ( curenemy < 0 && ( healthdecrease || EntityIsShooting( &entinfo ) ) ) {
fov = 360;
} else {
fov = 90 + 270 - ( ( dist > 810 ? 810 : dist ) / 3 );
}
// RF, smaller fov with sniperrifle
if ( BG_IsScopedWeapon( bs->weaponnum ) ) {
fov = 10;
}
VectorSubtract( bs->origin, entinfo.origin, dir );
/* //if the enemy is quite far away, not shooting and the bot is not damaged
if (curenemy < 0 && dist > 200 && !healthdecrease && !EntityIsShooting(&entinfo))
{
//check if we can avoid this enemy
vectoangles(dir, angles);
//if the bot isn't in the fov of the enemy
if (!InFieldOfVision(g_entities[j].client->ps.viewangles, 120, angles)) {
//update some stuff for this enemy
BotUpdateBattleInventory(bs, j);
//if the bot doesn't really want to fight
if (BotWantsToRetreat(bs))
continue;
}
}
*/
// Gordon: with our aggresive botclipping this is a LOT of places, so still attack them,
// NOTE: perhaps change this to check if not valid area, then see if we can hit with long range weapon, if not, THEN ignore
// if we are not mounted on an MG42, or sniping, then ignore clients that are not in a valid area
/* if (!(bs->cur_ps.eFlags & EF_MG42_ACTIVE) && !(bs->flags & BFL_SNIPING))
{
// if they are not in a valid area
if (!BotGetArea( j ))
continue;
} */
// check if the enemy visibility. This does a trace to make sure you're actually visible
vis = BotEntityVisible( bs->entitynum, bs->eye, bs->viewangles, fov, j, NULL );
// If we're not visible, and we're not heard, skip out
if ( ( vis <= 0 ) && !heardShooting && !heardFootSteps ) {
continue;
}
// found an enemy
bs->enemy = entinfo.number;
if ( curenemy >= 0 || bs->last_fire > level.time - 2000 || bs->last_enemysight > level.time - 2000 ) {
bs->enemysight_time = trap_AAS_Time() - 2;
} else {
bs->enemysight_time = trap_AAS_Time();
}
// bs->enemysuicide = qfalse;
bs->enemydeath_time = 0;
/* if (!bs->last_enemysight || (bs->last_enemysight < level.time - 10000))
{
Bot_ScriptEvent( bs->client, "enemysight", !(g_entities[bs->enemy].r.svFlags & SVF_BOT) ? g_entities[bs->enemy].client->pers.netname : g_entities[bs->enemy].scriptName );
}*/
if ( bot_profile.integer == 1 ) {
botTime_FindEnemy += trap_Milliseconds() - startTime;
}
bs->last_findenemy_enemy = bs->enemy;
bs->last_enemysight = level.time;
return qtrue;
}
if ( bot_profile.integer == 1 ) {
botTime_FindEnemy += trap_Milliseconds() - startTime;
}
bs->last_findenemy_enemy = -1;
return qfalse;
} // int BotFindEnemyMP(bot_state_t *bs, int curenemy) ...
// Reaction time for this bot
float BotGetReactionTime( bot_state_t *bs ) {
// Just use the scripted value
return bs->attribs[BOT_REACTION_TIME]; // * trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_REACTIONTIME, 0, 1);
}
float swayrand( float x, float y ) {
return sin( level.time / 1000.0 * x * M_PI * 2 ) * cos( level.time / 1000.0 * y * M_PI * 2 );
}
/*
==================
BotAimAtEnemy
==================
*/
void BotAimAtEnemy( bot_state_t *bs ) {
int /*i, */ enemyvisible;
float dist, f, aim_skill, aim_accuracy, speed, reactiontime;
vec3_t dir, bestorigin, end, start, groundtarget, cmdmove, enemyvelocity;
vec3_t mins = {-4,-4,-4}, maxs = {4, 4, 4};
weaponinfo_t wi;
aas_entityinfo_t entinfo;
bot_goal_t goal;
bsp_trace_t trace;
vec3_t target;
//if the bot has no enemy
if ( bs->enemy < 0 ) {
return;
}
//BotAI_Print(PRT_MESSAGE, "client %d: aiming at client %d\n", bs->entitynum, bs->enemy);
aim_skill = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_SKILL, 0, 1 );
aim_accuracy = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_ACCURACY, 0, 1 );
if ( aim_skill > 0.95 ) {
//don't aim too early
reactiontime = 0.5 * trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_REACTIONTIME, 0, 1 );
if ( bs->enemysight_time > trap_AAS_Time() - reactiontime ) {
return;
}
if ( bs->teleport_time > trap_AAS_Time() - reactiontime ) {
return;
}
}
//get the weapon information
trap_BotGetWeaponInfo( bs->ws, bs->weaponnum, &wi );
// if we have mounted an mg42
if ( g_entities[bs->client].s.eFlags & EF_MG42_ACTIVE ) {
// Gordon: lowering all of these from 0.8 per atvi request
aim_accuracy = 0.65;
aim_skill = 0.65;
} else {
//get the weapon specific aim accuracy and or aim skill
if ( bs->weaponnum == WP_GRENADE_LAUNCHER || bs->weaponnum == WP_GRENADE_PINEAPPLE ) {
aim_accuracy = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_ACCURACY_GRENADELAUNCHER, 0, 1 );
aim_skill = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_SKILL_GRENADELAUNCHER, 0, 1 );
}
if ( bs->weaponnum == WP_PANZERFAUST || bs->weaponnum == WP_MORTAR_SET ) {
aim_accuracy = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_ACCURACY_ROCKETLAUNCHER, 0, 1 );
aim_skill = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_SKILL_ROCKETLAUNCHER, 0, 1 );
}
if ( bs->weaponnum == WP_FLAMETHROWER ) {
aim_accuracy = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_ACCURACY_FLAMETHROWER, 0, 1 );
}
if ( bs->weaponnum == WP_MOBILE_MG42 && bs->cur_ps.eFlags & EF_PRONE ) {
aim_accuracy = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_ACCURACY_FLAMETHROWER, 0, 1 );
}
}
//
if ( aim_accuracy <= 0 ) {
aim_accuracy = 0.0001;
}
//get the enemy entity information
BotEntityInfo( bs->enemy, &entinfo );
//if the enemy is invisible then shoot crappy most of the time
if ( EntityIsInvisible( &entinfo ) ) {
if ( random() > 0.1 ) {
aim_accuracy *= 0.4;
}
}
//
VectorSubtract( entinfo.origin, entinfo.lastvisorigin, enemyvelocity );
VectorScale( enemyvelocity, 1 / entinfo.update_time, enemyvelocity );
//enemy origin and velocity is remembered every 0.5 seconds
if ( bs->enemyposition_time < trap_AAS_Time() ) {
//
bs->enemyposition_time = trap_AAS_Time() + 0.5;
VectorCopy( enemyvelocity, bs->enemyvelocity );
VectorCopy( entinfo.origin, bs->enemyorigin );
}
// aiming gets better with time
f = trap_AAS_Time() - bs->enemysight_time;
if ( f > 2.0 ) {
f = 2.0;
}
aim_accuracy += 0.2 * f / 2.0;
if ( aim_accuracy > 1.0 ) {
aim_accuracy = 1.0;
}
// better if enemy is not moving
f = VectorLength( bs->enemyvelocity );
if ( f > 200 ) {
f = 200;
}
aim_accuracy += 0.2 * ( 0.5 - ( f / 200.0 ) );
//if not extremely skilled
if ( aim_skill < 0.96 ) {
VectorSubtract( entinfo.origin, bs->enemyorigin, dir );
//if the enemy moved a bit
if ( VectorLengthSquared( dir ) > SQR( 48 ) ) {
//if the enemy changed direction
if ( DotProduct( bs->enemyvelocity, enemyvelocity ) < 0 ) {
//aim accuracy should be worse now
aim_accuracy *= 0.45;
}
}
}
if ( aim_accuracy > 1.0 ) {
aim_accuracy = 1.0;
}
//check visibility of enemy
enemyvisible = BotEntityVisible( bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy, NULL );
//if the enemy is visible
if ( enemyvisible ) {
//
VectorCopy( entinfo.origin, bestorigin );
bestorigin[2] += g_entities[bs->enemy].client->ps.viewheight - 6;
//get the start point shooting from
//NOTE: the x and y projectile start offsets are ignored
VectorCopy( bs->origin, start );
start[2] += bs->cur_ps.viewheight;
//start[2] += wi.offset[2];
//
BotAI_Trace( &trace, start, mins, maxs, bestorigin, bs->entitynum, MASK_SHOT );
//if the enemy is NOT hit
if ( trace.fraction <= 1 && trace.ent != entinfo.number ) {
bestorigin[2] = g_entities[bs->enemy].r.absmax[2] - 2;
}
//if it is not an instant hit weapon the bot might want to predict the enemy
if ( wi.speed ) {
//
VectorSubtract( bestorigin, bs->origin, dir );
dist = VectorLength( dir );
VectorSubtract( entinfo.origin, bs->enemyorigin, dir );
//if the enemy is NOT pretty far away and strafing just small steps left and right
if ( !( dist > 100 && VectorLengthSquared( dir ) < SQR( 32 ) ) ) {
//if skilled anough do exact prediction
if ( aim_skill > 0.8 &&
//if the weapon is ready to fire
bs->cur_ps.weaponstate == WEAPON_READY ) {
aas_clientmove_t move;
vec3_t origin;
VectorSubtract( entinfo.origin, bs->origin, dir );
//distance towards the enemy
dist = VectorLength( dir );
//direction the enemy is moving in
VectorSubtract( entinfo.origin, entinfo.lastvisorigin, dir );
//
VectorScale( dir, 1 / entinfo.update_time, dir );
//
VectorCopy( entinfo.origin, origin );
origin[2] += 1;
//
VectorClear( cmdmove );
//AAS_ClearShownDebugLines();
trap_AAS_PredictClientMovement( &move, bs->enemy, origin,
PRESENCE_CROUCH, qfalse,
dir, cmdmove, 0,
dist * 10 / wi.speed, 0.1, 0, 0, qfalse );
VectorCopy( move.endpos, bestorigin );
//BotAI_Print(PRT_MESSAGE, "%1.1f predicted speed = %f, frames = %f\n", trap_AAS_Time(), VectorLength(dir), dist * 10 / wi.speed);
}
//if not that skilled do linear prediction
else if ( aim_skill > 0.4 ) {
VectorSubtract( entinfo.origin, bs->origin, dir );
//distance towards the enemy
dist = VectorLength( dir );
//direction the enemy is moving in
VectorSubtract( entinfo.origin, entinfo.lastvisorigin, dir );
dir[2] = 0;
//
speed = VectorNormalize( dir ) / entinfo.update_time;
//botimport.Print(PRT_MESSAGE, "speed = %f, wi->speed = %f\n", speed, wi->speed);
//best spot to aim at
VectorMA( entinfo.origin, ( dist / wi.speed ) * speed, dir, bestorigin );
}
}
} // if (wi.speed) ...
//if the projectile does radial damage
if ( ( bs->weaponnum != WP_FLAMETHROWER ) && ( wi.proj.damagetype & DAMAGETYPE_RADIAL ) ) {
//if the enemy isn't standing significantly higher than the bot
if ( entinfo.origin[2] < bs->origin[2] + 16 ) {
//try to aim at the ground in front of the enemy
VectorCopy( entinfo.origin, end );
end[2] -= 64;
BotAI_Trace( &trace, bestorigin, NULL, NULL, end, entinfo.number, MASK_SHOT );
//
VectorCopy( bestorigin, groundtarget );
if ( trace.startsolid ) {
groundtarget[2] = entinfo.origin[2] - 16;
} else { groundtarget[2] = trace.endpos[2];}
//trace a line from projectile start to ground target
BotAI_Trace( &trace, start, NULL, NULL, groundtarget, bs->entitynum, MASK_SHOT );
//if hitpoint is not vertically too far from the ground target
if ( fabs( trace.endpos[2] - groundtarget[2] ) < 50 ) {
VectorSubtract( trace.endpos, groundtarget, dir );
//if the hitpoint is near anough the ground target
if ( VectorLengthSquared( dir ) < SQR( 100 ) ) {
VectorSubtract( trace.endpos, start, dir );
//if the hitpoint is far anough from the bot
if ( VectorLengthSquared( dir ) > SQR( 100 ) ) {
//check if the bot is visible from the ground target
trace.endpos[2] += 1;
BotAI_Trace( &trace, trace.endpos, NULL, NULL, entinfo.origin, entinfo.number, MASK_SHOT );
if ( trace.fraction >= .99f ) {
//botimport.Print(PRT_MESSAGE, "%1.1f aiming at ground\n", AAS_Time());
VectorCopy( groundtarget, bestorigin );
}
}
}
}
}
}
if ( BotScopedWeapon( bs->cur_ps.weapon ) ) {
bestorigin[0] += 20 * swayrand( 0.47f, 0.53f ) * ( 1 - aim_accuracy );
bestorigin[1] += 20 * swayrand( 0.44f, 0.57f ) * ( 1 - aim_accuracy );
bestorigin[2] += 10 * swayrand( 0.52f, 0.49f ) * ( 1 - aim_accuracy );
}
} else {
//
VectorCopy( bs->lastenemyorigin, bestorigin );
bestorigin[2] += 8;
//if the bot is skilled anough
if ( aim_skill > 0.5 ) {
//do prediction shots around corners
// if (wi.number == WP_BFG || //----(SA) removing old weapon references
if ( wi.number == WP_PANZERFAUST ||
wi.number == WP_GRENADE_LAUNCHER ||
wi.number == WP_GRENADE_PINEAPPLE ) {
//create the chase goal
goal.entitynum = bs->client;
goal.areanum = bs->areanum;
VectorCopy( bs->eye, goal.origin );
VectorSet( goal.mins, -8, -8, -8 );
VectorSet( goal.maxs, 8, 8, 8 );
//
if ( trap_BotPredictVisiblePosition( bs->lastenemyorigin, bs->lastenemyareanum, &goal, bs->tfl, target ) ) {
VectorCopy( target, bestorigin );
bestorigin[2] -= 20;
}
aim_accuracy = 1;
}
}
}
//
if ( enemyvisible ) {
BotAI_Trace( &trace, bs->eye, NULL, NULL, bestorigin, bs->entitynum, MASK_SHOT );
VectorCopy( trace.endpos, bs->aimtarget );
} else {
VectorCopy( bestorigin, bs->aimtarget );
}
//get aim direction
VectorSubtract( bestorigin, bs->eye, dir );
//
//distance towards the enemy
dist = VectorLength( dir );
if ( dist > 150 ) {
dist = 150;
}
f = 0.6 + dist / 150 * 0.4;
aim_accuracy *= f;
//
//add some random stuff to the aim direction depending on the aim accuracy
if ( aim_accuracy < 0.8 ) {
VectorNormalize( dir );
dir[0] += 0.3 * swayrand( 0.58f, 0.37f ) * ( 1 - aim_accuracy );
dir[1] += 0.3 * swayrand( 0.54f, 0.47f ) * ( 1 - aim_accuracy );
dir[2] += 0.3 * swayrand( 0.61f, 0.38f ) * ( 1 - aim_accuracy );
}
//set the ideal view angles
vectoangles( dir, bs->ideal_viewangles );
//take the weapon spread into account for lower skilled bots
bs->ideal_viewangles[PITCH] += 6 * wi.vspread * swayrand( 0.36f, 0.45f ) * ( 1 - aim_accuracy );
bs->ideal_viewangles[PITCH] = AngleMod( bs->ideal_viewangles[PITCH] );
bs->ideal_viewangles[YAW] += 6 * wi.hspread * swayrand( 0.654f, 0.54f ) * ( 1 - aim_accuracy );
bs->ideal_viewangles[YAW] = AngleMod( bs->ideal_viewangles[YAW] );
// adjust for sway
if ( BotScopedWeapon( bs->weaponnum ) ) {
float spreadfrac, phase;
vec3_t swayang;
gentity_t *ent = BotGetEntity( bs->client );
int i;
spreadfrac = ent->client->currentAimSpreadScale;
// rotate 'forward' vector by the sway
phase = level.time / 1000.0 * ZOOM_PITCH_FREQUENCY * M_PI * 2;
swayang[PITCH] = ZOOM_PITCH_AMPLITUDE * sin( phase ) * ( spreadfrac + ZOOM_PITCH_MIN_AMPLITUDE );
phase = level.time / 1000.0 * ZOOM_YAW_FREQUENCY * M_PI * 2;
swayang[YAW] = ZOOM_YAW_AMPLITUDE * sin( phase ) * ( spreadfrac + ZOOM_YAW_MIN_AMPLITUDE );
swayang[ROLL] = 0;
// adjust sway-correction with aim_skill
aim_skill = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_SKILL_SNIPERRIFLE, 0, 1 );
VectorMA( bs->ideal_viewangles, -aim_skill, swayang, bs->ideal_viewangles );
for ( i = 0; i < 3; i++ ) bs->ideal_viewangles[i] = AngleNormalize360( bs->ideal_viewangles[i] );
} else if ( !( bs->cur_ps.eFlags & EF_MG42_ACTIVE ) && ( bs->weaponnum == WP_MOBILE_MG42 ) ) {
// aim downward slightly to adjust for kick
bs->ideal_viewangles[PITCH] += 4;
}
// if smoke grenade, aim a bit higher
if ( bs->cur_ps.weapon == WP_SMOKE_MARKER ) {
if ( bs->ideal_viewangles[PITCH] > -60 ) {
bs->ideal_viewangles[PITCH] = -60;
}
}
//if the bot is really accurate and has the enemy in view for some time
if ( !( bs->cur_ps.eFlags & EF_MG42_ACTIVE ) && aim_accuracy > 0.95 && bs->enemysight_time < trap_AAS_Time() - 2 ) {
//set the view angles directly
if ( bs->ideal_viewangles[PITCH] > 180 ) {
bs->ideal_viewangles[PITCH] -= 360;
}
if ( AngleNormalize180( bs->ideal_viewangles[YAW] - bs->viewangles[YAW] ) < 25 ) {
VectorCopy( bs->ideal_viewangles, bs->viewangles );
trap_EA_View( bs->client, bs->viewangles );
}
}
}
/*
==================
BotAimAtEnemySP
==================
*/
void BotAimAtEnemySP( bot_state_t *bs ) {
}
/*
==================
BotMoveWhileFiring
==================
*/
qboolean BotMoveWhileFiring( int weapon ) {
switch ( weapon ) {
case WP_BINOCULARS:
case WP_PANZERFAUST:
case WP_MOBILE_MG42:
case WP_MORTAR:
case WP_GARAND_SCOPE:
case WP_K43_SCOPE:
case WP_FG42SCOPE:
return qfalse;
}
//
return qtrue;
}
/*
==================
BotThrottleWeapon
==================
*/
qboolean BotThrottleWeapon( int weapon ) {
switch ( weapon ) {
case WP_PANZERFAUST:
case WP_FLAMETHROWER:
case WP_GRENADE_LAUNCHER:
case WP_GRENADE_PINEAPPLE:
return qfalse;
}
//
return qtrue;
}
/*
===================
BotScopedWeapon
===================
*/
qboolean BotScopedWeapon( int weapon ) {
switch ( weapon ) {
case WP_BINOCULARS:
case WP_GARAND_SCOPE:
case WP_K43_SCOPE:
case WP_FG42SCOPE:
return qtrue;
}
//
return qfalse;
}
/*
==================
BotCheckAttack
returns qfalse if the enemy is unreachable. qtrue if it is possible to attack the enemy (this doesnt always mean
the trigger is pulled).
==================
*/
qboolean BotCheckAttack( bot_state_t *bs ) {
float reactiontime, fov, firethrottle, dist, aimskill = 0.0;
bsp_trace_t bsptrace;
//float selfpreservation;
vec3_t forward, right, start, end, dir, angles;
weaponinfo_t wi;
bsp_trace_t trace;
aas_entityinfo_t entinfo;
vec3_t mins = {-12, -12, -12}, maxs = {12, 12, 12};
int i, fcnt, ecnt;
int mask;
if ( bs->enemy < 0 ) {
return qfalse;
}
// Gordon: limit teh fire rate of the bots on these weapons
if ( ( bs->weaponnum == WP_GARAND_SCOPE || bs->weaponnum == WP_K43_SCOPE ) && ( level.time - bs->last_fire ) < 800 ) {
// pause for a bit
return qtrue;
}
{
float range = BotWeaponRange( bs, bs->weaponnum );
if ( SQR( range ) < VectorDistanceSquared( bs->origin, BotGetOrigin( bs->enemy ) ) ) {
// cant reach them
return qfalse;
}
}
//
// if we have recently called this routine, then repeat the last result
if ( ( bs->last_botcheckattack_weapon == bs->weaponnum ) && ( bs->enemy == bs->last_botcheckattack_enemy ) && ( bs->last_botcheckattack > level.time - 250 ) ) {
// if our last call passed, then let it through
if ( bs->last_botcheckattack <= bs->last_fire ) {
goto passed;
}
}
bs->last_botcheckattack = level.time + rand() % 80;
bs->last_botcheckattack_weapon = bs->weaponnum;
bs->last_botcheckattack_enemy = bs->enemy;
reactiontime = BotGetReactionTime( bs ); //trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_REACTIONTIME, 0, 1);
aimskill = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_SKILL, 0, 1 );
if ( bs->enemysight_time > trap_AAS_Time() - reactiontime ) {
// wait until we have had time to react
return qtrue;
}
if ( bs->teleport_time > trap_AAS_Time() - reactiontime ) {
return qtrue;
}
if ( bs->weaponchange_time > trap_AAS_Time() - 0.1 ) {
return qtrue;
}
if ( BotThrottleWeapon( bs->weaponnum ) ) {
//check fire throttle characteristic
if ( bs->firethrottlewait_time > trap_AAS_Time() ) {
return qtrue;
}
firethrottle = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_FIRETHROTTLE, 0, 1 );
if ( bs->firethrottleshoot_time < trap_AAS_Time() ) {
if ( random() > firethrottle ) {
bs->firethrottlewait_time = trap_AAS_Time() + firethrottle;
bs->firethrottleshoot_time = 0;
} else {
bs->firethrottleshoot_time = trap_AAS_Time() + 1 - firethrottle;
bs->firethrottlewait_time = 0;
}
}
}
// always pass if grenade is live
if ( ( bs->cur_ps.weapon == WP_GRENADE_LAUNCHER || bs->cur_ps.weapon == WP_GRENADE_PINEAPPLE ) && bs->cur_ps.grenadeTimeLeft ) {
goto passed;
}
//
// always pass if smoke grenade
if ( bs->cur_ps.weapon == WP_SMOKE_MARKER ) {
goto passed;
}
//
BotEntityInfo( bs->enemy, &entinfo );
VectorSubtract( entinfo.origin, bs->eye, dir );
//
if ( bs->weaponnum == WP_KNIFE ) {
if ( VectorLengthSquared( dir ) > SQR( 256 ) ) {
return qfalse; // too far away
}
}
//
if ( VectorLengthSquared( dir ) < SQR( 100 ) ) {
fov = 120;
} else {
fov = 50;
}
vectoangles( dir, angles );
if ( !InFieldOfVision( bs->viewangles, fov, angles ) ) {
return qtrue;
}
// set mask based on weapon
mask = MASK_SHOT;
switch ( bs->cur_ps.weapon ) {
case WP_PANZERFAUST:
case WP_GRENADE_LAUNCHER:
case WP_GRENADE_PINEAPPLE:
mask = MASK_MISSILESHOT;
}
BotAI_Trace( &bsptrace, bs->eye, NULL, NULL, bs->aimtarget, bs->client, mask );
if ( !( bs->cur_ps.weapon == WP_GRENADE_LAUNCHER || bs->cur_ps.weapon == WP_GRENADE_PINEAPPLE ) ) {
if ( bsptrace.fraction < .99f && bsptrace.ent != bs->enemy ) {
return qfalse;
}
}
//get the weapon info
trap_BotGetWeaponInfo( bs->ws, bs->cur_ps.weapon, &wi );
//get the start point shooting from
VectorCopy( bs->origin, start );
start[2] += bs->cur_ps.viewheight;
AngleVectors( bs->viewangles, forward, right, NULL );
start[0] += forward[0] * wi.offset[0] + right[0] * wi.offset[1];
start[1] += forward[1] * wi.offset[0] + right[1] * wi.offset[1];
start[2] += forward[2] * wi.offset[0] + right[2] * wi.offset[1] + wi.offset[2];
//end point aiming at
VectorMA( start, BotWeaponRange( bs, bs->weaponnum ), forward, end );
//a little back to make sure not inside a very close enemy
VectorMA( start, -12, forward, start );
BotAI_Trace( &trace, start, mins, maxs, end, bs->entitynum, MASK_SHOT ); //----(SA) should this maybe check the weapon type and adjust the clipflag? it seems like this is probably fine as-is, but I thought I'd note it.
//if won't hit the enemy
if ( trace.ent != bs->enemy ) {
//if the entity is a client
if ( trace.ent > 0 && trace.ent < MAX_CLIENTS ) {
//if a teammate is hit
if ( BotSameTeam( bs, trace.ent ) ) {
return qfalse;
}
}
}
//if the projectile does a radial damage
if ( wi.proj.damagetype & DAMAGETYPE_RADIAL ) {
// if they are carrying the flag, and are close to the destination, fire away
if ( BotCarryingFlag( bs->enemy ) ) {
gentity_t *trav;
trav = BotFindNextStaticEntity( NULL, BOTSTATICENTITY_FLAGONLY );
if ( !trav ) {
trav = BotFindNextStaticEntity( NULL, BOTSTATICENTITY_FLAGONLY_MULTIPLE );
}
if ( trav ) {
VectorAdd( trav->r.absmin, trav->r.absmax, end );
VectorScale( end, 0.5, end );
if ( VectorDistanceSquared( g_entities[bs->enemy].r.currentOrigin, end ) < SQR( 800 ) ) {
goto passed;
}
}
}
//check if a teammate gets radial damage
fcnt = 0;
ecnt = 0;
for ( i = 0; i < level.maxclients; i++ ) {
if ( !g_entities[i].inuse ) {
continue;
}
if ( i == bs->client ) {
continue;
}
if ( g_entities[i].client->pers.connected != CON_CONNECTED ) {
continue;
}
if ( g_entities[i].client->sess.sessionTeam != TEAM_AXIS && g_entities[i].client->sess.sessionTeam != TEAM_ALLIES ) {
continue;
}
if ( !CanDamage( BotGetEntity( i ), end ) && !CanDamage( BotGetEntity( i ), bs->origin ) ) {
continue;
}
// if they are within range
if ( ( dist = VectorDistance( g_entities[i].r.currentOrigin, trace.endpos ) ) < wi.proj.radius ) {
//points = (wi.proj.damage * (dist / wi.proj.radius));
//if (points > 10) {
if ( BotSameTeam( bs, i ) ) {
fcnt++;
} else {
ecnt++;
}
//}
// check distance from source
} else if ( ( dist = 4.0 * VectorDistance( g_entities[i].r.currentOrigin, bs->origin ) ) < wi.proj.radius ) {
//dist /= 4.0;
//points = (wi.proj.damage * (dist / wi.proj.radius));
//if (points > 10) {
if ( BotSameTeam( bs, i ) ) {
fcnt++;
} else {
ecnt++;
}
//}
}
}
//
if ( fcnt >= ecnt ) {
return qfalse; // too dangerous
}
}
passed:
//if fire has to be release to activate weapon
if ( bs->cur_ps.weapon == WP_GRENADE_LAUNCHER || bs->cur_ps.weapon == WP_GRENADE_PINEAPPLE ) {
if ( bs->cur_ps.grenadeTimeLeft &&
( ( bs->inventory[INVENTORY_HEALTH] < 15 ) ||
( bs->cur_ps.grenadeTimeLeft < ( 700 + (int)( 4000.0 * ( 1.0 - aimskill ) ) ) ) ) ) {
// release grenade
} else {
// hold onto it
trap_EA_Attack( bs->client );
}
} else {
trap_EA_Attack( bs->client );
}
bs->flags ^= BFL_ATTACKED;
bs->last_fire = level.time;
//
// we have attacked them
g_entities[bs->enemy].botLastAttackedTime = level.time;
g_entities[bs->enemy].botLastAttackedEnt = bs->client;
// if this is k43/garand, then add a delay since recoil usually makes it tough
switch ( bs->weaponnum ) {
case WP_K43:
case WP_GARAND:
case WP_K43_SCOPE:
case WP_GARAND_SCOPE:
bs->teleport_time = trap_AAS_Time() + 0.5 + random() * 0.4;
break;
}
//
return qtrue;
}
/*
==================
BotMapScripts
==================
*/
void BotMapScripts( bot_state_t *bs ) {
return;
}
/*
==================
BotCheckButtons
==================
*/
/*
void CheckButtons(void)
{
int modelindex, i, numbuttons = 0;
char *classname, *model;
float lip, health, dist;
bsp_entity_t *ent;
vec3_t mins, maxs, size, origin, angles, movedir, goalorigin;
vec3_t start, end, bboxmins, bboxmaxs;
aas_trace_t trace;
for (ent = entities; ent; ent = ent->next)
{
classname = AAS_ValueForBSPEpairKey(ent, "classname");
if (!strcmp(classname, "func_button"))
{
//create a bot goal towards the button
model = AAS_ValueForBSPEpairKey(ent, "model");
modelindex = AAS_IndexFromModel(model);
//if the model is not loaded
if (!modelindex) modelindex = atoi(model+1);
VectorClear(angles);
AAS_BSPModelMinsMaxsOrigin(modelindex - 1, angles, mins, maxs, NULL);
//get the lip of the button
lip = AAS_FloatForBSPEpairKey(ent, "lip");
if (!lip) lip = 4;
//get the move direction from the angle
VectorSet(angles, 0, AAS_FloatForBSPEpairKey(ent, "angle"), 0);
AAS_SetMovedir(angles, movedir);
//button size
VectorSubtract(maxs, mins, size);
//button origin
VectorAdd(mins, maxs, origin);
VectorScale(origin, 0.5, origin);
//touch distance of the button
dist = fabs(movedir[0]) * size[0] + fabs(movedir[1]) * size[1] + fabs(movedir[2]) * size[2];// - lip;
dist *= 0.5;
//
health = AAS_FloatForBSPEpairKey(ent, "health");
//if the button is shootable
if (health)
{
//calculate the goal origin
VectorMA(origin, -dist, movedir, goalorigin);
AAS_DrawPermanentCross(goalorigin, 4, LINECOLOR_BLUE);
} //end if
else
{
//add bounding box size to the dist
AAS_PresenceTypeBoundingBox(PRESENCE_CROUCH, bboxmins, bboxmaxs);
for (i = 0; i < 3; i++)
{
if (movedir[i] < 0) dist += fabs(movedir[i]) * fabs(bboxmaxs[i]);
else dist += fabs(movedir[i]) * fabs(bboxmins[i]);
} //end for
//calculate the goal origin
VectorMA(origin, -dist, movedir, goalorigin);
//
VectorCopy(goalorigin, start);
start[2] += 24;
VectorSet(end, start[0], start[1], start[2] - 100);
trace = AAS_TraceClientBBox(start, end, PRESENCE_CROUCH, -1);
if (!trace.startsolid)
{
VectorCopy(trace.endpos, goalorigin);
} //end if
//
AAS_DrawPermanentCross(goalorigin, 4, LINECOLOR_YELLOW);
//
VectorSubtract(mins, origin, mins);
VectorSubtract(maxs, origin, maxs);
//
VectorAdd(mins, origin, start);
AAS_DrawPermanentCross(start, 4, LINECOLOR_BLUE);
VectorAdd(maxs, origin, start);
AAS_DrawPermanentCross(start, 4, LINECOLOR_BLUE);
} //end else
if (++numbuttons > 5) return;
} //end if
} //end for
} //end of the function CheckButtons
*/
/*
==================
BotEntityToActivate
==================
*/
//#define OBSTACLEDEBUG
int BotEntityToActivate( int entitynum ) {
/* int i, ent, cur_entities[10];
char model[MAX_INFO_STRING], tmpmodel[128];
char target[128], classname[128];
float health;
char targetname[10][128];
aas_entityinfo_t entinfo;*/
// RF, disabled this until I can figure out a way of getting this all working with the current code
return 0;
/* BotEntityInfo(entitynum, &entinfo);
Com_sprintf(model, sizeof( model ), "*%d", entinfo.modelindex);
for (ent = trap_AAS_NextBSPEntity(0); ent; ent = trap_AAS_NextBSPEntity(ent)) {
if (!trap_AAS_ValueForBSPEpairKey(ent, "model", tmpmodel, sizeof(tmpmodel))) continue;
if (!strcmp(model, tmpmodel)) break;
}
if (!ent) {
BotAI_Print(PRT_ERROR, "BotEntityToActivate: no entity found with model %s\n", model);
return 0;
}
trap_AAS_ValueForBSPEpairKey(ent, "classname", classname, sizeof(classname));
if (!classname) {
BotAI_Print(PRT_ERROR, "BotEntityToActivate: entity with model %s has no classname\n", model);
return 0;
}
//if it is a door
if (!strcmp(classname, "func_door")) {
if (trap_AAS_FloatForBSPEpairKey(ent, "health", &health)) {
//if health the door must be shot to open
if (health) return ent;
}
}
//if it is a explosive
if (!strcmp(classname, "func_explosive")) {
if (trap_AAS_IntForBSPEpairKey(ent, "spawnflags", &i)) {
//if DYNOMITE then only dynomite can blow it up
if (!(i&64)) return ent;
}
}
//get the targetname so we can find an entity with a matching target
if (!trap_AAS_ValueForBSPEpairKey(ent, "targetname", targetname[0], sizeof(targetname[0]))) {
#ifdef OBSTACLEDEBUG
BotAI_Print(PRT_ERROR, "BotEntityToActivate: entity with model \"%s\" has no targetname\n", model);
#endif //OBSTACLEDEBUG
return 0;
}
cur_entities[0] = trap_AAS_NextBSPEntity(0);
for (i = 0; i >= 0 && i < 10;) {
for (ent = cur_entities[i]; ent; ent = trap_AAS_NextBSPEntity(ent)) {
if (!trap_AAS_ValueForBSPEpairKey(ent, "target", target, sizeof(target))) continue;
if (!strcmp(targetname[i], target)) {
cur_entities[i] = trap_AAS_NextBSPEntity(ent);
break;
}
}
if (!ent) {
BotAI_Print(PRT_ERROR, "BotEntityToActivate: no entity with target \"%s\"\n", targetname[i]);
i--;
continue;
}
if (!trap_AAS_ValueForBSPEpairKey(ent, "classname", classname, sizeof(classname))) {
BotAI_Print(PRT_ERROR, "BotEntityToActivate: entity with target \"%s\" has no classname\n", targetname[i]);
continue;
}
if (!strcmp(classname, "func_button")) {
//BSP button model
return ent;
}
else if (!strcmp(classname, "trigger_multiple")) {
//invisible trigger multiple box
return ent;
}
else {
i--;
}
}
//BotAI_Print(PRT_ERROR, "BotEntityToActivate: unknown activator with classname \"%s\"\n", classname);
return 0;*/
}
/*
==================
BotSetMovedir
==================
*/
vec3_t VEC_UP = {0, -1, 0};
vec3_t MOVEDIR_UP = {0, 0, 1};
vec3_t VEC_DOWN = {0, -2, 0};
vec3_t MOVEDIR_DOWN = {0, 0, -1};
void BotSetMovedir( vec3_t angles, vec3_t movedir ) {
if ( VectorCompare( angles, VEC_UP ) ) {
VectorCopy( MOVEDIR_UP, movedir );
} else if ( VectorCompare( angles, VEC_DOWN ) ) {
VectorCopy( MOVEDIR_DOWN, movedir );
} else {
AngleVectors( angles, movedir, NULL, NULL );
}
}
void BotModelMinsMaxs( int modelindex, vec3_t mins, vec3_t maxs ) {
gentity_t *ent;
int i;
ent = BotGetEntity( 0 );
for ( i = 0; i < level.num_entities; i++, ent++ ) {
if ( !ent->inuse ) {
continue;
}
if ( ent->s.modelindex == modelindex ) {
VectorCopy( ent->r.mins, mins );
VectorCopy( ent->r.maxs, maxs );
return;
}
}
VectorClear( mins );
VectorClear( maxs );
}
/*
==================
BotAIBlocked
==================
*/
void BotAIBlocked( bot_state_t *bs, bot_moveresult_t *moveresult, int activate ) {
int movetype, ent, modelindex;
char classname[128], model[128];
#ifdef OBSTACLEDEBUG
char buf[128];
#endif
vec3_t up = {0, 0, 1};
vec3_t angles, mins, maxs, origin, movedir, hordir, start, end, sideward;
aas_entityinfo_t entinfo;
int oldMoverState;
#ifdef OBSTACLEDEBUG
char netname[MAX_NETNAME];
#endif
if ( bs->blockentTime > level.time ) {
// just strafe to avoid other players
if ( level.time % 3000 < 1500 ) {
trap_EA_Move( bs->client, vec3_origin, 0 );
trap_EA_MoveLeft( bs->client );
} else {
trap_EA_Move( bs->client, vec3_origin, 0 );
trap_EA_MoveRight( bs->client );
}
return;
}
if ( !moveresult->blocked ) {
/*
if (bs->blockent >= 0 && (bs->blockentTime >= level.time - 500)) {
moveresult->blocked = qtrue;
moveresult->blockentity = bs->blockent;
//
bs->blockent = -1;
bs->blockentTime = 0;
} else {*/
return;
//}
}
//
BotEntityInfo( moveresult->blockentity, &entinfo );
// RF, dont avoid our leader, just set this as our goal position
if ( ( entinfo.number < level.maxclients ) && ( bs->blockentTime < level.time ) ) {
// RF, in multiplayer, only ignore if this is our leader and we are currently following them
if ( ( ( bs->ainode == AINode_MP_DefendTarget ) && ( bs->target_goal.flags & GFL_LEADER ) && ( bs->leader == entinfo.number ) ) ) {
// make this our spot
VectorCopy( bs->origin, bs->target_goal.origin );
bs->target_goal.areanum = bs->areanum;
return;
}
//
if ( G_IsSinglePlayerGame() && bs->leader == entinfo.number ) {
VectorCopy( bs->origin, bs->target_goal.origin );
bs->target_goal.areanum = bs->areanum;
return;
}
//
if ( bs->target_goal.flags & GFL_LEADER ) {
if ( ( BotPointWithinMovementAutonomy( bs, &bs->target_goal, bs->origin ) )
&& ( VectorDistanceSquared( bs->origin, g_entities[bs->leader].r.currentOrigin ) < SQR( 512 ) ) ) {
VectorCopy( bs->origin, bs->target_goal.origin );
bs->target_goal.areanum = bs->areanum;
return;
}
}
bs->blockentTime = level.time + 400;
bs->blockent = entinfo.number;
return;
}
//
#ifdef OBSTACLEDEBUG
ClientName( bs->client, netname, sizeof( netname ) );
BotAI_Print( PRT_MESSAGE, "%s: I'm blocked by model %d\n", netname, entinfo.modelindex );
#endif /* OBSTACLEDEBUG */
ent = 0;
//if blocked by a bsp model and the bot wants to activate it if possible
if ( entinfo.modelindex > 0 && entinfo.modelindex <= max_bspmodelindex && activate ) {
//find the bsp entity which should be activated in order to remove
//the blocking entity
if ( ( ( Q_stricmp( g_entities[entinfo.number].classname, "func_door" ) == 0 ) || ( Q_stricmp( g_entities[entinfo.number].classname, "func_door_rotating" ) == 0 ) ) ) {
if ( ( g_entities[entinfo.number].moverState == MOVER_POS1 ) || ( g_entities[entinfo.number].moverState == MOVER_POS1ROTATE ) ) {
oldMoverState = g_entities[entinfo.number].moverState;
G_TryDoor( BotGetEntity( entinfo.number ), BotGetEntity( bs->client ), BotGetEntity( bs->client ) );
if ( g_entities[entinfo.number].moverState != oldMoverState ) {
// wait for it to finish opening
return;
}
} else if ( ( g_entities[entinfo.number].moverState == MOVER_POS2 ) || ( g_entities[entinfo.number].moverState == MOVER_POS2ROTATE ) ) {
// it is fully open, try and avoid it
ent = 0;
} else {
// else just wait for it to finish moving
return;
}
} else {
ent = BotEntityToActivate( entinfo.number );
}
if ( !ent ) {
strcpy( classname, "" );
#ifdef OBSTACLEDEBUG
BotAI_Print( PRT_MESSAGE, "%s: can't find activator for blocking entity\n", ClientName( bs->client, netname, sizeof( netname ) ) );
#endif //OBSTACLEDEBUG
} else {
trap_AAS_ValueForBSPEpairKey( ent, "classname", classname, sizeof( classname ) );
#ifdef OBSTACLEDEBUG
ClientName( bs->client, netname, sizeof( netname ) );
BotAI_Print( PRT_MESSAGE, "%s: I should activate %s\n", netname, classname );
#endif /* OBSTACLEDEBUG */
}
#ifdef OBSTACLEDEBUG
// ClientName(bs->client, netname, sizeof(netname));
// BotAI_Print(PRT_MESSAGE, "%s: I've got no brain cells for activating entities\n", netname);
#endif /* OBSTACLEDEBUG */
//if it is an explosive we should shoot it
if ( !strcmp( classname, "func_explosive" ) ) {
//get the door model
trap_AAS_ValueForBSPEpairKey( ent, "model", model, sizeof( model ) );
modelindex = atoi( model + 1 );
//if the model is not loaded
if ( !modelindex ) {
return;
}
VectorClear( angles );
BotModelMinsMaxs( modelindex, mins, maxs );
//get a goal to shoot at
VectorAdd( maxs, mins, origin );
VectorScale( origin, 0.5, origin );
VectorSubtract( origin, bs->eye, movedir );
//
vectoangles( movedir, moveresult->ideal_viewangles );
moveresult->flags |= MOVERESULT_MOVEMENTVIEW;
//select a weapon
BotChooseWeapon( bs );
trap_EA_SelectWeapon( bs->client, WP_KNIFE );
//shoot
trap_EA_Attack( bs->client );
//
return;
} //end if*/
}
if ( !strcmp( g_entities[entinfo.number].classname, "script_mover" ) ) {
if ( entinfo.number == bs->target_goal.entitynum ) {
// it's our goal, we need to get closer
return;
}
//
bs->blockentTime = level.time + 400;
bs->blockent = entinfo.number;
return;
}
//just some basic dynamic obstacle avoidance code
hordir[0] = moveresult->movedir[0];
hordir[1] = moveresult->movedir[1];
hordir[2] = 0;
//if no direction just take a random direction
if ( VectorNormalize( hordir ) < 0.1 ) {
VectorSet( angles, 0, 360 * random(), 0 );
AngleVectors( angles, hordir, NULL, NULL );
}
//
// if (moveresult->flags & MOVERESULT_ONTOPOFOBSTACLE) movetype = MOVE_JUMP;
// else
movetype = MOVE_WALK;
//if there's an obstacle at the bot's feet and head then
//the bot might be able to crouch through
VectorCopy( bs->origin, start );
start[2] += 18;
VectorMA( start, 5, hordir, end );
VectorSet( mins, -16, -16, -24 );
VectorSet( maxs, 16, 16, 4 );
//
// bsptrace = AAS_Trace(start, mins, maxs, end, bs->entitynum, MASK_PLAYERSOLID);
// if (bsptrace.fraction >= 1) movetype = MOVE_CROUCH;
//get the sideward vector
// START xkan, 8/22/2002
// If the script says we should be crouching, then move crouched.
if ( bs->script.flags & BSFL_CROUCH ) {
movetype = MOVE_CROUCH;
}
// END xkan, 8/22/2002
CrossProduct( hordir, up, sideward );
//
if ( bs->flags & BFL_AVOIDRIGHT ) {
VectorNegate( sideward, sideward );
}
//try to crouch straight forward?
if ( movetype != MOVE_CROUCH || !trap_BotMoveInDirection( bs->ms, hordir, 400, movetype ) ) {
//perform the movement
if ( !trap_BotMoveInDirection( bs->ms, sideward, 400, movetype ) ) {
//flip the avoid direction flag
bs->flags ^= BFL_AVOIDRIGHT;
//flip the direction
VectorNegate( sideward, sideward );
//move in the other direction
trap_BotMoveInDirection( bs->ms, sideward, 400, movetype );
}
}
//just reset goals and hope the bot will go into another direction
//still needed??
}
/*
==================
BotCheckEvents
==================
*/
void BotCheckEvents( bot_state_t *bs, entityState_t *state ) {
int event;
char buf[128];
// The array "entityeventTime" is hardcoded to 1024
assert( state->number < 1024 );
//
//this sucks, we're accessing the gentity_t directly but there's no other fast way
//to do it right now
if ( bs->entityeventTime[state->number] == g_entities[state->number].eventTime ) {
return;
}
bs->entityeventTime[state->number] = g_entities[state->number].eventTime;
//@TEST. See if this is ever called when the state->number isn't us!
if ( state->number != bs->client ) {
int foo;
foo = 7;
} // if (state->number != bs->client)...
//if it's an event only entity
if ( state->eType > ET_EVENTS ) {
event = ( state->eType - ET_EVENTS ) & ~EV_EVENT_BITS;
} else {
event = state->event & ~EV_EVENT_BITS;
}
//
switch ( event ) {
//client obituary event
case EV_OBITUARY:
{
int target, attacker, mod;
target = state->otherEntityNum;
attacker = state->otherEntityNum2;
mod = state->eventParm;
//
if ( target == bs->client ) {
//
//
bs->num_deaths++;
}
//else if this client was killed by the bot
else if ( attacker == bs->client ) {
//
bs->num_kills++;
} else if ( attacker == bs->enemy && target == attacker ) {
}
break;
}
case EV_GLOBAL_SOUND:
break;
case EV_PLAYER_TELEPORT_IN:
{
VectorCopy( state->origin, lastteleport_origin );
lastteleport_time = trap_AAS_Time();
break;
}
case EV_GENERAL_SOUND:
{
//if this sound is played on the bot
if ( state->number == bs->client ) {
if ( state->eventParm < 0 || state->eventParm > MAX_SOUNDS ) {
BotAI_Print( PRT_ERROR, "EV_GENERAL_SOUND: eventParm (%d) out of range\n", state->eventParm );
break;
}
//check out the sound
trap_GetConfigstring( CS_SOUNDS + state->eventParm, buf, sizeof( buf ) );
//if falling into a death pit
if ( !strcmp( buf, "*falling1.wav" ) ) {
/* //if the bot has a personal teleporter
if (bs->inventory[INVENTORY_TELEPORTER] > 0) {
//use the holdable item
trap_EA_Use(bs->client);
}
*/ }
}
break;
}
}
}
/*
==================
BotCheckSnapshot
==================
*/
void BotCheckSnapshot( bot_state_t *bs ) {
int ent;
entityState_t state;
//
ent = 0;
while ( ( ent = BotAI_GetSnapshotEntity( bs->client, ent, &state ) ) != -1 ) {
//check the entity state for events
BotCheckEvents( bs, &state );
}
//check the player state for events
BotAI_GetEntityState( bs->client, &state );
//copy the player state events to the entity state
//state.event = bs->cur_ps.externalEvent;
//state.eventParm = bs->cur_ps.externalEventParm;
//
BotCheckEvents( bs, &state );
}
/*
==================
BotCheckAir
==================
*/
void BotCheckAir( bot_state_t *bs ) {
/* if (bs->inventory[INVENTORY_ENVIRONMENTSUIT] <= 0) {
if (trap_AAS_PointContents(bs->eye) & (CONTENTS_WATER|CONTENTS_SLIME|CONTENTS_LAVA)) {
return;
}
}*/
}
// START Mad Doctor I changes, 8/15/2002
/*
==================
BotCheckAlert
==================
*/
#define AISTATE_RELAXING_STAGE1 4000 // bot still feels like in battle mode
#define AISTATE_RELAXING_STAGE2 60000 // bot is in high alert
#define AISTATE_RELAXING_FOOTSTEP 4000 // time it takes to relax from foot step hearing
void BotCheckAlert( bot_state_t *bs ) {
}
// END Mad Doctor I changes, 8/15/2002
//============================================================================
// Start TAT 9/23/2002
// Update recon state information for a bot
void BotUpdateReconInfo( bot_state_t *bs ) {
// Index for looping through entities
int i = 0;
// Detailed info about the entities
aas_entityinfo_t entinfo;
// Only relevant for Single Player
if ( !BotSinglePlayer() && !BotCoop() ) {
return;
}
// See if I've been hit.
if ( bs->lasthealth > bs->inventory[INVENTORY_HEALTH] ) {
bs->reconInfo = BOTRECON_UNDERFIRE;
return;
}
// want to update if we've been hit above regardless, since that's worse than just spotting an enemy
// but now if it's not all clear, we don't need to do any further updates
if ( bs->reconInfo != BOTRECON_ALLCLEAR ) {
return;
}
// not hit, no enemies spotted, so see if we can find anyone
// Check all entities to see if someone near me is shooting
for ( i = 0; i < level.maxclients; i++ )
{
// We don't count
if ( i == bs->client ) {
continue;
}
// Fill in the detailed bot info
BotEntityInfo( i, &entinfo );
// Skip invalid entities
if ( !entinfo.valid ) {
continue;
}
// if the enemy isn't dead and the enemy isn't the bot self
if ( EntityIsDead( &entinfo ) || entinfo.number == bs->entitynum ) {
continue;
}
// if the enemy is invisible
if ( EntityIsInvisible( &entinfo ) ) {
continue;
}
// if it's an ally, skip it
if ( BotSameTeam( bs, entinfo.number ) ) {
continue;
}
// If I can see the bot, then we're done
if ( BotEntityVisible( bs->entitynum, bs->eye, bs->viewangles, 120, entinfo.number, NULL ) ) {
bs->reconInfo = BOTRECON_ENEMYSPOTTED;
return;
}
}
}
// End TAT 9/23/2002
#define MAX_NODESWITCHES 10
int numnodeswitches;
char nodeswitch[MAX_NODESWITCHES + 1][144];
/*
==================
BotResetNodeSwitches()
==================
*/
void BotResetNodeSwitches( void ) {
numnodeswitches = 0;
}
/*
==================
BotDumpNodeSwitches()
==================
*/
void BotDumpNodeSwitches( bot_state_t *bs ) {
int i;
char netname[MAX_NETNAME];
ClientName( bs->client, netname, sizeof( netname ) );
#ifndef DONT_PRINT_REPEATED_AI_ERRORS
BotAI_Print( PRT_MESSAGE, "%s at %1.1f switched more than %d AI nodes\n", netname, trap_AAS_Time(), MAX_NODESWITCHES );
#endif // #ifndef DONT_PRINT_REPEATED_AI_ERRORS
for ( i = 0; i < numnodeswitches; i++ ) {
BotAI_Print( PRT_MESSAGE, nodeswitch[i] );
}
#ifndef DONT_PRINT_REPEATED_AI_ERRORS
BotAI_Print( PRT_FATAL, "" );
#endif // #ifndef DONT_PRINT_REPEATED_AI_ERRORS
}
//============================================================================
/*
==================
BotSetupDeathmatchAI
==================
*/
void BotDeathmatchAIFirstCalled
(
bot_state_t *bs
) {
char gender[144], name[144];
char userinfo[MAX_INFO_STRING];
bs->setupcount--;
if ( bs->setupcount > 0 ) {
return;
}
//get the gender characteristic
trap_Characteristic_String( bs->character, CHARACTERISTIC_GENDER, gender, sizeof( gender ) );
//set the bot gender
trap_GetUserinfo( bs->client, userinfo, sizeof( userinfo ) );
// Info_SetValueForKey(userinfo, "sex", gender);
trap_SetUserinfo( bs->client, userinfo );
/*
//set the team
if ( g_gametype.integer != GT_TOURNAMENT ) {
Com_sprintf(buf, sizeof(buf), "team %s", bs->settings.team);
trap_EA_Command(bs->client, buf);
}
*/
//set the chat gender
if ( gender[0] == 'm' ) {
trap_BotSetChatGender( bs->cs, CHAT_GENDERMALE );
} else if ( gender[0] == 'f' ) {
trap_BotSetChatGender( bs->cs, CHAT_GENDERFEMALE );
} else { trap_BotSetChatGender( bs->cs, CHAT_GENDERLESS );}
//set the chat name
ClientName( bs->client, name, sizeof( name ) );
trap_BotSetChatName( bs->cs, name );
//
bs->lastframe_health = bs->inventory[INVENTORY_HEALTH];
//
bs->setupcount = 0;
}
void BotPowThink
(
bot_state_t *bs
) {
if ( bs->clientCheckTime < trap_AAS_Time() ) {
int i;
gentity_t* ent;
vec3_t vec;
qboolean axisNearby = qfalse;
qboolean alliesNearby = qfalse;
for ( i = 0; i < level.numConnectedClients; i++ ) {
if ( bs->client == level.sortedClients[i] ) {
continue;
}
ent = BotGetEntity( level.sortedClients[i] );
if ( !ent ) {
continue;
}
if ( ent->client->sess.sessionTeam != TEAM_AXIS && ent->client->sess.sessionTeam != TEAM_ALLIES ) {
continue;
}
if ( ent->health <= 0 ) {
continue;
}
if ( fabs( ent->client->ps.origin[2] - bs->origin[2] ) > 64 ) {
// basically, not thru ceilings
continue;
}
VectorSubtract( ent->client->ps.origin, bs->origin, vec );
vec[2] = 0;
// ignore height diffs
if ( DotProduct( vec, vec ) > ( 128 * 128 ) ) {
continue;
}
if ( ent->client->sess.sessionTeam == TEAM_AXIS ) {
axisNearby = qtrue;
if ( alliesNearby ) {
break;
}
} else if ( ent->client->sess.sessionTeam == TEAM_ALLIES ) {
alliesNearby = qtrue;
if ( axisNearby ) {
break;
}
}
}
if ( axisNearby && !alliesNearby ) {
Bot_ScriptEvent( bs->client, "trigger", "axisnearby" );
} else if ( !axisNearby && alliesNearby ) {
Bot_ScriptEvent( bs->client, "trigger", "alliesnearby" );
}
bs->clientCheckTime = trap_AAS_Time() + 1.f;
}
}
/*
==================
BotDeathmatchAI
==================
*/
void BotDeathmatchAI( bot_state_t *bs, float thinktime ) {
int i = 0;
//if the bot has just been setup
if ( bs->setupcount > 0 ) {
BotDeathmatchAIFirstCalled( bs );
}
//while in warmup, keep checking for best class
if ( ( level.warmupTime > level.time ) && ( bs->lastClassCheck < level.time - 1000 ) ) { // check for a better class
bs->mpClass = BotSuggestClass( bs, bs->mpTeam );
level.clients[bs->client].sess.latchPlayerType = bs->mpClass;
bs->lastClassCheck = level.time;
}
// if we want to dismount
if ( bs->flags & BFL_DISMOUNT_MG42 ) {
// clear scripted mg42 command
bs->script.flags &= ~BSFL_MOUNT_MG42;
bs->script.mg42entnum = -1;
//
if ( !( g_entities[bs->client].s.eFlags & EF_MG42_ACTIVE ) ) {
bs->flags &= ~BFL_DISMOUNT_MG42;
} else {
if ( rand() % 2 ) {
trap_EA_Activate( bs->client );
}
}
}
bs->leader_tagent = -1;
// share last attacked
BotShareLastAttacked( bs );
//no ideal view set
bs->flags &= ~BFL_IDEALVIEWSET;
bs->flags &= ~BFL_BATTLE_MODE; // set it if we are
bs->flags &= ~BFL_SNIPING;
//set the teleport time
BotSetTeleportTime( bs );
//update some inventory values
BotUpdateInventory( bs );
//check out the snapshot
BotCheckSnapshot( bs );
//check for air
BotCheckAir( bs );
//check for required engineer's
if ( BotIsDead( bs ) || bs->sess.playerType != PC_ENGINEER ) {
if ( BotCheckNeedEngineer( bs, bs->sess.sessionTeam ) ) {
// should change automatically
bs->mpClass = PC_ENGINEER;
level.clients[bs->client].sess.latchPlayerType = bs->mpClass;
if ( !BotIsDead( bs ) ) {
Cmd_Kill_f( &g_entities[bs->client] );
}
}
}
// Check to see if I need to change my level of alert
// BotCheckAlert(bs);
// xkan, 1/10/2003 - set aiState of playerState (used by animation system)
g_entities[bs->client].client->ps.aiState = bs->alertState;
//choose the best weapon to fight with
BotChooseWeapon( bs );
if ( BotIsPOW( bs ) ) {
BotPowThink( bs );
}
// if the bot has no ai node
if ( !bs->ainode ) {
BotDefaultNode( bs );
if ( !bs->ainode ) {
return; // nothing to do..
}
}
// reset the node switches from the previous frame
BotResetNodeSwitches();
// If we've been scripted to SLEEP, we want to not execute ainodes!
// But, we'd still want to do our scripted, etc...
/* if (!bs->scriptedSleep) {
//execute AI nodes
for (i = 0; i < MAX_NODESWITCHES && bs->ainode; i++) {
if(bot_profile.integer == 4 || bot_profile.integer == 5) {
qboolean ret;
int t = trap_Milliseconds();
const char* txt = bs->ainodeText;
ret = bs->ainode(bs);
t = trap_Milliseconds() - t;
if(bot_profile.integer == 5) {
if(t > 5) {
G_Printf( "AINode %s took %i ms\n", txt, t );
}
} else {
G_Printf( "AINode %s took %i ms\n", txt, t );
}
if( ret ) {
break;
}
} else {
if( bs->ainode(bs)) {
break;
}
}
}
}*/
//if the bot removed itself :)
if ( !bs->inuse ) {
return;
}
// if the node changed, log entry
if ( i > 0 ) {
// Bot_ScriptLog_Entry( bs, qfalse, ">> NODE CHANGE <<", va("%s\r\n", bs->ainodeText) );
}
// bot scripting
Bot_ScriptRun( bs, qfalse );
//if the bot executed too many AI nodes
if ( i >= MAX_NODESWITCHES ) {
#ifdef _DEBUG
char name[144];
trap_BotDumpGoalStack( bs->gs );
trap_BotDumpAvoidGoals( bs->gs );
BotDumpNodeSwitches( bs );
ClientName( bs->client, name, sizeof( name ) );
//#ifndef DONT_PRINT_REPEATED_AI_ERRORS
BotAI_Print( PRT_ERROR, "%s at %1.1f switched more than %d AI nodes\n", name, trap_AAS_Time(), MAX_NODESWITCHES );
//#endif // #ifndef DONT_PRINT_REPEATED_AI_ERRORS
#endif
}
//
bs->lastframe_health = bs->inventory[INVENTORY_HEALTH];
bs->lasthealth = g_entities[bs->client].health;
}
/*
==================
BotSetupDeathmatchAI
==================
*/
void BotSetupDeathmatchAI( void ) {
int ent, modelnum;
char model[128];
gametype = trap_Cvar_VariableIntegerValue( "g_gametype" );
trap_Cvar_Register( &bot_rocketjump, "bot_rocketjump", "0", 0 );
trap_Cvar_Register( &bot_grapple, "bot_grapple", "0", 0 );
trap_Cvar_Register( &bot_fastchat, "bot_fastchat", "0", 0 );
trap_Cvar_Register( &bot_nochat, "bot_nochat", "1", CVAR_ROM );
trap_Cvar_Register( &bot_testrchat, "bot_testrchat", "0", 0 );
//
max_bspmodelindex = 0;
for ( ent = trap_AAS_NextBSPEntity( 0 ); ent; ent = trap_AAS_NextBSPEntity( ent ) ) {
if ( !trap_AAS_ValueForBSPEpairKey( ent, "model", model, sizeof( model ) ) ) {
continue;
}
if ( model[0] == '*' ) {
modelnum = atoi( model + 1 );
if ( modelnum > max_bspmodelindex ) {
max_bspmodelindex = modelnum;
}
}
}
//initialize the waypoint heap
BotInitWaypoints();
}
/*
==================
BotShutdownDeathmatchAI
==================
*/
void BotShutdownDeathmatchAI( void ) {
}
/*
===============
BotSetBlockEnt
===============
*/
void BotSetBlockEnt( int client, int blocker ) {
botstates[client].blockent = blocker;
botstates[client].blockentTime = level.time;
}
/*
==================
BotMoveToIntermission
==================
*/
void BotMoveToIntermission( int client ) {
char cs[MAX_STRING_CHARS]; // DHM - Nerve
char *buf; // DHM - Nerve
int winner; // DHM - Nerve
bot_state_t *bs;
if ( !g_entities[client].r.svFlags & SVF_BOT ) {
return;
}
bs = &botstates[client];
winner = -1;
if ( g_gametype.integer >= GT_WOLF ) {
if ( rand() % 2 == 0 ) {
trap_GetConfigstring( CS_MULTI_MAPWINNER, cs, sizeof( cs ) );
buf = Info_ValueForKey( cs, "winner" );
winner = atoi( buf );
// if we won, talk it up
if ( winner == ( bs->sess.sessionTeam - 1 ) ) {
//if (rand()%3) {
BotSendVoiceChat( bs, "Cheer", SAY_ALL, 1000 + rand() % 5000, BOT_SHOWTEXT, qfalse );
//} else {
// BotSendVoiceChat( bs, "GoodGame", SAY_ALL, 1000 + rand()%3000, BOT_SHOWTEXT, qfalse );
//}
} else if ( bs->sess.sessionTeam ) {
//if (rand()%2) {
BotSendVoiceChat( bs, "Negative", SAY_ALL, 1000 + rand() % 5000, BOT_SHOWTEXT, qfalse );
//} else {
// BotSendVoiceChat( bs, "GoodGame", SAY_ALL, 1000 + rand()%3000, BOT_SHOWTEXT, qfalse );
//}
}
}
}
}
/*
================
BotRecordTeamChange
================
*/
void BotRecordTeamChange( int client ) {
int team;
int i;
bot_state_t *bs;
team = g_entities[client].client->sess.sessionTeam;
// greet the new player
for ( i = 0; i < level.maxclients; i++ ) {
bs = &botstates[i];
if ( !bs->inuse ) {
continue;
}
if ( bs->sess.sessionTeam != team ) {
continue;
}
// Don't use excess voices in Single Player
if ( !BotSinglePlayer() && !BotCoop() ) {
BotVoiceChatAfterIdleTime( bs->client, "Hi", SAY_TEAM, 1000 + rand() % 6000, BOT_SHOWTEXT, 7000, qfalse );
}
}
}
/*
==================
BotRecordKill
==================
*/
void BotRecordKill( int client, int enemy ) {
bot_state_t *bs = &botstates[client];
if ( client == enemy ) {
return;
}
// if this is a team kill, ignore
if ( BotSameTeam( bs, enemy ) ) {
// friendly fire!
// Don't use excess voices in Single Player
if ( !BotSinglePlayer() && !BotCoop() ) {
BotVoiceChatAfterIdleTime( bs->client, "Sorry", SAY_TEAM, 1000 + rand() % 4000, qfalse, 3000 + rand() % 2000, qfalse );
}
return;
}
// if last kill was too long ago
if ( bs->last_kill_time < level.time - 10000 ) {
bs->shorterm_kill_count = 0;
bs->last_kill_time = level.time;
return;
}
// another kill
bs->shorterm_kill_count++;
bs->last_kill_time = level.time;
// Don't use excess voices in Single Player
if ( !G_IsSinglePlayerGame() ) {
// if we have killed enough in the short term, talk it up
if ( bs->shorterm_kill_count > 2 ) {
BotSendVoiceChat( bs, "Yeah", SAY_ALL, 1000 + rand() % 1000, qfalse, qfalse );
} else if ( bs->shorterm_kill_count > 1 ) {
BotVoiceChatAfterIdleTime( bs->client, "EnemyWeak", SAY_TEAM, 1000 + rand() % 1000, qfalse, 3000, qfalse );
}
}
}
/*
===================
BotRecordPain
===================
*/
void BotRecordPain( int client, int enemy, int mod ) {
bot_state_t *bs = &botstates[client];
gentity_t *targ;
if ( client == enemy ) {
return;
}
// Mad Doc xkan, 10/30/2002 - if we are already dead, just return
if ( g_entities[bs->client].health <= 0 ) {
return;
}
// if this is a team kill, ignore
if ( enemy < level.maxclients && BotSameTeam( bs, enemy ) ) {
// friendly fire!
// Don't use excess voices in Single Player
BotVoiceChatAfterIdleTime( bs->client, "HoldYourFire", SAY_TEAM, 1000 + rand() % 1000, qfalse, 3000 + rand() % 2000, qfalse );
return;
}
if ( enemy >= level.maxclients ) {
return;
}
bs->last_pain = level.time;
bs->last_pain_client = enemy;
g_entities[bs->client].botLastAttackedTime = level.time;
// if we are defending/near an objective
if ( bs->defendgoal.entitynum >= level.maxclients ) {
targ = BotGetEntity( bs->defendgoal.entitynum );
if ( !targ->inuse ) {
return;
}
if ( Q_stricmp( targ->classname, "team_CTF_redflag" ) &&
Q_stricmp( targ->classname, "team_CTF_blueflag" ) &&
Q_stricmp( targ->classname, "trigger_flagonly" ) &&
Q_stricmp( targ->classname, "team_WOLF_checkpoint" ) ) {
return;
}
if ( VectorDistanceSquared( bs->origin, bs->defendgoal.origin ) > SQR( 1024 ) ) {
return;
}
//if (!trap_InPVS( bs->origin, bs->defendgoal.origin )) return;
//
// we are near the goal
// Don't use excess voices in Single Player
if ( !BotSinglePlayer() && !BotCoop() ) {
BotVoiceChatAfterIdleTime( bs->client, "TakingFire", SAY_TEAM, 1000 + rand() % 1000, qfalse, 5000 + rand() % 4000, qfalse );
}
}
}
/*
===================
BotRecordDeath
===================
*/
void BotRecordDeath( int client, int enemy ) {
bot_state_t *bs = &botstates[client];
gentity_t *targ;
if ( client == enemy ) {
return;
}
// if this is a team kill, ignore
if ( enemy < level.maxclients && BotSameTeam( bs, enemy ) ) {
return;
}
// if we are defending/near an objective
if ( bs->defendgoal.entitynum > level.maxclients ) {
targ = BotGetEntity( bs->defendgoal.entitynum );
if ( !targ->inuse ) {
return;
}
if ( Q_stricmp( targ->classname, "team_CTF_redflag" ) &&
Q_stricmp( targ->classname, "team_CTF_blueflag" ) &&
Q_stricmp( targ->classname, "trigger_flagonly" ) &&
Q_stricmp( targ->classname, "team_WOLF_checkpoint" ) ) {
return;
}
if ( VectorDistanceSquared( bs->origin, bs->defendgoal.origin ) > SQR( 1024 ) ) {
return;
}
//
// we are near the goal
BotVoiceChatAfterIdleTime( bs->client, "Incoming", SAY_TEAM, 1000 + rand() % 1000, qfalse, 6000, qfalse );
}
}
/*
===================
BotGetAimAccuracySkill
Get the bot's aim accuracy and skill accounting for distance from leader,
weapon, pose (standing vs. crouching vs. prone)
===================
*/
void BotGetAimAccuracySkill( bot_state_t *bs, float *outAimAccuracy, float *outAimSkill ) {
float aim_accuracy, aim_skill;
//weaponinfo_t wi;
gclient_t *client = g_entities[bs->client].client;
gclient_t *clientEnemy;
//
// What sort of penalty do we get for being away from a leader
float noLeaderPenalty = BotNoLeaderPenalty( bs );
aim_skill = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_SKILL, 0, 1 );
//aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY, 0, 1);
//
//get the weapon information
//trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi);
//get the weapon specific aim accuracy and or aim skill
if ( bs->weaponnum == WP_GRENADE_LAUNCHER || bs->weaponnum == WP_GRENADE_PINEAPPLE ) {
//aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_GRENADELAUNCHER, 0, 1);
aim_skill = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_SKILL_GRENADELAUNCHER, 0, 1 );
}
if ( bs->weaponnum == WP_PANZERFAUST ) {
//aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_ROCKETLAUNCHER, 0, 1);
aim_skill = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_SKILL_ROCKETLAUNCHER, 0, 1 );
}
if ( bs->weaponnum == WP_FLAMETHROWER ) {
//aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_FLAMETHROWER, 0, 1);
}
aim_accuracy = bs->attribs[BOT_AIM_ACCURACY];
if ( aim_skill > 1 ) {
aim_skill = 1;
}
if ( aim_accuracy > 1 ) {
aim_accuracy = 1;
}
// The current accuracy is original minus original times penalty
aim_accuracy = aim_accuracy
* ( 1.0f - ( NO_LEADER_MAX_AIM_PENALTY * noLeaderPenalty ) );
if ( client->ps.eFlags & EF_PRONE ) {
// if prone, increase aim accuracy/skill
aim_accuracy += ( 1 - aim_accuracy ) * AIM_ACCURACY_BONUS_PRONE;
aim_skill += ( 1 - aim_skill ) * AIM_SKILL_BONUS_PRONE;
} else if ( client->ps.eFlags & EF_CROUCHING ) {
// if crouching, increase aim accuracy
aim_accuracy += ( 1 - aim_accuracy ) * AIM_ACCURACY_BONUS_CROUCH;
}
if ( bs->enemy >= 0 && ( clientEnemy = g_entities[bs->enemy].client ) != NULL ) {
// if our enemy is prone/crouching, we are less accurate
if ( clientEnemy->ps.eFlags & EF_PRONE ) {
aim_accuracy *= ( 1 - AIM_ACCURACY_ENEMY_PENALTY_PRONE );
} else if ( clientEnemy->ps.eFlags & EF_CROUCHING ) {
aim_accuracy *= ( 1 - AIM_ACCURACY_ENEMY_PENALTY_CROUCH );
}
}
// return the result through output params
if ( outAimAccuracy ) {
*outAimAccuracy = aim_accuracy;
}
if ( outAimSkill ) {
*outAimSkill = aim_skill;
}
}
/*
================
BotBestSniperSpot
returns -1 if no spot found
================
*/
int BotBestSniperSpot( bot_state_t *bs ) {
gentity_t *trav, *bestSpot;
int areanum, t, bestTime;
trav = NULL;
bestSpot = NULL;
bestTime = 99999;
while ( ( trav = BotFindNextStaticEntity( trav, BOTSTATICENTITY_BOT_SNIPERSPOT ) ) ) {
// is it disabled?
if ( trav->aiInactive & ( 1 << bs->sess.sessionTeam ) ) {
continue;
}
// if this is not for our team
if ( trav->aiTeam && trav->aiTeam != bs->sess.sessionTeam ) {
continue;
}
// get the travel time to the goal
areanum = BotPointAreaNum( trav->s.number, trav->s.origin );
if ( !areanum ) {
continue;
}
t = trap_AAS_AreaTravelTimeToGoalArea( bs->areanum, bs->origin, areanum, bs->tfl );
if ( !t ) {
continue;
}
if ( bs->target_goal.entitynum != trav - g_entities ) {
// if we have recently used this spot, avoid it
if ( trav->missionLevel && ( trav->missionLevel - level.time < 60000 ) ) {
continue;
}
}
if ( t < bestTime ) {
bestTime = t;
bestSpot = trav;
}
}
if ( bestSpot ) {
// avoid this spot for a while
bestSpot->missionLevel = level.time;
return bestSpot->s.number;
}
return -1;
}
/*
================
BotBestLandmineSpotingSpot
returns -1 if no spot found
================
*/
int BotBestLandmineSpotingSpot( bot_state_t *bs ) {
gentity_t *trav, *bestSpot;
int areanum, t, bestTime;
trav = NULL;
bestSpot = NULL;
bestTime = 99999;
while ( ( trav = BotFindNextStaticEntity( trav, BOTSTATICENTITY_BOT_LANDMINESPOTINGSPOT ) ) ) {
// is it disabled?
if ( trav->aiInactive & ( 1 << bs->sess.sessionTeam ) ) {
continue;
}
// if this is not for our team
if ( trav->aiTeam && trav->aiTeam != bs->sess.sessionTeam ) {
continue;
}
// get the travel time to the goal
areanum = BotPointAreaNum( trav->s.number, trav->s.origin );
if ( !areanum ) {
continue;
}
t = trap_AAS_AreaTravelTimeToGoalArea( bs->areanum, bs->origin, areanum, bs->tfl );
if ( !t ) {
continue;
}
if ( bs->target_goal.entitynum != trav - g_entities ) {
// if we have recently used this spot, avoid it
if ( trav->missionLevel && ( trav->missionLevel - level.time < 20000 ) ) {
continue;
}
}
if ( t < bestTime ) {
bestTime = t;
bestSpot = trav;
}
}
if ( bestSpot ) {
// avoid this spot for a while
bestSpot->missionLevel = level.time;
return bestSpot->s.number;
}
return -1;
}
/*
================
BotBestMG42Spot
returns -1 if no spot found
================
*/
int BotBestMG42Spot( bot_state_t *bs, qboolean force ) {
gentity_t *trav, *bestSpot, *mg42;
int areanum, t, bestTime;
//
trav = NULL;
mg42 = NULL;
bestSpot = NULL;
bestTime = 99999;
while ( ( mg42 = BotFindNextStaticEntity( mg42, BOTSTATICENTITY_MG42 ) ) ) {
//while (trav = BotFindEntity( trav, FOFS(classname), "bot_mg42_spot" )) {
if ( !mg42->melee ) {
continue; // it doesnt have a "mg42 spot"
}
trav = mg42->melee;
// is it disabled?
if ( mg42->aiInactive & ( 1 << bs->sess.sessionTeam ) ) {
continue;
}
// if this is not for our team
//if (trav->aiTeam && trav->aiTeam != bs->sess.sessionTeam) continue;
if ( !trav->melee->takedamage ) {
continue;
}
if ( trav->melee->entstate != STATE_DEFAULT ) {
continue;
}
if ( trav->melee->active ) {
// if the person using it is from the other team, then we should still head for it, to try and take it from them
if ( mg42->r.ownerNum < level.maxclients && ( g_entities[mg42->r.ownerNum].client->sess.sessionTeam != bs->sess.sessionTeam ) ) {
// errr..
} else {
continue;
}
}
// if we have recently used this spot, ignore it
// NOTE: if it's active, and we got to here, then it must be an enemy, so "ignore" the ignoreTime
if ( !force && !mg42->active && trav->botIgnoreTime && ( trav->botIgnoreTime > level.time - 1000 ) ) {
continue;
}
// get the travel time to the goal
areanum = BotPointAreaNum( trav->s.number, trav->s.origin );
if ( !areanum ) {
continue;
}
t = trap_AAS_AreaTravelTimeToGoalArea( bs->areanum, bs->origin, areanum, bs->tfl );
if ( !t ) {
continue;
}
if ( t < bestTime ) {
bestTime = t;
bestSpot = trav;
}
}
//
if ( bestSpot ) {
// avoid this spot for a while
bestSpot->botIgnoreTime = level.time;
return bestSpot->s.number;
}
//
return -1;
}
/*
================
BotGetNumVisibleSniperSpots
================
*/
int BotGetNumVisibleSniperSpots( bot_state_t *bs ) {
gentity_t *trav;
int cnt;
vec3_t dest;
trace_t tr;
//
// count the spots first
trav = NULL;
cnt = 0;
while ( ( trav = BotFindNextStaticEntity( trav, BOTSTATICENTITY_BOT_SNIPERSPOT ) ) ) {
// if this is not for the other team
if ( trav->aiTeam && trav->aiTeam == bs->sess.sessionTeam ) {
continue;
}
// is this spot visible?
VectorCopy( trav->s.origin, dest );
trap_Trace( &tr, bs->eye, NULL, NULL, dest, bs->client, MASK_SHOT );
if ( tr.fraction < 0.9 ) {
continue;
}
cnt++;
}
//
// return a random spot
return cnt;
}
/*
================
BotGetRandomVisibleSniperSpot
================
*/
int BotGetRandomVisibleSniperSpot( bot_state_t *bs ) {
#define MAX_SNIPER_SPOTS 40
gentity_t *trav;
int cnt, spots[MAX_SNIPER_SPOTS];
vec3_t dest;
trace_t tr;
//
// count the spots first
trav = NULL;
cnt = 0;
while ( ( trav = BotFindEntity( trav, FOFS( classname ), "bot_sniper_spot" ) ) ) {
// if this is not for the other team
if ( trav->aiTeam && trav->aiTeam == bs->sess.sessionTeam ) {
continue;
}
// is this spot visible?
VectorCopy( trav->s.origin, dest );
trap_Trace( &tr, bs->eye, NULL, NULL, dest, bs->client, MASK_SHOT );
if ( tr.fraction < 0.9 ) {
continue;
}
spots[cnt++] = trav->s.number;
}
//
if ( !cnt ) {
return -1;
}
// return a random spot
return spots[rand() % cnt];
}
/*
==================
BotClearGoal
==================
*/
void BotClearGoal( bot_goal_t *goal ) {
memset( goal, 0, sizeof( *goal ) );
goal->entitynum = -1;
}
/*
==================
BotDropFlag
==================
*/
void BotDropFlag( bot_state_t *bs ) {
gentity_t *ent;
gitem_t *item = NULL;
gentity_t *flag = NULL;
vec3_t launchvel;
ent = BotGetEntity( bs->client );
if ( ent->client->ps.powerups[PW_REDFLAG] ) {
item = BG_FindItem( "Red Flag" );
if ( !item ) {
item = BG_FindItem( "Objective" );
}
ent->client->ps.powerups[PW_REDFLAG] = 0;
}
if ( ent->client->ps.powerups[PW_BLUEFLAG] ) {
item = BG_FindItem( "Blue Flag" );
if ( !item ) {
item = BG_FindItem( "Objective" );
}
ent->client->ps.powerups[PW_BLUEFLAG] = 0;
}
if ( item ) {
launchvel[0] = crandom() * 20;
launchvel[1] = crandom() * 20;
launchvel[2] = 10 + random() * 10;
flag = LaunchItem( item,ent->r.currentOrigin,launchvel,ent->s.number );
flag->s.modelindex2 = ent->s.otherEntityNum2; // JPW NERVE FIXME set player->otherentitynum2 with old modelindex2 from flag and restore here
flag->message = ent->message; // DHM - Nerve :: also restore item name
flag->botIgnoreTime = level.time + 2500;
flag->r.ownerNum = bs->client;
// Clear out player's temp copies
ent->s.otherEntityNum2 = 0;
ent->message = NULL;
}
}
/*
===================
BotCalculateMg42Spots
===================
*/
extern vec3_t playerMins, playerMaxs;
void BotCalculateMg42Spots( void ) {
gentity_t *trav, *sptrav, *newent;
vec3_t pos, epos, dir, v;
trace_t tr;
vec3_t mins, maxs;
int blueCount, redCount;
int i;
float dist;
// Start TAT 9/24/2002
// This isn't working for constructible MG42s, because our traces are hitting the constructible markers
// so we need to temporarily unlink all the linked constructible markers, and then we'll link
// them again after we're done, so they don't show up in the traces
int constructMarkers[MAX_GENTITIES];
int numMarkers = 0;
trav = NULL;
// loop through all the constructible markers
while ( ( trav = G_Find( trav, FOFS( classname ), "misc_constructiblemarker" ) ) )
{
// if it's linked
if ( trav->r.linked ) {
// add it to our list
constructMarkers[numMarkers++] = trav->s.number;
// and unlink it
trap_UnlinkEntity( trav );
}
}
// End TAT 9/24/2002
VectorCopy( playerMins, mins );
VectorCopy( playerMaxs, maxs );
trav = NULL;
while ( ( trav = BotFindNextStaticEntity( trav, BOTSTATICENTITY_MG42 ) ) ) {
//
// RF, if this entity already has an mg42 spot, then skip it
if ( trav->melee ) {
continue;
}
//
blueCount = 0;
redCount = 0;
// go backwards from the mg42 mount position
AngleVectors( trav->s.angles, dir, NULL, NULL );
if ( trav->r.maxs[0] > maxs[0] ) {
dist = trav->r.maxs[0];
} else {
dist = maxs[0];
}
while ( ( dist += 2 ) < 80 ) {
maxs[2] = 4;
mins[2] = 0;
//
VectorMA( trav->r.currentOrigin, -dist, dir, pos );
trap_Trace( &tr, pos, mins, maxs, pos, ENTITYNUM_NONE, MASK_PLAYERSOLID );
if ( tr.startsolid || tr.allsolid ) {
continue;
}
VectorCopy( tr.endpos, pos );
VectorCopy( tr.endpos, epos );
epos[2] -= 48;
trap_Trace( &tr, pos, mins, maxs, epos, ENTITYNUM_NONE, MASK_PLAYERSOLID );
if ( tr.startsolid || tr.allsolid ) {
continue;
}
VectorCopy( tr.endpos, pos );
// move it up to allow for bounding mins
pos[2] += -playerMins[2];
mins[2] = playerMins[2];
// one last check
trap_Trace( &tr, pos, mins, maxs, pos, ENTITYNUM_NONE, MASK_PLAYERSOLID );
if ( tr.startsolid || tr.allsolid ) {
continue;
}
break;
}
if ( tr.startsolid || tr.allsolid ) {
// didnt find a good spot
continue;
}
// go back to normal head height
//maxs[2] = playerMaxs[2];
// trace down to the ground
VectorCopy( pos, epos );
epos[2] -= 128;
//trap_Trace( &tr, pos, mins, maxs, epos, trav->s.number, MASK_PLAYERSOLID );
trap_Trace( &tr, pos, mins, maxs, epos, ENTITYNUM_NONE, MASK_PLAYERSOLID );
if ( tr.startsolid || tr.allsolid ) {
continue;
}
VectorCopy( tr.endpos, pos );
// go back to normal head height, check position
maxs[2] = playerMaxs[2];
trap_Trace( &tr, pos, mins, maxs, pos, ENTITYNUM_NONE, MASK_PLAYERSOLID );
if ( tr.startsolid || tr.allsolid ) {
continue;
}
// if this position is close enough to the mg42, then assume it's valid
if ( VectorDistanceSquared( trav->r.currentOrigin, pos ) > SQR( 48 ) ) {
continue;
}
//
// spawn an mg42 spot
newent = G_Spawn();
newent->classname = "bot_mg42_spot";
// try and place it into the bot game entities
// newent = BotCheckBotGameEntity( newent );
newent->melee = trav;
trav->melee = newent;
VectorCopy( pos, newent->s.origin );
VectorCopy( pos, newent->r.currentOrigin );
VectorAdd( pos, playerMaxs, newent->r.absmax );
VectorAdd( pos, playerMins, newent->r.absmin );
VectorCopy( trav->r.currentAngles, newent->r.currentAngles );
// is this pointing to an axis or allied spawn point
// BLUE
sptrav = NULL;
while ( ( sptrav = G_Find( sptrav, FOFS( classname ), "team_CTF_bluespawn" ) ) ) {
if ( !( sptrav->spawnflags & 2 ) ) {
continue; // ignore NON-STARTACTIVE spawns
}
VectorSubtract( sptrav->s.origin, trav->r.currentOrigin, v );
VectorNormalize( v );
if ( DotProduct( v, dir ) > 0 ) {
blueCount++;
}
}
// RED
sptrav = NULL;
while ( ( sptrav = G_Find( sptrav, FOFS( classname ), "team_CTF_redspawn" ) ) ) {
if ( !( sptrav->spawnflags & 2 ) ) {
continue; // ignore NON-STARTACTIVE spawns
}
VectorSubtract( sptrav->s.origin, trav->r.currentOrigin, v );
VectorNormalize( v );
if ( DotProduct( v, dir ) > 0 ) {
redCount++;
}
}
// if the count is hugely in favor of one side, then it must belong to the other side
if ( blueCount - redCount > 4 ) {
newent->aiTeam = TEAM_AXIS; // mostly facing blue spots
} else if ( redCount - blueCount > 4 ) {
newent->aiTeam = TEAM_ALLIES; // mostly facing red spots
} else { newent->aiTeam = 0;}
}
// Start TAT 9/24/2002
// Relink those constructible markers
for ( i = 0; i < numMarkers; i++ )
{
trap_LinkEntity( &g_entities[constructMarkers[i]] );
}
// End TAT 9/24/2002
}
/*
=================
BotMovementAutonomyForString
=================
*/
int BotMovementAutonomyForString( char *string ) {
if ( !Q_stricmp( string, "high" ) ) {
return BMA_HIGH;
} else if ( !Q_stricmp( string, "medium" ) ) {
return BMA_MEDIUM;
} else if ( !Q_stricmp( string, "low" ) ) {
return BMA_LOW;
} else {
return -1;
}
}
/*
=================
BotStringForMovementAutonomy
=================
*/
char *BotStringForMovementAutonomy( int value ) {
switch ( value ) {
case BMA_LOW: return "LOW";
case BMA_MEDIUM: return "MEDIUM";
case BMA_HIGH: return "HIGH";
}
//
return "(unknown)";
}
/*
=================
BotWeaponAutonomyForString
=================
*/
int BotWeaponAutonomyForString( char *string ) {
if ( !Q_stricmp( string, "high" ) ) {
return BWA_HIGH;
} else if ( !Q_stricmp( string, "medium" ) ) {
return BWA_MEDIUM;
} else if ( !Q_stricmp( string, "low" ) ) {
return BWA_LOW;
} else {
return -1;
}
}
/*
=================
BotStringForWeaponAutonomy
=================
*/
char *BotStringForWeaponAutonomy( int value ) {
switch ( value ) {
case BMA_LOW: return "LOW";
case BMA_MEDIUM: return "MEDIUM";
case BMA_HIGH: return "HIGH";
}
//
return "(unknown)";
}
/*
=================
BotScriptAutonomyForString
=================
*/
int BotScriptAutonomyForString( char *string ) {
if ( !Q_stricmp( string, "quitscript" ) ) {
return BSA_QUITSCRIPT;
} else if ( !Q_stricmp( string, "nochase" ) ) {
return BSA_NOCHASE;
} else if ( !Q_stricmp( string, "maintainscript" ) ) {
return BSA_MAINTAINSCRIPT;
} else if ( !Q_stricmp( string, "ignoreenemies" ) ) {
return BSA_IGNOREENEMIES;
} else {
return -1;
}
}
/*
==================
BotCheckEmergencyTargets()
==================
*/
qboolean BotCheckEmergencyTargets( bot_state_t *bs ) {
qboolean retval;
int startTime = 0;
if ( bot_profile.integer == 1 ) {
startTime = trap_Milliseconds();
}
retval = BotMP_CheckEmergencyGoals( bs );
if ( bot_profile.integer == 1 ) {
botTime_EmergencyGoals += trap_Milliseconds() - startTime;
}
return retval;
}
/*
==================
BotFindSpecialGoals()
==================
*/
qboolean BotFindSpecialGoals( bot_state_t *bs ) {
qboolean retval;
int startTime = 0;
if ( bot_profile.integer == 1 ) {
startTime = trap_Milliseconds();
}
trap_Cvar_Update( &bot_findgoal );
if ( !bot_findgoal.integer ) {
retval = BotMP_FindGoal_New( bs );
} else {
retval = BotMP_FindGoal( bs );
}
if ( bot_profile.integer == 1 ) {
botTime_FindGoals += trap_Milliseconds() - startTime;
}
return retval;
}
/*
===================
BotDefaultNode
===================
*/
void BotDefaultNode( bot_state_t *bs ) {
//if the bot is not in a valid area, then there's not much we can do
if ( !bs->areanum || bs->sess.sessionTeam >= TEAM_SPECTATOR || !bs->sess.sessionTeam ) {
AIEnter_MP_Stand( bs );
return;
}
//check emergency goals
BotClearGoal( &bs->target_goal );
bs->last_checkemergencytargets = 0; // bypass optimizations
if ( BotCheckEmergencyTargets( bs ) ) {
return;
}
//check idle goals
bs->last_findspecialgoals = 0; // bypass optimizations
if ( BotFindSpecialGoals( bs ) ) {
return;
}
//just stand around, waiting for a goal (or enemy) to present itself
AIEnter_MP_Stand( bs );
return;
}
#define PLAYER_PERFORMANCE_COMMENT_FREQENCY 5 // the number of recent shots we will comment on
#define ACCURACY_CHANGE_THRESHOLD 0.2 // how big should the change (in either direction) be to consider commenting
// If we're doing well even if it's not an improvement, comment
#define GOOD_ACCURACY_THRESHOLD ( 0.6f )
// If we're doing poorly even if it's not worse, comment
#define BAD_ACCURACY_THRESHOLD ( 0.1f )
/*
=================
BotCheckVoiceChats()
=================
*/
void BotCheckVoiceChats( bot_state_t *bs ) {
if ( VectorLengthSquared( bs->cur_ps.velocity ) < SQR( 10 ) ) {
// do we need ammo?
// TAT 10/8/2002 - Lieutenants shouldn't bother asking for ammo
if ( bs->sess.playerType != PC_FIELDOPS && ClientNeedsAmmo( bs->client ) ) {
BotVoiceChatAfterIdleTime( bs->client, "NeedAmmo", SAY_TEAM, 2000 + rand() % 10000, qfalse, 40000 + rand() % 15000, qfalse );
}
// do we need health?
// TAT 10/8/2002 - Medics only ask for health if they are dead
if ( ( bs->sess.playerType == PC_MEDIC && BotHealthScale( bs->client ) <= 0.0 ) || ( bs->sess.playerType != PC_MEDIC && BotHealthScale( bs->client ) <= 0.2 ) ) {
BotVoiceChatAfterIdleTime( bs->client, "Medic", SAY_TEAM, 2000 + rand() % 10000, qfalse, 30000 + rand() % 10000, qfalse );
}
// if we have received health, then thank someone
if ( bs->sess.playerType != PC_MEDIC && bs->last_checkvoice_health > 0 && bs->cur_ps.stats[STAT_HEALTH] > bs->last_checkvoice_health ) {
BotVoiceChatAfterIdleTime( bs->client, "Thanks", SAY_TEAM, 500 + rand() % 1000, qfalse, 5000 + rand() % 5000, qfalse );
}
}
bs->last_checkvoice_health = bs->cur_ps.stats[STAT_HEALTH];
}
/*
================
BotEnemyFire
================
*/
void BotEnemyFire( bot_state_t *bs ) {
if ( bs->enemy < 0 ) {
BotFindEnemyMP( bs, -1, qfalse );
}
if ( bs->enemy >= 0 ) {
aas_entityinfo_t entinfo;
BotEntityInfo( bs->enemy, &entinfo );
//if the enemy is dead
if ( bs->enemydeath_time ) {
if ( bs->enemydeath_time < trap_AAS_Time() - 0.3 ) {
bs->enemydeath_time = 0;
bs->enemy = -1;
}
} else {
if ( EntityIsDead( &entinfo ) ) {
bs->enemydeath_time = trap_AAS_Time();
}
}
//
if ( bs->enemy >= 0 ) {
// attack and keep moving
if ( BotEntityVisible( bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy, NULL ) ) {
//choose the best weapon to fight with
BotChooseWeapon( bs );
// TAT 12/6/2002 - don't use the knife here (only can successfully use it in BattleChase)
if ( bs->weaponnum != WP_KNIFE ) {
//aim at the enemy
BotAimAtEnemySP( bs );
//attack the enemy if possible
if ( bs->weaponnum == bs->cur_ps.weapon ) {
BotCheckAttack( bs );
}
}
} else {
bs->enemy = -1;
}
}
}
}
/*
================
BotShareLastAttacked
================
*/
void BotShareLastAttacked( bot_state_t *bs ) {
int i;
bot_state_t *ptbs, *tbs;
gentity_t *ent, *trav;
//
if ( bs->lastAttackShared > level.time - 400 ) {
return;
}
bs->lastAttackShared = level.time;
//
ent = BotGetEntity( bs->client );
//
for ( i = 0, ptbs = botstates, trav = g_entities; i < level.maxclients; i++, ptbs++, trav++ )
{
if ( !ptbs->inuse ) {
continue;
}
if ( !BotSameTeam( bs, i ) ) {
continue;
}
//
tbs = ptbs;
//
if ( ent->botLastAttackedTime >= trav->botLastAttackedTime ) {
continue; // no need to share
}
if ( VectorDistanceSquared( bs->origin, tbs->origin ) > SQR( 2048 ) ) {
continue;
}
if ( !trap_InPVS( bs->origin, tbs->origin ) ) {
continue;
}
//
// get their attack time
ent->botLastAttackedTime = trav->botLastAttackedTime;
ent->botLastAttackedEnt = trav->botLastAttackedEnt;
}
}
/*
==============
BotEnemyCarryingFlag
==============
*/
qboolean BotEnemyCarryingFlag( int entnum ) {
bot_state_t *pbs;
int i;
for ( i = 0, pbs = botstates; i < level.maxclients; i++, pbs++ ) {
if ( !pbs->inuse ) {
continue;
}
if ( BotSameTeam( ( pbs ), entnum ) ) {
continue;
}
if ( pbs->sess.sessionTeam == TEAM_SPECTATOR ) {
continue;
}
if ( !BotCarryingFlag( i ) ) {
continue;
}
//
// carrying flag
return qtrue;
}
//
return qfalse;
}
/*
==============
BotLastAttacked
==============
*/
int BotLastAttacked( bot_state_t *bs ) {
if ( !g_entities[bs->client].botLastAttackedTime ) {
return -99999; // allow for game starting at time index 0
}
return g_entities[bs->client].botLastAttackedTime;
}
/*
==============
BotLastHurt
==============
*/
int BotLastHurt( bot_state_t *bs ) {
if ( !bs->last_pain ) {
return -99999; // allow for game starting at time index 0
}
return bs->last_pain;
}
/*
=================
BotSeekCover
=================
*/
qboolean BotSeekCover( bot_state_t *bs ) {
int area, enemyarea;
vec3_t autonomyPos;
//static int lastcall;
if ( bs->enemy < 0 ) {
return qfalse;
}
// TAT 10/8/2002
// If I'm in an invalid area, then don't do this, it will crash
if ( bs->areanum == 0 ) {
return qfalse;
}
//
// limit calls
//if (bs->lastSeekCover >= level.time - 500) return qfalse; // dont hog calls
//if (lastcall == level.time) return qfalse;
//lastcall = level.time;
// look for a place to hide from our enemy
BotGetMovementAutonomyPos( bs, autonomyPos );
//
enemyarea = BotGetArea( bs->enemy );
//
area = trap_AAS_NearestHideArea( bs->client, bs->origin, bs->areanum, bs->enemy, g_entities[bs->enemy].r.currentOrigin, enemyarea, bs->tfl, BotGetMovementAutonomyRange( bs, NULL ), autonomyPos );
//
if ( area ) {
// get the area waypoint, and return qtrue
BotClearGoal( &bs->hidegoal );
if ( trap_AAS_AreaWaypoint( area, bs->hidegoal.origin ) ) {
bs->hidegoal.areanum = area;
return qtrue;
}
}
//
return qfalse;
}
/*
=================
BotGetEye
=================
*/
float *BotGetEye( int entnum ) {
#define BOT_GETEYE_NUMEYES 9
static vec3_t eyes[BOT_GETEYE_NUMEYES];
static int lastEye = 0;
float *eye;
//
if ( entnum < 0 || entnum >= level.maxclients ) {
G_Error( "BotGetEye: entnum out of range" );
return NULL;
}
//
eye = &eyes[lastEye][0];
if ( ++lastEye >= BOT_GETEYE_NUMEYES ) {
lastEye = 0;
}
//
VectorCopy( g_entities[entnum].client->ps.origin, eye );
eye[2] += g_entities[entnum].client->ps.viewheight;
//
return eye;
}
/*
=================
BotGetOrigin
=================
*/
float *BotGetOrigin( int entnum ) {
#define BOT_GETORIGIN_NUMEYES 9
static vec3_t eyes[BOT_GETORIGIN_NUMEYES];
static int lastEye = 0;
float *eye;
gentity_t *ent = BotGetEntity( entnum );
// TAT 11/12/2002 - using Ryan's botentity system, so might have an entity num out of range
// let's just see if we found one with BotGetEntity
if ( !ent ) {
G_Error( "BotGetOrigin: invalid entity num %d", entnum );
return NULL;
}
//
// if (entnum < 0 || entnum >= level.num_entities) {
// G_Error( "BotGetOrigin: entnum out of range" );
// return NULL;
// }
//
eye = &( eyes[lastEye][0] );
if ( ++lastEye >= BOT_GETORIGIN_NUMEYES ) {
lastEye = 0;
}
//
if ( ( entnum < level.maxclients ) && ( g_entities[entnum].client ) ) {
VectorCopy( ent->client->ps.origin, eye );
} else if ( g_entities[entnum].s.eType == ET_TRIGGER_MULTIPLE || g_entities[entnum].s.eType == ET_MOVER ) {
VectorAdd( ent->r.absmin, ent->r.absmax, eye );
VectorScale( eye, 0.5, eye );
} else if ( VectorLengthSquared( ent->r.currentOrigin ) ) {
VectorCopy( ent->r.currentOrigin, eye );
} else if ( VectorLengthSquared( ent->s.origin ) ) {
VectorCopy( ent->s.origin, eye );
} else {
VectorAdd( ent->r.absmin, ent->r.absmax, eye );
VectorScale( eye, 0.5, eye );
}
//
return eye;
}
/*
================
BotGetArea
================
*/
int BotGetArea( int entnum ) {
bot_state_t *bs = NULL;
gentity_t *ent;
//
if ( entnum < level.maxclients ) {
bs = &botstates[entnum];
}
ent = BotGetEntity( entnum );
//
if ( !ent ) {
// try to get a server entity
g_serverEntity_t *serverEnt = GetServerEntity( entnum );
// we found one, so return the cached value
if ( serverEnt ) {
if ( serverEnt->areaNum == -1 ) {
// we haven't calculated it yet
// these don't move, so only calc once
serverEnt->areaNum = BotPointAreaNum( -1, serverEnt->origin );
}
return serverEnt->areaNum;
}
// didn't find it
return 0;
}
if ( !bs || !bs->inuse ) {
if ( VectorCompare( BotGetOrigin( entnum ), ent->botGetAreaPos ) ) {
return ent->botGetAreaNum;
}
VectorCopy( BotGetOrigin( entnum ), ent->botGetAreaPos );
ent->botGetAreaNum = BotPointAreaNum( entnum, ent->botGetAreaPos );
return ent->botGetAreaNum;
} else {
return bs->areanum;
}
}
/*
==============
BotReduceListByRange
==============
*/
int BotReduceListByTravelTime( int *list, int numList, vec3_t destpos, int destarea, int traveltime ) {
int listCopy[MAX_CLIENTS], numListCopy = 0, i;
int areanum, t;
bot_state_t *lbs;
if ( !traveltime ) {
return numList; // no change
}
for ( i = 0; i < numList; i++ ) {
areanum = BotGetArea( list[i] );
if ( !areanum ) {
continue; // eliminate them
}
lbs = &botstates[list[i]];
if ( !lbs->inuse ) {
continue;
}
t = trap_AAS_AreaTravelTimeToGoalArea( areanum, BotGetOrigin( list[i] ), destarea, lbs->tfl );
if ( !t ) {
continue;
}
if ( t >= traveltime ) {
continue;
}
//
// they passed so copy to the new list
//
listCopy[numListCopy++] = list[i];
}
memcpy( list, listCopy, sizeof( int ) * numListCopy );
return numListCopy;
}
/*
================
BotTravelTimeToEntity
================
*/
int BotTravelTimeToEntity( bot_state_t *bs, int entnum ) {
int area = 0;
//
if ( !bs->areanum ) {
return 0;
}
//
area = BotGetArea( entnum );
//
if ( !area ) {
return 0;
}
//
if ( !bs->tfl ) {
bs->tfl = BotTravelFlagsForClient( bs->client );
}
return trap_AAS_AreaTravelTimeToGoalArea( bs->areanum, bs->origin, area, bs->tfl );
}
// How far can a bot go to seek cover?
int BotGetSeekCoverRange( bot_state_t *bs, int targetEntity );
/*
==============
BotBattleNewNode
==============
*/
qboolean BotBattleNewNode( bot_state_t *bs ) {
return qfalse;
}
/*
===============
BotDirectMoveToGoal
===============
*/
qboolean BotDirectMoveToGoal( bot_state_t *bs, bot_goal_t *goal, bot_moveresult_t *moveresult ) {
vec3_t dir;
aas_clientmove_t move;
trace_t tr;
float dist;
//
if ( VectorDistanceSquared( bs->origin, goal->origin ) > SQR( 1400 ) ) {
return qfalse;
}
if ( !trap_InPVS( bs->origin, goal->origin ) ) {
return qfalse;
}
// trap_Trace( &tr, bs->origin, vec3_origin, vec3_origin, goal->origin, bs->client, MASK_PLAYERSOLID & ~CONTENTS_BODY );
// if (tr.fraction < 1.0) return qfalse;
//
VectorSubtract( goal->origin, bs->origin, dir );
dist = VectorNormalize( dir );
VectorScale( dir, 300, dir );
//
if ( trap_AAS_PredictClientMovement( &move, bs->client, bs->origin,
goal->entitynum, qfalse,
dir, goal->origin, -1,
40, 0.05, SE_ENTERAREA | SE_HITGROUNDDAMAGE | SE_HITENT | SE_HITGROUNDAREA | SE_STUCK | SE_GAP, goal->areanum,
#ifdef _DEBUG
qtrue ) )
#else
qfalse ) )
#endif
{
//
// check for a good stop event
//
switch ( move.stopevent ) {
case SE_ENTERAREA:
case SE_HITENT:
case SE_HITGROUNDAREA:
memset( moveresult, 0, sizeof( *moveresult ) );
VectorNormalize( dir );
VectorCopy( dir, moveresult->movedir );
if ( dist < 200 ) {
trap_EA_Move( bs->client, dir, 400 - ( 320.0f * ( 128.0f - dist ) / 128.0f ) );
} else {
trap_EA_Move( bs->client, dir, 400 );
}
// check against other players/bots
// TAT 2/3/2003 - you can be blocked by stuff that isn't a player - changing trace mask to include other stuff
trap_Trace( &tr, bs->origin, bs->cur_ps.mins, bs->cur_ps.maxs, goal->origin, bs->client, MASK_SHOT /*CONTENTS_BODY*/ );
if ( tr.fraction < .99f && VectorDistanceSquared( bs->origin, tr.endpos ) < SQR( 30 ) && tr.entityNum != ENTITYNUM_WORLD ) { // best not be worldspawn
// blocked, avoid them
moveresult->blocked = qtrue;
moveresult->blockentity = tr.entityNum;
}
moveresult->flags |= MOVERESULT_DIRECTMOVE;
//
return qtrue;
}
}
//
return qfalse;
}
/*
=================
BotEntityTargetClassnameMatch
=================
*/
qboolean BotEntityTargetClassnameMatch( int entityNum, const char *classname ) {
gentity_t *ent;
//
if ( entityNum < 0 || entityNum > level.num_entities ) {
return qfalse;
}
ent = BotGetEntity( entityNum );
if ( !ent->inuse ) {
return qfalse;
}
if ( !ent->target ) {
return qfalse;
}
if ( !ent->target_ent ) {
return qfalse;
}
if ( !ent->target_ent->inuse ) {
return qfalse;
}
//
if ( !Q_stricmp( ent->target_ent->classname, classname ) ) {
return qtrue;
}
//
return qfalse;
}
/*
================
BotGetReachableEntityArea
================
*/
qboolean BotGetReachableEntityArea( bot_state_t *bs, int entityNum, bot_goal_t *goal ) {
vec3_t brushPos, vec, center, mins, maxs;
//int list[256], numList;
int oldestTime = 0, i, oldest = 0;
//float bestDist, dist;
gentity_t *ent;
trace_t tr;
ent = BotGetEntity( entityNum );
if ( VectorDistanceSquared( ent->r.absmin, ent->r.absmax ) > ( 16 * 16 ) ) {
VectorAdd( ent->r.absmin, ent->r.absmax, brushPos );
VectorScale( brushPos, 0.5, brushPos );
oldest = BotReachableBBoxAreaNum( bs, ent->r.absmin, ent->r.absmax );
if ( !oldest ) {
VectorCopy( ent->r.absmax, maxs );
maxs[2] += 32;
oldest = BotReachableBBoxAreaNum( bs, ent->r.absmin, maxs );
}
if ( oldest ) {
oldestTime = trap_AAS_AreaTravelTimeToGoalArea( bs->areanum, bs->origin, oldest, bs->tfl );
}
} else { // use entity point
// TODO
i = BotGetArea( entityNum );
oldestTime = trap_AAS_AreaTravelTimeToGoalArea( bs->areanum, bs->origin, i, bs->tfl );
}
if ( oldestTime <= 0 ) {
return qfalse;
}
BotClearGoal( goal );
// use this as the goal origin
if ( !trap_AAS_AreaWaypoint( oldest, center ) ) {
trap_AAS_AreaCenter( oldest, center );
}
// if the entity is a trigger, then we must make sure we are within the brush
if ( ent->r.contents & CONTENTS_TRIGGER ) {
VectorCopy( center, vec );
VectorAdd( center, bs->cur_ps.mins, mins );
VectorAdd( center, bs->cur_ps.maxs, maxs );
if ( !trap_EntityContactCapsule( mins, maxs, ent ) ) {
VectorCopy( brushPos, center );
center[2] = vec[2];
VectorCopy( brushPos, vec );
vec[2] -= 512;
// trace to the ground
trap_Trace( &tr, center, bs->cur_ps.mins, bs->cur_ps.maxs, vec, -1, MASK_PLAYERSOLID & ~CONTENTS_BODY );
VectorCopy( tr.endpos, center );
// test this spot
VectorAdd( center, bs->cur_ps.mins, mins );
VectorAdd( center, bs->cur_ps.maxs, maxs );
if ( !trap_EntityContactCapsule( mins, maxs, ent ) ) {
return qfalse;
}
oldest = BotPointAreaNum( bs->client, center );
if ( !oldest ) {
return qfalse;
}
}
}
VectorCopy( center, goal->origin );
VectorCopy( bs->cur_ps.mins, goal->mins );
VectorCopy( bs->cur_ps.maxs, goal->maxs );
goal->areanum = oldest;
goal->entitynum = ent->s.number;
goal->flags = GFL_NOSLOWAPPROACH;
return qtrue;
}
/*
====================
BotIsConstructible
returns qtrue if the contructible attached to the given target_objective_info can be built
====================
*/
qboolean BotIsConstructible( team_t team, int toiNum ) {
gentity_t* ent;
gentity_t *toi = &g_entities[toiNum];
// we dont wanna build this
if ( toi->aiInactive & ( 1 << team ) ) {
return qfalse;
}
if ( !( ent = G_ConstructionForTeam( toi, team ) ) ) {
return qfalse;
}
if ( G_ConstructionIsFullyBuilt( ent ) ) {
return qfalse;
}
if ( G_ConstructionIsPartlyBuilt( ent ) ) {
return qtrue;
}
if ( ent->chain && G_ConstructionBegun( ent->chain ) ) {
return qfalse;
}
return qtrue;
}
/*
=================
BotCanSnipe
returns WP_NONE if we cant snipe, otherwise returns the weapon we can snipe with
=================
*/
int BotCanSnipe( bot_state_t *bs, qboolean checkAmmo ) {
int sniperWeapons[] = {WP_GARAND_SCOPE, WP_K43_SCOPE, WP_FG42SCOPE, -1};
int i, best, bestAmmo, thisAmmo;
// Gordon: early out if not covert ops only they have sniper weapons
if ( bs->cur_ps.stats[STAT_PLAYER_CLASS] != PC_COVERTOPS ) {
return WP_NONE;
}
best = WP_NONE;
bestAmmo = 0;
for ( i = 0; sniperWeapons[i] > -1; i++ ) {
if ( !COM_BitCheck( bs->cur_ps.weapons, sniperWeapons[i] ) ) {
continue;
}
thisAmmo = BotGotEnoughAmmoForWeapon( bs, sniperWeapons[i] );
if ( checkAmmo && !thisAmmo ) {
continue;
}
if ( best > -1 && !thisAmmo ) {
continue;
}
if ( best == -1 || ( !bestAmmo && thisAmmo ) ) {
best = sniperWeapons[i];
bestAmmo = thisAmmo;
}
}
return best;
}
/*
=================
BotHealthScale
=================
*/
float BotHealthScale( int entnum ) {
return g_entities[entnum].health / (float)g_entities[entnum].client->ps.stats[STAT_MAX_HEALTH];
}
/*
==================
EnemyIsCloseEnoughToFight()
==================
Returns true if we can do the battle fight.
Returns false if enemy is too far or not visible
*/
qboolean EnemyIsCloseEnoughToFight
(
bot_state_t *bs
) {
// Local Variables ////////////////////////////////////////////////////////
aas_entityinfo_t entinfo;
///////////////////////////////////////////////////////////////////////////
// Get location of enemy and other info.
BotEntityInfo( bs->enemy, &entinfo );
// If we've got the knife, the enemy isn't visible, or the enemy is too far, we
// can't use battle fight.
if ( ( bs->weaponnum == WP_KNIFE )
|| !BotEntityVisible( bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy, NULL )
|| ( VectorDistanceSquared( bs->origin, entinfo.origin ) > SQR( kBOT_CHASE_RANGE ) )
) {
return qfalse;
}
// Otherwise, we're good to fight
return qtrue;
}
// TAT 11/21/2002
// This is silly - almost all the single player ai nodes do the same thing
// Find an enemy and try to attack it
void BotFindAndAttackEnemy( bot_state_t *bs ) {
// check for enemies
if ( bs->enemy < 0 ) {
BotFindEnemyMP( bs, -1, qfalse );
}
if ( bs->enemy >= 0 ) {
aas_entityinfo_t entinfo;
BotEntityInfo( bs->enemy, &entinfo );
//if the enemy is dead
if ( bs->enemydeath_time ) {
if ( bs->enemydeath_time < trap_AAS_Time() - 0.3 ) {
bs->enemydeath_time = 0;
bs->enemy = -1;
}
} else {
if ( EntityIsDead( &entinfo ) ) {
bs->enemydeath_time = trap_AAS_Time();
}
}
//
if ( bs->enemy >= 0 ) {
// attack and keep moving
if ( BotEntityVisible( bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy, NULL ) ) {
//choose the best weapon to fight with
BotChooseWeapon( bs );
BotAimAtEnemy( bs );
//attack the enemy if possible
if ( bs->weaponnum == bs->cur_ps.weapon ) {
BotCheckAttack( bs );
}
} else {
bs->enemy = -1;
}
}
}
}
void BotUpdateViewAngles( bot_state_t *bs, bot_goal_t *goal, bot_moveresult_t moveresult ) {
vec3_t target;
vec3_t dir;
if ( bs->enemy < 0 && !( bs->flags & BFL_IDEALVIEWSET ) ) {
//FIXME: look at cluster portals?
if ( VectorLengthSquared( moveresult.movedir ) ) {
vectoangles( moveresult.movedir, bs->ideal_viewangles );
} else if ( trap_BotMovementViewTarget( bs->ms, &goal, bs->tfl, 300, target ) ) {
VectorSubtract( target, bs->origin, dir );
vectoangles( dir, bs->ideal_viewangles );
} else if ( random() < bs->thinktime * 0.8 ) {
BotRoamGoal( bs, target );
VectorSubtract( target, bs->origin, dir );
vectoangles( dir, bs->ideal_viewangles );
bs->ideal_viewangles[2] *= 0.5;
}
bs->ideal_viewangles[2] *= 0.5;
}
}
// Gorodn: set whether a bot is a POW or not
void BotSetPOW( int entityNum, qboolean isPOW ) {
botstates[entityNum].isPOW = isPOW;
}
/*
==================
BotBestTargetWeapon
==================
*/
int BotBestTargetWeapon( bot_state_t *bs, int targetNum ) {
int bestWeapon, i, *ammo, validWeapons[2];
float wantScale, bestWantScale, dist;
gentity_t *ent = &g_entities[targetNum];
memset( validWeapons, 0, sizeof( validWeapons ) );
// this function currently only supports the following types of target entities
if ( ent->s.eType == ET_MOVER ) {
if ( ent->health > 0 ) {
// explosive weapons are always valid
COM_BitSet( validWeapons, WP_PANZERFAUST );
COM_BitSet( validWeapons, WP_GRENADE_LAUNCHER );
COM_BitSet( validWeapons, WP_GRENADE_PINEAPPLE );
COM_BitSet( validWeapons, WP_SMOKE_MARKER );
if ( bs->sess.playerType == PC_FIELDOPS ) {
COM_BitSet( validWeapons, WP_BINOCULARS );
}
COM_BitSet( validWeapons, WP_MORTAR );
COM_BitSet( validWeapons, WP_GPG40 );
COM_BitSet( validWeapons, WP_M7 );
if ( !( ent->spawnflags & 4 ) ) { // allow other weapons
// use any of these
COM_BitSet( validWeapons, WP_MP40 );
COM_BitSet( validWeapons, WP_THOMPSON );
COM_BitSet( validWeapons, WP_KAR98 );
COM_BitSet( validWeapons, WP_CARBINE );
COM_BitSet( validWeapons, WP_MOBILE_MG42 );
COM_BitSet( validWeapons, WP_K43 );
COM_BitSet( validWeapons, WP_FG42 );
}
}
} else if ( ent->s.eType == ET_CONSTRUCTIBLE ) {
if ( ent->health > 0 ) {
if ( ent->spawnflags & 16 ) {
// explosive
COM_BitSet( validWeapons, WP_PANZERFAUST );
COM_BitSet( validWeapons, WP_GRENADE_LAUNCHER );
COM_BitSet( validWeapons, WP_GRENADE_PINEAPPLE );
COM_BitSet( validWeapons, WP_SMOKE_MARKER );
if ( bs->sess.playerType == PC_FIELDOPS ) {
COM_BitSet( validWeapons, WP_BINOCULARS );
}
COM_BitSet( validWeapons, WP_MORTAR );
COM_BitSet( validWeapons, WP_GPG40 );
COM_BitSet( validWeapons, WP_M7 );
}
}
}
// fast out, if we simply have none of these weapons
if ( !( validWeapons[0] & bs->cur_ps.weapons[0] ) && !( validWeapons[1] & bs->cur_ps.weapons[0] ) ) {
return WP_NONE;
}
ammo = bs->cur_ps.ammo;
bestWantScale = 0.0;
bestWeapon = WP_NONE; // if nothing it found, return WP_NONE
dist = VectorDistanceSquared( bs->origin, BotGetOrigin( ent->s.number ) );
for ( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ ) {
if ( COM_BitCheck( bs->cur_ps.weapons, i ) && COM_BitCheck( validWeapons, i ) ) {
float range;
// check that our ammo is enough
if ( !BotGotEnoughAmmoForWeapon( bs, i ) ) {
continue;
}
// within range?
range = BotWeaponRange( bs, i ) /*+ 512*/;
if ( SQR( range ) < dist ) {
continue;
}
// get the wantScale for this weapon given the current circumstances (0.0 - 1.0)
wantScale = BotWeaponWantScale( bs, i );
if ( wantScale >= bestWantScale ) {
bestWeapon = i;
bestWantScale = wantScale;
}
}
}
return bestWeapon;
}
/*
====================
BotGetVisibleDamagableScriptMover
====================
*/
gentity_t *BotGetVisibleDamagableScriptMover( bot_state_t *bs ) {
gentity_t *trav;
int i, wpn;
for ( i = MAX_CLIENTS, trav = g_entities + MAX_CLIENTS; i < level.num_entities; i++, trav++ ) {
if ( !trav->inuse ) {
continue;
}
if ( trav->s.eType != ET_MOVER && trav->s.eType != ET_CONSTRUCTIBLE ) {
continue;
}
// is it disabled?
if ( trav->aiInactive & ( 1 << bs->sess.sessionTeam ) ) {
continue;
}
// is it damagable?
if ( trav->health <= 0 ) {
continue;
}
if ( trav->s.eType == ET_MOVER ) {
// is it an enemy item?
if ( bs->sess.sessionTeam == TEAM_ALLIES && ( trav->spawnflags & 32 ) ) {
continue; // it's ours
}
if ( bs->sess.sessionTeam == TEAM_AXIS && ( trav->spawnflags & 64 ) ) {
continue; // it's ours
}
} else {
if ( !( trav->spawnflags & 16 ) ) {
continue;
}
// is it an enemy item?
if ( bs->sess.sessionTeam == TEAM_ALLIES && ( trav->spawnflags & 8 ) ) {
continue; // it's ours
}
if ( bs->sess.sessionTeam == TEAM_AXIS && ( trav->spawnflags & 4 ) ) {
continue; // it's ours
}
}
// do we have a weapon that could hurt it?
if ( ( wpn = BotBestTargetWeapon( bs, i ) ) == WP_NONE ) {
continue;
}
if ( !BotEntityVisible( bs->entitynum, bs->eye, bs->viewangles, 360, i, NULL ) ) {
continue;
}
/* // it's an enemy mover, can we see it?
if (!trap_InPVS( bs->eye, BotGetOrigin( i ) )) {
continue;
}
trap_Trace( &tr, bs->eye, vec3_origin, vec3_origin, BotGetOrigin( i ), -1, MASK_PLAYERSOLID & ~(CONTENTS_BODY) );
if (tr.entityNum != i) {
continue;
}*/
//
// we found one
return trav;
}
return NULL;
}
/*
================
BotCountLandMines
================
*/
void BotCountLandMines( void ) {
gentity_t *trav, *mine;
vec3_t org;
static int lasttime;
// only check every second
if ( lasttime && lasttime < level.time && lasttime > level.time - 1000 ) {
return;
}
lasttime = level.time;
// reset counts
trav = NULL;
while ( ( trav = BotFindNextStaticEntity( trav, BOTSTATICENTITY_BOT_LANDMINE_AREA ) ) ) {
trav->count2 = 0;
VectorClear( trav->pos3 );
}
mine = g_entities + level.maxclients;
while ( ( mine = G_FindLandmine( mine ) ) ) {
// doesn't matter if it's not armed, so that we dont drop too many landmines at once
VectorCopy( mine->r.currentOrigin, org );
org[2] += 16;
trav = NULL;
while ( ( trav = BotFindNextStaticEntity( trav, BOTSTATICENTITY_BOT_LANDMINE_AREA ) ) ) {
// are we within range?
if ( PointInBounds( org, trav->r.absmin, trav->r.absmax ) ) {
trav->count2++;
// add this position to the average point
VectorAdd( trav->r.currentOrigin, trav->pos3, trav->pos3 );
// dont break here or else if we have overlapping areas, all mines in the overlapping section will only count to the first area
}
}
}
// finalize average points
trav = NULL;
while ( ( trav = BotFindNextStaticEntity( trav, BOTSTATICENTITY_BOT_LANDMINE_AREA ) ) ) {
if ( trav->count2 ) {
VectorSubtract( trav->pos3, BotGetOrigin( trav->s.number ), trav->pos3 );
VectorScale( trav->pos3, -1.0f / trav->count2, trav->pos3 );
}
}
}
// TAT 1/6/2003 - Bot picks up a new weapon
void BotPickupWeapon( int client, int weaponnum, qboolean alreadyHave ) {
// if we didn't have any weapon before, we want to use this one
bot_state_t *bs = &botstates[client];
gentity_t *player;
int i;
if ( !bs->inuse ) {
return;
}
if ( !alreadyHave && ( bs->weaponnum == WP_NONE ) ) {
bs->weaponnum = weaponnum;
}
// force an update of our weapon
BotChooseWeapon( bs );
// make sure we tell all the clients that we have a weapon
for ( i = 0; i < level.numConnectedClients; i++ )
{
// send the noweapon command with who as the 1st param, and 0 meaning we don't have no weapon
player = g_entities + level.sortedClients[i];
if ( player->inuse && player->client->sess.sessionTeam == bs->mpTeam ) {
trap_SendServerCommand( player->s.number, va( "nwp %i 0", bs->client ) );
}
}
}
/*
==================
BotEntityWithinView
==================
*/
qboolean BotEntityWithinView( bot_state_t *bs, int viewEnt ) {
vec3_t dir, ang;
pmoveExt_t *pmExt;
float arcMin, arcMax, arcDiff, yawDiff, pitchDiff;
float pitchMax = 40.f;
//
if ( viewEnt >= level.maxclients ) {
return qfalse;
}
if ( level.clients[viewEnt].pers.connected != CON_CONNECTED ) {
return qfalse;
}
//
VectorSubtract( BotGetOrigin( viewEnt ), bs->origin, dir );
VectorNormalize( dir );
vectoangles( dir, ang );
//
pmExt = &( level.clients[bs->client].pmext );
//
if ( BG_PlayerMounted( bs->cur_ps.eFlags ) ) {
// limit harc and varc
// pitch (varc)
arcMax = pmExt->varc;
if ( bs->cur_ps.eFlags & EF_AAGUN_ACTIVE ) {
arcMin = 0;
} else if ( bs->cur_ps.eFlags & EF_MOUNTEDTANK ) {
// FIXME: fix this at allow min angle clamp...
arcMin = 20;
arcMax = 50;
} else {
arcMin = pmExt->varc / 2;
}
arcDiff = AngleNormalize180( ang[PITCH] - pmExt->centerangles[PITCH] );
if ( arcDiff > arcMin ) {
return qfalse;
} else if ( arcDiff < -arcMax ) {
return qfalse;
}
if ( !( bs->cur_ps.eFlags & EF_MOUNTEDTANK ) ) {
// yaw (harc)
arcMin = arcMax = pmExt->harc;
arcDiff = AngleNormalize180( ang[YAW] - pmExt->centerangles[YAW] );
if ( arcDiff > arcMin ) {
return qfalse;
} else if ( arcDiff < -arcMax ) {
return qfalse;
}
}
} else if ( bs->cur_ps.weapon == WP_MORTAR_SET ) {
// yaw
yawDiff = ang[YAW] - pmExt->mountedWeaponAngles[YAW];
if ( yawDiff > 180 ) {
yawDiff -= 360;
} else if ( yawDiff < -180 ) {
yawDiff += 360;
}
if ( yawDiff > 30 ) {
return qfalse;
} else if ( yawDiff < -30 ) {
return qfalse;
}
// pitch
pitchDiff = ang[PITCH] - pmExt->mountedWeaponAngles[PITCH];
if ( pitchDiff > 180 ) {
pitchDiff -= 360;
} else if ( pitchDiff < -180 ) {
pitchDiff += 360;
}
if ( pitchDiff > ( pitchMax - 10.f ) ) {
return qfalse;
} else if ( pitchDiff < -( pitchMax ) ) {
return qfalse;
}
} else if ( bs->cur_ps.eFlags & EF_PRONE ) {
// Check if we are allowed to rotate to there
if ( bs->cur_ps.weapon == WP_MOBILE_MG42_SET ) {
pitchMax = 20.f;
// yaw
yawDiff = ang[YAW] - pmExt->mountedWeaponAngles[YAW];
if ( yawDiff > 180 ) {
yawDiff -= 360;
} else if ( yawDiff < -180 ) {
yawDiff += 360;
}
if ( yawDiff > 20 ) {
return qfalse;
} else if ( yawDiff < -20 ) {
return qfalse;
}
}
// pitch
pitchDiff = ang[PITCH] - pmExt->mountedWeaponAngles[PITCH];
if ( pitchDiff > 180 ) {
pitchDiff -= 360;
} else if ( pitchDiff < -180 ) {
pitchDiff += 360;
}
if ( pitchDiff > pitchMax ) {
return qfalse;
} else if ( pitchDiff < -pitchMax ) {
return qfalse;
}
}
return qtrue;
}