mirror of
https://github.com/UberGames/rpgxEF.git
synced 2024-11-15 01:11:25 +00:00
a39565b783
... not quite content with where the project files lie but it is ok for now. ... compiling works fine so far (only tested mingw32 right now)
2857 lines
81 KiB
C
2857 lines
81 KiB
C
// Copyright (C) 1999-2000 Id Software, Inc.
|
|
//
|
|
/*****************************************************************************
|
|
* name: ai_dmq3.c
|
|
*
|
|
* desc: Quake3 bot AI
|
|
*
|
|
* $Archive: /StarTrek/Code-DM/game/ai_dmq3.c $
|
|
* $Author: Mgummelt $
|
|
* $Revision: 33 $
|
|
* $Modtime: 4/04/01 5:01p $
|
|
* $Date: 4/04/01 5:17p $
|
|
*
|
|
*****************************************************************************/
|
|
|
|
|
|
#include "g_local.h"
|
|
#include "botlib.h"
|
|
#include "be_aas.h"
|
|
#include "be_ea.h"
|
|
#include "be_ai_char.h"
|
|
#include "be_ai_chat.h"
|
|
#include "be_ai_gen.h"
|
|
#include "be_ai_goal.h"
|
|
#include "be_ai_move.h"
|
|
#include "be_ai_weap.h"
|
|
//
|
|
#include "ai_main.h"
|
|
#include "ai_dmq3.h"
|
|
#include "ai_chat.h"
|
|
#include "ai_cmd.h"
|
|
#include "ai_dmnet.h"
|
|
#include "ai_team.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_PHASER 2
|
|
|
|
#define MAX_WAYPOINTS 128
|
|
//
|
|
bot_waypoint_t botai_waypoints[MAX_WAYPOINTS];
|
|
bot_waypoint_t *botai_freewaypoints;
|
|
|
|
//NOTE: not using a cvars which can be updated because the game should be reloaded anyway
|
|
int gametype; //game type
|
|
int maxclients; //maximum number of clients
|
|
|
|
vmCvar_t bot_grapple;
|
|
vmCvar_t bot_rocketjump;
|
|
vmCvar_t bot_fastchat;
|
|
vmCvar_t bot_nochat;
|
|
vmCvar_t bot_testrchat;
|
|
vmCvar_t bot_challenge;
|
|
|
|
vec3_t lastteleport_origin; //last teleport event origin
|
|
float lastteleport_time; //last teleport event time
|
|
int max_bspmodelindex; //maximum BSP model index
|
|
|
|
//CTF flag goals
|
|
bot_goal_t ctf_redflag;
|
|
bot_goal_t ctf_blueflag;
|
|
|
|
#ifdef CTF
|
|
/*
|
|
==================
|
|
BotCTFCarryingFlag
|
|
==================
|
|
*/
|
|
int BotCTFCarryingFlag(bot_state_t *bs) {
|
|
if (gametype != GT_CTF) return CTF_FLAG_NONE;
|
|
|
|
if (bs->inventory[INVENTORY_REDFLAG] > 0) return CTF_FLAG_RED;
|
|
else if (bs->inventory[INVENTORY_BLUEFLAG] > 0) return CTF_FLAG_BLUE;
|
|
return CTF_FLAG_NONE;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotCTFTeam
|
|
==================
|
|
*/
|
|
int BotCTFTeam(bot_state_t *bs) {
|
|
char info[1024];
|
|
|
|
if (gametype != GT_CTF) return CTF_TEAM_NONE;
|
|
if (bs->client < 0 || bs->client >= MAX_CLIENTS) {
|
|
//BotAI_Print(PRT_ERROR, "BotCTFTeam: client out of range\n");
|
|
return qfalse;
|
|
}
|
|
trap_GetConfigstring(CS_PLAYERS+bs->client, info, sizeof(info));
|
|
//
|
|
if (atoi(Info_ValueForKey(info, "t")) == TEAM_RED) return CTF_TEAM_RED;
|
|
else if (atoi(Info_ValueForKey(info, "t")) == TEAM_BLUE) return CTF_TEAM_BLUE;
|
|
return CTF_TEAM_NONE;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotCTFRetreatGoals
|
|
==================
|
|
*/
|
|
void BotCTFRetreatGoals(bot_state_t *bs) {
|
|
//when carrying a flag in ctf the bot should rush to the base
|
|
if (BotCTFCarryingFlag(bs)) {
|
|
//if not already rushing to the base
|
|
if (bs->ltgtype != LTG_RUSHBASE) {
|
|
bs->ltgtype = LTG_RUSHBASE;
|
|
bs->teamgoal_time = trap_AAS_Time() + CTF_RUSHBASE_TIME;
|
|
bs->rushbaseaway_time = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
EntityIsDead
|
|
==================
|
|
*/
|
|
qboolean EntityIsDead(aas_entityinfo_t *entinfo) {
|
|
playerState_t ps;
|
|
|
|
if (entinfo->number >= 0 && entinfo->number < MAX_CLIENTS) {
|
|
//retrieve the current client state
|
|
BotAI_GetClientState( entinfo->number, &ps );
|
|
if (ps.pm_type != PM_NORMAL) return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
EntityIsInvisible
|
|
==================
|
|
*/
|
|
qboolean EntityIsInvisible(aas_entityinfo_t *entinfo) {
|
|
if (entinfo->powerups & (1 << PW_GHOST))
|
|
{ // 50% chance of being visible?
|
|
if (((unsigned int)(level.time)/1024)&0x01) // Every second or so, the bot will see the player, so he doesn't jitter.
|
|
{
|
|
return qtrue;
|
|
}
|
|
else
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
else if (entinfo->powerups & (1 << PW_INVIS))
|
|
{
|
|
return qtrue;
|
|
}
|
|
else if ( entinfo->flags & EF_NODRAW )
|
|
{
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
EntityCarriesFlag
|
|
==================
|
|
*/
|
|
qboolean EntityCarriesFlag(aas_entityinfo_t *entinfo) {
|
|
if ( entinfo->powerups & ( 1 << PW_REDFLAG ) ) return qtrue;
|
|
if ( entinfo->powerups & ( 1 << PW_BLUEFLAG ) ) return qtrue;
|
|
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) {
|
|
if (entinfo->powerups & (1 << PW_QUAD)) {
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotCTFSeekGoals
|
|
==================
|
|
*/
|
|
void BotCTFSeekGoals(bot_state_t *bs) {
|
|
float rnd;
|
|
int flagstatus, c;
|
|
|
|
//when carrying a flag in ctf the bot should rush to the base
|
|
if (BotCTFCarryingFlag(bs)) {
|
|
//if not already rushing to the base
|
|
if (bs->ltgtype != LTG_RUSHBASE) {
|
|
bs->ltgtype = LTG_RUSHBASE;
|
|
bs->teamgoal_time = trap_AAS_Time() + CTF_RUSHBASE_TIME;
|
|
bs->rushbaseaway_time = 0;
|
|
}
|
|
else if (bs->rushbaseaway_time > trap_AAS_Time()) {
|
|
if (BotCTFTeam(bs) == CTF_TEAM_RED) flagstatus = bs->redflagstatus;
|
|
else flagstatus = bs->blueflagstatus;
|
|
//if the flag is back
|
|
if (flagstatus == 0) {
|
|
bs->rushbaseaway_time = 0;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
//
|
|
if (BotCTFTeam(bs) == CTF_TEAM_RED) flagstatus = bs->redflagstatus * 2 + bs->blueflagstatus;
|
|
else flagstatus = bs->blueflagstatus * 2 + bs->redflagstatus;
|
|
//if the enemy flag is not at it's base
|
|
if (flagstatus == 1) {
|
|
//if Not defending the base
|
|
if (!(bs->ltgtype == LTG_DEFENDKEYAREA &&
|
|
(bs->teamgoal.number == ctf_redflag.number ||
|
|
bs->teamgoal.number == ctf_blueflag.number))) {
|
|
//if not already accompanying someone
|
|
if (bs->ltgtype != LTG_TEAMACCOMPANY) {
|
|
//if there is avisible team mate flag carrier
|
|
c = BotTeamFlagCarrierVisible(bs);
|
|
if (c >= 0) {
|
|
//follow the flag carrier
|
|
//the team mate
|
|
bs->teammate = c;
|
|
//last time the team mate was visible
|
|
bs->teammatevisible_time = trap_AAS_Time();
|
|
//set the time to send a message to the team mates
|
|
bs->teammessage_time = trap_AAS_Time() + 2 * random();
|
|
//get the team goal time
|
|
bs->teamgoal_time = trap_AAS_Time() + TEAM_ACCOMPANY_TIME;
|
|
bs->ltgtype = LTG_TEAMACCOMPANY;
|
|
bs->formation_dist = 3.5 * 32; //3.5 meter
|
|
bs->arrive_time = 0;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//if the base flag is stolen
|
|
else if (flagstatus == 2) {
|
|
//if not already going for the enemy flag
|
|
if (bs->ltgtype != LTG_GETFLAG) {
|
|
//if there's no bot team leader
|
|
if (!BotTeamLeader(bs)) {
|
|
//go for the enemy flag
|
|
bs->ltgtype = LTG_GETFLAG;
|
|
//no team message
|
|
bs->teammessage_time = 1;
|
|
//set the time the bot will stop getting the flag
|
|
bs->teamgoal_time = trap_AAS_Time() + CTF_GETFLAG_TIME;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
//if both flags not at their bases
|
|
else if (flagstatus == 3) {
|
|
//
|
|
if (bs->ltgtype != LTG_GETFLAG &&
|
|
bs->ltgtype != LTG_TEAMACCOMPANY) {
|
|
//if there is avisible team mate flag carrier
|
|
c = BotTeamFlagCarrierVisible(bs);
|
|
if (c >= 0) {
|
|
//follow the flag carrier
|
|
return;
|
|
}
|
|
else {
|
|
//otherwise attack the enemy base
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
//if the bot is roaming
|
|
if (bs->ctfroam_time > trap_AAS_Time()) return;
|
|
//if already a CTF or 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_RETURNFLAG ||
|
|
bs->ltgtype == LTG_CAMPORDER ||
|
|
bs->ltgtype == LTG_PATROL) {
|
|
return;
|
|
}
|
|
//if the bot has anough aggression to decide what to do
|
|
if (BotAggression(bs) < 50) return;
|
|
//set the time to send a message to the team mates
|
|
bs->teammessage_time = trap_AAS_Time() + 2 * random();
|
|
//get the flag or defend the base
|
|
rnd = random();
|
|
if (rnd < 0.33 && ctf_redflag.areanum && ctf_blueflag.areanum) {
|
|
bs->ltgtype = LTG_GETFLAG;
|
|
//set the time the bot will stop getting the flag
|
|
bs->teamgoal_time = trap_AAS_Time() + CTF_GETFLAG_TIME;
|
|
}
|
|
else if (rnd < 0.66 && ctf_redflag.areanum && ctf_blueflag.areanum) {
|
|
//
|
|
if (BotCTFTeam(bs) == CTF_TEAM_RED) memcpy(&bs->teamgoal, &ctf_redflag, sizeof(bot_goal_t));
|
|
else memcpy(&bs->teamgoal, &ctf_blueflag, sizeof(bot_goal_t));
|
|
//set the ltg type
|
|
bs->ltgtype = LTG_DEFENDKEYAREA;
|
|
//set the time the bot stops defending the base
|
|
bs->teamgoal_time = trap_AAS_Time() + TEAM_DEFENDKEYAREA_TIME;
|
|
bs->defendaway_time = 0;
|
|
}
|
|
else {
|
|
bs->ltgtype = 0;
|
|
//set the time the bot will stop roaming
|
|
bs->ctfroam_time = trap_AAS_Time() + CTF_ROAM_TIME;
|
|
}
|
|
#ifdef DEBUG
|
|
BotPrintTeamGoal(bs);
|
|
#endif //DEBUG
|
|
}
|
|
|
|
#endif //CTF
|
|
|
|
/*
|
|
==================
|
|
BotPointAreaNum
|
|
==================
|
|
*/
|
|
int BotPointAreaNum(vec3_t origin) {
|
|
int areanum, numareas, areas[10];
|
|
vec3_t end;
|
|
|
|
areanum = trap_AAS_PointAreaNum(origin);
|
|
if (areanum) return areanum;
|
|
VectorCopy(origin, end);
|
|
end[2] += 10;
|
|
numareas = trap_AAS_TraceAreas(origin, end, areas, NULL, 10);
|
|
if (numareas > 0) return areas[0];
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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++) {
|
|
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;
|
|
}
|
|
|
|
qboolean BotUseMeleeWeapon(bot_state_t *bs) {
|
|
if ( bs->inventory[ENEMY_HORIZONTAL_DIST] < 64 )
|
|
{
|
|
if ( bs->cur_ps.persistant[PERS_CLASS] == PC_BORG || bs->cur_ps.persistant[PERS_CLASS] == PC_MEDIC )
|
|
{
|
|
return qtrue;
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
/*
|
|
==================
|
|
BotChooseWeapon
|
|
|
|
MCG - FIXME: This should really take into account:
|
|
Projectile vs. instant?
|
|
gravity on projectile?
|
|
Range to enemy vs range of weapon?
|
|
Some randomness on the weights?
|
|
|
|
==================
|
|
*/
|
|
void BotChooseWeapon(bot_state_t *bs) {
|
|
int newweaponnum;
|
|
|
|
if (bs->cur_ps.weaponstate == WEAPON_RAISING || bs->cur_ps.weaponstate == WEAPON_DROPPING)
|
|
{
|
|
trap_EA_SelectWeapon(bs->client, bs->weaponnum);
|
|
}
|
|
else
|
|
{
|
|
newweaponnum = trap_BotChooseBestFightWeapon(bs->ws, bs->inventory, BotUseMeleeWeapon(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);
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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;
|
|
//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) {
|
|
//armor
|
|
bs->inventory[INVENTORY_ARMOR] = bs->cur_ps.stats[STAT_ARMOR];
|
|
|
|
//weapons
|
|
bs->inventory[INVENTORY_GRENADELAUNCHER] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_GRENADE_LAUNCHER)) != 0;
|
|
bs->inventory[INVENTORY_STASIS] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_DISRUPTOR)) != 0;
|
|
bs->inventory[INVENTORY_PHASER] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_PHASER)) != 0;
|
|
bs->inventory[INVENTORY_DREADNOUGHT] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_DERMAL_REGEN)) != 0;
|
|
bs->inventory[INVENTORY_IMOD] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_NULL_HAND)) != 0;
|
|
bs->inventory[INVENTORY_COMPRESSION] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_COMPRESSION_RIFLE)) != 0;
|
|
bs->inventory[INVENTORY_TETRION] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_TR116)) != 0;
|
|
bs->inventory[INVENTORY_SCAVENGER] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_COFFEE)) != 0;
|
|
bs->inventory[INVENTORY_QUANTUM] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_QUANTUM_BURST)) != 0;
|
|
|
|
//ammo
|
|
bs->inventory[INVENTORY_GRENADES] = bs->cur_ps.ammo[WP_GRENADE_LAUNCHER];
|
|
bs->inventory[INVENTORY_STASISAMMO] = bs->cur_ps.ammo[WP_DISRUPTOR];
|
|
bs->inventory[INVENTORY_PHASERAMMO] = bs->cur_ps.ammo[WP_PHASER];
|
|
bs->inventory[INVENTORY_DREADNOUGHTAMMO] = bs->cur_ps.ammo[WP_DERMAL_REGEN];
|
|
bs->inventory[INVENTORY_IMODAMMO] = bs->cur_ps.ammo[WP_NULL_HAND];
|
|
bs->inventory[INVENTORY_COMPRESSIONAMMO] = bs->cur_ps.ammo[WP_COMPRESSION_RIFLE];
|
|
bs->inventory[INVENTORY_TETRIONAMMO] = bs->cur_ps.ammo[WP_TR116];
|
|
bs->inventory[INVENTORY_SCAVENGERAMMO] = bs->cur_ps.ammo[WP_COFFEE];
|
|
bs->inventory[INVENTORY_QUANTUMAMMO] = bs->cur_ps.ammo[WP_QUANTUM_BURST];
|
|
|
|
//powerups
|
|
bs->inventory[INVENTORY_HEALTH] = bs->cur_ps.stats[STAT_HEALTH];
|
|
bs->inventory[INVENTORY_TRANSPORTER] = 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_QUAD] = bs->cur_ps.powerups[PW_QUAD] != 0;
|
|
bs->inventory[INVENTORY_ENVIRONMENTSUIT] = bs->cur_ps.powerups[PW_BOLTON] != 0;
|
|
bs->inventory[INVENTORY_HASTE] = bs->cur_ps.powerups[PW_HASTE] != 0;
|
|
bs->inventory[INVENTORY_INVISIBILITY] = bs->cur_ps.powerups[PW_INVIS] != 0;
|
|
bs->inventory[INVENTORY_REGEN] = bs->cur_ps.powerups[PW_REGEN] != 0;
|
|
bs->inventory[INVENTORY_FLIGHT] = bs->cur_ps.powerups[PW_FLIGHT] != 0;
|
|
bs->inventory[INVENTORY_REDFLAG] = bs->cur_ps.powerups[PW_REDFLAG] != 0;
|
|
bs->inventory[INVENTORY_BLUEFLAG] = bs->cur_ps.powerups[PW_BLUEFLAG] != 0;
|
|
bs->inventory[INVENTORY_SEEKER] = bs->cur_ps.powerups[PW_SEEKER] != 0;
|
|
bs->inventory[INVENTORY_SHIELD] = bs->cur_ps.stats[STAT_HOLDABLE_ITEM] == MODELINDEX_SHIELD;
|
|
bs->inventory[INVENTORY_DETPACK] = bs->cur_ps.stats[STAT_HOLDABLE_ITEM] == MODELINDEX_DETPACK;
|
|
//
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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
|
|
}
|
|
|
|
/*
|
|
=========================
|
|
BotShouldDetonateDetPack
|
|
=========================
|
|
*/
|
|
#define DETPACK_RADIUS 500
|
|
|
|
qboolean BotShouldDetonateDetPack(bot_state_t *bs)
|
|
{
|
|
int botNum = 0, detWeight = 0;
|
|
vec3_t packOrg, dir;
|
|
float dist;
|
|
aas_entityinfo_t botinfo;
|
|
|
|
// find the location of the DetPack
|
|
gentity_t *detpack = NULL;
|
|
char *classname = BG_FindClassnameForHoldable(HI_DETPACK);
|
|
|
|
if (!classname)
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
while ((detpack = G_Find (detpack, FOFS(classname), classname)) != NULL)
|
|
{
|
|
VectorCopy(detpack->r.currentOrigin, packOrg);
|
|
}
|
|
|
|
// determine who would be killed in the blast radius
|
|
for (botNum = 0; botNum < MAX_CLIENTS; botNum++)
|
|
{
|
|
BotEntityInfo(botNum, &botinfo);
|
|
if (!botinfo.valid) continue;
|
|
|
|
//calculate the distance towards the enemy
|
|
VectorSubtract(botinfo.origin, packOrg, dir);
|
|
dist = VectorLength(dir);
|
|
|
|
if (dist < DETPACK_RADIUS) // bot would get caught in blast radius
|
|
{
|
|
if (BotSameTeam(bs, botNum)) // friendly casualty potential
|
|
{
|
|
if (botNum == bs->client) // suicide... bad
|
|
{
|
|
detWeight--;
|
|
}
|
|
if (EntityCarriesFlag(&botinfo)) // it's my teammate, and he's got the flag!
|
|
{
|
|
detWeight -= 11;
|
|
continue;
|
|
}
|
|
detWeight--;
|
|
}
|
|
else
|
|
{
|
|
if(EntityCarriesFlag(&botinfo)) // mwahaha
|
|
{
|
|
detWeight += 14;
|
|
}
|
|
detWeight++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Com_Printf("detWeight %d\n", detWeight);
|
|
|
|
if (detWeight > 0)
|
|
{
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
==================
|
|
BotBattleUseItems
|
|
==================
|
|
*/
|
|
void BotBattleUseItems(bot_state_t *bs) {
|
|
|
|
|
|
if (bs->inventory[INVENTORY_DETPACK] > 0)
|
|
{
|
|
// this needs to be in two stages: placement and detonation
|
|
if (bs->ltgtype == LTG_DEFENDKEYAREA)
|
|
{
|
|
if (bs->inventory[INVENTORY_DETPACK_PLACED] == 0) // not placed yet
|
|
{
|
|
bs->inventory[INVENTORY_DETPACK_PLACED] = 1;
|
|
trap_EA_Use(bs->client); // place it
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (bs->inventory[INVENTORY_DETPACK_PLACED] == 1) // placed
|
|
{
|
|
if (BotShouldDetonateDetPack(bs)) // logic
|
|
{
|
|
bs->inventory[INVENTORY_DETPACK_PLACED] = 0;
|
|
trap_EA_Use(bs->client); // BOOM
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (bs->inventory[INVENTORY_SHIELD] > 0)
|
|
{
|
|
if (BotWantsToRetreat(bs) && (bs->inventory[INVENTORY_HEALTH] < 50))
|
|
{
|
|
trap_EA_Use(bs->client);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (bs->inventory[INVENTORY_TRANSPORTER] > 0)
|
|
{
|
|
if (!BotCTFCarryingFlag(bs) && (bs->inventory[INVENTORY_HEALTH] < 50))
|
|
{
|
|
trap_EA_Use(bs->client);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (bs->inventory[INVENTORY_MEDKIT] > 0)
|
|
{
|
|
if (bs->inventory[INVENTORY_HEALTH] < 30)
|
|
{
|
|
trap_EA_Use(bs->client);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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) {
|
|
return (bs->cur_ps.pm_type == PM_DEAD);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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 be looking at the game code...
|
|
if (level.intermissiontime) return qtrue;
|
|
return (bs->cur_ps.pm_type == PM_FREEZE || bs->cur_ps.pm_type == PM_INTERMISSION);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotInLavaOrSlime
|
|
==================
|
|
*/
|
|
qboolean BotInLavaOrSlime(bot_state_t *bs) {
|
|
vec3_t feet;
|
|
|
|
VectorCopy(bs->origin, feet);
|
|
feet[2] -= 23;
|
|
return (trap_AAS_PointContents(feet) & (CONTENTS_LAVA|CONTENTS_SLIME));
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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_WAYPOINTS; i++) {
|
|
botai_waypoints[i].next = botai_freewaypoints;
|
|
botai_freewaypoints = &botai_waypoints[i];
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
TeamPlayIsOn
|
|
==================
|
|
*/
|
|
int TeamPlayIsOn(void) {
|
|
return ( gametype == GT_TEAM || gametype == GT_CTF );
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotAggression
|
|
==================
|
|
*/
|
|
float BotAggression(bot_state_t *bs) {
|
|
//if the bot has quad
|
|
if (bs->inventory[INVENTORY_QUAD]) {
|
|
//if the bot is not holding the gauntlet or the enemy is really nearby
|
|
if (bs->inventory[ENEMY_HORIZONTAL_DIST] < 80) {
|
|
return 70;
|
|
}
|
|
}
|
|
//if the enemy is located way higher than the bot
|
|
if (bs->inventory[ENEMY_HEIGHT] > 200) return 0;
|
|
//if the bot is very low on health
|
|
if (bs->inventory[INVENTORY_HEALTH] < 60) return 0;
|
|
//if the bot is low on health
|
|
if (bs->inventory[INVENTORY_HEALTH] < 80) {
|
|
//if the bot has insufficient armor
|
|
if (bs->inventory[INVENTORY_ARMOR] < 40) return 0;
|
|
}
|
|
|
|
if (bs->inventory[INVENTORY_DREADNOUGHT] > 0 &&
|
|
bs->inventory[INVENTORY_DREADNOUGHTAMMO] > 0) return 100;
|
|
|
|
if (bs->inventory[INVENTORY_TETRION] > 0 &&
|
|
bs->inventory[INVENTORY_TETRIONAMMO] > 0) return 95;
|
|
|
|
if (bs->inventory[INVENTORY_QUANTUM] > 0 &&
|
|
bs->inventory[INVENTORY_QUANTUMAMMO] > 0) return 90;
|
|
|
|
if (bs->inventory[INVENTORY_STASIS] > 0 &&
|
|
bs->inventory[INVENTORY_STASISAMMO] > 0) return 85;
|
|
|
|
if (bs->inventory[INVENTORY_SCAVENGER] > 0 &&
|
|
bs->inventory[INVENTORY_SCAVENGERAMMO] > 0) return 80;
|
|
|
|
if (bs->inventory[INVENTORY_GRENADELAUNCHER] > 0 &&
|
|
bs->inventory[INVENTORY_GRENADES] > 0) return 75;
|
|
|
|
if (bs->inventory[INVENTORY_IMOD] > 0 &&
|
|
bs->inventory[INVENTORY_IMODAMMO] > 0) return 70;
|
|
|
|
if (bs->inventory[INVENTORY_COMPRESSION] > 0 &&
|
|
bs->inventory[INVENTORY_COMPRESSIONAMMO] > 0) return 65;
|
|
|
|
//otherwise the bot is not feeling too aggressive
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotWantsToRetreat
|
|
==================
|
|
*/
|
|
int BotWantsToRetreat(bot_state_t *bs) {
|
|
aas_entityinfo_t entinfo;
|
|
|
|
//always retreat when carrying a CTF flag
|
|
if (BotCTFCarryingFlag(bs)) return qtrue;
|
|
//
|
|
if (bs->enemy >= 0) {
|
|
//if the enemy is carrying a flag
|
|
BotEntityInfo(bs->enemy, &entinfo);
|
|
if (EntityCarriesFlag(&entinfo)) return qfalse;
|
|
}
|
|
//if the bot is getting the flag
|
|
if (bs->ltgtype == LTG_GETFLAG) return qtrue;
|
|
//
|
|
if (BotAggression(bs) < 50) return qtrue;
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotWantsToChase
|
|
==================
|
|
*/
|
|
int BotWantsToChase(bot_state_t *bs) {
|
|
aas_entityinfo_t entinfo;
|
|
|
|
//always retreat when carrying a CTF flag
|
|
if (BotCTFCarryingFlag(bs)) return qfalse;
|
|
//if the enemy is carrying a flag
|
|
BotEntityInfo(bs->enemy, &entinfo);
|
|
if (EntityCarriesFlag(&entinfo)) return qtrue;
|
|
//if the bot is getting the flag
|
|
if (bs->ltgtype == LTG_GETFLAG) return qfalse;
|
|
//
|
|
if (BotAggression(bs) > 50) return qtrue;
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotWantsToHelp
|
|
==================
|
|
*/
|
|
int BotWantsToHelp(bot_state_t *bs) {
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotCanAndWantsToRocketJump
|
|
==================
|
|
*/
|
|
int BotCanAndWantsToRocketJump(bot_state_t *bs) {
|
|
float rocketjumper;
|
|
|
|
//if rocket jumping is disabled
|
|
if (!bot_rocketjump.integer) return qfalse;
|
|
//if no rocket launcher
|
|
if (bs->inventory[INVENTORY_QUANTUM] <= 0) return qfalse;
|
|
//if low on rockets
|
|
if (bs->inventory[INVENTORY_QUANTUMAMMO] < 1) return qfalse;
|
|
//never rocket jump with the Quad
|
|
if (bs->inventory[INVENTORY_QUAD])
|
|
{
|
|
if ( rpg_selfdamage.integer != 0 )
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
//if low on health
|
|
if (bs->inventory[INVENTORY_HEALTH] < 50)
|
|
{ //if not full health
|
|
if ( rpg_selfdamage.integer != 0 )
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
if (bs->inventory[INVENTORY_HEALTH] < 60)
|
|
{ //if the bot has insufficient armor
|
|
if (bs->inventory[INVENTORY_ARMOR] < 20)
|
|
{
|
|
if ( rpg_selfdamage.integer != 0 )
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
}
|
|
rocketjumper = 1;
|
|
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 = 0;
|
|
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;
|
|
camper = 0;
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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, "Quantum Weapon Enhancer");
|
|
BotDontAvoid(bs, "Nano-Regenerative Protoplasmer");
|
|
BotDontAvoid(bs, "Metaphasic Shielding");
|
|
BotDontAvoid(bs, "Temporal Accelerator");
|
|
BotDontAvoid(bs, "Personal Cloaking Device");
|
|
BotDontAvoid(bs, "Seeker Drone");
|
|
BotDontAvoid(bs, "Anti-Gravity Pack");
|
|
//reset the long term goal time so the bot will go for the powerup
|
|
//NOTE: the long term goal type doesn't change
|
|
bs->ltg_time = 0;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotRoamGoal
|
|
==================
|
|
*/
|
|
void BotRoamGoal(bot_state_t *bs, vec3_t goal) {
|
|
int pc, i;
|
|
float len, rnd;
|
|
vec3_t dir, bestorg, belowbestorg;
|
|
bsp_trace_t trace;
|
|
|
|
for (i = 0; i < 10; i++) {
|
|
//start at the bot origin
|
|
VectorCopy(bs->origin, bestorg);
|
|
rnd = random();
|
|
if (rnd > 0.25) {
|
|
//add a random value to the x-coordinate
|
|
if (random() < 0.5) bestorg[0] -= 800 * random() + 100;
|
|
else bestorg[0] += 800 * random() + 100;
|
|
}
|
|
if (rnd < 0.75) {
|
|
//add a random value to the y-coordinate
|
|
if (random() < 0.5) bestorg[1] -= 800 * random() + 100;
|
|
else bestorg[1] += 800 * random() + 100;
|
|
}
|
|
//add a random value to the z-coordinate (NOTE: 48 = maxjump?)
|
|
bestorg[2] += 2 * 48 * crandom();
|
|
//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(trace.endpos, 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 | 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};
|
|
aas_entityinfo_t entinfo;
|
|
bot_moveresult_t moveresult;
|
|
bot_goal_t goal;
|
|
|
|
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 = 1;
|
|
jumper = 1;
|
|
croucher = 1;
|
|
//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);
|
|
//walk, crouch or jump
|
|
movetype = MOVE_WALK;
|
|
//
|
|
if (bs->attackcrouch_time < trap_AAS_Time() - 1) {
|
|
if (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;
|
|
//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;
|
|
}
|
|
}
|
|
|
|
//if using assimilator or alt-fire hypo...
|
|
if ( bs->weaponnum == WP_TOOLKIT || bs->weaponnum == (WP_VOYAGER_HYPO+WP_NUM_WEAPONS) )
|
|
{//get real close
|
|
attack_dist = 16;
|
|
attack_range = 16;
|
|
}
|
|
else
|
|
{
|
|
attack_dist = IDEAL_ATTACKDIST;
|
|
attack_range = 40;
|
|
}
|
|
|
|
//increase the strafe time
|
|
bs->attackstrafe_time += bs->thinktime;
|
|
//get the strafe change time
|
|
strafechange_time = 0.4 + (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.935) {
|
|
//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);
|
|
}
|
|
//perform the movement
|
|
if (trap_BotMoveInDirection(bs->ms, sideward, 400, 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) {
|
|
char info1[1024], info2[1024];
|
|
|
|
if (bs->client < 0 || bs->client >= MAX_CLIENTS) {
|
|
//BotAI_Print(PRT_ERROR, "BotSameTeam: client out of range\n");
|
|
return qfalse;
|
|
}
|
|
if (entnum < 0 || entnum >= MAX_CLIENTS) {
|
|
//BotAI_Print(PRT_ERROR, "BotSameTeam: client out of range\n");
|
|
return qfalse;
|
|
}
|
|
if (gametype == GT_TEAM || gametype == GT_CTF) {
|
|
trap_GetConfigstring(CS_PLAYERS+bs->client, info1, sizeof(info1));
|
|
trap_GetConfigstring(CS_PLAYERS+entnum, info2, sizeof(info2));
|
|
//
|
|
if (atoi(Info_ValueForKey(info1, "t")) == atoi(Info_ValueForKey(info2, "t"))) return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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) {
|
|
int i, contents_mask, passent, hitent, infog, inwater, otherinfog, pc;
|
|
float fogdist, waterfactor, vis, bestvis;
|
|
bsp_trace_t trace;
|
|
aas_entityinfo_t entinfo;
|
|
vec3_t dir, entangles, start, end, middle;
|
|
|
|
//calculate middle of bounding box
|
|
BotEntityInfo(ent, &entinfo);
|
|
VectorAdd(entinfo.mins, entinfo.maxs, middle);
|
|
VectorScale(middle, 0.5, middle);
|
|
VectorAdd(entinfo.origin, middle, middle);
|
|
//check if entity is within field of vision
|
|
VectorSubtract(middle, eye, dir);
|
|
vectoangles(dir, entangles);
|
|
if (!InFieldOfVision(viewangles, fov, entangles)) return 0;
|
|
//
|
|
pc = trap_AAS_PointContents(eye);
|
|
infog = (pc & CONTENTS_SOLID);
|
|
inwater = (pc & (CONTENTS_LAVA|CONTENTS_SLIME|CONTENTS_WATER));
|
|
//
|
|
bestvis = 0;
|
|
for (i = 0; i < 3; i++) {
|
|
//if the point is not in potential visible sight
|
|
//if (!AAS_inPVS(eye, middle)) continue;
|
|
//
|
|
contents_mask = 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
|
|
if (1) {
|
|
//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 >= 1 || trace.ent == hitent) {
|
|
//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;
|
|
}
|
|
//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;
|
|
//
|
|
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;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotFindEnemy
|
|
==================
|
|
*/
|
|
int BotFindEnemy(bot_state_t *bs, int curenemy) {
|
|
int i, healthdecrease;
|
|
float f, dist, curdist, alertness, easyfragger, vis;
|
|
aas_entityinfo_t entinfo, curenemyinfo;
|
|
vec3_t dir, angles;
|
|
|
|
alertness = 1;
|
|
easyfragger = 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 (EntityCarriesFlag(&curenemyinfo)) return qfalse;
|
|
VectorSubtract(curenemyinfo.origin, bs->origin, dir);
|
|
curdist = VectorLength(dir);
|
|
}
|
|
else {
|
|
curdist = 0;
|
|
}
|
|
//
|
|
|
|
//FIXME: This only finds lowest numbered enemy, not the closest or best!!!
|
|
// 6/15/00 dpk changed this
|
|
for (i = 0; i < maxclients && i < MAX_CLIENTS; i++) {
|
|
|
|
if (i == bs->client) continue;
|
|
//if it's the current enemy
|
|
if (i == curenemy) continue;
|
|
//
|
|
BotEntityInfo(i, &entinfo);
|
|
//
|
|
if (!entinfo.valid) continue;
|
|
//if on the same team
|
|
if (BotSameTeam(bs, i)) 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 and not shooting
|
|
if (EntityIsInvisible(&entinfo) && !EntityIsShooting(&entinfo)) {
|
|
continue;
|
|
}
|
|
//if not an easy fragger don't shoot at chatting players
|
|
if (easyfragger < 0.5 && EntityIsChatting(&entinfo)) 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);
|
|
|
|
|
|
//if this entity is not carrying a flag
|
|
if (!EntityCarriesFlag(&entinfo))
|
|
// if (EntityCarriesFlag(&entinfo))
|
|
/* {
|
|
// pick this one!
|
|
}
|
|
else
|
|
*/ {
|
|
//if this enemy is further away than the current one
|
|
if (curenemy >= 0 && dist > curdist) continue;
|
|
}
|
|
|
|
//if the bot in too far away for me to notice
|
|
if (dist > 900 + alertness * 4000) continue;
|
|
|
|
|
|
//if the bot's health decreased or the enemy is shooting
|
|
if (curenemy < 0 && (healthdecrease || EntityIsShooting(&entinfo))) f = 360;
|
|
else f = 90 + 90 - (90 - (dist > 810 ? 810 : dist) / 9);
|
|
//check if the enemy is visible
|
|
vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, f, i);
|
|
if (vis <= 0) continue;
|
|
//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
|
|
VectorSubtract(bs->origin, entinfo.origin, dir);
|
|
vectoangles(dir, angles);
|
|
//if the bot isn't in the fov of the enemy
|
|
if (!InFieldOfVision(entinfo.angles, 120, angles)) {
|
|
//update some stuff for this enemy
|
|
BotUpdateBattleInventory(bs, i);
|
|
//if the bot doesn't really want to fight
|
|
if (BotWantsToRetreat(bs)) continue;
|
|
}
|
|
}
|
|
// }
|
|
//found an enemy
|
|
bs->enemy = entinfo.number;
|
|
if (curenemy >= 0) bs->enemysight_time = trap_AAS_Time() - 2;
|
|
else bs->enemysight_time = trap_AAS_Time();
|
|
bs->enemysuicide = qfalse;
|
|
bs->enemydeath_time = 0;
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotTeamFlagCarrierVisible
|
|
==================
|
|
*/
|
|
int BotTeamFlagCarrierVisible(bot_state_t *bs) {
|
|
int i;
|
|
float vis;
|
|
aas_entityinfo_t entinfo;
|
|
|
|
for (i = 0; i < maxclients && i < MAX_CLIENTS; i++) {
|
|
if (i == bs->client) continue;
|
|
//
|
|
BotEntityInfo(i, &entinfo);
|
|
//if this player is active
|
|
if (!entinfo.valid) continue;
|
|
//if this player is carrying a flag
|
|
if (!EntityCarriesFlag(&entinfo)) continue;
|
|
//if the flag carrier is not on the same team
|
|
if (!BotSameTeam(bs, i)) continue;
|
|
//if the flag carrier is not visible
|
|
vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, i);
|
|
if (vis <= 0) continue;
|
|
//
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotAimAtEnemy
|
|
|
|
MCG - FIXME: This is not set up at all correctly in the following areas:
|
|
Needs to consider if weapon is an alt weapon for aim and accuracy as well as functionality
|
|
Need to consider range?
|
|
Grenade Primary and alt as well as Scavenger alt need to take into account gravity
|
|
Needs to pick our new weapons correctly to determine which ones they should:
|
|
lead with (projectiles have speed)
|
|
decay aim purposely (instant hit weaps)
|
|
shoot "around corners" (bouncers, mines, splashdamage?)
|
|
|
|
==================
|
|
*/
|
|
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 = 1;
|
|
aim_accuracy = 1;
|
|
//
|
|
if (aim_skill > 0.95)
|
|
{
|
|
//don't aim too early
|
|
reactiontime = 0.5;
|
|
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);
|
|
//get the weapon specific aim accuracy and or aim skill
|
|
|
|
if (wi.number == WP_GRENADE_LAUNCHER) {
|
|
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 (wi.number == WP_DISRUPTOR) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_STASIS, 0, 1);
|
|
aim_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_STASIS, 0, 1);
|
|
}
|
|
if (wi.number == WP_PHASER) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_PHASER, 0, 1);
|
|
}
|
|
if (wi.number == WP_NULL_HAND) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_IMOD, 0, 1);
|
|
}
|
|
if (wi.number == WP_COMPRESSION_RIFLE) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_COMPRESSION, 0, 1);
|
|
}
|
|
if (wi.number == WP_TR116) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_TETRION, 0, 1);
|
|
}
|
|
if (wi.number == WP_DERMAL_REGEN) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_DREADNOUGHT, 0, 1);
|
|
}
|
|
if (wi.number == WP_QUANTUM_BURST) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_QUANTUM, 0, 1);
|
|
aim_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_QUANTUM, 0, 1);
|
|
}
|
|
if (wi.number == WP_COFFEE) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_SCAVENGER, 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);
|
|
}
|
|
//if not extremely skilled
|
|
if (aim_skill < 0.9) {
|
|
VectorSubtract(entinfo.origin, bs->enemyorigin, dir);
|
|
//if the enemy moved a bit
|
|
if (VectorLength(dir) > 48) {
|
|
//if the enemy changed direction
|
|
if (DotProduct(bs->enemyvelocity, enemyvelocity) < 0) {
|
|
//aim accuracy should be worse now
|
|
aim_accuracy *= 0.7;
|
|
}
|
|
}
|
|
}
|
|
//check visibility of enemy
|
|
enemyvisible = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy);
|
|
//if the enemy is visible
|
|
if (enemyvisible) {
|
|
//
|
|
VectorCopy(entinfo.origin, bestorigin);
|
|
bestorigin[2] += 8;
|
|
//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] += 16;
|
|
}
|
|
//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 && VectorLength(dir) < 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 the projectile does radial damage
|
|
if (aim_skill > 0.6 && 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, entinfo.origin, 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] - 8;
|
|
//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 (VectorLength(dir) < 60) {
|
|
VectorSubtract(trace.endpos, start, dir);
|
|
//if the hitpoint is far anough from the bot
|
|
if (VectorLength(dir) > 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 >= 1) {
|
|
//botimport.Print(PRT_MESSAGE, "%1.1f aiming at ground\n", AAS_Time());
|
|
VectorCopy(groundtarget, bestorigin);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
bestorigin[0] += 20 * crandom() * (1 - aim_accuracy);
|
|
bestorigin[1] += 20 * crandom() * (1 - aim_accuracy);
|
|
bestorigin[2] += 10 * crandom() * (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_DISRUPTOR ||
|
|
wi.number == WP_GRENADE_LAUNCHER)
|
|
{
|
|
//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, TFL_DEFAULT, target)) {
|
|
VectorSubtract(target, bs->eye, dir);
|
|
if (VectorLength(dir) > 80) {
|
|
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);
|
|
// kef -- fixme. i'm guessing this is listing all of the instant-hit weapons?
|
|
if (wi.number == WP_PHASER ||
|
|
wi.number == WP_NULL_HAND) {
|
|
//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);
|
|
for (i = 0; i < 3; i++) dir[i] += 0.3 * crandom() * (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 * crandom() * (1 - aim_accuracy);
|
|
bs->ideal_viewangles[PITCH] = AngleMod(bs->ideal_viewangles[PITCH]);
|
|
bs->ideal_viewangles[YAW] += 6 * wi.hspread * crandom() * (1 - aim_accuracy);
|
|
bs->ideal_viewangles[YAW] = AngleMod(bs->ideal_viewangles[YAW]);
|
|
//if the bots should be really challenging
|
|
//if the bot is really accurate and has the enemy in view for some time
|
|
if (aim_accuracy > 0.9 && bs->enemysight_time < trap_AAS_Time() - 1)
|
|
{
|
|
//set the view angles directly
|
|
if (bs->ideal_viewangles[PITCH] > 180)
|
|
{
|
|
bs->ideal_viewangles[PITCH] -= 360;
|
|
}
|
|
VectorCopy(bs->ideal_viewangles, bs->viewangles);
|
|
trap_EA_View(bs->client, bs->viewangles);
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotCheckAttack
|
|
==================
|
|
*/
|
|
void BotCheckAttack(bot_state_t *bs) {
|
|
float points, reactiontime, fov, firethrottle;
|
|
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 = {-8, -8, -8}, maxs = {8, 8, 8};
|
|
|
|
if (bs->enemy < 0) return;
|
|
//
|
|
reactiontime = 0;
|
|
if (bs->enemysight_time > trap_AAS_Time() - reactiontime) return;
|
|
if (bs->teleport_time > trap_AAS_Time() - reactiontime) return;
|
|
//if changing weapons
|
|
if (bs->weaponchange_time > trap_AAS_Time() - 0.1) return;
|
|
//check fire throttle characteristic
|
|
if (bs->firethrottlewait_time > trap_AAS_Time()) return;
|
|
firethrottle = 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;
|
|
}
|
|
}
|
|
//
|
|
BotEntityInfo(bs->enemy, &entinfo);
|
|
VectorSubtract(entinfo.origin, bs->eye, dir);
|
|
//
|
|
if (VectorLength(dir) < 100) fov = 120;
|
|
else fov = 50;
|
|
/*
|
|
//if the enemy isn't visible
|
|
if (!BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, fov, bs->enemy)) {
|
|
//botimport.Print(PRT_MESSAGE, "enemy not visible\n");
|
|
return;
|
|
}*/
|
|
vectoangles(dir, angles);
|
|
if (!InFieldOfVision(bs->viewangles, fov, angles)) return;
|
|
BotAI_Trace(&bsptrace, bs->eye, NULL, NULL, bs->aimtarget, bs->client, CONTENTS_SOLID|CONTENTS_PLAYERCLIP);
|
|
if (bsptrace.fraction < 1 && bsptrace.ent != bs->enemy) return;
|
|
|
|
//get the weapon info
|
|
trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &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, 1000, 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);
|
|
//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;
|
|
}
|
|
//if the projectile does a radial damage
|
|
if (wi.proj.damagetype & DAMAGETYPE_RADIAL) {
|
|
if (trace.fraction * 1000 < wi.proj.radius) {
|
|
points = (wi.proj.damage - 0.5 * trace.fraction * 1000) * 0.5;
|
|
if (points > 0) {
|
|
// selfpreservation = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_SELFPRESERVATION, 0, 1);
|
|
// if (random() < selfpreservation) return;
|
|
return;
|
|
}
|
|
}
|
|
//FIXME: check if a teammate gets radial damage
|
|
}
|
|
}
|
|
//if fire has to be release to activate weapon
|
|
if (wi.flags & WFL_FIRERELEASED) {
|
|
if (bs->flags & BFL_ATTACKED)
|
|
{// check in here to either call the alt_fire or not!! also, adjust the bots weapon... hackery?
|
|
if (bs->weaponnum > WP_NUM_WEAPONS) // it's an alt_fire!
|
|
{
|
|
bs->weaponnum -= WP_NUM_WEAPONS;
|
|
trap_EA_Alt_Attack(bs->client);
|
|
}
|
|
else
|
|
{
|
|
trap_EA_Attack(bs->client);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{ // check in here to either call the alt_fire or not!! also, adjust the bots weapon... hackery?
|
|
if (bs->weaponnum > WP_NUM_WEAPONS) // it's an alt_fire!
|
|
{
|
|
bs->weaponnum -= WP_NUM_WEAPONS;
|
|
trap_EA_Alt_Attack(bs->client);
|
|
}
|
|
else
|
|
{
|
|
trap_EA_Attack(bs->client);
|
|
}
|
|
}
|
|
bs->flags ^= BFL_ATTACKED;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotMapScripts
|
|
==================
|
|
*/
|
|
void BotMapScripts(bot_state_t *bs) {
|
|
char info[1024];
|
|
char mapname[128];
|
|
int i, shootbutton;
|
|
float aim_accuracy;
|
|
aas_entityinfo_t entinfo;
|
|
vec3_t dir;
|
|
|
|
trap_GetServerinfo(info, sizeof(info));
|
|
|
|
strncpy(mapname, Info_ValueForKey( info, "mapname" ), sizeof(mapname)-1);
|
|
mapname[sizeof(mapname)-1] = '\0';
|
|
|
|
if (!Q_stricmp(mapname, "q3tourney6")) {
|
|
vec3_t mins = {700, 204, 672}, maxs = {964, 468, 680};
|
|
vec3_t buttonorg = {304, 352, 920};
|
|
//NOTE: NEVER use the func_bobbing in q3tourney6
|
|
bs->tfl &= ~TFL_FUNCBOB;
|
|
//if the bot is below the bounding box
|
|
if (bs->origin[0] > mins[0] && bs->origin[0] < maxs[0]) {
|
|
if (bs->origin[1] > mins[1] && bs->origin[1] < maxs[1]) {
|
|
if (bs->origin[2] < mins[2]) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
shootbutton = qfalse;
|
|
//if an enemy is below this bounding box then shoot the button
|
|
for (i = 0; i < maxclients && i < MAX_CLIENTS; i++) {
|
|
|
|
if (i == bs->client) continue;
|
|
//
|
|
BotEntityInfo(i, &entinfo);
|
|
//
|
|
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 (entinfo.origin[0] > mins[0] && entinfo.origin[0] < maxs[0]) {
|
|
if (entinfo.origin[1] > mins[1] && entinfo.origin[1] < maxs[1]) {
|
|
if (entinfo.origin[2] < mins[2]) {
|
|
//if there's a team mate below the crusher
|
|
if (BotSameTeam(bs, i)) {
|
|
shootbutton = qfalse;
|
|
break;
|
|
}
|
|
else {
|
|
shootbutton = qtrue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (shootbutton) {
|
|
bs->flags |= BFL_IDEALVIEWSET;
|
|
VectorSubtract(buttonorg, bs->eye, dir);
|
|
vectoangles(dir, bs->ideal_viewangles);
|
|
aim_accuracy = 1;
|
|
bs->ideal_viewangles[PITCH] += 8 * crandom() * (1 - aim_accuracy);
|
|
bs->ideal_viewangles[PITCH] = AngleMod(bs->ideal_viewangles[PITCH]);
|
|
bs->ideal_viewangles[YAW] += 8 * crandom() * (1 - aim_accuracy);
|
|
bs->ideal_viewangles[YAW] = AngleMod(bs->ideal_viewangles[YAW]);
|
|
//
|
|
if (InFieldOfVision(bs->viewangles, 20, bs->ideal_viewangles))
|
|
{// check in here to either call the alt_fire or not!! also, adjust the bots weapon... hackery?
|
|
if (bs->weaponnum > WP_NUM_WEAPONS) // it's an alt_fire!
|
|
{
|
|
bs->weaponnum -= WP_NUM_WEAPONS;
|
|
trap_EA_Alt_Attack(bs->client);
|
|
}
|
|
else
|
|
{
|
|
trap_EA_Attack(bs->client);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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;
|
|
|
|
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;
|
|
}
|
|
}
|
|
//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;
|
|
}
|
|
//only allows single activation chains, tree-like activation can be added back in
|
|
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);
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotModelMinsMaxs
|
|
|
|
this is ugly
|
|
==================
|
|
*/
|
|
void BotModelMinsMaxs(int modelindex, int eType, vec3_t mins, vec3_t maxs) {
|
|
gentity_t *ent;
|
|
int i;
|
|
|
|
ent = &g_entities[0];
|
|
for (i = 0; i < level.num_entities; i++, ent++) {
|
|
if ( !ent->inuse ) {
|
|
continue;
|
|
}
|
|
if ( ent->s.eType != eType) {
|
|
continue;
|
|
}
|
|
if (ent->s.modelindex == modelindex) {
|
|
VectorAdd(ent->r.currentOrigin, ent->r.mins, mins);
|
|
VectorAdd(ent->r.currentOrigin, ent->r.maxs, maxs);
|
|
return;
|
|
}
|
|
}
|
|
VectorClear(mins);
|
|
VectorClear(maxs);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotAIBlocked
|
|
|
|
very basic handling of bots being blocked by other entities
|
|
check what kind of entity is blocking the bot and try to activate
|
|
it otherwise try to walk around the entity
|
|
before the bot ends in this part of the AI it should predict which doors to open,
|
|
which buttons to activate etc.
|
|
==================
|
|
*/
|
|
void BotAIBlocked(bot_state_t *bs, bot_moveresult_t *moveresult, int activate) {
|
|
int movetype, ent, i, areas[10], numareas, modelindex;
|
|
char classname[128], model[128];
|
|
float lip, dist, health, angle;
|
|
vec3_t hordir, size, start, end, mins, maxs, sideward, angles;
|
|
vec3_t movedir, origin, goalorigin, bboxmins, bboxmaxs;
|
|
vec3_t up = {0, 0, 1}, extramins = {1, 1, 1}, extramaxs = {-1, -1, -1};
|
|
aas_entityinfo_t entinfo;
|
|
//bsp_trace_t bsptrace;
|
|
#ifdef OBSTACLEDEBUG
|
|
char netname[MAX_NETNAME];
|
|
char buf[128];
|
|
#endif
|
|
|
|
if (!moveresult->blocked) {
|
|
bs->notblocked_time = trap_AAS_Time();
|
|
return;
|
|
}
|
|
//
|
|
BotEntityInfo(moveresult->blockentity, &entinfo);
|
|
#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
|
|
//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
|
|
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
|
|
}
|
|
if (!strcmp(classname, "func_button")) {
|
|
//create a bot goal towards the button
|
|
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, ET_MOVER, mins, maxs);
|
|
//get the lip of the button
|
|
trap_AAS_FloatForBSPEpairKey(ent, "lip", &lip);
|
|
if (!lip) lip = 4;
|
|
//get the move direction from the angle
|
|
trap_AAS_FloatForBSPEpairKey(ent, "angle", &angle);
|
|
VectorSet(angles, 0, angle, 0);
|
|
BotSetMovedir(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];
|
|
dist *= 0.5;
|
|
//
|
|
trap_AAS_FloatForBSPEpairKey(ent, "health", &health);
|
|
//if the button is shootable
|
|
if (health) {
|
|
//FIXME: walk to a point where the button is visible and shoot at the button
|
|
//calculate the goal origin
|
|
VectorMA(origin, -dist, movedir, goalorigin);
|
|
//
|
|
VectorSubtract(goalorigin, bs->origin, movedir);
|
|
vectoangles(movedir, moveresult->ideal_viewangles);
|
|
moveresult->flags |= MOVERESULT_MOVEMENTVIEW;
|
|
moveresult->flags |= MOVERESULT_MOVEMENTWEAPON;
|
|
//select the machinegun and shoot
|
|
trap_EA_SelectWeapon(bs->client, WEAPONINDEX_PHASER);
|
|
if (bs->cur_ps.weapon == WEAPONINDEX_PHASER) {
|
|
trap_EA_Attack(bs->client);
|
|
}
|
|
return;
|
|
}
|
|
else {
|
|
//add bounding box size to the dist
|
|
trap_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]);
|
|
}
|
|
//calculate the goal origin
|
|
VectorMA(origin, -dist, movedir, goalorigin);
|
|
//
|
|
VectorCopy(goalorigin, start);
|
|
start[2] += 24;
|
|
VectorCopy(start, end);
|
|
end[2] -= 100;
|
|
numareas = trap_AAS_TraceAreas(start, end, areas, NULL, 10);
|
|
//
|
|
for (i = 0; i < numareas; i++) {
|
|
if (trap_AAS_AreaReachability(areas[i])) {
|
|
break;
|
|
}
|
|
}
|
|
if (i < numareas) {
|
|
//
|
|
#ifdef OBSTACLEDEBUG
|
|
if (bs->activatemessage_time < trap_AAS_Time()) {
|
|
Com_sprintf(buf, sizeof(buf), "I have to activate a button at %1.1f %1.1f %1.1f in area %d\n",
|
|
goalorigin[0], goalorigin[1], goalorigin[2], areas[i]);
|
|
trap_EA_Say(bs->client, buf);
|
|
bs->activatemessage_time = trap_AAS_Time() + 5;
|
|
}
|
|
#endif //OBSTACLEDEBUG
|
|
//
|
|
VectorCopy(origin, bs->activategoal.origin);
|
|
bs->activategoal.areanum = areas[i];
|
|
VectorSubtract(mins, origin, bs->activategoal.mins);
|
|
VectorSubtract(maxs, origin, bs->activategoal.maxs);
|
|
//
|
|
for (i = 0; i < 3; i++)
|
|
{
|
|
if (movedir[i] < 0) bs->activategoal.maxs[i] += fabs(movedir[i]) * fabs(extramaxs[i]);
|
|
else bs->activategoal.mins[i] += fabs(movedir[i]) * fabs(extramins[i]);
|
|
} //end for
|
|
//
|
|
bs->activategoal.entitynum = entinfo.number;
|
|
bs->activategoal.number = 0;
|
|
bs->activategoal.flags = 0;
|
|
bs->activate_time = trap_AAS_Time() + 10;
|
|
AIEnter_Seek_ActivateEntity(bs);
|
|
return;
|
|
}
|
|
else {
|
|
#ifdef OBSTACLEDEBUG
|
|
if (!numareas) BotAI_Print(PRT_MESSAGE, "button not in an area\n");
|
|
else BotAI_Print(PRT_MESSAGE, "button area has no reachabilities\n");
|
|
#endif //OBSTACLEDEBUG
|
|
if (bs->ainode == AINode_Seek_NBG) bs->nbg_time = 0;
|
|
else if (bs->ainode == AINode_Seek_LTG) bs->ltg_time = 0;
|
|
}
|
|
}
|
|
}
|
|
else if (!strcmp(classname, "func_door")) {
|
|
//shoot at the shootable door
|
|
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, ET_MOVER, mins, maxs);
|
|
//door origin
|
|
VectorAdd(mins, maxs, origin);
|
|
VectorScale(origin, 0.5, origin);
|
|
//
|
|
VectorSubtract(origin, bs->eye, movedir);
|
|
vectoangles(movedir, moveresult->ideal_viewangles);
|
|
moveresult->flags |= MOVERESULT_MOVEMENTVIEW;
|
|
moveresult->flags |= MOVERESULT_MOVEMENTWEAPON;
|
|
//select the machinegun and shoot
|
|
trap_EA_SelectWeapon(bs->client, WEAPONINDEX_PHASER);
|
|
if (bs->cur_ps.weapon == WEAPONINDEX_PHASER) {
|
|
trap_EA_Attack(bs->client);
|
|
}
|
|
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
|
|
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);
|
|
}
|
|
}
|
|
//
|
|
if (bs->notblocked_time < trap_AAS_Time() - 0.4) {
|
|
//just reset goals and hope the bot will go into another direction
|
|
//is this still needed??
|
|
if (bs->ainode == AINode_Seek_NBG) bs->nbg_time = 0;
|
|
else if (bs->ainode == AINode_Seek_LTG) bs->ltg_time = 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotCheckConsoleMessages
|
|
==================
|
|
*/
|
|
void BotCheckConsoleMessages(bot_state_t *bs) {
|
|
char botname[MAX_NETNAME], message[MAX_MESSAGE_SIZE], netname[MAX_NETNAME], *ptr;
|
|
float chat_reply;
|
|
int context, handle;
|
|
bot_consolemessage_t m;
|
|
bot_match_t match;
|
|
|
|
//the name of this bot
|
|
ClientName(bs->client, botname, sizeof(botname));
|
|
//
|
|
while((handle = trap_BotNextConsoleMessage(bs->cs, &m)) != 0) {
|
|
//if the chat state is flooded with messages the bot will read them quickly
|
|
if (trap_BotNumConsoleMessages(bs->cs) < 10) {
|
|
//if it is a chat message the bot needs some time to read it
|
|
if (m.type == CMS_CHAT && m.time > trap_AAS_Time() - (1 + random())) break;
|
|
}
|
|
//
|
|
ptr = m.message;
|
|
//if it is a chat message then don't unify white spaces and don't
|
|
//replace synonyms in the netname
|
|
if (m.type == CMS_CHAT) {
|
|
//
|
|
if (trap_BotFindMatch(m.message, &match, MTCONTEXT_REPLYCHAT)) {
|
|
ptr = m.message + match.variables[MESSAGE].offset;
|
|
}
|
|
}
|
|
//unify the white spaces in the message
|
|
trap_UnifyWhiteSpaces(ptr);
|
|
//replace synonyms in the right context
|
|
context = CONTEXT_NORMAL|CONTEXT_NEARBYITEM|CONTEXT_NAMES;
|
|
if (BotCTFTeam(bs) == CTF_TEAM_RED) context |= CONTEXT_CTFREDTEAM;
|
|
else context |= CONTEXT_CTFBLUETEAM;
|
|
trap_BotReplaceSynonyms(ptr, context);
|
|
//if there's no match
|
|
if (!BotMatchMessage(bs, m.message)) {
|
|
//if it is a chat message
|
|
if (m.type == CMS_CHAT && !bot_nochat.integer) {
|
|
//
|
|
if (!trap_BotFindMatch(m.message, &match, MTCONTEXT_REPLYCHAT)) {
|
|
trap_BotRemoveConsoleMessage(bs->cs, handle);
|
|
continue;
|
|
}
|
|
//don't use eliza chats with team messages
|
|
if (match.subtype & ST_TEAM) {
|
|
trap_BotRemoveConsoleMessage(bs->cs, handle);
|
|
continue;
|
|
}
|
|
//
|
|
trap_BotMatchVariable(&match, NETNAME, netname, sizeof(netname));
|
|
trap_BotMatchVariable(&match, MESSAGE, message, sizeof(message));
|
|
//if this is a message from the bot self
|
|
if (!Q_stricmp(netname, botname)) {
|
|
trap_BotRemoveConsoleMessage(bs->cs, handle);
|
|
continue;
|
|
}
|
|
//unify the message
|
|
trap_UnifyWhiteSpaces(message);
|
|
//
|
|
trap_Cvar_Update(&bot_testrchat);
|
|
if (bot_testrchat.integer) {
|
|
//
|
|
trap_BotLibVarSet("bot_testrchat", "1");
|
|
//if bot replies with a chat message
|
|
if (trap_BotReplyChat(bs->cs, message, context, CONTEXT_REPLY,
|
|
NULL, NULL,
|
|
NULL, NULL,
|
|
NULL, NULL,
|
|
botname, netname)) {
|
|
BotAI_Print(PRT_MESSAGE, "------------------------\n");
|
|
}
|
|
else {
|
|
BotAI_Print(PRT_MESSAGE, "**** no valid reply ****\n");
|
|
}
|
|
}
|
|
//if at a valid chat position and not chatting already
|
|
else if (bs->ainode != AINode_Stand && BotValidChatPosition(bs)) {
|
|
chat_reply = 0;
|
|
if (random() < 1.5 / (NumBots()+1) && random() < chat_reply) {
|
|
//if bot replies with a chat message
|
|
if (trap_BotReplyChat(bs->cs, message, context, CONTEXT_REPLY,
|
|
NULL, NULL,
|
|
NULL, NULL,
|
|
NULL, NULL,
|
|
botname, netname)) {
|
|
//remove the console message
|
|
trap_BotRemoveConsoleMessage(bs->cs, handle);
|
|
bs->stand_time = trap_AAS_Time() + BotChatTime(bs);
|
|
AIEnter_Stand(bs);
|
|
//EA_Say(bs->client, bs->cs.chatmessage);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//remove the console message
|
|
trap_BotRemoveConsoleMessage(bs->cs, handle);
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotCheckEvents
|
|
==================
|
|
*/
|
|
void BotCheckEvents(bot_state_t *bs, entityState_t *state) {
|
|
int event;
|
|
char buf[128];
|
|
//
|
|
//NOTE: 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;
|
|
//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->botdeathtype = mod;
|
|
bs->lastkilledby = attacker;
|
|
//
|
|
if (target == attacker) bs->botsuicide = qtrue;
|
|
else bs->botsuicide = qfalse;
|
|
//
|
|
bs->num_deaths++;
|
|
}
|
|
//else if this client was killed by the bot
|
|
else if (attacker == bs->client) {
|
|
bs->enemydeathtype = mod;
|
|
bs->lastkilledplayer = target;
|
|
bs->killedenemy_time = trap_AAS_Time();
|
|
//
|
|
bs->num_kills++;
|
|
}
|
|
else if (attacker == bs->enemy && target == attacker) {
|
|
bs->enemysuicide = qtrue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case EV_GLOBAL_SOUND:
|
|
{
|
|
if (state->eventParm < 0 || state->eventParm > MAX_SOUNDS) {
|
|
BotAI_Print(PRT_ERROR, "EV_GLOBAL_SOUND: eventParm (%d) out of range\n", state->eventParm);
|
|
break;
|
|
}
|
|
else {
|
|
trap_GetConfigstring(CS_SOUNDS + state->eventParm, buf, sizeof(buf));
|
|
if (!strcmp(buf, "sound/items/poweruprespawn.wav")) {
|
|
//powerup respawned... go get it
|
|
BotGoForPowerups(bs);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case EV_TEAM_SOUND:
|
|
{
|
|
if (state->eventParm < 0 || state->eventParm > MAX_TEAM_SOUNDS) {
|
|
BotAI_Print(PRT_ERROR, "EV_TEAM_SOUND: eventParm (%d) out of range\n", state->eventParm);
|
|
break;
|
|
}
|
|
|
|
if (state->eventParm == RETURN_FLAG_SOUND)
|
|
{
|
|
if (state->otherEntityNum == TEAM_RED)
|
|
{
|
|
//red flag is returned
|
|
bs->redflagstatus = 0;
|
|
bs->flagstatuschanged = qtrue;
|
|
}
|
|
else
|
|
{
|
|
//blue flag is returned
|
|
bs->blueflagstatus = 0;
|
|
bs->flagstatuschanged = qtrue;
|
|
}
|
|
}
|
|
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_TRANSPORTER] > 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;
|
|
}
|
|
}
|
|
bs->lastair_time = trap_AAS_Time();
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotDeathmatchAI
|
|
==================
|
|
*/
|
|
void BotDeathmatchAI(bot_state_t *bs, float thinktime) {
|
|
char gender[144], name[144], buf[144];
|
|
char userinfo[MAX_INFO_STRING];
|
|
int i;
|
|
|
|
//if the bot has just been setup
|
|
if (bs->setupcount > 0) {
|
|
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);
|
|
}
|
|
if ( g_pModSpecialties.integer ) {
|
|
Com_sprintf(buf, sizeof(buf), "class %s", bs->settings.pclass);
|
|
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->lasthitcount = bs->cur_ps.persistant[PERS_HITS];
|
|
//
|
|
bs->setupcount = 0;
|
|
}
|
|
//no ideal view set
|
|
bs->flags &= ~BFL_IDEALVIEWSET;
|
|
//set the teleport time
|
|
BotSetTeleportTime(bs);
|
|
//update some inventory values
|
|
BotUpdateInventory(bs);
|
|
//check the console messages
|
|
BotCheckConsoleMessages(bs);
|
|
//check out the snapshot
|
|
BotCheckSnapshot(bs);
|
|
//check for air
|
|
BotCheckAir(bs);
|
|
//if not in the intermission and not in observer mode
|
|
if (!BotIntermission(bs) && !BotIsObserver(bs)) {
|
|
//do team AI
|
|
BotTeamAI(bs);
|
|
}
|
|
//if the bot has no ai node
|
|
if (!bs->ainode) {
|
|
AIEnter_Seek_LTG(bs);
|
|
}
|
|
//if the bot entered the game less than 8 seconds ago
|
|
if (!bs->entergamechat && bs->entergame_time > trap_AAS_Time() - 8) {
|
|
if (BotChat_EnterGame(bs)) {
|
|
bs->stand_time = trap_AAS_Time() + BotChatTime(bs);
|
|
AIEnter_Stand(bs);
|
|
}
|
|
bs->entergamechat = qtrue;
|
|
}
|
|
//reset the node switches from the previous frame
|
|
BotResetNodeSwitches();
|
|
//execute AI nodes
|
|
for (i = 0; i < MAX_NODESWITCHES; i++) {
|
|
if (bs->ainode(bs)) break;
|
|
}
|
|
//if the bot removed itself :)
|
|
if (!bs->inuse) return;
|
|
//if the bot executed too many AI nodes
|
|
if (i >= MAX_NODESWITCHES) {
|
|
trap_BotDumpGoalStack(bs->gs);
|
|
trap_BotDumpAvoidGoals(bs->gs);
|
|
BotDumpNodeSwitches(bs);
|
|
ClientName(bs->client, name, sizeof(name));
|
|
BotAI_Print(PRT_ERROR, "%s at %1.1f switched more than %d AI nodes\n", name, trap_AAS_Time(), MAX_NODESWITCHES);
|
|
}
|
|
//
|
|
bs->lastframe_health = bs->inventory[INVENTORY_HEALTH];
|
|
bs->lasthitcount = bs->cur_ps.persistant[PERS_HITS];
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotSetupDeathmatchAI
|
|
==================
|
|
*/
|
|
void BotSetupDeathmatchAI(void) {
|
|
int ent, modelnum;
|
|
char model[128];
|
|
|
|
gametype = trap_Cvar_VariableIntegerValue("g_gametype");
|
|
maxclients = trap_Cvar_VariableIntegerValue("sv_maxclients");
|
|
|
|
trap_Cvar_Register(&bot_rocketjump, "bot_rocketjump", "1", 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", "0", 0);
|
|
trap_Cvar_Register(&bot_testrchat, "bot_testrchat", "0", 0);
|
|
trap_Cvar_Register(&bot_challenge, "bot_challenge", "0", 0);
|
|
//
|
|
if (gametype == GT_CTF) {
|
|
if (trap_BotGetLevelItemGoal(-1, "Red Flag", &ctf_redflag) < 0)
|
|
BotAI_Print(PRT_WARNING, "CTF without Red Flag\n");
|
|
if (trap_BotGetLevelItemGoal(-1, "Blue Flag", &ctf_blueflag) < 0)
|
|
BotAI_Print(PRT_WARNING, "CTF without Blue Flag\n");
|
|
}
|
|
|
|
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) {
|
|
}
|
|
|