/*** * * 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. * ****/ #include "extdll.h" #include "util.h" #include "cbase.h" #include "monsters.h" #include "schedule.h" #include "talkmonster.h" #include "defaultai.h" #include "scripted.h" #include "soundent.h" #include "animation.h" //========================================================= // Talking monster base class // Used for scientists and barneys //========================================================= float CTalkMonster::g_talkWaitTime = 0; // time delay until it's ok to speak: used so that two NPCs don't talk at once // NOTE: m_voicePitch & m_szGrp should be fixed up by precache each save/restore TYPEDESCRIPTION CTalkMonster::m_SaveData[] = { DEFINE_FIELD(CTalkMonster, m_bitsSaid, FIELD_INTEGER), DEFINE_FIELD(CTalkMonster, m_nSpeak, FIELD_INTEGER), // Recalc'ed in Precache() // DEFINE_FIELD( CTalkMonster, m_voicePitch, FIELD_INTEGER ), // DEFINE_FIELD( CTalkMonster, m_szGrp, FIELD_??? ), DEFINE_FIELD(CTalkMonster, m_useTime, FIELD_TIME), DEFINE_FIELD(CTalkMonster, m_iszUse, FIELD_STRING), DEFINE_FIELD(CTalkMonster, m_iszUnUse, FIELD_STRING), DEFINE_FIELD(CTalkMonster, m_flLastSaidSmelled, FIELD_TIME), DEFINE_FIELD(CTalkMonster, m_flStopTalkTime, FIELD_TIME), DEFINE_FIELD(CTalkMonster, m_hTalkTarget, FIELD_EHANDLE), }; IMPLEMENT_SAVERESTORE(CTalkMonster, CBaseMonster); // array of friend names const char* CTalkMonster::m_szFriends[TLK_CFRIENDS] = { "monster_barney", "monster_scientist", "monster_sitting_scientist", }; //========================================================= // AI Schedules Specific to talking monsters //========================================================= Task_t tlIdleResponse[] = { {TASK_SET_ACTIVITY, (float)ACT_IDLE}, // Stop and listen {TASK_WAIT, (float)0.5}, // Wait until sure it's me they are talking to {TASK_TLK_EYECONTACT, (float)0}, // Wait until speaker is done {TASK_TLK_RESPOND, (float)0}, // Wait and then say my response {TASK_TLK_IDEALYAW, (float)0}, // look at who I'm talking to {TASK_FACE_IDEAL, (float)0}, {TASK_SET_ACTIVITY, (float)ACT_SIGNAL3}, {TASK_TLK_EYECONTACT, (float)0}, // Wait until speaker is done }; Schedule_t slIdleResponse[] = { {tlIdleResponse, ARRAYSIZE(tlIdleResponse), bits_COND_NEW_ENEMY | bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE, 0, "Idle Response" }, }; Task_t tlIdleSpeak[] = { {TASK_TLK_SPEAK, (float)0}, // question or remark {TASK_TLK_IDEALYAW, (float)0}, // look at who I'm talking to {TASK_FACE_IDEAL, (float)0}, {TASK_SET_ACTIVITY, (float)ACT_SIGNAL3}, {TASK_TLK_EYECONTACT, (float)0}, {TASK_WAIT_RANDOM, (float)0.5}, }; Schedule_t slIdleSpeak[] = { {tlIdleSpeak, ARRAYSIZE(tlIdleSpeak), bits_COND_NEW_ENEMY | bits_COND_CLIENT_PUSH | bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE, 0, "Idle Speak"}, }; Task_t tlIdleSpeakWait[] = { {TASK_SET_ACTIVITY, (float)ACT_SIGNAL3}, // Stop and talk {TASK_TLK_SPEAK, (float)0}, // question or remark {TASK_TLK_EYECONTACT, (float)0}, // {TASK_WAIT, (float)2}, // wait - used when sci is in 'use' mode to keep head turned }; Schedule_t slIdleSpeakWait[] = { {tlIdleSpeakWait, ARRAYSIZE(tlIdleSpeakWait), bits_COND_NEW_ENEMY | bits_COND_CLIENT_PUSH | bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE, 0, "Idle Speak Wait"}, }; Task_t tlIdleHello[] = { {TASK_SET_ACTIVITY, (float)ACT_SIGNAL3}, // Stop and talk {TASK_TLK_HELLO, (float)0}, // Try to say hello to player {TASK_TLK_EYECONTACT, (float)0}, {TASK_WAIT, (float)0.5}, // wait a bit {TASK_TLK_HELLO, (float)0}, // Try to say hello to player {TASK_TLK_EYECONTACT, (float)0}, {TASK_WAIT, (float)0.5}, // wait a bit {TASK_TLK_HELLO, (float)0}, // Try to say hello to player {TASK_TLK_EYECONTACT, (float)0}, {TASK_WAIT, (float)0.5}, // wait a bit {TASK_TLK_HELLO, (float)0}, // Try to say hello to player {TASK_TLK_EYECONTACT, (float)0}, {TASK_WAIT, (float)0.5}, // wait a bit }; Schedule_t slIdleHello[] = { {tlIdleHello, ARRAYSIZE(tlIdleHello), bits_COND_NEW_ENEMY | bits_COND_CLIENT_PUSH | bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE | bits_COND_HEAR_SOUND | bits_COND_PROVOKED, bits_SOUND_COMBAT, "Idle Hello"}, }; Task_t tlIdleStopShooting[] = { {TASK_TLK_STOPSHOOTING, (float)0}, // tell player to stop shooting friend // { TASK_TLK_EYECONTACT, (float)0 },// look at the player }; Schedule_t slIdleStopShooting[] = { {tlIdleStopShooting, ARRAYSIZE(tlIdleStopShooting), bits_COND_NEW_ENEMY | bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE | bits_COND_HEAR_SOUND, 0, "Idle Stop Shooting"}, }; Task_t tlMoveAway[] = { {TASK_SET_FAIL_SCHEDULE, (float)SCHED_MOVE_AWAY_FAIL}, {TASK_STORE_LASTPOSITION, (float)0}, {TASK_MOVE_AWAY_PATH, (float)100}, {TASK_WALK_PATH_FOR_UNITS, (float)100}, {TASK_STOP_MOVING, (float)0}, {TASK_FACE_PLAYER, (float)0.5}, }; Schedule_t slMoveAway[] = { {tlMoveAway, ARRAYSIZE(tlMoveAway), 0, 0, "MoveAway"}, }; Task_t tlMoveAwayFail[] = { {TASK_STOP_MOVING, (float)0}, {TASK_FACE_PLAYER, (float)0.5}, }; Schedule_t slMoveAwayFail[] = { {tlMoveAwayFail, ARRAYSIZE(tlMoveAwayFail), 0, 0, "MoveAwayFail"}, }; Task_t tlMoveAwayFollow[] = { {TASK_SET_FAIL_SCHEDULE, (float)SCHED_TARGET_FACE}, {TASK_STORE_LASTPOSITION, (float)0}, {TASK_MOVE_AWAY_PATH, (float)100}, {TASK_WALK_PATH_FOR_UNITS, (float)100}, {TASK_STOP_MOVING, (float)0}, {TASK_SET_SCHEDULE, (float)SCHED_TARGET_FACE}, }; Schedule_t slMoveAwayFollow[] = { {tlMoveAwayFollow, ARRAYSIZE(tlMoveAwayFollow), 0, 0, "MoveAwayFollow"}, }; Task_t tlTlkIdleWatchClient[] = { {TASK_STOP_MOVING, 0}, {TASK_SET_ACTIVITY, (float)ACT_IDLE}, {TASK_TLK_LOOK_AT_CLIENT, (float)6}, }; Task_t tlTlkIdleWatchClientStare[] = { {TASK_STOP_MOVING, 0}, {TASK_SET_ACTIVITY, (float)ACT_IDLE}, {TASK_TLK_CLIENT_STARE, (float)6}, {TASK_TLK_STARE, (float)0}, {TASK_TLK_IDEALYAW, (float)0}, // look at who I'm talking to {TASK_FACE_IDEAL, (float)0}, {TASK_SET_ACTIVITY, (float)ACT_SIGNAL3}, {TASK_TLK_EYECONTACT, (float)0}, }; Schedule_t slTlkIdleWatchClient[] = { {tlTlkIdleWatchClient, ARRAYSIZE(tlTlkIdleWatchClient), 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_CLIENT_UNSEEN | 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, "TlkIdleWatchClient"}, {tlTlkIdleWatchClientStare, ARRAYSIZE(tlTlkIdleWatchClientStare), 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_CLIENT_UNSEEN | 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, "TlkIdleWatchClientStare"}, }; Task_t tlTlkIdleEyecontact[] = { {TASK_TLK_IDEALYAW, (float)0}, // look at who I'm talking to {TASK_FACE_IDEAL, (float)0}, {TASK_SET_ACTIVITY, (float)ACT_SIGNAL3}, {TASK_TLK_EYECONTACT, (float)0}, // Wait until speaker is done }; Schedule_t slTlkIdleEyecontact[] = { {tlTlkIdleEyecontact, ARRAYSIZE(tlTlkIdleEyecontact), bits_COND_NEW_ENEMY | bits_COND_CLIENT_PUSH | bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE, 0, "TlkIdleEyecontact"}, }; DEFINE_CUSTOM_SCHEDULES(CTalkMonster){ slIdleResponse, slIdleSpeak, slIdleHello, slIdleSpeakWait, slIdleStopShooting, slMoveAway, slMoveAwayFollow, slMoveAwayFail, slTlkIdleWatchClient, &slTlkIdleWatchClient[1], slTlkIdleEyecontact, }; IMPLEMENT_CUSTOM_SCHEDULES(CTalkMonster, CBaseMonster); void CTalkMonster::SetActivity(Activity newActivity) { if (newActivity == ACT_IDLE && IsTalking()) newActivity = ACT_SIGNAL3; if (newActivity == ACT_SIGNAL3 && (LookupActivity(ACT_SIGNAL3) == ACTIVITY_NOT_AVAILABLE)) newActivity = ACT_IDLE; CBaseMonster::SetActivity(newActivity); } void CTalkMonster::StartTask(Task_t* pTask) { switch (pTask->iTask) { case TASK_TLK_SPEAK: // ask question or make statement FIdleSpeak(); TaskComplete(); break; case TASK_TLK_RESPOND: // respond to question IdleRespond(); TaskComplete(); break; case TASK_TLK_HELLO: // greet player FIdleHello(); TaskComplete(); break; case TASK_TLK_STARE: // let the player know I know he's staring at me. FIdleStare(); TaskComplete(); break; case TASK_FACE_PLAYER: case TASK_TLK_LOOK_AT_CLIENT: case TASK_TLK_CLIENT_STARE: // track head to the client for a while. m_flWaitFinished = gpGlobals->time + pTask->flData; break; case TASK_TLK_EYECONTACT: break; case TASK_TLK_IDEALYAW: if (m_hTalkTarget != NULL) { pev->yaw_speed = 60; float yaw = VecToYaw(m_hTalkTarget->pev->origin - pev->origin) - pev->angles.y; if (yaw > 180) yaw -= 360; if (yaw < -180) yaw += 360; if (yaw < 0) { pev->ideal_yaw = V_min(yaw + 45.0f, 0.0f) + pev->angles.y; } else { pev->ideal_yaw = V_max(yaw - 45.0f, 0.0f) + pev->angles.y; } } TaskComplete(); break; case TASK_TLK_HEADRESET: // reset head position after looking at something m_hTalkTarget = NULL; TaskComplete(); break; case TASK_TLK_STOPSHOOTING: // tell player to stop shooting PlaySentence(m_szGrp[TLK_NOSHOOT], RANDOM_FLOAT(2.8, 3.2), VOL_NORM, ATTN_NORM); TaskComplete(); break; case TASK_CANT_FOLLOW: StopFollowing(false); PlaySentence(m_szGrp[TLK_STOP], RANDOM_FLOAT(2, 2.5), VOL_NORM, ATTN_NORM); TaskComplete(); break; case TASK_WALK_PATH_FOR_UNITS: m_movementActivity = ACT_WALK; break; case TASK_MOVE_AWAY_PATH: { Vector dir = pev->angles; dir.y = pev->ideal_yaw + 180; Vector move; UTIL_MakeVectorsPrivate(dir, move, NULL, NULL); dir = pev->origin + move * pTask->flData; if (MoveToLocation(ACT_WALK, 2, dir)) { TaskComplete(); } else if (FindCover(pev->origin, pev->view_ofs, 0, CoverRadius())) { // then try for plain ole cover m_flMoveWaitFinished = gpGlobals->time + 2; TaskComplete(); } else { // nowhere to go? TaskFail(); } } break; case TASK_PLAY_SCRIPT: m_hTalkTarget = NULL; CBaseMonster::StartTask(pTask); break; default: CBaseMonster::StartTask(pTask); } } void CTalkMonster::RunTask(Task_t* pTask) { switch (pTask->iTask) { case TASK_TLK_CLIENT_STARE: case TASK_TLK_LOOK_AT_CLIENT: { // Get edict for one player CBaseEntity* pPlayer = UTIL_GetLocalPlayer(); // track head to the client for a while. if (pPlayer && m_MonsterState == MONSTERSTATE_IDLE && !IsMoving() && !IsTalking()) { IdleHeadTurn(pPlayer->pev->origin); } else { // started moving or talking TaskFail(); return; } if (pTask->iTask == TASK_TLK_CLIENT_STARE) { // fail out if the player looks away or moves away. if ((pPlayer->pev->origin - pev->origin).Length2D() > TLK_STARE_DIST) { // player moved away. TaskFail(); } UTIL_MakeVectors(pPlayer->pev->angles); if (UTIL_DotPoints(pPlayer->pev->origin, pev->origin, gpGlobals->v_forward) < m_flFieldOfView) { // player looked away TaskFail(); } } if (gpGlobals->time > m_flWaitFinished) { TaskComplete(); } break; } case TASK_FACE_PLAYER: { // Get edict for one player CBaseEntity* pPlayer = UTIL_GetLocalPlayer(); if (pPlayer) { MakeIdealYaw(pPlayer->pev->origin); ChangeYaw(pev->yaw_speed); IdleHeadTurn(pPlayer->pev->origin); if (gpGlobals->time > m_flWaitFinished && FlYawDiff() < 10) { TaskComplete(); } } else { TaskFail(); } } break; case TASK_TLK_EYECONTACT: if (!IsMoving() && IsTalking() && m_hTalkTarget != NULL) { // ALERT( at_console, "waiting %f\n", m_flStopTalkTime - gpGlobals->time ); IdleHeadTurn(m_hTalkTarget->pev->origin); } else { TaskComplete(); } break; case TASK_WALK_PATH_FOR_UNITS: { float distance; distance = (m_vecLastPosition - pev->origin).Length2D(); // Walk path until far enough away if (distance > pTask->flData || MovementIsComplete()) { TaskComplete(); RouteClear(); // Stop moving } } break; case TASK_WAIT_FOR_MOVEMENT: if (IsTalking() && m_hTalkTarget != NULL) { // ALERT(at_console, "walking, talking\n"); IdleHeadTurn(m_hTalkTarget->pev->origin); } else { IdleHeadTurn(pev->origin); // override so that during walk, a scientist may talk and greet player FIdleHello(); if (RANDOM_LONG(0, m_nSpeak * 20) == 0) { FIdleSpeak(); } } CBaseMonster::RunTask(pTask); if (TaskIsComplete()) IdleHeadTurn(pev->origin); break; default: if (IsTalking() && m_hTalkTarget != NULL) { IdleHeadTurn(m_hTalkTarget->pev->origin); } else { SetBoneController(0, 0); } CBaseMonster::RunTask(pTask); } } void CTalkMonster::Killed(entvars_t* pevAttacker, int iGib) { // If a client killed me (unless I was already Barnacle'd), make everyone else mad/afraid of him if ((pevAttacker->flags & FL_CLIENT) != 0 && m_MonsterState != MONSTERSTATE_PRONE) { AlertFriends(); LimitFollowers(CBaseEntity::Instance(pevAttacker), 0); } m_hTargetEnt = NULL; // Don't finish that sentence StopTalking(); SetUse(NULL); CBaseMonster::Killed(pevAttacker, iGib); } CBaseEntity* CTalkMonster::EnumFriends(CBaseEntity* pPrevious, int listNumber, bool bTrace) { CBaseEntity* pFriend = pPrevious; const char* pszFriend; TraceResult tr; Vector vecCheck; pszFriend = m_szFriends[FriendNumber(listNumber)]; while (pFriend = UTIL_FindEntityByClassname(pFriend, pszFriend)) { if (pFriend == this || !pFriend->IsAlive()) // don't talk to self or dead people continue; if (bTrace) { vecCheck = pFriend->pev->origin; vecCheck.z = pFriend->pev->absmax.z; UTIL_TraceLine(pev->origin, vecCheck, ignore_monsters, ENT(pev), &tr); } else tr.flFraction = 1.0; if (tr.flFraction == 1.0) { return pFriend; } } return NULL; } void CTalkMonster::AlertFriends() { CBaseEntity* pFriend = NULL; int i; // for each friend in this bsp... for (i = 0; i < TLK_CFRIENDS; i++) { while (pFriend = EnumFriends(pFriend, i, true)) { CBaseMonster* pMonster = pFriend->MyMonsterPointer(); if (pMonster->IsAlive()) { // don't provoke a friend that's playing a death animation. They're a goner pMonster->m_afMemory |= bits_MEMORY_PROVOKED; } } } } void CTalkMonster::ShutUpFriends() { CBaseEntity* pFriend = NULL; int i; // for each friend in this bsp... for (i = 0; i < TLK_CFRIENDS; i++) { while (pFriend = EnumFriends(pFriend, i, true)) { CBaseMonster* pMonster = pFriend->MyMonsterPointer(); if (pMonster) { pMonster->SentenceStop(); } } } } // UNDONE: Keep a follow time in each follower, make a list of followers in this function and do LRU // UNDONE: Check this in Restore to keep restored monsters from joining a full list of followers void CTalkMonster::LimitFollowers(CBaseEntity* pPlayer, int maxFollowers) { CBaseEntity* pFriend = NULL; int i, count; count = 0; // for each friend in this bsp... for (i = 0; i < TLK_CFRIENDS; i++) { while (pFriend = EnumFriends(pFriend, i, false)) { CBaseMonster* pMonster = pFriend->MyMonsterPointer(); if (pMonster) { if (pMonster->m_hTargetEnt == pPlayer) { count++; if (count > maxFollowers) pMonster->StopFollowing(true); } } } } } float CTalkMonster::TargetDistance() { // If we lose the player, or he dies, return a really large distance if (m_hTargetEnt == NULL || !m_hTargetEnt->IsAlive()) return 1e6; return (m_hTargetEnt->pev->origin - pev->origin).Length(); } //========================================================= // HandleAnimEvent - catches the monster-specific messages // that occur when tagged animation frames are played. //========================================================= void CTalkMonster::HandleAnimEvent(MonsterEvent_t* pEvent) { switch (pEvent->event) { case SCRIPT_EVENT_SENTENCE_RND1: // Play a named sentence group 25% of the time if (RANDOM_LONG(0, 99) < 75) break; [[fallthrough]]; case SCRIPT_EVENT_SENTENCE: // Play a named sentence group ShutUpFriends(); PlaySentence(pEvent->options, RANDOM_FLOAT(2.8, 3.4), VOL_NORM, ATTN_IDLE); //ALERT(at_console, "script event speak\n"); break; default: CBaseMonster::HandleAnimEvent(pEvent); break; } } // monsters derived from ctalkmonster should call this in precache() void CTalkMonster::TalkInit() { // every new talking monster must reset this global, otherwise // when a level is loaded, nobody will talk (time is reset to 0) CTalkMonster::g_talkWaitTime = 0; m_voicePitch = 100; } //========================================================= // FindNearestFriend // Scan for nearest, visible friend. If fPlayer is true, look for // nearest player //========================================================= CBaseEntity* CTalkMonster::FindNearestFriend(bool fPlayer) { CBaseEntity* pFriend = NULL; CBaseEntity* pNearest = NULL; float range = 10000000.0; TraceResult tr; Vector vecStart = pev->origin; Vector vecCheck; int i; const char* pszFriend; int cfriends; vecStart.z = pev->absmax.z; if (fPlayer) cfriends = 1; else cfriends = TLK_CFRIENDS; // for each type of friend... for (i = cfriends - 1; i > -1; i--) { if (fPlayer) pszFriend = "player"; else pszFriend = m_szFriends[FriendNumber(i)]; if (!pszFriend) continue; // for each friend in this bsp... while (pFriend = UTIL_FindEntityByClassname(pFriend, pszFriend)) { if (pFriend == this || !pFriend->IsAlive()) // don't talk to self or dead people continue; CBaseMonster* pMonster = pFriend->MyMonsterPointer(); // If not a monster for some reason, or in a script, or prone if (!pMonster || pMonster->m_MonsterState == MONSTERSTATE_SCRIPT || pMonster->m_MonsterState == MONSTERSTATE_PRONE) continue; vecCheck = pFriend->pev->origin; vecCheck.z = pFriend->pev->absmax.z; // if closer than previous friend, and in range, see if he's visible if (range > (vecStart - vecCheck).Length()) { UTIL_TraceLine(vecStart, vecCheck, ignore_monsters, ENT(pev), &tr); if (tr.flFraction == 1.0) { // visible and in range, this is the new nearest scientist if ((vecStart - vecCheck).Length() < TALKRANGE_MIN) { pNearest = pFriend; range = (vecStart - vecCheck).Length(); } } } } } return pNearest; } int CTalkMonster::GetVoicePitch() { return m_voicePitch + RANDOM_LONG(0, 3); } void CTalkMonster::Touch(CBaseEntity* pOther) { // Did the player touch me? if (pOther->IsPlayer()) { // Ignore if pissed at player if ((m_afMemory & bits_MEMORY_PROVOKED) != 0) return; // Stay put during speech if (IsTalking()) return; // Heuristic for determining if the player is pushing me away float speed = fabs(pOther->pev->velocity.x) + fabs(pOther->pev->velocity.y); if (speed > 50) { // From https://github.com/FreeSlave/halflife-updated/wiki/Fix-potential-incorrect-facing-in-scripted-sequence if (m_pSchedule != NULL && (m_pSchedule->iInterruptMask & bits_COND_CLIENT_PUSH) != 0) { SetConditions(bits_COND_CLIENT_PUSH); MakeIdealYaw(pOther->pev->origin); } } } } //========================================================= // IdleRespond // Respond to a previous question //========================================================= void CTalkMonster::IdleRespond() { int pitch = GetVoicePitch(); // play response PlaySentence(m_szGrp[TLK_ANSWER], RANDOM_FLOAT(2.8, 3.2), VOL_NORM, ATTN_IDLE); } bool CTalkMonster::FOkToSpeak() { // if in the grip of a barnacle, don't speak if (m_MonsterState == MONSTERSTATE_PRONE || m_IdealMonsterState == MONSTERSTATE_PRONE) { return false; } // if not alive, certainly don't speak if (pev->deadflag != DEAD_NO) { return false; } // if someone else is talking, don't speak if (gpGlobals->time <= CTalkMonster::g_talkWaitTime) return false; if ((pev->spawnflags & SF_MONSTER_GAG) != 0) return false; if (m_MonsterState == MONSTERSTATE_PRONE) return false; // if player is not in pvs, don't speak if (!IsAlive() || FNullEnt(FIND_CLIENT_IN_PVS(edict()))) return false; // don't talk if you're in combat if (m_hEnemy != NULL && FVisible(m_hEnemy)) return false; return true; } bool CTalkMonster::CanPlaySentence(bool fDisregardState) { if (fDisregardState) return CBaseMonster::CanPlaySentence(fDisregardState); return FOkToSpeak(); } //========================================================= // FIdleStare //========================================================= bool CTalkMonster::FIdleStare() { if (!FOkToSpeak()) return false; PlaySentence(m_szGrp[TLK_STARE], RANDOM_FLOAT(5, 7.5), VOL_NORM, ATTN_IDLE); m_hTalkTarget = FindNearestFriend(true); return true; } //========================================================= // IdleHello // Try to greet player first time he's seen //========================================================= bool CTalkMonster::FIdleHello() { if (!FOkToSpeak()) return false; // if this is first time scientist has seen player, greet him if (!FBitSet(m_bitsSaid, bit_saidHelloPlayer)) { // get a player CBaseEntity* pPlayer = FindNearestFriend(true); if (pPlayer) { if (FInViewCone(pPlayer) && FVisible(pPlayer)) { m_hTalkTarget = pPlayer; if (FBitSet(pev->spawnflags, SF_MONSTER_PREDISASTER)) PlaySentence(m_szGrp[TLK_PHELLO], RANDOM_FLOAT(3, 3.5), VOL_NORM, ATTN_IDLE); else PlaySentence(m_szGrp[TLK_HELLO], RANDOM_FLOAT(3, 3.5), VOL_NORM, ATTN_IDLE); SetBits(m_bitsSaid, bit_saidHelloPlayer); return true; } } } return false; } // turn head towards supplied origin void CTalkMonster::IdleHeadTurn(Vector& vecFriend) { // turn head in desired direction only if ent has a turnable head if ((m_afCapability & bits_CAP_TURN_HEAD) != 0) { float yaw = VecToYaw(vecFriend - pev->origin) - pev->angles.y; if (yaw > 180) yaw -= 360; if (yaw < -180) yaw += 360; // turn towards vector SetBoneController(0, yaw); } } //========================================================= // FIdleSpeak // ask question of nearby friend, or make statement //========================================================= bool CTalkMonster::FIdleSpeak() { // try to start a conversation, or make statement int pitch; const char* szIdleGroup; const char* szQuestionGroup; float duration; if (!FOkToSpeak()) return false; // set idle groups based on pre/post disaster if (FBitSet(pev->spawnflags, SF_MONSTER_PREDISASTER)) { szIdleGroup = m_szGrp[TLK_PIDLE]; szQuestionGroup = m_szGrp[TLK_PQUESTION]; // set global min delay for next conversation duration = RANDOM_FLOAT(4.8, 5.2); } else { szIdleGroup = m_szGrp[TLK_IDLE]; szQuestionGroup = m_szGrp[TLK_QUESTION]; // set global min delay for next conversation duration = RANDOM_FLOAT(2.8, 3.2); } pitch = GetVoicePitch(); // player using this entity is alive and wounded? CBaseEntity* pTarget = m_hTargetEnt; if (pTarget != NULL) { if (pTarget->IsPlayer()) { if (pTarget->IsAlive()) { m_hTalkTarget = m_hTargetEnt; if (!FBitSet(m_bitsSaid, bit_saidDamageHeavy) && (m_hTargetEnt->pev->health <= m_hTargetEnt->pev->max_health / 8)) { //EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, m_szGrp[TLK_PLHURT3], 1.0, ATTN_IDLE, 0, pitch); PlaySentence(m_szGrp[TLK_PLHURT3], duration, VOL_NORM, ATTN_IDLE); SetBits(m_bitsSaid, bit_saidDamageHeavy); return true; } else if (!FBitSet(m_bitsSaid, bit_saidDamageMedium) && (m_hTargetEnt->pev->health <= m_hTargetEnt->pev->max_health / 4)) { //EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, m_szGrp[TLK_PLHURT2], 1.0, ATTN_IDLE, 0, pitch); PlaySentence(m_szGrp[TLK_PLHURT2], duration, VOL_NORM, ATTN_IDLE); SetBits(m_bitsSaid, bit_saidDamageMedium); return true; } else if (!FBitSet(m_bitsSaid, bit_saidDamageLight) && (m_hTargetEnt->pev->health <= m_hTargetEnt->pev->max_health / 2)) { //EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, m_szGrp[TLK_PLHURT1], 1.0, ATTN_IDLE, 0, pitch); PlaySentence(m_szGrp[TLK_PLHURT1], duration, VOL_NORM, ATTN_IDLE); SetBits(m_bitsSaid, bit_saidDamageLight); return true; } } else { //!!!KELLY - here's a cool spot to have the talkmonster talk about the dead player if we want. // "Oh dear, Gordon Freeman is dead!" -Scientist // "Damn, I can't do this without you." -Barney } } } // if there is a friend nearby to speak to, play sentence, set friend's response time, return CBaseEntity* pFriend = FindNearestFriend(false); if (pFriend && !(pFriend->IsMoving()) && (RANDOM_LONG(0, 99) < 75)) { PlaySentence(szQuestionGroup, duration, VOL_NORM, ATTN_IDLE); //SENTENCEG_PlayRndSz( ENT(pev), szQuestionGroup, 1.0, ATTN_IDLE, 0, pitch ); // force friend to answer CTalkMonster* pTalkMonster = (CTalkMonster*)pFriend; m_hTalkTarget = pFriend; pTalkMonster->SetAnswerQuestion(this); // UNDONE: This is EVIL!!! pTalkMonster->m_flStopTalkTime = m_flStopTalkTime; m_nSpeak++; return true; } // otherwise, play an idle statement, try to face client when making a statement. if (RANDOM_LONG(0, 1)) { //SENTENCEG_PlayRndSz( ENT(pev), szIdleGroup, 1.0, ATTN_IDLE, 0, pitch ); CBaseEntity* pFriend = FindNearestFriend(true); if (pFriend) { m_hTalkTarget = pFriend; PlaySentence(szIdleGroup, duration, VOL_NORM, ATTN_IDLE); m_nSpeak++; return true; } } // didn't speak Talk(0); CTalkMonster::g_talkWaitTime = 0; return false; } void CTalkMonster::PlayScriptedSentence(const char* pszSentence, float duration, float volume, float attenuation, bool bConcurrent, CBaseEntity* pListener) { if (!bConcurrent) ShutUpFriends(); ClearConditions(bits_COND_CLIENT_PUSH); // Forget about moving! I've got something to say! m_useTime = gpGlobals->time + duration; PlaySentence(pszSentence, duration, volume, attenuation); m_hTalkTarget = pListener; } void CTalkMonster::PlaySentenceCore(const char* pszSentence, float duration, float volume, float attenuation) { Talk(duration); CTalkMonster::g_talkWaitTime = gpGlobals->time + duration + 2.0; if (pszSentence[0] == '!') EMIT_SOUND_DYN(edict(), CHAN_VOICE, pszSentence, volume, attenuation, 0, GetVoicePitch()); else SENTENCEG_PlayRndSz(edict(), pszSentence, volume, attenuation, 0, GetVoicePitch()); // If you say anything, don't greet the player - you may have already spoken to them SetBits(m_bitsSaid, bit_saidHelloPlayer); } //========================================================= // Talk - set a timer that tells us when the monster is done // talking. //========================================================= void CTalkMonster::Talk(float flDuration) { if (flDuration <= 0) { // no duration :( m_flStopTalkTime = gpGlobals->time + 3; } else { m_flStopTalkTime = gpGlobals->time + flDuration; } } // Prepare this talking monster to answer question void CTalkMonster::SetAnswerQuestion(CTalkMonster* pSpeaker) { if (!m_pCine) ChangeSchedule(slIdleResponse); m_hTalkTarget = (CBaseMonster*)pSpeaker; } bool CTalkMonster::TakeDamage(entvars_t* pevInflictor, entvars_t* pevAttacker, float flDamage, int bitsDamageType) { if (IsAlive()) { // if player damaged this entity, have other friends talk about it if (pevAttacker && m_MonsterState != MONSTERSTATE_PRONE && FBitSet(pevAttacker->flags, FL_CLIENT)) { CBaseEntity* pFriend = FindNearestFriend(false); if (pFriend && pFriend->IsAlive() && pFriend->pev->deadflag == DEAD_NO) { // only if not dead or dying! CTalkMonster* pTalkMonster = (CTalkMonster*)pFriend; pTalkMonster->ChangeSchedule(slIdleStopShooting); } } } return CBaseMonster::TakeDamage(pevInflictor, pevAttacker, flDamage, bitsDamageType); } Schedule_t* CTalkMonster::GetScheduleOfType(int Type) { switch (Type) { case SCHED_MOVE_AWAY: return slMoveAway; case SCHED_MOVE_AWAY_FOLLOW: return slMoveAwayFollow; case SCHED_MOVE_AWAY_FAIL: return slMoveAwayFail; case SCHED_TARGET_FACE: // speak during 'use' if (RANDOM_LONG(0, 99) < 2) //ALERT ( at_console, "target chase speak\n" ); return slIdleSpeakWait; else return slIdleStand; case SCHED_IDLE_STAND: { // if never seen player, try to greet him if (!FBitSet(m_bitsSaid, bit_saidHelloPlayer)) { return slIdleHello; } // sustained light wounds? if (!FBitSet(m_bitsSaid, bit_saidWoundLight) && (pev->health <= (pev->max_health * 0.75))) { //SENTENCEG_PlayRndSz( ENT(pev), m_szGrp[TLK_WOUND], 1.0, ATTN_IDLE, 0, GetVoicePitch() ); //CTalkMonster::g_talkWaitTime = gpGlobals->time + RANDOM_FLOAT(2.8, 3.2); PlaySentence(m_szGrp[TLK_WOUND], RANDOM_FLOAT(2.8, 3.2), VOL_NORM, ATTN_IDLE); SetBits(m_bitsSaid, bit_saidWoundLight); return slIdleStand; } // sustained heavy wounds? else if (!FBitSet(m_bitsSaid, bit_saidWoundHeavy) && (pev->health <= (pev->max_health * 0.5))) { //SENTENCEG_PlayRndSz( ENT(pev), m_szGrp[TLK_MORTAL], 1.0, ATTN_IDLE, 0, GetVoicePitch() ); //CTalkMonster::g_talkWaitTime = gpGlobals->time + RANDOM_FLOAT(2.8, 3.2); PlaySentence(m_szGrp[TLK_MORTAL], RANDOM_FLOAT(2.8, 3.2), VOL_NORM, ATTN_IDLE); SetBits(m_bitsSaid, bit_saidWoundHeavy); return slIdleStand; } // talk about world if (FOkToSpeak() && RANDOM_LONG(0, m_nSpeak * 2) == 0) { //ALERT ( at_console, "standing idle speak\n" ); return slIdleSpeak; } if (!IsTalking() && HasConditions(bits_COND_SEE_CLIENT) && RANDOM_LONG(0, 6) == 0) { CBaseEntity* pPlayer = UTIL_GetLocalPlayer(); if (pPlayer) { // watch the client. UTIL_MakeVectors(pPlayer->pev->angles); if ((pPlayer->pev->origin - pev->origin).Length2D() < TLK_STARE_DIST && UTIL_DotPoints(pPlayer->pev->origin, pev->origin, gpGlobals->v_forward) >= m_flFieldOfView) { // go into the special STARE schedule if the player is close, and looking at me too. return &slTlkIdleWatchClient[1]; } return slTlkIdleWatchClient; } } else { if (IsTalking()) // look at who we're talking to return slTlkIdleEyecontact; else // regular standing idle return slIdleStand; } // NOTE - caller must first CTalkMonster::GetScheduleOfType, // then check result and decide what to return ie: if sci gets back // slIdleStand, return slIdleSciStand } break; } return CBaseMonster::GetScheduleOfType(Type); } //========================================================= // IsTalking - am I saying a sentence right now? //========================================================= bool CTalkMonster::IsTalking() { if (m_flStopTalkTime > gpGlobals->time) { return true; } return false; } //========================================================= // If there's a player around, watch him. //========================================================= void CTalkMonster::PrescheduleThink() { if (!HasConditions(bits_COND_SEE_CLIENT)) { SetConditions(bits_COND_CLIENT_UNSEEN); } } // try to smell something void CTalkMonster::TrySmellTalk() { if (!FOkToSpeak()) return; // clear smell bits periodically if (gpGlobals->time > m_flLastSaidSmelled) { // ALERT ( at_aiconsole, "Clear smell bits\n" ); ClearBits(m_bitsSaid, bit_saidSmelled); } // smelled something? if (!FBitSet(m_bitsSaid, bit_saidSmelled) && HasConditions(bits_COND_SMELL)) { PlaySentence(m_szGrp[TLK_SMELL], RANDOM_FLOAT(2.8, 3.2), VOL_NORM, ATTN_IDLE); m_flLastSaidSmelled = gpGlobals->time + 60; // don't talk about the stinky for a while. SetBits(m_bitsSaid, bit_saidSmelled); } } int CTalkMonster::IRelationship(CBaseEntity* pTarget) { if (pTarget->IsPlayer()) if ((m_afMemory & bits_MEMORY_PROVOKED) != 0) return R_HT; return CBaseMonster::IRelationship(pTarget); } void CTalkMonster::StopFollowing(bool clearSchedule) { if (IsFollowing()) { if ((m_afMemory & bits_MEMORY_PROVOKED) == 0) { PlaySentence(m_szGrp[TLK_UNUSE], RANDOM_FLOAT(2.8, 3.2), VOL_NORM, ATTN_IDLE); m_hTalkTarget = m_hTargetEnt; } if (m_movementGoal == MOVEGOAL_TARGETENT) RouteClear(); // Stop him from walking toward the player m_hTargetEnt = NULL; if (clearSchedule) ClearSchedule(); if (m_hEnemy != NULL) m_IdealMonsterState = MONSTERSTATE_COMBAT; } } void CTalkMonster::StartFollowing(CBaseEntity* pLeader) { if (m_pCine) m_pCine->CancelScript(); if (m_hEnemy != NULL) m_IdealMonsterState = MONSTERSTATE_ALERT; m_hTargetEnt = pLeader; PlaySentence(m_szGrp[TLK_USE], RANDOM_FLOAT(2.8, 3.2), VOL_NORM, ATTN_IDLE); m_hTalkTarget = m_hTargetEnt; ClearConditions(bits_COND_CLIENT_PUSH); ClearSchedule(); } bool CTalkMonster::CanFollow() { if (m_MonsterState == MONSTERSTATE_SCRIPT || m_IdealMonsterState == MONSTERSTATE_SCRIPT) { // It's possible for m_MonsterState to still be MONSTERSTATE_SCRIPT when the script has already ended. // We'll treat a null pointer as an uninterruptable script and wait for the NPC to change states // before allowing players to make them follow them again. if (!m_pCine || !m_pCine->CanInterrupt()) return false; } if (!IsAlive()) return false; return !IsFollowing(); } void CTalkMonster::FollowerUse(CBaseEntity* pActivator, CBaseEntity* pCaller, USE_TYPE useType, float value) { // Don't allow use during a scripted_sentence if (m_useTime > gpGlobals->time) return; if (pCaller != NULL && pCaller->IsPlayer()) { // Pre-disaster followers can't be used if ((pev->spawnflags & SF_MONSTER_PREDISASTER) != 0) { DeclineFollowing(); } else if (CanFollow()) { LimitFollowers(pCaller, 1); if ((m_afMemory & bits_MEMORY_PROVOKED) != 0) ALERT(at_console, "I'm not following you, you evil person!\n"); else { StartFollowing(pCaller); SetBits(m_bitsSaid, bit_saidHelloPlayer); // Don't say hi after you've started following } } else { StopFollowing(true); } } } bool CTalkMonster::KeyValue(KeyValueData* pkvd) { if (FStrEq(pkvd->szKeyName, "UseSentence")) { m_iszUse = ALLOC_STRING(pkvd->szValue); return true; } else if (FStrEq(pkvd->szKeyName, "UnUseSentence")) { m_iszUnUse = ALLOC_STRING(pkvd->szValue); return true; } return CBaseMonster::KeyValue(pkvd); } void CTalkMonster::Precache() { if (!FStringNull(m_iszUse)) m_szGrp[TLK_USE] = STRING(m_iszUse); if (!FStringNull(m_iszUnUse)) m_szGrp[TLK_UNUSE] = STRING(m_iszUnUse); }