diff options
Diffstat (limited to 'game/server/tf/player_vs_environment/boss_alpha/boss_alpha.cpp')
| -rw-r--r-- | game/server/tf/player_vs_environment/boss_alpha/boss_alpha.cpp | 1385 |
1 files changed, 1385 insertions, 0 deletions
diff --git a/game/server/tf/player_vs_environment/boss_alpha/boss_alpha.cpp b/game/server/tf/player_vs_environment/boss_alpha/boss_alpha.cpp new file mode 100644 index 0000000..043455f --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/boss_alpha.cpp @@ -0,0 +1,1385 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha.cpp +// Our first "real" TF Boss +// Michael Booth, November 2010 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "tf_projectile_arrow.h" +#include "tf_projectile_rocket.h" +#include "tf_weapon_grenade_pipebomb.h" +#include "tf_ammo_pack.h" +#include "tf_obj_sentrygun.h" +#include "nav_mesh/tf_nav_area.h" +#include "NextBot/Path/NextBotChasePath.h" +#include "econ_wearable.h" +#include "team_control_point_master.h" +#include "particle_parse.h" +#include "CRagdollMagnet.h" +#include "nav_mesh/tf_path_follower.h" +#include "bot_npc/bot_npc_minion.h" +#include "player_vs_environment/monster_resource.h" +#include "bot/map_entities/tf_bot_generator.h" + +#include "player_vs_environment/boss_alpha/boss_alpha.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_behavior.h" + + +//#define USE_BOSS_SENTRY + + +ConVar tf_boss_alpha_health( "tf_boss_alpha_health", "30000"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_attack_range( "tf_boss_alpha_attack_range", "300"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_threat_tolerance( "tf_boss_alpha_threat_tolerance", "100"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_chase_range( "tf_boss_alpha_chase_range", "300"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_grenade_launch_range( "tf_boss_alpha_grenade_launch_range", "300"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_quit_range( "tf_boss_alpha_quit_range", "2500"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_reaction_time( "tf_boss_alpha_reaction_time", "0.5"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_stunned_injury_multiplier( "tf_boss_alpha_stunned_injury_multiplier", "10" ); +ConVar tf_boss_alpha_head_radius( "tf_boss_alpha_head_radius", "75" ); // 50 +ConVar tf_boss_alpha_hate_taunt_cooldown( "tf_boss_alpha_hate_taunt_cooldown", "10"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_debug_damage( "tf_boss_alpha_debug_damage", "0"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_min_nuke_after_stun_time( "tf_boss_alpha_min_nuke_after_stun_time", "5" /*, FCVAR_CHEAT */ ); + +ConVar tf_boss_alpha_always_stun( "tf_boss_alpha_always_stun", "0"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_stun_rocket_reflect_count( "tf_boss_alpha_stun_rocket_reflect_count", "2"/*, FCVAR_CHEAT */ ); +ConVar tf_boss_alpha_stun_rocket_reflect_duration( "tf_boss_alpha_stun_rocket_reflect_duration", "1"/*, FCVAR_CHEAT */ ); + +ConVar tf_boss_alpha_debug_skill_shots( "tf_boss_alpha_debug_skill_shots", "0"/*, FCVAR_CHEAT */ ); + +extern ConVar tf_boss_alpha_nuke_interval; + + +//----------------------------------------------------------------------------------------------------- +// The Alpha Boss: A rocket and stickybomb firing giant robot that periodically charges up a big +// "nuke" attack, and is invulnerable unless stunned. +//----------------------------------------------------------------------------------------------------- +LINK_ENTITY_TO_CLASS( boss_alpha, CBossAlpha ); + +PRECACHE_REGISTER( boss_alpha ); + +IMPLEMENT_SERVERCLASS_ST( CBossAlpha, DT_BossAlpha ) + + SendPropBool( SENDINFO( m_isNuking ) ), + +END_SEND_TABLE() + + +BEGIN_DATADESC( CBossAlpha ) + DEFINE_OUTPUT( m_outputOnStunned, "OnStunned" ), + DEFINE_OUTPUT( m_outputOnHealthBelow90Percent, "OnHealthBelow90Percent" ), + DEFINE_OUTPUT( m_outputOnHealthBelow80Percent, "OnHealthBelow80Percent" ), + DEFINE_OUTPUT( m_outputOnHealthBelow70Percent, "OnHealthBelow70Percent" ), + DEFINE_OUTPUT( m_outputOnHealthBelow60Percent, "OnHealthBelow60Percent" ), + DEFINE_OUTPUT( m_outputOnHealthBelow50Percent, "OnHealthBelow50Percent" ), + DEFINE_OUTPUT( m_outputOnHealthBelow40Percent, "OnHealthBelow40Percent" ), + DEFINE_OUTPUT( m_outputOnHealthBelow30Percent, "OnHealthBelow30Percent" ), + DEFINE_OUTPUT( m_outputOnHealthBelow20Percent, "OnHealthBelow20Percent" ), + DEFINE_OUTPUT( m_outputOnHealthBelow10Percent, "OnHealthBelow10Percent" ), + DEFINE_OUTPUT( m_outputOnKilled, "OnKilled" ), +END_DATADESC() + + + + +//----------------------------------------------------------------------------------------------------- +CBossAlpha::CBossAlpha() +{ + m_intention = new CBossAlphaIntention( this ); + m_locomotor = new CBossAlphaLocomotion( this ); + m_body = new CBotNPCBody( this ); + m_vision = new CBossAlphaVision( this ); + + m_conditionFlags = 0; + m_isNuking = false; + m_ageTimer.Invalidate(); + + m_lastHealthPercentage = 1.0f; + + ClearStunDamage(); +} + + +//----------------------------------------------------------------------------------------------------- +CBossAlpha::~CBossAlpha() +{ + if ( m_intention ) + delete m_intention; + + if ( m_locomotor ) + delete m_locomotor; + + if ( m_body ) + delete m_body; + + if ( m_vision ) + delete m_vision; +} + + +//----------------------------------------------------------------------------------------------------- +void CBossAlpha::Precache() +{ + BaseClass::Precache(); + +#ifdef USE_BOSS_SENTRY + int model = PrecacheModel( "models/bots/boss_sentry/boss_sentry.mdl" ); +#else + int model = PrecacheModel( "models/bots/knight/knight.mdl" ); +#endif + + PrecacheGibsForModel( model ); + + PrecacheScriptSound( "Weapon_Sword.Swing" ); + PrecacheScriptSound( "Weapon_Sword.HitFlesh" ); + PrecacheScriptSound( "Weapon_Sword.HitWorld" ); + PrecacheScriptSound( "DemoCharge.HitWorld" ); + PrecacheScriptSound( "TFPlayer.Pain" ); + PrecacheScriptSound( "Halloween.HeadlessBossAttack" ); + PrecacheScriptSound( "RobotBoss.StunStart" ); + PrecacheScriptSound( "RobotBoss.Stunned" ); + PrecacheScriptSound( "RobotBoss.StunRecover" ); + PrecacheScriptSound( "RobotBoss.Acquire" ); + PrecacheScriptSound( "RobotBoss.Vocalize" ); + PrecacheScriptSound( "RobotBoss.Footstep" ); + PrecacheScriptSound( "RobotBoss.LaunchGrenades" ); + PrecacheScriptSound( "RobotBoss.LaunchRockets" ); + PrecacheScriptSound( "RobotBoss.Hurt" ); + PrecacheScriptSound( "RobotBoss.Vulnerable" ); + PrecacheScriptSound( "RobotBoss.ChargeUpNukeAttack" ); + PrecacheScriptSound( "RobotBoss.NukeAttack" ); + PrecacheScriptSound( "RobotBoss.Scanning" ); + PrecacheScriptSound( "RobotBoss.ReinforcementsArrived" ); + PrecacheScriptSound( "RobotBoss.HardHitSkillShot" ); + PrecacheScriptSound( "RobotBoss.DamageSpongeSkillShot" ); + PrecacheScriptSound( "RobotBoss.PreciseHit1SkillShot" ); + PrecacheScriptSound( "RobotBoss.PreciseHit2SkillShot" ); + PrecacheScriptSound( "RobotBoss.PreciseHit3SkillShot" ); + PrecacheScriptSound( "Cart.Explode" ); + PrecacheScriptSound( "Weapon_Crowbar.Melee_HitWorld" ); + + PrecacheParticleSystem( "asplode_hoodoo_embers" ); + PrecacheParticleSystem( "charge_up" ); +} + + +//----------------------------------------------------------------------------------------------------- +void CBossAlpha::Spawn( void ) +{ + BaseClass::Spawn(); + +#ifdef USE_BOSS_SENTRY + SetModel( "models/bots/boss_sentry/boss_sentry.mdl" ); +#else + SetModel( "models/bots/knight/knight.mdl" ); +#endif + + m_conditionFlags = 0; + + ClearStunDamage(); + ResetSkillShots(); + + int health = tf_boss_alpha_health.GetInt(); + SetHealth( health ); + SetMaxHealth( health ); + + // show Boss' health meter on HUD + if ( g_pMonsterResource ) + { + g_pMonsterResource->SetBossHealthPercentage( 1.0f ); + } + + m_damagePoseParameter = -1; + + // randomize initial check + m_nearestVisibleEnemy = NULL; + m_nearestVisibleEnemyTimer.Start( RandomFloat( 0.0f, tf_boss_alpha_reaction_time.GetFloat() ) ); + + m_homePos = GetAbsOrigin(); + + m_currentDamagePerSecond = 0.0f; + m_lastDamagePerSecond = 0.0f; + + m_attackTarget = NULL; + m_attackTargetTimer.Invalidate(); + m_isAttackTargetLocked = false; + + m_nukeTimer.Start( tf_boss_alpha_nuke_interval.GetFloat() ); + m_isNuking = false; + + m_grenadeTimer.Start( GetGrenadeInterval() ); + m_ageTimer.Start(); + + m_lastHealthPercentage = 1.0f; + + ChangeTeam( TF_TEAM_RED ); + + TFGameRules()->SetActiveBoss( this ); + + // CollisionProp()->SetSurroundingBoundsType( USE_HITBOXES ); + + Vector mins( -50, -50, 0 ); + Vector maxs( 100, 100, 275 ); + CollisionProp()->SetSurroundingBoundsType( USE_SPECIFIED_BOUNDS, &mins, &maxs ); + + Vector collideMins( -50, -50, 125 ); + Vector collideMaxs( 50, 50, 260 ); + CollisionProp()->SetCollisionBounds( collideMins, collideMaxs ); +} + + +//----------------------------------------------------------------------------------------------------- +ConVar tf_boss_alpha_dmg_mult_sniper( "tf_boss_alpha_dmg_mult_sniper", "1"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_dmg_mult_minigun( "tf_boss_alpha_dmg_mult_minigun", "0.3"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_dmg_mult_flamethrower( "tf_boss_alpha_dmg_mult_flamethrower", "1"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_dmg_mult_sentrygun( "tf_boss_alpha_dmg_mult_sentrygun", "0.3"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_dmg_mult_grenade( "tf_boss_alpha_dmg_mult_grenade", "0.3"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_dmg_mult_rocket( "tf_boss_alpha_dmg_mult_rocket", "0.5"/*, FCVAR_CHEAT*/ ); + + +float ModifyBossDamage( const CTakeDamageInfo &info ) +{ + CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase * >( info.GetWeapon() ); + + if ( pWeapon ) + { + switch( pWeapon->GetWeaponID() ) + { + case TF_WEAPON_SNIPERRIFLE: + case TF_WEAPON_SNIPERRIFLE_DECAP: + case TF_WEAPON_SNIPERRIFLE_CLASSIC: + case TF_WEAPON_COMPOUND_BOW: + return info.GetDamage() * tf_boss_alpha_dmg_mult_sniper.GetFloat(); + + case TF_WEAPON_MINIGUN: + return info.GetDamage() * tf_boss_alpha_dmg_mult_minigun.GetFloat(); + + case TF_WEAPON_FLAMETHROWER: + return info.GetDamage() * tf_boss_alpha_dmg_mult_flamethrower.GetFloat(); + + case TF_WEAPON_SENTRY_BULLET: + return info.GetDamage() * tf_boss_alpha_dmg_mult_sentrygun.GetFloat(); + + case TF_WEAPON_GRENADELAUNCHER: + case TF_WEAPON_PIPEBOMBLAUNCHER: + case TF_WEAPON_GRENADE_DEMOMAN: + return info.GetDamage() * tf_boss_alpha_dmg_mult_grenade.GetFloat(); + + case TF_WEAPON_ROCKETLAUNCHER: + case TF_WEAPON_ROCKETLAUNCHER_DIRECTHIT: + return info.GetDamage() * tf_boss_alpha_dmg_mult_rocket.GetFloat(); + } + } + + // unmodified + return info.GetDamage(); +} + +#define HITBOX_SKILL_STICKYBOMB_1 23 +#define HITBOX_SKILL_STICKYBOMB_2 24 + +#define HITBOX_SKILL_PRECISION_1 20 +#define HITBOX_SKILL_PRECISION_2 21 +#define HITBOX_SKILL_PRECISION_3 22 +#define PRECISION_SHOT_COUNT 3 + +#define HITBOX_SKILL_DAMAGE_SPONGE 19 + +#define HITBOX_SKILL_HARD_HIT 18 + +ConVar tf_boss_alpha_skill_shot_combo_time( "tf_boss_alpha_skill_shot_combo_time", "10"/*, FCVAR_CHEAT */ ); +ConVar tf_boss_alpha_skill_shot_count( "tf_boss_alpha_skill_shot_count", "3"/*, FCVAR_CHEAT */ ); +ConVar tf_boss_alpha_skill_shot_precision_time( "tf_boss_alpha_skill_shot_precision_time", "6"/*, FCVAR_CHEAT */ ); +ConVar tf_boss_alpha_skill_shot_hard_hit_damage( "tf_boss_alpha_skill_shot_hard_hit_damage", "40"/*, FCVAR_CHEAT */ ); +ConVar tf_boss_alpha_skill_shot_hard_hit_z( "tf_boss_alpha_skill_shot_hard_hit_z", "0"/*, FCVAR_CHEAT */ ); +ConVar tf_boss_alpha_skill_shot_damage_sponge_total( "tf_boss_alpha_skill_shot_damage_sponge_total", "500"/*, FCVAR_CHEAT */ ); +ConVar tf_boss_alpha_skill_shot_damage_sponge_decay( "tf_boss_alpha_skill_shot_damage_sponge_decay", "100"/*, FCVAR_CHEAT */ ); + + +//----------------------------------------------------------------------------------------------------- +void CBossAlpha::ResetSkillShots( void ) +{ + m_skillShotComboTimer.Invalidate(); + m_skillShotCount = 0; + + m_isPrecisionShotDone = false; + m_precisionSkillShotTimer.Invalidate(); + + for( int i=0; i<PRECISION_SHOT_COUNT; ++i ) + { + m_isPrecisionShotHit[i] = false; + } + + m_isDamageSpongeSkillShotDone = false; + m_damageSpongeSkillShotAmount = 0.0f; + + m_isHardHitSkillShotDone = false; + + if ( g_pMonsterResource ) + { + g_pMonsterResource->HideSkillShotComboMeter(); + } +} + + +//----------------------------------------------------------------------------------------------------- +void CBossAlpha::OnSkillShotComboStarted( void ) +{ + m_skillShotComboTimer.Start( tf_boss_alpha_skill_shot_combo_time.GetFloat() ); + + if ( g_pMonsterResource ) + { + g_pMonsterResource->StartSkillShotComboMeter( tf_boss_alpha_skill_shot_combo_time.GetFloat() ); + } +} + + +//----------------------------------------------------------------------------------------------------- +void CBossAlpha::OnSkillShot( void ) +{ + if ( !m_skillShotComboTimer.HasStarted() || m_skillShotComboTimer.IsElapsed() ) + { + // start a new combo + OnSkillShotComboStarted(); + m_skillShotCount = 1; + } + else + { + // combo in progress + ++m_skillShotCount; + + if ( g_pMonsterResource ) + { + g_pMonsterResource->IncrementSkillShotComboMeter(); + } + } + + if ( m_skillShotCount >= tf_boss_alpha_skill_shot_count.GetInt() ) + { + AddCondition( STUNNED ); + EmitSound( "RobotBoss.Vulnerable" ); + } +} + + +//----------------------------------------------------------------------------------------------------- +// +// Invoked when we are struck. Check if a vulnerability was hit, and update the skill shot combo +// +bool CBossAlpha::CheckSkillShots( const CTakeDamageInfo &info ) +{ + if ( !HasAbility( CBossAlpha::CAN_BE_STUNNED ) || !info.GetAttacker() ) + { + return false; + } + + // skill shots are not available until the boss recovers + if ( IsInCondition( STUNNED ) ) + { + return false; + } + + if ( tf_boss_alpha_always_stun.GetBool() ) + { + m_skillShotComboTimer.Start( 1.0f ); + m_skillShotCount = 999; + OnSkillShot(); + return true; + } + +// const Vector &hitSpot = info.GetDamagePosition(); + + CBaseEntity *inflictor = info.GetInflictor(); + if ( !inflictor ) + { + return false; + } + + Vector hitDir = m_lastTraceAttackDir; + +/* + Vector hitDir = inflictor->GetAbsVelocity(); + + if ( inflictor->IsPlayer() ) + { + hitDir = hitSpot - inflictor->EyePosition(); + } + else + { + CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( inflictor ); + if ( sentry ) + { + hitDir = hitSpot - sentry->EyePosition(); + } + } + + hitDir.NormalizeInPlace(); +*/ + + Vector traceFrom = m_lastTraceAttackTrace.startpos - m_lastTraceAttackDir * 10.0f; + Vector traceTo = m_lastTraceAttackTrace.endpos + m_lastTraceAttackDir * 100.0f; + + trace_t result; + //UTIL_TraceLine( hitSpot - 50.0f * hitDir, hitSpot + 50.0f * hitDir, MASK_SOLID | CONTENTS_HITBOX, inflictor, COLLISION_GROUP_NONE, &result ); + UTIL_TraceLine( traceFrom, traceTo, MASK_SOLID | CONTENTS_HITBOX, inflictor, COLLISION_GROUP_NONE, &result ); + + if ( tf_boss_alpha_debug_skill_shots.GetBool() ) + { + if ( result.hitbox != 0 ) + { + NDebugOverlay::HorzArrow( traceFrom, traceTo, 3.0f, 0, 255, 0, 255, true, 9999.9f ); + } + else + { + NDebugOverlay::HorzArrow( traceFrom, traceTo, 3.0f, 255, 0, 0, 255, true, 9999.9f ); + } + } + + if ( !result.DidHit() ) + { + return false; + } + + switch( result.hitbox ) + { + case HITBOX_SKILL_PRECISION_1: + case HITBOX_SKILL_PRECISION_2: + case HITBOX_SKILL_PRECISION_3: + { + int which = result.hitbox - HITBOX_SKILL_PRECISION_1; + + if ( !m_isPrecisionShotDone && !m_isPrecisionShotHit[ which ] ) + { + if ( !m_precisionSkillShotTimer.HasStarted() ) + { + m_precisionSkillShotTimer.Start( tf_boss_alpha_skill_shot_precision_time.GetFloat() ); + } + + m_isPrecisionShotHit[ which ] = true; + UTIL_ClientPrintAll( HUD_PRINTTALK, CFmtStr( "PRECISION SHOT %d...", which+1 ) ); + + int i; + for( i=0; i<PRECISION_SHOT_COUNT; ++i ) + { + if ( !m_isPrecisionShotHit[i] ) + break; + } + + if ( i == PRECISION_SHOT_COUNT ) + { + // successfully completed the precision shot + m_isPrecisionShotDone = true; + UTIL_ClientPrintAll( HUD_PRINTTALK, "PRECISION SKILL SHOT!" ); + EmitSound( CFmtStr( "RobotBoss.PreciseHit%dSkillShot", which+1 ) ); + OnSkillShot(); + } + return true; + } + break; + } + + case HITBOX_SKILL_DAMAGE_SPONGE: + if ( !m_isDamageSpongeSkillShotDone ) + { + m_damageSpongeSkillShotAmount += info.GetDamage(); + + if ( m_damageSpongeSkillShotAmount > tf_boss_alpha_skill_shot_damage_sponge_total.GetFloat() ) + { + // successfully completed the damage sponge shot + m_isDamageSpongeSkillShotDone = true; + m_damageSpongeSkillShotAmount = 0.0f; + UTIL_ClientPrintAll( HUD_PRINTTALK, "DAMAGE SPONGE SKILL SHOT!" ); + EmitSound( "RobotBoss.DamageSpongeSkillShot" ); + OnSkillShot(); + return true; + } + + UTIL_ClientPrintAll( HUD_PRINTTALK, CFmtStr( "DAMAGE SPONGE = %3.2f", m_damageSpongeSkillShotAmount ) ); + return true; + } + break; + + case HITBOX_SKILL_HARD_HIT: + if ( !m_isHardHitSkillShotDone ) + { + if ( info.GetDamage() > tf_boss_alpha_skill_shot_hard_hit_damage.GetFloat() ) + { + // make sure player hit from above + if ( info.GetAttacker() ) + { + Vector toAttacker = info.GetAttacker()->EyePosition() - m_lastTraceAttackTrace.endpos; + toAttacker.NormalizeInPlace(); + + if ( toAttacker.z > tf_boss_alpha_skill_shot_hard_hit_z.GetFloat() ) + { + // successfully completed the hard hit shot + m_isHardHitSkillShotDone = true; + UTIL_ClientPrintAll( HUD_PRINTTALK, "HARD HIT SKILL SHOT!" ); + EmitSound( "RobotBoss.HardHitSkillShot" ); + OnSkillShot(); + } + } + } + return true; + } + break; + } + + return false; +} + + +//----------------------------------------------------------------------------------------------------- +void CBossAlpha::UpdateSkillShots( void ) +{ + m_damageSpongeSkillShotAmount -= tf_boss_alpha_skill_shot_damage_sponge_decay.GetFloat() * gpGlobals->frametime; + if ( m_damageSpongeSkillShotAmount < 0.0f ) + { + m_damageSpongeSkillShotAmount = 0.0f; + } + else + { + UTIL_ClientPrintAll( HUD_PRINTTALK, CFmtStr( "DAMAGE SPONGE = %3.2f/%3.2f", m_damageSpongeSkillShotAmount, tf_boss_alpha_skill_shot_damage_sponge_total.GetFloat() ) ); + } + + if ( m_skillShotComboTimer.HasStarted() && m_skillShotComboTimer.IsElapsed() ) + { + // took too long to perform skill shots - reset combo + ResetSkillShots(); + UTIL_ClientPrintAll( HUD_PRINTTALK, "SKILL SHOT CHAIN FAILED - TOO SLOW!" ); + } + + if ( !m_isPrecisionShotDone && m_precisionSkillShotTimer.HasStarted() && m_precisionSkillShotTimer.IsElapsed() ) + { + // took too long to hit all the precision targets - reset + m_precisionSkillShotTimer.Invalidate(); + for( int i=0; i<PRECISION_SHOT_COUNT; ++i ) + { + m_isPrecisionShotHit[i] = false; + } + UTIL_ClientPrintAll( HUD_PRINTTALK, "PRECISION SHOTS RESET - TOO SLOW!" ); + } +} + + +//----------------------------------------------------------------------------------------------------- +int CBossAlpha::OnTakeDamage_Alive( const CTakeDamageInfo &rawInfo ) +{ + CTakeDamageInfo info = rawInfo; + + // don't take damage from myself + if ( info.GetAttacker() == this ) + { + return 0; + } + + // weapon-specific damage modification + info.SetDamage( ModifyBossDamage( info ) ); + + // do the critical damage increase + if ( info.GetDamageType() & DMG_CRITICAL ) + { + info.SetDamage( info.GetDamage() * TF_DAMAGE_CRIT_MULTIPLIER ); + } + + bool isSkillShot = false; + if ( CheckSkillShots( info ) ) + { + isSkillShot = true; + + // skill shots don't deal damage + info.SetDamage( 0 ); + } + + bool isHeadHit = false; + if ( IsInCondition( VULNERABLE_TO_STUN ) ) + { + // track head damage when vulnerable + Vector headPos; + QAngle headAngles; + if ( GetAttachment( "head", headPos, headAngles ) ) + { + Vector damagePos = info.GetDamagePosition(); + + if ( tf_boss_alpha_debug_damage.GetBool() ) + { + NDebugOverlay::Cross3D( headPos, 5.0f, 255, 0, 0, true, 5.0f ); + NDebugOverlay::Cross3D( damagePos, 5.0f, 0, 255, 0, true, 5.0f ); + NDebugOverlay::Line( damagePos, headPos, 255, 255, 0, true, 5.0f ); + } + + isHeadHit = ( damagePos - headPos ).IsLengthLessThan( tf_boss_alpha_head_radius.GetFloat() ); + + if ( isHeadHit ) + { + // hit the head + AccumulateStunDamage( info.GetDamage() ); + DispatchParticleEffect( "asplode_hoodoo_embers", info.GetDamagePosition(), GetAbsAngles() ); + + if ( tf_boss_alpha_debug_damage.GetBool() ) + { + DevMsg( "Stun dmg = %f\n", GetStunDamage() ); + NDebugOverlay::Circle( headPos, tf_boss_alpha_head_radius.GetFloat(), 255, 0, 0, 255, true, 5.0f ); + } + } + else if ( tf_boss_alpha_debug_damage.GetBool() ) + { + NDebugOverlay::Circle( headPos, tf_boss_alpha_head_radius.GetFloat(), 255, 255, 0, 255, true, 5.0f ); + } + } + } + + // take extra damage when stunned + if ( IsInCondition( STUNNED ) ) + { + info.SetDamage( info.GetDamage() * tf_boss_alpha_stunned_injury_multiplier.GetFloat() ); + + if ( m_ouchTimer.IsElapsed() ) + { + m_ouchTimer.Start( 1.0f ); + EmitSound( "RobotBoss.Hurt" ); + } + } + else if ( !isHeadHit && !isSkillShot ) + { + // invulnerable until stunned + if ( m_ricochetSoundTimer.IsElapsed() ) + { + TFGameRules()->BroadcastSound( 255, "Weapon_Crowbar.Melee_HitWorld" ); + m_ricochetSoundTimer.Start( 0.15f ); + } + + return 0; + } + + + // keep a list of everyone who hurt me, and when + if ( info.GetAttacker() && info.GetAttacker()->MyCombatCharacterPointer() && !InSameTeam( info.GetAttacker() ) ) + { + CBaseCombatCharacter *attacker = info.GetAttacker()->MyCombatCharacterPointer(); + + // sentry guns are first class attackers + if ( info.GetInflictor() ) + { + CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( info.GetInflictor() ); + if ( sentry ) + { + attacker = sentry; + } + } + + RememberAttacker( attacker, info.GetDamage(), ( info.GetDamageType() & DMG_CRITICAL ) ? true : false ); + + CTFPlayer *playerAttacker = ToTFPlayer( attacker ); + if ( playerAttacker ) + { + for( int i=0; i<playerAttacker->m_Shared.GetNumHealers(); ++i ) + { + CTFPlayer *medic = ToTFPlayer( playerAttacker->m_Shared.GetHealerByIndex( i ) ); + if ( medic ) + { + // medics healing my attacker are also considered attackers + RememberAttacker( medic, 0, 0 ); + } + } + } + + // if we don't have an attack target yet, we do now + if ( !HasAttackTarget() ) + { + SetAttackTarget( attacker ); + } + } + + + // fire event for client combat text, beep, etc. + IGameEvent *event = gameeventmanager->CreateEvent( "npc_hurt" ); + if ( event ) + { + event->SetInt( "entindex", entindex() ); + event->SetInt( "health", MAX( 0, GetHealth() ) ); + event->SetInt( "damageamount", info.GetDamage() ); + event->SetBool( "crit", ( info.GetDamageType() & DMG_CRITICAL ) ? true : false ); + + CTFPlayer *attackerPlayer = ToTFPlayer( info.GetAttacker() ); + if ( attackerPlayer ) + { + event->SetInt( "attacker_player", attackerPlayer->GetUserID() ); + + if ( attackerPlayer->GetActiveTFWeapon() ) + { + event->SetInt( "weaponid", attackerPlayer->GetActiveTFWeapon()->GetWeaponID() ); + } + else + { + event->SetInt( "weaponid", 0 ); + } + } + else + { + // hurt by world + event->SetInt( "attacker_player", 0 ); + event->SetInt( "weaponid", 0 ); + } + + gameeventmanager->FireEvent( event ); + } + + int result = BaseClass::OnTakeDamage_Alive( info ); + + // emit injury outputs + float healthPercentage = (float)GetHealth() / (float)GetMaxHealth(); + + if ( m_lastHealthPercentage > 0.9f && healthPercentage < 0.9f ) + { + m_outputOnHealthBelow90Percent.FireOutput( this, this ); + } + else if ( m_lastHealthPercentage > 0.8f && healthPercentage < 0.8f ) + { + m_outputOnHealthBelow80Percent.FireOutput( this, this ); + } + else if ( m_lastHealthPercentage > 0.7f && healthPercentage < 0.7f ) + { + m_outputOnHealthBelow70Percent.FireOutput( this, this ); + } + else if ( m_lastHealthPercentage > 0.6f && healthPercentage < 0.6f ) + { + m_outputOnHealthBelow60Percent.FireOutput( this, this ); + } + else if ( m_lastHealthPercentage > 0.5f && healthPercentage < 0.5f ) + { + m_outputOnHealthBelow50Percent.FireOutput( this, this ); + } + else if ( m_lastHealthPercentage > 0.4f && healthPercentage < 0.4f ) + { + m_outputOnHealthBelow40Percent.FireOutput( this, this ); + } + else if ( m_lastHealthPercentage > 0.3f && healthPercentage < 0.3f ) + { + m_outputOnHealthBelow30Percent.FireOutput( this, this ); + } + else if ( m_lastHealthPercentage > 0.2f && healthPercentage < 0.2f ) + { + m_outputOnHealthBelow20Percent.FireOutput( this, this ); + } + else if ( m_lastHealthPercentage > 0.1f && healthPercentage < 0.1f ) + { + m_outputOnHealthBelow10Percent.FireOutput( this, this ); + } + + m_lastHealthPercentage = healthPercentage; + + if ( g_pMonsterResource ) + { + g_pMonsterResource->SetBossHealthPercentage( healthPercentage ); + } + + return result; +} + + +//--------------------------------------------------------------------------------------------- +// Returns true if we're in a condition that means we can't start another action +bool CBossAlpha::IsBusy( void ) const +{ + return IsInCondition( (Condition)( CHARGING | STUNNED | VULNERABLE_TO_STUN | BUSY ) ); +} + + +//--------------------------------------------------------------------------------------------- +void CBossAlpha::RememberAttacker( CBaseCombatCharacter *attacker, float damage, bool wasCritical ) +{ + AttackerInfo attackerInfo; + + attackerInfo.m_attacker = attacker; + attackerInfo.m_timestamp = gpGlobals->curtime; + attackerInfo.m_damage = damage; + attackerInfo.m_wasCritical = wasCritical; + + m_attackerVector.AddToHead( attackerInfo ); +} + + +//---------------------------------------------------------------------------------- +CTFPlayer *CBossAlpha::GetClosestMinionPrisoner( void ) +{ + CUtlVector< CBotNPCMinion * > minionVector; + CBotNPCMinion *minion = NULL; + while( ( minion = (CBotNPCMinion *)gEntList.FindEntityByClassname( minion, "bot_npc_minion" ) ) != NULL ) + { + minionVector.AddToTail( minion ); + } + + CTFPlayer *closeCapture = NULL; + float captureRangeSq = FLT_MAX; + + for( int m=0; m<minionVector.Count(); ++m ) + { + minion = minionVector[m]; + + if ( minion->HasTarget() ) + { + CTFPlayer *victim = minion->GetTarget(); + if ( victim->m_Shared.InCond( TF_COND_STUNNED ) ) + { + // they've got one! + float rangeSq = GetRangeSquaredTo( victim ); + if ( rangeSq < captureRangeSq ) + { + closeCapture = victim; + captureRangeSq = rangeSq; + } + } + } + } + + return closeCapture; +} + + +//---------------------------------------------------------------------------------- +bool CBossAlpha::IsPrisonerOfMinion( CBaseCombatCharacter *victim ) +{ + if ( !victim->IsPlayer() ) + { + return false; + } + + CUtlVector< CBotNPCMinion * > minionVector; + CBotNPCMinion *minion = NULL; + while( ( minion = (CBotNPCMinion *)gEntList.FindEntityByClassname( minion, "bot_npc_minion" ) ) != NULL ) + { + minionVector.AddToTail( minion ); + } + + for( int m=0; m<minionVector.Count(); ++m ) + { + minion = minionVector[m]; + + if ( minion->HasTarget() && minion->GetTarget() == victim ) + { + if ( minion->GetTarget()->m_Shared.InCond( TF_COND_STUNNED ) ) + { + return true; + } + } + } + + return false; +} + + +//---------------------------------------------------------------------------------- +void CBossAlpha::UpdateDamagePerSecond( void ) +{ + m_lastDamagePerSecond = m_currentDamagePerSecond; + + m_currentDamagePerSecond = 0.0f; + + const float windowDuration = 10.0f; // 5.0f; + int i; + + m_threatVector.RemoveAll(); + + for( i=0; i<m_attackerVector.Count(); ++i ) + { + float age = gpGlobals->curtime - m_attackerVector[i].m_timestamp; + + if ( age > windowDuration ) + { + // too old + break; + } + + float decayedDamage = ( ( windowDuration - age ) / windowDuration ) * m_attackerVector[i].m_damage; + + m_currentDamagePerSecond += decayedDamage; + + CBaseCombatCharacter *attacker = m_attackerVector[i].m_attacker; + + if ( attacker && attacker->IsAlive() ) + { + int j; + for( j=0; j<m_threatVector.Count(); ++j ) + { + if ( m_threatVector[j].m_who == attacker ) + { + m_threatVector[j].m_threat += decayedDamage; + break; + } + } + + if ( j >= m_threatVector.Count() ) + { + // new threat + ThreatInfo threat; + threat.m_who = attacker; + threat.m_threat = decayedDamage; + m_threatVector.AddToTail( threat ); + } + } + } + +// if ( m_currentDamagePerSecond > 0.0001f ) +// { +// DevMsg( "%3.2f: dps = %3.2f\n", gpGlobals->curtime, m_currentDamagePerSecond ); +// } +} + + +//---------------------------------------------------------------------------------- +const CBossAlpha::ThreatInfo *CBossAlpha::GetMaxThreat( void ) const +{ + int maxThreatIndex = -1; + + for( int i=0; i<m_threatVector.Count(); ++i ) + { + if ( maxThreatIndex < 0 || m_threatVector[i].m_threat > m_threatVector[ maxThreatIndex ].m_threat ) + { + maxThreatIndex = i; + } + } + + if ( maxThreatIndex < 0 ) + { + // no threat yet + return NULL; + } + + return &m_threatVector[ maxThreatIndex ]; +} + + +//---------------------------------------------------------------------------------- +const CBossAlpha::ThreatInfo *CBossAlpha::GetThreat( CBaseCombatCharacter *who ) const +{ + for( int i=0; i<m_threatVector.Count(); ++i ) + { + if ( m_threatVector[i].m_who == who ) + { + return &m_threatVector[i]; + } + } + + return NULL; +} + + +//---------------------------------------------------------------------------------- +void CBossAlpha::UpdateAttackTarget( void ) +{ + if ( m_isAttackTargetLocked && HasAttackTarget() ) + { + return; + } + + // who is most dangerous to me at the moment + const ThreatInfo *maxThreat = GetMaxThreat(); + + if ( !maxThreat ) + { + // nobody is hurting me at the moment + + if ( HasAttackTarget() ) + { + // stay focused on current target + return; + } + + // we have no current target, either + + // if my minions have captured someone, go get them + CTFPlayer *closeCapture = GetClosestMinionPrisoner(); + if ( closeCapture ) + { + SetAttackTarget( closeCapture ); + return; + } + + // if we see an enemy, attack them + CBaseCombatCharacter *visible = GetNearestVisibleEnemy(); + if ( visible ) + { + SetAttackTarget( visible ); + } + + return; + } + + // we are under attack, if we don't have a target, attack the highest threat + if ( !HasAttackTarget() ) + { + SetAttackTarget( maxThreat->m_who ); + return; + } + + if ( IsAttackTarget( maxThreat->m_who ) ) + { + // our current target is still dealing the most damage to us + return; + } + + // switch to new threat if is is more dangerous + const ThreatInfo *attackTargetThreat = GetThreat( GetAttackTarget() ); + + if ( !attackTargetThreat || maxThreat->m_threat > attackTargetThreat->m_threat + tf_boss_alpha_threat_tolerance.GetFloat() ) + { + // change threats + SetAttackTarget( maxThreat->m_who ); + } +} + + +//---------------------------------------------------------------------------------- +void CBossAlpha::RemoveCondition( Condition c ) +{ + if ( c == STUNNED ) + { + // reset the accumulator + ClearStunDamage(); + + ResetSkillShots(); + } + + m_conditionFlags &= ~c; +} + + +//--------------------------------------------------------------------------------------------- +void CBossAlpha::Update( void ) +{ + BaseClass::Update(); + + UpdateNearestVisibleEnemy(); + UpdateDamagePerSecond(); + UpdateAttackTarget(); + UpdateSkillShots(); + + if ( m_damagePoseParameter < 0 ) + { + m_damagePoseParameter = LookupPoseParameter( "damage" ); + } + + if ( m_damagePoseParameter >= 0 ) + { + SetPoseParameter( m_damagePoseParameter, 1.0f - ( (float)GetHealth() / (float)GetMaxHealth() ) ); + } + + // chase down players who taunt me + if ( m_hateTauntTimer.IsElapsed() ) + { + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS ); + + for( int i=0; i<playerVector.Count(); ++i ) + { + if ( playerVector[i]->IsTaunting() ) + { + m_hateTauntTimer.Start( tf_boss_alpha_hate_taunt_cooldown.GetFloat() ); + + if ( IsLineOfSightClear( playerVector[i], IGNORE_ACTORS ) ) + { + // the taunter becomes our new attack target + SetAttackTarget( playerVector[i], tf_boss_alpha_hate_taunt_cooldown.GetFloat() ); + } + } + } + } +} + + +//--------------------------------------------------------------------------------------------- +bool CBossAlpha::IsIgnored( CTFPlayer *player ) const +{ + if ( player->m_Shared.IsStealthed() ) + { + if ( player->m_Shared.GetPercentInvisible() < 0.75f ) + { + // spy is partially cloaked, and therefore attracts our attention + return false; + } + + if ( player->m_Shared.InCond( TF_COND_BURNING ) || + player->m_Shared.InCond( TF_COND_URINE ) || + player->m_Shared.InCond( TF_COND_STEALTHED_BLINK ) || + player->m_Shared.InCond( TF_COND_BLEEDING ) ) + { + // always notice players with these conditions + return false; + } + + // invisible! + return true; + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +void CBossAlpha::UpdateNearestVisibleEnemy( void ) +{ + if ( !m_nearestVisibleEnemyTimer.IsElapsed() ) + { + return; + } + + m_nearestVisibleEnemyTimer.Start( tf_boss_alpha_reaction_time.GetFloat() ); + + // collect everyone + CUtlVector< CTFPlayer * > playerVector; + //CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + Vector myForward; + GetVectors( &myForward, NULL, NULL ); + + m_nearestVisibleEnemy = NULL; + float victimRangeSq = FLT_MAX; + + for( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *victim = playerVector[i]; + + if ( IsIgnored( victim ) ) + { + continue; + } + + float rangeSq = GetRangeSquaredTo( playerVector[i] ); + if ( rangeSq < victimRangeSq ) + { + // FOV check + Vector to = playerVector[i]->WorldSpaceCenter() - WorldSpaceCenter(); + to.NormalizeInPlace(); + + if ( DotProduct( to, myForward ) > -0.7071f ) + { + if ( IsLineOfSightClear( playerVector[i] ) ) + { + m_nearestVisibleEnemy = playerVector[i]; + victimRangeSq = rangeSq; + } + } + } + } +} + + +//--------------------------------------------------------------------------------------------- +void CBossAlpha::SetAttackTarget( CBaseCombatCharacter *target, float duration ) +{ + if ( target && m_attackTarget != NULL && m_attackTarget->IsAlive() && m_attackTargetTimer.HasStarted() && !m_attackTargetTimer.IsElapsed() ) + { + // can't switch away from our still valid target yet + return; + } + + if ( m_attackTarget != target ) + { + if ( target ) + { + EmitSound( "RobotBoss.Acquire" ); + AddGesture( ACT_MP_GESTURE_FLINCH_CHEST ); + } + + TFGameRules()->SetIT( m_attackTarget ); + + m_attackTarget = target; + } + + if ( duration > 0.0f ) + { + m_attackTargetTimer.Start( duration ); + } + else + { + m_attackTargetTimer.Invalidate(); + } +} + + +//--------------------------------------------------------------------------------------------- +CBaseCombatCharacter *CBossAlpha::GetAttackTarget( void ) const +{ + if ( m_attackTarget != NULL && m_attackTarget->IsAlive() ) + { + return m_attackTarget; + } + + return NULL; +} + + +//--------------------------------------------------------------------------------------------- +void CBossAlpha::Break( void ) +{ + CPVSFilter filter( GetAbsOrigin() ); + UserMessageBegin( filter, "BreakModel" ); + WRITE_SHORT( GetModelIndex() ); + WRITE_VEC3COORD( GetAbsOrigin() ); + WRITE_ANGLES( GetAbsAngles() ); + WRITE_SHORT( GetSkin() ); + MessageEnd(); +} + + +//--------------------------------------------------------------------------------------------- +void CBossAlpha::CollectPlayersStandingOnMe( CUtlVector< CTFPlayer * > *playerVector ) +{ + CUtlVector< CTFPlayer * > allPlayerVector; + CollectPlayers( &allPlayerVector, TEAM_ANY, COLLECT_ONLY_LIVING_PLAYERS ); + + for( int i=0; i<allPlayerVector.Count(); ++i ) + { + CTFPlayer *player = allPlayerVector[i]; + + if ( player->GetGroundEntity() == this ) + { + playerVector->AddToTail( player ); + } + } +} + + +//--------------------------------------------------------------------------------------------- +void CBossAlpha::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + // cache the trace info so we can precisely re-trace to find hitbox hits in OnTakeDamage_Alive() later + if ( ptr ) + { + m_lastTraceAttackTrace = *ptr; + } + + m_lastTraceAttackDir = vecDir; + + BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator ); +} + + +//--------------------------------------------------------------------------------------------- +// Intention interface +//--------------------------------------------------------------------------------------------- +CBossAlphaIntention::CBossAlphaIntention( CBossAlpha *me ) : IIntention( me ) +{ + m_behavior = new Behavior< CBossAlpha >( new CBossAlphaBehavior ); +} + +CBossAlphaIntention::~CBossAlphaIntention() +{ + delete m_behavior; +} + +void CBossAlphaIntention::Reset( void ) +{ + delete m_behavior; + m_behavior = new Behavior< CBossAlpha >( new CBossAlphaBehavior ); +} + +void CBossAlphaIntention::Update( void ) +{ + m_behavior->Update( static_cast< CBossAlpha * >( GetBot() ), GetUpdateInterval() ); +} + +QueryResultType CBossAlphaIntention::IsPositionAllowed( const INextBot *meBot, const Vector &pos ) const +{ + // is this a place we can be? + return ANSWER_YES; +} + + +//--------------------------------------------------------------------------------------------- +// Locomotion interface +//--------------------------------------------------------------------------------------------- +CBossAlphaLocomotion::CBossAlphaLocomotion( INextBot *bot ) : NextBotGroundLocomotion( bot ) +{ + CBossAlpha *me = (CBossAlpha *)GetBot()->GetEntity(); + + m_runSpeed = me->GetMoveSpeed(); +} + + +//--------------------------------------------------------------------------------------------- +float CBossAlphaLocomotion::GetRunSpeed( void ) const +{ + CBossAlpha *me = (CBossAlpha *)GetBot()->GetEntity(); + + return me->IsInCondition( CBossAlpha::CHARGING ) ? 1000.0f : m_runSpeed; +} + + +//--------------------------------------------------------------------------------------------- +// if delta Z is greater than this, we have to jump to get up +float CBossAlphaLocomotion::GetStepHeight( void ) const +{ + return 18.0f; +} + + +//--------------------------------------------------------------------------------------------- +// return maximum height of a jump +float CBossAlphaLocomotion::GetMaxJumpHeight( void ) const +{ + return 18.0f; +} + + +//--------------------------------------------------------------------------------------------- +// Vision interface +//--------------------------------------------------------------------------------------------- + +//--------------------------------------------------------------------------------------------- +// Return true to completely ignore this entity (may not be in sight when this is called) +bool CBossAlphaVision::IsIgnored( CBaseEntity *subject ) const +{ + if ( subject->IsPlayer() ) + { + CTFPlayer *enemy = static_cast< CTFPlayer * >( subject ); + + if ( enemy->m_Shared.InCond( TF_COND_BURNING ) || + enemy->m_Shared.InCond( TF_COND_URINE ) || + enemy->m_Shared.InCond( TF_COND_STEALTHED_BLINK ) || + enemy->m_Shared.InCond( TF_COND_BLEEDING ) ) + { + // always notice players with these conditions + return false; + } + + if ( enemy->m_Shared.IsStealthed() ) + { + if ( enemy->m_Shared.GetPercentInvisible() < 0.75f ) + { + // spy is partially cloaked, and therefore attracts our attention + return false; + } + + // invisible! + return true; + } + + if ( enemy->IsPlacingSapper() ) + { + return false; + } + + if ( enemy->m_Shared.InCond( TF_COND_DISGUISING ) ) + { + return false; + } + + if ( enemy->m_Shared.InCond( TF_COND_DISGUISED ) && enemy->m_Shared.GetDisguiseTeam() == GetBot()->GetEntity()->GetTeamNumber() ) + { + // spy is disguised as a member of my team + return true; + } + } + + return false; +} + +#endif // TF_RAID_MODE |