Merge branch 'master' into terencehill/bot_AI_improvements
authorterencehill <piuntn@gmail.com>
Thu, 9 Feb 2017 12:58:13 +0000 (13:58 +0100)
committerterencehill <piuntn@gmail.com>
Thu, 9 Feb 2017 12:58:13 +0000 (13:58 +0100)
bots.txt
qcsrc/common/items/item/armor.qh
qcsrc/common/items/item/health.qh
qcsrc/common/t_items.qc
qcsrc/common/t_items.qh
qcsrc/server/bot/default/bot.qc
qcsrc/server/bot/default/bot.qh
qcsrc/server/bot/default/havocbot/havocbot.qc
qcsrc/server/bot/default/havocbot/roles.qc
qcsrc/server/bot/default/navigation.qc

index 18df7f8..540f5c0 100644 (file)
--- a/bots.txt
+++ b/bots.txt
@@ -1,21 +1,22 @@
-//bot configuration: name      model   skin    shirt   pants   team    keyboard use    moving  dodging ping    weapon use      aggressivity    range   aiming  calmhand        mouse   fightthink      aithink
-//default team values (team-override): 1 = red, 2 = blue, 3 = yellow, 4 = pink                                                                                                                                 
-//use -1 for shirt-color or pants-color to get random colors                                                                                                                                   
-Hellfire       ignis   0       4       0       0       0       0       -0.5    -1      1       1       -0.5    -1      -1      2       0.5     -1
-Toxic  gakmasked       0       14      7       0       -1      -1.5    -0.5    0       1       0       0       0       2       -0.5    -0.5    1
-Discovery      erebus  0       2       6       0       0       -1      -0.5    -0.5    1       -0.5    0.5     1.5     -0.5    -1      1       0.5
-Pegasus        umbra   0       13      11      0       1       1       1       1       -1      0       0.5     0       -2      0       -1      0
-Eureka umbra   0       12      7       0       0       0       -1.5    -0.5    -0.5    0       0       0       0       -0.5    1.5     1.5
-Airhead        ignis   0       11      1       0       -1      -1.5    -1      -0.5    1       1       -1      1       -0.5    1       0.5     0
-Gator  gak     0       3       10      0       0       1       0       0.5     -0.5    0.5     -0.5    -1      0       0       -0.5    0
-Delirium       gakmasked       0       8       12      0       0       -1      -1      -1      0       2       0       1       0       2       -1      -1
-Death  gakmasked       0       4       11      0       -0.5    0       0       1       -0.5    0       1       0       0       0       0       0
-Scorcher       ignismasked     0       13      13      0       0       -1      0       -0.5    0.5     1       0       1       -2      1       0       0
-Necrotic       nyx     0       12      14      0       0       0       0       1       0       -1      -0.5    -1      1       0       0       0
-Dominator      nyx     0       3       9       0       0       0       0       2       -1      0       0       0       -1      0       0       0
-Thunderstorm   erebus  0       13      6       0       0       0       0       -0.5    -1      1       0.5     0.5     -0.5    0       0.5     0
-Mystery        pyria   0       9       14      0       1       1       1       1       -1      -1      0       1       1       -2      -1      -1
-Lion   ignismasked     0       0       4       0       1       1.5     2       -1      -1      -1      1       0       1       -0.5    -1      -1
-Sensible       seraphina       0       9       9       0       0       0       0.5     -1      0       -1      0       -1      2.5     -1.5    1       0.5
-Shadow seraphinamasked 0       4       8       0       -0.5    2       1       0       0       -1      0       -1      0       1       -1      -0.5
-Resurrection   umbra   0       1       1       0       0       -0.5    -0.5    0       2       -1      -1      -1      0       -1      1       1
+//bot configuration:
+// default team values (team-override): 1 = red, 2 = blue, 3 = yellow, 4 = pink
+// use -1 for shirt-color or pants-color to get random colors
+//name                 model           skin shirt pants team keyboard moving dodging ping weapon aggres range aiming calmhand mouse fightthink aithink
+Hellfire               ignis                           0       4       0               0       0               0               -0.5    -1              1               1               -0.5    -1              -1              2               0.5             -1
+Toxic                  gakmasked                       0       14      7               0       -1              -1.5    -0.5    0               1               0               0               0               2               -0.5    -0.5    1
+Discovery              erebus                          0       2       6               0       0               -1              -0.5    -0.5    1               -0.5    0.5             1.5             -0.5    -1              1               0.5
+Pegasus                        umbra                           0       13      11              0       1               1               1               1               -1              0               0.5             0               -2              0               -1              0
+Eureka                 umbra                           0       12      7               0       0               0               -1.5    -0.5    -0.5    0               0               0               0               -0.5    1.5             1.5
+Airhead                        ignis                           0       11      1               0       -1              -1.5    -1              -0.5    1               1               -1              1               -0.5    1               0.5             0
+Gator                  gak                                     0       3       10              0       0               1               0               0.5             -0.5    0.5             -0.5    -1              0               0               -0.5    0
+Delirium               gakmasked                       0       8       12              0       0               -1              -1              -1              0               2               0               1               0               2               -1              -1
+Death                  gakmasked                       0       4       11              0       -0.5    0               0               1               -0.5    0               1               0               0               0               0               0
+Scorcher               ignismasked                     0       13      13              0       0               -1              0               -0.5    0.5             1               0               1               -2              1               0               0
+Necrotic               nyx                                     0       12      14              0       0               0               0               1               0               -1              -0.5    -1              1               0               0               0
+Dominator              nyx                                     0       3       9               0       0               0               0               2               -1              0               0               0               -1              0               0               0
+Thunderstorm   erebus                          0       13      6               0       0               0               0               -0.5    -1              1               0.5             0.5             -0.5    0               0.5             0
+Mystery                        pyria                           0       9       14              0       1               1               1               1               -1              -1              0               1               1               -2              -1              -1
+Lion                   ignismasked                     0       0       4               0       1               1.5             2               -1              -1              -1              1               0               1               -0.5    -1              -1
+Sensible               seraphina                       0       9       9               0       0               0               0.5             -1              0               -1              0               -1              2.5             -1.5    1               0.5
+Shadow                 seraphinamasked         0       4       8               0       -0.5    2               1               0               0               -1              0               -1              0               1               -1              -0.5
+Resurrection   umbra                           0       1       1               0       0               -0.5    -0.5    0               2               -1              -1              -1              0               -1              1               1
index f153bf0..bc2aa65 100644 (file)
@@ -6,7 +6,7 @@ CLASS(Armor, Pickup)
     ATTRIB(Armor, m_mins, vector, '-16 -16 0');
     ATTRIB(Armor, m_maxs, vector, '16 16 48');
     ATTRIB(Armor, m_pickupevalfunc, float(entity player, entity item), healtharmor_pickupevalfunc);
