//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: Antlion Guard // //=============================================================================// #include "cbase.h" #include "ai_hint.h" #include "ai_localnavigator.h" #include "ai_memory.h" #include "ai_moveprobe.h" #include "npcevent.h" #include "IEffects.h" #include "ndebugoverlay.h" #include "soundent.h" #include "soundenvelope.h" #include "ai_squad.h" #include "ai_network.h" #include "ai_pathfinder.h" #include "ai_navigator.h" #include "ai_senses.h" #include "npc_rollermine.h" #include "ai_blended_movement.h" #include "physics_prop_ragdoll.h" #include "iservervehicle.h" #include "player_pickup.h" #include "props.h" #include "antlion_dust.h" #include "npc_antlion.h" #include "decals.h" #include "prop_combine_ball.h" #include "eventqueue.h" #include "te_effect_dispatch.h" #include "Sprite.h" #include "particle_parse.h" #include "particle_system.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" inline void TraceHull_SkipPhysics(const Vector &vecAbsStart, const Vector &vecAbsEnd, const Vector &hullMin, const Vector &hullMax, unsigned int mask, const CBaseEntity *ignore, int collisionGroup, trace_t *ptr, float minMass); ConVar g_debug_antlionguard("g_debug_antlionguard", "0"); ConVar sk_antlionguard_dmg_charge("sk_antlionguard_dmg_charge", "0"); ConVar sk_antlionguard_dmg_shove("sk_antlionguard_dmg_shove", "0"); #if HL2_EPISODIC // When enabled, add code to have the antlion bleed profusely as it is badly injured. #define ANTLIONGUARD_BLOOD_EFFECTS 2 ConVar g_antlionguard_hemorrhage("g_antlionguard_hemorrhage", "1", FCVAR_NONE, "If 1, guard will emit a bleeding particle effect when wounded."); #endif // Spawnflags #define SF_ANTLIONGUARD_SERVERSIDE_RAGDOLL ( 1 << 16 ) #define SF_ANTLIONGUARD_INSIDE_FOOTSTEPS ( 1 << 17 ) #define ENVELOPE_CONTROLLER (CSoundEnvelopeController::GetController()) #define ANTLIONGUARD_MODEL "models/antlion_guard.mdl" #define MIN_BLAST_DAMAGE 25.0f #define MIN_CRUSH_DAMAGE 20.0f //================================================== // // Antlion Guard // //================================================== #define ANTLIONGUARD_MAX_OBJECTS 128 #define ANTLIONGUARD_MIN_OBJECT_MASS 8 #define ANTLIONGUARD_MAX_OBJECT_MASS 750 #define ANTLIONGUARD_FARTHEST_PHYSICS_OBJECT 350 #define ANTLIONGUARD_OBJECTFINDING_FOV DOT_45DEGREE // 1/sqrt(2) //Melee definitions #define ANTLIONGUARD_MELEE1_RANGE 156.0f #define ANTLIONGUARD_MELEE1_CONE 0.7f // Antlion summoning #define ANTLIONGUARD_SUMMON_COUNT 3 // Sight #define ANTLIONGUARD_FOV_NORMAL -0.4f // cavern guard's poisoning behavior #if HL2_EPISODIC #define ANTLIONGUARD_POISON_TO 12 // we only poison Gordon down to twelve to give him a chance to regen up to 20 by the next charge #endif #define ANTLIONGUARD_CHARGE_MIN 256 #define ANTLIONGUARD_CHARGE_MAX 2048 ConVar sk_antlionguard_health("sk_antlionguard_health", "0"); int g_interactionAntlionGuardFoundPhysicsObject = 0; // We're moving to a physics object to shove it, don't all choose the same object int g_interactionAntlionGuardShovedPhysicsObject = 0; // We've punted an object, it is now clear to be chosen by others //================================================== // AntlionGuardSchedules //================================================== enum { SCHED_ANTLIONGUARD_CHARGE = LAST_SHARED_SCHEDULE, SCHED_ANTLIONGUARD_CHARGE_CRASH, SCHED_ANTLIONGUARD_CHARGE_CANCEL, SCHED_ANTLIONGUARD_PHYSICS_ATTACK, SCHED_ANTLIONGUARD_PHYSICS_DAMAGE_HEAVY, SCHED_ANTLIONGUARD_UNBURROW, SCHED_ANTLIONGUARD_CHARGE_TARGET, SCHED_ANTLIONGUARD_FIND_CHARGE_POSITION, SCHED_ANTLIONGUARD_MELEE_ATTACK1, SCHED_ANTLIONGUARD_SUMMON, SCHED_ANTLIONGUARD_PATROL_RUN, SCHED_ANTLIONGUARD_ROAR, SCHED_ANTLIONGUARD_CHASE_ENEMY_TOLERANCE, SCHED_FORCE_ANTLIONGUARD_PHYSICS_ATTACK, SCHED_ANTLIONGUARD_CANT_ATTACK, SCHED_ANTLIONGUARD_TAKE_COVER_FROM_ENEMY, SCHED_ANTLIONGUARD_CHASE_ENEMY }; //================================================== // AntlionGuardTasks //================================================== enum { TASK_ANTLIONGUARD_CHARGE = LAST_SHARED_TASK, TASK_ANTLIONGUARD_GET_PATH_TO_PHYSOBJECT, TASK_ANTLIONGUARD_SHOVE_PHYSOBJECT, TASK_ANTLIONGUARD_SUMMON, TASK_ANTLIONGUARD_SET_FLINCH_ACTIVITY, TASK_ANTLIONGUARD_GET_PATH_TO_CHARGE_POSITION, TASK_ANTLIONGUARD_GET_PATH_TO_NEAREST_NODE, TASK_ANTLIONGUARD_GET_CHASE_PATH_ENEMY_TOLERANCE, TASK_ANTLIONGUARD_OPPORTUNITY_THROW, TASK_ANTLIONGUARD_FIND_PHYSOBJECT, }; //================================================== // AntlionGuardConditions //================================================== enum { COND_ANTLIONGUARD_PHYSICS_TARGET = LAST_SHARED_CONDITION, COND_ANTLIONGUARD_PHYSICS_TARGET_INVALID, COND_ANTLIONGUARD_HAS_CHARGE_TARGET, COND_ANTLIONGUARD_CAN_SUMMON, COND_ANTLIONGUARD_CAN_CHARGE }; enum { SQUAD_SLOT_ANTLIONGUARD_CHARGE = LAST_SHARED_SQUADSLOT, }; //================================================== // AntlionGuard Activities //================================================== Activity ACT_ANTLIONGUARD_SEARCH; Activity ACT_ANTLIONGUARD_PEEK_FLINCH; Activity ACT_ANTLIONGUARD_PEEK_ENTER; Activity ACT_ANTLIONGUARD_PEEK_EXIT; Activity ACT_ANTLIONGUARD_PEEK1; Activity ACT_ANTLIONGUARD_BARK; Activity ACT_ANTLIONGUARD_PEEK_SIGHTED; Activity ACT_ANTLIONGUARD_SHOVE_PHYSOBJECT; Activity ACT_ANTLIONGUARD_FLINCH_LIGHT; Activity ACT_ANTLIONGUARD_UNBURROW; Activity ACT_ANTLIONGUARD_ROAR; Activity ACT_ANTLIONGUARD_RUN_HURT; // Flinches Activity ACT_ANTLIONGUARD_PHYSHIT_FR; Activity ACT_ANTLIONGUARD_PHYSHIT_FL; Activity ACT_ANTLIONGUARD_PHYSHIT_RR; Activity ACT_ANTLIONGUARD_PHYSHIT_RL; // Charge Activity ACT_ANTLIONGUARD_CHARGE_START; Activity ACT_ANTLIONGUARD_CHARGE_CANCEL; Activity ACT_ANTLIONGUARD_CHARGE_RUN; Activity ACT_ANTLIONGUARD_CHARGE_CRASH; Activity ACT_ANTLIONGUARD_CHARGE_STOP; Activity ACT_ANTLIONGUARD_CHARGE_HIT; Activity ACT_ANTLIONGUARD_CHARGE_ANTICIPATION; // Anim events int AE_ANTLIONGUARD_CHARGE_HIT; int AE_ANTLIONGUARD_SHOVE_PHYSOBJECT; int AE_ANTLIONGUARD_SHOVE; int AE_ANTLIONGUARD_FOOTSTEP_LIGHT; int AE_ANTLIONGUARD_FOOTSTEP_HEAVY; int AE_ANTLIONGUARD_CHARGE_EARLYOUT; int AE_ANTLIONGUARD_VOICE_GROWL; int AE_ANTLIONGUARD_VOICE_BARK; int AE_ANTLIONGUARD_VOICE_PAIN; int AE_ANTLIONGUARD_VOICE_SQUEEZE; int AE_ANTLIONGUARD_VOICE_SCRATCH; int AE_ANTLIONGUARD_VOICE_GRUNT; int AE_ANTLIONGUARD_VOICE_ROAR; int AE_ANTLIONGUARD_BURROW_OUT; struct PhysicsObjectCriteria_t { CBaseEntity *pTarget; Vector vecCenter; // Center point to look around float flRadius; // Radius to search within float flTargetCone; bool bPreferObjectsAlongTargetVector; // Prefer objects that we can strike easily as we move towards our target float flNearRadius; // If we won't hit the player with the object, but get this close, throw anyway }; #define MAX_FAILED_PHYSOBJECTS 8 //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- class CNPC_AntlionGuard : public CAI_BlendedNPC { public: DECLARE_CLASS(CNPC_AntlionGuard, CAI_BlendedNPC); DECLARE_SERVERCLASS(); DECLARE_DATADESC(); CNPC_AntlionGuard(void); Class_T Classify(void) { return CLASS_ANTLION; } virtual int GetSoundInterests(void) { return (SOUND_WORLD | SOUND_COMBAT | SOUND_PLAYER | SOUND_DANGER); } virtual bool QueryHearSound(CSound *pSound); const impactdamagetable_t &GetPhysicsImpactDamageTable(void); virtual int MeleeAttack1Conditions(float flDot, float flDist); virtual int SelectFailSchedule(int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode); virtual int TranslateSchedule(int scheduleType); virtual int OnTakeDamage_Alive(const CTakeDamageInfo &info); virtual void DeathSound(const CTakeDamageInfo &info); virtual void Event_Killed(const CTakeDamageInfo &info); virtual int SelectSchedule(void); virtual float GetAutoAimRadius() { return 36.0f; } virtual void Precache(void); virtual void Spawn(void); virtual void Activate(void); virtual void HandleAnimEvent(animevent_t *pEvent); virtual void UpdateEfficiency(bool bInPVS) { SetEfficiency((GetSleepState() != AISS_AWAKE) ? AIE_DORMANT : AIE_NORMAL); SetMoveEfficiency(AIME_NORMAL); } virtual void PrescheduleThink(void); virtual void GatherConditions(void); virtual void TraceAttack(const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator); virtual void StartTask(const Task_t *pTask); virtual void RunTask(const Task_t *pTask); virtual void StopLoopingSounds(); virtual bool HandleInteraction(int interactionType, void *data, CBaseCombatCharacter *sender); // Input handlers. void InputSetShoveTarget(inputdata_t &inputdata); void InputSetChargeTarget(inputdata_t &inputdata); void InputClearChargeTarget(inputdata_t &inputdata); void InputUnburrow(inputdata_t &inputdata); void InputRagdoll(inputdata_t &inputdata); void InputEnableBark(inputdata_t &inputdata); void InputDisableBark(inputdata_t &inputdata); void InputSummonedAntlionDied(inputdata_t &inputdata); void InputEnablePreferPhysicsAttack(inputdata_t &inputdata); void InputDisablePreferPhysicsAttack(inputdata_t &inputdata); virtual bool IsLightDamage(const CTakeDamageInfo &info); virtual bool IsHeavyDamage(const CTakeDamageInfo &info); virtual bool OverrideMoveFacing(const AILocalMoveGoal_t &move, float flInterval); virtual bool BecomeRagdollOnClient(const Vector &force); virtual void UpdateOnRemove(void); virtual bool IsUnreachable(CBaseEntity* pEntity); // Is entity is unreachable? virtual float MaxYawSpeed(void); virtual bool OverrideMove(float flInterval); virtual bool CanBecomeRagdoll(void); virtual bool ShouldProbeCollideAgainstEntity(CBaseEntity *pEntity); virtual Activity NPC_TranslateActivity(Activity baseAct); #if HL2_EPISODIC //--------------------------------- // Navigation & Movement -- prevent stopping paths for the guard //--------------------------------- class CNavigator : public CAI_ComponentWithOuter { typedef CAI_ComponentWithOuter BaseClass; public: CNavigator(CNPC_AntlionGuard *pOuter) : BaseClass(pOuter) { } bool GetStoppingPath(CAI_WaypointList *pClippedWaypoints); }; CAI_Navigator * CreateNavigator() { return new CNavigator(this); } #endif DEFINE_CUSTOM_AI; private: inline bool CanStandAtPoint(const Vector &vecPos, Vector *pOut); bool RememberFailedPhysicsTarget(CBaseEntity *pTarget); void GetPhysicsShoveDir(CBaseEntity *pObject, float flMass, Vector *pOut); void CreateGlow(CSprite **pSprite, const char *pAttachName); void DestroyGlows(void); void Footstep(bool bHeavy); int SelectCombatSchedule(void); int SelectUnreachableSchedule(void); bool CanSummon(bool bIgnoreTime); void SummonAntlions(void); void ChargeLookAhead(void); bool EnemyIsRightInFrontOfMe(CBaseEntity **pEntity); bool HandleChargeImpact(Vector vecImpact, CBaseEntity *pEntity); bool ShouldCharge(const Vector &startPos, const Vector &endPos, bool useTime, bool bCheckForCancel); bool ShouldWatchEnemy(void); void ImpactShock(const Vector &origin, float radius, float magnitude, CBaseEntity *pIgnored = NULL); void BuildScheduleTestBits(void); void Shove(void); void FoundEnemy(void); void LostEnemy(void); void UpdateHead(void); void UpdatePhysicsTarget(bool bPreferObjectsAlongTargetVector, float flRadius = ANTLIONGUARD_FARTHEST_PHYSICS_OBJECT); void MaintainPhysicsTarget(void); void ChargeDamage(CBaseEntity *pTarget); void StartSounds(void); void SetHeavyDamageAnim(const Vector &vecSource); float ChargeSteer(void); CBaseEntity *FindPhysicsObjectTarget(const PhysicsObjectCriteria_t &criteria); Vector GetPhysicsHitPosition(CBaseEntity *pObject, CBaseEntity *pTarget, Vector *vecTrajectory, float *flClearDistance); bool CanStandAtShoveTarget(CBaseEntity *pShoveObject, CBaseEntity *pTarget, Vector *pOut); CBaseEntity *GetNextShoveTarget(CBaseEntity *pLastEntity, AISightIter_t &iter); int m_nFlinchActivity; bool m_bStopped; bool m_bIsBurrowed; bool m_bBarkEnabled; float m_flNextSummonTime; int m_iNumLiveAntlions; float m_flSearchNoiseTime; float m_flAngerNoiseTime; float m_flBreathTime; float m_flChargeTime; float m_flPhysicsCheckTime; float m_flNextHeavyFlinchTime; float m_flNextRoarTime; int m_iChargeMisses; bool m_bDecidedNotToStop; bool m_bPreferPhysicsAttack; CNetworkVar(bool, m_bCavernBreed); // If this guard is meant to be a cavern dweller (uses different assets) CNetworkVar(bool, m_bInCavern); // Behavioral hint telling the guard to change his behavior Vector m_vecPhysicsTargetStartPos; Vector m_vecPhysicsHitPosition; EHANDLE m_hShoveTarget; EHANDLE m_hChargeTarget; EHANDLE m_hChargeTargetPosition; EHANDLE m_hOldTarget; EHANDLE m_hPhysicsTarget; CUtlVectorFixed m_FailedPhysicsTargets; COutputEvent m_OnSummon; CSoundPatch *m_pGrowlHighSound; CSoundPatch *m_pGrowlLowSound; CSoundPatch *m_pGrowlIdleSound; CSoundPatch *m_pBreathSound; CSoundPatch *m_pConfusedSound; string_t m_iszPhysicsPropClass; string_t m_strShoveTargets; CSprite *m_hCaveGlow[2]; #if ANTLIONGUARD_BLOOD_EFFECTS CNetworkVar(uint8, m_iBleedingLevel); unsigned char GetBleedingLevel(void) const; #endif protected: int m_poseThrow; int m_poseHead_Yaw, m_poseHead_Pitch; virtual void PopulatePoseParameters(void); virtual bool CanBecomeServersideRagdoll(); // inline accessors public: inline bool IsCavernBreed(void) const { return m_bCavernBreed; } inline bool IsInCavern(void) const { return m_bInCavern; } }; //================================================== // CNPC_AntlionGuard::m_DataDesc //================================================== BEGIN_DATADESC(CNPC_AntlionGuard) DEFINE_FIELD(m_nFlinchActivity, FIELD_INTEGER), DEFINE_FIELD(m_bStopped, FIELD_BOOLEAN), DEFINE_KEYFIELD(m_bIsBurrowed, FIELD_BOOLEAN, "startburrowed"), DEFINE_KEYFIELD(m_bBarkEnabled, FIELD_BOOLEAN, "allowbark"), DEFINE_FIELD(m_flNextSummonTime, FIELD_TIME), DEFINE_FIELD(m_iNumLiveAntlions, FIELD_INTEGER), DEFINE_FIELD(m_flSearchNoiseTime, FIELD_TIME), DEFINE_FIELD(m_flAngerNoiseTime, FIELD_TIME), DEFINE_FIELD(m_flBreathTime, FIELD_TIME), DEFINE_FIELD(m_flChargeTime, FIELD_TIME), DEFINE_FIELD(m_hShoveTarget, FIELD_EHANDLE), DEFINE_FIELD(m_hChargeTarget, FIELD_EHANDLE), DEFINE_FIELD(m_hChargeTargetPosition, FIELD_EHANDLE), DEFINE_FIELD(m_hOldTarget, FIELD_EHANDLE), // m_FailedPhysicsTargets // We do not save/load these DEFINE_FIELD(m_hPhysicsTarget, FIELD_EHANDLE), DEFINE_FIELD(m_vecPhysicsTargetStartPos, FIELD_POSITION_VECTOR), DEFINE_FIELD(m_vecPhysicsHitPosition, FIELD_POSITION_VECTOR), DEFINE_FIELD(m_flPhysicsCheckTime, FIELD_TIME), DEFINE_FIELD(m_flNextHeavyFlinchTime, FIELD_TIME), DEFINE_FIELD(m_flNextRoarTime, FIELD_TIME), DEFINE_FIELD(m_iChargeMisses, FIELD_INTEGER), DEFINE_FIELD(m_bDecidedNotToStop, FIELD_BOOLEAN), DEFINE_FIELD(m_bPreferPhysicsAttack, FIELD_BOOLEAN), #if ANTLIONGUARD_BLOOD_EFFECTS DEFINE_FIELD(m_iBleedingLevel, FIELD_CHARACTER), #endif DEFINE_KEYFIELD(m_bCavernBreed, FIELD_BOOLEAN, "cavernbreed"), DEFINE_KEYFIELD(m_bInCavern, FIELD_BOOLEAN, "incavern"), DEFINE_KEYFIELD(m_strShoveTargets, FIELD_STRING, "shovetargets"), DEFINE_AUTO_ARRAY(m_hCaveGlow, FIELD_CLASSPTR), DEFINE_OUTPUT(m_OnSummon, "OnSummon"), DEFINE_SOUNDPATCH(m_pGrowlHighSound), DEFINE_SOUNDPATCH(m_pGrowlLowSound), DEFINE_SOUNDPATCH(m_pGrowlIdleSound), DEFINE_SOUNDPATCH(m_pBreathSound), DEFINE_SOUNDPATCH(m_pConfusedSound), DEFINE_INPUTFUNC(FIELD_STRING, "SetShoveTarget", InputSetShoveTarget), DEFINE_INPUTFUNC(FIELD_STRING, "SetChargeTarget", InputSetChargeTarget), DEFINE_INPUTFUNC(FIELD_VOID, "ClearChargeTarget", InputClearChargeTarget), DEFINE_INPUTFUNC(FIELD_VOID, "Unburrow", InputUnburrow), DEFINE_INPUTFUNC(FIELD_VOID, "Ragdoll", InputRagdoll), DEFINE_INPUTFUNC(FIELD_VOID, "EnableBark", InputEnableBark), DEFINE_INPUTFUNC(FIELD_VOID, "DisableBark", InputDisableBark), DEFINE_INPUTFUNC(FIELD_VOID, "SummonedAntlionDied", InputSummonedAntlionDied), DEFINE_INPUTFUNC(FIELD_VOID, "EnablePreferPhysicsAttack", InputEnablePreferPhysicsAttack), DEFINE_INPUTFUNC(FIELD_VOID, "DisablePreferPhysicsAttack", InputDisablePreferPhysicsAttack), END_DATADESC() //Fast Growl (Growl High) envelopePoint_t envAntlionGuardFastGrowl[] = { { 1.0f, 1.0f, 0.2f, 0.4f, }, { 0.1f, 0.1f, 0.8f, 1.0f, }, { 0.0f, 0.0f, 0.4f, 0.8f, }, }; //Bark 1 (Growl High) envelopePoint_t envAntlionGuardBark1[] = { { 1.0f, 1.0f, 0.1f, 0.2f, }, { 0.0f, 0.0f, 0.4f, 0.6f, }, }; //Bark 2 (Confused) envelopePoint_t envAntlionGuardBark2[] = { { 1.0f, 1.0f, 0.1f, 0.2f, }, { 0.2f, 0.3f, 0.1f, 0.2f, }, { 0.0f, 0.0f, 0.4f, 0.6f, }, }; //Pain envelopePoint_t envAntlionGuardPain1[] = { { 1.0f, 1.0f, 0.1f, 0.2f, }, { -1.0f, -1.0f, 0.5f, 0.8f, }, { 0.1f, 0.2f, 0.1f, 0.2f, }, { 0.0f, 0.0f, 0.5f, 0.75f, }, }; //Squeeze (High Growl) envelopePoint_t envAntlionGuardSqueeze[] = { { 1.0f, 1.0f, 0.1f, 0.2f, }, { 0.0f, 0.0f, 1.0f, 1.5f, }, }; //Scratch (Low Growl) envelopePoint_t envAntlionGuardScratch[] = { { 1.0f, 1.0f, 0.4f, 0.6f, }, { 0.5f, 0.5f, 0.4f, 0.6f, }, { 0.0f, 0.0f, 1.0f, 1.5f, }, }; //Grunt envelopePoint_t envAntlionGuardGrunt[] = { { 0.6f, 1.0f, 0.1f, 0.2f, }, { 0.0f, 0.0f, 0.1f, 0.2f, }, }; envelopePoint_t envAntlionGuardGrunt2[] = { { 0.2f, 0.4f, 0.1f, 0.2f, }, { 0.0f, 0.0f, 0.4f, 0.6f, }, }; //============================================================================================== // ANTLION GUARD PHYSICS DAMAGE TABLE //============================================================================================== static impactentry_t antlionGuardLinearTable[] = { { 100 * 100, 10 }, { 250 * 250, 25 }, { 350 * 350, 50 }, { 500 * 500, 75 }, { 1000 * 1000, 100 }, }; static impactentry_t antlionGuardAngularTable[] = { { 50 * 50, 10 }, { 100 * 100, 25 }, { 150 * 150, 50 }, { 200 * 200, 75 }, }; impactdamagetable_t gAntlionGuardImpactDamageTable = { antlionGuardLinearTable, antlionGuardAngularTable, ARRAYSIZE(antlionGuardLinearTable), ARRAYSIZE(antlionGuardAngularTable), 200 * 200,// minimum linear speed squared 180 * 180,// minimum angular speed squared (360 deg/s to cause spin/slice damage) 15, // can't take damage from anything under 15kg 10, // anything less than 10kg is "small" 5, // never take more than 1 pt of damage from anything under 15kg 128 * 128,// <15kg objects must go faster than 36 in/s to do damage 45, // large mass in kg 2, // large mass scale (anything over 500kg does 4X as much energy to read from damage table) 1, // large mass falling scale 0, // my min velocity }; //----------------------------------------------------------------------------- // Purpose: // Output : const impactdamagetable_t //----------------------------------------------------------------------------- const impactdamagetable_t &CNPC_AntlionGuard::GetPhysicsImpactDamageTable(void) { return gAntlionGuardImpactDamageTable; } //================================================== // CNPC_AntlionGuard //================================================== CNPC_AntlionGuard::CNPC_AntlionGuard(void) { m_bCavernBreed = false; m_bInCavern = false; m_iszPhysicsPropClass = AllocPooledString("prop_physics"); } LINK_ENTITY_TO_CLASS(npc_antlionguard, CNPC_AntlionGuard); IMPLEMENT_SERVERCLASS_ST(CNPC_AntlionGuard, DT_NPC_AntlionGuard) SendPropBool(SENDINFO(m_bCavernBreed)), SendPropBool(SENDINFO(m_bInCavern)), #if ANTLIONGUARD_BLOOD_EFFECTS SendPropInt(SENDINFO(m_iBleedingLevel), 2, SPROP_UNSIGNED), #endif END_SEND_TABLE() //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::UpdateOnRemove(void) { DestroyGlows(); // Chain to the base class BaseClass::UpdateOnRemove(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::Precache(void) { PrecacheModel(ANTLIONGUARD_MODEL); PrecacheScriptSound("NPC_AntlionGuard.Shove"); PrecacheScriptSound("NPC_AntlionGuard.HitHard"); if (HasSpawnFlags(SF_ANTLIONGUARD_INSIDE_FOOTSTEPS)) { PrecacheScriptSound("NPC_AntlionGuard.Inside.StepLight"); PrecacheScriptSound("NPC_AntlionGuard.Inside.StepHeavy"); } else { PrecacheScriptSound("NPC_AntlionGuard.StepLight"); PrecacheScriptSound("NPC_AntlionGuard.StepHeavy"); } #if HL2_EPISODIC PrecacheScriptSound("NPC_AntlionGuard.NearStepLight"); PrecacheScriptSound("NPC_AntlionGuard.NearStepHeavy"); PrecacheScriptSound("NPC_AntlionGuard.FarStepLight"); PrecacheScriptSound("NPC_AntlionGuard.FarStepHeavy"); PrecacheScriptSound("NPC_AntlionGuard.BreatheLoop"); PrecacheScriptSound("NPC_AntlionGuard.ShellCrack"); PrecacheScriptSound("NPC_AntlionGuard.Pain_Roar"); PrecacheModel("sprites/grubflare1.vmt"); #endif // HL2_EPISODIC PrecacheScriptSound("NPC_AntlionGuard.Anger"); PrecacheScriptSound("NPC_AntlionGuard.Roar"); PrecacheScriptSound("NPC_AntlionGuard.Die"); PrecacheScriptSound("NPC_AntlionGuard.GrowlHigh"); PrecacheScriptSound("NPC_AntlionGuard.GrowlIdle"); PrecacheScriptSound("NPC_AntlionGuard.BreathSound"); PrecacheScriptSound("NPC_AntlionGuard.Confused"); PrecacheScriptSound("NPC_AntlionGuard.Fallover"); PrecacheScriptSound("NPC_AntlionGuard.FrustratedRoar"); PrecacheParticleSystem("blood_antlionguard_injured_light"); PrecacheParticleSystem("blood_antlionguard_injured_heavy"); BaseClass::Precache(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::DestroyGlows(void) { if (m_hCaveGlow[0]) { UTIL_Remove(m_hCaveGlow[0]); // reset it to NULL in case there is a double death cleanup for some reason. m_hCaveGlow[0] = NULL; } if (m_hCaveGlow[1]) { UTIL_Remove(m_hCaveGlow[1]); // reset it to NULL in case there is a double death cleanup for some reason. m_hCaveGlow[1] = NULL; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::CreateGlow(CSprite **pSprite, const char *pAttachName) { if (pSprite == NULL) return; // Create the glow sprite *pSprite = CSprite::SpriteCreate("sprites/grubflare1.vmt", GetLocalOrigin(), false); Assert(*pSprite); if (*pSprite == NULL) return; (*pSprite)->TurnOn(); (*pSprite)->SetTransparency(kRenderWorldGlow, 156, 169, 121, 164, kRenderFxNoDissipation); (*pSprite)->SetScale(1.0f); (*pSprite)->SetGlowProxySize(16.0f); int nAttachment = LookupAttachment(pAttachName); (*pSprite)->SetParent(this, nAttachment); (*pSprite)->SetLocalOrigin(vec3_origin); // Don't uselessly animate, we're a static sprite! (*pSprite)->SetThink(NULL); (*pSprite)->SetNextThink(TICK_NEVER_THINK); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::Spawn(void) { Precache(); SetModel(ANTLIONGUARD_MODEL); // Switch our skin (for now), if we're the cavern guard if (m_bCavernBreed) { m_nSkin = 1; // Add glows CreateGlow(&(m_hCaveGlow[0]), "attach_glow1"); CreateGlow(&(m_hCaveGlow[1]), "attach_glow2"); } else { m_hCaveGlow[0] = NULL; m_hCaveGlow[1] = NULL; } SetHullType(HULL_LARGE); SetHullSizeNormal(); SetDefaultEyeOffset(); SetSolid(SOLID_BBOX); AddSolidFlags(FSOLID_NOT_STANDABLE); SetMoveType(MOVETYPE_STEP); SetNavType(NAV_GROUND); SetBloodColor(BLOOD_COLOR_YELLOW); m_iHealth = sk_antlionguard_health.GetFloat(); m_iMaxHealth = m_iHealth; m_flFieldOfView = ANTLIONGUARD_FOV_NORMAL; m_flPhysicsCheckTime = 0; m_flChargeTime = 0; m_flNextRoarTime = 0; m_flNextSummonTime = 0; m_iNumLiveAntlions = 0; m_iChargeMisses = 0; m_flNextHeavyFlinchTime = 0; m_bDecidedNotToStop = false; ClearHintGroup(); m_bStopped = false; m_hShoveTarget = NULL; m_hChargeTarget = NULL; m_hChargeTargetPosition = NULL; m_hPhysicsTarget = NULL; m_HackedGunPos.x = 10; m_HackedGunPos.y = 0; m_HackedGunPos.z = 30; CapabilitiesClear(); CapabilitiesAdd(bits_CAP_MOVE_GROUND | bits_CAP_INNATE_MELEE_ATTACK1 | bits_CAP_SQUAD); CapabilitiesAdd(bits_CAP_SKIP_NAV_GROUND_CHECK); NPCInit(); BaseClass::Spawn(); //See if we're supposed to start burrowed if (m_bIsBurrowed) { AddEffects(EF_NODRAW); AddFlag(FL_NOTARGET); m_spawnflags |= SF_NPC_GAG; AddSolidFlags(FSOLID_NOT_SOLID); m_takedamage = DAMAGE_NO; if (m_hCaveGlow[0]) m_hCaveGlow[0]->TurnOff(); if (m_hCaveGlow[1]) m_hCaveGlow[1]->TurnOff(); } // Do not dissolve AddEFlags(EFL_NO_DISSOLVE); // We get a minute of free knowledge about the target GetEnemies()->SetEnemyDiscardTime(120.0f); GetEnemies()->SetFreeKnowledgeDuration(60.0f); // We need to bloat the absbox to encompass all the hitboxes Vector absMin = -Vector(100, 100, 0); Vector absMax = Vector(100, 100, 128); CollisionProp()->SetSurroundingBoundsType(USE_SPECIFIED_BOUNDS, &absMin, &absMax); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::Activate(void) { BaseClass::Activate(); // Find all nearby physics objects and add them to the list of objects we will sense CBaseEntity *pObject = NULL; while ((pObject = gEntList.FindEntityInSphere(pObject, WorldSpaceCenter(), 2500)) != NULL) { // Can't throw around debris if (pObject->GetCollisionGroup() == COLLISION_GROUP_DEBRIS) continue; // We can only throw a few types of things if (!FClassnameIs(pObject, "prop_physics") && !FClassnameIs(pObject, "func_physbox")) continue; // Ensure it's mass is within range IPhysicsObject *pPhysObj = pObject->VPhysicsGetObject(); if ((pPhysObj == NULL) || (pPhysObj->GetMass() > ANTLIONGUARD_MAX_OBJECT_MASS) || (pPhysObj->GetMass() < ANTLIONGUARD_MIN_OBJECT_MASS)) continue; // Tell the AI sensing list that we want to consider this g_AI_SensedObjectsManager.AddEntity(pObject); if (g_debug_antlionguard.GetInt() == 5) { Msg("Antlion Guard: Added prop with model '%s' to sense list.\n", STRING(pObject->GetModelName())); pObject->m_debugOverlays |= OVERLAY_BBOX_BIT; } } } //----------------------------------------------------------------------------- // Purpose: Return true if the guard's allowed to summon antlions //----------------------------------------------------------------------------- bool CNPC_AntlionGuard::CanSummon(bool bIgnoreTime) { if (!m_bBarkEnabled) return false; if (!bIgnoreTime && m_flNextSummonTime > gpGlobals->curtime) return false; // Hit the max number of them allowed? Only summon when we're 2 down. if (m_iNumLiveAntlions >= MAX(1, ANTLIONGUARD_SUMMON_COUNT - 1)) return false; return true; } //----------------------------------------------------------------------------- // Purpose: Our enemy is unreachable. Select a schedule. //----------------------------------------------------------------------------- int CNPC_AntlionGuard::SelectUnreachableSchedule(void) { // If we're in the cavern setting, we opt out of this if (m_bInCavern) return SCHED_ANTLIONGUARD_CHASE_ENEMY_TOLERANCE; // Summon antlions if we can if (HasCondition(COND_ANTLIONGUARD_CAN_SUMMON)) return SCHED_ANTLIONGUARD_SUMMON; // First, look for objects near ourselves PhysicsObjectCriteria_t criteria; criteria.bPreferObjectsAlongTargetVector = false; criteria.flNearRadius = (40 * 12); criteria.flRadius = (250 * 12); criteria.flTargetCone = -1.0f; criteria.pTarget = GetEnemy(); criteria.vecCenter = GetAbsOrigin(); CBaseEntity *pTarget = FindPhysicsObjectTarget(criteria); if (pTarget == NULL && GetEnemy()) { // Use the same criteria, but search near the target instead of us criteria.vecCenter = GetEnemy()->GetAbsOrigin(); pTarget = FindPhysicsObjectTarget(criteria); } // If we found one, we'll want to attack it if (pTarget) { m_hPhysicsTarget = pTarget; SetCondition(COND_ANTLIONGUARD_PHYSICS_TARGET); m_vecPhysicsTargetStartPos = m_hPhysicsTarget->WorldSpaceCenter(); // Tell any squadmates I'm going for this item so they don't as well if (GetSquad() != NULL) { GetSquad()->BroadcastInteraction(g_interactionAntlionGuardFoundPhysicsObject, (void *)((CBaseEntity *)m_hPhysicsTarget), this); } return SCHED_ANTLIONGUARD_PHYSICS_ATTACK; } // Deal with a visible player if (HasCondition(COND_SEE_ENEMY)) { // Roar at the player as show of frustration if (m_flNextRoarTime < gpGlobals->curtime) { m_flNextRoarTime = gpGlobals->curtime + RandomFloat(20, 40); return SCHED_ANTLIONGUARD_ROAR; } // If we're under attack, then let's leave for a bit if (GetEnemy() && HasCondition(COND_HEAVY_DAMAGE)) { Vector dir = GetEnemy()->GetAbsOrigin() - GetAbsOrigin(); VectorNormalize(dir); GetMotor()->SetIdealYaw(-dir); return SCHED_ANTLIONGUARD_TAKE_COVER_FROM_ENEMY; } } // If we're too far away, try to close distance to our target first float flDistToEnemySqr = (GetEnemy()->GetAbsOrigin() - GetAbsOrigin()).LengthSqr(); if (flDistToEnemySqr > Square(100 * 12)) return SCHED_ANTLIONGUARD_CHASE_ENEMY_TOLERANCE; // Fire that we're unable to reach our target! if (GetEnemy() && GetEnemy()->IsPlayer()) { m_OnLostPlayer.FireOutput(this, this); } m_OnLostEnemy.FireOutput(this, this); GetEnemies()->MarkAsEluded(GetEnemy()); // Move randomly for the moment return SCHED_ANTLIONGUARD_CANT_ATTACK; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CNPC_AntlionGuard::SelectCombatSchedule(void) { ClearHintGroup(); /* bool bCanCharge = false; if ( HasCondition( COND_SEE_ENEMY ) ) { bCanCharge = ShouldCharge( GetAbsOrigin(), GetEnemy()->GetAbsOrigin(), true, false ); } */ // Attack if we can if (HasCondition(COND_CAN_MELEE_ATTACK1)) return SCHED_MELEE_ATTACK1; // Otherwise, summon antlions if (HasCondition(COND_ANTLIONGUARD_CAN_SUMMON)) { // If I can charge, and have antlions, charge instead if (HasCondition(COND_ANTLIONGUARD_CAN_CHARGE) && m_iNumLiveAntlions) return SCHED_ANTLIONGUARD_CHARGE; return SCHED_ANTLIONGUARD_SUMMON; } // See if we can bark if (HasCondition(COND_ENEMY_UNREACHABLE)) return SelectUnreachableSchedule(); //Physics target if (HasCondition(COND_ANTLIONGUARD_PHYSICS_TARGET) && !m_bInCavern) return SCHED_ANTLIONGUARD_PHYSICS_ATTACK; // If we've missed a couple of times, and we can summon, make it harder if (m_iChargeMisses >= 2 && CanSummon(true)) { m_iChargeMisses--; return SCHED_ANTLIONGUARD_SUMMON; } // Charging if (HasCondition(COND_ANTLIONGUARD_CAN_CHARGE)) { // Don't let other squad members charge while we're doing it OccupyStrategySlot(SQUAD_SLOT_ANTLIONGUARD_CHARGE); return SCHED_ANTLIONGUARD_CHARGE; } return BaseClass::SelectSchedule(); } //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CNPC_AntlionGuard::ShouldCharge(const Vector &startPos, const Vector &endPos, bool useTime, bool bCheckForCancel) { // Don't charge in tight spaces unless forced to if (hl2_episodic.GetBool() && m_bInCavern && !(m_hChargeTarget.Get() && m_hChargeTarget->IsAlive())) return false; // Must have a target if (!GetEnemy()) return false; // No one else in the squad can be charging already if (IsStrategySlotRangeOccupied(SQUAD_SLOT_ANTLIONGUARD_CHARGE, SQUAD_SLOT_ANTLIONGUARD_CHARGE)) return false; // Don't check the distance once we start charging if (!bCheckForCancel) { // Don't allow use to charge again if it's been too soon if (useTime && (m_flChargeTime > gpGlobals->curtime)) return false; float distance = UTIL_DistApprox2D(startPos, endPos); // Must be within our tolerance range if ((distance < ANTLIONGUARD_CHARGE_MIN) || (distance > ANTLIONGUARD_CHARGE_MAX)) return false; } if (GetSquad()) { // If someone in our squad is closer to the enemy, then don't charge (we end up hitting them more often than not!) float flOurDistToEnemySqr = (GetAbsOrigin() - GetEnemy()->GetAbsOrigin()).LengthSqr(); AISquadIter_t iter; for (CAI_BaseNPC *pSquadMember = GetSquad()->GetFirstMember(&iter); pSquadMember; pSquadMember = GetSquad()->GetNextMember(&iter)) { if (pSquadMember->IsAlive() == false || pSquadMember == this) continue; if ((pSquadMember->GetAbsOrigin() - GetEnemy()->GetAbsOrigin()).LengthSqr() < flOurDistToEnemySqr) return false; } } //FIXME: We'd like to exclude small physics objects from this check! // We only need to hit the endpos with the edge of our bounding box Vector vecDir = endPos - startPos; VectorNormalize(vecDir); float flWidth = WorldAlignSize().x * 0.5; Vector vecTargetPos = endPos - (vecDir * flWidth); // See if we can directly move there AIMoveTrace_t moveTrace; GetMoveProbe()->MoveLimit(NAV_GROUND, startPos, vecTargetPos, MASK_NPCSOLID_BRUSHONLY, GetEnemy(), &moveTrace); // Draw the probe if (g_debug_antlionguard.GetInt() == 1) { Vector enemyDir = (vecTargetPos - startPos); float enemyDist = VectorNormalize(enemyDir); NDebugOverlay::BoxDirection(startPos, GetHullMins(), GetHullMaxs() + Vector(enemyDist, 0, 0), enemyDir, 0, 255, 0, 8, 1.0f); } // If we're not blocked, charge if (IsMoveBlocked(moveTrace)) { // Don't allow it if it's too close to us if (UTIL_DistApprox(WorldSpaceCenter(), moveTrace.vEndPosition) < ANTLIONGUARD_CHARGE_MIN) return false; // Allow some special cases to not block us if (moveTrace.pObstruction != NULL) { // If we've hit the world, see if it's a cliff if (moveTrace.pObstruction == GetContainingEntity(INDEXENT(0))) { // Can't be too far above/below the target if (fabs(moveTrace.vEndPosition.z - vecTargetPos.z) > StepHeight()) return false; // Allow it if we got pretty close if (UTIL_DistApprox(moveTrace.vEndPosition, vecTargetPos) < 64) return true; } // Hit things that will take damage if (moveTrace.pObstruction->m_takedamage != DAMAGE_NO) return true; // Hit things that will move if (moveTrace.pObstruction->GetMoveType() == MOVETYPE_VPHYSICS) return true; } return false; } // Only update this if we've requested it if (useTime) { m_flChargeTime = gpGlobals->curtime + 4.0f; } return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CNPC_AntlionGuard::SelectSchedule(void) { // Don't do anything if we're burrowed if (m_bIsBurrowed) return SCHED_IDLE_STAND; #if 0 // Debug physics object finding if (0) //g_debug_antlionguard.GetInt() == 3 ) { m_flPhysicsCheckTime = 0; UpdatePhysicsTarget(false); return SCHED_IDLE_STAND; } #endif // Flinch on heavy damage, but not if we've flinched too recently. // This is done to prevent stun-locks from grenades. if (!m_bInCavern && HasCondition(COND_HEAVY_DAMAGE) && m_flNextHeavyFlinchTime < gpGlobals->curtime) { m_flNextHeavyFlinchTime = gpGlobals->curtime + 8.0f; return SCHED_ANTLIONGUARD_PHYSICS_DAMAGE_HEAVY; } // Prefer to use physics, in this case if (m_bPreferPhysicsAttack) { // If we have a target, try to go for it if (HasCondition(COND_ANTLIONGUARD_PHYSICS_TARGET)) return SCHED_ANTLIONGUARD_PHYSICS_ATTACK; } // Charge after a target if it's set if (m_hChargeTarget && m_hChargeTargetPosition) { ClearCondition(COND_ANTLIONGUARD_HAS_CHARGE_TARGET); ClearHintGroup(); if (m_hChargeTarget->IsAlive() == false) { m_hChargeTarget = NULL; m_hChargeTargetPosition = NULL; SetEnemy(m_hOldTarget); if (m_hOldTarget == NULL) { m_NPCState = NPC_STATE_ALERT; } } else { m_hOldTarget = GetEnemy(); SetEnemy(m_hChargeTarget); UpdateEnemyMemory(m_hChargeTarget, m_hChargeTarget->GetAbsOrigin()); //If we can't see the target, run to somewhere we can if (ShouldCharge(GetAbsOrigin(), GetEnemy()->GetAbsOrigin(), false, false) == false) return SCHED_ANTLIONGUARD_FIND_CHARGE_POSITION; return SCHED_ANTLIONGUARD_CHARGE_TARGET; } } // See if we need to clear a path to our enemy if (HasCondition(COND_ENEMY_OCCLUDED) || HasCondition(COND_ENEMY_UNREACHABLE)) { CBaseEntity *pBlocker = GetEnemyOccluder(); if ((pBlocker != NULL) && FClassnameIs(pBlocker, "prop_physics") && !m_bInCavern) { m_hPhysicsTarget = pBlocker; return SCHED_ANTLIONGUARD_PHYSICS_ATTACK; } } //Only do these in combat states if (m_NPCState == NPC_STATE_COMBAT && GetEnemy()) return SelectCombatSchedule(); return BaseClass::SelectSchedule(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CNPC_AntlionGuard::MeleeAttack1Conditions(float flDot, float flDist) { // Don't attack again too soon if (GetNextAttack() > gpGlobals->curtime) return 0; // While charging, we can't melee attack if (IsCurSchedule(SCHED_ANTLIONGUARD_CHARGE)) return 0; if (hl2_episodic.GetBool() && m_bInCavern) { // Predict where they'll be and see if THAT is within range Vector vecPredPos; UTIL_PredictedPosition(GetEnemy(), 0.25f, &vecPredPos); if ((GetAbsOrigin() - vecPredPos).Length() > ANTLIONGUARD_MELEE1_RANGE) return COND_TOO_FAR_TO_ATTACK; } else { // Must be close enough if (flDist > ANTLIONGUARD_MELEE1_RANGE) return COND_TOO_FAR_TO_ATTACK; } // Must be within a viable cone if (flDot < ANTLIONGUARD_MELEE1_CONE) return COND_NOT_FACING_ATTACK; // If the enemy is on top of me, I'm allowed to hit the sucker if (GetEnemy()->GetGroundEntity() == this) return COND_CAN_MELEE_ATTACK1; trace_t tr; TraceHull_SkipPhysics(WorldSpaceCenter(), GetEnemy()->WorldSpaceCenter(), Vector(-10, -10, -10), Vector(10, 10, 10), MASK_SHOT_HULL, this, COLLISION_GROUP_NONE, &tr, VPhysicsGetObject()->GetMass() * 0.5); // If we hit anything, go for it if (tr.fraction < 1.0f) return COND_CAN_MELEE_ATTACK1; return 0; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- float CNPC_AntlionGuard::MaxYawSpeed(void) { Activity eActivity = GetActivity(); // Stay still if ((eActivity == ACT_ANTLIONGUARD_SEARCH) || (eActivity == ACT_ANTLIONGUARD_PEEK_ENTER) || (eActivity == ACT_ANTLIONGUARD_PEEK_EXIT) || (eActivity == ACT_ANTLIONGUARD_PEEK1) || (eActivity == ACT_ANTLIONGUARD_BARK) || (eActivity == ACT_ANTLIONGUARD_PEEK_SIGHTED) || (eActivity == ACT_MELEE_ATTACK1)) return 0.0f; CBaseEntity *pEnemy = GetEnemy(); if (pEnemy != NULL && pEnemy->IsPlayer() == false) return 16.0f; // Turn slowly when you're charging if (eActivity == ACT_ANTLIONGUARD_CHARGE_START) return 4.0f; if (hl2_episodic.GetBool() && m_bInCavern) { // Allow a better turning rate when moving quickly but not charging the player if ((eActivity == ACT_ANTLIONGUARD_CHARGE_RUN) && IsCurSchedule(SCHED_ANTLIONGUARD_CHARGE) == false) return 16.0f; } // Turn more slowly as we close in on our target if (eActivity == ACT_ANTLIONGUARD_CHARGE_RUN) { if (pEnemy == NULL) return 2.0f; float dist = UTIL_DistApprox2D(GetEnemy()->WorldSpaceCenter(), WorldSpaceCenter()); if (dist > 512) return 16.0f; //FIXME: Alter by skill level float yawSpeed = RemapVal(dist, 0, 512, 1.0f, 2.0f); yawSpeed = clamp(yawSpeed, 1.0f, 2.0f); return yawSpeed; } if (eActivity == ACT_ANTLIONGUARD_CHARGE_STOP) return 8.0f; switch (eActivity) { case ACT_TURN_LEFT: case ACT_TURN_RIGHT: return 40.0f; break; case ACT_RUN: default: return 20.0f; break; } } //----------------------------------------------------------------------------- // Purpose: // Input : flInterval - // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CNPC_AntlionGuard::OverrideMove(float flInterval) { // If the guard's charging, we're handling the movement if (IsCurSchedule(SCHED_ANTLIONGUARD_CHARGE)) return true; // TODO: This code increases the guard's ability to successfully hit a target, but adds a new dimension of navigation // trouble to do with him not being able to "close the distance" between himself and the object he wants to hit. // Fixing this will require some thought on how he picks the correct distances to his targets and when he's "close enough". -- jdw /* if ( m_hPhysicsTarget != NULL ) { float flWidth = m_hPhysicsTarget->CollisionProp()->BoundingRadius2D(); GetLocalNavigator()->AddObstacle( m_hPhysicsTarget->WorldSpaceCenter(), flWidth * 0.75f, AIMST_AVOID_OBJECT ); //NDebugOverlay::Sphere( m_hPhysicsTarget->WorldSpaceCenter(), vec3_angle, flWidth, 255, 255, 255, 0, true, 0.5f ); } */ return BaseClass::OverrideMove(flInterval); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::Shove(void) { if (GetNextAttack() > gpGlobals->curtime) return; CBaseEntity *pHurt = NULL; CBaseEntity *pTarget; pTarget = (m_hShoveTarget == NULL) ? GetEnemy() : m_hShoveTarget.Get(); if (pTarget == NULL) { m_hShoveTarget = NULL; return; } //Always damage bullseyes if we're scripted to hit them if (pTarget->Classify() == CLASS_BULLSEYE) { pTarget->TakeDamage(CTakeDamageInfo(this, this, 1.0f, DMG_CRUSH)); m_hShoveTarget = NULL; return; } float damage = (pTarget->IsPlayer()) ? sk_antlionguard_dmg_shove.GetFloat() : 250; // If the target's still inside the shove cone, ensure we hit him Vector vecForward, vecEnd; AngleVectors(GetAbsAngles(), &vecForward); float flDistSqr = (pTarget->WorldSpaceCenter() - WorldSpaceCenter()).LengthSqr(); Vector2D v2LOS = (pTarget->WorldSpaceCenter() - WorldSpaceCenter()).AsVector2D(); Vector2DNormalize(v2LOS); float flDot = DotProduct2D(v2LOS, vecForward.AsVector2D()); if (flDistSqr < (ANTLIONGUARD_MELEE1_RANGE*ANTLIONGUARD_MELEE1_RANGE) && flDot >= ANTLIONGUARD_MELEE1_CONE) { vecEnd = pTarget->WorldSpaceCenter(); } else { vecEnd = WorldSpaceCenter() + (BodyDirection3D() * ANTLIONGUARD_MELEE1_RANGE); } // Use the melee trace to ensure we hit everything there trace_t tr; CTakeDamageInfo dmgInfo(this, this, damage, DMG_SLASH); CTraceFilterMelee traceFilter(this, COLLISION_GROUP_NONE, &dmgInfo, 1.0, true); Ray_t ray; ray.Init(WorldSpaceCenter(), vecEnd, Vector(-16, -16, -16), Vector(16, 16, 16)); enginetrace->TraceRay(ray, MASK_SHOT_HULL, &traceFilter, &tr); pHurt = tr.m_pEnt; // Knock things around ImpactShock(tr.endpos, 100.0f, 250.0f); if (pHurt) { Vector traceDir = (tr.endpos - tr.startpos); VectorNormalize(traceDir); // Generate enough force to make a 75kg guy move away at 600 in/sec Vector vecForce = traceDir * ImpulseScale(75, 600); CTakeDamageInfo info(this, this, vecForce, tr.endpos, damage, DMG_CLUB); pHurt->TakeDamage(info); m_hShoveTarget = NULL; EmitSound("NPC_AntlionGuard.Shove"); // If the player, throw him around if (pHurt->IsPlayer()) { //Punch the view pHurt->ViewPunch(QAngle(20, 0, -20)); //Shake the screen UTIL_ScreenShake(pHurt->GetAbsOrigin(), 100.0, 1.5, 1.0, 2, SHAKE_START); //Red damage indicator color32 red = { 128, 0, 0, 128 }; UTIL_ScreenFade(pHurt, red, 1.0f, 0.1f, FFADE_IN); Vector forward, up; AngleVectors(GetLocalAngles(), &forward, NULL, &up); pHurt->ApplyAbsVelocityImpulse(forward * 400 + up * 150); // in the episodes, the cavern guard poisons the player #if HL2_EPISODIC // If I am a cavern guard attacking the player, and he still lives, then poison him too. if (m_bInCavern && pHurt->IsPlayer() && pHurt->IsAlive() && pHurt->m_iHealth > ANTLIONGUARD_POISON_TO) { // That didn't finish them. Take them down to one point with poison damage. It'll heal. pHurt->TakeDamage(CTakeDamageInfo(this, this, pHurt->m_iHealth - ANTLIONGUARD_POISON_TO, DMG_POISON)); } #endif } else { CBaseCombatCharacter *pVictim = ToBaseCombatCharacter(pHurt); if (pVictim) { if (NPC_Rollermine_IsRollermine(pVictim)) { Pickup_OnPhysGunDrop(pVictim, NULL, LAUNCHED_BY_CANNON); } // FIXME: This causes NPCs that are not physically motivated to hop into the air strangely -- jdw // pVictim->ApplyAbsVelocityImpulse( BodyDirection2D() * 400 + Vector( 0, 0, 250 ) ); } m_hShoveTarget = NULL; } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- class CTraceFilterCharge : public CTraceFilterEntitiesOnly { public: // It does have a base, but we'll never network anything below here.. DECLARE_CLASS_NOBASE(CTraceFilterCharge); CTraceFilterCharge(const IHandleEntity *passentity, int collisionGroup, CNPC_AntlionGuard *pAttacker) : m_pPassEnt(passentity), m_collisionGroup(collisionGroup), m_pAttacker(pAttacker) { } virtual bool ShouldHitEntity(IHandleEntity *pHandleEntity, int contentsMask) { if (!StandardFilterRules(pHandleEntity, contentsMask)) return false; if (!PassServerEntityFilter(pHandleEntity, m_pPassEnt)) return false; // Don't test if the game code tells us we should ignore this collision... CBaseEntity *pEntity = EntityFromEntityHandle(pHandleEntity); if (pEntity) { if (!pEntity->ShouldCollide(m_collisionGroup, contentsMask)) return false; if (!g_pGameRules->ShouldCollide(m_collisionGroup, pEntity->GetCollisionGroup())) return false; if (pEntity->m_takedamage == DAMAGE_NO) return false; // Translate the vehicle into its driver for damage if (pEntity->GetServerVehicle() != NULL) { CBaseEntity *pDriver = pEntity->GetServerVehicle()->GetPassenger(); if (pDriver != NULL) { pEntity = pDriver; } } Vector attackDir = pEntity->WorldSpaceCenter() - m_pAttacker->WorldSpaceCenter(); VectorNormalize(attackDir); float flDamage = (pEntity->IsPlayer()) ? sk_antlionguard_dmg_shove.GetFloat() : 250;; CTakeDamageInfo info(m_pAttacker, m_pAttacker, flDamage, DMG_CRUSH); CalculateMeleeDamageForce(&info, attackDir, info.GetAttacker()->WorldSpaceCenter(), 4.0f); CBaseCombatCharacter *pVictimBCC = pEntity->MyCombatCharacterPointer(); // Only do these comparisons between NPCs if (pVictimBCC) { // Can only damage other NPCs that we hate if (m_pAttacker->IRelationType(pEntity) == D_HT) { pEntity->TakeDamage(info); return true; } } else { // Otherwise just damage passive objects in our way pEntity->TakeDamage(info); Pickup_ForcePlayerToDropThisObject(pEntity); } } return false; } public: const IHandleEntity *m_pPassEnt; int m_collisionGroup; CNPC_AntlionGuard *m_pAttacker; }; #define MIN_FOOTSTEP_NEAR_DIST Square( 80*12.0f )// ft //----------------------------------------------------------------------------- // Purpose: Plays a footstep sound with temporary distance fades // Input : bHeavy - Larger back hoof is considered a "heavy" step //----------------------------------------------------------------------------- void CNPC_AntlionGuard::Footstep(bool bHeavy) { CBasePlayer *pPlayer = AI_GetSinglePlayer(); Assert(pPlayer != NULL); if (pPlayer == NULL) return; float flDistanceToPlayerSqr = (pPlayer->GetAbsOrigin() - GetAbsOrigin()).LengthSqr(); float flNearVolume = RemapValClamped(flDistanceToPlayerSqr, Square(10 * 12.0f), MIN_FOOTSTEP_NEAR_DIST, VOL_NORM, 0.0f); EmitSound_t soundParams; CPASAttenuationFilter filter(this); if (bHeavy) { if (flNearVolume > 0.0f) { soundParams.m_pSoundName = "NPC_AntlionGuard.NearStepHeavy"; soundParams.m_flVolume = flNearVolume; soundParams.m_nFlags = SND_CHANGE_VOL; EmitSound(filter, entindex(), soundParams); } EmitSound("NPC_AntlionGuard.FarStepHeavy"); } else { if (flNearVolume > 0.0f) { soundParams.m_pSoundName = "NPC_AntlionGuard.NearStepLight"; soundParams.m_flVolume = flNearVolume; soundParams.m_nFlags = SND_CHANGE_VOL; EmitSound(filter, entindex(), soundParams); } EmitSound("NPC_AntlionGuard.FarStepLight"); } } float GetCurrentGravity(void); //----------------------------------------------------------------------------- // Purpose: // Input : *pObject - // *pOut - //----------------------------------------------------------------------------- void CNPC_AntlionGuard::GetPhysicsShoveDir(CBaseEntity *pObject, float flMass, Vector *pOut) { const Vector vecStart = pObject->WorldSpaceCenter(); const Vector vecTarget = GetEnemy()->WorldSpaceCenter(); const float flBaseSpeed = 800.0f; float flSpeed = RemapValClamped(flMass, 0.0f, 150.0f, flBaseSpeed * 2.0f, flBaseSpeed); // Try the most direct route Vector vecToss = VecCheckThrow(this, vecStart, vecTarget, flSpeed, 1.0f); // If this failed then try a little faster (flattens the arc) if (vecToss == vec3_origin) { vecToss = VecCheckThrow(this, vecStart, vecTarget, flSpeed * 1.25f, 1.0f); if (vecToss == vec3_origin) { const float flGravity = GetCurrentGravity(); vecToss = (vecTarget - vecStart); // throw at a constant time float time = vecToss.Length() / flSpeed; vecToss = vecToss * (1.0f / time); // adjust upward toss to compensate for gravity loss vecToss.z += flGravity * time * 0.5f; } } // Save out the result if (pOut) { *pOut = vecToss; } } //----------------------------------------------------------------------------- // Purpose: // Input : *pEvent - //----------------------------------------------------------------------------- void CNPC_AntlionGuard::HandleAnimEvent(animevent_t *pEvent) { if (pEvent->event == AE_ANTLIONGUARD_CHARGE_EARLYOUT) { // Robin: Removed this because it usually made him look less intelligent, not more. // This code left here so we don't get warnings in the console. /* // Cancel the charge if it's no longer valid if ( ShouldCharge( GetAbsOrigin(), GetEnemy()->GetAbsOrigin(), false, true ) == false ) { SetSchedule( SCHED_ANTLIONGUARD_CHARGE_CANCEL ); } */ return; } // Don't handle anim events after death if (m_NPCState == NPC_STATE_DEAD) { BaseClass::HandleAnimEvent(pEvent); return; } if (pEvent->event == AE_ANTLIONGUARD_SHOVE_PHYSOBJECT) { if (m_hPhysicsTarget == NULL) { // Disrupt other objects near us anyway ImpactShock(WorldSpaceCenter(), 150, 250.0f); return; } // If we have no enemy, we don't really have a direction to throw the object // in. But, we still want to clobber it so the animevent doesn't happen fruitlessly, // and we want to clear the condition flags and other state regarding the m_hPhysicsTarget. // So, skip the code relevant to computing a launch vector specific to the object I'm // striking, but use the ImpactShock call further below to punch everything in the neighborhood. CBaseEntity *pEnemy = GetEnemy(); if (pEnemy != NULL) { //Setup the throw velocity IPhysicsObject *physObj = m_hPhysicsTarget->VPhysicsGetObject(); Vector vecShoveVel = (pEnemy->GetAbsOrigin() - m_hPhysicsTarget->WorldSpaceCenter()); float flTargetDist = VectorNormalize(vecShoveVel); // Must still be close enough to our target Vector shoveDir = m_hPhysicsTarget->WorldSpaceCenter() - WorldSpaceCenter(); float shoveDist = VectorNormalize(shoveDir); if (shoveDist > 300.0f) { // Pick a new target next time (this one foiled us!) RememberFailedPhysicsTarget(m_hPhysicsTarget); m_hPhysicsTarget = NULL; return; } // Toss this if we're episodic if (hl2_episodic.GetBool()) { Vector vecTargetDir = vecShoveVel; // Get our shove direction GetPhysicsShoveDir(m_hPhysicsTarget, physObj->GetMass(), &vecShoveVel); // If the player isn't looking at me, and isn't reachable, be more forgiving about hitting them if (HasCondition(COND_ENEMY_UNREACHABLE) && HasCondition(COND_ENEMY_FACING_ME) == false) { // Build an arc around the top of the target that we'll offset our aim by Vector vecOffset; float flSin, flCos; float flRad = random->RandomFloat(0, M_PI / 6.0f); // +/- 30 deg if (random->RandomInt(0, 1)) flRad *= -1.0f; SinCos(flRad, &flSin, &flCos); // Rotate the 2-d circle to be "facing" along our shot direction Vector vecArc; QAngle vecAngles; VectorAngles(vecTargetDir, vecAngles); VectorRotate(Vector(0.0f, flCos, flSin), vecAngles, vecArc); // Find the radius by which to avoid the player float flOffsetRadius = (m_hPhysicsTarget->CollisionProp()->BoundingRadius() + GetEnemy()->CollisionProp()->BoundingRadius()) * 1.5f; // Add this to our velocity to offset it vecShoveVel += (vecArc * flOffsetRadius); } // Factor out mass vecShoveVel *= physObj->GetMass(); // FIXME: We need to restore this on the object at some point if we do this! float flDragCoef = 0.0f; physObj->SetDragCoefficient(&flDragCoef, &flDragCoef); } else { if (flTargetDist < 512) flTargetDist = 512; if (flTargetDist > 1024) flTargetDist = 1024; vecShoveVel *= flTargetDist * 3 * physObj->GetMass(); //FIXME: Scale by skill vecShoveVel[2] += physObj->GetMass() * 350.0f; } if (NPC_Rollermine_IsRollermine(m_hPhysicsTarget)) { Pickup_OnPhysGunDrop(m_hPhysicsTarget, NULL, LAUNCHED_BY_CANNON); } //Send it flying AngularImpulse angVel(random->RandomFloat(-180, 180), 100, random->RandomFloat(-360, 360)); physObj->ApplyForceCenter(vecShoveVel); physObj->AddVelocity(NULL, &angVel); } //Display dust Vector vecRandom = RandomVector(-1, 1); VectorNormalize(vecRandom); g_pEffects->Dust(m_hPhysicsTarget->WorldSpaceCenter(), vecRandom, 64.0f, 32); // If it's being held by the player, break that bond Pickup_ForcePlayerToDropThisObject(m_hPhysicsTarget); EmitSound("NPC_AntlionGuard.HitHard"); //Clear the state information, we're done ClearCondition(COND_ANTLIONGUARD_PHYSICS_TARGET); ClearCondition(COND_ANTLIONGUARD_PHYSICS_TARGET_INVALID); // Disrupt other objects near it, including the m_hPhysicsTarget if we had no valid enemy ImpactShock(m_hPhysicsTarget->WorldSpaceCenter(), 150, 250.0f, pEnemy != NULL ? m_hPhysicsTarget : NULL); // Notify any squad members that we're no longer monopolizing this object if (GetSquad() != NULL) { GetSquad()->BroadcastInteraction(g_interactionAntlionGuardShovedPhysicsObject, (void *)((CBaseEntity *)m_hPhysicsTarget), this); } m_hPhysicsTarget = NULL; m_FailedPhysicsTargets.RemoveAll(); return; } if (pEvent->event == AE_ANTLIONGUARD_CHARGE_HIT) { UTIL_ScreenShake(GetAbsOrigin(), 32.0f, 4.0f, 1.0f, 512, SHAKE_START); EmitSound("NPC_AntlionGuard.HitHard"); Vector startPos = GetAbsOrigin(); float checkSize = (CollisionProp()->BoundingRadius() + 8.0f); Vector endPos = startPos + (BodyDirection3D() * checkSize); CTraceFilterCharge traceFilter(this, COLLISION_GROUP_NONE, this); Ray_t ray; ray.Init(startPos, endPos, GetHullMins(), GetHullMaxs()); trace_t tr; enginetrace->TraceRay(ray, MASK_SHOT, &traceFilter, &tr); if (g_debug_antlionguard.GetInt() == 1) { Vector hullMaxs = GetHullMaxs(); hullMaxs.x += checkSize; NDebugOverlay::BoxDirection(startPos, GetHullMins(), hullMaxs, BodyDirection2D(), 100, 255, 255, 20, 1.0f); } //NDebugOverlay::Box3D( startPos, endPos, BodyDirection2D(), if (m_hChargeTarget && m_hChargeTarget->IsAlive() == false) { m_hChargeTarget = NULL; m_hChargeTargetPosition = NULL; } // Cause a shock wave from this point which will distrupt nearby physics objects ImpactShock(tr.endpos, 200, 500); return; } if (pEvent->event == AE_ANTLIONGUARD_SHOVE) { EmitSound("NPC_AntlionGuard.StepLight", pEvent->eventtime); Shove(); return; } if (pEvent->event == AE_ANTLIONGUARD_FOOTSTEP_LIGHT) { if (HasSpawnFlags(SF_ANTLIONGUARD_INSIDE_FOOTSTEPS)) { #if HL2_EPISODIC Footstep(false); #else EmitSound("NPC_AntlionGuard.Inside.StepLight", pEvent->eventtime); #endif // HL2_EPISODIC } else { #if HL2_EPISODIC Footstep(false); #else EmitSound("NPC_AntlionGuard.StepLight", pEvent->eventtime); #endif // HL2_EPISODIC } return; } if (pEvent->event == AE_ANTLIONGUARD_FOOTSTEP_HEAVY) { if (HasSpawnFlags(SF_ANTLIONGUARD_INSIDE_FOOTSTEPS)) { #if HL2_EPISODIC Footstep(true); #else EmitSound("NPC_AntlionGuard.Inside.StepHeavy", pEvent->eventtime); #endif // HL2_EPISODIC } else { #if HL2_EPISODIC Footstep(true); #else EmitSound("NPC_AntlionGuard.StepHeavy", pEvent->eventtime); #endif // HL2_EPISODIC } return; } if (pEvent->event == AE_ANTLIONGUARD_VOICE_GROWL) { StartSounds(); float duration = 0.0f; if (random->RandomInt(0, 10) < 6) { duration = ENVELOPE_CONTROLLER.SoundPlayEnvelope(m_pGrowlHighSound, SOUNDCTRL_CHANGE_VOLUME, envAntlionGuardFastGrowl, ARRAYSIZE(envAntlionGuardFastGrowl)); } else { duration = 1.0f; EmitSound("NPC_AntlionGuard.FrustratedRoar"); ENVELOPE_CONTROLLER.SoundFadeOut(m_pGrowlHighSound, 0.5f, false); } m_flAngerNoiseTime = gpGlobals->curtime + duration + random->RandomFloat(2.0f, 4.0f); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pBreathSound, 0.0f, 0.1f); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pGrowlIdleSound, 0.0f, 0.1f); m_flBreathTime = gpGlobals->curtime + duration - 0.2f; EmitSound("NPC_AntlionGuard.Anger"); return; } if (pEvent->event == AE_ANTLIONGUARD_VOICE_BARK) { StartSounds(); float duration = ENVELOPE_CONTROLLER.SoundPlayEnvelope(m_pGrowlHighSound, SOUNDCTRL_CHANGE_VOLUME, envAntlionGuardBark1, ARRAYSIZE(envAntlionGuardBark1)); ENVELOPE_CONTROLLER.SoundPlayEnvelope(m_pConfusedSound, SOUNDCTRL_CHANGE_VOLUME, envAntlionGuardBark2, ARRAYSIZE(envAntlionGuardBark2)); m_flAngerNoiseTime = gpGlobals->curtime + duration + random->RandomFloat(2.0f, 4.0f); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pBreathSound, 0.0f, 0.1f); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pGrowlIdleSound, 0.0f, 0.1f); m_flBreathTime = gpGlobals->curtime + duration - 0.2f; return; } if (pEvent->event == AE_ANTLIONGUARD_VOICE_ROAR) { StartSounds(); float duration = ENVELOPE_CONTROLLER.SoundPlayEnvelope(m_pGrowlHighSound, SOUNDCTRL_CHANGE_VOLUME, envAntlionGuardFastGrowl, ARRAYSIZE(envAntlionGuardFastGrowl)); m_flAngerNoiseTime = gpGlobals->curtime + duration + random->RandomFloat(2.0f, 4.0f); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pBreathSound, 0.0f, 0.1f); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pGrowlIdleSound, 0.0f, 0.1f); m_flBreathTime = gpGlobals->curtime + duration - 0.2f; EmitSound("NPC_AntlionGuard.Roar"); return; } if (pEvent->event == AE_ANTLIONGUARD_VOICE_PAIN) { StartSounds(); float duration = ENVELOPE_CONTROLLER.SoundPlayEnvelope(m_pConfusedSound, SOUNDCTRL_CHANGE_VOLUME, envAntlionGuardPain1, ARRAYSIZE(envAntlionGuardPain1)); ENVELOPE_CONTROLLER.SoundPlayEnvelope(m_pGrowlHighSound, SOUNDCTRL_CHANGE_VOLUME, envAntlionGuardBark2, ARRAYSIZE(envAntlionGuardBark2)); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pBreathSound, 0.0f, 0.1f); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pGrowlIdleSound, 0.0f, 0.1f); m_flBreathTime = gpGlobals->curtime + duration - 0.2f; return; } if (pEvent->event == AE_ANTLIONGUARD_VOICE_SQUEEZE) { StartSounds(); float duration = ENVELOPE_CONTROLLER.SoundPlayEnvelope(m_pGrowlHighSound, SOUNDCTRL_CHANGE_VOLUME, envAntlionGuardSqueeze, ARRAYSIZE(envAntlionGuardSqueeze)); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pBreathSound, 0.6f, random->RandomFloat(2.0f, 4.0f)); ENVELOPE_CONTROLLER.SoundChangePitch(m_pBreathSound, random->RandomInt(60, 80), random->RandomFloat(2.0f, 4.0f)); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pGrowlIdleSound, 0.0f, 0.1f); m_flBreathTime = gpGlobals->curtime + (duration * 0.5f); EmitSound("NPC_AntlionGuard.Anger"); return; } if (pEvent->event == AE_ANTLIONGUARD_VOICE_SCRATCH) { StartSounds(); float duration = random->RandomFloat(2.0f, 4.0f); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pBreathSound, 0.6f, duration); ENVELOPE_CONTROLLER.SoundChangePitch(m_pBreathSound, random->RandomInt(60, 80), duration); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pGrowlIdleSound, 0.0f, 0.1f); m_flBreathTime = gpGlobals->curtime + duration; EmitSound("NPC_AntlionGuard.Anger"); return; } if (pEvent->event == AE_ANTLIONGUARD_VOICE_GRUNT) { StartSounds(); float duration = ENVELOPE_CONTROLLER.SoundPlayEnvelope(m_pConfusedSound, SOUNDCTRL_CHANGE_VOLUME, envAntlionGuardGrunt, ARRAYSIZE(envAntlionGuardGrunt)); ENVELOPE_CONTROLLER.SoundPlayEnvelope(m_pGrowlHighSound, SOUNDCTRL_CHANGE_VOLUME, envAntlionGuardGrunt2, ARRAYSIZE(envAntlionGuardGrunt2)); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pBreathSound, 0.0f, 0.1f); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pGrowlIdleSound, 0.0f, 0.1f); m_flBreathTime = gpGlobals->curtime + duration; return; } if (pEvent->event == AE_ANTLIONGUARD_BURROW_OUT) { EmitSound("NPC_Antlion.BurrowOut"); //Shake the screen UTIL_ScreenShake(GetAbsOrigin(), 0.5f, 80.0f, 1.0f, 256.0f, SHAKE_START); //Throw dust up UTIL_CreateAntlionDust(GetAbsOrigin() + Vector(0, 0, 24), GetLocalAngles()); RemoveEffects(EF_NODRAW); RemoveFlag(FL_NOTARGET); if (m_bCavernBreed) { if (m_hCaveGlow[0]) m_hCaveGlow[0]->TurnOn(); if (m_hCaveGlow[1]) m_hCaveGlow[1]->TurnOn(); } return; } BaseClass::HandleAnimEvent(pEvent); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::SetHeavyDamageAnim(const Vector &vecSource) { if (!m_bInCavern) { Vector vFacing = BodyDirection2D(); Vector vDamageDir = (vecSource - WorldSpaceCenter()); vDamageDir.z = 0.0f; VectorNormalize(vDamageDir); Vector vDamageRight, vDamageUp; VectorVectors(vDamageDir, vDamageRight, vDamageUp); float damageDot = DotProduct(vFacing, vDamageDir); float damageRightDot = DotProduct(vFacing, vDamageRight); // See if it's in front if (damageDot > 0.0f) { // See if it's right if (damageRightDot > 0.0f) { m_nFlinchActivity = ACT_ANTLIONGUARD_PHYSHIT_FR; } else { m_nFlinchActivity = ACT_ANTLIONGUARD_PHYSHIT_FL; } } else { // Otherwise it's from behind if (damageRightDot < 0.0f) { m_nFlinchActivity = ACT_ANTLIONGUARD_PHYSHIT_RR; } else { m_nFlinchActivity = ACT_ANTLIONGUARD_PHYSHIT_RL; } } } } //----------------------------------------------------------------------------- // Purpose: // Input : &info - //----------------------------------------------------------------------------- int CNPC_AntlionGuard::OnTakeDamage_Alive(const CTakeDamageInfo &info) { CTakeDamageInfo dInfo = info; // Don't take damage from another antlion guard! if (dInfo.GetAttacker() && dInfo.GetAttacker() != this && FClassnameIs(dInfo.GetAttacker(), "npc_antlionguard")) return 0; if ((dInfo.GetDamageType() & DMG_CRUSH) && !(dInfo.GetDamageType() & DMG_VEHICLE)) { // Don't take damage from physics objects that weren't thrown by the player. CBaseEntity *pInflictor = dInfo.GetInflictor(); IPhysicsObject *pObj = pInflictor->VPhysicsGetObject(); if (!pObj || !(pObj->GetGameFlags() & FVPHYSICS_WAS_THROWN)) { return 0; } } // Hack to make antlion guard harder in HARD if (g_pGameRules->IsSkillLevel(SKILL_HARD) && !(info.GetDamageType() & DMG_CRUSH)) { dInfo.SetDamage(dInfo.GetDamage() * 0.75); } // Cap damage taken by crushing (otherwise we can get crushed oddly) if ((dInfo.GetDamageType() & DMG_CRUSH) && dInfo.GetDamage() > 100) { dInfo.SetDamage(100); } // Only take damage from what we classify as heavy damages (explosions, refrigerators, etc) if (IsHeavyDamage(dInfo)) { // Always take a set amount of damage from a combine ball if (info.GetInflictor() && UTIL_IsCombineBall(info.GetInflictor())) { dInfo.SetDamage(50); } UTIL_ScreenShake(GetAbsOrigin(), 32.0f, 8.0f, 0.5f, 512, SHAKE_START); // Set our response animation SetHeavyDamageAnim(dInfo.GetDamagePosition()); // Explosive barrels don't carry through their attacker, so this // condition isn't set, and we still want to flinch. So we set it. SetCondition(COND_HEAVY_DAMAGE); // TODO: Make this its own effect! CEffectData data; data.m_vOrigin = dInfo.GetDamagePosition(); data.m_vNormal = -dInfo.GetDamageForce(); VectorNormalize(data.m_vNormal); DispatchEffect("HunterDamage", data); // Play a sound for a physics impact if (dInfo.GetDamageType() & DMG_CRUSH) { EmitSound("NPC_AntlionGuard.ShellCrack"); } // Roar in pain EmitSound("NPC_AntlionGuard.Pain_Roar"); // TODO: This will require a more complete solution; for now let's shelve it! /* if ( dInfo.GetDamageType() & DMG_BLAST ) { // Create the dust effect in place CParticleSystem *pParticle = (CParticleSystem *) CreateEntityByName( "info_particle_system" ); if ( pParticle != NULL ) { // Setup our basic parameters pParticle->KeyValue( "start_active", "1" ); pParticle->KeyValue( "effect_name", "fire_medium_02_nosmoke" ); pParticle->SetParent( this ); pParticle->SetParentAttachment( "SetParentAttachment", "attach_glow2", true ); pParticle->SetLocalOrigin( Vector( -16, 24, 0 ) ); DispatchSpawn( pParticle ); if ( gpGlobals->curtime > 0.5f ) pParticle->Activate(); pParticle->SetThink( &CBaseEntity::SUB_Remove ); pParticle->SetNextThink( gpGlobals->curtime + random->RandomFloat( 2.0f, 3.0f ) ); } } */ } int nPreHealth = GetHealth(); int nDamageTaken = BaseClass::OnTakeDamage_Alive(dInfo); // See if we've crossed a measured phase in our health and flinch from that to show we do take damage if (!m_bInCavern && HasCondition(COND_HEAVY_DAMAGE) == false && (info.GetDamageType() & DMG_BULLET)) { bool bTakeHeavyDamage = false; // Do an early flinch so that players understand the guard can be hurt by if (((float)GetHealth() / (float)GetMaxHealth()) > 0.9f) { float flPrePerc = (float)nPreHealth / (float)GetMaxHealth(); float flPostPerc = (float)GetHealth() / (float)GetMaxHealth(); if (flPrePerc >= 0.95f && flPostPerc < 0.95f) { bTakeHeavyDamage = true; } } // Otherwise see if we've passed a measured point in our health if (bTakeHeavyDamage == false) { const float flNumDamagePhases = 5.0f; float flDenom = ((float)GetMaxHealth() / flNumDamagePhases); int nPreDamagePhase = ceil((float)nPreHealth / flDenom); int nPostDamagePhase = ceil((float)GetHealth() / flDenom); if (nPreDamagePhase != nPostDamagePhase) { bTakeHeavyDamage = true; } } // Flinch if we should if (bTakeHeavyDamage) { // Set our response animation SetHeavyDamageAnim(dInfo.GetDamagePosition()); // Roar in pain EmitSound("NPC_AntlionGuard.Pain_Roar"); // Flinch! SetCondition(COND_HEAVY_DAMAGE); } } return nDamageTaken; } //----------------------------------------------------------------------------- // Purpose: // Input : *pAttacker - // flDamage - // &vecDir - // *ptr - // bitsDamageType - //----------------------------------------------------------------------------- void CNPC_AntlionGuard::TraceAttack(const CTakeDamageInfo &inputInfo, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator) { CTakeDamageInfo info = inputInfo; // Bullets are weak against us, buckshot less so if (info.GetDamageType() & DMG_BUCKSHOT) { info.ScaleDamage(0.5f); } else if (info.GetDamageType() & DMG_BULLET) { info.ScaleDamage(0.25f); } // Make sure we haven't rounded down to a minimal amount if (info.GetDamage() < 1.0f) { info.SetDamage(1.0f); } BaseClass::TraceAttack(info, vecDir, ptr, pAccumulator); } //----------------------------------------------------------------------------- // Purpose: // Input : *pTask - //----------------------------------------------------------------------------- void CNPC_AntlionGuard::StartTask(const Task_t *pTask) { switch (pTask->iTask) { case TASK_ANTLIONGUARD_SET_FLINCH_ACTIVITY: SetIdealActivity((Activity)m_nFlinchActivity); break; case TASK_ANTLIONGUARD_SUMMON: SummonAntlions(); m_OnSummon.FireOutput(this, this, 0); TaskComplete(); break; case TASK_ANTLIONGUARD_SHOVE_PHYSOBJECT: { if ((m_hPhysicsTarget == NULL) || (GetEnemy() == NULL)) { TaskFail("Tried to shove a NULL physics target!\n"); break; } //Get the direction and distance to our thrown object Vector dirToTarget = (m_hPhysicsTarget->WorldSpaceCenter() - WorldSpaceCenter()); float distToTarget = VectorNormalize(dirToTarget); dirToTarget.z = 0; //Validate it's still close enough to shove //FIXME: Real numbers if (distToTarget > 256.0f) { RememberFailedPhysicsTarget(m_hPhysicsTarget); m_hPhysicsTarget = NULL; TaskFail("Shove target moved\n"); break; } //Validate its offset from our facing float targetYaw = UTIL_VecToYaw(dirToTarget); float offset = UTIL_AngleDiff(targetYaw, UTIL_AngleMod(GetLocalAngles().y)); if (fabs(offset) > 55) { RememberFailedPhysicsTarget(m_hPhysicsTarget); m_hPhysicsTarget = NULL; TaskFail("Shove target off-center\n"); break; } //Blend properly SetPoseParameter(m_poseThrow, offset); //Start playing the animation SetActivity(ACT_ANTLIONGUARD_SHOVE_PHYSOBJECT); } break; case TASK_ANTLIONGUARD_FIND_PHYSOBJECT: { if (m_bInCavern && !m_bPreferPhysicsAttack) { TaskFail("Cavern guard is not allowed to use physics attacks."); } // Force the antlion guard to find a physobject m_flPhysicsCheckTime = 0; UpdatePhysicsTarget(false, (100 * 12)); if (m_hPhysicsTarget) { TaskComplete(); } else { TaskFail("Failed to find a physobject.\n"); } } break; case TASK_ANTLIONGUARD_GET_PATH_TO_PHYSOBJECT: { if ((m_hPhysicsTarget == NULL) || (GetEnemy() == NULL)) { TaskFail("Tried to find a path to NULL physics target!\n"); break; } Vector vecGoalPos = m_vecPhysicsHitPosition; AI_NavGoal_t goal(GOALTYPE_LOCATION, vecGoalPos, ACT_RUN); if (GetNavigator()->SetGoal(goal)) { if (g_debug_antlionguard.GetInt() == 1) { NDebugOverlay::Cross3D(vecGoalPos, Vector(8, 8, 8), -Vector(8, 8, 8), 0, 255, 0, true, 2.0f); NDebugOverlay::Line(vecGoalPos, m_hPhysicsTarget->WorldSpaceCenter(), 0, 255, 0, true, 2.0f); NDebugOverlay::Line(m_hPhysicsTarget->WorldSpaceCenter(), GetEnemy()->WorldSpaceCenter(), 0, 255, 0, true, 2.0f); } // Face the enemy GetNavigator()->SetArrivalDirection(GetEnemy()); TaskComplete(); } else { if (g_debug_antlionguard.GetInt() == 1) { NDebugOverlay::Cross3D(vecGoalPos, Vector(8, 8, 8), -Vector(8, 8, 8), 255, 0, 0, true, 2.0f); NDebugOverlay::Line(vecGoalPos, m_hPhysicsTarget->WorldSpaceCenter(), 255, 0, 0, true, 2.0f); NDebugOverlay::Line(m_hPhysicsTarget->WorldSpaceCenter(), GetEnemy()->WorldSpaceCenter(), 255, 0, 0, true, 2.0f); } RememberFailedPhysicsTarget(m_hPhysicsTarget); m_hPhysicsTarget = NULL; TaskFail("Unable to navigate to physics attack target!\n"); break; } //!!!HACKHACK - this is a hack that covers a bug in antlion guard physics target selection! (Tracker #77601) // This piece of code (combined with some code in GatherConditions) COVERS THE BUG by escaping the schedule // if 30 seconds have passed (it should never take this long for the guard to get to an object and hit it). // It's too scary to figure out why this particular antlion guard can't get to its object, but we're shipping // like, tomorrow. (sjb) 8/22/2007 m_flWaitFinished = gpGlobals->curtime + 30.0f; } break; case TASK_ANTLIONGUARD_CHARGE: { // HACK: Because the guard stops running his normal blended movement // here, he also needs to remove his blended movement layers! GetMotor()->MoveStop(); SetActivity(ACT_ANTLIONGUARD_CHARGE_START); m_bDecidedNotToStop = false; } break; case TASK_ANTLIONGUARD_GET_PATH_TO_CHARGE_POSITION: { if (!m_hChargeTargetPosition) { TaskFail("Tried to find a charge position without one specified.\n"); break; } // Move to the charge position AI_NavGoal_t goal(GOALTYPE_LOCATION, m_hChargeTargetPosition->GetAbsOrigin(), ACT_RUN); if (GetNavigator()->SetGoal(goal)) { // We want to face towards the charge target Vector vecDir = m_hChargeTarget->GetAbsOrigin() - m_hChargeTargetPosition->GetAbsOrigin(); VectorNormalize(vecDir); vecDir.z = 0; GetNavigator()->SetArrivalDirection(vecDir); TaskComplete(); } else { m_hChargeTarget = NULL; m_hChargeTargetPosition = NULL; TaskFail(FAIL_NO_ROUTE); } } break; case TASK_ANTLIONGUARD_GET_PATH_TO_NEAREST_NODE: { if (!GetEnemy()) { TaskFail(FAIL_NO_ENEMY); break; } // Find the nearest node to the enemy int node = GetNavigator()->GetNetwork()->NearestNodeToPoint(this, GetEnemy()->GetAbsOrigin(), false); CAI_Node *pNode = GetNavigator()->GetNetwork()->GetNode(node); if (pNode == NULL) { TaskFail(FAIL_NO_ROUTE); break; } Vector vecNodePos = pNode->GetPosition(GetHullType()); AI_NavGoal_t goal(GOALTYPE_LOCATION, vecNodePos, ACT_RUN); if (GetNavigator()->SetGoal(goal)) { GetNavigator()->SetArrivalDirection(GetEnemy()); TaskComplete(); break; } TaskFail(FAIL_NO_ROUTE); break; } break; case TASK_ANTLIONGUARD_GET_CHASE_PATH_ENEMY_TOLERANCE: { // Chase the enemy, but allow local navigation to succeed if it gets within the goal tolerance GetNavigator()->SetLocalSucceedOnWithinTolerance(true); if (GetNavigator()->SetGoal(GOALTYPE_ENEMY)) { TaskComplete(); } else { RememberUnreachable(GetEnemy()); TaskFail(FAIL_NO_ROUTE); } GetNavigator()->SetLocalSucceedOnWithinTolerance(false); } break; case TASK_ANTLIONGUARD_OPPORTUNITY_THROW: { // If we've got some live antlions, look for a physics object to throw if (m_iNumLiveAntlions >= 2 && RandomFloat(0, 1) > 0.5) { m_FailedPhysicsTargets.RemoveAll(); UpdatePhysicsTarget(false, m_bPreferPhysicsAttack ? (100 * 12) : ANTLIONGUARD_FARTHEST_PHYSICS_OBJECT); if (HasCondition(COND_ANTLIONGUARD_PHYSICS_TARGET) && !m_bInCavern) { SetSchedule(SCHED_ANTLIONGUARD_PHYSICS_ATTACK); } } TaskComplete(); } break; default: BaseClass::StartTask(pTask); break; } } //----------------------------------------------------------------------------- // Purpose: Calculate & apply damage & force for a charge to a target. // Done outside of the guard because we need to do this inside a trace filter. //----------------------------------------------------------------------------- void ApplyChargeDamage(CBaseEntity *pAntlionGuard, CBaseEntity *pTarget, float flDamage) { Vector attackDir = (pTarget->WorldSpaceCenter() - pAntlionGuard->WorldSpaceCenter()); VectorNormalize(attackDir); Vector offset = RandomVector(-32, 32) + pTarget->WorldSpaceCenter(); // Generate enough force to make a 75kg guy move away at 700 in/sec Vector vecForce = attackDir * ImpulseScale(75, 700); // Deal the damage CTakeDamageInfo info(pAntlionGuard, pAntlionGuard, vecForce, offset, flDamage, DMG_CLUB); pTarget->TakeDamage(info); #if HL2_EPISODIC // If I am a cavern guard attacking the player, and he still lives, then poison him too. Assert(dynamic_cast(pAntlionGuard)); if (static_cast(pAntlionGuard)->IsInCavern() && pTarget->IsPlayer() && pTarget->IsAlive() && pTarget->m_iHealth > ANTLIONGUARD_POISON_TO) { // That didn't finish them. Take them down to one point with poison damage. It'll heal. pTarget->TakeDamage(CTakeDamageInfo(pAntlionGuard, pAntlionGuard, pTarget->m_iHealth - ANTLIONGUARD_POISON_TO, DMG_POISON)); } #endif } //----------------------------------------------------------------------------- // Purpose: A simple trace filter class to skip small moveable physics objects //----------------------------------------------------------------------------- class CTraceFilterSkipPhysics : public CTraceFilter { public: // It does have a base, but we'll never network anything below here.. DECLARE_CLASS_NOBASE(CTraceFilterSkipPhysics); CTraceFilterSkipPhysics(const IHandleEntity *passentity, int collisionGroup, float minMass) : m_pPassEnt(passentity), m_collisionGroup(collisionGroup), m_minMass(minMass) { } virtual bool ShouldHitEntity(IHandleEntity *pHandleEntity, int contentsMask) { if (!StandardFilterRules(pHandleEntity, contentsMask)) return false; if (!PassServerEntityFilter(pHandleEntity, m_pPassEnt)) return false; // Don't test if the game code tells us we should ignore this collision... CBaseEntity *pEntity = EntityFromEntityHandle(pHandleEntity); if (pEntity) { if (!pEntity->ShouldCollide(m_collisionGroup, contentsMask)) return false; if (!g_pGameRules->ShouldCollide(m_collisionGroup, pEntity->GetCollisionGroup())) return false; // don't test small moveable physics objects (unless it's an NPC) if (!pEntity->IsNPC() && pEntity->GetMoveType() == MOVETYPE_VPHYSICS) { IPhysicsObject *pPhysics = pEntity->VPhysicsGetObject(); Assert(pPhysics); if (pPhysics->IsMoveable() && pPhysics->GetMass() < m_minMass) return false; } // If we hit an antlion, don't stop, but kill it if (pEntity->Classify() == CLASS_ANTLION) { CBaseEntity *pGuard = (CBaseEntity*)EntityFromEntityHandle(m_pPassEnt); ApplyChargeDamage(pGuard, pEntity, pEntity->GetHealth()); return false; } } return true; } private: const IHandleEntity *m_pPassEnt; int m_collisionGroup; float m_minMass; }; inline void TraceHull_SkipPhysics(const Vector &vecAbsStart, const Vector &vecAbsEnd, const Vector &hullMin, const Vector &hullMax, unsigned int mask, const CBaseEntity *ignore, int collisionGroup, trace_t *ptr, float minMass) { Ray_t ray; ray.Init(vecAbsStart, vecAbsEnd, hullMin, hullMax); CTraceFilterSkipPhysics traceFilter(ignore, collisionGroup, minMass); enginetrace->TraceRay(ray, mask, &traceFilter, ptr); } //----------------------------------------------------------------------------- // Purpose: Return true if our charge target is right in front of the guard // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CNPC_AntlionGuard::EnemyIsRightInFrontOfMe(CBaseEntity **pEntity) { if (!GetEnemy()) return false; if ((GetEnemy()->WorldSpaceCenter() - WorldSpaceCenter()).LengthSqr() < (156 * 156)) { Vector vecLOS = (GetEnemy()->GetAbsOrigin() - GetAbsOrigin()); vecLOS.z = 0; VectorNormalize(vecLOS); Vector vBodyDir = BodyDirection2D(); if (DotProduct(vecLOS, vBodyDir) > 0.8) { // He's in front of me, and close. Make sure he's not behind a wall. trace_t tr; UTIL_TraceLine(WorldSpaceCenter(), GetEnemy()->EyePosition(), MASK_SOLID, this, COLLISION_GROUP_NONE, &tr); if (tr.m_pEnt == GetEnemy()) { *pEntity = tr.m_pEnt; return true; } } } return false; } //----------------------------------------------------------------------------- // Purpose: While charging, look ahead and see if we're going to run into anything. // If we are, start the gesture so it looks like we're anticipating the hit. //----------------------------------------------------------------------------- void CNPC_AntlionGuard::ChargeLookAhead(void) { trace_t tr; Vector vecForward; GetVectors(&vecForward, NULL, NULL); Vector vecTestPos = GetAbsOrigin() + (vecForward * m_flGroundSpeed * 0.75); Vector testHullMins = GetHullMins(); testHullMins.z += (StepHeight() * 2); TraceHull_SkipPhysics(GetAbsOrigin(), vecTestPos, testHullMins, GetHullMaxs(), MASK_SHOT_HULL, this, COLLISION_GROUP_NONE, &tr, VPhysicsGetObject()->GetMass() * 0.5); //NDebugOverlay::Box( tr.startpos, testHullMins, GetHullMaxs(), 0, 255, 0, true, 0.1f ); //NDebugOverlay::Box( vecTestPos, testHullMins, GetHullMaxs(), 255, 0, 0, true, 0.1f ); if (tr.fraction != 1.0) { // Start playing the hit animation AddGesture(ACT_ANTLIONGUARD_CHARGE_ANTICIPATION); } } //----------------------------------------------------------------------------- // Purpose: Handles the guard charging into something. Returns true if it hit the world. //----------------------------------------------------------------------------- bool CNPC_AntlionGuard::HandleChargeImpact(Vector vecImpact, CBaseEntity *pEntity) { // Cause a shock wave from this point which will disrupt nearby physics objects ImpactShock(vecImpact, 128, 350); // Did we hit anything interesting? if (!pEntity || pEntity->IsWorld()) { // Robin: Due to some of the finicky details in the motor, the guard will hit // the world when it is blocked by our enemy when trying to step up // during a moveprobe. To get around this, we see if the enemy's within // a volume in front of the guard when we hit the world, and if he is, // we hit him anyway. EnemyIsRightInFrontOfMe(&pEntity); // Did we manage to find him? If not, increment our charge miss count and abort. if (pEntity->IsWorld()) { m_iChargeMisses++; return true; } } // Hit anything we don't like if (IRelationType(pEntity) == D_HT && (GetNextAttack() < gpGlobals->curtime)) { EmitSound("NPC_AntlionGuard.Shove"); if (!IsPlayingGesture(ACT_ANTLIONGUARD_CHARGE_HIT)) { RestartGesture(ACT_ANTLIONGUARD_CHARGE_HIT); } ChargeDamage(pEntity); pEntity->ApplyAbsVelocityImpulse((BodyDirection2D() * 400) + Vector(0, 0, 200)); if (!pEntity->IsAlive() && GetEnemy() == pEntity) { SetEnemy(NULL); } SetNextAttack(gpGlobals->curtime + 2.0f); SetActivity(ACT_ANTLIONGUARD_CHARGE_STOP); // We've hit something, so clear our miss count m_iChargeMisses = 0; return false; } // Hit something we don't hate. If it's not moveable, crash into it. if (pEntity->GetMoveType() == MOVETYPE_NONE || pEntity->GetMoveType() == MOVETYPE_PUSH) return true; // If it's a vphysics object that's too heavy, crash into it too. if (pEntity->GetMoveType() == MOVETYPE_VPHYSICS) { IPhysicsObject *pPhysics = pEntity->VPhysicsGetObject(); if (pPhysics) { // If the object is being held by the player, knock it out of his hands if (pPhysics->GetGameFlags() & FVPHYSICS_PLAYER_HELD) { Pickup_ForcePlayerToDropThisObject(pEntity); return false; } if ((!pPhysics->IsMoveable() || pPhysics->GetMass() > VPhysicsGetObject()->GetMass() * 0.5f)) return true; } } return false; /* ROBIN: Wrote & then removed this. If we want to have large rocks that the guard should smack around, then we should enable it. else { // If we hit a physics prop, smack the crap out of it. (large rocks) // Factor the object mass into it, because we want to move it no matter how heavy it is. if ( pEntity->GetMoveType() == MOVETYPE_VPHYSICS ) { CTakeDamageInfo info( this, this, 250, DMG_BLAST ); info.SetDamagePosition( vecImpact ); float flForce = ImpulseScale( pEntity->VPhysicsGetObject()->GetMass(), 250 ); flForce *= random->RandomFloat( 0.85, 1.15 ); // Calculate the vector and stuff it into the takedamageinfo Vector vecForce = BodyDirection3D(); VectorNormalize( vecForce ); vecForce *= flForce; vecForce *= phys_pushscale.GetFloat(); info.SetDamageForce( vecForce ); pEntity->VPhysicsTakeDamage( info ); } } */ } //----------------------------------------------------------------------------- // Purpose: // Output : float //----------------------------------------------------------------------------- float CNPC_AntlionGuard::ChargeSteer(void) { trace_t tr; Vector testPos, steer, forward, right; QAngle angles; const float testLength = m_flGroundSpeed * 0.15f; //Get our facing GetVectors(&forward, &right, NULL); steer = forward; const float faceYaw = UTIL_VecToYaw(forward); //Offset right VectorAngles(forward, angles); angles[YAW] += 45.0f; AngleVectors(angles, &forward); //Probe out testPos = GetAbsOrigin() + (forward * testLength); //Offset by step height Vector testHullMins = GetHullMins(); testHullMins.z += (StepHeight() * 2); //Probe TraceHull_SkipPhysics(GetAbsOrigin(), testPos, testHullMins, GetHullMaxs(), MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr, VPhysicsGetObject()->GetMass() * 0.5f); //Debug info if (g_debug_antlionguard.GetInt() == 1) { if (tr.fraction == 1.0f) { NDebugOverlay::BoxDirection(GetAbsOrigin(), testHullMins, GetHullMaxs() + Vector(testLength, 0, 0), forward, 0, 255, 0, 8, 0.1f); } else { NDebugOverlay::BoxDirection(GetAbsOrigin(), testHullMins, GetHullMaxs() + Vector(testLength, 0, 0), forward, 255, 0, 0, 8, 0.1f); } } //Add in this component steer += (right * 0.5f) * (1.0f - tr.fraction); //Offset left angles[YAW] -= 90.0f; AngleVectors(angles, &forward); //Probe out testPos = GetAbsOrigin() + (forward * testLength); // Probe TraceHull_SkipPhysics(GetAbsOrigin(), testPos, testHullMins, GetHullMaxs(), MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr, VPhysicsGetObject()->GetMass() * 0.5f); //Debug if (g_debug_antlionguard.GetInt() == 1) { if (tr.fraction == 1.0f) { NDebugOverlay::BoxDirection(GetAbsOrigin(), testHullMins, GetHullMaxs() + Vector(testLength, 0, 0), forward, 0, 255, 0, 8, 0.1f); } else { NDebugOverlay::BoxDirection(GetAbsOrigin(), testHullMins, GetHullMaxs() + Vector(testLength, 0, 0), forward, 255, 0, 0, 8, 0.1f); } } //Add in this component steer -= (right * 0.5f) * (1.0f - tr.fraction); //Debug if (g_debug_antlionguard.GetInt() == 1) { NDebugOverlay::Line(GetAbsOrigin(), GetAbsOrigin() + (steer * 512.0f), 255, 255, 0, true, 0.1f); NDebugOverlay::Cross3D(GetAbsOrigin() + (steer * 512.0f), Vector(2, 2, 2), -Vector(2, 2, 2), 255, 255, 0, true, 0.1f); NDebugOverlay::Line(GetAbsOrigin(), GetAbsOrigin() + (BodyDirection3D() * 256.0f), 255, 0, 255, true, 0.1f); NDebugOverlay::Cross3D(GetAbsOrigin() + (BodyDirection3D() * 256.0f), Vector(2, 2, 2), -Vector(2, 2, 2), 255, 0, 255, true, 0.1f); } return UTIL_AngleDiff(UTIL_VecToYaw(steer), faceYaw); } //----------------------------------------------------------------------------- // Purpose: // Input : *pTask - //----------------------------------------------------------------------------- void CNPC_AntlionGuard::RunTask(const Task_t *pTask) { switch (pTask->iTask) { case TASK_ANTLIONGUARD_SET_FLINCH_ACTIVITY: AutoMovement(); if (IsActivityFinished()) { TaskComplete(); } break; case TASK_ANTLIONGUARD_SHOVE_PHYSOBJECT: if (IsActivityFinished()) { TaskComplete(); } break; /* case TASK_RUN_PATH: { } break; */ case TASK_ANTLIONGUARD_CHARGE: { Activity eActivity = GetActivity(); // See if we're trying to stop after hitting/missing our target if (eActivity == ACT_ANTLIONGUARD_CHARGE_STOP || eActivity == ACT_ANTLIONGUARD_CHARGE_CRASH) { if (IsActivityFinished()) { TaskComplete(); return; } // Still in the process of slowing down. Run movement until it's done. AutoMovement(); return; } // Check for manual transition if ((eActivity == ACT_ANTLIONGUARD_CHARGE_START) && (IsActivityFinished())) { SetActivity(ACT_ANTLIONGUARD_CHARGE_RUN); } // See if we're still running if (eActivity == ACT_ANTLIONGUARD_CHARGE_RUN || eActivity == ACT_ANTLIONGUARD_CHARGE_START) { if (HasCondition(COND_NEW_ENEMY) || HasCondition(COND_LOST_ENEMY) || HasCondition(COND_ENEMY_DEAD)) { SetActivity(ACT_ANTLIONGUARD_CHARGE_STOP); return; } else { if (GetEnemy() != NULL) { Vector goalDir = (GetEnemy()->GetAbsOrigin() - GetAbsOrigin()); VectorNormalize(goalDir); if (DotProduct(BodyDirection2D(), goalDir) < 0.25f) { if (!m_bDecidedNotToStop) { // We've missed the target. Randomly decide not to stop, which will cause // the guard to just try and swing around for another pass. m_bDecidedNotToStop = true; if (RandomFloat(0, 1) > 0.3) { m_iChargeMisses++; SetActivity(ACT_ANTLIONGUARD_CHARGE_STOP); } } } else { m_bDecidedNotToStop = false; } } } } // Steer towards our target float idealYaw; if (GetEnemy() == NULL) { idealYaw = GetMotor()->GetIdealYaw(); } else { idealYaw = CalcIdealYaw(GetEnemy()->GetAbsOrigin()); } // Add in our steering offset idealYaw += ChargeSteer(); // Turn to face GetMotor()->SetIdealYawAndUpdate(idealYaw); // See if we're going to run into anything soon ChargeLookAhead(); // Let our animations simply move us forward. Keep the result // of the movement so we know whether we've hit our target. AIMoveTrace_t moveTrace; if (AutoMovement(GetEnemy(), &moveTrace) == false) { // Only stop if we hit the world if (HandleChargeImpact(moveTrace.vEndPosition, moveTrace.pObstruction)) { // If we're starting up, this is an error if (eActivity == ACT_ANTLIONGUARD_CHARGE_START) { TaskFail("Unable to make initial movement of charge\n"); return; } // Crash unless we're trying to stop already if (eActivity != ACT_ANTLIONGUARD_CHARGE_STOP) { if (moveTrace.fStatus == AIMR_BLOCKED_WORLD && moveTrace.vHitNormal == vec3_origin) { SetActivity(ACT_ANTLIONGUARD_CHARGE_STOP); } else { SetActivity(ACT_ANTLIONGUARD_CHARGE_CRASH); } } } else if (moveTrace.pObstruction) { // If we hit an antlion, don't stop, but kill it if (moveTrace.pObstruction->Classify() == CLASS_ANTLION) { if (FClassnameIs(moveTrace.pObstruction, "npc_antlionguard")) { // Crash unless we're trying to stop already if (eActivity != ACT_ANTLIONGUARD_CHARGE_STOP) { SetActivity(ACT_ANTLIONGUARD_CHARGE_STOP); } } else { ApplyChargeDamage(this, moveTrace.pObstruction, moveTrace.pObstruction->GetHealth()); } } } } } break; case TASK_WAIT_FOR_MOVEMENT: { // the cavern antlion can clothesline gordon if (m_bInCavern) { // See if we're going to run into anything soon ChargeLookAhead(); if (HasCondition(COND_CAN_MELEE_ATTACK1)) { CBaseEntity *pEntity = GetEnemy(); if (pEntity && pEntity->IsPlayer()) { EmitSound("NPC_AntlionGuard.Shove"); if (!IsPlayingGesture(ACT_ANTLIONGUARD_CHARGE_HIT)) { RestartGesture(ACT_ANTLIONGUARD_CHARGE_HIT); } ChargeDamage(pEntity); pEntity->ApplyAbsVelocityImpulse((BodyDirection2D() * 400) + Vector(0, 0, 200)); if (!pEntity->IsAlive() && GetEnemy() == pEntity) { SetEnemy(NULL); } SetNextAttack(gpGlobals->curtime + 2.0f); SetActivity(ACT_ANTLIONGUARD_CHARGE_STOP); // We've hit something, so clear our miss count m_iChargeMisses = 0; AutoMovement(); TaskComplete(); return; } } } BaseClass::RunTask(pTask); } break; default: BaseClass::RunTask(pTask); break; } } //----------------------------------------------------------------------------- // Purpose: Summon antlions around the guard //----------------------------------------------------------------------------- void CNPC_AntlionGuard::SummonAntlions(void) { // We want to spawn them around the guard Vector vecForward, vecRight; AngleVectors(QAngle(0, GetAbsAngles().y, 0), &vecForward, &vecRight, NULL); // Spawn positions struct spawnpos_t { float flForward; float flRight; }; spawnpos_t sAntlionSpawnPositions[] = { { 0, 200 }, { 0, -200 }, { 128, 128 }, { 128, -128 }, { -128, 128 }, { -128, -128 }, { 200, 0 }, { -200, 0 }, }; // Only spawn up to our max count int iSpawnPoint = 0; for (int i = 0; (m_iNumLiveAntlions < ANTLIONGUARD_SUMMON_COUNT) && (iSpawnPoint < ARRAYSIZE(sAntlionSpawnPositions)); i++) { // Determine spawn position for the antlion Vector vecSpawn = GetAbsOrigin() + (sAntlionSpawnPositions[iSpawnPoint].flForward * vecForward) + (sAntlionSpawnPositions[iSpawnPoint].flRight * vecRight); iSpawnPoint++; // Randomise it a little vecSpawn.x += RandomFloat(-64, 64); vecSpawn.y += RandomFloat(-64, 64); vecSpawn.z += 64; // Make sure it's clear, and make sure we hit something trace_t tr; UTIL_TraceHull(vecSpawn, vecSpawn - Vector(0, 0, 128), NAI_Hull::Mins(HULL_MEDIUM), NAI_Hull::Maxs(HULL_MEDIUM), MASK_NPCSOLID, NULL, COLLISION_GROUP_NONE, &tr); if (tr.startsolid || tr.allsolid || tr.fraction == 1.0) { if (g_debug_antlionguard.GetInt() == 2) { NDebugOverlay::Box(tr.endpos, NAI_Hull::Mins(HULL_MEDIUM), NAI_Hull::Maxs(HULL_MEDIUM), 255, 0, 0, true, 5.0f); } continue; } // Ensure it's dirt or sand const surfacedata_t *pdata = physprops->GetSurfaceData(tr.surface.surfaceProps); if ((pdata->game.material != CHAR_TEX_DIRT) && (pdata->game.material != CHAR_TEX_SAND)) { if (g_debug_antlionguard.GetInt() == 2) { NDebugOverlay::Box(tr.endpos, NAI_Hull::Mins(HULL_MEDIUM), NAI_Hull::Maxs(HULL_MEDIUM), 255, 128, 128, true, 5.0f); } continue; } // Make sure the guard can see it trace_t tr_vis; UTIL_TraceLine(WorldSpaceCenter(), tr.endpos, MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr_vis); if (tr_vis.fraction != 1.0) { if (g_debug_antlionguard.GetInt() == 2) { NDebugOverlay::Line(WorldSpaceCenter(), tr.endpos, 255, 0, 0, true, 5.0f); } continue; } CAI_BaseNPC *pent = (CAI_BaseNPC*)CreateEntityByName("npc_antlion"); if (!pent) break; CNPC_Antlion *pAntlion = assert_cast(pent); if (g_debug_antlionguard.GetInt() == 2) { NDebugOverlay::Box(tr.endpos, NAI_Hull::Mins(HULL_MEDIUM), NAI_Hull::Maxs(HULL_MEDIUM), 0, 255, 0, true, 5.0f); NDebugOverlay::Line(WorldSpaceCenter(), tr.endpos, 0, 255, 0, true, 5.0f); } vecSpawn = tr.endpos; pAntlion->SetAbsOrigin(vecSpawn); // Start facing our enemy if we have one, otherwise just match the guard. Vector vecFacing = vecForward; if (GetEnemy()) { vecFacing = GetEnemy()->GetAbsOrigin() - GetAbsOrigin(); VectorNormalize(vecFacing); } QAngle vecAngles; VectorAngles(vecFacing, vecAngles); pAntlion->SetAbsAngles(vecAngles); pAntlion->AddSpawnFlags(SF_NPC_FALL_TO_GROUND); pAntlion->AddSpawnFlags(SF_NPC_FADE_CORPSE); // Make the antlion fire my input when he dies pAntlion->KeyValue("OnDeath", UTIL_VarArgs("%s,SummonedAntlionDied,,0,-1", STRING(GetEntityName()))); // Start the antlion burrowed, and tell him to come up pAntlion->m_bStartBurrowed = true; DispatchSpawn(pAntlion); pAntlion->Activate(); g_EventQueue.AddEvent(pAntlion, "Unburrow", RandomFloat(0.1, 1.0), this, this); // Add it to our squad if (GetSquad() != NULL) { GetSquad()->AddToSquad(pAntlion); } // Set the antlion's enemy to our enemy if (GetEnemy()) { pAntlion->SetEnemy(GetEnemy()); pAntlion->SetState(NPC_STATE_COMBAT); pAntlion->UpdateEnemyMemory(GetEnemy(), GetEnemy()->GetAbsOrigin()); } m_iNumLiveAntlions++; } if (g_debug_antlionguard.GetInt() == 2) { Msg("Guard summoned antlion count: %d\n", m_iNumLiveAntlions); } if (m_iNumLiveAntlions > 2) { m_flNextSummonTime = gpGlobals->curtime + RandomFloat(15, 20); } else { m_flNextSummonTime = gpGlobals->curtime + RandomFloat(10, 15); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::FoundEnemy(void) { m_flAngerNoiseTime = gpGlobals->curtime + 2.0f; SetState(NPC_STATE_COMBAT); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::LostEnemy(void) { m_flSearchNoiseTime = gpGlobals->curtime + 2.0f; SetState(NPC_STATE_ALERT); m_OnLostPlayer.FireOutput(this, this); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::InputSetShoveTarget(inputdata_t &inputdata) { if (IsAlive() == false) return; CBaseEntity *pTarget = gEntList.FindEntityByName(NULL, inputdata.value.String(), NULL, inputdata.pActivator, inputdata.pCaller); if (pTarget == NULL) { Warning("**Guard %s cannot find shove target %s\n", GetClassname(), inputdata.value.String()); m_hShoveTarget = NULL; return; } m_hShoveTarget = pTarget; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::InputSetChargeTarget(inputdata_t &inputdata) { if (!IsAlive()) return; // Pull the target & position out of the string char parseString[255]; Q_strncpy(parseString, inputdata.value.String(), sizeof(parseString)); // Get charge target name char *pszParam = strtok(parseString, " "); CBaseEntity *pTarget = gEntList.FindEntityByName(NULL, pszParam, NULL, inputdata.pActivator, inputdata.pCaller); if (!pTarget) { Warning("ERROR: Guard %s cannot find charge target '%s'\n", STRING(GetEntityName()), pszParam); return; } // Get the charge position name pszParam = strtok(NULL, " "); CBaseEntity *pPosition = gEntList.FindEntityByName(NULL, pszParam, NULL, inputdata.pActivator, inputdata.pCaller); if (!pPosition) { Warning("ERROR: Guard %s cannot find charge position '%s'\nMake sure you've specified the parameters as [target start]!\n", STRING(GetEntityName()), pszParam); return; } // Make sure we don't stack charge targets if (m_hChargeTarget) { if (GetEnemy() == m_hChargeTarget) { SetEnemy(NULL); } } SetCondition(COND_ANTLIONGUARD_HAS_CHARGE_TARGET); m_hChargeTarget = pTarget; m_hChargeTargetPosition = pPosition; } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CNPC_AntlionGuard::InputClearChargeTarget(inputdata_t &inputdata) { m_hChargeTarget = NULL; } //----------------------------------------------------------------------------- // Purpose: // Input : baseAct - // Output : Activity //----------------------------------------------------------------------------- Activity CNPC_AntlionGuard::NPC_TranslateActivity(Activity baseAct) { //See which run to use if ((baseAct == ACT_RUN) && IsCurSchedule(SCHED_ANTLIONGUARD_CHARGE)) return (Activity)ACT_ANTLIONGUARD_CHARGE_RUN; // Do extra code if we're trying to close on an enemy in a confined space (unless scripted) if (hl2_episodic.GetBool() && m_bInCavern && baseAct == ACT_RUN && IsInAScript() == false) return (Activity)ACT_ANTLIONGUARD_CHARGE_RUN; if ((baseAct == ACT_RUN) && (m_iHealth <= (m_iMaxHealth / 4))) return (Activity)ACT_ANTLIONGUARD_RUN_HURT; return baseAct; } //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CNPC_AntlionGuard::ShouldWatchEnemy(void) { Activity nActivity = GetActivity(); if ((nActivity == ACT_ANTLIONGUARD_SEARCH) || (nActivity == ACT_ANTLIONGUARD_PEEK_ENTER) || (nActivity == ACT_ANTLIONGUARD_PEEK_EXIT) || (nActivity == ACT_ANTLIONGUARD_PEEK1) || (nActivity == ACT_ANTLIONGUARD_PEEK_SIGHTED) || (nActivity == ACT_ANTLIONGUARD_SHOVE_PHYSOBJECT) || (nActivity == ACT_ANTLIONGUARD_PHYSHIT_FR) || (nActivity == ACT_ANTLIONGUARD_PHYSHIT_FL) || (nActivity == ACT_ANTLIONGUARD_PHYSHIT_RR) || (nActivity == ACT_ANTLIONGUARD_PHYSHIT_RL) || (nActivity == ACT_ANTLIONGUARD_CHARGE_CRASH) || (nActivity == ACT_ANTLIONGUARD_CHARGE_HIT) || (nActivity == ACT_ANTLIONGUARD_CHARGE_ANTICIPATION)) { return false; } return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::UpdateHead(void) { float yaw = GetPoseParameter(m_poseHead_Yaw); float pitch = GetPoseParameter(m_poseHead_Pitch); // If we should be watching our enemy, turn our head if (ShouldWatchEnemy() && (GetEnemy() != NULL)) { Vector enemyDir = GetEnemy()->WorldSpaceCenter() - WorldSpaceCenter(); VectorNormalize(enemyDir); float angle = VecToYaw(BodyDirection3D()); float angleDiff = VecToYaw(enemyDir); angleDiff = UTIL_AngleDiff(angleDiff, angle + yaw); SetPoseParameter(m_poseHead_Yaw, UTIL_Approach(yaw + angleDiff, yaw, 50)); angle = UTIL_VecToPitch(BodyDirection3D()); angleDiff = UTIL_VecToPitch(enemyDir); angleDiff = UTIL_AngleDiff(angleDiff, angle + pitch); SetPoseParameter(m_poseHead_Pitch, UTIL_Approach(pitch + angleDiff, pitch, 50)); } else { // Otherwise turn the head back to its normal position SetPoseParameter(m_poseHead_Yaw, UTIL_Approach(0, yaw, 10)); SetPoseParameter(m_poseHead_Pitch, UTIL_Approach(0, pitch, 10)); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::MaintainPhysicsTarget(void) { if (m_hPhysicsTarget == NULL || GetEnemy() == NULL) return; // Update our current target to make sure it's still valid float flTargetDistSqr = (m_hPhysicsTarget->WorldSpaceCenter() - m_vecPhysicsTargetStartPos).LengthSqr(); bool bTargetMoved = (flTargetDistSqr > Square(30 * 12.0f)); bool bEnemyCloser = ((GetEnemy()->GetAbsOrigin() - GetAbsOrigin()).LengthSqr() <= flTargetDistSqr); // Make sure this hasn't moved too far or that the player is now closer if (bTargetMoved || bEnemyCloser) { ClearCondition(COND_ANTLIONGUARD_PHYSICS_TARGET); SetCondition(COND_ANTLIONGUARD_PHYSICS_TARGET_INVALID); m_hPhysicsTarget = NULL; return; } else { SetCondition(COND_ANTLIONGUARD_PHYSICS_TARGET); ClearCondition(COND_ANTLIONGUARD_PHYSICS_TARGET_INVALID); } if (g_debug_antlionguard.GetInt() == 3) { NDebugOverlay::Cross3D(m_hPhysicsTarget->WorldSpaceCenter(), -Vector(32, 32, 32), Vector(32, 32, 32), 255, 255, 255, true, 1.0f); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::UpdatePhysicsTarget(bool bPreferObjectsAlongTargetVector, float flRadius) { if (GetEnemy() == NULL) return; // Already have a target, don't bother looking if (m_hPhysicsTarget != NULL) return; // Too soon to check again if (m_flPhysicsCheckTime > gpGlobals->curtime) return; // Attempt to find a valid shove target PhysicsObjectCriteria_t criteria; criteria.pTarget = GetEnemy(); criteria.vecCenter = GetEnemy()->GetAbsOrigin(); criteria.flRadius = flRadius; criteria.flTargetCone = ANTLIONGUARD_OBJECTFINDING_FOV; criteria.bPreferObjectsAlongTargetVector = bPreferObjectsAlongTargetVector; criteria.flNearRadius = (20 * 12); // TODO: It may preferable to disable this for legacy products as well -- jdw m_hPhysicsTarget = FindPhysicsObjectTarget(criteria); // Found one, so interrupt us if we care if (m_hPhysicsTarget != NULL) { SetCondition(COND_ANTLIONGUARD_PHYSICS_TARGET); m_vecPhysicsTargetStartPos = m_hPhysicsTarget->WorldSpaceCenter(); } // Don't search again for another second m_flPhysicsCheckTime = gpGlobals->curtime + 1.0f; } //----------------------------------------------------------------------------- // Purpose: Let the probe know I can run through small debris //----------------------------------------------------------------------------- bool CNPC_AntlionGuard::ShouldProbeCollideAgainstEntity(CBaseEntity *pEntity) { if (m_iszPhysicsPropClass != pEntity->m_iClassname) return BaseClass::ShouldProbeCollideAgainstEntity(pEntity); if (m_hPhysicsTarget == pEntity) return false; if (pEntity->GetMoveType() == MOVETYPE_VPHYSICS) { IPhysicsObject *pPhysObj = pEntity->VPhysicsGetObject(); if (pPhysObj && pPhysObj->GetMass() <= ANTLIONGUARD_MAX_OBJECT_MASS) { return false; } } return BaseClass::ShouldProbeCollideAgainstEntity(pEntity); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::PrescheduleThink(void) { BaseClass::PrescheduleThink(); // Don't do anything after death if (m_NPCState == NPC_STATE_DEAD) return; // If we're burrowed, then don't do any of this if (m_bIsBurrowed) return; // Automatically update our physics target when chasing enemies if (IsCurSchedule(SCHED_ANTLIONGUARD_CHASE_ENEMY) || IsCurSchedule(SCHED_ANTLIONGUARD_PATROL_RUN) || IsCurSchedule(SCHED_ANTLIONGUARD_CANT_ATTACK) || IsCurSchedule(SCHED_ANTLIONGUARD_CHASE_ENEMY_TOLERANCE)) { bool bCheckAlongLine = (IsCurSchedule(SCHED_ANTLIONGUARD_CHASE_ENEMY) || IsCurSchedule(SCHED_ANTLIONGUARD_CHASE_ENEMY_TOLERANCE)); UpdatePhysicsTarget(bCheckAlongLine); } else if (!IsCurSchedule(SCHED_ANTLIONGUARD_PHYSICS_ATTACK)) { ClearCondition(COND_ANTLIONGUARD_PHYSICS_TARGET); m_hPhysicsTarget = NULL; } UpdateHead(); if ((m_flGroundSpeed <= 0.0f)) { if (m_bStopped == false) { StartSounds(); float duration = random->RandomFloat(2.0f, 8.0f); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pBreathSound, 0.0f, duration); ENVELOPE_CONTROLLER.SoundChangePitch(m_pBreathSound, random->RandomInt(40, 60), duration); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pGrowlIdleSound, 0.0f, duration); ENVELOPE_CONTROLLER.SoundChangePitch(m_pGrowlIdleSound, random->RandomInt(120, 140), duration); m_flBreathTime = gpGlobals->curtime + duration - (duration*0.75f); } m_bStopped = true; if (m_flBreathTime < gpGlobals->curtime) { StartSounds(); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pGrowlIdleSound, random->RandomFloat(0.2f, 0.3f), random->RandomFloat(0.5f, 1.0f)); ENVELOPE_CONTROLLER.SoundChangePitch(m_pGrowlIdleSound, random->RandomInt(80, 120), random->RandomFloat(0.5f, 1.0f)); m_flBreathTime = gpGlobals->curtime + random->RandomFloat(1.0f, 8.0f); } } else { if (m_bStopped) { StartSounds(); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pBreathSound, 0.6f, random->RandomFloat(2.0f, 4.0f)); ENVELOPE_CONTROLLER.SoundChangePitch(m_pBreathSound, random->RandomInt(140, 160), random->RandomFloat(2.0f, 4.0f)); ENVELOPE_CONTROLLER.SoundChangeVolume(m_pGrowlIdleSound, 0.0f, 1.0f); ENVELOPE_CONTROLLER.SoundChangePitch(m_pGrowlIdleSound, random->RandomInt(90, 110), 0.2f); } m_bStopped = false; } // Put danger sounds out in front of us for (int i = 0; i < 3; i++) { CSoundEnt::InsertSound(SOUND_DANGER, WorldSpaceCenter() + (BodyDirection3D() * 128 * (i + 1)), 128, 0.1f, this); } #if ANTLIONGUARD_BLOOD_EFFECTS // compute and if necessary transmit the bleeding level for the particle effect m_iBleedingLevel = GetBleedingLevel(); #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::GatherConditions(void) { BaseClass::GatherConditions(); if (CanSummon(false)) { SetCondition(COND_ANTLIONGUARD_CAN_SUMMON); } else { ClearCondition(COND_ANTLIONGUARD_CAN_SUMMON); } // Make sure our physics target is still valid MaintainPhysicsTarget(); if (IsCurSchedule(SCHED_ANTLIONGUARD_PHYSICS_ATTACK)) { if (gpGlobals->curtime > m_flWaitFinished) { ClearCondition(COND_ANTLIONGUARD_PHYSICS_TARGET); SetCondition(COND_ANTLIONGUARD_PHYSICS_TARGET_INVALID); m_hPhysicsTarget = NULL; } } // See if we can charge the target if (GetEnemy()) { if (ShouldCharge(GetAbsOrigin(), GetEnemy()->GetAbsOrigin(), true, false)) { SetCondition(COND_ANTLIONGUARD_CAN_CHARGE); } else { ClearCondition(COND_ANTLIONGUARD_CAN_CHARGE); } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::StopLoopingSounds() { //Stop all sounds ENVELOPE_CONTROLLER.SoundDestroy(m_pGrowlHighSound); ENVELOPE_CONTROLLER.SoundDestroy(m_pGrowlIdleSound); ENVELOPE_CONTROLLER.SoundDestroy(m_pBreathSound); ENVELOPE_CONTROLLER.SoundDestroy(m_pConfusedSound); m_pGrowlHighSound = NULL; m_pGrowlIdleSound = NULL; m_pBreathSound = NULL; m_pConfusedSound = NULL; BaseClass::StopLoopingSounds(); } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CNPC_AntlionGuard::InputUnburrow(inputdata_t &inputdata) { if (IsAlive() == false) return; if (m_bIsBurrowed == false) return; m_spawnflags &= ~SF_NPC_GAG; RemoveSolidFlags(FSOLID_NOT_SOLID); AddSolidFlags(FSOLID_NOT_STANDABLE); m_takedamage = DAMAGE_YES; SetSchedule(SCHED_ANTLIONGUARD_UNBURROW); m_bIsBurrowed = false; } //------------------------------------------------------------------------------ // Purpose : Returns true is entity was remembered as unreachable. // After a time delay reachability is checked // Input : // Output : //------------------------------------------------------------------------------ bool CNPC_AntlionGuard::IsUnreachable(CBaseEntity *pEntity) { float UNREACHABLE_DIST_TOLERANCE_SQ = (240 * 240); // Note that it's ok to remove elements while I'm iterating // as long as I iterate backwards and remove them using FastRemove for (int i = m_UnreachableEnts.Size() - 1; i >= 0; i--) { // Remove any dead elements if (m_UnreachableEnts[i].hUnreachableEnt == NULL) { m_UnreachableEnts.FastRemove(i); } else if (pEntity == m_UnreachableEnts[i].hUnreachableEnt) { // Test for reachability on any elements that have timed out if (gpGlobals->curtime > m_UnreachableEnts[i].fExpireTime || pEntity->GetAbsOrigin().DistToSqr(m_UnreachableEnts[i].vLocationWhenUnreachable) > UNREACHABLE_DIST_TOLERANCE_SQ) { m_UnreachableEnts.FastRemove(i); return false; } return true; } } return false; } //----------------------------------------------------------------------------- // Purpose: Return the point at which the guard wants to stand on to knock the physics object at the target entity // Input : *pObject - Object to be shoved. // *pTarget - Target to be shoved at. // *vecTrajectory - Trajectory to our target // *flClearDistance - Distance behind the entity we're clear to use // Output : Position at which to attempt to strike the object //----------------------------------------------------------------------------- Vector CNPC_AntlionGuard::GetPhysicsHitPosition(CBaseEntity *pObject, CBaseEntity *pTarget, Vector *vecTrajectory, float *flClearDistance) { // Get the trajectory we want to knock the object along Vector vecToTarget = pTarget->WorldSpaceCenter() - pObject->WorldSpaceCenter(); VectorNormalize(vecToTarget); vecToTarget.z = 0; // Get the distance we want to be from the object when we hit it IPhysicsObject *pPhys = pObject->VPhysicsGetObject(); Vector extent = physcollision->CollideGetExtent(pPhys->GetCollide(), pObject->GetAbsOrigin(), pObject->GetAbsAngles(), -vecToTarget); float flDist = (extent - pObject->WorldSpaceCenter()).Length() + CollisionProp()->BoundingRadius() + 32.0f; if (vecTrajectory != NULL) { *vecTrajectory = vecToTarget; } if (flClearDistance != NULL) { *flClearDistance = flDist; } // Position at which we'd like to be return (pObject->WorldSpaceCenter() + (vecToTarget * -flDist)); } //----------------------------------------------------------------------------- // Purpose: See if we're able to stand on the ground at this point // Input : &vecPos - Position to try // *pOut - Result position (only valid if we return true) // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- inline bool CNPC_AntlionGuard::CanStandAtPoint(const Vector &vecPos, Vector *pOut) { Vector vecStart = vecPos + Vector(0, 0, (4 * 12)); Vector vecEnd = vecPos - Vector(0, 0, (4 * 12)); trace_t tr; bool bTraceCleared = false; // Start high and try to go lower, looking for the ground between here and there // We do this first because it's more likely to succeed in the typical guard arenas (with open terrain) UTIL_TraceHull(vecStart, vecEnd, GetHullMins(), GetHullMaxs(), MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr); if (tr.startsolid && !tr.allsolid) { // We started in solid but didn't end up there, see if we can stand where we ended up UTIL_TraceHull(tr.endpos, tr.endpos, GetHullMins(), GetHullMaxs(), MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr); // Must not be in solid bTraceCleared = (!tr.allsolid && !tr.startsolid); } else { // Must not be in solid and must have found a floor (otherwise we're potentially hanging over a ledge) bTraceCleared = (tr.allsolid == false && tr.fraction < 1.0f); } // Either we're clear or we're still unlucky if (bTraceCleared == false) { if (g_debug_antlionguard.GetInt() == 3) { NDebugOverlay::Box(vecPos, GetHullMins(), GetHullMaxs(), 255, 0, 0, 0, 15.0f); } return false; } if (pOut) { *pOut = tr.endpos; } if (g_debug_antlionguard.GetInt() == 3) { NDebugOverlay::Box((*pOut), GetHullMins(), GetHullMaxs(), 0, 255, 0, 0, 15.0f); } return true; } //----------------------------------------------------------------------------- // Purpose: Determines whether or not the guard can stand in a position to strike a specified object // Input : *pShoveObject - Object being shoved // *pTarget - Target we're shoving the object at // *pOut - The position we decide to stand at // Output : Returns true if the guard can stand and deliver. //----------------------------------------------------------------------------- bool CNPC_AntlionGuard::CanStandAtShoveTarget(CBaseEntity *pShoveObject, CBaseEntity *pTarget, Vector *pOut) { // Get the position we want to be at to swing at the object float flClearDistance; Vector vecTrajectory; Vector vecHitPosition = GetPhysicsHitPosition(pShoveObject, pTarget, &vecTrajectory, &flClearDistance); Vector vecStandPosition; if (g_debug_antlionguard.GetInt() == 3) { NDebugOverlay::HorzArrow(pShoveObject->WorldSpaceCenter(), pShoveObject->WorldSpaceCenter() + vecTrajectory * 64.0f, 16.0f, 255, 255, 0, 16, true, 15.0f); } // If we failed, try around the sides if (CanStandAtPoint(vecHitPosition, &vecStandPosition) == false) { // Get the angle (in reverse) to the target float flRad = atan2(-vecTrajectory.y, -vecTrajectory.x); float flRadOffset = DEG2RAD(45.0f); // Build an offset vector, rotating around the base Vector vecSkewTrajectory; SinCos(flRad + flRadOffset, &vecSkewTrajectory.y, &vecSkewTrajectory.x); vecSkewTrajectory.z = 0.0f; // Try to the side if (CanStandAtPoint((pShoveObject->WorldSpaceCenter() + (vecSkewTrajectory * flClearDistance)), &vecStandPosition) == false) { // Try the other side SinCos(flRad - flRadOffset, &vecSkewTrajectory.y, &vecSkewTrajectory.x); vecSkewTrajectory.z = 0.0f; if (CanStandAtPoint((pShoveObject->WorldSpaceCenter() + (vecSkewTrajectory * flClearDistance)), &vecStandPosition) == false) return false; } } // Found it, report it if (pOut != NULL) { *pOut = vecStandPosition; } return true; } //----------------------------------------------------------------------------- // Purpose: Iterate through a number of lists depending on our criteria //----------------------------------------------------------------------------- CBaseEntity *CNPC_AntlionGuard::GetNextShoveTarget(CBaseEntity *pLastEntity, AISightIter_t &iter) { // Try to find scripted items first if (m_strShoveTargets != NULL_STRING) { CBaseEntity *pFound = gEntList.FindEntityByName(pLastEntity, m_strShoveTargets); if (pFound) return pFound; } // Failing that, use our senses if (iter != (AISightIter_t)(-1)) return GetSenses()->GetNextSeenEntity(&iter); return GetSenses()->GetFirstSeenEntity(&iter, SEEN_MISC); } //----------------------------------------------------------------------------- // Purpose: Search for a physics item to swat at the player // Output : Returns the object we're going to swat. //----------------------------------------------------------------------------- CBaseEntity *CNPC_AntlionGuard::FindPhysicsObjectTarget(const PhysicsObjectCriteria_t &criteria) { // Must have a valid target entity if (criteria.pTarget == NULL) return NULL; if (g_debug_antlionguard.GetInt() == 3) { NDebugOverlay::Circle(GetAbsOrigin(), QAngle(-90, 0, 0), criteria.flRadius, 255, 0, 0, 8, true, 2.0f); } // Get the vector to our target, from ourself Vector vecDirToTarget = criteria.pTarget->GetAbsOrigin() - GetAbsOrigin(); VectorNormalize(vecDirToTarget); vecDirToTarget.z = 0; // Cost is determined by distance to the object, modified by how "in line" it is with our target direction of travel // Use the distance to the player as the base cost for throwing an object (avoids pushing things too close to the player) float flLeastCost = (criteria.bPreferObjectsAlongTargetVector) ? (criteria.pTarget->GetAbsOrigin() - GetAbsOrigin()).LengthSqr() : Square(criteria.flRadius); float flCost; AISightIter_t iter = (AISightIter_t)(-1); CBaseEntity *pObject = NULL; CBaseEntity *pNearest = NULL; Vector vecBestHitPosition = vec3_origin; // Look through the list of sensed objects for possible targets while ((pObject = GetNextShoveTarget(pObject, iter)) != NULL) { // If we couldn't shove this object last time, don't try again if (m_FailedPhysicsTargets.Find(pObject) != m_FailedPhysicsTargets.InvalidIndex()) continue; // Ignore things less than half a foot in diameter if (pObject->CollisionProp()->BoundingRadius() < 6.0f) continue; IPhysicsObject *pPhysObj = pObject->VPhysicsGetObject(); if (pPhysObj == NULL) continue; // Ignore motion disabled props if (pPhysObj->IsMoveable() == false) continue; // Ignore things lighter than 5kg if (pPhysObj->GetMass() < 5.0f) continue; // Ignore objects moving too quickly (they'll be too hard to catch otherwise) Vector velocity; pPhysObj->GetVelocity(&velocity, NULL); if (velocity.LengthSqr() > (16 * 16)) continue; // Get the direction from us to the physics object Vector vecDirToObject = pObject->WorldSpaceCenter() - GetAbsOrigin(); VectorNormalize(vecDirToObject); vecDirToObject.z = 0; Vector vecObjCenter = pObject->WorldSpaceCenter(); float flDistSqr = 0.0f; float flDot = 0.0f; // If we want to find things along the vector to the target, do so if (criteria.bPreferObjectsAlongTargetVector) { // Object must be closer than our target if ((GetAbsOrigin() - vecObjCenter).LengthSqr() > (GetAbsOrigin() - criteria.pTarget->GetAbsOrigin()).LengthSqr()) continue; // Calculate a "cost" to this object flDistSqr = (GetAbsOrigin() - vecObjCenter).LengthSqr(); flDot = DotProduct(vecDirToTarget, vecDirToObject); // Ignore things outside our allowed cone if (flDot < criteria.flTargetCone) continue; // The more perpendicular we are, the higher the cost float flCostScale = RemapValClamped(flDot, 1.0f, criteria.flTargetCone, 1.0f, 4.0f); flCost = flDistSqr * flCostScale; } else { // Straight distance cost flCost = (criteria.vecCenter - vecObjCenter).LengthSqr(); } // This must be a less costly object to use if (flCost >= flLeastCost) { if (g_debug_antlionguard.GetInt() == 3) { NDebugOverlay::Box(vecObjCenter, -Vector(16, 16, 16), Vector(16, 16, 16), 255, 0, 0, 0, 2.0f); } continue; } // Check for a (roughly) clear trajectory path from the object to target trace_t tr; UTIL_TraceLine(vecObjCenter, criteria.pTarget->BodyTarget(vecObjCenter), MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr); // See how close to our target we got (we still look good hurling things that won't necessarily hit) if ((tr.endpos - criteria.pTarget->WorldSpaceCenter()).LengthSqr() > Square(criteria.flNearRadius)) continue; // Must be able to stand at a position to hit the object Vector vecHitPosition; if (CanStandAtShoveTarget(pObject, criteria.pTarget, &vecHitPosition) == false) { if (g_debug_antlionguard.GetInt() == 3) { NDebugOverlay::HorzArrow(GetAbsOrigin(), pObject->WorldSpaceCenter(), 32.0f, 255, 0, 0, 64, true, 2.0f); } continue; } // Take this as the best object so far pNearest = pObject; flLeastCost = flCost; vecBestHitPosition = vecHitPosition; if (g_debug_antlionguard.GetInt() == 3) { NDebugOverlay::HorzArrow(GetAbsOrigin(), pObject->WorldSpaceCenter(), 16.0f, 255, 255, 0, 0, true, 2.0f); } } // Set extra info if we've succeeded if (pNearest != NULL) { m_vecPhysicsHitPosition = vecBestHitPosition; if (g_debug_antlionguard.GetInt() == 3) { NDebugOverlay::HorzArrow(GetAbsOrigin(), pNearest->WorldSpaceCenter(), 32.0f, 0, 255, 0, 128, true, 2.0f); } } return pNearest; } //----------------------------------------------------------------------------- // 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_AntlionGuard::BuildScheduleTestBits(void) { BaseClass::BuildScheduleTestBits(); // Interrupt if we can shove something if (IsCurSchedule(SCHED_ANTLIONGUARD_CHASE_ENEMY)) { SetCustomInterruptCondition(COND_ANTLIONGUARD_PHYSICS_TARGET); SetCustomInterruptCondition(COND_ANTLIONGUARD_CAN_SUMMON); } // Interrupt if we've been given a charge target if (IsCurSchedule(SCHED_ANTLIONGUARD_CHARGE) == false) { SetCustomInterruptCondition(COND_ANTLIONGUARD_HAS_CHARGE_TARGET); } // Once we commit to doing this, just do it! if (IsCurSchedule(SCHED_MELEE_ATTACK1)) { ClearCustomInterruptCondition(COND_ENEMY_OCCLUDED); } // Always take heavy damage SetCustomInterruptCondition(COND_HEAVY_DAMAGE); } //----------------------------------------------------------------------------- // Purpose: // Input : &origin - // radius - // magnitude - //----------------------------------------------------------------------------- void CNPC_AntlionGuard::ImpactShock(const Vector &origin, float radius, float magnitude, CBaseEntity *pIgnored) { // Also do a local physics explosion to push objects away float adjustedDamage, flDist; Vector vecSpot; float falloff = 1.0f / 2.5f; CBaseEntity *pEntity = NULL; // Find anything within our radius while ((pEntity = gEntList.FindEntityInSphere(pEntity, origin, radius)) != NULL) { // Don't affect the ignored target if (pEntity == pIgnored) continue; if (pEntity == this) continue; // UNDONE: Ask the object if it should get force if it's not MOVETYPE_VPHYSICS? if (pEntity->GetMoveType() == MOVETYPE_VPHYSICS || (pEntity->VPhysicsGetObject() && pEntity->IsPlayer() == false)) { vecSpot = pEntity->BodyTarget(GetAbsOrigin()); // decrease damage for an ent that's farther from the bomb. flDist = (GetAbsOrigin() - vecSpot).Length(); if (radius == 0 || flDist <= radius) { adjustedDamage = flDist * falloff; adjustedDamage = magnitude - adjustedDamage; if (adjustedDamage < 1) { adjustedDamage = 1; } CTakeDamageInfo info(this, this, adjustedDamage, DMG_BLAST); CalculateExplosiveDamageForce(&info, (vecSpot - GetAbsOrigin()), GetAbsOrigin()); pEntity->VPhysicsTakeDamage(info); } } } } //----------------------------------------------------------------------------- // Purpose: // Input : *pTarget - //----------------------------------------------------------------------------- void CNPC_AntlionGuard::ChargeDamage(CBaseEntity *pTarget) { if (pTarget == NULL) return; CBasePlayer *pPlayer = ToBasePlayer(pTarget); if (pPlayer != NULL) { //Kick the player angles pPlayer->ViewPunch(QAngle(20, 20, -30)); Vector dir = pPlayer->WorldSpaceCenter() - WorldSpaceCenter(); VectorNormalize(dir); dir.z = 0.0f; Vector vecNewVelocity = dir * 250.0f; vecNewVelocity[2] += 128.0f; pPlayer->SetAbsVelocity(vecNewVelocity); color32 red = { 128, 0, 0, 128 }; UTIL_ScreenFade(pPlayer, red, 1.0f, 0.1f, FFADE_IN); } // Player takes less damage float flDamage = (pPlayer == NULL) ? 250 : sk_antlionguard_dmg_charge.GetFloat(); // If it's being held by the player, break that bond Pickup_ForcePlayerToDropThisObject(pTarget); // Calculate the physics force ApplyChargeDamage(this, pTarget, flDamage); } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CNPC_AntlionGuard::InputRagdoll(inputdata_t &inputdata) { if (IsAlive() == false) return; //Set us to nearly dead so the velocity from death is minimal SetHealth(1); CTakeDamageInfo info(this, this, GetHealth(), DMG_CRUSH); BaseClass::TakeDamage(info); } //----------------------------------------------------------------------------- // Purpose: make m_bPreferPhysicsAttack true //----------------------------------------------------------------------------- void CNPC_AntlionGuard::InputEnablePreferPhysicsAttack(inputdata_t &inputdata) { m_bPreferPhysicsAttack = true; } //----------------------------------------------------------------------------- // Purpose: make m_bPreferPhysicsAttack false //----------------------------------------------------------------------------- void CNPC_AntlionGuard::InputDisablePreferPhysicsAttack(inputdata_t &inputdata) { m_bPreferPhysicsAttack = false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CNPC_AntlionGuard::SelectFailSchedule(int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode) { // Figure out what to do next if (failedSchedule == SCHED_ANTLIONGUARD_CHASE_ENEMY && HasCondition(COND_ENEMY_UNREACHABLE)) return SelectUnreachableSchedule(); return BaseClass::SelectFailSchedule(failedSchedule, failedTask, taskFailCode); } //----------------------------------------------------------------------------- // Purpose: // Input : scheduleType - // Output : int //----------------------------------------------------------------------------- int CNPC_AntlionGuard::TranslateSchedule(int scheduleType) { switch (scheduleType) { case SCHED_CHASE_ENEMY: return SCHED_ANTLIONGUARD_CHASE_ENEMY; break; } return BaseClass::TranslateSchedule(scheduleType); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::StartSounds(void) { //Initialize the additive sound channels CPASAttenuationFilter filter(this); if (m_pGrowlHighSound == NULL) { m_pGrowlHighSound = ENVELOPE_CONTROLLER.SoundCreate(filter, entindex(), CHAN_VOICE, "NPC_AntlionGuard.GrowlHigh", ATTN_NORM); if (m_pGrowlHighSound) { ENVELOPE_CONTROLLER.Play(m_pGrowlHighSound, 0.0f, 100); } } if (m_pGrowlIdleSound == NULL) { m_pGrowlIdleSound = ENVELOPE_CONTROLLER.SoundCreate(filter, entindex(), CHAN_STATIC, "NPC_AntlionGuard.GrowlIdle", ATTN_NORM); if (m_pGrowlIdleSound) { ENVELOPE_CONTROLLER.Play(m_pGrowlIdleSound, 0.0f, 100); } } if (m_pBreathSound == NULL) { m_pBreathSound = ENVELOPE_CONTROLLER.SoundCreate(filter, entindex(), CHAN_ITEM, "NPC_AntlionGuard.BreathSound", ATTN_NORM); if (m_pBreathSound) { ENVELOPE_CONTROLLER.Play(m_pBreathSound, 0.0f, 100); } } if (m_pConfusedSound == NULL) { m_pConfusedSound = ENVELOPE_CONTROLLER.SoundCreate(filter, entindex(), CHAN_WEAPON, "NPC_AntlionGuard.Confused", ATTN_NORM); if (m_pConfusedSound) { ENVELOPE_CONTROLLER.Play(m_pConfusedSound, 0.0f, 100); } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::InputEnableBark(inputdata_t &inputdata) { m_bBarkEnabled = true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::InputDisableBark(inputdata_t &inputdata) { m_bBarkEnabled = false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AntlionGuard::DeathSound(const CTakeDamageInfo &info) { EmitSound("NPC_AntlionGuard.Die"); } //----------------------------------------------------------------------------- // Purpose: // Input : &info - //----------------------------------------------------------------------------- void CNPC_AntlionGuard::Event_Killed(const CTakeDamageInfo &info) { BaseClass::Event_Killed(info); // Tell all of my antlions to burrow away, 'cos they fear the Freeman if (m_iNumLiveAntlions) { CBaseEntity *pSearch = NULL; // Iterate through all antlions and see if there are any orphans while ((pSearch = gEntList.FindEntityByClassname(pSearch, "npc_antlion")) != NULL) { CNPC_Antlion *pAntlion = assert_cast(pSearch); // See if it's a live orphan if (pAntlion && pAntlion->GetOwnerEntity() == NULL && pAntlion->IsAlive()) { g_EventQueue.AddEvent(pAntlion, "BurrowAway", RandomFloat(0.1, 2.0), this, this); } } } DestroyGlows(); // If I'm bleeding, stop due to decreased pressure of hemolymph after // cessation of aortic contraction #if ANTLIONGUARD_BLOOD_EFFECTS m_iBleedingLevel = 0; #endif } //----------------------------------------------------------------------------- // Purpose: Don't become a ragdoll until we've finished our death anim // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CNPC_AntlionGuard::CanBecomeRagdoll(void) { return true; //GetState() == SCHED_DIE_RAGDOLL //if (IsCurSchedule(SCHED_DIE_RAGDOLL)) //return true; //if (IsCurSchedule(SCHED_DIE)) //return true; //if (Q_strstr(STRING(gpGlobals->mapname), "ep1") || Q_strstr(STRING(gpGlobals->mapname), "ep1")) //return true; //return false; //return hl2_episodic.GetBool(); } bool CNPC_AntlionGuard::CanBecomeServersideRagdoll() { return false; } //----------------------------------------------------------------------------- // Purpose: // Input : &force - // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CNPC_AntlionGuard::BecomeRagdollOnClient(const Vector &force) { if (!CanBecomeRagdoll()) return false; EmitSound("NPC_AntlionGuard.Fallover"); // Become server-side ragdoll if we're flagged to do it if (m_spawnflags & SF_ANTLIONGUARD_SERVERSIDE_RAGDOLL) { CTakeDamageInfo info; // Fake the info info.SetDamageType(DMG_GENERIC); info.SetDamageForce(force); info.SetDamagePosition(WorldSpaceCenter()); CBaseEntity *pRagdoll = CreateServerRagdoll(this, 0, info, COLLISION_GROUP_NONE); // Transfer our name to the new ragdoll pRagdoll->SetName(GetEntityName()); pRagdoll->SetCollisionGroup(COLLISION_GROUP_DEBRIS); // Get rid of our old body UTIL_Remove(this); return true; } return BaseClass::BecomeRagdollOnClient(force); } //----------------------------------------------------------------------------- // Purpose: Override how we face our target as we move // Output : //----------------------------------------------------------------------------- bool CNPC_AntlionGuard::OverrideMoveFacing(const AILocalMoveGoal_t &move, float flInterval) { Vector vecFacePosition = vec3_origin; CBaseEntity *pFaceTarget = NULL; bool bFaceTarget = false; // FIXME: this will break scripted sequences that walk when they have an enemy if (m_hChargeTarget) { vecFacePosition = m_hChargeTarget->GetAbsOrigin(); pFaceTarget = m_hChargeTarget; bFaceTarget = true; } #ifdef HL2_EPISODIC else if (GetEnemy() && IsCurSchedule(SCHED_ANTLIONGUARD_CANT_ATTACK)) { // Always face our enemy when randomly patrolling around vecFacePosition = GetEnemy()->EyePosition(); pFaceTarget = GetEnemy(); bFaceTarget = true; } #endif // HL2_EPISODIC else if (GetEnemy() && GetNavigator()->GetMovementActivity() == ACT_RUN) { Vector vecEnemyLKP = GetEnemyLKP(); // Only start facing when we're close enough if ((UTIL_DistApprox(vecEnemyLKP, GetAbsOrigin()) < 512) || IsCurSchedule(SCHED_ANTLIONGUARD_PATROL_RUN)) { vecFacePosition = vecEnemyLKP; pFaceTarget = GetEnemy(); bFaceTarget = true; } } // Face if (bFaceTarget) { AddFacingTarget(pFaceTarget, vecFacePosition, 1.0, 0.2); } return BaseClass::OverrideMoveFacing(move, flInterval); } //----------------------------------------------------------------------------- // Purpose: // Input : &info - // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CNPC_AntlionGuard::IsHeavyDamage(const CTakeDamageInfo &info) { // Struck by blast if (info.GetDamageType() & DMG_BLAST) { if (info.GetDamage() > MIN_BLAST_DAMAGE) return true; } // Struck by large object if (info.GetDamageType() & DMG_CRUSH) { IPhysicsObject *pPhysObject = info.GetInflictor()->VPhysicsGetObject(); if ((pPhysObject != NULL) && (pPhysObject->GetGameFlags() & FVPHYSICS_WAS_THROWN)) { // Always take hits from a combine ball if (UTIL_IsAR2CombineBall(info.GetInflictor())) return true; // If we're under half health, stop being interrupted by heavy damage if (GetHealth() < (GetMaxHealth() * 0.25)) return false; // Ignore physics damages that don't do much damage if (info.GetDamage() < MIN_CRUSH_DAMAGE) return false; return true; } return false; } return false; } //----------------------------------------------------------------------------- // Purpose: // Input : &info - // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CNPC_AntlionGuard::IsLightDamage(const CTakeDamageInfo &info) { return false; } //----------------------------------------------------------------------------- // Purpose: // Input : *pChild - //----------------------------------------------------------------------------- void CNPC_AntlionGuard::InputSummonedAntlionDied(inputdata_t &inputdata) { m_iNumLiveAntlions--; Assert(m_iNumLiveAntlions >= 0); if (g_debug_antlionguard.GetInt() == 2) { Msg("Guard summoned antlion count: %d\n", m_iNumLiveAntlions); } } //----------------------------------------------------------------------------- // Purpose: Filter out sounds we don't care about // Input : *pSound - sound to test against // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CNPC_AntlionGuard::QueryHearSound(CSound *pSound) { // Don't bother with danger sounds from antlions or other guards if (pSound->SoundType() == SOUND_DANGER && (pSound->m_hOwner != NULL && pSound->m_hOwner->Classify() == CLASS_ANTLION)) return false; return BaseClass::QueryHearSound(pSound); } #if HL2_EPISODIC //--------------------------------------------------------- // Prevent the cavern guard from using stopping paths, as it occasionally forces him off the navmesh. //--------------------------------------------------------- bool CNPC_AntlionGuard::CNavigator::GetStoppingPath(CAI_WaypointList *pClippedWaypoints) { if (GetOuter()->m_bInCavern) { return false; } else { return BaseClass::GetStoppingPath(pClippedWaypoints); } } #endif //----------------------------------------------------------------------------- // Purpose: // Input : *pTarget - // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CNPC_AntlionGuard::RememberFailedPhysicsTarget(CBaseEntity *pTarget) { // Already in the list? if (m_FailedPhysicsTargets.Find(pTarget) != m_FailedPhysicsTargets.InvalidIndex()) return true; // We're not holding on to any more if ((m_FailedPhysicsTargets.Count() + 1) > MAX_FAILED_PHYSOBJECTS) return false; m_FailedPhysicsTargets.AddToTail(pTarget); return true; } //----------------------------------------------------------------------------- // Purpose: Handle squad or NPC interactions // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CNPC_AntlionGuard::HandleInteraction(int interactionType, void *data, CBaseCombatCharacter *sender) { // Don't chase targets that other guards in our squad may be going after! if (interactionType == g_interactionAntlionGuardFoundPhysicsObject) { RememberFailedPhysicsTarget((CBaseEntity *)data); return true; } // Mark a shoved object as being free to pursue again if (interactionType == g_interactionAntlionGuardShovedPhysicsObject) { CBaseEntity *pObject = (CBaseEntity *)data; m_FailedPhysicsTargets.FindAndRemove(pObject); return true; } return BaseClass::HandleInteraction(interactionType, data, sender); } //----------------------------------------------------------------------------- // Purpose: Cache whatever pose parameters we intend to use //----------------------------------------------------------------------------- void CNPC_AntlionGuard::PopulatePoseParameters(void) { m_poseThrow = LookupPoseParameter("throw"); m_poseHead_Pitch = LookupPoseParameter("head_pitch"); m_poseHead_Yaw = LookupPoseParameter("head_yaw"); BaseClass::PopulatePoseParameters(); } #if ANTLIONGUARD_BLOOD_EFFECTS //----------------------------------------------------------------------------- // Purpose: Return desired level for the continuous bleeding effect (not the // individual blood spurts you see per bullet hit) // Return 0 for don't bleed, // 1 for mild bleeding // 2 for severe bleeding //----------------------------------------------------------------------------- unsigned char CNPC_AntlionGuard::GetBleedingLevel(void) const { if (m_iHealth > (m_iMaxHealth >> 1)) { // greater than 50% return 0; } else if (m_iHealth > (m_iMaxHealth >> 2)) { // less than 50% but greater than 25% return 1; } else { return 2; } } #endif //----------------------------------------------------------------------------- // // Schedules // //----------------------------------------------------------------------------- AI_BEGIN_CUSTOM_NPC(npc_antlionguard, CNPC_AntlionGuard) // Interactions DECLARE_INTERACTION(g_interactionAntlionGuardFoundPhysicsObject) DECLARE_INTERACTION(g_interactionAntlionGuardShovedPhysicsObject) // Squadslots DECLARE_SQUADSLOT(SQUAD_SLOT_ANTLIONGUARD_CHARGE) //Tasks DECLARE_TASK(TASK_ANTLIONGUARD_CHARGE) DECLARE_TASK(TASK_ANTLIONGUARD_GET_PATH_TO_PHYSOBJECT) DECLARE_TASK(TASK_ANTLIONGUARD_SHOVE_PHYSOBJECT) DECLARE_TASK(TASK_ANTLIONGUARD_SUMMON) DECLARE_TASK(TASK_ANTLIONGUARD_SET_FLINCH_ACTIVITY) DECLARE_TASK(TASK_ANTLIONGUARD_GET_PATH_TO_CHARGE_POSITION) DECLARE_TASK(TASK_ANTLIONGUARD_GET_PATH_TO_NEAREST_NODE) DECLARE_TASK(TASK_ANTLIONGUARD_GET_CHASE_PATH_ENEMY_TOLERANCE) DECLARE_TASK(TASK_ANTLIONGUARD_OPPORTUNITY_THROW) DECLARE_TASK(TASK_ANTLIONGUARD_FIND_PHYSOBJECT) //Activities DECLARE_ACTIVITY(ACT_ANTLIONGUARD_SEARCH) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_PEEK_FLINCH) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_PEEK_ENTER) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_PEEK_EXIT) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_PEEK1) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_BARK) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_PEEK_SIGHTED) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_CHARGE_START) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_CHARGE_CANCEL) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_CHARGE_RUN) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_CHARGE_CRASH) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_CHARGE_STOP) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_CHARGE_HIT) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_CHARGE_ANTICIPATION) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_SHOVE_PHYSOBJECT) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_FLINCH_LIGHT) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_UNBURROW) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_ROAR) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_RUN_HURT) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_PHYSHIT_FR) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_PHYSHIT_FL) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_PHYSHIT_RR) DECLARE_ACTIVITY(ACT_ANTLIONGUARD_PHYSHIT_RL) //Adrian: events go here DECLARE_ANIMEVENT(AE_ANTLIONGUARD_CHARGE_HIT) DECLARE_ANIMEVENT(AE_ANTLIONGUARD_SHOVE_PHYSOBJECT) DECLARE_ANIMEVENT(AE_ANTLIONGUARD_SHOVE) DECLARE_ANIMEVENT(AE_ANTLIONGUARD_FOOTSTEP_LIGHT) DECLARE_ANIMEVENT(AE_ANTLIONGUARD_FOOTSTEP_HEAVY) DECLARE_ANIMEVENT(AE_ANTLIONGUARD_CHARGE_EARLYOUT) DECLARE_ANIMEVENT(AE_ANTLIONGUARD_VOICE_GROWL) DECLARE_ANIMEVENT(AE_ANTLIONGUARD_VOICE_BARK) DECLARE_ANIMEVENT(AE_ANTLIONGUARD_VOICE_PAIN) DECLARE_ANIMEVENT(AE_ANTLIONGUARD_VOICE_SQUEEZE) DECLARE_ANIMEVENT(AE_ANTLIONGUARD_VOICE_SCRATCH) DECLARE_ANIMEVENT(AE_ANTLIONGUARD_VOICE_GRUNT) DECLARE_ANIMEVENT(AE_ANTLIONGUARD_BURROW_OUT) DECLARE_ANIMEVENT(AE_ANTLIONGUARD_VOICE_ROAR) DECLARE_CONDITION(COND_ANTLIONGUARD_PHYSICS_TARGET) DECLARE_CONDITION(COND_ANTLIONGUARD_PHYSICS_TARGET_INVALID) DECLARE_CONDITION(COND_ANTLIONGUARD_HAS_CHARGE_TARGET) DECLARE_CONDITION(COND_ANTLIONGUARD_CAN_SUMMON) DECLARE_CONDITION(COND_ANTLIONGUARD_CAN_CHARGE) //================================================== // SCHED_ANTLIONGUARD_SUMMON //================================================== DEFINE_SCHEDULE ( SCHED_ANTLIONGUARD_SUMMON, " Tasks" " TASK_STOP_MOVING 0" " TASK_FACE_ENEMY 0" " TASK_PLAY_SEQUENCE ACTIVITY:ACT_ANTLIONGUARD_BARK" " TASK_ANTLIONGUARD_SUMMON 0" " TASK_ANTLIONGUARD_OPPORTUNITY_THROW 0" " " " Interrupts" " COND_HEAVY_DAMAGE" ) //================================================== // SCHED_ANTLIONGUARD_CHARGE //================================================== DEFINE_SCHEDULE ( SCHED_ANTLIONGUARD_CHARGE, " Tasks" " TASK_STOP_MOVING 0" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ANTLIONGUARD_CHASE_ENEMY" " TASK_FACE_ENEMY 0" " TASK_ANTLIONGUARD_CHARGE 0" "" " Interrupts" " COND_TASK_FAILED" " COND_HEAVY_DAMAGE" // These are deliberately left out so they can be detected during the // charge Task and correctly play the charge stop animation. //" COND_NEW_ENEMY" //" COND_ENEMY_DEAD" //" COND_LOST_ENEMY" ) //================================================== // SCHED_ANTLIONGUARD_CHARGE_TARGET //================================================== DEFINE_SCHEDULE ( SCHED_ANTLIONGUARD_CHARGE_TARGET, " Tasks" " TASK_STOP_MOVING 0" " TASK_FACE_ENEMY 0" " TASK_ANTLIONGUARD_CHARGE 0" "" " Interrupts" " COND_TASK_FAILED" " COND_ENEMY_DEAD" " COND_HEAVY_DAMAGE" ) //================================================== // SCHED_ANTLIONGUARD_CHARGE_SMASH //================================================== DEFINE_SCHEDULE ( SCHED_ANTLIONGUARD_CHARGE_CRASH, " Tasks" " TASK_STOP_MOVING 0" " TASK_PLAY_SEQUENCE ACTIVITY:ACT_ANTLIONGUARD_CHARGE_CRASH" "" " Interrupts" " COND_TASK_FAILED" " COND_HEAVY_DAMAGE" ) //================================================== // SCHED_ANTLIONGUARD_PHYSICS_ATTACK //================================================== DEFINE_SCHEDULE ( SCHED_ANTLIONGUARD_PHYSICS_ATTACK, " Tasks" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ANTLIONGUARD_CHASE_ENEMY" " TASK_ANTLIONGUARD_GET_PATH_TO_PHYSOBJECT 0" " TASK_RUN_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_FACE_ENEMY 0" " TASK_ANTLIONGUARD_SHOVE_PHYSOBJECT 0" "" " Interrupts" " COND_TASK_FAILED" " COND_ENEMY_DEAD" " COND_LOST_ENEMY" " COND_NEW_ENEMY" " COND_ANTLIONGUARD_PHYSICS_TARGET_INVALID" " COND_HEAVY_DAMAGE" ) //================================================== // SCHED_FORCE_ANTLIONGUARD_PHYSICS_ATTACK //================================================== DEFINE_SCHEDULE ( SCHED_FORCE_ANTLIONGUARD_PHYSICS_ATTACK, " Tasks" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ANTLIONGUARD_CANT_ATTACK" " TASK_ANTLIONGUARD_FIND_PHYSOBJECT 0" " TASK_SET_SCHEDULE SCHEDULE:SCHED_ANTLIONGUARD_PHYSICS_ATTACK" "" " Interrupts" " COND_ANTLIONGUARD_PHYSICS_TARGET" " COND_HEAVY_DAMAGE" ) //================================================== // SCHED_ANTLIONGUARD_CANT_ATTACK // If we're here, the guard can't chase enemy, can't find a physobject to attack with, and can't summon //================================================== #ifdef HL2_EPISODIC DEFINE_SCHEDULE ( SCHED_ANTLIONGUARD_CANT_ATTACK, " Tasks" " TASK_SET_ROUTE_SEARCH_TIME 2" // Spend 5 seconds trying to build a path if stuck " TASK_GET_PATH_TO_RANDOM_NODE 1024" " TASK_WALK_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_WAIT_PVS 0" "" " Interrupts" " COND_GIVE_WAY" " COND_NEW_ENEMY" " COND_ANTLIONGUARD_PHYSICS_TARGET" " COND_HEAVY_DAMAGE" ) #else DEFINE_SCHEDULE ( SCHED_ANTLIONGUARD_CANT_ATTACK, " Tasks" " TASK_WAIT 5" "" " Interrupts" ) #endif //================================================== // SCHED_ANTLIONGUARD_PHYSICS_DAMAGE_HEAVY //================================================== DEFINE_SCHEDULE ( SCHED_ANTLIONGUARD_PHYSICS_DAMAGE_HEAVY, " Tasks" " TASK_STOP_MOVING 0" " TASK_RESET_ACTIVITY 0" " TASK_ANTLIONGUARD_SET_FLINCH_ACTIVITY 0" "" " Interrupts" ) //================================================== // SCHED_ANTLIONGUARD_UNBURROW //================================================== DEFINE_SCHEDULE ( SCHED_ANTLIONGUARD_UNBURROW, " Tasks" " TASK_PLAY_SEQUENCE ACTIVITY:ACT_ANTLIONGUARD_UNBURROW" "" " Interrupts" ) //================================================== // SCHED_ANTLIONGUARD_CHARGE_CANCEL //================================================== DEFINE_SCHEDULE ( SCHED_ANTLIONGUARD_CHARGE_CANCEL, " Tasks" " TASK_PLAY_SEQUENCE ACTIVITY:ACT_ANTLIONGUARD_CHARGE_CANCEL" "" " Interrupts" ) //================================================== // SCHED_ANTLIONGUARD_FIND_CHARGE_POSITION //================================================== DEFINE_SCHEDULE ( SCHED_ANTLIONGUARD_FIND_CHARGE_POSITION, " Tasks" " TASK_ANTLIONGUARD_GET_PATH_TO_CHARGE_POSITION 0" " TASK_RUN_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " " " Interrupts" " COND_ENEMY_DEAD" " COND_GIVE_WAY" " COND_TASK_FAILED" " COND_HEAVY_DAMAGE" ) //========================================================= // > SCHED_ANTLIONGUARD_CHASE_ENEMY_TOLERANCE //========================================================= DEFINE_SCHEDULE ( SCHED_ANTLIONGUARD_CHASE_ENEMY_TOLERANCE, " Tasks" " TASK_STOP_MOVING 0" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ANTLIONGUARD_PATROL_RUN" " TASK_ANTLIONGUARD_GET_PATH_TO_NEAREST_NODE 500" " TASK_RUN_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_FACE_ENEMY 0" "" " Interrupts" " COND_TASK_FAILED" " COND_CAN_MELEE_ATTACK1" " COND_GIVE_WAY" " COND_NEW_ENEMY" " COND_ANTLIONGUARD_CAN_SUMMON" " COND_ANTLIONGUARD_PHYSICS_TARGET" " COND_HEAVY_DAMAGE" " COND_ANTLIONGUARD_CAN_CHARGE" ); //========================================================= // > PATROL_RUN //========================================================= DEFINE_SCHEDULE ( SCHED_ANTLIONGUARD_PATROL_RUN, " Tasks" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ANTLIONGUARD_CANT_ATTACK" " TASK_SET_ROUTE_SEARCH_TIME 3" // Spend 3 seconds trying to build a path if stuck " TASK_ANTLIONGUARD_GET_PATH_TO_NEAREST_NODE 500" " TASK_RUN_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" "" " Interrupts" " COND_TASK_FAILED" " COND_CAN_MELEE_ATTACK1" " COND_GIVE_WAY" " COND_NEW_ENEMY" " COND_ANTLIONGUARD_PHYSICS_TARGET" " COND_ANTLIONGUARD_CAN_SUMMON" " COND_HEAVY_DAMAGE" " COND_ANTLIONGUARD_CAN_CHARGE" ); //================================================== // SCHED_ANTLIONGUARD_ROAR //================================================== DEFINE_SCHEDULE ( SCHED_ANTLIONGUARD_ROAR, " Tasks" " TASK_STOP_MOVING 0" " TASK_FACE_ENEMY 0" " TASK_PLAY_SEQUENCE ACTIVITY:ACT_ANTLIONGUARD_ROAR" " " " Interrupts" " COND_HEAVY_DAMAGE" ) //================================================== // SCHED_ANTLIONGUARD_TAKE_COVER_FROM_ENEMY //================================================== DEFINE_SCHEDULE ( SCHED_ANTLIONGUARD_TAKE_COVER_FROM_ENEMY, " Tasks" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ANTLIONGUARD_CANT_ATTACK" " TASK_FIND_COVER_FROM_ENEMY 0" " TASK_RUN_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_STOP_MOVING 0" "" " Interrupts" " COND_TASK_FAILED" " COND_NEW_ENEMY" " COND_ENEMY_DEAD" " COND_ANTLIONGUARD_PHYSICS_TARGET" " COND_ANTLIONGUARD_CAN_SUMMON" " COND_HEAVY_DAMAGE" ) //========================================================= // SCHED_ANTLIONGUARD_CHASE_ENEMY //========================================================= DEFINE_SCHEDULE ( SCHED_ANTLIONGUARD_CHASE_ENEMY, " Tasks" " TASK_STOP_MOVING 0" " TASK_GET_CHASE_PATH_TO_ENEMY 300" " TASK_RUN_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_FACE_ENEMY 0" "" " Interrupts" " COND_NEW_ENEMY" " COND_ENEMY_DEAD" " COND_ENEMY_UNREACHABLE" " 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_LOST_ENEMY" " COND_HEAVY_DAMAGE" " COND_ANTLIONGUARD_CAN_CHARGE" ) AI_END_CUSTOM_NPC()