2611 lines
70 KiB
C
2611 lines
70 KiB
C
// ****************************************************************************
|
|
// mg_guide
|
|
//
|
|
// High level monster guide information using BUOYAH! Navigation System(tm)
|
|
//
|
|
// Heretic II
|
|
// Copyright 1998 Raven Software
|
|
//
|
|
// Mike Gummelt & Josh Weier
|
|
//
|
|
// FIXME:
|
|
//
|
|
// 1) Way to send monsters to a buoy out of water/lava if they are drowning
|
|
// or burning.
|
|
//
|
|
// 2) When you get to a buoy, do a trace to the next if it's blocked, you
|
|
// need to find another way...
|
|
//
|
|
// 3) Way to handle lots of monsters gathered around the same buoy... the one
|
|
// that is at it can't get out, the others cant get to it, they just crowd it.
|
|
//
|
|
// ****************************************************************************
|
|
|
|
#include "g_local.h"
|
|
#include "Angles.h"
|
|
#include "Utilities.h"
|
|
#include "random.h"
|
|
#include "vector.h"
|
|
#include "buoy.h"
|
|
#include "g_monster.h"
|
|
#include "m_stats.h"
|
|
#include "fx.h"
|
|
#include "mg_guide.h"
|
|
|
|
#define BUOY_SEARCH_TIME 10//10 seconds between choosing a buoy and getting there
|
|
|
|
#define BUOY_SEARCH_PASSES 6//sfs--number of passes through buoy list when searching
|
|
// (only accept buoys closer than 1/BUOY_SEARCH_PASSES * MAX_BUOY_DIST
|
|
// for first pass, etc.). if a buoy is found after a pass,
|
|
// we know we've got the closest buoy, and further passes can
|
|
// be skipped.
|
|
|
|
buoy_t *find_next_buoy(edict_t *self, int sb_id, int fb_id);
|
|
qboolean Clear_Path(edict_t *self, vec3_t end);
|
|
void MG_AddBuoyEffect(edict_t *self, qboolean endbuoy);
|
|
qboolean MG_MakeConnection(edict_t *self, buoy_t *first_buoy, qboolean skipjump);
|
|
void assassinPrepareTeleportDest(edict_t *self, vec3_t spot);
|
|
qboolean MG_CheckClearPathToSpot(edict_t *self, vec3_t spot);
|
|
|
|
/*
|
|
*
|
|
*
|
|
* Helper functions
|
|
*
|
|
*
|
|
*/
|
|
|
|
qboolean MG_ReachedBuoy (edict_t *self, vec3_t pspot)
|
|
{
|
|
float len, radius, z_diff, center;
|
|
vec3_t spot;
|
|
|
|
if(!pspot)
|
|
VectorCopy(self->monsterinfo.nav_goal, spot);
|
|
else
|
|
VectorCopy(pspot, spot);
|
|
|
|
center = (self->absmin[2] + self->absmax[2]) * 0.5;
|
|
z_diff = Q_fabs(spot[2] - center);
|
|
if(z_diff > self->size[2])
|
|
return false;
|
|
|
|
len = vhlen(spot, self->s.origin);
|
|
|
|
if(self->maxs[0]>16)
|
|
radius = 24 + self->maxs[0];
|
|
else
|
|
radius = 40;//24 + 16
|
|
|
|
if (len < (24+radius))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
qboolean Clear_Path(edict_t *self, vec3_t end)
|
|
{
|
|
trace_t trace;
|
|
vec3_t mins, maxs;
|
|
|
|
if(DEACTIVATE_BUOYS)
|
|
return false;
|
|
|
|
VectorCopy(self->mins, mins);
|
|
VectorCopy(self->maxs, maxs);
|
|
|
|
//sfs--i guess it's an ob/com thing, since msdev can probably be relied on to
|
|
// optimize it during compile, but mults are faster than divides.
|
|
|
|
mins[0] *= 0.5;
|
|
mins[1] *= 0.5;
|
|
mins[2] *= 0.5;
|
|
|
|
maxs[0] *= 0.5;
|
|
maxs[1] *= 0.5;
|
|
maxs[2] *= 0.5;
|
|
|
|
if(self->mins[2] + 18 > mins[2])//need to account for stepheight
|
|
{//took off less than 18
|
|
mins[2] = self->mins[2] + 18;
|
|
if(mins[2] > maxs[2])
|
|
maxs[2] = mins[2];
|
|
}
|
|
|
|
//quicker way to discard points that are very not in a clear path
|
|
if (!gi.inPVS(self->s.origin, end))
|
|
return false;
|
|
|
|
gi.trace(self->s.origin, mins, maxs, end, self, MASK_SOLID,&trace);
|
|
|
|
if ((trace.fraction < 1) && (trace.ent != self->enemy))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
=============
|
|
clear_visible_pos
|
|
|
|
returns 1 if the spot is visible, but not through transparencies
|
|
=============
|
|
*/
|
|
qboolean clear_visible_pos (edict_t *self, vec3_t spot2)
|
|
{
|
|
vec3_t spot1;
|
|
trace_t trace;
|
|
|
|
if (!self)
|
|
return false;
|
|
|
|
VectorCopy (self->s.origin, spot1);
|
|
spot1[2] += self->viewheight;
|
|
if(self->classID == CID_TBEAST)
|
|
{
|
|
vec3_t forward;
|
|
|
|
AngleVectors(self->s.angles, forward, NULL, NULL);
|
|
VectorMA(spot1, self->maxs[0], forward, spot1);
|
|
}
|
|
|
|
//quicker way to discard points that are very not visible
|
|
if (!gi.inPVS(self->s.origin, spot2))
|
|
return false;
|
|
|
|
gi.trace (spot1, vec3_origin, vec3_origin, spot2, self, MASK_SOLID,&trace);
|
|
|
|
if (trace.fraction == 1.0)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
int MG_SetFirstBuoy(edict_t *self)
|
|
{
|
|
buoy_t *found_buoy = NULL;
|
|
buoy_t *bestbuoy = NULL;
|
|
qboolean vis;
|
|
vec3_t vec;
|
|
float bestdist, len;
|
|
int i = 0;
|
|
int tracecount=0;
|
|
float buoy_passes = BUOY_SEARCH_PASSES;
|
|
|
|
float search_pass_interval = MAX_BUOY_DIST / buoy_passes;
|
|
float j;
|
|
|
|
bestdist = 9999999;
|
|
|
|
if(!self->client)
|
|
{
|
|
if(!(self->monsterinfo.aiflags & AI_USING_BUOYS))
|
|
{
|
|
self->ai_mood = AI_MOOD_PURSUE;
|
|
return NULL_BUOY;
|
|
}
|
|
}
|
|
|
|
if(DEACTIVATE_BUOYS)
|
|
return NULL_BUOY;
|
|
//first, precalc all distances
|
|
for(i = 0; i <= level.active_buoys; i++)
|
|
{
|
|
found_buoy = &level.buoy_list[i];
|
|
VectorSubtract(self->s.origin, found_buoy->origin, vec);
|
|
found_buoy->temp_dist = VectorLength(vec);
|
|
}
|
|
|
|
//now, do all the passes, going from closest to farthest
|
|
for(j = 0; (j < buoy_passes)&&(!bestbuoy); j++)
|
|
{
|
|
for(i = 0; i <= level.active_buoys; i++)
|
|
{
|
|
vis = false;
|
|
|
|
found_buoy = &level.buoy_list[i];
|
|
len = found_buoy->temp_dist;
|
|
|
|
//only consider buoys in the current interval--closer ones have already been
|
|
//checked, and we'll save farther ones for later
|
|
if (len < bestdist && len > search_pass_interval*j && len < search_pass_interval*(j+1.0))
|
|
{
|
|
tracecount++;
|
|
vis = Clear_Path(self, found_buoy->origin);
|
|
}
|
|
|
|
if (vis)
|
|
{
|
|
bestdist = len;
|
|
bestbuoy = found_buoy;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!bestbuoy)
|
|
return NULL_BUOY;
|
|
|
|
if(!self->client)
|
|
{
|
|
self->lastbuoy = NULL_BUOY;
|
|
self->buoy_index = bestbuoy->id;
|
|
VectorCopy(bestbuoy->origin, self->monsterinfo.nav_goal);
|
|
}
|
|
|
|
return bestbuoy->id;
|
|
}
|
|
|
|
qboolean MG_GoToRandomBuoy(edict_t *self)
|
|
{
|
|
buoy_t *found_buoy;
|
|
qboolean searching;
|
|
qboolean dead_end = false;
|
|
int i, nextbranch, j;
|
|
int branches_checked;
|
|
qboolean branch_checked[MAX_BUOY_BRANCHES];
|
|
int last_buoy = NULL_BUOY;
|
|
|
|
if(MG_SetFirstBuoy(self) == NULL_BUOY)
|
|
return false;
|
|
|
|
found_buoy = &level.buoy_list[self->buoy_index];
|
|
for(i = 0; i < self->mintel; i++)
|
|
{
|
|
searching = true;
|
|
for(j = 0; j<MAX_BUOY_BRANCHES; j++)
|
|
branch_checked[j] = false;
|
|
|
|
while(searching)
|
|
{
|
|
nextbranch = irand(0, 2);
|
|
branch_checked[nextbranch] = true;
|
|
|
|
branches_checked = false;
|
|
for(j = 0; j<MAX_BUOY_BRANCHES; j++)
|
|
{
|
|
if(branch_checked[j])
|
|
{
|
|
branches_checked++;
|
|
}
|
|
}
|
|
|
|
if(found_buoy->nextbuoy[nextbranch] > NULL_BUOY &&
|
|
found_buoy->nextbuoy[nextbranch] != self->buoy_index &&
|
|
(found_buoy->nextbuoy[nextbranch] != self->lastbuoy || branches_checked >= MAX_BUOY_BRANCHES)&&
|
|
(found_buoy->nextbuoy[nextbranch] != last_buoy || branches_checked >= MAX_BUOY_BRANCHES))
|
|
{//nextbuoy off this one is not null, not my start buoy, not my last buoy, and not last buoy found
|
|
last_buoy = found_buoy->id;
|
|
found_buoy = &level.buoy_list[found_buoy->nextbuoy[nextbranch]];
|
|
searching = false;
|
|
}
|
|
else if (branches_checked >= MAX_BUOY_BRANCHES)
|
|
{//a dead end, checked all 3 branches
|
|
if(i < 3)//can't run away far enough
|
|
return false;
|
|
searching = false;
|
|
dead_end = true;
|
|
}
|
|
}
|
|
if(dead_end)
|
|
break;
|
|
}
|
|
|
|
if(self->ai_mood == AI_MOOD_FLEE)
|
|
self->ai_mood_flags|=AI_MOOD_FLAG_IGNORE_ENEMY;
|
|
else
|
|
self->ai_mood = AI_MOOD_NAVIGATE;//wander?
|
|
|
|
self->ai_mood_flags &= ~AI_MOOD_FLAG_DUMB_FLEE;
|
|
self->ai_mood_flags|=AI_MOOD_FLAG_FORCED_BUOY;
|
|
self->forced_buoy = found_buoy->id;
|
|
|
|
QPostMessage(self, MSG_RUN,PRI_DIRECTIVE, NULL);
|
|
|
|
MG_RemoveBuoyEffects(self);
|
|
MG_MakeConnection(self, NULL, false);
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
MG_AssignMonsterNextBuoy(edict_t *self, buoy_t *startbuoy, buoy_t *endbuoy)
|
|
|
|
Actually assigns the bstartbuoy as the monster's buoy
|
|
*/
|
|
|
|
void MG_AssignMonsterNextBuoy(edict_t *self, buoy_t *startbuoy, buoy_t *endbuoy)
|
|
{
|
|
edict_t *showme;
|
|
|
|
VectorCopy(startbuoy->origin, self->monsterinfo.nav_goal);
|
|
if(self->buoy_index!=NULL_BUOY)
|
|
self->lastbuoy = self->buoy_index;
|
|
else
|
|
self->lastbuoy = NULL_BUOY;
|
|
self->buoy_index = startbuoy->id;
|
|
self->last_buoy_time = level.time;
|
|
if(BUOY_DEBUG>1)
|
|
{
|
|
showme = G_Find(NULL, FOFS(targetname), startbuoy->targetname);
|
|
if(showme)
|
|
{
|
|
self->nextbuoy[0] = showme;
|
|
MG_AddBuoyEffect(self, false);
|
|
}
|
|
|
|
if(endbuoy)
|
|
{
|
|
showme = G_Find(NULL, FOFS(targetname), endbuoy->targetname);
|
|
if(showme)
|
|
{
|
|
self->nextbuoy[1] = showme;
|
|
MG_AddBuoyEffect(self, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
========================
|
|
|
|
MG_ValidBestBuoyForEnt
|
|
|
|
Just see if this entity and this buoy are ok to be associated (clear path, etc.)
|
|
|
|
========================
|
|
*/
|
|
qboolean MG_ValidBestBuoyForEnt (edict_t *ent, buoy_t *test_buoy)
|
|
{
|
|
vec3_t v;
|
|
float dist;
|
|
|
|
VectorSubtract(ent->s.origin, test_buoy->origin, v);
|
|
dist = VectorLengthSquared(v);
|
|
if(dist > 250000)//500 squared
|
|
return false;//to damn far!
|
|
|
|
return MG_CheckClearPathToSpot(ent, test_buoy->origin);
|
|
// return Clear_Path(ent, test_buoy->origin);
|
|
}
|
|
|
|
/*
|
|
========================
|
|
|
|
MG_ResolveBuoyConnection
|
|
|
|
Actually makes the connection between two buoys
|
|
|
|
========================
|
|
*/
|
|
qboolean MG_ResolveBuoyConnection(edict_t *self, buoy_t *bestbuoy, buoy_t *e_bestbuoy, vec3_t goalpos, qboolean dont_use_last, qboolean skipjump)
|
|
{//When called directly, this does not and should not set player_buoy
|
|
buoy_t *found_buoy = NULL;
|
|
buoy_t *dest = NULL;
|
|
vec3_t vec;
|
|
float len, e_len;
|
|
edict_t *showme = NULL;
|
|
|
|
//FIXME: Allow assassins to take any buoy even if can;t make a connection, since they can teleport
|
|
//- Basically, pick the player's buoy and randomly pick one off of it or even that one...?
|
|
|
|
if(self->lastbuoy == e_bestbuoy->id)
|
|
{
|
|
if(!MG_ReachedBuoy(self, e_bestbuoy->origin) && !clear_visible_pos(self, goalpos))
|
|
{
|
|
self->lastbuoy = NULL_BUOY;
|
|
dont_use_last = false;
|
|
}
|
|
}
|
|
|
|
if (bestbuoy->modflags&BUOY_JUMP && bestbuoy->jump_target_id == e_bestbuoy->id)
|
|
{
|
|
if(skipjump)
|
|
dest = e_bestbuoy;
|
|
else
|
|
dest = bestbuoy;
|
|
|
|
self->monsterinfo.searchType = SEARCH_BUOY;
|
|
|
|
if(self->ai_mood != AI_MOOD_FLEE)
|
|
self->ai_mood = AI_MOOD_NAVIGATE;
|
|
|
|
VectorCopy(dest->origin, self->monsterinfo.nav_goal);
|
|
|
|
#ifdef _DEVEL
|
|
if (BUOY_DEBUG)
|
|
gi.dprintf("Found 1-step JUMP connection at %s\n", dest->targetname);
|
|
#endif
|
|
if(self->buoy_index!=NULL_BUOY)
|
|
self->lastbuoy = self->buoy_index;
|
|
else
|
|
self->lastbuoy = NULL_BUOY;
|
|
|
|
self->buoy_index =dest->id;
|
|
|
|
self->last_buoy_time = level.time;
|
|
return true;
|
|
}
|
|
|
|
//hey what if we're touching our e_bestbuoy buoy? - also, if this is a jump buoy and e_bestboy is the target of it, foce use of bestbuoy
|
|
if (bestbuoy == e_bestbuoy)
|
|
{
|
|
if(dont_use_last)
|
|
if(self->lastbuoy == bestbuoy->id)//found same buoy just touched as next buoy
|
|
return false;
|
|
|
|
self->monsterinfo.searchType = SEARCH_BUOY;
|
|
|
|
if(self->ai_mood != AI_MOOD_FLEE)
|
|
self->ai_mood = AI_MOOD_NAVIGATE;
|
|
|
|
VectorCopy(bestbuoy->origin, self->monsterinfo.nav_goal);
|
|
|
|
#ifdef _DEVEL
|
|
if (BUOY_DEBUG)
|
|
gi.dprintf("Found 1-step connection at %s\n", bestbuoy->targetname);
|
|
#endif
|
|
if(self->buoy_index!=NULL_BUOY)
|
|
self->lastbuoy = self->buoy_index;
|
|
else
|
|
self->lastbuoy = NULL_BUOY;
|
|
self->buoy_index = bestbuoy->id;
|
|
self->last_buoy_time = level.time;
|
|
return true;
|
|
}
|
|
|
|
if (bestbuoy && e_bestbuoy)
|
|
{
|
|
VectorSubtract(goalpos, self->s.origin, vec);
|
|
e_len = VectorLength(vec);
|
|
|
|
VectorSubtract(e_bestbuoy->origin, goalpos, vec);
|
|
len = VectorLength(vec);
|
|
|
|
dest = find_next_buoy(self, bestbuoy->id, e_bestbuoy->id);
|
|
|
|
if (dest != NULL)
|
|
{
|
|
#ifdef _DEVEL
|
|
if (BUOY_DEBUG)
|
|
{
|
|
gi.dprintf("Found connection from %s to %s\n", bestbuoy->targetname, e_bestbuoy->targetname);
|
|
}
|
|
#endif
|
|
if(dont_use_last)
|
|
{
|
|
if(self->lastbuoy == dest->id)//found same buoy just touched as next buoy
|
|
return false;
|
|
}
|
|
|
|
self->monsterinfo.searchType = SEARCH_BUOY;
|
|
|
|
if(self->ai_mood != AI_MOOD_FLEE)
|
|
self->ai_mood = AI_MOOD_NAVIGATE;
|
|
|
|
if((!(bestbuoy->modflags&BUOY_JUMP)||skipjump) && MG_ReachedBuoy(self, bestbuoy->origin))
|
|
{
|
|
MG_AssignMonsterNextBuoy(self, dest, e_bestbuoy);
|
|
}
|
|
else
|
|
{
|
|
MG_AssignMonsterNextBuoy(self, bestbuoy, e_bestbuoy);
|
|
}
|
|
return true;
|
|
}
|
|
#ifdef _DEVEL
|
|
else if (BUOY_DEBUG)
|
|
{
|
|
gi.dprintf("Failed to find a path\n");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
//EXPERIMENTAL
|
|
//ok, now you can backtrack since couldn't get there
|
|
//self->lastbuoy = NULL_BUOY;
|
|
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG_LITE||BUOY_DEBUG)
|
|
{
|
|
if (!bestbuoy)
|
|
gi.dprintf("%s COULDN'T FIND BUOYS FOR SELF!!!\n", self->classname);
|
|
else if (!e_bestbuoy)
|
|
gi.dprintf("%s COULDN'T FIND BUOYS FOR %s!!!\n", self->classname, self->enemy->classname);
|
|
}
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
========================
|
|
|
|
MG_MakeStartForcedConnection
|
|
|
|
Attempts to make a connection between a buoy and a monster's enemy
|
|
This function itself just finds the two buoys to attempt to make the connection between,
|
|
MG_ResolveBuoyConnection actually makes the connection between two buoys
|
|
|
|
========================
|
|
*/
|
|
qboolean MG_MakeStartForcedConnection(edict_t *self, int sforced_buoy, qboolean dont_use_last, qboolean skipjump)
|
|
{
|
|
buoy_t *found_buoy = NULL;
|
|
buoy_t *e_bestbuoy = NULL;
|
|
buoy_t *bestbuoy = NULL;
|
|
buoy_t *dest = NULL;
|
|
qboolean e_vis;
|
|
vec3_t vec, goalpos, e_buoyvec;
|
|
float bestdist, e_bestdist, e_len, e_buoydist;
|
|
int i;
|
|
int tracecount = 0;
|
|
float tracedist_total=0;
|
|
float buoy_passes = BUOY_SEARCH_PASSES;
|
|
float e_radius;
|
|
|
|
float search_pass_interval = MAX_BUOY_DIST / buoy_passes;
|
|
float k;
|
|
|
|
if(DEACTIVATE_BUOYS)
|
|
return false;
|
|
|
|
bestbuoy = &level.buoy_list[sforced_buoy];
|
|
bestdist = 0;
|
|
e_bestdist = 9999999;
|
|
|
|
self->last_buoyed_enemy = self->enemy;//Remember the last enemy I looked for
|
|
|
|
VectorCopy(self->enemy->s.origin, goalpos);
|
|
goalpos[2] += self->viewheight;
|
|
|
|
if(self->enemy->maxs[0]>16)
|
|
e_radius = 24 + self->enemy->maxs[0];
|
|
else
|
|
e_radius = 40;//24 + 16
|
|
|
|
//first, precalc all distances
|
|
for(i = 0; i <= level.active_buoys; i++)
|
|
{
|
|
found_buoy = &level.buoy_list[i];
|
|
VectorSubtract(goalpos, found_buoy->origin, vec);
|
|
found_buoy->temp_e_dist = VectorLength(vec);
|
|
if (found_buoy->temp_e_dist < (24+e_radius))
|
|
{
|
|
e_bestbuoy = found_buoy;
|
|
e_bestdist = found_buoy->temp_dist;
|
|
break;
|
|
}
|
|
}
|
|
|
|
//now, do all the passes, going from closest to farthest
|
|
for(k = 0; (k < buoy_passes)&&(!bestbuoy || !e_bestbuoy); k++)
|
|
{
|
|
for(i = 0; i <= level.active_buoys; i++)
|
|
{
|
|
found_buoy = &level.buoy_list[i];
|
|
|
|
e_vis = false;
|
|
e_len = found_buoy->temp_e_dist;
|
|
|
|
//only consider buoys in the current interval--closer ones have already been
|
|
//checked, and we'll save farther ones for later
|
|
if (e_len < e_bestdist && e_len > search_pass_interval*k && e_len < search_pass_interval*(k+1.0))
|
|
{
|
|
tracecount++;
|
|
tracedist_total+=e_len;
|
|
e_vis = Clear_Path(self->enemy, found_buoy->origin);
|
|
}
|
|
|
|
if (e_vis)
|
|
{
|
|
e_bestdist = e_len;
|
|
e_bestbuoy = found_buoy;
|
|
}
|
|
}
|
|
}
|
|
|
|
tracecount=0;
|
|
tracedist_total=0;
|
|
|
|
if (!e_bestbuoy && irand(0,10) < 5)
|
|
{//ok, Clear_Path too restrictive, try just clear_visible
|
|
|
|
//distances precalced already, so skip that step
|
|
|
|
//now, do all the passes, going from closest to farthest
|
|
for(k = 0; (k < buoy_passes)&&(!e_bestbuoy); k++)
|
|
{
|
|
for(i = 0; i <= level.active_buoys; i++)
|
|
{
|
|
found_buoy = &level.buoy_list[i];
|
|
e_vis = false;
|
|
|
|
e_len = found_buoy->temp_e_dist;
|
|
|
|
//only consider buoys in the current interval--closer ones have already been
|
|
//checked, and we'll save farther ones for later
|
|
if (e_len < e_bestdist && e_len > search_pass_interval*k && e_len < search_pass_interval*(k+1.0))
|
|
{
|
|
tracecount++;
|
|
tracedist_total+=e_len;
|
|
e_vis = clear_visible_pos(self->enemy, found_buoy->origin);
|
|
}
|
|
|
|
if (e_vis)
|
|
{
|
|
e_bestdist = e_len;
|
|
e_bestbuoy = found_buoy;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (e_bestdist > MAX_BUOY_DIST)
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG_LITE||BUOY_DEBUG)
|
|
gi.dprintf("%s's %s CLOSEST BUOY TOO FAR AWAY (%4.2f)\n", self->enemy->classname, vtos(self->s.origin), e_bestdist);
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
if(!(bestbuoy->modflags&BUOY_JUMP)||skipjump)
|
|
{//don't skip jump buoys, they're crucial
|
|
if(e_bestbuoy)
|
|
{
|
|
VectorSubtract(e_bestbuoy->origin, self->s.origin, e_buoyvec);
|
|
e_buoydist = VectorLength(e_buoyvec);
|
|
if (bestbuoy != e_bestbuoy && e_buoydist > bestdist)
|
|
{//enemy best buoy is farther away from me and not my buoy
|
|
if(Clear_Path(self, e_bestbuoy->origin))
|
|
{//can go straight at enemy best buoy even though farther away
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG_LITE||BUOY_DEBUG)
|
|
gi.dprintf("%s going after %s's buoy even though farther\n", self->classname, self->enemy->classname);
|
|
#endif
|
|
bestbuoy = e_bestbuoy;
|
|
bestdist = e_bestdist;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if(e_bestbuoy)
|
|
{//if going after a player, set his buoy for other monsters this frame
|
|
if(self->enemy->client)
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("%s setting player_buoy %d to %s\n", self->classname, self->enemy->s.number, e_bestbuoy->targetname);
|
|
#endif
|
|
level.player_buoy[self->enemy->s.number - 1] = e_bestbuoy->id;
|
|
}
|
|
}
|
|
|
|
return MG_ResolveBuoyConnection(self, bestbuoy, e_bestbuoy, goalpos, dont_use_last, skipjump);
|
|
}
|
|
|
|
/*
|
|
========================
|
|
|
|
MG_MakeForcedConnection
|
|
|
|
Attempts to make a connection between a monster and its forced_buoy
|
|
This function itself just finds the two buoys to attempt to make the connection between,
|
|
MG_ResolveBuoyConnection actually makes the connection between two buoys
|
|
|
|
========================
|
|
*/
|
|
qboolean MG_MakeForcedConnection(edict_t *self, int forced_buoy, qboolean dont_use_last, qboolean skipjump)
|
|
{
|
|
buoy_t *found_buoy = NULL;
|
|
buoy_t *e_bestbuoy = NULL;
|
|
buoy_t *bestbuoy = NULL;
|
|
buoy_t *dest = NULL;
|
|
qboolean vis;
|
|
vec3_t vec, goalpos, e_buoyvec;
|
|
float bestdist, e_bestdist, len, e_buoydist;
|
|
int i;
|
|
int tracecount = 0;
|
|
float tracedist_total=0;
|
|
float buoy_passes = BUOY_SEARCH_PASSES;
|
|
float radius;
|
|
|
|
float search_pass_interval = MAX_BUOY_DIST / buoy_passes;
|
|
float k;
|
|
|
|
bestdist = 9999999;
|
|
e_bestdist = 9999999;
|
|
|
|
if(DEACTIVATE_BUOYS)
|
|
return false;
|
|
|
|
e_bestbuoy = &level.buoy_list[forced_buoy];
|
|
e_bestdist = 0;
|
|
VectorCopy(e_bestbuoy->origin, goalpos);
|
|
|
|
#ifdef _DEVEL
|
|
if(!e_bestbuoy)
|
|
gi.dprintf("ERROR: forced_buoy not a valid buoy!!!\n");
|
|
#endif
|
|
if(self->maxs[0]>16)
|
|
radius = 24 + self->maxs[0];
|
|
else
|
|
radius = 40;//24 + 16
|
|
|
|
|
|
//first, precalc all distances
|
|
for(i = 0; i <= level.active_buoys; i++)
|
|
{
|
|
found_buoy = &level.buoy_list[i];
|
|
VectorSubtract(self->s.origin, found_buoy->origin, vec);
|
|
found_buoy->temp_dist = VectorLength(vec);
|
|
if (found_buoy->temp_dist < (24+radius))
|
|
{
|
|
bestbuoy = found_buoy;
|
|
bestdist = found_buoy->temp_dist;
|
|
break;
|
|
}
|
|
}
|
|
|
|
//now, do all the passes, going from closest to farthest
|
|
for(k = 0; (k < buoy_passes)&&(!bestbuoy || !e_bestbuoy); k++)
|
|
{
|
|
for(i = 0; i <= level.active_buoys; i++)
|
|
{
|
|
found_buoy = &level.buoy_list[i];
|
|
len = found_buoy->temp_dist;
|
|
|
|
vis = false;
|
|
|
|
//only consider buoys in the current interval--closer ones have already been
|
|
//checked, and we'll save farther ones for later
|
|
if (len < bestdist && len > search_pass_interval*k && len < search_pass_interval*(k+1.0))
|
|
{
|
|
tracecount++;
|
|
tracedist_total+=len;
|
|
vis = Clear_Path(self, found_buoy->origin);
|
|
}
|
|
|
|
if (vis)
|
|
{
|
|
bestdist = len;
|
|
bestbuoy = found_buoy;
|
|
}
|
|
}
|
|
}
|
|
|
|
tracecount=0;
|
|
tracedist_total=0;
|
|
|
|
if (bestdist > MAX_BUOY_DIST)
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG_LITE||BUOY_DEBUG)
|
|
gi.dprintf("%s's at %s CLOSEST BUOY TOO FAR AWAY (%4.2f)\n", self->classname, vtos(self->s.origin), bestdist);
|
|
#endif
|
|
self->pathfind_nextthink = level.time + 3;//wait 3 seconds before trying to use buoys again
|
|
return false;
|
|
}
|
|
|
|
if(!(bestbuoy->modflags&BUOY_JUMP)||skipjump)
|
|
{//don't skip jump buoys, they're crucial
|
|
VectorSubtract(e_bestbuoy->origin, self->s.origin, e_buoyvec);
|
|
e_buoydist = VectorLength(e_buoyvec);
|
|
if (bestbuoy != e_bestbuoy && e_buoydist > bestdist)
|
|
{//enemy best buoy is farther away from me and not my buoy
|
|
if(Clear_Path(self, e_bestbuoy->origin))
|
|
{//can go straight at enemy best buoy even though farther away
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG_LITE||BUOY_DEBUG)
|
|
gi.dprintf("%s going after forced_buoy %s even though farther\n", self->classname, e_bestbuoy->targetname);
|
|
#endif
|
|
bestbuoy = e_bestbuoy;
|
|
bestdist = e_bestdist;
|
|
}
|
|
}
|
|
}
|
|
|
|
return MG_ResolveBuoyConnection(self, bestbuoy, e_bestbuoy, goalpos, dont_use_last, skipjump);
|
|
}
|
|
|
|
/*
|
|
========================
|
|
|
|
MG_MakeNormalConnection
|
|
|
|
Attempts to make a buoy connection between a monster and its enemy
|
|
This function itself just finds the two buoys to attempt to make the connection between,
|
|
MG_ResolveBuoyConnection actually makes the connection between two buoys
|
|
|
|
========================
|
|
*/
|
|
qboolean MG_MakeNormalConnection(edict_t *self, qboolean dont_use_last, qboolean skipjump)
|
|
{
|
|
buoy_t *found_buoy = NULL;
|
|
buoy_t *e_bestbuoy = NULL;
|
|
buoy_t *bestbuoy = NULL;
|
|
buoy_t *dest = NULL;
|
|
qboolean e_vis, vis;
|
|
vec3_t vec, goalpos, e_buoyvec;
|
|
float bestdist, e_bestdist, len, e_len, e_buoydist;
|
|
int i;
|
|
int tracecount = 0;
|
|
float tracedist_total=0;
|
|
float buoy_passes = BUOY_SEARCH_PASSES;
|
|
float radius, e_radius;
|
|
|
|
float search_pass_interval = MAX_BUOY_DIST / buoy_passes;
|
|
float k;
|
|
|
|
bestdist = 9999999;
|
|
e_bestdist = 9999999;
|
|
|
|
if(DEACTIVATE_BUOYS)
|
|
return false;
|
|
|
|
VectorCopy(self->enemy->s.origin, goalpos);
|
|
goalpos[2] += self->viewheight;
|
|
|
|
if(self->maxs[0]>16)
|
|
radius = 24 + self->maxs[0];
|
|
else
|
|
radius = 40;//24 + 16
|
|
|
|
if(self->enemy->maxs[0]>16)
|
|
e_radius = 24 + self->enemy->maxs[0];
|
|
else
|
|
e_radius = 40;//24 + 16
|
|
|
|
//first, precalc all distances
|
|
for(i = 0; i <= level.active_buoys; i++)
|
|
{
|
|
found_buoy = &level.buoy_list[i];
|
|
|
|
if(!bestbuoy)
|
|
{
|
|
VectorSubtract(self->s.origin, found_buoy->origin, vec);
|
|
found_buoy->temp_dist = VectorLength(vec);
|
|
if (found_buoy->temp_dist < (24+radius))
|
|
{
|
|
bestbuoy = found_buoy;
|
|
bestdist = found_buoy->temp_dist;
|
|
}
|
|
}
|
|
|
|
if(!e_bestbuoy)
|
|
{
|
|
VectorSubtract(goalpos, found_buoy->origin, vec);
|
|
found_buoy->temp_e_dist = VectorLength(vec);
|
|
if (found_buoy->temp_e_dist < (24+e_radius))
|
|
{
|
|
e_bestbuoy = found_buoy;
|
|
e_bestdist = found_buoy->temp_dist;
|
|
}
|
|
}
|
|
if(e_bestbuoy && bestbuoy)
|
|
break;
|
|
}
|
|
|
|
//now, do all the passes, going from closest to farthest
|
|
for(k = 0; (k < buoy_passes)&&(!bestbuoy || !e_bestbuoy); k++)
|
|
{
|
|
for(i = 0; i <= level.active_buoys; i++)
|
|
{
|
|
found_buoy = &level.buoy_list[i];
|
|
len = found_buoy->temp_dist;
|
|
|
|
vis = false;
|
|
e_vis = false;
|
|
|
|
//only consider buoys in the current interval--closer ones have already been
|
|
//checked, and we'll save farther ones for later
|
|
if (len < bestdist && len > search_pass_interval*k && len < search_pass_interval*(k+1.0))
|
|
{
|
|
tracecount++;
|
|
tracedist_total+=len;
|
|
vis = Clear_Path(self, found_buoy->origin);
|
|
}
|
|
|
|
if (vis)
|
|
{
|
|
bestdist = len;
|
|
bestbuoy = found_buoy;
|
|
}
|
|
|
|
e_len = found_buoy->temp_e_dist;
|
|
|
|
//only consider buoys in the current interval--closer ones have already been
|
|
//checked, and we'll save farther ones for later
|
|
if (e_len < e_bestdist && e_len > search_pass_interval*k && e_len < search_pass_interval*(k+1.0))
|
|
{
|
|
tracecount++;
|
|
tracedist_total+=e_len;
|
|
e_vis = Clear_Path(self->enemy, found_buoy->origin);
|
|
}
|
|
|
|
if (e_vis)
|
|
{
|
|
e_bestdist = e_len;
|
|
e_bestbuoy = found_buoy;
|
|
}
|
|
}
|
|
}
|
|
|
|
tracecount=0;
|
|
tracedist_total=0;
|
|
|
|
if (bestdist > MAX_BUOY_DIST)
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG_LITE||BUOY_DEBUG)
|
|
gi.dprintf("%s's %s CLOSEST BUOY TOO FAR AWAY (%4.2f)\n", self->classname, vtos(self->s.origin), bestdist);
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
if (!bestbuoy && !e_bestbuoy)
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG_LITE||BUOY_DEBUG)
|
|
gi.dprintf("%s' %s COULDN'T FIND BUOYS FOR SELF OR %s!!!\n", self->classname, vtos(self->s.origin), self->enemy->classname);
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
if (!e_bestbuoy && irand(0,10) < 5)
|
|
{//ok, Clear_Path too restrictive, try just clear_visible
|
|
|
|
//distances precalced already, so skip that step
|
|
|
|
//now, do all the passes, going from closest to farthest
|
|
for(k = 0; (k < buoy_passes)&&(!e_bestbuoy); k++)
|
|
{
|
|
for(i = 0; i <= level.active_buoys; i++)
|
|
{
|
|
found_buoy = &level.buoy_list[i];
|
|
e_vis = false;
|
|
|
|
e_len = found_buoy->temp_e_dist;
|
|
|
|
//only consider buoys in the current interval--closer ones have already been
|
|
//checked, and we'll save farther ones for later
|
|
if (e_len < e_bestdist && e_len > search_pass_interval*k && e_len < search_pass_interval*(k+1.0))
|
|
{
|
|
tracecount++;
|
|
tracedist_total+=e_len;
|
|
e_vis = clear_visible_pos(self->enemy, found_buoy->origin);
|
|
}
|
|
|
|
if (e_vis)
|
|
{
|
|
e_bestdist = e_len;
|
|
e_bestbuoy = found_buoy;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (e_bestdist > MAX_BUOY_DIST)
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG_LITE||BUOY_DEBUG)
|
|
gi.dprintf("%s's %s CLOSEST BUOY TOO FAR AWAY (%4.2f)\n", self->enemy->classname, vtos(self->s.origin), e_bestdist);
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
if(!(bestbuoy->modflags&BUOY_JUMP)||skipjump)
|
|
{//don't skip jump buoys, they're crucial
|
|
if(e_bestbuoy)
|
|
{
|
|
VectorSubtract(e_bestbuoy->origin, self->s.origin, e_buoyvec);
|
|
e_buoydist = VectorLength(e_buoyvec);
|
|
if (bestbuoy != e_bestbuoy && e_buoydist > bestdist)
|
|
{//enemy best buoy is farther away from me and not my buoy
|
|
if(Clear_Path(self, e_bestbuoy->origin))
|
|
{//can go straight at enemy best buoy even though farther away
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG_LITE||BUOY_DEBUG)
|
|
gi.dprintf("%s going after %s's buoy even though farther\n", self->classname, self->enemy->classname);
|
|
#endif
|
|
bestbuoy = e_bestbuoy;
|
|
bestdist = e_bestdist;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(e_bestbuoy)
|
|
{//if going after a player, set his buoy for other monsters this frame
|
|
if(self->enemy->client)
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("%s setting player_buoy %d to %s\n", self->classname, self->enemy->s.number, e_bestbuoy->targetname);
|
|
#endif
|
|
level.player_buoy[self->enemy->s.number - 1] = e_bestbuoy->id;
|
|
}
|
|
}
|
|
|
|
return MG_ResolveBuoyConnection(self, bestbuoy, e_bestbuoy, goalpos, dont_use_last, skipjump);
|
|
}
|
|
|
|
|
|
/*
|
|
========================
|
|
|
|
MG_MakeConnection
|
|
|
|
Determines if monster should be pursuing a forced_buoy, if not, calls normal
|
|
enemy-tracking buoy connection finding function
|
|
|
|
========================
|
|
*/
|
|
int MG_MakeConnection_Go(edict_t *self, buoy_t *first_buoy, qboolean skipjump)
|
|
{
|
|
qboolean found_path = false;
|
|
qboolean dont_use_last;
|
|
buoy_t *forced_buoy = NULL;
|
|
buoy_t *found_buoy = NULL;
|
|
int k;
|
|
qboolean last_buoy_clear = false;
|
|
|
|
if(self->spawnflags & MSF_FIXED)
|
|
return false;
|
|
|
|
MG_RemoveBuoyEffects(self);
|
|
|
|
if(self->enemy && !(self->ai_mood_flags&AI_MOOD_FLAG_IGNORE_ENEMY) && self->ai_mood != AI_MOOD_FLEE)
|
|
{
|
|
self->ai_mood_flags &= ~AIMF_CANT_FIND_ENEMY;
|
|
|
|
if(self->enemy!=self->last_buoyed_enemy)//Current enemywasn't the last enemy I looked for...
|
|
{
|
|
dont_use_last = false;
|
|
self->lastbuoy = NULL_BUOY;//so forget last buoy I used
|
|
}
|
|
else if(self->monsterinfo.searchType == SEARCH_BUOY)
|
|
dont_use_last = true;
|
|
else
|
|
dont_use_last = false;
|
|
|
|
self->last_buoyed_enemy = self->enemy;//Remember the last enemy I looked for
|
|
|
|
if(self->enemy->client)
|
|
{//see if this player already has a bestbuoy found by a previous monster this frame
|
|
if(level.player_buoy[self->enemy->s.number - 1]>NULL_BUOY)//entity numbers 0 - 8 should be players
|
|
{//FIXME: if not, try his last player_buoy first!
|
|
if(BUOY_DEBUG)
|
|
{
|
|
for(k = 0; k<level.active_buoys; k++)
|
|
{
|
|
if(k == level.player_buoy[self->enemy->s.number - 1])
|
|
{
|
|
found_buoy = &level.buoy_list[k];
|
|
break;
|
|
}
|
|
}
|
|
if(found_buoy)
|
|
{
|
|
#ifdef _DEVEL
|
|
gi.dprintf("%s using player_buoy %d (%s) set previously this frame\n",
|
|
self->classname, self->enemy->s.number, found_buoy->targetname);
|
|
#endif
|
|
}
|
|
}
|
|
self->forced_buoy = level.player_buoy[self->enemy->s.number - 1];//just stores id in player_buoy
|
|
if(first_buoy)
|
|
{
|
|
forced_buoy = &level.buoy_list[self->forced_buoy];
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("%s trying connection to player_buoy %d (%s) from first_buoy %s\n",
|
|
self->classname, self->enemy->s.number, found_buoy->targetname, first_buoy->targetname);
|
|
#endif
|
|
found_path = MG_ResolveBuoyConnection(self, first_buoy, forced_buoy, forced_buoy->origin, dont_use_last, skipjump);
|
|
}
|
|
else
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("%s trying normal connection to player_buoy %d (%s)\n",
|
|
self->classname, self->enemy->s.number, found_buoy->targetname);
|
|
#endif
|
|
found_path = MG_MakeForcedConnection(self, self->forced_buoy, dont_use_last, skipjump);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dont_use_last = false;
|
|
self->last_buoyed_enemy = NULL;
|
|
|
|
if(self->ai_mood_flags&AI_MOOD_FLAG_FORCED_BUOY && self->forced_buoy != -1)
|
|
{
|
|
if(first_buoy)
|
|
{
|
|
forced_buoy = &level.buoy_list[self->forced_buoy];
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("%s trying non-enemy forced_buoy connection to %s from first_buoy %s\n",
|
|
self->classname, forced_buoy->targetname, first_buoy->targetname);
|
|
#endif
|
|
return MG_ResolveBuoyConnection(self, first_buoy, forced_buoy, forced_buoy->origin, dont_use_last, skipjump);
|
|
}
|
|
else
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("%s trying non-enemy forced_buoy connection to %s\n",
|
|
self->classname, level.buoy_list[self->forced_buoy].targetname);
|
|
#endif
|
|
return MG_MakeForcedConnection(self, self->forced_buoy, dont_use_last, skipjump);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self->lastbuoy = NULL_BUOY;//so forget last buoy I used
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if(!found_path)
|
|
{
|
|
if(self->ai_mood == AI_MOOD_FLEE)
|
|
return false;
|
|
|
|
if(self->enemy->client)
|
|
{
|
|
if(level.player_last_buoy[self->enemy->s.number - 1] > NULL_BUOY)
|
|
{//see if the player_last_buoy is a valid buoy for the enemy, if so, go for it
|
|
if(MG_ValidBestBuoyForEnt(self->enemy, &level.buoy_list[level.player_last_buoy[self->enemy->s.number - 1]]))
|
|
{
|
|
last_buoy_clear = true;
|
|
goto last_resort;
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("%s searching for player(%d)'s bestbuoy\n", self->classname, self->enemy->s.number);
|
|
#endif
|
|
if(first_buoy)
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("%s making connection from forced start %s to %s\n",
|
|
self->classname, first_buoy->targetname, self->enemy->classname);
|
|
#endif
|
|
found_path = MG_MakeStartForcedConnection(self, first_buoy->id, dont_use_last, skipjump);
|
|
}
|
|
else
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("%s making normal connection to %s\n", self->classname, self->enemy->classname);
|
|
#endif
|
|
found_path = MG_MakeNormalConnection(self, dont_use_last, skipjump);
|
|
}
|
|
}
|
|
|
|
if(found_path)
|
|
return true;
|
|
|
|
//OK you! Can't find ANY buoy connections, let's go with player_lastbuoy even if it can't connect to you!
|
|
self->ai_mood_flags |= AIMF_CANT_FIND_ENEMY;
|
|
|
|
last_resort:
|
|
if(self->enemy->client)
|
|
{
|
|
if(level.player_last_buoy[self->enemy->s.number - 1] > NULL_BUOY)
|
|
{//try the player_last_buoy, don't care if it can connect to player!
|
|
//FIXME: require that the player be withing 250 of this buoy at least?
|
|
if(BUOY_DEBUG)
|
|
{
|
|
for(k = 0; k<level.active_buoys; k++)
|
|
{
|
|
if(k == level.player_last_buoy[self->enemy->s.number - 1])
|
|
{
|
|
found_buoy = &level.buoy_list[k];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
//we dont actually set self->forced_buoy since we want to find a better buoy next time we look
|
|
forced_buoy = &level.buoy_list[level.player_last_buoy[self->enemy->s.number - 1]];
|
|
if(!last_buoy_clear)
|
|
{
|
|
if(self->ai_mood_flags & AIMF_SEARCHING || MG_ReachedBuoy(self, forced_buoy->origin))
|
|
{
|
|
return 3;
|
|
}
|
|
}
|
|
|
|
if(first_buoy)
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
{
|
|
if(last_buoy_clear)
|
|
gi.dprintf("%s using clear-path player_last_buoy %d (%s) from first_buoy %s\n",
|
|
self->classname, self->enemy->s.number, found_buoy->targetname, first_buoy->targetname);
|
|
else
|
|
gi.dprintf("%s using player_last_buoy %d (%s) from first_buoy %s even though it doesn't connect to player!\n",
|
|
self->classname, self->enemy->s.number, found_buoy->targetname, first_buoy->targetname);
|
|
}
|
|
#endif
|
|
if(MG_ResolveBuoyConnection(self, first_buoy, forced_buoy, forced_buoy->origin, dont_use_last, skipjump))
|
|
{
|
|
return 2;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
{
|
|
if(last_buoy_clear)
|
|
gi.dprintf("%s using clear-path player_last_buoy %d (%s)\n",
|
|
self->classname, self->enemy->s.number, found_buoy->targetname);
|
|
else
|
|
gi.dprintf("%s using player_last_buoy %d (%s) even though it doesn't connect to player!\n",
|
|
self->classname, self->enemy->s.number, found_buoy->targetname);
|
|
}
|
|
#endif
|
|
if(MG_MakeForcedConnection(self, forced_buoy->id, dont_use_last, skipjump))
|
|
{
|
|
return 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//damn, lost him! Currrzzze you Corvuzzz!!!
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("FAILURE: %s Could not find %s in any way shape or form!!!\n", self->classname, self->enemy->classname);
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
qboolean MG_MakeConnection(edict_t *self, buoy_t *first_buoy, qboolean skipjump)
|
|
{//just for debug info
|
|
qboolean result;
|
|
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("========================================================\n %s Start MakeConnection \n========================================================\n", self->classname);
|
|
#endif
|
|
result = MG_MakeConnection_Go (self, first_buoy, skipjump);
|
|
|
|
if(!(self->ai_mood_flags&AIMF_CANT_FIND_ENEMY))
|
|
{
|
|
self->monsterinfo.last_successful_enemy_tracking_time = level.time;
|
|
self->ai_mood_flags &= ~AIMF_SEARCHING;
|
|
}
|
|
|
|
if(result != true)
|
|
{//If can't find him(not including player_last_buoys) for 5 - 10 seconds, go into wander mode...
|
|
#ifdef _DEVEL
|
|
if (BUOY_DEBUG && !result)
|
|
{
|
|
gi.dprintf("MG_MakeConnection: failed\n");
|
|
}
|
|
#endif
|
|
if(result == 3 && !(self->ai_mood_flags&AIMF_SEARCHING))
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("%s got to %s's last_buoy, searching normally...\n", self->classname, self->enemy->classname);
|
|
#endif
|
|
self->monsterinfo.last_successful_enemy_tracking_time = level.time;
|
|
self->monsterinfo.searchType = SEARCH_COMMON;
|
|
self->ai_mood = AI_MOOD_PURSUE;
|
|
self->ai_mood_flags |= AIMF_SEARCHING;
|
|
}
|
|
else if(self->enemy &&
|
|
self->ai_mood != AI_MOOD_FLEE &&
|
|
!(self->ai_mood_flags & AI_MOOD_FLAG_IGNORE_ENEMY) &&
|
|
self->monsterinfo.last_successful_enemy_tracking_time + MONSTER_SEARCH_TIME < level.time)
|
|
{//give up, can't see him or find path to him for ten seconds now...
|
|
if(self->classID == CID_ASSASSIN && self->monsterinfo.last_successful_enemy_tracking_time + MONSTER_SEARCH_TIME + 20 > level.time)
|
|
{//assassins get an extra 20 seconds to look for the enemy and try to teleport to him
|
|
}
|
|
else
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("%s giving up finding %s, wandering around\n", self->classname, self->enemy->classname);
|
|
#endif
|
|
if(self->enemy->client)
|
|
self->oldenemy = self->enemy;
|
|
self->enemy = NULL;
|
|
|
|
if(!result && self->ai_mood == AI_MOOD_WANDER)
|
|
self->ai_mood = AI_MOOD_STAND;
|
|
else
|
|
self->ai_mood = AI_MOOD_WANDER;
|
|
}
|
|
}
|
|
else if(!result && self->ai_mood != AI_MOOD_FLEE && self->enemy)
|
|
{
|
|
self->monsterinfo.searchType = SEARCH_COMMON;
|
|
self->ai_mood = AI_MOOD_PURSUE;
|
|
}
|
|
|
|
if(!result && self->ai_mood == AI_MOOD_WANDER)
|
|
{
|
|
self->monsterinfo.pausetime = 0;
|
|
self->ai_mood = AI_MOOD_STAND;
|
|
}
|
|
|
|
if(result == 3)
|
|
result = false;
|
|
}
|
|
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("========================================================\n %s End MakeConnection \n========================================================\n", self->classname);
|
|
#endif
|
|
return result;
|
|
}
|
|
|
|
qboolean MG_CheckClearPathToEnemy(edict_t *self)
|
|
{
|
|
trace_t trace;
|
|
vec3_t mins, checkspot, enemy_dir, center;
|
|
float dist, i;
|
|
qboolean close_ok = false;
|
|
|
|
if(!self->enemy)
|
|
return false;
|
|
|
|
VectorCopy(self->mins, mins);
|
|
mins[2] += 18;
|
|
gi.trace(self->s.origin, mins, self->maxs, self->enemy->s.origin, self, MASK_SOLID,&trace);
|
|
|
|
if(trace.ent)
|
|
{
|
|
if(trace.ent == self->enemy)
|
|
return true;//bumped into our enemy!
|
|
}
|
|
|
|
if(trace.allsolid||trace.startsolid)//trace is through a wall next to me?
|
|
return false;
|
|
|
|
if(trace.fraction<1.0)
|
|
{//couldn't get to enemy
|
|
VectorSubtract(self->enemy->s.origin, trace.endpos, enemy_dir);
|
|
dist = VectorLength(enemy_dir);
|
|
if(dist>48 || !visible(self, self->enemy))
|
|
return false;//couldn't even get close to a visible enemy
|
|
}
|
|
|
|
if(!self->groundentity || (self->flags & FL_INWATER && self->enemy->flags & FL_INWATER))
|
|
return true;
|
|
|
|
if(self->flags & FL_FLY || self->movetype == PHYSICSTYPE_FLY || !self->gravity || self->classID == CID_GORGON)
|
|
return true;
|
|
|
|
if((self->flags & FL_INWATER || (self->flags & FL_SWIM) || (self->flags & FL_AMPHIBIAN))&&
|
|
self->enemy->flags & FL_INWATER)
|
|
return true;
|
|
|
|
//Now lets see if there is a solid ground or steps path to the enemy
|
|
|
|
//FIXME: what about jumping monsters? Call a jump message?
|
|
|
|
VectorAverage(self->absmin, self->absmax, center);
|
|
|
|
VectorSubtract(self->enemy->s.origin, center, enemy_dir);
|
|
dist = VectorNormalize(enemy_dir);
|
|
|
|
VectorCopy(self->s.origin, mins);
|
|
mins[2] += self->mins[2];
|
|
|
|
for(i = 0; i < dist; i += 8)
|
|
{//check to see if ground is there all the way to enemy
|
|
VectorMA(mins, i, enemy_dir, checkspot);
|
|
checkspot[2] -= 3;
|
|
if(!(gi.pointcontents(checkspot) & CONTENTS_SOLID))
|
|
{//not solid underneath
|
|
checkspot[2] -= 16;
|
|
if(!(gi.pointcontents(checkspot) & CONTENTS_SOLID))
|
|
{
|
|
return false;//not a step down
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
qboolean MG_CheckClearPathToSpot(edict_t *self, vec3_t spot)
|
|
{
|
|
trace_t trace;
|
|
vec3_t mins, checkspot, enemy_dir;
|
|
float dist, i;
|
|
qboolean close_ok = false;
|
|
|
|
VectorCopy(self->mins, mins);
|
|
mins[2] += 18;
|
|
gi.trace(self->s.origin, mins, self->maxs, spot, self, MASK_SOLID,&trace);
|
|
|
|
if(trace.ent)
|
|
{
|
|
if(trace.ent == self->enemy)
|
|
return true;//bumped into our enemy!
|
|
}
|
|
|
|
if(trace.allsolid||trace.startsolid)//trace is through a wall next to me?
|
|
return false;
|
|
|
|
if(trace.fraction<1.0)
|
|
{//couldn't get to enemy
|
|
VectorSubtract(spot, trace.endpos, enemy_dir);
|
|
dist = VectorLength(enemy_dir);
|
|
if(dist>48 || !visible_pos(self, spot))
|
|
return false;//couldn't even get close to a visible enemy
|
|
}
|
|
|
|
if(!self->groundentity || (self->flags & FL_INWATER && gi.pointcontents(spot)&MASK_WATER))
|
|
return true;
|
|
|
|
if(self->flags & FL_FLY || self->movetype == PHYSICSTYPE_FLY || !self->gravity || self->classID == CID_GORGON)
|
|
return true;
|
|
|
|
//Now lets see if there is a solid ground or steps path to the enemy
|
|
|
|
//FIXME: what about jumping monsters? Call a jump message?
|
|
|
|
VectorSubtract(spot, self->s.origin, enemy_dir);
|
|
dist = VectorNormalize(enemy_dir);
|
|
|
|
VectorCopy(self->s.origin, mins);
|
|
mins[2] += self->mins[2];
|
|
|
|
for(i = 0; i < dist; i += 8)
|
|
{//check to see if ground is there all the way to enemy
|
|
VectorMA(mins, i, enemy_dir, checkspot);
|
|
checkspot[2] -= 3;
|
|
if(!(gi.pointcontents(checkspot) & CONTENTS_SOLID))
|
|
{//not solid underneath
|
|
checkspot[2] -= 16;
|
|
if(!(gi.pointcontents(checkspot) & CONTENTS_SOLID))
|
|
return false;//not a step down
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
qboolean MG_OK_ToShoot(edict_t *self, edict_t *target)
|
|
{
|
|
if(target == self->enemy ||
|
|
(target->takedamage &&
|
|
(target->classID == CID_RAT ||
|
|
(!(target->svflags&SVF_MONSTER) && target->health<50)
|
|
)
|
|
)
|
|
)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
qboolean MG_CheckClearShotToEnemy(edict_t *self)
|
|
{
|
|
trace_t trace;
|
|
vec3_t startpos, endpos;
|
|
vec3_t zerovec = {0, 0, 0};
|
|
|
|
VectorCopy(self->s.origin, startpos);
|
|
startpos[2]+=self->viewheight;
|
|
|
|
VectorCopy(self->enemy->s.origin, endpos);
|
|
|
|
gi.trace(startpos, zerovec, zerovec, endpos, self, MASK_MONSTERSOLID,&trace);
|
|
// trace = gi.trace(startpos, vec3_origin, vec3_origin, endpos, self, MASK_MONSTERSOLID);
|
|
|
|
if(MG_OK_ToShoot(self, trace.ent))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void MG_MonsterFirePathTarget(edict_t *self, char *ptarg)
|
|
{
|
|
edict_t *ent;
|
|
|
|
ent = NULL;
|
|
|
|
while ((ent = G_Find(ent, FOFS(pathtargetname), ptarg)) != NULL)
|
|
{
|
|
if (ent->use)
|
|
ent->use(ent, self, self);
|
|
}
|
|
}
|
|
|
|
qboolean MG_MonsterAttemptTeleport(edict_t *self, vec3_t destination, qboolean ignoreLOS)
|
|
{
|
|
qboolean no_teleport = false;
|
|
trace_t trace;
|
|
vec3_t top, bottom, mins, maxs;
|
|
edict_t *ent = NULL;
|
|
int i;
|
|
|
|
if(self->svflags & SVF_BOSS || self->classID == CID_OGLE)
|
|
return false;
|
|
|
|
if(self->classID != CID_ASSASSIN)
|
|
{
|
|
//if still SEE monsters cheat, re-enable the following 2 lines
|
|
//if(skill->value < 2)
|
|
// return false;//only cheat on hard
|
|
|
|
if(!ignoreLOS)
|
|
{//check line of sight with all players
|
|
ent = g_edicts;
|
|
|
|
for(i = 0; i <= game.maxclients; i++)
|
|
{
|
|
ent = &g_edicts[i];
|
|
|
|
if(ent->client)
|
|
{
|
|
edict_t *temp;
|
|
|
|
temp = G_Spawn();
|
|
|
|
VectorSet(temp->s.origin,
|
|
ent->client->playerinfo.pcmd.camera_vieworigin[0] * 0.125,
|
|
ent->client->playerinfo.pcmd.camera_vieworigin[1] * 0.125,
|
|
ent->client->playerinfo.pcmd.camera_vieworigin[2] * 0.125);
|
|
|
|
if(gi.inPVS(temp->s.origin, destination))
|
|
{
|
|
no_teleport = true;
|
|
G_FreeEdict(temp);
|
|
break;
|
|
}
|
|
|
|
if(gi.inPVS(temp->s.origin, self->s.origin))
|
|
{
|
|
no_teleport = true;
|
|
G_FreeEdict(temp);
|
|
break;
|
|
}
|
|
|
|
G_FreeEdict(temp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!no_teleport)
|
|
{//do traces
|
|
VectorCopy(destination, bottom);
|
|
bottom[2] -= self->size[2];
|
|
|
|
VectorCopy(self->mins, mins);
|
|
VectorCopy(self->maxs, maxs);
|
|
mins[2] = 0;
|
|
maxs[2] = 1;
|
|
|
|
gi.trace(destination, mins, maxs, bottom, self, MASK_MONSTERSOLID, &trace);//self->clipmask
|
|
|
|
if(trace.fraction<1.0)
|
|
{
|
|
VectorCopy(trace.endpos, bottom);
|
|
VectorCopy(bottom, top);
|
|
top[2] += self->size[2] - 1;
|
|
gi.trace(bottom, mins, maxs, top, self, MASK_MONSTERSOLID, &trace);
|
|
|
|
if(trace.allsolid || trace.startsolid)
|
|
return false;
|
|
|
|
if(trace.fraction == 1.0)
|
|
{
|
|
bottom[2] -= self->mins[2];
|
|
|
|
if(self->classID == CID_ASSASSIN)
|
|
assassinPrepareTeleportDest(self, bottom);
|
|
else
|
|
{
|
|
VectorCopy(bottom, self->s.origin);
|
|
gi.linkentity(self);
|
|
}
|
|
|
|
MG_RemoveBuoyEffects(self);
|
|
self->lastbuoy = -1;
|
|
return true;
|
|
}
|
|
}
|
|
else if(!trace.allsolid && !trace.startsolid)
|
|
{
|
|
bottom[2] -= self->mins[2];
|
|
if(self->classID == CID_ASSASSIN)
|
|
assassinPrepareTeleportDest(self, bottom);
|
|
else
|
|
{
|
|
VectorCopy(bottom, self->s.origin);
|
|
gi.linkentity(self);
|
|
}
|
|
MG_RemoveBuoyEffects(self);
|
|
self->lastbuoy = -1;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void MG_AddBuoyEffect(edict_t *self, qboolean endbuoy)
|
|
{
|
|
if(BUOY_DEBUG)
|
|
{//turn on sparkly effects on my start and end buoys
|
|
if(!endbuoy)
|
|
{//marking our next buoy
|
|
if(self->nextbuoy[0])
|
|
{//check a 10 second debouce timer
|
|
if(!self->nextbuoy[0]->count)
|
|
{
|
|
#ifdef _DEVEL
|
|
gi.dprintf("Adding green effect to buoy %s\n", self->nextbuoy[0]->targetname);
|
|
#endif
|
|
gi.CreateEffect(&self->nextbuoy[0]->s,
|
|
FX_M_EFFECTS,
|
|
CEF_OWNERS_ORIGIN|CEF_FLAG6,//green
|
|
self->nextbuoy[0]->s.origin,
|
|
"bv",
|
|
FX_BUOY,
|
|
vec3_origin);
|
|
}
|
|
self->nextbuoy[0]->s.frame = self->nextbuoy[0]->count++;
|
|
}
|
|
}
|
|
else
|
|
{//marking our end buoy (enemy's closest buoy)
|
|
if(self->nextbuoy[1])
|
|
{
|
|
if(!self->nextbuoy[1]->s.frame)
|
|
{
|
|
#ifdef _DEVEL
|
|
gi.dprintf("Adding red effect to buoy %s\n", self->nextbuoy[1]->targetname);
|
|
#endif
|
|
gi.CreateEffect(&self->nextbuoy[1]->s,
|
|
FX_M_EFFECTS,
|
|
CEF_OWNERS_ORIGIN,//red
|
|
self->nextbuoy[1]->s.origin,
|
|
"bv",
|
|
FX_BUOY,
|
|
vec3_origin);
|
|
}
|
|
self->nextbuoy[1]->s.frame = self->nextbuoy[1]->count++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void MG_RemoveBuoyEffects(edict_t *self)
|
|
{//fixme- because this is really using only one effect with flags, it's turning off both red and green when it turns either off...
|
|
if(BUOY_DEBUG)
|
|
{//turn off sparkly effects on my start and end buoys
|
|
if(self->nextbuoy[0])
|
|
{//reset a 10 second debouce timer
|
|
if(self->nextbuoy[0]->count>1)
|
|
self->nextbuoy[0]->count--;
|
|
else
|
|
{
|
|
self->nextbuoy[0]->count = 0;
|
|
}
|
|
self->nextbuoy[0]->s.frame = self->nextbuoy[0]->count;
|
|
}
|
|
|
|
if(self->nextbuoy[1])
|
|
{
|
|
if(self->nextbuoy[1]->count>1)
|
|
self->nextbuoy[1]->count--;
|
|
else
|
|
{
|
|
self->nextbuoy[1]->count = 0;
|
|
}
|
|
self->nextbuoy[1]->s.frame = self->nextbuoy[1]->count;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
//FIXME: If a monster CAN see player but can't get to him for a short while and does not have a clear path to him, use the buoys anyway!
|
|
void MG_Pathfind(edict_t *self, qboolean check_clear_path)
|
|
{
|
|
buoy_t *current_buoy = NULL;
|
|
buoy_t *last_buoy = NULL;
|
|
buoy_t *jump_buoy = NULL;
|
|
qboolean clear_path = false;
|
|
|
|
if(self->spawnflags & MSF_FIXED)
|
|
return;
|
|
|
|
if(!(self->monsterinfo.aiflags & AI_USING_BUOYS))
|
|
{
|
|
self->ai_mood = AI_MOOD_PURSUE;
|
|
return;
|
|
}
|
|
|
|
if(DEACTIVATE_BUOYS)
|
|
{
|
|
self->monsterinfo.searchType = SEARCH_COMMON;
|
|
self->ai_mood = AI_MOOD_PURSUE;
|
|
return;
|
|
}
|
|
|
|
if (self->monsterinfo.searchType == SEARCH_COMMON)
|
|
{
|
|
if(!self->enemy)
|
|
return;
|
|
|
|
if(check_clear_path)
|
|
clear_path = MG_CheckClearPathToEnemy(self);
|
|
else
|
|
clear_path = false;
|
|
|
|
if(!clear_path)
|
|
{//this sucks- why should I do this every time pathfind is called- I need to know if the monster can get to the player directly.. if so, no Makeconnection attempt, less traces
|
|
if(!MG_MakeConnection(self, NULL, false))//if(!MG_MakeConnection(self, true, false))
|
|
{
|
|
}
|
|
}
|
|
}
|
|
else if (self->monsterinfo.searchType == SEARCH_BUOY)
|
|
{
|
|
current_buoy = &level.buoy_list[self->buoy_index];
|
|
last_buoy = &level.buoy_list[self->lastbuoy];
|
|
|
|
if(self->ai_mood != AI_MOOD_FLEE && self->ai_mood != AI_MOOD_WANDER)
|
|
self->ai_mood = AI_MOOD_NAVIGATE;
|
|
|
|
if (self->ai_mood == AI_MOOD_DELAY)
|
|
self->ai_mood = AI_MOOD_NAVIGATE;
|
|
|
|
if (self->ai_mood == AI_MOOD_JUMP && self->groundentity)
|
|
self->ai_mood = AI_MOOD_NAVIGATE;
|
|
|
|
if (MG_ReachedBuoy(self, NULL))
|
|
{
|
|
MG_RemoveBuoyEffects(self);
|
|
#ifdef _DEVEL
|
|
if (BUOY_DEBUG)
|
|
{
|
|
gi.dprintf("Reached goal %s\n", current_buoy->targetname);
|
|
}
|
|
#endif
|
|
//Check the possibility of activating something
|
|
if ((current_buoy->modflags & BUOY_ACTIVATE) && (self->ai_mood != AI_MOOD_DELAY))
|
|
{
|
|
#ifdef _DEVEL
|
|
if (BUOY_DEBUG)
|
|
gi.dprintf("Activating target %s\n", current_buoy->pathtarget);
|
|
#endif
|
|
if (self->wait < level.time)
|
|
{
|
|
self->wait = level.time + current_buoy->wait;
|
|
MG_MonsterFirePathTarget(self, current_buoy->pathtarget);
|
|
if (current_buoy->delay)
|
|
{
|
|
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
|
|
self->ai_mood = AI_MOOD_DELAY;
|
|
self->mood_nextthink = level.time + current_buoy->delay;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//if in AI_MOOD_FORCED_BUOY mode and this buoy is my forced_buoy,
|
|
//take off that ai_mood flag and clear forced_buoy
|
|
//also, if AI_MOOD_IGNORE_ENEMY flag, remove it
|
|
if(self->ai_mood_flags&AI_MOOD_FLAG_FORCED_BUOY && self->forced_buoy == current_buoy->id)
|
|
{
|
|
self->forced_buoy = -1;
|
|
self->ai_mood_flags &= ~AI_MOOD_FLAG_FORCED_BUOY;
|
|
|
|
if(self->ai_mood_flags&AI_MOOD_FLAG_GOTO_STAND)
|
|
{
|
|
self->ai_mood_flags &= ~AI_MOOD_FLAG_GOTO_STAND;
|
|
self->enemy = NULL;
|
|
self->ai_mood = AI_MOOD_STAND;
|
|
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, "");
|
|
return;
|
|
}
|
|
|
|
if(self->ai_mood_flags&AI_MOOD_FLAG_GOTO_WANDER)
|
|
{
|
|
self->ai_mood_flags &= ~AI_MOOD_FLAG_GOTO_WANDER;
|
|
self->enemy = NULL;
|
|
self->ai_mood = AI_MOOD_WANDER;
|
|
return;
|
|
}
|
|
|
|
if(self->ai_mood_flags&AI_MOOD_FLAG_GOTO_FIXED)
|
|
{
|
|
self->ai_mood_flags &= ~AI_MOOD_FLAG_GOTO_FIXED;
|
|
self->spawnflags |= MSF_FIXED;
|
|
if(self->enemy)
|
|
self->ai_mood = AI_MOOD_PURSUE;
|
|
else
|
|
self->ai_mood = AI_MOOD_STAND;
|
|
}
|
|
|
|
if(self->ai_mood != AI_MOOD_FLEE)
|
|
self->ai_mood_flags &= ~AI_MOOD_FLAG_IGNORE_ENEMY;
|
|
else
|
|
{//reached buoy was fleeing to now what?
|
|
if(MG_GoToRandomBuoy(self))
|
|
{
|
|
self->monsterinfo.searchType = SEARCH_BUOY;
|
|
return;
|
|
}
|
|
else
|
|
{//couldn't flee using buoys, use dumb fleeing
|
|
//FIXME: cowering if can't flee using buoys?
|
|
self->ai_mood_flags |= AI_MOOD_FLAG_DUMB_FLEE;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if(!M_ValidTarget(self, self->enemy))
|
|
{//got to where I was going, no enemy, so chill, baby.
|
|
if (self->monsterinfo.pausetime == -1)
|
|
{
|
|
self->spawnflags |= MSF_WANDER;
|
|
self->ai_mood = AI_MOOD_WANDER;
|
|
}
|
|
else if (level.time > self->monsterinfo.pausetime)
|
|
self->ai_mood = AI_MOOD_WALK;
|
|
else
|
|
{
|
|
self->ai_mood = AI_MOOD_STAND;
|
|
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, "");
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ((current_buoy->modflags & BUOY_JUMP) && (self->ai_mood != AI_MOOD_JUMP))
|
|
{
|
|
if(MG_MakeConnection(self, current_buoy, true))//make a regular connection, allowing skipping of jump_buoys
|
|
{
|
|
jump_buoy = current_buoy;
|
|
current_buoy = &level.buoy_list[self->buoy_index];
|
|
if(jump_buoy == current_buoy)
|
|
{//Shit, found same buoy, shouldn't happen with dont_use_last = true! unless switching enemies
|
|
#ifdef _DEVEL
|
|
if (BUOY_DEBUG)
|
|
gi.dprintf("Warning: %s found same next buoy as last buoy at jump buoy\n",self->classname);
|
|
#endif
|
|
}
|
|
else if(current_buoy->id == jump_buoy->jump_target_id)
|
|
{//go ahead and jump
|
|
if(self->groundentity)
|
|
{
|
|
vec3_t jumpangles, jumpfwd, jump_spot;
|
|
|
|
#ifdef _DEVEL
|
|
if (BUOY_DEBUG)
|
|
gi.dprintf("Jumping after buoy %s at angle %4.2f with height %4.2f and speed %4.2f\n",
|
|
current_buoy->targetname, jump_buoy->jump_yaw, jump_buoy->jump_uspeed, jump_buoy->jump_fspeed);
|
|
#endif
|
|
VectorSet(jumpangles, 0, jump_buoy->jump_yaw, 0);
|
|
AngleVectors(jumpangles, jumpfwd, NULL, NULL);
|
|
|
|
//since we may not be right on the buoy, find out where they want us to go by extrapolating and finding MY dir to there
|
|
VectorMA(jump_buoy->origin, jump_buoy->jump_fspeed, jumpfwd, jump_spot);
|
|
VectorSubtract(jump_spot, self->s.origin, jumpfwd);
|
|
jumpfwd[2] = 0;
|
|
VectorNormalize(jumpfwd);
|
|
|
|
VectorScale(jumpfwd, jump_buoy->jump_fspeed, self->movedir);
|
|
self->movedir[2] = jump_buoy->jump_uspeed;
|
|
self->ai_mood = AI_MOOD_JUMP;//don't technically need this line
|
|
self->mood_nextthink = level.time + 0.5;
|
|
//as an alternative, call self->forced_jump(self);
|
|
QPostMessage(self, MSG_CHECK_MOOD, PRI_DIRECTIVE, "i", AI_MOOD_JUMP);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{//follow the new path
|
|
#ifdef _DEVEL
|
|
if (BUOY_DEBUG)
|
|
{
|
|
current_buoy = &level.buoy_list[self->buoy_index];
|
|
gi.dprintf("Heading to new goal %s\n", current_buoy->targetname);
|
|
}
|
|
#endif
|
|
return;
|
|
//WAS: oops, not the right one, set it back and search down below again
|
|
// current_buoy = jump_buoy;
|
|
// MG_AssignMonsterNextBuoy(self, current_buoy, NULL);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return;//?
|
|
}
|
|
}
|
|
|
|
if (!MG_MakeConnection(self, current_buoy, false))
|
|
{
|
|
return;//?
|
|
}
|
|
else
|
|
{
|
|
#ifdef _DEVEL
|
|
if (BUOY_DEBUG)
|
|
{
|
|
gi.dprintf("Heading to new goal %s\n", current_buoy->targetname);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
if(self->last_buoy_time > 0 && self->last_buoy_time + BUOY_SEARCH_TIME < level.time)
|
|
{
|
|
#ifdef _DEVEL
|
|
if (BUOY_DEBUG)
|
|
{
|
|
gi.dprintf("Buoy search timed out trying to get to %s\n", current_buoy->targetname);
|
|
}
|
|
#endif
|
|
|
|
if(self->classID == CID_ASSASSIN)
|
|
{
|
|
if(MG_MonsterAttemptTeleport(self, current_buoy->origin, true))
|
|
{
|
|
self->monsterinfo.aiflags |= AI_OVERRIDE_GUIDE;
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("%s teleported to buoy %s (ignoring player LOS)\n",
|
|
self->classname, current_buoy->targetname);
|
|
#endif
|
|
return;
|
|
}
|
|
}
|
|
else if(CHEATING_MONSTERS)
|
|
{
|
|
if(CHEATING_MONSTERS<2)
|
|
{
|
|
#ifdef _DEVEL
|
|
if(MG_MonsterAttemptTeleport(self, current_buoy->origin, false))
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("%s cheated and teleported to buoy %s\n",
|
|
self->classname, current_buoy->targetname);
|
|
#else
|
|
MG_MonsterAttemptTeleport(self, current_buoy->origin, false);
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
#ifdef _DEVEL
|
|
if(MG_MonsterAttemptTeleport(self, current_buoy->origin, true))
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("%s cheated and teleported to buoy %s (ignoring player LOS)\n",
|
|
self->classname, current_buoy->targetname);
|
|
#else
|
|
MG_MonsterAttemptTeleport(self, current_buoy->origin, true);
|
|
#endif
|
|
}
|
|
}
|
|
if (!MG_MakeConnection(self, NULL, false))
|
|
{
|
|
}
|
|
}
|
|
else if(!irand(0, 4) && !clear_visible_pos(self, current_buoy->origin))
|
|
{//DAMN! Lost sight of buoy, let's re-aquire
|
|
#ifdef _DEVEL
|
|
if (BUOY_DEBUG)
|
|
gi.dprintf("%s Lost sight of buoy %s looking for another...\n",
|
|
self->classname, current_buoy->targetname);
|
|
#endif
|
|
if (!MG_MakeConnection(self, NULL, false))
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
* Guide functions
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*/
|
|
|
|
void MG_BuoyNavigate(edict_t *self)
|
|
{//Only handles buoy selection, some mood changing
|
|
//called from ai_run
|
|
qboolean valid_enemy = false;
|
|
int i;
|
|
buoy_t *found_buoy = NULL;
|
|
qboolean found;
|
|
|
|
//See if my enemy is still valid
|
|
valid_enemy = M_ValidTarget(self, self->enemy);
|
|
|
|
if(self->spawnflags & MSF_FIXED)
|
|
return;
|
|
|
|
if(!(self->monsterinfo.aiflags & AI_USING_BUOYS))
|
|
{
|
|
self->ai_mood = AI_MOOD_PURSUE;//ai_mood_normal?
|
|
return;
|
|
}
|
|
|
|
//STEP 1: See if should be running away or wandering
|
|
|
|
if(self->monsterinfo.flee_finished < level.time)
|
|
self->monsterinfo.aiflags &= ~AI_FLEE;//clear the flee flag now
|
|
|
|
if(self->monsterinfo.aiflags & AI_COWARD ||
|
|
(self->monsterinfo.aiflags&AI_FLEE && self->monsterinfo.flee_finished >= level.time))
|
|
self->ai_mood = AI_MOOD_FLEE;
|
|
|
|
if(!valid_enemy)
|
|
{//no enemy, now what?
|
|
self->enemy = NULL;
|
|
if(self->spawnflags & MSF_WANDER || self->monsterinfo.pausetime == -1)
|
|
{
|
|
self->spawnflags |= MSF_WANDER;
|
|
self->ai_mood = AI_MOOD_WANDER;
|
|
}
|
|
else if (level.time > self->monsterinfo.pausetime)
|
|
self->ai_mood = AI_MOOD_WALK;
|
|
else
|
|
self->ai_mood = AI_MOOD_STAND;
|
|
}
|
|
else
|
|
{
|
|
if(self->ai_mood == AI_MOOD_WANDER)
|
|
self->ai_mood = AI_MOOD_PURSUE;
|
|
}
|
|
|
|
if(self->ai_mood == AI_MOOD_FLEE || self->ai_mood == AI_MOOD_WANDER)
|
|
{//go off in a random buoy path
|
|
|
|
if(!(self->ai_mood_flags&AI_MOOD_FLAG_FORCED_BUOY))
|
|
{//first time, find closest buoy, alert other enemies
|
|
if(self->ai_mood == AI_MOOD_FLEE)
|
|
{//wake up enemies for next 10 seconds
|
|
level.sight_entity = self;
|
|
level.sight_entity_framenum = level.framenum + 100;
|
|
level.sight_entity->light_level = 128;
|
|
}
|
|
|
|
if(MG_GoToRandomBuoy(self))
|
|
{
|
|
self->monsterinfo.searchType = SEARCH_BUOY;
|
|
return;
|
|
}
|
|
else if(self->ai_mood == AI_MOOD_FLEE)
|
|
{//couldn't flee using buoys, use dumb fleeing
|
|
//FIXME: cowering if can't flee using buoys?
|
|
self->ai_mood_flags |= AI_MOOD_FLAG_DUMB_FLEE;
|
|
return;
|
|
|
|
}
|
|
//otherwise, want to wander, but can't, continue down the possibilities
|
|
}
|
|
else
|
|
{
|
|
self->monsterinfo.searchType = SEARCH_BUOY;
|
|
MG_Pathfind(self, false);
|
|
return;//already wandering normal buoy navigation
|
|
}
|
|
}
|
|
//STEP 2: Not running away or wandering, see what should be doing
|
|
|
|
if(!valid_enemy)
|
|
{//No enemy, Not wandering or can't, see if I have a homebuoy
|
|
if(self->homebuoy)
|
|
{//have a home base, let's get back there if no enemy
|
|
for(i = 0; i <= level.active_buoys; i++)
|
|
{
|
|
found_buoy = &level.buoy_list[i];
|
|
if(found_buoy->targetname && !stricmp(found_buoy->targetname, self->homebuoy))
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!found)
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("ERROR: %s can't find it's homebuoy %s\n", self->classname, self->homebuoy);
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
if(!MG_ReachedBuoy(self, found_buoy->origin))
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("%s heading for homebuoy %s\n", self->classname, self->homebuoy);
|
|
#endif
|
|
self->ai_mood_flags|=AI_MOOD_FLAG_FORCED_BUOY;
|
|
self->forced_buoy = found_buoy->id;
|
|
|
|
if(MG_MakeConnection(self, NULL, false))
|
|
{
|
|
self->ai_mood = AI_MOOD_NAVIGATE;
|
|
|
|
QPostMessage(self, MSG_WALK,PRI_DIRECTIVE, NULL);
|
|
|
|
MG_RemoveBuoyEffects(self);
|
|
}
|
|
else
|
|
{
|
|
self->ai_mood_flags &= ~AI_MOOD_FLAG_FORCED_BUOY;
|
|
self->forced_buoy = -1;
|
|
}
|
|
}
|
|
}
|
|
//No enemy Not wandering, not going to homebuoy (or can't do these for some reason), so just stand around
|
|
//do we really need to clear the enemy? mAybe we shouldn't...
|
|
if(self->enemy)
|
|
{
|
|
if(self->enemy->client)
|
|
self->oldenemy = self->enemy;//remember last player enemy
|
|
}
|
|
self->enemy = NULL;
|
|
|
|
self->mood_nextthink = level.time + 1;
|
|
//fixme: check for a self->target also?
|
|
if (self->monsterinfo.pausetime == -1)
|
|
{
|
|
self->spawnflags |= MSF_WANDER;
|
|
self->ai_mood = AI_MOOD_WANDER;
|
|
}
|
|
else if (level.time > self->monsterinfo.pausetime)
|
|
self->ai_mood = AI_MOOD_WALK;
|
|
else
|
|
self->ai_mood = AI_MOOD_STAND;
|
|
return;
|
|
}
|
|
else if(self->ai_mood_flags&AI_MOOD_FLAG_IGNORE_ENEMY)
|
|
{//have an enemy, but being forced to use buoys, and ignore enemy until get to forced_buoy
|
|
self->ai_mood = AI_MOOD_NAVIGATE;
|
|
MG_Pathfind(self, false);
|
|
return;
|
|
}
|
|
|
|
//Actually have a valid enemy. let's try to get him
|
|
self->ai_mood = AI_MOOD_PURSUE;
|
|
|
|
MG_Pathfind(self, true);
|
|
if(self->ai_mood == AI_MOOD_PURSUE)
|
|
self->goalentity = self->enemy;
|
|
}
|
|
|
|
void Cvar_SetValue (char *var_name, float value);
|
|
void MG_GenericMoodSet(edict_t *self)
|
|
{
|
|
vec3_t v, forward, pursue_vel;
|
|
float enemydist;
|
|
qboolean coward = false;
|
|
qboolean can_attack_ranged = false;
|
|
qboolean can_attack_close = false;
|
|
qboolean clear_shot = false;
|
|
qboolean enemyvis = false;
|
|
qboolean enemyinfront = false;
|
|
qboolean found = false;
|
|
qboolean melee_go = false;
|
|
int i;
|
|
buoy_t *found_buoy;
|
|
qboolean valid_enemy = false;
|
|
|
|
if(!level.active_buoys)
|
|
{
|
|
if(!DEACTIVATE_BUOYS)
|
|
{
|
|
gi.dprintf("WARNING: no buoys on this map!!!\n");
|
|
if(!level.active_buoys)
|
|
{//1st buoy, initialize a couple arrays
|
|
for(i = 0; i < MAX_CLIENTS; i++)
|
|
{
|
|
level.player_buoy[i] = NULL_BUOY; //stores current bestbuoy for a player enemy (if any)
|
|
level.player_last_buoy[i] = NULL_BUOY; //when player_buoy is invalid, saves it here so monsters can check it first instead of having to do a whole search
|
|
}
|
|
}
|
|
Cvar_SetValue("deactivate_buoys", 1);
|
|
DEACTIVATE_BUOYS = true;
|
|
}
|
|
}
|
|
|
|
if(self->mood_nextthink > level.time || self->mood_nextthink <= 0.0f)
|
|
return;
|
|
|
|
//See if my enemy is still valid
|
|
valid_enemy = M_ValidTarget(self, self->enemy);
|
|
|
|
if(!(self->monsterinfo.aiflags & AI_USING_BUOYS))
|
|
{//skip buoy stuff
|
|
self->ai_mood = AI_MOOD_PURSUE;
|
|
}
|
|
else
|
|
{//use buoys
|
|
//STEP 1: See if should be running away or wandering
|
|
if(self->monsterinfo.flee_finished < level.time)
|
|
self->monsterinfo.aiflags &= ~AI_FLEE;//clear the flee flag now
|
|
|
|
if(self->monsterinfo.aiflags & AI_COWARD ||
|
|
(self->monsterinfo.aiflags&AI_FLEE && self->monsterinfo.flee_finished >= level.time))
|
|
self->ai_mood = AI_MOOD_FLEE;
|
|
|
|
if(!valid_enemy)
|
|
{//no enemy, now what?
|
|
self->enemy = NULL;
|
|
|
|
if(self->spawnflags & MSF_FIXED)
|
|
return;
|
|
|
|
if(self->spawnflags & MSF_WANDER || self->monsterinfo.pausetime == -1)
|
|
{
|
|
self->spawnflags |= MSF_WANDER;
|
|
self->ai_mood = AI_MOOD_WANDER;
|
|
}
|
|
else if (level.time > self->monsterinfo.pausetime)
|
|
self->ai_mood = AI_MOOD_WALK;
|
|
else
|
|
self->ai_mood = AI_MOOD_STAND;
|
|
}
|
|
else
|
|
{
|
|
if(self->spawnflags & MSF_FIXED)
|
|
goto checkattacks;
|
|
|
|
if(self->ai_mood == AI_MOOD_WANDER)
|
|
self->ai_mood = AI_MOOD_PURSUE;
|
|
}
|
|
|
|
if(self->ai_mood == AI_MOOD_FLEE || self->ai_mood == AI_MOOD_WANDER)
|
|
{//go off in a random buoy path
|
|
if(!(self->ai_mood_flags&AI_MOOD_FLAG_FORCED_BUOY))
|
|
{//first time, find closest buoy, alert other enemies
|
|
if(self->ai_mood == AI_MOOD_FLEE)
|
|
{//wake up enemies for next 10 seconds
|
|
level.sight_entity = self;
|
|
level.sight_entity_framenum = level.framenum + 100;
|
|
level.sight_entity->light_level = 128;
|
|
}
|
|
|
|
if(MG_GoToRandomBuoy(self))
|
|
{
|
|
self->monsterinfo.searchType = SEARCH_BUOY;
|
|
return;
|
|
}
|
|
else if(self->ai_mood == AI_MOOD_FLEE)
|
|
{//couldn't flee using buoys, use dumb fleeing
|
|
//FIXME: cowering if can't flee using buoys?
|
|
self->ai_mood_flags |= AI_MOOD_FLAG_DUMB_FLEE;
|
|
return;
|
|
}
|
|
//otherwise, want to wander, but can't, continue down the possibilities
|
|
}
|
|
else
|
|
{
|
|
self->monsterinfo.searchType = SEARCH_BUOY;
|
|
MG_Pathfind(self, false);
|
|
return;//already wandering normal buoy navigation
|
|
}
|
|
}
|
|
//STEP 2: Not running away or wandering, see what should be doing
|
|
|
|
if(!valid_enemy)
|
|
{//No enemy, Not wandering or can't, see if I have a homebuoy
|
|
if(self->homebuoy)
|
|
{//have a home base, let's get back there if no enemy
|
|
for(i = 0; i <= level.active_buoys; i++)
|
|
{
|
|
found_buoy = &level.buoy_list[i];
|
|
if(found_buoy->targetname && !stricmp(found_buoy->targetname, self->homebuoy))
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
//////////////////////////// BUGBUGBUGBUG 7 lines above, targetname can be NULL if you just happen to die.
|
|
|
|
if(!found)
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("ERROR: %s can't find it's homebuoy %s\n", self->classname, self->homebuoy);
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
if(!MG_ReachedBuoy(self, found_buoy->origin))
|
|
{
|
|
#ifdef _DEVEL
|
|
if(BUOY_DEBUG)
|
|
gi.dprintf("%s heading for homebuoy %s\n", self->classname, self->homebuoy);
|
|
#endif
|
|
|
|
self->ai_mood_flags|=AI_MOOD_FLAG_FORCED_BUOY;
|
|
self->forced_buoy = found_buoy->id;
|
|
|
|
if(MG_MakeConnection(self, NULL, false))
|
|
{
|
|
self->ai_mood = AI_MOOD_NAVIGATE;
|
|
|
|
QPostMessage(self, MSG_WALK,PRI_DIRECTIVE, NULL);
|
|
|
|
MG_RemoveBuoyEffects(self);
|
|
}
|
|
else
|
|
{
|
|
self->ai_mood_flags &= ~AI_MOOD_FLAG_FORCED_BUOY;
|
|
self->forced_buoy = -1;
|
|
}
|
|
}
|
|
}
|
|
//No enemy Not wandering, not going to homebuoy (or can't do these for some reason), so just stand around
|
|
//do we really need to clear the enemy? mAybe we shouldn't...
|
|
if(self->enemy)
|
|
{
|
|
if(self->enemy->client)
|
|
self->oldenemy = self->enemy;//remember last player enemy
|
|
}
|
|
self->enemy = NULL;
|
|
|
|
self->mood_nextthink = level.time + 1;
|
|
//fixme: check for a self->target also?
|
|
if (self->monsterinfo.pausetime == -1)
|
|
{
|
|
self->spawnflags |= MSF_WANDER;
|
|
self->ai_mood = AI_MOOD_WANDER;
|
|
}
|
|
else if (level.time > self->monsterinfo.pausetime)
|
|
self->ai_mood = AI_MOOD_WALK;
|
|
else
|
|
self->ai_mood = AI_MOOD_STAND;
|
|
return;
|
|
}
|
|
else if(self->ai_mood_flags&AI_MOOD_FLAG_IGNORE_ENEMY)
|
|
{//have an enemy, but being forced to use buoys, and ignore enemy until get to forced_buoy
|
|
self->ai_mood = AI_MOOD_NAVIGATE;
|
|
MG_Pathfind(self, false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if(!valid_enemy || !self->enemy)
|
|
{
|
|
if (self->monsterinfo.aiflags & AI_EATING)
|
|
self->ai_mood = AI_MOOD_EAT;
|
|
return;
|
|
}
|
|
|
|
//STEP 3: OK, have a valid enemy, let's go get him!
|
|
checkattacks:
|
|
self->ai_mood = AI_MOOD_PURSUE;
|
|
|
|
//get distance to target, ignore Z diff if close
|
|
VectorSubtract (self->s.origin, self->enemy->s.origin, v);
|
|
if (v[2] <= 40)
|
|
v[2] = 0;
|
|
enemydist = VectorLength(v) - self->enemy->maxs[0];
|
|
|
|
if ((self->monsterinfo.aiflags & AI_EATING) && (enemydist > self->wakeup_distance) && !self->monsterinfo.awake)
|
|
{
|
|
self->monsterinfo.last_successful_enemy_tracking_time = level.time;
|
|
self->ai_mood = AI_MOOD_EAT;
|
|
return;
|
|
}
|
|
|
|
if(self->monsterinfo.aiflags & AI_NO_MISSILE)
|
|
self->spawnflags &= ~MSF_FIXED;//don't stand around if can't fire
|
|
|
|
if(self->attack_debounce_time > level.time || self->monsterinfo.attack_finished > level.time)
|
|
{//can't attack yet
|
|
can_attack_ranged = false;
|
|
clear_shot = false;
|
|
can_attack_close = false;
|
|
}
|
|
else
|
|
{
|
|
if(classStatics[self->classID].msgReceivers[MSG_MISSILE] && !(self->monsterinfo.aiflags&AI_NO_MISSILE))
|
|
{
|
|
can_attack_ranged = true;
|
|
clear_shot = MG_CheckClearShotToEnemy(self);
|
|
}
|
|
else
|
|
clear_shot = false;
|
|
|
|
if(classStatics[self->classID].msgReceivers[MSG_MELEE] && !(self->monsterinfo.aiflags&AI_NO_MELEE))
|
|
can_attack_close = true;
|
|
}
|
|
|
|
if(enemyvis = visible(self, self->enemy))
|
|
{
|
|
self->ai_mood_flags &= ~AIMF_CANT_FIND_ENEMY;
|
|
self->ai_mood_flags &= ~AIMF_SEARCHING;
|
|
self->monsterinfo.last_successful_enemy_tracking_time = level.time;
|
|
|
|
if(self->ai_mood_flags&AI_MOOD_FLAG_BACKSTAB)
|
|
{//only approach and attack the enemy's back- be sure to take this off if hurt?
|
|
if(enemydist < 128)
|
|
{
|
|
self->ai_mood_flags &= ~AI_MOOD_FLAG_BACKSTAB;
|
|
}
|
|
else if(infront(self->enemy, self))
|
|
{
|
|
self->ai_mood = AI_MOOD_DELAY;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
enemyinfront = infront(self, self->enemy);
|
|
|
|
//HEY! What if too close- backpedal or flee for a bit?
|
|
//Also, need a chance of closing in anyway- a bypass_missile_chance?
|
|
if(enemyvis && enemyinfront &&
|
|
can_attack_ranged &&
|
|
clear_shot &&
|
|
enemydist <= self->missile_range)
|
|
{//are they far enough away?
|
|
if(irand(0, 100)>self->bypass_missile_chance)
|
|
{
|
|
if(enemydist >= self->min_missile_range)
|
|
{//ranged attack!
|
|
self->ai_mood = AI_MOOD_ATTACK;
|
|
self->ai_mood_flags &= ~AI_MOOD_FLAG_MELEE;
|
|
self->ai_mood_flags |= AI_MOOD_FLAG_MISSILE;
|
|
self->attack_debounce_time = level.time + (3 - skill->value)/2;
|
|
return;
|
|
}
|
|
else if(!can_attack_close)
|
|
{//too close and can't melee!
|
|
goto enemy_too_close;
|
|
}
|
|
}
|
|
}
|
|
|
|
//otherwise, close in
|
|
if (!MG_CheckClearPathToEnemy(self)&&self->classID!=CID_TBEAST)
|
|
{//can't directly approach enemy
|
|
// if(enemyinfront)//ie, failed to shoot for some other reason
|
|
// {//can't shoot enemy, use buoys to get there
|
|
MG_Pathfind(self, false);//false means don't do a mg_checkclearpath
|
|
if(self->ai_mood == AI_MOOD_PURSUE)
|
|
self->goalentity = self->enemy;
|
|
if(self->cant_attack_think)
|
|
self->cant_attack_think(self, enemydist, enemyvis, enemyinfront);
|
|
return;
|
|
// }
|
|
}
|
|
|
|
//use dummy AI
|
|
self->monsterinfo.searchType = SEARCH_COMMON;
|
|
self->movetarget = self->goalentity = self->enemy;
|
|
|
|
//can directly approach player
|
|
|
|
if(self->melee_range < 0)
|
|
{//keep a distance
|
|
if(enemydist <= -self->melee_range)
|
|
{//hang back and wait until CAN fire a shot off
|
|
goto enemy_too_close;
|
|
}//else close in
|
|
}
|
|
|
|
//check for melee range attack
|
|
|
|
if(can_attack_close)
|
|
{
|
|
if(!enemyvis || !enemyinfront || enemydist > self->melee_range || enemydist < self->min_melee_range)
|
|
{
|
|
AngleVectors(self->s.angles, forward, NULL, NULL);
|
|
VectorSubtract(self->s.origin, self->s.old_origin, pursue_vel);
|
|
melee_go = M_PredictTargetEvasion(self, self->enemy, pursue_vel, self->enemy->velocity, self->melee_range, 5);//predict for next half second
|
|
}
|
|
else
|
|
melee_go = true;
|
|
}
|
|
|
|
if(melee_go)
|
|
{//Close enough to melee
|
|
self->ai_mood = AI_MOOD_ATTACK;
|
|
self->ai_mood_flags |= AI_MOOD_FLAG_MELEE;
|
|
self->ai_mood_flags &= ~AI_MOOD_FLAG_MISSILE;
|
|
self->attack_debounce_time = level.time + (3 - skill->value)/2;
|
|
//OR: ok to missile too?
|
|
return;
|
|
}
|
|
else if(enemydist < self->min_melee_range)
|
|
{
|
|
goto enemy_too_close;
|
|
return;
|
|
}
|
|
|
|
//Can't melee, so just run blindly
|
|
self->ai_mood = AI_MOOD_PURSUE;
|
|
if(self->cant_attack_think)
|
|
self->cant_attack_think(self, enemydist, enemyvis, enemyinfront);
|
|
return;
|
|
|
|
enemy_too_close:
|
|
if(classStatics[self->classID].msgReceivers[MSG_FALLBACK])
|
|
{//what if I hit a wall? Go into attack anyway?
|
|
self->ai_mood = AI_MOOD_BACKUP;//walk back while firing
|
|
}
|
|
else//maybe turn and run for a bit?
|
|
{
|
|
#ifdef _DEVEL
|
|
if(MGAI_DEBUG)
|
|
gi.dprintf("%s running away to get some distance from %s\n", self->classname, self->enemy->classname);
|
|
#endif
|
|
self->monsterinfo.aiflags |= AI_FLEE;
|
|
self->monsterinfo.flee_finished = level.time + flrand(3, 6);
|
|
//self->ai_mood = AI_MOOD_DELAY;//fixme: this is not good!
|
|
}
|
|
return;
|
|
}
|
|
|
|
void MG_InitMoods(edict_t *self)
|
|
{
|
|
self->monsterinfo.searchType = SEARCH_COMMON;
|
|
|
|
if(!self->mintel)
|
|
self->mintel = MaxBuoysForClass[self->classID];
|
|
|
|
self->mood_think = MG_GenericMoodSet;//we'll re-specialize these soon
|
|
self->mood_nextthink = level.time + 0.1;
|
|
if(self->mood_nextthink <= 0.0f)
|
|
self->mood_nextthink = 0.1;
|
|
|
|
//setup attack ranges for the mood functions to use
|
|
//these can be set by the designer if desired and can be
|
|
//affected later by the loss of a weapon or limb...
|
|
if(!self->min_melee_range)
|
|
self->min_melee_range = 0;//rendundant, I know, but clearer to see it here with other stuff
|
|
|
|
if(!self->melee_range)
|
|
self->melee_range = AttackRangesForClass[self->classID * 4 + 0];
|
|
|
|
if(!self->missile_range)
|
|
self->missile_range = AttackRangesForClass[self->classID * 4 + 1];
|
|
|
|
if(!self->min_missile_range)
|
|
self->min_missile_range = AttackRangesForClass[self->classID * 4 + 2];
|
|
|
|
if(!self->bypass_missile_chance)
|
|
self->bypass_missile_chance = AttackRangesForClass[self->classID * 4 + 3];
|
|
|
|
if(!self->jump_chance)
|
|
self->jump_chance = JumpChanceForClass[self->classID];
|
|
|
|
if(!self->wakeup_distance)
|
|
self->wakeup_distance = MAX_SIGHT_PLAYER_DIST;
|
|
|
|
//so ai_run knows to call MG_BuoyNavigate...
|
|
if(self->mintel > 0)
|
|
self->monsterinfo.aiflags |= AI_USING_BUOYS;
|
|
|
|
if(!skill->value)//no skill = 1/2 health monsters
|
|
self->max_health = self->health = self->health * 0.5;
|
|
|
|
self->lastbuoy = NULL_BUOY;
|
|
}
|
|
|
|
|