You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3379 lines
97 KiB
3379 lines
97 KiB
/*** |
|
* |
|
* Copyright (c) 1996-2002, 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 ); |
|
|
|
extern DLL_GLOBAL BOOL g_fDrawLines; |
|
extern DLL_GLOBAL short g_sModelIndexLaser;// holds the index for the laser beam |
|
extern DLL_GLOBAL short g_sModelIndexLaserDot;// holds the index for the laser beam dot |
|
|
|
extern CGraph WorldGraph;// the world node graph |
|
|
|
// 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 ), |
|
}; |
|
|
|
//IMPLEMENT_SAVERESTORE( CBaseMonster, CBaseToggle ) |
|
|
|
int CBaseMonster::Save( CSave &save ) |
|
{ |
|
if( !CBaseToggle::Save( save ) ) |
|
return 0; |
|
return save.WriteFields( "CBaseMonster", this, m_SaveData, ARRAYSIZE( m_SaveData ) ); |
|
} |
|
|
|
int CBaseMonster::Restore( CRestore &restore ) |
|
{ |
|
if( !CBaseToggle::Restore( restore ) ) |
|
return 0; |
|
int 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 == 0 ) |
|
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( void ) |
|
{ |
|
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( void ) |
|
{ |
|
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( void ) |
|
{ |
|
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( pCurrentSound && |
|
( pCurrentSound->m_iType & iMySounds ) && |
|
( 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 ) ) |
|
{ |
|
// 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 ) |
|
{ |
|
CBaseMonster *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( pClient && !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( void ) |
|
{ |
|
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( void ) |
|
{ |
|
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( void ) |
|
{ |
|
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( void ) |
|
{ |
|
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 ) |
|
{ |
|
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( void ) |
|
{ |
|
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( void ) |
|
{ |
|
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( void ) |
|
{ |
|
m_Route[0].iType = 0; |
|
m_iRouteIndex = 0; |
|
} |
|
|
|
//========================================================= |
|
// FRouteClear - returns TRUE is the Route is cleared out |
|
// ( invalid ) |
|
//========================================================= |
|
BOOL CBaseMonster::FRouteClear( void ) |
|
{ |
|
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( void ) |
|
{ |
|
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 != 0 ) |
|
{ |
|
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 ) || ( 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 |
|
|
|
int ShouldSimplify( int routeType ) |
|
{ |
|
routeType &= ~bits_MF_IS_GOAL; |
|
|
|
if( ( routeType == bits_MF_TO_PATHCORNER ) || ( routeType & bits_MF_DONT_SIMPLIFY ) ) |
|
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( !m_Route[i].iType ) |
|
break; |
|
else |
|
count++; |
|
if( m_Route[i].iType & bits_MF_IS_GOAL ) |
|
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( void ) |
|
{ |
|
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 != 0 && 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 ) |
|
{ |
|
if( CheckRangeAttack1( flDot, flDist ) ) |
|
SetConditions( bits_COND_CAN_RANGE_ATTACK1 ); |
|
} |
|
if( m_afCapability & bits_CAP_RANGE_ATTACK2 ) |
|
{ |
|
if( CheckRangeAttack2( flDot, flDist ) ) |
|
SetConditions( bits_COND_CAN_RANGE_ATTACK2 ); |
|
} |
|
if( m_afCapability & bits_CAP_MELEE_ATTACK1 ) |
|
{ |
|
if( CheckMeleeAttack1( flDot, flDist ) ) |
|
SetConditions( bits_COND_CAN_MELEE_ATTACK1 ); |
|
} |
|
if( m_afCapability & bits_CAP_MELEE_ATTACK2 ) |
|
{ |
|
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( void ) |
|
{ |
|
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. |
|
//========================================================= |
|
int CBaseMonster::CheckEnemy( CBaseEntity *pEnemy ) |
|
{ |
|
float flDistToEnemy; |
|
int 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; |
|
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] == 0 ) // 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] != 0 ) |
|
{ |
|
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 ) |
|
{ |
|
int iSequence; |
|
|
|
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( !( m_Activity == ACT_WALK || m_Activity == 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) |
|
} |
|
|
|
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; |
|
} |
|
|
|
//========================================================= |
|
// 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 ) ) ) |
|
{ |
|
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 ) ) && ( !pTarget || ( pTarget->pev->flags & FL_ONGROUND ) ) ) |
|
{ |
|
// 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( 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 ) ) |
|
{ |
|
// 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 ) |
|
movementGoal = MOVEGOAL_TARGETENT; |
|
else if( iMoveFlag & bits_MF_TO_ENEMY ) |
|
movementGoal = MOVEGOAL_ENEMY; |
|
else if( iMoveFlag & bits_MF_TO_PATHCORNER ) |
|
movementGoal = MOVEGOAL_PATHCORNER; |
|
else if( iMoveFlag & bits_MF_TO_NODE ) |
|
movementGoal = MOVEGOAL_NODE; |
|
else if( iMoveFlag & bits_MF_TO_LOCATION ) |
|
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 ) ) |
|
{ |
|
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 ), 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 = Q_min( 16.0, 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( void ) |
|
{ |
|
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 ) |
|
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( void ) |
|
{ |
|
StartMonster(); |
|
} |
|
|
|
//========================================================= |
|
// StartMonster - final bit of initization before a monster |
|
// is turned over to the AI. |
|
//========================================================= |
|
void CBaseMonster::StartMonster( void ) |
|
{ |
|
// 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\n", 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( void ) |
|
{ |
|
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; |
|
} |
|
|
|
int CBaseMonster::TaskIsRunning( void ) |
|
{ |
|
if( m_iTaskStatus != TASKSTATUS_COMPLETE && |
|
m_iTaskStatus != TASKSTATUS_RUNNING_MOVEMENT ) |
|
return 1; |
|
|
|
return 0; |
|
} |
|
|
|
//========================================================= |
|
// 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_FR ,R_NO ,R_HT ,R_DL ,R_FR ,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( !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( !WorldGraph.m_fGraphPresent || !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( !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( !WorldGraph.m_fGraphPresent || !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( void ) |
|
{ |
|
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( void ) |
|
{ |
|
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 ) |
|
{ |
|
speed = (float)yawSpeed * gpGlobals->frametime * 10; |
|
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 ) |
|
{ |
|
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( void ) |
|
{ |
|
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; |
|
// fall through... |
|
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 ) |
|
{ |
|
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 ) |
|
{ |
|
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( !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( !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( void ) |
|
{ |
|
int i; |
|
TraceResult tr; |
|
|
|
if( !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( 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( !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( void ) |
|
{ |
|
ALERT_TYPE level = at_console; |
|
|
|
static const char *pStateNames[] = { "None", "Idle", "Combat", "Alert", "Hunt", "Prone", "Scripted", "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 != 0 ) |
|
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 ) |
|
ALERT( level, " PRISONER! " ); |
|
if( pev->spawnflags & SF_MONSTER_PREDISASTER ) |
|
ALERT( level, " Pre-Disaster! " ); |
|
ALERT( level, "\n" ); |
|
} |
|
|
|
//========================================================= |
|
// KeyValue |
|
// |
|
// !!! netname entvar field is used in squadmonster for groupname!!! |
|
//========================================================= |
|
void CBaseMonster::KeyValue( KeyValueData *pkvd ) |
|
{ |
|
if( FStrEq( pkvd->szKeyName, "TriggerTarget" ) ) |
|
{ |
|
m_iszTriggerTarget = ALLOC_STRING( pkvd->szValue ); |
|
pkvd->fHandled = TRUE; |
|
} |
|
else if( FStrEq( pkvd->szKeyName, "TriggerCondition" ) ) |
|
{ |
|
m_iTriggerCondition = atoi( pkvd->szValue ); |
|
pkvd->fHandled = TRUE; |
|
} |
|
else |
|
{ |
|
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( void ) |
|
{ |
|
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 != 0 && 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 ) ) |
|
{ |
|
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 && m_afSoundTypes & bits_SOUND_WORLD ) |
|
{ |
|
fFireTarget = TRUE; |
|
} |
|
break; |
|
case AITRIGGER_HEARPLAYER: |
|
if( m_afConditions & bits_COND_HEAR_SOUND && m_afSoundTypes & bits_SOUND_PLAYER ) |
|
{ |
|
fFireTarget = TRUE; |
|
} |
|
break; |
|
case AITRIGGER_HEARCOMBAT: |
|
if( m_afConditions & bits_COND_HEAR_SOUND && m_afSoundTypes & bits_SOUND_COMBAT ) |
|
{ |
|
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. |
|
//========================================================= |
|
int 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( void ) |
|
{ |
|
if( fabs( FlYawDiff() ) <= 0.006 )//!!!BUGBUG - no magic numbers!!! |
|
{ |
|
return TRUE; |
|
} |
|
|
|
return FALSE; |
|
} |
|
|
|
//========================================================= |
|
// FCanActiveIdle |
|
//========================================================= |
|
BOOL CBaseMonster::FCanActiveIdle( void ) |
|
{ |
|
/* |
|
if( m_MonsterState == MONSTERSTATE_IDLE && m_IdealMonsterState == MONSTERSTATE_IDLE && !IsMoving() ) |
|
{ |
|
return TRUE; |
|
} |
|
*/ |
|
return FALSE; |
|
} |
|
|
|
void CBaseMonster::PlaySentence( const char *pszSentence, float duration, float volume, float attenuation ) |
|
{ |
|
if( pszSentence && IsAlive() ) |
|
{ |
|
if( pszSentence[0] == '!' ) |
|
EMIT_SOUND_DYN( edict(), CHAN_VOICE, pszSentence, volume, attenuation, 0, PITCH_NORM ); |
|
else |
|
SENTENCEG_PlayRndSz( edict(), pszSentence, volume, attenuation, 0, PITCH_NORM ); |
|
} |
|
} |
|
|
|
void CBaseMonster::PlayScriptedSentence( const char *pszSentence, float duration, float volume, float attenuation, BOOL bConcurrent, CBaseEntity *pListener ) |
|
{ |
|
PlaySentence( pszSentence, duration, volume, attenuation ); |
|
} |
|
|
|
void CBaseMonster::SentenceStop( void ) |
|
{ |
|
EMIT_SOUND( edict(), CHAN_VOICE, "common/null.wav", 1.0, ATTN_IDLE ); |
|
} |
|
|
|
void CBaseMonster::CorpseFallThink( void ) |
|
{ |
|
if( pev->flags & FL_ONGROUND ) |
|
{ |
|
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( void ) |
|
{ |
|
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( void ) |
|
{ |
|
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( void ) |
|
{ |
|
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 ) |
|
{ |
|
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 ) && IRelationship( pOwner ) != R_NO ) |
|
PushEnemy( pOwner, m_vecEnemyLKP ); |
|
} |
|
} |
|
} |
|
} |
|
|
|
// remember old enemies |
|
if( m_hEnemy == 0 && PopEnemy() ) |
|
{ |
|
if( m_pSchedule ) |
|
{ |
|
if( m_pSchedule->iInterruptMask & bits_COND_NEW_ENEMY ) |
|
{ |
|
SetConditions( bits_COND_NEW_ENEMY ); |
|
} |
|
} |
|
} |
|
|
|
if( m_hEnemy != 0 ) |
|
{ |
|
// 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 NULL; |
|
} |
|
|
|
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 ); |
|
|
|
// Dropped items should never respawn (unless this rule changes in the future). - Solokiller |
|
pItem->pev->spawnflags |= SF_NORESPAWN; |
|
return pItem; |
|
} |
|
else |
|
{ |
|
ALERT( at_console, "DropItem() - Didn't create!\n" ); |
|
return FALSE; |
|
} |
|
} |
|
|
|
BOOL CBaseMonster::ShouldFadeOnDeath( void ) |
|
{ |
|
// if flagged to fade out or I have an owner (I came from a monster spawner) |
|
if( ( pev->spawnflags & SF_MONSTER_FADECORPSE ) || !FNullEnt( pev->owner ) ) |
|
return TRUE; |
|
|
|
return FALSE; |
|
}
|
|
|