mirror of
https://github.com/id-Software/Enemy-Territory.git
synced 2026-03-20 00:49:36 +01:00
7573 lines
194 KiB
C
7573 lines
194 KiB
C
/*
|
||
===========================================================================
|
||
|
||
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;
|
||
}
|