From: Mario Date: Mon, 22 Jan 2018 11:28:22 +0000 (+0000) Subject: Merge branch 'terencehill/bot_ai' into 'master' X-Git-Tag: xonotic-v0.8.5~2378 X-Git-Url: http://de.git.xonotic.org/?p=xonotic%2Fxonotic-data.pk3dir.git;a=commitdiff_plain;h=3c7ac3f983fcc360961822c570a713ede68453ad;hp=3851515c57e042c64be949987970f03ca458fd50 Merge branch 'terencehill/bot_ai' into 'master' Bot AI improvements See merge request xonotic/xonotic-data.pk3dir!513 --- diff --git a/defaultServer.cfg b/defaultServer.cfg index db91e38549..7397462677 100644 --- a/defaultServer.cfg +++ b/defaultServer.cfg @@ -116,7 +116,8 @@ set bot_debug_goalstack 0 "Visualize the current path that each bot is following 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." diff --git a/qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qc b/qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qc index 0150de3925..5a0e0975e5 100644 --- a/qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qc +++ b/qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qc @@ -1488,7 +1488,7 @@ void havocbot_role_ons_offense(entity this) if(this.havocbot_attack_time>time) return; - if (this.bot_strategytime < time) + if (navigation_goalrating_timeout(this)) { navigation_goalrating_start(this); havocbot_goalrating_enemyplayers(this, 20000, this.origin, 650); @@ -1497,7 +1497,7 @@ void havocbot_role_ons_offense(entity this) havocbot_goalrating_ons_offenseitems(this, 10000, this.origin, 10000); navigation_goalrating_end(this); - this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + navigation_goalrating_timeout_set(this); } } diff --git a/qcsrc/common/t_items.qc b/qcsrc/common/t_items.qc index 8469d1e3c2..e13df065a4 100644 --- a/qcsrc/common/t_items.qc +++ b/qcsrc/common/t_items.qc @@ -1117,8 +1117,6 @@ float ammo_pickupevalfunc(entity player, entity item) return rating; } -.int item_group; -.int item_group_count; float healtharmor_pickupevalfunc(entity player, entity item) { float c = 0; diff --git a/qcsrc/common/t_items.qh b/qcsrc/common/t_items.qh index 455952dd63..d2f44c61da 100644 --- a/qcsrc/common/t_items.qh +++ b/qcsrc/common/t_items.qh @@ -26,6 +26,8 @@ const int ISF_SIZE = BIT(7); #ifdef SVQC void StartItem(entity this, entity a); +.int item_group; +.int item_group_count; #endif #ifdef CSQC diff --git a/qcsrc/common/triggers/func/ladder.qc b/qcsrc/common/triggers/func/ladder.qc index f4a7ffb02c..92f361a145 100644 --- a/qcsrc/common/triggers/func/ladder.qc +++ b/qcsrc/common/triggers/func/ladder.qc @@ -42,9 +42,71 @@ void func_ladder_link(entity this) void func_ladder_init(entity this) { settouch(this, func_ladder_touch); - trigger_init(this); func_ladder_link(this); + + if(min(this.absmax.x - this.absmin.x, this.absmax.y - this.absmin.y) > 100) + return; + + entity tracetest_ent = spawn(); + setsize(tracetest_ent, PL_MIN_CONST, PL_MAX_CONST); + tracetest_ent.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP; + + vector top_min = (this.absmin + this.absmax) / 2; + top_min.z = this.absmax.z; + vector top_max = top_min; + top_max.z += PL_MAX_CONST.z - PL_MIN_CONST.z; + tracebox(top_max + jumpstepheightvec, PL_MIN_CONST, PL_MAX_CONST, top_min, MOVE_NOMONSTERS, tracetest_ent); + if(trace_startsolid) + { + tracebox(top_max + stepheightvec, PL_MIN_CONST, PL_MAX_CONST, top_min, MOVE_NOMONSTERS, tracetest_ent); + if(trace_startsolid) + { + tracebox(top_max, PL_MIN_CONST, PL_MAX_CONST, top_min, MOVE_NOMONSTERS, tracetest_ent); + if(trace_startsolid) + { + if(this.absmax.x - this.absmin.x > PL_MAX_CONST.x - PL_MIN_CONST.x + && this.absmax.y - this.absmin.y < this.absmax.x - this.absmin.x) + { + // move top on one side + top_max.y = top_min.y = this.absmin.y + (PL_MAX_CONST.y - PL_MIN_CONST.y) * 0.75; + } + else if(this.absmax.y - this.absmin.y > PL_MAX_CONST.y - PL_MIN_CONST.y + && this.absmax.x - this.absmin.x < this.absmax.y - this.absmin.y) + { + // move top on one side + top_max.x = top_min.x = this.absmin.x + (PL_MAX_CONST.x - PL_MIN_CONST.x) * 0.75; + } + tracebox(top_max, PL_MIN_CONST, PL_MAX_CONST, top_min, MOVE_NOMONSTERS, tracetest_ent); + if(trace_startsolid) + { + if(this.absmax.x - this.absmin.x > PL_MAX_CONST.x - PL_MIN_CONST.x + && this.absmax.y - this.absmin.y < this.absmax.x - this.absmin.x) + { + // alternatively on the other side + top_max.y = top_min.y = this.absmax.y - (PL_MAX_CONST.y - PL_MIN_CONST.y) * 0.75; + } + else if(this.absmax.y - this.absmin.y > PL_MAX_CONST.y - PL_MIN_CONST.y + && this.absmax.x - this.absmin.x < this.absmax.y - this.absmin.y) + { + // alternatively on the other side + top_max.x = top_min.x = this.absmax.x - (PL_MAX_CONST.x - PL_MIN_CONST.x) * 0.75; + } + tracebox(top_max, PL_MIN_CONST, PL_MAX_CONST, top_min, MOVE_NOMONSTERS, tracetest_ent); + } + } + } + } + if(trace_startsolid || trace_endpos.z < this.absmax.z) + { + delete(tracetest_ent); + return; + } + + this.bot_pickup = true; // allow bots to make use of this ladder + float cost = waypoint_getlinearcost(trace_endpos.z - this.absmin.z); + top_min = trace_endpos; + waypoint_spawnforteleporter_boxes(this, WAYPOINTFLAG_LADDER, this.absmin, this.absmax, top_min, top_min, cost); } spawnfunc(func_ladder) diff --git a/qcsrc/common/triggers/teleporters.qc b/qcsrc/common/triggers/teleporters.qc index e1cd95058d..5aedf30214 100644 --- a/qcsrc/common/triggers/teleporters.qc +++ b/qcsrc/common/triggers/teleporters.qc @@ -165,6 +165,7 @@ void TeleportPlayer(entity teleporter, entity player, vector to, vector to_angle } player.lastteleporttime = time; + player.lastteleport_origin = from; } #endif } @@ -236,7 +237,13 @@ void teleport_findtarget(entity this) ++n; #ifdef SVQC if(e.move_movetype == MOVETYPE_NONE) - waypoint_spawnforteleporter(this, e.origin, 0); + { + entity tracetest_ent = spawn(); + setsize(tracetest_ent, PL_MIN_CONST, PL_MAX_CONST); + tracetest_ent.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP; + waypoint_spawnforteleporter(this, e.origin, 0, tracetest_ent); + delete(tracetest_ent); + } if(e.classname != "info_teleport_destination") LOG_INFO("^3MAPPER ERROR: teleporter does target an invalid teleport destination entity. Angles will not work."); #endif @@ -304,7 +311,5 @@ void WarpZone_PostTeleportPlayer_Callback(entity pl) #ifdef SVQC pl.oldvelocity = pl.velocity; #endif - // reset teleport time tracking too (or multijump can cause insane speeds) - pl.lastteleporttime = time; } } diff --git a/qcsrc/common/triggers/teleporters.qh b/qcsrc/common/triggers/teleporters.qh index d7faaeff80..6f5b2b595d 100644 --- a/qcsrc/common/triggers/teleporters.qh +++ b/qcsrc/common/triggers/teleporters.qh @@ -67,5 +67,4 @@ void WarpZone_PostTeleportPlayer_Callback(entity pl); #ifdef CSQC .entity realowner; -.float lastteleporttime; #endif diff --git a/qcsrc/common/triggers/trigger/jumppads.qc b/qcsrc/common/triggers/trigger/jumppads.qc index 4316a0edf3..9e40cfd401 100644 --- a/qcsrc/common/triggers/trigger/jumppads.qc +++ b/qcsrc/common/triggers/trigger/jumppads.qc @@ -28,10 +28,7 @@ REGISTER_NET_LINKED(ENT_CLIENT_TARGET_PUSH) pushed_entity - object that is to be pushed Returns: velocity for the jump - the global trigger_push_calculatevelocity_flighttime is set to the total - jump time */ - vector trigger_push_calculatevelocity(vector org, entity tgt, float ht, entity pushed_entity) { float grav, sdist, zdist, vs, vz, jumpheight; @@ -89,6 +86,7 @@ vector trigger_push_calculatevelocity(vector org, entity tgt, float ht, entity p if(zdist == 0) solution_x = solution.y; // solution_x is 0 in this case, so don't use it, but rather use solution_y (which will be sqrt(0.5 * jumpheight / grav), actually) + float flighttime; if(zdist < 0) { // down-jump @@ -97,14 +95,14 @@ vector trigger_push_calculatevelocity(vector org, entity tgt, float ht, entity p // almost straight line type // jump apex is before the jump // we must take the larger one - trigger_push_calculatevelocity_flighttime = solution.y; + flighttime = solution.y; } else { // regular jump // jump apex is during the jump // we must take the larger one too - trigger_push_calculatevelocity_flighttime = solution.y; + flighttime = solution.y; } } else @@ -115,17 +113,17 @@ vector trigger_push_calculatevelocity(vector org, entity tgt, float ht, entity p // almost straight line type // jump apex is after the jump // we must take the smaller one - trigger_push_calculatevelocity_flighttime = solution.x; + flighttime = solution.x; } else { // regular jump // jump apex is during the jump // we must take the larger one - trigger_push_calculatevelocity_flighttime = solution.y; + flighttime = solution.y; } } - vs = sdist / trigger_push_calculatevelocity_flighttime; + vs = sdist / flighttime; // finally calculate the velocity return sdir * vs + '0 0 1' * vz; @@ -209,13 +207,16 @@ bool jumppad_push(entity this, entity targ) centerprint(targ, this.message); } else + { targ.lastteleporttime = time; + targ.lastteleport_origin = targ.origin; + } if (!IS_DEAD(targ)) 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; @@ -273,38 +274,152 @@ void trigger_push_touch(entity this, entity toucher) #ifdef SVQC void trigger_push_link(entity this); void trigger_push_updatelink(entity this); +bool trigger_push_testorigin(entity tracetest_ent, entity targ, entity jp, vector org) +{ + setorigin(tracetest_ent, org); + tracetoss(tracetest_ent, tracetest_ent); + if(trace_startsolid) + return false; + + if (!jp.height) + { + // since tracetoss starting from jumppad's origin often fails when target + // is very close to real destination, start it directly from target's + // origin instead + vector ofs = '0 0 0'; + if (vdist(vec2(tracetest_ent.velocity), <, autocvar_sv_maxspeed)) + ofs = stepheightvec; + + tracetest_ent.velocity.z = 0; + setorigin(tracetest_ent, targ.origin + ofs); + tracetoss(tracetest_ent, tracetest_ent); + if (trace_startsolid && ofs.z) + { + setorigin(tracetest_ent, targ.origin + ofs / 2); + tracetoss(tracetest_ent, tracetest_ent); + if (trace_startsolid && ofs.z) + { + setorigin(tracetest_ent, targ.origin); + tracetoss(tracetest_ent, tracetest_ent); + if (trace_startsolid) + return false; + } + } + } + tracebox(trace_endpos, tracetest_ent.mins, tracetest_ent.maxs, trace_endpos - eZ * 1500, true, tracetest_ent); + 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; - org.z = this.absmax.z - PL_MIN_CONST.z; + org.z = this.absmax.z - PL_MIN_CONST.z - 10; if (this.target) { int n = 0; +#ifdef SVQC + vector vel = '0 0 0'; +#endif for(entity t = NULL; (t = find(t, targetname, this.target)); ) { ++n; #ifdef SVQC + if(t.move_movetype != MOVETYPE_NONE) + continue; + entity e = spawn(); - setorigin(e, org); 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, e); - tracetoss(e, e); - if(e.move_movetype == MOVETYPE_NONE) - waypoint_spawnforteleporter(this, trace_endpos, vlen(trace_endpos - org) / vlen(e.velocity)); + + 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'; + vector best_vel = '0 0 0'; + bool valid_best_target = false; + if (trigger_push_testorigin(e, t, this, org)) + { + best_target = trace_endpos; + best_org = org; + best_vel = e.velocity; + valid_best_target = true; + } + + vector new_org; + vector dist = t.origin - org; + if (dist.x || dist.y) // if not perfectly vertical + { + // test trajectory with different starting points, sometimes the trajectory + // starting from the jumppad origin can't reach the real destination + // and destination waypoint ends up near the jumppad itself + vector flatdir = normalize(dist - eZ * dist.z); + vector ofs = flatdir * 0.5 * min(fabs(this.absmax.x - this.absmin.x), fabs(this.absmax.y - this.absmin.y)); + new_org = org + ofs; + e.velocity = trigger_push_calculatevelocity(new_org, t, this.height, e); + vel = e.velocity; + if (vdist(vec2(e.velocity), <, autocvar_sv_maxspeed)) + e.velocity = autocvar_sv_maxspeed * flatdir; + if (trigger_push_testorigin(e, t, this, new_org) && (!valid_best_target || trace_endpos.z > best_target.z + 50)) + { + best_target = trace_endpos; + best_org = new_org; + best_vel = vel; + valid_best_target = true; + } + new_org = org - ofs; + e.velocity = trigger_push_calculatevelocity(new_org, t, this.height, e); + vel = e.velocity; + if (vdist(vec2(e.velocity), <, autocvar_sv_maxspeed)) + e.velocity = autocvar_sv_maxspeed * flatdir; + if (trigger_push_testorigin(e, t, this, new_org) && (!valid_best_target || trace_endpos.z > best_target.z + 50)) + { + best_target = trace_endpos; + best_org = new_org; + best_vel = vel; + valid_best_target = true; + } + } + + if (valid_best_target) + { + if (!(boxesoverlap(this.absmin, this.absmax + eZ * 50, best_target + PL_MIN_CONST, best_target + PL_MAX_CONST))) + { + float velxy = vlen(vec2(best_vel)); + float cost = vlen(vec2(t.origin - best_org)) / velxy; + if(velxy < autocvar_sv_maxspeed) + velxy = autocvar_sv_maxspeed; + cost += vlen(vec2(best_target - t.origin)) / velxy; + waypoint_spawnforteleporter(this, best_target, cost, e); + } + } delete(e); #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) { @@ -321,16 +436,30 @@ void trigger_push_findtarget(entity this) else { entity e = spawn(); - setorigin(e, org); setsize(e, PL_MIN_CONST, PL_MAX_CONST); + e.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP; + setorigin(e, org); e.velocity = this.movedir; tracetoss(e, e); - waypoint_spawnforteleporter(this, trace_endpos, vlen(trace_endpos - org) / vlen(e.velocity)); + 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 @@ -395,6 +524,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); } diff --git a/qcsrc/common/triggers/trigger/jumppads.qh b/qcsrc/common/triggers/trigger/jumppads.qh index 8615bc66d6..50ed0a343c 100644 --- a/qcsrc/common/triggers/trigger/jumppads.qh +++ b/qcsrc/common/triggers/trigger/jumppads.qh @@ -1,5 +1,8 @@ #pragma once +IntrusiveList g_jumppads; +STATIC_INIT(g_jumppads) { g_jumppads = IL_NEW(); } + const float PUSH_ONCE = 1; const float PUSH_SILENT = 2; @@ -11,11 +14,10 @@ const int NUM_JUMPPADSUSED = 3; .float jumppadcount; .entity jumppadsused[NUM_JUMPPADSUSED]; -float trigger_push_calculatevelocity_flighttime; - #ifdef SVQC void SUB_UseTargets(entity this, entity actor, entity trigger); void trigger_push_use(entity this, entity actor, entity trigger); +bool trigger_push_testorigin(entity tracetest_ent, entity targ, entity jp, vector org); #endif /* @@ -29,15 +31,13 @@ void trigger_push_use(entity this, entity actor, entity trigger); pushed_entity - object that is to be pushed Returns: velocity for the jump - the global trigger_push_calculatevelocity_flighttime is set to the total - jump time */ - vector trigger_push_calculatevelocity(vector org, entity tgt, float ht, entity pushed_entity); void trigger_push_touch(entity this, entity toucher); .vector dest; +bool trigger_push_test(entity this, entity item); void trigger_push_findtarget(entity this); /* diff --git a/qcsrc/ecs/systems/physics.qc b/qcsrc/ecs/systems/physics.qc index e6c2f7ccd8..ec97bb2a15 100644 --- a/qcsrc/ecs/systems/physics.qc +++ b/qcsrc/ecs/systems/physics.qc @@ -108,6 +108,7 @@ void sys_phys_update(entity this, float dt) this.com_phys_water = true; sys_phys_simulate(this, dt); this.com_phys_water = false; + this.jumppadcount = 0; } else if (time < this.ladder_time) { this.com_phys_friction = PHYS_FRICTION(this); this.com_phys_vel_max = PHYS_MAXSPEED(this) * maxspeed_mod; diff --git a/qcsrc/lib/warpzone/server.qc b/qcsrc/lib/warpzone/server.qc index 6db6122352..936d075da8 100644 --- a/qcsrc/lib/warpzone/server.qc +++ b/qcsrc/lib/warpzone/server.qc @@ -37,6 +37,10 @@ void WarpZone_TeleportPlayer(entity teleporter, entity player, vector to, vector to_angles, vector to_velocity) { +#ifdef SVQC + player.lastteleport_origin = player.origin; + player.lastteleporttime = time; +#endif setorigin(player, to); // NOTE: this also aborts the move, when this is called by touch #ifdef SVQC player.oldorigin = to; // for DP's unsticking diff --git a/qcsrc/server/autocvars.qh b/qcsrc/server/autocvars.qh index b71d4459d1..4b3745dc00 100644 --- a/qcsrc/server/autocvars.qh +++ b/qcsrc/server/autocvars.qh @@ -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; diff --git a/qcsrc/server/bot/api.qh b/qcsrc/server/bot/api.qh index f33cc4f264..51ac148fc1 100644 --- a/qcsrc/server/bot/api.qh +++ b/qcsrc/server/bot/api.qh @@ -2,15 +2,17 @@ #include #include +#include const int WAYPOINTFLAG_GENERATED = BIT(23); const int WAYPOINTFLAG_ITEM = BIT(22); -const int WAYPOINTFLAG_TELEPORT = BIT(21); +const int WAYPOINTFLAG_TELEPORT = BIT(21); // teleports, warpzones and jumppads const int WAYPOINTFLAG_NORELINK = BIT(20); const int WAYPOINTFLAG_PERSONAL = BIT(19); const int WAYPOINTFLAG_PROTECTED = BIT(18); // Useless WP detection never kills these. const int WAYPOINTFLAG_USEFUL = BIT(17); // Useless WP detection temporary flag. const int WAYPOINTFLAG_DEAD_END = BIT(16); // Useless WP detection temporary flag. +const int WAYPOINTFLAG_LADDER = BIT(15); entity kh_worldkeylist; .entity kh_worldkeynext; @@ -21,6 +23,7 @@ float bot_weapons_far[Weapons_MAX]; float bot_weapons_mid[Weapons_MAX]; float skill; +.float bot_tracewalk_time; .float bot_attack; .float bot_dodgerating; .float bot_dodge; @@ -28,11 +31,13 @@ float skill; .float bot_moveskill; // moving technique .float bot_pickup; .float(entity player, entity item) bot_pickupevalfunc; -.float bot_strategytime; .string cleanname; .float havocbot_role_timeout; +.void(entity this) havocbot_role; +.void(entity this) havocbot_previous_role; .float isbot; // true if this client is actually a bot .float lastteleporttime; +.vector lastteleport_origin; .float navigation_hasgoals; .float nearestwaypointtimeout; .entity nearestwaypoint; @@ -70,10 +75,16 @@ void havocbot_goalrating_enemyplayers(entity this, float ratingscale, vector org void havocbot_goalrating_items(entity this, float ratingscale, vector org, float sradius); void havocbot_goalrating_waypoints(entity this, float ratingscale, vector org, float sradius); +bool havocbot_goalrating_item_pickable_check_players(entity this, vector org, entity item, vector item_org); + vector havocbot_middlepoint; float havocbot_middlepoint_radius; vector havocbot_symmetryaxis_equation; +.float goalentity_lock_timeout; +.float ignoregoaltime; +.entity ignoregoal; + .entity bot_basewaypoint; .bool navigation_dynamicgoal; void navigation_dynamicgoal_init(entity this, bool initially_static); @@ -82,21 +93,30 @@ void navigation_dynamicgoal_unset(entity this); entity navigation_findnearestwaypoint(entity ent, float walkfromwp); void navigation_goalrating_end(entity this); void navigation_goalrating_start(entity this); +void navigation_goalrating_timeout_set(entity this); +void navigation_goalrating_timeout_force(entity this); +void navigation_goalrating_timeout_expire(entity this, float seconds); +bool navigation_goalrating_timeout(entity this); +bool navigation_goalrating_timeout_can_be_anticipated(entity this); void navigation_markroutes(entity this, entity fixed_source_waypoint); void navigation_markroutes_inverted(entity fixed_source_waypoint); void navigation_routerating(entity this, entity e, float f, float rangebias); -bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float movemode); +vector get_closer_dest(entity ent, vector org); -void waypoint_remove(entity e); +void set_tracewalk_dest(entity ent, vector org, bool fix_player_dest); +vector set_tracewalk_dest_2(entity ent, vector org); +bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float end_height, float movemode); + +void waypoint_remove_fromeditor(entity pl); +void waypoint_remove(entity wp); void waypoint_saveall(); void waypoint_schedulerelinkall(); void waypoint_schedulerelink(entity wp); void waypoint_spawnforitem(entity e); void waypoint_spawnforitem_force(entity e, vector org); -void waypoint_spawnforteleporter(entity e, vector destination, float timetaken); -void waypoint_spawnforteleporter_v(entity e, vector org, vector destination, float timetaken); +void waypoint_spawnforteleporter(entity e, vector destination, float timetaken, entity tracetest_ent); +void waypoint_spawnforteleporter_wz(entity e, vector org, vector destination, float timetaken, vector down_dir, entity tracetest_ent); +void waypoint_spawn_fromeditor(entity pl); entity waypoint_spawn(vector m1, vector m2, float f); - -.entity goalcurrent; -void navigation_clearroute(entity this); +void waypoint_unreachable(entity pl); diff --git a/qcsrc/server/bot/default/aim.qc b/qcsrc/server/bot/default/aim.qc index 8f2abb3f82..7441ce7bca 100644 --- a/qcsrc/server/bot/default/aim.qc +++ b/qcsrc/server/bot/default/aim.qc @@ -174,7 +174,7 @@ void bot_lagfunc(entity this, float t, float f1, float f2, entity e1, vector v1, this.bot_canfire = 1; } -float bot_aimdir(entity this, vector v, float maxfiredeviation) +void bot_aimdir(entity this, vector v, float maxfiredeviation) { float dist, delta_t, blend; vector desiredang, diffang; @@ -184,6 +184,9 @@ float bot_aimdir(entity this, vector v, float maxfiredeviation) this.v_angle_y = this.v_angle.y - floor(this.v_angle.y / 360) * 360; this.v_angle_z = 0; + // invalid aim dir (can happen when bot overlaps target) + if(!v) return; + // get the desired angles to aim at //dprint(" at:", vtos(v)); v = normalize(v); @@ -314,7 +317,7 @@ float bot_aimdir(entity this, vector v, float maxfiredeviation) //dprint(ftos(maxfiredeviation),"\n"); //dprint(" diff:", vtos(diffang), "\n"); - return this.bot_canfire && (time < this.bot_firetimer); + //return this.bot_canfire && (time < this.bot_firetimer); } vector bot_shotlead(vector targorigin, vector targvelocity, float shotspeed, float shotdelay) @@ -325,7 +328,7 @@ vector bot_shotlead(vector targorigin, vector targvelocity, float shotspeed, flo bool bot_aim(entity this, .entity weaponentity, float shotspeed, float shotspeedupward, float maxshottime, bool applygravity) { - float f, r, hf, distanceratio; + float r, hf, distanceratio; vector v; /* eprint(this); @@ -367,11 +370,11 @@ bool bot_aim(entity this, .entity weaponentity, float shotspeed, float shotspeed return false; } - f = bot_aimdir(this, findtrajectory_velocity - shotspeedupward * '0 0 1', r); + bot_aimdir(this, findtrajectory_velocity - shotspeedupward * '0 0 1', r); } else { - f = bot_aimdir(this, v - shotorg, r); + bot_aimdir(this, v - shotorg, r); //dprint("AIM: ");dprint(vtos(this.bot_aimtargorigin));dprint(" + ");dprint(vtos(this.bot_aimtargvelocity));dprint(" * ");dprint(ftos(this.bot_aimlatency + vlen(this.bot_aimtargorigin - shotorg) / shotspeed));dprint(" = ");dprint(vtos(v));dprint(" : aimdir = ");dprint(vtos(normalize(v - shotorg)));dprint(" : ");dprint(vtos(shotdir));dprint("\n"); //traceline(shotorg, shotorg + shotdir * 10000, false, this); //if (trace_ent.takedamage) diff --git a/qcsrc/server/bot/default/aim.qh b/qcsrc/server/bot/default/aim.qh index e7c60758ae..b8b35f1c20 100644 --- a/qcsrc/server/bot/default/aim.qh +++ b/qcsrc/server/bot/default/aim.qh @@ -90,7 +90,7 @@ void lag_update(entity this); void bot_lagfunc(entity this, float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4); float bot_shouldattack(entity this, entity targ); -float bot_aimdir(entity this, vector v, float maxfiredeviation); +void bot_aimdir(entity this, vector v, float maxfiredeviation); bool bot_aim(entity this, .entity weaponentity, float shotspeed, float shotspeedupward, float maxshottime, bool applygravity); float findtrajectorywithleading(vector org, vector m1, vector m2, entity targ, float shotspeed, float shotspeedupward, float maxtime, float shotdelay, entity ignore); diff --git a/qcsrc/server/bot/default/bot.qc b/qcsrc/server/bot/default/bot.qc index 6ea375c087..af238029ab 100644 --- a/qcsrc/server/bot/default/bot.qc +++ b/qcsrc/server/bot/default/bot.qc @@ -133,7 +133,7 @@ void bot_think(entity this) if (this.deadflag == DEAD_DEAD) { PHYS_INPUT_BUTTON_JUMP(this) = true; // press jump to respawn - this.bot_strategytime = 0; + navigation_goalrating_timeout_force(this); } } else if(this.aistatus & AI_STATUS_STUCK) @@ -579,9 +579,8 @@ void autoskill(float factor) void bot_calculate_stepheightvec() { stepheightvec = autocvar_sv_stepheight * '0 0 1'; - jumpstepheightvec = stepheightvec + - ((autocvar_sv_jumpvelocity * autocvar_sv_jumpvelocity) / (2 * autocvar_sv_gravity)) * '0 0 0.85'; - // 0.75 factor is for safety to make the jumps easy + jumpheight_vec = (autocvar_sv_jumpvelocity ** 2) / (2 * autocvar_sv_gravity) * '0 0 1'; + jumpstepheightvec = stepheightvec + jumpheight_vec * 0.85; // reduce it a bit to make the jumps easy } float bot_fixcount() @@ -699,6 +698,19 @@ void bot_serverframe() return; } + if(autocvar_skill != skill) + { + float wpcost_update = false; + if(skill >= autocvar_bot_ai_bunnyhop_skilloffset && autocvar_skill < autocvar_bot_ai_bunnyhop_skilloffset) + wpcost_update = true; + if(skill < autocvar_bot_ai_bunnyhop_skilloffset && autocvar_skill >= autocvar_bot_ai_bunnyhop_skilloffset) + wpcost_update = true; + + skill = autocvar_skill; + if (wpcost_update) + waypoint_updatecost_foralllinks(); + } + bot_calculate_stepheightvec(); bot_navigation_movemode = ((autocvar_bot_navigation_ignoreplayers) ? MOVE_NOMONSTERS : MOVE_NORMAL); @@ -747,8 +759,7 @@ void bot_serverframe() { botframe_spawnedwaypoints = true; waypoint_loadall(); - if(!waypoint_load_links()) - waypoint_schedulerelinkall(); + waypoint_load_links(); } if (bot_list) @@ -758,11 +769,26 @@ void bot_serverframe() // frame, which causes choppy framerates) if (bot_strategytoken_taken) { + // give goal token to the first bot without goals; if all bots don't have + // any goal (or are dead/frozen) simply give it to the next one bot_strategytoken_taken = false; - if (bot_strategytoken) - bot_strategytoken = bot_strategytoken.nextbot; - if (!bot_strategytoken) - bot_strategytoken = bot_list; + entity bot_strategytoken_save = bot_strategytoken; + while (true) + { + if (bot_strategytoken) + bot_strategytoken = bot_strategytoken.nextbot; + if (!bot_strategytoken) + bot_strategytoken = bot_list; + + if (!(IS_DEAD(bot_strategytoken) || STAT(FROZEN, bot_strategytoken)) + && !bot_strategytoken.goalcurrent) + break; + + if (!bot_strategytoken_save) // break loop if all the bots are dead or frozen + break; + if (bot_strategytoken == bot_strategytoken_save) + bot_strategytoken_save = NULL; // looped through all the bots + } } if (botframe_nextdangertime < time) diff --git a/qcsrc/server/bot/default/bot.qh b/qcsrc/server/bot/default/bot.qh index b72fad9bd6..ea37ccf8ff 100644 --- a/qcsrc/server/bot/default/bot.qh +++ b/qcsrc/server/bot/default/bot.qh @@ -78,6 +78,12 @@ float botframe_nextdangertime; float bot_cvar_nextthink; float bot_ignore_bots; // let bots not attack other bots (only works in non-teamplay) +int _content_type; +#define IN_LAVA(pos) (_content_type = pointcontents(pos), (_content_type == CONTENT_LAVA || _content_type == CONTENT_SLIME)) +#define IN_LIQUID(pos) (_content_type = pointcontents(pos), (_content_type == CONTENT_WATER || _content_type == CONTENT_LAVA || _content_type == CONTENT_SLIME)) +#define SUBMERGED(pos) IN_LIQUID(pos + autocvar_sv_player_viewoffset) +#define WETFEET(pos) IN_LIQUID(pos + eZ * (m1.z + 1)) + /* * Functions */ diff --git a/qcsrc/server/bot/default/havocbot/havocbot.qc b/qcsrc/server/bot/default/havocbot/havocbot.qc index 46acf88287..2a0d0c8503 100644 --- a/qcsrc/server/bot/default/havocbot/havocbot.qc +++ b/qcsrc/server/bot/default/havocbot/havocbot.qc @@ -33,8 +33,7 @@ void havocbot_ai(entity this) if(bot_execute_commands(this)) return; - if (bot_strategytoken == this) - if (!bot_strategytoken_taken) + if (bot_strategytoken == this && !bot_strategytoken_taken) { if(this.havocbot_blockhead) { @@ -46,7 +45,6 @@ void havocbot_ai(entity this) this.havocbot_role(this); // little too far down the rabbit hole } - // TODO: tracewalk() should take care of this job (better path finding under water) // if we don't have a goal and we're under water look for a waypoint near the "shore" and push it if(!(IS_DEAD(this) || STAT(FROZEN, this))) if(!this.goalcurrent) @@ -86,7 +84,11 @@ void havocbot_ai(entity this) } if(IS_DEAD(this) || STAT(FROZEN, this)) + { + if (this.goalcurrent) + navigation_clearroute(this); return; + } havocbot_chooseenemy(this); @@ -139,11 +141,23 @@ void havocbot_ai(entity this) this.aistatus |= AI_STATUS_ROAMING; this.aistatus &= ~AI_STATUS_ATTACKING; - vector now,v,next;//,heading; + vector now, next; float aimdistance,skillblend,distanceblend,blend; - next = now = ( (this.goalcurrent.absmin + this.goalcurrent.absmax) * 0.5) - (this.origin + this.view_ofs); + + vector v = get_closer_dest(this.goalcurrent, this.origin); + if(this.goalcurrent.wpisbox) + { + // avoid a glitch when bot is teleported but teleport waypoint isn't removed yet + if(this.goalstack02 && this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT + && this.lastteleporttime > 0 && time - this.lastteleporttime < 0.15) + v = (this.goalstack02.absmin + this.goalstack02.absmax) * 0.5; + // aim to teleport origin if bot is inside teleport waypoint but hasn't touched the real teleport yet + else if(boxesoverlap(this.goalcurrent.absmin, this.goalcurrent.absmax, this.origin, this.origin)) + v = this.goalcurrent.origin; + } + next = now = v - (this.origin + this.view_ofs); aimdistance = vlen(now); - //heading = this.velocity; + //dprint(this.goalstack01.classname,etos(this.goalstack01),"\n"); if( this.goalstack01 != this && this.goalstack01 && !wasfreed(this.goalstack01) && ((this.aistatus & AI_STATUS_RUNNING) == 0) && @@ -275,7 +289,6 @@ void havocbot_bunnyhop(entity this, vector dir) float bunnyhopdistance; vector deviation; float maxspeed; - vector gco, gno; // Don't jump when attacking if(this.aistatus & AI_STATUS_ATTACKING) @@ -308,12 +321,12 @@ void havocbot_bunnyhop(entity this, vector dir) this.bot_timelastseengoal = 0; } - gco = (this.goalcurrent.absmin + this.goalcurrent.absmax) * 0.5; + vector gco = get_closer_dest(this.goalcurrent, this.origin); bunnyhopdistance = vlen(this.origin - gco); // Run only to visible goals if(IS_ONGROUND(this)) - if(vlen(this.velocity - eZ * this.velocity.z) >= autocvar_sv_maxspeed) // if -really- running + if(vdist(vec2(this.velocity), >=, autocvar_sv_maxspeed)) // if -really- running if(checkpvs(this.origin + this.view_ofs, this.goalcurrent)) { this.bot_lastseengoal = this.goalcurrent; @@ -345,7 +358,7 @@ void havocbot_bunnyhop(entity this, vector dir) if(fabs(gco.z - this.origin.z) < this.maxs.z - this.mins.z) if(this.goalstack01 && !wasfreed(this.goalstack01)) { - gno = (this.goalstack01.absmin + this.goalstack01.absmax) * 0.5; + vector gno = (this.goalstack01.absmin + this.goalstack01.absmax) * 0.5; deviation = vectoangles(gno - this.origin) - vectoangles(gco - this.origin); while (deviation.y < -180) deviation.y = deviation.y + 360; while (deviation.y > 180) deviation.y = deviation.y - 360; @@ -415,21 +428,67 @@ void havocbot_bunnyhop(entity this, vector dir) #endif } -.entity goalcurrent_prev; -.float goalcurrent_distance; -.float goalcurrent_distance_time; +// return true when bot isn't getting closer to the current goal +bool havocbot_checkgoaldistance(entity this, vector gco) +{ + float curr_dist_z = max(20, fabs(this.origin.z - gco.z)); + float curr_dist_2d = max(20, vlen(vec2(this.origin - gco))); + float distance_time = this.goalcurrent_distance_time; + if(distance_time < 0) + distance_time = -distance_time; + if(curr_dist_z >= this.goalcurrent_distance_z && curr_dist_2d >= this.goalcurrent_distance_2d) + { + if(!distance_time) + this.goalcurrent_distance_time = time; + else if (time - distance_time > 0.5) + return true; + } + else + { + // reduce it a little bit so it works even with very small approaches to the goal + this.goalcurrent_distance_z = max(20, curr_dist_z - 10); + this.goalcurrent_distance_2d = max(20, curr_dist_2d - 10); + this.goalcurrent_distance_time = 0; + } + return false; +} + +entity havocbot_select_an_item_of_group(entity this, int gr) +{ + entity selected = NULL; + float selected_dist2 = 0; + // select farthest item of this group from bot's position + IL_EACH(g_items, it.item_group == gr && it.solid, + { + float dist2 = vlen2(this.origin - it.origin); + if (dist2 < 600 ** 2 && dist2 > selected_dist2) + { + selected = it; + selected_dist2 = vlen2(this.origin - selected.origin); + } + }); + + if (!selected) + return NULL; + + set_tracewalk_dest(selected, 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 selected; +} + void havocbot_movetogoal(entity this) { - vector destorg; vector diff; vector dir; vector flatdir; - vector m1; - vector m2; vector evadeobstacle; vector evadelava; float maxspeed; - vector gco; //float dist; vector dodge; //if (this.goalentity) @@ -437,8 +496,8 @@ void havocbot_movetogoal(entity this) CS(this).movement = '0 0 0'; maxspeed = autocvar_sv_maxspeed; + PHYS_INPUT_BUTTON_JETPACK(this) = false; // Jetpack navigation - if(this.goalcurrent) if(this.navigation_jetpack_goal) if(this.goalcurrent==this.navigation_jetpack_goal) if(this.ammo_fuel) @@ -465,18 +524,14 @@ void havocbot_movetogoal(entity this) if(this.aistatus & AI_STATUS_JETPACK_LANDING) { // Calculate brake distance in xy - float db, v, d; - vector dxy; - - dxy = this.origin - ( ( this.goalcurrent.absmin + this.goalcurrent.absmax ) * 0.5 ); dxy.z = 0; - d = vlen(dxy); - v = vlen(this.velocity - this.velocity.z * '0 0 1'); - db = ((v ** 2) / (autocvar_g_jetpack_acceleration_side * 2)) + 100; - // dprint("distance ", ftos(ceil(d)), " velocity ", ftos(ceil(v)), " brake at ", ftos(ceil(db)), "\n"); + float d = vlen(vec2(this.origin - (this.goalcurrent.absmin + this.goalcurrent.absmax) * 0.5)); + float v = vlen(vec2(this.velocity)); + float db = ((v ** 2) / (autocvar_g_jetpack_acceleration_side * 2)) + 100; + //LOG_INFOF("distance %d, velocity %d, brake at %d ", ceil(d), ceil(v), ceil(db)); if(d < db || d < 500) { // Brake - if(fabs(this.velocity.x)>maxspeed*0.3) + if(v > maxspeed * 0.3) { CS(this).movement_x = dir * v_forward * -maxspeed; return; @@ -497,7 +552,7 @@ void havocbot_movetogoal(entity this) } // Flying - PHYS_INPUT_BUTTON_HOOK(this) = true; + PHYS_INPUT_BUTTON_JETPACK(this) = true; if(this.navigation_jetpack_point.z - STAT(PL_MAX, this).z + STAT(PL_MIN, this).z < this.origin.z) { CS(this).movement_x = dir * v_forward * maxspeed; @@ -509,17 +564,25 @@ void havocbot_movetogoal(entity this) // Handling of jump pads if(this.jumppadcount) { - // If got stuck on the jump pad try to reach the farthest visible waypoint - // but with some randomness so it can try out different paths - if(this.aistatus & AI_STATUS_OUT_JUMPPAD) + if(this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT) + { + this.aistatus |= AI_STATUS_OUT_JUMPPAD; + navigation_poptouchedgoals(this); + return; + } + else if(this.aistatus & AI_STATUS_OUT_JUMPPAD) { - if(fabs(this.velocity.z)<50) + // If got stuck on the jump pad try to reach the farthest visible waypoint + // but with some randomness so it can try out different paths + if(!this.goalcurrent) { entity newgoal = NULL; - if (vdist(this.origin - this.goalcurrent.origin, <, 150)) - this.aistatus &= ~AI_STATUS_OUT_JUMPPAD; - else IL_EACH(g_waypoints, vdist(it.origin - this.origin, <=, 1000), + IL_EACH(g_waypoints, vdist(it.origin - this.origin, <=, 1000), { + if(it.wpflags & WAYPOINTFLAG_TELEPORT) + if(it.origin.z < this.origin.z - 100 && vdist(vec2(it.origin - this.origin), <, 100)) + continue; + traceline(this.origin + this.view_ofs, ((it.absmin + it.absmax) * 0.5), true, this); if(trace_fraction < 1) @@ -541,11 +604,32 @@ void havocbot_movetogoal(entity this) } } else - return; + { + if (this.goalcurrent.bot_pickup) + { + entity jumppad_wp = this.goalcurrent_prev; + navigation_poptouchedgoals(this); + if(!this.goalcurrent && jumppad_wp.wp00) + { + // head to the jumppad destination once bot reaches the goal item + navigation_pushroute(this, jumppad_wp.wp00); + } + } + vector gco = (this.goalcurrent.absmin + this.goalcurrent.absmax) * 0.5; + if (this.origin.z > gco.z && vdist(vec2(this.velocity), <, autocvar_sv_maxspeed)) + this.aistatus &= ~AI_STATUS_OUT_JUMPPAD; + else if(havocbot_checkgoaldistance(this, gco)) + { + navigation_clearroute(this); + navigation_goalrating_timeout_force(this); + } + else + return; + } } else { - if(time - this.lastteleporttime > 0.3 && this.velocity.z > 0) + if(time - this.lastteleporttime > 0.2 && this.velocity.z > 0) { vector velxy = this.velocity; velxy_z = 0; if(vdist(velxy, <, autocvar_sv_maxspeed * 0.2)) @@ -565,9 +649,12 @@ void havocbot_movetogoal(entity this) this.aistatus &= ~AI_STATUS_OUT_JUMPPAD; // If there is a trigger_hurt right below try to use the jetpack or make a rocketjump - if(skill>6) - if (!(IS_ONGROUND(this))) + if (skill > 6 && !(IS_ONGROUND(this))) { + #define ROCKETJUMP_DAMAGE() WEP_CVAR(devastator, damage) * 0.8 \ + * ((this.strength_finished > time) ? autocvar_g_balance_powerup_strength_selfdamage : 1) \ + * ((this.invincible_finished > time) ? autocvar_g_balance_powerup_invincible_takedamage : 1) + tracebox(this.origin, this.mins, this.maxs, this.origin + '0 0 -65536', MOVE_NOMONSTERS, this); if(tracebox_hits_trigger_hurt(this.origin, this.mins, this.maxs, trace_endpos )) if(this.items & IT_JETPACK) @@ -576,12 +663,10 @@ void havocbot_movetogoal(entity this) if(tracebox_hits_trigger_hurt(this.origin, this.mins, this.maxs, trace_endpos + '0 0 1' )) { if(this.velocity.z<0) - { - PHYS_INPUT_BUTTON_HOOK(this) = true; - } + PHYS_INPUT_BUTTON_JETPACK(this) = true; } else - PHYS_INPUT_BUTTON_HOOK(this) = true; + PHYS_INPUT_BUTTON_JETPACK(this) = true; // If there is no goal try to move forward @@ -610,7 +695,7 @@ void havocbot_movetogoal(entity this) return; } - else if(this.health > WEP_CVAR(devastator, damage) * 0.5 * ((this.strength_finished < time) ? autocvar_g_balance_powerup_strength_selfdamage : 1)) + else if(this.health + this.armorvalue > ROCKETJUMP_DAMAGE()) { if(this.velocity.z < 0) { @@ -653,8 +738,7 @@ void havocbot_movetogoal(entity this) } // If we are under water with no goals, swim up - if(this.waterlevel) - if(this.goalcurrent==NULL) + if(this.waterlevel && !this.goalcurrent) { dir = '0 0 0'; if(this.waterlevel>WATERLEVEL_SWIMMING) @@ -675,12 +759,17 @@ void havocbot_movetogoal(entity this) bool locked_goal = false; - if(this.goalentity && wasfreed(this.goalentity)) + if((this.goalentity && wasfreed(this.goalentity)) + || (this.goalcurrent == this.goalentity && this.goalentity.tag_entity)) { navigation_clearroute(this); - this.bot_strategytime = 0; + navigation_goalrating_timeout_force(this); return; } + else if(this.goalentity.tag_entity) + { + navigation_goalrating_timeout_expire(this, 2); + } else if(this.goalentity.bot_pickup) { if(this.goalentity.bot_pickup_respawning) @@ -689,28 +778,91 @@ void havocbot_movetogoal(entity this) this.goalentity.bot_pickup_respawning = false; else if(time < this.goalentity.scheduledrespawntime - 10) // item already taken (by someone else) { - this.goalentity.bot_pickup_respawning = false; - navigation_clearroute(this); - this.bot_strategytime = 0; - return; + if(checkpvs(this.origin, this.goalentity)) + { + this.goalentity.bot_pickup_respawning = false; + navigation_goalrating_timeout_expire(this, random()); + } + locked_goal = true; // wait for item to respawn } else if(this.goalentity == this.goalcurrent) locked_goal = true; // wait for item to respawn } - else if(!this.goalentity.solid) + else if(!this.goalentity.solid && !boxesoverlap(this.goalentity.absmin, this.goalentity.absmax, this.absmin, this.absmax)) { - navigation_clearroute(this); - this.bot_strategytime = 0; - return; + if(checkpvs(this.origin, this.goalentity)) + { + navigation_goalrating_timeout_expire(this, random()); + } + } + } + if (this.goalcurrent == this.goalentity && this.goalentity_lock_timeout > time) + locked_goal = true; + + navigation_shortenpath(this); + + if (IS_MOVABLE(this.goalcurrent)) + { + if (IS_DEAD(this.goalcurrent)) + { + if (checkpvs(this.origin + this.view_ofs, this.goalcurrent)) + { + navigation_goalrating_timeout_force(this); + return; + } + } + else if (this.bot_tracewalk_time < time) + { + set_tracewalk_dest(this.goalcurrent, this.origin, true); + if (!(trace_ent == this || tracewalk(this, this.origin, this.mins, this.maxs, + tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))) + { + navigation_goalrating_timeout_force(this); + return; + } + this.bot_tracewalk_time = max(time, this.bot_tracewalk_time) + 0.25; } } if(!locked_goal) - navigation_poptouchedgoals(this); + { + // optimize path finding by anticipating goalrating when bot is near a waypoint; + // in this case path finding can start directly from a waypoint instead of + // looking for all the reachable waypoints up to a certain distance + if (navigation_poptouchedgoals(this)) + { + if (this.goalcurrent) + { + if (IS_MOVABLE(this.goalcurrent) && IS_DEAD(this.goalcurrent)) + { + // remove even if not visible + navigation_goalrating_timeout_force(this); + return; + } + else if (navigation_goalrating_timeout_can_be_anticipated(this)) + navigation_goalrating_timeout_force(this); + } + else + { + entity old_goal = this.goalcurrent_prev; + if (old_goal.item_group && this.item_group != old_goal.item_group) + { + // Avoid multiple costly calls of path finding code that selects one of the closest + // item of the group by telling the bot to head directly to the farthest item. + // Next time we let the bot select a goal as usual which can be another item + // of this group (the closest one) and so on + this.item_group = old_goal.item_group; + entity new_goal = havocbot_select_an_item_of_group(this, old_goal.item_group); + if (new_goal) + navigation_pushroute(this, new_goal); + } + } + } + } // if ran out of goals try to use an alternative goal or get a new strategy asap if(this.goalcurrent == NULL) { - this.bot_strategytime = 0; + navigation_goalrating_timeout_force(this); return; } @@ -718,23 +870,37 @@ void havocbot_movetogoal(entity this) if(autocvar_bot_debug_goalstack) debuggoalstack(this); - m1 = this.goalcurrent.origin + this.goalcurrent.mins; - m2 = this.goalcurrent.origin + this.goalcurrent.maxs; - destorg = this.origin; - destorg.x = bound(m1_x, destorg.x, m2_x); - destorg.y = bound(m1_y, destorg.y, m2_y); - destorg.z = bound(m1_z, destorg.z, m2_z); + bool bunnyhop_forbidden = false; + vector destorg = get_closer_dest(this.goalcurrent, this.origin); + + // in case bot ends up inside the teleport waypoint without touching + // the teleport itself, head to the teleport origin + if(this.goalcurrent.wpisbox && boxesoverlap(this.goalcurrent.absmin, this.goalcurrent.absmax, this.origin + eZ * this.mins.z, this.origin + eZ * this.maxs.z)) + { + bunnyhop_forbidden = true; + destorg = this.goalcurrent.origin; + if(destorg.z > this.origin.z) + PHYS_INPUT_BUTTON_JUMP(this) = true; + } + diff = destorg - this.origin; - //dist = vlen(diff); + + if (fabs(diff.x) < 10 && fabs(diff.y) < 10 + && this.goalcurrent == this.goalentity && time < this.goalentity_lock_timeout) + { + destorg = this.origin; + diff.x = 0; + diff.y = 0; + } + dir = normalize(diff); flatdir = diff;flatdir.z = 0; flatdir = normalize(flatdir); - gco = (this.goalcurrent.absmin + this.goalcurrent.absmax) * 0.5; //if (this.bot_dodgevector_time < time) { - // this.bot_dodgevector_time = time + cvar("bot_ai_dodgeupdateinterval"); - // this.bot_dodgevector_jumpbutton = 1; + //this.bot_dodgevector_time = time + cvar("bot_ai_dodgeupdateinterval"); + //this.bot_dodgevector_jumpbutton = 1; evadeobstacle = '0 0 0'; evadelava = '0 0 0'; @@ -744,18 +910,20 @@ void havocbot_movetogoal(entity this) { if(this.waterlevel>WATERLEVEL_SWIMMING) { - // flatdir_z = 1; - this.aistatus |= AI_STATUS_OUT_WATER; + if(!this.goalcurrent) + this.aistatus |= AI_STATUS_OUT_WATER; + else if(destorg.z > this.origin.z) + PHYS_INPUT_BUTTON_JUMP(this) = true; } else { - if(this.velocity.z >= 0 && !(this.watertype == CONTENT_WATER && gco.z < this.origin.z) && + dir = flatdir; + if(this.velocity.z >= 0 && !(this.watertype == CONTENT_WATER && destorg.z < this.origin.z) && ( !(this.waterlevel == WATERLEVEL_WETFEET && this.watertype == CONTENT_WATER) || this.aistatus & AI_STATUS_OUT_WATER)) PHYS_INPUT_BUTTON_JUMP(this) = true; else PHYS_INPUT_BUTTON_JUMP(this) = false; } - dir = normalize(flatdir); } else { @@ -766,62 +934,91 @@ void havocbot_movetogoal(entity this) // jump if going toward an obstacle that doesn't look like stairs we // can walk up directly - offset = (vdist(this.velocity, >, 32) ? this.velocity * 0.2 : v_forward * 32); - tracebox(this.origin, this.mins, this.maxs, this.origin + offset, false, this); + vector deviation = '0 0 0'; + if (this.velocity) + { + deviation = vectoangles(diff) - vectoangles(this.velocity); + while (deviation.y < -180) deviation.y += 360; + while (deviation.y > 180) deviation.y -= 360; + } + vector flat_diff = vec2(diff); + offset = max(32, vlen(vec2(this.velocity)) * cos(deviation.y * DEG2RAD) * 0.2) * flatdir; + vector actual_destorg = this.origin + offset; + if (!this.goalstack01 || this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT) + { + if (vlen2(flat_diff) < vlen2(offset)) + { + actual_destorg.x = destorg.x; + actual_destorg.y = destorg.y; + } + } + else if (vdist(flat_diff, <, 32) && diff.z < -16) // destination is under the bot + { + actual_destorg.x = destorg.x; + actual_destorg.y = destorg.y; + } + else if (vlen2(flat_diff) < vlen2(offset)) + { + vector next_goal_org = (this.goalstack01.absmin + this.goalstack01.absmax) * 0.5; + vector next_dir = normalize(vec2(next_goal_org - destorg)); + float next_dist = vlen(vec2(this.origin + offset - destorg)); + actual_destorg = vec2(destorg) + next_dist * next_dir; + actual_destorg.z = this.origin.z; + } + + tracebox(this.origin, this.mins, this.maxs, actual_destorg, false, this); if (trace_fraction < 1) if (trace_plane_normal.z < 0.7) { s = trace_fraction; - tracebox(this.origin + stepheightvec, this.mins, this.maxs, this.origin + offset + stepheightvec, false, this); + tracebox(this.origin + stepheightvec, this.mins, this.maxs, actual_destorg + stepheightvec, false, this); if (trace_fraction < s + 0.01) if (trace_plane_normal.z < 0.7) { s = trace_fraction; - tracebox(this.origin + jumpstepheightvec, this.mins, this.maxs, this.origin + offset + jumpstepheightvec, false, this); + tracebox(this.origin + jumpstepheightvec, this.mins, this.maxs, actual_destorg + jumpstepheightvec, false, this); if (trace_fraction > s) PHYS_INPUT_BUTTON_JUMP(this) = true; } } // if bot for some reason doesn't get close to the current goal find another one - if(!IS_PLAYER(this.goalcurrent) && !(this.goalcurrent.bot_pickup_respawning && this.goalcurrent_distance < 50)) + if(!this.jumppadcount && !IS_PLAYER(this.goalcurrent)) + if(!(locked_goal && this.goalcurrent_distance_z < 50 && this.goalcurrent_distance_2d < 50)) + if(havocbot_checkgoaldistance(this, destorg)) { - float curr_dist = vlen(this.origin - this.goalcurrent.origin); - if(this.goalcurrent != this.goalcurrent_prev) - { - this.goalcurrent_prev = this.goalcurrent; - this.goalcurrent_distance = curr_dist; - this.goalcurrent_distance_time = 0; - } - else if(curr_dist > this.goalcurrent_distance) + if(this.goalcurrent_distance_time < 0) // can't get close for the second time { - if(!this.goalcurrent_distance_time) - this.goalcurrent_distance_time = time; - else if (time - this.goalcurrent_distance_time > 0.5) - { - this.goalcurrent_prev = NULL; - navigation_clearroute(this); - this.bot_strategytime = 0; - return; - } + navigation_clearroute(this); + navigation_goalrating_timeout_force(this); + return; } - else + + set_tracewalk_dest(this.goalcurrent, this.origin, false); + if (!tracewalk(this, this.origin, this.mins, this.maxs, + tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode)) { - // reduce it a little bit so it works even with very small approaches to the goal - this.goalcurrent_distance = max(20, curr_dist - 15); - this.goalcurrent_distance_time = 0; + navigation_clearroute(this); + navigation_goalrating_timeout_force(this); + return; } + + // give bot only another chance to prevent bot getting stuck + // in case it thinks it can walk but actually can't + this.goalcurrent_distance_z = FLOAT_MAX; + this.goalcurrent_distance_2d = FLOAT_MAX; + this.goalcurrent_distance_time = -time; // mark second try } // 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 : v_forward * 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; - bool ignorehazards = false; s = CONTENT_SOLID; if(trace_fraction == 1 && this.jumppadcount == 0 && !this.goalcurrent.wphardwired ) if((IS_ONGROUND(this)) || (this.aistatus & AI_STATUS_RUNNING) || (this.aistatus & AI_STATUS_ROAMING) || PHYS_INPUT_BUTTON_JUMP(this)) @@ -835,16 +1032,7 @@ void havocbot_movetogoal(entity this) s = pointcontents(trace_endpos + '0 0 1'); if (s != CONTENT_SOLID) if (s == CONTENT_LAVA || s == CONTENT_SLIME) - { evadelava = normalize(this.velocity) * -1; - if(this.waterlevel >= WATERLEVEL_WETFEET && (this.watertype == CONTENT_LAVA || this.watertype == CONTENT_SLIME)) - ignorehazards = true; - } - else if (s == CONTENT_WATER) - { - if(this.waterlevel >= WATERLEVEL_WETFEET && this.watertype == CONTENT_WATER) - ignorehazards = true; - } else if (s == CONTENT_SKY) evadeobstacle = normalize(this.velocity) * -1; else if (tracebox_hits_trigger_hurt(dst_ahead, this.mins, this.maxs, trace_endpos)) @@ -854,7 +1042,7 @@ void havocbot_movetogoal(entity this) 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 (gco.z > this.origin.z + jumpstepheightvec.z) + if (destorg.z > this.origin.z + jumpstepheightvec.z) { // the goal is probably on an upper platform, assume bot can't get there unreachable = true; @@ -873,15 +1061,16 @@ void havocbot_movetogoal(entity this) if(evadeobstacle || evadelava || (s == CONTENT_WATER)) { - if(!ignorehazards) - this.aistatus |= AI_STATUS_DANGER_AHEAD; + this.aistatus |= AI_STATUS_DANGER_AHEAD; if(IS_PLAYER(this.goalcurrent)) unreachable = true; } if(unreachable) { navigation_clearroute(this); - this.bot_strategytime = 0; + navigation_goalrating_timeout_force(this); + this.ignoregoal = this.goalcurrent; + this.ignoregoaltime = time + autocvar_bot_ai_ignoregoal_timeout; } } @@ -923,8 +1112,8 @@ void havocbot_movetogoal(entity this) havocbot_keyboard_movement(this, destorg); // Bunnyhop! -// if(this.aistatus & AI_STATUS_ROAMING) - if(this.goalcurrent) + //if(this.aistatus & AI_STATUS_ROAMING) + if(!bunnyhop_forbidden && this.goalcurrent) if(skill+this.bot_moveskill >= autocvar_bot_ai_bunnyhop_skilloffset) havocbot_bunnyhop(this, dir); @@ -1265,7 +1454,8 @@ float havocbot_moveto(entity this, vector pos) debuggoalstack(this); // Heading - vector dir = ( ( this.goalcurrent.absmin + this.goalcurrent.absmax ) * 0.5 ) - (this.origin + this.view_ofs); + vector dir = get_closer_dest(this.goalcurrent, this.origin); + dir = dir - (this.origin + this.view_ofs); dir.z = 0; bot_aimdir(this, dir, -1); diff --git a/qcsrc/server/bot/default/havocbot/havocbot.qh b/qcsrc/server/bot/default/havocbot/havocbot.qh index 88ba407a2d..2f987f674e 100644 --- a/qcsrc/server/bot/default/havocbot/havocbot.qh +++ b/qcsrc/server/bot/default/havocbot/havocbot.qh @@ -23,6 +23,7 @@ .float havocbot_stickenemy; .float havocbot_role_timeout; +.float bot_tracewalk_time; .entity ignoregoal; .entity bot_lastseengoal; .entity havocbot_personal_waypoint; diff --git a/qcsrc/server/bot/default/havocbot/roles.qc b/qcsrc/server/bot/default/havocbot/roles.qc index aa1884a335..e469436014 100644 --- a/qcsrc/server/bot/default/havocbot/roles.qc +++ b/qcsrc/server/bot/default/havocbot/roles.qc @@ -10,6 +10,8 @@ #include "../bot.qh" #include "../navigation.qh" +.float bot_ratingscale; +.float bot_ratingscale_time; .float max_armorvalue; .float havocbot_role_timeout; @@ -45,15 +47,81 @@ void havocbot_goalrating_waypoints(entity this, float ratingscale, vector org, f } }; +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) { @@ -88,8 +156,9 @@ void havocbot_goalrating_items(entity this, float ratingscale, vector org, float continue; traceline(o, o + '0 0 -1500', true, NULL); - if(Mod_Q1BSP_SuperContentsFromNativeContents(pointcontents(trace_endpos + '0 0 1')) & DPCONTENTS_LIQUIDSMASK) + if(IN_LAVA(trace_endpos + '0 0 1')) continue; + // this tracebox_hits_trigger_hurt call isn't needed: // dropped weapons are removed as soon as they fall on a trigger_hurt // and can't be rated while they are in the air @@ -98,61 +167,14 @@ void havocbot_goalrating_items(entity this, float ratingscale, vector org, float } else { - // Ignore items under water - // TODO: can't .waterlevel be used here? - if(Mod_Q1BSP_SuperContentsFromNativeContents(pointcontents(it.origin + ((it.mins + it.maxs) * 0.5))) & DPCONTENTS_LIQUIDSMASK) + if(IN_LAVA(it.origin + (it.mins + it.maxs) * 0.5)) 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); }); @@ -175,6 +197,8 @@ void havocbot_goalrating_enemyplayers(entity this, float ratingscale, vector org // 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 /* @@ -189,6 +213,8 @@ void havocbot_goalrating_enemyplayers(entity this, float ratingscale, vector org { 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; @@ -204,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); } } @@ -226,7 +250,7 @@ void havocbot_chooserole_generic(entity this) 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); } diff --git a/qcsrc/server/bot/default/navigation.qc b/qcsrc/server/bot/default/navigation.qc index d0061b9006..3c3f9a8a84 100644 --- a/qcsrc/server/bot/default/navigation.qc +++ b/qcsrc/server/bot/default/navigation.qc @@ -17,6 +17,54 @@ .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; @@ -30,6 +78,8 @@ void navigation_dynamicgoal_init(entity this, bool initially_static) void navigation_dynamicgoal_set(entity this) { this.nearestwaypointtimeout = time; + if (this.nearestwaypoint) + this.nearestwaypointtimeout += 2; } void navigation_dynamicgoal_unset(entity this) @@ -39,49 +89,185 @@ void navigation_dynamicgoal_unset(entity this) this.nearestwaypointtimeout = -1; } -// rough simulation of walking from one point to another to test if a path -// can be traveled, used for waypoint linking and havocbot +// 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; +} -bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float movemode) +void set_tracewalk_dest(entity ent, vector org, bool fix_player_dest) { - vector org; - vector move; - vector dir; - float dist; - float totaldist; - float stepdist; - float ignorehazards; - float swimming; - entity tw_ladder = NULL; + 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); + 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; + if(IS_PLAYER(ent)) + submerged = (ent.waterlevel == WATERLEVEL_SUBMERGED); + else if(ent.nav_submerged_state != SUBMERGED_UNDEFINED) + submerged = (ent.nav_submerged_state == SUBMERGED_YES); + else + { + submerged = SUBMERGED(pos); + // NOTE: SUBMERGED check of box waypoint origin may fail even if origin + // is actually submerged because often they are inside some solid. + // That's why submerged state is saved now that we know current pos is + // not stuck in solid (previous tracewalk call to this pos was successfully) + if(!ent.navigation_dynamicgoal) + ent.nav_submerged_state = (submerged) ? SUBMERGED_YES : SUBMERGED_NO; + } + return submerged; +} + +bool navigation_checkladders(entity e, vector org, vector m1, vector m2, vector end, vector end2, int movemode) +{ + IL_EACH(g_ladders, it.classname == "func_ladder", + { + if(it.bot_pickup) + if(boxesoverlap(org + m1 + '-1 -1 -1', org + m2 + '1 1 1', it.absmin, it.absmax)) + if(boxesoverlap(end, end2, it.absmin + vec2(m1) + '-1 -1 0', it.absmax + vec2(m2) + '1 1 0')) + { + vector top = org; + top.z = it.absmax.z + (PL_MAX_CONST.z - PL_MIN_CONST.z); + tracebox(org, m1, m2, top, movemode, e); + if(trace_fraction == 1) + return true; + } + }); + return false; +} + +vector resurface_limited(vector org, float lim, vector m1) +{ + if (WETFEET(org + eZ * (lim - org.z))) + org.z = lim; + else + { + float RES_min_h = org.z; + float RES_max_h = lim; + do { + org.z = 0.5 * (RES_min_h + RES_max_h); + if(WETFEET(org)) + RES_min_h = org.z; + else + RES_max_h = org.z; + } while (RES_max_h - RES_min_h >= 1); + org.z = RES_min_h; + } + return org; +} +#define RESURFACE_LIMITED(org, lim) org = resurface_limited(org, lim, m1) + +#define NAV_WALK 0 +#define NAV_SWIM_ONWATER 1 +#define NAV_SWIM_UNDERWATER 2 + +// 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] +bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float end_height, float movemode) +{ if(autocvar_bot_debug_tracewalk) { debugresetnodes(); debugnode(e, start); } - move = end - start; - move.z = 0; - org = start; - dist = totaldist = vlen(move); - dir = normalize(move); - stepdist = 32; - ignorehazards = false; - swimming = false; + vector org = start; + vector flatdir = end - start; + flatdir.z = 0; + float flatdist = vlen(flatdir); + flatdir = normalize(flatdir); + float stepdist = 32; + bool ignorehazards = false; + int nav_action; // Analyze starting point traceline(start, start, MOVE_NORMAL, e); if (trace_dpstartcontents & (DPCONTENTS_SLIME | DPCONTENTS_LAVA)) ignorehazards = true; - else - { - traceline( start, start + '0 0 -65536', MOVE_NORMAL, e); - if (trace_dpstartcontents & (DPCONTENTS_SLIME | DPCONTENTS_LAVA)) - { - ignorehazards = true; - swimming = true; - } - } + tracebox(start, m1, m2, start, MOVE_NOMONSTERS, e); if (trace_startsolid) { @@ -93,78 +279,309 @@ bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float m return false; } + vector end2 = end; + if(end_height) + end2.z += end_height; + + vector fixed_end = end; + vector move; + + if (flatdist > 0 && WETFEET(org)) + { + if (SUBMERGED(org)) + nav_action = NAV_SWIM_UNDERWATER; + else + { + // tracebox down by player's height + // useful to know if water level is so low that bot can still walk + tracebox(org, m1, m2, org - eZ * (m2.z - m1.z), movemode, e); + if (SUBMERGED(trace_endpos)) + { + org = trace_endpos; + nav_action = NAV_SWIM_UNDERWATER; + } + else + nav_action = NAV_WALK; + } + } + else + nav_action = NAV_WALK; + // Movement loop - move = end - org; - for (;;) + while (true) { - if (boxesoverlap(end, end, org + m1 + '-1 -1 -1', org + m2 + '1 1 1')) + if (flatdist <= 0) { - // Succeeded - if(autocvar_bot_debug_tracewalk) - debugnodestatus(org, DEBUG_NODE_SUCCESS); + bool success = true; + if (org.z > end2.z + 1) + { + tracebox(org, m1, m2, end2, movemode, e); + org = trace_endpos; + if (org.z > end2.z + 1) + success = false; + } + else if (org.z < end.z - 1) + { + tracebox(org, m1, m2, org - jumpheight_vec, movemode, e); + if (SUBMERGED(trace_endpos)) + { + vector v = trace_endpos; + tracebox(v, m1, m2, end, movemode, e); + if(trace_endpos.z >= end.z - 1) + { + RESURFACE_LIMITED(v, trace_endpos.z); + trace_endpos = v; + } + } + else if (trace_endpos.z > org.z - jumpheight_vec.z) + tracebox(trace_endpos, m1, m2, trace_endpos + jumpheight_vec, movemode, e); + org = trace_endpos; + if (org.z < end.z - 1) + success = false; + } - //print("tracewalk: ", vtos(start), " can reach ", vtos(end), "\n"); - return true; + if (success) + { + // Succeeded + if(autocvar_bot_debug_tracewalk) + { + debugnode(e, org); + debugnodestatus(org, DEBUG_NODE_SUCCESS); + } + + //print("tracewalk: ", vtos(start), " can reach ", vtos(end), "\n"); + return true; + } } + if(autocvar_bot_debug_tracewalk) debugnode(e, org); - if (dist <= 0) + if (flatdist <= 0) break; - if (stepdist > dist) - stepdist = dist; - dist = dist - stepdist; - traceline(org, org, MOVE_NORMAL, e); - if (!ignorehazards) - { - if (trace_dpstartcontents & (DPCONTENTS_SLIME | DPCONTENTS_LAVA)) - { - // hazards blocking path - if(autocvar_bot_debug_tracewalk) - debugnodestatus(org, DEBUG_NODE_FAIL); - //print("tracewalk: ", vtos(start), " hits a hazard when trying to reach ", vtos(end), "\n"); - return false; + if (stepdist > flatdist) + stepdist = flatdist; + if(nav_action == NAV_SWIM_UNDERWATER || (nav_action == NAV_SWIM_ONWATER && org.z > end2.z)) + { + // can't use movement direction here to calculate move because of + // precision errors especially when direction has a high enough z value + //water_dir = normalize(water_end - org); + //move = org + water_dir * stepdist; + fixed_end.z = bound(end.z, org.z, end2.z); + if (stepdist == flatdist) { + move = fixed_end; + flatdist = 0; + } else { + move = org + (fixed_end - org) * (stepdist / flatdist); + flatdist = vlen(vec2(fixed_end - move)); } } - if (trace_dpstartcontents & DPCONTENTS_LIQUIDSMASK) + else // horiz. direction { - move = normalize(end - org); - tracebox(org, m1, m2, org + move * stepdist, movemode, e); + flatdist -= stepdist; + move = org + flatdir * stepdist; + } - if(autocvar_bot_debug_tracewalk) - debugnode(e, trace_endpos); + if(nav_action == NAV_SWIM_ONWATER) + { + tracebox(org, m1, m2, move, movemode, e); // swim + // hit something if (trace_fraction < 1) { - swimming = true; - org = trace_endpos + normalize(org - trace_endpos) * stepdist; - for (; org.z < end.z + e.maxs.z; org.z += stepdist) + // stepswim + tracebox(org + stepheightvec, m1, m2, move + stepheightvec, movemode, e); + + if (trace_fraction < 1 || trace_startsolid) // can't jump obstacle out of water { - if(autocvar_bot_debug_tracewalk) - debugnode(e, org); + org = trace_endpos; + if(navigation_checkladders(e, org, m1, m2, end, end2, movemode)) + { + if(autocvar_bot_debug_tracewalk) + { + debugnode(e, org); + debugnodestatus(org, DEBUG_NODE_SUCCESS); + } - if(pointcontents(org) == CONTENT_EMPTY) - break; - } + //print("tracewalk: ", vtos(start), " can reach ", vtos(end), "\n"); + return true; + } - if(pointcontents(org + '0 0 1') != CONTENT_EMPTY) - { if(autocvar_bot_debug_tracewalk) debugnodestatus(org, DEBUG_NODE_FAIL); return false; - //print("tracewalk: ", vtos(start), " failed under water\n"); + //print("tracewalk: ", vtos(start), " hit something when trying to reach ", vtos(end), "\n"); + } + + //succesful stepswim + + if (flatdist <= 0) + { + org = trace_endpos; + continue; } - continue; + if (org.z <= move.z) // going horiz. + { + tracebox(trace_endpos, m1, m2, move, movemode, e); + org = trace_endpos; + nav_action = NAV_WALK; + continue; + } } - else + + if (org.z <= move.z) // going horiz. + { org = trace_endpos; + nav_action = NAV_SWIM_ONWATER; + } + else // going down + { + org = trace_endpos; + if (SUBMERGED(org)) + nav_action = NAV_SWIM_UNDERWATER; + else + nav_action = NAV_SWIM_ONWATER; + } } - else + else if(nav_action == NAV_SWIM_UNDERWATER) + { + if (move.z >= org.z) // swimming upwards or horiz. + { + tracebox(org, m1, m2, move, movemode, e); // swim + + bool stepswum = false; + + // hit something + if (trace_fraction < 1) + { + // stepswim + vector stepswim_move = move + stepheightvec; + if (flatdist > 0 && stepswim_move.z > end2.z + stepheightvec.z) // don't allow stepswim to go higher than destination + stepswim_move.z = end2.z; + + tracebox(org + stepheightvec, m1, m2, stepswim_move, movemode, e); + + // hit something + if (trace_startsolid) + { + if(autocvar_bot_debug_tracewalk) + debugnodestatus(org, DEBUG_NODE_FAIL); + + //print("tracewalk: ", vtos(start), " hit something when trying to reach ", vtos(end), "\n"); + return false; + } + + if (trace_fraction < 1) + { + float org_z_prev = org.z; + RESURFACE_LIMITED(org, end2.z); + if(org.z == org_z_prev) + { + if(autocvar_bot_debug_tracewalk) + debugnodestatus(org, DEBUG_NODE_FAIL); + + //print("tracewalk: ", vtos(start), " can't reach ", vtos(end), "\n"); + return false; + } + if(SUBMERGED(org)) + nav_action = NAV_SWIM_UNDERWATER; + else + nav_action = NAV_SWIM_ONWATER; + + // we didn't advance horiz. in this step, flatdist decrease should be reverted + // but we can't do it properly right now... apply this workaround instead + if (flatdist <= 0) + flatdist = 1; + + continue; + } + + //succesful stepswim + + if (flatdist <= 0) + { + org = trace_endpos; + continue; + } + + stepswum = true; + } + + if (!WETFEET(trace_endpos)) + { + tracebox(trace_endpos, m1, m2, trace_endpos - eZ * (stepdist + (m2.z - m1.z)), movemode, e); + // if stepswum we'll land on the obstacle, avoid the SUBMERGED check + if (!stepswum && SUBMERGED(trace_endpos)) + { + RESURFACE_LIMITED(trace_endpos, end2.z); + org = trace_endpos; + nav_action = NAV_SWIM_ONWATER; + continue; + } + + // not submerged + org = trace_endpos; + nav_action = NAV_WALK; + continue; + } + + // wetfeet + org = trace_endpos; + nav_action = NAV_SWIM_UNDERWATER; + continue; + } + else //if (move.z < org.z) // swimming downwards + { + tracebox(org, m1, m2, move, movemode, e); // swim + + // hit something + if (trace_fraction < 1) + { + // stepswim + tracebox(org + stepheightvec, m1, m2, move + stepheightvec, movemode, e); + + // hit something + if (trace_fraction < 1 || trace_startsolid) // can't jump obstacle out of water + { + if(autocvar_bot_debug_tracewalk) + debugnodestatus(move, DEBUG_NODE_FAIL); + + //print("tracewalk: ", vtos(start), " hit something when trying to reach ", vtos(end), "\n"); + return false; + } + + //succesful stepswim + + if (flatdist <= 0) + { + org = trace_endpos; + continue; + } + + if (trace_endpos.z > org.z && !SUBMERGED(trace_endpos)) + { + // stepswim caused upwards direction + tracebox(trace_endpos, m1, m2, trace_endpos - stepheightvec, movemode, e); + if (!SUBMERGED(trace_endpos)) + { + org = trace_endpos; + nav_action = NAV_WALK; + continue; + } + } + } + + org = trace_endpos; + nav_action = NAV_SWIM_UNDERWATER; + continue; + } + } + else if(nav_action == NAV_WALK) { - move = dir * stepdist + org; + // walk tracebox(org, m1, m2, move, movemode, e); if(autocvar_bot_debug_tracewalk) @@ -177,48 +594,55 @@ bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float m tracebox(org + stepheightvec, m1, m2, move + stepheightvec, movemode, e); if (trace_fraction < 1 || trace_startsolid) { - tracebox(org + jumpstepheightvec, m1, m2, move + jumpstepheightvec, movemode, e); - if (trace_fraction < 1 || trace_startsolid) + if (trace_startsolid) // hit ceiling above org + { + // reduce stepwalk height + tracebox(org, m1, m2, org + stepheightvec, movemode, e); + tracebox(trace_endpos, m1, m2, move + eZ * (trace_endpos.z - move.z), movemode, e); + } + else //if (trace_fraction < 1) + { + tracebox(org + jumpstepheightvec, m1, m2, move + jumpstepheightvec, movemode, e); + if (trace_startsolid) // hit ceiling above org + { + // reduce jumpstepwalk height + tracebox(org, m1, m2, org + jumpstepheightvec, movemode, e); + tracebox(trace_endpos, m1, m2, move + eZ * (trace_endpos.z - move.z), movemode, e); + } + } + + if (trace_fraction < 1) { + vector v = trace_endpos; + v.z = org.z + jumpheight_vec.z; + if(navigation_checkladders(e, v, m1, m2, end, end2, movemode)) + { + if(autocvar_bot_debug_tracewalk) + { + debugnode(e, v); + debugnodestatus(v, DEBUG_NODE_SUCCESS); + } + + //print("tracewalk: ", vtos(start), " can reach ", vtos(end), "\n"); + return true; + } + if(autocvar_bot_debug_tracewalk) debugnodestatus(trace_endpos, DEBUG_NODE_WARNING); - IL_EACH(g_ladders, it.classname == "func_ladder", - { it.solid = SOLID_BSP; }); - traceline( org, move, movemode, e); - IL_EACH(g_ladders, it.classname == "func_ladder", - { it.solid = SOLID_TRIGGER; }); - if ( trace_ent.classname == "door_rotating" || trace_ent.classname == "door") { vector nextmove; move = trace_endpos; while(trace_ent.classname == "door_rotating" || trace_ent.classname == "door") { - nextmove = move + (dir * stepdist); + nextmove = move + (flatdir * stepdist); traceline( move, nextmove, movemode, e); move = nextmove; } - } - else if (trace_ent.classname == "func_ladder") - { - tw_ladder = trace_ent; - vector ladder_bottom = trace_endpos - dir * m2.x; - vector ladder_top = ladder_bottom; - ladder_top.z = trace_ent.absmax.z + (-m1.z + 1); - tracebox(ladder_bottom, m1, m2, ladder_top, movemode, e); - if (trace_fraction < 1 || trace_startsolid) - { - if(autocvar_bot_debug_tracewalk) - debugnodestatus(trace_endpos, DEBUG_NODE_FAIL); - - return false; // failed - } - org = ladder_top + dir * m2.x; - move = org + dir * stepdist; - continue; + flatdist = vlen(vec2(end - move)); } else { @@ -246,28 +670,43 @@ bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float m // (this is the same logic as the Quake walkmove function used) tracebox(move, m1, m2, move + '0 0 -65536', movemode, e); - // moved successfully - if(swimming) + org = trace_endpos; + + if (!ignorehazards) { - float c; - c = pointcontents(org + '0 0 1'); - if (!(c == CONTENT_WATER || c == CONTENT_LAVA || c == CONTENT_SLIME)) - swimming = false; - else - continue; + if (IN_LAVA(org)) + { + if(autocvar_bot_debug_tracewalk) + { + debugnode(e, trace_endpos); + debugnodestatus(org, DEBUG_NODE_FAIL); + } + + //print("tracewalk: ", vtos(start), " hits a hazard when trying to reach ", vtos(end), "\n"); + return false; + } } - org = trace_endpos; - } + if (flatdist <= 0) + { + if(move.z >= end2.z && org.z < end2.z) + org.z = end2.z; + continue; + } - if(tw_ladder && org.z < tw_ladder.absmax.z) - { - // stop tracewalk if destination height is lower than the top of the ladder - // otherwise bot can't easily figure out climbing direction + if(org.z > move.z - 1 || !SUBMERGED(org)) + { + nav_action = NAV_WALK; + continue; + } + + // ended up submerged while walking if(autocvar_bot_debug_tracewalk) - debugnodestatus(org, DEBUG_NODE_FAIL); + debugnode(e, org); - return false; + RESURFACE_LIMITED(org, move.z); + nav_action = NAV_SWIM_ONWATER; + continue; } } @@ -287,7 +726,11 @@ bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float m // completely empty the goal stack, used when deciding where to go void navigation_clearroute(entity this) { - //print("bot ", etos(this), " clear\n"); + this.goalcurrent_prev = this.goalcurrent; + this.goalcurrent_distance_2d = FLOAT_MAX; + this.goalcurrent_distance_z = FLOAT_MAX; + this.goalcurrent_distance_time = 0; + this.goalentity_lock_timeout = 0; this.goalentity = NULL; this.goalcurrent = NULL; this.goalstack01 = NULL; @@ -331,6 +774,10 @@ void navigation_clearroute(entity this) // steps to the goal, and then recalculate the path. void navigation_pushroute(entity this, entity e) { + this.goalcurrent_prev = this.goalcurrent; + 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) this.goalentity = NULL; @@ -373,9 +820,16 @@ void navigation_pushroute(entity this, entity e) // (used when a spawnfunc_waypoint is reached) void navigation_poproute(entity this) { + this.goalcurrent_prev = this.goalcurrent; + 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; @@ -410,23 +864,24 @@ void navigation_poproute(entity this) this.goalstack31 = NULL; } -float navigation_waypoint_will_link(vector v, vector org, entity ent, float walkfromwp, float bestdist) +// walking to wp (walkfromwp == false) v2 and v2_height will be used as +// waypoint destination coordinates instead of v (only useful for box waypoints) +// for normal waypoints v2 == v and v2_height == 0 +float navigation_waypoint_will_link(vector v, vector org, entity ent, vector v2, float v2_height, vector o2, float o2_height, float walkfromwp, float bestdist) { - float dist; - dist = vlen(v - org); - if (bestdist > dist) + if (vdist(v - org, <, bestdist)) { traceline(v, org, true, ent); if (trace_fraction == 1) { if (walkfromwp) { - if (tracewalk(ent, v, PL_MIN_CONST, PL_MAX_CONST, org, bot_navigation_movemode)) + if (tracewalk(ent, v, PL_MIN_CONST, PL_MAX_CONST, v2, v2_height, bot_navigation_movemode)) return true; } else { - if (tracewalk(ent, org, PL_MIN_CONST, PL_MAX_CONST, v, bot_navigation_movemode)) + if (tracewalk(ent, org, PL_MIN_CONST, PL_MAX_CONST, o2, o2_height, bot_navigation_movemode)) return true; } } @@ -437,6 +892,9 @@ float navigation_waypoint_will_link(vector v, vector org, entity ent, float walk // find the spawnfunc_waypoint near a dynamic goal such as a dropped weapon entity navigation_findnearestwaypoint_withdist_except(entity ent, float walkfromwp, float bestdist, entity except) { + if(ent.tag_entity) + ent = ent.tag_entity; + vector pm1 = ent.origin + ent.mins; vector pm2 = ent.origin + ent.maxs; @@ -444,39 +902,92 @@ entity navigation_findnearestwaypoint_withdist_except(entity ent, float walkfrom 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 + 0.5 * (ent.mins + ent.maxs); - org.z = ent.origin.z + ent.mins.z - PL_MIN_CONST.z; // player height - // TODO possibly make other code have the same support for bboxes - if(ent.tag_entity) - org = org + ent.tag_entity.origin; + vector org = ent.origin; if (navigation_testtracewalk) te_plasmaburn(org); entity best = NULL; - vector v; + vector v = '0 0 0'; + + if(ent.size && !IS_PLAYER(ent)) + { + org += 0.5 * (ent.mins + ent.maxs); + org.z = ent.origin.z + ent.mins.z - PL_MIN_CONST.z; // player height + } + + if(!autocvar_g_waypointeditor && walkfromwp && !ent.navigation_dynamicgoal) + { + waypoint_clearlinks(ent); // initialize wpXXmincost fields + IL_EACH(g_waypoints, it != ent, + { + if(walkfromwp && (it.wpflags & WAYPOINTFLAG_NORELINK)) + continue; + + 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); + } + }); + } // box check failed, try walk IL_EACH(g_waypoints, it != ent, { - if(it.wpisbox) + if(walkfromwp && (it.wpflags & WAYPOINTFLAG_NORELINK)) + continue; + v = it.origin; + + if (walkfromwp) { - vector wm1 = it.origin + it.mins; - vector wm2 = it.origin + it.maxs; - v.x = bound(wm1_x, org.x, wm2_x); - v.y = bound(wm1_y, org.y, wm2_y); - v.z = bound(wm1_z, org.z, wm2_z); + set_tracewalk_dest(ent, v, true); + if (trace_ent == ent) + { + bestdist = 0; + best = it; + break; + } } else - v = it.origin; - if(navigation_waypoint_will_link(v, org, ent, 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) @@ -494,31 +1005,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, m1, m2; -// navigation_testtracewalk = true; + //navigation_testtracewalk = true; int c = 0; IL_EACH(g_waypoints, !it.wpconsidered, { - if (it.wpisbox) - { - m1 = it.origin + it.mins; - m2 = it.origin + it.maxs; - v = this.origin; - v.x = bound(m1_x, v.x, m2_x); - v.y = bound(m1_y, v.y, m2_y); - v.z = bound(m1_z, v.z, m2_z); - } - else - v = it.origin; - vector diff = v - this.origin; + set_tracewalk_dest(it, this.origin, false); + + 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, 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 = vlen(v - this.origin) + 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; @@ -530,25 +1032,27 @@ float navigation_markroutes_nearestwaypoints(entity this, float maxdist) } // updates a path link if a spawnfunc_waypoint link is better than the current one -void navigation_markroutes_checkwaypoint(entity w, entity wp, float cost2, vector p) +void navigation_markroutes_checkwaypoint(entity w, entity wp, float cost, vector p) { - vector m1; - vector m2; + vector m1, m2; vector v; if (wp.wpisbox) { - m1 = wp.absmin; - m2 = wp.absmax; + m1 = wp.origin + wp.mins; + m2 = wp.origin + wp.maxs; v.x = bound(m1_x, p.x, m2_x); v.y = bound(m1_y, p.y, m2_y); v.z = bound(m1_z, p.z, m2_z); } else v = wp.origin; - cost2 = cost2 + vlen(v - p); - if (wp.wpcost > cost2) + if (w.wpflags & WAYPOINTFLAG_TELEPORT) + cost += w.wp00mincost; // assuming teleport has exactly one destination + else + cost += waypoint_gettravelcost(p, v, w, wp); + if (wp.wpcost > cost) { - wp.wpcost = cost2; + wp.wpcost = cost; wp.enemy = w; wp.wpfire = 1; wp.wpnearestpoint = v; @@ -683,16 +1187,9 @@ void navigation_markroutes_inverted(entity fixed_source_waypoint) cost = it.wpcost; // cost to walk from it to home p = it.wpnearestpoint; entity wp = it; - IL_EACH(g_waypoints, true, + IL_EACH(g_waypoints, it != wp, { - if(wp != it.wp00) if(wp != it.wp01) if(wp != it.wp02) if(wp != it.wp03) - if(wp != it.wp04) if(wp != it.wp05) if(wp != it.wp06) if(wp != it.wp07) - if(wp != it.wp08) if(wp != it.wp09) if(wp != it.wp10) if(wp != it.wp11) - if(wp != it.wp12) if(wp != it.wp13) if(wp != it.wp14) if(wp != it.wp15) - if(wp != it.wp16) if(wp != it.wp17) if(wp != it.wp18) if(wp != it.wp19) - if(wp != it.wp20) if(wp != it.wp21) if(wp != it.wp22) if(wp != it.wp23) - if(wp != it.wp24) if(wp != it.wp25) if(wp != it.wp26) if(wp != it.wp27) - if(wp != it.wp28) if(wp != it.wp29) if(wp != it.wp30) if(wp != it.wp31) + if(!waypoint_islinked(it, wp)) continue; cost2 = cost + it.dmg; navigation_markroutes_checkwaypoint(wp, it, cost2, p); @@ -710,6 +1207,9 @@ void navigation_routerating(entity this, entity e, float f, float rangebias) if(e.blacklisted) return; + rangebias = waypoint_getlinearcost(rangebias); + f = waypoint_getlinearcost(f); + if (IS_PLAYER(e)) { bool rate_wps = false; @@ -751,7 +1251,7 @@ void navigation_routerating(entity this, entity e, float f, float rangebias) } } - vector o = (e.absmin + e.absmax) * 0.5; + vector goal_org = (e.absmin + e.absmax) * 0.5; //print("routerating ", etos(e), " = ", ftos(f), " - ", ftos(rangebias), "\n"); @@ -759,7 +1259,7 @@ void navigation_routerating(entity this, entity e, float f, float rangebias) if(g_jetpack) if(this.items & IT_JETPACK) if(autocvar_bot_ai_navigation_jetpack) - if(vdist(this.origin - o, >, autocvar_bot_ai_navigation_jetpack_mindistance)) + if(vdist(this.origin - goal_org, >, autocvar_bot_ai_navigation_jetpack_mindistance)) { vector pointa, pointb; @@ -770,7 +1270,7 @@ void navigation_routerating(entity this, entity e, float f, float rangebias) pointa = trace_endpos - '0 0 1'; // Point B - traceline(o, o + '0 0 65535', MOVE_NORMAL, e); + traceline(goal_org, goal_org + '0 0 65535', MOVE_NORMAL, e); pointb = trace_endpos - '0 0 1'; // Can I see these two points from the sky? @@ -861,10 +1361,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; + 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)); @@ -887,12 +1388,17 @@ void navigation_routerating(entity this, entity e, float f, float rangebias) } 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); - LOG_DEBUG(e.classname, " ", ftos(f), "/(1+", ftos((nwp.wpcost + vlen(e.origin - nwp.wpnearestpoint))), "/", ftos(rangebias), ") = "); - f = f * rangebias / (rangebias + (nwp.wpcost + vlen(o - nwp.wpnearestpoint))); + 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), ")"); if (navigation_bestrating < f) { @@ -910,12 +1416,25 @@ bool navigation_routetogoal(entity this, entity e, vector startposition) if (!e) return false; + entity teleport_goal = NULL; + this.goalentity = e; + if(e.wpflags & WAYPOINTFLAG_TELEPORT) + { + // force teleport destination as route destination + teleport_goal = e; + navigation_pushroute(this, e.wp00); + this.goalentity = e.wp00; + } + // put the entity on the goal stack //print("routetogoal ", etos(e), "\n"); navigation_pushroute(this, e); + if(teleport_goal) + e = this.goalentity; + if(e.classname == "waypoint" && !(e.wpflags & WAYPOINTFLAG_PERSONAL)) { this.wp_goal_prev1 = this.wp_goal_prev0; @@ -927,8 +1446,13 @@ bool navigation_routetogoal(entity this, entity e, vector startposition) return true; // if it can reach the goal there is nothing more to do - if (tracewalk(this, startposition, STAT(PL_MIN, this), STAT(PL_MAX, this), (e.absmin + e.absmax) * 0.5, 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 @@ -937,6 +1461,8 @@ bool navigation_routetogoal(entity this, entity e, vector startposition) e = e.nearestwaypoint; nearest_wp = e; } + else if(teleport_goal) + e = teleport_goal; else e = e.enemy; // we already have added it, so... @@ -946,7 +1472,26 @@ 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(tracewalk(this, nearest_wp.enemy.origin, STAT(PL_MIN, this), STAT(PL_MAX, this), (this.goalentity.absmin + this.goalentity.absmax) * 0.5, bot_navigation_movemode)) + if (this.goalentity.navigation_dynamicgoal || autocvar_g_waypointeditor) + { + if (nearest_wp.enemy.wpcost < autocvar_bot_ai_strategyinterval_movingtarget) + { + 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; } @@ -963,21 +1508,91 @@ bool navigation_routetogoal(entity this, entity e, vector startposition) 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) { - vector org, m1, m2; - org = this.origin; - m1 = org + this.mins; - m2 = org + this.maxs; + 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 // as they may report a touch even if it didn't really happen - if(this.lastteleporttime>0) - if(time - this.lastteleporttime < ((this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL) ? 2 : 0.15)) + if(this.lastteleporttime > 0 && TELEPORT_USED(this, this.goalcurrent)) { if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) if(this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && this.goalcurrent.owner==this) @@ -986,31 +1601,48 @@ void navigation_poptouchedgoals(entity this) this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED; } navigation_poproute(this); - return; + this.lastteleporttime = 0; + ++removed_goals; } + else + 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)) - if(tracewalk(this, this.origin, this.mins, this.maxs, (this.goalstack01.absmin + this.goalstack01.absmax) * 0.5, bot_navigation_movemode)) + else if (this.lastteleporttime > 0) { - LOG_DEBUG("path optimized for ", this.netname, ", removed a goal from the queue"); - navigation_poproute(this); - // 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 + // 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)) + { + 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); + this.lastteleporttime = 0; + ++removed_goals; + return removed_goals; + } } // Loose goal touching check when running if(this.aistatus & AI_STATUS_RUNNING) if(this.goalcurrent.classname=="waypoint") - if(!(this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT)) - if(vlen(this.velocity - eZ * this.velocity.z) >= autocvar_sv_maxspeed) // if -really- running + if(vdist(vec2(this.velocity), >=, autocvar_sv_maxspeed)) // if -really- running { if(vdist(this.origin - this.goalcurrent.origin, <, 150)) { @@ -1026,6 +1658,9 @@ void navigation_poptouchedgoals(entity this) } navigation_poproute(this); + ++removed_goals; + if(this.goalcurrent && this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT) + return removed_goals; } } } @@ -1039,10 +1674,7 @@ 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(m1, m2, gc_min, gc_max)) - break; - - if((this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT)) + if(!boxesoverlap(this.absmin, this.absmax, gc_min, gc_max)) break; // Detect personal waypoints @@ -1054,7 +1686,56 @@ void navigation_poptouchedgoals(entity this) } navigation_poproute(this); + ++removed_goals; + if(this.goalcurrent && this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT) + 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) @@ -1065,9 +1746,10 @@ 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); } // ends a goal selection session (updates goal stack to the best goal) @@ -1095,11 +1777,13 @@ void botframe_updatedangerousobjects(float maxupdate) vector m1, m2, v, o; float c, d, danger; c = 0; + entity wp_cur; IL_EACH(g_waypoints, true, { danger = 0; - m1 = it.mins; - m2 = it.maxs; + m1 = it.absmin; + m2 = it.absmax; + wp_cur = it; IL_EACH(g_bot_dodge, it.bot_dodge, { v = it.origin; @@ -1107,7 +1791,7 @@ void botframe_updatedangerousobjects(float maxupdate) v.y = bound(m1_y, v.y, m2_y); v.z = bound(m1_z, v.z, m2_z); o = (it.absmin + it.absmax) * 0.5; - d = it.bot_dodgerating - vlen(o - v); + d = waypoint_getlinearcost(it.bot_dodgerating) - waypoint_gettravelcost(o, v, it, wp_cur); if (d > 0) { traceline(o, v, true, NULL); @@ -1145,7 +1829,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)); - if(tracewalk(bot_waypoint_queue_goal, this.origin, STAT(PL_MIN, this), STAT(PL_MAX, this), bot_waypoint_queue_goal.origin, 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) { @@ -1161,7 +1847,7 @@ void navigation_unstuck(entity this) { 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 diff --git a/qcsrc/server/bot/default/navigation.qh b/qcsrc/server/bot/default/navigation.qh index 8f1f03ad19..f3103cc4fc 100644 --- a/qcsrc/server/bot/default/navigation.qh +++ b/qcsrc/server/bot/default/navigation.qh @@ -9,6 +9,7 @@ float navigation_testtracewalk; vector jumpstepheightvec; vector stepheightvec; +vector jumpheight_vec; entity navigation_bestgoal; @@ -22,13 +23,45 @@ entity navigation_bestgoal; .entity goalstack20, goalstack21, goalstack22, goalstack23; .entity goalstack24, goalstack25, goalstack26, goalstack27; .entity goalstack28, goalstack29, goalstack30, goalstack31; + +.entity goalcurrent_prev; +.float goalcurrent_distance_z; +.float goalcurrent_distance_2d; +.float goalcurrent_distance_time; + +.float goalentity_lock_timeout; + .entity nearestwaypoint; +.float nearestwaypointtimeout; + +/* +// item it is linked from waypoint it.wpXX (INCOMING link) +// links are sorted by their cost (wpXXmincost) +.entity wp00, wp01, wp02, wp03, wp04, wp05, wp06, wp07, wp08, wp09, wp10, wp11, wp12, wp13, wp14, wp15; +.entity wp16, wp17, wp18, wp19, wp20, wp21, wp22, wp23, wp24, wp25, wp26, wp27, wp28, wp29, wp30, wp31; + +.float wp00mincost, wp01mincost, wp02mincost, wp03mincost, wp04mincost, wp05mincost, wp06mincost, wp07mincost; +.float wp08mincost, wp09mincost, wp10mincost, wp11mincost, wp12mincost, wp13mincost, wp14mincost, wp15mincost; +.float wp16mincost, wp17mincost, wp18mincost, wp19mincost, wp20mincost, wp21mincost, wp22mincost, wp23mincost; +.float wp24mincost, wp25mincost, wp26mincost, wp27mincost, wp28mincost, wp29mincost, wp30mincost, wp31mincost; +*/ + +#define navigation_item_islinked(from_wp, to_item) waypoint_islinked(to_item, from_wp) +#define navigation_item_addlink(from_wp, to_item) \ + waypoint_addlink_customcost(to_item, from_wp, waypoint_getlinkcost(from_wp, to_item)) + +#define TELEPORT_USED(pl, tele_wp) \ + (time - pl.lastteleporttime < ((tele_wp.wpflags & WAYPOINTFLAG_PERSONAL) ? 2 : 0.15) \ + && boxesoverlap(tele_wp.absmin, tele_wp.absmax, pl.lastteleport_origin + STAT(PL_MIN, pl), pl.lastteleport_origin + STAT(PL_MAX, pl))) + +vector tracewalk_dest; +float tracewalk_dest_height; .entity wp_goal_prev0; .entity wp_goal_prev1; -.float nearestwaypointtimeout; .float lastteleporttime; +.vector lastteleport_origin; .float blacklisted; @@ -52,6 +85,12 @@ void navigation_dynamicgoal_init(entity this, bool initially_static); void navigation_dynamicgoal_set(entity this); void navigation_dynamicgoal_unset(entity this); +.int nav_submerged_state; +#define SUBMERGED_UNDEFINED 0 +#define SUBMERGED_NO 1 +#define SUBMERGED_YES 2 +bool navigation_check_submerged_state(entity ent, vector pos); + /* * Functions @@ -63,7 +102,7 @@ void debugnodestatus(vector position, float status); void debuggoalstack(entity this); -float tracewalk(entity e, vector start, vector m1, vector m2, vector end, float movemode); +float tracewalk(entity e, vector start, vector m1, vector m2, vector end, float end_height, float movemode); float navigation_markroutes_nearestwaypoints(entity this, float maxdist); float navigation_routetogoal(entity this, entity e, vector startposition); @@ -75,12 +114,16 @@ void navigation_markroutes_checkwaypoint(entity w, entity wp, float cost2, vecto void navigation_markroutes(entity this, entity fixed_source_waypoint); void navigation_markroutes_inverted(entity fixed_source_waypoint); void navigation_routerating(entity this, entity e, float f, float rangebias); -void navigation_poptouchedgoals(entity this); +void navigation_shortenpath(entity this); +int navigation_poptouchedgoals(entity this); void navigation_goalrating_start(entity this); void navigation_goalrating_end(entity this); +void navigation_goalrating_timeout_set(entity this); +void navigation_goalrating_timeout_force(entity this); +bool navigation_goalrating_timeout(entity this); void navigation_unstuck(entity this); void botframe_updatedangerousobjects(float maxupdate); entity navigation_findnearestwaypoint(entity ent, float walkfromwp); -float navigation_waypoint_will_link(vector v, vector org, entity ent, float walkfromwp, float bestdist); +float navigation_waypoint_will_link(vector v, vector org, entity ent, vector v2, float v2_height, vector o2, float o2_height, float walkfromwp, float bestdist); diff --git a/qcsrc/server/bot/default/waypoints.qc b/qcsrc/server/bot/default/waypoints.qc index b1ff75db72..fa82d926b6 100644 --- a/qcsrc/server/bot/default/waypoints.qc +++ b/qcsrc/server/bot/default/waypoints.qc @@ -13,10 +13,149 @@ #include #include +#include #include #include +.entity spawnpointmodel; +void waypoint_unreachable(entity pl) +{ + IL_EACH(g_waypoints, true, + { + it.colormod = '0.5 0.5 0.5'; + it.effects &= ~(EF_NODEPTHTEST | EF_RED | EF_BLUE); + }); + + entity e2 = navigation_findnearestwaypoint(pl, false); + if(!e2) + { + LOG_INFOF("Can't find any waypoint nearby\n"); + return; + } + + navigation_markroutes(pl, e2); + + int j = 0; + int m = 0; + IL_EACH(g_waypoints, it.wpcost >= 10000000, + { + LOG_INFO("unreachable: ", etos(it), " ", vtos(it.origin), "\n"); + it.colormod_z = 8; + it.effects |= EF_NODEPTHTEST | EF_BLUE; + j++; + m++; + }); + if (j) LOG_INFOF("%d waypoints cannot be reached from here in any way (marked with blue light)\n", j); + navigation_markroutes_inverted(e2); + + j = 0; + IL_EACH(g_waypoints, it.wpcost >= 10000000, + { + LOG_INFO("cannot reach me: ", etos(it), " ", vtos(it.origin), "\n"); + it.colormod_x = 8; + if (!(it.effects & EF_NODEPTHTEST)) // not already reported before + m++; + it.effects |= EF_NODEPTHTEST | EF_RED; + j++; + }); + if (j) LOG_INFOF("%d waypoints cannot walk to here in any way (marked with red light)\n", j); + if (m) LOG_INFOF("%d waypoints have been marked total\n", m); + + j = 0; + IL_EACH(g_spawnpoints, true, + { + if (navigation_findnearestwaypoint(it, false)) + { + if(it.spawnpointmodel) + { + delete(it.spawnpointmodel); + it.spawnpointmodel = NULL; + } + } + else + { + if(!it.spawnpointmodel) + { + tracebox(it.origin, PL_MIN_CONST, PL_MAX_CONST, it.origin - '0 0 512', MOVE_NOMONSTERS, NULL); + entity e = new(spawnpointmodel); + vector org = trace_endpos + eZ; + setorigin(e, org); + e.solid = SOLID_TRIGGER; + it.spawnpointmodel = e; + } + LOG_INFO("spawn without waypoint: ", etos(it), " ", vtos(it.origin), "\n"); + it.spawnpointmodel.effects |= EF_NODEPTHTEST; + _setmodel(it.spawnpointmodel, pl.model); + it.spawnpointmodel.frame = pl.frame; + it.spawnpointmodel.skin = pl.skin; + it.spawnpointmodel.colormap = pl.colormap; + it.spawnpointmodel.colormod = pl.colormod; + it.spawnpointmodel.glowmod = pl.glowmod; + setsize(it.spawnpointmodel, PL_MIN_CONST, PL_MAX_CONST); + j++; + } + }); + if (j) LOG_INFOF("%d spawnpoints have no nearest waypoint (marked by player model)\n", j); + + j = 0; + IL_EACH(g_items, true, + { + it.effects &= ~(EF_NODEPTHTEST | EF_RED | EF_BLUE); + it.colormod = '0.5 0.5 0.5'; + }); + IL_EACH(g_items, true, + { + if (navigation_findnearestwaypoint(it, false)) + continue; + LOG_INFO("item without waypoint: ", etos(it), " ", vtos(it.origin), "\n"); + it.effects |= EF_NODEPTHTEST | EF_RED; + it.colormod_x = 8; + j++; + }); + if (j) LOG_INFOF("%d items have no nearest waypoint and cannot be walked away from (marked with red light)\n", j); + + j = 0; + IL_EACH(g_items, true, + { + if (navigation_findnearestwaypoint(it, true)) + continue; + LOG_INFO("item without waypoint: ", etos(it), " ", vtos(it.origin), "\n"); + it.effects |= EF_NODEPTHTEST | EF_BLUE; + it.colormod_z = 8; + j++; + }); + if (j) LOG_INFOF("%d items have no nearest waypoint and cannot be walked to (marked with blue light)\n", j); +} + +vector waypoint_getSymmetricalOrigin(vector org, int ctf_flags) +{ + vector new_org = org; + if (fabs(autocvar_g_waypointeditor_symmetrical) == 1) + { + vector map_center = havocbot_middlepoint; + if (autocvar_g_waypointeditor_symmetrical == -1) + map_center = autocvar_g_waypointeditor_symmetrical_origin; + + new_org = Rotate(org - map_center, 360 * DEG2RAD / ctf_flags) + map_center; + } + else if (fabs(autocvar_g_waypointeditor_symmetrical) == 2) + { + float m = havocbot_symmetryaxis_equation.x; + float q = havocbot_symmetryaxis_equation.y; + if (autocvar_g_waypointeditor_symmetrical == -2) + { + m = autocvar_g_waypointeditor_symmetrical_axis.x; + q = autocvar_g_waypointeditor_symmetrical_axis.y; + } + + new_org.x = (1 / (1 + m*m)) * ((1 - m*m) * org.x + 2 * m * org.y - 2 * m * q); + new_org.y = (1 / (1 + m*m)) * (2 * m * org.x + (m*m - 1) * org.y + 2 * q); + } + new_org.z = org.z; + return new_org; +} + void waypoint_setupmodel(entity wp) { if (autocvar_g_waypointeditor) @@ -31,6 +170,8 @@ void waypoint_setupmodel(entity wp) wp.colormod = '1 0 0'; else if (wp.wpflags & WAYPOINTFLAG_GENERATED) wp.colormod = '1 1 0'; + else if (wp.wphardwired) + wp.colormod = '0.5 0 1'; else wp.colormod = '1 1 1'; } @@ -38,14 +179,13 @@ void waypoint_setupmodel(entity wp) wp.model = ""; } -// create a new spawnfunc_waypoint and automatically link it to other waypoints, and link -// them back to it as well -// (suitable for spawnfunc_waypoint editor) entity waypoint_spawn(vector m1, vector m2, float f) { - if(!(f & WAYPOINTFLAG_PERSONAL)) + if(!(f & (WAYPOINTFLAG_PERSONAL | WAYPOINTFLAG_GENERATED)) && m1 == m2) { - IL_EACH(g_waypoints, boxesoverlap(m1, m2, it.absmin, it.absmax), + vector em1 = m1 - '8 8 8'; + vector em2 = m2 + '8 8 8'; + IL_EACH(g_waypoints, boxesoverlap(em1, em2, it.absmin, it.absmax), { return it; }); @@ -92,6 +232,120 @@ entity waypoint_spawn(vector m1, vector m2, float f) return w; } +void waypoint_spawn_fromeditor(entity pl) +{ + entity e; + vector org = pl.origin; + int ctf_flags = havocbot_symmetryaxis_equation.z; + bool sym = ((autocvar_g_waypointeditor_symmetrical > 0 && ctf_flags >= 2) + || (autocvar_g_waypointeditor_symmetrical < 0)); + int order = ctf_flags; + if(autocvar_g_waypointeditor_symmetrical_order >= 2) + { + order = autocvar_g_waypointeditor_symmetrical_order; + ctf_flags = order; + } + + if(!PHYS_INPUT_BUTTON_CROUCH(pl)) + { + // snap waypoint to item's origin if close enough + IL_EACH(g_items, true, + { + vector item_org = (it.absmin + it.absmax) * 0.5; + item_org.z = it.absmin.z - PL_MIN_CONST.z; + if(vlen(item_org - org) < 30) + { + org = item_org; + break; + } + }); + } + + LABEL(add_wp); + e = waypoint_spawn(org, org, 0); + if(!e) + { + LOG_INFOF("Couldn't spawn waypoint at %v\n", org); + return; + } + waypoint_schedulerelink(e); + bprint(strcat("Waypoint spawned at ", vtos(e.origin), "\n")); + if(sym) + { + org = waypoint_getSymmetricalOrigin(e.origin, ctf_flags); + if (vdist(org - pl.origin, >, 32)) + { + if(order > 2) + order--; + else + sym = false; + goto add_wp; + } + } +} + +void waypoint_remove(entity wp) +{ + // tell all waypoints linked to wp that they need to relink + IL_EACH(g_waypoints, it != wp, + { + if (waypoint_islinked(it, wp)) + waypoint_removelink(it, wp); + }); + delete(wp); +} + +void waypoint_remove_fromeditor(entity pl) +{ + entity e = navigation_findnearestwaypoint(pl, false); + + int ctf_flags = havocbot_symmetryaxis_equation.z; + bool sym = ((autocvar_g_waypointeditor_symmetrical > 0 && ctf_flags >= 2) + || (autocvar_g_waypointeditor_symmetrical < 0)); + int order = ctf_flags; + if(autocvar_g_waypointeditor_symmetrical_order >= 2) + { + order = autocvar_g_waypointeditor_symmetrical_order; + ctf_flags = order; + } + + LABEL(remove_wp); + if (!e) return; + if (e.wpflags & WAYPOINTFLAG_GENERATED) return; + + if (e.wphardwired) + { + LOG_INFO("^1Warning: ^7Removal of hardwired waypoints is not allowed in the editor. Please remove links from/to this waypoint (", vtos(e.origin), ") by hand from maps/", mapname, ".waypoints.hardwired\n"); + return; + } + + entity wp_sym = NULL; + if (sym) + { + vector org = waypoint_getSymmetricalOrigin(e.origin, ctf_flags); + FOREACH_ENTITY_CLASS("waypoint", !(it.wpflags & WAYPOINTFLAG_GENERATED), { + if(vdist(org - it.origin, <, 3)) + { + wp_sym = it; + break; + } + }); + } + + bprint(strcat("Waypoint removed at ", vtos(e.origin), "\n")); + waypoint_remove(e); + + if (sym && wp_sym) + { + e = wp_sym; + if(order > 2) + order--; + else + sym = false; + goto remove_wp; + } +} + void waypoint_removelink(entity from, entity to) { if (from == to || (from.wpflags & WAYPOINTFLAG_NORELINK)) @@ -145,41 +399,113 @@ bool waypoint_islinked(entity from, entity to) return false; } -// add a new link to the spawnfunc_waypoint, replacing the furthest link it already has -void waypoint_addlink(entity from, entity to) +void waypoint_updatecost_foralllinks() { - float c; + IL_EACH(g_waypoints, !(it.wpflags & WAYPOINTFLAG_TELEPORT), + { + if(it.wp00) it.wp00mincost = waypoint_getlinkcost(it, it.wp00); + if(it.wp01) it.wp01mincost = waypoint_getlinkcost(it, it.wp01); + if(it.wp02) it.wp02mincost = waypoint_getlinkcost(it, it.wp02); + if(it.wp03) it.wp03mincost = waypoint_getlinkcost(it, it.wp03); + if(it.wp04) it.wp04mincost = waypoint_getlinkcost(it, it.wp04); + if(it.wp05) it.wp05mincost = waypoint_getlinkcost(it, it.wp05); + if(it.wp06) it.wp06mincost = waypoint_getlinkcost(it, it.wp06); + if(it.wp07) it.wp07mincost = waypoint_getlinkcost(it, it.wp07); + if(it.wp08) it.wp08mincost = waypoint_getlinkcost(it, it.wp08); + if(it.wp09) it.wp09mincost = waypoint_getlinkcost(it, it.wp09); + if(it.wp10) it.wp10mincost = waypoint_getlinkcost(it, it.wp10); + if(it.wp11) it.wp11mincost = waypoint_getlinkcost(it, it.wp11); + if(it.wp12) it.wp12mincost = waypoint_getlinkcost(it, it.wp12); + if(it.wp13) it.wp13mincost = waypoint_getlinkcost(it, it.wp13); + if(it.wp14) it.wp14mincost = waypoint_getlinkcost(it, it.wp14); + if(it.wp15) it.wp15mincost = waypoint_getlinkcost(it, it.wp15); + if(it.wp16) it.wp16mincost = waypoint_getlinkcost(it, it.wp16); + if(it.wp17) it.wp17mincost = waypoint_getlinkcost(it, it.wp17); + if(it.wp18) it.wp18mincost = waypoint_getlinkcost(it, it.wp18); + if(it.wp19) it.wp19mincost = waypoint_getlinkcost(it, it.wp19); + if(it.wp20) it.wp20mincost = waypoint_getlinkcost(it, it.wp20); + if(it.wp21) it.wp21mincost = waypoint_getlinkcost(it, it.wp21); + if(it.wp22) it.wp22mincost = waypoint_getlinkcost(it, it.wp22); + if(it.wp23) it.wp23mincost = waypoint_getlinkcost(it, it.wp23); + if(it.wp24) it.wp24mincost = waypoint_getlinkcost(it, it.wp24); + if(it.wp25) it.wp25mincost = waypoint_getlinkcost(it, it.wp25); + if(it.wp26) it.wp26mincost = waypoint_getlinkcost(it, it.wp26); + if(it.wp27) it.wp27mincost = waypoint_getlinkcost(it, it.wp27); + if(it.wp28) it.wp28mincost = waypoint_getlinkcost(it, it.wp28); + if(it.wp29) it.wp29mincost = waypoint_getlinkcost(it, it.wp29); + if(it.wp30) it.wp30mincost = waypoint_getlinkcost(it, it.wp30); + if(it.wp31) it.wp31mincost = waypoint_getlinkcost(it, it.wp31); + }); +} - if (from == to) - return; - if (from.wpflags & WAYPOINTFLAG_NORELINK) - return; +float waypoint_getlinearcost(float dist) +{ + if(skill >= autocvar_bot_ai_bunnyhop_skilloffset) + return dist / (autocvar_sv_maxspeed * 1.25); + return dist / autocvar_sv_maxspeed; +} +float waypoint_getlinearcost_underwater(float dist) +{ + // NOTE: this value is hardcoded on the engine too, see SV_WaterMove + return dist / (autocvar_sv_maxspeed * 0.7); +} - if (waypoint_islinked(from, to)) - return; +float waypoint_gettravelcost(vector from, vector to, entity from_ent, entity to_ent) +{ + bool submerged_from = navigation_check_submerged_state(from_ent, from); + bool submerged_to = navigation_check_submerged_state(to_ent, to); + + if (submerged_from && submerged_to) + return waypoint_getlinearcost_underwater(vlen(to - from)); + + float c = waypoint_getlinearcost(vlen(to - from)); - if (to.wpisbox || from.wpisbox) + float height = from.z - to.z; + if(height > jumpheight_vec.z && autocvar_sv_gravity > 0) { - // if either is a box we have to find the nearest points on them to - // calculate the distance properly - vector v1, v2, m1, m2; - v1 = from.origin; - m1 = to.absmin; - m2 = to.absmax; - v1_x = bound(m1_x, v1_x, m2_x); - v1_y = bound(m1_y, v1_y, m2_y); - v1_z = bound(m1_z, v1_z, m2_z); - v2 = to.origin; - m1 = from.absmin; - m2 = from.absmax; - v2_x = bound(m1_x, v2_x, m2_x); - v2_y = bound(m1_y, v2_y, m2_y); - v2_z = bound(m1_z, v2_z, m2_z); - v2 = to.origin; - c = vlen(v2 - v1); + float height_cost = sqrt(height / (autocvar_sv_gravity / 2)); + c = waypoint_getlinearcost(vlen(vec2(to - from))); // xy distance cost + if(height_cost > c) + c = height_cost; } - else - c = vlen(to.origin - from.origin); + + if (submerged_from || submerged_to) + return (c + waypoint_getlinearcost_underwater(vlen(to - from))) / 2; + return c; +} + +float waypoint_getlinkcost(entity from, entity to) +{ + vector v1 = from.origin; + vector v2 = to.origin; + if (from.wpisbox) + { + vector m1 = from.absmin, m2 = from.absmax; + v1.x = bound(m1.x, v2.x, m2.x); + v1.y = bound(m1.y, v2.y, m2.y); + v1.z = bound(m1.z, v2.z, m2.z); + } + if (to.wpisbox) + { + vector m1 = to.absmin, m2 = to.absmax; + v2.x = bound(m1.x, v1.x, m2.x); + v2.y = bound(m1.y, v1.y, m2.y); + v2.z = bound(m1.z, v1.z, m2.z); + } + return waypoint_gettravelcost(v1, v2, from, to); +} + +// add a new link to the spawnfunc_waypoint, replacing the furthest link it already has +// if c == -1 automatically determine cost of the link +void waypoint_addlink_customcost(entity from, entity to, float c) +{ + if (from == to || waypoint_islinked(from, to)) + return; + if (c == -1 && (from.wpflags & WAYPOINTFLAG_NORELINK)) + return; + + if(c == -1) + c = waypoint_getlinkcost(from, to); if (from.wp31mincost < c) return; if (from.wp30mincost < c) {from.wp31 = to;from.wp31mincost = c;return;} from.wp31 = from.wp30;from.wp31mincost = from.wp30mincost; @@ -216,20 +542,24 @@ void waypoint_addlink(entity from, entity to) from.wp00 = to;from.wp00mincost = c;return; } +void waypoint_addlink(entity from, entity to) +{ + waypoint_addlink_customcost(from, to, -1); +} + // relink this spawnfunc_waypoint // (precompile a list of all reachable waypoints from this spawnfunc_waypoint) // (SLOW!) void waypoint_think(entity this) { - vector sv, sm1, sm2, ev, em1, em2, dv; + vector sv = '0 0 0', sv2 = '0 0 0', ev = '0 0 0', ev2 = '0 0 0', dv; + float sv2_height = 0, ev2_height = 0; bot_calculate_stepheightvec(); bot_navigation_movemode = ((autocvar_bot_navigation_ignoreplayers) ? MOVE_NOMONSTERS : MOVE_NORMAL); //dprint("waypoint_think wpisbox = ", ftos(this.wpisbox), "\n"); - sm1 = this.origin + this.mins; - sm2 = this.origin + this.maxs; IL_EACH(g_waypoints, this != it, { if (boxesoverlap(this.absmin, this.absmax, it.absmin, it.absmax)) @@ -245,16 +575,14 @@ void waypoint_think(entity this) ++relink_pvsculled; continue; } - sv = it.origin; - sv.x = bound(sm1_x, sv.x, sm2_x); - sv.y = bound(sm1_y, sv.y, sm2_y); - sv.z = bound(sm1_z, sv.z, sm2_z); - ev = this.origin; - em1 = it.origin + it.mins; - em2 = it.origin + it.maxs; - ev.x = bound(em1_x, ev.x, em2_x); - ev.y = bound(em1_y, ev.y, em2_y); - ev.z = bound(em1_z, ev.z, em2_z); + + sv = set_tracewalk_dest_2(this, it.origin); + sv2 = tracewalk_dest; + sv2_height = tracewalk_dest_height; + ev = set_tracewalk_dest_2(it, this.origin); + ev2 = tracewalk_dest; + ev2_height = tracewalk_dest_height; + dv = ev - sv; dv.z = 0; if(vdist(dv, >=, 1050)) // max search distance in XY @@ -262,35 +590,30 @@ void waypoint_think(entity this) ++relink_lengthculled; continue; } + navigation_testtracewalk = 0; - if (!this.wpisbox) - { - tracebox(sv - PL_MIN_CONST.z * '0 0 1', PL_MIN_CONST, PL_MAX_CONST, sv, false, this); - if (!trace_startsolid) - { - //dprint("sv deviation", vtos(trace_endpos - sv), "\n"); - sv = trace_endpos + '0 0 1'; - } - } - if (!it.wpisbox) - { - tracebox(ev - PL_MIN_CONST.z * '0 0 1', PL_MIN_CONST, PL_MAX_CONST, ev, false, it); - if (!trace_startsolid) - { - //dprint("ev deviation", vtos(trace_endpos - ev), "\n"); - ev = trace_endpos + '0 0 1'; - } - } + //traceline(this.origin, it.origin, false, NULL); //if (trace_fraction == 1) - if (!this.wpisbox && tracewalk(this, sv, PL_MIN_CONST, PL_MAX_CONST, ev, MOVE_NOMONSTERS)) - waypoint_addlink(this, it); - else + if (this.wpisbox) relink_walkculled += 0.5; - if (!it.wpisbox && tracewalk(it, ev, PL_MIN_CONST, PL_MAX_CONST, sv, MOVE_NOMONSTERS)) - waypoint_addlink(it, this); else + { + if (tracewalk(this, sv, PL_MIN_CONST, PL_MAX_CONST, ev2, ev2_height, MOVE_NOMONSTERS)) + waypoint_addlink(this, it); + else + relink_walkculled += 0.5; + } + + if (it.wpisbox) relink_walkculled += 0.5; + else + { + if (tracewalk(it, ev, PL_MIN_CONST, PL_MAX_CONST, sv2, sv2_height, MOVE_NOMONSTERS)) + waypoint_addlink(it, this); + else + relink_walkculled += 0.5; + } } }); navigation_testtracewalk = 0; @@ -344,25 +667,6 @@ spawnfunc(waypoint) //waypoint_schedulerelink(this); } -void waypoint_remove(entity wp) -{ - // tell all waypoints linked to wp that they need to relink - IL_EACH(g_waypoints, it != wp, - { - if (waypoint_islinked(it, wp)) - waypoint_removelink(it, wp); - }); - delete(wp); -} - -void waypoint_removeall() -{ - IL_EACH(g_waypoints, true, - { - delete(it); - }); -} - // tell all waypoints to relink // actually this is useful only to update relink_* stats void waypoint_schedulerelinkall() @@ -375,33 +679,76 @@ void waypoint_schedulerelinkall() waypoint_load_links_hardwired(); } +#define GET_GAMETYPE_EXTENSION() ((g_race) ? ".race" : "") + // Load waypoint links from file -float waypoint_load_links() +bool waypoint_load_links() { - string filename, s; + string s; float file, tokens, c = 0, found; entity wp_from = NULL, wp_to; vector wp_to_pos, wp_from_pos; - filename = strcat("maps/", mapname); - filename = strcat(filename, ".waypoints.cache"); + + string gt_ext = GET_GAMETYPE_EXTENSION(); + + string filename = sprintf("maps/%s.waypoints.cache", strcat(mapname, gt_ext)); file = fopen(filename, FILE_READ); + if (gt_ext != "" && file < 0) + { + // if race waypoint file doesn't exist load the default one + filename = sprintf("maps/%s.waypoints.cache", mapname); + file = fopen(filename, FILE_READ); + } + if (file < 0) { - LOG_TRACE("waypoint links load from "); - LOG_TRACE(filename); - LOG_TRACE(" failed"); + LOG_TRACE("waypoint links load from ", filename, " failed"); + waypoint_schedulerelinkall(); return false; } + bool parse_comments = true; + float ver = 0; + while ((s = fgets(file))) { + if(parse_comments) + { + if(substring(s, 0, 2) == "//") + { + if(substring(s, 2, 17) == "WAYPOINT_VERSION ") + ver = stof(substring(s, 19, -1)); + continue; + } + else + { + if(ver < WAYPOINT_VERSION) + { + LOG_TRACE("waypoint links for this map are outdated."); + if (g_assault) + { + LOG_TRACE("Assault waypoint links need to be manually updated in the editor"); + } + else + { + LOG_TRACE("automatically updating..."); + waypoint_schedulerelinkall(); + fclose(file); + return false; + } + } + parse_comments = false; + } + } + tokens = tokenizebyseparator(s, "*"); if (tokens!=2) { // bad file format fclose(file); + waypoint_schedulerelinkall(); // link all the autogenerated waypoints (teleporters) return false; } @@ -429,7 +776,6 @@ float waypoint_load_links() LOG_TRACE("waypoint_load_links: couldn't find 'from' waypoint at ", vtos(wp_from_pos)); continue; } - } // Search "to" waypoint @@ -458,7 +804,19 @@ float waypoint_load_links() fclose(file); - LOG_TRACE("loaded ", ftos(c), " waypoint links from maps/", mapname, ".waypoints.cache"); + LOG_TRACE("loaded ", ftos(c), " waypoint links from ", filename); + + bool scheduled = false; + IL_EACH(g_waypoints, it.wpflags & WAYPOINTFLAG_ITEM, + { + if (!it.wp00) + { + waypoint_schedulerelink(it); + scheduled = true; + } + }); + if (scheduled) + return false; botframe_cachedwaypointlinks = true; return true; @@ -466,14 +824,23 @@ float waypoint_load_links() void waypoint_load_or_remove_links_hardwired(bool removal_mode) { - string filename, s; + string s; float file, tokens, c = 0, found; entity wp_from = NULL, wp_to; vector wp_to_pos, wp_from_pos; - filename = strcat("maps/", mapname); - filename = strcat(filename, ".waypoints.hardwired"); + + string gt_ext = GET_GAMETYPE_EXTENSION(); + + string filename = sprintf("maps/%s.waypoints.hardwired", strcat(mapname, gt_ext)); file = fopen(filename, FILE_READ); + if (gt_ext != "" && file < 0) + { + // if race waypoint file doesn't exist load the default one + filename = sprintf("maps/%s.waypoints.hardwired", mapname); + file = fopen(filename, FILE_READ); + } + botframe_loadedforcedlinks = true; if (file < 0) @@ -554,17 +921,16 @@ void waypoint_load_or_remove_links_hardwired(bool removal_mode) waypoint_addlink(wp_from, wp_to); wp_from.wphardwired = true; wp_to.wphardwired = true; + waypoint_setupmodel(wp_from); + waypoint_setupmodel(wp_to); } fclose(file); - if(!removal_mode) - LOG_TRACE("loaded ", ftos(c), " waypoint links from maps/", mapname, ".waypoints.hardwired"); + LOG_TRACE(((removal_mode) ? "unloaded " : "loaded "), + ftos(c), " waypoint links from maps/", mapname, ".waypoints.hardwired"); } -void waypoint_load_links_hardwired() { waypoint_load_or_remove_links_hardwired(false); } -void waypoint_remove_links_hardwired() { waypoint_load_or_remove_links_hardwired(true); } - entity waypoint_get_link(entity w, float i) { switch(i) @@ -611,7 +977,9 @@ void waypoint_save_links() // temporarily remove hardwired links so they don't get saved among normal links waypoint_remove_links_hardwired(); - string filename = sprintf("maps/%s.waypoints.cache", mapname); + string gt_ext = GET_GAMETYPE_EXTENSION(); + + string filename = sprintf("maps/%s.waypoints.cache", strcat(mapname, gt_ext)); int file = fopen(filename, FILE_WRITE); if (file < 0) { @@ -619,6 +987,8 @@ void waypoint_save_links() return; } + fputs(file, strcat("//", "WAYPOINT_VERSION ", ftos_decimals(WAYPOINT_VERSION, 2), "\n")); + int c = 0; IL_EACH(g_waypoints, true, { @@ -634,9 +1004,10 @@ void waypoint_save_links() } }); fclose(file); + botframe_cachedwaypointlinks = true; - LOG_INFOF("saved %d waypoint links to maps/%s.waypoints.cache", c, mapname); + LOG_INFOF("saved %d waypoint links to %s", c, filename); waypoint_load_links_hardwired(); } @@ -644,7 +1015,9 @@ void waypoint_save_links() // save waypoints to gamedir/data/maps/mapname.waypoints void waypoint_saveall() { - string filename = sprintf("maps/%s.waypoints", mapname); + string gt_ext = GET_GAMETYPE_EXTENSION(); + + string filename = sprintf("maps/%s.waypoints", strcat(mapname, gt_ext)); int file = fopen(filename, FILE_WRITE); if (file < 0) { @@ -655,6 +1028,12 @@ void waypoint_saveall() return; } + // add 3 comments to not break compatibility with older Xonotic versions + // (they are read as a waypoint with origin '0 0 0' and flag 0 though) + fputs(file, strcat("//", "WAYPOINT_VERSION ", ftos_decimals(WAYPOINT_VERSION, 2), "\n")); + fputs(file, strcat("//", "\n")); + fputs(file, strcat("//", "\n")); + int c = 0; IL_EACH(g_waypoints, true, { @@ -674,63 +1053,99 @@ void waypoint_saveall() waypoint_save_links(); botframe_loadedforcedlinks = false; - LOG_INFOF("saved %d waypoints to maps/%s.waypoints", c, mapname); + LOG_INFOF("saved %d waypoints to %s", c, filename); } // load waypoints from file float waypoint_loadall() { - string filename, s; + string s; float file, cwp, cwb, fl; vector m1, m2; cwp = 0; cwb = 0; - filename = strcat("maps/", mapname); - filename = strcat(filename, ".waypoints"); + + string gt_ext = GET_GAMETYPE_EXTENSION(); + + string filename = sprintf("maps/%s.waypoints", strcat(mapname, gt_ext)); file = fopen(filename, FILE_READ); - if (file >= 0) + + if (gt_ext != "" && file < 0) { - while ((s = fgets(file))) - { - m1 = stov(s); - s = fgets(file); - if (!s) - break; - m2 = stov(s); - s = fgets(file); - if (!s) - break; - fl = stof(s); - waypoint_spawn(m1, m2, fl); - if (m1 == m2) - cwp = cwp + 1; - else - cwb = cwb + 1; - } - fclose(file); - LOG_TRACE("loaded ", ftos(cwp), " waypoints and ", ftos(cwb), " wayboxes from maps/", mapname, ".waypoints"); + // if race waypoint file doesn't exist load the default one + filename = sprintf("maps/%s.waypoints", mapname); + file = fopen(filename, FILE_READ); } - else + + if (file < 0) { LOG_TRACE("waypoint load from ", filename, " failed"); + return 0; } + + bool parse_comments = true; + float ver = 0; + + while ((s = fgets(file))) + { + if(parse_comments) + { + if(substring(s, 0, 2) == "//") + { + if(substring(s, 2, 17) == "WAYPOINT_VERSION ") + ver = stof(substring(s, 19, -1)); + continue; + } + else + { + if(floor(ver) < floor(WAYPOINT_VERSION)) + { + LOG_TRACE("waypoints for this map are outdated"); + LOG_TRACE("please update them in the editor"); + } + parse_comments = false; + } + } + m1 = stov(s); + s = fgets(file); + if (!s) + break; + m2 = stov(s); + s = fgets(file); + if (!s) + break; + fl = stof(s); + waypoint_spawn(m1, m2, fl); + if (m1 == m2) + cwp = cwp + 1; + else + cwb = cwb + 1; + } + fclose(file); + LOG_TRACE("loaded ", ftos(cwp), " waypoints and ", ftos(cwb), " wayboxes from maps/", mapname, ".waypoints"); + return cwp + cwb; } -vector waypoint_fixorigin(vector position) +#define waypoint_fixorigin(position, tracetest_ent) \ + waypoint_fixorigin_down_dir(position, tracetest_ent, '0 0 -1') + +vector waypoint_fixorigin_down_dir(vector position, entity tracetest_ent, vector down_dir) { - tracebox(position + '0 0 1' * (1 - PL_MIN_CONST.z), PL_MIN_CONST, PL_MAX_CONST, position + '0 0 -512', MOVE_NOMONSTERS, NULL); + tracebox(position + '0 0 1', PL_MIN_CONST, PL_MAX_CONST, position + down_dir * 3000, MOVE_NOMONSTERS, tracetest_ent); + if(trace_startsolid) + tracebox(position + '0 0 1' * (1 - PL_MIN_CONST.z / 2), PL_MIN_CONST, PL_MAX_CONST, position + down_dir * 3000, MOVE_NOMONSTERS, tracetest_ent); + if(trace_startsolid) + tracebox(position + '0 0 1' * (1 - PL_MIN_CONST.z), PL_MIN_CONST, PL_MAX_CONST, position + down_dir * 3000, MOVE_NOMONSTERS, tracetest_ent); if(trace_fraction < 1) position = trace_endpos; - //traceline(position, position + '0 0 -512', MOVE_NOMONSTERS, NULL); - //print("position is ", ftos(trace_endpos_z - position_z), " above solid\n"); return position; } void waypoint_spawnforitem_force(entity e, vector org) { // Fix the waypoint altitude if necessary - org = waypoint_fixorigin(org); + org = waypoint_fixorigin(org, NULL); // don't spawn an item spawnfunc_waypoint if it already exists IL_EACH(g_waypoints, true, @@ -764,11 +1179,11 @@ void waypoint_spawnforitem(entity e) waypoint_spawnforitem_force(e, e.origin); } -void waypoint_spawnforteleporter_boxes(entity e, vector org1, vector org2, vector destination1, vector destination2, float timetaken) +void waypoint_spawnforteleporter_boxes(entity e, int teleport_flag, vector org1, vector org2, vector destination1, vector destination2, float timetaken) { entity w; entity dw; - w = waypoint_spawn(org1, org2, WAYPOINTFLAG_GENERATED | WAYPOINTFLAG_TELEPORT | WAYPOINTFLAG_NORELINK); + w = waypoint_spawn(org1, org2, WAYPOINTFLAG_GENERATED | teleport_flag | WAYPOINTFLAG_NORELINK); dw = waypoint_spawn(destination1, destination2, WAYPOINTFLAG_GENERATED); // one way link to the destination w.wp00 = dw; @@ -779,17 +1194,25 @@ void waypoint_spawnforteleporter_boxes(entity e, vector org1, vector org2, vecto e.nearestwaypointtimeout = -1; } -void waypoint_spawnforteleporter_v(entity e, vector org, vector destination, float timetaken) +void waypoint_spawnforteleporter_wz(entity e, vector org, vector destination, float timetaken, vector down_dir, entity tracetest_ent) { - org = waypoint_fixorigin(org); - destination = waypoint_fixorigin(destination); - waypoint_spawnforteleporter_boxes(e, org, org, destination, destination, timetaken); + // warpzones with oblique warp plane rely on down_dir to snap waypoints + // to the ground without leaving the warp plane + // warpzones with horizontal warp plane (down_dir.x == -1) generate + // destination waypoint snapped to the ground (leaving warpzone), source + // waypoint in the center of the warp plane + if(down_dir.x != -1) + org = waypoint_fixorigin_down_dir(org, tracetest_ent, down_dir); + if(down_dir.x == -1) + down_dir = '0 0 -1'; + destination = waypoint_fixorigin_down_dir(destination, tracetest_ent, down_dir); + waypoint_spawnforteleporter_boxes(e, WAYPOINTFLAG_TELEPORT, org, org, destination, destination, timetaken); } -void waypoint_spawnforteleporter(entity e, vector destination, float timetaken) +void waypoint_spawnforteleporter(entity e, vector destination, float timetaken, entity tracetest_ent) { - destination = waypoint_fixorigin(destination); - waypoint_spawnforteleporter_boxes(e, e.absmin, e.absmax, destination, destination, timetaken); + destination = waypoint_fixorigin(destination, tracetest_ent); + waypoint_spawnforteleporter_boxes(e, WAYPOINTFLAG_TELEPORT, e.absmin - PL_MAX_CONST + '1 1 1', e.absmax - PL_MIN_CONST + '-1 -1 -1', destination, destination, timetaken); } entity waypoint_spawnpersonal(entity this, vector position) @@ -799,7 +1222,7 @@ entity waypoint_spawnpersonal(entity this, vector position) // drop the waypoint to a proper location: // first move it up by a player height // then move it down to hit the floor with player bbox size - position = waypoint_fixorigin(position); + position = waypoint_fixorigin(position, this); w = waypoint_spawn(position, position, WAYPOINTFLAG_GENERATED | WAYPOINTFLAG_PERSONAL); w.nearestwaypoint = NULL; @@ -822,6 +1245,35 @@ void waypoint_showlink(entity wp1, entity wp2, int display_type) te_lightning2(NULL, wp1.origin, wp2.origin); } +void waypoint_showlinks_to(entity wp, int display_type) +{ + IL_EACH(g_waypoints, it != wp, + { + if (waypoint_islinked(it, wp)) + waypoint_showlink(it, wp, display_type); + }); +} + +void waypoint_showlinks_from(entity wp, int display_type) +{ + waypoint_showlink(wp.wp00, wp, display_type); waypoint_showlink(wp.wp16, wp, display_type); + waypoint_showlink(wp.wp01, wp, display_type); waypoint_showlink(wp.wp17, wp, display_type); + waypoint_showlink(wp.wp02, wp, display_type); waypoint_showlink(wp.wp18, wp, display_type); + waypoint_showlink(wp.wp03, wp, display_type); waypoint_showlink(wp.wp19, wp, display_type); + waypoint_showlink(wp.wp04, wp, display_type); waypoint_showlink(wp.wp20, wp, display_type); + waypoint_showlink(wp.wp05, wp, display_type); waypoint_showlink(wp.wp21, wp, display_type); + waypoint_showlink(wp.wp06, wp, display_type); waypoint_showlink(wp.wp22, wp, display_type); + waypoint_showlink(wp.wp07, wp, display_type); waypoint_showlink(wp.wp23, wp, display_type); + waypoint_showlink(wp.wp08, wp, display_type); waypoint_showlink(wp.wp24, wp, display_type); + waypoint_showlink(wp.wp09, wp, display_type); waypoint_showlink(wp.wp25, wp, display_type); + waypoint_showlink(wp.wp10, wp, display_type); waypoint_showlink(wp.wp26, wp, display_type); + waypoint_showlink(wp.wp11, wp, display_type); waypoint_showlink(wp.wp27, wp, display_type); + waypoint_showlink(wp.wp12, wp, display_type); waypoint_showlink(wp.wp28, wp, display_type); + waypoint_showlink(wp.wp13, wp, display_type); waypoint_showlink(wp.wp29, wp, display_type); + waypoint_showlink(wp.wp14, wp, display_type); waypoint_showlink(wp.wp30, wp, display_type); + waypoint_showlink(wp.wp15, wp, display_type); waypoint_showlink(wp.wp31, wp, display_type); +} + void botframe_showwaypointlinks() { if (time < botframe_waypointeditorlightningtime) @@ -844,38 +1296,10 @@ void botframe_showwaypointlinks() if (head) { te_lightning2(NULL, head.origin, it.origin); - waypoint_showlink(head.wp00, head, display_type); - waypoint_showlink(head.wp01, head, display_type); - waypoint_showlink(head.wp02, head, display_type); - waypoint_showlink(head.wp03, head, display_type); - waypoint_showlink(head.wp04, head, display_type); - waypoint_showlink(head.wp05, head, display_type); - waypoint_showlink(head.wp06, head, display_type); - waypoint_showlink(head.wp07, head, display_type); - waypoint_showlink(head.wp08, head, display_type); - waypoint_showlink(head.wp09, head, display_type); - waypoint_showlink(head.wp10, head, display_type); - waypoint_showlink(head.wp11, head, display_type); - waypoint_showlink(head.wp12, head, display_type); - waypoint_showlink(head.wp13, head, display_type); - waypoint_showlink(head.wp14, head, display_type); - waypoint_showlink(head.wp15, head, display_type); - waypoint_showlink(head.wp16, head, display_type); - waypoint_showlink(head.wp17, head, display_type); - waypoint_showlink(head.wp18, head, display_type); - waypoint_showlink(head.wp19, head, display_type); - waypoint_showlink(head.wp20, head, display_type); - waypoint_showlink(head.wp21, head, display_type); - waypoint_showlink(head.wp22, head, display_type); - waypoint_showlink(head.wp23, head, display_type); - waypoint_showlink(head.wp24, head, display_type); - waypoint_showlink(head.wp25, head, display_type); - waypoint_showlink(head.wp26, head, display_type); - waypoint_showlink(head.wp27, head, display_type); - waypoint_showlink(head.wp28, head, display_type); - waypoint_showlink(head.wp29, head, display_type); - waypoint_showlink(head.wp30, head, display_type); - waypoint_showlink(head.wp31, head, display_type); + if(PHYS_INPUT_BUTTON_CROUCH(it)) + waypoint_showlinks_to(head, display_type); + else + waypoint_showlinks_from(head, display_type); } } }); @@ -927,7 +1351,7 @@ float botframe_autowaypoints_fix_from(entity p, float walkfromwp, entity wp, .en // if wp -> porg, then OK float maxdist; - if(navigation_waypoint_will_link(wp.origin, porg, p, walkfromwp, 1050)) + if(navigation_waypoint_will_link(wp.origin, porg, p, porg, 0, wp.origin, 0, walkfromwp, 1050)) { // we may find a better one maxdist = vlen(wp.origin - porg); @@ -943,8 +1367,8 @@ float botframe_autowaypoints_fix_from(entity p, float walkfromwp, entity wp, .en { float d = vlen(wp.origin - it.origin) + vlen(it.origin - porg); if(d < bestdist) - if(navigation_waypoint_will_link(wp.origin, it.origin, p, walkfromwp, 1050)) - if(navigation_waypoint_will_link(it.origin, porg, p, walkfromwp, 1050)) + if(navigation_waypoint_will_link(wp.origin, it.origin, p, it.origin, 0, wp.origin, 0, walkfromwp, 1050)) + if(navigation_waypoint_will_link(it.origin, porg, p, porg, 0, it.origin, 0, walkfromwp, 1050)) { bestdist = d; p.(fld) = it; @@ -998,7 +1422,7 @@ float botframe_autowaypoints_fix_from(entity p, float walkfromwp, entity wp, .en if(wp) { - if(!navigation_waypoint_will_link(wp.origin, o, p, walkfromwp, 1050)) + if(!navigation_waypoint_will_link(wp.origin, o, p, o, 0, wp.origin, 0, walkfromwp, 1050)) { // we cannot walk from wp.origin to o // get closer to tmax @@ -1024,7 +1448,7 @@ float botframe_autowaypoints_fix_from(entity p, float walkfromwp, entity wp, .en // if we get here, o is valid regarding waypoints // check if o is connected right to the player // we break if it succeeds, as that means o is a good waypoint location - if(navigation_waypoint_will_link(o, porg, p, walkfromwp, 1050)) + if(navigation_waypoint_will_link(o, porg, p, porg, 0, o, 0, walkfromwp, 1050)) break; // o is no good, we need to get closer to the player @@ -1071,6 +1495,8 @@ void botframe_deleteuselesswaypoints() it.wpflags |= WAYPOINTFLAG_USEFUL; if (it.wpflags & WAYPOINTFLAG_TELEPORT) it.wpflags |= WAYPOINTFLAG_USEFUL; + if (it.wpflags & WAYPOINTFLAG_LADDER) + it.wpflags |= WAYPOINTFLAG_USEFUL; if (it.wpflags & WAYPOINTFLAG_PROTECTED) it.wpflags |= WAYPOINTFLAG_USEFUL; // b) WP is closest WP for an item/spawnpoint/other entity diff --git a/qcsrc/server/bot/default/waypoints.qh b/qcsrc/server/bot/default/waypoints.qh index 38d57a04a1..bea11e9299 100644 --- a/qcsrc/server/bot/default/waypoints.qh +++ b/qcsrc/server/bot/default/waypoints.qh @@ -3,6 +3,11 @@ * Globals and Fields */ +// increase by 0.01 when changes require only waypoint relinking +// increase by 1 when changes require to manually edit waypoints +// max 2 decimal places, always specified +#define WAYPOINT_VERSION 1.00 + // fields you can query using prvm_global server to get some statistics about waypoint linking culling float relink_total, relink_walkculled, relink_pvsculled, relink_lengthculled; @@ -10,10 +15,11 @@ float botframe_waypointeditorlightningtime; float botframe_loadedforcedlinks; float botframe_cachedwaypointlinks; +// waypoint wp links to waypoint wp.wpXX (OUTGOING link) +// links are sorted by their cost (wpXXmincost) .entity wp00, wp01, wp02, wp03, wp04, wp05, wp06, wp07, wp08, wp09, wp10, wp11, wp12, wp13, wp14, wp15; .entity wp16, wp17, wp18, wp19, wp20, wp21, wp22, wp23, wp24, wp25, wp26, wp27, wp28, wp29, wp30, wp31; -// itemscore = (howmuchmoreIwant / howmuchIcanwant) / itemdistance .float wp00mincost, wp01mincost, wp02mincost, wp03mincost, wp04mincost, wp05mincost, wp06mincost, wp07mincost; .float wp08mincost, wp09mincost, wp10mincost, wp11mincost, wp12mincost, wp13mincost, wp14mincost, wp15mincost; .float wp16mincost, wp17mincost, wp18mincost, wp19mincost, wp20mincost, wp21mincost, wp22mincost, wp23mincost; @@ -29,30 +35,43 @@ float botframe_cachedwaypointlinks; */ spawnfunc(waypoint); +void waypoint_removelink(entity from, entity to); +bool waypoint_islinked(entity from, entity to); +void waypoint_addlink_customcost(entity from, entity to, float c); void waypoint_addlink(entity from, entity to); void waypoint_think(entity this); void waypoint_clearlinks(entity wp); void waypoint_schedulerelink(entity wp); -void waypoint_remove(entity e); -void waypoint_removeall(); +float waypoint_getlinkcost(entity from, entity to); +float waypoint_gettravelcost(vector from, vector to, entity from_ent, entity to_ent); +float waypoint_getlinearcost(float dist); +void waypoint_updatecost_foralllinks(); + +void waypoint_remove_fromeditor(entity pl); +void waypoint_remove(entity wp); void waypoint_schedulerelinkall(); -void waypoint_load_links_hardwired(); void waypoint_save_links(); void waypoint_saveall(); void waypoint_spawnforitem_force(entity e, vector org); void waypoint_spawnforitem(entity e); -void waypoint_spawnforteleporter(entity e, vector destination, float timetaken); -void waypoint_spawnforteleporter_v(entity e, vector org, vector destination, float timetaken); +void waypoint_spawnforteleporter(entity e, vector destination, float timetaken, entity tracetest_ent); +void waypoint_spawnforteleporter_wz(entity e, vector org, vector destination, float timetaken, vector down_dir, entity tracetest_ent); void botframe_showwaypointlinks(); float waypoint_loadall(); -float waypoint_load_links(); +bool waypoint_load_links(); +#define waypoint_load_links_hardwired() waypoint_load_or_remove_links_hardwired(false) +#define waypoint_remove_links_hardwired() waypoint_load_or_remove_links_hardwired(true) +void waypoint_load_or_remove_links_hardwired(bool removal_mode); +void waypoint_spawn_fromeditor(entity pl); entity waypoint_spawn(vector m1, vector m2, float f); entity waypoint_spawnpersonal(entity this, vector position); -vector waypoint_fixorigin(vector position); +void waypoint_unreachable(entity pl); + +vector waypoint_fixorigin_down_dir(vector position, entity tracetest_ent, vector down_dir); void botframe_autowaypoints(); diff --git a/qcsrc/server/bot/null/bot_null.qc b/qcsrc/server/bot/null/bot_null.qc index 68ae416706..f8738f80fb 100644 --- a/qcsrc/server/bot/null/bot_null.qc +++ b/qcsrc/server/bot/null/bot_null.qc @@ -28,15 +28,17 @@ void navigation_markroutes(entity this, entity fixed_source_waypoint) { } void navigation_markroutes_inverted(entity fixed_source_waypoint) { } void navigation_routerating(entity this, entity e, float f, float rangebias) { } -bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float movemode) { return false; } +bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float end_height, float movemode) { return false; } -void waypoint_remove(entity e) { } +void waypoint_remove_fromeditor(entity pl) { } +void waypoint_remove(entity wp) { } void waypoint_saveall() { } void waypoint_schedulerelinkall() { } void waypoint_schedulerelink(entity wp) { } void waypoint_spawnforitem(entity e) { } void waypoint_spawnforitem_force(entity e, vector org) { } -void waypoint_spawnforteleporter(entity e, vector destination, float timetaken) { } -void waypoint_spawnforteleporter_v(entity e, vector org, vector destination, float timetaken) { } +void waypoint_spawnforteleporter(entity e, vector destination, float timetaken, entity tracetest_ent) { } +void waypoint_spawnforteleporter_wz(entity e, vector org, vector destination, float timetaken, vector down_dir, entity tracetest_ent) { } +void waypoint_spawn_fromeditor(entity pl) { } entity waypoint_spawn(vector m1, vector m2, float f) { return NULL; } #endif diff --git a/qcsrc/server/client.qc b/qcsrc/server/client.qc index ec6342a2a2..bffd503e5e 100644 --- a/qcsrc/server/client.qc +++ b/qcsrc/server/client.qc @@ -639,7 +639,6 @@ void PutPlayerInServer(entity this) setorigin(this, spot.origin + '0 0 1' * (1 - this.mins.z - 24)); // don't reset back to last position, even if new position is stuck in solid this.oldorigin = this.origin; - this.lastteleporttime = time; // prevent insane speeds due to changing origin if(this.conveyor) IL_REMOVE(g_conveyed, this); this.conveyor = NULL; // prevent conveyors at the previous location from moving a freshly spawned player diff --git a/qcsrc/server/command/sv_cmd.qc b/qcsrc/server/command/sv_cmd.qc index f5569c08f2..6de4507b17 100644 --- a/qcsrc/server/command/sv_cmd.qc +++ b/qcsrc/server/command/sv_cmd.qc @@ -1574,11 +1574,13 @@ void GameCommand_trace(float request, float argc) case "walk": { - if (argc == 4) + if (argc == 4 || argc == 5) { e = nextent(NULL); - if (tracewalk(e, stov(argv(2)), e.mins, e.maxs, stov(argv(3)), MOVE_NORMAL)) LOG_INFO("can walk"); - else LOG_INFO("cannot walk"); + if (tracewalk(e, stov(argv(2)), e.mins, e.maxs, stov(argv(3)), stof(argv(4)), MOVE_NORMAL)) + LOG_INFO("can walk"); + else + LOG_INFO("cannot walk"); return; } } @@ -1604,7 +1606,9 @@ void GameCommand_trace(float request, float argc) LOG_INFO("Incorrect parameters for ^2trace^7"); case CMD_REQUEST_USAGE: { - LOG_INFO("Usage:^3 sv_cmd trace command (startpos endpos)"); + LOG_INFO("Usage:^3 sv_cmd trace command [startpos endpos] [endpos_height]"); + LOG_INFO(" Where startpos and endpos are parameters for 'walk' and 'showline' commands,"); + LOG_INFO(" 'endpos_height' is an optional parameter for 'walk' command,"); LOG_INFO(" Full list of commands here: \"debug, debug2, walk, showline.\""); LOG_INFO("See also: ^2bbox, gettaginfo^7"); return; diff --git a/qcsrc/server/impulse.qc b/qcsrc/server/impulse.qc index 5b426c1c3f..5010d28371 100644 --- a/qcsrc/server/impulse.qc +++ b/qcsrc/server/impulse.qc @@ -572,114 +572,16 @@ IMPULSE(waypoint_clear) sprint(this, "all waypoints cleared\n"); } -vector waypoint_getSymmetricalOrigin(vector org, int ctf_flags) -{ - vector new_org = org; - if (fabs(autocvar_g_waypointeditor_symmetrical) == 1) - { - vector map_center = havocbot_middlepoint; - if (autocvar_g_waypointeditor_symmetrical == -1) - map_center = autocvar_g_waypointeditor_symmetrical_origin; - - new_org = Rotate(org - map_center, 360 * DEG2RAD / ctf_flags) + map_center; - } - else if (fabs(autocvar_g_waypointeditor_symmetrical) == 2) - { - float m = havocbot_symmetryaxis_equation.x; - float q = havocbot_symmetryaxis_equation.y; - if (autocvar_g_waypointeditor_symmetrical == -2) - { - m = autocvar_g_waypointeditor_symmetrical_axis.x; - q = autocvar_g_waypointeditor_symmetrical_axis.y; - } - - new_org.x = (1 / (1 + m*m)) * ((1 - m*m) * org.x + 2 * m * org.y - 2 * m * q); - new_org.y = (1 / (1 + m*m)) * (2 * m * org.x + (m*m - 1) * org.y + 2 * q); - } - new_org.z = org.z; - return new_org; -} - IMPULSE(navwaypoint_spawn) { if (!autocvar_g_waypointeditor) return; - entity e; - vector org = this.origin; - int ctf_flags = havocbot_symmetryaxis_equation.z; - bool sym = ((autocvar_g_waypointeditor_symmetrical > 0 && ctf_flags >= 2) - || (autocvar_g_waypointeditor_symmetrical < 0)); - int order = ctf_flags; - if(autocvar_g_waypointeditor_symmetrical_order >= 2) - { - order = autocvar_g_waypointeditor_symmetrical_order; - ctf_flags = order; - } - - LABEL(add_wp); - e = waypoint_spawn(org, org, 0); - waypoint_schedulerelink(e); - bprint(strcat("Waypoint spawned at ", vtos(org), "\n")); - if(sym) - { - org = waypoint_getSymmetricalOrigin(e.origin, ctf_flags); - if (vdist(org - this.origin, >, 32)) - { - if(order > 2) - order--; - else - sym = false; - goto add_wp; - } - } + waypoint_spawn_fromeditor(this); } IMPULSE(navwaypoint_remove) { if (!autocvar_g_waypointeditor) return; - entity e = navigation_findnearestwaypoint(this, false); - int ctf_flags = havocbot_symmetryaxis_equation.z; - bool sym = ((autocvar_g_waypointeditor_symmetrical > 0 && ctf_flags >= 2) - || (autocvar_g_waypointeditor_symmetrical < 0)); - int order = ctf_flags; - if(autocvar_g_waypointeditor_symmetrical_order >= 2) - { - order = autocvar_g_waypointeditor_symmetrical_order; - ctf_flags = order; - } - - LABEL(remove_wp); - if (!e) return; - if (e.wpflags & WAYPOINTFLAG_GENERATED) return; - - if (e.wphardwired) - { - LOG_INFO("^1Warning: ^7Removal of hardwired waypoints is not allowed in the editor. Please remove links from/to this waypoint (", vtos(e.origin), ") by hand from maps/", mapname, ".waypoints.hardwired"); - return; - } - - entity wp_sym = NULL; - if (sym) - { - vector org = waypoint_getSymmetricalOrigin(e.origin, ctf_flags); - IL_EACH(g_waypoints, !(it.wpflags & WAYPOINTFLAG_GENERATED), { - if(vdist(org - it.origin, <, 3)) - { - wp_sym = it; - break; - } - }); - } - bprint(strcat("Waypoint removed at ", vtos(e.origin), "\n")); - waypoint_remove(e); - if (sym && wp_sym) - { - e = wp_sym; - if(order > 2) - order--; - else - sym = false; - goto remove_wp; - } + waypoint_remove_fromeditor(this); } IMPULSE(navwaypoint_relink) @@ -697,93 +599,5 @@ IMPULSE(navwaypoint_save) IMPULSE(navwaypoint_unreachable) { if (!autocvar_g_waypointeditor) return; - IL_EACH(g_waypoints, true, - { - it.colormod = '0.5 0.5 0.5'; - it.effects &= ~(EF_NODEPTHTEST | EF_RED | EF_BLUE); - }); - entity e2 = navigation_findnearestwaypoint(this, false); - navigation_markroutes(this, e2); - - int j, m; - - j = 0; - m = 0; - IL_EACH(g_waypoints, it.wpcost >= 10000000, - { - LOG_INFO("unreachable: ", etos(it), " ", vtos(it.origin)); - it.colormod_z = 8; - it.effects |= EF_NODEPTHTEST | EF_BLUE; - ++j; - ++m; - }); - if (j) LOG_INFOF("%d waypoints cannot be reached from here in any way (marked with blue light)", j); - navigation_markroutes_inverted(e2); - - j = 0; - IL_EACH(g_waypoints, it.wpcost >= 10000000, - { - LOG_INFO("cannot reach me: ", etos(it), " ", vtos(it.origin)); - it.colormod_x = 8; - if (!(it.effects & EF_NODEPTHTEST)) // not already reported before - ++m; - it.effects |= EF_NODEPTHTEST | EF_RED; - ++j; - }); - if (j) LOG_INFOF("%d waypoints cannot walk to here in any way (marked with red light)", j); - if (m) LOG_INFOF("%d waypoints have been marked total", m); - - j = 0; - IL_EACH(g_spawnpoints, true, - { - vector org = it.origin; - tracebox(it.origin, PL_MIN_CONST, PL_MAX_CONST, it.origin - '0 0 512', MOVE_NOMONSTERS, NULL); - setorigin(it, trace_endpos); - if (navigation_findnearestwaypoint(it, false)) - { - setorigin(it, org); - it.effects &= ~EF_NODEPTHTEST; - it.model = ""; - } - else - { - setorigin(it, org); - LOG_INFO("spawn without waypoint: ", etos(it), " ", vtos(it.origin)); - it.effects |= EF_NODEPTHTEST; - _setmodel(it, this.model); - it.frame = this.frame; - it.skin = this.skin; - it.colormod = '8 0.5 8'; - setsize(it, '0 0 0', '0 0 0'); - ++j; - } - }); - if (j) LOG_INFOF("%d spawnpoints have no nearest waypoint (marked by player model)", j); - - j = 0; - IL_EACH(g_items, true, - { - it.effects &= ~(EF_NODEPTHTEST | EF_RED | EF_BLUE); - it.colormod = '0.5 0.5 0.5'; - }); - IL_EACH(g_items, true, - { - if (navigation_findnearestwaypoint(it, false)) continue; - LOG_INFO("item without waypoint: ", etos(it), " ", vtos(it.origin)); - it.effects |= EF_NODEPTHTEST | EF_RED; - it.colormod_x = 8; - ++j; - }); - if (j) LOG_INFOF("%d items have no nearest waypoint and cannot be walked away from (marked with red light)", j); - - j = 0; - IL_EACH(g_items, true, - { - if (navigation_findnearestwaypoint(it, true)) continue; - LOG_INFO("item without waypoint: ", etos(it), " ", vtos(it.origin)); - it.effects |= EF_NODEPTHTEST | EF_BLUE; - it.colormod_z = 8; - ++j; - }); - if (j) LOG_INFOF("%d items have no nearest waypoint and cannot be walked to (marked with blue light)", j); + waypoint_unreachable(this); } diff --git a/qcsrc/server/mutators/mutator/gamemode_assault.qc b/qcsrc/server/mutators/mutator/gamemode_assault.qc index affa033de1..50861c32f7 100644 --- a/qcsrc/server/mutators/mutator/gamemode_assault.qc +++ b/qcsrc/server/mutators/mutator/gamemode_assault.qc @@ -461,7 +461,7 @@ void havocbot_role_ast_offense(entity this) if(this.havocbot_attack_time>time) return; - if (this.bot_strategytime < time) + if (navigation_goalrating_timeout(this)) { navigation_goalrating_start(this); havocbot_goalrating_enemyplayers(this, 20000, this.origin, 650); @@ -469,7 +469,7 @@ void havocbot_role_ast_offense(entity this) havocbot_goalrating_items(this, 15000, this.origin, 10000); navigation_goalrating_end(this); - this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + navigation_goalrating_timeout_set(this); } } @@ -495,7 +495,7 @@ void havocbot_role_ast_defense(entity this) if(this.havocbot_attack_time>time) return; - if (this.bot_strategytime < time) + if (navigation_goalrating_timeout(this)) { navigation_goalrating_start(this); havocbot_goalrating_enemyplayers(this, 20000, this.origin, 3000); @@ -503,7 +503,7 @@ void havocbot_role_ast_defense(entity this) havocbot_goalrating_items(this, 15000, this.origin, 10000); navigation_goalrating_end(this); - this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + navigation_goalrating_timeout_set(this); } } diff --git a/qcsrc/server/mutators/mutator/gamemode_assault.qh b/qcsrc/server/mutators/mutator/gamemode_assault.qh index f437d98b52..ea714e6a23 100644 --- a/qcsrc/server/mutators/mutator/gamemode_assault.qh +++ b/qcsrc/server/mutators/mutator/gamemode_assault.qh @@ -31,9 +31,6 @@ const int HAVOCBOT_AST_ROLE_OFFENSE = 4; .int havocbot_role_flags; .float havocbot_attack_time; -.void(entity this) havocbot_role; -.void(entity this) havocbot_previous_role; - void(entity this) havocbot_role_ast_defense; void(entity this) havocbot_role_ast_offense; diff --git a/qcsrc/server/mutators/mutator/gamemode_ctf.qc b/qcsrc/server/mutators/mutator/gamemode_ctf.qc index 3cf560ebed..3c714ad539 100644 --- a/qcsrc/server/mutators/mutator/gamemode_ctf.qc +++ b/qcsrc/server/mutators/mutator/gamemode_ctf.qc @@ -540,6 +540,9 @@ void ctf_Handle_Capture(entity flag, entity toucher, int capturetype) if(CTF_DIFFTEAM(player, flag)) { return; } if((flag.cnt || enemy_flag.cnt) && flag.cnt != enemy_flag.cnt) { return; } // this should catch some edge cases (capturing grouped flag at ungrouped flag disallowed etc) + if (toucher.goalentity == flag.bot_basewaypoint) + toucher.goalentity_lock_timeout = 0; + if(ctf_oneflag) for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext) if(SAME_TEAM(tmp_entity, player)) @@ -594,6 +597,8 @@ void ctf_Handle_Capture(entity flag, entity toucher, int capturetype) { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); } } + flag.enemy = toucher; + // reset the flag player.next_take_time = time + autocvar_g_ctf_flag_collect_delay; ctf_RespawnFlag(enemy_flag); @@ -636,6 +641,8 @@ void ctf_Handle_Return(entity flag, entity player) if(player.flagcarried == flag) WaypointSprite_Kill(player.wps_flagcarrier); + flag.enemy = player; + // reset the flag ctf_RespawnFlag(flag); } @@ -773,6 +780,7 @@ void ctf_CheckFlagReturn(entity flag, int returntype) } _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE); ctf_EventLog("returned", flag.team, NULL); + flag.enemy = NULL; ctf_RespawnFlag(flag); } } @@ -1176,8 +1184,9 @@ void ctf_RespawnFlag(entity flag) void ctf_Reset(entity this) { if(this.owner && IS_PLAYER(this.owner)) - ctf_Handle_Throw(this.owner, NULL, DROP_RESET); + ctf_Handle_Throw(this.owner, NULL, DROP_RESET); + this.enemy = NULL; ctf_RespawnFlag(this); } @@ -1653,11 +1662,10 @@ void havocbot_role_ctf_carrier(entity this) return; } - if (this.bot_strategytime < time) + if (navigation_goalrating_timeout(this)) { - this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; - navigation_goalrating_start(this); + if(ctf_oneflag) havocbot_goalrating_ctf_enemybase(this, 50000); else @@ -1668,6 +1676,19 @@ void havocbot_role_ctf_carrier(entity this) navigation_goalrating_end(this); + navigation_goalrating_timeout_set(this); + + entity head = ctf_worldflaglist; + while (head) + { + if (this.goalentity == head.bot_basewaypoint) + { + this.goalentity_lock_timeout = time + 5; + break; + } + head = head.ctf_worldflagnext; + } + if (this.goalentity) this.havocbot_cantfindflag = time + 10; else if (time > this.havocbot_cantfindflag) @@ -1729,14 +1750,17 @@ void havocbot_role_ctf_escort(entity this) } // Chase the flag carrier - if (this.bot_strategytime < time) + if (navigation_goalrating_timeout(this)) { - this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; navigation_goalrating_start(this); + havocbot_goalrating_ctf_enemyflag(this, 30000); havocbot_goalrating_ctf_ourstolenflag(this, 40000); havocbot_goalrating_items(this, 10000, this.origin, 10000); + navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); } } @@ -1809,15 +1833,18 @@ void havocbot_role_ctf_offense(entity 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_ctf_ourstolenflag(this, 50000); havocbot_goalrating_ctf_enemybase(this, 20000); havocbot_goalrating_items(this, 5000, this.origin, 1000); havocbot_goalrating_items(this, 1000, this.origin, 10000); + navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); } } @@ -1842,11 +1869,8 @@ void havocbot_role_ctf_retriever(entity this) mf = havocbot_ctf_find_flag(this); if(mf.ctf_status==FLAG_BASE) { - if(this.goalcurrent == mf) - { - navigation_clearroute(this); - this.bot_strategytime = 0; - } + if (mf.enemy == this) // did this bot return the flag? + navigation_goalrating_timeout_force(this); havocbot_ctf_reset_role(this); return; } @@ -1860,18 +1884,21 @@ void havocbot_role_ctf_retriever(entity this) return; } - if (this.bot_strategytime < time) + if (navigation_goalrating_timeout(this)) { float rt_radius; rt_radius = 10000; - this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; navigation_goalrating_start(this); + havocbot_goalrating_ctf_ourstolenflag(this, 50000); havocbot_goalrating_ctf_droppedflags(this, 40000, this.origin, rt_radius); havocbot_goalrating_ctf_enemybase(this, 30000); havocbot_goalrating_items(this, 500, this.origin, rt_radius); + navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); } } @@ -1907,22 +1934,25 @@ void havocbot_role_ctf_middle(entity this) return; } - if (this.bot_strategytime < time) + if (navigation_goalrating_timeout(this)) { vector org; org = havocbot_middlepoint; org.z = this.origin.z; - this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; navigation_goalrating_start(this); + havocbot_goalrating_ctf_ourstolenflag(this, 50000); havocbot_goalrating_ctf_droppedflags(this, 30000, this.origin, 10000); havocbot_goalrating_enemyplayers(this, 10000, org, havocbot_middlepoint_radius * 0.5); havocbot_goalrating_items(this, 5000, org, havocbot_middlepoint_radius * 0.5); havocbot_goalrating_items(this, 2500, this.origin, 10000); havocbot_goalrating_ctf_enemybase(this, 2500); + navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); } } @@ -1958,11 +1988,10 @@ void havocbot_role_ctf_defense(entity this) havocbot_ctf_reset_role(this); return; } - if (this.bot_strategytime < time) + if (navigation_goalrating_timeout(this)) { vector org = mf.dropped_origin; - this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; navigation_goalrating_start(this); // if enemies are closer to our base, go there @@ -1988,7 +2017,10 @@ void havocbot_role_ctf_defense(entity this) havocbot_goalrating_enemyplayers(this, 15000, org, havocbot_middlepoint_radius); havocbot_goalrating_items(this, 10000, org, havocbot_middlepoint_radius); havocbot_goalrating_items(this, 5000, this.origin, 10000); + navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); } } @@ -2002,7 +2034,8 @@ void havocbot_role_ctf_setrole(entity bot, int role) bot.havocbot_role = havocbot_role_ctf_carrier; bot.havocbot_role_timeout = 0; bot.havocbot_cantfindflag = time + 10; - bot.bot_strategytime = 0; + if (bot.havocbot_previous_role != bot.havocbot_role) + navigation_goalrating_timeout_force(bot); break; case HAVOCBOT_CTF_ROLE_DEFENSE: s = "defense"; @@ -2024,14 +2057,16 @@ void havocbot_role_ctf_setrole(entity bot, int role) bot.havocbot_previous_role = bot.havocbot_role; bot.havocbot_role = havocbot_role_ctf_retriever; bot.havocbot_role_timeout = time + 10; - bot.bot_strategytime = 0; + if (bot.havocbot_previous_role != bot.havocbot_role) + navigation_goalrating_timeout_expire(bot, 2); break; case HAVOCBOT_CTF_ROLE_ESCORT: s = "escort"; bot.havocbot_previous_role = bot.havocbot_role; bot.havocbot_role = havocbot_role_ctf_escort; bot.havocbot_role_timeout = time + 30; - bot.bot_strategytime = 0; + if (bot.havocbot_previous_role != bot.havocbot_role) + navigation_goalrating_timeout_expire(bot, 2); break; } LOG_TRACE(bot.netname, " switched to ", s); diff --git a/qcsrc/server/mutators/mutator/gamemode_ctf.qh b/qcsrc/server/mutators/mutator/gamemode_ctf.qh index 0b86a57f80..33d64a074a 100644 --- a/qcsrc/server/mutators/mutator/gamemode_ctf.qh +++ b/qcsrc/server/mutators/mutator/gamemode_ctf.qh @@ -133,6 +133,7 @@ float ctf_captimerecord; // record time for capturing the flag .float next_take_time; .bool ctf_flagdamaged_byworld; int ctf_teams; +.entity enemy; // when flag is back in the base, it remembers last player who carried/touched the flag, useful to bots // passing/throwing properties .float pass_distance; diff --git a/qcsrc/server/mutators/mutator/gamemode_cts.qc b/qcsrc/server/mutators/mutator/gamemode_cts.qc index ca892c52ff..daaf8a9695 100644 --- a/qcsrc/server/mutators/mutator/gamemode_cts.qc +++ b/qcsrc/server/mutators/mutator/gamemode_cts.qc @@ -13,20 +13,34 @@ 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); + bool raw_touch_check = true; + int cp = this.race_checkpoint; + + LABEL(search_racecheckpoints) IL_EACH(g_racecheckpoints, true, { - if(it.cnt == this.race_checkpoint) - navigation_routerating(this, it, 1000000, 5000); - else if(this.race_checkpoint == -1) + if(it.cnt == cp || cp == -1) + { + // redirect bot to next goal if it touched the waypoint of an untouchable checkpoint + // e.g. checkpoint in front of Stormkeep's warpzone + // the same workaround is applied in Race game mode + if (raw_touch_check && vdist(this.origin - it.nearestwaypoint.origin, <, 30)) + { + cp = race_NextCheckpoint(cp); + raw_touch_check = false; + goto search_racecheckpoints; + } navigation_routerating(this, it, 1000000, 5000); + } }); navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); } } diff --git a/qcsrc/server/mutators/mutator/gamemode_domination.qc b/qcsrc/server/mutators/mutator/gamemode_domination.qc index be38553c95..5980cfbaf3 100644 --- a/qcsrc/server/mutators/mutator/gamemode_domination.qc +++ b/qcsrc/server/mutators/mutator/gamemode_domination.qc @@ -402,15 +402,16 @@ void havocbot_role_dom(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_controlpoints(this, 10000, this.origin, 15000); havocbot_goalrating_items(this, 8000, this.origin, 8000); //havocbot_goalrating_enemyplayers(this, 3000, this.origin, 2000); havocbot_goalrating_waypoints(this, 1, this.origin, 3000); navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); } } diff --git a/qcsrc/server/mutators/mutator/gamemode_freezetag.qc b/qcsrc/server/mutators/mutator/gamemode_freezetag.qc index 88afaa755d..2ea70c1f1a 100644 --- a/qcsrc/server/mutators/mutator/gamemode_freezetag.qc +++ b/qcsrc/server/mutators/mutator/gamemode_freezetag.qc @@ -257,16 +257,16 @@ void havocbot_role_ft_offense(entity this) return; } - if (time > this.bot_strategytime) + 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_freeplayers(this, 9000, this.origin, 10000); havocbot_goalrating_waypoints(this, 1, this.origin, 3000); navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); } } @@ -286,16 +286,16 @@ void havocbot_role_ft_freeing(entity this) return; } - if (time > this.bot_strategytime) + if (navigation_goalrating_timeout(this)) { - this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; - navigation_goalrating_start(this); havocbot_goalrating_items(this, 8000, this.origin, 10000); havocbot_goalrating_enemyplayers(this, 10000, this.origin, 10000); havocbot_goalrating_freeplayers(this, 20000, this.origin, 10000); havocbot_goalrating_waypoints(this, 1, this.origin, 3000); navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); } } diff --git a/qcsrc/server/mutators/mutator/gamemode_keepaway.qc b/qcsrc/server/mutators/mutator/gamemode_keepaway.qc index 7b33bea6f3..badae12c2d 100644 --- a/qcsrc/server/mutators/mutator/gamemode_keepaway.qc +++ b/qcsrc/server/mutators/mutator/gamemode_keepaway.qc @@ -232,21 +232,21 @@ void havocbot_role_ka_carrier(entity this) if (IS_DEAD(this)) return; - if (time > this.bot_strategytime) + 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); + + navigation_goalrating_timeout_set(this); } if (!this.ballcarried) { this.havocbot_role = havocbot_role_ka_collector; - this.bot_strategytime = 0; + navigation_goalrating_timeout_expire(this, 2); } } @@ -255,21 +255,21 @@ void havocbot_role_ka_collector(entity this) if (IS_DEAD(this)) return; - if (time > this.bot_strategytime) + 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, 1000, this.origin, 10000); havocbot_goalrating_ball(this, 20000, this.origin); navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); } if (this.ballcarried) { this.havocbot_role = havocbot_role_ka_carrier; - this.bot_strategytime = 0; + navigation_goalrating_timeout_expire(this, 2); } } diff --git a/qcsrc/server/mutators/mutator/gamemode_keyhunt.qc b/qcsrc/server/mutators/mutator/gamemode_keyhunt.qc index 15b6e0f4a6..e6253b091f 100644 --- a/qcsrc/server/mutators/mutator/gamemode_keyhunt.qc +++ b/qcsrc/server/mutators/mutator/gamemode_keyhunt.qc @@ -1071,9 +1071,8 @@ void havocbot_role_kh_carrier(entity this) return; } - if (this.bot_strategytime < time) + if (navigation_goalrating_timeout(this)) { - this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; navigation_goalrating_start(this); if(kh_Key_AllOwnedByWhichTeam() == this.team) @@ -1082,6 +1081,8 @@ void havocbot_role_kh_carrier(entity this) havocbot_goalrating_kh(this, 4, 4, 1); // play defensively navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); } } @@ -1108,10 +1109,9 @@ void havocbot_role_kh_defense(entity this) return; } - if (this.bot_strategytime < time) + if (navigation_goalrating_timeout(this)) { float key_owner_team; - this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; navigation_goalrating_start(this); key_owner_team = kh_Key_AllOwnedByWhichTeam(); @@ -1123,6 +1123,8 @@ void havocbot_role_kh_defense(entity this) havocbot_goalrating_kh(this, 0.1, 0.1, 10); // ATTACK ANYWAY navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); } } @@ -1149,11 +1151,10 @@ void havocbot_role_kh_offense(entity this) return; } - if (this.bot_strategytime < time) + if (navigation_goalrating_timeout(this)) { float key_owner_team; - this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; navigation_goalrating_start(this); key_owner_team = kh_Key_AllOwnedByWhichTeam(); @@ -1165,6 +1166,8 @@ void havocbot_role_kh_offense(entity this) havocbot_goalrating_kh(this, 0.1, 0.1, 10); // ATTACK! EMERGENCY! navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); } } @@ -1199,9 +1202,8 @@ void havocbot_role_kh_freelancer(entity this) return; } - if (this.bot_strategytime < time) + if (navigation_goalrating_timeout(this)) { - this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; navigation_goalrating_start(this); int key_owner_team = kh_Key_AllOwnedByWhichTeam(); @@ -1213,6 +1215,8 @@ void havocbot_role_kh_freelancer(entity this) havocbot_goalrating_kh(this, 0.1, 0.1, 10); // ATTACK ANYWAY navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); } } diff --git a/qcsrc/server/mutators/mutator/gamemode_race.qc b/qcsrc/server/mutators/mutator/gamemode_race.qc index 2f581d8c43..aa6d12a83e 100644 --- a/qcsrc/server/mutators/mutator/gamemode_race.qc +++ b/qcsrc/server/mutators/mutator/gamemode_race.qc @@ -14,24 +14,34 @@ void havocbot_role_race(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); + bool raw_touch_check = true; + int cp = this.race_checkpoint; + + LABEL(search_racecheckpoints) IL_EACH(g_racecheckpoints, true, { - if(it.cnt == this.race_checkpoint) - { - navigation_routerating(this, it, 1000000, 5000); - } - else if(this.race_checkpoint == -1) + if(it.cnt == cp || cp == -1) { + // redirect bot to next goal if it touched the waypoint of an untouchable checkpoint + // e.g. checkpoint in front of Stormkeep's warpzone + // the same workaround is applied in CTS game mode + if (raw_touch_check && vdist(this.origin - it.nearestwaypoint.origin, <, 30)) + { + cp = race_NextCheckpoint(cp); + raw_touch_check = false; + goto search_racecheckpoints; + } navigation_routerating(this, it, 1000000, 5000); } }); navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); } } diff --git a/qcsrc/server/sv_main.qc b/qcsrc/server/sv_main.qc index 97299ffeff..1e5435795e 100644 --- a/qcsrc/server/sv_main.qc +++ b/qcsrc/server/sv_main.qc @@ -216,8 +216,6 @@ void StartFrame() if (timeout_status == TIMEOUT_LEADTIME) // just before the timeout (when timeout_status will be TIMEOUT_ACTIVE) orig_slowmo = autocvar_slowmo; // slowmo will be restored after the timeout - skill = autocvar_skill; - // detect when the pre-game countdown (if any) has ended and the game has started bool game_delay = (time < game_starttime); if (autocvar_sv_eventlog && game_delay_last && !game_delay) @@ -365,6 +363,9 @@ LABEL(cleanup) void WarpZone_PostInitialize_Callback() { // create waypoint links for warpzones + entity tracetest_ent = spawn(); + setsize(tracetest_ent, PL_MIN_CONST, PL_MAX_CONST); + tracetest_ent.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP; //for(entity e = warpzone_first; e; e = e.warpzone_next) for(entity e = NULL; (e = find(e, classname, "trigger_warpzone")); ) { @@ -375,6 +376,7 @@ void WarpZone_PostInitialize_Callback() dst = (e.enemy.absmin + e.enemy.absmax) * 0.5; makevectors(e.enemy.warpzone_angles); dst = dst + ((e.enemy.warpzone_origin - dst) * v_forward) * v_forward - 16 * v_right; - waypoint_spawnforteleporter_v(e, src, dst, 0); + waypoint_spawnforteleporter_wz(e, src, dst, 0, -v_up, tracetest_ent); } + delete(tracetest_ent); } diff --git a/qcsrc/server/utils.qh b/qcsrc/server/utils.qh index 4a603c0225..097685abf1 100644 --- a/qcsrc/server/utils.qh +++ b/qcsrc/server/utils.qh @@ -22,6 +22,8 @@ const string STR_OBSERVER = "observer"; #define IS_VEHICLE(v) (v.vehicle_flags & VHF_ISVEHICLE) #define IS_TURRET(v) (v.turret_flags & TUR_FLAG_ISTURRET) +#define IS_MOVABLE(v) ((IS_PLAYER(v) || IS_MONSTER(v)) && !STAT(FROZEN, v)) + // NOTE: FOR_EACH_CLIENTSLOT deprecated! Use the following instead: FOREACH_CLIENTSLOT(true, { code; }); // NOTE: FOR_EACH_CLIENT deprecated! Use the following instead: FOREACH_CLIENT(true, { code; }); // NOTE: FOR_EACH_REALCLIENT deprecated! Use the following instead: FOREACH_CLIENT(IS_REAL_CLIENT(it), { code; });