mirror of
https://github.com/ReactionQuake3/reaction.git
synced 2024-12-01 16:42:42 +00:00
5500 lines
149 KiB
C
5500 lines
149 KiB
C
//-----------------------------------------------------------------------------
|
|
//
|
|
// $Id$
|
|
//
|
|
//-----------------------------------------------------------------------------
|
|
//
|
|
// $Log$
|
|
// Revision 1.56 2005/02/15 16:33:39 makro
|
|
// Tons of updates (entity tree attachment system, UI vectors)
|
|
//
|
|
// Revision 1.55 2003/03/09 21:30:38 jbravo
|
|
// Adding unlagged. Still needs work.
|
|
//
|
|
// Revision 1.54 2003/02/27 07:33:58 jbravo
|
|
// Bots stfy about flags. Its cases. Teamname fixes. TP style TP fixes.
|
|
//
|
|
// Revision 1.53 2003/01/08 04:46:26 jbravo
|
|
// Wrote a new hackish model replacement system
|
|
//
|
|
// Revision 1.52 2002/11/17 20:14:15 jbravo
|
|
// Itembanning added
|
|
//
|
|
// Revision 1.51 2002/09/29 16:06:44 jbravo
|
|
// Work done at the HPWorld expo
|
|
//
|
|
// Revision 1.50 2002/07/22 06:34:46 niceass
|
|
// cleaned up the powerup code
|
|
//
|
|
// Revision 1.49 2002/07/02 20:22:35 jbravo
|
|
// Changed the files to use the right ui.
|
|
//
|
|
// Revision 1.48 2002/07/01 19:55:50 makro
|
|
// Reorganized things a little
|
|
//
|
|
// Revision 1.47 2002/07/01 19:20:46 makro
|
|
// Bots now use semi-auto when firing the mk23 from long range
|
|
//
|
|
// Revision 1.46 2002/06/16 20:06:13 jbravo
|
|
// Reindented all the source files with "indent -kr -ut -i8 -l120 -lc120 -sob -bad -bap"
|
|
//
|
|
// Revision 1.45 2002/06/16 17:37:59 jbravo
|
|
// Removed the MISSIONPACK ifdefs and missionpack only code.
|
|
//
|
|
// Revision 1.44 2002/06/15 15:02:05 makro
|
|
// Recoded AI for weapon mode switching. Bots can now zoom with the sg
|
|
//
|
|
// Revision 1.43 2002/06/11 14:03:04 makro
|
|
// Forgot (), heh
|
|
//
|
|
// Revision 1.42 2002/06/11 13:42:54 makro
|
|
// Added a time limit for changing weapon modes for bots
|
|
//
|
|
// Revision 1.41 2002/06/06 20:04:10 makro
|
|
// Checking teams
|
|
//
|
|
// Revision 1.40 2002/06/01 13:37:02 makro
|
|
// Tweaked bandaging code
|
|
//
|
|
// Revision 1.39 2002/05/31 18:17:10 makro
|
|
// Bot stuff. Added a server command that prints a line to a client
|
|
// and everyone who is spectating him
|
|
//
|
|
// Revision 1.38 2002/05/30 21:18:28 makro
|
|
// Bots should reload/bandage when roaming around
|
|
// Added "pathtarget" key to all the entities
|
|
//
|
|
// Revision 1.37 2002/05/27 06:47:22 niceass
|
|
// removed some kamakazi stuff
|
|
//
|
|
// Revision 1.36 2002/05/23 18:37:50 makro
|
|
// Bots should crouch more often when they attack with a SSG
|
|
// Made this depend on skill. Also, elevator stuff
|
|
//
|
|
// Revision 1.35 2002/05/19 15:43:51 makro
|
|
// Bots now know about weapon modes. Just for grenades so far.
|
|
//
|
|
// Revision 1.34 2002/05/18 14:52:16 makro
|
|
// Bot stuff. Other stuff. Just... stuff :p
|
|
//
|
|
// Revision 1.33 2002/05/11 19:18:20 makro
|
|
// Sand surfaceparm
|
|
//
|
|
// Revision 1.32 2002/05/11 14:22:06 makro
|
|
// Func_statics now reset at the beginning of each round
|
|
//
|
|
// Revision 1.31 2002/05/11 12:45:25 makro
|
|
// Spectators can go through breakables and doors with
|
|
// a targetname or health. Bots should crouch more/jump less
|
|
// often when attacking at long range
|
|
//
|
|
// Revision 1.30 2002/05/10 13:21:53 makro
|
|
// Mainly bot stuff. Also fixed a couple of crash bugs
|
|
//
|
|
// Revision 1.29 2002/05/05 15:18:02 makro
|
|
// Fixed some crash bugs. Bot stuff. Triggerable func_statics.
|
|
// Made flags only spawn in CTF mode
|
|
//
|
|
// Revision 1.28 2002/05/04 16:13:04 makro
|
|
// Bots
|
|
//
|
|
// Revision 1.27 2002/05/04 01:21:47 makro
|
|
// Crash bug :/
|
|
//
|
|
// Revision 1.26 2002/05/04 01:03:42 makro
|
|
// Bots
|
|
//
|
|
// Revision 1.25 2002/05/03 18:09:19 makro
|
|
// Bot stuff. Jump kicks
|
|
//
|
|
// Revision 1.24 2002/05/02 23:05:25 makro
|
|
// Loading screen. Jump kicks. Bot stuff
|
|
//
|
|
// Revision 1.23 2002/05/02 12:44:58 makro
|
|
// Customizable color for the loading screen text. Bot stuff
|
|
//
|
|
// Revision 1.22 2002/05/02 00:12:22 makro
|
|
// Improved reloading and ammo handling for akimbo/hc
|
|
//
|
|
// Revision 1.21 2002/05/01 05:32:45 makro
|
|
// Bots reload akimbos/handcannons. Also, they can decide whether
|
|
// or not an item on the ground is better than theirs
|
|
//
|
|
// Revision 1.20 2002/04/30 12:14:53 makro
|
|
// Fixed a small warning
|
|
//
|
|
// Revision 1.19 2002/04/30 11:54:37 makro
|
|
// Bots rule ! Also, added clips to give all. Maybe some other things
|
|
//
|
|
// Revision 1.18 2002/04/20 15:03:47 makro
|
|
// More footstep sounds, a few other things
|
|
//
|
|
// Revision 1.17 2002/04/06 21:42:19 makro
|
|
// Changes to bot code. New surfaceparm system.
|
|
//
|
|
// Revision 1.16 2002/04/05 18:52:26 makro
|
|
// Cleaned things up a bit
|
|
//
|
|
// Revision 1.15 2002/04/04 18:06:44 makro
|
|
// Improved door code. Bots reply to radio treport from teammates.
|
|
// Improved reloading code.
|
|
//
|
|
// Revision 1.14 2002/04/03 17:46:14 makro
|
|
// Fixed one more thing
|
|
//
|
|
// Revision 1.13 2002/04/03 17:39:36 makro
|
|
// Made bots handle incoming radio spam better
|
|
//
|
|
// Revision 1.12 2002/04/03 16:33:20 makro
|
|
// Bots now respond to radio commands
|
|
//
|
|
// Revision 1.11 2002/04/01 12:45:54 makro
|
|
// Changed some weapon names
|
|
//
|
|
// Revision 1.9 2002/03/31 19:16:56 makro
|
|
// Bandaging, reloading, opening rotating doors (still needs a lot of work),
|
|
// shooting breakables
|
|
//
|
|
// Revision 1.8 2002/03/18 12:25:10 jbravo
|
|
// Live players dont get fraglines, except their own. Cleanups and some
|
|
// hacks to get bots to stop using knives only.
|
|
//
|
|
// Revision 1.7 2002/01/11 20:20:58 jbravo
|
|
// Adding TP to main branch
|
|
//
|
|
// Revision 1.6 2002/01/11 19:48:29 jbravo
|
|
// Formatted the source in non DOS format.
|
|
//
|
|
// Revision 1.5 2001/12/31 16:28:41 jbravo
|
|
// I made a Booboo with the Log tag.
|
|
//
|
|
//
|
|
//-----------------------------------------------------------------------------
|
|
// Copyright (C) 1999-2000 Id Software, Inc.
|
|
//
|
|
|
|
/*****************************************************************************
|
|
* name: ai_dmq3.c
|
|
*
|
|
* desc: Quake3 bot AI
|
|
*
|
|
* $Archive: /MissionPack/code/game/ai_dmq3.c $
|
|
* $Author$
|
|
* $Revision$
|
|
* $Modtime: 11/28/00 9:12a $
|
|
* $Date$
|
|
*
|
|
*****************************************************************************/
|
|
|
|
#include "g_local.h"
|
|
#include "../botlib/botlib.h"
|
|
#include "../botlib/be_aas.h"
|
|
#include "../botlib/be_ea.h"
|
|
#include "../botlib/be_ai_char.h"
|
|
#include "../botlib/be_ai_chat.h"
|
|
#include "../botlib/be_ai_gen.h"
|
|
#include "../botlib/be_ai_goal.h"
|
|
#include "../botlib/be_ai_move.h"
|
|
#include "../botlib/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
|
|
|
|
// for the voice chats
|
|
|
|
//Blaze: was there a extra ../ here?
|
|
#include "../ui/menudef.h"
|
|
|
|
//Makro - to get rid of the warnings
|
|
void Cmd_Bandage(gentity_t * ent);
|
|
void G_Say(gentity_t * ent, gentity_t * target, int mode, const char *chatText);
|
|
gentity_t *SelectRandomDeathmatchSpawnPoint(void);
|
|
void Cmd_New_Weapon(gentity_t * ent);
|
|
|
|
// from aasfile.h
|
|
#define AREACONTENTS_MOVER 1024
|
|
#define AREACONTENTS_MODELNUMSHIFT 24
|
|
#define AREACONTENTS_MAXMODELNUM 0xFF
|
|
#define AREACONTENTS_MODELNUM (AREACONTENTS_MAXMODELNUM << AREACONTENTS_MODELNUMSHIFT)
|
|
|
|
#define IDEAL_ATTACKDIST 140
|
|
|
|
#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;
|
|
vmCvar_t bot_predictobstacles;
|
|
vmCvar_t g_spSkill;
|
|
|
|
extern vmCvar_t bot_developer;
|
|
|
|
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;
|
|
|
|
#define MAX_ALTROUTEGOALS 32
|
|
|
|
int altroutegoals_setup;
|
|
aas_altroutegoal_t red_altroutegoals[MAX_ALTROUTEGOALS];
|
|
int red_numaltroutegoals;
|
|
aas_altroutegoal_t blue_altroutegoals[MAX_ALTROUTEGOALS];
|
|
int blue_numaltroutegoals;
|
|
|
|
//Makro - the vector located on the line from src to dest dist units away
|
|
void VectorTargetDist(vec3_t src, vec3_t dest, int dist, vec3_t final)
|
|
{
|
|
VectorSubtract(src, dest, final);
|
|
VectorNormalize(final);
|
|
VectorMA(src, dist, final, final);
|
|
//VectorScale(final, dist, final);
|
|
//VectorAdd(final, src, final);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
RQ3_Bot_ClipsForWeapon
|
|
|
|
Added by Makro
|
|
==================
|
|
*/
|
|
int RQ3_Bot_ClipsForWeapon(bot_state_t * bs, int weapon)
|
|
{
|
|
switch (weapon) {
|
|
case WP_PISTOL:
|
|
return bs->inventory[INVENTORY_PISTOLCLIP];
|
|
break;
|
|
case WP_AKIMBO:
|
|
return bs->inventory[INVENTORY_AKIMBOCLIP];
|
|
break;
|
|
case WP_M3:
|
|
return bs->inventory[INVENTORY_M3CLIP];
|
|
break;
|
|
case WP_HANDCANNON:
|
|
return bs->inventory[INVENTORY_HANDCANNONCLIP];
|
|
break;
|
|
case WP_MP5:
|
|
return bs->inventory[INVENTORY_MP5CLIP];
|
|
break;
|
|
case WP_M4:
|
|
return bs->inventory[INVENTORY_M4CLIP];
|
|
break;
|
|
case WP_SSG3000:
|
|
return bs->inventory[INVENTORY_SSG3000CLIP];
|
|
break;
|
|
default:
|
|
return 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
RQ3_Bot_CanReload
|
|
|
|
Added by Makro
|
|
==================
|
|
*/
|
|
qboolean RQ3_Bot_CanReload(bot_state_t * bs, int weapon)
|
|
{
|
|
int clips = RQ3_Bot_ClipsForWeapon(bs, weapon);
|
|
|
|
//If no clips or not a valid weapon
|
|
if (!clips || !(bs->cur_ps.stats[STAT_WEAPONS] & (1 << weapon)))
|
|
return qfalse;
|
|
|
|
if (weapon == WP_AKIMBO || weapon == WP_HANDCANNON)
|
|
return (clips >= 2);
|
|
else
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotAttack
|
|
|
|
Added by Makro
|
|
==================
|
|
*/
|
|
void BotAttack(bot_state_t * bs)
|
|
{
|
|
|
|
//Makro - if the weapon isn't ready, stop
|
|
if (bs->cur_ps.weaponstate != WEAPON_READY) {
|
|
return;
|
|
}
|
|
//If the gun is empty
|
|
if ((bs->cur_ps.ammo[bs->cur_ps.weapon]) == 0) {
|
|
//If the bot has extra ammo
|
|
if (RQ3_Bot_CanReload(bs, bs->cur_ps.weapon) >= 1) {
|
|
//Cmd_Reload( &g_entities[bs->entitynum] );
|
|
trap_EA_Action(bs->client, ACTION_AFFIRMATIVE);
|
|
#ifdef DEBUG
|
|
BotAI_Print(PRT_MESSAGE, "empty weapon (%i), reloading\n", bs->cur_ps.weapon);
|
|
#endif //DEBUG
|
|
}
|
|
} else {
|
|
//Gun is not empty
|
|
//trap_EA_Attack(bs->client);
|
|
trap_EA_Action(bs->client, ACTION_ATTACK);
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotMoveTo
|
|
|
|
Added by Makro
|
|
==================
|
|
*/
|
|
bot_moveresult_t BotMoveTo(bot_state_t * bs, vec3_t dest)
|
|
{
|
|
bot_goal_t goal;
|
|
bot_moveresult_t moveresult;
|
|
|
|
//create goal
|
|
goal.entitynum = 0;
|
|
VectorCopy(dest, goal.origin);
|
|
VectorSet(goal.mins, -8, -8, -8);
|
|
VectorSet(goal.maxs, 8, 8, 8);
|
|
//VectorAdd(goal.mins, goal.origin, goal.mins);
|
|
//VectorAdd(goal.maxs, goal.origin, goal.maxs);
|
|
goal.areanum = trap_AAS_PointAreaNum(goal.origin);
|
|
//initialize the movement state
|
|
BotSetupForMovement(bs);
|
|
//trap_BotMoveInDirection(bs->ms, dir, dist, MOVE_RUN);
|
|
//trap_BotPushGoal(bs->gs, &goal);
|
|
//move towards the goal
|
|
trap_BotMoveToGoal(&moveresult, bs->ms, &goal, bs->tfl);
|
|
//if movement failed
|
|
if (moveresult.failure) {
|
|
//G_Printf("BotMoveTo: moveresult.failure = 1\n");
|
|
//G_Printf("BotMoveTo: moveresult.blocked = %i\n", moveresult.blocked);
|
|
//G_Printf("BotMoveTo: moveresult.type = %i\n", moveresult.type);
|
|
//reset the avoid reach, otherwise bot is stuck in current area
|
|
trap_BotResetAvoidReach(bs->ms);
|
|
if (bs->activatestack != NULL)
|
|
bs->activatestack->time = 0;
|
|
}
|
|
//G_Printf("BotMoveTo: moveresult.flags = %i\n", moveresult.flags);
|
|
return moveresult;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotMoveTowardsEnt
|
|
|
|
Added by Makro
|
|
==================
|
|
*/
|
|
void BotMoveTowardsEnt(bot_state_t * bs, vec3_t dest, int dist)
|
|
{
|
|
vec3_t dir;
|
|
|
|
/*
|
|
VectorTargetDist(bs->origin, dest, dist, dir);
|
|
//dir[2] = bs->origin[2];
|
|
BotMoveTo(bs, dir);
|
|
*/
|
|
VectorSubtract(dest, bs->origin, dir);
|
|
VectorNormalize(dir);
|
|
BotSetupForMovement(bs);
|
|
trap_BotMoveInDirection(bs->ms, dir, 400, MOVE_WALK);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
RQ3_Bot_GetWeaponMode
|
|
|
|
Added by Makro
|
|
==================
|
|
*/
|
|
int RQ3_Bot_GetWeaponMode(bot_state_t * bs, int weapon)
|
|
{
|
|
switch (weapon) {
|
|
case WP_GRENADE:
|
|
{
|
|
//long range
|
|
if ((bs->cur_ps.persistant[PERS_WEAPONMODES] & RQ3_GRENMED) == RQ3_GRENMED &&
|
|
(bs->cur_ps.persistant[PERS_WEAPONMODES] & RQ3_GRENSHORT) == RQ3_GRENSHORT) {
|
|
return 2;
|
|
//medium range
|
|
} else if ((bs->cur_ps.persistant[PERS_WEAPONMODES] & RQ3_GRENMED) == RQ3_GRENMED) {
|
|
return 1;
|
|
//short range
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
case WP_SSG3000:
|
|
{
|
|
//6x
|
|
if ((bs->cur_ps.stats[STAT_RQ3] & RQ3_ZOOM_MED) == RQ3_ZOOM_MED &&
|
|
(bs->cur_ps.stats[STAT_RQ3] & RQ3_ZOOM_LOW) == RQ3_ZOOM_LOW) {
|
|
return 3;
|
|
//4x
|
|
} else if ((bs->cur_ps.stats[STAT_RQ3] & RQ3_ZOOM_MED) == RQ3_ZOOM_MED) {
|
|
return 2;
|
|
//2x
|
|
} else if ((bs->cur_ps.stats[STAT_RQ3] & RQ3_ZOOM_LOW) == RQ3_ZOOM_LOW) {
|
|
return 1;
|
|
//unzoomed
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
case WP_KNIFE:
|
|
{
|
|
if ((bs->cur_ps.persistant[PERS_WEAPONMODES] & RQ3_KNIFEMODE) == RQ3_KNIFEMODE) {
|
|
//slashing
|
|
return 0;
|
|
} else {
|
|
//throwing
|
|
return 1;
|
|
}
|
|
}
|
|
case WP_PISTOL:
|
|
{
|
|
if ((bs->cur_ps.persistant[PERS_WEAPONMODES] & RQ3_MK23MODE) == RQ3_MK23MODE) {
|
|
//3rb
|
|
return 1;
|
|
} else {
|
|
//full auto
|
|
return 0;
|
|
}
|
|
}
|
|
case WP_MP5:
|
|
{
|
|
if ((bs->cur_ps.persistant[PERS_WEAPONMODES] & RQ3_MP5MODE) == RQ3_MP5MODE) {
|
|
//3rb
|
|
return 1;
|
|
} else {
|
|
//normal
|
|
return 0;
|
|
}
|
|
}
|
|
case WP_M4:
|
|
{
|
|
if ((bs->cur_ps.persistant[PERS_WEAPONMODES] & RQ3_M4MODE) == RQ3_M4MODE) {
|
|
//3rb
|
|
return 1;
|
|
} else {
|
|
//normal
|
|
return 0;
|
|
}
|
|
}
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
RQ3_Bot_SetWeaponMode
|
|
|
|
Added by Makro
|
|
==================
|
|
*/
|
|
void RQ3_Bot_SetWeaponMode(bot_state_t * bs, int weapon, int mode)
|
|
{
|
|
//int i, modeCount, oldMode, press;
|
|
float reactionTime = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_REACTIONTIME, 0, 1);
|
|
|
|
//invalid weapon
|
|
if (weapon <= WP_NONE || weapon >= WP_NUM_WEAPONS)
|
|
return;
|
|
|
|
/* Makro - old code
|
|
//not holding the right weapon
|
|
if (weapon != bs->cur_ps.weapon)
|
|
return;
|
|
*/
|
|
|
|
//too soon ?
|
|
//TODO: array with weapon mode change times for individual weapons ?
|
|
if (FloatTime() < bs->weaponModeChange_time + 2 - reactionTime)
|
|
return;
|
|
|
|
/* Old code
|
|
switch (bs->cur_ps.weapon) {
|
|
case WP_PISTOL:
|
|
case WP_KNIFE:
|
|
modeCount = 2;
|
|
break;
|
|
case WP_MP5:
|
|
case WP_M4:
|
|
case WP_GRENADE:
|
|
modeCount = 3;
|
|
break;
|
|
case WP_SSG3000:
|
|
modeCount = 4;
|
|
break;
|
|
default:
|
|
modeCount = 1;
|
|
break;
|
|
}
|
|
|
|
//no modes available
|
|
if (modeCount <= 1)
|
|
return;
|
|
|
|
mode = mode % modeCount;
|
|
oldMode = RQ3_Bot_GetWeaponMode(bs, weapon) % modeCount;
|
|
press = mode - oldMode;
|
|
if (press < 0)
|
|
press += modeCount;
|
|
|
|
//use the weapon command "press" times
|
|
for (i = 0; i < press; i++) {
|
|
//changed it to use Slicer's new command
|
|
//Cmd_Weapon( &g_entities[bs->entitynum] );
|
|
Cmd_New_Weapon( &g_entities[bs->entitynum] );
|
|
}
|
|
*/
|
|
|
|
//new code
|
|
bs->weapMode[weapon] = mode;
|
|
|
|
bs->weaponModeChange_time = FloatTime();
|
|
}
|
|
|
|
/*
|
|
==================
|
|
RQ3_Bot_GetWeaponInfo
|
|
|
|
Added by Makro
|
|
==================
|
|
*/
|
|
//TODO: - set spreads here depending on what the player is doing - crouching, running etc. ?
|
|
qboolean RQ3_Bot_GetWeaponInfo(bot_state_t * bs, int weaponstate, int weapon, void *weaponinfo)
|
|
{
|
|
|
|
//if the weapon is not valid
|
|
if (weapon <= WP_NONE || weapon >= WP_NUM_WEAPONS) {
|
|
#ifdef DEBUG
|
|
BotAI_Print(PRT_MESSAGE, "Bot_GetWeaponInfo: weapon number out of range (%i)\n", weapon);
|
|
#else //DEBUG
|
|
if (trap_Cvar_VariableIntegerValue("developer")) {
|
|
BotAI_Print(PRT_ERROR, "Bot_GetWeaponInfo: weapon number out of range (%i)\n", weapon);
|
|
}
|
|
return qfalse;
|
|
#endif
|
|
} else {
|
|
weaponinfo_t *wi;
|
|
int weaponMode = 0;
|
|
|
|
trap_BotGetWeaponInfo(weaponstate, weapon, weaponinfo);
|
|
wi = (weaponinfo_t *) weaponinfo;
|
|
|
|
if (!wi)
|
|
return qfalse;
|
|
if (!wi->valid)
|
|
return qfalse;
|
|
|
|
weaponMode = RQ3_Bot_GetWeaponMode(bs, wi->number);
|
|
if (wi->number == WP_GRENADE) {
|
|
//long range
|
|
if (weaponMode == 2) {
|
|
wi->speed = GRENADE_LONG_SPEED;
|
|
//medium range
|
|
} else if (weaponMode == 1) {
|
|
wi->speed = GRENADE_MEDIUM_SPEED;
|
|
//short range
|
|
} else {
|
|
wi->speed = GRENADE_SHORT_SPEED;
|
|
}
|
|
}
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotSetUserInfo
|
|
==================
|
|
*/
|
|
void BotSetUserInfo(bot_state_t * bs, char *key, char *value)
|
|
{
|
|
char userinfo[MAX_INFO_STRING];
|
|
|
|
trap_GetUserinfo(bs->client, userinfo, sizeof(userinfo));
|
|
Info_SetValueForKey(userinfo, key, value);
|
|
trap_SetUserinfo(bs->client, userinfo);
|
|
ClientUserinfoChanged(bs->client);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotGetUserInfoKey
|
|
|
|
Added by Makro
|
|
==================
|
|
*/
|
|
char *BotGetUserInfoKey(bot_state_t * bs, char *key)
|
|
{
|
|
char userinfo[MAX_INFO_STRING];
|
|
|
|
trap_GetUserinfo(bs->client, userinfo, sizeof(userinfo));
|
|
return Info_ValueForKey(userinfo, key);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotTeam
|
|
==================
|
|
*/
|
|
int BotTeam(bot_state_t * bs)
|
|
{
|
|
char info[1024];
|
|
|
|
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 TEAM_RED;
|
|
else if (atoi(Info_ValueForKey(info, "t")) == TEAM_BLUE)
|
|
return TEAM_BLUE;
|
|
return TEAM_FREE;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotOppositeTeam
|
|
==================
|
|
*/
|
|
int BotOppositeTeam(bot_state_t * bs)
|
|
{
|
|
switch (BotTeam(bs)) {
|
|
case TEAM_RED:
|
|
return TEAM_BLUE;
|
|
case TEAM_BLUE:
|
|
return TEAM_RED;
|
|
default:
|
|
return TEAM_FREE;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotEnemyFlag
|
|
==================
|
|
*/
|
|
bot_goal_t *BotEnemyFlag(bot_state_t * bs)
|
|
{
|
|
if (BotTeam(bs) == TEAM_RED) {
|
|
return &ctf_blueflag;
|
|
} else {
|
|
return &ctf_redflag;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotTeamFlag
|
|
==================
|
|
*/
|
|
bot_goal_t *BotTeamFlag(bot_state_t * bs)
|
|
{
|
|
if (BotTeam(bs) == TEAM_RED) {
|
|
return &ctf_redflag;
|
|
} else {
|
|
return &ctf_blueflag;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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);
|
|
//Makro - added health check
|
|
if (ps.pm_type != PM_NORMAL || ps.stats[STAT_HEALTH] <= 0)
|
|
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;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
EntityIsInvisible
|
|
==================
|
|
*/
|
|
qboolean EntityIsInvisible(aas_entityinfo_t * entinfo)
|
|
{
|
|
// the flag is always visible
|
|
if (EntityCarriesFlag(entinfo)) {
|
|
return qfalse;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotRememberLastOrderedTask
|
|
==================
|
|
*/
|
|
void BotRememberLastOrderedTask(bot_state_t * bs)
|
|
{
|
|
if (!bs->ordered) {
|
|
return;
|
|
}
|
|
bs->lastgoal_decisionmaker = bs->decisionmaker;
|
|
bs->lastgoal_ltgtype = bs->ltgtype;
|
|
memcpy(&bs->lastgoal_teamgoal, &bs->teamgoal, sizeof(bot_goal_t));
|
|
bs->lastgoal_teammate = bs->teammate;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotSetTeamStatus
|
|
==================
|
|
*/
|
|
void BotSetTeamStatus(bot_state_t * bs)
|
|
{
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotSetLastOrderedTask
|
|
==================
|
|
*/
|
|
int BotSetLastOrderedTask(bot_state_t * bs)
|
|
{
|
|
|
|
if (gametype == GT_CTF) {
|
|
// don't go back to returning the flag if it's at the base
|
|
if (bs->lastgoal_ltgtype == LTG_RETURNFLAG) {
|
|
if (BotTeam(bs) == TEAM_RED) {
|
|
if (bs->redflagstatus == 0) {
|
|
bs->lastgoal_ltgtype = 0;
|
|
}
|
|
} else {
|
|
if (bs->blueflagstatus == 0) {
|
|
bs->lastgoal_ltgtype = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bs->lastgoal_ltgtype) {
|
|
bs->decisionmaker = bs->lastgoal_decisionmaker;
|
|
bs->ordered = qtrue;
|
|
bs->ltgtype = bs->lastgoal_ltgtype;
|
|
memcpy(&bs->teamgoal, &bs->lastgoal_teamgoal, sizeof(bot_goal_t));
|
|
bs->teammate = bs->lastgoal_teammate;
|
|
bs->teamgoal_time = FloatTime() + 300;
|
|
BotSetTeamStatus(bs);
|
|
//
|
|
if (gametype == GT_CTF) {
|
|
if (bs->ltgtype == LTG_GETFLAG) {
|
|
bot_goal_t *tb, *eb;
|
|
int tt, et;
|
|
|
|
tb = BotTeamFlag(bs);
|
|
eb = BotEnemyFlag(bs);
|
|
tt = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, tb->areanum,
|
|
TFL_DEFAULT);
|
|
et = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, eb->areanum,
|
|
TFL_DEFAULT);
|
|
// if the travel time towards the enemy base is larger than towards our base
|
|
if (et > tt) {
|
|
//get an alternative route goal towards the enemy base
|
|
BotGetAlternateRouteGoal(bs, BotOppositeTeam(bs));
|
|
}
|
|
}
|
|
}
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotRefuseOrder
|
|
==================
|
|
*/
|
|
void BotRefuseOrder(bot_state_t * bs)
|
|
{
|
|
if (!bs->ordered)
|
|
return;
|
|
// if the bot was ordered to do something
|
|
if (bs->order_time && bs->order_time > FloatTime() - 10) {
|
|
trap_EA_Action(bs->client, ACTION_NEGATIVE);
|
|
BotVoiceChat(bs, bs->decisionmaker, VOICECHAT_NO);
|
|
bs->order_time = 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotCTFSeekGoals
|
|
==================
|
|
*/
|
|
void BotCTFSeekGoals(bot_state_t * bs)
|
|
{
|
|
float rnd, l1, l2;
|
|
int flagstatus, c;
|
|
vec3_t dir;
|
|
aas_entityinfo_t entinfo;
|
|
|
|
//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) {
|
|
BotRefuseOrder(bs);
|
|
bs->ltgtype = LTG_RUSHBASE;
|
|
bs->teamgoal_time = FloatTime() + CTF_RUSHBASE_TIME;
|
|
bs->rushbaseaway_time = 0;
|
|
bs->decisionmaker = bs->client;
|
|
bs->ordered = qfalse;
|
|
//
|
|
switch (BotTeam(bs)) {
|
|
case TEAM_RED:
|
|
VectorSubtract(bs->origin, ctf_blueflag.origin, dir);
|
|
break;
|
|
case TEAM_BLUE:
|
|
VectorSubtract(bs->origin, ctf_redflag.origin, dir);
|
|
break;
|
|
default:
|
|
VectorSet(dir, 999, 999, 999);
|
|
break;
|
|
}
|
|
// if the bot picked up the flag very close to the enemy base
|
|
if (VectorLength(dir) < 128) {
|
|
// get an alternative route goal through the enemy base
|
|
BotGetAlternateRouteGoal(bs, BotOppositeTeam(bs));
|
|
} else {
|
|
// don't use any alt route goal, just get the hell out of the base
|
|
bs->altroutegoal.areanum = 0;
|
|
}
|
|
BotSetUserInfo(bs, "teamtask", va("%d", TEAMTASK_OFFENSE));
|
|
BotVoiceChat(bs, -1, VOICECHAT_IHAVEFLAG);
|
|
} else if (bs->rushbaseaway_time > FloatTime()) {
|
|
if (BotTeam(bs) == TEAM_RED)
|
|
flagstatus = bs->redflagstatus;
|
|
else
|
|
flagstatus = bs->blueflagstatus;
|
|
//if the flag is back
|
|
if (flagstatus == 0) {
|
|
bs->rushbaseaway_time = 0;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
// if the bot decided to follow someone
|
|
if (bs->ltgtype == LTG_TEAMACCOMPANY && !bs->ordered) {
|
|
// if the team mate being accompanied no longer carries the flag
|
|
BotEntityInfo(bs->teammate, &entinfo);
|
|
if (!EntityCarriesFlag(&entinfo)) {
|
|
bs->ltgtype = 0;
|
|
}
|
|
}
|
|
//
|
|
if (BotTeam(bs) == TEAM_RED)
|
|
flagstatus = bs->redflagstatus * 2 + bs->blueflagstatus;
|
|
else
|
|
flagstatus = bs->blueflagstatus * 2 + bs->redflagstatus;
|
|
//if our team has the enemy flag and our flag is at the base
|
|
if (flagstatus == 1) {
|
|
//
|
|
if (bs->owndecision_time < FloatTime()) {
|
|
//if Not defending the base already
|
|
if (!(bs->ltgtype == LTG_DEFENDKEYAREA &&
|
|
(bs->teamgoal.number == ctf_redflag.number ||
|
|
bs->teamgoal.number == ctf_blueflag.number))) {
|
|
//if there is avisible team mate flag carrier
|
|
c = BotTeamFlagCarrierVisible(bs);
|
|
if (c >= 0 &&
|
|
// and not already following the team mate flag carrier
|
|
(bs->ltgtype != LTG_TEAMACCOMPANY || bs->teammate != c)) {
|
|
//
|
|
BotRefuseOrder(bs);
|
|
//follow the flag carrier
|
|
bs->decisionmaker = bs->client;
|
|
bs->ordered = qfalse;
|
|
//the team mate
|
|
bs->teammate = c;
|
|
//last time the team mate was visible
|
|
bs->teammatevisible_time = FloatTime();
|
|
//no message
|
|
bs->teammessage_time = 0;
|
|
//no arrive message
|
|
bs->arrive_time = 1;
|
|
//
|
|
BotVoiceChat(bs, bs->teammate, VOICECHAT_ONFOLLOW);
|
|
//get the team goal time
|
|
bs->teamgoal_time = FloatTime() + TEAM_ACCOMPANY_TIME;
|
|
bs->ltgtype = LTG_TEAMACCOMPANY;
|
|
bs->formation_dist = 3.5 * 32; //3.5 meter
|
|
BotSetTeamStatus(bs);
|
|
bs->owndecision_time = FloatTime() + 5;
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
//if the enemy has our flag
|
|
else if (flagstatus == 2) {
|
|
//
|
|
if (bs->owndecision_time < FloatTime()) {
|
|
//if enemy flag carrier is visible
|
|
c = BotEnemyFlagCarrierVisible(bs);
|
|
if (c >= 0) {
|
|
//FIXME: fight enemy flag carrier
|
|
}
|
|
//if not already doing something important
|
|
if (bs->ltgtype != LTG_GETFLAG &&
|
|
bs->ltgtype != LTG_RETURNFLAG &&
|
|
bs->ltgtype != LTG_TEAMHELP &&
|
|
bs->ltgtype != LTG_TEAMACCOMPANY &&
|
|
bs->ltgtype != LTG_CAMPORDER && bs->ltgtype != LTG_PATROL && bs->ltgtype != LTG_GETITEM) {
|
|
|
|
BotRefuseOrder(bs);
|
|
bs->decisionmaker = bs->client;
|
|
bs->ordered = qfalse;
|
|
//
|
|
if (random() < 0.5) {
|
|
//go for the enemy flag
|
|
bs->ltgtype = LTG_GETFLAG;
|
|
} else {
|
|
bs->ltgtype = LTG_RETURNFLAG;
|
|
}
|
|
//no team message
|
|
bs->teammessage_time = 0;
|
|
//set the time the bot will stop getting the flag
|
|
bs->teamgoal_time = FloatTime() + CTF_GETFLAG_TIME;
|
|
//get an alternative route goal towards the enemy base
|
|
BotGetAlternateRouteGoal(bs, BotOppositeTeam(bs));
|
|
//
|
|
BotSetTeamStatus(bs);
|
|
bs->owndecision_time = FloatTime() + 5;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
//if both flags Not at their bases
|
|
else if (flagstatus == 3) {
|
|
//
|
|
if (bs->owndecision_time < FloatTime()) {
|
|
// if not trying to return the flag and not following the team flag carrier
|
|
if (bs->ltgtype != LTG_RETURNFLAG && bs->ltgtype != LTG_TEAMACCOMPANY) {
|
|
//
|
|
c = BotTeamFlagCarrierVisible(bs);
|
|
// if there is a visible team mate flag carrier
|
|
if (c >= 0) {
|
|
BotRefuseOrder(bs);
|
|
//follow the flag carrier
|
|
bs->decisionmaker = bs->client;
|
|
bs->ordered = qfalse;
|
|
//the team mate
|
|
bs->teammate = c;
|
|
//last time the team mate was visible
|
|
bs->teammatevisible_time = FloatTime();
|
|
//no message
|
|
bs->teammessage_time = 0;
|
|
//no arrive message
|
|
bs->arrive_time = 1;
|
|
//
|
|
BotVoiceChat(bs, bs->teammate, VOICECHAT_ONFOLLOW);
|
|
//get the team goal time
|
|
bs->teamgoal_time = FloatTime() + TEAM_ACCOMPANY_TIME;
|
|
bs->ltgtype = LTG_TEAMACCOMPANY;
|
|
bs->formation_dist = 3.5 * 32; //3.5 meter
|
|
//
|
|
BotSetTeamStatus(bs);
|
|
bs->owndecision_time = FloatTime() + 5;
|
|
} else {
|
|
BotRefuseOrder(bs);
|
|
bs->decisionmaker = bs->client;
|
|
bs->ordered = qfalse;
|
|
//get the enemy flag
|
|
bs->teammessage_time = FloatTime() + 2 * random();
|
|
//get the flag
|
|
bs->ltgtype = LTG_RETURNFLAG;
|
|
//set the time the bot will stop getting the flag
|
|
bs->teamgoal_time = FloatTime() + CTF_RETURNFLAG_TIME;
|
|
//get an alternative route goal towards the enemy base
|
|
BotGetAlternateRouteGoal(bs, BotOppositeTeam(bs));
|
|
//
|
|
BotSetTeamStatus(bs);
|
|
bs->owndecision_time = FloatTime() + 5;
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
// don't just do something wait for the bot team leader to give orders
|
|
if (BotTeamLeader(bs)) {
|
|
return;
|
|
}
|
|
// if the bot is ordered to do something
|
|
if (bs->lastgoal_ltgtype) {
|
|
bs->teamgoal_time += 60;
|
|
}
|
|
// if the bot decided to do something on it's own and has a last ordered goal
|
|
if (!bs->ordered && bs->lastgoal_ltgtype) {
|
|
bs->ltgtype = 0;
|
|
}
|
|
//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 ||
|
|
bs->ltgtype == LTG_GETITEM || bs->ltgtype == LTG_MAKELOVE_UNDER || bs->ltgtype == LTG_MAKELOVE_ONTOP) {
|
|
return;
|
|
}
|
|
//
|
|
if (BotSetLastOrderedTask(bs))
|
|
return;
|
|
//
|
|
if (bs->owndecision_time > FloatTime())
|
|
return;;
|
|
//if the bot is roaming
|
|
if (bs->ctfroam_time > FloatTime())
|
|
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 = FloatTime() + 2 * random();
|
|
//
|
|
if (bs->teamtaskpreference & (TEAMTP_ATTACKER | TEAMTP_DEFENDER)) {
|
|
if (bs->teamtaskpreference & TEAMTP_ATTACKER) {
|
|
l1 = 0.7f;
|
|
} else {
|
|
l1 = 0.2f;
|
|
}
|
|
l2 = 0.9f;
|
|
} else {
|
|
l1 = 0.4f;
|
|
l2 = 0.7f;
|
|
}
|
|
//get the flag or defend the base
|
|
rnd = random();
|
|
if (rnd < l1 && ctf_redflag.areanum && ctf_blueflag.areanum) {
|
|
bs->decisionmaker = bs->client;
|
|
bs->ordered = qfalse;
|
|
bs->ltgtype = LTG_GETFLAG;
|
|
//set the time the bot will stop getting the flag
|
|
bs->teamgoal_time = FloatTime() + CTF_GETFLAG_TIME;
|
|
//get an alternative route goal towards the enemy base
|
|
BotGetAlternateRouteGoal(bs, BotOppositeTeam(bs));
|
|
BotSetTeamStatus(bs);
|
|
} else if (rnd < l2 && ctf_redflag.areanum && ctf_blueflag.areanum) {
|
|
bs->decisionmaker = bs->client;
|
|
bs->ordered = qfalse;
|
|
//
|
|
if (BotTeam(bs) == 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 = FloatTime() + TEAM_DEFENDKEYAREA_TIME;
|
|
bs->defendaway_time = 0;
|
|
BotSetTeamStatus(bs);
|
|
} else {
|
|
bs->ltgtype = 0;
|
|
//set the time the bot will stop roaming
|
|
bs->ctfroam_time = FloatTime() + CTF_ROAM_TIME;
|
|
BotSetTeamStatus(bs);
|
|
}
|
|
bs->owndecision_time = FloatTime() + 5;
|
|
#ifdef DEBUG
|
|
BotPrintTeamGoal(bs);
|
|
#endif //DEBUG
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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) {
|
|
BotRefuseOrder(bs);
|
|
bs->ltgtype = LTG_RUSHBASE;
|
|
bs->teamgoal_time = FloatTime() + CTF_RUSHBASE_TIME;
|
|
bs->rushbaseaway_time = 0;
|
|
bs->decisionmaker = bs->client;
|
|
bs->ordered = qfalse;
|
|
BotSetTeamStatus(bs);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotRQ3TPSeekGoals
|
|
|
|
Added by Makro
|
|
==================
|
|
*/
|
|
void BotRQ3TPSeekGoals(bot_state_t * bs)
|
|
{
|
|
/*
|
|
int firstBot = -1, firstHuman = -1, leader = -1, i;
|
|
static int maxclients;
|
|
|
|
//if the bot already has a goal
|
|
if (bs->ltgtype || bs->ltg_time < FloatTime())
|
|
return;
|
|
|
|
if (!maxclients)
|
|
maxclients = trap_Cvar_VariableIntegerValue("sv_maxclients");
|
|
|
|
|
|
//find the first human/bot teammates
|
|
for ( i=0; i < MAX_CLIENTS && i < maxclients; i++ ) {
|
|
if ( !(g_entities[i].inuse) || !(g_entities[i].client) )
|
|
continue;
|
|
if (!BotSameTeam(bs, i))
|
|
continue;
|
|
if (g_entities[i].r.svFlags & SVF_BOT) {
|
|
if (i != bs->entitynum)
|
|
firstBot = i;
|
|
} else {
|
|
firstHuman = i;
|
|
}
|
|
if (firstHuman && firstBot)
|
|
break;
|
|
}
|
|
|
|
if (firstHuman != -1)
|
|
leader = firstHuman;
|
|
else if (firstBot != -1)
|
|
leader = firstBot;
|
|
else
|
|
return;
|
|
|
|
//the team mate
|
|
bs->teammate = leader;
|
|
bs->decisionmaker = bs->client;
|
|
bs->ordered = qfalse;
|
|
//no message
|
|
bs->teammessage_time = 0;
|
|
//no arrive message
|
|
//bs->arrive_time = 0;
|
|
//
|
|
bs->ctfroam_time = FloatTime() + CTF_ROAM_TIME;
|
|
BotVoiceChat(bs, bs->teammate, VOICECHAT_ONFOLLOW);
|
|
//get the team goal time
|
|
bs->teamgoal_time = FloatTime() + TEAM_ACCOMPANY_TIME;
|
|
bs->ltgtype = LTG_TEAMACCOMPANY;
|
|
bs->formation_dist = 3.5 * 32; //3.5 meter
|
|
BotSetTeamStatus(bs);
|
|
*/
|
|
return;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotTeamGoals
|
|
==================
|
|
*/
|
|
void BotTeamGoals(bot_state_t * bs, int retreat)
|
|
{
|
|
//Makro - unused
|
|
//bot_goal_t *goal;
|
|
if (retreat) {
|
|
if (gametype == GT_CTF) {
|
|
BotCTFRetreatGoals(bs);
|
|
}
|
|
} else {
|
|
if (gametype == GT_CTF) {
|
|
//decide what to do in CTF mode
|
|
BotCTFSeekGoals(bs);
|
|
}
|
|
//Makro - decide what to do in TP mode
|
|
else if (gametype == GT_TEAMPLAY) {
|
|
BotRQ3TPSeekGoals(bs);
|
|
}
|
|
}
|
|
// reset the order time which is used to see if
|
|
// we decided to refuse an order
|
|
bs->order_time = 0;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
ClientOnSameTeamFromName
|
|
==================
|
|
*/
|
|
int ClientOnSameTeamFromName(bot_state_t * bs, 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 (!BotSameTeam(bs, i))
|
|
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];
|
|
|
|
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;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotSynonymContext
|
|
==================
|
|
*/
|
|
int BotSynonymContext(bot_state_t * bs)
|
|
{
|
|
int context;
|
|
|
|
context = CONTEXT_NORMAL | CONTEXT_NEARBYITEM | CONTEXT_NAMES;
|
|
//
|
|
if (gametype == GT_CTF) {
|
|
if (BotTeam(bs) == TEAM_RED)
|
|
context |= CONTEXT_CTFREDTEAM;
|
|
else
|
|
context |= CONTEXT_CTFBLUETEAM;
|
|
}
|
|
return context;
|
|
}
|
|
|
|
/*
|
|
====================================
|
|
RQ3_Bot_WeaponFitness
|
|
|
|
Added by Makro
|
|
We have to take into account factors
|
|
that the trap calls don't seem to
|
|
====================================
|
|
*/
|
|
#define Score_M3_1 50
|
|
#define Score_M3_2 65
|
|
#define Score_M3_3 80
|
|
|
|
#define Score_HC_1 50
|
|
#define Score_HC_2 65
|
|
#define Score_HC_3 80
|
|
|
|
#define PLAYER_SIZE_X 30.0f
|
|
#define PLAYER_SIZE_Y 49.0f
|
|
|
|
float RQ3_Bot_WeaponFitness(bot_state_t * bs, int weapon)
|
|
{
|
|
int dist = sqrt(bs->inventory[ENEMY_HORIZONTAL_DIST] * bs->inventory[ENEMY_HORIZONTAL_DIST] +
|
|
bs->inventory[ENEMY_HEIGHT] * bs->inventory[ENEMY_HEIGHT]);
|
|
if (dist <= 0) {
|
|
dist = 8;
|
|
}
|
|
//no ammo
|
|
if (bs->cur_ps.ammo[weapon] <= 0 && !RQ3_Bot_CanReload(bs, weapon))
|
|
return 0;
|
|
|
|
//psx * psy / ((hs * d / 8192) * (vs * d / 8192))
|
|
switch (weapon) {
|
|
case WP_PISTOL:
|
|
return (float) PLAYER_SIZE_X *PLAYER_SIZE_Y / ((PISTOL_SPREAD * dist / 8192) *
|
|
(PISTOL_SPREAD * dist / 8192)) *
|
|
bs->inventory[INVENTORY_PISTOLAMMO] * PISTOL_DAMAGE;
|
|
case WP_AKIMBO:
|
|
return (float) PLAYER_SIZE_X *PLAYER_SIZE_Y / ((PISTOL_SPREAD * dist / 8192) *
|
|
(PISTOL_SPREAD * dist / 8192)) *
|
|
bs->inventory[INVENTORY_AKIMBOAMMO] * PISTOL_DAMAGE;
|
|
case WP_M3:
|
|
return (float) PLAYER_SIZE_X *PLAYER_SIZE_Y / ((DEFAULT_M3_HSPREAD * dist / 8192) *
|
|
(DEFAULT_M3_VSPREAD * dist / 8192)) *
|
|
bs->inventory[INVENTORY_M3AMMO] * M3_DAMAGE * 10;
|
|
case WP_HANDCANNON:
|
|
return (float) PLAYER_SIZE_X *PLAYER_SIZE_Y / ((DEFAULT_SHOTGUN_HSPREAD * dist / 8192) *
|
|
(DEFAULT_SHOTGUN_VSPREAD * dist / 8192)) *
|
|
bs->inventory[INVENTORY_HANDCANNONAMMO] * HANDCANNON_DAMAGE * 10;
|
|
case WP_M4:
|
|
return (float) PLAYER_SIZE_X *PLAYER_SIZE_Y / ((M4_SPREAD * dist / 8192) * (M4_SPREAD * dist / 8192)) *
|
|
bs->inventory[INVENTORY_M4AMMO] * M4_DAMAGE;
|
|
case WP_MP5:
|
|
return (float) PLAYER_SIZE_X *PLAYER_SIZE_Y / ((MP5_SPREAD * dist / 8192) *
|
|
(MP5_SPREAD * dist / 8192)) *
|
|
bs->inventory[INVENTORY_MP5AMMO] * MP5_DAMAGE;
|
|
case WP_SSG3000:
|
|
return (float) PLAYER_SIZE_X *PLAYER_SIZE_Y / ((SNIPER_SPREAD * dist / 8192) *
|
|
(SNIPER_SPREAD * dist / 8192)) *
|
|
bs->inventory[INVENTORY_SSG3000AMMO] * SNIPER_DAMAGE;
|
|
case WP_KNIFE:
|
|
return 0.1f;
|
|
case WP_GRENADE:
|
|
return 1.0f;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
======================
|
|
RQ3_Bot_ChooseBestFightWeapon
|
|
|
|
Added by Makro
|
|
======================
|
|
*/
|
|
int RQ3_Bot_ChooseBestFightWeapon(bot_state_t * bs)
|
|
{
|
|
/*
|
|
int i;
|
|
|
|
for (i=WP_NONE+1; i<WP_NUM_WEAPONS; i++) {
|
|
if ((bs->cur_ps.stats[STAT_WEAPONS] & (1 << i)) == (1 << i)) {
|
|
BotAI_Print(PRT_MESSAGE, "Weapon %i: %f\n", i, RQ3_Bot_WeaponFitness(bs, i));
|
|
}
|
|
}
|
|
*/
|
|
return trap_BotChooseBestFightWeapon(bs->ws, bs->inventory);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
RQ3_Bot_ChooseWeaponMode
|
|
|
|
Added by Makro
|
|
==================
|
|
*/
|
|
void RQ3_Bot_ChooseWeaponMode(bot_state_t * bs)
|
|
{
|
|
//distance from the enemy
|
|
int dist;
|
|
|
|
if (bs->enemy != -1) {
|
|
dist = sqrt(bs->inventory[ENEMY_HORIZONTAL_DIST] * bs->inventory[ENEMY_HORIZONTAL_DIST] +
|
|
bs->inventory[ENEMY_HEIGHT] * bs->inventory[ENEMY_HEIGHT]);
|
|
} else {
|
|
dist = 8;
|
|
}
|
|
|
|
if (dist <= 0) {
|
|
dist = 8;
|
|
}
|
|
|
|
if (bs->weaponnum == WP_GRENADE) {
|
|
//long range
|
|
if (dist > 800) {
|
|
RQ3_Bot_SetWeaponMode(bs, WP_GRENADE, 2);
|
|
//medium range
|
|
} else if (dist > 400) {
|
|
RQ3_Bot_SetWeaponMode(bs, WP_GRENADE, 1);
|
|
//short range
|
|
} else {
|
|
RQ3_Bot_SetWeaponMode(bs, WP_GRENADE, 0);
|
|
}
|
|
} else if (bs->weaponnum == WP_SSG3000) {
|
|
//2x
|
|
if (bs->enemy != -1 && dist > 600) {
|
|
RQ3_Bot_SetWeaponMode(bs, WP_SSG3000, 1);
|
|
//unzoomed
|
|
} else {
|
|
RQ3_Bot_SetWeaponMode(bs, WP_SSG3000, 0);
|
|
}
|
|
} else if (bs->weaponnum == WP_PISTOL) {
|
|
if (dist > 800) {
|
|
//semi-auto
|
|
RQ3_Bot_SetWeaponMode(bs, WP_PISTOL, 1);
|
|
} else {
|
|
//full auto
|
|
RQ3_Bot_SetWeaponMode(bs, WP_PISTOL, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotChooseWeapon
|
|
==================
|
|
*/
|
|
void BotChooseWeapon(bot_state_t * bs)
|
|
{
|
|
int newweaponnum;
|
|
|
|
//Makro - don't change weapons while bandaging
|
|
if (bs->cur_ps.weaponstate == WEAPON_BANDAGING) {
|
|
return;
|
|
}
|
|
|
|
// JBravo: Force the bot to use a pistol when he decides to go for a case.
|
|
if (bs->ltgtype == LTG_GETFLAG) {
|
|
bs->weaponnum = WP_PISTOL;
|
|
RQ3_Bot_ChooseWeaponMode(bs);
|
|
} else if (bs->cur_ps.weaponstate == WEAPON_RAISING || bs->cur_ps.weaponstate == WEAPON_DROPPING) {
|
|
trap_EA_SelectWeapon(bs->client, bs->weaponnum);
|
|
//Makro - choose weapon mode
|
|
RQ3_Bot_ChooseWeaponMode(bs);
|
|
} else {
|
|
//Makro - new function
|
|
//newweaponnum = trap_BotChooseBestFightWeapon(bs->ws, bs->inventory);
|
|
newweaponnum = RQ3_Bot_ChooseBestFightWeapon(bs);
|
|
if (bs->weaponnum != newweaponnum)
|
|
bs->weaponchange_time = FloatTime();
|
|
// JBravo: test hack
|
|
// Makro - test unhack :P
|
|
bs->weaponnum = newweaponnum;
|
|
//bs->weaponnum = WP_PISTOL;
|
|
//BotAI_Print(PRT_MESSAGE, "bs->weaponnum = %d\n", bs->weaponnum);
|
|
trap_EA_SelectWeapon(bs->client, bs->weaponnum);
|
|
//Makro - choose weapon mode
|
|
RQ3_Bot_ChooseWeaponMode(bs);
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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);
|
|
VectorClear(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);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotCheckItemPickup
|
|
==================
|
|
*/
|
|
void BotCheckItemPickup(bot_state_t * bs, int *oldinventory)
|
|
{
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotUpdateInventory
|
|
==================
|
|
*/
|
|
void BotUpdateInventory(bot_state_t * bs)
|
|
{
|
|
int oldinventory[MAX_ITEMS];
|
|
gentity_t *ent = &g_entities[bs->entitynum];
|
|
int amt = 0;
|
|
|
|
//DEBUG STUFF
|
|
//qboolean showInfo = (trap_Cvar_VariableIntegerValue("bot_RQ3_report") != 0);
|
|
|
|
memcpy(oldinventory, bs->inventory, sizeof(oldinventory));
|
|
//armor
|
|
bs->inventory[INVENTORY_ARMOR] = bs->cur_ps.stats[STAT_ARMOR];
|
|
|
|
// WEAPONS //
|
|
|
|
//Blaze: Reaction Weapons
|
|
bs->inventory[INVENTORY_KNIFE] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_KNIFE)) != 0;
|
|
bs->inventory[INVENTORY_PISTOL] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_PISTOL)) != 0;
|
|
bs->inventory[INVENTORY_M4] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_M4)) != 0;
|
|
bs->inventory[INVENTORY_SSG3000] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_SSG3000)) != 0;
|
|
bs->inventory[INVENTORY_MP5] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_MP5)) != 0;
|
|
bs->inventory[INVENTORY_HANDCANNON] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_HANDCANNON)) != 0;
|
|
bs->inventory[INVENTORY_M3] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_M3)) != 0;
|
|
bs->inventory[INVENTORY_AKIMBO] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_AKIMBO)) != 0;
|
|
bs->inventory[INVENTORY_GRENADE] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_GRENADE)) != 0;
|
|
|
|
// CLIPS //
|
|
|
|
//Makro - adding clip info
|
|
//Note - this stuff is also added to the ammo info, so bots know they still have ammo for their guns
|
|
bs->inventory[INVENTORY_KNIFECLIP] = ent->client->numClips[WP_KNIFE];
|
|
bs->inventory[INVENTORY_PISTOLCLIP] = ent->client->numClips[WP_PISTOL];
|
|
bs->inventory[INVENTORY_M3CLIP] = ent->client->numClips[WP_M3];
|
|
bs->inventory[INVENTORY_SSG3000CLIP] = ent->client->numClips[WP_SSG3000];
|
|
bs->inventory[INVENTORY_MP5CLIP] = ent->client->numClips[WP_MP5];
|
|
bs->inventory[INVENTORY_M4CLIP] = ent->client->numClips[WP_M4];
|
|
amt = ent->client->numClips[WP_HANDCANNON];
|
|
if (amt < 2)
|
|
amt = 0;
|
|
bs->inventory[INVENTORY_HANDCANNONCLIP] = amt;
|
|
amt = ent->client->numClips[WP_AKIMBO];
|
|
if (amt < 2)
|
|
amt = 0;
|
|
bs->inventory[INVENTORY_AKIMBOCLIP] = amt;
|
|
bs->inventory[INVENTORY_GRENADECLIP] = ent->client->numClips[WP_GRENADE];
|
|
|
|
// AMMO //
|
|
|
|
//Makro - clips should be taken into account
|
|
bs->inventory[INVENTORY_PISTOLAMMO] =
|
|
bs->cur_ps.ammo[WP_PISTOL] + bs->inventory[INVENTORY_PISTOLCLIP] * RQ3_PISTOL_CLIP;
|
|
bs->inventory[INVENTORY_KNIFEAMMO] = bs->cur_ps.ammo[WP_KNIFE];
|
|
bs->inventory[INVENTORY_M4AMMO] = bs->cur_ps.ammo[WP_M4] + bs->inventory[INVENTORY_M4CLIP] * RQ3_M4_CLIP;
|
|
bs->inventory[INVENTORY_SSG3000AMMO] = bs->cur_ps.ammo[WP_SSG3000] + bs->inventory[INVENTORY_SSG3000CLIP];
|
|
bs->inventory[INVENTORY_MP5AMMO] = bs->cur_ps.ammo[WP_MP5] + bs->inventory[INVENTORY_MP5CLIP] * RQ3_MP5_CLIP;
|
|
//Blaze: Same ammo for shotgun and handcannon
|
|
//Makro - this was odd
|
|
//bs->inventory[INVENTORY_M3AMMO] = bs->cur_ps.ammo[WP_HANDCANNON];
|
|
//bs->inventory[INVENTORY_M3AMMO] = bs->cur_ps.ammo[WP_M3];
|
|
bs->inventory[INVENTORY_M3AMMO] = bs->cur_ps.ammo[WP_M3] + bs->inventory[INVENTORY_M3CLIP];
|
|
amt = bs->cur_ps.ammo[WP_HANDCANNON] + bs->inventory[INVENTORY_HANDCANNONCLIP];
|
|
//Makro - hackish, but oh well... bots shouldn't want to use a HC when they only have one shell left
|
|
if (amt < 2)
|
|
amt = 0;
|
|
bs->inventory[INVENTORY_HANDCANNONAMMO] = amt;
|
|
//Blaze: Same ammo for Pistol and Akimbo Pistols
|
|
//bs->inventory[INVENTORY_PISTOLAMMO] = bs->cur_ps.ammo[WP_AKIMBO];
|
|
bs->inventory[INVENTORY_AKIMBOAMMO] =
|
|
bs->cur_ps.ammo[WP_AKIMBO] + bs->inventory[INVENTORY_AKIMBOCLIP] * RQ3_PISTOL_CLIP;
|
|
bs->inventory[INVENTORY_GRENADEAMMO] = bs->cur_ps.ammo[WP_GRENADE];
|
|
|
|
// bs->inventory[INVENTORY_BFGAMMO] = bs->cur_ps.ammo[WP_BFG];
|
|
//powerups
|
|
bs->inventory[INVENTORY_HEALTH] = bs->cur_ps.stats[STAT_HEALTH];
|
|
//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;
|
|
|
|
/*
|
|
if (showInfo) {
|
|
BotAI_Print(PRT_MESSAGE, "Inventory for %s :\n-----------------\n", ent->client->pers.netname);
|
|
BotAI_Print(PRT_MESSAGE, "KNIFE : %i / %i\n", bs->inventory[INVENTORY_KNIFE], bs->inventory[INVENTORY_KNIFEAMMO]);
|
|
BotAI_Print(PRT_MESSAGE, "PISTOL: %i / %i\n", bs->inventory[INVENTORY_PISTOL], bs->inventory[INVENTORY_PISTOLAMMO]);
|
|
BotAI_Print(PRT_MESSAGE, "AKIMBO: %i / %i\n", bs->inventory[INVENTORY_AKIMBO], bs->inventory[INVENTORY_AKIMBOAMMO]);
|
|
BotAI_Print(PRT_MESSAGE, "M3 : %i / %i\n", bs->inventory[INVENTORY_M3], bs->inventory[INVENTORY_M3AMMO]);
|
|
BotAI_Print(PRT_MESSAGE, "HC : %i / %i\n", bs->inventory[INVENTORY_HANDCANNON], bs->inventory[INVENTORY_HANDCANNONAMMO]);
|
|
BotAI_Print(PRT_MESSAGE, "MP5 : %i / %i\n", bs->inventory[INVENTORY_MP5], bs->inventory[INVENTORY_MP5AMMO]);
|
|
BotAI_Print(PRT_MESSAGE, "M4 : %i / %i\n", bs->inventory[INVENTORY_M4], bs->inventory[INVENTORY_M4AMMO]);
|
|
BotAI_Print(PRT_MESSAGE, "M26 G : %i / %i\n", bs->inventory[INVENTORY_GRENADE], bs->inventory[INVENTORY_GRENADEAMMO]);
|
|
trap_Cvar_Set("bot_RQ3_report", "0");
|
|
}
|
|
*/
|
|
BotCheckItemPickup(bs, oldinventory);
|
|
|
|
//Makro - new weapon mode switching code
|
|
if (RQ3_Bot_GetWeaponMode(bs, bs->cur_ps.weapon) != bs->weapMode[bs->cur_ps.weapon]) {
|
|
if (bs->weaponModeClick_time + 0.1 < FloatTime()) {
|
|
if (bs->cur_ps.weaponstate == WEAPON_READY) {
|
|
Cmd_New_Weapon(&g_entities[bs->entitynum]);
|
|
}
|
|
bs->weaponModeClick_time = FloatTime();
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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
|
|
}
|
|
|
|
/*
|
|
==================
|
|
RQ3_Bot_NeedToBandage
|
|
|
|
Added by Makro
|
|
==================
|
|
*/
|
|
int RQ3_Bot_NeedToBandage(bot_state_t * bs)
|
|
{
|
|
if ((bs->cur_ps.stats[STAT_RQ3] & RQ3_LEGDAMAGE) == RQ3_LEGDAMAGE)
|
|
return 2;
|
|
if ((bs->cur_ps.stats[STAT_RQ3] & RQ3_BANDAGE_NEED) == RQ3_BANDAGE_NEED)
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
RQ3_Bot_CheckBandage
|
|
|
|
Added by Makro
|
|
==================
|
|
*/
|
|
qboolean RQ3_Bot_CheckBandage(bot_state_t * bs)
|
|
{
|
|
qboolean doBandage = qfalse;
|
|
|
|
if ((bs->flags & BFL_FIGHTSUICIDAL) == BFL_FIGHTSUICIDAL)
|
|
return qfalse;
|
|
|
|
if (bs->inventory[INVENTORY_HEALTH] > 20)
|
|
doBandage = (random() > (float) bs->inventory[INVENTORY_HEALTH] / 100.0f);
|
|
else
|
|
doBandage = qtrue;
|
|
|
|
return doBandage;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
RQ3_Bot_RQ3_Bot_IdleActions
|
|
|
|
Added by Makro
|
|
==================
|
|
*/
|
|
void RQ3_Bot_IdleActions(bot_state_t * bs)
|
|
{
|
|
int damage = RQ3_Bot_NeedToBandage(bs);
|
|
int ammo = bs->cur_ps.ammo[bs->cur_ps.weapon];
|
|
int weapon = bs->cur_ps.weapon;
|
|
|
|
//float reactiontime = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_REACTIONTIME, 0, 1);
|
|
|
|
//always unzoom SSG when idle
|
|
if (RQ3_Bot_GetWeaponMode(bs, WP_SSG3000)) {
|
|
RQ3_Bot_SetWeaponMode(bs, WP_SSG3000, 0);
|
|
}
|
|
//too soon to reload/bandage again ?
|
|
if (bs->idleAction_time > FloatTime())
|
|
return;
|
|
|
|
//check if the bot needs to bandage
|
|
if (damage && RQ3_Bot_CheckBandage(bs)) {
|
|
if (bs->cur_ps.weaponstate != WEAPON_BANDAGING) {
|
|
Cmd_Bandage(&g_entities[bs->entitynum]);
|
|
bs->idleAction_time = FloatTime() + 4;
|
|
return;
|
|
}
|
|
}
|
|
//check if the bot needs to reload
|
|
if (ammo == 0 || (weapon == WP_SSG3000 && ammo < RQ3_SSG3000_AMMO) || (weapon == WP_M3 && ammo < RQ3_M3_AMMO)) {
|
|
if (RQ3_Bot_CanReload(bs, weapon)) {
|
|
trap_EA_Action(bs->client, ACTION_AFFIRMATIVE);
|
|
bs->idleAction_time = FloatTime() + 1;
|
|
return;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotBattleUseItems
|
|
==================
|
|
*/
|
|
void BotBattleUseItems(bot_state_t * bs)
|
|
{
|
|
//Makro - bot was hit; if very low on health, bandage immediately, otherwise, bandage randomly
|
|
/*
|
|
if ( bs->lastframe_health > bs->inventory[INVENTORY_HEALTH] ) {
|
|
if (RQ3_Bot_CheckBandage(bs))
|
|
//Makro - if not bandaging already
|
|
if (bs->cur_ps.weaponstate != WEAPON_BANDAGING) {
|
|
Cmd_Bandage( &g_entities[bs->entitynum] );
|
|
}
|
|
}
|
|
*/
|
|
//On second though, bots shouldn't bandage at all during combat
|
|
|
|
if (bs->inventory[INVENTORY_HEALTH] < 40) {
|
|
if (bs->inventory[INVENTORY_TELEPORTER] > 0) {
|
|
if (!BotCTFCarryingFlag(bs)) {
|
|
trap_EA_Use(bs->client);
|
|
}
|
|
}
|
|
}
|
|
if (bs->inventory[INVENTORY_HEALTH] < 60) {
|
|
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 = FloatTime();
|
|
}
|
|
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);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotAggression
|
|
==================
|
|
*/
|
|
float BotAggression(bot_state_t * bs)
|
|
{
|
|
//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
|
|
// Elder: ignore armor checks
|
|
/*
|
|
if (bs->inventory[INVENTORY_HEALTH] < 80) {
|
|
//if the bot has insufficient armor
|
|
if (bs->inventory[INVENTORY_ARMOR] < 40) return 0;
|
|
}
|
|
*/
|
|
//if the bot can use the Handcannon
|
|
if (bs->inventory[INVENTORY_HANDCANNON] > 0 && bs->inventory[INVENTORY_HANDCANNONAMMO] >= 2)
|
|
return 100;
|
|
//if the bot can use the Sniper
|
|
if (bs->inventory[INVENTORY_SSG3000] > 0 && bs->inventory[INVENTORY_SSG3000] > 3)
|
|
return 95;
|
|
//if the bot can use the MP5
|
|
if (bs->inventory[INVENTORY_MP5] > 0 && bs->inventory[INVENTORY_MP5AMMO] > 10)
|
|
return 90;
|
|
//if the bot can use the M4
|
|
if (bs->inventory[INVENTORY_M4] > 0 && bs->inventory[INVENTORY_M4AMMO] > 5)
|
|
return 90;
|
|
//if the bot can use the plasmagun
|
|
if (bs->inventory[INVENTORY_M3] > 0 && bs->inventory[INVENTORY_M3AMMO] > 4)
|
|
return 85;
|
|
//if the bot can use the grenade launcher
|
|
if (bs->inventory[INVENTORY_GRENADE] > 0 && bs->inventory[INVENTORY_GRENADE] > 2)
|
|
return 80;
|
|
// if the bot can use akimbos
|
|
if (bs->inventory[INVENTORY_AKIMBO] > 0 && bs->inventory[INVENTORY_AKIMBOAMMO] > 4)
|
|
return 80;
|
|
//if the bot can use the shotgun
|
|
//if (bs->inventory[INVENTORY_SHOTGUN] > 0 &&
|
|
// bs->inventory[INVENTORY_SHELLS] > 10) return 50;
|
|
//otherwise the bot is not feeling too good
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotFeelingBad
|
|
==================
|
|
*/
|
|
float BotFeelingBad(bot_state_t * bs)
|
|
{
|
|
if (bs->weaponnum == WP_KNIFE) {
|
|
return 100;
|
|
}
|
|
if (bs->inventory[INVENTORY_HEALTH] < 40) {
|
|
return 100;
|
|
}
|
|
if (bs->weaponnum == WP_PISTOL) {
|
|
return 90;
|
|
}
|
|
if (bs->inventory[INVENTORY_HEALTH] < 60) {
|
|
return 80;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotWantsToRetreat
|
|
==================
|
|
*/
|
|
int BotWantsToRetreat(bot_state_t * bs)
|
|
{
|
|
aas_entityinfo_t entinfo;
|
|
|
|
if (gametype == GT_CTF) {
|
|
//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;
|
|
|
|
if (gametype == GT_CTF) {
|
|
//never chase when carrying a CTF flag
|
|
if (BotCTFCarryingFlag(bs))
|
|
return qfalse;
|
|
//always chase 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)
|
|
{
|
|
return qfalse; //No Rocketjumping
|
|
/*
|
|
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;
|
|
*/
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotHasPersistantPowerupAndWeapon
|
|
==================
|
|
*/
|
|
int BotHasPersistantPowerupAndWeapon(bot_state_t * bs)
|
|
{
|
|
//if the bot is very low on health
|
|
if (bs->inventory[INVENTORY_HEALTH] < 60)
|
|
return qfalse;
|
|
//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 qfalse;
|
|
}
|
|
//if the bot can use the bfg
|
|
if (bs->inventory[INVENTORY_M4] > 0 && bs->inventory[INVENTORY_M4AMMO] > 7)
|
|
return qtrue;
|
|
//if the bot can use the railgun
|
|
if (bs->inventory[INVENTORY_SSG3000] > 0 && bs->inventory[INVENTORY_SSG3000] > 5)
|
|
return qtrue;
|
|
//if the bot can use the lightning gun
|
|
if (bs->inventory[INVENTORY_MP5] > 0 && bs->inventory[INVENTORY_MP5AMMO] > 50)
|
|
return qtrue;
|
|
//if the bot can use the rocketlauncher
|
|
if (bs->inventory[INVENTORY_HANDCANNON] > 0 && bs->inventory[INVENTORY_M3AMMO] > 5)
|
|
return qtrue;
|
|
//
|
|
/*if (bs->inventory[INVENTORY_NAILGUN] > 0 &&
|
|
bs->inventory[INVENTORY_NAILS] > 5) return qtrue;
|
|
//
|
|
if (bs->inventory[INVENTORY_PROXLAUNCHER] > 0 &&
|
|
bs->inventory[INVENTORY_MINES] > 5) return qtrue;
|
|
//
|
|
if (bs->inventory[INVENTORY_CHAINGUN] > 0 &&
|
|
bs->inventory[INVENTORY_BELT] > 40) return qtrue;
|
|
//if the bot can use the plasmagun
|
|
if (bs->inventory[INVENTORY_PLASMAGUN] > 0 &&
|
|
bs->inventory[INVENTORY_CELLS] > 20) return qtrue;
|
|
*/
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotGoCamp
|
|
==================
|
|
*/
|
|
void BotGoCamp(bot_state_t * bs, bot_goal_t * goal)
|
|
{
|
|
float camper;
|
|
|
|
bs->decisionmaker = bs->client;
|
|
//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 = FloatTime() + 99999;
|
|
else
|
|
bs->teamgoal_time = FloatTime() + 120 + 180 * camper + random() * 15;
|
|
//set the last time the bot started camping
|
|
bs->camp_time = FloatTime();
|
|
//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 > FloatTime() - 60 + 300 * (1 - camper))
|
|
return qfalse;
|
|
//
|
|
if (random() > camper) {
|
|
bs->camp_time = FloatTime();
|
|
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
|
|
// Elder: changed a few of the numbers
|
|
//Makro - fixed a typo - bs->inventory[INVENTORY_M3AMMO < 5]
|
|
if ((bs->inventory[INVENTORY_HANDCANNON] <= 0 || bs->inventory[INVENTORY_M3AMMO] < 5) &&
|
|
(bs->inventory[INVENTORY_M4] <= 0 || bs->inventory[INVENTORY_M4AMMO] < 10) &&
|
|
(bs->inventory[INVENTORY_SSG3000] <= 0 || bs->inventory[INVENTORY_SSG3000AMMO] < 6)) {
|
|
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, TFL_DEFAULT);
|
|
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);
|
|
bs->ordered = qfalse;
|
|
//
|
|
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
|
|
//Makro - replaced with Q3 items
|
|
/*
|
|
BotDontAvoid(bs, "Quad Damage");
|
|
BotDontAvoid(bs, "Regeneration");
|
|
BotDontAvoid(bs, "Battle Suit");
|
|
BotDontAvoid(bs, "Speed");
|
|
BotDontAvoid(bs, "Invisibility");
|
|
*/
|
|
BotDontAvoid(bs, RQ3_KEVLAR_NAME);
|
|
BotDontAvoid(bs, RQ3_LASER_NAME);
|
|
BotDontAvoid(bs, RQ3_BANDOLIER_NAME);
|
|
BotDontAvoid(bs, RQ3_SLIPPERS_NAME);
|
|
BotDontAvoid(bs, RQ3_SILENCER_NAME);
|
|
//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
|
|
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, attackentity;
|
|
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;
|
|
|
|
attackentity = bs->enemy;
|
|
//
|
|
if (bs->attackchase_time > FloatTime()) {
|
|
//create the chase goal
|
|
goal.entitynum = attackentity;
|
|
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 = 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;
|
|
//initialize the movement state
|
|
BotSetupForMovement(bs);
|
|
//get the enemy entity info
|
|
BotEntityInfo(attackentity, &entinfo);
|
|
//direction towards the enemy
|
|
VectorSubtract(entinfo.origin, bs->origin, forward);
|
|
//the distance towards the enemy
|
|
dist = VectorNormalize(forward);
|
|
//Makro - for long range attacks the bots should crouch more often
|
|
// same for sniper now; also added skill as a factor
|
|
if (dist > 512 || (dist > 128 && bs->cur_ps.weapon == WP_SSG3000)) {
|
|
croucher = Com_Clamp(0.1f, 1, croucher * (attack_skill + 1));
|
|
jumper = Com_Clamp(0, 1, jumper / (attack_skill + 1));
|
|
}
|
|
VectorNegate(forward, backward);
|
|
//walk, crouch or jump
|
|
movetype = MOVE_WALK;
|
|
//
|
|
if (bs->attackcrouch_time < FloatTime() - 1) {
|
|
if (random() < jumper) {
|
|
movetype = MOVE_JUMP;
|
|
}
|
|
//wait at least one second before crouching again
|
|
else if (bs->attackcrouch_time < FloatTime() - 1 && random() < croucher) {
|
|
bs->attackcrouch_time = FloatTime() + croucher * 5;
|
|
}
|
|
}
|
|
if (bs->attackcrouch_time > FloatTime())
|
|
movetype = MOVE_CROUCH;
|
|
//if the bot should jump
|
|
if (movetype == MOVE_JUMP) {
|
|
//if jumped last frame
|
|
if (bs->attackjump_time > FloatTime()) {
|
|
movetype = MOVE_WALK;
|
|
} else {
|
|
bs->attackjump_time = FloatTime() + 1;
|
|
}
|
|
}
|
|
if (bs->cur_ps.weapon == WP_KNIFE) {
|
|
attack_dist = 0;
|
|
attack_range = 0;
|
|
} else {
|
|
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;
|
|
}
|
|
//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];
|
|
int team1, team2;
|
|
|
|
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) {
|
|
//Makro - added teamplay code; changed some stuff
|
|
team1 = g_entities[bs->client].client->sess.savedTeam;
|
|
team2 = g_entities[entnum].client->sess.savedTeam;
|
|
if (team1 == TEAM_SPECTATOR || team2 == TEAM_SPECTATOR)
|
|
return qfalse;
|
|
if (team1 == team2)
|
|
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 squaredfogdist, 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_FOG);
|
|
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);
|
|
squaredfogdist = VectorLengthSquared(dir);
|
|
} else if (infog) {
|
|
VectorCopy(trace.endpos, start);
|
|
BotAI_Trace(&trace, start, NULL, NULL, eye, viewer, CONTENTS_FOG);
|
|
VectorSubtract(eye, trace.endpos, dir);
|
|
squaredfogdist = VectorLengthSquared(dir);
|
|
} else if (otherinfog) {
|
|
VectorCopy(trace.endpos, end);
|
|
BotAI_Trace(&trace, eye, NULL, NULL, end, viewer, CONTENTS_FOG);
|
|
VectorSubtract(end, trace.endpos, dir);
|
|
squaredfogdist = VectorLengthSquared(dir);
|
|
} else {
|
|
//if the entity and the viewer are not in fog assume there's no fog in between
|
|
squaredfogdist = 0;
|
|
}
|
|
//decrease visibility with the view distance through fog
|
|
vis = 1 / ((squaredfogdist * 0.001) < 1 ? 1 : (squaredfogdist * 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, alertness, easyfragger, vis;
|
|
float squaredist, cursquaredist;
|
|
aas_entityinfo_t entinfo, curenemyinfo;
|
|
vec3_t dir, angles;
|
|
|
|
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 (EntityCarriesFlag(&curenemyinfo))
|
|
return qfalse;
|
|
VectorSubtract(curenemyinfo.origin, bs->origin, dir);
|
|
cursquaredist = VectorLengthSquared(dir);
|
|
} else {
|
|
cursquaredist = 0;
|
|
}
|
|
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 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;
|
|
}
|
|
// JBravo: unlagged
|
|
if (g_entities[i].flags & FL_NOTARGET) {
|
|
continue;
|
|
}
|
|
|
|
//if not an easy fragger don't shoot at chatting players
|
|
if (easyfragger < 0.5 && EntityIsChatting(&entinfo))
|
|
continue;
|
|
//
|
|
if (lastteleport_time > FloatTime() - 3) {
|
|
VectorSubtract(entinfo.origin, lastteleport_origin, dir);
|
|
if (VectorLengthSquared(dir) < Square(70))
|
|
continue;
|
|
}
|
|
//calculate the distance towards the enemy
|
|
VectorSubtract(entinfo.origin, bs->origin, dir);
|
|
squaredist = VectorLengthSquared(dir);
|
|
//if this entity is not carrying a flag
|
|
if (!EntityCarriesFlag(&entinfo)) {
|
|
//if this enemy is further away than the current one
|
|
if (curenemy >= 0 && squaredist > cursquaredist)
|
|
continue;
|
|
} //end if
|
|
//if the bot has no
|
|
if (squaredist > Square(900.0 + alertness * 4000.0))
|
|
continue;
|
|
//if on the same team
|
|
if (BotSameTeam(bs, i))
|
|
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 - (squaredist > Square(810) ? Square(810) : squaredist) / (810 * 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 && squaredist > Square(100) && !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, 90, 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 = FloatTime() - 2;
|
|
else
|
|
bs->enemysight_time = FloatTime();
|
|
bs->enemysuicide = qfalse;
|
|
bs->enemydeath_time = 0;
|
|
bs->enemyvisible_time = FloatTime();
|
|
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;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotTeamFlagCarrier
|
|
==================
|
|
*/
|
|
int BotTeamFlagCarrier(bot_state_t * bs)
|
|
{
|
|
int i;
|
|
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;
|
|
//
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotEnemyFlagCarrierVisible
|
|
==================
|
|
*/
|
|
int BotEnemyFlagCarrierVisible(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 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;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotVisibleTeamMatesAndEnemies
|
|
==================
|
|
*/
|
|
void BotVisibleTeamMatesAndEnemies(bot_state_t * bs, int *teammates, int *enemies, float range)
|
|
{
|
|
int i;
|
|
float vis;
|
|
aas_entityinfo_t entinfo;
|
|
vec3_t dir;
|
|
|
|
if (teammates)
|
|
*teammates = 0;
|
|
if (enemies)
|
|
*enemies = 0;
|
|
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 not within range
|
|
VectorSubtract(entinfo.origin, bs->origin, dir);
|
|
if (VectorLengthSquared(dir) > Square(range))
|
|
continue;
|
|
//if the flag carrier is not visible
|
|
vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, i);
|
|
if (vis <= 0)
|
|
continue;
|
|
//if the flag carrier is on the same team
|
|
if (BotSameTeam(bs, i)) {
|
|
if (teammates)
|
|
(*teammates)++;
|
|
} else {
|
|
if (enemies)
|
|
(*enemies)++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotAimAtEnemy
|
|
==================
|
|
*/
|
|
void BotAimAtEnemy(bot_state_t * bs)
|
|
{
|
|
int 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;
|
|
}
|
|
//get the enemy entity information
|
|
BotEntityInfo(bs->enemy, &entinfo);
|
|
//if this is not a player (should be an obelisk)
|
|
if (bs->enemy >= MAX_CLIENTS) {
|
|
//if the obelisk is visible
|
|
VectorCopy(entinfo.origin, target);
|
|
//aim at the obelisk
|
|
VectorSubtract(target, bs->eye, dir);
|
|
vectoangles(dir, bs->ideal_viewangles);
|
|
//set the aim target before trying to attack
|
|
VectorCopy(target, bs->aimtarget);
|
|
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 > FloatTime() - reactiontime)
|
|
return;
|
|
if (bs->teleport_time > FloatTime() - reactiontime)
|
|
return;
|
|
}
|
|
//get the weapon information
|
|
//trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi);
|
|
//Makro - new function; if weapon/weapon info not ok, stop
|
|
if (!RQ3_Bot_GetWeaponInfo(bs, bs->ws, bs->weaponnum, &wi))
|
|
return;
|
|
|
|
//get the weapon specific aim accuracy and or aim skill
|
|
//Blaze: just gonna set the Characteristic aim to machinegun for all of these, but I am still doing the if's so we can edit it later for bot support
|
|
//Blaze: Reaction Pistol
|
|
if (wi.number == WP_PISTOL) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_MACHINEGUN, 0, 1);
|
|
}
|
|
//Blaze: Reaction Knife
|
|
if (wi.number == WP_KNIFE) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_MACHINEGUN, 0, 1);
|
|
}
|
|
//Blaze: Reaction M4
|
|
if (wi.number == WP_M4) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_MACHINEGUN, 0, 1);
|
|
}
|
|
//Blaze: Reaction SSG3000
|
|
if (wi.number == WP_SSG3000) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_MACHINEGUN, 0, 1);
|
|
}
|
|
//Blaze: Reaction MP5
|
|
if (wi.number == WP_MP5) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_MACHINEGUN, 0, 1);
|
|
}
|
|
//Blaze: Reaction HandCannon
|
|
//Makro - using shotgun accuracy for now
|
|
if (wi.number == WP_HANDCANNON) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_SHOTGUN, 0, 1);
|
|
}
|
|
//Blaze: Reaction Shotgun
|
|
//Makro - using shotgun accuracy for now
|
|
if (wi.number == WP_M3) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_SHOTGUN, 0, 1);
|
|
}
|
|
//Blaze: Reaction Akimbo
|
|
if (wi.number == WP_AKIMBO) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_MACHINEGUN, 0, 1);
|
|
}
|
|
//Blaze: Reaction Grenade
|
|
//Makro - changed to grenade launcher accuracy
|
|
if (wi.number == WP_GRENADE) {
|
|
aim_accuracy =
|
|
trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_GRENADELAUNCHER, 0, 1);
|
|
}
|
|
//
|
|
if (aim_accuracy <= 0)
|
|
aim_accuracy = 0.0001f;
|
|
//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.4f;
|
|
}
|
|
//
|
|
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 < FloatTime()) {
|
|
//
|
|
bs->enemyposition_time = FloatTime() + 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 (VectorLengthSquared(dir) > Square(48)) {
|
|
//if the enemy changed direction
|
|
if (DotProduct(bs->enemyvelocity, enemyvelocity) < 0) {
|
|
//aim accuracy should be worse now
|
|
aim_accuracy *= 0.7f;
|
|
}
|
|
}
|
|
}
|
|
//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 && VectorLengthSquared(dir) < Square(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.1f, 0, 0, qfalse);
|
|
VectorCopy(move.endpos, bestorigin);
|
|
//BotAI_Print(PRT_MESSAGE, "%1.1f predicted speed = %f, frames = %f\n", FloatTime(), 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 (VectorLengthSquared(dir) < Square(60)) {
|
|
VectorSubtract(trace.endpos, start, dir);
|
|
//if the hitpoint is far anough from the bot
|
|
if (VectorLengthSquared(dir) > Square(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
|
|
//Blaze: Knifes are the only thing bots would need to do predictions shots with, but will need lots of tweaking
|
|
if (wi.number == WP_KNIFE) {
|
|
//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 (VectorLengthSquared(dir) > Square(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);
|
|
//
|
|
//Blaze: dont wanna have non hitscan weapons here I think like the knife or Grenades
|
|
if (wi.number == WP_PISTOL ||
|
|
wi.number == WP_M4 ||
|
|
wi.number == WP_SSG3000 ||
|
|
wi.number == WP_MP5 || wi.number == WP_HANDCANNON || wi.number == WP_M3 || wi.number == WP_AKIMBO) {
|
|
//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
|
|
//Makro - bots look even more stupid than they are with this
|
|
//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 (bot_challenge.integer) {
|
|
//if the bot is really accurate and has the enemy in view for some time
|
|
if (aim_accuracy > 0.9 && bs->enemysight_time < FloatTime() - 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;
|
|
int attackentity;
|
|
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};
|
|
|
|
attackentity = bs->enemy;
|
|
//
|
|
BotEntityInfo(attackentity, &entinfo);
|
|
// if not attacking a player
|
|
if (attackentity >= MAX_CLIENTS) {
|
|
}
|
|
//
|
|
//Makro - we need the weapon info sooner
|
|
//get the weapon info
|
|
//trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi);
|
|
//Makro - new function; if weapon/weapon info not ok, stop
|
|
if (!RQ3_Bot_GetWeaponInfo(bs, bs->ws, bs->weaponnum, &wi))
|
|
return;
|
|
reactiontime = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_REACTIONTIME, 0, 1);
|
|
if (bs->enemysight_time > FloatTime() - reactiontime)
|
|
return;
|
|
if (bs->teleport_time > FloatTime() - reactiontime)
|
|
return;
|
|
//if changing weapons
|
|
//Makro - changed from 0.1 to an expression that takes into account weapon info
|
|
if (bs->weaponchange_time > FloatTime() - 0.1 - wi.activate)
|
|
return;
|
|
//check fire throttle characteristic
|
|
if (bs->firethrottlewait_time > FloatTime())
|
|
return;
|
|
firethrottle = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_FIRETHROTTLE, 0, 1);
|
|
if (bs->firethrottleshoot_time < FloatTime()) {
|
|
if (random() > firethrottle) {
|
|
bs->firethrottlewait_time = FloatTime() + firethrottle;
|
|
bs->firethrottleshoot_time = 0;
|
|
} else {
|
|
bs->firethrottleshoot_time = FloatTime() + 1 - firethrottle;
|
|
bs->firethrottlewait_time = 0;
|
|
}
|
|
}
|
|
//
|
|
//
|
|
VectorSubtract(bs->aimtarget, bs->eye, dir);
|
|
//
|
|
if (bs->weaponnum == WP_KNIFE) {
|
|
if (VectorLengthSquared(dir) > Square(60)) {
|
|
return;
|
|
}
|
|
}
|
|
if (VectorLengthSquared(dir) < Square(100))
|
|
fov = 120;
|
|
else
|
|
fov = 50;
|
|
//
|
|
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 != attackentity)
|
|
return;
|
|
|
|
//Makro - get weapon info sooner; was here before
|
|
//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 the entity is a client
|
|
if (trace.ent > 0 && trace.ent <= MAX_CLIENTS) {
|
|
if (trace.ent != attackentity) {
|
|
//if a teammate is hit
|
|
if (BotSameTeam(bs, trace.ent))
|
|
return;
|
|
}
|
|
}
|
|
//if won't hit the enemy or not attacking a player (obelisk)
|
|
if (trace.ent != attackentity || attackentity >= MAX_CLIENTS) {
|
|
//if the projectile does 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) {
|
|
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) {
|
|
//Makro - using custom function to allow in-combat reloads
|
|
//trap_EA_Attack(bs->client);
|
|
BotAttack(bs);
|
|
}
|
|
} else {
|
|
//Makro - using custom function to allow in-combat reloads
|
|
//trap_EA_Attack(bs->client);
|
|
BotAttack(bs);
|
|
}
|
|
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 = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY, 0, 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)) {
|
|
//Makro - using custom function to allow in-combat reloads
|
|
//trap_EA_Attack(bs->client);
|
|
BotAttack(bs);
|
|
}
|
|
}
|
|
} else if (!Q_stricmp(mapname, "mpq3tourney6")) {
|
|
//NOTE: NEVER use the func_bobbing in mpq3tourney6
|
|
bs->tfl &= ~TFL_FUNCBOB;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotSetMovedir
|
|
==================
|
|
*/
|
|
// bk001205 - made these static
|
|
static vec3_t VEC_UP = { 0, -1, 0 };
|
|
static vec3_t MOVEDIR_UP = { 0, 0, 1 };
|
|
static vec3_t VEC_DOWN = { 0, -2, 0 };
|
|
static 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
|
|
==================
|
|
*/
|
|
int BotModelMinsMaxs(int modelindex, int eType, int contents, vec3_t mins, vec3_t maxs)
|
|
{
|
|
gentity_t *ent;
|
|
int i;
|
|
|
|
//Makro - started from 0
|
|
//ent = &g_entities[0];
|
|
ent = &g_entities[MAX_CLIENTS];
|
|
for (i = MAX_CLIENTS; i < level.num_entities; i++, ent++) {
|
|
if (!ent->inuse) {
|
|
continue;
|
|
}
|
|
if (eType && ent->s.eType != eType) {
|
|
continue;
|
|
}
|
|
if (contents && ent->r.contents != contents) {
|
|
continue;
|
|
}
|
|
if (ent->s.modelindex == modelindex) {
|
|
if (mins)
|
|
VectorAdd(ent->r.currentOrigin, ent->r.mins, mins);
|
|
if (maxs)
|
|
VectorAdd(ent->r.currentOrigin, ent->r.maxs, maxs);
|
|
return i;
|
|
}
|
|
}
|
|
if (mins)
|
|
VectorClear(mins);
|
|
if (maxs)
|
|
VectorClear(maxs);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotFuncButtonGoal
|
|
==================
|
|
*/
|
|
int BotFuncButtonActivateGoal(bot_state_t * bs, int bspent, bot_activategoal_t * activategoal)
|
|
{
|
|
int i, areas[10], numareas, modelindex, entitynum;
|
|
char model[128];
|
|
float lip, dist, health, angle;
|
|
vec3_t size, start, end, mins, maxs, angles, points[10];
|
|
vec3_t movedir, origin, goalorigin, bboxmins, bboxmaxs;
|
|
vec3_t extramins = { 1, 1, 1 }, extramaxs = {
|
|
-1, -1, -1};
|
|
bsp_trace_t bsptrace;
|
|
|
|
activategoal->shoot = qfalse;
|
|
VectorClear(activategoal->target);
|
|
//create a bot goal towards the button
|
|
trap_AAS_ValueForBSPEpairKey(bspent, "model", model, sizeof(model));
|
|
if (!*model)
|
|
return qfalse;
|
|
modelindex = atoi(model + 1);
|
|
if (!modelindex)
|
|
return qfalse;
|
|
VectorClear(angles);
|
|
entitynum = BotModelMinsMaxs(modelindex, ET_MOVER, 0, mins, maxs);
|
|
//get the lip of the button
|
|
trap_AAS_FloatForBSPEpairKey(bspent, "lip", &lip);
|
|
if (!lip)
|
|
lip = 4;
|
|
//get the move direction from the angle
|
|
trap_AAS_FloatForBSPEpairKey(bspent, "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(bspent, "health", &health);
|
|
//if the button is shootable
|
|
if (health) {
|
|
//calculate the shoot target
|
|
VectorMA(origin, -dist, movedir, goalorigin);
|
|
//
|
|
VectorCopy(goalorigin, activategoal->target);
|
|
activategoal->shoot = qtrue;
|
|
//
|
|
BotAI_Trace(&bsptrace, bs->eye, NULL, NULL, goalorigin, bs->entitynum, MASK_SHOT);
|
|
// if the button is visible from the current position
|
|
if (bsptrace.fraction >= 1.0 || bsptrace.ent == entitynum) {
|
|
//
|
|
activategoal->goal.entitynum = entitynum; //NOTE: this is the entity number of the shootable button
|
|
activategoal->goal.number = 0;
|
|
activategoal->goal.flags = 0;
|
|
VectorCopy(bs->origin, activategoal->goal.origin);
|
|
activategoal->goal.areanum = bs->areanum;
|
|
VectorSet(activategoal->goal.mins, -8, -8, -8);
|
|
VectorSet(activategoal->goal.maxs, 8, 8, 8);
|
|
//
|
|
return qtrue;
|
|
} else {
|
|
//create a goal from where the button is visible and shoot at the button from there
|
|
//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] -= 512;
|
|
numareas = trap_AAS_TraceAreas(start, end, areas, points, 10);
|
|
//
|
|
for (i = numareas - 1; i >= 0; i--) {
|
|
if (trap_AAS_AreaReachability(areas[i])) {
|
|
break;
|
|
}
|
|
}
|
|
if (i < 0) {
|
|
// FIXME: trace forward and maybe in other directions to find a valid area
|
|
}
|
|
if (i >= 0) {
|
|
//
|
|
VectorCopy(points[i], activategoal->goal.origin);
|
|
activategoal->goal.areanum = areas[i];
|
|
VectorSet(activategoal->goal.mins, 8, 8, 8);
|
|
VectorSet(activategoal->goal.maxs, -8, -8, -8);
|
|
//
|
|
for (i = 0; i < 3; i++) {
|
|
if (movedir[i] < 0)
|
|
activategoal->goal.maxs[i] += fabs(movedir[i]) * fabs(extramaxs[i]);
|
|
else
|
|
activategoal->goal.mins[i] += fabs(movedir[i]) * fabs(extramins[i]);
|
|
} //end for
|
|
//
|
|
activategoal->goal.entitynum = entitynum;
|
|
activategoal->goal.number = 0;
|
|
activategoal->goal.flags = 0;
|
|
return qtrue;
|
|
}
|
|
}
|
|
return qfalse;
|
|
} 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) {
|
|
//
|
|
VectorCopy(origin, activategoal->goal.origin);
|
|
activategoal->goal.areanum = areas[i];
|
|
VectorSubtract(mins, origin, activategoal->goal.mins);
|
|
VectorSubtract(maxs, origin, activategoal->goal.maxs);
|
|
//
|
|
for (i = 0; i < 3; i++) {
|
|
if (movedir[i] < 0)
|
|
activategoal->goal.maxs[i] += fabs(movedir[i]) * fabs(extramaxs[i]);
|
|
else
|
|
activategoal->goal.mins[i] += fabs(movedir[i]) * fabs(extramins[i]);
|
|
} //end for
|
|
//
|
|
activategoal->goal.entitynum = entitynum;
|
|
activategoal->goal.number = 0;
|
|
activategoal->goal.flags = 0;
|
|
return qtrue;
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotFuncDoorGoal
|
|
==================
|
|
*/
|
|
int BotFuncDoorActivateGoal(bot_state_t * bs, int bspent, bot_activategoal_t * activategoal)
|
|
{
|
|
int modelindex, entitynum;
|
|
char model[MAX_INFO_STRING];
|
|
vec3_t mins, maxs, origin;
|
|
|
|
//shoot at the shootable door
|
|
trap_AAS_ValueForBSPEpairKey(bspent, "model", model, sizeof(model));
|
|
if (!*model)
|
|
return qfalse;
|
|
modelindex = atoi(model + 1);
|
|
if (!modelindex)
|
|
return qfalse;
|
|
//VectorClear(angles);
|
|
entitynum = BotModelMinsMaxs(modelindex, ET_MOVER, 0, mins, maxs);
|
|
//door origin
|
|
VectorAdd(mins, maxs, origin);
|
|
VectorScale(origin, 0.5, origin);
|
|
VectorCopy(origin, activategoal->target);
|
|
activategoal->shoot = qtrue;
|
|
activategoal->goal.entitynum = entitynum; //NOTE: this is the entity number of the shootable door
|
|
activategoal->goal.number = 0;
|
|
activategoal->goal.flags = 0;
|
|
VectorCopy(bs->origin, activategoal->goal.origin);
|
|
activategoal->goal.areanum = bs->areanum;
|
|
VectorSet(activategoal->goal.mins, -8, -8, -8);
|
|
VectorSet(activategoal->goal.maxs, 8, 8, 8);
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
====================================
|
|
BotFuncBreakableGoal
|
|
|
|
Added by Makro
|
|
Basically a rip off of the previous
|
|
function !
|
|
====================================
|
|
*/
|
|
int BotFuncBreakableGoal(bot_state_t * bs, int bspent, bot_activategoal_t * activategoal)
|
|
{
|
|
int modelindex, entitynum;
|
|
char model[MAX_INFO_STRING];
|
|
vec3_t mins, maxs, origin;
|
|
|
|
//shoot at the func_breakable
|
|
trap_AAS_ValueForBSPEpairKey(bspent, "model", model, sizeof(model));
|
|
if (!*model)
|
|
return qfalse;
|
|
modelindex = atoi(model + 1);
|
|
if (!modelindex)
|
|
return qfalse;
|
|
//VectorClear(angles);
|
|
//Makro - changing from ET_BREAKABLE to 0
|
|
entitynum = BotModelMinsMaxs(modelindex, 0, 0, mins, maxs);
|
|
//breakable origin
|
|
VectorAdd(mins, maxs, origin);
|
|
VectorScale(origin, 0.5, origin);
|
|
VectorCopy(origin, activategoal->target);
|
|
activategoal->shoot = qtrue;
|
|
activategoal->goal.entitynum = entitynum;
|
|
activategoal->goal.number = 0;
|
|
activategoal->goal.flags = 0;
|
|
//activategoal->weapon = WP_NUM_WEAPONS;
|
|
activategoal->noWeapon = qtrue;
|
|
//Makro - hmm, not quite sure this is right, but they did it for func_doors
|
|
VectorCopy(bs->origin, activategoal->goal.origin);
|
|
activategoal->goal.areanum = bs->areanum;
|
|
VectorSet(activategoal->goal.mins, -8, -8, -8);
|
|
VectorSet(activategoal->goal.maxs, 8, 8, 8);
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
====================================
|
|
BotFuncDoorRotatingActivateGoal
|
|
|
|
Added by Makro
|
|
====================================
|
|
*/
|
|
int BotFuncDoorRotatingActivateGoal(bot_state_t * bs, int bspent, bot_activategoal_t * activategoal)
|
|
{
|
|
int modelindex, entitynum;
|
|
char model[MAX_INFO_STRING];
|
|
vec3_t mins, maxs, origin, angles;
|
|
|
|
trap_AAS_ValueForBSPEpairKey(bspent, "model", model, sizeof(model));
|
|
if (!*model)
|
|
return qfalse;
|
|
modelindex = atoi(model + 1);
|
|
if (!modelindex)
|
|
return qfalse;
|
|
VectorClear(angles);
|
|
entitynum = BotModelMinsMaxs(modelindex, ET_MOVER, 0, mins, maxs);
|
|
//door origin
|
|
VectorAdd(mins, maxs, origin);
|
|
VectorScale(origin, 0.5, origin);
|
|
activategoal->goal.entitynum = entitynum;
|
|
VectorTargetDist(bs->origin, origin, -48, activategoal->origin);
|
|
activategoal->openDoor = qtrue;
|
|
activategoal->goal.number = 0;
|
|
activategoal->goal.flags = 0;
|
|
//VectorCopy(bs->origin, activategoal->goal.origin);
|
|
VectorCopy(activategoal->origin, activategoal->goal.origin);
|
|
activategoal->goal.areanum = bs->areanum;
|
|
VectorSet(activategoal->goal.mins, -8, -8, -8);
|
|
VectorSet(activategoal->goal.maxs, 8, 8, 8);
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotTriggerMultipleGoal
|
|
==================
|
|
*/
|
|
int BotTriggerMultipleActivateGoal(bot_state_t * bs, int bspent, bot_activategoal_t * activategoal)
|
|
{
|
|
int i, areas[10], numareas, modelindex, entitynum;
|
|
char model[128];
|
|
vec3_t start, end, mins, maxs, angles;
|
|
vec3_t origin, goalorigin;
|
|
|
|
activategoal->shoot = qfalse;
|
|
VectorClear(activategoal->target);
|
|
//create a bot goal towards the trigger
|
|
trap_AAS_ValueForBSPEpairKey(bspent, "model", model, sizeof(model));
|
|
if (!*model)
|
|
return qfalse;
|
|
modelindex = atoi(model + 1);
|
|
if (!modelindex)
|
|
return qfalse;
|
|
VectorClear(angles);
|
|
entitynum = BotModelMinsMaxs(modelindex, 0, CONTENTS_TRIGGER, mins, maxs);
|
|
//trigger origin
|
|
VectorAdd(mins, maxs, origin);
|
|
VectorScale(origin, 0.5, origin);
|
|
VectorCopy(origin, 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) {
|
|
VectorCopy(origin, activategoal->goal.origin);
|
|
activategoal->goal.areanum = areas[i];
|
|
VectorSubtract(mins, origin, activategoal->goal.mins);
|
|
VectorSubtract(maxs, origin, activategoal->goal.maxs);
|
|
//
|
|
activategoal->goal.entitynum = entitynum;
|
|
activategoal->goal.number = 0;
|
|
activategoal->goal.flags = 0;
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotPopFromActivateGoalStack
|
|
==================
|
|
*/
|
|
int BotPopFromActivateGoalStack(bot_state_t * bs)
|
|
{
|
|
if (!bs->activatestack)
|
|
return qfalse;
|
|
BotEnableActivateGoalAreas(bs->activatestack, qtrue);
|
|
bs->activatestack->inuse = qfalse;
|
|
bs->activatestack->justused_time = FloatTime();
|
|
bs->activatestack = bs->activatestack->next;
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotPushOntoActivateGoalStack
|
|
==================
|
|
*/
|
|
int BotPushOntoActivateGoalStack(bot_state_t * bs, bot_activategoal_t * activategoal)
|
|
{
|
|
int i, best;
|
|
float besttime;
|
|
|
|
best = -1;
|
|
besttime = FloatTime() + 9999;
|
|
//
|
|
for (i = 0; i < MAX_ACTIVATESTACK; i++) {
|
|
if (!bs->activategoalheap[i].inuse) {
|
|
if (bs->activategoalheap[i].justused_time < besttime) {
|
|
besttime = bs->activategoalheap[i].justused_time;
|
|
best = i;
|
|
}
|
|
}
|
|
}
|
|
if (best != -1) {
|
|
memcpy(&bs->activategoalheap[best], activategoal, sizeof(bot_activategoal_t));
|
|
bs->activategoalheap[best].inuse = qtrue;
|
|
bs->activategoalheap[best].next = bs->activatestack;
|
|
bs->activatestack = &bs->activategoalheap[best];
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotClearActivateGoalStack
|
|
==================
|
|
*/
|
|
void BotClearActivateGoalStack(bot_state_t * bs)
|
|
{
|
|
while (bs->activatestack)
|
|
BotPopFromActivateGoalStack(bs);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotEnableActivateGoalAreas
|
|
==================
|
|
*/
|
|
void BotEnableActivateGoalAreas(bot_activategoal_t * activategoal, int enable)
|
|
{
|
|
int i;
|
|
|
|
if (activategoal->areasdisabled == !enable)
|
|
return;
|
|
for (i = 0; i < activategoal->numareas; i++)
|
|
trap_AAS_EnableRoutingArea(activategoal->areas[i], enable);
|
|
activategoal->areasdisabled = !enable;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotIsGoingToActivateEntity
|
|
==================
|
|
*/
|
|
int BotIsGoingToActivateEntity(bot_state_t * bs, int entitynum)
|
|
{
|
|
bot_activategoal_t *a;
|
|
int i;
|
|
|
|
for (a = bs->activatestack; a; a = a->next) {
|
|
if (a->time < FloatTime())
|
|
continue;
|
|
if (a->goal.entitynum == entitynum)
|
|
return qtrue;
|
|
}
|
|
for (i = 0; i < MAX_ACTIVATESTACK; i++) {
|
|
if (bs->activategoalheap[i].inuse)
|
|
continue;
|
|
//
|
|
if (bs->activategoalheap[i].goal.entitynum == entitynum) {
|
|
// if the bot went for this goal less than 2 seconds ago
|
|
if (bs->activategoalheap[i].justused_time > FloatTime() - 2)
|
|
return qtrue;
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotGetActivateGoal
|
|
|
|
returns the number of the bsp entity to activate
|
|
goal->entitynum will be set to the game entity to activate
|
|
==================
|
|
*/
|
|
//#define OBSTACLEDEBUG
|
|
|
|
int BotGetActivateGoal(bot_state_t * bs, int entitynum, bot_activategoal_t * activategoal)
|
|
{
|
|
int i, ent, cur_entities[10], spawnflags, modelindex, areas[MAX_ACTIVATEAREAS * 2], numareas, t;
|
|
char model[MAX_INFO_STRING], tmpmodel[128];
|
|
char target[128], classname[128];
|
|
float health;
|
|
char targetname[10][128];
|
|
aas_entityinfo_t entinfo;
|
|
aas_areainfo_t areainfo;
|
|
vec3_t origin, angles, absmins, absmaxs;
|
|
|
|
memset(activategoal, 0, sizeof(bot_activategoal_t));
|
|
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, "BotGetActivateGoal: no entity found with model %s\n", model);
|
|
return 0;
|
|
}
|
|
trap_AAS_ValueForBSPEpairKey(ent, "classname", classname, sizeof(classname));
|
|
if (!*classname) {
|
|
BotAI_Print(PRT_ERROR, "BotGetActivateGoal: entity with model %s has no classname\n", model);
|
|
return 0;
|
|
}
|
|
//if it is a door
|
|
//Makro - or a door_rotating
|
|
if (!strcmp(classname, "func_door") || !strcmp(classname, "func_door_rotating")) {
|
|
qboolean rotating = (strcmp(classname, "func_door_rotating") == 0);
|
|
|
|
if (trap_AAS_FloatForBSPEpairKey(ent, "health", &health)) {
|
|
//if the door has health then the door must be shot to open
|
|
if (health) {
|
|
BotFuncDoorActivateGoal(bs, ent, activategoal);
|
|
return ent;
|
|
}
|
|
}
|
|
//
|
|
trap_AAS_IntForBSPEpairKey(ent, "spawnflags", &spawnflags);
|
|
// if the door starts open then just wait for the door to return
|
|
// Makro - only if toggle is not set
|
|
if ((spawnflags & 1) && !(spawnflags & 8))
|
|
return 0;
|
|
//get the door origin
|
|
if (!trap_AAS_VectorForBSPEpairKey(ent, "origin", origin)) {
|
|
VectorClear(origin);
|
|
}
|
|
//if the door is open or opening already
|
|
//Makro - different code for rotating doors
|
|
if (!rotating) {
|
|
if (!VectorCompare(origin, entinfo.origin))
|
|
return 0;
|
|
} else {
|
|
if (g_entities[entinfo.number].moverState == ROTATOR_1TO2
|
|
|| g_entities[entinfo.number].moverState == ROTATOR_2TO1)
|
|
return 0;
|
|
}
|
|
// store all the areas the door is in
|
|
trap_AAS_ValueForBSPEpairKey(ent, "model", model, sizeof(model));
|
|
if (*model) {
|
|
modelindex = atoi(model + 1);
|
|
if (modelindex) {
|
|
VectorClear(angles);
|
|
BotModelMinsMaxs(modelindex, ET_MOVER, 0, absmins, absmaxs);
|
|
//
|
|
numareas = trap_AAS_BBoxAreas(absmins, absmaxs, areas, MAX_ACTIVATEAREAS * 2);
|
|
// store the areas with reachabilities first
|
|
for (i = 0; i < numareas; i++) {
|
|
if (activategoal->numareas >= MAX_ACTIVATEAREAS)
|
|
break;
|
|
if (!trap_AAS_AreaReachability(areas[i])) {
|
|
continue;
|
|
}
|
|
trap_AAS_AreaInfo(areas[i], &areainfo);
|
|
if (areainfo.contents & AREACONTENTS_MOVER) {
|
|
activategoal->areas[activategoal->numareas++] = areas[i];
|
|
}
|
|
}
|
|
// store any remaining areas
|
|
for (i = 0; i < numareas; i++) {
|
|
if (activategoal->numareas >= MAX_ACTIVATEAREAS)
|
|
break;
|
|
if (trap_AAS_AreaReachability(areas[i])) {
|
|
continue;
|
|
}
|
|
trap_AAS_AreaInfo(areas[i], &areainfo);
|
|
if (areainfo.contents & AREACONTENTS_MOVER) {
|
|
activategoal->areas[activategoal->numareas++] = areas[i];
|
|
}
|
|
}
|
|
}
|
|
//Makro - open the door
|
|
if (!g_entities[entinfo.number].targetname) {
|
|
if (!rotating) {
|
|
Cmd_OpenDoor(&g_entities[bs->entitynum]);
|
|
return 0;
|
|
} else {
|
|
BotMoveTowardsEnt(bs, entinfo.origin, -80);
|
|
//BotMoveTowardsEnt(bs, g_entities[entinfo.number].s.origin2, -80);
|
|
//BotFuncDoorRotatingActivateGoal(bs, ent, activategoal);
|
|
Cmd_OpenDoor(&g_entities[bs->entitynum]);
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//if it is some glass
|
|
if (!strcmp(classname, "func_breakable")) {
|
|
//disable all areas the blocking entity is in
|
|
BotEnableActivateGoalAreas(activategoal, qfalse);
|
|
//nothing the bot can do to unbreakable glass
|
|
if (g_entities[entinfo.number].unbreakable)
|
|
return 0;
|
|
if (!g_entities[entinfo.number].targetname) {
|
|
BotFuncBreakableGoal(bs, ent, activategoal);
|
|
return ent;
|
|
}
|
|
}
|
|
/* Handled above now
|
|
//Makro - checking for rotating doors
|
|
if ( !strcmp(classname, "func_door_rotating") ) {
|
|
//if door is moving, wait till it stops
|
|
if ( g_entities[entitynum].moverState == ROTATOR_1TO2 || g_entities[entitynum].moverState == ROTATOR_2TO1 || (g_entities[entitynum].targetname) ) {
|
|
BotMoveTowardsEnt(bs, entinfo.origin, -80);
|
|
if ( g_entities[entitynum].targetname == NULL ) {
|
|
return 0;
|
|
}
|
|
} else {
|
|
//Cmd_OpenDoor( &g_entities[bs->entitynum] );
|
|
BotFuncDoorRotatingActivateGoal(bs, ent, activategoal);
|
|
//disable all areas the blocking entity is in
|
|
BotEnableActivateGoalAreas( activategoal, qfalse );
|
|
return ent;
|
|
}
|
|
} */
|
|
// if the bot is blocked by or standing on top of a button
|
|
if (!strcmp(classname, "func_button")) {
|
|
return 0;
|
|
}
|
|
// get the targetname so we can find an entity with a matching target
|
|
if (!trap_AAS_ValueForBSPEpairKey(ent, "targetname", targetname[0], sizeof(targetname[0]))) {
|
|
if (bot_developer.integer) {
|
|
BotAI_Print(PRT_ERROR, "BotGetActivateGoal: entity with model \"%s\" has no targetname\n",
|
|
model);
|
|
}
|
|
return 0;
|
|
}
|
|
// allow tree-like activation
|
|
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) {
|
|
if (bot_developer.integer) {
|
|
BotAI_Print(PRT_ERROR, "BotGetActivateGoal: no entity with target \"%s\"\n",
|
|
targetname[i]);
|
|
}
|
|
i--;
|
|
continue;
|
|
}
|
|
if (!trap_AAS_ValueForBSPEpairKey(ent, "classname", classname, sizeof(classname))) {
|
|
if (bot_developer.integer) {
|
|
BotAI_Print(PRT_ERROR,
|
|
"BotGetActivateGoal: entity with target \"%s\" has no classname\n",
|
|
targetname[i]);
|
|
}
|
|
continue;
|
|
}
|
|
// BSP button model
|
|
if (!strcmp(classname, "func_button")) {
|
|
//
|
|
if (!BotFuncButtonActivateGoal(bs, ent, activategoal))
|
|
continue;
|
|
// if the bot tries to activate this button already
|
|
if (bs->activatestack && bs->activatestack->inuse &&
|
|
bs->activatestack->goal.entitynum == activategoal->goal.entitynum &&
|
|
bs->activatestack->time > FloatTime() && bs->activatestack->start_time < FloatTime() - 2)
|
|
continue;
|
|
// if the bot is in a reachability area
|
|
if (trap_AAS_AreaReachability(bs->areanum)) {
|
|
// disable all areas the blocking entity is in
|
|
BotEnableActivateGoalAreas(activategoal, qfalse);
|
|
//
|
|
t = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin,
|
|
activategoal->goal.areanum, bs->tfl);
|
|
// if the button is not reachable
|
|
if (!t) {
|
|
continue;
|
|
}
|
|
activategoal->time = FloatTime() + t * 0.01 + 5;
|
|
}
|
|
return ent;
|
|
}
|
|
// invisible trigger multiple box
|
|
else if (!strcmp(classname, "trigger_multiple")) {
|
|
//
|
|
if (!BotTriggerMultipleActivateGoal(bs, ent, activategoal))
|
|
continue;
|
|
// if the bot tries to activate this trigger already
|
|
if (bs->activatestack && bs->activatestack->inuse &&
|
|
bs->activatestack->goal.entitynum == activategoal->goal.entitynum &&
|
|
bs->activatestack->time > FloatTime() && bs->activatestack->start_time < FloatTime() - 2)
|
|
continue;
|
|
// if the bot is in a reachability area
|
|
if (trap_AAS_AreaReachability(bs->areanum)) {
|
|
// disable all areas the blocking entity is in
|
|
BotEnableActivateGoalAreas(activategoal, qfalse);
|
|
//
|
|
t = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin,
|
|
activategoal->goal.areanum, bs->tfl);
|
|
// if the trigger is not reachable
|
|
if (!t) {
|
|
continue;
|
|
}
|
|
activategoal->time = FloatTime() + t * 0.01 + 5;
|
|
}
|
|
return ent;
|
|
} else if (!strcmp(classname, "func_timer")) {
|
|
// just skip the func_timer
|
|
continue;
|
|
}
|
|
// the actual button or trigger might be linked through a target_relay or target_delay
|
|
else if (!strcmp(classname, "target_relay") || !strcmp(classname, "target_delay")) {
|
|
if (trap_AAS_ValueForBSPEpairKey(ent, "targetname", targetname[i + 1], sizeof(targetname[0]))) {
|
|
i++;
|
|
cur_entities[i] = trap_AAS_NextBSPEntity(0);
|
|
}
|
|
}
|
|
}
|
|
#ifdef OBSTACLEDEBUG
|
|
BotAI_Print(PRT_ERROR, "BotGetActivateGoal: no valid activator for entity with target \"%s\"\n", targetname[0]);
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotGoForActivateGoal
|
|
==================
|
|
*/
|
|
int BotGoForActivateGoal(bot_state_t * bs, bot_activategoal_t * activategoal)
|
|
{
|
|
aas_entityinfo_t activateinfo;
|
|
|
|
activategoal->inuse = qtrue;
|
|
if (!activategoal->time)
|
|
activategoal->time = FloatTime() + 10;
|
|
activategoal->start_time = FloatTime();
|
|
BotEntityInfo(activategoal->goal.entitynum, &activateinfo);
|
|
VectorCopy(activateinfo.origin, activategoal->origin);
|
|
//
|
|
if (BotPushOntoActivateGoalStack(bs, activategoal)) {
|
|
// enter the activate entity AI node
|
|
AIEnter_Seek_ActivateEntity(bs, "BotGoForActivateGoal");
|
|
return qtrue;
|
|
} else {
|
|
// enable any routing areas that were disabled
|
|
BotEnableActivateGoalAreas(activategoal, qtrue);
|
|
return qfalse;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotPrintActivateGoalInfo
|
|
==================
|
|
*/
|
|
void BotPrintActivateGoalInfo(bot_state_t * bs, bot_activategoal_t * activategoal, int bspent)
|
|
{
|
|
char netname[MAX_NETNAME];
|
|
char classname[128];
|
|
char buf[128];
|
|
|
|
ClientName(bs->client, netname, sizeof(netname));
|
|
trap_AAS_ValueForBSPEpairKey(bspent, "classname", classname, sizeof(classname));
|
|
if (activategoal->shoot) {
|
|
Com_sprintf(buf, sizeof(buf), "%s: I have to shoot at a %s from %1.1f %1.1f %1.1f in area %d\n",
|
|
netname, classname,
|
|
activategoal->goal.origin[0],
|
|
activategoal->goal.origin[1], activategoal->goal.origin[2], activategoal->goal.areanum);
|
|
} else {
|
|
Com_sprintf(buf, sizeof(buf), "%s: I have to activate a %s at %1.1f %1.1f %1.1f in area %d\n",
|
|
netname, classname,
|
|
activategoal->goal.origin[0],
|
|
activategoal->goal.origin[1], activategoal->goal.origin[2], activategoal->goal.areanum);
|
|
}
|
|
trap_EA_Say(bs->client, buf);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotRandomMove
|
|
==================
|
|
*/
|
|
void BotRandomMove(bot_state_t * bs, bot_moveresult_t * moveresult)
|
|
{
|
|
vec3_t dir, angles;
|
|
|
|
angles[0] = 0;
|
|
angles[1] = random() * 360;
|
|
angles[2] = 0;
|
|
AngleVectors(angles, dir, NULL, NULL);
|
|
|
|
trap_BotMoveInDirection(bs->ms, dir, 400, MOVE_WALK);
|
|
|
|
moveresult->failure = qfalse;
|
|
VectorCopy(dir, moveresult->movedir);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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. If that's not an option then try to walk around or over 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, bspent;
|
|
vec3_t hordir, start, end, mins, maxs, sideward, angles, up = { 0, 0, 1 };
|
|
aas_entityinfo_t entinfo;
|
|
bot_activategoal_t activategoal;
|
|
|
|
// if the bot is not blocked by anything
|
|
if (!moveresult->blocked) {
|
|
bs->notblocked_time = FloatTime();
|
|
return;
|
|
}
|
|
// if stuck in a solid area
|
|
if (moveresult->type == RESULTTYPE_INSOLIDAREA) {
|
|
// move in a random direction in the hope to get out
|
|
BotRandomMove(bs, moveresult);
|
|
//
|
|
return;
|
|
}
|
|
// get info for the entity that is blocking the bot
|
|
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);
|
|
// JBravo: silencing a compiler warning by commenting out text after endif
|
|
#endif /* OBSTACLEDEBUG */
|
|
// if blocked by a bsp model and the bot wants to activate it
|
|
if (activate && entinfo.modelindex > 0 && entinfo.modelindex <= max_bspmodelindex) {
|
|
// find the bsp entity which should be activated in order to get the blocking entity out of the way
|
|
bspent = BotGetActivateGoal(bs, entinfo.number, &activategoal);
|
|
if (bspent) {
|
|
//
|
|
if (bs->activatestack && !bs->activatestack->inuse)
|
|
bs->activatestack = NULL;
|
|
// if not already trying to activate this entity
|
|
if (!BotIsGoingToActivateEntity(bs, activategoal.goal.entitynum)) {
|
|
//
|
|
BotGoForActivateGoal(bs, &activategoal);
|
|
}
|
|
// if ontop of an obstacle or
|
|
// if the bot is not in a reachability area it'll still
|
|
// need some dynamic obstacle avoidance, otherwise return
|
|
if (!(moveresult->flags & MOVERESULT_ONTOPOFOBSTACLE) && trap_AAS_AreaReachability(bs->areanum))
|
|
return;
|
|
} else {
|
|
// enable any routing areas that were disabled
|
|
BotEnableActivateGoalAreas(&activategoal, qtrue);
|
|
}
|
|
}
|
|
//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);
|
|
VectorMA(sideward, -1, hordir, sideward);
|
|
//move in the other direction
|
|
trap_BotMoveInDirection(bs->ms, sideward, 400, movetype);
|
|
}
|
|
}
|
|
//
|
|
if (bs->notblocked_time < FloatTime() - 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;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotAIPredictObstacles
|
|
|
|
Predict the route towards the goal and check if the bot
|
|
will be blocked by certain obstacles. When the bot has obstacles
|
|
on it's path the bot should figure out if they can be removed
|
|
by activating certain entities.
|
|
==================
|
|
*/
|
|
int BotAIPredictObstacles(bot_state_t * bs, bot_goal_t * goal)
|
|
{
|
|
int modelnum, entitynum, bspent;
|
|
bot_activategoal_t activategoal;
|
|
aas_predictroute_t route;
|
|
|
|
if (!bot_predictobstacles.integer)
|
|
return qfalse;
|
|
|
|
// always predict when the goal change or at regular intervals
|
|
if (bs->predictobstacles_goalareanum == goal->areanum && bs->predictobstacles_time > FloatTime() - 6) {
|
|
return qfalse;
|
|
}
|
|
bs->predictobstacles_goalareanum = goal->areanum;
|
|
bs->predictobstacles_time = FloatTime();
|
|
|
|
// predict at most 100 areas or 10 seconds ahead
|
|
trap_AAS_PredictRoute(&route, bs->areanum, bs->origin,
|
|
goal->areanum, bs->tfl, 100, 1000,
|
|
RSE_USETRAVELTYPE | RSE_ENTERCONTENTS, AREACONTENTS_MOVER, TFL_BRIDGE, 0);
|
|
// if bot has to travel through an area with a mover
|
|
if (route.stopevent & RSE_ENTERCONTENTS) {
|
|
// if the bot will run into a mover
|
|
if (route.endcontents & AREACONTENTS_MOVER) {
|
|
//NOTE: this only works with bspc 2.1 or higher
|
|
modelnum = (route.endcontents & AREACONTENTS_MODELNUM) >> AREACONTENTS_MODELNUMSHIFT;
|
|
if (modelnum) {
|
|
//
|
|
entitynum = BotModelMinsMaxs(modelnum, ET_MOVER, 0, NULL, NULL);
|
|
if (entitynum) {
|
|
//NOTE: BotGetActivateGoal already checks if the door is open or not
|
|
bspent = BotGetActivateGoal(bs, entitynum, &activategoal);
|
|
if (bspent) {
|
|
//
|
|
if (bs->activatestack && !bs->activatestack->inuse)
|
|
bs->activatestack = NULL;
|
|
// if not already trying to activate this entity
|
|
if (!BotIsGoingToActivateEntity(bs, activategoal.goal.entitynum)) {
|
|
//
|
|
//BotAI_Print(PRT_MESSAGE, "blocked by mover model %d, entity %d ?\n", modelnum, entitynum);
|
|
//
|
|
BotGoForActivateGoal(bs, &activategoal);
|
|
return qtrue;
|
|
} else {
|
|
// enable any routing areas that were disabled
|
|
BotEnableActivateGoalAreas(&activategoal, qtrue);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (route.stopevent & RSE_USETRAVELTYPE) {
|
|
if (route.endtravelflags & TFL_BRIDGE) {
|
|
//FIXME: check if the bridge is available to travel over
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
#define BOT_RADIO_REPLY_TIME 2
|
|
|
|
/*
|
|
==================
|
|
BotReplyToRadioMessage
|
|
|
|
Added by Makro
|
|
==================
|
|
*/
|
|
void BotReplyToRadioMessage(bot_state_t * bs, char *msg, int handle)
|
|
{
|
|
|
|
//Must be in a team-based game to reply
|
|
if (gametype < GT_TEAM) {
|
|
return;
|
|
}
|
|
//Bot must be alive to report
|
|
if (!BotIsDead(bs)) {
|
|
char *token;
|
|
char *initial = msg;
|
|
|
|
token = COM_ParseExt(&msg, qtrue);
|
|
if (!Q_stricmp(token, "radio")) {
|
|
qboolean willreply;
|
|
|
|
//The bot is more likely to reply if you don't spam him with treport's
|
|
if (bs->radioresponse_count > 15) {
|
|
willreply = (random() < 0.3);
|
|
} else if (bs->radioresponse_count > 10) {
|
|
willreply = (random() < 0.5);
|
|
} else if (bs->radioresponse_count > 5) {
|
|
willreply = (random() < 0.7);
|
|
} else {
|
|
willreply = (random() < 0.9);
|
|
}
|
|
|
|
//Bots won't reply to ALL radio messages; they do have a life, you know
|
|
if ((willreply)
|
|
&& (FloatTime() >
|
|
BOT_RADIO_REPLY_TIME +
|
|
bs->radioresponse_time) /*&& (bs->radioresponse_count < 20) */ ) {
|
|
char *sender = COM_ParseExt(&msg, qtrue);
|
|
qboolean responded = qfalse;
|
|
|
|
//Lazy bots
|
|
if (random() < 0.5) {
|
|
//sender = Q_strlwr(sender);
|
|
EasyClientName(ClientFromName(sender), sender, 32);
|
|
}
|
|
|
|
if (strstr(msg, "treport")) {
|
|
//Team, report in
|
|
G_Say(&g_entities[bs->entitynum], NULL, SAY_TEAM,
|
|
va("%s, I have a $W with $A ammo and $H health", sender));
|
|
responded = qtrue;
|
|
}
|
|
|
|
/*
|
|
if (strstr(msg, "im_hit")) {
|
|
int senderId = ClientFromName(sender);
|
|
if (senderId != -1) {
|
|
BotMoveTo(bs, g_entities[senderId].r.currentOrigin);
|
|
G_Printf("%s: coming over to %s\n", g_entities[bs->entitynum].client->pers.netname, g_entities[senderId].client->pers.netname);
|
|
responded = qtrue;
|
|
}
|
|
}
|
|
*/
|
|
|
|
if (responded) {
|
|
bs->radioresponse_time = FloatTime();
|
|
bs->radioresponse_count++;
|
|
}
|
|
//TODO:
|
|
// - Add code that makes the bot move towards
|
|
// the player that sent the radio message
|
|
// - Prioritize teamdown, taking_f, im_hit
|
|
// over enemyd and enemys
|
|
// - Make the bot talk dirty to spammers
|
|
}
|
|
}
|
|
msg = initial;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotCheckRadioMessage
|
|
|
|
Added by Makro
|
|
==================
|
|
*/
|
|
qboolean BotCheckRadioMessage(bot_state_t * bs, char *msg, int handle)
|
|
{
|
|
char *token;
|
|
char *initial = msg;
|
|
|
|
//Must be in a team-based game to reply
|
|
if (gametype < GT_TEAM) {
|
|
return qfalse;
|
|
}
|
|
|
|
token = COM_ParseExt(&msg, qtrue);
|
|
msg = initial;
|
|
if (!Q_stricmp(token, "radio")) {
|
|
return qtrue;
|
|
}
|
|
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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;
|
|
qboolean radio;
|
|
|
|
//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 > FloatTime() - (1 + random()))
|
|
break;
|
|
}
|
|
//Makro - check for radio commands
|
|
radio = BotCheckRadioMessage(bs, m.message, handle);
|
|
if (radio) {
|
|
//The bot needs at least two seconds to reply
|
|
if (FloatTime() > (m.time + 2 + random() * 2)) {
|
|
BotReplyToRadioMessage(bs, m.message, handle);
|
|
//remove the console message
|
|
trap_BotRemoveConsoleMessage(bs->cs, handle);
|
|
continue;
|
|
} else {
|
|
//Too soon to reply
|
|
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 = BotSynonymContext(bs);
|
|
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 (bs->client == ClientFromName(netname)) {
|
|
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 and not in teamplay
|
|
else if (bs->ainode != AINode_Stand && BotValidChatPosition(bs) && !TeamPlayIsOn()) {
|
|
chat_reply =
|
|
trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_CHAT_REPLY, 0, 1);
|
|
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 = FloatTime() + BotChatTime(bs);
|
|
AIEnter_Stand(bs, "BotCheckConsoleMessages: reply chat");
|
|
//EA_Say(bs->client, bs->cs.chatmessage);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//remove the console message
|
|
trap_BotRemoveConsoleMessage(bs->cs, handle);
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotCheckEvents
|
|
==================
|
|
*/
|
|
void BotCheckForGrenades(bot_state_t * bs, entityState_t * state)
|
|
{
|
|
// if this is not a grenade
|
|
if (state->eType != ET_MISSILE || state->weapon != WP_GRENADE)
|
|
return;
|
|
// try to avoid the grenade
|
|
trap_BotAddAvoidSpot(bs->ms, state->pos.trBase, 160, AVOID_ALWAYS);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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 || target == ENTITYNUM_NONE || target == ENTITYNUM_WORLD)
|
|
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 = FloatTime();
|
|
//
|
|
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;
|
|
}
|
|
trap_GetConfigstring(CS_SOUNDS + state->eventParm, buf, sizeof(buf));
|
|
/*
|
|
if (!strcmp(buf, "sound/teamplay/flagret_red.wav")) {
|
|
//red flag is returned
|
|
bs->redflagstatus = 0;
|
|
bs->flagstatuschanged = qtrue;
|
|
}
|
|
else if (!strcmp(buf, "sound/teamplay/flagret_blu.wav")) {
|
|
//blue flag is returned
|
|
bs->blueflagstatus = 0;
|
|
bs->flagstatuschanged = qtrue;
|
|
}
|
|
else */
|
|
if (!strcmp(buf, "sound/items/poweruprespawn.wav")) {
|
|
//powerup respawned... go get it
|
|
BotGoForPowerups(bs);
|
|
}
|
|
break;
|
|
}
|
|
case EV_GLOBAL_TEAM_SOUND:
|
|
{
|
|
if (gametype == GT_CTF) {
|
|
switch (state->eventParm) {
|
|
case GTS_RED_CAPTURE:
|
|
bs->blueflagstatus = 0;
|
|
bs->redflagstatus = 0;
|
|
bs->flagstatuschanged = qtrue;
|
|
break; //see BotMatch_CTF
|
|
case GTS_BLUE_CAPTURE:
|
|
bs->blueflagstatus = 0;
|
|
bs->redflagstatus = 0;
|
|
bs->flagstatuschanged = qtrue;
|
|
break; //see BotMatch_CTF
|
|
case GTS_RED_RETURN:
|
|
//blue flag is returned
|
|
bs->blueflagstatus = 0;
|
|
bs->flagstatuschanged = qtrue;
|
|
break;
|
|
case GTS_BLUE_RETURN:
|
|
//red flag is returned
|
|
bs->redflagstatus = 0;
|
|
bs->flagstatuschanged = qtrue;
|
|
break;
|
|
case GTS_RED_TAKEN:
|
|
//blue flag is taken
|
|
bs->blueflagstatus = 1;
|
|
bs->flagstatuschanged = qtrue;
|
|
break; //see BotMatch_CTF
|
|
case GTS_BLUE_TAKEN:
|
|
//red flag is taken
|
|
bs->redflagstatus = 1;
|
|
bs->flagstatuschanged = qtrue;
|
|
break; //see BotMatch_CTF
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case EV_PLAYER_TELEPORT_IN:
|
|
{
|
|
VectorCopy(state->origin, lastteleport_origin);
|
|
lastteleport_time = FloatTime();
|
|
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;
|
|
}
|
|
case EV_FOOTSTEP:
|
|
case EV_FOOTSTEP_METAL:
|
|
case EV_FOOTSTEP_GRASS: // Elder: new surfaces
|
|
case EV_FOOTSTEP_WOOD:
|
|
case EV_FOOTSTEP_CARPET:
|
|
case EV_FOOTSTEP_METAL2:
|
|
case EV_FOOTSTEP_GRAVEL:
|
|
case EV_FOOTSTEP_SNOW: // JBravo: new surfaces
|
|
case EV_FOOTSTEP_MUD:
|
|
case EV_FOOTSTEP_WOOD2:
|
|
case EV_FOOTSTEP_HARDMETAL:
|
|
case EV_FOOTSTEP_LEAVES: // Makro: new surfaces
|
|
case EV_FOOTSTEP_CEMENT:
|
|
case EV_FOOTSTEP_MARBLE:
|
|
case EV_FOOTSTEP_SNOW2:
|
|
case EV_FOOTSTEP_HARDSTEPS:
|
|
case EV_FOOTSTEP_SAND:
|
|
case EV_FOOTSPLASH:
|
|
case EV_FOOTWADE:
|
|
case EV_SWIM:
|
|
case EV_FALL_SHORT:
|
|
break;
|
|
case EV_FALL_MEDIUM:
|
|
case EV_FALL_FAR:
|
|
case EV_FALL_FAR_NOSOUND: // Makro - check for falling damage
|
|
{
|
|
if (RQ3_Bot_NeedToBandage(bs) == 2) {
|
|
qboolean willBandage = qfalse;
|
|
|
|
//Makro - this is the attack skill, we should be using the overall skill
|
|
int skill =
|
|
trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_ATTACK_SKILL, 0, 1);
|
|
//if the bot isn't in the middle of a fight
|
|
if (bs->enemy == -1) {
|
|
if (BotFindEnemy(bs, -1)) {
|
|
//if an enemy is nearby, a smart bot won't bandage
|
|
willBandage = (random() > skill);
|
|
} else {
|
|
//but it will otherwise
|
|
willBandage = (random() > (1.0f - skill));
|
|
}
|
|
} else {
|
|
//smart bots don't bandage during a fight
|
|
willBandage = (random() > skill);
|
|
}
|
|
if (willBandage) {
|
|
Cmd_Bandage(&g_entities[bs->entitynum]);
|
|
}
|
|
}
|
|
}
|
|
case EV_STEP_4:
|
|
case EV_STEP_8:
|
|
case EV_STEP_12:
|
|
case EV_STEP_16:
|
|
case EV_JUMP_PAD:
|
|
case EV_JUMP:
|
|
case EV_TAUNT:
|
|
case EV_WATER_TOUCH:
|
|
case EV_WATER_LEAVE:
|
|
case EV_WATER_UNDER:
|
|
case EV_WATER_CLEAR:
|
|
case EV_ITEM_PICKUP:
|
|
case EV_GLOBAL_ITEM_PICKUP:
|
|
case EV_NOAMMO:
|
|
case EV_CHANGE_WEAPON:
|
|
case EV_FIRE_WEAPON:
|
|
//FIXME: either add to sound queue or mark player as someone making noise
|
|
break;
|
|
case EV_USE_ITEM0:
|
|
case EV_USE_ITEM1:
|
|
case EV_USE_ITEM2:
|
|
case EV_USE_ITEM3:
|
|
case EV_USE_ITEM4:
|
|
case EV_USE_ITEM5:
|
|
case EV_USE_ITEM6:
|
|
case EV_USE_ITEM7:
|
|
case EV_USE_ITEM8:
|
|
case EV_USE_ITEM9:
|
|
case EV_USE_ITEM10:
|
|
case EV_USE_ITEM11:
|
|
case EV_USE_ITEM12:
|
|
case EV_USE_ITEM13:
|
|
case EV_USE_ITEM14:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotCheckSnapshot
|
|
==================
|
|
*/
|
|
void BotCheckSnapshot(bot_state_t * bs)
|
|
{
|
|
int ent;
|
|
entityState_t state;
|
|
|
|
//remove all avoid spots
|
|
trap_BotAddAvoidSpot(bs->ms, vec3_origin, 0, AVOID_CLEAR);
|
|
//reset kamikaze body
|
|
bs->kamikazebody = 0;
|
|
//reset number of proxmines
|
|
bs->numproxmines = 0;
|
|
//
|
|
ent = 0;
|
|
while ((ent = BotAI_GetSnapshotEntity(bs->client, ent, &state)) != -1) {
|
|
//check the entity state for events
|
|
BotCheckEvents(bs, &state);
|
|
//check for grenades the bot should avoid
|
|
BotCheckForGrenades(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)
|
|
{
|
|
bs->lastair_time = FloatTime();
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotAlternateRoute
|
|
==================
|
|
*/
|
|
bot_goal_t *BotAlternateRoute(bot_state_t * bs, bot_goal_t * goal)
|
|
{
|
|
int t;
|
|
|
|
// if the bot has an alternative route goal
|
|
if (bs->altroutegoal.areanum) {
|
|
//
|
|
if (bs->reachedaltroutegoal_time)
|
|
return goal;
|
|
// travel time towards alternative route goal
|
|
t = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, bs->altroutegoal.areanum, bs->tfl);
|
|
if (t && t < 20) {
|
|
//BotAI_Print(PRT_MESSAGE, "reached alternate route goal\n");
|
|
bs->reachedaltroutegoal_time = FloatTime();
|
|
}
|
|
memcpy(goal, &bs->altroutegoal, sizeof(bot_goal_t));
|
|
return &bs->altroutegoal;
|
|
}
|
|
return goal;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotGetAlternateRouteGoal
|
|
==================
|
|
*/
|
|
int BotGetAlternateRouteGoal(bot_state_t * bs, int base)
|
|
{
|
|
aas_altroutegoal_t *altroutegoals;
|
|
bot_goal_t *goal;
|
|
int numaltroutegoals, rnd;
|
|
|
|
if (base == TEAM_RED) {
|
|
altroutegoals = red_altroutegoals;
|
|
numaltroutegoals = red_numaltroutegoals;
|
|
} else {
|
|
altroutegoals = blue_altroutegoals;
|
|
numaltroutegoals = blue_numaltroutegoals;
|
|
}
|
|
if (!numaltroutegoals)
|
|
return qfalse;
|
|
rnd = (float) random() * numaltroutegoals;
|
|
if (rnd >= numaltroutegoals)
|
|
rnd = numaltroutegoals - 1;
|
|
goal = &bs->altroutegoal;
|
|
goal->areanum = altroutegoals[rnd].areanum;
|
|
VectorCopy(altroutegoals[rnd].origin, goal->origin);
|
|
VectorSet(goal->mins, -8, -8, -8);
|
|
VectorSet(goal->maxs, 8, 8, 8);
|
|
goal->entitynum = 0;
|
|
goal->iteminfo = 0;
|
|
goal->number = 0;
|
|
goal->flags = 0;
|
|
//
|
|
bs->reachedaltroutegoal_time = 0;
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotSetupAlternateRouteGoals
|
|
==================
|
|
*/
|
|
void BotSetupAlternativeRouteGoals(void)
|
|
{
|
|
|
|
if (altroutegoals_setup)
|
|
return;
|
|
altroutegoals_setup = qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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 (!bs->map_restart && 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->client);
|
|
//
|
|
bs->lastframe_health = bs->inventory[INVENTORY_HEALTH];
|
|
bs->lasthitcount = bs->cur_ps.persistant[PERS_HITS];
|
|
//
|
|
bs->setupcount = 0;
|
|
//
|
|
BotSetupAlternativeRouteGoals();
|
|
}
|
|
//no ideal view set
|
|
bs->flags &= ~BFL_IDEALVIEWSET;
|
|
//
|
|
if (!BotIntermission(bs)) {
|
|
//set the teleport time
|
|
BotSetTeleportTime(bs);
|
|
//update some inventory values
|
|
BotUpdateInventory(bs);
|
|
//check out the snapshot
|
|
BotCheckSnapshot(bs);
|
|
//check for air
|
|
BotCheckAir(bs);
|
|
}
|
|
//check the console messages
|
|
BotCheckConsoleMessages(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, "BotDeathmatchAI: no ai node");
|
|
}
|
|
//if the bot entered the game less than 8 seconds ago
|
|
if (!bs->entergamechat && bs->entergame_time > FloatTime() - 8) {
|
|
if (BotChat_EnterGame(bs)) {
|
|
bs->stand_time = FloatTime() + BotChatTime(bs);
|
|
AIEnter_Stand(bs, "BotDeathmatchAI: chat enter game");
|
|
}
|
|
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, FloatTime(),
|
|
MAX_NODESWITCHES);
|
|
}
|
|
//
|
|
bs->lastframe_health = bs->inventory[INVENTORY_HEALTH];
|
|
bs->lasthitcount = bs->cur_ps.persistant[PERS_HITS];
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotSetEntityNumForGoalWithModel
|
|
==================
|
|
*/
|
|
void BotSetEntityNumForGoalWithModel(bot_goal_t * goal, int eType, char *modelname)
|
|
{
|
|
gentity_t *ent;
|
|
int i, modelindex;
|
|
vec3_t dir;
|
|
|
|
modelindex = G_ModelIndex(modelname);
|
|
ent = &g_entities[0];
|
|
for (i = 0; i < level.num_entities; i++, ent++) {
|
|
if (!ent->inuse) {
|
|
continue;
|
|
}
|
|
if (eType && ent->s.eType != eType) {
|
|
continue;
|
|
}
|
|
if (ent->s.modelindex != modelindex) {
|
|
continue;
|
|
}
|
|
VectorSubtract(goal->origin, ent->s.origin, dir);
|
|
if (VectorLengthSquared(dir) < Square(10)) {
|
|
goal->entitynum = i;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotSetEntityNumForGoal
|
|
==================
|
|
*/
|
|
void BotSetEntityNumForGoal(bot_goal_t * goal, char *classname)
|
|
{
|
|
gentity_t *ent;
|
|
int i;
|
|
vec3_t dir;
|
|
|
|
ent = &g_entities[0];
|
|
for (i = 0; i < level.num_entities; i++, ent++) {
|
|
if (!ent->inuse) {
|
|
continue;
|
|
}
|
|
if (!Q_stricmp(ent->classname, classname)) {
|
|
continue;
|
|
}
|
|
VectorSubtract(goal->origin, ent->s.origin, dir);
|
|
if (VectorLengthSquared(dir) < Square(10)) {
|
|
goal->entitynum = i;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotGoalForBSPEntity
|
|
==================
|
|
*/
|
|
int BotGoalForBSPEntity(char *classname, bot_goal_t * goal)
|
|
{
|
|
char value[MAX_INFO_STRING];
|
|
vec3_t origin, start, end;
|
|
int ent, numareas, areas[10];
|
|
|
|
memset(goal, 0, sizeof(bot_goal_t));
|
|
for (ent = trap_AAS_NextBSPEntity(0); ent; ent = trap_AAS_NextBSPEntity(ent)) {
|
|
if (!trap_AAS_ValueForBSPEpairKey(ent, "classname", value, sizeof(value)))
|
|
continue;
|
|
if (!strcmp(value, classname)) {
|
|
if (!trap_AAS_VectorForBSPEpairKey(ent, "origin", origin))
|
|
return qfalse;
|
|
VectorCopy(origin, goal->origin);
|
|
VectorCopy(origin, start);
|
|
start[2] -= 32;
|
|
VectorCopy(origin, end);
|
|
end[2] += 32;
|
|
numareas = trap_AAS_TraceAreas(start, end, areas, NULL, 10);
|
|
if (!numareas)
|
|
return qfalse;
|
|
goal->areanum = areas[0];
|
|
return qtrue;
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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);
|
|
trap_Cvar_Register(&bot_predictobstacles, "bot_predictobstacles", "1", 0);
|
|
trap_Cvar_Register(&g_spSkill, "g_spSkill", "2", 0);
|
|
//
|
|
if (gametype == GT_CTF) {
|
|
if (trap_BotGetLevelItemGoal(-1, "Silver Case", &ctf_redflag) < 0)
|
|
BotAI_Print(PRT_WARNING, "CTB without Silver Case\n");
|
|
if (trap_BotGetLevelItemGoal(-1, "Black Case", &ctf_blueflag) < 0)
|
|
BotAI_Print(PRT_WARNING, "CTB without Black Case\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)
|
|
{
|
|
altroutegoals_setup = qfalse;
|
|
}
|