//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // //=============================================================================// #include "cbase.h" #include "soundent.h" #include "game.h" #include "beam_shared.h" #include "Sprite.h" #include "npcevent.h" #include "npc_stalker.h" #include "ai_hull.h" #include "ai_default.h" #include "ai_node.h" #include "ai_network.h" #include "ai_hint.h" #include "ai_link.h" #include "ai_waypoint.h" #include "ai_navigator.h" #include "ai_senses.h" #include "ai_squadslot.h" #include "ai_squad.h" #include "ai_memory.h" #include "ai_tacticalservices.h" #include "ai_moveprobe.h" #include "npc_talker.h" #include "activitylist.h" #include "bitstring.h" #include "decals.h" #include "player.h" #include "entitylist.h" #include "ndebugoverlay.h" #include "ai_interactions.h" #include "animation.h" #include "scriptedtarget.h" #include "vstdlib/random.h" #include "engine/IEngineSound.h" #include "world.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" //#define STALKER_DEBUG #define MIN_STALKER_FIRE_RANGE 64 #define MAX_STALKER_FIRE_RANGE 3600 // 3600 feet. #define STALKER_LASER_ATTACHMENT 1 #define STALKER_TRIGGER_DIST 200 // Enemy dist. that wakes up the stalker #define STALKER_SENTENCE_VOLUME (float)0.35 #define STALKER_LASER_DURATION 99999 #define STALKER_LASER_RECHARGE 1 #define STALKER_PLAYER_AGGRESSION 1 enum StalkerBeamPower_e { STALKER_BEAM_LOW, STALKER_BEAM_MED, STALKER_BEAM_HIGH, }; //Animation events #define STALKER_AE_MELEE_HIT 1 ConVar sk_stalker_health( "sk_stalker_health","0"); ConVar sk_stalker_melee_dmg( "sk_stalker_melee_dmg","0"); extern void SpawnBlood(Vector vecSpot, const Vector &vAttackDir, int bloodColor, float flDamage); LINK_ENTITY_TO_CLASS( npc_stalker, CNPC_Stalker ); float g_StalkerBeamThinkTime = 0.0; //0.025; //========================================================= // Private activities. //========================================================= static int ACT_STALKER_WORK = 0; //========================================================= // Stalker schedules //========================================================= enum { SCHED_STALKER_CHASE_ENEMY = LAST_SHARED_SCHEDULE, SCHED_STALKER_RANGE_ATTACK, SCHED_STALKER_ACQUIRE_PLAYER, SCHED_STALKER_PATROL, }; //========================================================= // Stalker Tasks //========================================================= enum { TASK_STALKER_ZIGZAG = LAST_SHARED_TASK, TASK_STALKER_SCREAM, }; // ----------------------------------------------- // > Squad slots // ----------------------------------------------- enum SquadSlot_T { SQUAD_SLOT_CHASE_ENEMY_1 = LAST_SHARED_SQUADSLOT, SQUAD_SLOT_CHASE_ENEMY_2, }; //----------------------------------------------------------------------------- BEGIN_DATADESC( CNPC_Stalker ) DEFINE_KEYFIELD( m_eBeamPower, FIELD_INTEGER, "BeamPower" ), DEFINE_FIELD( m_vLaserDir, FIELD_VECTOR), DEFINE_FIELD( m_vLaserTargetPos, FIELD_POSITION_VECTOR), DEFINE_FIELD( m_fBeamEndTime, FIELD_FLOAT), DEFINE_FIELD( m_fBeamRechargeTime, FIELD_FLOAT), DEFINE_FIELD( m_fNextDamageTime, FIELD_FLOAT), DEFINE_FIELD( m_bPlayingHitWall, FIELD_FLOAT), DEFINE_FIELD( m_bPlayingHitFlesh, FIELD_FLOAT), DEFINE_FIELD( m_pBeam, FIELD_CLASSPTR), DEFINE_FIELD( m_pLightGlow, FIELD_CLASSPTR), DEFINE_FIELD( m_flNextNPCThink, FIELD_FLOAT), DEFINE_FIELD( m_vLaserCurPos, FIELD_POSITION_VECTOR), DEFINE_FIELD( m_flNextAttackSoundTime, FIELD_TIME ), DEFINE_FIELD( m_flNextBreatheSoundTime, FIELD_TIME ), DEFINE_FIELD( m_flNextScrambleSoundTime, FIELD_TIME ), DEFINE_FIELD( m_nextSmokeTime, FIELD_TIME ), DEFINE_FIELD( m_iPlayerAggression, FIELD_INTEGER ), DEFINE_FIELD( m_flNextScreamTime, FIELD_TIME ), // Function Pointers DEFINE_THINKFUNC( StalkerThink ), END_DATADESC() //----------------------------------------------------------------------------- // Purpose: // Input : // Output : //----------------------------------------------------------------------------- int CNPC_Stalker::OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ) { CTakeDamageInfo info = inputInfo; // -------------------------------------------- // Don't take a lot of damage from Vortigaunt // -------------------------------------------- if (info.GetAttacker()->Classify() == CLASS_VORTIGAUNT) { info.ScaleDamage( 0.25 ); } int ret = BaseClass::OnTakeDamage_Alive( info ); // If player shot me make sure I'm mad at him even if I wasn't earlier if ( (info.GetAttacker()->GetFlags() & FL_CLIENT) ) { AddClassRelationship( CLASS_PLAYER, D_HT, 0 ); } return ret; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- float CNPC_Stalker::MaxYawSpeed( void ) { #ifdef HL2_EPISODIC return 10.0f; #else switch( GetActivity() ) { case ACT_TURN_LEFT: case ACT_TURN_RIGHT: return 160; break; case ACT_RUN: case ACT_RUN_HURT: return 280; break; default: return 160; break; } #endif } //----------------------------------------------------------------------------- // Purpose: // // // Output : int //----------------------------------------------------------------------------- Class_T CNPC_Stalker::Classify( void ) { return CLASS_STALKER; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Stalker::PrescheduleThink() { if (gpGlobals->curtime > m_flNextBreatheSoundTime) { EmitSound( "NPC_Stalker.Ambient01" ); m_flNextBreatheSoundTime = gpGlobals->curtime + 3.0 + random->RandomFloat( 0.0, 5.0 ); } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Stalker::IsValidEnemy( CBaseEntity *pEnemy ) { Class_T enemyClass = pEnemy->Classify(); if( enemyClass == CLASS_PLAYER || enemyClass == CLASS_PLAYER_ALLY || enemyClass == CLASS_PLAYER_ALLY_VITAL ) { // Don't get angry at these folks unless provoked. if( m_iPlayerAggression < STALKER_PLAYER_AGGRESSION ) { return false; } } if( enemyClass == CLASS_BULLSEYE && pEnemy->GetParent() ) { // This bullseye is in heirarchy with something. If that // something is held by the physcannon, this bullseye is // NOT a valid enemy. IPhysicsObject *pPhys = pEnemy->GetParent()->VPhysicsGetObject(); if( pPhys && (pPhys->GetGameFlags() & FVPHYSICS_PLAYER_HELD) ) { return false; } } if( GetEnemy() && HasCondition(COND_SEE_ENEMY) ) { // Short attention span. If I have an enemy, stick with it. if( GetEnemy() != pEnemy ) { return false; } } if( IsStrategySlotRangeOccupied( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) && !HasStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) ) { return false; } if( !FVisible(pEnemy) ) { // Don't take an enemy you can't see. Since stalkers move way too slowly to // establish line of fire, usually an enemy acquired by means other than // the Stalker's own eyesight will always get away while the stalker plods // slowly to their last known position. So don't take enemies you can't see. return false; } return BaseClass::IsValidEnemy(pEnemy); } //----------------------------------------------------------------------------- // Purpose: // // //----------------------------------------------------------------------------- void CNPC_Stalker::Spawn( void ) { Precache( ); SetModel( "models/stalker.mdl" ); SetHullType(HULL_HUMAN); SetHullSizeNormal(); SetSolid( SOLID_BBOX ); AddSolidFlags( FSOLID_NOT_STANDABLE ); SetMoveType( MOVETYPE_STEP ); m_bloodColor = DONT_BLEED; m_iHealth = sk_stalker_health.GetFloat(); m_flFieldOfView = 0.1;// indicates the width of this NPC's forward view cone ( as a dotproduct result ) m_NPCState = NPC_STATE_NONE; CapabilitiesAdd( bits_CAP_SQUAD | bits_CAP_MOVE_GROUND ); CapabilitiesAdd( bits_CAP_INNATE_RANGE_ATTACK1); m_flNextAttackSoundTime = 0; m_flNextBreatheSoundTime = gpGlobals->curtime + random->RandomFloat( 0.0, 10.0 ); m_flNextScrambleSoundTime = 0; m_nextSmokeTime = 0; m_bPlayingHitWall = false; m_bPlayingHitFlesh = false; m_fBeamEndTime = 0; m_fBeamRechargeTime = 0; m_fNextDamageTime = 0; NPCInit(); m_flDistTooFar = MAX_STALKER_FIRE_RANGE; m_iPlayerAggression = 0; GetSenses()->SetDistLook(MAX_STALKER_FIRE_RANGE - 1); } //----------------------------------------------------------------------------- // Purpose: // // //----------------------------------------------------------------------------- void CNPC_Stalker::Precache( void ) { PrecacheModel("models/stalker.mdl"); PrecacheModel("sprites/laser.vmt"); PrecacheModel("sprites/redglow1.vmt"); PrecacheModel("sprites/orangeglow1.vmt"); PrecacheModel("sprites/yellowglow1.vmt"); PrecacheScriptSound( "NPC_Stalker.BurnFlesh" ); PrecacheScriptSound( "NPC_Stalker.BurnWall" ); PrecacheScriptSound( "NPC_Stalker.FootstepLeft" ); PrecacheScriptSound( "NPC_Stalker.FootstepRight" ); PrecacheScriptSound( "NPC_Stalker.Hit" ); PrecacheScriptSound( "NPC_Stalker.Ambient01" ); PrecacheScriptSound( "NPC_Stalker.Scream" ); PrecacheScriptSound( "NPC_Stalker.Pain" ); PrecacheScriptSound( "NPC_Stalker.Die" ); BaseClass::Precache(); } //----------------------------------------------------------------------------- // Purpose: // Input : - // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CNPC_Stalker::CreateBehaviors() { AddBehavior( &m_ActBusyBehavior ); return BaseClass::CreateBehaviors(); } //----------------------------------------------------------------------------- // Purpose: // Input : // Output : //----------------------------------------------------------------------------- void CNPC_Stalker::IdleSound ( void ) { } void CNPC_Stalker::OnScheduleChange() { KillAttackBeam(); BaseClass::OnScheduleChange(); } //----------------------------------------------------------------------------- // Purpose: // Input : pInflictor - // pAttacker - // flDamage - // bitsDamageType - //----------------------------------------------------------------------------- void CNPC_Stalker::Event_Killed( const CTakeDamageInfo &info ) { if( IsInSquad() && info.GetAttacker()->IsPlayer() ) { AISquadIter_t iter; for ( CAI_BaseNPC *pSquadMember = GetSquad()->GetFirstMember( &iter ); pSquadMember; pSquadMember = GetSquad()->GetNextMember( &iter ) ) { if ( pSquadMember->IsAlive() && pSquadMember != this ) { CNPC_Stalker *pStalker = dynamic_cast (pSquadMember); if( pStalker && pStalker->FVisible(info.GetAttacker()) ) { pStalker->m_iPlayerAggression++; } } } } KillAttackBeam(); BaseClass::Event_Killed( info ); } //----------------------------------------------------------------------------- // Purpose: // Input : // Output : //----------------------------------------------------------------------------- void CNPC_Stalker::DeathSound( const CTakeDamageInfo &info ) { EmitSound( "NPC_Stalker.Die" ); }; //----------------------------------------------------------------------------- // Purpose: // Input : // Output : //----------------------------------------------------------------------------- void CNPC_Stalker::PainSound( const CTakeDamageInfo &info ) { EmitSound( "NPC_Stalker.Pain" ); m_flNextScrambleSoundTime = gpGlobals->curtime + 1.5; m_flNextBreatheSoundTime = gpGlobals->curtime + 1.5; m_flNextAttackSoundTime = gpGlobals->curtime + 1.5; }; //----------------------------------------------------------------------------- // Purpose: Translates squad slot positions into schedules // Input : // Output : //----------------------------------------------------------------------------- #if 0 // @TODO (toml 07-18-03): this function is never called. Presumably what it is trying to do still needs to be done... int CNPC_Stalker::GetSlotSchedule(int slotID) { switch (slotID) { case SQUAD_SLOT_CHASE_ENEMY_1: case SQUAD_SLOT_CHASE_ENEMY_2: return SCHED_STALKER_CHASE_ENEMY; break; } return SCHED_NONE; } #endif void CNPC_Stalker::UpdateAttackBeam( void ) { CBaseEntity *pEnemy = GetEnemy(); // If not burning at a target if (pEnemy) { if (gpGlobals->curtime > m_fBeamEndTime) { TaskComplete(); } else { Vector enemyLKP = GetEnemyLKP(); m_vLaserTargetPos = enemyLKP + pEnemy->GetViewOffset(); // Face my enemy GetMotor()->SetIdealYawToTargetAndUpdate( enemyLKP ); // --------------------------------------------- // Get beam end point // --------------------------------------------- Vector vecSrc = LaserStartPosition(GetAbsOrigin()); Vector targetDir = m_vLaserTargetPos - vecSrc; VectorNormalize(targetDir); // -------------------------------------------------------- // If beam position and laser dir are way off, end attack // -------------------------------------------------------- if ( DotProduct(targetDir,m_vLaserDir) < 0.5 ) { TaskComplete(); return; } trace_t tr; AI_TraceLine( vecSrc, vecSrc + m_vLaserDir * MAX_STALKER_FIRE_RANGE, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr); // --------------------------------------------- // If beam not long enough, stop attacking // --------------------------------------------- if (tr.fraction == 1.0) { TaskComplete(); return; } CSoundEnt::InsertSound(SOUND_DANGER, tr.endpos, 60, 0.025, this); } } else { TaskFail(FAIL_NO_ENEMY); } } //========================================================= // start task //========================================================= void CNPC_Stalker::StartTask( const Task_t *pTask ) { switch ( pTask->iTask ) { case TASK_STALKER_SCREAM: { if( gpGlobals->curtime > m_flNextScreamTime ) { EmitSound( "NPC_Stalker.Scream" ); m_flNextScreamTime = gpGlobals->curtime + random->RandomFloat( 10.0, 15.0 ); } TaskComplete(); } case TASK_ANNOUNCE_ATTACK: { // If enemy isn't facing me and I haven't attacked in a while // annouce my attack before I start wailing away CBaseCombatCharacter *pBCC = GetEnemyCombatCharacterPointer(); if (pBCC && (!pBCC->FInViewCone ( this )) && (gpGlobals->curtime - m_flLastAttackTime > 1.0) ) { m_flLastAttackTime = gpGlobals->curtime; // Always play this sound EmitSound( "NPC_Stalker.Scream" ); m_flNextScrambleSoundTime = gpGlobals->curtime + 2; m_flNextBreatheSoundTime = gpGlobals->curtime + 2; // Wait two seconds SetWait( 2.0 ); SetActivity(ACT_IDLE); } break; } case TASK_STALKER_ZIGZAG: break; case TASK_RANGE_ATTACK1: { CBaseEntity *pEnemy = GetEnemy(); if (pEnemy) { m_vLaserTargetPos = GetEnemyLKP() + pEnemy->GetViewOffset(); // Never hit target on first try Vector missPos = m_vLaserTargetPos; if( pEnemy->Classify() == CLASS_BULLSEYE && hl2_episodic.GetBool() ) { missPos.x += 60 + 120*random->RandomInt(-1,1); missPos.y += 60 + 120*random->RandomInt(-1,1); } else { missPos.x += 80*random->RandomInt(-1,1); missPos.y += 80*random->RandomInt(-1,1); } // ---------------------------------------------------------------------- // If target is facing me and not running towards me shoot below his feet // so he can see the laser coming // ---------------------------------------------------------------------- CBaseCombatCharacter *pBCC = ToBaseCombatCharacter(pEnemy); if (pBCC) { Vector targetToMe = (pBCC->GetAbsOrigin() - GetAbsOrigin()); Vector vBCCFacing = pBCC->BodyDirection2D( ); if ((DotProduct(vBCCFacing,targetToMe) < 0) && (pBCC->GetSmoothedVelocity().Length() < 50)) { missPos.z -= 150; } // -------------------------------------------------------- // If facing away or running towards laser, // shoot above target's head // -------------------------------------------------------- else { missPos.z += 60; } } m_vLaserDir = missPos - LaserStartPosition(GetAbsOrigin()); VectorNormalize(m_vLaserDir); } else { TaskFail(FAIL_NO_ENEMY); return; } StartAttackBeam(); SetActivity(ACT_RANGE_ATTACK1); break; } case TASK_GET_PATH_TO_ENEMY_LOS: { if ( GetEnemy() != NULL ) { BaseClass:: StartTask( pTask ); return; } Vector posLos; if (GetTacticalServices()->FindLos(m_vLaserCurPos, m_vLaserCurPos, MIN_STALKER_FIRE_RANGE, MAX_STALKER_FIRE_RANGE, 1.0, &posLos)) { AI_NavGoal_t goal( posLos, ACT_RUN, AIN_HULL_TOLERANCE ); GetNavigator()->SetGoal( goal ); } else { TaskFail(FAIL_NO_SHOOT); } break; } case TASK_FACE_ENEMY: { if ( GetEnemy() != NULL ) { BaseClass:: StartTask( pTask ); return; } GetMotor()->SetIdealYawToTarget( m_vLaserCurPos ); break; } default: BaseClass:: StartTask( pTask ); break; } } //========================================================= // RunTask //========================================================= void CNPC_Stalker::RunTask( const Task_t *pTask ) { switch ( pTask->iTask ) { case TASK_ANNOUNCE_ATTACK: { // Stop waiting if enemy facing me or lost enemy CBaseCombatCharacter* pBCC = GetEnemyCombatCharacterPointer(); if (!pBCC || pBCC->FInViewCone( this )) { TaskComplete(); } if ( IsWaitFinished() ) { TaskComplete(); } break; } case TASK_STALKER_ZIGZAG : { if (GetNavigator()->GetGoalType() == GOALTYPE_NONE) { TaskComplete(); GetNavigator()->StopMoving(); // Stop moving } else if (!GetNavigator()->IsGoalActive()) { SetIdealActivity( GetStoppedActivity() ); } else if (ValidateNavGoal()) { SetIdealActivity( GetNavigator()->GetMovementActivity() ); AddZigZagToPath(); } break; } case TASK_RANGE_ATTACK1: UpdateAttackBeam(); if ( !TaskIsRunning() || HasCondition( COND_TASK_FAILED )) { KillAttackBeam(); } break; case TASK_FACE_ENEMY: { if ( GetEnemy() != NULL ) { BaseClass:: RunTask( pTask ); return; } GetMotor()->SetIdealYawToTargetAndUpdate( m_vLaserCurPos ); if ( FacingIdeal() ) { TaskComplete(); } break; } default: { BaseClass::RunTask( pTask ); break; } } } //----------------------------------------------------------------------------- // Purpose: // Input : // Output : //----------------------------------------------------------------------------- int CNPC_Stalker::SelectSchedule( void ) { if ( BehaviorSelectSchedule() ) { return BaseClass::SelectSchedule(); } switch ( m_NPCState ) { case NPC_STATE_IDLE: case NPC_STATE_ALERT: { if( HasCondition(COND_IN_PVS) ) { return SCHED_STALKER_PATROL; } return SCHED_IDLE_STAND; break; } case NPC_STATE_COMBAT: { // ----------- // new enemy // ----------- if( HasCondition( COND_NEW_ENEMY ) ) { if( GetEnemy()->IsPlayer() ) { return SCHED_STALKER_ACQUIRE_PLAYER; } } if( HasCondition( COND_CAN_RANGE_ATTACK1 ) ) { if( OccupyStrategySlotRange(SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2) ) { return SCHED_RANGE_ATTACK1; } else { return SCHED_STALKER_PATROL; } } if( !HasCondition(COND_SEE_ENEMY) ) { return SCHED_STALKER_PATROL; } return SCHED_COMBAT_FACE; break; } } // no special cases here, call the base class return BaseClass::SelectSchedule(); } //----------------------------------------------------------------------------- // Purpose: // Input : // Output : //----------------------------------------------------------------------------- int CNPC_Stalker::TranslateSchedule( int scheduleType ) { switch( scheduleType ) { case SCHED_RANGE_ATTACK1: { return SCHED_STALKER_RANGE_ATTACK; } case SCHED_FAIL_ESTABLISH_LINE_OF_FIRE: { return SCHED_COMBAT_STAND; break; } case SCHED_FAIL_TAKE_COVER: { return SCHED_RUN_RANDOM; break; } } return BaseClass::TranslateSchedule( scheduleType ); } //------------------------------------------------------------------------------ // Purpose : Returns position of laser for any given position of the staler // Input : // Output : //------------------------------------------------------------------------------ Vector CNPC_Stalker::LaserStartPosition(Vector vStalkerPos) { // Get attachment position Vector vAttachPos; GetAttachment(STALKER_LASER_ATTACHMENT,vAttachPos); // Now convert to vStalkerPos vAttachPos = vAttachPos - GetAbsOrigin() + vStalkerPos; return vAttachPos; } //------------------------------------------------------------------------------ // Purpose : Calculate position of beam // Input : // Output : //------------------------------------------------------------------------------ void CNPC_Stalker::CalcBeamPosition(void) { Vector targetDir = m_vLaserTargetPos - LaserStartPosition(GetAbsOrigin()); VectorNormalize(targetDir); // --------------------------------------- // Otherwise if burning towards an enemy // --------------------------------------- if ( GetEnemy() ) { // --------------------------------------- // Integrate towards target position // --------------------------------------- float iRate = 0.95; if( GetEnemy()->Classify() == CLASS_BULLSEYE ) { // Seek bullseyes faster iRate = 0.8; } m_vLaserDir.x = (iRate * m_vLaserDir.x + (1-iRate) * targetDir.x); m_vLaserDir.y = (iRate * m_vLaserDir.y + (1-iRate) * targetDir.y); m_vLaserDir.z = (iRate * m_vLaserDir.z + (1-iRate) * targetDir.z); VectorNormalize( m_vLaserDir ); // ----------------------------------------- // Add time-coherent noise to the position // Must be scaled with distance // ----------------------------------------- float fTargetDist = (GetAbsOrigin() - m_vLaserTargetPos).Length(); float noiseScale = atan(0.2/fTargetDist); float m_fNoiseModX = 5; float m_fNoiseModY = 5; float m_fNoiseModZ = 5; m_vLaserDir.x += 5*noiseScale*sin(m_fNoiseModX * gpGlobals->curtime + m_fNoiseModX); m_vLaserDir.y += 5*noiseScale*sin(m_fNoiseModY * gpGlobals->curtime + m_fNoiseModY); m_vLaserDir.z += 5*noiseScale*sin(m_fNoiseModZ * gpGlobals->curtime + m_fNoiseModZ); } } void CNPC_Stalker::StartAttackBeam( void ) { if ( m_fBeamEndTime > gpGlobals->curtime || m_fBeamRechargeTime > gpGlobals->curtime ) { // UNDONE: Debug this and fix!?!?! m_fBeamRechargeTime = gpGlobals->curtime; } // --------------------------------------------- // If I don't have a beam yet, create one // --------------------------------------------- // UNDONE: Why would I ever have a beam already?!?!?! if (!m_pBeam) { Vector vecSrc = LaserStartPosition(GetAbsOrigin()); trace_t tr; AI_TraceLine ( vecSrc, vecSrc + m_vLaserDir * MAX_STALKER_FIRE_RANGE, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr); if ( tr.fraction >= 1.0 ) { // too far TaskComplete(); return; } m_pBeam = CBeam::BeamCreate( "sprites/laser.vmt", 2.0 ); m_pBeam->PointEntInit( tr.endpos, this ); m_pBeam->SetEndAttachment( STALKER_LASER_ATTACHMENT ); m_pBeam->SetBrightness( 255 ); m_pBeam->SetNoise( 0 ); switch (m_eBeamPower) { case STALKER_BEAM_LOW: m_pBeam->SetColor( 255, 0, 0 ); m_pLightGlow = CSprite::SpriteCreate( "sprites/redglow1.vmt", GetAbsOrigin(), FALSE ); break; case STALKER_BEAM_MED: m_pBeam->SetColor( 255, 50, 0 ); m_pLightGlow = CSprite::SpriteCreate( "sprites/orangeglow1.vmt", GetAbsOrigin(), FALSE ); break; case STALKER_BEAM_HIGH: m_pBeam->SetColor( 255, 150, 0 ); m_pLightGlow = CSprite::SpriteCreate( "sprites/yellowglow1.vmt", GetAbsOrigin(), FALSE ); break; } // ---------------------------- // Light myself in a red glow // ---------------------------- m_pLightGlow->SetTransparency( kRenderGlow, 255, 200, 200, 0, kRenderFxNoDissipation ); m_pLightGlow->SetAttachment( this, 1 ); m_pLightGlow->SetBrightness( 255 ); m_pLightGlow->SetScale( 0.65 ); #if 0 CBaseEntity *pEnemy = GetEnemy(); // -------------------------------------------------------- // Play start up sound - client should always hear this! // -------------------------------------------------------- if (pEnemy != NULL && (pEnemy->IsPlayer()) ) { EmitAmbientSound( 0, pEnemy->GetAbsOrigin(), "NPC_Stalker.AmbientLaserStart" ); } else { EmitAmbientSound( 0, GetAbsOrigin(), "NPC_Stalker.AmbientLaserStart" ); } #endif } SetThink( &CNPC_Stalker::StalkerThink ); m_flNextNPCThink = GetNextThink(); SetNextThink( gpGlobals->curtime + g_StalkerBeamThinkTime ); m_fBeamEndTime = gpGlobals->curtime + STALKER_LASER_DURATION; } //------------------------------------------------------------------------------ // Purpose : Update beam more often then regular NPC think so it doesn't // move so jumpily over the ground // Input : // Output : //------------------------------------------------------------------------------ void CNPC_Stalker::StalkerThink(void) { DrawAttackBeam(); if (gpGlobals->curtime >= m_flNextNPCThink) { NPCThink(); m_flNextNPCThink = GetNextThink(); } if ( m_pBeam ) { SetNextThink( gpGlobals->curtime + g_StalkerBeamThinkTime ); // sanity check?! const Task_t *pTask = GetTask(); if ( !pTask || pTask->iTask != TASK_RANGE_ATTACK1 || !TaskIsRunning() ) { KillAttackBeam(); } } else { DevMsg( 2, "In StalkerThink() but no stalker beam found?\n" ); SetNextThink( m_flNextNPCThink ); } } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ void CNPC_Stalker::NotifyDeadFriend( CBaseEntity *pFriend ) { BaseClass::NotifyDeadFriend(pFriend); } void CNPC_Stalker::DoSmokeEffect( const Vector &position ) { if ( gpGlobals->curtime > m_nextSmokeTime ) { m_nextSmokeTime = gpGlobals->curtime + 0.5; UTIL_Smoke(position, random->RandomInt(5, 10), 10); } } //------------------------------------------------------------------------------ // Purpose : Draw attack beam and do damage / decals // Input : // Output : //------------------------------------------------------------------------------ void CNPC_Stalker::DrawAttackBeam(void) { if (!m_pBeam) return; // --------------------------------------------- // Get beam end point // --------------------------------------------- Vector vecSrc = LaserStartPosition(GetAbsOrigin()); trace_t tr; AI_TraceLine( vecSrc, vecSrc + m_vLaserDir * MAX_STALKER_FIRE_RANGE, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr); CalcBeamPosition(); bool bInWater = (UTIL_PointContents ( tr.endpos ) & MASK_WATER)?true:false; // --------------------------------------------- // Update the beam position // --------------------------------------------- m_pBeam->SetStartPos( tr.endpos ); m_pBeam->RelinkBeam(); Vector vAttachPos; GetAttachment(STALKER_LASER_ATTACHMENT,vAttachPos); Vector vecAimDir = tr.endpos - vAttachPos; VectorNormalize( vecAimDir ); SetAim( vecAimDir ); // -------------------------------------------- // Play burn sounds // -------------------------------------------- CBaseCombatCharacter *pBCC = ToBaseCombatCharacter( tr.m_pEnt ); if (pBCC) { if (gpGlobals->curtime > m_fNextDamageTime) { ClearMultiDamage(); float damage = 0.0; switch (m_eBeamPower) { case STALKER_BEAM_LOW: damage = 1; break; case STALKER_BEAM_MED: damage = 3; break; case STALKER_BEAM_HIGH: damage = 10; break; } CTakeDamageInfo info( this, this, damage, DMG_SHOCK ); CalculateMeleeDamageForce( &info, m_vLaserDir, tr.endpos ); pBCC->DispatchTraceAttack( info, m_vLaserDir, &tr ); ApplyMultiDamage(); m_fNextDamageTime = gpGlobals->curtime + 0.1; } if (pBCC->Classify()!=CLASS_BULLSEYE) { if (!m_bPlayingHitFlesh) { CPASAttenuationFilter filter( m_pBeam,"NPC_Stalker.BurnFlesh" ); filter.MakeReliable(); EmitSound( filter, m_pBeam->entindex(),"NPC_Stalker.BurnFlesh" ); m_bPlayingHitFlesh = true; } if (m_bPlayingHitWall) { StopSound( m_pBeam->entindex(), "NPC_Stalker.BurnWall" ); m_bPlayingHitWall = false; } tr.endpos.z -= 24.0f; if (!bInWater) { DoSmokeEffect(tr.endpos + tr.plane.normal * 8); } } } if (!pBCC || pBCC->Classify()==CLASS_BULLSEYE) { if (!m_bPlayingHitWall) { CPASAttenuationFilter filter( m_pBeam, "NPC_Stalker.BurnWall" ); filter.MakeReliable(); EmitSound( filter, m_pBeam->entindex(), "NPC_Stalker.BurnWall" ); m_bPlayingHitWall = true; } if (m_bPlayingHitFlesh) { StopSound(m_pBeam->entindex(), "NPC_Stalker.BurnFlesh" ); m_bPlayingHitFlesh = false; } UTIL_DecalTrace( &tr, "RedGlowFade"); UTIL_DecalTrace( &tr, "FadingScorch" ); tr.endpos.z -= 24.0f; if (!bInWater) { DoSmokeEffect(tr.endpos + tr.plane.normal * 8); } } if (bInWater) { UTIL_Bubbles(tr.endpos-Vector(3,3,3),tr.endpos+Vector(3,3,3),10); } /* CBroadcastRecipientFilter filter; TE_DynamicLight( filter, 0.0, EyePosition(), 255, 0, 0, 5, 0.2, 0 ); */ } //------------------------------------------------------------------------------ // Purpose : Draw attack beam and do damage / decals // Input : // Output : //------------------------------------------------------------------------------ void CNPC_Stalker::KillAttackBeam(void) { if ( !m_pBeam ) return; // Kill sound StopSound(m_pBeam->entindex(), "NPC_Stalker.BurnWall" ); StopSound(m_pBeam->entindex(), "NPC_Stalker.BurnFlesh" ); UTIL_Remove( m_pLightGlow ); UTIL_Remove( m_pBeam); m_pBeam = NULL; m_bPlayingHitWall = false; m_bPlayingHitFlesh = false; SetThink(&CNPC_Stalker::CallNPCThink); if ( m_flNextNPCThink > gpGlobals->curtime ) { SetNextThink( m_flNextNPCThink ); } // Beam has to recharge m_fBeamRechargeTime = gpGlobals->curtime + STALKER_LASER_RECHARGE; ClearCondition( COND_CAN_RANGE_ATTACK1 ); RelaxAim(); } //----------------------------------------------------------------------------- // Purpose: Override so can handle LOS to m_pScriptedTarget // Input : // Output : //----------------------------------------------------------------------------- bool CNPC_Stalker::InnateWeaponLOSCondition( const Vector &ownerPos, const Vector &targetPos, bool bSetConditions ) { // -------------------- // Check for occlusion // -------------------- // Base class version assumes innate weapon position is at eye level Vector barrelPos = LaserStartPosition(ownerPos); trace_t tr; AI_TraceLine( barrelPos, targetPos, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr); if ( tr.fraction == 1.0 ) { return true; } CBaseEntity *pBE = tr.m_pEnt; CBaseCombatCharacter *pBCC = ToBaseCombatCharacter( pBE ); if ( pBE == GetEnemy() ) { return true; } else if (pBCC) { if (IRelationType( pBCC ) == D_HT) { return true; } else if (bSetConditions) { SetCondition(COND_WEAPON_BLOCKED_BY_FRIEND); } } else if (bSetConditions) { SetCondition(COND_WEAPON_SIGHT_OCCLUDED); SetEnemyOccluder(pBE); } return false; } //----------------------------------------------------------------------------- // Purpose: For innate melee attack // Input : // Output : //----------------------------------------------------------------------------- int CNPC_Stalker::MeleeAttack1Conditions ( float flDot, float flDist ) { if (flDist > MIN_STALKER_FIRE_RANGE) { return COND_TOO_FAR_TO_ATTACK; } else if (flDot < 0.7) { return COND_NOT_FACING_ATTACK; } return COND_CAN_MELEE_ATTACK1; } //----------------------------------------------------------------------------- // Purpose: For innate range attack // Input : // Output : //----------------------------------------------------------------------------- int CNPC_Stalker::RangeAttack1Conditions( float flDot, float flDist ) { if (gpGlobals->curtime < m_fBeamRechargeTime ) { return COND_NONE; } if( IsStrategySlotRangeOccupied( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) ) { // Couldn't attack if I wanted to. return COND_NONE; } if (flDist <= MIN_STALKER_FIRE_RANGE) { return COND_TOO_CLOSE_TO_ATTACK; } else if (flDist > (MAX_STALKER_FIRE_RANGE * 0.66f) ) { return COND_TOO_FAR_TO_ATTACK; } else if (flDot < 0.7) { return COND_NOT_FACING_ATTACK; } return COND_CAN_RANGE_ATTACK1; } //----------------------------------------------------------------------------- // Purpose: Catch stalker specific messages // Input : // Output : //----------------------------------------------------------------------------- void CNPC_Stalker::HandleAnimEvent( animevent_t *pEvent ) { switch( pEvent->event ) { case NPC_EVENT_LEFTFOOT: { EmitSound( "NPC_Stalker.FootstepLeft", pEvent->eventtime ); } break; case NPC_EVENT_RIGHTFOOT: { EmitSound( "NPC_Stalker.FootstepRight", pEvent->eventtime ); } break; case STALKER_AE_MELEE_HIT: { CBaseEntity *pHurt; pHurt = CheckTraceHullAttack( 32, Vector(-16,-16,-16), Vector(16,16,16), sk_stalker_melee_dmg.GetFloat(), DMG_SLASH ); if ( pHurt ) { if ( pHurt->GetFlags() & (FL_NPC|FL_CLIENT) ) { pHurt->ViewPunch( QAngle( 5, 0, random->RandomInt(-10,10)) ); } // Spawn some extra blood if we hit a BCC CBaseCombatCharacter* pBCC = ToBaseCombatCharacter( pHurt ); if (pBCC) { SpawnBlood(pBCC->EyePosition(), g_vecAttackDir, pBCC->BloodColor(), sk_stalker_melee_dmg.GetFloat()); } // Play a attack hit sound EmitSound( "NPC_Stalker.Hit" ); } break; } default: BaseClass::HandleAnimEvent( pEvent ); break; } } //----------------------------------------------------------------------------- // Purpose: Tells use whether or not the NPC cares about a given type of hint node. // Input : sHint - // Output : TRUE if the NPC is interested in this hint type, FALSE if not. //----------------------------------------------------------------------------- bool CNPC_Stalker::FValidateHintType(CAI_Hint *pHint) { return(pHint->HintType() == HINT_WORLD_WORK_POSITION); } //----------------------------------------------------------------------------- // Purpose: Override in subclasses to associate specific hint types // with activities // Input : // Output : //----------------------------------------------------------------------------- Activity CNPC_Stalker::GetHintActivity( short sHintType, Activity HintsActivity ) { if (sHintType == HINT_WORLD_WORK_POSITION) { return ( Activity )ACT_STALKER_WORK; } return BaseClass::GetHintActivity( sHintType, HintsActivity ); } //----------------------------------------------------------------------------- // Purpose: Override in subclasses to give specific hint types delays // before they can be used again // Input : // Output : //----------------------------------------------------------------------------- float CNPC_Stalker::GetHintDelay( short sHintType ) { if (sHintType == HINT_WORLD_WORK_POSITION) { return 2.0; } return 0; } //----------------------------------------------------------------------------- // Purpose: // Input : // Output : //----------------------------------------------------------------------------- #define ZIG_ZAG_SIZE 3600 void CNPC_Stalker::AddZigZagToPath(void) { // If already on a detour don't add a zigzag if (GetNavigator()->GetCurWaypointFlags() & bits_WP_TO_DETOUR) { return; } // If enemy isn't facing me or occluded, don't add a zigzag if (HasCondition(COND_ENEMY_OCCLUDED) || !HasCondition ( COND_ENEMY_FACING_ME )) { return; } Vector waypointPos = GetNavigator()->GetCurWaypointPos(); Vector waypointDir = (waypointPos - GetAbsOrigin()); // If the distance to the next node is greater than ZIG_ZAG_SIZE // then add a random zig/zag to the path if (waypointDir.LengthSqr() > ZIG_ZAG_SIZE) { // Pick a random distance for the zigzag (less that sqrt(ZIG_ZAG_SIZE) float distance = random->RandomFloat( 30, 60 ); // Get me a vector orthogonal to the direction of motion VectorNormalize( waypointDir ); Vector vDirUp(0,0,1); Vector vDir; CrossProduct( waypointDir, vDirUp, vDir); // Pick a random direction (left/right) for the zigzag if (random->RandomInt(0,1)) { vDir = -1 * vDir; } // Get zigzag position in direction of target waypoint Vector zigZagPos = GetAbsOrigin() + waypointDir * 60; // Now offset zigZagPos = zigZagPos + (vDir * distance); // Now make sure that we can still get to the zigzag position and the waypoint AIMoveTrace_t moveTrace1, moveTrace2; GetMoveProbe()->MoveLimit( NAV_GROUND, GetAbsOrigin(), zigZagPos, MASK_NPCSOLID, NULL, &moveTrace1); GetMoveProbe()->MoveLimit( NAV_GROUND, zigZagPos, waypointPos, MASK_NPCSOLID, NULL, &moveTrace2); if ( !IsMoveBlocked( moveTrace1 ) && !IsMoveBlocked( moveTrace2 ) ) { GetNavigator()->PrependWaypoint( zigZagPos, NAV_GROUND, bits_WP_TO_DETOUR ); } } } //------------------------------------------------------------------------------ // Purpose : // Input : // Output : //------------------------------------------------------------------------------ CNPC_Stalker::CNPC_Stalker(void) { #ifdef _DEBUG m_vLaserDir.Init(); m_vLaserTargetPos.Init(); m_vLaserCurPos.Init(); #endif } //------------------------------------------------------------------------------ // // Schedules // //------------------------------------------------------------------------------ AI_BEGIN_CUSTOM_NPC( npc_stalker, CNPC_Stalker ) DECLARE_TASK(TASK_STALKER_ZIGZAG) DECLARE_TASK(TASK_STALKER_SCREAM) DECLARE_ACTIVITY(ACT_STALKER_WORK) DECLARE_SQUADSLOT(SQUAD_SLOT_CHASE_ENEMY_1) DECLARE_SQUADSLOT(SQUAD_SLOT_CHASE_ENEMY_2) //========================================================= // > SCHED_STALKER_RANGE_ATTACK //========================================================= DEFINE_SCHEDULE ( SCHED_STALKER_RANGE_ATTACK, " Tasks" " TASK_STOP_MOVING 0" " TASK_FACE_ENEMY 0" " TASK_RANGE_ATTACK1 0" "" " Interrupts" " COND_CAN_MELEE_ATTACK1" " COND_HEAVY_DAMAGE" " COND_REPEATED_DAMAGE" " COND_HEAR_DANGER" " COND_NEW_ENEMY" " COND_ENEMY_DEAD" " COND_ENEMY_OCCLUDED" // Don't break on this. Keep shooting at last location ) //========================================================= // > SCHED_STALKER_CHASE_ENEMY //========================================================= DEFINE_SCHEDULE ( SCHED_STALKER_CHASE_ENEMY, " Tasks" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_CHASE_ENEMY_FAILED" " TASK_SET_TOLERANCE_DISTANCE 24" " TASK_GET_PATH_TO_ENEMY 0" " TASK_RUN_PATH 0" " TASK_STALKER_ZIGZAG 0" "" " Interrupts" " COND_NEW_ENEMY" " COND_ENEMY_DEAD" " COND_CAN_RANGE_ATTACK1" " COND_CAN_MELEE_ATTACK1" " COND_CAN_RANGE_ATTACK2" " COND_CAN_MELEE_ATTACK2" " COND_TOO_CLOSE_TO_ATTACK" " COND_TASK_FAILED" " COND_HEAR_DANGER" ) DEFINE_SCHEDULE ( SCHED_STALKER_ACQUIRE_PLAYER, " Tasks" " TASK_STOP_MOVING 0" " TASK_FACE_ENEMY 0" " TASK_WAIT_RANDOM 0.5" " TASK_STALKER_SCREAM 0" " TASK_WAIT 0.5" " TASK_WAIT_RANDOM 0.5" "" " Interrupts" ) DEFINE_SCHEDULE ( SCHED_STALKER_PATROL, " Tasks" " TASK_STOP_MOVING 0" " TASK_WAIT 0.5"// This makes them look a bit more vigilant, instead of INSTANTLY patrolling after some other action. " TASK_WAIT_RANDOM 0.5" " TASK_WANDER 18000600" " TASK_FACE_PATH 0" " TASK_WALK_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_STOP_MOVING 0" " TASK_FACE_REASONABLE 0" " TASK_SET_SCHEDULE SCHEDULE:SCHED_STALKER_PATROL" "" " Interrupts" " COND_NEW_ENEMY" " COND_CAN_RANGE_ATTACK1" " COND_SEE_ENEMY" ) AI_END_CUSTOM_NPC()