diff --git a/sp/src/game/server/ai_basenpc.cpp b/sp/src/game/server/ai_basenpc.cpp index 5b3fab8b..002f3f36 100644 --- a/sp/src/game/server/ai_basenpc.cpp +++ b/sp/src/game/server/ai_basenpc.cpp @@ -671,13 +671,27 @@ void CAI_BaseNPC::Ignite( float flFlameLifetime, bool bNPCOnly, float flSize, bo { BaseClass::Ignite( flFlameLifetime, bNPCOnly, flSize, bCalledByLevelDesigner ); +#ifdef MAPBASE + // Alyx's enemy ignited code from below can now be run on any NPC as long as + // it's our current enemy. + if ( GetEnemy() && GetEnemy()->IsNPC() ) + { + GetEnemy()->MyNPCPointer()->EnemyIgnited( this ); + } +#endif + #ifdef HL2_EPISODIC CBasePlayer *pPlayer = AI_GetSinglePlayer(); if ( pPlayer && pPlayer->IRelationType( this ) != D_LI ) { CNPC_Alyx *alyx = CNPC_Alyx::GetAlyx(); +#ifdef MAPBASE + // Alyx's code continues to run if Alyx was not this NPC's enemy. + if ( alyx && alyx != GetEnemy() ) +#else if ( alyx ) +#endif { alyx->EnemyIgnited( this ); } @@ -16476,6 +16490,21 @@ void CAI_BaseNPC::ModifyOrAppendEnemyCriteria( AI_CriteriaSet& set, CBaseEntity set.AppendCriteria( "enemyclass", g_pGameRules->AIClassText( pEnemy->Classify() ) ); // UTIL_VarArgs("%i", pEnemy->Classify()) set.AppendCriteria( "distancetoenemy", UTIL_VarArgs( "%f", EnemyDistance(pEnemy) ) ); set.AppendCriteria( "timesincecombat", "-1" ); + + CAI_BaseNPC *pNPC = pEnemy->MyNPCPointer(); + if (pNPC) + { + set.AppendCriteria("enemy_is_npc", "1"); + + set.AppendCriteria( "enemy_activity", CAI_BaseNPC::GetActivityName( pNPC->GetActivity() ) ); + set.AppendCriteria( "enemy_weapon", pNPC->GetActiveWeapon() ? pNPC->GetActiveWeapon()->GetClassname() : "0" ); + } + else + { + set.AppendCriteria("enemy_is_npc", "0"); + } + + pEnemy->AppendContextToCriteria( set, "enemy_" ); } else { diff --git a/sp/src/game/server/ai_basenpc.h b/sp/src/game/server/ai_basenpc.h index 81150cce..dbc229e2 100644 --- a/sp/src/game/server/ai_basenpc.h +++ b/sp/src/game/server/ai_basenpc.h @@ -1995,6 +1995,9 @@ public: //--------------------------------- virtual void Ignite( float flFlameLifetime, bool bNPCOnly = true, float flSize = 0.0f, bool bCalledByLevelDesigner = false ); +#ifdef MAPBASE + virtual void EnemyIgnited( CAI_BaseNPC *pVictim ) {} +#endif virtual bool PassesDamageFilter( const CTakeDamageInfo &info ); //--------------------------------- diff --git a/sp/src/game/server/ai_playerally.cpp b/sp/src/game/server/ai_playerally.cpp index f7594feb..39b49739 100644 --- a/sp/src/game/server/ai_playerally.cpp +++ b/sp/src/game/server/ai_playerally.cpp @@ -128,6 +128,12 @@ ConceptInfo_t g_ConceptInfos[] = // Passenger behaviour { TLK_PASSENGER_NEW_RADAR_CONTACT, SPEECH_IMPORTANT, -1, -1, -1, -1, -1, -1, AICF_DEFAULT, }, + +#ifdef MAPBASE + { TLK_TAKING_FIRE, SPEECH_IMPORTANT,-1, -1, -1, -1, -1, -1, AICF_DEFAULT, }, + { TLK_NEW_ENEMY, SPEECH_IMPORTANT,-1, -1, -1, -1, -1, -1, AICF_DEFAULT, }, + { TLK_COMBAT_IDLE, SPEECH_IMPORTANT,-1, -1, -1, -1, -1, -1, AICF_DEFAULT, }, +#endif }; //----------------------------------------------------------------------------- @@ -1259,6 +1265,38 @@ void CAI_PlayerAlly::OnKilledNPC( CBaseCombatCharacter *pKilled ) } } +#ifdef MAPBASE +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CAI_PlayerAlly::OnEnemyRangeAttackedMe( CBaseEntity *pEnemy, const Vector &vecDir, const Vector &vecEnd ) +{ + BaseClass::OnEnemyRangeAttackedMe( pEnemy, vecDir, vecEnd ); + + if ( IRelationType( pEnemy ) <= D_FR ) + { + AI_CriteriaSet modifiers; + ModifyOrAppendEnemyCriteria( modifiers, pEnemy ); + + Vector vecEntDir = (pEnemy->EyePosition() - EyePosition()); + float flDot = DotProduct( vecEntDir.Normalized(), vecDir ); + modifiers.AppendCriteria( "shot_dot", CNumStr( flDot ) ); + + if (GetLastDamageTime() == gpGlobals->curtime) + modifiers.AppendCriteria( "missed", "0" ); + else + modifiers.AppendCriteria( "missed", "1" ); + + // Check if they're out of ammo + if ( pEnemy->IsCombatCharacter() && pEnemy->MyCombatCharacterPointer()->GetActiveWeapon() && pEnemy->MyCombatCharacterPointer()->GetActiveWeapon()->Clip1() <= 0 ) + modifiers.AppendCriteria( "last_attack", "1" ); + else + modifiers.AppendCriteria( "last_attack", "0" ); + + SpeakIfAllowed( TLK_TAKING_FIRE, modifiers ); + } +} +#endif + //----------------------------------------------------------------------------- void CAI_PlayerAlly::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) { diff --git a/sp/src/game/server/ai_playerally.h b/sp/src/game/server/ai_playerally.h index 93448ec7..a1ff01c8 100644 --- a/sp/src/game/server/ai_playerally.h +++ b/sp/src/game/server/ai_playerally.h @@ -132,6 +132,13 @@ #define TLK_TGCATCHUP "TLK_TGCATCHUP" #define TLK_TGENDTOUR "TLK_TGENDTOUR" +#ifdef MAPBASE +// Additional concepts for companions in mods +#define TLK_TAKING_FIRE "TLK_TAKING_FIRE" // Someone fired at me (regardless of whether I was hit) +#define TLK_NEW_ENEMY "TLK_NEW_ENEMY" // A new enemy appeared while combat was already in progress +#define TLK_COMBAT_IDLE "TLK_COMBAT_IDLE" // Similar to TLK_ATTACKING, but specifically for when *not* currently attacking (e.g. when in cover or reloading) +#endif + //----------------------------------------------------------------------------- #define TALKRANGE_MIN 500.0 // don't talk to anyone farther away than this @@ -315,6 +322,10 @@ public: //--------------------------------- void OnKilledNPC( CBaseCombatCharacter *pKilled ); +#ifdef MAPBASE + void OnEnemyRangeAttackedMe( CBaseEntity *pEnemy, const Vector &vecDir, const Vector &vecEnd ); +#endif + //--------------------------------- // Damage handling //--------------------------------- diff --git a/sp/src/game/server/basecombatcharacter.h b/sp/src/game/server/basecombatcharacter.h index 22f07eb9..cdae243d 100644 --- a/sp/src/game/server/basecombatcharacter.h +++ b/sp/src/game/server/basecombatcharacter.h @@ -263,6 +263,10 @@ public: virtual bool CanBecomeServerRagdoll( void ) { return true; } +#ifdef MAPBASE + virtual void OnEnemyRangeAttackedMe( CBaseEntity *pEnemy, const Vector &vecDir, const Vector &vecEnd ) {} +#endif + // ----------------------- // Damage // ----------------------- diff --git a/sp/src/game/server/hl2/ai_behavior_functank.cpp b/sp/src/game/server/hl2/ai_behavior_functank.cpp index 89f79228..105bf6e2 100644 --- a/sp/src/game/server/hl2/ai_behavior_functank.cpp +++ b/sp/src/game/server/hl2/ai_behavior_functank.cpp @@ -190,6 +190,24 @@ int CAI_FuncTankBehavior::SelectSchedule() return SCHED_IDLE_STAND; } +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_FuncTankBehavior::ModifyOrAppendCriteria( AI_CriteriaSet &set ) +{ + BaseClass::ModifyOrAppendCriteria( set ); + +#ifdef MAPBASE + set.AppendCriteria( "ft_mounted", m_bMounted ? "1" : "0" ); + + if (m_hFuncTank) + { + set.AppendCriteria( "ft_classname", m_hFuncTank->GetClassname() ); + m_hFuncTank->AppendContextToCriteria( set, "ft_" ); + } +#endif +} + //----------------------------------------------------------------------------- // Purpose: // Input : activity - diff --git a/sp/src/game/server/hl2/ai_behavior_functank.h b/sp/src/game/server/hl2/ai_behavior_functank.h index 75d5b8df..54f91d1f 100644 --- a/sp/src/game/server/hl2/ai_behavior_functank.h +++ b/sp/src/game/server/hl2/ai_behavior_functank.h @@ -55,6 +55,8 @@ public: bool CanManTank( CFuncTank *pTank, bool bForced ); #endif + void ModifyOrAppendCriteria( AI_CriteriaSet &set ); + Activity NPC_TranslateActivity( Activity activity ); // Conditions: diff --git a/sp/src/game/server/hl2/npc_BaseZombie.cpp b/sp/src/game/server/hl2/npc_BaseZombie.cpp index dc60673b..824f6bf1 100644 --- a/sp/src/game/server/hl2/npc_BaseZombie.cpp +++ b/sp/src/game/server/hl2/npc_BaseZombie.cpp @@ -2608,6 +2608,9 @@ void CNPC_BaseZombie::ReleaseHeadcrab( const Vector &vecOrigin, const Vector &ve // Inherit some misc. properties pCrab->m_bForceServerRagdoll = m_bForceServerRagdoll; pCrab->m_iViewHideFlags = m_iViewHideFlags; + + // Add response context for companion response (more reliable than checking for post-death zombie entity) + pCrab->AddContext( "from_zombie", "1", 2.0f ); #endif // make me the crab's owner to avoid collision issues diff --git a/sp/src/game/server/hl2/npc_alyx_episodic.cpp b/sp/src/game/server/hl2/npc_alyx_episodic.cpp index ee8b197c..dd5a35b0 100644 --- a/sp/src/game/server/hl2/npc_alyx_episodic.cpp +++ b/sp/src/game/server/hl2/npc_alyx_episodic.cpp @@ -1092,10 +1092,14 @@ void CNPC_Alyx::Event_KilledOther( CBaseEntity *pVictim, const CTakeDamageInfo & //----------------------------------------------------------------------------- void CNPC_Alyx::EnemyIgnited( CAI_BaseNPC *pVictim ) { +#ifdef MAPBASE + BaseClass::EnemyIgnited( pVictim ); +#else if ( FVisible( pVictim ) ) { SpeakIfAllowed( TLK_ENEMY_BURNING ); } +#endif } //----------------------------------------------------------------------------- @@ -1252,6 +1256,7 @@ void CNPC_Alyx::DoCustomSpeechAI( void ) CBasePlayer *pPlayer = AI_GetSinglePlayer(); +#ifndef MAPBASE // Ported to CNPC_PlayerCompanion if ( HasCondition(COND_NEW_ENEMY) && GetEnemy() ) { if ( GetEnemy()->Classify() == CLASS_HEADCRAB ) @@ -1278,6 +1283,7 @@ void CNPC_Alyx::DoCustomSpeechAI( void ) } } } +#endif // Darkness mode speech ClearCondition( COND_ALYX_IN_DARK ); @@ -1917,6 +1923,7 @@ int CNPC_Alyx::SelectSchedule( void ) //----------------------------------------------------------------------------- int CNPC_Alyx::SelectScheduleDanger( void ) { +#ifndef MAPBASE if( HasCondition( COND_HEAR_DANGER ) ) { CSound *pSound; @@ -1929,6 +1936,7 @@ int CNPC_Alyx::SelectScheduleDanger( void ) SpeakIfAllowed( TLK_DANGER_ZOMBINE_GRENADE ); } } +#endif return BaseClass::SelectScheduleDanger(); } diff --git a/sp/src/game/server/hl2/npc_playercompanion.cpp b/sp/src/game/server/hl2/npc_playercompanion.cpp index 68568a72..660cb218 100644 --- a/sp/src/game/server/hl2/npc_playercompanion.cpp +++ b/sp/src/game/server/hl2/npc_playercompanion.cpp @@ -35,6 +35,8 @@ #include "mapbase/GlobalStrings.h" #include "world.h" #include "vehicle_base.h" +#include "npc_headcrab.h" +#include "npc_BaseZombie.h" #endif ConVar ai_debug_readiness("ai_debug_readiness", "0" ); @@ -640,6 +642,55 @@ void CNPC_PlayerCompanion::DoCustomSpeechAI( void ) { SpeakIfAllowed( TLK_PLDEAD ); } + +#ifdef MAPBASE + // Unique new enemy concepts ported from Alyx + // The casts have been changed to dynamic_cast due to the risk of non-CBaseHeadcrab/CNPC_BaseZombie enemies using those classes + if ( HasCondition(COND_NEW_ENEMY) && GetEnemy() ) + { + int nClass = GetEnemy()->Classify(); + if ( nClass == CLASS_HEADCRAB ) + { + CBaseHeadcrab *pHC = dynamic_cast(GetEnemy()); + if ( pHC ) + { + // If we see a headcrab for the first time as he's jumping at me, freak out! + if ( ( GetEnemy()->GetEnemy() == this ) && pHC->IsJumping() && gpGlobals->curtime - GetEnemies()->FirstTimeSeen(GetEnemy()) < 0.5 ) + { + SpeakIfAllowed( "TLK_SPOTTED_INCOMING_HEADCRAB" ); + } + else + { + // If we see a headcrab leaving a zombie that just died, mention it + // (Note that this is now a response context since some death types remove the zombie instantly) + int nContext = pHC->FindContextByName( "from_zombie" ); + if ( nContext > -1 && !ContextExpired( nContext ) ) // pHC->GetOwnerEntity() && ( pHC->GetOwnerEntity()->Classify() == CLASS_ZOMBIE ) && !pHC->GetOwnerEntity()->IsAlive() + { + SpeakIfAllowed( "TLK_SPOTTED_HEADCRAB_LEAVING_ZOMBIE" ); + } + } + } + } + else if ( nClass == CLASS_ZOMBIE ) + { + CNPC_BaseZombie *pZombie = dynamic_cast(GetEnemy()); + // If we see a zombie getting up, mention it + if ( pZombie && pZombie->IsGettingUp() ) + { + SpeakIfAllowed( "TLK_SPOTTED_ZOMBIE_WAKEUP" ); + } + } + + if ( gpGlobals->curtime - GetEnemies()->TimeAtFirstHand( GetEnemy() ) <= 1.0f && nClass != CLASS_BULLSEYE ) + { + // New concept which did not originate from Alyx, but is in the same category as the above concepts. + // This is meant to be used when a new enemy enters the arena while combat is already in progress. + // (Note that this can still trigger when combat begins, but unlike TLK_STARTCOMBAT, it has no delay + // between combat engagements.) + SpeakIfAllowed( TLK_NEW_ENEMY ); + } + } +#endif } //----------------------------------------------------------------------------- @@ -910,8 +961,21 @@ int CNPC_PlayerCompanion::SelectScheduleDanger() if ( pSound && (pSound->m_iType & SOUND_DANGER) ) { +#ifdef MAPBASE + if ( pSound->SoundChannel() == SOUNDENT_CHANNEL_ZOMBINE_GRENADE ) + { + SetSpeechTarget( pSound->m_hOwner ); + SpeakIfAllowed( TLK_DANGER_ZOMBINE_GRENADE ); + } + else if (!(pSound->SoundContext() & (SOUND_CONTEXT_MORTAR | SOUND_CONTEXT_FROM_SNIPER)) || IsOkToCombatSpeak()) + { + SetSpeechTarget( pSound->m_hOwner ); + SpeakIfAllowed( TLK_DANGER ); + } +#else if ( !(pSound->SoundContext() & (SOUND_CONTEXT_MORTAR|SOUND_CONTEXT_FROM_SNIPER)) || IsOkToCombatSpeak() ) SpeakIfAllowed( TLK_DANGER ); +#endif if ( HasCondition( COND_PC_SAFE_FROM_MORTAR ) ) { @@ -4309,6 +4373,20 @@ void CNPC_PlayerCompanion::Event_KilledOther( CBaseEntity *pVictim, const CTakeD } } +//----------------------------------------------------------------------------- +// Purpose: Called by enemy NPC's when they are ignited +// Input : pVictim - entity that was ignited +//----------------------------------------------------------------------------- +void CNPC_PlayerCompanion::EnemyIgnited( CAI_BaseNPC *pVictim ) +{ + BaseClass::EnemyIgnited( pVictim ); + + if ( FVisible( pVictim ) ) + { + SpeakIfAllowed( TLK_ENEMY_BURNING ); + } +} + //----------------------------------------------------------------------------- // Purpose: Handles custom combat speech stuff ported from Alyx. //----------------------------------------------------------------------------- @@ -4376,6 +4454,21 @@ void CNPC_PlayerCompanion::DoCustomCombatAI( void ) { SpeakIfAllowed( TLK_MANY_ENEMIES ); } + + // If we're not currently attacking or vulnerable, try speaking + else if ( gpGlobals->curtime - GetLastAttackTime() > 1.0f && (!HasCondition( COND_SEE_ENEMY ) || IsCurSchedule( SCHED_RELOAD ) || IsCurSchedule( SCHED_HIDE_AND_RELOAD )) ) + { + int chance = ( IsMoving() ) ? 20 : 3; + if ( ShouldSpeakRandom( TLK_COMBAT_IDLE, chance ) ) + { + AI_CriteriaSet modifiers; + + modifiers.AppendCriteria( "in_cover", HasMemory( bits_MEMORY_INCOVER ) ? "1" : "0" ); + modifiers.AppendCriteria( "lastseenenemy", UTIL_VarArgs( "%f", gpGlobals->curtime - GetEnemyLastTimeSeen() ) ); + + SpeakIfAllowed( TLK_COMBAT_IDLE, modifiers ); + } + } } #endif diff --git a/sp/src/game/server/hl2/npc_playercompanion.h b/sp/src/game/server/hl2/npc_playercompanion.h index 8dcf1aa1..e0f6769e 100644 --- a/sp/src/game/server/hl2/npc_playercompanion.h +++ b/sp/src/game/server/hl2/npc_playercompanion.h @@ -242,6 +242,7 @@ public: virtual void Event_Killed( const CTakeDamageInfo &info ); virtual void Event_KilledOther( CBaseEntity *pVictim, const CTakeDamageInfo &info ); + virtual void EnemyIgnited( CAI_BaseNPC *pVictim ); virtual void DoCustomCombatAI( void ); #endif diff --git a/sp/src/game/server/player.cpp b/sp/src/game/server/player.cpp index f8bf33a2..28c767c5 100644 --- a/sp/src/game/server/player.cpp +++ b/sp/src/game/server/player.cpp @@ -7734,6 +7734,93 @@ void CBasePlayer::ResetAutoaim( void ) m_fOnTarget = false; } +#ifdef MAPBASE +ConVar player_debug_probable_aim_target( "player_debug_probable_aim_target", "0", FCVAR_CHEAT, "" ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseEntity *CBasePlayer::GetProbableAimTarget( const Vector &vecSrc, const Vector &vecDir ) +{ + trace_t tr; + CBaseEntity *pIgnore = NULL; + if (IsInAVehicle()) + pIgnore = GetVehicleEntity(); + + CTraceFilterSkipTwoEntities traceFilter( this, pIgnore, COLLISION_GROUP_NONE ); + + // Based on dot product and distance + // If we aim directly at something, only return it if there's not a larger entity slightly off-center + // Should be weighted based on whether an entity is a NPC, etc. + CBaseEntity *pBestEnt = NULL; + float flBestWeight = 0.0f; + for (CBaseEntity *pEntity = UTIL_EntitiesInPVS( this, NULL ); pEntity; pEntity = UTIL_EntitiesInPVS( this, pEntity )) + { + // Combat characters can be unviewable if they just died + if (!pEntity->IsViewable() && !pEntity->IsCombatCharacter()) + continue; + + if (pEntity == this || pEntity->GetMoveParent() == this || pEntity == GetVehicleEntity()) + continue; + + Vector vecEntDir = (pEntity->EyePosition() - vecSrc); + float flDot = DotProduct( vecEntDir.Normalized(), vecDir); + + if (flDot < m_flFieldOfView) + continue; + + // Make sure we can see it + UTIL_TraceLine( vecSrc, pEntity->EyePosition(), MASK_SHOT, &traceFilter, &tr ); + if (tr.m_pEnt != pEntity) + { + if (pEntity->IsCombatCharacter()) + { + // Trace between centers as well just in case our eyes are blocked + UTIL_TraceLine( WorldSpaceCenter(), pEntity->WorldSpaceCenter(), MASK_SHOT, &traceFilter, &tr ); + if (tr.m_pEnt != pEntity) + continue; + } + else + continue; + } + + float flWeight = flDot - (vecEntDir.LengthSqr() / Square( 2048.0f )); + + if (pEntity->IsCombatCharacter()) + { + // Hostile NPCs are more likely targets + if (IRelationType( pEntity ) <= D_FR) + flWeight += 0.5f; + } + else if (pEntity->GetFlags() & FL_AIMTARGET) + { + // FL_AIMTARGET is often used for props like explosive barrels + flWeight += 0.25f; + } + + if (player_debug_probable_aim_target.GetBool()) + { + float flWeightClamped = 1.0f - RemapValClamped( flWeight, -2.0f, 2.0f, 0.0f, 1.0f ); + pEntity->EntityText( 0, UTIL_VarArgs( "%f", flWeight ), 2.0f, flWeightClamped * 255.0f, 255.0f, flWeightClamped * 255.0f, 255 ); + } + + if (flWeight > flBestWeight) + { + pBestEnt = pEntity; + flBestWeight = flWeight; + } + } + + if (player_debug_probable_aim_target.GetBool()) + { + Msg( "Best probable aim target is %s\n", pBestEnt->GetDebugName() ); + NDebugOverlay::EntityBounds( pBestEnt, 255, 100, 0, 0, 2.0f ); + } + + return pBestEnt; +} +#endif + // ========================================================================== // > Weapon stuff // ========================================================================== diff --git a/sp/src/game/server/player.h b/sp/src/game/server/player.h index 0bfba8fb..72a1e38e 100644 --- a/sp/src/game/server/player.h +++ b/sp/src/game/server/player.h @@ -608,6 +608,11 @@ public: virtual bool ShouldAutoaim( void ); void SetTargetInfo( Vector &vecSrc, float flDist ); +#ifdef MAPBASE + // Tries to figure out what the player is trying to aim at + CBaseEntity *GetProbableAimTarget( const Vector &vecSrc, const Vector &vecDir ); +#endif + void SetViewEntity( CBaseEntity *pEntity ); CBaseEntity *GetViewEntity( void ) { return m_hViewEntity; } diff --git a/sp/src/game/shared/baseentity_shared.cpp b/sp/src/game/shared/baseentity_shared.cpp index e5588287..b2cd6bdc 100644 --- a/sp/src/game/shared/baseentity_shared.cpp +++ b/sp/src/game/shared/baseentity_shared.cpp @@ -83,6 +83,10 @@ ConVar ai_shot_bias_min( "ai_shot_bias_min", "-1.0", FCVAR_REPLICATED ); ConVar ai_shot_bias_max( "ai_shot_bias_max", "1.0", FCVAR_REPLICATED ); ConVar ai_debug_shoot_positions( "ai_debug_shoot_positions", "0", FCVAR_REPLICATED | FCVAR_CHEAT ); +#if defined(MAPBASE) && defined(GAME_DLL) +ConVar ai_shot_notify_targets( "ai_shot_notify_targets", "0", FCVAR_NONE, "Allows fired bullets to notify the NPCs and players they are targeting, regardless of whether they hit them or not. Can be used for custom AI and speech." ); +#endif + // Utility func to throttle rate at which the "reasonable position" spew goes out static double s_LastEntityReasonableEmitTime; bool CheckEmitReasonablePhysicsSpew() @@ -2081,6 +2085,25 @@ void CBaseEntity::FireBullets( const FireBulletsInfo_t &info ) CTakeDamageInfo dmgInfo( this, pAttacker, flCumulativeDamage, nDamageType ); gamestats->Event_WeaponHit( pPlayer, info.m_bPrimaryAttack, pPlayer->GetActiveWeapon()->GetClassname(), dmgInfo ); } + +#ifdef MAPBASE + if ( ai_shot_notify_targets.GetBool() ) + { + if ( IsPlayer() ) + { + // Look for probable target to notify of attack + CBaseEntity *pAimTarget = static_cast(this)->GetProbableAimTarget( info.m_vecSrc, info.m_vecDirShooting ); + if ( pAimTarget && pAimTarget->IsCombatCharacter() ) + { + pAimTarget->MyCombatCharacterPointer()->OnEnemyRangeAttackedMe( this, vecDir, vecEnd ); + } + } + else if ( GetEnemy() && GetEnemy()->IsCombatCharacter() ) + { + GetEnemy()->MyCombatCharacterPointer()->OnEnemyRangeAttackedMe( this, vecDir, vecEnd ); + } + } +#endif #endif }