]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blobdiff - qcsrc/server/bot/default/navigation.qc
Merge branch 'master' into terencehill/bot_ai
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / bot / default / navigation.qc
index cfaec7f2669957068c58c41b5116e6b08008a72a..4665be482a69287c8eb0d3d46b4b292e226f7717 100644 (file)
@@ -13,7 +13,7 @@
 
 #include <common/constants.qh>
 #include <common/net_linked.qh>
-#include <common/triggers/trigger/jumppads.qh>
+#include <common/mapobjects/trigger/jumppads.qh>
 
 .float speed;
 
@@ -25,9 +25,20 @@ void navigation_goalrating_timeout_set(entity this)
                this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
 }
 
+// use this when current goal must be discarded immediately
 void navigation_goalrating_timeout_force(entity this)
 {
-       this.bot_strategytime = 0;
+       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)
@@ -35,6 +46,7 @@ bool navigation_goalrating_timeout(entity this)
        return this.bot_strategytime < time;
 }
 
+#define MAX_CHASE_DISTANCE 700
 bool navigation_goalrating_timeout_can_be_anticipated(entity this)
 {
        if(time > this.bot_strategytime - (IS_MOVABLE(this.goalentity) ? 3 : 2))
@@ -66,6 +78,8 @@ void navigation_dynamicgoal_init(entity this, bool initially_static)
 void navigation_dynamicgoal_set(entity this)
 {
        this.nearestwaypointtimeout = time;
+       if (this.nearestwaypoint)
+               this.nearestwaypointtimeout += 2;
 }
 
 void navigation_dynamicgoal_unset(entity this)
@@ -75,6 +89,97 @@ 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;
+               vector wm2 = ent.origin + ent.maxs;
+               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;
+               vector wm2 = ent.origin + ent.maxs;
+               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;
+               vector wm2 = ent.origin + ent.maxs;
+               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;
@@ -141,6 +246,7 @@ vector resurface_limited(vector org, float lim, vector m1)
 // 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]
+// INFO: the command sv_cmd trace walk is useful to test this function in game
 bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float end_height, float movemode)
 {
        if(autocvar_bot_debug_tracewalk)
@@ -159,8 +265,7 @@ bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float e
        int nav_action;
 
        // Analyze starting point
-       traceline(start, start, MOVE_NORMAL, e);
-       if (trace_dpstartcontents & (DPCONTENTS_SLIME | DPCONTENTS_LAVA))
+       if (IN_LAVA(start))
                ignorehazards = true;
 
        tracebox(start, m1, m2, start, MOVE_NOMONSTERS, e);
@@ -625,7 +730,8 @@ void navigation_clearroute(entity this)
        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_shouldbefrozen = false;
        this.goalentity = NULL;
        this.goalcurrent = NULL;
        this.goalstack01 = NULL;
@@ -721,7 +827,10 @@ void navigation_poproute(entity this)
        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;
@@ -809,8 +918,7 @@ entity navigation_findnearestwaypoint_withdist_except(entity ent, float walkfrom
                te_plasmaburn(org);
 
        entity best = NULL;
-       vector v = '0 0 0', v2 = '0 0 0';
-       float v2_height = 0;
+       vector v = '0 0 0';
 
        if(ent.size && !IS_PLAYER(ent))
        {
@@ -826,10 +934,13 @@ entity navigation_findnearestwaypoint_withdist_except(entity ent, float walkfrom
                        if(walkfromwp && (it.wpflags & WAYPOINTFLAG_NORELINK))
                                continue;
 
-                       SET_TRACEWALK_DESTCOORDS(ent, it.origin, v2, v2_height);
-                       if(vdist(v2 - it.origin, <, 1050))
-                       if(tracewalk(ent, it.origin, PL_MIN_CONST, PL_MAX_CONST, v2, v2_height, bot_navigation_movemode))
+                       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))
+                       {
                                navigation_item_addlink(it, ent);
+                       }
                });
        }
 
