]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Add a new bot skill level for extremely tough bots
authorDr. Jaska <drjaska83@gmail.com>
Sat, 27 May 2023 18:21:00 +0000 (18:21 +0000)
committerDr. Jaska <drjaska83@gmail.com>
Sat, 27 May 2023 18:21:00 +0000 (18:21 +0000)
.gitlab-ci.yml
qcsrc/server/bot/default/aim.qc
qcsrc/server/bot/default/bot.qc
qcsrc/server/bot/default/bot.qh
qcsrc/server/bot/default/havocbot/havocbot.qc
qcsrc/server/bot/default/havocbot/havocbot.qh

index b56f2b44d1b4b126e5b11f3be048aa75e21feaca..1feae253cbcd4231b69ae551e0466917e3ee043e 100644 (file)
@@ -75,7 +75,7 @@ test_sv_game:
     - wget -nv -O data/maps/stormkeep.waypoints https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.waypoints\r
     - wget -nv -O data/maps/stormkeep.waypoints.cache https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.waypoints.cache\r
 \r
-    - EXPECT=f2226bf353b6ff3dd3c489a742ce4e6a\r
+    - EXPECT=20c9c4ea0364fbb611656cc0b876e02b\r
     - HASH=$(${ENGINE} +exec serverbench.cfg\r
       | tee /dev/stderr\r
       | grep '^:'\r
index 1d0f78b74615647ecc501160b7f35ab215aeceef..c058c06f55c5a5df7f8b167d8766fa9adc27092c 100644 (file)
@@ -161,6 +161,22 @@ void bot_aimdir(entity this, vector v, float maxfiredeviation)
        if (this.bot_prevaimtime == time)
                return;
 
+       // if skill is high enough bots will not have any aim smoothing or aim errors
+       if (SUPERBOT)
+       {
+               this.v_angle = vectoangles(normalize(v));
+
+               this.v_angle.x *= -1;
+
+               makevectors(this.v_angle);
+               shotorg = this.origin + this.view_ofs;
+               shotdir = v_forward;
+
+               // bot will fire on the next tick
+               this.bot_firetimer = time + 0.001;
+               return;
+       }
+
        // invalid aim dir (can happen when bot overlaps target)
        if(!v) return;
 
index 5bfa79aa55edbb3ace3986e6fe60c8721d259329..2a663a10dc7e02b377704e7540ab6e730e32d526 100644 (file)
@@ -65,7 +65,11 @@ void bot_think(entity this)
        if(autocvar_bot_god)
                this.flags |= FL_GODMODE;
 
-       this.bot_nextthink = max(time, this.bot_nextthink) + max(0.01, autocvar_bot_ai_thinkinterval * min(14 / (skill + this.bot_aiskill + 14), 1));
+       // if bot skill is high enough don't limit their think frequency
+       if (SUPERBOT)
+               this.bot_nextthink = max(time, this.bot_nextthink) + 0.005;
+       else
+               this.bot_nextthink = max(time, this.bot_nextthink) + max(0.01, autocvar_bot_ai_thinkinterval * min(14 / (skill + this.bot_aiskill + 14), 1));
 
        if (!IS_PLAYER(this) || (autocvar_g_campaign && !campaign_bots_may_start))
        {
@@ -91,13 +95,19 @@ void bot_think(entity this)
        this.dmg_save = 0;
        this.dmg_inflictor = NULL;
 
-       // calculate an aiming latency based on the skill setting
-       // (simulated network latency + naturally delayed reflexes)
-       //this.ping = 0.7 - bound(0, 0.05 * skill, 0.5); // moved the reflexes to bot_aimdir (under the name 'think')
-       // minimum ping 20+10 random
-       CS(this).ping = bound(0,0.07 - bound(0, (skill + this.bot_pingskill) * 0.005,0.05)+random()*0.01,0.65); // Now holds real lag to server, and higer skill players take a less laggy server
-       // skill 10 = ping 0.2 (adrenaline)
-       // skill 0 = ping 0.7 (slightly drunk)
+       // if bot skill is high enough don't assign latency to them
+       if (SUPERBOT)
+               CS(this).ping = 0;
+       else
+       {
+               // calculate an aiming latency based on the skill setting
+               // (simulated network latency + naturally delayed reflexes)
+               //this.ping = 0.7 - bound(0, 0.05 * skill, 0.5); // moved the reflexes to bot_aimdir (under the name 'think')
+               // minimum ping 20+10 random
+               CS(this).ping = bound(0,0.07 - bound(0, (skill + this.bot_pingskill) * 0.005,0.05)+random()*0.01,0.65); // Now holds real lag to server, and higher skill players take a less laggy server
+               // skill 10 = ping 0.2 (adrenaline)
+               // skill 0 = ping 0.7 (slightly drunk)
+       }
 
        // clear buttons
        PHYS_INPUT_BUTTON_ATCK(this) = false;
@@ -285,6 +295,24 @@ void bot_setnameandstuff(entity this)
        READSKILL(bot_thinkskill, 1, 0.5); // think skill
        READSKILL(bot_aiskill, 2, 0); // "ai" skill
 
+       // if bot skill is high enough don't limit their skill
+       if (SUPERBOT)
+       {
+               // commented out means they're meaningless with this high skill
+               // no reason to set them, uncomment if this changes
+               //this.havocbot_keyboardskill = 10;
+               //this.bot_moveskill = 10; //midair modifier sets this to 0 to disable bhop
+               //this.bot_dodgeskill = 10;
+               //this.bot_pingskill = 10;
+               //this.bot_weaponskill = 10;
+               //this.bot_aggresskill = 10;
+               this.bot_rangepreference = 1; // no range preference modification
+               //this.bot_aimskill = 10;
+               //this.bot_offsetskill = 10;
+               //this.bot_mouseskill = 10;
+               //this.bot_thinkskill = 10;
+               //this.bot_aiskill = 10;
+       }
        if (file >= 0 && argv(prio) != "")
                LOG_INFOF("^1Warning^7: too many parameters for bot %s, please check format of %s", bot_name, autocvar_bot_config_file);
 
index 95744035abebca2514da2256254f9edddb5d1719..6205e41b73b20bb86b731a939237de6d6bb4d92d 100644 (file)
@@ -20,6 +20,8 @@ const int AI_STATUS_STUCK                      = BIT(11); // Cannot reach any go
 .int aistatus;
 
 // Skill system
+#define SUPERBOT (skill > 100)
+
 float autoskill_nextthink;
 
 // havocbot_keyboardskill // keyboard movement
index 57c18f096f219b702a8e72cac6e66d1fc891cd93..3fd55f12a35639dd85e19246170f2cc5697e989a 100644 (file)
@@ -394,19 +394,66 @@ entity havocbot_select_an_item_of_group(entity this, int gr)
        return selected;
 }
 
+// Check for water/slime/lava and dangerous edges
+// (only when the bot is on the ground or jumping intentionally)
+// returns true for danger
+bool havocbot_checkdanger(entity this, vector dst_ahead)
+{
+       vector dst_down = dst_ahead - '0 0 3000';
+       traceline(this.origin + this.view_ofs, dst_ahead, true, NULL);
+
+       float s = CONTENT_SOLID;
+       if (trace_fraction == 1 && !this.jumppadcount
+               && !waypoint_is_hardwiredlink(this.goalcurrent_prev, this.goalcurrent)
+               && !(this.goalcurrent_prev && (this.goalcurrent_prev.wpflags & WAYPOINTFLAG_JUMP)))
+       if ((IS_ONGROUND(this)) || (this.aistatus & AI_STATUS_RUNNING) || (this.aistatus & AI_STATUS_ROAMING) || PHYS_INPUT_BUTTON_JUMP(this))
+       {
+               // Look downwards
+               traceline(dst_ahead , dst_down, true, NULL);
+               //te_lightning2(NULL, this.origin + this.view_ofs, dst_ahead); // Draw "ahead" look
+               //te_lightning2(NULL, dst_ahead, trace_endpos); // Draw "downwards" look
+               if (trace_endpos.z < this.origin.z + this.mins.z)
+               {
+                       if (trace_dphitq3surfaceflags & Q3SURFACEFLAG_SKY)
+                               return true;
+                       else if (trace_endpos.z < min(this.origin.z + this.mins.z, this.goalcurrent.origin.z) - 100)
+                               return true;
+                       else
+                       {
+                               s = pointcontents(trace_endpos + '0 0 1');
+                               if (s != CONTENT_SOLID)
+                               {
+                                       if (s == CONTENT_LAVA || s == CONTENT_SLIME)
+                                               return true;
+                                       else if (tracebox_hits_trigger_hurt(dst_ahead, this.mins, this.maxs, trace_endpos))
+                                       {
+                                               // the traceline check isn't enough but is good as optimization,
+                                               // when not true (most of the time) this tracebox call is avoided
+                                               tracebox(dst_ahead, this.mins, this.maxs, dst_down, true, this);
+                                               if (tracebox_hits_trigger_hurt(dst_ahead, this.mins, this.maxs, trace_endpos))
+                                               {
+                                                       return true;
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+       return false;
+}
+
 void havocbot_movetogoal(entity this)
 {
        vector diff;
        vector dir;
        vector flatdir;
        float dodge_enemy_factor = 1;
-       float maxspeed;
+       float maxspeed = autocvar_sv_maxspeed;
        //float dist;
        vector dodge;
        //if (this.goalentity)
        //      te_lightning2(this, this.origin, (this.goalentity.absmin + this.goalentity.absmax) * 0.5);
        CS(this).movement = '0 0 0';
-       maxspeed = autocvar_sv_maxspeed;
 
        PHYS_INPUT_BUTTON_CROUCH(this) = boolean(this.goalcurrent.wpflags & WAYPOINTFLAG_CROUCH);
 
@@ -530,7 +577,7 @@ void havocbot_movetogoal(entity this)
                                        }
                                }
                                vector gco = (this.goalcurrent.absmin + this.goalcurrent.absmax) * 0.5;
-                               if (this.origin.z > gco.z && vdist(vec2(this.velocity), <, autocvar_sv_maxspeed))
+                               if (this.origin.z > gco.z && vdist(vec2(this.velocity), <, maxspeed))
                                {
                                        if (this.velocity.z < 0)
                                                this.aistatus &= ~AI_STATUS_OUT_JUMPPAD;
@@ -549,7 +596,7 @@ void havocbot_movetogoal(entity this)
                        if(this.velocity.z > 0 && this.origin.z - this.lastteleport_origin.z > (this.maxs.z - this.mins.z) * 0.5)
                        {
                                vector velxy = this.velocity; velxy_z = 0;
-                               if(vdist(velxy, <, autocvar_sv_maxspeed * 0.2))
+                               if(vdist(velxy, <, maxspeed * 0.2))
                                {
                                        LOG_TRACE("Warning: ", this.netname, " got stuck on a jumppad (velocity in xy is ", vtos(velxy), "), trying to get out of it now");
                                        this.aistatus |= AI_STATUS_OUT_JUMPPAD;
@@ -921,7 +968,7 @@ void havocbot_movetogoal(entity this)
                }
                else
                {
-                       float s;
+                       float s = 0;
                        vector offset;
                        if(this.aistatus & AI_STATUS_OUT_WATER)
                                this.aistatus &= ~AI_STATUS_OUT_WATER;
@@ -1087,55 +1134,18 @@ void havocbot_movetogoal(entity this)
 
                        // Check for water/slime/lava and dangerous edges
                        // (only when the bot is on the ground or jumping intentionally)
-
                        offset = (vdist(this.velocity, >, 32) ? this.velocity * 0.2 : flatdir * 32);
                        vector dst_ahead = this.origin + this.view_ofs + offset;
-                       vector dst_down = dst_ahead - '0 0 3000';
-                       traceline(this.origin + this.view_ofs, dst_ahead, true, NULL);
-
                        bool unreachable = false;
-                       s = CONTENT_SOLID;
-                       if (trace_fraction == 1 && !this.jumppadcount
-                               && !waypoint_is_hardwiredlink(this.goalcurrent_prev, this.goalcurrent)
-                               && !(this.goalcurrent_prev && (this.goalcurrent_prev.wpflags & WAYPOINTFLAG_JUMP)))
-                       if((IS_ONGROUND(this)) || (this.aistatus & AI_STATUS_RUNNING) || (this.aistatus & AI_STATUS_ROAMING) || PHYS_INPUT_BUTTON_JUMP(this))
+                       if (havocbot_checkdanger(this, dst_ahead))
                        {
-                               // Look downwards
-                               traceline(dst_ahead , dst_down, true, NULL);
-                               //te_lightning2(NULL, this.origin + this.view_ofs, dst_ahead); // Draw "ahead" look
-                               //te_lightning2(NULL, dst_ahead, trace_endpos); // Draw "downwards" look
-                               if(trace_endpos.z < this.origin.z + this.mins.z)
+                               if (destorg.z > this.origin.z + jumpstepheightvec.z)
                                {
-                                       if (trace_dphitq3surfaceflags & Q3SURFACEFLAG_SKY)
-                                               danger_detected = true;
-                                       else if (trace_endpos.z < min(this.origin.z + this.mins.z, this.goalcurrent.origin.z) - 100)
-                                               danger_detected = true;
-                                       else
-                                       {
-                                               s = pointcontents(trace_endpos + '0 0 1');
-                                               if (s != CONTENT_SOLID)
-                                               {
-                                                       if (s == CONTENT_LAVA || s == CONTENT_SLIME)
-                                                               danger_detected = true;
-                                                       else if (tracebox_hits_trigger_hurt(dst_ahead, this.mins, this.maxs, trace_endpos))
-                                                       {
-                                                               // the traceline check isn't enough but is good as optimization,
-                                                               // when not true (most of the time) this tracebox call is avoided
-                                                               tracebox(dst_ahead, this.mins, this.maxs, dst_down, true, this);
-                                                               if (tracebox_hits_trigger_hurt(dst_ahead, this.mins, this.maxs, trace_endpos))
-                                                               {
-                                                                       if (destorg.z > this.origin.z + jumpstepheightvec.z)
-                                                                       {
-                                                                               // the goal is probably on an upper platform, assume bot can't get there
-                                                                               unreachable = true;
-                                                                       }
-                                                                       else
-                                                                               danger_detected = true;
-                                                               }
-                                                       }
-                                               }
-                                       }
+                                       // the goal is probably on an upper platform, assume bot can't get there
+                                       unreachable = true;
                                }
+                               else
+                                       danger_detected = true;
                        }
 
                        dir = flatdir;
@@ -1171,14 +1181,23 @@ void havocbot_movetogoal(entity this)
                dodge = havocbot_dodge(this);
                if (dodge)
                        dodge *= bound(0, 0.5 + (skill + this.bot_dodgeskill) * 0.1, 1);
+               // midair sets moveskill to 0 so avoid jumping when dodging in midair mutator
+               if (dodge.z > 0 && this.bot_moveskill == 0)
+                       dodge.z = 0;
                if (this.enemy)
                {
                        traceline(this.origin, (this.enemy.absmin + this.enemy.absmax) * 0.5, true, NULL);
                        if (IS_PLAYER(trace_ent))
                                dodge_enemy_factor = bound(0, (skill + this.bot_dodgeskill) / 7, 1);
                }
-       //      this.bot_dodgevector = dir;
-       //      this.bot_dodgevector_jumpbutton = PHYS_INPUT_BUTTON_JUMP(this);
+               //this.bot_dodgevector = dir;
+               //this.bot_dodgevector_jumpbutton = PHYS_INPUT_BUTTON_JUMP(this);
+
+               // don't dodge to danger
+               if (havocbot_checkdanger(this, this.origin + this.view_ofs + dodge * 32))
+               {
+                       dodge = '0 0 0';
+               }
        }
 
        float ladder_zdir = 0;
@@ -1254,6 +1273,36 @@ void havocbot_movetogoal(entity this)
        CS(this).movement_y = dir * v_right * maxspeed;
        CS(this).movement_z = dir * v_up * maxspeed;
 
+       // when high enough skill bots engage in combat they move randomly
+       if (SUPERBOT && this.aistatus == AI_STATUS_ATTACKING && !dodge)
+       {
+               if (!this.randomdirectiontime || this.randomdirectiontime + 0.35 < time)
+               {
+                       // 75% chance to generate a random direction to follow for
+                       // 0.3 seconds, there's a 15% chance to fail the generation
+                       // and only generation attempt one every 0.35s so bots move
+                       // towards their goal slightly
+                       if (random() < 0.15)
+                               this.randomdirection = '0 0 0';
+                       else
+                       {
+                               // random values from -1 to 1
+                               this.randomdirection.x = crandom() * maxspeed;
+                               this.randomdirection.y = crandom() * maxspeed;
+                               //this.randomdirection.z = crandom() * maxspeed;
+                       }
+
+                       this.randomdirectiontime = time;
+               }
+               if (this.randomdirectiontime + 0.3 >= time && this.randomdirection)
+               {
+                       CS(this).movement_x = this.randomdirection.x;
+                       CS(this).movement_y = this.randomdirection.y;
+                       // no random vertical direction
+               }
+       }
+
+
        // Emulate keyboard interface
        if (skill < 10)
                havocbot_keyboard_movement(this, destorg);
@@ -1269,7 +1318,12 @@ void havocbot_movetogoal(entity this)
                if (dodge * v_up > 0 && random() * frametime >= 0.2 * bound(0, (10 - skill - this.bot_dodgeskill) * 0.1, 1))
                        PHYS_INPUT_BUTTON_JUMP(this) = true;
                if (dodge * v_up < 0 && random() * frametime >= 0.5 * bound(0, (10 - skill - this.bot_dodgeskill) * 0.1, 1))
+               {
+                       if(IS_ONGROUND(this))
+                               PHYS_INPUT_BUTTON_JUMP(this) = false;
                        this.havocbot_ducktime = time + 0.3 / bound(0.1, skill + this.bot_dodgeskill, 10);
+                       PHYS_INPUT_BUTTON_CROUCH(this) = true;
+               }
        }
 }
 
@@ -1308,7 +1362,12 @@ void havocbot_chooseenemy(entity this)
        }
        if (time < this.havocbot_chooseenemy_finished)
                return;
-       this.havocbot_chooseenemy_finished = time + autocvar_bot_ai_enemydetectioninterval;
+       // don't limit the detection interval to several seconds for bots with enough skill
+       if (SUPERBOT)
+               this.havocbot_chooseenemy_finished = time + 0.1;
+       else
+               this.havocbot_chooseenemy_finished = time + autocvar_bot_ai_enemydetectioninterval;
+
        vector eye = this.origin + this.view_ofs;
        entity best = NULL;
        float bestrating = autocvar_bot_ai_enemydetectionradius ** 2;
@@ -1341,14 +1400,37 @@ void havocbot_chooseenemy(entity this)
                                continue;
 
                        vector v = (it.absmin + it.absmax) * 0.5;
-                       float rating = vlen2(v - eye);
-                       if (rating < bestrating && bot_shouldattack(this, it))
+                       float distance = vlen2(v - eye);
+
+                       if (SUPERBOT)
+                       {
+                               if (bot_shouldattack(this, it))
+                               {
+                                       // skilled enough bots take account target health and distance
+                                       float health = GetResource(it, RES_HEALTH);
+                                       float armor  = GetResource(it, RES_ARMOR);
+                                       float rating = bound(50, health + armor, 250) * distance;
+                                       if (!best || (rating < bestrating))
+                                       {
+                                               traceline(eye, v, true, this);
+                                               if (trace_ent == it || trace_fraction >= 1)
+                                               {
+                                                       best = it;
+                                                       bestrating = rating;
+                                               }
+                                       }
+                               }
+                       }
+                       else
                        {
-                               traceline(eye, v, true, this);
-                               if (trace_ent == it || trace_fraction >= 1)
+                               if (distance < bestrating && bot_shouldattack(this, it))
                                {
-                                       best = it;
-                                       bestrating = rating;
+                                       traceline(eye, v, true, this);
+                                       if (trace_ent == it || trace_fraction >= 1)
+                                       {
+                                               best = it;
+                                               bestrating = distance;
+                                       }
                                }
                        }
                });
@@ -1358,6 +1440,7 @@ void havocbot_chooseenemy(entity this)
                        scan_secondary_targets = true;
                        // restart the loop
                        bestrating = autocvar_bot_ai_enemydetectionradius ** 2;
+
                        goto scan_targets;
                }
 
@@ -1406,6 +1489,7 @@ float havocbot_chooseweapon_checkreload(entity this, .entity weaponentity, int n
 void havocbot_chooseweapon(entity this, .entity weaponentity)
 {
        int i;
+       float w;
 
        // ;)
        if(g_weaponarena_weapons == WEPSET(TUBA))
@@ -1415,17 +1499,33 @@ void havocbot_chooseweapon(entity this, .entity weaponentity)
        }
 
        // TODO: clean this up by moving it to weapon code
-       if(this.enemy==NULL)
+       if (this.enemy == NULL)
        {
-               // If no weapon was chosen get the first available weapon
-               if(this.(weaponentity).m_weapon==WEP_Null)
-               FOREACH(Weapons, it != WEP_Null, {
-                       if(client_hasweapon(this, it, weaponentity, true, false))
+               // Choose the first available weapon from medium range weaponlist
+               // TODO: don't do this but don't make bots hold out a blaster out either
+               for (i = 0; i < REGISTRY_COUNT(Weapons) && bot_weapons_mid[i] != -1 ; ++i){
+                       w = bot_weapons_mid[i];
+                       if (bot_custom_weapon)
                        {
-                               this.(weaponentity).m_switchweapon = it;
-                               return;
+                               if (client_hasweapon(this, REGISTRY_GET(Weapons, w), weaponentity, true, false))
+                               {
+                                       if ((this.(weaponentity).m_weapon == WEP_Null) || havocbot_chooseweapon_checkreload(this, weaponentity, w))
+                                               continue;
+                                       this.(weaponentity).m_switchweapon = REGISTRY_GET(Weapons, w);
+                                       return;
+                               }
                        }
-               });
+               }
+
+               // If no weapon was chosen get the first available weapon
+               if (this.(weaponentity).m_weapon == WEP_Null)
+                       FOREACH(Weapons, it != WEP_Null, {
+                               if (client_hasweapon(this, it, weaponentity, true, false))
+                               {
+                                       this.(weaponentity).m_switchweapon = it;
+                                       return;
+                               }
+                       });
                return;
        }
 
@@ -1434,7 +1534,6 @@ void havocbot_chooseweapon(entity this, .entity weaponentity)
        if(f < 1)
                return;
 
-       float w;
        float distance; distance=bound(10,vlen(this.origin-this.enemy.origin)-200,10000);
 
        // Should it do a weapon combo?
@@ -1672,8 +1771,11 @@ void havocbot_setupbot(entity this)
 vector havocbot_dodge(entity this)
 {
        // LordHavoc: disabled because this is too expensive
-       return '0 0 0';
-#if 0
+       // Dr. Jaska: re-enable this but only for bots with high enough skill
+       if (!SUPERBOT)
+               return '0 0 0';
+
+#if 1
        entity head;
        vector dodge, v, n;
        float danger, bestdanger, vl, d;
@@ -1694,7 +1796,8 @@ vector havocbot_dodge(entity this)
                                if (d > (0 - head.bot_dodgerating))
                                if (d < (vl * 0.2 + head.bot_dodgerating))
                                {
-                                       // calculate direction and distance from the flight path, by removing the forward axis
+                                       // calculate direction and distance from the
+                                       // flight path by removing the forward axis
                                        v = v - (n * (v * n));
                                        danger = head.bot_dodgerating - vlen(v);
                                        if (bestdanger < danger)
@@ -1718,5 +1821,7 @@ vector havocbot_dodge(entity this)
                head = head.chain;
        }
        return dodge;
+#else
+       return '0 0 0';
 #endif
 }
index 02c0f3e7d76b3d8912afcea77f8f0ea5fb779df1..d28d4d84b45d36cdab6e3ed9f6a8625e8034cfac 100644 (file)
@@ -29,6 +29,9 @@
 
 .vector havocbot_keyboard;
 
+.float randomdirectiontime;
+.vector randomdirection;
+
 /*
  * Functions
  */