mirror of
https://github.com/yquake2/rogue.git
synced 2024-11-26 06:00:50 +00:00
1748 lines
30 KiB
C
1748 lines
30 KiB
C
/*
|
|
* =======================================================================
|
|
*
|
|
* Rogue specific AI code
|
|
*
|
|
* =======================================================================
|
|
*/
|
|
|
|
#include "header/local.h"
|
|
|
|
#define STATE_TOP 0
|
|
#define STATE_BOTTOM 1
|
|
#define STATE_UP 2
|
|
#define STATE_DOWN 3
|
|
|
|
#define HINT_ENDPOINT 0x0001
|
|
#define MAX_HINT_CHAINS 100
|
|
|
|
#define TESLA_DAMAGE_RADIUS 128
|
|
|
|
edict_t *hint_path_start[MAX_HINT_CHAINS];
|
|
int hint_paths_present;
|
|
int num_hint_paths;
|
|
|
|
qboolean face_wall(edict_t *self);
|
|
qboolean monsterlost_checkhint2(edict_t *self);
|
|
void HuntTarget(edict_t *self);
|
|
|
|
qboolean
|
|
blocked_checkplat(edict_t *self, float dist)
|
|
{
|
|
int playerPosition;
|
|
trace_t trace;
|
|
vec3_t pt1, pt2;
|
|
vec3_t forward;
|
|
edict_t *plat;
|
|
|
|
if (!self)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!self->enemy)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/* check player's relative altitude */
|
|
if (self->enemy->absmin[2] >= self->absmax[2])
|
|
{
|
|
playerPosition = 1;
|
|
}
|
|
else if (self->enemy->absmax[2] <= self->absmin[2])
|
|
{
|
|
playerPosition = -1;
|
|
}
|
|
else
|
|
{
|
|
playerPosition = 0;
|
|
}
|
|
|
|
/* if we're close to the same position, don't bother trying plats. */
|
|
if (playerPosition == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
plat = NULL;
|
|
|
|
/* see if we're already standing on a plat. */
|
|
if (self->groundentity && (self->groundentity != world))
|
|
{
|
|
if (!strncmp(self->groundentity->classname, "func_plat", 8))
|
|
{
|
|
plat = self->groundentity;
|
|
}
|
|
}
|
|
|
|
/* if we're not, check to see if we'll step onto one with this move */
|
|
if (!plat)
|
|
{
|
|
AngleVectors(self->s.angles, forward, NULL, NULL);
|
|
VectorMA(self->s.origin, dist, forward, pt1);
|
|
VectorCopy(pt1, pt2);
|
|
pt2[2] -= 384;
|
|
|
|
trace = gi.trace(pt1, vec3_origin, vec3_origin, pt2,
|
|
self, MASK_MONSTERSOLID);
|
|
|
|
if ((trace.fraction < 1) && !trace.allsolid && !trace.startsolid)
|
|
{
|
|
if (!strncmp(trace.ent->classname, "func_plat", 8))
|
|
{
|
|
plat = trace.ent;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* if we've found a plat, trigger it. */
|
|
if (plat && plat->use)
|
|
{
|
|
if (playerPosition == 1)
|
|
{
|
|
if (((self->groundentity == plat) &&
|
|
(plat->moveinfo.state == STATE_BOTTOM)) ||
|
|
((self->groundentity != plat) &&
|
|
(plat->moveinfo.state == STATE_TOP)))
|
|
{
|
|
plat->use(plat, self, self);
|
|
return true;
|
|
}
|
|
}
|
|
else if (playerPosition == -1)
|
|
{
|
|
if (((self->groundentity == plat) &&
|
|
(plat->moveinfo.state == STATE_TOP)) ||
|
|
((self->groundentity != plat) &&
|
|
(plat->moveinfo.state == STATE_BOTTOM)))
|
|
{
|
|
plat->use(plat, self, self);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
qboolean
|
|
blocked_checkjump(edict_t *self, float dist, float maxDown, float maxUp)
|
|
{
|
|
int playerPosition;
|
|
trace_t trace;
|
|
vec3_t pt1, pt2;
|
|
vec3_t forward, up;
|
|
|
|
if (!self)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!self->enemy)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
AngleVectors(self->s.angles, forward, NULL, up);
|
|
|
|
if (self->enemy->absmin[2] > (self->absmin[2] + 16))
|
|
{
|
|
playerPosition = 1;
|
|
}
|
|
else if (self->enemy->absmin[2] < (self->absmin[2] - 16))
|
|
{
|
|
playerPosition = -1;
|
|
}
|
|
else
|
|
{
|
|
playerPosition = 0;
|
|
}
|
|
|
|
if ((playerPosition == -1) && maxDown)
|
|
{
|
|
/* check to make sure we can even get to the spot we're going to "fall" from */
|
|
VectorMA(self->s.origin, 48, forward, pt1);
|
|
trace = gi.trace(self->s.origin, self->mins, self->maxs, pt1,
|
|
self, MASK_MONSTERSOLID);
|
|
|
|
if (trace.fraction < 1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
VectorCopy(pt1, pt2);
|
|
pt2[2] = self->absmin[2] - maxDown - 1;
|
|
|
|
trace = gi.trace(pt1, vec3_origin, vec3_origin, pt2, self,
|
|
MASK_MONSTERSOLID | MASK_WATER);
|
|
|
|
if ((trace.fraction < 1) && !trace.allsolid && !trace.startsolid)
|
|
{
|
|
if (((self->absmin[2] - trace.endpos[2]) >=
|
|
24) && (trace.contents & MASK_SOLID))
|
|
{
|
|
if ((self->enemy->absmin[2] - trace.endpos[2]) > 32)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (trace.plane.normal[2] < 0.9)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
else if ((playerPosition == 1) && maxUp)
|
|
{
|
|
VectorMA(self->s.origin, 48, forward, pt1);
|
|
VectorCopy(pt1, pt2);
|
|
pt1[2] = self->absmax[2] + maxUp;
|
|
|
|
trace = gi.trace(pt1, vec3_origin, vec3_origin, pt2, self,
|
|
MASK_MONSTERSOLID | MASK_WATER);
|
|
|
|
if ((trace.fraction < 1) && !trace.allsolid && !trace.startsolid)
|
|
{
|
|
if (((trace.endpos[2] - self->absmin[2]) <= maxUp) &&
|
|
trace.contents & MASK_SOLID)
|
|
{
|
|
face_wall(self);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
qboolean
|
|
blocked_checknewenemy(edict_t *self)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
edict_t *
|
|
hintpath_findstart(edict_t *ent)
|
|
{
|
|
edict_t *e;
|
|
edict_t *last;
|
|
int field;
|
|
|
|
if (!ent)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
if (ent->target) /* starting point */
|
|
{
|
|
last = world;
|
|
field = FOFS(targetname);
|
|
e = G_Find(NULL, field, ent->target);
|
|
|
|
while (e)
|
|
{
|
|
last = e;
|
|
|
|
if (!e->target)
|
|
{
|
|
break;
|
|
}
|
|
|
|
e = G_Find(NULL, field, e->target);
|
|
}
|
|
}
|
|
else /* end point */
|
|
{
|
|
last = world;
|
|
field = FOFS(target);
|
|
e = G_Find(NULL, field, ent->targetname);
|
|
|
|
while (e)
|
|
{
|
|
last = e;
|
|
|
|
if (!e->targetname)
|
|
{
|
|
break;
|
|
}
|
|
|
|
e = G_Find(NULL, field, e->targetname);
|
|
}
|
|
}
|
|
|
|
if (!(last->spawnflags & HINT_ENDPOINT))
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
if (last == world)
|
|
{
|
|
last = NULL;
|
|
}
|
|
|
|
return last;
|
|
}
|
|
|
|
edict_t *
|
|
hintpath_other_end(edict_t *ent)
|
|
{
|
|
edict_t *e;
|
|
edict_t *last;
|
|
int field;
|
|
|
|
if (!ent)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
if (ent->target) /* starting point */
|
|
{
|
|
last = world;
|
|
field = FOFS(targetname);
|
|
e = G_Find(NULL, field, ent->target);
|
|
|
|
while (e)
|
|
{
|
|
last = e;
|
|
|
|
if (!e->target)
|
|
{
|
|
break;
|
|
}
|
|
|
|
e = G_Find(NULL, field, e->target);
|
|
}
|
|
}
|
|
else /* end point */
|
|
{
|
|
last = world;
|
|
field = FOFS(target);
|
|
e = G_Find(NULL, field, ent->targetname);
|
|
|
|
while (e)
|
|
{
|
|
last = e;
|
|
|
|
if (!e->targetname)
|
|
{
|
|
break;
|
|
}
|
|
|
|
e = G_Find(NULL, field, e->targetname);
|
|
}
|
|
}
|
|
|
|
if (!(last->spawnflags & HINT_ENDPOINT))
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
if (last == world)
|
|
{
|
|
last = NULL;
|
|
}
|
|
|
|
return last;
|
|
}
|
|
|
|
void
|
|
hintpath_go(edict_t *self, edict_t *point)
|
|
{
|
|
vec3_t dir;
|
|
vec3_t angles;
|
|
|
|
if (!self || !point)
|
|
{
|
|
return;
|
|
}
|
|
|
|
VectorSubtract(point->s.origin, self->s.origin, dir);
|
|
vectoangles2(dir, angles);
|
|
|
|
self->ideal_yaw = angles[YAW];
|
|
self->goalentity = self->movetarget = point;
|
|
self->monsterinfo.pausetime = 0;
|
|
self->monsterinfo.aiflags |= AI_HINT_PATH;
|
|
self->monsterinfo.aiflags &= ~(AI_SOUND_TARGET | AI_PURSUIT_LAST_SEEN | AI_PURSUE_NEXT | AI_PURSUE_TEMP);
|
|
|
|
/* run for it */
|
|
self->monsterinfo.search_time = level.time;
|
|
self->monsterinfo.run(self);
|
|
}
|
|
|
|
void
|
|
hintpath_stop(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->goalentity = NULL;
|
|
self->movetarget = NULL;
|
|
self->monsterinfo.last_hint_time = level.time;
|
|
self->monsterinfo.goal_hint = NULL;
|
|
self->monsterinfo.aiflags &= ~AI_HINT_PATH;
|
|
|
|
if (has_valid_enemy(self))
|
|
{
|
|
/* if we can see our target, go nuts */
|
|
if (visible(self, self->enemy))
|
|
{
|
|
FoundTarget(self);
|
|
return;
|
|
}
|
|
|
|
/* otherwise, keep chasing */
|
|
HuntTarget(self);
|
|
return;
|
|
}
|
|
|
|
/* if our enemy is no longer valid, forget about our enemy and go into stand */
|
|
self->enemy = NULL;
|
|
|
|
/* we need the pausetime otherwise the stand code
|
|
will just revert to walking with no target and
|
|
the monsters will wonder around aimlessly trying
|
|
to hunt the world entity */
|
|
self->monsterinfo.pausetime = level.time + 100000000;
|
|
self->monsterinfo.stand(self);
|
|
}
|
|
|
|
qboolean
|
|
monsterlost_checkhint(edict_t *self)
|
|
{
|
|
edict_t *e, *monster_pathchain, *target_pathchain;
|
|
edict_t *checkpoint = NULL;
|
|
edict_t *closest;
|
|
float closest_range = 1000000;
|
|
edict_t *start, *destination;
|
|
int count1 = 0, count2 = 0, count4 = 0, count5 = 0;
|
|
float r;
|
|
int i;
|
|
qboolean hint_path_represented[MAX_HINT_CHAINS];
|
|
|
|
if (!self)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/* if there are no hint paths on this map, exit immediately. */
|
|
if (!hint_paths_present)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!self->enemy)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (self->monsterinfo.aiflags & AI_STAND_GROUND)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!strcmp(self->classname, "monster_turret"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
monster_pathchain = NULL;
|
|
|
|
/* find all the hint_paths. */
|
|
for (i = 0; i < num_hint_paths; i++)
|
|
{
|
|
e = hint_path_start[i];
|
|
|
|
while (e)
|
|
{
|
|
count1++;
|
|
|
|
if (e->monster_hint_chain)
|
|
{
|
|
e->monster_hint_chain = NULL;
|
|
}
|
|
|
|
if (monster_pathchain)
|
|
{
|
|
checkpoint->monster_hint_chain = e;
|
|
checkpoint = e;
|
|
}
|
|
else
|
|
{
|
|
monster_pathchain = e;
|
|
checkpoint = e;
|
|
}
|
|
|
|
e = e->hint_chain;
|
|
}
|
|
}
|
|
|
|
/* filter them by distance and visibility to the monster */
|
|
e = monster_pathchain;
|
|
checkpoint = NULL;
|
|
|
|
while (e)
|
|
{
|
|
r = realrange(self, e);
|
|
|
|
if (r > 512)
|
|
{
|
|
count2++;
|
|
|
|
if (checkpoint)
|
|
{
|
|
checkpoint->monster_hint_chain = e->monster_hint_chain;
|
|
e->monster_hint_chain = NULL;
|
|
e = checkpoint->monster_hint_chain;
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
/* use checkpoint as temp pointer */
|
|
checkpoint = e;
|
|
e = e->monster_hint_chain;
|
|
checkpoint->monster_hint_chain = NULL;
|
|
|
|
/* and clear it again */
|
|
checkpoint = NULL;
|
|
|
|
/* since we have yet to find a valid one (or else
|
|
checkpoint would be set) move the start of
|
|
monster_pathchain */
|
|
monster_pathchain = e;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!visible(self, e))
|
|
{
|
|
count4++;
|
|
|
|
if (checkpoint)
|
|
{
|
|
checkpoint->monster_hint_chain = e->monster_hint_chain;
|
|
e->monster_hint_chain = NULL;
|
|
e = checkpoint->monster_hint_chain;
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
/* use checkpoint as temp pointer */
|
|
checkpoint = e;
|
|
e = e->monster_hint_chain;
|
|
checkpoint->monster_hint_chain = NULL;
|
|
|
|
/* and clear it again */
|
|
|
|
checkpoint = NULL;
|
|
/* since we have yet to find a valid one (or else
|
|
checkpoint would be set) move the start of
|
|
monster_pathchain */
|
|
monster_pathchain = e;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
/* if it passes all the tests, it's a keeper */
|
|
count5++;
|
|
checkpoint = e;
|
|
e = e->monster_hint_chain;
|
|
}
|
|
|
|
/* at this point, we have a list of all of the eligible
|
|
hint nodes for the monster we now take them, figure out
|
|
what hint chains they're on, and traverse down those
|
|
chains, seeing whether any can see the player. first,
|
|
we figure out which hint chains we have represented
|
|
in monster_pathchain */
|
|
if (count5 == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (i = 0; i < num_hint_paths; i++)
|
|
{
|
|
hint_path_represented[i] = false;
|
|
}
|
|
|
|
e = monster_pathchain;
|
|
checkpoint = NULL;
|
|
|
|
while (e)
|
|
{
|
|
if ((e->hint_chain_id < 0) || (e->hint_chain_id > num_hint_paths))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
hint_path_represented[e->hint_chain_id] = true;
|
|
e = e->monster_hint_chain;
|
|
}
|
|
|
|
count2 = 0;
|
|
count4 = 0;
|
|
count5 = 0;
|
|
|
|
/* now, build the target_pathchain which contains all of
|
|
the hint_path nodes we need to check for validity
|
|
(within range, visibility) */
|
|
target_pathchain = NULL;
|
|
checkpoint = NULL;
|
|
|
|
for (i = 0; i < num_hint_paths; i++)
|
|
{
|
|
/* if this hint chain is represented in the
|
|
monster_hint_chain, add all of it's nodes
|
|
to the target_pathchain for validity checking */
|
|
if (hint_path_represented[i])
|
|
{
|
|
e = hint_path_start[i];
|
|
|
|
while (e)
|
|
{
|
|
if (target_pathchain)
|
|
{
|
|
checkpoint->target_hint_chain = e;
|
|
checkpoint = e;
|
|
}
|
|
else
|
|
{
|
|
target_pathchain = e;
|
|
checkpoint = e;
|
|
}
|
|
|
|
e = e->hint_chain;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* target_pathchain is a list of all of the hint_path nodes
|
|
we need to check for validity relative to the target */
|
|
e = target_pathchain;
|
|
checkpoint = NULL;
|
|
|
|
while (e)
|
|
{
|
|
r = realrange(self->enemy, e);
|
|
|
|
if (r > 512)
|
|
{
|
|
count2++;
|
|
|
|
if (checkpoint)
|
|
{
|
|
checkpoint->target_hint_chain = e->target_hint_chain;
|
|
e->target_hint_chain = NULL;
|
|
e = checkpoint->target_hint_chain;
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
/* use checkpoint as temp pointer */
|
|
checkpoint = e;
|
|
e = e->target_hint_chain;
|
|
checkpoint->target_hint_chain = NULL;
|
|
|
|
/* and clear it again */
|
|
checkpoint = NULL;
|
|
target_pathchain = e;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!visible(self->enemy, e))
|
|
{
|
|
count4++;
|
|
|
|
if (checkpoint)
|
|
{
|
|
checkpoint->target_hint_chain = e->target_hint_chain;
|
|
e->target_hint_chain = NULL;
|
|
e = checkpoint->target_hint_chain;
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
/* use checkpoint as temp pointer */
|
|
checkpoint = e;
|
|
e = e->target_hint_chain;
|
|
checkpoint->target_hint_chain = NULL;
|
|
|
|
/* and clear it again */
|
|
checkpoint = NULL;
|
|
target_pathchain = e;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
/* if it passes all the tests, it's a keeper */
|
|
count5++;
|
|
checkpoint = e;
|
|
e = e->target_hint_chain;
|
|
}
|
|
|
|
/* at this point we should have:
|
|
- monster_pathchain - a list of "monster valid" hint_path nodes linked
|
|
together by monster_hint_chain
|
|
- target_pathcain - a list of "target valid" hint_path nodes linked
|
|
together by target_hint_chain. these are filtered
|
|
such that only nodes which are on the same chain
|
|
as "monster valid" nodes
|
|
|
|
Now, we figure out which "monster valid" node we want to use. To do this, we
|
|
first off make sure we have some target nodes. If we don't, there are no
|
|
valid hint_path nodes for us to take. If we have some, we filter all of our
|
|
"monster valid" nodes by which ones have "target valid" nodes on them. Once
|
|
this filter is finished, we select the closest "monster valid" node, and go to it. */
|
|
|
|
if (count5 == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/* reuse the hint_chain_represented array, this time
|
|
to see which chains are represented by the target */
|
|
for (i = 0; i < num_hint_paths; i++)
|
|
{
|
|
hint_path_represented[i] = false;
|
|
}
|
|
|
|
e = target_pathchain;
|
|
checkpoint = NULL;
|
|
|
|
while (e)
|
|
{
|
|
if ((e->hint_chain_id < 0) || (e->hint_chain_id > num_hint_paths))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
hint_path_represented[e->hint_chain_id] = true;
|
|
e = e->target_hint_chain;
|
|
}
|
|
|
|
/* traverse the monster_pathchain - if the hint_node isn't represented
|
|
in the "target valid" chain list, remove it. if it is on the list,
|
|
check it for range from the monster. If the range is the closest, keep it */
|
|
closest = NULL;
|
|
e = monster_pathchain;
|
|
|
|
while (e)
|
|
{
|
|
if (!(hint_path_represented[e->hint_chain_id]))
|
|
{
|
|
checkpoint = e->monster_hint_chain;
|
|
e->monster_hint_chain = NULL;
|
|
e = checkpoint;
|
|
continue;
|
|
}
|
|
|
|
r = realrange(self, e);
|
|
|
|
if (r < closest_range)
|
|
{
|
|
closest = e;
|
|
}
|
|
|
|
e = e->monster_hint_chain;
|
|
}
|
|
|
|
if (!closest)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
start = closest;
|
|
|
|
/* now we know which one is the closest to the monster..
|
|
this is the one the monster will go to. we need to
|
|
finally determine what the DESTINATION node is for the
|
|
monster. walk down the hint_chain, and find the closest one
|
|
to the player */
|
|
closest = NULL;
|
|
closest_range = 10000000;
|
|
e = target_pathchain;
|
|
|
|
while (e)
|
|
{
|
|
if (start->hint_chain_id == e->hint_chain_id)
|
|
{
|
|
r = realrange(self, e);
|
|
|
|
if (r < closest_range)
|
|
{
|
|
closest = e;
|
|
}
|
|
}
|
|
|
|
e = e->target_hint_chain;
|
|
}
|
|
|
|
if (!closest)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
destination = closest;
|
|
|
|
self->monsterinfo.goal_hint = destination;
|
|
hintpath_go(self, start);
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
hint_path_touch(edict_t *self, edict_t *other, cplane_t *plane /* unused */,
|
|
csurface_t *surf /* unused */)
|
|
{
|
|
edict_t *e, *goal;
|
|
edict_t *next = NULL;
|
|
qboolean goalFound = false;
|
|
|
|
if (!self || !other)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* make sure we're the target of it's obsession */
|
|
if (other->movetarget == self)
|
|
{
|
|
goal = other->monsterinfo.goal_hint;
|
|
|
|
/* if the monster is where he wants to be */
|
|
if (goal == self)
|
|
{
|
|
hintpath_stop(other);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
/* if we aren't, figure out which way we want to go */
|
|
e = hint_path_start[self->hint_chain_id];
|
|
|
|
while (e)
|
|
{
|
|
/* if we get up to ourselves on the hint chain, we're going down it */
|
|
if (e == self)
|
|
{
|
|
next = e->hint_chain;
|
|
break;
|
|
}
|
|
|
|
if (e == goal)
|
|
{
|
|
goalFound = true;
|
|
}
|
|
|
|
/* if we get to where the next link on the chain is this hint_path
|
|
and have found the goal on the way we're going upstream, so
|
|
remember who the previous link is */
|
|
if ((e->hint_chain == self) && goalFound)
|
|
{
|
|
next = e;
|
|
break;
|
|
}
|
|
|
|
e = e->hint_chain;
|
|
}
|
|
}
|
|
|
|
/* if we couldn't find it, have the monster go back to normal hunting. */
|
|
if (!next)
|
|
{
|
|
hintpath_stop(other);
|
|
return;
|
|
}
|
|
|
|
/* set the last_hint entry to this hint_path,
|
|
and send him on his way */
|
|
hintpath_go(other, next);
|
|
|
|
/* have the monster freeze if the hint path we
|
|
just touched has a wait time on it, for e
|
|
xample, when riding a plat. */
|
|
if (self->wait)
|
|
{
|
|
other->nextthink = level.time + self->wait;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* QUAKED hint_path (.5 .3 0) (-8 -8 -8) (8 8 8) END
|
|
*
|
|
* Target: next hint path
|
|
*
|
|
* END - set this flag on the endpoints of each hintpath.
|
|
* "wait" - set this if you want the monster to freeze when they touch this hintpath
|
|
*/
|
|
void
|
|
SP_hint_path(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (deathmatch->value)
|
|
{
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
if (!self->targetname && !self->target)
|
|
{
|
|
gi.dprintf("unlinked hint_path at %s\n", vtos(self->s.origin));
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
self->solid = SOLID_TRIGGER;
|
|
self->touch = hint_path_touch;
|
|
VectorSet(self->mins, -8, -8, -8);
|
|
VectorSet(self->maxs, 8, 8, 8);
|
|
self->svflags |= SVF_NOCLIENT;
|
|
gi.linkentity(self);
|
|
}
|
|
|
|
void
|
|
InitHintPaths(void)
|
|
{
|
|
edict_t *e, *current;
|
|
int field, i, count2;
|
|
|
|
hint_paths_present = 0;
|
|
|
|
/* check all the hint_paths. */
|
|
field = FOFS(classname);
|
|
e = G_Find(NULL, field, "hint_path");
|
|
|
|
if (e)
|
|
{
|
|
hint_paths_present = 1;
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
|
|
memset(hint_path_start, 0, MAX_HINT_CHAINS * sizeof(edict_t *));
|
|
num_hint_paths = 0;
|
|
|
|
while (e)
|
|
{
|
|
if (e->spawnflags & HINT_ENDPOINT)
|
|
{
|
|
if (e->target) /* start point */
|
|
{
|
|
if (e->targetname) /* this is a bad end, ignore it */
|
|
{
|
|
gi.dprintf("Hint path at %s marked as endpoint with both target (%s) and targetname (%s)\n",
|
|
vtos(e->s.origin), e->target, e->targetname);
|
|
}
|
|
else
|
|
{
|
|
if (num_hint_paths >= MAX_HINT_CHAINS)
|
|
{
|
|
break;
|
|
}
|
|
|
|
hint_path_start[num_hint_paths++] = e;
|
|
}
|
|
}
|
|
}
|
|
|
|
e = G_Find(e, field, "hint_path");
|
|
}
|
|
|
|
field = FOFS(targetname);
|
|
|
|
for (i = 0; i < num_hint_paths; i++)
|
|
{
|
|
count2 = 1;
|
|
current = hint_path_start[i];
|
|
current->hint_chain_id = i;
|
|
e = G_Find(NULL, field, current->target);
|
|
|
|
if (G_Find(e, field, current->target))
|
|
{
|
|
gi.dprintf("\nForked hint path at %s detected for chain %d, target %s\n",
|
|
vtos(current->s.origin), num_hint_paths, current->target);
|
|
hint_path_start[i]->hint_chain = NULL;
|
|
continue;
|
|
}
|
|
|
|
while (e)
|
|
{
|
|
if (e->hint_chain)
|
|
{
|
|
gi.dprintf("\nCircular hint path at %s detected for chain %d, targetname %s\n",
|
|
vtos(e->s.origin), num_hint_paths, e->targetname);
|
|
hint_path_start[i]->hint_chain = NULL;
|
|
break;
|
|
}
|
|
|
|
count2++;
|
|
current->hint_chain = e;
|
|
current = e;
|
|
current->hint_chain_id = i;
|
|
|
|
if (!current->target)
|
|
{
|
|
break;
|
|
}
|
|
|
|
e = G_Find(NULL, field, current->target);
|
|
|
|
if (G_Find(e, field, current->target))
|
|
{
|
|
gi.dprintf("\nForked hint path at %s detected for chain %d, target %s\n",
|
|
vtos(current->s.origin), num_hint_paths, current->target);
|
|
hint_path_start[i]->hint_chain = NULL;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
qboolean
|
|
inback(edict_t *self, edict_t *other)
|
|
{
|
|
vec3_t vec;
|
|
float dot;
|
|
vec3_t forward;
|
|
|
|
if (!self || !other)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
AngleVectors(self->s.angles, forward, NULL, NULL);
|
|
VectorSubtract(other->s.origin, self->s.origin, vec);
|
|
VectorNormalize(vec);
|
|
dot = DotProduct(vec, forward);
|
|
|
|
if (dot < -0.3)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
float
|
|
realrange(edict_t *self, edict_t *other)
|
|
{
|
|
vec3_t dir;
|
|
|
|
if (!self || !other)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
VectorSubtract(self->s.origin, other->s.origin, dir);
|
|
|
|
return VectorLength(dir);
|
|
}
|
|
|
|
qboolean
|
|
face_wall(edict_t *self)
|
|
{
|
|
vec3_t pt;
|
|
vec3_t forward;
|
|
vec3_t ang;
|
|
trace_t tr;
|
|
|
|
if (!self)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
AngleVectors(self->s.angles, forward, NULL, NULL);
|
|
VectorMA(self->s.origin, 64, forward, pt);
|
|
tr = gi.trace(self->s.origin, vec3_origin, vec3_origin,
|
|
pt, self, MASK_MONSTERSOLID);
|
|
|
|
if ((tr.fraction < 1) && !tr.allsolid && !tr.startsolid)
|
|
{
|
|
vectoangles2(tr.plane.normal, ang);
|
|
self->ideal_yaw = ang[YAW] + 180;
|
|
|
|
if (self->ideal_yaw > 360)
|
|
{
|
|
self->ideal_yaw -= 360;
|
|
}
|
|
|
|
M_ChangeYaw(self);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
badarea_touch(edict_t *ent, edict_t *other, cplane_t *plane, csurface_t *surf)
|
|
{
|
|
}
|
|
|
|
edict_t *
|
|
SpawnBadArea(vec3_t mins, vec3_t maxs, float lifespan, edict_t *owner)
|
|
{
|
|
edict_t *badarea;
|
|
vec3_t origin;
|
|
|
|
if (!owner)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
VectorAdd(mins, maxs, origin);
|
|
VectorScale(origin, 0.5, origin);
|
|
|
|
VectorSubtract(maxs, origin, maxs);
|
|
VectorSubtract(mins, origin, mins);
|
|
|
|
badarea = G_Spawn();
|
|
VectorCopy(origin, badarea->s.origin);
|
|
VectorCopy(maxs, badarea->maxs);
|
|
VectorCopy(mins, badarea->mins);
|
|
badarea->touch = badarea_touch;
|
|
badarea->movetype = MOVETYPE_NONE;
|
|
badarea->solid = SOLID_TRIGGER;
|
|
badarea->classname = "bad_area";
|
|
gi.linkentity(badarea);
|
|
|
|
if (lifespan)
|
|
{
|
|
badarea->think = G_FreeEdict;
|
|
badarea->nextthink = level.time + lifespan;
|
|
}
|
|
|
|
badarea->owner = owner;
|
|
|
|
return badarea;
|
|
}
|
|
|
|
edict_t *
|
|
CheckForBadArea(edict_t *ent)
|
|
{
|
|
int i, num;
|
|
edict_t *touch[MAX_EDICTS], *hit;
|
|
vec3_t mins, maxs;
|
|
|
|
if (!ent)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
VectorAdd(ent->s.origin, ent->mins, mins);
|
|
VectorAdd(ent->s.origin, ent->maxs, maxs);
|
|
|
|
num = gi.BoxEdicts(mins, maxs, touch, MAX_EDICTS, AREA_TRIGGERS);
|
|
|
|
/* be careful, it is possible to have an entity in this
|
|
list removed before we get to it (killtriggered) */
|
|
for (i = 0; i < num; i++)
|
|
{
|
|
hit = touch[i];
|
|
|
|
if (!hit->inuse)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (hit->touch == badarea_touch)
|
|
{
|
|
return hit;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
qboolean
|
|
MarkTeslaArea(edict_t *self, edict_t *tesla)
|
|
{
|
|
vec3_t mins, maxs;
|
|
edict_t *e;
|
|
edict_t *tail;
|
|
edict_t *area;
|
|
|
|
if (!tesla || !self)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
area = NULL;
|
|
|
|
/* make sure this tesla doesn't have a bad area around it already... */
|
|
e = tesla->teamchain;
|
|
tail = tesla;
|
|
|
|
while (e)
|
|
{
|
|
tail = tail->teamchain;
|
|
|
|
if (!strcmp(e->classname, "bad_area"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
e = e->teamchain;
|
|
}
|
|
|
|
/* see if we can grab the trigger directly */
|
|
if (tesla->teamchain && tesla->teamchain->inuse)
|
|
{
|
|
edict_t *trigger;
|
|
|
|
trigger = tesla->teamchain;
|
|
|
|
VectorCopy(trigger->absmin, mins);
|
|
VectorCopy(trigger->absmax, maxs);
|
|
|
|
if (tesla->air_finished)
|
|
{
|
|
area = SpawnBadArea(mins, maxs, tesla->air_finished, tesla);
|
|
}
|
|
else
|
|
{
|
|
area = SpawnBadArea(mins, maxs, tesla->nextthink, tesla);
|
|
}
|
|
}
|
|
/* otherwise we just guess at how long it'll last. */
|
|
else
|
|
{
|
|
VectorSet(mins, -TESLA_DAMAGE_RADIUS, -TESLA_DAMAGE_RADIUS,
|
|
tesla->mins[2]);
|
|
VectorSet(maxs, TESLA_DAMAGE_RADIUS, TESLA_DAMAGE_RADIUS, TESLA_DAMAGE_RADIUS);
|
|
|
|
area = SpawnBadArea(mins, maxs, 30, tesla);
|
|
}
|
|
|
|
/* if we spawned a bad area, then link it to the tesla */
|
|
if (area)
|
|
{
|
|
tail->teamchain = area;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
PredictAim(edict_t *target, vec3_t start, float bolt_speed, qboolean eye_height,
|
|
float offset, vec3_t aimdir, vec3_t aimpoint)
|
|
{
|
|
vec3_t dir, vec;
|
|
float dist, time;
|
|
|
|
if (!target || !target->inuse)
|
|
{
|
|
VectorCopy(vec3_origin, aimdir);
|
|
return;
|
|
}
|
|
|
|
VectorSubtract(target->s.origin, start, dir);
|
|
|
|
if (eye_height)
|
|
{
|
|
dir[2] += target->viewheight;
|
|
}
|
|
|
|
dist = VectorLength(dir);
|
|
time = dist / bolt_speed;
|
|
|
|
VectorMA(target->s.origin, time - offset, target->velocity, vec);
|
|
|
|
if (eye_height)
|
|
{
|
|
vec[2] += target->viewheight;
|
|
}
|
|
|
|
if (aimdir)
|
|
{
|
|
VectorSubtract(vec, start, aimdir);
|
|
VectorNormalize(aimdir);
|
|
}
|
|
|
|
if (aimpoint)
|
|
{
|
|
VectorCopy(vec, aimpoint);
|
|
}
|
|
}
|
|
|
|
qboolean
|
|
below(edict_t *self, edict_t *other)
|
|
{
|
|
vec3_t vec;
|
|
float dot;
|
|
vec3_t down;
|
|
|
|
if (!self || !other)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
VectorSubtract(other->s.origin, self->s.origin, vec);
|
|
VectorNormalize(vec);
|
|
VectorSet(down, 0, 0, -1);
|
|
dot = DotProduct(vec, down);
|
|
|
|
if (dot > 0.95) /* 18 degree arc below */
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
drawbbox(edict_t *self)
|
|
{
|
|
int lines[4][3] = {
|
|
{1, 2, 4},
|
|
{1, 2, 7},
|
|
{1, 4, 5},
|
|
{2, 4, 7}
|
|
};
|
|
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int starts[4] = {0, 3, 5, 6};
|
|
|
|
vec3_t pt[8];
|
|
int i, j, k;
|
|
vec3_t coords[2];
|
|
vec3_t newbox;
|
|
vec3_t f, r, u, dir;
|
|
|
|
VectorCopy(self->absmin, coords[0]);
|
|
VectorCopy(self->absmax, coords[1]);
|
|
|
|
for (i = 0; i <= 1; i++)
|
|
{
|
|
for (j = 0; j <= 1; j++)
|
|
{
|
|
for (k = 0; k <= 1; k++)
|
|
{
|
|
pt[4 * i + 2 * j + k][0] = coords[i][0];
|
|
pt[4 * i + 2 * j + k][1] = coords[j][1];
|
|
pt[4 * i + 2 * j + k][2] = coords[k][2];
|
|
}
|
|
}
|
|
}
|
|
|
|
for (i = 0; i <= 3; i++)
|
|
{
|
|
for (j = 0; j <= 2; j++)
|
|
{
|
|
gi.WriteByte(svc_temp_entity);
|
|
gi.WriteByte(TE_DEBUGTRAIL);
|
|
gi.WritePosition(pt[starts[i]]);
|
|
gi.WritePosition(pt[lines[i][j]]);
|
|
gi.multicast(pt[starts[i]], MULTICAST_ALL);
|
|
}
|
|
}
|
|
|
|
vectoangles2(self->s.angles, dir);
|
|
AngleVectors(dir, f, r, u);
|
|
|
|
VectorMA(self->s.origin, 50, f, newbox);
|
|
gi.WriteByte(svc_temp_entity);
|
|
gi.WriteByte(TE_DEBUGTRAIL);
|
|
gi.WritePosition(self->s.origin);
|
|
gi.WritePosition(newbox);
|
|
gi.multicast(self->s.origin, MULTICAST_PVS);
|
|
VectorClear(newbox);
|
|
|
|
VectorMA(self->s.origin, 50, r, newbox);
|
|
gi.WriteByte(svc_temp_entity);
|
|
gi.WriteByte(TE_DEBUGTRAIL);
|
|
gi.WritePosition(self->s.origin);
|
|
gi.WritePosition(newbox);
|
|
gi.multicast(self->s.origin, MULTICAST_PVS);
|
|
VectorClear(newbox);
|
|
|
|
VectorMA(self->s.origin, 50, u, newbox);
|
|
gi.WriteByte(svc_temp_entity);
|
|
gi.WriteByte(TE_DEBUGTRAIL);
|
|
gi.WritePosition(self->s.origin);
|
|
gi.WritePosition(newbox);
|
|
gi.multicast(self->s.origin, MULTICAST_PVS);
|
|
VectorClear(newbox);
|
|
}
|
|
|
|
void
|
|
M_MonsterDodge(edict_t *self, edict_t *attacker, float eta, trace_t *tr)
|
|
{
|
|
float r = random();
|
|
float height;
|
|
qboolean ducker = false, dodger = false;
|
|
|
|
if (!self || !attacker || !tr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* this needs to be here since this can be
|
|
called after the monster has "died" */
|
|
if (self->health < 1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ((self->monsterinfo.duck) && (self->monsterinfo.unduck))
|
|
{
|
|
ducker = true;
|
|
}
|
|
|
|
if ((self->monsterinfo.sidestep) &&
|
|
!(self->monsterinfo.aiflags & AI_STAND_GROUND))
|
|
{
|
|
dodger = true;
|
|
}
|
|
|
|
if ((!ducker) && (!dodger))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!self->enemy)
|
|
{
|
|
self->enemy = attacker;
|
|
FoundTarget(self);
|
|
}
|
|
|
|
if ((eta < 0.1) || (eta > 5))
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* skill level determination.. */
|
|
if (r > (0.25 * ((skill->value) + 1)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ducker)
|
|
{
|
|
height = self->absmax[2] - 32 - 1; /* the -1 is because the absmax is s.origin + maxs + 1 */
|
|
|
|
if ((!dodger) && ((tr->endpos[2] <= height) || (self->monsterinfo.aiflags & AI_DUCKED)))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
height = self->absmax[2];
|
|
}
|
|
|
|
if (dodger)
|
|
{
|
|
/* if we're already dodging, just finish
|
|
the sequence, i.e. don't do anything else */
|
|
if (self->monsterinfo.aiflags & AI_DODGING)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* if we're ducking already, or the shot is at our knees */
|
|
if ((tr->endpos[2] <= height) || (self->monsterinfo.aiflags & AI_DUCKED))
|
|
{
|
|
vec3_t right, diff;
|
|
|
|
AngleVectors(self->s.angles, NULL, right, NULL);
|
|
VectorSubtract(tr->endpos, self->s.origin, diff);
|
|
|
|
if (DotProduct(right, diff) < 0)
|
|
{
|
|
self->monsterinfo.lefty = 0;
|
|
}
|
|
else
|
|
{
|
|
self->monsterinfo.lefty = 1;
|
|
}
|
|
|
|
/* if we are currently ducked, unduck */
|
|
if ((ducker) && (self->monsterinfo.aiflags & AI_DUCKED))
|
|
{
|
|
self->monsterinfo.unduck(self);
|
|
}
|
|
|
|
self->monsterinfo.aiflags |= AI_DODGING;
|
|
self->monsterinfo.attack_state = AS_SLIDING;
|
|
|
|
/* call the monster specific code here */
|
|
self->monsterinfo.sidestep(self);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (ducker)
|
|
{
|
|
if (self->monsterinfo.next_duck_time > level.time)
|
|
{
|
|
return;
|
|
}
|
|
|
|
monster_done_dodge(self);
|
|
|
|
/* set this prematurely; it doesn't hurt, and prevents extra iterations */
|
|
self->monsterinfo.aiflags |= AI_DUCKED;
|
|
self->monsterinfo.duck(self, eta);
|
|
}
|
|
}
|
|
|
|
void
|
|
monster_duck_down(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->monsterinfo.aiflags |= AI_DUCKED;
|
|
self->maxs[2] = self->monsterinfo.base_height - 32;
|
|
self->takedamage = DAMAGE_YES;
|
|
|
|
if (self->monsterinfo.duck_wait_time < level.time)
|
|
{
|
|
self->monsterinfo.duck_wait_time = level.time + 1;
|
|
}
|
|
|
|
gi.linkentity(self);
|
|
}
|
|
|
|
void
|
|
monster_duck_hold(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (level.time >= self->monsterinfo.duck_wait_time)
|
|
{
|
|
self->monsterinfo.aiflags &= ~AI_HOLD_FRAME;
|
|
}
|
|
else
|
|
{
|
|
self->monsterinfo.aiflags |= AI_HOLD_FRAME;
|
|
}
|
|
}
|
|
|
|
void
|
|
monster_duck_up(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->monsterinfo.aiflags &= ~AI_DUCKED;
|
|
self->maxs[2] = self->monsterinfo.base_height;
|
|
self->takedamage = DAMAGE_AIM;
|
|
self->monsterinfo.next_duck_time = level.time + DUCK_INTERVAL;
|
|
gi.linkentity(self);
|
|
}
|
|
|
|
qboolean
|
|
has_valid_enemy(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!self->enemy)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!self->enemy->inuse)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (self->enemy->health < 1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
TargetTesla(edict_t *self, edict_t *tesla)
|
|
{
|
|
if ((!self) || (!tesla))
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* medic bails on healing things */
|
|
if (self->monsterinfo.aiflags & AI_MEDIC)
|
|
{
|
|
if (self->enemy)
|
|
{
|
|
cleanupHealTarget(self->enemy);
|
|
}
|
|
|
|
self->monsterinfo.aiflags &= ~AI_MEDIC;
|
|
}
|
|
|
|
/* store the player enemy in case we lose track of him. */
|
|
if (self->enemy && self->enemy->client)
|
|
{
|
|
self->monsterinfo.last_player_enemy = self->enemy;
|
|
}
|
|
|
|
if (self->enemy != tesla)
|
|
{
|
|
self->oldenemy = self->enemy;
|
|
self->enemy = tesla;
|
|
|
|
if (self->monsterinfo.attack)
|
|
{
|
|
if (self->health <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->monsterinfo.attack(self);
|
|
}
|
|
else
|
|
{
|
|
FoundTarget(self);
|
|
}
|
|
}
|
|
}
|
|
|
|
edict_t *
|
|
PickCoopTarget(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
/* no more than 4 players in coop, so.. */
|
|
edict_t *targets[4];
|
|
int num_targets = 0, targetID;
|
|
edict_t *ent;
|
|
int player;
|
|
|
|
/* if we're not in coop, this is a noop */
|
|
if (!coop || !coop->value)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
memset(targets, 0, 4 * sizeof(edict_t *));
|
|
|
|
for (player = 1; player <= game.maxclients; player++)
|
|
{
|
|
ent = &g_edicts[player];
|
|
|
|
if (!ent->inuse)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!ent->client)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (visible(self, ent))
|
|
{
|
|
targets[num_targets++] = ent;
|
|
}
|
|
}
|
|
|
|
if (!num_targets)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
/* get a number from 0 to (num_targets-1) */
|
|
targetID = (random() * (float)num_targets);
|
|
|
|
/* just in case we got a 1.0 from random */
|
|
if (targetID == num_targets)
|
|
{
|
|
targetID--;
|
|
}
|
|
|
|
return targets[targetID];
|
|
}
|
|
|
|
int
|
|
CountPlayers(void)
|
|
{
|
|
edict_t *ent;
|
|
int count = 0;
|
|
int player;
|
|
|
|
/* if we're not in coop, this is a noop */
|
|
if (!coop || !coop->value)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
for (player = 1; player <= game.maxclients; player++)
|
|
{
|
|
ent = &g_edicts[player];
|
|
|
|
if (!ent->inuse)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!ent->client)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
void
|
|
monster_jump_start(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->timestamp = level.time;
|
|
}
|
|
|
|
qboolean
|
|
monster_jump_finished(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ((level.time - self->timestamp) > 3)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|