mirror of
https://github.com/yquake2/yquake2remaster.git
synced 2024-12-02 17:11:55 +00:00
1336 lines
24 KiB
C
1336 lines
24 KiB
C
/*
|
|
* Copyright (C) 1997-2001 Id Software, Inc.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or (at
|
|
* your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
*
|
|
* See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
|
|
* 02111-1307, USA.
|
|
*
|
|
* =======================================================================
|
|
*
|
|
* The basic AI functions like enemy detection, attacking and so on.
|
|
*
|
|
* =======================================================================
|
|
*/
|
|
|
|
#include "header/local.h"
|
|
|
|
extern cvar_t *maxclients;
|
|
|
|
qboolean enemy_vis;
|
|
qboolean enemy_infront;
|
|
int enemy_range;
|
|
float enemy_yaw;
|
|
|
|
qboolean FindTarget(edict_t *self);
|
|
qboolean ai_checkattack(edict_t *self);
|
|
|
|
/*
|
|
* 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 */
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Move the specified distance at current facing.
|
|
*/
|
|
void
|
|
ai_move(edict_t *self, float dist)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
M_walkmove(self, self->s.angles[YAW], dist);
|
|
}
|
|
|
|
/*
|
|
*
|
|
* 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 (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (dist)
|
|
{
|
|
M_walkmove(self, self->s.angles[YAW], dist);
|
|
}
|
|
|
|
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);
|
|
self->monsterinfo.run(self);
|
|
}
|
|
|
|
M_ChangeYaw(self);
|
|
ai_checkattack(self);
|
|
}
|
|
else
|
|
{
|
|
FindTarget(self);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (FindTarget(self))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (level.time > self->monsterinfo.pausetime)
|
|
{
|
|
self->monsterinfo.walk(self);
|
|
return;
|
|
}
|
|
|
|
if (!(self->spawnflags & 1) && (self->monsterinfo.idle) &&
|
|
(level.time > self->monsterinfo.idle_time))
|
|
{
|
|
if (self->monsterinfo.idle_time)
|
|
{
|
|
self->monsterinfo.idle(self);
|
|
self->monsterinfo.idle_time = level.time + 15 + random() * 15;
|
|
}
|
|
else
|
|
{
|
|
self->monsterinfo.idle_time = level.time + random() * 15;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* The monster is walking it's beat
|
|
*/
|
|
void
|
|
ai_walk(edict_t *self, float dist)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
M_MoveToGoal(self, dist);
|
|
|
|
/* check for noticing a player */
|
|
if (FindTarget(self))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ((self->monsterinfo.search) && (level.time > self->monsterinfo.idle_time))
|
|
{
|
|
if (self->monsterinfo.idle_time)
|
|
{
|
|
self->monsterinfo.search(self);
|
|
self->monsterinfo.idle_time = level.time + 15 + random() * 15;
|
|
}
|
|
else
|
|
{
|
|
self->monsterinfo.idle_time = level.time + random() * 15;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Turns towards target and advances
|
|
* Use this call with a distance of 0
|
|
* to replace ai_face
|
|
*/
|
|
void
|
|
ai_charge(edict_t *self, float dist)
|
|
{
|
|
vec3_t v;
|
|
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if(self->enemy)
|
|
{
|
|
VectorSubtract(self->enemy->s.origin, self->s.origin, v);
|
|
}
|
|
|
|
self->ideal_yaw = vectoyaw(v);
|
|
M_ChangeYaw(self);
|
|
|
|
if (dist)
|
|
{
|
|
M_walkmove(self, self->s.angles[YAW], dist);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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)
|
|
{
|
|
return;
|
|
}
|
|
|
|
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.
|
|
*/
|
|
|
|
/* ============================================================================ */
|
|
|
|
/*
|
|
* returns the range categorization of an entity relative 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;
|
|
|
|
if (!self || !other)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
VectorSubtract(self->s.origin, other->s.origin, v);
|
|
len = VectorLength(v);
|
|
|
|
if (len < MELEE_DISTANCE)
|
|
{
|
|
return RANGE_MELEE;
|
|
}
|
|
|
|
if (len < 500)
|
|
{
|
|
return RANGE_NEAR;
|
|
}
|
|
|
|
if (len < 1000)
|
|
{
|
|
return RANGE_MID;
|
|
}
|
|
|
|
return RANGE_FAR;
|
|
}
|
|
|
|
/*
|
|
* 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 (!self || !other)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
VectorCopy(self->s.origin, spot1);
|
|
spot1[2] += self->viewheight;
|
|
VectorCopy(other->s.origin, spot2);
|
|
spot2[2] += other->viewheight;
|
|
trace = gi.trace(spot1, vec3_origin, vec3_origin, spot2, self, MASK_OPAQUE);
|
|
|
|
if (trace.fraction == 1.0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
|
|
if (!self || !other)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
AngleVectors(self->s.angles, forward, NULL, NULL);
|
|
|
|
VectorSubtract(other->s.origin, self->s.origin, vec);
|
|
VectorNormalize(vec);
|
|
dot = DotProduct(vec, forward);
|
|
|
|
if (dot > 0.3)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/* ============================================================================ */
|
|
|
|
void
|
|
HuntTarget(edict_t *self)
|
|
{
|
|
vec3_t vec;
|
|
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->goalentity = self->enemy;
|
|
|
|
if (self->monsterinfo.aiflags & AI_STAND_GROUND)
|
|
{
|
|
self->monsterinfo.stand(self);
|
|
}
|
|
else
|
|
{
|
|
self->monsterinfo.run(self);
|
|
}
|
|
|
|
if(visible(self, self->enemy))
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
void
|
|
FoundTarget(edict_t *self)
|
|
{
|
|
if (!self|| !self->enemy || !self->enemy->inuse)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* let other monsters see this monster for a while */
|
|
if (self->enemy->client)
|
|
{
|
|
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);
|
|
self->monsterinfo.trail_time = level.time;
|
|
|
|
if (!self->combattarget)
|
|
{
|
|
HuntTarget(self);
|
|
return;
|
|
}
|
|
|
|
self->goalentity = self->movetarget = G_PickTarget(self->combattarget);
|
|
|
|
if (!self->movetarget)
|
|
{
|
|
self->goalentity = self->movetarget = self->enemy;
|
|
HuntTarget(self);
|
|
gi.dprintf("%s at %s, combattarget %s not found\n",
|
|
self->classname,
|
|
vtos(self->s.origin),
|
|
self->combattarget);
|
|
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;
|
|
self->monsterinfo.pausetime = 0;
|
|
|
|
/* run for it */
|
|
self->monsterinfo.run(self);
|
|
}
|
|
|
|
/*
|
|
* 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, the point
|
|
* of impact becomes a fakeplayer so that
|
|
* monsters that see the impact will respond
|
|
* as if they had seen the player.
|
|
*
|
|
* To avoid spending too much time, only
|
|
* a single client (or fakeclient) is
|
|
* checked each frame. This means multi
|
|
* player games will have slightly
|
|
* slower noticing monsters.
|
|
*/
|
|
qboolean
|
|
FindTarget(edict_t *self)
|
|
{
|
|
edict_t *client;
|
|
qboolean heardit;
|
|
int r;
|
|
|
|
if (!self)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (self->monsterinfo.aiflags & AI_GOOD_GUY)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/* if we're going to a combat point, just proceed */
|
|
if (self->monsterinfo.aiflags & AI_COMBAT_POINT)
|
|
{
|
|
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 */
|
|
|
|
heardit = false;
|
|
|
|
if ((level.sight_entity_framenum >= (level.framenum - 1)) &&
|
|
!(self->spawnflags & 1))
|
|
{
|
|
client = level.sight_entity;
|
|
|
|
if (client->enemy == self->enemy)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else if (level.sound_entity_framenum >= (level.framenum - 1))
|
|
{
|
|
client = level.sound_entity;
|
|
heardit = true;
|
|
}
|
|
else if (!(self->enemy) &&
|
|
(level.sound2_entity_framenum >= (level.framenum - 1)) &&
|
|
!(self->spawnflags & 1))
|
|
{
|
|
client = level.sound2_entity;
|
|
heardit = true;
|
|
}
|
|
else
|
|
{
|
|
client = level.sight_client;
|
|
|
|
if (!client)
|
|
{
|
|
return false; /* no clients to get mad at */
|
|
}
|
|
}
|
|
|
|
/* if the entity went away, forget it */
|
|
if (!client->inuse)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (client == self->enemy)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (client->client)
|
|
{
|
|
if (client->flags & FL_NOTARGET)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else if (client->svflags & SVF_MONSTER)
|
|
{
|
|
if (!client->enemy)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (client->enemy->flags & FL_NOTARGET)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else if (heardit)
|
|
{
|
|
if (client->owner->flags & FL_NOTARGET)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!heardit)
|
|
{
|
|
r = range(self, client);
|
|
|
|
if (r == RANGE_FAR)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/* is client in an spot too dark to be seen? */
|
|
if (client->light_level <= 5)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!visible(self, client))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (r == RANGE_NEAR)
|
|
{
|
|
if ((client->show_hostile < level.time) && !infront(self, client))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else if (r == RANGE_MID)
|
|
{
|
|
if (!infront(self, client))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
self->enemy = client;
|
|
|
|
if (strcmp(self->enemy->classname, "player_noise") != 0)
|
|
{
|
|
self->monsterinfo.aiflags &= ~AI_SOUND_TARGET;
|
|
|
|
if (!self->enemy->client)
|
|
{
|
|
self->enemy = self->enemy->enemy;
|
|
|
|
if (!self->enemy->client)
|
|
{
|
|
self->enemy = NULL;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else /* heardit */
|
|
{
|
|
vec3_t temp;
|
|
|
|
if (self->spawnflags & 1)
|
|
{
|
|
if (!visible(self, client))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!gi.inPHS(self->s.origin, client->s.origin))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
VectorSubtract(client->s.origin, self->s.origin, temp);
|
|
|
|
if (VectorLength(temp) > 1000) /* too far to hear */
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/* check area portals - if they are different
|
|
and not connected then we can't hear it */
|
|
if (client->areanum != self->areanum)
|
|
{
|
|
if (!gi.AreasConnected(self->areanum, client->areanum))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
self->ideal_yaw = vectoyaw(temp);
|
|
M_ChangeYaw(self);
|
|
|
|
/* hunt the sound for a bit; hopefully find the real player */
|
|
self->monsterinfo.aiflags |= AI_SOUND_TARGET;
|
|
self->enemy = client;
|
|
}
|
|
|
|
FoundTarget(self);
|
|
|
|
if (!(self->monsterinfo.aiflags & AI_SOUND_TARGET) &&
|
|
(self->monsterinfo.sight))
|
|
{
|
|
self->monsterinfo.sight(self, self->enemy);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* ============================================================================= */
|
|
|
|
qboolean
|
|
FacingIdeal(edict_t *self)
|
|
{
|
|
float delta;
|
|
|
|
if (!self)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
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 || !self->enemy || !self->enemy->inuse)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
|
|
tr = gi.trace(spot1, NULL, NULL, spot2, self,
|
|
CONTENTS_SOLID | CONTENTS_MONSTER | CONTENTS_SLIME |
|
|
CONTENTS_LAVA | CONTENTS_WINDOW);
|
|
|
|
/* 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 == SKILL_EASY) && (randk() & 3))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (self->monsterinfo.melee)
|
|
{
|
|
self->monsterinfo.attack_state = AS_MELEE;
|
|
}
|
|
else
|
|
{
|
|
self->monsterinfo.attack_state = AS_MISSILE;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* missile attack */
|
|
if (!self->monsterinfo.attack)
|
|
{
|
|
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_NEAR)
|
|
{
|
|
chance = 0.1;
|
|
}
|
|
else if (enemy_range == RANGE_MID)
|
|
{
|
|
chance = 0.02;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (skill->value == SKILL_EASY)
|
|
{
|
|
chance *= 0.5;
|
|
}
|
|
else if (skill->value >= SKILL_HARD)
|
|
{
|
|
chance *= 2;
|
|
}
|
|
|
|
if (random() < chance)
|
|
{
|
|
self->monsterinfo.attack_state = AS_MISSILE;
|
|
self->monsterinfo.attack_finished = level.time + 2 * random();
|
|
return true;
|
|
}
|
|
|
|
if (self->flags & FL_FLY)
|
|
{
|
|
if (random() < 0.3)
|
|
{
|
|
self->monsterinfo.attack_state = AS_SLIDING;
|
|
}
|
|
else
|
|
{
|
|
self->monsterinfo.attack_state = AS_STRAIGHT;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Turn and close until within an
|
|
* angle to launch a melee attack
|
|
*/
|
|
void
|
|
ai_run_melee(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->ideal_yaw = enemy_yaw;
|
|
M_ChangeYaw(self);
|
|
|
|
if (FacingIdeal(self))
|
|
{
|
|
if (self->monsterinfo.melee) {
|
|
self->monsterinfo.melee(self);
|
|
self->monsterinfo.attack_state = AS_STRAIGHT;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Turn in place until within an
|
|
* angle to launch a missile attack
|
|
*/
|
|
void
|
|
ai_run_missile(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->ideal_yaw = enemy_yaw;
|
|
M_ChangeYaw(self);
|
|
|
|
if (FacingIdeal(self))
|
|
{
|
|
if (self->monsterinfo.attack) {
|
|
self->monsterinfo.attack(self);
|
|
self->monsterinfo.attack_state = AS_STRAIGHT;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Strafe sideways, but stay at
|
|
* approximately the same range
|
|
*/
|
|
void
|
|
ai_run_slide(edict_t *self, float distance)
|
|
{
|
|
float ofs;
|
|
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/*
|
|
* Decides if we're going to attack
|
|
* or do something else used by
|
|
* ai_run and ai_stand
|
|
*/
|
|
qboolean
|
|
ai_checkattack(edict_t *self)
|
|
{
|
|
vec3_t temp;
|
|
qboolean hesDeadJim;
|
|
|
|
if (!self)
|
|
{
|
|
enemy_vis = false;
|
|
|
|
return false;
|
|
}
|
|
|
|
/* this causes monsters to run blindly
|
|
to the combat point w/o firing */
|
|
if (self->goalentity)
|
|
{
|
|
if (self->monsterinfo.aiflags & AI_COMBAT_POINT)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ((self->monsterinfo.aiflags & AI_SOUND_TARGET) && !visible(self, self->goalentity))
|
|
{
|
|
if ((level.time - self->enemy->last_sound_time) > 5.0)
|
|
{
|
|
if (self->goalentity == self->enemy)
|
|
{
|
|
if (self->movetarget)
|
|
{
|
|
self->goalentity = self->movetarget;
|
|
}
|
|
else
|
|
{
|
|
self->goalentity = NULL;
|
|
}
|
|
}
|
|
|
|
self->monsterinfo.aiflags &= ~AI_SOUND_TARGET;
|
|
|
|
if (self->monsterinfo.aiflags & AI_TEMP_STAND_GROUND)
|
|
{
|
|
self->monsterinfo.aiflags &=
|
|
~(AI_STAND_GROUND | AI_TEMP_STAND_GROUND);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self->show_hostile = level.time + 1;
|
|
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_MEDIC)
|
|
{
|
|
if (self->enemy->health > 0)
|
|
{
|
|
hesDeadJim = true;
|
|
self->monsterinfo.aiflags &= ~AI_MEDIC;
|
|
}
|
|
}
|
|
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;
|
|
|
|
if (self->oldenemy && (self->oldenemy->health > 0))
|
|
{
|
|
self->enemy = self->oldenemy;
|
|
self->oldenemy = NULL;
|
|
HuntTarget(self);
|
|
}
|
|
else
|
|
{
|
|
if (self->movetarget)
|
|
{
|
|
self->goalentity = self->movetarget;
|
|
self->monsterinfo.walk(self);
|
|
}
|
|
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;
|
|
self->monsterinfo.stand(self);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/* wake up other monsters */
|
|
self->show_hostile = level.time + 1;
|
|
|
|
/* check knowledge of enemy */
|
|
enemy_vis = 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->value && (self->monsterinfo.search_time < level.time))
|
|
{
|
|
if (FindTarget(self))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (self->enemy)
|
|
{
|
|
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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/*
|
|
* The monster has an enemy
|
|
* it is trying to kill
|
|
*/
|
|
void
|
|
ai_run(edict_t *self, float dist)
|
|
{
|
|
vec3_t v;
|
|
edict_t *tempgoal;
|
|
edict_t *save;
|
|
qboolean new;
|
|
edict_t *marker;
|
|
float d1, d2;
|
|
trace_t tr;
|
|
vec3_t v_forward, v_right;
|
|
float left, center, right;
|
|
vec3_t left_target, right_target;
|
|
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* if we're going to a combat point, just proceed */
|
|
if (self->monsterinfo.aiflags & AI_COMBAT_POINT)
|
|
{
|
|
M_MoveToGoal(self, dist);
|
|
return;
|
|
}
|
|
|
|
if (self->monsterinfo.aiflags & AI_SOUND_TARGET)
|
|
{
|
|
/* Special case: Some projectiles like grenades or rockets are
|
|
classified as an enemy. When they explode they generate a
|
|
sound entity, triggering this code path. Since they're gone
|
|
after the explosion their entity pointer is NULL. Therefor
|
|
self->enemy is also NULL and we're crashing. Work around
|
|
this by predending that the enemy is still there, and move
|
|
to it. */
|
|
if (self->enemy)
|
|
{
|
|
VectorSubtract(self->s.origin, self->enemy->s.origin, v);
|
|
|
|
if (VectorLength(v) < 64)
|
|
{
|
|
self->monsterinfo.aiflags |= (AI_STAND_GROUND | AI_TEMP_STAND_GROUND);
|
|
self->monsterinfo.stand(self);
|
|
return;
|
|
}
|
|
}
|
|
|
|
M_MoveToGoal(self, dist);
|
|
|
|
if (!FindTarget(self))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (ai_checkattack(self))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self->monsterinfo.attack_state == AS_SLIDING)
|
|
{
|
|
ai_run_slide(self, dist);
|
|
return;
|
|
}
|
|
|
|
if (enemy_vis)
|
|
{
|
|
M_MoveToGoal(self, dist);
|
|
self->monsterinfo.aiflags &= ~AI_LOST_SIGHT;
|
|
VectorCopy(self->enemy->s.origin, self->monsterinfo.last_sighting);
|
|
self->monsterinfo.trail_time = level.time;
|
|
return;
|
|
}
|
|
|
|
if ((self->monsterinfo.search_time) &&
|
|
(level.time > (self->monsterinfo.search_time + 20)))
|
|
{
|
|
M_MoveToGoal(self, dist);
|
|
self->monsterinfo.search_time = 0;
|
|
return;
|
|
}
|
|
|
|
tempgoal = G_SpawnOptional();
|
|
|
|
if (!tempgoal)
|
|
{
|
|
M_MoveToGoal(self, dist);
|
|
return;
|
|
}
|
|
|
|
save = self->goalentity;
|
|
self->goalentity = tempgoal;
|
|
|
|
new = false;
|
|
|
|
if (!(self->monsterinfo.aiflags & AI_LOST_SIGHT))
|
|
{
|
|
/* just lost sight of the player, decide where to go first */
|
|
self->monsterinfo.aiflags |= (AI_LOST_SIGHT | AI_PURSUIT_LAST_SEEN);
|
|
self->monsterinfo.aiflags &= ~(AI_PURSUE_NEXT | AI_PURSUE_TEMP);
|
|
new = true;
|
|
}
|
|
|
|
if (self->monsterinfo.aiflags & AI_PURSUE_NEXT)
|
|
{
|
|
self->monsterinfo.aiflags &= ~AI_PURSUE_NEXT;
|
|
|
|
/* give ourself more time since we got this far */
|
|
self->monsterinfo.search_time = level.time + 5;
|
|
|
|
if (self->monsterinfo.aiflags & AI_PURSUE_TEMP)
|
|
{
|
|
self->monsterinfo.aiflags &= ~AI_PURSUE_TEMP;
|
|
marker = NULL;
|
|
VectorCopy(self->monsterinfo.saved_goal,
|
|
self->monsterinfo.last_sighting);
|
|
new = true;
|
|
}
|
|
else if (self->monsterinfo.aiflags & AI_PURSUIT_LAST_SEEN)
|
|
{
|
|
self->monsterinfo.aiflags &= ~AI_PURSUIT_LAST_SEEN;
|
|
marker = PlayerTrail_PickFirst(self);
|
|
}
|
|
else
|
|
{
|
|
marker = PlayerTrail_PickNext(self);
|
|
}
|
|
|
|
if (marker)
|
|
{
|
|
VectorCopy(marker->s.origin, self->monsterinfo.last_sighting);
|
|
self->monsterinfo.trail_time = marker->timestamp;
|
|
self->s.angles[YAW] = self->ideal_yaw = marker->s.angles[YAW];
|
|
new = true;
|
|
}
|
|
}
|
|
|
|
VectorSubtract(self->s.origin, self->monsterinfo.last_sighting, v);
|
|
d1 = VectorLength(v);
|
|
|
|
if (d1 <= dist)
|
|
{
|
|
self->monsterinfo.aiflags |= AI_PURSUE_NEXT;
|
|
dist = d1;
|
|
}
|
|
|
|
VectorCopy(self->monsterinfo.last_sighting, self->goalentity->s.origin);
|
|
|
|
if (new)
|
|
{
|
|
tr = gi.trace(self->s.origin, self->mins, self->maxs,
|
|
self->monsterinfo.last_sighting, self,
|
|
MASK_PLAYERSOLID);
|
|
|
|
if (tr.fraction < 1)
|
|
{
|
|
VectorSubtract(self->goalentity->s.origin, self->s.origin, v);
|
|
d1 = VectorLength(v);
|
|
center = tr.fraction;
|
|
d2 = d1 * ((center + 1) / 2);
|
|
self->s.angles[YAW] = self->ideal_yaw = vectoyaw(v);
|
|
AngleVectors(self->s.angles, v_forward, v_right, NULL);
|
|
|
|
VectorSet(v, d2, -16, 0);
|
|
G_ProjectSource(self->s.origin, v, v_forward, v_right, left_target);
|
|
tr = gi.trace(self->s.origin, self->mins, self->maxs, left_target,
|
|
self, MASK_PLAYERSOLID);
|
|
left = tr.fraction;
|
|
|
|
VectorSet(v, d2, 16, 0);
|
|
G_ProjectSource(self->s.origin, v, v_forward, v_right, right_target);
|
|
tr = gi.trace(self->s.origin, self->mins, self->maxs, right_target,
|
|
self, MASK_PLAYERSOLID);
|
|
right = tr.fraction;
|
|
|
|
center = (d1 * center) / d2;
|
|
|
|
if ((left >= center) && (left > right))
|
|
{
|
|
if (left < 1)
|
|
{
|
|
VectorSet(v, d2 * left * 0.5, -16, 0);
|
|
G_ProjectSource(self->s.origin, v, v_forward,
|
|
v_right, left_target);
|
|
}
|
|
|
|
VectorCopy(self->monsterinfo.last_sighting,
|
|
self->monsterinfo.saved_goal);
|
|
self->monsterinfo.aiflags |= AI_PURSUE_TEMP;
|
|
VectorCopy(left_target, self->goalentity->s.origin);
|
|
VectorCopy(left_target, self->monsterinfo.last_sighting);
|
|
VectorSubtract(self->goalentity->s.origin, self->s.origin, v);
|
|
self->s.angles[YAW] = self->ideal_yaw = vectoyaw(v);
|
|
}
|
|
else if ((right >= center) && (right > left))
|
|
{
|
|
if (right < 1)
|
|
{
|
|
VectorSet(v, d2 * right * 0.5, 16, 0);
|
|
G_ProjectSource(self->s.origin, v, v_forward, v_right,
|
|
right_target);
|
|
}
|
|
|
|
VectorCopy(self->monsterinfo.last_sighting,
|
|
self->monsterinfo.saved_goal);
|
|
self->monsterinfo.aiflags |= AI_PURSUE_TEMP;
|
|
VectorCopy(right_target, self->goalentity->s.origin);
|
|
VectorCopy(right_target, self->monsterinfo.last_sighting);
|
|
VectorSubtract(self->goalentity->s.origin, self->s.origin, v);
|
|
self->s.angles[YAW] = self->ideal_yaw = vectoyaw(v);
|
|
}
|
|
}
|
|
}
|
|
|
|
M_MoveToGoal(self, dist);
|
|
|
|
G_FreeEdict(tempgoal);
|
|
|
|
self->goalentity = save;
|
|
}
|