heretic2-sdk/Toolkit/Programming/GameCode/game/g_ai.c
1999-03-18 00:00:00 +00:00

2537 lines
59 KiB
C

// g_ai.c
#include <assert.h>
#include "g_local.h"
#include "g_monster.h"
#include "Random.h"
#include "vector.h"
#include "buoy.h"
#include "m_stats.h"
#include "p_anims.h"
qboolean FindTarget (edict_t *self);
extern cvar_t *maxclients;
qboolean ai_checkattack (edict_t *self, float dist);
//void gkrokon_maintain_waypoints(edict_t *self, float mintel, float foo1, float foo2);
void SV_NewChaseDir (edict_t *actor, edict_t *enemy, float dist);
//void gkrokon_hopdown(edict_t *self);
void ssithraCheckJump (edict_t *self);
void ssithraStartle (edict_t *self);
void ssithraLookRight (edict_t *self);
void seraph_startle(edict_t *self);
void SV_FixCheckBottom (edict_t *ent);
qboolean clear_visible (edict_t *self, edict_t *other);
trace_t MG_WalkMove (edict_t *self, float yaw, float dist);
qboolean MG_MoveToGoal (edict_t *self, float dist);
float MG_ChangeYaw (edict_t *self);
void MG_BuoyNavigate(edict_t *self);
void MG_Pathfind(edict_t *self, qboolean check_clear_path);
float MG_FaceGoal (edict_t *self, qboolean doturn);
qboolean ok_to_wake (edict_t *monster, qboolean gorgon_roar, qboolean ignore_ambush);
qboolean EqualAngle(float angle1, float angle2, float leniency);
// AI Targeting Globals
qboolean enemy_vis; // TRUE if enemy is visible
qboolean enemy_infront; // TRUE if enemy is in front
int enemy_range; // range from enemy RANGE_MELEE, RANGE_NEAR, RANGE_MID, RANGE_FAR
float enemy_yaw; // ideal yaw to face enemy
//============================================================================
// ****************************************************************************
// ai_trystep
//
// Like other functions to attempt a step, but this will return exactly
// what happened to make it fail.
// ****************************************************************************
int ai_trystep(edict_t *ent, vec3_t move)
{
vec3_t oldorg, neworg, copyorg, end, inf_mins, inf_maxs;
trace_t trace;
float stepsize = 18;
vec3_t test;
int contents;
// try the move
VectorCopy (ent->s.origin, oldorg);
VectorCopy (ent->s.origin, copyorg);
VectorCopy (ent->mins, inf_mins);
VectorCopy (ent->maxs, inf_maxs);
VectorAdd (ent->s.origin, move, neworg);
VectorScale(inf_mins, 2, inf_mins);
VectorScale(inf_maxs, 2, inf_maxs);
// push down from a step height above the wished position
neworg[2] += stepsize;
VectorCopy (neworg, end);
end[2] -= stepsize*2;
gi.trace (neworg, inf_mins, inf_maxs, end, ent, MASK_MONSTERSOLID,&trace);
if (trace.allsolid)
return TRYSTEP_ALLSOLID;
if (trace.startsolid)
{
neworg[2] -= stepsize;
gi.trace (neworg, ent->mins, ent->maxs, end, ent, MASK_MONSTERSOLID,&trace);
if (trace.allsolid || trace.startsolid)
return TRYSTEP_STARTSOLID;
}
// don't go in to water unless only 40% hieght deep
if (ent->waterlevel == 0)
{
test[0] = trace.endpos[0];
test[1] = trace.endpos[1];
test[2] = trace.endpos[2] + ent->mins[2];// + 1;
test[2] += (ent->maxs[2] - ent->mins[2]) * 0.4;
contents = gi.pointcontents(test);
if (contents & MASK_WATER)
return TRYSTEP_INWATER;
}
if (trace.fraction == 1)
{
// if monster had the ground pulled out, go ahead and fall
if ( ent->flags & FL_PARTIALGROUND )
{
VectorAdd (ent->s.origin, move, ent->s.origin);
ent->groundentity = NULL;
return TRYSTEP_OK;
}
// walked off an edge
return TRYSTEP_OFFEDGE;
}
// check point traces down for dangling corners
VectorCopy (trace.endpos, ent->s.origin);
if (!M_CheckBottom (ent))
{
if ( ent->flags & FL_PARTIALGROUND )
{
VectorCopy (oldorg, ent->s.origin);
return TRYSTEP_OK;
}
VectorCopy (oldorg, ent->s.origin);
return TRYSTEP_NOSUPPORT;
}
if ( ent->flags & FL_PARTIALGROUND )
ent->flags &= ~FL_PARTIALGROUND;
ent->groundentity = trace.ent;
ent->groundentity_linkcount = trace.ent->linkcount;
VectorCopy (oldorg, ent->s.origin);
return TRYSTEP_OK;
}
/*
=================
AI_SetSightClient
Called once each frame to set level.sight_client to the
player to be checked for in findtarget.
If all clients are either dead or in notarget, sight_client
will be null.
In coop games, sight_client will cycle between the clients.
=================
*/
void AI_SetSightClient (void)
{
edict_t *ent;
int start, check;
if (level.sight_client == NULL)
start = 1;
else
start = level.sight_client - g_edicts;
check = start;
while (1)
{
check++;
if (check > game.maxclients)
check = 1;
ent = &g_edicts[check];
if (ent->inuse
&& ent->health > 0
&& !(ent->flags & FL_NOTARGET) )
{
level.sight_client = ent;
return; // got one
}
if (check == start)
{
level.sight_client = NULL;
return; // nobody to see
}
}
}
// ****************************************************************************
// ai_maintain_waypoints
//
// checks all waypoints and corrects all buoys
// ****************************************************************************
void ai_maintain_waypoints(edict_t *self, float mintel, float foo1, float foo2)
{
vec3_t vec;
float len;
if (self->enemy)
{
if (visible(self, self->enemy))
{
VectorCopy(self->pos1, self->pos2);
VectorCopy(self->enemy->s.origin, self->pos1);
}
}
if (self->monsterinfo.searchType == SEARCH_COMMON)
return;
else if (self->monsterinfo.searchType == SEARCH_BUOY)
{
VectorSubtract(self->s.origin, self->monsterinfo.nav_goal, vec);
len = VectorLength(vec);
if (len < 24)
{
//gi.dprintf("gkrokon_maintain_waypoints: arrived at target buoy\n");
if ((self->monsterinfo.stepState == PATHDIR_FORWARD && self->goalentity->enemy == NULL) ||
(self->monsterinfo.stepState == PATHDIR_BACKWARD && self->goalentity->owner == NULL))
{
//gi.dprintf("gkrokon_maintain_waypoints: path exhausted, seeking target");
FindTarget(self);
self->monsterinfo.searchType = SEARCH_COMMON;
return;
}
if (self->monsterinfo.stepState == PATHDIR_FORWARD)
{
//gi.dprintf("gkrokon_maintain_waypoints: forward targetting %s\n", self->goalentity->targetname);
self->enemy = self->movetarget = self->goalentity = self->goalentity->enemy;
VectorCopy(self->goalentity->s.origin, self->monsterinfo.nav_goal);
}
else if (self->monsterinfo.stepState == PATHDIR_BACKWARD)
{
//gi.dprintf("gkrokon_maintain_waypoints: reverse targetting %s\n", self->goalentity->owner->targetname);
self->enemy = self->movetarget = self->goalentity = self->goalentity->owner;
VectorCopy(self->goalentity->s.origin, self->monsterinfo.nav_goal);
}
/*else
gi.dprintf("gkrokon_maintain_waypoints: volatile assignment\n");*/
}
}
}
// ****************************************************************************
// ai_hopdown
//
// Checks to see if entity can hop down safely
// ****************************************************************************
int ai_hopdown(edict_t *self, vec3_t goalpos, float height_max)
{
vec3_t vf, source, source2;
vec3_t maxs;
trace_t trace;
//Setup the trace
VectorCopy(self->maxs, maxs);
VectorCopy(self->s.origin, source);
AngleVectors(self->s.angles, vf, NULL, NULL);
VectorMA(source, 128, vf, source);
maxs[2] += 128;
gi.trace (self->s.origin, self->mins, maxs, source, self, MASK_ALL,&trace);
if (trace.fraction == 1)
{
VectorCopy(source, source2);
source2[2] -= height_max;
gi.trace (source, self->mins, self->maxs, source2, self, MASK_ALL,&trace);
if (trace.allsolid || trace.startsolid)
return false;
if (trace.fraction == 1)
{
if (stricmp(trace.ent->classname, "worldspawn"))
return false;
}
else
{
if (trace.ent == (struct edict_s *)-1)
return false;
if (trace.contents != CONTENTS_SOLID)
return false;
else
{
VectorSubtract(trace.endpos, self->s.origin, source2);
VectorNormalize(source2);
self->ideal_yaw = vectoyaw(source2);
return true;
}
}
}
return false;
}
//============================================================================
/*
=============
ai_eat
Monster will eat until harm is done to him or the player moves within range
==============
*/
void ai_eat (edict_t *self, float dist)
{
self->enemy = NULL;
FindTarget (self);
}
/*
=============
ai_move
Move the specified distance at current facing.
This replaces the QC functions: ai_forward, ai_back, ai_pain, and ai_painforward
==============
*/
void ai_move (edict_t *self, float dist)
{
MG_WalkMove (self, self->s.angles[YAW], dist);
}
/*
=============
ai_stand
Used for standing around and looking for players
Distance is for slight position adjustments needed by the animations
==============
*/
void ai_stand (edict_t *self, float dist)
{
vec3_t v;
if (dist)
M_walkmove (self, self->s.angles[YAW], dist);
if(self->enemy)
return;
if (self->monsterinfo.aiflags & AI_STAND_GROUND)
{
if (self->enemy)
{
VectorSubtract (self->enemy->s.origin, self->s.origin, v);
self->ideal_yaw = vectoyaw(v);
if (self->s.angles[YAW] != self->ideal_yaw && self->monsterinfo.aiflags & AI_TEMP_STAND_GROUND)
{
self->monsterinfo.aiflags &= ~(AI_STAND_GROUND | AI_TEMP_STAND_GROUND);
QPostMessage(self, MSG_RUN, PRI_DIRECTIVE, NULL);
}
M_ChangeYaw (self);
ai_checkattack (self, 0);
}
else
FindTarget (self);
return;
}
if (FindTarget (self))
return;
//FIXME: walking a beat monsters, but that may not be working, need to test!
//check for target also?
if (self->monsterinfo.pausetime == -1)
{
self->spawnflags |= MSF_WANDER;
self->ai_mood = AI_MOOD_WANDER;
QPostMessage(self, MSG_CHECK_MOOD, PRI_DIRECTIVE, "i", AI_MOOD_WANDER);
return;
}
else if (level.time > self->monsterinfo.pausetime)
{
self->ai_mood = AI_MOOD_WALK;
QPostMessage(self, MSG_WALK, PRI_DIRECTIVE, NULL);
return;
}
if (!(self->spawnflags & MSF_AMBUSH) && (self->monsterinfo.idle) && (level.time > self->monsterinfo.idle_time))
{
if (self->monsterinfo.idle_time)
{
self->monsterinfo.idle (self);
self->monsterinfo.idle_time = level.time + flrand(15.0F, 30.0F);
}
else
{
self->monsterinfo.idle_time = level.time + flrand(0.0F, 15.0F);
}
}
}
/*
=============
ai_walk
The monster is walking it's beat
=============
*/
qboolean MG_ReachedBuoy (edict_t *self, vec3_t pspot);
void ai_walk (edict_t *self, float dist)
{
if (FindTarget (self))
{
QPostMessage(self, MSG_RUN, PRI_DIRECTIVE, NULL);
return;
}
if(self->monsterinfo.searchType == SEARCH_BUOY)
{
if(!(self->monsterinfo.aiflags&AI_STRAIGHT_TO_ENEMY))
{
MG_BuoyNavigate(self);//only called if already wandering
MG_Pathfind(self, false);
}
}
else if(!self->enemy && !self->pathtarget)
{
if(self->movetarget)
{
if(self->movetarget->classname && strcmp(self->movetarget->classname, "path_corner"))
{
if(MG_ReachedBuoy(self, self->movetarget->s.origin))
{
self->movetarget = NULL;
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
}
}
}
else if(!irand(0, 30) || (Vec3NotZero(self->monsterinfo.nav_goal) && MG_ReachedBuoy(self, self->monsterinfo.nav_goal)))
{
self->monsterinfo.pausetime = 0;
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
}
}
MG_MoveToGoal (self, dist);
}
/*
=============
ai_charge
Turns towards enemy and advances
Use this call with a distnace of 0 to replace ai_face
==============
*/
void ai_charge (edict_t *self, float dist)
{
vec3_t v;
if(!self->enemy)
{
if(MGAI_DEBUG)
gi.dprintf("ERROR: AI_CHARGE at a NULL enemy!\n");
MG_FaceGoal(self, true);//get ideal yaw and turn
return;//send stand MSG?
}
VectorSubtract (self->enemy->s.origin, self->s.origin, v);
self->ideal_yaw = vectoyaw(v);
MG_ChangeYaw (self);
if(self->spawnflags&MSF_FIXED)
return;
if (dist)
MG_WalkMove (self, self->s.angles[YAW], dist);
}
/*
=============
ai_moveright
simple sliding right (or left if dist <0)
==============
*/
void ai_moveright (edict_t *self, float dist)
{
vec3_t right;
float movedir;
if(self->spawnflags&MSF_FIXED)
return;
if (!dist)
return;
AngleVectors (self->s.angles, NULL, right, NULL);
movedir = vectoyaw(right);
MG_WalkMove (self, movedir, dist);
}
/*
=============
ai_goal_charge
Turns towards target and advances
Use this call with a distnace of 0 to replace ai_face
==============
*/
void ai_goal_charge (edict_t *self, float dist)
{
MG_FaceGoal(self, true);
if(self->spawnflags&MSF_FIXED)
return;
if (dist)
MG_WalkMove (self, self->s.angles[YAW], dist);
}
/*
=============
ai_charge2
Turns towards target and advances
Use this call with a distnace of 0 to replace ai_face
==============
*/
void ai_charge2 (edict_t *self, float dist)
{
vec3_t v;
if(!self->enemy)
return;
VectorSubtract (self->enemy->s.origin, self->s.origin, v);
self->ideal_yaw = vectoyaw(v);
MG_ChangeYaw (self);
if(self->spawnflags&MSF_FIXED)
return;
if (dist)
MG_WalkMove (self, self->s.angles[YAW], dist);
}
/*
=============
ai_turn
don't move, but turn towards ideal_yaw
Distance is for slight position adjustments needed by the animations
=============
*/
void ai_turn (edict_t *self, float dist)
{
if(!(self->spawnflags&MSF_FIXED))
if (dist)
M_walkmove (self, self->s.angles[YAW], dist);
if (FindTarget (self))
return;
M_ChangeYaw (self);
}
/*
.enemy
Will be world if not currently angry at anyone.
.movetarget
The next path spot to walk toward. If .enemy, ignore .movetarget.
When an enemy is killed, the monster will try to return to it's path.
.hunt_time
Set to time + something when the player is in sight, but movement straight for
him is blocked. This causes the monster to use wall following code for
movement direction instead of sighting on the player.
.ideal_yaw
A yaw angle of the intended direction, which will be turned towards at up
to 45 deg / state. If the enemy is in view and hunt_time is not active,
this will be the exact line towards the enemy.
.pausetime
A monster will leave it's stand state and head towards it's .movetarget when
time > .pausetime.
walkmove(angle, speed) primitive is all or nothing
*/
int categorize_range (edict_t *self, edict_t *other, float len)
{
float dist;
if (self->monsterinfo.aiflags & AI_EATING) // Eating
{
if (len < MELEE_DISTANCE) // Melee distance
{
if ( len < (self->maxs[0] + other->maxs[0] + 15))
return RANGE_MELEE;
}
if (len < 175) // Attacking distance
return RANGE_NEAR;
if (len < 350) // Hissing distance
return RANGE_MID;
return RANGE_FAR;
}
else // Not eating
{
if (len < MELEE_DISTANCE)
{
if ( len < (self->maxs[0] + other->maxs[0] + 25))
return RANGE_MELEE;
}
if (len < 500)
return RANGE_NEAR;
if(self->wakeup_distance)
dist = self->wakeup_distance;
else
dist = MAX_SIGHT_PLAYER_DIST;
if (len < dist)
return RANGE_MID;
return RANGE_FAR;
}
}
/*
=============
range
returns the range catagorization of an entity reletive to self
0 melee range, will become hostile even if back is turned
1 visibility and infront, or visibility and show hostile
2 infront and show hostile
3 only triggered by damage
=============
*/
int range (edict_t *self, edict_t *other)
{
vec3_t v;
float len;
VectorSubtract (self->s.origin, other->s.origin, v);
len = VectorLength (v);
return categorize_range(self, other, len);
}
/*
=============
visible
returns 1 if the entity is visible to self, even if not infront ()
=============
*/
qboolean visible (edict_t *self, edict_t *other)
{
vec3_t spot1;
vec3_t spot2;
trace_t trace;
if (!other)
return false;
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);
}
VectorCopy (other->s.origin, spot2);
spot2[2] += other->viewheight;
gi.trace (spot1, vec3_origin, vec3_origin, spot2, self, MASK_OPAQUE,&trace);
if (trace.fraction == 1.0)
return true;
return false;
}
/*
=============
clear_visible
returns 1 if the entity is visible, but not through transparencies
=============
*/
qboolean clear_visible (edict_t *self, edict_t *other)
{
vec3_t spot1;
vec3_t spot2;
trace_t trace;
if (!other)
return false;
if (!self)
return false;
VectorCopy (self->s.origin, spot1);
spot1[2] += self->viewheight;
VectorCopy (other->s.origin, spot2);
spot2[2] += other->viewheight;
gi.trace (spot1, vec3_origin, vec3_origin, spot2, self, MASK_SOLID,&trace);
if (trace.fraction == 1.0)
return true;
return false;
}
/*
=============
infront
returns 1 if the entity is in front (in sight) of self
=============
*/
qboolean infront (edict_t *self, edict_t *other)
{
vec3_t vec;
float dot;
vec3_t forward, check_angles;
if(Vec3NotZero(self->v_angle_ofs))
VectorAdd(self->v_angle_ofs,self->s.angles,check_angles);
else
VectorCopy(self->s.angles,check_angles);
AngleVectors (check_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;
}
//============================================================================
/*
Alerted
Checks and see if an alert entity is capable of waking up a monster
*/
void alert_timed_out(alertent_t *self);
qboolean Alerted (edict_t *self)
{//This alert entity wakes up monsters, let's see what's up...
edict_t *enemy = NULL;
alertent_t *alerter;
vec3_t e_spot, viewspot, dir;
qboolean saw_it = false;
float dist;
if(self->monsterinfo.aiflags&AI_NO_ALERT)
return false;
//start the search from the most recent alert to the oldest
alerter = level.last_alert;//OOPS, SKIPS LAST
alertloop:
if(self->enemy)
goto loopagain;
//alerter is gone
if(!alerter)
goto loopagain;
if(!alerter->inuse)//loading a saved game invalidates all alerts
goto loopagain;
if(alerter->lifetime < level.time)
{//alert timed out, remove from list
alert_timed_out(alerter);
goto loopagain;
}
if(self->last_alert)//don't be woken up by the same alert twice
{
if(self->last_alert->inuse)
{
if(alerter == self->last_alert && self->last_alert->inuse)
goto loopagain;
}
else
self->last_alert = NULL;
}
//alerter's enemy is gone
if(!alerter->enemy)
goto loopagain;
if(alerter->enemy->client)
{//no alerts for notarget players
if(alerter->enemy->flags & FL_NOTARGET)
goto loopagain;
}
//NEVER alert ambush monsters?
//if(!(self->spawnflags & MSF_AMBUSH))
// goto loopagain;
if(!(self->svflags&SVF_MONSTER))
goto loopagain;
if(self->health<=0)
goto loopagain;
//eating or in a cinematic or not awake, leave them alone
if(!ok_to_wake(self, false, true))
goto loopagain;
//should we keep track of owner in case they move to move the alert with them? Only for monsters
//if(alerter->owner)
// VectorCopy(alerter->owner->s.origin, alerter->s.origin);
VectorSubtract(self->s.origin, alerter->origin, dir);
dist = VectorLength(dir);
//if monster's wakeup_distance is shorter than dist to alerter, leave it alone
if(dist > self->wakeup_distance)
goto loopagain;
//closer means better chance to alert
//problem - different alerts might be more likely to be heard/seen...
if(dist > flrand(100, self->wakeup_distance))//if within 100 always wake up?
goto loopagain;
//if not a player, a player's missile or a monster, goto loopagain?
//get center of alert enemy
enemy = alerter->enemy;
VectorAdd(enemy->s.origin, enemy->mins, e_spot);
VectorMA(e_spot, 0.5, enemy->size, e_spot);
//get my view spot
VectorCopy(self->s.origin, viewspot);
viewspot[2] += self->viewheight;
//if being alerted by a monster and not waiting to ambush
if(alerter->alert_svflags&SVF_MONSTER && !(self->spawnflags&MSF_AMBUSH))
{//can "see" the owner of the alert even around a corner
saw_it = gi.inPVS(e_spot, viewspot);
}
else
{//alerter not a monster, a projectile or player
//can I see (even through translucent brushes) the owner of the alert?
//if so, ok even if I'm an ambush monster
saw_it = visible_pos(self, e_spot);
if(!saw_it&&!(self->spawnflags&MSF_AMBUSH))
{//no line of sight and not an ambush monster
if(gi.inPVS(viewspot, alerter->origin))
{//25% chance will see impact(alerter) and detect alert owner anyway
if(!irand(0,3))
saw_it = true;//go ahead and go for it
}
}
}
if(!saw_it)
goto loopagain;
if(!self->monsterinfo.alert)
goto loopagain;
self->last_alert = alerter;
return self->monsterinfo.alert(self, alerter, enemy);
loopagain:
if(alerter)
{
alerter = alerter->prev_alert;
if(alerter)
goto alertloop;
}
return false;
}
/*
===========
HuntTarget - a target has been found, it is visible, so do we attack it, watch it, or stand there?
============
*/
void HuntTarget (edict_t *self)
{
vec3_t vec;
int r;
self->goalentity = self->enemy;
if (self->monsterinfo.aiflags & AI_STAND_GROUND)
{
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
}
else
{
r = range(self,self->enemy);
if ((self->monsterinfo.aiflags & AI_EATING) && (r == RANGE_MID))
{
QPostMessage(self, MSG_WATCH, PRI_DIRECTIVE, NULL);
}
else
{
QPostMessage(self, MSG_RUN, PRI_DIRECTIVE, NULL);
}
}
VectorSubtract (self->enemy->s.origin, self->s.origin, vec);
self->ideal_yaw = vectoyaw(vec);
// wait a while before first attack
if (!(self->monsterinfo.aiflags & AI_STAND_GROUND))
AttackFinished (self, 1);
}
/*
===========
FoundTarget - a target has been found, let other monsters know this. Then hunt it
============
*/
void FoundTarget (edict_t *self, qboolean setsightent)
{
char *o_target;
// let other monsters see this monster for a while
if (!self->enemy)
return;
if(self->classID == CID_OGLE)
self->spawnflags = 0;
self->monsterinfo.awake = true;
self->spawnflags &= ~MSF_AMBUSH;
self->targetname = NULL;
self->monsterinfo.pausetime = -1;//was 0;
if(self->wakeup_target)
{
o_target = self->target;
self->target = self->wakeup_target;
G_UseTargets(self, self->enemy);
self->target = o_target;
self->wakeup_target = NULL;
}
if (self->enemy->client && setsightent)
{
level.sight_entity = self;
level.sight_entity_framenum = level.framenum;
level.sight_entity->light_level = 128;
}
self->show_hostile = level.time + 1; // wake up other monsters
VectorCopy(self->enemy->s.origin, self->monsterinfo.last_sighting);
if (!self->combattarget ) // Not going for a combat point?
{
// dont want to do this if we are a fish
if (self->classID != CID_FISH)
HuntTarget (self);
if (!self->oldenemy)
{
if (!(self->monsterinfo.aiflags & AI_SOUND_TARGET) )
QPostMessage(self, MSG_VOICE_SIGHT, PRI_DIRECTIVE, "be", SIGHT_SOUND_TARGET, self->enemy);
else
QPostMessage(self, MSG_VOICE_SIGHT, PRI_DIRECTIVE, "be", SIGHT_VISIBLE_TARGET, self->enemy);
}
self->spawnflags &= ~MSF_AMBUSH;
return;
}
self->goalentity = self->movetarget = G_PickTarget(self->combattarget);
if (!self->movetarget)
{
self->goalentity = self->movetarget = self->enemy;
// dont want to do this if we are a fish
if (self->classID != CID_FISH)
HuntTarget (self);
if (!self->oldenemy)
{
if (!(self->monsterinfo.aiflags & AI_SOUND_TARGET) )
QPostMessage(self, MSG_VOICE_SIGHT, PRI_DIRECTIVE, "be", SIGHT_SOUND_TARGET, self->enemy);
else
QPostMessage(self, MSG_VOICE_SIGHT, PRI_DIRECTIVE, "be", SIGHT_VISIBLE_TARGET, self->enemy);
}
// gi.dprintf("%s at %s, combattarget %s not found\n", self->classname, vtos(self->s.origin), self->combattarget);
self->spawnflags &= ~MSF_AMBUSH;
return;
}
// clear out our combattarget, these are a one shot deal
self->combattarget = NULL;
self->monsterinfo.aiflags |= AI_COMBAT_POINT;
// clear the targetname, that point is ours!
self->movetarget->targetname = NULL;
// run for it , assuming we aren't a fish
if (self->classID != CID_FISH)
QPostMessage(self, MSG_RUN, PRI_DIRECTIVE, NULL);
//Make a sight sound
if (!self->oldenemy)
{
if (!(self->monsterinfo.aiflags & AI_SOUND_TARGET) )
QPostMessage(self, MSG_VOICE_SIGHT, PRI_DIRECTIVE, "be", SIGHT_SOUND_TARGET, self->enemy);
else
QPostMessage(self, MSG_VOICE_SIGHT, PRI_DIRECTIVE, "be", SIGHT_VISIBLE_TARGET, self->enemy);
}
self->spawnflags &= ~MSF_AMBUSH;
}
/*
qboolean ok_to_wake (edict_t *monster, qboolean gorgon_roar)
Can this monster be woken up by something other than direct line of sight to player
*/
qboolean ok_to_wake (edict_t *monster, qboolean gorgon_roar, qboolean ignore_ambush)
{
if(gorgon_roar)
{
if(monster->monsterinfo.c_mode)
return false;
}
else if(monster->monsterinfo.aiflags & AI_EATING ||//eating or perching
monster->targetname ||//a monster that's supposed to be triggered - problem, one a monster is used and woken up, won't respond to alerts like others...?
monster->monsterinfo.c_mode ||//cinematic
monster->spawnflags & MSF_ASLEEP ||//shouldn't happen, but just in case
(monster->spawnflags & MSF_AMBUSH && !ignore_ambush))
return false;
return true;
}
qboolean PlayerCreeping(playerinfo_t *playerinfo)
{
if(playerinfo->upperseq == ASEQ_CREEPF ||
playerinfo->upperseq == ASEQ_STAND ||
playerinfo->upperseq == ASEQ_CREEPB ||
playerinfo->upperseq == ASEQ_CREEPB_END ||
playerinfo->upperseq == ASEQ_CROUCH_GO ||
playerinfo->upperseq == ASEQ_CROUCH ||
playerinfo->upperseq == ASEQ_CROUCH_END ||
playerinfo->upperseq == ASEQ_CROUCH_WALK_F ||
playerinfo->upperseq == ASEQ_CROUCH_WALK_B ||
playerinfo->upperseq == ASEQ_CROUCH_WALK_L ||
playerinfo->upperseq == ASEQ_CROUCH_WALK_R ||
playerinfo->lowerseq == ASEQ_CREEPF ||
playerinfo->lowerseq == ASEQ_STAND ||
playerinfo->lowerseq == ASEQ_CREEPB ||
playerinfo->lowerseq == ASEQ_CREEPB_END ||
playerinfo->lowerseq == ASEQ_CROUCH_GO ||
playerinfo->lowerseq == ASEQ_CROUCH ||
playerinfo->lowerseq == ASEQ_CROUCH_END||
playerinfo->lowerseq == ASEQ_CROUCH_WALK_F ||
playerinfo->lowerseq == ASEQ_CROUCH_WALK_B ||
playerinfo->lowerseq == ASEQ_CROUCH_WALK_L ||
playerinfo->lowerseq == ASEQ_CROUCH_WALK_R)
return (true);
return (false);
}
/*
===========
FindTarget
Self is currently not attacking anything, so try to find a target
Returns TRUE if an enemy was sighted
When a player fires a missile or does other things to make noise, the
point of impact becomes an alertent so that monsters that see the
impact will respond as if they had seen the player.
Since FindTarget is not called every frame for monsters (average
about once every 3 frames per monster), this does two potential checks.
First it checks against the current sight_client which cycles through
the players.
If that check fails, it will check for all the secondary alerts and
enemies. if it can't find any, it will check for another player if
it can find one other than the first one it checked.
============
*/
qboolean ogle_findtarget (edict_t *self);
qboolean FindTarget (edict_t *self)
{
edict_t *client, *firstclient;
qboolean heardit = false;
int r;
edict_t *ent;
int flag;
qboolean clientonly = true;
qboolean e_infront = false;
vec3_t v;
float dist;
//FIXME: wakeup_distance -1 never look?
if(self->classID == CID_OGLE)
return ogle_findtarget(self);
if (self->monsterinfo.aiflags & AI_GOOD_GUY)
{
if (self->goalentity)
{
if (strcmp(self->goalentity->classname, "target_actor") == 0)
return false;
}
//FIXME look for monsters?
return false;
}
// if we're going to a combat point, just proceed
if (self->monsterinfo.aiflags & AI_COMBAT_POINT)
return false;
if(self->ai_mood_flags&AI_MOOD_FLAG_IGNORE_ENEMY)
{//being forced to use buoys, and ignore enemy until get to forced_buoy
return false;
}
// if the first spawnflag bit is set, the monster will only wake up on
// really seeing the player, not another monster getting angry or hearing
// something
// revised behavior so they will wake up if they "see" a player make a noise
// but not weapon impact/explosion noises
startcheck:
flag = 1;
if(clientonly)
{//look oly at the level.sight_client
firstclient = client = level.sight_client;
}
else
{
if(ANARCHY)
{//crazy monsters mode
int checkcnt = 0;
client = self;
while((!client || !client->inuse || !(client->svflags & SVF_MONSTER)||client->health<=0||client == self) && checkcnt < globals.num_edicts)
{
client = &g_edicts[irand(0, globals.num_edicts)];
checkcnt++;
}
}
else
{
if(level.sight_entity == self)
{
level.sight_entity = NULL;
return false;
}
if (level.sight_entity && (level.sight_entity_framenum >= (level.framenum - 1)) && !(self->spawnflags & MSF_AMBUSH) )
{//go after the enemy another monster saw saw, but only if not in ambush
client = level.sight_entity;
if (client->enemy == self->enemy)
{
return false;
}
}
else if (Alerted(self))
{//picked up an enemy from an alert
return true;
}
else
{
client = NULL;
// Looking for secondary enemies
if ((self->monsterinfo.otherenemyname) && (self->monsterinfo.chase_finished < level.time))
{
ent = NULL;
while((ent=findradius(ent,self->s.origin,175)) != NULL)
{
if (!strcmp(ent->classname,self->monsterinfo.otherenemyname)&&ent!=self)
{
flag = 0;
client = ent;
break;
}
}
}
// Look at the sight client
if (!client)
{//found no non-clients, cycle to next client and check it for second check
AI_SetSightClient();
if(firstclient == level.sight_client)
return false;//same as first check, and that failed if we're here, so return.
client = level.sight_client;
}
}
}
}
if (!client)
goto nextcheck; // no clients to get mad at
// if the entity went away, forget it
if (!client->inuse)
goto nextcheck;
if (client == self)
goto nextcheck; //????
if (client == self->enemy)
return true; // JDC false;
if (self->monsterinfo.otherenemyname)
{
if (!strcmp(client->classname,self->monsterinfo.otherenemyname))
client->light_level = 128; // Let it be seen
}
// if we are a fish - is the target in the water - have to be at least waist deep?
if (self->classID == CID_FISH && client->waterlevel < 2)
goto nextcheck; //????
if (client->client)
{
if (client->flags & FL_NOTARGET)
goto nextcheck;
}
else if (client->svflags & SVF_MONSTER)
{
if(flag)
{//not a secondary enemy
if(!ANARCHY)
{
if (ok_to_wake(self, false, true))
{//eating or in a cinematic or not awake or targeted, leave them alone
goto nextcheck;
}
}
if (!client->enemy)
{
if(!ANARCHY)
goto nextcheck;
}
else
{
if (client->enemy->health<0 && !ANARCHY)
goto nextcheck;
if (client->enemy->flags & FL_NOTARGET)
goto nextcheck;
}
if(!visible(self, client))
goto nextcheck;
if(!ANARCHY)
self->enemy = client->enemy;
else
self->enemy = client;
if(client->ai_mood == AI_FLEE)
FoundTarget(self, false);//let them stay the sight entity
else
FoundTarget(self, true);//make me the sight entity
/*if (!(self->monsterinfo.aiflags & AI_SOUND_TARGET))
QPostMessage(self, MSG_VOICE_SIGHT, PRI_DIRECTIVE, "e", self->enemy);*/
//self->monsterinfo.sight (self, self->enemy);
return true;
}
}
else if (heardit)
{
if (client->owner->flags & FL_NOTARGET)
goto nextcheck;
}
else
goto nextcheck;
if (!heardit)
{
if(self->classID == CID_ASSASSIN)
e_infront = true;
else
e_infront = infront(self, client);
if(!e_infront && client->client)
{
if(PlayerCreeping(&client->client->playerinfo))
goto nextcheck;
}
VectorSubtract(client->s.origin, self->s.origin, v);
dist = VectorLength(v);
if(dist > self->wakeup_distance)
goto nextcheck;
r = categorize_range (self, client, dist);
if (r == RANGE_FAR)
goto nextcheck;
if ((self->monsterinfo.aiflags & AI_EATING) && (r > RANGE_MID))
{
self->enemy = client;
goto nextcheck;
}
// this is where we would check invisibility
// is client in an spot too dark to be seen?
if (client->light_level <= 5)
goto nextcheck;
if(self->svflags&SVF_MONSTER && client->client)
{
if(skill->value < 2.0 && !(self->monsterinfo.aiflags & AI_NIGHTVISION))
{
if(client->light_level < flrand(6, 77))
{
goto nextcheck;
}
}
}
if (r == RANGE_NEAR)
{
if (client->show_hostile < level.time && !e_infront)
{
goto nextcheck;
}
}
else if (r == RANGE_MID)
{
if (!e_infront)
{
goto nextcheck;
}
}
//sfs--this check is much less trivial than infront: prolly wasn't a noticeable deal,
// since RANGE_FAR was first rejection check, but it's still better to try the
// dotproduct before the traceline
if (!visible (self, client))
{
goto nextcheck;
}
self->enemy = client;
flag=1;
if (self->monsterinfo.otherenemyname)
{
if (strcmp(self->enemy->classname, self->monsterinfo.otherenemyname) == 0) // This is a secondary enemy
{
self->monsterinfo.chase_finished = level.time + 15;
flag=0;
}
}
if (flag) // This is not a secondary enemy
{
self->monsterinfo.aiflags &= ~AI_SOUND_TARGET;
if (!self->enemy->client)
{
self->enemy = self->enemy->enemy;
if (!self->enemy->client)
{
self->enemy = NULL;
goto nextcheck;
}
}
}
}
else // heardit
{
goto nextcheck;
}
//
// got one
//
FoundTarget (self, true);
return true;
nextcheck:
if(clientonly)
{
clientonly = false;
goto startcheck;
}
else
return false;
}
//=============================================================================
/*
============
FacingIdeal
============
*/
qboolean FacingIdeal(edict_t *self)
{
float delta;
delta = anglemod(self->s.angles[YAW] - self->ideal_yaw);
if (delta > 45 && delta < 315)
return false;
return true;
}
//=============================================================================
qboolean M_CheckAttack (edict_t *self)
{
vec3_t spot1, spot2;
float chance;
trace_t tr;
if (self->enemy->health > 0)
{
// see if any entities are in the way of the shot
VectorCopy (self->s.origin, spot1);
spot1[2] += self->viewheight;
VectorCopy (self->enemy->s.origin, spot2);
spot2[2] += self->enemy->viewheight;
gi.trace (spot1, vec3_origin, vec3_origin, spot2, self, CONTENTS_SOLID|CONTENTS_MONSTER|CONTENTS_SLIME|CONTENTS_LAVA,&tr);
// do we have a clear shot?
if (tr.ent != self->enemy)
return false;
}
// melee attack
if (enemy_range == RANGE_MELEE)
{
// don't always melee in easy mode
if (skill->value == 0 && irand(0, 3) )
return false;
if (classStatics[self->classID].msgReceivers[MSG_MELEE])
{
self->monsterinfo.attack_state = AS_MELEE;
}
else
{
self->monsterinfo.attack_state = AS_MISSILE;
}
return true;
}
// missile attack
if (!classStatics[self->classID].msgReceivers[MSG_MISSILE])
return false;
if (level.time < self->monsterinfo.attack_finished)
return false;
if (enemy_range == RANGE_FAR)
return false;
if (self->monsterinfo.aiflags & AI_STAND_GROUND)
{
chance = 0.4;
}
else if (enemy_range == RANGE_MELEE)
{
chance = 0.2;
}
else if (enemy_range == RANGE_NEAR)
{
chance = 0.1;
}
else if (enemy_range == RANGE_MID)
{
chance = 0.02;
}
else
{
return false;
}
if (skill->value == 0)
chance *= 0.5;
else if (skill->value >= 2)
chance *= 2;
if (flrand(0.0, 1.0) < chance)
{
self->monsterinfo.attack_state = AS_MISSILE;
self->monsterinfo.attack_finished = level.time + flrand(0.0, 2.0);
return true;
}
if (self->flags & FL_FLY)
{
if (!irand(0, 2))
self->monsterinfo.attack_state = AS_SLIDING;
else
self->monsterinfo.attack_state = AS_STRAIGHT;
}
return false;
}
/*
=============
ai_run_melee
Turn and close until within an angle to launch a melee attack
=============
*/
void ai_run_melee(edict_t *self)
{
self->ideal_yaw = enemy_yaw;
M_ChangeYaw (self);
if (FacingIdeal(self))
{
QPostMessage(self, MSG_MELEE, PRI_DIRECTIVE, NULL);
self->monsterinfo.attack_state = AS_STRAIGHT;
}
}
/*
=============
ai_run_missile
Turn in place until within an angle to launch a missile attack
=============
*/
void ai_run_missile(edict_t *self)
{
self->ideal_yaw = enemy_yaw;
M_ChangeYaw (self);
if (FacingIdeal(self))
{
QPostMessage(self, MSG_MISSILE, PRI_DIRECTIVE, NULL);
self->monsterinfo.attack_state = AS_STRAIGHT;
}
};
/*
=============
ai_run_slide
Strafe sideways, but stay at aproximately the same range
=============
*/
void ai_run_slide(edict_t *self, float distance)
{
float ofs;
self->ideal_yaw = enemy_yaw;
M_ChangeYaw (self);
if (self->monsterinfo.lefty)
ofs = 90;
else
ofs = -90;
if (M_walkmove (self, self->ideal_yaw + ofs, distance))
return;
self->monsterinfo.lefty = 1 - self->monsterinfo.lefty;
M_walkmove (self, self->ideal_yaw - ofs, distance);
}
/*
=============
ai_checkattack
Decides if we're going to attack or do something else
used by ai_run, and ai_stand
=============
*/
qboolean ai_checkattack (edict_t *self, float dist)
{
vec3_t temp;
qboolean hesDeadJim;
if ((self->monsterinfo.aiflags & AI_FLEE)||(self->monsterinfo.aiflags & AI_COWARD)) // He's running away, not attacking
{
return false;
}
enemy_vis = false;
// see if the enemy is dead
hesDeadJim = false;
if ((!self->enemy) || (!self->enemy->inuse))
{
hesDeadJim = true;
}
else
{
if (self->monsterinfo.aiflags & AI_BRUTAL)
{
if (self->enemy->health <= -80)
hesDeadJim = true;
}
else
{
if (self->enemy->health <= 0)
hesDeadJim = true;
}
}
if (hesDeadJim)
{
self->enemy = NULL;
// FIXME: look all around for other targets
if (self->oldenemy && self->oldenemy->health > 0)
{
self->enemy = self->oldenemy;
self->oldenemy = NULL;
HuntTarget (self);
}
else
{
if (self->movetarget)
{
self->goalentity = self->movetarget;
QPostMessage(self, MSG_WALK, PRI_DIRECTIVE, NULL);
}
else
{
// 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;
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
}
return true;
}
}
self->show_hostile = level.time + 1; // wake up other monsters
// check knowledge of enemy
enemy_vis = clear_visible(self, self->enemy);
if (enemy_vis)
{
self->monsterinfo.search_time = level.time + 5;
VectorCopy (self->enemy->s.origin, self->monsterinfo.last_sighting);
}
// look for other coop players here
// if (coop && self->monsterinfo.search_time < level.time)
// {
// if (FindTarget (self))
// return true;
// }
enemy_infront = infront(self, self->enemy);
enemy_range = range(self, self->enemy);
VectorSubtract (self->enemy->s.origin, self->s.origin, temp);
enemy_yaw = vectoyaw(temp);
// JDC self->ideal_yaw = enemy_yaw;
if (self->monsterinfo.attack_state == AS_MISSILE)
{
ai_run_missile (self);
return true;
}
if (self->monsterinfo.attack_state == AS_MELEE)
{
ai_run_melee (self);
return true;
}
// if enemy is not currently visible, we will never attack
if (!enemy_vis)
return false;
return self->monsterinfo.checkattack (self);
}
qboolean ai_inpack(edict_t *self)
{
edict_t *ent;
ent = NULL;
while ((ent = findradius(ent, self->s.origin, 1024)) != NULL)
{
if (ent == self)
continue;
if (stricmp(ent->classname, self->classname))
continue;
return true;
}
return false;
}
/*
=============
ai_runaway
The monster has an enemy it is trying to flee
=============
*/
void ai_runaway (edict_t *self, float dist)
{
vec3_t move;
vec3_t vec, source, vf;
qboolean goleft = false;
vec3_t mins, maxs;
trace_t trace;
vec3_t na, nvr;
float yaw;
int ret;
if (!self->enemy)
return;
// gi.dprintf("%s running away from %s!\n", self->classname, self->enemy->classname);
//Setup the trace
VectorCopy(self->mins, mins);
VectorCopy(self->maxs, maxs);
VectorCopy(self->s.origin, source);
AngleVectors(self->s.angles, vf, NULL, NULL);
VectorMA(source, dist*2, vf, source);
//Account for STEPSIZE
mins[2] += 18;
gi.trace (self->s.origin, mins, self->maxs, source, self, MASK_SHOT,&trace);
//We hit something
if (trace.fraction < 1)
{
vectoangles(trace.plane.normal,na);
AngleVectors(na,NULL,nvr,NULL);
if(DotProduct(nvr,vf)>0)
self->ideal_yaw=vectoyaw(nvr);
else
{
VectorScale(nvr, -1, nvr);
self->ideal_yaw=vectoyaw(nvr);
}
VectorCopy(self->mins, mins);
VectorCopy(self->maxs, maxs);
VectorCopy(self->s.origin, source);
AngleVectors(self->s.angles, vf, NULL, NULL);
VectorMA(source, dist*4, nvr, source);
//Account for STEPSIZE
mins[2] += 18;
gi.trace (self->s.origin, mins, self->maxs, source, self, MASK_SHOT,&trace);
if (trace.fraction < 1)
{
//gi.dprintf("Failed next move!\n");
VectorScale(nvr, -1, nvr);
self->ideal_yaw=vectoyaw(nvr);
}
M_ChangeYaw (self);
if (abs(self->s.angles[YAW] - self->ideal_yaw) < self->yaw_speed)
{
yaw = self->s.angles[YAW];
yaw = yaw*M_PI*2 / 360;
move[0] = cos(yaw)*dist * 4;
move[1] = sin(yaw)*dist * 4;
move[2] = 0;
ret = ai_trystep(self, move);
if ((ret != TRYSTEP_OK) && (self->monsterinfo.idle_time < level.time))
{
self->monsterinfo.idle_time = level.time + flrand(1.0, 2.0);
SV_NewChaseDir(self, self->enemy, dist);
}
else if (!M_walkmove (self, self->s.angles[YAW], dist))
{
self->monsterinfo.idle_time = level.time + flrand(1.0, 2.0);
SV_NewChaseDir(self, self->enemy, dist);
}
}
return;
}
VectorSubtract(self->enemy->s.origin, self->s.origin, vec);
if (self->monsterinfo.idle_time < level.time)
{
self->ideal_yaw = vectoyaw(vec);
self->ideal_yaw += 180;
M_ChangeYaw (self);
}
if (dist) //Going somewhere?
{
if(self->classID == CID_SSITHRA)
ssithraCheckJump(self);
if (!M_walkmove (self, self->s.angles[YAW], dist))
{
if (self->monsterinfo.searchType == SEARCH_BUOY)
return;
yaw = self->s.angles[YAW];
yaw = yaw*M_PI*2 / 360;
move[0] = cos(yaw)*dist;
move[1] = sin(yaw)*dist;
move[2] = 0;
ret = ai_trystep(self, move);
if ((ret != TRYSTEP_OK) && (self->monsterinfo.idle_time < level.time))
{
self->monsterinfo.idle_time = level.time + flrand(1.0, 2.0);
SV_NewChaseDir(self, self->enemy, dist);
}
else
{
M_ChangeYaw(self);
if (!M_walkmove (self, self->s.angles[YAW], dist/2)&&(self->classID != CID_SSITHRA))
{
if (ai_hopdown(self, self->enemy->s.origin, 1024))
{
if (self->monsterinfo.jump_time < level.time)
{
//FIXME: Re-implement this!
//gkrokon_hopdown(self);
self->monsterinfo.jump_time = level.time + 1;
}
else return;
}
}
}
}
}
}
float ai_face_goal (edict_t *self)
{
vec3_t vec;
if (self->monsterinfo.searchType == SEARCH_BUOY)
VectorSubtract(self->monsterinfo.nav_goal, self->s.origin, vec);
else if(self->goalentity)
VectorSubtract(self->goalentity->s.origin, self->s.origin, vec);
else if(self->enemy)
VectorSubtract(self->enemy->s.origin, self->s.origin, vec);
else
return false;
if (self->monsterinfo.idle_time < level.time)
{
self->ideal_yaw = vectoyaw(vec);
return M_ChangeYaw(self);
}
else
return false;
}
void old_ai_run (edict_t *self, float dist)
{//FIXME: only ssithra in water use this, let;s junk it!
vec3_t move;
vec3_t vec, source, vf;
qboolean goleft = false;
vec3_t mins, maxs;
trace_t trace;
vec3_t na, nvr;//, v;
float yaw, turnamt, distloss;//,bbox
int ret, oldyaw;
if (!self->enemy)
return;
if(self->classID == CID_GORGON)
{
if(visible(self, self->enemy))
{
if(Vec3IsZero(self->enemy->velocity))
{
VectorCopy(self->enemy->s.origin, self->pos1);
}
else
VectorClear(self->pos1);
}
else
VectorClear(self->pos1);
}
if (self->monsterinfo.attack_state == AS_SLIDING)
{
ai_run_slide (self, dist);
return;
}
// He's done fleeing, time to stand and see what's happening
if ((self->monsterinfo.aiflags & AI_FLEE) && (self->monsterinfo.flee_finished < level.time))
{
self->monsterinfo.aiflags &= ~AI_FLEE;
if(irand(0,10<2))
{
self->enemy = NULL;
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
}
return;
}
if (self->monsterinfo.aiflags & AI_FLEE||self->monsterinfo.aiflags & AI_COWARD)
{
VectorSubtract (self->enemy->s.origin, self->s.origin, vec);
self->ideal_yaw = vectoyaw(vec);
self->ideal_yaw += 180;
ai_runaway(self, dist);
return;
}
if (self->monsterinfo.aiflags & AI_STAND_GROUND)
{
turnamt = Q_fabs(ai_face_goal(self));
return;
}
if (self->monsterinfo.aiflags & AI_SOUND_TARGET)
{
self->monsterinfo.aiflags &= ~AI_SOUND_TARGET;
if (!FindTarget (self))
{
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
return;
}
}
if (!self->enemy)
{
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
return;
}
// ai_maintain_waypoints(self, 0, 0, 0);
//Setup the trace
VectorCopy(self->mins, mins);
VectorCopy(self->maxs, maxs);
VectorCopy(self->s.origin, source);
AngleVectors(self->s.angles, vf, NULL, NULL);
/* if (self->maxs[0] > self->maxs[1]) // Add in monster's bounding box
bbox = self->maxs[0];
else
bbox = self->maxs[1];*/
VectorMA(source, (dist*2)/*+bbox*/, vf, source);
//Account for STEPSIZE
mins[2] += 18;
gi.trace (self->s.origin, mins, self->maxs, source, self, MASK_SHOT,&trace);
//We hit something
if (trace.fraction < 1)
{
if (trace.ent == self->enemy)
{
QPostMessage(self, MSG_MELEE, PRI_DIRECTIVE, NULL);
return;
}
else if(self->monsterinfo.otherenemyname)
{
if(!stricmp(trace.ent->classname, self->monsterinfo.otherenemyname))//&&irand(0,10)<3)
{
QPostMessage(self, MSG_MELEE, PRI_DIRECTIVE, NULL);
return;
}
}
if(trace.ent->takedamage && self->monsterinfo.aiflags & AI_SHOVE && trace.fraction <= 0.5)
{//hurt them some too?
if(self->classID == CID_GORGON &&
self->s.scale >1.5 &&
trace.ent->classID!=self->classID &&
trace.ent->health <= 200&&
infront(self, self->enemy))//chomp!
{
// gi.dprintf("Chomp!\n");
self->oldenemy = self->enemy;
self->enemy = trace.ent;
QPostMessage(self, MSG_MELEE, PRI_DIRECTIVE, NULL);
}
else
{
// gi.dprintf("Shove!\n");
VectorSubtract(trace.ent->s.origin,self->s.origin,vec);
if(vec[2]<30)
vec[2] = 30;
VectorNormalize(vec);
VectorMA(trace.ent->velocity,200,vec,trace.ent->velocity);
trace.ent->groundentity = NULL;
}
}
vectoangles(trace.plane.normal,na);
AngleVectors(na,NULL,nvr,NULL);
if(DotProduct(nvr,vf)>0)
{
self->ideal_yaw=vectoyaw(nvr);
}
else
{
VectorScale(nvr, -1, nvr);
self->ideal_yaw=vectoyaw(nvr);
}
VectorCopy(self->mins, mins);
VectorCopy(self->maxs, maxs);
VectorCopy(self->s.origin, source);
AngleVectors(self->s.angles, vf, NULL, NULL);
VectorMA(source, dist, nvr, source);//was 4?
//Account for STEPSIZE
mins[2] += 18;
gi.trace (self->s.origin, mins, self->maxs, source, self, MASK_SOLID,&trace);//was MASK_SHOT
if (trace.fraction < 1||trace.allsolid||trace.startsolid)
{
VectorScale(nvr, -1, nvr);
self->ideal_yaw=vectoyaw(nvr);
}
oldyaw = self->s.angles[YAW];
turnamt = Q_fabs(M_ChangeYaw(self));
if (abs(anglemod(self->s.angles[YAW] - self->ideal_yaw)) < self->yaw_speed)
{
yaw = self->s.angles[YAW];
yaw = yaw*M_PI*2 / 360;
move[0] = cos(yaw)*dist * 2;//4
move[1] = sin(yaw)*dist * 2;//4
move[2] = 0;
ret = ai_trystep(self, move);
if ((ret != TRYSTEP_OK) && (self->monsterinfo.idle_time < level.time))
{
self->ideal_yaw = -vectoyaw(nvr);
turnamt = Q_fabs(M_ChangeYaw(self));
distloss = turnamt/self->yaw_speed * 0.3;
dist -= (dist * distloss);
if (!M_walkmove (self, self->s.angles[YAW], dist))
{
self->monsterinfo.idle_time = level.time + flrand(0.5, 1.2);
SV_NewChaseDir(self, self->enemy, dist);
}
}
else
{
distloss = turnamt/self->yaw_speed * 0.3;
dist -= (dist * distloss);
M_walkmove (self, self->s.angles[YAW], dist);
}
//actually tried move
return;
}
}
else
turnamt = Q_fabs(ai_face_goal(self));
//greater the turn, lesser the dist, full yawspeed turn is 50% dist
//FIXME: also make this dist less if turn is high and close to enemy?
//(so you don't run around it in a circle)?
distloss = turnamt/self->yaw_speed * 0.3;
dist -= (dist * distloss);
if (dist) //Going somewhere?
{
if(self->classID == CID_SSITHRA)
ssithraCheckJump(self);
if (!M_walkmove (self, self->s.angles[YAW], dist))
{
yaw = self->s.angles[YAW];
yaw = yaw*M_PI*2 / 360;
move[0] = cos(yaw)*dist;
move[1] = sin(yaw)*dist;
move[2] = 0;
ret = ai_trystep(self, move);
if ((ret != TRYSTEP_OK) && (self->monsterinfo.idle_time < level.time))
{
//Jump down?
if ((self->enemy->s.origin[2] < self->s.origin[2])&&(self->classID != CID_SSITHRA))
{
if (ai_hopdown(self, self->enemy->s.origin, 1024))
{
if (self->monsterinfo.jump_time < level.time)
{
self->monsterinfo.jump_time = level.time + 1;
return;
}
else return;
}
else
{
self->monsterinfo.idle_time = level.time + flrand(1.0, 2.0);
SV_NewChaseDir(self, self->enemy, dist);
}
}
else
{
self->monsterinfo.idle_time = level.time + flrand(0.5, 1.2);
SV_NewChaseDir(self, self->enemy, dist);
}
}
else
{
turnamt = Q_fabs(M_ChangeYaw(self));
if (!M_walkmove (self, self->s.angles[YAW], dist/2)&&(self->classID != CID_SSITHRA))
{
if (ai_hopdown(self, self->enemy->s.origin, 1024))
{
if (self->monsterinfo.jump_time < level.time)
{
self->monsterinfo.jump_time = level.time + 1;
return;
}
else return;
}
}
}
}
}
}
/*
=============
ai_flee
The monster has an enemy it is trying to get away from
=============
*/
void ai_flee (edict_t *self, float dist)
{
vec3_t vec;
if (self->enemy)
{
VectorSubtract (self->enemy->s.origin, self->s.origin, vec);
self->ideal_yaw = vectoyaw(vec);
self->ideal_yaw = anglemod(self->ideal_yaw + self->best_move_yaw);
M_ChangeYaw(self);
if(!M_walkmove(self, self->s.angles[YAW], dist) && EqualAngle(self->s.angles[YAW], self->ideal_yaw, 5))
self->best_move_yaw = flrand(60, 300);
else
self->best_move_yaw = 180;
/*
VectorSubtract(self->s.origin, self->enemy->s.origin, vec);
self->ideal_yaw = vectoyaw(vec);
M_ChangeYaw(self);
M_MoveAwayFromGoal (self, dist);*/
}
}
/*
=============================================================
void extrapolateFiredir (edict_t *self,vec3_t p1,float pspeed,edict_t *targ,float accept,vec3_t vec2)
MG
Estimates where the "targ" will be by the time a projectile
travelling at "pspeed" leaving "org" arrives at "targ"'s origin.
It then calculates a new spot to shoot at so that the
projectile will arrive at such spot at the same time as
"targ". Will return '0 0 0' (FALSE) if there is not a clear
line of fire to the spot or if the new vector is out of the
acceptable range (based on dot product of original vec and
the new vec).
PROPOSAL: Factor in skill->value? 0 = no leading, 4 = perfect leading, 1-3 innaccuracy levels for leading
=============================================================
*/
void extrapolateFiredir (edict_t *self,vec3_t p1,float pspeed,edict_t *targ,float accept,vec3_t vec2)
{
float dist1, dist2, tspeed, dot, eta1, eta2, eta_delta, tdist;
qboolean failed = false;
vec3_t p2, p3, targ_dir, vec1, tempv;
trace_t trace;
float offset;
if(!targ)
{
VectorClear(vec2);
return;
}
tdist = vhlen(targ->s.origin, self->s.origin);
if(!skill->value)
{//poor shot, take forward and screw it up
AngleVectors(self->s.angles, tempv, NULL, NULL);
VectorMA(p1, pspeed, tempv, p2);
if(tdist < 128)
offset = 48 * tdist/128;
else
offset = 48;
p2[0] += flrand(-offset, offset);
p2[1] += flrand(-offset, offset);
p2[2] += flrand(-offset/2, offset * 0.666);
VectorSubtract(p2, p1, vec2);
VectorNormalize(vec2);
return;
}
offset = 2 - skill->value;//skill >= 2 = perfect aim, skill 1 is very poor
if(tdist < 128)
offset *= tdist/128;
if(offset<0)
offset = 0;
if(skill->value < 2.0 && !(self->monsterinfo.aiflags & AI_NIGHTVISION))
{
if(targ->client)
{
offset += targ->light_level/32;
}
}
VectorCopy(targ->s.origin, p2);
if(offset)
{
p2[0] += flrand(-offset*12, offset*12);
p2[1] += flrand(-offset*12, offset*12);
p2[2] += flrand(-offset*8, offset*8);
}
VectorSubtract(p2, p1, vec1);
dist1 = VectorNormalize(vec1);
VectorCopy(targ->velocity, targ_dir);
tspeed = VectorNormalize(targ_dir);
eta1 = dist1/pspeed; //Estimated time of arrival of projectile to p2
VectorMA(p2, tspeed*eta1, targ_dir, p3);
VectorSubtract(p3, p1, tempv);
dist2 = VectorNormalize(tempv);
eta2 = dist2/pspeed; //ETA of projectile to p3
eta_delta = eta2-eta1; //change in ETA's
VectorMA(p3, tspeed*eta_delta*flrand(0, 1), targ_dir, p3);
//careful, above version does not modify targ_dir
gi.trace(p1, vec3_origin, vec3_origin, p3, self, MASK_SOLID,&trace);
if(trace.fraction<1.0)
failed = true;
VectorSubtract(p3, p1, vec2);
VectorNormalize(vec2);
dot = DotProduct(vec1, vec2);
if(dot<accept) //Change in dir too great
{
failed = true;
}
if(failed)
{
VectorClear(vec2);
}
}
/*
ClearLastAlerts
takes last alerts away from any monster who's last alert was the "self" alert
*/
void ClearLastAlerts(alertent_t *self)
{
int i;
for(i = 0; i < game.maxentities; i++)
{
if(g_edicts[i].svflags & SVF_MONSTER)
{
if(g_edicts[i].last_alert == self)
{
g_edicts[i].last_alert = NULL;
}
}
}
}
/*
alert_timed_out
clears out all the fields for an alert, removes it from the linked list and sets it's slot to being empty to make room for insertion of others later
*/
void alert_timed_out(alertent_t *self)
{
if (self==NULL)
return;
//take self out of alert chain
if(self->prev_alert)
{
if(self->next_alert)
self->prev_alert->next_alert = self->next_alert;
else
{//I'm the last one!
level.last_alert = self->prev_alert;
self->prev_alert->next_alert = NULL;
}
}
else
{//I'm the first one!
if(self->next_alert)
level.alert_entity = self->next_alert;
else
level.last_alert = level.alert_entity = NULL;
}
//Clear out all fields
self->next_alert = NULL;
self->prev_alert = NULL;
self->enemy = NULL;
VectorClear(self->origin);
self->alert_svflags = 0;
self->lifetime = 0;
self->inuse = false;
ClearLastAlerts(self);
level.num_alert_ents--;
}
/*
GetFirstEmptyAlertInList
returns the first alert in the level.alertents list that isn;t in use
*/
alertent_t *GetFirstEmptyAlertInList(void)
{
int i;
//have max number of alerts, remove the first one
if(level.num_alert_ents >= MAX_ALERT_ENTS)
alert_timed_out(level.alert_entity);
for(i = 0; i < MAX_ALERT_ENTS; i++)
{
if(!level.alertents[i].inuse)
{
return &level.alertents[i];
}
}
return NULL;
}
/*
AlertMonsters
allots an alertent monsters will check during FindTarget to see if they should be alerted by it
self = used for sv_flags info and positioning of the alert entity
enemy = entity to make the monsters mad at if they're alerted
(float)lifetime = how many seconds the alert exists for
(qboolean)ignore_shadows = this alert gives away enemy's position, even if he is in shadows (I use this for staff hits on the floor and any other effect the player makes at his own location(like shooting a weapon), not for projectiles impacting)
*/
void AlertMonsters (edict_t *self, edict_t *enemy, float lifetime, qboolean ignore_shadows)
{//FIXME: if and stick new one at the end
alertent_t *alerter = level.alert_entity;
alertent_t *last_alert = NULL;
if (deathmatch->value) // Don't need this if no monsters...
return;
if(!lifetime)
lifetime = 1.0;//stick around for 1 second
//stick into the level's alerter chain
if(alerter)
{//go down the list and find an empty slot
//fixme: just store the entnum?
while(alerter->next_alert)
{
last_alert = alerter;
alerter = alerter->next_alert;
}
level.last_alert = alerter = GetFirstEmptyAlertInList();
}
else//we're the first one!
level.alert_entity = level.last_alert = alerter = GetFirstEmptyAlertInList();
if(!alerter)
{
#ifdef _DEVEL
gi.dprintf("Error: out of alerts and can't find any empty slots!\n");
#endif
return;
}
//I'm active, don't let my slot be used until I'm freed
alerter->inuse = true;
//point to the previous alerter, if any
alerter->prev_alert = last_alert;
//make the previous alerter point to me as the next one
if(alerter->prev_alert)
alerter->prev_alert->next_alert = alerter;
//put me in the "self"'s spot
VectorCopy(self->s.origin, alerter->origin);
alerter->enemy = enemy;
//should we keep track of owner in case they move to move the alert with them? Only for monsters
//alerter->owner = self;
alerter->alert_svflags = self->svflags;
if(ignore_shadows)//whatever happened would give away enemy's position, even in shadows
alerter->alert_svflags |= SVF_ALERT_NO_SHADE;
//stick around until after this point in time
alerter->lifetime = level.time + lifetime;
level.num_alert_ents++;
}
void ai_spin (edict_t *self, float amount)
{
self->s.angles[YAW] += amount;
}
qboolean ai_have_enemy (edict_t *self)
{
qboolean enemy_gone = false;
if(!self->enemy)
enemy_gone = true;
else if(self->enemy->health <= 0)
enemy_gone = true;
if(!enemy_gone)
return true;
else
{
if(self->oldenemy)
{
if(self->oldenemy->health>0)
{
self->enemy=self->oldenemy;
self->oldenemy = NULL;
// gi.dprintf("Going for old enemy\n");
return true;
}
}
}
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
// gi.dprintf("Lost enemies\n");
return false;
}
qboolean movable (edict_t *ent)
{
if( ent->movetype!=PHYSICSTYPE_NONE &&
ent->movetype!=MOVETYPE_PUSH &&
ent->movetype!=PHYSICSTYPE_PUSH)
return true;
return false;
}