From: Dr. Jaska Date: Sat, 27 May 2023 18:21:00 +0000 (+0000) Subject: Add a new bot skill level for extremely tough bots X-Git-Tag: xonotic-v0.8.6~63^2 X-Git-Url: https://git.xonotic.org/?p=xonotic%2Fxonotic-data.pk3dir.git;a=commitdiff_plain;h=d54fa29177802e1d636f768bb447678efae3ecd2 Add a new bot skill level for extremely tough bots --- diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b56f2b44d..1feae253c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 - wget -nv -O data/maps/stormkeep.waypoints.cache https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.waypoints.cache - - EXPECT=f2226bf353b6ff3dd3c489a742ce4e6a + - EXPECT=20c9c4ea0364fbb611656cc0b876e02b - HASH=$(${ENGINE} +exec serverbench.cfg | tee /dev/stderr | grep '^:' diff --git a/qcsrc/server/bot/default/aim.qc b/qcsrc/server/bot/default/aim.qc index 1d0f78b74..c058c06f5 100644 --- a/qcsrc/server/bot/default/aim.qc +++ b/qcsrc/server/bot/default/aim.qc @@ -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; diff --git a/qcsrc/server/bot/default/bot.qc b/qcsrc/server/bot/default/bot.qc index 5bfa79aa5..2a663a10d 100644 --- a/qcsrc/server/bot/default/bot.qc +++ b/qcsrc/server/bot/default/bot.qc @@ -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); diff --git a/qcsrc/server/bot/default/bot.qh b/qcsrc/server/bot/default/bot.qh index 95744035a..6205e41b7 100644 --- a/qcsrc/server/bot/default/bot.qh +++ b/qcsrc/server/bot/default/bot.qh @@ -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 diff --git a/qcsrc/server/bot/default/havocbot/havocbot.qc b/qcsrc/server/bot/default/havocbot/havocbot.qc index 57c18f096..3fd55f12a 100644 --- a/qcsrc/server/bot/default/havocbot/havocbot.qc +++ b/qcsrc/server/bot/default/havocbot/havocbot.qc @@ -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 } diff --git a/qcsrc/server/bot/default/havocbot/havocbot.qh b/qcsrc/server/bot/default/havocbot/havocbot.qh index 02c0f3e7d..d28d4d84b 100644 --- a/qcsrc/server/bot/default/havocbot/havocbot.qh +++ b/qcsrc/server/bot/default/havocbot/havocbot.qh @@ -29,6 +29,9 @@ .vector havocbot_keyboard; +.float randomdirectiontime; +.vector randomdirection; + /* * Functions */