You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1769 lines
48 KiB
1769 lines
48 KiB
#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<CASW_Alien*>(pEntity); |
|
if (pAlien) |
|
pAlien->MeleeBleed(&info); |
|
CASW_Marine* pMarine = CASW_Marine::AsMarine( pEntity ); |
|
if (pMarine) |
|
pMarine->MeleeBleed(&info); |
|
else |
|
{ |
|
CASW_Colonist *pColonist = dynamic_cast<CASW_Colonist*>(pEntity); |
|
if (pColonist) |
|
{ |
|
pColonist->MeleeBleed(&info); |
|
} |
|
else |
|
{ |
|
CASW_Sentry_Base *pSentry = dynamic_cast<CASW_Sentry_Base*>(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<CBreakableProp*>(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;i<pGameResource->GetMaxMarineResources();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<CASW_Marine*>(info.GetAttacker()); |
|
if (pMarine) |
|
{ |
|
CASW_Weapon_Assault_Shotgun *pVindicator = dynamic_cast<CASW_Weapon_Assault_Shotgun*>(pMarine->GetActiveASWWeapon()); |
|
if (pVindicator) |
|
damage *= 0.45f; |
|
else |
|
damage *= 0.6f; |
|
} |
|
} |
|
} |
|
|
|
// make queen immune to buzzers |
|
if (dynamic_cast<CASW_Buzzer*>(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<CAI_BaseNPC*>(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<IASW_Spawnable_NPC*>(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<CASW_Parasite*>(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<CASW_Sentry_Base *>( 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<CASW_Sentry_Base*>(pEntity); |
|
if (pSentry) |
|
return true; |
|
CBreakableProp* pProp = dynamic_cast<CBreakableProp*>( 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()
|
|
|