mirror of
https://github.com/ioquake/ioq3.git
synced 2024-11-10 07:11:46 +00:00
4474297af8
Bot's lastkilledplayer was set to -1 after carrying out an ordered kill. Later in BotChat_Random() the PlayerName function was passed -1 which caused a "Error: PlayerName: playernum out of range" message. I think the reason it was set to negative one is so that if the bot is ordered to kill the player again, the bot will not say it's done and drop the goal. Though, if the bot killed the player based on it's own decision, it will just say it's done and drop the goal (bug?). Let's check the time of the last kill to see if it happened since the team order was received instead of setting lastkilledplayer to -1 after completing the team ordered kill. This fixes bot dropping goal if target player was the last player they killed and the PlayerName out of range error.
2622 lines
78 KiB
C
2622 lines
78 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_dmnet.c
|
|
*
|
|
* desc: Quake3 bot AI
|
|
*
|
|
* $Archive: /MissionPack/code/game/ai_dmnet.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"
|
|
//data file headers
|
|
#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"
|
|
|
|
//goal flag, see ../botlib/be_ai_goal.h for the other GFL_*
|
|
#define GFL_AIR 128
|
|
|
|
int numnodeswitches;
|
|
char nodeswitch[MAX_NODESWITCHES+1][144];
|
|
|
|
#define LOOKAHEAD_DISTANCE 300
|
|
|
|
/*
|
|
==================
|
|
BotResetNodeSwitches
|
|
==================
|
|
*/
|
|
void BotResetNodeSwitches(void) {
|
|
numnodeswitches = 0;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotDumpNodeSwitches
|
|
==================
|
|
*/
|
|
void BotDumpNodeSwitches(bot_state_t *bs) {
|
|
int i;
|
|
char netname[MAX_NETNAME];
|
|
|
|
ClientName(bs->client, netname, sizeof(netname));
|
|
BotAI_Print(PRT_MESSAGE, "%s at %1.1f switched more than %d AI nodes\n", netname, FloatTime(), MAX_NODESWITCHES);
|
|
for (i = 0; i < numnodeswitches; i++) {
|
|
BotAI_Print(PRT_MESSAGE, "%s", nodeswitch[i]);
|
|
}
|
|
BotAI_Print(PRT_FATAL, "");
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotRecordNodeSwitch
|
|
==================
|
|
*/
|
|
void BotRecordNodeSwitch(bot_state_t *bs, char *node, char *str, char *s) {
|
|
char netname[MAX_NETNAME];
|
|
|
|
ClientName(bs->client, netname, sizeof(netname));
|
|
Com_sprintf(nodeswitch[numnodeswitches], 144, "%s at %2.1f entered %s: %s from %s\n", netname, FloatTime(), node, str, s);
|
|
#ifdef DEBUG
|
|
if (0) {
|
|
BotAI_Print(PRT_MESSAGE, "%s", nodeswitch[numnodeswitches]);
|
|
}
|
|
#endif //DEBUG
|
|
numnodeswitches++;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotGetAirGoal
|
|
==================
|
|
*/
|
|
int BotGetAirGoal(bot_state_t *bs, bot_goal_t *goal) {
|
|
bsp_trace_t bsptrace;
|
|
vec3_t end, mins = {-15, -15, -2}, maxs = {15, 15, 2};
|
|
int areanum;
|
|
|
|
//trace up until we hit solid
|
|
VectorCopy(bs->origin, end);
|
|
end[2] += 1000;
|
|
BotAI_Trace(&bsptrace, bs->origin, mins, maxs, end, bs->entitynum, CONTENTS_SOLID|CONTENTS_PLAYERCLIP);
|
|
//trace down until we hit water
|
|
VectorCopy(bsptrace.endpos, end);
|
|
BotAI_Trace(&bsptrace, end, mins, maxs, bs->origin, bs->entitynum, CONTENTS_WATER|CONTENTS_SLIME|CONTENTS_LAVA);
|
|
//if we found the water surface
|
|
if (bsptrace.fraction > 0) {
|
|
areanum = BotPointAreaNum(bsptrace.endpos);
|
|
if (areanum) {
|
|
VectorCopy(bsptrace.endpos, goal->origin);
|
|
goal->origin[2] -= 2;
|
|
goal->areanum = areanum;
|
|
goal->mins[0] = -15;
|
|
goal->mins[1] = -15;
|
|
goal->mins[2] = -1;
|
|
goal->maxs[0] = 15;
|
|
goal->maxs[1] = 15;
|
|
goal->maxs[2] = 1;
|
|
goal->flags = GFL_AIR;
|
|
goal->number = 0;
|
|
goal->iteminfo = 0;
|
|
goal->entitynum = 0;
|
|
return qtrue;
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotGoForAir
|
|
==================
|
|
*/
|
|
int BotGoForAir(bot_state_t *bs, int tfl, bot_goal_t *ltg, float range) {
|
|
bot_goal_t goal;
|
|
|
|
//if the bot needs air
|
|
if (bs->lastair_time < FloatTime() - 6) {
|
|
//
|
|
#ifdef DEBUG
|
|
//BotAI_Print(PRT_MESSAGE, "going for air\n");
|
|
#endif //DEBUG
|
|
//if we can find an air goal
|
|
if (BotGetAirGoal(bs, &goal)) {
|
|
trap_BotPushGoal(bs->gs, &goal);
|
|
return qtrue;
|
|
}
|
|
else {
|
|
//get a nearby goal outside the water
|
|
while(trap_BotChooseNBGItem(bs->gs, bs->origin, bs->inventory, tfl, ltg, range)) {
|
|
trap_BotGetTopGoal(bs->gs, &goal);
|
|
//if the goal is not in water
|
|
if (!(trap_AAS_PointContents(goal.origin) & (CONTENTS_WATER|CONTENTS_SLIME|CONTENTS_LAVA))) {
|
|
return qtrue;
|
|
}
|
|
trap_BotPopGoal(bs->gs);
|
|
}
|
|
trap_BotResetAvoidGoals(bs->gs);
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotNearbyGoal
|
|
==================
|
|
*/
|
|
int BotNearbyGoal(bot_state_t *bs, int tfl, bot_goal_t *ltg, float range) {
|
|
int ret;
|
|
|
|
//check if the bot should go for air
|
|
if (BotGoForAir(bs, tfl, ltg, range)) return qtrue;
|
|
// if the bot is carrying a flag or cubes
|
|
if (BotCTFCarryingFlag(bs)
|
|
#ifdef MISSIONPACK
|
|
|| Bot1FCTFCarryingFlag(bs) || BotHarvesterCarryingCubes(bs)
|
|
#endif
|
|
) {
|
|
//if the bot is just a few secs away from the base
|
|
if (trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin,
|
|
bs->teamgoal.areanum, TFL_DEFAULT) < 300) {
|
|
//make the range really small
|
|
range = 50;
|
|
}
|
|
}
|
|
//
|
|
ret = trap_BotChooseNBGItem(bs->gs, bs->origin, bs->inventory, tfl, ltg, range);
|
|
/*
|
|
if (ret)
|
|
{
|
|
char buf[128];
|
|
//get the goal at the top of the stack
|
|
trap_BotGetTopGoal(bs->gs, &goal);
|
|
trap_BotGoalName(goal.number, buf, sizeof(buf));
|
|
BotAI_Print(PRT_MESSAGE, "%1.1f: new nearby goal %s\n", FloatTime(), buf);
|
|
}
|
|
*/
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotReachedGoal
|
|
==================
|
|
*/
|
|
int BotReachedGoal(bot_state_t *bs, bot_goal_t *goal) {
|
|
if (goal->flags & GFL_ITEM) {
|
|
//if touching the goal
|
|
if (trap_BotTouchingGoal(bs->origin, goal)) {
|
|
if (!(goal->flags & GFL_DROPPED)) {
|
|
trap_BotSetAvoidGoalTime(bs->gs, goal->number, -1);
|
|
}
|
|
return qtrue;
|
|
}
|
|
//if the goal isn't there
|
|
if (trap_BotItemGoalInVisButNotVisible(bs->entitynum, bs->eye, bs->viewangles, goal)) {
|
|
/*
|
|
float avoidtime;
|
|
int t;
|
|
|
|
avoidtime = trap_BotAvoidGoalTime(bs->gs, goal->number);
|
|
if (avoidtime > 0) {
|
|
t = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, goal->areanum, bs->tfl);
|
|
if ((float) t * 0.009 < avoidtime)
|
|
return qtrue;
|
|
}
|
|
*/
|
|
return qtrue;
|
|
}
|
|
//if in the goal area and below or above the goal and not swimming
|
|
if (bs->areanum == goal->areanum) {
|
|
if (bs->origin[0] > goal->origin[0] + goal->mins[0] && bs->origin[0] < goal->origin[0] + goal->maxs[0]) {
|
|
if (bs->origin[1] > goal->origin[1] + goal->mins[1] && bs->origin[1] < goal->origin[1] + goal->maxs[1]) {
|
|
if (!trap_AAS_Swimming(bs->origin)) {
|
|
return qtrue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (goal->flags & GFL_AIR) {
|
|
//if touching the goal
|
|
if (trap_BotTouchingGoal(bs->origin, goal)) return qtrue;
|
|
//if the bot got air
|
|
if (bs->lastair_time > FloatTime() - 1) return qtrue;
|
|
}
|
|
else {
|
|
//if touching the goal
|
|
if (trap_BotTouchingGoal(bs->origin, goal)) return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotGetItemLongTermGoal
|
|
==================
|
|
*/
|
|
int BotGetItemLongTermGoal(bot_state_t *bs, int tfl, bot_goal_t *goal) {
|
|
//if the bot has no goal
|
|
if (!trap_BotGetTopGoal(bs->gs, goal)) {
|
|
//BotAI_Print(PRT_MESSAGE, "no ltg on stack\n");
|
|
bs->ltg_time = 0;
|
|
}
|
|
//if the bot touches the current goal
|
|
else if (BotReachedGoal(bs, goal)) {
|
|
BotChooseWeapon(bs);
|
|
bs->ltg_time = 0;
|
|
}
|
|
//if it is time to find a new long term goal
|
|
if (bs->ltg_time < FloatTime()) {
|
|
//pop the current goal from the stack
|
|
trap_BotPopGoal(bs->gs);
|
|
//BotAI_Print(PRT_MESSAGE, "%s: choosing new ltg\n", ClientName(bs->client, netname, sizeof(netname)));
|
|
//choose a new goal
|
|
//BotAI_Print(PRT_MESSAGE, "%6.1f client %d: BotChooseLTGItem\n", FloatTime(), bs->client);
|
|
if (trap_BotChooseLTGItem(bs->gs, bs->origin, bs->inventory, tfl)) {
|
|
/*
|
|
char buf[128];
|
|
//get the goal at the top of the stack
|
|
trap_BotGetTopGoal(bs->gs, goal);
|
|
trap_BotGoalName(goal->number, buf, sizeof(buf));
|
|
BotAI_Print(PRT_MESSAGE, "%1.1f: new long term goal %s\n", FloatTime(), buf);
|
|
*/
|
|
bs->ltg_time = FloatTime() + 20;
|
|
}
|
|
else {//the bot gets sorta stuck with all the avoid timings, shouldn't happen though
|
|
//
|
|
#ifdef DEBUG
|
|
char netname[128];
|
|
|
|
BotAI_Print(PRT_MESSAGE, "%s: no valid ltg (probably stuck)\n", ClientName(bs->client, netname, sizeof(netname)));
|
|
#endif
|
|
//trap_BotDumpAvoidGoals(bs->gs);
|
|
//reset the avoid goals and the avoid reach
|
|
trap_BotResetAvoidGoals(bs->gs);
|
|
trap_BotResetAvoidReach(bs->ms);
|
|
}
|
|
//get the goal at the top of the stack
|
|
return trap_BotGetTopGoal(bs->gs, goal);
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotGetLongTermGoal
|
|
|
|
we could also create a seperate AI node for every long term goal type
|
|
however this saves us a lot of code
|
|
==================
|
|
*/
|
|
int BotGetLongTermGoal(bot_state_t *bs, int tfl, int retreat, bot_goal_t *goal) {
|
|
vec3_t target, dir, dir2;
|
|
char netname[MAX_NETNAME];
|
|
char buf[MAX_MESSAGE_SIZE];
|
|
int areanum;
|
|
float croucher;
|
|
aas_entityinfo_t entinfo, botinfo;
|
|
bot_waypoint_t *wp;
|
|
|
|
if (bs->ltgtype == LTG_TEAMHELP && !retreat) {
|
|
//check for bot typing status message
|
|
if (bs->teammessage_time && bs->teammessage_time < FloatTime()) {
|
|
BotAI_BotInitialChat(bs, "help_start", EasyClientName(bs->teammate, netname, sizeof(netname)), NULL);
|
|
trap_BotEnterChat(bs->cs, bs->decisionmaker, CHAT_TELL);
|
|
BotVoiceChatOnly(bs, bs->decisionmaker, VOICECHAT_YES);
|
|
trap_EA_Action(bs->client, ACTION_AFFIRMATIVE);
|
|
bs->teammessage_time = 0;
|
|
}
|
|
//if trying to help the team mate for more than a minute
|
|
if (bs->teamgoal_time < FloatTime())
|
|
bs->ltgtype = 0;
|
|
//if the team mate IS visible for quite some time
|
|
if (bs->teammatevisible_time < FloatTime() - 10) bs->ltgtype = 0;
|
|
//get entity information of the companion
|
|
BotEntityInfo(bs->teammate, &entinfo);
|
|
//if the team mate is visible
|
|
if (BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, bs->teammate)) {
|
|
//if close just stand still there
|
|
VectorSubtract(entinfo.origin, bs->origin, dir);
|
|
if (VectorLengthSquared(dir) < Square(100)) {
|
|
trap_BotResetAvoidReach(bs->ms);
|
|
return qfalse;
|
|
}
|
|
}
|
|
else {
|
|
//last time the bot was NOT visible
|
|
bs->teammatevisible_time = FloatTime();
|
|
}
|
|
//if the entity information is valid (entity in PVS)
|
|
if (entinfo.valid) {
|
|
areanum = BotPointAreaNum(entinfo.origin);
|
|
if (areanum && trap_AAS_AreaReachability(areanum)) {
|
|
//update team goal
|
|
bs->teamgoal.entitynum = bs->teammate;
|
|
bs->teamgoal.areanum = areanum;
|
|
VectorCopy(entinfo.origin, bs->teamgoal.origin);
|
|
VectorSet(bs->teamgoal.mins, -8, -8, -8);
|
|
VectorSet(bs->teamgoal.maxs, 8, 8, 8);
|
|
}
|
|
}
|
|
memcpy(goal, &bs->teamgoal, sizeof(bot_goal_t));
|
|
return qtrue;
|
|
}
|
|
//if the bot accompanies someone
|
|
if (bs->ltgtype == LTG_TEAMACCOMPANY && !retreat) {
|
|
//check for bot typing status message
|
|
if (bs->teammessage_time && bs->teammessage_time < FloatTime()) {
|
|
BotAI_BotInitialChat(bs, "accompany_start", EasyClientName(bs->teammate, netname, sizeof(netname)), NULL);
|
|
trap_BotEnterChat(bs->cs, bs->decisionmaker, CHAT_TELL);
|
|
BotVoiceChatOnly(bs, bs->decisionmaker, VOICECHAT_YES);
|
|
trap_EA_Action(bs->client, ACTION_AFFIRMATIVE);
|
|
bs->teammessage_time = 0;
|
|
}
|
|
//if accompanying the companion for 3 minutes
|
|
if (bs->teamgoal_time < FloatTime()) {
|
|
BotAI_BotInitialChat(bs, "accompany_stop", EasyClientName(bs->teammate, netname, sizeof(netname)), NULL);
|
|
trap_BotEnterChat(bs->cs, bs->teammate, CHAT_TELL);
|
|
bs->ltgtype = 0;
|
|
}
|
|
//get entity information of the companion
|
|
BotEntityInfo(bs->teammate, &entinfo);
|
|
//if the companion is visible
|
|
if (BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, bs->teammate)) {
|
|
//update visible time
|
|
bs->teammatevisible_time = FloatTime();
|
|
VectorSubtract(entinfo.origin, bs->origin, dir);
|
|
if (VectorLengthSquared(dir) < Square(bs->formation_dist)) {
|
|
//
|
|
// if the client being followed bumps into this bot then
|
|
// the bot should back up
|
|
BotEntityInfo(bs->entitynum, &botinfo);
|
|
// if the followed client is not standing ontop of the bot
|
|
if (botinfo.origin[2] + botinfo.maxs[2] > entinfo.origin[2] + entinfo.mins[2]) {
|
|
// if the bounding boxes touch each other
|
|
if (botinfo.origin[0] + botinfo.maxs[0] > entinfo.origin[0] + entinfo.mins[0] - 4&&
|
|
botinfo.origin[0] + botinfo.mins[0] < entinfo.origin[0] + entinfo.maxs[0] + 4) {
|
|
if (botinfo.origin[1] + botinfo.maxs[1] > entinfo.origin[1] + entinfo.mins[1] - 4 &&
|
|
botinfo.origin[1] + botinfo.mins[1] < entinfo.origin[1] + entinfo.maxs[1] + 4) {
|
|
if (botinfo.origin[2] + botinfo.maxs[2] > entinfo.origin[2] + entinfo.mins[2] - 4 &&
|
|
botinfo.origin[2] + botinfo.mins[2] < entinfo.origin[2] + entinfo.maxs[2] + 4) {
|
|
// if the followed client looks in the direction of this bot
|
|
AngleVectors(entinfo.angles, dir, NULL, NULL);
|
|
dir[2] = 0;
|
|
VectorNormalize(dir);
|
|
//VectorSubtract(entinfo.origin, entinfo.lastvisorigin, dir);
|
|
VectorSubtract(bs->origin, entinfo.origin, dir2);
|
|
VectorNormalize(dir2);
|
|
if (DotProduct(dir, dir2) > 0.7) {
|
|
// back up
|
|
BotSetupForMovement(bs);
|
|
trap_BotMoveInDirection(bs->ms, dir2, 400, MOVE_WALK);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//check if the bot wants to crouch
|
|
//don't crouch if crouched less than 5 seconds ago
|
|
if (bs->attackcrouch_time < FloatTime() - 5) {
|
|
croucher = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_CROUCHER, 0, 1);
|
|
if (random() < bs->thinktime * croucher) {
|
|
bs->attackcrouch_time = FloatTime() + 5 + croucher * 15;
|
|
}
|
|
}
|
|
//don't crouch when swimming
|
|
if (trap_AAS_Swimming(bs->origin)) bs->attackcrouch_time = FloatTime() - 1;
|
|
//if not arrived yet or arived some time ago
|
|
if (bs->arrive_time < FloatTime() - 2) {
|
|
//if not arrived yet
|
|
if (!bs->arrive_time) {
|
|
trap_EA_Gesture(bs->client);
|
|
BotAI_BotInitialChat(bs, "accompany_arrive", EasyClientName(bs->teammate, netname, sizeof(netname)), NULL);
|
|
trap_BotEnterChat(bs->cs, bs->teammate, CHAT_TELL);
|
|
bs->arrive_time = FloatTime();
|
|
}
|
|
//if the bot wants to crouch
|
|
else if (bs->attackcrouch_time > FloatTime()) {
|
|
trap_EA_Crouch(bs->client);
|
|
}
|
|
//else do some model taunts
|
|
else if (random() < bs->thinktime * 0.05) {
|
|
//do a gesture :)
|
|
trap_EA_Gesture(bs->client);
|
|
}
|
|
}
|
|
//if just arrived look at the companion
|
|
if (bs->arrive_time > FloatTime() - 2) {
|
|
VectorSubtract(entinfo.origin, bs->origin, dir);
|
|
vectoangles(dir, bs->ideal_viewangles);
|
|
bs->ideal_viewangles[2] *= 0.5;
|
|
}
|
|
//else look strategically around for enemies
|
|
else if (random() < bs->thinktime * 0.8) {
|
|
BotRoamGoal(bs, target);
|
|
VectorSubtract(target, bs->origin, dir);
|
|
vectoangles(dir, bs->ideal_viewangles);
|
|
bs->ideal_viewangles[2] *= 0.5;
|
|
}
|
|
//check if the bot wants to go for air
|
|
if (BotGoForAir(bs, bs->tfl, &bs->teamgoal, 400)) {
|
|
trap_BotResetLastAvoidReach(bs->ms);
|
|
//get the goal at the top of the stack
|
|
//trap_BotGetTopGoal(bs->gs, &tmpgoal);
|
|
//trap_BotGoalName(tmpgoal.number, buf, 144);
|
|
//BotAI_Print(PRT_MESSAGE, "new nearby goal %s\n", buf);
|
|
//time the bot gets to pick up the nearby goal item
|
|
bs->nbg_time = FloatTime() + 8;
|
|
AIEnter_Seek_NBG(bs, "BotLongTermGoal: go for air");
|
|
return qfalse;
|
|
}
|
|
//
|
|
trap_BotResetAvoidReach(bs->ms);
|
|
return qfalse;
|
|
}
|
|
}
|
|
//if the entity information is valid (entity in PVS)
|
|
if (entinfo.valid) {
|
|
areanum = BotPointAreaNum(entinfo.origin);
|
|
if (areanum && trap_AAS_AreaReachability(areanum)) {
|
|
//update team goal
|
|
bs->teamgoal.entitynum = bs->teammate;
|
|
bs->teamgoal.areanum = areanum;
|
|
VectorCopy(entinfo.origin, bs->teamgoal.origin);
|
|
VectorSet(bs->teamgoal.mins, -8, -8, -8);
|
|
VectorSet(bs->teamgoal.maxs, 8, 8, 8);
|
|
}
|
|
}
|
|
//the goal the bot should go for
|
|
memcpy(goal, &bs->teamgoal, sizeof(bot_goal_t));
|
|
//if the companion is NOT visible for too long
|
|
if (bs->teammatevisible_time < FloatTime() - 60) {
|
|
BotAI_BotInitialChat(bs, "accompany_cannotfind", EasyClientName(bs->teammate, netname, sizeof(netname)), NULL);
|
|
trap_BotEnterChat(bs->cs, bs->teammate, CHAT_TELL);
|
|
bs->ltgtype = 0;
|
|
// just to make sure the bot won't spam this message
|
|
bs->teammatevisible_time = FloatTime();
|
|
}
|
|
return qtrue;
|
|
}
|
|
//
|
|
if (bs->ltgtype == LTG_DEFENDKEYAREA) {
|
|
if (trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin,
|
|
bs->teamgoal.areanum, TFL_DEFAULT) > bs->defendaway_range) {
|
|
bs->defendaway_time = 0;
|
|
}
|
|
}
|
|
//if defending a key area
|
|
if (bs->ltgtype == LTG_DEFENDKEYAREA && !retreat &&
|
|
bs->defendaway_time < FloatTime()) {
|
|
//check for bot typing status message
|
|
if (bs->teammessage_time && bs->teammessage_time < FloatTime()) {
|
|
trap_BotGoalName(bs->teamgoal.number, buf, sizeof(buf));
|
|
BotAI_BotInitialChat(bs, "defend_start", buf, NULL);
|
|
trap_BotEnterChat(bs->cs, 0, CHAT_TEAM);
|
|
BotVoiceChatOnly(bs, -1, VOICECHAT_ONDEFENSE);
|
|
bs->teammessage_time = 0;
|
|
}
|
|
//set the bot goal
|
|
memcpy(goal, &bs->teamgoal, sizeof(bot_goal_t));
|
|
//stop after 2 minutes
|
|
if (bs->teamgoal_time < FloatTime()) {
|
|
trap_BotGoalName(bs->teamgoal.number, buf, sizeof(buf));
|
|
BotAI_BotInitialChat(bs, "defend_stop", buf, NULL);
|
|
trap_BotEnterChat(bs->cs, 0, CHAT_TEAM);
|
|
bs->ltgtype = 0;
|
|
}
|
|
//if very close... go away for some time
|
|
VectorSubtract(goal->origin, bs->origin, dir);
|
|
if (VectorLengthSquared(dir) < Square(70)) {
|
|
trap_BotResetAvoidReach(bs->ms);
|
|
bs->defendaway_time = FloatTime() + 3 + 3 * random();
|
|
if (BotHasPersistantPowerupAndWeapon(bs)) {
|
|
bs->defendaway_range = 100;
|
|
}
|
|
else {
|
|
bs->defendaway_range = 350;
|
|
}
|
|
}
|
|
return qtrue;
|
|
}
|
|
//going to kill someone
|
|
if (bs->ltgtype == LTG_KILL && !retreat) {
|
|
//check for bot typing status message
|
|
if (bs->teammessage_time && bs->teammessage_time < FloatTime()) {
|
|
EasyClientName(bs->teamgoal.entitynum, buf, sizeof(buf));
|
|
BotAI_BotInitialChat(bs, "kill_start", buf, NULL);
|
|
trap_BotEnterChat(bs->cs, bs->decisionmaker, CHAT_TELL);
|
|
bs->teammessage_time = 0;
|
|
}
|
|
//
|
|
if (bs->killedenemy_time > bs->teamgoal_time - TEAM_KILL_SOMEONE && bs->lastkilledplayer == bs->teamgoal.entitynum) {
|
|
EasyClientName(bs->teamgoal.entitynum, buf, sizeof(buf));
|
|
BotAI_BotInitialChat(bs, "kill_done", buf, NULL);
|
|
trap_BotEnterChat(bs->cs, bs->decisionmaker, CHAT_TELL);
|
|
bs->ltgtype = 0;
|
|
}
|
|
//
|
|
if (bs->teamgoal_time < FloatTime()) {
|
|
bs->ltgtype = 0;
|
|
}
|
|
//just roam around
|
|
return BotGetItemLongTermGoal(bs, tfl, goal);
|
|
}
|
|
//get an item
|
|
if (bs->ltgtype == LTG_GETITEM && !retreat) {
|
|
//check for bot typing status message
|
|
if (bs->teammessage_time && bs->teammessage_time < FloatTime()) {
|
|
trap_BotGoalName(bs->teamgoal.number, buf, sizeof(buf));
|
|
BotAI_BotInitialChat(bs, "getitem_start", buf, NULL);
|
|
trap_BotEnterChat(bs->cs, bs->decisionmaker, CHAT_TELL);
|
|
BotVoiceChatOnly(bs, bs->decisionmaker, VOICECHAT_YES);
|
|
trap_EA_Action(bs->client, ACTION_AFFIRMATIVE);
|
|
bs->teammessage_time = 0;
|
|
}
|
|
//set the bot goal
|
|
memcpy(goal, &bs->teamgoal, sizeof(bot_goal_t));
|
|
//stop after some time
|
|
if (bs->teamgoal_time < FloatTime()) {
|
|
bs->ltgtype = 0;
|
|
}
|
|
//
|
|
if (trap_BotItemGoalInVisButNotVisible(bs->entitynum, bs->eye, bs->viewangles, goal)) {
|
|
trap_BotGoalName(bs->teamgoal.number, buf, sizeof(buf));
|
|
BotAI_BotInitialChat(bs, "getitem_notthere", buf, NULL);
|
|
trap_BotEnterChat(bs->cs, bs->decisionmaker, CHAT_TELL);
|
|
bs->ltgtype = 0;
|
|
}
|
|
else if (BotReachedGoal(bs, goal)) {
|
|
trap_BotGoalName(bs->teamgoal.number, buf, sizeof(buf));
|
|
BotAI_BotInitialChat(bs, "getitem_gotit", buf, NULL);
|
|
trap_BotEnterChat(bs->cs, bs->decisionmaker, CHAT_TELL);
|
|
bs->ltgtype = 0;
|
|
}
|
|
return qtrue;
|
|
}
|
|
//if camping somewhere
|
|
if ((bs->ltgtype == LTG_CAMP || bs->ltgtype == LTG_CAMPORDER) && !retreat) {
|
|
//check for bot typing status message
|
|
if (bs->teammessage_time && bs->teammessage_time < FloatTime()) {
|
|
if (bs->ltgtype == LTG_CAMPORDER) {
|
|
BotAI_BotInitialChat(bs, "camp_start", EasyClientName(bs->teammate, netname, sizeof(netname)), NULL);
|
|
trap_BotEnterChat(bs->cs, bs->decisionmaker, CHAT_TELL);
|
|
BotVoiceChatOnly(bs, bs->decisionmaker, VOICECHAT_YES);
|
|
trap_EA_Action(bs->client, ACTION_AFFIRMATIVE);
|
|
}
|
|
bs->teammessage_time = 0;
|
|
}
|
|
//set the bot goal
|
|
memcpy(goal, &bs->teamgoal, sizeof(bot_goal_t));
|
|
//
|
|
if (bs->teamgoal_time < FloatTime()) {
|
|
if (bs->ltgtype == LTG_CAMPORDER) {
|
|
BotAI_BotInitialChat(bs, "camp_stop", NULL);
|
|
trap_BotEnterChat(bs->cs, bs->decisionmaker, CHAT_TELL);
|
|
}
|
|
bs->ltgtype = 0;
|
|
}
|
|
//if really near the camp spot
|
|
VectorSubtract(goal->origin, bs->origin, dir);
|
|
if (VectorLengthSquared(dir) < Square(60))
|
|
{
|
|
//if not arrived yet
|
|
if (!bs->arrive_time) {
|
|
if (bs->ltgtype == LTG_CAMPORDER) {
|
|
BotAI_BotInitialChat(bs, "camp_arrive", EasyClientName(bs->teammate, netname, sizeof(netname)), NULL);
|
|
trap_BotEnterChat(bs->cs, bs->decisionmaker, CHAT_TELL);
|
|
BotVoiceChatOnly(bs, bs->decisionmaker, VOICECHAT_INPOSITION);
|
|
}
|
|
bs->arrive_time = FloatTime();
|
|
}
|
|
//look strategically around for enemies
|
|
if (random() < bs->thinktime * 0.8) {
|
|
BotRoamGoal(bs, target);
|
|
VectorSubtract(target, bs->origin, dir);
|
|
vectoangles(dir, bs->ideal_viewangles);
|
|
bs->ideal_viewangles[2] *= 0.5;
|
|
}
|
|
//check if the bot wants to crouch
|
|
//don't crouch if crouched less than 5 seconds ago
|
|
if (bs->attackcrouch_time < FloatTime() - 5) {
|
|
croucher = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_CROUCHER, 0, 1);
|
|
if (random() < bs->thinktime * croucher) {
|
|
bs->attackcrouch_time = FloatTime() + 5 + croucher * 15;
|
|
}
|
|
}
|
|
//if the bot wants to crouch
|
|
if (bs->attackcrouch_time > FloatTime()) {
|
|
trap_EA_Crouch(bs->client);
|
|
}
|
|
//don't crouch when swimming
|
|
if (trap_AAS_Swimming(bs->origin)) bs->attackcrouch_time = FloatTime() - 1;
|
|
//make sure the bot is not gonna drown
|
|
if (trap_PointContents(bs->eye,bs->entitynum) & (CONTENTS_WATER|CONTENTS_SLIME|CONTENTS_LAVA)) {
|
|
if (bs->ltgtype == LTG_CAMPORDER) {
|
|
BotAI_BotInitialChat(bs, "camp_stop", NULL);
|
|
trap_BotEnterChat(bs->cs, bs->decisionmaker, CHAT_TELL);
|
|
//
|
|
if (bs->lastgoal_ltgtype == LTG_CAMPORDER) {
|
|
bs->lastgoal_ltgtype = 0;
|
|
}
|
|
}
|
|
bs->ltgtype = 0;
|
|
}
|
|
//
|
|
//FIXME: move around a bit
|
|
//
|
|
trap_BotResetAvoidReach(bs->ms);
|
|
return qfalse;
|
|
}
|
|
return qtrue;
|
|
}
|
|
//patrolling along several waypoints
|
|
if (bs->ltgtype == LTG_PATROL && !retreat) {
|
|
//check for bot typing status message
|
|
if (bs->teammessage_time && bs->teammessage_time < FloatTime()) {
|
|
strcpy(buf, "");
|
|
for (wp = bs->patrolpoints; wp; wp = wp->next) {
|
|
strcat(buf, wp->name);
|
|
if (wp->next) strcat(buf, " to ");
|
|
}
|
|
BotAI_BotInitialChat(bs, "patrol_start", buf, NULL);
|
|
trap_BotEnterChat(bs->cs, bs->decisionmaker, CHAT_TELL);
|
|
BotVoiceChatOnly(bs, bs->decisionmaker, VOICECHAT_YES);
|
|
trap_EA_Action(bs->client, ACTION_AFFIRMATIVE);
|
|
bs->teammessage_time = 0;
|
|
}
|
|
//
|
|
if (!bs->curpatrolpoint) {
|
|
bs->ltgtype = 0;
|
|
return qfalse;
|
|
}
|
|
//if the bot touches the current goal
|
|
if (trap_BotTouchingGoal(bs->origin, &bs->curpatrolpoint->goal)) {
|
|
if (bs->patrolflags & PATROL_BACK) {
|
|
if (bs->curpatrolpoint->prev) {
|
|
bs->curpatrolpoint = bs->curpatrolpoint->prev;
|
|
}
|
|
else {
|
|
bs->curpatrolpoint = bs->curpatrolpoint->next;
|
|
bs->patrolflags &= ~PATROL_BACK;
|
|
}
|
|
}
|
|
else {
|
|
if (bs->curpatrolpoint->next) {
|
|
bs->curpatrolpoint = bs->curpatrolpoint->next;
|
|
}
|
|
else {
|
|
bs->curpatrolpoint = bs->curpatrolpoint->prev;
|
|
bs->patrolflags |= PATROL_BACK;
|
|
}
|
|
}
|
|
}
|
|
//stop after 5 minutes
|
|
if (bs->teamgoal_time < FloatTime()) {
|
|
BotAI_BotInitialChat(bs, "patrol_stop", NULL);
|
|
trap_BotEnterChat(bs->cs, bs->decisionmaker, CHAT_TELL);
|
|
bs->ltgtype = 0;
|
|
}
|
|
if (!bs->curpatrolpoint) {
|
|
bs->ltgtype = 0;
|
|
return qfalse;
|
|
}
|
|
memcpy(goal, &bs->curpatrolpoint->goal, sizeof(bot_goal_t));
|
|
return qtrue;
|
|
}
|
|
#ifdef CTF
|
|
if (gametype == GT_CTF) {
|
|
//if going for enemy flag
|
|
if (bs->ltgtype == LTG_GETFLAG) {
|
|
//check for bot typing status message
|
|
if (bs->teammessage_time && bs->teammessage_time < FloatTime()) {
|
|
BotAI_BotInitialChat(bs, "captureflag_start", NULL);
|
|
trap_BotEnterChat(bs->cs, 0, CHAT_TEAM);
|
|
BotVoiceChatOnly(bs, -1, VOICECHAT_ONGETFLAG);
|
|
bs->teammessage_time = 0;
|
|
}
|
|
//
|
|
switch(BotTeam(bs)) {
|
|
case TEAM_RED: memcpy(goal, &ctf_blueflag, sizeof(bot_goal_t)); break;
|
|
case TEAM_BLUE: memcpy(goal, &ctf_redflag, sizeof(bot_goal_t)); break;
|
|
default: bs->ltgtype = 0; return qfalse;
|
|
}
|
|
//if touching the flag
|
|
if (trap_BotTouchingGoal(bs->origin, goal)) {
|
|
// make sure the bot knows the flag isn't there anymore
|
|
switch(BotTeam(bs)) {
|
|
case TEAM_RED: bs->blueflagstatus = 1; break;
|
|
case TEAM_BLUE: bs->redflagstatus = 1; break;
|
|
}
|
|
bs->ltgtype = 0;
|
|
}
|
|
//stop after 3 minutes
|
|
if (bs->teamgoal_time < FloatTime()) {
|
|
bs->ltgtype = 0;
|
|
}
|
|
BotAlternateRoute(bs, goal);
|
|
return qtrue;
|
|
}
|
|
//if rushing to the base
|
|
if (bs->ltgtype == LTG_RUSHBASE && bs->rushbaseaway_time < FloatTime()) {
|
|
switch(BotTeam(bs)) {
|
|
case TEAM_RED: memcpy(goal, &ctf_redflag, sizeof(bot_goal_t)); break;
|
|
case TEAM_BLUE: memcpy(goal, &ctf_blueflag, sizeof(bot_goal_t)); break;
|
|
default: bs->ltgtype = 0; return qfalse;
|
|
}
|
|
//if not carrying the flag anymore
|
|
if (!BotCTFCarryingFlag(bs)) bs->ltgtype = 0;
|
|
//quit rushing after 2 minutes
|
|
if (bs->teamgoal_time < FloatTime()) bs->ltgtype = 0;
|
|
//if touching the base flag the bot should loose the enemy flag
|
|
if (trap_BotTouchingGoal(bs->origin, goal)) {
|
|
//if the bot is still carrying the enemy flag then the
|
|
//base flag is gone, now just walk near the base a bit
|
|
if (BotCTFCarryingFlag(bs)) {
|
|
trap_BotResetAvoidReach(bs->ms);
|
|
bs->rushbaseaway_time = FloatTime() + 5 + 10 * random();
|
|
//FIXME: add chat to tell the others to get back the flag
|
|
}
|
|
else {
|
|
bs->ltgtype = 0;
|
|
}
|
|
}
|
|
BotAlternateRoute(bs, goal);
|
|
return qtrue;
|
|
}
|
|
//returning flag
|
|
if (bs->ltgtype == LTG_RETURNFLAG) {
|
|
//check for bot typing status message
|
|
if (bs->teammessage_time && bs->teammessage_time < FloatTime()) {
|
|
BotAI_BotInitialChat(bs, "returnflag_start", NULL);
|
|
trap_BotEnterChat(bs->cs, 0, CHAT_TEAM);
|
|
BotVoiceChatOnly(bs, -1, VOICECHAT_ONRETURNFLAG);
|
|
bs->teammessage_time = 0;
|
|
}
|
|
//
|
|
switch(BotTeam(bs)) {
|
|
case TEAM_RED: memcpy(goal, &ctf_blueflag, sizeof(bot_goal_t)); break;
|
|
case TEAM_BLUE: memcpy(goal, &ctf_redflag, sizeof(bot_goal_t)); break;
|
|
default: bs->ltgtype = 0; return qfalse;
|
|
}
|
|
//if touching the flag
|
|
if (trap_BotTouchingGoal(bs->origin, goal)) bs->ltgtype = 0;
|
|
//stop after 3 minutes
|
|
if (bs->teamgoal_time < FloatTime()) {
|
|
bs->ltgtype = 0;
|
|
}
|
|
BotAlternateRoute(bs, goal);
|
|
return qtrue;
|
|
}
|
|
}
|
|
#endif //CTF
|
|
#ifdef MISSIONPACK
|
|
else if (gametype == GT_1FCTF) {
|
|
if (bs->ltgtype == LTG_GETFLAG) {
|
|
//check for bot typing status message
|
|
if (bs->teammessage_time && bs->teammessage_time < FloatTime()) {
|
|
BotAI_BotInitialChat(bs, "captureflag_start", NULL);
|
|
trap_BotEnterChat(bs->cs, 0, CHAT_TEAM);
|
|
BotVoiceChatOnly(bs, -1, VOICECHAT_ONGETFLAG);
|
|
bs->teammessage_time = 0;
|
|
}
|
|
memcpy(goal, &ctf_neutralflag, sizeof(bot_goal_t));
|
|
//if touching the flag
|
|
if (trap_BotTouchingGoal(bs->origin, goal)) {
|
|
bs->ltgtype = 0;
|
|
}
|
|
//stop after 3 minutes
|
|
if (bs->teamgoal_time < FloatTime()) {
|
|
bs->ltgtype = 0;
|
|
}
|
|
return qtrue;
|
|
}
|
|
//if rushing to the base
|
|
if (bs->ltgtype == LTG_RUSHBASE) {
|
|
switch(BotTeam(bs)) {
|
|
case TEAM_RED: memcpy(goal, &ctf_blueflag, sizeof(bot_goal_t)); break;
|
|
case TEAM_BLUE: memcpy(goal, &ctf_redflag, sizeof(bot_goal_t)); break;
|
|
default: bs->ltgtype = 0; return qfalse;
|
|
}
|
|
//if not carrying the flag anymore
|
|
if (!Bot1FCTFCarryingFlag(bs)) {
|
|
bs->ltgtype = 0;
|
|
}
|
|
//quit rushing after 2 minutes
|
|
if (bs->teamgoal_time < FloatTime()) {
|
|
bs->ltgtype = 0;
|
|
}
|
|
//if touching the base flag the bot should loose the enemy flag
|
|
if (trap_BotTouchingGoal(bs->origin, goal)) {
|
|
bs->ltgtype = 0;
|
|
}
|
|
BotAlternateRoute(bs, goal);
|
|
return qtrue;
|
|
}
|
|
//attack the enemy base
|
|
if (bs->ltgtype == LTG_ATTACKENEMYBASE &&
|
|
bs->attackaway_time < FloatTime()) {
|
|
//check for bot typing status message
|
|
if (bs->teammessage_time && bs->teammessage_time < FloatTime()) {
|
|
BotAI_BotInitialChat(bs, "attackenemybase_start", NULL);
|
|
trap_BotEnterChat(bs->cs, 0, CHAT_TEAM);
|
|
BotVoiceChatOnly(bs, -1, VOICECHAT_ONOFFENSE);
|
|
bs->teammessage_time = 0;
|
|
}
|
|
switch(BotTeam(bs)) {
|
|
case TEAM_RED: memcpy(goal, &ctf_blueflag, sizeof(bot_goal_t)); break;
|
|
case TEAM_BLUE: memcpy(goal, &ctf_redflag, sizeof(bot_goal_t)); break;
|
|
default: bs->ltgtype = 0; return qfalse;
|
|
}
|
|
//quit rushing after 2 minutes
|
|
if (bs->teamgoal_time < FloatTime()) {
|
|
bs->ltgtype = 0;
|
|
}
|
|
//if touching the base flag the bot should loose the enemy flag
|
|
if (trap_BotTouchingGoal(bs->origin, goal)) {
|
|
bs->attackaway_time = FloatTime() + 2 + 5 * random();
|
|
}
|
|
return qtrue;
|
|
}
|
|
//returning flag
|
|
if (bs->ltgtype == LTG_RETURNFLAG) {
|
|
//check for bot typing status message
|
|
if (bs->teammessage_time && bs->teammessage_time < FloatTime()) {
|
|
BotAI_BotInitialChat(bs, "returnflag_start", NULL);
|
|
trap_BotEnterChat(bs->cs, 0, CHAT_TEAM);
|
|
BotVoiceChatOnly(bs, -1, VOICECHAT_ONRETURNFLAG);
|
|
bs->teammessage_time = 0;
|
|
}
|
|
//
|
|
if (bs->teamgoal_time < FloatTime()) {
|
|
bs->ltgtype = 0;
|
|
}
|
|
//just roam around
|
|
return BotGetItemLongTermGoal(bs, tfl, goal);
|
|
}
|
|
}
|
|
else if (gametype == GT_OBELISK) {
|
|
if (bs->ltgtype == LTG_ATTACKENEMYBASE &&
|
|
bs->attackaway_time < FloatTime()) {
|
|
|
|
//check for bot typing status message
|
|
if (bs->teammessage_time && bs->teammessage_time < FloatTime()) {
|
|
BotAI_BotInitialChat(bs, "attackenemybase_start", NULL);
|
|
trap_BotEnterChat(bs->cs, 0, CHAT_TEAM);
|
|
BotVoiceChatOnly(bs, -1, VOICECHAT_ONOFFENSE);
|
|
bs->teammessage_time = 0;
|
|
}
|
|
switch(BotTeam(bs)) {
|
|
case TEAM_RED: memcpy(goal, &blueobelisk, sizeof(bot_goal_t)); break;
|
|
case TEAM_BLUE: memcpy(goal, &redobelisk, sizeof(bot_goal_t)); break;
|
|
default: bs->ltgtype = 0; return qfalse;
|
|
}
|
|
//if the bot no longer wants to attack the obelisk
|
|
if (BotFeelingBad(bs) > 50) {
|
|
return BotGetItemLongTermGoal(bs, tfl, goal);
|
|
}
|
|
//if touching the obelisk
|
|
if (trap_BotTouchingGoal(bs->origin, goal)) {
|
|
bs->attackaway_time = FloatTime() + 3 + 5 * random();
|
|
}
|
|
// or very close to the obelisk
|
|
VectorSubtract(bs->origin, goal->origin, dir);
|
|
if (VectorLengthSquared(dir) < Square(60)) {
|
|
bs->attackaway_time = FloatTime() + 3 + 5 * random();
|
|
}
|
|
//quit rushing after 2 minutes
|
|
if (bs->teamgoal_time < FloatTime()) {
|
|
bs->ltgtype = 0;
|
|
}
|
|
BotAlternateRoute(bs, goal);
|
|
//just move towards the obelisk
|
|
return qtrue;
|
|
}
|
|
}
|
|
else if (gametype == GT_HARVESTER) {
|
|
//if rushing to the base
|
|
if (bs->ltgtype == LTG_RUSHBASE) {
|
|
switch(BotTeam(bs)) {
|
|
case TEAM_RED: memcpy(goal, &blueobelisk, sizeof(bot_goal_t)); break;
|
|
case TEAM_BLUE: memcpy(goal, &redobelisk, sizeof(bot_goal_t)); break;
|
|
default: BotGoHarvest(bs); return qfalse;
|
|
}
|
|
//if not carrying any cubes
|
|
if (!BotHarvesterCarryingCubes(bs)) {
|
|
BotGoHarvest(bs);
|
|
return qfalse;
|
|
}
|
|
//quit rushing after 2 minutes
|
|
if (bs->teamgoal_time < FloatTime()) {
|
|
BotGoHarvest(bs);
|
|
return qfalse;
|
|
}
|
|
//if touching the base flag the bot should loose the enemy flag
|
|
if (trap_BotTouchingGoal(bs->origin, goal)) {
|
|
BotGoHarvest(bs);
|
|
return qfalse;
|
|
}
|
|
BotAlternateRoute(bs, goal);
|
|
return qtrue;
|
|
}
|
|
//attack the enemy base
|
|
if (bs->ltgtype == LTG_ATTACKENEMYBASE &&
|
|
bs->attackaway_time < FloatTime()) {
|
|
//check for bot typing status message
|
|
if (bs->teammessage_time && bs->teammessage_time < FloatTime()) {
|
|
BotAI_BotInitialChat(bs, "attackenemybase_start", NULL);
|
|
trap_BotEnterChat(bs->cs, 0, CHAT_TEAM);
|
|
BotVoiceChatOnly(bs, -1, VOICECHAT_ONOFFENSE);
|
|
bs->teammessage_time = 0;
|
|
}
|
|
switch(BotTeam(bs)) {
|
|
case TEAM_RED: memcpy(goal, &blueobelisk, sizeof(bot_goal_t)); break;
|
|
case TEAM_BLUE: memcpy(goal, &redobelisk, sizeof(bot_goal_t)); break;
|
|
default: bs->ltgtype = 0; return qfalse;
|
|
}
|
|
//quit rushing after 2 minutes
|
|
if (bs->teamgoal_time < FloatTime()) {
|
|
bs->ltgtype = 0;
|
|
}
|
|
//if touching the base flag the bot should loose the enemy flag
|
|
if (trap_BotTouchingGoal(bs->origin, goal)) {
|
|
bs->attackaway_time = FloatTime() + 2 + 5 * random();
|
|
}
|
|
return qtrue;
|
|
}
|
|
//harvest cubes
|
|
if (bs->ltgtype == LTG_HARVEST &&
|
|
bs->harvestaway_time < FloatTime()) {
|
|
//check for bot typing status message
|
|
if (bs->teammessage_time && bs->teammessage_time < FloatTime()) {
|
|
BotAI_BotInitialChat(bs, "harvest_start", NULL);
|
|
trap_BotEnterChat(bs->cs, 0, CHAT_TEAM);
|
|
BotVoiceChatOnly(bs, -1, VOICECHAT_ONOFFENSE);
|
|
bs->teammessage_time = 0;
|
|
}
|
|
memcpy(goal, &neutralobelisk, sizeof(bot_goal_t));
|
|
//
|
|
if (bs->teamgoal_time < FloatTime()) {
|
|
bs->ltgtype = 0;
|
|
}
|
|
//
|
|
if (trap_BotTouchingGoal(bs->origin, goal)) {
|
|
bs->harvestaway_time = FloatTime() + 4 + 3 * random();
|
|
}
|
|
return qtrue;
|
|
}
|
|
}
|
|
#endif
|
|
//normal goal stuff
|
|
return BotGetItemLongTermGoal(bs, tfl, goal);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotLongTermGoal
|
|
==================
|
|
*/
|
|
int BotLongTermGoal(bot_state_t *bs, int tfl, int retreat, bot_goal_t *goal) {
|
|
aas_entityinfo_t entinfo;
|
|
char teammate[MAX_MESSAGE_SIZE];
|
|
float squaredist;
|
|
int areanum;
|
|
vec3_t dir;
|
|
|
|
//FIXME: also have air long term goals?
|
|
//
|
|
//if the bot is leading someone and not retreating
|
|
if (bs->lead_time > 0 && !retreat) {
|
|
if (bs->lead_time < FloatTime()) {
|
|
BotAI_BotInitialChat(bs, "lead_stop", EasyClientName(bs->lead_teammate, teammate, sizeof(teammate)), NULL);
|
|
trap_BotEnterChat(bs->cs, bs->teammate, CHAT_TELL);
|
|
bs->lead_time = 0;
|
|
return BotGetLongTermGoal(bs, tfl, retreat, goal);
|
|
}
|
|
//
|
|
if (bs->leadmessage_time < 0 && -bs->leadmessage_time < FloatTime()) {
|
|
BotAI_BotInitialChat(bs, "followme", EasyClientName(bs->lead_teammate, teammate, sizeof(teammate)), NULL);
|
|
trap_BotEnterChat(bs->cs, bs->teammate, CHAT_TELL);
|
|
bs->leadmessage_time = FloatTime();
|
|
}
|
|
//get entity information of the companion
|
|
BotEntityInfo(bs->lead_teammate, &entinfo);
|
|
//
|
|
if (entinfo.valid) {
|
|
areanum = BotPointAreaNum(entinfo.origin);
|
|
if (areanum && trap_AAS_AreaReachability(areanum)) {
|
|
//update team goal
|
|
bs->lead_teamgoal.entitynum = bs->lead_teammate;
|
|
bs->lead_teamgoal.areanum = areanum;
|
|
VectorCopy(entinfo.origin, bs->lead_teamgoal.origin);
|
|
VectorSet(bs->lead_teamgoal.mins, -8, -8, -8);
|
|
VectorSet(bs->lead_teamgoal.maxs, 8, 8, 8);
|
|
}
|
|
}
|
|
//if the team mate is visible
|
|
if (BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, bs->lead_teammate)) {
|
|
bs->leadvisible_time = FloatTime();
|
|
}
|
|
//if the team mate is not visible for 1 seconds
|
|
if (bs->leadvisible_time < FloatTime() - 1) {
|
|
bs->leadbackup_time = FloatTime() + 2;
|
|
}
|
|
//distance towards the team mate
|
|
VectorSubtract(bs->origin, bs->lead_teamgoal.origin, dir);
|
|
squaredist = VectorLengthSquared(dir);
|
|
//if backing up towards the team mate
|
|
if (bs->leadbackup_time > FloatTime()) {
|
|
if (bs->leadmessage_time < FloatTime() - 20) {
|
|
BotAI_BotInitialChat(bs, "followme", EasyClientName(bs->lead_teammate, teammate, sizeof(teammate)), NULL);
|
|
trap_BotEnterChat(bs->cs, bs->teammate, CHAT_TELL);
|
|
bs->leadmessage_time = FloatTime();
|
|
}
|
|
//if very close to the team mate
|
|
if (squaredist < Square(100)) {
|
|
bs->leadbackup_time = 0;
|
|
}
|
|
//the bot should go back to the team mate
|
|
memcpy(goal, &bs->lead_teamgoal, sizeof(bot_goal_t));
|
|
return qtrue;
|
|
}
|
|
else {
|
|
//if quite distant from the team mate
|
|
if (squaredist > Square(500)) {
|
|
if (bs->leadmessage_time < FloatTime() - 20) {
|
|
BotAI_BotInitialChat(bs, "followme", EasyClientName(bs->lead_teammate, teammate, sizeof(teammate)), NULL);
|
|
trap_BotEnterChat(bs->cs, bs->teammate, CHAT_TELL);
|
|
bs->leadmessage_time = FloatTime();
|
|
}
|
|
//look at the team mate
|
|
VectorSubtract(entinfo.origin, bs->origin, dir);
|
|
vectoangles(dir, bs->ideal_viewangles);
|
|
bs->ideal_viewangles[2] *= 0.5;
|
|
//just wait for the team mate
|
|
return qfalse;
|
|
}
|
|
}
|
|
}
|
|
return BotGetLongTermGoal(bs, tfl, retreat, goal);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AIEnter_Intermission
|
|
==================
|
|
*/
|
|
void AIEnter_Intermission(bot_state_t *bs, char *s) {
|
|
BotRecordNodeSwitch(bs, "intermission", "", s);
|
|
//reset the bot state
|
|
BotResetState(bs);
|
|
//check for end level chat
|
|
if (BotChat_EndLevel(bs)) {
|
|
trap_BotEnterChat(bs->cs, 0, bs->chatto);
|
|
}
|
|
bs->ainode = AINode_Intermission;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AINode_Intermission
|
|
==================
|
|
*/
|
|
int AINode_Intermission(bot_state_t *bs) {
|
|
//if the intermission ended
|
|
if (!BotIntermission(bs)) {
|
|
if (BotChat_StartLevel(bs)) {
|
|
bs->stand_time = FloatTime() + BotChatTime(bs);
|
|
}
|
|
else {
|
|
bs->stand_time = FloatTime() + 2;
|
|
}
|
|
AIEnter_Stand(bs, "intermission: chat");
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AIEnter_Observer
|
|
==================
|
|
*/
|
|
void AIEnter_Observer(bot_state_t *bs, char *s) {
|
|
BotRecordNodeSwitch(bs, "observer", "", s);
|
|
//reset the bot state
|
|
BotResetState(bs);
|
|
bs->ainode = AINode_Observer;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AINode_Observer
|
|
==================
|
|
*/
|
|
int AINode_Observer(bot_state_t *bs) {
|
|
//if the bot left observer mode
|
|
if (!BotIsObserver(bs)) {
|
|
AIEnter_Stand(bs, "observer: left observer");
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AIEnter_Stand
|
|
==================
|
|
*/
|
|
void AIEnter_Stand(bot_state_t *bs, char *s) {
|
|
BotRecordNodeSwitch(bs, "stand", "", s);
|
|
bs->standfindenemy_time = FloatTime() + 1;
|
|
bs->ainode = AINode_Stand;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AINode_Stand
|
|
==================
|
|
*/
|
|
int AINode_Stand(bot_state_t *bs) {
|
|
|
|
//if the bot's health decreased
|
|
if (bs->lastframe_health > bs->inventory[INVENTORY_HEALTH]) {
|
|
if (BotChat_HitTalking(bs)) {
|
|
bs->standfindenemy_time = FloatTime() + BotChatTime(bs) + 0.1;
|
|
bs->stand_time = FloatTime() + BotChatTime(bs) + 0.1;
|
|
}
|
|
}
|
|
if (bs->standfindenemy_time < FloatTime()) {
|
|
if (BotFindEnemy(bs, -1)) {
|
|
AIEnter_Battle_Fight(bs, "stand: found enemy");
|
|
return qfalse;
|
|
}
|
|
bs->standfindenemy_time = FloatTime() + 1;
|
|
}
|
|
// put up chat icon
|
|
trap_EA_Talk(bs->client);
|
|
// when done standing
|
|
if (bs->stand_time < FloatTime()) {
|
|
trap_BotEnterChat(bs->cs, 0, bs->chatto);
|
|
AIEnter_Seek_LTG(bs, "stand: time out");
|
|
return qfalse;
|
|
}
|
|
//
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AIEnter_Respawn
|
|
==================
|
|
*/
|
|
void AIEnter_Respawn(bot_state_t *bs, char *s) {
|
|
BotRecordNodeSwitch(bs, "respawn", "", s);
|
|
//reset some states
|
|
trap_BotResetMoveState(bs->ms);
|
|
trap_BotResetGoalState(bs->gs);
|
|
trap_BotResetAvoidGoals(bs->gs);
|
|
trap_BotResetAvoidReach(bs->ms);
|
|
//if the bot wants to chat
|
|
if (BotChat_Death(bs)) {
|
|
bs->respawn_time = FloatTime() + BotChatTime(bs);
|
|
bs->respawnchat_time = FloatTime();
|
|
}
|
|
else {
|
|
bs->respawn_time = FloatTime() + 1 + random();
|
|
bs->respawnchat_time = 0;
|
|
}
|
|
//set respawn state
|
|
bs->respawn_wait = qfalse;
|
|
bs->ainode = AINode_Respawn;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AINode_Respawn
|
|
==================
|
|
*/
|
|
int AINode_Respawn(bot_state_t *bs) {
|
|
// if waiting for the actual respawn
|
|
if (bs->respawn_wait) {
|
|
if (!BotIsDead(bs)) {
|
|
AIEnter_Seek_LTG(bs, "respawn: respawned");
|
|
}
|
|
else {
|
|
trap_EA_Respawn(bs->client);
|
|
}
|
|
}
|
|
else if (bs->respawn_time < FloatTime()) {
|
|
// wait until respawned
|
|
bs->respawn_wait = qtrue;
|
|
// elementary action respawn
|
|
trap_EA_Respawn(bs->client);
|
|
//
|
|
if (bs->respawnchat_time) {
|
|
trap_BotEnterChat(bs->cs, 0, bs->chatto);
|
|
bs->enemy = -1;
|
|
}
|
|
}
|
|
if (bs->respawnchat_time && bs->respawnchat_time < FloatTime() - 0.5) {
|
|
trap_EA_Talk(bs->client);
|
|
}
|
|
//
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotSelectActivateWeapon
|
|
==================
|
|
*/
|
|
int BotSelectActivateWeapon(bot_state_t *bs) {
|
|
//
|
|
if (bs->inventory[INVENTORY_MACHINEGUN] > 0 && bs->inventory[INVENTORY_BULLETS] > 0)
|
|
return WEAPONINDEX_MACHINEGUN;
|
|
else if (bs->inventory[INVENTORY_SHOTGUN] > 0 && bs->inventory[INVENTORY_SHELLS] > 0)
|
|
return WEAPONINDEX_SHOTGUN;
|
|
else if (bs->inventory[INVENTORY_PLASMAGUN] > 0 && bs->inventory[INVENTORY_CELLS] > 0)
|
|
return WEAPONINDEX_PLASMAGUN;
|
|
else if (bs->inventory[INVENTORY_LIGHTNING] > 0 && bs->inventory[INVENTORY_LIGHTNINGAMMO] > 0)
|
|
return WEAPONINDEX_LIGHTNING;
|
|
#ifdef MISSIONPACK
|
|
else if (bs->inventory[INVENTORY_CHAINGUN] > 0 && bs->inventory[INVENTORY_BELT] > 0)
|
|
return WEAPONINDEX_CHAINGUN;
|
|
else if (bs->inventory[INVENTORY_NAILGUN] > 0 && bs->inventory[INVENTORY_NAILS] > 0)
|
|
return WEAPONINDEX_NAILGUN;
|
|
else if (bs->inventory[INVENTORY_PROXLAUNCHER] > 0 && bs->inventory[INVENTORY_MINES] > 0)
|
|
return WEAPONINDEX_PROXLAUNCHER;
|
|
#endif
|
|
else if (bs->inventory[INVENTORY_GRENADELAUNCHER] > 0 && bs->inventory[INVENTORY_GRENADES] > 0)
|
|
return WEAPONINDEX_GRENADE_LAUNCHER;
|
|
else if (bs->inventory[INVENTORY_RAILGUN] > 0 && bs->inventory[INVENTORY_SLUGS] > 0)
|
|
return WEAPONINDEX_RAILGUN;
|
|
else if (bs->inventory[INVENTORY_ROCKETLAUNCHER] > 0 && bs->inventory[INVENTORY_ROCKETS] > 0)
|
|
return WEAPONINDEX_ROCKET_LAUNCHER;
|
|
else if (bs->inventory[INVENTORY_BFG10K] > 0 && bs->inventory[INVENTORY_BFGAMMO] > 0)
|
|
return WEAPONINDEX_BFG;
|
|
else {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotClearPath
|
|
|
|
try to deactivate obstacles like proximity mines on the bot's path
|
|
==================
|
|
*/
|
|
void BotClearPath(bot_state_t *bs, bot_moveresult_t *moveresult) {
|
|
int i, bestmine;
|
|
float dist, bestdist;
|
|
vec3_t target, dir;
|
|
bsp_trace_t bsptrace;
|
|
entityState_t state;
|
|
|
|
// if there is a dead body wearing kamikze nearby
|
|
if (bs->kamikazebody) {
|
|
// if the bot's view angles and weapon are not used for movement
|
|
if ( !(moveresult->flags & (MOVERESULT_MOVEMENTVIEW | MOVERESULT_MOVEMENTWEAPON)) ) {
|
|
//
|
|
BotAI_GetEntityState(bs->kamikazebody, &state);
|
|
VectorCopy(state.pos.trBase, target);
|
|
target[2] += 8;
|
|
VectorSubtract(target, bs->eye, dir);
|
|
vectoangles(dir, moveresult->ideal_viewangles);
|
|
//
|
|
moveresult->weapon = BotSelectActivateWeapon(bs);
|
|
if (moveresult->weapon == -1) {
|
|
// FIXME: run away!
|
|
moveresult->weapon = 0;
|
|
}
|
|
if (moveresult->weapon) {
|
|
//
|
|
moveresult->flags |= MOVERESULT_MOVEMENTWEAPON | MOVERESULT_MOVEMENTVIEW;
|
|
// if holding the right weapon
|
|
if (bs->cur_ps.weapon == moveresult->weapon) {
|
|
// if the bot is pretty close with its aim
|
|
if (InFieldOfVision(bs->viewangles, 20, moveresult->ideal_viewangles)) {
|
|
//
|
|
BotAI_Trace(&bsptrace, bs->eye, NULL, NULL, target, bs->entitynum, MASK_SHOT);
|
|
// if the mine is visible from the current position
|
|
if (bsptrace.fraction >= 1.0 || bsptrace.ent == state.number) {
|
|
// shoot at the mine
|
|
trap_EA_Attack(bs->client);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (moveresult->flags & MOVERESULT_BLOCKEDBYAVOIDSPOT) {
|
|
bs->blockedbyavoidspot_time = FloatTime() + 5;
|
|
}
|
|
// if blocked by an avoid spot and the view angles and weapon are used for movement
|
|
if (bs->blockedbyavoidspot_time > FloatTime() &&
|
|
!(moveresult->flags & (MOVERESULT_MOVEMENTVIEW | MOVERESULT_MOVEMENTWEAPON)) ) {
|
|
bestdist = 300;
|
|
bestmine = -1;
|
|
for (i = 0; i < bs->numproxmines; i++) {
|
|
BotAI_GetEntityState(bs->proxmines[i], &state);
|
|
VectorSubtract(state.pos.trBase, bs->origin, dir);
|
|
dist = VectorLength(dir);
|
|
if (dist < bestdist) {
|
|
bestdist = dist;
|
|
bestmine = i;
|
|
}
|
|
}
|
|
if (bestmine != -1) {
|
|
//
|
|
// state->generic1 == TEAM_RED || state->generic1 == TEAM_BLUE
|
|
//
|
|
// deactivate prox mines in the bot's path by shooting
|
|
// rockets or plasma cells etc. at them
|
|
BotAI_GetEntityState(bs->proxmines[bestmine], &state);
|
|
VectorCopy(state.pos.trBase, target);
|
|
target[2] += 2;
|
|
VectorSubtract(target, bs->eye, dir);
|
|
vectoangles(dir, moveresult->ideal_viewangles);
|
|
// if the bot has a weapon that does splash damage
|
|
if (bs->inventory[INVENTORY_PLASMAGUN] > 0 && bs->inventory[INVENTORY_CELLS] > 0)
|
|
moveresult->weapon = WEAPONINDEX_PLASMAGUN;
|
|
else if (bs->inventory[INVENTORY_ROCKETLAUNCHER] > 0 && bs->inventory[INVENTORY_ROCKETS] > 0)
|
|
moveresult->weapon = WEAPONINDEX_ROCKET_LAUNCHER;
|
|
else if (bs->inventory[INVENTORY_BFG10K] > 0 && bs->inventory[INVENTORY_BFGAMMO] > 0)
|
|
moveresult->weapon = WEAPONINDEX_BFG;
|
|
else {
|
|
moveresult->weapon = 0;
|
|
}
|
|
if (moveresult->weapon) {
|
|
//
|
|
moveresult->flags |= MOVERESULT_MOVEMENTWEAPON | MOVERESULT_MOVEMENTVIEW;
|
|
// if holding the right weapon
|
|
if (bs->cur_ps.weapon == moveresult->weapon) {
|
|
// if the bot is pretty close with its aim
|
|
if (InFieldOfVision(bs->viewangles, 20, moveresult->ideal_viewangles)) {
|
|
//
|
|
BotAI_Trace(&bsptrace, bs->eye, NULL, NULL, target, bs->entitynum, MASK_SHOT);
|
|
// if the mine is visible from the current position
|
|
if (bsptrace.fraction >= 1.0 || bsptrace.ent == state.number) {
|
|
// shoot at the mine
|
|
trap_EA_Attack(bs->client);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AIEnter_Seek_ActivateEntity
|
|
==================
|
|
*/
|
|
void AIEnter_Seek_ActivateEntity(bot_state_t *bs, char *s) {
|
|
BotRecordNodeSwitch(bs, "activate entity", "", s);
|
|
bs->ainode = AINode_Seek_ActivateEntity;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AINode_Seek_Activate_Entity
|
|
==================
|
|
*/
|
|
int AINode_Seek_ActivateEntity(bot_state_t *bs) {
|
|
bot_goal_t *goal;
|
|
vec3_t target, dir, ideal_viewangles;
|
|
bot_moveresult_t moveresult;
|
|
int targetvisible;
|
|
bsp_trace_t bsptrace;
|
|
aas_entityinfo_t entinfo;
|
|
|
|
if (BotIsObserver(bs)) {
|
|
BotClearActivateGoalStack(bs);
|
|
AIEnter_Observer(bs, "active entity: observer");
|
|
return qfalse;
|
|
}
|
|
//if in the intermission
|
|
if (BotIntermission(bs)) {
|
|
BotClearActivateGoalStack(bs);
|
|
AIEnter_Intermission(bs, "activate entity: intermission");
|
|
return qfalse;
|
|
}
|
|
//respawn if dead
|
|
if (BotIsDead(bs)) {
|
|
BotClearActivateGoalStack(bs);
|
|
AIEnter_Respawn(bs, "activate entity: bot dead");
|
|
return qfalse;
|
|
}
|
|
//
|
|
bs->tfl = TFL_DEFAULT;
|
|
if (bot_grapple.integer) bs->tfl |= TFL_GRAPPLEHOOK;
|
|
// if in lava or slime the bot should be able to get out
|
|
if (BotInLavaOrSlime(bs)) bs->tfl |= TFL_LAVA|TFL_SLIME;
|
|
// map specific code
|
|
BotMapScripts(bs);
|
|
// no enemy
|
|
bs->enemy = -1;
|
|
// if the bot has no activate goal
|
|
if (!bs->activatestack) {
|
|
BotClearActivateGoalStack(bs);
|
|
AIEnter_Seek_NBG(bs, "activate entity: no goal");
|
|
return qfalse;
|
|
}
|
|
//
|
|
goal = &bs->activatestack->goal;
|
|
// initialize target being visible to false
|
|
targetvisible = qfalse;
|
|
// if the bot has to shoot at a target to activate something
|
|
if (bs->activatestack->shoot) {
|
|
//
|
|
BotAI_Trace(&bsptrace, bs->eye, NULL, NULL, bs->activatestack->target, bs->entitynum, MASK_SHOT);
|
|
// if the shootable entity is visible from the current position
|
|
if (bsptrace.fraction >= 1.0 || bsptrace.ent == goal->entitynum) {
|
|
targetvisible = qtrue;
|
|
// if holding the right weapon
|
|
if (bs->cur_ps.weapon == bs->activatestack->weapon) {
|
|
VectorSubtract(bs->activatestack->target, bs->eye, dir);
|
|
vectoangles(dir, ideal_viewangles);
|
|
// if the bot is pretty close with its aim
|
|
if (InFieldOfVision(bs->viewangles, 20, ideal_viewangles)) {
|
|
trap_EA_Attack(bs->client);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// if the shoot target is visible
|
|
if (targetvisible) {
|
|
// get the entity info of the entity the bot is shooting at
|
|
BotEntityInfo(goal->entitynum, &entinfo);
|
|
// if the entity the bot shoots at moved
|
|
if (!VectorCompare(bs->activatestack->origin, entinfo.origin)) {
|
|
#ifdef DEBUG
|
|
BotAI_Print(PRT_MESSAGE, "hit shootable button or trigger\n");
|
|
#endif //DEBUG
|
|
bs->activatestack->time = 0;
|
|
}
|
|
// if the activate goal has been activated or the bot takes too long
|
|
if (bs->activatestack->time < FloatTime()) {
|
|
BotPopFromActivateGoalStack(bs);
|
|
// if there are more activate goals on the stack
|
|
if (bs->activatestack) {
|
|
bs->activatestack->time = FloatTime() + 10;
|
|
return qfalse;
|
|
}
|
|
AIEnter_Seek_NBG(bs, "activate entity: time out");
|
|
return qfalse;
|
|
}
|
|
memset(&moveresult, 0, sizeof(bot_moveresult_t));
|
|
}
|
|
else {
|
|
// if the bot has no goal
|
|
if (!goal) {
|
|
bs->activatestack->time = 0;
|
|
}
|
|
// if the bot does not have a shoot goal
|
|
else if (!bs->activatestack->shoot) {
|
|
//if the bot touches the current goal
|
|
if (trap_BotTouchingGoal(bs->origin, goal)) {
|
|
#ifdef DEBUG
|
|
BotAI_Print(PRT_MESSAGE, "touched button or trigger\n");
|
|
#endif //DEBUG
|
|
bs->activatestack->time = 0;
|
|
}
|
|
}
|
|
// if the activate goal has been activated or the bot takes too long
|
|
if (bs->activatestack->time < FloatTime()) {
|
|
BotPopFromActivateGoalStack(bs);
|
|
// if there are more activate goals on the stack
|
|
if (bs->activatestack) {
|
|
bs->activatestack->time = FloatTime() + 10;
|
|
return qfalse;
|
|
}
|
|
AIEnter_Seek_NBG(bs, "activate entity: activated");
|
|
return qfalse;
|
|
}
|
|
//predict obstacles
|
|
if (BotAIPredictObstacles(bs, goal))
|
|
return qfalse;
|
|
//initialize the movement state
|
|
BotSetupForMovement(bs);
|
|
//move towards the goal
|
|
trap_BotMoveToGoal(&moveresult, bs->ms, goal, bs->tfl);
|
|
//if the movement failed
|
|
if (moveresult.failure) {
|
|
//reset the avoid reach, otherwise bot is stuck in current area
|
|
trap_BotResetAvoidReach(bs->ms);
|
|
//
|
|
bs->activatestack->time = 0;
|
|
}
|
|
//check if the bot is blocked
|
|
BotAIBlocked(bs, &moveresult, qtrue);
|
|
}
|
|
//
|
|
BotClearPath(bs, &moveresult);
|
|
// if the bot has to shoot to activate
|
|
if (bs->activatestack->shoot) {
|
|
// if the view angles aren't yet used for the movement
|
|
if (!(moveresult.flags & MOVERESULT_MOVEMENTVIEW)) {
|
|
VectorSubtract(bs->activatestack->target, bs->eye, dir);
|
|
vectoangles(dir, moveresult.ideal_viewangles);
|
|
moveresult.flags |= MOVERESULT_MOVEMENTVIEW;
|
|
}
|
|
// if there's no weapon yet used for the movement
|
|
if (!(moveresult.flags & MOVERESULT_MOVEMENTWEAPON)) {
|
|
moveresult.flags |= MOVERESULT_MOVEMENTWEAPON;
|
|
//
|
|
bs->activatestack->weapon = BotSelectActivateWeapon(bs);
|
|
if (bs->activatestack->weapon == -1) {
|
|
//FIXME: find a decent weapon first
|
|
bs->activatestack->weapon = 0;
|
|
}
|
|
moveresult.weapon = bs->activatestack->weapon;
|
|
}
|
|
}
|
|
// if the ideal view angles are set for movement
|
|
if (moveresult.flags & (MOVERESULT_MOVEMENTVIEWSET|MOVERESULT_MOVEMENTVIEW|MOVERESULT_SWIMVIEW)) {
|
|
VectorCopy(moveresult.ideal_viewangles, bs->ideal_viewangles);
|
|
}
|
|
// if waiting for something
|
|
else if (moveresult.flags & MOVERESULT_WAITING) {
|
|
if (random() < bs->thinktime * 0.8) {
|
|
BotRoamGoal(bs, target);
|
|
VectorSubtract(target, bs->origin, dir);
|
|
vectoangles(dir, bs->ideal_viewangles);
|
|
bs->ideal_viewangles[2] *= 0.5;
|
|
}
|
|
}
|
|
else if (!(bs->flags & BFL_IDEALVIEWSET)) {
|
|
if (trap_BotMovementViewTarget(bs->ms, goal, bs->tfl, 300, target)) {
|
|
VectorSubtract(target, bs->origin, dir);
|
|
vectoangles(dir, bs->ideal_viewangles);
|
|
}
|
|
else {
|
|
vectoangles(moveresult.movedir, bs->ideal_viewangles);
|
|
}
|
|
bs->ideal_viewangles[2] *= 0.5;
|
|
}
|
|
// if the weapon is used for the bot movement
|
|
if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON)
|
|
bs->weaponnum = moveresult.weapon;
|
|
// if there is an enemy
|
|
if (BotFindEnemy(bs, -1)) {
|
|
if (BotWantsToRetreat(bs)) {
|
|
//keep the current long term goal and retreat
|
|
AIEnter_Battle_NBG(bs, "activate entity: found enemy");
|
|
}
|
|
else {
|
|
trap_BotResetLastAvoidReach(bs->ms);
|
|
//empty the goal stack
|
|
trap_BotEmptyGoalStack(bs->gs);
|
|
//go fight
|
|
AIEnter_Battle_Fight(bs, "activate entity: found enemy");
|
|
}
|
|
BotClearActivateGoalStack(bs);
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AIEnter_Seek_NBG
|
|
==================
|
|
*/
|
|
void AIEnter_Seek_NBG(bot_state_t *bs, char *s) {
|
|
bot_goal_t goal;
|
|
char buf[144];
|
|
|
|
if (trap_BotGetTopGoal(bs->gs, &goal)) {
|
|
trap_BotGoalName(goal.number, buf, 144);
|
|
BotRecordNodeSwitch(bs, "seek NBG", buf, s);
|
|
}
|
|
else {
|
|
BotRecordNodeSwitch(bs, "seek NBG", "no goal", s);
|
|
}
|
|
bs->ainode = AINode_Seek_NBG;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AINode_Seek_NBG
|
|
==================
|
|
*/
|
|
int AINode_Seek_NBG(bot_state_t *bs) {
|
|
bot_goal_t goal;
|
|
vec3_t target, dir;
|
|
bot_moveresult_t moveresult;
|
|
|
|
if (BotIsObserver(bs)) {
|
|
AIEnter_Observer(bs, "seek nbg: observer");
|
|
return qfalse;
|
|
}
|
|
//if in the intermission
|
|
if (BotIntermission(bs)) {
|
|
AIEnter_Intermission(bs, "seek nbg: intermision");
|
|
return qfalse;
|
|
}
|
|
//respawn if dead
|
|
if (BotIsDead(bs)) {
|
|
AIEnter_Respawn(bs, "seek nbg: bot dead");
|
|
return qfalse;
|
|
}
|
|
//
|
|
bs->tfl = TFL_DEFAULT;
|
|
if (bot_grapple.integer) bs->tfl |= TFL_GRAPPLEHOOK;
|
|
//if in lava or slime the bot should be able to get out
|
|
if (BotInLavaOrSlime(bs)) bs->tfl |= TFL_LAVA|TFL_SLIME;
|
|
//
|
|
if (BotCanAndWantsToRocketJump(bs)) {
|
|
bs->tfl |= TFL_ROCKETJUMP;
|
|
}
|
|
//map specific code
|
|
BotMapScripts(bs);
|
|
//no enemy
|
|
bs->enemy = -1;
|
|
//if the bot has no goal
|
|
if (!trap_BotGetTopGoal(bs->gs, &goal)) bs->nbg_time = 0;
|
|
//if the bot touches the current goal
|
|
else if (BotReachedGoal(bs, &goal)) {
|
|
BotChooseWeapon(bs);
|
|
bs->nbg_time = 0;
|
|
}
|
|
//
|
|
if (bs->nbg_time < FloatTime()) {
|
|
//pop the current goal from the stack
|
|
trap_BotPopGoal(bs->gs);
|
|
//check for new nearby items right away
|
|
//NOTE: we canNOT reset the check_time to zero because it would create an endless loop of node switches
|
|
bs->check_time = FloatTime() + 0.05;
|
|
//go back to seek ltg
|
|
AIEnter_Seek_LTG(bs, "seek nbg: time out");
|
|
return qfalse;
|
|
}
|
|
//predict obstacles
|
|
if (BotAIPredictObstacles(bs, &goal))
|
|
return qfalse;
|
|
//initialize the movement state
|
|
BotSetupForMovement(bs);
|
|
//move towards the goal
|
|
trap_BotMoveToGoal(&moveresult, bs->ms, &goal, bs->tfl);
|
|
//if the movement failed
|
|
if (moveresult.failure) {
|
|
//reset the avoid reach, otherwise bot is stuck in current area
|
|
trap_BotResetAvoidReach(bs->ms);
|
|
bs->nbg_time = 0;
|
|
}
|
|
//check if the bot is blocked
|
|
BotAIBlocked(bs, &moveresult, qtrue);
|
|
//
|
|
BotClearPath(bs, &moveresult);
|
|
//if the viewangles are used for the movement
|
|
if (moveresult.flags & (MOVERESULT_MOVEMENTVIEWSET|MOVERESULT_MOVEMENTVIEW|MOVERESULT_SWIMVIEW)) {
|
|
VectorCopy(moveresult.ideal_viewangles, bs->ideal_viewangles);
|
|
}
|
|
//if waiting for something
|
|
else if (moveresult.flags & MOVERESULT_WAITING) {
|
|
if (random() < bs->thinktime * 0.8) {
|
|
BotRoamGoal(bs, target);
|
|
VectorSubtract(target, bs->origin, dir);
|
|
vectoangles(dir, bs->ideal_viewangles);
|
|
bs->ideal_viewangles[2] *= 0.5;
|
|
}
|
|
}
|
|
else if (!(bs->flags & BFL_IDEALVIEWSET)) {
|
|
if (!trap_BotGetSecondGoal(bs->gs, &goal)) trap_BotGetTopGoal(bs->gs, &goal);
|
|
if (trap_BotMovementViewTarget(bs->ms, &goal, bs->tfl, 300, target)) {
|
|
VectorSubtract(target, bs->origin, dir);
|
|
vectoangles(dir, bs->ideal_viewangles);
|
|
}
|
|
//FIXME: look at cluster portals?
|
|
else vectoangles(moveresult.movedir, bs->ideal_viewangles);
|
|
bs->ideal_viewangles[2] *= 0.5;
|
|
}
|
|
//if the weapon is used for the bot movement
|
|
if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) bs->weaponnum = moveresult.weapon;
|
|
//if there is an enemy
|
|
if (BotFindEnemy(bs, -1)) {
|
|
if (BotWantsToRetreat(bs)) {
|
|
//keep the current long term goal and retreat
|
|
AIEnter_Battle_NBG(bs, "seek nbg: found enemy");
|
|
}
|
|
else {
|
|
trap_BotResetLastAvoidReach(bs->ms);
|
|
//empty the goal stack
|
|
trap_BotEmptyGoalStack(bs->gs);
|
|
//go fight
|
|
AIEnter_Battle_Fight(bs, "seek nbg: found enemy");
|
|
}
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AIEnter_Seek_LTG
|
|
==================
|
|
*/
|
|
void AIEnter_Seek_LTG(bot_state_t *bs, char *s) {
|
|
bot_goal_t goal;
|
|
char buf[144];
|
|
|
|
if (trap_BotGetTopGoal(bs->gs, &goal)) {
|
|
trap_BotGoalName(goal.number, buf, 144);
|
|
BotRecordNodeSwitch(bs, "seek LTG", buf, s);
|
|
}
|
|
else {
|
|
BotRecordNodeSwitch(bs, "seek LTG", "no goal", s);
|
|
}
|
|
bs->ainode = AINode_Seek_LTG;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AINode_Seek_LTG
|
|
==================
|
|
*/
|
|
int AINode_Seek_LTG(bot_state_t *bs)
|
|
{
|
|
bot_goal_t goal;
|
|
vec3_t target, dir;
|
|
bot_moveresult_t moveresult;
|
|
int range;
|
|
//char buf[128];
|
|
//bot_goal_t tmpgoal;
|
|
|
|
if (BotIsObserver(bs)) {
|
|
AIEnter_Observer(bs, "seek ltg: observer");
|
|
return qfalse;
|
|
}
|
|
//if in the intermission
|
|
if (BotIntermission(bs)) {
|
|
AIEnter_Intermission(bs, "seek ltg: intermission");
|
|
return qfalse;
|
|
}
|
|
//respawn if dead
|
|
if (BotIsDead(bs)) {
|
|
AIEnter_Respawn(bs, "seek ltg: bot dead");
|
|
return qfalse;
|
|
}
|
|
//
|
|
if (BotChat_Random(bs)) {
|
|
bs->stand_time = FloatTime() + BotChatTime(bs);
|
|
AIEnter_Stand(bs, "seek ltg: random chat");
|
|
return qfalse;
|
|
}
|
|
//
|
|
bs->tfl = TFL_DEFAULT;
|
|
if (bot_grapple.integer) bs->tfl |= TFL_GRAPPLEHOOK;
|
|
//if in lava or slime the bot should be able to get out
|
|
if (BotInLavaOrSlime(bs)) bs->tfl |= TFL_LAVA|TFL_SLIME;
|
|
//
|
|
if (BotCanAndWantsToRocketJump(bs)) {
|
|
bs->tfl |= TFL_ROCKETJUMP;
|
|
}
|
|
//map specific code
|
|
BotMapScripts(bs);
|
|
//no enemy
|
|
bs->enemy = -1;
|
|
//
|
|
if (bs->killedenemy_time > FloatTime() - 2) {
|
|
if (random() < bs->thinktime * 1) {
|
|
trap_EA_Gesture(bs->client);
|
|
}
|
|
}
|
|
//if there is an enemy
|
|
if (BotFindEnemy(bs, -1)) {
|
|
if (BotWantsToRetreat(bs)) {
|
|
//keep the current long term goal and retreat
|
|
AIEnter_Battle_Retreat(bs, "seek ltg: found enemy");
|
|
return qfalse;
|
|
}
|
|
else {
|
|
trap_BotResetLastAvoidReach(bs->ms);
|
|
//empty the goal stack
|
|
trap_BotEmptyGoalStack(bs->gs);
|
|
//go fight
|
|
AIEnter_Battle_Fight(bs, "seek ltg: found enemy");
|
|
return qfalse;
|
|
}
|
|
}
|
|
//
|
|
BotTeamGoals(bs, qfalse);
|
|
//get the current long term goal
|
|
if (!BotLongTermGoal(bs, bs->tfl, qfalse, &goal)) {
|
|
return qtrue;
|
|
}
|
|
//check for nearby goals periodicly
|
|
if (bs->check_time < FloatTime()) {
|
|
bs->check_time = FloatTime() + 0.5;
|
|
//check if the bot wants to camp
|
|
BotWantsToCamp(bs);
|
|
//
|
|
if (bs->ltgtype == LTG_DEFENDKEYAREA) range = 400;
|
|
else range = 150;
|
|
//
|
|
#ifdef CTF
|
|
if (gametype == GT_CTF) {
|
|
//if carrying a flag the bot shouldn't be distracted too much
|
|
if (BotCTFCarryingFlag(bs))
|
|
range = 50;
|
|
}
|
|
#endif //CTF
|
|
#ifdef MISSIONPACK
|
|
else if (gametype == GT_1FCTF) {
|
|
if (Bot1FCTFCarryingFlag(bs))
|
|
range = 50;
|
|
}
|
|
else if (gametype == GT_HARVESTER) {
|
|
if (BotHarvesterCarryingCubes(bs))
|
|
range = 80;
|
|
}
|
|
#endif
|
|
//
|
|
if (BotNearbyGoal(bs, bs->tfl, &goal, range)) {
|
|
trap_BotResetLastAvoidReach(bs->ms);
|
|
//get the goal at the top of the stack
|
|
//trap_BotGetTopGoal(bs->gs, &tmpgoal);
|
|
//trap_BotGoalName(tmpgoal.number, buf, 144);
|
|
//BotAI_Print(PRT_MESSAGE, "new nearby goal %s\n", buf);
|
|
//time the bot gets to pick up the nearby goal item
|
|
bs->nbg_time = FloatTime() + 4 + range * 0.01;
|
|
AIEnter_Seek_NBG(bs, "ltg seek: nbg");
|
|
return qfalse;
|
|
}
|
|
}
|
|
//predict obstacles
|
|
if (BotAIPredictObstacles(bs, &goal))
|
|
return qfalse;
|
|
//initialize the movement state
|
|
BotSetupForMovement(bs);
|
|
//move towards the goal
|
|
trap_BotMoveToGoal(&moveresult, bs->ms, &goal, bs->tfl);
|
|
//if the movement failed
|
|
if (moveresult.failure) {
|
|
//reset the avoid reach, otherwise bot is stuck in current area
|
|
trap_BotResetAvoidReach(bs->ms);
|
|
//BotAI_Print(PRT_MESSAGE, "movement failure %d\n", moveresult.traveltype);
|
|
bs->ltg_time = 0;
|
|
}
|
|
//
|
|
BotAIBlocked(bs, &moveresult, qtrue);
|
|
//
|
|
BotClearPath(bs, &moveresult);
|
|
//if the viewangles are used for the movement
|
|
if (moveresult.flags & (MOVERESULT_MOVEMENTVIEWSET|MOVERESULT_MOVEMENTVIEW|MOVERESULT_SWIMVIEW)) {
|
|
VectorCopy(moveresult.ideal_viewangles, bs->ideal_viewangles);
|
|
}
|
|
//if waiting for something
|
|
else if (moveresult.flags & MOVERESULT_WAITING) {
|
|
if (random() < bs->thinktime * 0.8) {
|
|
BotRoamGoal(bs, target);
|
|
VectorSubtract(target, bs->origin, dir);
|
|
vectoangles(dir, bs->ideal_viewangles);
|
|
bs->ideal_viewangles[2] *= 0.5;
|
|
}
|
|
}
|
|
else if (!(bs->flags & BFL_IDEALVIEWSET)) {
|
|
if (trap_BotMovementViewTarget(bs->ms, &goal, bs->tfl, 300, target)) {
|
|
VectorSubtract(target, bs->origin, dir);
|
|
vectoangles(dir, bs->ideal_viewangles);
|
|
}
|
|
//FIXME: look at cluster portals?
|
|
else if (VectorLengthSquared(moveresult.movedir)) {
|
|
vectoangles(moveresult.movedir, bs->ideal_viewangles);
|
|
}
|
|
else if (random() < bs->thinktime * 0.8) {
|
|
BotRoamGoal(bs, target);
|
|
VectorSubtract(target, bs->origin, dir);
|
|
vectoangles(dir, bs->ideal_viewangles);
|
|
bs->ideal_viewangles[2] *= 0.5;
|
|
}
|
|
bs->ideal_viewangles[2] *= 0.5;
|
|
}
|
|
//if the weapon is used for the bot movement
|
|
if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) bs->weaponnum = moveresult.weapon;
|
|
//
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AIEnter_Battle_Fight
|
|
==================
|
|
*/
|
|
void AIEnter_Battle_Fight(bot_state_t *bs, char *s) {
|
|
BotRecordNodeSwitch(bs, "battle fight", "", s);
|
|
trap_BotResetLastAvoidReach(bs->ms);
|
|
bs->ainode = AINode_Battle_Fight;
|
|
bs->flags &= ~BFL_FIGHTSUICIDAL;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AIEnter_Battle_SuicidalFight
|
|
==================
|
|
*/
|
|
void AIEnter_Battle_SuicidalFight(bot_state_t *bs, char *s) {
|
|
BotRecordNodeSwitch(bs, "battle fight", "", s);
|
|
trap_BotResetLastAvoidReach(bs->ms);
|
|
bs->ainode = AINode_Battle_Fight;
|
|
bs->flags |= BFL_FIGHTSUICIDAL;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AINode_Battle_Fight
|
|
==================
|
|
*/
|
|
int AINode_Battle_Fight(bot_state_t *bs) {
|
|
int areanum;
|
|
vec3_t target;
|
|
aas_entityinfo_t entinfo;
|
|
bot_moveresult_t moveresult;
|
|
|
|
if (BotIsObserver(bs)) {
|
|
AIEnter_Observer(bs, "battle fight: observer");
|
|
return qfalse;
|
|
}
|
|
|
|
//if in the intermission
|
|
if (BotIntermission(bs)) {
|
|
AIEnter_Intermission(bs, "battle fight: intermission");
|
|
return qfalse;
|
|
}
|
|
//respawn if dead
|
|
if (BotIsDead(bs)) {
|
|
AIEnter_Respawn(bs, "battle fight: bot dead");
|
|
return qfalse;
|
|
}
|
|
//if there is another better enemy
|
|
if (BotFindEnemy(bs, bs->enemy)) {
|
|
#ifdef DEBUG
|
|
BotAI_Print(PRT_MESSAGE, "found new better enemy\n");
|
|
#endif
|
|
}
|
|
//if no enemy
|
|
if (bs->enemy < 0) {
|
|
AIEnter_Seek_LTG(bs, "battle fight: no enemy");
|
|
return qfalse;
|
|
}
|
|
//
|
|
BotEntityInfo(bs->enemy, &entinfo);
|
|
//if the enemy is dead
|
|
if (bs->enemydeath_time) {
|
|
if (bs->enemydeath_time < FloatTime() - 1.0) {
|
|
bs->enemydeath_time = 0;
|
|
if (bs->enemysuicide) {
|
|
BotChat_EnemySuicide(bs);
|
|
}
|
|
if (bs->lastkilledplayer == bs->enemy && BotChat_Kill(bs)) {
|
|
bs->stand_time = FloatTime() + BotChatTime(bs);
|
|
AIEnter_Stand(bs, "battle fight: enemy dead");
|
|
}
|
|
else {
|
|
bs->ltg_time = 0;
|
|
AIEnter_Seek_LTG(bs, "battle fight: enemy dead");
|
|
}
|
|
return qfalse;
|
|
}
|
|
}
|
|
else {
|
|
if (EntityIsDead(&entinfo)) {
|
|
bs->enemydeath_time = FloatTime();
|
|
}
|
|
}
|
|
//if the enemy is invisible and not shooting the bot looses track easily
|
|
if (EntityIsInvisible(&entinfo) && !EntityIsShooting(&entinfo)) {
|
|
if (random() < 0.2) {
|
|
AIEnter_Seek_LTG(bs, "battle fight: invisible");
|
|
return qfalse;
|
|
}
|
|
}
|
|
//
|
|
VectorCopy(entinfo.origin, target);
|
|
// if not a player enemy
|
|
if (bs->enemy >= MAX_CLIENTS) {
|
|
#ifdef MISSIONPACK
|
|
// if attacking an obelisk
|
|
if ( bs->enemy == redobelisk.entitynum ||
|
|
bs->enemy == blueobelisk.entitynum ) {
|
|
target[2] += 16;
|
|
}
|
|
#endif
|
|
}
|
|
//update the reachability area and origin if possible
|
|
areanum = BotPointAreaNum(target);
|
|
if (areanum && trap_AAS_AreaReachability(areanum)) {
|
|
VectorCopy(target, bs->lastenemyorigin);
|
|
bs->lastenemyareanum = areanum;
|
|
}
|
|
//update the attack inventory values
|
|
BotUpdateBattleInventory(bs, bs->enemy);
|
|
//if the bot's health decreased
|
|
if (bs->lastframe_health > bs->inventory[INVENTORY_HEALTH]) {
|
|
if (BotChat_HitNoDeath(bs)) {
|
|
bs->stand_time = FloatTime() + BotChatTime(bs);
|
|
AIEnter_Stand(bs, "battle fight: chat health decreased");
|
|
return qfalse;
|
|
}
|
|
}
|
|
//if the bot hit someone
|
|
if (bs->cur_ps.persistant[PERS_HITS] > bs->lasthitcount) {
|
|
if (BotChat_HitNoKill(bs)) {
|
|
bs->stand_time = FloatTime() + BotChatTime(bs);
|
|
AIEnter_Stand(bs, "battle fight: chat hit someone");
|
|
return qfalse;
|
|
}
|
|
}
|
|
//if the enemy is not visible
|
|
if (!BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy)) {
|
|
#ifdef MISSIONPACK
|
|
if (bs->enemy == redobelisk.entitynum || bs->enemy == blueobelisk.entitynum) {
|
|
AIEnter_Battle_Chase(bs, "battle fight: obelisk out of sight");
|
|
return qfalse;
|
|
}
|
|
#endif
|
|
if (BotWantsToChase(bs)) {
|
|
AIEnter_Battle_Chase(bs, "battle fight: enemy out of sight");
|
|
return qfalse;
|
|
}
|
|
else {
|
|
AIEnter_Seek_LTG(bs, "battle fight: enemy out of sight");
|
|
return qfalse;
|
|
}
|
|
}
|
|
//use holdable items
|
|
BotBattleUseItems(bs);
|
|
//
|
|
bs->tfl = TFL_DEFAULT;
|
|
if (bot_grapple.integer) bs->tfl |= TFL_GRAPPLEHOOK;
|
|
//if in lava or slime the bot should be able to get out
|
|
if (BotInLavaOrSlime(bs)) bs->tfl |= TFL_LAVA|TFL_SLIME;
|
|
//
|
|
if (BotCanAndWantsToRocketJump(bs)) {
|
|
bs->tfl |= TFL_ROCKETJUMP;
|
|
}
|
|
//choose the best weapon to fight with
|
|
BotChooseWeapon(bs);
|
|
//do attack movements
|
|
moveresult = BotAttackMove(bs, bs->tfl);
|
|
//if the movement failed
|
|
if (moveresult.failure) {
|
|
//reset the avoid reach, otherwise bot is stuck in current area
|
|
trap_BotResetAvoidReach(bs->ms);
|
|
//BotAI_Print(PRT_MESSAGE, "movement failure %d\n", moveresult.traveltype);
|
|
bs->ltg_time = 0;
|
|
}
|
|
//
|
|
BotAIBlocked(bs, &moveresult, qfalse);
|
|
//aim at the enemy
|
|
BotAimAtEnemy(bs);
|
|
//attack the enemy if possible
|
|
BotCheckAttack(bs);
|
|
//if the bot wants to retreat
|
|
if (!(bs->flags & BFL_FIGHTSUICIDAL)) {
|
|
if (BotWantsToRetreat(bs)) {
|
|
AIEnter_Battle_Retreat(bs, "battle fight: wants to retreat");
|
|
return qtrue;
|
|
}
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AIEnter_Battle_Chase
|
|
==================
|
|
*/
|
|
void AIEnter_Battle_Chase(bot_state_t *bs, char *s) {
|
|
BotRecordNodeSwitch(bs, "battle chase", "", s);
|
|
bs->chase_time = FloatTime();
|
|
bs->ainode = AINode_Battle_Chase;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AINode_Battle_Chase
|
|
==================
|
|
*/
|
|
int AINode_Battle_Chase(bot_state_t *bs)
|
|
{
|
|
bot_goal_t goal;
|
|
vec3_t target, dir;
|
|
bot_moveresult_t moveresult;
|
|
float range;
|
|
|
|
if (BotIsObserver(bs)) {
|
|
AIEnter_Observer(bs, "battle chase: observer");
|
|
return qfalse;
|
|
}
|
|
//if in the intermission
|
|
if (BotIntermission(bs)) {
|
|
AIEnter_Intermission(bs, "battle chase: intermission");
|
|
return qfalse;
|
|
}
|
|
//respawn if dead
|
|
if (BotIsDead(bs)) {
|
|
AIEnter_Respawn(bs, "battle chase: bot dead");
|
|
return qfalse;
|
|
}
|
|
//if no enemy
|
|
if (bs->enemy < 0) {
|
|
AIEnter_Seek_LTG(bs, "battle chase: no enemy");
|
|
return qfalse;
|
|
}
|
|
//if the enemy is visible
|
|
if (BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy)) {
|
|
AIEnter_Battle_Fight(bs, "battle chase");
|
|
return qfalse;
|
|
}
|
|
//if there is another enemy
|
|
if (BotFindEnemy(bs, -1)) {
|
|
AIEnter_Battle_Fight(bs, "battle chase: better enemy");
|
|
return qfalse;
|
|
}
|
|
//there is no last enemy area
|
|
if (!bs->lastenemyareanum) {
|
|
AIEnter_Seek_LTG(bs, "battle chase: no enemy area");
|
|
return qfalse;
|
|
}
|
|
//
|
|
bs->tfl = TFL_DEFAULT;
|
|
if (bot_grapple.integer) bs->tfl |= TFL_GRAPPLEHOOK;
|
|
//if in lava or slime the bot should be able to get out
|
|
if (BotInLavaOrSlime(bs)) bs->tfl |= TFL_LAVA|TFL_SLIME;
|
|
//
|
|
if (BotCanAndWantsToRocketJump(bs)) {
|
|
bs->tfl |= TFL_ROCKETJUMP;
|
|
}
|
|
//map specific code
|
|
BotMapScripts(bs);
|
|
//create the chase goal
|
|
goal.entitynum = bs->enemy;
|
|
goal.areanum = bs->lastenemyareanum;
|
|
VectorCopy(bs->lastenemyorigin, goal.origin);
|
|
VectorSet(goal.mins, -8, -8, -8);
|
|
VectorSet(goal.maxs, 8, 8, 8);
|
|
//if the last seen enemy spot is reached the enemy could not be found
|
|
if (trap_BotTouchingGoal(bs->origin, &goal)) bs->chase_time = 0;
|
|
//if there's no chase time left
|
|
if (!bs->chase_time || bs->chase_time < FloatTime() - 10) {
|
|
AIEnter_Seek_LTG(bs, "battle chase: time out");
|
|
return qfalse;
|
|
}
|
|
//check for nearby goals periodicly
|
|
if (bs->check_time < FloatTime()) {
|
|
bs->check_time = FloatTime() + 1;
|
|
range = 150;
|
|
//
|
|
if (BotNearbyGoal(bs, bs->tfl, &goal, range)) {
|
|
//the bot gets 5 seconds to pick up the nearby goal item
|
|
bs->nbg_time = FloatTime() + 0.1 * range + 1;
|
|
trap_BotResetLastAvoidReach(bs->ms);
|
|
AIEnter_Battle_NBG(bs, "battle chase: nbg");
|
|
return qfalse;
|
|
}
|
|
}
|
|
//
|
|
BotUpdateBattleInventory(bs, bs->enemy);
|
|
//initialize the movement state
|
|
BotSetupForMovement(bs);
|
|
//move towards the goal
|
|
trap_BotMoveToGoal(&moveresult, bs->ms, &goal, bs->tfl);
|
|
//if the movement failed
|
|
if (moveresult.failure) {
|
|
//reset the avoid reach, otherwise bot is stuck in current area
|
|
trap_BotResetAvoidReach(bs->ms);
|
|
//BotAI_Print(PRT_MESSAGE, "movement failure %d\n", moveresult.traveltype);
|
|
bs->ltg_time = 0;
|
|
}
|
|
//
|
|
BotAIBlocked(bs, &moveresult, qfalse);
|
|
//
|
|
if (moveresult.flags & (MOVERESULT_MOVEMENTVIEWSET|MOVERESULT_MOVEMENTVIEW|MOVERESULT_SWIMVIEW)) {
|
|
VectorCopy(moveresult.ideal_viewangles, bs->ideal_viewangles);
|
|
}
|
|
else if (!(bs->flags & BFL_IDEALVIEWSET)) {
|
|
if (bs->chase_time > FloatTime() - 2) {
|
|
BotAimAtEnemy(bs);
|
|
}
|
|
else {
|
|
if (trap_BotMovementViewTarget(bs->ms, &goal, bs->tfl, 300, target)) {
|
|
VectorSubtract(target, bs->origin, dir);
|
|
vectoangles(dir, bs->ideal_viewangles);
|
|
}
|
|
else {
|
|
vectoangles(moveresult.movedir, bs->ideal_viewangles);
|
|
}
|
|
}
|
|
bs->ideal_viewangles[2] *= 0.5;
|
|
}
|
|
//if the weapon is used for the bot movement
|
|
if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) bs->weaponnum = moveresult.weapon;
|
|
//if the bot is in the area the enemy was last seen in
|
|
if (bs->areanum == bs->lastenemyareanum) bs->chase_time = 0;
|
|
//if the bot wants to retreat (the bot could have been damage during the chase)
|
|
if (BotWantsToRetreat(bs)) {
|
|
AIEnter_Battle_Retreat(bs, "battle chase: wants to retreat");
|
|
return qtrue;
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AIEnter_Battle_Retreat
|
|
==================
|
|
*/
|
|
void AIEnter_Battle_Retreat(bot_state_t *bs, char *s) {
|
|
BotRecordNodeSwitch(bs, "battle retreat", "", s);
|
|
bs->ainode = AINode_Battle_Retreat;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AINode_Battle_Retreat
|
|
==================
|
|
*/
|
|
int AINode_Battle_Retreat(bot_state_t *bs) {
|
|
bot_goal_t goal;
|
|
aas_entityinfo_t entinfo;
|
|
bot_moveresult_t moveresult;
|
|
vec3_t target, dir;
|
|
float attack_skill, range;
|
|
int areanum;
|
|
|
|
if (BotIsObserver(bs)) {
|
|
AIEnter_Observer(bs, "battle retreat: observer");
|
|
return qfalse;
|
|
}
|
|
//if in the intermission
|
|
if (BotIntermission(bs)) {
|
|
AIEnter_Intermission(bs, "battle retreat: intermission");
|
|
return qfalse;
|
|
}
|
|
//respawn if dead
|
|
if (BotIsDead(bs)) {
|
|
AIEnter_Respawn(bs, "battle retreat: bot dead");
|
|
return qfalse;
|
|
}
|
|
//if no enemy
|
|
if (bs->enemy < 0) {
|
|
AIEnter_Seek_LTG(bs, "battle retreat: no enemy");
|
|
return qfalse;
|
|
}
|
|
//
|
|
BotEntityInfo(bs->enemy, &entinfo);
|
|
if (EntityIsDead(&entinfo)) {
|
|
AIEnter_Seek_LTG(bs, "battle retreat: enemy dead");
|
|
return qfalse;
|
|
}
|
|
//if there is another better enemy
|
|
if (BotFindEnemy(bs, bs->enemy)) {
|
|
#ifdef DEBUG
|
|
BotAI_Print(PRT_MESSAGE, "found new better enemy\n");
|
|
#endif
|
|
}
|
|
//
|
|
bs->tfl = TFL_DEFAULT;
|
|
if (bot_grapple.integer) bs->tfl |= TFL_GRAPPLEHOOK;
|
|
//if in lava or slime the bot should be able to get out
|
|
if (BotInLavaOrSlime(bs)) bs->tfl |= TFL_LAVA|TFL_SLIME;
|
|
//map specific code
|
|
BotMapScripts(bs);
|
|
//update the attack inventory values
|
|
BotUpdateBattleInventory(bs, bs->enemy);
|
|
//if the bot doesn't want to retreat anymore... probably picked up some nice items
|
|
if (BotWantsToChase(bs)) {
|
|
//empty the goal stack, when chasing, only the enemy is the goal
|
|
trap_BotEmptyGoalStack(bs->gs);
|
|
//go chase the enemy
|
|
AIEnter_Battle_Chase(bs, "battle retreat: wants to chase");
|
|
return qfalse;
|
|
}
|
|
//update the last time the enemy was visible
|
|
if (BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy)) {
|
|
bs->enemyvisible_time = FloatTime();
|
|
VectorCopy(entinfo.origin, target);
|
|
// if not a player enemy
|
|
if (bs->enemy >= MAX_CLIENTS) {
|
|
#ifdef MISSIONPACK
|
|
// if attacking an obelisk
|
|
if ( bs->enemy == redobelisk.entitynum ||
|
|
bs->enemy == blueobelisk.entitynum ) {
|
|
target[2] += 16;
|
|
}
|
|
#endif
|
|
}
|
|
//update the reachability area and origin if possible
|
|
areanum = BotPointAreaNum(target);
|
|
if (areanum && trap_AAS_AreaReachability(areanum)) {
|
|
VectorCopy(target, bs->lastenemyorigin);
|
|
bs->lastenemyareanum = areanum;
|
|
}
|
|
}
|
|
//if the enemy is NOT visible for 4 seconds
|
|
if (bs->enemyvisible_time < FloatTime() - 4) {
|
|
AIEnter_Seek_LTG(bs, "battle retreat: lost enemy");
|
|
return qfalse;
|
|
}
|
|
//else if the enemy is NOT visible
|
|
else if (bs->enemyvisible_time < FloatTime()) {
|
|
//if there is another enemy
|
|
if (BotFindEnemy(bs, -1)) {
|
|
AIEnter_Battle_Fight(bs, "battle retreat: another enemy");
|
|
return qfalse;
|
|
}
|
|
}
|
|
//
|
|
BotTeamGoals(bs, qtrue);
|
|
//use holdable items
|
|
BotBattleUseItems(bs);
|
|
//get the current long term goal while retreating
|
|
if (!BotLongTermGoal(bs, bs->tfl, qtrue, &goal)) {
|
|
AIEnter_Battle_SuicidalFight(bs, "battle retreat: no way out");
|
|
return qfalse;
|
|
}
|
|
//check for nearby goals periodicly
|
|
if (bs->check_time < FloatTime()) {
|
|
bs->check_time = FloatTime() + 1;
|
|
range = 150;
|
|
#ifdef CTF
|
|
if (gametype == GT_CTF) {
|
|
//if carrying a flag the bot shouldn't be distracted too much
|
|
if (BotCTFCarryingFlag(bs))
|
|
range = 50;
|
|
}
|
|
#endif //CTF
|
|
#ifdef MISSIONPACK
|
|
else if (gametype == GT_1FCTF) {
|
|
if (Bot1FCTFCarryingFlag(bs))
|
|
range = 50;
|
|
}
|
|
else if (gametype == GT_HARVESTER) {
|
|
if (BotHarvesterCarryingCubes(bs))
|
|
range = 80;
|
|
}
|
|
#endif
|
|
//
|
|
if (BotNearbyGoal(bs, bs->tfl, &goal, range)) {
|
|
trap_BotResetLastAvoidReach(bs->ms);
|
|
//time the bot gets to pick up the nearby goal item
|
|
bs->nbg_time = FloatTime() + range / 100 + 1;
|
|
AIEnter_Battle_NBG(bs, "battle retreat: nbg");
|
|
return qfalse;
|
|
}
|
|
}
|
|
//initialize the movement state
|
|
BotSetupForMovement(bs);
|
|
//move towards the goal
|
|
trap_BotMoveToGoal(&moveresult, bs->ms, &goal, bs->tfl);
|
|
//if the movement failed
|
|
if (moveresult.failure) {
|
|
//reset the avoid reach, otherwise bot is stuck in current area
|
|
trap_BotResetAvoidReach(bs->ms);
|
|
//BotAI_Print(PRT_MESSAGE, "movement failure %d\n", moveresult.traveltype);
|
|
bs->ltg_time = 0;
|
|
}
|
|
//
|
|
BotAIBlocked(bs, &moveresult, qfalse);
|
|
//choose the best weapon to fight with
|
|
BotChooseWeapon(bs);
|
|
//if the view is fixed for the movement
|
|
if (moveresult.flags & (MOVERESULT_MOVEMENTVIEW|MOVERESULT_SWIMVIEW)) {
|
|
VectorCopy(moveresult.ideal_viewangles, bs->ideal_viewangles);
|
|
}
|
|
else if (!(moveresult.flags & MOVERESULT_MOVEMENTVIEWSET)
|
|
&& !(bs->flags & BFL_IDEALVIEWSET) ) {
|
|
attack_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_ATTACK_SKILL, 0, 1);
|
|
//if the bot is skilled enough
|
|
if (attack_skill > 0.3) {
|
|
BotAimAtEnemy(bs);
|
|
}
|
|
else {
|
|
if (trap_BotMovementViewTarget(bs->ms, &goal, bs->tfl, 300, target)) {
|
|
VectorSubtract(target, bs->origin, dir);
|
|
vectoangles(dir, bs->ideal_viewangles);
|
|
}
|
|
else {
|
|
vectoangles(moveresult.movedir, bs->ideal_viewangles);
|
|
}
|
|
bs->ideal_viewangles[2] *= 0.5;
|
|
}
|
|
}
|
|
//if the weapon is used for the bot movement
|
|
if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) bs->weaponnum = moveresult.weapon;
|
|
//attack the enemy if possible
|
|
BotCheckAttack(bs);
|
|
//
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AIEnter_Battle_NBG
|
|
==================
|
|
*/
|
|
void AIEnter_Battle_NBG(bot_state_t *bs, char *s) {
|
|
BotRecordNodeSwitch(bs, "battle NBG", "", s);
|
|
bs->ainode = AINode_Battle_NBG;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
AINode_Battle_NBG
|
|
==================
|
|
*/
|
|
int AINode_Battle_NBG(bot_state_t *bs) {
|
|
int areanum;
|
|
bot_goal_t goal;
|
|
aas_entityinfo_t entinfo;
|
|
bot_moveresult_t moveresult;
|
|
float attack_skill;
|
|
vec3_t target, dir;
|
|
|
|
if (BotIsObserver(bs)) {
|
|
AIEnter_Observer(bs, "battle nbg: observer");
|
|
return qfalse;
|
|
}
|
|
//if in the intermission
|
|
if (BotIntermission(bs)) {
|
|
AIEnter_Intermission(bs, "battle nbg: intermission");
|
|
return qfalse;
|
|
}
|
|
//respawn if dead
|
|
if (BotIsDead(bs)) {
|
|
AIEnter_Respawn(bs, "battle nbg: bot dead");
|
|
return qfalse;
|
|
}
|
|
//if no enemy
|
|
if (bs->enemy < 0) {
|
|
AIEnter_Seek_NBG(bs, "battle nbg: no enemy");
|
|
return qfalse;
|
|
}
|
|
//
|
|
BotEntityInfo(bs->enemy, &entinfo);
|
|
if (EntityIsDead(&entinfo)) {
|
|
AIEnter_Seek_NBG(bs, "battle nbg: enemy dead");
|
|
return qfalse;
|
|
}
|
|
//
|
|
bs->tfl = TFL_DEFAULT;
|
|
if (bot_grapple.integer) bs->tfl |= TFL_GRAPPLEHOOK;
|
|
//if in lava or slime the bot should be able to get out
|
|
if (BotInLavaOrSlime(bs)) bs->tfl |= TFL_LAVA|TFL_SLIME;
|
|
//
|
|
if (BotCanAndWantsToRocketJump(bs)) {
|
|
bs->tfl |= TFL_ROCKETJUMP;
|
|
}
|
|
//map specific code
|
|
BotMapScripts(bs);
|
|
//update the last time the enemy was visible
|
|
if (BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy)) {
|
|
bs->enemyvisible_time = FloatTime();
|
|
VectorCopy(entinfo.origin, target);
|
|
// if not a player enemy
|
|
if (bs->enemy >= MAX_CLIENTS) {
|
|
#ifdef MISSIONPACK
|
|
// if attacking an obelisk
|
|
if ( bs->enemy == redobelisk.entitynum ||
|
|
bs->enemy == blueobelisk.entitynum ) {
|
|
target[2] += 16;
|
|
}
|
|
#endif
|
|
}
|
|
//update the reachability area and origin if possible
|
|
areanum = BotPointAreaNum(target);
|
|
if (areanum && trap_AAS_AreaReachability(areanum)) {
|
|
VectorCopy(target, bs->lastenemyorigin);
|
|
bs->lastenemyareanum = areanum;
|
|
}
|
|
}
|
|
//if the bot has no goal or touches the current goal
|
|
if (!trap_BotGetTopGoal(bs->gs, &goal)) {
|
|
bs->nbg_time = 0;
|
|
}
|
|
else if (BotReachedGoal(bs, &goal)) {
|
|
bs->nbg_time = 0;
|
|
}
|
|
//
|
|
if (bs->nbg_time < FloatTime()) {
|
|
//pop the current goal from the stack
|
|
trap_BotPopGoal(bs->gs);
|
|
//if the bot still has a goal
|
|
if (trap_BotGetTopGoal(bs->gs, &goal))
|
|
AIEnter_Battle_Retreat(bs, "battle nbg: time out");
|
|
else
|
|
AIEnter_Battle_Fight(bs, "battle nbg: time out");
|
|
//
|
|
return qfalse;
|
|
}
|
|
//initialize the movement state
|
|
BotSetupForMovement(bs);
|
|
//move towards the goal
|
|
trap_BotMoveToGoal(&moveresult, bs->ms, &goal, bs->tfl);
|
|
//if the movement failed
|
|
if (moveresult.failure) {
|
|
//reset the avoid reach, otherwise bot is stuck in current area
|
|
trap_BotResetAvoidReach(bs->ms);
|
|
//BotAI_Print(PRT_MESSAGE, "movement failure %d\n", moveresult.traveltype);
|
|
bs->nbg_time = 0;
|
|
}
|
|
//
|
|
BotAIBlocked(bs, &moveresult, qfalse);
|
|
//update the attack inventory values
|
|
BotUpdateBattleInventory(bs, bs->enemy);
|
|
//choose the best weapon to fight with
|
|
BotChooseWeapon(bs);
|
|
//if the view is fixed for the movement
|
|
if (moveresult.flags & (MOVERESULT_MOVEMENTVIEW|MOVERESULT_SWIMVIEW)) {
|
|
VectorCopy(moveresult.ideal_viewangles, bs->ideal_viewangles);
|
|
}
|
|
else if (!(moveresult.flags & MOVERESULT_MOVEMENTVIEWSET)
|
|
&& !(bs->flags & BFL_IDEALVIEWSET)) {
|
|
attack_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_ATTACK_SKILL, 0, 1);
|
|
//if the bot is skilled enough and the enemy is visible
|
|
if (attack_skill > 0.3) {
|
|
//&& BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy)
|
|
BotAimAtEnemy(bs);
|
|
}
|
|
else {
|
|
if (trap_BotMovementViewTarget(bs->ms, &goal, bs->tfl, 300, target)) {
|
|
VectorSubtract(target, bs->origin, dir);
|
|
vectoangles(dir, bs->ideal_viewangles);
|
|
}
|
|
else {
|
|
vectoangles(moveresult.movedir, bs->ideal_viewangles);
|
|
}
|
|
bs->ideal_viewangles[2] *= 0.5;
|
|
}
|
|
}
|
|
//if the weapon is used for the bot movement
|
|
if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) bs->weaponnum = moveresult.weapon;
|
|
//attack the enemy if possible
|
|
BotCheckAttack(bs);
|
|
//
|
|
return qtrue;
|
|
}
|
|
|