diff options
Diffstat (limited to 'game/server/hl1/hl1_npc_hgrunt.cpp')
| -rw-r--r-- | game/server/hl1/hl1_npc_hgrunt.cpp | 2645 |
1 files changed, 2645 insertions, 0 deletions
diff --git a/game/server/hl1/hl1_npc_hgrunt.cpp b/game/server/hl1/hl1_npc_hgrunt.cpp new file mode 100644 index 0000000..375ddf7 --- /dev/null +++ b/game/server/hl1/hl1_npc_hgrunt.cpp @@ -0,0 +1,2645 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Bullseyes act as targets for other NPC's to attack and to trigger +// events +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "beam_shared.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "hl1_npc_hgrunt.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "basecombatweapon.h" +#include "hl1_basegrenade.h" +#include "ai_interactions.h" +#include "scripted.h" +#include "hl1_basegrenade.h" +#include "hl1_grenade_mp5.h" + +ConVar sk_hgrunt_health( "sk_hgrunt_health","0"); +ConVar sk_hgrunt_kick ( "sk_hgrunt_kick", "0" ); +ConVar sk_hgrunt_pellets ( "sk_hgrunt_pellets", "0" ); +ConVar sk_hgrunt_gspeed ( "sk_hgrunt_gspeed", "0" ); + +extern ConVar sk_plr_dmg_grenade; +extern ConVar sk_plr_dmg_mp5_grenade; + +#define SF_GRUNT_LEADER ( 1 << 5 ) + +int g_fGruntQuestion; // true if an idle grunt asked a question. Cleared when someone answers. +int g_iSquadIndex = 0; + +#define HGRUNT_GUN_SPREAD 0.08716f + +//========================================================= +// monster-specific DEFINE's +//========================================================= +#define GRUNT_CLIP_SIZE 36 // how many bullets in a clip? - NOTE: 3 round burst sound, so keep as 3 * x! +#define GRUNT_VOL 0.35 // volume of grunt sounds +#define GRUNT_SNDLVL SNDLVL_NORM // soundlevel of grunt sentences +#define HGRUNT_LIMP_HEALTH 20 +#define HGRUNT_DMG_HEADSHOT ( DMG_BULLET | DMG_CLUB ) // damage types that can kill a grunt with a single headshot. +#define HGRUNT_NUM_HEADS 2 // how many grunt heads are there? +#define HGRUNT_MINIMUM_HEADSHOT_DAMAGE 15 // must do at least this much damage in one shot to head to score a headshot kill +#define HGRUNT_SENTENCE_VOLUME (float)0.35 // volume of grunt sentences + +#define HGRUNT_9MMAR ( 1 << 0) +#define HGRUNT_HANDGRENADE ( 1 << 1) +#define HGRUNT_GRENADELAUNCHER ( 1 << 2) +#define HGRUNT_SHOTGUN ( 1 << 3) + +#define HEAD_GROUP 1 +#define HEAD_GRUNT 0 +#define HEAD_COMMANDER 1 +#define HEAD_SHOTGUN 2 +#define HEAD_M203 3 +#define GUN_GROUP 2 +#define GUN_MP5 0 +#define GUN_SHOTGUN 1 +#define GUN_NONE 2 + +//========================================================= +// Monster's Anim Events Go Here +//========================================================= +#define HGRUNT_AE_RELOAD ( 2 ) +#define HGRUNT_AE_KICK ( 3 ) +#define HGRUNT_AE_BURST1 ( 4 ) +#define HGRUNT_AE_BURST2 ( 5 ) +#define HGRUNT_AE_BURST3 ( 6 ) +#define HGRUNT_AE_GREN_TOSS ( 7 ) +#define HGRUNT_AE_GREN_LAUNCH ( 8 ) +#define HGRUNT_AE_GREN_DROP ( 9 ) +#define HGRUNT_AE_CAUGHT_ENEMY ( 10) // grunt established sight with an enemy (player only) that had previously eluded the squad. +#define HGRUNT_AE_DROP_GUN ( 11) // grunt (probably dead) is dropping his mp5. + + +const char *CNPC_HGrunt::pGruntSentences[] = +{ + "HG_GREN", // grenade scared grunt + "HG_ALERT", // sees player + "HG_MONSTER", // sees monster + "HG_COVER", // running to cover + "HG_THROW", // about to throw grenade + "HG_CHARGE", // running out to get the enemy + "HG_TAUNT", // say rude things +}; + +enum HGRUNT_SENTENCE_TYPES +{ + HGRUNT_SENT_NONE = -1, + HGRUNT_SENT_GREN = 0, + HGRUNT_SENT_ALERT, + HGRUNT_SENT_MONSTER, + HGRUNT_SENT_COVER, + HGRUNT_SENT_THROW, + HGRUNT_SENT_CHARGE, + HGRUNT_SENT_TAUNT, +} ; + +LINK_ENTITY_TO_CLASS( monster_human_grunt, CNPC_HGrunt ); + +//========================================================= +// monster-specific schedule types +//========================================================= +enum +{ + SCHED_GRUNT_FAIL = LAST_SHARED_SCHEDULE, + SCHED_GRUNT_COMBAT_FAIL, + SCHED_GRUNT_VICTORY_DANCE, + SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE, + SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE_RETRY, + SCHED_GRUNT_FOUND_ENEMY, + SCHED_GRUNT_COMBAT_FACE, + SCHED_GRUNT_SIGNAL_SUPPRESS, + SCHED_GRUNT_SUPPRESS, + SCHED_GRUNT_WAIT_IN_COVER, + SCHED_GRUNT_TAKE_COVER, + SCHED_GRUNT_GRENADE_COVER, + SCHED_GRUNT_TOSS_GRENADE_COVER, + SCHED_GRUNT_HIDE_RELOAD, + SCHED_GRUNT_SWEEP, + SCHED_GRUNT_RANGE_ATTACK1A, + SCHED_GRUNT_RANGE_ATTACK1B, + SCHED_GRUNT_RANGE_ATTACK2, + SCHED_GRUNT_REPEL, + SCHED_GRUNT_REPEL_ATTACK, + SCHED_GRUNT_REPEL_LAND, + SCHED_GRUNT_TAKE_COVER_FAILED, + SCHED_GRUNT_RELOAD, + SCHED_GRUNT_TAKE_COVER_FROM_ENEMY, + SCHED_GRUNT_BARNACLE_HIT, + SCHED_GRUNT_BARNACLE_PULL, + SCHED_GRUNT_BARNACLE_CHOMP, + SCHED_GRUNT_BARNACLE_CHEW, +}; + +//========================================================= +// monster-specific tasks +//========================================================= +enum +{ + TASK_GRUNT_FACE_TOSS_DIR = LAST_SHARED_TASK + 1, + TASK_GRUNT_SPEAK_SENTENCE, + TASK_GRUNT_CHECK_FIRE, +}; + + +//========================================================= +// monster-specific conditions +//========================================================= +enum +{ + COND_GRUNT_NOFIRE = LAST_SHARED_CONDITION + 1, +}; + +// ----------------------------------------------- +// > Squad slots +// ----------------------------------------------- +enum SquadSlot_T +{ + SQUAD_SLOT_GRENADE1 = LAST_SHARED_SQUADSLOT, + SQUAD_SLOT_GRENADE2, + SQUAD_SLOT_ENGAGE1, + SQUAD_SLOT_ENGAGE2, +}; + + +int ACT_GRUNT_LAUNCH_GRENADE; +int ACT_GRUNT_TOSS_GRENADE; +int ACT_GRUNT_MP5_STANDING; +int ACT_GRUNT_MP5_CROUCHING; +int ACT_GRUNT_SHOTGUN_STANDING; +int ACT_GRUNT_SHOTGUN_CROUCHING; + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CNPC_HGrunt ) + DEFINE_FIELD( m_flNextGrenadeCheck, FIELD_TIME ), + DEFINE_FIELD( m_flNextPainTime, FIELD_TIME ), + DEFINE_FIELD( m_flCheckAttackTime, FIELD_FLOAT ), + DEFINE_FIELD( m_vecTossVelocity, FIELD_VECTOR ), + DEFINE_FIELD( m_iLastGrenadeCondition, FIELD_INTEGER ), + DEFINE_FIELD( m_fStanding, FIELD_BOOLEAN ), + DEFINE_FIELD( m_fFirstEncounter, FIELD_BOOLEAN ), + DEFINE_FIELD( m_iClipSize, FIELD_INTEGER ), + DEFINE_FIELD( m_voicePitch, FIELD_INTEGER ), + DEFINE_FIELD( m_iSentence, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_iWeapons, FIELD_INTEGER, "weapons" ), + DEFINE_KEYFIELD( m_SquadName, FIELD_STRING, "netname" ), + + DEFINE_FIELD( m_bInBarnacleMouth, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flLastEnemySightTime, FIELD_TIME ), + DEFINE_FIELD( m_flTalkWaitTime, FIELD_TIME ), + //DEFINE_FIELD( m_iAmmoType, FIELD_INTEGER ), + +END_DATADESC() + + +//========================================================= +// Spawn +//========================================================= +void CNPC_HGrunt::Spawn() +{ + Precache( ); + + SetModel( "models/hgrunt.mdl" ); + + SetHullType(HULL_HUMAN); + SetHullSizeNormal(); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + m_bloodColor = BLOOD_COLOR_RED; + ClearEffects(); + m_iHealth = sk_hgrunt_health.GetFloat(); + m_flFieldOfView = 0.2;// indicates the width of this monster's forward view cone ( as a dotproduct result ) + m_NPCState = NPC_STATE_NONE; + m_flNextGrenadeCheck = gpGlobals->curtime + 1; + m_flNextPainTime = gpGlobals->curtime; + m_iSentence = HGRUNT_SENT_NONE; + + CapabilitiesClear(); + CapabilitiesAdd ( bits_CAP_SQUAD | bits_CAP_TURN_HEAD | bits_CAP_DOORS_GROUP | bits_CAP_MOVE_GROUND ); + + CapabilitiesAdd(bits_CAP_INNATE_RANGE_ATTACK1 ); + + // Innate range attack for grenade + CapabilitiesAdd(bits_CAP_INNATE_RANGE_ATTACK2 ); + // Innate range attack for kicking + CapabilitiesAdd(bits_CAP_INNATE_MELEE_ATTACK1 ); + + m_fFirstEncounter = true;// this is true when the grunt spawns, because he hasn't encountered an enemy yet. + + m_HackedGunPos = Vector ( 0, 0, 55 ); + + if ( m_iWeapons == 0) + { + // initialize to original values + m_iWeapons = HGRUNT_9MMAR | HGRUNT_HANDGRENADE; + // pev->weapons = HGRUNT_SHOTGUN; + // pev->weapons = HGRUNT_9MMAR | HGRUNT_GRENADELAUNCHER; + } + + if (FBitSet( m_iWeapons, HGRUNT_SHOTGUN )) + { + SetBodygroup( GUN_GROUP, GUN_SHOTGUN ); + m_iClipSize = 8; + } + else + { + m_iClipSize = GRUNT_CLIP_SIZE; + } + m_cAmmoLoaded = m_iClipSize; + + if ( random->RandomInt( 0, 99 ) < 80) + m_nSkin = 0; // light skin + else + m_nSkin = 1; // dark skin + + if (FBitSet( m_iWeapons, HGRUNT_SHOTGUN )) + { + SetBodygroup( HEAD_GROUP, HEAD_SHOTGUN); + } + else if (FBitSet( m_iWeapons, HGRUNT_GRENADELAUNCHER )) + { + SetBodygroup( HEAD_GROUP, HEAD_M203 ); + m_nSkin = 1; // alway dark skin + } + + m_flTalkWaitTime = 0; + + + //HACK + g_iSquadIndex = 0; + + BaseClass::Spawn(); + + NPCInit(); +} + +int CNPC_HGrunt::IRelationPriority( CBaseEntity *pTarget ) +{ + //I hate alien grunts more than anything. + if ( pTarget->Classify() == CLASS_ALIEN_MILITARY ) + { + if ( FClassnameIs( pTarget, "monster_alien_grunt" ) ) + { + return ( BaseClass::IRelationPriority ( pTarget ) + 1 ); + } + } + + return BaseClass::IRelationPriority( pTarget ); +} + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CNPC_HGrunt::Precache() +{ + m_iAmmoType = GetAmmoDef()->Index("9mmRound"); + + PrecacheModel("models/hgrunt.mdl"); + + // get voice pitch + if ( random->RandomInt(0,1)) + m_voicePitch = 109 + random->RandomInt(0,7); + else + m_voicePitch = 100; + + PrecacheScriptSound( "HGrunt.Reload" ); + PrecacheScriptSound( "HGrunt.GrenadeLaunch" ); + PrecacheScriptSound( "HGrunt.9MM" ); + PrecacheScriptSound( "HGrunt.Shotgun" ); + PrecacheScriptSound( "HGrunt.Pain" ); + PrecacheScriptSound( "HGrunt.Die" ); + + BaseClass::Precache(); + + UTIL_PrecacheOther( "grenade_hand" ); + UTIL_PrecacheOther( "grenade_mp5" ); +} + +//========================================================= +// someone else is talking - don't speak +//========================================================= +bool CNPC_HGrunt::FOkToSpeak( void ) +{ +// if someone else is talking, don't speak + if ( gpGlobals->curtime <= m_flTalkWaitTime ) + return FALSE; + + if ( m_spawnflags & SF_NPC_GAG ) + { + if ( m_NPCState != NPC_STATE_COMBAT ) + { + // no talking outside of combat if gagged. + return FALSE; + } + } + + return TRUE; +} + + +//========================================================= +// Speak Sentence - say your cued up sentence. +// +// Some grunt sentences (take cover and charge) rely on actually +// being able to execute the intended action. It's really lame +// when a grunt says 'COVER ME' and then doesn't move. The problem +// is that the sentences were played when the decision to TRY +// to move to cover was made. Now the sentence is played after +// we know for sure that there is a valid path. The schedule +// may still fail but in most cases, well after the grunt has +// started moving. +//========================================================= +void CNPC_HGrunt::SpeakSentence( void ) +{ + if ( m_iSentence == HGRUNT_SENT_NONE ) + { + // no sentence cued up. + return; + } + + if ( FOkToSpeak() ) + { + SENTENCEG_PlayRndSz( edict(), pGruntSentences[ m_iSentence ], HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + JustSpoke(); + } +} + +//========================================================= +//========================================================= +void CNPC_HGrunt::JustSpoke( void ) +{ + m_flTalkWaitTime = gpGlobals->curtime + random->RandomFloat( 1.5f, 2.0f ); + m_iSentence = HGRUNT_SENT_NONE; +} + +//========================================================= +// PrescheduleThink - this function runs after conditions +// are collected and before scheduling code is run. +//========================================================= +void CNPC_HGrunt::PrescheduleThink ( void ) +{ + BaseClass::PrescheduleThink(); + + if ( m_pSquad && GetEnemy() != NULL ) + { + if ( m_pSquad->GetLeader() == NULL ) + return; + + CNPC_HGrunt *pSquadLeader = (CNPC_HGrunt*)m_pSquad->GetLeader()->MyNPCPointer(); + + if ( pSquadLeader == NULL ) + return; //Paranoid, so making sure it's ok. + + if ( HasCondition ( COND_SEE_ENEMY ) ) + { + // update the squad's last enemy sighting time. + pSquadLeader->m_flLastEnemySightTime = gpGlobals->curtime; + } + else + { + if ( gpGlobals->curtime - pSquadLeader->m_flLastEnemySightTime > 5 ) + { + // been a while since we've seen the enemy + pSquadLeader->GetEnemies()->MarkAsEluded( GetEnemy() ); + } + } + } +} + +Class_T CNPC_HGrunt::Classify ( void ) +{ + return CLASS_HUMAN_MILITARY; +} + +//========================================================= +// +// SquadRecruit(), get some monsters of my classification and +// link them as a group. returns the group size +// +//========================================================= +int CNPC_HGrunt::SquadRecruit( int searchRadius, int maxMembers ) +{ + int squadCount; + int iMyClass = Classify();// cache this monster's class + + if ( maxMembers < 2 ) + return 0; + + // I am my own leader + squadCount = 1; + + CBaseEntity *pEntity = NULL; + + if ( m_SquadName != NULL_STRING ) + { + // I have a netname, so unconditionally recruit everyone else with that name. + pEntity = gEntList.FindEntityByClassname( pEntity, "monster_human_grunt" ); + + while ( pEntity ) + { + CNPC_HGrunt *pRecruit = (CNPC_HGrunt*)pEntity->MyNPCPointer(); + + if ( pRecruit ) + { + if ( !pRecruit->m_pSquad && pRecruit->Classify() == iMyClass && pRecruit != this ) + { + // minimum protection here against user error.in worldcraft. + if ( pRecruit->m_SquadName != NULL_STRING && FStrEq( STRING( m_SquadName ), STRING( pRecruit->m_SquadName ) ) ) + { + pRecruit->InitSquad(); + squadCount++; + } + } + } + + pEntity = gEntList.FindEntityByClassname( pEntity, "monster_human_grunt" ); + } + + return squadCount; + } + else + { + char szSquadName[64]; + Q_snprintf( szSquadName, sizeof( szSquadName ), "squad%d\n", g_iSquadIndex ); + + m_SquadName = MAKE_STRING( szSquadName ); + + while ( ( pEntity = gEntList.FindEntityInSphere( pEntity, GetAbsOrigin(), searchRadius ) ) != NULL ) + { + if ( !FClassnameIs ( pEntity, "monster_human_grunt" ) ) + continue; + + CNPC_HGrunt *pRecruit = (CNPC_HGrunt*)pEntity->MyNPCPointer(); + + if ( pRecruit && pRecruit != this && pRecruit->IsAlive() && !pRecruit->m_hCine ) + { + // Can we recruit this guy? + if ( !pRecruit->m_pSquad && pRecruit->Classify() == iMyClass && + ( (iMyClass != CLASS_ALIEN_MONSTER) || FClassnameIs( this, pRecruit->GetClassname() ) ) && + !pRecruit->m_SquadName ) + { + trace_t tr; + UTIL_TraceLine( GetAbsOrigin() + GetViewOffset(), pRecruit->GetAbsOrigin() + GetViewOffset(), MASK_NPCSOLID_BRUSHONLY, pRecruit, COLLISION_GROUP_NONE, &tr );// try to hit recruit with a traceline. + + if ( tr.fraction == 1.0 ) + { + //We're ready to recruit people, so start a squad if I don't have one. + if ( !m_pSquad ) + { + InitSquad(); + } + + pRecruit->m_SquadName = m_SquadName; + + pRecruit->CapabilitiesAdd ( bits_CAP_SQUAD ); + pRecruit->InitSquad(); + + squadCount++; + } + } + } + } + + if ( squadCount > 1 ) + { + g_iSquadIndex++; + } + } + + return squadCount; +} + +void CNPC_HGrunt::StartNPC ( void ) +{ + if ( !m_pSquad ) + { + if ( m_SquadName != NULL_STRING ) + { + // if I have a groupname, I can only recruit if I'm flagged as leader + if ( GetSpawnFlags() & SF_GRUNT_LEADER ) + { + InitSquad(); + + // try to form squads now. + int iSquadSize = SquadRecruit( 1024, 4 ); + + if ( iSquadSize ) + { + Msg ( "Squad of %d %s formed\n", iSquadSize, GetClassname() ); + } + } + else + { + + //Hacky. + //Revisit me later. + const char *pSquadName = STRING( m_SquadName ); + + m_SquadName = NULL_STRING; + + BaseClass::StartNPC(); + + m_SquadName = MAKE_STRING( pSquadName ); + + return; + } + } + else + { + int iSquadSize = SquadRecruit( 1024, 4 ); + + if ( iSquadSize ) + { + Msg ( "Squad of %d %s formed\n", iSquadSize, GetClassname() ); + } + } + } + + BaseClass::StartNPC(); + + if ( m_pSquad && m_pSquad->IsLeader( this ) ) + { + SetBodygroup( 1, 1 ); // UNDONE: truly ugly hack + m_nSkin = 0; + } +} + +//========================================================= +// CheckMeleeAttack1 +//========================================================= +int CNPC_HGrunt::MeleeAttack1Conditions ( float flDot, float flDist ) +{ + if (flDist > 64) + return COND_TOO_FAR_TO_ATTACK; + else if (flDot < 0.7) + return COND_NOT_FACING_ATTACK; + + return COND_CAN_MELEE_ATTACK1; +} + +//========================================================= +// CheckRangeAttack1 - overridden for HGrunt, cause +// FCanCheckAttacks() doesn't disqualify all attacks based +// on whether or not the enemy is occluded because unlike +// the base class, the HGrunt can attack when the enemy is +// occluded (throw grenade over wall, etc). We must +// disqualify the machine gun attack if the enemy is occluded. +//========================================================= +int CNPC_HGrunt::RangeAttack1Conditions ( float flDot, float flDist ) +{ + if ( !HasCondition( COND_ENEMY_OCCLUDED ) && flDist <= 2048 && flDot >= 0.5 && NoFriendlyFire() ) + { + trace_t tr; + + if ( !GetEnemy()->IsPlayer() && flDist <= 64 ) + { + // kick nonclients, but don't shoot at them. + return COND_NONE; + } + + Vector vecSrc; + QAngle angAngles; + + GetAttachment( "0", vecSrc, angAngles ); + + //NDebugOverlay::Line( GetAbsOrigin() + GetViewOffset(), GetEnemy()->BodyTarget(GetAbsOrigin() + GetViewOffset()), 255, 0, 0, false, 0.1 ); + // verify that a bullet fired from the gun will hit the enemy before the world. + UTIL_TraceLine( GetAbsOrigin() + GetViewOffset(), GetEnemy()->BodyTarget(GetAbsOrigin() + GetViewOffset()), MASK_SHOT, this/*pentIgnore*/, COLLISION_GROUP_NONE, &tr); + + if ( tr.fraction == 1.0 || tr.m_pEnt == GetEnemy() ) + { + //NDebugOverlay::Line( tr.startpos, tr.endpos, 0, 255, 0, false, 1.0 ); + return COND_CAN_RANGE_ATTACK1; + } + + //NDebugOverlay::Line( tr.startpos, tr.endpos, 255, 0, 0, false, 1.0 ); + } + + + if ( !NoFriendlyFire() ) + return COND_WEAPON_BLOCKED_BY_FRIEND; //err =| + + return COND_NONE; +} + +int CNPC_HGrunt::RangeAttack2Conditions( float flDot, float flDist ) +{ + m_iLastGrenadeCondition = GetGrenadeConditions( flDot, flDist ); + return m_iLastGrenadeCondition; +} + +int CNPC_HGrunt::GetGrenadeConditions( float flDot, float flDist ) +{ + if ( !FBitSet( m_iWeapons, ( HGRUNT_HANDGRENADE | HGRUNT_GRENADELAUNCHER ) ) ) + return COND_NONE; + + // assume things haven't changed too much since last time + if (gpGlobals->curtime < m_flNextGrenadeCheck ) + return m_iLastGrenadeCondition; + + if ( m_flGroundSpeed != 0 ) + return COND_NONE; + + CBaseEntity *pEnemy = GetEnemy(); + + if (!pEnemy) + return COND_NONE; + + Vector flEnemyLKP = GetEnemyLKP(); + if ( !(pEnemy->GetFlags() & FL_ONGROUND) && pEnemy->GetWaterLevel() == 0 && flEnemyLKP.z > (GetAbsOrigin().z + WorldAlignMaxs().z) ) + { + //!!!BUGBUG - we should make this check movetype and make sure it isn't FLY? Players who jump a lot are unlikely to + // be grenaded. + // don't throw grenades at anything that isn't on the ground! + return COND_NONE; + } + + Vector vecTarget; + + if (FBitSet( m_iWeapons, HGRUNT_HANDGRENADE)) + { + // find feet + if ( random->RandomInt( 0,1 ) ) + { + // magically know where they are + pEnemy->CollisionProp()->NormalizedToWorldSpace( Vector( 0.5f, 0.5f, 0.0f ), &vecTarget ); + } + else + { + // toss it to where you last saw them + vecTarget = flEnemyLKP; + } + } + else + { + // find target + // vecTarget = GetEnemy()->BodyTarget( GetAbsOrigin() ); + vecTarget = GetEnemy()->GetAbsOrigin() + (GetEnemy()->BodyTarget( GetAbsOrigin() ) - GetEnemy()->GetAbsOrigin()); + // estimate position + if ( HasCondition( COND_SEE_ENEMY)) + { + vecTarget = vecTarget + ((vecTarget - GetAbsOrigin()).Length() / sk_hgrunt_gspeed.GetFloat()) * GetEnemy()->GetAbsVelocity(); + } + } + + // are any of my squad members near the intended grenade impact area? + if ( m_pSquad ) + { + if ( m_pSquad->SquadMemberInRange( vecTarget, 256 ) ) + { + // crap, I might blow my own guy up. Don't throw a grenade and don't check again for a while. + m_flNextGrenadeCheck = gpGlobals->curtime + 1; // one full second. + return COND_NONE; + } + } + + if ( ( vecTarget - GetAbsOrigin() ).Length2D() <= 256 ) + { + // crap, I don't want to blow myself up + m_flNextGrenadeCheck = gpGlobals->curtime + 1; // one full second. + return COND_NONE; + } + + + if (FBitSet( m_iWeapons, HGRUNT_HANDGRENADE)) + { + Vector vGunPos; + QAngle angGunAngles; + GetAttachment( "0", vGunPos, angGunAngles ); + + + Vector vecToss = VecCheckToss( this, vGunPos, vecTarget, -1, 0.5, false ); + + if ( vecToss != vec3_origin ) + { + m_vecTossVelocity = vecToss; + + // don't check again for a while. + m_flNextGrenadeCheck = gpGlobals->curtime + 0.3; // 1/3 second. + + return COND_CAN_RANGE_ATTACK2; + } + else + { + // don't check again for a while. + m_flNextGrenadeCheck = gpGlobals->curtime + 1; // one full second. + + return COND_NONE; + } + } + else + { + Vector vGunPos; + QAngle angGunAngles; + GetAttachment( "0", vGunPos, angGunAngles ); + + Vector vecToss = VecCheckThrow( this, vGunPos, vecTarget, sk_hgrunt_gspeed.GetFloat(), 0.5 ); + + if ( vecToss != vec3_origin ) + { + m_vecTossVelocity = vecToss; + + // don't check again for a while. + m_flNextGrenadeCheck = gpGlobals->curtime + 0.3; // 1/3 second. + + return COND_CAN_RANGE_ATTACK2; + } + else + { + // don't check again for a while. + m_flNextGrenadeCheck = gpGlobals->curtime + 1; // one full second. + + return COND_NONE; + } + } +} + + +//========================================================= +// FCanCheckAttacks - this is overridden for human grunts +// because they can throw/shoot grenades when they can't see their +// target and the base class doesn't check attacks if the monster +// cannot see its enemy. +// +// !!!BUGBUG - this gets called before a 3-round burst is fired +// which means that a friendly can still be hit with up to 2 rounds. +// ALSO, grenades will not be tossed if there is a friendly in front, +// this is a bad bug. Friendly machine gun fire avoidance +// will unecessarily prevent the throwing of a grenade as well. +//========================================================= +bool CNPC_HGrunt::FCanCheckAttacks( void ) +{ + // This condition set when too close to a grenade to blow it up + if ( !HasCondition( COND_TOO_CLOSE_TO_ATTACK ) ) + { + return true; + } + else + { + return false; + } +} + +int CNPC_HGrunt::GetSoundInterests( void ) +{ + return SOUND_WORLD | + SOUND_COMBAT | + SOUND_PLAYER | + SOUND_BULLET_IMPACT | + SOUND_DANGER; +} + + +//========================================================= +// TraceAttack - make sure we're not taking it in the helmet +//========================================================= +void CNPC_HGrunt::TraceAttack( const CTakeDamageInfo &inputInfo, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + CTakeDamageInfo info = inputInfo; + + // check for helmet shot + if (ptr->hitgroup == 11) + { + // make sure we're wearing one + if ( GetBodygroup( 1 ) == HEAD_GRUNT && (info.GetDamageType() & (DMG_BULLET | DMG_SLASH | DMG_BLAST | DMG_CLUB))) + { + // absorb damage + info.SetDamage( info.GetDamage() - 20 ); + if ( info.GetDamage() <= 0 ) + info.SetDamage( 0.01 ); + } + // it's head shot anyways + ptr->hitgroup = HITGROUP_HEAD; + } + BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator ); +} + + +//========================================================= +// TakeDamage - overridden for the grunt because the grunt +// needs to forget that he is in cover if he's hurt. (Obviously +// not in a safe place anymore). +//========================================================= +int CNPC_HGrunt::OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ) +{ + Forget( bits_MEMORY_INCOVER ); + + return BaseClass::OnTakeDamage_Alive ( inputInfo ); +} + + +//========================================================= +// SetYawSpeed - allows each sequence to have a different +// turn rate associated with it. +//========================================================= +float CNPC_HGrunt::MaxYawSpeed( void ) +{ + float flYS; + + switch ( GetActivity() ) + { + case ACT_IDLE: + flYS = 150; + break; + case ACT_RUN: + flYS = 150; + break; + case ACT_WALK: + flYS = 180; + break; + case ACT_RANGE_ATTACK1: + flYS = 120; + break; + case ACT_RANGE_ATTACK2: + flYS = 120; + break; + case ACT_MELEE_ATTACK1: + flYS = 120; + break; + case ACT_MELEE_ATTACK2: + flYS = 120; + break; + case ACT_TURN_LEFT: + case ACT_TURN_RIGHT: + flYS = 180; + break; + case ACT_GLIDE: + case ACT_FLY: + flYS = 30; + break; + default: + flYS = 90; + break; + } + + // Yaw speed is handled differently now! + return flYS * 0.5f; +} + +void CNPC_HGrunt::IdleSound( void ) +{ + if (FOkToSpeak() && ( g_fGruntQuestion || random->RandomInt( 0,1 ) ) ) + { + if (!g_fGruntQuestion) + { + // ask question or make statement + switch ( random->RandomInt( 0,2 ) ) + { + case 0: // check in + SENTENCEG_PlayRndSz( edict(), "HG_CHECK", HGRUNT_SENTENCE_VOLUME, SNDLVL_NORM, 0, m_voicePitch); + g_fGruntQuestion = 1; + break; + case 1: // question + SENTENCEG_PlayRndSz( edict(), "HG_QUEST", HGRUNT_SENTENCE_VOLUME, SNDLVL_NORM, 0, m_voicePitch); + g_fGruntQuestion = 2; + break; + case 2: // statement + SENTENCEG_PlayRndSz( edict(), "HG_IDLE", HGRUNT_SENTENCE_VOLUME, SNDLVL_NORM, 0, m_voicePitch); + break; + } + } + else + { + switch (g_fGruntQuestion) + { + case 1: // check in + SENTENCEG_PlayRndSz( edict(), "HG_CLEAR", HGRUNT_SENTENCE_VOLUME, SNDLVL_NORM, 0, m_voicePitch); + break; + case 2: // question + SENTENCEG_PlayRndSz( edict(), "HG_ANSWER", HGRUNT_SENTENCE_VOLUME, SNDLVL_NORM, 0, m_voicePitch); + break; + } + g_fGruntQuestion = 0; + } + JustSpoke(); + } +} + +bool CNPC_HGrunt::HandleInteraction(int interactionType, void *data, CBaseCombatCharacter* sourceEnt) +{ + if (interactionType == g_interactionBarnacleVictimDangle) + { + // Force choosing of a new schedule + ClearSchedule( "Soldier being eaten by a barnacle" ); + m_bInBarnacleMouth = true; + return true; + } + else if ( interactionType == g_interactionBarnacleVictimReleased ) + { + SetState ( NPC_STATE_IDLE ); + m_bInBarnacleMouth = false; + SetAbsVelocity( vec3_origin ); + SetMoveType( MOVETYPE_STEP ); + return true; + } + else if ( interactionType == g_interactionBarnacleVictimGrab ) + { + if ( GetFlags() & FL_ONGROUND ) + { + SetGroundEntity( NULL ); + } + + //Maybe this will break something else. + if ( GetState() == NPC_STATE_SCRIPT ) + { + m_hCine->CancelScript(); + ClearSchedule( "Soldier grabbed by a barnacle" ); + } + + SetState( NPC_STATE_PRONE ); + + CTakeDamageInfo info; + PainSound( info ); + return true; + } + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Combine needs to check ammo +// Input : +// Output : +//----------------------------------------------------------------------------- +void CNPC_HGrunt::CheckAmmo ( void ) +{ + if ( m_cAmmoLoaded <= 0 ) + SetCondition( COND_NO_PRIMARY_AMMO ); + +} + +//========================================================= +//========================================================= +CBaseEntity *CNPC_HGrunt::Kick( void ) +{ + trace_t tr; + + Vector forward; + AngleVectors( GetAbsAngles(), &forward ); + Vector vecStart = GetAbsOrigin(); + vecStart.z += WorldAlignSize().z * 0.5; + Vector vecEnd = vecStart + (forward * 70); + + UTIL_TraceHull( vecStart, vecEnd, Vector(-16,-16,-18), Vector(16,16,18), MASK_SHOT_HULL, this, COLLISION_GROUP_NONE, &tr ); + + if ( tr.m_pEnt ) + { + CBaseEntity *pEntity = tr.m_pEnt; + return pEntity; + } + + return NULL; +} + +Vector CNPC_HGrunt::Weapon_ShootPosition( void ) +{ + if ( m_fStanding ) + return GetAbsOrigin() + Vector( 0, 0, 60 ); + else + return GetAbsOrigin() + Vector( 0, 0, 48 ); +} + +void CNPC_HGrunt::Event_Killed( const CTakeDamageInfo &info ) +{ + Vector vecGunPos; + QAngle vecGunAngles; + + GetAttachment( "0", vecGunPos, vecGunAngles ); + + // switch to body group with no gun. + SetBodygroup( GUN_GROUP, GUN_NONE ); + + // If the gun would drop into a wall, spawn it at our origin + if( UTIL_PointContents( vecGunPos ) & CONTENTS_SOLID ) + { + vecGunPos = GetAbsOrigin(); + } + + // now spawn a gun. + if (FBitSet( m_iWeapons, HGRUNT_SHOTGUN )) + { + DropItem( "weapon_shotgun", vecGunPos, vecGunAngles ); + } + else + { + DropItem( "weapon_mp5", vecGunPos, vecGunAngles ); + } + + if (FBitSet( m_iWeapons, HGRUNT_GRENADELAUNCHER )) + { + DropItem( "ammo_ARgrenades", BodyTarget( GetAbsOrigin() ), vecGunAngles ); + } + + BaseClass::Event_Killed( info ); +} + +//========================================================= +// HandleAnimEvent - catches the monster-specific messages +// that occur when tagged animation frames are played. +//========================================================= +void CNPC_HGrunt::HandleAnimEvent( animevent_t *pEvent ) +{ + Vector vecShootDir; + Vector vecShootOrigin; + + switch( pEvent->event ) + { + case HGRUNT_AE_RELOAD: + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "HGrunt.Reload" ); + + m_cAmmoLoaded = m_iClipSize; + ClearCondition( COND_NO_PRIMARY_AMMO); + } + break; + + case HGRUNT_AE_GREN_TOSS: + { + CHandGrenade *pGrenade = (CHandGrenade*)Create( "grenade_hand", GetAbsOrigin() + Vector(0,0,60), vec3_angle ); + if ( pGrenade ) + { + pGrenade->ShootTimed( this, m_vecTossVelocity, 3.5 ); + } + + m_iLastGrenadeCondition = COND_NONE; + m_flNextGrenadeCheck = gpGlobals->curtime + 6;// wait six seconds before even looking again to see if a grenade can be thrown. + + Msg( "Tossing a grenade to flush you out!\n" ); + } + break; + + case HGRUNT_AE_GREN_LAUNCH: + { + CPASAttenuationFilter filter2( this ); + EmitSound( filter2, entindex(), "HGrunt.GrenadeLaunch" ); + + Vector vecSrc; + QAngle angAngles; + + GetAttachment( "0", vecSrc, angAngles ); + + CGrenadeMP5 * m_pMyGrenade = (CGrenadeMP5*)Create( "grenade_mp5", vecSrc, angAngles, this ); + m_pMyGrenade->SetAbsVelocity( m_vecTossVelocity ); + m_pMyGrenade->SetLocalAngularVelocity( QAngle( random->RandomFloat( -100, -500 ), 0, 0 ) ); + m_pMyGrenade->SetMoveType( MOVETYPE_FLYGRAVITY ); + m_pMyGrenade->SetThrower( this ); + m_pMyGrenade->SetDamage( sk_plr_dmg_mp5_grenade.GetFloat() ); + + if (g_iSkillLevel == SKILL_HARD) + m_flNextGrenadeCheck = gpGlobals->curtime + random->RandomFloat( 2, 5 );// wait a random amount of time before shooting again + else + m_flNextGrenadeCheck = gpGlobals->curtime + 6;// wait six seconds before even looking again to see if a grenade can be thrown. + + m_iLastGrenadeCondition = COND_NONE; + + Msg( "Using grenade launcer to flush you out!\n" ); + } + break; + + case HGRUNT_AE_GREN_DROP: + { + CHandGrenade *pGrenade = (CHandGrenade*)Create( "grenade_hand", Weapon_ShootPosition(), vec3_angle ); + if ( pGrenade ) + { + pGrenade->ShootTimed( this, m_vecTossVelocity, 3.5 ); + } + + m_iLastGrenadeCondition = COND_NONE; + Msg( "Dropping a grenade!\n" ); + } + break; + + case HGRUNT_AE_BURST1: + { + if ( FBitSet( m_iWeapons, HGRUNT_9MMAR ) ) + { + Shoot(); + + CPASAttenuationFilter filter3( this ); + // the first round of the three round burst plays the sound and puts a sound in the world sound list. + EmitSound( filter3, entindex(), "HGrunt.9MM" ); + } + else + { + Shotgun( ); + + CPASAttenuationFilter filter4( this ); + EmitSound( filter4, entindex(), "HGrunt.Shotgun" ); + } + + CSoundEnt::InsertSound ( SOUND_COMBAT, GetAbsOrigin(), 384, 0.3 ); + } + break; + + case HGRUNT_AE_BURST2: + case HGRUNT_AE_BURST3: + Shoot(); + break; + + case HGRUNT_AE_KICK: + { + CBaseEntity *pHurt = Kick(); + + if ( pHurt ) + { + // SOUND HERE! + Vector forward, up; + AngleVectors( GetAbsAngles(), &forward, NULL, &up ); + + if ( pHurt->GetFlags() & ( FL_NPC | FL_CLIENT ) ) + pHurt->ViewPunch( QAngle( 15, 0, 0) ); + + // Don't give velocity or damage to the world + if( pHurt->entindex() > 0 ) + { + pHurt->ApplyAbsVelocityImpulse( forward * 100 + up * 50 ); + + CTakeDamageInfo info( this, this, sk_hgrunt_kick.GetFloat(), DMG_CLUB ); + CalculateMeleeDamageForce( &info, forward, pHurt->GetAbsOrigin() ); + pHurt->TakeDamage( info ); + } + } + } + break; + + case HGRUNT_AE_CAUGHT_ENEMY: + { + if ( FOkToSpeak() ) + { + SENTENCEG_PlayRndSz( edict(), "HG_ALERT", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + JustSpoke(); + } + + } + + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } +} + +void CNPC_HGrunt::SetAim( const Vector &aimDir ) +{ + QAngle angDir; + VectorAngles( aimDir, angDir ); + + float curPitch = GetPoseParameter( "XR" ); + float newPitch = curPitch + UTIL_AngleDiff( UTIL_ApproachAngle( angDir.x, curPitch, 60 ), curPitch ); + + SetPoseParameter( "XR", -newPitch ); +} + +//========================================================= +// Shoot +//========================================================= +void CNPC_HGrunt::Shoot ( void ) +{ + if ( GetEnemy() == NULL ) + return; + + Vector vecShootOrigin = Weapon_ShootPosition(); + Vector vecShootDir = GetShootEnemyDir( vecShootOrigin ); + + Vector forward, right, up; + AngleVectors( GetAbsAngles(), &forward, &right, &up ); + + Vector vecShellVelocity = right * random->RandomFloat(40,90) + up * random->RandomFloat( 75,200 ) + forward * random->RandomFloat( -40, 40 ); + EjectShell( vecShootOrigin - vecShootDir * 24, vecShellVelocity, GetAbsAngles().y, 0 ); + FireBullets(1, vecShootOrigin, vecShootDir, VECTOR_CONE_10DEGREES, 2048, m_iAmmoType ); // shoot +-5 degrees + + DoMuzzleFlash(); + + m_cAmmoLoaded--;// take away a bullet! + + SetAim( vecShootDir ); +} + +//========================================================= +// Shoot +//========================================================= +void CNPC_HGrunt::Shotgun ( void ) +{ + if ( GetEnemy() == NULL ) + return; + + Vector vecShootOrigin = Weapon_ShootPosition(); + Vector vecShootDir = GetShootEnemyDir( vecShootOrigin ); + + Vector forward, right, up; + AngleVectors( GetAbsAngles(), &forward, &right, &up ); + + Vector vecShellVelocity = right * random->RandomFloat(40,90) + up * random->RandomFloat( 75,200 ) + forward * random->RandomFloat( -40, 40 ); + EjectShell( vecShootOrigin - vecShootDir * 24, vecShellVelocity, GetAbsAngles().y, 1 ); + FireBullets( sk_hgrunt_pellets.GetFloat(), vecShootOrigin, vecShootDir, VECTOR_CONE_15DEGREES, 2048, m_iAmmoType, 0 ); // shoot +-7.5 degrees + + DoMuzzleFlash(); + + m_cAmmoLoaded--;// take away a bullet! + + SetAim( vecShootDir ); +} + +//========================================================= +// start task +//========================================================= +void CNPC_HGrunt::StartTask ( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_GRUNT_CHECK_FIRE: + if ( !NoFriendlyFire() ) + { + SetCondition( COND_WEAPON_BLOCKED_BY_FRIEND ); + } + TaskComplete(); + break; + + case TASK_GRUNT_SPEAK_SENTENCE: + SpeakSentence(); + TaskComplete(); + break; + + case TASK_WALK_PATH: + case TASK_RUN_PATH: + // grunt no longer assumes he is covered if he moves + Forget( bits_MEMORY_INCOVER ); + BaseClass ::StartTask( pTask ); + break; + + case TASK_RELOAD: + SetIdealActivity( ACT_RELOAD ); + break; + + case TASK_GRUNT_FACE_TOSS_DIR: + break; + + case TASK_FACE_IDEAL: + case TASK_FACE_ENEMY: + BaseClass::StartTask( pTask ); + if (GetMoveType() == MOVETYPE_FLYGRAVITY) + { + SetIdealActivity( ACT_GLIDE ); + } + break; + + default: + BaseClass::StartTask( pTask ); + break; + } +} + +//========================================================= +// RunTask +//========================================================= +void CNPC_HGrunt::RunTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_GRUNT_FACE_TOSS_DIR: + { + // project a point along the toss vector and turn to face that point. + GetMotor()->SetIdealYawToTargetAndUpdate( GetAbsOrigin() + m_vecTossVelocity * 64, AI_KEEP_YAW_SPEED ); + + if ( FacingIdeal() ) + { + TaskComplete(); + } + break; + } + default: + { + BaseClass::RunTask( pTask ); + break; + } + } +} + +//========================================================= +// PainSound +//========================================================= +void CNPC_HGrunt::PainSound( const CTakeDamageInfo &info ) +{ + if ( gpGlobals->curtime > m_flNextPainTime ) + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "HGrunt.Pain" ); + + m_flNextPainTime = gpGlobals->curtime + 1; + } +} + +//========================================================= +// DeathSound +//========================================================= +void CNPC_HGrunt::DeathSound( const CTakeDamageInfo &info ) +{ + CPASAttenuationFilter filter( this, ATTN_IDLE ); + EmitSound( filter, entindex(), "HGrunt.Die" ); +} + +//========================================================= +// SetActivity +//========================================================= +Activity CNPC_HGrunt::NPC_TranslateActivity( Activity eNewActivity ) +{ + switch ( eNewActivity) + { + case ACT_RANGE_ATTACK1: + // grunt is either shooting standing or shooting crouched + if (FBitSet( m_iWeapons, HGRUNT_9MMAR)) + { + if ( m_fStanding ) + { + // get aimable sequence + return (Activity)ACT_GRUNT_MP5_STANDING; + } + else + { + // get crouching shoot + return (Activity)ACT_GRUNT_MP5_CROUCHING; + } + } + else + { + if ( m_fStanding ) + { + // get aimable sequence + return (Activity)ACT_GRUNT_SHOTGUN_STANDING; + } + else + { + // get crouching shoot + return (Activity)ACT_GRUNT_SHOTGUN_CROUCHING; + } + } + break; + case ACT_RANGE_ATTACK2: + // grunt is going to a secondary long range attack. This may be a thrown + // grenade or fired grenade, we must determine which and pick proper sequence + if ( m_iWeapons & HGRUNT_HANDGRENADE ) + { + // get toss anim + return (Activity)ACT_GRUNT_TOSS_GRENADE; + } + else + { + // get launch anim + return (Activity)ACT_GRUNT_LAUNCH_GRENADE; + } + break; + case ACT_RUN: + if ( m_iHealth <= HGRUNT_LIMP_HEALTH ) + { + // limp! + return ACT_RUN_HURT; + } + else + { + return eNewActivity; + } + break; + case ACT_WALK: + if ( m_iHealth <= HGRUNT_LIMP_HEALTH ) + { + // limp! + return ACT_WALK_HURT; + } + else + { + return eNewActivity; + } + break; + case ACT_IDLE: + if ( m_NPCState == NPC_STATE_COMBAT ) + { + eNewActivity = ACT_IDLE_ANGRY; + } + + break; + } + + return BaseClass::NPC_TranslateActivity( eNewActivity ); +} + +void CNPC_HGrunt::ClearAttackConditions( void ) +{ + bool fCanRangeAttack2 = HasCondition( COND_CAN_RANGE_ATTACK2 ); + + // Call the base class. + BaseClass::ClearAttackConditions(); + + if( fCanRangeAttack2 ) + { + // We don't allow the base class to clear this condition because we + // don't sense for it every frame. + SetCondition( COND_CAN_RANGE_ATTACK2 ); + } +} + +int CNPC_HGrunt::SelectSchedule( void ) +{ + // clear old sentence + m_iSentence = HGRUNT_SENT_NONE; + + // flying? If PRONE, barnacle has me. IF not, it's assumed I am rapelling. + if ( GetMoveType() == MOVETYPE_FLYGRAVITY && m_NPCState != NPC_STATE_PRONE ) + { + if (GetFlags() & FL_ONGROUND) + { + // just landed + SetMoveType( MOVETYPE_STEP ); + SetGravity( 1.0 ); + return SCHED_GRUNT_REPEL_LAND; + } + else + { + // repel down a rope, + if ( m_NPCState == NPC_STATE_COMBAT ) + return SCHED_GRUNT_REPEL_ATTACK; + else + return SCHED_GRUNT_REPEL; + } + } + + // grunts place HIGH priority on running away from danger sounds. + if ( HasCondition ( COND_HEAR_DANGER ) ) + { + // dangerous sound nearby! + + //!!!KELLY - currently, this is the grunt's signal that a grenade has landed nearby, + // and the grunt should find cover from the blast + // good place for "SHIT!" or some other colorful verbal indicator of dismay. + // It's not safe to play a verbal order here "Scatter", etc cause + // this may only affect a single individual in a squad. + + if (FOkToSpeak()) + { + SENTENCEG_PlayRndSz( edict(), "HG_GREN", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + JustSpoke(); + } + + return SCHED_TAKE_COVER_FROM_BEST_SOUND; + } + + switch ( m_NPCState ) + { + + case NPC_STATE_PRONE: + { + if (m_bInBarnacleMouth) + { + return SCHED_GRUNT_BARNACLE_CHOMP; + } + else + { + return SCHED_GRUNT_BARNACLE_HIT; + } + } + + case NPC_STATE_COMBAT: + { +// dead enemy + if ( HasCondition( COND_ENEMY_DEAD ) ) + { + // call base class, all code to handle dead enemies is centralized there. + return BaseClass::SelectSchedule(); + } + +// new enemy + if ( HasCondition( COND_NEW_ENEMY) ) + { + if ( m_pSquad ) + { + if ( !m_pSquad->IsLeader( this ) ) + { + return SCHED_TAKE_COVER_FROM_ENEMY; + } + else + { + //!!!KELLY - the leader of a squad of grunts has just seen the player or a + // monster and has made it the squad's enemy. You + // can check pev->flags for FL_CLIENT to determine whether this is the player + // or a monster. He's going to immediately start + // firing, though. If you'd like, we can make an alternate "first sight" + // schedule where the leader plays a handsign anim + // that gives us enough time to hear a short sentence or spoken command + // before he starts pluggin away. + if (FOkToSpeak())// && RANDOM_LONG(0,1)) + { + if ((GetEnemy() != NULL) && GetEnemy()->IsPlayer()) + // player + SENTENCEG_PlayRndSz( edict(), "HG_ALERT", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + else if ((GetEnemy() != NULL) && + (GetEnemy()->Classify() != CLASS_PLAYER_ALLY) && + (GetEnemy()->Classify() != CLASS_HUMAN_PASSIVE) && + (GetEnemy()->Classify() != CLASS_MACHINE) ) + // monster + SENTENCEG_PlayRndSz( edict(), "HG_MONST", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + + JustSpoke(); + } + + if ( HasCondition ( COND_CAN_RANGE_ATTACK1 ) ) + { + return SCHED_GRUNT_SUPPRESS; + } + else + { + return SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE; + } + } + } + } +// no ammo + else if ( HasCondition ( COND_NO_PRIMARY_AMMO ) ) + { + //!!!KELLY - this individual just realized he's out of bullet ammo. + // He's going to try to find cover to run to and reload, but rarely, if + // none is available, he'll drop and reload in the open here. + return SCHED_GRUNT_HIDE_RELOAD; + } + +// damaged just a little + else if ( HasCondition( COND_LIGHT_DAMAGE ) ) + { + // if hurt: + // 90% chance of taking cover + // 10% chance of flinch. + int iPercent = random->RandomInt(0,99); + + if ( iPercent <= 90 && GetEnemy() != NULL ) + { + // only try to take cover if we actually have an enemy! + + //!!!KELLY - this grunt was hit and is going to run to cover. + if (FOkToSpeak()) // && RANDOM_LONG(0,1)) + { + //SENTENCEG_PlayRndSz( ENT(pev), "HG_COVER", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + m_iSentence = HGRUNT_SENT_COVER; + //JustSpoke(); + } + return SCHED_TAKE_COVER_FROM_ENEMY; + } + else + { + return SCHED_SMALL_FLINCH; + } + } +// can kick + else if ( HasCondition( COND_CAN_MELEE_ATTACK1 ) ) + { + return SCHED_MELEE_ATTACK1; + } +// can grenade launch + + else if ( FBitSet( m_iWeapons, HGRUNT_GRENADELAUNCHER) && HasCondition ( COND_CAN_RANGE_ATTACK2 ) && OccupyStrategySlotRange( SQUAD_SLOT_GRENADE1, SQUAD_SLOT_GRENADE2 ) ) + { + // shoot a grenade if you can + return SCHED_RANGE_ATTACK2; + } +// can shoot + else if ( HasCondition ( COND_CAN_RANGE_ATTACK1 ) ) + { + if ( m_pSquad ) + { + if ( m_pSquad->GetLeader() != NULL ) + { + + CAI_BaseNPC *pSquadLeader = m_pSquad->GetLeader()->MyNPCPointer(); + + // if the enemy has eluded the squad and a squad member has just located the enemy + // and the enemy does not see the squad member, issue a call to the squad to waste a + // little time and give the player a chance to turn. + if ( pSquadLeader && pSquadLeader->EnemyHasEludedMe() && !HasCondition ( COND_ENEMY_FACING_ME ) ) + { + return SCHED_GRUNT_FOUND_ENEMY; + } + } + } + + if ( OccupyStrategySlotRange ( SQUAD_SLOT_ENGAGE1, SQUAD_SLOT_ENGAGE2 ) ) + { + // try to take an available ENGAGE slot + return SCHED_RANGE_ATTACK1; + } + else if ( HasCondition ( COND_CAN_RANGE_ATTACK2 ) && OccupyStrategySlotRange( SQUAD_SLOT_GRENADE1, SQUAD_SLOT_GRENADE2 ) ) + { + // throw a grenade if can and no engage slots are available + return SCHED_RANGE_ATTACK2; + } + else + { + // hide! + return SCHED_TAKE_COVER_FROM_ENEMY; + } + } +// can't see enemy + else if ( HasCondition( COND_ENEMY_OCCLUDED ) ) + { + if ( HasCondition( COND_CAN_RANGE_ATTACK2 ) && OccupyStrategySlotRange( SQUAD_SLOT_GRENADE1, SQUAD_SLOT_GRENADE2 ) ) + { + //!!!KELLY - this grunt is about to throw or fire a grenade at the player. Great place for "fire in the hole" "frag out" etc + if (FOkToSpeak()) + { + SENTENCEG_PlayRndSz( edict(), "HG_THROW", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + JustSpoke(); + } + return SCHED_RANGE_ATTACK2; + } + else if ( OccupyStrategySlotRange ( SQUAD_SLOT_ENGAGE1, SQUAD_SLOT_ENGAGE2 ) ) + { + //!!!KELLY - grunt cannot see the enemy and has just decided to + // charge the enemy's position. + if (FOkToSpeak())// && RANDOM_LONG(0,1)) + { + //SENTENCEG_PlayRndSz( ENT(pev), "HG_CHARGE", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + m_iSentence = HGRUNT_SENT_CHARGE; + //JustSpoke(); + } + + return SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE; + } + else + { + //!!!KELLY - grunt is going to stay put for a couple seconds to see if + // the enemy wanders back out into the open, or approaches the + // grunt's covered position. Good place for a taunt, I guess? + if (FOkToSpeak() && random->RandomInt(0,1)) + { + SENTENCEG_PlayRndSz( edict(), "HG_TAUNT", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + JustSpoke(); + } + return SCHED_STANDOFF; + } + } + + if ( HasCondition( COND_SEE_ENEMY ) && !HasCondition ( COND_CAN_RANGE_ATTACK1 ) ) + { + return SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE; + } + } + case NPC_STATE_ALERT: + if ( HasCondition( COND_ENEMY_DEAD ) && SelectWeightedSequence( ACT_VICTORY_DANCE ) != ACTIVITY_NOT_AVAILABLE ) + { + // Scan around for new enemies + return SCHED_VICTORY_DANCE; + } + break; + } + + return BaseClass::SelectSchedule(); +} + +int CNPC_HGrunt::TranslateSchedule( int scheduleType ) +{ + + if ( scheduleType == SCHED_CHASE_ENEMY_FAILED ) + { + return SCHED_ESTABLISH_LINE_OF_FIRE; + } + switch ( scheduleType ) + { + case SCHED_TAKE_COVER_FROM_ENEMY: + { + if ( m_pSquad ) + { + if ( g_iSkillLevel == SKILL_HARD && HasCondition( COND_CAN_RANGE_ATTACK2 ) && OccupyStrategySlotRange( SQUAD_SLOT_GRENADE1, SQUAD_SLOT_GRENADE2 ) ) + { + if (FOkToSpeak()) + { + SENTENCEG_PlayRndSz( edict(), "HG_THROW", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + JustSpoke(); + } + return SCHED_GRUNT_TOSS_GRENADE_COVER; + } + else + { + return SCHED_GRUNT_TAKE_COVER; + } + } + else + { + if ( random->RandomInt(0,1) ) + { + return SCHED_GRUNT_TAKE_COVER; + } + else + { + return SCHED_GRUNT_GRENADE_COVER; + } + } + } + case SCHED_GRUNT_TAKE_COVER_FAILED: + { + if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) && OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) ) + { + return SCHED_RANGE_ATTACK1; + } + + return SCHED_FAIL; + } + break; + + case SCHED_RANGE_ATTACK1: + { + // randomly stand or crouch + if ( random->RandomInt( 0,9 ) == 0) + { + m_fStanding = random->RandomInt( 0, 1 ) != 0; + } + + if ( m_fStanding ) + return SCHED_GRUNT_RANGE_ATTACK1B; + else + return SCHED_GRUNT_RANGE_ATTACK1A; + } + + case SCHED_RANGE_ATTACK2: + { + return SCHED_GRUNT_RANGE_ATTACK2; + } + case SCHED_VICTORY_DANCE: + { + if ( m_pSquad ) + { + if ( !m_pSquad->IsLeader( this ) ) + { + return SCHED_GRUNT_FAIL; + } + } + + return SCHED_GRUNT_VICTORY_DANCE; + } + case SCHED_GRUNT_SUPPRESS: + { + if ( GetEnemy()->IsPlayer() && m_fFirstEncounter ) + { + m_fFirstEncounter = FALSE;// after first encounter, leader won't issue handsigns anymore when he has a new enemy + return SCHED_GRUNT_SIGNAL_SUPPRESS; + } + else + { + return SCHED_GRUNT_SUPPRESS; + } + } + case SCHED_FAIL: + { + if ( GetEnemy() != NULL ) + { + // grunt has an enemy, so pick a different default fail schedule most likely to help recover. + return SCHED_GRUNT_COMBAT_FAIL; + } + + return SCHED_GRUNT_FAIL; + } + case SCHED_GRUNT_REPEL: + { + Vector vecVel = GetAbsVelocity(); + if ( vecVel.z > -128 ) + { + vecVel.z -= 32; + SetAbsVelocity( vecVel ); + } + + return SCHED_GRUNT_REPEL; + } + case SCHED_GRUNT_REPEL_ATTACK: + { + Vector vecVel = GetAbsVelocity(); + if ( vecVel.z > -128 ) + { + vecVel.z -= 32; + SetAbsVelocity( vecVel ); + } + + return SCHED_GRUNT_REPEL_ATTACK; + } + default: + { + return BaseClass::TranslateSchedule( scheduleType ); + } + } +} + +//========================================================= +// CHGruntRepel - when triggered, spawns a monster_human_grunt +// repelling down a line. +//========================================================= + +class CNPC_HGruntRepel:public CAI_BaseNPC +{ + DECLARE_CLASS( CNPC_HGruntRepel, CAI_BaseNPC ); +public: + void Spawn( void ); + void Precache( void ); + void RepelUse ( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + int m_iSpriteTexture; // Don't save, precache + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( monster_grunt_repel, CNPC_HGruntRepel ); + + + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CNPC_HGruntRepel ) + DEFINE_USEFUNC( RepelUse ), + //DEFINE_FIELD( m_iSpriteTexture, FIELD_INTEGER ), +END_DATADESC() + +void CNPC_HGruntRepel::Spawn( void ) +{ + Precache( ); + SetSolid( SOLID_NONE ); + + SetUse( &CNPC_HGruntRepel::RepelUse ); +} + +void CNPC_HGruntRepel::Precache( void ) +{ + UTIL_PrecacheOther( "monster_human_grunt" ); + m_iSpriteTexture = PrecacheModel( "sprites/rope.vmt" ); +} + +void CNPC_HGruntRepel::RepelUse ( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + trace_t tr; + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + Vector( 0, 0, -4096.0), MASK_NPCSOLID, this,COLLISION_GROUP_NONE, &tr); + + CBaseEntity *pEntity = Create( "monster_human_grunt", GetAbsOrigin(), GetAbsAngles() ); + CAI_BaseNPC *pGrunt = pEntity->MyNPCPointer( ); + pGrunt->SetMoveType( MOVETYPE_FLYGRAVITY ); + pGrunt->SetGravity( 0.001 ); + pGrunt->SetAbsVelocity( Vector( 0, 0, random->RandomFloat( -196, -128 ) ) ); + pGrunt->SetActivity( ACT_GLIDE ); + // UNDONE: position? + pGrunt->m_vecLastPosition = tr.endpos; + + CBeam *pBeam = CBeam::BeamCreate( "sprites/rope.vmt", 10 ); + pBeam->PointEntInit( GetAbsOrigin() + Vector(0,0,112), pGrunt ); + pBeam->SetBeamFlags( FBEAM_SOLID ); + pBeam->SetColor( 255, 255, 255 ); + pBeam->SetThink( &CBaseEntity::SUB_Remove ); + SetNextThink( gpGlobals->curtime + -4096.0 * tr.fraction / pGrunt->GetAbsVelocity().z + 0.5 ); + + UTIL_Remove( this ); +} + + +//------------------------------------------------------------------------------ +// +// Schedules +// +//------------------------------------------------------------------------------ +AI_BEGIN_CUSTOM_NPC( monster_human_grunt, CNPC_HGrunt ) + + DECLARE_ACTIVITY( ACT_GRUNT_LAUNCH_GRENADE ) + DECLARE_ACTIVITY( ACT_GRUNT_TOSS_GRENADE ) + DECLARE_ACTIVITY( ACT_GRUNT_MP5_STANDING ); + DECLARE_ACTIVITY( ACT_GRUNT_MP5_CROUCHING ); + DECLARE_ACTIVITY( ACT_GRUNT_SHOTGUN_STANDING ); + DECLARE_ACTIVITY( ACT_GRUNT_SHOTGUN_CROUCHING ); + + DECLARE_CONDITION( COND_GRUNT_NOFIRE ) + + DECLARE_TASK( TASK_GRUNT_FACE_TOSS_DIR ) + DECLARE_TASK( TASK_GRUNT_SPEAK_SENTENCE ) + DECLARE_TASK( TASK_GRUNT_CHECK_FIRE ) + + DECLARE_SQUADSLOT( SQUAD_SLOT_GRENADE1 ) + DECLARE_SQUADSLOT( SQUAD_SLOT_GRENADE2 ) + DECLARE_SQUADSLOT( SQUAD_SLOT_ENGAGE1 ) + DECLARE_SQUADSLOT( SQUAD_SLOT_ENGAGE2 ) + + //========================================================= + // > SCHED_GRUNT_FAIL + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_FAIL, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT 0" + " TASK_WAIT_PVS 0" + " " + " Interrupts" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_MELEE_ATTACK1" + ) + + //========================================================= + // > SCHED_GRUNT_COMBAT_FAIL + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_COMBAT_FAIL, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT_FACE_ENEMY 2" + " TASK_WAIT_PVS 0" + " " + " Interrupts" + " COND_CAN_RANGE_ATTACK1" + ) + + //========================================================= + // > SCHED_GRUNT_VICTORY_DANCE + // Victory dance! + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_VICTORY_DANCE, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_WAIT 1.5" + " TASK_GET_PATH_TO_ENEMY_CORPSE 0" + " TASK_WALK_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_FACE_ENEMY 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_VICTORY_DANCE" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + ) + + //========================================================= + // > SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE + // Establish line of fire - move to a position that allows + // the grunt to attack. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE_RETRY" + " TASK_GET_PATH_TO_ENEMY 0" + " TASK_GRUNT_SPEAK_SENTENCE 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + " COND_CAN_MELEE_ATTACK2" + " COND_HEAR_DANGER" + ) + + //========================================================= + // This is a schedule I added that borrows some HL2 technology + // to be smarter in cases where HL1 was pretty dumb. I've wedged + // this between ESTABLISH_LINE_OF_FIRE and TAKE_COVER_FROM_ENEMY (sjb) + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE_RETRY, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_GRUNT_TAKE_COVER_FROM_ENEMY" + " TASK_GET_PATH_TO_ENEMY_LKP_LOS 0" + " TASK_GRUNT_SPEAK_SENTENCE 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + " COND_CAN_MELEE_ATTACK2" + " COND_HEAR_DANGER" + ) + + //========================================================= + // > SCHED_GRUNT_FOUND_ENEMY + // Grunt established sight with an enemy + // that was hiding from the squad. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_FOUND_ENEMY, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_SIGNAL1" + " " + " Interrupts" + " COND_HEAR_DANGER" + ) + + + //========================================================= + // > SCHED_GRUNT_COMBAT_FACE + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_COMBAT_FACE, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_FACE_ENEMY 0" + " TASK_WAIT 1.5" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_GRUNT_SWEEP" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + ) + + + //========================================================= + // > SCHED_GRUNT_SIGNAL_SUPPRESS + // Suppressing fire - don't stop shooting until the clip is + // empty or grunt gets hurt. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_SIGNAL_SUPPRESS, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_IDEAL 0" + " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_SIGNAL2" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " " + " Interrupts" + " COND_ENEMY_DEAD" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_GRUNT_NOFIRE" + " COND_NO_PRIMARY_AMMO" + " COND_HEAR_DANGER" + ) + + //========================================================= + // > SCHED_GRUNT_SUPPRESS + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_SUPPRESS, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " " + " Interrupts" + " COND_ENEMY_DEAD" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_GRUNT_NOFIRE" + " COND_NO_PRIMARY_AMMO" + " COND_HEAR_DANGER" + ) + + //========================================================= + // > SCHED_GRUNT_WAIT_IN_COVER + // grunt wait in cover - we don't allow danger or the ability + // to attack to break a grunt's run to cover schedule, but + // when a grunt is in cover, we do want them to attack if they can. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_WAIT_IN_COVER, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT_FACE_ENEMY 1" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_HEAR_DANGER" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_MELEE_ATTACK2" + ) + + + //========================================================= + // > SCHED_GRUNT_TAKE_COVER + // !!!BUGBUG - set a decent fail schedule here. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_TAKE_COVER, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_GRUNT_TAKE_COVER_FAILED" + " TASK_WAIT 0.2" + " TASK_FIND_COVER_FROM_ENEMY 0" + " TASK_GRUNT_SPEAK_SENTENCE 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_REMEMBER MEMORY:INCOVER" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_GRUNT_WAIT_IN_COVER" + " " + " Interrupts" + ) + + + //========================================================= + // > SCHED_GRUNT_GRENADE_COVER + // drop grenade then run to cover. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_GRENADE_COVER, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FIND_COVER_FROM_ENEMY 99" + " TASK_FIND_FAR_NODE_COVER_FROM_ENEMY 384" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_SPECIAL_ATTACK1" + " TASK_CLEAR_MOVE_WAIT 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_GRUNT_WAIT_IN_COVER" + " " + " Interrupts" + ) + + //========================================================= + // > SCHED_GRUNT_TOSS_GRENADE_COVER + // drop grenade then run to cover. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_TOSS_GRENADE_COVER, + + " Tasks" + " TASK_FACE_ENEMY 0" + " TASK_RANGE_ATTACK2 0" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_GRUNT_TAKE_COVER_FROM_ENEMY" + " " + " Interrupts" + ) + + //========================================================= + // > SCHED_GRUNT_HIDE_RELOAD + // Grunt reload schedule + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_HIDE_RELOAD, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_GRUNT_RELOAD" + " TASK_FIND_COVER_FROM_ENEMY 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_REMEMBER MEMORY:INCOVER" + " TASK_FACE_ENEMY 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_RELOAD" + " " + " Interrupts" + " COND_HEAVY_DAMAGE" + " COND_HEAR_DANGER" + ) + + //========================================================= + // > SCHED_GRUNT_SWEEP + // Do a turning sweep of the area + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_SWEEP, + + " Tasks" + " TASK_TURN_LEFT 179" + " TASK_WAIT 1" + " TASK_TURN_LEFT 179" + " TASK_WAIT 1" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + " COND_HEAR_WORLD" + " COND_HEAR_DANGER" + " COND_HEAR_PLAYER" + ) + + //========================================================= + // > SCHED_GRUNT_RANGE_ATTACK1A + // primary range attack. Overriden because base class stops attacking when the enemy is occluded. + // grunt's grenade toss requires the enemy be occluded. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_RANGE_ATTACK1A, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_CROUCH" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_HEAVY_DAMAGE" + " COND_ENEMY_OCCLUDED" + " COND_HEAR_DANGER" + " COND_GRUNT_NOFIRE" + " COND_NO_PRIMARY_AMMO" + ) + + //========================================================= + // > SCHED_GRUNT_RANGE_ATTACK1B + // primary range attack. Overriden because base class stops attacking when the enemy is occluded. + // grunt's grenade toss requires the enemy be occluded. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_RANGE_ATTACK1B, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_IDLE_ANGRY" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_HEAVY_DAMAGE" + " COND_ENEMY_OCCLUDED" + " COND_HEAR_DANGER" + " COND_GRUNT_NOFIRE" + " COND_NO_PRIMARY_AMMO" + ) + + //========================================================= + // > SCHED_GRUNT_RANGE_ATTACK2 + // secondary range attack. Overriden because base class stops attacking when the enemy is occluded. + // grunt's grenade toss requires the enemy be occluded. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_RANGE_ATTACK2, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_GRUNT_FACE_TOSS_DIR 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_RANGE_ATTACK2" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_GRUNT_WAIT_IN_COVER" // don't run immediately after throwing grenade. + " " + " Interrupts" + ) + + //========================================================= + // > SCHED_GRUNT_REPEL + // secondary range attack. Overriden because base class stops attacking when the enemy is occluded. + // grunt's grenade toss requires the enemy be occluded. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_REPEL, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_IDEAL 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_GLIDE" + " " + " Interrupts" + " COND_SEE_ENEMY" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_HEAR_DANGER" + " COND_HEAR_PLAYER" + " COND_HEAR_COMBAT" + ) + + //========================================================= + // > SCHED_GRUNT_REPEL_ATTACK + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_REPEL_ATTACK, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_FLY" + " " + " Interrupts" + " COND_ENEMY_OCCLUDED" + ) + + //========================================================= + // > SCHED_GRUNT_REPEL_LAND + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_REPEL_LAND, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_LAND" + " TASK_GET_PATH_TO_LASTPOSITION 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_CLEAR_LASTPOSITION 0" + " " + " Interrupts" + " COND_SEE_ENEMY" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_HEAR_DANGER" + " COND_HEAR_COMBAT" + " COND_HEAR_PLAYER" + ) + + //========================================================= + // > SCHED_GRUNT_RELOAD + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_RELOAD, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_RELOAD" + " " + " Interrupts" + " COND_HEAVY_DAMAGE" + ) + + //========================================================= + // > SCHED_GRUNT_TAKE_COVER_FROM_ENEMY + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_TAKE_COVER_FROM_ENEMY, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_WAIT 0.2" + " TASK_FIND_COVER_FROM_ENEMY 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_REMEMBER MEMORY:INCOVER" + " TASK_FACE_ENEMY 0" + " TASK_WAIT 1" + " " + " Interrupts" + " COND_NEW_ENEMY" + ) + + //========================================================= + // > SCHED_GRUNT_TAKE_COVER_FAILED + // special schedule type that forces analysis of conditions and picks + // the best possible schedule to recover from this type of failure. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_TAKE_COVER_FAILED, + " Tasks" + " Interrupts" + ) + + //========================================================= + // > SCHED_GRUNT_BARNACLE_HIT + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_BARNACLE_HIT, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_BARNACLE_HIT" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_GRUNT_BARNACLE_PULL" + "" + " Interrupts" + ) + + //========================================================= + // > SCHED_GRUNT_BARNACLE_PULL + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_BARNACLE_PULL, + + " Tasks" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_BARNACLE_PULL" + "" + " Interrupts" + ) + + //========================================================= + // > SCHED_GRUNT_BARNACLE_CHOMP + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_BARNACLE_CHOMP, + + " Tasks" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_BARNACLE_CHOMP" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_GRUNT_BARNACLE_CHEW" + "" + " Interrupts" + ) + + //========================================================= + // > SCHED_GRUNT_BARNACLE_CHEW + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_BARNACLE_CHEW, + + " Tasks" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_BARNACLE_CHEW" + ) + +AI_END_CUSTOM_NPC() + + +//========================================================= +// DEAD HGRUNT PROP +//========================================================= +class CNPC_DeadHGrunt : public CAI_BaseNPC +{ + DECLARE_CLASS( CNPC_DeadHGrunt, CAI_BaseNPC ); +public: + void Spawn( void ); + Class_T Classify ( void ) { return CLASS_HUMAN_MILITARY; } + float MaxYawSpeed( void ) { return 8.0f; } + + bool KeyValue( const char *szKeyName, const char *szValue ); + + int m_iPose;// which sequence to display -- temporary, don't need to save + static char *m_szPoses[3]; +}; + +char *CNPC_DeadHGrunt::m_szPoses[] = { "deadstomach", "deadside", "deadsitting" }; + +bool CNPC_DeadHGrunt::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "pose" ) ) + m_iPose = atoi( szValue ); + else + CAI_BaseNPC::KeyValue( szKeyName, szValue ); + + return true; +} + +LINK_ENTITY_TO_CLASS( monster_hgrunt_dead, CNPC_DeadHGrunt ); + +//========================================================= +// ********** DeadHGrunt SPAWN ********** +//========================================================= +void CNPC_DeadHGrunt::Spawn( void ) +{ + PrecacheModel("models/hgrunt.mdl"); + SetModel( "models/hgrunt.mdl" ); + + ClearEffects(); + SetSequence( 0 ); + m_bloodColor = BLOOD_COLOR_RED; + + SetSequence( LookupSequence( m_szPoses[m_iPose] ) ); + + if ( GetSequence() == -1 ) + { + Msg ( "Dead hgrunt with bad pose\n" ); + } + + // Corpses have less health + m_iHealth = 8; + + // map old bodies onto new bodies + switch( m_nBody ) + { + case 0: // Grunt with Gun + m_nBody = 0; + m_nSkin = 0; + SetBodygroup( HEAD_GROUP, HEAD_GRUNT ); + SetBodygroup( GUN_GROUP, GUN_MP5 ); + break; + case 1: // Commander with Gun + m_nBody = 0; + m_nSkin = 0; + SetBodygroup( HEAD_GROUP, HEAD_COMMANDER ); + SetBodygroup( GUN_GROUP, GUN_MP5 ); + break; + case 2: // Grunt no Gun + m_nBody = 0; + m_nSkin = 0; + SetBodygroup( HEAD_GROUP, HEAD_GRUNT ); + SetBodygroup( GUN_GROUP, GUN_NONE ); + break; + case 3: // Commander no Gun + m_nBody = 0; + m_nSkin = 0; + SetBodygroup( HEAD_GROUP, HEAD_COMMANDER ); + SetBodygroup( GUN_GROUP, GUN_NONE ); + break; + } + + NPCInitDead(); +} |