1026 lines
25 KiB
C++
1026 lines
25 KiB
C++
/***
|
|
*
|
|
* 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 "cmbase.h"
|
|
#include "cmbasemonster.h"
|
|
#include "monsters.h"
|
|
#include "schedule.h"
|
|
#include "defaultai.h"
|
|
#include "animation.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
|
|
//=======================================================
|
|
|
|
|
|
//=========================================================
|
|
// 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,
|
|
0,
|
|
"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,
|
|
0,
|
|
"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,
|
|
0,
|
|
"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,
|
|
0,
|
|
"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,
|
|
0,
|
|
"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,
|
|
0,
|
|
"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( CMScientist )
|
|
{
|
|
slFollow,
|
|
slFaceTarget,
|
|
slIdleSciStand,
|
|
slFear,
|
|
slScientistCover,
|
|
slScientistHide,
|
|
slScientistStartle,
|
|
slHeal,
|
|
slStopFollowing,
|
|
slSciPanic,
|
|
slFollowScared,
|
|
slFaceTargetScared,
|
|
};
|
|
|
|
|
|
IMPLEMENT_CUSTOM_SCHEDULES( CMScientist, CMTalkMonster );
|
|
|
|
|
|
void CMScientist::DeclineFollowing( void )
|
|
{
|
|
Talk( 10 );
|
|
m_hTalkTarget = m_hEnemy;
|
|
PlaySentence( "SC_POK", 2, VOL_NORM, ATTN_NORM );
|
|
}
|
|
|
|
|
|
void CMScientist :: Scream( void )
|
|
{
|
|
if ( FOkToSpeak() )
|
|
{
|
|
Talk( 10 );
|
|
m_hTalkTarget = m_hEnemy;
|
|
PlaySentence( "SC_SCREAM", RANDOM_FLOAT(3, 6), VOL_NORM, ATTN_NORM );
|
|
}
|
|
}
|
|
|
|
|
|
Activity CMScientist::GetStoppedActivity( void )
|
|
{
|
|
if ( m_hEnemy != NULL )
|
|
return ACT_EXCITED;
|
|
return CMTalkMonster::GetStoppedActivity();
|
|
}
|
|
|
|
|
|
void CMScientist :: 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 ( UTIL_IsPlayer(m_hEnemy) )
|
|
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->v.origin - pev->origin).Length() < 1 )
|
|
TaskComplete();
|
|
else
|
|
{
|
|
m_vecMoveGoal = m_hTargetEnt->v.origin;
|
|
if ( !MoveToTarget( ACT_WALK_SCARED, 0.5 ) )
|
|
TaskFail();
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
CMTalkMonster::StartTask( pTask );
|
|
break;
|
|
}
|
|
}
|
|
|
|
void CMScientist :: 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->v.origin).Length() > pTask->flData * 0.5 )
|
|
{
|
|
m_vecMoveGoal = m_hTargetEnt->v.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->v.origin - pev->origin );
|
|
ChangeYaw( pev->yaw_speed );
|
|
}
|
|
break;
|
|
default:
|
|
CMTalkMonster::RunTask( pTask );
|
|
break;
|
|
}
|
|
}
|
|
|
|
//=========================================================
|
|
// Classify - indicates this monster's place in the
|
|
// relationship table.
|
|
//=========================================================
|
|
int CMScientist :: Classify ( void )
|
|
{
|
|
if ( m_iClassifyOverride == -1 ) // helper
|
|
return CLASS_NONE;
|
|
else if ( m_iClassifyOverride > 0 )
|
|
return m_iClassifyOverride; // override
|
|
|
|
return CLASS_HUMAN_PASSIVE;
|
|
}
|
|
|
|
|
|
//=========================================================
|
|
// SetYawSpeed - allows each sequence to have a different
|
|
// turn rate associated with it.
|
|
//=========================================================
|
|
void CMScientist :: 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 CMScientist :: 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:
|
|
CMTalkMonster::HandleAnimEvent( pEvent );
|
|
}
|
|
}
|
|
|
|
//=========================================================
|
|
// Spawn
|
|
//=========================================================
|
|
void CMScientist :: Spawn( void )
|
|
{
|
|
Precache( );
|
|
|
|
// every new scientist must call this, otherwise
|
|
// when a level is loaded, nobody will talk (time is reset to 0)
|
|
TalkInit();
|
|
|
|
SET_MODEL(ENT(pev), (!FStringNull( pev->model ) ? STRING( pev->model ) : "models/scientist.mdl"));
|
|
UTIL_SetSize(pev, VEC_HUMAN_HULL_MIN, VEC_HUMAN_HULL_MAX);
|
|
|
|
pev->solid = SOLID_SLIDEBOX;
|
|
pev->movetype = MOVETYPE_STEP;
|
|
m_bloodColor = !m_bloodColor ? BLOOD_COLOR_RED : m_bloodColor;
|
|
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;
|
|
|
|
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();
|
|
|
|
pev->classname = MAKE_STRING( "monster_scientist" );
|
|
if ( strlen( STRING( m_szMonsterName ) ) == 0 )
|
|
{
|
|
// default name
|
|
m_szMonsterName = MAKE_STRING( "Scientist" );
|
|
}
|
|
}
|
|
|
|
//=========================================================
|
|
// Precache - precaches all resources this monster needs
|
|
//=========================================================
|
|
void CMScientist :: 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");
|
|
|
|
CMTalkMonster::Precache();
|
|
}
|
|
|
|
// Init talk data
|
|
void CMScientist :: TalkInit()
|
|
{
|
|
CMTalkMonster::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 % NUM_SCIENTIST_HEADS)
|
|
{
|
|
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 CMScientist :: 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 CMTalkMonster::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 CMScientist :: ISoundMask ( void )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
//=========================================================
|
|
// PainSound
|
|
//=========================================================
|
|
void CMScientist :: 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 CMScientist :: DeathSound ( void )
|
|
{
|
|
PainSound();
|
|
}
|
|
|
|
|
|
void CMScientist::Killed( entvars_t *pevAttacker, int iGib )
|
|
{
|
|
SetUse( NULL );
|
|
CMTalkMonster::Killed( pevAttacker, iGib );
|
|
}
|
|
|
|
|
|
void CMScientist :: 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;
|
|
CMTalkMonster::SetActivity( newActivity );
|
|
}
|
|
|
|
|
|
Schedule_t* CMScientist :: 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 = CMTalkMonster::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 = CMTalkMonster::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 CMTalkMonster::GetScheduleOfType( Type );
|
|
}
|
|
|
|
Schedule_t *CMScientist :: GetSchedule ( void )
|
|
{
|
|
// so we don't keep calling through the EHANDLE stuff
|
|
edict_t *pEnemy = m_hEnemy;
|
|
int relationship;
|
|
|
|
switch( m_MonsterState )
|
|
{
|
|
case MONSTERSTATE_ALERT:
|
|
case MONSTERSTATE_IDLE:
|
|
if ( ( pEnemy ) && !UTIL_IsPlayer(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 );
|
|
}
|
|
|
|
// Nothing scary, just me and the player
|
|
if ( pEnemy != NULL )
|
|
{
|
|
relationship = R_NO;
|
|
|
|
if (UTIL_IsPlayer(pEnemy))
|
|
relationship = R_AL; // allies
|
|
else if (pEnemy->v.euser4 != NULL)
|
|
{
|
|
CMBaseMonster *pMonster = GetClassPtr((CMBaseMonster *)VARS(pEnemy));
|
|
relationship = IRelationship( pMonster );
|
|
}
|
|
|
|
// 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 ) // uses m_hTargetEnt
|
|
{
|
|
if ( CanHeal() ) // Heal opportunistically
|
|
return slHeal;
|
|
if ( HasConditions( bits_COND_CLIENT_PUSH ) ) // Player wants me to move
|
|
return GetScheduleOfType( SCHED_MOVE_AWAY_FOLLOW );
|
|
}
|
|
//jlb
|
|
m_hEnemy = NULL;
|
|
//jlb
|
|
|
|
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 CMTalkMonster::GetSchedule();
|
|
}
|
|
|
|
MONSTERSTATE CMScientist :: GetIdealState ( void )
|
|
{
|
|
switch ( m_MonsterState )
|
|
{
|
|
case MONSTERSTATE_ALERT:
|
|
case MONSTERSTATE_IDLE:
|
|
if ( HasConditions( bits_COND_NEW_ENEMY ) )
|
|
{
|
|
if ( IsFollowing() )
|
|
{
|
|
int relationship = R_NO;
|
|
|
|
if (UTIL_IsPlayer(m_hEnemy))
|
|
relationship = R_AL; // allies
|
|
else if (m_hEnemy->v.euser4 != NULL)
|
|
{
|
|
edict_t *pEdict = m_hEnemy;
|
|
CMBaseMonster *pMonster = GetClassPtr((CMBaseMonster *)VARS(pEdict));
|
|
relationship = IRelationship( pMonster );
|
|
}
|
|
|
|
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:
|
|
{
|
|
edict_t *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 CMTalkMonster::GetIdealState();
|
|
}
|
|
|
|
|
|
BOOL CMScientist::CanHeal( void )
|
|
{
|
|
if ( (m_healTime > gpGlobals->time) || (m_hTargetEnt == NULL) || (m_hTargetEnt->v.health > (m_hTargetEnt->v.max_health * 0.5)) )
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
void CMScientist::Heal( void )
|
|
{
|
|
if ( !CanHeal() )
|
|
return;
|
|
|
|
Vector target = m_hTargetEnt->v.origin - pev->origin;
|
|
if ( target.Length() > 100 )
|
|
return;
|
|
|
|
if (UTIL_IsPlayer(m_hTargetEnt))
|
|
UTIL_TakeHealth( m_hTargetEnt, gSkillData.scientistHeal, DMG_GENERIC );
|
|
else if (m_hTargetEnt->v.euser4 != NULL)
|
|
{
|
|
edict_t *pEdict = m_hTargetEnt;
|
|
CMBaseMonster *pMonster = GetClassPtr((CMBaseMonster *)VARS(pEdict));
|
|
pMonster->TakeHealth( gSkillData.scientistHeal, DMG_GENERIC );
|
|
}
|
|
|
|
// Don't heal again for 1 minute
|
|
m_healTime = gpGlobals->time + 60;
|
|
}
|
|
|
|
int CMScientist::FriendNumber( int arrayNumber )
|
|
{
|
|
static int array[3] = { 1, 2, 0 };
|
|
if ( arrayNumber < 3 )
|
|
return array[ arrayNumber ];
|
|
return arrayNumber;
|
|
}
|
|
|
|
BOOL CMScientist :: DisregardEnemy( edict_t *pEnemy )
|
|
{
|
|
return !UTIL_IsAlive(pEnemy) || (gpGlobals->time - m_fearTime) > 15;
|
|
}
|
|
|