#include "cbase.h" #include "asw_queen.h" #include "asw_queen_spit.h" #include "te_effect_dispatch.h" #include "npc_bullseye.h" #include "npcevent.h" #include "asw_marine.h" #include "asw_marine_resource.h" #include "asw_parasite.h" #include "asw_buzzer.h" #include "asw_game_resource.h" #include "asw_gamerules.h" #include "soundenvelope.h" #include "ai_memory.h" #include "ai_moveprobe.h" #include "asw_util_shared.h" #include "asw_queen_divers_shared.h" #include "asw_queen_grabber_shared.h" #include "asw_colonist.h" #include "ndebugoverlay.h" #include "asw_weapon_assault_shotgun_shared.h" #include "asw_sentry_base.h" #include "props.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" #define SWARM_QUEEN_MODEL "models/swarm/Queen/Queen.mdl" //#define SWARM_QUEEN_MODEL "models/antlion_guard.mdl" #define ASW_QUEEN_MAX_ATTACK_DISTANCE 1500 // define this to make the queen not move/turn //#define ASW_QUEEN_STATIONARY LINK_ENTITY_TO_CLASS( asw_queen, CASW_Queen ); IMPLEMENT_SERVERCLASS_ST( CASW_Queen, DT_ASW_Queen ) SendPropExclude( "DT_BaseAnimating", "m_flPoseParameter" ), SendPropEHandle( SENDINFO ( m_hQueenEnemy ) ), SendPropBool( SENDINFO(m_bChestOpen) ), SendPropInt( SENDINFO(m_iMaxHealth), 14, SPROP_UNSIGNED ), END_SEND_TABLE() BEGIN_DATADESC( CASW_Queen ) DEFINE_FIELD( m_angQueenFacing, FIELD_VECTOR ), DEFINE_FIELD( m_vecLastClawPos, FIELD_VECTOR ), DEFINE_FIELD( m_bChestOpen, FIELD_BOOLEAN ), DEFINE_FIELD( m_fLastHeadYaw, FIELD_FLOAT ), DEFINE_FIELD( m_fLastShieldPose, FIELD_FLOAT ), DEFINE_FIELD( m_iSpitNum, FIELD_INTEGER ), DEFINE_FIELD( m_iDiverState, FIELD_INTEGER ), DEFINE_FIELD( m_fLastDiverAttack, FIELD_TIME ), DEFINE_FIELD( m_fNextDiverState, FIELD_TIME ), DEFINE_FIELD( m_hPreventMovementMarine, FIELD_EHANDLE ), DEFINE_FIELD( m_hGrabbingEnemy, FIELD_EHANDLE ), DEFINE_FIELD( m_hPrimaryGrabber, FIELD_EHANDLE ), DEFINE_FIELD( m_hRetreatSpot, FIELD_EHANDLE ), DEFINE_FIELD( m_fLastRangedAttack, FIELD_FLOAT ), DEFINE_FIELD( m_iCrittersAlive, FIELD_INTEGER ), DEFINE_FIELD( m_hBlockingSentry, FIELD_EHANDLE ), DEFINE_OUTPUT( m_OnSummonWave1, "OnSummonWave1" ), DEFINE_OUTPUT( m_OnSummonWave2, "OnSummonWave2" ), DEFINE_OUTPUT( m_OnSummonWave3, "OnSummonWave3" ), DEFINE_OUTPUT( m_OnSummonWave4, "OnSummonWave4" ), DEFINE_OUTPUT( m_OnQueenKilled, "OnQueenKilled" ), END_DATADESC() // Activities int ACT_QUEEN_SCREAM; int ACT_QUEEN_SCREAM_LOW; int ACT_QUEEN_SINGLE_SPIT; int ACT_QUEEN_TRIPLE_SPIT; int AE_QUEEN_SPIT; int AE_QUEEN_START_SPIT; int ACT_QUEEN_LOW_IDLE; int ACT_QUEEN_LOW_TO_HIGH; int ACT_QUEEN_HIGH_TO_LOW; int ACT_QUEEN_TENTACLE_ATTACK; // AnimEvents int AE_QUEEN_SLASH_HIT; int AE_QUEEN_R_SLASH_HIT; int AE_QUEEN_START_SLASH; ConVar asw_queen_health_easy("asw_queen_health_easy", "2500", FCVAR_CHEAT, "Initial health of the Swarm Queen"); ConVar asw_queen_health_normal("asw_queen_health_normal", "3500", FCVAR_CHEAT, "Initial health of the Swarm Queen"); ConVar asw_queen_health_hard("asw_queen_health_hard", "5000", FCVAR_CHEAT, "Initial health of the Swarm Queen"); ConVar asw_queen_health_insane("asw_queen_health_insane", "6000", FCVAR_CHEAT, "Initial health of the Swarm Queen"); ConVar asw_queen_slash_damage("asw_queen_slash_damage", "5", FCVAR_CHEAT, "Damage caused by the Swarm Queen's slash attack"); ConVar asw_queen_slash_size("asw_queen_slash_size", "100", FCVAR_CHEAT, "Padding around the Swarm Queen's claw when calculating melee attack collision"); ConVar asw_queen_slash_debug("asw_queen_slash_debug", "0", FCVAR_CHEAT, "Visualize Swarm Queen slash collision"); ConVar asw_queen_slash_range("asw_queen_slash_range", "200", FCVAR_CHEAT, "Range of Swarm Queen slash attack"); ConVar asw_queen_min_mslash("asw_queen_min_mslash", "160", FCVAR_CHEAT, "Min Range of Swarm Queen moving slash attack"); ConVar asw_queen_max_mslash("asw_queen_max_mslash", "350", FCVAR_CHEAT, "Max Range of Swarm Queen moving slash attack"); ConVar asw_queen_spit_autoaim_angle("asw_queen_spit_autoaim_angle", "10", FCVAR_CHEAT, "Angle in degrees in which the Queen's spit attack will adjust to fire at a marine"); ConVar asw_queen_debug("asw_queen_debug", "0", FCVAR_CHEAT, "Display debug info about the queen"); ConVar asw_queen_flame_flinch_chance("asw_queen_flame_flinch_chance", "0", FCVAR_CHEAT, "Chance of queen flinching when she takes fire damage"); ConVar asw_queen_force_parasite_spawn("asw_queen_force_parasite_spawn", "0", FCVAR_CHEAT, "Set to 1 to force the queen to spawn parasites"); ConVar asw_queen_force_spit("asw_queen_force_spit", "0", FCVAR_CHEAT, "Set to 1 to force the queen to spit"); #define ASW_QUEEN_CLAW_MINS Vector(-asw_queen_slash_size.GetFloat(), -asw_queen_slash_size.GetFloat(), -asw_queen_slash_size.GetFloat() * 2.0f) #define ASW_QUEEN_CLAW_MAXS Vector(asw_queen_slash_size.GetFloat(), asw_queen_slash_size.GetFloat(), asw_queen_slash_size.GetFloat() * 2.0f) #define ASW_QUEEN_SLASH_DAMAGE asw_queen_slash_damage.GetInt() #define ASW_QUEEN_MELEE_RANGE asw_queen_slash_range.GetFloat() #define ASW_QUEEN_MELEE2_MIN_RANGE asw_queen_min_mslash.GetFloat() #define ASW_QUEEN_MELEE2_MAX_RANGE asw_queen_max_mslash.GetFloat() // health points at which the queen will stop to call in waves of allies #define QUEEN_SUMMON_WAVE_POINT_1 0.8f // wave of drones #define QUEEN_SUMMON_WAVE_POINT_2 0.6f // wave of buzzers #define QUEEN_SUMMON_WAVE_POINT_3 0.4f // wave of drone jumpers #define QUEEN_SUMMON_WAVE_POINT_4 0.2f // wave of shieldbugs #define ASW_DIVER_ATTACK_CHANCE 0.5f #define ASW_DIVER_ATTACK_INTERVAL 20.0f #define ASW_RANGED_ATTACK_INTERVAL 30.0f #define ASW_MAX_QUEEN_PARASITES 5 CASW_Queen::CASW_Queen() { m_iDiverState = ASW_QUEEN_DIVER_NONE; m_fLastDiverAttack = 0; m_iCrittersAlive = 0; m_fLayParasiteTime = 0; m_iCrittersSpawnedRecently = 0; m_pszAlienModelName = SWARM_QUEEN_MODEL; m_nAlienCollisionGroup = ASW_COLLISION_GROUP_ALIEN; } CASW_Queen::~CASW_Queen() { } void CASW_Queen::Spawn( void ) { SetHullType(HULL_LARGE_CENTERED); BaseClass::Spawn(); SetHullType(HULL_LARGE_CENTERED); //UTIL_SetSize(this, Vector(-23,-23,0), Vector(23,23,69)); //UTIL_SetSize(this, Vector(-140, -140, 0), Vector(140, 140, 200) ); #ifdef ASW_QUEEN_STATIONARY UTIL_SetSize(this, Vector(-140, -40, 0), Vector(140, 40, 200) ); #else UTIL_SetSize(this, Vector(-120,-120,0), Vector(120,120,160)); #endif SetHealthByDifficultyLevel(); CapabilitiesAdd( bits_CAP_MOVE_GROUND | bits_CAP_INNATE_RANGE_ATTACK1 //| bits_CAP_INNATE_RANGE_ATTACK2 | bits_CAP_INNATE_MELEE_ATTACK1 | bits_CAP_INNATE_MELEE_ATTACK2); #ifdef ASW_QUEEN_STATIONARY CapabilitiesRemove( bits_CAP_MOVE_GROUND ); #endif m_flDistTooFar = 9999999.0f; m_angQueenFacing = GetAbsAngles(); m_hDiver = CASW_Queen_Divers::Create_Queen_Divers(this); //CollisionProp()->SetSurroundingBoundsType( USE_HITBOXES ); m_takedamage = DAMAGE_NO; // queen is invulnerable until she finds her first enemy m_hRetreatSpot = gEntList.FindEntityByClassname( NULL, "asw_queen_retreat_spot" ); } void CASW_Queen::NPCInit() { BaseClass::NPCInit(); //SetDistSwarmSense(1024.0f); //SetDistLook(1024.0f); } void CASW_Queen::Precache( void ) { PrecacheScriptSound( "ASW_Queen.Death" ); PrecacheScriptSound( "ASW_Queen.Pain" ); PrecacheScriptSound( "ASW_Queen.PainBig" ); PrecacheScriptSound( "ASW_Queen.Slash" ); PrecacheScriptSound( "ASW_Queen.SlashShort" ); PrecacheScriptSound( "ASW_Queen.AttackWave" ); PrecacheScriptSound( "ASW_Queen.Spit" ); PrecacheScriptSound( "ASW_Queen.TentacleAttackStart" ); BaseClass::Precache(); } // queen doesn't move, like Kompressor does not dance float CASW_Queen::GetIdealSpeed() const { #ifdef ASW_QUEEN_STATIONARY return 0; #else return BaseClass::GetIdealSpeed() * m_flPlaybackRate; #endif } float CASW_Queen::GetIdealAccel( ) const { return GetIdealSpeed() * 1.5f; } // queen doesn't turn float CASW_Queen::MaxYawSpeed( void ) { #ifdef ASW_QUEEN_STATIONARY return 0; #else Activity eActivity = GetActivity(); //CBaseEntity *pEnemy = GetEnemy(); // Stay still if (( eActivity == ACT_MELEE_ATTACK1 ) ) return 0.0f; return 20; #endif } // ============================= SOUNDS ============================= void CASW_Queen::AlertSound() { // no alert sound atm //EmitSound("ASW_ShieldBug.Alert"); } void CASW_Queen::PainSound( const CTakeDamageInfo &info ) { if (gpGlobals->curtime > m_fNextPainSound ) { if (info.GetInflictor() == m_hDiver.Get()) // if the damage comes from our vulnerable divers, then scream big time EmitSound("ASW_Queen.PainBig"); else EmitSound("ASW_Queen.Pain"); m_fNextPainSound = gpGlobals->curtime + 0.5f; } //SetChestOpen(!m_bChestOpen); } void CASW_Queen::AttackSound() { if (IsCurSchedule(SCHED_MELEE_ATTACK2)) EmitSound("ASW_Queen.SlashShort"); else EmitSound("ASW_Queen.Slash"); } void CASW_Queen::SummonWaveSound() { EmitSound("ASW_Queen.AttackWave"); } void CASW_Queen::IdleSound() { // queen has no idle... //EmitSound("ASW_ShieldBug.Idle"); } void CASW_Queen::DeathSound( const CTakeDamageInfo &info ) { EmitSound( "ASW_Queen.Death" ); } // ============================= END SOUNDS ============================= // make the queen always look in his starting direction bool CASW_Queen::OverrideMoveFacing( const AILocalMoveGoal_t &move, float flInterval ) { #ifdef ASW_QUEEN_STATIONARY Vector vForward; AngleVectors( m_angQueenFacing, &vForward ); AddFacingTarget( vForward, 1.0f, 0.2f ); #endif return BaseClass::OverrideMoveFacing( move, flInterval ); } void CASW_Queen::HandleAnimEvent( animevent_t *pEvent ) { int nEvent = pEvent->Event(); if ( nEvent == AE_QUEEN_SLASH_HIT ) { SlashAttack(false); return; } else if ( nEvent == AE_QUEEN_R_SLASH_HIT ) { SlashAttack(true); return; } else if ( nEvent == AE_QUEEN_START_SLASH ) { AttackSound(); m_vecLastClawPos = vec3_origin; return; } else if ( nEvent == AE_QUEEN_START_SPIT) { m_iSpitNum = 0; return; } else if ( nEvent == AE_QUEEN_SPIT) { SpitProjectile(); m_iSpitNum++; return; } BaseClass::HandleAnimEvent( pEvent ); } // queen can attack without LOS so long as they're near enough bool CASW_Queen::WeaponLOSCondition(const Vector &ownerPos, const Vector &targetPos, bool bSetConditions) { if (targetPos.DistTo(ownerPos) < ASW_QUEEN_MAX_ATTACK_DISTANCE) return true; return false; } // is the enemy near enough to left slash at? int CASW_Queen::MeleeAttack1Conditions ( float flDot, float flDist ) { float fRangeBoost = 1.0f; if (flDot > 0) { fRangeBoost = 1.0f + (1.0f - flDot) * 0.25f; // 25% range boost at fldot of 0 } if ( flDist > ASW_QUEEN_MELEE_RANGE * fRangeBoost) { return COND_TOO_FAR_TO_ATTACK; } /*else if (GetEnemy() == NULL) { return 0; } // check he's to our left Vector diff = GetEnemy()->GetAbsOrigin() - GetAbsOrigin(); float yaw = UTIL_VecToYaw(diff); yaw = AngleDiff(yaw, GetAbsAngles()[YAW]); if (yaw < 0) return 0;*/ return COND_CAN_MELEE_ATTACK1; } // attack 2 is the moving attack, enemy has to be over x units away int CASW_Queen::MeleeAttack2Conditions ( float flDot, float flDist ) { float fRangeBoost = 1.0f; if (flDot > 0) { fRangeBoost = 1.0f + (1.0f - flDot) * 0.25f; // 25% range boost at fldot of 0 } if ( flDist > ASW_QUEEN_MELEE2_MAX_RANGE * fRangeBoost) { return COND_TOO_FAR_TO_ATTACK; } if ( flDist < ASW_QUEEN_MELEE2_MIN_RANGE) { return COND_TOO_CLOSE_TO_ATTACK; } /*else if (GetEnemy() == NULL) { return 0; } // check he's to our right Vector diff = GetEnemy()->GetAbsOrigin() - GetAbsOrigin(); float yaw = UTIL_VecToYaw(diff); yaw = AngleDiff(yaw, GetAbsAngles()[YAW]); if (yaw > 0) return 0; */ return COND_CAN_MELEE_ATTACK2; } //----------------------------------------------------------------------------- // Purpose: For innate melee attack // Input : // Output : //----------------------------------------------------------------------------- float CASW_Queen::InnateRange1MinRange( void ) { return ASW_QUEEN_MELEE_RANGE; } float CASW_Queen::InnateRange1MaxRange( void ) { return ASW_QUEEN_MAX_ATTACK_DISTANCE; } int CASW_Queen::RangeAttack1Conditions ( float flDot, float flDist ) { if ( flDist < ASW_QUEEN_MELEE_RANGE) { return COND_TOO_CLOSE_TO_ATTACK; } else if (flDist > ASW_QUEEN_MAX_ATTACK_DISTANCE) { return COND_TOO_FAR_TO_ATTACK; } else if (flDot < 0.5) { return COND_NOT_FACING_ATTACK; } // we also have a timer that can prevent us from attacking, make sure we don't try while we're still in that time if (gpGlobals->curtime <= m_fLastRangedAttack + ASW_RANGED_ATTACK_INTERVAL) return COND_TOO_FAR_TO_ATTACK; return COND_CAN_RANGE_ATTACK1; } bool CASW_Queen::FCanCheckAttacks() { if ( GetNavType() == NAV_CLIMB || GetNavType() == NAV_JUMP ) return false; //if ( HasCondition(COND_SEE_ENEMY) && !HasCondition( COND_ENEMY_TOO_FAR)) //{ return true; //} //return false; } void CASW_Queen::GatherConditions() { BaseClass::GatherConditions(); ClearCondition( COND_QUEEN_BLOCKED_BY_DOOR ); if (m_hBlockingSentry.Get()) { SetCondition( COND_QUEEN_BLOCKED_BY_DOOR ); } } int CASW_Queen::SelectSchedule() { if ( HasCondition( COND_NEW_ENEMY ) && GetHealth() > 0 ) { m_takedamage = DAMAGE_YES; } if ( HasCondition( COND_FLOATING_OFF_GROUND ) ) { SetGravity( 1.0 ); SetGroundEntity( NULL ); return SCHED_FALL_TO_GROUND; } if (m_NPCState == NPC_STATE_COMBAT) return SelectQueenCombatSchedule(); return BaseClass::SelectSchedule(); } int CASW_Queen::SelectQueenCombatSchedule() { if (asw_queen_force_spit.GetBool()) { asw_queen_force_spit.SetValue(false); return SCHED_RANGE_ATTACK1; } if (asw_queen_force_parasite_spawn.GetBool()) { asw_queen_force_parasite_spawn.SetValue(false); return SCHED_ASW_SPAWN_PARASITES; } // see if we were hurt, if so, flinch! int nSched = SelectFlinchSchedule_ASW(); if ( nSched != SCHED_NONE ) return nSched; // if we're in the middle of a diver attack, just wait if (m_iDiverState > ASW_QUEEN_DIVER_IDLE) { return SCHED_WAIT_DIVER; } // wake up angrily when we first see a marine if ( HasCondition(COND_NEW_ENEMY) && gpGlobals->curtime - GetEnemies()->FirstTimeSeen(GetEnemy()) < 2.0 ) { return SCHED_WAKE_ANGRY; } // if our enemy died, clear him and try to find another if ( HasCondition( COND_ENEMY_DEAD ) ) { SetEnemy( NULL ); if ( ChooseEnemy() ) { ClearCondition( COND_ENEMY_DEAD ); return SelectSchedule(); } SetState( NPC_STATE_ALERT ); return SelectSchedule(); } if ( GetShotRegulator()->IsInRestInterval() ) { if ( HasCondition(COND_CAN_RANGE_ATTACK1) ) return SCHED_COMBAT_FACE; } // see if it's time to call in a wave of allies nSched = SelectSummonSchedule(); if ( nSched != SCHED_NONE ) { return nSched; } // occasionally do a diver attack if (m_fLastDiverAttack == 0) m_fLastDiverAttack = gpGlobals->curtime; // forces delay before first diver attack if ((m_fLastDiverAttack == 0 || gpGlobals->curtime > m_fLastDiverAttack + ASW_DIVER_ATTACK_INTERVAL) ) //&& random->RandomFloat() > ASW_DIVER_ATTACK_CHANCE) return SCHED_START_DIVER_ATTACK; // if we're blocked by a sentry, smash that mofo if ( HasCondition(COND_QUEEN_BLOCKED_BY_DOOR) ) return SCHED_SMASH_SENTRY; // melee if we can if ( HasCondition(COND_CAN_MELEE_ATTACK1) ) return SCHED_MELEE_ATTACK1; if ( HasCondition(COND_CAN_MELEE_ATTACK2) ) return SCHED_MELEE_ATTACK2; #ifdef ASW_QUEEN_STATIONARY // we can see the enemy if ( HasCondition(COND_CAN_RANGE_ATTACK1) ) { return SCHED_RANGE_ATTACK1; } if ( HasCondition(COND_CAN_RANGE_ATTACK2) ) return SCHED_RANGE_ATTACK2; #else if (m_fLastRangedAttack == 0) m_fLastRangedAttack = gpGlobals->curtime; // forces delay before first diver attack if (gpGlobals->curtime > m_fLastRangedAttack + ASW_RANGED_ATTACK_INTERVAL) { if ( HasCondition(COND_CAN_RANGE_ATTACK1) ) { m_fLastRangedAttack = gpGlobals->curtime; // randomly either spit or spawn parasites for our ranged attack //if (m_iCrittersAlive < ASW_MAX_QUEEN_PARASITES && random->RandomFloat() < 0.5f) //return SCHED_ASW_SPAWN_PARASITES; return SCHED_RANGE_ATTACK1; } /*if ( HasCondition(COND_CAN_RANGE_ATTACK2) ) { m_fLastRangedAttack = gpGlobals->curtime; return SCHED_RANGE_ATTACK2; }*/ } #endif if ( HasCondition(COND_NOT_FACING_ATTACK) ) return SCHED_COMBAT_FACE; // if we're not attacking, then just look at them #ifdef ASW_QUEEN_STATIONARY return SCHED_COMBAT_FACE; #else return SCHED_CHASE_ENEMY; #endif DevWarning( 2, "No suitable combat schedule!\n" ); return SCHED_FAIL; } int CASW_Queen::TranslateSchedule( int scheduleType ) { if ( scheduleType == SCHED_RANGE_ATTACK1 ) { RemoveAllGestures(); return SCHED_QUEEN_RANGE_ATTACK; } else if (scheduleType == SCHED_ASW_ALIEN_MELEE_ATTACK1 || scheduleType == SCHED_MELEE_ATTACK2) { RemoveAllGestures(); } return BaseClass::TranslateSchedule( scheduleType ); } bool CASW_Queen::ShouldGib( const CTakeDamageInfo &info ) { return false; } void CASW_Queen::SetChestOpen(bool bOpen) { if (bOpen != m_bChestOpen) { m_bChestOpen = bOpen; } } int CASW_Queen::SelectDeadSchedule() { // Adrian - Alread dead (by animation event maybe?) // Is it safe to set it to SCHED_NONE? if ( m_lifeState == LIFE_DEAD ) return SCHED_NONE; CleanupOnDeath(); return SCHED_DIE; } void CASW_Queen::UpdatePoseParams() { /* float yaw = m_fLastHeadYaw; //GetPoseParameter( LookupPoseParameter("head_yaw") ); if ( m_hQueenEnemy.Get() != NULL ) { Vector enemyDir = m_hQueenEnemy->WorldSpaceCenter() - WorldSpaceCenter(); VectorNormalize( enemyDir ); float angle = VecToYaw( BodyDirection3D() ); float angleDiff = VecToYaw( enemyDir ); angleDiff = UTIL_AngleDiff( angleDiff, angle + yaw ); //ASW_ClampYaw(500.0f, yaw, yaw + angleDiff, gpGlobals->frametime); //Msg("yaw=%f targ=%f delta=%f ", yaw, yaw+angleDiff, gpGlobals->frametime * 3.0f); yaw = ASW_Linear_Approach(yaw, yaw + angleDiff, gpGlobals->frametime * 1200.0f); //Msg(" result=%f\n", yaw); SetPoseParameter( "head_yaw", yaw ); m_fLastHeadYaw = yaw; //SetPoseParameter( "head_yaw", Approach( yaw + angleDiff, yaw, 5 ) ); } else { // Otherwise turn the head back to its normal position //ASW_ClampYaw(500.0f, yaw, 0, gpGlobals->frametime); yaw = ASW_Linear_Approach(yaw, 0, gpGlobals->frametime * 1200.0f); SetPoseParameter( "head_yaw", yaw ); m_fLastHeadYaw = yaw; //SetPoseParameter( "head_yaw", Approach( 0, yaw, 10 ) ); } */ float shield = m_fLastShieldPose; //GetPoseParameter( LookupPoseParameter("shield_open") ); float targetshield = m_bChestOpen ? 1.0f : 0.0f; if (shield != targetshield) { shield = ASW_Linear_Approach(shield, targetshield, gpGlobals->frametime * 3.0f); m_fLastShieldPose = shield; } SetPoseParameter( "shield_open", shield ); } bool CASW_Queen::ShouldWatchEnemy() { /*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; } void CASW_Queen::PrescheduleThink() { BaseClass::PrescheduleThink(); // Don't do anything after death if ( m_NPCState == NPC_STATE_DEAD ) return; m_hQueenEnemy = GetEnemy(); UpdatePoseParams(); UpdateDiver(); //Msg("%f: UpdatePoseParams\n", gpGlobals->curtime); } int CASW_Queen::SelectFlinchSchedule_ASW() { // don't flinch if we didn't take any heavy damage if ( !HasCondition(COND_HEAVY_DAMAGE) ) // && !HasCondition(COND_LIGHT_DAMAGE) return SCHED_NONE; // don't flinch midway through a flinch if ( IsCurSchedule( SCHED_BIG_FLINCH ) ) return SCHED_NONE; // only flinch if shot during a melee attack //if (! (GetTask() && (GetTask()->iTask == TASK_MELEE_ATTACK1)) ) //return SCHED_NONE; // Do the flinch, if we have the anim Activity iFlinchActivity = GetFlinchActivity( true, false ); if ( HaveSequenceForActivity( iFlinchActivity ) ) return SCHED_BIG_FLINCH; return SCHED_NONE; } // see if we've been hurt enough to warrant summoning a wave int CASW_Queen::SelectSummonSchedule() { switch (m_iSummonWave) { case 0: if (GetHealth() <= QUEEN_SUMMON_WAVE_POINT_1 * GetMaxHealth()) return SCHED_ASW_RETREAT_AND_SUMMON; break; case 1: if (GetHealth() <= QUEEN_SUMMON_WAVE_POINT_2 * GetMaxHealth()) return SCHED_ASW_RETREAT_AND_SUMMON; break; case 2: if (GetHealth() <= QUEEN_SUMMON_WAVE_POINT_3 * GetMaxHealth()) return SCHED_ASW_RETREAT_AND_SUMMON; break; case 3: if (GetHealth() <= QUEEN_SUMMON_WAVE_POINT_4 * GetMaxHealth()) return SCHED_ASW_RETREAT_AND_SUMMON; break; default: return SCHED_NONE; break; } return SCHED_NONE; } void CASW_Queen::StartTask( const Task_t *pTask ) { switch( pTask->iTask ) { #ifdef ASW_QUEEN_STATIONARY case TASK_FACE_IDEAL: case TASK_FACE_ENEMY: { // stationary queen doesn't turn TaskComplete(); break; } #endif case TASK_CLEAR_BLOCKING_SENTRY: { m_hBlockingSentry = NULL; TaskComplete(); break; } case TASK_FACE_SENTRY: { if (!m_hBlockingSentry.Get()) { TaskFail("No sentry to smash\n"); } else { Vector vecEnemyLKP = m_hBlockingSentry->GetAbsOrigin(); if (!FInAimCone( vecEnemyLKP )) { GetMotor()->SetIdealYawToTarget( vecEnemyLKP ); GetMotor()->SetIdealYaw( CalcReasonableFacing( true ) ); // CalcReasonableFacing() is based on previously set ideal yaw SetTurnActivity(); } else { float flReasonableFacing = CalcReasonableFacing( true ); if ( fabsf( flReasonableFacing - GetMotor()->GetIdealYaw() ) < 1 ) TaskComplete(); else { GetMotor()->SetIdealYaw( flReasonableFacing ); SetTurnActivity(); } } } break; break; } case TASK_ASW_SOUND_SUMMON: { SummonWaveSound(); TaskComplete(); break; } case TASK_ASW_SUMMON_WAVE: { // fire our outputs to summon waves switch (m_iSummonWave) { case 0: m_OnSummonWave1.FireOutput(this, this); break; case 1: m_OnSummonWave2.FireOutput(this, this); break; case 2: m_OnSummonWave3.FireOutput(this, this); break; case 3: m_OnSummonWave4.FireOutput(this, this); break; default: break; } m_iSummonWave++; TaskComplete(); break; } case TASK_ASW_WAIT_DIVER: { // make sure we're still doing this activity (can be broken out of it by a grenade flinch) SetIdealActivity((Activity) ACT_QUEEN_TENTACLE_ATTACK); break; } case TASK_ASW_START_DIVER_ATTACK: { if (m_iDiverState > ASW_QUEEN_DIVER_IDLE) { // already diver attacking TaskComplete(); } m_fLastDiverAttack = gpGlobals->curtime; // set us plunging, which will set off the whole diver attacking routine and wait schedules SetDiverState(ASW_QUEEN_DIVER_PLUNGING); RemoveAllGestures(); // make us play an anim while we plunge those divers into the ground SetIdealActivity((Activity) ACT_QUEEN_TENTACLE_ATTACK); TaskComplete(); break; } case TASK_ASW_GET_PATH_TO_RETREAT_SPOT: { if ( m_hRetreatSpot == NULL ) { TaskFail( "Tried to find a path to NULL retreat spot!\n" ); break; } Vector vecGoalPos = m_hRetreatSpot->GetAbsOrigin(); AI_NavGoal_t goal( GOALTYPE_LOCATION, vecGoalPos, ACT_RUN ); if ( GetNavigator()->SetGoal( goal ) ) { if ( asw_queen_debug.GetInt() == 1 ) { NDebugOverlay::Cross3D( vecGoalPos, Vector(8,8,8), -Vector(8,8,8), 0, 255, 0, true, 2.0f ); NDebugOverlay::Line( vecGoalPos, m_hRetreatSpot->WorldSpaceCenter(), 0, 255, 0, true, 2.0f ); } // Face the enemy GetNavigator()->SetArrivalDirection( m_hRetreatSpot->GetAbsAngles() ); TaskComplete(); } else { if ( asw_queen_debug.GetInt() == 1 ) { NDebugOverlay::Cross3D( vecGoalPos, Vector(8,8,8), -Vector(8,8,8), 255, 0, 0, true, 2.0f ); NDebugOverlay::Line( vecGoalPos, m_hRetreatSpot->WorldSpaceCenter(), 255, 0, 0, true, 2.0f ); } TaskFail( "Unable to navigate to retreat spot attack target!\n" ); break; } } break; case TASK_SPAWN_PARASITES: { RemoveAllGestures(); SetChestOpen(true); // make us play an anim while we spawn parasites SetIdealActivity((Activity) ACT_QUEEN_TENTACLE_ATTACK); m_fLayParasiteTime = gpGlobals->curtime + 1.0f; m_iCrittersSpawnedRecently = 0; } break; default: BaseClass::StartTask( pTask ); break; } } void CASW_Queen::RunTask( const Task_t *pTask ) { switch ( pTask->iTask ) { case TASK_ASW_SUMMON_WAVE: { // should never get into here break; } case TASK_FACE_SENTRY: { // If the yaw is locked, this function will not act correctly Assert( GetMotor()->IsYawLocked() == false ); if (!m_hBlockingSentry.Get()) { TaskFail("No sentry!\n"); } else { Vector vecEnemyLKP = m_hBlockingSentry->GetAbsOrigin(); if (!FInAimCone( vecEnemyLKP )) { GetMotor()->SetIdealYawToTarget( vecEnemyLKP ); GetMotor()->SetIdealYaw( CalcReasonableFacing( true ) ); // CalcReasonableFacing() is based on previously set ideal yaw } else { float flReasonableFacing = CalcReasonableFacing( true ); if ( fabsf( flReasonableFacing - GetMotor()->GetIdealYaw() ) > 1 ) GetMotor()->SetIdealYaw( flReasonableFacing ); } GetMotor()->UpdateYaw(); if ( FacingIdeal() ) { TaskComplete(); } } break; } case TASK_ASW_WAIT_DIVER: { if (m_iDiverState <= ASW_QUEEN_DIVER_IDLE) TaskComplete(); break; } case TASK_SPAWN_PARASITES: { if (gpGlobals->curtime > m_fLayParasiteTime) { if (m_iCrittersAlive >= ASW_MAX_QUEEN_PARASITES || m_iCrittersSpawnedRecently > ASW_MAX_QUEEN_PARASITES) { SetChestOpen(false); TaskComplete(); } else { SpawnParasite(); m_fLayParasiteTime = gpGlobals->curtime + 1.5f; // setup timer to spawn another one until we're at our max } } break; } default: { BaseClass::RunTask(pTask); break; } } } void CASW_Queen::SlashAttack(bool bRightClaw) { //Msg("Queen slash attack\n"); Vector vecClawBase; Vector vecClawTip; QAngle angClaw; if (bRightClaw) { if (!GetAttachment( "RightClawBase", vecClawBase, angClaw )) { Msg("Error, failed to find Queen claw attachment point\n"); return; } if (!GetAttachment( "RightClawPoint", vecClawTip, angClaw )) { Msg("Error, failed to find Queen claw attachment point\n"); return; } } else { if (!GetAttachment( "LeftClawBase", vecClawBase, angClaw )) { Msg("Error, failed to find Queen claw attachment point\n"); return; } if (!GetAttachment( "LeftClawPoint", vecClawTip, angClaw )) { Msg("Error, failed to find Queen claw attachment point\n"); return; } } if (asw_queen_slash_debug.GetBool()) Msg("Slash trace: cycle = %f\n", GetCycle()); // find the midpoint of the claw, then move it back towards the queen origin a bit (this makes her swipes occur as a volume inside her reach, still hitting marines up close) Vector vecMidPoint = (vecClawBase + vecClawTip) * 0.5f; Vector diff = vecMidPoint - GetAbsOrigin(); vecMidPoint -= diff * 0.3f; // if we don't have a last claw pos, this must be the starting point of a sweep // store current claw pos so we can do the sweeping hull check from here next time if (m_vecLastClawPos == vec3_origin) { m_vecLastClawPos = vecMidPoint; return; } // sweep an expanded collision test hull from the tip of the claw to the base trace_t tr; Ray_t ray; CASW_TraceFilterOnlyQueenTargets filter( this, COLLISION_GROUP_NONE ); ray.Init( m_vecLastClawPos, vecMidPoint, ASW_QUEEN_CLAW_MINS, ASW_QUEEN_CLAW_MAXS ); enginetrace->TraceRay( ray, MASK_SOLID, &filter, &tr ); if (tr.m_pEnt) { CBaseEntity *pEntity = tr.m_pEnt; CTakeDamageInfo info( this, this, ASWGameRules()->ModifyAlienDamageBySkillLevel(ASW_QUEEN_SLASH_DAMAGE), DMG_SLASH ); info.SetDamagePosition(vecMidPoint); Vector force = vecMidPoint - m_vecLastClawPos; force.NormalizeInPlace(); if (force.IsZero()) info.SetDamageForce( Vector(0.1, 0.1, 0.1) ); else info.SetDamageForce(force * 10000); CASW_Alien* pAlien = dynamic_cast(pEntity); if (pAlien) pAlien->MeleeBleed(&info); CASW_Marine* pMarine = CASW_Marine::AsMarine( pEntity ); if (pMarine) pMarine->MeleeBleed(&info); else { CASW_Colonist *pColonist = dynamic_cast(pEntity); if (pColonist) { pColonist->MeleeBleed(&info); } else { CASW_Sentry_Base *pSentry = dynamic_cast(pEntity); if (pSentry) { // scale the damage up a bit so we don't take so many swipes to kill the sentry info.ScaleDamage(5.55f); Vector position = pSentry->GetAbsOrigin() + Vector(0,0,30); Vector sparkNormal = GetAbsOrigin() - position; sparkNormal.z = 0; sparkNormal.NormalizeInPlace(); CPVSFilter filter( position ); filter.SetIgnorePredictionCull(true); te->Sparks( filter, 0.0, &position, 1, 1, &sparkNormal ); } } } // change damage type to make sure we burst explosive barrels if (dynamic_cast(pEntity)) info.SetDamageType(DMG_BULLET); pEntity->TakeDamage( info ); if (asw_queen_slash_debug.GetBool()) { Msg("Slash hit %d %s\n", tr.m_pEnt->entindex(), tr.m_pEnt->GetClassname()); NDebugOverlay::SweptBox(m_vecLastClawPos, vecMidPoint, ASW_QUEEN_CLAW_MINS, ASW_QUEEN_CLAW_MAXS, vec3_angle, 255, 255, 0, 0 ,1.0f); NDebugOverlay::Line(vecMidPoint, tr.m_pEnt->GetAbsOrigin(), 255, 255, 0, false, 1.0f ); } } else { if (asw_queen_slash_debug.GetBool()) { NDebugOverlay::SweptBox(m_vecLastClawPos, vecMidPoint, ASW_QUEEN_CLAW_MINS, ASW_QUEEN_CLAW_MAXS, vec3_angle, 255, 0, 0, 0 ,1.0f); } } m_vecLastClawPos = vecMidPoint; } void CASW_Queen::SpitProjectile() { // Get angle from our head bone attachment (or do it by m_iSpitNum?) Vector vecSpitSource; QAngle angSpit; if (!GetAttachment( "SpitSource", vecSpitSource, angSpit )) { Msg("Error, failed to find Queen spit attachment point\n"); return; } //Msg("SpitSource pos = %s ", VecToString(vecSpitSource)); //Msg("ang = %s (", VecToString(angSpit)); Vector vecAiming; AngleVectors(angSpit, &vecAiming); //Msg("%s)\n", VecToString(vecAiming)); // angle it flat angSpit[PITCH] = 0; // do an autoaim routine in that rough direction to see if we can angle the shot to hit a marine Vector vecThrow = GetQueenAutoaimVector(vecSpitSource, angSpit); //Msg(" autoaim changed to %s\n", VecToString(vecThrow)); // setup the speed VectorScale( vecThrow, 1000.0f, vecThrow ); // create it! CASW_Queen_Spit::Queen_Spit_Create( vecSpitSource, angSpit, vecThrow, AngularImpulse(0,0,0), this ); // problems: shot comes from up high, meaning it should be very easy to dodge // could make it an AoE explosion so if they don't dodge enough, they'll get caught in the splash damage // make a sound for the spit EmitSound("ASW_Queen.Spit"); } // don't hurt ourselves ever float CASW_Queen::GetAttackDamageScale( CBaseEntity *pVictim ) { if (pVictim == this) return 0; return BaseClass::GetAttackDamageScale(pVictim); } Vector CASW_Queen::GetQueenAutoaimVector(Vector &spitSrc, QAngle &angSpit) { Vector vecResult; // find a marine close to this vector CASW_Game_Resource *pGameResource = ASWGameResource(); if (!pGameResource) return Vector(0,0,0); CASW_Marine *pChosenMarine = NULL; float fClosestAngle = 999; float fChosenYaw = 0; Vector vecChosenDiff; for (int i=0;iGetMaxMarineResources();i++) { CASW_Marine_Resource* pMarineResource = pGameResource->GetMarineResource(i); if (!pMarineResource) continue; CASW_Marine* pMarine = pMarineResource->GetMarineEntity(); if (!pMarine) continue; Vector diff = pMarine->GetAbsOrigin() - spitSrc; if (diff.Length2D() > ASW_QUEEN_MAX_ATTACK_DISTANCE * 1.5f) continue; // todo: do movement prediction float fYawToMarine = UTIL_VecToYaw(diff); float fYawDiff = AngleDiff(fYawToMarine, angSpit[YAW]); if (abs(fYawDiff) < abs(fClosestAngle)) { fClosestAngle = fYawDiff; pChosenMarine = pMarine; fChosenYaw = fYawToMarine; vecChosenDiff = diff; } } if (pChosenMarine) { // we have a marine to autoaim at if (abs(fClosestAngle) < asw_queen_spit_autoaim_angle.GetFloat()) { // adjust the yaw to point directly at him angSpit[YAW] = fChosenYaw; // adjust the pitch so it lands near him angSpit[PITCH] = UTIL_VecToPitch(vecChosenDiff); // convert to a vector AngleVectors(angSpit, &vecResult); return vecResult; } } AngleVectors(angSpit, &vecResult); return vecResult; } // todo: should depend on how far away the enemy is? #define ASW_DIVER_CHASE_TIME 6.0f void CASW_Queen::SetDiverState(int iNewState) { if (!m_hDiver.Get()) return; m_iDiverState = iNewState; if (iNewState >= ASW_QUEEN_DIVER_PLUNGING && iNewState <=ASW_QUEEN_DIVER_RETRACTING) { m_hDiver.Get()->SetBurrowing(true); } else { m_hDiver.Get()->SetBurrowing(false); } if (iNewState >= ASW_QUEEN_DIVER_PLUNGING && iNewState <= ASW_QUEEN_DIVER_UNPLUNGING) { SetChestOpen(true); m_fLastDiverAttack = gpGlobals->curtime; } else { SetChestOpen(false); } if (iNewState == ASW_QUEEN_DIVER_CHASING) { if (m_iLiveGrabbers > 0) { Msg("WARNING: Queen started chasing when already had some live grabbers"); } // we've started a chase! init the grabber pos and set it on its merry way Vector vecGrabberPos = GetDiverSpot(); //vecGrabberPos.z += 30; CASW_Queen_Grabber* pGrabber = CASW_Queen_Grabber::Create_Queen_Grabber(this, vecGrabberPos, GetAbsAngles()); if (pGrabber) { m_iLiveGrabbers = 1; pGrabber->MakePrimary(); pGrabber->m_fMaxChasingTime = gpGlobals->curtime + 6.0f; // 6 seconds of chasing m_hPrimaryGrabber = pGrabber; } } else if (iNewState == ASW_QUEEN_DIVER_GRABBING) { m_hGrabbingEnemy = GetEnemy(); } if (iNewState <= ASW_QUEEN_DIVER_IDLE) m_hDiver.Get()->SetVisible(false); // set time for next diver state switch (iNewState) { case ASW_QUEEN_DIVER_PLUNGING: m_fNextDiverState = gpGlobals->curtime + 1.0f; break; case ASW_QUEEN_DIVER_CHASING: m_fNextDiverState = 0; break; // Grabber will advance us to the grabbing state when he catches us case ASW_QUEEN_DIVER_GRABBING: m_fNextDiverState = 0; break; // Grabber will advance us to the retracting state when all grabbers are shot away case ASW_QUEEN_DIVER_RETRACTING: m_fNextDiverState = 0; break; // Primary grabber will advance us to unplunging when he's back home case ASW_QUEEN_DIVER_UNPLUNGING: m_fNextDiverState = gpGlobals->curtime + 1.5f; break; default: m_fNextDiverState = 0; break; } //Msg("Diver state set to %d\n", iNewState); } void CASW_Queen::NotifyGrabberKilled(CASW_Queen_Grabber* pGrabber) { if (pGrabber == m_hPrimaryGrabber.Get()) m_hPrimaryGrabber = NULL; m_iLiveGrabbers--; if (m_iLiveGrabbers < 0) { m_iLiveGrabbers = 0; Msg("WARNING: Live grabbers went below 0\n"); } if (m_iLiveGrabbers <= 0) { // all grabbers are dead SetDiverState(ASW_QUEEN_DIVER_RETRACTING); if (m_hPreventMovementMarine.Get()) m_hPreventMovementMarine->m_bPreventMovement = false; } } void CASW_Queen::AdvanceDiverState() { if (m_iDiverState >= ASW_QUEEN_DIVER_UNPLUNGING) SetDiverState(ASW_QUEEN_DIVER_IDLE); else SetDiverState(m_iDiverState + 1); } void CASW_Queen::UpdateDiver() { if (m_fNextDiverState != 0) { if (gpGlobals->curtime >= m_fNextDiverState) { AdvanceDiverState(); } } if (m_iDiverState == ASW_QUEEN_DIVER_CHASING) { // todo: pick the grabber target enemy when we start chasing and don't change midchase if (!GetEnemy() || !m_hDiver.Get()) { // todo: stop chasing and retract? or change enemy? } //CASW_Queen_Divers *pDiver = m_hDiver.Get(); // we're chasing down our enemy! duhh duh duh duh duuhh duh duh duh OH NOES! } } int CASW_Queen::OnTakeDamage_Alive( const CTakeDamageInfo &info ) { CTakeDamageInfo newInfo(info); float damage = info.GetDamage(); // reduce all damage because the queen is TUFF! damage *= 0.2f; // reduce damage from shotguns and mining laser if (info.GetDamageType() & DMG_ENERGYBEAM) { damage *= 0.4f; } if (info.GetDamageType() & DMG_BUCKSHOT) { // hack to reduce vindicator damage (not reducing normal shotty as much as it's not too strong) if (info.GetAttacker() && info.GetAttacker()->Classify() == CLASS_ASW_MARINE) { CASW_Marine *pMarine = dynamic_cast(info.GetAttacker()); if (pMarine) { CASW_Weapon_Assault_Shotgun *pVindicator = dynamic_cast(pMarine->GetActiveASWWeapon()); if (pVindicator) damage *= 0.45f; else damage *= 0.6f; } } } // make queen immune to buzzers if (dynamic_cast(info.GetAttacker())) { return 0; } // make queen immune to crush damage if (info.GetDamageType() & DMG_CRUSH) { return 0; } newInfo.SetDamage(damage); return BaseClass::OnTakeDamage_Alive(newInfo); } void CASW_Queen::Event_Killed( const CTakeDamageInfo &info ) { BaseClass::Event_Killed(info); m_OnQueenKilled.FireOutput(this, this); // make sure to kill our grabbers when queen dies if (m_hPrimaryGrabber.Get()) { CTakeDamageInfo info2(info); info2.SetDamage(1000); m_hPrimaryGrabber->OnTakeDamage(info2); } CEffectData data; data.m_nEntIndex = entindex(); CPASFilter filter( GetAbsOrigin() ); filter.SetIgnorePredictionCull(true); data.m_vOrigin = GetAbsOrigin(); DispatchEffect( filter, 0.0, "QueenDie", data ); // if we're in the middle of plunging divers into the ground, stop it (for other states, killing our grabber will trigger the whole retract sequence of events) if (m_iDiverState == ASW_QUEEN_DIVER_PLUNGING) { SetDiverState(ASW_QUEEN_DIVER_NONE); if (m_hDiver.Get()) m_hDiver.Get()->SetBurrowing(false); } } Vector CASW_Queen::GetDiverSpot() { Vector vecForward; AngleVectors(GetAbsAngles(), &vecForward); vecForward.z = 0; return GetAbsOrigin() + vecForward * 80; } // don't allow us to be hurt by allies bool CASW_Queen::PassesDamageFilter( const CTakeDamageInfo &info ) { if (info.GetAttacker() && IsAlienClass( info.GetAttacker()->Classify() ) ) return false; return BaseClass::PassesDamageFilter(info); } void CASW_Queen::SetHealthByDifficultyLevel() { int health = 5000; if (ASWGameRules()) { switch (ASWGameRules()->GetSkillLevel()) { case 1: health = asw_queen_health_easy.GetInt(); break; case 2: health = asw_queen_health_normal.GetInt(); break; case 3: health = asw_queen_health_hard.GetInt(); break; case 4: health = asw_queen_health_insane.GetInt(); break; case 5: health = asw_queen_health_insane.GetInt(); break; default: 5000; } } SetHealth(health); SetMaxHealth(health); } int CASW_Queen::DrawDebugTextOverlays() { int text_offset = BaseClass::DrawDebugTextOverlays(); if (m_debugOverlays & OVERLAY_TEXT_BIT) { NDebugOverlay::EntityText(entindex(),text_offset,m_bChestOpen ? "Chest Open" : "Chest Closed",0); text_offset++; char buffer[256]; Q_snprintf(buffer, sizeof(buffer), "shieldpos %f\n", m_fLastShieldPose); NDebugOverlay::EntityText(entindex(),text_offset,buffer,0); text_offset++; NDebugOverlay::EntityText(entindex(),text_offset, HasCondition(COND_CAN_RANGE_ATTACK1) ? "Can Range Attack" : "No Range attack",0); text_offset++; NDebugOverlay::EntityText(entindex(),text_offset, HasCondition(COND_CAN_MELEE_ATTACK1) ? "Can Melee Attack1" : "No Melee Attack1",0); text_offset++; NDebugOverlay::EntityText(entindex(),text_offset, HasCondition(COND_CAN_MELEE_ATTACK2) ? "Can Melee Attack2" : "No Melee aAttack2",0); text_offset++; } return text_offset; } void CASW_Queen::DrawDebugGeometryOverlays() { // draw arrows showing the extent of our melee attacks for (int i=0;i<360;i+=45) { float flBaseSize = 10; Vector vBasePos = GetAbsOrigin() + Vector( 0, 0, 5 ); QAngle angles( 0, 0, 0 ); Vector vForward, vRight, vUp; float flHeight = ASW_QUEEN_MELEE2_MAX_RANGE; angles[YAW] = i; AngleVectors( angles, &vForward, &vRight, &vUp ); NDebugOverlay::Triangle( vBasePos+vRight*flBaseSize/2, vBasePos-vRight*flBaseSize/2, vBasePos+vForward*flHeight, 128, 0, 0, 128, false, 0.1 ); flHeight = ASW_QUEEN_MELEE2_MIN_RANGE; angles[YAW] = i+5; AngleVectors( angles, &vForward, &vRight, &vUp ); vBasePos.z += 1; NDebugOverlay::Triangle( vBasePos+vRight*flBaseSize/2, vBasePos-vRight*flBaseSize/2, vBasePos+vForward*flHeight, 255, 0, 0, 128, false, 0.1 ); flHeight = ASW_QUEEN_MELEE_RANGE; angles[YAW] = i+10; AngleVectors( angles, &vForward, &vRight, &vUp ); vBasePos.z += 2; NDebugOverlay::Triangle( vBasePos+vRight*flBaseSize/2, vBasePos-vRight*flBaseSize/2, vBasePos+vForward*flHeight, 0, 255, 0, 128, false, 0.1 ); } BaseClass::DrawDebugGeometryOverlays(); } // the queen often flinches on explosions and fire damage bool CASW_Queen::IsHeavyDamage( const CTakeDamageInfo &info ) { // shock damage never causes flinching if (( info.GetDamageType() & DMG_SHOCK ) != 0 ) return false; // explosions always cause a flinch if (( info.GetDamageType() & DMG_BLAST ) != 0 ) return true; // flame causes a flinch some of the time if (( info.GetDamageType() & DMG_BURN ) != 0 ) { float f = random->RandomFloat(); bool bFlinch = (f < asw_queen_flame_flinch_chance.GetFloat()); if (bFlinch) Msg("Queen flinching from fire\n"); return bFlinch; } return false; } void CASW_Queen::BuildScheduleTestBits( void ) { // Ignore damage if we were recently damaged or we're attacking. if ( GetActivity() == ACT_MELEE_ATTACK1 || GetActivity() == ACT_MELEE_ATTACK2 ) { ClearCustomInterruptCondition( COND_LIGHT_DAMAGE ); } BaseClass::BuildScheduleTestBits(); } CAI_BaseNPC* CASW_Queen::SpawnParasite() { CBaseEntity *pEntity = CreateEntityByName( "asw_parasite" ); CAI_BaseNPC *pNPC = dynamic_cast(pEntity); if ( !pNPC ) { Warning("NULL Ent in CASW_Queen::SpawnParasite\n"); return NULL; } // spawn slightly in front of us and up, to be where the chest is Vector vecSpawnOffset = Vector(70, 0, 0); // was 10, if we're attempting the jump Vector vecSpawnPos(0,0,0); matrix3x4_t matrix; QAngle angFacing = GetAbsAngles(); AngleMatrix( angFacing, matrix ); VectorTransform(vecSpawnOffset, matrix, vecSpawnPos); vecSpawnPos+=GetAbsOrigin(); //if (m_iCrittersAlive == 0) //{ //NDebugOverlay::Axis(vecSpawnPos, angFacing, 10, false, 20.0f); //NDebugOverlay::Axis(GetAbsOrigin(), angFacing, 10, false, 20.0f); //} pNPC->AddSpawnFlags( SF_NPC_FALL_TO_GROUND ); // stops it teleporting to the ground on spawn pNPC->SetAbsOrigin( vecSpawnPos ); // Strip pitch and roll from the spawner's angles. Pass only yaw to the spawned NPC. QAngle angles = GetAbsAngles(); angles.x = 0.0; angles.z = 0.0; // vary the yaw a bit angles.y += random->RandomFloat(-30, 30); pNPC->SetAbsAngles( angles ); IASW_Spawnable_NPC* pSpawnable = dynamic_cast(pNPC); ASSERT(pSpawnable); if ( !pSpawnable ) { Warning("NULL Spawnable Ent in CASW_Queen!\n"); UTIL_Remove(pNPC); return NULL; } DispatchSpawn( pNPC ); pNPC->SetOwnerEntity( this ); pNPC->Activate(); CASW_Parasite *pParasite = dynamic_cast(pNPC); if (pParasite) { m_iCrittersAlive++; pParasite->SetMother(this); //pParasite->DoJumpFromEgg(); } return pNPC; } void CASW_Queen::ChildAlienKilled(CASW_Alien* pAlien) { m_iCrittersAlive--; } bool CASW_Queen::OnObstructionPreSteer( AILocalMoveGoal_t *pMoveGoal, float distClear, AIMoveResult_t *pResult ) { if ( pMoveGoal->directTrace.pObstruction ) { // check if we collide with a door or door padding CASW_Sentry_Base *pSentry = dynamic_cast( pMoveGoal->directTrace.pObstruction ); if (pSentry) { m_hBlockingSentry = pSentry; return true; } } return false; } // only hits NPCs and sentry gun bool CASW_TraceFilterOnlyQueenTargets::ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask ) { if ( CTraceFilterSimple::ShouldHitEntity(pServerEntity, contentsMask) ) { CBaseEntity *pEntity = EntityFromEntityHandle( pServerEntity ); CASW_Sentry_Base* pSentry = dynamic_cast(pEntity); if (pSentry) return true; CBreakableProp* pProp = dynamic_cast( pServerEntity ); if (pProp) return true; return (pEntity->IsNPC() || pEntity->IsPlayer()); } return false; } AI_BEGIN_CUSTOM_NPC( asw_queen, CASW_Queen ) // Tasks DECLARE_TASK( TASK_ASW_SUMMON_WAVE ) DECLARE_TASK( TASK_ASW_SOUND_SUMMON ) DECLARE_TASK( TASK_ASW_WAIT_DIVER ) DECLARE_TASK( TASK_ASW_START_DIVER_ATTACK ) DECLARE_TASK( TASK_ASW_GET_PATH_TO_RETREAT_SPOT ) DECLARE_TASK( TASK_SPAWN_PARASITES ) DECLARE_TASK( TASK_FACE_SENTRY ) DECLARE_TASK( TASK_CLEAR_BLOCKING_SENTRY ) // Activities DECLARE_ACTIVITY( ACT_QUEEN_SCREAM ) DECLARE_ACTIVITY( ACT_QUEEN_SCREAM_LOW ) DECLARE_ACTIVITY( ACT_QUEEN_TRIPLE_SPIT ) DECLARE_ACTIVITY( ACT_QUEEN_SINGLE_SPIT ) DECLARE_ACTIVITY( ACT_QUEEN_LOW_IDLE ) DECLARE_ACTIVITY( ACT_QUEEN_LOW_TO_HIGH ) DECLARE_ACTIVITY( ACT_QUEEN_HIGH_TO_LOW ) DECLARE_ACTIVITY( ACT_QUEEN_TENTACLE_ATTACK ) // Events DECLARE_ANIMEVENT( AE_QUEEN_SLASH_HIT ) DECLARE_ANIMEVENT( AE_QUEEN_R_SLASH_HIT ) DECLARE_ANIMEVENT( AE_QUEEN_START_SLASH ) DECLARE_ANIMEVENT( AE_QUEEN_START_SPIT ) DECLARE_ANIMEVENT( AE_QUEEN_SPIT ) // conditions DECLARE_CONDITION( COND_QUEEN_BLOCKED_BY_DOOR ) DEFINE_SCHEDULE ( SCHED_QUEEN_RANGE_ATTACK, " Tasks" " TASK_STOP_MOVING 0" " TASK_FACE_ENEMY 0" " TASK_PLAY_SEQUENCE ACTIVITY:ACT_QUEEN_HIGH_TO_LOW" " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack " TASK_RANGE_ATTACK1 0" "" " Interrupts" " COND_NEW_ENEMY" " COND_ENEMY_DEAD" " COND_LIGHT_DAMAGE" " COND_HEAVY_DAMAGE" " COND_ENEMY_OCCLUDED" " COND_NO_PRIMARY_AMMO" " COND_HEAR_DANGER" " COND_WEAPON_BLOCKED_BY_FRIEND" " COND_WEAPON_SIGHT_OCCLUDED" ) DEFINE_SCHEDULE ( SCHED_ASW_SUMMON_WAVE, " Tasks" " TASK_FACE_ENEMY 0" " TASK_ASW_SOUND_SUMMON 0" " TASK_PLAY_SEQUENCE ACTIVITY:ACT_QUEEN_SCREAM" " TASK_ASW_SUMMON_WAVE 0" "" " Interrupts" ) DEFINE_SCHEDULE ( SCHED_WAIT_DIVER, " Tasks" //" TASK_PLAY_SEQUENCE ACTIVITY:ACT_QUEEN_LOW_IDLE" " TASK_ASW_WAIT_DIVER 0" " TASK_PLAY_SEQUENCE ACTIVITY:ACT_QUEEN_LOW_TO_HIGH" "" " Interrupts" " COND_LIGHT_DAMAGE" " COND_HEAVY_DAMAGE" ) DEFINE_SCHEDULE ( SCHED_START_DIVER_ATTACK, " Tasks" " TASK_FACE_ENEMY 0" " TASK_PLAY_SEQUENCE ACTIVITY:ACT_QUEEN_HIGH_TO_LOW" " TASK_ASW_START_DIVER_ATTACK 0" "" " Interrupts" ) DEFINE_SCHEDULE ( SCHED_ASW_RETREAT_AND_SUMMON, " Tasks" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ASW_SUMMON_WAVE" " TASK_ASW_GET_PATH_TO_RETREAT_SPOT 0" " TASK_RUN_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_FACE_ENEMY 0" " TASK_ASW_SOUND_SUMMON 0" " TASK_PLAY_SEQUENCE ACTIVITY:ACT_QUEEN_SCREAM" " TASK_ASW_SUMMON_WAVE 0" "" " Interrupts" " COND_TASK_FAILED" ) DEFINE_SCHEDULE ( SCHED_SMASH_SENTRY, " Tasks" " TASK_FACE_SENTRY 0" " TASK_CLEAR_BLOCKING_SENTRY 0" " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack " TASK_MELEE_ATTACK1 0" "" " Interrupts" " COND_NEW_ENEMY" " COND_ENEMY_DEAD" " COND_LIGHT_DAMAGE" " COND_HEAVY_DAMAGE" " COND_ENEMY_OCCLUDED" ) DEFINE_SCHEDULE ( SCHED_ASW_SPAWN_PARASITES, " Tasks" " TASK_STOP_MOVING 0" " TASK_FACE_ENEMY 0" " TASK_PLAY_SEQUENCE ACTIVITY:ACT_QUEEN_HIGH_TO_LOW" " TASK_SPAWN_PARASITES 0" " TASK_PLAY_SEQUENCE ACTIVITY:ACT_QUEEN_LOW_TO_HIGH" "" " Interrupts" //" COND_HEAVY_DAMAGE" // can't do this as we need to be sure the chest closes when leaving this schedule ) AI_END_CUSTOM_NPC()