/*** * * 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. * ****/ //========================================================= // human scientist (passive lab worker) //========================================================= #include "extdll.h" #include "util.h" #include "cbase.h" #include "monsters.h" #include "talkmonster.h" #include "schedule.h" #include "defaultai.h" #include "scripted.h" #include "animation.h" #include "soundent.h" #define NUM_SCIENTIST_HEADS 4 // four heads available for scientist model enum { HEAD_GLASSES = 0, HEAD_EINSTEIN = 1, HEAD_LUTHER = 2, HEAD_SLICK = 3 }; enum { SCHED_HIDE = LAST_TALKMONSTER_SCHEDULE + 1, SCHED_FEAR, SCHED_PANIC, SCHED_STARTLE, SCHED_TARGET_CHASE_SCARED, SCHED_TARGET_FACE_SCARED, }; enum { TASK_SAY_HEAL = LAST_TALKMONSTER_TASK + 1, TASK_HEAL, TASK_SAY_FEAR, TASK_RUN_PATH_SCARED, TASK_SCREAM, TASK_RANDOM_SCREAM, TASK_MOVE_TO_TARGET_RANGE_SCARED, }; //========================================================= // Monster's Anim Events Go Here //========================================================= #define SCIENTIST_AE_HEAL ( 1 ) #define SCIENTIST_AE_NEEDLEON ( 2 ) #define SCIENTIST_AE_NEEDLEOFF ( 3 ) //======================================================= // Scientist //======================================================= class CScientist : public CTalkMonster { public: void Spawn( void ); void Precache( void ); void SetYawSpeed( void ); int Classify ( void ); void HandleAnimEvent( MonsterEvent_t *pEvent ); void RunTask( Task_t *pTask ); void StartTask( Task_t *pTask ); int ObjectCaps( void ) { return CTalkMonster :: ObjectCaps() | FCAP_IMPULSE_USE; } int TakeDamage( entvars_t* pevInflictor, entvars_t* pevAttacker, float flDamage, int bitsDamageType); virtual int FriendNumber( int arrayNumber ); void SetActivity ( Activity newActivity ); Activity GetStoppedActivity( void ); int ISoundMask( void ); void DeclineFollowing( void ); float CoverRadius( void ) { return 1200; } // Need more room for cover because scientists want to get far away! BOOL DisregardEnemy( CBaseEntity *pEnemy ) { return !pEnemy->IsAlive() || (gpGlobals->time - m_fearTime) > 15; } BOOL CanHeal( void ); void Heal( void ); void Scream( void ); // Override these to set behavior Schedule_t *GetScheduleOfType ( int Type ); Schedule_t *GetSchedule ( void ); MONSTERSTATE GetIdealState ( void ); void DeathSound( void ); void PainSound( void ); void TalkInit( void ); void Killed( entvars_t *pevAttacker, int iGib ); virtual int Save( CSave &save ); virtual int Restore( CRestore &restore ); static TYPEDESCRIPTION m_SaveData[]; CUSTOM_SCHEDULES; private: float m_painTime; float m_healTime; float m_fearTime; }; LINK_ENTITY_TO_CLASS( monster_scientist, CScientist ); TYPEDESCRIPTION CScientist::m_SaveData[] = { DEFINE_FIELD( CScientist, m_painTime, FIELD_TIME ), DEFINE_FIELD( CScientist, m_healTime, FIELD_TIME ), DEFINE_FIELD( CScientist, m_fearTime, FIELD_TIME ), }; IMPLEMENT_SAVERESTORE( CScientist, CTalkMonster ); //========================================================= // AI Schedules Specific to this monster //========================================================= Task_t tlFollow[] = { { TASK_SET_FAIL_SCHEDULE, (float)SCHED_CANT_FOLLOW }, // If you fail, bail out of follow { TASK_MOVE_TO_TARGET_RANGE,(float)128 }, // Move within 128 of target ent (client) // { TASK_SET_SCHEDULE, (float)SCHED_TARGET_FACE }, }; Schedule_t slFollow[] = { { tlFollow, ARRAYSIZE ( tlFollow ), bits_COND_NEW_ENEMY | bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE | bits_COND_HEAR_SOUND, bits_SOUND_COMBAT | bits_SOUND_DANGER, "Follow" }, }; Task_t tlFollowScared[] = { { TASK_SET_FAIL_SCHEDULE, (float)SCHED_TARGET_CHASE },// If you fail, follow normally { TASK_MOVE_TO_TARGET_RANGE_SCARED,(float)128 }, // Move within 128 of target ent (client) // { TASK_SET_SCHEDULE, (float)SCHED_TARGET_FACE_SCARED }, }; Schedule_t slFollowScared[] = { { tlFollowScared, ARRAYSIZE ( tlFollowScared ), bits_COND_NEW_ENEMY | bits_COND_HEAR_SOUND | bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE, bits_SOUND_DANGER, "FollowScared" }, }; Task_t tlFaceTargetScared[] = { { TASK_FACE_TARGET, (float)0 }, { TASK_SET_ACTIVITY, (float)ACT_CROUCHIDLE }, { TASK_SET_SCHEDULE, (float)SCHED_TARGET_CHASE_SCARED }, }; Schedule_t slFaceTargetScared[] = { { tlFaceTargetScared, ARRAYSIZE ( tlFaceTargetScared ), bits_COND_HEAR_SOUND | bits_COND_NEW_ENEMY, bits_SOUND_DANGER, "FaceTargetScared" }, }; Task_t tlStopFollowing[] = { { TASK_CANT_FOLLOW, (float)0 }, }; Schedule_t slStopFollowing[] = { { tlStopFollowing, ARRAYSIZE ( tlStopFollowing ), 0, 0, "StopFollowing" }, }; Task_t tlHeal[] = { { TASK_MOVE_TO_TARGET_RANGE,(float)50 }, // Move within 60 of target ent (client) { TASK_SET_FAIL_SCHEDULE, (float)SCHED_TARGET_CHASE }, // If you fail, catch up with that guy! (change this to put syringe away and then chase) { TASK_FACE_IDEAL, (float)0 }, { TASK_SAY_HEAL, (float)0 }, { TASK_PLAY_SEQUENCE_FACE_TARGET, (float)ACT_ARM }, // Whip out the needle { TASK_HEAL, (float)0 }, // Put it in the player { TASK_PLAY_SEQUENCE_FACE_TARGET, (float)ACT_DISARM }, // Put away the needle }; Schedule_t slHeal[] = { { tlHeal, ARRAYSIZE ( tlHeal ), 0, // Don't interrupt or he'll end up running around with a needle all the time 0, "Heal" }, }; Task_t tlFaceTarget[] = { { TASK_STOP_MOVING, (float)0 }, { TASK_FACE_TARGET, (float)0 }, { TASK_SET_ACTIVITY, (float)ACT_IDLE }, { TASK_SET_SCHEDULE, (float)SCHED_TARGET_CHASE }, }; Schedule_t slFaceTarget[] = { { tlFaceTarget, ARRAYSIZE ( tlFaceTarget ), bits_COND_CLIENT_PUSH | bits_COND_NEW_ENEMY | bits_COND_HEAR_SOUND, bits_SOUND_COMBAT | bits_SOUND_DANGER, "FaceTarget" }, }; Task_t tlSciPanic[] = { { TASK_STOP_MOVING, (float)0 }, { TASK_FACE_ENEMY, (float)0 }, { TASK_SCREAM, (float)0 }, { TASK_PLAY_SEQUENCE_FACE_ENEMY, (float)ACT_EXCITED }, // This is really fear-stricken excitement { TASK_SET_ACTIVITY, (float)ACT_IDLE }, }; Schedule_t slSciPanic[] = { { tlSciPanic, ARRAYSIZE ( tlSciPanic ), 0, 0, "SciPanic" }, }; Task_t tlIdleSciStand[] = { { 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 slIdleSciStand[] = { { tlIdleSciStand, ARRAYSIZE ( tlIdleSciStand ), bits_COND_NEW_ENEMY | bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE | bits_COND_HEAR_SOUND | bits_COND_SMELL | bits_COND_CLIENT_PUSH | bits_COND_PROVOKED, bits_SOUND_COMBAT |// sound flags //bits_SOUND_PLAYER | //bits_SOUND_WORLD | bits_SOUND_DANGER | bits_SOUND_MEAT |// scents bits_SOUND_CARCASS | bits_SOUND_GARBAGE, "IdleSciStand" }, }; Task_t tlScientistCover[] = { { TASK_SET_FAIL_SCHEDULE, (float)SCHED_PANIC }, // If you fail, just panic! { TASK_STOP_MOVING, (float)0 }, { TASK_FIND_COVER_FROM_ENEMY, (float)0 }, { TASK_RUN_PATH_SCARED, (float)0 }, { TASK_TURN_LEFT, (float)179 }, { TASK_SET_SCHEDULE, (float)SCHED_HIDE }, }; Schedule_t slScientistCover[] = { { tlScientistCover, ARRAYSIZE ( tlScientistCover ), bits_COND_NEW_ENEMY, 0, "ScientistCover" }, }; Task_t tlScientistHide[] = { { TASK_SET_FAIL_SCHEDULE, (float)SCHED_PANIC }, // If you fail, just panic! { TASK_STOP_MOVING, (float)0 }, { TASK_PLAY_SEQUENCE, (float)ACT_CROUCH }, { TASK_SET_ACTIVITY, (float)ACT_CROUCHIDLE }, // FIXME: This looks lame { TASK_WAIT_RANDOM, (float)10.0 }, }; Schedule_t slScientistHide[] = { { tlScientistHide, ARRAYSIZE ( tlScientistHide ), bits_COND_NEW_ENEMY | bits_COND_HEAR_SOUND | bits_COND_SEE_ENEMY | bits_COND_SEE_HATE | bits_COND_SEE_FEAR | bits_COND_SEE_DISLIKE, bits_SOUND_DANGER, "ScientistHide" }, }; Task_t tlScientistStartle[] = { { TASK_SET_FAIL_SCHEDULE, (float)SCHED_PANIC }, // If you fail, just panic! { TASK_RANDOM_SCREAM, (float)0.3 }, // Scream 30% of the time { TASK_STOP_MOVING, (float)0 }, { TASK_PLAY_SEQUENCE_FACE_ENEMY, (float)ACT_CROUCH }, { TASK_RANDOM_SCREAM, (float)0.1 }, // Scream again 10% of the time { TASK_PLAY_SEQUENCE_FACE_ENEMY, (float)ACT_CROUCHIDLE }, { TASK_WAIT_RANDOM, (float)1.0 }, }; Schedule_t slScientistStartle[] = { { tlScientistStartle, ARRAYSIZE ( tlScientistStartle ), bits_COND_NEW_ENEMY | bits_COND_SEE_ENEMY | bits_COND_SEE_HATE | bits_COND_SEE_FEAR | bits_COND_SEE_DISLIKE, 0, "ScientistStartle" }, }; Task_t tlFear[] = { { TASK_STOP_MOVING, (float)0 }, { TASK_FACE_ENEMY, (float)0 }, { TASK_SAY_FEAR, (float)0 }, // { TASK_PLAY_SEQUENCE, (float)ACT_FEAR_DISPLAY }, }; Schedule_t slFear[] = { { tlFear, ARRAYSIZE ( tlFear ), bits_COND_NEW_ENEMY, 0, "Fear" }, }; DEFINE_CUSTOM_SCHEDULES( CScientist ) { slFollow, slFaceTarget, slIdleSciStand, slFear, slScientistCover, slScientistHide, slScientistStartle, slHeal, slStopFollowing, slSciPanic, slFollowScared, slFaceTargetScared, }; IMPLEMENT_CUSTOM_SCHEDULES( CScientist, CTalkMonster ); void CScientist::DeclineFollowing( void ) { Talk( 10 ); m_hTalkTarget = m_hEnemy; PlaySentence( "SC_POK", 2, VOL_NORM, ATTN_NORM ); } void CScientist :: Scream( void ) { if ( FOkToSpeak() ) { Talk( 10 ); m_hTalkTarget = m_hEnemy; PlaySentence( "SC_SCREAM", RANDOM_FLOAT(3, 6), VOL_NORM, ATTN_NORM ); } } Activity CScientist::GetStoppedActivity( void ) { if ( m_hEnemy != NULL ) return ACT_EXCITED; return CTalkMonster::GetStoppedActivity(); } void CScientist :: StartTask( Task_t *pTask ) { switch( pTask->iTask ) { case TASK_SAY_HEAL: // if ( FOkToSpeak() ) Talk( 2 ); m_hTalkTarget = m_hTargetEnt; PlaySentence( "SC_HEAL", 2, VOL_NORM, ATTN_IDLE ); TaskComplete(); break; case TASK_SCREAM: Scream(); TaskComplete(); break; case TASK_RANDOM_SCREAM: if ( RANDOM_FLOAT( 0, 1 ) < pTask->flData ) Scream(); TaskComplete(); break; case TASK_SAY_FEAR: if ( FOkToSpeak() ) { Talk( 2 ); m_hTalkTarget = m_hEnemy; if ( m_hEnemy->IsPlayer() ) PlaySentence( "SC_PLFEAR", 5, VOL_NORM, ATTN_NORM ); else PlaySentence( "SC_FEAR", 5, VOL_NORM, ATTN_NORM ); } TaskComplete(); break; case TASK_HEAL: m_IdealActivity = ACT_MELEE_ATTACK1; break; case TASK_RUN_PATH_SCARED: m_movementActivity = ACT_RUN_SCARED; break; case TASK_MOVE_TO_TARGET_RANGE_SCARED: { if ( (m_hTargetEnt->pev->origin - pev->origin).Length() < 1 ) TaskComplete(); else { m_vecMoveGoal = m_hTargetEnt->pev->origin; if ( !MoveToTarget( ACT_WALK_SCARED, 0.5 ) ) TaskFail(); } } break; default: CTalkMonster::StartTask( pTask ); break; } } void CScientist :: RunTask( Task_t *pTask ) { switch ( pTask->iTask ) { case TASK_RUN_PATH_SCARED: if ( MovementIsComplete() ) TaskComplete(); if ( RANDOM_LONG(0,31) < 8 ) Scream(); break; case TASK_MOVE_TO_TARGET_RANGE_SCARED: { if ( RANDOM_LONG(0,63)< 8 ) Scream(); if ( m_hEnemy == NULL ) { TaskFail(); } else { float distance; distance = ( m_vecMoveGoal - pev->origin ).Length2D(); // Re-evaluate when you think your finished, or the target has moved too far if ( (distance < pTask->flData) || (m_vecMoveGoal - m_hTargetEnt->pev->origin).Length() > pTask->flData * 0.5 ) { m_vecMoveGoal = m_hTargetEnt->pev->origin; distance = ( m_vecMoveGoal - pev->origin ).Length2D(); FRefreshRoute(); } // Set the appropriate activity based on an overlapping range // overlap the range to prevent oscillation if ( distance < pTask->flData ) { TaskComplete(); RouteClear(); // Stop moving } else if ( distance < 190 && m_movementActivity != ACT_WALK_SCARED ) m_movementActivity = ACT_WALK_SCARED; else if ( distance >= 270 && m_movementActivity != ACT_RUN_SCARED ) m_movementActivity = ACT_RUN_SCARED; } } break; case TASK_HEAL: if ( m_fSequenceFinished ) { TaskComplete(); } else { if ( TargetDistance() > 90 ) TaskComplete(); pev->ideal_yaw = UTIL_VecToYaw( m_hTargetEnt->pev->origin - pev->origin ); ChangeYaw( pev->yaw_speed ); } break; default: CTalkMonster::RunTask( pTask ); break; } } //========================================================= // Classify - indicates this monster's place in the // relationship table. //========================================================= int CScientist :: Classify ( void ) { return CLASS_HUMAN_PASSIVE; } //========================================================= // SetYawSpeed - allows each sequence to have a different // turn rate associated with it. //========================================================= void CScientist :: SetYawSpeed ( void ) { int ys; ys = 90; switch ( m_Activity ) { case ACT_IDLE: ys = 120; break; case ACT_WALK: ys = 180; break; case ACT_RUN: ys = 150; break; case ACT_TURN_LEFT: case ACT_TURN_RIGHT: ys = 120; break; } pev->yaw_speed = ys; } //========================================================= // HandleAnimEvent - catches the monster-specific messages // that occur when tagged animation frames are played. //========================================================= void CScientist :: HandleAnimEvent( MonsterEvent_t *pEvent ) { switch( pEvent->event ) { case SCIENTIST_AE_HEAL: // Heal my target (if within range) Heal(); break; case SCIENTIST_AE_NEEDLEON: { int oldBody = pev->body; pev->body = (oldBody % NUM_SCIENTIST_HEADS) + NUM_SCIENTIST_HEADS * 1; } break; case SCIENTIST_AE_NEEDLEOFF: { int oldBody = pev->body; pev->body = (oldBody % NUM_SCIENTIST_HEADS) + NUM_SCIENTIST_HEADS * 0; } break; default: CTalkMonster::HandleAnimEvent( pEvent ); } } //========================================================= // Spawn //========================================================= void CScientist :: Spawn( void ) { Precache( ); SET_MODEL(ENT(pev), "models/scientist.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.scientistHealth; 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 scientists will notice player and say hello m_MonsterState = MONSTERSTATE_NONE; // m_flDistTooFar = 256.0; m_afCapability = bits_CAP_HEAR | bits_CAP_TURN_HEAD | bits_CAP_OPEN_DOORS | bits_CAP_AUTO_DOORS | bits_CAP_USE; // White hands pev->skin = 0; if ( pev->body == -1 ) {// -1 chooses a random head pev->body = RANDOM_LONG(0, NUM_SCIENTIST_HEADS-1);// pick a head, any head } // Luther is black, make his hands black if ( pev->body == HEAD_LUTHER ) pev->skin = 1; MonsterInit(); SetUse( &CScientist::FollowerUse ); } //========================================================= // Precache - precaches all resources this monster needs //========================================================= void CScientist :: Precache( void ) { PRECACHE_MODEL("models/scientist.mdl"); PRECACHE_SOUND("scientist/sci_pain1.wav"); PRECACHE_SOUND("scientist/sci_pain2.wav"); PRECACHE_SOUND("scientist/sci_pain3.wav"); PRECACHE_SOUND("scientist/sci_pain4.wav"); PRECACHE_SOUND("scientist/sci_pain5.wav"); // every new scientist must call this, otherwise // when a level is loaded, nobody will talk (time is reset to 0) TalkInit(); CTalkMonster::Precache(); } // Init talk data void CScientist :: TalkInit() { CTalkMonster::TalkInit(); // scientist will try to talk to friends in this order: m_szFriends[0] = "monster_scientist"; m_szFriends[1] = "monster_sitting_scientist"; m_szFriends[2] = "monster_barney"; // scientists speach group names (group names are in sentences.txt) m_szGrp[TLK_ANSWER] = "SC_ANSWER"; m_szGrp[TLK_QUESTION] = "SC_QUESTION"; m_szGrp[TLK_IDLE] = "SC_IDLE"; m_szGrp[TLK_STARE] = "SC_STARE"; m_szGrp[TLK_USE] = "SC_OK"; m_szGrp[TLK_UNUSE] = "SC_WAIT"; m_szGrp[TLK_STOP] = "SC_STOP"; m_szGrp[TLK_NOSHOOT] = "SC_SCARED"; m_szGrp[TLK_HELLO] = "SC_HELLO"; m_szGrp[TLK_PLHURT1] = "!SC_CUREA"; m_szGrp[TLK_PLHURT2] = "!SC_CUREB"; m_szGrp[TLK_PLHURT3] = "!SC_CUREC"; m_szGrp[TLK_PHELLO] = "SC_PHELLO"; m_szGrp[TLK_PIDLE] = "SC_PIDLE"; m_szGrp[TLK_PQUESTION] = "SC_PQUEST"; m_szGrp[TLK_SMELL] = "SC_SMELL"; m_szGrp[TLK_WOUND] = "SC_WOUND"; m_szGrp[TLK_MORTAL] = "SC_MORTAL"; // get voice for head switch (pev->body % 3) { default: case HEAD_GLASSES: m_voicePitch = 105; break; //glasses case HEAD_EINSTEIN: m_voicePitch = 100; break; //einstein case HEAD_LUTHER: m_voicePitch = 95; break; //luther case HEAD_SLICK: m_voicePitch = 100; break;//slick } } int CScientist :: TakeDamage( entvars_t *pevInflictor, entvars_t *pevAttacker, float flDamage, int bitsDamageType) { if ( pevInflictor && pevInflictor->flags & FL_CLIENT ) { Remember( bits_MEMORY_PROVOKED ); StopFollowing( TRUE ); } // make sure friends talk about it if player hurts scientist... return CTalkMonster::TakeDamage(pevInflictor, pevAttacker, flDamage, bitsDamageType); } //========================================================= // ISoundMask - returns a bit mask indicating which types // of sounds this monster regards. In the base class implementation, // monsters care about all sounds, but no scents. //========================================================= int CScientist :: ISoundMask ( void ) { return bits_SOUND_WORLD | bits_SOUND_COMBAT | bits_SOUND_DANGER | bits_SOUND_PLAYER; } //========================================================= // PainSound //========================================================= void CScientist :: PainSound ( void ) { if (gpGlobals->time < m_painTime ) return; m_painTime = gpGlobals->time + RANDOM_FLOAT(0.5, 0.75); switch (RANDOM_LONG(0,4)) { case 0: EMIT_SOUND_DYN( ENT(pev), CHAN_VOICE, "scientist/sci_pain1.wav", 1, ATTN_NORM, 0, GetVoicePitch()); break; case 1: EMIT_SOUND_DYN( ENT(pev), CHAN_VOICE, "scientist/sci_pain2.wav", 1, ATTN_NORM, 0, GetVoicePitch()); break; case 2: EMIT_SOUND_DYN( ENT(pev), CHAN_VOICE, "scientist/sci_pain3.wav", 1, ATTN_NORM, 0, GetVoicePitch()); break; case 3: EMIT_SOUND_DYN( ENT(pev), CHAN_VOICE, "scientist/sci_pain4.wav", 1, ATTN_NORM, 0, GetVoicePitch()); break; case 4: EMIT_SOUND_DYN( ENT(pev), CHAN_VOICE, "scientist/sci_pain5.wav", 1, ATTN_NORM, 0, GetVoicePitch()); break; } } //========================================================= // DeathSound //========================================================= void CScientist :: DeathSound ( void ) { PainSound(); } void CScientist::Killed( entvars_t *pevAttacker, int iGib ) { SetUse( NULL ); CTalkMonster::Killed( pevAttacker, iGib ); } void CScientist :: SetActivity ( Activity newActivity ) { int iSequence; iSequence = LookupActivity ( newActivity ); // Set to the desired anim, or default anim if the desired is not present if ( iSequence == ACTIVITY_NOT_AVAILABLE ) newActivity = ACT_IDLE; CTalkMonster::SetActivity( newActivity ); } Schedule_t* CScientist :: GetScheduleOfType ( int Type ) { Schedule_t *psched; switch( Type ) { // Hook these to make a looping schedule case SCHED_TARGET_FACE: // call base class default so that scientist will talk // when 'used' psched = CTalkMonster::GetScheduleOfType(Type); if (psched == slIdleStand) return slFaceTarget; // override this for different target face behavior else return psched; case SCHED_TARGET_CHASE: return slFollow; case SCHED_CANT_FOLLOW: return slStopFollowing; case SCHED_PANIC: return slSciPanic; case SCHED_TARGET_CHASE_SCARED: return slFollowScared; case SCHED_TARGET_FACE_SCARED: return slFaceTargetScared; case SCHED_IDLE_STAND: // call base class default so that scientist will talk // when standing during idle psched = CTalkMonster::GetScheduleOfType(Type); if (psched == slIdleStand) return slIdleSciStand; else return psched; case SCHED_HIDE: return slScientistHide; case SCHED_STARTLE: return slScientistStartle; case SCHED_FEAR: return slFear; } return CTalkMonster::GetScheduleOfType( Type ); } Schedule_t *CScientist :: GetSchedule ( void ) { // so we don't keep calling through the EHANDLE stuff CBaseEntity *pEnemy = m_hEnemy; if ( HasConditions( bits_COND_HEAR_SOUND ) ) { CSound *pSound; pSound = PBestSound(); ASSERT( pSound != NULL ); if ( pSound && (pSound->m_iType & bits_SOUND_DANGER) ) return GetScheduleOfType( SCHED_TAKE_COVER_FROM_BEST_SOUND ); } switch( m_MonsterState ) { case MONSTERSTATE_ALERT: case MONSTERSTATE_IDLE: if ( pEnemy ) { if ( HasConditions( bits_COND_SEE_ENEMY ) ) m_fearTime = gpGlobals->time; else if ( DisregardEnemy( pEnemy ) ) // After 15 seconds of being hidden, return to alert { m_hEnemy = NULL; pEnemy = NULL; } } if ( HasConditions(bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE)) { // flinch if hurt return GetScheduleOfType( SCHED_SMALL_FLINCH ); } // Cower when you hear something scary if ( HasConditions( bits_COND_HEAR_SOUND ) ) { CSound *pSound; pSound = PBestSound(); ASSERT( pSound != NULL ); if ( pSound ) { if ( pSound->m_iType & (bits_SOUND_DANGER | bits_SOUND_COMBAT) ) { if ( gpGlobals->time - m_fearTime > 3 ) // Only cower every 3 seconds or so { m_fearTime = gpGlobals->time; // Update last fear return GetScheduleOfType( SCHED_STARTLE ); // This will just duck for a second } } } } // Behavior for following the player if ( IsFollowing() ) { if ( !m_hTargetEnt->IsAlive() ) { // UNDONE: Comment about the recently dead player here? StopFollowing( FALSE ); break; } int relationship = R_NO; // Nothing scary, just me and the player if ( pEnemy != NULL ) relationship = IRelationship( pEnemy ); // UNDONE: Model fear properly, fix R_FR and add multiple levels of fear if ( relationship != R_DL && relationship != R_HT ) { // If I'm already close enough to my target if ( TargetDistance() <= 128 ) { if ( CanHeal() ) // Heal opportunistically return slHeal; if ( HasConditions( bits_COND_CLIENT_PUSH ) ) // Player wants me to move return GetScheduleOfType( SCHED_MOVE_AWAY_FOLLOW ); } return GetScheduleOfType( SCHED_TARGET_FACE ); // Just face and follow. } else // UNDONE: When afraid, scientist won't move out of your way. Keep This? If not, write move away scared { if ( HasConditions( bits_COND_NEW_ENEMY ) ) // I just saw something new and scary, react return GetScheduleOfType( SCHED_FEAR ); // React to something scary return GetScheduleOfType( SCHED_TARGET_FACE_SCARED ); // face and follow, but I'm scared! } } if ( HasConditions( bits_COND_CLIENT_PUSH ) ) // Player wants me to move return GetScheduleOfType( SCHED_MOVE_AWAY ); // try to say something about smells TrySmellTalk(); break; case MONSTERSTATE_COMBAT: if ( HasConditions( bits_COND_NEW_ENEMY ) ) return slFear; // Point and scream! if ( HasConditions( bits_COND_SEE_ENEMY ) ) return slScientistCover; // Take Cover if ( HasConditions( bits_COND_HEAR_SOUND ) ) return slTakeCoverFromBestSound; // Cower and panic from the scary sound! return slScientistCover; // Run & Cower break; } return CTalkMonster::GetSchedule(); } MONSTERSTATE CScientist :: GetIdealState ( void ) { switch ( m_MonsterState ) { case MONSTERSTATE_ALERT: case MONSTERSTATE_IDLE: if ( HasConditions( bits_COND_NEW_ENEMY ) ) { if ( IsFollowing() ) { int relationship = IRelationship( m_hEnemy ); if ( relationship != R_FR || relationship != R_HT && !HasConditions( bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE ) ) { // Don't go to combat if you're following the player m_IdealMonsterState = MONSTERSTATE_ALERT; return m_IdealMonsterState; } StopFollowing( TRUE ); } } else if ( HasConditions( bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE ) ) { // Stop following if you take damage if ( IsFollowing() ) StopFollowing( TRUE ); } break; case MONSTERSTATE_COMBAT: { CBaseEntity *pEnemy = m_hEnemy; if ( pEnemy != NULL ) { if ( DisregardEnemy( pEnemy ) ) // After 15 seconds of being hidden, return to alert { // Strip enemy when going to alert m_IdealMonsterState = MONSTERSTATE_ALERT; m_hEnemy = NULL; return m_IdealMonsterState; } // Follow if only scared a little if ( m_hTargetEnt != NULL ) { m_IdealMonsterState = MONSTERSTATE_ALERT; return m_IdealMonsterState; } if ( HasConditions ( bits_COND_SEE_ENEMY ) ) { m_fearTime = gpGlobals->time; m_IdealMonsterState = MONSTERSTATE_COMBAT; return m_IdealMonsterState; } } } break; } return CTalkMonster::GetIdealState(); } BOOL CScientist::CanHeal( void ) { if ( (m_healTime > gpGlobals->time) || (m_hTargetEnt == NULL) || (m_hTargetEnt->pev->health > (m_hTargetEnt->pev->max_health * 0.5)) ) return FALSE; return TRUE; } void CScientist::Heal( void ) { if ( !CanHeal() ) return; Vector target = m_hTargetEnt->pev->origin - pev->origin; if ( target.Length() > 100 ) return; m_hTargetEnt->TakeHealth( gSkillData.scientistHeal, DMG_GENERIC ); // Don't heal again for 1 minute m_healTime = gpGlobals->time + 60; } int CScientist::FriendNumber( int arrayNumber ) { static int array[3] = { 1, 2, 0 }; if ( arrayNumber < 3 ) return array[ arrayNumber ]; return arrayNumber; } //========================================================= // Dead Scientist PROP //========================================================= class CDeadScientist : public CBaseMonster { public: void Spawn( void ); int Classify ( void ) { return CLASS_HUMAN_PASSIVE; } void KeyValue( KeyValueData *pkvd ); int m_iPose;// which sequence to display static char *m_szPoses[7]; }; char *CDeadScientist::m_szPoses[] = { "lying_on_back", "lying_on_stomach", "dead_sitting", "dead_hang", "dead_table1", "dead_table2", "dead_table3" }; void CDeadScientist::KeyValue( KeyValueData *pkvd ) { if (FStrEq(pkvd->szKeyName, "pose")) { m_iPose = atoi(pkvd->szValue); pkvd->fHandled = TRUE; } else CBaseMonster::KeyValue( pkvd ); } LINK_ENTITY_TO_CLASS( monster_scientist_dead, CDeadScientist ); // // ********** DeadScientist SPAWN ********** // void CDeadScientist :: Spawn( ) { PRECACHE_MODEL("models/scientist.mdl"); SET_MODEL(ENT(pev), "models/scientist.mdl"); pev->effects = 0; pev->sequence = 0; // Corpses have less health pev->health = 8;//gSkillData.scientistHealth; m_bloodColor = BLOOD_COLOR_RED; if ( pev->body == -1 ) {// -1 chooses a random head pev->body = RANDOM_LONG(0, NUM_SCIENTIST_HEADS-1);// pick a head, any head } // Luther is black, make his hands black if ( pev->body == HEAD_LUTHER ) pev->skin = 1; else pev->skin = 0; pev->sequence = LookupSequence( m_szPoses[m_iPose] ); if (pev->sequence == -1) { ALERT ( at_console, "Dead scientist with bad pose\n" ); } // pev->skin += 2; // use bloody skin -- UNDONE: Turn this back on when we have a bloody skin again! MonsterInitDead(); } //========================================================= // Sitting Scientist PROP //========================================================= class CSittingScientist : public CScientist // kdb: changed from public CBaseMonster so he can speak { public: void Spawn( void ); void Precache( void ); void EXPORT SittingThink( void ); int Classify ( void ); virtual int Save( CSave &save ); virtual int Restore( CRestore &restore ); static TYPEDESCRIPTION m_SaveData[]; virtual void SetAnswerQuestion( CTalkMonster *pSpeaker ); int FriendNumber( int arrayNumber ); int FIdleSpeak ( void ); int m_baseSequence; int m_headTurn; float m_flResponseDelay; }; LINK_ENTITY_TO_CLASS( monster_sitting_scientist, CSittingScientist ); TYPEDESCRIPTION CSittingScientist::m_SaveData[] = { // Don't need to save/restore m_baseSequence (recalced) DEFINE_FIELD( CSittingScientist, m_headTurn, FIELD_INTEGER ), DEFINE_FIELD( CSittingScientist, m_flResponseDelay, FIELD_FLOAT ), }; IMPLEMENT_SAVERESTORE( CSittingScientist, CScientist ); // animation sequence aliases typedef enum { SITTING_ANIM_sitlookleft, SITTING_ANIM_sitlookright, SITTING_ANIM_sitscared, SITTING_ANIM_sitting2, SITTING_ANIM_sitting3 } SITTING_ANIM; // // ********** Scientist SPAWN ********** // void CSittingScientist :: Spawn( ) { PRECACHE_MODEL("models/scientist.mdl"); SET_MODEL(ENT(pev), "models/scientist.mdl"); Precache(); InitBoneControllers(); UTIL_SetSize(pev, Vector(-14, -14, 0), Vector(14, 14, 36)); pev->solid = SOLID_SLIDEBOX; pev->movetype = MOVETYPE_STEP; pev->effects = 0; pev->health = 50; m_bloodColor = BLOOD_COLOR_RED; m_flFieldOfView = VIEW_FIELD_WIDE; // indicates the width of this monster's forward view cone ( as a dotproduct result ) m_afCapability = bits_CAP_HEAR | bits_CAP_TURN_HEAD; SetBits(pev->spawnflags, SF_MONSTER_PREDISASTER); // predisaster only! if ( pev->body == -1 ) {// -1 chooses a random head pev->body = RANDOM_LONG(0, NUM_SCIENTIST_HEADS-1);// pick a head, any head } // Luther is black, make his hands black if ( pev->body == HEAD_LUTHER ) pev->skin = 1; m_baseSequence = LookupSequence( "sitlookleft" ); pev->sequence = m_baseSequence + RANDOM_LONG(0,4); ResetSequenceInfo( ); SetThink (&CSittingScientist::SittingThink); pev->nextthink = gpGlobals->time + 0.1; DROP_TO_FLOOR ( ENT(pev) ); } void CSittingScientist :: Precache( void ) { m_baseSequence = LookupSequence( "sitlookleft" ); TalkInit(); } //========================================================= // ID as a passive human //========================================================= int CSittingScientist :: Classify ( void ) { return CLASS_HUMAN_PASSIVE; } int CSittingScientist::FriendNumber( int arrayNumber ) { static int array[3] = { 2, 1, 0 }; if ( arrayNumber < 3 ) return array[ arrayNumber ]; return arrayNumber; } //========================================================= // sit, do stuff //========================================================= void CSittingScientist :: SittingThink( void ) { CBaseEntity *pent; StudioFrameAdvance( ); // try to greet player if (FIdleHello()) { pent = FindNearestFriend(TRUE); if (pent) { float yaw = VecToYaw(pent->pev->origin - pev->origin) - pev->angles.y; if (yaw > 180) yaw -= 360; if (yaw < -180) yaw += 360; if (yaw > 0) pev->sequence = m_baseSequence + SITTING_ANIM_sitlookleft; else pev->sequence = m_baseSequence + SITTING_ANIM_sitlookright; ResetSequenceInfo( ); pev->frame = 0; SetBoneController( 0, 0 ); } } else if (m_fSequenceFinished) { int i = RANDOM_LONG(0,99); m_headTurn = 0; if (m_flResponseDelay && gpGlobals->time > m_flResponseDelay) { // respond to question IdleRespond(); pev->sequence = m_baseSequence + SITTING_ANIM_sitscared; m_flResponseDelay = 0; } else if (i < 30) { pev->sequence = m_baseSequence + SITTING_ANIM_sitting3; // turn towards player or nearest friend and speak if (!FBitSet(m_bitsSaid, bit_saidHelloPlayer)) pent = FindNearestFriend(TRUE); else pent = FindNearestFriend(FALSE); if (!FIdleSpeak() || !pent) { m_headTurn = RANDOM_LONG(0,8) * 10 - 40; pev->sequence = m_baseSequence + SITTING_ANIM_sitting3; } else { // only turn head if we spoke float yaw = VecToYaw(pent->pev->origin - pev->origin) - pev->angles.y; if (yaw > 180) yaw -= 360; if (yaw < -180) yaw += 360; if (yaw > 0) pev->sequence = m_baseSequence + SITTING_ANIM_sitlookleft; else pev->sequence = m_baseSequence + SITTING_ANIM_sitlookright; //ALERT(at_console, "sitting speak\n"); } } else if (i < 60) { pev->sequence = m_baseSequence + SITTING_ANIM_sitting3; m_headTurn = RANDOM_LONG(0,8) * 10 - 40; if (RANDOM_LONG(0,99) < 5) { //ALERT(at_console, "sitting speak2\n"); FIdleSpeak(); } } else if (i < 80) { pev->sequence = m_baseSequence + SITTING_ANIM_sitting2; } else if (i < 100) { pev->sequence = m_baseSequence + SITTING_ANIM_sitscared; } ResetSequenceInfo( ); pev->frame = 0; SetBoneController( 0, m_headTurn ); } pev->nextthink = gpGlobals->time + 0.1; } // prepare sitting scientist to answer a question void CSittingScientist :: SetAnswerQuestion( CTalkMonster *pSpeaker ) { m_flResponseDelay = gpGlobals->time + RANDOM_FLOAT(3, 4); m_hTalkTarget = (CBaseMonster *)pSpeaker; } //========================================================= // FIdleSpeak // ask question of nearby friend, or make statement //========================================================= int CSittingScientist :: FIdleSpeak ( void ) { // try to start a conversation, or make statement int pitch; if (!FOkToSpeak()) return FALSE; // set global min delay for next conversation CTalkMonster::g_talkWaitTime = gpGlobals->time + RANDOM_FLOAT(4.8, 5.2); pitch = GetVoicePitch(); // if there is a friend nearby to speak to, play sentence, set friend's response time, return // try to talk to any standing or sitting scientists nearby CBaseEntity *pentFriend = FindNearestFriend(FALSE); if (pentFriend && RANDOM_LONG(0,1)) { CTalkMonster *pTalkMonster = GetClassPtr((CTalkMonster *)pentFriend->pev); pTalkMonster->SetAnswerQuestion( this ); IdleHeadTurn(pentFriend->pev->origin); SENTENCEG_PlayRndSz( ENT(pev), m_szGrp[TLK_PQUESTION], 1.0, ATTN_IDLE, 0, pitch ); // set global min delay for next conversation CTalkMonster::g_talkWaitTime = gpGlobals->time + RANDOM_FLOAT(4.8, 5.2); return TRUE; } // otherwise, play an idle statement if (RANDOM_LONG(0,1)) { SENTENCEG_PlayRndSz( ENT(pev), m_szGrp[TLK_PIDLE], 1.0, ATTN_IDLE, 0, pitch ); // set global min delay for next conversation CTalkMonster::g_talkWaitTime = gpGlobals->time + RANDOM_FLOAT(4.8, 5.2); return TRUE; } // never spoke CTalkMonster::g_talkWaitTime = 0; return FALSE; }