quake2-rerelease-dll/rerelease/m_move.cpp
2023-08-07 14:48:30 -05:00

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;
}