mirror of
https://github.com/nillerusr/source-engine.git
synced 2026-07-17 14:55:21 +00:00
431 lines
13 KiB
C++
431 lines
13 KiB
C++
//=//=============================================================================//
|
|
//
|
|
// Purpose: A malevolent being from a parallel universe which at one point
|
|
// may have been human.
|
|
//
|
|
// npc_shadow_walker is designed to be reusable as a generic horror
|
|
// game style npc. Its model and sound files may be configured through
|
|
// the hammer editor using keyfields.
|
|
//
|
|
// Author: 1upD
|
|
//
|
|
//=============================================================================//
|
|
#include "cbase.h"
|
|
#include "npc_shadow_walker.h"
|
|
#include "ai_hull.h"
|
|
#include "game.h"
|
|
#include "npcevent.h"
|
|
#include "basehlcombatweapon_shared.h"
|
|
#include "ai_squadslot.h"
|
|
#include "weapon_custom_melee.h"
|
|
|
|
// memdbgon must be the last include file in a .cpp file!!!
|
|
#include "tier0/memdbgon.h"
|
|
|
|
//---------------------------------------------------------
|
|
// Save/Restore
|
|
//---------------------------------------------------------
|
|
BEGIN_DATADESC(CNPC_ShadowWalker)
|
|
/// Custom fields go here
|
|
END_DATADESC()
|
|
|
|
//---------------------------------------------------------
|
|
// Custom AI
|
|
//---------------------------------------------------------
|
|
AI_BEGIN_CUSTOM_NPC(npc_shadow_walker, CNPC_ShadowWalker)
|
|
// Custom schedules go here
|
|
AI_END_CUSTOM_NPC()
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//
|
|
//
|
|
//-----------------------------------------------------------------------------
|
|
void CNPC_ShadowWalker::Precache( void )
|
|
{
|
|
// If no model name is supplied, use the default Shadow Walker model
|
|
if (!GetModelName())
|
|
{
|
|
SetModelName(MAKE_STRING("models/monster/subject.mdl"));
|
|
}
|
|
|
|
if (&m_iszWeaponModelName && m_iszWeaponModelName != MAKE_STRING("")) {
|
|
PrecacheModel(STRING(m_iszWeaponModelName));
|
|
}
|
|
else {
|
|
PrecacheModel("models/props_canal/mattpipe.mdl");
|
|
}
|
|
|
|
PrecacheModel(STRING(GetModelName()));
|
|
PrecacheNPCSoundScript(&m_iszFearSound, MAKE_STRING("NPC_ShadowWalker.Fear"));
|
|
PrecacheNPCSoundScript(&m_iszIdleSound, MAKE_STRING("NPC_ShadowWalker.Idle"));
|
|
PrecacheNPCSoundScript(&m_iszAlertSound, MAKE_STRING("NPC_ShadowWalker.Alert"));
|
|
PrecacheNPCSoundScript(&m_iszPainSound, MAKE_STRING("NPC_ShadowWalker.Pain"));
|
|
PrecacheNPCSoundScript(&m_iszLostEnemySound, MAKE_STRING("NPC_ShadowWalker.LostEnemy"));
|
|
PrecacheNPCSoundScript(&m_iszFoundEnemySound, MAKE_STRING("NPC_ShadowWalker.FoundEnemy"));
|
|
PrecacheNPCSoundScript(&m_iszDeathSound, MAKE_STRING("NPC_ShadowWalker.Death"));
|
|
|
|
m_bWanderToggle = false;
|
|
|
|
BaseClass::Precache();
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//
|
|
//
|
|
//-----------------------------------------------------------------------------
|
|
void CNPC_ShadowWalker::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_ShadowWalker::FixupWeapon()
|
|
{
|
|
// If no weapons supplied, give a crowbar
|
|
CBaseCombatWeapon *pWeapon = GetActiveWeapon();
|
|
if (pWeapon == NULL) {
|
|
CWeaponCustomMelee *pMeleeWeapon = (CWeaponCustomMelee*)CreateEntityByName("weapon_custommelee");
|
|
|
|
// Apply weapon model override
|
|
if (&m_iszWeaponModelName && m_iszWeaponModelName != MAKE_STRING("")) {
|
|
pMeleeWeapon->m_iszWeaponModelName = this->m_iszWeaponModelName;
|
|
}
|
|
// Default custom weapon model
|
|
else {
|
|
pMeleeWeapon->m_iszWeaponModelName = MAKE_STRING("models/props_canal/mattpipe.mdl");
|
|
}
|
|
|
|
pWeapon = (CBaseCombatWeapon *)pMeleeWeapon;
|
|
|
|
DispatchSpawn(pWeapon);
|
|
Weapon_Equip(pWeapon);
|
|
}
|
|
|
|
}
|
|
|
|
void CNPC_ShadowWalker::Activate()
|
|
{
|
|
BaseClass::Activate();
|
|
FixupWeapon();
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Choose a schedule after schedule failed
|
|
//-----------------------------------------------------------------------------
|
|
int CNPC_ShadowWalker::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_TAKE_COVER_FROM_ENEMY:
|
|
// I can't take cover, so I need to run away!
|
|
return SCHED_RUN_FROM_ENEMY;
|
|
case SCHED_CHASE_ENEMY:
|
|
// I can't run towards the enemy, so I will just run randomly!
|
|
return SCHED_CHASE_ENEMY_FAILED;
|
|
case SCHED_RUN_FROM_ENEMY:
|
|
// I can't run away, so I will just run randomly!
|
|
return SCHED_RUN_RANDOM;
|
|
case SCHED_INVESTIGATE_SOUND:
|
|
// I can't investigate a sound I heard.
|
|
return SCHED_IDLE_WANDER;
|
|
}
|
|
|
|
return BaseClass::SelectFailSchedule(failedSchedule, failedTask, taskFailCode);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Idle schedule selection
|
|
//-----------------------------------------------------------------------------
|
|
int CNPC_ShadowWalker::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_INVESTIGATE_SOUND;
|
|
}
|
|
|
|
nSched = SelectScheduleRetrieveItem();
|
|
if (nSched != SCHED_NONE)
|
|
return nSched;
|
|
|
|
// no valid route! Wander instead
|
|
if (GetNavigator()->GetGoalType() == GOALTYPE_NONE) {
|
|
nSched = SelectScheduleWander();
|
|
if (nSched != SCHED_NONE)
|
|
return nSched;
|
|
return SCHED_IDLE_STAND;
|
|
}
|
|
|
|
// valid route. Get moving
|
|
return SCHED_IDLE_WALK;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Alert schedule selection
|
|
//-----------------------------------------------------------------------------
|
|
int CNPC_ShadowWalker::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_INVESTIGATE_SOUND;
|
|
}
|
|
|
|
nSched = SelectScheduleRetrieveItem();
|
|
if (nSched != SCHED_NONE)
|
|
return nSched;
|
|
|
|
// no valid route! Wander instead
|
|
if (GetNavigator()->GetGoalType() == GOALTYPE_NONE) {
|
|
nSched = SelectScheduleWander();
|
|
if (nSched != SCHED_NONE)
|
|
return nSched;
|
|
return SCHED_IDLE_STAND;
|
|
}
|
|
|
|
// valid route. Get moving
|
|
return SCHED_ALERT_WALK;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Combat schedule selection
|
|
//-----------------------------------------------------------------------------
|
|
int CNPC_ShadowWalker::SelectCombatSchedule()
|
|
{
|
|
// Check flinch first
|
|
int nSched = SelectFlinchSchedule();
|
|
if (nSched != SCHED_NONE)
|
|
return nSched;
|
|
|
|
// Check enemy death
|
|
if (HasCondition(COND_ENEMY_DEAD))
|
|
{
|
|
// clear the current (dead) enemy and try to find another.
|
|
SetEnemy(NULL);
|
|
|
|
if (ChooseEnemy())
|
|
{
|
|
FoundEnemySound();
|
|
ClearCondition(COND_ENEMY_DEAD);
|
|
return SelectSchedule();
|
|
}
|
|
|
|
SetState(NPC_STATE_ALERT);
|
|
return SelectSchedule();
|
|
}
|
|
|
|
// Can any enemies see me?
|
|
bool bEnemyCanSeeMe = HasCondition(COND_SEE_ENEMY) && HasCondition(COND_ENEMY_FACING_ME) && HasCondition(COND_HAVE_ENEMY_LOS);
|
|
|
|
|
|
// If I'm scared of this enemy and he's looking at me, run away
|
|
if ((IRelationType(GetEnemy()) == D_FR) && bEnemyCanSeeMe)
|
|
{
|
|
FearSound();
|
|
return SCHED_RUN_FROM_ENEMY;
|
|
}
|
|
|
|
// If in a squad, only one or two shadow walkers can chase the player. This is configurable through Hammer.
|
|
bool bCanChase = true;
|
|
if (m_bUseBothSquadSlots) {
|
|
bCanChase = OccupyStrategySlotRange(SQUAD_SLOT_CHASE_1, SQUAD_SLOT_CHASE_2);
|
|
}
|
|
else {
|
|
bCanChase = OccupyStrategySlot(SQUAD_SLOT_CHASE_1);
|
|
}
|
|
|
|
bCanChase = bCanChase || EnemyDistance(GetEnemy()) < 128 || (bEnemyCanSeeMe && (HasCondition(COND_LIGHT_DAMAGE) || HasCondition(COND_HEAVY_DAMAGE)));
|
|
|
|
// If I'm not allowed to chase this enemy of this enemy and he's looking at me, set up an ambush
|
|
if (!bCanChase)
|
|
{
|
|
FearSound();
|
|
SetState((NPC_STATE)NPC_STATE_AMBUSH);
|
|
return SCHED_HIDE;
|
|
|
|
}
|
|
|
|
// Reloading conditions are necessary just in case for some reason somebody gives the Shadow Walker a gun
|
|
if (HasRangedWeapon() && (HasCondition(COND_LOW_PRIMARY_AMMO) || HasCondition(COND_NO_PRIMARY_AMMO)))
|
|
{
|
|
return SCHED_HIDE_AND_RELOAD;
|
|
}
|
|
|
|
// Can we see the enemy?
|
|
if (!HasCondition(COND_SEE_ENEMY))
|
|
{
|
|
// Chase!
|
|
return SCHED_CHASE_ENEMY;
|
|
}
|
|
|
|
if (HasCondition(COND_TOO_CLOSE_TO_ATTACK))
|
|
return SCHED_BACK_AWAY_FROM_ENEMY;
|
|
|
|
|
|
// we can see the enemy
|
|
if (HasCondition(COND_CAN_MELEE_ATTACK1)) {
|
|
return SCHED_MELEE_ATTACK1;
|
|
}
|
|
|
|
if (HasCondition(COND_CAN_MELEE_ATTACK2)) {
|
|
return SCHED_MELEE_ATTACK2;
|
|
}
|
|
|
|
if (HasRangedWeapon() && GetShotRegulator()->IsInRestInterval())
|
|
{
|
|
if (HasCondition(COND_CAN_RANGE_ATTACK1))
|
|
return SCHED_COMBAT_FACE;
|
|
}
|
|
|
|
if (HasRangedWeapon() && HasCondition(COND_CAN_RANGE_ATTACK1))
|
|
{
|
|
if (OccupyStrategySlotRange(SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2))
|
|
return SCHED_RANGE_ATTACK1;
|
|
return SCHED_RUN_FROM_ENEMY;
|
|
}
|
|
|
|
if (HasRangedWeapon() && HasCondition(COND_CAN_RANGE_ATTACK2))
|
|
return SCHED_RANGE_ATTACK2;
|
|
|
|
|
|
if (HasCondition(COND_NOT_FACING_ATTACK))
|
|
return SCHED_COMBAT_FACE;
|
|
|
|
if (!HasCondition(COND_CAN_RANGE_ATTACK1) && !HasCondition(COND_CAN_MELEE_ATTACK1))
|
|
{
|
|
// if we can see enemy but can't use either attack type, we must need to get closer to enemy
|
|
if (HasRangedWeapon())
|
|
return SCHED_MOVE_TO_WEAPON_RANGE;
|
|
|
|
return SCHED_CHASE_ENEMY;
|
|
}
|
|
|
|
DevWarning(2, "No suitable combat schedule!\n");
|
|
return SCHED_FAIL;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Override base class schedules
|
|
//-----------------------------------------------------------------------------
|
|
int CNPC_ShadowWalker::TranslateSchedule(int scheduleType)
|
|
{
|
|
switch (scheduleType)
|
|
{
|
|
case SCHED_MELEE_ATTACK1:
|
|
return SCHED_MELEE_ATTACK_NOINTERRUPT;
|
|
case SCHED_IDLE_WANDER: // We want idle wandering to be interruptible - patrol walk is a better schedule
|
|
return SCHED_PATROL_WALK;
|
|
}
|
|
|
|
return BaseClass::TranslateSchedule(scheduleType);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Override base class activiites
|
|
//-----------------------------------------------------------------------------
|
|
Activity CNPC_ShadowWalker::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 walker 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:
|
|
//
|
|
//
|
|
// Output :
|
|
//-----------------------------------------------------------------------------
|
|
Class_T CNPC_ShadowWalker::Classify( void )
|
|
{
|
|
return CLASS_ZOMBIE;
|
|
}
|