/*** * * Copyright (c) 1996-2001, Valve LLC. All rights reserved. * * This product contains software technology licensed from Id * Software, Inc. ("Id Technology"). Id Technology (c) 1996 Id Software, Inc. * All Rights Reserved. * * This source code contains proprietary and confidential information of * Valve LLC and its suppliers. Access to this code is restricted to * persons who have executed a written SDK license with Valve. Any access, * use or distribution of this code by or to any unlicensed person is illegal. * ****/ //========================================================= // friendly grunt //========================================================= // UNDONE: Holster weapon? #include "extdll.h" #include "plane.h" #include "util.h" #include "cbase.h" #include "monsters.h" #include "squad.h" #include "squadmonster.h" #include "talkmonster.h" #include "schedule.h" #include "animation.h" #include "weapons.h" #include "talkmonster.h" #include "soundent.h" #include "effects.h" #include "customentity.h" #include "fgrunt.h" #ifdef DEBUG #include "gearbox_utils.h" #endif extern int g_fGruntQuestion; // true if an idle grunt asked a question. Cleared when someone answers. extern DLL_GLOBAL int g_iSkillLevel; extern Schedule_t slIdleStand[]; extern Schedule_t slGruntFail[]; extern Schedule_t slGruntCombatFail[]; extern Schedule_t slGruntVictoryDance[]; extern Schedule_t slGruntEstablishLineOfFire[]; extern Schedule_t slGruntFoundEnemy[]; extern Schedule_t slGruntCombatFace[]; extern Schedule_t slGruntSignalSuppress[]; extern Schedule_t slGruntSuppress[]; extern Schedule_t slGruntWaitInCover[]; extern Schedule_t slGruntTakeCover[]; extern Schedule_t slGruntGrenadeCover[]; extern Schedule_t slGruntTossGrenadeCover[]; extern Schedule_t slGruntTakeCoverFromBestSound[]; extern Schedule_t slGruntHideReload[]; extern Schedule_t slGruntSweep[]; extern Schedule_t slGruntRangeAttack1A[]; extern Schedule_t slGruntRangeAttack1B[]; extern Schedule_t slGruntRangeAttack2[]; extern Schedule_t slGruntRepel[]; extern Schedule_t slGruntRepelAttack[]; extern Schedule_t slGruntRepelLand[]; extern Schedule_t slBaFollow[]; extern Schedule_t slBarneyEnemyDraw[]; extern Schedule_t slBaFaceTarget[]; extern Schedule_t slIdleBaStand[]; //========================================================= // monster-specific DEFINE's //========================================================= #define FGRUNT_CLIP_SIZE_MP5 36 // how many bullets in a clip? - NOTE: 3 round burst sound, so keep as 3 * x! #define FGRUNT_CLIP_SIZE_SAW 50 // Same as above #define FGRUNT_VOL 0.35 // volume of grunt sounds #define FGRUNT_ATTN ATTN_NORM // attenutation of grunt sentences #define FGRUNT_LIMP_HEALTH 20 #define FGRUNT_DMG_HEADSHOT ( DMG_BULLET | DMG_CLUB ) // damage types that can kill a grunt with a single headshot. #define FGRUNT_NUM_HEADS 8 // how many grunt heads are there? #define FGRUNT_MINIMUM_HEADSHOT_DAMAGE 15 // must do at least this much damage in one shot to head to score a headshot kill #define FGRUNT_SENTENCE_VOLUME (float)0.35 // volume of grunt sentences #define FGRUNT_9MMAR ( 1 << 0 ) #define FGRUNT_HANDGRENADE ( 1 << 1 ) #define FGRUNT_GRENADELAUNCHER ( 1 << 2 ) #define FGRUNT_SHOTGUN ( 1 << 3 ) #define FGRUNT_SAW ( 1 << 4 ) #define HEAD_GROUP 1 #define TORSO_GROUP 2 #define GUN_GROUP 3 #define HEAD_GASMASK 0 #define HEAD_BERET 1 #define HEAD_OPS_MASK 2 #define HEAD_BANDANA_WHITE 3 #define HEAD_BANDANA_BLACK 4 #define HEAD_MP 5 #define HEAD_MAJOR 6 #define HEAD_BERET_BLACK 7 #define TORSO_BACKPACK 0 #define TORSO_SAW 1 #define TORSO_NOBACKPACK 2 #define TORSO_SHELLS 3 #define GUN_MP5 0 #define GUN_SHOTGUN 1 #define GUN_SAW 2 #define GUN_NONE 3 //========================================================= // Monster's Anim Events Go Here //========================================================= #define FGRUNT_AE_RELOAD ( 2 ) #define FGRUNT_AE_KICK ( 3 ) #define FGRUNT_AE_BURST1 ( 4 ) #define FGRUNT_AE_BURST2 ( 5 ) #define FGRUNT_AE_BURST3 ( 6 ) #define FGRUNT_AE_GREN_TOSS ( 7 ) #define FGRUNT_AE_GREN_LAUNCH ( 8 ) #define FGRUNT_AE_GREN_DROP ( 9 ) #define FGRUNT_AE_CAUGHT_ENEMY ( 10) // grunt established sight with an enemy (player only) that had previously eluded the squad. #define FGRUNT_AE_DROP_GUN ( 11) // grunt (probably dead) is dropping his mp5. //========================================================= // monster-specific schedule types //========================================================= enum { SCHED_FGRUNT_SUPPRESS = LAST_COMMON_SCHEDULE + 1, SCHED_FGRUNT_ESTABLISH_LINE_OF_FIRE,// move to a location to set up an attack against the enemy. (usually when a friendly is in the way). SCHED_FGRUNT_COVER_AND_RELOAD, SCHED_FGRUNT_SWEEP, SCHED_FGRUNT_FOUND_ENEMY, SCHED_FGRUNT_REPEL, SCHED_FGRUNT_REPEL_ATTACK, SCHED_FGRUNT_REPEL_LAND, SCHED_FGRUNT_WAIT_FACE_ENEMY, SCHED_FGRUNT_TAKECOVER_FAILED,// special schedule type that forces analysis of conditions and picks the best possible schedule to recover from this type of failure. SCHED_FGRUNT_ELOF_FAIL, }; //========================================================= // monster-specific tasks //========================================================= enum { TASK_FGRUNT_FACE_TOSS_DIR = LAST_COMMON_TASK + 1, TASK_FGRUNT_SPEAK_SENTENCE, TASK_FGRUNT_CHECK_FIRE, }; static const char* g_pszDeathSounds[] = { "fgrunt/death1.wav", "fgrunt/death2.wav", "fgrunt/death3.wav", "fgrunt/death4.wav", "fgrunt/death5.wav", "fgrunt/death6.wav", }; static const char* g_pszPainSounds[] = { "fgrunt/pain1.wav", "fgrunt/pain2.wav", "fgrunt/pain3.wav", "fgrunt/pain4.wav", "fgrunt/pain5.wav", "fgrunt/pain6.wav", }; #define FGRUNT_NUM_DEATH_SOUNDS ARRAYSIZE( g_pszDeathSounds ) #define FGRUNT_NUM_PAIN_SOUNDS ARRAYSIZE( g_pszPainSounds ) //========================================================= // CFGrunt //========================================================= LINK_ENTITY_TO_CLASS(monster_human_grunt_ally, CFGrunt); TYPEDESCRIPTION CFGrunt::m_SaveData[] = { DEFINE_FIELD(CFGrunt, m_painTime, FIELD_TIME), DEFINE_FIELD(CFGrunt, m_checkAttackTime, FIELD_TIME), DEFINE_FIELD(CFGrunt, m_lastAttackCheck, FIELD_BOOLEAN), DEFINE_FIELD(CFGrunt, m_flPlayerDamage, FIELD_FLOAT), DEFINE_FIELD(CFGrunt, head, FIELD_INTEGER), DEFINE_FIELD(CFGrunt, torso, FIELD_INTEGER), }; IMPLEMENT_SAVERESTORE(CFGrunt, CTalkMonster); #if 0 //========================================================= // AI Schedules Specific to this monster //========================================================= Task_t tlFgFollow[] = { { TASK_MOVE_TO_TARGET_RANGE, (float)128 }, // Move within 128 of target ent (client) { TASK_SET_SCHEDULE, (float)SCHED_TARGET_FACE }, }; Schedule_t slFgFollow[] = { { tlFgFollow, ARRAYSIZE(tlFgFollow), bits_COND_NEW_ENEMY | bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE | bits_COND_HEAR_SOUND | bits_COND_PROVOKED, bits_SOUND_DANGER, "Follow" }, }; //========================================================= // BarneyDraw- much better looking draw schedule for when // barney knows who he's gonna attack. //========================================================= Task_t tlFgEnemyDraw[] = { { TASK_STOP_MOVING, 0 }, { TASK_FACE_ENEMY, 0 }, { TASK_PLAY_SEQUENCE_FACE_ENEMY, (float)ACT_ARM }, }; Schedule_t slFgEnemyDraw[] = { { tlFgEnemyDraw, ARRAYSIZE(tlFgEnemyDraw), 0, 0, "FGrunt Enemy Draw" } }; Task_t tlFgFaceTarget[] = { { TASK_SET_ACTIVITY, (float)ACT_IDLE }, { TASK_FACE_TARGET, (float)0 }, { TASK_SET_ACTIVITY, (float)ACT_IDLE }, { TASK_SET_SCHEDULE, (float)SCHED_TARGET_CHASE }, }; Schedule_t slFgFaceTarget[] = { { tlFgFaceTarget, ARRAYSIZE(tlFgFaceTarget), bits_COND_CLIENT_PUSH | bits_COND_NEW_ENEMY | bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE | bits_COND_HEAR_SOUND | bits_COND_PROVOKED, bits_SOUND_DANGER, "FaceTarget" }, }; Task_t tlIdleFgStand[] = { { TASK_STOP_MOVING, 0 }, { TASK_SET_ACTIVITY, (float)ACT_IDLE }, { TASK_WAIT, (float)2 }, // repick IDLESTAND every two seconds. { TASK_TLK_HEADRESET, (float)0 }, // reset head position }; Schedule_t slIdleFgStand[] = { { tlIdleFgStand, ARRAYSIZE(tlIdleFgStand), bits_COND_NEW_ENEMY | bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE | bits_COND_HEAR_SOUND | bits_COND_SMELL | bits_COND_PROVOKED, bits_SOUND_COMBAT |// sound flags - change these, and you'll break the talking code. //bits_SOUND_PLAYER | //bits_SOUND_WORLD | bits_SOUND_DANGER | bits_SOUND_MEAT |// scents bits_SOUND_CARCASS | bits_SOUND_GARBAGE, "IdleStand" }, }; DEFINE_CUSTOM_SCHEDULES(CFGrunt) { slFgFollow, slFgEnemyDraw, slFgFaceTarget, slIdleFgStand, }; #else DEFINE_CUSTOM_SCHEDULES(CFGrunt) { slGruntFail, slGruntCombatFail, slGruntVictoryDance, slGruntEstablishLineOfFire, slGruntFoundEnemy, slGruntCombatFace, slGruntSignalSuppress, slGruntSuppress, slGruntWaitInCover, slGruntTakeCover, slGruntGrenadeCover, slGruntTossGrenadeCover, slGruntTakeCoverFromBestSound, slGruntHideReload, slGruntSweep, slGruntRangeAttack1A, slGruntRangeAttack1B, slGruntRangeAttack2, slGruntRepel, slGruntRepelAttack, slGruntRepelLand, slBaFollow, slBarneyEnemyDraw, slBaFaceTarget, slIdleBaStand, }; #endif IMPLEMENT_CUSTOM_SCHEDULES(CFGrunt, CTalkMonster); const char *CFGrunt::pGruntSentences[] = { "FG_GREN", // grenade scared grunt "FG_ALERT", // sees player "FG_MONSTER", // sees monster "FG_COVER", // running to cover "FG_THROW", // about to throw grenade "FG_CHARGE", // running out to get the enemy "FG_TAUNT", // say rude things }; typedef enum { FGRUNT_SENT_NONE = -1, FGRUNT_SENT_GREN = 0, FGRUNT_SENT_ALERT, FGRUNT_SENT_MONSTER, FGRUNT_SENT_COVER, FGRUNT_SENT_THROW, FGRUNT_SENT_CHARGE, FGRUNT_SENT_TAUNT, } FGRUNT_SENTENCE_TYPES; //========================================================= // 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 CFGrunt::SpeakSentence(void) { if (m_iSentence == FGRUNT_SENT_NONE) { // no sentence cued up. return; } if (FOkToSpeak()) { SENTENCEG_PlayRndSz(ENT(pev), pGruntSentences[m_iSentence], FGRUNT_SENTENCE_VOLUME, FGRUNT_ATTN, 0, m_voicePitch); JustSpoke(); } } //========================================================= // IRelationship - overridden because Alien Grunts are // Human Grunt's nemesis. //========================================================= int CFGrunt::IRelationship(CBaseEntity *pTarget) { if (FClassnameIs(pTarget->pev, "monster_alien_grunt") || (FClassnameIs(pTarget->pev, "monster_gargantua"))) { return R_NM; } return CTalkMonster::IRelationship(pTarget); } //========================================================= // GibMonster - make gun fly through the air. //========================================================= void CFGrunt::GibMonster(void) { Vector vecGunPos; Vector vecGunAngles; if (GetBodygroup(2) != 2) {// throw a gun if the grunt has one GetAttachment(0, vecGunPos, vecGunAngles); CBaseEntity *pGun; if (FBitSet(pev->weapons, FGRUNT_SHOTGUN)) { pGun = DropItem("weapon_shotgun", vecGunPos, vecGunAngles); } else if (FBitSet(pev->weapons, FGRUNT_SAW)) { pGun = DropItem("weapon_m249", vecGunPos, vecGunAngles); } else { pGun = DropItem("weapon_9mmAR", vecGunPos, vecGunAngles); } if (pGun) { pGun->pev->velocity = Vector(RANDOM_FLOAT(-100, 100), RANDOM_FLOAT(-100, 100), RANDOM_FLOAT(200, 300)); pGun->pev->avelocity = Vector(0, RANDOM_FLOAT(200, 400), 0); } if (FBitSet(pev->weapons, FGRUNT_GRENADELAUNCHER)) { pGun = DropItem("ammo_ARgrenades", vecGunPos, vecGunAngles); if (pGun) { pGun->pev->velocity = Vector(RANDOM_FLOAT(-100, 100), RANDOM_FLOAT(-100, 100), RANDOM_FLOAT(200, 300)); pGun->pev->avelocity = Vector(0, RANDOM_FLOAT(200, 400), 0); } } } CTalkMonster::GibMonster(); } //========================================================= // ISoundMask - returns a bit mask indicating which types // of sounds this monster regards. //========================================================= int CFGrunt::ISoundMask(void) { return bits_SOUND_WORLD | bits_SOUND_COMBAT | bits_SOUND_CARCASS | bits_SOUND_MEAT | bits_SOUND_GARBAGE | bits_SOUND_DANGER | bits_SOUND_PLAYER; } //========================================================= // someone else is talking - don't speak //========================================================= BOOL CFGrunt::FOkToSpeak(void) { // if someone else is talking, don't speak if (gpGlobals->time <= CTalkMonster::g_talkWaitTime) return FALSE; if (pev->spawnflags & SF_MONSTER_GAG) { if (m_MonsterState != MONSTERSTATE_COMBAT) { // no talking outside of combat if gagged. return FALSE; } } // if player is not in pvs, don't speak // if (FNullEnt(FIND_CLIENT_IN_PVS(edict()))) // return FALSE; return TRUE; } //========================================================= //========================================================= void CFGrunt::JustSpoke(void) { CTalkMonster::g_talkWaitTime = gpGlobals->time + RANDOM_FLOAT(1.5, 2.0); m_iSentence = FGRUNT_SENT_NONE; } //========================================================= // Classify - indicates this monster's place in the // relationship table. //========================================================= int CFGrunt::Classify(void) { return CLASS_PLAYER_ALLY; } //========================================================= //========================================================= CBaseEntity *CFGrunt::Kick(void) { TraceResult tr; UTIL_MakeVectors(pev->angles); Vector vecStart = pev->origin; vecStart.z += pev->size.z * 0.5; Vector vecEnd = vecStart + (gpGlobals->v_forward * 70); UTIL_TraceHull(vecStart, vecEnd, dont_ignore_monsters, head_hull, ENT(pev), &tr); if (tr.pHit) { CBaseEntity *pEntity = CBaseEntity::Instance(tr.pHit); return pEntity; } return NULL; } //========================================================= // GetGunPosition return the end of the barrel //========================================================= Vector CFGrunt::GetGunPosition() { if (m_fStanding) { return pev->origin + Vector(0, 0, 60); } else { return pev->origin + Vector(0, 0, 48); } } //========================================================= // ALertSound - barney says "Freeze!" //========================================================= void CFGrunt::AlertSound(void) { if (m_hEnemy != 0) { if (FOkToSpeak()) { PlaySentence("FG_ATTACK", RANDOM_FLOAT(2.8, 3.2), VOL_NORM, ATTN_IDLE); } } } //========================================================= // SetYawSpeed - allows each sequence to have a different // turn rate associated with it. //========================================================= void CFGrunt::SetYawSpeed(void) { int ys; ys = 0; switch (m_Activity) { case ACT_IDLE: ys = 70; break; case ACT_WALK: ys = 70; break; case ACT_RUN: ys = 90; break; default: ys = 70; break; } pev->yaw_speed = ys; } //========================================================= // 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 CFGrunt::FCanCheckAttacks(void) { if (!HasConditions(bits_COND_ENEMY_TOOFAR)) { return TRUE; } else { return FALSE; } } //========================================================= // CheckMeleeAttack1 //========================================================= BOOL CFGrunt::CheckMeleeAttack1(float flDot, float flDist) { CBaseMonster *pEnemy; if (m_hEnemy != 0) { pEnemy = m_hEnemy->MyMonsterPointer(); if (!pEnemy) { return FALSE; } } if (flDist <= 64 && flDot >= 0.7 && pEnemy->Classify() != CLASS_ALIEN_BIOWEAPON && pEnemy->Classify() != CLASS_PLAYER_BIOWEAPON) { return TRUE; } return FALSE; } //========================================================= // CheckRangeAttack1 //========================================================= BOOL CFGrunt::CheckRangeAttack1(float flDot, float flDist) { if (flDist <= 1024 && flDot >= 0.5) { if (gpGlobals->time > m_checkAttackTime) { TraceResult tr; Vector shootOrigin = pev->origin + Vector(0, 0, 55); CBaseEntity *pEnemy = m_hEnemy; Vector shootTarget = ((pEnemy->BodyTarget(shootOrigin) - pEnemy->pev->origin) + m_vecEnemyLKP); UTIL_TraceLine(shootOrigin, shootTarget, dont_ignore_monsters, ENT(pev), &tr); m_checkAttackTime = gpGlobals->time + 1; if (tr.flFraction == 1.0 || (tr.pHit != NULL && CBaseEntity::Instance(tr.pHit) == pEnemy)) m_lastAttackCheck = TRUE; else m_lastAttackCheck = FALSE; m_checkAttackTime = gpGlobals->time + 1.5; } return m_lastAttackCheck; } CSquadMonster* pSquadMonster = MySquadMonsterPointer(); if (pSquadMonster && pSquadMonster->InSquad()) { if (!HasConditions(bits_COND_ENEMY_OCCLUDED) && flDist <= 2048 && flDot >= 0.5 && pSquadMonster->NoFriendlyFire()) { TraceResult tr; if (!m_hEnemy->IsPlayer() && flDist <= 64) { // kick nonclients, but don't shoot at them. return FALSE; } Vector vecSrc = GetGunPosition(); // verify that a bullet fired from the gun will hit the enemy before the world. UTIL_TraceLine(vecSrc, m_hEnemy->BodyTarget(vecSrc), ignore_monsters, ignore_glass, ENT(pev), &tr); if (tr.flFraction == 1.0) { return TRUE; } } } return FALSE; } //========================================================= // CheckRangeAttack2 - this checks the Grunt's grenade // attack. //========================================================= BOOL CFGrunt::CheckRangeAttack2(float flDot, float flDist) { if (!FBitSet(pev->weapons, (FGRUNT_HANDGRENADE | FGRUNT_GRENADELAUNCHER))) { return FALSE; } // if the grunt isn't moving, it's ok to check. if (m_flGroundSpeed != 0) { m_fThrowGrenade = FALSE; return m_fThrowGrenade; } // assume things haven't changed too much since last time if (gpGlobals->time < m_flNextGrenadeCheck) { return m_fThrowGrenade; } if (!FBitSet(m_hEnemy->pev->flags, FL_ONGROUND) && m_hEnemy->pev->waterlevel == 0 && m_vecEnemyLKP.z > pev->absmax.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! m_fThrowGrenade = FALSE; return m_fThrowGrenade; } Vector vecTarget; if (FBitSet(pev->weapons, FGRUNT_HANDGRENADE)) { // find feet if (RANDOM_LONG(0, 1)) { // magically know where they are vecTarget = Vector(m_hEnemy->pev->origin.x, m_hEnemy->pev->origin.y, m_hEnemy->pev->absmin.z); } else { // toss it to where you last saw them vecTarget = m_vecEnemyLKP; } // vecTarget = m_vecEnemyLKP + (m_hEnemy->BodyTarget( pev->origin ) - m_hEnemy->pev->origin); // estimate position // vecTarget = vecTarget + m_hEnemy->pev->velocity * 2; } else { // find target // vecTarget = m_hEnemy->BodyTarget( pev->origin ); vecTarget = m_vecEnemyLKP + (m_hEnemy->BodyTarget(pev->origin) - m_hEnemy->pev->origin); // estimate position if (HasConditions(bits_COND_SEE_ENEMY)) vecTarget = vecTarget + ((vecTarget - pev->origin).Length() / gSkillData.hgruntGrenadeSpeed) * m_hEnemy->pev->velocity; } // are any of my squad members near the intended grenade impact area? CSquadMonster* pSquadMonster = MySquadMonsterPointer(); if (pSquadMonster && pSquadMonster->InSquad()) { if (pSquadMonster->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->time + 1; // one full second. m_fThrowGrenade = FALSE; } } if ((vecTarget - pev->origin).Length2D() <= 256) { // crap, I don't want to blow myself up m_flNextGrenadeCheck = gpGlobals->time + 1; // one full second. m_fThrowGrenade = FALSE; return m_fThrowGrenade; } if (FBitSet(pev->weapons, FGRUNT_HANDGRENADE)) { Vector vecToss = VecCheckToss(pev, GetGunPosition(), vecTarget, 0.5); if (vecToss != g_vecZero) { m_vecTossVelocity = vecToss; // throw a hand grenade m_fThrowGrenade = TRUE; // don't check again for a while. m_flNextGrenadeCheck = gpGlobals->time; // 1/3 second. } else { // don't throw m_fThrowGrenade = FALSE; // don't check again for a while. m_flNextGrenadeCheck = gpGlobals->time + 1; // one full second. } } else { Vector vecToss = VecCheckThrow(pev, GetGunPosition(), vecTarget, gSkillData.hgruntGrenadeSpeed, 0.5); if (vecToss != g_vecZero) { m_vecTossVelocity = vecToss; // throw a hand grenade m_fThrowGrenade = TRUE; // don't check again for a while. m_flNextGrenadeCheck = gpGlobals->time + 0.3; // 1/3 second. } else { // don't throw m_fThrowGrenade = FALSE; // don't check again for a while. m_flNextGrenadeCheck = gpGlobals->time + 1; // one full second. } } return m_fThrowGrenade; } //========================================================= // Fire //========================================================= void CFGrunt::Fire(const Vector& vecShootOrigin, const Vector& vecShootDir, const Vector& vecSpread, int model, int effects, int bulletType, int soundType) { UTIL_MakeVectors(pev->angles); Vector vecShellVelocity = gpGlobals->v_right * RANDOM_FLOAT(40, 90) + gpGlobals->v_up * RANDOM_FLOAT(75, 200) + gpGlobals->v_forward * RANDOM_FLOAT(-40, 40); EjectBrass(vecShootOrigin - vecShootDir * 24, vecShellVelocity, pev->angles.y, model, soundType); FireBullets(1, vecShootOrigin, vecShootDir, vecSpread, 2048, bulletType); // shoot +-5 degrees pev->effects |= effects; Vector angDir = UTIL_VecToAngles(vecShootDir); SetBlending(0, angDir.x); } //========================================================= // Shoot //========================================================= void CFGrunt::Shoot(void) { if (m_hEnemy == 0) { return; } Fire(GetGunPosition(), ShootAtEnemy(GetGunPosition()), VECTOR_CONE_10DEGREES, m_iBrassShell, EF_MUZZLEFLASH, BULLET_MONSTER_MP5, TE_BOUNCE_SHELL); m_cAmmoLoaded--;// take away a bullet! } //========================================================= // Shotgun //========================================================= void CFGrunt::Shotgun(void) { if (m_hEnemy == 0) { return; } Fire(GetGunPosition(), ShootAtEnemy(GetGunPosition()), VECTOR_CONE_15DEGREES, m_iShotgunShell, EF_MUZZLEFLASH, BULLET_PLAYER_BUCKSHOT, TE_BOUNCE_SHELL); m_cAmmoLoaded--;// take away a bullet! } //========================================================= // Saw //========================================================= void CFGrunt::Saw(void) { if (m_hEnemy == 0) { return; } Fire(GetGunPosition(), ShootAtEnemy(GetGunPosition()), VECTOR_CONE_10DEGREES, m_iSawShell, EF_MUZZLEFLASH, BULLET_PLAYER_556, TE_BOUNCE_SHELL); m_cAmmoLoaded--;// take away a bullet! } //========================================================= // HandleAnimEvent - catches the monster-specific messages // that occur when tagged animation frames are played. // // Returns number of events handled, 0 if none. //========================================================= void CFGrunt::HandleAnimEvent(MonsterEvent_t *pEvent) { Vector vecShootDir; Vector vecShootOrigin; switch (pEvent->event) { case FGRUNT_AE_DROP_GUN: { Vector vecGunPos; Vector vecGunAngles; GetAttachment(0, vecGunPos, vecGunAngles); // switch to body group with no gun. SetBodygroup(GUN_GROUP, GUN_NONE); // now spawn a gun. DropGun(vecGunPos, vecGunAngles); // Drop grenades if supported. if (FBitSet(pev->weapons, FGRUNT_GRENADELAUNCHER)) { DropItem("ammo_ARgrenades", BodyTarget(pev->origin), vecGunAngles); } } break; case FGRUNT_AE_RELOAD: EMIT_SOUND(ENT(pev), CHAN_WEAPON, "hgrunt/gr_reload1.wav", 1, ATTN_NORM); m_cAmmoLoaded = m_cClipSize; ClearConditions(bits_COND_NO_AMMO_LOADED); break; case FGRUNT_AE_GREN_TOSS: { UTIL_MakeVectors(pev->angles); // CGrenade::ShootTimed( pev, pev->origin + gpGlobals->v_forward * 34 + Vector (0, 0, 32), m_vecTossVelocity, 3.5 ); CGrenade::ShootTimed(pev, GetGunPosition(), m_vecTossVelocity, 3.5); m_fThrowGrenade = FALSE; m_flNextGrenadeCheck = gpGlobals->time + 6;// wait six seconds before even looking again to see if a grenade can be thrown. // !!!LATER - when in a group, only try to throw grenade if ordered. } break; case FGRUNT_AE_GREN_LAUNCH: { EMIT_SOUND(ENT(pev), CHAN_WEAPON, "weapons/glauncher.wav", 0.8, ATTN_NORM); CGrenade::ShootContact(pev, GetGunPosition(), m_vecTossVelocity); m_fThrowGrenade = FALSE; if (g_iSkillLevel == SKILL_HARD) m_flNextGrenadeCheck = gpGlobals->time + RANDOM_FLOAT(2, 5);// wait a random amount of time before shooting again else m_flNextGrenadeCheck = gpGlobals->time + 6;// wait six seconds before even looking again to see if a grenade can be thrown. } break; case FGRUNT_AE_GREN_DROP: { UTIL_MakeVectors(pev->angles); CGrenade::ShootTimed(pev, pev->origin + gpGlobals->v_forward * 17 - gpGlobals->v_right * 27 + gpGlobals->v_up * 6, g_vecZero, 3); } break; case FGRUNT_AE_BURST1: { if (FBitSet(pev->weapons, FGRUNT_9MMAR)) { Shoot(); // the first round of the three round burst plays the sound and puts a sound in the world sound list. if (RANDOM_LONG(0, 1)) { EMIT_SOUND(ENT(pev), CHAN_WEAPON, "hgrunt/gr_mgun1.wav", 1, ATTN_NORM); } else { EMIT_SOUND(ENT(pev), CHAN_WEAPON, "hgrunt/gr_mgun2.wav", 1, ATTN_NORM); } } else if (FBitSet(pev->weapons, FGRUNT_SAW)) { Saw(); switch (RANDOM_LONG(0, 2)) { case 0: EMIT_SOUND(ENT(pev), CHAN_WEAPON, "weapons/saw_fire1.wav", 1, ATTN_NORM); break; case 1: EMIT_SOUND(ENT(pev), CHAN_WEAPON, "weapons/saw_fire2.wav", 1, ATTN_NORM); break; case 2: EMIT_SOUND(ENT(pev), CHAN_WEAPON, "weapons/saw_fire3.wav", 1, ATTN_NORM); break; default: break; } } else { Shotgun(); EMIT_SOUND(ENT(pev), CHAN_WEAPON, "weapons/sbarrel1.wav", 1, ATTN_NORM); } CSoundEnt::InsertSound(bits_SOUND_COMBAT, pev->origin, 384, 0.3); } break; case FGRUNT_AE_BURST2: case FGRUNT_AE_BURST3: if (FBitSet(pev->weapons, FGRUNT_9MMAR)) { Shoot(); } else if(FBitSet(pev->weapons, FGRUNT_SAW)) { Saw(); } break; case FGRUNT_AE_KICK: { CBaseEntity *pHurt = Kick(); if (pHurt) { // SOUND HERE! UTIL_MakeVectors(pev->angles); pHurt->pev->punchangle.x = 15; pHurt->pev->velocity = pHurt->pev->velocity + gpGlobals->v_forward * 100 + gpGlobals->v_up * 50; pHurt->TakeDamage(pev, pev, gSkillData.hgruntAllyDmgKick, DMG_CLUB); } } break; case FGRUNT_AE_CAUGHT_ENEMY: { if (FOkToSpeak()) { SENTENCEG_PlayRndSz(ENT(pev), "FG_ALERT", FGRUNT_SENTENCE_VOLUME, FGRUNT_ATTN, 0, m_voicePitch); JustSpoke(); } } default: CTalkMonster::HandleAnimEvent(pEvent); break; } } //========================================================= // Spawn //========================================================= void CFGrunt::Spawn() { Precache(); SET_MODEL(ENT(pev), "models/hgrunt_opfor.mdl"); UTIL_SetSize(pev, VEC_HUMAN_HULL_MIN, VEC_HUMAN_HULL_MAX); pev->solid = SOLID_SLIDEBOX; pev->movetype = MOVETYPE_STEP; m_bloodColor = BLOOD_COLOR_RED; pev->health = gSkillData.hgruntAllyHealth; pev->view_ofs = Vector(0, 0, 50);// position of the eyes relative to monster's origin. m_flFieldOfView = VIEW_FIELD_WIDE; // NOTE: we need a wide field of view so npc will notice player and say hello m_MonsterState = MONSTERSTATE_NONE; pev->body = 0; // gun in holster m_afCapability = bits_CAP_HEAR | bits_CAP_TURN_HEAD | bits_CAP_DOORS_GROUP; // Select a random head. if (head == -1) { SetBodygroup(HEAD_GROUP, RANDOM_LONG(0, FGRUNT_NUM_HEADS - 1 )); } else { SetBodygroup(HEAD_GROUP, head); } if (pev->weapons == 0) { // initialize to original values pev->weapons = FGRUNT_9MMAR | FGRUNT_HANDGRENADE; } // Setup bodygroups. if (FBitSet(pev->weapons, FGRUNT_SHOTGUN)) { torso = TORSO_SHELLS; SetBodygroup(TORSO_GROUP, torso); SetBodygroup(GUN_GROUP, GUN_SHOTGUN); m_cClipSize = 8; } else if (FBitSet(pev->weapons, FGRUNT_SAW)) { torso = TORSO_SAW; SetBodygroup(TORSO_GROUP, torso); SetBodygroup(GUN_GROUP, GUN_SAW); m_cClipSize = FGRUNT_CLIP_SIZE_SAW; } else { torso = TORSO_BACKPACK; SetBodygroup(TORSO_GROUP, torso); SetBodygroup(GUN_GROUP, GUN_MP5); m_cClipSize = FGRUNT_CLIP_SIZE_MP5; } m_cAmmoLoaded = m_cClipSize; MonsterInit(); SetUse(&CFGrunt::FollowerUse); } //========================================================= // Precache - precaches all resources this monster needs //========================================================= void CFGrunt::Precache() { PRECACHE_MODEL("models/hgrunt_opfor.mdl"); PRECACHE_SOUND("hgrunt/gr_mgun1.wav"); PRECACHE_SOUND("hgrunt/gr_mgun2.wav"); PRECACHE_SOUND("hgrunt/saw_fire1.wav"); PRECACHE_SOUND("hgrunt/saw_fire2.wav"); PRECACHE_SOUND("hgrunt/saw_fire3.wav"); PRECACHE_SOUND("hgrunt/gr_reload1.wav"); PRECACHE_SOUND("weapons/glauncher.wav"); PRECACHE_SOUND("weapons/sbarrel1.wav"); PRECACHE_SOUND("zombie/claw_miss2.wav");// because we use the basemonster SWIPE animation event PRECACHE_SOUND_ARRAY( g_pszDeathSounds ); PRECACHE_SOUND_ARRAY( g_pszPainSounds ); m_iBrassShell = PRECACHE_MODEL("models/shell.mdl");// brass shell m_iShotgunShell = PRECACHE_MODEL("models/shotgunshell.mdl"); m_iSawShell = PRECACHE_MODEL("models/saw_shell.mdl"); // every new barney must call this, otherwise // when a level is loaded, nobody will talk (time is reset to 0) TalkInit(); CTalkMonster::Precache(); } // Init talk data void CFGrunt::TalkInit() { CTalkMonster::TalkInit(); // scientists speach group names (group names are in sentences.txt) m_szGrp[TLK_ANSWER] = "FG_ANSWER"; m_szGrp[TLK_QUESTION] = "FG_QUESTION"; m_szGrp[TLK_IDLE] = "FG_IDLE"; m_szGrp[TLK_STARE] = "FG_STARE"; m_szGrp[TLK_USE] = "FG_OK"; m_szGrp[TLK_UNUSE] = "FG_WAIT"; m_szGrp[TLK_STOP] = "FG_STOP"; m_szGrp[TLK_NOSHOOT] = "FG_SCARED"; m_szGrp[TLK_HELLO] = "FG_HELLO"; m_szGrp[TLK_PLHURT1] = "!FG_CUREA"; m_szGrp[TLK_PLHURT2] = "!FG_CUREB"; m_szGrp[TLK_PLHURT3] = "!FG_CUREC"; m_szGrp[TLK_PHELLO] = NULL; m_szGrp[TLK_PIDLE] = NULL; m_szGrp[TLK_PQUESTION] = "FG_PQUEST"; m_szGrp[TLK_SMELL] = "FG_SMELL"; m_szGrp[TLK_WOUND] = "FG_WOUND"; m_szGrp[TLK_MORTAL] = "FG_MORTAL"; // get voice for head - just one barney voice for now m_voicePitch = 100; } static BOOL IsFacing(entvars_t *pevTest, const Vector &reference) { Vector vecDir = (reference - pevTest->origin); vecDir.z = 0; vecDir = vecDir.Normalize(); Vector forward, angle; angle = pevTest->v_angle; angle.x = 0; UTIL_MakeVectorsPrivate(angle, forward, NULL, NULL); // He's facing me, he meant it if (DotProduct(forward, vecDir) > 0.96) // +/- 15 degrees or so { return TRUE; } return FALSE; } int CFGrunt::TakeDamage(entvars_t* pevInflictor, entvars_t* pevAttacker, float flDamage, int bitsDamageType) { // make sure friends talk about it if player hurts talkmonsters... int ret = CTalkMonster::TakeDamage(pevInflictor, pevAttacker, flDamage, bitsDamageType); if (!IsAlive() || pev->deadflag == DEAD_DYING) return ret; if (m_MonsterState != MONSTERSTATE_PRONE && (pevAttacker->flags & FL_CLIENT)) { m_flPlayerDamage += flDamage; // This is a heurstic to determine if the player intended to harm me // If I have an enemy, we can't establish intent (may just be crossfire) if (m_hEnemy == 0) { // If the player was facing directly at me, or I'm already suspicious, get mad if ((m_afMemory & bits_MEMORY_SUSPICIOUS) || IsFacing(pevAttacker, pev->origin)) { // Alright, now I'm pissed! PlaySentence("FG_MAD", 4, VOL_NORM, ATTN_NORM); Remember(bits_MEMORY_PROVOKED); StopFollowing(TRUE); } else { // Hey, be careful with that PlaySentence("FG_SHOT", 4, VOL_NORM, ATTN_NORM); Remember(bits_MEMORY_SUSPICIOUS); } } else if (!(m_hEnemy->IsPlayer()) && pev->deadflag == DEAD_NO) { PlaySentence("FG_SHOT", 4, VOL_NORM, ATTN_NORM); } } return ret; } //========================================================= // PainSound //========================================================= void CFGrunt::PainSound(void) { if (gpGlobals->time < m_painTime) return; m_painTime = gpGlobals->time + RANDOM_FLOAT(0.5, 0.75); EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, g_pszPainSounds[ RANDOM_LONG(0, FGRUNT_NUM_PAIN_SOUNDS - 1) ], 1, ATTN_NORM, 0, GetVoicePitch()); } //========================================================= // DeathSound //========================================================= void CFGrunt::DeathSound(void) { EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, g_pszDeathSounds[RANDOM_LONG(0, FGRUNT_NUM_DEATH_SOUNDS - 1)], 1, ATTN_NORM, 0, GetVoicePitch()); } //========================================================= // TractAttack //========================================================= void CFGrunt::TraceAttack(entvars_t *pevAttacker, float flDamage, Vector vecDir, TraceResult *ptr, int bitsDamageType) { switch (ptr->iHitgroup) { case HITGROUP_CHEST: case HITGROUP_STOMACH: if (bitsDamageType & (DMG_BULLET | DMG_SLASH | DMG_BLAST)) { flDamage = flDamage / 2; } break; case 10: if (bitsDamageType & (DMG_BULLET | DMG_SLASH | DMG_CLUB)) { flDamage -= 20; if (flDamage <= 0) { UTIL_Ricochet(ptr->vecEndPos, 1.0); flDamage = 0.01; } } // always a head shot ptr->iHitgroup = HITGROUP_HEAD; break; } CTalkMonster::TraceAttack(pevAttacker, flDamage, vecDir, ptr, bitsDamageType); } void CFGrunt::Killed(entvars_t *pevAttacker, int iGib) { if (GetBodygroup(2) != 2) { Vector vecGunPos; Vector vecGunAngles; // throw a gun if the grunt has one GetAttachment(0, vecGunPos, vecGunAngles); DropGun(vecGunPos, vecGunAngles); } SetUse(NULL); CTalkMonster::Killed(pevAttacker, iGib); } //========================================================= // DropGun //========================================================= CBaseEntity* CFGrunt::DropGun(const Vector& vecGunPos, const Vector& vecGunAngles, char* szClassname) { CBaseEntity* pGun = NULL; if (szClassname && *szClassname) { pGun = DropItem(szClassname, vecGunPos, vecGunAngles); if (pGun) { return pGun; } else { ALERT(at_console, "ERROR: Could not find classname %s. No such class.\n", szClassname); } } if (FBitSet(pev->weapons, FGRUNT_SHOTGUN)) { pGun = DropItem("weapon_shotgun", vecGunPos, vecGunAngles); } else if (FBitSet(pev->weapons, FGRUNT_SAW)) { pGun = DropItem("weapon_m249", vecGunPos, vecGunAngles); } else { pGun = DropItem("weapon_9mmAR", vecGunPos, vecGunAngles); } return pGun; } //========================================================= // PrescheduleThink - this function runs after conditions // are collected and before scheduling code is run. //========================================================= void CFGrunt::PrescheduleThink(void) { CSquadMonster* pSquadMonster = MySquadMonsterPointer(); if (pSquadMonster && pSquadMonster->InSquad() && m_hEnemy != 0) { if (HasConditions(bits_COND_SEE_ENEMY)) { // update the squad's last enemy sighting time. pSquadMonster->MySquadLeader()->m_flLastEnemySightTime = gpGlobals->time; } else { if (gpGlobals->time - pSquadMonster->MySquadLeader()->m_flLastEnemySightTime > 5) { // been a while since we've seen the enemy pSquadMonster->MySquadLeader()->m_fEnemyEluded = TRUE; } } } } //========================================================= // AI Schedules Specific to this monster //========================================================= Schedule_t* CFGrunt::GetScheduleOfType(int Type) { #if 1 Schedule_t *psched; switch( Type ) { case SCHED_TAKE_COVER_FROM_ENEMY: { CSquadMonster* pSquadMonster = MySquadMonsterPointer(); if (pSquadMonster && pSquadMonster->InSquad()) { if (g_iSkillLevel == SKILL_HARD && HasConditions(bits_COND_CAN_RANGE_ATTACK2) && pSquadMonster->OccupySlot(bits_SLOTS_HGRUNT_GRENADE)) { if (FOkToSpeak()) { SENTENCEG_PlayRndSz(ENT(pev), "FG_THROW", FGRUNT_SENTENCE_VOLUME, FGRUNT_ATTN, 0, m_voicePitch); JustSpoke(); } return slGruntTossGrenadeCover; } else { return &slGruntTakeCover[0]; } } else { if (RANDOM_LONG(0, 1)) { return &slGruntTakeCover[0]; } else { return &slGruntGrenadeCover[0]; } } } case SCHED_TAKE_COVER_FROM_BEST_SOUND: { return &slGruntTakeCoverFromBestSound[0]; } case SCHED_FGRUNT_TAKECOVER_FAILED: { CSquadMonster* pSquadMonster = MySquadMonsterPointer(); if (pSquadMonster && pSquadMonster->InSquad()) { if (HasConditions(bits_COND_CAN_RANGE_ATTACK1) && pSquadMonster->OccupySlot(bits_SLOTS_HGRUNT_ENGAGE)) { return GetScheduleOfType(SCHED_RANGE_ATTACK1); } } return GetScheduleOfType(SCHED_FAIL); } break; case SCHED_FGRUNT_ELOF_FAIL: { // human grunt is unable to move to a position that allows him to attack the enemy. return GetScheduleOfType(SCHED_TAKE_COVER_FROM_ENEMY); } break; case SCHED_FGRUNT_ESTABLISH_LINE_OF_FIRE: { return &slGruntEstablishLineOfFire[0]; } break; case SCHED_RANGE_ATTACK1: { // randomly stand or crouch if (RANDOM_LONG(0, 9) == 0) m_fStanding = RANDOM_LONG(0, 1); if (m_fStanding) return &slGruntRangeAttack1B[0]; else return &slGruntRangeAttack1A[0]; } case SCHED_RANGE_ATTACK2: { return &slGruntRangeAttack2[0]; } case SCHED_COMBAT_FACE: { return &slGruntCombatFace[0]; } case SCHED_FGRUNT_WAIT_FACE_ENEMY: { return &slGruntWaitInCover[0]; } case SCHED_FGRUNT_SWEEP: { return &slGruntSweep[0]; } case SCHED_FGRUNT_COVER_AND_RELOAD: { return &slGruntHideReload[0]; } case SCHED_FGRUNT_FOUND_ENEMY: { return &slGruntFoundEnemy[0]; } case SCHED_VICTORY_DANCE: { CSquadMonster* pSquadMonster = MySquadMonsterPointer(); if (pSquadMonster && pSquadMonster->InSquad()) { if (!pSquadMonster->IsLeader()) { return &slGruntFail[0]; } } return &slGruntVictoryDance[0]; } case SCHED_FGRUNT_SUPPRESS: { if (m_hEnemy->IsPlayer() && m_fFirstEncounter) { m_fFirstEncounter = FALSE;// after first encounter, leader won't issue handsigns anymore when he has a new enemy return &slGruntSignalSuppress[0]; } else { return &slGruntSuppress[0]; } } case SCHED_FAIL: { if (m_hEnemy != 0) { // grunt has an enemy, so pick a different default fail schedule most likely to help recover. return &slGruntCombatFail[0]; } return &slGruntFail[0]; } case SCHED_FGRUNT_REPEL: { if (pev->velocity.z > -128) pev->velocity.z -= 32; return &slGruntRepel[0]; } case SCHED_FGRUNT_REPEL_ATTACK: { if (pev->velocity.z > -128) pev->velocity.z -= 32; return &slGruntRepelAttack[0]; } case SCHED_FGRUNT_REPEL_LAND: { return &slGruntRepelLand[0]; } case SCHED_ARM_WEAPON: if ( m_hEnemy != 0 ) { // face enemy, then draw. return slBarneyEnemyDraw; } break; // Hook these to make a looping schedule case SCHED_TARGET_FACE: // call base class default so that barney will talk // when 'used' psched = CTalkMonster::GetScheduleOfType(Type); if (psched == slIdleStand) return slBaFaceTarget; // override this gfor different target face behavior else return psched; case SCHED_TARGET_CHASE: return slBaFollow; case SCHED_IDLE_STAND: // call base class default so that scientist will talk // when standing during idle psched = CTalkMonster::GetScheduleOfType(Type); if (psched == slIdleStand) { // just look straight ahead. return slIdleBaStand; } else return psched; } #else Schedule_t *psched; switch (Type) { case SCHED_ARM_WEAPON: if (m_hEnemy != NULL) { // face enemy, then draw. return slFgEnemyDraw; } break; // Hook these to make a looping schedule case SCHED_TARGET_FACE: // call base class default so that barney will talk // when 'used' psched = CTalkMonster::GetScheduleOfType(Type); if (psched == slIdleStand) return slFgFaceTarget; // override this for different target face behavior else return psched; case SCHED_TARGET_CHASE: return slFgFollow; case SCHED_IDLE_STAND: // call base class default so that scientist will talk // when standing during idle psched = CTalkMonster::GetScheduleOfType(Type); if (psched == slIdleStand) { // just look straight ahead. return slIdleFgStand; } else return psched; } #endif return CTalkMonster::GetScheduleOfType(Type); } //========================================================= // GetSchedule - Decides which type of schedule best suits // the monster's current state and conditions. Then calls // monster's member function to get a pointer to a schedule // of the proper type. // // Only supported when this monster has squad support! // //========================================================= Schedule_t *CFGrunt::GetSquadSchedule(void) { CSquadMonster* pSquadMonster = MySquadMonsterPointer(); // This method is reserved for NPCs with squad support!. ASSERT(pSquadMonster != NULL); CSquadMonster* pSquadLeader = pSquadMonster->MySquadLeader(); switch (m_MonsterState) { case MONSTERSTATE_COMBAT: { // new enemy if (HasConditions(bits_COND_NEW_ENEMY)) { pSquadLeader->m_fEnemyEluded = FALSE; if (!pSquadMonster->IsLeader()) { return GetScheduleOfType(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 ((m_hEnemy != 0) && m_hEnemy->IsPlayer()) // player SENTENCEG_PlayRndSz(ENT(pev), "FG_ALERT", FGRUNT_SENTENCE_VOLUME, FGRUNT_ATTN, 0, m_voicePitch); else if ((m_hEnemy != 0) && (m_hEnemy->Classify() != CLASS_PLAYER_ALLY) && (m_hEnemy->Classify() != CLASS_HUMAN_PASSIVE) && (m_hEnemy->Classify() != CLASS_MACHINE)) // monster SENTENCEG_PlayRndSz(ENT(pev), "FG_MONST", FGRUNT_SENTENCE_VOLUME, FGRUNT_ATTN, 0, m_voicePitch); JustSpoke(); } if (HasConditions(bits_COND_CAN_RANGE_ATTACK1)) { return GetScheduleOfType(SCHED_FGRUNT_SUPPRESS); } else { return GetScheduleOfType(SCHED_FGRUNT_ESTABLISH_LINE_OF_FIRE); } } } // can grenade launch else if (FBitSet(pev->weapons, FGRUNT_GRENADELAUNCHER) && HasConditions(bits_COND_CAN_RANGE_ATTACK2) && pSquadMonster->OccupySlot(bits_SLOTS_HGRUNT_GRENADE)) { // shoot a grenade if you can return GetScheduleOfType(SCHED_RANGE_ATTACK2); } // can shoot else if (HasConditions(bits_COND_CAN_RANGE_ATTACK1)) { // 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->m_fEnemyEluded && !HasConditions(bits_COND_ENEMY_FACING_ME)) { pSquadLeader->m_fEnemyEluded = FALSE; return GetScheduleOfType(SCHED_FGRUNT_FOUND_ENEMY); } if (pSquadMonster->OccupySlot(bits_SLOTS_HGRUNT_ENGAGE)) { // try to take an available ENGAGE slot return GetScheduleOfType(SCHED_RANGE_ATTACK1); } else if (HasConditions(bits_COND_CAN_RANGE_ATTACK2) && pSquadMonster->OccupySlot(bits_SLOTS_HGRUNT_GRENADE)) { // throw a grenade if can and no engage slots are available return GetScheduleOfType(SCHED_RANGE_ATTACK2); } else { // hide! return GetScheduleOfType(SCHED_TAKE_COVER_FROM_ENEMY); } } // can't see enemy else if (HasConditions(bits_COND_ENEMY_OCCLUDED)) { if (HasConditions(bits_COND_CAN_RANGE_ATTACK2) && pSquadMonster->OccupySlot(bits_SLOTS_HGRUNT_GRENADE)) { //!!!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(ENT(pev), "FG_THROW", FGRUNT_SENTENCE_VOLUME, FGRUNT_ATTN, 0, m_voicePitch); JustSpoke(); } return GetScheduleOfType(SCHED_RANGE_ATTACK2); } else if (pSquadMonster->OccupySlot(bits_SLOTS_HGRUNT_ENGAGE)) { //!!!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_ATTN, 0, m_voicePitch); m_iSentence = FGRUNT_SENT_CHARGE; //JustSpoke(); } return GetScheduleOfType(SCHED_FGRUNT_ESTABLISH_LINE_OF_FIRE); } } } break; default: break; } return NULL; } //========================================================= // GetSchedule - Decides which type of schedule best suits // the monster's current state and conditions. Then calls // monster's member function to get a pointer to a schedule // of the proper type. //========================================================= Schedule_t *CFGrunt::GetSchedule(void) { // clear old sentence m_iSentence = FGRUNT_SENT_NONE; // flying? If PRONE, barnacle has me. IF not, it's assumed I am rapelling. if (pev->movetype == MOVETYPE_FLY && m_MonsterState != MONSTERSTATE_PRONE) { if (pev->flags & FL_ONGROUND) { // just landed pev->movetype = MOVETYPE_STEP; return GetScheduleOfType(SCHED_FGRUNT_REPEL_LAND); } else { // repel down a rope, if (m_MonsterState == MONSTERSTATE_COMBAT) return GetScheduleOfType(SCHED_FGRUNT_REPEL_ATTACK); else return GetScheduleOfType(SCHED_FGRUNT_REPEL); } } // grunts place HIGH priority on running away from danger sounds. if (HasConditions(bits_COND_HEAR_SOUND)) { CSound *pSound; pSound = PBestSound(); ASSERT( pSound != NULL ); if ( pSound) { if (pSound->m_iType & bits_SOUND_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( ENT(pev), "FG_GREN", FGRUNT_SENTENCE_VOLUME, FGRUNT_ATTN, 0, m_voicePitch); JustSpoke(); } return GetScheduleOfType( SCHED_TAKE_COVER_FROM_BEST_SOUND ); } /* if (!HasConditions( bits_COND_SEE_ENEMY ) && ( pSound->m_iType & (bits_SOUND_PLAYER | bits_SOUND_COMBAT) )) { MakeIdealYaw( pSound->m_vecOrigin ); } */ } } if (HasConditions(bits_COND_ENEMY_DEAD) && FOkToSpeak()) { PlaySentence("FG_KILL", 4, VOL_NORM, ATTN_NORM); } #if 1 // Check if we have squad support. CSquadMonster* pSquadMonster = MySquadMonsterPointer(); if (pSquadMonster) { // squad schedule. Schedule_t* squadSched = GetSquadSchedule(); if (squadSched) { return squadSched; } } #endif switch (m_MonsterState) { case MONSTERSTATE_COMBAT: { // dead enemy if (HasConditions(bits_COND_ENEMY_DEAD)) { // call base class, all code to handle dead enemies is centralized there. return CBaseMonster::GetSchedule(); } // new enemy if (HasConditions(bits_COND_NEW_ENEMY)) { } // no ammo else if (HasConditions(bits_COND_NO_AMMO_LOADED)) { //!!!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 GetScheduleOfType(SCHED_FGRUNT_COVER_AND_RELOAD); } // damaged just a little else if (HasConditions(bits_COND_LIGHT_DAMAGE)) { // if hurt: // 90% chance of taking cover // 10% chance of flinch. int iPercent = RANDOM_LONG(0, 99); if (iPercent <= 90 && m_hEnemy != 0) { // 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_ATTN, 0, m_voicePitch); m_iSentence = FGRUNT_SENT_COVER; //JustSpoke(); } return GetScheduleOfType(SCHED_TAKE_COVER_FROM_ENEMY); } else { return GetScheduleOfType(SCHED_SMALL_FLINCH); } } // can kick else if (HasConditions(bits_COND_CAN_MELEE_ATTACK1)) { return GetScheduleOfType(SCHED_MELEE_ATTACK1); } // can shoot else if (HasConditions(bits_COND_CAN_RANGE_ATTACK1)) { if (FBitSet(pev->weapons, FGRUNT_GRENADELAUNCHER) && HasConditions(bits_COND_CAN_RANGE_ATTACK2)) { // shoot a grenade if you can return GetScheduleOfType(SCHED_RANGE_ATTACK2); } // can shoot else if (HasConditions(bits_COND_CAN_RANGE_ATTACK1)) { return GetScheduleOfType(SCHED_RANGE_ATTACK1); } } // can't see enemy else if (HasConditions(bits_COND_ENEMY_OCCLUDED)) { //!!!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_LONG(0, 1)) { SENTENCEG_PlayRndSz(ENT(pev), "FG_TAUNT", FGRUNT_SENTENCE_VOLUME, FGRUNT_ATTN, 0, m_voicePitch); JustSpoke(); } return GetScheduleOfType(SCHED_STANDOFF); } if (HasConditions(bits_COND_SEE_ENEMY) && !HasConditions(bits_COND_CAN_RANGE_ATTACK1)) { return GetScheduleOfType(SCHED_FGRUNT_ESTABLISH_LINE_OF_FIRE); } } case MONSTERSTATE_ALERT: case MONSTERSTATE_IDLE: { if (HasConditions(bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE)) { // flinch if hurt return GetScheduleOfType(SCHED_SMALL_FLINCH); } if (m_hEnemy == 0 && IsFollowing()) { if (!m_hTargetEnt->IsAlive()) { // UNDONE: Comment about the recently dead player here? StopFollowing(FALSE); break; } else { if (HasConditions(bits_COND_CLIENT_PUSH)) { return GetScheduleOfType(SCHED_MOVE_AWAY_FOLLOW); } return GetScheduleOfType(SCHED_TARGET_FACE); } } if (HasConditions(bits_COND_CLIENT_PUSH)) { return GetScheduleOfType(SCHED_MOVE_AWAY); } // try to say something about smells TrySmellTalk(); break; } } return CTalkMonster::GetSchedule(); } MONSTERSTATE CFGrunt::GetIdealState(void) { return CTalkMonster::GetIdealState(); } void CFGrunt::DeclineFollowing(void) { PlaySentence("FG_POK", 2, VOL_NORM, ATTN_NORM); } void CFGrunt::StartTask(Task_t *pTask) { #if 1 m_iTaskStatus = TASKSTATUS_RUNNING; switch (pTask->iTask) { case TASK_FGRUNT_CHECK_FIRE: { CSquadMonster* pSquadMonster = MySquadMonsterPointer(); if (pSquadMonster && !pSquadMonster->NoFriendlyFire()) { SetConditions(bits_COND_SPECIAL1); // bits_COND_GRUNT_NOFIRE } TaskComplete(); } break; case TASK_FGRUNT_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); CTalkMonster::StartTask(pTask); break; case TASK_RELOAD: m_IdealActivity = ACT_RELOAD; break; case TASK_FGRUNT_FACE_TOSS_DIR: break; case TASK_FACE_IDEAL: case TASK_FACE_ENEMY: CTalkMonster::StartTask(pTask); if (pev->movetype == MOVETYPE_FLY) { m_IdealActivity = ACT_GLIDE; } break; default: CTalkMonster::StartTask(pTask); break; } #endif CTalkMonster::StartTask(pTask); } void CFGrunt::RunTask(Task_t *pTask) { #if 1 switch ( pTask->iTask ) { case TASK_FGRUNT_FACE_TOSS_DIR: { // project a point along the toss vector and turn to face that point. MakeIdealYaw( pev->origin + m_vecTossVelocity * 64 ); ChangeYaw( pev->yaw_speed ); if ( FacingIdeal() ) { m_iTaskStatus = TASKSTATUS_COMPLETE; } } break; case TASK_RANGE_ATTACK1: { //if (m_hEnemy != NULL && (m_hEnemy->IsPlayer())) //{ // pev->framerate = 1.5; //} CTalkMonster::RunTask(pTask); } break; default: { CTalkMonster::RunTask(pTask); break; } } #else switch (pTask->iTask) { case TASK_RANGE_ATTACK1: if (m_hEnemy != NULL && (m_hEnemy->IsPlayer())) { pev->framerate = 1.5; } CTalkMonster::RunTask(pTask); break; default: CTalkMonster::RunTask(pTask); break; } #endif } void CFGrunt::KeyValue(KeyValueData *pkvd) { if (FStrEq(pkvd->szKeyName, "head")) { head = atoi(pkvd->szValue); pkvd->fHandled = TRUE; } else CTalkMonster::KeyValue(pkvd); } //========================================================= // SetActivity //========================================================= void CFGrunt::SetActivity(Activity NewActivity) { int iSequence = ACTIVITY_NOT_AVAILABLE; void *pmodel = GET_MODEL_PTR(ENT(pev)); #ifdef DEBUG ALERT(at_console, "ALERT: incoming activity: %s\n", UTIL_GetActivityNameBySequence(LookupActivity(NewActivity))); #endif switch (NewActivity) { case ACT_RANGE_ATTACK1: // grunt is either shooting standing or shooting crouched if (FBitSet(pev->weapons, FGRUNT_9MMAR)) { if (m_fStanding) { // get aimable sequence iSequence = LookupSequence("standing_mp5"); } else { // get crouching shoot iSequence = LookupSequence("crouching_mp5"); } } else if (FBitSet(pev->weapons, FGRUNT_SAW)) { if (m_fStanding) { // get aimable sequence iSequence = LookupSequence("standing_saw"); } else { // get crouching shoot iSequence = LookupSequence("crouching_saw"); } } else { if (m_fStanding) { // get aimable sequence iSequence = LookupSequence("standing_shotgun"); } else { // get crouching shoot iSequence = LookupSequence("crouching_shotgun"); } } 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 (pev->weapons & FGRUNT_HANDGRENADE) { // get toss anim iSequence = LookupSequence("throwgrenade"); } else { // get launch anim iSequence = LookupSequence("launchgrenade"); } break; case ACT_RUN: if (pev->health <= FGRUNT_LIMP_HEALTH) { // limp! iSequence = LookupActivity(ACT_RUN_HURT); } else { iSequence = LookupActivity(NewActivity); } break; case ACT_WALK: if (pev->health <= FGRUNT_LIMP_HEALTH) { // limp! iSequence = LookupActivity(ACT_WALK_HURT); } else { iSequence = LookupActivity(NewActivity); } break; case ACT_IDLE: if (m_MonsterState == MONSTERSTATE_COMBAT) { NewActivity = ACT_IDLE_ANGRY; } iSequence = LookupActivity(NewActivity); break; #if 1 case ACT_FLINCH_LEFTLEG: case ACT_FLINCH_RIGHTLEG: case ACT_FLINCH_LEFTARM: case ACT_FLINCH_RIGHTARM: { NewActivity = ACT_SMALL_FLINCH; iSequence = LookupActivity(NewActivity); } break; #endif default: iSequence = LookupActivity(NewActivity); break; } m_Activity = NewActivity; // Go ahead and set this so it doesn't keep trying when the anim is not present // 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_console, "%s has no sequence for act:%d\n", STRING(pev->classname), NewActivity); pev->sequence = 0; // Set to the reset anim (if it's there) } #ifdef DEBUG UTIL_PrintActivity(this); #endif } //========================================================= // CHGruntRepel - when triggered, spawns a monster_human_grunt // repelling down a line. //========================================================= class CFGruntRepel : public CBaseMonster { public: virtual void Spawn(void); virtual void Precache(void); virtual void EXPORT RepelUse(CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value); int m_iSpriteTexture; // Don't save, precache }; //========================================================= // CHGruntRepel - when triggered, spawns a monster_human_grunt // repelling down a line. //========================================================= LINK_ENTITY_TO_CLASS(monster_grunt_ally_repel, CFGruntRepel); void CFGruntRepel::Spawn(void) { Precache(); pev->solid = SOLID_NOT; SetUse(&CFGruntRepel::RepelUse); } void CFGruntRepel::Precache(void) { UTIL_PrecacheOther("monster_human_grunt_ally"); m_iSpriteTexture = PRECACHE_MODEL("sprites/rope.spr"); } void CFGruntRepel::RepelUse(CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value) { TraceResult tr; UTIL_TraceLine(pev->origin, pev->origin + Vector(0, 0, -4096.0), dont_ignore_monsters, ENT(pev), &tr); /* if ( tr.pHit && Instance( tr.pHit )->pev->solid != SOLID_BSP) return NULL; */ CBaseEntity *pEntity = Create("monster_human_grunt_ally", pev->origin, pev->angles); CBaseMonster *pGrunt = pEntity->MyMonsterPointer(); pGrunt->pev->movetype = MOVETYPE_FLY; pGrunt->pev->velocity = Vector(0, 0, RANDOM_FLOAT(-196, -128)); pGrunt->SetActivity(ACT_GLIDE); // UNDONE: position? pGrunt->m_vecLastPosition = tr.vecEndPos; CBeam *pBeam = CBeam::BeamCreate("sprites/rope.spr", 10); pBeam->PointEntInit(pev->origin + Vector(0, 0, 112), pGrunt->entindex()); pBeam->SetFlags(BEAM_FSOLID); pBeam->SetColor(255, 255, 255); pBeam->SetThink(&CBeam::SUB_Remove); pBeam->pev->nextthink = gpGlobals->time + -4096.0 * tr.flFraction / pGrunt->pev->velocity.z + 0.5; UTIL_Remove(this); } //========================================================= // DEAD FGRUNT PROP //========================================================= class CDeadFGrunt : public CBaseMonster { public: void Spawn(void); int Classify(void) { return CLASS_HUMAN_MILITARY; } void KeyValue(KeyValueData *pkvd); virtual int Save(CSave &save); virtual int Restore(CRestore &restore); static TYPEDESCRIPTION m_SaveData[]; int m_iPose;// which sequence to display -- temporary, don't need to save int m_iHead; static const char *m_szPoses[6]; }; LINK_ENTITY_TO_CLASS(monster_human_grunt_ally_dead, CDeadFGrunt); TYPEDESCRIPTION CDeadFGrunt::m_SaveData[] = { DEFINE_FIELD(CDeadFGrunt, m_iHead, FIELD_INTEGER), }; IMPLEMENT_SAVERESTORE(CDeadFGrunt, CBaseMonster); const char *CDeadFGrunt::m_szPoses[] = { "deadstomach", "deadside", "deadsitting", "dead_on_back", "dead_on_stomach", "dead_headcrabed" }; void CDeadFGrunt::KeyValue(KeyValueData *pkvd) { if (FStrEq(pkvd->szKeyName, "pose")) { m_iPose = atoi(pkvd->szValue); pkvd->fHandled = TRUE; } else if (FStrEq(pkvd->szKeyName, "head")) { m_iHead = atoi(pkvd->szValue); pkvd->fHandled = TRUE; } else CBaseMonster::KeyValue(pkvd); } //========================================================= // ********** DeadHGrunt SPAWN ********** //========================================================= void CDeadFGrunt::Spawn(void) { PRECACHE_MODEL("models/hgrunt_opfor.mdl"); SET_MODEL(ENT(pev), "models/hgrunt_opfor.mdl"); pev->effects = 0; pev->yaw_speed = 8; pev->sequence = 0; m_bloodColor = BLOOD_COLOR_RED; pev->sequence = LookupSequence(m_szPoses[m_iPose]); if (pev->sequence == -1) { ALERT(at_console, "Dead fgrunt with bad pose\n"); } // Corpses have less health pev->health = 8; // Select a random head. if (m_iHead == -1) { SetBodygroup(HEAD_GROUP, RANDOM_LONG(0, FGRUNT_NUM_HEADS - 1)); } if (pev->weapons == 0) { SetBodygroup(HEAD_GROUP, HEAD_GASMASK); SetBodygroup(TORSO_GROUP, TORSO_BACKPACK); SetBodygroup(GUN_GROUP, GUN_MP5); } else if (FBitSet(pev->weapons, FGRUNT_9MMAR)) { SetBodygroup(TORSO_GROUP, TORSO_BACKPACK); SetBodygroup(GUN_GROUP, GUN_MP5); } #if 0 // map old bodies onto new bodies switch (pev->body) { case 0: // Grunt with Gun pev->body = 0; pev->skin = 0; SetBodygroup(HEAD_GROUP, HEAD_GRUNT); SetBodygroup(GUN_GROUP, GUN_MP5); break; case 1: // Commander with Gun pev->body = 0; pev->skin = 0; SetBodygroup(HEAD_GROUP, HEAD_COMMANDER); SetBodygroup(GUN_GROUP, GUN_MP5); break; case 2: // Grunt no Gun pev->body = 0; pev->skin = 0; SetBodygroup(HEAD_GROUP, HEAD_GRUNT); SetBodygroup(GUN_GROUP, GUN_NONE); break; case 3: // Commander no Gun pev->body = 0; pev->skin = 0; SetBodygroup(HEAD_GROUP, HEAD_COMMANDER); SetBodygroup(GUN_GROUP, GUN_NONE); break; } #else // Until we get head support, set these to default. pev->body = 0; pev->skin = 0; #endif MonsterInitDead(); }