CBaseMonster
to CTalkMonster
, so do this and don't forget to include the talkmonster.h
header file.const char* CTalkMonster::m_szFriends[TLK_CFRIENDS]
array at the beginning of talkmonster.cpp
(don't forget to update the array's size as well) to add the classname of our Friendly Assassin. This is required by CTalkMonster
in order to have the other friendly monsters (Barney, scientists...) be aware of our Friendly Assassin for certain behaviors (talk to each other, alert each other in case of danger, warn the player in case of friendly fire).scientist.cpp
and more specifically the CScientist::TalkInit()
method, you need to add our Friendly Assassin alongside the other friendly monsters.
Precache()
method where you can add this:
PRECACHE_SOUND("fassassin/fa_die1.wav");
PRECACHE_SOUND("fassassin/fa_pain1.wav");
CTalkMonster::TalkInit();
m_szGrp[TLK_ANSWER] = "FA_ANSWER";
m_szGrp[TLK_QUESTION] = "FA_QUESTION";
m_szGrp[TLK_IDLE] = "FA_IDLE";
m_szGrp[TLK_STARE] = "FA_STARE";
m_szGrp[TLK_USE] = "FA_OK";
m_szGrp[TLK_UNUSE] = "FA_WAIT";
m_szGrp[TLK_STOP] = "FA_STOP";
m_szGrp[TLK_NOSHOOT] = "FA_SCARED";
m_szGrp[TLK_HELLO] = "FA_HELLO";
m_szGrp[TLK_PLHURT1] = "!FA_CUREA";
m_szGrp[TLK_PLHURT2] = "!FA_CUREB";
m_szGrp[TLK_PLHURT3] = "!FA_CUREC";
m_szGrp[TLK_PHELLO] = nullptr;
m_szGrp[TLK_PIDLE] = nullptr;
m_szGrp[TLK_PQUESTION] = "FA_PQUEST";
m_szGrp[TLK_SMELL] = "FA_SMELL";
m_szGrp[TLK_WOUND] = "FA_WOUND";
m_szGrp[TLK_MORTAL] = "FA_MORTAL";
m_voicePitch = PITCH_NORM;
CTalkMonster::Precache();
The pain and death sounds are "normal" sounds, this is explained a bit later as to why, not everything needs to be tied to a sentence.TLK_
values mean, these are values from the TALKGROUPNAMES
(talkmonster.h
) enumeration which represent a context of the conversation. Values are:
Enumeration name | Value | Context |
---|---|---|
TLK_ANSWER |
0 |
Answering a question that a friend asked (idle chit-chat). |
TLK_QUESTION |
1 |
Asking a question to a friend (idle chit-chat). |
TLK_IDLE |
2 |
Idle chit-chat (usually to the player when there is no friend nearby). |
TLK_STARE |
3 |
React to a client (bot or player) staring at me for some time. |
TLK_USE |
4 |
Client is asking me to follow me and I accept it. |
TLK_UNUSE |
4 |
Client is asking me to stay at my position and I accept it. |
TLK_STOP |
5 |
I can't follow the client anymore (usually blocked path) and I must stop. |
TLK_NOSHOOT |
6 |
Client is shooting my friend (friendly fire) and I'm asking him/her/them to stop. |
TLK_HELLO |
7 |
Greeting the client (after seeing him/her/them for the first time). |
TLK_PHELLO |
8 |
Same as previous one but in "pre-disaster" (before resonance cascade) scenario. |
TLK_PIDLE |
8 |
Same as TLK_IDLE but in "pre-disaster" scenario. |
TLK_PQUESTION |
9 |
Same as TLK_QUESTION but in "pre-disaster" scenario. |
TLK_PLHURT1 |
10 |
I want to chit-chat in this idle situation but the client I'm following has less than 50% of his/her/their health. |
TLK_PLHURT2 |
11 |
Same as previous one but with the client's health being less than 25%. |
TLK_PLHURT3 |
12 |
Same as previous one but with the client's health being less than 12,5%. |
TLK_SMELL |
13 |
I smell something foul. |
TLK_WOUND |
14 |
I want to chit-chat in this idle situation but my health is below 75%. |
TLK_MORTAL |
15 |
Same as previous one but my health is below 50%. |
TLK_CGROUPS |
16 |
Not used by monsters but must this value must always be the last one since it acts as the enumeration's count (used to define const char* CTalkMonster::m_szGrp[TLK_CGROUPS] array size). |
CTalkMonster::TalkInit()
is to reset the "global talking timer" (float CTalkMonster::g_talkWaitTime
) common to all talkative monsters. The purpose of said timer is to prevent conversations from "overlapping" and being interrupted, one quirk with that method is that it also sets the voice pitch (m_voicePitch
) to 100 (PITCH_NORM
). For monsters that have different voice pitches based on certain criterias (like the scientists having a different voice pitch depending on their head), that can be problematic in certain scenarios. That's why it's a good practice to set m_voicePitch
at the end of the Precache()
method.
m_szGrp
is an array of sentence groups where the index correspond to a context like player staring at the monster, asking him to follow/stay here, asking/answering a question before the incident ("pre-disaster") and after it ("post-disaster").CTalkMonster::Precache()
precache and sets the custom "use/unuse" sentences if asked by the level designer. Even if you are not using that feature, it's a good idea to include that line because the "core" of said feature is part of CTalkMonster
.void CFAssassin::AlertSound()
{
if (m_hEnemy == nullptr || !FOkToSpeak())
return;
PlaySentence("FA_ATTACK", RANDOM_FLOAT(2.8f, 3.2f), VOL_NORM, ATTN_IDLE);
}
void CFAssassin::DeathSound()
{
EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, "fassassin/fa_die1.wav", VOL_NORM, ATTN_NORM, 0, GetVoicePitch());
}
void CFAssassin::PainSound()
{
if (gpGlobals->time < m_flPainTime)
return;
m_flPainTime = gpGlobals->time + RANDOM_FLOAT(0.5f, 0.75f);
EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, "fassassin/fa_pain1.wav", VOL_NORM, ATTN_NORM, 0, GetVoicePitch());
}
In AlertSound()
, you might have noticed the usage of bool CTalkMonster::FOkToSpeak()
. This method is responsible of checking if the monsters can speak by checking if various criterias are met such as:
MONSTERSTATE_PRONE
).CTalkMonster::PlaySentence(const char* pszSentence, float duration, float volume, float attenuation)
is the method that play sentences. While the parameters are self-explanatory, there is a special case for pszSentence
. In sentences.txt
, to define multiple lines for the same context, you suffix the name with an incremental number (FA_WOUND0
, FA_WOUND1
for example). For pszSentence
, if the first character is an exclamation mark (!
), you can play a specific sentence directly and bypass the randomization (!FA_WOUND1
will always play the sentence FA_WOUND1
). If you do not put that exclamation mark, then a random line for said context will be picked up.m_bitsSaid
attribute. See the similarity with conditions/memory bits? These bits are cleared later (except for the "hello" one) when friendly monsters can repeat that kind of dialogue.CTalkMonster::PlaySentence(const char* pszSentence, float duration, float volume, float attenuation)
? When playing a sentence through that method, the "hello said bit" is set even if the friendly monster never said "hello" in the first place and/or the sentence to play is not a "hello" one.GetVoicePitch()
as you might expect returns the value of m_voicePitch
but do note that there is a tiny bit of randomization added as well (in reality, it's m_voicePitch + RANDOM_LONG(0, 3)
).m_flPainTime
to our Friendly Assassin class to prevent the pain sounds being played too frequently.CTalkMonster::TrySmellTalk()
(which will be used later) in order to make the monster smell for scents and try to talk about it and CTalkMonster::ShutUpFriends()
to silence everyone besides the current monster. This is used by the concurrent spawnflag in scripted_sentence and the animation events 1005 (play a sentence) and 1009 (25% chance to play a sentence).int ISoundMask()
) as the Human Assassin, you might want to use the same as Barney's in order to keep things consistent between all friendly monsters.
int ObjectCaps()
for our Friendly Assassin and return the result of CTalkMonster::ObjectCaps() | FCAP_IMPULSE_USE
so that the USE key works. Then you can simply add SetUse(&CFAssassin::FollowerUse)
in the Spawn()
method to use the code from CTalkMonster
.void CTalkMonster::FollowerUse(CBaseEntity* pActivator, CBaseEntity* pCaller, USE_TYPE useType, float value)
a bit. For starters, it won't do anything if you are "using" the monster during a script (like scripted_sequence).CTalkMonster::DeclineFollowing()
method is executed. It might be a good idea to override this method in our Friendly Assassin class to play the FA_POK
sentence there.bool CTalkMonster::CanFollow()
is made to check if she can follow the player. It returns false
if in a script state, dead or bool CTalkMonster::IsFollowing()
returns true
, it returns false
otherwise.CTalkMonster::StartFollowing(CBaseEntity* pLeader)
is called with the player being the leader.CTalkMonster::StopFollowing(bool clearSchedule)
is called with clearSchedule
being true
.Task_t tlFAssassinFaceTarget[] =
{
{TASK_SET_ACTIVITY, (float)ACT_IDLE},
{TASK_FACE_TARGET, 0.0f},
{TASK_SET_ACTIVITY, (float)ACT_IDLE},
{TASK_SET_SCHEDULE, (float)SCHED_TARGET_CHASE},
};
Schedule_t slFAssassinFaceTarget[] =
{
{tlFAssassinFaceTarget,
ARRAYSIZE(tlFAssassinFaceTarget),
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,
"FAssassinFaceTarget"},
};
Task_t tlFAssassinFollow[] =
{
{TASK_MOVE_TO_TARGET_RANGE, 128.0f},
{TASK_SET_SCHEDULE, (float)SCHED_TARGET_FACE},
};
Schedule_t slFAssassinFollow[] =
{
{tlFAssassinFollow,
ARRAYSIZE(tlFAssassinFollow),
bits_COND_NEW_ENEMY |
bits_COND_LIGHT_DAMAGE |
bits_COND_HEAVY_DAMAGE |
bits_COND_HEAR_SOUND |
bits_COND_PROVOKED,
bits_SOUND_DANGER,
"FAssassinFollow"},
};
Don't forget to update the custom schedules table. Then we can update Schedule_t *GetSchedule()
and Schedule_t *GetScheduleOfType(int Type)
like this:
Schedule_t* CFAssassin::GetSchedule()
{
if (HasConditions(bits_COND_HEAR_SOUND))
{
CSound* pSound = PBestSound();
if (pSound && (pSound->m_iType & bits_SOUND_DANGER) != 0)
return GetScheduleOfType(SCHED_TAKE_COVER_FROM_BEST_SOUND);
}
switch (m_MonsterState)
{
case MONSTERSTATE_COMBAT:
{
// Check ASAP if we can heal a friend right now
m_pHealTarget = CheckHealTarget();
if (m_pHealTarget)
return GetScheduleOfType(SCHED_RANGE_ATTACK2);
if (HasConditions(bits_COND_ENEMY_DEAD))
return CTalkMonster::GetSchedule();
if (HasConditions(bits_COND_HEAVY_DAMAGE))
return GetScheduleOfType(SCHED_TAKE_COVER_FROM_ENEMY);
if (HasConditions(bits_COND_CAN_RANGE_ATTACK1))
return GetScheduleOfType(SCHED_RANGE_ATTACK1);
if (HasConditions(bits_COND_SEE_ENEMY))
return GetScheduleOfType(SCHED_COMBAT_FACE);
}
break;
case MONSTERSTATE_ALERT:
case MONSTERSTATE_IDLE:
{
// Check ASAP if we can heal a friend right now
m_pHealTarget = CheckHealTarget();
if (m_pHealTarget)
return GetScheduleOfType(SCHED_RANGE_ATTACK2);
// No enemy and ordered to follow the player
if (m_hEnemy == nullptr && IsFollowing())
{
// If the player died, stop following him
if (!m_hTargetEnt->IsAlive())
{
StopFollowing(false);
break;
}
else
{
// Player is pushing me (probably because I'm blocking his path), run the "move away and follow" schedule
if (HasConditions(bits_COND_CLIENT_PUSH))
return GetScheduleOfType(SCHED_MOVE_AWAY_FOLLOW);
// Just face the player to know where he's going
return GetScheduleOfType(SCHED_TARGET_FACE);
}
}
// Player is pushing me (probably because I'm blocking his path), run the "move away" schedule
if (HasConditions(bits_COND_CLIENT_PUSH))
return GetScheduleOfType(SCHED_MOVE_AWAY);
// Smell and try to talk about it if there's a scent
TrySmellTalk();
return GetScheduleOfType(SCHED_ALERT_STAND);
}
break;
}
return CTalkMonster::GetSchedule();
}
Schedule_t* CFAssassin::GetScheduleOfType(int Type)
{
Schedule_t* pSched;
switch (Type)
{
case SCHED_TAKE_COVER_FROM_ENEMY:
return pev->health > 30 ? slFAssassinTakeCoverFromEnemy : slFAssassinTakeCoverFromEnemy2;
case SCHED_TAKE_COVER_FROM_BEST_SOUND:
return slFAssassinTakeCoverFromBestSound;
case SCHED_CHASE_ENEMY:
return slFAssassinHunt;
case SCHED_TARGET_CHASE: // Part of the follow code, make sure to actually follow the player
return slFAssassinFollow;
case SCHED_TARGET_FACE: // Use our version of "face target" instead of the default one
pSched = CTalkMonster::GetScheduleOfType(Type);
if (pSched == slIdleStand)
return slFAssassinFaceTarget;
else
return pSched;
break;
}
return CTalkMonster::GetScheduleOfType(Type);
}
Do notice the importance (and duplication) of the code that order the friendly assassin to check and eventually heal an ally. If the code has been placed outside of the switch
, there would have been risks where said code could run in "unexpected scenarios" such as "being barnacled" (MONSTERSTATE_PRONE
), running a scripted sequence (MONSTERSTATE_SCRIPT
) or even dead (MONSTERSTATE_DEAD
).
CBaseMonster
to CTalkMonster
, you can check the CTalkMonster::Killed()
method to see how it's done.bool CBaseEntity::TakeDamage(entvars_t* pevInflictor, entvars_t* pevAttacker, float flDamage, int bitsDamageType)
method like this:
bool CFAssassin::TakeDamage(entvars_t* pevInflictor, entvars_t* pevAttacker, float flDamage, int bitsDamageType)
{
bool bReturn = CTalkMonster::TakeDamage(pevInflictor, pevAttacker, flDamage, bitsDamageType);
if (!IsAlive() || pev->deadflag == DEAD_DYING)
return bReturn;
if (m_MonsterState == MONSTERSTATE_PRONE || (pevAttacker->flags & FL_CLIENT) == 0)
return bReturn;
if (m_hEnemy == nullptr)
{
if ((m_afMemory & bits_MEMORY_SUSPICIOUS) != 0 || IsFacing(pevAttacker, pev->origin))
{
PlaySentence("FA_MAD", 4.0f, VOL_NORM, ATTN_NORM);
Remember(bits_MEMORY_PROVOKED);
StopFollowing(true);
}
else
{
PlaySentence("FA_SHOT", 4.0f, VOL_NORM, ATTN_NORM);
Remember(bits_MEMORY_SUSPICIOUS);
}
}
else if (!(m_hEnemy->IsPlayer()) && pev->deadflag == DEAD_NO)
{
PlaySentence("FA_SHOT", 4.0f, VOL_NORM, ATTN_NORM);
}
return bReturn;
}
First, we call the parent bool CTalkDamage::TakeDamage(pevInflictor, pevAttacker, flDamage, bitsDamageType)
and store it's result. CTalkMonster
override is responsible for having nearby friendlies asking the player to stop before calling the CBaseMonster
version to handle the damage, pain sound, death and so on.m_MonsterState == MONSTERSTATE_PRONE
) or the damage come from a non-player, then there is nothing extra to do.CTalkMonster
code in talkmonster.cpp
:
ACT_IDLE
, it tries to use the ACT_SIGNAL3
activity if possible instead. This is not used by current Half-Life's talkative monsters and might be some leftover from its development. Barney for example, does not have any sequence that has ACT_SIGNAL3
so the code just disregard it. If you replace the model, for example with Human Grunts, it will play the sequences that have ACT_SIGNAL3
on it.m_hTalkTarget
is the target entity to look at when speaking (if set).m_hTargetEnt
is the target to follow.void CTalkMonster::FIdleSpeak()
, there is an unused spot where if you add some code, friendly monsters could comment about player's death.m_bitsSaid
member in CTalkMonster
with various bits like bit_saidSmelled
, bit_saidHelloPlayer
and an unused bit_saidHeard
one.You must log in to post a comment. You can login or register a new account.