-    ATTRIB(Armor, m_botvalue, int, 3000);
+    ATTRIB(Armor, m_botvalue, int, 5000);
 #endif
 ENDCLASS(Armor)
 
index 1fc544a..9366bc4 100644 (file)
@@ -6,7 +6,7 @@ CLASS(Health, Pickup)
     ATTRIB(Health, m_mins, vector, '-16 -16 0');
     ATTRIB(Health, m_maxs, vector, '16 16 48');
     ATTRIB(Health, m_pickupevalfunc, float(entity player, entity item), healtharmor_pickupevalfunc);
-    ATTRIB(Health, m_botvalue, int, 2500);
+    ATTRIB(Health, m_botvalue, int, 5000);
 #endif
 ENDCLASS(Health)
 
index 9aab742..6d4fa77 100644 (file)
@@ -957,7 +957,16 @@ float weapon_pickupevalfunc(entity player, entity item)
                        return 0;
                return ammo_pickupevalfunc(player, item);
        }
-       return item.bot_pickupbasevalue;
+
+       // reduce weapon value if bot already got a good arsenal
+       float c = 1;
+       int weapons_value = 0;
+       FOREACH(Weapons, it != WEP_Null && (player.weapons & it.m_wepset), {
+               weapons_value += it.bot_pickupbasevalue;
+       });
+       c -= bound(0, weapons_value / 20000, 1) * 0.5;
+
+       return item.bot_pickupbasevalue * c;
 }
 
 float ammo_pickupevalfunc(entity player, entity item)
