source-engine/game/server/mod/npc_base_custom.cpp
2025-02-02 01:43:34 +05:00

633 lines
20 KiB
C++

//=//=============================================================================//
//
// Purpose: A base class from which to extend new custom NPCs.
// This class may seem redundant with CAI_BaseNPC and a lot of Valve's NPC classes.
// However, the redundancy is necessary for compatibility with a variety of mods;
// I want all new NPC content to be isolated from existing classes.
//
// Author: 1upD
//
//=============================================================================//
#include "cbase.h"
#include "npc_base_custom.h"
#include "ai_hull.h"
#include "soundent.h"
#include "game.h"
#include "npcevent.h"
#include "engine/IEngineSound.h"
#include "basehlcombatweapon_shared.h"
#include "ai_squadslot.h"
#include "ai_squad.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//---------------------------------------------------------
// Constants
//---------------------------------------------------------
// TODO: Replace these with fields so that other NPCs can override them
const float MIN_TIME_NEXT_SOUND = 0.5f;
const float MAX_TIME_NEXT_SOUND = 1.0f;
const float MIN_TIME_NEXT_FOUNDENEMY_SOUND = 2.0f;
const float MAX_TIME_NEXT_FOUNDENEMY_SOUND = 5.0f;
//---------------------------------------------------------
// Save/Restore
//---------------------------------------------------------
BEGIN_DATADESC(CNPC_BaseCustomNPC)
DEFINE_KEYFIELD(m_iszWeaponModelName, FIELD_STRING, "WeaponModel"),
DEFINE_KEYFIELD(m_iHealth, FIELD_INTEGER, "Health"),
DEFINE_KEYFIELD(m_iszFearSound, FIELD_SOUNDNAME, "FearSound"),
DEFINE_KEYFIELD(m_iszDeathSound, FIELD_SOUNDNAME, "DeathSound"),
DEFINE_KEYFIELD(m_iszIdleSound, FIELD_SOUNDNAME, "IdleSound"),
DEFINE_KEYFIELD(m_iszPainSound, FIELD_SOUNDNAME, "PainSound"),
DEFINE_KEYFIELD(m_iszAlertSound, FIELD_SOUNDNAME, "AlertSound"),
DEFINE_KEYFIELD(m_iszLostEnemySound, FIELD_SOUNDNAME, "LostEnemySound"),
DEFINE_KEYFIELD(m_iszFoundEnemySound, FIELD_SOUNDNAME, "FoundEnemySound"),
DEFINE_KEYFIELD(m_bUseBothSquadSlots, FIELD_BOOLEAN, "UseBothSquadSlots"),
DEFINE_KEYFIELD(m_bCannotOpenDoors, FIELD_BOOLEAN, "CannotOpenDoors"),
DEFINE_KEYFIELD(m_bCanPickupWeapons, FIELD_BOOLEAN, "CanPickupWeapons"),
DEFINE_FIELD(m_iNumSquadmates, FIELD_INTEGER),
DEFINE_FIELD(m_bWanderToggle, FIELD_BOOLEAN),
DEFINE_FIELD(m_flNextSoundTime, FIELD_TIME),
DEFINE_FIELD(m_flNextFoundEnemySoundTime, FIELD_TIME),
DEFINE_FIELD(m_flSpeedModifier, FIELD_TIME),
DEFINE_INPUTFUNC(FIELD_FLOAT, "SetSpeedModifier", InputSetSpeedModifier),
DEFINE_INPUTFUNC(FIELD_VOID, "EnableOpenDoors", InputEnableOpenDoors),
DEFINE_INPUTFUNC(FIELD_VOID, "DisableOpenDoors", InputDisableOpenDoors),
DEFINE_INPUTFUNC(FIELD_VOID, "EnablePickupWeapons", InputEnablePickupWeapons),
DEFINE_INPUTFUNC(FIELD_VOID, "DisablePickupWeapons", InputDisablePickupWeapons)
END_DATADESC()
AI_BEGIN_CUSTOM_NPC(npc_base_custom, CNPC_BaseCustomNPC)
//=========================================================
// > Melee_Attack_NoInterrupt
//=========================================================
DEFINE_SCHEDULE
(
SCHED_MELEE_ATTACK_NOINTERRUPT,
" Tasks"
" TASK_STOP_MOVING 0"
" TASK_FACE_ENEMY 0"
" TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack
" TASK_MELEE_ATTACK1 0"
""
" Interrupts"
" COND_ENEMY_DEAD"
" COND_ENEMY_OCCLUDED"
);
//=========================================================
// SCHED_HIDE
//=========================================================
DEFINE_SCHEDULE
(
SCHED_HIDE,
" Tasks"
" TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_COMBAT_FACE"
" TASK_STOP_MOVING 0"
" TASK_FIND_COVER_FROM_ENEMY 0"
" TASK_RUN_PATH 0"
" TASK_WAIT_FOR_MOVEMENT 0"
" TASK_REMEMBER MEMORY:INCOVER"
" TASK_FACE_ENEMY 0"
""
" Interrupts"
" COND_HEAR_DANGER"
" COND_NEW_ENEMY"
" COND_ENEMY_DEAD"
);
AI_END_CUSTOM_NPC()
//-----------------------------------------------------------------------------
// Purpose:
//
//
//-----------------------------------------------------------------------------
void CNPC_BaseCustomNPC::Precache( void )
{
// If no model name is supplied, use the default citizen model
if (!GetModelName())
{
SetModelName(MAKE_STRING("models/monster/subject.mdl")); // TODO replace this with citizen
}
if (&m_iszWeaponModelName && m_iszWeaponModelName != MAKE_STRING("")) {
PrecacheModel(STRING(m_iszWeaponModelName));
}
else {
PrecacheModel("models/props_canal/mattpipe.mdl"); // Default weapon model
}
PrecacheModel(STRING(GetModelName()));
PrecacheNPCSoundScript(&m_iszFearSound, MAKE_STRING("NPC_BaseCustomr.Fear"));
PrecacheNPCSoundScript(&m_iszIdleSound, MAKE_STRING("NPC_BaseCustom.Idle"));
PrecacheNPCSoundScript(&m_iszAlertSound, MAKE_STRING("NPC_BaseCustom.Alert"));
PrecacheNPCSoundScript(&m_iszPainSound, MAKE_STRING("NPC_BaseCustom.Pain"));
PrecacheNPCSoundScript(&m_iszLostEnemySound, MAKE_STRING("NPC_BaseCustom.LostEnemy"));
PrecacheNPCSoundScript(&m_iszFoundEnemySound, MAKE_STRING("NPC_BaseCustom.FoundEnemy"));
PrecacheNPCSoundScript(&m_iszDeathSound, MAKE_STRING("NPC_BaseCustom.Death"));
m_bWanderToggle = false;
BaseClass::Precache();
}
//-----------------------------------------------------------------------------
// Purpose:
//
//
//-----------------------------------------------------------------------------
void CNPC_BaseCustomNPC::Spawn( void )
{
Precache();
SetModel(STRING(GetModelName()));
SetHullType(HULL_HUMAN);
SetHullSizeNormal();
SetSolid( SOLID_BBOX );
AddSolidFlags( FSOLID_NOT_STANDABLE );
SetMoveType( MOVETYPE_STEP );
SetBloodColor( BLOOD_COLOR_RED );
// If the health has not been set through Hammer, use a default health value of 75
if (m_iHealth < 1)
{
m_iHealth = 75;
}
m_flFieldOfView = 0.5;
m_flNextSoundTime = gpGlobals->curtime;
m_flNextFoundEnemySoundTime = gpGlobals->curtime;
m_NPCState = NPC_STATE_NONE;
m_flSpeedModifier = 1.0f;
CapabilitiesClear();
if (!HasSpawnFlags(SF_NPC_START_EFFICIENT))
{
CapabilitiesAdd(bits_CAP_ANIMATEDFACE | bits_CAP_TURN_HEAD); // The default model has no face animations, but a custom model might
CapabilitiesAdd(bits_CAP_SQUAD);
CapabilitiesAdd(bits_CAP_USE_WEAPONS | bits_CAP_AIM_GUN | bits_CAP_MOVE_SHOOT);
CapabilitiesAdd(bits_CAP_WEAPON_MELEE_ATTACK1 || bits_CAP_WEAPON_MELEE_ATTACK2);
CapabilitiesAdd(bits_CAP_DUCK);
CapabilitiesAdd(bits_CAP_USE_SHOT_REGULATOR);
if (!m_bCannotOpenDoors) {
CapabilitiesAdd(bits_CAP_DOORS_GROUP);
}
}
CapabilitiesAdd(bits_CAP_MOVE_GROUND);
SetMoveType(MOVETYPE_STEP);
NPCInit();
}
void CNPC_BaseCustomNPC::Activate()
{
BaseClass::Activate();
FixupWeapon();
}
//-----------------------------------------------------------------------------
// Purpose: If this NPC has some kind of custom weapon behavior,
// set up the weapon after spawn.
//-----------------------------------------------------------------------------
void CNPC_BaseCustomNPC::FixupWeapon()
{
// Do nothing
}
//-----------------------------------------------------------------------------
// Purpose: Choose a schedule after schedule failed
//-----------------------------------------------------------------------------
int CNPC_BaseCustomNPC::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;
}
return BaseClass::SelectFailSchedule(failedSchedule, failedTask, taskFailCode);
}
//-----------------------------------------------------------------------------
// Purpose: Select a schedule to retrieve better weapons if they are available.
//-----------------------------------------------------------------------------
int CNPC_BaseCustomNPC::SelectScheduleRetrieveItem()
{
if (m_bCanPickupWeapons && 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;
}
}
return SCHED_NONE;
}
//-----------------------------------------------------------------------------
// Purpose: Select ideal state.
// Conditions for custom states are defined here.
//-----------------------------------------------------------------------------
NPC_STATE CNPC_BaseCustomNPC::SelectIdealState(void)
{
switch ((int)this->m_NPCState) {
case NPC_STATE_AMBUSH:
return SelectAmbushIdealState();
case NPC_STATE_SURRENDER:
return SelectSurrenderIdealState();
default:
return BaseClass::SelectIdealState();
}
}
NPC_STATE CNPC_BaseCustomNPC::SelectAmbushIdealState()
{
// AMBUSH goes to ALERT upon death of enemy
if (GetEnemy() == NULL)
{
return NPC_STATE_ALERT;
}
// If I am not in a squad, there is no reason to ambush
if (!m_pSquad) {
return NPC_STATE_COMBAT;
}
// If I am the last in a squad, attack!
if (m_pSquad->NumMembers() == 1) {
return NPC_STATE_COMBAT;
}
if (OccupyStrategySlotRange(SQUAD_SLOT_CHASE_1, SQUAD_SLOT_CHASE_2)) {
return NPC_STATE_COMBAT;
}
// The best ideal state is the current ideal state.
return (NPC_STATE)NPC_STATE_AMBUSH;
}
NPC_STATE CNPC_BaseCustomNPC::SelectSurrenderIdealState()
{
return NPC_STATE_ALERT;
}
//-----------------------------------------------------------------------------
// Purpose: Select a schedule to retrieve better weapons if they are available.
//-----------------------------------------------------------------------------
int CNPC_BaseCustomNPC::SelectScheduleWander()
{
m_bWanderToggle = !m_bWanderToggle;
if (m_bWanderToggle) {
return SCHED_IDLE_WANDER;
}
else {
return SCHED_NONE;
}
}
//-----------------------------------------------------------------------------
// Purpose: Select a schedule to execute based on conditions.
// This is the most critical AI method.
//-----------------------------------------------------------------------------
int CNPC_BaseCustomNPC::SelectSchedule()
{
switch ((int)m_NPCState)
{
case NPC_STATE_IDLE:
AssertMsgOnce(GetEnemy() == NULL, "NPC has enemy but is not in combat state?");
return SelectIdleSchedule();
case NPC_STATE_ALERT:
AssertMsgOnce(GetEnemy() == NULL, "NPC has enemy but is not in combat state?");
return SelectAlertSchedule();
case NPC_STATE_COMBAT:
return SelectCombatSchedule();
case NPC_STATE_AMBUSH:
return SelectAmbushSchedule();
case NPC_STATE_SURRENDER:
return SelectSurrenderSchedule();
default:
return BaseClass::SelectSchedule();
}
}
//-----------------------------------------------------------------------------
// Idle schedule selection
//-----------------------------------------------------------------------------
int CNPC_BaseCustomNPC::SelectIdleSchedule()
{
int nSched = SelectFlinchSchedule();
if (nSched != SCHED_NONE)
return nSched;
if (HasCondition(COND_HEAR_DANGER) ||
HasCondition(COND_HEAR_COMBAT) ||
HasCondition(COND_HEAR_WORLD) ||
HasCondition(COND_HEAR_BULLET_IMPACT) ||
HasCondition(COND_HEAR_PLAYER))
{
// Investigate sound source
return SCHED_ALERT_FACE_BESTSOUND;
}
nSched = SelectScheduleRetrieveItem();
if (nSched != SCHED_NONE)
return nSched;
// no valid route! Wander instead
if (GetNavigator()->GetGoalType() == GOALTYPE_NONE) {
return SCHED_IDLE_STAND;
}
// valid route. Get moving
return SCHED_IDLE_WALK;
}
//-----------------------------------------------------------------------------
// Alert schedule selection
//-----------------------------------------------------------------------------
int CNPC_BaseCustomNPC::SelectAlertSchedule()
{
// Per default base NPC, check flinch schedule first
int nSched = SelectFlinchSchedule();
if (nSched != SCHED_NONE)
return nSched;
// Scan around for new enemies
if (HasCondition(COND_ENEMY_DEAD) && SelectWeightedSequence(ACT_VICTORY_DANCE) != ACTIVITY_NOT_AVAILABLE)
return SCHED_ALERT_SCAN;
if (HasCondition(COND_HEAR_DANGER) ||
HasCondition(COND_HEAR_PLAYER) ||
HasCondition(COND_HEAR_WORLD) ||
HasCondition(COND_HEAR_BULLET_IMPACT) ||
HasCondition(COND_HEAR_COMBAT))
{
// Investigate sound source
AlertSound();
return SCHED_ALERT_FACE_BESTSOUND;
}
nSched = SelectScheduleRetrieveItem();
if (nSched != SCHED_NONE)
return nSched;
// no valid route! Wander instead
if (GetNavigator()->GetGoalType() == GOALTYPE_NONE) {
return SCHED_ALERT_STAND;
}
// valid route. Get moving
return SCHED_ALERT_WALK;
}
//-----------------------------------------------------------------------------
// Combat schedule selection
//-----------------------------------------------------------------------------
int CNPC_BaseCustomNPC::SelectCombatSchedule()
{
return BaseClass::SelectSchedule(); // Let Base NPC handle it
}
//-----------------------------------------------------------------------------
// Combat schedule selection
//-----------------------------------------------------------------------------
int CNPC_BaseCustomNPC::SelectAmbushSchedule()
{
// Check enemy death
if (HasCondition(COND_ENEMY_DEAD))
{
// clear the current (dead) enemy and try to find another.
SetEnemy(NULL);
if (ChooseEnemy())
{
SetState(NPC_STATE_COMBAT);
FoundEnemySound();
ClearCondition(COND_ENEMY_DEAD);
return SelectSchedule();
}
SetState(NPC_STATE_ALERT);
return SelectSchedule();
}
CBaseEntity* pEnemy = GetEnemy();
if (pEnemy && EnemyDistance(pEnemy) < 128)
{
SetState(NPC_STATE_COMBAT);
return SelectSchedule();
}
if (pEnemy == NULL || HasCondition(COND_LOST_ENEMY)) {
SetState(NPC_STATE_ALERT);
return SelectSchedule();
}
// If I am the last in a squad, attack!
if (m_iNumSquadmates > m_pSquad->NumMembers())
SetState(SelectAmbushIdealState());
if (HasCondition(COND_LIGHT_DAMAGE)) {
SetState(NPC_STATE_COMBAT);
}
if (HasCondition(COND_SEE_ENEMY) && HasCondition(COND_ENEMY_FACING_ME) && HasCondition(COND_HAVE_ENEMY_LOS)) {
if(GetState() != NPC_STATE_COMBAT)
SetState(SelectAmbushIdealState());
return SCHED_HIDE;
}
m_iNumSquadmates = m_pSquad->NumMembers();
return SCHED_COMBAT_FACE;
}
int CNPC_BaseCustomNPC::SelectSurrenderSchedule()
{
return BaseClass::SelectSchedule();
}
bool CNPC_BaseCustomNPC::HasRangedWeapon()
{
CBaseCombatWeapon *pWeapon = GetActiveWeapon();
if (pWeapon)
return !(FClassnameIs(pWeapon, "weapon_crowbar") || FClassnameIs(pWeapon, "weapon_stunstick") || FClassnameIs(pWeapon, "weapon_custommelee"));
return false;
}
//-----------------------------------------------------------------------------
// Purpose: Override base class activiites
//-----------------------------------------------------------------------------
Activity CNPC_BaseCustomNPC::NPC_TranslateActivity(Activity activity)
{
switch (activity) {
case ACT_RUN_AIM_SHOTGUN:
return ACT_RUN_AIM_RIFLE;
case ACT_WALK_AIM_SHOTGUN:
return ACT_WALK_AIM_RIFLE;
case ACT_IDLE_ANGRY_SHOTGUN:
return ACT_IDLE_ANGRY_SMG1;
case ACT_RANGE_ATTACK_SHOTGUN_LOW:
return ACT_RANGE_ATTACK_SMG1_LOW;
case ACT_IDLE_MELEE:
case ACT_IDLE_ANGRY_MELEE: // If the NPC has a melee weapon but is in an idle state, don't raise the weapon
if (m_NPCState == NPC_STATE_IDLE)
return ACT_IDLE_SUITCASE;
default:
return BaseClass::NPC_TranslateActivity(activity);
}
}
//-----------------------------------------------------------------------------
// Purpose: Override base class schedules
//-----------------------------------------------------------------------------
int CNPC_BaseCustomNPC::TranslateSchedule(int scheduleType)
{
return BaseClass::TranslateSchedule(scheduleType);
}
//-----------------------------------------------------------------------------
// Purpose: Play sound when an enemy is spotted. This sound has a separate
// timer from other sounds to prevent looping if the NPC gets caught
// in a 'found enemy' condition.
//-----------------------------------------------------------------------------
void CNPC_BaseCustomNPC::FoundEnemySound(void)
{
if (gpGlobals->curtime > m_flNextFoundEnemySoundTime)
{
m_flNextFoundEnemySoundTime = gpGlobals->curtime + random->RandomFloat(MIN_TIME_NEXT_FOUNDENEMY_SOUND, MAX_TIME_NEXT_FOUNDENEMY_SOUND);
PlaySound(m_iszFoundEnemySound, true);
}
}
//-----------------------------------------------------------------------------
// Purpose: Play NPC soundscript
//-----------------------------------------------------------------------------
void CNPC_BaseCustomNPC::PlaySound(string_t soundname, bool required /*= false */)
{
// TODO: Check if silent
if (required || gpGlobals->curtime > m_flNextSoundTime)
{
m_flNextSoundTime = gpGlobals->curtime + random->RandomFloat(MIN_TIME_NEXT_SOUND, MAX_TIME_NEXT_SOUND);
//CPASAttenuationFilter filter2(this, STRING(soundname));
EmitSound(STRING(soundname));
}
}
//-----------------------------------------------------------------------------
// Purpose: Assign a default soundscript if none is provided, then precache
//-----------------------------------------------------------------------------
void CNPC_BaseCustomNPC::PrecacheNPCSoundScript(string_t * SoundName, string_t defaultSoundName)
{
if (!SoundName) {
*SoundName = defaultSoundName;
}
PrecacheScriptSound(STRING(*SoundName));
}
//-----------------------------------------------------------------------------
// Purpose: Get movement speed, multipled by modifier
//-----------------------------------------------------------------------------
float CNPC_BaseCustomNPC::GetSequenceGroundSpeed(CStudioHdr *pStudioHdr, int iSequence)
{
float t = SequenceDuration(pStudioHdr, iSequence);
if (t > 0)
{
return (GetSequenceMoveDist(pStudioHdr, iSequence) * m_flSpeedModifier / t);
}
else
{
return 0;
}
}
//-----------------------------------------------------------------------------
// Purpose: Hammer input to change the speed of the NPC
//-----------------------------------------------------------------------------
void CNPC_BaseCustomNPC::InputSetSpeedModifier(inputdata_t &inputdata)
{
this->m_flSpeedModifier = inputdata.value.Float();
}
//-----------------------------------------------------------------------------
// Purpose: Hammer input to enable opening doors
//-----------------------------------------------------------------------------
void CNPC_BaseCustomNPC::InputEnableOpenDoors(inputdata_t &inputdata)
{
m_bCannotOpenDoors = false;
if (!HasSpawnFlags(SF_NPC_START_EFFICIENT))
{
CapabilitiesAdd(bits_CAP_DOORS_GROUP);
}
}
//-----------------------------------------------------------------------------
// Purpose: Hammer input to enable opening doors
//-----------------------------------------------------------------------------
void CNPC_BaseCustomNPC::InputDisableOpenDoors(inputdata_t &inputdata)
{
m_bCannotOpenDoors = true;
if (!HasSpawnFlags(SF_NPC_START_EFFICIENT))
{
CapabilitiesRemove(bits_CAP_DOORS_GROUP);
}
}
//-----------------------------------------------------------------------------
// Purpose: Hammer input to enable weapon pickup behavior
//-----------------------------------------------------------------------------
void CNPC_BaseCustomNPC::InputEnablePickupWeapons(inputdata_t &inputdata)
{
m_bCanPickupWeapons = true;
}
//-----------------------------------------------------------------------------
// Purpose: Hammer input to enable weapon pickup behavior
//-----------------------------------------------------------------------------
void CNPC_BaseCustomNPC::InputDisablePickupWeapons(inputdata_t &inputdata)
{
m_bCanPickupWeapons = false;
}
//-----------------------------------------------------------------------------
// Purpose:
//
//
// Output :
//-----------------------------------------------------------------------------
Class_T CNPC_BaseCustomNPC::Classify( void )
{
return CLASS_NONE;
}