/*** * * 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. * ****/ //========================================================= // monster template //========================================================= // UNDONE: Holster weapon? #include "extdll.h" #include "util.h" #include "cmbase.h" #include "cmbasemonster.h" #include "monsters.h" #include "schedule.h" #include "defaultai.h" #include "weapons.h" //========================================================= // Monster's Anim Events Go Here //========================================================= // first flag is barney dying for scripted sequences? #define BARNEY_AE_DRAW ( 2 ) #define BARNEY_AE_SHOOT ( 3 ) #define BARNEY_AE_HOLSTER ( 4 ) #define BARNEY_BODY_GUNHOLSTERED 0 #define BARNEY_BODY_GUNDRAWN 1 #define BARNEY_BODY_GUNGONE 2 //========================================================= // AI Schedules Specific to this monster //========================================================= Task_t tlBaFollow[] = { { TASK_MOVE_TO_TARGET_RANGE,(float)128 }, // Move within 128 of target ent (client) { TASK_SET_SCHEDULE, (float)SCHED_TARGET_FACE }, }; Schedule_t slBaFollow[] = { { tlBaFollow, ARRAYSIZE ( tlBaFollow ), bits_COND_NEW_ENEMY | bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE | bits_COND_HEAR_SOUND | bits_COND_PROVOKED, 0, "Follow" }, }; //========================================================= // BarneyDraw- much better looking draw schedule for when // barney knows who he's gonna attack. //========================================================= Task_t tlBarneyEnemyDraw[] = { { TASK_STOP_MOVING, 0 }, { TASK_FACE_ENEMY, 0 }, { TASK_PLAY_SEQUENCE_FACE_ENEMY, (float) ACT_ARM }, }; Schedule_t slBarneyEnemyDraw[] = { { tlBarneyEnemyDraw, ARRAYSIZE ( tlBarneyEnemyDraw ), 0, 0, "Barney Enemy Draw" } }; Task_t tlBaFaceTarget[] = { { 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 slBaFaceTarget[] = { { tlBaFaceTarget, ARRAYSIZE ( tlBaFaceTarget ), bits_COND_CLIENT_PUSH | bits_COND_NEW_ENEMY | bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE | bits_COND_HEAR_SOUND | bits_COND_PROVOKED, 0, "FaceTarget" }, }; Task_t tlIdleBaStand[] = { { 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 slIdleBaStand[] = { { tlIdleBaStand, ARRAYSIZE ( tlIdleBaStand ), bits_COND_NEW_ENEMY | bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE | bits_COND_HEAR_SOUND | bits_COND_SMELL | bits_COND_PROVOKED, 0, "IdleStand" }, }; DEFINE_CUSTOM_SCHEDULES( CMBarney ) { slBaFollow, slBarneyEnemyDraw, slBaFaceTarget, slIdleBaStand, }; IMPLEMENT_CUSTOM_SCHEDULES( CMBarney, CMTalkMonster ); void CMBarney :: StartTask( Task_t *pTask ) { CMTalkMonster::StartTask( pTask ); } void CMBarney :: RunTask( Task_t *pTask ) { switch ( pTask->iTask ) { case TASK_RANGE_ATTACK1: if (m_hEnemy != NULL && (UTIL_IsPlayer(m_hEnemy))) { pev->framerate = 1.5; } CMTalkMonster::RunTask( pTask ); break; default: CMTalkMonster::RunTask( pTask ); break; } } //========================================================= // ISoundMask - returns a bit mask indicating which types // of sounds this monster regards. //========================================================= int CMBarney :: ISoundMask ( void) { return 0; } //========================================================= // Classify - indicates this monster's place in the // relationship table. //========================================================= int CMBarney :: Classify ( void ) { if ( m_iClassifyOverride == -1 ) // helper return CLASS_NONE; else if ( m_iClassifyOverride > 0 ) return m_iClassifyOverride; // override return CLASS_PLAYER_ALLY; } //========================================================= // ALertSound - barney says "Freeze!" //========================================================= void CMBarney :: AlertSound( void ) { if ( m_hEnemy != NULL ) { if ( FOkToSpeak() ) { PlaySentence( "BA_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 CMBarney :: 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; } //========================================================= // CheckRangeAttack1 //========================================================= BOOL CMBarney :: 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 ); edict_t *pEnemy = m_hEnemy; Vector shootTarget = ( (UTIL_BodyTarget( pEnemy, shootOrigin ) - pEnemy->v.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) && (tr.pHit == pEnemy) ) m_lastAttackCheck = TRUE; else m_lastAttackCheck = FALSE; m_checkAttackTime = gpGlobals->time + 1.5; } return m_lastAttackCheck; } return FALSE; } //========================================================= // BarneyFirePistol - shoots one round from the pistol at // the enemy barney is facing. //========================================================= void CMBarney :: BarneyFirePistol ( void ) { Vector vecShootOrigin; UTIL_MakeVectors(pev->angles); vecShootOrigin = pev->origin + Vector( 0, 0, 55 ); Vector vecShootDir = ShootAtEnemy( vecShootOrigin ); Vector angDir = UTIL_VecToAngles( vecShootDir ); SetBlending( 0, angDir.x ); pev->effects = EF_MUZZLEFLASH; FireBullets(1, vecShootOrigin, vecShootDir, VECTOR_CONE_2DEGREES, 1024, BULLET_MONSTER_9MM ); int pitchShift = RANDOM_LONG( 0, 20 ); // Only shift about half the time if ( pitchShift > 10 ) pitchShift = 0; else pitchShift -= 5; EMIT_SOUND_DYN( ENT(pev), CHAN_WEAPON, "barney/ba_attack2.wav", 1, ATTN_NORM, 0, 100 + pitchShift ); // UNDONE: Reload? 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 CMBarney :: HandleAnimEvent( MonsterEvent_t *pEvent ) { switch( pEvent->event ) { case BARNEY_AE_SHOOT: BarneyFirePistol(); break; case BARNEY_AE_DRAW: // barney's bodygroup switches here so he can pull gun from holster pev->body = BARNEY_BODY_GUNDRAWN; m_fGunDrawn = TRUE; break; case BARNEY_AE_HOLSTER: // change bodygroup to replace gun in holster pev->body = BARNEY_BODY_GUNHOLSTERED; m_fGunDrawn = FALSE; break; default: CMTalkMonster::HandleAnimEvent( pEvent ); } } //========================================================= // Spawn //========================================================= void CMBarney :: Spawn() { Precache( ); // every new barney must call this, otherwise // when a level is loaded, nobody will talk (time is reset to 0) TalkInit(); SET_MODEL(ENT(pev), "models/barney.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.barneyHealth; pev->view_ofs = Vector ( 0, 0, 50 );// position of the eyes relative to monster's origin. m_flFieldOfView = VIEW_FIELD_FULL; m_MonsterState = MONSTERSTATE_NONE; pev->body = 0; // gun in holster m_fGunDrawn = FALSE; m_afCapability = bits_CAP_HEAR | bits_CAP_TURN_HEAD | bits_CAP_DOORS_GROUP; MonsterInit(); pev->classname = MAKE_STRING( "monster_barney" ); if ( strlen( STRING( m_szMonsterName ) ) == 0 ) { // default name m_szMonsterName = MAKE_STRING( "Barney" ); } } //========================================================= // Precache - precaches all resources this monster needs //========================================================= void CMBarney :: Precache() { PRECACHE_MODEL("models/barney.mdl"); PRECACHE_SOUND("barney/ba_attack1.wav" ); PRECACHE_SOUND("barney/ba_attack2.wav" ); PRECACHE_SOUND("barney/ba_pain1.wav"); PRECACHE_SOUND("barney/ba_pain2.wav"); PRECACHE_SOUND("barney/ba_pain3.wav"); PRECACHE_SOUND("barney/ba_die1.wav"); PRECACHE_SOUND("barney/ba_die2.wav"); PRECACHE_SOUND("barney/ba_die3.wav"); CMTalkMonster::Precache(); } // Init talk data void CMBarney :: TalkInit() { CMTalkMonster::TalkInit(); // scientists speach group names (group names are in sentences.txt) m_szGrp[TLK_ANSWER] = "BA_ANSWER"; m_szGrp[TLK_QUESTION] = "BA_QUESTION"; m_szGrp[TLK_IDLE] = "BA_IDLE"; m_szGrp[TLK_STARE] = "BA_STARE"; m_szGrp[TLK_USE] = "BA_OK"; m_szGrp[TLK_UNUSE] = "BA_WAIT"; m_szGrp[TLK_STOP] = "BA_STOP"; m_szGrp[TLK_NOSHOOT] = "BA_SCARED"; m_szGrp[TLK_HELLO] = "BA_HELLO"; m_szGrp[TLK_PLHURT1] = "!BA_CUREA"; m_szGrp[TLK_PLHURT2] = "!BA_CUREB"; m_szGrp[TLK_PLHURT3] = "!BA_CUREC"; m_szGrp[TLK_PHELLO] = NULL; //"BA_PHELLO"; // UNDONE m_szGrp[TLK_PIDLE] = NULL; //"BA_PIDLE"; // UNDONE m_szGrp[TLK_PQUESTION] = "BA_PQUEST"; // UNDONE m_szGrp[TLK_SMELL] = "BA_SMELL"; m_szGrp[TLK_WOUND] = "BA_WOUND"; m_szGrp[TLK_MORTAL] = "BA_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 CMBarney :: TakeDamage( entvars_t* pevInflictor, entvars_t* pevAttacker, float flDamage, int bitsDamageType) { // make sure friends talk about it if player hurts talkmonsters... int ret = CMTalkMonster::TakeDamage(pevInflictor, pevAttacker, flDamage, bitsDamageType); if ( !IsAlive() || pev->deadflag == DEAD_DYING ) return ret; if ( m_MonsterState != MONSTERSTATE_PRONE && (pevAttacker->flags & FL_CLIENT) ) { // 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 != NULL ) && UTIL_IsPlayer(m_hEnemy) ) { Remember( bits_MEMORY_PROVOKED ); } } return ret; } //========================================================= // PainSound //========================================================= void CMBarney :: PainSound ( void ) { if (gpGlobals->time < m_painTime) return; m_painTime = gpGlobals->time + RANDOM_FLOAT(0.5, 0.75); switch (RANDOM_LONG(0,2)) { case 0: EMIT_SOUND_DYN( ENT(pev), CHAN_VOICE, "barney/ba_pain1.wav", 1, ATTN_NORM, 0, GetVoicePitch()); break; case 1: EMIT_SOUND_DYN( ENT(pev), CHAN_VOICE, "barney/ba_pain2.wav", 1, ATTN_NORM, 0, GetVoicePitch()); break; case 2: EMIT_SOUND_DYN( ENT(pev), CHAN_VOICE, "barney/ba_pain3.wav", 1, ATTN_NORM, 0, GetVoicePitch()); break; } } //========================================================= // DeathSound //========================================================= void CMBarney :: DeathSound ( void ) { switch (RANDOM_LONG(0,2)) { case 0: EMIT_SOUND_DYN( ENT(pev), CHAN_VOICE, "barney/ba_die1.wav", 1, ATTN_NORM, 0, GetVoicePitch()); break; case 1: EMIT_SOUND_DYN( ENT(pev), CHAN_VOICE, "barney/ba_die2.wav", 1, ATTN_NORM, 0, GetVoicePitch()); break; case 2: EMIT_SOUND_DYN( ENT(pev), CHAN_VOICE, "barney/ba_die3.wav", 1, ATTN_NORM, 0, GetVoicePitch()); break; } } void CMBarney::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; } CMTalkMonster::TraceAttack( pevAttacker, flDamage, vecDir, ptr, bitsDamageType ); } void CMBarney::Killed( entvars_t *pevAttacker, int iGib ) { if ( pev->body < BARNEY_BODY_GUNGONE ) {// drop the gun! Vector vecGunPos; Vector vecGunAngles; pev->body = BARNEY_BODY_GUNGONE; GetAttachment( 0, vecGunPos, vecGunAngles ); } SetUse( NULL ); CMTalkMonster::Killed( pevAttacker, iGib ); } //========================================================= // AI Schedules Specific to this monster //========================================================= Schedule_t* CMBarney :: GetScheduleOfType ( int Type ) { Schedule_t *psched; switch( Type ) { case SCHED_ARM_WEAPON: if ( m_hEnemy != NULL ) { // 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 = CMTalkMonster::GetScheduleOfType(Type); if (psched == slIdleStand) return slBaFaceTarget; // override this for 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 = CMTalkMonster::GetScheduleOfType(Type); if (psched == slIdleStand) { // just look straight ahead. return slIdleBaStand; } else return psched; } return CMTalkMonster::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. //========================================================= Schedule_t *CMBarney :: GetSchedule ( void ) { if ( HasConditions( bits_COND_ENEMY_DEAD ) && FOkToSpeak() ) { PlaySentence( "BA_KILL", 4, VOL_NORM, ATTN_NORM ); } 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 CMBaseMonster :: GetSchedule(); } // always act surprized with a new enemy if ( HasConditions( bits_COND_NEW_ENEMY ) && HasConditions( bits_COND_LIGHT_DAMAGE) ) return GetScheduleOfType( SCHED_SMALL_FLINCH ); // wait for one schedule to draw gun if (!m_fGunDrawn ) return GetScheduleOfType( SCHED_ARM_WEAPON ); if ( HasConditions( bits_COND_HEAVY_DAMAGE ) ) return GetScheduleOfType( SCHED_TAKE_COVER_FROM_ENEMY ); } break; 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 == NULL && IsFollowing() ) { if ( !UTIL_IsAlive(m_hTargetEnt) ) { // 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 CMTalkMonster::GetSchedule(); } MONSTERSTATE CMBarney :: GetIdealState ( void ) { return CMTalkMonster::GetIdealState(); }