aboutsummaryrefslogtreecommitdiff
path: root/code/game/ai_dmnet.c
diff options
context:
space:
mode:
Diffstat (limited to 'code/game/ai_dmnet.c')
-rw-r--r--code/game/ai_dmnet.c2610
1 files changed, 2610 insertions, 0 deletions
diff --git a/code/game/ai_dmnet.c b/code/game/ai_dmnet.c
new file mode 100644
index 0000000..5050bd1
--- /dev/null
+++ b/code/game/ai_dmnet.c
@@ -0,0 +1,2610 @@
+/*
+===========================================================================
+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 the enemy flag
+ if (BotCTFCarryingFlag(bs)) {
+ //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->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->lastkilledplayer = -1;
+ 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;
+ }
+ //
+ if (bs->camp_range > 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;
+#endif
+ 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 it's 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 it's 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 it's 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;
+}
+
+/*
+==================
+AIEnter_Battle_Fight
+==================
+*/
+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)) {
+ 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 anough
+ 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 anough 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;
+}
+