#include "navigation.qh"
+#include <server/defs.qh>
+#include <server/miscfunctions.qh>
#include "cvars.qh"
#include "bot.qh"
.float speed;
+void navigation_goalrating_timeout_set(entity this)
+{
+ if(IS_MOVABLE(this.goalentity))
+ this.bot_strategytime = time + autocvar_bot_ai_strategyinterval_movingtarget;
+ else
+ this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
+}
+
+// use this when current goal must be discarded immediately
+void navigation_goalrating_timeout_force(entity this)
+{
+ navigation_goalrating_timeout_expire(this, 0);
+}
+
+// use this when current goal can be kept for a short while to increase the chance
+// of bot touching a waypoint, which helps to find a new goal more efficiently
+void navigation_goalrating_timeout_expire(entity this, float seconds)
+{
+ if (seconds <= 0)
+ this.bot_strategytime = 0;
+ else if (this.bot_strategytime > time + seconds)
+ this.bot_strategytime = time + seconds;
+}
+
+bool navigation_goalrating_timeout(entity this)
+{
+ return this.bot_strategytime < time;
+}
+
+bool navigation_goalrating_timeout_can_be_anticipated(entity this)
+{
+ if(time > this.bot_strategytime - (IS_MOVABLE(this.goalentity) ? 3 : 2))
+ return true;
+
+ if (this.goalentity.bot_pickup && time > this.bot_strategytime - 5)
+ {
+ vector gco = (this.goalentity.absmin + this.goalentity.absmax) * 0.5;
+ if(!havocbot_goalrating_item_pickable_check_players(this, this.origin, this.goalentity, gco))
+ {
+ this.ignoregoal = this.goalentity;
+ this.ignoregoaltime = time + autocvar_bot_ai_ignoregoal_timeout;
+ return true;
+ }
+ }
+ return false;
+}
+
void navigation_dynamicgoal_init(entity this, bool initially_static)
{
this.navigation_dynamicgoal = true;
void navigation_dynamicgoal_set(entity this)
{
this.nearestwaypointtimeout = time;
+ if (this.nearestwaypoint)
+ this.nearestwaypointtimeout += 2;
}
void navigation_dynamicgoal_unset(entity this)
this.nearestwaypointtimeout = -1;
}
+// returns point of ent closer to org
+vector get_closer_dest(entity ent, vector org)
+{
+ vector dest = '0 0 0';
+ if ((ent.classname != "waypoint") || ent.wpisbox)
+ {
+ vector wm1 = ent.origin + ent.mins - eZ * (PL_MAX_CONST.z - 1);
+ vector wm2 = ent.origin + ent.maxs - eZ * (PL_MIN_CONST.z + 1);
+ dest.x = bound(wm1.x, org.x, wm2.x);
+ dest.y = bound(wm1.y, org.y, wm2.y);
+ dest.z = bound(wm1.z, org.z, wm2.z);
+ }
+ else
+ dest = ent.origin;
+ return dest;
+}
+
+void set_tracewalk_dest(entity ent, vector org, bool fix_player_dest)
+{
+ if ((ent.classname != "waypoint") || ent.wpisbox)
+ {
+ vector wm1 = ent.origin + ent.mins - eZ * (PL_MAX_CONST.z - 1);
+ vector wm2 = ent.origin + ent.maxs - eZ * (PL_MIN_CONST.z + 1);
+ if (IS_PLAYER(ent) || IS_MONSTER(ent))
+ {
+ // move destination point out of player bbox otherwise tracebox always fails
+ // (if bot_navigation_ignoreplayers is false)
+ wm1 += vec2(PL_MIN_CONST) + '-1 -1 0';
+ wm2 += vec2(PL_MAX_CONST) + '1 1 0';
+ }
+ // set destination point to x and y coords of ent that are closer to org
+ // z coord is set to ent's min height
+ tracewalk_dest.x = bound(wm1.x, org.x, wm2.x);
+ tracewalk_dest.y = bound(wm1.y, org.y, wm2.y);
+ tracewalk_dest.z = wm1.z;
+ tracewalk_dest_height = wm2.z - wm1.z; // destination height
+ }
+ else
+ {
+ tracewalk_dest = ent.origin;
+ tracewalk_dest_height = 0;
+ }
+ if (fix_player_dest && IS_PLAYER(ent) && !IS_ONGROUND(ent))
+ {
+ // snap player to the ground
+ if (org.x == tracewalk_dest.x && org.y == tracewalk_dest.y)
+ {
+ // bot is right under the player
+ tracebox(ent.origin, ent.mins, ent.maxs, ent.origin - '0 0 700', MOVE_NORMAL, ent);
+ tracewalk_dest = trace_endpos;
+ tracewalk_dest_height = 0;
+ }
+ else
+ {
+ tracebox(tracewalk_dest, ent.mins, ent.maxs, tracewalk_dest - '0 0 700', MOVE_NORMAL, ent);
+ if (!trace_startsolid && tracewalk_dest.z - trace_endpos.z > 0)
+ {
+ tracewalk_dest_height = tracewalk_dest.z - trace_endpos.z;
+ tracewalk_dest.z = trace_endpos.z;
+ }
+ }
+ }
+}
+
+// returns point of ent closer to org
+vector set_tracewalk_dest_2(entity ent, vector org)
+{
+ vector closer_dest = '0 0 0';
+ if ((ent.classname != "waypoint") || ent.wpisbox)
+ {
+ vector wm1 = ent.origin + ent.mins - eZ * (PL_MAX_CONST.z - 1);
+ vector wm2 = ent.origin + ent.maxs - eZ * (PL_MIN_CONST.z + 1);
+ closer_dest.x = bound(wm1.x, org.x, wm2.x);
+ closer_dest.y = bound(wm1.y, org.y, wm2.y);
+ closer_dest.z = bound(wm1.z, org.z, wm2.z);
+ // set destination point to x and y coords of ent that are closer to org
+ // z coord is set to ent's min height
+ tracewalk_dest.x = closer_dest.x;
+ tracewalk_dest.y = closer_dest.y;
+ tracewalk_dest.z = wm1.z;
+ tracewalk_dest_height = wm2.z - wm1.z; // destination height
+ }
+ else
+ {
+ closer_dest = ent.origin;
+ tracewalk_dest = closer_dest;
+ tracewalk_dest_height = 0;
+ }
+ return closer_dest;
+}
+
+bool navigation_check_submerged_state(entity ent, vector pos)
+{
+ bool submerged;
+ if(IS_PLAYER(ent))
+ submerged = (ent.waterlevel == WATERLEVEL_SUBMERGED);
+ else if(ent.nav_submerged_state != SUBMERGED_UNDEFINED)
+ submerged = (ent.nav_submerged_state == SUBMERGED_YES);
+ else
+ {
+ submerged = SUBMERGED(pos);
+ // NOTE: SUBMERGED check of box waypoint origin may fail even if origin
+ // is actually submerged because often they are inside some solid.
+ // That's why submerged state is saved now that we know current pos is
+ // not stuck in solid (previous tracewalk call to this pos was successfully)
+ if(!ent.navigation_dynamicgoal)
+ ent.nav_submerged_state = (submerged) ? SUBMERGED_YES : SUBMERGED_NO;
+ }
+ return submerged;
+}
+
+bool navigation_checkladders(entity e, vector org, vector m1, vector m2, vector end, vector end2, int movemode)
+{
+ IL_EACH(g_ladders, it.classname == "func_ladder",
+ {
+ if(it.bot_pickup)
+ if(boxesoverlap(org + m1 + '-1 -1 -1', org + m2 + '1 1 1', it.absmin, it.absmax))
+ if(boxesoverlap(end, end2, it.absmin + vec2(m1) + '-1 -1 0', it.absmax + vec2(m2) + '1 1 0'))
+ {
+ vector top = org;
+ top.z = it.absmax.z + (PL_MAX_CONST.z - PL_MIN_CONST.z);
+ tracebox(org, m1, m2, top, movemode, e);
+ if(trace_fraction == 1)
+ return true;
+ }
+ });
+ return false;
+}
+
+vector resurface_limited(vector org, float lim, vector m1)
+{
+ if (WETFEET(org + eZ * (lim - org.z)))
+ org.z = lim;
+ else
+ {
+ float RES_min_h = org.z;
+ float RES_max_h = lim;
+ do {
+ org.z = 0.5 * (RES_min_h + RES_max_h);
+ if(WETFEET(org))
+ RES_min_h = org.z;
+ else
+ RES_max_h = org.z;
+ } while (RES_max_h - RES_min_h >= 1);
+ org.z = RES_min_h;
+ }
+ return org;
+}
+#define RESURFACE_LIMITED(org, lim) org = resurface_limited(org, lim, m1)
+
+#define NAV_WALK 0
+#define NAV_SWIM_ONWATER 1
+#define NAV_SWIM_UNDERWATER 2
+
// rough simulation of walking from one point to another to test if a path
// can be traveled, used for waypoint linking and havocbot
// if end_height is > 0 destination is any point in the vertical segment [end, end + end_height * eZ]
bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float end_height, float movemode)
{
- vector org;
- vector move;
- vector dir;
- float dist;
- float totaldist;
- float stepdist;
- float ignorehazards;
- float swimming;
-
if(autocvar_bot_debug_tracewalk)
{
debugresetnodes();
debugnode(e, start);
}
- move = end - start;
- move.z = 0;
- org = start;
- dist = totaldist = vlen(move);
- dir = normalize(move);
- stepdist = 32;
- ignorehazards = false;
- swimming = false;
+ vector org = start;
+ vector flatdir = end - start;
+ flatdir.z = 0;
+ float flatdist = vlen(flatdir);
+ flatdir = normalize(flatdir);
+ float stepdist = 32;
+ bool ignorehazards = false;
+ int nav_action;
// Analyze starting point
traceline(start, start, MOVE_NORMAL, e);
if (trace_dpstartcontents & (DPCONTENTS_SLIME | DPCONTENTS_LAVA))
ignorehazards = true;
- else
- {
- traceline( start, start + '0 0 -65536', MOVE_NORMAL, e);
- if (trace_dpstartcontents & (DPCONTENTS_SLIME | DPCONTENTS_LAVA))
- {
- ignorehazards = true;
- swimming = true;
- }
- }
+
tracebox(start, m1, m2, start, MOVE_NOMONSTERS, e);
if (trace_startsolid)
{
vector end2 = end;
if(end_height)
end2.z += end_height;
+
+ vector fixed_end = end;
+ vector move;
+
+ if (flatdist > 0 && WETFEET(org))
+ {
+ if (SUBMERGED(org))
+ nav_action = NAV_SWIM_UNDERWATER;
+ else
+ {
+ // tracebox down by player's height
+ // useful to know if water level is so low that bot can still walk
+ tracebox(org, m1, m2, org - eZ * (m2.z - m1.z), movemode, e);
+ if (SUBMERGED(trace_endpos))
+ {
+ org = trace_endpos;
+ nav_action = NAV_SWIM_UNDERWATER;
+ }
+ else
+ nav_action = NAV_WALK;
+ }
+ }
+ else
+ nav_action = NAV_WALK;
+
// Movement loop
- for (;;)
+ while (true)
{
- if (boxesoverlap(end, end2, org + m1 + '-1 -1 -1', org + m2 + '1 1 1'))
+ if (flatdist <= 0)
{
- // Succeeded
- if(autocvar_bot_debug_tracewalk)
- debugnodestatus(org, DEBUG_NODE_SUCCESS);
+ bool success = true;
+ if (org.z > end2.z + 1)
+ {
+ tracebox(org, m1, m2, end2, movemode, e);
+ org = trace_endpos;
+ if (org.z > end2.z + 1)
+ success = false;
+ }
+ else if (org.z < end.z - 1)
+ {
+ tracebox(org, m1, m2, org - jumpheight_vec, movemode, e);
+ if (SUBMERGED(trace_endpos))
+ {
+ vector v = trace_endpos;
+ tracebox(v, m1, m2, end, movemode, e);
+ if(trace_endpos.z >= end.z - 1)
+ {
+ RESURFACE_LIMITED(v, trace_endpos.z);
+ trace_endpos = v;
+ }
+ }
+ else if (trace_endpos.z > org.z - jumpheight_vec.z)
+ tracebox(trace_endpos, m1, m2, trace_endpos + jumpheight_vec, movemode, e);
+ org = trace_endpos;
+ if (org.z < end.z - 1)
+ success = false;
+ }
- //print("tracewalk: ", vtos(start), " can reach ", vtos(end), "\n");
- return true;
+ if (success)
+ {
+ // Succeeded
+ if(autocvar_bot_debug_tracewalk)
+ {
+ debugnode(e, org);
+ debugnodestatus(org, DEBUG_NODE_SUCCESS);
+ }
+
+ //print("tracewalk: ", vtos(start), " can reach ", vtos(end), "\n");
+ return true;
+ }
}
+
if(autocvar_bot_debug_tracewalk)
debugnode(e, org);
- if (dist <= 0)
+ if (flatdist <= 0)
break;
- if (stepdist > dist)
- stepdist = dist;
- dist = dist - stepdist;
- traceline(org, org, MOVE_NORMAL, e);
- if (!ignorehazards)
- {
- if (trace_dpstartcontents & (DPCONTENTS_SLIME | DPCONTENTS_LAVA))
- {
- // hazards blocking path
- if(autocvar_bot_debug_tracewalk)
- debugnodestatus(org, DEBUG_NODE_FAIL);
- //print("tracewalk: ", vtos(start), " hits a hazard when trying to reach ", vtos(end), "\n");
- return false;
+ if (stepdist > flatdist)
+ stepdist = flatdist;
+ if(nav_action == NAV_SWIM_UNDERWATER || (nav_action == NAV_SWIM_ONWATER && org.z > end2.z))
+ {
+ // can't use movement direction here to calculate move because of
+ // precision errors especially when direction has a high enough z value
+ //water_dir = normalize(water_end - org);
+ //move = org + water_dir * stepdist;
+ fixed_end.z = bound(end.z, org.z, end2.z);
+ if (stepdist == flatdist) {
+ move = fixed_end;
+ flatdist = 0;
+ } else {
+ move = org + (fixed_end - org) * (stepdist / flatdist);
+ flatdist = vlen(vec2(fixed_end - move));
}
}
- if (trace_dpstartcontents & DPCONTENTS_LIQUIDSMASK)
+ else // horiz. direction
{
- move = normalize(end - org);
- tracebox(org, m1, m2, org + move * stepdist, movemode, e);
+ flatdist -= stepdist;
+ move = org + flatdir * stepdist;
+ }
- if(autocvar_bot_debug_tracewalk)
- debugnode(e, trace_endpos);
+ if(nav_action == NAV_SWIM_ONWATER)
+ {
+ tracebox(org, m1, m2, move, movemode, e); // swim
+ // hit something
if (trace_fraction < 1)
{
- swimming = true;
- org = trace_endpos + normalize(org - trace_endpos) * stepdist;
- for (; org.z < end.z + e.maxs.z; org.z += stepdist)
+ // stepswim
+ tracebox(org + stepheightvec, m1, m2, move + stepheightvec, movemode, e);
+
+ if (trace_fraction < 1 || trace_startsolid) // can't jump obstacle out of water
{
- if(autocvar_bot_debug_tracewalk)
- debugnode(e, org);
+ org = trace_endpos;
+ if(navigation_checkladders(e, org, m1, m2, end, end2, movemode))
+ {
+ if(autocvar_bot_debug_tracewalk)
+ {
+ debugnode(e, org);
+ debugnodestatus(org, DEBUG_NODE_SUCCESS);
+ }
- if(pointcontents(org) == CONTENT_EMPTY)
- break;
- }
+ //print("tracewalk: ", vtos(start), " can reach ", vtos(end), "\n");
+ return true;
+ }
- if(pointcontents(org + '0 0 1') != CONTENT_EMPTY)
- {
if(autocvar_bot_debug_tracewalk)
debugnodestatus(org, DEBUG_NODE_FAIL);
return false;
- //print("tracewalk: ", vtos(start), " failed under water\n");
+ //print("tracewalk: ", vtos(start), " hit something when trying to reach ", vtos(end), "\n");
}
- continue;
+ //succesful stepswim
+
+ if (flatdist <= 0)
+ {
+ org = trace_endpos;
+ continue;
+ }
+
+ if (org.z <= move.z) // going horiz.
+ {
+ tracebox(trace_endpos, m1, m2, move, movemode, e);
+ org = trace_endpos;
+ nav_action = NAV_WALK;
+ continue;
+ }
}
- else
+
+ if (org.z <= move.z) // going horiz.
+ {
+ org = trace_endpos;
+ nav_action = NAV_SWIM_ONWATER;
+ }
+ else // going down
+ {
org = trace_endpos;
+ if (SUBMERGED(org))
+ nav_action = NAV_SWIM_UNDERWATER;
+ else
+ nav_action = NAV_SWIM_ONWATER;
+ }
}
- else
+ else if(nav_action == NAV_SWIM_UNDERWATER)
+ {
+ if (move.z >= org.z) // swimming upwards or horiz.
+ {
+ tracebox(org, m1, m2, move, movemode, e); // swim
+
+ bool stepswum = false;
+
+ // hit something
+ if (trace_fraction < 1)
+ {
+ // stepswim
+ vector stepswim_move = move + stepheightvec;
+ if (flatdist > 0 && stepswim_move.z > end2.z + stepheightvec.z) // don't allow stepswim to go higher than destination
+ stepswim_move.z = end2.z;
+
+ tracebox(org + stepheightvec, m1, m2, stepswim_move, movemode, e);
+
+ // hit something
+ if (trace_startsolid)
+ {
+ if(autocvar_bot_debug_tracewalk)
+ debugnodestatus(org, DEBUG_NODE_FAIL);
+
+ //print("tracewalk: ", vtos(start), " hit something when trying to reach ", vtos(end), "\n");
+ return false;
+ }
+
+ if (trace_fraction < 1)
+ {
+ float org_z_prev = org.z;
+ RESURFACE_LIMITED(org, end2.z);
+ if(org.z == org_z_prev)
+ {
+ if(autocvar_bot_debug_tracewalk)
+ debugnodestatus(org, DEBUG_NODE_FAIL);
+
+ //print("tracewalk: ", vtos(start), " can't reach ", vtos(end), "\n");
+ return false;
+ }
+ if(SUBMERGED(org))
+ nav_action = NAV_SWIM_UNDERWATER;
+ else
+ nav_action = NAV_SWIM_ONWATER;
+
+ // we didn't advance horiz. in this step, flatdist decrease should be reverted
+ // but we can't do it properly right now... apply this workaround instead
+ if (flatdist <= 0)
+ flatdist = 1;
+
+ continue;
+ }
+
+ //succesful stepswim
+
+ if (flatdist <= 0)
+ {
+ org = trace_endpos;
+ continue;
+ }
+
+ stepswum = true;
+ }
+
+ if (!WETFEET(trace_endpos))
+ {
+ tracebox(trace_endpos, m1, m2, trace_endpos - eZ * (stepdist + (m2.z - m1.z)), movemode, e);
+ // if stepswum we'll land on the obstacle, avoid the SUBMERGED check
+ if (!stepswum && SUBMERGED(trace_endpos))
+ {
+ RESURFACE_LIMITED(trace_endpos, end2.z);
+ org = trace_endpos;
+ nav_action = NAV_SWIM_ONWATER;
+ continue;
+ }
+
+ // not submerged
+ org = trace_endpos;
+ nav_action = NAV_WALK;
+ continue;
+ }
+
+ // wetfeet
+ org = trace_endpos;
+ nav_action = NAV_SWIM_UNDERWATER;
+ continue;
+ }
+ else //if (move.z < org.z) // swimming downwards
+ {
+ tracebox(org, m1, m2, move, movemode, e); // swim
+
+ // hit something
+ if (trace_fraction < 1)
+ {
+ // stepswim
+ tracebox(org + stepheightvec, m1, m2, move + stepheightvec, movemode, e);
+
+ // hit something
+ if (trace_fraction < 1 || trace_startsolid) // can't jump obstacle out of water
+ {
+ if(autocvar_bot_debug_tracewalk)
+ debugnodestatus(move, DEBUG_NODE_FAIL);
+
+ //print("tracewalk: ", vtos(start), " hit something when trying to reach ", vtos(end), "\n");
+ return false;
+ }
+
+ //succesful stepswim
+
+ if (flatdist <= 0)
+ {
+ org = trace_endpos;
+ continue;
+ }
+
+ if (trace_endpos.z > org.z && !SUBMERGED(trace_endpos))
+ {
+ // stepswim caused upwards direction
+ tracebox(trace_endpos, m1, m2, trace_endpos - stepheightvec, movemode, e);
+ if (!SUBMERGED(trace_endpos))
+ {
+ org = trace_endpos;
+ nav_action = NAV_WALK;
+ continue;
+ }
+ }
+ }
+
+ org = trace_endpos;
+ nav_action = NAV_SWIM_UNDERWATER;
+ continue;
+ }
+ }
+ else if(nav_action == NAV_WALK)
{
- move = dir * stepdist + org;
+ // walk
tracebox(org, m1, m2, move, movemode, e);
if(autocvar_bot_debug_tracewalk)
tracebox(org + stepheightvec, m1, m2, move + stepheightvec, movemode, e);
if (trace_fraction < 1 || trace_startsolid)
{
- tracebox(org + jumpstepheightvec, m1, m2, move + jumpstepheightvec, movemode, e);
- if (trace_fraction < 1 || trace_startsolid)
+ if (trace_startsolid) // hit ceiling above org
+ {
+ // reduce stepwalk height
+ tracebox(org, m1, m2, org + stepheightvec, movemode, e);
+ tracebox(trace_endpos, m1, m2, move + eZ * (trace_endpos.z - move.z), movemode, e);
+ }
+ else //if (trace_fraction < 1)
{
- bool ladder_found = false;
- IL_EACH(g_ladders, it.classname == "func_ladder",
+ tracebox(org + jumpstepheightvec, m1, m2, move + jumpstepheightvec, movemode, e);
+ if (trace_startsolid) // hit ceiling above org
{
- if(it.bot_pickup)
- if(boxesoverlap(org + jumpheight_vec + m1 + '-1 -1 -1', org + jumpheight_vec + m2 + '1 1 1', it.absmin, it.absmax))
- if(boxesoverlap(end, end2, it.absmin + (m1 - eZ * m1.z - '1 1 0'), it.absmax + (m2 - eZ * m2.z + '1 1 0')))
- ladder_found = true; // can't return here ("Loop mutex held by tracewalk" error)
- });
- if(ladder_found)
+ // reduce jumpstepwalk height
+ tracebox(org, m1, m2, org + jumpstepheightvec, movemode, e);
+ tracebox(trace_endpos, m1, m2, move + eZ * (trace_endpos.z - move.z), movemode, e);
+ }
+ }
+
+ if (trace_fraction < 1)
+ {
+ vector v = trace_endpos;
+ v.z = org.z + jumpheight_vec.z;
+ if(navigation_checkladders(e, v, m1, m2, end, end2, movemode))
{
if(autocvar_bot_debug_tracewalk)
- debugnodestatus(end, DEBUG_NODE_SUCCESS);
+ {
+ debugnode(e, v);
+ debugnodestatus(v, DEBUG_NODE_SUCCESS);
+ }
//print("tracewalk: ", vtos(start), " can reach ", vtos(end), "\n");
return true;
move = trace_endpos;
while(trace_ent.classname == "door_rotating" || trace_ent.classname == "door")
{
- nextmove = move + (dir * stepdist);
+ nextmove = move + (flatdir * stepdist);
traceline( move, nextmove, movemode, e);
move = nextmove;
}
+ flatdist = vlen(vec2(end - move));
}
else
{
// (this is the same logic as the Quake walkmove function used)
tracebox(move, m1, m2, move + '0 0 -65536', movemode, e);
- // moved successfully
- if(swimming)
+ org = trace_endpos;
+
+ if (!ignorehazards)
{
- float c;
- c = pointcontents(org + '0 0 1');
- if (!(c == CONTENT_WATER || c == CONTENT_LAVA || c == CONTENT_SLIME))
- swimming = false;
- else
- continue;
+ if (IN_LAVA(org))
+ {
+ if(autocvar_bot_debug_tracewalk)
+ {
+ debugnode(e, trace_endpos);
+ debugnodestatus(org, DEBUG_NODE_FAIL);
+ }
+
+ //print("tracewalk: ", vtos(start), " hits a hazard when trying to reach ", vtos(end), "\n");
+ return false;
+ }
}
- org = trace_endpos;
+ if (flatdist <= 0)
+ {
+ if(move.z >= end2.z && org.z < end2.z)
+ org.z = end2.z;
+ continue;
+ }
+
+ if(org.z > move.z - 1 || !SUBMERGED(org))
+ {
+ nav_action = NAV_WALK;
+ continue;
+ }
+
+ // ended up submerged while walking
+ if(autocvar_bot_debug_tracewalk)
+ debugnode(e, org);
+
+ RESURFACE_LIMITED(org, move.z);
+ nav_action = NAV_SWIM_ONWATER;
+ continue;
}
}
void navigation_clearroute(entity this)
{
this.goalcurrent_prev = this.goalcurrent;
- this.goalcurrent_distance = 10000000;
+ this.goalcurrent_distance_2d = FLOAT_MAX;
+ this.goalcurrent_distance_z = FLOAT_MAX;
this.goalcurrent_distance_time = 0;
- //print("bot ", etos(this), " clear\n");
+ this.goalentity_lock_timeout = 0;
this.goalentity = NULL;
this.goalcurrent = NULL;
this.goalstack01 = NULL;
void navigation_pushroute(entity this, entity e)
{
this.goalcurrent_prev = this.goalcurrent;
- this.goalcurrent_distance = 10000000;
+ this.goalcurrent_distance_2d = FLOAT_MAX;
+ this.goalcurrent_distance_z = FLOAT_MAX;
this.goalcurrent_distance_time = 0;
//print("bot ", etos(this), " push ", etos(e), "\n");
if(this.goalstack31 == this.goalentity)
void navigation_poproute(entity this)
{
this.goalcurrent_prev = this.goalcurrent;
- this.goalcurrent_distance = 10000000;
+ this.goalcurrent_distance_2d = FLOAT_MAX;
+ this.goalcurrent_distance_z = FLOAT_MAX;
this.goalcurrent_distance_time = 0;
//print("bot ", etos(this), " pop\n");
if(this.goalcurrent == this.goalentity)
+ {
this.goalentity = NULL;
+ this.goalentity_lock_timeout = 0;
+ }
this.goalcurrent = this.goalstack01;
this.goalstack01 = this.goalstack02;
this.goalstack02 = this.goalstack03;
this.goalstack31 = NULL;
}
-float navigation_waypoint_will_link(vector v, vector org, entity ent, float walkfromwp, float bestdist)
+// walking to wp (walkfromwp == false) v2 and v2_height will be used as
+// waypoint destination coordinates instead of v (only useful for box waypoints)
+// for normal waypoints v2 == v and v2_height == 0
+float navigation_waypoint_will_link(vector v, vector org, entity ent, vector v2, float v2_height, vector o2, float o2_height, float walkfromwp, float bestdist)
{
- float dist;
- dist = vlen(v - org);
- if (bestdist > dist)
+ if (vdist(v - org, <, bestdist))
{
traceline(v, org, true, ent);
if (trace_fraction == 1)
{
if (walkfromwp)
{
- if (tracewalk(ent, v, PL_MIN_CONST, PL_MAX_CONST, org, 0, bot_navigation_movemode))
+ if (tracewalk(ent, v, PL_MIN_CONST, PL_MAX_CONST, v2, v2_height, bot_navigation_movemode))
return true;
}
else
{
- if (tracewalk(ent, org, PL_MIN_CONST, PL_MAX_CONST, v, 0, bot_navigation_movemode))
+ if (tracewalk(ent, org, PL_MIN_CONST, PL_MAX_CONST, o2, o2_height, bot_navigation_movemode))
return true;
}
}
// find the spawnfunc_waypoint near a dynamic goal such as a dropped weapon
entity navigation_findnearestwaypoint_withdist_except(entity ent, float walkfromwp, float bestdist, entity except)
{
+ if(ent.tag_entity)
+ ent = ent.tag_entity;
+
vector pm1 = ent.origin + ent.mins;
vector pm2 = ent.origin + ent.maxs;
IL_EACH(g_waypoints, it != ent && it != except,
{
if(boxesoverlap(pm1, pm2, it.absmin, it.absmax))
+ {
+ if(!autocvar_g_waypointeditor && walkfromwp && !ent.navigation_dynamicgoal)
+ {
+ waypoint_clearlinks(ent); // initialize wpXXmincost fields
+ navigation_item_addlink(it, ent);
+ }
return it;
+ }
});
- vector org = ent.origin + 0.5 * (ent.mins + ent.maxs);
- org.z = ent.origin.z + ent.mins.z - PL_MIN_CONST.z; // player height
- // TODO possibly make other code have the same support for bboxes
- if(ent.tag_entity)
- org = org + ent.tag_entity.origin;
+ vector org = ent.origin;
if (navigation_testtracewalk)
te_plasmaburn(org);
entity best = NULL;
- vector v;
+ vector v = '0 0 0';
+
+ if(ent.size && !IS_PLAYER(ent))
+ {
+ org += 0.5 * (ent.mins + ent.maxs);
+ org.z = ent.origin.z + ent.mins.z - PL_MIN_CONST.z; // player height
+ }
- if(!autocvar_g_waypointeditor && !ent.navigation_dynamicgoal)
+ if(!autocvar_g_waypointeditor && walkfromwp && !ent.navigation_dynamicgoal)
{
waypoint_clearlinks(ent); // initialize wpXXmincost fields
IL_EACH(g_waypoints, it != ent,
{
- if(it.wpisbox)
+ if(walkfromwp && (it.wpflags & WAYPOINTFLAG_NORELINK))
+ continue;
+
+ set_tracewalk_dest(ent, it.origin, false);
+ if (vdist(tracewalk_dest - it.origin, <, 1050)
+ && tracewalk(ent, it.origin, PL_MIN_CONST, PL_MAX_CONST,
+ tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))
{
- vector wm1 = it.absmin;
- vector wm2 = it.absmax;
- v.x = bound(wm1_x, org.x, wm2_x);
- v.y = bound(wm1_y, org.y, wm2_y);
- v.z = bound(wm1_z, org.z, wm2_z);
- }
- else
- v = it.origin;
- if(navigation_waypoint_will_link(v, org, ent, walkfromwp, 1050))
navigation_item_addlink(it, ent);
+ }
});
}
// box check failed, try walk
IL_EACH(g_waypoints, it != ent,
{
- if(it.wpisbox)
+ if(walkfromwp && (it.wpflags & WAYPOINTFLAG_NORELINK))
+ continue;
+ v = it.origin;
+
+ if (walkfromwp)
{
- vector wm1 = it.origin + it.mins;
- vector wm2 = it.origin + it.maxs;
- v.x = bound(wm1_x, org.x, wm2_x);
- v.y = bound(wm1_y, org.y, wm2_y);
- v.z = bound(wm1_z, org.z, wm2_z);
+ set_tracewalk_dest(ent, v, true);
+ if (trace_ent == ent)
+ {
+ bestdist = 0;
+ best = it;
+ break;
+ }
}
else
- v = it.origin;
- if(navigation_waypoint_will_link(v, org, ent, walkfromwp, bestdist))
+ set_tracewalk_dest(it, org, false);
+
+ if (navigation_waypoint_will_link(v, org, ent,
+ tracewalk_dest, tracewalk_dest_height,
+ tracewalk_dest, tracewalk_dest_height, walkfromwp, bestdist))
{
- bestdist = vlen(v - org);
+ if (walkfromwp)
+ bestdist = vlen(tracewalk_dest - org);
+ else
+ bestdist = vlen(v - org);
best = it;
}
});
+ if(!best && !ent.navigation_dynamicgoal)
+ {
+ int solid_save = ent.solid;
+ ent.solid = SOLID_BSP;
+ IL_EACH(g_jumppads, true,
+ {
+ if(trigger_push_test(it, ent))
+ {
+ best = it.nearestwaypoint;
+ break;
+ }
+ });
+ ent.solid = solid_save;
+ }
return best;
}
entity navigation_findnearestwaypoint(entity ent, float walkfromwp)
// finds the waypoints near the bot initiating a navigation query
float navigation_markroutes_nearestwaypoints(entity this, float maxdist)
{
- vector v, m1, m2;
//navigation_testtracewalk = true;
int c = 0;
- float v_height;
IL_EACH(g_waypoints, !it.wpconsidered,
{
- if (it.wpisbox)
- {
- m1 = it.origin + it.mins;
- m2 = it.origin + it.maxs;
- v = this.origin;
- v.x = bound(m1_x, v.x, m2_x);
- v.y = bound(m1_y, v.y, m2_y);
- v.z = m1.z;
- v_height = m2.z - m1.z;
- }
- else
- {
- v = it.origin;
- v_height = 0;
- }
- vector diff = v - this.origin;
+ set_tracewalk_dest(it, this.origin, false);
+
+ vector diff = tracewalk_dest - this.origin;
diff.z = max(0, diff.z);
if(vdist(diff, <, maxdist))
{
it.wpconsidered = true;
- if (tracewalk(this, this.origin, this.mins, this.maxs, v, v_height, bot_navigation_movemode))
+ if (tracewalk(this, this.origin, this.mins, this.maxs,
+ tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))
{
- it.wpnearestpoint = v;
- it.wpcost = waypoint_getdistancecost(this.origin, v) + it.dmg;
+ it.wpnearestpoint = tracewalk_dest;
+ it.wpcost = waypoint_gettravelcost(this.origin, tracewalk_dest, this, it) + it.dmg;
it.wpfire = 1;
it.enemy = NULL;
c = c + 1;
// updates a path link if a spawnfunc_waypoint link is better than the current one
void navigation_markroutes_checkwaypoint(entity w, entity wp, float cost, vector p)
{
- vector m1;
- vector m2;
+ vector m1, m2;
vector v;
if (wp.wpisbox)
{
- m1 = wp.absmin;
- m2 = wp.absmax;
+ m1 = wp.origin + wp.mins;
+ m2 = wp.origin + wp.maxs;
v.x = bound(m1_x, p.x, m2_x);
v.y = bound(m1_y, p.y, m2_y);
v.z = bound(m1_z, p.z, m2_z);
if (w.wpflags & WAYPOINTFLAG_TELEPORT)
cost += w.wp00mincost; // assuming teleport has exactly one destination
else
- cost += waypoint_getdistancecost(p, v);
+ cost += waypoint_gettravelcost(p, v, w, wp);
if (wp.wpcost > cost)
{
wp.wpcost = cost;
if(e.blacklisted)
return;
- rangebias = waypoint_getdistancecost_simple(rangebias);
- f = waypoint_getdistancecost_simple(f);
+ rangebias = waypoint_getlinearcost(rangebias);
+ f = waypoint_getlinearcost(f);
if (IS_PLAYER(e))
{
if ((!e.nearestwaypoint || e.navigation_dynamicgoal)
&& e.nearestwaypointtimeout >= 0 && time > e.nearestwaypointtimeout)
{
- nwp = navigation_findnearestwaypoint(e, true);
- if(nwp)
- {
- e.nearestwaypoint = nwp;
-
- vector m1 = nwp.absmin, m2 = nwp.absmax;
- m1.x = nwp.origin.x; m1.y = nwp.origin.y;
- m2.x = nwp.origin.x; m2.y = nwp.origin.y;
- vector ve = (e.absmin - e.absmax) * 0.5;
- ve.x = bound(m1.x, ve.x, m2.x);
- ve.y = bound(m1.y, ve.y, m2.y);
- ve.z = bound(m1.z, ve.z, m2.z);
-
- m1 = e.absmin; m2 = e.absmax;
- m1.x = e.origin.x; m1.y = e.origin.y;
- m2.x = e.origin.x; m2.y = e.origin.y;
- vector vnwp = nwp.origin;
- vnwp.x = bound(m1.x, vnwp.x, m2.x);
- vnwp.y = bound(m1.y, vnwp.y, m2.y);
- vnwp.z = bound(m1.z, vnwp.z, m2.z);
- e.nearestwaypoint_dist = vlen(ve - vnwp);
- }
+ if(IS_BOT_CLIENT(e) && e.goalcurrent && e.goalcurrent.classname == "waypoint")
+ e.nearestwaypoint = nwp = e.goalcurrent;
else
+ e.nearestwaypoint = nwp = navigation_findnearestwaypoint(e, true);
+ if(!nwp)
{
LOG_DEBUG("FAILED to find a nearest waypoint to '", e.classname, "' #", etos(e));
}
LOG_DEBUG("-- checking ", e.classname, " (with cost ", ftos(nwp.wpcost), ")");
- if (nwp)
- if (nwp.wpcost < 10000000)
+ if (nwp && nwp.wpcost < 10000000)
{
//te_wizspike(nwp.wpnearestpoint);
- float cost = nwp.wpcost + waypoint_getdistancecost(nwp.wpnearestpoint, goal_org);
+ float nwptoitem_cost = 0;
+ if(nwp.wpflags & WAYPOINTFLAG_TELEPORT)
+ nwptoitem_cost = nwp.wp00mincost;
+ else
+ nwptoitem_cost = waypoint_gettravelcost(nwp.wpnearestpoint, goal_org, nwp, e);
+ float cost = nwp.wpcost + nwptoitem_cost;
LOG_DEBUG(e.classname, " ", ftos(f), "/(1+", ftos(cost), "/", ftos(rangebias), ") = ");
f = f * rangebias / (rangebias + cost);
LOG_DEBUG("considering ", e.classname, " (with rating ", ftos(f), ")");
if (!e)
return false;
+ entity teleport_goal = NULL;
+
+ this.goalentity = e;
+
if(e.wpflags & WAYPOINTFLAG_TELEPORT)
{
// force teleport destination as route destination
- e.wp00.enemy = e;
- e = e.wp00;
+ teleport_goal = e;
+ navigation_pushroute(this, e.wp00);
+ this.goalentity = e.wp00;
}
- this.goalentity = e;
-
// put the entity on the goal stack
//print("routetogoal ", etos(e), "\n");
navigation_pushroute(this, e);
+ if(teleport_goal)
+ e = this.goalentity;
+
if(e.classname == "waypoint" && !(e.wpflags & WAYPOINTFLAG_PERSONAL))
{
this.wp_goal_prev1 = this.wp_goal_prev0;
return true;
// if it can reach the goal there is nothing more to do
- vector dest = (e.absmin + e.absmax) * 0.5;
- dest.z = e.absmin.z;
- float dest_height = e.absmax.z - e.absmin.z;
- if (tracewalk(this, startposition, STAT(PL_MIN, this), STAT(PL_MAX, this), dest, dest_height, bot_navigation_movemode))
+ set_tracewalk_dest(e, startposition, true);
+ if (trace_ent == this || tracewalk(this, startposition, STAT(PL_MIN, this), STAT(PL_MAX, this),
+ tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))
+ {
return true;
+ }
entity nearest_wp = NULL;
// see if there are waypoints describing a path to the item
e = e.nearestwaypoint;
nearest_wp = e;
}
+ else if(teleport_goal)
+ e = teleport_goal;
else
e = e.enemy; // we already have added it, so...
if(nearest_wp && nearest_wp.enemy)
{
// often path can be optimized by not adding the nearest waypoint
- if (this.goalentity.nearestwaypoint_dist < 8)
- e = nearest_wp.enemy;
- else
+ if (this.goalentity.navigation_dynamicgoal || autocvar_g_waypointeditor)
{
- if (this.goalentity.navigation_dynamicgoal)
+ set_tracewalk_dest(this.goalentity, nearest_wp.enemy.origin, true);
+ if (trace_ent == this || (vdist(tracewalk_dest - nearest_wp.enemy.origin, <, 1050)
+ && tracewalk(this, nearest_wp.enemy.origin, STAT(PL_MIN, this), STAT(PL_MAX, this),
+ tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode)))
{
- vector dest = (this.goalentity.absmin + this.goalentity.absmax) * 0.5;
- dest.z = this.goalentity.absmin.z;
- float dest_height = this.goalentity.absmax.z - this.goalentity.absmin.z;
- if(tracewalk(this, nearest_wp.enemy.origin, STAT(PL_MIN, this), STAT(PL_MAX, this), dest, dest_height, bot_navigation_movemode))
- e = nearest_wp.enemy;
- }
- else if(navigation_item_islinked(nearest_wp.enemy, this.goalentity))
e = nearest_wp.enemy;
+ }
}
+ else if(navigation_item_islinked(nearest_wp.enemy, this.goalentity))
+ e = nearest_wp.enemy;
}
for (;;)
// removes any currently touching waypoints from the goal stack
// (this is how bots detect if they reached a goal)
-void navigation_poptouchedgoals(entity this)
+int navigation_poptouchedgoals(entity this)
{
+ int removed_goals = 0;
+
+ if(!this.goalcurrent)
+ return removed_goals;
+
if(this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT)
{
// make sure jumppad is really hit, don't rely on distance based checks
this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED;
}
navigation_poproute(this);
+ this.lastteleporttime = 0;
+ ++removed_goals;
}
else
- return;
+ return removed_goals;
}
// If for some reason the bot is closer to the next goal, pop the current one
- if(this.goalstack01 && !wasfreed(this.goalstack01))
- if(vlen2(this.goalcurrent.origin - this.origin) > vlen2(this.goalstack01.origin - this.origin))
- if(checkpvs(this.origin + this.view_ofs, this.goalstack01))
+ // randomness should help to get unstuck bot on certain hard paths with climbs and tight corners
+ if (this.goalstack01 && !wasfreed(this.goalstack01) && random() < 0.7)
{
- vector dest = (this.goalstack01.absmin + this.goalstack01.absmax) * 0.5;
- dest.z = this.goalstack01.absmin.z;
- float dest_height = this.goalstack01.absmax.z - this.goalstack01.absmin.z;
- if(tracewalk(this, this.origin, this.mins, this.maxs, dest, dest_height, bot_navigation_movemode))
+ entity next = IS_PLAYER(this.goalentity) ? this.goalentity : this.goalstack01;
+ if (vlen2(this.goalcurrent.origin - next.origin) > vlen2(next.origin - this.origin)
+ && checkpvs(this.origin + this.view_ofs, next))
{
- LOG_DEBUG("path optimized for ", this.netname, ", removed a goal from the queue");
- navigation_poproute(this);
- // TODO this may also be a nice idea to do "early" (e.g. by
- // manipulating the vlen() comparisons) to shorten paths in
- // general - this would make bots walk more "on rails" than
- // "zigzagging" which they currently do with sufficiently
- // random-like waypoints, and thus can make a nice bot
- // personality property
+ set_tracewalk_dest(next, this.origin, true);
+ if (trace_ent == this || tracewalk(this, this.origin, this.mins, this.maxs,
+ tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))
+ {
+ LOG_DEBUG("path optimized for ", this.netname, ", removed a goal from the queue");
+ do
+ {
+ // loop clears the whole route if next is a player
+ navigation_poproute(this);
+ ++removed_goals;
+ }
+ while (this.goalcurrent == next);
+ if (this.goalcurrent && this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT)
+ return removed_goals;
+ // TODO this may also be a nice idea to do "early" (e.g. by
+ // manipulating the vlen() comparisons) to shorten paths in
+ // general - this would make bots walk more "on rails" than
+ // "zigzagging" which they currently do with sufficiently
+ // random-like waypoints, and thus can make a nice bot
+ // personality property
+ }
}
}
// Loose goal touching check when running
if(this.aistatus & AI_STATUS_RUNNING)
if(this.goalcurrent.classname=="waypoint")
- if(vlen(this.velocity - eZ * this.velocity.z) >= autocvar_sv_maxspeed) // if -really- running
+ if(vdist(vec2(this.velocity), >=, autocvar_sv_maxspeed)) // if -really- running
{
if(vdist(this.origin - this.goalcurrent.origin, <, 150))
{
}
navigation_poproute(this);
+ ++removed_goals;
+ if(this.goalcurrent && this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT)
+ return removed_goals;
}
}
}
}
navigation_poproute(this);
+ ++removed_goals;
+ if(this.goalcurrent && this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT)
+ return removed_goals;
+ }
+ return removed_goals;
+}
+
+entity navigation_get_really_close_waypoint(entity this)
+{
+ entity wp = this.goalcurrent;
+ if(!wp || vdist(wp.origin - this.origin, >, 50))
+ wp = this.goalcurrent_prev;
+ if(!wp)
+ return NULL;
+ if(wp.classname != "waypoint")
+ {
+ wp = wp.nearestwaypoint;
+ if(!wp)
+ return NULL;
}
+ if(vdist(wp.origin - this.origin, >, 50))
+ {
+ IL_EACH(g_waypoints, !(it.wpflags & WAYPOINTFLAG_TELEPORT),
+ {
+ if(vdist(it.origin - this.origin, <, 50))
+ {
+ wp = it;
+ break;
+ }
+ });
+ }
+ if(wp.wpflags & WAYPOINTFLAG_TELEPORT)
+ return NULL;
+
+ set_tracewalk_dest(wp, this.origin, false);
+ if (!tracewalk(this, this.origin, STAT(PL_MIN, this), STAT(PL_MAX, this),
+ tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))
+ {
+ return NULL;
+ }
+ return wp;
}
// begin a goal selection session (queries spawnfunc_waypoint network)
this.navigation_jetpack_goal = NULL;
navigation_bestrating = -1;
+ entity wp = navigation_get_really_close_waypoint(this);
navigation_clearroute(this);
navigation_bestgoal = NULL;
- navigation_markroutes(this, NULL);
+ navigation_markroutes(this, wp);
}
// ends a goal selection session (updates goal stack to the best goal)
vector m1, m2, v, o;
float c, d, danger;
c = 0;
+ entity wp_cur;
IL_EACH(g_waypoints, true,
{
danger = 0;
m1 = it.absmin;
m2 = it.absmax;
+ wp_cur = it;
IL_EACH(g_bot_dodge, it.bot_dodge,
{
v = it.origin;
v.y = bound(m1_y, v.y, m2_y);
v.z = bound(m1_z, v.z, m2_z);
o = (it.absmin + it.absmax) * 0.5;
- d = waypoint_getdistancecost_simple(it.bot_dodgerating) - waypoint_getdistancecost(o, v);
+ d = waypoint_getlinearcost(it.bot_dodgerating) - waypoint_gettravelcost(o, v, it, wp_cur);
if (d > 0)
{
traceline(o, v, true, NULL);
// evaluate the next goal on the queue
float d = vlen2(this.origin - bot_waypoint_queue_goal.origin);
LOG_DEBUG(this.netname, " evaluating ", bot_waypoint_queue_goal.classname, " with distance ", ftos(d));
- vector dest = (bot_waypoint_queue_goal.absmin + bot_waypoint_queue_goal.absmax) * 0.5;
- dest.z = bot_waypoint_queue_goal.absmin.z;
- float dest_height = bot_waypoint_queue_goal.absmax.z - bot_waypoint_queue_goal.absmin.z;
- if(tracewalk(bot_waypoint_queue_goal, this.origin, STAT(PL_MIN, this), STAT(PL_MAX, this), dest, dest_height, bot_navigation_movemode))
+ set_tracewalk_dest(bot_waypoint_queue_goal, this.origin, false);
+ if (tracewalk(bot_waypoint_queue_goal, this.origin, STAT(PL_MIN, this), STAT(PL_MAX, this),
+ tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))
{
if( d > bot_waypoint_queue_bestgoalrating)
{
{
LOG_DEBUG(this.netname, " stuck, reachable waypoint found, heading to it");
navigation_routetogoal(this, bot_waypoint_queue_bestgoal, this.origin);
- this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
+ navigation_goalrating_timeout_set(this);
this.aistatus &= ~AI_STATUS_STUCK;
}
else