X-Git-Url: http://de.git.xonotic.org/?p=xonotic%2Fxonotic-data.pk3dir.git;a=blobdiff_plain;f=qcsrc%2Fserver%2Fbot%2Fdefault%2Fnavigation.qc;h=9dc1e47aa37e326a626501b9e90d24fb51c63e49;hp=8d592269341022695a551e8195eea9d19d88646e;hb=797bf448a96c0c13d783c7c919bb2caf6fa16707;hpb=14ed5595ec730cc91afdef42f91712781162ba7e diff --git a/qcsrc/server/bot/default/navigation.qc b/qcsrc/server/bot/default/navigation.qc index 8d59226934..9dc1e47aa3 100644 --- a/qcsrc/server/bot/default/navigation.qc +++ b/qcsrc/server/bot/default/navigation.qc @@ -13,10 +13,62 @@ #include #include -#include +#include +#include .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) +{ + vector gco = (this.goalentity.absmin + this.goalentity.absmax) * 0.5; + if (vdist(gco - this.origin, >, autocvar_sv_maxspeed * 1.5) + && time > this.bot_strategytime - (IS_MOVABLE(this.goalentity) ? 3 : 2)) + { + return true; + } + + if (this.goalentity.bot_pickup && time > this.bot_strategytime - 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; @@ -27,9 +79,13 @@ void navigation_dynamicgoal_init(entity this, bool initially_static) this.nearestwaypointtimeout = time; } -void navigation_dynamicgoal_set(entity this) +void navigation_dynamicgoal_set(entity this, entity dropper) { this.nearestwaypointtimeout = time; + if (dropper && dropper.nearestwaypointtimeout && dropper.nearestwaypointtimeout < time + 2) + this.nearestwaypoint = dropper.nearestwaypoint; + if (this.nearestwaypoint) + this.nearestwaypointtimeout += 2; } void navigation_dynamicgoal_unset(entity this) @@ -39,6 +95,107 @@ void navigation_dynamicgoal_unset(entity this) this.nearestwaypointtimeout = -1; } +// returns point of ent closer to org +vector get_closer_dest(entity ent, vector org) +{ + vector dest = '0 0 0'; + if ((ent.classname != "waypoint") || ent.wpisbox) + { + vector wm1 = ent.origin + ent.mins; + vector wm2 = ent.origin + ent.maxs; + dest.x = bound(wm1.x, org.x, wm2.x); + dest.y = bound(wm1.y, org.y, wm2.y); + dest.z = bound(wm1.z, org.z, wm2.z); + } + else + dest = ent.origin; + return dest; +} + +void set_tracewalk_dest(entity ent, vector org, bool fix_player_dest) +{ + if ((ent.classname != "waypoint") || ent.wpisbox) + { + vector wm1 = ent.origin + ent.mins; + vector wm2 = ent.origin + ent.maxs; + if (IS_PLAYER(ent) || IS_MONSTER(ent)) + { + // move destination point out of player bbox otherwise tracebox always fails + // (if bot_navigation_ignoreplayers is false) + wm1 += vec2(PL_MIN_CONST) + '-1 -1 0'; + wm2 += vec2(PL_MAX_CONST) + '1 1 0'; + } + // set destination point to x and y coords of ent that are closer to org + // z coord is set to ent's min height + tracewalk_dest.x = bound(wm1.x, org.x, wm2.x); + tracewalk_dest.y = bound(wm1.y, org.y, wm2.y); + if ((IS_PLAYER(ent) || IS_MONSTER(ent)) + && org.x == tracewalk_dest.x && org.y == tracewalk_dest.y && org.z > tracewalk_dest.z) + { + tracewalk_dest.z = wm2.z - PL_MIN_CONST.z; + tracewalk_dest_height = 0; + fix_player_dest = false; + } + else + { + tracewalk_dest.z = wm1.z; + tracewalk_dest_height = wm2.z - wm1.z; + } + } + 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; @@ -105,6 +262,7 @@ vector resurface_limited(vector org, float lim, vector m1) // rough simulation of walking from one point to another to test if a path // can be traveled, used for waypoint linking and havocbot // if end_height is > 0 destination is any point in the vertical segment [end, end + end_height * eZ] +// INFO: the command sv_cmd trace walk is useful to test this function in game bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float end_height, float movemode) { if(autocvar_bot_debug_tracewalk) @@ -123,8 +281,7 @@ bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float e int nav_action; // Analyze starting point - traceline(start, start, MOVE_NORMAL, e); - if (trace_dpstartcontents & (DPCONTENTS_SLIME | DPCONTENTS_LAVA)) + if (IN_LAVA(start)) ignorehazards = true; tracebox(start, m1, m2, start, MOVE_NOMONSTERS, e); @@ -585,10 +742,13 @@ bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float e // completely empty the goal stack, used when deciding where to go void navigation_clearroute(entity this) { + this.lastteleporttime = 0; 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_shouldbefrozen = false; this.goalentity = NULL; this.goalcurrent = NULL; this.goalstack01 = NULL; @@ -633,7 +793,8 @@ void navigation_clearroute(entity this) 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) @@ -678,11 +839,15 @@ void navigation_pushroute(entity this, entity e) 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; @@ -752,10 +917,17 @@ entity navigation_findnearestwaypoint_withdist_except(entity ent, float walkfrom vector pm2 = ent.origin + ent.maxs; // do two scans, because box test is cheaper - IL_EACH(g_waypoints, it != ent && it != except, + IL_EACH(g_waypoints, it != ent && it != except && !(it.wpflags & WAYPOINTFLAG_TELEPORT), { 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; @@ -763,8 +935,7 @@ entity navigation_findnearestwaypoint_withdist_except(entity ent, float walkfrom te_plasmaburn(org); entity best = NULL; - vector v = '0 0 0', v2 = '0 0 0'; - float v2_height = 0; + vector v = '0 0 0'; if(ent.size && !IS_PLAYER(ent)) { @@ -780,10 +951,13 @@ entity navigation_findnearestwaypoint_withdist_except(entity ent, float walkfrom 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); + } }); } @@ -793,16 +967,45 @@ entity navigation_findnearestwaypoint_withdist_except(entity ent, float walkfrom if(walkfromwp && (it.wpflags & WAYPOINTFLAG_NORELINK)) continue; v = it.origin; - if(walkfromwp) - SET_TRACEWALK_DESTCOORDS(ent, v, v2, v2_height); + + if (walkfromwp) + { + set_tracewalk_dest(ent, v, true); + if (trace_ent == ent) + { + bestdist = 0; + best = it; + break; + } + } else - SET_TRACEWALK_DESTCOORDS(it, org, v2, v2_height); - if(navigation_waypoint_will_link(v, org, ent, v2, v2_height, v2, v2_height, walkfromwp, bestdist)) + set_tracewalk_dest(it, org, false); + + if (navigation_waypoint_will_link(v, org, ent, + tracewalk_dest, tracewalk_dest_height, + tracewalk_dest, tracewalk_dest_height, walkfromwp, bestdist)) { - bestdist = vlen(v - org); + if (walkfromwp) + bestdist = vlen(tracewalk_dest - v); + else + bestdist = vlen(v - org); best = it; } }); + if(!best && !ent.navigation_dynamicgoal) + { + int solid_save = ent.solid; + ent.solid = SOLID_BSP; + IL_EACH(g_jumppads, true, + { + if(trigger_push_test(it, ent)) + { + best = it.nearestwaypoint; + break; + } + }); + ent.solid = solid_save; + } return best; } entity navigation_findnearestwaypoint(entity ent, float walkfromwp) @@ -820,23 +1023,22 @@ entity navigation_findnearestwaypoint(entity ent, float walkfromwp) // finds the waypoints near the bot initiating a navigation query float navigation_markroutes_nearestwaypoints(entity this, float maxdist) { - vector v = '0 0 0'; //navigation_testtracewalk = true; int c = 0; - float v_height = 0; IL_EACH(g_waypoints, !it.wpconsidered, { - SET_TRACEWALK_DESTCOORDS(it, this.origin, v, v_height); + set_tracewalk_dest(it, this.origin, false); - vector diff = v - this.origin; + vector diff = tracewalk_dest - this.origin; diff.z = max(0, diff.z); if(vdist(diff, <, maxdist)) { it.wpconsidered = true; - if (tracewalk(this, this.origin, this.mins, this.maxs, v, v_height, bot_navigation_movemode)) + if (tracewalk(this, this.origin, this.mins, this.maxs, + tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode)) { - it.wpnearestpoint = v; - it.wpcost = waypoint_gettravelcost(this.origin, v, this, it) + it.dmg; + it.wpnearestpoint = tracewalk_dest; + it.wpcost = waypoint_gettravelcost(this.origin, tracewalk_dest, this, it) + it.dmg; it.wpfire = 1; it.enemy = NULL; c = c + 1; @@ -1017,11 +1219,7 @@ void navigation_markroutes_inverted(entity fixed_source_waypoint) // updates the best goal according to a weighted calculation of travel cost and item value of a new proposed item void navigation_routerating(entity this, entity e, float f, float rangebias) { - if (!e) - return; - - if(e.blacklisted) - return; + if (!e || e.blacklisted) { return; } rangebias = waypoint_getlinearcost(rangebias); f = waypoint_getlinearcost(f); @@ -1029,8 +1227,11 @@ void navigation_routerating(entity this, entity e, float f, float rangebias) if (IS_PLAYER(e)) { bool rate_wps = false; - if((e.flags & FL_INWATER) || (e.flags & FL_PARTIALGROUND)) + if (e.watertype < CONTENT_WATER || (e.waterlevel > WATERLEVEL_WETFEET && !STAT(FROZEN, e)) + || (e.flags & FL_PARTIALGROUND)) + { rate_wps = true; + } if(!IS_ONGROUND(e)) { @@ -1049,12 +1250,13 @@ void navigation_routerating(entity this, entity e, float f, float rangebias) { entity theEnemy = e; entity best_wp = NULL; - float best_dist = 10000; - IL_EACH(g_waypoints, vdist(it.origin - theEnemy.origin, <, 500) + float best_dist = FLOAT_MAX; + IL_EACH(g_waypoints, !(it.wpflags & WAYPOINTFLAG_TELEPORT) + && vdist(it.origin - theEnemy.origin, <, 500) && vdist(it.origin - this.origin, >, 100) - && !(it.wpflags & WAYPOINTFLAG_TELEPORT), + && vdist(it.origin - this.origin, <, 10000), { - float dist = vlen(it.origin - theEnemy.origin); + float dist = vlen2(it.origin - theEnemy.origin); if (dist < best_dist) { best_wp = it; @@ -1072,7 +1274,6 @@ void navigation_routerating(entity this, entity e, float f, float rangebias) //print("routerating ", etos(e), " = ", ftos(f), " - ", ftos(rangebias), "\n"); // Evaluate path using jetpack - if(g_jetpack) if(this.items & IT_JETPACK) if(autocvar_bot_ai_navigation_jetpack) if(vdist(this.origin - goal_org, >, autocvar_bot_ai_navigation_jetpack_mindistance)) @@ -1131,10 +1332,10 @@ void navigation_routerating(entity this, entity e, float f, float rangebias) t += xydistance / autocvar_g_jetpack_maxspeed_side; fuel = t * autocvar_g_jetpack_fuel * 0.8; - LOG_DEBUG("jetpack ai: required fuel ", ftos(fuel), " this.ammo_fuel ", ftos(this.ammo_fuel)); + LOG_DEBUG("jetpack ai: required fuel ", ftos(fuel), ", have ", ftos(GetResource(this, RES_FUEL))); // enough fuel ? - if(this.ammo_fuel>fuel) + if(GetResource(this, RES_FUEL) > fuel || (this.items & IT_UNLIMITED_WEAPON_AMMO)) { // Estimate cost // (as onground costs calculation is mostly based on distances, here we do the same establishing some relationship @@ -1177,29 +1378,11 @@ void navigation_routerating(entity this, entity e, float f, float rangebias) 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)); @@ -1221,18 +1404,21 @@ void navigation_routerating(entity this, entity e, float f, float rangebias) nwp = e.nearestwaypoint; } - LOG_DEBUG("-- checking ", e.classname, " (with cost ", ftos(nwp.wpcost), ")"); - if (nwp) - if (nwp.wpcost < 10000000) + if (nwp && nwp.wpcost < 10000000) { //te_wizspike(nwp.wpnearestpoint); - float cost = nwp.wpcost + waypoint_gettravelcost(nwp.wpnearestpoint, goal_org, nwp, e); - LOG_DEBUG(e.classname, " ", ftos(f), "/(1+", ftos(cost), "/", ftos(rangebias), ") = "); + float nwptoitem_cost = 0; + if(nwp.wpflags & WAYPOINTFLAG_TELEPORT) + nwptoitem_cost = nwp.wp00mincost; + else + nwptoitem_cost = waypoint_gettravelcost(nwp.wpnearestpoint, goal_org, nwp, e); + float cost = nwp.wpcost + nwptoitem_cost; + LOG_DEBUG("checking ^5", e.classname, "^7 with base rating ^xf04", ftos(f), "^7 and rangebias ^xf40", ftos(rangebias)); f = f * rangebias / (rangebias + cost); - LOG_DEBUG("considering ", e.classname, " (with rating ", ftos(f), ")"); + LOG_DEBUG(" ^5", e.classname, "^7 with cost ^6", ftos(cost), "^7 and final rating ^2", ftos(f)); if (navigation_bestrating < f) { - LOG_DEBUG("ground path: added goal ", e.classname, " (with rating ", ftos(f), ")"); + LOG_DEBUG(" ground path: ^3added goal ^5", e.classname); navigation_bestrating = f; navigation_bestgoal = e; } @@ -1276,11 +1462,13 @@ bool navigation_routetogoal(entity this, entity e, vector startposition) return true; // if it can reach the goal there is nothing more to do - vector dest = '0 0 0'; - float dest_height = 0; - SET_TRACEWALK_DESTCOORDS(e, startposition, dest, dest_height); - if (tracewalk(this, startposition, STAT(PL_MIN, this), STAT(PL_MAX, this), dest, dest_height, bot_navigation_movemode)) + set_tracewalk_dest(e, startposition, true); + if ((!IS_MOVABLE(this.goalcurrent) || vdist(tracewalk_dest - this.origin, <, MAX_CHASE_DISTANCE)) + && (trace_ent == this || tracewalk(this, startposition, STAT(PL_MIN, this), STAT(PL_MAX, this), + tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))) + { return true; + } entity nearest_wp = NULL; // see if there are waypoints describing a path to the item @@ -1300,20 +1488,27 @@ bool navigation_routetogoal(entity this, entity e, vector startposition) 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) { - 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)) + if (vdist(vec2(this.goalentity.origin - nearest_wp.origin), <, 32)) e = nearest_wp.enemy; + else + { + set_tracewalk_dest(this.goalentity, nearest_wp.enemy.origin, true); + if (trace_ent == this || (vdist(tracewalk_dest - nearest_wp.enemy.origin, <, 1050) + && vlen2(tracewalk_dest - nearest_wp.enemy.origin) < vlen2(nearest_wp.origin - nearest_wp.enemy.origin) + && tracewalk(this, nearest_wp.enemy.origin, STAT(PL_MIN, this), STAT(PL_MAX, this), + tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))) + { + e = nearest_wp.enemy; + } + } } - else if(navigation_item_islinked(nearest_wp.enemy, this.goalentity)) - e = nearest_wp.enemy; } + else if(navigation_item_islinked(nearest_wp.enemy, this.goalentity)) + e = nearest_wp.enemy; } for (;;) @@ -1329,56 +1524,167 @@ bool navigation_routetogoal(entity this, entity e, vector startposition) return false; } +// shorten path by removing intermediate goals +bool navigation_shortenpath(entity this) +{ + if (!this.goalstack01 || wasfreed(this.goalstack01)) + return false; + if (this.bot_tracewalk_time > time) + return false; + this.bot_tracewalk_time = max(time, this.bot_tracewalk_time) + 0.25; + + bool cut_allowed = false; + entity next = this.goalentity; + // evaluate whether bot can discard current route and chase directly a player, trying to + // keep waypoint route as long as possible, as it is safer and faster (bot can bunnyhop) + if (IS_MOVABLE(next)) + { + set_tracewalk_dest(next, this.origin, true); + if (vdist(this.origin - tracewalk_dest, <, 200)) + cut_allowed = true; + else if (vdist(tracewalk_dest - this.origin, <, MAX_CHASE_DISTANCE) + && vdist(tracewalk_dest - this.goalcurrent.origin, >, 200) + && vdist(this.origin - this.goalcurrent.origin, >, 100) + && checkpvs(this.origin + this.view_ofs, next)) + { + if (vlen2(next.origin - this.origin) < vlen2(this.goalcurrent.origin - this.origin)) + cut_allowed = true; + else + { + vector deviation = vectoangles(this.goalcurrent.origin - this.origin) - vectoangles(next.origin - this.origin); + while (deviation.y < -180) deviation.y += 360; + while (deviation.y > 180) deviation.y -= 360; + if (fabs(deviation.y) > 25) + cut_allowed = true; + } + } + if (cut_allowed) + { + if (trace_ent == this || tracewalk(this, this.origin, this.mins, this.maxs, + tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode)) + { + LOG_DEBUG("path optimized for ", this.netname, ", route cleared"); + do + { + navigation_poproute(this); + } + while (this.goalcurrent != next); + return true; + } + return false; + } + } + + next = this.goalstack01; + // if for some reason the bot is closer to the next goal, pop the current one + if (!IS_MOVABLE(next) // already checked in the previous case + && vlen2(this.goalcurrent.origin - next.origin) > vlen2(next.origin - this.origin) + && checkpvs(this.origin + this.view_ofs, next)) + { + set_tracewalk_dest(next, this.origin, true); + cut_allowed = true; + } + + if (cut_allowed) + { + if (trace_ent == this || tracewalk(this, this.origin, this.mins, this.maxs, + tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode)) + { + LOG_DEBUG("path optimized for ", this.netname, ", removed a goal from the queue"); + navigation_poproute(this); + return true; + } + } + return false; +} + // removes any currently touching waypoints from the goal stack // (this is how bots detect if they reached a goal) -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) { + if (!this.goalcurrent.wpisbox // warpzone + && vlen2(this.origin - this.goalstack01.origin) < vlen2(this.origin - this.goalcurrent.origin)) + { + // immediately remove origin and destination waypoints + navigation_poproute(this); + ++removed_goals; + navigation_poproute(this); + ++removed_goals; + this.lastteleporttime = 0; + } + // make sure jumppad is really hit, don't rely on distance based checks // as they may report a touch even if it didn't really happen - if(this.lastteleporttime > 0 - && time - this.lastteleporttime < ((this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL) ? 2 : 0.15)) + if(this.lastteleporttime > 0 && TELEPORT_USED(this, this.goalcurrent)) { - if (this.jumppadcount && !boxesoverlap(this.goalcurrent.absmin, this.goalcurrent.absmax, - this.lastteleport_origin + STAT(PL_MIN, this), this.lastteleport_origin + STAT(PL_MAX, this))) - { - return; - } - if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) if(this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && this.goalcurrent.owner==this) { this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED; } + if(this.jumppadcount) + { + // remove jumppad waypoint after a random delay to prevent bots getting + // stuck on certain jumppads that require an extra initial horizontal speed + float max_delay = 0.1; + if (vdist(vec2(this.velocity), >, 2 * autocvar_sv_maxspeed)) + max_delay = 0.05; + if (time - this.lastteleporttime < random() * max_delay) + return removed_goals; + } + else if (this.goalcurrent.wpisbox) // teleport + { + // immediately remove origin and destination waypoints + navigation_poproute(this); + ++removed_goals; + } navigation_poproute(this); + this.lastteleporttime = 0; + ++removed_goals; } - else - return; + return removed_goals; } - - // If for some reason the bot is closer to the next goal, pop the current one - if(this.goalstack01 && !wasfreed(this.goalstack01)) - if(vlen2(this.goalcurrent.origin - this.origin) > vlen2(this.goalstack01.origin - this.origin)) - if(checkpvs(this.origin + this.view_ofs, this.goalstack01)) + else if (this.lastteleporttime > 0) { - 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)) + // sometimes bot is pushed so hard (by a jumppad or a shot) that ends up touching the next + // teleport / jumppad / warpzone present in its path skipping check of one or more goals + // if so immediately fix bot path by removing skipped goals + entity tele_ent = NULL; + if (this.goalstack01 && (this.goalstack01.wpflags & WAYPOINTFLAG_TELEPORT)) + tele_ent = this.goalstack01; + else if (this.goalstack02 && (this.goalstack02.wpflags & WAYPOINTFLAG_TELEPORT)) + tele_ent = this.goalstack02; + else if (this.goalstack03 && (this.goalstack03.wpflags & WAYPOINTFLAG_TELEPORT)) + tele_ent = this.goalstack03; + if (tele_ent && TELEPORT_USED(this, tele_ent)) { - LOG_DEBUG("path optimized for ", this.netname, ", removed a goal from the queue"); + if (this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) + if (tele_ent.wpflags & WAYPOINTFLAG_PERSONAL && tele_ent.owner == this) + { + this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; + this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED; + } + while (this.goalcurrent != tele_ent) + { + navigation_poproute(this); + ++removed_goals; + } navigation_poproute(this); - 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 + this.lastteleporttime = 0; + ++removed_goals; + return removed_goals; } + // reset of lastteleporttime can be overriden by a jumppad when it's set + // in more than one frame: make sure it's reset + this.lastteleporttime = 0; } // Loose goal touching check when running @@ -1400,8 +1706,9 @@ void navigation_poptouchedgoals(entity this) } navigation_poproute(this); + ++removed_goals; if(this.goalcurrent && this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT) - return; + return removed_goals; } } } @@ -1415,8 +1722,16 @@ void navigation_poptouchedgoals(entity this) gc_min = this.goalcurrent.origin - '1 1 1' * 12; gc_max = this.goalcurrent.origin + '1 1 1' * 12; } - if(!boxesoverlap(this.absmin, this.absmax, gc_min, gc_max)) - break; + if (time < this.ladder_time) + { + if (!boxesoverlap(this.absmin, this.absmax - eZ * STAT(PL_MAX, this).z, gc_min, gc_max)) + break; + } + else + { + if (!boxesoverlap(this.absmin, this.absmax, gc_min, gc_max)) + break; + } // Detect personal waypoints if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) @@ -1427,9 +1742,56 @@ void navigation_poptouchedgoals(entity this) } 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) + wp = this.goalcurrent_prev; + if(!wp) + return NULL; + if(wp != this.goalcurrent_prev && vdist(wp.origin - this.origin, >, 50)) + { + wp = this.goalcurrent_prev; + if(!wp) + return NULL; + } + if(wp.classname != "waypoint") + { + wp = wp.nearestwaypoint; + if(!wp) + return NULL; + } + if(vdist(wp.origin - this.origin, >, 50)) + { + wp = NULL; + IL_EACH(g_waypoints, !(it.wpflags & WAYPOINTFLAG_TELEPORT), + { + if(vdist(it.origin - this.origin, <, 50)) + { + wp = it; + break; + } + }); + if(!wp) + return NULL; } + 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) @@ -1440,9 +1802,11 @@ void navigation_goalrating_start(entity this) 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); + this.goalstack31 = wp; // temporarly save the really close waypoint } // ends a goal selection session (updates goal stack to the best goal) @@ -1451,18 +1815,27 @@ void navigation_goalrating_end(entity this) if(this.aistatus & AI_STATUS_STUCK) return; + entity wp = this.goalstack31; // save to wp as this.goalstack31 is set by navigation_routetogoal + this.goalstack31 = NULL; + navigation_routetogoal(this, navigation_bestgoal, this.origin); LOG_DEBUG("best goal ", this.goalcurrent.classname); + if (wp && this.goalcurrent == wp) + navigation_poproute(this); + // If the bot got stuck then try to reach the farthest waypoint - if (!this.goalentity && autocvar_bot_wander_enable) + if (!this.goalentity) { - if (!(this.aistatus & AI_STATUS_STUCK)) + if (autocvar_bot_wander_enable && !(this.aistatus & AI_STATUS_STUCK)) { LOG_DEBUG(this.netname, " cannot walk to any goal"); this.aistatus |= AI_STATUS_STUCK; } + this.goalentity_shouldbefrozen = false; } + else + this.goalentity_shouldbefrozen = boolean(STAT(FROZEN, this.goalentity)); } void botframe_updatedangerousobjects(float maxupdate) @@ -1501,11 +1874,20 @@ void botframe_updatedangerousobjects(float maxupdate) void navigation_unstuck(entity this) { - float search_radius = 1000; - if (!autocvar_bot_wander_enable) return; + bool has_user_waypoints = false; + IL_EACH(g_waypoints, !(it.wpflags & WAYPOINTFLAG_GENERATED), + { + has_user_waypoints = true; + break; + }); + if (!has_user_waypoints) + return; + + float search_radius = 1000; + if (!bot_waypoint_queue_owner) { LOG_DEBUG(this.netname, " stuck, taking over the waypoints queue"); @@ -1522,10 +1904,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(this, this.origin, STAT(PL_MIN, this), STAT(PL_MAX, this), + tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode)) { if( d > bot_waypoint_queue_bestgoalrating) { @@ -1533,6 +1914,17 @@ void navigation_unstuck(entity this) bot_waypoint_queue_bestgoal = bot_waypoint_queue_goal; } } + + // move to a random waypoint while bot is searching for a walkable path; + // this is usually sufficient to unstuck bots from bad spots or when other + // bots of the same team block all their ways + if (!bot_waypoint_queue_bestgoal && (!this.goalentity || random() < 0.1)) + { + navigation_clearroute(this); + navigation_routetogoal(this, bot_waypoint_queue_goal, this.origin); + navigation_goalrating_timeout_expire(this, 1 + random() * 2); + } + bot_waypoint_queue_goal = bot_waypoint_queue_goal.bot_waypoint_queue_nextgoal; if (!bot_waypoint_queue_goal) @@ -1540,8 +1932,9 @@ void navigation_unstuck(entity this) if (bot_waypoint_queue_bestgoal) { LOG_DEBUG(this.netname, " stuck, reachable waypoint found, heading to it"); + navigation_clearroute(this); 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