@@ -1006,48 +1015,56 @@ float ammo_pickupevalfunc(entity player, entity item)
        if (need_shells)
        if (item.ammo_shells)
        if (player.ammo_shells < g_pickup_shells_max)
-               c = (player.ammo_shells + item.ammo_shells) / player.ammo_shells;
+               c = item.ammo_shells / player.ammo_shells;
        if (need_nails)
        if (item.ammo_nails)
        if (player.ammo_nails < g_pickup_nails_max)
-               c = (player.ammo_nails + item.ammo_nails) / player.ammo_nails;
+               c = item.ammo_nails / player.ammo_nails;
        if (need_rockets)
        if (item.ammo_rockets)
        if (player.ammo_rockets < g_pickup_rockets_max)
-               c = (player.ammo_rockets + item.ammo_rockets) / player.ammo_rockets;
+               c = item.ammo_rockets / player.ammo_rockets;
        if (need_cells)
        if (item.ammo_cells)
        if (player.ammo_cells < g_pickup_cells_max)
-               c = (player.ammo_cells + item.ammo_cells) / player.ammo_cells;
+               c = item.ammo_cells / player.ammo_cells;
        if (need_plasma)
        if (item.ammo_plasma)
        if (player.ammo_plasma < g_pickup_plasma_max)
-               c = (player.ammo_plasma + item.ammo_plasma) / player.ammo_plasma;
+               c = item.ammo_plasma / player.ammo_plasma;
        if (need_fuel)
        if (item.ammo_fuel)
        if (player.ammo_fuel < g_pickup_fuel_max)
-               c = (player.ammo_fuel + item.ammo_fuel) / player.ammo_fuel;
+               c = item.ammo_fuel / player.ammo_fuel;
 
-       rating *= min(3, c);
+       rating *= min(2, c);
        if(wpn)
-               // Skilled bots will grab more
-               rating += wpn.bot_pickupbasevalue * (0.1 + 0.1 * bound(0, skill / 10, 1));
+               rating += wpn.bot_pickupbasevalue * 0.1;
        return rating;
 }
 
+.int item_group;
+.int item_group_count;
 float healtharmor_pickupevalfunc(entity player, entity item)
 {
        float c = 0;
        float rating = item.bot_pickupbasevalue;
 
-       if (item.armorvalue)
+       float itemarmor = item.armorvalue;
+       float itemhealth = item.health;
+       if(item.item_group)
+       {
+               itemarmor *= min(4, item.item_group_count);
+               itemhealth *= min(4, item.item_group_count);
+       }
+       if (itemarmor)
        if (player.armorvalue < item.max_armorvalue)
-               c = (player.armorvalue + player.health + item.armorvalue) / (max(1, player.armorvalue + player.health));
-       if (item.health)
+               c = itemarmor / max(1, player.armorvalue * 2/3 + player.health * 1/3);
+       if (itemhealth)
        if (player.health < item.max_health)
-               c = (player.health + item.health) / (max(1, player.health));
+               c = itemhealth / max(1, player.health);
 
-       rating *= min(3, c);
+       rating *= min(2, c);
        return rating;
 }
 
@@ -1269,6 +1286,8 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
                delete(this);
                return;
        }
+
+       setItemGroup(this);
 }
 
 void StartItem(entity this, GameItem def)
@@ -1281,6 +1300,54 @@ void StartItem(entity this, GameItem def)
        );
 }
 
