summaryrefslogtreecommitdiff
path: root/game/server/tf/tf_obj_dispenser.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'game/server/tf/tf_obj_dispenser.cpp')
-rw-r--r--game/server/tf/tf_obj_dispenser.cpp1261
1 files changed, 1261 insertions, 0 deletions
diff --git a/game/server/tf/tf_obj_dispenser.cpp b/game/server/tf/tf_obj_dispenser.cpp
new file mode 100644
index 0000000..ba22acd
--- /dev/null
+++ b/game/server/tf/tf_obj_dispenser.cpp
@@ -0,0 +1,1261 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+// Purpose: Engineer's Dispenser
+//
+// $NoKeywords: $
+//=============================================================================//
+#include "cbase.h"
+
+#include "tf_obj_dispenser.h"
+#include "engine/IEngineSound.h"
+#include "tf_player.h"
+#include "tf_team.h"
+#include "tf_gamerules.h"
+#include "vguiscreen.h"
+#include "world.h"
+#include "explode.h"
+#include "tf_gamestats.h"
+#include "tf_halloween_souls_pickup.h"
+#include "tf_fx.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+#define DISPENSER_MINS Vector( -20, -20, 0 )
+#define DISPENSER_MAXS Vector( 20, 20, 55 ) // tweak me
+
+#define MINI_DISPENSER_MINS Vector( -20, -20, 0 )
+#define MINI_DISPENSER_MAXS Vector( 20, 20, 20 )
+
+#define DISPENSER_TRIGGER_MINS Vector( -70, -70, 0 )
+#define DISPENSER_TRIGGER_MAXS Vector( 70, 70, 50 ) // tweak me
+
+#define REFILL_CONTEXT "RefillContext"
+#define DISPENSE_CONTEXT "DispenseContext"
+
+ConVar tf_cart_spell_drop_rate( "tf_cart_spell_drop_rate", "4" );
+ConVar tf_cart_duck_drop_rate( "tf_cart_duck_drop_rate", "10", FCVAR_DEVELOPMENTONLY );
+ConVar tf_cart_soul_drop_rate( "tf_cart_soul_drop_rate", "10", FCVAR_DEVELOPMENTONLY );
+
+//-----------------------------------------------------------------------------
+// Purpose: SendProxy that converts the Healing list UtlVector to entindices
+//-----------------------------------------------------------------------------
+void SendProxy_HealingList( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID )
+{
+ CObjectDispenser *pDispenser = (CObjectDispenser*)pStruct;
+
+ // If this assertion fails, then SendProxyArrayLength_HealingArray must have failed.
+ Assert( iElement < pDispenser->m_hHealingTargets.Size() );
+
+ CBaseEntity *pEnt = pDispenser->m_hHealingTargets[iElement].Get();
+ EHANDLE hOther = pEnt;
+
+ SendProxy_EHandleToInt( pProp, pStruct, &hOther, pOut, iElement, objectID );
+}
+
+int SendProxyArrayLength_HealingArray( const void *pStruct, int objectID )
+{
+ CObjectDispenser *pDispenser = (CObjectDispenser*)pStruct;
+ return pDispenser->m_hHealingTargets.Count();
+}
+
+IMPLEMENT_SERVERCLASS_ST( CObjectDispenser, DT_ObjectDispenser )
+ SendPropInt( SENDINFO( m_iState ), 2 ),
+ SendPropInt( SENDINFO( m_iAmmoMetal ), -1, SPROP_VARINT ),
+ SendPropInt( SENDINFO( m_iMiniBombCounter ), -1, SPROP_VARINT ),
+
+ SendPropArray2(
+ SendProxyArrayLength_HealingArray,
+ SendPropInt("healing_array_element", 0, SIZEOF_IGNORE, NUM_NETWORKED_EHANDLE_BITS, SPROP_UNSIGNED, SendProxy_HealingList),
+ MAX_PLAYERS,
+ 0,
+ "healing_array"
+ )
+
+END_SEND_TABLE()
+
+BEGIN_DATADESC( CObjectDispenser )
+ DEFINE_THINKFUNC( RefillThink ),
+ DEFINE_THINKFUNC( DispenseThink ),
+
+ // key
+ DEFINE_KEYFIELD( m_iszCustomTouchTrigger, FIELD_STRING, "touch_trigger" ),
+
+END_DATADESC()
+
+LINK_ENTITY_TO_CLASS(obj_dispenser, CObjectDispenser);
+PRECACHE_REGISTER(obj_dispenser);
+
+// How much ammo is given our per use
+#define DISPENSER_DROP_METAL 40
+
+float g_flDispenserHealRates[4] =
+{
+ 0,
+ 10.0,
+ 15.0,
+ 20.0
+};
+
+float g_flDispenserAmmoRates[4] =
+{
+ 0,
+ 0.2,
+ 0.3,
+ 0.4
+};
+
+LINK_ENTITY_TO_CLASS( dispenser_touch_trigger, CDispenserTouchTrigger );
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+CObjectDispenser::CObjectDispenser()
+{
+ int iHealth = GetMaxHealthForCurrentLevel();
+ m_hTouchTrigger = NULL;
+ SetMaxHealth( iHealth );
+ SetHealth( iHealth );
+ UseClientSideAnimation();
+
+ m_hTouchingEntities.Purge();
+ m_bUseGenerateMetalSound = true;
+ m_bThrown = false;
+
+ m_bPlayAmmoPickupSound = true;
+
+ SetType( OBJ_DISPENSER );
+}
+
+CObjectDispenser::~CObjectDispenser()
+{
+ if ( m_hTouchTrigger.Get() )
+ {
+ UTIL_Remove( m_hTouchTrigger );
+ }
+
+ ResetHealingTargets();
+
+ StopSound( "Building_Dispenser.Idle" );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectDispenser::DetonateObject( void )
+{
+ // We already dying?
+ if ( m_bDying )
+ return;
+
+#ifdef STAGING_ONLY
+ // If we're built, explode for damage
+ if ( IsMiniBuilding() && !IsCarried() && !IsBuilding() && !IsPlacing() )
+ {
+ Vector vecOrigin = GetAbsOrigin();
+ CTraceFilterIgnorePlayers traceFilter( NULL, COLLISION_GROUP_PROJECTILE );
+
+ // base 50 damage, scale by metal amount
+ float flDamage = RemapValClamped( m_iAmmoMetal, 0, MINI_DISPENSER_MAX_METAL, 50.0f, 300.0f );
+ CTakeDamageInfo info( this, GetOwner(), flDamage, DMG_BLAST );
+
+ // Scale blast radius
+ float flRadius = RemapValClamped( m_iAmmoMetal, 0, MINI_DISPENSER_MAX_METAL, 150.0f, 200.0f );
+ CTFRadiusDamageInfo radiusinfo( &info, vecOrigin, flRadius, NULL, flRadius );
+ TFGameRules()->RadiusDamage( radiusinfo );
+
+ CPVSFilter filter( vecOrigin );
+ TE_TFExplosion( filter, 0.0f, vecOrigin, Vector(0,0,0), TF_WEAPON_GRENADE_PIPEBOMB, kInvalidEHandleExplosion, -1, SPECIAL1, INVALID_STRING_INDEX );
+ }
+#endif
+
+ TFGameRules()->OnDispenserDestroyed( this );
+
+ BaseClass::DetonateObject();
+}
+
+//-----------------------------------------------------------------------------
+void CObjectDispenser::DestroyObject( void )
+{
+ if ( TFGameRules() )
+ {
+ TFGameRules()->OnDispenserDestroyed( this );
+ }
+
+ BaseClass::DestroyObject();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectDispenser::Spawn()
+{
+ SetModel( GetPlacementModel() );
+
+ m_iState.Set( DISPENSER_STATE_IDLE );
+
+ SetTouch( &CObjectDispenser::Touch );
+
+ BaseClass::Spawn();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectDispenser::FirstSpawn()
+{
+ SetSolid( SOLID_BBOX );
+
+ bool bShouldBeMini = ShouldBeMiniBuilding( GetOwner() );
+
+ UTIL_SetSize(this,
+ bShouldBeMini ? MINI_DISPENSER_MINS : DISPENSER_MINS,
+ bShouldBeMini ? MINI_DISPENSER_MAXS : DISPENSER_MAXS );
+
+ m_takedamage = DAMAGE_YES;
+ m_iAmmoMetal = 0;
+
+ int iHealth = GetMaxHealthForCurrentLevel();
+
+ SetMaxHealth( iHealth );
+ SetHealth( iHealth );
+
+ BaseClass::FirstSpawn();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+const char* CObjectDispenser::GetBuildingModel( int iLevel )
+{
+#ifdef STAGING_ONLY
+ if ( ShouldBeMiniBuilding( GetOwner() ) )
+ {
+ return MINI_DISPENSER_MODEL_BUILDING;
+ }
+ else
+#endif // STAGING_ONLY
+ {
+ switch ( iLevel )
+ {
+ case 1:
+ return DISPENSER_MODEL_BUILDING;
+ break;
+ case 2:
+ return DISPENSER_MODEL_BUILDING_LVL2;
+ break;
+ case 3:
+ return DISPENSER_MODEL_BUILDING_LVL3;
+ break;
+ default:
+ return DISPENSER_MODEL_BUILDING;
+ break;
+ }
+ }
+
+ Assert( 0 );
+ return DISPENSER_MODEL;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+const char* CObjectDispenser::GetFinishedModel( int iLevel )
+{
+#ifdef STAGING_ONLY
+ if ( IsMiniBuilding() )
+ {
+ return MINI_DISPENSER_MODEL;
+ }
+ else
+#endif // STAGING_ONLY
+ {
+ switch ( iLevel )
+ {
+ case 1:
+ return DISPENSER_MODEL;
+ break;
+ case 2:
+ return DISPENSER_MODEL_LVL2;
+ break;
+ case 3:
+ return DISPENSER_MODEL_LVL3;
+ break;
+ default:
+ return DISPENSER_MODEL;
+ break;
+ }
+ }
+
+ Assert( 0 );
+ return DISPENSER_MODEL;
+}
+
+const char* CObjectDispenser::GetPlacementModel()
+{
+ return /*IsMiniBuilding() ? MINI_DISPENSER_MODEL_PLACEMENT :*/ DISPENSER_MODEL_PLACEMENT;
+}
+
+void CObjectDispenser::StartPlacement( CTFPlayer *pPlayer )
+{
+ BaseClass::StartPlacement( pPlayer );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Start building the object
+//-----------------------------------------------------------------------------
+bool CObjectDispenser::StartBuilding( CBaseEntity *pBuilder )
+{
+ SetStartBuildingModel();
+
+ // Have to re-call this in case the player changed their weapon
+ // between StartPlacement and StartBuilding.
+ MakeMiniBuilding( GetBuilder() );
+
+ CreateBuildPoints();
+
+ TFGameRules()->OnDispenserBuilt( this );
+
+ bool bIsCarried = IsCarried();
+
+ bool bStartBuildingResult = BaseClass::StartBuilding( pBuilder );
+
+ if ( bIsCarried )
+ {
+ OnEndBeingCarried( pBuilder );
+ }
+
+ return bStartBuildingResult;
+}
+
+void CObjectDispenser::SetStartBuildingModel( void )
+{
+ SetModel( GetBuildingModel( 1 ) );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectDispenser::MakeMiniBuilding( CTFPlayer* pPlayer )
+{
+ if ( !ShouldBeMiniBuilding( pPlayer ) || IsMiniBuilding() )
+ return;
+
+ BaseClass::MakeMiniBuilding( pPlayer );
+
+ int iHealth = GetMaxHealthForCurrentLevel();
+
+ SetMaxHealth( iHealth );
+ SetHealth( iHealth );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Raises the Dispenser one level
+//-----------------------------------------------------------------------------
+void CObjectDispenser::StartUpgrading( void )
+{
+ // m_iUpgradeLevel is incremented in BaseClass::StartUpgrading(),
+ // but we need to set the model before calling it so SetActivity() will be successful
+ SetModel( GetBuildingModel( m_iUpgradeLevel+1 ) );
+
+ // clear our healing list, everyone will be
+ // added again at the new heal rate in DispenseThink()
+ ResetHealingTargets();
+
+ BaseClass::StartUpgrading();
+
+ m_iState.Set( DISPENSER_STATE_UPGRADING );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectDispenser::FinishUpgrading( void )
+{
+ m_iState.Set( DISPENSER_STATE_IDLE );
+
+ BaseClass::FinishUpgrading();
+
+ if ( m_iUpgradeLevel > 1 )
+ {
+ SetModel( GetFinishedModel( m_iUpgradeLevel ) );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectDispenser::SetModel( const char *pModel )
+{
+ BaseClass::SetModel( pModel );
+
+ // Reset this after model change
+#ifdef STAGING_ONLY
+ UTIL_SetSize(this,
+ IsMiniBuilding() ? MINI_DISPENSER_MINS : DISPENSER_MINS,
+ IsMiniBuilding() ? MINI_DISPENSER_MAXS : DISPENSER_MAXS );
+#else
+ UTIL_SetSize( this,
+ DISPENSER_MINS,
+ DISPENSER_MAXS );
+#endif // STAGING_ONLY
+ ResetSequenceInfo();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectDispenser::InitializeMapPlacedObject( void )
+{
+ // make sure we are using an appropriate model so that the control panels are in the right place
+ SetModel( GetFinishedModel( 1 ) );
+
+ BaseClass::InitializeMapPlacedObject();
+}
+
+bool CObjectDispenser::ShouldBeMiniBuilding( CTFPlayer* pPlayer )
+{
+#ifdef STAGING_ONLY
+ int nMiniDispenserEnabled = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( GetOwner(), nMiniDispenserEnabled, allows_building_mini_dispenser );
+ return nMiniDispenserEnabled != 0;
+#else
+ return false;
+#endif // STAGING_ONLY
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CObjectDispenser::GetMaxUpgradeLevel()
+{
+#ifdef STAGING_ONLY
+ if ( IsMiniBuilding() )
+ return DISPENSER_MINI_MAX_LEVEL;
+#endif // STAGING_ONLY
+
+ return BaseClass::GetMaxUpgradeLevel();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Finished building
+//-----------------------------------------------------------------------------
+void CObjectDispenser::OnGoActive( void )
+{
+ SetModel( GetFinishedModel( 1 ) );
+
+ CreateBuildPoints();
+
+ ReattachChildren();
+
+ // Put some ammo in the Dispenser
+ if ( !m_bCarryDeploy && !IsMiniBuilding() )
+ {
+ m_iAmmoMetal = TFGameRules()->IsQuickBuildTime() ? DISPENSER_MAX_METAL_AMMO : 25;
+ }
+
+ // Begin thinking
+ SetContextThink( &CObjectDispenser::RefillThink, gpGlobals->curtime + 3, REFILL_CONTEXT );
+ SetContextThink( &CObjectDispenser::DispenseThink, gpGlobals->curtime + 0.1, DISPENSE_CONTEXT );
+
+ m_flNextAmmoDispense = gpGlobals->curtime + 0.5;
+
+ if ( m_hTouchTrigger.Get() && dynamic_cast<CObjectCartDispenser*>(this) != NULL )
+ {
+ UTIL_Remove( m_hTouchTrigger.Get() );
+ m_hTouchTrigger = NULL;
+ }
+
+ float flRadius = GetDispenserRadius();
+
+ if ( m_iszCustomTouchTrigger != NULL_STRING )
+ {
+ m_hTouchTrigger = dynamic_cast<CDispenserTouchTrigger *> ( gEntList.FindEntityByName( NULL, m_iszCustomTouchTrigger ) );
+
+ if ( m_hTouchTrigger.Get() != NULL )
+ {
+ m_hTouchTrigger->SetOwnerEntity( this ); //owned
+ }
+ }
+
+ if ( m_hTouchTrigger.Get() == NULL )
+ {
+ m_hTouchTrigger = CBaseEntity::Create( "dispenser_touch_trigger", GetAbsOrigin(), vec3_angle, this );
+ UTIL_SetSize(m_hTouchTrigger.Get(), Vector(-flRadius,-flRadius,-flRadius), Vector(flRadius,flRadius,flRadius) );
+ m_hTouchTrigger->SetSolid(SOLID_BBOX);
+ }
+
+ Assert( m_hTouchTrigger.Get() );
+
+ BaseClass::OnGoActive();
+
+ PlayActiveSound();
+}
+
+void CObjectDispenser::PlayActiveSound()
+{
+ EmitSound( "Building_Dispenser.Idle" );
+}
+
+//-----------------------------------------------------------------------------
+// Spawn the vgui control screens on the object
+//-----------------------------------------------------------------------------
+void CObjectDispenser::GetControlPanelInfo( int nPanelIndex, const char *&pPanelName )
+{
+ // Panels 0 and 1 are both control panels for now
+ if ( nPanelIndex == 0 || nPanelIndex == 1 )
+ {
+ if ( GetTeamNumber() == TF_TEAM_RED )
+ {
+ pPanelName = "screen_obj_dispenser_red";
+ }
+ else
+ {
+ pPanelName = "screen_obj_dispenser_blue";
+ }
+ }
+ else
+ {
+ BaseClass::GetControlPanelInfo( nPanelIndex, pPanelName );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectDispenser::Precache()
+{
+ BaseClass::Precache();
+
+ int iModelIndex;
+
+ PrecacheModel( DISPENSER_MODEL_PLACEMENT );
+
+ iModelIndex = PrecacheModel( DISPENSER_MODEL_BUILDING );
+ PrecacheGibsForModel( iModelIndex );
+
+ iModelIndex = PrecacheModel( DISPENSER_MODEL );
+ PrecacheGibsForModel( iModelIndex );
+
+ iModelIndex = PrecacheModel( DISPENSER_MODEL_BUILDING_LVL2 );
+ PrecacheGibsForModel( iModelIndex );
+
+ iModelIndex = PrecacheModel( DISPENSER_MODEL_LVL2 );
+ PrecacheGibsForModel( iModelIndex );
+
+ iModelIndex = PrecacheModel( DISPENSER_MODEL_BUILDING_LVL3 );
+ PrecacheGibsForModel( iModelIndex );
+
+ iModelIndex = PrecacheModel( DISPENSER_MODEL_LVL3 );
+ PrecacheGibsForModel( iModelIndex );
+
+#ifdef STAGING_ONLY
+ PrecacheGibsForModel( PrecacheModel( MINI_DISPENSER_MODEL_PLACEMENT ) );
+ PrecacheGibsForModel( PrecacheModel( MINI_DISPENSER_MODEL_BUILDING ) );
+ PrecacheGibsForModel( PrecacheModel( MINI_DISPENSER_MODEL ) );
+#endif // STAGING_ONLY
+
+ PrecacheVGuiScreen( "screen_obj_dispenser_blue" );
+ PrecacheVGuiScreen( "screen_obj_dispenser_red" );
+
+ PrecacheScriptSound( "Building_Dispenser.Idle" );
+ PrecacheScriptSound( "Building_Dispenser.GenerateMetal" );
+ PrecacheScriptSound( "Building_Dispenser.Heal" );
+
+ PrecacheParticleSystem( "dispenser_heal_red" );
+ PrecacheParticleSystem( "dispenser_heal_blue" );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CObjectDispenser::DispenseAmmo( CTFPlayer *pPlayer )
+{
+ int iTotalPickedUp = 0;
+ int iAmmoToAdd = 0;
+
+ int nNoPrimaryAmmoFromDispensersWhileActive = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pPlayer->GetActiveWeapon(), nNoPrimaryAmmoFromDispensersWhileActive, no_primary_ammo_from_dispensers );
+
+#ifdef STAGING_ONLY
+ float flAmmoRate = IsMiniBuilding() ? DISPENSER_MINI_AMMO_RATE : g_flDispenserAmmoRates[GetUpgradeLevel()];
+#else
+ float flAmmoRate = g_flDispenserAmmoRates[GetUpgradeLevel()];
+#endif
+
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetBuilder(), flAmmoRate, mult_dispenser_rate );
+
+ if ( nNoPrimaryAmmoFromDispensersWhileActive == 0 )
+ {
+ // primary
+ iAmmoToAdd = (int)( pPlayer->GetMaxAmmo( TF_AMMO_PRIMARY ) * flAmmoRate );
+ iTotalPickedUp += pPlayer->GiveAmmo( iAmmoToAdd, TF_AMMO_PRIMARY, !m_bPlayAmmoPickupSound, kAmmoSource_DispenserOrCart );
+ }
+
+ // secondary
+ iAmmoToAdd = (int)( pPlayer->GetMaxAmmo( TF_AMMO_SECONDARY ) * flAmmoRate );
+ iTotalPickedUp += pPlayer->GiveAmmo( iAmmoToAdd, TF_AMMO_SECONDARY, !m_bPlayAmmoPickupSound, kAmmoSource_DispenserOrCart );
+
+ // metal
+ int iNoMetalFromDispenserWhileActive = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pPlayer->GetActiveWeapon(), iNoMetalFromDispenserWhileActive, no_metal_from_dispensers_while_active );
+ if ( iNoMetalFromDispenserWhileActive == 0 )
+ {
+ iTotalPickedUp += DispenseMetal( pPlayer );
+ }
+
+ if ( iTotalPickedUp > 0 )
+ {
+ if ( m_bPlayAmmoPickupSound )
+ {
+ EmitSound( "BaseCombatCharacter.AmmoPickup" );
+ }
+
+ CTFPlayer *pOwner = GetOwner();
+ if ( pOwner && pOwner != pPlayer )
+ {
+ // This is crude; it doesn't account for the value difference in resupplying rockets vs pistol bullets.
+ // Still, it's better than nothing when trying to measure the value classes generate.
+ if ( TFGameRules() &&
+ TFGameRules()->GameModeUsesUpgrades() &&
+ TFGameRules()->State_Get() == GR_STATE_RND_RUNNING )
+ {
+ CTF_GameStats.Event_PlayerAwardBonusPoints( pOwner, pPlayer, 1 );
+ }
+ }
+
+ return true;
+ }
+
+ // return false if we didn't pick up anything
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CObjectDispenser::DispenseMetal( CTFPlayer *pPlayer )
+{
+ int iMetalToGive = DISPENSER_DROP_METAL + ((GetUpgradeLevel()-1) * 10);
+ int iMetal = pPlayer->GiveAmmo( Min( m_iAmmoMetal.Get(), iMetalToGive ), TF_AMMO_METAL, false, kAmmoSource_DispenserOrCart );
+ m_iAmmoMetal -= iMetal;
+
+ return iMetal;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectDispenser::RefillThink( void )
+{
+ if ( IsCarried() )
+ return;
+
+ SetContextThink( &CObjectDispenser::RefillThink, gpGlobals->curtime + 6, REFILL_CONTEXT );
+
+ if ( IsDisabled() )
+ {
+ return;
+ }
+
+ // Auto-refill half the amount as tfc, but twice as often
+ if ( m_iAmmoMetal < DISPENSER_MAX_METAL_AMMO )
+ {
+ int iMetal = (DISPENSER_MAX_METAL_AMMO * 0.1) + ((GetUpgradeLevel()-1) * 10);
+
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetBuilder(), iMetal, mult_dispenser_rate );
+
+ m_iAmmoMetal = Min( m_iAmmoMetal + iMetal, DISPENSER_MAX_METAL_AMMO );
+
+ if ( m_bUseGenerateMetalSound )
+ {
+ EmitSound( "Building_Dispenser.GenerateMetal" );
+ }
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Generate ammo over time
+//-----------------------------------------------------------------------------
+void CObjectDispenser::DispenseThink( void )
+{
+ if ( IsCarried() )
+ return;
+
+ if ( IsDisabled() )
+ {
+ // Don't heal or dispense ammo
+ SetContextThink( &CObjectDispenser::DispenseThink, gpGlobals->curtime + 0.1, DISPENSE_CONTEXT );
+
+ // stop healing everyone
+ ResetHealingTargets();
+ return;
+ }
+
+ float flRadius = GetDispenserRadius();
+
+ if ( m_flNextAmmoDispense <= gpGlobals->curtime )
+ {
+ int iNumNearbyPlayers = 0;
+
+ if ( GetOwner() )
+ {
+ // find players in sphere, that are visible
+ if ( ( flRadius != m_flPrevRadius ) && m_hTouchTrigger.Get() )
+ {
+ UTIL_SetSize( m_hTouchTrigger.Get(), Vector( -flRadius, -flRadius, -flRadius ), Vector( flRadius, flRadius, flRadius ) );
+ }
+ }
+
+ m_flPrevRadius = flRadius;
+
+ Vector vecOrigin = GetAbsOrigin() + Vector(0,0,32);
+
+ CBaseEntity *pListOfNearbyEntities[32];
+ int iNumberOfNearbyEntities = UTIL_EntitiesInSphere( pListOfNearbyEntities, ARRAYSIZE( pListOfNearbyEntities ), vecOrigin, flRadius, FL_CLIENT );
+ for ( int i=0;i<iNumberOfNearbyEntities;i++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( pListOfNearbyEntities[i] );
+
+ if ( !pPlayer || !pPlayer->IsAlive() )
+ continue;
+
+ if ( pPlayer->GetTeamNumber() != GetTeamNumber() )
+ {
+ if ( !pPlayer->IsPlayerClass( TF_CLASS_SPY ) || ( pPlayer->m_Shared.GetDisguiseTeam() != GetTeamNumber() ) )
+ continue;
+ }
+
+ DispenseAmmo( pPlayer );
+
+ iNumNearbyPlayers++;
+ }
+
+ // Try to dispense more often when no players are around so we
+ // give it as soon as possible when a new player shows up
+#ifdef STAGING_ONLY
+ float flNextAmmoDelay = IsMiniBuilding() ? DISPENSER_MINI_AMMO_THINK : 1.0;
+#else
+ float flNextAmmoDelay = 1.0;
+#endif
+ m_flNextAmmoDispense = gpGlobals->curtime + ( ( iNumNearbyPlayers > 0 ) ? flNextAmmoDelay : 0.1 );
+ }
+
+ // for each player in touching list
+ int iSize = m_hTouchingEntities.Count();
+ bool bIsAnyTeammateTouching = false;
+
+ if ( m_hTouchTrigger )
+ {
+ for ( int i = iSize-1; i >= 0; i-- )
+ {
+ EHANDLE hOther = m_hTouchingEntities[i];
+
+ CBaseEntity *pEnt = hOther.Get();
+ if ( !pEnt )
+ continue;
+
+ // stop touching and healing a dead entity, or one that is grossly out of range (EndTouch() can be flakey)
+ float flDistSqr = (m_hTouchTrigger->WorldSpaceCenter() - pEnt->WorldSpaceCenter()).LengthSqr();
+ Vector vecMins, vecMaxs;
+ m_hTouchTrigger->GetCollideable()->WorldSpaceSurroundingBounds( &vecMins, &vecMaxs );
+ float flDoubleRadiusSqr = ( vecMaxs - vecMins ).LengthSqr();
+ if ( !pEnt->IsAlive() || ( flDistSqr > flDoubleRadiusSqr ) )
+ {
+ m_hTouchingEntities.FindAndRemove( hOther );
+ StopHealing( pEnt );
+ continue;
+ }
+
+ bIsAnyTeammateTouching |= ( pEnt->IsPlayer() && pEnt->GetTeamNumber() == GetTeamNumber() );
+
+
+ bool bHealingTarget = IsHealingTarget( pEnt );
+ bool bValidHealTarget = CouldHealTarget( pEnt );
+
+ if ( bHealingTarget && !bValidHealTarget )
+ {
+ // if we can't see them, remove them from healing list
+ // does nothing if we are not healing them already
+ StopHealing( pEnt );
+ }
+ else if ( !bHealingTarget && bValidHealTarget )
+ {
+ // if we can see them, add to healing list
+ // does nothing if we are healing them already
+ StartHealing( pEnt );
+ }
+ }
+ }
+
+ if ( bIsAnyTeammateTouching )
+ {
+ if ( !m_spellTimer.HasStarted() )
+ {
+ m_spellTimer.Start( tf_cart_spell_drop_rate.GetFloat() );
+ }
+
+ if ( !m_duckTimer.HasStarted() )
+ {
+ m_duckTimer.Start( tf_cart_duck_drop_rate.GetFloat() );
+ }
+
+ if ( !m_soulTimer.HasStarted() )
+ {
+ m_soulTimer.Start( tf_cart_soul_drop_rate.GetFloat() );
+ }
+ }
+ else
+ {
+ m_spellTimer.Invalidate();
+ m_duckTimer.Invalidate();
+ m_soulTimer.Invalidate();
+ }
+
+ if ( m_spellTimer.HasStarted() && m_spellTimer.IsElapsed() )
+ {
+ m_spellTimer.Start( tf_cart_spell_drop_rate.GetFloat() );
+ DropSpellPickup();
+ }
+
+ if ( m_duckTimer.HasStarted() && m_duckTimer.IsElapsed() )
+ {
+ m_duckTimer.Start( tf_cart_duck_drop_rate.GetFloat() );
+ DropDuckPickup();
+ }
+
+ if ( m_soulTimer.HasStarted() && m_soulTimer.IsElapsed() )
+ {
+ m_soulTimer.Start( tf_cart_soul_drop_rate.GetFloat() );
+ DispenseSouls();
+ }
+
+ SetContextThink( &CObjectDispenser::DispenseThink, gpGlobals->curtime + 0.1, DISPENSE_CONTEXT );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectDispenser::StartTouch( CBaseEntity *pOther )
+{
+ // add to touching entities
+ EHANDLE hOther = pOther;
+ m_hTouchingEntities.AddToTail( hOther );
+
+ if ( !IsBuilding() && !IsDisabled() && CouldHealTarget( pOther ) && !IsHealingTarget( pOther ) )
+ {
+ // try to start healing them
+ StartHealing( pOther );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectDispenser::Touch( CBaseEntity *pOther )
+{
+ // We dont want to touch these
+ if ( pOther->IsSolidFlagSet( FSOLID_TRIGGER | FSOLID_VOLUME_CONTENTS ) )
+ return;
+
+ // Handle hitting skybox (disappear).
+ const trace_t *pTrace = &CBaseEntity::GetTouchTrace();
+ if( pTrace->surface.flags & SURF_SKY )
+ {
+ UTIL_Remove( this );
+ return;
+ }
+
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectDispenser::EndTouch( CBaseEntity *pOther )
+{
+ // remove from touching entities
+ EHANDLE hOther = pOther;
+ m_hTouchingEntities.FindAndRemove( hOther );
+
+ // remove from healing list
+ StopHealing( pOther );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectDispenser::ResetHealingTargets( void )
+{
+ // tell all the players we're not healing them anymore
+ for ( int i = m_hHealingTargets.Count()-1 ; i >= 0 ; i-- )
+ {
+ EHANDLE hEnt = m_hHealingTargets[i];
+ CBaseEntity *pOther = hEnt.Get();
+
+ if ( pOther )
+ {
+ StopHealing( pOther );
+ }
+ }
+
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Try to start healing this target
+//-----------------------------------------------------------------------------
+float CObjectDispenser::GetHealRate() const
+{
+#ifdef STAGING_ONLY
+ float flHealRate = IsMiniBuilding() ? DISPENSER_MINI_HEAL_RATE : g_flDispenserHealRates[GetUpgradeLevel()];
+#else
+ float flHealRate = g_flDispenserHealRates[GetUpgradeLevel()];
+#endif
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetBuilder(), flHealRate, mult_dispenser_rate );
+
+ return flHealRate;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Try to start healing this target
+//-----------------------------------------------------------------------------
+void CObjectDispenser::StartHealing( CBaseEntity *pOther )
+{
+ if ( IsCarried() )
+ return;
+
+ AddHealingTarget( pOther );
+
+ CTFPlayer *pPlayer = ToTFPlayer( pOther );
+
+ if ( pPlayer )
+ {
+ float flHealRate = GetHealRate();
+ float flOverhealBonus = 1.0;
+ pPlayer->m_Shared.Heal( this, flHealRate, flOverhealBonus, 1.0, true, GetBuilder() );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Stop healing this target
+//-----------------------------------------------------------------------------
+void CObjectDispenser::StopHealing( CBaseEntity *pOther )
+{
+ if ( RemoveHealingTarget( pOther ) )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( pOther );
+
+ if ( pPlayer )
+ {
+ float flHealingDone = pPlayer->m_Shared.StopHealing( this );
+ if ( GetBuilder() && pOther != GetBuilder() && flHealingDone > 0 )
+ {
+ GetBuilder()->AwardAchievement( ACHIEVEMENT_TF_ENGINEER_DISPENSER_HEAL_GRIND, floor( flHealingDone ) );
+
+ if ( GetBuilder()->GetTeam() == pOther->GetTeam() )
+ {
+ // Strange Health Provided to Allies
+ EconEntity_OnOwnerKillEaterEvent(
+ dynamic_cast<CEconEntity *>( GetBuilder()->GetEntityForLoadoutSlot( LOADOUT_POSITION_PDA ) ),
+ GetBuilder(),
+ pPlayer,
+ kKillEaterEvent_HealingProvided,
+ (int)flHealingDone
+ );
+ }
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Is this a valid heal target? and not already healing them?
+//-----------------------------------------------------------------------------
+bool CObjectDispenser::CouldHealTarget( CBaseEntity *pTarget )
+{
+ if ( !HasSpawnFlags( SF_DISPENSER_IGNORE_LOS ) && !pTarget->FVisible( this, MASK_BLOCKLOS ) )
+ return false;
+
+ if ( pTarget->IsPlayer() && pTarget->IsAlive() )
+ {
+ CTFPlayer *pTFPlayer = ToTFPlayer( pTarget );
+
+ // Don't heal while in purgatory. Players take damage constantly while down there in
+ // order to flush them out. If we're healing players down there, that goes against
+ // the purpose of the damage.
+ if ( pTFPlayer->IsInPurgatory() )
+ return false;
+
+ // don't heal enemies unless they are disguised as our team
+ int iTeam = GetTeamNumber();
+ int iPlayerTeam = pTFPlayer->GetTeamNumber();
+
+ if ( iPlayerTeam != iTeam && pTFPlayer->m_Shared.InCond( TF_COND_DISGUISED ) )
+ {
+ iPlayerTeam = pTFPlayer->m_Shared.GetDisguiseTeam();
+ }
+
+ if ( iPlayerTeam != iTeam )
+ {
+ return false;
+ }
+
+ if ( HasSpawnFlags( SF_DISPENSER_DONT_HEAL_DISGUISED_SPIES ) )
+ {
+ // if they're invis, no heals
+ if ( pTFPlayer->m_Shared.IsStealthed() )
+ {
+ return false;
+ }
+
+ // if they're disguised as enemy
+ if ( pTFPlayer->m_Shared.InCond( TF_COND_DISGUISED ) &&
+ pTFPlayer->m_Shared.GetDisguiseTeam() != iTeam )
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+float CObjectDispenser::GetDispenserRadius( void )
+{
+ float flRadius = 64.f;
+
+ if ( GetOwner() )
+ {
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetOwner(), flRadius, mult_dispenser_radius );
+ }
+
+ return flRadius;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectDispenser::AddHealingTarget( CBaseEntity *pOther )
+{
+ // add to tail
+ EHANDLE hOther = pOther;
+ m_hHealingTargets.AddToTail( hOther );
+ NetworkStateChanged();
+
+ // check how many healing targets we now have and possibly award an achievement
+ if ( m_hHealingTargets.Count() >= 3 && GetBuilder() )
+ {
+ GetBuilder()->AwardAchievement( ACHIEVEMENT_TF_ENGINEER_DISPENSER_HEAL_GROUP );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CObjectDispenser::RemoveHealingTarget( CBaseEntity *pOther )
+{
+ // remove
+ EHANDLE hOther = pOther;
+ bool bFound = m_hHealingTargets.FindAndRemove( hOther );
+ NetworkStateChanged();
+
+ return bFound;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Are we healing this target already
+//-----------------------------------------------------------------------------
+bool CObjectDispenser::IsHealingTarget( CBaseEntity *pTarget )
+{
+ EHANDLE hOther = pTarget;
+ return m_hHealingTargets.HasElement( hOther );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CObjectDispenser::DrawDebugTextOverlays(void)
+{
+ int text_offset = BaseClass::DrawDebugTextOverlays();
+
+ if (m_debugOverlays & OVERLAY_TEXT_BIT)
+ {
+ char tempstr[512];
+ Q_snprintf( tempstr, sizeof( tempstr ),"Metal: %d", m_iAmmoMetal.Get() );
+ EntityText(text_offset,tempstr,0);
+ text_offset++;
+ }
+ return text_offset;
+}
+
+//-----------------------------------------------------------------------------
+//
+//-----------------------------------------------------------------------------
+void CObjectDispenser::MakeCarriedObject( CTFPlayer *pCarrier )
+{
+ if ( m_hTouchTrigger.Get() )
+ {
+ UTIL_Remove( m_hTouchTrigger );
+ }
+
+ ResetHealingTargets();
+
+ m_hTouchingEntities.Purge();
+
+ StopSound( "Building_Dispenser.Idle" );
+
+ BaseClass::MakeCarriedObject( pCarrier );
+}
+
+
+//-----------------------------------------------------------------------------
+// Cart Dispenser
+//-----------------------------------------------------------------------------
+
+BEGIN_DATADESC( CObjectCartDispenser )
+ DEFINE_INPUTFUNC( FIELD_INTEGER, "FireHalloweenBonus", InputFireHalloweenBonus ),
+ DEFINE_INPUTFUNC( FIELD_INTEGER, "SetDispenserLevel", InputSetDispenserLevel ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ),
+END_DATADESC()
+
+IMPLEMENT_SERVERCLASS_ST( CObjectCartDispenser, DT_ObjectCartDispenser )
+END_SEND_TABLE()
+
+LINK_ENTITY_TO_CLASS( mapobj_cart_dispenser, CObjectCartDispenser );
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+CObjectCartDispenser::CObjectCartDispenser()
+{
+ m_bUseGenerateMetalSound = false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectCartDispenser::Spawn( void )
+{
+ // This cast is for the benefit of GCC
+ m_fObjectFlags |= (int)OF_DOESNT_HAVE_A_MODEL;
+ m_takedamage = DAMAGE_NO;
+ m_iUpgradeLevel = 1;
+
+ TFGameRules()->OnDispenserBuilt( this );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Finished building
+//-----------------------------------------------------------------------------
+void CObjectCartDispenser::OnGoActive( void )
+{
+ BaseClass::OnGoActive();
+
+ SetModel( "" );
+}
+
+//-----------------------------------------------------------------------------
+// Spawn the vgui control screens on the object
+//-----------------------------------------------------------------------------
+void CObjectCartDispenser::GetControlPanelInfo( int nPanelIndex, const char *&pPanelName )
+{
+ // no panels
+ return;
+}
+
+//-----------------------------------------------------------------------------
+// Don't decrement our metal count
+//-----------------------------------------------------------------------------
+int CObjectCartDispenser::DispenseMetal( CTFPlayer *pPlayer )
+{
+ int iMetal = pPlayer->GiveAmmo( MIN( m_iAmmoMetal, DISPENSER_DROP_METAL ), TF_AMMO_METAL, false, kAmmoSource_DispenserOrCart );
+ return iMetal;
+}
+
+
+void CObjectCartDispenser::DropSpellPickup()
+{
+ if ( TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_HIGHTOWER ) )
+ {
+ TFGameRules()->DropSpellPickup( GetAbsOrigin() );
+ }
+}
+
+void CObjectCartDispenser::DropDuckPickup()
+{
+ if ( TFGameRules()->IsHolidayActive( kHoliday_EOTL ) && TFGameRules()->ShouldDropBonusDuck() )
+ {
+ TFGameRules()->DropBonusDuck( GetAbsOrigin() );
+ }
+}
+
+void CObjectCartDispenser::DispenseSouls()
+{
+ // Give a soul to the entire team
+ if ( TFGameRules() && TFGameRules()->IsHolidayActive( kHoliday_Halloween ) )
+ {
+ TFGameRules()->DropHalloweenSoulPackToTeam( 1, GetAbsOrigin(), GetTeamNumber(), TEAM_SPECTATOR );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectCartDispenser::SetModel( const char *pModel )
+{
+ CBaseObject::SetModel( pModel );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Give players near the dispenser a bonus
+//-----------------------------------------------------------------------------
+void CObjectCartDispenser::InputFireHalloweenBonus( inputdata_t &inputdata )
+{
+ for ( int i = m_hHealingTargets.Count()-1 ; i >= 0 ; i-- )
+ {
+ EHANDLE hEnt = m_hHealingTargets[i];
+ CTFPlayer *pTFPlayer = ToTFPlayer( hEnt.Get() );
+
+ if ( pTFPlayer )
+ {
+ pTFPlayer->m_Shared.AddCond( TF_COND_CRITBOOSTED_CTF_CAPTURE, inputdata.value.Int() );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectCartDispenser::InputSetDispenserLevel( inputdata_t &inputdata )
+{
+ int iLevel = inputdata.value.Int();
+
+ if ( iLevel >= 1 && iLevel <= 3 )
+ {
+ m_iUpgradeLevel = iLevel;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectCartDispenser::InputEnable( inputdata_t &inputdata )
+{
+ SetDisabled( false );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CObjectCartDispenser::InputDisable( inputdata_t &inputdata )
+{
+ SetDisabled( true );
+}
+
+