//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: The downtrodden citizens of City 17. // //=============================================================================// #include "cbase.h" #include "npc_citizen17.h" #include "ammodef.h" #include "globalstate.h" #include "soundent.h" #include "BasePropDoor.h" #include "weapon_rpg.h" #include "hl2_player.h" #include "items.h" #ifdef HL2MP #include "hl2mp/weapon_crowbar.h" #else #include "weapon_crowbar.h" #endif #include "eventqueue.h" #include "ai_squad.h" #include "ai_pathfinder.h" #include "ai_route.h" #include "ai_hint.h" #include "ai_interactions.h" #include "ai_looktarget.h" #include "sceneentity.h" #include "tier0/icommandline.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" #define INSIGNIA_MODEL "models/chefhat.mdl" //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- #define CIT_INSPECTED_DELAY_TIME 120 //How often I'm allowed to be inspected extern ConVar sk_healthkit; extern ConVar sk_healthvial; const int MAX_PLAYER_SQUAD = 4; ConVar sk_citizen_health ( "sk_citizen_health", "0"); ConVar sk_citizen_heal_player ( "sk_citizen_heal_player", "25"); ConVar sk_citizen_heal_player_delay ( "sk_citizen_heal_player_delay", "25"); ConVar sk_citizen_giveammo_player_delay( "sk_citizen_giveammo_player_delay", "10"); ConVar sk_citizen_heal_player_min_pct ( "sk_citizen_heal_player_min_pct", "0.60"); ConVar sk_citizen_heal_player_min_forced( "sk_citizen_heal_player_min_forced", "10.0"); ConVar sk_citizen_heal_ally ( "sk_citizen_heal_ally", "30"); ConVar sk_citizen_heal_ally_delay ( "sk_citizen_heal_ally_delay", "20"); ConVar sk_citizen_heal_ally_min_pct ( "sk_citizen_heal_ally_min_pct", "0.90"); ConVar sk_citizen_player_stare_time ( "sk_citizen_player_stare_time", "1.0" ); ConVar sk_citizen_player_stare_dist ( "sk_citizen_player_stare_dist", "72" ); ConVar sk_citizen_stare_heal_time ( "sk_citizen_stare_heal_time", "5" ); ConVar g_ai_citizen_show_enemy( "g_ai_citizen_show_enemy", "0" ); ConVar npc_citizen_insignia( "npc_citizen_insignia", "0" ); ConVar npc_citizen_squad_marker( "npc_citizen_squad_marker", "0" ); ConVar npc_citizen_explosive_resist( "npc_citizen_explosive_resist", "0" ); ConVar npc_citizen_auto_player_squad( "npc_citizen_auto_player_squad", "1" ); ConVar npc_citizen_auto_player_squad_allow_use( "npc_citizen_auto_player_squad_allow_use", "0" ); ConVar npc_citizen_dont_precache_all( "npc_citizen_dont_precache_all", "0" ); ConVar npc_citizen_medic_emit_sound("npc_citizen_medic_emit_sound", "1" ); #ifdef HL2_EPISODIC // todo: bake these into pound constants (for now they're not just for tuning purposes) ConVar npc_citizen_heal_chuck_medkit("npc_citizen_heal_chuck_medkit" , "1" , FCVAR_ARCHIVE, "Set to 1 to use new experimental healthkit-throwing medic."); ConVar npc_citizen_medic_throw_style( "npc_citizen_medic_throw_style", "1", FCVAR_ARCHIVE, "Set to 0 for a lobbier trajectory" ); ConVar npc_citizen_medic_throw_speed( "npc_citizen_medic_throw_speed", "650" ); ConVar sk_citizen_heal_toss_player_delay("sk_citizen_heal_toss_player_delay", "26", FCVAR_NONE, "how long between throwing healthkits" ); #define MEDIC_THROW_SPEED npc_citizen_medic_throw_speed.GetFloat() #define USE_EXPERIMENTAL_MEDIC_CODE() (npc_citizen_heal_chuck_medkit.GetBool() && NameMatches("griggs")) #endif ConVar player_squad_autosummon_time( "player_squad_autosummon_time", "5" ); ConVar player_squad_autosummon_move_tolerance( "player_squad_autosummon_move_tolerance", "20" ); ConVar player_squad_autosummon_player_tolerance( "player_squad_autosummon_player_tolerance", "10" ); ConVar player_squad_autosummon_time_after_combat( "player_squad_autosummon_time_after_combat", "8" ); ConVar player_squad_autosummon_debug( "player_squad_autosummon_debug", "0" ); #define ShouldAutosquad() (npc_citizen_auto_player_squad.GetBool()) enum SquadSlot_T { SQUAD_SLOT_CITIZEN_RPG1 = LAST_SHARED_SQUADSLOT, SQUAD_SLOT_CITIZEN_RPG2, }; const float HEAL_MOVE_RANGE = 30*12; const float HEAL_TARGET_RANGE = 120; // 10 feet #ifdef HL2_EPISODIC const float HEAL_TOSS_TARGET_RANGE = 480; // 40 feet when we are throwing medkits const float HEAL_TARGET_RANGE_Z = 72; // a second check that Gordon isn't too far above us -- 6 feet #endif // player must be at least this distance away from an enemy before we fire an RPG at him const float RPG_SAFE_DISTANCE = CMissile::EXPLOSION_RADIUS + 64.0; // Animation events int AE_CITIZEN_GET_PACKAGE; int AE_CITIZEN_HEAL; //------------------------------------- //------------------------------------- ConVar ai_follow_move_commands( "ai_follow_move_commands", "1" ); ConVar ai_citizen_debug_commander( "ai_citizen_debug_commander", "1" ); #define DebuggingCommanderMode() (ai_citizen_debug_commander.GetBool() && (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT)) //----------------------------------------------------------------------------- // Citizen expressions for the citizen expression types //----------------------------------------------------------------------------- #define STATES_WITH_EXPRESSIONS 3 // Idle, Alert, Combat #define EXPRESSIONS_PER_STATE 1 char *szExpressionTypes[CIT_EXP_LAST_TYPE] = { "Unassigned", "Scared", "Normal", "Angry" }; struct citizen_expression_list_t { char *szExpressions[EXPRESSIONS_PER_STATE]; }; // Scared citizen_expression_list_t ScaredExpressions[STATES_WITH_EXPRESSIONS] = { { "scenes/Expressions/citizen_scared_idle_01.vcd" }, { "scenes/Expressions/citizen_scared_alert_01.vcd" }, { "scenes/Expressions/citizen_scared_combat_01.vcd" }, }; // Normal citizen_expression_list_t NormalExpressions[STATES_WITH_EXPRESSIONS] = { { "scenes/Expressions/citizen_normal_idle_01.vcd" }, { "scenes/Expressions/citizen_normal_alert_01.vcd" }, { "scenes/Expressions/citizen_normal_combat_01.vcd" }, }; // Angry citizen_expression_list_t AngryExpressions[STATES_WITH_EXPRESSIONS] = { { "scenes/Expressions/citizen_angry_idle_01.vcd" }, { "scenes/Expressions/citizen_angry_alert_01.vcd" }, { "scenes/Expressions/citizen_angry_combat_01.vcd" }, }; //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- #define COMMAND_POINT_CLASSNAME "info_target_command_point" class CCommandPoint : public CPointEntity { DECLARE_CLASS( CCommandPoint, CPointEntity ); public: CCommandPoint() : m_bNotInTransition(false) { if ( ++gm_nCommandPoints > 1 ) DevMsg( "WARNING: More than one citizen command point present\n" ); } ~CCommandPoint() { --gm_nCommandPoints; } int ObjectCaps() { int caps = ( BaseClass::ObjectCaps() | FCAP_NOTIFY_ON_TRANSITION ); if ( m_bNotInTransition ) caps |= FCAP_DONT_SAVE; return caps; } void InputOutsideTransition( inputdata_t &inputdata ) { if ( !AI_IsSinglePlayer() ) return; m_bNotInTransition = true; CAI_Squad *pPlayerAISquad = g_AI_SquadManager.FindSquad(AllocPooledString(PLAYER_SQUADNAME)); if ( pPlayerAISquad ) { AISquadIter_t iter; for ( CAI_BaseNPC *pAllyNpc = pPlayerAISquad->GetFirstMember(&iter); pAllyNpc; pAllyNpc = pPlayerAISquad->GetNextMember(&iter) ) { if ( pAllyNpc->GetCommandGoal() != vec3_invalid ) { bool bHadGag = pAllyNpc->HasSpawnFlags(SF_NPC_GAG); pAllyNpc->AddSpawnFlags(SF_NPC_GAG); pAllyNpc->TargetOrder( UTIL_GetLocalPlayer(), &pAllyNpc, 1 ); if ( !bHadGag ) pAllyNpc->RemoveSpawnFlags(SF_NPC_GAG); } } } } DECLARE_DATADESC(); private: bool m_bNotInTransition; // does not need to be saved. If this is ever not default, the object is not being saved. static int gm_nCommandPoints; }; int CCommandPoint::gm_nCommandPoints; LINK_ENTITY_TO_CLASS( info_target_command_point, CCommandPoint ); BEGIN_DATADESC( CCommandPoint ) // DEFINE_FIELD( m_bNotInTransition, FIELD_BOOLEAN ), DEFINE_INPUTFUNC( FIELD_VOID, "OutsideTransition", InputOutsideTransition ), END_DATADESC() //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- class CMattsPipe : public CWeaponCrowbar { DECLARE_CLASS( CMattsPipe, CWeaponCrowbar ); const char *GetWorldModel() const { return "models/props_canal/mattpipe.mdl"; } void SetPickupTouch( void ) { /* do nothing */ } }; //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- //--------------------------------------------------------- // Citizen models //--------------------------------------------------------- static const char *g_ppszRandomHeads[] = { "male_01.mdl", "male_02.mdl", "female_01.mdl", "male_03.mdl", "female_02.mdl", "male_04.mdl", "female_03.mdl", "male_05.mdl", "female_04.mdl", "male_06.mdl", "female_06.mdl", "male_07.mdl", "female_07.mdl", "male_08.mdl", "male_09.mdl", }; static const char *g_ppszModelLocs[] = { "Group01", "Group01", "Group02", "Group03%s", }; #define IsExcludedHead( type, bMedic, iHead) false // see XBox codeline for an implementation //--------------------------------------------------------- // Citizen activities //--------------------------------------------------------- int ACT_CIT_HANDSUP; int ACT_CIT_BLINDED; // Blinded by scanner photo int ACT_CIT_SHOWARMBAND; int ACT_CIT_HEAL; int ACT_CIT_STARTLED; // Startled by sneaky scanner //--------------------------------------------------------- LINK_ENTITY_TO_CLASS( npc_citizen, CNPC_Citizen ); //--------------------------------------------------------- BEGIN_DATADESC( CNPC_Citizen ) DEFINE_CUSTOM_FIELD( m_nInspectActivity, ActivityDataOps() ), DEFINE_FIELD( m_flNextFearSoundTime, FIELD_TIME ), DEFINE_FIELD( m_flStopManhackFlinch, FIELD_TIME ), DEFINE_FIELD( m_fNextInspectTime, FIELD_TIME ), DEFINE_FIELD( m_flPlayerHealTime, FIELD_TIME ), DEFINE_FIELD( m_flNextHealthSearchTime, FIELD_TIME ), DEFINE_FIELD( m_flAllyHealTime, FIELD_TIME ), // gm_PlayerSquadEvaluateTimer // m_AssaultBehavior // m_FollowBehavior // m_StandoffBehavior // m_LeadBehavior // m_FuncTankBehavior DEFINE_FIELD( m_flPlayerGiveAmmoTime, FIELD_TIME ), DEFINE_KEYFIELD( m_iszAmmoSupply, FIELD_STRING, "ammosupply" ), DEFINE_KEYFIELD( m_iAmmoAmount, FIELD_INTEGER, "ammoamount" ), DEFINE_FIELD( m_bRPGAvoidPlayer, FIELD_BOOLEAN ), DEFINE_FIELD( m_bShouldPatrol, FIELD_BOOLEAN ), DEFINE_FIELD( m_iszOriginalSquad, FIELD_STRING ), DEFINE_FIELD( m_flTimeJoinedPlayerSquad, FIELD_TIME ), DEFINE_FIELD( m_bWasInPlayerSquad, FIELD_BOOLEAN ), DEFINE_FIELD( m_flTimeLastCloseToPlayer, FIELD_TIME ), DEFINE_EMBEDDED( m_AutoSummonTimer ), DEFINE_FIELD( m_vAutoSummonAnchor, FIELD_POSITION_VECTOR ), DEFINE_KEYFIELD( m_Type, FIELD_INTEGER, "citizentype" ), DEFINE_KEYFIELD( m_ExpressionType, FIELD_INTEGER, "expressiontype" ), DEFINE_FIELD( m_iHead, FIELD_INTEGER ), DEFINE_FIELD( m_flTimePlayerStare, FIELD_TIME ), DEFINE_FIELD( m_flTimeNextHealStare, FIELD_TIME ), DEFINE_FIELD( m_hSavedFollowGoalEnt, FIELD_EHANDLE ), DEFINE_KEYFIELD( m_bNotifyNavFailBlocked, FIELD_BOOLEAN, "notifynavfailblocked" ), DEFINE_KEYFIELD( m_bNeverLeavePlayerSquad, FIELD_BOOLEAN, "neverleaveplayersquad" ), DEFINE_KEYFIELD( m_iszDenyCommandConcept, FIELD_STRING, "denycommandconcept" ), DEFINE_OUTPUT( m_OnJoinedPlayerSquad, "OnJoinedPlayerSquad" ), DEFINE_OUTPUT( m_OnLeftPlayerSquad, "OnLeftPlayerSquad" ), DEFINE_OUTPUT( m_OnFollowOrder, "OnFollowOrder" ), DEFINE_OUTPUT( m_OnStationOrder, "OnStationOrder" ), DEFINE_OUTPUT( m_OnPlayerUse, "OnPlayerUse" ), DEFINE_OUTPUT( m_OnNavFailBlocked, "OnNavFailBlocked" ), DEFINE_INPUTFUNC( FIELD_VOID, "RemoveFromPlayerSquad", InputRemoveFromPlayerSquad ), DEFINE_INPUTFUNC( FIELD_VOID, "StartPatrolling", InputStartPatrolling ), DEFINE_INPUTFUNC( FIELD_VOID, "StopPatrolling", InputStopPatrolling ), DEFINE_INPUTFUNC( FIELD_VOID, "SetCommandable", InputSetCommandable ), DEFINE_INPUTFUNC( FIELD_VOID, "SetMedicOn", InputSetMedicOn ), DEFINE_INPUTFUNC( FIELD_VOID, "SetMedicOff", InputSetMedicOff ), DEFINE_INPUTFUNC( FIELD_VOID, "SetAmmoResupplierOn", InputSetAmmoResupplierOn ), DEFINE_INPUTFUNC( FIELD_VOID, "SetAmmoResupplierOff", InputSetAmmoResupplierOff ), DEFINE_INPUTFUNC( FIELD_VOID, "SpeakIdleResponse", InputSpeakIdleResponse ), #if HL2_EPISODIC DEFINE_INPUTFUNC( FIELD_VOID, "ThrowHealthKit", InputForceHealthKitToss ), #endif DEFINE_USEFUNC( CommanderUse ), DEFINE_USEFUNC( SimpleUse ), END_DATADESC() //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- CSimpleSimTimer CNPC_Citizen::gm_PlayerSquadEvaluateTimer; //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::CreateBehaviors() { BaseClass::CreateBehaviors(); AddBehavior( &m_FuncTankBehavior ); return true; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::Precache() { SelectModel(); SelectExpressionType(); if ( !npc_citizen_dont_precache_all.GetBool() ) PrecacheAllOfType( m_Type ); else PrecacheModel( STRING( GetModelName() ) ); if ( NameMatches( "matt" ) ) PrecacheModel( "models/props_canal/mattpipe.mdl" ); PrecacheModel( INSIGNIA_MODEL ); PrecacheScriptSound( "NPC_Citizen.FootstepLeft" ); PrecacheScriptSound( "NPC_Citizen.FootstepRight" ); PrecacheScriptSound( "NPC_Citizen.Die" ); PrecacheInstancedScene( "scenes/Expressions/CitizenIdle.vcd" ); PrecacheInstancedScene( "scenes/Expressions/CitizenAlert_loop.vcd" ); PrecacheInstancedScene( "scenes/Expressions/CitizenCombat_loop.vcd" ); for ( int i = 0; i < STATES_WITH_EXPRESSIONS; i++ ) { for ( int j = 0; j < ARRAYSIZE(ScaredExpressions[i].szExpressions); j++ ) { PrecacheInstancedScene( ScaredExpressions[i].szExpressions[j] ); } for ( int j = 0; j < ARRAYSIZE(NormalExpressions[i].szExpressions); j++ ) { PrecacheInstancedScene( NormalExpressions[i].szExpressions[j] ); } for ( int j = 0; j < ARRAYSIZE(AngryExpressions[i].szExpressions); j++ ) { PrecacheInstancedScene( AngryExpressions[i].szExpressions[j] ); } } BaseClass::Precache(); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::PrecacheAllOfType( CitizenType_t type ) { if ( m_Type == CT_UNIQUE ) return; int nHeads = ARRAYSIZE( g_ppszRandomHeads ); int i; for ( i = 0; i < nHeads; ++i ) { if ( !IsExcludedHead( type, false, i ) ) { PrecacheModel( CFmtStr( "models/Humans/%s/%s", (const char *)(CFmtStr(g_ppszModelLocs[m_Type], "")), g_ppszRandomHeads[i] ) ); } } if ( m_Type == CT_REBEL ) { for ( i = 0; i < nHeads; ++i ) { if ( !IsExcludedHead( type, true, i ) ) { PrecacheModel( CFmtStr( "models/Humans/%s/%s", (const char *)(CFmtStr(g_ppszModelLocs[m_Type], "m")), g_ppszRandomHeads[i] ) ); } } } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::Spawn() { BaseClass::Spawn(); #ifdef _XBOX // Always fade the corpse AddSpawnFlags( SF_NPC_FADE_CORPSE ); #endif // _XBOX if ( ShouldAutosquad() ) { if ( m_SquadName == GetPlayerSquadName() ) { CAI_Squad *pPlayerSquad = g_AI_SquadManager.FindSquad( GetPlayerSquadName() ); if ( pPlayerSquad && pPlayerSquad->NumMembers() >= MAX_PLAYER_SQUAD ) m_SquadName = NULL_STRING; } gm_PlayerSquadEvaluateTimer.Force(); } if ( IsAmmoResupplier() ) m_nSkin = 2; m_bRPGAvoidPlayer = false; m_bShouldPatrol = false; m_iHealth = sk_citizen_health.GetFloat(); // Are we on a train? Used in trainstation to have NPCs on trains. if ( GetMoveParent() && FClassnameIs( GetMoveParent(), "func_tracktrain" ) ) { CapabilitiesRemove( bits_CAP_MOVE_GROUND ); SetMoveType( MOVETYPE_NONE ); if ( NameMatches("citizen_train_2") ) { SetSequenceByName( "d1_t01_TrainRide_Sit_Idle" ); SetIdealActivity( ACT_DO_NOT_DISTURB ); } else { SetSequenceByName( "d1_t01_TrainRide_Stand" ); SetIdealActivity( ACT_DO_NOT_DISTURB ); } } m_flStopManhackFlinch = -1; m_iszIdleExpression = MAKE_STRING("scenes/expressions/citizenidle.vcd"); m_iszAlertExpression = MAKE_STRING("scenes/expressions/citizenalert_loop.vcd"); m_iszCombatExpression = MAKE_STRING("scenes/expressions/citizencombat_loop.vcd"); m_iszOriginalSquad = m_SquadName; m_flNextHealthSearchTime = gpGlobals->curtime; CWeaponRPG *pRPG = dynamic_cast(GetActiveWeapon()); if ( pRPG ) { CapabilitiesRemove( bits_CAP_USE_SHOT_REGULATOR ); pRPG->StopGuiding(); } m_flTimePlayerStare = FLT_MAX; AddEFlags( EFL_NO_DISSOLVE | EFL_NO_MEGAPHYSCANNON_RAGDOLL | EFL_NO_PHYSCANNON_INTERACTION ); NPCInit(); SetUse( &CNPC_Citizen::CommanderUse ); Assert( !ShouldAutosquad() || !IsInPlayerSquad() ); m_bWasInPlayerSquad = IsInPlayerSquad(); // Use render bounds instead of human hull for guys sitting in chairs, etc. m_ActBusyBehavior.SetUseRenderBounds( HasSpawnFlags( SF_CITIZEN_USE_RENDER_BOUNDS ) ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::PostNPCInit() { if ( !gEntList.FindEntityByClassname( NULL, COMMAND_POINT_CLASSNAME ) ) { CreateEntityByName( COMMAND_POINT_CLASSNAME ); } if ( IsInPlayerSquad() ) { if ( m_pSquad->NumMembers() > MAX_PLAYER_SQUAD ) DevMsg( "Error: Spawning citizen in player squad but exceeds squad limit of %d members\n", MAX_PLAYER_SQUAD ); FixupPlayerSquad(); } else { if ( ( m_spawnflags & SF_CITIZEN_FOLLOW ) && AI_IsSinglePlayer() ) { m_FollowBehavior.SetFollowTarget( UTIL_GetLocalPlayer() ); m_FollowBehavior.SetParameters( AIF_SIMPLE ); } } BaseClass::PostNPCInit(); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- struct HeadCandidate_t { int iHead; int nHeads; static int __cdecl Sort( const HeadCandidate_t *pLeft, const HeadCandidate_t *pRight ) { return ( pLeft->nHeads - pRight->nHeads ); } }; void CNPC_Citizen::SelectModel() { // If making reslists, precache everything!!! static bool madereslists = false; if ( CommandLine()->CheckParm("-makereslists") && !madereslists ) { madereslists = true; PrecacheAllOfType( CT_DOWNTRODDEN ); PrecacheAllOfType( CT_REFUGEE ); PrecacheAllOfType( CT_REBEL ); } const char *pszModelName = NULL; if ( m_Type == CT_DEFAULT ) { struct CitizenTypeMapping { const char *pszMapTag; CitizenType_t type; }; static CitizenTypeMapping CitizenTypeMappings[] = { { "trainstation", CT_DOWNTRODDEN }, { "canals", CT_REFUGEE }, { "town", CT_REFUGEE }, { "coast", CT_REFUGEE }, { "prison", CT_DOWNTRODDEN }, { "c17", CT_REBEL }, { "citadel", CT_DOWNTRODDEN }, }; char szMapName[256]; Q_strncpy(szMapName, STRING(gpGlobals->mapname), sizeof(szMapName) ); Q_strlower(szMapName); for ( int i = 0; i < ARRAYSIZE(CitizenTypeMappings); i++ ) { if ( Q_stristr( szMapName, CitizenTypeMappings[i].pszMapTag ) ) { m_Type = CitizenTypeMappings[i].type; break; } } if ( m_Type == CT_DEFAULT ) m_Type = CT_DOWNTRODDEN; } if( HasSpawnFlags( SF_CITIZEN_RANDOM_HEAD | SF_CITIZEN_RANDOM_HEAD_MALE | SF_CITIZEN_RANDOM_HEAD_FEMALE ) || GetModelName() == NULL_STRING ) { Assert( m_iHead == -1 ); char gender = ( HasSpawnFlags( SF_CITIZEN_RANDOM_HEAD_MALE ) ) ? 'm' : ( HasSpawnFlags( SF_CITIZEN_RANDOM_HEAD_FEMALE ) ) ? 'f' : 0; RemoveSpawnFlags( SF_CITIZEN_RANDOM_HEAD | SF_CITIZEN_RANDOM_HEAD_MALE | SF_CITIZEN_RANDOM_HEAD_FEMALE ); if( HasSpawnFlags( SF_NPC_START_EFFICIENT ) ) { SetModelName( AllocPooledString("models/humans/male_cheaple.mdl" ) ); return; } else { // Count the heads int headCounts[ARRAYSIZE(g_ppszRandomHeads)] = { 0 }; int i; for ( i = 0; i < g_AI_Manager.NumAIs(); i++ ) { CNPC_Citizen *pCitizen = dynamic_cast(g_AI_Manager.AccessAIs()[i]); if ( pCitizen && pCitizen != this && pCitizen->m_iHead >= 0 && pCitizen->m_iHead < ARRAYSIZE(g_ppszRandomHeads) ) { headCounts[pCitizen->m_iHead]++; } } // Find all candidates CUtlVectorFixed candidates; for ( i = 0; i < ARRAYSIZE(g_ppszRandomHeads); i++ ) { if ( !gender || g_ppszRandomHeads[i][0] == gender ) { if ( !IsExcludedHead( m_Type, IsMedic(), i ) ) { HeadCandidate_t candidate = { i, headCounts[i] }; candidates.AddToTail( candidate ); } } } Assert( candidates.Count() ); candidates.Sort( &HeadCandidate_t::Sort ); int iSmallestCount = candidates[0].nHeads; int iLimit; for ( iLimit = 0; iLimit < candidates.Count(); iLimit++ ) { if ( candidates[iLimit].nHeads > iSmallestCount ) break; } m_iHead = candidates[random->RandomInt( 0, iLimit - 1 )].iHead; pszModelName = g_ppszRandomHeads[m_iHead]; SetModelName(NULL_STRING); } } Assert( pszModelName || GetModelName() != NULL_STRING ); if ( !pszModelName ) { if ( GetModelName() == NULL_STRING ) return; pszModelName = strrchr(STRING(GetModelName()), '/' ); if ( !pszModelName ) pszModelName = STRING(GetModelName()); else { pszModelName++; if ( m_iHead == -1 ) { for ( int i = 0; i < ARRAYSIZE(g_ppszRandomHeads); i++ ) { if ( Q_stricmp( g_ppszRandomHeads[i], pszModelName ) == 0 ) { m_iHead = i; break; } } } } if ( !*pszModelName ) return; } // Unique citizen models are left alone if ( m_Type != CT_UNIQUE ) { SetModelName( AllocPooledString( CFmtStr( "models/Humans/%s/%s", (const char *)(CFmtStr(g_ppszModelLocs[ m_Type ], ( IsMedic() ) ? "m" : "" )), pszModelName ) ) ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_Citizen::SelectExpressionType() { // If we've got a mapmaker assigned type, leave it alone if ( m_ExpressionType != CIT_EXP_UNASSIGNED ) return; switch ( m_Type ) { case CT_DOWNTRODDEN: m_ExpressionType = (CitizenExpressionTypes_t)RandomInt( CIT_EXP_SCARED, CIT_EXP_NORMAL ); break; case CT_REFUGEE: m_ExpressionType = (CitizenExpressionTypes_t)RandomInt( CIT_EXP_SCARED, CIT_EXP_NORMAL ); break; case CT_REBEL: m_ExpressionType = (CitizenExpressionTypes_t)RandomInt( CIT_EXP_SCARED, CIT_EXP_ANGRY ); break; case CT_DEFAULT: case CT_UNIQUE: default: break; } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::FixupMattWeapon() { CBaseCombatWeapon *pWeapon = GetActiveWeapon(); if ( pWeapon && pWeapon->ClassMatches( "weapon_crowbar" ) && NameMatches( "matt" ) ) { Weapon_Drop( pWeapon ); UTIL_Remove( pWeapon ); pWeapon = (CBaseCombatWeapon *)CREATE_UNSAVED_ENTITY( CMattsPipe, "weapon_crowbar" ); pWeapon->SetName( AllocPooledString( "matt_weapon" ) ); DispatchSpawn( pWeapon ); #ifdef DEBUG extern bool g_bReceivedChainedActivate; g_bReceivedChainedActivate = false; #endif pWeapon->Activate(); Weapon_Equip( pWeapon ); } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::Activate() { BaseClass::Activate(); FixupMattWeapon(); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::OnRestore() { gm_PlayerSquadEvaluateTimer.Force(); BaseClass::OnRestore(); if ( !gEntList.FindEntityByClassname( NULL, COMMAND_POINT_CLASSNAME ) ) { CreateEntityByName( COMMAND_POINT_CLASSNAME ); } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- string_t CNPC_Citizen::GetModelName() const { string_t iszModelName = BaseClass::GetModelName(); // // If the model refers to an obsolete model, pretend it was blank // so that we pick the new default model. // if (!Q_strnicmp(STRING(iszModelName), "models/c17_", 11) || !Q_strnicmp(STRING(iszModelName), "models/male", 11) || !Q_strnicmp(STRING(iszModelName), "models/female", 13) || !Q_strnicmp(STRING(iszModelName), "models/citizen", 14)) { return NULL_STRING; } return iszModelName; } //----------------------------------------------------------------------------- // Purpose: Overridden to switch our behavior between passive and rebel. We // become combative after Gordon becomes a criminal. //----------------------------------------------------------------------------- Class_T CNPC_Citizen::Classify() { if (GlobalEntity_GetState("gordon_precriminal") == GLOBAL_ON) return CLASS_CITIZEN_PASSIVE; if (GlobalEntity_GetState("citizens_passive") == GLOBAL_ON) return CLASS_CITIZEN_PASSIVE; return CLASS_PLAYER_ALLY; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::ShouldAlwaysThink() { return ( BaseClass::ShouldAlwaysThink() || IsInPlayerSquad() ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- #define CITIZEN_FOLLOWER_DESERT_FUNCTANK_DIST 45.0f*12.0f bool CNPC_Citizen::ShouldBehaviorSelectSchedule( CAI_BehaviorBase *pBehavior ) { if( pBehavior == &m_FollowBehavior ) { // Suppress follow behavior if I have a func_tank and the func tank is near // what I'm supposed to be following. if( m_FuncTankBehavior.CanSelectSchedule() ) { // Is the tank close to the follow target? Vector vecTank = m_FuncTankBehavior.GetFuncTank()->WorldSpaceCenter(); Vector vecFollowGoal = m_FollowBehavior.GetFollowGoalInfo().position; float flTankDistSqr = (vecTank - vecFollowGoal).LengthSqr(); float flAllowDist = m_FollowBehavior.GetFollowGoalInfo().followPointTolerance * 2.0f; float flAllowDistSqr = flAllowDist * flAllowDist; if( flTankDistSqr < flAllowDistSqr ) { // Deny follow behavior so the tank can go. return false; } } } else if( IsInPlayerSquad() && pBehavior == &m_FuncTankBehavior && m_FuncTankBehavior.IsMounted() ) { if( m_FollowBehavior.GetFollowTarget() ) { Vector vecFollowGoal = m_FollowBehavior.GetFollowTarget()->GetAbsOrigin(); if( vecFollowGoal.DistToSqr( GetAbsOrigin() ) > Square(CITIZEN_FOLLOWER_DESERT_FUNCTANK_DIST) ) { return false; } } } return BaseClass::ShouldBehaviorSelectSchedule( pBehavior ); } void CNPC_Citizen::OnChangeRunningBehavior( CAI_BehaviorBase *pOldBehavior, CAI_BehaviorBase *pNewBehavior ) { if ( pNewBehavior == &m_FuncTankBehavior ) { m_bReadinessCapable = false; } else if ( pOldBehavior == &m_FuncTankBehavior ) { m_bReadinessCapable = IsReadinessCapable(); } BaseClass::OnChangeRunningBehavior( pOldBehavior, pNewBehavior ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::GatherConditions() { BaseClass::GatherConditions(); if( IsInPlayerSquad() && hl2_episodic.GetBool() ) { // Leave the player squad if someone has made me neutral to player. if( IRelationType(UTIL_GetLocalPlayer()) == D_NU ) { RemoveFromPlayerSquad(); } } if ( !SpokeConcept( TLK_JOINPLAYER ) && IsRunningScriptedSceneWithSpeech( this, true ) ) { SetSpokeConcept( TLK_JOINPLAYER, NULL ); for ( int i = 0; i < g_AI_Manager.NumAIs(); i++ ) { CAI_BaseNPC *pNpc = g_AI_Manager.AccessAIs()[i]; if ( pNpc != this && pNpc->GetClassname() == GetClassname() && pNpc->GetAbsOrigin().DistToSqr( GetAbsOrigin() ) < Square( 15*12 ) && FVisible( pNpc ) ) { (assert_cast(pNpc))->SetSpokeConcept( TLK_JOINPLAYER, NULL ); } } } if( ShouldLookForHealthItem() ) { if( FindHealthItem( GetAbsOrigin(), Vector( 240, 240, 240 ) ) ) SetCondition( COND_HEALTH_ITEM_AVAILABLE ); else ClearCondition( COND_HEALTH_ITEM_AVAILABLE ); m_flNextHealthSearchTime = gpGlobals->curtime + 4.0; } // If the player is standing near a medic and can see the medic, // assume the player is 'staring' and wants health. if( CanHeal() ) { CBasePlayer *pPlayer = AI_GetSinglePlayer(); if ( !pPlayer ) { m_flTimePlayerStare = FLT_MAX; return; } float flDistSqr = ( GetAbsOrigin() - pPlayer->GetAbsOrigin() ).Length2DSqr(); float flStareDist = sk_citizen_player_stare_dist.GetFloat(); float flPlayerDamage = pPlayer->GetMaxHealth() - pPlayer->GetHealth(); if( pPlayer->IsAlive() && flPlayerDamage > 0 && (flDistSqr <= flStareDist * flStareDist) && pPlayer->FInViewCone( this ) && pPlayer->FVisible( this ) ) { if( m_flTimePlayerStare == FLT_MAX ) { // Player wasn't looking at me at last think. He started staring now. m_flTimePlayerStare = gpGlobals->curtime; } // Heal if it's been long enough since last time I healed a staring player. if( gpGlobals->curtime - m_flTimePlayerStare >= sk_citizen_player_stare_time.GetFloat() && gpGlobals->curtime > m_flTimeNextHealStare && !IsCurSchedule( SCHED_CITIZEN_HEAL ) ) { if ( ShouldHealTarget( pPlayer, true ) ) { SetCondition( COND_CIT_PLAYERHEALREQUEST ); } else { m_flTimeNextHealStare = gpGlobals->curtime + sk_citizen_stare_heal_time.GetFloat() * .5f; ClearCondition( COND_CIT_PLAYERHEALREQUEST ); } } #ifdef HL2_EPISODIC // Heal if I'm on an assault. The player hasn't had time to stare at me. if( m_AssaultBehavior.IsRunning() && IsMoving() ) { SetCondition( COND_CIT_PLAYERHEALREQUEST ); } #endif } else { m_flTimePlayerStare = FLT_MAX; } } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::PredictPlayerPush() { if ( !AI_IsSinglePlayer() ) return; if ( HasCondition( COND_CIT_PLAYERHEALREQUEST ) ) return; bool bHadPlayerPush = HasCondition( COND_PLAYER_PUSHING ); BaseClass::PredictPlayerPush(); CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); if ( !bHadPlayerPush && HasCondition( COND_PLAYER_PUSHING ) && pPlayer->FInViewCone( this ) && CanHeal() ) { if ( ShouldHealTarget( pPlayer, true ) ) { ClearCondition( COND_PLAYER_PUSHING ); SetCondition( COND_CIT_PLAYERHEALREQUEST ); } } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::PrescheduleThink() { BaseClass::PrescheduleThink(); UpdatePlayerSquad(); UpdateFollowCommandPoint(); if ( !npc_citizen_insignia.GetBool() && npc_citizen_squad_marker.GetBool() && IsInPlayerSquad() ) { Vector mins = WorldAlignMins() * .5 + GetAbsOrigin(); Vector maxs = WorldAlignMaxs() * .5 + GetAbsOrigin(); float rMax = 255; float gMax = 255; float bMax = 255; float rMin = 255; float gMin = 128; float bMin = 0; const float TIME_FADE = 1.0; float timeInSquad = gpGlobals->curtime - m_flTimeJoinedPlayerSquad; timeInSquad = MIN( TIME_FADE, MAX( timeInSquad, 0 ) ); float fade = ( 1.0 - timeInSquad / TIME_FADE ); float r = rMin + ( rMax - rMin ) * fade; float g = gMin + ( gMax - gMin ) * fade; float b = bMin + ( bMax - bMin ) * fade; // THIS IS A PLACEHOLDER UNTIL WE HAVE A REAL DESIGN & ART -- DO NOT REMOVE NDebugOverlay::Line( Vector( mins.x, GetAbsOrigin().y, GetAbsOrigin().z+1 ), Vector( maxs.x, GetAbsOrigin().y, GetAbsOrigin().z+1 ), r, g, b, false, .11 ); NDebugOverlay::Line( Vector( GetAbsOrigin().x, mins.y, GetAbsOrigin().z+1 ), Vector( GetAbsOrigin().x, maxs.y, GetAbsOrigin().z+1 ), r, g, b, false, .11 ); } if( GetEnemy() && g_ai_citizen_show_enemy.GetBool() ) { NDebugOverlay::Line( EyePosition(), GetEnemy()->EyePosition(), 255, 0, 0, false, .1 ); } if ( DebuggingCommanderMode() ) { if ( HaveCommandGoal() ) { CBaseEntity *pCommandPoint = gEntList.FindEntityByClassname( NULL, COMMAND_POINT_CLASSNAME ); if ( pCommandPoint ) { NDebugOverlay::Cross3D(pCommandPoint->GetAbsOrigin(), 16, 0, 255, 255, false, 0.1 ); } } } } //----------------------------------------------------------------------------- // Purpose: Allows for modification of the interrupt mask for the current schedule. // In the most cases the base implementation should be called first. //----------------------------------------------------------------------------- void CNPC_Citizen::BuildScheduleTestBits() { BaseClass::BuildScheduleTestBits(); if ( IsCurSchedule( SCHED_IDLE_STAND ) || IsCurSchedule( SCHED_ALERT_STAND ) ) { SetCustomInterruptCondition( COND_CIT_START_INSPECTION ); } if ( IsMedic() && IsCustomInterruptConditionSet( COND_HEAR_MOVE_AWAY ) ) { if( !IsCurSchedule(SCHED_RELOAD, false) ) { // Since schedule selection code prioritizes reloading over requests to heal // the player, we must prevent this condition from breaking the reload schedule. SetCustomInterruptCondition( COND_CIT_PLAYERHEALREQUEST ); } SetCustomInterruptCondition( COND_CIT_COMMANDHEAL ); } if( !IsCurSchedule( SCHED_NEW_WEAPON ) ) { SetCustomInterruptCondition( COND_RECEIVED_ORDERS ); } if( GetCurSchedule()->HasInterrupt( COND_IDLE_INTERRUPT ) ) { SetCustomInterruptCondition( COND_BETTER_WEAPON_AVAILABLE ); } #ifdef HL2_EPISODIC if( IsMedic() && m_AssaultBehavior.IsRunning() ) { if( !IsCurSchedule(SCHED_RELOAD, false) ) { SetCustomInterruptCondition( COND_CIT_PLAYERHEALREQUEST ); } SetCustomInterruptCondition( COND_CIT_COMMANDHEAL ); } #else if( IsMedic() && m_AssaultBehavior.IsRunning() && !IsMoving() ) { if( !IsCurSchedule(SCHED_RELOAD, false) ) { SetCustomInterruptCondition( COND_CIT_PLAYERHEALREQUEST ); } SetCustomInterruptCondition( COND_CIT_COMMANDHEAL ); } #endif } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::FInViewCone( CBaseEntity *pEntity ) { #if 0 if ( IsMortar( pEntity ) ) { // @TODO (toml 11-20-03): do this only if have heard mortar shell recently and it's active return true; } #endif return BaseClass::FInViewCone( pEntity ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- int CNPC_Citizen::SelectFailSchedule( int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode ) { switch( failedSchedule ) { case SCHED_NEW_WEAPON: // If failed trying to pick up a weapon, try again in one second. This is because other AI code // has put this off for 10 seconds under the assumption that the citizen would be able to // pick up the weapon that they found. m_flNextWeaponSearchTime = gpGlobals->curtime + 1.0f; break; case SCHED_ESTABLISH_LINE_OF_FIRE_FALLBACK: case SCHED_MOVE_TO_WEAPON_RANGE: if( !IsMortar( GetEnemy() ) ) { if ( GetActiveWeapon() && ( GetActiveWeapon()->CapabilitiesGet() & bits_CAP_WEAPON_RANGE_ATTACK1 ) && random->RandomInt( 0, 1 ) && HasCondition(COND_SEE_ENEMY) && !HasCondition ( COND_NO_PRIMARY_AMMO ) ) return TranslateSchedule( SCHED_RANGE_ATTACK1 ); return SCHED_STANDOFF; } break; } return BaseClass::SelectFailSchedule( failedSchedule, failedTask, taskFailCode ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- int CNPC_Citizen::SelectSchedule() { // If we can't move, we're on a train, and should be sitting. if ( GetMoveType() == MOVETYPE_NONE ) { // For now, we're only ever parented to trains. If you hit this assert, you've parented a citizen // to something else, and now we need to figure out a better system. Assert( GetMoveParent() && FClassnameIs( GetMoveParent(), "func_tracktrain" ) ); return SCHED_CITIZEN_SIT_ON_TRAIN; } CWeaponRPG *pRPG = dynamic_cast(GetActiveWeapon()); if ( pRPG && pRPG->IsGuiding() ) { DevMsg( "Citizen in select schedule but RPG is guiding?\n"); pRPG->StopGuiding(); } return BaseClass::SelectSchedule(); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- int CNPC_Citizen::SelectSchedulePriorityAction() { int schedule = SelectScheduleHeal(); if ( schedule != SCHED_NONE ) return schedule; schedule = BaseClass::SelectSchedulePriorityAction(); if ( schedule != SCHED_NONE ) return schedule; schedule = SelectScheduleRetrieveItem(); if ( schedule != SCHED_NONE ) return schedule; return SCHED_NONE; } //----------------------------------------------------------------------------- // Determine if citizen should perform heal action. //----------------------------------------------------------------------------- int CNPC_Citizen::SelectScheduleHeal() { // episodic medics may toss the healthkits rather than poke you with them #if HL2_EPISODIC if ( CanHeal() ) { CBaseEntity *pEntity = PlayerInRange( GetLocalOrigin(), HEAL_TOSS_TARGET_RANGE ); if ( pEntity ) { if ( USE_EXPERIMENTAL_MEDIC_CODE() && IsMedic() ) { // use the new heal toss algorithm if ( ShouldHealTossTarget( pEntity, HasCondition( COND_CIT_PLAYERHEALREQUEST ) ) ) { SetTarget( pEntity ); return SCHED_CITIZEN_HEAL_TOSS; } } else if ( PlayerInRange( GetLocalOrigin(), HEAL_MOVE_RANGE ) ) { // use old mechanism for ammo if ( ShouldHealTarget( pEntity, HasCondition( COND_CIT_PLAYERHEALREQUEST ) ) ) { SetTarget( pEntity ); return SCHED_CITIZEN_HEAL; } } } if ( m_pSquad ) { pEntity = NULL; float distClosestSq = HEAL_MOVE_RANGE*HEAL_MOVE_RANGE; float distCurSq; AISquadIter_t iter; CAI_BaseNPC *pSquadmate = m_pSquad->GetFirstMember( &iter ); while ( pSquadmate ) { if ( pSquadmate != this ) { distCurSq = ( GetAbsOrigin() - pSquadmate->GetAbsOrigin() ).LengthSqr(); if ( distCurSq < distClosestSq && ShouldHealTarget( pSquadmate ) ) { distClosestSq = distCurSq; pEntity = pSquadmate; } } pSquadmate = m_pSquad->GetNextMember( &iter ); } if ( pEntity ) { SetTarget( pEntity ); return SCHED_CITIZEN_HEAL; } } } else { if ( HasCondition( COND_CIT_PLAYERHEALREQUEST ) ) DevMsg( "Would say: sorry, need to recharge\n" ); } return SCHED_NONE; #else if ( CanHeal() ) { CBaseEntity *pEntity = PlayerInRange( GetLocalOrigin(), HEAL_MOVE_RANGE ); if ( pEntity && ShouldHealTarget( pEntity, HasCondition( COND_CIT_PLAYERHEALREQUEST ) ) ) { SetTarget( pEntity ); return SCHED_CITIZEN_HEAL; } if ( m_pSquad ) { pEntity = NULL; float distClosestSq = HEAL_MOVE_RANGE*HEAL_MOVE_RANGE; float distCurSq; AISquadIter_t iter; CAI_BaseNPC *pSquadmate = m_pSquad->GetFirstMember( &iter ); while ( pSquadmate ) { if ( pSquadmate != this ) { distCurSq = ( GetAbsOrigin() - pSquadmate->GetAbsOrigin() ).LengthSqr(); if ( distCurSq < distClosestSq && ShouldHealTarget( pSquadmate ) ) { distClosestSq = distCurSq; pEntity = pSquadmate; } } pSquadmate = m_pSquad->GetNextMember( &iter ); } if ( pEntity ) { SetTarget( pEntity ); return SCHED_CITIZEN_HEAL; } } } else { if ( HasCondition( COND_CIT_PLAYERHEALREQUEST ) ) DevMsg( "Would say: sorry, need to recharge\n" ); } return SCHED_NONE; #endif } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- int CNPC_Citizen::SelectScheduleRetrieveItem() { if ( HasCondition(COND_BETTER_WEAPON_AVAILABLE) ) { CBaseHLCombatWeapon *pWeapon = dynamic_cast(Weapon_FindUsable( WEAPON_SEARCH_DELTA )); if ( pWeapon ) { m_flNextWeaponSearchTime = gpGlobals->curtime + 10.0; // Now lock the weapon for several seconds while we go to pick it up. pWeapon->Lock( 10.0, this ); SetTarget( pWeapon ); return SCHED_NEW_WEAPON; } } if( HasCondition(COND_HEALTH_ITEM_AVAILABLE) ) { if( !IsInPlayerSquad() ) { // Been kicked out of the player squad since the time I located the health. ClearCondition( COND_HEALTH_ITEM_AVAILABLE ); } else { CBaseEntity *pBase = FindHealthItem(m_FollowBehavior.GetFollowTarget()->GetAbsOrigin(), Vector( 120, 120, 120 ) ); CItem *pItem = dynamic_cast(pBase); if( pItem ) { SetTarget( pItem ); return SCHED_GET_HEALTHKIT; } } } return SCHED_NONE; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- int CNPC_Citizen::SelectScheduleNonCombat() { if ( m_NPCState == NPC_STATE_IDLE ) { // Handle being inspected by the scanner if ( HasCondition( COND_CIT_START_INSPECTION ) ) { ClearCondition( COND_CIT_START_INSPECTION ); return SCHED_CITIZEN_PLAY_INSPECT_ACTIVITY; } } ClearCondition( COND_CIT_START_INSPECTION ); if ( m_bShouldPatrol ) return SCHED_CITIZEN_PATROL; return SCHED_NONE; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- int CNPC_Citizen::SelectScheduleManhackCombat() { if ( m_NPCState == NPC_STATE_COMBAT && IsManhackMeleeCombatant() ) { if ( !HasCondition( COND_CAN_MELEE_ATTACK1 ) ) { float distSqEnemy = ( GetEnemy()->GetAbsOrigin() - EyePosition() ).LengthSqr(); if ( distSqEnemy < 48.0*48.0 && ( ( GetEnemy()->GetAbsOrigin() + GetEnemy()->GetSmoothedVelocity() * .1 ) - EyePosition() ).LengthSqr() < distSqEnemy ) return SCHED_COWER; int iRoll = random->RandomInt( 1, 4 ); if ( iRoll == 1 ) return SCHED_BACK_AWAY_FROM_ENEMY; else if ( iRoll == 2 ) return SCHED_CHASE_ENEMY; } } return SCHED_NONE; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- int CNPC_Citizen::SelectScheduleCombat() { int schedule = SelectScheduleManhackCombat(); if ( schedule != SCHED_NONE ) return schedule; return BaseClass::SelectScheduleCombat(); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::ShouldDeferToFollowBehavior() { #if 0 if ( HaveCommandGoal() ) return false; #endif return BaseClass::ShouldDeferToFollowBehavior(); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- int CNPC_Citizen::TranslateSchedule( int scheduleType ) { CBasePlayer *pLocalPlayer = AI_GetSinglePlayer(); switch( scheduleType ) { case SCHED_IDLE_STAND: case SCHED_ALERT_STAND: if( m_NPCState != NPC_STATE_COMBAT && pLocalPlayer && !pLocalPlayer->IsAlive() && CanJoinPlayerSquad() ) { // Player is dead! float flDist; flDist = ( pLocalPlayer->GetAbsOrigin() - GetAbsOrigin() ).Length(); if( flDist < 50 * 12 ) { AddSpawnFlags( SF_CITIZEN_NOT_COMMANDABLE ); return SCHED_CITIZEN_MOURN_PLAYER; } } break; case SCHED_ESTABLISH_LINE_OF_FIRE: case SCHED_MOVE_TO_WEAPON_RANGE: if( !IsMortar( GetEnemy() ) && HaveCommandGoal() ) { if ( GetActiveWeapon() && ( GetActiveWeapon()->CapabilitiesGet() & bits_CAP_WEAPON_RANGE_ATTACK1 ) && random->RandomInt( 0, 1 ) && HasCondition(COND_SEE_ENEMY) && !HasCondition ( COND_NO_PRIMARY_AMMO ) ) return TranslateSchedule( SCHED_RANGE_ATTACK1 ); return SCHED_STANDOFF; } break; case SCHED_CHASE_ENEMY: if( !IsMortar( GetEnemy() ) && HaveCommandGoal() ) { return SCHED_STANDOFF; } break; case SCHED_RANGE_ATTACK1: // If we have an RPG, we use a custom schedule for it if ( !IsMortar( GetEnemy() ) && GetActiveWeapon() && FClassnameIs( GetActiveWeapon(), "weapon_rpg" ) ) { if ( GetEnemy() && GetEnemy()->ClassMatches( "npc_strider" ) ) { if (OccupyStrategySlotRange( SQUAD_SLOT_CITIZEN_RPG1, SQUAD_SLOT_CITIZEN_RPG2 ) ) { return SCHED_CITIZEN_STRIDER_RANGE_ATTACK1_RPG; } else { return SCHED_STANDOFF; } } else { CBasePlayer *pPlayer = AI_GetSinglePlayer(); if ( pPlayer && GetEnemy() && ( ( GetEnemy()->GetAbsOrigin() - pPlayer->GetAbsOrigin() ).LengthSqr() < RPG_SAFE_DISTANCE * RPG_SAFE_DISTANCE ) ) { // Don't fire our RPG at an enemy too close to the player return SCHED_STANDOFF; } else { return SCHED_CITIZEN_RANGE_ATTACK1_RPG; } } } break; } return BaseClass::TranslateSchedule( scheduleType ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::ShouldAcceptGoal( CAI_BehaviorBase *pBehavior, CAI_GoalEntity *pGoal ) { if ( BaseClass::ShouldAcceptGoal( pBehavior, pGoal ) ) { CAI_FollowBehavior *pFollowBehavior = dynamic_cast(pBehavior ); if ( pFollowBehavior ) { if ( IsInPlayerSquad() ) { m_hSavedFollowGoalEnt = (CAI_FollowGoal *)pGoal; return false; } } return true; } return false; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::OnClearGoal( CAI_BehaviorBase *pBehavior, CAI_GoalEntity *pGoal ) { if ( m_hSavedFollowGoalEnt == pGoal ) m_hSavedFollowGoalEnt = NULL; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::StartTask( const Task_t *pTask ) { switch( pTask->iTask ) { case TASK_CIT_PLAY_INSPECT_SEQUENCE: SetIdealActivity( (Activity) m_nInspectActivity ); break; case TASK_CIT_SIT_ON_TRAIN: if ( NameMatches("citizen_train_2") ) { SetSequenceByName( "d1_t01_TrainRide_Sit_Idle" ); SetIdealActivity( ACT_DO_NOT_DISTURB ); } else { SetSequenceByName( "d1_t01_TrainRide_Stand" ); SetIdealActivity( ACT_DO_NOT_DISTURB ); } break; case TASK_CIT_LEAVE_TRAIN: if ( NameMatches("citizen_train_2") ) { SetSequenceByName( "d1_t01_TrainRide_Sit_Exit" ); SetIdealActivity( ACT_DO_NOT_DISTURB ); } else { SetSequenceByName( "d1_t01_TrainRide_Stand_Exit" ); SetIdealActivity( ACT_DO_NOT_DISTURB ); } break; case TASK_CIT_HEAL: #if HL2_EPISODIC case TASK_CIT_HEAL_TOSS: #endif if ( IsMedic() ) { if ( GetTarget() && GetTarget()->IsPlayer() && GetTarget()->m_iMaxHealth == GetTarget()->m_iHealth ) { // Doesn't need us anymore TaskComplete(); break; } Speak( TLK_HEAL ); } else if ( IsAmmoResupplier() ) { Speak( TLK_GIVEAMMO ); } SetIdealActivity( (Activity)ACT_CIT_HEAL ); break; case TASK_CIT_RPG_AUGER: m_bRPGAvoidPlayer = false; SetWait( 15.0 ); // maximum time auger before giving up break; case TASK_CIT_SPEAK_MOURNING: if ( !IsSpeaking() && CanSpeakAfterMyself() ) { //CAI_AllySpeechManager *pSpeechManager = GetAllySpeechManager(); //if ( pSpeechManager-> ) Speak(TLK_PLDEAD); } TaskComplete(); break; default: BaseClass::StartTask( pTask ); break; } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::RunTask( const Task_t *pTask ) { switch( pTask->iTask ) { case TASK_WAIT_FOR_MOVEMENT: { if ( IsManhackMeleeCombatant() ) { AddFacingTarget( GetEnemy(), 1.0, 0.5 ); } BaseClass::RunTask( pTask ); break; } case TASK_MOVE_TO_TARGET_RANGE: { // If we're moving to heal a target, and the target dies, stop if ( IsCurSchedule( SCHED_CITIZEN_HEAL ) && (!GetTarget() || !GetTarget()->IsAlive()) ) { TaskFail(FAIL_NO_TARGET); return; } BaseClass::RunTask( pTask ); break; } case TASK_CIT_PLAY_INSPECT_SEQUENCE: { AutoMovement(); if ( IsSequenceFinished() ) { TaskComplete(); } break; } case TASK_CIT_SIT_ON_TRAIN: { // If we were on a train, but we're not anymore, enable movement if ( !GetMoveParent() ) { SetMoveType( MOVETYPE_STEP ); CapabilitiesAdd( bits_CAP_MOVE_GROUND ); TaskComplete(); } break; } case TASK_CIT_LEAVE_TRAIN: { if ( IsSequenceFinished() ) { SetupVPhysicsHull(); TaskComplete(); } break; } case TASK_CIT_HEAL: if ( IsSequenceFinished() ) { TaskComplete(); } else if (!GetTarget()) { // Our heal target was killed or deleted somehow. TaskFail(FAIL_NO_TARGET); } else { if ( ( GetTarget()->GetAbsOrigin() - GetAbsOrigin() ).Length2D() > HEAL_MOVE_RANGE/2 ) TaskComplete(); GetMotor()->SetIdealYawToTargetAndUpdate( GetTarget()->GetAbsOrigin() ); } break; #if HL2_EPISODIC case TASK_CIT_HEAL_TOSS: if ( IsSequenceFinished() ) { TaskComplete(); } else if (!GetTarget()) { // Our heal target was killed or deleted somehow. TaskFail(FAIL_NO_TARGET); } else { GetMotor()->SetIdealYawToTargetAndUpdate( GetTarget()->GetAbsOrigin() ); } break; #endif case TASK_CIT_RPG_AUGER: { // Keep augering until the RPG has been destroyed CWeaponRPG *pRPG = dynamic_cast(GetActiveWeapon()); if ( !pRPG ) { TaskFail( FAIL_ITEM_NO_FIND ); return; } // Has the RPG detonated? if ( !pRPG->GetMissile() ) { pRPG->StopGuiding(); TaskComplete(); return; } Vector vecLaserPos = pRPG->GetNPCLaserPosition(); if ( !m_bRPGAvoidPlayer ) { // Abort if we've lost our enemy if ( !GetEnemy() ) { pRPG->StopGuiding(); TaskFail( FAIL_NO_ENEMY ); return; } // Is our enemy occluded? if ( HasCondition( COND_ENEMY_OCCLUDED ) ) { // Turn off the laserdot, but don't stop augering pRPG->StopGuiding(); return; } else if ( pRPG->IsGuiding() == false ) { pRPG->StartGuiding(); } Vector vecEnemyPos = GetEnemy()->BodyTarget(GetAbsOrigin(), false); CBasePlayer *pPlayer = AI_GetSinglePlayer(); if ( pPlayer && ( ( vecEnemyPos - pPlayer->GetAbsOrigin() ).LengthSqr() < RPG_SAFE_DISTANCE * RPG_SAFE_DISTANCE ) ) { m_bRPGAvoidPlayer = true; Speak( TLK_WATCHOUT ); } else { // Pull the laserdot towards the target Vector vecToTarget = (vecEnemyPos - vecLaserPos); float distToMove = VectorNormalize( vecToTarget ); if ( distToMove > 90 ) distToMove = 90; vecLaserPos += vecToTarget * distToMove; } } if ( m_bRPGAvoidPlayer ) { // Pull the laserdot up vecLaserPos.z += 90; } if ( IsWaitFinished() ) { pRPG->StopGuiding(); TaskFail( FAIL_NO_SHOOT ); return; } // Add imprecision to avoid obvious robotic perfection stationary targets float imprecision = 18*sin(gpGlobals->curtime); vecLaserPos.x += imprecision; vecLaserPos.y += imprecision; vecLaserPos.z += imprecision; pRPG->UpdateNPCLaserPosition( vecLaserPos ); } break; default: BaseClass::RunTask( pTask ); break; } } //----------------------------------------------------------------------------- // Purpose: // Input : code - //----------------------------------------------------------------------------- void CNPC_Citizen::TaskFail( AI_TaskFailureCode_t code ) { // If our heal task has failed, push out the heal time if ( IsCurSchedule( SCHED_CITIZEN_HEAL ) ) { m_flPlayerHealTime = gpGlobals->curtime + sk_citizen_heal_ally_delay.GetFloat(); } if( code == FAIL_NO_ROUTE_BLOCKED && m_bNotifyNavFailBlocked ) { m_OnNavFailBlocked.FireOutput( this, this ); } BaseClass::TaskFail( code ); } //----------------------------------------------------------------------------- // Purpose: Override base class activiites //----------------------------------------------------------------------------- Activity CNPC_Citizen::NPC_TranslateActivity( Activity activity ) { if ( activity == ACT_MELEE_ATTACK1 ) { return ACT_MELEE_ATTACK_SWING; } // !!!HACK - Citizens don't have the required animations for shotguns, // so trick them into using the rifle counterparts for now (sjb) if ( activity == ACT_RUN_AIM_SHOTGUN ) return ACT_RUN_AIM_RIFLE; if ( activity == ACT_WALK_AIM_SHOTGUN ) return ACT_WALK_AIM_RIFLE; if ( activity == ACT_IDLE_ANGRY_SHOTGUN ) return ACT_IDLE_ANGRY_SMG1; if ( activity == ACT_RANGE_ATTACK_SHOTGUN_LOW ) return ACT_RANGE_ATTACK_SMG1_LOW; return BaseClass::NPC_TranslateActivity( activity ); } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ void CNPC_Citizen::HandleAnimEvent( animevent_t *pEvent ) { if ( pEvent->event == AE_CITIZEN_GET_PACKAGE ) { // Give the citizen a package CBaseCombatWeapon *pWeapon = Weapon_Create( "weapon_citizenpackage" ); if ( pWeapon ) { // If I have a name, make my weapon match it with "_weapon" appended if ( GetEntityName() != NULL_STRING ) { pWeapon->SetName( AllocPooledString(UTIL_VarArgs("%s_weapon", STRING(GetEntityName()) )) ); } Weapon_Equip( pWeapon ); } return; } else if ( pEvent->event == AE_CITIZEN_HEAL ) { // Heal my target (if within range) #if HL2_EPISODIC if ( USE_EXPERIMENTAL_MEDIC_CODE() && IsMedic() ) { CBaseCombatCharacter *pTarget = dynamic_cast( GetTarget() ); Assert(pTarget); if ( pTarget ) { m_flPlayerHealTime = gpGlobals->curtime + sk_citizen_heal_toss_player_delay.GetFloat();; TossHealthKit( pTarget, Vector(48.0f, 0.0f, 0.0f) ); } } else { Heal(); } #else Heal(); #endif return; } switch( pEvent->event ) { case NPC_EVENT_LEFTFOOT: { EmitSound( "NPC_Citizen.FootstepLeft", pEvent->eventtime ); } break; case NPC_EVENT_RIGHTFOOT: { EmitSound( "NPC_Citizen.FootstepRight", pEvent->eventtime ); } break; default: BaseClass::HandleAnimEvent( pEvent ); break; } } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ void CNPC_Citizen::PickupItem( CBaseEntity *pItem ) { Assert( pItem != NULL ); if( FClassnameIs( pItem, "item_healthkit" ) ) { if ( TakeHealth( sk_healthkit.GetFloat(), DMG_GENERIC ) ) { RemoveAllDecals(); UTIL_Remove( pItem ); } } else if( FClassnameIs( pItem, "item_healthvial" ) ) { if ( TakeHealth( sk_healthvial.GetFloat(), DMG_GENERIC ) ) { RemoveAllDecals(); UTIL_Remove( pItem ); } } else { DevMsg("Citizen doesn't know how to pick up %s!\n", pItem->GetClassname() ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CNPC_Citizen::IgnorePlayerPushing( void ) { // If the NPC's on a func_tank that the player cannot man, ignore player pushing if ( m_FuncTankBehavior.IsMounted() ) { CFuncTank *pTank = m_FuncTankBehavior.GetFuncTank(); if ( pTank && !pTank->IsControllable() ) return true; } return false; } //----------------------------------------------------------------------------- // Purpose: Return a random expression for the specified state to play over // the state's expression loop. //----------------------------------------------------------------------------- const char *CNPC_Citizen::SelectRandomExpressionForState( NPC_STATE state ) { // Hacky remap of NPC states to expression states that we care about int iExpressionState = 0; switch ( state ) { case NPC_STATE_IDLE: iExpressionState = 0; break; case NPC_STATE_ALERT: iExpressionState = 1; break; case NPC_STATE_COMBAT: iExpressionState = 2; break; default: // An NPC state we don't have expressions for return NULL; } // Now pick the right one for our expression type switch ( m_ExpressionType ) { case CIT_EXP_SCARED: { int iRandom = RandomInt( 0, ARRAYSIZE(ScaredExpressions[iExpressionState].szExpressions)-1 ); return ScaredExpressions[iExpressionState].szExpressions[iRandom]; } case CIT_EXP_NORMAL: { int iRandom = RandomInt( 0, ARRAYSIZE(NormalExpressions[iExpressionState].szExpressions)-1 ); return NormalExpressions[iExpressionState].szExpressions[iRandom]; } case CIT_EXP_ANGRY: { int iRandom = RandomInt( 0, ARRAYSIZE(AngryExpressions[iExpressionState].szExpressions)-1 ); return AngryExpressions[iExpressionState].szExpressions[iRandom]; } default: break; } return NULL; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::SimpleUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) { // Under these conditions, citizens will refuse to go with the player. // Robin: NPCs should always respond to +USE even if someone else has the semaphore. m_bDontUseSemaphore = true; // First, try to speak the +USE concept if ( !SelectPlayerUseSpeech() ) { if ( HasSpawnFlags(SF_CITIZEN_NOT_COMMANDABLE) || IRelationType( pActivator ) == D_NU ) { // If I'm denying commander mode because a level designer has made that decision, // then fire this output in case they've hooked it to an event. m_OnDenyCommanderUse.FireOutput( this, this ); } } m_bDontUseSemaphore = false; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::OnBeginMoveAndShoot() { if ( BaseClass::OnBeginMoveAndShoot() ) { if( m_iMySquadSlot == SQUAD_SLOT_ATTACK1 || m_iMySquadSlot == SQUAD_SLOT_ATTACK2 ) return true; // already have the slot I need if( m_iMySquadSlot == SQUAD_SLOT_NONE && OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) ) return true; } return false; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::OnEndMoveAndShoot() { VacateStrategySlot(); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::LocateEnemySound() { #if 0 if ( !GetEnemy() ) return; float flZDiff = GetLocalOrigin().z - GetEnemy()->GetLocalOrigin().z; if( flZDiff < -128 ) { EmitSound( "NPC_Citizen.UpThere" ); } else if( flZDiff > 128 ) { EmitSound( "NPC_Citizen.DownThere" ); } else { EmitSound( "NPC_Citizen.OverHere" ); } #endif } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::IsManhackMeleeCombatant() { CBaseCombatWeapon *pWeapon = GetActiveWeapon(); CBaseEntity *pEnemy = GetEnemy(); return ( pEnemy && pWeapon && pEnemy->Classify() == CLASS_MANHACK && pWeapon->ClassMatches( "weapon_crowbar" ) ); } //----------------------------------------------------------------------------- // Purpose: Return the actual position the NPC wants to fire at when it's trying // to hit it's current enemy. //----------------------------------------------------------------------------- Vector CNPC_Citizen::GetActualShootPosition( const Vector &shootOrigin ) { Vector vecTarget = BaseClass::GetActualShootPosition( shootOrigin ); CWeaponRPG *pRPG = dynamic_cast(GetActiveWeapon()); // If we're firing an RPG at a gunship, aim off to it's side, because we'll auger towards it. if ( pRPG && GetEnemy() ) { if ( FClassnameIs( GetEnemy(), "npc_combinegunship" ) ) { Vector vecRight; GetVectors( NULL, &vecRight, NULL ); // Random height vecRight.z = 0; // Find a clear shot by checking for clear shots around it float flShotOffsets[] = { 512, -512, 128, -128 }; for ( int i = 0; i < ARRAYSIZE(flShotOffsets); i++ ) { Vector vecTest = vecTarget + (vecRight * flShotOffsets[i]); // Add some random height to it vecTest.z += RandomFloat( -512, 512 ); trace_t tr; AI_TraceLine( shootOrigin, vecTest, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr); // If we can see the point, it's a clear shot if ( tr.fraction == 1.0 && tr.m_pEnt != GetEnemy() ) { pRPG->SetNPCLaserPosition( vecTest ); return vecTest; } } } else { pRPG->SetNPCLaserPosition( vecTarget ); } } return vecTarget; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::OnChangeActiveWeapon( CBaseCombatWeapon *pOldWeapon, CBaseCombatWeapon *pNewWeapon ) { if ( pNewWeapon ) { GetShotRegulator()->SetParameters( pNewWeapon->GetMinBurst(), pNewWeapon->GetMaxBurst(), pNewWeapon->GetMinRestTime(), pNewWeapon->GetMaxRestTime() ); } BaseClass::OnChangeActiveWeapon( pOldWeapon, pNewWeapon ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- #define SHOTGUN_DEFER_SEARCH_TIME 20.0f #define OTHER_DEFER_SEARCH_TIME FLT_MAX bool CNPC_Citizen::ShouldLookForBetterWeapon() { if ( BaseClass::ShouldLookForBetterWeapon() ) { if ( IsInPlayerSquad() && (GetActiveWeapon()&&IsMoving()) && ( m_FollowBehavior.GetFollowTarget() && m_FollowBehavior.GetFollowTarget()->IsPlayer() ) ) { // For citizens in the player squad, you must be unarmed, or standing still (if armed) in order to // divert attention to looking for a new weapon. return false; } if ( GetActiveWeapon() && IsMoving() ) return false; if ( GlobalEntity_GetState("gordon_precriminal") == GLOBAL_ON ) { // This stops the NPC looking altogether. m_flNextWeaponSearchTime = FLT_MAX; return false; } #ifdef DBGFLAG_ASSERT // Cached off to make sure you change this if you ask the code to defer. float flOldWeaponSearchTime = m_flNextWeaponSearchTime; #endif CBaseCombatWeapon *pWeapon = GetActiveWeapon(); if( pWeapon ) { bool bDefer = false; if( FClassnameIs( pWeapon, "weapon_ar2" ) ) { // Content to keep this weapon forever m_flNextWeaponSearchTime = OTHER_DEFER_SEARCH_TIME; bDefer = true; } else if( FClassnameIs( pWeapon, "weapon_rpg" ) ) { // Content to keep this weapon forever m_flNextWeaponSearchTime = OTHER_DEFER_SEARCH_TIME; bDefer = true; } else if( FClassnameIs( pWeapon, "weapon_shotgun" ) ) { // Shotgunners do not defer their weapon search indefinitely. // If more than one citizen in the squad has a shotgun, we force // some of them to trade for another weapon. if( NumWeaponsInSquad("weapon_shotgun") > 1 ) { // Check for another weapon now. If I don't find one, this code will // retry in 2 seconds or so. bDefer = false; } else { // I'm the only shotgunner in the group right now, so I'll check // again in 3 0seconds or so. This code attempts to distribute // the desire to reduce shotguns amongst squadmates so that all // shotgunners do not discard their weapons when they suddenly realize // the squad has too many. if( random->RandomInt( 0, 1 ) == 0 ) { m_flNextWeaponSearchTime = gpGlobals->curtime + SHOTGUN_DEFER_SEARCH_TIME; } else { m_flNextWeaponSearchTime = gpGlobals->curtime + SHOTGUN_DEFER_SEARCH_TIME + 10.0f; } bDefer = true; } } if( bDefer ) { // I'm happy with my current weapon. Don't search now. // If you ask the code to defer, you must have set m_flNextWeaponSearchTime to when // you next want to try to search. Assert( m_flNextWeaponSearchTime != flOldWeaponSearchTime ); return false; } } return true; } return false; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- int CNPC_Citizen::OnTakeDamage_Alive( const CTakeDamageInfo &info ) { if( (info.GetDamageType() & DMG_BURN) && (info.GetDamageType() & DMG_DIRECT) ) { #define CITIZEN_SCORCH_RATE 6 #define CITIZEN_SCORCH_FLOOR 75 Scorch( CITIZEN_SCORCH_RATE, CITIZEN_SCORCH_FLOOR ); } CTakeDamageInfo newInfo = info; if( IsInSquad() && (info.GetDamageType() & DMG_BLAST) && info.GetInflictor() ) { if( npc_citizen_explosive_resist.GetBool() ) { // Blast damage. If this kills a squad member, give the // remaining citizens a resistance bonus to this inflictor // to try to avoid having the entire squad wiped out by a // single explosion. if( m_pSquad->IsSquadInflictor( info.GetInflictor() ) ) { newInfo.ScaleDamage( 0.5 ); } else { // If this blast is going to kill me, designate the inflictor // so that the rest of the squad can enjoy a damage resist. if( info.GetDamage() >= GetHealth() ) { m_pSquad->SetSquadInflictor( info.GetInflictor() ); } } } } return BaseClass::OnTakeDamage_Alive( newInfo ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::IsCommandable() { return ( !HasSpawnFlags(SF_CITIZEN_NOT_COMMANDABLE) && IsInPlayerSquad() ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::IsPlayerAlly( CBasePlayer *pPlayer ) { if ( Classify() == CLASS_CITIZEN_PASSIVE && GlobalEntity_GetState("gordon_precriminal") == GLOBAL_ON ) { // Robin: Citizens use friendly speech semaphore in trainstation return true; } return BaseClass::IsPlayerAlly( pPlayer ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::CanJoinPlayerSquad() { if ( !AI_IsSinglePlayer() ) return false; if ( m_NPCState == NPC_STATE_SCRIPT || m_NPCState == NPC_STATE_PRONE ) return false; if ( HasSpawnFlags(SF_CITIZEN_NOT_COMMANDABLE) ) return false; if ( IsInAScript() ) return false; // Don't bother people who don't want to be bothered if ( !CanBeUsedAsAFriend() ) return false; if ( IRelationType( UTIL_GetLocalPlayer() ) != D_LI ) return false; return true; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::WasInPlayerSquad() { return m_bWasInPlayerSquad; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::HaveCommandGoal() const { if (GetCommandGoal() != vec3_invalid) return true; return false; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::IsCommandMoving() { if ( AI_IsSinglePlayer() && IsInPlayerSquad() ) { if ( m_FollowBehavior.GetFollowTarget() == UTIL_GetLocalPlayer() || IsFollowingCommandPoint() ) { return ( m_FollowBehavior.IsMovingToFollowTarget() ); } } return false; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::ShouldAutoSummon() { if ( !AI_IsSinglePlayer() || !IsFollowingCommandPoint() || !IsInPlayerSquad() ) return false; CHL2_Player *pPlayer = (CHL2_Player *)UTIL_GetLocalPlayer(); float distMovedSq = ( pPlayer->GetAbsOrigin() - m_vAutoSummonAnchor ).LengthSqr(); float moveTolerance = player_squad_autosummon_move_tolerance.GetFloat() * 12; const Vector &vCommandGoal = GetCommandGoal(); if ( distMovedSq < Square(moveTolerance * 10) && (GetAbsOrigin() - vCommandGoal).LengthSqr() > Square(10*12) && IsCommandMoving() ) { m_AutoSummonTimer.Set( player_squad_autosummon_time.GetFloat() ); if ( player_squad_autosummon_debug.GetBool() ) DevMsg( "Waiting for arrival before initiating autosummon logic\n"); } else if ( m_AutoSummonTimer.Expired() ) { bool bSetFollow = false; bool bTestEnemies = true; // Auto summon unconditionally if a significant amount of time has passed if ( gpGlobals->curtime - m_AutoSummonTimer.GetNext() > player_squad_autosummon_time.GetFloat() * 2 ) { bSetFollow = true; if ( player_squad_autosummon_debug.GetBool() ) DevMsg( "Auto summoning squad: long time (%f)\n", ( gpGlobals->curtime - m_AutoSummonTimer.GetNext() ) + player_squad_autosummon_time.GetFloat() ); } // Player must move for autosummon if ( distMovedSq > Square(12) ) { bool bCommandPointIsVisible = pPlayer->FVisible( vCommandGoal + pPlayer->GetViewOffset() ); // Auto summon if the player is close by the command point if ( !bSetFollow && bCommandPointIsVisible && distMovedSq > Square(24) ) { float closenessTolerance = player_squad_autosummon_player_tolerance.GetFloat() * 12; if ( (pPlayer->GetAbsOrigin() - vCommandGoal).LengthSqr() < Square( closenessTolerance ) && ((m_vAutoSummonAnchor - vCommandGoal).LengthSqr() > Square( closenessTolerance )) ) { bSetFollow = true; if ( player_squad_autosummon_debug.GetBool() ) DevMsg( "Auto summoning squad: player close to command point (%f)\n", (GetAbsOrigin() - vCommandGoal).Length() ); } } // Auto summon if moved a moderate distance and can't see command point, or moved a great distance if ( !bSetFollow ) { if ( distMovedSq > Square( moveTolerance * 2 ) ) { bSetFollow = true; bTestEnemies = ( distMovedSq < Square( moveTolerance * 10 ) ); if ( player_squad_autosummon_debug.GetBool() ) DevMsg( "Auto summoning squad: player very far from anchor (%f)\n", sqrt(distMovedSq) ); } else if ( distMovedSq > Square( moveTolerance ) ) { if ( !bCommandPointIsVisible ) { bSetFollow = true; if ( player_squad_autosummon_debug.GetBool() ) DevMsg( "Auto summoning squad: player far from anchor (%f)\n", sqrt(distMovedSq) ); } } } } // Auto summon only if there are no readily apparent enemies if ( bSetFollow && bTestEnemies ) { for ( int i = 0; i < g_AI_Manager.NumAIs(); i++ ) { CAI_BaseNPC *pNpc = g_AI_Manager.AccessAIs()[i]; float timeSinceCombatTolerance = player_squad_autosummon_time_after_combat.GetFloat(); if ( pNpc->IsInPlayerSquad() ) { if ( gpGlobals->curtime - pNpc->GetLastAttackTime() > timeSinceCombatTolerance || gpGlobals->curtime - pNpc->GetLastDamageTime() > timeSinceCombatTolerance ) continue; } else if ( pNpc->GetEnemy() ) { CBaseEntity *pNpcEnemy = pNpc->GetEnemy(); if ( !IsSniper( pNpc ) && ( gpGlobals->curtime - pNpc->GetEnemyLastTimeSeen() ) > timeSinceCombatTolerance ) continue; if ( pNpcEnemy == pPlayer ) { if ( pNpc->CanBeAnEnemyOf( pPlayer ) ) { bSetFollow = false; break; } } else if ( pNpcEnemy->IsNPC() && ( pNpcEnemy->MyNPCPointer()->GetSquad() == GetSquad() || pNpcEnemy->Classify() == CLASS_PLAYER_ALLY_VITAL ) ) { if ( pNpc->CanBeAnEnemyOf( this ) ) { bSetFollow = false; break; } } } } if ( !bSetFollow && player_squad_autosummon_debug.GetBool() ) DevMsg( "Auto summon REVOKED: Combat recent \n"); } return bSetFollow; } return false; } //----------------------------------------------------------------------------- // Is this entity something that the citizen should interact with (return true) // or something that he should try to get close to (return false) //----------------------------------------------------------------------------- bool CNPC_Citizen::IsValidCommandTarget( CBaseEntity *pTarget ) { return false; } //----------------------------------------------------------------------------- bool CNPC_Citizen::SpeakCommandResponse( AIConcept_t concept, const char *modifiers ) { return SpeakIfAllowed( concept, CFmtStr( "numselected:%d," "useradio:%d%s", ( GetSquad() ) ? GetSquad()->NumMembers() : 1, ShouldSpeakRadio( AI_GetSinglePlayer() ), ( modifiers ) ? CFmtStr(",%s", modifiers).operator const char *() : "" ) ); } //----------------------------------------------------------------------------- // Purpose: return TRUE if the commander mode should try to give this order // to more people. return FALSE otherwise. For instance, we don't // try to send all 3 selectedcitizens to pick up the same gun. //----------------------------------------------------------------------------- bool CNPC_Citizen::TargetOrder( CBaseEntity *pTarget, CAI_BaseNPC **Allies, int numAllies ) { if ( pTarget->IsPlayer() ) { // I'm the target! Toggle follow! if( m_FollowBehavior.GetFollowTarget() != pTarget ) { ClearFollowTarget(); SetCommandGoal( vec3_invalid ); // Turn follow on! m_AssaultBehavior.Disable(); m_FollowBehavior.SetFollowTarget( pTarget ); m_FollowBehavior.SetParameters( AIF_SIMPLE ); SpeakCommandResponse( TLK_STARTFOLLOW ); m_OnFollowOrder.FireOutput( this, this ); } else if ( m_FollowBehavior.GetFollowTarget() == pTarget ) { // Stop following. m_FollowBehavior.SetFollowTarget( NULL ); SpeakCommandResponse( TLK_STOPFOLLOW ); } } return true; } //----------------------------------------------------------------------------- // Purpose: Turn off following before processing a move order. //----------------------------------------------------------------------------- void CNPC_Citizen::MoveOrder( const Vector &vecDest, CAI_BaseNPC **Allies, int numAllies ) { if ( !AI_IsSinglePlayer() ) return; if( hl2_episodic.GetBool() && m_iszDenyCommandConcept != NULL_STRING ) { SpeakCommandResponse( STRING(m_iszDenyCommandConcept) ); return; } CHL2_Player *pPlayer = (CHL2_Player *)UTIL_GetLocalPlayer(); m_AutoSummonTimer.Set( player_squad_autosummon_time.GetFloat() ); m_vAutoSummonAnchor = pPlayer->GetAbsOrigin(); if( m_StandoffBehavior.IsRunning() ) { m_StandoffBehavior.SetStandoffGoalPosition( vecDest ); } // If in assault, cancel and move. if( m_AssaultBehavior.HasHitRallyPoint() && !m_AssaultBehavior.HasHitAssaultPoint() ) { m_AssaultBehavior.Disable(); ClearSchedule( "Moving from rally point to assault point" ); } bool spoke = false; CAI_BaseNPC *pClosest = NULL; float closestDistSq = FLT_MAX; for( int i = 0 ; i < numAllies ; i++ ) { if( Allies[i]->IsInPlayerSquad() ) { Assert( Allies[i]->IsCommandable() ); float distSq = ( pPlayer->GetAbsOrigin() - Allies[i]->GetAbsOrigin() ).LengthSqr(); if( distSq < closestDistSq ) { pClosest = Allies[i]; closestDistSq = distSq; } } } if( m_FollowBehavior.GetFollowTarget() && !IsFollowingCommandPoint() ) { ClearFollowTarget(); #if 0 if ( ( pPlayer->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr() < Square( 180 ) && ( ( vecDest - pPlayer->GetAbsOrigin() ).LengthSqr() < Square( 120 ) || ( vecDest - GetAbsOrigin() ).LengthSqr() < Square( 120 ) ) ) { if ( pClosest == this ) SpeakIfAllowed( TLK_STOPFOLLOW ); spoke = true; } #endif } if ( !spoke && pClosest == this ) { float destDistToPlayer = ( vecDest - pPlayer->GetAbsOrigin() ).Length(); float destDistToClosest = ( vecDest - GetAbsOrigin() ).Length(); CFmtStr modifiers( "commandpoint_dist_to_player:%.0f," "commandpoint_dist_to_npc:%.0f", destDistToPlayer, destDistToClosest ); SpeakCommandResponse( TLK_COMMANDED, modifiers ); } m_OnStationOrder.FireOutput( this, this ); BaseClass::MoveOrder( vecDest, Allies, numAllies ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::OnMoveOrder() { SetReadinessLevel( AIRL_STIMULATED, false, false ); BaseClass::OnMoveOrder(); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::CommanderUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) { m_OnPlayerUse.FireOutput( pActivator, pCaller ); // Under these conditions, citizens will refuse to go with the player. // Robin: NPCs should always respond to +USE even if someone else has the semaphore. if ( !AI_IsSinglePlayer() || !CanJoinPlayerSquad() ) { SimpleUse( pActivator, pCaller, useType, value ); return; } if ( pActivator == UTIL_GetLocalPlayer() ) { // Don't say hi after you've been addressed by the player SetSpokeConcept( TLK_HELLO, NULL ); if ( npc_citizen_auto_player_squad_allow_use.GetBool() ) { if ( !ShouldAutosquad() ) TogglePlayerSquadState(); else if ( !IsInPlayerSquad() && npc_citizen_auto_player_squad_allow_use.GetBool() ) AddToPlayerSquad(); } else if ( GetCurSchedule() && ConditionInterruptsCurSchedule( COND_IDLE_INTERRUPT ) ) { if ( SpeakIfAllowed( TLK_QUESTION, NULL, true ) ) { if ( random->RandomInt( 1, 4 ) < 4 ) { CBaseEntity *pRespondant = FindSpeechTarget( AIST_NPCS ); if ( pRespondant ) { g_EventQueue.AddEvent( pRespondant, "SpeakIdleResponse", ( GetTimeSpeechComplete() - gpGlobals->curtime ) + .2, this, this ); } } } } } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::ShouldSpeakRadio( CBaseEntity *pListener ) { if ( !pListener ) return false; const float radioRange = 384 * 384; Vector vecDiff; vecDiff = WorldSpaceCenter() - pListener->WorldSpaceCenter(); if( vecDiff.LengthSqr() > radioRange ) { return true; } return false; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::OnMoveToCommandGoalFailed() { // Clear the goal. SetCommandGoal( vec3_invalid ); // Announce failure. SpeakCommandResponse( TLK_COMMAND_FAILED ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::AddToPlayerSquad() { Assert( !IsInPlayerSquad() ); AddToSquad( AllocPooledString(PLAYER_SQUADNAME) ); m_hSavedFollowGoalEnt = m_FollowBehavior.GetFollowGoal(); m_FollowBehavior.SetFollowGoalDirect( NULL ); FixupPlayerSquad(); SetCondition( COND_PLAYER_ADDED_TO_SQUAD ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::RemoveFromPlayerSquad() { Assert( IsInPlayerSquad() ); ClearFollowTarget(); ClearCommandGoal(); if ( m_iszOriginalSquad != NULL_STRING && strcmp( STRING( m_iszOriginalSquad ), PLAYER_SQUADNAME ) != 0 ) AddToSquad( m_iszOriginalSquad ); else RemoveFromSquad(); if ( m_hSavedFollowGoalEnt ) m_FollowBehavior.SetFollowGoal( m_hSavedFollowGoalEnt ); SetCondition( COND_PLAYER_REMOVED_FROM_SQUAD ); // Don't evaluate the player squad for 2 seconds. gm_PlayerSquadEvaluateTimer.Set( 2.0 ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::TogglePlayerSquadState() { if ( !AI_IsSinglePlayer() ) return; if ( !IsInPlayerSquad() ) { AddToPlayerSquad(); if ( HaveCommandGoal() ) { SpeakCommandResponse( TLK_COMMANDED ); } else if ( m_FollowBehavior.GetFollowTarget() == UTIL_GetLocalPlayer() ) { SpeakCommandResponse( TLK_STARTFOLLOW ); } } else { SpeakCommandResponse( TLK_STOPFOLLOW ); RemoveFromPlayerSquad(); } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- struct SquadCandidate_t { CNPC_Citizen *pCitizen; bool bIsInSquad; float distSq; int iSquadIndex; }; void CNPC_Citizen::UpdatePlayerSquad() { if ( !AI_IsSinglePlayer() ) return; CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); if ( ( pPlayer->GetAbsOrigin().AsVector2D() - GetAbsOrigin().AsVector2D() ).LengthSqr() < Square(20*12) ) m_flTimeLastCloseToPlayer = gpGlobals->curtime; if ( !gm_PlayerSquadEvaluateTimer.Expired() ) return; gm_PlayerSquadEvaluateTimer.Set( 2.0 ); // Remove stragglers CAI_Squad *pPlayerSquad = g_AI_SquadManager.FindSquad( MAKE_STRING( PLAYER_SQUADNAME ) ); if ( pPlayerSquad ) { CUtlVectorFixed squadMembersToRemove; AISquadIter_t iter; for ( CAI_BaseNPC *pPlayerSquadMember = pPlayerSquad->GetFirstMember(&iter); pPlayerSquadMember; pPlayerSquadMember = pPlayerSquad->GetNextMember(&iter) ) { if ( pPlayerSquadMember->GetClassname() != GetClassname() ) continue; CNPC_Citizen *pCitizen = assert_cast(pPlayerSquadMember); if ( !pCitizen->m_bNeverLeavePlayerSquad && pCitizen->m_FollowBehavior.GetFollowTarget() && !pCitizen->m_FollowBehavior.FollowTargetVisible() && pCitizen->m_FollowBehavior.GetNumFailedFollowAttempts() > 0 && gpGlobals->curtime - pCitizen->m_FollowBehavior.GetTimeFailFollowStarted() > 20 && ( fabsf(( pCitizen->m_FollowBehavior.GetFollowTarget()->GetAbsOrigin().z - pCitizen->GetAbsOrigin().z )) > 196 || ( pCitizen->m_FollowBehavior.GetFollowTarget()->GetAbsOrigin().AsVector2D() - pCitizen->GetAbsOrigin().AsVector2D() ).LengthSqr() > Square(50*12) ) ) { if ( DebuggingCommanderMode() ) { DevMsg( "Player follower is lost (%d, %f, %d)\n", pCitizen->m_FollowBehavior.GetNumFailedFollowAttempts(), gpGlobals->curtime - pCitizen->m_FollowBehavior.GetTimeFailFollowStarted(), (int)((pCitizen->m_FollowBehavior.GetFollowTarget()->GetAbsOrigin().AsVector2D() - pCitizen->GetAbsOrigin().AsVector2D() ).Length()) ); } squadMembersToRemove.AddToTail( pCitizen ); } } for ( int i = 0; i < squadMembersToRemove.Count(); i++ ) { squadMembersToRemove[i]->RemoveFromPlayerSquad(); } } // Autosquadding const float JOIN_PLAYER_XY_TOLERANCE_SQ = Square(36*12); const float UNCONDITIONAL_JOIN_PLAYER_XY_TOLERANCE_SQ = Square(12*12); const float UNCONDITIONAL_JOIN_PLAYER_Z_TOLERANCE = 5*12; const float SECOND_TIER_JOIN_DIST_SQ = Square(48*12); if ( pPlayer && ShouldAutosquad() && !(pPlayer->GetFlags() & FL_NOTARGET ) && pPlayer->IsAlive() ) { CAI_BaseNPC **ppAIs = g_AI_Manager.AccessAIs(); CUtlVector candidates; const Vector &vPlayerPos = pPlayer->GetAbsOrigin(); bool bFoundNewGuy = false; int i; for ( i = 0; i < g_AI_Manager.NumAIs(); i++ ) { if ( ppAIs[i]->GetState() == NPC_STATE_DEAD ) continue; if ( ppAIs[i]->GetClassname() != GetClassname() ) continue; CNPC_Citizen *pCitizen = assert_cast(ppAIs[i]); int iNew; if ( pCitizen->IsInPlayerSquad() ) { iNew = candidates.AddToTail(); candidates[iNew].pCitizen = pCitizen; candidates[iNew].bIsInSquad = true; candidates[iNew].distSq = 0; candidates[iNew].iSquadIndex = pCitizen->GetSquad()->GetSquadIndex( pCitizen ); } else { float distSq = (vPlayerPos.AsVector2D() - pCitizen->GetAbsOrigin().AsVector2D()).LengthSqr(); if ( distSq > JOIN_PLAYER_XY_TOLERANCE_SQ && ( pCitizen->m_flTimeJoinedPlayerSquad == 0 || gpGlobals->curtime - pCitizen->m_flTimeJoinedPlayerSquad > 60.0 ) && ( pCitizen->m_flTimeLastCloseToPlayer == 0 || gpGlobals->curtime - pCitizen->m_flTimeLastCloseToPlayer > 15.0 ) ) continue; if ( !pCitizen->CanJoinPlayerSquad() ) continue; bool bShouldAdd = false; if ( pCitizen->HasCondition( COND_SEE_PLAYER ) ) bShouldAdd = true; else { bool bPlayerVisible = pCitizen->FVisible( pPlayer ); if ( bPlayerVisible ) { if ( pCitizen->HasCondition( COND_HEAR_PLAYER ) ) bShouldAdd = true; else if ( distSq < UNCONDITIONAL_JOIN_PLAYER_XY_TOLERANCE_SQ && fabsf(vPlayerPos.z - pCitizen->GetAbsOrigin().z) < UNCONDITIONAL_JOIN_PLAYER_Z_TOLERANCE ) bShouldAdd = true; } } if ( bShouldAdd ) { // @TODO (toml 05-25-04): probably everyone in a squad should be a candidate if one of them sees the player AI_Waypoint_t *pPathToPlayer = pCitizen->GetPathfinder()->BuildRoute( pCitizen->GetAbsOrigin(), vPlayerPos, pPlayer, 5*12, NAV_NONE, true ); GetPathfinder()->UnlockRouteNodes( pPathToPlayer ); if ( !pPathToPlayer ) continue; CAI_Path tempPath; tempPath.SetWaypoints( pPathToPlayer ); // path object will delete waypoints iNew = candidates.AddToTail(); candidates[iNew].pCitizen = pCitizen; candidates[iNew].bIsInSquad = false; candidates[iNew].distSq = distSq; candidates[iNew].iSquadIndex = -1; bFoundNewGuy = true; } } } if ( bFoundNewGuy ) { // Look for second order guys int initialCount = candidates.Count(); for ( i = 0; i < initialCount; i++ ) candidates[i].pCitizen->AddSpawnFlags( SF_CITIZEN_NOT_COMMANDABLE ); // Prevents double-add for ( i = 0; i < initialCount; i++ ) { if ( candidates[i].iSquadIndex == -1 ) { for ( int j = 0; j < g_AI_Manager.NumAIs(); j++ ) { if ( ppAIs[j]->GetState() == NPC_STATE_DEAD ) continue; if ( ppAIs[j]->GetClassname() != GetClassname() ) continue; if ( ppAIs[j]->HasSpawnFlags( SF_CITIZEN_NOT_COMMANDABLE ) ) continue; CNPC_Citizen *pCitizen = assert_cast(ppAIs[j]); float distSq = (vPlayerPos - pCitizen->GetAbsOrigin()).Length2DSqr(); if ( distSq > JOIN_PLAYER_XY_TOLERANCE_SQ ) continue; distSq = (candidates[i].pCitizen->GetAbsOrigin() - pCitizen->GetAbsOrigin()).Length2DSqr(); if ( distSq > SECOND_TIER_JOIN_DIST_SQ ) continue; if ( !pCitizen->CanJoinPlayerSquad() ) continue; if ( !pCitizen->FVisible( pPlayer ) ) continue; int iNew = candidates.AddToTail(); candidates[iNew].pCitizen = pCitizen; candidates[iNew].bIsInSquad = false; candidates[iNew].distSq = distSq; candidates[iNew].iSquadIndex = -1; pCitizen->AddSpawnFlags( SF_CITIZEN_NOT_COMMANDABLE ); // Prevents double-add } } } for ( i = 0; i < candidates.Count(); i++ ) candidates[i].pCitizen->RemoveSpawnFlags( SF_CITIZEN_NOT_COMMANDABLE ); if ( candidates.Count() > MAX_PLAYER_SQUAD ) { candidates.Sort( PlayerSquadCandidateSortFunc ); for ( i = MAX_PLAYER_SQUAD; i < candidates.Count(); i++ ) { if ( candidates[i].pCitizen->IsInPlayerSquad() ) { candidates[i].pCitizen->RemoveFromPlayerSquad(); } } } if ( candidates.Count() ) { CNPC_Citizen *pClosest = NULL; float closestDistSq = FLT_MAX; int nJoined = 0; for ( i = 0; i < candidates.Count() && i < MAX_PLAYER_SQUAD; i++ ) { if ( !candidates[i].pCitizen->IsInPlayerSquad() ) { candidates[i].pCitizen->AddToPlayerSquad(); nJoined++; if ( candidates[i].distSq < closestDistSq ) { pClosest = candidates[i].pCitizen; closestDistSq = candidates[i].distSq; } } } if ( pClosest ) { if ( !pClosest->SpokeConcept( TLK_JOINPLAYER ) ) { pClosest->SpeakCommandResponse( TLK_JOINPLAYER, CFmtStr( "numjoining:%d", nJoined ) ); } else { pClosest->SpeakCommandResponse( TLK_STARTFOLLOW ); } for ( i = 0; i < candidates.Count() && i < MAX_PLAYER_SQUAD; i++ ) { candidates[i].pCitizen->SetSpokeConcept( TLK_JOINPLAYER, NULL ); } } } } } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- int CNPC_Citizen::PlayerSquadCandidateSortFunc( const SquadCandidate_t *pLeft, const SquadCandidate_t *pRight ) { // "Bigger" means less approprate CNPC_Citizen *pLeftCitizen = pLeft->pCitizen; CNPC_Citizen *pRightCitizen = pRight->pCitizen; // Medics are better than anyone if ( pLeftCitizen->IsMedic() && !pRightCitizen->IsMedic() ) return -1; if ( !pLeftCitizen->IsMedic() && pRightCitizen->IsMedic() ) return 1; CBaseCombatWeapon *pLeftWeapon = pLeftCitizen->GetActiveWeapon(); CBaseCombatWeapon *pRightWeapon = pRightCitizen->GetActiveWeapon(); // People with weapons are better than those without if ( pLeftWeapon && !pRightWeapon ) return -1; if ( !pLeftWeapon && pRightWeapon ) return 1; // Existing squad members are better than non-members if ( pLeft->bIsInSquad && !pRight->bIsInSquad ) return -1; if ( !pLeft->bIsInSquad && pRight->bIsInSquad ) return 1; // New squad members are better than older ones if ( pLeft->bIsInSquad && pRight->bIsInSquad ) return pRight->iSquadIndex - pLeft->iSquadIndex; // Finally, just take the closer return (int)(pRight->distSq - pLeft->distSq); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::FixupPlayerSquad() { if ( !AI_IsSinglePlayer() ) return; m_flTimeJoinedPlayerSquad = gpGlobals->curtime; m_bWasInPlayerSquad = true; if ( m_pSquad->NumMembers() > MAX_PLAYER_SQUAD ) { CAI_BaseNPC *pFirstMember = m_pSquad->GetFirstMember(NULL); m_pSquad->RemoveFromSquad( pFirstMember ); pFirstMember->ClearCommandGoal(); CNPC_Citizen *pFirstMemberCitizen = dynamic_cast< CNPC_Citizen * >( pFirstMember ); if ( pFirstMemberCitizen ) { pFirstMemberCitizen->ClearFollowTarget(); } else { CAI_FollowBehavior *pOldMemberFollowBehavior; if ( pFirstMember->GetBehavior( &pOldMemberFollowBehavior ) ) { pOldMemberFollowBehavior->SetFollowTarget( NULL ); } } } ClearFollowTarget(); CAI_BaseNPC *pLeader = NULL; AISquadIter_t iter; for ( CAI_BaseNPC *pAllyNpc = m_pSquad->GetFirstMember(&iter); pAllyNpc; pAllyNpc = m_pSquad->GetNextMember(&iter) ) { if ( pAllyNpc->IsCommandable() ) { pLeader = pAllyNpc; break; } } if ( pLeader && pLeader != this ) { const Vector &commandGoal = pLeader->GetCommandGoal(); if ( commandGoal != vec3_invalid ) { SetCommandGoal( commandGoal ); SetCondition( COND_RECEIVED_ORDERS ); OnMoveOrder(); } else { CAI_FollowBehavior *pLeaderFollowBehavior; if ( pLeader->GetBehavior( &pLeaderFollowBehavior ) ) { m_FollowBehavior.SetFollowTarget( pLeaderFollowBehavior->GetFollowTarget() ); m_FollowBehavior.SetParameters( m_FollowBehavior.GetFormation() ); } } } else { m_FollowBehavior.SetFollowTarget( UTIL_GetLocalPlayer() ); m_FollowBehavior.SetParameters( AIF_SIMPLE ); } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::ClearFollowTarget() { m_FollowBehavior.SetFollowTarget( NULL ); m_FollowBehavior.SetParameters( AIF_SIMPLE ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::UpdateFollowCommandPoint() { if ( !AI_IsSinglePlayer() ) return; if ( IsInPlayerSquad() ) { if ( HaveCommandGoal() ) { CBaseEntity *pFollowTarget = m_FollowBehavior.GetFollowTarget(); CBaseEntity *pCommandPoint = gEntList.FindEntityByClassname( NULL, COMMAND_POINT_CLASSNAME ); if( !pCommandPoint ) { DevMsg("**\nVERY BAD THING\nCommand point vanished! Creating a new one\n**\n"); pCommandPoint = CreateEntityByName( COMMAND_POINT_CLASSNAME ); } if ( pFollowTarget != pCommandPoint ) { pFollowTarget = pCommandPoint; m_FollowBehavior.SetFollowTarget( pFollowTarget ); m_FollowBehavior.SetParameters( AIF_COMMANDER ); } if ( ( pCommandPoint->GetAbsOrigin() - GetCommandGoal() ).LengthSqr() > 0.01 ) { UTIL_SetOrigin( pCommandPoint, GetCommandGoal(), false ); } } else { if ( IsFollowingCommandPoint() ) ClearFollowTarget(); if ( m_FollowBehavior.GetFollowTarget() != UTIL_GetLocalPlayer() ) { DevMsg( "Expected to be following player, but not\n" ); m_FollowBehavior.SetFollowTarget( UTIL_GetLocalPlayer() ); m_FollowBehavior.SetParameters( AIF_SIMPLE ); } } } else if ( IsFollowingCommandPoint() ) ClearFollowTarget(); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::IsFollowingCommandPoint() { CBaseEntity *pFollowTarget = m_FollowBehavior.GetFollowTarget(); if ( pFollowTarget ) return FClassnameIs( pFollowTarget, COMMAND_POINT_CLASSNAME ); return false; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- struct SquadMemberInfo_t { CNPC_Citizen * pMember; bool bSeesPlayer; float distSq; }; int __cdecl SquadSortFunc( const SquadMemberInfo_t *pLeft, const SquadMemberInfo_t *pRight ) { if ( pLeft->bSeesPlayer && !pRight->bSeesPlayer ) { return -1; } if ( !pLeft->bSeesPlayer && pRight->bSeesPlayer ) { return 1; } return ( pLeft->distSq - pRight->distSq ); } CAI_BaseNPC *CNPC_Citizen::GetSquadCommandRepresentative() { if ( !AI_IsSinglePlayer() ) return NULL; if ( IsInPlayerSquad() ) { static float lastTime; static AIHANDLE hCurrent; if ( gpGlobals->curtime - lastTime > 2.0 || !hCurrent || !hCurrent->IsInPlayerSquad() ) // hCurrent will be NULL after level change { lastTime = gpGlobals->curtime; hCurrent = NULL; CUtlVectorFixed candidates; CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); if ( pPlayer ) { AISquadIter_t iter; for ( CAI_BaseNPC *pAllyNpc = m_pSquad->GetFirstMember(&iter); pAllyNpc; pAllyNpc = m_pSquad->GetNextMember(&iter) ) { if ( pAllyNpc->IsCommandable() && dynamic_cast(pAllyNpc) ) { int i = candidates.AddToTail(); candidates[i].pMember = (CNPC_Citizen *)(pAllyNpc); candidates[i].bSeesPlayer = pAllyNpc->HasCondition( COND_SEE_PLAYER ); candidates[i].distSq = ( pAllyNpc->GetAbsOrigin() - pPlayer->GetAbsOrigin() ).LengthSqr(); } } if ( candidates.Count() > 0 ) { candidates.Sort( SquadSortFunc ); hCurrent = candidates[0].pMember; } } } if ( hCurrent != NULL ) { Assert( dynamic_cast(hCurrent.Get()) && hCurrent->IsInPlayerSquad() ); return hCurrent; } } return NULL; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::SetSquad( CAI_Squad *pSquad ) { bool bWasInPlayerSquad = IsInPlayerSquad(); BaseClass::SetSquad( pSquad ); if( IsInPlayerSquad() && !bWasInPlayerSquad ) { m_OnJoinedPlayerSquad.FireOutput(this, this); if ( npc_citizen_insignia.GetBool() ) AddInsignia(); } else if ( !IsInPlayerSquad() && bWasInPlayerSquad ) { if ( npc_citizen_insignia.GetBool() ) RemoveInsignia(); m_OnLeftPlayerSquad.FireOutput(this, this); } } //----------------------------------------------------------------------------- // Purpose: This is a generic function (to be implemented by sub-classes) to // handle specific interactions between different types of characters // (For example the barnacle grabbing an NPC) // Input : Constant for the type of interaction // Output : true - if sub-class has a response for the interaction // false - if sub-class has no response //----------------------------------------------------------------------------- bool CNPC_Citizen::HandleInteraction(int interactionType, void *data, CBaseCombatCharacter* sourceEnt) { // TODO: As citizen gets more complex, we will have to only allow // these interruptions to happen from certain schedules if (interactionType == g_interactionScannerInspect) { if ( gpGlobals->curtime > m_fNextInspectTime ) { //SetLookTarget(sourceEnt); // Don't let anyone else pick me for a couple seconds SetNextScannerInspectTime( gpGlobals->curtime + 5.0 ); return true; } return false; } else if (interactionType == g_interactionScannerInspectBegin) { // Don't inspect me again for a while SetNextScannerInspectTime( gpGlobals->curtime + CIT_INSPECTED_DELAY_TIME ); Vector targetDir = ( sourceEnt->WorldSpaceCenter() - WorldSpaceCenter() ); VectorNormalize( targetDir ); // If we're hit from behind, startle if ( DotProduct( targetDir, BodyDirection3D() ) < 0 ) { m_nInspectActivity = ACT_CIT_STARTLED; } else { // Otherwise we're blinded by the flash m_nInspectActivity = ACT_CIT_BLINDED; } SetCondition( COND_CIT_START_INSPECTION ); return true; } else if (interactionType == g_interactionScannerInspectHandsUp) { m_nInspectActivity = ACT_CIT_HANDSUP; SetSchedule(SCHED_CITIZEN_PLAY_INSPECT_ACTIVITY); return true; } else if (interactionType == g_interactionScannerInspectShowArmband) { m_nInspectActivity = ACT_CIT_SHOWARMBAND; SetSchedule(SCHED_CITIZEN_PLAY_INSPECT_ACTIVITY); return true; } else if (interactionType == g_interactionScannerInspectDone) { SetSchedule(SCHED_IDLE_WANDER); return true; } else if (interactionType == g_interactionHitByPlayerThrownPhysObj ) { if ( IsOkToSpeakInResponseToPlayer() ) { Speak( TLK_PLYR_PHYSATK ); } return true; } return BaseClass::HandleInteraction( interactionType, data, sourceEnt ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::FValidateHintType( CAI_Hint *pHint ) { switch( pHint->HintType() ) { case HINT_WORLD_VISUALLY_INTERESTING: return true; break; default: break; } return BaseClass::FValidateHintType( pHint ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::CanHeal() { if ( !IsMedic() && !IsAmmoResupplier() ) return false; if( !hl2_episodic.GetBool() ) { // If I'm not armed, my priority should be to arm myself. if ( IsMedic() && !GetActiveWeapon() ) return false; } if ( IsInAScript() || (m_NPCState == NPC_STATE_SCRIPT) ) return false; return true; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::ShouldHealTarget( CBaseEntity *pTarget, bool bActiveUse ) { Disposition_t disposition; if ( !pTarget && ( ( disposition = IRelationType( pTarget ) ) != D_LI && disposition != D_NU ) ) return false; // Don't heal if I'm in the middle of talking if ( IsSpeaking() ) return false; bool bTargetIsPlayer = pTarget->IsPlayer(); // Don't heal or give ammo to targets in vehicles CBaseCombatCharacter *pCCTarget = pTarget->MyCombatCharacterPointer(); if ( pCCTarget != NULL && pCCTarget->IsInAVehicle() ) return false; if ( IsMedic() ) { Vector toPlayer = ( pTarget->GetAbsOrigin() - GetAbsOrigin() ); if (( bActiveUse || !HaveCommandGoal() || toPlayer.Length() < HEAL_TARGET_RANGE) #ifdef HL2_EPISODIC && fabs(toPlayer.z) < HEAL_TARGET_RANGE_Z #endif ) { if ( pTarget->m_iHealth > 0 ) { if ( bActiveUse ) { // Ignore heal requests if we're going to heal a tiny amount float timeFullHeal = m_flPlayerHealTime; float timeRecharge = sk_citizen_heal_player_delay.GetFloat(); float maximumHealAmount = sk_citizen_heal_player.GetFloat(); float healAmt = ( maximumHealAmount * ( 1.0 - ( timeFullHeal - gpGlobals->curtime ) / timeRecharge ) ); if ( healAmt > pTarget->m_iMaxHealth - pTarget->m_iHealth ) healAmt = pTarget->m_iMaxHealth - pTarget->m_iHealth; if ( healAmt < sk_citizen_heal_player_min_forced.GetFloat() ) return false; return ( pTarget->m_iMaxHealth > pTarget->m_iHealth ); } // Are we ready to heal again? bool bReadyToHeal = ( ( bTargetIsPlayer && m_flPlayerHealTime <= gpGlobals->curtime ) || ( !bTargetIsPlayer && m_flAllyHealTime <= gpGlobals->curtime ) ); // Only heal if we're ready if ( bReadyToHeal ) { int requiredHealth; if ( bTargetIsPlayer ) requiredHealth = pTarget->GetMaxHealth() - sk_citizen_heal_player.GetFloat(); else requiredHealth = pTarget->GetMaxHealth() * sk_citizen_heal_player_min_pct.GetFloat(); if ( ( pTarget->m_iHealth <= requiredHealth ) && IRelationType( pTarget ) == D_LI ) return true; } } } } // Only players need ammo if ( IsAmmoResupplier() && bTargetIsPlayer ) { if ( m_flPlayerGiveAmmoTime <= gpGlobals->curtime ) { int iAmmoType = GetAmmoDef()->Index( STRING(m_iszAmmoSupply) ); if ( iAmmoType == -1 ) { DevMsg("ERROR: Citizen attempting to give unknown ammo type (%s)\n", STRING(m_iszAmmoSupply) ); } else { // Does the player need the ammo we can give him? int iMax = GetAmmoDef()->MaxCarry(iAmmoType); int iCount = ((CBasePlayer*)pTarget)->GetAmmoCount(iAmmoType); if ( !iCount || ((iMax - iCount) >= m_iAmmoAmount) ) { // Only give the player ammo if he has a weapon that uses it if ( ((CBasePlayer*)pTarget)->Weapon_GetWpnForAmmo( iAmmoType ) ) return true; } } } } return false; } #ifdef HL2_EPISODIC //----------------------------------------------------------------------------- // Determine if the citizen is in a position to be throwing medkits //----------------------------------------------------------------------------- bool CNPC_Citizen::ShouldHealTossTarget( CBaseEntity *pTarget, bool bActiveUse ) { Disposition_t disposition; Assert( IsMedic() ); if ( !IsMedic() ) return false; if ( !pTarget && ( ( disposition = IRelationType( pTarget ) ) != D_LI && disposition != D_NU ) ) return false; // Don't heal if I'm in the middle of talking if ( IsSpeaking() ) return false; bool bTargetIsPlayer = pTarget->IsPlayer(); // Don't heal or give ammo to targets in vehicles CBaseCombatCharacter *pCCTarget = pTarget->MyCombatCharacterPointer(); if ( pCCTarget != NULL && pCCTarget->IsInAVehicle() ) return false; Vector toPlayer = ( pTarget->GetAbsOrigin() - GetAbsOrigin() ); if ( bActiveUse || !HaveCommandGoal() || toPlayer.Length() < HEAL_TOSS_TARGET_RANGE ) { if ( pTarget->m_iHealth > 0 ) { if ( bActiveUse ) { // Ignore heal requests if we're going to heal a tiny amount float timeFullHeal = m_flPlayerHealTime; float timeRecharge = sk_citizen_heal_player_delay.GetFloat(); float maximumHealAmount = sk_citizen_heal_player.GetFloat(); float healAmt = ( maximumHealAmount * ( 1.0 - ( timeFullHeal - gpGlobals->curtime ) / timeRecharge ) ); if ( healAmt > pTarget->m_iMaxHealth - pTarget->m_iHealth ) healAmt = pTarget->m_iMaxHealth - pTarget->m_iHealth; if ( healAmt < sk_citizen_heal_player_min_forced.GetFloat() ) return false; return ( pTarget->m_iMaxHealth > pTarget->m_iHealth ); } // Are we ready to heal again? bool bReadyToHeal = ( ( bTargetIsPlayer && m_flPlayerHealTime <= gpGlobals->curtime ) || ( !bTargetIsPlayer && m_flAllyHealTime <= gpGlobals->curtime ) ); // Only heal if we're ready if ( bReadyToHeal ) { int requiredHealth; if ( bTargetIsPlayer ) requiredHealth = pTarget->GetMaxHealth() - sk_citizen_heal_player.GetFloat(); else requiredHealth = pTarget->GetMaxHealth() * sk_citizen_heal_player_min_pct.GetFloat(); if ( ( pTarget->m_iHealth <= requiredHealth ) && IRelationType( pTarget ) == D_LI ) return true; } } } return false; } #endif //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::Heal() { if ( !CanHeal() ) return; CBaseEntity *pTarget = GetTarget(); Vector target = pTarget->GetAbsOrigin() - GetAbsOrigin(); if ( target.Length() > HEAL_TARGET_RANGE * 2 ) return; // Don't heal a player that's staring at you until a few seconds have passed. m_flTimeNextHealStare = gpGlobals->curtime + sk_citizen_stare_heal_time.GetFloat(); if ( IsMedic() ) { float timeFullHeal; float timeRecharge; float maximumHealAmount; if ( pTarget->IsPlayer() ) { timeFullHeal = m_flPlayerHealTime; timeRecharge = sk_citizen_heal_player_delay.GetFloat(); maximumHealAmount = sk_citizen_heal_player.GetFloat(); m_flPlayerHealTime = gpGlobals->curtime + timeRecharge; } else { timeFullHeal = m_flAllyHealTime; timeRecharge = sk_citizen_heal_ally_delay.GetFloat(); maximumHealAmount = sk_citizen_heal_ally.GetFloat(); m_flAllyHealTime = gpGlobals->curtime + timeRecharge; } float healAmt = ( maximumHealAmount * ( 1.0 - ( timeFullHeal - gpGlobals->curtime ) / timeRecharge ) ); if ( healAmt > maximumHealAmount ) healAmt = maximumHealAmount; else healAmt = RoundFloatToInt( healAmt ); if ( healAmt > 0 ) { if ( pTarget->IsPlayer() && npc_citizen_medic_emit_sound.GetBool() ) { CPASAttenuationFilter filter( pTarget, "HealthKit.Touch" ); EmitSound( filter, pTarget->entindex(), "HealthKit.Touch" ); } pTarget->TakeHealth( healAmt, DMG_GENERIC ); pTarget->RemoveAllDecals(); } } if ( IsAmmoResupplier() ) { // Non-players don't use ammo if ( pTarget->IsPlayer() ) { int iAmmoType = GetAmmoDef()->Index( STRING(m_iszAmmoSupply) ); if ( iAmmoType == -1 ) { DevMsg("ERROR: Citizen attempting to give unknown ammo type (%s)\n", STRING(m_iszAmmoSupply) ); } else { ((CBasePlayer*)pTarget)->GiveAmmo( m_iAmmoAmount, iAmmoType, false ); } m_flPlayerGiveAmmoTime = gpGlobals->curtime + sk_citizen_giveammo_player_delay.GetFloat(); } } } #if HL2_EPISODIC //----------------------------------------------------------------------------- // Like Heal(), but tosses a healthkit in front of the player rather than just juicing him up. //----------------------------------------------------------------------------- void CNPC_Citizen::TossHealthKit(CBaseCombatCharacter *pThrowAt, const Vector &offset) { Assert( pThrowAt ); Vector forward, right, up; GetVectors( &forward, &right, &up ); Vector medKitOriginPoint = WorldSpaceCenter() + ( forward * 20.0f ); Vector destinationPoint; // this doesn't work without a moveparent: pThrowAt->ComputeAbsPosition( offset, &destinationPoint ); VectorTransform( offset, pThrowAt->EntityToWorldTransform(), destinationPoint ); // flatten out any z change due to player looking up/down destinationPoint.z = pThrowAt->EyePosition().z; Vector tossVelocity; if (npc_citizen_medic_throw_style.GetInt() == 0) { CTraceFilterSkipTwoEntities tracefilter( this, pThrowAt, COLLISION_GROUP_NONE ); tossVelocity = VecCheckToss( this, &tracefilter, medKitOriginPoint, destinationPoint, 0.233f, 1.0f, false ); } else { tossVelocity = VecCheckThrow( this, medKitOriginPoint, destinationPoint, MEDIC_THROW_SPEED, 1.0f ); if (vec3_origin == tossVelocity) { // if out of range, just throw it as close as I can tossVelocity = destinationPoint - medKitOriginPoint; // rotate upwards against gravity float len = VectorLength(tossVelocity); tossVelocity *= (MEDIC_THROW_SPEED / len); tossVelocity.z += 0.57735026918962576450914878050196 * MEDIC_THROW_SPEED; } } // create a healthkit and toss it into the world CBaseEntity *pHealthKit = CreateEntityByName( "item_healthkit" ); Assert(pHealthKit); if (pHealthKit) { pHealthKit->SetAbsOrigin( medKitOriginPoint ); pHealthKit->SetOwnerEntity( this ); // pHealthKit->SetAbsVelocity( tossVelocity ); DispatchSpawn( pHealthKit ); { IPhysicsObject *pPhysicsObject = pHealthKit->VPhysicsGetObject(); Assert( pPhysicsObject ); if ( pPhysicsObject ) { unsigned int cointoss = random->RandomInt(0,0xFF); // int bits used for bools // some random precession Vector angDummy(random->RandomFloat(-200,200), random->RandomFloat(-200,200), cointoss & 0x01 ? random->RandomFloat(200,600) : -1.0f * random->RandomFloat(200,600)); pPhysicsObject->SetVelocity( &tossVelocity, &angDummy ); } } } else { Warning("Citizen tried to heal but could not spawn item_healthkit!\n"); } } //----------------------------------------------------------------------------- // cause an immediate call to TossHealthKit with some default numbers //----------------------------------------------------------------------------- void CNPC_Citizen::InputForceHealthKitToss( inputdata_t &inputdata ) { TossHealthKit( UTIL_GetLocalPlayer(), Vector(48.0f, 0.0f, 0.0f) ); } #endif //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Citizen::ShouldLookForHealthItem() { // Definitely do not take health if not in the player's squad. if( !IsInPlayerSquad() ) return false; if( gpGlobals->curtime < m_flNextHealthSearchTime ) return false; // I'm fully healthy. if( GetHealth() >= GetMaxHealth() ) return false; // Player is hurt, don't steal his health. if( AI_IsSinglePlayer() && UTIL_GetLocalPlayer()->GetHealth() <= UTIL_GetLocalPlayer()->GetHealth() * 0.75f ) return false; // Wait till you're standing still. if( IsMoving() ) return false; return true; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::InputStartPatrolling( inputdata_t &inputdata ) { m_bShouldPatrol = true; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::InputStopPatrolling( inputdata_t &inputdata ) { m_bShouldPatrol = false; } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ void CNPC_Citizen::OnGivenWeapon( CBaseCombatWeapon *pNewWeapon ) { FixupMattWeapon(); } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ void CNPC_Citizen::InputSetCommandable( inputdata_t &inputdata ) { RemoveSpawnFlags( SF_CITIZEN_NOT_COMMANDABLE ); gm_PlayerSquadEvaluateTimer.Force(); } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CNPC_Citizen::InputSetMedicOn( inputdata_t &inputdata ) { AddSpawnFlags( SF_CITIZEN_MEDIC ); } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CNPC_Citizen::InputSetMedicOff( inputdata_t &inputdata ) { RemoveSpawnFlags( SF_CITIZEN_MEDIC ); } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CNPC_Citizen::InputSetAmmoResupplierOn( inputdata_t &inputdata ) { AddSpawnFlags( SF_CITIZEN_AMMORESUPPLIER ); } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CNPC_Citizen::InputSetAmmoResupplierOff( inputdata_t &inputdata ) { RemoveSpawnFlags( SF_CITIZEN_AMMORESUPPLIER ); } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ void CNPC_Citizen::InputSpeakIdleResponse( inputdata_t &inputdata ) { SpeakIfAllowed( TLK_ANSWER, NULL, true ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Citizen::DeathSound( const CTakeDamageInfo &info ) { // Sentences don't play on dead NPCs SentenceStop(); EmitSound( "NPC_Citizen.Die" ); } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ void CNPC_Citizen::FearSound( void ) { } //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CNPC_Citizen::UseSemaphore( void ) { // Ignore semaphore if we're told to work outside it if ( HasSpawnFlags(SF_CITIZEN_IGNORE_SEMAPHORE) ) return false; return BaseClass::UseSemaphore(); } //----------------------------------------------------------------------------- // // Schedules // //----------------------------------------------------------------------------- AI_BEGIN_CUSTOM_NPC( npc_citizen, CNPC_Citizen ) DECLARE_TASK( TASK_CIT_HEAL ) DECLARE_TASK( TASK_CIT_RPG_AUGER ) DECLARE_TASK( TASK_CIT_PLAY_INSPECT_SEQUENCE ) DECLARE_TASK( TASK_CIT_SIT_ON_TRAIN ) DECLARE_TASK( TASK_CIT_LEAVE_TRAIN ) DECLARE_TASK( TASK_CIT_SPEAK_MOURNING ) #if HL2_EPISODIC DECLARE_TASK( TASK_CIT_HEAL_TOSS ) #endif DECLARE_ACTIVITY( ACT_CIT_HANDSUP ) DECLARE_ACTIVITY( ACT_CIT_BLINDED ) DECLARE_ACTIVITY( ACT_CIT_SHOWARMBAND ) DECLARE_ACTIVITY( ACT_CIT_HEAL ) DECLARE_ACTIVITY( ACT_CIT_STARTLED ) DECLARE_CONDITION( COND_CIT_PLAYERHEALREQUEST ) DECLARE_CONDITION( COND_CIT_COMMANDHEAL ) DECLARE_CONDITION( COND_CIT_START_INSPECTION ) //Events DECLARE_ANIMEVENT( AE_CITIZEN_GET_PACKAGE ) DECLARE_ANIMEVENT( AE_CITIZEN_HEAL ) //========================================================= // > SCHED_SCI_HEAL //========================================================= DEFINE_SCHEDULE ( SCHED_CITIZEN_HEAL, " Tasks" " TASK_GET_PATH_TO_TARGET 0" " TASK_MOVE_TO_TARGET_RANGE 50" " TASK_STOP_MOVING 0" " TASK_FACE_IDEAL 0" // " TASK_SAY_HEAL 0" // " TASK_PLAY_SEQUENCE_FACE_TARGET ACTIVITY:ACT_ARM" " TASK_CIT_HEAL 0" // " TASK_PLAY_SEQUENCE_FACE_TARGET ACTIVITY:ACT_DISARM" " " " Interrupts" ) #if HL2_EPISODIC //========================================================= // > SCHED_CITIZEN_HEAL_TOSS // this is for the episodic behavior where the citizen hurls the medkit //========================================================= DEFINE_SCHEDULE ( SCHED_CITIZEN_HEAL_TOSS, " Tasks" // " TASK_GET_PATH_TO_TARGET 0" // " TASK_MOVE_TO_TARGET_RANGE 50" " TASK_STOP_MOVING 0" " TASK_FACE_IDEAL 0" // " TASK_SAY_HEAL 0" // " TASK_PLAY_SEQUENCE_FACE_TARGET ACTIVITY:ACT_ARM" " TASK_CIT_HEAL_TOSS 0" // " TASK_PLAY_SEQUENCE_FACE_TARGET ACTIVITY:ACT_DISARM" " " " Interrupts" ) #endif //========================================================= // > SCHED_CITIZEN_RANGE_ATTACK1_RPG //========================================================= DEFINE_SCHEDULE ( SCHED_CITIZEN_RANGE_ATTACK1_RPG, " Tasks" " TASK_STOP_MOVING 0" " TASK_FACE_ENEMY 0" " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack " TASK_RANGE_ATTACK1 0" " TASK_CIT_RPG_AUGER 1" "" " Interrupts" ) //========================================================= // > SCHED_CITIZEN_RANGE_ATTACK1_RPG //========================================================= DEFINE_SCHEDULE ( SCHED_CITIZEN_STRIDER_RANGE_ATTACK1_RPG, " Tasks" " TASK_STOP_MOVING 0" " TASK_FACE_ENEMY 0" " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack " TASK_WAIT 1" " TASK_RANGE_ATTACK1 0" " TASK_CIT_RPG_AUGER 1" " TASK_SET_SCHEDULE SCHEDULE:SCHED_TAKE_COVER_FROM_ENEMY" "" " Interrupts" ) //========================================================= // > SCHED_CITIZEN_PATROL //========================================================= DEFINE_SCHEDULE ( SCHED_CITIZEN_PATROL, " Tasks" " TASK_STOP_MOVING 0" " TASK_WANDER 901024" // 90 to 1024 units " TASK_WALK_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_STOP_MOVING 0" " TASK_WAIT 3" " TASK_WAIT_RANDOM 3" " TASK_SET_SCHEDULE SCHEDULE:SCHED_CITIZEN_PATROL" // keep doing it "" " Interrupts" " COND_ENEMY_DEAD" " COND_LIGHT_DAMAGE" " COND_HEAVY_DAMAGE" " COND_HEAR_DANGER" " COND_NEW_ENEMY" ) DEFINE_SCHEDULE ( SCHED_CITIZEN_MOURN_PLAYER, " Tasks" " TASK_GET_PATH_TO_PLAYER 0" " TASK_RUN_PATH_WITHIN_DIST 180" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_STOP_MOVING 0" " TASK_TARGET_PLAYER 0" " TASK_FACE_TARGET 0" " TASK_CIT_SPEAK_MOURNING 0" " TASK_SUGGEST_STATE STATE:IDLE" "" " Interrupts" " COND_LIGHT_DAMAGE" " COND_HEAVY_DAMAGE" " COND_HEAR_DANGER" " COND_NEW_ENEMY" ) DEFINE_SCHEDULE ( SCHED_CITIZEN_PLAY_INSPECT_ACTIVITY, " Tasks" " TASK_STOP_MOVING 0" " TASK_CIT_PLAY_INSPECT_SEQUENCE 0" // Play the sequence the scanner requires " TASK_WAIT 2" "" " Interrupts" " " ) DEFINE_SCHEDULE ( SCHED_CITIZEN_SIT_ON_TRAIN, " Tasks" " TASK_CIT_SIT_ON_TRAIN 0" " TASK_WAIT_RANDOM 1" " TASK_CIT_LEAVE_TRAIN 0" "" " Interrupts" ) AI_END_CUSTOM_NPC() //================================================================================================================== // CITIZEN PLAYER-RESPONSE SYSTEM // // NOTE: This system is obsolete, and left here for legacy support. // It has been superseded by the ai_eventresponse system. // //================================================================================================================== CHandle g_pCitizenResponseSystem = NULL; CCitizenResponseSystem *GetCitizenResponse() { return g_pCitizenResponseSystem; } char *CitizenResponseConcepts[MAX_CITIZEN_RESPONSES] = { "TLK_CITIZEN_RESPONSE_SHOT_GUNSHIP", "TLK_CITIZEN_RESPONSE_KILLED_GUNSHIP", "TLK_VITALNPC_DIED", }; LINK_ENTITY_TO_CLASS( ai_citizen_response_system, CCitizenResponseSystem ); BEGIN_DATADESC( CCitizenResponseSystem ) DEFINE_ARRAY( m_flResponseAddedTime, FIELD_FLOAT, MAX_CITIZEN_RESPONSES ), DEFINE_FIELD( m_flNextResponseTime, FIELD_FLOAT ), DEFINE_INPUTFUNC( FIELD_VOID, "ResponseVitalNPC", InputResponseVitalNPC ), DEFINE_THINKFUNC( ResponseThink ), END_DATADESC() //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CCitizenResponseSystem::Spawn() { if ( g_pCitizenResponseSystem ) { Warning("Multiple citizen response systems in level.\n"); UTIL_Remove( this ); return; } g_pCitizenResponseSystem = this; // Invisible, non solid. AddSolidFlags( FSOLID_NOT_SOLID ); AddEffects( EF_NODRAW ); SetThink( &CCitizenResponseSystem::ResponseThink ); m_flNextResponseTime = 0; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CCitizenResponseSystem::OnRestore() { BaseClass::OnRestore(); g_pCitizenResponseSystem = this; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CCitizenResponseSystem::AddResponseTrigger( citizenresponses_t iTrigger ) { m_flResponseAddedTime[ iTrigger ] = gpGlobals->curtime; SetNextThink( gpGlobals->curtime + 0.1 ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CCitizenResponseSystem::InputResponseVitalNPC( inputdata_t &inputdata ) { AddResponseTrigger( CR_VITALNPC_DIED ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CCitizenResponseSystem::ResponseThink() { bool bStayActive = false; if ( AI_IsSinglePlayer() ) { for ( int i = 0; i < MAX_CITIZEN_RESPONSES; i++ ) { if ( m_flResponseAddedTime[i] ) { // Should it have expired by now? if ( (m_flResponseAddedTime[i] + CITIZEN_RESPONSE_GIVEUP_TIME) < gpGlobals->curtime ) { m_flResponseAddedTime[i] = 0; } else if ( m_flNextResponseTime < gpGlobals->curtime ) { // Try and find the nearest citizen to the player float flNearestDist = (CITIZEN_RESPONSE_DISTANCE * CITIZEN_RESPONSE_DISTANCE); CBaseEntity *pNearestCitizen = NULL; CBaseEntity *pCitizen = NULL; CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); while ( (pCitizen = gEntList.FindEntityByClassname( pCitizen, "npc_citizen" ) ) != NULL) { float flDistToPlayer = (pPlayer->WorldSpaceCenter() - pCitizen->WorldSpaceCenter()).LengthSqr(); if ( flDistToPlayer < flNearestDist ) { flNearestDist = flDistToPlayer; pNearestCitizen = pCitizen; } } // Found one? if ( pNearestCitizen && ((CNPC_Citizen*)pNearestCitizen)->RespondedTo( CitizenResponseConcepts[i], false, false ) ) { m_flResponseAddedTime[i] = 0; m_flNextResponseTime = gpGlobals->curtime + CITIZEN_RESPONSE_REFIRE_TIME; // Don't issue multiple responses break; } } else { bStayActive = true; } } } } // Do we need to keep thinking? if ( bStayActive ) { SetNextThink( gpGlobals->curtime + 0.1 ); } } void CNPC_Citizen::AddInsignia() { CBaseEntity *pMark = CreateEntityByName( "squadinsignia" ); pMark->SetOwnerEntity( this ); pMark->Spawn(); } void CNPC_Citizen::RemoveInsignia() { // This is crap right now. CBaseEntity *FirstEnt(); CBaseEntity *pEntity = gEntList.FirstEnt(); while( pEntity ) { if( pEntity->GetOwnerEntity() == this ) { // Is this my insignia? CSquadInsignia *pInsignia = dynamic_cast(pEntity); if( pInsignia ) { UTIL_Remove( pInsignia ); return; } } pEntity = gEntList.NextEnt( pEntity ); } } //----------------------------------------------------------------------------- LINK_ENTITY_TO_CLASS( squadinsignia, CSquadInsignia ); void CSquadInsignia::Spawn() { CAI_BaseNPC *pOwner = ( GetOwnerEntity() ) ? GetOwnerEntity()->MyNPCPointer() : NULL; if ( pOwner ) { int attachment = pOwner->LookupAttachment( "eyes" ); if ( attachment ) { SetAbsAngles( GetOwnerEntity()->GetAbsAngles() ); SetParent( GetOwnerEntity(), attachment ); Vector vecPosition; vecPosition.Init( -2.5, 0, 3.9 ); SetLocalOrigin( vecPosition ); } } SetModel( INSIGNIA_MODEL ); SetSolid( SOLID_NONE ); } //----------------------------------------------------------------------------- // Purpose: Draw any debug text overlays // Input : // Output : Current text offset from the top //----------------------------------------------------------------------------- int CNPC_Citizen::DrawDebugTextOverlays( void ) { int text_offset = BaseClass::DrawDebugTextOverlays(); if (m_debugOverlays & OVERLAY_TEXT_BIT) { char tempstr[512]; Q_snprintf(tempstr,sizeof(tempstr),"Expression type: %s", szExpressionTypes[m_ExpressionType]); EntityText(text_offset,tempstr,0); text_offset++; } return text_offset; }