//========= 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 \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; iGetCurrencyAmountPerDeath(); } 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 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 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"); } }