You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
4263 lines
124 KiB
4263 lines
124 KiB
//========= 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<CWeaponRPG*>(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<CNPC_Citizen *>(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<HeadCandidate_t, ARRAYSIZE(g_ppszRandomHeads)> 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<CNPC_Citizen *>(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<CWeaponRPG*>(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<CBaseHLCombatWeapon *>(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<CItem *>(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<CAI_FollowBehavior *>(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<CWeaponRPG*>(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<CBaseCombatCharacter *>( 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<CWeaponRPG*>(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<CNPC_Citizen *, MAX_PLAYER_SQUAD> 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<CNPC_Citizen *>(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<SquadCandidate_t> 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<CNPC_Citizen *>(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<CNPC_Citizen *>(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<SquadMemberInfo_t, MAX_SQUAD_MEMBERS> 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<CNPC_Citizen *>(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<CNPC_Citizen *>(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<CCitizenResponseSystem> 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() |
|
{ |
|
CBaseEntity *pEntity = gEntList.FirstEnt(); |
|
|
|
while( pEntity ) |
|
{ |
|
if( pEntity->GetOwnerEntity() == this ) |
|
{ |
|
// Is this my insignia? |
|
CSquadInsignia *pInsignia = dynamic_cast<CSquadInsignia *>(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; |
|
}
|
|
|