diff options
Diffstat (limited to 'game/server/tf/player_vs_environment')
48 files changed, 15943 insertions, 0 deletions
diff --git a/game/server/tf/player_vs_environment/archer_proxy.cpp b/game/server/tf/player_vs_environment/archer_proxy.cpp new file mode 100644 index 0000000..0125d12 --- /dev/null +++ b/game/server/tf/player_vs_environment/archer_proxy.cpp @@ -0,0 +1,188 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#include "cbase.h" + +#include "tf_player.h" +#include "tf_projectile_arrow.h" +#include "tf_weapon_grenade_pipebomb.h" +#include "archer_proxy.h" + +LINK_ENTITY_TO_CLASS( archer_proxy, CTFArcherProxy ); + + +ConVar tf_archer_proxy_fire_rate( "tf_archer_proxy_fire_rate", "1", FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------- +void CTFArcherProxy::Precache( void ) +{ + // don't need to precache, since player does this for us + BaseClass::Precache(); +} + + +//-------------------------------------------------------------------------------------- +void CTFArcherProxy::Spawn( void ) +{ + BaseClass::Spawn(); + + SetThink( &CTFArcherProxy::Update ); + SetNextThink( gpGlobals->curtime + RandomFloat( 0.1, 1.0f ) * tf_archer_proxy_fire_rate.GetFloat() ); + + m_state = HIDDEN; + m_timer.Invalidate(); + AddEffects( EF_NODRAW ); + m_homePos = GetAbsOrigin(); + + SetModel( "models/weapons/w_models/w_arrow.mdl" ); +} + + +//--------------------------------------------------------------------------------------------- +CTFPlayer *CTFArcherProxy::SelectTarget( void ) +{ + // 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 ); + + CTFPlayer *newVictim = NULL; + float victimRangeSq = FLT_MAX; + trace_t result; + + for( int i=0; i<playerVector.Count(); ++i ) + { + float rangeSq = ( playerVector[i]->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr(); + if ( rangeSq < victimRangeSq ) + { + UTIL_TraceLine( GetAbsOrigin(), playerVector[i]->EyePosition(), MASK_SOLID, this, COLLISION_GROUP_DEBRIS, &result ); + + if ( !result.DidHit() ) + { + newVictim = playerVector[i]; + victimRangeSq = rangeSq; + } + } + } + + return newVictim; +} + + +//-------------------------------------------------------------------------------------- +void CTFArcherProxy::Update( void ) +{ + SetNextThink( gpGlobals->curtime ); + + switch( m_state ) + { + case HIDDEN: + m_state = EMERGE; + RemoveEffects( EF_NODRAW ); + m_timer.Start( 1.0f ); + break; + + case EMERGE: + m_state = AIM_AND_FIRE; + m_timer.Start( 1.0f ); + break; + + case AIM_AND_FIRE: + { + CTFPlayer *target = SelectTarget(); + if ( target ) + { + Vector to = target->GetAbsOrigin() - GetAbsOrigin(); + QAngle angles; + VectorAngles( to, angles ); + + SetAbsAngles( angles ); + + if ( m_timer.IsElapsed() ) + { + ShootArrowAt( target ); + // ShootGrenadeAt( target ); + + m_state = HIDE; + m_timer.Start( 1.0f ); + } + } + break; + } + + case HIDE: + if ( m_timer.IsElapsed() ) + { + m_state = HIDDEN; + AddEffects( EF_NODRAW ); + m_timer.Start( 1.0f ); + } + break; + } + +} + + +//-------------------------------------------------------------------------------------- +void CTFArcherProxy::ShootArrowAt( CBaseEntity *target ) +{ + if ( !target ) + { + return; + } + + Vector to = target->EyePosition() - GetAbsOrigin(); + to.NormalizeInPlace(); + + QAngle angles; + VectorAngles( to, angles ); + + const float arrowSpeed = 2600.0f; + const float arrowGravity = 0.1f; + CTFProjectile_Arrow *arrow = CTFProjectile_Arrow::Create( GetAbsOrigin() + 20.0f * to, angles, arrowSpeed, arrowGravity, TF_PROJECTILE_ARROW, this, this ); + if ( arrow ) + { + arrow->SetLauncher( this ); + arrow->SetCritical( false ); + arrow->SetDamage( 100.0f ); + + EmitSound( "Weapon_CompoundBow.Single" ); + } +} + + +//-------------------------------------------------------------------------------------- +void CTFArcherProxy::ShootGrenadeAt( CBaseEntity *target ) +{ + if ( !target ) + { + return; + } + + Vector to = target->EyePosition() - GetAbsOrigin(); + to.NormalizeInPlace(); + + QAngle angles; + VectorAngles( to, angles ); + + + float launchSpeed = 1000.0f; + Vector velocity = ( to * launchSpeed ) + ( Vector( 0, 0, 1.0f ) * 200.0f ); + AngularImpulse angVelocity = AngularImpulse( 600, random->RandomInt( -1200, 1200 ), 0 ); + + CTFGrenadePipebombProjectile *grenade = static_cast< CTFGrenadePipebombProjectile * >( CBaseEntity::CreateNoSpawn( "tf_projectile_pipe", GetAbsOrigin(), angles, NULL ) ); + if ( grenade ) + { + // Set the pipebomb mode before calling spawn, so the model & associated vphysics get setup properly + grenade->SetPipebombMode(); + DispatchSpawn( grenade ); + + const int damage = 100; + const float radius = 100.0f; + + grenade->InitGrenade( velocity, angVelocity, NULL, damage, radius ); + grenade->m_flFullDamage = grenade->GetDamage(); + grenade->ApplyLocalAngularVelocityImpulse( angVelocity ); + grenade->SetCritical( false ); + grenade->SetLauncher( this ); + } +} diff --git a/game/server/tf/player_vs_environment/archer_proxy.h b/game/server/tf/player_vs_environment/archer_proxy.h new file mode 100644 index 0000000..f947394 --- /dev/null +++ b/game/server/tf/player_vs_environment/archer_proxy.h @@ -0,0 +1,36 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#ifndef ARCHER_PROXY_H +#define ARCHER_PROXY_H + +class CTFPlayer; + +class CTFArcherProxy : public CBaseAnimating +{ +public: + DECLARE_CLASS( CTFArcherProxy, CBaseAnimating ); + + virtual void Precache( void ); + virtual void Spawn( void ); + + void Update( void ); + + void ShootArrowAt( CBaseEntity *target ); + void ShootGrenadeAt( CBaseEntity *target ); + +protected: + CTFPlayer *SelectTarget( void ); + + enum BehaviorStateType + { + HIDDEN, + EMERGE, + AIM_AND_FIRE, + HIDE, + } + m_state; + + CountdownTimer m_timer; + Vector m_homePos; +}; + +#endif // ARCHER_PROXY_H diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_behavior.cpp b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_behavior.cpp new file mode 100644 index 0000000..6cfe287 --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_behavior.cpp @@ -0,0 +1,109 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_behavior.cpp +// Michael Booth, November 2010 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "CRagdollMagnet.h" +#include "tf_gamerules.h" +#include "tf_shareddefs.h" +#include "player_vs_environment/monster_resource.h" +#include "player_vs_environment/boss_alpha/boss_alpha.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_behavior.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_tactical_monitor.h" + + +//--------------------------------------------------------------------------------------------- +Action< CBossAlpha > *CBossAlphaBehavior::InitialContainedAction( CBossAlpha *me ) +{ + return new CBossAlphaTacticalMonitor; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaBehavior::Update( CBossAlpha *me, float interval ) +{ + if ( m_vocalTimer.IsElapsed() ) + { + m_vocalTimer.Start( RandomFloat( 3.0f, 5.0f ) ); + + if ( !me->IsBusy() ) + { + me->EmitSound( "RobotBoss.Vocalize" ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBossAlpha > CBossAlphaBehavior::OnKilled( CBossAlpha *me, const CTakeDamageInfo &info ) +{ + // relay the event to the map logic + me->m_outputOnKilled.FireOutput( me, me ); + + // Calculate death force + Vector forceVector = me->CalcDamageForceVector( info ); + + // See if there's a ragdoll magnet that should influence our force. + CRagdollMagnet *magnet = CRagdollMagnet::FindBestMagnet( me ); + if ( magnet ) + { + forceVector += magnet->GetForceVector( me ); + } + + if ( me->IsMiniBoss() ) + { + me->EmitSound( "Cart.Explode" ); + me->BecomeRagdoll( info, forceVector ); + + if ( g_pMonsterResource ) + { + g_pMonsterResource->HideBossHealthMeter(); + } + } + else + { + // full end-of-game boss + UTIL_Remove( me ); + + if ( TFGameRules()->IsBossBattleMode() ) + { + // check that ALL bosses are dead + bool isBossBattleWon = true; + + CBossAlpha *boss = NULL; + while( ( boss = (CBossAlpha *)gEntList.FindEntityByClassname( boss, "boss_alpha" ) ) != NULL ) + { + if ( !me->IsSelf( boss ) && boss->IsAlive() && !boss->IsMiniBoss() ) + { + isBossBattleWon = false; + } + } + + if ( isBossBattleWon ) + { + TFGameRules()->SetWinningTeam( TF_TEAM_BLUE, WINREASON_OPPONENTS_DEAD ); + + if ( g_pMonsterResource ) + { + g_pMonsterResource->HideBossHealthMeter(); + } + } + } + } + + return TryDone(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBossAlpha > CBossAlphaBehavior::OnContact( CBossAlpha *me, CBaseEntity *other, CGameTrace *result ) +{ + return TryContinue(); +} + +#endif // TF_RAID_MODE diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_behavior.h b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_behavior.h new file mode 100644 index 0000000..c637bf5 --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_behavior.h @@ -0,0 +1,30 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_behavior.h +// Michael Booth, November 2010 + +#ifndef BOSS_ALPHA_BEHAVIOR_H +#define BOSS_ALPHA_BEHAVIOR_H + +#ifdef TF_RAID_MODE + +//--------------------------------------------------------------------------------------------- +class CBossAlphaBehavior : public Action< CBossAlpha > +{ +public: + virtual Action< CBossAlpha > *InitialContainedAction( CBossAlpha *me ); + + virtual ActionResult< CBossAlpha > Update( CBossAlpha *me, float interval ); + + virtual EventDesiredResult< CBossAlpha > OnKilled( CBossAlpha *me, const CTakeDamageInfo &info ); + virtual EventDesiredResult< CBossAlpha > OnContact( CBossAlpha *me, CBaseEntity *other, CGameTrace *result = NULL ); + + virtual const char *GetName( void ) const { return "Behavior"; } // return name of this action + +private: + CountdownTimer m_vocalTimer; +}; + + +#endif // TF_RAID_MODE + +#endif // BOSS_ALPHA_BEHAVIOR_H diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_chase_victim.cpp b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_chase_victim.cpp new file mode 100644 index 0000000..77204b7 --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_chase_victim.cpp @@ -0,0 +1,170 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_chase_victim.cpp +// Michael Booth, November 2010 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "player_vs_environment/boss_alpha/boss_alpha.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_chase_victim.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_lost_victim.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_nuke_attack.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_launch_grenades.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_launch_rockets.h" + +extern ConVar tf_boss_alpha_grenade_launch_range; +extern ConVar tf_boss_alpha_chase_range; + + +//--------------------------------------------------------------------------------------------- +CBossAlphaChaseVictim::CBossAlphaChaseVictim( CBaseCombatCharacter *chaseTarget ) +{ + m_chaseTarget = chaseTarget; + m_lastKnownTargetSpot = chaseTarget->GetAbsOrigin(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaChaseVictim::OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ) +{ + if ( m_chaseTarget == NULL ) + { + return Done( "Target is NULL" ); + } + + m_lastKnownTargetSpot = m_chaseTarget->GetAbsOrigin(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaChaseVictim::Update( CBossAlpha *me, float interval ) +{ + if ( m_chaseTarget == NULL || !m_chaseTarget->IsAlive() ) + { + return ChangeTo( new CBossAlphaLostVictim, "No victim" ); + } + + if ( m_chaseTarget != me->GetAttackTarget() ) + { + return Done( "Changing targets" ); + } + + Vector moveGoal = m_chaseTarget->GetAbsOrigin(); + + if ( me->IsLineOfSightClear( m_chaseTarget ) ) + { + if ( !m_visibleTimer.HasStarted() ) + { + m_visibleTimer.Start(); + } + + if ( me->HasAbility( CBossAlpha::CAN_NUKE ) && me->GetNukeTimer()->IsElapsed() ) + { + return SuspendFor( new CBossAlphaNukeAttack, "Nuking!" ); + } + + m_lastKnownTargetSpot = m_chaseTarget->GetAbsOrigin(); + + if ( me->HasAbility( CBossAlpha::CAN_LAUNCH_STICKIES ) ) + { + if ( ( me->GetGrenadeTimer()->IsElapsed() && me->IsRangeLessThan( m_chaseTarget, tf_boss_alpha_grenade_launch_range.GetFloat() ) ) || + me->IsInCondition( CBossAlpha::ENRAGED ) ) + { + return SuspendFor( new CBossAlphaLaunchGrenades, "Target is close (or I am enraged) - grenades!" ); + } + } + + // chase into line of sight a bit so they can't immediately get behind cover again + if ( me->HasAbility( CBossAlpha::CAN_FIRE_ROCKETS ) ) + { + if ( m_visibleTimer.IsGreaterThen( 1.0f ) || + me->IsRangeLessThan( m_chaseTarget, tf_boss_alpha_chase_range.GetFloat() ) ) + { + return SuspendFor( new CBossAlphaLaunchRockets, "Fire!" ); + } + } + + if ( me->IsRangeLessThan( m_chaseTarget, 150.0f ) ) + { + // too close - stand still + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_STAND_MELEE ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_MELEE ); + } + + return Continue(); + } + } + else + { + m_visibleTimer.Invalidate(); + + // move to where we last saw our target + moveGoal = m_lastKnownTargetSpot; + + if ( me->IsRangeLessThan( m_lastKnownTargetSpot, 20.0f ) ) + { + // reached spot where we last saw our victim - give up + me->SetAttackTarget( NULL ); + + return ChangeTo( new CBossAlphaLostVictim, "I lost my chase victim" ); + } + } + + + // move into sight of target + if ( m_path.GetAge() > 1.0f ) + { + CBossAlphaPathCost cost( me ); + m_path.Compute( me, moveGoal, cost ); + } + + me->GetLocomotionInterface()->Run(); + m_path.Update( me ); + + // play running animation + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_RUN_MELEE ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBossAlpha > CBossAlphaChaseVictim::OnMoveToSuccess( CBossAlpha *me, const Path *path ) +{ + return TryDone( RESULT_CRITICAL, "Reached move goal" ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBossAlpha > CBossAlphaChaseVictim::OnMoveToFailure( CBossAlpha *me, const Path *path, MoveToFailureType reason ) +{ + return TryDone( RESULT_CRITICAL, "Path follow failed" ); +} + + +//--------------------------------------------------------------------------------------------- +void CBossAlphaChaseVictim::OnEnd( CBossAlpha *me, Action< CBossAlpha > *nextAction ) +{ +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBossAlpha > CBossAlphaChaseVictim::OnStuck( CBossAlpha *me ) +{ + // we're stuck - just warp to the our next path goal + if ( m_path.GetCurrentGoal() ) + { + me->SetAbsOrigin( m_path.GetCurrentGoal()->pos + Vector( 0, 0, 10.0f ) ); + } + + return TryContinue(); +} + +#endif // TF_RAID_MODE diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_chase_victim.h b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_chase_victim.h new file mode 100644 index 0000000..4cdf5ff --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_chase_victim.h @@ -0,0 +1,41 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_chase_victim.h +// Michael Booth, November 2010 + +#ifndef BOSS_ALPHA_CHASE_VICTIM_H +#define BOSS_ALPHA_CHASE_VICTIM_H + +#ifdef TF_RAID_MODE + +#include "nav_mesh/tf_path_follower.h" + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBossAlphaChaseVictim : public Action< CBossAlpha > +{ +public: + CBossAlphaChaseVictim( CBaseCombatCharacter *chaseTarget ); + + virtual ActionResult< CBossAlpha > OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ); + virtual ActionResult< CBossAlpha > Update( CBossAlpha *me, float interval ); + virtual void OnEnd( CBossAlpha *me, Action< CBossAlpha > *nextAction ); + + virtual EventDesiredResult< CBossAlpha > OnStuck( CBossAlpha *me ); + virtual EventDesiredResult< CBossAlpha > OnMoveToSuccess( CBossAlpha *me, const Path *path ); + virtual EventDesiredResult< CBossAlpha > OnMoveToFailure( CBossAlpha *me, const Path *path, MoveToFailureType reason ); + + virtual const char *GetName( void ) const { return "ChaseVictim"; } // return name of this action + +private: + CTFPathFollower m_path; + IntervalTimer m_visibleTimer; + CHandle< CBaseCombatCharacter > m_lastTarget; + + CHandle< CBaseCombatCharacter > m_chaseTarget; + Vector m_lastKnownTargetSpot; +}; + + +#endif // TF_RAID_MODE + +#endif // BOSS_ALPHA_CHASE_VICTIM_H diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_get_off_me.cpp b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_get_off_me.cpp new file mode 100644 index 0000000..b68ca8e --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_get_off_me.cpp @@ -0,0 +1,93 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_get_off_me.cpp +// Michael Booth, November 2010 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "tf_player.h" +#include "player_vs_environment/boss_alpha/boss_alpha.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_get_off_me.h" + +ConVar tf_boss_alpha_charge_pushaway_force( "tf_boss_alpha_charge_pushaway_force", "500"/*, FCVAR_CHEAT*/ ); + + +//--------------------------------------------------------------------------------------------- +void PushawayPlayer( CTFPlayer *victim, const Vector &pushOrigin, float pushForce ) +{ + if ( !victim ) + return; + + if ( victim->GetFlags() & FL_ONGROUND ) + { + // launching into the air + victim->SetAbsVelocity( vec3_origin ); + + const float stunTime = 0.5f; + victim->m_Shared.StunPlayer( stunTime, 1.0, TF_STUN_MOVEMENT ); + + victim->ApplyPunchImpulseX( RandomInt( 10, 15 ) ); + victim->SpeakConceptIfAllowed( MP_CONCEPT_DEFLECTED, "projectile:0,victim:1" ); + } + + victim->RemoveFlag( FL_ONGROUND ); + + Vector toVictim = victim->WorldSpaceCenter() - pushOrigin; + toVictim.z = 0.0f; + toVictim.NormalizeInPlace(); + toVictim.z = 1.0f; + + victim->ApplyAbsVelocityImpulse( pushForce * toVictim ); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaGetOffMe::OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ) +{ + me->AddGestureSequence( me->LookupSequence( "gesture_melee_help" ) ); + m_timer.Start( 0.5f ); + + me->AddCondition( CBossAlpha::BUSY ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaGetOffMe::Update( CBossAlpha *me, float interval ) +{ + if ( m_timer.IsElapsed() ) + { + // blast players off of my head + CUtlVector< CTFPlayer * > onMeVector; + me->CollectPlayersStandingOnMe( &onMeVector ); + + Vector headPos; + QAngle headAngles; + if ( me->GetAttachment( "head", headPos, headAngles ) ) + { + for( int i=0; i<onMeVector.Count(); ++i ) + { + // push 'em off + PushawayPlayer( onMeVector[i], headPos, tf_boss_alpha_charge_pushaway_force.GetFloat() ); + } + } + + me->EmitSound( "Weapon_FlameThrower.AirBurstAttack" ); + + return Done(); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBossAlphaGetOffMe::OnEnd( CBossAlpha *me, Action< CBossAlpha > *nextAction ) +{ + me->RemoveCondition( CBossAlpha::BUSY ); +} + + +#endif // TF_RAID_MODE diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_get_off_me.h b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_get_off_me.h new file mode 100644 index 0000000..5322103 --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_get_off_me.h @@ -0,0 +1,27 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_get_off_me.h +// Michael Booth, November 2010 + +#ifndef BOSS_ALPHA_GET_OFF_ME_H +#define BOSS_ALPHA_GET_OFF_ME_H + +#ifdef TF_RAID_MODE + + +//---------------------------------------------------------------------------- +class CBossAlphaGetOffMe : public Action< CBossAlpha > +{ +public: + virtual ActionResult< CBossAlpha > OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ); + virtual ActionResult< CBossAlpha > Update( CBossAlpha *me, float interval ); + virtual void OnEnd( CBossAlpha *me, Action< CBossAlpha > *nextAction ); + + virtual const char *GetName( void ) const { return "GetOffMe"; } // return name of this action + +private: + CountdownTimer m_timer; +}; + +#endif // TF_RAID_MODE + +#endif // BOSS_ALPHA_GET_OFF_ME_H diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_guard_spot.cpp b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_guard_spot.cpp new file mode 100644 index 0000000..7f80e68 --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_guard_spot.cpp @@ -0,0 +1,129 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_guard_spot.cpp +// Michael Booth, November 2010 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "tf_player.h" +#include "nav_mesh/tf_nav_area.h" +#include "tf_projectile_rocket.h" +#include "player_vs_environment/boss_alpha/boss_alpha.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_guard_spot.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_chase_victim.h" + + +//----------------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaGuardSpot::OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ) +{ + m_path.SetMinLookAheadDistance( 300.0f ); + + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_ITEM1 ); + me->SetHomePosition( me->GetAbsOrigin() ); + + m_lookAtSpot = vec3_origin; + + return Continue(); +} + + +//----------------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaGuardSpot::Update( CBossAlpha *me, float interval ) +{ + CBaseCombatCharacter *target = me->GetAttackTarget(); + if ( target ) + { + if ( me->IsLineOfSightClear( target ) || me->IsPrisonerOfMinion( target ) ) + { + return SuspendFor( new CBossAlphaChaseVictim( me->GetAttackTarget() ), "Get 'em!" ); + } + } + + CBaseCombatCharacter *visible = me->GetNearestVisibleEnemy(); + if ( visible ) + { + // look at visible victim out of range + me->GetLocomotionInterface()->FaceTowards( visible->WorldSpaceCenter() ); + } + + const float atHomeRange = 50.0f; + if ( me->IsRangeGreaterThan( me->GetHomePosition(), atHomeRange ) ) + { + if ( m_path.GetAge() > 3.0f ) + { + CBossAlphaPathCost cost( me ); + if ( m_path.Compute( me, me->GetHomePosition(), cost ) == false ) + { + // can't reach guard post - just jump there for now + me->Teleport( &me->GetHomePosition(), NULL, NULL ); + } + } + + m_path.Update( me ); + } + else + { + // on guard spot - look around + if ( m_lookTimer.IsElapsed() ) + { + m_lookTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFNavArea *myArea = (CTFNavArea *)me->GetLastKnownArea(); + if ( myArea ) + { + const CUtlVector< CTFNavArea * > &invasionAreaVector = myArea->GetEnemyInvasionAreaVector( TF_TEAM_RED ); + + if ( invasionAreaVector.Count() > 0 ) + { + // try to not look directly at walls + const float minGazeRange = 300.0f; + const int retryCount = 20.0f; + for( int r=0; r<retryCount; ++r ) + { + int which = RandomInt( 0, invasionAreaVector.Count()-1 ); + Vector gazeSpot = invasionAreaVector[ which ]->GetRandomPoint() + Vector( 0, 0, 0.75f * HumanHeight ); + + if ( me->IsRangeGreaterThan( gazeSpot, minGazeRange ) && me->GetVisionInterface()->IsLineOfSightClear( gazeSpot ) ) + { + // use maxLookInterval so these looks override body aiming from path following + m_lookAtSpot = gazeSpot; + break; + } + } + } + } + } + + me->GetLocomotionInterface()->FaceTowards( m_lookAtSpot ); + } + + if ( me->GetLocomotionInterface()->IsAttemptingToMove() ) + { + // play running animation + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_RUN_MELEE ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE ); + } + } + else + { + // standing still + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_STAND_ITEM1 ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_ITEM1 ); + } + } + + return Continue(); +} + + +//----------------------------------------------------------------------------------------------------- +EventDesiredResult< CBossAlpha > CBossAlphaGuardSpot::OnInjured( CBossAlpha *me, const CTakeDamageInfo &info ) +{ + return TryContinue(); +} + + +#endif // TF_RAID_MODE diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_guard_spot.h b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_guard_spot.h new file mode 100644 index 0000000..8fc8685 --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_guard_spot.h @@ -0,0 +1,28 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_guard_spot.h +// Michael Booth, November 2010 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "nav_mesh/tf_path_follower.h" + +//--------------------------------------------------------------------------------------------- +class CBossAlphaGuardSpot : public Action< CBossAlpha > +{ +public: + virtual ActionResult< CBossAlpha > OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ); + virtual ActionResult< CBossAlpha > Update( CBossAlpha *me, float interval ); + virtual EventDesiredResult< CBossAlpha > OnInjured( CBossAlpha *me, const CTakeDamageInfo &info ); + + virtual const char *GetName( void ) const { return "GuardSpot"; } // return name of this action + +private: + CTFPathFollower m_path; + CountdownTimer m_lookTimer; + Vector m_lookAtSpot; +}; + + +#endif // TF_RAID_MODE diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_launch_grenades.cpp b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_launch_grenades.cpp new file mode 100644 index 0000000..34008d3 --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_launch_grenades.cpp @@ -0,0 +1,204 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_launch_grenades.cpp +// Michael Booth, November 2010 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "player_vs_environment/boss_alpha/boss_alpha.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_launch_grenades.h" + +ConVar tf_boss_alpha_grenade_ring_min_horiz_vel( "tf_boss_alpha_grenade_ring_min_horiz_vel", "100"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_grenade_ring_max_horiz_vel( "tf_boss_alpha_grenade_ring_max_horiz_vel", "350"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_grenade_vert_vel( "tf_boss_alpha_grenade_vert_vel", "750"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_grenade_det_time( "tf_boss_alpha_grenade_det_time", "3"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_grenade_damage( "tf_boss_alpha_grenade_damage", "25"/*, FCVAR_CHEAT*/ ); + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaLaunchGrenades::OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ) +{ + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_SECONDARY ); + m_animLayer = me->AddLayeredSequence( me->LookupSequence( "gesture_melee_cheer" ), 0 ); + + m_timer.Start( 1.0f ); + m_detonateTimer.Invalidate(); + me->AddCondition( CBossAlpha::BUSY ); + me->GetGrenadeTimer()->Start( me->GetGrenadeInterval() ); + + me->EmitSound( "RobotBoss.LaunchGrenades" ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBossAlphaLaunchGrenades::LaunchGrenade( CBossAlpha *me, const Vector &launchVel, CTFWeaponInfo *weaponInfo ) +{ + CTFGrenadePipebombProjectile *pProjectile = CTFGrenadePipebombProjectile::Create( me->WorldSpaceCenter() + Vector( 0, 0, 100 ), vec3_angle, launchVel, + AngularImpulse( 600, random->RandomInt( -1200, 1200 ), 0 ), + me, *weaponInfo, TF_PROJECTILE_PIPEBOMB_REMOTE, 1 ); + if ( pProjectile ) + { + pProjectile->SetLauncher( me ); + pProjectile->SetDamage( tf_boss_alpha_grenade_damage.GetFloat() ); + + if ( me->IsInCondition( CBossAlpha::ENRAGED ) ) + { + pProjectile->SetCritical( true ); + } + + m_grenadeVector.AddToTail( pProjectile ); + } +} + + +//--------------------------------------------------------------------------------------------- +void CBossAlphaLaunchGrenades::LaunchGrenadeRings( CBossAlpha *me ) +{ + const char *weaponAlias = WeaponIdToAlias( TF_WEAPON_GRENADELAUNCHER ); + if ( !weaponAlias ) + return; + + WEAPON_FILE_INFO_HANDLE weaponInfoHandle = LookupWeaponInfoSlot( weaponAlias ); + if ( weaponInfoHandle == GetInvalidWeaponInfoHandle() ) + return; + + CTFWeaponInfo *weaponInfo = static_cast< CTFWeaponInfo * >( GetFileWeaponInfoFromHandle( weaponInfoHandle ) ); + + QAngle myAngles = me->EyeAngles(); + + // create rings of stickies + float deltaVel = tf_boss_alpha_grenade_ring_max_horiz_vel.GetFloat() - tf_boss_alpha_grenade_ring_min_horiz_vel.GetFloat(); + const int ringCount = 2; + for( int r=0; r<ringCount; ++r ) + { + float u = (float)r/(float)(ringCount-1); + + float horizVel = tf_boss_alpha_grenade_ring_min_horiz_vel.GetFloat() + u * deltaVel; + + float angleDelta = 10.0f + 20.0f * ( 1.0f - u ); + + for( float angle=0.0f; angle<360.0f; angle += angleDelta ) + { + Vector forward; + AngleVectors( myAngles, &forward ); + + Vector vecVelocity( horizVel * forward.x, horizVel * forward.y, tf_boss_alpha_grenade_vert_vel.GetFloat() ); + + LaunchGrenade( me, vecVelocity, weaponInfo ); + + myAngles.y += angleDelta; + } + } +} + + +ConVar tf_boss_alpha_grenade_spoke_angle( "tf_boss_alpha_grenade_spoke_angle", "45"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_grenade_spoke_count( "tf_boss_alpha_grenade_spoke_count", "15"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_grenade_spoke_min_horiz_vel( "tf_boss_alpha_grenade_spoke_min_horiz_vel", "100"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_grenade_spoke_max_horiz_vel( "tf_boss_alpha_grenade_spoke_max_horiz_vel", "750"/*, FCVAR_CHEAT*/ ); + + +//--------------------------------------------------------------------------------------------- +void CBossAlphaLaunchGrenades::LaunchGrenadeSpokes( CBossAlpha *me ) +{ + const char *weaponAlias = WeaponIdToAlias( TF_WEAPON_GRENADELAUNCHER ); + if ( !weaponAlias ) + return; + + WEAPON_FILE_INFO_HANDLE weaponInfoHandle = LookupWeaponInfoSlot( weaponAlias ); + if ( weaponInfoHandle == GetInvalidWeaponInfoHandle() ) + return; + + CTFWeaponInfo *weaponInfo = static_cast< CTFWeaponInfo * >( GetFileWeaponInfoFromHandle( weaponInfoHandle ) ); + + // create spokes of stickies + float deltaVel = tf_boss_alpha_grenade_spoke_max_horiz_vel.GetFloat() - tf_boss_alpha_grenade_spoke_min_horiz_vel.GetFloat(); + float angleDelta = tf_boss_alpha_grenade_spoke_angle.GetFloat(); + QAngle myAngles = me->EyeAngles(); + + for( float angle=0.0f; angle<360.0f; angle += angleDelta ) + { + Vector forward; + AngleVectors( myAngles, &forward ); + + int spokeCount = tf_boss_alpha_grenade_spoke_count.GetInt(); + + for( int i=0; i<spokeCount; ++i ) + { + float u = (float)i/(float)(spokeCount-1); + + float horizVel = tf_boss_alpha_grenade_spoke_min_horiz_vel.GetFloat() + u * deltaVel; + + Vector vecVelocity( horizVel * forward.x, horizVel * forward.y, tf_boss_alpha_grenade_vert_vel.GetFloat() ); + + LaunchGrenade( me, vecVelocity, weaponInfo ); + } + + myAngles.y += angleDelta; + } +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaLaunchGrenades::Update( CBossAlpha *me, float interval ) +{ + QAngle myAngles = me->EyeAngles(); + + if ( m_timer.HasStarted() && m_timer.IsElapsed() ) + { + m_timer.Invalidate(); + + if ( RandomInt( 0, 100 ) < 50 ) + { + LaunchGrenadeRings( me ); + } + else + { + LaunchGrenadeSpokes( me ); + } + + me->EmitSound( "Weapon_Grenade_Normal.Single" ); + + m_detonateTimer.Start( tf_boss_alpha_grenade_det_time.GetFloat() ); + } + + if ( m_detonateTimer.HasStarted() && m_detonateTimer.IsElapsed() ) + { + // detonate the stickies + for( int i=0; i<m_grenadeVector.Count(); ++i ) + { + if ( m_grenadeVector[i] ) + { + m_grenadeVector[i]->Detonate(); + } + } + + return Done(); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBossAlphaLaunchGrenades::OnEnd( CBossAlpha *me, Action< CBossAlpha > *nextAction ) +{ + // fizzle any outstanding stickies + for( int i=0; i<m_grenadeVector.Count(); ++i ) + { + if ( m_grenadeVector[i] ) + { + m_grenadeVector[i]->Fizzle(); + m_grenadeVector[i]->Detonate(); + } + } + + me->RemoveCondition( CBossAlpha::ENRAGED ); + me->RemoveCondition( CBossAlpha::BUSY ); + me->FastRemoveLayer( m_animLayer ); +} + +#endif // TF_RAID_MODE diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_launch_grenades.h b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_launch_grenades.h new file mode 100644 index 0000000..592bfd5 --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_launch_grenades.h @@ -0,0 +1,36 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_launch_grenades.h +// Michael Booth, November 2010 + +#ifndef BOSS_ALPHA_LAUNCH_GRENADES_H +#define BOSS_ALPHA_LAUNCH_GRENADES_H + +#ifdef TF_RAID_MODE + +#include "tf_weapon_grenade_pipebomb.h" + +class CBossAlphaLaunchGrenades : public Action< CBossAlpha > +{ +public: + virtual ActionResult< CBossAlpha > OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ); + virtual ActionResult< CBossAlpha > Update( CBossAlpha *me, float interval ); + virtual void OnEnd( CBossAlpha *me, Action< CBossAlpha > *nextAction ); + + // if anything interrupts this action, abort it + virtual ActionResult< CBossAlpha > OnSuspend( CBossAlpha *me, Action< CBossAlpha > *interruptingAction ) { return Done(); } + + virtual const char *GetName( void ) const { return "LaunchGrenades"; } // return name of this action + +private: + CountdownTimer m_timer; + CountdownTimer m_detonateTimer; + CUtlVector< CHandle< CTFGrenadePipebombProjectile > > m_grenadeVector; + void LaunchGrenade( CBossAlpha *me, const Vector &launchVel, CTFWeaponInfo *weaponInfo ); + void LaunchGrenadeRings( CBossAlpha *me ); + void LaunchGrenadeSpokes( CBossAlpha *me ); + int m_animLayer; +}; + +#endif // TF_RAID_MODE + +#endif // BOSS_ALPHA_LAUNCH_GRENADES_H
\ No newline at end of file diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_launch_rockets.cpp b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_launch_rockets.cpp new file mode 100644 index 0000000..a1558d8 --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_launch_rockets.cpp @@ -0,0 +1,119 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_launch_rockets.cpp +// Michael Booth, November 2010 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "tf_projectile_rocket.h" +#include "player_vs_environment/boss_alpha/boss_alpha.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_launch_rockets.h" + +ConVar tf_boss_alpha_dont_shoot( "tf_boss_alpha_dont_shoot", "0"/*, FCVAR_CHEAT*/ ); + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaLaunchRockets::OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ) +{ + // start animation + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_SECONDARY ); + + m_animLayer = me->AddLayeredSequence( me->LookupSequence( "taunt02" ), 0 ); + + m_timer.Start( 1.0f ); + + m_rocketsLeft = me->GetRocketLaunchCount(); + + me->AddCondition( CBossAlpha::BUSY ); + me->LockAttackTarget(); + + me->EmitSound( "RobotBoss.LaunchRockets" ); + + if ( me->GetAttackTarget() == NULL ) + { + return Done( "No target" ); + } + + m_target = me->GetAttackTarget(); + m_lastTargetPosition = m_target->WorldSpaceCenter(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaLaunchRockets::Update( CBossAlpha *me, float interval ) +{ + if ( m_target != NULL ) + { + m_lastTargetPosition = m_target->WorldSpaceCenter(); + } + + me->GetLocomotionInterface()->FaceTowards( m_lastTargetPosition ); + + if ( m_timer.IsElapsed() && m_launchTimer.IsElapsed() ) + { + if ( !m_rocketsLeft ) + { + return Done(); + } + + --m_rocketsLeft; + m_launchTimer.Start( me->GetRocketInterval() ); + + QAngle launchAngles = me->GetAbsAngles(); + + if ( m_target == NULL ) + { + Vector to = m_lastTargetPosition - me->WorldSpaceCenter(); + VectorAngles( to, launchAngles ); + } + else + { + float range = me->GetRangeTo( m_target->EyePosition() ); + + const float rocketSpeed = me->GetRocketAimError() * 1100.0f; // 2000.0f; // 1100.0f; nerfing accuracy + float flightTime = range / rocketSpeed; + + Vector aimSpot = m_target->EyePosition() + m_target->GetAbsVelocity() * flightTime; + + Vector to = aimSpot - me->WorldSpaceCenter(); + VectorAngles( to, launchAngles ); + } + + if ( !tf_boss_alpha_dont_shoot.GetBool() ) + { + CTFProjectile_Rocket *pRocket = CTFProjectile_Rocket::Create( me, me->WorldSpaceCenter(), launchAngles, me, me ); + if ( pRocket ) + { + if ( me->IsInCondition( CBossAlpha::ENRAGED ) ) + { + pRocket->SetCritical( true ); + pRocket->EmitSound( "Weapon_RPG.SingleCrit" ); + } + else + { + me->EmitSound( me->GetRocketSoundEffect() ); + } + + pRocket->SetDamage( me->GetRocketDamage() ); + } + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBossAlphaLaunchRockets::OnEnd( CBossAlpha *me, Action< CBossAlpha > *nextAction ) +{ + me->RemoveCondition( CBossAlpha::ENRAGED ); + me->RemoveCondition( CBossAlpha::BUSY ); + me->FastRemoveLayer( m_animLayer ); + me->UnlockAttackTarget(); +} + + +#endif // TF_RAID_MODE diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_launch_rockets.h b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_launch_rockets.h new file mode 100644 index 0000000..2f216b9 --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_launch_rockets.h @@ -0,0 +1,38 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_launch_rockets.h +// Michael Booth, November 2010 + +#ifndef BOSS_ALPHA_LAUNCH_ROCKETS_H +#define BOSS_ALPHA_LAUNCH_ROCKETS_H + +#ifdef TF_RAID_MODE + +//--------------------------------------------------------------------------------------------- +class CBossAlphaLaunchRockets : public Action< CBossAlpha > +{ +public: + virtual ActionResult< CBossAlpha > OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ); + virtual ActionResult< CBossAlpha > Update( CBossAlpha *me, float interval ); + virtual void OnEnd( CBossAlpha *me, Action< CBossAlpha > *nextAction ); + + // if anything interrupts this action, abort it + virtual ActionResult< CBossAlpha > OnSuspend( CBossAlpha *me, Action< CBossAlpha > *interruptingAction ) { return Done(); } + + virtual const char *GetName( void ) const { return "LaunchRockets"; } // return name of this action + +private: + CountdownTimer m_timer; + + CountdownTimer m_launchTimer; + int m_rocketsLeft; + + int m_animLayer; + + CHandle< CBaseCombatCharacter > m_target; + Vector m_lastTargetPosition; +}; + + +#endif // TF_RAID_MODE + +#endif // BOSS_ALPHA_LAUNCH_ROCKETS_H diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_lost_victim.cpp b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_lost_victim.cpp new file mode 100644 index 0000000..f935cfa --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_lost_victim.cpp @@ -0,0 +1,64 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_lost_victim.cpp +// Michael Booth, November 2010 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "player_vs_environment/boss_alpha/boss_alpha.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_lost_victim.h" + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaLostVictim::OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ) +{ + m_headTurn = 0.0f; + m_headYawPoseParameter = me->LookupPoseParameter( "body_yaw" ); + + m_timer.Start( RandomFloat( 3.0f, 5.0f ) ); + + me->EmitSound( "RobotBoss.Scanning" ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaLostVictim::Update( CBossAlpha *me, float interval ) +{ + if ( m_timer.IsElapsed() ) + { + return Done( "Giving up" ); + } + + CBaseCombatCharacter *target = me->GetAttackTarget(); + if ( target ) + { + if ( me->IsLineOfSightClear( target ) || me->IsPrisonerOfMinion( target ) ) + { + me->EmitSound( "RobotBoss.Acquire" ); + me->AddGesture( ACT_MP_GESTURE_FLINCH_CHEST ); + return Done( "Ah hah!" ); + } + } + + const float rate = M_PI / 3.0f; + m_headTurn += rate * interval; + + float s, c; + SinCos( m_headTurn, &s, &c ); + + me->SetPoseParameter( m_headYawPoseParameter, 40.0f * s ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBossAlphaLostVictim::OnEnd( CBossAlpha *me, Action< CBossAlpha > *nextAction ) +{ + me->SetPoseParameter( m_headYawPoseParameter, 0 ); +} + + +#endif // TF_RAID_MODE diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_lost_victim.h b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_lost_victim.h new file mode 100644 index 0000000..b5be9c1 --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_lost_victim.h @@ -0,0 +1,28 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_lost_victim.h +// Michael Booth, November 2010 + +#ifndef BOSS_ALPHA_LOST_VICTIM_H +#define BOSS_ALPHA_LOST_VICTIM_H + +#ifdef TF_RAID_MODE + +//--------------------------------------------------------------------------------------------- +class CBossAlphaLostVictim : public Action< CBossAlpha > +{ +public: + virtual ActionResult< CBossAlpha > OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ); + virtual ActionResult< CBossAlpha > Update( CBossAlpha *me, float interval ); + virtual void OnEnd( CBossAlpha *me, Action< CBossAlpha > *nextAction ); + + virtual const char *GetName( void ) const { return "LostVictim"; } // return name of this action + +private: + CountdownTimer m_timer; + float m_headTurn; + int m_headYawPoseParameter; +}; + +#endif // TF_RAID_MODE + +#endif // BOSS_ALPHA_LOST_VICTIM_H diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_nuke_attack.cpp b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_nuke_attack.cpp new file mode 100644 index 0000000..f1a3289 --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_nuke_attack.cpp @@ -0,0 +1,208 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_nuke_attack.cpp +// Michael Booth, November 2010 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "tf_player.h" +#include "tf_team.h" +#include "player_vs_environment/monster_resource.h" +#include "player_vs_environment/boss_alpha/boss_alpha.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_nuke_attack.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_stunned.h" + + +ConVar tf_boss_alpha_nuke_charge_time( "tf_boss_alpha_nuke_charge_time", "5" ); +ConVar tf_boss_alpha_nuke_interval( "tf_boss_alpha_nuke_interval", "20" ); +ConVar tf_boss_alpha_nuke_lethal_time( "tf_boss_alpha_nuke_lethal_time", "999999999" ); // 300 +ConVar tf_boss_alpha_nuke_damage( "tf_boss_alpha_nuke_damage", "75"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_nuke_max_remaining_health( "tf_boss_alpha_nuke_max_remaining_health", "60"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_nuke_afterburn_time( "tf_boss_alpha_nuke_afterburn_time", "5"/*, FCVAR_CHEAT*/ ); + +extern ConVar tf_boss_alpha_stunned_duration; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaNukeAttack::OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ) +{ + me->GetBodyInterface()->StartActivity( ACT_MP_JUMP_FLOAT_LOSERSTATE ); + me->StartNukeEffect(); + + me->EmitSound( "RobotBoss.ChargeUpNukeAttack" ); + // me->AddCondition( CBossAlpha::VULNERABLE_TO_STUN ); + + m_chargeUpTimer.Start( tf_boss_alpha_nuke_charge_time.GetFloat() ); + m_shakeTimer.Start( 0.25f ); + + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaNukeAttack::Update( CBossAlpha *me, float interval ) +{ + float stunRatio = me->GetStunDamage() / me->GetBecomeStunnedDamage(); + + if ( me->HasAbility( CBossAlpha::CAN_BE_STUNNED ) && stunRatio >= 1.0f ) + { + return ChangeTo( new CBossAlphaStunned( tf_boss_alpha_stunned_duration.GetFloat() ), "They got me" ); + } + + // update the client's HUD + if ( g_pMonsterResource ) + { + g_pMonsterResource->SetBossStunPercentage( 1.0f - stunRatio ); + } + + if ( m_shakeTimer.IsElapsed() ) + { + m_shakeTimer.Reset(); + UTIL_ScreenShake( me->GetAbsOrigin(), 15.0f, 5.0f, 1.0f, 3000.0f, SHAKE_START ); + } + + if ( m_chargeUpTimer.IsElapsed() ) + { + // BLAST! + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + me->EmitSound( "RobotBoss.NukeAttack" ); + + CUtlVector< CBaseCombatCharacter * > victimVector; + + int i; + + // players + for ( i=0; i<playerVector.Count(); ++i ) + { + CBasePlayer *player = playerVector[i]; + + if ( player && player->IsAlive() && player->GetTeamNumber() == TF_TEAM_BLUE ) + { + victimVector.AddToTail( player ); + } + } + + // objects + CTFTeam *team = GetGlobalTFTeam( TF_TEAM_BLUE ); + if ( team ) + { + for ( i=0; i<team->GetNumObjects(); ++i ) + { + CBaseObject *object = team->GetObject( i ); + if ( object ) + { + victimVector.AddToTail( object ); + } + } + } + +#ifdef SKIPME + team = GetGlobalTFTeam( TF_TEAM_RED ); + if ( team ) + { + for ( i=0; i<team->GetNumObjects(); ++i ) + { + CBaseObject *object = team->GetObject( i ); + if ( object ) + { + victimVector.AddToTail( object ); + } + } + } + + // non-player bots + CUtlVector< INextBot * > botVector; + TheNextBots().CollectAllBots( &botVector ); + for( i=0; i<botVector.Count(); ++i ) + { + CBaseCombatCharacter *bot = botVector[i]->GetEntity(); + + if ( !bot->IsPlayer() && bot->IsAlive() ) + { + victimVector.AddToTail( bot ); + } + } +#endif // SKIPME + + for( int i=0; i<victimVector.Count(); ++i ) + { + CBaseCombatCharacter *victim = victimVector[i]; + + if ( me->IsSelf( victim ) ) + continue; + + if ( me->IsLineOfSightClear( victim ) ) + { + Vector toVictim = victim->WorldSpaceCenter() - me->WorldSpaceCenter(); + toVictim.NormalizeInPlace(); + + float damage = tf_boss_alpha_nuke_damage.GetFloat(); + + if ( me->GetAge() > tf_boss_alpha_nuke_lethal_time.GetFloat() ) + { + // nuke is now lethal + damage = 999.9f; + } + else if ( tf_boss_alpha_nuke_max_remaining_health.GetFloat() >= 0.0f ) + { + // nuke slams everyone's health to this + if ( victim->GetHealth() > tf_boss_alpha_nuke_max_remaining_health.GetFloat() ) + { + damage = victim->GetHealth() - tf_boss_alpha_nuke_max_remaining_health.GetFloat(); + } + } + + CTakeDamageInfo info( me, me, damage, DMG_ENERGYBEAM, TF_DMG_CUSTOM_NONE ); + CalculateMeleeDamageForce( &info, toVictim, me->WorldSpaceCenter(), 1.0f ); + victim->TakeDamage( info ); + + if ( victim->IsPlayer() ) + { + CTFPlayer *playerVictim = ToTFPlayer( victim ); + + // catch them on fire (unless they are a Pyro) + if ( !playerVictim->IsPlayerClass( TF_CLASS_PYRO ) ) + { + playerVictim->m_Shared.Burn( me, tf_boss_alpha_nuke_afterburn_time.GetFloat() ); + } + + color32 colorHit = { 255, 255, 255, 255 }; + UTIL_ScreenFade( victim, colorHit, 1.0f, 0.1f, FFADE_IN ); + } + } + } + + return Done(); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBossAlphaNukeAttack::OnEnd( CBossAlpha *me, Action< CBossAlpha > *nextAction ) +{ + me->RemoveCondition( CBossAlpha::VULNERABLE_TO_STUN ); + me->StopNukeEffect(); + me->ClearStunDamage(); + me->GetNukeTimer()->Start( tf_boss_alpha_nuke_interval.GetFloat() ); + + if ( g_pMonsterResource ) + { + g_pMonsterResource->HideBossStunMeter(); + } +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBossAlpha > CBossAlphaNukeAttack::OnInjured( CBossAlpha *me, const CTakeDamageInfo &info ) +{ + return TryToSustain( RESULT_CRITICAL ); +} + +#endif // TF_RAID_MODE diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_nuke_attack.h b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_nuke_attack.h new file mode 100644 index 0000000..3566a59 --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_nuke_attack.h @@ -0,0 +1,31 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_nuke_attack.h +// Michael Booth, November 2010 + +#ifndef BOSS_ALPHA_NUKE_ATTACK_H +#define BOSS_ALPHA_NUKE_ATTACK_H + +#ifdef TF_RAID_MODE + +class CBossAlphaNukeAttack : public Action< CBossAlpha > +{ +public: + virtual ActionResult< CBossAlpha > OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ); + virtual ActionResult< CBossAlpha > Update( CBossAlpha *me, float interval ); + virtual void OnEnd( CBossAlpha *me, Action< CBossAlpha > *nextAction ); + + // if anything interrupts this action, abort it + virtual ActionResult< CBossAlpha > OnSuspend( CBossAlpha *me, Action< CBossAlpha > *interruptingAction ) { return Done(); } + + virtual EventDesiredResult< CBossAlpha > OnInjured( CBossAlpha *me, const CTakeDamageInfo &info ); + + virtual const char *GetName( void ) const { return "NukeAttack"; } // return name of this action + +private: + CountdownTimer m_shakeTimer; + CountdownTimer m_chargeUpTimer; +}; + +#endif // TF_RAID_MODE + +#endif // BOSS_ALPHA_NUKE_ATTACK_H diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_stunned.cpp b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_stunned.cpp new file mode 100644 index 0000000..19f340f --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_stunned.cpp @@ -0,0 +1,171 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_stunned.cpp +// Michael Booth, November 2010 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "tf_shareddefs.h" +#include "tf_ammo_pack.h" +#include "player_vs_environment/boss_alpha/boss_alpha.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_stunned.h" + + +extern ConVar tf_boss_alpha_min_nuke_after_stun_time; + + +//--------------------------------------------------------------------------------------------- +CBossAlphaStunned::CBossAlphaStunned( float duration, Action< CBossAlpha > *nextAction ) +{ + m_timer.Start( duration ); + m_nextAction = nextAction; +} + + +//--------------------------------------------------------------------------------------------- +ConVar tf_boss_alpha_stun_ammo_count( "tf_boss_alpha_stun_ammo_count", "3"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_stun_ammo_amount( "tf_boss_alpha_stun_ammo_amount", "100"/*, FCVAR_CHEAT*/ ); +ConVar tf_boss_alpha_stun_ammo_velocity( "tf_boss_alpha_stun_ammo_velocity", "100"/*, FCVAR_CHEAT*/ ); + +void TossAmmoPack( CBossAlpha *me ) +{ + int iPrimary = tf_boss_alpha_stun_ammo_amount.GetInt(); + int iSecondary = tf_boss_alpha_stun_ammo_amount.GetInt(); + int iMetal = tf_boss_alpha_stun_ammo_amount.GetInt(); + + // Create the ammo pack. + CTFAmmoPack *pAmmoPack = CTFAmmoPack::Create( me->GetAbsOrigin(), me->GetAbsAngles(), NULL, "models/items/ammopack_medium.mdl" ); + if ( pAmmoPack ) + { +/* + Vector vel; + + vel.x = RandomFloat( -1.0f, 1.0f ) * tf_boss_alpha_stun_ammo_velocity.GetFloat(); + vel.y = RandomFloat( -1.0f, 1.0f ) * tf_boss_alpha_stun_ammo_velocity.GetFloat(); + vel.z = tf_boss_alpha_stun_ammo_velocity.GetFloat(); + + pAmmoPack->SetInitialVelocity( vel ); +*/ + pAmmoPack->m_nSkin = 0; + + // Give the ammo pack some health, so that trains can destroy it. + pAmmoPack->SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + pAmmoPack->m_takedamage = DAMAGE_YES; + pAmmoPack->SetHealth( 900 ); + + pAmmoPack->SetBodygroup( 1, 1 ); + + pAmmoPack->ApplyLocalAngularVelocityImpulse( AngularImpulse( 600, random->RandomInt( -1200, 1200 ), 0 ) ); + + DispatchSpawn( pAmmoPack ); + + // Fill up the ammo pack. + pAmmoPack->GiveAmmo( iPrimary, TF_AMMO_PRIMARY ); + pAmmoPack->GiveAmmo( iSecondary, TF_AMMO_SECONDARY ); + pAmmoPack->GiveAmmo( iMetal, TF_AMMO_METAL ); + } +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaStunned::OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ) +{ + // start animation + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_MELEE ); + m_layerUsed = me->AddLayeredSequence( me->LookupSequence( "PRIMARY_Stun_begin" ), 0 ); + m_state = BECOMING_STUNNED; + + m_timer.Reset(); + + me->AddCondition( CBossAlpha::STUNNED ); + me->EmitSound( "RobotBoss.StunStart" ); + + // throw out some ammo + for( int i=0; i<tf_boss_alpha_stun_ammo_count.GetInt(); ++i ) + { + TossAmmoPack( me ); + } + + // relay the event to the map logic + me->m_outputOnStunned.FireOutput( me, me ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaStunned::Update( CBossAlpha *me, float interval ) +{ + switch( m_state ) + { + case BECOMING_STUNNED: + if ( me->IsSequenceFinished() ) + { + me->FastRemoveLayer( m_layerUsed ); + + m_state = STUNNED; + m_layerUsed = me->AddLayeredSequence( me->LookupSequence( "PRIMARY_stun_middle" ), 0 ); + me->SetLayerLooping( m_layerUsed, true ); + me->EmitSound( "RobotBoss.Stunned" ); + } + break; + + case STUNNED: + if ( m_timer.IsElapsed() ) + { + me->FastRemoveLayer( m_layerUsed ); + + m_state = RECOVERING; + m_layerUsed = me->AddLayeredSequence( me->LookupSequence( "PRIMARY_stun_end" ), 0 ); + me->StopSound( "RobotBoss.Stunned" ); + me->EmitSound( "RobotBoss.StunRecover" ); + } + break; + + case RECOVERING: + if ( me->IsSequenceFinished() ) + { + me->FastRemoveLayer( m_layerUsed ); + + if ( m_nextAction ) + { + return ChangeTo( m_nextAction, "Stun finished" ); + } + + return Done( "Stun finished" ); + } + break; + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBossAlpha > CBossAlphaStunned::OnInjured( CBossAlpha *me, const CTakeDamageInfo &info ) +{ + return TryToSustain( RESULT_CRITICAL ); +} + + +//--------------------------------------------------------------------------------------------- +void CBossAlphaStunned::OnEnd( CBossAlpha *me, Action< CBossAlpha > *nextAction ) +{ + me->RemoveCondition( CBossAlpha::STUNNED ); + + if ( me->HasAbility( CBossAlpha::CAN_ENRAGE ) ) + { + // being stunned makes the boss ANGRY! + me->AddCondition( CBossAlpha::ENRAGED ); + } + + // make sure the boss attacks at least once before he starts a nuke + if ( me->GetNukeTimer()->GetRemainingTime() < tf_boss_alpha_min_nuke_after_stun_time.GetFloat() ) + { + me->GetNukeTimer()->Start( tf_boss_alpha_min_nuke_after_stun_time.GetFloat() ); + } +} + + +#endif // TF_RAID_MODE diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_stunned.h b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_stunned.h new file mode 100644 index 0000000..d73ba17 --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_stunned.h @@ -0,0 +1,39 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_stunned.h +// Michael Booth, November 2010 + +#ifndef BOSS_ALPHA_STUNNED_H +#define BOSS_ALPHA_STUNNED_H + +#ifdef TF_RAID_MODE + +class CBossAlphaStunned : public Action< CBossAlpha > +{ +public: + CBossAlphaStunned( float duration, Action< CBossAlpha > *nextAction = NULL ); + + virtual ActionResult< CBossAlpha > OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ); + virtual ActionResult< CBossAlpha > Update( CBossAlpha *me, float interval ); + virtual void OnEnd( CBossAlpha *me, Action< CBossAlpha > *nextAction ); + + virtual EventDesiredResult< CBossAlpha > OnInjured( CBossAlpha *me, const CTakeDamageInfo &info ); + + virtual const char *GetName( void ) const { return "Stunned"; } // return name of this action + +private: + CountdownTimer m_timer; + enum StunStateType + { + BECOMING_STUNNED, + STUNNED, + RECOVERING + } + m_state; + int m_layerUsed; + + Action< CBossAlpha > *m_nextAction; +}; + +#endif // TF_RAID_MODE + +#endif // BOSS_ALPHA_STUNNED_H diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_tactical_monitor.cpp b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_tactical_monitor.cpp new file mode 100644 index 0000000..eca4107 --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_tactical_monitor.cpp @@ -0,0 +1,81 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_tactical_monitor.cpp +// Michael Booth, November 2010 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "tf_gamerules.h" +#include "player_vs_environment/boss_alpha/boss_alpha.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_tactical_monitor.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_get_off_me.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_wait_for_players.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_stunned.h" + + +ConVar tf_boss_alpha_get_off_me_duration( "tf_boss_alpha_get_off_me_duration", "3"/*, FCVAR_CHEAT */ ); +ConVar tf_boss_alpha_stunned_duration( "tf_boss_alpha_stunned_duration", "10" ); + + +//--------------------------------------------------------------------------------------------- +Action< CBossAlpha > *CBossAlphaTacticalMonitor::InitialContainedAction( CBossAlpha *me ) +{ + if ( TFGameRules()->IsBossBattleMode() ) + { + return new CBossAlphaWaitForPlayers; + } + + return NULL; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaTacticalMonitor::OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ) +{ + m_getOffMeTimer.Invalidate(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaTacticalMonitor::Update( CBossAlpha *me, float interval ) +{ + if ( me->IsInCondition( CBossAlpha::STUNNED ) ) + { + return SuspendFor( new CBossAlphaStunned( tf_boss_alpha_stunned_duration.GetFloat() ), "Ouch!" ); + } + + if ( !m_getOffMeTimer.HasStarted() ) + { + CUtlVector< CTFPlayer * > onMeVector; + me->CollectPlayersStandingOnMe( &onMeVector ); + + if ( onMeVector.Count() ) + { + // someone is standing on me - push them off soon + m_getOffMeTimer.Start( tf_boss_alpha_get_off_me_duration.GetFloat() ); + } + } + else if ( m_getOffMeTimer.IsElapsed() ) + { + if ( !me->IsBusy() ) + { + m_getOffMeTimer.Invalidate(); + + // if someone is still on me, push them off + CUtlVector< CTFPlayer * > onMeVector; + me->CollectPlayersStandingOnMe( &onMeVector ); + if ( onMeVector.Count() ) + { + return SuspendFor( new CBossAlphaGetOffMe, "Get offa me!" ); + } + } + } + + return Continue(); +} + + +#endif // TF_RAID_MODE diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_tactical_monitor.h b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_tactical_monitor.h new file mode 100644 index 0000000..e612b7c --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_tactical_monitor.h @@ -0,0 +1,32 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_tactical_monitor.h +// Michael Booth, November 2010 + +#ifndef BOSS_ALPHA_TACTICAL_MONITOR_H +#define BOSS_ALPHA_TACTICAL_MONITOR_H + +#ifdef TF_RAID_MODE + +class CBossAlpha; + + +//--------------------------------------------------------------------------------------------- +class CBossAlphaTacticalMonitor : public Action< CBossAlpha > +{ +public: + virtual Action< CBossAlpha > *InitialContainedAction( CBossAlpha *me ); + + virtual ActionResult< CBossAlpha > OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ); + virtual ActionResult< CBossAlpha > Update( CBossAlpha *me, float interval ); + + virtual const char *GetName( void ) const { return "TacticalMonitor"; } // return name of this action + +private: + CountdownTimer m_backOffCooldownTimer; + CountdownTimer m_getOffMeTimer; +}; + + +#endif // TF_RAID_MODE + +#endif // BOSS_ALPHA_TACTICAL_MONITOR_H diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_wait_for_players.cpp b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_wait_for_players.cpp new file mode 100644 index 0000000..cec965f --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_wait_for_players.cpp @@ -0,0 +1,79 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_wait_for_players.cpp +// Michael Booth, November 2010 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "player_vs_environment/boss_alpha/boss_alpha.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_wait_for_players.h" +#include "player_vs_environment/boss_alpha/behavior/boss_alpha_guard_spot.h" + + +extern ConVar tf_boss_alpha_nuke_interval; + +ConVar tf_boss_alpha_sleep( "tf_boss_alpha_sleep", "0"/*, FCVAR_CHEAT */ ); + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaWaitForPlayers::OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ) +{ + me->AddCondition( CBossAlpha::BUSY ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBossAlpha > CBossAlphaWaitForPlayers::Update( CBossAlpha *me, float interval ) +{ + if ( tf_boss_alpha_sleep.GetBool() ) + { + return Continue(); + } + + CBaseCombatCharacter *target = me->GetAttackTarget(); + if ( target ) + { + return ChangeTo( new CBossAlphaGuardSpot, "I see you..." ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBossAlphaWaitForPlayers::OnEnd( CBossAlpha *me, Action< CBossAlpha > *nextAction ) +{ + me->RemoveCondition( CBossAlpha::BUSY ); + + me->GetNukeTimer()->Start( tf_boss_alpha_nuke_interval.GetFloat() ); + me->GetGrenadeTimer()->Reset(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBossAlpha > CBossAlphaWaitForPlayers::OnInjured( CBossAlpha *me, const CTakeDamageInfo &info ) +{ + if ( tf_boss_alpha_sleep.GetBool() ) + { + return TryContinue(); + } + + return TryChangeTo( new CBossAlphaGuardSpot, RESULT_CRITICAL, "Ouch!" ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBossAlpha > CBossAlphaWaitForPlayers::OnContact( CBossAlpha *me, CBaseEntity *other, CGameTrace *result ) +{ + if ( other && other->IsPlayer() && !tf_boss_alpha_sleep.GetBool() ) + { + return TryChangeTo( new CBossAlphaGuardSpot, RESULT_CRITICAL, "Don't touch me" ); + } + + return TryContinue(); +} + +#endif // TF_RAID_MODE diff --git a/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_wait_for_players.h b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_wait_for_players.h new file mode 100644 index 0000000..a24011d --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_wait_for_players.h @@ -0,0 +1,25 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha_wait_for_players.h +// Michael Booth, November 2010 + +#ifndef BOSS_ALPHA_WAIT_FOR_PLAYER_H +#define BOSS_ALPHA_WAIT_FOR_PLAYER_H + +#ifdef TF_RAID_MODE + +class CBossAlphaWaitForPlayers : public Action< CBossAlpha > +{ +public: + virtual ActionResult< CBossAlpha > OnStart( CBossAlpha *me, Action< CBossAlpha > *priorAction ); + virtual ActionResult< CBossAlpha > Update( CBossAlpha *me, float interval ); + virtual void OnEnd( CBossAlpha *me, Action< CBossAlpha > *nextAction ); + + virtual EventDesiredResult< CBossAlpha > OnInjured( CBossAlpha *me, const CTakeDamageInfo &info ); + virtual EventDesiredResult< CBossAlpha > OnContact( CBossAlpha *me, CBaseEntity *other, CGameTrace *result = NULL ); + + virtual const char *GetName( void ) const { return "WaitForPlayers"; } // return name of this action +}; + +#endif // TF_RAID_MODE + +#endif // BOSS_ALPHA_WAIT_FOR_PLAYER_H 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 diff --git a/game/server/tf/player_vs_environment/boss_alpha/boss_alpha.h b/game/server/tf/player_vs_environment/boss_alpha/boss_alpha.h new file mode 100644 index 0000000..e4e7859 --- /dev/null +++ b/game/server/tf/player_vs_environment/boss_alpha/boss_alpha.h @@ -0,0 +1,508 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// boss_alpha.h +// Our first "real" TF Boss +// Michael Booth, November 2010 + +#ifndef BOSS_ALPHA_H +#define BOSS_ALPHA_H + +#ifdef TF_RAID_MODE + +#include "NextBot.h" +#include "NextBotBehavior.h" +#include "NextBotGroundLocomotion.h" +#include "Path/NextBotPathFollow.h" +#include "bot_npc/bot_npc_body.h" +#include "bot/map_entities/tf_spawner_boss.h" + +class CTFPlayer; +class CBossAlpha; + + +//---------------------------------------------------------------------------- +class CBossAlphaLocomotion : public NextBotGroundLocomotion +{ +public: + CBossAlphaLocomotion( INextBot *bot ); + virtual ~CBossAlphaLocomotion() { } + + virtual float GetRunSpeed( void ) const; // get maximum running speed + virtual float GetStepHeight( void ) const; // if delta Z is greater than this, we have to jump to get up + virtual float GetMaxJumpHeight( void ) const; // return maximum height of a jump + + virtual float GetMaxAcceleration( void ) const + { + return 2500.0f; + } + + virtual float GetMaxYawRate( void ) const // return max rate of yaw rotation + { + return 50.0f; + } + +private: + float m_runSpeed; +}; + + +//---------------------------------------------------------------------------- +class CBossAlphaIntention : public IIntention +{ +public: + CBossAlphaIntention( CBossAlpha *me ); + virtual ~CBossAlphaIntention(); + + virtual void Reset( void ); + virtual void Update( void ); + + virtual QueryResultType IsPositionAllowed( const INextBot *me, const Vector &pos ) const; // is the a place we can be? + + virtual INextBotEventResponder *FirstContainedResponder( void ) const { return m_behavior; } + virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const { return NULL; } + +private: + Behavior< CBossAlpha > *m_behavior; +}; + + +//---------------------------------------------------------------------------- +class CBossAlphaVision : public IVision +{ +public: + CBossAlphaVision( INextBot *bot ) : IVision( bot ) + { + } + + virtual ~CBossAlphaVision() { } + + virtual bool IsIgnored( CBaseEntity *subject ) const; // return true to completely ignore this entity (may not be in sight when this is called) +}; + + +//---------------------------------------------------------------------------- +//---------------------------------------------------------------------------- +class CBossAlpha : public NextBotCombatCharacter +{ +public: + DECLARE_CLASS( CBossAlpha, NextBotCombatCharacter ); + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CBossAlpha(); + virtual ~CBossAlpha(); + + virtual void Precache(); + virtual void Spawn( void ); + + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + + // INextBot + virtual CBossAlphaIntention *GetIntentionInterface( void ) const { return m_intention; } + virtual CBossAlphaLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; } + virtual CBotNPCBody *GetBodyInterface( void ) const { return m_body; } + virtual CBossAlphaVision *GetVisionInterface( void ) const { return m_vision; } + + virtual bool IsRemovedOnReset( void ) const { return false; } // remove this bot when the NextBot manager calls Reset + + virtual void Update( void ); + +// virtual bool IsPotentiallyChaseable( CTFPlayer *victim ); + + void Break( void ); // bust into gibs + + struct AttackerInfo + { + CHandle< CBaseCombatCharacter > m_attacker; + float m_timestamp; + float m_damage; + bool m_wasCritical; + }; + const CUtlVector< AttackerInfo > &GetAttackerVector( void ) const; + void RememberAttacker( CBaseCombatCharacter *attacker, float damage, bool wasCritical ); + + struct ThreatInfo + { + CHandle< CBaseCombatCharacter > m_who; + float m_threat; + }; + + const ThreatInfo *GetMaxThreat( void ) const; + const ThreatInfo *GetThreat( CBaseCombatCharacter *who ) const; + + void SwingAxe( void ); + void UpdateAxeSwing( void ); + bool IsSwingingAxe( void ) const; + + + //---------------------------------- + enum Ability + { + CAN_BE_STUNNED = 0x01, + CAN_NUKE = 0x02, + CAN_ENRAGE = 0x04, + CAN_FIRE_ROCKETS = 0x08, + CAN_LAUNCH_STICKIES = 0x10, + CAN_LAUNCH_MINIONS = 0x20, + }; + virtual bool HasAbility( Ability ability ) const; + + virtual bool IsMiniBoss( void ) const { return false; } + + virtual float GetMoveSpeed( void ) const { return 300.0f; } + + virtual int GetRocketLaunchCount( void ) const { return 5; } + virtual float GetRocketDamage( void ) const { return 25.0f; } + virtual float GetRocketAimError( void ) const { return 1.81f; } + virtual float GetRocketInterval( void ) const { return 0.3f; } + virtual const char *GetRocketSoundEffect( void ) const { return "Weapon_RPG.Single"; } + + virtual float GetGrenadeInterval( void ) const { return 10.0f; } + + virtual float GetBecomeStunnedDamage( void ) const { return 500.0f; } + + + //---------------------------------- + enum Condition + { + SHIELDED = 0x01, + CHARGING = 0x02, + STUNNED = 0x04, + INVULNERABLE = 0x08, + VULNERABLE_TO_STUN = 0x10, + BUSY = 0x20, + ENRAGED = 0x40, + }; + + bool IsBusy( void ) const; // returns true if we're in a condition that means we can't start another action + + void AddCondition( Condition c ); + void RemoveCondition( Condition c ); + bool IsInCondition( Condition c ) const; + + bool IsAttackTarget( CBaseCombatCharacter *target ) const; + bool HasAttackTarget( void ) const; + void SetAttackTarget( CBaseCombatCharacter *target, float duration = 0.0f ); + CBaseCombatCharacter *GetAttackTarget( void ) const; + void LockAttackTarget( void ); // don't allow target to change until it is unlocked or the target is destroyed + void UnlockAttackTarget( void ); + + CBaseCombatCharacter *GetNearestVisibleEnemy( void ) const; + + void SetHomePosition( const Vector &pos ); + const Vector &GetHomePosition( void ) const; + + CBaseAnimating *GetWeapon( void ) const; + CBaseAnimating *GetShield( void ) const; + + CountdownTimer *GetNukeTimer( void ); + CountdownTimer *GetGrenadeTimer( void ); + + float GetReceivedDamagePerSecond( void ) const; + float GetReceivedDamagePerSecondDelta( void ) const; + + void ClearStunDamage( void ); + void AccumulateStunDamage( float damage ); + float GetStunDamage( void ) const; + + CTFPlayer *GetClosestMinionPrisoner( void ); + bool IsPrisonerOfMinion( CBaseCombatCharacter *victim ); + + void StartNukeEffect( void ); + void StopNukeEffect( void ); + + float GetAge( void ) const; // how long have we been alive + + void CollectPlayersStandingOnMe( CUtlVector< CTFPlayer * > *playerVector ); + + // Entity I/O + COutputEvent m_outputOnStunned; + + COutputEvent m_outputOnHealthBelow90Percent; + COutputEvent m_outputOnHealthBelow80Percent; + COutputEvent m_outputOnHealthBelow70Percent; + COutputEvent m_outputOnHealthBelow60Percent; + COutputEvent m_outputOnHealthBelow50Percent; + COutputEvent m_outputOnHealthBelow40Percent; + COutputEvent m_outputOnHealthBelow30Percent; + COutputEvent m_outputOnHealthBelow20Percent; + COutputEvent m_outputOnHealthBelow10Percent; + + COutputEvent m_outputOnKilled; + +protected: + virtual void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + +private: + CBossAlphaIntention *m_intention; + CBossAlphaLocomotion *m_locomotor; + CBotNPCBody *m_body; + CBossAlphaVision *m_vision; + + CBaseAnimating *m_axe; + CBaseAnimating *m_shield; + + CountdownTimer m_axeSwingTimer; + CountdownTimer m_attackTimer; + CountdownTimer m_nukeTimer; + CountdownTimer m_grenadeTimer; + CountdownTimer m_ouchTimer; + CountdownTimer m_hateTauntTimer; + + CNetworkVar( bool, m_isNuking ); + + CHandle< CBaseCombatCharacter > m_nearestVisibleEnemy; + void UpdateNearestVisibleEnemy( void ); + CountdownTimer m_nearestVisibleEnemyTimer; + + CUtlVector< AttackerInfo > m_attackerVector; // list of everyone who injured me, and when + CUtlVector< ThreatInfo > m_threatVector; // list of attackers and their current damage/second on me + + float m_currentDamagePerSecond; + float m_lastDamagePerSecond; + void UpdateDamagePerSecond( void ); + + CHandle< CBaseCombatCharacter > m_attackTarget; + CountdownTimer m_attackTargetTimer; + bool m_isAttackTargetLocked; + void UpdateAttackTarget( void ); + + int m_damagePoseParameter; + + bool m_isShielded; + Vector m_homePos; + + bool IsIgnored( CTFPlayer *player ) const; + + unsigned int m_conditionFlags; + + float m_stunDamage; + float m_lastHealthPercentage; + + IntervalTimer m_ageTimer; + + void UpdateSkillShots( void ); + + bool CheckSkillShots( const CTakeDamageInfo &info ); + CountdownTimer m_headStunTimer; + CountdownTimer m_consecutiveRocketTimer; + int m_consecutiveRockets; + + CountdownTimer m_ricochetSoundTimer; + + void ResetSkillShots( void ); + void OnSkillShotComboStarted( void ); + void OnSkillShot( void ); + + CountdownTimer m_skillShotComboTimer; + int m_skillShotCount; + + bool m_isPrecisionShotDone; + CountdownTimer m_precisionSkillShotTimer; + bool m_isPrecisionShotHit[3]; + + bool m_isDamageSpongeSkillShotDone; + float m_damageSpongeSkillShotAmount; + + bool m_isHardHitSkillShotDone; + + trace_t m_lastTraceAttackTrace; + Vector m_lastTraceAttackDir; +}; + + +inline bool CBossAlpha::HasAbility( Ability ability ) const +{ + const int myAbilities = CAN_BE_STUNNED | CAN_NUKE | CAN_ENRAGE | CAN_FIRE_ROCKETS | CAN_LAUNCH_STICKIES | CAN_LAUNCH_MINIONS; + + return myAbilities & ability ? true : false; +} + +inline bool CBossAlpha::IsAttackTarget( CBaseCombatCharacter *target ) const +{ + if ( HasAttackTarget() ) + { + return ( m_attackTarget == target ) ? true : false; + } + return false; +} + +inline bool CBossAlpha::HasAttackTarget( void ) const +{ + return ( m_attackTarget == NULL || !m_attackTarget->IsAlive() ) ? false : true; +} + +inline void CBossAlpha::LockAttackTarget( void ) +{ + m_isAttackTargetLocked = HasAttackTarget(); +} + +inline void CBossAlpha::UnlockAttackTarget( void ) +{ + m_isAttackTargetLocked = false; +} + +inline float CBossAlpha::GetAge( void ) const +{ + return m_ageTimer.GetElapsedTime(); +} + +inline void CBossAlpha::StartNukeEffect( void ) +{ + m_isNuking = true; +} + +inline void CBossAlpha::StopNukeEffect( void ) +{ + m_isNuking = false; +} + +inline void CBossAlpha::ClearStunDamage( void ) +{ + m_stunDamage = 0.0f; +} + +inline void CBossAlpha::AccumulateStunDamage( float damage ) +{ + m_stunDamage += damage; +} + +inline float CBossAlpha::GetStunDamage( void ) const +{ + return m_stunDamage; +} + +inline float CBossAlpha::GetReceivedDamagePerSecond( void ) const +{ + return m_currentDamagePerSecond; +} + +inline float CBossAlpha::GetReceivedDamagePerSecondDelta( void ) const +{ + return m_currentDamagePerSecond - m_lastDamagePerSecond; +} + +inline CountdownTimer *CBossAlpha::GetNukeTimer( void ) +{ + return &m_nukeTimer; +} + +inline CountdownTimer *CBossAlpha::GetGrenadeTimer( void ) +{ + return &m_grenadeTimer; +} + +inline CBaseAnimating *CBossAlpha::GetWeapon( void ) const +{ + return m_axe; +} + +inline CBaseAnimating *CBossAlpha::GetShield( void ) const +{ + return m_shield; +} + +inline void CBossAlpha::SetHomePosition( const Vector &pos ) +{ + m_homePos = pos; +} + +inline const Vector &CBossAlpha::GetHomePosition( void ) const +{ + return m_homePos; +} + +inline CBaseCombatCharacter *CBossAlpha::GetNearestVisibleEnemy( void ) const +{ + return m_nearestVisibleEnemy; +} + +inline void CBossAlpha::AddCondition( Condition c ) +{ + m_conditionFlags |= c; +} + +inline bool CBossAlpha::IsInCondition( Condition c ) const +{ + return ( m_conditionFlags & c ) ? true : false; +} + +inline const CUtlVector< CBossAlpha::AttackerInfo > &CBossAlpha::GetAttackerVector( void ) const +{ + return m_attackerVector; +} + + +//-------------------------------------------------------------------------------------------------------------- +class CBossAlphaPathCost : public IPathCost +{ +public: + CBossAlphaPathCost( CBossAlpha *me ) + { + m_me = me; + } + + // return the cost (weighted distance between) of moving from "fromArea" to "area", or -1 if the move is not allowed + virtual float operator()( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) const + { + if ( fromArea == NULL ) + { + // first area in path, no cost + return 0.0f; + } + else + { + if ( !m_me->GetLocomotionInterface()->IsAreaTraversable( area ) ) + { + // our locomotor says we can't move here + return -1.0f; + } + + // compute distance traveled along path so far + float dist; + + if ( ladder ) + { + dist = ladder->m_length; + } + else if ( length > 0.0 ) + { + // optimization to avoid recomputing length + dist = length; + } + else + { + dist = ( area->GetCenter() - fromArea->GetCenter() ).Length(); + } + + float cost = dist + fromArea->GetCostSoFar(); + + // check height change + float deltaZ = fromArea->ComputeAdjacentConnectionHeightChange( area ); + if ( deltaZ >= m_me->GetLocomotionInterface()->GetStepHeight() ) + { + if ( deltaZ >= m_me->GetLocomotionInterface()->GetMaxJumpHeight() ) + { + // too high to reach + return -1.0f; + } + + // jumping is slower than flat ground + const float jumpPenalty = 5.0f; + cost += jumpPenalty * dist; + } + else if ( deltaZ < -m_me->GetLocomotionInterface()->GetDeathDropHeight() ) + { + // too far to drop + return -1.0f; + } + + return cost; + } + } + + CBossAlpha *m_me; +}; + +#endif // TF_RAID_MODE + +#endif // BOSS_ALPHA_H diff --git a/game/server/tf/player_vs_environment/monster_resource.cpp b/game/server/tf/player_vs_environment/monster_resource.cpp new file mode 100644 index 0000000..2f4124d --- /dev/null +++ b/game/server/tf/player_vs_environment/monster_resource.cpp @@ -0,0 +1,129 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Entity that propagates general data needed by clients for non-player AI characters +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "player.h" +#include "monster_resource.h" +#include <coordsize.h> + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +IMPLEMENT_SERVERCLASS_ST_NOBASE( CMonsterResource, DT_MonsterResource ) + + SendPropInt( SENDINFO( m_iBossHealthPercentageByte ), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_iBossStunPercentageByte ), 8, SPROP_UNSIGNED ), + + SendPropInt( SENDINFO( m_iSkillShotCompleteCount ), 3, SPROP_UNSIGNED ), + SendPropTime( SENDINFO( m_fSkillShotComboEndTime ) ), + + SendPropInt( SENDINFO( m_iBossState ) ), + +END_SEND_TABLE() + + +BEGIN_DATADESC( CMonsterResource ) + + DEFINE_FIELD( m_iBossHealthPercentageByte, FIELD_INTEGER ), + DEFINE_FIELD( m_iBossStunPercentageByte, FIELD_INTEGER ), + + DEFINE_FIELD( m_iSkillShotCompleteCount, FIELD_INTEGER ), + DEFINE_FIELD( m_fSkillShotComboEndTime, FIELD_TIME ), + + // Function Pointers + DEFINE_FUNCTION( Update ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( monster_resource, CMonsterResource ); + +CMonsterResource *g_pMonsterResource = NULL; + + +//----------------------------------------------------------------------------- +void CMonsterResource::Spawn( void ) +{ + SetThink( &CMonsterResource::Update ); + SetNextThink( gpGlobals->curtime ); + + m_iBossHealthPercentageByte = 0; + m_iBossStunPercentageByte = 0; + m_iSkillShotCompleteCount = 0; + m_fSkillShotComboEndTime = 0; + m_iBossState = 0; +} + + +//----------------------------------------------------------------------------- +// +// The Player resource is always transmitted to clients +// +int CMonsterResource::UpdateTransmitState( void ) +{ + // ALWAYS transmit to all clients. + return SetTransmitState( FL_EDICT_ALWAYS ); +} + + +//----------------------------------------------------------------------------- +void CMonsterResource::Update( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1f ); + +} + + +//----------------------------------------------------------------------------- +void CMonsterResource::SetBossHealthPercentage( float percentFull ) +{ + m_iBossHealthPercentageByte = 255.0f * percentFull; +} + + +//----------------------------------------------------------------------------- +void CMonsterResource::HideBossHealthMeter( void ) +{ + m_iBossHealthPercentageByte = 0; + m_iBossStunPercentageByte = 0; +} + + +//----------------------------------------------------------------------------- +void CMonsterResource::SetBossStunPercentage( float percentFull ) +{ + m_iBossStunPercentageByte = 255.0f * percentFull; +} + + +//----------------------------------------------------------------------------- +void CMonsterResource::HideBossStunMeter( void ) +{ + m_iBossStunPercentageByte = 0; +} + + +//----------------------------------------------------------------------------- +void CMonsterResource::StartSkillShotComboMeter( float comboMaxDuration ) +{ + m_fSkillShotComboEndTime = gpGlobals->curtime + comboMaxDuration; + m_iSkillShotCompleteCount = 1; +} + + +//----------------------------------------------------------------------------- +void CMonsterResource::IncrementSkillShotComboMeter( void ) +{ + m_iSkillShotCompleteCount = m_iSkillShotCompleteCount + 1; +} + + +//----------------------------------------------------------------------------- +void CMonsterResource::HideSkillShotComboMeter( void ) +{ + m_iSkillShotCompleteCount = 0; +} diff --git a/game/server/tf/player_vs_environment/monster_resource.h b/game/server/tf/player_vs_environment/monster_resource.h new file mode 100644 index 0000000..98cdfe7 --- /dev/null +++ b/game/server/tf/player_vs_environment/monster_resource.h @@ -0,0 +1,53 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Entity that propagates general data needed by clients for non-player AI characters +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef MONSTER_RESOURCE_H +#define MONSTER_RESOURCE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "shareddefs.h" + +class CMonsterResource : public CBaseEntity +{ + DECLARE_CLASS( CMonsterResource, CBaseEntity ); +public: + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + virtual void Spawn( void ); + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() | FCAP_DONT_SAVE; } + + virtual void Update( void ); + virtual int UpdateTransmitState( void ); + + virtual void SetBossHealthPercentage( float percentFull ); // if this is nonnegative, a HUD meter will be shown + virtual void HideBossHealthMeter( void ); + + virtual void SetBossStunPercentage( float percentFull ); + virtual void HideBossStunMeter( void ); + + virtual void StartSkillShotComboMeter( float comboMaxDuration ); + virtual void IncrementSkillShotComboMeter( void ); + virtual void HideSkillShotComboMeter( void ); + + void SetBossState( int iState ) { m_iBossState = iState; } + +protected: + CNetworkVar( int, m_iBossHealthPercentageByte ); // 0-255 + CNetworkVar( int, m_iBossStunPercentageByte ); // 0-255 + + CNetworkVar( int, m_iSkillShotCompleteCount ); // the number of consecutive skill shots that have been completed. 0 = don't show combo HUD + CNetworkVar( float, m_fSkillShotComboEndTime ); // the time when the current skill shot combo window closes + + CNetworkVar( int, m_iBossState ); // boss state? +}; + +extern CMonsterResource *g_pMonsterResource; + +#endif // MONSTER_RESOURCE_H diff --git a/game/server/tf/player_vs_environment/tf_base_boss.cpp b/game/server/tf/player_vs_environment/tf_base_boss.cpp new file mode 100644 index 0000000..8e31cd1 --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_base_boss.cpp @@ -0,0 +1,639 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#include "cbase.h" + +#include "tf_gamerules.h" +#include "tf_base_boss.h" +#include "entity_currencypack.h" +#include "tf_gamestats.h" +#include "tf_player.h" + +LINK_ENTITY_TO_CLASS( base_boss, CTFBaseBoss ); + +PRECACHE_REGISTER( base_boss ); + +IMPLEMENT_SERVERCLASS_ST( CTFBaseBoss, DT_TFBaseBoss) + SendPropFloat( SENDINFO(m_lastHealthPercentage), 11, SPROP_NOSCALE, 0.0, 1.0 ), +END_SEND_TABLE() + +BEGIN_DATADESC( CTFBaseBoss ) + + DEFINE_KEYFIELD( m_initialHealth, FIELD_INTEGER, "health" ), + DEFINE_KEYFIELD( m_modelString, FIELD_STRING, "model" ), + DEFINE_KEYFIELD( m_speed, FIELD_FLOAT, "speed" ), + DEFINE_KEYFIELD( m_startDisabled, FIELD_INTEGER, "start_disabled" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeed", InputSetSpeed ), + + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetHealth", InputSetHealth ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetMaxHealth", InputSetMaxHealth ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "AddHealth", InputAddHealth ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "RemoveHealth", InputRemoveHealth ), + + 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" ), + + DEFINE_THINKFUNC( BossThink ), + +END_DATADESC() + +ConVar tf_base_boss_speed( "tf_base_boss_speed", "75", FCVAR_CHEAT ); +ConVar tf_base_boss_max_turn_rate( "tf_base_boss_max_turn_rate", "25", FCVAR_CHEAT ); + +extern void HandleRageGain( CTFPlayer *pPlayer, unsigned int iRequiredBuffFlags, float flDamage, float fInverseRageGainScale ); + +//-------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------- +float CTFBaseBossLocomotion::GetRunSpeed( void ) const +{ + CTFBaseBoss *boss = (CTFBaseBoss *)GetBot()->GetEntity(); + return boss->GetMaxSpeed(); +} + + +//-------------------------------------------------------------------------------------- +void CTFBaseBossLocomotion::FaceTowards( const Vector &target ) +{ + CTFBaseBoss *pTank = static_cast< CTFBaseBoss* >( GetBot()->GetEntity() ); + + const float deltaT = GetUpdateInterval(); + + QAngle angles = pTank->GetLocalAngles(); + + float desiredYaw = UTIL_VecToYaw( target - GetFeet() ); + + float angleDiff = UTIL_AngleDiff( desiredYaw, angles.y ); + + float deltaYaw = tf_base_boss_max_turn_rate.GetFloat() * deltaT; + + if ( angleDiff < -deltaYaw ) + { + angles.y -= deltaYaw; + } + else if ( angleDiff > deltaYaw ) + { + angles.y += deltaYaw; + } + else + { + angles.y += angleDiff; + } + + Vector forward, right; + pTank->GetVectors( NULL, &right, NULL ); + + forward = CrossProduct( GetGroundNormal(), right ); + + float desiredPitch = UTIL_VecToPitch( forward ); + + angleDiff = UTIL_AngleDiff( desiredPitch, angles.x ); + + float deltaPitch = tf_base_boss_max_turn_rate.GetFloat() * deltaT; + + if ( angleDiff < -deltaPitch ) + { + angles.x -= deltaPitch; + } + else if ( angleDiff > deltaPitch ) + { + angles.x += deltaPitch; + } + else + { + angles.x += angleDiff; + } + + pTank->SetLocalAngles( angles ); + pTank->UpdateCollisionBounds(); +} + + +//-------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------- +CTFBaseBoss::CTFBaseBoss() +{ + m_modelString = NULL_STRING; + m_lastHealthPercentage = 1.0f; + m_speed = tf_base_boss_speed.GetFloat(); + m_locomotor = new CTFBaseBossLocomotion( this ); + m_currencyValue = TF_BASE_BOSS_CURRENCY; + m_initialHealth = 0; + + m_bResolvePlayerCollisions = true; +} + + +//-------------------------------------------------------------------------------------- +CTFBaseBoss::~CTFBaseBoss() +{ + delete m_locomotor; +} + + +//-------------------------------------------------------------------------------------- +void CTFBaseBoss::Precache( void ) +{ + if ( m_modelString != NULL_STRING ) + { + PrecacheModel( STRING( m_modelString ) ); + } + + BaseClass::Precache(); +} + + +//-------------------------------------------------------------------------------------- +void CTFBaseBoss::Spawn( void ) +{ + Precache(); + + BaseClass::Spawn(); + + if ( m_modelString != NULL_STRING ) + { + SetModel( STRING( m_modelString ) ); + } + + m_isEnabled = m_startDisabled ? false : true; + + SetHealth( m_initialHealth ); + SetMaxHealth( m_initialHealth ); + + if ( TFGameRules() ) + { + TFGameRules()->AddActiveBoss( this ); + } + + m_lastHealthPercentage = 1.0f; + m_damagePoseParameter = -1; + + SetThink( &CTFBaseBoss::BossThink ); + SetNextThink( gpGlobals->curtime ); +} + +int CTFBaseBoss::UpdateTransmitState() +{ + // ALWAYS transmit to all clients. + return SetTransmitState( FL_EDICT_ALWAYS ); +} + + +//-------------------------------------------------------------------------------------- +void CTFBaseBoss::ResolvePlayerCollision( CTFPlayer *player ) +{ + Vector bossGlobalMins = WorldAlignMins() + GetAbsOrigin(); + Vector bossGlobalMaxs = WorldAlignMaxs() + GetAbsOrigin(); + + Vector playerGlobalMins = player->WorldAlignMins() + player->GetAbsOrigin(); + Vector playerGlobalMaxs = player->WorldAlignMaxs() + player->GetAbsOrigin(); + + Vector newPlayerPos = player->GetAbsOrigin(); + + + if ( playerGlobalMins.x > bossGlobalMaxs.x || + playerGlobalMaxs.x < bossGlobalMins.x || + playerGlobalMins.y > bossGlobalMaxs.y || + playerGlobalMaxs.y < bossGlobalMins.y || + playerGlobalMins.z > bossGlobalMaxs.z || + playerGlobalMaxs.z < bossGlobalMins.z ) + { + // no overlap + return; + } + + Vector toPlayer = player->WorldSpaceCenter() - WorldSpaceCenter(); + + Vector overlap; + float signX, signY, signZ; + + if ( toPlayer.x >= 0 ) + { + overlap.x = bossGlobalMaxs.x - playerGlobalMins.x; + signX = 1.0f; + } + else + { + overlap.x = playerGlobalMaxs.x - bossGlobalMins.x; + signX = -1.0f; + } + + if ( toPlayer.y >= 0 ) + { + overlap.y = bossGlobalMaxs.y - playerGlobalMins.y; + signY = 1.0f; + } + else + { + overlap.y = playerGlobalMaxs.y - bossGlobalMins.y; + signY = -1.0f; + } + + if ( toPlayer.z >= 0 ) + { + overlap.z = bossGlobalMaxs.z - playerGlobalMins.z; + signZ = 1.0f; + } + else + { + // don't push player underground + overlap.z = 99999.9f; // playerGlobalMaxs.z - bossGlobalMins.z; + signZ = -1.0f; + } + + float bloat = 5.0f; + + if ( overlap.x < overlap.y ) + { + if ( overlap.x < overlap.z ) + { + // X is least overlap + newPlayerPos.x += signX * ( overlap.x + bloat ); + } + else + { + // Z is least overlap + newPlayerPos.z += signZ * ( overlap.z + bloat ); + } + } + else if ( overlap.z < overlap.y ) + { + // Z is least overlap + newPlayerPos.z += signZ * ( overlap.z + bloat ); + } + else + { + // Y is least overlap + newPlayerPos.y += signY * ( overlap.y + bloat ); + } + + // check if new location is valid + trace_t result; + Ray_t ray; + ray.Init( newPlayerPos, newPlayerPos, player->WorldAlignMins(), player->WorldAlignMaxs() ); + UTIL_TraceRay( ray, MASK_PLAYERSOLID, player, COLLISION_GROUP_PLAYER_MOVEMENT, &result ); + + if ( result.DidHit() ) + { + // Trace down from above to find safe ground + ray.Init( newPlayerPos + Vector( 0.0f, 0.0f, 32.0f ), newPlayerPos, player->WorldAlignMins(), player->WorldAlignMaxs() ); + UTIL_TraceRay( ray, MASK_PLAYERSOLID, player, COLLISION_GROUP_PLAYER_MOVEMENT, &result ); + + if ( result.startsolid ) + { + // player was crushed against something + player->TakeDamage( CTakeDamageInfo( this, this, 99999.9f, DMG_CRUSH ) ); + return; + } + else + { + // Use the trace end position + newPlayerPos = result.endpos; + } + } + + player->SetAbsOrigin( newPlayerPos ); +} + + +//-------------------------------------------------------------------------------------- +void CTFBaseBoss::Touch( CBaseEntity *pOther ) +{ + BaseClass::Touch( pOther ); + + if ( pOther && pOther->IsBaseObject() ) + { + // ran over an engineer building - destroy it + pOther->TakeDamage( CTakeDamageInfo( this, this, 99999.9f, DMG_CRUSH ) ); + } +} + + +//-------------------------------------------------------------------------------------- +void CTFBaseBoss::BossThink( void ) +{ + SetNextThink( gpGlobals->curtime ); + + if ( m_damagePoseParameter < 0 ) + { + m_damagePoseParameter = LookupPoseParameter( "damage" ); + } + + if ( m_damagePoseParameter >= 0 ) + { + // Avoid dividing by zero + if ( GetMaxHealth() ) + { + SetPoseParameter( m_damagePoseParameter, 1.0f - ( (float)GetHealth() / (float)GetMaxHealth() ) ); + } + else + { + SetPoseParameter( m_damagePoseParameter, 1.0f ); + } + } + + if ( !m_isEnabled ) + { + return; + } + + Update(); + + if ( m_bResolvePlayerCollisions ) + { + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TEAM_ANY, COLLECT_ONLY_LIVING_PLAYERS ); + for( int i=0; i<playerVector.Count(); ++i ) + { + ResolvePlayerCollision( playerVector[i] ); + } + } +} + +//-------------------------------------------------------------------------------------- +void CTFBaseBoss::Event_Killed( const CTakeDamageInfo &info ) +{ + m_outputOnKilled.FireOutput( this, this ); + + // drop some loot! + m_currencyValue = GetCurrencyValue(); + + int nRemainingMoney = m_currencyValue; + + QAngle angRand = vec3_angle; + + while( nRemainingMoney > 0 ) + { + int nAmount = 0; + + if ( nRemainingMoney >= 100 ) + { + nAmount = 25; + } + else if ( nRemainingMoney >= 40 ) + { + nAmount = 10; + } + else if ( nRemainingMoney >= 5 ) + { + nAmount = 5; + } + else + { + nAmount = nRemainingMoney; + } + + nRemainingMoney -= nAmount; + + angRand.y = RandomFloat( -180.0f, 180.0f ); + + CCurrencyPackCustom *pCurrencyPack = assert_cast< CCurrencyPackCustom* >( CBaseEntity::CreateNoSpawn( "item_currencypack_custom", WorldSpaceCenter(), angRand, this ) ); + + if ( pCurrencyPack ) + { + pCurrencyPack->SetAmount( nAmount ); + + Vector vecImpulse = RandomVector( -1,1 ); + vecImpulse.z = RandomFloat( 5.0f, 20.0f ); + VectorNormalize( vecImpulse ); + Vector vecVelocity = vecImpulse * 250.0 * RandomFloat( 1.0f, 4.0f ); + + DispatchSpawn( pCurrencyPack ); + pCurrencyPack->DropSingleInstance( vecVelocity, this, 0, 0 ); + } + } + + BaseClass::Event_Killed( info ); + + UTIL_Remove( this ); +} + +void CTFBaseBoss::UpdateOnRemove() +{ + if ( TFGameRules() ) + { + TFGameRules()->RemoveActiveBoss( this ); + } + + BaseClass::UpdateOnRemove(); +} + +int CTFBaseBoss::OnTakeDamage( const CTakeDamageInfo &rawInfo ) +{ + CTakeDamageInfo info = rawInfo; + + if ( TFGameRules() ) + { + TFGameRules()->ApplyOnDamageModifyRules( info, this, true ); + } + + // On damage Rage + // Give the soldier/pyro some rage points for dealing/taking damage. + if ( info.GetDamage() && info.GetAttacker() != this ) + { + CTFPlayer *pAttacker = ToTFPlayer( info.GetAttacker() ); + + // Buff flag 1: we get rage when we deal damage. Here, that means the soldier that attacked + // gets rage when we take damage. + HandleRageGain( pAttacker, kRageBuffFlag_OnDamageDealt, info.GetDamage(), 6.0f ); + + // Buff 5: our pyro attacker get rage when we're damaged by fire + if ( ( info.GetDamageType() & DMG_BURN ) != 0 || ( info.GetDamageType() & DMG_PLASMA ) != 0 ) + { + HandleRageGain( pAttacker, kRageBuffFlag_OnBurnDamageDealt, info.GetDamage(), 30.f ); + } + + if ( pAttacker && info.GetWeapon() ) + { + CTFWeaponBase *pWeapon = dynamic_cast<CTFWeaponBase *>( info.GetWeapon() ); + if ( pWeapon ) + { + pWeapon->ApplyOnHitAttributes( this, pAttacker, info ); + } + } + } + + return BaseClass::OnTakeDamage( info ); +} + +//-------------------------------------------------------------------------------------- +int CTFBaseBoss::OnTakeDamage_Alive( const CTakeDamageInfo &rawInfo ) +{ + if ( !rawInfo.GetAttacker() || rawInfo.GetAttacker()->GetTeamNumber() == GetTeamNumber() ) + { + // no friendly fire damage + return 0; + } + + CTakeDamageInfo info = rawInfo; + + // weapon-specific damage modification + ModifyDamage( &info ); + + if ( TFGameRules() ) + { + CTFGameRules::DamageModifyExtras_t outParams; + info.SetDamage( TFGameRules()->ApplyOnDamageAliveModifyRules( info, this, outParams ) ); + } + + // 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; + + // Let attacker react to the damage they dealt + CTFPlayer *pAttacker = ToTFPlayer( rawInfo.GetAttacker() ); + if ( pAttacker ) + { + pAttacker->OnDealtDamage( this, info ); + + CTF_GameStats.Event_BossDamage( pAttacker, info.GetDamage() ); + } + + return result; +} + + +//-------------------------------------------------------------------------------------- +void CTFBaseBoss::InputEnable( inputdata_t &inputdata ) +{ + m_isEnabled = true; +} + + +//-------------------------------------------------------------------------------------- +void CTFBaseBoss::InputDisable( inputdata_t &inputdata ) +{ + m_isEnabled = false; +} + + +//------------------------------------------------------------------------------ +void CTFBaseBoss::InputSetSpeed( inputdata_t &inputdata ) +{ + m_speed = inputdata.value.Float(); +} + +//----------------------------------------------------------------------------- +// Purpose: Set the health of the boss +//----------------------------------------------------------------------------- +void CTFBaseBoss::InputSetHealth( inputdata_t &inputdata ) +{ + m_iHealth = inputdata.value.Int(); + SetHealth( m_iHealth ); +} + +//----------------------------------------------------------------------------- +// Purpose: Set the max health of the boss +//----------------------------------------------------------------------------- +void CTFBaseBoss::InputSetMaxHealth( inputdata_t &inputdata ) +{ + m_iMaxHealth = inputdata.value.Int(); + SetMaxHealth( m_iMaxHealth ); +} + +//----------------------------------------------------------------------------- +// Purpose: Add health to the boss +//----------------------------------------------------------------------------- +void CTFBaseBoss::InputAddHealth( inputdata_t &inputdata ) +{ + int iHealth = inputdata.value.Int(); + SetHealth( MIN( GetMaxHealth(), GetHealth() + iHealth ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: Remove health from the boss +//----------------------------------------------------------------------------- +void CTFBaseBoss::InputRemoveHealth( inputdata_t &inputdata ) +{ + int iDamage = inputdata.value.Int(); + + SetHealth( GetHealth() - iDamage ); + if ( GetHealth() <= 0 ) + { + CTakeDamageInfo info( inputdata.pCaller, inputdata.pActivator, vec3_origin, GetAbsOrigin(), iDamage, DMG_GENERIC, TF_DMG_CUSTOM_NONE ); + Event_Killed( info ); + } +} diff --git a/game/server/tf/player_vs_environment/tf_base_boss.h b/game/server/tf/player_vs_environment/tf_base_boss.h new file mode 100644 index 0000000..06b2d57 --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_base_boss.h @@ -0,0 +1,121 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#ifndef TF_BASE_BOSS_H +#define TF_BASE_BOSS_H + +#include "NextBot/NextBot.h" +#include "NextBot/NextBotGroundLocomotion.h" +#include "pathtrack.h" + +class CTFPlayer; + +#define TF_BASE_BOSS_CURRENCY 125 + + +//---------------------------------------------------------------------------- +class CTFBaseBossLocomotion : public NextBotGroundLocomotion +{ +public: + CTFBaseBossLocomotion( INextBot *bot ) : NextBotGroundLocomotion( bot ) { } + virtual ~CTFBaseBossLocomotion() { } + + virtual float GetRunSpeed( void ) const; // get maximum running speed + virtual float GetStepHeight( void ) const { return 100.0f; } // if delta Z is greater than this, we have to jump to get up + virtual float GetMaxJumpHeight( void ) const { return 100.0f; } // return maximum height of a jump + + virtual void FaceTowards( const Vector &target ); // rotate body to face towards "target" +}; + + +//---------------------------------------------------------------------------- +class CTFBaseBoss : public NextBotCombatCharacter +{ +public: + DECLARE_CLASS( CTFBaseBoss, NextBotCombatCharacter ); + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CTFBaseBoss(); + virtual ~CTFBaseBoss(); + + virtual void Precache( void ); + virtual void Spawn( void ); + int UpdateTransmitState( void ); + + virtual void UpdateCollisionBounds( void ) {} + + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + virtual void Event_Killed( const CTakeDamageInfo &info ); + virtual void UpdateOnRemove(); + + virtual bool IsRemovedOnReset( void ) const { return false; } // remove this bot when the NextBot manager calls Reset + + virtual CTFBaseBossLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; } + + virtual void Touch( CBaseEntity *pOther ); + + virtual int GetCurrencyValue( void ){ return TF_BASE_BOSS_CURRENCY; } + + void BossThink( void ); + + void SetResolvePlayerCollisions( bool bResolve ) { m_bResolvePlayerCollisions = bResolve; } + + void SetMaxSpeed( float value ) { m_speed = value; } + float GetMaxSpeed( void ) const { return m_speed; } + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + void InputSetSpeed( inputdata_t &inputdata ); + void InputSetHealth( inputdata_t &inputdata ); + void InputSetMaxHealth( inputdata_t &inputdata ); + void InputAddHealth( inputdata_t &inputdata ); + void InputRemoveHealth( inputdata_t &inputdata ); + + void SetInitialHealth( int value ); + void SetCurrencyValue( int value ); // how much cash do we drop when we die? + + COutputEvent m_outputOnHealthBelow90Percent; + COutputEvent m_outputOnHealthBelow80Percent; + COutputEvent m_outputOnHealthBelow70Percent; + COutputEvent m_outputOnHealthBelow60Percent; + COutputEvent m_outputOnHealthBelow50Percent; + COutputEvent m_outputOnHealthBelow40Percent; + COutputEvent m_outputOnHealthBelow30Percent; + COutputEvent m_outputOnHealthBelow20Percent; + COutputEvent m_outputOnHealthBelow10Percent; + + COutputEvent m_outputOnKilled; + +protected: + virtual void ModifyDamage( CTakeDamageInfo *info ) const { } + int GetInitialHealth( ) const { return m_initialHealth; } + +private: + int m_initialHealth; + CNetworkVar( float, m_lastHealthPercentage ); + string_t m_modelString; + float m_speed; + int m_startDisabled; + bool m_isEnabled; + int m_damagePoseParameter; + int m_currencyValue; + + bool m_bResolvePlayerCollisions; + + CTFBaseBossLocomotion *m_locomotor; + + void ResolvePlayerCollision( CTFPlayer *player ); +}; + + +inline void CTFBaseBoss::SetInitialHealth( int value ) +{ + m_initialHealth = value; +} + +inline void CTFBaseBoss::SetCurrencyValue( int value ) +{ + m_currencyValue = value; +} + + +#endif // TF_BASE_BOSS_H diff --git a/game/server/tf/player_vs_environment/tf_boss_battle_logic.cpp b/game/server/tf/player_vs_environment/tf_boss_battle_logic.cpp new file mode 100644 index 0000000..6e0a66d --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_boss_battle_logic.cpp @@ -0,0 +1,125 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_boss_battle_logic.cpp +// Boss battle game mode singleton manager +// Michael Booth, April 2011 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "tf_team.h" +#include "bot/map_entities/tf_bot_generator.h" +#include "player_vs_environment/tf_boss_battle_logic.h" + +CBossBattleLogic *g_pBossBattleLogic = NULL; + + +//------------------------------------------------------------------------- +//------------------------------------------------------------------------- +BEGIN_DATADESC( CBossBattleLogic ) + DEFINE_THINKFUNC( Update ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( tf_logic_boss_battle, CBossBattleLogic ); + + +ConVar tf_bot_npc_minion_wave_launch_count( "tf_bot_npc_minion_wave_launch_count", "0"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_wave_initial_launch_interval( "tf_bot_npc_minion_wave_initial_launch_interval", "15"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_wave_launch_interval( "tf_bot_npc_minion_wave_launch_interval", "30"/*, FCVAR_CHEAT*/ ); + +ConVar tf_bot_npc_grunt_wave_launch_count( "tf_bot_npc_grunt_wave_launch_count", "2"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_grunt_wave_initial_launch_interval( "tf_bot_grunt_wave_initial_launch_interval", "25"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_grunt_wave_launch_interval( "tf_bot_npc_grunt_wave_launch_interval", "30"/*, FCVAR_CHEAT*/ ); + + +//------------------------------------------------------------------------- +CBossBattleLogic::CBossBattleLogic() +{ + ListenForGameEvent( "teamplay_round_win" ); + ListenForGameEvent( "teamplay_round_start" ); +} + + +//------------------------------------------------------------------------- +CBossBattleLogic::~CBossBattleLogic() +{ + g_pBossBattleLogic = NULL; +} + + +//------------------------------------------------------------------------- +void CBossBattleLogic::Spawn( void ) +{ + BaseClass::Spawn(); + + Reset(); + + SetThink( &CBossBattleLogic::Update ); + SetNextThink( gpGlobals->curtime ); + + g_pBossBattleLogic = this; +} + + +//------------------------------------------------------------------------- +void CBossBattleLogic::Reset( void ) +{ + // unspawn entire red team + CTeam *defendingTeam = GetGlobalTeam( TF_TEAM_RED ); + int i; + for( i=0; i<defendingTeam->GetNumPlayers(); ++i ) + { + engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", defendingTeam->GetPlayer(i)->GetUserID() ) ); + } + + // remove all minions + CBaseEntity *minion = NULL; + while( ( minion = gEntList.FindEntityByClassname( minion, "bot_npc_minion" ) ) != NULL ) + { + UTIL_Remove( minion ); + } +} + + +//------------------------------------------------------------------------- +void CBossBattleLogic::OnRoundStart( void ) +{ + if ( !TFGameRules() || !TFGameRules()->IsBossBattleMode() ) + return; + + Reset(); +} + + +//-------------------------------------------------------------------------------------------------------- +void CBossBattleLogic::FireGameEvent( IGameEvent *event ) +{ + const char *eventName = event->GetName(); + + if ( !Q_strcmp( eventName, "teamplay_round_win" ) ) + { + if ( event->GetInt( "team" ) == TF_TEAM_RED ) + { + } + } + else if ( !Q_strcmp( eventName, "teamplay_round_start" ) ) + { + OnRoundStart(); + } +} + + +//-------------------------------------------------------------------------------------------------------- +void CBossBattleLogic::Update( void ) +{ + VPROF_BUDGET( "CBossBattleLogic::Update", "Game" ); + + const float deltaT = 0.33f; + SetNextThink( gpGlobals->curtime + deltaT ); + + if ( !TFGameRules()->IsBossBattleMode() ) + return; +} + + +#endif // TF_RAID_MODE diff --git a/game/server/tf/player_vs_environment/tf_boss_battle_logic.h b/game/server/tf/player_vs_environment/tf_boss_battle_logic.h new file mode 100644 index 0000000..a666b8e --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_boss_battle_logic.h @@ -0,0 +1,45 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_boss_battle_logic.h +// Boss battle game mode singleton manager +// Michael Booth, April 2011 + +#ifndef TF_BOSS_BATTLE_H +#define TF_BOSS_BATTLE_H + +#ifdef TF_RAID_MODE + +#include "tf_gamerules.h" + +class CTFBotActionPoint; + + +//----------------------------------------------------------------------- +class CBossBattleLogic : public CPointEntity, public CGameEventListener +{ + DECLARE_CLASS( CBossBattleLogic, CPointEntity ); +public: + DECLARE_DATADESC(); + + CBossBattleLogic(); + virtual ~CBossBattleLogic(); + + virtual void Spawn( void ); + void Reset( void ); + void Update( void ); + + void OnRoundStart( void ); + + virtual void FireGameEvent( IGameEvent *event ); + + virtual int UpdateTransmitState() + { + return SetTransmitState( FL_EDICT_ALWAYS ); + } +}; + +extern CBossBattleLogic *g_pBossBattleLogic; + +#endif // TF_RAID_MODE + + +#endif // TF_BOSS_BATTLE_H diff --git a/game/server/tf/player_vs_environment/tf_mann_vs_machine_logic.cpp b/game/server/tf/player_vs_environment/tf_mann_vs_machine_logic.cpp new file mode 100644 index 0000000..1f5d99e --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_mann_vs_machine_logic.cpp @@ -0,0 +1,186 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_mann_vs_macine_logic.h +// Mann Vs Machine game mode singleton manager +// Michael Booth, June 2011 + +#include "cbase.h" + +#include "tf_team.h" +#include "tf_obj_sentrygun.h" +#include "tf_population_manager.h" +#include "bot/map_entities/tf_bot_generator.h" +#include "player_vs_environment/tf_mann_vs_machine_logic.h" +#include "tf_gamerules.h" +#include "tf_objective_resource.h" + +CHandle<CMannVsMachineLogic> g_hMannVsMachineLogic; + +//------------------------------------------------------------------------- +//------------------------------------------------------------------------- +BEGIN_DATADESC( CMannVsMachineLogic ) + DEFINE_THINKFUNC( Update ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( tf_logic_mann_vs_machine, CMannVsMachineLogic ); + +//------------------------------------------------------------------------- +CMannVsMachineLogic::CMannVsMachineLogic() +{ + InitPopulationManager(); + + m_flNextAlarmCheck = 0.0f; +} + + +//------------------------------------------------------------------------- +CMannVsMachineLogic::~CMannVsMachineLogic() +{ + g_hMannVsMachineLogic = NULL; +} + + +//------------------------------------------------------------------------- +void CMannVsMachineLogic::Spawn( void ) +{ + BaseClass::Spawn(); + + SetThink( &CMannVsMachineLogic::Update ); + SetNextThink( gpGlobals->curtime ); + + g_hMannVsMachineLogic = this; +} + + +//------------------------------------------------------------------------- +void CMannVsMachineLogic::SetupOnRoundStart( void ) +{ + if ( !TFGameRules() || !TFGameRules()->IsMannVsMachineMode() ) + return; + + if ( m_populationManager ) + { + m_populationManager->SetupOnRoundStart(); + } +} + + +//-------------------------------------------------------------------------------------------------------- +void CMannVsMachineLogic::Update( void ) +{ + VPROF_BUDGET( "CMannVsMachineLogic::Update", "Game" ); + + SetNextThink( gpGlobals->curtime + 0.05f ); + + if ( !TFGameRules() || !TFGameRules()->IsMannVsMachineMode() ) + return; + + if ( m_populationManager ) + { + m_populationManager->Update(); + } + + // we don't need to run this check as often as we're calling our update() function + if ( m_flNextAlarmCheck < gpGlobals->curtime ) + { + m_flNextAlarmCheck = gpGlobals->curtime + 0.1; + for ( int i=0; i<ICaptureFlagAutoList::AutoList().Count(); ++i ) + { + CCaptureFlag *pFlag = static_cast<CCaptureFlag *>( ICaptureFlagAutoList::AutoList()[i] ); + if ( pFlag->IsStolen() ) + { + for ( int j=0; j<IFlagDetectionZoneAutoList::AutoList().Count(); ++j ) + { + CFlagDetectionZone *pZone = static_cast<CFlagDetectionZone *>( IFlagDetectionZoneAutoList::AutoList()[j] ); + if ( !pZone->IsDisabled() && pZone->IsAlarmZone() && pZone->PointIsWithin( pFlag->GetAbsOrigin() ) ) + { + // Is the alarm currently off? + if ( TFGameRules()->GetMannVsMachineAlarmStatus() == false ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_bomb_alarm_triggered" ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } + } + + TFGameRules()->SetMannVsMachineAlarmStatus( true ); + return; + } + } + } + } + + TFGameRules()->SetMannVsMachineAlarmStatus( false ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMannVsMachineLogic::InitPopulationManager( void ) +{ + bool bFound = false; + const char *pszFormat = MVM_POP_FILE_PATH "/%s.pop"; + char szFileName[MAX_PATH] = { 0 }; + + // Did they request something specific? + if ( Q_strlen( TFGameRules()->GetNextMvMPopfile() ) ) + { + Q_snprintf( szFileName, sizeof( szFileName ), pszFormat, TFGameRules()->GetNextMvMPopfile() ); + if ( g_pFullFileSystem->FileExists( szFileName, "GAME" ) ) + { + bFound = true; + } + else + { + // It might need the additional map prefix to find the file + Q_snprintf( szFileName, sizeof( szFileName ), MVM_POP_FILE_PATH "/%s_%s.pop", STRING( gpGlobals->mapname ), TFGameRules()->GetNextMvMPopfile() ); + if ( g_pFullFileSystem->FileExists( szFileName, "GAME" ) ) + { + bFound = true; + } + if ( !bFound ) + { + Warning( "Population file '%s' not found", TFGameRules()->GetNextMvMPopfile() ); + } + } + } + + // See if we have any default popfiles and use the default-iest one + if ( !bFound ) + { + CUtlVector< CUtlString > defaultPopFileList; + CUtlString defaultPopFileName; + g_pPopulationManager->FindDefaultPopulationFileShortNames( defaultPopFileList ); + if ( defaultPopFileList.Count() ) + { + if ( g_pPopulationManager->FindPopulationFileByShortName( defaultPopFileList[0], defaultPopFileName ) ) + { + V_strncpy( szFileName, defaultPopFileName, sizeof( szFileName ) ); + bFound = true; + } + } + } + + // Use mapname.pop. This won't exist but would've been the highest priority default so you'll at least get an error + // about the file of last resort. + if ( !bFound ) + { + Q_snprintf( szFileName, sizeof( szFileName ), pszFormat, STRING( gpGlobals->mapname ) ); + } + + if ( m_populationManager && V_strcmp( m_populationManager->GetPopulationFilename(), szFileName ) != 0 ) + { + UTIL_RemoveImmediate( m_populationManager ); + m_populationManager = NULL; + } + + if ( !m_populationManager ) + { + m_populationManager = (CPopulationManager *)CreateEntityByName( "info_populator" ); + m_populationManager->SetPopulationFilename( szFileName ); + } + + // Clear the Value since its now loaded + TFGameRules()->SetNextMvMPopfile( "" ); +} diff --git a/game/server/tf/player_vs_environment/tf_mann_vs_machine_logic.h b/game/server/tf/player_vs_environment/tf_mann_vs_machine_logic.h new file mode 100644 index 0000000..57b0d3e --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_mann_vs_machine_logic.h @@ -0,0 +1,43 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_mann_vs_machine_logic.h +// Mann Vs Machine game mode singleton manager +// Michael Booth, June 2011 + +#ifndef TF_MANN_VS_MACHINE_LOGIC_H +#define TF_MANN_VS_MACHINE_LOGIC_H + +#include "tf_gamerules.h" + +class CTFBotActionPoint; + + +//----------------------------------------------------------------------- +class CMannVsMachineLogic : public CPointEntity +{ + DECLARE_CLASS( CMannVsMachineLogic, CPointEntity ); +public: + DECLARE_DATADESC(); + + CMannVsMachineLogic(); + virtual ~CMannVsMachineLogic(); + + virtual void Spawn( void ); + void Update( void ); + + void SetupOnRoundStart( void ); + + virtual int UpdateTransmitState() + { + return SetTransmitState( FL_EDICT_ALWAYS ); + } + +private: + CHandle< CPopulationManager > m_populationManager; + void InitPopulationManager( void ); + + float m_flNextAlarmCheck; +}; + +extern CHandle<CMannVsMachineLogic> g_hMannVsMachineLogic; + +#endif // TF_MANN_VS_MACHINE_LOGIC_H diff --git a/game/server/tf/player_vs_environment/tf_point_weapon_mimic.cpp b/game/server/tf/player_vs_environment/tf_point_weapon_mimic.cpp new file mode 100644 index 0000000..911c736 --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_point_weapon_mimic.cpp @@ -0,0 +1,312 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// + +#include "cbase.h" +#include "tf_projectile_rocket.h" +#include "tf_projectile_arrow.h" +#include "tf_weapon_grenade_pipebomb.h" + +class CTFPointWeaponMimic : public CPointEntity +{ + DECLARE_CLASS( CTFPointWeaponMimic, CPointEntity ); +public: + DECLARE_DATADESC(); + + CTFPointWeaponMimic(); + ~CTFPointWeaponMimic(); + virtual void Spawn(); + + void InputFireOnce( inputdata_t& inputdata ); + void InputFireMultiple( inputdata_t& inputdata ); + void DetonateStickies( inputdata_t& inputdata ); +private: + void Fire(); + + void FireRocket(); + void FireGrenade(); + void FireArrow(); + void FireStickyGrenade(); + + enum eWeaponType + { + WEAPON_STANDARD_ROCKET, + WEAPON_STANDARD_GRENADE, + WEAPON_STANDARD_ARROW, + WEAPON_STICKY_GRENADE, + + WEAPON_TYPES + }; + + QAngle GetFiringAngles() const; + float GetSpeed() const; + + int m_nWeaponType; + bool m_bContinousFire; + + // Effects for firing + const char* m_pzsFireSound; + const char* m_pzsFireParticles; + + // Override/defaults for the projectile/bullets + const char* m_pzsModelOverride; + float m_flModelScale; + float m_flSpeedMin; + float m_flSpeedMax; + float m_flDamage; + float m_flSplashRadius; + float m_flSpreadAngle; + bool m_bCrits; + + // List of active pipebombs + typedef CHandle<CTFGrenadePipebombProjectile> PipebombHandle; + CUtlVector<PipebombHandle> m_Pipebombs; +}; + +LINK_ENTITY_TO_CLASS( tf_point_weapon_mimic, CTFPointWeaponMimic ); + +// Data Description +BEGIN_DATADESC( CTFPointWeaponMimic ) + + // Keyfields + DEFINE_KEYFIELD( m_nWeaponType, FIELD_INTEGER, "WeaponType" ), + DEFINE_KEYFIELD( m_pzsFireSound, FIELD_SOUNDNAME, "FireSound" ), + DEFINE_KEYFIELD( m_pzsFireParticles, FIELD_STRING, "ParticleEffect" ), + DEFINE_KEYFIELD( m_pzsModelOverride, FIELD_MODELNAME, "ModelOverride" ), + DEFINE_KEYFIELD( m_flModelScale, FIELD_FLOAT, "ModelScale" ), + DEFINE_KEYFIELD( m_flSpeedMin, FIELD_FLOAT, "SpeedMin" ), + DEFINE_KEYFIELD( m_flSpeedMax, FIELD_FLOAT, "SpeedMax" ), + DEFINE_KEYFIELD( m_flDamage, FIELD_FLOAT, "Damage" ), + DEFINE_KEYFIELD( m_flSplashRadius, FIELD_FLOAT, "SplashRadius" ), + DEFINE_KEYFIELD( m_flSpreadAngle, FIELD_FLOAT, "SpreadAngle" ), + DEFINE_KEYFIELD( m_bCrits, FIELD_BOOLEAN, "Crits" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "FireOnce", InputFireOnce ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "FireMultiple", InputFireMultiple ), + DEFINE_INPUTFUNC( FIELD_VOID, "DetonateStickies", DetonateStickies ), +END_DATADESC() + + + +CTFPointWeaponMimic::CTFPointWeaponMimic() +: m_pzsModelOverride( NULL ) +{ +} + +CTFPointWeaponMimic::~CTFPointWeaponMimic() +{ +} + + +void CTFPointWeaponMimic::Spawn() +{ + BaseClass::Spawn(); + + if( m_pzsModelOverride ) + { + PrecacheModel( m_pzsModelOverride ); + } + + ChangeTeam( TF_TEAM_BLUE ); +} + + +void CTFPointWeaponMimic::InputFireOnce( inputdata_t& inputdata ) +{ + Fire(); +} + +void CTFPointWeaponMimic::InputFireMultiple( inputdata_t& inputdata ) +{ + int nNumFires = Max( 1, abs(inputdata.value.Int()) ); + + while( nNumFires-- ) + { + Fire(); + } +} + +void CTFPointWeaponMimic::DetonateStickies( inputdata_t& inputdata ) +{ + int count = m_Pipebombs.Count(); + + for ( int i = 0; i < count; i++ ) + { + CTFGrenadePipebombProjectile *pTemp = m_Pipebombs[i]; + if ( pTemp ) + { + //This guy will die soon enough. + if ( pTemp->IsEffectActive( EF_NODRAW ) ) + continue; + + pTemp->Detonate(); + } + } + + m_Pipebombs.Purge(); +} + + +void CTFPointWeaponMimic::Fire() +{ + Assert( m_nWeaponType >= 0 && m_nWeaponType < WEAPON_TYPES ); + + switch( m_nWeaponType ) + { + case WEAPON_STANDARD_ROCKET: + FireRocket(); + break; + case WEAPON_STANDARD_GRENADE: + FireGrenade(); + break; + case WEAPON_STANDARD_ARROW: + FireArrow(); + break; + case WEAPON_STICKY_GRENADE: + FireStickyGrenade(); + break; + } +} + +void CTFPointWeaponMimic::FireRocket() +{ + CTFProjectile_Rocket *pProjectile = CTFProjectile_Rocket::Create( this, GetAbsOrigin(), GetFiringAngles(), this, NULL); + + if ( pProjectile ) + { + if( m_pzsModelOverride ) + { + pProjectile->SetModel( m_pzsModelOverride ); + } + pProjectile->ChangeTeam( TF_TEAM_BLUE ); + pProjectile->SetCritical( m_bCrits ); + pProjectile->SetDamage( m_flDamage ); + Vector vVelocity = pProjectile->GetAbsVelocity().Normalized() * GetSpeed(); + pProjectile->SetAbsVelocity( vVelocity ); + pProjectile->SetupInitialTransmittedGrenadeVelocity( vVelocity ); + pProjectile->SetCollisionGroup( TFCOLLISION_GROUP_ROCKET_BUT_NOT_WITH_OTHER_ROCKETS ); + } +} + +void CTFPointWeaponMimic::FireGrenade() +{ + QAngle vFireAngles = GetFiringAngles(); + Vector vForward, vUp; + AngleVectors( vFireAngles, &vForward, NULL, &vUp ); + Vector vVelocity( vForward * GetSpeed() ); + + CTFGrenadePipebombProjectile *pGrenade = static_cast<CTFGrenadePipebombProjectile*>( CBaseEntity::CreateNoSpawn( "tf_projectile_pipe", GetAbsOrigin(), vFireAngles, this ) ); + if ( pGrenade ) + { + DispatchSpawn( pGrenade ); + if( m_pzsModelOverride ) + { + pGrenade->SetModel( m_pzsModelOverride ); + } + pGrenade->InitGrenade( vVelocity, AngularImpulse( 600, random->RandomInt( -1200, 1200 ), 0 ), NULL, m_flDamage, m_flSplashRadius ); + pGrenade->ChangeTeam( TF_TEAM_BLUE ); + pGrenade->m_nSkin = 1; + pGrenade->SetDetonateTimerLength( 2.f ); + pGrenade->SetModelScale( m_flModelScale ); + pGrenade->SetCollisionGroup( TFCOLLISION_GROUP_ROCKETS ); // we want to use collision_group_rockets so we don't ever collide with players + pGrenade->SetDamage( m_flDamage ); + pGrenade->SetFullDamage( m_flDamage ); + pGrenade->SetDamageRadius( m_flSplashRadius ); + pGrenade->SetCritical( m_bCrits ); + vVelocity = pGrenade->GetAbsVelocity().Normalized() * GetSpeed(); + pGrenade->SetAbsVelocity( vVelocity ); + pGrenade->SetupInitialTransmittedGrenadeVelocity( vVelocity ); + } +} + +void CTFPointWeaponMimic::FireArrow() +{ + CTFProjectile_Arrow *pProjectile = CTFProjectile_Arrow::Create( GetAbsOrigin(), GetFiringAngles(), 2000, 0.7f, TF_PROJECTILE_ARROW, this, NULL ); + + if ( pProjectile ) + { + if( m_pzsModelOverride ) + { + pProjectile->SetModel( m_pzsModelOverride ); + } + pProjectile->ChangeTeam( TF_TEAM_BLUE ); + pProjectile->SetCritical( m_bCrits ); + pProjectile->SetDamage( m_flDamage ); + Vector vVelocity = pProjectile->GetAbsVelocity().Normalized() * GetSpeed(); + pProjectile->SetAbsVelocity( vVelocity ); + pProjectile->SetupInitialTransmittedGrenadeVelocity( vVelocity ); + pProjectile->SetCollisionGroup( TFCOLLISION_GROUP_ROCKET_BUT_NOT_WITH_OTHER_ROCKETS ); + } +} + +void CTFPointWeaponMimic::FireStickyGrenade() +{ + QAngle vFireAngles = GetFiringAngles(); + Vector vForward, vUp; + AngleVectors( vFireAngles, &vForward, NULL, &vUp ); + Vector vVelocity( vForward * GetSpeed() ); + + CTFGrenadePipebombProjectile *pGrenade = static_cast<CTFGrenadePipebombProjectile*>( CBaseEntity::CreateNoSpawn( "tf_projectile_pipe", GetAbsOrigin(), vFireAngles, this ) ); + if ( pGrenade ) + { + pGrenade->m_bDefensiveBomb = true; + + pGrenade->SetPipebombMode( TF_GL_MODE_REMOTE_DETONATE ); + pGrenade->SetModelScale( m_flModelScale ); + pGrenade->SetCollisionGroup( TFCOLLISION_GROUP_ROCKETS ); // we want to use collision_group_rockets so we don't ever collide with players + pGrenade->SetCanTakeDamage( false ); + DispatchSpawn( pGrenade ); + if( m_pzsModelOverride ) + { + pGrenade->SetModel( m_pzsModelOverride ); + } + else + { + pGrenade->SetModel( "models/weapons/w_models/w_stickybomb_d.mdl" ); + } + + pGrenade->InitGrenade( vVelocity, AngularImpulse( 600, random->RandomInt( -1200, 1200 ), 0 ), NULL, m_flDamage, m_flSplashRadius ); + vVelocity = pGrenade->GetAbsVelocity().Normalized() * GetSpeed(); + pGrenade->SetAbsVelocity( vVelocity ); + pGrenade->SetupInitialTransmittedGrenadeVelocity( vVelocity ); + + pGrenade->SetDamage( m_flDamage ); + pGrenade->SetFullDamage( m_flDamage ); + pGrenade->SetDamageRadius( m_flSplashRadius ); + pGrenade->SetCritical( m_bCrits ); + pGrenade->ChangeTeam( TF_TEAM_BLUE ); + pGrenade->m_nSkin = 1; + + m_Pipebombs.AddToTail( pGrenade ); + } +} + +QAngle CTFPointWeaponMimic::GetFiringAngles() const +{ + // No spread? Straight along our angles, then + QAngle angles = GetAbsAngles(); + if( m_flSpreadAngle == 0 ) + return angles; + + Vector vForward, vRight, vUp; + AngleVectors( angles, &vForward, &vRight, &vUp ); + + // Rotate around up by half the spread input, then rotate around the original forward by +-180 + float flHalfSpread = m_flSpreadAngle / 2.f; + VMatrix mtxRotateAroundUp = SetupMatrixAxisRot( vUp, RandomFloat( -flHalfSpread, flHalfSpread ) ); + VMatrix mtxRotateAroundForward = SetupMatrixAxisRot( vForward, RandomFloat( -180, 180 ) ); + + // Rotate forward + VMatrix mtxSpreadRot; + MatrixMultiply( mtxRotateAroundForward, mtxRotateAroundUp, mtxSpreadRot ); + vForward = mtxSpreadRot * vForward; + + // Back to angles + VectorAngles( vForward, vUp, angles ); + + return angles; + +} + +float CTFPointWeaponMimic::GetSpeed() const +{ + return RandomFloat( m_flSpeedMin, m_flSpeedMax ); +}
\ No newline at end of file diff --git a/game/server/tf/player_vs_environment/tf_population_manager.cpp b/game/server/tf/player_vs_environment/tf_population_manager.cpp new file mode 100644 index 0000000..e5c2cc1 --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_population_manager.cpp @@ -0,0 +1,2734 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_populator.cpp +// KeyValues driven procedural population system +// Michael Booth, April 2011 + +#include "cbase.h" + +#include "tf_population_manager.h" +#include "tf_team.h" +#include "tf_mann_vs_machine_stats.h" +#include "tf_shareddefs.h" +#include "filesystem.h" +#include "tf_obj_sentrygun.h" +#include "tf_objective_resource.h" +#include "econ_entity_creation.h" +#include "econ_wearable.h" +#include "tf_upgrades.h" +#include "tf_item_powerup_bottle.h" +#include "tf_gc_server.h" +#include "vote_controller.h" +#include "tf_gamestats.h" +#include "tf_gamerules.h" +#include "econ_item_schema.h" +#include "tf_upgrades_shared.h" + +#include "etwprof.h" + +extern ConVar tf_mvm_skill; +extern ConVar tf_mm_trusted; +extern ConVar tf_mvm_respec_limit; +extern ConVar tf_mvm_respec_credit_goal; +extern ConVar tf_mvm_buybacks_method; +extern ConVar tf_mvm_buybacks_per_wave; + +void MvMMissionCycleFileChangedCallback( IConVar *var, const char *pOldString, float flOldValue ) +{ + if ( g_pPopulationManager ) + { + g_pPopulationManager->LoadMissionCycleFile(); + } +} + +ConVar tf_mvm_missioncyclefile( "tf_mvm_missioncyclefile", "tf_mvm_missioncycle.res", FCVAR_NONE, "Name of the .res file used to cycle mvm misisons", MvMMissionCycleFileChangedCallback ); + +ConVar tf_populator_debug( "tf_populator_debug", "0", TF_MVM_FCVAR_CHEAT ); +ConVar tf_populator_active_buffer_range( "tf_populator_active_buffer_range", "3000", FCVAR_CHEAT, "Populate the world this far ahead of lead raider, and this far behind last raider" ); + +ConVar tf_mvm_default_sentry_buster_damage_dealt_threshold( "tf_mvm_default_sentry_buster_damage_dealt_threshold", "3000", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); +ConVar tf_mvm_default_sentry_buster_kill_threshold( "tf_mvm_default_sentry_buster_kill_threshold", "15", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); + +#ifdef STAGING_ONLY +ConVar tf_mvm_mm_bonus( "tf_mvm_mm_bonus", "0.2" ); +#endif // STAGING_ONLY + +void MinibossScaleChangedCallBack( IConVar *pVar, const char *pOldString, float flOldValue ) +{ + ConVarRef cVarRef( pVar ); + // Change the scale of all the minibosses + for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) + { + CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( iPlayerIndex ) ); + if ( pPlayer && pPlayer->IsMiniBoss() ) + { + pPlayer->SetModelScale( cVarRef.GetFloat(), 1.0f ); + } + } +} +ConVar tf_mvm_miniboss_scale( "tf_mvm_miniboss_scale", "1.75", FCVAR_REPLICATED | TF_MVM_FCVAR_CHEAT, "Full body scale for minibosses.", MinibossScaleChangedCallBack ); + +ConVar tf_mvm_disconnect_on_victory( "tf_mvm_disconnect_on_victory", "0", FCVAR_REPLICATED, "Enable to Disconnect Players after completing MvM" ); +ConVar tf_mvm_victory_reset_time( "tf_mvm_victory_reset_time", "60.0", FCVAR_REPLICATED, "Seconds to wait after MvM victory before cycling to the next mission. (Only used if tf_mvm_disconnect_on_victory is false.)" ); +ConVar tf_mvm_victory_disconnect_time( "tf_mvm_victory_disconnect_time", "180.0", FCVAR_REPLICATED, "Seconds to wait after MvM victory before kicking players. (Only used if tf_mvm_disconnect_on_victory is true.)" ); + +ConVar tf_mvm_endless_force_on( "tf_mvm_endless_force_on", "0", FCVAR_DONTRECORD | FCVAR_REPLICATED | FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Force MvM Endless mode on" ); +ConVar tf_mvm_endless_wait_time( "tf_mvm_endless_wait_time", "5.0f", FCVAR_DONTRECORD | FCVAR_REPLICATED | FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY); +ConVar tf_mvm_endless_bomb_reset( "tf_mvm_endless_bomb_reset", "5", FCVAR_DONTRECORD | FCVAR_REPLICATED | FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Number of Waves to Complete before bomb reset" ); +ConVar tf_mvm_endless_bot_cash( "tf_mvm_endless_bot_cash", "120", FCVAR_DONTRECORD | FCVAR_REPLICATED | FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "In Endless, number of credits bots get per wave" ); +ConVar tf_mvm_endless_tank_boost( "tf_mvm_endless_tank_boost", "0.2", FCVAR_DONTRECORD | FCVAR_REPLICATED | FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "In Endless, amount of extra health for the tank per wave" ); + + +ConVar tf_populator_health_multiplier( "tf_populator_health_multiplier", "1.0", FCVAR_DONTRECORD | FCVAR_REPLICATED | FCVAR_CHEAT ); +ConVar tf_populator_damage_multiplier( "tf_populator_damage_multiplier", "1.0", FCVAR_DONTRECORD | FCVAR_REPLICATED | FCVAR_CHEAT ); + +static bool HaveMap( const char *pszMapName ) +{ + char szCanonName[64] = { 0 }; + V_strncpy( szCanonName, pszMapName, sizeof( szCanonName ) ); + IVEngineServer::eFindMapResult eResult = engine->FindMap( szCanonName, sizeof( szCanonName ) ); + + switch ( eResult ) + { + case IVEngineServer::eFindMap_Found: + case IVEngineServer::eFindMap_NonCanonical: + return true; + case IVEngineServer::eFindMap_NotFound: + case IVEngineServer::eFindMap_FuzzyMatch: + // Maps that are contingent on just-in-time preparation should probably not be baked into cycle files... yet? + case IVEngineServer::eFindMap_PossiblyAvailable: + return false; + } + + AssertMsg( false, "Unhandled engine->FindMap return value\n" ); + return false; +} + +void MVMSkillChangedCallback( IConVar *pVar, const char *pOldString, float flOldValue ) +{ + ConVarRef cVarRef( pVar ); + // Testing the effects of skill setting + float flHealth = 1.f; // Health modifier for bots + float flDamage = 1.f; // Damage modifier in bot vs player + + switch ( cVarRef.GetInt() ) + { + case 1: + { + flHealth = 0.75f; + flDamage = 0.75f; + break; + } + case 2: + { + flHealth = 0.9f; + flDamage = 0.9f; + break; + } + // "Normal" + case 3: + { + flHealth = 1.f; + flDamage = 1.f; + break; + } + case 4: + { + flHealth = 1.10f; + flDamage = 1.10f; + break; + } + case 5: + { + flHealth = 1.25f; + flDamage = 1.25f; + break; + } + } + + tf_populator_health_multiplier.SetValue( flHealth ); + tf_populator_damage_multiplier.SetValue( flDamage ); +} + +ConVar tf_mvm_skill( "tf_mvm_skill", "3", FCVAR_DONTRECORD | FCVAR_REPLICATED | TF_MVM_FCVAR_CHEAT, "Sets the challenge level of the invading bot army. 1 = easiest, 3 = normal, 5 = hardest", true, 1, true, 5, MVMSkillChangedCallback ); + +//------------------------------------------------------------------------- +// Console command to cheat and force victory. (To test econ work, etc) +CON_COMMAND_F( tf_mvm_nextmission, "Load the next mission", FCVAR_CHEAT ) +{ + if ( g_pPopulationManager ) + { + g_pPopulationManager->CycleMission(); + } +} + +// Console command to cheat and force victory. (To test econ work, etc) +CON_COMMAND_F( tf_mvm_force_victory, "Force immediate victory.", FCVAR_CHEAT ) +{ + if ( g_pPopulationManager ) + { + g_pPopulationManager->JumpToWave( g_pPopulationManager->GetTotalWaveCount() - 1 ); + g_pPopulationManager->WaveEnd( true ); + g_pPopulationManager->MvMVictory(); + } +} + +//------------------------------------------------------------------------- +CON_COMMAND_F( tf_mvm_checkpoint, "Save a checkpoint snapshot", FCVAR_CHEAT ) +{ + if ( g_pPopulationManager ) + { + g_pPopulationManager->SetCheckpoint( -1 ); + } +} + +//------------------------------------------------------------------------- +CON_COMMAND_F( tf_mvm_checkpoint_clear, "Clear the saved checkpoint", FCVAR_CHEAT ) +{ + if ( g_pPopulationManager ) + { + g_pPopulationManager->ClearCheckpoint(); + } +} + +//------------------------------------------------------------------------- +CON_COMMAND_F( tf_mvm_jump_to_wave, "Jumps directly to the given Mann Vs Machine wave number", FCVAR_CHEAT ) +{ + if ( args.ArgC() <= 1 ) + { + Msg( "Missing wave number\n" ); + return; + } + + float fCleanMoneyPercent = -1.0f; + if ( args.ArgC() >= 3 ) + { + fCleanMoneyPercent = atof( args.Arg(2) ); + } + + // find the population manager + CPopulationManager *manager = (CPopulationManager *)gEntList.FindEntityByClassname( NULL, "info_populator" ); + if ( !manager ) + { + Msg( "No Population Manager found in the map\n" ); + return; + } + + uint32 desiredWave = (uint32)Max( atoi( args.Arg(1) ) - 1, 0) ; + manager->JumpToWave( desiredWave, fCleanMoneyPercent ); +} + +//------------------------------------------------------------------------- +CON_COMMAND_F( tf_mvm_debugstats, "Dumpout MvM Data", FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if ( g_pPopulationManager ) + { + g_pPopulationManager->DebugWaveStats(); + } +} + +//------------------------------------------------------------------------- +// CPopulationManager +//------------------------------------------------------------------------- + +BEGIN_DATADESC( CPopulationManager ) + DEFINE_THINKFUNC( Update ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( info_populator, CPopulationManager ); +PRECACHE_REGISTER( info_populator ); + +CPopulationManager *g_pPopulationManager = NULL; + +// initialized to zero (1st wave), and not reset unless game won or map change event received. +int CPopulationManager::m_checkpointWaveIndex = 0; +CUtlVector< CPopulationManager::CheckpointSnapshotInfo * > CPopulationManager::m_checkpointSnapshot; +int CPopulationManager::m_nNumConsecutiveWipes = 0; + +// Mission Cycle Vars +static int s_iLastKnownMissionCategory = 1; +static int s_iLastKnownMission = 1; + +//------------------------------------------------------------------------- +// CPopulationManager +//------------------------------------------------------------------------- +CPopulationManager::CPopulationManager( void ) +{ + m_bIsInitialized = false; + m_bAllocatedBots = false; + m_popfileFull[ 0 ] = '\0'; + m_popfileShort[ 0 ] = '\0'; + m_nStartingCurrency = 0; + m_nLobbyBonusCurrency = 0; + m_canBotsAttackWhileInSpawnRoom = true; + m_pTemplates = NULL; + m_isRestoringCheckpoint = false; + m_nRespawnWaveTime = 10; + m_bFixedRespawnWaveTime = false; + m_sentryBusterDamageDealtThreshold = tf_mvm_default_sentry_buster_damage_dealt_threshold.GetInt(); + m_sentryBusterKillThreshold = tf_mvm_default_sentry_buster_kill_threshold.GetInt(); + m_bCheckForCurrencyAchievement = true; + m_bEndlessOn = false; + m_bIsWaveJumping = false; + m_bSpawningPaused = false; + + m_iCurrentWaveIndex = 0; + m_nNumConsecutiveWipes = 0; + m_nMvMEventPopfileType = MVM_EVENT_POPFILE_NONE; + + SetThink( &CPopulationManager::Update ); + SetNextThink( gpGlobals->curtime ); + + g_pPopulationManager = this; + m_pMVMStats = MannVsMachineStats_GetInstance(); + + m_pKvpMvMMapCycle = NULL; + + ListenForGameEvent( "pve_win_panel" ); + + // Endless + m_randomizer.SetSeed( 0 ); + m_EndlessSeeds.Purge(); + for ( int i = 0; i < 27; i++ ) + { + m_EndlessSeeds.AddToTail( m_randomizer.RandomInt( 0, INT_MAX ) ); + } + EndlessParseBotUpgrades(); + m_bShouldResetFlag = false; + + m_bBonusRound = false; + m_hBonusBoss = NULL; + + m_nRespecsAwarded = 0; + m_nRespecsAwardedInWave = 0; + m_nCurrencyCollectedForRespec = 0; + m_PlayerRespecPoints.SetLessFunc( DefLessFunc (uint64) ); + m_PlayerRespecPoints.EnsureCapacity( MAX_PLAYERS ); + + m_PlayerBuybackPoints.SetLessFunc( DefLessFunc( uint64 ) ); + m_PlayerBuybackPoints.EnsureCapacity( MAX_PLAYERS ); +} + +//------------------------------------------------------------------------- +CPopulationManager::~CPopulationManager() +{ + Reset(); + + m_populatorVector.PurgeAndDeleteElements(); + m_waveVector.RemoveAll(); + + if ( m_pTemplates ) + { + m_pTemplates->deleteThis(); + m_pTemplates = NULL; + } + + g_pPopulationManager = NULL; +} + +//------------------------------------------------------------------------- +// Purpose : CPointEntity Override +//------------------------------------------------------------------------- +void CPopulationManager::Spawn( void ) +{ + BaseClass::Spawn(); + Initialize(); +} + +//------------------------------------------------------------------------- +// Purpose : CGameEventListener +//------------------------------------------------------------------------- +void CPopulationManager::FireGameEvent( IGameEvent *event ) +{ + const char *pEventName = event->GetName(); + + if ( V_strcmp( "pve_win_panel", pEventName ) == 0 ) + { + // Always release people even if the match isn't ending + // XXX(JohnS): This is just how the code was, but why wouldn't the match be ending? + MarkAllCurrentPlayersSafeToLeave(); + } +} + +//------------------------------------------------------------------------- +// Purpose : +//------------------------------------------------------------------------- +void CPopulationManager::PlayerDoneViewingLoot( const CTFPlayer* pPlayer ) +{ + CUtlVector< const CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_PVE_DEFENDERS ); + + if ( m_donePlayers.Find( pPlayer ) == m_donePlayers.InvalidIndex() + && playerVector.Find( pPlayer ) != playerVector.InvalidIndex() ) + { + m_donePlayers.AddToTail( pPlayer ); + + float flTimeRemaining = m_flMapRestartTime - gpGlobals->curtime; + const float flMinTime = 15.f; + + if ( flTimeRemaining > flMinTime ) + { + // Figure out if this is restart or kick to lobby time + float flReduceTimeBy = ( tf_mm_trusted.GetBool() == true || tf_mvm_disconnect_on_victory.GetBool() == true ) + ? tf_mvm_victory_disconnect_time.GetFloat() + : tf_mvm_victory_reset_time.GetFloat(); + + // Each player can reduce the clock by a certain amount, based on how + // many players there are + flReduceTimeBy = ( flReduceTimeBy * 0.8 ) / playerVector.Count(); + flTimeRemaining -= flReduceTimeBy ; + flTimeRemaining = Max( flTimeRemaining, flMinTime ); + + m_flMapRestartTime = gpGlobals->curtime + flTimeRemaining; + + // Notify Users of new remaining time + CBroadcastRecipientFilter filter; + filter.MakeReliable(); + UserMessageBegin( filter, "MVMServerKickTimeUpdate" ); + WRITE_BYTE((uint8)flTimeRemaining); + MessageEnd(); + } + } +} + +//------------------------------------------------------------------------- +// Purpose : Full Clear of Population Manager State and Data +//------------------------------------------------------------------------- +void CPopulationManager::Reset( void ) +{ + m_nStartingCurrency = 0; + m_canBotsAttackWhileInSpawnRoom = true; + m_nRespawnWaveTime = 10; + m_bFixedRespawnWaveTime = false; + m_sentryBusterDamageDealtThreshold = tf_mvm_default_sentry_buster_damage_dealt_threshold.GetInt(); + m_sentryBusterKillThreshold = tf_mvm_default_sentry_buster_kill_threshold.GetInt(); + m_bAdvancedPopFile = false; + m_nMvMEventPopfileType = MVM_EVENT_POPFILE_NONE; + m_bSpawningPaused = false; + m_donePlayers.Purge(); + m_nRespecsAwardedInWave = 0; + + // don't clobber this value if we're wave jumping + if ( !m_bIsWaveJumping ) + { + m_iCurrentWaveIndex = 0; + } + + m_defaultEventChangeAttributesName = "Default"; +} + +//------------------------------------------------------------------------- +// Purpose : Restart Population Manager at Current Wave +//------------------------------------------------------------------------- +bool CPopulationManager::Initialize( void ) +{ + if ( ( TheNavMesh == NULL ) || ( TheNavMesh->GetNavAreaCount() <= 0 ) ) + { + Warning( "No Nav Mesh CPopulationManager::Initialize for %s", m_popfileFull ); + return false; + } + + Reset(); + + if ( !Parse() ) + { + Warning( "Parse Failed in CPopulationManager::Initialize for %s", m_popfileFull ); + return false; + } + +#ifdef STAGING_ONLY + // only calculate lobby bonus one time when the lobby first match to the server + CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch(); + if ( pMatch && GetWaveNumber() == 0 && m_nLobbyBonusCurrency == 0 ) + { + // Count unique parties + CUtlVector<uint64> vecPartyIDs; + int total = pMatch->GetNumTotalMatchPlayers(); + + for ( int idx = 0; idx < total; idx++ ) + { + uint64 uPartyID = pMatch->GetMatchDataForPlayer( idx )->uPartyID; + if ( vecPartyIDs.Find( uPartyID ) == vecPartyIDs.InvalidIndex() ) + { + vecPartyIDs.AddToTail( uPartyID ); + } + } + + int nUniqueParties = vecPartyIDs.Count(); + + // give some bonus currency for each extra unique party in the lobby + float flBonusScale = tf_mvm_mm_bonus.GetFloat() * ( nUniqueParties - 1 ); + m_nLobbyBonusCurrency = flBonusScale * m_nStartingCurrency; + } +#endif // STAGING_ONLY + + if ( TFGameRules()->State_Get() == GR_STATE_PREGAME ) + { + // new game + ClearCheckpoint(); + m_iCurrentWaveIndex = 0; + m_nNumConsecutiveWipes = 0; + m_pMVMStats->SetCurrentWave( m_iCurrentWaveIndex ); + } + else + { + RestoreCheckpoint(); + + // Restore Check Point is being called on RoundStart so this check is currently needed + // Report loss to Stats + if ( m_bIsInitialized && ( m_iCurrentWaveIndex > 0 || m_nNumConsecutiveWipes > 1 ) ) + { + m_pMVMStats->RoundEvent_WaveEnd( false ); + } + + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_wave_failed" ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } + } + + if ( IsInEndlessWaves() ) + { + EndlessRollEscalation(); + } + + m_bIsInitialized = true; + UpdateObjectiveResource(); + DebugWaveStats(); + PostInitialize(); + + // Space respecs based on the number allowed + if ( tf_mvm_respec_limit.GetBool() ) + { + int nAmount = tf_mvm_respec_limit.GetInt() + 1; + tf_mvm_respec_credit_goal.SetValue( GetTotalPopFileCurrency() / nAmount ); + } + + m_nRespecsAwardedInWave = 0; + + return true; +} + +//------------------------------------------------------------------------- +// Purpose : Precache data for PopulationManager, typically sounds +//------------------------------------------------------------------------- +void CPopulationManager::Precache( void ) +{ + PrecacheScriptSound( "music.mvm_end_wave" ); + PrecacheScriptSound( "music.mvm_end_tank_wave" ); + PrecacheScriptSound( "music.mvm_end_mid_wave" ); + PrecacheScriptSound( "music.mvm_end_last_wave" ); + PrecacheScriptSound( "MVM.PlayerUpgraded" ); + PrecacheScriptSound( "MVM.PlayerBoughtIn" ); + PrecacheScriptSound( "MVM.PlayerUsedPowerup" ); + PrecacheScriptSound( "MVM.PlayerDied" ); + PrecacheScriptSound( "MVM.PlayerDiedScout" ); + PrecacheScriptSound( "MVM.PlayerDiedSniper" ); + PrecacheScriptSound( "MVM.PlayerDiedSoldier" ); + PrecacheScriptSound( "MVM.PlayerDiedDemoman" ); + PrecacheScriptSound( "MVM.PlayerDiedMedic" ); + PrecacheScriptSound( "MVM.PlayerDiedHeavy" ); + PrecacheScriptSound( "MVM.PlayerDiedPyro" ); + PrecacheScriptSound( "MVM.PlayerDiedSpy" ); + PrecacheScriptSound( "MVM.PlayerDiedEngineer" ); + + BaseClass::Precache(); +} + +//------------------------------------------------------------------------- +bool CPopulationManager::FindPopulationFileByShortName( const char *pShortName, CUtlString &outFullName ) +{ + // Form full path + char szFullPath[MAX_PATH] = { 0 }; + V_sprintf_safe( szFullPath, MVM_POP_FILE_PATH "/%s.pop", pShortName ); + + if ( g_pFullFileSystem->FileExists( szFullPath, "GAME" ) ) + { + outFullName = szFullPath; + return true; + } + + // Check mapname_shorthand.pop + V_sprintf_safe( szFullPath, MVM_POP_FILE_PATH "/%s_%s.pop", STRING( gpGlobals->mapname ), pShortName ); + if ( g_pFullFileSystem->FileExists( szFullPath, "GAME" ) ) + { + outFullName = szFullPath; + return true; + } + + // If using special name "normal", check just scripts/population/mapname.pop as last resort + V_sprintf_safe( szFullPath, MVM_POP_FILE_PATH "/%s.pop", STRING( gpGlobals->mapname ) ); + if ( g_pFullFileSystem->FileExists( szFullPath, "GAME" ) ) + { + outFullName = szFullPath; + return true; + } + + return false; +} + +//------------------------------------------------------------------------- +void CPopulationManager::FindDefaultPopulationFileShortNames( CUtlVector< CUtlString > &outVecShortNames ) +{ + // Search for all loose pop files that are prefixed with the current map name + char szBaseName[MAX_PATH] = { 0 }; + V_snprintf( szBaseName, sizeof( szBaseName ), MVM_POP_FILE_PATH "/%s*.pop", STRING(gpGlobals->mapname) ); + + FileFindHandle_t popHandle; + const char *pPopFileName = filesystem->FindFirstEx( szBaseName, "GAME", &popHandle ); + + while ( pPopFileName && pPopFileName[ 0 ] != '\0' ) + { + // Skip it if it's a directory or is the folder info + if ( filesystem->FindIsDirectory( popHandle ) ) + { + pPopFileName = filesystem->FindNext( popHandle ); + continue; + } + + const char *pchPopPostfix = StringAfterPrefix( pPopFileName, STRING(gpGlobals->mapname) ); + if ( pchPopPostfix ) + { + char szShortName[MAX_PATH] = { 0 }; + V_strncpy( szShortName, ( ( pchPopPostfix[ 0 ] == '_' ) ? ( pchPopPostfix + 1 ) : "normal" ), sizeof( szShortName ) ); // skip the '_' + V_StripExtension( szShortName, szShortName, sizeof( szShortName ) ); + + if ( outVecShortNames.Find( szShortName ) == outVecShortNames.InvalidIndex() ) + { + outVecShortNames.AddToTail( szShortName ); + } + } + + pPopFileName = filesystem->FindNext( popHandle ); + } + + filesystem->FindClose( popHandle ); + + // Search for all pop files in the BSP next. Note that loose files override these (by short name) + FileFindHandle_t popHandleBSP; + const char *pPopFileNameBSP = filesystem->FindFirstEx( MVM_POP_FILE_PATH "/*.pop", "BSP", &popHandleBSP ); + + while ( pPopFileNameBSP && pPopFileNameBSP[ 0 ] != '\0' ) + { + // Skip it if it's a directory or is the folder info + if ( filesystem->FindIsDirectory( popHandleBSP ) ) + { + pPopFileNameBSP = filesystem->FindNext( popHandleBSP ); + continue; + } + + char szShortName[MAX_PATH] = { 0 }; + V_strncpy( szShortName, pPopFileNameBSP, sizeof( szShortName ) ); + V_StripExtension( szShortName, szShortName, sizeof( szShortName ) ); + + // Legacy: Prior to proper support for in-BSP pop files, maps could jankily match their popfile to their exact + // map name in the BSP. Map this to "normal" + if ( V_stricmp( szShortName, STRING(gpGlobals->mapname) ) == 0 ) + { + V_strncpy( szShortName, "normal", sizeof( szShortName ) ); + } + + if ( outVecShortNames.Find( szShortName ) == outVecShortNames.InvalidIndex() ) + { + outVecShortNames.AddToTail( szShortName ); + } + + pPopFileNameBSP = filesystem->FindNext( popHandleBSP ); + } + + filesystem->FindClose( popHandleBSP ); + + // Always treat "normal" as the default pop-file + int normalIdx = outVecShortNames.Find( "normal" ); + if ( normalIdx != outVecShortNames.InvalidIndex() && normalIdx != 0 ) + { + outVecShortNames.Remove( normalIdx ); + outVecShortNames.AddToHead( "normal" ); + } +} + +//------------------------------------------------------------------------- +const char *CPopulationManager::GetPopulationFilename( void ) +{ + return m_popfileFull; +} + +//------------------------------------------------------------------------- +const char *CPopulationManager::GetPopulationFilenameShort( void ) +{ + return m_popfileShort; +} + +//------------------------------------------------------------------------- +void CPopulationManager::SetPopulationFilename( const char *populationFile ) +{ + m_bIsInitialized = false; + V_strcpy_safe( m_popfileFull, populationFile ); + V_FileBase( m_popfileFull, m_popfileShort, sizeof( m_popfileShort ) ); + + MannVsMachineStats_SetPopulationFile( m_popfileFull ); + ResetMap(); + + if ( TFObjectiveResource() ) + { + TFObjectiveResource()->SetMannVsMachineChallengeIndex( GetItemSchema()->FindMvmMissionByName( m_popfileFull ) ); + TFObjectiveResource()->SetMvMPopfileName( MAKE_STRING( m_popfileFull ) ); + } +} + +//------------------------------------------------------------------------- +// Invoked when we are spawned at round (re)start +void CPopulationManager::SetupOnRoundStart( void ) +{ + Initialize(); +} + +//------------------------------------------------------------------------- +// Continuously invoked to modify population over time +//------------------------------------------------------------------------- +void CPopulationManager::Update( void ) +{ + VPROF_BUDGET( "CMissionPopulator::Update", "NextBot" ); + + SetNextThink( gpGlobals->curtime ); + + m_isRestoringCheckpoint = false; + + // update populators + for( int i=0; i<m_populatorVector.Count(); ++i ) + { + m_populatorVector[i]->Update(); + } + + // Update Current Wave + CWave * pWave = GetCurrentWave(); + if ( pWave ) + { + pWave->Update(); + } + + // Check for GAMEOVER for MapReset + if ( TFGameRules()->State_Get() == GR_STATE_GAME_OVER ) + { + if ( m_flMapRestartTime < gpGlobals->curtime ) + { + if ( tf_mvm_disconnect_on_victory.GetBool() ) + { + // Shut down the managed match now, ask GC to return players to lobbies. + if ( !TFGameRules()->IsManagedMatchEnded() ) + { + TFGameRules()->EndManagedMvMMatch( /* bKickPlayersToParties */ true ); + } + } + else + { + CycleMission(); + } + } + + // If players haven't left via the GC returning them to parties by now, due to connection issues/GC down, etc, + // kick them with a thanks-for-playing. + if ( tf_mvm_disconnect_on_victory.GetBool() == true && m_flMapRestartTime + 5.0f < gpGlobals->curtime ) + { + Log( "Kicking all players\n" ); + engine->ServerCommand( "kickall #TF_PVE_Disconnect\n" ); + CycleMission(); + } + + // See if the team got the bonus on every wave + if ( m_bCheckForCurrencyAchievement ) + { + if ( ( MannVsMachineStats_GetDroppedCredits() > 0 ) && ( MannVsMachineStats_GetMissedCredits() == 0 ) ) + { + const char *pszName = IsAdvancedPopFile() ? "mvm_creditbonus_all_advanced" : "mvm_creditbonus_all"; + IGameEvent *event = gameeventmanager->CreateEvent( pszName ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } + + m_bCheckForCurrencyAchievement = false; + } + } + } + else if ( TFGameRules()->State_Get() == GR_STATE_STARTGAME ) + { + AllocateBots(); + } +} + +//------------------------------------------------------------------------- +// Purpose: Invoked by the gamerules think to give us a chance to behave +// like a sub-gamerules-thing. This will always run when +// gamerules thinks, unlike Update which runs in the entity-update +// phase and doesn't run when we're hibernating etc.. +//------------------------------------------------------------------------- +void CPopulationManager::GameRulesThink( void ) +{ + // If we reach zero players in managed match mode, drop the match (but otherwise just hang out in our current state, + // in bootcamp servers ad-hoc players may want to rejoin and keep playing/etc.., server hibernation will handle + // shutting down the game if desired) + CMatchInfo *pLiveMatch = GTFGCClientSystem()->GetLiveMatch(); + if ( pLiveMatch && !TFGameRules()->IsManagedMatchEnded() && pLiveMatch->GetNumActiveMatchPlayers() == 0 ) + { + Log( "No players remaining, ending managed MvM\n" ); + TFGameRules()->EndManagedMvMMatch( /* bSendVictory */ false ); + } +} + +//------------------------------------------------------------------------- +// Purpose : +//------------------------------------------------------------------------- +void CPopulationManager::UpdateObjectiveResource( void ) +{ + if ( m_waveVector.Count() == 0 || !TFObjectiveResource() ) + { + return; + } + + TFObjectiveResource()->SetMannVsMachineEventPopfileType( m_nMvMEventPopfileType ); + + if ( IsInEndlessWaves() ) + { + TFObjectiveResource()->SetMannVsMachineMaxWaveCount( 0 ); + } + else + { + TFObjectiveResource()->SetMannVsMachineMaxWaveCount( m_waveVector.Count() ); + } + + TFObjectiveResource()->SetMannVsMachineWaveCount( m_iCurrentWaveIndex + 1 ); + + const CWave *wave = GetCurrentWave(); + if ( wave ) + { + TFObjectiveResource()->SetMannVsMachineWaveEnemyCount( wave->GetEnemyCount() ); + TFObjectiveResource()->ClearMannVsMachineWaveClassFlags(); + + int i = 0; + bool bHasEngineer = false; + for ( i; i < MVM_CLASS_TYPES_PER_WAVE_MAX_NEW && i < wave->GetNumClassTypes(); ++i ) + { + if ( !bHasEngineer ) + { + const char* pszClassIconName = wave->GetClassIconName( i ).ToCStr(); + bHasEngineer |= FStrEq( pszClassIconName, "engineer" ); + } + TFObjectiveResource()->SetMannVsMachineWaveClassName( i, wave->GetClassIconName( i ) ); + TFObjectiveResource()->SetMannVsMachineWaveClassCount( i, wave->GetClassCount( i ) ); + TFObjectiveResource()->AddMannVsMachineWaveClassFlags( i, wave->GetClassFlags( i ) ); + } + + if ( bHasEngineer ) + { + if ( i < MVM_CLASS_TYPES_PER_WAVE_MAX_NEW ) + { + TFObjectiveResource()->SetMannVsMachineWaveClassName( i, TFObjectiveResource()->GetTeleporterString() ); + TFObjectiveResource()->SetMannVsMachineWaveClassCount( i, 0 ); + TFObjectiveResource()->AddMannVsMachineWaveClassFlags( i, MVM_CLASS_FLAG_MISSION ); // only mission will flash + i++; + } + else + { + AssertMsg( 0, "Failed to add teleporter icon to TFObjectiveResource" ); + } + } + + for ( i; i < MVM_CLASS_TYPES_PER_WAVE_MAX_NEW; ++i ) + { + TFObjectiveResource()->SetMannVsMachineWaveClassCount( i, 0 ); + TFObjectiveResource()->SetMannVsMachineWaveClassName( i, NULL_STRING ); + TFObjectiveResource()->AddMannVsMachineWaveClassFlags( i, MVM_CLASS_FLAG_NONE ); + } + } +} + +//------------------------------------------------------------------------- +// Purpose : Reset Players, Stats, CheckPoint +//------------------------------------------------------------------------- +void CPopulationManager::ResetMap( void ) +{ + // Reset Scores + for( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = ToBasePlayer( UTIL_PlayerByIndex( i ) ); + + if ( !pPlayer ) + continue; + + if ( FNullEnt( pPlayer->edict() ) ) + continue; + + if ( pPlayer->GetTeamNumber() != TF_TEAM_PVE_DEFENDERS ) + continue; + + pPlayer->ResetScores(); + } + + // Reset Stats and go to wave 0 clean + m_pMVMStats->ResetStats( ); + ResetRespecPoints(); + ClearCheckpoint(); + + for ( int i = 1; i <= MAX_PLAYERS; ++i ) + { + CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) ); + if ( !pTFPlayer ) + continue; + + if ( pTFPlayer->IsBot() ) + continue; + + pTFPlayer->ResetRefundableUpgrades(); + } + + JumpToWave( 0, 0 ); +} + +//------------------------------------------------------------------------- +void CPopulationManager::CycleMission ( void ) +{ + bool isLoaded = true; + if ( !m_pKvpMvMMapCycle ) + { + isLoaded = LoadMissionCycleFile(); + } + + const char * pCurrentMap = STRING( gpGlobals->mapname ); + char szCurrentPopfile[MAX_PATH]; + V_FileBase( m_popfileFull, szCurrentPopfile, sizeof( szCurrentPopfile ) ); + + //engine->GetMap + if ( isLoaded ) + { + int iMaxCat = m_pKvpMvMMapCycle->GetInt( "categories", 0 ); + + for ( int iCat = 1; iCat <= iMaxCat; iCat++ ) + { + KeyValues *pCategory = m_pKvpMvMMapCycle->FindKey( UTIL_VarArgs( "%d", iCat ), false ); + + if ( pCategory ) + { + int iMapCount = pCategory->GetInt( "count", 0 ); + for ( int iMap = 1; iMap <= iMapCount; ++iMap ) + { + KeyValues *pMission = pCategory->FindKey( UTIL_VarArgs( "%d", iMap ), false ); + if ( pMission ) + { + const char * pMap = pMission->GetString( "map", "" ); + const char * pPopfile = pMission->GetString( "popfile", "" ); + + if ( !Q_strcmp( pCurrentMap, pMap ) && !Q_strcmp( szCurrentPopfile, pPopfile ) ) + { + // match, advance to the next entry and use those values + int nextMap = (iMap % iMapCount) + 1; + + KeyValues *pNextMission = pCategory->FindKey( UTIL_VarArgs( "%d", nextMap ), false ); + if ( LoadMvMMission( pNextMission ) ) + { + s_iLastKnownMission = nextMap; + s_iLastKnownMissionCategory = iCat; + return; + } + // Next map is invalid, load last known + LoadLastKnownMission(); + return; + } + } + } // for ( int iMap = 1; iMap <= iMapCount; ++iMap ) + } + } // for ( int iCat = 1; iCat <= iMaxCat; iCat++ ) + } + + // Unable to load mvm mapcycle, load last known + LoadLastKnownMission(); +} + +//------------------------------------------------------------------------- +bool CPopulationManager::LoadMissionCycleFile( void ) +{ + if ( m_pKvpMvMMapCycle ) + { + m_pKvpMvMMapCycle->deleteThis(); + } + + m_pKvpMvMMapCycle = new KeyValues( tf_mvm_missioncyclefile.GetString() ); + + return m_pKvpMvMMapCycle->LoadFromFile( g_pFullFileSystem, tf_mvm_missioncyclefile.GetString(), "MOD" ); +} + +//------------------------------------------------------------------------- +void CPopulationManager::LoadLastKnownMission( void ) +{ + // + // + bool isLoaded = true; + if ( !m_pKvpMvMMapCycle ) + { + isLoaded = LoadMissionCycleFile(); + } + + if ( !isLoaded ) + { + ResetMap(); + return; + } + + // Grab the category + KeyValues *pCategory = m_pKvpMvMMapCycle->FindKey( UTIL_VarArgs( "%d", s_iLastKnownMissionCategory ), false ); + if ( pCategory ) + { + KeyValues *pNextMission = pCategory->FindKey( UTIL_VarArgs( "%d", s_iLastKnownMission ), false ); + if ( LoadMvMMission( pNextMission ) ) + { + return; + } + } + + // Did not succeed + // Attempt to load the first Category / Mission instead + pCategory = m_pKvpMvMMapCycle->FindKey( UTIL_VarArgs( "%d", 1 ), false ); + if ( pCategory ) + { + KeyValues *pNextMission = pCategory->FindKey( UTIL_VarArgs( "%d", 1 ), false ); + if ( LoadMvMMission( pNextMission ) ) + { + s_iLastKnownMissionCategory = 1; + s_iLastKnownMission = 1; + return; + } + } + + // if 1,1 does not exist (likely due to a bad .res file) just reset map instead + ResetMap(); +} + +//------------------------------------------------------------------------- +// Returns True if the mission was successfully found and loaded +// +bool CPopulationManager::LoadMvMMission ( KeyValues *pNextMission ) +{ + if ( !pNextMission ) + return false; + + const char * pNextMap = pNextMission->GetString( "map", NULL ); + const char * pNextPopfile = pNextMission->GetString( "popfile", NULL ); + + if ( pNextMap && pNextPopfile ) + { + char szPopFileName[MAX_PATH]; + Q_snprintf( szPopFileName, sizeof( szPopFileName ), MVM_POP_FILE_PATH "/%s.pop", pNextPopfile ); + if ( g_pFullFileSystem->FileExists( szPopFileName, "MOD" ) && HaveMap( pNextMap ) ) + { + engine->ChangeLevel( pNextMap, NULL ); + TFGameRules()->SetNextMvMPopfile( pNextPopfile ); + return true; + } + } + return false; +} + +//------------------------------------------------------------------------- +bool CPopulationManager::IsValidMvMMap( const char *pszMapName ) +{ + if ( !m_pKvpMvMMapCycle ) + { + LoadMissionCycleFile(); + } + + if ( pszMapName && m_pKvpMvMMapCycle ) + { + int iMaxCat = m_pKvpMvMMapCycle->GetInt( "categories", 0 ); + for ( int iCat = 1; iCat <= iMaxCat; iCat++ ) + { + KeyValues *pCategory = m_pKvpMvMMapCycle->FindKey( UTIL_VarArgs( "%d", iCat ), false ); + if ( pCategory ) + { + int iMapCount = pCategory->GetInt( "count", 0 ); + for ( int iMap = 1; iMap <= iMapCount; ++iMap ) + { + KeyValues *pMission = pCategory->FindKey( UTIL_VarArgs( "%d", iMap ), false ); + if ( pMission ) + { + const char *pszMap = pMission->GetString( "map", "" ); + if ( Q_strcmp( pszMapName, pszMap ) == 0 ) + { + // Valid? + return ( HaveMap( pszMapName ) ? true : false ); + } + } + } + } + } + } + + return false; +} +//------------------------------------------------------------------------- +void CPopulationManager::ShowNextWaveDescription( void ) +{ + UpdateObjectiveResource(); +} + +//------------------------------------------------------------------------- +#ifdef STAGING_ONLY +ConVar tf_mvm_bonus( "tf_mvm_bonus", "0" ); +#endif +void CPopulationManager::StartCurrentWave( void ) +{ + if ( TFObjectiveResource() ) + { + TFObjectiveResource()->SetMannVsMachineNextWaveTime( 0 ); + TFObjectiveResource()->SetMannVsMachineBetweenWaves( false ); + } + + UpdateObjectiveResource(); + m_pMVMStats->RoundEvent_WaveStart(); + + TFGameRules()->State_Transition( GR_STATE_RND_RUNNING ); + +#ifdef STAGING_ONLY + m_bBonusRound = tf_mvm_bonus.GetBool(); + if ( m_bBonusRound ) + { + Assert( m_hBonusBoss == NULL ); + m_hBonusBoss = dynamic_cast< CBaseCombatCharacter * >( CreateEntityByName( "eyeball_boss" ) ); + if ( m_hBonusBoss ) + { + bool bFoundSpawnPoint = false; + CBaseEntity *spawnPoint = NULL; + while( ( spawnPoint = gEntList.FindEntityByClassname( spawnPoint, "info_target" ) ) != NULL ) + { + if ( FStrEq( STRING( spawnPoint->GetEntityName() ), "spawn_boss_startpoint" ) ) + { + bFoundSpawnPoint = true; + break; + } + } + + if ( bFoundSpawnPoint ) + { + m_hBonusBoss->SetAbsOrigin( spawnPoint->GetAbsOrigin() ); + DispatchSpawn( m_hBonusBoss ); + } + else + { + AssertMsg( 0, "CPopulationManager::StartCurrentWave trying to spawn a bonus boss, but cannot find spawn_boss_startpoint info_target in the map" ); + UTIL_Remove( m_hBonusBoss ); + m_hBonusBoss = NULL; + m_bBonusRound = false; + } + } + } +#endif + + m_nRespecsAwardedInWave = 0; + + FOR_EACH_MAP( m_PlayerBuybackPoints, i ) + { + m_PlayerBuybackPoints[i] = tf_mvm_buybacks_per_wave.GetInt(); + } +} + +//------------------------------------------------------------------------- +CWave * CPopulationManager::GetCurrentWave( void ) +{ + if ( !m_bIsInitialized || m_waveVector.Count() == 0 ) + return NULL; + + // Wrap for Infinite MVM + if ( IsInEndlessWaves() ) + { + return m_waveVector[m_iCurrentWaveIndex % m_waveVector.Count() ]; + } + else if ( (int)m_iCurrentWaveIndex < m_waveVector.Count() ) + { + return m_waveVector[m_iCurrentWaveIndex]; + } + + return NULL; +} + +//------------------------------------------------------------------------- +void CPopulationManager::JumpToWave( uint32 waveNumber, float fCleanMoneyPercent /*= -1.0f*/ ) +{ + if ( !IsInEndlessWaves() && (waveNumber >= (uint32)m_waveVector.Count() ) ) + { + if ( m_waveVector.Count() > 0 ) + { + Warning( "Invalid wave number\n" ); + } + return; + } + + CWave * pWave = GetCurrentWave(); + if ( pWave ) + { + pWave->ForceFinish(); + } + m_bIsWaveJumping = true; + + m_iCurrentWaveIndex = waveNumber; + + // Set Money for New Wave + if ( fCleanMoneyPercent != -1.0f ) + { + ClearCheckpoint(); + + Initialize(); + m_pMVMStats->ResetStats( ); + + for ( m_iCurrentWaveIndex = 0; m_iCurrentWaveIndex < waveNumber; ++m_iCurrentWaveIndex ) + { + pWave = GetCurrentWave(); + if ( pWave ) + { + int nCurrency = pWave->GetTotalCurrency(); + m_pMVMStats->SetCurrentWave( m_iCurrentWaveIndex ); + + if ( m_iCurrentWaveIndex < waveNumber ) + { + m_pMVMStats->RoundEvent_CreditsDropped( m_iCurrentWaveIndex, nCurrency ); + m_pMVMStats->RoundEvent_AcquiredCredits( m_iCurrentWaveIndex, nCurrency * fCleanMoneyPercent, false ); + } + } + } + } + + // Reset the new wave + m_iCurrentWaveIndex = waveNumber; + pWave = GetCurrentWave(); + if ( pWave ) + { + pWave->ForceReset(); + } + m_pMVMStats->SetCurrentWave( m_iCurrentWaveIndex ); + if ( IsInEndlessWaves() ) + { + EndlessRollEscalation(); + } + UpdateObjectiveResource(); + + SetCheckpoint( -1 ); + TFGameRules()->SetAllowBetweenRounds( true ); + TFGameRules()->State_Transition( GR_STATE_PREROUND ); + TFGameRules()->PlayerReadyStatus_ResetState(); + TFObjectiveResource()->SetMannVsMachineBetweenWaves( true ); + RestorePlayerCurrency(); + m_bIsWaveJumping = false; + + CTF_GameStats.ResetRoundStats(); + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_reset_stats" ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } + + ResetRespecPoints(); +} + +//------------------------------------------------------------------------- +// Report that a wave has been completed +void CPopulationManager::WaveEnd( bool bSuccess ) +{ + m_pMVMStats->RoundEvent_WaveEnd( bSuccess ); + + // Save off round stats before we reset them + IGameEvent *event = gameeventmanager->CreateEvent( "scorestats_accumulated_update" ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } + + // Treat completed waves as rounds for the purposes of TF stats + CTF_GameStats.ResetRoundStats(); + + // Completing any wave removes everyone's obligation to stay in MvM matches. Because that's how it was when I got + // here. + MarkAllCurrentPlayersSafeToLeave(); + + if ( bSuccess ) + { + if ( m_bBonusRound ) + { + if ( m_hBonusBoss ) + { + UTIL_Remove( m_hBonusBoss ); + m_hBonusBoss = NULL; + } + m_bBonusRound = false; + } + else + { + m_iCurrentWaveIndex++; + } + m_pMVMStats->SetCurrentWave( m_iCurrentWaveIndex ); + + // get Current Wave + CWave *nextWave = GetCurrentWave(); + if ( nextWave ) + { + // we've reached a checkpoint + SetCheckpoint( -1 ); + + // display the upcoming wave's description + ShowNextWaveDescription(); + nextWave->StartUpgradesAlertTimer( 3.0f ); + + if ( IsInEndlessWaves() ) + { + EndlessRollEscalation(); + nextWave->ForceReset(); + + // (Double time between waves on reset waves) + float flTime = gpGlobals->curtime + tf_mvm_endless_wait_time.GetFloat(); + if ( m_iCurrentWaveIndex % tf_mvm_endless_bomb_reset.GetInt() == 0 ) + { + flTime += tf_mvm_endless_wait_time.GetFloat(); + m_bShouldResetFlag = true; + } + + nextWave->SetStartTime( flTime ); + } + } + + if ( (int)m_iCurrentWaveIndex >= m_waveVector.Count() && !IsInEndlessWaves() ) + { + // Restart the Map after a time delay + if ( tf_mm_trusted.GetBool() == true || tf_mvm_disconnect_on_victory.GetBool() == true ) + { + m_flMapRestartTime = gpGlobals->curtime + tf_mvm_victory_disconnect_time.GetFloat(); + } + else + { + m_flMapRestartTime = gpGlobals->curtime + tf_mvm_victory_reset_time.GetFloat(); + } + + TFObjectiveResource()->SetMannVsMachineBetweenWaves( true ); + TFGameRules()->State_Transition( GR_STATE_GAME_OVER ); + return; + } + } + + if ( !IsInEndlessWaves() ) + { + TFGameRules()->State_Transition( GR_STATE_BETWEEN_RNDS ); + TFObjectiveResource()->SetMannVsMachineBetweenWaves( true ); + } +} + +//------------------------------------------------------------------------- +// Save the current wave as a checkpoint. +// When the scenario restarts from a loss, it will restart at the checkpoint. +void CPopulationManager::SetCheckpoint( int waveNumber ) +{ + // No checkpoints for Endless + if ( IsInEndlessWaves() ) + return; + + // Auto SetCheckPoint from ConsoleCommand + if ( waveNumber < 0 ) + { + waveNumber = m_iCurrentWaveIndex; + } + + if ( waveNumber < 0 || waveNumber >= m_waveVector.Count() ) + { + Warning( "Warning: SetCheckpoint() called with invalid wave number %d\n", waveNumber ); + return; + } + + m_nNumConsecutiveWipes = 0; + + m_checkpointWaveIndex = waveNumber; + + DevMsg( "Checkpoint Saved\n" ); + + // snapshot each player's state + // Save off all upgrades and purge it, copy back in existing players + for( int i=0; i<m_playerUpgrades.Count(); ++i ) + { + // Get this players check point + CheckpointSnapshotInfo *snapshot = FindCheckpointSnapshot( m_playerUpgrades[i]->m_steamId ); + if ( snapshot == NULL ) + { + // New SnapshotInfo, save the player id + snapshot = new CheckpointSnapshotInfo; + snapshot->m_steamId = m_playerUpgrades[i]->m_steamId; + m_checkpointSnapshot.AddToTail( snapshot ); + } + + // Save the Player upgrade history + snapshot->m_currencySpent = m_playerUpgrades[i]->m_currencySpent; + snapshot->m_upgradeVector.RemoveAll(); + // copy in to upgrade history + for( int j = 0; j < m_playerUpgrades[i]->m_upgradeVector.Count(); ++j ) + { + snapshot->m_upgradeVector.AddToTail( m_playerUpgrades[i]->m_upgradeVector[j]); + } + } +} + +//------------------------------------------------------------------------- +void CPopulationManager::RestoreItemToCheckpointState( CTFPlayer *player, CEconItemView *item ) +{ + CheckpointSnapshotInfo *snapshot = FindCheckpointSnapshot( player ); + + if ( !snapshot ) + return; + + if ( !player->Inventory() ) + return; + + if ( !item || !item->IsValid() ) + return; + + player->BeginPurchasableUpgrades(); + + // restore the item's upgrade(s) + for( int u=0; u<snapshot->m_upgradeVector.Count(); ++u ) + { + if ( item->GetItemDefIndex() == snapshot->m_upgradeVector[u].m_itemDefIndex ) + { + if ( player->GetPlayerClass()->GetClassIndex() == snapshot->m_upgradeVector[u].m_iPlayerClass ) + { + if ( g_hUpgradeEntity->ApplyUpgradeToItem( player, item, snapshot->m_upgradeVector[u].m_upgrade, snapshot->m_upgradeVector[u].m_nCost ) ) + { + if ( tf_populator_debug.GetBool() ) + { + const char *upgradeName = g_hUpgradeEntity->GetUpgradeAttributeName( snapshot->m_upgradeVector[u].m_upgrade ); + DevMsg( "%3.2f: CHECKPOINT_RESTORE_ITEM: Player '%s', item '%s', upgrade '%s'\n", + gpGlobals->curtime, + player->GetPlayerName(), + item->GetStaticData()->GetItemBaseName(), + upgradeName ? upgradeName : "<self>" ); + } + } + } + } + } + + player->EndPurchasableUpgrades(); +} + +//------------------------------------------------------------------------- +void CPopulationManager::ForgetOtherBottleUpgrades ( CTFPlayer *player, CEconItemView *pItem, int upgradeToKeep ) +{ + PlayerUpgradeHistory *history = FindOrAddPlayerUpgradeHistory( player ); + + // This only applies to the current class, skip bottle upgrades for other classes + int iClass = player->GetPlayerClass()->GetClassIndex(); + for( int i = 0; i < history->m_upgradeVector.Count(); ++i ) + { + if ( iClass != history->m_upgradeVector[i].m_iPlayerClass ) + { + continue; + } + + // remove upgrades that do NOT match the target + if ( history->m_upgradeVector[i].m_itemDefIndex == pItem->GetItemDefIndex() && history->m_upgradeVector[i].m_upgrade != upgradeToKeep ) // item upgrade + { + history->m_upgradeVector.FastRemove( i ); + --i; + } + } +} + +// ---------------------------------------------------------------------------------- +void CPopulationManager::RestorePlayerCurrency () +{ + // Set the players money on round start + int nRoundCurrency = m_pMVMStats->GetAcquiredCredits( -1 ); + nRoundCurrency += GetStartingCurrency(); + + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_PVE_DEFENDERS ); + + for( int i=0; i<playerVector.Count(); ++i ) + { + // deduct any cash that has already been spent + int spentCurrency = GetPlayerCurrencySpent( playerVector[i] ); + playerVector[i]->SetCurrency( nRoundCurrency - spentCurrency ); + } +} + +// ---------------------------------------------------------------------------------- +// Purpose : Get the player's upgrade list +// ---------------------------------------------------------------------------------- +CUtlVector< CUpgradeInfo > *CPopulationManager::GetPlayerUpgradeHistory ( CTFPlayer *player ) +{ + PlayerUpgradeHistory *history = FindOrAddPlayerUpgradeHistory ( player ); + + if ( !history ) + return NULL; + + return &(history->m_upgradeVector); +} + +// ---------------------------------------------------------------------------------- +// Purpose : Get the player's currency history +// ---------------------------------------------------------------------------------- +int CPopulationManager::GetPlayerCurrencySpent ( CTFPlayer *player ) +{ + PlayerUpgradeHistory *history = FindOrAddPlayerUpgradeHistory ( player ); + if ( !history ) + return 0; + + return history->m_currencySpent; +} + +// ---------------------------------------------------------------------------------- +// Purpose : Get the player's currency history +// ---------------------------------------------------------------------------------- +void CPopulationManager::AddPlayerCurrencySpent ( CTFPlayer *player, int cost ) +{ + PlayerUpgradeHistory *history = FindOrAddPlayerUpgradeHistory ( player ); + if ( !history ) + return; + + history->m_currencySpent += cost; +} + +// ---------------------------------------------------------------------------------- +// Purpose : Send the player their upgrades +void CPopulationManager::SendUpgradesToPlayer ( CTFPlayer *player ) +{ + CUtlVector< CUpgradeInfo > *upgrades = GetPlayerUpgradeHistory ( player ); + m_pMVMStats->SendUpgradesToPlayer( player, upgrades ); +} + +//----------------------------------------------------------------------------------- +void CPopulationManager::RestoreCheckpoint( void ) +{ + // No checkpoints for Endless + + m_isRestoringCheckpoint = true; + + if ( !IsInEndlessWaves() ) + { + m_iCurrentWaveIndex = m_checkpointWaveIndex; + } + + // Purge all player upgrades + // Set them to the checkpoint state + m_playerUpgrades.PurgeAndDeleteElements(); + + // We must clear each player's upgrade history to get rid of upgrades they + // purchased since the last checkpoint. The history will be rebuilt + // as the checkpoint snapshot is restored. + for( int i=0; i<m_checkpointSnapshot.Count(); ++i ) + { + CheckpointSnapshotInfo *snapshot = m_checkpointSnapshot[i]; + + // Create new Entry since we Purged the list + PlayerUpgradeHistory *history = FindOrAddPlayerUpgradeHistory( snapshot->m_steamId ); + history->m_currencySpent = snapshot->m_currencySpent; + + for (int j = 0; j < snapshot->m_upgradeVector.Count(); ++j ) + { + history->m_upgradeVector.AddToTail( snapshot->m_upgradeVector[j] ); + } + } + + // Iterate over play + // clear Bottles and Sentry danger and send their upgrades + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_PVE_DEFENDERS ); + for( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *player = playerVector[i]; + + // Clear sentry danger + player->ResetAccumulatedSentryGunDamageDealt(); + player->ResetAccumulatedSentryGunKillCount(); + + // Bottles must be purged separately, charges will be restored with other items + CTFWearable *pWearable = player->GetEquippedWearableForLoadoutSlot( LOADOUT_POSITION_ACTION ); + CTFPowerupBottle *pPowerupBottle = dynamic_cast< CTFPowerupBottle* >( pWearable ); + if ( pPowerupBottle ) + { + pPowerupBottle->Reset(); + } + + SendUpgradesToPlayer( player ); + } + + m_nNumConsecutiveWipes++; + + // players are restored to their checkpoint state after they spawn + m_pMVMStats->SetCurrentWave( m_iCurrentWaveIndex ); + + UpdateObjectiveResource(); + + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Get_To_Upgrade" ); + + m_nRespecsAwardedInWave = 0; +} + + +//------------------------------------------------------------------------- +void CPopulationManager::ClearCheckpoint( void ) +{ + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "%3.2f: CHECKPOINT_CLEAR\n", gpGlobals->curtime ); + } + + m_nNumConsecutiveWipes = 0; + m_checkpointWaveIndex = 0; + m_checkpointSnapshot.PurgeAndDeleteElements(); + + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_PVE_DEFENDERS ); + + for( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *player = playerVector[i]; + + player->ClearUpgradeHistory(); + } + +} + +//------------------------------------------------------------------------- +void CPopulationManager::OnPlayerKilled( CTFPlayer *corpse ) +{ + for( int i=0; i<m_populatorVector.Count(); ++i ) + { + m_populatorVector[i]->OnPlayerKilled( corpse ); + } + + CWave * pWave = GetCurrentWave(); + if ( pWave ) + { + pWave->OnPlayerKilled( corpse ); + } +} + +//------------------------------------------------------------------------- +void CPopulationManager::OnCurrencyPackFade( void ) +{ +} + + +//------------------------------------------------------------------------- +void CPopulationManager::OnCurrencyCollected( int nAmount, bool bCountAsDropped, bool bIsBonus ) +{ + // Store how much money players collect between waves so we can update the checkpoint + int iWaveNumber = GetWaveNumber(); + if ( TFObjectiveResource()->GetMannVsMachineIsBetweenWaves() ) + { + // Decrement WaveNumber if between waves, money was for previous wave + iWaveNumber--; + } + + if ( bCountAsDropped ) + { + m_pMVMStats->RoundEvent_CreditsDropped( iWaveNumber, nAmount ); + } + m_pMVMStats->RoundEvent_AcquiredCredits( iWaveNumber, nAmount, bIsBonus ); + + // Respec + int nRespecLimit = tf_mvm_respec_limit.GetInt(); + if ( nRespecLimit ) + { + bool bAtLimit = m_nRespecsAwarded >= nRespecLimit; + if ( !bAtLimit ) + { + m_nCurrencyCollectedForRespec += nAmount; + + // It's possible to earn multiple respecs from a large cash award + int nCreditGoal = tf_mvm_respec_credit_goal.GetInt(); + while ( m_nCurrencyCollectedForRespec >= nCreditGoal && !bAtLimit ) + { + ++m_nRespecsAwarded; + ++m_nRespecsAwardedInWave; + + // Award each player a respec + CUtlVector< CTFPlayer* > playerVector; + CollectPlayers( &playerVector, TF_TEAM_PVE_DEFENDERS ); + FOR_EACH_VEC( playerVector, i ) + { + AddRespecToPlayer( playerVector[i] ); + } + + // Allowed to earn another? + bAtLimit = m_nRespecsAwarded >= nRespecLimit; + if ( !bAtLimit ) + { + m_nCurrencyCollectedForRespec -= nCreditGoal; + } + else + { + // If we're at the limit, peg this value for client UI. + // i.e. "Respec Goal: 100 of 100" + m_nCurrencyCollectedForRespec = nCreditGoal; + } + } + + // Send down to clients + CMannVsMachineStats *pStats = MannVsMachineStats_GetInstance(); + if ( pStats ) + { + pStats->SetNumRespecsEarnedInWave( m_nRespecsAwardedInWave ); + pStats->SetAcquiredCreditsForRespec( m_nCurrencyCollectedForRespec ); + } + } + } +} + +//------------------------------------------------------------------------- +int CPopulationManager::GetTotalPopFileCurrency( void ) +{ + uint32 nTotalPopCurrency = 0; + + FOR_EACH_VEC( m_waveVector, i ) + { + nTotalPopCurrency += m_waveVector[i]->GetTotalCurrency(); + } + + return nTotalPopCurrency; +} + +//------------------------------------------------------------------------- +void CPopulationManager::AdjustMinPlayerSpawnTime( void ) +{ + enum { kMvMRespawnTimeAddPerWave = 2, }; + + int iWaveNum = GetWaveNumber() + 1; + + float flTime = 1.0f; + if ( IsInEndlessWaves() ) + { + flTime = iWaveNum / 3.0f; + } + else + { + flTime = m_bFixedRespawnWaveTime ? m_nRespawnWaveTime : + MIN( m_nRespawnWaveTime, float( iWaveNum * kMvMRespawnTimeAddPerWave ) ); + } + TFGameRules()->SetTeamRespawnWaveTime( TF_TEAM_PVE_DEFENDERS, flTime ); +} + +//------------------------------------------------------------------------- +void CPopulationManager::MarkAllCurrentPlayersSafeToLeave() +{ + // If we have a match, mark everyone currently in the match safe to leave. + CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch(); + if ( !pMatch ) + { return; } + + // Mark everyone that was meant to be in the match safe to leave now, not just those who were actually present, + // mirroring old behavior (we are usually using this to release every current player from obligations to stay) + int total = pMatch->GetNumTotalMatchPlayers(); + for ( int idx = 0; idx < total; idx++ ) + { + pMatch->GetMatchDataForPlayer( idx )->MarkAlwaysSafeToLeave(); + } +} + +//------------------------------------------------------------------------- +void CPopulationManager::MvMVictory() +{ + // Give "Bonus_Time Buff" + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) ); + if ( !pTFPlayer || !pTFPlayer->IsPlayer() ) + continue; + + if ( pTFPlayer->IsAlive() ) + { + pTFPlayer->m_Shared.AddCond( TF_COND_CRITBOOSTED_BONUS_TIME, 10.0 ); + } + } + + // Set Game state + TFGameRules()->BroadcastSound( 255, "Game.YourTeamWon" ); + + m_pMVMStats->RoundOver( true ); + ClearCheckpoint(); + + // Notify Players of Victory + CBroadcastRecipientFilter filter; + filter.MakeReliable(); + UserMessageBegin( filter, "MVMVictory" ); + + bool bIsKicking = tf_mvm_disconnect_on_victory.GetBool(); + WRITE_BYTE( (uint8)bIsKicking ); + + if ( bIsKicking ) + { + WRITE_BYTE((uint8)tf_mvm_victory_disconnect_time.GetFloat()); + } + else + { + WRITE_BYTE((uint8)tf_mvm_victory_reset_time.GetFloat()); + } + MessageEnd(); + + // Note that because MvM is weird, we can have multiple victories per one match, as players can keep going + GTFGCClientSystem()->SendMvMVictoryResult(); +} + +//------------------------------------------------------------------------- +void CPopulationManager::GetSentryBusterDamageAndKillThreshold( int &nDamage, int &nKills ) const +{ + const int nSentryThreshold = 2; + + int nSentries = 0; + for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i ) + { + CBaseObject* pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] ); + if ( pObj->ObjectType() == OBJ_SENTRYGUN ) + { + // Disposable sentries are not valid targets + if ( pObj->IsDisposableBuilding() ) + continue; + + if ( pObj->GetTeamNumber() == TF_TEAM_PVE_DEFENDERS ) + { + nSentries++; + } + } + } + + // Adjust damage and kill threshold based on number of sentries in the world + // otherwise players trivially handle the spawn rate with raw damage + float flScale = RemapValClamped( nSentries, 1, 6, 1.f, 0.5f ); + nDamage = ( nSentries >= nSentryThreshold ) ? m_sentryBusterDamageDealtThreshold * flScale : m_sentryBusterDamageDealtThreshold; + nKills = ( nSentries >= nSentryThreshold ) ? m_sentryBusterKillThreshold * flScale : m_sentryBusterKillThreshold; +} + +//------------------------------------------------------------------------- +bool CPopulationManager::IsInEndlessWaves ( void ) +{ + return (m_bEndlessOn || tf_mvm_endless_force_on.GetBool() ) && m_waveVector.Count() > 0; +} + +//------------------------------------------------------------------------- +float CPopulationManager::GetHealthMultiplier ( bool bIsTank /*= false*/ ) +{ + if ( !IsInEndlessWaves() || !bIsTank ) + return tf_populator_health_multiplier.GetFloat(); + + // Calculate how much health the tank should get per wave + return tf_populator_health_multiplier.GetFloat() + m_iCurrentWaveIndex * tf_mvm_endless_tank_boost.GetFloat(); +} + +//------------------------------------------------------------------------- +float CPopulationManager::GetDamageMultiplier () +{ + //if ( !IsInEndlessWaves() ) + return tf_populator_damage_multiplier.GetFloat(); + + // Find out how many times over t + // Floor of the result, ie 9 / 7 returns 1, 15 / 7 returns 2; + //int nRepeatCount = m_iCurrentWaveIndex / tf_mvm_endless_scale_rate.GetInt(); + //return tf_populator_damage_multiplier.GetFloat() + tf_mvm_endless_damage_boost_rate.GetFloat() * nRepeatCount; +} + +//------------------------------------------------------------------------- +void CPopulationManager::EndlessParseBotUpgrades () +{ + // Don't do anything if we don't have raid mode. + m_BotUpgradesList.RemoveAll(); + + KeyValues *pKV = new KeyValues( "Upgrades" ); + + if ( !pKV->LoadFromFile( filesystem, "scripts/items/mvm_botupgrades.txt", "MOD" ) ) + { + Warning( "Can't open scripts/items/mvm_botupgrades.txt\n" ); + pKV->deleteThis(); + return; + } + + for ( KeyValues *pData = pKV->GetFirstSubKey(); pData != NULL; pData = pData->GetNextKey() ) + { + const char *pszAttrib = pData->GetString( "attribute" ); + int iAttribIndex = 0; + bool bIsBotAttr = pData->GetBool( "IsBotAttr" ); + bool bIsSkillAttr = pData->GetBool( "IsSkillAttr" ); + + float flValue = pData->GetFloat( "value" ); + float flMax = pData->GetFloat( "max" ); + int nCost = pData->GetFloat( "cost", 100 ); + int nWeight = pData->GetInt( "weight", 1 ); + + // Normal econ attr + if ( !bIsBotAttr && !bIsSkillAttr ) + { + CEconItemSchema *pSchema = ItemSystem()->GetItemSchema(); + if ( pSchema ) + { + // If we can't find a matching attribute, continue + const CEconItemAttributeDefinition *pAttr = pSchema->GetAttributeDefinitionByName( pszAttrib ); + if ( !pAttr ) + { + DevMsg( "Unable to Find Attribute %s when parsing EndlessParseBotUpgrades \n", pszAttrib ); + continue; + } + + iAttribIndex = pAttr->GetDefinitionIndex(); + } + } + + int index = m_BotUpgradesList.AddToTail(); + + for ( int i = 0; i < nWeight; ++i ) + { + CMvMBotUpgrade *pUpgrade = &( m_BotUpgradesList[ index ] ); + + // load her up + V_strncpy( pUpgrade->szAttrib, pszAttrib, sizeof( pUpgrade->szAttrib ) ); + + pUpgrade->flValue = flValue; + pUpgrade->flMax = flMax; + pUpgrade->nCost = nCost; + pUpgrade->bIsBotAttr = bIsBotAttr; + pUpgrade->bIsSkillAttr = bIsSkillAttr; + pUpgrade->iAttribIndex = iAttribIndex; + } + } + + pKV->deleteThis(); +} + +//------------------------------------------------------------------------- +void CPopulationManager::EndlessRollEscalation( void ) +{ + // Get the wave and calculate the amount of "money" the bots have + + // for now + int nBotCash = ( m_iCurrentWaveIndex * tf_mvm_endless_bot_cash.GetFloat() ); + + m_EndlessActiveBotUpgrades.Purge(); + + // Create a list of items that can be purchased + CUtlVector< CMvMBotUpgrade > vecAvailableUpgrades; + + FOR_EACH_VEC( m_BotUpgradesList, i ) + { + if ( m_BotUpgradesList[i].nCost <= nBotCash ) + { + vecAvailableUpgrades.AddToTail( m_BotUpgradesList[i] ); + } + } + + CUniformRandomStream rRandom; + rRandom.SetSeed( m_EndlessSeeds[ m_iCurrentWaveIndex % m_EndlessSeeds.Count() ] ); + + while ( nBotCash >= 100 && vecAvailableUpgrades.Count() > 0 ) + { + int index = rRandom.RandomInt( 0, vecAvailableUpgrades.Count() - 1); + + CMvMBotUpgrade upgrade = vecAvailableUpgrades[index]; + + // Scan the existing list and append the value if it does, otherwise add new entry + bool bUpgradeFound = false; + FOR_EACH_VEC( m_EndlessActiveBotUpgrades, iUpgrade ) + { + if ( !V_strcmp( m_EndlessActiveBotUpgrades[iUpgrade].szAttrib, upgrade.szAttrib) ) + { + bUpgradeFound = true; + // increment the value + m_EndlessActiveBotUpgrades[iUpgrade].flValue += upgrade.flValue; + nBotCash -= upgrade.nCost; + + // remove this upgrade if its been max + if ( ( upgrade.flMax > 0 && upgrade.flMax <= m_EndlessActiveBotUpgrades[iUpgrade].flValue ) + || ( upgrade.flMax < 0 && upgrade.flMax >= m_EndlessActiveBotUpgrades[iUpgrade].flValue ) ) + { + vecAvailableUpgrades.FastRemove( index ); + } + break; + } + } + + if ( !bUpgradeFound ) + { + m_EndlessActiveBotUpgrades.AddToTail( upgrade ); + nBotCash -= upgrade.nCost; + // remove this upgrade if its been max + if ( ( upgrade.flMax > 0 && upgrade.flMax <= upgrade.flValue ) + || ( upgrade.flMax < 0 && upgrade.flMax >= upgrade.flValue ) ) + { + vecAvailableUpgrades.FastRemove( index ); + } + } + + // Scan available upgrades and remove any that we can't cover + if ( nBotCash > 0 ) + { + FOR_EACH_VEC_BACK( vecAvailableUpgrades, iUpgrade ) + { + if ( vecAvailableUpgrades[iUpgrade].nCost > nBotCash ) + { + vecAvailableUpgrades.FastRemove( iUpgrade ); + } + } + } + } + + char msg[255]; + V_strcpy_safe( msg, "*** Bot Upgrades\n" ); + FOR_EACH_VEC( m_EndlessActiveBotUpgrades, iUpgrade ) + { + char line[255]; + V_sprintf_safe( line, "-%s %.1f\n", m_EndlessActiveBotUpgrades[iUpgrade].szAttrib, m_EndlessActiveBotUpgrades[iUpgrade].flValue ); + V_strcat_safe( msg, line ); + } + + UTIL_CenterPrintAll( msg ); + UTIL_ClientPrintAll( HUD_PRINTCONSOLE, msg ); +} + +//------------------------------------------------------------------------- +void CPopulationManager::EndlessSetAttributesForBot( CTFBot *pBot ) +{ + FOR_EACH_VEC( m_EndlessActiveBotUpgrades, i ) + { + //DevMsg( " - %s %d ", m_EndlessActiveBotUpgrades[iUpgrade].szAttrib, m_EndlessActiveBotUpgrades[iUpgrade].flValue ); + //pBot->m_AttributeManager + if ( m_EndlessActiveBotUpgrades[i].bIsBotAttr == true ) + { + pBot->SetAttribute( (int)m_EndlessActiveBotUpgrades[i].flValue ); + } + else if ( m_EndlessActiveBotUpgrades[i].bIsSkillAttr == true ) + { + //switch ( (int)m_EndlessActiveBotUpgrades[i].flValue ) + pBot->SetDifficulty( (CTFBot::DifficultyType)(int)m_EndlessActiveBotUpgrades[i].flValue ); + } + else + { + CEconItemAttributeDefinition *pDef = ItemSystem()->GetItemSchema()->GetAttributeDefinition( m_EndlessActiveBotUpgrades[i].iAttribIndex ); + if ( pDef ) + { + int iFormat = pDef->GetDescriptionFormat(); + float flValue = m_EndlessActiveBotUpgrades[i].flValue; + if ( iFormat == ATTDESCFORM_VALUE_IS_PERCENTAGE || iFormat == ATTDESCFORM_VALUE_IS_INVERTED_PERCENTAGE ) + { + flValue += 1.0; + } + Assert( pBot->GetAttributeList() ); + pBot->GetAttributeList()->SetRuntimeAttributeValue( pDef, flValue ); + } + } + } +} + +//------------------------------------------------------------------------- +bool CPopulationManager::EndlessShouldResetFlag () +{ + return m_bShouldResetFlag; +} + +//------------------------------------------------------------------------- +void CPopulationManager::EndlessFlagHasReset () +{ + m_bShouldResetFlag = false; +} + +//------------------------------------------------------------------------- +// PRIVATE +//------------------------------------------------------------------------- + +// ------------------------------------------------------------------------- +// Purpose : PostInit +// ------------------------------------------------------------------------- +void CPopulationManager::PostInitialize( void ) +{ + if ( TheNavMesh->GetNavAreaCount() <= 0 ) + { + Warning( "Cannot populate - no Navigation Mesh exists.\n" ); + return; + } + + FOR_EACH_VEC ( m_populatorVector, i ) + { + m_populatorVector[i]->PostInitialize(); + } + + FOR_EACH_VEC ( m_waveVector, i ) + { + m_waveVector[i]->PostInitialize(); + } +} + +//------------------------------------------------------------------------- +// Purpose : Read the target file (m_filename) and populate initial data fields +//------------------------------------------------------------------------- +bool CPopulationManager::Parse( void ) +{ + if ( m_popfileFull[ 0 ] == '\0' ) + { + Warning( "No population file specified.\n" ); + return false; + } + + //if ( m_bIsInitialized ) +// return true; + + KeyValues *values = new KeyValues( "Population" ); + if ( !values->LoadFromFile( filesystem, m_popfileFull, "GAME" ) ) + { + Warning( "Can't open %s.\n", m_popfileFull ); + values->deleteThis(); + return false; + } + + // Clear out existing Data structures + m_populatorVector.PurgeAndDeleteElements(); + m_waveVector.RemoveAll(); + m_bEndlessOn = false; + + if ( m_pTemplates ) + { + m_pTemplates->deleteThis(); + m_pTemplates = NULL; + } + + // find templates first + KeyValues *pTemplates = values->FindKey( "Templates" ); + + if ( pTemplates ) + { + m_pTemplates = pTemplates->MakeCopy(); + } + + for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) + { + const char *name = data->GetName(); + + if ( Q_strlen( name ) <= 0 ) + { + continue; + } + + if ( !Q_stricmp( name, "StartingCurrency" ) ) + { + m_nStartingCurrency = data->GetInt(); + } + else if ( !Q_stricmp( name, "RespawnWaveTime" ) ) + { + m_nRespawnWaveTime = data->GetInt(); + } + else if ( !Q_stricmp( name, "EventPopfile" ) ) + { + if ( !Q_stricmp( data->GetString(), "Halloween" ) ) + { + m_nMvMEventPopfileType = MVM_EVENT_POPFILE_HALLOWEEN; + } + else + { + m_nMvMEventPopfileType = MVM_EVENT_POPFILE_NONE; + } + } + else if ( !Q_stricmp( name, "FixedRespawnWaveTime" ) ) + { + m_bFixedRespawnWaveTime = true; + } + else if ( !Q_stricmp( name, "AddSentryBusterWhenDamageDealtExceeds" ) ) + { + m_sentryBusterDamageDealtThreshold = data->GetInt(); + } + else if ( !Q_stricmp( name, "AddSentryBusterWhenKillCountExceeds" ) ) + { + m_sentryBusterKillThreshold = data->GetInt(); + } + else if ( !Q_stricmp( name, "CanBotsAttackWhileInSpawnRoom" ) ) + { + if ( !Q_stricmp( data->GetString(), "no" ) || !Q_stricmp( data->GetString(), "false" ) ) + { + m_canBotsAttackWhileInSpawnRoom = false; + } + else + { + m_canBotsAttackWhileInSpawnRoom = true; + } + } + else if ( !Q_stricmp( name, "RandomPlacement" ) ) + { + CRandomPlacementPopulator *randomPopulator = new CRandomPlacementPopulator( this ); + + if ( randomPopulator->Parse( data ) == false ) + { + Warning( "Error reading RandomPlacement definition\n" ); + return false; + } + + m_populatorVector.AddToTail( randomPopulator ); + } + else if ( !Q_stricmp( name, "PeriodicSpawn" ) ) + { + CPeriodicSpawnPopulator *periodicPopulator = new CPeriodicSpawnPopulator( this ); + + if ( periodicPopulator->Parse( data ) == false ) + { + Warning( "Error reading PeriodicSpawn definition\n" ); + return false; + } + + m_populatorVector.AddToTail( periodicPopulator ); + } + else if ( !Q_stricmp( name, "Wave" ) ) + { + CWave *wave = new CWave( this ); + + if ( !wave->Parse( data ) ) + { + Warning( "Error reading Wave definition\n" ); + return false; + } + + + // also keep vector of wave pointers for convenience + m_waveVector.AddToTail( wave ); + } + else if ( !Q_stricmp( name, "Mission" ) ) + { + CMissionPopulator *missionPopulator = new CMissionPopulator( this ); + + if ( missionPopulator->Parse( data ) == false ) + { + Warning( "Error reading Mission definition\n" ); + return false; + } + + m_populatorVector.AddToTail( missionPopulator ); + } + else if ( !Q_stricmp( name, "Templates" ) ) + { + // handled above + } + else if ( !Q_stricmp( name, "Advanced" ) ) + { + m_bAdvancedPopFile = true; + } + else if ( !Q_stricmp( name, "IsEndless" ) ) + { + m_bEndlessOn = true; + } + else + { + Warning( "Invalid populator '%s'\n", name ); + return false; + } + } + + for ( int nPopulator = 0; nPopulator < m_populatorVector.Count(); ++nPopulator ) + { + CMissionPopulator *pMission = dynamic_cast< CMissionPopulator* >( m_populatorVector[ nPopulator ] ); + + if ( pMission ) + { + // FIXME: Need a way to handle missions that spawn multiple types + int nStartWave = pMission->BeginAtWave(); + int nStopWave = pMission->StopAtWave(); + + if ( pMission->m_spawner && !pMission->m_spawner->IsVarious() ) + { + for ( int i = nStartWave; i < nStopWave; ++i ) + { + if ( m_waveVector.IsValidIndex( i ) ) + { + CWave *pWave = m_waveVector[ i ]; + + unsigned int iFlags = MVM_CLASS_FLAG_MISSION; + if ( pMission->m_spawner->IsMiniBoss() ) + { + iFlags |= MVM_CLASS_FLAG_MINIBOSS; + } + if ( pMission->m_spawner->HasAttribute( CTFBot::ALWAYS_CRIT ) ) + { + iFlags |= MVM_CLASS_FLAG_ALWAYSCRIT; + } + pWave->AddClassType( pMission->m_spawner->GetClassIcon(), 0, iFlags ); + } + } + } + } + } + + values->deleteThis(); + + return true; +} + + +// ---------------------------------------------------------------------------------- +// Purpose : Find a Checkpoint info +//------------------------------------------------------------------------- +CPopulationManager::CheckpointSnapshotInfo *CPopulationManager::FindCheckpointSnapshot( CTFPlayer *player ) const +{ + CSteamID steamId; + if (!player->GetSteamID( &steamId )) + return NULL; + + return FindCheckpointSnapshot( steamId ); +} + +// ---------------------------------------------------------------------------------- +// Purpose : Find a Checkpoint info +//------------------------------------------------------------------------- +CPopulationManager::CheckpointSnapshotInfo *CPopulationManager::FindCheckpointSnapshot( CSteamID id ) const +{ + for( int i=0; i<m_checkpointSnapshot.Count(); ++i ) + { + CheckpointSnapshotInfo *snapshot = m_checkpointSnapshot[i]; + + if ( id == snapshot->m_steamId ) + return snapshot; + } + + return NULL; +} + +// ---------------------------------------------------------------------------------- +// Purpose : Returns the Player's Upgrade History Struct, Adds a new entry if not present +// ---------------------------------------------------------------------------------- +CPopulationManager::PlayerUpgradeHistory *CPopulationManager::FindOrAddPlayerUpgradeHistory ( CTFPlayer *player ) +{ + CSteamID steamId; + if (!player->GetSteamID( &steamId )) + { + Log( "MvM : Unable to Find SteamID for player %s, unable to locate their upgrade history!", player->GetPlayerName() ); + return NULL; + } + + return FindOrAddPlayerUpgradeHistory( steamId ); +} + +// ---------------------------------------------------------------------------------- +// Purpose : Returns the Player's Upgrade History Struct, Adds a new entry if not present +// ---------------------------------------------------------------------------------- +CPopulationManager::PlayerUpgradeHistory *CPopulationManager::FindOrAddPlayerUpgradeHistory ( CSteamID steamId ) +{ + FOR_EACH_VEC( m_playerUpgrades, i ) + { + if ( steamId == m_playerUpgrades[i]->m_steamId ) + { + return m_playerUpgrades[i]; + } + } + + PlayerUpgradeHistory *history = new PlayerUpgradeHistory; + + history->m_steamId = steamId; + history->m_currencySpent = 0; + + m_playerUpgrades.AddToTail( history ); + return history; +} + +// ---------------------------------------------------------------------------------- +// Purpose : Remove upgrade tracking tied to the player and their items (ignores bottles, buybacks) +// ---------------------------------------------------------------------------------- +void CPopulationManager::RemovePlayerAndItemUpgradesFromHistory( CTFPlayer *pPlayer ) +{ + CSteamID steamId; + if ( !pPlayer->GetSteamID( &steamId ) ) + return; + + // Remove player and item upgrades from snapshots + FOR_EACH_VEC_BACK( m_checkpointSnapshot, i ) + { + CheckpointSnapshotInfo *pSnapshot = m_checkpointSnapshot[i]; + if ( steamId != pSnapshot->m_steamId ) + continue; + + FOR_EACH_VEC_BACK( pSnapshot->m_upgradeVector, j ) + { + int iUpgrade = pSnapshot->m_upgradeVector[j].m_upgrade; + CMannVsMachineUpgrades *pUpgrade = &(g_MannVsMachineUpgrades.m_Upgrades[ iUpgrade ]); + if ( pUpgrade && ( pUpgrade->nUIGroup == UIGROUP_UPGRADE_ATTACHED_TO_ITEM || pUpgrade->nUIGroup == UIGROUP_UPGRADE_ATTACHED_TO_PLAYER ) ) + { + pSnapshot->m_currencySpent -= pSnapshot->m_upgradeVector[j].m_nCost; + pSnapshot->m_upgradeVector.Remove( j ); + } + } + } + + // Remove player and item upgrades from current + FOR_EACH_VEC_BACK( m_playerUpgrades, i ) + { + if ( steamId != m_playerUpgrades[i]->m_steamId ) + continue; + + FOR_EACH_VEC_BACK( m_playerUpgrades[i]->m_upgradeVector, j ) + { + int iUpgrade = m_playerUpgrades[i]->m_upgradeVector[j].m_upgrade; + CMannVsMachineUpgrades *pUpgrade = &(g_MannVsMachineUpgrades.m_Upgrades[ iUpgrade ]); + if ( pUpgrade && ( pUpgrade->nUIGroup == UIGROUP_UPGRADE_ATTACHED_TO_ITEM || pUpgrade->nUIGroup == UIGROUP_UPGRADE_ATTACHED_TO_PLAYER ) ) + { + m_playerUpgrades[i]->m_currencySpent -= m_playerUpgrades[i]->m_upgradeVector[j].m_nCost; + m_playerUpgrades[i]->m_upgradeVector.Remove( j ); + } + } + } + + // Only do this step in MvM + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && m_pMVMStats ) + { + // This should put us at the right currency, given that we've removed item and player upgrade tracking by this point + int nTotalAcquiredCurrency = m_pMVMStats->GetAcquiredCredits( -1 ) + GetStartingCurrency(); + int nSpentCurrency = GetPlayerCurrencySpent( pPlayer ); + pPlayer->SetCurrency( nTotalAcquiredCurrency - nSpentCurrency ); + + // Reset the stat that tracks upgrade purchases + m_pMVMStats->ResetUpgradeSpending( pPlayer ); + } +}; + +//----------------------------------------------------------------------------- +// Purpose: This adds one per call +//----------------------------------------------------------------------------- +void CPopulationManager::AddRespecToPlayer( CTFPlayer *pPlayer ) +{ + if ( !pPlayer ) + return; + + CSteamID steamID; + if ( pPlayer->GetSteamID( &steamID ) ) + { + int iIndex = m_PlayerRespecPoints.Find( steamID.ConvertToUint64() ); + if ( iIndex != m_PlayerRespecPoints.InvalidIndex() ) + { + int nCount = m_PlayerRespecPoints[iIndex]; + if ( nCount >= tf_mvm_respec_limit.GetInt() ) + return; + + m_PlayerRespecPoints[iIndex]++; + } + else + { + m_PlayerRespecPoints.Insert( steamID.ConvertToUint64(), 1 ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: This removes one per call +//----------------------------------------------------------------------------- +void CPopulationManager::RemoveRespecFromPlayer( CTFPlayer *pPlayer ) +{ + if ( !pPlayer ) + return; + + // Unlimited + if ( !tf_mvm_respec_limit.GetInt() ) + return; + + CSteamID steamID; + if ( pPlayer->GetSteamID( &steamID ) ) + { + int iIndex = m_PlayerRespecPoints.Find( steamID.ConvertToUint64() ); + if ( iIndex != m_PlayerRespecPoints.InvalidIndex() ) + { + Assert( m_PlayerRespecPoints[iIndex] ); + + m_PlayerRespecPoints[iIndex]--; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: This stomps whatever value we have +//----------------------------------------------------------------------------- +void CPopulationManager::SetNumRespecsForPlayer( CTFPlayer *pPlayer, int nCount ) +{ + if ( !pPlayer ) + return; + + CSteamID steamID; + if ( pPlayer->GetSteamID( &steamID ) ) + { + int iIndex = m_PlayerRespecPoints.Find( steamID.ConvertToUint64() ); + if ( iIndex != m_PlayerRespecPoints.InvalidIndex() ) + { + m_PlayerRespecPoints[iIndex] = nCount; + } + else + { + m_PlayerRespecPoints.Insert( steamID.ConvertToUint64(), nCount ); + } + } +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +int CPopulationManager::GetNumRespecsAvailableForPlayer( CTFPlayer *pPlayer ) +{ + // Unlimited + if ( !tf_mvm_respec_limit.GetBool() ) + return 1; + + CSteamID steamID; + if ( pPlayer && pPlayer->GetSteamID( &steamID ) ) + { + int iIndex = m_PlayerRespecPoints.Find( steamID.ConvertToUint64() ); + if ( iIndex != m_PlayerRespecPoints.InvalidIndex() ) + { + return m_PlayerRespecPoints[iIndex]; + } + } + + return 0; +} + +//----------------------------------------------------------------------------- +// Purpose: Slam the value to nCount +//----------------------------------------------------------------------------- +void CPopulationManager::SetBuybackCreditsForPlayer( CTFPlayer *pPlayer, int nCount ) +{ + if ( !pPlayer ) + return; + + CSteamID steamID; + if ( pPlayer->GetSteamID( &steamID ) ) + { + int iIndex = m_PlayerBuybackPoints.Find( steamID.ConvertToUint64() ); + if ( iIndex != m_PlayerBuybackPoints.InvalidIndex() ) + { + m_PlayerBuybackPoints[iIndex] = nCount; + } + else + { + m_PlayerBuybackPoints.Insert( steamID.ConvertToUint64(), nCount ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: This removes one per call +//----------------------------------------------------------------------------- +void CPopulationManager::RemoveBuybackCreditFromPlayer( CTFPlayer *pPlayer ) +{ + if ( !pPlayer ) + return; + + // Unlimited + if ( !tf_mvm_buybacks_method.GetInt() ) + return; + + CSteamID steamID; + if ( pPlayer->GetSteamID( &steamID ) ) + { + int iIndex = m_PlayerBuybackPoints.Find( steamID.ConvertToUint64() ); + if ( iIndex != m_PlayerBuybackPoints.InvalidIndex() ) + { + Assert( m_PlayerBuybackPoints[iIndex] ); + + m_PlayerBuybackPoints[iIndex]--; + } + } +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +int CPopulationManager::GetNumBuybackCreditsForPlayer( CTFPlayer *pPlayer ) +{ + if ( !tf_mvm_buybacks_method.GetBool() ) + return 1; + + CSteamID steamID; + if ( pPlayer && pPlayer->GetSteamID( &steamID ) ) + { + int iIndex = m_PlayerBuybackPoints.Find( steamID.ConvertToUint64() ); + if ( iIndex != m_PlayerBuybackPoints.InvalidIndex() ) + { + return m_PlayerBuybackPoints[iIndex]; + } + } + + return 0; +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +bool CPopulationManager::IsPlayerBeingTrackedForBuybacks( CTFPlayer *pPlayer ) +{ + int iIndex = m_PlayerBuybackPoints.InvalidIndex(); + + CSteamID steamID; + if ( pPlayer && pPlayer->GetSteamID( &steamID ) ) + { + iIndex = m_PlayerBuybackPoints.Find( steamID.ConvertToUint64() ); + } + + return ( iIndex != m_PlayerBuybackPoints.InvalidIndex() ); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CPopulationManager::ResetRespecPoints( void ) +{ + m_PlayerRespecPoints.RemoveAll(); + m_nRespecsAwarded = 0; + m_nRespecsAwardedInWave = 0; + m_nCurrencyCollectedForRespec = 0; + + // Send down to clients + if ( tf_mvm_respec_limit.GetBool() ) + { + CMannVsMachineStats *pStats = MannVsMachineStats_GetInstance(); + if ( pStats ) + { + pStats->SetNumRespecsEarnedInWave( m_nRespecsAwardedInWave ); + pStats->SetAcquiredCreditsForRespec( m_nCurrencyCollectedForRespec ); + } + } +} + +// ---------------------------------------------------------------------------------- +// Purpose : Output useful info to console - Remove before ship +// ---------------------------------------------------------------------------------- +void CPopulationManager::DebugWaveStats () +{ + // map economy stats to spit to the console + // remove before shipping + if ( m_waveVector.Count() ) + { + uint32 nTotalPopCurrency = GetTotalPopFileCurrency(); + uint32 nTotalWaves = GetTotalWaveCount(); + + DevMsg( "---\n" ); + DevMsg( "Credits: %d\n", nTotalPopCurrency ); + DevMsg( "Waves: %d ( %3.2f credits per wave )\n", nTotalWaves, float( (float)nTotalPopCurrency / (float)nTotalWaves ) ); + DevMsg( "---\n" ); + } + + if ( m_EndlessActiveBotUpgrades.Count() > 0) + { + DevMsg( "*** Endless Bot Upgrades - %.0f Cash *** \n", m_iCurrentWaveIndex * tf_mvm_endless_bot_cash.GetFloat() ); + FOR_EACH_VEC( m_EndlessActiveBotUpgrades, iUpgrade ) + { + DevMsg( " - %s %.2f\n", m_EndlessActiveBotUpgrades[iUpgrade].szAttrib, m_EndlessActiveBotUpgrades[iUpgrade].flValue ); + } + + char msg[255]; + V_strcpy_safe( msg, "*** Bot Upgrades\n" ); + FOR_EACH_VEC( m_EndlessActiveBotUpgrades, iUpgrade ) + { + char line[255]; + V_sprintf_safe( line, "-%s %.1f\n", m_EndlessActiveBotUpgrades[iUpgrade].szAttrib, m_EndlessActiveBotUpgrades[iUpgrade].flValue ); + V_strcat_safe( msg, line ); + } + + UTIL_CenterPrintAll( msg ); + UTIL_ClientPrintAll( HUD_PRINTCONSOLE, msg ); + } + + DevMsg( "Popfile: %s\n", GetPopulationFilename() ); +} + + +void CPopulationManager::AllocateBots() +{ + if ( m_bAllocatedBots ) + { + return; + } + + int nNumEnemyBots = 0; + + CUtlVector<CTFPlayer *> botVector; + nNumEnemyBots = CollectMvMBots( &botVector ); + + if ( botVector.Count() > 0 ) + { + Assert( botVector.Count() == 0 ); + Warning( "%d bots were already allocated some how before CPopulationManager::AllocateBots was called\n", botVector.Count() ); + } + + for ( int i = nNumEnemyBots; i < MVM_INVADERS_TEAM_SIZE; ++i ) + { + CTFBot* newBot = NextBotCreatePlayerBot< CTFBot >( "TFBot", false ); + if ( newBot ) + { + newBot->ChangeTeam( TEAM_SPECTATOR, false, true ); + } + } + + m_bAllocatedBots = true; +} + +void CPopulationManager::PauseSpawning() +{ + DevMsg( "Wave paused\n" ); + m_bSpawningPaused = true; +} + +void CPopulationManager::UnpauseSpawning() +{ + DevMsg( "Wave unpaused\n" ); + + m_bSpawningPaused = false; + + // Some populators need to reset their timers or do other things when we unpause. + // Go through and let them know we've un-paused. + FOR_EACH_VEC( m_populatorVector, i ) + { + m_populatorVector[i]->UnpauseSpawning(); + } +} + +bool CPopulationManager::HasEventChangeAttributes( const char* pszEventName ) const +{ + for ( int i=0; i<m_waveVector.Count(); ++i ) + { + if ( m_waveVector[i]->HasEventChangeAttributes( pszEventName ) ) + { + return true; + } + } + + for ( int i=0; i<m_populatorVector.Count(); ++i ) + { + if ( m_populatorVector[i]->HasEventChangeAttributes( pszEventName ) ) + { + return true; + } + } + + return false; +} + +/*static*/ int CPopulationManager::CollectMvMBots ( CUtlVector< CTFPlayer *> *pBots ) +{ + pBots->RemoveAll(); + + for( int i=1; i<=gpGlobals->maxClients; ++i ) + { + CTFPlayer *player = ToTFPlayer( UTIL_PlayerByIndex( i ) ); + + if ( player == NULL ) + continue; + + if ( FNullEnt( player->edict() ) ) + continue; + + if ( !player->IsPlayer() ) + continue; + + if ( !player->IsBot() ) + continue; + + if ( !player->IsConnected() ) + continue; + + if ( player->GetTeamNumber() == TF_TEAM_PVE_DEFENDERS ) // Want everything but defenders + continue; + + pBots->AddToTail( player ); + } + + return pBots->Count(); +} diff --git a/game/server/tf/player_vs_environment/tf_population_manager.h b/game/server/tf/player_vs_environment/tf_population_manager.h new file mode 100644 index 0000000..d1420bf --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_population_manager.h @@ -0,0 +1,286 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_populator.h +// KeyValues driven procedural population system +// Michael Booth, April 2011 + +#ifndef TF_POPULATOR_H +#define TF_POPULATOR_H + + +#include "bot/tf_bot.h" +#include "tf_mann_vs_machine_stats.h" +#include "tf_populator_spawners.h" +#include "tf_populators.h" + +class CMannVsMachineStats; +class KeyValues; +class IPopulator; +class CPopulationManager; +class CWave; + +#define MVM_POP_FILE_PATH "scripts/population" + +//----------------------------------------------------------------------- +class CPopulationManager : public CPointEntity, public CGameEventListener +{ +public: + DECLARE_CLASS( CPopulationManager, CPointEntity ); + DECLARE_DATADESC(); + + CPopulationManager( void ); + virtual ~CPopulationManager(); + + // CPointEntity + virtual void Spawn( void ); // entity spawn + + // CGameEventListener + virtual void FireGameEvent( IGameEvent *gameEvent ); + + virtual void Reset( void ); + virtual bool Initialize( void ); + virtual void Precache( void ); + + const char *GetPopulationFilename( void ); + const char *GetPopulationFilenameShort( void ); + void SetPopulationFilename( const char *populationFile ); + + // Resolve a pop file shortname to a full name + bool FindPopulationFileByShortName( const char *pShortName, CUtlString &outFullName ); + + // Enumerate possible population files for this map, searching e.g. mapname_*.pop, //BSP/.../*.pop + // This is separate from the explicit popfile lists for tours. + static void FindDefaultPopulationFileShortNames( CUtlVector< CUtlString > &outVecShortNames ); + + void SetupOnRoundStart( void ); + + void Update( void ); // continuously invoked to modify population over time + + // Triggered by GameRules for any mode specific logic that isn't suitable for the entity think (e.g. not simulating) + void GameRulesThink(); + + void UpdateObjectiveResource( void ); + void ResetMap( void ); + + void CycleMission ( void ); + bool LoadMissionCycleFile ( void ); + bool IsValidMvMMap( const char *pszMapName ); + + // Waves + void ShowNextWaveDescription( void ); + void StartCurrentWave( void ); + void JumpToWave( uint32 waveNumber, float fCleanMoneyPercent = -1.0f ); + void WaveEnd ( bool bSuccess ); + + CWave *GetCurrentWave( void ); + + int GetWaveNumber( void ) { return m_iCurrentWaveIndex; } + int GetTotalWaveCount( void ) { return m_waveVector.Count(); } + + // Check Points + void ClearCheckpoint( void ); + void SetCheckpoint( int waveNumber ); + void RestoreCheckpoint( void ); + void RestoreItemToCheckpointState( CTFPlayer *player, CEconItemView *item ); + void ForgetOtherBottleUpgrades ( CTFPlayer *player, CEconItemView *pItem, int upgradeToKeep ); + void RestorePlayerCurrency (); + + CUtlVector< CUpgradeInfo > *GetPlayerUpgradeHistory ( CTFPlayer *player ); // Returns the Upgrade vector for a given player + int GetPlayerCurrencySpent ( CTFPlayer *player ); + void AddPlayerCurrencySpent ( CTFPlayer *player, int cost ); + void SendUpgradesToPlayer ( CTFPlayer *player ); + + void OnPlayerKilled( CTFPlayer *corpse ); + void OnCurrencyPackFade( void ); + void OnCurrencyCollected( int nAmount, bool bCountAsDropped, bool bIsBonus ); + int GetTotalPopFileCurrency( void ); + + void AdjustMinPlayerSpawnTime( void ); + + void MarkAllCurrentPlayersSafeToLeave(); + void MvMVictory( void ); + void PlayerDoneViewingLoot( const CTFPlayer* pPlayer ); + + void GetSentryBusterDamageAndKillThreshold( int &nDamage, int &nKills ) const; + + bool IsInEndlessWaves ( void ); + + // Endless + float GetHealthMultiplier ( bool bIsTank = false ); + float GetDamageMultiplier ( void ); + void EndlessParseBotUpgrades( void ); + void EndlessRollEscalation ( void ); + void EndlessSetAttributesForBot( CTFBot *pBot ); + bool EndlessShouldResetFlag (); + void EndlessFlagHasReset (); + + // inlined + bool IsRestoringCheckpoint( void ) const; // return true if we are in the process of restoring players to their checkpointed state + float GetElapsedTime( void ) const; // return elapsed time since scenario started + int GetStartingCurrency( void ) const; + int GetRespawnWaveTime( void ) const; + bool CanBotsAttackWhileInSpawnRoom( void ) const; + KeyValues *GetTemplate( const char *pszName ) const; + + bool IsAdvancedPopFile( void ) { return m_bAdvancedPopFile; } + void SetMapRestartTime( float flTime ) { m_flMapRestartTime = flTime; } + bool IsPopFileEventType( int fileType ) { return m_nMvMEventPopfileType == fileType; } + + void DebugWaveStats(); + + void AllocateBots(); + + void PauseSpawning(); + void UnpauseSpawning(); + bool IsSpawningPaused() const { return m_bSpawningPaused; } + + bool IsBonusRound() const { return m_bBonusRound; } + CBaseCombatCharacter* GetBonusBoss() const { return m_hBonusBoss; } + + enum { MVM_INVADERS_TEAM_SIZE = 22 }; + + static bool GetWavesUseReadyBetween() { return true; } + + void SetDefaultEventChangeAttributesName( const char* pszDefaultEventChangeAttributesName ) { m_defaultEventChangeAttributesName = pszDefaultEventChangeAttributesName; } + const char* GetDefaultEventChangeAttributesName() const { return m_defaultEventChangeAttributesName.String(); } + bool HasEventChangeAttributes( const char* pszEventName ) const; + + static int CollectMvMBots ( CUtlVector< CTFPlayer *> *pBots ); + + // Respec + void RemovePlayerAndItemUpgradesFromHistory( CTFPlayer *pPlayer ); + void AddRespecToPlayer( CTFPlayer *pPlayer ); + void RemoveRespecFromPlayer( CTFPlayer *pPlayer ); + void SetNumRespecsForPlayer( CTFPlayer *pPlayer, int nCount ); + int GetNumRespecsAvailableForPlayer( CTFPlayer *pPlayer ); + int GetNumRespecsEarnedInWave( void ) { return m_nRespecsAwardedInWave; } + int GetNumRespecsEarned( void ) { return m_nRespecsAwarded; } + void ResetRespecPoints( void ); + + // Buyback + void SetBuybackCreditsForPlayer( CTFPlayer *pPlayer, int nCount ); + void RemoveBuybackCreditFromPlayer( CTFPlayer *pPlayer ); + int GetNumBuybackCreditsForPlayer( CTFPlayer *pPlayer ); + bool IsPlayerBeingTrackedForBuybacks( CTFPlayer *pPlayer ); + + +private: + struct CheckpointSnapshotInfo + { + CSteamID m_steamId; // which player this snapshot is for + int m_currencySpent; // how much money they had spent up to this check point + CUtlVector< CUpgradeInfo > m_upgradeVector; // the upgrades the player had as this checkpoint + }; + + struct PlayerUpgradeHistory + { + CSteamID m_steamId; // which player this snapshot is for + CUtlVector< CUpgradeInfo > m_upgradeVector; + int m_currencySpent; + }; + + void PostInitialize( void ); + bool Parse( void ); // read in population from file from m_filename + + CheckpointSnapshotInfo *FindCheckpointSnapshot( CTFPlayer *player ) const; + CheckpointSnapshotInfo *FindCheckpointSnapshot( CSteamID id ) const; + PlayerUpgradeHistory *FindOrAddPlayerUpgradeHistory ( CTFPlayer *player ); + PlayerUpgradeHistory *FindOrAddPlayerUpgradeHistory ( CSteamID steamId ); + + void LoadLastKnownMission(); + bool LoadMvMMission( KeyValues *pNextMission ); + + CUtlVector< IPopulator * > m_populatorVector; + char m_popfileFull[ MAX_PATH ]; + char m_popfileShort[ MAX_PATH ]; + + KeyValues *m_pTemplates; + + bool m_bIsInitialized; + bool m_bAllocatedBots; + + bool m_bBonusRound; + CHandle< CBaseCombatCharacter > m_hBonusBoss; + + int m_nStartingCurrency; + int m_nLobbyBonusCurrency; + int m_nMvMEventPopfileType; + int m_nRespawnWaveTime; + bool m_bFixedRespawnWaveTime; + bool m_canBotsAttackWhileInSpawnRoom; + int m_sentryBusterDamageDealtThreshold; + int m_sentryBusterKillThreshold; + + uint32 m_iCurrentWaveIndex; + CUtlVector< CWave * > m_waveVector; // pointers to waves within m_populationVector + + float m_flMapRestartTime; // Restart the Map if gameover and this time elapses + + CUtlVector< PlayerUpgradeHistory * > m_playerUpgrades; // list of all players and their upgrades who have played on this MVM rotation + + bool m_isRestoringCheckpoint; + + // checkpoint data must be static because the population manager entity is destroyed and recreated each round + static CUtlVector< CheckpointSnapshotInfo * > m_checkpointSnapshot; // snapshot of player state at the saved checkpoint + + static int m_checkpointWaveIndex; // wave to restart at if scenario lost + static int m_nNumConsecutiveWipes; + + bool m_bAdvancedPopFile; + bool m_bCheckForCurrencyAchievement; + + CMannVsMachineStats *m_pMVMStats; + KeyValues *m_pKvpMvMMapCycle; + + bool m_bSpawningPaused; + bool m_bIsWaveJumping; + bool m_bEndlessOn; + CUtlVector< CMvMBotUpgrade > m_BotUpgradesList; + CUtlVector< CMvMBotUpgrade > m_EndlessActiveBotUpgrades; + CUniformRandomStream m_randomizer; + CUtlVector< int > m_EndlessSeeds; + bool m_bShouldResetFlag; + CUtlVector< const CTFPlayer* > m_donePlayers; + + CUtlString m_defaultEventChangeAttributesName; + + // Respec + CUtlMap< uint64, int > m_PlayerRespecPoints; // The number of upgrade respecs players (steamID) have + int m_nRespecsAwarded; + int m_nRespecsAwardedInWave; + int m_nCurrencyCollectedForRespec; + + // Buyback + CUtlMap< uint64, int > m_PlayerBuybackPoints; // The number of times a player can buyback + +}; + +inline bool CPopulationManager::IsRestoringCheckpoint( void ) const +{ + return m_isRestoringCheckpoint; +} + +inline int CPopulationManager::GetStartingCurrency( void ) const +{ + return m_nStartingCurrency + m_nLobbyBonusCurrency; +} + +inline int CPopulationManager::GetRespawnWaveTime( void ) const +{ + return m_nRespawnWaveTime; +} + +inline bool CPopulationManager::CanBotsAttackWhileInSpawnRoom( void ) const +{ + return m_canBotsAttackWhileInSpawnRoom; +} + +inline KeyValues *CPopulationManager::GetTemplate( const char *pszName ) const +{ + return m_pTemplates->FindKey( pszName ); +} + +// singleton accessor +extern CPopulationManager *g_pPopulationManager; + + +#endif // TF_POPULATOR_H diff --git a/game/server/tf/player_vs_environment/tf_populator_interface.cpp b/game/server/tf/player_vs_environment/tf_populator_interface.cpp new file mode 100644 index 0000000..7441c96 --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_populator_interface.cpp @@ -0,0 +1,103 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements populator interface entity. Used for map +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "tf_population_manager.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern ConVar tf_populator_debug; + + +class CPointPopulatorInterface : public CPointEntity +{ + DECLARE_CLASS( CPointPopulatorInterface, CPointEntity ); + +public: + + // Input handlers + void InputPauseBotSpawning(inputdata_t &inputdata); + void InputUnpauseBotSpawning(inputdata_t &inputdata); + void InputChangeBotAttributes(inputdata_t &inputdata); + void InputChangeDefaultEventAttributes(inputdata_t &inputdata); + + DECLARE_DATADESC(); +}; + +BEGIN_DATADESC( CPointPopulatorInterface ) + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "PauseBotSpawning", InputPauseBotSpawning ), + DEFINE_INPUTFUNC( FIELD_VOID, "UnpauseBotSpawning", InputUnpauseBotSpawning ), + DEFINE_INPUTFUNC( FIELD_STRING, "ChangeBotAttributes", InputChangeBotAttributes ), + DEFINE_INPUTFUNC( FIELD_STRING, "ChangeDefaultEventAttributes", InputChangeDefaultEventAttributes ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( point_populator_interface, CPointPopulatorInterface ); + + +void CPointPopulatorInterface::InputPauseBotSpawning( inputdata_t &inputdata ) +{ + Assert( g_pPopulationManager ); + if( g_pPopulationManager ) + { + g_pPopulationManager->PauseSpawning(); + } +} + +void CPointPopulatorInterface::InputUnpauseBotSpawning( inputdata_t &inputdata ) +{ + Assert( g_pPopulationManager ); + if( g_pPopulationManager ) + { + g_pPopulationManager->UnpauseSpawning(); + } +} + +void CPointPopulatorInterface::InputChangeBotAttributes( inputdata_t &inputdata ) +{ + const char* pszEventName = inputdata.value.String(); + + if ( tf_populator_debug.GetBool() && g_pPopulationManager && !g_pPopulationManager->HasEventChangeAttributes( pszEventName ) ) + { + Warning( "ChangeBotAttributes: Failed to find event [%s] in the pop file\n", pszEventName ); + return; + } + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + CUtlVector< CTFBot* > botVector; + CollectPlayers( &botVector, TF_TEAM_PVE_INVADERS, COLLECT_ONLY_LIVING_PLAYERS ); + + for ( int i=0; i<botVector.Count(); ++i ) + { + const CTFBot::EventChangeAttributes_t* pEvent = botVector[i]->GetEventChangeAttributes( pszEventName ); + if ( pEvent ) + { + botVector[i]->OnEventChangeAttributes( pEvent ); + } + } + } +} + +void CPointPopulatorInterface::InputChangeDefaultEventAttributes(inputdata_t &inputdata) +{ + const char* pszEventName = inputdata.value.String(); + + if ( tf_populator_debug.GetBool() && g_pPopulationManager && !g_pPopulationManager->HasEventChangeAttributes( pszEventName ) ) + { + Warning( "ChangeBotAttributes: Failed to find event [%s] in the pop file\n", pszEventName ); + return; + } + + if ( g_pPopulationManager ) + { + g_pPopulationManager->SetDefaultEventChangeAttributesName( pszEventName ); + } +} diff --git a/game/server/tf/player_vs_environment/tf_populator_spawners.cpp b/game/server/tf/player_vs_environment/tf_populator_spawners.cpp new file mode 100644 index 0000000..53bb61d --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_populator_spawners.cpp @@ -0,0 +1,1890 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: tf_populator_spawners +// Implementations of NPC Spawning Code for PvE related game modes (MvM) +//=============================================================================// + +#include "cbase.h" + +#include "tf_population_manager.h" +#include "tf_team.h" +#include "tf_obj_sentrygun.h" +#include "tf_weapon_medigun.h" +#include "tf_tank_boss.h" +#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.h" + +#include "etwprof.h" + +extern ConVar tf_mvm_skill; +extern ConVar tf_mm_trusted; + +extern ConVar tf_populator_debug; +extern ConVar tf_populator_active_buffer_range; +extern ConVar tf_mvm_miniboss_scale; + +ConVar tf_debug_placement_failure( "tf_debug_placement_failure", "0", FCVAR_CHEAT ); + +LINK_ENTITY_TO_CLASS( populator_internal_spawn_point, CPopulatorInternalSpawnPoint ); +CHandle< CPopulatorInternalSpawnPoint > g_internalSpawnPoint = NULL; + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if a player has room to spawn at the given position + */ +bool IsSpaceToSpawnHere( const Vector &where ) +{ + // make sure a player will fit here + trace_t result; + float bloat = 5.0f; + UTIL_TraceHull( where, where, VEC_HULL_MIN - Vector( bloat, bloat, 0 ), VEC_HULL_MAX + Vector( bloat, bloat, bloat ), MASK_SOLID | CONTENTS_PLAYERCLIP, NULL, COLLISION_GROUP_PLAYER_MOVEMENT, &result ); + + if ( tf_debug_placement_failure.GetBool() && result.fraction < 1.0f ) + { + NDebugOverlay::Cross3D( where, 5.0f, 255, 100, 0, true, 99999.9f ); + } + + return result.fraction >= 1.0; +} + +//----------------------------------------------------------------------- +//----------------------------------------------------------------------- +/*static*/ IPopulationSpawner *IPopulationSpawner::ParseSpawner( IPopulator *populator, KeyValues *data ) +{ + const char *name = data->GetName(); + + if ( Q_strlen( name ) <= 0 ) + { + return NULL; + } + + if ( !Q_stricmp( name, "TFBot" ) ) + { + CTFBotSpawner *botSpawner = new CTFBotSpawner( populator ); + + if ( botSpawner->Parse( data ) == false ) + { + Warning( "Warning reading TFBot spawner definition\n" ); + delete botSpawner; + return NULL; + } + + return botSpawner; + } + else if ( !Q_stricmp( name, "Tank" ) ) + { + CTankSpawner *tankSpawner = new CTankSpawner( populator ); + + if ( tankSpawner->Parse( data ) == false ) + { + Warning( "Warning reading Tank spawner definition\n" ); + delete tankSpawner; + return NULL; + } + + return tankSpawner; + } + else if ( !Q_stricmp( name, "SentryGun" ) ) + { + CSentryGunSpawner *sentrySpawner = new CSentryGunSpawner( populator ); + + if ( sentrySpawner->Parse( data ) == false ) + { + Warning( "Warning reading SentryGun spawner definition\n" ); + delete sentrySpawner; + return NULL; + } + + return sentrySpawner; + } + else if ( !Q_stricmp( name, "Squad" ) ) + { + CSquadSpawner *squadSpawner = new CSquadSpawner( populator ); + + if ( squadSpawner->Parse( data ) == false ) + { + Warning( "Warning reading Squad spawner definition\n" ); + delete squadSpawner; + return NULL; + } + + return squadSpawner; + } + else if ( !Q_stricmp( name, "Mob" ) ) + { + CMobSpawner *mobSpawner = new CMobSpawner( populator ); + + if ( mobSpawner->Parse( data ) == false ) + { + Warning( "Warning reading Mob spawner definition\n" ); + delete mobSpawner; + return NULL; + } + + return mobSpawner; + } + else if ( !Q_stricmp( name, "RandomChoice" ) ) + { + CRandomChoiceSpawner *randomSpawner = new CRandomChoiceSpawner( populator ); + + if ( randomSpawner->Parse( data ) == false ) + { + Warning( "Warning reading RandomChoice spawner definition\n" ); + delete randomSpawner; + return NULL; + } + + return randomSpawner; + } + + return NULL; +} + +//----------------------------------------------------------------------- +//----------------------------------------------------------------------- +static EventInfo *ParseEvent( KeyValues *values ) +{ + EventInfo *eventInfo = new EventInfo; + + for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) + { + const char *name = data->GetName(); + + if ( Q_strlen( name ) <= 0 ) + { + continue; + } + + if ( !Q_stricmp( name, "Target" ) ) + { + eventInfo->m_target.sprintf( "%s", data->GetString() ); + } + else if ( !Q_stricmp( name, "Action" ) ) + { + eventInfo->m_action.sprintf( "%s", data->GetString() ); + } + else + { + Warning( "Unknown field '%s' in WaveSpawn event definition.\n", data->GetString() ); + delete eventInfo; + return NULL; + } + } + + return eventInfo; +} + +//----------------------------------------------------------------------- +// CRandomChoiceSpawner +//----------------------------------------------------------------------- +CRandomChoiceSpawner::CRandomChoiceSpawner( IPopulator *populator ) : IPopulationSpawner( populator ) +{ + m_spawnerVector.RemoveAll(); + m_nRandomPickDecision.RemoveAll(); + m_nNumSpawned = 0; +} + + +//----------------------------------------------------------------------- +CRandomChoiceSpawner::~CRandomChoiceSpawner() +{ + for( int i=0; i<m_spawnerVector.Count(); ++i ) + { + delete m_spawnerVector[i]; + } + m_spawnerVector.RemoveAll(); + m_nRandomPickDecision.RemoveAll(); + m_nNumSpawned = 0; +} + + +//----------------------------------------------------------------------- +bool CRandomChoiceSpawner::Parse( KeyValues *values ) +{ + for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) + { + const char *name = data->GetName(); + + if ( Q_strlen( name ) <= 0 ) + { + continue; + } + + IPopulationSpawner *spawner = ParseSpawner( m_populator, data ); + + if ( spawner == NULL ) + { + Warning( "Unknown attribute '%s' in RandomChoice definition.\n", name ); + } + else + { + m_spawnerVector.AddToTail( spawner ); + } + } + + return true; +} + + +//----------------------------------------------------------------------- +bool CRandomChoiceSpawner::Spawn( const Vector &here, EntityHandleVector_t *result ) +{ + if ( m_spawnerVector.Count() < 1 ) + return false; + + int nCurrentCount = m_nRandomPickDecision.Count(); + int nNumToAdd = ( m_nNumSpawned + 1 ) - nCurrentCount; + + if ( nNumToAdd > 0 ) + { + m_nRandomPickDecision.AddMultipleToTail( nNumToAdd ); + + for ( int i = nCurrentCount; i < m_nNumSpawned + 1; ++i ) + { + m_nRandomPickDecision[ i ] = RandomInt( 0, m_spawnerVector.Count() - 1 ); + } + } + + bool bResult = m_spawnerVector[ m_nRandomPickDecision[ m_nNumSpawned ] ]->Spawn( here, result ); + + m_nNumSpawned++; + + return bResult; +} + +int CRandomChoiceSpawner::GetClass( int nSpawnNum /*= -1*/ ) +{ + if ( nSpawnNum < 0 ) + { + return TF_CLASS_UNDEFINED; + } + + int nCurrentCount = m_nRandomPickDecision.Count(); + int nNumToAdd = ( nSpawnNum + 1 ) - nCurrentCount; + + if ( nNumToAdd > 0 ) + { + m_nRandomPickDecision.AddMultipleToTail( nNumToAdd ); + + for ( int i = nCurrentCount; i < nSpawnNum + 1; ++i ) + { + m_nRandomPickDecision[ i ] = RandomInt( 0, m_spawnerVector.Count() - 1 ); + } + } + + if ( !m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->IsVarious() ) + { + return m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->GetClass(); + } + else + { + // FIXME: Nested complex spawner types... need a method for counting these. + Assert( 1 ); + DevWarning( "Nested complex spawner types... need a method for counting these." ); + return TF_CLASS_UNDEFINED; + } +} + +string_t CRandomChoiceSpawner::GetClassIcon( int nSpawnNum /*= -1*/ ) +{ + if ( nSpawnNum < 0 ) + { + return NULL_STRING; + } + + int nCurrentCount = m_nRandomPickDecision.Count(); + int nNumToAdd = ( nSpawnNum + 1 ) - nCurrentCount; + + if ( nNumToAdd > 0 ) + { + m_nRandomPickDecision.AddMultipleToTail( nNumToAdd ); + + for ( int i = nCurrentCount; i < nSpawnNum + 1; ++i ) + { + m_nRandomPickDecision[ i ] = RandomInt( 0, m_spawnerVector.Count() - 1 ); + } + } + + if ( !m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->IsVarious() ) + { + return m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->GetClassIcon(); + } + else + { + // FIXME: Nested complex spawner types... need a method for counting these. + Assert( 1 ); + DevWarning( "Nested complex spawner types... need a method for counting these." ); + return NULL_STRING; + } +} + +int CRandomChoiceSpawner::GetHealth( int nSpawnNum /*= -1*/ ) +{ + if ( nSpawnNum < 0 ) + { + return 0; + } + + int nCurrentCount = m_nRandomPickDecision.Count(); + int nNumToAdd = ( nSpawnNum + 1 ) - nCurrentCount; + + if ( nNumToAdd > 0 ) + { + m_nRandomPickDecision.AddMultipleToTail( nNumToAdd ); + + for ( int i = nCurrentCount; i < nSpawnNum + 1; ++i ) + { + m_nRandomPickDecision[ i ] = RandomInt( 0, m_spawnerVector.Count() - 1 ); + } + } + + if ( !m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->IsVarious() ) + { + return m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->GetHealth(); + } + else + { + // FIXME: Nested complex spawner types... need a method for counting these. + Assert( 1 ); + DevWarning( "Nested complex spawner types... need a method for counting these." ); + return 0; + } +} + + +bool CRandomChoiceSpawner::IsMiniBoss( int nSpawnNum /*= -1*/ ) +{ + if ( nSpawnNum < 0 ) + { + return false; + } + + int nCurrentCount = m_nRandomPickDecision.Count(); + int nNumToAdd = ( nSpawnNum + 1 ) - nCurrentCount; + + if ( nNumToAdd > 0 ) + { + m_nRandomPickDecision.AddMultipleToTail( nNumToAdd ); + + for ( int i = nCurrentCount; i < nSpawnNum + 1; ++i ) + { + m_nRandomPickDecision[ i ] = RandomInt( 0, m_spawnerVector.Count() - 1 ); + } + } + + if ( !m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->IsVarious() ) + { + return m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->IsMiniBoss(); + } + else + { + // FIXME: Nested complex spawner types... need a method for counting these. + Assert( 1 ); + DevWarning( "Nested complex spawner types... need a method for counting these." ); + return false; + } +} + + +bool CRandomChoiceSpawner::HasAttribute( CTFBot::AttributeType type, int nSpawnNum /*= -1*/ ) +{ + if ( nSpawnNum < 0 ) + { + return false; + } + + int nCurrentCount = m_nRandomPickDecision.Count(); + int nNumToAdd = ( nSpawnNum + 1 ) - nCurrentCount; + + if ( nNumToAdd > 0 ) + { + m_nRandomPickDecision.AddMultipleToTail( nNumToAdd ); + + for ( int i = nCurrentCount; i < nSpawnNum + 1; ++i ) + { + m_nRandomPickDecision[ i ] = RandomInt( 0, m_spawnerVector.Count() - 1 ); + } + } + + if ( !m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->IsVarious() ) + { + return m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->HasAttribute( type, nSpawnNum ); + } + else + { + // FIXME: Nested complex spawner types... need a method for counting these. + Assert( 1 ); + DevWarning( "Nested complex spawner types... need a method for counting these." ); + return false; + } +} + + +bool CRandomChoiceSpawner::HasEventChangeAttributes( const char* pszEventName ) const +{ + for ( int i=0; i<m_spawnerVector.Count(); ++i ) + { + m_spawnerVector[i]->HasEventChangeAttributes( pszEventName ); + } + + return false; +} + + +//----------------------------------------------------------------------- +//----------------------------------------------------------------------- +CTFBotSpawner::CTFBotSpawner( IPopulator *populator ) : IPopulationSpawner( populator ) +{ + m_class = TF_CLASS_UNDEFINED; + m_iszClassIcon = NULL_STRING; + + m_health = -1; // default health + m_scale = -1.0f; // default scale + m_flAutoJumpMin = m_flAutoJumpMax = 0.f; // default AutoJumpMin/Max + + m_defaultAttributes.Reset(); +} + +static void ParseCharacterAttributes( CTFBot::EventChangeAttributes_t& event, KeyValues *data ) +{ + CUtlVector<static_attrib_t> vecStaticAttribs; + + FOR_EACH_SUBKEY( data, pKVAttribute ) + { + CUtlVector<CUtlString> vecErrors; + + static_attrib_t staticAttrib; + if ( !staticAttrib.BInitFromKV_SingleLine( "CharacterAttributes", pKVAttribute, &vecErrors ) ) + { + FOR_EACH_VEC( vecErrors, i ) + { + Warning( "TFBotSpawner: attribute error: '%s'\n", vecErrors[i].Get() ); + } + continue; + } + + vecStaticAttribs.AddToTail( staticAttrib ); + } + + // found an existing entry for this name -- stomp attribute values if present or add + // new entries if not + FOR_EACH_VEC( vecStaticAttribs, iNewAttribute ) + { + const static_attrib_t& newStaticAttrib = vecStaticAttribs[iNewAttribute]; + bool bAdd = true; + + int iExistingAttribute; + for ( iExistingAttribute = 0; iExistingAttribute < event.m_characterAttributes.Count(); iExistingAttribute++ ) + { + // matching existing attribute? stomp value + if ( event.m_characterAttributes[iExistingAttribute].iDefIndex == newStaticAttrib.iDefIndex ) + { + // stomp value + event.m_characterAttributes[iExistingAttribute].m_value = newStaticAttrib.m_value; + bAdd = false; + // move on to next new attribute + break; + } + } + + if ( bAdd ) + { + // couldn't find? add new attribute entry + event.m_characterAttributes.AddToTail( newStaticAttrib ); + } + } +} + +static void ParseItemAttributes( CTFBot::EventChangeAttributes_t& event, KeyValues *data ) +{ + const char *pszItemName = NULL; + CUtlVector<static_attrib_t> vecStaticAttribs; + + FOR_EACH_SUBKEY( data, pKVSubkey ) + { + if ( !Q_stricmp( pKVSubkey->GetName(), "ItemName" ) ) + { + if ( pszItemName ) + { + Warning( "TFBotSpawner: \"ItemName\" field specified multiple times ('%s' / '%s').\n", pszItemName, pKVSubkey->GetString() ); + } + + pszItemName = pKVSubkey->GetString(); + } + else + { + CUtlVector<CUtlString> vecErrors; + + static_attrib_t staticAttrib; + if ( !staticAttrib.BInitFromKV_SingleLine( "ItemAttributes", pKVSubkey, &vecErrors ) ) + { + FOR_EACH_VEC( vecErrors, i ) + { + Warning( "TFBotSpawner: attribute error: '%s'\n", vecErrors[i].Get() ); + } + continue; + } + + vecStaticAttribs.AddToTail( staticAttrib ); + } + } + + if ( !pszItemName ) + { + Warning( "TFBotSpawner: need to specify ItemName in ItemAttributes.\n" ); + return; + } + + // check if the item name is already on the list + FOR_EACH_VEC( event.m_itemsAttributes, i ) + { + CTFBot::EventChangeAttributes_t::item_attributes_t& botItemAttrs = event.m_itemsAttributes[i]; + + if ( !Q_stricmp( botItemAttrs.m_itemName, pszItemName ) ) + { + // found an existing entry for this name -- stomp attribute values if present or add + // new entries if not + FOR_EACH_VEC( vecStaticAttribs, iNewAttribute ) + { + const static_attrib_t& newStaticAttrib = vecStaticAttribs[iNewAttribute]; + + int iExistingAttribute; + for ( iExistingAttribute = 0; iExistingAttribute < botItemAttrs.m_attributes.Count(); iExistingAttribute++ ) + { + // matching existing attribute? stomp value + if ( botItemAttrs.m_attributes[iExistingAttribute].iDefIndex == newStaticAttrib.iDefIndex ) + { + // stomp value + botItemAttrs.m_attributes[iExistingAttribute].m_value = newStaticAttrib.m_value; + + // move on to next new attribute + break; + } + } + + // couldn't find? add new attribute entry + botItemAttrs.m_attributes.AddToTail( newStaticAttrib ); + } + + // only one entry expected -- done here, before we add a new one + return; + } + } + + // new item + CTFBot::EventChangeAttributes_t::item_attributes_t botItemAttributes; + botItemAttributes.m_itemName = pszItemName; + botItemAttributes.m_attributes.AddVectorToTail( vecStaticAttribs ); + + event.m_itemsAttributes.AddToTail( botItemAttributes ); +} + +static bool ParseDynamicAttributes( CTFBot::EventChangeAttributes_t& event, KeyValues* data ) +{ + const char *name = data->GetName(); + const char *value = data->GetString(); + + if ( !Q_stricmp( name, "Skill" ) ) + { + if ( !Q_stricmp( value, "Easy" ) ) + { + event.m_skill = CTFBot::EASY; + } + else if ( !Q_stricmp( value, "Normal" ) ) + { + event.m_skill = CTFBot::NORMAL; + } + else if ( !Q_stricmp( value, "Hard" ) ) + { + event.m_skill = CTFBot::HARD; + } + else if ( !Q_stricmp( value, "Expert" ) ) + { + event.m_skill = CTFBot::EXPERT; + } + else + { + Warning( "TFBotSpawner: Invalid skill '%s'\n", value ); + return false; + } + } + else if ( !Q_stricmp( name, "WeaponRestrictions" ) ) + { + if ( !Q_stricmp( value, "MeleeOnly" ) ) + { + event.m_weaponRestriction = CTFBot::MELEE_ONLY; + } + else if ( !Q_stricmp( value, "PrimaryOnly" ) ) + { + event.m_weaponRestriction = CTFBot::PRIMARY_ONLY; + } + else if ( !Q_stricmp( value, "SecondaryOnly" ) ) + { + event.m_weaponRestriction = CTFBot::SECONDARY_ONLY; + } + else + { + Warning( "TFBotSpawner: Invalid weapon restriction '%s'\n", value ); + return false; + } + } + else if ( !Q_stricmp( name, "BehaviorModifiers" ) ) + { + // modifying bot attribute flags here due to legacy code + if ( !Q_stricmp( value, "Mobber" ) || !Q_stricmp( value, "Push" ) ) + { + event.m_attributeFlags |= CTFBot::AGGRESSIVE; + } + else + { + Warning( "TFBotSpawner: Invalid behavior modifier '%s'\n", value ); + return false; + } + } + else if ( !Q_stricmp( name, "Attributes" ) ) + { + if ( !Q_stricmp( value, "RemoveOnDeath" ) ) + { + event.m_attributeFlags |= CTFBot::REMOVE_ON_DEATH; + } + else if ( !Q_stricmp( value, "Aggressive" ) ) + { + event.m_attributeFlags |= CTFBot::AGGRESSIVE; + } + else if ( !Q_stricmp( value, "SuppressFire" ) ) + { + event.m_attributeFlags |= CTFBot::SUPPRESS_FIRE; + } + else if ( !Q_stricmp( value, "DisableDodge" ) ) + { + event.m_attributeFlags |= CTFBot::DISABLE_DODGE; + } + else if ( !Q_stricmp( value, "BecomeSpectatorOnDeath" ) ) + { + event.m_attributeFlags |= CTFBot::BECOME_SPECTATOR_ON_DEATH; + } + else if ( !Q_stricmp( value, "RetainBuildings" ) ) + { + event.m_attributeFlags |= CTFBot::RETAIN_BUILDINGS; + } + else if ( !Q_stricmp( value, "SpawnWithFullCharge" ) ) + { + event.m_attributeFlags |= CTFBot::SPAWN_WITH_FULL_CHARGE; + } + else if ( !Q_stricmp( value, "AlwaysCrit" ) ) + { + event.m_attributeFlags |= CTFBot::ALWAYS_CRIT; + } + else if ( !Q_stricmp( value, "IgnoreEnemies" ) ) + { + event.m_attributeFlags |= CTFBot::IGNORE_ENEMIES; + } + else if ( !Q_stricmp( value, "HoldFireUntilFullReload" ) ) + { + event.m_attributeFlags |= CTFBot::HOLD_FIRE_UNTIL_FULL_RELOAD; + } + else if ( !Q_stricmp( value, "AlwaysFireWeapon" ) ) + { + event.m_attributeFlags |= CTFBot::ALWAYS_FIRE_WEAPON; + } + else if ( !Q_stricmp( value, "TeleportToHint" ) ) + { + event.m_attributeFlags |= CTFBot::TELEPORT_TO_HINT; + } + else if ( !Q_stricmp( value, "MiniBoss" ) ) + { + event.m_attributeFlags |= CTFBot::MINIBOSS; + } + else if ( !Q_stricmp( value, "UseBossHealthBar" ) ) + { + event.m_attributeFlags |= CTFBot::USE_BOSS_HEALTH_BAR; + } + else if ( !Q_stricmp( value, "IgnoreFlag" ) ) + { + event.m_attributeFlags |= CTFBot::IGNORE_FLAG; + } + else if ( !Q_stricmp( value, "AutoJump" ) ) + { + event.m_attributeFlags |= CTFBot::AUTO_JUMP; + } + else if ( !Q_stricmp( value, "AirChargeOnly" ) ) + { + event.m_attributeFlags |= CTFBot::AIR_CHARGE_ONLY; + } + else if( !Q_stricmp( value, "VaccinatorBullets" ) ) + { + event.m_attributeFlags |= CTFBot::PREFER_VACCINATOR_BULLETS; + } + else if( !Q_stricmp( value, "VaccinatorBlast" ) ) + { + event.m_attributeFlags |= CTFBot::PREFER_VACCINATOR_BLAST; + } + else if( !Q_stricmp( value, "VaccinatorFire" ) ) + { + event.m_attributeFlags |= CTFBot::PREFER_VACCINATOR_FIRE; + } + else if( !Q_stricmp( value, "BulletImmune" ) ) + { + event.m_attributeFlags |= CTFBot::BULLET_IMMUNE; + } + else if( !Q_stricmp( value, "BlastImmune" ) ) + { + event.m_attributeFlags |= CTFBot::BLAST_IMMUNE; + } + else if( !Q_stricmp( value, "FireImmune" ) ) + { + event.m_attributeFlags |= CTFBot::FIRE_IMMUNE; + } + else if ( !Q_stricmp( value, "Parachute" ) ) + { + event.m_attributeFlags |= CTFBot::PARACHUTE; + } + else if ( !Q_stricmp( value, "ProjectileShield" ) ) + { + event.m_attributeFlags |= CTFBot::PROJECTILE_SHIELD; + } + else + { + Warning( "TFBotSpawner: Invalid attribute '%s'\n", value ); + return false; + } + } + else if ( !Q_stricmp( name, "MaxVisionRange" ) ) + { + event.m_maxVisionRange = data->GetFloat(); + } + else if ( !Q_stricmp( name, "Item" ) ) + { + event.m_items.CopyAndAddToTail( value ); + } + else if ( !Q_stricmp( name, "ItemAttributes" ) ) + { + ParseItemAttributes( event, data ); + } + else if ( !Q_stricmp( name, "CharacterAttributes" ) ) + { + ParseCharacterAttributes( event, data ); + } + else if ( !Q_stricmp( name, "Tag" ) ) + { + event.m_tags.CopyAndAddToTail( value ); + } + else + { + return false; + } + + return true; +} + +//----------------------------------------------------------------------- +bool CTFBotSpawner::Parse( KeyValues *values ) +{ + // Reset All values + m_class = TF_CLASS_UNDEFINED; + m_iszClassIcon = NULL_STRING; + + m_health = -1; // default health + m_scale = -1.0f; // default scale + m_flAutoJumpMin = m_flAutoJumpMax = 0.f; // default AutoJumpMin/Max + + m_defaultAttributes.Reset(); + m_eventChangeAttributes.RemoveAll(); + + // First, see if we have any Template key + KeyValues *pTemplate = values->FindKey("Template"); + if ( pTemplate ) + { + KeyValues *pTemplateKV = GetPopulator()->GetManager()->GetTemplate( pTemplate->GetString() ); + if ( pTemplateKV ) + { + // Pump all the keys into ourself now + if ( Parse( pTemplateKV ) == false ) + { + return false; + } + } + else + { + Warning( "Unknown Template '%s' in TFBotSpawner definition\n", pTemplate->GetString() ); + } + } + + for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) + { + const char *name = data->GetName(); + const char *value = data->GetString(); + + if ( Q_strlen( name ) <= 0 ) + { + continue; + } + + // Skip templates when looping through the rest of the keys + if ( !Q_stricmp( name, "Template" ) ) + continue; + + if ( !Q_stricmp( name, "Class" ) ) + { + m_class = GetClassIndexFromString( value ); + if ( m_class == TF_CLASS_UNDEFINED ) + { + Warning( "TFBotSpawner: Invalid class '%s'\n", value ); + return false; + } + if ( m_name.IsEmpty() ) + { + m_name = value; + } + } + else if ( !Q_stricmp( name, "ClassIcon" ) ) + { + m_iszClassIcon = AllocPooledString( value ); + } + else if ( !Q_stricmp( name, "Health" ) ) + { + m_health = data->GetInt(); + } + else if ( !Q_stricmp( name, "Scale" ) ) + { + m_scale = data->GetFloat(); + } + else if ( !Q_stricmp( name, "Name" ) ) + { + m_name = value; + } + else if ( !Q_stricmp( name, "TeleportWhere" ) ) + { + m_teleportWhereName.CopyAndAddToTail( data->GetString() ); + } + else if ( !Q_stricmp( name, "AutoJumpMin" ) ) + { + m_flAutoJumpMin = data->GetFloat(); + } + else if ( !Q_stricmp( name, "AutoJumpMax" ) ) + { + m_flAutoJumpMax = data->GetFloat(); + } + else if ( !Q_stricmp( name, "EventChangeAttributes" ) ) + { + if ( !ParseEventChangeAttributes( data ) ) + { + Warning( "TFBotSpawner: Failed to parse EventChangeAttributes\n" ); + return false; + } + } + else if ( ParseDynamicAttributes( m_defaultAttributes, data ) ) + { + // Do nothing on success + } + else + { + Warning( "TFBotSpawner: Unknown field '%s'\n", name ); + return false; + } + } + + return true; +} + + +//----------------------------------------------------------------------- +bool CTFBotSpawner::ParseEventChangeAttributes( KeyValues *data ) +{ + for ( KeyValues *pKVEvent = data->GetFirstSubKey(); pKVEvent != NULL; pKVEvent = pKVEvent->GetNextKey() ) + { + const char* pszEventName = pKVEvent->GetName(); + + // Add new event + int index = m_eventChangeAttributes.AddToTail(); + CTFBot::EventChangeAttributes_t& event = m_eventChangeAttributes[index]; + event.m_eventName = pszEventName; + + for ( KeyValues *pAttr=pKVEvent->GetFirstSubKey(); pAttr != NULL; pAttr = pAttr->GetNextKey() ) + { + if ( !ParseDynamicAttributes( event, pAttr ) ) + { + Warning( "TFBotSpawner EventChangeAttributes: Failed to parse event '%s' with unknown attribute '%s'\n", pKVEvent->GetName(), pAttr->GetName() ); + return false; + } + } + + // should override default attr? + if ( !Q_stricmp( pszEventName, "default" ) ) + { + m_defaultAttributes = event; + } + } + + return true; +} + + +//----------------------------------------------------------------------- +bool CTFBotSpawner::Spawn( const Vector &rawHere, EntityHandleVector_t *result ) +{ + CETWScope timer( "CTFBotSpawner::Spawn" ); + VPROF_BUDGET( "CTFBotSpawner::Spawn", "NextBot" ); + + CTFBot *newBot = NULL; + + Vector here = rawHere; + + CTFNavArea *area = (CTFNavArea *)TheTFNavMesh()->GetNavArea( here ); + if ( area && area->HasAttributeTF( TF_NAV_NO_SPAWNING ) ) + { + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "CTFBotSpawner: %3.2f: *** Tried to spawn in a NO_SPAWNING area at (%f, %f, %f)\n", gpGlobals->curtime, here.x, here.y, here.z ); + } + return false; + } + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + // Only spawn bots while the round is running in MVM mode + if ( TFGameRules()->State_Get() != GR_STATE_RND_RUNNING ) + return false; + } + + // the ground may be variable here, try a few heights + float z; + for( z = 0.0f; z<StepHeight; z += 4.0f ) + { + here.z = rawHere.z + StepHeight; + + if ( IsSpaceToSpawnHere( here ) ) + { + break; + } + } + + if ( z >= StepHeight ) + { + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "CTFBotSpawner: %3.2f: *** No space to spawn at (%f, %f, %f)\n", gpGlobals->curtime, here.x, here.y, here.z ); + } + return false; + } + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + if ( m_class == TF_CLASS_ENGINEER && m_defaultAttributes.m_attributeFlags & CTFBot::TELEPORT_TO_HINT && CTFBotMvMEngineerHintFinder::FindHint( true, false ) == false ) + { + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "CTFBotSpawner: %3.2f: *** No teleporter hint for engineer\n", gpGlobals->curtime ); + } + + return false; + } + } + + + // find dead bot we can re-use + CTeam *deadTeam = GetGlobalTeam( TEAM_SPECTATOR ); + for( int i=0; i<deadTeam->GetNumPlayers(); ++i ) + { + if ( !deadTeam->GetPlayer(i)->IsBot() ) + continue; + + // reuse this guy + newBot = (CTFBot *)deadTeam->GetPlayer(i); + newBot->ClearAllAttributes(); + break; + } + + if ( newBot == NULL ) + { + //AssertMsg( 0, "Bots should be preallocated. This block of code should never get called.\n" ); + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + int nNumEnemyBots = 0; + + CUtlVector<CTFPlayer *> botVector; + CPopulationManager::CollectMvMBots( &botVector ); + + nNumEnemyBots = botVector.Count(); + + if ( nNumEnemyBots >= CPopulationManager::MVM_INVADERS_TEAM_SIZE ) + { + // no room for more + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "CTFBotSpawner: %3.2f: *** Can't spawn. Max number invaders already spawned.\n", gpGlobals->curtime ); + } + + // extra guard if we're over full on bots + if ( nNumEnemyBots > CPopulationManager::MVM_INVADERS_TEAM_SIZE ) + { + // Kick bots until we are at the proper number starting with spectator bots + int iNumberToKick = nNumEnemyBots - CPopulationManager::MVM_INVADERS_TEAM_SIZE; + int iKickedBots = 0; + + // loop through spectators and invaders in that order + // Kick Spectators first + CUtlVector<CTFPlayer *> botsToKick; + for ( int iTeam = 0; iTeam < 2; iTeam++ ) + { + int targetTeam = TEAM_SPECTATOR; + if ( iTeam == 1 ) + { + targetTeam = TF_TEAM_PVE_INVADERS; + } + + FOR_EACH_VEC( botVector, iBot ) + { + if ( iKickedBots >= iNumberToKick ) + break; + + if ( botVector[iBot]->GetTeamNumber() == targetTeam ) + { + botsToKick.AddToTail( botVector[iBot] ); + iKickedBots++; + } + } + } + + FOR_EACH_VEC ( botsToKick, iKick ) + { + engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", botsToKick[iKick]->GetUserID() ) ); + } + } + + return false; + } + } + else + { + // need to add another bot - is there room? + int totalPlayerCount = 0; + totalPlayerCount += GetGlobalTeam( TEAM_SPECTATOR )->GetNumPlayers(); + totalPlayerCount += GetGlobalTeam( TF_TEAM_BLUE )->GetNumPlayers(); + totalPlayerCount += GetGlobalTeam( TF_TEAM_RED )->GetNumPlayers(); + + if ( totalPlayerCount >= gpGlobals->maxClients ) + { + // no room for more + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "CTFBotSpawner: %3.2f: *** Can't spawn. No free player slot.\n", gpGlobals->curtime ); + } + return false; + } + } + + newBot = NextBotCreatePlayerBot< CTFBot >( "TFBot", false ); + } + + if ( newBot ) + { + // remove any player attributes + newBot->RemovePlayerAttributes( false ); + + // clear any old TeleportWhere settings + newBot->ClearTeleportWhere(); + + if ( g_internalSpawnPoint == NULL ) + { + g_internalSpawnPoint = (CPopulatorInternalSpawnPoint *)CreateEntityByName( "populator_internal_spawn_point" ); + g_internalSpawnPoint->Spawn(); + } + + // set name + engine->SetFakeClientConVarValue( newBot->edict(), "name", m_name.IsEmpty() ? "TFBot" : m_name.Get() ); + + g_internalSpawnPoint->SetAbsOrigin( here ); + g_internalSpawnPoint->SetLocalAngles( vec3_angle ); + newBot->SetSpawnPoint( g_internalSpawnPoint ); + + int team = TF_TEAM_RED; + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + team = TF_TEAM_PVE_INVADERS; + } + + newBot->ChangeTeam( team, false, true ); + + newBot->AllowInstantSpawn(); + newBot->HandleCommand_JoinClass( GetPlayerClassData( m_class )->m_szClassName ); + newBot->GetPlayerClass()->SetClassIconName( GetClassIcon() ); + + newBot->ClearEventChangeAttributes(); + for ( int i=0; i<m_eventChangeAttributes.Count(); ++i ) + { + newBot->AddEventChangeAttributes( &m_eventChangeAttributes[i] ); + } + + // Request to Add in Endless + if ( g_pPopulationManager->IsInEndlessWaves() ) + { + g_pPopulationManager->EndlessSetAttributesForBot( newBot ); + } + + newBot->SetTeleportWhere( m_teleportWhereName ); + + if ( m_defaultAttributes.m_attributeFlags & CTFBot::MINIBOSS ) + { + newBot->SetIsMiniBoss( true ); + } + + if ( m_defaultAttributes.m_attributeFlags & CTFBot::USE_BOSS_HEALTH_BAR ) + { + newBot->SetUseBossHealthBar( true ); + } + + if ( m_defaultAttributes.m_attributeFlags & CTFBot::AUTO_JUMP ) + { + newBot->SetAutoJump( m_flAutoJumpMin, m_flAutoJumpMax ); + } + + if( m_defaultAttributes.m_attributeFlags & CTFBot::BULLET_IMMUNE ) + { + newBot->m_Shared.AddCond( TF_COND_BULLET_IMMUNE ); + } + + if( m_defaultAttributes.m_attributeFlags & CTFBot::BLAST_IMMUNE ) + { + newBot->m_Shared.AddCond( TF_COND_BLAST_IMMUNE ); + } + + if( m_defaultAttributes.m_attributeFlags & CTFBot::FIRE_IMMUNE ) + { + newBot->m_Shared.AddCond( TF_COND_FIRE_IMMUNE ); + } + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + // initialize currency to be dropped on death to zero + newBot->SetCurrency( 0 ); + + // announce Spies + if ( m_class == TF_CLASS_SPY ) + { + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_PVE_INVADERS, COLLECT_ONLY_LIVING_PLAYERS ); + + int spyCount = 0; + for( int i=0; i<playerVector.Count(); ++i ) + { + if ( playerVector[i]->IsPlayerClass( TF_CLASS_SPY ) ) + { + ++spyCount; + } + } + + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_mission_update" ); + if ( event ) + { + event->SetInt( "class", TF_CLASS_SPY ); + event->SetInt( "count", spyCount ); + gameeventmanager->FireEvent( event ); + } + } + } + + newBot->SetScaleOverride( m_scale ); + + int nHealth = m_health; + + if ( nHealth <= 0.0f ) + { + nHealth = newBot->GetMaxHealth(); + } + + nHealth *= g_pPopulationManager->GetHealthMultiplier( false ); + newBot->ModifyMaxHealth( nHealth ); + + newBot->StartIdleSound(); + + // Add our items first, they'll get replaced below by the normal MvM items if any are needed + if ( TFGameRules()->IsMannVsMachineMode() && ( newBot->GetTeamNumber() == TF_TEAM_PVE_INVADERS ) ) + { + // Apply the Rome 2 promo items to each bot. They'll be + // filtered out for clients that do not have Romevision. + CMissionPopulator *pMission = dynamic_cast< CMissionPopulator* >( GetPopulator() ); + if ( pMission && ( pMission->GetMissionType() == CTFBot::MISSION_DESTROY_SENTRIES ) ) + { + newBot->AddItem( "tw_sentrybuster" ); + } + else + { + newBot->AddItem( g_szRomePromoItems_Hat[m_class] ); + newBot->AddItem( g_szRomePromoItems_Misc[m_class] ); + } + } + + // apply default attributes + const CTFBot::EventChangeAttributes_t* pEventChangeAttributes = newBot->GetEventChangeAttributes( g_pPopulationManager->GetDefaultEventChangeAttributesName() ); + if ( !pEventChangeAttributes ) + { + pEventChangeAttributes = &m_defaultAttributes; + } + newBot->OnEventChangeAttributes( pEventChangeAttributes ); + + CCaptureFlag *pFlag = newBot->GetFlagToFetch(); + if ( pFlag ) + { + newBot->SetFlagTarget( pFlag ); + } + + if ( newBot->HasAttribute( CTFBot::SPAWN_WITH_FULL_CHARGE ) ) + { + // charge up our weapons + + // Medigun Ubercharge + CWeaponMedigun *medigun = dynamic_cast< CWeaponMedigun * >( newBot->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) ); + if ( medigun ) + { + medigun->AddCharge( 1.0f ); + } + + newBot->m_Shared.SetRageMeter( 100.0f ); + } + + int nClassIndex = ( newBot->GetPlayerClass() ? newBot->GetPlayerClass()->GetClassIndex() : TF_CLASS_UNDEFINED ); + + if ( GetPopulator()->GetManager()->IsPopFileEventType( MVM_EVENT_POPFILE_HALLOWEEN ) ) + { + // zombies use the original player models + newBot->m_nSkin = 4; + + const char *name = g_aRawPlayerClassNamesShort[ nClassIndex ]; + + newBot->AddItem( CFmtStr( "Zombie %s", name ) ); + } + else + { + // use the nifty new robot model + if ( nClassIndex >= TF_CLASS_SCOUT && nClassIndex <= TF_CLASS_ENGINEER ) + { + if ( ( m_scale >= tf_mvm_miniboss_scale.GetFloat() || newBot->IsMiniBoss() ) && g_pFullFileSystem->FileExists( g_szBotBossModels[ nClassIndex ] ) ) + { + newBot->GetPlayerClass()->SetCustomModel( g_szBotBossModels[ nClassIndex ], USE_CLASS_ANIMATIONS ); + newBot->UpdateModel(); + newBot->SetBloodColor( DONT_BLEED ); + } + else if ( g_pFullFileSystem->FileExists( g_szBotModels[ nClassIndex ] ) ) + { + newBot->GetPlayerClass()->SetCustomModel( g_szBotModels[ nClassIndex ], USE_CLASS_ANIMATIONS ); + newBot->UpdateModel(); + newBot->SetBloodColor( DONT_BLEED ); + } + } + } + + if ( result ) + { + result->AddToTail( newBot ); + } + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + if ( newBot->IsMiniBoss() ) + { + TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_GIANT_CALLOUT, TF_TEAM_PVE_DEFENDERS ); + } + } + + if ( tf_populator_debug.GetBool() ) + { + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "%3.2f: Spawned TFBot '%s'\n", gpGlobals->curtime, m_name.IsEmpty() ? newBot->GetPlayerClass()->GetName() : m_name.Get() ); + } + } + } + else + { + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "CTFBotSpawner: %3.2f: *** Can't create TFBot to spawn.\n", gpGlobals->curtime ); + } + + return false; + } + + return true; +} + +//----------------------------------------------------------------------- +int CTFBotSpawner::GetClass( int nSpawnNum /*= -1*/ ) +{ + return m_class; +} + +//----------------------------------------------------------------------- +string_t CTFBotSpawner::GetClassIcon( int nSpawnNum /*= -1*/ ) +{ + if ( m_iszClassIcon != NULL_STRING ) + return m_iszClassIcon; + + return AllocPooledString( g_aRawPlayerClassNamesShort[ m_class ] ); +} + +//----------------------------------------------------------------------- +int CTFBotSpawner::GetHealth( int nSpawnNum /*= -1*/ ) +{ + return m_health; +} + +//----------------------------------------------------------------------- +bool CTFBotSpawner::IsMiniBoss( int nSpawnNum /*= -1*/ ) +{ + return ( m_defaultAttributes.m_attributeFlags & CTFBot::MINIBOSS ) != 0; +} + +//----------------------------------------------------------------------- +bool CTFBotSpawner::HasAttribute( CTFBot::AttributeType type, int nSpawnNum /*= -1*/ ) +{ + return ( m_defaultAttributes.m_attributeFlags & type ) != 0; +} + +//----------------------------------------------------------------------- +bool CTFBotSpawner::HasEventChangeAttributes( const char* pszEventName ) const +{ + for ( int i=0; i<m_eventChangeAttributes.Count(); ++i ) + { + if ( FStrEq( pszEventName, m_eventChangeAttributes[i].m_eventName ) ) + { + return true; + } + } + + return false; +} + + +//----------------------------------------------------------------------- +// CTankSpawner +//----------------------------------------------------------------------- +CTankSpawner::CTankSpawner( IPopulator *populator ) : IPopulationSpawner( populator ) +{ + m_health = 50000; + m_speed = 75; + m_name = "Tank"; + m_skin = 0; + m_startingPathTrackNodeName = NULL; + m_onKilledOutput = NULL; + m_onBombDroppedOutput = NULL; +} + + +//----------------------------------------------------------------------- +bool CTankSpawner::Parse( KeyValues *values ) +{ + for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) + { + const char *name = data->GetName(); + + if ( Q_strlen( name ) <= 0 ) + { + continue; + } + + if ( !Q_stricmp( name, "Health" ) ) + { + m_health = data->GetInt(); + } + else if ( !Q_stricmp( name, "Speed" ) ) + { + m_speed = data->GetFloat(); + } + else if ( !Q_stricmp( name, "Name" ) ) + { + m_name = data->GetString(); + } + else if ( !Q_stricmp( name, "Skin" ) ) + { + m_skin = data->GetInt(); + } + else if ( !Q_stricmp( name, "StartingPathTrackNode" ) ) + { + m_startingPathTrackNodeName = data->GetString(); + } + else if ( !Q_stricmp( name, "OnKilledOutput" ) ) + { + m_onKilledOutput = ParseEvent( data ); + } + else if ( !Q_stricmp( name, "OnBombDroppedOutput" ) ) + { + m_onBombDroppedOutput = ParseEvent( data ); + } + else + { + Warning( "Invalid attribute '%s' in Tank definition\n", name ); + return false; + } + } + + return true; +} + + +//----------------------------------------------------------------------- +bool CTankSpawner::Spawn( const Vector &here, EntityHandleVector_t *result ) +{ + CTFTankBoss *tank = (CTFTankBoss *)CreateEntityByName( "tank_boss" ); + if ( tank ) + { + tank->SetAbsOrigin( here ); + tank->SetAbsAngles( vec3_angle ); + + int nHealth = m_health * g_pPopulationManager->GetHealthMultiplier( true ); + tank->SetInitialHealth( nHealth ); + + tank->SetMaxSpeed( m_speed ); + tank->SetName( MAKE_STRING( m_name ) ); + tank->SetSkin( m_skin ); + tank->SetStartingPathTrackNode( m_startingPathTrackNodeName.GetForModify() ); + + tank->Spawn(); + + tank->DefineOnKilledOutput( m_onKilledOutput ); + tank->DefineOnBombDroppedOutput( m_onBombDroppedOutput ); + + if ( result ) + { + result->AddToTail( tank ); + } + + return true; + } + + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "CTankSpawner: %3.2f: Failed to create base_boss\n", gpGlobals->curtime ); + } + return false; +} + + +//----------------------------------------------------------------------- +//----------------------------------------------------------------------- +CSentryGunSpawner::CSentryGunSpawner( IPopulator *populator ) : IPopulationSpawner( populator ) +{ + m_level = 0; +} + + +//----------------------------------------------------------------------- +bool CSentryGunSpawner::Parse( KeyValues *values ) +{ + for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) + { + const char *name = data->GetName(); + + if ( Q_strlen( name ) <= 0 ) + { + continue; + } + + if ( !Q_stricmp( name, "Level" ) ) + { + m_level = data->GetInt(); + } + else + { + Warning( "Invalid attribute '%s' in SentryGun definition\n", name ); + return false; + } + } + + return true; +} + + +//----------------------------------------------------------------------- +bool CSentryGunSpawner::Spawn( const Vector &here, EntityHandleVector_t *result ) +{ + // directly create a sentry gun at the precise position and orientation desired + CObjectSentrygun *sentry = (CObjectSentrygun *)CreateEntityByName( "obj_sentrygun" ); + if ( sentry ) + { + sentry->SetAbsOrigin( here ); + sentry->SetAbsAngles( vec3_angle ); + + sentry->Spawn(); + sentry->ChangeTeam( TF_TEAM_RED ); + + sentry->m_nDefaultUpgradeLevel = m_level+1; + + sentry->InitializeMapPlacedObject(); + + if ( result ) + { + result->AddToTail( sentry ); + } + + return true; + } + + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "CSentryGunSpawner: %3.2f: Failed to create obj_sentrygun\n", gpGlobals->curtime ); + } + return false; +} + + +//----------------------------------------------------------------------- +//----------------------------------------------------------------------- +CSquadSpawner::CSquadSpawner( IPopulator *populator ) : IPopulationSpawner( populator ) +{ + m_memberSpawnerVector.RemoveAll(); + m_formationSize = -1.0f; + m_bShouldPreserveSquad = false; +} + + +//----------------------------------------------------------------------- +CSquadSpawner::~CSquadSpawner() +{ + for( int i=0; i<m_memberSpawnerVector.Count(); ++i ) + { + delete m_memberSpawnerVector[i]; + } + m_memberSpawnerVector.RemoveAll(); +} + + +//----------------------------------------------------------------------- +bool CSquadSpawner::Parse( KeyValues *values ) +{ + for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) + { + const char *name = data->GetName(); + + if ( V_strlen( name ) <= 0 ) + { + continue; + } + + if ( !V_stricmp( name, "FormationSize" ) ) + { + m_formationSize = data->GetFloat(); + continue; + } + else if ( !V_stricmp( name, "ShouldPreserveSquad" ) ) + { + m_bShouldPreserveSquad = data->GetBool(); + continue; + } + + // NOTE: It doesn't make sense for Squads to contain SentryGun or Mobs, but this + // allows for interesting trees of RandomChoice, etc. + IPopulationSpawner *spawner = ParseSpawner( GetPopulator(), data ); + + if ( spawner == NULL ) + { + Warning( "Unknown attribute '%s' in Squad definition.\n", name ); + } + else + { + m_memberSpawnerVector.AddToTail( spawner ); + } + } + + return true; +} + + +//----------------------------------------------------------------------- +bool CSquadSpawner::Spawn( const Vector &here, EntityHandleVector_t *result ) +{ + VPROF_BUDGET( "CSquadSpawner::Spawn", "NextBot" ); + + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "CSquadSpawner: %3.2f: <<<< Spawning Squad >>>>\n", gpGlobals->curtime ); + } + + // Is there enough slots to spawn? + CTeam *deadTeam = GetGlobalTeam( TEAM_SPECTATOR ); + if ( deadTeam->GetNumPlayers() < m_memberSpawnerVector.Count() ) + { + return false; + } + + // spawn all of the squad members + bool isComplete = true; + EntityHandleVector_t squadVector; + for( int i=0; i<m_memberSpawnerVector.Count(); ++i ) + { + if ( m_memberSpawnerVector[i]->Spawn( here, &squadVector ) == false ) + { + isComplete = false; + break; + } + } + + if ( !isComplete ) + { + // unable to spawn entire squad + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "%3.2f: CSquadSpawner: Unable to spawn entire squad\n", gpGlobals->curtime ); + } + + // unspawn partial squad + // TODO: Respect TFBot attributes + for( int i=0; i<squadVector.Count(); ++i ) + { + CTFPlayer *player = ToTFPlayer( squadVector[i] ); + + if ( player ) + { + player->ChangeTeam( TEAM_SPECTATOR, false, true ); + } + else + { + UTIL_Remove( squadVector[i] ); + } + } + + return false; + } + + // create the squad + CTFBotSquad *squad = new CTFBotSquad; + if ( squad ) + { + squad->SetFormationSize( m_formationSize ); + squad->SetShouldPreserveSquad( m_bShouldPreserveSquad ); + + for( int i=0; i<squadVector.Count(); ++i ) + { + CTFBot *bot = ToTFBot( squadVector[i] ); + if ( bot ) + { + bot->JoinSquad( squad ); + } + } + } + + if ( result ) + { + result->AddVectorToTail( squadVector ); + } + + return true; +} + +int CSquadSpawner::GetClass( int nSpawnNum /*= -1*/ ) +{ + if ( nSpawnNum < 0 || m_memberSpawnerVector.Count() == 0 ) + { + return TF_CLASS_UNDEFINED; + } + + int nSpawner = nSpawnNum % m_memberSpawnerVector.Count(); + + if ( !m_memberSpawnerVector[ nSpawner ]->IsVarious() ) + { + return m_memberSpawnerVector[ nSpawner ]->GetClass(); + } + else + { + // FIXME: Nested complex spawner types... need a method for counting these. + Assert( 1 ); + DevWarning( "Nested complex spawner types... need a method for counting these." ); + return TF_CLASS_UNDEFINED; + } +} + +string_t CSquadSpawner::GetClassIcon( int nSpawnNum /*= -1*/ ) +{ + if ( nSpawnNum < 0 || m_memberSpawnerVector.Count() == 0 ) + { + return NULL_STRING; + } + + int nSpawner = nSpawnNum % m_memberSpawnerVector.Count(); + + if ( !m_memberSpawnerVector[ nSpawner ]->IsVarious() ) + { + return m_memberSpawnerVector[ nSpawner ]->GetClassIcon(); + } + else + { + // FIXME: Nested complex spawner types... need a method for counting these. + Assert( 1 ); + DevWarning( "Nested complex spawner types... need a method for counting these." ); + return NULL_STRING; + } +} + +int CSquadSpawner::GetHealth( int nSpawnNum /*= -1*/ ) +{ + if ( nSpawnNum < 0 || m_memberSpawnerVector.Count() == 0 ) + { + return 0; + } + + int nSpawner = nSpawnNum % m_memberSpawnerVector.Count(); + + if ( !m_memberSpawnerVector[ nSpawner ]->IsVarious() ) + { + return m_memberSpawnerVector[ nSpawner ]->GetHealth(); + } + else + { + // FIXME: Nested complex spawner types... need a method for counting these. + Assert( 1 ); + DevWarning( "Nested complex spawner types... need a method for counting these." ); + return 0; + } +} + +bool CSquadSpawner::IsMiniBoss( int nSpawnNum /*= -1*/ ) +{ + if ( nSpawnNum < 0 || m_memberSpawnerVector.Count() == 0 ) + { + return false; + } + + int nSpawner = nSpawnNum % m_memberSpawnerVector.Count(); + + if ( !m_memberSpawnerVector[ nSpawner ]->IsVarious() ) + { + return m_memberSpawnerVector[ nSpawner ]->IsMiniBoss(); + } + else + { + // FIXME: Nested complex spawner types... need a method for counting these. + Assert( 1 ); + DevWarning( "Nested complex spawner types... need a method for counting these." ); + return false; + } +} + +bool CSquadSpawner::HasAttribute( CTFBot::AttributeType type, int nSpawnNum /*= -1*/ ) +{ + if ( nSpawnNum < 0 || m_memberSpawnerVector.Count() == 0 ) + { + return false; + } + + int nSpawner = nSpawnNum % m_memberSpawnerVector.Count(); + + if ( !m_memberSpawnerVector[ nSpawner ]->IsVarious() ) + { + return m_memberSpawnerVector[ nSpawner ]->HasAttribute( type, nSpawnNum ); + } + else + { + // FIXME: Nested complex spawner types... need a method for counting these. + Assert( 1 ); + DevWarning( "Nested complex spawner types... need a method for counting these." ); + return false; + } +} + +bool CSquadSpawner::HasEventChangeAttributes( const char* pszEventName ) const +{ + for ( int i=0; i<m_memberSpawnerVector.Count(); ++i ) + { + if ( m_memberSpawnerVector[i]->HasEventChangeAttributes( pszEventName ) ) + { + return true; + } + } + + return false; +} + +//----------------------------------------------------------------------- +//----------------------------------------------------------------------- +CMobSpawner::CMobSpawner( IPopulator *populator ) : IPopulationSpawner( populator ) +{ + m_count = 0; + m_spawner = NULL; + + m_mobSpawnTimer.Invalidate(); + m_mobLifetimeTimer.Invalidate(); + m_mobArea = NULL; + m_mobCountRemaining = 0; +} + + +//----------------------------------------------------------------------- +CMobSpawner::~CMobSpawner() +{ + if ( m_spawner ) + { + delete m_spawner; + } + m_spawner = NULL; +} + + +//----------------------------------------------------------------------- +bool CMobSpawner::Parse( KeyValues *values ) +{ + for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) + { + const char *name = data->GetName(); + + if ( Q_strlen( name ) <= 0 ) + { + continue; + } + + if ( !Q_stricmp( name, "Count" ) ) + { + m_count = data->GetInt(); + } + else + { + // NOTE: It doesn't make sense for Mobs to contain SentryGuns, but this + // allows for interesting trees of RandomChoice, etc. + IPopulationSpawner *spawner = ParseSpawner( GetPopulator(), data ); + + if ( spawner && m_spawner ) + { + Warning( "CMobSpawner: Duplicate spawner encountered - discarding!\n" ); + delete spawner; + } + else if ( spawner == NULL ) + { + Warning( "Unknown attribute '%s' in Mob definition.\n", name ); + } + else + { + m_spawner = spawner; + } + } + } + + return true; +} + + +//----------------------------------------------------------------------- +bool CMobSpawner::Spawn( const Vector &here, EntityHandleVector_t *result ) +{ + if ( m_spawner == NULL ) + return false; + + // spawn the mob + for( int i=0; i<m_count; ++i ) + { + if ( m_spawner->Spawn( here, result ) == false ) + { + return false; + } + } + + return true; +} + + +//----------------------------------------------------------------------- +bool CMobSpawner::HasEventChangeAttributes( const char* pszEventName ) const +{ + if ( m_spawner == NULL ) + return false; + + return m_spawner->HasEventChangeAttributes( pszEventName ); +} diff --git a/game/server/tf/player_vs_environment/tf_populator_spawners.h b/game/server/tf/player_vs_environment/tf_populator_spawners.h new file mode 100644 index 0000000..471168a --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_populator_spawners.h @@ -0,0 +1,283 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: tf_populator_spawners +// Implementations of NPC Spawning Code for PvE related game modes (MvM) +//=============================================================================// +#ifndef TF_POPULATOR_SPAWNERS_H +#define TF_POPULATOR_SPAWNERS_H + + +#include "bot/tf_bot.h" +#include "tf_mann_vs_machine_stats.h" + +class CMannVsMachineStats; +class KeyValues; +class IPopulator; +class CPopulationManager; +class CWave; + +enum RelativePositionType +{ + UNDEFINED = 0, + AHEAD, + BEHIND, + ANYWHERE +}; + +struct EventInfo +{ + CFmtStr m_target; + CFmtStr m_action; +}; + +enum SpawnLocationResult +{ + SPAWN_LOCATION_NOT_FOUND = 0, + SPAWN_LOCATION_NAV, + SPAWN_LOCATION_TELEPORTER +}; + +typedef CUtlVector< CHandle< CBaseEntity > > EntityHandleVector_t; +typedef CUtlVector< CHandle< CTFTeamSpawn > > TFTeamSpawnVector_t; + +//-------------------------------------------------------------------------------------------------------- +// +// Return a random value with a distribution like so: +// 1| / +// | / +// |/ +// /---- +// 0 1 +inline float SkewedRandomValue( void ) +{ + float x = RandomFloat( 0, 1.0f ); + float y = RandomFloat( 0, 1.0f ); + return x < y ? y : x; +} + +//----------------------------------------------------------------------- +class CMvMBotUpgrade +{ +public: + char szAttrib[ MAX_ATTRIBUTE_DESCRIPTION_LENGTH ]; // Debug + int iAttribIndex; + float flValue; + float flMax; + int nCost; + bool bIsBotAttr; + bool bIsSkillAttr; // Probably want to make these an enum or flag later +}; + +//----------------------------------------------------------------------- +class CTFNavAreaIncursionLess +{ +public: + bool Less( const CTFNavArea *a, const CTFNavArea *b, void *pCtx ) + { + return a->GetIncursionDistance( TF_TEAM_BLUE ) < b->GetIncursionDistance( TF_TEAM_BLUE ); + } +}; + +//----------------------------------------------------------------------- +// A Spawner is responsible for actually spawning a particular +// instance of an entity into the environment. +// If 'result' is non-NULL, spawned entities are added to this vector. +class IPopulationSpawner +{ +public: + // We need a virtual destructor or else the derived-class destructors won't be called, + // leading to memory leaks. Found via clang warning. + virtual ~IPopulationSpawner() + { + } + + IPopulationSpawner( IPopulator *populator ) + { + m_populator = populator; + } + + IPopulator *GetPopulator( void ) const + { + return m_populator; + } + + virtual bool Parse( KeyValues *data ) = 0; + virtual bool Spawn( const Vector &here, EntityHandleVector_t *result = NULL ) = 0; + virtual bool IsWhereRequired( void ) const // does this spawner need a valid Where parameter? + { + return true; + } + + virtual bool IsVarious( void ) { return false; } + virtual int GetClass( int nSpawnNum = -1 ) { return TF_CLASS_UNDEFINED; } + virtual string_t GetClassIcon( int nSpawnNum = -1 ) { return NULL_STRING; } + virtual int GetHealth( int nSpawnNum = -1 ){ return 0; } + virtual bool IsMiniBoss( int nSpawnNum = -1 ) { return false; } + virtual bool HasAttribute( CTFBot::AttributeType type, int nSpawnNum = -1 ) { return false; } + virtual bool HasEventChangeAttributes( const char* pszEventName ) const = 0; + + static IPopulationSpawner *ParseSpawner( IPopulator *populator, KeyValues *data ); + +protected: + IPopulator *m_populator; +}; + + +//----------------------------------------------------------------------- +// A RandomChoice spawner picks one of the Spawners in its +// vector at random and invokes it to spawn entities. +class CRandomChoiceSpawner : public IPopulationSpawner +{ +public: + CRandomChoiceSpawner( IPopulator *populator ); + virtual ~CRandomChoiceSpawner(); + + virtual bool Parse( KeyValues *data ); + virtual bool Spawn( const Vector &here, CUtlVector< CHandle< CBaseEntity > > *result = NULL ); + + virtual bool IsVarious( void ) { return true; } + virtual int GetClass( int nSpawnNum = -1 ); + virtual string_t GetClassIcon( int nSpawnNum = -1 ); + virtual int GetHealth( int nSpawnNum = -1 ); + virtual bool IsMiniBoss( int nSpawnNum = -1 ) OVERRIDE; + virtual bool HasAttribute( CTFBot::AttributeType type, int nSpawnNum = -1 ); + + virtual bool HasEventChangeAttributes( const char* pszEventName ) const OVERRIDE; + + CUtlVector< IPopulationSpawner * > m_spawnerVector; + CUtlVector< int > m_nRandomPickDecision; + + int m_nNumSpawned; +}; + + +//----------------------------------------------------------------------- +class CTFBotSpawner : public IPopulationSpawner +{ +public: + CTFBotSpawner( IPopulator *populator ); + virtual ~CTFBotSpawner() { } + + virtual bool Parse( KeyValues *data ); + bool ParseEventChangeAttributes( KeyValues *data ); + virtual bool Spawn( const Vector &here, EntityHandleVector_t *result = NULL ); + + virtual int GetClass( int nSpawnNum = -1 ); + virtual string_t GetClassIcon( int nSpawnNum = -1 ); + virtual int GetHealth( int nSpawnNum = -1 ); + virtual bool IsMiniBoss( int nSpawnNum = -1 ) OVERRIDE; + virtual bool HasAttribute( CTFBot::AttributeType type, int nSpawnNum = -1 ); + + virtual bool HasEventChangeAttributes( const char* pszEventName ) const OVERRIDE; + + int m_class; + string_t m_iszClassIcon; + + int m_health; + float m_scale; + float m_flAutoJumpMin; + float m_flAutoJumpMax; + + CUtlString m_name; + CUtlStringList m_teleportWhereName; + + CTFBot::EventChangeAttributes_t m_defaultAttributes; + CUtlVector< CTFBot::EventChangeAttributes_t > m_eventChangeAttributes; +}; + +//----------------------------------------------------------------------- +class CTankSpawner : public IPopulationSpawner +{ +public: + CTankSpawner( IPopulator *populator ); + + virtual string_t GetClassIcon( int nSpawnNum = -1 ) { return MAKE_STRING( "tank" ); } + virtual int GetHealth( int nSpawnNum = -1 ){ return m_health; } + + virtual bool Parse( KeyValues *data ); + virtual bool Spawn( const Vector &here, EntityHandleVector_t *result = NULL ); + + virtual bool IsWhereRequired( void ) const // does this spawner need a valid Where parameter? + { + // the Tank spawns at a given path node + return false; + } + + virtual bool IsMiniBoss( int nSpawnNum = -1 ) OVERRIDE { return true; } + + virtual bool HasEventChangeAttributes( const char* pszEventName ) const OVERRIDE { return false; } + + int m_health; + float m_speed; + CUtlString m_name; + CUtlString m_startingPathTrackNodeName; // which path_track we start at + int m_skin; + EventInfo *m_onKilledOutput; + EventInfo *m_onBombDroppedOutput; +}; + +//----------------------------------------------------------------------- +class CSentryGunSpawner : public IPopulationSpawner +{ +public: + CSentryGunSpawner( IPopulator *populator ); + + virtual bool Parse( KeyValues *data ); + virtual bool Spawn( const Vector &here, EntityHandleVector_t *result = NULL ); + + virtual bool HasEventChangeAttributes( const char* pszEventName ) const OVERRIDE { return false; } + + int m_level; +}; + + +//----------------------------------------------------------------------- +class CSquadSpawner : public IPopulationSpawner +{ +public: + CSquadSpawner( IPopulator *populator ); + virtual ~CSquadSpawner(); + + virtual bool Parse( KeyValues *data ); + virtual bool Spawn( const Vector &here, EntityHandleVector_t *result = NULL ); + + virtual bool IsVarious( void ) { return true; } + virtual int GetClass( int nSpawnNum = -1 ); + virtual string_t GetClassIcon( int nSpawnNum = -1 ); + virtual int GetHealth( int nSpawnNum = -1 ); + virtual bool IsMiniBoss( int nSpawnNum = -1 ) OVERRIDE; + virtual bool HasAttribute( CTFBot::AttributeType type, int nSpawnNum = -1 ); + + virtual bool HasEventChangeAttributes( const char* pszEventName ) const OVERRIDE; + + CUtlVector< IPopulationSpawner * > m_memberSpawnerVector; // all of these are invoked to instantiate the squad + + float m_formationSize; + bool m_bShouldPreserveSquad; +}; + + +//----------------------------------------------------------------------- +class CMobSpawner : public IPopulationSpawner +{ +public: + CMobSpawner( IPopulator *populator ); + virtual ~CMobSpawner(); + + virtual bool Parse( KeyValues *data ); + virtual bool Spawn( const Vector &here, EntityHandleVector_t *result = NULL ); + virtual bool HasEventChangeAttributes( const char* pszEventName ) const OVERRIDE; + + int m_count; + IPopulationSpawner *m_spawner; + +private: + // TODO: Rethink this, since spawners are one-shot, and mobs need to spawn over time... + CountdownTimer m_mobSpawnTimer; + CountdownTimer m_mobLifetimeTimer; + CTFNavArea *m_mobArea; + int m_mobCountRemaining; +}; + + +#endif // TF_POPULATOR_SPAWNERS_H diff --git a/game/server/tf/player_vs_environment/tf_populators.cpp b/game/server/tf/player_vs_environment/tf_populators.cpp new file mode 100644 index 0000000..3e0c5e8 --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_populators.cpp @@ -0,0 +1,2324 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: tf_populator_spawners +// Implementations of NPC Spawning Code for PvE related game modes (MvM) +//=============================================================================// + +#include "cbase.h" + +#include "tf_populators.h" +#include "tf_populator_spawners.h" +#include "tf_team.h" +#include "tf_obj_sentrygun.h" +#include "tf_objective_resource.h" +#include "eventqueue.h" +#include "tf_tank_boss.h" +#include "tf_gc_server.h" +#include "tf_gamerules.h" +#include "etwprof.h" +#include "team_control_point_master.h" + +extern ConVar tf_populator_debug; +extern ConVar tf_populator_active_buffer_range; + +ConVar tf_mvm_engineer_teleporter_uber_duration( "tf_mvm_engineer_teleporter_uber_duration", "5.f", FCVAR_CHEAT ); +ConVar tf_mvm_currency_bonus_ratio_min( "tf_mvm_currency_bonus_ratio_min", "0.95f", FCVAR_HIDDEN, "The minimum percentage of wave money players must collect in order to qualify for min bonus - 0.1 to 1.0. Half the bonus amount will be awarded when reaching min ratio, and half when reaching max.", true, 0.1, true, 1.0 ); +ConVar tf_mvm_currency_bonus_ratio_max( "tf_mvm_currency_bonus_ratio_max", "1.f", FCVAR_HIDDEN, "The highest percentage of wave money players must collect in order to qualify for max bonus - 0.1 to 1.0. Half the bonus amount will be awarded when reaching min ratio, and half when reaching max.", true, 0.1, true, 1.0 ); + +//----------------------------------------------------------------------- +static void FireEvent( EventInfo *eventInfo, const char *eventName ) +{ + if ( eventInfo ) + { + CBaseEntity *targetEntity = gEntList.FindEntityByName( NULL, eventInfo->m_target ); + if ( !targetEntity ) + { + Warning( "WaveSpawnPopulator: Can't find target entity '%s' for %s\n", eventInfo->m_target.Access(), eventName ); + } + else + { + g_EventQueue.AddEvent( targetEntity, eventInfo->m_action, 0.0f, NULL, NULL ); + } + } +} + +//----------------------------------------------------------------------- +static EventInfo *ParseEvent( KeyValues *values ) +{ + EventInfo *eventInfo = new EventInfo; + + for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) + { + const char *name = data->GetName(); + + if ( Q_strlen( name ) <= 0 ) + { + continue; + } + + if ( !Q_stricmp( name, "Target" ) ) + { + eventInfo->m_target.sprintf( "%s", data->GetString() ); + } + else if ( !Q_stricmp( name, "Action" ) ) + { + eventInfo->m_action.sprintf( "%s", data->GetString() ); + } + else + { + Warning( "Unknown field '%s' in WaveSpawn event definition.\n", data->GetString() ); + delete eventInfo; + return NULL; + } + } + + return eventInfo; +} + +static CHandle<CBaseEntity> s_lastTeleporter = NULL; +static float s_flLastTeleportTime = -1; + +//----------------------------------------------------------------------- +// Given a named entity, select a random invader teleporter with the same name and +// return it's WorldSpaceCenter. +SpawnLocationResult DoTeleporterOverride( CBaseEntity *spawnEnt, Vector& vSpawnPosition, bool bClosestPointOnNav ) +{ + CUtlVector< CBaseEntity * > teleporterVector; + + for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i ) + { + CBaseObject *pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] ); + if ( pObj->GetType() != OBJ_TELEPORTER ) + continue; + + if ( pObj->GetTeamNumber() != TF_TEAM_PVE_INVADERS ) + continue; + + if ( pObj->IsBuilding() ) + continue; + + if ( pObj->HasSapper() ) + continue; + + if ( pObj->IsPlasmaDisabled() ) + continue; + + CObjectTeleporter *teleporter = assert_cast< CObjectTeleporter* >( pObj ); + const CUtlStringList& teleportWhereNames = teleporter->GetTeleportWhere(); + + const char* pszSpawnPointName = STRING( spawnEnt->GetEntityName() ); + for ( int iTelePoints =0; iTelePoints<teleportWhereNames.Count(); ++iTelePoints ) + { + // check if this teleporter can replace the original spawn point + if ( FStrEq( teleportWhereNames[iTelePoints], pszSpawnPointName ) ) + { + teleporterVector.AddToTail( teleporter ); + break; + } + } + } + + if ( teleporterVector.Count() > 0 ) + { + int which = RandomInt( 0, teleporterVector.Count()-1 ); + vSpawnPosition = teleporterVector[ which ]->WorldSpaceCenter(); + s_lastTeleporter = teleporterVector[ which ]; + return SPAWN_LOCATION_TELEPORTER; + } + + CTFNavArea *pNav = (CTFNavArea *)TheNavMesh->GetNearestNavArea( spawnEnt->WorldSpaceCenter() ); + if ( !pNav ) + return SPAWN_LOCATION_NOT_FOUND; + + if ( bClosestPointOnNav ) + { + pNav->GetClosestPointOnArea( spawnEnt->WorldSpaceCenter(), &vSpawnPosition ); + } + else + { + vSpawnPosition = pNav->GetCenter(); + } + + return SPAWN_LOCATION_NAV; +} + +//----------------------------------------------------------------------- +void OnBotTeleported( CTFBot* bot ) +{ + const Vector& origin = s_lastTeleporter->GetAbsOrigin(); + + // don't too many sound and effect when lots of bots teleporting in short time. + if ( gpGlobals->curtime - s_flLastTeleportTime > 0.1f ) + { + CPVSFilter filter( origin ); +#if 0 + // These are pretty, but they're chewing into our particle budget (1000 particles each!) + // They're also basically invisible because bots spawn in ubered. + + TE_TFParticleEffect( filter, 0.0, "teleported_blue", origin, vec3_angle ); + TE_TFParticleEffect( filter, 0.0, "player_sparkles_blue", origin, vec3_angle ); +#endif + s_lastTeleporter->EmitSound( "MVM.Robot_Teleporter_Deliver" ); + + s_flLastTeleportTime = gpGlobals->curtime; + } + + // force bot to face in the direction specified by the teleporter + Vector vForward; + AngleVectors( s_lastTeleporter->GetAbsAngles(), &vForward, NULL, NULL ); + bot->GetLocomotionInterface()->FaceTowards( bot->GetAbsOrigin() + 50 * vForward ); + + // spy shouldn't get any effect from the teleporter + if ( !bot->IsPlayerClass( TF_CLASS_SPY ) ) + { + bot->TeleportEffect(); + + // invading bots get uber while they leave their spawn so they don't drop their cash where players can't pick it up + float flUberTime = tf_mvm_engineer_teleporter_uber_duration.GetFloat(); + bot->m_Shared.AddCond( TF_COND_INVULNERABLE, flUberTime ); + bot->m_Shared.AddCond( TF_COND_INVULNERABLE_WEARINGOFF, flUberTime ); + } +} + +//----------------------------------------------------------------------- +// CSpawnLocation +//----------------------------------------------------------------------- +CSpawnLocation::CSpawnLocation() +{ + m_relative = UNDEFINED; + m_teamSpawnVector.RemoveAll(); + m_nSpawnCount = 0; + m_nRandomSeed = RandomInt( 0, 9999 ); + m_bClosestPointOnNav = false; +} + +//----------------------------------------------------------------------- +// Return true if we successfully parse a "Where" clause +bool CSpawnLocation::Parse( KeyValues *data ) +{ + const char *name = data->GetName(); + const char *value = data->GetString(); + + if ( Q_strlen( name ) <= 0 ) + { + return false; + } + + if ( FStrEq( name, "Where" ) || FStrEq( name, "ClosestPoint" ) ) + { + if ( FStrEq( value, "Ahead" ) ) + { + m_relative = AHEAD; + } + else if ( FStrEq( value, "Behind" ) ) + { + m_relative = BEHIND; + } + else if ( FStrEq( value, "Anywhere" ) ) + { + m_relative = ANYWHERE; + } + else + { + m_bClosestPointOnNav = FStrEq( name, "ClosestPoint" ); + + // collect entities with given name + bool bFound = false; + for ( int i=0; i<ITFTeamSpawnAutoList::AutoList().Count(); ++i ) + { + CTFTeamSpawn* pTeamSpawn = static_cast< CTFTeamSpawn* >( ITFTeamSpawnAutoList::AutoList()[i] ); + if ( FStrEq( STRING( pTeamSpawn->GetEntityName() ), value ) ) + { + m_teamSpawnVector.AddToTail( pTeamSpawn ); + bFound = true; + } + } + + if ( !bFound ) + { + Warning( "Invalid Where argument '%s'\n", value ); + return false; + } + } + + return true; + } + + return false; +} + +//----------------------------------------------------------------------- +SpawnLocationResult CSpawnLocation::FindSpawnLocation( Vector& vSpawnPosition ) +{ + TFTeamSpawnVector_t activeSpawn; + for ( int i=0; i<m_teamSpawnVector.Count(); ++i ) + { + if ( m_teamSpawnVector[i]->IsDisabled() ) + continue; + + activeSpawn.AddToTail( m_teamSpawnVector[i] ); + } + + // treat spawn points as deck of cards. shuffle it when we run out + if ( m_nSpawnCount >= activeSpawn.Count() ) + { + m_nRandomSeed = RandomInt( 0, 9999 ); + m_nSpawnCount = 0; + } + CUniformRandomStream randomSpawn; + randomSpawn.SetSeed( m_nRandomSeed ); + activeSpawn.Shuffle( &randomSpawn ); + + if ( activeSpawn.Count() > 0 ) + { + // if any invading teleporters exist with this name, use them instead + SpawnLocationResult result = DoTeleporterOverride( activeSpawn[ m_nSpawnCount ], vSpawnPosition, m_bClosestPointOnNav ); + if ( result != SPAWN_LOCATION_NOT_FOUND ) + { + m_nSpawnCount++; + return result; + } + } + + CTFNavArea *spawnArea = SelectSpawnArea(); + if ( spawnArea ) + { + vSpawnPosition = spawnArea->GetCenter(); + return SPAWN_LOCATION_NAV; + } + + return SPAWN_LOCATION_NOT_FOUND; +} + +//----------------------------------------------------------------------- +CTFNavArea *CSpawnLocation::SelectSpawnArea( void ) const +{ + VPROF_BUDGET( "CSpawnLocation::SelectSpawnArea", "NextBot" ); + + if ( m_relative == UNDEFINED ) + { + return NULL; + } + +#ifdef TF_RAID_MODE + CTFPlayer *farRaider = g_pRaidLogic->GetFarthestAlongRaider(); + + if ( !farRaider ) + { + return NULL; + } +#endif // TF_RAID_MODE + + // + // Collect all areas surrounding the invading team and + // build a vector sorted by increasing incursion distance + // + CUtlSortVector< CTFNavArea *, CTFNavAreaIncursionLess > theaterAreaVector; + + CTFNavArea::MakeNewTFMarker(); + + CTeam *team = GetGlobalTeam( TF_TEAM_BLUE ); + for( int t=0; t<team->GetNumPlayers(); ++t ) + { + CTFPlayer *teamMember = (CTFPlayer *)team->GetPlayer(t); + + if ( !teamMember->IsAlive() ) + continue; + + CTFBot *bot = ToTFBot( teamMember ); + if ( bot && bot->HasAttribute( CTFBot::IS_NPC ) ) + continue; + + if ( teamMember->GetLastKnownArea() == NULL ) + continue; + + // collect areas surrounding this invader + CUtlVector< CNavArea * > nearbyAreaVector; + CollectSurroundingAreas( &nearbyAreaVector, teamMember->GetLastKnownArea(), tf_populator_active_buffer_range.GetFloat() ); + + for( int i=0; i<nearbyAreaVector.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)nearbyAreaVector[i]; + + if ( !area->IsTFMarked() ) + { + area->TFMark(); + + if ( area->IsPotentiallyVisibleToTeam( TF_TEAM_BLUE ) ) + continue; + + if ( !area->IsValidForWanderingPopulation() ) + continue; + + theaterAreaVector.Insert( area ); + + if ( tf_populator_debug.GetBool() ) + { + TheTFNavMesh()->AddToSelectedSet( area ); + } + } + } + } + + if ( theaterAreaVector.Count() == 0 ) + { + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "%3.2f: SelectSpawnArea: Empty theater!\n", gpGlobals->curtime ); + } + return NULL; + } + + const int maxRetries = 5; + CTFNavArea *spawnArea = NULL; + + for( int r=0; r<maxRetries; ++r ) + { + int which = 0; + + switch( m_relative ) + { + case AHEAD: + // areas are sorted from behind to ahead - weight the selection to choose ahead + which = SkewedRandomValue() * theaterAreaVector.Count(); + break; + + case BEHIND: + // areas are sorted from behind to ahead - weight the selection to choose behind + which = ( 1.0f - SkewedRandomValue() ) * theaterAreaVector.Count(); + break; + + case ANYWHERE: + // choose any valid area at random + which = RandomFloat( 0.0f, 1.0f ) * theaterAreaVector.Count(); + break; + } + + if ( which >= theaterAreaVector.Count() ) + which = theaterAreaVector.Count()-1; + + spawnArea = theaterAreaVector[ which ]; + + // well behaved spawn area + return spawnArea; + + } + + return NULL; +} + +//----------------------------------------------------------------------- +// CMissionPopulator +//----------------------------------------------------------------------- +CMissionPopulator::CMissionPopulator( CPopulationManager *manager ) : IPopulator( manager ) +{ + m_mission = CTFBot::NO_MISSION; + m_initialCooldown = 0.0f; + m_cooldownDuration = 0.0f; + m_desiredCount = 0; + m_beginAtWaveIndex = 0; + m_stopAtWaveIndex = 99999; + m_state = NOT_STARTED; +} + + +//----------------------------------------------------------------------- +bool CMissionPopulator::Parse( KeyValues *values ) +{ + int waveDuration = 99999; + + for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) + { + const char *name = data->GetName(); + + if ( Q_strlen( name ) <= 0 ) + { + continue; + } + + if ( m_where.Parse( data ) ) + { + continue; + } + + if ( !Q_stricmp( name, "Objective" ) ) + { + if ( !Q_stricmp( data->GetString(), "DestroySentries" ) ) + { + m_mission = CTFBot::MISSION_DESTROY_SENTRIES; + } + else if ( !Q_stricmp( data->GetString(), "Sniper" ) ) + { + m_mission = CTFBot::MISSION_SNIPER; + } + else if ( !Q_stricmp( data->GetString(), "Spy" ) ) + { + m_mission = CTFBot::MISSION_SPY; + } + else if ( !Q_stricmp( data->GetString(), "Engineer" ) ) + { + m_mission = CTFBot::MISSION_ENGINEER; + } + else if ( !Q_stricmp( data->GetString(), "SeekAndDestroy" ) ) + { + m_mission = CTFBot::MISSION_DESTROY_SENTRIES; + } + else + { + Warning( "Invalid mission '%s'\n", data->GetString() ); + return false; + } + } + else if ( !Q_stricmp( name, "InitialCooldown" ) ) + { + m_initialCooldown = data->GetFloat(); + } + else if ( !Q_stricmp( name, "CooldownTime" ) ) + { + m_cooldownDuration = data->GetFloat(); + } + else if ( !Q_stricmp( name, "BeginAtWave" ) ) + { + m_beginAtWaveIndex = data->GetInt() - 1; // internally counts from 0 + } + else if ( !Q_stricmp( name, "RunForThisManyWaves" ) ) + { + waveDuration = data->GetInt(); + } + else if ( !Q_stricmp( name, "DesiredCount" ) ) + { + m_desiredCount = data->GetInt(); + } + else + { + m_spawner = IPopulationSpawner::ParseSpawner( this, data ); + + if ( m_spawner == NULL ) + { + Warning( "Unknown attribute '%s' in Mission definition.\n", name ); + } + } + } + + m_stopAtWaveIndex = m_beginAtWaveIndex + waveDuration; + + return true; +} + + +//-------------------------------------------------------------------------------------------------------- +// Dispatch sentry killer squads +bool CMissionPopulator::UpdateMissionDestroySentries( void ) +{ + VPROF_BUDGET( "CMissionPopulator::UpdateMissionDestroySentries", "NextBot" ); + + if ( !m_cooldownTimer.IsElapsed() ) + { + return false; + } + + if ( !m_checkForDangerousSentriesTimer.IsElapsed() ) + { + return false; + } + + if( g_pPopulationManager->IsSpawningPaused() ) + { + return false; + } + + m_checkForDangerousSentriesTimer.Start( RandomFloat( 5.0f, 10.0f ) ); + + // collect all of the dangerous sentries + CUtlVector< CObjectSentrygun * > dangerousSentryVector; + + int nDmgLimit = 0; + int nKillLimit = 0; + GetManager()->GetSentryBusterDamageAndKillThreshold( nDmgLimit, nKillLimit ); + + for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i ) + { + CBaseObject* pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] ); + if ( pObj->ObjectType() == OBJ_SENTRYGUN ) + { + // Disposable sentries are not valid targets + if ( pObj->IsDisposableBuilding() ) + continue; + + if ( pObj->GetTeamNumber() == TF_TEAM_PVE_DEFENDERS ) + { + CTFPlayer *sentryOwner = pObj->GetOwner(); + if ( sentryOwner ) + { + int nDmgDone = sentryOwner->GetAccumulatedSentryGunDamageDealt(); + int nKillsMade = sentryOwner->GetAccumulatedSentryGunKillCount(); + + if ( nDmgDone >= nDmgLimit || nKillsMade >= nKillLimit ) + { + dangerousSentryVector.AddToTail( static_cast< CObjectSentrygun* >( pObj ) ); + } + } + } + } + } + + CUtlVector< CTFPlayer * > livePlayerVector; + CollectPlayers( &livePlayerVector, TF_TEAM_PVE_INVADERS, COLLECT_ONLY_LIVING_PLAYERS ); + + // dispatch a sentry busting squad for each dangerous sentry + bool didSpawn = false; + + for( int i=0; i<dangerousSentryVector.Count(); ++i ) + { + CObjectSentrygun *targetSentry = dangerousSentryVector[i]; + + // if there is already a squad out there destroying this sentry, don't spawn another one + int j; + for( j=0; j<livePlayerVector.Count(); ++j ) + { + CTFBot *bot = dynamic_cast<CTFBot *>( livePlayerVector[j] ); + if ( bot && bot->HasMission( CTFBot::MISSION_DESTROY_SENTRIES ) && bot->GetMissionTarget() == targetSentry ) + { + // there is already a sentry busting squad active for this sentry + break; + } + } + + if ( j < livePlayerVector.Count() ) + { + continue; + } + + // spawn a sentry buster squad to destroy this sentry + Vector vSpawnPosition; + SpawnLocationResult spawnLocationResult = m_where.FindSpawnLocation( vSpawnPosition ); + if ( spawnLocationResult != SPAWN_LOCATION_NOT_FOUND ) + { + EntityHandleVector_t spawnVector; + + if ( m_spawner && m_spawner->Spawn( vSpawnPosition, &spawnVector ) ) + { + // success + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "MANN VS MACHINE: %3.2f: <<<< Spawning Sentry Busting Mission >>>>\n", gpGlobals->curtime ); + } + + for( int k=0; k<spawnVector.Count(); ++k ) + { + CTFBot *bot = ToTFBot( spawnVector[k] ); + if ( bot ) + { + bot->SetFlagTarget( NULL ); + bot->SetMission( CTFBot::MISSION_DESTROY_SENTRIES ); + bot->SetMissionTarget( targetSentry ); + + // force an update to start the behavior so we can set the sentry + bot->Update(); + + bot->MarkAsMissionEnemy(); + + didSpawn = true; + + bot->GetPlayerClass()->SetCustomModel( g_szBotBossSentryBusterModel, USE_CLASS_ANIMATIONS ); + bot->UpdateModel(); + bot->SetBloodColor( DONT_BLEED ); + + if ( TFObjectiveResource() ) + { + unsigned int iFlags = MVM_CLASS_FLAG_MISSION; + if ( bot->IsMiniBoss() ) + { + iFlags |= MVM_CLASS_FLAG_MINIBOSS; + } + if ( bot->HasAttribute( CTFBot::ALWAYS_CRIT ) ) + { + iFlags |= MVM_CLASS_FLAG_ALWAYSCRIT; + } + TFObjectiveResource()->IncrementMannVsMachineWaveClassCount( m_spawner->GetClassIcon( k ), iFlags ); + } + + if ( TFGameRules() ) + { + TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_SENTRY_BUSTER, TF_TEAM_PVE_DEFENDERS ); + } + + // what bot should do after spawning at teleporter exit + if ( spawnLocationResult == SPAWN_LOCATION_TELEPORTER ) + { + OnBotTeleported( bot ); + } + } + } + } + } + else if ( tf_populator_debug.GetBool() ) + { + Warning( "MissionPopulator: %3.2f: Can't find a place to spawn a sentry destroying squad\n", gpGlobals->curtime ); + } + } + + if ( didSpawn ) + { + float flCoolDown = m_cooldownDuration; + + CWave *pWave = GetManager()->GetCurrentWave(); + if ( pWave ) + { + pWave->IncrementSentryBustersSpawned(); + + if ( TFGameRules() ) + { + if ( pWave->NumSentryBustersSpawned() > 1 ) + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Sentry_Buster_Alert_Another" ); + } + else + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Sentry_Buster_Alert" ); + } + } + + flCoolDown = m_cooldownDuration + pWave->NumSentryBustersKilled() * m_cooldownDuration; + + pWave->ResetSentryBustersKilled(); + } + + m_cooldownTimer.Start( flCoolDown ); + } + + return didSpawn; +} + + +//----------------------------------------------------------------------- +bool CMissionPopulator::UpdateMission( CTFBot::MissionType mission ) +{ + VPROF_BUDGET( "CMissionPopulator::UpdateMission", "NextBot" ); + + int activeMissionMembers = 0; + + CUtlVector< CTFPlayer * > livePlayerVector; + CollectPlayers( &livePlayerVector, TF_TEAM_PVE_INVADERS, COLLECT_ONLY_LIVING_PLAYERS ); + + for( int i=0; i<livePlayerVector.Count(); ++i ) + { + CTFBot *pBot = dynamic_cast<CTFBot *>( livePlayerVector[i] ); + if ( pBot && pBot ->HasMission( mission ) ) + { + ++activeMissionMembers; + } + } + + if( g_pPopulationManager->IsSpawningPaused() ) + { + return false; + } + + if ( activeMissionMembers > 0 ) + { + // wait until prior mission is dead + + // cooldown is time after death of last mission member + m_cooldownTimer.Start( m_cooldownDuration ); + + return false; + } + + if ( !m_cooldownTimer.IsElapsed() ) + { + return false; + } + + // are there enough free slots? + int currentEnemyCount = GetGlobalTeam( TF_TEAM_PVE_INVADERS )->GetNumPlayers(); + + if ( currentEnemyCount + m_desiredCount > CPopulationManager::MVM_INVADERS_TEAM_SIZE ) + { + // not enough slots yet + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "MANN VS MACHINE: %3.2f: Waiting for slots to spawn mission.\n", gpGlobals->curtime ); + } + + return false; + } + + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "MANN VS MACHINE: %3.2f: <<<< Spawning Mission >>>>\n", gpGlobals->curtime ); + } + + int nSniperCount = 0; + FOR_EACH_VEC( livePlayerVector, iLiveBot ) + { + CTFBot *pBot = dynamic_cast<CTFBot *>( livePlayerVector[iLiveBot] ); + if ( pBot && pBot->IsPlayerClass( TF_CLASS_SNIPER ) ) + { + nSniperCount++; + } + } + + // dispatch mission members + for( int iDesiredCount=0; iDesiredCount<m_desiredCount; ++iDesiredCount ) + { + Vector vSpawnPosition; + SpawnLocationResult spawnLocationResult = m_where.FindSpawnLocation( vSpawnPosition ); + if ( spawnLocationResult != SPAWN_LOCATION_NOT_FOUND ) + { + EntityHandleVector_t spawnedVector; + if ( m_spawner && m_spawner->Spawn( vSpawnPosition, &spawnedVector ) ) + { + // success + for( int iSpawn=0; iSpawn<spawnedVector.Count(); ++iSpawn ) + { + CTFBot *bot = ToTFBot( spawnedVector[iSpawn] ); + if ( bot ) + { + bot->SetFlagTarget( NULL ); + bot->SetMission( mission ); + //bot->SetMissionString( "" ); + bot->MarkAsMissionEnemy(); + + if ( TFObjectiveResource() ) + { + unsigned int iFlags = MVM_CLASS_FLAG_MISSION; + if ( bot->IsMiniBoss() ) + { + iFlags |= MVM_CLASS_FLAG_MINIBOSS; + } + if ( bot->HasAttribute( CTFBot::ALWAYS_CRIT ) ) + { + iFlags |= MVM_CLASS_FLAG_ALWAYSCRIT; + } + TFObjectiveResource()->IncrementMannVsMachineWaveClassCount( bot->GetPlayerClass()->GetClassIconName(), iFlags ); + } + + // Response rules stuff for MvM + if ( TFGameRules()->IsMannVsMachineMode() ) + { + // Only have defenders announce the arrival of the first enemy Sniper + if ( bot->HasMission( CTFBot::MISSION_SNIPER ) ) + { + nSniperCount++; + + if ( nSniperCount == 1 ) + { + TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_SNIPER_CALLOUT, TF_TEAM_PVE_DEFENDERS ); + } + } + } + + // what bot should do after spawning at teleporter exit + if ( spawnLocationResult == SPAWN_LOCATION_TELEPORTER ) + { + OnBotTeleported( bot ); + } + } + } + } + } + else + { + if ( tf_populator_debug.GetBool() ) + { + Warning( "MissionPopulator: %3.2f: Skipped a member - can't find a place to spawn\n", gpGlobals->curtime ); + } + } + } + + m_cooldownTimer.Start( m_cooldownDuration ); + + return true; +} + + +//----------------------------------------------------------------------- +void CMissionPopulator::Update( void ) +{ + VPROF_BUDGET( "CMissionPopulator::Update", "NextBot" ); + + if ( TFGameRules()->InSetup() || + GetManager()->GetWaveNumber() < m_beginAtWaveIndex || + GetManager()->GetWaveNumber() >= m_stopAtWaveIndex || + TFObjectiveResource()->GetMannVsMachineIsBetweenWaves() ) + { + m_state = NOT_STARTED; + return; + } + + if ( m_state == NOT_STARTED ) + { + if ( m_initialCooldown > 0.0f ) + { + m_state = INITIAL_COOLDOWN; + m_cooldownTimer.Start( m_initialCooldown ); + return; + } + + m_state = RUNNING; + m_cooldownTimer.Invalidate(); + } + else if ( m_state == INITIAL_COOLDOWN ) + { + if ( !m_cooldownTimer.IsElapsed() ) + { + return; + } + + m_state = RUNNING; + m_cooldownTimer.Invalidate(); + } + + switch( m_mission ) + { + case CTFBot::MISSION_SEEK_AND_DESTROY: + break; + + case CTFBot::MISSION_DESTROY_SENTRIES: + UpdateMissionDestroySentries(); + break; + + case CTFBot::MISSION_SNIPER: + case CTFBot::MISSION_SPY: + case CTFBot::MISSION_ENGINEER: + UpdateMission( m_mission ); + break; + } +} + + +void CMissionPopulator::UnpauseSpawning( void ) +{ + m_cooldownTimer.Start( m_cooldownDuration ); + m_checkForDangerousSentriesTimer.Start( RandomFloat( 5.0f, 10.0f ) ); +} + + +//----------------------------------------------------------------------- +//----------------------------------------------------------------------- +CRandomPlacementPopulator::CRandomPlacementPopulator( CPopulationManager *manager ) : IPopulator( manager ) +{ + m_count = 0; + m_minSeparation = 0.0f; + m_navAreaFilter = 0xFFFFFFFF; +} + + +//----------------------------------------------------------------------- +bool CRandomPlacementPopulator::Parse( KeyValues *values ) +{ + for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) + { + const char *name = data->GetName(); + + if ( Q_strlen( name ) <= 0 ) + { + continue; + } + + if ( !Q_stricmp( name, "Count" ) ) + { + m_count = data->GetInt(); + } + else if ( !Q_stricmp( name, "MinimumSeparation" ) ) + { + m_minSeparation = data->GetFloat(); + } + else if ( !Q_stricmp( name, "NavAreaFilter" ) ) + { + if ( !Q_stricmp( data->GetString(), "SENTRY_SPOT" ) ) + { + m_navAreaFilter = TF_NAV_SENTRY_SPOT; + } + else if ( !Q_stricmp( data->GetString(), "SNIPER_SPOT" ) ) + { + m_navAreaFilter = TF_NAV_SNIPER_SPOT; + } + else + { + Warning( "Unknown NavAreaFilter value '%s'\n", data->GetString() ); + } + } + else + { + m_spawner = IPopulationSpawner::ParseSpawner( this, data ); + + if ( m_spawner == NULL ) + { + Warning( "Unknown attribute '%s' in RandomPlacement definition.\n", name ); + } + } + } + + return true; +} + + +//----------------------------------------------------------------------- +// Create initial population at start of scenario +void CRandomPlacementPopulator::PostInitialize( void ) +{ + int i; + CUtlVector< CTFNavArea * > candidateAreaVector; + + for( i=0; i<TheNavAreas.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)TheNavAreas[i]; + + if ( area->HasAttributeTF( m_navAreaFilter ) ) + { + candidateAreaVector.AddToTail( area ); + } + } + + CUtlVector< CTFNavArea * > selectedAreaVector; + SelectSeparatedShuffleSet< CTFNavArea >( m_count, m_minSeparation, candidateAreaVector, &selectedAreaVector ); + + if ( m_spawner ) + { + for( i=0; i<selectedAreaVector.Count(); ++i ) + { + m_spawner->Spawn( selectedAreaVector[i]->GetCenter() ); + } + } +} + + +//----------------------------------------------------------------------- +//----------------------------------------------------------------------- +CPeriodicSpawnPopulator::CPeriodicSpawnPopulator( CPopulationManager *manager ) : IPopulator( manager ) +{ + m_minInterval = 30.0f; + m_maxInterval = 30.0f; +} + + +//----------------------------------------------------------------------- +bool CPeriodicSpawnPopulator::Parse( KeyValues *values ) +{ + for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) + { + const char *name = data->GetName(); + + if ( Q_strlen( name ) <= 0 ) + { + continue; + } + + if ( m_where.Parse( data ) ) + { + continue; + } + + if ( !Q_stricmp( name, "When" ) ) + { + if ( data->GetFirstSubKey() ) + { + for ( KeyValues *whenData = data->GetFirstSubKey(); whenData != NULL; whenData = whenData->GetNextKey() ) + { + if ( !Q_stricmp( whenData->GetName(), "MinInterval" ) ) + { + m_minInterval = whenData->GetFloat(); + } + else if ( !Q_stricmp( whenData->GetName(), "MaxInterval" ) ) + { + m_maxInterval = whenData->GetFloat(); + } + else + { + Warning( "Invalid field '%s' encountered in When\n", whenData->GetName() ); + return false; + } + } + } + else + { + // single constant value + m_minInterval = data->GetFloat(); + m_maxInterval = m_minInterval; + } + } + else + { + m_spawner = IPopulationSpawner::ParseSpawner( this, data ); + + if ( m_spawner == NULL ) + { + Warning( "Unknown attribute '%s' in PeriodicSpawn definition.\n", name ); + } + } + } + + return true; +} + + +//----------------------------------------------------------------------- +// Create initial population at start of scenario +void CPeriodicSpawnPopulator::PostInitialize( void ) +{ + m_timer.Start( RandomFloat( m_minInterval, m_maxInterval ) ); +} + + + +//----------------------------------------------------------------------- +// Continuously invoked to modify population over time +void CPeriodicSpawnPopulator::Update( void ) +{ + if ( m_timer.IsElapsed() && !g_pPopulationManager->IsSpawningPaused() ) + { + m_timer.Start( RandomFloat( m_minInterval, m_maxInterval ) ); + + Vector vSpawnPosition; + SpawnLocationResult spawnLocationResult = m_where.FindSpawnLocation( vSpawnPosition ); + if ( spawnLocationResult != SPAWN_LOCATION_NOT_FOUND ) + { + EntityHandleVector_t spawnedVector; + if ( m_spawner && m_spawner->Spawn( vSpawnPosition, &spawnedVector ) ) + { + // success + for( int i=0; i<spawnedVector.Count(); ++i ) + { + CTFBot *bot = ToTFBot( spawnedVector[i] ); + if ( bot ) + { + // what bot should do after spawning at teleporter exit + if ( spawnLocationResult == SPAWN_LOCATION_TELEPORTER ) + { + OnBotTeleported( bot ); + } + } + } + + return; + } + } + + // retry soon, in the hopes constraints have changed + m_timer.Start( 2.0f ); + } +} + +void CPeriodicSpawnPopulator::UnpauseSpawning( void ) +{ + m_timer.Start( RandomFloat( m_minInterval, m_maxInterval ) ); +} + + +//----------------------------------------------------------------------- +//----------------------------------------------------------------------- + +int CWaveSpawnPopulator::m_reservedPlayerSlotCount = 0; + +CWaveSpawnPopulator::CWaveSpawnPopulator( CPopulationManager *manager ) : IPopulator( manager ) +{ + m_totalCount = 0; + m_maxActive = 999; + m_spawnCount = 1; + m_waitBeforeStarting = 0.0f; + m_waitBetweenSpawns = 0.0f; + m_bWaitBetweenSpawnAfterDeath = false; + m_totalCurrency = -1; + SetState( PENDING ); + + m_startWaveOutput = NULL; + m_firstSpawnOutput = NULL; + m_lastSpawnOutput = NULL; + m_doneOutput = NULL; + + m_bSupportWave = false; + m_bLimitedSupport = false; + m_pParent = NULL; + + m_bRandomSpawn = false; + m_spawnLocationResult = SPAWN_LOCATION_NOT_FOUND; +} + +//----------------------------------------------------------------------- +CWaveSpawnPopulator::~CWaveSpawnPopulator() +{ + delete m_startWaveOutput; + delete m_firstSpawnOutput; + delete m_lastSpawnOutput; + delete m_doneOutput; +} + +//----------------------------------------------------------------------- +void CWaveSpawnPopulator::ForceFinish() +{ + if ( m_state < WAIT_FOR_ALL_DEAD ) + { + SetState( WAIT_FOR_ALL_DEAD ); + } + else if ( m_state != WAIT_FOR_ALL_DEAD ) + { + SetState( DONE ); + } + + FOR_EACH_VEC( m_activeVector, i ) + { + // Move bots over to spectator + CTFBot *pBot = ToTFBot( m_activeVector[i] ); + if ( pBot ) + { + pBot->ChangeTeam( TEAM_SPECTATOR, false, true ); + } + else // Other things just get removed. (ie. Tanks) + { + m_activeVector[i]->Remove(); + } + } + + m_activeVector.Purge(); +} + + +//----------------------------------------------------------------------- +bool CWaveSpawnPopulator::Parse( KeyValues *values ) +{ + // First, see if we have any Template keys + KeyValues *pTemplate = values->FindKey( "Template" ); + if ( pTemplate ) + { + KeyValues *pTemplateKV = GetManager()->GetTemplate( pTemplate->GetString() ); + if ( pTemplateKV ) + { + // Pump all the keys into ourself now + if ( Parse( pTemplateKV ) == false ) + { + return false; + } + } + else + { + Warning( "Unknown Template '%s' in WaveSpawn definition\n", pTemplate->GetString() ); + } + } + + for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) + { + const char *name = data->GetName(); + + if ( Q_strlen( name ) <= 0 ) + { + continue; + } + + if ( m_where.Parse( data ) ) + { + continue; + } + + // Skip templates when looping through the rest of the keys + if ( !Q_stricmp( name, "Template" ) ) + continue; + + if ( !Q_stricmp( name, "TotalCount" ) ) + { + m_totalCount = data->GetInt(); + } + else if ( !Q_stricmp( name, "MaxActive" ) ) + { + m_maxActive = data->GetInt(); + } + else if ( !Q_stricmp( name, "SpawnCount" ) ) + { + m_spawnCount = data->GetInt(); + } + else if ( !Q_stricmp( name, "WaitBeforeStarting" ) ) + { + m_waitBeforeStarting = data->GetFloat(); + } + else if ( !Q_stricmp( name, "WaitBetweenSpawns" ) ) + { + if ( m_waitBetweenSpawns != 0.f && m_bWaitBetweenSpawnAfterDeath ) + { + Warning( "Already specified WaitBetweenSpawnsAfterDeath time, WaitBetweenSpawns won't be used\n" ); + continue; + } + + m_waitBetweenSpawns = data->GetFloat(); + } + else if ( !Q_stricmp( name, "WaitBetweenSpawnsAfterDeath" ) ) + { + if ( m_waitBetweenSpawns != 0.f ) + { + Warning( "Already specified WaitBetweenSpawns time, WaitBetweenSpawnsAfterDeath won't be used\n" ); + continue; + } + + m_bWaitBetweenSpawnAfterDeath = true; + m_waitBetweenSpawns = data->GetFloat(); + } + else if ( !Q_stricmp( name, "StartWaveWarningSound" ) ) + { + m_startWaveWarningSound.sprintf( "%s", data->GetString() ); + } + else if ( !Q_stricmp( name, "StartWaveOutput" ) ) + { + m_startWaveOutput = ParseEvent( data ); + } + else if ( !Q_stricmp( name, "FirstSpawnWarningSound" ) ) + { + m_firstSpawnWarningSound.sprintf( "%s", data->GetString() ); + } + else if ( !Q_stricmp( name, "FirstSpawnOutput" ) ) + { + m_firstSpawnOutput = ParseEvent( data ); + } + else if ( !Q_stricmp( name, "LastSpawnWarningSound" ) ) + { + m_lastSpawnWarningSound.sprintf( "%s", data->GetString() ); + } + else if ( !Q_stricmp( name, "LastSpawnOutput" ) ) + { + m_lastSpawnOutput = ParseEvent( data ); + } + else if ( !Q_stricmp( name, "DoneWarningSound" ) ) + { + m_doneWarningSound.sprintf( "%s", data->GetString() ); + } + else if ( !Q_stricmp( name, "DoneOutput" ) ) + { + m_doneOutput = ParseEvent( data ); + } + else if ( !Q_stricmp( name, "TotalCurrency" ) ) + { + m_totalCurrency = data->GetInt(); + } + else if ( !Q_stricmp( name, "Name" ) ) + { + m_name = data->GetString(); + } + else if ( !Q_stricmp( name, "WaitForAllSpawned" ) ) + { + m_waitForAllSpawned = data->GetString(); + } + else if ( !Q_stricmp( name, "WaitForAllDead" ) ) + { + m_waitForAllDead = data->GetString(); + } + else if ( !Q_stricmp( name, "Support" ) ) + { + m_bLimitedSupport = !Q_stricmp( data->GetString(), "Limited" ); + m_bSupportWave = true; + } + else if ( !Q_stricmp( name, "RandomSpawn" ) ) + { + m_bRandomSpawn = data->GetBool(); + } + else + { + m_spawner = IPopulationSpawner::ParseSpawner( this, data ); + + if ( m_spawner == NULL ) + { + Warning( "Unknown attribute '%s' in WaveSpawn definition.\n", name ); + } + } + + // These allow us to avoid rounding errors later when divvying money to bots + m_unallocatedCurrency = m_totalCurrency; + m_remainingCount = m_totalCount; + } + + return true; +} + + +//----------------------------------------------------------------------- +void CWaveSpawnPopulator::OnPlayerKilled( CTFPlayer *corpse ) +{ + m_activeVector.FindAndFastRemove( corpse ); +} + + +//----------------------------------------------------------------------- +bool CWaveSpawnPopulator::IsFinishedSpawning( void ) +{ + if ( m_bSupportWave && !m_bLimitedSupport ) + { + // support waves are never done spawning until + // we get OnNonSupportWavesDone called + return false; + } + + return ( m_countSpawnedSoFar >= m_totalCount ); +} + + +//----------------------------------------------------------------------- +void CWaveSpawnPopulator::SetState( InternalStateType eState ) +{ + m_state = eState; + + switch( m_state ) + { + case PENDING: + case PRE_SPAWN_DELAY: + case SPAWNING: + break; + case WAIT_FOR_ALL_DEAD: + // last spawn has occurred + if ( m_lastSpawnWarningSound.Length() > 0 ) + { + TFGameRules()->BroadcastSound( 255, m_lastSpawnWarningSound ); + } + + FireEvent( m_lastSpawnOutput, "LastSpawnOutput" ); + + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "%3.2f: WaveSpawn(%s) started WAIT_FOR_ALL_DEAD\n", gpGlobals->curtime, m_name.IsEmpty() ? "" : m_name.Get() ); + } + break; + case DONE: + if ( m_doneWarningSound.Length() > 0 ) + { + TFGameRules()->BroadcastSound( 255, m_doneWarningSound ); + } + + FireEvent( m_doneOutput, "DoneOutput" ); + + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "%3.2f: WaveSpawn(%s) DONE\n", gpGlobals->curtime, m_name.IsEmpty() ? "" : m_name.Get() ); + } + break; + } +} + + +//----------------------------------------------------------------------- +void CWaveSpawnPopulator::OnNonSupportWavesDone( void ) +{ + if ( m_bSupportWave ) + { + switch( m_state ) + { + case PENDING: + case PRE_SPAWN_DELAY: + SetState( DONE ); + break; + case SPAWNING: + case WAIT_FOR_ALL_DEAD: + if ( TFGameRules() && ( m_unallocatedCurrency > 0 ) ) + { + TFGameRules()->DistributeCurrencyAmount( m_unallocatedCurrency, NULL, true, true ); + m_unallocatedCurrency = 0; + } + SetState( WAIT_FOR_ALL_DEAD ); + case DONE: + break; + } + } +} + + +//----------------------------------------------------------------------- +int CWaveSpawnPopulator::GetCurrencyAmountPerDeath( void ) +{ + int nCurrency = 0; + + if ( m_bSupportWave ) + { + if ( m_state == WAIT_FOR_ALL_DEAD ) + { + // we're still in the m_ActiveVector at this point so the number of players + // in the vector is the division of the money since we're done spawning + m_remainingCount = m_activeVector.Count(); + } + } + + if ( m_unallocatedCurrency > 0 ) + { + // We shouldn't be back in here if our remaining count is 0 + Assert ( m_remainingCount > 0 ); + + // Band-aid for playtest + m_remainingCount = m_remainingCount <= 0 ? 1 : m_remainingCount; + + nCurrency = m_unallocatedCurrency / m_remainingCount; + m_unallocatedCurrency -= nCurrency; + m_remainingCount--; + } + + return nCurrency; +} + + +//----------------------------------------------------------------------- +// Continuously invoked to modify population over time +void CWaveSpawnPopulator::Update( void ) +{ + VPROF_BUDGET( "CWaveSpawnPopulator::Update", "NextBot" ); + + switch( m_state ) + { + case DONE: + return; + + case PENDING: + m_timer.Start( m_waitBeforeStarting ); + SetState( PRE_SPAWN_DELAY ); + + // zero this here to ensure it is cleared between Waves + // since all WaveSpawns start at the same time at the beginning of a Wave + m_reservedPlayerSlotCount = 0; + + if ( m_startWaveWarningSound.Length() > 0 ) + { + TFGameRules()->BroadcastSound( 255, m_startWaveWarningSound ); + } + + FireEvent( m_startWaveOutput, "StartWaveOutput" ); + + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "%3.2f: WaveSpawn(%s) started PRE_SPAWN_DELAY\n", gpGlobals->curtime, m_name.IsEmpty() ? "" : m_name.Get() ); + } + break; + + case PRE_SPAWN_DELAY: + if ( m_timer.IsElapsed() ) + { + m_countSpawnedSoFar = 0; + m_myReservedSlotCount = 0; + SetState( SPAWNING ); + + if ( m_firstSpawnWarningSound.Length() > 0 ) + { + TFGameRules()->BroadcastSound( 255, m_firstSpawnWarningSound ); + } + + FireEvent( m_firstSpawnOutput, "FirstSpawnOutput" ); + + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "%3.2f: WaveSpawn(%s) started SPAWNING\n", gpGlobals->curtime, m_name.IsEmpty() ? "" : m_name.Get() ); + } + } + break; + + case SPAWNING: + if ( m_timer.IsElapsed() ) + { + if( g_pPopulationManager->IsSpawningPaused() ) + { + return; + } + + if ( !m_spawner ) + { + Warning( "Invalid spawner\n" ); + SetState( DONE ); + return; + } + + // count up how many entities we've spawned are still active + int currentActive = 0; + for( int i=0; i<m_activeVector.Count(); ++i ) + { + if ( m_activeVector[i] != NULL && m_activeVector[i]->IsAlive() ) + { + ++currentActive; + } + } + + if ( m_bWaitBetweenSpawnAfterDeath ) + { + if ( currentActive == 0 ) + { + if ( m_spawnLocationResult != SPAWN_LOCATION_NOT_FOUND ) + { + // free up the current spawn area so we select a new one for the next group + m_spawnLocationResult = SPAWN_LOCATION_NOT_FOUND; + + if ( m_waitBetweenSpawns > 0.0f ) + { + // start delay + m_timer.Start( m_waitBetweenSpawns ); + } + + // wait for the timer + return; + } + } + else + { + // wait until all current actives are dead + return; + } + } + + if ( currentActive >= m_maxActive ) + { + // we've reached our allowed cap + return; + } + + if ( m_myReservedSlotCount <= 0 ) + { + // are there enough free slots? + if ( ( m_maxActive - currentActive ) < m_spawnCount ) + { + // not enough room to spawn a group so wait + return; + } + + int currentEnemyCount = GetGlobalTeam( TF_TEAM_PVE_INVADERS )->GetNumPlayers(); + + if ( currentEnemyCount + m_spawnCount + m_reservedPlayerSlotCount > CPopulationManager::MVM_INVADERS_TEAM_SIZE ) + { + // no space right now + return; + } + + // there is room - reserve our slots to ensure another concurrent WaveSpawn doesn't consume them + m_reservedPlayerSlotCount += m_spawnCount; + m_myReservedSlotCount = m_spawnCount; + } + + bool bTeleported = ( m_spawnLocationResult == SPAWN_LOCATION_TELEPORTER ); + + Vector vSpawnPosition = vec3_origin; + if ( m_spawner && m_spawner->IsWhereRequired() ) + { + // try to look for a spawn point or a new teleport location + if ( m_spawnLocationResult == SPAWN_LOCATION_NOT_FOUND || m_spawnLocationResult == SPAWN_LOCATION_TELEPORTER ) + { + m_spawnLocationResult = m_where.FindSpawnLocation( m_vSpawnPosition ); + if ( m_spawnLocationResult == SPAWN_LOCATION_NOT_FOUND ) + { + // try again + return; + } + } + + vSpawnPosition = m_vSpawnPosition; + bTeleported = ( m_spawnLocationResult == SPAWN_LOCATION_TELEPORTER ); + + // reset m_pCurrentSpawnArea if we want to pick a new spawn area for the next bot to spawn + if ( m_bRandomSpawn ) + { + m_spawnLocationResult = SPAWN_LOCATION_NOT_FOUND; + } + } + + EntityHandleVector_t m_justSpawnedVector; + if ( m_spawner && m_spawner->Spawn( vSpawnPosition, &m_justSpawnedVector ) ) + { + // successfully spawned + + FOR_EACH_VEC( m_justSpawnedVector, i ) + { + if ( m_justSpawnedVector[i].Get() == NULL ) + continue; + + CTFBot *bot = ToTFBot( m_justSpawnedVector[i] ); + if ( bot ) + { + bot->SetCustomCurrencyWorth( 0 ); + bot->SetWaveSpawnPopulator( this ); + + // Allows client UI to know if a specific spawner is active + TFObjectiveResource()->SetMannVsMachineWaveClassActive( bot->GetPlayerClass()->GetClassIconName() ); + + if ( IsLimitedSupportWave() ) + { + bot->MarkAsLimitedSupportEnemy(); + } + + // what bot should do after spawning at teleporter exit + if ( bTeleported ) + { + OnBotTeleported( bot ); + } + } + else + { + CTFTankBoss *tank = dynamic_cast< CTFTankBoss * >( m_justSpawnedVector[i].Get() ); + if ( tank ) + { + tank->SetCurrencyValue( 0 ); + tank->SetWaveSpawnPopulator( this ); + + m_pParent->IncrementTanksSpawned(); + } + } + } + + int justSpawnedCount = m_justSpawnedVector.Count(); + + m_countSpawnedSoFar += justSpawnedCount; + + // release our reserved slots + int slotsToReleaseCount = ( justSpawnedCount <= m_myReservedSlotCount ) ? justSpawnedCount : m_myReservedSlotCount; + m_myReservedSlotCount -= slotsToReleaseCount; + m_reservedPlayerSlotCount -= slotsToReleaseCount; + + // somehow, duplicate entries can end up in m_activeVector if we just AddVectorToTail() - look into this + for( int i = 0 ; i < m_justSpawnedVector.Count() ; ++i ) + { + CBaseEntity *newEntity = m_justSpawnedVector[i]; + + for( int j = 0 ; j < m_activeVector.Count() ; ++j ) + { + if ( m_activeVector[j] == NULL ) + continue; + + if ( m_activeVector[j]->entindex() == newEntity->entindex() ) + { + Warning( "WaveSpawn duplicate entry in active vector\n" ); + continue; + } + } + + m_activeVector.AddToTail( newEntity ); + } + + if ( IsFinishedSpawning() ) + { + SetState( WAIT_FOR_ALL_DEAD ); + return; + } + + // successfully spawned a group of SpawnCount entities + if ( m_myReservedSlotCount <= 0 && !m_bWaitBetweenSpawnAfterDeath ) + { + // free up the current spawn area so we select a new one for the next group + m_spawnLocationResult = SPAWN_LOCATION_NOT_FOUND; + + if ( m_waitBetweenSpawns > 0.0f ) + { + // start delay + m_timer.Start( m_waitBetweenSpawns ); + } + } + + // not done yet + return; + } + + // couldn't spawn - retry soon + m_timer.Start( 1.0f ); + } + break; + + case WAIT_FOR_ALL_DEAD: + FOR_EACH_VEC( m_activeVector, i ) + { + if ( m_activeVector[i] != NULL && m_activeVector[i]->IsAlive() ) + { + // not done yet + return; + } + } + + // everyone we spawned is dead + SetState( DONE ); + break; + + } // switch + + // not done yet +} + + +//------------------------------------------------------------------------- +// End CWaveSpawnPopulator +//------------------------------------------------------------------------- + +//------------------------------------------------------------------------- +// CWave +//------------------------------------------------------------------------- +CWave::CWave( CPopulationManager *manager ) : IPopulator( manager ) +{ + m_iEnemyCount = 0; + m_nTanksSpawned = 0; + m_nSentryBustersSpawned = 0; + m_nNumEngineersTeleportSpawned = 0; + m_nNumSentryBustersKilled = 0; + m_totalCurrency = 0; + m_waitWhenDone = 0.0f; + m_isStarted = false; + m_bFiredInitWaveOutput = false; + m_startOutput = NULL; + m_doneOutput = NULL; + m_initOutput = NULL; + m_bCheckBonusCreditsMin = true; + m_bCheckBonusCreditsMax = true; + m_bPlayedUpgradeAlert = false; + m_flBonusCreditsTime = 0; + m_isEveryContainedWaveSpawnDone = false; + m_flStartTime = 0; + + m_doneTimer.Invalidate(); +} + +//------------------------------------------------------------------------- +CWave::~CWave() +{ + delete m_startOutput; + delete m_doneOutput; + delete m_initOutput; + m_waveSpawnVector.PurgeAndDeleteElements(); +} + +//------------------------------------------------------------------------- +bool CWave::Parse( KeyValues *data ) +{ + m_iEnemyCount = 0; + m_nWaveClassCounts.RemoveAll(); + m_totalCurrency = 0; + + FOR_EACH_SUBKEY( data, kvWave ) + { + if ( !Q_stricmp( kvWave->GetName(), "WaveSpawn" ) ) + { + CWaveSpawnPopulator *wavePopulator = new CWaveSpawnPopulator( GetManager() ); + + if ( wavePopulator->Parse( kvWave ) == false ) + { + Warning( "Error reading WaveSpawn definition\n" ); + return false; + } + + m_waveSpawnVector.AddToTail( wavePopulator ); + + if ( !wavePopulator->IsSupportWave() ) + { + // this is a total of all enemies we have to fight that are NOT support enemies + m_iEnemyCount += wavePopulator->m_totalCount; + } + m_totalCurrency += wavePopulator->m_totalCurrency; + + wavePopulator->SetParent( this ); + + if ( wavePopulator->m_spawner ) + { + if ( wavePopulator->m_spawner->IsVarious() ) + { + for ( int i = 0; i < wavePopulator->m_totalCount; ++i ) + { + unsigned int iFlags = wavePopulator->IsSupportWave() ? MVM_CLASS_FLAG_SUPPORT : MVM_CLASS_FLAG_NORMAL; + if ( wavePopulator->m_spawner->IsMiniBoss( i ) ) + { + iFlags |= MVM_CLASS_FLAG_MINIBOSS; + } + if ( wavePopulator->m_spawner->HasAttribute( CTFBot::ALWAYS_CRIT, i ) ) + { + iFlags |= MVM_CLASS_FLAG_ALWAYSCRIT; + } + if ( wavePopulator->IsLimitedSupportWave() ) + { + iFlags |= MVM_CLASS_FLAG_SUPPORT_LIMITED; + } + AddClassType( wavePopulator->m_spawner->GetClassIcon( i ), 1, iFlags ); + } + } + else + { + unsigned int iFlags = wavePopulator->IsSupportWave() ? MVM_CLASS_FLAG_SUPPORT : MVM_CLASS_FLAG_NORMAL; + if ( wavePopulator->m_spawner->IsMiniBoss() ) + { + iFlags |= MVM_CLASS_FLAG_MINIBOSS; + } + if ( wavePopulator->m_spawner->HasAttribute( CTFBot::ALWAYS_CRIT ) ) + { + iFlags |= MVM_CLASS_FLAG_ALWAYSCRIT; + } + if ( wavePopulator->IsLimitedSupportWave() ) + { + iFlags |= MVM_CLASS_FLAG_SUPPORT_LIMITED; + } + AddClassType( wavePopulator->m_spawner->GetClassIcon(), wavePopulator->m_totalCount, iFlags ); + } + } + } + else if ( !Q_stricmp( kvWave->GetName(), "Sound" ) ) + { + m_soundName.sprintf( "%s", kvWave->GetString() ); + } + else if ( !Q_stricmp( kvWave->GetName(), "Description" ) ) + { + m_description.sprintf( "%s", kvWave->GetString() ); + } + else if ( !Q_stricmp( kvWave->GetName(), "WaitWhenDone" ) ) + { + m_waitWhenDone = kvWave->GetFloat(); + } + else if ( !Q_stricmp( kvWave->GetName(), "Checkpoint" ) ) + { + //m_isCheckpoint = true; + } + else if ( !Q_stricmp( kvWave->GetName(), "StartWaveOutput" ) ) + { + m_startOutput = ParseEvent( kvWave ); + } + else if ( !Q_stricmp( kvWave->GetName(), "DoneOutput" ) ) + { + m_doneOutput = ParseEvent( kvWave ); + } + else if ( !Q_stricmp( kvWave->GetName(), "InitWaveOutput" ) ) + { + m_initOutput = ParseEvent( kvWave ); + } + else + { + Warning( "Unknown attribute '%s' in Wave definition.\n", kvWave->GetName() ); + } + } + + return true; +} +//------------------------------------------------------------------------- +// If we are the currently active wave, update all contained WaveSpawns. +void CWave::Update( void ) +{ + VPROF_BUDGET( "CWave::Update", "NextBot" ); + + if ( TFGameRules()->State_Get() == GR_STATE_RND_RUNNING ) + { + ActiveWaveUpdate(); + } + else if ( TFGameRules()->State_Get() == GR_STATE_BETWEEN_RNDS || TFGameRules()->State_Get() == GR_STATE_TEAM_WIN ) + { + WaveIntermissionUpdate(); + } + + // is the wave done? + if ( m_isEveryContainedWaveSpawnDone && TFGameRules()->State_Get() == GR_STATE_RND_RUNNING ) + { + if ( GetManager()->IsBonusRound() && GetManager()->GetBonusBoss() && GetManager()->GetBonusBoss()->IsAlive() ) + { + return; + } + WaveCompleteUpdate(); + } +} + + +//------------------------------------------------------------------------- +void CWave::OnPlayerKilled( CTFPlayer *corpse ) +{ + for( int i=0; i<m_waveSpawnVector.Count(); ++i ) + { + m_waveSpawnVector[i]->OnPlayerKilled( corpse ); + } +} + + +//------------------------------------------------------------------------- +bool CWave::HasEventChangeAttributes( const char* pszEventName ) const +{ + for ( int i=0; i<m_waveSpawnVector.Count(); ++i ) + { + if ( m_waveSpawnVector[i]->HasEventChangeAttributes( pszEventName ) ) + { + return true; + } + } + + return false; +} + + +//------------------------------------------------------------------------- +void CWave::ForceFinish() +{ + FOR_EACH_VEC( m_waveSpawnVector, i ) + { + m_waveSpawnVector[i]->ForceFinish(); + } +} + + +//------------------------------------------------------------------------- +void CWave::ForceReset() +{ + m_isStarted = false; + m_bFiredInitWaveOutput = false; + m_flBonusCreditsTime = 0; + m_isEveryContainedWaveSpawnDone = false; + m_flStartTime = 0; + + m_doneTimer.Invalidate(); + + FOR_EACH_VEC( m_waveSpawnVector, i ) + { + m_waveSpawnVector[i]->ForceReset(); + } +} + +//------------------------------------------------------------------------- +CWaveSpawnPopulator *CWave::FindWaveSpawnPopulator( const char *name ) +{ + FOR_EACH_VEC( m_waveSpawnVector, i ) + { + CWaveSpawnPopulator *waveSpawnPopulator = m_waveSpawnVector[i]; + if ( !Q_stricmp( waveSpawnPopulator->m_name.Get(), name ) ) + { + return waveSpawnPopulator; + } + } + + return NULL; +} + +//------------------------------------------------------------------------- +void CWave::AddClassType( string_t iszClassIconName, int nCount, unsigned int iFlags ) +{ + int nIndex = -1; + for ( int nClass = 0; nClass < m_nWaveClassCounts.Count(); ++nClass ) + { + if ( ( m_nWaveClassCounts[ nClass ].iszClassIconName == iszClassIconName ) && ( m_nWaveClassCounts[ nClass ].iFlags & iFlags ) ) + { + nIndex = nClass; + break; + } + } + + if ( nIndex == -1 ) + { + nIndex = m_nWaveClassCounts.AddToTail(); + m_nWaveClassCounts[ nIndex ].iszClassIconName = iszClassIconName; + m_nWaveClassCounts[ nIndex ].nClassCount = 0; + m_nWaveClassCounts[ nIndex ].iFlags = MVM_CLASS_FLAG_NONE; + } + + m_nWaveClassCounts[ nIndex ].nClassCount += nCount; + m_nWaveClassCounts[ nIndex ].iFlags |= iFlags; +} + +//------------------------------------------------------------------------- +// Private +//------------------------------------------------------------------------- +bool CWave::IsDoneWithNonSupportWaves( void ) +{ + FOR_EACH_VEC( m_waveSpawnVector, i ) + { + CWaveSpawnPopulator *waveSpawnPopulator = m_waveSpawnVector[i]; + if ( waveSpawnPopulator ) + { + if ( !waveSpawnPopulator->IsSupportWave() && !waveSpawnPopulator->IsDone() ) + return false; + } + } + + return true; +} + +//------------------------------------------------------------------------- +void CWave::ActiveWaveUpdate( void ) +{ + VPROF_BUDGET( "CWave::ActiveWaveUpdate", "NextBot" ); + + if ( !m_isStarted ) + { + // Delay the start of the next wave + if ( GetManager()->IsInEndlessWaves() && m_flStartTime > gpGlobals->curtime ) + return; + + // wave just started + m_isStarted = true; + + FireEvent( m_startOutput, "StartWaveOutput" ); + + if ( m_soundName.Length() > 0 ) + { + TFGameRules()->BroadcastSound( 255, m_soundName ); + } + + GetManager()->AdjustMinPlayerSpawnTime(); + } + + m_isEveryContainedWaveSpawnDone = true; + + if ( GetManager()->IsBonusRound() ) + { + return; + } + + // update each contained WaveSpawn + FOR_EACH_VEC( m_waveSpawnVector, i ) + { + CWaveSpawnPopulator *waveSpawnPopulator = m_waveSpawnVector[i]; + bool bWaiting = false; + + // check if this WaveSpawn is waiting for another WaveSpawn to be done spawning players + if ( !waveSpawnPopulator->m_waitForAllSpawned.IsEmpty() ) + { + char *name = waveSpawnPopulator->m_waitForAllSpawned.GetForModify(); + FOR_EACH_VEC( m_waveSpawnVector, j ) + { + CWaveSpawnPopulator *predecessor = m_waveSpawnVector[j]; + if ( predecessor && !Q_stricmp( predecessor->m_name.Get(), name ) ) + { + if ( !predecessor->IsDoneSpawningBots() ) + { + bWaiting = true; + break; + } + } + } + } + + if ( !bWaiting ) + { + // check if this WaveSpawn is waiting for another WaveSpawn's players to all be dead + if ( !waveSpawnPopulator->m_waitForAllDead.IsEmpty() ) + { + const char *name = waveSpawnPopulator->m_waitForAllDead.Get(); + FOR_EACH_VEC( m_waveSpawnVector, j ) + { + CWaveSpawnPopulator *predecessor = m_waveSpawnVector[j]; + if ( predecessor && !Q_stricmp( predecessor->m_name.Get(), name ) ) + { + if ( !predecessor->IsDone() ) + { + bWaiting = true; + break; + } + } + } + } + } + + if ( bWaiting ) + { + continue; + } + + waveSpawnPopulator->Update(); + + m_isEveryContainedWaveSpawnDone &= waveSpawnPopulator->IsDone(); + } + + if ( IsDoneWithNonSupportWaves() ) + { + // Loop through and tell all the WaveSpawns + FOR_EACH_VEC( m_waveSpawnVector, i ) + { + CWaveSpawnPopulator *waveSpawnPopulator = m_waveSpawnVector[i]; + waveSpawnPopulator->OnNonSupportWavesDone(); + } + + for ( int i = 1 ; i <= gpGlobals->maxClients ; i++ ) + { + // Now let's kill everyone left on the attacking team + CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && pPlayer->IsAlive() && + ( ( pPlayer->GetTeamNumber() == TF_TEAM_PVE_INVADERS ) || pPlayer->m_Shared.InCond( TF_COND_REPROGRAMMED ) ) ) + { + pPlayer->CommitSuicide( true, false ); + } + } + } +} + +//------------------------------------------------------------------------- +void CWave::WaveCompleteUpdate( void ) +{ + bool bHasTank = NumTanksSpawned() >= 1; + + FireEvent( m_doneOutput, "DoneOutput" ); + + bool bLastWave = ( GetManager()->GetWaveNumber() + 1 ) >= GetManager()->GetTotalWaveCount(); + bool bMidWave = ( GetManager()->GetWaveNumber() + 1 ) >= ( GetManager()->GetTotalWaveCount() / 2 ); + bool bAdvancedPopfile = ( g_pPopulationManager ? g_pPopulationManager->IsAdvancedPopFile() : false ); + + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_wave_complete" ); + if ( event ) + { + event->SetBool( "advanced", bAdvancedPopfile ); + gameeventmanager->FireEvent( event ); + } + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + if ( bAdvancedPopfile ) + { + CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + if ( pMaster && ( pMaster->GetNumPoints() > 0 ) ) + { + if ( pMaster->GetNumPointsOwnedByTeam( TF_TEAM_PVE_DEFENDERS ) == pMaster->GetNumPoints() ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_adv_wave_complete_no_gates" ); + if ( event ) + { + event->SetInt( "index", GetManager()->GetWaveNumber() ); + gameeventmanager->FireEvent( event ); + } + } + } + } + } + + if ( bLastWave && !GetManager()->IsInEndlessWaves() ) + { + GetManager()->MvMVictory(); + + if ( TFGameRules() ) + { + if ( GTFGCClientSystem()->GetMatch() && GTFGCClientSystem()->GetMatch()->m_eMatchGroup == k_nMatchGroup_MvM_MannUp ) + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Manned_Up_Wave_End" ); + } + else + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Final_Wave_End" ); + } + + TFGameRules()->BroadcastSound( 255, "music.mvm_end_last_wave" ); + } + + event = gameeventmanager->CreateEvent( "mvm_mission_complete" ); + if ( event ) + { + event->SetString( "mission", GetManager()->GetPopulationFilename() ); + gameeventmanager->FireEvent( event ); + } + } + else + { + if ( TFGameRules() ) + { + TFGameRules()->BroadcastSound( 255,"Announcer.MVM_Wave_End" ); + + if( bHasTank ) + { + TFGameRules()->BroadcastSound( 255, "music.mvm_end_tank_wave" ); + } + else if( bMidWave ) + { + TFGameRules()->BroadcastSound( 255, "music.mvm_end_mid_wave" ); + } + else + { + TFGameRules()->BroadcastSound( 255, "music.mvm_end_wave" ); + } + } + } + + CBroadcastRecipientFilter filter; + filter.MakeReliable(); + UserMessageBegin( filter, "MVMAnnouncement" ); + WRITE_CHAR( TF_MVM_ANNOUNCEMENT_WAVE_COMPLETE ); + WRITE_CHAR( GetManager()->GetWaveNumber() ); + MessageEnd(); + + if ( TFObjectiveResource() ) + { + // if we're using a timer between waves... + if ( !g_pPopulationManager->GetWavesUseReadyBetween() ) + { + if ( !m_doneTimer.HasStarted() ) + { + m_doneTimer.Start( m_waitWhenDone ); + } + + TFObjectiveResource()->SetMannVsMachineNextWaveTime( gpGlobals->curtime + m_waitWhenDone ); + } + + // Force respawn dead defenders + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_PVE_DEFENDERS ); + FOR_EACH_VEC( playerVector, i ) + { + if ( !playerVector[i]->IsAlive() ) + { + playerVector[i]->ForceRespawn(); + } + + // clear player's accumulated sentry damage + playerVector[i]->ResetAccumulatedSentryGunDamageDealt(); + playerVector[i]->ResetAccumulatedSentryGunKillCount(); + } + } + + GetManager()->WaveEnd( true ); +} + +//------------------------------------------------------------------------- +void CWave::WaveIntermissionUpdate ( void ) +{ + if ( !m_bFiredInitWaveOutput ) + { + FireEvent( m_initOutput, "InitWaveOutput" ); + + m_bFiredInitWaveOutput = true; + } + + if ( m_GetUpgradesAlertTimer.HasStarted() && m_GetUpgradesAlertTimer.IsElapsed() ) + { + // Monitor for full wave currency collection bonus + if ( ( m_bCheckBonusCreditsMin || m_bCheckBonusCreditsMax ) && gpGlobals->curtime > m_flBonusCreditsTime ) + { + int nWaveNum = GetManager()->GetWaveNumber() - 1; + int nDropped = MannVsMachineStats_GetDroppedCredits( nWaveNum ); + int nAcquired = MannVsMachineStats_GetAcquiredCredits( nWaveNum, false ); + float flRatioCollected = clamp( ( (float)nAcquired / (float)nDropped ), 0.1f, 1.f ); + + float flMinBonus = tf_mvm_currency_bonus_ratio_min.GetFloat(); + float flMaxBonus = tf_mvm_currency_bonus_ratio_max.GetFloat(); + + Assert( flMinBonus <= flMaxBonus ); + if ( flMinBonus > flMaxBonus ) + flMinBonus = flMaxBonus; + + // Max bonus + if ( m_bCheckBonusCreditsMax && nDropped > 0 && flRatioCollected >= flMaxBonus ) + { + int nAmount = (float)TFGameRules()->CalculateCurrencyAmount_ByType( TF_CURRENCY_WAVE_COLLECTION_BONUS ) * 0.5f; + TFGameRules()->DistributeCurrencyAmount( nAmount, NULL, true, false, true ); + + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Bonus" ); + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_creditbonus_wave" ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } + + m_bCheckBonusCreditsMax = false; + m_GetUpgradesAlertTimer.Reset(); + } + // Min bonus + if ( m_bCheckBonusCreditsMin && nDropped > 0 && flRatioCollected >= flMinBonus ) + { + int nAmount = (float)TFGameRules()->CalculateCurrencyAmount_ByType( TF_CURRENCY_WAVE_COLLECTION_BONUS ) * 0.5f; + TFGameRules()->DistributeCurrencyAmount( nAmount, NULL, true, false, true ); + + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Bonus" ); + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_creditbonus_wave" ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } + + m_bCheckBonusCreditsMin = false; + } + else if ( !m_bPlayedUpgradeAlert ) + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Get_To_Upgrade" ); + + m_bPlayedUpgradeAlert = true; + m_GetUpgradesAlertTimer.Reset(); + } + + m_flBonusCreditsTime = gpGlobals->curtime + 0.25f; + } + } + + // When we use a timer between waves, start it here + if ( m_doneTimer.HasStarted() && m_doneTimer.IsElapsed() ) + { + m_doneTimer.Invalidate(); + GetManager()->StartCurrentWave(); + } +} +//------------------------------------------------------------------------- +// End CWave +//------------------------------------------------------------------------- diff --git a/game/server/tf/player_vs_environment/tf_populators.h b/game/server/tf/player_vs_environment/tf_populators.h new file mode 100644 index 0000000..84b1d98 --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_populators.h @@ -0,0 +1,467 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: tf_populator_spawners +// Implementations of NPC Spawning Code for PvE related game modes (MvM) +//=============================================================================// +#ifndef TF_POPULATORS_H +#define TF_POPULATORS_H + +#include "tf_population_manager.h" + +class KeyValues; +class IPopulator; +class IPopulationSpawner; +class CPopulationManager; +class CWave; +class CSpawnLocation; + +//----------------------------------------------------------------------- +class CSpawnLocation +{ +public: + CSpawnLocation(); + + bool Parse( KeyValues *data ); + + bool IsValid( void ) const; + + SpawnLocationResult FindSpawnLocation( Vector& vSpawnPosition ); + +private: + CTFNavArea *SelectSpawnArea( void ) const; + + RelativePositionType m_relative; + TFTeamSpawnVector_t m_teamSpawnVector; + + int m_nSpawnCount; + int m_nRandomSeed; + bool m_bClosestPointOnNav; +}; + +inline bool CSpawnLocation::IsValid( void ) const +{ + return m_relative != UNDEFINED || m_teamSpawnVector.Count() > 0; +} + + +//----------------------------------------------------------------------- +// For spawning bots/players at a specific position +class CPopulatorInternalSpawnPoint : public CPointEntity +{ + DECLARE_CLASS( CPopulatorInternalSpawnPoint, CPointEntity ); +}; + +extern CHandle< CPopulatorInternalSpawnPoint > g_internalSpawnPoint; + +//----------------------------------------------------------------------- +// A Populator manages the populating of entities in the environment. +class IPopulator +{ +public: + IPopulator( CPopulationManager *manager ) + { + m_manager = manager; + m_spawner = NULL; + } + + virtual ~IPopulator() + { + if ( m_spawner ) + { + delete m_spawner; + } + m_spawner = NULL; + } + + virtual bool Parse( KeyValues *data ) = 0; + + virtual void PostInitialize( void ) { } // create initial population at start of scenario + virtual void Update( void ) { } // continuously invoked to modify population over time + virtual void UnpauseSpawning() {} + + virtual void OnPlayerKilled( CTFPlayer *corpse ) { } + + CPopulationManager *GetManager( void ) const { return m_manager; } + + virtual bool HasEventChangeAttributes( const char* pszEventName ) const + { + if ( m_spawner ) + { + return m_spawner->HasEventChangeAttributes( pszEventName ); + } + + return false; + } + + IPopulationSpawner *m_spawner; + +private: + CPopulationManager *m_manager; +}; + + +//----------------------------------------------------------------------- +// Invokes its spawner when mission conditions are met +class CMissionPopulator : public IPopulator +{ +public: + CMissionPopulator( CPopulationManager *manager ); + virtual ~CMissionPopulator() { } + + virtual bool Parse( KeyValues *data ); + + virtual void Update( void ); // continuously invoked to modify population over time + virtual void UnpauseSpawning( void ); + + int BeginAtWave( void ) { return m_beginAtWaveIndex; } + int StopAtWave( void ) { return m_stopAtWaveIndex; } + + CTFBot::MissionType GetMissionType( void ){ return m_mission; } + +private: + CTFBot::MissionType m_mission; + CSpawnLocation m_where; + + bool UpdateMissionDestroySentries( void ); + bool UpdateMission( CTFBot::MissionType mission ); + + enum StateType + { + NOT_STARTED, + INITIAL_COOLDOWN, + RUNNING + }; + + StateType m_state; + + float m_initialCooldown; + float m_cooldownDuration; + CountdownTimer m_cooldownTimer; + CountdownTimer m_checkForDangerousSentriesTimer; + int m_desiredCount; + int m_beginAtWaveIndex; // this mission becomes active at this wave number + int m_stopAtWaveIndex; // stop when this wave becomes active +}; + + +//----------------------------------------------------------------------- +// Invokes its spawner at random positions scattered throughout +// the environment at PostInitialize() +class CRandomPlacementPopulator : public IPopulator +{ +public: + CRandomPlacementPopulator( CPopulationManager *manager ); + virtual ~CRandomPlacementPopulator() { } + + virtual bool Parse( KeyValues *data ); + + virtual void PostInitialize( void ); // create initial population at start of scenario + + int m_count; + float m_minSeparation; + unsigned int m_navAreaFilter; +}; + + +//----------------------------------------------------------------------- +// Invokes its spawner periodically +class CPeriodicSpawnPopulator : public IPopulator +{ +public: + CPeriodicSpawnPopulator( CPopulationManager *manager ); + virtual ~CPeriodicSpawnPopulator() { } + + virtual bool Parse( KeyValues *data ); + + virtual void PostInitialize( void ); // create initial population at start of scenario + virtual void Update( void ); // continuously invoked to modify population over time + virtual void UnpauseSpawning( void ); + + CSpawnLocation m_where; + float m_minInterval; + float m_maxInterval; + +private: + CountdownTimer m_timer; +}; + + +//----------------------------------------------------------------------- +// Spawns a group of entities within a Wave +class CWaveSpawnPopulator : public IPopulator +{ +public: + CWaveSpawnPopulator( CPopulationManager *manager ); + virtual ~CWaveSpawnPopulator(); + + virtual bool Parse( KeyValues *data ); + + virtual void Update( void ); // continuously invoked to modify population over time + + virtual void OnPlayerKilled( CTFPlayer *corpse ); + + CSpawnLocation m_where; + int m_totalCount; + int m_remainingCount; + int m_nClassCounts; + int m_maxActive; // the maximum number of entities active at one time + int m_spawnCount; // the number of entities to spawn at once + float m_waitBeforeStarting; + float m_waitBetweenSpawns; // between spawns of mobs + bool m_bWaitBetweenSpawnAfterDeath; + + CFmtStr m_startWaveWarningSound; + EventInfo *m_startWaveOutput; + + CFmtStr m_firstSpawnWarningSound; + EventInfo *m_firstSpawnOutput; + + CFmtStr m_lastSpawnWarningSound; + EventInfo *m_lastSpawnOutput; + + CFmtStr m_doneWarningSound; + EventInfo *m_doneOutput; + + int m_totalCurrency; + int m_unallocatedCurrency; + + CUtlString m_name; + CUtlString m_waitForAllSpawned; + CUtlString m_waitForAllDead; + + bool IsDone( void ) const + { + return m_state == DONE; + } + + bool IsDoneSpawningBots( void ) const + { + return m_state > SPAWNING; + } + + // Invoked by td_setnextwave to finish off a wave + void ForceFinish( void ); + void ForceReset( void ) + { + m_unallocatedCurrency = m_totalCurrency; + m_remainingCount = m_totalCount; + m_state = PENDING; + } + + bool IsSupportWave( void ) const { return m_bSupportWave; } + bool IsLimitedSupportWave( void ) const { return m_bLimitedSupport; } + void SetParent( CWave *pParent ) { m_pParent = pParent; } + int GetCurrencyAmountPerDeath( void ); + void OnNonSupportWavesDone( void ); + +private: + bool IsFinishedSpawning( void ); + + CountdownTimer m_timer; + EntityHandleVector_t m_activeVector; + int m_countSpawnedSoFar; + int m_myReservedSlotCount; + + bool m_bSupportWave; + bool m_bLimitedSupport; + CWave *m_pParent; + + enum InternalStateType + { + PENDING, + PRE_SPAWN_DELAY, + SPAWNING, + WAIT_FOR_ALL_DEAD, + DONE + }; + InternalStateType m_state; + void SetState( InternalStateType eState ); + + int ReservePlayerSlots( int count ); // reserve 'count' player slots so other WaveSpawns don't take them + void ReleasePlayerSlots( int count ); // release 'count' player slots that have been previously reserved + static int m_reservedPlayerSlotCount; + + bool m_bRandomSpawn; + SpawnLocationResult m_spawnLocationResult; + Vector m_vSpawnPosition; +}; + + +struct WaveClassCount_t +{ + int nClassCount; + string_t iszClassIconName; + unsigned int iFlags; +}; + + +//----------------------------------------------------------------------- +// Spawns sequential "waves" of entities over time. +// A wave consists of one or more WaveSpawns that all run concurrently. +// The wave is done when all contained WaveSpawns are done. +class CWave : public IPopulator +{ +public: + CWave( CPopulationManager *manager ); + virtual ~CWave(); + + virtual bool Parse( KeyValues *data ); + virtual void Update( void ); + + virtual void OnPlayerKilled( CTFPlayer *corpse ); + + virtual bool HasEventChangeAttributes( const char* pszEventName ) const OVERRIDE; + + void ForceFinish(); // used when forcing a wave to finish + void ForceReset(); // used when forcing a wave to start + + CWaveSpawnPopulator *FindWaveSpawnPopulator( const char *name ); // find a CWaveSpawnPopulator by name + + void AddClassType( string_t iszClassIconName, int nCount, unsigned int iFlags ); + + int GetNumClassTypes( void ) const { return m_nWaveClassCounts.Count(); } + void StartUpgradesAlertTimer ( float flTime ) { m_GetUpgradesAlertTimer.Start( flTime ); } + void SetStartTime (float flTime) { m_flStartTime = flTime; } + + // inline + bool IsCheckpoint( void ) const; + CWave *GetNextWave( void ) const; + void SetNextWave( CWave *wave ); + const char *GetDescription( void ) const; + int GetTotalCurrency( void ) const; + int GetEnemyCount( void ) const; + int GetClassCount( int nIndex ) const; + string_t GetClassIconName( int nIndex ) const; + unsigned int GetClassFlags( int nIndex ) const; + + int NumTanksSpawned( void ) const; + void IncrementTanksSpawned( void ); + + int NumSentryBustersSpawned( void ) const; + void IncrementSentryBustersSpawned( void ); + int NumSentryBustersKilled( void ) const; + void IncrementSentryBustersKilled( void ); + void ResetSentryBustersKilled( void ); + + int NumEngineersTeleportSpawned( void ) const; + void IncrementEngineerTeleportSpawned( void ); + +private: + bool IsDoneWithNonSupportWaves( void ); + + void ActiveWaveUpdate(); + void WaveCompleteUpdate(); + void WaveIntermissionUpdate(); + + CUtlVector< CWaveSpawnPopulator * > m_waveSpawnVector; + + bool m_isStarted; + bool m_bFiredInitWaveOutput; + int m_iEnemyCount; + int m_nTanksSpawned; + int m_nSentryBustersSpawned; + int m_nNumEngineersTeleportSpawned; + + int m_nNumSentryBustersKilled; + + CUtlVector< WaveClassCount_t > m_nWaveClassCounts; + int m_totalCurrency; + + EventInfo *m_startOutput; + EventInfo *m_doneOutput; + EventInfo *m_initOutput; + + CFmtStr m_description; + CFmtStr m_soundName; + + float m_waitWhenDone; + CountdownTimer m_doneTimer; + + bool m_bCheckBonusCreditsMin; + bool m_bCheckBonusCreditsMax; + float m_flBonusCreditsTime; + + bool m_bPlayedUpgradeAlert; + CountdownTimer m_GetUpgradesAlertTimer; + + bool m_isEveryContainedWaveSpawnDone; + float m_flStartTime; +}; + +inline const char *CWave::GetDescription( void ) const +{ + return m_description; +} + +inline int CWave::GetTotalCurrency( void ) const +{ + return m_totalCurrency; +} + +inline int CWave::GetEnemyCount( void ) const +{ + return m_iEnemyCount; +} + +inline int CWave::GetClassCount( int nIndex ) const +{ + return m_nWaveClassCounts[ nIndex ].nClassCount; +} + +inline string_t CWave::GetClassIconName( int nIndex ) const +{ + return m_nWaveClassCounts[ nIndex ].iszClassIconName; +} + +inline unsigned int CWave::GetClassFlags( int nIndex ) const +{ + return m_nWaveClassCounts[ nIndex ].iFlags; +} + +inline int CWave::NumTanksSpawned( void ) const +{ + return m_nTanksSpawned; +} + +inline void CWave::IncrementTanksSpawned( void ) +{ + m_nTanksSpawned++; +} + + +inline int CWave::NumSentryBustersSpawned( void ) const +{ + return m_nSentryBustersSpawned; +} + +inline void CWave::IncrementSentryBustersSpawned( void ) +{ + m_nSentryBustersSpawned++; +} + +inline int CWave::NumSentryBustersKilled( void ) const +{ + return m_nNumSentryBustersKilled; +} + +inline void CWave::IncrementSentryBustersKilled( void ) +{ + m_nNumSentryBustersKilled++; +} + +inline void CWave::ResetSentryBustersKilled( void ) +{ + m_nNumSentryBustersKilled = 0; +} + +inline int CWave::NumEngineersTeleportSpawned( void ) const +{ + return m_nNumEngineersTeleportSpawned; +} + +inline void CWave::IncrementEngineerTeleportSpawned( void ) +{ + m_nNumEngineersTeleportSpawned++; +} + +#endif // TF_POPULATORS_H diff --git a/game/server/tf/player_vs_environment/tf_tank_boss.cpp b/game/server/tf/player_vs_environment/tf_tank_boss.cpp new file mode 100644 index 0000000..57ab63c --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_tank_boss.cpp @@ -0,0 +1,1077 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#include "cbase.h" + +#include "tf_weaponbase.h" +#include "eventqueue.h" +#include "particle_parse.h" +#include "tf_tank_boss.h" +#include "tf_objective_resource.h" +#include "engine/IEngineSound.h" +#include "logicrelay.h" + + +#define TANK_DAMAGE_MODEL_COUNT 4 +#define TANK_DEFAULT_HEALTH 1000 + + +static const char *s_TankModel[ TANK_DAMAGE_MODEL_COUNT ] = +{ + "models/bots/boss_bot/boss_tank.mdl", + "models/bots/boss_bot/boss_tank_damage1.mdl", + "models/bots/boss_bot/boss_tank_damage2.mdl", + "models/bots/boss_bot/boss_tank_damage3.mdl" +}; + +static const char *s_TankModelRome[ TANK_DAMAGE_MODEL_COUNT ] = +{ + "models/bots/tw2/boss_bot/boss_tank.mdl", + "models/bots/tw2/boss_bot/boss_tank_damage1.mdl", + "models/bots/tw2/boss_bot/boss_tank_damage2.mdl", + "models/bots/tw2/boss_bot/boss_tank_damage3.mdl" +}; + +#define TANK_LEFT_TRACK_MODEL "models/bots/boss_bot/tank_track_L.mdl" +#define TANK_RIGHT_TRACK_MODEL "models/bots/boss_bot/tank_track_R.mdl" +#define TANK_BOMB "models/bots/boss_bot/bomb_mechanism.mdl" +#define TANK_DESTRUCTION "models/bots/boss_bot/boss_tank_part1_destruction.mdl" + +#define TANK_LEFT_TRACK_MODEL_ROME "models/bots/tw2/boss_bot/tank_track_L.mdl" +#define TANK_RIGHT_TRACK_MODEL_ROME "models/bots/tw2/boss_bot/tank_track_R.mdl" +#define TANK_BOMB_ROME "models/bots/boss_bot/bomb_mechanism.mdl" +#define TANK_DESTRUCTION_ROME "models/bots/tw2/boss_bot/boss_tank_part1_destruction.mdl" + + +#define MVM_DESTROY_TANK_QUICKLY_TIME 25.0f + +float CTFTankBoss::m_flLastTankAlert = 0.0f; + + +class CTFTankDestruction : public CBaseAnimating +{ +public: + DECLARE_CLASS( CTFTankDestruction, CBaseAnimating ); + DECLARE_DATADESC(); + + CTFTankDestruction( void ); + + virtual void Precache( void ); + virtual void Spawn( void ); + + void AnimThink( void ); + +private: + + float m_flVanishTime; + +public: + + bool m_bIsAtCapturePoint; + int m_nDeathAnimPick; + char m_szDeathPostfix[ 8 ]; +}; + + +LINK_ENTITY_TO_CLASS( tank_destruction, CTFTankDestruction ); + +PRECACHE_REGISTER( tank_destruction ); + +BEGIN_DATADESC( CTFTankDestruction ) + DEFINE_THINKFUNC( AnimThink ), +END_DATADESC(); + + +CTFTankDestruction::CTFTankDestruction( void ) +{ + m_bIsAtCapturePoint = false; + m_nDeathAnimPick = 0; + m_szDeathPostfix[ 0 ] = '\0'; +} + +void CTFTankDestruction::Precache( void ) +{ + PrecacheModel( TANK_DESTRUCTION ); + PrecacheModel( TANK_DESTRUCTION_ROME ); + + PrecacheParticleSystem( "explosionTrail_seeds_mvm" ); + PrecacheParticleSystem( "fluidSmokeExpl_ring_mvm" ); + + PrecacheScriptSound( "MVM.TankExplodes" ); + + BaseClass::Precache(); +} + +void CTFTankDestruction::Spawn( void ) +{ + SetModel( TANK_DESTRUCTION ); + SetModelIndexOverride( VISION_MODE_NONE, modelinfo->GetModelIndex( TANK_DESTRUCTION ) ); + SetModelIndexOverride( VISION_MODE_ROME, modelinfo->GetModelIndex( TANK_DESTRUCTION_ROME ) ); + + BaseClass::Spawn(); + + int nDestroySequence = -1; + + int nDeathAnimPick = ( m_nDeathAnimPick != 0 ? m_nDeathAnimPick : RandomInt( 1, 3 ) ); + + if ( m_bIsAtCapturePoint ) + { + // Use map specific random + nDestroySequence = LookupSequence( UTIL_VarArgs( "destroy_%s%i%s", gpGlobals->mapname.ToCStr(), nDeathAnimPick, m_szDeathPostfix ) ); + } + + if ( nDestroySequence == -1 ) + { + // Fallback to default random + nDestroySequence = LookupSequence( UTIL_VarArgs( "destroy%i", nDeathAnimPick ) ); + } + + if ( nDestroySequence != -1 ) + { + SetSequence( nDestroySequence ); + SetPlaybackRate( 1.0f ); + SetCycle( 0 ); + } + + DispatchParticleEffect( "explosionTrail_seeds_mvm", GetAbsOrigin(), GetAbsAngles() ); + DispatchParticleEffect( "fluidSmokeExpl_ring_mvm", GetAbsOrigin(), GetAbsAngles() ); + + StopSound( "MVM.TankEngineLoop" ); + + CBroadcastRecipientFilter filter; + const Vector originVector = GetAbsOrigin(); + CBaseEntity::EmitSound( filter, SOUND_FROM_WORLD, "MVM.TankExplodes", &originVector ); + CBaseEntity::EmitSound( filter, SOUND_FROM_WORLD, "MVM.TankEnd" ); + + UTIL_ScreenShake( GetAbsOrigin(), 25.0f, 5.0f, 5.0f, 1000.0f, SHAKE_START ); + + m_flVanishTime = gpGlobals->curtime + SequenceDuration(); + + SetThink( &CTFTankDestruction::AnimThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); + + if ( nDestroySequence == -1 ) + { + SetThink( &CTFTankDestruction::SUB_FadeOut ); + SetNextThink( gpGlobals->curtime ); + } +} + +void CTFTankDestruction::AnimThink( void ) +{ + if ( gpGlobals->curtime > m_flVanishTime ) + { + SetThink( &CTFTankDestruction::SUB_FadeOut ); + SetNextThink( gpGlobals->curtime ); + return; + } + + StudioFrameAdvance(); + DispatchAnimEvents( this ); + SetNextThink( gpGlobals->curtime + 0.1f ); +} + + +LINK_ENTITY_TO_CLASS( tank_boss, CTFTankBoss ); + +PRECACHE_REGISTER( tank_boss ); + +IMPLEMENT_SERVERCLASS_ST( CTFTankBoss, DT_TFTankBoss) + //SendPropVector(SENDINFO(m_StartColor), 8, 0, 0, 1), +END_SEND_TABLE() + + +BEGIN_DATADESC( CTFTankBoss ) + + DEFINE_THINKFUNC( TankBossThink ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "DestroyIfAtCapturePoint", InputDestroyIfAtCapturePoint ), + DEFINE_INPUTFUNC( FIELD_STRING, "AddCaptureDestroyPostfix", InputAddCaptureDestroyPostfix ), + +END_DATADESC() + +//----------------------------------------------------------------------------------------------------- +void CMD_TankKill( void ) +{ + CBasePlayer *player = UTIL_GetCommandClient(); + if ( !player ) + return; + + CBaseEntity *tank = NULL; + while( ( tank = gEntList.FindEntityByClassname( tank, "tank_boss" ) ) != NULL ) + { + CTakeDamageInfo info( player, player, 9999999.9f, DMG_CRUSH, TF_DMG_CUSTOM_NONE ); + tank->TakeDamage( info ); + } +} +static ConCommand tf_mvm_tank_kill( "tf_mvm_tank_kill", CMD_TankKill, "", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//----------------------------------------------------------------------------------------------------- +void CMD_TankHealth( const CCommand& args ) +{ + CBasePlayer *player = UTIL_GetCommandClient(); + if ( !player ) + return; + + if ( args.ArgC() < 2 ) + { + Msg( "Usage: %s <health to set all active tanks to>\n", args[0] ); + return; + } + + CBaseEntity *tank = NULL; + while( ( tank = gEntList.FindEntityByClassname( tank, "tank_boss" ) ) != NULL ) + { + tank->SetMaxHealth( atoi( args[1] ) ); + tank->SetHealth( atoi( args[1] ) ); + } +} +static ConCommand tf_mvm_tank_health( "tf_mvm_tank_health", CMD_TankHealth, "", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------- +CTFTankBoss::CTFTankBoss() +{ + m_goalNode = NULL; + m_body = new CTFTankBossBody( this ); + m_exhaustAttachment = -1; + m_isSmoking = false; + m_bIsPlayerKilled = true; + m_bPlayedHalfwayAlert = false; + m_bPlayedNearAlert = false; + m_damageModelIndex = 0; + m_pWaveSpawnPopulator = NULL; + m_nDeathAnimPick = 0; + m_szDeathPostfix[ 0 ] = '\0'; + m_flDroppingStart = 0.0f; + m_flSpawnTime = 0.0f; +} + + +//-------------------------------------------------------------------------------------- +CTFTankBoss::~CTFTankBoss() +{ + delete m_body; +} + + +//-------------------------------------------------------------------------------------- +void CTFTankBoss::Precache( void ) +{ + for( int i=0; i<TANK_DAMAGE_MODEL_COUNT; ++i ) + { + PrecacheModel( s_TankModel[i] ); + PrecacheModel( s_TankModelRome[i] ); + } + + PrecacheModel( TANK_BOMB ); + PrecacheModel( TANK_LEFT_TRACK_MODEL ); + PrecacheModel( TANK_RIGHT_TRACK_MODEL ); + + PrecacheModel( TANK_BOMB_ROME ); + PrecacheModel( TANK_LEFT_TRACK_MODEL_ROME ); + PrecacheModel( TANK_RIGHT_TRACK_MODEL_ROME ); + + PrecacheParticleSystem( "smoke_train" ); + PrecacheParticleSystem( "bot_impact_light" ); + PrecacheParticleSystem( "bot_impact_heavy" ); + + PrecacheScriptSound( "MVM.TankEngineLoop" ); + PrecacheScriptSound( "MVM.TankPing" ); + PrecacheScriptSound( "MVM.TankDeploy" ); + PrecacheScriptSound( "MVM.TankStart" ); + PrecacheScriptSound( "MVM.TankEnd" ); + PrecacheScriptSound( "MVM.TankSmash" ); + + BaseClass::Precache(); +} + +//-------------------------------------------------------------------------------------- +int CTFTankBoss::GetCurrencyValue( void ) +{ + if ( m_goalNode == NULL && !m_bIsPlayerKilled ) + { + return 0; + } + + if ( m_pWaveSpawnPopulator ) + { + return m_pWaveSpawnPopulator->GetCurrencyAmountPerDeath(); + } + + return BaseClass::GetCurrencyValue(); +} + +void CTFTankBoss::InputDestroyIfAtCapturePoint( inputdata_t &inputdata ) +{ + m_nDeathAnimPick = inputdata.value.Int(); + + if ( m_goalNode == NULL ) + { + TakeDamage( CTakeDamageInfo( GetContainingEntity(INDEXENT(0)), GetContainingEntity(INDEXENT(0)), 9999999.9f, DMG_CRUSH, TF_DMG_CUSTOM_NONE ) ); + } +} + +void CTFTankBoss::InputAddCaptureDestroyPostfix( inputdata_t &inputdata ) +{ + V_strncpy( m_szDeathPostfix, inputdata.value.String(), ARRAYSIZE( m_szDeathPostfix ) ); +} + + +//-------------------------------------------------------------------------------------- +void CTFTankBoss::Spawn( void ) +{ + if ( ( !TFGameRules() || !TFGameRules()->IsMannVsMachineMode() ) && GetInitialHealth() == 0 ) + { + if ( GetHealth() > 0 ) + SetInitialHealth( GetHealth() ); + else + SetInitialHealth( TANK_DEFAULT_HEALTH ); + } + + BaseClass::Spawn(); + m_vCollisionMins.Init(); + m_vCollisionMaxs.Init(); + + ChangeTeam( TF_TEAM_PVE_INVADERS ); + + m_damageModelIndex = 0; + SetModel( s_TankModel[ m_damageModelIndex ] ); + SetModelIndexOverride( VISION_MODE_NONE, modelinfo->GetModelIndex( s_TankModel[ m_damageModelIndex ] ) ); + SetModelIndexOverride( VISION_MODE_ROME, modelinfo->GetModelIndex( s_TankModelRome[ m_damageModelIndex ] ) ); + m_lastHealth = GetMaxHealth(); + + AddGlowEffect(); + + m_leftTracks = (CBaseAnimating *)CreateEntityByName( "prop_dynamic" ); + if ( m_leftTracks ) + { + m_leftTracks->SetModel( TANK_LEFT_TRACK_MODEL ); + m_leftTracks->SetModelIndexOverride( VISION_MODE_NONE, modelinfo->GetModelIndex( TANK_LEFT_TRACK_MODEL ) ); + m_leftTracks->SetModelIndexOverride( VISION_MODE_ROME, modelinfo->GetModelIndex( TANK_LEFT_TRACK_MODEL_ROME ) ); + + // bonemerge into our model + m_leftTracks->FollowEntity( this, true ); + + int animSequence = m_leftTracks->LookupSequence( "forward" ); + if ( animSequence ) + { + m_leftTracks->SetSequence( animSequence ); + m_leftTracks->SetPlaybackRate( 1.0f ); + m_leftTracks->SetCycle( 0 ); + m_leftTracks->ResetSequenceInfo(); + } + + m_lastLeftTrackPos = m_leftTracks->GetAbsOrigin(); + } + + m_rightTracks = (CBaseAnimating *)CreateEntityByName( "prop_dynamic" ); + if ( m_rightTracks ) + { + m_rightTracks->SetModel( TANK_RIGHT_TRACK_MODEL ); + m_rightTracks->SetModelIndexOverride( VISION_MODE_NONE, modelinfo->GetModelIndex( TANK_RIGHT_TRACK_MODEL ) ); + m_rightTracks->SetModelIndexOverride( VISION_MODE_ROME, modelinfo->GetModelIndex( TANK_RIGHT_TRACK_MODEL_ROME ) ); + + // bonemerge into our model + m_rightTracks->FollowEntity( this, true ); + + int animSequence = m_rightTracks->LookupSequence( "forward" ); + if ( animSequence ) + { + m_rightTracks->SetSequence( animSequence ); + m_rightTracks->SetPlaybackRate( 1.0f ); + m_rightTracks->SetCycle( 0 ); + m_rightTracks->ResetSequenceInfo(); + } + + m_lastRightTrackPos = m_rightTracks->GetAbsOrigin(); + } + + m_bomb = (CBaseAnimating *)CreateEntityByName( "prop_dynamic" ); + if ( m_bomb ) + { + m_bomb->SetModel( TANK_BOMB ); + m_bomb->SetModelIndexOverride( VISION_MODE_NONE, modelinfo->GetModelIndex( TANK_BOMB ) ); + m_bomb->SetModelIndexOverride( VISION_MODE_ROME, modelinfo->GetModelIndex( TANK_BOMB_ROME ) ); + + // bonemerge into our model + m_bomb->FollowEntity( this, true ); + } + + GetBodyInterface()->StartSequence( "movement" ); + + m_exhaustAttachment = LookupAttachment( "smoke_attachment" ); + + if ( m_goalNode == NULL ) + { + m_goalNode = dynamic_cast< CPathTrack * >( gEntList.FindEntityByClassname( NULL, "path_track" ) ); + + if ( m_goalNode ) + { + // find first node + while( m_goalNode->GetPrevious() ) + { + m_goalNode = m_goalNode->GetPrevious(); + } + + SetAbsOrigin( m_goalNode->WorldSpaceCenter() ); + } + } + else + { + SetAbsOrigin( m_goalNode->WorldSpaceCenter() ); + } + + // We've traveled nowhere if we're at the first node + m_fTotalDistance = 0.0f; + m_CumulativeDistances.AddToTail( m_fTotalDistance ); + + // Remember starting node + m_startNode = m_goalNode; + m_endNode = m_startNode; + m_nNodeNumber = 0; + + // Orient the Tank along the path + if ( m_goalNode != NULL ) + { + CPathTrack *pPrevNode = m_goalNode; + CPathTrack *pNextNode = m_goalNode->GetNext(); + + if ( pNextNode ) + { + Vector along = pNextNode->GetAbsOrigin() - m_goalNode->GetAbsOrigin(); + + QAngle angles; + VectorAngles( along, angles ); + + SetAbsAngles( angles ); + + // Find last node and calculate cumulative distance + while( pNextNode ) + { + along = pNextNode->GetAbsOrigin() - pPrevNode->GetAbsOrigin(); + along.z = 0.0f; + + m_fTotalDistance += along.Length(); + m_CumulativeDistances.AddToTail( m_fTotalDistance ); + + pPrevNode = pNextNode; + pNextNode = pNextNode->GetNext(); + } + } + } + + SetBloodColor( DONT_BLEED ); + + m_flLastPingTime = gpGlobals->curtime; + + CBroadcastRecipientFilter filter; + EmitSound( filter, entindex(), "MVM.TankEngineLoop" ); + EmitSound( "MVM.TankStart" ); + + if ( TFGameRules() ) + { + int nTankCount = 0; + + CBaseEntity *tank = NULL; + while( ( tank = gEntList.FindEntityByClassname( tank, "tank_boss" ) ) != NULL ) + { + nTankCount++; + } + + if ( nTankCount <= 1 ) + { + if ( m_flLastTankAlert + 5.0f < gpGlobals->curtime ) + { + CWave *pWave = g_pPopulationManager ? g_pPopulationManager->GetCurrentWave() : NULL; + if ( pWave && pWave->NumTanksSpawned() > 1 ) + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Tank_Alert_Another" ); + } + else + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Tank_Alert_Spawn" ); + } + + m_flLastTankAlert = gpGlobals->curtime; + } + } + else + { + // Don't worry about when the last alert was in this case because 2 tanks can spawn at once + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Tank_Alert_Multiple" ); + m_flLastTankAlert = gpGlobals->curtime; + } + + TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_TANK_CALLOUT, TF_TEAM_PVE_DEFENDERS ); + } + + m_isDroppingBomb = false; + m_flDroppingStart = 0.0f; + m_flSpawnTime = gpGlobals->curtime; + + SetThink( &CTFTankBoss::TankBossThink ); + SetNextThink( gpGlobals->curtime ); +} + + +//-------------------------------------------------------------------------------------- +void CTFTankBoss::UpdateOnRemove( void ) +{ + StopSound( "MVM.TankEngineLoop" ); + + if ( TFObjectiveResource() ) + { + TFObjectiveResource()->DecrementMannVsMachineWaveClassCount( MAKE_STRING( "tank" ), MVM_CLASS_FLAG_NORMAL | MVM_CLASS_FLAG_MINIBOSS ); + } + + BaseClass::UpdateOnRemove(); +} + + +int CTFTankBoss::OnTakeDamage_Alive( const CTakeDamageInfo &rawInfo ) +{ + if ( static_cast< float >( GetHealth() ) / GetMaxHealth() > 0.3f ) + { + DispatchParticleEffect( "bot_impact_light", rawInfo.GetDamagePosition(), vec3_angle ); + } + else + { + DispatchParticleEffect( "bot_impact_heavy", rawInfo.GetDamagePosition(), vec3_angle ); + } + + // Calculate Final Damage values + if ( BaseClass::OnTakeDamage_Alive( rawInfo ) && rawInfo.GetAttacker() ) + { + // track who damaged us + CTFPlayer *pTFPlayer = dynamic_cast< CTFPlayer* >( rawInfo.GetAttacker() ); + if ( pTFPlayer ) + { + // is the attacker being healed by any Medic(s)? + CUtlVector<CTFPlayer*> pTempPlayerQueue; + pTFPlayer->AddConnectedPlayers( pTempPlayerQueue, pTFPlayer ); + + for ( int i = 0 ; i < pTempPlayerQueue.Count() ; i++ ) + { + EntityHistory_t newHist; + newHist.hEntity = pTempPlayerQueue[i]; + newHist.flTimeDamage = gpGlobals->curtime; + m_vecDamagers.InsertHistory( newHist ); + } + + // Report Tank dmg to Stats + CMannVsMachineStats *pStats = MannVsMachineStats_GetInstance(); + if ( pStats ) + { + pStats->PlayerEvent_DealtDamageToTanks( pTFPlayer, rawInfo.GetDamage() ); + } + } + + return 1; + } + + return 0; +} + + +//-------------------------------------------------------------------------------------- +void CTFTankBoss::Event_Killed( const CTakeDamageInfo &info ) +{ + m_bIsPlayerKilled = ( info.GetDamageType() & DMG_CRUSH ) == 0; + + Explode(); + + // check for MvM achievement + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + if ( FStrEq( "mvm_rottenburg", STRING( gpGlobals->mapname ) ) ) + { + CLogicRelay *pLogicRelay = dynamic_cast< CLogicRelay* >( gEntList.FindEntityByName( NULL, "Barricade_Achievement_Check" ) ); + if ( pLogicRelay && !pLogicRelay->IsDisabled() ) + { + CUtlVector<CTFPlayer *> playerVector; + CollectPlayers( &playerVector, TF_TEAM_PVE_DEFENDERS ); + FOR_EACH_VEC( playerVector, i ) + { + if ( !playerVector[i] ) + continue; + + if ( playerVector[i]->IsBot() ) + continue; + + playerVector[i]->AwardAchievement( ACHIEVEMENT_TF_MVM_MAPS_ROTTENBURG_TANK ); + } + } + } + } + + BaseClass::Event_Killed( info ); +} + + +//-------------------------------------------------------------------------------------- +void CTFTankBoss::SetStartingPathTrackNode( char *name ) +{ + m_goalNode = dynamic_cast< CPathTrack * >( gEntList.FindEntityByName( NULL, name ) ); +} + + +//----------------------------------------------------------------------------------------------------- +void CTFTankBoss::TankBossThink( void ) +{ + // damage states + if ( GetHealth() != m_lastHealth ) + { + // health changed - potentially change damage model + m_lastHealth = GetHealth(); + + int healthPerModel = GetMaxHealth() / TANK_DAMAGE_MODEL_COUNT; + int healthThreshold = GetMaxHealth() - healthPerModel; + + int desiredModelIndex; + for( desiredModelIndex = 0; desiredModelIndex < TANK_DAMAGE_MODEL_COUNT; ++desiredModelIndex ) + { + if ( GetHealth() > healthThreshold ) + { + break; + } + + healthThreshold -= healthPerModel; + } + + if ( desiredModelIndex >= TANK_DAMAGE_MODEL_COUNT ) + { + desiredModelIndex = TANK_DAMAGE_MODEL_COUNT-1; + } + + if ( desiredModelIndex != m_damageModelIndex ) + { + // update model + const char *pchSequence = GetSequenceName( GetSequence() ); + float fCycle = GetCycle(); + + m_damageModelIndex = desiredModelIndex; + SetModel( s_TankModel[ m_damageModelIndex ] ); + SetModelIndexOverride( VISION_MODE_NONE, modelinfo->GetModelIndex( s_TankModel[ m_damageModelIndex ] ) ); + SetModelIndexOverride( VISION_MODE_ROME, modelinfo->GetModelIndex( s_TankModelRome[ m_damageModelIndex ] ) ); + + int nAnimSequence = LookupSequence( pchSequence ); + if ( nAnimSequence > 0 ) + { + SetSequence( nAnimSequence ); + SetPlaybackRate( 1.0f ); + ResetSequenceInfo(); + SetCycle( fCycle ); + } + else + { + GetBodyInterface()->StartSequence( "movement" ); + } + } + } + + // left/right track speed + const float trackMaxSpeed = 80.0f; + const float trackOffset = 56.221f; + + Vector forward, right, up; + GetVectors( &forward, &right, &up ); + + if ( m_leftTracks ) + { + Vector trackCenter = GetAbsOrigin() - trackOffset * right; + + float speed = ( trackCenter - m_lastLeftTrackPos ).Length() / gpGlobals->frametime; + + if ( speed >= trackMaxSpeed ) + { + m_leftTracks->SetPlaybackRate( 1.0f ); + } + else + { + m_leftTracks->SetPlaybackRate( speed / trackMaxSpeed ); + } + + m_lastLeftTrackPos = trackCenter; + } + + if ( m_rightTracks ) + { + Vector trackCenter = GetAbsOrigin() + trackOffset * right; + + float speed = ( trackCenter - m_lastRightTrackPos ).Length() / gpGlobals->frametime; + + if ( speed >= trackMaxSpeed ) + { + m_rightTracks->SetPlaybackRate( 1.0f ); + } + else + { + m_rightTracks->SetPlaybackRate( speed / trackMaxSpeed ); + } + + m_lastRightTrackPos = trackCenter; + } + + + if ( m_goalNode != NULL ) + { + Vector toGoal = m_goalNode->WorldSpaceCenter() - GetAbsOrigin(); + toGoal.z = 0.0f; + float range = toGoal.NormalizeInPlace(); + + if ( GetParent() ) + { + // Track train might be closer + toGoal = m_goalNode->WorldSpaceCenter() - GetParent()->GetAbsOrigin(); + toGoal.z = 0.0f; + float flTempRange = toGoal.NormalizeInPlace(); + range = MIN( range, flTempRange ); + } + + if ( TFGameRules() ) + { + if ( m_nNodeNumber <= 0 ) + { + //TFGameRules()->SetBossNormalizedTravelDistance( 0.0f ); + } + else + { + Assert( m_CumulativeDistances.IsValidIndex( m_nNodeNumber ) ); + float fBaseDistance = m_CumulativeDistances[ m_nNodeNumber - 1 ]; + float fDistanceFromPreviousToNext = m_CumulativeDistances[ m_nNodeNumber ] - fBaseDistance; + float fDistanceTraveled = fBaseDistance + ( fDistanceFromPreviousToNext - range ); + float fDistancePercent = fDistanceTraveled / m_fTotalDistance; + //TFGameRules()->SetBossNormalizedTravelDistance( fDistancePercent ); + + if ( m_flLastTankAlert + 5.0f < gpGlobals->curtime ) + { + if ( !m_bPlayedNearAlert && fDistancePercent > 0.75f) + { + TFGameRules()->PlayThrottledAlert( 255, "Announcer.MVM_Tank_Alert_Near_Hatch", 5.0f ); + m_flLastTankAlert = gpGlobals->curtime; + m_bPlayedNearAlert = true; + } + else if ( !m_bPlayedHalfwayAlert && fDistancePercent > 0.5f) + { + int nTankCount = 0; + + CBaseEntity *tank = NULL; + while( ( tank = gEntList.FindEntityByClassname( tank, "tank_boss" ) ) != NULL ) + { + nTankCount++; + } + + if ( nTankCount > 1 ) + { + TFGameRules()->PlayThrottledAlert( 255, "Announcer.MVM_Tank_Alert_Halfway_Multiple", 5.0f ); + } + else + { + TFGameRules()->PlayThrottledAlert( 255, "Announcer.MVM_Tank_Alert_Halfway", 5.0f ); + } + + m_flLastTankAlert = gpGlobals->curtime; + m_bPlayedHalfwayAlert = true; + } + } + } + } + + if ( range < 20.0f ) + { + // reached node + inputdata_t dummyData; + dummyData.pActivator = this; + dummyData.pCaller = this; + dummyData.nOutputID = 0; + + m_goalNode->InputPass( dummyData ); + + m_goalNode = m_goalNode->GetNext(); + m_nNodeNumber++; + + if ( m_goalNode == NULL && m_bomb ) + { + //DevMsg( "Tank's final position: %.2f %.2f %.2f", GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z ); + + /*if ( TFGameRules() ) + { + TFGameRules()->SetBossNormalizedTravelDistance( 1.0f ); + }*/ + + // reached end of track - deploy the bomb + int animSequence = m_bomb->LookupSequence( "deploy" ); + if ( animSequence ) + { + m_bomb->SetSequence( animSequence ); + m_bomb->SetPlaybackRate( 1.0f ); + m_bomb->SetCycle( 0 ); + m_bomb->ResetSequenceInfo(); + } + + animSequence = LookupSequence( "deploy" ); + if ( animSequence ) + { + SetSequence( animSequence ); + SetPlaybackRate( 1.0f ); + SetCycle( 0 ); + ResetSequenceInfo(); + } + + if ( m_flLastTankAlert + 5.0f < gpGlobals->curtime ) + { + TFGameRules()->PlayThrottledAlert( 255, "Announcer.MVM_Tank_Alert_Deploying", 5.0f ); + m_flLastTankAlert = gpGlobals->curtime; + m_bPlayedNearAlert = true; + } + + m_isDroppingBomb = true; + m_flDroppingStart = gpGlobals->curtime; + + StopSound( "MVM.TankEngineLoop" ); + + EmitSound( "MVM.TankDeploy" ); + + TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_TANK_DEPLOYING, TF_TEAM_PVE_DEFENDERS ); + } + } + + if ( m_goalNode ) + { + Vector goal = m_goalNode->WorldSpaceCenter(); + + GetLocomotionInterface()->SetDesiredSpeed( GetMaxSpeed() ); + GetLocomotionInterface()->Approach( goal ); + GetLocomotionInterface()->FaceTowards( goal ); + + if ( m_rumbleTimer.IsElapsed() ) + { + m_rumbleTimer.Start( 0.25f ); + + // shake nearby players' screens. + UTIL_ScreenShake( GetAbsOrigin(), 2.0f, 5.0f, 1.0f, 500.0f, SHAKE_START ); + } + } + } + + if ( m_isDroppingBomb && IsSequenceFinished() ) + { + FirePopFileEvent( &m_onBombDroppedEventInfo ); + m_isDroppingBomb = false; + + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Tank_Planted" ); + } + + // if the Tank is driving under something, shut off its smokestack + if ( m_exhaustAttachment > 0 ) + { + Vector smokePos; + GetAttachment( m_exhaustAttachment, smokePos ); + + trace_t result; + UTIL_TraceLine( smokePos, smokePos + Vector( 0, 0, 300.0f ), MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &result ); + + if ( result.DidHit() ) + { + if ( m_isSmoking ) + { + StopParticleEffects( this ); + m_isSmoking = false; + } + } + else if ( !m_isSmoking ) + { + DispatchParticleEffect( "smoke_train", PATTACH_POINT_FOLLOW, this, m_exhaustAttachment ); + m_isSmoking = true; + } + } + + // destroy things we drive into/over + if ( m_crushTimer.IsElapsed() ) + { + m_crushTimer.Start( 0.5f ); + + const int maxCollectedEntities = 64; + CBaseEntity *intersectingEntities[ maxCollectedEntities ]; + int count = UTIL_EntitiesInBox( intersectingEntities, maxCollectedEntities, + GetAbsOrigin() + WorldAlignMins() * 0.75f, // a little fudge room for players on the top or sides + GetAbsOrigin() + WorldAlignMaxs() * 0.75f, + FL_CLIENT | FL_OBJECT ); + + for( int i = 0; i < count; ++i ) + { + CBaseEntity *victim = intersectingEntities[i]; + + if ( victim == NULL ) + continue; + + int damage = MAX( victim->GetMaxHealth(), victim->GetHealth() ); + + CTakeDamageInfo info( this, this, 4 * damage, DMG_CRUSH, TF_DMG_CUSTOM_NONE ); + victim->TakeDamage( info ); + } + } + + UpdatePingSound(); + + BaseClass::BossThink(); +} + + +//----------------------------------------------------------------------------------------------------- +void CTFTankBoss::ModifyDamage( CTakeDamageInfo *info ) const +{ + CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase * >( info->GetWeapon() ); + + if ( pWeapon && pWeapon->GetWeaponID() == TF_WEAPON_MINIGUN ) + { + // miniguns are crazy powerful when all bullets always hit + const float minigunFactor = 0.25f; + info->SetDamage( info->GetDamage() * minigunFactor ); + } +} + +void CTFTankBoss::UpdateCollisionBounds( void ) +{ + // Remember the initial bounds + if ( m_vCollisionMins.IsZero() || m_vCollisionMaxs.IsZero() ) + { + m_vCollisionMins = WorldAlignMins(); + m_vCollisionMaxs = WorldAlignMaxs(); + } + + // When the tank is at a diagonal angle we don't want the bounds to bloat too far + float flDiagonalShrinkMultiplier = 1.0f - fabsf( sinf( DEG2RAD( GetAbsAngles().y ) * 2.0f ) ) * 0.4f; + + Vector vMins = m_vCollisionMins; + vMins.x *= flDiagonalShrinkMultiplier; + vMins.y *= flDiagonalShrinkMultiplier; + + Vector vMaxs = m_vCollisionMaxs; + vMaxs.x *= flDiagonalShrinkMultiplier; + vMaxs.y *= flDiagonalShrinkMultiplier; + + // Build new world aligned bounds based on how it's rotated + VMatrix rot; + MatrixFromAngles( GetAbsAngles(), rot ); + + Vector vMinsOut, vMaxsOut; + TransformAABB( rot.As3x4(), vMins, vMaxs, vMinsOut, vMaxsOut ); + CollisionProp()->SetCollisionBounds( vMinsOut, vMaxsOut ); +} + + +//----------------------------------------------------------------------------------------------------- +void CTFTankBoss::FirePopFileEvent( EventInfo *eventInfo ) +{ + if ( eventInfo && eventInfo->m_action.Length() > 0 ) + { + CBaseEntity *targetEntity = gEntList.FindEntityByName( NULL, eventInfo->m_target ); + if ( !targetEntity ) + { + Warning( "CTFTankBoss: Can't find target entity '%s' for event\n", eventInfo->m_target.Access() ); + } + else + { + g_EventQueue.AddEvent( targetEntity, eventInfo->m_action, 0.0f, NULL, NULL ); + } + } +} + +void CTFTankBoss::Explode( void ) +{ + StopSound( "MVM.TankEngineLoop" ); + + FirePopFileEvent( &m_onKilledEventInfo ); + + CTFTankDestruction *pDestruction = dynamic_cast< CTFTankDestruction* >( CreateEntityByName( "tank_destruction" ) ); + if ( pDestruction ) + { + // Only do special capture point death if it was force killed by bomb drop + pDestruction->m_bIsAtCapturePoint = ( m_goalNode == NULL && !m_bIsPlayerKilled ); + pDestruction->m_nDeathAnimPick = m_nDeathAnimPick; + V_strncpy( pDestruction->m_szDeathPostfix, m_szDeathPostfix, ARRAYSIZE( pDestruction->m_szDeathPostfix ) ); + + pDestruction->SetAbsOrigin( GetAbsOrigin() ); + pDestruction->SetAbsAngles( GetAbsAngles() ); + DispatchSpawn( pDestruction ); + } + + if ( m_bIsPlayerKilled ) + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_General_Destruction" ); + TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_TANK_DEAD, TF_TEAM_PVE_DEFENDERS ); + + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_tank_destroyed_by_players" ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + // ACHIEVEMENT_TF_MVM_DESTROY_TANK_WHILE_DEPLOYING + if ( m_isDroppingBomb ) + { + // short delay so you only get the achievement if the bomb doors have opened/closed and it's ready to deploy + if ( gpGlobals->curtime - m_flDroppingStart > 5.8f ) + { + // anyone who has damaged the tank since the deploy anim began will get the achievement + float flWindow = gpGlobals->curtime - m_flDroppingStart; + + for ( int i = 0; i < m_vecDamagers.Count(); i++ ) + { + // get the achievement if you have damaged the tank since the deploy anim began + if ( ( gpGlobals->curtime - m_vecDamagers[i].flTimeDamage ) < flWindow ) + { + CTFPlayer *pTFPlayer = dynamic_cast< CTFPlayer* >( m_vecDamagers[i].hEntity.Get() ); + if ( pTFPlayer ) + { + pTFPlayer->AwardAchievement( ACHIEVEMENT_TF_MVM_DESTROY_TANK_WHILE_DEPLOYING ); + } + } + } + } + } + + // ACHIEVEMENT_TF_MVM_DESTROY_TANK_QUICKLY + if ( ( gpGlobals->curtime - m_flSpawnTime ) < MVM_DESTROY_TANK_QUICKLY_TIME ) + { + for ( int i = 0; i < m_vecDamagers.Count(); i++ ) + { + // get the achievement if you have damaged the tank since the deploy anim began + if ( ( gpGlobals->curtime - m_vecDamagers[i].flTimeDamage ) < MVM_DESTROY_TANK_QUICKLY_TIME ) + { + CTFPlayer *pTFPlayer = dynamic_cast< CTFPlayer* >( m_vecDamagers[i].hEntity.Get() ); + if ( pTFPlayer ) + { + pTFPlayer->AwardAchievement( ACHIEVEMENT_TF_MVM_DESTROY_TANK_QUICKLY ); + } + } + } + } + + // strange part credit? (this logic isn't so correct -- it'll try to grant the credit to the active + // weapon of anyone who damaged the tank, *not* the weapon that actually did the damage, as we don't + // track that) + FOR_EACH_VEC( m_vecDamagers, i ) + { + CTFPlayer *pTFPlayer = dynamic_cast< CTFPlayer* >( m_vecDamagers[i].hEntity.Get() ); + if ( !pTFPlayer ) + continue; + + EconEntity_OnOwnerKillEaterEventNoPartner( pTFPlayer->GetActiveTFWeapon(), pTFPlayer, kKillEaterEvent_TanksDestroyed ); + } + } + } +} +#define TANK_PING_TIME 5.0 +void CTFTankBoss::UpdatePingSound( void ) +{ + if( gpGlobals->curtime - m_flLastPingTime >= TANK_PING_TIME ) + { + m_flLastPingTime = gpGlobals->curtime; + EmitSound( "MVM.TankPing"); + } +} + diff --git a/game/server/tf/player_vs_environment/tf_tank_boss.h b/game/server/tf/player_vs_environment/tf_tank_boss.h new file mode 100644 index 0000000..005a490 --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_tank_boss.h @@ -0,0 +1,131 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#ifndef TF_TANK_BOSS_H +#define TF_TANK_BOSS_H + +#include "tf_population_manager.h" +#include "tf_base_boss.h" +#include "tf_tank_boss_body.h" +#include "tf_achievementdata.h" + +//---------------------------------------------------------------------------- +class CTFTankBoss : public CTFBaseBoss +{ +public: + DECLARE_CLASS( CTFTankBoss, CTFBaseBoss ); + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CTFTankBoss(); + virtual ~CTFTankBoss(); + + virtual void Precache( void ); + virtual void Spawn( void ); + virtual void SetSkin( int nSkin ) { if ( m_body ) m_body->SetSkin( nSkin ); } + + virtual void UpdateOnRemove( void ); + + virtual void UpdateCollisionBounds( void ); + + virtual CTFTankBossBody *GetBodyInterface( void ) const { return m_body; } + + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &rawInfo ); + + virtual void Event_Killed( const CTakeDamageInfo &info ); + + void TankBossThink( void ); + + void SetStartingPathTrackNode( char *name ); + + void DefineOnKilledOutput( EventInfo *eventInfo ); + void DefineOnBombDroppedOutput( EventInfo *eventInfo ); + + void SetWaveSpawnPopulator( CWaveSpawnPopulator *pWave ){ m_pWaveSpawnPopulator = pWave; } + + virtual int GetCurrencyValue( void ); + + // Input handlers + void InputDestroyIfAtCapturePoint( inputdata_t &inputdata ); + void InputAddCaptureDestroyPostfix( inputdata_t &inputdata ); + + void UpdatePingSound( void ); + +protected: + virtual void ModifyDamage( CTakeDamageInfo *info ) const; + +private: + + void Explode(); + +private: + CTFTankBossBody *m_body; + + CHandle< CPathTrack > m_startNode; + CHandle< CPathTrack > m_endNode; + CHandle< CPathTrack > m_goalNode; + CUtlVector< float > m_CumulativeDistances; + float m_fTotalDistance; + int m_nNodeNumber; + + float m_flSpawnTime; + + bool m_isDroppingBomb; + float m_flDroppingStart; + + int m_exhaustAttachment; + bool m_isSmoking; + + bool m_bIsPlayerKilled; + bool m_bPlayedHalfwayAlert; + bool m_bPlayedNearAlert; + + int m_lastHealth; + int m_damageModelIndex; + int m_nDeathAnimPick; + char m_szDeathPostfix[ 8 ]; + + Vector m_lastRightTrackPos; + Vector m_lastLeftTrackPos; + + CountdownTimer m_rumbleTimer; + + EventInfo m_onKilledEventInfo; + EventInfo m_onBombDroppedEventInfo; + void FirePopFileEvent( EventInfo *eventInfo ); + + CHandle< CBaseAnimating > m_bomb; + CHandle< CBaseAnimating > m_leftTracks; + CHandle< CBaseAnimating > m_rightTracks; + + CountdownTimer m_crushTimer; + CWaveSpawnPopulator *m_pWaveSpawnPopulator; + + Vector m_vCollisionMins; + Vector m_vCollisionMaxs; + + float m_flLastPingTime; + + static float m_flLastTankAlert; + + CHistoryVector< EntityHistory_t, CEntityHistoryLess, 12 > m_vecDamagers; +}; + + +inline void CTFTankBoss::DefineOnKilledOutput( EventInfo *eventInfo ) +{ + if ( eventInfo ) + { + m_onKilledEventInfo.m_action = eventInfo->m_action; + m_onKilledEventInfo.m_target = eventInfo->m_target; + } +} + +inline void CTFTankBoss::DefineOnBombDroppedOutput( EventInfo *eventInfo ) +{ + if ( eventInfo ) + { + m_onBombDroppedEventInfo.m_action = eventInfo->m_action; + m_onBombDroppedEventInfo.m_target = eventInfo->m_target; + } +} + +#endif // TF_TANK_BOSS_H diff --git a/game/server/tf/player_vs_environment/tf_tank_boss_body.cpp b/game/server/tf/player_vs_environment/tf_tank_boss_body.cpp new file mode 100644 index 0000000..d3dd638 --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_tank_boss_body.cpp @@ -0,0 +1,58 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#include "cbase.h" + +#include "NextBot.h" +#include "tf_tank_boss.h" +#include "tf_tank_boss_body.h" + + +//------------------------------------------------------------------------------------------- +CTFTankBossBody::CTFTankBossBody( INextBot *bot ) : IBody( bot ) +{ +} + + +//------------------------------------------------------------------------------------------- +bool CTFTankBossBody::StartSequence( const char *name ) +{ + CTFTankBoss *me = (CTFTankBoss *)GetBot()->GetEntity(); + + int animSequence = me->LookupSequence( name ); + + if ( animSequence ) + { + me->SetSequence( animSequence ); + me->SetPlaybackRate( 1.0f ); + me->SetCycle( 0 ); + me->ResetSequenceInfo(); + + return true; + } + + return false; +} + +void CTFTankBossBody::SetSkin( int nSkin ) +{ + CTFTankBoss *me = (CTFTankBoss *)GetBot()->GetEntity(); + me->m_nSkin = nSkin; +} + + +//------------------------------------------------------------------------------------------- +void CTFTankBossBody::Update( void ) +{ + CTFTankBoss *me = (CTFTankBoss *)GetBot()->GetEntity(); + + // move the animation ahead in time + me->StudioFrameAdvance(); + me->DispatchAnimEvents( me ); +} + + +//--------------------------------------------------------------------------------------------- +// return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface) +unsigned int CTFTankBossBody::GetSolidMask( void ) const +{ + return CONTENTS_SOLID; +} diff --git a/game/server/tf/player_vs_environment/tf_tank_boss_body.h b/game/server/tf/player_vs_environment/tf_tank_boss_body.h new file mode 100644 index 0000000..761e871 --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_tank_boss_body.h @@ -0,0 +1,30 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#ifndef TF_TANK_BOSS_BODY_H +#define TF_TANK_BOSS_BODY_H + +#include "animation.h" +#include "NextBotBodyInterface.h" + +class INextBot; + + +//---------------------------------------------------------------------------------------------------------------- +/** + * The interface for control and information about the bot's body state (posture, animation state, etc) + */ +class CTFTankBossBody : public IBody +{ +public: + CTFTankBossBody( INextBot *bot ); + virtual ~CTFTankBossBody() { } + + virtual void Update( void ); + virtual unsigned int GetSolidMask( void ) const; // return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface) + + bool StartSequence( const char *name ); + void SetSkin( int nSkin ); +}; + + + +#endif // TF_TANK_BOSS_BODY_H diff --git a/game/server/tf/player_vs_environment/tf_upgrades.cpp b/game/server/tf/player_vs_environment/tf_upgrades.cpp new file mode 100644 index 0000000..dd27c07 --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_upgrades.cpp @@ -0,0 +1,941 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Load item upgrade data from KeyValues +// +// $NoKeywords: $ +//============================================================================= + +#include "cbase.h" + +#include "tf_shareddefs.h" +#include "tf_upgrades.h" +#include "tf_upgrades_shared.h" +#include "econ_item_system.h" +#include "dt_utlvector_send.h" +#include "tf_player.h" +#include "econ_wearable.h" +#include "tf_item_powerup_bottle.h" +#include "tf_mann_vs_machine_stats.h" +#include "tf_weapon_wrench.h" +#include "tf_weapon_builder.h" +#include "tf_objective_resource.h" + + +extern ConVar tf_mvm_skill; +extern ConVar *sv_cheats; + +CHandle<CUpgrades> g_hUpgradeEntity; + + +BEGIN_DATADESC( CUpgrades ) + DEFINE_KEYFIELD( m_nStartDisabled, FIELD_INTEGER, "start_disabled" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + + DEFINE_ENTITYFUNC( UpgradeTouch ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( func_upgradestation, CUpgrades ); + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CUpgrades::Spawn( void ) +{ + BaseClass::Spawn(); + + // Don't do anything if we don't have raid mode. + g_hUpgradeEntity = this; + + AddSpawnFlags(SF_TRIGGER_ALLOW_CLIENTS); + + InitTrigger(); + + SetTouch( &CUpgrades::UpgradeTouch ); + + ListenForGameEvent( "round_start" ); + ListenForGameEvent( "teamplay_round_start" ); + + m_bIsEnabled = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CUpgrades::InputEnable( inputdata_t &inputdata ) +{ + m_bIsEnabled = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CUpgrades::InputDisable( inputdata_t &inputdata ) +{ + m_bIsEnabled = false; + + int iCount = m_hTouchingEntities.Count(); + for ( int i = 0; i < iCount; i++ ) + { + CBaseEntity *pEntity = m_hTouchingEntities[i]; + if ( pEntity && pEntity->IsPlayer() ) + { + CTFPlayer *pTFPlayer = ToTFPlayer( pEntity ); + if ( pTFPlayer ) + { + pTFPlayer->m_Shared.SetInUpgradeZone( false ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CUpgrades::InputReset( inputdata_t &inputdata ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CUpgrades::FireGameEvent( IGameEvent *gameEvent ) +{ + if ( FStrEq( gameEvent->GetName(), "round_start" ) || FStrEq( gameEvent->GetName(), "teamplay_round_start" ) ) + { + // Enable/disable based on round state + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CUpgrades::UpgradeTouch( CBaseEntity *pOther ) +{ + if ( m_bIsEnabled ) + { + if ( PassesTriggerFilters(pOther) ) + { + if ( pOther->IsPlayer() ) + { + CTFPlayer *pTFPlayer = ToTFPlayer( pOther ); + pTFPlayer->m_Shared.SetInUpgradeZone( true ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CUpgrades::EndTouch( CBaseEntity *pOther ) +{ + if ( IsTouching( pOther ) ) + { + if ( pOther->IsPlayer() ) + { + CTFPlayer *pTFPlayer = ToTFPlayer( pOther ); + pTFPlayer->m_Shared.SetInUpgradeZone( false ); + } + } + + BaseClass::EndTouch( pOther ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CUpgrades::GrantOrRemoveAllUpgrades( CTFPlayer *pTFPlayer, bool bRemove /*= false*/, bool bRefund /*= true*/ ) +{ + // If we're being asked to remove and refund everything, it's a respec (the population manager actually handles refunding later) + bool bRespec = bRemove && bRefund; + + if ( pTFPlayer && ( ( sv_cheats && sv_cheats->GetBool() ) || bRemove ) ) + { + pTFPlayer->BeginPurchasableUpgrades(); + + // for each upgrade + for ( int iUpgrade = 0 ; iUpgrade < g_MannVsMachineUpgrades.m_Upgrades.Count() ; iUpgrade++ ) + { + CMannVsMachineUpgrades upgrade = g_MannVsMachineUpgrades.m_Upgrades[ iUpgrade ]; + CEconItemAttributeDefinition *pAttribDef = ItemSystem()->GetStaticDataForAttributeByName( upgrade.szAttrib ); + + // don't process bottle upgrades + if ( upgrade.nUIGroup == UIGROUP_POWERUPBOTTLE ) + continue; + + if ( pAttribDef ) + { + loadout_positions_t nLastLoadoutPos = bRespec ? LOADOUT_POSITION_MISC2 : LOADOUT_POSITION_HEAD; + // for each item + for ( int iItemSlot = LOADOUT_POSITION_PRIMARY ; iItemSlot < nLastLoadoutPos ; iItemSlot++ ) + { + // Don't respec bottle charges + if ( bRespec && iItemSlot == LOADOUT_POSITION_ACTION ) + continue; + + // can this item use this upgrade? + if ( bRespec || ( TFGameRules() && TFGameRules()->CanUpgradeWithAttrib( pTFPlayer, iItemSlot, pAttribDef->GetDefinitionIndex(), &upgrade ) ) ) + { + // If we're not removing, assume we're giving the player everything for free (cheat) + bool bFree = ( !bRemove || !bRefund ) ? true : false; + + // compute number of upgrade steps this upgrade has + bool bOverCap = false; + int iCurrentUpgradeStep = 0; + const int iNumMaxUpgradeStep = GetUpgradeStepData( pTFPlayer, iItemSlot, iUpgrade, iCurrentUpgradeStep, bOverCap ); + + // for each upgrade step + for ( int iStep = 0; iStep < iNumMaxUpgradeStep; ++iStep ) + { + PlayerPurchasingUpgrade( pTFPlayer, iItemSlot, iUpgrade, bRemove, bFree, bRespec ); + } + } + } + } + } + pTFPlayer->EndPurchasableUpgrades(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CUpgrades::PlayerPurchasingUpgrade( CTFPlayer *pTFPlayer, int iItemSlot, int iUpgrade, bool bDowngrade, bool bFree /*= false */, bool bRespec /*= false*/ ) +{ + if ( !pTFPlayer || + iUpgrade < 0 || + iUpgrade >= g_MannVsMachineUpgrades.m_Upgrades.Count() ) + return; + + // Verify that this upgrade can be accepted on this player + CMannVsMachineUpgrades upgrade = g_MannVsMachineUpgrades.m_Upgrades[ iUpgrade ]; + CEconItemAttributeDefinition *pAttribDef = ItemSystem()->GetStaticDataForAttributeByName( upgrade.szAttrib ); + if ( !bRespec && ( !TFGameRules() || !TFGameRules()->CanUpgradeWithAttrib( pTFPlayer, iItemSlot, pAttribDef->GetDefinitionIndex(), &upgrade ) ) ) + { + return; + } + + int nCost = 0; + + if ( bDowngrade ) + { + // See if we know the actual price paid, rather than asking what the current price is, which is exploitable + CUtlVector< CUpgradeInfo > *pUpgrades = pTFPlayer->GetRefundableUpgrades(); + if ( pUpgrades && pUpgrades->Count() ) + { + FOR_EACH_VEC( *pUpgrades, i ) + { + CUpgradeInfo *pInfo = &pUpgrades->Element( i ); + if ( pInfo && pInfo->m_upgrade == iUpgrade ) + { + nCost = pInfo->m_nCost; + break; + } + } + } + } + if ( !nCost ) + { + nCost = TFGameRules()->GetCostForUpgrade( &g_MannVsMachineUpgrades.m_Upgrades[iUpgrade], iItemSlot, pTFPlayer->GetPlayerClass()->GetClassIndex(), pTFPlayer ); + } + if ( bDowngrade ) + { + nCost *= -1; + } + + if ( !bFree ) + { + // Make sure the player can afford it + if ( pTFPlayer->GetCurrency() < nCost ) + return; + } + + CEconItemView *pItem = NULL; + + // Make sure the item slot is correct for attributes that need to attach to an item + if ( g_MannVsMachineUpgrades.m_Upgrades[ iUpgrade ].nUIGroup != UIGROUP_UPGRADE_ATTACHED_TO_PLAYER ) + { + if ( !( iItemSlot == LOADOUT_POSITION_ACTION || ( iItemSlot >= LOADOUT_POSITION_PRIMARY && iItemSlot <= LOADOUT_POSITION_PDA2 ) ) ) + return; + + pItem = CTFPlayerSharedUtils::GetEconItemViewByLoadoutSlot( pTFPlayer, iItemSlot ); + } + + if ( bDowngrade ) + { + if ( bRespec ) + { + // Approve everything and don't refund currency here. The population manager handles that later. + nCost = 0; + } + else + { + bool bCanSell = false; + + // Before we sell anything, make sure it's verified as valid + item_definition_index_t iItemIndex = pItem ? pItem->GetItemDefIndex() : INVALID_ITEM_DEF_INDEX; + + for ( int i = 0; i < pTFPlayer->GetRefundableUpgrades()->Count(); ++i ) + { + CUpgradeInfo pInfo = pTFPlayer->GetRefundableUpgrades()->Element( i ); + if ( ( pInfo.m_iPlayerClass == pTFPlayer->GetPlayerClass()->GetClassIndex() ) && ( pInfo.m_itemDefIndex == iItemIndex ) && ( pInfo.m_upgrade == iUpgrade ) ) + { + // Found a matching upgrade that we can sell + nCost = ( pInfo.m_nCost * -1 ); + bCanSell = true; + break; + } + } + + if ( !bCanSell ) + { + // No matched recent purchases! + // No sale! + return; + } + } + } + else + { + // If the upgrade has a tier, it's mutually exclusive with upgrades of the same tier for the same itemslot + int nTier = TFGameRules()->GetUpgradeTier( iUpgrade ); + if ( nTier && !TFGameRules()->IsUpgradeTierEnabled( pTFPlayer, iItemSlot, iUpgrade ) ) + return; + } + + const attrib_definition_index_t nUpgradedAttrDefIndex = ApplyUpgradeToItem( pTFPlayer, pItem, iUpgrade, nCost, bDowngrade, !bFree ); + if ( nUpgradedAttrDefIndex != INVALID_ATTRIB_DEF_INDEX ) + { + if ( !bFree ) + { + // Remove Currency + pTFPlayer->RemoveCurrency( nCost ); + } + + // remember our upgrades so we can restore them at a checkpoint + pTFPlayer->RememberUpgrade( pTFPlayer->GetPlayerClass()->GetClassIndex(), pItem, iUpgrade, nCost, bDowngrade ); + + // Only regenerate if between waves + pTFPlayer->Regenerate( TFObjectiveResource()->GetMannVsMachineIsBetweenWaves() ); + } + + // See if we need to notify items about an upgrade + NotifyItemOnUpgrade( pTFPlayer, nUpgradedAttrDefIndex, bDowngrade ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +static attrib_definition_index_t ApplyUpgrade_Bottle( int iUpgrade, CTFPlayer *pTFPlayer, CEconItemView *pEconItemView, int nCost, bool bDowngrade ) +{ + Assert( pTFPlayer ); + + if ( !pEconItemView ) + return INVALID_ATTRIB_DEF_INDEX; + + const CMannVsMachineUpgrades& upgrade = g_MannVsMachineUpgrades.m_Upgrades[iUpgrade]; + + const CEconItemAttributeDefinition *pAttrDef = GetItemSchema()->GetAttributeDefinitionByName( upgrade.szAttrib ); + if ( !pAttrDef ) + return INVALID_ATTRIB_DEF_INDEX; + + CTFWearable *pWearable = pTFPlayer->GetEquippedWearableForLoadoutSlot( LOADOUT_POSITION_ACTION ); + CTFPowerupBottle *pPowerupBottle = dynamic_cast< CTFPowerupBottle * >( pWearable ); + if ( !pPowerupBottle ) + return INVALID_ATTRIB_DEF_INDEX; + + Assert( pPowerupBottle->GetAttributeContainer()->GetItem() == pEconItemView ); + + const bool bMvM = TFGameRules() && TFGameRules()->IsMannVsMachineMode(); + Assert( !bMvM || g_pPopulationManager ); + + CAttributeList *pAttrList = pEconItemView->GetAttributeList(); + Assert( pAttrList ); + + // Attribute doesn't exist, remove any other attributes -- this code assumes that this is the + // only code path by which bottles will manipulate on-item attributes! + if ( !::FindAttribute( pAttrList, pAttrDef ) ) + { + // Can't downgrade an attribute that doesn't exist. + if ( bDowngrade ) + return INVALID_ATTRIB_DEF_INDEX; + + // Remove the old attributes + pPowerupBottle->RemoveEffect(); + pAttrList->DestroyAllAttributes(); + + // Forget charges for other attributes and refund player money + if ( bMvM ) + { + g_pPopulationManager->ForgetOtherBottleUpgrades( pTFPlayer, pEconItemView, iUpgrade ); + } + + // set this afterwards, since it may alter attributes + pPowerupBottle->SetNumCharges( 1 ); + + pAttrList->SetRuntimeAttributeValue( pAttrDef, upgrade.flIncrement ); + pAttrList->SetRuntimeAttributeRefundableCurrency( pAttrDef, nCost ); + } + // attribute exists, just increase number of charges + else + { + const int nChange = bDowngrade ? -1 : 1; + const int nNewCharges = pPowerupBottle->GetNumCharges() + nChange; + + // is the bottle full? + if ( nNewCharges < 0 || nNewCharges > pPowerupBottle->GetMaxNumCharges() ) + return INVALID_ATTRIB_DEF_INDEX; + + pPowerupBottle->SetNumCharges( nNewCharges ); + +#ifdef DBGFLAG_ASSERT + float flExistingValue; + Assert( ::FindAttribute_UnsafeBitwiseCast<attrib_value_t>( pAttrList, pAttrDef, &flExistingValue ) ); + Assert( AlmostEqual( flExistingValue, upgrade.flIncrement ) ); +#endif // DBGFLAG_ASSERT + pAttrList->AdjustRuntimeAttributeRefundableCurrency( pAttrDef, nCost * nChange ); + + if ( nNewCharges == 0 ) + { + Assert( bDowngrade ); + + // Downgraded to 0... remove attributes + pPowerupBottle->RemoveEffect(); + pAttrList->DestroyAllAttributes(); + + // Forget charges for other attributes and refund player money + if ( bMvM ) + { + g_pPopulationManager->ForgetOtherBottleUpgrades( pTFPlayer, pEconItemView, iUpgrade ); + } + } + } + + return pAttrDef->GetDefinitionIndex(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +static attrib_definition_index_t ApplyUpgrade_Default( const CMannVsMachineUpgrades& upgrade, CTFPlayer *pTFPlayer, CEconItemView *pEconItemView, int nCost, bool bDowngrade ) +{ + Assert( pTFPlayer ); + Assert( upgrade.nUIGroup == UIGROUP_UPGRADE_ATTACHED_TO_PLAYER || upgrade.nUIGroup == UIGROUP_UPGRADE_ATTACHED_TO_ITEM ); + // Assert( pEconItemView || upgrade.nUIGroup != UIGROUP_UPGRADE_ATTACHED_TO_ITEM ); // ugh, if loadouts change behind the scenes or if we have bugs elsewhere, we might + // feed an empty slot in here -- check for this case below if ATTACHED_TO_ITEM + + const CEconItemAttributeDefinition *pAttrDef = GetItemSchema()->GetAttributeDefinitionByName( upgrade.szAttrib ); + if ( !pAttrDef ) + return INVALID_ATTRIB_DEF_INDEX; + + Assert( !pAttrDef->BIsSetBonusAttribute() ); + + + CAttributeList *pAttrList = upgrade.nUIGroup == UIGROUP_UPGRADE_ATTACHED_TO_PLAYER + ? pTFPlayer->GetAttributeList() + : pEconItemView->GetAttributeList(); + Assert( pAttrList ); + + // ... + float fDefaultValue = pAttrDef->GetDescriptionFormat() == ATTDESCFORM_VALUE_IS_PERCENTAGE || pAttrDef->GetDescriptionFormat() == ATTDESCFORM_VALUE_IS_INVERTED_PERCENTAGE + ? 1.0f + : 0.0f; + + // ... + if ( upgrade.nUIGroup == UIGROUP_UPGRADE_ATTACHED_TO_ITEM ) + { + // If we're trying to attach to an item and we don't have an item to attach to, give up. + if ( !pEconItemView ) + return INVALID_ATTRIB_DEF_INDEX; + + ::FindAttribute_UnsafeBitwiseCast<attrib_value_t>( pEconItemView->GetItemDefinition(), pAttrDef, &fDefaultValue ); + } + + // if the attribute exists, add the increment (but not if it's a set bonus attribute, they're recreated on each respawn) + float flIncrement = upgrade.flIncrement; + + float flCurrentValue; + if ( ::FindAttribute_UnsafeBitwiseCast<attrib_value_t>( pAttrList, pAttrDef, &flCurrentValue ) ) + { + const float flCap = upgrade.flCap; + + if ( !bDowngrade && BIsAttributeValueWithDeltaOverCap( flCurrentValue, flIncrement, flCap ) ) + return INVALID_ATTRIB_DEF_INDEX; + + // Add the increment + float flNewValue = 0.0f; + + if ( bDowngrade ) + { + float flInitialValue = fDefaultValue; + flIncrement = fabsf( flIncrement ); + + if ( AlmostEqual( flCurrentValue, flCap ) && !AlmostEqual( flInitialValue, fDefaultValue ) ) + { + // If we're at the cap and the initial value isn't normal, we might need to do a smaller increment + // This is because incrementing from the initial value in steps would have gone past the hard cap + float flStart = flInitialValue; + + if ( flIncrement > 0 ) + { + while ( flStart < flCap && !AlmostEqual( flStart, flCap ) ) + { + flStart += flIncrement; + } + } + else + { + while ( flStart > flCap && !AlmostEqual( flStart, flCap ) ) + { + flStart += flIncrement; + } + } + + const float flDiff = fabsf( flCap - flStart ); + if ( !AlmostEqual( flIncrement, flDiff ) ) + { + flIncrement -= flDiff; + } + } + + flNewValue = Approach( flInitialValue, flCurrentValue, flIncrement ); + + // We downgraded back to the point of not needing the attribute + if ( AlmostEqual( flNewValue, flInitialValue ) ) + { + Assert( bDowngrade ); + + pAttrList->RemoveAttribute( pAttrDef ); + return pAttrDef->GetDefinitionIndex(); + } + } + else + { + flNewValue = Approach( flCap, flCurrentValue, fabsf( flIncrement ) ); + } + + pAttrList->SetRuntimeAttributeValue( pAttrDef, flNewValue ); + pAttrList->AdjustRuntimeAttributeRefundableCurrency( pAttrDef, nCost ); + return pAttrDef->GetDefinitionIndex(); + } + + if ( bDowngrade ) + { + // Can't downgrade an attribute we didn't find + return INVALID_ATTRIB_DEF_INDEX; + } + + // Didn't exist, so we need to add the attribute. + + // Convert the increment into an actual multiplier amount + pAttrList->SetRuntimeAttributeValue( pAttrDef, fDefaultValue + flIncrement ); + pAttrList->SetRuntimeAttributeRefundableCurrency( pAttrDef, nCost ); + + // For NotifyItemOnUpgrade() - can't do it here because we Regenerate() downstream + return pAttrDef->GetDefinitionIndex(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +attrib_definition_index_t CUpgrades::ApplyUpgradeToItem( CTFPlayer *pTFPlayer, CEconItemView *pView, int iUpgrade, int nCost, bool bDowngrade, bool bIsFresh ) +{ + Assert( pTFPlayer ); + Assert( g_MannVsMachineUpgrades.m_Upgrades.IsValidIndex( iUpgrade ) ); + + if ( !pTFPlayer || !pTFPlayer->CanPurchaseUpgrades() ) + return INVALID_ATTRIB_DEF_INDEX; + + const CMannVsMachineUpgrades& upgrade = g_MannVsMachineUpgrades.m_Upgrades[iUpgrade]; + + const CEconItemAttributeDefinition *pAttrDef = GetItemSchema()->GetAttributeDefinitionByName( upgrade.szAttrib ); + if ( !pAttrDef ) + return INVALID_ATTRIB_DEF_INDEX; + + bool bIsBottle = upgrade.nUIGroup == UIGROUP_POWERUPBOTTLE; + + ReportUpgrade( + pTFPlayer, + pView ? pView->GetItemDefIndex() : 0, + pAttrDef->GetDefinitionIndex(), + upgrade.nQuality, + nCost, + bDowngrade, + bIsFresh, + bIsBottle + ); + + // ...powerup bottle? + if ( bIsBottle ) + return ApplyUpgrade_Bottle( iUpgrade, pTFPlayer, pView, nCost, bDowngrade ); + + // ...player upgrade? + if ( upgrade.nUIGroup == UIGROUP_UPGRADE_ATTACHED_TO_PLAYER ) + return ApplyUpgrade_Default( upgrade, pTFPlayer, pView, nCost, bDowngrade ); + + Assert( upgrade.nUIGroup == UIGROUP_UPGRADE_ATTACHED_TO_ITEM ); + return ApplyUpgrade_Default( upgrade, pTFPlayer, pView, nCost, bDowngrade ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Given an upgrade index, return it's associated attribute description +//----------------------------------------------------------------------------- +const char *CUpgrades::GetUpgradeAttributeName( int iUpgrade ) const +{ + if ( iUpgrade < 0 || iUpgrade >= g_MannVsMachineUpgrades.m_Upgrades.Count() ) + return NULL; + + return g_MannVsMachineUpgrades.m_Upgrades[ iUpgrade ].szAttrib; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CUpgrades::NotifyItemOnUpgrade( CTFPlayer *pTFPlayer, attrib_definition_index_t nAttrDefIndex, bool bDowngrade /*= false*/ ) +{ + if ( !pTFPlayer ) + return; + + switch( nAttrDefIndex ) + { + case 286: // "engy building health bonus" + { + // Tell the wrench we've upgraded our object health (which handles the rest) + CTFWrench *pWrench = dynamic_cast<CTFWrench *>( pTFPlayer->Weapon_OwnsThisID( TF_WEAPON_WRENCH ) ); + if ( pWrench ) + { + pWrench->ApplyBuildingHealthUpgrade(); + } + } + break; + case 320: // "robot sapper" + { + // Sets the UI active + CTFWeaponBuilder *pBuilder = dynamic_cast<CTFWeaponBuilder *>( pTFPlayer->Weapon_OwnsThisID( TF_WEAPON_BUILDER ) ); + if ( pBuilder ) + { + pBuilder->m_bRoboSapper.Set( true ); + } + } + break; + case 351: + if ( bDowngrade ) + { + // if we're refunding the engy_disposable_sentries attribute we need to destroy the disposable sentry if we have one + if ( pTFPlayer->IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + for ( int i = pTFPlayer->GetObjectCount() - 1; i >= 0; i-- ) + { + CBaseObject *pObj = pTFPlayer->GetObject( i ); + if ( pObj ) + { + if ( ( pObj->GetType() == OBJ_SENTRYGUN ) && ( pObj->IsDisposableBuilding() ) ) + { + pObj->DetonateObject(); + } + } + } + } + } + break; + case 375: + { + // Reduce buff duration when a Heavy gets the Rage upgrade in MvM + if ( TFGameRules() && TFGameRules()->GameModeUsesUpgrades() ) + { + if ( pTFPlayer->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) ) + { + const int mod_buff_duration = 319; + const float flMod = 0.5f; + CEconItemAttributeDefinition *pDef = GetItemSchema()->GetAttributeDefinition( mod_buff_duration ); + if ( !pDef ) + return; + + pTFPlayer->GetAttributeList()->SetRuntimeAttributeValue( pDef, flMod ); + } + } + } + break; + case 499: + { + // Increase buff duration for Medics with projectile shields + if ( TFGameRules() && TFGameRules()->GameModeUsesUpgrades() ) + { + if ( pTFPlayer->IsPlayerClass( TF_CLASS_MEDIC ) ) + { + const int mod_buff_duration = 319; + const float flMod = 1.2f; + CEconItemAttributeDefinition *pDef = GetItemSchema()->GetAttributeDefinition( mod_buff_duration ); + if ( !pDef ) + return; + + pTFPlayer->GetAttributeList()->SetRuntimeAttributeValue( pDef, flMod ); + } + } + } + break; +#ifdef STAGING_ONLY + case 555: // medigun specialist + { + static UpgradeAttribBlock_t upgradeBlock[] = + { + { "healing mastery", 4.f, LOADOUT_POSITION_SECONDARY }, + { "overheal expert", 4.f, LOADOUT_POSITION_SECONDARY }, + { "uber duration bonus", 4.f, LOADOUT_POSITION_SECONDARY }, + { "ubercharge rate bonus", 2.f, LOADOUT_POSITION_SECONDARY }, + }; + + ApplyUpgradeAttributeBlock( upgradeBlock, ARRAYSIZE( upgradeBlock ), pTFPlayer, bDowngrade ); + } + break; + case 605: // master sniper + { + static UpgradeAttribBlock_t upgradeBlock[] = + { + { "projectile penetration", 1.f, LOADOUT_POSITION_PRIMARY }, + { "SRifle Charge rate increased", 1.5f, LOADOUT_POSITION_PRIMARY }, + { "faster reload rate", 0.6f, LOADOUT_POSITION_PRIMARY }, + }; + + ApplyUpgradeAttributeBlock( upgradeBlock, ARRAYSIZE( upgradeBlock ), pTFPlayer, bDowngrade ); + } + break; + case 611: // airborne infantry + { + static UpgradeAttribBlock_t upgradeBlock[] = + { + { "increased air control", 10.f, LOADOUT_POSITION_PRIMARY }, + { "rocket launch impulse", 1, LOADOUT_POSITION_PRIMARY }, + { "cancel falling damage", 1, LOADOUT_POSITION_PRIMARY }, + }; + + ApplyUpgradeAttributeBlock( upgradeBlock, ARRAYSIZE( upgradeBlock ), pTFPlayer, bDowngrade ); + } + break; + case 624: // construction expert + { + static UpgradeAttribBlock_t upgradeBlock[] = + { + { "build rate bonus", 0.7f, LOADOUT_POSITION_MELEE }, + { "maxammo metal increased", 1.5f, LOADOUT_POSITION_INVALID }, + { "engy building health penalty", 0.7f, LOADOUT_POSITION_MELEE }, + }; + + ApplyUpgradeAttributeBlock( upgradeBlock, ARRAYSIZE( upgradeBlock ), pTFPlayer, bDowngrade ); + } + break; + case 626: // support engineer + { + static UpgradeAttribBlock_t upgradeBlock[] = + { + { "teleporter recharge rate bonus", 0.5f, LOADOUT_POSITION_INVALID }, + { "teleporter speed boost", 1, LOADOUT_POSITION_INVALID }, + { "bidirectional teleport", 1, LOADOUT_POSITION_INVALID }, + { "dispenser rate bonus", 1.25f, LOADOUT_POSITION_INVALID }, + { "engy dispenser radius increased", 3.f, LOADOUT_POSITION_INVALID }, + }; + + ApplyUpgradeAttributeBlock( upgradeBlock, ARRAYSIZE( upgradeBlock ), pTFPlayer, bDowngrade ); + } + break; +#endif // STAGING_ONLY + + default: + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Reports the Upgrade to systems that care (clients / ogs) +//----------------------------------------------------------------------------- +void CUpgrades::ReportUpgrade( CTFPlayer *pTFPlayer, int nItemDef, int nAttributeDef, int nQuality, int nCost, bool bDowngrade, bool bIsFresh, bool bIsBottle /*= false*/ ) +{ + if ( !pTFPlayer ) + { + return; + } + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + // Calculate how much money is being used on active class / items + int nSpending = 0; + int iClass = pTFPlayer->GetPlayerClass()->GetClassIndex(); + CUtlVector< CUpgradeInfo > *upgrades = g_pPopulationManager->GetPlayerUpgradeHistory( pTFPlayer ); + if ( upgrades ) + { + for( int u = 0; u < upgrades->Count(); ++u ) + { + // Class Match, Check to see if we have this item equipped + if ( iClass == upgrades->Element(u).m_iPlayerClass) + { + // Player upgrade + if ( upgrades->Element( u ).m_itemDefIndex == INVALID_ITEM_DEF_INDEX ) + { + nSpending += upgrades->Element(u).m_nCost; + continue; + } + + // Item upgrade, look at equipment only not miscs or bottle + for ( int itemIndex = 0; itemIndex <= LOADOUT_POSITION_PDA2; itemIndex++ ) + { + CEconItemView *pItem = pTFPlayer->GetLoadoutItem( iClass, itemIndex, true ); + if ( upgrades->Element(u).m_itemDefIndex == pItem->GetItemDefIndex() ) + { + nSpending += upgrades->Element(u).m_nCost; + break; + } + } + } + } + } + + CMannVsMachineStats *pStats = MannVsMachineStats_GetInstance(); + if ( pStats ) + { + pStats->NotifyPlayerActiveUpgradeCosts( pTFPlayer, nSpending ); + } + + // Only report fresh upgrades + if ( !bIsFresh ) + return; + + MannVsMachineStats_PlayerEvent_Upgraded( pTFPlayer, nItemDef, nAttributeDef, nQuality, nCost, bIsBottle ); + } + + if ( bDowngrade ) + { + return; + } + + pTFPlayer->EmitSound( "MVM.PlayerUpgraded" ); + + IGameEvent *event = gameeventmanager->CreateEvent( "player_upgraded" ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CUpgrades::RestoreItemAttributeToBaseValue( CEconItemAttributeDefinition *pAttrib, CEconItemView *pItem ) +{ + Assert( pAttrib ); + Assert( pItem ); + + CAttributeList *pAttrList = pItem->GetAttributeList(); + Assert( pAttrList ); + + float flCurrentValue; + if ( ::FindAttribute_UnsafeBitwiseCast<attrib_value_t>( pAttrList, pAttrib, &flCurrentValue ) ) + { + float fDefaultValue = pAttrib->GetDescriptionFormat() == ATTDESCFORM_VALUE_IS_PERCENTAGE || pAttrib->GetDescriptionFormat() == ATTDESCFORM_VALUE_IS_INVERTED_PERCENTAGE ? 1.f : 0.f; + ::FindAttribute_UnsafeBitwiseCast<attrib_value_t>( pAttrList, pAttrib, &fDefaultValue ); + + // We don't need the attribute + if ( AlmostEqual( flCurrentValue, fDefaultValue ) ) + { + pAttrList->RemoveAttribute( pAttrib ); + return; + } + + pAttrList->SetRuntimeAttributeValue( pAttrib, fDefaultValue ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CUpgrades::RestorePlayerAttributeToBaseValue( CEconItemAttributeDefinition *pAttrib, CTFPlayer *pTFPlayer ) +{ + Assert( pAttrib ); + Assert( pTFPlayer ); + + CAttributeList *pAttrList = pTFPlayer->GetAttributeList(); + Assert( pAttrList ); + + float flCurrentValue; + if ( ::FindAttribute_UnsafeBitwiseCast<attrib_value_t>( pAttrList, pAttrib, &flCurrentValue ) ) + { + float fDefaultValue = pAttrib->GetDescriptionFormat() == ATTDESCFORM_VALUE_IS_PERCENTAGE || pAttrib->GetDescriptionFormat() == ATTDESCFORM_VALUE_IS_INVERTED_PERCENTAGE ? 1.f : 0.f; + ::FindAttribute_UnsafeBitwiseCast<attrib_value_t>( pAttrList, pAttrib, &fDefaultValue ); + + // We don't need the attribute + if ( AlmostEqual( flCurrentValue, fDefaultValue ) ) + { + pAttrList->RemoveAttribute( pAttrib ); + return; + } + + pAttrList->SetRuntimeAttributeValue( pAttrib, fDefaultValue ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CUpgrades::ApplyUpgradeAttributeBlock( UpgradeAttribBlock_t *upgradeBlock, int upgradeCount, CTFPlayer *pPlayer, bool bDowngrade ) +{ + if ( !pPlayer ) + return; + + for ( int i = 0; i < upgradeCount; i++ ) + { + if ( !upgradeBlock[i].szName || !upgradeBlock[i].szName[0] ) + continue; + + CAttributeList *pAttribList = NULL; + CEconItemView *pItem = NULL; + + // Player or item? + if ( upgradeBlock[i].iSlot == LOADOUT_POSITION_INVALID ) + { + pAttribList = pPlayer->GetAttributeList(); + } + else + { + pItem = CTFPlayerSharedUtils::GetEconItemViewByLoadoutSlot( pPlayer, upgradeBlock[i].iSlot ); + if ( !pItem ) + continue; + + pAttribList = pItem->GetAttributeList(); + } + + if ( !pAttribList ) + continue; + + CEconItemAttributeDefinition *pDef = ItemSystem()->GetItemSchema()->GetAttributeDefinitionByName( upgradeBlock[i].szName ); + if ( !pDef ) + continue; + + if ( bDowngrade ) + { + if ( pItem ) + { + RestoreItemAttributeToBaseValue( pDef, pItem ); + continue; + } + else + { + RestorePlayerAttributeToBaseValue( pDef, pPlayer ); + } + } + else + { + pAttribList->SetRuntimeAttributeValue( pDef, upgradeBlock[i].flValue ); + } + } + + pPlayer->NetworkStateChanged(); +} + + + + + diff --git a/game/server/tf/player_vs_environment/tf_upgrades.h b/game/server/tf/player_vs_environment/tf_upgrades.h new file mode 100644 index 0000000..3def047 --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_upgrades.h @@ -0,0 +1,67 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Load item upgrade data from KeyValues +// +// $NoKeywords: $ +//============================================================================= + +#ifndef TF_UPGRADES_H +#define TF_UPGRADES_H + +#include "bot/tf_bot.h" +#include "networkvar.h" +#include "triggers.h" +#include "tf_population_manager.h" + +struct UpgradeAttribBlock_t +{ + char szName[MAX_ATTRIBUTE_DESCRIPTION_LENGTH]; + float flValue; + loadout_positions_t iSlot; +}; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CUpgrades : public CBaseTrigger, public CGameEventListener +{ +public: + DECLARE_CLASS( CUpgrades, CBaseTrigger ); + DECLARE_DATADESC(); + + virtual void Spawn( void ); + + virtual void FireGameEvent( IGameEvent *gameEvent ); + + void UpgradeTouch( CBaseEntity *pOther ); + virtual void EndTouch( CBaseEntity *pEntity ); + + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + void InputReset( inputdata_t &inputdata ); + + void GrantOrRemoveAllUpgrades( CTFPlayer *pTFPlayer, bool bRemove = false, bool bRefund = true ); + void PlayerPurchasingUpgrade( CTFPlayer *pTFPlayer, int iItemSlot, int iUpgrade, bool bDowngrade, bool bFree = false, bool bRespec = false ); + + attrib_definition_index_t ApplyUpgradeToItem( CTFPlayer *pTFPlayer, CEconItemView *pView, int iUpgrade, int nCost, bool bDowngrade = false, bool bIsFresh = false ); // needed for checkpoint restore + + const char * GetUpgradeAttributeName( int iUpgrade ) const; + +private: + void NotifyItemOnUpgrade( CTFPlayer *pTFPlayer, attrib_definition_index_t nAttrDefIndex, bool bDowngrade = false ); + void ReportUpgrade ( CTFPlayer *pTFPlayer, int nItemDef, int nAttributeDef, int nQuality, int nCost, bool bDowngrade, bool bIsFresh, bool bIsBottle = false ); + void RestoreItemAttributeToBaseValue( CEconItemAttributeDefinition *pAttrib, CEconItemView *pItem ); + void RestorePlayerAttributeToBaseValue( CEconItemAttributeDefinition *pAttrib, CTFPlayer *pTFPlayer ); + + void ApplyUpgradeAttributeBlock( UpgradeAttribBlock_t *upgradeBlock, int upgradeCount, CTFPlayer *pPlayer, bool bDowngrade ); + + int m_nStartDisabled; + + COutputEvent m_onPeriodicSpawn; + + bool m_bIsEnabled; +}; + +extern CHandle<CUpgrades> g_hUpgradeEntity; + +#endif // TF_UPGRADES_H |