+#define IS_SMALL(def) ((def.instanceOfHealth && def == ITEM_HealthSmall) || (def.instanceOfArmor && def == ITEM_ArmorSmall))
+int group_count = 1;
+
+void setItemGroup(entity this)
+{
+       if(!IS_SMALL(this.itemdef))
+               return;
+
+       FOREACH_ENTITY_RADIUS(this.origin, 120, (it != this) && IS_SMALL(it.itemdef),
+       {
+               if(!this.item_group)
+               {
+                       if(!it.item_group)
+                       {
+                               it.item_group = group_count;
+                               group_count++;
+                       }
+                       this.item_group = it.item_group;
+               }
+               else // spawning item is already part of a item_group X
+               {
+                       if(!it.item_group)
+                               it.item_group = this.item_group;
+                       else if(it.item_group != this.item_group) // found an item near the spawning item that is part of a different item_group Y
+                       {
+                               int grY = it.item_group;
+                               // move all items of item_group Y to item_group X
+                               FOREACH_ENTITY(IS_SMALL(it.itemdef),
+                               {
+                                       if(it.item_group == grY)
+                                               it.item_group = this.item_group;
+                               });
+                       }
+               }
+       });
+}
+
+void setItemGroupCount()
+{
+       for (int k = 1; k <= group_count; k++)
+       {
+               int count = 0;
+               FOREACH_ENTITY(IS_SMALL(it.itemdef) && it.item_group == k, { count++; });
+               if (count)
+                       FOREACH_ENTITY(IS_SMALL(it.itemdef) && it.item_group == k, { it.item_group_count = count; });
+       }
+}
+
 spawnfunc(item_rockets)
 {
        if(!this.ammo_rockets)
index f5a3105..1b2293b 100644 (file)
@@ -114,6 +114,8 @@ float healtharmor_pickupevalfunc(entity player, entity item);
 .entity itemdef;
 void _StartItem(entity this, entity def, float defaultrespawntime, float defaultrespawntimejitter);
 
+void setItemGroup(entity this);
+void setItemGroupCount();
 
 float GiveWeapon(entity e, float wpn, float op, float val);
 
index b52efbb..3184221 100644 (file)
@@ -46,6 +46,7 @@ entity bot_spawn()
        entity bot = spawnclient();
        if (bot)
        {
+               setItemGroupCount();
                currentbots = currentbots + 1;
                bot_setnameandstuff(bot);
                ClientConnect(bot);
@@ -63,9 +64,8 @@ void bot_think(entity this)
        if(autocvar_bot_god)
                this.flags |= FL_GODMODE;
 
-       this.bot_nextthink = this.bot_nextthink + autocvar_bot_ai_thinkinterval * pow(0.5, this.bot_aiskill);
-       if(this.bot_nextthink < time)
-               this.bot_nextthink = time + autocvar_bot_ai_thinkinterval * pow(0.5, this.bot_aiskill);
+       this.bot_nextthink = max(time, this.bot_nextthink) + max(0.01, autocvar_bot_ai_thinkinterval * pow(0.5, this.bot_aiskill) * min(14 / (skill + 14), 1));
+
        //if (this.bot_painintensity > 0)
        //      this.bot_painintensity = this.bot_painintensity - (skill + 1) * 40 * frametime;
 
index f615c04..b72fad9 100644 (file)
@@ -62,6 +62,7 @@ entity bot_list;
 
 .float bot_pickup;
 .float bot_pickupbasevalue;
+.bool bot_pickup_respawning;
 .float bot_canfire;
 .float bot_strategytime;
 
index 7613000..fad7dbd 100644 (file)
@@ -290,7 +290,8 @@ void havocbot_bunnyhop(entity this, vector dir)
 
        maxspeed = autocvar_sv_maxspeed;
 
-       if(this.aistatus & AI_STATUS_DANGER_AHEAD)
+       if(this.aistatus & AI_STATUS_RUNNING && vdist(this.velocity, <, autocvar_sv_maxspeed * 0.75)
+               || this.aistatus & AI_STATUS_DANGER_AHEAD)
        {
                this.aistatus &= ~AI_STATUS_RUNNING;
                PHYS_INPUT_BUTTON_JUMP(this) = false;
@@ -316,7 +317,7 @@ void havocbot_bunnyhop(entity this, vector dir)
 
        // Run only to visible goals
        if(IS_ONGROUND(this))
-       if(this.speed==maxspeed)
+       if(vlen(this.velocity - eZ * this.velocity.z) >= autocvar_sv_maxspeed) // if -really- running
        if(checkpvs(this.origin + this.view_ofs, this.goalcurrent))
        {
                        this.bot_lastseengoal = this.goalcurrent;
@@ -428,7 +429,6 @@ void havocbot_movetogoal(entity this)
        vector m2;
        vector evadeobstacle;
        vector evadelava;
-       float s;
        float maxspeed;
        vector gco;
        //float dist;
@@ -707,6 +707,7 @@ void havocbot_movetogoal(entity this)
                evadeobstacle = '0 0 0';
                evadelava = '0 0 0';
 
+               makevectors(this.v_angle.y * '0 1 0');
                if (this.waterlevel)
                {
                        if(this.waterlevel>WATERLEVEL_SWIMMING)
@@ -723,33 +724,36 @@ void havocbot_movetogoal(entity this)
                                        PHYS_INPUT_BUTTON_JUMP(this) = false;
                        }
                        dir = normalize(flatdir);
-                       makevectors(this.v_angle.y * '0 1 0');
                }
                else
                {
+                       float s;
+                       vector offset;
                        if(this.aistatus & AI_STATUS_OUT_WATER)
                                this.aistatus &= ~AI_STATUS_OUT_WATER;
 
                        // jump if going toward an obstacle that doesn't look like stairs we
                        // can walk up directly
-                       tracebox(this.origin, this.mins, this.maxs, this.origin + this.velocity * 0.2, false, this);
+                       offset = (vdist(this.velocity, >, 32) ? this.velocity * 0.2 : v_forward * 32);
+                       tracebox(this.origin, this.mins, this.maxs, this.origin + offset, false, this);
                        if (trace_fraction < 1)
                        if (trace_plane_normal.z < 0.7)
                        {
                                s = trace_fraction;
-                               tracebox(this.origin + stepheightvec, this.mins, this.maxs, this.origin + this.velocity * 0.2 + stepheightvec, false, this);
+                               tracebox(this.origin + stepheightvec, this.mins, this.maxs, this.origin + offset + stepheightvec, false, this);
                                if (trace_fraction < s + 0.01)
                                if (trace_plane_normal.z < 0.7)
                                {
                                        s = trace_fraction;
-                                       tracebox(this.origin + jumpstepheightvec, this.mins, this.maxs, this.origin + this.velocity * 0.2 + jumpstepheightvec, false, this);
+                                       tracebox(this.origin + jumpstepheightvec, this.mins, this.maxs, this.origin + offset + jumpstepheightvec, false, this);
                                        if (trace_fraction > s)
                                                PHYS_INPUT_BUTTON_JUMP(this) = true;
                                }
                        }
 
                        // avoiding dangers and obstacles
-                       vector dst_ahead = this.origin + this.view_ofs + this.velocity * 0.5;
+                       offset = (vdist(this.velocity, >, 32) ? this.velocity * 0.5 : v_forward * 32);
+                       vector dst_ahead = this.origin + this.view_ofs + offset;
                        vector dst_down = dst_ahead - '0 0 3000';
 
                        // Look ahead
@@ -786,6 +790,8 @@ void havocbot_movetogoal(entity this)
                        // (only when the bot is on the ground or jumping intentionally)
                        this.aistatus &= ~AI_STATUS_DANGER_AHEAD;
 
+                       bool unreachable = false;
+                       s = CONTENT_SOLID;
                        if(trace_fraction == 1 && this.jumppadcount == 0 && !this.goalcurrent.wphardwired )
                        if((IS_ONGROUND(this)) || (this.aistatus & AI_STATUS_RUNNING) || (this.aistatus & AI_STATUS_ROAMING) || PHYS_INPUT_BUTTON_JUMP(this))
                        {
@@ -809,11 +815,9 @@ void havocbot_movetogoal(entity this)
                                                if (tracebox_hits_trigger_hurt(dst_ahead, this.mins, this.maxs, trace_endpos))
                                                {
                                                        if (gco.z > this.origin.z + jumpstepheightvec.z)
-                                                       { 
+                                                       {
                                                                // the goal is probably on an upper platform, assume bot can't get there
-                                                               LOG_TRACE("bot ", this.netname, " avoided the goal ", this.goalcurrent.classname, " ", etos(this.goalcurrent), " because it led to a dangerous path; goal stack cleared");
-                                                               navigation_clearroute(this);
-                                                               this.bot_strategytime = 0;
+                                                               unreachable = true;
                                                        }
                                                        else
                                                                evadelava = normalize(this.velocity) * -1;
@@ -827,8 +831,17 @@ void havocbot_movetogoal(entity this)
                        evadelava.z = 0;
                        makevectors(this.v_angle.y * '0 1 0');
 
-                       if(evadeobstacle!='0 0 0'||evadelava!='0 0 0')
+                       if(evadeobstacle || evadelava || (s == CONTENT_WATER))
+                       {
                                this.aistatus |= AI_STATUS_DANGER_AHEAD;
+                               if(IS_PLAYER(this.goalcurrent))
+                                       unreachable = true;
+                       }
+                       if(unreachable)
+                       {
+                               navigation_clearroute(this);
+                               this.bot_strategytime = 0;
+                       }
                }
 
                dodge = havocbot_dodge(this);
index f57a18f..27710bf 100644 (file)
@@ -23,8 +23,26 @@ void havocbot_goalrating_items(entity this, float ratingscale, vector org, float
        {
                rating = 0;
 
+               if(!it.solid)
+               {
+                       if(!it.scheduledrespawntime)
+                               continue;
+                       if(it.respawntime < 30 || (it.respawntimejitter && !it.itemdef.instanceOfPowerup))
+                               continue;
+
+                       float t = 0;
+                       if(it.itemdef.instanceOfPowerup)
+                               t = bound(0, skill / 10, 1) * 6;
+                       else if(skill >= 9)
+                               t = 4;
+
+                       if(time < it.scheduledrespawntime - t)
+                               continue;
+
+                       it.bot_pickup_respawning = true;
+               }
                o = (it.absmin + it.absmax) * 0.5;
-               if(!it.solid || vdist(o - org, >, sradius) || (it == this.ignoregoal && time < this.ignoregoaltime) )
+               if(vdist(o - org, >, sradius) || (it == this.ignoregoal && time < this.ignoregoaltime) )
                        continue;
 
                // Check if the item can be picked up safely
@@ -105,6 +123,7 @@ void havocbot_goalrating_items(entity this, float ratingscale, vector org, float
        });
 }
 
+#define BOT_RATING_ENEMY 2500
 void havocbot_goalrating_enemyplayers(entity this, float ratingscale, vector org, float sradius)
 {
        if (autocvar_bot_nofire)
@@ -114,8 +133,9 @@ void havocbot_goalrating_enemyplayers(entity this, float ratingscale, vector org
        if(this.waterlevel>WATERLEVEL_WETFEET)
                return;
 
-       int t;
+       ratingscale = ratingscale * 0.00005; // enemies are rated around 20000 already
 
+       float t;
        FOREACH_CLIENT(IS_PLAYER(it) && bot_shouldattack(this, it), LAMBDA(
                // TODO: Merge this logic with the bot_shouldattack function
                if(vdist(it.origin - org, <, 100) || vdist(it.origin - org, >, sradius))
@@ -128,27 +148,17 @@ void havocbot_goalrating_enemyplayers(entity this, float ratingscale, vector org
                        continue;
                */
 
-               if((it.flags & FL_INWATER) || (it.flags & FL_PARTIALGROUND))
-                       continue;
-
-               // not falling
-               if(!IS_ONGROUND(it))
+               t = ((this.health + this.armorvalue) - (it.health + it.armorvalue)) / 150;
+               t = bound(0, 1 + t, 3);
+               if (skill > 3)
                {
-                       traceline(it.origin, it.origin + '0 0 -1500', true, NULL);
-                       t = pointcontents(trace_endpos + '0 0 1');
-                       if(t != CONTENT_SOLID )
-                       if(t == CONTENT_WATER || t == CONTENT_SLIME || t == CONTENT_LAVA)
-                               continue;
-                       if(tracebox_hits_trigger_hurt(it.origin, it.mins, it.maxs, trace_endpos))
-                               continue;
+                       if (time < this.strength_finished - 1) t += 0.5;
+                       if (time < it.strength_finished - 1) t -= 0.5;
                }
-
-               // TODO: rate waypoints near the targeted player at that moment, instead of the player itself
-               //               adding a player as a goal seems to be quite dangerous, especially on space maps
-               //               remove hack in navigation_poptouchedgoals() after performing this change
-
-               t = (this.health + this.armorvalue ) / (it.health + it.armorvalue );
-               navigation_routerating(this, it, t * ratingscale, 2000);
+               t += max(0, 8 - skill) * 0.05; // less skilled bots attack more mindlessly
+               ratingscale *= t;
+               if (ratingscale > 0)
+                       navigation_routerating(this, it, ratingscale * BOT_RATING_ENEMY, 2000);
        ));
 }
 
index 4c8982e..742213a 100644 (file)
@@ -643,15 +643,52 @@ void navigation_markroutes_inverted(entity fixed_source_waypoint)
 // updates the best goal according to a weighted calculation of travel cost and item value of a new proposed item
 void navigation_routerating(entity this, entity e, float f, float rangebias)
 {
-       entity nwp;
-       vector o;
        if (!e)
                return;
 
        if(e.blacklisted)
                return;
 
-       o = (e.absmin + e.absmax) * 0.5;
+       if (IS_PLAYER(e))
+       {
+               bool rate_wps = false;
+               if((e.flags & FL_INWATER) || (e.flags & FL_PARTIALGROUND))
+                       rate_wps = true;
+
+               if(!IS_ONGROUND(e))
+               {
+                       traceline(e.origin, e.origin + '0 0 -1500', true, NULL);
+                       int t = pointcontents(trace_endpos + '0 0 1');
+                       if(t != CONTENT_SOLID )
+                       {
+                               if(t == CONTENT_WATER || t == CONTENT_SLIME || t == CONTENT_LAVA)
+                                       rate_wps = true;
+                               else if(tracebox_hits_trigger_hurt(e.origin, e.mins, e.maxs, trace_endpos))
+                                       return;
+                       }
+               }
+
+               if(rate_wps)
+               {
+                       entity theEnemy = e;
+                       entity best_wp = NULL;
+                       float best_dist = 10000;
+                       IL_EACH(g_waypoints, vdist(it.origin - theEnemy.origin, <, 500) && vdist(it.origin - this.origin, >, 100),
+                       {
+                               float dist = vlen(it.origin - theEnemy.origin);
+                               if (dist < best_dist)
+                               {
+                                       best_wp = it;
+                                       best_dist = dist;
+                               }
+                       });
+                       if (!best_wp)
+                               return;
+                       e = best_wp;
+               }
+       }
+
+       vector o = (e.absmin + e.absmax) * 0.5;
 
        //print("routerating ", etos(e), " = ", ftos(f), " - ", ftos(rangebias), "\n");
 
@@ -744,6 +781,7 @@ void navigation_routerating(entity this, entity e, float f, float rangebias)
                }
        }
 
+       entity nwp;
        //te_wizspike(e.origin);
        //bprint(etos(e));
        //bprint("\n");
@@ -897,6 +935,13 @@ void navigation_poptouchedgoals(entity this)
                }
        }
 
+       if(this.goalcurrent.bot_pickup_respawning)
+       {
+               if(!this.goalcurrent.solid)
+                       return;
+               this.goalcurrent.bot_pickup_respawning = false;
+       }
+
        // 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))
@@ -913,16 +958,11 @@ void navigation_poptouchedgoals(entity this)
                // personality property
        }
 
-       // HACK: remove players/bots as goals, they can lead a bot to unexpected places (cliffs, lava, etc)
-       // TODO: rate waypoints near the targetted player at that moment, instead of the player itself
-       if(IS_PLAYER(this.goalcurrent))
-               navigation_poproute(this);
-
        // Loose goal touching check when running
        if(this.aistatus & AI_STATUS_RUNNING)
-       if(this.speed >= autocvar_sv_maxspeed) // if -really- running
        if(this.goalcurrent.classname=="waypoint")
        if(!(this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT))
+       if(vlen(this.velocity - eZ * this.velocity.z) >= autocvar_sv_maxspeed) // if -really- running
        {
                if(vdist(this.origin - this.goalcurrent.origin, <, 150))
                {
@@ -942,7 +982,7 @@ void navigation_poptouchedgoals(entity this)
                }
        }
 
-       while (this.goalcurrent && boxesoverlap(m1, m2, this.goalcurrent.absmin, this.goalcurrent.absmax))
+       while (this.goalcurrent && !IS_PLAYER(this.goalcurrent) && boxesoverlap(m1, m2, this.goalcurrent.absmin, this.goalcurrent.absmax))
        {
                if((this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT))
                        break;