mirror of
https://github.com/DrBeef/ioq3quest.git
synced 2024-11-27 06:13:01 +00:00
f7c3276fe8
There are various issues caused by not knowing the initial team for the local client and bots when they connect. This is can be reproduced by starting a team game from the main menu. When g_teamAutoJoin is enabled, bots and local client join a random team at connect and then execute their team command a few frames later. This may result in the player being killed if they specify a different team. In Team Arena's Harvester mode this causes harvester skulls to be spawned at the beginning of the game. When g_teamForceBalance is enabled, the local client and bots may not be able to join their desired team. This may result in them being spectators. If g_teamAutoJoin is also enabled they may be left on the opposite (red/blue) team they were meant to join. There is a hack for including bot's team in their player info string (used by cgame for which team skin to use) before the bot joins their desired team. Bots aren't guaranteed to join their desired team (as may happen when both g_teamAutoJoin and g_teamForceBalance are enabled) so clients may see them as being on the wrong team! ---- Add teampref userinfo option for team preference. If teampref is set it will be used for attempting to join the team immediately at connect. Bots now join team at connect using teampref userinfo. So remove the hack for setting bot's team in player info string before the bot joins the team. To avoid the client sending teampref userinfo to all network servers, the local client uses a g_localTeamPref cvar. The g_localTeamPref cvar is cleared after it's used so it doesn't get used when starting another server later. Another reason not to use a teampref userinfo cvar is there isn't a reliable way to clear it in CGame/UI which are likely loaded from baseq3 pk3. Make it so g_teamAutoJoin doesn't affect clients who specify teampref. If teampref is invalid, the client will join a random team like g_teamAutoJoin. Don't apply g_teamForceBalance to the local client or bots. Otherwise they may be left as spectators when starting team game from menu. The start server menus use team command and g_localTeamPref to set the human player's team. This way it's compatible with vanilla Q3 game VMs and the new setting team at connect feature.
5467 lines
154 KiB
C
5467 lines
154 KiB
C
/*
|
|
===========================================================================
|
|
Copyright (C) 1999-2005 Id Software, Inc.
|
|
|
|
This file is part of Quake III Arena source code.
|
|
|
|
Quake III Arena source code is free software; you can redistribute it
|
|
and/or modify it under the terms of the GNU General Public License as
|
|
published by the Free Software Foundation; either version 2 of the License,
|
|
or (at your option) any later version.
|
|
|
|
Quake III Arena source code is distributed in the hope that it will be
|
|
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Quake III Arena source code; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
===========================================================================
|
|
*/
|
|
//
|
|
|
|
/*****************************************************************************
|
|
* name: ai_dmq3.c
|
|
*
|
|
* desc: Quake3 bot AI
|
|
*
|
|
* $Archive: /MissionPack/code/game/ai_dmq3.c $
|
|
*
|
|
*****************************************************************************/
|
|
|
|
|
|
#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
|
|
#include "../../ui/menudef.h" // sos001205 - for q3_ui also
|
|
|
|
// 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
|
|
|
|
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;
|
|
#ifdef MISSIONPACK
|
|
bot_goal_t ctf_neutralflag;
|
|
bot_goal_t redobelisk;
|
|
bot_goal_t blueobelisk;
|
|
bot_goal_t neutralobelisk;
|
|
#endif
|
|
|
|
#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;
|
|
|
|
|
|
/*
|
|
==================
|
|
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 );
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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) {
|
|
|
|
if (bs->client < 0 || bs->client >= MAX_CLIENTS) {
|
|
return qfalse;
|
|
}
|
|
|
|
if (level.clients[bs->client].sess.sessionTeam == TEAM_RED) {
|
|
return TEAM_RED;
|
|
} else if (level.clients[bs->client].sess.sessionTeam == 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
|
|
if (!BotAI_GetClientState(entinfo->number, &ps)) {
|
|
return qfalse;
|
|
}
|
|
|
|
if (ps.pm_type != PM_NORMAL) 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;
|
|
#ifdef MISSIONPACK
|
|
if ( entinfo->powerups & ( 1 << PW_NEUTRALFLAG ) )
|
|
return qtrue;
|
|
#endif
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
EntityIsInvisible
|
|
==================
|
|
*/
|
|
qboolean EntityIsInvisible(aas_entityinfo_t *entinfo) {
|
|
// the flag is always visible
|
|
if (EntityCarriesFlag(entinfo)) {
|
|
return qfalse;
|
|
}
|
|
if (entinfo->powerups & (1 << PW_INVIS)) {
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
EntityIsShooting
|
|
==================
|
|
*/
|
|
qboolean EntityIsShooting(aas_entityinfo_t *entinfo) {
|
|
if (entinfo->flags & EF_FIRING) {
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
EntityIsChatting
|
|
==================
|
|
*/
|
|
qboolean EntityIsChatting(aas_entityinfo_t *entinfo) {
|
|
if (entinfo->flags & EF_TALK) {
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
EntityHasQuad
|
|
==================
|
|
*/
|
|
qboolean EntityHasQuad(aas_entityinfo_t *entinfo) {
|
|
if (entinfo->powerups & (1 << PW_QUAD)) {
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
#ifdef MISSIONPACK
|
|
/*
|
|
==================
|
|
EntityHasKamikze
|
|
==================
|
|
*/
|
|
qboolean EntityHasKamikaze(aas_entityinfo_t *entinfo) {
|
|
if (entinfo->flags & EF_KAMIKAZE) {
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
EntityCarriesCubes
|
|
==================
|
|
*/
|
|
qboolean EntityCarriesCubes(aas_entityinfo_t *entinfo) {
|
|
entityState_t state;
|
|
|
|
if (gametype != GT_HARVESTER)
|
|
return qfalse;
|
|
//FIXME: get this info from the aas_entityinfo_t ?
|
|
BotAI_GetEntityState(entinfo->number, &state);
|
|
if (state.generic1 > 0)
|
|
return qtrue;
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
Bot1FCTFCarryingFlag
|
|
==================
|
|
*/
|
|
int Bot1FCTFCarryingFlag(bot_state_t *bs) {
|
|
if (gametype != GT_1FCTF) return qfalse;
|
|
|
|
if (bs->inventory[INVENTORY_NEUTRALFLAG] > 0) return qtrue;
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotHarvesterCarryingCubes
|
|
==================
|
|
*/
|
|
int BotHarvesterCarryingCubes(bot_state_t *bs) {
|
|
if (gametype != GT_HARVESTER) return qfalse;
|
|
|
|
if (bs->inventory[INVENTORY_REDCUBE] > 0) return qtrue;
|
|
if (bs->inventory[INVENTORY_BLUECUBE] > 0) return qtrue;
|
|
return qfalse;
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
==================
|
|
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) {
|
|
#ifdef MISSIONPACK
|
|
int teamtask;
|
|
aas_entityinfo_t entinfo;
|
|
|
|
teamtask = TEAMTASK_PATROL;
|
|
|
|
switch(bs->ltgtype) {
|
|
case LTG_TEAMHELP:
|
|
break;
|
|
case LTG_TEAMACCOMPANY:
|
|
BotEntityInfo(bs->teammate, &entinfo);
|
|
if ( ( (gametype == GT_CTF || gametype == GT_1FCTF) && EntityCarriesFlag(&entinfo))
|
|
|| ( gametype == GT_HARVESTER && EntityCarriesCubes(&entinfo)) ) {
|
|
teamtask = TEAMTASK_ESCORT;
|
|
}
|
|
else {
|
|
teamtask = TEAMTASK_FOLLOW;
|
|
}
|
|
break;
|
|
case LTG_DEFENDKEYAREA:
|
|
teamtask = TEAMTASK_DEFENSE;
|
|
break;
|
|
case LTG_GETFLAG:
|
|
teamtask = TEAMTASK_OFFENSE;
|
|
break;
|
|
case LTG_RUSHBASE:
|
|
teamtask = TEAMTASK_DEFENSE;
|
|
break;
|
|
case LTG_RETURNFLAG:
|
|
teamtask = TEAMTASK_RETRIEVE;
|
|
break;
|
|
case LTG_CAMP:
|
|
case LTG_CAMPORDER:
|
|
teamtask = TEAMTASK_CAMP;
|
|
break;
|
|
case LTG_PATROL:
|
|
teamtask = TEAMTASK_PATROL;
|
|
break;
|
|
case LTG_GETITEM:
|
|
teamtask = TEAMTASK_PATROL;
|
|
break;
|
|
case LTG_KILL:
|
|
teamtask = TEAMTASK_PATROL;
|
|
break;
|
|
case LTG_HARVEST:
|
|
teamtask = TEAMTASK_OFFENSE;
|
|
break;
|
|
case LTG_ATTACKENEMYBASE:
|
|
teamtask = TEAMTASK_OFFENSE;
|
|
break;
|
|
default:
|
|
teamtask = TEAMTASK_PATROL;
|
|
break;
|
|
}
|
|
BotSetUserInfo(bs, "teamtask", va("%d", teamtask));
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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 a visible 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 its 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 enough 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef MISSIONPACK
|
|
/*
|
|
==================
|
|
Bot1FCTFSeekGoals
|
|
==================
|
|
*/
|
|
void Bot1FCTFSeekGoals(bot_state_t *bs) {
|
|
aas_entityinfo_t entinfo;
|
|
float rnd, l1, l2;
|
|
int c;
|
|
|
|
//when carrying a flag in ctf the bot should rush to the base
|
|
if (Bot1FCTFCarryingFlag(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;
|
|
//get an alternative route goal towards the enemy base
|
|
BotGetAlternateRouteGoal(bs, BotOppositeTeam(bs));
|
|
//
|
|
BotSetTeamStatus(bs);
|
|
BotVoiceChat(bs, -1, VOICECHAT_IHAVEFLAG);
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
//our team has the flag
|
|
if (bs->neutralflagstatus == 1) {
|
|
if (bs->owndecision_time < FloatTime()) {
|
|
// if not already following someone
|
|
if (bs->ltgtype != LTG_TEAMACCOMPANY) {
|
|
//if there is a visible team mate flag carrier
|
|
c = BotTeamFlagCarrierVisible(bs);
|
|
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;
|
|
return;
|
|
}
|
|
}
|
|
//if already a CTF or team goal
|
|
if (bs->ltgtype == LTG_TEAMHELP ||
|
|
bs->ltgtype == LTG_TEAMACCOMPANY ||
|
|
bs->ltgtype == LTG_DEFENDKEYAREA ||
|
|
bs->ltgtype == LTG_GETFLAG ||
|
|
bs->ltgtype == LTG_RUSHBASE ||
|
|
bs->ltgtype == LTG_CAMPORDER ||
|
|
bs->ltgtype == LTG_PATROL ||
|
|
bs->ltgtype == LTG_ATTACKENEMYBASE ||
|
|
bs->ltgtype == LTG_GETITEM ||
|
|
bs->ltgtype == LTG_MAKELOVE_UNDER ||
|
|
bs->ltgtype == LTG_MAKELOVE_ONTOP) {
|
|
return;
|
|
}
|
|
//if not already attacking the enemy base
|
|
if (bs->ltgtype != LTG_ATTACKENEMYBASE) {
|
|
BotRefuseOrder(bs);
|
|
bs->decisionmaker = bs->client;
|
|
bs->ordered = qfalse;
|
|
//
|
|
if (BotTeam(bs) == TEAM_RED) memcpy(&bs->teamgoal, &ctf_blueflag, sizeof(bot_goal_t));
|
|
else memcpy(&bs->teamgoal, &ctf_redflag, sizeof(bot_goal_t));
|
|
//set the ltg type
|
|
bs->ltgtype = LTG_ATTACKENEMYBASE;
|
|
//set the time the bot will stop getting the flag
|
|
bs->teamgoal_time = FloatTime() + TEAM_ATTACKENEMYBASE_TIME;
|
|
BotSetTeamStatus(bs);
|
|
bs->owndecision_time = FloatTime() + 5;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
//enemy team has the flag
|
|
else if (bs->neutralflagstatus == 2) {
|
|
if (bs->owndecision_time < FloatTime()) {
|
|
c = BotEnemyFlagCarrierVisible(bs);
|
|
if (c >= 0) {
|
|
//FIXME: attack enemy flag carrier
|
|
}
|
|
//if already a CTF or team goal
|
|
if (bs->ltgtype == LTG_TEAMHELP ||
|
|
bs->ltgtype == LTG_TEAMACCOMPANY ||
|
|
bs->ltgtype == LTG_CAMPORDER ||
|
|
bs->ltgtype == LTG_PATROL ||
|
|
bs->ltgtype == LTG_GETITEM) {
|
|
return;
|
|
}
|
|
// if not already defending the base
|
|
if (bs->ltgtype != LTG_DEFENDKEYAREA) {
|
|
BotRefuseOrder(bs);
|
|
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);
|
|
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 its 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_ATTACKENEMYBASE ||
|
|
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 enough 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_neutralflag.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;
|
|
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
|
|
}
|
|
|
|
/*
|
|
==================
|
|
Bot1FCTFRetreatGoals
|
|
==================
|
|
*/
|
|
void Bot1FCTFRetreatGoals(bot_state_t *bs) {
|
|
//when carrying a flag in ctf the bot should rush to the enemy base
|
|
if (Bot1FCTFCarryingFlag(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;
|
|
//get an alternative route goal towards the enemy base
|
|
BotGetAlternateRouteGoal(bs, BotOppositeTeam(bs));
|
|
BotSetTeamStatus(bs);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotObeliskSeekGoals
|
|
==================
|
|
*/
|
|
void BotObeliskSeekGoals(bot_state_t *bs) {
|
|
float rnd, l1, l2;
|
|
|
|
// 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 already 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_RETURNFLAG ||
|
|
bs->ltgtype == LTG_CAMPORDER ||
|
|
bs->ltgtype == LTG_PATROL ||
|
|
bs->ltgtype == LTG_ATTACKENEMYBASE ||
|
|
bs->ltgtype == LTG_GETITEM ||
|
|
bs->ltgtype == LTG_MAKELOVE_UNDER ||
|
|
bs->ltgtype == LTG_MAKELOVE_ONTOP) {
|
|
return;
|
|
}
|
|
//
|
|
if (BotSetLastOrderedTask(bs))
|
|
return;
|
|
//if the bot is roaming
|
|
if (bs->ctfroam_time > FloatTime())
|
|
return;
|
|
//if the bot has enough 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 && redobelisk.areanum && blueobelisk.areanum) {
|
|
bs->decisionmaker = bs->client;
|
|
bs->ordered = qfalse;
|
|
//
|
|
if (BotTeam(bs) == TEAM_RED) memcpy(&bs->teamgoal, &blueobelisk, sizeof(bot_goal_t));
|
|
else memcpy(&bs->teamgoal, &redobelisk, sizeof(bot_goal_t));
|
|
//set the ltg type
|
|
bs->ltgtype = LTG_ATTACKENEMYBASE;
|
|
//set the time the bot will stop attacking the enemy base
|
|
bs->teamgoal_time = FloatTime() + TEAM_ATTACKENEMYBASE_TIME;
|
|
//get an alternate route goal towards the enemy base
|
|
BotGetAlternateRouteGoal(bs, BotOppositeTeam(bs));
|
|
BotSetTeamStatus(bs);
|
|
}
|
|
else if (rnd < l2 && redobelisk.areanum && blueobelisk.areanum) {
|
|
bs->decisionmaker = bs->client;
|
|
bs->ordered = qfalse;
|
|
//
|
|
if (BotTeam(bs) == TEAM_RED) memcpy(&bs->teamgoal, &redobelisk, sizeof(bot_goal_t));
|
|
else memcpy(&bs->teamgoal, &blueobelisk, 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);
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotGoHarvest
|
|
==================
|
|
*/
|
|
void BotGoHarvest(bot_state_t *bs) {
|
|
//
|
|
if (BotTeam(bs) == TEAM_RED) memcpy(&bs->teamgoal, &blueobelisk, sizeof(bot_goal_t));
|
|
else memcpy(&bs->teamgoal, &redobelisk, sizeof(bot_goal_t));
|
|
//set the ltg type
|
|
bs->ltgtype = LTG_HARVEST;
|
|
//set the time the bot will stop harvesting
|
|
bs->teamgoal_time = FloatTime() + TEAM_HARVEST_TIME;
|
|
bs->harvestaway_time = 0;
|
|
BotSetTeamStatus(bs);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotObeliskRetreatGoals
|
|
==================
|
|
*/
|
|
void BotObeliskRetreatGoals(bot_state_t *bs) {
|
|
//nothing special
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotHarvesterSeekGoals
|
|
==================
|
|
*/
|
|
void BotHarvesterSeekGoals(bot_state_t *bs) {
|
|
aas_entityinfo_t entinfo;
|
|
float rnd, l1, l2;
|
|
int c;
|
|
|
|
//when carrying cubes in harvester the bot should rush to the base
|
|
if (BotHarvesterCarryingCubes(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;
|
|
//get an alternative route goal towards the enemy base
|
|
BotGetAlternateRouteGoal(bs, BotOppositeTeam(bs));
|
|
//
|
|
BotSetTeamStatus(bs);
|
|
}
|
|
return;
|
|
}
|
|
// don't just do something wait for the bot team leader to give orders
|
|
if (BotTeamLeader(bs)) {
|
|
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 (!EntityCarriesCubes(&entinfo)) {
|
|
bs->ltgtype = 0;
|
|
}
|
|
}
|
|
// if the bot is ordered to do something
|
|
if ( bs->lastgoal_ltgtype ) {
|
|
bs->teamgoal_time += 60;
|
|
}
|
|
//if not yet doing something
|
|
if (bs->ltgtype == LTG_TEAMHELP ||
|
|
bs->ltgtype == LTG_TEAMACCOMPANY ||
|
|
bs->ltgtype == LTG_DEFENDKEYAREA ||
|
|
bs->ltgtype == LTG_GETFLAG ||
|
|
bs->ltgtype == LTG_CAMPORDER ||
|
|
bs->ltgtype == LTG_PATROL ||
|
|
bs->ltgtype == LTG_ATTACKENEMYBASE ||
|
|
bs->ltgtype == LTG_HARVEST ||
|
|
bs->ltgtype == LTG_GETITEM ||
|
|
bs->ltgtype == LTG_MAKELOVE_UNDER ||
|
|
bs->ltgtype == LTG_MAKELOVE_ONTOP) {
|
|
return;
|
|
}
|
|
//
|
|
if (BotSetLastOrderedTask(bs))
|
|
return;
|
|
//if the bot is roaming
|
|
if (bs->ctfroam_time > FloatTime())
|
|
return;
|
|
//if the bot has enough 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();
|
|
//
|
|
c = BotEnemyCubeCarrierVisible(bs);
|
|
if (c >= 0) {
|
|
//FIXME: attack enemy cube carrier
|
|
}
|
|
if (bs->ltgtype != LTG_TEAMACCOMPANY) {
|
|
//if there is a visible team mate carrying cubes
|
|
c = BotTeamCubeCarrierVisible(bs);
|
|
if (c >= 0) {
|
|
//follow the team mate carrying cubes
|
|
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);
|
|
return;
|
|
}
|
|
}
|
|
//
|
|
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;
|
|
}
|
|
//
|
|
rnd = random();
|
|
if (rnd < l1 && redobelisk.areanum && blueobelisk.areanum) {
|
|
bs->decisionmaker = bs->client;
|
|
bs->ordered = qfalse;
|
|
BotGoHarvest(bs);
|
|
}
|
|
else if (rnd < l2 && redobelisk.areanum && blueobelisk.areanum) {
|
|
bs->decisionmaker = bs->client;
|
|
bs->ordered = qfalse;
|
|
//
|
|
if (BotTeam(bs) == TEAM_RED) memcpy(&bs->teamgoal, &redobelisk, sizeof(bot_goal_t));
|
|
else memcpy(&bs->teamgoal, &blueobelisk, 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);
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotHarvesterRetreatGoals
|
|
==================
|
|
*/
|
|
void BotHarvesterRetreatGoals(bot_state_t *bs) {
|
|
//when carrying cubes in harvester the bot should rush to the base
|
|
if (BotHarvesterCarryingCubes(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);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
==================
|
|
BotTeamGoals
|
|
==================
|
|
*/
|
|
void BotTeamGoals(bot_state_t *bs, int retreat) {
|
|
|
|
if ( retreat ) {
|
|
if (gametype == GT_CTF) {
|
|
BotCTFRetreatGoals(bs);
|
|
}
|
|
#ifdef MISSIONPACK
|
|
else if (gametype == GT_1FCTF) {
|
|
Bot1FCTFRetreatGoals(bs);
|
|
}
|
|
else if (gametype == GT_OBELISK) {
|
|
BotObeliskRetreatGoals(bs);
|
|
}
|
|
else if (gametype == GT_HARVESTER) {
|
|
BotHarvesterRetreatGoals(bs);
|
|
}
|
|
#endif
|
|
}
|
|
else {
|
|
if (gametype == GT_CTF) {
|
|
//decide what to do in CTF mode
|
|
BotCTFSeekGoals(bs);
|
|
}
|
|
#ifdef MISSIONPACK
|
|
else if (gametype == GT_1FCTF) {
|
|
Bot1FCTFSeekGoals(bs);
|
|
}
|
|
else if (gametype == GT_OBELISK) {
|
|
BotObeliskSeekGoals(bs);
|
|
}
|
|
else if (gametype == GT_HARVESTER) {
|
|
BotHarvesterSeekGoals(bs);
|
|
}
|
|
#endif
|
|
}
|
|
// 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];
|
|
|
|
for (i = 0; i < level.maxclients; 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];
|
|
|
|
for (i = 0; i < level.maxclients; 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] = {0};
|
|
|
|
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
|
|
#ifdef MISSIONPACK
|
|
|| gametype == GT_1FCTF
|
|
#endif
|
|
) {
|
|
if (BotTeam(bs) == TEAM_RED) context |= CONTEXT_CTFREDTEAM;
|
|
else context |= CONTEXT_CTFBLUETEAM;
|
|
}
|
|
#ifdef MISSIONPACK
|
|
else if (gametype == GT_OBELISK) {
|
|
if (BotTeam(bs) == TEAM_RED) context |= CONTEXT_OBELISKREDTEAM;
|
|
else context |= CONTEXT_OBELISKBLUETEAM;
|
|
}
|
|
else if (gametype == GT_HARVESTER) {
|
|
if (BotTeam(bs) == TEAM_RED) context |= CONTEXT_HARVESTERREDTEAM;
|
|
else context |= CONTEXT_HARVESTERBLUETEAM;
|
|
}
|
|
#endif
|
|
return context;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotChooseWeapon
|
|
==================
|
|
*/
|
|
void BotChooseWeapon(bot_state_t *bs) {
|
|
int newweaponnum;
|
|
|
|
if (bs->cur_ps.weaponstate == WEAPON_RAISING ||
|
|
bs->cur_ps.weaponstate == WEAPON_DROPPING) {
|
|
trap_EA_SelectWeapon(bs->client, bs->weaponnum);
|
|
}
|
|
else {
|
|
newweaponnum = trap_BotChooseBestFightWeapon(bs->ws, bs->inventory);
|
|
if (bs->weaponnum != newweaponnum) bs->weaponchange_time = FloatTime();
|
|
bs->weaponnum = newweaponnum;
|
|
//BotAI_Print(PRT_MESSAGE, "bs->weaponnum = %d\n", bs->weaponnum);
|
|
trap_EA_SelectWeapon(bs->client, bs->weaponnum);
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotSetupForMovement
|
|
==================
|
|
*/
|
|
void BotSetupForMovement(bot_state_t *bs) {
|
|
bot_initmove_t initmove;
|
|
|
|
memset(&initmove, 0, sizeof(bot_initmove_t));
|
|
VectorCopy(bs->cur_ps.origin, initmove.origin);
|
|
VectorCopy(bs->cur_ps.velocity, initmove.velocity);
|
|
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) {
|
|
#ifdef MISSIONPACK
|
|
int offence, leader;
|
|
|
|
if (gametype <= GT_TEAM)
|
|
return;
|
|
|
|
offence = -1;
|
|
// go into offence if picked up the kamikaze or invulnerability
|
|
if (!oldinventory[INVENTORY_KAMIKAZE] && bs->inventory[INVENTORY_KAMIKAZE] >= 1) {
|
|
offence = qtrue;
|
|
}
|
|
if (!oldinventory[INVENTORY_INVULNERABILITY] && bs->inventory[INVENTORY_INVULNERABILITY] >= 1) {
|
|
offence = qtrue;
|
|
}
|
|
// if not already wearing the kamikaze or invulnerability
|
|
if (!bs->inventory[INVENTORY_KAMIKAZE] && !bs->inventory[INVENTORY_INVULNERABILITY]) {
|
|
if (!oldinventory[INVENTORY_SCOUT] && bs->inventory[INVENTORY_SCOUT] >= 1) {
|
|
offence = qtrue;
|
|
}
|
|
if (!oldinventory[INVENTORY_GUARD] && bs->inventory[INVENTORY_GUARD] >= 1) {
|
|
offence = qtrue;
|
|
}
|
|
if (!oldinventory[INVENTORY_DOUBLER] && bs->inventory[INVENTORY_DOUBLER] >= 1) {
|
|
offence = qfalse;
|
|
}
|
|
if (!oldinventory[INVENTORY_AMMOREGEN] && bs->inventory[INVENTORY_AMMOREGEN] >= 1) {
|
|
offence = qfalse;
|
|
}
|
|
}
|
|
|
|
if (offence >= 0) {
|
|
leader = ClientFromName(bs->teamleader);
|
|
if (offence) {
|
|
if (!(bs->teamtaskpreference & TEAMTP_ATTACKER)) {
|
|
// if we have a bot team leader
|
|
if (BotTeamLeader(bs)) {
|
|
// tell the leader we want to be on offence
|
|
BotVoiceChat(bs, leader, VOICECHAT_WANTONOFFENSE);
|
|
//BotAI_BotInitialChat(bs, "wantoffence", NULL);
|
|
//trap_BotEnterChat(bs->cs, leader, CHAT_TELL);
|
|
}
|
|
else if (g_spSkill.integer <= 3) {
|
|
if ( bs->ltgtype != LTG_GETFLAG &&
|
|
bs->ltgtype != LTG_ATTACKENEMYBASE &&
|
|
bs->ltgtype != LTG_HARVEST ) {
|
|
//
|
|
if ((gametype != GT_CTF || (bs->redflagstatus == 0 && bs->blueflagstatus == 0)) &&
|
|
(gametype != GT_1FCTF || bs->neutralflagstatus == 0) ) {
|
|
// tell the leader we want to be on offence
|
|
BotVoiceChat(bs, leader, VOICECHAT_WANTONOFFENSE);
|
|
//BotAI_BotInitialChat(bs, "wantoffence", NULL);
|
|
//trap_BotEnterChat(bs->cs, leader, CHAT_TELL);
|
|
}
|
|
}
|
|
}
|
|
bs->teamtaskpreference |= TEAMTP_ATTACKER;
|
|
}
|
|
bs->teamtaskpreference &= ~TEAMTP_DEFENDER;
|
|
}
|
|
else {
|
|
if (!(bs->teamtaskpreference & TEAMTP_DEFENDER)) {
|
|
// if we have a bot team leader
|
|
if (BotTeamLeader(bs)) {
|
|
// tell the leader we want to be on defense
|
|
BotVoiceChat(bs, -1, VOICECHAT_WANTONDEFENSE);
|
|
//BotAI_BotInitialChat(bs, "wantdefence", NULL);
|
|
//trap_BotEnterChat(bs->cs, leader, CHAT_TELL);
|
|
}
|
|
else if (g_spSkill.integer <= 3) {
|
|
if ( bs->ltgtype != LTG_DEFENDKEYAREA ) {
|
|
//
|
|
if ((gametype != GT_CTF || (bs->redflagstatus == 0 && bs->blueflagstatus == 0)) &&
|
|
(gametype != GT_1FCTF || bs->neutralflagstatus == 0) ) {
|
|
// tell the leader we want to be on defense
|
|
BotVoiceChat(bs, -1, VOICECHAT_WANTONDEFENSE);
|
|
//BotAI_BotInitialChat(bs, "wantdefence", NULL);
|
|
//trap_BotEnterChat(bs->cs, leader, CHAT_TELL);
|
|
}
|
|
}
|
|
}
|
|
bs->teamtaskpreference |= TEAMTP_DEFENDER;
|
|
}
|
|
bs->teamtaskpreference &= ~TEAMTP_ATTACKER;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotUpdateInventory
|
|
==================
|
|
*/
|
|
void BotUpdateInventory(bot_state_t *bs) {
|
|
int oldinventory[MAX_ITEMS];
|
|
|
|
memcpy(oldinventory, bs->inventory, sizeof(oldinventory));
|
|
//armor
|
|
bs->inventory[INVENTORY_ARMOR] = bs->cur_ps.stats[STAT_ARMOR];
|
|
//weapons
|
|
bs->inventory[INVENTORY_GAUNTLET] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_GAUNTLET)) != 0;
|
|
bs->inventory[INVENTORY_SHOTGUN] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_SHOTGUN)) != 0;
|
|
bs->inventory[INVENTORY_MACHINEGUN] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_MACHINEGUN)) != 0;
|
|
bs->inventory[INVENTORY_GRENADELAUNCHER] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_GRENADE_LAUNCHER)) != 0;
|
|
bs->inventory[INVENTORY_ROCKETLAUNCHER] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_ROCKET_LAUNCHER)) != 0;
|
|
bs->inventory[INVENTORY_LIGHTNING] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_LIGHTNING)) != 0;
|
|
bs->inventory[INVENTORY_RAILGUN] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_RAILGUN)) != 0;
|
|
bs->inventory[INVENTORY_PLASMAGUN] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_PLASMAGUN)) != 0;
|
|
bs->inventory[INVENTORY_BFG10K] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_BFG)) != 0;
|
|
bs->inventory[INVENTORY_GRAPPLINGHOOK] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_GRAPPLING_HOOK)) != 0;
|
|
#ifdef MISSIONPACK
|
|
bs->inventory[INVENTORY_NAILGUN] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_NAILGUN)) != 0;;
|
|
bs->inventory[INVENTORY_PROXLAUNCHER] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_PROX_LAUNCHER)) != 0;;
|
|
bs->inventory[INVENTORY_CHAINGUN] = (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_CHAINGUN)) != 0;;
|
|
#endif
|
|
//ammo
|
|
bs->inventory[INVENTORY_SHELLS] = bs->cur_ps.ammo[WP_SHOTGUN];
|
|
bs->inventory[INVENTORY_BULLETS] = bs->cur_ps.ammo[WP_MACHINEGUN];
|
|
bs->inventory[INVENTORY_GRENADES] = bs->cur_ps.ammo[WP_GRENADE_LAUNCHER];
|
|
bs->inventory[INVENTORY_CELLS] = bs->cur_ps.ammo[WP_PLASMAGUN];
|
|
bs->inventory[INVENTORY_LIGHTNINGAMMO] = bs->cur_ps.ammo[WP_LIGHTNING];
|
|
bs->inventory[INVENTORY_ROCKETS] = bs->cur_ps.ammo[WP_ROCKET_LAUNCHER];
|
|
bs->inventory[INVENTORY_SLUGS] = bs->cur_ps.ammo[WP_RAILGUN];
|
|
bs->inventory[INVENTORY_BFGAMMO] = bs->cur_ps.ammo[WP_BFG];
|
|
#ifdef MISSIONPACK
|
|
bs->inventory[INVENTORY_NAILS] = bs->cur_ps.ammo[WP_NAILGUN];
|
|
bs->inventory[INVENTORY_MINES] = bs->cur_ps.ammo[WP_PROX_LAUNCHER];
|
|
bs->inventory[INVENTORY_BELT] = bs->cur_ps.ammo[WP_CHAINGUN];
|
|
#endif
|
|
//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;
|
|
#ifdef MISSIONPACK
|
|
bs->inventory[INVENTORY_KAMIKAZE] = bs->cur_ps.stats[STAT_HOLDABLE_ITEM] == MODELINDEX_KAMIKAZE;
|
|
bs->inventory[INVENTORY_PORTAL] = bs->cur_ps.stats[STAT_HOLDABLE_ITEM] == MODELINDEX_PORTAL;
|
|
bs->inventory[INVENTORY_INVULNERABILITY] = bs->cur_ps.stats[STAT_HOLDABLE_ITEM] == MODELINDEX_INVULNERABILITY;
|
|
#endif
|
|
bs->inventory[INVENTORY_QUAD] = bs->cur_ps.powerups[PW_QUAD] != 0;
|
|
bs->inventory[INVENTORY_ENVIRONMENTSUIT] = bs->cur_ps.powerups[PW_BATTLESUIT] != 0;
|
|
bs->inventory[INVENTORY_HASTE] = bs->cur_ps.powerups[PW_HASTE] != 0;
|
|
bs->inventory[INVENTORY_INVISIBILITY] = bs->cur_ps.powerups[PW_INVIS] != 0;
|
|
bs->inventory[INVENTORY_REGEN] = bs->cur_ps.powerups[PW_REGEN] != 0;
|
|
bs->inventory[INVENTORY_FLIGHT] = bs->cur_ps.powerups[PW_FLIGHT] != 0;
|
|
#ifdef MISSIONPACK
|
|
bs->inventory[INVENTORY_SCOUT] = bs->cur_ps.stats[STAT_PERSISTANT_POWERUP] == MODELINDEX_SCOUT;
|
|
bs->inventory[INVENTORY_GUARD] = bs->cur_ps.stats[STAT_PERSISTANT_POWERUP] == MODELINDEX_GUARD;
|
|
bs->inventory[INVENTORY_DOUBLER] = bs->cur_ps.stats[STAT_PERSISTANT_POWERUP] == MODELINDEX_DOUBLER;
|
|
bs->inventory[INVENTORY_AMMOREGEN] = bs->cur_ps.stats[STAT_PERSISTANT_POWERUP] == MODELINDEX_AMMOREGEN;
|
|
#endif
|
|
bs->inventory[INVENTORY_REDFLAG] = bs->cur_ps.powerups[PW_REDFLAG] != 0;
|
|
bs->inventory[INVENTORY_BLUEFLAG] = bs->cur_ps.powerups[PW_BLUEFLAG] != 0;
|
|
#ifdef MISSIONPACK
|
|
bs->inventory[INVENTORY_NEUTRALFLAG] = bs->cur_ps.powerups[PW_NEUTRALFLAG] != 0;
|
|
if (BotTeam(bs) == TEAM_RED) {
|
|
bs->inventory[INVENTORY_REDCUBE] = bs->cur_ps.generic1;
|
|
bs->inventory[INVENTORY_BLUECUBE] = 0;
|
|
}
|
|
else {
|
|
bs->inventory[INVENTORY_REDCUBE] = 0;
|
|
bs->inventory[INVENTORY_BLUECUBE] = bs->cur_ps.generic1;
|
|
}
|
|
#endif
|
|
BotCheckItemPickup(bs, oldinventory);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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
|
|
}
|
|
|
|
#ifdef MISSIONPACK
|
|
/*
|
|
==================
|
|
BotUseKamikaze
|
|
==================
|
|
*/
|
|
#define KAMIKAZE_DIST 1024
|
|
|
|
void BotUseKamikaze(bot_state_t *bs) {
|
|
int c, teammates, enemies;
|
|
aas_entityinfo_t entinfo;
|
|
vec3_t dir, target;
|
|
bot_goal_t *goal;
|
|
bsp_trace_t trace;
|
|
|
|
//if the bot has no kamikaze
|
|
if (bs->inventory[INVENTORY_KAMIKAZE] <= 0)
|
|
return;
|
|
if (bs->kamikaze_time > FloatTime())
|
|
return;
|
|
bs->kamikaze_time = FloatTime() + 0.2;
|
|
if (gametype == GT_CTF) {
|
|
//never use kamikaze if the team flag carrier is visible
|
|
if (BotCTFCarryingFlag(bs))
|
|
return;
|
|
c = BotTeamFlagCarrierVisible(bs);
|
|
if (c >= 0) {
|
|
BotEntityInfo(c, &entinfo);
|
|
VectorSubtract(entinfo.origin, bs->origin, dir);
|
|
if (VectorLengthSquared(dir) < Square(KAMIKAZE_DIST))
|
|
return;
|
|
}
|
|
c = BotEnemyFlagCarrierVisible(bs);
|
|
if (c >= 0) {
|
|
BotEntityInfo(c, &entinfo);
|
|
VectorSubtract(entinfo.origin, bs->origin, dir);
|
|
if (VectorLengthSquared(dir) < Square(KAMIKAZE_DIST)) {
|
|
trap_EA_Use(bs->client);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else if (gametype == GT_1FCTF) {
|
|
//never use kamikaze if the team flag carrier is visible
|
|
if (Bot1FCTFCarryingFlag(bs))
|
|
return;
|
|
c = BotTeamFlagCarrierVisible(bs);
|
|
if (c >= 0) {
|
|
BotEntityInfo(c, &entinfo);
|
|
VectorSubtract(entinfo.origin, bs->origin, dir);
|
|
if (VectorLengthSquared(dir) < Square(KAMIKAZE_DIST))
|
|
return;
|
|
}
|
|
c = BotEnemyFlagCarrierVisible(bs);
|
|
if (c >= 0) {
|
|
BotEntityInfo(c, &entinfo);
|
|
VectorSubtract(entinfo.origin, bs->origin, dir);
|
|
if (VectorLengthSquared(dir) < Square(KAMIKAZE_DIST)) {
|
|
trap_EA_Use(bs->client);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else if (gametype == GT_OBELISK) {
|
|
switch(BotTeam(bs)) {
|
|
case TEAM_RED: goal = &blueobelisk; break;
|
|
default: goal = &redobelisk; break;
|
|
}
|
|
//if the obelisk is visible
|
|
VectorCopy(goal->origin, target);
|
|
target[2] += 1;
|
|
VectorSubtract(bs->origin, target, dir);
|
|
if (VectorLengthSquared(dir) < Square(KAMIKAZE_DIST * 0.9)) {
|
|
BotAI_Trace(&trace, bs->eye, NULL, NULL, target, bs->client, CONTENTS_SOLID);
|
|
if (trace.fraction >= 1 || trace.ent == goal->entitynum) {
|
|
trap_EA_Use(bs->client);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else if (gametype == GT_HARVESTER) {
|
|
//
|
|
if (BotHarvesterCarryingCubes(bs))
|
|
return;
|
|
//never use kamikaze if a team mate carrying cubes is visible
|
|
c = BotTeamCubeCarrierVisible(bs);
|
|
if (c >= 0) {
|
|
BotEntityInfo(c, &entinfo);
|
|
VectorSubtract(entinfo.origin, bs->origin, dir);
|
|
if (VectorLengthSquared(dir) < Square(KAMIKAZE_DIST))
|
|
return;
|
|
}
|
|
c = BotEnemyCubeCarrierVisible(bs);
|
|
if (c >= 0) {
|
|
BotEntityInfo(c, &entinfo);
|
|
VectorSubtract(entinfo.origin, bs->origin, dir);
|
|
if (VectorLengthSquared(dir) < Square(KAMIKAZE_DIST)) {
|
|
trap_EA_Use(bs->client);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
//
|
|
BotVisibleTeamMatesAndEnemies(bs, &teammates, &enemies, KAMIKAZE_DIST);
|
|
//
|
|
if (enemies > 2 && enemies > teammates+1) {
|
|
trap_EA_Use(bs->client);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotUseInvulnerability
|
|
==================
|
|
*/
|
|
void BotUseInvulnerability(bot_state_t *bs) {
|
|
int c;
|
|
vec3_t dir, target;
|
|
bot_goal_t *goal;
|
|
bsp_trace_t trace;
|
|
|
|
//if the bot has no invulnerability
|
|
if (bs->inventory[INVENTORY_INVULNERABILITY] <= 0)
|
|
return;
|
|
if (bs->invulnerability_time > FloatTime())
|
|
return;
|
|
bs->invulnerability_time = FloatTime() + 0.2;
|
|
if (gametype == GT_CTF) {
|
|
//never use kamikaze if the team flag carrier is visible
|
|
if (BotCTFCarryingFlag(bs))
|
|
return;
|
|
c = BotEnemyFlagCarrierVisible(bs);
|
|
if (c >= 0)
|
|
return;
|
|
//if near enemy flag and the flag is visible
|
|
switch(BotTeam(bs)) {
|
|
case TEAM_RED: goal = &ctf_blueflag; break;
|
|
default: goal = &ctf_redflag; break;
|
|
}
|
|
//if the obelisk is visible
|
|
VectorCopy(goal->origin, target);
|
|
target[2] += 1;
|
|
VectorSubtract(bs->origin, target, dir);
|
|
if (VectorLengthSquared(dir) < Square(200)) {
|
|
BotAI_Trace(&trace, bs->eye, NULL, NULL, target, bs->client, CONTENTS_SOLID);
|
|
if (trace.fraction >= 1 || trace.ent == goal->entitynum) {
|
|
trap_EA_Use(bs->client);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else if (gametype == GT_1FCTF) {
|
|
//never use kamikaze if the team flag carrier is visible
|
|
if (Bot1FCTFCarryingFlag(bs))
|
|
return;
|
|
c = BotEnemyFlagCarrierVisible(bs);
|
|
if (c >= 0)
|
|
return;
|
|
//if near enemy flag and the flag is visible
|
|
switch(BotTeam(bs)) {
|
|
case TEAM_RED: goal = &ctf_blueflag; break;
|
|
default: goal = &ctf_redflag; break;
|
|
}
|
|
//if the obelisk is visible
|
|
VectorCopy(goal->origin, target);
|
|
target[2] += 1;
|
|
VectorSubtract(bs->origin, target, dir);
|
|
if (VectorLengthSquared(dir) < Square(200)) {
|
|
BotAI_Trace(&trace, bs->eye, NULL, NULL, target, bs->client, CONTENTS_SOLID);
|
|
if (trace.fraction >= 1 || trace.ent == goal->entitynum) {
|
|
trap_EA_Use(bs->client);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else if (gametype == GT_OBELISK) {
|
|
switch(BotTeam(bs)) {
|
|
case TEAM_RED: goal = &blueobelisk; break;
|
|
default: goal = &redobelisk; break;
|
|
}
|
|
//if the obelisk is visible
|
|
VectorCopy(goal->origin, target);
|
|
target[2] += 1;
|
|
VectorSubtract(bs->origin, target, dir);
|
|
if (VectorLengthSquared(dir) < Square(300)) {
|
|
BotAI_Trace(&trace, bs->eye, NULL, NULL, target, bs->client, CONTENTS_SOLID);
|
|
if (trace.fraction >= 1 || trace.ent == goal->entitynum) {
|
|
trap_EA_Use(bs->client);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else if (gametype == GT_HARVESTER) {
|
|
//
|
|
if (BotHarvesterCarryingCubes(bs))
|
|
return;
|
|
c = BotEnemyCubeCarrierVisible(bs);
|
|
if (c >= 0)
|
|
return;
|
|
//if near enemy base and enemy base is visible
|
|
switch(BotTeam(bs)) {
|
|
case TEAM_RED: goal = &blueobelisk; break;
|
|
default: goal = &redobelisk; break;
|
|
}
|
|
//if the obelisk is visible
|
|
VectorCopy(goal->origin, target);
|
|
target[2] += 1;
|
|
VectorSubtract(bs->origin, target, dir);
|
|
if (VectorLengthSquared(dir) < Square(200)) {
|
|
BotAI_Trace(&trace, bs->eye, NULL, NULL, target, bs->client, CONTENTS_SOLID);
|
|
if (trace.fraction >= 1 || trace.ent == goal->entitynum) {
|
|
trap_EA_Use(bs->client);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
==================
|
|
BotBattleUseItems
|
|
==================
|
|
*/
|
|
void BotBattleUseItems(bot_state_t *bs) {
|
|
if (bs->inventory[INVENTORY_HEALTH] < 40) {
|
|
if (bs->inventory[INVENTORY_TELEPORTER] > 0) {
|
|
if (!BotCTFCarryingFlag(bs)
|
|
#ifdef MISSIONPACK
|
|
&& !Bot1FCTFCarryingFlag(bs)
|
|
&& !BotHarvesterCarryingCubes(bs)
|
|
#endif
|
|
) {
|
|
trap_EA_Use(bs->client);
|
|
}
|
|
}
|
|
}
|
|
if (bs->inventory[INVENTORY_HEALTH] < 60) {
|
|
if (bs->inventory[INVENTORY_MEDKIT] > 0) {
|
|
trap_EA_Use(bs->client);
|
|
}
|
|
}
|
|
#ifdef MISSIONPACK
|
|
BotUseKamikaze(bs);
|
|
BotUseInvulnerability(bs);
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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 bot has quad
|
|
if (bs->inventory[INVENTORY_QUAD]) {
|
|
//if the bot is not holding the gauntlet or the enemy is really nearby
|
|
if (bs->weaponnum != WP_GAUNTLET ||
|
|
bs->inventory[ENEMY_HORIZONTAL_DIST] < 80) {
|
|
return 70;
|
|
}
|
|
}
|
|
//if the enemy is located way higher than the bot
|
|
if (bs->inventory[ENEMY_HEIGHT] > 200) return 0;
|
|
//if the bot is very low on health
|
|
if (bs->inventory[INVENTORY_HEALTH] < 60) return 0;
|
|
//if the bot is low on health
|
|
if (bs->inventory[INVENTORY_HEALTH] < 80) {
|
|
//if the bot has insufficient armor
|
|
if (bs->inventory[INVENTORY_ARMOR] < 40) return 0;
|
|
}
|
|
//if the bot can use the bfg
|
|
if (bs->inventory[INVENTORY_BFG10K] > 0 &&
|
|
bs->inventory[INVENTORY_BFGAMMO] > 7) return 100;
|
|
//if the bot can use the railgun
|
|
if (bs->inventory[INVENTORY_RAILGUN] > 0 &&
|
|
bs->inventory[INVENTORY_SLUGS] > 5) return 95;
|
|
//if the bot can use the lightning gun
|
|
if (bs->inventory[INVENTORY_LIGHTNING] > 0 &&
|
|
bs->inventory[INVENTORY_LIGHTNINGAMMO] > 50) return 90;
|
|
//if the bot can use the rocketlauncher
|
|
if (bs->inventory[INVENTORY_ROCKETLAUNCHER] > 0 &&
|
|
bs->inventory[INVENTORY_ROCKETS] > 5) return 90;
|
|
//if the bot can use the plasmagun
|
|
if (bs->inventory[INVENTORY_PLASMAGUN] > 0 &&
|
|
bs->inventory[INVENTORY_CELLS] > 40) return 85;
|
|
//if the bot can use the grenade launcher
|
|
if (bs->inventory[INVENTORY_GRENADELAUNCHER] > 0 &&
|
|
bs->inventory[INVENTORY_GRENADES] > 10) 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_GAUNTLET) {
|
|
return 100;
|
|
}
|
|
if (bs->inventory[INVENTORY_HEALTH] < 40) {
|
|
return 100;
|
|
}
|
|
if (bs->weaponnum == WP_MACHINEGUN) {
|
|
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;
|
|
}
|
|
#ifdef MISSIONPACK
|
|
else if (gametype == GT_1FCTF) {
|
|
//if carrying the flag then always retreat
|
|
if (Bot1FCTFCarryingFlag(bs))
|
|
return qtrue;
|
|
}
|
|
else if (gametype == GT_OBELISK) {
|
|
//the bots should be dedicated to attacking the enemy obelisk
|
|
if (bs->ltgtype == LTG_ATTACKENEMYBASE) {
|
|
if (bs->enemy != redobelisk.entitynum &&
|
|
bs->enemy != blueobelisk.entitynum) {
|
|
return qtrue;
|
|
}
|
|
}
|
|
if (BotFeelingBad(bs) > 50) {
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
else if (gametype == GT_HARVESTER) {
|
|
//if carrying cubes then always retreat
|
|
if (BotHarvesterCarryingCubes(bs)) return qtrue;
|
|
}
|
|
#endif
|
|
//
|
|
if (bs->enemy >= 0) {
|
|
BotEntityInfo(bs->enemy, &entinfo);
|
|
// if the enemy is carrying a flag
|
|
if (EntityCarriesFlag(&entinfo)) return qfalse;
|
|
#ifdef MISSIONPACK
|
|
// if the enemy is carrying cubes
|
|
if (EntityCarriesCubes(&entinfo)) return qfalse;
|
|
#endif
|
|
}
|
|
//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;
|
|
}
|
|
#ifdef MISSIONPACK
|
|
else if (gametype == GT_1FCTF) {
|
|
//never chase if carrying the flag
|
|
if (Bot1FCTFCarryingFlag(bs))
|
|
return qfalse;
|
|
//always chase if the enemy is carrying a flag
|
|
BotEntityInfo(bs->enemy, &entinfo);
|
|
if (EntityCarriesFlag(&entinfo))
|
|
return qtrue;
|
|
}
|
|
else if (gametype == GT_OBELISK) {
|
|
//the bots should be dedicated to attacking the enemy obelisk
|
|
if (bs->ltgtype == LTG_ATTACKENEMYBASE) {
|
|
if (bs->enemy != redobelisk.entitynum &&
|
|
bs->enemy != blueobelisk.entitynum) {
|
|
return qfalse;
|
|
}
|
|
}
|
|
}
|
|
else if (gametype == GT_HARVESTER) {
|
|
//never chase if carrying cubes
|
|
if (BotHarvesterCarryingCubes(bs)) return qfalse;
|
|
|
|
BotEntityInfo(bs->enemy, &entinfo);
|
|
// always chase if the enemy is carrying cubes
|
|
if (EntityCarriesCubes(&entinfo)) return qtrue;
|
|
}
|
|
#endif
|
|
//if the bot is getting the flag
|
|
if (bs->ltgtype == LTG_GETFLAG)
|
|
return qfalse;
|
|
//
|
|
if (BotAggression(bs) > 50)
|
|
return qtrue;
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotWantsToHelp
|
|
==================
|
|
*/
|
|
int BotWantsToHelp(bot_state_t *bs) {
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotCanAndWantsToRocketJump
|
|
==================
|
|
*/
|
|
int BotCanAndWantsToRocketJump(bot_state_t *bs) {
|
|
float rocketjumper;
|
|
|
|
//if rocket jumping is disabled
|
|
if (!bot_rocketjump.integer) return qfalse;
|
|
//if no rocket launcher
|
|
if (bs->inventory[INVENTORY_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) {
|
|
#ifdef MISSIONPACK
|
|
// if the bot does not have a persistant powerup
|
|
if (!bs->inventory[INVENTORY_SCOUT] &&
|
|
!bs->inventory[INVENTORY_GUARD] &&
|
|
!bs->inventory[INVENTORY_DOUBLER] &&
|
|
!bs->inventory[INVENTORY_AMMOREGEN] ) {
|
|
return qfalse;
|
|
}
|
|
#endif
|
|
//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_BFG10K] > 0 &&
|
|
bs->inventory[INVENTORY_BFGAMMO] > 7) return qtrue;
|
|
//if the bot can use the railgun
|
|
if (bs->inventory[INVENTORY_RAILGUN] > 0 &&
|
|
bs->inventory[INVENTORY_SLUGS] > 5) return qtrue;
|
|
//if the bot can use the lightning gun
|
|
if (bs->inventory[INVENTORY_LIGHTNING] > 0 &&
|
|
bs->inventory[INVENTORY_LIGHTNINGAMMO] > 50) return qtrue;
|
|
//if the bot can use the rocketlauncher
|
|
if (bs->inventory[INVENTORY_ROCKETLAUNCHER] > 0 &&
|
|
bs->inventory[INVENTORY_ROCKETS] > 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 enough
|
|
if (BotAggression(bs) < 50) return qfalse;
|
|
//the bot should have at least have the rocket launcher, the railgun or the bfg10k with some ammo
|
|
if ((bs->inventory[INVENTORY_ROCKETLAUNCHER] <= 0 || bs->inventory[INVENTORY_ROCKETS] < 10) &&
|
|
(bs->inventory[INVENTORY_RAILGUN] <= 0 || bs->inventory[INVENTORY_SLUGS] < 10) &&
|
|
(bs->inventory[INVENTORY_BFG10K] <= 0 || bs->inventory[INVENTORY_BFGAMMO] < 10)) {
|
|
return qfalse;
|
|
}
|
|
//find the closest camp spot
|
|
besttraveltime = 99999;
|
|
for (cs = trap_BotGetNextCampSpotGoal(0, &goal); cs; cs = trap_BotGetNextCampSpotGoal(cs, &goal)) {
|
|
traveltime = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, goal.areanum, 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
|
|
BotDontAvoid(bs, "Quad Damage");
|
|
BotDontAvoid(bs, "Regeneration");
|
|
BotDontAvoid(bs, "Battle Suit");
|
|
BotDontAvoid(bs, "Speed");
|
|
BotDontAvoid(bs, "Invisibility");
|
|
//BotDontAvoid(bs, "Flight");
|
|
//reset the long term goal time so the bot will go for the powerup
|
|
//NOTE: the long term goal type doesn't change
|
|
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 enough
|
|
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);
|
|
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_GAUNTLET) {
|
|
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) {
|
|
|
|
if (bs->client < 0 || bs->client >= MAX_CLIENTS) {
|
|
return qfalse;
|
|
}
|
|
|
|
if (entnum < 0 || entnum >= MAX_CLIENTS) {
|
|
return qfalse;
|
|
}
|
|
|
|
if (gametype >= GT_TEAM) {
|
|
if (level.clients[bs->client].sess.sessionTeam == level.clients[entnum].sess.sessionTeam) 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;
|
|
|
|
BotEntityInfo(ent, &entinfo);
|
|
if (!entinfo.valid) {
|
|
return 0;
|
|
}
|
|
|
|
//calculate middle of bounding box
|
|
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;
|
|
//note: trace.contents is always 0, see BotAI_Trace
|
|
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;
|
|
}
|
|
#ifdef MISSIONPACK
|
|
if (gametype == GT_OBELISK) {
|
|
vec3_t target;
|
|
bot_goal_t *goal;
|
|
bsp_trace_t trace;
|
|
|
|
if (BotTeam(bs) == TEAM_RED)
|
|
goal = &blueobelisk;
|
|
else
|
|
goal = &redobelisk;
|
|
//if the obelisk is visible
|
|
VectorCopy(goal->origin, target);
|
|
target[2] += 1;
|
|
BotAI_Trace(&trace, bs->eye, NULL, NULL, target, bs->client, CONTENTS_SOLID);
|
|
if (trace.fraction >= 1 || trace.ent == goal->entitynum) {
|
|
if (goal->entitynum == bs->enemy) {
|
|
return qfalse;
|
|
}
|
|
bs->enemy = goal->entitynum;
|
|
bs->enemysight_time = FloatTime();
|
|
bs->enemysuicide = qfalse;
|
|
bs->enemydeath_time = 0;
|
|
bs->enemyvisible_time = FloatTime();
|
|
return qtrue;
|
|
}
|
|
}
|
|
#endif
|
|
//
|
|
for (i = 0; i < level.maxclients; i++) {
|
|
|
|
if (i == bs->client) continue;
|
|
//if it's the current enemy
|
|
if (i == curenemy) continue;
|
|
//if the enemy has targeting disabled
|
|
if (g_entities[i].flags & FL_NOTARGET) {
|
|
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;
|
|
}
|
|
//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 < level.maxclients; 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 < level.maxclients; 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 < level.maxclients; 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 < level.maxclients; 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)++;
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef MISSIONPACK
|
|
/*
|
|
==================
|
|
BotTeamCubeCarrierVisible
|
|
==================
|
|
*/
|
|
int BotTeamCubeCarrierVisible(bot_state_t *bs) {
|
|
int i;
|
|
float vis;
|
|
aas_entityinfo_t entinfo;
|
|
|
|
for (i = 0; i < level.maxclients; 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 (!EntityCarriesCubes(&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;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotEnemyCubeCarrierVisible
|
|
==================
|
|
*/
|
|
int BotEnemyCubeCarrierVisible(bot_state_t *bs) {
|
|
int i;
|
|
float vis;
|
|
aas_entityinfo_t entinfo;
|
|
|
|
for (i = 0; i < level.maxclients; 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 (!EntityCarriesCubes(&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;
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
==================
|
|
BotAimAtEnemy
|
|
==================
|
|
*/
|
|
void BotAimAtEnemy(bot_state_t *bs) {
|
|
int i, enemyvisible;
|
|
float dist, f, aim_skill, aim_accuracy, speed, reactiontime;
|
|
vec3_t dir, bestorigin, end, start, groundtarget, cmdmove, enemyvelocity;
|
|
vec3_t mins = {-4,-4,-4}, maxs = {4, 4, 4};
|
|
weaponinfo_t wi;
|
|
aas_entityinfo_t entinfo;
|
|
bot_goal_t goal;
|
|
bsp_trace_t trace;
|
|
vec3_t target;
|
|
|
|
//if the bot has no enemy
|
|
if (bs->enemy < 0) {
|
|
return;
|
|
}
|
|
//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);
|
|
#ifdef MISSIONPACK
|
|
// if attacking an obelisk
|
|
if ( bs->enemy == redobelisk.entitynum ||
|
|
bs->enemy == blueobelisk.entitynum ) {
|
|
target[2] += 32;
|
|
}
|
|
#endif
|
|
//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);
|
|
//get the weapon specific aim accuracy and or aim skill
|
|
if (wi.number == WP_MACHINEGUN) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_MACHINEGUN, 0, 1);
|
|
}
|
|
else if (wi.number == WP_SHOTGUN) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_SHOTGUN, 0, 1);
|
|
}
|
|
else if (wi.number == WP_GRENADE_LAUNCHER) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_GRENADELAUNCHER, 0, 1);
|
|
aim_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_GRENADELAUNCHER, 0, 1);
|
|
}
|
|
else if (wi.number == WP_ROCKET_LAUNCHER) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_ROCKETLAUNCHER, 0, 1);
|
|
aim_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_ROCKETLAUNCHER, 0, 1);
|
|
}
|
|
else if (wi.number == WP_LIGHTNING) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_LIGHTNING, 0, 1);
|
|
}
|
|
else if (wi.number == WP_RAILGUN) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_RAILGUN, 0, 1);
|
|
}
|
|
else if (wi.number == WP_PLASMAGUN) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_PLASMAGUN, 0, 1);
|
|
aim_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_PLASMAGUN, 0, 1);
|
|
}
|
|
else if (wi.number == WP_BFG) {
|
|
aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_BFG10K, 0, 1);
|
|
aim_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_BFG10K, 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 enough 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 enough the ground target
|
|
if (VectorLengthSquared(dir) < Square(60)) {
|
|
VectorSubtract(trace.endpos, start, dir);
|
|
//if the hitpoint is far enough 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 enough
|
|
if (aim_skill > 0.5) {
|
|
//do prediction shots around corners
|
|
if (wi.number == WP_BFG ||
|
|
wi.number == WP_ROCKET_LAUNCHER ||
|
|
wi.number == WP_GRENADE_LAUNCHER) {
|
|
//create the chase goal
|
|
goal.entitynum = bs->client;
|
|
goal.areanum = bs->areanum;
|
|
VectorCopy(bs->eye, goal.origin);
|
|
VectorSet(goal.mins, -8, -8, -8);
|
|
VectorSet(goal.maxs, 8, 8, 8);
|
|
//
|
|
if (trap_BotPredictVisiblePosition(bs->lastenemyorigin, bs->lastenemyareanum, &goal, TFL_DEFAULT, target)) {
|
|
VectorSubtract(target, bs->eye, dir);
|
|
if (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);
|
|
//
|
|
if (wi.number == WP_MACHINEGUN ||
|
|
wi.number == WP_SHOTGUN ||
|
|
wi.number == WP_LIGHTNING ||
|
|
wi.number == WP_RAILGUN) {
|
|
//distance towards the enemy
|
|
dist = VectorLength(dir);
|
|
if (dist > 150) dist = 150;
|
|
f = 0.6 + dist / 150 * 0.4;
|
|
aim_accuracy *= f;
|
|
}
|
|
//add some random stuff to the aim direction depending on the aim accuracy
|
|
if (aim_accuracy < 0.8) {
|
|
VectorNormalize(dir);
|
|
for (i = 0; i < 3; i++) dir[i] += 0.3 * crandom() * (1 - aim_accuracy);
|
|
}
|
|
//set the ideal view angles
|
|
vectoangles(dir, bs->ideal_viewangles);
|
|
//take the weapon spread into account for lower skilled bots
|
|
bs->ideal_viewangles[PITCH] += 6 * wi.vspread * crandom() * (1 - aim_accuracy);
|
|
bs->ideal_viewangles[PITCH] = AngleMod(bs->ideal_viewangles[PITCH]);
|
|
bs->ideal_viewangles[YAW] += 6 * wi.hspread * crandom() * (1 - aim_accuracy);
|
|
bs->ideal_viewangles[YAW] = AngleMod(bs->ideal_viewangles[YAW]);
|
|
//if the bots should be really challenging
|
|
if (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) {
|
|
#ifdef MISSIONPACK
|
|
// if attacking an obelisk
|
|
if ( entinfo.number == redobelisk.entitynum ||
|
|
entinfo.number == blueobelisk.entitynum ) {
|
|
// if obelisk is respawning return
|
|
if ( g_entities[entinfo.number].activator &&
|
|
g_entities[entinfo.number].activator->s.frame == 2 ) {
|
|
return;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
//
|
|
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
|
|
if (bs->weaponchange_time > FloatTime() - 0.1) 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_GAUNTLET) {
|
|
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;
|
|
|
|
//get the weapon info
|
|
trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi);
|
|
//get the start point shooting from
|
|
VectorCopy(bs->origin, start);
|
|
start[2] += bs->cur_ps.viewheight;
|
|
AngleVectors(bs->viewangles, forward, right, NULL);
|
|
start[0] += forward[0] * wi.offset[0] + right[0] * wi.offset[1];
|
|
start[1] += forward[1] * wi.offset[0] + right[1] * wi.offset[1];
|
|
start[2] += forward[2] * wi.offset[0] + right[2] * wi.offset[1] + wi.offset[2];
|
|
//end point aiming at
|
|
VectorMA(start, 1000, forward, end);
|
|
//a little back to make sure not inside a very close enemy
|
|
VectorMA(start, -12, forward, start);
|
|
BotAI_Trace(&trace, start, mins, maxs, end, bs->entitynum, MASK_SHOT);
|
|
//if 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) {
|
|
trap_EA_Attack(bs->client);
|
|
}
|
|
}
|
|
else {
|
|
trap_EA_Attack(bs->client);
|
|
}
|
|
bs->flags ^= BFL_ATTACKED;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotMapScripts
|
|
==================
|
|
*/
|
|
void BotMapScripts(bot_state_t *bs) {
|
|
char info[1024];
|
|
char mapname[128];
|
|
int i, shootbutton;
|
|
float aim_accuracy;
|
|
aas_entityinfo_t entinfo;
|
|
vec3_t dir;
|
|
|
|
trap_GetServerinfo(info, sizeof(info));
|
|
|
|
strncpy(mapname, Info_ValueForKey( info, "mapname" ), sizeof(mapname)-1);
|
|
mapname[sizeof(mapname)-1] = '\0';
|
|
|
|
if (!Q_stricmp(mapname, "q3tourney6")) {
|
|
vec3_t mins = {700, 204, 672}, maxs = {964, 468, 680};
|
|
vec3_t buttonorg = {304, 352, 920};
|
|
//NOTE: NEVER use the func_bobbing in q3tourney6
|
|
bs->tfl &= ~TFL_FUNCBOB;
|
|
//if the bot is below the bounding box
|
|
if (bs->origin[0] > mins[0] && bs->origin[0] < maxs[0]) {
|
|
if (bs->origin[1] > mins[1] && bs->origin[1] < maxs[1]) {
|
|
if (bs->origin[2] < mins[2]) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
shootbutton = qfalse;
|
|
//if an enemy is below this bounding box then shoot the button
|
|
for (i = 0; i < level.maxclients; 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)) {
|
|
trap_EA_Attack(bs->client);
|
|
}
|
|
}
|
|
}
|
|
else if (!Q_stricmp(mapname, "mpq3tourney6")) {
|
|
//NOTE: NEVER use the func_bobbing in mpq3tourney6
|
|
bs->tfl &= ~TFL_FUNCBOB;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotSetMovedir
|
|
==================
|
|
*/
|
|
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;
|
|
|
|
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 ( 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;
|
|
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;
|
|
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;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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;
|
|
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;
|
|
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, 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
|
|
if (!strcmp(classname, "func_door")) {
|
|
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
|
|
if ( spawnflags & 1 )
|
|
return 0;
|
|
//get the door origin
|
|
if (!trap_AAS_VectorForBSPEpairKey(ent, "origin", origin)) {
|
|
VectorClear(origin);
|
|
}
|
|
//if the door is open or opening already
|
|
if (!VectorCompare(origin, entinfo.origin))
|
|
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) {
|
|
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];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 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) {
|
|
#ifdef OBSTACLEDEBUG
|
|
char netname[MAX_NETNAME];
|
|
#endif
|
|
int movetype, bspent;
|
|
vec3_t hordir, sideward, angles, up = {0, 0, 1};
|
|
//vec3_t start, end, mins, maxs;
|
|
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);
|
|
#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 its 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 1 second 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;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotCheckConsoleMessages
|
|
==================
|
|
*/
|
|
void BotCheckConsoleMessages(bot_state_t *bs) {
|
|
char botname[MAX_NETNAME], message[MAX_MESSAGE_SIZE], netname[MAX_NETNAME], *ptr;
|
|
float chat_reply;
|
|
int context, handle;
|
|
bot_consolemessage_t m;
|
|
bot_match_t match;
|
|
|
|
//the name of this bot
|
|
ClientName(bs->client, botname, sizeof(botname));
|
|
//
|
|
while((handle = trap_BotNextConsoleMessage(bs->cs, &m)) != 0) {
|
|
//if the chat state is flooded with messages the bot will read them quickly
|
|
if (trap_BotNumConsoleMessages(bs->cs) < 10) {
|
|
//if it is a chat message the bot needs some time to read it
|
|
if (m.type == CMS_CHAT && m.time > FloatTime() - (1 + random())) break;
|
|
}
|
|
//
|
|
ptr = m.message;
|
|
//if it is a chat message then don't unify white spaces and don't
|
|
//replace synonyms in the netname
|
|
if (m.type == CMS_CHAT) {
|
|
//
|
|
if (trap_BotFindMatch(m.message, &match, MTCONTEXT_REPLYCHAT)) {
|
|
ptr = m.message + match.variables[MESSAGE].offset;
|
|
}
|
|
}
|
|
//unify the white spaces in the message
|
|
trap_UnifyWhiteSpaces(ptr);
|
|
//replace synonyms in the right context
|
|
context = 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_LAUNCHER)
|
|
return;
|
|
// try to avoid the grenade
|
|
trap_BotAddAvoidSpot(bs->ms, state->pos.trBase, 160, AVOID_ALWAYS);
|
|
}
|
|
|
|
#ifdef MISSIONPACK
|
|
/*
|
|
==================
|
|
BotCheckForProxMines
|
|
==================
|
|
*/
|
|
void BotCheckForProxMines(bot_state_t *bs, entityState_t *state) {
|
|
// if this is not a prox mine
|
|
if (state->eType != ET_MISSILE || state->weapon != WP_PROX_LAUNCHER)
|
|
return;
|
|
// if this prox mine is from someone on our own team
|
|
if (state->generic1 == BotTeam(bs))
|
|
return;
|
|
// if the bot doesn't have a weapon to deactivate the mine
|
|
if (!(bs->inventory[INVENTORY_PLASMAGUN] > 0 && bs->inventory[INVENTORY_CELLS] > 0) &&
|
|
!(bs->inventory[INVENTORY_ROCKETLAUNCHER] > 0 && bs->inventory[INVENTORY_ROCKETS] > 0) &&
|
|
!(bs->inventory[INVENTORY_BFG10K] > 0 && bs->inventory[INVENTORY_BFGAMMO] > 0) ) {
|
|
return;
|
|
}
|
|
// try to avoid the prox mine
|
|
trap_BotAddAvoidSpot(bs->ms, state->pos.trBase, 160, AVOID_ALWAYS);
|
|
//
|
|
if (bs->numproxmines >= MAX_PROXMINES)
|
|
return;
|
|
bs->proxmines[bs->numproxmines] = state->number;
|
|
bs->numproxmines++;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotCheckForKamikazeBody
|
|
==================
|
|
*/
|
|
void BotCheckForKamikazeBody(bot_state_t *bs, entityState_t *state) {
|
|
// if this entity is not wearing the kamikaze
|
|
if (!(state->eFlags & EF_KAMIKAZE))
|
|
return;
|
|
// if this entity isn't dead
|
|
if (!(state->eFlags & EF_DEAD))
|
|
return;
|
|
//remember this kamikaze body
|
|
bs->kamikazebody = state->number;
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
==================
|
|
BotCheckEvents
|
|
==================
|
|
*/
|
|
void BotCheckEvents(bot_state_t *bs, entityState_t *state) {
|
|
int event;
|
|
char buf[128];
|
|
#ifdef MISSIONPACK
|
|
aas_entityinfo_t entinfo;
|
|
#endif
|
|
|
|
//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;
|
|
}
|
|
//
|
|
#ifdef MISSIONPACK
|
|
if (gametype == GT_1FCTF) {
|
|
//
|
|
BotEntityInfo(target, &entinfo);
|
|
if ( entinfo.powerups & ( 1 << PW_NEUTRALFLAG ) ) {
|
|
if (!BotSameTeam(bs, target)) {
|
|
bs->neutralflagstatus = 3; //enemy dropped the flag
|
|
bs->flagstatuschanged = qtrue;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
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*/
|
|
#ifdef MISSIONPACK
|
|
if (!strcmp(buf, "sound/items/kamikazerespawn.wav" )) {
|
|
//the kamikaze respawned so dont avoid it
|
|
BotDontAvoid(bs, "Kamikaze");
|
|
}
|
|
else
|
|
#endif
|
|
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
|
|
}
|
|
}
|
|
#ifdef MISSIONPACK
|
|
else if (gametype == GT_1FCTF) {
|
|
switch(state->eventParm) {
|
|
case GTS_RED_CAPTURE:
|
|
bs->neutralflagstatus = 0;
|
|
bs->flagstatuschanged = qtrue;
|
|
break;
|
|
case GTS_BLUE_CAPTURE:
|
|
bs->neutralflagstatus = 0;
|
|
bs->flagstatuschanged = qtrue;
|
|
break;
|
|
case GTS_RED_RETURN:
|
|
//flag has returned
|
|
bs->neutralflagstatus = 0;
|
|
bs->flagstatuschanged = qtrue;
|
|
break;
|
|
case GTS_BLUE_RETURN:
|
|
//flag has returned
|
|
bs->neutralflagstatus = 0;
|
|
bs->flagstatuschanged = qtrue;
|
|
break;
|
|
case GTS_RED_TAKEN:
|
|
bs->neutralflagstatus = BotTeam(bs) == TEAM_RED ? 2 : 1; //FIXME: check Team_TakeFlagSound in g_team.c
|
|
bs->flagstatuschanged = qtrue;
|
|
break;
|
|
case GTS_BLUE_TAKEN:
|
|
bs->neutralflagstatus = BotTeam(bs) == TEAM_BLUE ? 2 : 1; //FIXME: check Team_TakeFlagSound in g_team.c
|
|
bs->flagstatuschanged = qtrue;
|
|
break;
|
|
}
|
|
}
|
|
#endif
|
|
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_FOOTSPLASH:
|
|
case EV_FOOTWADE:
|
|
case EV_SWIM:
|
|
case EV_FALL_SHORT:
|
|
case EV_FALL_MEDIUM:
|
|
case EV_FALL_FAR:
|
|
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:
|
|
case EV_USE_ITEM15:
|
|
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);
|
|
//
|
|
#ifdef MISSIONPACK
|
|
//check for proximity mines which the bot should deactivate
|
|
BotCheckForProxMines(bs, &state);
|
|
//check for dead bodies with the kamikaze effect which should be gibbed
|
|
BotCheckForKamikazeBody(bs, &state);
|
|
#endif
|
|
}
|
|
//check the player state for events
|
|
BotAI_GetEntityState(bs->client, &state);
|
|
//copy the player state events to the entity state
|
|
state.event = bs->cur_ps.externalEvent;
|
|
state.eventParm = bs->cur_ps.externalEventParm;
|
|
//
|
|
BotCheckEvents(bs, &state);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotCheckAir
|
|
==================
|
|
*/
|
|
void BotCheckAir(bot_state_t *bs) {
|
|
if (bs->inventory[INVENTORY_ENVIRONMENTSUIT] <= 0) {
|
|
if (trap_AAS_PointContents(bs->eye) & (CONTENTS_WATER|CONTENTS_SLIME|CONTENTS_LAVA)) {
|
|
return;
|
|
}
|
|
}
|
|
bs->lastair_time = 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;
|
|
#ifdef MISSIONPACK
|
|
if (gametype == GT_CTF) {
|
|
if (trap_BotGetLevelItemGoal(-1, "Neutral Flag", &ctf_neutralflag) < 0)
|
|
BotAI_Print(PRT_WARNING, "No alt routes without Neutral Flag\n");
|
|
if (ctf_neutralflag.areanum) {
|
|
//
|
|
red_numaltroutegoals = trap_AAS_AlternativeRouteGoals(
|
|
ctf_neutralflag.origin, ctf_neutralflag.areanum,
|
|
ctf_redflag.origin, ctf_redflag.areanum, TFL_DEFAULT,
|
|
red_altroutegoals, MAX_ALTROUTEGOALS,
|
|
ALTROUTEGOAL_CLUSTERPORTALS|
|
|
ALTROUTEGOAL_VIEWPORTALS);
|
|
blue_numaltroutegoals = trap_AAS_AlternativeRouteGoals(
|
|
ctf_neutralflag.origin, ctf_neutralflag.areanum,
|
|
ctf_blueflag.origin, ctf_blueflag.areanum, TFL_DEFAULT,
|
|
blue_altroutegoals, MAX_ALTROUTEGOALS,
|
|
ALTROUTEGOAL_CLUSTERPORTALS|
|
|
ALTROUTEGOAL_VIEWPORTALS);
|
|
}
|
|
}
|
|
else if (gametype == GT_1FCTF) {
|
|
if (trap_BotGetLevelItemGoal(-1, "Neutral Obelisk", &neutralobelisk) < 0)
|
|
BotAI_Print(PRT_WARNING, "One Flag CTF without Neutral Obelisk\n");
|
|
red_numaltroutegoals = trap_AAS_AlternativeRouteGoals(
|
|
ctf_neutralflag.origin, ctf_neutralflag.areanum,
|
|
ctf_redflag.origin, ctf_redflag.areanum, TFL_DEFAULT,
|
|
red_altroutegoals, MAX_ALTROUTEGOALS,
|
|
ALTROUTEGOAL_CLUSTERPORTALS|
|
|
ALTROUTEGOAL_VIEWPORTALS);
|
|
blue_numaltroutegoals = trap_AAS_AlternativeRouteGoals(
|
|
ctf_neutralflag.origin, ctf_neutralflag.areanum,
|
|
ctf_blueflag.origin, ctf_blueflag.areanum, TFL_DEFAULT,
|
|
blue_altroutegoals, MAX_ALTROUTEGOALS,
|
|
ALTROUTEGOAL_CLUSTERPORTALS|
|
|
ALTROUTEGOAL_VIEWPORTALS);
|
|
}
|
|
else if (gametype == GT_OBELISK) {
|
|
if (trap_BotGetLevelItemGoal(-1, "Neutral Obelisk", &neutralobelisk) < 0)
|
|
BotAI_Print(PRT_WARNING, "No alt routes without Neutral Obelisk\n");
|
|
//
|
|
red_numaltroutegoals = trap_AAS_AlternativeRouteGoals(
|
|
neutralobelisk.origin, neutralobelisk.areanum,
|
|
redobelisk.origin, redobelisk.areanum, TFL_DEFAULT,
|
|
red_altroutegoals, MAX_ALTROUTEGOALS,
|
|
ALTROUTEGOAL_CLUSTERPORTALS|
|
|
ALTROUTEGOAL_VIEWPORTALS);
|
|
blue_numaltroutegoals = trap_AAS_AlternativeRouteGoals(
|
|
neutralobelisk.origin, neutralobelisk.areanum,
|
|
blueobelisk.origin, blueobelisk.areanum, TFL_DEFAULT,
|
|
blue_altroutegoals, MAX_ALTROUTEGOALS,
|
|
ALTROUTEGOAL_CLUSTERPORTALS|
|
|
ALTROUTEGOAL_VIEWPORTALS);
|
|
}
|
|
else if (gametype == GT_HARVESTER) {
|
|
if (trap_BotGetLevelItemGoal(-1, "Neutral Obelisk", &neutralobelisk) < 0)
|
|
BotAI_Print(PRT_WARNING, "Harvester without Neutral Obelisk\n");
|
|
red_numaltroutegoals = trap_AAS_AlternativeRouteGoals(
|
|
neutralobelisk.origin, neutralobelisk.areanum,
|
|
redobelisk.origin, redobelisk.areanum, TFL_DEFAULT,
|
|
red_altroutegoals, MAX_ALTROUTEGOALS,
|
|
ALTROUTEGOAL_CLUSTERPORTALS|
|
|
ALTROUTEGOAL_VIEWPORTALS);
|
|
blue_numaltroutegoals = trap_AAS_AlternativeRouteGoals(
|
|
neutralobelisk.origin, neutralobelisk.areanum,
|
|
blueobelisk.origin, blueobelisk.areanum, TFL_DEFAULT,
|
|
blue_altroutegoals, MAX_ALTROUTEGOALS,
|
|
ALTROUTEGOAL_CLUSTERPORTALS|
|
|
ALTROUTEGOAL_VIEWPORTALS);
|
|
}
|
|
#endif
|
|
altroutegoals_setup = qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotDeathmatchAI
|
|
==================
|
|
*/
|
|
void BotDeathmatchAI(bot_state_t *bs, float thinktime) {
|
|
char gender[144], name[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 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");
|
|
|
|
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, "Red Flag", &ctf_redflag) < 0)
|
|
BotAI_Print(PRT_WARNING, "CTF without Red Flag\n");
|
|
if (trap_BotGetLevelItemGoal(-1, "Blue Flag", &ctf_blueflag) < 0)
|
|
BotAI_Print(PRT_WARNING, "CTF without Blue Flag\n");
|
|
}
|
|
#ifdef MISSIONPACK
|
|
else if (gametype == GT_1FCTF) {
|
|
if (trap_BotGetLevelItemGoal(-1, "Neutral Flag", &ctf_neutralflag) < 0)
|
|
BotAI_Print(PRT_WARNING, "One Flag CTF without Neutral Flag\n");
|
|
if (trap_BotGetLevelItemGoal(-1, "Red Flag", &ctf_redflag) < 0)
|
|
BotAI_Print(PRT_WARNING, "One Flag CTF without Red Flag\n");
|
|
if (trap_BotGetLevelItemGoal(-1, "Blue Flag", &ctf_blueflag) < 0)
|
|
BotAI_Print(PRT_WARNING, "One Flag CTF without Blue Flag\n");
|
|
}
|
|
else if (gametype == GT_OBELISK) {
|
|
if (trap_BotGetLevelItemGoal(-1, "Red Obelisk", &redobelisk) < 0)
|
|
BotAI_Print(PRT_WARNING, "Overload without Red Obelisk\n");
|
|
BotSetEntityNumForGoal(&redobelisk, "team_redobelisk");
|
|
if (trap_BotGetLevelItemGoal(-1, "Blue Obelisk", &blueobelisk) < 0)
|
|
BotAI_Print(PRT_WARNING, "Overload without Blue Obelisk\n");
|
|
BotSetEntityNumForGoal(&blueobelisk, "team_blueobelisk");
|
|
}
|
|
else if (gametype == GT_HARVESTER) {
|
|
if (trap_BotGetLevelItemGoal(-1, "Red Obelisk", &redobelisk) < 0)
|
|
BotAI_Print(PRT_WARNING, "Harvester without Red Obelisk\n");
|
|
BotSetEntityNumForGoal(&redobelisk, "team_redobelisk");
|
|
if (trap_BotGetLevelItemGoal(-1, "Blue Obelisk", &blueobelisk) < 0)
|
|
BotAI_Print(PRT_WARNING, "Harvester without Blue Obelisk\n");
|
|
BotSetEntityNumForGoal(&blueobelisk, "team_blueobelisk");
|
|
if (trap_BotGetLevelItemGoal(-1, "Neutral Obelisk", &neutralobelisk) < 0)
|
|
BotAI_Print(PRT_WARNING, "Harvester without Neutral Obelisk\n");
|
|
BotSetEntityNumForGoal(&neutralobelisk, "team_neutralobelisk");
|
|
}
|
|
#endif
|
|
|
|
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;
|
|
}
|