]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge remote-tracking branch 'origin/terencehill/bot_waypoints' into terencehill...
authorterencehill <piuntn@gmail.com>
Sat, 2 Dec 2017 21:36:00 +0000 (22:36 +0100)
committerterencehill <piuntn@gmail.com>
Sat, 2 Dec 2017 21:36:00 +0000 (22:36 +0100)
# Conflicts:
# qcsrc/server/bot/default/navigation.qh

1  2 
defaultServer.cfg
qcsrc/common/triggers/trigger/jumppads.qc
qcsrc/server/autocvars.qh
qcsrc/server/bot/default/havocbot/roles.qc
qcsrc/server/bot/default/navigation.qc
qcsrc/server/mutators/mutator/gamemode_cts.qc

diff --combined defaultServer.cfg
index f3f2a81bf10c766d7a8b8cc31b774cae72797b88,99845f6480f256dd2eaf79057b48ce76997b5a5b..61aab3a4ec54ba5e2f65479be12e53fb1a14ff07
@@@ -77,6 -77,7 +77,7 @@@ set sv_jumpspeedcap_min "" "lower boun
  set sv_jumpspeedcap_max "" "upper bound on the baseline velocity of a jump; final velocity will be <= (jumpheight * max + jumpheight)"
  set sv_jumpspeedcap_max_disable_on_ramps 0 "disable upper baseline velocity bound on ramps to preserve the old rampjump style"
  set sv_track_canjump 0 "track if the player released the jump key between 2 jumps to decide if they are able to jump or not"
+ set sv_jumpvelocity_crouch 0 "jump height while crouching, set to 0 to use regular jump height"
  
  set sv_precacheplayermodels 1
  set sv_precacheweapons 0
@@@ -115,8 -116,7 +116,8 @@@ set bot_debug_goalstack 0 "Visualize th
  set bot_wander_enable 1 "Have bots wander around if they are unable to reach any useful goal. Disable only for debugging purposes."
  // general bot AI cvars
  set bot_ai_thinkinterval 0.05
 -set bot_ai_strategyinterval 5 "How often a new objective is chosen"
 +set bot_ai_strategyinterval 7 "How often a new objective is chosen"
 +set bot_ai_strategyinterval_movingtarget 5.5 "How often a new objective is chosen when current objective can move"
  set bot_ai_enemydetectioninterval 2 "How often bots pick a new target"
  set bot_ai_enemydetectionradius 10000 "How far bots can see enemies"
  set bot_ai_dodgeupdateinterval 0.2 "How often scan for items to dodge. Currently not in use."
