summaryrefslogtreecommitdiff
path: root/game/server/tf/player_vs_environment
diff options
context:
space:
mode:
Diffstat (limited to 'game/server/tf/player_vs_environment')
-rw-r--r--game/server/tf/player_vs_environment/archer_proxy.cpp188
-rw-r--r--game/server/tf/player_vs_environment/archer_proxy.h36
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_behavior.cpp109
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_behavior.h30
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_chase_victim.cpp170
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_chase_victim.h41
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_get_off_me.cpp93
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_get_off_me.h27
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_guard_spot.cpp129
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_guard_spot.h28
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_launch_grenades.cpp204
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_launch_grenades.h36
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_launch_rockets.cpp119
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_launch_rockets.h38
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_lost_victim.cpp64
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_lost_victim.h28
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_nuke_attack.cpp208
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_nuke_attack.h31
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_stunned.cpp171
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_stunned.h39
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_tactical_monitor.cpp81
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_tactical_monitor.h32
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_wait_for_players.cpp79
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/behavior/boss_alpha_wait_for_players.h25
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/boss_alpha.cpp1385
-rw-r--r--game/server/tf/player_vs_environment/boss_alpha/boss_alpha.h508
-rw-r--r--game/server/tf/player_vs_environment/monster_resource.cpp129
-rw-r--r--game/server/tf/player_vs_environment/monster_resource.h53
-rw-r--r--game/server/tf/player_vs_environment/tf_base_boss.cpp639
-rw-r--r--game/server/tf/player_vs_environment/tf_base_boss.h121
-rw-r--r--game/server/tf/player_vs_environment/tf_boss_battle_logic.cpp125
-rw-r--r--game/server/tf/player_vs_environment/tf_boss_battle_logic.h45
-rw-r--r--game/server/tf/player_vs_environment/tf_mann_vs_machine_logic.cpp186
-rw-r--r--game/server/tf/player_vs_environment/tf_mann_vs_machine_logic.h43
-rw-r--r--game/server/tf/player_vs_environment/tf_point_weapon_mimic.cpp312
-rw-r--r--game/server/tf/player_vs_environment/tf_population_manager.cpp2734
-rw-r--r--game/server/tf/player_vs_environment/tf_population_manager.h286
-rw-r--r--game/server/tf/player_vs_environment/tf_populator_interface.cpp103
-rw-r--r--game/server/tf/player_vs_environment/tf_populator_spawners.cpp1890
-rw-r--r--game/server/tf/player_vs_environment/tf_populator_spawners.h283
-rw-r--r--game/server/tf/player_vs_environment/tf_populators.cpp2324
-rw-r--r--game/server/tf/player_vs_environment/tf_populators.h467
-rw-r--r--game/server/tf/player_vs_environment/tf_tank_boss.cpp1077
-rw-r--r--game/server/tf/player_vs_environment/tf_tank_boss.h131
-rw-r--r--game/server/tf/player_vs_environment/tf_tank_boss_body.cpp58
-rw-r--r--game/server/tf/player_vs_environment/tf_tank_boss_body.h30
-rw-r--r--game/server/tf/player_vs_environment/tf_upgrades.cpp941
-rw-r--r--game/server/tf/player_vs_environment/tf_upgrades.h67
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