mirror of
https://github.com/id-Software/quake2-rerelease-dll.git
synced 2025-02-14 15:51:42 +00:00
1497 lines
38 KiB
C++
1497 lines
38 KiB
C++
// Copyright (c) ZeniMax Media Inc.
|
|
// Licensed under the GNU General Public License 2.0.
|
|
// m_move.c -- monster movement
|
|
|
|
#include "g_local.h"
|
|
|
|
// this is used for communications out of sv_movestep to say what entity
|
|
// is blocking us
|
|
edict_t *new_bad; // pmm
|
|
|
|
/*
|
|
=============
|
|
M_CheckBottom
|
|
|
|
Returns false if any part of the bottom of the entity is off an edge that
|
|
is not a staircase.
|
|
|
|
=============
|
|
*/
|
|
bool M_CheckBottom_Fast_Generic(const vec3_t &absmins, const vec3_t &absmaxs, bool ceiling)
|
|
{
|
|
// PGM
|
|
// FIXME - this will only handle 0,0,1 and 0,0,-1 gravity vectors
|
|
vec3_t start;
|
|
|
|
start[2] = absmins[2] - 1;
|
|
if (ceiling)
|
|
start[2] = absmaxs[2] + 1;
|
|
// PGM
|
|
|
|
for (int x = 0; x <= 1; x++)
|
|
for (int y = 0; y <= 1; y++)
|
|
{
|
|
start[0] = x ? absmaxs[0] : absmins[0];
|
|
start[1] = y ? absmaxs[1] : absmins[1];
|
|
if (gi.pointcontents(start) != CONTENTS_SOLID)
|
|
return false;
|
|
}
|
|
|
|
return true; // we got out easy
|
|
}
|
|
|
|
bool M_CheckBottom_Slow_Generic(const vec3_t &origin, const vec3_t &mins, const vec3_t &maxs, edict_t *ignore, contents_t mask, bool ceiling, bool allow_any_step_height)
|
|
{
|
|
vec3_t start;
|
|
|
|
//
|
|
// check it for real...
|
|
//
|
|
vec3_t step_quadrant_size = (maxs - mins) * 0.5f;
|
|
step_quadrant_size.z = 0;
|
|
|
|
vec3_t half_step_quadrant = step_quadrant_size * 0.5f;
|
|
vec3_t half_step_quadrant_mins = -half_step_quadrant;
|
|
|
|
vec3_t stop;
|
|
|
|
start[0] = stop[0] = origin.x;
|
|
start[1] = stop[1] = origin.y;
|
|
|
|
// PGM
|
|
if (!ceiling)
|
|
{
|
|
start[2] = origin.z + mins.z;
|
|
stop[2] = start[2] - STEPSIZE * 2;
|
|
}
|
|
else
|
|
{
|
|
start[2] = origin.z + maxs.z;
|
|
stop[2] = start[2] + STEPSIZE * 2;
|
|
}
|
|
// PGM
|
|
|
|
vec3_t mins_no_z = mins;
|
|
vec3_t maxs_no_z = maxs;
|
|
mins_no_z.z = maxs_no_z.z = 0;
|
|
|
|
trace_t trace = gi.trace(start, mins_no_z, maxs_no_z, stop, ignore, mask);
|
|
|
|
if (trace.fraction == 1.0f)
|
|
return false;
|
|
|
|
// [Paril-KEX]
|
|
if (allow_any_step_height)
|
|
return true;
|
|
|
|
start[0] = stop[0] = origin.x + ((mins.x + maxs.x) * 0.5f);
|
|
start[1] = stop[1] = origin.y + ((mins.y + maxs.y) * 0.5f);
|
|
|
|
float mid = trace.endpos[2];
|
|
|
|
// the corners must be within 16 of the midpoint
|
|
for (int32_t x = 0; x <= 1; x++)
|
|
for (int32_t y = 0; y <= 1; y++)
|
|
{
|
|
vec3_t quadrant_start = start;
|
|
|
|
if (x)
|
|
quadrant_start.x += half_step_quadrant.x;
|
|
else
|
|
quadrant_start.x -= half_step_quadrant.x;
|
|
|
|
if (y)
|
|
quadrant_start.y += half_step_quadrant.y;
|
|
else
|
|
quadrant_start.y -= half_step_quadrant.y;
|
|
|
|
vec3_t quadrant_end = quadrant_start;
|
|
quadrant_end.z = stop.z;
|
|
|
|
trace = gi.trace(quadrant_start, half_step_quadrant_mins, half_step_quadrant, quadrant_end, ignore, mask);
|
|
|
|
// PGM
|
|
// FIXME - this will only handle 0,0,1 and 0,0,-1 gravity vectors
|
|
if (ceiling)
|
|
{
|
|
if (trace.fraction == 1.0f || trace.endpos[2] - mid > (STEPSIZE))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (trace.fraction == 1.0f || mid - trace.endpos[2] > (STEPSIZE))
|
|
return false;
|
|
}
|
|
// PGM
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool M_CheckBottom(edict_t *ent)
|
|
{
|
|
// if all of the points under the corners are solid world, don't bother
|
|
// with the tougher checks
|
|
|
|
if (M_CheckBottom_Fast_Generic(ent->s.origin + ent->mins, ent->s.origin + ent->maxs, ent->gravityVector[2] > 0))
|
|
return true; // we got out easy
|
|
|
|
contents_t mask = (ent->svflags & SVF_MONSTER) ? MASK_MONSTERSOLID : (MASK_SOLID | CONTENTS_MONSTER | CONTENTS_PLAYER);
|
|
return M_CheckBottom_Slow_Generic(ent->s.origin, ent->mins, ent->maxs, ent, mask, ent->gravityVector[2] > 0, ent->spawnflags.has(SPAWNFLAG_MONSTER_SUPER_STEP));
|
|
}
|
|
|
|
//============
|
|
// ROGUE
|
|
bool IsBadAhead(edict_t *self, edict_t *bad, const vec3_t &move)
|
|
{
|
|
vec3_t dir;
|
|
vec3_t forward;
|
|
float dp_bad, dp_move;
|
|
vec3_t move_copy;
|
|
|
|
move_copy = move;
|
|
|
|
dir = bad->s.origin - self->s.origin;
|
|
dir.normalize();
|
|
AngleVectors(self->s.angles, forward, nullptr, nullptr);
|
|
dp_bad = forward.dot(dir);
|
|
|
|
move_copy.normalize();
|
|
AngleVectors(self->s.angles, forward, nullptr, nullptr);
|
|
dp_move = forward.dot(move_copy);
|
|
|
|
if ((dp_bad < 0) && (dp_move < 0))
|
|
return true;
|
|
if ((dp_bad > 0) && (dp_move > 0))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static vec3_t G_IdealHoverPosition(edict_t *ent)
|
|
{
|
|
if ((!ent->enemy && !(ent->monsterinfo.aiflags & AI_MEDIC)) || (ent->monsterinfo.aiflags & (AI_COMBAT_POINT | AI_SOUND_TARGET | AI_HINT_PATH | AI_PATHING)))
|
|
return { 0, 0, 0 }; // go right for the center
|
|
|
|
// pick random direction
|
|
float theta = frandom(2 * PIf);
|
|
float phi;
|
|
|
|
// buzzards pick half sphere
|
|
if (ent->monsterinfo.fly_above)
|
|
phi = acos(0.7f + frandom(0.3f));
|
|
else if (ent->monsterinfo.fly_buzzard || (ent->monsterinfo.aiflags & AI_MEDIC))
|
|
phi = acos(frandom());
|
|
// non-buzzards pick a level around the center
|
|
else
|
|
phi = acos(crandom() * 0.06f);
|
|
|
|
vec3_t d {
|
|
sin(phi) * cos(theta),
|
|
sin(phi) * sin(theta),
|
|
cos(phi)
|
|
};
|
|
|
|
return d * frandom(ent->monsterinfo.fly_min_distance, ent->monsterinfo.fly_max_distance);
|
|
}
|
|
|
|
inline bool SV_flystep_testvisposition(vec3_t start, vec3_t end, vec3_t starta, vec3_t startb, edict_t *ent)
|
|
{
|
|
trace_t tr = gi.traceline(start, end, ent, MASK_SOLID | CONTENTS_MONSTERCLIP);
|
|
|
|
if (tr.fraction == 1.0f)
|
|
{
|
|
tr = gi.trace(starta, ent->mins, ent->maxs, startb, ent, MASK_SOLID | CONTENTS_MONSTERCLIP);
|
|
|
|
if (tr.fraction == 1.0f)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool SV_alternate_flystep(edict_t *ent, vec3_t move, bool relink, edict_t *current_bad)
|
|
{
|
|
// swimming monsters just follow their velocity in the air
|
|
if ((ent->flags & FL_SWIM) && ent->waterlevel < WATER_UNDER)
|
|
return true;
|
|
|
|
if (ent->monsterinfo.fly_position_time <= level.time ||
|
|
(ent->enemy && ent->monsterinfo.fly_pinned && !visible(ent, ent->enemy)))
|
|
{
|
|
ent->monsterinfo.fly_pinned = false;
|
|
ent->monsterinfo.fly_position_time = level.time + random_time(3_sec, 10_sec);
|
|
ent->monsterinfo.fly_ideal_position = G_IdealHoverPosition(ent);
|
|
}
|
|
|
|
vec3_t towards_origin, towards_velocity = {};
|
|
|
|
float current_speed;
|
|
vec3_t dir = ent->velocity.normalized(current_speed);
|
|
|
|
// FIXME
|
|
if (isnan(dir[0]) || isnan(dir[1]) || isnan(dir[2]))
|
|
{
|
|
#if defined(_DEBUG) && defined(_WIN32)
|
|
__debugbreak();
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
if (ent->monsterinfo.aiflags & AI_PATHING)
|
|
towards_origin = (ent->monsterinfo.nav_path.returnCode == PathReturnCode::TraversalPending) ?
|
|
ent->monsterinfo.nav_path.secondMovePoint : ent->monsterinfo.nav_path.firstMovePoint;
|
|
else if (ent->enemy && !(ent->monsterinfo.aiflags & (AI_COMBAT_POINT | AI_SOUND_TARGET | AI_LOST_SIGHT)))
|
|
{
|
|
towards_origin = ent->enemy->s.origin;
|
|
towards_velocity = ent->enemy->velocity;
|
|
}
|
|
else if (ent->goalentity)
|
|
towards_origin = ent->goalentity->s.origin;
|
|
else // what we're going towards probably died or something
|
|
{
|
|
// change speed
|
|
if (current_speed)
|
|
{
|
|
if (current_speed > 0)
|
|
current_speed = max(0.f, current_speed - ent->monsterinfo.fly_acceleration);
|
|
else if (current_speed < 0)
|
|
current_speed = min(0.f, current_speed + ent->monsterinfo.fly_acceleration);
|
|
|
|
ent->velocity = dir * current_speed;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
vec3_t wanted_pos;
|
|
|
|
if (ent->monsterinfo.fly_pinned)
|
|
wanted_pos = ent->monsterinfo.fly_ideal_position;
|
|
else if (ent->monsterinfo.aiflags & (AI_PATHING | AI_COMBAT_POINT | AI_SOUND_TARGET | AI_LOST_SIGHT))
|
|
wanted_pos = towards_origin;
|
|
else
|
|
wanted_pos = (towards_origin + (towards_velocity * 0.25f)) + ent->monsterinfo.fly_ideal_position;
|
|
|
|
// find a place we can fit in from here
|
|
trace_t tr = gi.trace(towards_origin, { -8.f, -8.f, -8.f }, { 8.f, 8.f, 8.f }, wanted_pos, ent, MASK_SOLID | CONTENTS_MONSTERCLIP);
|
|
|
|
if (!tr.allsolid)
|
|
wanted_pos = tr.endpos;
|
|
|
|
float dist_to_wanted;
|
|
vec3_t dest_diff = (wanted_pos - ent->s.origin);
|
|
|
|
if (dest_diff.z > ent->mins.z && dest_diff.z < ent->maxs.z)
|
|
dest_diff.z = 0;
|
|
|
|
vec3_t wanted_dir = dest_diff.normalized(dist_to_wanted);
|
|
|
|
if (!(ent->monsterinfo.aiflags & AI_MANUAL_STEERING))
|
|
ent->ideal_yaw = vectoyaw((towards_origin - ent->s.origin).normalized());
|
|
|
|
// check if we're blocked from moving this way from where we are
|
|
tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, ent->s.origin + (wanted_dir * ent->monsterinfo.fly_acceleration), ent, MASK_SOLID | CONTENTS_MONSTERCLIP);
|
|
|
|
vec3_t aim_fwd, aim_rgt, aim_up;
|
|
vec3_t yaw_angles = { 0, ent->s.angles.y, 0 };
|
|
|
|
AngleVectors(yaw_angles, aim_fwd, aim_rgt, aim_up);
|
|
|
|
// it's a fairly close block, so we may want to shift more dramatically
|
|
if (tr.fraction < 0.25f)
|
|
{
|
|
bool bottom_visible = SV_flystep_testvisposition(ent->s.origin + vec3_t{0, 0, ent->mins.z}, wanted_pos,
|
|
ent->s.origin, ent->s.origin + vec3_t{0, 0, ent->mins.z - ent->monsterinfo.fly_acceleration}, ent);
|
|
bool top_visible = SV_flystep_testvisposition(ent->s.origin + vec3_t{0, 0, ent->maxs.z}, wanted_pos,
|
|
ent->s.origin, ent->s.origin + vec3_t{0, 0, ent->maxs.z + ent->monsterinfo.fly_acceleration}, ent);
|
|
|
|
// top & bottom are same, so we need to try right/left
|
|
if (bottom_visible == top_visible)
|
|
{
|
|
bool left_visible = gi.traceline(ent->s.origin + aim_fwd.scaled(ent->maxs) - aim_rgt.scaled(ent->maxs), wanted_pos, ent, MASK_SOLID | CONTENTS_MONSTERCLIP).fraction == 1.0f;
|
|
bool right_visible = gi.traceline(ent->s.origin + aim_fwd.scaled(ent->maxs) + aim_rgt.scaled(ent->maxs), wanted_pos, ent, MASK_SOLID | CONTENTS_MONSTERCLIP).fraction == 1.0f;
|
|
|
|
if (left_visible != right_visible)
|
|
{
|
|
if (right_visible)
|
|
wanted_dir += aim_rgt;
|
|
else
|
|
wanted_dir -= aim_rgt;
|
|
}
|
|
else
|
|
// we're probably stuck, push us directly away
|
|
wanted_dir = tr.plane.normal;
|
|
}
|
|
else
|
|
{
|
|
if (top_visible)
|
|
wanted_dir += aim_up;
|
|
else
|
|
wanted_dir -= aim_up;
|
|
}
|
|
|
|
wanted_dir.normalize();
|
|
}
|
|
|
|
// the closer we are to zero, the more we can change dir.
|
|
// if we're pushed past our max speed we shouldn't
|
|
// turn at all.
|
|
float turn_factor;
|
|
|
|
if (((ent->monsterinfo.fly_thrusters && !ent->monsterinfo.fly_pinned) || ent->monsterinfo.aiflags & (AI_PATHING | AI_COMBAT_POINT | AI_LOST_SIGHT)) && dir.dot(wanted_dir) > 0.0f)
|
|
turn_factor = 0.45f;
|
|
else
|
|
turn_factor = min(1.f, 0.84f + (0.08f * (current_speed / ent->monsterinfo.fly_speed)));
|
|
|
|
vec3_t final_dir = dir ? dir : wanted_dir;
|
|
|
|
// FIXME
|
|
if (isnan(final_dir[0]) || isnan(final_dir[1]) || isnan(final_dir[2]))
|
|
{
|
|
#if defined(_DEBUG) && defined(_WIN32)
|
|
__debugbreak();
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
// swimming monsters don't exit water voluntarily, and
|
|
// flying monsters don't enter water voluntarily (but will
|
|
// try to leave it)
|
|
bool bad_movement_direction = false;
|
|
|
|
//if (!(ent->monsterinfo.aiflags & AI_COMBAT_POINT))
|
|
{
|
|
if (ent->flags & FL_SWIM)
|
|
bad_movement_direction = !(gi.pointcontents(ent->s.origin + (wanted_dir * current_speed)) & CONTENTS_WATER);
|
|
else if ((ent->flags & FL_FLY) && ent->waterlevel < WATER_UNDER)
|
|
bad_movement_direction = gi.pointcontents(ent->s.origin + (wanted_dir * current_speed)) & CONTENTS_WATER;
|
|
}
|
|
|
|
if (bad_movement_direction)
|
|
{
|
|
if (ent->monsterinfo.fly_recovery_time < level.time)
|
|
{
|
|
ent->monsterinfo.fly_recovery_dir = vec3_t{ crandom(), crandom(), crandom() }.normalized();
|
|
ent->monsterinfo.fly_recovery_time = level.time + 1_sec;
|
|
}
|
|
|
|
wanted_dir = ent->monsterinfo.fly_recovery_dir;
|
|
}
|
|
|
|
if (dir && turn_factor > 0)
|
|
final_dir = slerp(dir, wanted_dir, 1.0f - turn_factor).normalized();
|
|
|
|
// the closer we are to the wanted position, we want to slow
|
|
// down so we don't fly past it.
|
|
float speed_factor;
|
|
|
|
if (!ent->enemy || (ent->monsterinfo.fly_thrusters && !ent->monsterinfo.fly_pinned) || (ent->monsterinfo.aiflags & (AI_PATHING | AI_COMBAT_POINT | AI_LOST_SIGHT)))
|
|
speed_factor = 1.f;
|
|
else if (aim_fwd.dot(wanted_dir) < -0.25 && dir)
|
|
speed_factor = 0.f;
|
|
else
|
|
speed_factor = min(1.f, dist_to_wanted / ent->monsterinfo.fly_speed);
|
|
|
|
if (bad_movement_direction)
|
|
speed_factor = -speed_factor;
|
|
|
|
float accel = ent->monsterinfo.fly_acceleration;
|
|
|
|
// if we're flying away from our destination, apply reverse thrusters
|
|
if (final_dir.dot(wanted_dir) < 0.25f)
|
|
accel *= 2.0f;
|
|
|
|
float wanted_speed = ent->monsterinfo.fly_speed * speed_factor;
|
|
|
|
if (ent->monsterinfo.aiflags & AI_MANUAL_STEERING)
|
|
wanted_speed = 0;
|
|
|
|
// change speed
|
|
if (current_speed > wanted_speed)
|
|
current_speed = max(wanted_speed, current_speed - accel);
|
|
else if (current_speed < wanted_speed)
|
|
current_speed = min(wanted_speed, current_speed + accel);
|
|
|
|
// FIXME
|
|
if (isnan(final_dir[0]) || isnan(final_dir[1]) || isnan(final_dir[2]) ||
|
|
isnan(current_speed))
|
|
{
|
|
#if defined(_DEBUG) && defined(_WIN32)
|
|
__debugbreak();
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
// commit
|
|
ent->velocity = final_dir * current_speed;
|
|
|
|
// for buzzards, set their pitch
|
|
if (ent->enemy && (ent->monsterinfo.fly_buzzard || (ent->monsterinfo.aiflags & AI_MEDIC)))
|
|
{
|
|
vec3_t d = (ent->s.origin - towards_origin).normalized();
|
|
d = vectoangles(d);
|
|
ent->s.angles[PITCH] = LerpAngle(ent->s.angles[PITCH], -d[PITCH], gi.frame_time_s * 4.0f);
|
|
}
|
|
else
|
|
ent->s.angles[PITCH] = 0;
|
|
|
|
return true;
|
|
}
|
|
|
|
// flying monsters don't step up
|
|
static bool SV_flystep(edict_t *ent, vec3_t move, bool relink, edict_t *current_bad)
|
|
{
|
|
if (ent->monsterinfo.aiflags & AI_ALTERNATE_FLY)
|
|
{
|
|
if (SV_alternate_flystep(ent, move, relink, current_bad))
|
|
return true;
|
|
}
|
|
|
|
// try the move
|
|
vec3_t oldorg = ent->s.origin;
|
|
vec3_t neworg = ent->s.origin + move;
|
|
|
|
// fixme: move to monsterinfo
|
|
// we want the carrier to stay a certain distance off the ground, to help prevent him
|
|
// from shooting his fliers, who spawn in below him
|
|
float minheight;
|
|
|
|
if (!strcmp(ent->classname, "monster_carrier"))
|
|
minheight = 104;
|
|
else
|
|
minheight = 40;
|
|
|
|
// try one move with vertical motion, then one without
|
|
for (int i = 0; i < 2; i++)
|
|
{
|
|
vec3_t new_move = move;
|
|
|
|
if (i == 0 && ent->enemy)
|
|
{
|
|
if (!ent->goalentity)
|
|
ent->goalentity = ent->enemy;
|
|
|
|
vec3_t &goal_position = (ent->monsterinfo.aiflags & AI_PATHING) ? ent->monsterinfo.nav_path.firstMovePoint : ent->goalentity->s.origin;
|
|
|
|
float dz = ent->s.origin[2] - goal_position[2];
|
|
float dist = move.length();
|
|
|
|
if (ent->goalentity->client)
|
|
{
|
|
if (dz > minheight)
|
|
{
|
|
// pmm
|
|
new_move *= 0.5f;
|
|
new_move[2] -= dist;
|
|
}
|
|
if (!((ent->flags & FL_SWIM) && (ent->waterlevel < WATER_WAIST)))
|
|
if (dz < (minheight - 10))
|
|
{
|
|
new_move *= 0.5f;
|
|
new_move[2] += dist;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// RAFAEL
|
|
if (strcmp(ent->classname, "monster_fixbot") == 0)
|
|
{
|
|
if (ent->s.frame >= 105 && ent->s.frame <= 120)
|
|
{
|
|
if (dz > 12)
|
|
new_move[2]--;
|
|
else if (dz < -12)
|
|
new_move[2]++;
|
|
}
|
|
else if (ent->s.frame >= 31 && ent->s.frame <= 88)
|
|
{
|
|
if (dz > 12)
|
|
new_move[2] -= 12;
|
|
else if (dz < -12)
|
|
new_move[2] += 12;
|
|
}
|
|
else
|
|
{
|
|
if (dz > 12)
|
|
new_move[2] -= 8;
|
|
else if (dz < -12)
|
|
new_move[2] += 8;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// RAFAEL
|
|
if (dz > 0)
|
|
{
|
|
new_move *= 0.5f;
|
|
new_move[2] -= min(dist, dz);
|
|
}
|
|
else if (dz < 0)
|
|
{
|
|
new_move *= 0.5f;
|
|
new_move[2] += -max(-dist, dz);
|
|
}
|
|
// RAFAEL
|
|
}
|
|
// RAFAEL
|
|
}
|
|
}
|
|
|
|
neworg = ent->s.origin + new_move;
|
|
|
|
trace_t trace = gi.trace(ent->s.origin, ent->mins, ent->maxs, neworg, ent, MASK_MONSTERSOLID);
|
|
|
|
// fly monsters don't enter water voluntarily
|
|
if (ent->flags & FL_FLY)
|
|
{
|
|
if (!ent->waterlevel)
|
|
{
|
|
vec3_t test { trace.endpos[0], trace.endpos[1], trace.endpos[2] + ent->mins[2] + 1 };
|
|
contents_t contents = gi.pointcontents(test);
|
|
if (contents & MASK_WATER)
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// swim monsters don't exit water voluntarily
|
|
if (ent->flags & FL_SWIM)
|
|
{
|
|
if (ent->waterlevel < WATER_WAIST)
|
|
{
|
|
vec3_t test { trace.endpos[0], trace.endpos[1], trace.endpos[2] + ent->mins[2] + 1 };
|
|
contents_t contents = gi.pointcontents(test);
|
|
if (!(contents & MASK_WATER))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ROGUE
|
|
if ((trace.fraction == 1) && (!trace.allsolid) && (!trace.startsolid))
|
|
// ROGUE
|
|
{
|
|
ent->s.origin = trace.endpos;
|
|
//=====
|
|
// PGM
|
|
if (!current_bad && CheckForBadArea(ent))
|
|
ent->s.origin = oldorg;
|
|
else
|
|
{
|
|
if (relink)
|
|
{
|
|
gi.linkentity(ent);
|
|
G_TouchTriggers(ent);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
// PGM
|
|
//=====
|
|
}
|
|
|
|
G_Impact(ent, trace);
|
|
|
|
if (!ent->enemy)
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
=============
|
|
SV_movestep
|
|
|
|
Called by monster program code.
|
|
The move will be adjusted for slopes and stairs, but if the move isn't
|
|
possible, no move is done, false is returned, and
|
|
pr_global_struct->trace_normal is set to the normal of the blocking wall
|
|
=============
|
|
*/
|
|
// FIXME since we need to test end position contents here, can we avoid doing
|
|
// it again later in catagorize position?
|
|
bool SV_movestep(edict_t *ent, vec3_t move, bool relink)
|
|
{
|
|
//======
|
|
// PGM
|
|
edict_t *current_bad = nullptr;
|
|
|
|
// PMM - who cares about bad areas if you're dead?
|
|
if (ent->health > 0)
|
|
{
|
|
current_bad = CheckForBadArea(ent);
|
|
if (current_bad)
|
|
{
|
|
ent->bad_area = current_bad;
|
|
|
|
if (ent->enemy && !strcmp(ent->enemy->classname, "tesla_mine"))
|
|
{
|
|
// if the tesla is in front of us, back up...
|
|
if (IsBadAhead(ent, current_bad, move))
|
|
move *= -1;
|
|
}
|
|
}
|
|
else if (ent->bad_area)
|
|
{
|
|
// if we're no longer in a bad area, get back to business.
|
|
ent->bad_area = nullptr;
|
|
if (ent->oldenemy) // && ent->bad_area->owner == ent->enemy)
|
|
{
|
|
ent->enemy = ent->oldenemy;
|
|
ent->goalentity = ent->oldenemy;
|
|
FoundTarget(ent);
|
|
}
|
|
}
|
|
}
|
|
// PGM
|
|
//======
|
|
|
|
// flying monsters don't step up
|
|
if (ent->flags & (FL_SWIM | FL_FLY))
|
|
return SV_flystep(ent, move, relink, current_bad);
|
|
|
|
// try the move
|
|
vec3_t oldorg = ent->s.origin;
|
|
|
|
float stepsize;
|
|
|
|
// push down from a step height above the wished position
|
|
if (ent->spawnflags.has(SPAWNFLAG_MONSTER_SUPER_STEP))
|
|
stepsize = 64.f;
|
|
else if (!(ent->monsterinfo.aiflags & AI_NOSTEP))
|
|
stepsize = STEPSIZE;
|
|
else
|
|
stepsize = 1;
|
|
|
|
stepsize += 0.75f;
|
|
|
|
contents_t mask = (ent->svflags & SVF_MONSTER) ? MASK_MONSTERSOLID : (MASK_SOLID | CONTENTS_MONSTER | CONTENTS_PLAYER);
|
|
|
|
vec3_t start_up = oldorg + ent->gravityVector * (-1 * stepsize);
|
|
|
|
start_up = gi.trace(oldorg, ent->mins, ent->maxs, start_up, ent, mask).endpos;
|
|
|
|
vec3_t end_up = start_up + move;
|
|
|
|
trace_t up_trace = gi.trace(start_up, ent->mins, ent->maxs, end_up, ent, mask);
|
|
|
|
if (up_trace.startsolid)
|
|
{
|
|
start_up += ent->gravityVector * (-1 * stepsize);
|
|
up_trace = gi.trace(start_up, ent->mins, ent->maxs, end_up, ent, mask);
|
|
}
|
|
|
|
vec3_t start_fwd = oldorg;
|
|
vec3_t end_fwd = start_fwd + move;
|
|
|
|
trace_t fwd_trace = gi.trace(start_fwd, ent->mins, ent->maxs, end_fwd, ent, mask);
|
|
|
|
if (fwd_trace.startsolid)
|
|
{
|
|
start_up += ent->gravityVector * (-1 * stepsize);
|
|
fwd_trace = gi.trace(start_fwd, ent->mins, ent->maxs, end_fwd, ent, mask);
|
|
}
|
|
|
|
// pick the one that went farther
|
|
trace_t &chosen_forward = (up_trace.fraction > fwd_trace.fraction) ? up_trace : fwd_trace;
|
|
|
|
if (chosen_forward.startsolid || chosen_forward.allsolid)
|
|
return false;
|
|
|
|
int32_t steps = 1;
|
|
bool stepped = false;
|
|
|
|
if (up_trace.fraction > fwd_trace.fraction)
|
|
steps = 2;
|
|
|
|
// step us down
|
|
vec3_t end = chosen_forward.endpos + (ent->gravityVector * (steps * stepsize));
|
|
trace_t trace = gi.trace(chosen_forward.endpos, ent->mins, ent->maxs, end, ent, mask);
|
|
|
|
if (fabsf(ent->s.origin.z - trace.endpos.z) > 8.f)
|
|
stepped = true;
|
|
|
|
// Paril: improved the water handling here.
|
|
// monsters are okay with stepping into water
|
|
// up to their waist.
|
|
if (ent->waterlevel <= WATER_WAIST)
|
|
{
|
|
water_level_t end_waterlevel;
|
|
contents_t end_watertype;
|
|
M_CatagorizePosition(ent, trace.endpos, end_waterlevel, end_watertype);
|
|
|
|
// don't go into deep liquids or
|
|
// slime/lava voluntarily
|
|
if (end_watertype & (CONTENTS_SLIME | CONTENTS_LAVA) ||
|
|
end_waterlevel > WATER_WAIST)
|
|
return false;
|
|
}
|
|
|
|
if (trace.fraction == 1)
|
|
{
|
|
// if monster had the ground pulled out, go ahead and fall
|
|
if (ent->flags & FL_PARTIALGROUND)
|
|
{
|
|
ent->s.origin += move;
|
|
if (relink)
|
|
{
|
|
gi.linkentity(ent);
|
|
G_TouchTriggers(ent);
|
|
}
|
|
ent->groundentity = nullptr;
|
|
return true;
|
|
}
|
|
else if (!ent->spawnflags.has(SPAWNFLAG_MONSTER_SUPER_STEP))
|
|
return false; // walked off an edge
|
|
}
|
|
|
|
// [Paril-KEX] if we didn't move at all (or barely moved), don't count it
|
|
if ((trace.endpos - oldorg).length() < move.length() * 0.05f)
|
|
{
|
|
ent->monsterinfo.bad_move_time = level.time + 1000_ms;
|
|
|
|
if (ent->monsterinfo.bump_time < level.time && chosen_forward.fraction < 1.0f)
|
|
{
|
|
// adjust ideal_yaw to move against the object we hit and try again
|
|
vec3_t dir = SlideClipVelocity(AngleVectors(vec3_t{0.f, ent->ideal_yaw, 0.f}).forward, chosen_forward.plane.normal, 1.0f);
|
|
float new_yaw = vectoyaw(dir);
|
|
|
|
if (dir.lengthSquared() > 0.1f && ent->ideal_yaw != new_yaw)
|
|
{
|
|
ent->ideal_yaw = new_yaw;
|
|
ent->monsterinfo.random_change_time = level.time + 100_ms;
|
|
ent->monsterinfo.bump_time = level.time + 200_ms;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// check point traces down for dangling corners
|
|
ent->s.origin = trace.endpos;
|
|
|
|
// PGM
|
|
// PMM - don't bother with bad areas if we're dead
|
|
if (ent->health > 0)
|
|
{
|
|
// use AI_BLOCKED to tell the calling layer that we're now mad at a tesla
|
|
new_bad = CheckForBadArea(ent);
|
|
if (!current_bad && new_bad)
|
|
{
|
|
if (new_bad->owner)
|
|
{
|
|
if (!strcmp(new_bad->owner->classname, "tesla_mine"))
|
|
{
|
|
if ((!(ent->enemy)) || (!(ent->enemy->inuse)))
|
|
{
|
|
TargetTesla(ent, new_bad->owner);
|
|
ent->monsterinfo.aiflags |= AI_BLOCKED;
|
|
}
|
|
else if (!strcmp(ent->enemy->classname, "tesla_mine"))
|
|
{
|
|
}
|
|
else if ((ent->enemy) && (ent->enemy->client))
|
|
{
|
|
if (!visible(ent, ent->enemy))
|
|
{
|
|
TargetTesla(ent, new_bad->owner);
|
|
ent->monsterinfo.aiflags |= AI_BLOCKED;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TargetTesla(ent, new_bad->owner);
|
|
ent->monsterinfo.aiflags |= AI_BLOCKED;
|
|
}
|
|
}
|
|
}
|
|
|
|
ent->s.origin = oldorg;
|
|
return false;
|
|
}
|
|
}
|
|
// PGM
|
|
|
|
if (!M_CheckBottom(ent))
|
|
{
|
|
if (ent->flags & FL_PARTIALGROUND)
|
|
{ // entity had floor mostly pulled out from underneath it
|
|
// and is trying to correct
|
|
if (relink)
|
|
{
|
|
gi.linkentity(ent);
|
|
G_TouchTriggers(ent);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// walked off an edge that wasn't a stairway
|
|
ent->s.origin = oldorg;
|
|
return false;
|
|
}
|
|
|
|
if (ent->spawnflags.has(SPAWNFLAG_MONSTER_SUPER_STEP))
|
|
{
|
|
if (!ent->groundentity || ent->groundentity->solid == SOLID_BSP)
|
|
{
|
|
if (!(trace.ent->solid == SOLID_BSP))
|
|
{
|
|
// walked off an edge
|
|
ent->s.origin = oldorg;
|
|
M_CheckGround(ent, G_GetClipMask(ent));
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// [Paril-KEX]
|
|
M_CheckGround(ent, G_GetClipMask(ent));
|
|
|
|
if (!ent->groundentity)
|
|
{
|
|
// walked off an edge
|
|
ent->s.origin = oldorg;
|
|
M_CheckGround(ent, G_GetClipMask(ent));
|
|
return false;
|
|
}
|
|
|
|
if (ent->flags & FL_PARTIALGROUND)
|
|
{
|
|
ent->flags &= ~FL_PARTIALGROUND;
|
|
}
|
|
ent->groundentity = trace.ent;
|
|
ent->groundentity_linkcount = trace.ent->linkcount;
|
|
|
|
// the move is ok
|
|
if (relink)
|
|
{
|
|
gi.linkentity(ent);
|
|
G_TouchTriggers(ent);
|
|
}
|
|
|
|
if (stepped)
|
|
ent->s.renderfx |= RF_STAIR_STEP;
|
|
|
|
if (trace.fraction < 1.f)
|
|
G_Impact(ent, trace);
|
|
|
|
return true;
|
|
}
|
|
|
|
// check if a movement would succeed
|
|
bool ai_check_move(edict_t *self, float dist)
|
|
{
|
|
if ( ai_movement_disabled->integer ) {
|
|
return false;
|
|
}
|
|
|
|
float yaw = self->s.angles[YAW] * PIf * 2 / 360;
|
|
vec3_t move = {
|
|
cosf(yaw) * dist,
|
|
sinf(yaw) * dist,
|
|
0
|
|
};
|
|
|
|
vec3_t old_origin = self->s.origin;
|
|
|
|
if (!SV_movestep(self, move, false))
|
|
return false;
|
|
|
|
self->s.origin = old_origin;
|
|
gi.linkentity(self);
|
|
return true;
|
|
}
|
|
|
|
//============================================================================
|
|
|
|
/*
|
|
===============
|
|
M_ChangeYaw
|
|
|
|
===============
|
|
*/
|
|
void M_ChangeYaw(edict_t *ent)
|
|
{
|
|
float ideal;
|
|
float current;
|
|
float move;
|
|
float speed;
|
|
|
|
current = anglemod(ent->s.angles[YAW]);
|
|
ideal = ent->ideal_yaw;
|
|
|
|
if (current == ideal)
|
|
return;
|
|
|
|
move = ideal - current;
|
|
// [Paril-KEX] high tick rate
|
|
speed = ent->yaw_speed / (gi.tick_rate / 10);
|
|
|
|
if (ideal > current)
|
|
{
|
|
if (move >= 180)
|
|
move = move - 360;
|
|
}
|
|
else
|
|
{
|
|
if (move <= -180)
|
|
move = move + 360;
|
|
}
|
|
if (move > 0)
|
|
{
|
|
if (move > speed)
|
|
move = speed;
|
|
}
|
|
else
|
|
{
|
|
if (move < -speed)
|
|
move = -speed;
|
|
}
|
|
|
|
ent->s.angles[YAW] = anglemod(current + move);
|
|
}
|
|
|
|
/*
|
|
======================
|
|
SV_StepDirection
|
|
|
|
Turns to the movement direction, and walks the current distance if
|
|
facing it.
|
|
|
|
======================
|
|
*/
|
|
bool SV_StepDirection(edict_t *ent, float yaw, float dist, bool allow_no_turns)
|
|
{
|
|
vec3_t move, oldorigin;
|
|
|
|
if (!ent->inuse)
|
|
return true; // PGM g_touchtrigger free problem
|
|
|
|
float old_ideal_yaw = ent->ideal_yaw;
|
|
float old_current_yaw = ent->s.angles[YAW];
|
|
|
|
ent->ideal_yaw = yaw;
|
|
M_ChangeYaw(ent);
|
|
|
|
yaw = yaw * PIf * 2 / 360;
|
|
move[0] = cosf(yaw) * dist;
|
|
move[1] = sinf(yaw) * dist;
|
|
move[2] = 0;
|
|
|
|
oldorigin = ent->s.origin;
|
|
if (SV_movestep(ent, move, false))
|
|
{
|
|
ent->monsterinfo.aiflags &= ~AI_BLOCKED;
|
|
if (!ent->inuse)
|
|
return true; // PGM g_touchtrigger free problem
|
|
|
|
if (strncmp(ent->classname, "monster_widow", 13))
|
|
{
|
|
if (!FacingIdeal(ent))
|
|
{
|
|
// not turned far enough, so don't take the step
|
|
// but still turn
|
|
ent->s.origin = oldorigin;
|
|
M_CheckGround(ent, G_GetClipMask(ent));
|
|
return allow_no_turns; // [Paril-KEX]
|
|
}
|
|
}
|
|
gi.linkentity(ent);
|
|
G_TouchTriggers(ent);
|
|
G_TouchProjectiles(ent, oldorigin);
|
|
return true;
|
|
}
|
|
gi.linkentity(ent);
|
|
G_TouchTriggers(ent);
|
|
ent->ideal_yaw = old_ideal_yaw;
|
|
ent->s.angles[YAW] = old_current_yaw;
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
======================
|
|
SV_FixCheckBottom
|
|
|
|
======================
|
|
*/
|
|
void SV_FixCheckBottom(edict_t *ent)
|
|
{
|
|
ent->flags |= FL_PARTIALGROUND;
|
|
}
|
|
|
|
/*
|
|
================
|
|
SV_NewChaseDir
|
|
|
|
================
|
|
*/
|
|
constexpr float DI_NODIR = -1;
|
|
|
|
bool SV_NewChaseDir(edict_t *actor, vec3_t pos, float dist)
|
|
{
|
|
float deltax, deltay;
|
|
float d[3];
|
|
float tdir, olddir, turnaround;
|
|
|
|
olddir = anglemod(truncf(actor->ideal_yaw / 45) * 45);
|
|
turnaround = anglemod(olddir - 180);
|
|
|
|
deltax = pos[0] - actor->s.origin[0];
|
|
deltay = pos[1] - actor->s.origin[1];
|
|
if (deltax > 10)
|
|
d[1] = 0;
|
|
else if (deltax < -10)
|
|
d[1] = 180;
|
|
else
|
|
d[1] = DI_NODIR;
|
|
if (deltay < -10)
|
|
d[2] = 270;
|
|
else if (deltay > 10)
|
|
d[2] = 90;
|
|
else
|
|
d[2] = DI_NODIR;
|
|
|
|
// try direct route
|
|
if (d[1] != DI_NODIR && d[2] != DI_NODIR)
|
|
{
|
|
if (d[1] == 0)
|
|
tdir = d[2] == 90 ? 45.f : 315.f;
|
|
else
|
|
tdir = d[2] == 90 ? 135.f : 215.f;
|
|
|
|
if (tdir != turnaround && SV_StepDirection(actor, tdir, dist, false))
|
|
return true;
|
|
}
|
|
|
|
// try other directions
|
|
if (brandom() || fabsf(deltay) > fabsf(deltax))
|
|
{
|
|
tdir = d[1];
|
|
d[1] = d[2];
|
|
d[2] = tdir;
|
|
}
|
|
|
|
if (d[1] != DI_NODIR && d[1] != turnaround && SV_StepDirection(actor, d[1], dist, false))
|
|
return true;
|
|
|
|
if (d[2] != DI_NODIR && d[2] != turnaround && SV_StepDirection(actor, d[2], dist, false))
|
|
return true;
|
|
|
|
// ROGUE
|
|
if (actor->monsterinfo.blocked)
|
|
{
|
|
if ((actor->inuse) && (actor->health > 0) && !(actor->monsterinfo.aiflags & AI_TARGET_ANGER))
|
|
{
|
|
// if block "succeeds", the actor will not move or turn.
|
|
if (actor->monsterinfo.blocked(actor, dist))
|
|
{
|
|
actor->monsterinfo.move_block_counter = -2;
|
|
return true;
|
|
}
|
|
|
|
// we couldn't step; instead of running endlessly in our current
|
|
// spot, try switching to node navigation temporarily to get to
|
|
// where we need to go.
|
|
if (!(actor->monsterinfo.aiflags & (AI_LOST_SIGHT | AI_COMBAT_POINT | AI_TARGET_ANGER | AI_PATHING | AI_TEMP_MELEE_COMBAT | AI_NO_PATH_FINDING)))
|
|
{
|
|
if (++actor->monsterinfo.move_block_counter > 2)
|
|
{
|
|
actor->monsterinfo.aiflags |= AI_TEMP_MELEE_COMBAT;
|
|
actor->monsterinfo.move_block_change_time = level.time + 3_sec;
|
|
actor->monsterinfo.move_block_counter = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// ROGUE
|
|
|
|
/* there is no direct path to the player, so pick another direction */
|
|
|
|
if (olddir != DI_NODIR && SV_StepDirection(actor, olddir, dist, false))
|
|
return true;
|
|
|
|
if (brandom()) /*randomly determine direction of search*/
|
|
{
|
|
for (tdir = 0; tdir <= 315; tdir += 45)
|
|
if (tdir != turnaround && SV_StepDirection(actor, tdir, dist, false))
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
for (tdir = 315; tdir >= 0; tdir -= 45)
|
|
if (tdir != turnaround && SV_StepDirection(actor, tdir, dist, false))
|
|
return true;
|
|
}
|
|
|
|
if (turnaround != DI_NODIR && SV_StepDirection(actor, turnaround, dist, false))
|
|
return true;
|
|
|
|
actor->ideal_yaw = frandom(0, 360); // can't move; pick a random yaw...
|
|
|
|
// if a bridge was pulled out from underneath a monster, it may not have
|
|
// a valid standing position at all
|
|
|
|
if (!M_CheckBottom(actor))
|
|
SV_FixCheckBottom(actor);
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
======================
|
|
SV_CloseEnough
|
|
|
|
======================
|
|
*/
|
|
bool SV_CloseEnough(edict_t *ent, edict_t *goal, float dist)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < 3; i++)
|
|
{
|
|
if (goal->absmin[i] > ent->absmax[i] + dist)
|
|
return false;
|
|
if (goal->absmax[i] < ent->absmin[i] - dist)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool M_NavPathToGoal(edict_t *self, float dist, const vec3_t &goal)
|
|
{
|
|
// mark us as *trying* now (nav_pos is valid)
|
|
self->monsterinfo.aiflags |= AI_PATHING;
|
|
|
|
vec3_t &path_to = (self->monsterinfo.nav_path.returnCode == PathReturnCode::TraversalPending) ?
|
|
self->monsterinfo.nav_path.secondMovePoint : self->monsterinfo.nav_path.firstMovePoint;
|
|
|
|
if ((self->monsterinfo.nav_path.returnCode != PathReturnCode::TraversalPending && (path_to - self->s.origin).length() <= (self->size.length() * 0.5f)) ||
|
|
self->monsterinfo.nav_path_cache_time <= level.time)
|
|
{
|
|
PathRequest request;
|
|
if (self->enemy)
|
|
request.goal = self->enemy->s.origin;
|
|
else
|
|
request.goal = self->goalentity->s.origin;
|
|
request.moveDist = dist;
|
|
if (g_debug_monster_paths->integer == 1)
|
|
request.debugging.drawTime = gi.frame_time_s;
|
|
request.start = self->s.origin;
|
|
request.pathFlags = PathFlags::Walk;
|
|
|
|
if (self->monsterinfo.can_jump || (self->flags & FL_FLY))
|
|
{
|
|
if (self->monsterinfo.jump_height)
|
|
{
|
|
request.pathFlags |= PathFlags::BarrierJump;
|
|
request.traversals.jumpHeight = self->monsterinfo.jump_height;
|
|
}
|
|
if (self->monsterinfo.drop_height)
|
|
{
|
|
request.pathFlags |= PathFlags::WalkOffLedge;
|
|
request.traversals.dropHeight = self->monsterinfo.drop_height;
|
|
}
|
|
}
|
|
|
|
if (self->flags & FL_FLY)
|
|
{
|
|
request.nodeSearch.maxHeight = request.nodeSearch.minHeight = 8192.f;
|
|
request.pathFlags |= PathFlags::LongJump;
|
|
}
|
|
|
|
if (!gi.GetPathToGoal(request, self->monsterinfo.nav_path))
|
|
{
|
|
// fatal error, don't bother ever trying nodes
|
|
if (self->monsterinfo.nav_path.returnCode == PathReturnCode::NoNavAvailable)
|
|
self->monsterinfo.aiflags |= AI_NO_PATH_FINDING;
|
|
return false;
|
|
}
|
|
|
|
self->monsterinfo.nav_path_cache_time = level.time + 2_sec;
|
|
}
|
|
|
|
float yaw;
|
|
float old_yaw = self->s.angles[YAW];
|
|
float old_ideal_yaw = self->ideal_yaw;
|
|
|
|
if (self->monsterinfo.random_change_time >= level.time &&
|
|
!(self->monsterinfo.aiflags & AI_ALTERNATE_FLY))
|
|
yaw = self->ideal_yaw;
|
|
else
|
|
yaw = vectoyaw((path_to - self->s.origin).normalized());
|
|
|
|
if ( !SV_StepDirection( self, yaw, dist, true ) ) {
|
|
|
|
if (!self->inuse)
|
|
return false;
|
|
|
|
if (self->monsterinfo.blocked && !(self->monsterinfo.aiflags & AI_TARGET_ANGER))
|
|
{
|
|
if ((self->inuse) && (self->health > 0))
|
|
{
|
|
// if we're blocked, the blocked function will be deferred to for yaw
|
|
self->s.angles[YAW] = old_yaw;
|
|
self->ideal_yaw = old_ideal_yaw;
|
|
if (self->monsterinfo.blocked(self, dist))
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// try the first point
|
|
if (self->monsterinfo.random_change_time >= level.time)
|
|
yaw = self->ideal_yaw;
|
|
else
|
|
yaw = vectoyaw((self->monsterinfo.nav_path.firstMovePoint - self->s.origin).normalized());
|
|
|
|
if ( !SV_StepDirection( self, yaw, dist, true ) ) {
|
|
|
|
// we got blocked, but all is not lost yet; do a similar bump around-ish behavior
|
|
// to try to regain our composure
|
|
if (self->monsterinfo.aiflags & AI_BLOCKED)
|
|
{
|
|
self->monsterinfo.aiflags &= ~AI_BLOCKED;
|
|
return true;
|
|
}
|
|
|
|
if (self->monsterinfo.random_change_time < level.time && self->inuse)
|
|
{
|
|
self->monsterinfo.random_change_time = level.time + 1500_ms;
|
|
if (SV_NewChaseDir(self, path_to, dist))
|
|
return true;
|
|
}
|
|
|
|
self->monsterinfo.path_blocked_counter += FRAME_TIME_S * 3;
|
|
}
|
|
|
|
if (self->monsterinfo.path_blocked_counter > 1.5_sec)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
=============
|
|
M_MoveToPath
|
|
|
|
Advanced movement code that use the bots pathfinder if allowed and conditions are right.
|
|
Feel free to add any other conditions needed.
|
|
=============
|
|
*/
|
|
static bool M_MoveToPath(edict_t *self, float dist)
|
|
{
|
|
if (self->flags & FL_STATIONARY)
|
|
return false;
|
|
else if (self->monsterinfo.aiflags & AI_NO_PATH_FINDING)
|
|
return false;
|
|
else if (self->monsterinfo.path_wait_time > level.time)
|
|
return false;
|
|
else if (!self->enemy)
|
|
return false;
|
|
else if (self->enemy->client && self->enemy->client->invisible_time > level.time && self->enemy->client->invisibility_fade_time <= level.time)
|
|
return false;
|
|
else if (self->monsterinfo.attack_state >= AS_MISSILE)
|
|
return true;
|
|
|
|
combat_style_t style = self->monsterinfo.combat_style;
|
|
|
|
if (self->monsterinfo.aiflags & AI_TEMP_MELEE_COMBAT)
|
|
style = COMBAT_MELEE;
|
|
|
|
if ( visible(self, self->enemy, false) ) {
|
|
if ( (self->flags & (FL_SWIM | FL_FLY)) || style == COMBAT_RANGED ) {
|
|
// do the normal "shoot, walk, shoot" behavior...
|
|
return false;
|
|
} else if ( style == COMBAT_MELEE ) {
|
|
// path pretty close to the enemy, then let normal Quake movement take over.
|
|
if ( range_to(self, self->enemy) > 240.f ||
|
|
fabs(self->s.origin.z - self->enemy->s.origin.z) > max(self->maxs.z, -self->mins.z) ) {
|
|
if ( M_NavPathToGoal( self, dist, self->enemy->s.origin ) ) {
|
|
return true;
|
|
}
|
|
self->monsterinfo.aiflags &= ~AI_TEMP_MELEE_COMBAT;
|
|
} else {
|
|
self->monsterinfo.aiflags &= ~AI_TEMP_MELEE_COMBAT;
|
|
return false;
|
|
}
|
|
} else if ( style == COMBAT_MIXED ) {
|
|
// most mixed combat AI have fairly short range attacks, so try to path within mid range.
|
|
if ( range_to(self, self->enemy) > RANGE_NEAR ||
|
|
fabs(self->s.origin.z - self->enemy->s.origin.z) > max(self->maxs.z, -self->mins.z) * 2.0f ) {
|
|
if ( M_NavPathToGoal( self, dist, self->enemy->s.origin ) ) {
|
|
return true;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
// we can't see our enemy, let's see if we can path to them
|
|
if ( M_NavPathToGoal( self, dist, self->enemy->s.origin ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (!self->inuse)
|
|
return false;
|
|
|
|
if (self->monsterinfo.nav_path.returnCode > PathReturnCode::StartPathErrors)
|
|
{
|
|
self->monsterinfo.path_wait_time = level.time + 10_sec;
|
|
return false;
|
|
}
|
|
|
|
self->monsterinfo.path_blocked_counter += FRAME_TIME_S * 3;
|
|
|
|
if (self->monsterinfo.path_blocked_counter > 5_sec)
|
|
{
|
|
self->monsterinfo.path_blocked_counter = 0_ms;
|
|
self->monsterinfo.path_wait_time = level.time + 5_sec;
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
======================
|
|
M_MoveToGoal
|
|
======================
|
|
*/
|
|
void M_MoveToGoal(edict_t *ent, float dist)
|
|
{
|
|
if ( ai_movement_disabled->integer ) {
|
|
if ( !FacingIdeal( ent ) ) {
|
|
M_ChangeYaw( ent );
|
|
} // mal: don't move, but still face toward target
|
|
return;
|
|
}
|
|
|
|
edict_t *goal;
|
|
|
|
goal = ent->goalentity;
|
|
|
|
if (!ent->groundentity && !(ent->flags & (FL_FLY | FL_SWIM)))
|
|
return;
|
|
// ???
|
|
else if (!goal)
|
|
return;
|
|
|
|
// [Paril-KEX] try paths if we can't see the enemy
|
|
if (!(ent->monsterinfo.aiflags & AI_COMBAT_POINT) && ent->monsterinfo.attack_state < AS_MISSILE)
|
|
{
|
|
if (M_MoveToPath(ent, dist))
|
|
{
|
|
ent->monsterinfo.path_blocked_counter = max(0_ms, ent->monsterinfo.path_blocked_counter - FRAME_TIME_S);
|
|
return;
|
|
}
|
|
}
|
|
|
|
ent->monsterinfo.aiflags &= ~AI_PATHING;
|
|
|
|
//if (goal)
|
|
// gi.Draw_Point(goal->s.origin, 1.f, rgba_red, gi.frame_time_ms, false);
|
|
|
|
// [Paril-KEX] dumb hack; in some n64 maps, the corners are way too high and
|
|
// I'm too lazy to fix them individually in maps, so here's a game fix..
|
|
if (!(goal->flags & FL_PARTIALGROUND) && !(ent->flags & (FL_FLY | FL_SWIM)) &&
|
|
goal->classname && (!strcmp(goal->classname, "path_corner") || !strcmp(goal->classname, "point_combat")))
|
|
{
|
|
vec3_t p = goal->s.origin;
|
|
p.z = ent->s.origin.z;
|
|
|
|
if (boxes_intersect(ent->absmin, ent->absmax, p, p))
|
|
{
|
|
// mark this so we don't do it again later
|
|
goal->flags |= FL_PARTIALGROUND;
|
|
|
|
if (!boxes_intersect(ent->absmin, ent->absmax, goal->s.origin, goal->s.origin))
|
|
{
|
|
// move it if we would have touched it if the corner was lower
|
|
goal->s.origin.z = p.z;
|
|
gi.linkentity(goal);
|
|
}
|
|
}
|
|
}
|
|
|
|
// [Paril-KEX] if we have a straight shot to our target, just move
|
|
// straight instead of trying to stick to invisible guide lines
|
|
if ((ent->monsterinfo.bad_move_time <= level.time || (ent->monsterinfo.aiflags & AI_CHARGING)) && goal)
|
|
{
|
|
if (!FacingIdeal(ent))
|
|
{
|
|
M_ChangeYaw(ent);
|
|
return;
|
|
}
|
|
|
|
trace_t tr = gi.traceline(ent->s.origin, goal->s.origin, ent, MASK_MONSTERSOLID);
|
|
|
|
if (tr.fraction == 1.0f || tr.ent == goal)
|
|
{
|
|
if (SV_StepDirection(ent, vectoyaw((goal->s.origin - ent->s.origin).normalized()), dist, false))
|
|
return;
|
|
}
|
|
|
|
// we didn't make a step, so don't try this for a while
|
|
// *unless* we're going to a path corner
|
|
if (goal->classname && strcmp(goal->classname, "path_corner") && strcmp(goal->classname, "point_combat"))
|
|
{
|
|
ent->monsterinfo.bad_move_time = level.time + 5_sec;
|
|
ent->monsterinfo.aiflags &= ~AI_CHARGING;
|
|
}
|
|
}
|
|
|
|
// bump around...
|
|
if ((ent->monsterinfo.random_change_time <= level.time // random change time is up
|
|
&& irandom(4) == 1 // random bump around
|
|
&& !(ent->monsterinfo.aiflags & AI_CHARGING) // PMM - charging monsters (AI_CHARGING) don't deflect unless they have to
|
|
&& !((ent->monsterinfo.aiflags & AI_ALTERNATE_FLY) && ent->enemy && !(ent->monsterinfo.aiflags & AI_LOST_SIGHT))) // alternate fly monsters don't do this either unless they have to
|
|
|| !SV_StepDirection(ent, ent->ideal_yaw, dist, ent->monsterinfo.bad_move_time > level.time))
|
|
{
|
|
if (ent->monsterinfo.aiflags & AI_BLOCKED)
|
|
{
|
|
ent->monsterinfo.aiflags &= ~AI_BLOCKED;
|
|
return;
|
|
}
|
|
ent->monsterinfo.random_change_time = level.time + random_time(500_ms, 1000_ms);
|
|
SV_NewChaseDir(ent, goal->s.origin, dist);
|
|
ent->monsterinfo.move_block_counter = 0;
|
|
}
|
|
else
|
|
ent->monsterinfo.bad_move_time -= 250_ms;
|
|
|
|
//vec3_t dir = AngleVectors({ 0.f, ent->ideal_yaw, 0.f }).forward;
|
|
//gi.Draw_Line(ent->s.origin, ent->s.origin + (dir * 24), rgba_blue, gi.frame_time_ms, false);
|
|
}
|
|
|
|
/*
|
|
===============
|
|
M_walkmove
|
|
===============
|
|
*/
|
|
bool M_walkmove(edict_t *ent, float yaw, float dist)
|
|
{
|
|
if ( ai_movement_disabled->integer ) {
|
|
return false;
|
|
}
|
|
|
|
vec3_t move;
|
|
// PMM
|
|
bool retval;
|
|
|
|
if (!ent->groundentity && !(ent->flags & (FL_FLY | FL_SWIM)))
|
|
return false;
|
|
|
|
yaw = yaw * PIf * 2 / 360;
|
|
|
|
move[0] = cosf(yaw) * dist;
|
|
move[1] = sinf(yaw) * dist;
|
|
move[2] = 0;
|
|
|
|
// PMM
|
|
retval = SV_movestep(ent, move, true);
|
|
ent->monsterinfo.aiflags &= ~AI_BLOCKED;
|
|
return retval;
|
|
}
|