@@ -839,16 +950,45 @@ entity navigation_findnearestwaypoint_withdist_except(entity ent, float walkfrom
                if(walkfromwp && (it.wpflags & WAYPOINTFLAG_NORELINK))
                        continue;
                v = it.origin;
-               if(walkfromwp)
-                       SET_TRACEWALK_DESTCOORDS(ent, v, v2, v2_height);
+
+               if (walkfromwp)
+               {
+                       set_tracewalk_dest(ent, v, true);
+                       if (trace_ent == ent)
+                       {
+                               bestdist = 0;
+                               best = it;
+                               break;
+                       }
+               }
                else
-                       SET_TRACEWALK_DESTCOORDS(it, org, v2, v2_height);
-               if(navigation_waypoint_will_link(v, org, ent, v2, v2_height, v2, v2_height, 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 - v);
+                       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)
@@ -866,23 +1006,22 @@ 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 = '0 0 0';
        //navigation_testtracewalk = true;
        int c = 0;
-       float v_height = 0;
        IL_EACH(g_waypoints, !it.wpconsidered,
        {
-               SET_TRACEWALK_DESTCOORDS(it, this.origin, v, v_height);
+               set_tracewalk_dest(it, this.origin, false);
 
-               vector diff = v - this.origin;
+               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_gettravelcost(this.origin, v, this, it) + 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;
@@ -1095,12 +1234,13 @@ void navigation_routerating(entity this, entity e, float f, float rangebias)
                {
                        entity theEnemy = e;
                        entity best_wp = NULL;
-                       float best_dist = 10000;
-                       IL_EACH(g_waypoints, vdist(it.origin - theEnemy.origin, <, 500)
+                       float best_dist = FLOAT_MAX;
+                       IL_EACH(g_waypoints, !(it.wpflags & WAYPOINTFLAG_TELEPORT)
+                               && vdist(it.origin - theEnemy.origin, <, 500)
                                && vdist(it.origin - this.origin, >, 100)
-                               && !(it.wpflags & WAYPOINTFLAG_TELEPORT),
+                               && vdist(it.origin - this.origin, <, 10000),
                        {
-                               float dist = vlen(it.origin - theEnemy.origin);
+                               float dist = vlen2(it.origin - theEnemy.origin);
                                if (dist < best_dist)
                                {
                                        best_wp = it;
@@ -1118,7 +1258,6 @@ void navigation_routerating(entity this, entity e, float f, float rangebias)
        //print("routerating ", etos(e), " = ", ftos(f), " - ", ftos(rangebias), "\n");
 
        // Evaluate path using jetpack
-       if(g_jetpack)
        if(this.items & IT_JETPACK)
        if(autocvar_bot_ai_navigation_jetpack)
        if(vdist(this.origin - goal_org, >, autocvar_bot_ai_navigation_jetpack_mindistance))
@@ -1177,10 +1316,10 @@ void navigation_routerating(entity this, entity e, float f, float rangebias)
                        t += xydistance / autocvar_g_jetpack_maxspeed_side;
                        fuel = t * autocvar_g_jetpack_fuel * 0.8;
 
-                       LOG_DEBUG("jetpack ai: required fuel ", ftos(fuel), " this.ammo_fuel ", ftos(this.ammo_fuel));
+                       LOG_DEBUG("jetpack ai: required fuel ", ftos(fuel), ", have ", ftos(GetResourceAmount(this, RESOURCE_FUEL)));
 
                        // enough fuel ?
-                       if(this.ammo_fuel>fuel)
+                       if(GetResourceAmount(this, RESOURCE_FUEL) > fuel || (this.items & IT_UNLIMITED_WEAPON_AMMO))
                        {
                                // Estimate cost
                                // (as onground costs calculation is mostly based on distances, here we do the same establishing some relationship
@@ -1223,7 +1362,10 @@ void navigation_routerating(entity this, entity e, float f, float rangebias)
                if ((!e.nearestwaypoint || e.navigation_dynamicgoal)
                        && e.nearestwaypointtimeout >= 0 && time > e.nearestwaypointtimeout)
                {
-                       e.nearestwaypoint = nwp = navigation_findnearestwaypoint(e, true);
+                       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));
@@ -1246,18 +1388,21 @@ void navigation_routerating(entity this, entity e, float f, float rangebias)
                nwp = e.nearestwaypoint;
        }
 
-       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_gettravelcost(nwp.wpnearestpoint, goal_org, nwp, e);
-               LOG_DEBUG(e.classname, " ", ftos(f), "/(1+", ftos(cost), "/", ftos(rangebias), ") = ");
+               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("checking ^5", e.classname, "^7 with base rating ^xf04", ftos(f), "^7 and rangebias ^xf40", ftos(rangebias));
                f = f * rangebias / (rangebias + cost);
-               LOG_DEBUG("considering ", e.classname, " (with rating ", ftos(f), ")");
+               LOG_DEBUG("         ^5", e.classname, "^7 with cost ^6", ftos(cost), "^7 and final rating ^2", ftos(f));
                if (navigation_bestrating < f)
                {
-                       LOG_DEBUG("ground path: added goal ", e.classname, " (with rating ", ftos(f), ")");
+                       LOG_DEBUG(" ground path: ^3added goal ^5", e.classname);
                        navigation_bestrating = f;
                        navigation_bestgoal = e;
                }
@@ -1301,11 +1446,13 @@ bool navigation_routetogoal(entity this, entity e, vector startposition)
                return true;
 
        // if it can reach the goal there is nothing more to do
-       vector dest = '0 0 0';
-       float dest_height = 0;
-       SET_TRACEWALK_DESTCOORDS(e, startposition, dest, dest_height);
-       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 ((!IS_MOVABLE(this.goalcurrent) || vdist(tracewalk_dest - this.origin, <, MAX_CHASE_DISTANCE))
+               && (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
@@ -1327,10 +1474,22 @@ bool navigation_routetogoal(entity this, entity e, vector startposition)
                // often path can be optimized by not adding the nearest waypoint
                if (this.goalentity.navigation_dynamicgoal || autocvar_g_waypointeditor)
                {
-                       SET_TRACEWALK_DESTCOORDS(this.goalentity, nearest_wp.enemy.origin, dest, dest_height);
-                       if(vdist(dest - nearest_wp.enemy.origin, <, 1050))
-                       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;
+                       if (nearest_wp.enemy.wpcost < autocvar_bot_ai_strategyinterval_movingtarget)
+                       {
+                               if (vdist(vec2(this.goalentity.origin - nearest_wp.origin), <, 32))
+                                       e = nearest_wp.enemy;
+                               else
+                               {
+                                       set_tracewalk_dest(this.goalentity, nearest_wp.enemy.origin, true);
+                                       if (trace_ent == this || (vdist(tracewalk_dest - nearest_wp.enemy.origin, <, 1050)
+                                               && vlen2(tracewalk_dest - nearest_wp.enemy.origin) < vlen2(nearest_wp.origin - nearest_wp.enemy.origin)
+                                               && tracewalk(this, nearest_wp.enemy.origin, STAT(PL_MIN, this), STAT(PL_MAX, this),
+                                               tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode)))
+                                       {
+                                               e = nearest_wp.enemy;
+                                       }
+                               }
+                       }
                }
                else if(navigation_item_islinked(nearest_wp.enemy, this.goalentity))
                        e = nearest_wp.enemy;
@@ -1349,24 +1508,94 @@ bool navigation_routetogoal(entity this, entity e, vector startposition)
        return false;
 }
 
+// shorten path by removing intermediate goals
+bool navigation_shortenpath(entity this)
+{
+       if (!this.goalstack01 || wasfreed(this.goalstack01))
+               return false;
+       if (this.bot_tracewalk_time > time)
+               return false;
+       this.bot_tracewalk_time = max(time, this.bot_tracewalk_time) + 0.25;
+
+       bool cut_allowed = false;
+       entity next = this.goalentity;
+       // evaluate whether bot can discard current route and chase directly a player, trying to
+       // keep waypoint route as long as possible, as it is safer and faster (bot can bunnyhop)
+       if (IS_MOVABLE(next))
+       {
+               set_tracewalk_dest(next, this.origin, true);
+               if (vdist(this.origin - tracewalk_dest, <, 200))
+                       cut_allowed = true;
+               else if (vdist(tracewalk_dest - this.origin, <, MAX_CHASE_DISTANCE)
+                       && vdist(tracewalk_dest - this.goalcurrent.origin, >, 200)
+                       && vdist(this.origin - this.goalcurrent.origin, >, 100)
+                       && checkpvs(this.origin + this.view_ofs, next))
+               {
+                       if (vlen2(next.origin - this.origin) < vlen2(this.goalcurrent.origin - this.origin))
+                               cut_allowed = true;
+                       else
+                       {
+                               vector deviation = vectoangles(this.goalcurrent.origin - this.origin) - vectoangles(next.origin - this.origin);
+                               while (deviation.y < -180) deviation.y += 360;
+                               while (deviation.y > 180) deviation.y -= 360;
+                               if (fabs(deviation.y) > 25)
+                                       cut_allowed = true;
+                       }
+               }
+               if (cut_allowed)
+               {
+                       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, ", route cleared");
+                               do
+                               {
+                                       navigation_poproute(this);
+                               }
+                               while (this.goalcurrent != next);
+                               return true;
+                       }
+                       return false;
+               }
+       }
+
+       next = this.goalstack01;
+       // if for some reason the bot is closer to the next goal, pop the current one
+       if (!IS_MOVABLE(next) // already checked in the previous case
+               && vlen2(this.goalcurrent.origin - next.origin) > vlen2(next.origin - this.origin)
+               && checkpvs(this.origin + this.view_ofs, next))
+       {
+               set_tracewalk_dest(next, this.origin, true);
+               cut_allowed = true;
+       }
+
+       if (cut_allowed)
+       {
+               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");
+                       navigation_poproute(this);
+                       return true;
+               }
+       }
+       return false;
+}
+
 // removes any currently touching waypoints from the goal stack
 // (this is how bots detect if they reached a goal)
 int navigation_poptouchedgoals(entity this)
 {
        int removed_goals = 0;
 
-       if(IS_PLAYER(this.goalcurrent) && IS_DEAD(this.goalcurrent) && checkpvs(this.origin + this.view_ofs, this.goalcurrent))
-       {
-               navigation_poproute(this);
-               ++removed_goals;
-       }
+       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
                // as they may report a touch even if it didn't really happen
-               if(this.lastteleporttime > 0
-                       && time - this.lastteleporttime < ((this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL) ? 2 : 0.15))
+               if(this.lastteleporttime > 0 && TELEPORT_USED(this, this.goalcurrent))
                {
                        if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING)
                        if(this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && this.goalcurrent.owner==this)
@@ -1374,6 +1603,16 @@ int navigation_poptouchedgoals(entity this)
                                this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING;
                                this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED;
                        }
+                       if(this.jumppadcount)
+                       {
+                               // remove jumppad waypoint after a random delay to prevent bots getting
+                               // stuck on certain jumppads that require an extra initial horizontal speed
+                               float max_delay = 0.1;
+                               if (vdist(vec2(this.velocity), >, 2 * autocvar_sv_maxspeed))
+                                       max_delay = 0.05;
+                               if (time - this.lastteleporttime < random() * max_delay)
+                                       return removed_goals;
+                       }
                        navigation_poproute(this);
                        this.lastteleporttime = 0;
                        ++removed_goals;
@@ -1381,29 +1620,35 @@ int navigation_poptouchedgoals(entity this)
                else
                        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(random() < 0.7) // randomness should help on certain hard paths with climbs and tight corners
-       if(vlen2(this.goalcurrent.origin - this.goalstack01.origin) > vlen2(this.goalstack01.origin - this.origin))
-       if(checkpvs(this.origin + this.view_ofs, this.goalstack01))
+       else if (this.lastteleporttime > 0)
        {
-               vector dest = '0 0 0';
-               float dest_height = 0;
-               SET_TRACEWALK_DESTCOORDS(this.goalstack01, this.origin, dest, dest_height);
-               if(tracewalk(this, this.origin, this.mins, this.maxs, dest, dest_height, bot_navigation_movemode))
+               // sometimes bot is pushed so hard (by a jumppad or a shot) that ends up touching the next
+               // teleport / jumppad / warpzone present in its path skipping check of one or more goals
+               // if so immediately fix bot path by removing skipped goals
+               entity tele_ent = NULL;
+               if (this.goalstack01 && (this.goalstack01.wpflags & WAYPOINTFLAG_TELEPORT))
+                       tele_ent = this.goalstack01;
+               else if (this.goalstack02 && (this.goalstack02.wpflags & WAYPOINTFLAG_TELEPORT))
+                       tele_ent = this.goalstack02;
+               else if (this.goalstack03 && (this.goalstack03.wpflags & WAYPOINTFLAG_TELEPORT))
+                       tele_ent = this.goalstack03;
+               if (tele_ent && TELEPORT_USED(this, tele_ent))
                {
-                       LOG_DEBUG("path optimized for ", this.netname, ", removed a goal from the queue");
+                       if (this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING)
+                       if (tele_ent.wpflags & WAYPOINTFLAG_PERSONAL && tele_ent.owner == this)
+                       {
+                               this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING;
+                               this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED;
+                       }
+                       while (this.goalcurrent != tele_ent)
+                       {
+                               navigation_poproute(this);
+                               ++removed_goals;
+                       }
                        navigation_poproute(this);
+                       this.lastteleporttime = 0;
                        ++removed_goals;
-                       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
+                       return removed_goals;
                }
        }
 
@@ -1464,10 +1709,16 @@ int navigation_poptouchedgoals(entity this)
 entity navigation_get_really_close_waypoint(entity this)
 {
        entity wp = this.goalcurrent;
-       if(!wp || vdist(wp.origin - this.origin, >, 50))
+       if(!wp)
                wp = this.goalcurrent_prev;
        if(!wp)
                return NULL;
+       if(wp != this.goalcurrent_prev && vdist(wp.origin - this.origin, >, 50))
+       {
+               wp = this.goalcurrent_prev;
+               if(!wp)
+                       return NULL;
+       }
        if(wp.classname != "waypoint")
        {
                wp = wp.nearestwaypoint;
@@ -1476,6 +1727,7 @@ entity navigation_get_really_close_waypoint(entity this)
        }
        if(vdist(wp.origin - this.origin, >, 50))
        {
+               wp = NULL;
                IL_EACH(g_waypoints, !(it.wpflags & WAYPOINTFLAG_TELEPORT),
                {
                        if(vdist(it.origin - this.origin, <, 50))
@@ -1484,15 +1736,18 @@ entity navigation_get_really_close_waypoint(entity this)
                                break;
                        }
                });
+               if(!wp)
+                       return NULL;
        }
        if(wp.wpflags & WAYPOINTFLAG_TELEPORT)
                return NULL;
 
-       vector dest = '0 0 0';
-       float dest_height = 0;
-       SET_TRACEWALK_DESTCOORDS(wp, this.origin, dest, dest_height);
-       if (!tracewalk(this, this.origin, STAT(PL_MIN, this), STAT(PL_MAX, this), dest, dest_height, bot_navigation_movemode))
+       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;
 }
 
@@ -1528,6 +1783,7 @@ void navigation_goalrating_end(entity this)
                        this.aistatus |= AI_STATUS_STUCK;
                }
        }
+       this.goalentity_shouldbefrozen = boolean(STAT(FROZEN, this.goalentity));
 }
 
 void botframe_updatedangerousobjects(float maxupdate)
@@ -1566,11 +1822,20 @@ void botframe_updatedangerousobjects(float maxupdate)
 
 void navigation_unstuck(entity this)
 {
-       float search_radius = 1000;
-
        if (!autocvar_bot_wander_enable)
                return;
 
+       bool has_user_waypoints = false;
+       IL_EACH(g_waypoints, !(it.wpflags & WAYPOINTFLAG_GENERATED),
+       {
+               has_user_waypoints = true;
+               break;
+       });
+       if (!has_user_waypoints)
+               return;
+
+       float search_radius = 1000;
+
        if (!bot_waypoint_queue_owner)
        {
                LOG_DEBUG(this.netname, " stuck, taking over the waypoints queue");
@@ -1587,10 +1852,9 @@ void navigation_unstuck(entity this)
                // 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 = '0 0 0';
-               float dest_height = 0;
-               SET_TRACEWALK_DESTCOORDS(bot_waypoint_queue_goal, this.origin, dest, dest_height);
-               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(this, this.origin, STAT(PL_MIN, this), STAT(PL_MAX, this),
+                       tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))
                {
                        if( d > bot_waypoint_queue_bestgoalrating)
                        {