halflife-photomode/dlls/monsters.cpp
2024-08-28 18:20:20 +02:00

3430 lines
96 KiB
C++

/***
*
* Copyright (c) 1996-2001, Valve LLC. All rights reserved.
*
* This product contains software technology licensed from Id
* Software, Inc. ("Id Technology"). Id Technology (c) 1996 Id Software, Inc.
* All Rights Reserved.
*
* This source code contains proprietary and confidential information of
* Valve LLC and its suppliers. Access to this code is restricted to
* persons who have executed a written SDK license with Valve. Any access,
* use or distribution of this code by or to any unlicensed person is illegal.
*
****/
/*
===== monsters.cpp ========================================================
Monster-related utility code
*/
#include "extdll.h"
#include "util.h"
#include "cbase.h"
#include "nodes.h"
#include "monsters.h"
#include "animation.h"
#include "saverestore.h"
#include "weapons.h"
#include "scripted.h"
#include "squadmonster.h"
#include "decals.h"
#include "soundent.h"
#include "gamerules.h"
#define MONSTER_CUT_CORNER_DIST 8 // 8 means the monster's bounding box is contained without the box of the node in WC
Vector VecBModelOrigin(entvars_t* pevBModel);
// Global Savedata for monster
// UNDONE: Save schedule data? Can this be done? We may
// lose our enemy pointer or other data (goal ent, target, etc)
// that make the current schedule invalid, perhaps it's best
// to just pick a new one when we start up again.
TYPEDESCRIPTION CBaseMonster::m_SaveData[] =
{
DEFINE_FIELD(CBaseMonster, m_hEnemy, FIELD_EHANDLE),
DEFINE_FIELD(CBaseMonster, m_hTargetEnt, FIELD_EHANDLE),
DEFINE_ARRAY(CBaseMonster, m_hOldEnemy, FIELD_EHANDLE, MAX_OLD_ENEMIES),
DEFINE_ARRAY(CBaseMonster, m_vecOldEnemy, FIELD_POSITION_VECTOR, MAX_OLD_ENEMIES),
DEFINE_FIELD(CBaseMonster, m_flFieldOfView, FIELD_FLOAT),
DEFINE_FIELD(CBaseMonster, m_flWaitFinished, FIELD_TIME),
DEFINE_FIELD(CBaseMonster, m_flMoveWaitFinished, FIELD_TIME),
DEFINE_FIELD(CBaseMonster, m_Activity, FIELD_INTEGER),
DEFINE_FIELD(CBaseMonster, m_IdealActivity, FIELD_INTEGER),
DEFINE_FIELD(CBaseMonster, m_LastHitGroup, FIELD_INTEGER),
DEFINE_FIELD(CBaseMonster, m_MonsterState, FIELD_INTEGER),
DEFINE_FIELD(CBaseMonster, m_IdealMonsterState, FIELD_INTEGER),
DEFINE_FIELD(CBaseMonster, m_iTaskStatus, FIELD_INTEGER),
//Schedule_t *m_pSchedule;
DEFINE_FIELD(CBaseMonster, m_iScheduleIndex, FIELD_INTEGER),
DEFINE_FIELD(CBaseMonster, m_afConditions, FIELD_INTEGER),
//WayPoint_t m_Route[ ROUTE_SIZE ];
// DEFINE_FIELD( CBaseMonster, m_movementGoal, FIELD_INTEGER ),
// DEFINE_FIELD( CBaseMonster, m_iRouteIndex, FIELD_INTEGER ),
// DEFINE_FIELD( CBaseMonster, m_moveWaitTime, FIELD_FLOAT ),
DEFINE_FIELD(CBaseMonster, m_vecMoveGoal, FIELD_POSITION_VECTOR),
DEFINE_FIELD(CBaseMonster, m_movementActivity, FIELD_INTEGER),
// int m_iAudibleList; // first index of a linked list of sounds that the monster can hear.
// DEFINE_FIELD( CBaseMonster, m_afSoundTypes, FIELD_INTEGER ),
DEFINE_FIELD(CBaseMonster, m_vecLastPosition, FIELD_POSITION_VECTOR),
DEFINE_FIELD(CBaseMonster, m_iHintNode, FIELD_INTEGER),
DEFINE_FIELD(CBaseMonster, m_afMemory, FIELD_INTEGER),
DEFINE_FIELD(CBaseMonster, m_iMaxHealth, FIELD_INTEGER),
DEFINE_FIELD(CBaseMonster, m_vecEnemyLKP, FIELD_POSITION_VECTOR),
DEFINE_FIELD(CBaseMonster, m_cAmmoLoaded, FIELD_INTEGER),
DEFINE_FIELD(CBaseMonster, m_afCapability, FIELD_INTEGER),
DEFINE_FIELD(CBaseMonster, m_flNextAttack, FIELD_TIME),
DEFINE_FIELD(CBaseMonster, m_bitsDamageType, FIELD_INTEGER),
DEFINE_ARRAY(CBaseMonster, m_rgbTimeBasedDamage, FIELD_CHARACTER, CDMG_TIMEBASED),
DEFINE_FIELD(CBaseMonster, m_bloodColor, FIELD_INTEGER),
DEFINE_FIELD(CBaseMonster, m_failSchedule, FIELD_INTEGER),
DEFINE_FIELD(CBaseMonster, m_flHungryTime, FIELD_TIME),
DEFINE_FIELD(CBaseMonster, m_flDistTooFar, FIELD_FLOAT),
DEFINE_FIELD(CBaseMonster, m_flDistLook, FIELD_FLOAT),
DEFINE_FIELD(CBaseMonster, m_iTriggerCondition, FIELD_INTEGER),
DEFINE_FIELD(CBaseMonster, m_iszTriggerTarget, FIELD_STRING),
DEFINE_FIELD(CBaseMonster, m_HackedGunPos, FIELD_VECTOR),
DEFINE_FIELD(CBaseMonster, m_scriptState, FIELD_INTEGER),
DEFINE_FIELD(CBaseMonster, m_pCine, FIELD_CLASSPTR),
DEFINE_FIELD(CBaseMonster, m_AllowItemDropping, FIELD_BOOLEAN),
};
//IMPLEMENT_SAVERESTORE( CBaseMonster, CBaseToggle );
bool CBaseMonster::Save(CSave& save)
{
if (!CBaseToggle::Save(save))
return false;
return save.WriteFields("CBaseMonster", this, m_SaveData, ARRAYSIZE(m_SaveData));
}
bool CBaseMonster::Restore(CRestore& restore)
{
if (!CBaseToggle::Restore(restore))
return false;
bool status = restore.ReadFields("CBaseMonster", this, m_SaveData, ARRAYSIZE(m_SaveData));
// We don't save/restore routes yet
RouteClear();
// We don't save/restore schedules yet
m_pSchedule = NULL;
m_iTaskStatus = TASKSTATUS_NEW;
// Reset animation
m_Activity = ACT_RESET;
// If we don't have an enemy, clear conditions like see enemy, etc.
if (m_hEnemy == NULL)
m_afConditions = 0;
return status;
}
//=========================================================
// Eat - makes a monster full for a little while.
//=========================================================
void CBaseMonster::Eat(float flFullDuration)
{
m_flHungryTime = gpGlobals->time + flFullDuration;
}
//=========================================================
// FShouldEat - returns true if a monster is hungry.
//=========================================================
bool CBaseMonster::FShouldEat()
{
if (m_flHungryTime > gpGlobals->time)
{
return false;
}
return true;
}
//=========================================================
// BarnacleVictimBitten - called
// by Barnacle victims when the barnacle pulls their head
// into its mouth
//=========================================================
void CBaseMonster::BarnacleVictimBitten(entvars_t* pevBarnacle)
{
Schedule_t* pNewSchedule;
pNewSchedule = GetScheduleOfType(SCHED_BARNACLE_VICTIM_CHOMP);
if (pNewSchedule)
{
ChangeSchedule(pNewSchedule);
}
}
//=========================================================
// BarnacleVictimReleased - called by barnacle victims when
// the host barnacle is killed.
//=========================================================
void CBaseMonster::BarnacleVictimReleased()
{
m_IdealMonsterState = MONSTERSTATE_IDLE;
pev->velocity = g_vecZero;
pev->movetype = MOVETYPE_STEP;
}
//=========================================================
// Listen - monsters dig through the active sound list for
// any sounds that may interest them. (smells, too!)
//=========================================================
void CBaseMonster::Listen()
{
int iSound;
int iMySounds;
float hearingSensitivity;
CSound* pCurrentSound;
m_iAudibleList = SOUNDLIST_EMPTY;
ClearConditions(bits_COND_HEAR_SOUND | bits_COND_SMELL | bits_COND_SMELL_FOOD);
m_afSoundTypes = 0;
iMySounds = ISoundMask();
if (m_pSchedule)
{
//!!!WATCH THIS SPOT IF YOU ARE HAVING SOUND RELATED BUGS!
// Make sure your schedule AND personal sound masks agree!
iMySounds &= m_pSchedule->iSoundMask;
}
iSound = CSoundEnt::ActiveList();
// UNDONE: Clear these here?
ClearConditions(bits_COND_HEAR_SOUND | bits_COND_SMELL_FOOD | bits_COND_SMELL);
hearingSensitivity = HearingSensitivity();
while (iSound != SOUNDLIST_EMPTY)
{
pCurrentSound = CSoundEnt::SoundPointerForIndex(iSound);
if (nullptr != pCurrentSound &&
(pCurrentSound->m_iType & iMySounds) != 0 &&
(pCurrentSound->m_vecOrigin - EarPosition()).Length() <= pCurrentSound->m_iVolume * hearingSensitivity)
//if ( ( g_pSoundEnt->m_SoundPool[ iSound ].m_iType & iMySounds ) && ( g_pSoundEnt->m_SoundPool[ iSound ].m_vecOrigin - EarPosition()).Length () <= g_pSoundEnt->m_SoundPool[ iSound ].m_iVolume * hearingSensitivity )
{
// the monster cares about this sound, and it's close enough to hear.
//g_pSoundEnt->m_SoundPool[ iSound ].m_iNextAudible = m_iAudibleList;
pCurrentSound->m_iNextAudible = m_iAudibleList;
if (pCurrentSound->FIsSound())
{
// this is an audible sound.
SetConditions(bits_COND_HEAR_SOUND);
}
else
{
// if not a sound, must be a smell - determine if it's just a scent, or if it's a food scent
// if ( g_pSoundEnt->m_SoundPool[ iSound ].m_iType & ( bits_SOUND_MEAT | bits_SOUND_CARCASS ) )
if ((pCurrentSound->m_iType & (bits_SOUND_MEAT | bits_SOUND_CARCASS)) != 0)
{
// the detected scent is a food item, so set both conditions.
// !!!BUGBUG - maybe a virtual function to determine whether or not the scent is food?
SetConditions(bits_COND_SMELL_FOOD);
SetConditions(bits_COND_SMELL);
}
else
{
// just a normal scent.
SetConditions(bits_COND_SMELL);
}
}
// m_afSoundTypes |= g_pSoundEnt->m_SoundPool[ iSound ].m_iType;
m_afSoundTypes |= pCurrentSound->m_iType;
m_iAudibleList = iSound;
}
// iSound = g_pSoundEnt->m_SoundPool[ iSound ].m_iNext;
iSound = pCurrentSound->m_iNext;
}
}
//=========================================================
// FLSoundVolume - subtracts the volume of the given sound
// from the distance the sound source is from the caller,
// and returns that value, which is considered to be the 'local'
// volume of the sound.
//=========================================================
float CBaseMonster::FLSoundVolume(CSound* pSound)
{
return (pSound->m_iVolume - ((pSound->m_vecOrigin - pev->origin).Length()));
}
//=========================================================
// FValidateHintType - tells use whether or not the monster cares
// about the type of Hint Node given
//=========================================================
bool CBaseMonster::FValidateHintType(short sHint)
{
return false;
}
//=========================================================
// Look - Base class monster function to find enemies or
// food by sight. iDistance is distance ( in units ) that the
// monster can see.
//
// Sets the sight bits of the m_afConditions mask to indicate
// which types of entities were sighted.
// Function also sets the Looker's m_pLink
// to the head of a link list that contains all visible ents.
// (linked via each ent's m_pLink field)
//
//=========================================================
void CBaseMonster::Look(int iDistance)
{
int iSighted = 0;
// DON'T let visibility information from last frame sit around!
ClearConditions(bits_COND_SEE_HATE | bits_COND_SEE_DISLIKE | bits_COND_SEE_ENEMY | bits_COND_SEE_FEAR | bits_COND_SEE_NEMESIS | bits_COND_SEE_CLIENT);
m_pLink = NULL;
CBaseEntity* pSightEnt = NULL; // the current visible entity that we're dealing with
// See no evil if prisoner is set
if (!FBitSet(pev->spawnflags, SF_MONSTER_PRISONER))
{
CBaseEntity* pList[100];
Vector delta = Vector(iDistance, iDistance, iDistance);
// Find only monsters/clients in box, NOT limited to PVS
int count = UTIL_EntitiesInBox(pList, 100, pev->origin - delta, pev->origin + delta, FL_CLIENT | FL_MONSTER);
for (int i = 0; i < count; i++)
{
pSightEnt = pList[i];
// !!!temporarily only considering other monsters and clients, don't see prisoners
if (pSightEnt != this &&
!FBitSet(pSightEnt->pev->spawnflags, SF_MONSTER_PRISONER) &&
pSightEnt->pev->health > 0)
{
// the looker will want to consider this entity
// don't check anything else about an entity that can't be seen, or an entity that you don't care about.
if (IRelationship(pSightEnt) != R_NO && FInViewCone(pSightEnt) && !FBitSet(pSightEnt->pev->flags, FL_NOTARGET) && FVisible(pSightEnt))
{
if (pSightEnt->IsPlayer())
{
if ((pev->spawnflags & SF_MONSTER_WAIT_TILL_SEEN) != 0)
{
CBaseMonster* pClient;
pClient = pSightEnt->MyMonsterPointer();
// don't link this client in the list if the monster is wait till seen and the player isn't facing the monster
if (pSightEnt && !pClient->FInViewCone(this))
{
// we're not in the player's view cone.
continue;
}
else
{
// player sees us, become normal now.
pev->spawnflags &= ~SF_MONSTER_WAIT_TILL_SEEN;
}
}
// if we see a client, remember that (mostly for scripted AI)
iSighted |= bits_COND_SEE_CLIENT;
}
pSightEnt->m_pLink = m_pLink;
m_pLink = pSightEnt;
if (pSightEnt == m_hEnemy)
{
// we know this ent is visible, so if it also happens to be our enemy, store that now.
iSighted |= bits_COND_SEE_ENEMY;
}
// don't add the Enemy's relationship to the conditions. We only want to worry about conditions when
// we see monsters other than the Enemy.
switch (IRelationship(pSightEnt))
{
case R_NM:
iSighted |= bits_COND_SEE_NEMESIS;
break;
case R_HT:
iSighted |= bits_COND_SEE_HATE;
break;
case R_DL:
iSighted |= bits_COND_SEE_DISLIKE;
break;
case R_FR:
iSighted |= bits_COND_SEE_FEAR;
break;
case R_AL:
break;
default:
ALERT(at_aiconsole, "%s can't assess %s\n", STRING(pev->classname), STRING(pSightEnt->pev->classname));
break;
}
}
}
}
}
SetConditions(iSighted);
}
//=========================================================
// ISoundMask - returns a bit mask indicating which types
// of sounds this monster regards. In the base class implementation,
// monsters care about all sounds, but no scents.
//=========================================================
int CBaseMonster::ISoundMask()
{
return bits_SOUND_WORLD |
bits_SOUND_COMBAT |
bits_SOUND_PLAYER;
}
//=========================================================
// PBestSound - returns a pointer to the sound the monster
// should react to. Right now responds only to nearest sound.
//=========================================================
CSound* CBaseMonster::PBestSound()
{
int iThisSound;
int iBestSound = -1;
float flBestDist = 8192; // so first nearby sound will become best so far.
float flDist;
CSound* pSound;
iThisSound = m_iAudibleList;
if (iThisSound == SOUNDLIST_EMPTY)
{
ALERT(at_aiconsole, "ERROR! monster %s has no audible sounds!\n", STRING(pev->classname));
#if _DEBUG
ALERT(at_error, "NULL Return from PBestSound\n");
#endif
return NULL;
}
while (iThisSound != SOUNDLIST_EMPTY)
{
pSound = CSoundEnt::SoundPointerForIndex(iThisSound);
if (pSound && pSound->FIsSound())
{
flDist = (pSound->m_vecOrigin - EarPosition()).Length();
if (flDist < flBestDist)
{
iBestSound = iThisSound;
flBestDist = flDist;
}
}
iThisSound = pSound->m_iNextAudible;
}
if (iBestSound >= 0)
{
pSound = CSoundEnt::SoundPointerForIndex(iBestSound);
return pSound;
}
#if _DEBUG
ALERT(at_error, "NULL Return from PBestSound\n");
#endif
return NULL;
}
//=========================================================
// PBestScent - returns a pointer to the scent the monster
// should react to. Right now responds only to nearest scent
//=========================================================
CSound* CBaseMonster::PBestScent()
{
int iThisScent;
int iBestScent = -1;
float flBestDist = 8192; // so first nearby smell will become best so far.
float flDist;
CSound* pSound;
iThisScent = m_iAudibleList; // smells are in the sound list.
if (iThisScent == SOUNDLIST_EMPTY)
{
ALERT(at_aiconsole, "ERROR! PBestScent() has empty soundlist!\n");
#if _DEBUG
ALERT(at_error, "NULL Return from PBestSound\n");
#endif
return NULL;
}
while (iThisScent != SOUNDLIST_EMPTY)
{
pSound = CSoundEnt::SoundPointerForIndex(iThisScent);
if (pSound->FIsScent())
{
flDist = (pSound->m_vecOrigin - pev->origin).Length();
if (flDist < flBestDist)
{
iBestScent = iThisScent;
flBestDist = flDist;
}
}
iThisScent = pSound->m_iNextAudible;
}
if (iBestScent >= 0)
{
pSound = CSoundEnt::SoundPointerForIndex(iBestScent);
return pSound;
}
#if _DEBUG
ALERT(at_error, "NULL Return from PBestScent\n");
#endif
return NULL;
}
//=========================================================
// Monster Think - calls out to core AI functions and handles this
// monster's specific animation events
//=========================================================
void CBaseMonster::MonsterThink()
{
pev->nextthink = gpGlobals->time + 0.1; // keep monster thinking.
RunAI();
float flInterval = StudioFrameAdvance(); // animate
// start or end a fidget
// This needs a better home -- switching animations over time should be encapsulated on a per-activity basis
// perhaps MaintainActivity() or a ShiftAnimationOverTime() or something.
if (m_MonsterState != MONSTERSTATE_SCRIPT && m_MonsterState != MONSTERSTATE_DEAD && m_Activity == ACT_IDLE && m_fSequenceFinished)
{
int iSequence;
if (m_fSequenceLoops)
{
// animation does loop, which means we're playing subtle idle. Might need to
// fidget.
iSequence = LookupActivity(m_Activity);
}
else
{
// animation that just ended doesn't loop! That means we just finished a fidget
// and should return to our heaviest weighted idle (the subtle one)
iSequence = LookupActivityHeaviest(m_Activity);
}
if (iSequence != ACTIVITY_NOT_AVAILABLE)
{
pev->sequence = iSequence; // Set to new anim (if it's there)
ResetSequenceInfo();
}
}
DispatchAnimEvents(flInterval);
if (!MovementIsComplete())
{
Move(flInterval);
}
#if _DEBUG
else
{
if (!TaskIsRunning() && !TaskIsComplete())
ALERT(at_error, "Schedule stalled!!\n");
}
#endif
}
//=========================================================
// CBaseMonster - USE - will make a monster angry at whomever
// activated it.
//=========================================================
void CBaseMonster::MonsterUse(CBaseEntity* pActivator, CBaseEntity* pCaller, USE_TYPE useType, float value)
{
//Don't do this because it can resurrect dying monsters
//m_IdealMonsterState = MONSTERSTATE_ALERT;
}
//=========================================================
// Ignore conditions - before a set of conditions is allowed
// to interrupt a monster's schedule, this function removes
// conditions that we have flagged to interrupt the current
// schedule, but may not want to interrupt the schedule every
// time. (Pain, for instance)
//=========================================================
int CBaseMonster::IgnoreConditions()
{
int iIgnoreConditions = 0;
if (!FShouldEat())
{
// not hungry? Ignore food smell.
iIgnoreConditions |= bits_COND_SMELL_FOOD;
}
if (m_MonsterState == MONSTERSTATE_SCRIPT && m_pCine)
iIgnoreConditions |= m_pCine->IgnoreConditions();
return iIgnoreConditions;
}
//=========================================================
// RouteClear - zeroes out the monster's route array and goal
//=========================================================
void CBaseMonster::RouteClear()
{
RouteNew();
m_movementGoal = MOVEGOAL_NONE;
m_movementActivity = ACT_IDLE;
Forget(bits_MEMORY_MOVE_FAILED);
}
//=========================================================
// Route New - clears out a route to be changed, but keeps
// goal intact.
//=========================================================
void CBaseMonster::RouteNew()
{
m_Route[0].iType = 0;
m_iRouteIndex = 0;
}
//=========================================================
// FRouteClear - returns true is the Route is cleared out
// ( invalid )
//=========================================================
bool CBaseMonster::FRouteClear()
{
if (m_Route[m_iRouteIndex].iType == 0 || m_movementGoal == MOVEGOAL_NONE)
return true;
return false;
}
//=========================================================
// FRefreshRoute - after calculating a path to the monster's
// target, this function copies as many waypoints as possible
// from that path to the monster's Route array
//=========================================================
bool CBaseMonster::FRefreshRoute()
{
CBaseEntity* pPathCorner;
int i;
bool returnCode;
RouteNew();
returnCode = false;
switch (m_movementGoal)
{
case MOVEGOAL_PATHCORNER:
{
// monster is on a path_corner loop
pPathCorner = m_pGoalEnt;
i = 0;
while (pPathCorner && i < ROUTE_SIZE)
{
m_Route[i].iType = bits_MF_TO_PATHCORNER;
m_Route[i].vecLocation = pPathCorner->pev->origin;
pPathCorner = pPathCorner->GetNextTarget();
// Last path_corner in list?
if (!pPathCorner)
m_Route[i].iType |= bits_MF_IS_GOAL;
i++;
}
}
returnCode = true;
break;
case MOVEGOAL_ENEMY:
returnCode = BuildRoute(m_vecEnemyLKP, bits_MF_TO_ENEMY, m_hEnemy);
break;
case MOVEGOAL_LOCATION:
returnCode = BuildRoute(m_vecMoveGoal, bits_MF_TO_LOCATION, NULL);
break;
case MOVEGOAL_TARGETENT:
if (m_hTargetEnt != NULL)
{
returnCode = BuildRoute(m_hTargetEnt->pev->origin, bits_MF_TO_TARGETENT, m_hTargetEnt);
}
break;
case MOVEGOAL_NODE:
returnCode = FGetNodeRoute(m_vecMoveGoal);
// if ( returnCode )
// RouteSimplify( NULL );
break;
}
return returnCode;
}
bool CBaseMonster::MoveToEnemy(Activity movementAct, float waitTime)
{
m_movementActivity = movementAct;
m_moveWaitTime = waitTime;
m_movementGoal = MOVEGOAL_ENEMY;
return FRefreshRoute();
}
bool CBaseMonster::MoveToLocation(Activity movementAct, float waitTime, const Vector& goal)
{
m_movementActivity = movementAct;
m_moveWaitTime = waitTime;
m_movementGoal = MOVEGOAL_LOCATION;
m_vecMoveGoal = goal;
return FRefreshRoute();
}
bool CBaseMonster::MoveToTarget(Activity movementAct, float waitTime)
{
m_movementActivity = movementAct;
m_moveWaitTime = waitTime;
m_movementGoal = MOVEGOAL_TARGETENT;
return FRefreshRoute();
}
bool CBaseMonster::MoveToNode(Activity movementAct, float waitTime, const Vector& goal)
{
m_movementActivity = movementAct;
m_moveWaitTime = waitTime;
m_movementGoal = MOVEGOAL_NODE;
m_vecMoveGoal = goal;
return FRefreshRoute();
}
#ifdef _DEBUG
void DrawRoute(entvars_t* pev, WayPoint_t* m_Route, int m_iRouteIndex, int r, int g, int b)
{
int i;
if (m_Route[m_iRouteIndex].iType == 0)
{
ALERT(at_aiconsole, "Can't draw route!\n");
return;
}
// UTIL_ParticleEffect ( m_Route[ m_iRouteIndex ].vecLocation, g_vecZero, 255, 25 );
MESSAGE_BEGIN(MSG_BROADCAST, SVC_TEMPENTITY);
WRITE_BYTE(TE_BEAMPOINTS);
WRITE_COORD(pev->origin.x);
WRITE_COORD(pev->origin.y);
WRITE_COORD(pev->origin.z);
WRITE_COORD(m_Route[m_iRouteIndex].vecLocation.x);
WRITE_COORD(m_Route[m_iRouteIndex].vecLocation.y);
WRITE_COORD(m_Route[m_iRouteIndex].vecLocation.z);
WRITE_SHORT(g_sModelIndexLaser);
WRITE_BYTE(0); // frame start
WRITE_BYTE(10); // framerate
WRITE_BYTE(1); // life
WRITE_BYTE(16); // width
WRITE_BYTE(0); // noise
WRITE_BYTE(r); // r, g, b
WRITE_BYTE(g); // r, g, b
WRITE_BYTE(b); // r, g, b
WRITE_BYTE(255); // brightness
WRITE_BYTE(10); // speed
MESSAGE_END();
for (i = m_iRouteIndex; i < ROUTE_SIZE - 1; i++)
{
if ((m_Route[i].iType & bits_MF_IS_GOAL) != 0 || (m_Route[i + 1].iType == 0))
break;
MESSAGE_BEGIN(MSG_BROADCAST, SVC_TEMPENTITY);
WRITE_BYTE(TE_BEAMPOINTS);
WRITE_COORD(m_Route[i].vecLocation.x);
WRITE_COORD(m_Route[i].vecLocation.y);
WRITE_COORD(m_Route[i].vecLocation.z);
WRITE_COORD(m_Route[i + 1].vecLocation.x);
WRITE_COORD(m_Route[i + 1].vecLocation.y);
WRITE_COORD(m_Route[i + 1].vecLocation.z);
WRITE_SHORT(g_sModelIndexLaser);
WRITE_BYTE(0); // frame start
WRITE_BYTE(10); // framerate
WRITE_BYTE(1); // life
WRITE_BYTE(8); // width
WRITE_BYTE(0); // noise
WRITE_BYTE(r); // r, g, b
WRITE_BYTE(g); // r, g, b
WRITE_BYTE(b); // r, g, b
WRITE_BYTE(255); // brightness
WRITE_BYTE(10); // speed
MESSAGE_END();
// UTIL_ParticleEffect ( m_Route[ i ].vecLocation, g_vecZero, 255, 25 );
}
}
#endif
bool ShouldSimplify(int routeType)
{
routeType &= ~bits_MF_IS_GOAL;
//TODO: verify this this needs to be a comparison and not a bit check
if ((routeType == bits_MF_TO_PATHCORNER) || (routeType & bits_MF_DONT_SIMPLIFY) != 0)
return false;
return true;
}
//=========================================================
// RouteSimplify
//
// Attempts to make the route more direct by cutting out
// unnecessary nodes & cutting corners.
//
//=========================================================
void CBaseMonster::RouteSimplify(CBaseEntity* pTargetEnt)
{
// BUGBUG: this doesn't work 100% yet
int i, count, outCount;
Vector vecStart;
WayPoint_t outRoute[ROUTE_SIZE * 2]; // Any points except the ends can turn into 2 points in the simplified route
count = 0;
for (i = m_iRouteIndex; i < ROUTE_SIZE; i++)
{
if (0 == m_Route[i].iType)
break;
else
count++;
if ((m_Route[i].iType & bits_MF_IS_GOAL) != 0)
break;
}
// Can't simplify a direct route!
if (count < 2)
{
// DrawRoute( pev, m_Route, m_iRouteIndex, 0, 0, 255 );
return;
}
outCount = 0;
vecStart = pev->origin;
for (i = 0; i < count - 1; i++)
{
// Don't eliminate path_corners
if (!ShouldSimplify(m_Route[m_iRouteIndex + i].iType))
{
outRoute[outCount] = m_Route[m_iRouteIndex + i];
outCount++;
}
else if (CheckLocalMove(vecStart, m_Route[m_iRouteIndex + i + 1].vecLocation, pTargetEnt, NULL) == LOCALMOVE_VALID)
{
// Skip vert
continue;
}
else
{
Vector vecTest, vecSplit;
// Halfway between this and next
vecTest = (m_Route[m_iRouteIndex + i + 1].vecLocation + m_Route[m_iRouteIndex + i].vecLocation) * 0.5;
// Halfway between this and previous
vecSplit = (m_Route[m_iRouteIndex + i].vecLocation + vecStart) * 0.5;
int iType = (m_Route[m_iRouteIndex + i].iType | bits_MF_TO_DETOUR) & ~bits_MF_NOT_TO_MASK;
if (CheckLocalMove(vecStart, vecTest, pTargetEnt, NULL) == LOCALMOVE_VALID)
{
outRoute[outCount].iType = iType;
outRoute[outCount].vecLocation = vecTest;
}
else if (CheckLocalMove(vecSplit, vecTest, pTargetEnt, NULL) == LOCALMOVE_VALID)
{
outRoute[outCount].iType = iType;
outRoute[outCount].vecLocation = vecSplit;
outRoute[outCount + 1].iType = iType;
outRoute[outCount + 1].vecLocation = vecTest;
outCount++; // Adding an extra point
}
else
{
outRoute[outCount] = m_Route[m_iRouteIndex + i];
}
}
// Get last point
vecStart = outRoute[outCount].vecLocation;
outCount++;
}
ASSERT(i < count);
outRoute[outCount] = m_Route[m_iRouteIndex + i];
outCount++;
// Terminate
outRoute[outCount].iType = 0;
ASSERT(outCount < (ROUTE_SIZE * 2));
// Copy the simplified route, disable for testing
m_iRouteIndex = 0;
for (i = 0; i < ROUTE_SIZE && i < outCount; i++)
{
m_Route[i] = outRoute[i];
}
// Terminate route
if (i < ROUTE_SIZE)
m_Route[i].iType = 0;
// Debug, test movement code
#if 0
// if ( CVAR_GET_FLOAT( "simplify" ) != 0 )
DrawRoute( pev, outRoute, 0, 255, 0, 0 );
// else
DrawRoute( pev, m_Route, m_iRouteIndex, 0, 255, 0 );
#endif
}
//=========================================================
// FBecomeProne - tries to send a monster into PRONE state.
// right now only used when a barnacle snatches someone, so
// may have some special case stuff for that.
//=========================================================
bool CBaseMonster::FBecomeProne()
{
if (FBitSet(pev->flags, FL_ONGROUND))
{
pev->flags -= FL_ONGROUND;
}
m_IdealMonsterState = MONSTERSTATE_PRONE;
return true;
}
//=========================================================
// CheckRangeAttack1
//=========================================================
bool CBaseMonster::CheckRangeAttack1(float flDot, float flDist)
{
if (flDist > 64 && flDist <= 784 && flDot >= 0.5)
{
return true;
}
return false;
}
//=========================================================
// CheckRangeAttack2
//=========================================================
bool CBaseMonster::CheckRangeAttack2(float flDot, float flDist)
{
if (flDist > 64 && flDist <= 512 && flDot >= 0.5)
{
return true;
}
return false;
}
//=========================================================
// CheckMeleeAttack1
//=========================================================
bool CBaseMonster::CheckMeleeAttack1(float flDot, float flDist)
{
// Decent fix to keep folks from kicking/punching hornets and snarks is to check the onground flag(sjb)
if (flDist <= 64 && flDot >= 0.7 && m_hEnemy != NULL && FBitSet(m_hEnemy->pev->flags, FL_ONGROUND))
{
return true;
}
return false;
}
//=========================================================
// CheckMeleeAttack2
//=========================================================
bool CBaseMonster::CheckMeleeAttack2(float flDot, float flDist)
{
if (flDist <= 64 && flDot >= 0.7)
{
return true;
}
return false;
}
//=========================================================
// CheckAttacks - sets all of the bits for attacks that the
// monster is capable of carrying out on the passed entity.
//=========================================================
void CBaseMonster::CheckAttacks(CBaseEntity* pTarget, float flDist)
{
Vector2D vec2LOS;
float flDot;
UTIL_MakeVectors(pev->angles);
vec2LOS = (pTarget->pev->origin - pev->origin).Make2D();
vec2LOS = vec2LOS.Normalize();
flDot = DotProduct(vec2LOS, gpGlobals->v_forward.Make2D());
// we know the enemy is in front now. We'll find which attacks the monster is capable of by
// checking for corresponding Activities in the model file, then do the simple checks to validate
// those attack types.
// Clear all attack conditions
ClearConditions(bits_COND_CAN_RANGE_ATTACK1 | bits_COND_CAN_RANGE_ATTACK2 | bits_COND_CAN_MELEE_ATTACK1 | bits_COND_CAN_MELEE_ATTACK2);
if ((m_afCapability & bits_CAP_RANGE_ATTACK1) != 0)
{
if (CheckRangeAttack1(flDot, flDist))
SetConditions(bits_COND_CAN_RANGE_ATTACK1);
}
if ((m_afCapability & bits_CAP_RANGE_ATTACK2) != 0)
{
if (CheckRangeAttack2(flDot, flDist))
SetConditions(bits_COND_CAN_RANGE_ATTACK2);
}
if ((m_afCapability & bits_CAP_MELEE_ATTACK1) != 0)
{
if (CheckMeleeAttack1(flDot, flDist))
SetConditions(bits_COND_CAN_MELEE_ATTACK1);
}
if ((m_afCapability & bits_CAP_MELEE_ATTACK2) != 0)
{
if (CheckMeleeAttack2(flDot, flDist))
SetConditions(bits_COND_CAN_MELEE_ATTACK2);
}
}
//=========================================================
// CanCheckAttacks - prequalifies a monster to do more fine
// checking of potential attacks.
//=========================================================
bool CBaseMonster::FCanCheckAttacks()
{
if (HasConditions(bits_COND_SEE_ENEMY) && !HasConditions(bits_COND_ENEMY_TOOFAR))
{
return true;
}
return false;
}
//=========================================================
// CheckEnemy - part of the Condition collection process,
// gets and stores data and conditions pertaining to a monster's
// enemy. Returns true if Enemy LKP was updated.
//=========================================================
bool CBaseMonster::CheckEnemy(CBaseEntity* pEnemy)
{
float flDistToEnemy;
bool iUpdatedLKP; // set this to true if you update the EnemyLKP in this function.
iUpdatedLKP = false;
ClearConditions(bits_COND_ENEMY_FACING_ME);
if (!FVisible(pEnemy))
{
ASSERT(!HasConditions(bits_COND_SEE_ENEMY));
SetConditions(bits_COND_ENEMY_OCCLUDED);
}
else
ClearConditions(bits_COND_ENEMY_OCCLUDED);
if (!pEnemy->IsAlive())
{
SetConditions(bits_COND_ENEMY_DEAD);
ClearConditions(bits_COND_SEE_ENEMY | bits_COND_ENEMY_OCCLUDED);
return false;
}
Vector vecEnemyPos = pEnemy->pev->origin;
// distance to enemy's origin
flDistToEnemy = (vecEnemyPos - pev->origin).Length();
vecEnemyPos.z += pEnemy->pev->size.z * 0.5;
// distance to enemy's head
float flDistToEnemy2 = (vecEnemyPos - pev->origin).Length();
if (flDistToEnemy2 < flDistToEnemy)
flDistToEnemy = flDistToEnemy2;
else
{
// distance to enemy's feet
vecEnemyPos.z -= pEnemy->pev->size.z;
float flDistToEnemy2 = (vecEnemyPos - pev->origin).Length();
if (flDistToEnemy2 < flDistToEnemy)
flDistToEnemy = flDistToEnemy2;
}
if (HasConditions(bits_COND_SEE_ENEMY))
{
CBaseMonster* pEnemyMonster;
iUpdatedLKP = true;
m_vecEnemyLKP = pEnemy->pev->origin;
pEnemyMonster = pEnemy->MyMonsterPointer();
if (pEnemyMonster)
{
if (pEnemyMonster->FInViewCone(this))
{
SetConditions(bits_COND_ENEMY_FACING_ME);
}
else
ClearConditions(bits_COND_ENEMY_FACING_ME);
}
if (pEnemy->pev->velocity != Vector(0, 0, 0))
{
// trail the enemy a bit
m_vecEnemyLKP = m_vecEnemyLKP - pEnemy->pev->velocity * RANDOM_FLOAT(-0.05, 0);
}
else
{
// UNDONE: use pev->oldorigin?
}
}
else if (!HasConditions(bits_COND_ENEMY_OCCLUDED | bits_COND_SEE_ENEMY) && (flDistToEnemy <= 256))
{
// if the enemy is not occluded, and unseen, that means it is behind or beside the monster.
// if the enemy is near enough the monster, we go ahead and let the monster know where the
// enemy is.
iUpdatedLKP = true;
m_vecEnemyLKP = pEnemy->pev->origin;
}
if (flDistToEnemy >= m_flDistTooFar)
{
// enemy is very far away from monster
SetConditions(bits_COND_ENEMY_TOOFAR);
}
else
ClearConditions(bits_COND_ENEMY_TOOFAR);
if (FCanCheckAttacks())
{
CheckAttacks(m_hEnemy, flDistToEnemy);
}
if (m_movementGoal == MOVEGOAL_ENEMY)
{
for (int i = m_iRouteIndex; i < ROUTE_SIZE; i++)
{
if (m_Route[i].iType == (bits_MF_IS_GOAL | bits_MF_TO_ENEMY))
{
// UNDONE: Should we allow monsters to override this distance (80?)
if ((m_Route[i].vecLocation - m_vecEnemyLKP).Length() > 80)
{
// Refresh
FRefreshRoute();
return iUpdatedLKP;
}
}
}
}
return iUpdatedLKP;
}
//=========================================================
// PushEnemy - remember the last few enemies, always remember the player
//=========================================================
void CBaseMonster::PushEnemy(CBaseEntity* pEnemy, Vector& vecLastKnownPos)
{
int i;
if (pEnemy == NULL)
return;
// UNDONE: blah, this is bad, we should use a stack but I'm too lazy to code one.
for (i = 0; i < MAX_OLD_ENEMIES; i++)
{
if (m_hOldEnemy[i] == pEnemy)
return;
if (m_hOldEnemy[i] == NULL) // someone died, reuse their slot
break;
}
if (i >= MAX_OLD_ENEMIES)
return;
m_hOldEnemy[i] = pEnemy;
m_vecOldEnemy[i] = vecLastKnownPos;
}
//=========================================================
// PopEnemy - try remembering the last few enemies
//=========================================================
bool CBaseMonster::PopEnemy()
{
// UNDONE: blah, this is bad, we should use a stack but I'm too lazy to code one.
for (int i = MAX_OLD_ENEMIES - 1; i >= 0; i--)
{
if (m_hOldEnemy[i] != NULL)
{
if (m_hOldEnemy[i]->IsAlive()) // cheat and know when they die
{
m_hEnemy = m_hOldEnemy[i];
m_vecEnemyLKP = m_vecOldEnemy[i];
// ALERT( at_console, "remembering\n");
return true;
}
else
{
m_hOldEnemy[i] = NULL;
}
}
}
return false;
}
//=========================================================
// SetActivity
//=========================================================
void CBaseMonster::SetActivity(Activity NewActivity)
{
const Activity oldActivity = NewActivity;
m_Activity = NewActivity; // Go ahead and set this so it doesn't keep trying when the anim is not present
// In case someone calls this with something other than the ideal activity
m_IdealActivity = m_Activity;
const int iSequence = LookupActivity(NewActivity);
// Set to the desired anim, or default anim if the desired is not present
if (iSequence > ACTIVITY_NOT_AVAILABLE)
{
if (pev->sequence != iSequence || !m_fSequenceLoops)
{
// don't reset frame between walk and run
if (!(oldActivity == ACT_WALK || oldActivity == ACT_RUN) || !(NewActivity == ACT_WALK || NewActivity == ACT_RUN))
pev->frame = 0;
}
pev->sequence = iSequence; // Set to the reset anim (if it's there)
ResetSequenceInfo();
SetYawSpeed();
}
else
{
// Not available try to get default anim
ALERT(at_aiconsole, "%s has no sequence for act:%d\n", STRING(pev->classname), NewActivity);
pev->sequence = 0; // Set to the reset anim (if it's there)
}
}
//=========================================================
// SetSequenceByName
//=========================================================
void CBaseMonster::SetSequenceByName(const char* szSequence)
{
int iSequence;
iSequence = LookupSequence(szSequence);
// Set to the desired anim, or default anim if the desired is not present
if (iSequence > ACTIVITY_NOT_AVAILABLE)
{
if (pev->sequence != iSequence || !m_fSequenceLoops)
{
pev->frame = 0;
}
pev->sequence = iSequence; // Set to the reset anim (if it's there)
ResetSequenceInfo();
SetYawSpeed();
}
else
{
// Not available try to get default anim
ALERT(at_aiconsole, "%s has no sequence named:%f\n", STRING(pev->classname), szSequence);
pev->sequence = 0; // Set to the reset anim (if it's there)
}
}
//=========================================================
// CheckLocalMove - returns true if the caller can walk a
// straight line from its current origin to the given
// location. If so, don't use the node graph!
//
// if a valid pointer to a int is passed, the function
// will fill that int with the distance that the check
// reached before hitting something. THIS ONLY HAPPENS
// IF THE LOCAL MOVE CHECK FAILS!
//
// !!!PERFORMANCE - should we try to load balance this?
// DON"T USE SETORIGIN!
//=========================================================
#define LOCAL_STEP_SIZE 16
int CBaseMonster::CheckLocalMove(const Vector& vecStart, const Vector& vecEnd, CBaseEntity* pTarget, float* pflDist)
{
Vector vecStartPos; // record monster's position before trying the move
float flYaw;
float flDist;
float flStep, stepSize;
int iReturn;
vecStartPos = pev->origin;
flYaw = UTIL_VecToYaw(vecEnd - vecStart); // build a yaw that points to the goal.
flDist = (vecEnd - vecStart).Length2D(); // get the distance.
iReturn = LOCALMOVE_VALID; // assume everything will be ok.
// move the monster to the start of the local move that's to be checked.
UTIL_SetOrigin(pev, vecStart); // !!!BUGBUG - won't this fire triggers? - nope, SetOrigin doesn't fire
if ((pev->flags & (FL_FLY | FL_SWIM)) == 0)
{
DROP_TO_FLOOR(ENT(pev)); //make sure monster is on the floor!
}
//pev->origin.z = vecStartPos.z;//!!!HACKHACK
// pev->origin = vecStart;
/*
if ( flDist > 1024 )
{
// !!!PERFORMANCE - this operation may be too CPU intensive to try checks this large.
// We don't lose much here, because a distance this great is very likely
// to have something in the way.
// since we've actually moved the monster during the check, undo the move.
pev->origin = vecStartPos;
return false;
}
*/
// this loop takes single steps to the goal.
for (flStep = 0; flStep < flDist; flStep += LOCAL_STEP_SIZE)
{
stepSize = LOCAL_STEP_SIZE;
if ((flStep + LOCAL_STEP_SIZE) >= (flDist - 1))
stepSize = (flDist - flStep) - 1;
// UTIL_ParticleEffect ( pev->origin, g_vecZero, 255, 25 );
if (!WALK_MOVE(ENT(pev), flYaw, stepSize, WALKMOVE_CHECKONLY))
{ // can't take the next step, fail!
if (pflDist != NULL)
{
*pflDist = flStep;
}
if (pTarget && pTarget->edict() == gpGlobals->trace_ent)
{
// if this step hits target ent, the move is legal.
iReturn = LOCALMOVE_VALID;
break;
}
else
{
// If we're going toward an entity, and we're almost getting there, it's OK.
// if ( pTarget && fabs( flDist - iStep ) < LOCAL_STEP_SIZE )
// fReturn = true;
// else
iReturn = LOCALMOVE_INVALID;
break;
}
}
}
if (iReturn == LOCALMOVE_VALID && (pev->flags & (FL_FLY | FL_SWIM)) == 0 && (!pTarget || (pTarget->pev->flags & FL_ONGROUND) != 0))
{
// The monster can move to a spot UNDER the target, but not to it. Don't try to triangulate, go directly to the node graph.
// UNDONE: Magic # 64 -- this used to be pev->size.z but that won't work for small creatures like the headcrab
if (fabs(vecEnd.z - pev->origin.z) > 64)
{
iReturn = LOCALMOVE_INVALID_DONT_TRIANGULATE;
}
}
/*
// uncommenting this block will draw a line representing the nearest legal move.
WRITE_BYTE(MSG_BROADCAST, SVC_TEMPENTITY);
WRITE_BYTE(MSG_BROADCAST, TE_SHOWLINE);
WRITE_COORD(MSG_BROADCAST, pev->origin.x);
WRITE_COORD(MSG_BROADCAST, pev->origin.y);
WRITE_COORD(MSG_BROADCAST, pev->origin.z);
WRITE_COORD(MSG_BROADCAST, vecStart.x);
WRITE_COORD(MSG_BROADCAST, vecStart.y);
WRITE_COORD(MSG_BROADCAST, vecStart.z);
*/
// since we've actually moved the monster during the check, undo the move.
UTIL_SetOrigin(pev, vecStartPos);
return iReturn;
}
float CBaseMonster::OpenDoorAndWait(entvars_t* pevDoor)
{
float flTravelTime = 0;
//ALERT(at_aiconsole, "A door. ");
CBaseEntity* pcbeDoor = CBaseEntity::Instance(pevDoor);
if (pcbeDoor && !pcbeDoor->IsLockedByMaster())
{
//ALERT(at_aiconsole, "unlocked! ");
pcbeDoor->Use(this, this, USE_ON, 0.0);
//ALERT(at_aiconsole, "pevDoor->nextthink = %d ms\n", (int)(1000*pevDoor->nextthink));
//ALERT(at_aiconsole, "pevDoor->ltime = %d ms\n", (int)(1000*pevDoor->ltime));
//ALERT(at_aiconsole, "pev-> nextthink = %d ms\n", (int)(1000*pev->nextthink));
//ALERT(at_aiconsole, "pev->ltime = %d ms\n", (int)(1000*pev->ltime));
flTravelTime = pevDoor->nextthink - pevDoor->ltime;
//ALERT(at_aiconsole, "Waiting %d ms\n", (int)(1000*flTravelTime));
if (!FStringNull(pcbeDoor->pev->targetname))
{
edict_t* pentTarget = NULL;
for (;;)
{
pentTarget = FIND_ENTITY_BY_TARGETNAME(pentTarget, STRING(pcbeDoor->pev->targetname));
if (VARS(pentTarget) != pcbeDoor->pev)
{
if (FNullEnt(pentTarget))
break;
if (FClassnameIs(pentTarget, STRING(pcbeDoor->pev->classname)))
{
CBaseEntity* pDoor = Instance(pentTarget);
if (pDoor)
pDoor->Use(this, this, USE_ON, 0.0);
}
}
}
}
}
return gpGlobals->time + flTravelTime;
}
//=========================================================
// AdvanceRoute - poorly named function that advances the
// m_iRouteIndex. If it goes beyond ROUTE_SIZE, the route
// is refreshed.
//=========================================================
void CBaseMonster::AdvanceRoute(float distance)
{
if (m_iRouteIndex == ROUTE_SIZE - 1)
{
// time to refresh the route.
if (!FRefreshRoute())
{
ALERT(at_aiconsole, "Can't Refresh Route!!\n");
}
}
else
{
if ((m_Route[m_iRouteIndex].iType & bits_MF_IS_GOAL) == 0)
{
// If we've just passed a path_corner, advance m_pGoalEnt
if ((m_Route[m_iRouteIndex].iType & ~bits_MF_NOT_TO_MASK) == bits_MF_TO_PATHCORNER)
m_pGoalEnt = m_pGoalEnt->GetNextTarget();
// IF both waypoints are nodes, then check for a link for a door and operate it.
//
if ((m_Route[m_iRouteIndex].iType & bits_MF_TO_NODE) == bits_MF_TO_NODE && (m_Route[m_iRouteIndex + 1].iType & bits_MF_TO_NODE) == bits_MF_TO_NODE)
{
//ALERT(at_aiconsole, "SVD: Two nodes. ");
int iSrcNode = WorldGraph.FindNearestNode(m_Route[m_iRouteIndex].vecLocation, this);
int iDestNode = WorldGraph.FindNearestNode(m_Route[m_iRouteIndex + 1].vecLocation, this);
int iLink;
WorldGraph.HashSearch(iSrcNode, iDestNode, iLink);
if (iLink >= 0 && WorldGraph.m_pLinkPool[iLink].m_pLinkEnt != NULL)
{
//ALERT(at_aiconsole, "A link. ");
if (WorldGraph.HandleLinkEnt(iSrcNode, WorldGraph.m_pLinkPool[iLink].m_pLinkEnt, m_afCapability, CGraph::NODEGRAPH_DYNAMIC))
{
//ALERT(at_aiconsole, "usable.");
entvars_t* pevDoor = WorldGraph.m_pLinkPool[iLink].m_pLinkEnt;
if (pevDoor)
{
m_flMoveWaitFinished = OpenDoorAndWait(pevDoor);
// ALERT( at_aiconsole, "Wating for door %.2f\n", m_flMoveWaitFinished-gpGlobals->time );
}
}
}
//ALERT(at_aiconsole, "\n");
}
m_iRouteIndex++;
}
else // At goal!!!
{
if (distance < m_flGroundSpeed * 0.2 /* FIX */)
{
MovementComplete();
}
}
}
}
int CBaseMonster::RouteClassify(int iMoveFlag)
{
int movementGoal;
movementGoal = MOVEGOAL_NONE;
if ((iMoveFlag & bits_MF_TO_TARGETENT) != 0)
movementGoal = MOVEGOAL_TARGETENT;
else if ((iMoveFlag & bits_MF_TO_ENEMY) != 0)
movementGoal = MOVEGOAL_ENEMY;
else if ((iMoveFlag & bits_MF_TO_PATHCORNER) != 0)
movementGoal = MOVEGOAL_PATHCORNER;
else if ((iMoveFlag & bits_MF_TO_NODE) != 0)
movementGoal = MOVEGOAL_NODE;
else if ((iMoveFlag & bits_MF_TO_LOCATION) != 0)
movementGoal = MOVEGOAL_LOCATION;
return movementGoal;
}
//=========================================================
// BuildRoute
//=========================================================
bool CBaseMonster::BuildRoute(const Vector& vecGoal, int iMoveFlag, CBaseEntity* pTarget)
{
float flDist;
Vector vecApex;
int iLocalMove;
RouteNew();
m_movementGoal = RouteClassify(iMoveFlag);
// so we don't end up with no moveflags
m_Route[0].vecLocation = vecGoal;
m_Route[0].iType = iMoveFlag | bits_MF_IS_GOAL;
// check simple local move
iLocalMove = CheckLocalMove(pev->origin, vecGoal, pTarget, &flDist);
if (iLocalMove == LOCALMOVE_VALID)
{
// monster can walk straight there!
return true;
}
// try to triangulate around any obstacles.
else if (iLocalMove != LOCALMOVE_INVALID_DONT_TRIANGULATE && FTriangulate(pev->origin, vecGoal, flDist, pTarget, &vecApex))
{
// there is a slightly more complicated path that allows the monster to reach vecGoal
m_Route[0].vecLocation = vecApex;
m_Route[0].iType = (iMoveFlag | bits_MF_TO_DETOUR);
m_Route[1].vecLocation = vecGoal;
m_Route[1].iType = iMoveFlag | bits_MF_IS_GOAL;
/*
WRITE_BYTE(MSG_BROADCAST, SVC_TEMPENTITY);
WRITE_BYTE(MSG_BROADCAST, TE_SHOWLINE);
WRITE_COORD(MSG_BROADCAST, vecApex.x );
WRITE_COORD(MSG_BROADCAST, vecApex.y );
WRITE_COORD(MSG_BROADCAST, vecApex.z );
WRITE_COORD(MSG_BROADCAST, vecApex.x );
WRITE_COORD(MSG_BROADCAST, vecApex.y );
WRITE_COORD(MSG_BROADCAST, vecApex.z + 128 );
*/
RouteSimplify(pTarget);
return true;
}
// last ditch, try nodes
if (FGetNodeRoute(vecGoal))
{
// ALERT ( at_console, "Can get there on nodes\n" );
m_vecMoveGoal = vecGoal;
RouteSimplify(pTarget);
return true;
}
// b0rk
return false;
}
//=========================================================
// InsertWaypoint - Rebuilds the existing route so that the
// supplied vector and moveflags are the first waypoint in
// the route, and fills the rest of the route with as much
// of the pre-existing route as possible
//=========================================================
void CBaseMonster::InsertWaypoint(Vector vecLocation, int afMoveFlags)
{
int i, type;
// we have to save some Index and Type information from the real
// path_corner or node waypoint that the monster was trying to reach. This makes sure that data necessary
// to refresh the original path exists even in the new waypoints that don't correspond directy to a path_corner
// or node.
type = afMoveFlags | (m_Route[m_iRouteIndex].iType & ~bits_MF_NOT_TO_MASK);
for (i = ROUTE_SIZE - 1; i > 0; i--)
m_Route[i] = m_Route[i - 1];
m_Route[m_iRouteIndex].vecLocation = vecLocation;
m_Route[m_iRouteIndex].iType = type;
}
//=========================================================
// FTriangulate - tries to overcome local obstacles by
// triangulating a path around them.
//
// iApexDist is how far the obstruction that we are trying
// to triangulate around is from the monster.
//=========================================================
bool CBaseMonster::FTriangulate(const Vector& vecStart, const Vector& vecEnd, float flDist, CBaseEntity* pTargetEnt, Vector* pApex)
{
Vector vecDir;
Vector vecForward;
Vector vecLeft; // the spot we'll try to triangulate to on the left
Vector vecRight; // the spot we'll try to triangulate to on the right
Vector vecTop; // the spot we'll try to triangulate to on the top
Vector vecBottom; // the spot we'll try to triangulate to on the bottom
Vector vecFarSide; // the spot that we'll move to after hitting the triangulated point, before moving on to our normal goal.
int i;
float sizeX, sizeZ;
// If the hull width is less than 24, use 24 because CheckLocalMove uses a min of
// 24.
sizeX = pev->size.x;
if (sizeX < 24.0)
sizeX = 24.0;
else if (sizeX > 48.0)
sizeX = 48.0;
sizeZ = pev->size.z;
//if (sizeZ < 24.0)
// sizeZ = 24.0;
vecForward = (vecEnd - vecStart).Normalize();
Vector vecDirUp(0, 0, 1);
vecDir = CrossProduct(vecForward, vecDirUp);
// start checking right about where the object is, picking two equidistant starting points, one on
// the left, one on the right. As we progress through the loop, we'll push these away from the obstacle,
// hoping to find a way around on either side. pev->size.x is added to the ApexDist in order to help select
// an apex point that insures that the monster is sufficiently past the obstacle before trying to turn back
// onto its original course.
vecLeft = pev->origin + (vecForward * (flDist + sizeX)) - vecDir * (sizeX * 3);
vecRight = pev->origin + (vecForward * (flDist + sizeX)) + vecDir * (sizeX * 3);
if (pev->movetype == MOVETYPE_FLY)
{
vecTop = pev->origin + (vecForward * flDist) + (vecDirUp * sizeZ * 3);
vecBottom = pev->origin + (vecForward * flDist) - (vecDirUp * sizeZ * 3);
}
vecFarSide = m_Route[m_iRouteIndex].vecLocation;
vecDir = vecDir * sizeX * 2;
if (pev->movetype == MOVETYPE_FLY)
vecDirUp = vecDirUp * sizeZ * 2;
for (i = 0; i < 8; i++)
{
// Debug, Draw the triangulation
#if 0
MESSAGE_BEGIN( MSG_BROADCAST, SVC_TEMPENTITY );
WRITE_BYTE( TE_SHOWLINE);
WRITE_COORD( pev->origin.x );
WRITE_COORD( pev->origin.y );
WRITE_COORD( pev->origin.z );
WRITE_COORD( vecRight.x );
WRITE_COORD( vecRight.y );
WRITE_COORD( vecRight.z );
MESSAGE_END();
MESSAGE_BEGIN( MSG_BROADCAST, SVC_TEMPENTITY );
WRITE_BYTE( TE_SHOWLINE );
WRITE_COORD( pev->origin.x );
WRITE_COORD( pev->origin.y );
WRITE_COORD( pev->origin.z );
WRITE_COORD( vecLeft.x );
WRITE_COORD( vecLeft.y );
WRITE_COORD( vecLeft.z );
MESSAGE_END();
#endif
#if 0
if (pev->movetype == MOVETYPE_FLY)
{
MESSAGE_BEGIN( MSG_BROADCAST, SVC_TEMPENTITY );
WRITE_BYTE( TE_SHOWLINE );
WRITE_COORD( pev->origin.x );
WRITE_COORD( pev->origin.y );
WRITE_COORD( pev->origin.z );
WRITE_COORD( vecTop.x );
WRITE_COORD( vecTop.y );
WRITE_COORD( vecTop.z );
MESSAGE_END();
MESSAGE_BEGIN( MSG_BROADCAST, SVC_TEMPENTITY );
WRITE_BYTE( TE_SHOWLINE );
WRITE_COORD( pev->origin.x );
WRITE_COORD( pev->origin.y );
WRITE_COORD( pev->origin.z );
WRITE_COORD( vecBottom.x );
WRITE_COORD( vecBottom.y );
WRITE_COORD( vecBottom.z );
MESSAGE_END();
}
#endif
if (CheckLocalMove(pev->origin, vecRight, pTargetEnt, NULL) == LOCALMOVE_VALID)
{
if (CheckLocalMove(vecRight, vecFarSide, pTargetEnt, NULL) == LOCALMOVE_VALID)
{
if (pApex)
{
*pApex = vecRight;
}
return true;
}
}
if (CheckLocalMove(pev->origin, vecLeft, pTargetEnt, NULL) == LOCALMOVE_VALID)
{
if (CheckLocalMove(vecLeft, vecFarSide, pTargetEnt, NULL) == LOCALMOVE_VALID)
{
if (pApex)
{
*pApex = vecLeft;
}
return true;
}
}
if (pev->movetype == MOVETYPE_FLY)
{
if (CheckLocalMove(pev->origin, vecTop, pTargetEnt, NULL) == LOCALMOVE_VALID)
{
if (CheckLocalMove(vecTop, vecFarSide, pTargetEnt, NULL) == LOCALMOVE_VALID)
{
if (pApex)
{
*pApex = vecTop;
//ALERT(at_aiconsole, "triangulate over\n");
}
return true;
}
}
#if 1
if (CheckLocalMove(pev->origin, vecBottom, pTargetEnt, NULL) == LOCALMOVE_VALID)
{
if (CheckLocalMove(vecBottom, vecFarSide, pTargetEnt, NULL) == LOCALMOVE_VALID)
{
if (pApex)
{
*pApex = vecBottom;
//ALERT(at_aiconsole, "triangulate under\n");
}
return true;
}
}
#endif
}
vecRight = vecRight + vecDir;
vecLeft = vecLeft - vecDir;
if (pev->movetype == MOVETYPE_FLY)
{
vecTop = vecTop + vecDirUp;
vecBottom = vecBottom - vecDirUp;
}
}
return false;
}
//=========================================================
// Move - take a single step towards the next ROUTE location
//=========================================================
#define DIST_TO_CHECK 200
void CBaseMonster::Move(float flInterval)
{
float flWaypointDist;
float flCheckDist;
float flDist; // how far the lookahead check got before hitting an object.
Vector vecDir;
Vector vecApex;
CBaseEntity* pTargetEnt;
// Don't move if no valid route
if (FRouteClear())
{
// If we still have a movement goal, then this is probably a route truncated by SimplifyRoute()
// so refresh it.
if (m_movementGoal == MOVEGOAL_NONE || !FRefreshRoute())
{
ALERT(at_aiconsole, "Tried to move with no route!\n");
TaskFail();
return;
}
}
if (m_flMoveWaitFinished > gpGlobals->time)
return;
// Debug, test movement code
#if 0
// if ( CVAR_GET_FLOAT("stopmove" ) != 0 )
{
if ( m_movementGoal == MOVEGOAL_ENEMY )
RouteSimplify( m_hEnemy );
else
RouteSimplify( m_hTargetEnt );
FRefreshRoute();
return;
}
#else
// Debug, draw the route
// DrawRoute( pev, m_Route, m_iRouteIndex, 0, 200, 0 );
#endif
// if the monster is moving directly towards an entity (enemy for instance), we'll set this pointer
// to that entity for the CheckLocalMove and Triangulate functions.
pTargetEnt = NULL;
// local move to waypoint.
vecDir = (m_Route[m_iRouteIndex].vecLocation - pev->origin).Normalize();
flWaypointDist = (m_Route[m_iRouteIndex].vecLocation - pev->origin).Length2D();
MakeIdealYaw(m_Route[m_iRouteIndex].vecLocation);
ChangeYaw(pev->yaw_speed);
// if the waypoint is closer than CheckDist, CheckDist is the dist to waypoint
if (flWaypointDist < DIST_TO_CHECK)
{
flCheckDist = flWaypointDist;
}
else
{
flCheckDist = DIST_TO_CHECK;
}
if ((m_Route[m_iRouteIndex].iType & (~bits_MF_NOT_TO_MASK)) == bits_MF_TO_ENEMY)
{
// only on a PURE move to enemy ( i.e., ONLY MF_TO_ENEMY set, not MF_TO_ENEMY and DETOUR )
pTargetEnt = m_hEnemy;
}
else if ((m_Route[m_iRouteIndex].iType & ~bits_MF_NOT_TO_MASK) == bits_MF_TO_TARGETENT)
{
pTargetEnt = m_hTargetEnt;
}
// !!!BUGBUG - CheckDist should be derived from ground speed.
// If this fails, it should be because of some dynamic entity blocking this guy.
// We've already checked this path, so we should wait and time out if the entity doesn't move
flDist = 0;
if (CheckLocalMove(pev->origin, pev->origin + vecDir * flCheckDist, pTargetEnt, &flDist) != LOCALMOVE_VALID)
{
CBaseEntity* pBlocker;
// Can't move, stop
Stop();
// Blocking entity is in global trace_ent
pBlocker = CBaseEntity::Instance(gpGlobals->trace_ent);
if (pBlocker)
{
DispatchBlocked(edict(), pBlocker->edict());
}
if (pBlocker && m_moveWaitTime > 0 && pBlocker->IsMoving() && !pBlocker->IsPlayer() && (gpGlobals->time - m_flMoveWaitFinished) > 3.0)
{
// Can we still move toward our target?
if (flDist < m_flGroundSpeed)
{
// No, Wait for a second
m_flMoveWaitFinished = gpGlobals->time + m_moveWaitTime;
return;
}
// Ok, still enough room to take a step
}
else
{
// try to triangulate around whatever is in the way.
if (FTriangulate(pev->origin, m_Route[m_iRouteIndex].vecLocation, flDist, pTargetEnt, &vecApex))
{
InsertWaypoint(vecApex, bits_MF_TO_DETOUR);
RouteSimplify(pTargetEnt);
}
else
{
// ALERT ( at_aiconsole, "Couldn't Triangulate\n" );
Stop();
// Only do this once until your route is cleared
if (m_moveWaitTime > 0 && (m_afMemory & bits_MEMORY_MOVE_FAILED) == 0)
{
FRefreshRoute();
if (FRouteClear())
{
TaskFail();
}
else
{
// Don't get stuck
if ((gpGlobals->time - m_flMoveWaitFinished) < 0.2)
Remember(bits_MEMORY_MOVE_FAILED);
m_flMoveWaitFinished = gpGlobals->time + 0.1;
}
}
else
{
TaskFail();
ALERT(at_aiconsole, "%s Failed to move (%d)!\n", STRING(pev->classname), static_cast<int>(HasMemory(bits_MEMORY_MOVE_FAILED)));
//ALERT( at_aiconsole, "%f, %f, %f\n", pev->origin.z, (pev->origin + (vecDir * flCheckDist)).z, m_Route[m_iRouteIndex].vecLocation.z );
}
return;
}
}
}
// close enough to the target, now advance to the next target. This is done before actually reaching
// the target so that we get a nice natural turn while moving.
if (ShouldAdvanceRoute(flWaypointDist)) ///!!!BUGBUG- magic number
{
AdvanceRoute(flWaypointDist);
}
// Might be waiting for a door
if (m_flMoveWaitFinished > gpGlobals->time)
{
Stop();
return;
}
// UNDONE: this is a hack to quit moving farther than it has looked ahead.
if (flCheckDist < m_flGroundSpeed * flInterval)
{
flInterval = flCheckDist / m_flGroundSpeed;
// ALERT( at_console, "%.02f\n", flInterval );
}
MoveExecute(pTargetEnt, vecDir, flInterval);
if (MovementIsComplete())
{
Stop();
RouteClear();
}
}
bool CBaseMonster::ShouldAdvanceRoute(float flWaypointDist)
{
if (flWaypointDist <= MONSTER_CUT_CORNER_DIST)
{
// ALERT( at_console, "cut %f\n", flWaypointDist );
return true;
}
return false;
}
void CBaseMonster::MoveExecute(CBaseEntity* pTargetEnt, const Vector& vecDir, float flInterval)
{
// float flYaw = UTIL_VecToYaw ( m_Route[ m_iRouteIndex ].vecLocation - pev->origin );// build a yaw that points to the goal.
// WALK_MOVE( ENT(pev), flYaw, m_flGroundSpeed * flInterval, WALKMOVE_NORMAL );
if (m_IdealActivity != m_movementActivity)
m_IdealActivity = m_movementActivity;
float flTotal = m_flGroundSpeed * pev->framerate * flInterval;
float flStep;
while (flTotal > 0.001)
{
// don't walk more than 16 units or stairs stop working
flStep = V_min(16.0f, flTotal);
UTIL_MoveToOrigin(ENT(pev), m_Route[m_iRouteIndex].vecLocation, flStep, MOVE_NORMAL);
flTotal -= flStep;
}
// ALERT( at_console, "dist %f\n", m_flGroundSpeed * pev->framerate * flInterval );
}
//=========================================================
// MonsterInit - after a monster is spawned, it needs to
// be dropped into the world, checked for mobility problems,
// and put on the proper path, if any. This function does
// all of those things after the monster spawns. Any
// initialization that should take place for all monsters
// goes here.
//=========================================================
void CBaseMonster::MonsterInit()
{
if (!g_pGameRules->FAllowMonsters())
{
pev->flags |= FL_KILLME; // Post this because some monster code modifies class data after calling this function
// REMOVE_ENTITY(ENT(pev));
return;
}
// Set fields common to all monsters
pev->effects = 0;
pev->takedamage = DAMAGE_AIM;
pev->ideal_yaw = pev->angles.y;
pev->max_health = pev->health;
pev->deadflag = DEAD_NO;
m_IdealMonsterState = MONSTERSTATE_IDLE; // Assume monster will be idle, until proven otherwise
m_IdealActivity = ACT_IDLE;
SetBits(pev->flags, FL_MONSTER);
if ((pev->spawnflags & SF_MONSTER_HITMONSTERCLIP) != 0)
pev->flags |= FL_MONSTERCLIP;
ClearSchedule();
RouteClear();
InitBoneControllers(); // FIX: should be done in Spawn
m_iHintNode = NO_NODE;
m_afMemory = MEMORY_CLEAR;
m_hEnemy = NULL;
m_flDistTooFar = 1024.0;
m_flDistLook = 2048.0;
// set eye position
SetEyePosition();
SetThink(&CBaseMonster::MonsterInitThink);
pev->nextthink = gpGlobals->time + 0.1;
SetUse(&CBaseMonster::MonsterUse);
}
//=========================================================
// MonsterInitThink - Calls StartMonster. Startmonster is
// virtual, but this function cannot be
//=========================================================
void CBaseMonster::MonsterInitThink()
{
StartMonster();
}
//=========================================================
// StartMonster - final bit of initization before a monster
// is turned over to the AI.
//=========================================================
void CBaseMonster::StartMonster()
{
// update capabilities
if (LookupActivity(ACT_RANGE_ATTACK1) != ACTIVITY_NOT_AVAILABLE)
{
m_afCapability |= bits_CAP_RANGE_ATTACK1;
}
if (LookupActivity(ACT_RANGE_ATTACK2) != ACTIVITY_NOT_AVAILABLE)
{
m_afCapability |= bits_CAP_RANGE_ATTACK2;
}
if (LookupActivity(ACT_MELEE_ATTACK1) != ACTIVITY_NOT_AVAILABLE)
{
m_afCapability |= bits_CAP_MELEE_ATTACK1;
}
if (LookupActivity(ACT_MELEE_ATTACK2) != ACTIVITY_NOT_AVAILABLE)
{
m_afCapability |= bits_CAP_MELEE_ATTACK2;
}
// Raise monster off the floor one unit, then drop to floor
if (pev->movetype != MOVETYPE_FLY && !FBitSet(pev->spawnflags, SF_MONSTER_FALL_TO_GROUND))
{
pev->origin.z += 1;
DROP_TO_FLOOR(ENT(pev));
// Try to move the monster to make sure it's not stuck in a brush.
if (!WALK_MOVE(ENT(pev), 0, 0, WALKMOVE_NORMAL))
{
ALERT(at_error, "Monster %s stuck in wall--level design error", STRING(pev->classname));
pev->effects = EF_BRIGHTFIELD;
}
}
else
{
pev->flags &= ~FL_ONGROUND;
}
if (!FStringNull(pev->target)) // this monster has a target
{
// Find the monster's initial target entity, stash it
m_pGoalEnt = CBaseEntity::Instance(FIND_ENTITY_BY_TARGETNAME(NULL, STRING(pev->target)));
if (!m_pGoalEnt)
{
ALERT(at_error, "ReadyMonster()--%s couldn't find target %s", STRING(pev->classname), STRING(pev->target));
}
else
{
// Monster will start turning towards his destination
MakeIdealYaw(m_pGoalEnt->pev->origin);
// JAY: How important is this error message? Big Momma doesn't obey this rule, so I took it out.
#if 0
// At this point, we expect only a path_corner as initial goal
if (!FClassnameIs( m_pGoalEnt->pev, "path_corner"))
{
ALERT(at_warning, "ReadyMonster--monster's initial goal '%s' is not a path_corner", STRING(pev->target));
}
#endif
// set the monster up to walk a path corner path.
// !!!BUGBUG - this is a minor bit of a hack.
// JAYJAY
m_movementGoal = MOVEGOAL_PATHCORNER;
if (pev->movetype == MOVETYPE_FLY)
m_movementActivity = ACT_FLY;
else
m_movementActivity = ACT_WALK;
if (!FRefreshRoute())
{
ALERT(at_aiconsole, "Can't Create Route!\n");
}
SetState(MONSTERSTATE_IDLE);
ChangeSchedule(GetScheduleOfType(SCHED_IDLE_WALK));
}
}
//SetState ( m_IdealMonsterState );
//SetActivity ( m_IdealActivity );
// Delay drop to floor to make sure each door in the level has had its chance to spawn
// Spread think times so that they don't all happen at the same time (Carmack)
SetThink(&CBaseMonster::CallMonsterThink);
pev->nextthink += RANDOM_FLOAT(0.1, 0.4); // spread think times.
if (!FStringNull(pev->targetname)) // wait until triggered
{
SetState(MONSTERSTATE_IDLE);
// UNDONE: Some scripted sequence monsters don't have an idle?
SetActivity(ACT_IDLE);
ChangeSchedule(GetScheduleOfType(SCHED_WAIT_TRIGGER));
}
}
void CBaseMonster::MovementComplete()
{
switch (m_iTaskStatus)
{
case TASKSTATUS_NEW:
case TASKSTATUS_RUNNING:
m_iTaskStatus = TASKSTATUS_RUNNING_TASK;
break;
case TASKSTATUS_RUNNING_MOVEMENT:
TaskComplete();
break;
case TASKSTATUS_RUNNING_TASK:
ALERT(at_error, "Movement completed twice!\n");
break;
case TASKSTATUS_COMPLETE:
break;
}
m_movementGoal = MOVEGOAL_NONE;
}
bool CBaseMonster::TaskIsRunning()
{
if (m_iTaskStatus != TASKSTATUS_COMPLETE &&
m_iTaskStatus != TASKSTATUS_RUNNING_MOVEMENT)
return true;
return false;
}
//=========================================================
// IRelationship - returns an integer that describes the
// relationship between two types of monster.
//=========================================================
int CBaseMonster::IRelationship(CBaseEntity* pTarget)
{
static int iEnemy[14][14] =
{// NONE MACH PLYR HPASS HMIL AMIL APASS AMONST APREY APRED INSECT PLRALY PBWPN ABWPN
/*NONE*/ {R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO},
/*MACHINE*/ {R_NO, R_NO, R_DL, R_DL, R_NO, R_DL, R_DL, R_DL, R_DL, R_DL, R_NO, R_DL, R_DL, R_DL},
/*PLAYER*/ {R_NO, R_DL, R_NO, R_NO, R_DL, R_DL, R_DL, R_DL, R_DL, R_DL, R_NO, R_NO, R_DL, R_DL},
/*HUMANPASSIVE*/ {R_NO, R_NO, R_AL, R_AL, R_HT, R_HT, R_NO, R_HT, R_DL, R_DL, R_NO, R_AL, R_NO, R_NO},
/*HUMANMILITAR*/ {R_NO, R_NO, R_HT, R_DL, R_NO, R_HT, R_DL, R_DL, R_DL, R_DL, R_NO, R_HT, R_NO, R_NO},
/*ALIENMILITAR*/ {R_NO, R_DL, R_HT, R_DL, R_HT, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_DL, R_NO, R_NO},
/*ALIENPASSIVE*/ {R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO},
/*ALIENMONSTER*/ {R_NO, R_DL, R_DL, R_DL, R_DL, R_NO, R_NO, R_NO, R_NO, R_NO, R_NO, R_DL, R_NO, R_NO},
/*ALIENPREY */ {R_NO, R_NO, R_DL, R_DL, R_DL, R_NO, R_NO, R_NO, R_NO, R_FR, R_NO, R_DL, R_NO, R_NO},
/*ALIENPREDATO*/ {R_NO, R_NO, R_DL, R_DL, R_DL, R_NO, R_NO, R_NO, R_HT, R_DL, R_NO, R_DL, R_NO, R_NO},
/*INSECT*/ {R_FR, R_FR, R_FR, R_FR, R_FR, R_NO, R_FR, R_FR, R_FR, R_FR, R_NO, R_FR, R_NO, R_NO},
/*PLAYERALLY*/ {R_NO, R_DL, R_AL, R_AL, R_DL, R_DL, R_DL, R_DL, R_DL, R_DL, R_NO, R_NO, R_NO, R_NO},
/*PBIOWEAPON*/ {R_NO, R_NO, R_DL, R_DL, R_DL, R_DL, R_DL, R_DL, R_DL, R_DL, R_NO, R_DL, R_NO, R_DL},
/*ABIOWEAPON*/ {R_NO, R_NO, R_DL, R_DL, R_DL, R_AL, R_NO, R_DL, R_DL, R_NO, R_NO, R_DL, R_DL, R_NO}};
return iEnemy[Classify()][pTarget->Classify()];
}
//=========================================================
// FindCover - tries to find a nearby node that will hide
// the caller from its enemy.
//
// If supplied, search will return a node at least as far
// away as MinDist, but no farther than MaxDist.
// if MaxDist isn't supplied, it defaults to a reasonable
// value
//=========================================================
// UNDONE: Should this find the nearest node?
//float CGraph::PathLength( int iStart, int iDest, int iHull, int afCapMask )
bool CBaseMonster::FindCover(Vector vecThreat, Vector vecViewOffset, float flMinDist, float flMaxDist)
{
int i;
int iMyHullIndex;
int iMyNode;
int iThreatNode;
float flDist;
Vector vecLookersOffset;
TraceResult tr;
if (0 == flMaxDist)
{
// user didn't supply a MaxDist, so work up a crazy one.
flMaxDist = 784;
}
if (flMinDist > 0.5 * flMaxDist)
{
#if _DEBUG
ALERT(at_console, "FindCover MinDist (%.0f) too close to MaxDist (%.0f)\n", flMinDist, flMaxDist);
#endif
flMinDist = 0.5 * flMaxDist;
}
if (0 == WorldGraph.m_fGraphPresent || 0 == WorldGraph.m_fGraphPointersSet)
{
ALERT(at_aiconsole, "Graph not ready for findcover!\n");
return false;
}
iMyNode = WorldGraph.FindNearestNode(pev->origin, this);
iThreatNode = WorldGraph.FindNearestNode(vecThreat, this);
iMyHullIndex = WorldGraph.HullIndex(this);
if (iMyNode == NO_NODE)
{
ALERT(at_aiconsole, "FindCover() - %s has no nearest node!\n", STRING(pev->classname));
return false;
}
if (iThreatNode == NO_NODE)
{
// ALERT ( at_aiconsole, "FindCover() - Threat has no nearest node!\n" );
iThreatNode = iMyNode;
// return false;
}
vecLookersOffset = vecThreat + vecViewOffset; // calculate location of enemy's eyes
// we'll do a rough sample to find nodes that are relatively nearby
for (i = 0; i < WorldGraph.m_cNodes; i++)
{
int nodeNumber = (i + WorldGraph.m_iLastCoverSearch) % WorldGraph.m_cNodes;
CNode& node = WorldGraph.Node(nodeNumber);
WorldGraph.m_iLastCoverSearch = nodeNumber + 1; // next monster that searches for cover node will start where we left off here.
// could use an optimization here!!
flDist = (pev->origin - node.m_vecOrigin).Length();
// DON'T do the trace check on a node that is farther away than a node that we've already found to
// provide cover! Also make sure the node is within the mins/maxs of the search.
if (flDist >= flMinDist && flDist < flMaxDist)
{
UTIL_TraceLine(node.m_vecOrigin + vecViewOffset, vecLookersOffset, ignore_monsters, ignore_glass, ENT(pev), &tr);
// if this node will block the threat's line of sight to me...
if (tr.flFraction != 1.0)
{
// ..and is also closer to me than the threat, or the same distance from myself and the threat the node is good.
if ((iMyNode == iThreatNode) || WorldGraph.PathLength(iMyNode, nodeNumber, iMyHullIndex, m_afCapability) <= WorldGraph.PathLength(iThreatNode, nodeNumber, iMyHullIndex, m_afCapability))
{
if (FValidateCover(node.m_vecOrigin) && MoveToLocation(ACT_RUN, 0, node.m_vecOrigin))
{
/*
MESSAGE_BEGIN( MSG_BROADCAST, SVC_TEMPENTITY );
WRITE_BYTE( TE_SHOWLINE);
WRITE_COORD( node.m_vecOrigin.x );
WRITE_COORD( node.m_vecOrigin.y );
WRITE_COORD( node.m_vecOrigin.z );
WRITE_COORD( vecLookersOffset.x );
WRITE_COORD( vecLookersOffset.y );
WRITE_COORD( vecLookersOffset.z );
MESSAGE_END();
*/
return true;
}
}
}
}
}
return false;
}
//=========================================================
// BuildNearestRoute - tries to build a route as close to the target
// as possible, even if there isn't a path to the final point.
//
// If supplied, search will return a node at least as far
// away as MinDist from vecThreat, but no farther than MaxDist.
// if MaxDist isn't supplied, it defaults to a reasonable
// value
//=========================================================
bool CBaseMonster::BuildNearestRoute(Vector vecThreat, Vector vecViewOffset, float flMinDist, float flMaxDist)
{
int i;
int iMyHullIndex;
int iMyNode;
float flDist;
Vector vecLookersOffset;
TraceResult tr;
if (0 == flMaxDist)
{
// user didn't supply a MaxDist, so work up a crazy one.
flMaxDist = 784;
}
if (flMinDist > 0.5 * flMaxDist)
{
#if _DEBUG
ALERT(at_console, "FindCover MinDist (%.0f) too close to MaxDist (%.0f)\n", flMinDist, flMaxDist);
#endif
flMinDist = 0.5 * flMaxDist;
}
if (0 == WorldGraph.m_fGraphPresent || 0 == WorldGraph.m_fGraphPointersSet)
{
ALERT(at_aiconsole, "Graph not ready for BuildNearestRoute!\n");
return false;
}
iMyNode = WorldGraph.FindNearestNode(pev->origin, this);
iMyHullIndex = WorldGraph.HullIndex(this);
if (iMyNode == NO_NODE)
{
ALERT(at_aiconsole, "BuildNearestRoute() - %s has no nearest node!\n", STRING(pev->classname));
return false;
}
vecLookersOffset = vecThreat + vecViewOffset; // calculate location of enemy's eyes
// we'll do a rough sample to find nodes that are relatively nearby
for (i = 0; i < WorldGraph.m_cNodes; i++)
{
int nodeNumber = (i + WorldGraph.m_iLastCoverSearch) % WorldGraph.m_cNodes;
CNode& node = WorldGraph.Node(nodeNumber);
WorldGraph.m_iLastCoverSearch = nodeNumber + 1; // next monster that searches for cover node will start where we left off here.
// can I get there?
if (WorldGraph.NextNodeInRoute(iMyNode, nodeNumber, iMyHullIndex, 0) != iMyNode)
{
flDist = (vecThreat - node.m_vecOrigin).Length();
// is it close?
if (flDist > flMinDist && flDist < flMaxDist)
{
// can I see where I want to be from there?
UTIL_TraceLine(node.m_vecOrigin + pev->view_ofs, vecLookersOffset, ignore_monsters, edict(), &tr);
if (tr.flFraction == 1.0)
{
// try to actually get there
if (BuildRoute(node.m_vecOrigin, bits_MF_TO_LOCATION, NULL))
{
flMaxDist = flDist;
m_vecMoveGoal = node.m_vecOrigin;
return true; // UNDONE: keep looking for something closer!
}
}
}
}
}
return false;
}
//=========================================================
// BestVisibleEnemy - this functions searches the link
// list whose head is the caller's m_pLink field, and returns
// a pointer to the enemy entity in that list that is nearest the
// caller.
//
// !!!UNDONE - currently, this only returns the closest enemy.
// we'll want to consider distance, relationship, attack types, back turned, etc.
//=========================================================
CBaseEntity* CBaseMonster::BestVisibleEnemy()
{
CBaseEntity* pReturn;
CBaseEntity* pNextEnt;
int iNearest;
int iDist;
int iBestRelationship;
iNearest = 8192; // so first visible entity will become the closest.
pNextEnt = m_pLink;
pReturn = NULL;
iBestRelationship = R_NO;
while (pNextEnt != NULL)
{
if (pNextEnt->IsAlive())
{
if (IRelationship(pNextEnt) > iBestRelationship)
{
// this entity is disliked MORE than the entity that we
// currently think is the best visible enemy. No need to do
// a distance check, just get mad at this one for now.
iBestRelationship = IRelationship(pNextEnt);
iNearest = (pNextEnt->pev->origin - pev->origin).Length();
pReturn = pNextEnt;
}
else if (IRelationship(pNextEnt) == iBestRelationship)
{
// this entity is disliked just as much as the entity that
// we currently think is the best visible enemy, so we only
// get mad at it if it is closer.
iDist = (pNextEnt->pev->origin - pev->origin).Length();
if (iDist <= iNearest)
{
iNearest = iDist;
iBestRelationship = IRelationship(pNextEnt);
pReturn = pNextEnt;
}
}
}
pNextEnt = pNextEnt->m_pLink;
}
return pReturn;
}
//=========================================================
// MakeIdealYaw - gets a yaw value for the caller that would
// face the supplied vector. Value is stuffed into the monster's
// ideal_yaw
//=========================================================
void CBaseMonster::MakeIdealYaw(Vector vecTarget)
{
Vector vecProjection;
// strafing monster needs to face 90 degrees away from its goal
if (m_movementActivity == ACT_STRAFE_LEFT)
{
vecProjection.x = -vecTarget.y;
vecProjection.y = vecTarget.x;
pev->ideal_yaw = UTIL_VecToYaw(vecProjection - pev->origin);
}
else if (m_movementActivity == ACT_STRAFE_RIGHT)
{
vecProjection.x = vecTarget.y;
vecProjection.y = vecTarget.x;
pev->ideal_yaw = UTIL_VecToYaw(vecProjection - pev->origin);
}
else
{
pev->ideal_yaw = UTIL_VecToYaw(vecTarget - pev->origin);
}
}
//=========================================================
// FlYawDiff - returns the difference ( in degrees ) between
// monster's current yaw and ideal_yaw
//
// Positive result is left turn, negative is right turn
//=========================================================
float CBaseMonster::FlYawDiff()
{
float flCurrentYaw;
flCurrentYaw = UTIL_AngleMod(pev->angles.y);
if (flCurrentYaw == pev->ideal_yaw)
{
return 0;
}
return UTIL_AngleDiff(pev->ideal_yaw, flCurrentYaw);
}
//=========================================================
// Changeyaw - turns a monster towards its ideal_yaw
//=========================================================
float CBaseMonster::ChangeYaw(int yawSpeed)
{
float ideal, current, move, speed;
current = UTIL_AngleMod(pev->angles.y);
ideal = pev->ideal_yaw;
if (current != ideal)
{
if (m_flLastYawTime == 0.0f)
{
m_flLastYawTime = gpGlobals->time - gpGlobals->frametime;
}
float delta = gpGlobals->time - m_flLastYawTime;
m_flLastYawTime = gpGlobals->time;
// Clamp delta like the engine does with frametime
if (delta > 0.25)
delta = 0.25;
speed = (float)yawSpeed * delta * 2;
move = ideal - current;
if (ideal > current)
{
if (move >= 180)
move = move - 360;
}
else
{
if (move <= -180)
move = move + 360;
}
if (move > 0)
{ // turning to the monster's left
if (move > speed)
move = speed;
}
else
{ // turning to the monster's right
if (move < -speed)
move = -speed;
}
pev->angles.y = UTIL_AngleMod(current + move);
// turn head in desired direction only if they have a turnable head
if ((m_afCapability & bits_CAP_TURN_HEAD) != 0)
{
float yaw = pev->ideal_yaw - pev->angles.y;
if (yaw > 180)
yaw -= 360;
if (yaw < -180)
yaw += 360;
// yaw *= 0.8;
SetBoneController(0, yaw);
}
}
else
move = 0;
return move;
}
//=========================================================
// VecToYaw - turns a directional vector into a yaw value
// that points down that vector.
//=========================================================
float CBaseMonster::VecToYaw(Vector vecDir)
{
if (vecDir.x == 0 && vecDir.y == 0 && vecDir.z == 0)
return pev->angles.y;
return UTIL_VecToYaw(vecDir);
}
//=========================================================
// SetEyePosition
//
// queries the monster's model for $eyeposition and copies
// that vector to the monster's view_ofs
//
//=========================================================
void CBaseMonster::SetEyePosition()
{
Vector vecEyePosition;
void* pmodel = GET_MODEL_PTR(ENT(pev));
GetEyePosition(pmodel, vecEyePosition);
pev->view_ofs = vecEyePosition;
if (pev->view_ofs == g_vecZero)
{
ALERT(at_aiconsole, "%s has no view_ofs!\n", STRING(pev->classname));
}
}
void CBaseMonster::HandleAnimEvent(MonsterEvent_t* pEvent)
{
switch (pEvent->event)
{
case SCRIPT_EVENT_DEAD:
if (m_MonsterState == MONSTERSTATE_SCRIPT)
{
pev->deadflag = DEAD_DYING;
// Kill me now! (and fade out when CineCleanup() is called)
#if _DEBUG
ALERT(at_aiconsole, "Death event: %s\n", STRING(pev->classname));
#endif
pev->health = 0;
}
#if _DEBUG
else
ALERT(at_aiconsole, "INVALID death event:%s\n", STRING(pev->classname));
#endif
break;
case SCRIPT_EVENT_NOT_DEAD:
if (m_MonsterState == MONSTERSTATE_SCRIPT)
{
pev->deadflag = DEAD_NO;
// This is for life/death sequences where the player can determine whether a character is dead or alive after the script
pev->health = pev->max_health;
}
break;
case SCRIPT_EVENT_SOUND: // Play a named wave file
EMIT_SOUND(edict(), CHAN_BODY, pEvent->options, 1.0, ATTN_IDLE);
break;
case SCRIPT_EVENT_SOUND_VOICE:
EMIT_SOUND(edict(), CHAN_VOICE, pEvent->options, 1.0, ATTN_IDLE);
break;
case SCRIPT_EVENT_SENTENCE_RND1: // Play a named sentence group 33% of the time
if (RANDOM_LONG(0, 2) == 0)
break;
[[fallthrough]];
case SCRIPT_EVENT_SENTENCE: // Play a named sentence group
SENTENCEG_PlayRndSz(edict(), pEvent->options, 1.0, ATTN_IDLE, 0, 100);
break;
case SCRIPT_EVENT_FIREEVENT: // Fire a trigger
FireTargets(pEvent->options, this, this, USE_TOGGLE, 0);
break;
case SCRIPT_EVENT_NOINTERRUPT: // Can't be interrupted from now on
if (m_pCine)
m_pCine->AllowInterrupt(false);
break;
case SCRIPT_EVENT_CANINTERRUPT: // OK to interrupt now
if (m_pCine)
m_pCine->AllowInterrupt(true);
break;
#if 0
case SCRIPT_EVENT_INAIR: // Don't DROP_TO_FLOOR()
case SCRIPT_EVENT_ENDANIMATION: // Set ending animation sequence to
break;
#endif
case MONSTER_EVENT_BODYDROP_HEAVY:
if ((pev->flags & FL_ONGROUND) != 0)
{
if (RANDOM_LONG(0, 1) == 0)
{
EMIT_SOUND_DYN(ENT(pev), CHAN_BODY, "common/bodydrop3.wav", 1, ATTN_NORM, 0, 90);
}
else
{
EMIT_SOUND_DYN(ENT(pev), CHAN_BODY, "common/bodydrop4.wav", 1, ATTN_NORM, 0, 90);
}
}
break;
case MONSTER_EVENT_BODYDROP_LIGHT:
if ((pev->flags & FL_ONGROUND) != 0)
{
if (RANDOM_LONG(0, 1) == 0)
{
EMIT_SOUND(ENT(pev), CHAN_BODY, "common/bodydrop3.wav", 1, ATTN_NORM);
}
else
{
EMIT_SOUND(ENT(pev), CHAN_BODY, "common/bodydrop4.wav", 1, ATTN_NORM);
}
}
break;
case MONSTER_EVENT_SWISHSOUND:
{
// NO MONSTER may use this anim event unless that monster's precache precaches this sound!!!
EMIT_SOUND(ENT(pev), CHAN_BODY, "zombie/claw_miss2.wav", 1, ATTN_NORM);
break;
}
default:
ALERT(at_aiconsole, "Unhandled animation event %d for %s\n", pEvent->event, STRING(pev->classname));
break;
}
}
// Combat
Vector CBaseMonster::GetGunPosition()
{
UTIL_MakeVectors(pev->angles);
// Vector vecSrc = pev->origin + gpGlobals->v_forward * 10;
//vecSrc.z = pevShooter->absmin.z + pevShooter->size.z * 0.7;
//vecSrc.z = pev->origin.z + (pev->view_ofs.z - 4);
Vector vecSrc = pev->origin + gpGlobals->v_forward * m_HackedGunPos.y + gpGlobals->v_right * m_HackedGunPos.x + gpGlobals->v_up * m_HackedGunPos.z;
return vecSrc;
}
//=========================================================
// NODE GRAPH
//=========================================================
//=========================================================
// FGetNodeRoute - tries to build an entire node path from
// the callers origin to the passed vector. If this is
// possible, ROUTE_SIZE waypoints will be copied into the
// callers m_Route. true is returned if the operation
// succeeds (path is valid) or false if failed (no path
// exists )
//=========================================================
bool CBaseMonster::FGetNodeRoute(Vector vecDest)
{
int iPath[MAX_PATH_SIZE];
int iSrcNode, iDestNode;
int iResult;
int i;
int iNumToCopy;
iSrcNode = WorldGraph.FindNearestNode(pev->origin, this);
iDestNode = WorldGraph.FindNearestNode(vecDest, this);
if (iSrcNode == -1)
{
// no node nearest self
// ALERT ( at_aiconsole, "FGetNodeRoute: No valid node near self!\n" );
return false;
}
else if (iDestNode == -1)
{
// no node nearest target
// ALERT ( at_aiconsole, "FGetNodeRoute: No valid node near target!\n" );
return false;
}
// valid src and dest nodes were found, so it's safe to proceed with
// find shortest path
int iNodeHull = WorldGraph.HullIndex(this); // make this a monster virtual function
iResult = WorldGraph.FindShortestPath(iPath, iSrcNode, iDestNode, iNodeHull, m_afCapability);
if (0 == iResult)
{
#if 1
ALERT(at_aiconsole, "No Path from %d to %d!\n", iSrcNode, iDestNode);
return false;
#else
bool bRoutingSave = WorldGraph.m_fRoutingComplete;
WorldGraph.m_fRoutingComplete = false;
iResult = WorldGraph.FindShortestPath(iPath, iSrcNode, iDestNode, iNodeHull, m_afCapability);
WorldGraph.m_fRoutingComplete = bRoutingSave;
if (0 == iResult)
{
ALERT(at_aiconsole, "No Path from %d to %d!\n", iSrcNode, iDestNode);
return false;
}
else
{
ALERT(at_aiconsole, "Routing is inconsistent!");
}
#endif
}
// there's a valid path within iPath now, so now we will fill the route array
// up with as many of the waypoints as it will hold.
// don't copy ROUTE_SIZE entries if the path returned is shorter
// than ROUTE_SIZE!!!
if (iResult < ROUTE_SIZE)
{
iNumToCopy = iResult;
}
else
{
iNumToCopy = ROUTE_SIZE;
}
for (i = 0; i < iNumToCopy; i++)
{
m_Route[i].vecLocation = WorldGraph.m_pNodes[iPath[i]].m_vecOrigin;
m_Route[i].iType = bits_MF_TO_NODE;
}
if (iNumToCopy < ROUTE_SIZE)
{
m_Route[iNumToCopy].vecLocation = vecDest;
m_Route[iNumToCopy].iType |= bits_MF_IS_GOAL;
}
return true;
}
//=========================================================
// FindHintNode
//=========================================================
int CBaseMonster::FindHintNode()
{
int i;
TraceResult tr;
if (0 == WorldGraph.m_fGraphPresent)
{
ALERT(at_aiconsole, "find_hintnode: graph not ready!\n");
return NO_NODE;
}
if (WorldGraph.m_iLastActiveIdleSearch >= WorldGraph.m_cNodes)
{
WorldGraph.m_iLastActiveIdleSearch = 0;
}
for (i = 0; i < WorldGraph.m_cNodes; i++)
{
int nodeNumber = (i + WorldGraph.m_iLastActiveIdleSearch) % WorldGraph.m_cNodes;
CNode& node = WorldGraph.Node(nodeNumber);
if (0 != node.m_sHintType)
{
// this node has a hint. Take it if it is visible, the monster likes it, and the monster has an animation to match the hint's activity.
if (FValidateHintType(node.m_sHintType))
{
if (0 == node.m_sHintActivity || LookupActivity(node.m_sHintActivity) != ACTIVITY_NOT_AVAILABLE)
{
UTIL_TraceLine(pev->origin + pev->view_ofs, node.m_vecOrigin + pev->view_ofs, ignore_monsters, ENT(pev), &tr);
if (tr.flFraction == 1.0)
{
WorldGraph.m_iLastActiveIdleSearch = nodeNumber + 1; // next monster that searches for hint nodes will start where we left off.
return nodeNumber; // take it!
}
}
}
}
}
WorldGraph.m_iLastActiveIdleSearch = 0; // start at the top of the list for the next search.
return NO_NODE;
}
void CBaseMonster::ReportAIState()
{
ALERT_TYPE level = at_console;
static const char* pStateNames[] = {"None", "Idle", "Combat", "Alert", "Hunt", "Prone", "Scripted", "PlayDead", "Dead"};
ALERT(level, "%s: ", STRING(pev->classname));
if ((int)m_MonsterState < ARRAYSIZE(pStateNames))
ALERT(level, "State: %s, ", pStateNames[m_MonsterState]);
int i = 0;
while (activity_map[i].type != 0)
{
if (activity_map[i].type == (int)m_Activity)
{
ALERT(level, "Activity %s, ", activity_map[i].name);
break;
}
i++;
}
if (m_pSchedule)
{
const char* pName = NULL;
pName = m_pSchedule->pName;
if (!pName)
pName = "Unknown";
ALERT(level, "Schedule %s, ", pName);
Task_t* pTask = GetTask();
if (pTask)
ALERT(level, "Task %d (#%d), ", pTask->iTask, m_iScheduleIndex);
}
else
ALERT(level, "No Schedule, ");
if (m_hEnemy != NULL)
ALERT(level, "\nEnemy is %s", STRING(m_hEnemy->pev->classname));
else
ALERT(level, "No enemy");
if (IsMoving())
{
ALERT(level, " Moving ");
if (m_flMoveWaitFinished > gpGlobals->time)
ALERT(level, ": Stopped for %.2f. ", m_flMoveWaitFinished - gpGlobals->time);
else if (m_IdealActivity == GetStoppedActivity())
ALERT(level, ": In stopped anim. ");
}
CSquadMonster* pSquadMonster = MySquadMonsterPointer();
if (pSquadMonster)
{
if (!pSquadMonster->InSquad())
{
ALERT(level, "not ");
}
ALERT(level, "In Squad, ");
if (!pSquadMonster->IsLeader())
{
ALERT(level, "not ");
}
ALERT(level, "Leader.");
}
ALERT(level, "\n");
ALERT(level, "Yaw speed:%3.1f,Health: %3.1f\n", pev->yaw_speed, pev->health);
if ((pev->spawnflags & SF_MONSTER_PRISONER) != 0)
ALERT(level, " PRISONER! ");
if ((pev->spawnflags & SF_MONSTER_PREDISASTER) != 0)
ALERT(level, " Pre-Disaster! ");
ALERT(level, "\n");
}
//=========================================================
// KeyValue
//
// !!! netname entvar field is used in squadmonster for groupname!!!
//=========================================================
bool CBaseMonster::KeyValue(KeyValueData* pkvd)
{
if (FStrEq(pkvd->szKeyName, "TriggerTarget"))
{
m_iszTriggerTarget = ALLOC_STRING(pkvd->szValue);
return true;
}
else if (FStrEq(pkvd->szKeyName, "TriggerCondition"))
{
m_iTriggerCondition = atoi(pkvd->szValue);
return true;
}
else if (FStrEq(pkvd->szKeyName, "allow_item_dropping"))
{
m_AllowItemDropping = atoi(pkvd->szValue) != 0;
}
return CBaseToggle::KeyValue(pkvd);
}
//=========================================================
// FCheckAITrigger - checks the monster's AI Trigger Conditions,
// if there is a condition, then checks to see if condition is
// met. If yes, the monster's TriggerTarget is fired.
//
// Returns true if the target is fired.
//=========================================================
bool CBaseMonster::FCheckAITrigger()
{
bool fFireTarget;
if (m_iTriggerCondition == AITRIGGER_NONE)
{
// no conditions, so this trigger is never fired.
return false;
}
fFireTarget = false;
switch (m_iTriggerCondition)
{
case AITRIGGER_SEEPLAYER_ANGRY_AT_PLAYER:
if (m_hEnemy != NULL && m_hEnemy->IsPlayer() && HasConditions(bits_COND_SEE_ENEMY))
{
fFireTarget = true;
}
break;
case AITRIGGER_SEEPLAYER_UNCONDITIONAL:
if (HasConditions(bits_COND_SEE_CLIENT))
{
fFireTarget = true;
}
break;
case AITRIGGER_SEEPLAYER_NOT_IN_COMBAT:
if (HasConditions(bits_COND_SEE_CLIENT) &&
m_MonsterState != MONSTERSTATE_COMBAT &&
m_MonsterState != MONSTERSTATE_PRONE &&
m_MonsterState != MONSTERSTATE_SCRIPT)
{
fFireTarget = true;
}
break;
case AITRIGGER_TAKEDAMAGE:
if ((m_afConditions & (bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE)) != 0)
{
fFireTarget = true;
}
break;
case AITRIGGER_DEATH:
if (pev->deadflag != DEAD_NO)
{
fFireTarget = true;
}
break;
case AITRIGGER_HALFHEALTH:
if (IsAlive() && pev->health <= (pev->max_health / 2))
{
fFireTarget = true;
}
break;
/*
// !!!UNDONE - no persistant game state that allows us to track these two.
case AITRIGGER_SQUADMEMBERDIE:
break;
case AITRIGGER_SQUADLEADERDIE:
break;
*/
case AITRIGGER_HEARWORLD:
if ((m_afConditions & bits_COND_HEAR_SOUND) != 0 && (m_afSoundTypes & bits_SOUND_WORLD) != 0)
{
fFireTarget = true;
}
break;
case AITRIGGER_HEARPLAYER:
if ((m_afConditions & bits_COND_HEAR_SOUND) != 0 && (m_afSoundTypes & bits_SOUND_PLAYER) != 0)
{
fFireTarget = true;
}
break;
case AITRIGGER_HEARCOMBAT:
if ((m_afConditions & bits_COND_HEAR_SOUND) != 0 && (m_afSoundTypes & bits_SOUND_COMBAT) != 0)
{
fFireTarget = true;
}
break;
}
if (fFireTarget)
{
// fire the target, then set the trigger conditions to NONE so we don't fire again
ALERT(at_aiconsole, "AI Trigger Fire Target\n");
FireTargets(STRING(m_iszTriggerTarget), this, this, USE_TOGGLE, 0);
m_iTriggerCondition = AITRIGGER_NONE;
return true;
}
return false;
}
//=========================================================
// CanPlaySequence - determines whether or not the monster
// can play the scripted sequence or AI sequence that is
// trying to possess it. If DisregardState is set, the monster
// will be sucked into the script no matter what state it is
// in. ONLY Scripted AI ents should allow this.
//=========================================================
bool CBaseMonster::CanPlaySequence(bool fDisregardMonsterState, int interruptLevel)
{
if (m_pCine || !IsAlive() || m_MonsterState == MONSTERSTATE_PRONE)
{
// monster is already running a scripted sequence or dead!
return false;
}
if (fDisregardMonsterState)
{
// ok to go, no matter what the monster state. (scripted AI)
return true;
}
if (m_MonsterState == MONSTERSTATE_NONE || m_MonsterState == MONSTERSTATE_IDLE || m_IdealMonsterState == MONSTERSTATE_IDLE)
{
// ok to go, but only in these states
return true;
}
if (m_MonsterState == MONSTERSTATE_ALERT && interruptLevel >= SS_INTERRUPT_BY_NAME)
return true;
// unknown situation
return false;
}
//=========================================================
// FindLateralCover - attempts to locate a spot in the world
// directly to the left or right of the caller that will
// conceal them from view of pSightEnt
//=========================================================
#define COVER_CHECKS 5 // how many checks are made
#define COVER_DELTA 48 // distance between checks
bool CBaseMonster::FindLateralCover(const Vector& vecThreat, const Vector& vecViewOffset)
{
TraceResult tr;
Vector vecBestOnLeft;
Vector vecBestOnRight;
Vector vecLeftTest;
Vector vecRightTest;
Vector vecStepRight;
int i;
UTIL_MakeVectors(pev->angles);
vecStepRight = gpGlobals->v_right * COVER_DELTA;
vecStepRight.z = 0;
vecLeftTest = vecRightTest = pev->origin;
for (i = 0; i < COVER_CHECKS; i++)
{
vecLeftTest = vecLeftTest - vecStepRight;
vecRightTest = vecRightTest + vecStepRight;
// it's faster to check the SightEnt's visibility to the potential spot than to check the local move, so we do that first.
UTIL_TraceLine(vecThreat + vecViewOffset, vecLeftTest + pev->view_ofs, ignore_monsters, ignore_glass, ENT(pev) /*pentIgnore*/, &tr);
if (tr.flFraction != 1.0)
{
if (FValidateCover(vecLeftTest) && CheckLocalMove(pev->origin, vecLeftTest, NULL, NULL) == LOCALMOVE_VALID)
{
if (MoveToLocation(ACT_RUN, 0, vecLeftTest))
{
return true;
}
}
}
// it's faster to check the SightEnt's visibility to the potential spot than to check the local move, so we do that first.
UTIL_TraceLine(vecThreat + vecViewOffset, vecRightTest + pev->view_ofs, ignore_monsters, ignore_glass, ENT(pev) /*pentIgnore*/, &tr);
if (tr.flFraction != 1.0)
{
if (FValidateCover(vecRightTest) && CheckLocalMove(pev->origin, vecRightTest, NULL, NULL) == LOCALMOVE_VALID)
{
if (MoveToLocation(ACT_RUN, 0, vecRightTest))
{
return true;
}
}
}
}
return false;
}
Vector CBaseMonster::ShootAtEnemy(const Vector& shootOrigin)
{
CBaseEntity* pEnemy = m_hEnemy;
if (pEnemy)
{
return ((pEnemy->BodyTarget(shootOrigin) - pEnemy->pev->origin) + m_vecEnemyLKP - shootOrigin).Normalize();
}
else
return gpGlobals->v_forward;
}
//=========================================================
// FacingIdeal - tells us if a monster is facing its ideal
// yaw. Created this function because many spots in the
// code were checking the yawdiff against this magic
// number. Nicer to have it in one place if we're gonna
// be stuck with it.
//=========================================================
bool CBaseMonster::FacingIdeal()
{
if (fabs(FlYawDiff()) <= 0.006) //!!!BUGBUG - no magic numbers!!!
{
return true;
}
return false;
}
//=========================================================
// FCanActiveIdle
//=========================================================
bool CBaseMonster::FCanActiveIdle()
{
/*
if ( m_MonsterState == MONSTERSTATE_IDLE && m_IdealMonsterState == MONSTERSTATE_IDLE && !IsMoving() )
{
return true;
}
*/
return false;
}
void CBaseMonster::CorpseFallThink()
{
if ((pev->flags & FL_ONGROUND) != 0)
{
SetThink(NULL);
SetSequenceBox();
UTIL_SetOrigin(pev, pev->origin); // link into world.
}
else
pev->nextthink = gpGlobals->time + 0.1;
}
// Call after animation/pose is set up
void CBaseMonster::MonsterInitDead()
{
InitBoneControllers();
pev->solid = SOLID_BBOX;
pev->movetype = MOVETYPE_TOSS; // so he'll fall to ground
pev->frame = 0;
ResetSequenceInfo();
pev->framerate = 0;
// Copy health
pev->max_health = pev->health;
pev->deadflag = DEAD_DEAD;
UTIL_SetSize(pev, g_vecZero, g_vecZero);
UTIL_SetOrigin(pev, pev->origin);
// Setup health counters, etc.
BecomeDead();
SetThink(&CBaseMonster::CorpseFallThink);
pev->nextthink = gpGlobals->time + 0.5;
}
//=========================================================
// BBoxIsFlat - check to see if the monster's bounding box
// is lying flat on a surface (traces from all four corners
// are same length.)
//=========================================================
bool CBaseMonster::BBoxFlat()
{
TraceResult tr;
Vector vecPoint;
float flXSize, flYSize;
float flLength;
float flLength2;
flXSize = pev->size.x / 2;
flYSize = pev->size.y / 2;
vecPoint.x = pev->origin.x + flXSize;
vecPoint.y = pev->origin.y + flYSize;
vecPoint.z = pev->origin.z;
UTIL_TraceLine(vecPoint, vecPoint - Vector(0, 0, 100), ignore_monsters, ENT(pev), &tr);
flLength = (vecPoint - tr.vecEndPos).Length();
vecPoint.x = pev->origin.x - flXSize;
vecPoint.y = pev->origin.y - flYSize;
UTIL_TraceLine(vecPoint, vecPoint - Vector(0, 0, 100), ignore_monsters, ENT(pev), &tr);
flLength2 = (vecPoint - tr.vecEndPos).Length();
if (flLength2 > flLength)
{
return false;
}
flLength = flLength2;
vecPoint.x = pev->origin.x - flXSize;
vecPoint.y = pev->origin.y + flYSize;
UTIL_TraceLine(vecPoint, vecPoint - Vector(0, 0, 100), ignore_monsters, ENT(pev), &tr);
flLength2 = (vecPoint - tr.vecEndPos).Length();
if (flLength2 > flLength)
{
return false;
}
flLength = flLength2;
vecPoint.x = pev->origin.x + flXSize;
vecPoint.y = pev->origin.y - flYSize;
UTIL_TraceLine(vecPoint, vecPoint - Vector(0, 0, 100), ignore_monsters, ENT(pev), &tr);
flLength2 = (vecPoint - tr.vecEndPos).Length();
if (flLength2 > flLength)
{
return false;
}
flLength = flLength2;
return true;
}
//=========================================================
// Get Enemy - tries to find the best suitable enemy for the monster.
//=========================================================
bool CBaseMonster::GetEnemy()
{
CBaseEntity* pNewEnemy;
if (HasConditions(bits_COND_SEE_HATE | bits_COND_SEE_DISLIKE | bits_COND_SEE_NEMESIS))
{
pNewEnemy = BestVisibleEnemy();
if (pNewEnemy != m_hEnemy && pNewEnemy != NULL)
{
// DO NOT mess with the monster's m_hEnemy pointer unless the schedule the monster is currently running will be interrupted
// by COND_NEW_ENEMY. This will eliminate the problem of monsters getting a new enemy while they are in a schedule that doesn't care,
// and then not realizing it by the time they get to a schedule that does. I don't feel this is a good permanent fix.
if (m_pSchedule)
{
if ((m_pSchedule->iInterruptMask & bits_COND_NEW_ENEMY) != 0)
{
PushEnemy(m_hEnemy, m_vecEnemyLKP);
SetConditions(bits_COND_NEW_ENEMY);
m_hEnemy = pNewEnemy;
m_vecEnemyLKP = m_hEnemy->pev->origin;
}
// if the new enemy has an owner, take that one as well
if (pNewEnemy->pev->owner != NULL)
{
CBaseEntity* pOwner = GetMonsterPointer(pNewEnemy->pev->owner);
if (pOwner && (pOwner->pev->flags & FL_MONSTER) != 0 && IRelationship(pOwner) != R_NO)
PushEnemy(pOwner, m_vecEnemyLKP);
}
}
}
}
// remember old enemies
if (m_hEnemy == NULL && PopEnemy())
{
if (m_pSchedule)
{
if ((m_pSchedule->iInterruptMask & bits_COND_NEW_ENEMY) != 0)
{
SetConditions(bits_COND_NEW_ENEMY);
}
}
}
if (m_hEnemy != NULL)
{
// monster has an enemy.
return true;
}
return false; // monster has no enemy
}
//=========================================================
// DropItem - dead monster drops named item
//=========================================================
CBaseEntity* CBaseMonster::DropItem(const char* pszItemName, const Vector& vecPos, const Vector& vecAng)
{
if (!pszItemName)
{
ALERT(at_console, "DropItem() - No item name!\n");
return nullptr;
}
if (!m_AllowItemDropping)
{
return nullptr;
}
CBaseEntity* pItem = CBaseEntity::Create(pszItemName, vecPos, vecAng, edict());
if (pItem)
{
// do we want this behavior to be default?! (sjb)
pItem->pev->velocity = pev->velocity;
pItem->pev->avelocity = Vector(0, RANDOM_FLOAT(0, 100), 0);
return pItem;
}
else
{
ALERT(at_console, "DropItem() - Didn't create!\n");
return nullptr;
}
}
bool CBaseMonster::ShouldFadeOnDeath()
{
// if flagged to fade out or I have an owner (I came from a monster spawner)
if ((pev->spawnflags & SF_MONSTER_FADECORPSE) != 0 || !FNullEnt(pev->owner))
return true;
return false;
}