index c9729f52226503d0e0fe3d03a48ddeb05abb4272,999a615cb7c9ac9b835cc275482720e06cbd76ac..ded21c377e291b3584dbb77be51abf8c707d478a
@@@ -35,9 -35,9 +35,9 @@@ vector trigger_push_calculatevelocity(v
  
        torg = tgt.origin + (tgt.mins + tgt.maxs) * 0.5;
  
-       grav = PHYS_GRAVITY(other);
-       if(PHYS_ENTGRAVITY(other))
-               grav *= PHYS_ENTGRAVITY(other);
+       grav = PHYS_GRAVITY(tgt);
+       if(PHYS_ENTGRAVITY(tgt))
+               grav *= PHYS_ENTGRAVITY(tgt);
  
        zdist = torg.z - org.z;
        sdist = vlen(torg - org - zdist * '0 0 1');
@@@ -212,7 -212,7 +212,7 @@@ bool jumppad_push(entity this, entity t
                                animdecide_setaction(targ, ANIMACTION_JUMP, true);
                }
                else
 -                      targ.jumppadcount = true;
 +                      targ.jumppadcount = 1;
  
                // reset tracking of who pushed you into a hazard (for kill credit)
                targ.pushltime = 0;
@@@ -302,10 -302,7 +302,10 @@@ bool trigger_push_testorigin(entity tra
        return true;
  }
  #endif
 -void trigger_push_findtarget(entity this)
 +
 +/// if (item != NULL) returns true if the item can be reached by using this jumppad, false otherwise
 +/// if (item == NULL) tests jumppad's trajectory and eventually spawns waypoints for it (return value doesn't matter)
 +bool trigger_push_test(entity this, entity item)
  {
        // first calculate a typical start point for the jump
        vector org = (this.absmin + this.absmax) * 0.5;
                        setsize(e, PL_MIN_CONST, PL_MAX_CONST);
                        e.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
                        e.velocity = trigger_push_calculatevelocity(org, t, this.height);
 +
 +                      if(item)
 +                      {
 +                              setorigin(e, org);
 +                              tracetoss(e, e);
 +                              bool r = (trace_ent == item);
 +                              delete(e);
 +                              return r;
 +                      }
 +
                        vel = e.velocity;
                        vector best_target = '0 0 0';
                        vector best_org = '0 0 0';
  #endif
                }
  
 +              if(item)
 +                      return false;
 +
                if(!n)
                {
                        // no dest!
  #ifdef SVQC
                        objerror (this, "Jumppad with nonexistant target");
  #endif
 -                      return;
 +                      return false;
                }
                else if(n == 1)
                {
                setorigin(e, org);
                e.velocity = this.movedir;
                tracetoss(e, e);
 +              if(item)
 +              {
 +                      bool r = (trace_ent == item);
 +                      delete(e);
 +                      return r;
 +              }
                if (!(boxesoverlap(this.absmin, this.absmax + eZ * 50, trace_endpos + PL_MIN_CONST, trace_endpos + PL_MAX_CONST)))
                        waypoint_spawnforteleporter(this, trace_endpos, vlen(trace_endpos - org) / vlen(e.velocity), e);
                delete(e);
  
        defer(this, 0.1, trigger_push_updatelink);
  #endif
 +      return true;
 +}
 +
 +void trigger_push_findtarget(entity this)
 +{
 +      trigger_push_test(this, NULL);
  }
  
  #ifdef SVQC
@@@ -516,8 -488,6 +516,8 @@@ spawnfunc(trigger_push
  
        trigger_push_link(this); // link it now
  
 +      IL_PUSH(g_jumppads, this);
 +
        // this must be called to spawn the teleport waypoints for bots
        InitializeEntity(this, trigger_push_findtarget, INITPRIO_FINDTARGET);
  }
index dea758e481e9e3f6adc19a08836517ab70a1adaa,b71d4459d1387469f9c1732fa906dda15617f965..4b3745dc00784589e9c288854c3b6f948bfbfbd8
@@@ -6,7 -6,6 +6,7 @@@ int autocvar__campaign_index
  string autocvar__campaign_name;
  bool autocvar__sv_init;
  float autocvar_bot_ai_strategyinterval;
 +float autocvar_bot_ai_strategyinterval_movingtarget;
  #define autocvar_bot_number cvar("bot_number")
  int autocvar_bot_vs_human;
  int autocvar_captureleadlimit_override;
@@@ -312,6 -311,7 +312,7 @@@ string autocvar_sv_jumpspeedcap_max
  float autocvar_sv_jumpspeedcap_max_disable_on_ramps;
  string autocvar_sv_jumpspeedcap_min;
  float autocvar_sv_jumpvelocity;
+ float autocvar_sv_jumpvelocity_crouch;
  bool autocvar_sv_logscores_bots;
  bool autocvar_sv_logscores_console;
  bool autocvar_sv_logscores_file;
index 56133862b420ec2fe4b0dadf3c37782b6c1ee176,6d0b3c6a1cae803294d4adb588868ab6da8dba94..e469436014e1e030cc7dd1fc3bad16e61ad9fb11
@@@ -2,6 -2,7 +2,7 @@@
  
  #include <server/defs.qh>
  #include <server/miscfunctions.qh>
+ #include <server/items.qh>
  #include "havocbot.qh"
  
  #include "../cvars.qh"
@@@ -9,8 -10,6 +10,8 @@@
  #include "../bot.qh"
  #include "../navigation.qh"
  
 +.float bot_ratingscale;
 +.float bot_ratingscale_time;
  .float max_armorvalue;
  .float havocbot_role_timeout;
  
@@@ -46,81 -45,15 +47,81 @@@ void havocbot_goalrating_waypoints(enti
        }
  };
  
 +bool havocbot_goalrating_item_can_be_left_to_teammate(entity this, entity player, entity item)
 +{
 +      if (item.health && player.health <= this.health) {return true;}
 +      if (item.armorvalue && player.armorvalue <= this.armorvalue) {return true;}
 +      if (item.weapons && !(player.weapons & item.weapons)) {return true;}
 +      if (item.ammo_shells && player.ammo_shells <= this.ammo_shells) {return true;}
 +      if (item.ammo_nails && player.ammo_nails <= this.ammo_nails) {return true;}
 +      if (item.ammo_rockets && player.ammo_rockets <= this.ammo_rockets) {return true;}
 +      if (item.ammo_cells && player.ammo_cells <= this.ammo_cells) {return true;}
 +      if (item.ammo_plasma && player.ammo_plasma <= this.ammo_plasma) {return true;}
 +      if (item.itemdef.instanceOfPowerup) {return true;}
 +
 +      return false;
 +};
 +
 +bool havocbot_goalrating_item_pickable_check_players(entity this, vector org, entity item, vector item_org)
 +{
 +      if(!teamplay)
 +              return true;
 +
 +      // actually these variables hold the squared distances in order to optimize code
 +      float friend_distance = FLOAT_MAX;
 +      float enemy_distance = FLOAT_MAX;
 +      float dist;
 +
 +      FOREACH_CLIENT(IS_PLAYER(it) && it != this && !IS_DEAD(it),
 +      {
 +              if (it.team == this.team)
 +              {
 +                      if (!IS_REAL_CLIENT(it))
 +                              continue;
 +
 +                      dist = vlen2(it.origin - item_org);
 +                      if(dist > friend_distance)
 +                              continue;
 +
 +                      if(havocbot_goalrating_item_can_be_left_to_teammate(this, it, item))
 +                      {
 +                              friend_distance = dist;
 +                              continue;
 +                      }
 +              }
 +              else
 +              {
 +                      // If enemy only track distances
 +                      // TODO: track only if visible ?
 +                      dist = vlen2(it.origin - item_org);
 +                      if(dist < enemy_distance)
 +                              enemy_distance = dist;
 +              }
 +      });
 +
 +      // Rate the item only if no one needs it, or if an enemy is closer to it
 +      dist = vlen2(item_org - org);
 +      if ((enemy_distance < friend_distance && dist < enemy_distance) ||
 +              (friend_distance > autocvar_bot_ai_friends_aware_pickup_radius ** 2) ||
 +              (dist < friend_distance && dist < 200 ** 2))
 +              return true;
 +      return false;
 +};
 +
  void havocbot_goalrating_items(entity this, float ratingscale, vector org, float sradius)
  {
 -      float rating, discard, friend_distance, enemy_distance;
 +      float rating;
        vector o;
        ratingscale = ratingscale * 0.0001; // items are rated around 10000 already
  
        IL_EACH(g_items, it.bot_pickup,
        {
 -              rating = 0;
 +              // ignore if bot already rated this item with a higher ratingscale
 +              // NOTE: this code assumes each bot rates items in a different frame
 +              if(it.bot_ratingscale_time == time && ratingscale < it.bot_ratingscale)
 +                      continue;
 +              it.bot_ratingscale_time = time;
 +              it.bot_ratingscale = ratingscale;
  
                if(!it.solid)
                {
                        continue;
  
                // Check if the item can be picked up safely
-               if(it.classname == "droppedweapon")
+               if(Item_IsLoot(it))
                {
                        if(!IS_ONGROUND(it))
                                continue;
                                continue;
                }
  
 -              if(teamplay)
 -              {
 -                      friend_distance = 10000; enemy_distance = 10000;
 -                      discard = false;
 -
 -                      entity picker = it;
 -                      FOREACH_CLIENT(IS_PLAYER(it) && it != this && !IS_DEAD(it),
 -                      {
 -                              if ( it.team == this.team )
 -                              {
 -                                      if ( !IS_REAL_CLIENT(it) || discard )
 -                                              continue;
 -
 -                                      if( vdist(it.origin - o, >, friend_distance) )
 -                                              continue;
 -
 -                                      friend_distance = vlen(it.origin - o); // distance between player and item
 -                                      discard = true;
 -
 -                                      if (picker.health && it.health > this.health) continue;
 -                                      if (picker.armorvalue && it.armorvalue > this.armorvalue) continue;
 -
 -                                      if (picker.weapons && (picker.weapons & ~it.weapons)) continue;
 -
 -                                      if (picker.ammo_shells && it.ammo_shells > this.ammo_shells) continue;
 -                                      if (picker.ammo_nails && it.ammo_nails > this.ammo_nails) continue;
 -                                      if (picker.ammo_rockets && it.ammo_rockets > this.ammo_rockets) continue;
 -                                      if (picker.ammo_cells && it.ammo_cells > this.ammo_cells) continue;
 -                                      if (picker.ammo_plasma && it.ammo_plasma > this.ammo_plasma) continue;
 -
 -                                      discard = false;
 -                              }
 -                              else
 -                              {
 -                                      // If enemy only track distances
 -                                      // TODO: track only if visible ?
 -                                      if( vdist(it.origin - o, <, enemy_distance) )
 -                                              enemy_distance = vlen(it.origin - o); // distance between player and item
 -                              }
 -                      });
 -
 -                      // Rate the item only if no one needs it, or if an enemy is closer to it
 -                      if ( (enemy_distance < friend_distance && vdist(o - org, <, enemy_distance)) ||
 -                              (friend_distance > autocvar_bot_ai_friends_aware_pickup_radius ) || !discard )
 -                              rating = it.bot_pickupevalfunc(this, it);
 -              }
 -              else
 -                      rating = it.bot_pickupevalfunc(this, it);
 +              if(!havocbot_goalrating_item_pickable_check_players(this, org, it, o))
 +                      continue;
  
 +              rating = it.bot_pickupevalfunc(this, it);
                if(rating > 0)
                        navigation_routerating(this, it, rating * ratingscale, 2000);
        });
@@@ -196,8 -174,6 +197,8 @@@ void havocbot_goalrating_enemyplayers(e
                // TODO: Merge this logic with the bot_shouldattack function
                if(vdist(it.origin - org, <, 100) || vdist(it.origin - org, >, sradius))
                        continue;
 +              if(vdist(vec2(it.velocity), >, autocvar_sv_maxspeed * 2))
 +                      continue;
  
                // rate only visible enemies
                /*
                {
                        if (time < this.strength_finished - 1) t += 0.5;
                        if (time < it.strength_finished - 1) t -= 0.5;
 +                      if (time < this.invincible_finished - 1) t += 0.2;
 +                      if (time < it.invincible_finished - 1) t -= 0.4;
                }
                t += max(0, 8 - skill) * 0.05; // less skilled bots attack more mindlessly
                ratingscale *= t;
@@@ -229,15 -203,17 +230,15 @@@ void havocbot_role_generic(entity this
        if(IS_DEAD(this))
                return;
  
 -      if (this.bot_strategytime < time)
 +      if (navigation_goalrating_timeout(this))
        {
 -              this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
                navigation_goalrating_start(this);
                havocbot_goalrating_items(this, 10000, this.origin, 10000);
                havocbot_goalrating_enemyplayers(this, 20000, this.origin, 10000);
                havocbot_goalrating_waypoints(this, 1, this.origin, 3000);
                navigation_goalrating_end(this);
  
 -              if(IS_PLAYER(this.goalentity))
 -                      this.bot_strategytime = time + min(2, autocvar_bot_ai_strategyinterval);
 +              navigation_goalrating_timeout_set(this);
        }
  }
  
@@@ -249,7 -225,7 +250,7 @@@ void havocbot_chooserole_generic(entit
  void havocbot_chooserole(entity this)
  {
        LOG_TRACE("choosing a role...");
 -      this.bot_strategytime = 0;
 +      navigation_goalrating_timeout_force(this);
        if(!MUTATOR_CALLHOOK(HavocBot_ChooseRole, this))
                havocbot_chooserole_generic(this);
  }
index e28509618a821a18949cf758ef0b3d2b70712014,86f2feebdf718b4c36e4fcb0d578ebb8279fe4f9..356c04b17ba6c4ef30230f0e82ba35997419c378
  
  .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;
 +}
 +
 +#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))
 +              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;
@@@ -78,8 -30,6 +78,8 @@@
  void navigation_dynamicgoal_set(entity this)
  {
        this.nearestwaypointtimeout = time;
 +      if (this.nearestwaypoint)
 +              this.nearestwaypointtimeout += 2;
  }
  
  void navigation_dynamicgoal_unset(entity this)
        this.nearestwaypointtimeout = -1;
  }
  
-               vector wm1 = ent.origin + ent.mins - eZ * (PL_MAX_CONST.z - 1);
-               vector wm2 = ent.origin + ent.maxs - eZ * (PL_MIN_CONST.z + 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);
++              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 - eZ * (PL_MAX_CONST.z - 1);
-               vector wm2 = ent.origin + ent.maxs - eZ * (PL_MIN_CONST.z + 1);
++              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;
@@@ -727,10 -586,9 +727,10 @@@ bool tracewalk(entity e, vector start, 
  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;
@@@ -902,14 -755,7 +902,14 @@@ entity navigation_findnearestwaypoint_w
        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;
                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))
        {
                        if(walkfromwp && (it.wpflags & WAYPOINTFLAG_NORELINK))
                                continue;
  
 -                      SET_TRACEWALK_DESTCOORDS(it, org, v2, v2_height);
 -                      if(vdist(v2 - org, <, 1050))
 -                      if(tracewalk(ent, org, 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);
 +                      }
                });
        }
  
                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)
  // 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;
@@@ -1361,11 -1177,29 +1361,11 @@@ void navigation_routerating(entity this
                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_gettravelcost(nwp.wpnearestpoint, goal_org, nwp, e);
 +              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), ")");
@@@ -1446,13 -1276,11 +1446,13 @@@ bool navigation_routetogoal(entity this
                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
        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 || autocvar_g_waypointeditor)
 +                      if (nearest_wp.enemy.wpcost < autocvar_bot_ai_strategyinterval_movingtarget
 +                              && vdist(vec2(this.goalentity.origin - nearest_wp.origin), >, 16))
                        {
 -                              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))
 +                              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;
                }
 +              else if(navigation_item_islinked(nearest_wp.enemy, this.goalentity))
 +                      e = nearest_wp.enemy;
        }
  
        for (;;)
        return false;
  }
  
 +// shorten path by removing intermediate goals
 +void navigation_shortenpath(entity this)
 +{
 +      if (!this.goalstack01 || wasfreed(this.goalstack01))
 +              return;
 +      if (this.bot_tracewalk_time > time)
 +              return;
 +      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;
 +              }
 +      }
 +
 +      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);
 +              }
 +      }
 +}
 +
  // 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;
 -      }
 -
 -      // 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))
 -      {
 -              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))
 -              {
 -                      LOG_DEBUG("path optimized for ", this.netname, ", removed a goal from the queue");
 -                      navigation_poproute(this);
 -                      if(this.goalcurrent && this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT)
 -                              return;
 -                      // 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;
        }
  
        // Loose goal touching check when running
                                }
  
                                navigation_poproute(this);
 +                              ++removed_goals;
                                if(this.goalcurrent && this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT)
 -                                      return;
 +                                      return removed_goals;
                        }
                }
        }
                }
  
                navigation_poproute(this);
 +              ++removed_goals;
                if(this.goalcurrent && this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT)
 -                      return;
 +                      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)
@@@ -1703,10 -1434,9 +1703,10 @@@ void navigation_goalrating_start(entit
  
        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)
@@@ -1786,9 -1516,10 +1786,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 = (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
index b942447954d69f79280c0c2637cc57ec7990f85f,ca892c52ff538c59f2205bf25d1090ed48ddf1bc..ba2405b9db3837b67df1350876ccb5565b489f7d
@@@ -1,7 -1,7 +1,7 @@@
  #include "gamemode_cts.qh"
- #include <server/race.qh>
  
  #include <server/race.qh>
+ #include <server/items.qh>
  
  float autocvar_g_cts_finish_kill_delay;
  bool autocvar_g_cts_selfdamage;
@@@ -13,8 -13,9 +13,8 @@@ void havocbot_role_cts(entity this
        if(IS_DEAD(this))
                return;
  
 -      if (this.bot_strategytime < time)
 +      if (navigation_goalrating_timeout(this))
        {
 -              this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
                navigation_goalrating_start(this);
  
                IL_EACH(g_racecheckpoints, true,
@@@ -26,8 -27,6 +26,8 @@@
                });
  
                navigation_goalrating_end(this);
 +
 +              navigation_goalrating_timeout_set(this);
        }
  }
  
@@@ -315,8 -314,10 +315,10 @@@ MUTATOR_HOOKFUNCTION(cts, FilterItem
  {
        entity item = M_ARGV(0, entity);
  
-       if(item.classname == "droppedweapon")
+       if (Item_IsLoot(item))
+       {
                return true;
+       }
  }
  
  MUTATOR_HOOKFUNCTION(cts, Damage_Calculate)