//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: Base Object built by players // // $NoKeywords: $ //=============================================================================// #include "cbase.h" #include "tf_player.h" #include "tf_team.h" #include "tf_obj.h" #include "tf_weaponbase.h" #include "rope.h" #include "rope_shared.h" #include "bone_setup.h" #include "ndebugoverlay.h" #include "rope_helpers.h" #include "IEffects.h" #include "vstdlib/random.h" #include "tier1/strtools.h" #include "basegrenade_shared.h" #include "tf_gamerules.h" #include "engine/IEngineSound.h" #include "tf_shareddefs.h" #include "vguiscreen.h" #include "hierarchy.h" #include "func_no_build.h" #include "func_respawnroom.h" #include #include "ihasbuildpoints.h" #include "utldict.h" #include "filesystem.h" #include "npcevent.h" #include "tf_shareddefs.h" #include "animation.h" #include "effect_dispatch_data.h" #include "te_effect_dispatch.h" #include "tf_gamestats.h" #include "tf_ammo_pack.h" #include "tf_obj_sapper.h" #include "particle_parse.h" #include "tf_fx.h" #include "trains.h" #include "serverbenchmark_base.h" #include "tf_weapon_wrench.h" #include "tf_weapon_grenade_pipebomb.h" #include "tf_weapon_builder.h" #include "player_vs_environment/tf_population_manager.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" // Control panels #define SCREEN_OVERLAY_MATERIAL "vgui/screens/vgui_overlay" #define ROPE_HANG_DIST 150 #define UPGRADE_LEVEL_HEALTH_MULTIPLIER 1.2f ConVar tf_obj_gib_velocity_min( "tf_obj_gib_velocity_min", "100", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); ConVar tf_obj_gib_velocity_max( "tf_obj_gib_velocity_max", "450", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); ConVar tf_obj_gib_maxspeed( "tf_obj_gib_maxspeed", "800", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); ConVar tf_obj_upgrade_per_hit( "tf_obj_upgrade_per_hit", "25", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); ConVar object_verbose( "object_verbose", "0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Debug object system." ); ConVar obj_damage_factor( "obj_damage_factor","0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Factor applied to all damage done to objects" ); ConVar obj_child_damage_factor( "obj_child_damage_factor","0.25", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Factor applied to damage done to objects that are built on a buildpoint" ); ConVar tf_fastbuild("tf_fastbuild", "0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); ConVar tf_obj_ground_clearance( "tf_obj_ground_clearance", "32", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Object corners can be this high above the ground" ); ConVar tf_obj_damage_tank_achievement_amount( "tf_obj_damage_tank_achievement_amount", "2000", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); extern short g_sModelIndexFireball; extern ConVar tf_cheapobjects; // Minimum distance between 2 objects to ensure player movement between them #define MINIMUM_OBJECT_SAFE_DISTANCE 100 // Maximum number of a type of objects on a single resource zone #define MAX_OBJECTS_PER_ZONE 1 // Time it takes a fully healed object to deteriorate ConVar object_deterioration_time( "object_deterioration_time", "30", 0, "Time it takes for a fully-healed object to deteriorate." ); // Time after taking damage that an object will still drop resources on death #define MAX_DROP_TIME_AFTER_DAMAGE 5 #define OBJ_BASE_THINK_CONTEXT "BaseObjectThink" #define PLASMA_DISABLE_TIME 4 IMPLEMENT_AUTO_LIST( IBaseObjectAutoList ); BEGIN_DATADESC( CBaseObject ) // keys DEFINE_KEYFIELD_NOT_SAVED( m_SolidToPlayers, FIELD_INTEGER, "SolidToPlayer" ), DEFINE_KEYFIELD( m_nDefaultUpgradeLevel, FIELD_INTEGER, "defaultupgrade" ), DEFINE_THINKFUNC( UpgradeThink ), // Inputs DEFINE_INPUTFUNC( FIELD_INTEGER, "SetHealth", InputSetHealth ), DEFINE_INPUTFUNC( FIELD_INTEGER, "AddHealth", InputAddHealth ), DEFINE_INPUTFUNC( FIELD_INTEGER, "RemoveHealth", InputRemoveHealth ), DEFINE_INPUTFUNC( FIELD_INTEGER, "SetSolidToPlayer", InputSetSolidToPlayer ), DEFINE_INPUTFUNC( FIELD_STRING, "SetBuilder", InputSetBuilder ), DEFINE_INPUTFUNC( FIELD_VOID, "Show", InputShow ), DEFINE_INPUTFUNC( FIELD_VOID, "Hide", InputHide ), DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), // Outputs DEFINE_OUTPUT( m_OnDestroyed, "OnDestroyed" ), DEFINE_OUTPUT( m_OnDamaged, "OnDamaged" ), DEFINE_OUTPUT( m_OnRepaired, "OnRepaired" ), DEFINE_OUTPUT( m_OnBecomingDisabled, "OnDisabled" ), DEFINE_OUTPUT( m_OnBecomingReenabled, "OnReenabled" ), DEFINE_OUTPUT( m_OnObjectHealthChanged, "OnObjectHealthChanged" ) END_DATADESC() IMPLEMENT_SERVERCLASS_ST(CBaseObject, DT_BaseObject) SendPropInt(SENDINFO(m_iHealth), -1, SPROP_VARINT ), SendPropInt(SENDINFO(m_iMaxHealth), -1, SPROP_VARINT ), SendPropBool(SENDINFO(m_bHasSapper) ), SendPropInt(SENDINFO(m_iObjectType), Q_log2( OBJ_LAST ) + 1, SPROP_UNSIGNED ), SendPropBool(SENDINFO(m_bBuilding) ), SendPropBool(SENDINFO(m_bPlacing) ), SendPropBool(SENDINFO(m_bCarried) ), SendPropBool(SENDINFO(m_bCarryDeploy) ), SendPropBool(SENDINFO(m_bMiniBuilding) ), SendPropFloat(SENDINFO(m_flPercentageConstructed), 8, 0, 0.0, 1.0f ), SendPropInt(SENDINFO(m_fObjectFlags), OF_BIT_COUNT, SPROP_UNSIGNED ), SendPropEHandle(SENDINFO(m_hBuiltOnEntity)), SendPropBool( SENDINFO( m_bDisabled ) ), SendPropEHandle( SENDINFO( m_hBuilder ) ), SendPropVector( SENDINFO( m_vecBuildMaxs ), -1, SPROP_COORD ), SendPropVector( SENDINFO( m_vecBuildMins ), -1, SPROP_COORD ), SendPropInt( SENDINFO( m_iDesiredBuildRotations ), 2, SPROP_UNSIGNED ), SendPropBool( SENDINFO( m_bServerOverridePlacement ) ), SendPropInt( SENDINFO(m_iUpgradeLevel), 3 ), SendPropInt( SENDINFO(m_iUpgradeMetal), 10 ), SendPropInt( SENDINFO(m_iUpgradeMetalRequired), 10 ), SendPropInt( SENDINFO(m_iHighestUpgradeLevel), 3 ), SendPropInt( SENDINFO(m_iObjectMode), 2, SPROP_UNSIGNED ), SendPropBool( SENDINFO( m_bDisposableBuilding ) ), SendPropBool( SENDINFO( m_bWasMapPlaced ) ), SendPropBool( SENDINFO( m_bPlasmaDisable ) ), END_SEND_TABLE(); bool PlayerIndexLessFunc( const int &lhs, const int &rhs ) { return lhs < rhs; } // This controls whether ropes attached to objects are transmitted or not. It's important that // ropes aren't transmitted to guys who don't own them. class CObjectRopeTransmitProxy : public CBaseTransmitProxy { public: CObjectRopeTransmitProxy( CBaseEntity *pRope ) : CBaseTransmitProxy( pRope ) { } virtual int ShouldTransmit( const CCheckTransmitInfo *pInfo, int nPrevShouldTransmitResult ) { // Don't transmit the rope if it's not even visible. if ( !nPrevShouldTransmitResult ) return FL_EDICT_DONTSEND; // This proxy only wants to be active while one of the two objects is being placed. // When they're done being placed, the proxy goes away and the rope draws like normal. bool bAnyObjectPlacing = (m_hObj1 && m_hObj1->IsPlacing()) || (m_hObj2 && m_hObj2->IsPlacing()); if ( !bAnyObjectPlacing ) { Release(); return nPrevShouldTransmitResult; } // Give control to whichever object is being placed. if ( m_hObj1 && m_hObj1->IsPlacing() ) return m_hObj1->ShouldTransmit( pInfo ); else if ( m_hObj2 && m_hObj2->IsPlacing() ) return m_hObj2->ShouldTransmit( pInfo ); else return FL_EDICT_ALWAYS; } CHandle m_hObj1; CHandle m_hObj2; }; //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CBaseObject::CBaseObject() { m_iHealth = m_iMaxHealth = m_flHealth = 0; m_flPercentageConstructed = 0; m_bPlacing = false; m_bBuilding = false; m_Activity = ACT_INVALID; m_bDisabled = false; m_SolidToPlayers = SOLID_TO_PLAYER_USE_DEFAULT; m_bPlacementOK = false; m_aGibs.Purge(); m_iHighestUpgradeLevel = 1; m_bCarryDeploy = false; m_flCarryDeployTime = 0; m_iHealthOnPickup = 0; m_iLifetimeDamage = 0; m_bCannotDie = false; m_bMiniBuilding = false; m_flPlasmaDisableTime = 0; m_bPlasmaDisable = false; m_bDisposableBuilding = false; m_vecBuildForward = vec3_origin; m_flBuildDistance = 0.0f; m_bForceQuickBuild = false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::UpdateOnRemove( void ) { m_bDying = true; // check for sapper crits CObjectSapper *pSapper = GetSapper(); if ( pSapper ) { // give an assist to the sapper's owner CTFPlayer *pSapperOwner = pSapper->GetOwner(); if ( pSapperOwner ) { pSapperOwner->m_Shared.IncrementRevengeCrits(); } } DestroyObject(); if ( GetTeam() ) { ((CTFTeam*)GetTeam())->RemoveObject( this ); } DetachObjectFromObject(); // Make sure the object isn't in either team's list of objects... //Assert( !GetGlobalTFTeam(1)->IsObjectOnTeam( this ) ); //Assert( !GetGlobalTFTeam(2)->IsObjectOnTeam( this ) ); // Chain at end to mimic destructor unwind order BaseClass::UpdateOnRemove(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CBaseObject::UpdateTransmitState() { return SetTransmitState( FL_EDICT_FULLCHECK ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CBaseObject::ShouldTransmit( const CCheckTransmitInfo *pInfo ) { // Always transmit to owner if ( GetBuilder() && pInfo->m_pClientEnt == GetBuilder()->edict() ) return FL_EDICT_ALWAYS; // Placement models only transmit to owners if ( IsPlacing() ) return FL_EDICT_DONTSEND; if ( pInfo->m_pClientEnt ) { CBaseEntity *pRecipientEntity = CBaseEntity::Instance( pInfo->m_pClientEnt ); if ( pRecipientEntity && pRecipientEntity->ShouldForceTransmitsForTeam( GetTeamNumber() ) ) return FL_EDICT_ALWAYS; } return BaseClass::ShouldTransmit( pInfo ); } void CBaseObject::SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ) { // Are we already marked for transmission? if ( pInfo->m_pTransmitEdict->Get( entindex() ) ) return; BaseClass::SetTransmit( pInfo, bAlways ); // Force our screens to be sent too. int nTeam = CBaseEntity::Instance( pInfo->m_pClientEnt )->GetTeamNumber(); for ( int i=0; i < m_hScreens.Count(); i++ ) { CVGuiScreen *pScreen = m_hScreens[i].Get(); if ( pScreen && pScreen->IsVisibleToTeam( nTeam ) ) pScreen->SetTransmit( pInfo, bAlways ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::Precache() { PrecacheMaterial( SCREEN_OVERLAY_MATERIAL ); PrecacheScriptSound( GetObjectInfo( ObjectType() )->m_pExplodeSound ); PrecacheScriptSound( GetObjectInfo( ObjectType() )->m_pUpgradeSound ); const char *pEffect = GetObjectInfo( ObjectType() )->m_pExplosionParticleEffect; if ( pEffect && pEffect[0] != '\0' ) { PrecacheParticleSystem( pEffect ); } PrecacheParticleSystem( "nutsnbolts_build" ); PrecacheParticleSystem( "nutsnbolts_upgrade" ); PrecacheParticleSystem( "nutsnbolts_repair" ); PrecacheModel( "models/weapons/w_models/w_toolbox.mdl" ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::Spawn( void ) { Precache(); CollisionProp()->SetSurroundingBoundsType( USE_BEST_COLLISION_BOUNDS ); SetSolidToPlayers( m_SolidToPlayers, true ); m_bWasMapPlaced = false; m_bHasSapper = false; if ( HasSpawnFlags(SF_BASEOBJ_INVULN) ) { m_takedamage = DAMAGE_NO; } else { m_takedamage = DAMAGE_YES; } AddFlag( FL_OBJECT ); // So NPCs will notice it SetViewOffset( WorldSpaceCenter() - GetAbsOrigin() ); m_iDesiredBuildRotations = 0; m_flCurrentBuildRotation = 0; if ( MustBeBuiltOnAttachmentPoint() ) { AddEffects( EF_NODRAW ); } // assume valid placement m_bServerOverridePlacement = true; m_iUpgradeLevel = 1; m_iUpgradeMetalRequired = GetUpgradeMetalRequired(); if ( !IsCarried() ) { FirstSpawn(); } UpdateLastKnownArea(); } //----------------------------------------------------------------------------- // Purpose: Initialization that should only be done when the object is first created. //----------------------------------------------------------------------------- void CBaseObject::FirstSpawn() { if ( !VPhysicsGetObject() ) VPhysicsInitStatic(); m_iUpgradeMetal = 0; m_iKills = 0; m_iAssists = 0; m_ConstructorList.SetLessFunc( PlayerIndexLessFunc ); m_flHealth = m_iMaxHealth = m_iHealth; SetContextThink( &CBaseObject::BaseObjectThink, gpGlobals->curtime + 0.1, OBJ_BASE_THINK_CONTEXT ); } //----------------------------------------------------------------------------- // Returns information about the various control panels //----------------------------------------------------------------------------- void CBaseObject::GetControlPanelInfo( int nPanelIndex, const char *&pPanelName ) { pPanelName = NULL; } //----------------------------------------------------------------------------- // Returns information about the various control panels //----------------------------------------------------------------------------- void CBaseObject::GetControlPanelClassName( int nPanelIndex, const char *&pPanelName ) { pPanelName = "vgui_screen"; } //----------------------------------------------------------------------------- // This is called by the base object when it's time to spawn the control panels //----------------------------------------------------------------------------- void CBaseObject::SpawnControlPanels() { char buf[64]; // FIXME: Deal with dynamically resizing control panels? // If we're attached to an entity, spawn control panels on it instead of use CBaseAnimating *pEntityToSpawnOn = this; const char *pOrgLL = "controlpanel%d_ll"; const char *pOrgUR = "controlpanel%d_ur"; const char *pAttachmentNameLL = pOrgLL; const char *pAttachmentNameUR = pOrgUR; if ( IsBuiltOnAttachment() ) { pEntityToSpawnOn = dynamic_cast((CBaseEntity*)m_hBuiltOnEntity.Get()); if ( pEntityToSpawnOn ) { char sBuildPointLL[64]; char sBuildPointUR[64]; Q_snprintf( sBuildPointLL, sizeof( sBuildPointLL ), "bp%d_controlpanel%%d_ll", m_iBuiltOnPoint ); Q_snprintf( sBuildPointUR, sizeof( sBuildPointUR ), "bp%d_controlpanel%%d_ur", m_iBuiltOnPoint ); pAttachmentNameLL = sBuildPointLL; pAttachmentNameUR = sBuildPointUR; } else { pEntityToSpawnOn = this; } } Assert( pEntityToSpawnOn ); // Lookup the attachment point... int nPanel; for ( nPanel = 0; true; ++nPanel ) { Q_snprintf( buf, sizeof( buf ), pAttachmentNameLL, nPanel ); int nLLAttachmentIndex = pEntityToSpawnOn->LookupAttachment(buf); if (nLLAttachmentIndex <= 0) { // Try and use my panels then pEntityToSpawnOn = this; Q_snprintf( buf, sizeof( buf ), pOrgLL, nPanel ); nLLAttachmentIndex = pEntityToSpawnOn->LookupAttachment(buf); if (nLLAttachmentIndex <= 0) return; } Q_snprintf( buf, sizeof( buf ), pAttachmentNameUR, nPanel ); int nURAttachmentIndex = pEntityToSpawnOn->LookupAttachment(buf); if (nURAttachmentIndex <= 0) { // Try and use my panels then Q_snprintf( buf, sizeof( buf ), pOrgUR, nPanel ); nURAttachmentIndex = pEntityToSpawnOn->LookupAttachment(buf); if (nURAttachmentIndex <= 0) return; } const char *pScreenName = NULL; GetControlPanelInfo( nPanel, pScreenName ); if (!pScreenName) continue; const char *pScreenClassname; GetControlPanelClassName( nPanel, pScreenClassname ); if ( !pScreenClassname ) continue; // Compute the screen size from the attachment points... matrix3x4_t panelToWorld; pEntityToSpawnOn->GetAttachment( nLLAttachmentIndex, panelToWorld ); matrix3x4_t worldToPanel; MatrixInvert( panelToWorld, worldToPanel ); // Now get the lower right position + transform into panel space Vector lr, lrlocal; pEntityToSpawnOn->GetAttachment( nURAttachmentIndex, panelToWorld ); MatrixGetColumn( panelToWorld, 3, lr ); VectorTransform( lr, worldToPanel, lrlocal ); float flWidth = lrlocal.x; float flHeight = lrlocal.y; CVGuiScreen *pScreen = CreateVGuiScreen( pScreenClassname, pScreenName, pEntityToSpawnOn, this, nLLAttachmentIndex ); pScreen->ChangeTeam( GetTeamNumber() ); pScreen->SetActualSize( flWidth, flHeight ); pScreen->SetActive( false ); pScreen->MakeVisibleOnlyToTeammates( true ); pScreen->SetOverlayMaterial( SCREEN_OVERLAY_MATERIAL ); pScreen->SetTransparency( true ); // for now, only input by the owning player pScreen->SetPlayerOwner( GetBuilder(), true ); int nScreen = m_hScreens.AddToTail( ); m_hScreens[nScreen].Set( pScreen ); } } //----------------------------------------------------------------------------- // Handle commands sent from vgui panels on the client //----------------------------------------------------------------------------- bool CBaseObject::ClientCommand( CTFPlayer *pSender, const CCommand &args ) { //const char *pCmd = args[0]; return false; } #define BASE_OBJECT_THINK_DELAY 0.1 //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::BaseObjectThink( void ) { SetNextThink( gpGlobals->curtime + BASE_OBJECT_THINK_DELAY, OBJ_BASE_THINK_CONTEXT ); // Make sure animation is up to date DetermineAnimation(); DeterminePlaybackRate(); if ( m_bPlasmaDisable ) { if ( gpGlobals->curtime > (m_flPlasmaDisableTime ) ) { m_bPlasmaDisable = false; UpdateDisabledState(); } } // Do nothing while we're being placed if ( IsPlacing() ) { if ( MustBeBuiltOnAttachmentPoint() ) { UpdateAttachmentPlacement(); m_bServerOverridePlacement = true; } else { m_bServerOverridePlacement = IsPlacementPosValid(); UpdateDesiredBuildRotation( BASE_OBJECT_THINK_DELAY ); } return; } // If we're building, keep going if ( IsBuilding() ) { BuildingThink(); return; } if ( IsUpgrading() ) { UpgradeThink(); } else { if ( GetReversesBuildingConstructionSpeed() > 0.0f ) { DoReverseBuild(); } else { if ( GetUpgradeLevel() < GetHighestUpgradeLevel() ) { // Keep moving up levels until we reach the level we were at before. StartUpgrading(); } else { m_bCarryDeploy = false; } } } } void CBaseObject::ResetPlacement( void ) { m_bPlacementOK = false; // Clear out previous parent if ( m_hBuiltOnEntity.Get() ) { m_hBuiltOnEntity = NULL; m_iBuiltOnPoint = 0; SetParent( NULL ); } // teleport to builder's origin CTFPlayer *pPlayer = GetOwner(); if ( pPlayer ) { Teleport( &pPlayer->WorldSpaceCenter(), &GetLocalAngles(), NULL ); } } bool CBaseObject::UpdateAttachmentPlacement( CBaseObject *pObjectOverride ) { // See if we should snap to a build position // finding one implies it is a valid position if ( FindSnapToBuildPos( pObjectOverride ) ) { m_bPlacementOK = true; Teleport( &m_vecBuildOrigin, &GetLocalAngles(), NULL ); } else { ResetPlacement(); } return m_bPlacementOK; } //----------------------------------------------------------------------------- // Purpose: Cheap check to see if we are in any server-defined No-build areas. //----------------------------------------------------------------------------- bool CBaseObject::EstimateValidBuildPos( void ) { // Make sure CalculatePlacementPos() has been called to setup the member variables used below CTFPlayer *pPlayer = GetOwner(); if ( !pPlayer ) return false; // Cannot build inside a nobuild brush if ( PointInNoBuild( m_vecBuildOrigin, this ) ) return false; if ( PointInNoBuild( m_vecBuildCenterOfMass, this ) ) return false; // If we're receiving trigger hurt damage, don't allow building here. if ( IsTakingTriggerHurtDamageAtPoint( m_vecBuildOrigin ) ) return false; if ( IsTakingTriggerHurtDamageAtPoint( m_vecBuildCenterOfMass ) ) return false; if ( PointInRespawnRoom( NULL, m_vecBuildOrigin ) && !g_pServerBenchmark->IsBenchmarkRunning() ) return false; if ( PointInRespawnRoom( NULL, m_vecBuildCenterOfMass ) && !g_pServerBenchmark->IsBenchmarkRunning() ) return false; Vector vecBuildFarEdge = m_vecBuildOrigin + m_vecBuildForward * ( m_flBuildDistance + 8.0f ); if ( PointsCrossRespawnRoomVisualizer( pPlayer->WorldSpaceCenter(), vecBuildFarEdge ) ) return false; return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::DeterminePlaybackRate( void ) { float flReverseBuildingConstructionSpeed = GetReversesBuildingConstructionSpeed(); if ( flReverseBuildingConstructionSpeed == 0.0f ) { flReverseBuildingConstructionSpeed = 1.0f; } else { flReverseBuildingConstructionSpeed *= -1.0f; } // If a sapper was added or removed part way through construction we need to invert the time to completion bool bAdjustCompleteTime = ( flReverseBuildingConstructionSpeed > 0.0f && GetPlaybackRate() < 0.0f ) || ( flReverseBuildingConstructionSpeed < 0.0f && GetPlaybackRate() >= 0.0f ); if ( IsBuilding() ) { // Default half rate, author build anim as if one player is building // ConstructionMultiplier already contains the reverse SetPlaybackRate( GetConstructionMultiplier() * 0.5 ); } else { SetPlaybackRate( 1.0 * flReverseBuildingConstructionSpeed ); } if ( bAdjustCompleteTime ) { float fRelativeCycle = ( ( flReverseBuildingConstructionSpeed > 0.0f ) ? ( 1.0f - GetCycle() ) : ( GetCycle() ) ); float flUpgradeTime = ( ShouldQuickBuild() ? 0 : GetObjectInfo( ObjectType() )->m_flUpgradeDuration ); flUpgradeTime /= ( ( flReverseBuildingConstructionSpeed < 0.0f ) ? ( flReverseBuildingConstructionSpeed * -1.0 ) : 1.0f ); m_flUpgradeCompleteTime = gpGlobals->curtime + flUpgradeTime * fRelativeCycle; m_flTotalConstructionTime = m_flConstructionTimeLeft = GetTotalTime(); float flNewConstructionTimeLeft = m_flConstructionTimeLeft * fRelativeCycle; m_flConstructionTimeLeft *= fRelativeCycle; m_flConstructionStartTime += m_flConstructionTimeLeft - flNewConstructionTimeLeft; m_flConstructionTimeLeft = flNewConstructionTimeLeft; } if ( !(m_fObjectFlags & OF_DOESNT_HAVE_A_MODEL) ) { StudioFrameAdvance(); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CTFPlayer *CBaseObject::GetOwner() { return m_hBuilder; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::SetBuilder( CTFPlayer *pBuilder ) { TRACE_OBJECT( UTIL_VarArgs( "%0.2f CBaseObject::SetBuilder builder %s\n", gpGlobals->curtime, pBuilder ? pBuilder->GetPlayerName() : "NULL" ) ); m_hBuilder = pBuilder; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CBaseObject::ObjectType( ) const { return m_iObjectType; } //----------------------------------------------------------------------------- // Purpose: Destroys the object, gives a chance to spawn an explosion //----------------------------------------------------------------------------- void CBaseObject::DetonateObject( void ) { // Blow us up. CTakeDamageInfo info( this, this, vec3_origin, GetAbsOrigin(), 0, DMG_GENERIC ); Killed( info ); } //----------------------------------------------------------------------------- // Purpose: Remove this object from it's team and mark for deletion //----------------------------------------------------------------------------- void CBaseObject::DestroyObject( void ) { TRACE_OBJECT( UTIL_VarArgs( "%0.2f CBaseObject::DestroyObject %p:%s\n", gpGlobals->curtime, this, GetClassname() ) ); // If we are carried, uncarry us before destruction. if ( IsCarried() && GetBuilder() ) { DropCarriedObject( GetBuilder() ); CTFWeaponBuilder *pBuilder = dynamic_cast( GetBuilder()->Weapon_OwnsThisID( TF_WEAPON_BUILDER ) ); if ( pBuilder ) { pBuilder->SwitchOwnersWeaponToLast(); } } if ( GetBuilder() ) { GetBuilder()->OwnedObjectDestroyed( this ); } UTIL_Remove( this ); DestroyScreens(); } //----------------------------------------------------------------------------- // Purpose: Remove any screens that are active on this object //----------------------------------------------------------------------------- void CBaseObject::DestroyScreens( void ) { // Kill the control panels int i; for ( i = m_hScreens.Count(); --i >= 0; ) { DestroyVGuiScreen( m_hScreens[i].Get() ); } m_hScreens.RemoveAll(); } //----------------------------------------------------------------------------- // Purpose: Get the total time it will take to build this object //----------------------------------------------------------------------------- float CBaseObject::GetTotalTime( void ) { float flBuildTime = GetObjectInfo( ObjectType() )->m_flBuildTime; CTFPlayer *pTFBuilder= GetBuilder(); if ( pTFBuilder ) { CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pTFBuilder, flBuildTime, mod_build_rate ); } if ( tf_fastbuild.GetInt() ) { flBuildTime = MIN( 2.f, flBuildTime ); } // quick builds for engineers in Mann Vs Machine mode during setup time if ( TFGameRules()->IsQuickBuildTime() ) { flBuildTime = MIN( 1.0f, flBuildTime ); } return flBuildTime; } //----------------------------------------------------------------------------- // Purpose: Start placing the object //----------------------------------------------------------------------------- void CBaseObject::StartPlacement( CTFPlayer *pPlayer ) { AddSolidFlags( FSOLID_NOT_SOLID ); m_bPlacing = true; m_bBuilding = false; if ( pPlayer ) { SetBuilder( pPlayer ); ChangeTeam( pPlayer->GetTeamNumber() ); } // needed? m_nRenderMode = kRenderNormal; // Set my build size CollisionProp()->WorldSpaceAABB( &m_vecBuildMins.GetForModify(), &m_vecBuildMaxs.GetForModify() ); m_vecBuildMins -= Vector( 4,4,0 ); m_vecBuildMaxs += Vector( 4,4,0 ); m_vecBuildMins -= GetAbsOrigin(); m_vecBuildMaxs -= GetAbsOrigin(); // Set the skin m_nSkin = ( GetTeamNumber() == TF_TEAM_RED ) ? 0 : 1; } //----------------------------------------------------------------------------- // Purpose: Stop placing the object //----------------------------------------------------------------------------- void CBaseObject::StopPlacement( void ) { UTIL_Remove( this ); } //----------------------------------------------------------------------------- // Purpose: Find the nearest buildpoint on the specified entity //----------------------------------------------------------------------------- bool CBaseObject::FindNearestBuildPoint( CBaseEntity *pEntity, CBasePlayer *pBuilder, float &flNearestPoint, Vector &vecNearestBuildPoint, bool bIgnoreChecks ) { bool bFoundPoint = false; IHasBuildPoints *pBPInterface = dynamic_cast(pEntity); Assert( pBPInterface ); // Any empty buildpoints? for ( int i = 0; i < pBPInterface->GetNumBuildPoints(); i++ ) { // Can this object build on this point? if ( pBPInterface->CanBuildObjectOnBuildPoint( i, GetType() ) ) { // Close to this point? Vector vecBPOrigin; QAngle vecBPAngles; if ( pBPInterface->GetBuildPoint(i, vecBPOrigin, vecBPAngles) ) { if ( !bIgnoreChecks ) { // ignore build points outside our view if ( !pBuilder->FInViewCone( vecBPOrigin ) ) continue; // Do a trace to make sure we don't place attachments through things (players, world, etc...) Vector vecStart = pBuilder->EyePosition(); trace_t trace; CTraceFilterNoNPCsOrPlayer ignorePlayersFilter( pBuilder, COLLISION_GROUP_NONE ); UTIL_TraceLine( vecStart, vecBPOrigin, MASK_SOLID, &ignorePlayersFilter, &trace ); if ( trace.m_pEnt != pEntity && trace.fraction != 1.0 ) continue; } float flDist = (vecBPOrigin - pBuilder->GetAbsOrigin()).Length(); // if this is closer, or is the first one in our view, check it out if ( bIgnoreChecks || ( flDist < MIN(flNearestPoint, pBPInterface->GetMaxSnapDistance( i )) ) ) { flNearestPoint = flDist; vecNearestBuildPoint = vecBPOrigin; m_hBuiltOnEntity = pEntity; m_iBuiltOnPoint = i; // Set our angles to the buildpoint's angles SetAbsAngles( vecBPAngles ); bFoundPoint = true; } } } } return bFoundPoint; } //----------------------------------------------------------------------------- // Purpose: Find a buildpoint on the specified player //----------------------------------------------------------------------------- bool CBaseObject::FindBuildPointOnPlayer( CTFPlayer *pTFPlayer, CBasePlayer *pBuilder, float &flNearestPoint, Vector &vecNearestBuildPoint ) { bool bFoundPoint = false; if ( !pTFPlayer ) return false; if ( pTFPlayer->m_Shared.InCond( TF_COND_SAPPED ) ) return false; if ( pTFPlayer->m_Shared.IsInvulnerable() ) return false; if ( pTFPlayer->m_Shared.InCond( TF_COND_PHASE ) ) return false; Vector vecOrigin = pTFPlayer->GetAbsOrigin(); QAngle vecAngles = pTFPlayer->GetAbsAngles(); float flDist = ( vecOrigin - pBuilder->GetAbsOrigin() ).Length(); if ( flDist <= 160.f ) { flNearestPoint = flDist; vecNearestBuildPoint = vecOrigin; m_hBuiltOnEntity = (CBaseEntity *)pTFPlayer; // Set our angles to the buildpoint's angles SetAbsAngles( vecAngles ); bFoundPoint = true; } return bFoundPoint; } /* class CTraceFilterIgnorePlayers : public CTraceFilterSimple { public: // It does have a base, but we'll never network anything below here.. DECLARE_CLASS( CTraceFilterIgnorePlayers, CTraceFilterSimple ); CTraceFilterIgnorePlayers( const IHandleEntity *passentity, int collisionGroup ) : CTraceFilterSimple( passentity, collisionGroup ) { } virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask ) { CBaseEntity *pEntity = EntityFromEntityHandle( pServerEntity ); if ( pEntity->IsPlayer() ) { return false; } return true; } }; //----------------------------------------------------------------------------- // Purpose: Test around this build position to make sure it does not block a path //----------------------------------------------------------------------------- bool CBaseObject::TestPositionForPlayerBlock( Vector vecBuildOrigin, CBasePlayer *pPlayer ) { // find out the status of the 8 regions around this position int i; bool bNodeVisited[8]; bool bNodeClear[8]; // The first zone that is clear of obstructions int iFirstClear = -1; Vector vHalfPlayerDims = (VEC_HULL_MAX - VEC_HULL_MIN) * 0.5f; Vector vBuildDims = m_vecBuildMaxs - m_vecBuildMins; Vector vHalfBuildDims = vBuildDims * 0.5; // the locations of the 8 test positions // boxes are adjacent to the object box and are at least as large as // a player to ensure that a player can pass this location // 0 1 2 // 7 X 3 // 6 5 4 static int iPositions[8][2] = { { -1, -1 }, { 0, -1 }, { 1, -1 }, { 1, 0 }, { 1, 1 }, { 0, 1 }, { -1, 1 }, { -1, 0 } }; CTraceFilterIgnorePlayers traceFilter( this, COLLISION_GROUP_NONE ); for ( i=0;i<8;i++ ) { // mark them all as unvisited bNodeVisited[i] = false; Vector vecTest = vecBuildOrigin; vecTest.x += ( iPositions[i][0] * ( vHalfBuildDims.x + vHalfPlayerDims.x ) ); vecTest.y += ( iPositions[i][1] * ( vHalfBuildDims.y + vHalfPlayerDims.y ) ); trace_t trace; UTIL_TraceHull( vecTest, vecTest, VEC_HULL_MIN, VEC_HULL_MAX, MASK_SOLID_BRUSHONLY, &traceFilter, &trace ); bNodeClear[i] = ( trace.fraction == 1 && trace.allsolid != 1 && (trace.startsolid != 1) ); // NDebugOverlay::Box( vecTest, VEC_HULL_MIN, VEC_HULL_MAX, bNodeClear[i] ? 0 : 255, bNodeClear[i] ? 255 : 0, 0, 20, 0.1 ); // Store off the first clear location if ( iFirstClear < 0 && bNodeClear[i] ) { iFirstClear = i; } } if ( iFirstClear < 0 ) { // no clear space return false; } // visit all nodes that are adjacent RecursiveTestBuildSpace( iFirstClear, bNodeClear, bNodeVisited ); // if we still have unvisited nodes, return false // unvisited nodes means that one or more nodes was unreachable from our start position // ie, two places the player might want to traverse but would not be able to if we built here for ( i=0;i<8;i++ ) { if ( bNodeVisited[i] == false && bNodeClear[i] == true ) { return false; } } return true; } //----------------------------------------------------------------------------- // Purpose: Test around the build position, one quadrant at a time //----------------------------------------------------------------------------- void CBaseObject::RecursiveTestBuildSpace( int iNode, bool *bNodeClear, bool *bNodeVisited ) { // if the node is visited already if ( bNodeVisited[iNode] == true ) return; // if the test node is blocked if ( bNodeClear[iNode] == false ) return; bNodeVisited[iNode] = true; int iLeftNode = iNode - 1; if ( iLeftNode < 0 ) iLeftNode = 7; RecursiveTestBuildSpace( iLeftNode, bNodeClear, bNodeVisited ); int iRightNode = ( iNode + 1 ) % 8; RecursiveTestBuildSpace( iRightNode, bNodeClear, bNodeVisited ); } */ //----------------------------------------------------------------------------- // Purpose: Move the placement model to the current position. Return false if it's an invalid position //----------------------------------------------------------------------------- bool CBaseObject::UpdatePlacement( void ) { if ( MustBeBuiltOnAttachmentPoint() ) { return UpdateAttachmentPlacement(); } // Finds bsp-valid place for building to be built // Checks for validity, nearby to other entities, in line of sight m_bPlacementOK = IsPlacementPosValid(); Teleport( &m_vecBuildOrigin, &GetLocalAngles(), NULL ); return m_bPlacementOK; } //----------------------------------------------------------------------------- // Purpose: See if we should be snapping to a build position //----------------------------------------------------------------------------- bool CBaseObject::FindSnapToBuildPos( CBaseObject *pObjectOverride ) { if ( !MustBeBuiltOnAttachmentPoint() ) return false; CTFPlayer *pPlayer = GetOwner(); if ( !pPlayer ) { return false; } bool bSnappedToPoint = false; bool bShouldAttachToParent = false; Vector vecNearestBuildPoint = vec3_origin; // See if there are any nearby build positions to snap to float flNearestPoint = 9999; int i; bool bHostileAttachment = IsHostileUpgrade(); int iMyTeam = GetTeamNumber(); if ( !pObjectOverride ) { int nTeamCount = TFTeamMgr()->GetTeamCount(); for ( int iTeam = FIRST_GAME_TEAM; iTeam < nTeamCount; ++iTeam ) { // Hostile attachments look for enemy objects only if ( bHostileAttachment ) { if ( iTeam == iMyTeam ) { continue; } } // Friendly attachments look for friendly objects only else if ( iTeam != iMyTeam ) { continue; } CTFTeam *pTeam = ( CTFTeam * )GetGlobalTeam( iTeam ); if ( !pTeam ) continue; // See if we're allowed to build on Robots if ( TFGameRules() && TFGameRules()->GameModeUsesMiniBosses() && GetType() == OBJ_ATTACHMENT_SAPPER && !pPlayer->IsBot() ) { CUtlVector< CTFPlayer * > playerVector; CollectPlayers( &playerVector, pPlayer->GetOpposingTFTeam()->GetTeamNumber(), COLLECT_ONLY_LIVING_PLAYERS ); FOR_EACH_VEC( playerVector, i ) { if ( !playerVector[i]->IsBot() ) continue; if ( FindBuildPointOnPlayer( playerVector[i], pPlayer, flNearestPoint, vecNearestBuildPoint ) ) { bSnappedToPoint = true; bShouldAttachToParent = true; } } } // look for nearby buildpoints on other objects for ( i = 0; i < pTeam->GetNumObjects(); i++ ) { CBaseObject *pObject = pTeam->GetObject(i); Assert( pObject ); if ( pObject && !pObject->IsPlacing() ) { if ( FindNearestBuildPoint( pObject, pPlayer, flNearestPoint, vecNearestBuildPoint ) ) { bSnappedToPoint = true; bShouldAttachToParent = true; } } } } } else { if ( !pObjectOverride->IsPlacing() ) { if ( FindNearestBuildPoint( pObjectOverride, pPlayer, flNearestPoint, vecNearestBuildPoint, true ) ) { bSnappedToPoint = true; bShouldAttachToParent = true; } } } if ( !bSnappedToPoint ) { AddEffects( EF_NODRAW ); } else { RemoveEffects( EF_NODRAW ); if ( bShouldAttachToParent ) { AttachObjectToObject( m_hBuiltOnEntity.Get(), m_iBuiltOnPoint, vecNearestBuildPoint ); } m_vecBuildOrigin = vecNearestBuildPoint; } return bSnappedToPoint; } //----------------------------------------------------------------------------- // Purpose: Are we currently in a buildable position //----------------------------------------------------------------------------- bool CBaseObject::IsValidPlacement( void ) const { return m_bPlacementOK; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- const char *CBaseObject::GetResponseRulesModifier( void ) { switch ( GetType() ) { case OBJ_DISPENSER: return "objtype:dispenser"; break; case OBJ_TELEPORTER: return "objtype:teleporter"; break; case OBJ_SENTRYGUN: return "objtype:sentrygun"; break; case OBJ_ATTACHMENT_SAPPER: return "objtype:sapper"; break; default: break; } return NULL; } //----------------------------------------------------------------------------- // Purpose: Start building the object //----------------------------------------------------------------------------- bool CBaseObject::StartBuilding( CBaseEntity *pBuilder ) { // Need to add the object to the team now... CTFTeam *pTFTeam = ( CTFTeam * )GetGlobalTeam( GetTeamNumber() ); // Deduct the cost from the player if ( pBuilder && pBuilder->IsPlayer() ) { /* if ( ((CTFPlayer*)pBuilder)->IsPlayerClass( TF_CLASS_ENGINEER ) ) { ((CTFPlayer*)pBuilder)->HintMessage( HINT_ENGINEER_USE_WRENCH_ONOWN ); } */ if ( !IsCarried() ) { if ( !ShouldQuickBuild() ) { int iAmountPlayerPaidForMe = ((CTFPlayer*)pBuilder)->StartedBuildingObject( m_iObjectType ); if ( !iAmountPlayerPaidForMe ) { // Player couldn't afford to pay for me, so abort ClientPrint( (CBasePlayer*)pBuilder, HUD_PRINTCENTER, "Not enough resources.\n" ); StopPlacement(); return false; } } ((CTFPlayer*)pBuilder)->SpeakConceptIfAllowed( MP_CONCEPT_BUILDING_OBJECT, GetResponseRulesModifier() ); } else { m_bCarried = false; m_bCarryDeploy = true; m_flCarryDeployTime = gpGlobals->curtime; SetActivity( ACT_OBJ_ASSEMBLING ); ((CTFPlayer*)pBuilder)->m_flCommentOnCarrying = 0.f; ((CTFPlayer*)pBuilder)->SpeakConceptIfAllowed( MP_CONCEPT_REDEPLOY_BUILDING, GetResponseRulesModifier() ); } } // Check to see if we need to add this to a hierarchy. We can just do a simple ray trace from the center as // the placement code has guarenteed we are in a valid position. trace_t trace; UTIL_TraceHull( GetAbsOrigin() + Vector( 0.0f, 0.0f, 2.0f ), GetAbsOrigin() - Vector( 0.0f, 0.0f, 2.0f ), vec3_origin, vec3_origin, MASK_PLAYERSOLID_BRUSHONLY, this, COLLISION_GROUP_PLAYER_MOVEMENT, &trace ); if ( trace.m_pEnt && trace.m_pEnt->IsBSPModel() ) { CFuncTrackTrain *pTrain = dynamic_cast( trace.m_pEnt ); if ( pTrain ) { SetParent( pTrain ); } } // Add this object to the team's list (because we couldn't add it during // placement mode) if ( pTFTeam && !pTFTeam->IsObjectOnTeam( this ) ) { pTFTeam->AddObject( this ); } m_bPlacing = false; m_bBuilding = true; if ( m_bCarryDeploy ) { SetHealth( m_iHealthOnPickup ); IGameEvent * event = gameeventmanager->CreateEvent( "player_dropobject" ); if ( event ) { CTFPlayer *pTFPlayer = ToTFPlayer( pBuilder ); event->SetInt( "userid", pTFPlayer ? pTFPlayer->GetUserID() : 0 ); event->SetInt( "object", GetType() ); event->SetInt( "index", entindex() ); // object entity index gameeventmanager->FireEvent( event, true ); // don't send to clients } } else if ( IsMiniBuilding() ) { int iHealth = GetMaxHealthForCurrentLevel(); if ( !IsDisposableBuilding() ) { iHealth /= 2.0f; } SetHealth( iHealth ); } else { SetHealth( OBJECT_CONSTRUCTION_STARTINGHEALTH ); } m_flPercentageConstructed = 0; m_nRenderMode = kRenderNormal; RemoveSolidFlags( FSOLID_NOT_SOLID ); // NOTE: We must spawn the control panels now, instead of during // Spawn, because until placement is started, we don't actually know // the position of the control panel because we don't know what it's // been attached to (could be a vehicle which supplies a different // place for the control panel) // NOTE: We must also spawn it before FinishedBuilding can be called if ( !(m_fObjectFlags & OF_DOESNT_HAVE_A_MODEL) ) { SpawnControlPanels(); } // Tell the object we've been built on that we exist if ( IsBuiltOnAttachment() && !m_hBuiltOnEntity->IsPlayer() ) { IHasBuildPoints *pBPInterface = dynamic_cast((CBaseEntity*)m_hBuiltOnEntity.Get()); Assert( pBPInterface ); pBPInterface->SetObjectOnBuildPoint( m_iBuiltOnPoint, this ); } // Start the build animations m_flTotalConstructionTime = m_flConstructionTimeLeft = GetTotalTime(); m_flConstructionStartTime = gpGlobals->curtime; if ( pBuilder && pBuilder->IsPlayer() ) { CTFPlayer *pTFBuilder = ToTFPlayer( pBuilder ); pTFBuilder->FinishedObject( this ); IGameEvent * event = gameeventmanager->CreateEvent( "player_builtobject" ); if ( event ) { event->SetInt( "userid", pTFBuilder->GetUserID() ); event->SetInt( "object", ObjectType() ); event->SetInt( "index", entindex() ); // object entity index gameeventmanager->FireEvent( event, true ); // don't send to clients } } m_vecBuildOrigin = GetAbsOrigin(); int contents = UTIL_PointContents( m_vecBuildOrigin ); if ( contents & MASK_WATER ) { SetWaterLevel( 3 ); } // instantly play the build anim DetermineAnimation(); if ( IsMiniBuilding() && ( GetType() != OBJ_DISPENSER ) ) { // Set the skin after placement mode. m_nSkin = ( GetTeamNumber() == TF_TEAM_RED ) ? 2 : 3; } if ( ShouldQuickBuild() ) { DoQuickBuild(); } return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CBaseObject::ShouldBeMiniBuilding( CTFPlayer* pPlayer ) { if ( !pPlayer ) return false; CTFWrench* pWrench = dynamic_cast( pPlayer->Weapon_OwnsThisID( TF_WEAPON_WRENCH ) ); if ( !pWrench ) return false; if ( TFGameRules()->GameModeUsesUpgrades() ) { if ( pPlayer->GetNumObjects( OBJ_SENTRYGUN ) && pPlayer->CanBuild( OBJ_SENTRYGUN ) == CB_CAN_BUILD && !IsCarried() ) return true; } if ( !pWrench->IsPDQ() ) return false; return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::MakeMiniBuilding( CTFPlayer* pPlayer ) { if ( !ShouldBeMiniBuilding( pPlayer ) || IsMiniBuilding() ) return; m_bMiniBuilding = true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::MakeDisposableBuilding( CTFPlayer *pPlayer ) { m_bDisposableBuilding = true; } //----------------------------------------------------------------------------- // Purpose: Continue construction of this object //----------------------------------------------------------------------------- void CBaseObject::BuildingThink( void ) { // Continue construction Construct( (GetMaxHealth() - OBJECT_CONSTRUCTION_STARTINGHEALTH) / m_flTotalConstructionTime * OBJECT_CONSTRUCTION_INTERVAL ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::SetControlPanelsActive( bool bState ) { // Activate control panel screens for ( int i = m_hScreens.Count(); --i >= 0; ) { if (m_hScreens[i].Get()) { m_hScreens[i]->SetActive( bState ); } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::FinishedBuilding( void ) { SetControlPanelsActive( true ); // Only make a shadow if the object doesn't use vphysics if (!VPhysicsGetObject()) { VPhysicsInitStatic(); } m_bBuilding = false; OnGoActive(); // We're done building, add in the stat... ////TFStats()->IncrementStat( (TFStatId_t)(TF_STAT_FIRST_OBJECT_BUILT + ObjectType()), 1 ); // Spawn any objects on this one SpawnObjectPoints(); if ( IsUsingReverseBuild() ) { // if we don't have a sapper (but we should!) then set ourselves as the damager CObjectSapper *pSapper = GetSapper(); CBaseEntity *pDamager = pSapper ? pSapper : this; int iCustomDamageType = pSapper ? TF_DMG_CUSTOM_SAPPER_RECORDER_DEATH : 0; CTakeDamageInfo info; info.SetInflictor( pDamager ); info.SetAttacker( pDamager ); info.SetDamageForce( vec3_origin ); info.SetDamagePosition( GetAbsOrigin() ); info.SetDamage( 0 ); info.SetDamageType( DMG_CRUSH ); info.SetDamageCustom( iCustomDamageType ); Killed( info ); } } //----------------------------------------------------------------------------- // Purpose: Objects store health in hacky ways //----------------------------------------------------------------------------- void CBaseObject::SetHealth( float flHealth ) { if ( m_bCarryDeploy && (flHealth>m_iHealthOnPickup) ) { // If we are re-deploying after being carried we shouldn't gain more health than we had // on pickup until the deploy process is finished. flHealth = m_iHealthOnPickup; } bool changed = m_flHealth != flHealth; m_flHealth = flHealth; m_iHealth = ceil(m_flHealth); /* // If we a pose parameter, set the pose parameter to reflect our health if ( LookupPoseParameter( "object_health") >= 0 && GetMaxHealth() > 0 ) { SetPoseParameter( "object_health", 100 * ( GetHealth() / (float)GetMaxHealth() ) ); } */ if ( changed ) { // Set value and fire output m_OnObjectHealthChanged.Set( m_flHealth, this, this ); } } //----------------------------------------------------------------------------- // Purpose: Override base traceattack to prevent visible effects from team members shooting me //----------------------------------------------------------------------------- void CBaseObject::TraceAttack( const CTakeDamageInfo &inputInfo, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) { // Prevent team damage here so blood doesn't appear if ( inputInfo.GetAttacker() ) { if ( InSameTeam(inputInfo.GetAttacker()) ) { // Pass Damage to enemy attachments int iNumObjects = GetNumObjectsOnMe(); for ( int iPoint=iNumObjects-1;iPoint >= 0; --iPoint ) { CBaseObject *pObject = GetBuildPointObject( iPoint ); if ( pObject && pObject->IsHostileUpgrade() ) { pObject->TraceAttack(inputInfo, vecDir, ptr, pAccumulator ); } } return; } } SpawnBlood( ptr->endpos, vecDir, BloodColor(), inputInfo.GetDamage() ); AddMultiDamage( inputInfo, this ); } //----------------------------------------------------------------------------- // Purpose: Prevent Team Damage //----------------------------------------------------------------------------- ConVar object_show_damage( "obj_show_damage", "0", 0, "Show all damage taken by objects." ); ConVar object_capture_damage( "obj_capture_damage", "0", 0, "Captures all damage taken by objects for dumping later." ); CUtlDict g_DamageMap; void Cmd_DamageDump_f(void) { CUtlDict g_UniqueColumns; int idx; // Build the unique columns: for( idx = g_DamageMap.First(); idx != g_DamageMap.InvalidIndex(); idx = g_DamageMap.Next(idx) ) { char* szColumnName = strchr(g_DamageMap.GetElementName(idx),',') + 1; int ColumnIdx = g_UniqueColumns.Find( szColumnName ); if( ColumnIdx == g_UniqueColumns.InvalidIndex() ) { g_UniqueColumns.Insert( szColumnName, false ); } } // Dump the column names: FileHandle_t f = filesystem->Open("damage.txt","wt+"); for( idx = g_UniqueColumns.First(); idx != g_UniqueColumns.InvalidIndex(); idx = g_UniqueColumns.Next(idx) ) { filesystem->FPrintf(f,"\t%s",g_UniqueColumns.GetElementName(idx)); } filesystem->FPrintf(f,"\n"); CUtlDict g_CompletedRows; // Dump each row: bool bDidRow; do { bDidRow = false; for( idx = g_DamageMap.First(); idx != g_DamageMap.InvalidIndex(); idx = g_DamageMap.Next(idx) ) { char szRowName[256]; // Check the Row name of each entry to see if I've done this row yet. Q_strncpy(szRowName, g_DamageMap.GetElementName(idx), sizeof( szRowName ) ); *strchr(szRowName,',') = '\0'; char szRowNameComma[256]; Q_snprintf( szRowNameComma, sizeof( szRowNameComma ), "%s,", szRowName ); if( g_CompletedRows.Find(szRowName) == g_CompletedRows.InvalidIndex() ) { bDidRow = true; g_CompletedRows.Insert(szRowName,false); // Output the row name: filesystem->FPrintf(f,szRowName); for( int ColumnIdx = g_UniqueColumns.First(); ColumnIdx != g_UniqueColumns.InvalidIndex(); ColumnIdx = g_UniqueColumns.Next( ColumnIdx ) ) { char szRowNameCommaColumn[256]; Q_strncpy( szRowNameCommaColumn, szRowNameComma, sizeof( szRowNameCommaColumn ) ); Q_strncat( szRowNameCommaColumn, g_UniqueColumns.GetElementName( ColumnIdx ), sizeof( szRowNameCommaColumn ), COPY_ALL_CHARACTERS ); int nDamageAmount = 0; // Fine to reuse idx since we are going to break anyways. for( idx = g_DamageMap.First(); idx != g_DamageMap.InvalidIndex(); idx = g_DamageMap.Next(idx) ) { if( !stricmp( g_DamageMap.GetElementName(idx), szRowNameCommaColumn ) ) { nDamageAmount = g_DamageMap[idx]; break; } } filesystem->FPrintf(f,"\t%i",nDamageAmount); } filesystem->FPrintf(f,"\n"); break; } } // Grab the row name: } while(bDidRow); // close the file: filesystem->Close(f); } static ConCommand obj_dump_damage( "obj_dump_damage", Cmd_DamageDump_f ); //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void ReportDamage( const char* szInflictor, const char* szVictim, float fAmount, int nCurrent, int nMax ) { int iAmount = (int)fAmount; if( object_show_damage.GetBool() && iAmount ) { Msg( "ShowDamage: Object %s taking %0.1f damage from %s ( %i / %i )\n", szVictim, fAmount, szInflictor, nCurrent, nMax ); } if( object_capture_damage.GetBool() ) { char szMangledKey[256]; Q_snprintf(szMangledKey,sizeof(szMangledKey)/sizeof(szMangledKey[0]),"%s,%s",szInflictor,szVictim); int idx = g_DamageMap.Find( szMangledKey ); if( idx == g_DamageMap.InvalidIndex() ) { g_DamageMap.Insert( szMangledKey, iAmount ); } else { g_DamageMap[idx] += iAmount; } } } //----------------------------------------------------------------------------- // Purpose: Return the first non-hostile object build on this object //----------------------------------------------------------------------------- CBaseEntity *CBaseObject::GetFirstFriendlyObjectOnMe( void ) { CBaseObject *pFirstObject = NULL; IHasBuildPoints *pBPInterface = dynamic_cast(this); int iNumObjects = pBPInterface->GetNumObjectsOnMe(); for ( int iPoint=0;iPointIsHostileUpgrade() ) { pFirstObject = pObject; break; } } return pFirstObject; } //----------------------------------------------------------------------------- // Purpose: Pass the specified amount of damage through to any objects I have built on me //----------------------------------------------------------------------------- bool CBaseObject::PassDamageOntoChildren( const CTakeDamageInfo &info, float *flDamageLeftOver ) { float flDamage = info.GetDamage(); // Double the amount of damage done (and get around the child damage modifier) flDamage *= 2; if ( obj_child_damage_factor.GetFloat() ) { flDamage *= (1 / obj_child_damage_factor.GetFloat()); } // Remove blast damage because child objects (well specifically upgrades) // want to ignore direct blast damage but still take damage from parent CTakeDamageInfo childInfo = info; childInfo.SetDamage( flDamage ); childInfo.SetDamageType( info.GetDamageType() & (~DMG_BLAST) ); CBaseEntity *pEntity = GetFirstFriendlyObjectOnMe(); while ( pEntity ) { Assert( pEntity->m_takedamage != DAMAGE_NO ); // Do damage to the next object float flDamageTaken = pEntity->OnTakeDamage( childInfo ); // If we didn't kill it, abort CBaseObject *pObject = dynamic_cast(pEntity); if ( !pObject || !pObject->IsDying() ) { const char* szInflictor = "unknown"; if( info.GetInflictor() ) szInflictor = (char*)info.GetInflictor()->GetClassname(); ReportDamage( szInflictor, GetClassname(), flDamageTaken, GetHealth(), GetMaxHealth() ); *flDamageLeftOver = flDamage; return true; } // Reduce the damage and move on to the next flDamage -= flDamageTaken; pEntity = GetFirstFriendlyObjectOnMe(); } *flDamageLeftOver = flDamage; return false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CBaseObject::OnTakeDamage( const CTakeDamageInfo &info ) { if ( !IsAlive() ) return info.GetDamage(); if ( m_takedamage == DAMAGE_NO ) return 0; if ( IsPlacing() ) return 0; // Check teams if ( info.GetAttacker() ) { if ( InSameTeam(info.GetAttacker()) ) return 0; if ( TFGameRules() && TFGameRules()->IsTruceActive() ) { // players cannot damage buildings while a truce is active if ( info.GetAttacker()->IsPlayer() && info.GetAttacker()->IsTruceValidForEnt() && ( ( info.GetAttacker()->GetTeamNumber() == TF_TEAM_RED ) || ( info.GetAttacker()->GetTeamNumber() == TF_TEAM_BLUE ) ) ) return 0; } } m_AchievementData.AddDamagerToHistory( info.GetAttacker() ); if ( info.GetAttacker()->IsPlayer() ) { ToTFPlayer( info.GetAttacker() )->m_AchievementData.AddTargetToHistory( this ); } IHasBuildPoints *pBPInterface = dynamic_cast(this); float flDamage = info.GetDamage(); // Buildings are resistant to plasma damage. if ( info.GetDamageCustom() == TF_DMG_CUSTOM_PLASMA ) { flDamage *= 0.2f; } // Charged plasma damage disables buildings for a short time. if ( info.GetDamageCustom() == TF_DMG_CUSTOM_PLASMA_CHARGED ) { flDamage *= 0.2f; m_flPlasmaDisableTime = gpGlobals->curtime + PLASMA_DISABLE_TIME; m_bPlasmaDisable = true; UpdateDisabledState(); } // Objects build on other objects take less damage if ( !IsAnUpgrade() && GetParentObject() ) { flDamage *= obj_child_damage_factor.GetFloat(); } if (obj_damage_factor.GetFloat()) { flDamage *= obj_damage_factor.GetFloat(); } CTFWeaponBase *pWeapon = dynamic_cast(info.GetWeapon()); if ( pWeapon ) { #ifdef STAGING_ONLY // Attacker has building disabling properties float flDisablingAttack = 0.0f; CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pWeapon, flDisablingAttack, disable_buildings_on_hit ); if ( flDisablingAttack ) { // do not override if existing time is longer m_flPlasmaDisableTime = Max( gpGlobals->curtime + flDisablingAttack, m_flPlasmaDisableTime ); m_bPlasmaDisable = true; UpdateDisabledState(); } #endif // STAGING_ONLY // Apply attributes that increase damage vs buildings CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pWeapon, flDamage, mult_dmg_vs_buildings ); CTFPlayer *pAttacker = ToTFPlayer( info.GetAttacker() ); if ( pAttacker ) { pWeapon->ApplyOnHitAttributes( NULL, pAttacker, info ); } } if ( TFGameRules()->IsPowerupMode() ) { CTFPlayer *pAttacker = ToTFPlayer( info.GetAttacker() ); if ( pAttacker ) { if ( pAttacker->m_Shared.GetCarryingRuneType() == RUNE_STRENGTH || pAttacker->m_Shared.InCond( TF_COND_RUNE_IMBALANCE ) ) { flDamage *= 2.f; } if ( pAttacker->m_Shared.GetCarryingRuneType() == RUNE_KNOCKOUT ) { flDamage *= 4.f; } if ( pAttacker->m_Shared.GetCarryingRuneType() == RUNE_VAMPIRE ) { int iModHealthOnHit = flDamage; if ( iModHealthOnHit > 0 ) { pAttacker->TakeHealth( iModHealthOnHit, DMG_GENERIC ); } } } } bool bFriendlyObjectsAttached = false; int iNumObjects = pBPInterface->GetNumObjectsOnMe(); for ( int iPoint=0;iPointIsHostileUpgrade() ) { continue; } bFriendlyObjectsAttached = true; break; } // Don't look, Tom Bui! static struct { bool operator()( const float flHealth, const float flDamage ) const { return ( ( flHealth - flDamage ) < 1 ); } } IsDamageFatal; // Only track actual damage - not overkill m_AchievementData.AddDamageEventToHistory( info.GetAttacker(), ( IsDamageFatal( m_flHealth, flDamage ) ) ? m_flHealth : flDamage ); // if we cannot die if ( m_bCannotDie && IsDamageFatal( m_flHealth, flDamage ) ) { flDamage = m_flHealth - 1; } // If I have objects on me, I can't be destroyed until they're gone. Ditto if I can't be killed. bool bWillDieButCant = ( bFriendlyObjectsAttached ) && IsDamageFatal( m_flHealth, flDamage ); if ( bWillDieButCant ) { // Soak up the damage it would take to drop us to 1 health flDamage = flDamage - m_flHealth; SetHealth( 1 ); // Pass leftover damage if ( flDamage ) { if ( PassDamageOntoChildren( info, &flDamage ) ) return flDamage; } } if ( flDamage ) { m_iLifetimeDamage += floor( MIN( flDamage, m_flHealth ) ); if ( m_iLifetimeDamage > tf_obj_damage_tank_achievement_amount.GetInt() && GetBuilder() ) { GetBuilder()->AwardAchievement( ACHIEVEMENT_TF_ENGINEER_TANK_DAMAGE ); } // Recheck our death possibility, because our objects may have all been blown off us by now bWillDieButCant = ( bFriendlyObjectsAttached ) && IsDamageFatal( m_flHealth, flDamage ); if ( !bWillDieButCant ) { // Reduce health SetHealth( m_flHealth - flDamage ); } } m_OnDamaged.FireOutput(info.GetAttacker(), this); if ( GetHealth() <= 0 ) { if ( info.GetAttacker() ) { //TFStats()->IncrementTeamStat( info.GetAttacker()->GetTeamNumber(), TF_TEAM_STAT_DESTROYED_OBJECT_COUNT, 1 ); //TFStats()->IncrementPlayerStat( info.GetAttacker(), TF_PLAYER_STAT_DESTROYED_OBJECT_COUNT, 1 ); } m_lifeState = LIFE_DEAD; m_OnDestroyed.FireOutput( info.GetAttacker(), this); Killed( info ); // Tell our builder to speak about it if ( m_hBuilder ) { m_hBuilder->SpeakConceptIfAllowed( MP_CONCEPT_LOST_OBJECT, GetResponseRulesModifier() ); } } const char* szInflictor = "unknown"; if( info.GetInflictor() ) szInflictor = (char*)info.GetInflictor()->GetClassname(); ReportDamage( szInflictor, GetClassname(), flDamage, GetHealth(), GetMaxHealth() ); IGameEvent *event = gameeventmanager->CreateEvent( "npc_hurt" ); if ( event ) { event->SetInt( "entindex", entindex() ); event->SetInt( "health", Max( 0, (int)GetHealth() ) ); event->SetInt( "damageamount", flDamage ); event->SetBool( "crit", ( info.GetDamageType() & DMG_CRITICAL ) ? true : false ); CTFPlayer *pTFAttacker = ToTFPlayer( info.GetAttacker() ); if ( pTFAttacker ) { event->SetInt( "attacker_player", pTFAttacker->GetUserID() ); if ( pTFAttacker->GetActiveTFWeapon() ) { event->SetInt( "weaponid", pTFAttacker->GetActiveTFWeapon()->GetWeaponID() ); } else { event->SetInt( "weaponid", 0 ); } } else { // hurt by world event->SetInt( "attacker_player", 0 ); event->SetInt( "weaponid", 0 ); } gameeventmanager->FireEvent( event ); } return flDamage; } //----------------------------------------------------------------------------- // Purpose: Repair / Help-Construct this object the specified amount //----------------------------------------------------------------------------- bool CBaseObject::Construct( float flHealth ) { // Multiply it by the repair rate flHealth *= GetConstructionMultiplier(); if ( !flHealth ) return false; if ( IsBuilding() ) { // Reduce the construction time by the correct amount for the health passed in float flConstructionTime = flHealth / ((GetMaxHealth() - OBJECT_CONSTRUCTION_STARTINGHEALTH) / m_flTotalConstructionTime); if ( flConstructionTime < 0.0f ) { flConstructionTime *= -1.0f; } m_flConstructionTimeLeft = MAX( 0, m_flConstructionTimeLeft - flConstructionTime); m_flConstructionTimeLeft = clamp( m_flConstructionTimeLeft, 0.0f, m_flTotalConstructionTime ); m_flPercentageConstructed = m_flConstructionTimeLeft / m_flTotalConstructionTime; if ( flHealth >= 0.0f ) { // Only do this if we're not reversing construction m_flPercentageConstructed = 1.0f - m_flPercentageConstructed; } m_flPercentageConstructed = clamp( (float) m_flPercentageConstructed, 0.0f, 1.0f ); // Increase health (unless it's a mini-building, which start at max health) // Minibuildings build health at a reduced rate // Staging_engy { SetHealth( MIN( GetMaxHealth(), m_flHealth + (IsMiniBuilding() ? (flHealth * 0.5f) : flHealth) ) ); } // Return true if we're constructed now if ( m_flConstructionTimeLeft <= 0.0f ) { FinishedBuilding(); return true; } } else { // Return true if we're already fully healed if ( GetHealth() >= GetMaxHealth() ) return true; // Increase health. SetHealth( MIN( GetMaxHealth(), MAX( 1, m_flHealth + flHealth ) ) ); m_OnRepaired.FireOutput( this, this); // Return true if we're fully healed now if ( GetHealth() == GetMaxHealth() ) return true; } return false; } //---------------------------------------------------------------------------------------------------------------------------------------- void CBaseObject::OnConstructionHit( CTFPlayer *pPlayer, CTFWrench *pWrench, Vector hitLoc ) { // Get the player index int iPlayerIndex = pPlayer->entindex(); // The time the repair is going to expire float flRepairExpireTime = gpGlobals->curtime + 1.0; // Update or Add the expire time to the list int index = m_ConstructorList.Find( iPlayerIndex ); if ( index == m_ConstructorList.InvalidIndex() ) { index = m_ConstructorList.Insert( iPlayerIndex ); m_ConstructorList[index].flValue = pWrench->GetConstructionValue(); } m_ConstructorList[index].flHitTime = flRepairExpireTime; // Play a construction hit effect. CPVSFilter filter( hitLoc ); TE_TFParticleEffect( filter, 0.0f, "nutsnbolts_build", hitLoc, QAngle(0,0,0) ); } //---------------------------------------------------------------------------------------------------------------------------------------- float CBaseObject::GetConstructionMultiplier( void ) { if ( IsUsingReverseBuild() ) return -1.0f; float flMultiplier = 1.0; // expire all the old int i = m_ConstructorList.LastInorder(); while ( i != m_ConstructorList.InvalidIndex() ) { int iThis = i; i = m_ConstructorList.PrevInorder( i ); if ( m_ConstructorList[iThis].flHitTime < gpGlobals->curtime ) { m_ConstructorList.RemoveAt( iThis ); } else { // STAGING_ENGY // each Player adds a fixed amount of speed boost // Carry deploy hits add more flMultiplier += ( m_ConstructorList[iThis].flValue ); } } // See if we have any attributes that want to modify our build rate CTFPlayer* pBuilder = GetOwner(); if( pBuilder ) { flMultiplier += pBuilder->GetObjectBuildSpeedMultiplier( ObjectType(), m_bCarryDeploy ); } return flMultiplier; } //----------------------------------------------------------------------------- // Purpose: Object is exploding because it was killed or detonate //----------------------------------------------------------------------------- void CBaseObject::Explode( void ) { const char *pExplodeSound = GetObjectInfo( ObjectType() )->m_pExplodeSound; if ( pExplodeSound && Q_strlen(pExplodeSound) > 0 ) { EmitSound( pExplodeSound ); } const char *pExplodeEffect = GetObjectInfo( ObjectType() )->m_pExplosionParticleEffect; if ( pExplodeEffect && pExplodeEffect[0] != '\0' ) { // Send to everyone - we're inside prediction for the engy who hit this off, but we // don't predict that the hit will kill this object. CDisablePredictionFiltering disabler; Vector origin = GetAbsOrigin(); QAngle up(-90,0,0); CPVSFilter filter( origin ); TE_TFParticleEffect( filter, 0.0f, pExplodeEffect, origin, up ); } // create some delicious, metal filled gibs CreateObjectGibs(); } void CBaseObject::CreateObjectGibs( void ) { if ( m_aGibs.Count() <= 0 ) { return; } const CObjectInfo *pObjectInfo = GetObjectInfo( ObjectType() ); // grant some percentage of the cost to build if number of metal to drop is not specified const float flMetalCostPercentage = 0.5f; const int nTotalMetal = pObjectInfo->m_iMetalToDropInGibs == 0 ? pObjectInfo->m_Cost * flMetalCostPercentage : pObjectInfo->m_iMetalToDropInGibs; int nMetalPerGib = nTotalMetal / m_aGibs.Count(); int nLeftOver = nTotalMetal % m_aGibs.Count(); if ( IsMiniBuilding() ) { // STAGING_ENGY nMetalPerGib = 0; nLeftOver = 0; } int i; for ( i=0; iActivateWhenAtRest(); // Fill up the ammo pack. pAmmoPack->GiveAmmo( nMetal, TF_AMMO_METAL ); // Calculate the initial impulse on the weapon. Vector vecImpulse( random->RandomFloat( -0.5, 0.5 ), random->RandomFloat( -0.5, 0.5 ), random->RandomFloat( 0.75, 1.25 ) ); VectorNormalize( vecImpulse ); vecImpulse *= random->RandomFloat( tf_obj_gib_velocity_min.GetFloat(), tf_obj_gib_velocity_max.GetFloat() ); // Cap the impulse. float flSpeed = vecImpulse.Length(); if ( flSpeed > tf_obj_gib_maxspeed.GetFloat() ) { VectorScale( vecImpulse, tf_obj_gib_maxspeed.GetFloat() / flSpeed, vecImpulse ); } if ( pAmmoPack->VPhysicsGetObject() ) { // We can probably remove this when the mass on the weapons is correct! //pAmmoPack->VPhysicsGetObject()->SetMass( 25.0f ); AngularImpulse angImpulse( 0, random->RandomFloat( 0, 100 ), 0 ); pAmmoPack->VPhysicsGetObject()->SetVelocityInstantaneous( &vecImpulse, &angImpulse ); } pAmmoPack->SetInitialVelocity( vecImpulse ); pAmmoPack->m_nSkin = ( GetTeamNumber() == TF_TEAM_RED ) ? 0 : 1; // 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->m_bObjGib = true; if ( IsMiniBuilding() ) { pAmmoPack->SetModelScale( 0.6f ); } } return pAmmoPack; } //----------------------------------------------------------------------------- // Purpose: Object has been blown up. Drop resource chunks upto the value of my max health. //----------------------------------------------------------------------------- void CBaseObject::Killed( const CTakeDamageInfo &info ) { m_bDying = true; // Find the killer & the scorer CBaseEntity *pInflictor = info.GetInflictor(); CBaseEntity *pKiller = info.GetAttacker(); CTFPlayer *pScorer = ToTFPlayer( TFGameRules()->GetDeathScorer( pKiller, pInflictor, this ) ); CTFPlayer *pAssister = NULL; // If we are being carried, // if this object has a sapper on it, and was not killed by the sapper (killed by damage other than crush, since sapper does crushing damage), // award an assist to the owner of the sapper since it probably contributed to destroying this object CObjectSapper *pSapper = GetSapper(); if ( pSapper && !( DMG_CRUSH & info.GetDamageType() ) && !m_bPlasmaDisable ) { // give an assist to the sapper's owner pAssister = pSapper->GetOwner(); if ( pAssister ) { CTF_GameStats.Event_AssistDestroyBuilding( pAssister, this ); // Also increment the SapBuildings grind achievement pAssister->AwardAchievement( ACHIEVEMENT_TF_SPY_SAPPER_GRIND ); } } else if ( pScorer ) { // If a player is healing the scorer, give that player credit for the assist CTFPlayer *pHealer = ToTFPlayer( static_cast( pScorer->m_Shared.GetFirstHealer() ) ); // Must be a medic to receive a healing assist, otherwise engineers get credit for assists from dispensers doing healing. // Also don't give an assist for healing if the inflictor was a sentry gun, otherwise medics healing engineers get assists for the engineer's sentry kills. if ( pHealer && ( pHealer->GetPlayerClass()->GetClassIndex() == TF_CLASS_MEDIC ) ) { pAssister = pHealer; } } // Don't do anything if we were detonated or dismantled if ( pScorer && pInflictor != this ) { IGameEvent * event = gameeventmanager->CreateEvent( "object_destroyed" ); // Work out what killed the player, and send a message to all clients about it int iWeaponID; const char *killer_weapon_name = TFGameRules()->GetKillingWeaponName( info, NULL, &iWeaponID ); const char *killer_weapon_log_name = killer_weapon_name; CTFPlayer *pTFPlayer = GetOwner(); CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase * >( pScorer->Weapon_OwnsThisID( iWeaponID ) ); if ( pWeapon ) { CEconItemView *pItem = pWeapon->GetAttributeContainer()->GetItem(); if ( pItem ) { if ( pItem->GetStaticData()->GetIconClassname() ) { killer_weapon_name = pItem->GetStaticData()->GetIconClassname(); } if ( pItem->GetStaticData()->GetLogClassname() ) { killer_weapon_log_name = pItem->GetStaticData()->GetLogClassname(); } } } if ( event ) { if ( pTFPlayer ) { event->SetInt( "userid", pTFPlayer->GetUserID() ); } if ( pAssister && ( pAssister != pScorer ) ) { event->SetInt( "assister", pAssister->GetUserID() ); } event->SetInt( "attacker", pScorer->GetUserID() ); // attacker event->SetString( "weapon", killer_weapon_name ); event->SetString( "weapon_logclassname", killer_weapon_log_name ); event->SetInt( "weaponid", iWeaponID ); event->SetInt( "priority", 6 ); // HLTV event priority, not transmitted event->SetInt( "objecttype", GetType() ); event->SetInt( "index", entindex() ); // object entity index event->SetBool( "was_building", m_bBuilding ); gameeventmanager->FireEvent( event ); } CTF_GameStats.Event_PlayerDestroyedBuilding( pScorer, this ); pScorer->Event_KilledOther(this, info); // Also track stats for strange sappers. if ( pSapper ) { CTFPlayer *pSapperOwner = pSapper->GetOwner(); Assert( pSapperOwner ); if ( pSapperOwner ) { EconEntity_OnOwnerKillEaterEvent( dynamic_cast( pSapperOwner->GetEntityForLoadoutSlot( LOADOUT_POSITION_BUILDING ) ), pSapperOwner, GetOwner(), kKillEaterEvent_BuildingSapped ); } } // Check for Demo achievement: // Kill an Engineer building that you can't see with a direct hit from a Grenade Launcher if ( pScorer && pScorer->IsPlayerClass( TF_CLASS_DEMOMAN) ) { if ( pScorer->GetActiveTFWeapon() && ( pScorer->GetActiveTFWeapon()->GetWeaponID() == TF_WEAPON_GRENADELAUNCHER) ) { if ( pInflictor && pInflictor->IsPlayer() == false ) { CTFGrenadePipebombProjectile *pBaseGrenade = dynamic_cast< CTFGrenadePipebombProjectile* >( pInflictor ); if ( pBaseGrenade && pBaseGrenade->m_bTouched == false ) { if ( pScorer->FVisible( this ) == false ) { pScorer->AwardAchievement( ACHIEVEMENT_TF_DEMOMAN_KILL_BUILDING_DIRECT_HIT ); } } } } } } else { IGameEvent * event = gameeventmanager->CreateEvent( "object_detonated" ); if ( event ) { CTFPlayer *pTFPlayer = GetOwner(); if ( pTFPlayer ) { event->SetInt( "userid", pTFPlayer->GetUserID() ); } event->SetInt( "objecttype", GetType() ); // object type event->SetInt( "index", entindex() ); // object entity index gameeventmanager->FireEvent( event ); } } // Don't create gibs if it reversed back to a toolbox if ( IsUsingReverseBuild() && ( DMG_CRUSH & info.GetDamageType() ) != 0 ) { CTFAmmoPack *pAmmoPack = CreateAmmoPack( "models/weapons/w_models/w_toolbox.mdl", GetObjectInfo( ObjectType() )->m_iMetalToDropInGibs ); if ( pAmmoPack ) { pAmmoPack->SetBodygroup( 1, 1 ); } CObjectSapper *pSapper = GetSapper(); if ( pSapper ) { pSapper->Explode(); } } else { // Do an explosion. Explode(); } // Stats tracking for strange items. EconEntity_OnOwnerKillEaterEvent( dynamic_cast( info.GetWeapon() ), pScorer, GetOwner(), kKillEaterEvent_BuildingDestroyed ); UTIL_Remove( this ); } //----------------------------------------------------------------------------- // Purpose: Indicates this NPC's place in the relationship table. //----------------------------------------------------------------------------- Class_T CBaseObject::Classify( void ) { return CLASS_NONE; } //----------------------------------------------------------------------------- // Purpose: Get the type of this object //----------------------------------------------------------------------------- int CBaseObject::GetType() const { return m_iObjectType; } //----------------------------------------------------------------------------- // Purpose: Get the builder of this object //----------------------------------------------------------------------------- CTFPlayer *CBaseObject::GetBuilder( void ) const { return m_hBuilder; } //----------------------------------------------------------------------------- // Purpose: Return true if the Owning CTeam should clean this object up automatically //----------------------------------------------------------------------------- bool CBaseObject::ShouldAutoRemove( void ) { return true; } //----------------------------------------------------------------------------- // Purpose: // Input : iTeamNum - //----------------------------------------------------------------------------- void CBaseObject::ChangeTeam( int iTeamNum ) { CTFTeam *pTeam = ( CTFTeam * )GetGlobalTeam( iTeamNum ); CTFTeam *pExisting = ( CTFTeam * )GetTeam(); TRACE_OBJECT( UTIL_VarArgs( "%0.2f CBaseObject::ChangeTeam old %s new %s\n", gpGlobals->curtime, pExisting ? pExisting->GetName() : "NULL", pTeam ? pTeam->GetName() : "NULL" ) ); // Already on this team if ( GetTeamNumber() == iTeamNum ) return; if ( pExisting ) { // Remove it from current team ( if it's in one ) and give it to new team pExisting->RemoveObject( this ); } // Change to new team BaseClass::ChangeTeam( iTeamNum ); // Add this object to the team's list // But only if we're not placing it if ( pTeam && (!m_bPlacing) ) { pTeam->AddObject( this ); } // Setup for our new team's model CreateBuildPoints(); } CObjectSapper* CBaseObject::GetSapper( void ) { if ( !HasSapper() ) return NULL; return dynamic_cast< CObjectSapper* >( FirstMoveChild() ); } //----------------------------------------------------------------------------- // Purpose: Return true if I have at least 1 sapper on me //----------------------------------------------------------------------------- bool CBaseObject::HasSapper( void ) { return m_bHasSapper; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CBaseObject::IsPlasmaDisabled( void ) { return m_bPlasmaDisable; } //----------------------------------------------------------------------------- void CBaseObject::OnAddSapper( void ) { // Assume we can only build 1 sapper per object Assert( m_bHasSapper == false ); m_bHasSapper = true; CTFPlayer *pPlayer = GetBuilder(); if ( pPlayer ) { //pPlayer->HintMessage( HINT_OBJECT_YOUR_OBJECT_SAPPED, true ); pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_SPY_SAPPER, GetResponseRulesModifier() ); } UpdateDisabledState(); } //----------------------------------------------------------------------------- void CBaseObject::OnRemoveSapper( void ) { m_bHasSapper = false; UpdateDisabledState(); } //----------------------------------------------------------------------------- int CBaseObject::GetUpgradeMetalRequired() { return GetObjectInfo( GetType() )->m_UpgradeCost; } //----------------------------------------------------------------------------- bool CBaseObject::ShowVGUIScreen( int panelIndex, bool bShow ) { Assert( panelIndex >= 0 && panelIndex < m_hScreens.Count() ); if ( m_hScreens[panelIndex].Get() ) { m_hScreens[panelIndex]->SetActive( bShow ); return true; } else { return false; } } //----------------------------------------------------------------------------- // Purpose: Set the health of the object //----------------------------------------------------------------------------- void CBaseObject::InputSetHealth( inputdata_t &inputdata ) { m_iMaxHealth = inputdata.value.Int(); SetHealth( m_iMaxHealth ); } //----------------------------------------------------------------------------- // Purpose: Add health to the object //----------------------------------------------------------------------------- void CBaseObject::InputAddHealth( inputdata_t &inputdata ) { int iHealth = inputdata.value.Int(); SetHealth( MIN( GetMaxHealth(), m_flHealth + iHealth ) ); } //----------------------------------------------------------------------------- // Purpose: Remove health from the object //----------------------------------------------------------------------------- void CBaseObject::InputRemoveHealth( inputdata_t &inputdata ) { int iDamage = inputdata.value.Int(); SetHealth( m_flHealth - iDamage ); if ( GetHealth() <= 0 ) { m_lifeState = LIFE_DEAD; m_OnDestroyed.FireOutput(this, this); CTakeDamageInfo info( inputdata.pCaller, inputdata.pActivator, vec3_origin, GetAbsOrigin(), iDamage, DMG_GENERIC ); Killed( info ); } } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CBaseObject::InputSetSolidToPlayer( inputdata_t &inputdata ) { int ival = inputdata.value.Int(); ival = clamp( ival, (int)SOLID_TO_PLAYER_USE_DEFAULT, (int)SOLID_TO_PLAYER_NO ); OBJSOLIDTYPE stp = (OBJSOLIDTYPE)ival; SetSolidToPlayers( stp ); } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CBaseObject::InputSetBuilder( inputdata_t &inputdata ) { CTFPlayer *pPlayer = ToTFPlayer( inputdata.pActivator ); if ( GetBuilder() == NULL && pPlayer != NULL ) { SetBuilder( pPlayer ); ChangeTeam( pPlayer->GetTeamNumber() ); pPlayer->AddObject( this ); } } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CBaseObject::InputShow( inputdata_t &inputdata ) { RemoveEffects( EF_NODRAW ); UpdateDisabledState(); } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CBaseObject::InputHide( inputdata_t &inputdata ) { AddEffects( EF_NODRAW ); SetDisabled( true ); } //----------------------------------------------------------------------------- // Purpose: // Input : // Output : did this wrench hit do any work on the object? //----------------------------------------------------------------------------- bool CBaseObject::InputWrenchHit( CTFPlayer *pPlayer, CTFWrench *pWrench, Vector hitLoc ) { Assert( pPlayer ); if ( !pPlayer ) return false; bool bDidWork = false; if ( HasSapper() ) { // do damage to any attached buildings CTakeDamageInfo info( pPlayer, pPlayer, pWrench, WRENCH_DMG_VS_SAPPER, DMG_CLUB, TF_DMG_WRENCH_FIX ); IHasBuildPoints *pBPInterface = dynamic_cast< IHasBuildPoints * >( this ); int iNumObjects = pBPInterface->GetNumObjectsOnMe(); for ( int iPoint=0;iPointIsHostileUpgrade() ) { int iBeforeHealth = pObject->GetHealth(); pObject->TakeDamage( info ); // This should always be true if ( iBeforeHealth != pObject->GetHealth() ) { bDidWork = true; Assert( bDidWork ); } } } } else if ( IsUpgrading() ) { bDidWork = OnWrenchHit( pPlayer, pWrench, hitLoc ); // bDidWork = false; } else if ( IsBuilding() ) { OnConstructionHit( pPlayer, pWrench, hitLoc ); bDidWork = true; } else { // upgrade, refill, repair damage bDidWork = OnWrenchHit( pPlayer, pWrench, hitLoc ); } if ( bDidWork ) { pPlayer->m_AchievementData.AddTargetToHistory( this ); } return bDidWork; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CBaseObject::OnWrenchHit( CTFPlayer *pPlayer, CTFWrench *pWrench, Vector hitLoc ) { bool bRepairHit = false; bool bUpgradeHit = false; bRepairHit = Command_Repair( pPlayer, pWrench->GetRepairValue() ); if ( !bRepairHit ) { bUpgradeHit = CheckUpgradeOnHit( pPlayer ); } DoWrenchHitEffect( hitLoc, bRepairHit, bUpgradeHit ); return bUpgradeHit || bRepairHit; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::DoWrenchHitEffect( Vector hitLoc, bool bRepairHit, bool bUpgradeHit ) { if ( bRepairHit ) { // Play a repair hit effect. CPVSFilter filter( hitLoc ); TE_TFParticleEffect( filter, 0.0f, "nutsnbolts_repair", hitLoc, QAngle(0,0,0) ); } else if ( bUpgradeHit ) { // Play an upgrade hit effect. CPVSFilter filter( hitLoc ); TE_TFParticleEffect( filter, 0.0f, "nutsnbolts_upgrade", hitLoc, QAngle(0,0,0) ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CBaseObject::CheckUpgradeOnHit( CTFPlayer *pPlayer ) { if ( !CanBeUpgraded() ) return false; if ( m_bCarryDeploy ) return false; if ( CanBeUpgraded( pPlayer ) ) { int iPlayerMetal = pPlayer->GetAmmoCount( TF_AMMO_METAL ); int nMaxToAdd = tf_obj_upgrade_per_hit.GetInt(); if ( TFGameRules() && TFGameRules()->IsPowerupMode() ) { nMaxToAdd *= 2; } CALL_ATTRIB_HOOK_INT_ON_OTHER( pPlayer, nMaxToAdd, upgrade_rate_mod ); int iAmountToAdd = MIN( nMaxToAdd, iPlayerMetal ); if ( iAmountToAdd > ( m_iUpgradeMetalRequired - m_iUpgradeMetal ) ) iAmountToAdd = ( m_iUpgradeMetalRequired - m_iUpgradeMetal ); if ( tf_cheapobjects.GetBool() == false && !ShouldQuickBuild() ) { pPlayer->RemoveAmmo( iAmountToAdd, TF_AMMO_METAL ); } // testing quick builds for engineers in Raid mode if ( TFGameRules() && !TFGameRules()->IsPVEModeControlled( pPlayer ) ) { #ifdef TF_RAID_MODE if ( TFGameRules()->IsRaidMode() ) { iAmountToAdd = 200; } #endif if ( TFGameRules()->GameModeUsesUpgrades() && TFGameRules()->IsQuickBuildTime() ) { iAmountToAdd = 200; } } m_iUpgradeMetal += iAmountToAdd; bool bDidWork = false; if ( iAmountToAdd > 0 ) { bDidWork = true; } if ( m_iUpgradeMetal >= m_iUpgradeMetalRequired ) { IGameEvent * event = gameeventmanager->CreateEvent( "player_upgradedobject" ); if ( event ) { event->SetInt( "userid", pPlayer->GetUserID() ); event->SetInt( "object", ObjectType() ); event->SetInt( "index", entindex() ); event->SetBool( "isbuilder", pPlayer == GetBuilder() ); gameeventmanager->FireEvent( event ); } StartUpgrading(); m_iUpgradeMetal = 0; } return bDidWork; } return false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CBaseObject::CanBeUpgraded( CTFPlayer *pPlayer ) { // Already upgrading if ( IsUpgrading() ) return false; if ( IsMiniBuilding() || IsDisposableBuilding() ) return false; // only engineers if ( !ClassCanBuild( pPlayer->GetPlayerClass()->GetClassIndex(), GetType() ) ) return false; // max upgraded if ( m_iUpgradeLevel >= OBJ_MAX_UPGRADE_LEVEL ) return false; return true; } //----------------------------------------------------------------------------- // Purpose: Separated so it can be triggered by wrench hit or by vgui screen //----------------------------------------------------------------------------- bool CBaseObject::Command_Repair( CTFPlayer *pActivator, float flRepairMod ) { if ( !CanBeRepaired() ) return false; const float flRepairToMetalRatio = 3.0f; // Amount Repaired per metal float flTargetHeal = 100.0f * flRepairMod; int iAmountToHeal = MIN( flTargetHeal, GetMaxHealth() - RoundFloatToInt( GetHealth() ) ); // repair the building int iRepairCost = ceil( (float)( iAmountToHeal ) / flRepairToMetalRatio ); TRACE_OBJECT( UTIL_VarArgs( "%0.2f CBaseObject::Command_Repair ( %f / %d ) - cost = %d\n", gpGlobals->curtime, GetHealth(), GetMaxHealth(), iRepairCost ) ); if ( iRepairCost > 0 ) { if ( iRepairCost > pActivator->GetBuildResources() ) { iRepairCost = pActivator->GetBuildResources(); } pActivator->RemoveBuildResources( iRepairCost ); float flNewHealth = MIN( GetMaxHealth(), m_flHealth + ( iRepairCost * flRepairToMetalRatio ) ); if ( pActivator != GetBuilder() ) { float flAmountRepaired = flNewHealth - m_flHealth; pActivator->AwardAchievement( ACHIEVEMENT_TF_ENGINEER_REPAIR_TEAM_GRIND, floor( flAmountRepaired ) ); } SetHealth( flNewHealth ); return ( iRepairCost > 0 ); } return false; } //----------------------------------------------------------------------------- // Purpose: Upgrade this object a single level //----------------------------------------------------------------------------- void CBaseObject::StartUpgrading( void ) { // Increase level m_iUpgradeLevel++; if ( GetHighestUpgradeLevel() < m_iUpgradeLevel ) { m_iHighestUpgradeLevel = m_iUpgradeLevel; } // more health if ( !m_bCarryDeploy && !IsUsingReverseBuild() ) { int iMaxHealth = GetMaxHealthForCurrentLevel(); SetMaxHealth( iMaxHealth ); SetHealth( iMaxHealth ); } const char *pUpgradeSound = GetObjectInfo( ObjectType() )->m_pUpgradeSound; if ( pUpgradeSound && *pUpgradeSound ) { EmitSound( pUpgradeSound ); } if ( ( !m_bWasMapPlaced || ( m_iUpgradeLevel > (m_nDefaultUpgradeLevel+1) ) ) ) { SetActivity( ACT_OBJ_UPGRADING ); float flConstructionTime = ( ShouldQuickBuild() ? 0 : GetObjectInfo( ObjectType() )->m_flUpgradeDuration ); float flReverseBuildingConstructionSpeed = GetReversesBuildingConstructionSpeed(); flConstructionTime /= ( flReverseBuildingConstructionSpeed == 0.0f ? 1.0f : flReverseBuildingConstructionSpeed ); m_flUpgradeCompleteTime = gpGlobals->curtime + flConstructionTime; } else { m_flUpgradeCompleteTime = gpGlobals->curtime; //asap } RemoveAllGestures(); if ( TFGameRules() && TFGameRules()->IsInTraining() && TFGameRules()->GetTrainingModeLogic() && GetOwner() && GetOwner()->IsFakeClient() == false ) { TFGameRules()->GetTrainingModeLogic()->OnPlayerUpgradedBuilding( GetOwner(), this ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::FinishUpgrading( void ) { const char *pUpgradeSound = GetObjectInfo( ObjectType() )->m_pUpgradeSound; if ( pUpgradeSound && *pUpgradeSound ) { EmitSound( pUpgradeSound ); } if ( IsUsingReverseBuild() ) { m_iUpgradeLevel--; DoReverseBuild(); } } //----------------------------------------------------------------------------- // Playing the upgrade animation //----------------------------------------------------------------------------- void CBaseObject::UpgradeThink( void ) { if ( gpGlobals->curtime > m_flUpgradeCompleteTime ) { FinishUpgrading(); } } //----------------------------------------------------------------------------- // Purpose: Handles health upgrade for objects we've already built //----------------------------------------------------------------------------- void CBaseObject::ApplyHealthUpgrade( void ) { CTFPlayer *pTFPlayer = GetOwner(); if ( !pTFPlayer ) return; int iHealth = GetMaxHealthForCurrentLevel(); SetMaxHealth( iHealth ); SetHealth( iHealth ); //DevMsg( "%i\n", GetMaxHealth() ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::PlayStartupAnimation( void ) { SetActivity( ACT_OBJ_STARTUP ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::DetermineAnimation( void ) { Activity desiredActivity = m_Activity; switch ( m_Activity ) { default: { if ( IsUpgrading() ) { desiredActivity = ACT_OBJ_UPGRADING; } else if ( IsPlacing() ) { /* if (1 || m_bPlacementOK ) { desiredActivity = ACT_OBJ_PLACING; } else { desiredActivity = ACT_OBJ_IDLE; } */ } else if ( IsBuilding() ) { desiredActivity = ACT_OBJ_ASSEMBLING; } else { desiredActivity = ACT_OBJ_RUNNING; } } break; case ACT_OBJ_STARTUP: { if ( IsActivityFinished() ) { desiredActivity = ACT_OBJ_RUNNING; } } break; } if ( desiredActivity == m_Activity ) return; SetActivity( desiredActivity ); } //----------------------------------------------------------------------------- // Purpose: Attach this object to the specified object //----------------------------------------------------------------------------- void CBaseObject::AttachObjectToObject( CBaseEntity *pEntity, int iPoint, Vector &vecOrigin ) { m_hBuiltOnEntity = pEntity; m_iBuiltOnPoint = iPoint; int iAttachment = 0; if ( m_hBuiltOnEntity.Get() ) { CTFPlayer *pTFPlayer = ToTFPlayer( pEntity ); if ( pTFPlayer ) { iAttachment = pTFPlayer->LookupAttachment( "head" ); } else { CBaseAnimating *pAnimate = dynamic_cast( pEntity ); if ( pAnimate ) { iAttachment = pAnimate->LookupBone( "weapon_bone" ); if ( iAttachment >= 1 ) { FollowEntity( m_hBuiltOnEntity.Get() ); } } // Parent ourselves to the object IHasBuildPoints *pBPInterface = dynamic_cast( pEntity ); Assert( pBPInterface ); if ( pBPInterface ) { iAttachment = pBPInterface->GetBuildPointAttachmentIndex( iPoint ); // re-link to the build points if the sapper is already built if ( !( IsPlacing() || IsBuilding() ) ) { pBPInterface->SetObjectOnBuildPoint( m_iBuiltOnPoint, this ); } } } SetParent( m_hBuiltOnEntity.Get(), iAttachment ); if ( iAttachment >= 1 ) { // Stick right onto the attachment point. vecOrigin.Init(); SetLocalOrigin( vecOrigin ); SetLocalAngles( QAngle(0,0,0) ); } else { SetAbsOrigin( vecOrigin ); vecOrigin = GetLocalOrigin(); } SetupAttachedVersion(); } Assert( m_hBuiltOnEntity.Get() == GetMoveParent() ); } //----------------------------------------------------------------------------- // Purpose: Detach this object from its parent, if it has one //----------------------------------------------------------------------------- void CBaseObject::DetachObjectFromObject( void ) { if ( !GetParentObject() ) return; // Clear the build point IHasBuildPoints *pBPInterface = dynamic_cast(GetParentObject() ); Assert( pBPInterface ); pBPInterface->SetObjectOnBuildPoint( m_iBuiltOnPoint, NULL ); SetParent( NULL ); m_hBuiltOnEntity = NULL; m_iBuiltOnPoint = 0; } //----------------------------------------------------------------------------- // Purpose: Spawn any objects specified inside the mdl //----------------------------------------------------------------------------- void CBaseObject::SpawnEntityOnBuildPoint( const char *pEntityName, int iAttachmentNumber ) { // Try and spawn the object CBaseEntity *pEntity = CreateEntityByName( pEntityName ); if ( !pEntity ) return; Vector vecOrigin; QAngle vecAngles; GetAttachment( iAttachmentNumber, vecOrigin, vecAngles ); pEntity->SetAbsOrigin( vecOrigin ); pEntity->SetAbsAngles( vecAngles ); pEntity->Spawn(); // If it's an object, finish setting it up CBaseObject *pObject = dynamic_cast(pEntity); if ( !pObject ) return; // Add a buildpoint here int iPoint = AddBuildPoint( iAttachmentNumber ); AddValidObjectToBuildPoint( iPoint, pObject->GetType() ); pObject->SetBuilder( GetBuilder() ); pObject->ChangeTeam( GetTeamNumber() ); if ( !(pObject->m_fObjectFlags & OF_DOESNT_HAVE_A_MODEL) ) { pObject->SpawnControlPanels(); } pObject->SetHealth( pObject->GetMaxHealth() ); pObject->FinishedBuilding(); pObject->AttachObjectToObject( this, iPoint, vecOrigin ); //pObject->m_fObjectFlags |= OF_CANNOT_BE_DISMANTLED; IHasBuildPoints *pBPInterface = dynamic_cast(this); Assert( pBPInterface ); pBPInterface->SetObjectOnBuildPoint( iPoint, pObject ); } //----------------------------------------------------------------------------- // Purpose: Spawn any objects specified inside the mdl //----------------------------------------------------------------------------- void CBaseObject::SpawnObjectPoints( void ) { KeyValues *modelKeyValues = new KeyValues(""); if ( !modelKeyValues->LoadFromBuffer( modelinfo->GetModelName( GetModel() ), modelinfo->GetModelKeyValueText( GetModel() ) ) ) { modelKeyValues->deleteThis(); return; } // Do we have a build point section? KeyValues *pkvAllObjectPoints = modelKeyValues->FindKey("object_points"); if ( !pkvAllObjectPoints ) { modelKeyValues->deleteThis(); return; } // Start grabbing the sounds and slotting them in KeyValues *pkvObjectPoint; for ( pkvObjectPoint = pkvAllObjectPoints->GetFirstSubKey(); pkvObjectPoint; pkvObjectPoint = pkvObjectPoint->GetNextKey() ) { // Find the attachment first const char *sAttachment = pkvObjectPoint->GetName(); int iAttachmentNumber = LookupAttachment( sAttachment ); if ( iAttachmentNumber <= 0 ) { Msg( "ERROR: Model %s specifies object point %s, but has no attachment named %s.\n", STRING(GetModelName()), pkvObjectPoint->GetString(), pkvObjectPoint->GetString() ); continue; } // Now see what we're supposed to spawn there // The count check is because it seems wrong to emit multiple entities on the same point int nCount = 0; KeyValues *pkvObject; for ( pkvObject = pkvObjectPoint->GetFirstSubKey(); pkvObject; pkvObject = pkvObject->GetNextKey() ) { SpawnEntityOnBuildPoint( pkvObject->GetName(), iAttachmentNumber ); ++nCount; Assert( nCount <= 1 ); } } modelKeyValues->deleteThis(); } bool CBaseObject::IsSolidToPlayers( void ) const { switch ( m_SolidToPlayers ) { default: break; case SOLID_TO_PLAYER_USE_DEFAULT: { if ( GetObjectInfo( ObjectType() ) ) { return GetObjectInfo( ObjectType() )->m_bSolidToPlayerMovement; } } break; case SOLID_TO_PLAYER_YES: return true; case SOLID_TO_PLAYER_NO: return false; } return false; } void CBaseObject::SetSolidToPlayers( OBJSOLIDTYPE stp, bool force ) { bool changed = stp != m_SolidToPlayers; m_SolidToPlayers = stp; if ( changed || force ) { SetCollisionGroup( IsSolidToPlayers() ? TFCOLLISION_GROUP_OBJECT_SOLIDTOPLAYERMOVEMENT : TFCOLLISION_GROUP_OBJECT ); } } int CBaseObject::DrawDebugTextOverlays(void) { int text_offset = BaseClass::DrawDebugTextOverlays(); if (m_debugOverlays & OVERLAY_TEXT_BIT) { char tempstr[512]; Q_snprintf( tempstr, sizeof( tempstr ),"Health: %f / %d ( %.1f )", GetHealth(), GetMaxHealth(), (float)GetHealth() / (float)GetMaxHealth() ); EntityText(text_offset,tempstr,0); text_offset++; CTFPlayer *pBuilder = GetBuilder(); Q_snprintf( tempstr, sizeof( tempstr ),"Built by: (%d) %s", pBuilder ? pBuilder->entindex() : -1, pBuilder ? pBuilder->GetPlayerName() : "invalid builder" ); EntityText(text_offset,tempstr,0); text_offset++; if ( IsBuilding() ) { Q_snprintf( tempstr, sizeof( tempstr ),"Build Rate: %.1f", GetConstructionMultiplier() ); EntityText(text_offset,tempstr,0); text_offset++; } } return text_offset; } //----------------------------------------------------------------------------- // Purpose: Change build orientation //----------------------------------------------------------------------------- void CBaseObject::RotateBuildAngles( void ) { // rotate the build angles by 90 degrees ( final angle calculated after we network this ) m_iDesiredBuildRotations++; m_iDesiredBuildRotations = m_iDesiredBuildRotations % 4; } //----------------------------------------------------------------------------- // Purpose: called on edge cases to see if we need to change our disabled state //----------------------------------------------------------------------------- void CBaseObject::UpdateDisabledState( void ) { const bool bShouldBeEnabled = !m_bHasSapper && !m_bPlasmaDisable && (!TFGameRules()->RoundHasBeenWon() || TFGameRules()->GetWinningTeam() == GetTeamNumber()); SetDisabled( !bShouldBeEnabled ); } //----------------------------------------------------------------------------- // Purpose: called when our disabled state changes //----------------------------------------------------------------------------- void CBaseObject::SetDisabled( bool bDisabled ) { if ( bDisabled && !m_bDisabled ) { OnStartDisabled(); } else if ( !bDisabled && m_bDisabled ) { OnEndDisabled(); } m_bDisabled = bDisabled; } //----------------------------------------------------------------------------- void CBaseObject::SetPlasmaDisabled( float flDuration ) { m_bPlasmaDisable = true; m_flPlasmaDisableTime = gpGlobals->curtime + flDuration; UpdateDisabledState(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::OnStartDisabled( void ) { } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::OnEndDisabled( void ) { } //----------------------------------------------------------------------------- // Purpose: Called when the model changes, find new attachments for the children //----------------------------------------------------------------------------- void CBaseObject::ReattachChildren( void ) { // Go through and store the children one by one, then reattach them. We need // to store them like this because if we didnt and instead went through and // reattached them as we iterated over them we could get into a state where we have // children A and B and A has B as a sibling and B has NULL has a sibling, // but we reattach B first which set's B's sibling to A creating a infinite loop CUtlVector vecChildren; for (CBaseEntity *pChild = FirstMoveChild(); pChild; pChild = pChild->NextMovePeer()) { if( vecChildren.Find( pChild ) != vecChildren.InvalidIndex() ) { AssertMsg( 0, "Cyclic siblings found when reattaching children!" ); break; } vecChildren.AddToTail( pChild ); } int iNumBuildPoints = GetNumBuildPoints(); FOR_EACH_VEC( vecChildren, i ) { CBaseObject *pObject = dynamic_cast( vecChildren[i] ); if ( !pObject ) { continue; } Assert( pObject->GetParent() == this ); // get the type int iObjectType = pObject->GetType(); bool bReattached = false; Vector vecDummy; for ( int j = 0; j < iNumBuildPoints && bReattached == false; j++ ) { // Can this object build on this point? if ( CanBuildObjectOnBuildPoint( j, iObjectType ) ) { pObject->AttachObjectToObject( this, j, vecDummy ); bReattached = true; } } // if we can't find an attach for the child, remove it and print an error if ( bReattached == false ) { if ( m_bCarried && ( pObject->GetType() == OBJ_ATTACHMENT_SAPPER ) ) { pObject->ResetPlacement(); } else { pObject->DestroyObject(); Assert( !"Couldn't find attachment point on upgraded object for existing child.\n" ); } } } } void CBaseObject::SetModel( const char *pModel ) { // Skip if we're already the proper model if ( V_strcmp( GetModelName().ToCStr(), pModel ) == 0 ) return; BaseClass::SetModel( pModel ); // Clear out the gib list and create a new one. m_aGibs.Purge(); BuildGibList( m_aGibs, GetModelIndex(), 1.0f, COLLISION_GROUP_NONE ); CObjectSapper *pSapper = GetSapper(); if ( pSapper ) { pSapper->OnGoActive(); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::Activate( void ) { BaseClass::Activate(); InitializeMapPlacedObject(); } //----------------------------------------------------------------------------- // Purpose: Map placed objects need to setup here. //----------------------------------------------------------------------------- void CBaseObject::InitializeMapPlacedObject( void ) { m_bWasMapPlaced = true; //m_fObjectFlags |= OF_CANNOT_BE_DISMANTLED; // If a map-placed object spawns child objects with their own control // panels, all of this lovely code will already have been run if ( m_hBuiltOnEntity.Get() ) return; SetBuilder( NULL ); // NOTE: We must spawn the control panels now, instead of during // Spawn, because until placement is started, we don't actually know // the position of the control panel because we don't know what it's // been attached to (could be a vehicle which supplies a different // place for the control panel) if ( !(m_fObjectFlags & OF_DOESNT_HAVE_A_MODEL) ) { SpawnControlPanels(); } SetHealth( GetMaxHealth() ); //AlignToGround( GetAbsOrigin() ); FinishedBuilding(); // Set the skin m_nSkin = ( GetTeamNumber() == TF_TEAM_RED ) ? 0 : 1; } //----------------------------------------------------------------------------- // Purpose: Turns the object into one carried by someone. //----------------------------------------------------------------------------- void CBaseObject::MakeCarriedObject( CTFPlayer *pCarrier ) { if ( pCarrier ) { // Make the object inactive. m_bCarried = true; m_bCarryDeploy = false; pCarrier->m_Shared.SetCarriedObject( this ); m_iHealthOnPickup = m_iHealth; // If we are damaged, we want to remember how much damage we had sustained. // Remove screens. DestroyScreens(); // Mount it to the player. FollowEntity( pCarrier ); IGameEvent * event = gameeventmanager->CreateEvent( "player_carryobject" ); if ( event ) { event->SetInt( "userid", pCarrier->GetUserID() ); event->SetInt( "object", GetType() ); event->SetInt( "index", entindex() ); // object entity index gameeventmanager->FireEvent( event, true ); // don't send to clients } } } //----------------------------------------------------------------------------- // Purpose: Turns the object into one carried by someone. //----------------------------------------------------------------------------- void CBaseObject::DropCarriedObject( CTFPlayer* pCarrier ) { m_bCarried = false; m_bCarryDeploy = false; if ( pCarrier ) { pCarrier->m_Shared.SetCarriedObject( NULL ); } StopFollowingEntity(); } //----------------------------------------------------------------------------- // Purpose: Instantly build and upgrade this object //----------------------------------------------------------------------------- void CBaseObject::DoQuickBuild( bool bForceMax /* = false */ ) { if ( IsBuilding() ) { FinishedBuilding(); } int iTargetLevel = ( ( ( TFGameRules() && TFGameRules()->IsQuickBuildTime() ) || bForceMax ) ? OBJ_MAX_UPGRADE_LEVEL : GetUpgradeLevel() ); if ( CanBeUpgraded( GetOwner() ) ) { for ( int i = GetUpgradeLevel(); i < iTargetLevel; i++ ) { StartUpgrading(); } } else { int iMaxHealth = GetMaxHealthForCurrentLevel(); SetMaxHealth( iMaxHealth ); SetHealth( iMaxHealth ); } } //----------------------------------------------------------------------------- // Builds instantly under certain conditions/modes //----------------------------------------------------------------------------- bool CBaseObject::ShouldQuickBuild( void ) { if ( TFGameRules() ) { if ( GetType() == OBJ_ATTACHMENT_SAPPER ) return false; #ifdef STAGING_ONLY if ( GetType() == OBJ_SPY_TRAP ) return false; #endif if ( TFGameRules()->IsQuickBuildTime() ) { return true; } if ( TFGameRules()->IsMannVsMachineMode() ) { if ( GetTeamNumber() == TF_TEAM_PVE_INVADERS ) { // Engineer bots in MvM deploy pre-built sentries that build up at the normal rate return m_bForceQuickBuild; } if ( m_bCarryDeploy || TFGameRules()->State_Get() == GR_STATE_BETWEEN_RNDS ) { return true; } } } return m_bForceQuickBuild; } void CBaseObject::DoReverseBuild( void ) { m_iHighestUpgradeLevel = m_iUpgradeLevel; m_iUpgradeMetal = 0; int iMaxHealth = GetMaxHealthForCurrentLevel(); SetMaxHealth( iMaxHealth ); if ( GetHealth() > iMaxHealth ) { SetHealth( iMaxHealth ); } if ( m_iUpgradeLevel > 1 ) { m_iUpgradeLevel--; StartUpgrading(); } else { m_bBuilding = true; m_bCarryDeploy = false; m_flTotalConstructionTime = m_flConstructionTimeLeft = GetTotalTime(); m_flConstructionStartTime = gpGlobals->curtime; SetStartBuildingModel(); SetControlPanelsActive( false ); } } float CBaseObject::GetReversesBuildingConstructionSpeed( void ) { CObjectSapper *pSapper = GetSapper(); if ( !pSapper ) return 0.0f; return pSapper->GetReversesBuildingConstructionSpeed(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::InputEnable( inputdata_t &inputdata ) { if ( IsDisabled() ) { UpdateDisabledState(); if ( !IsDisabled() ) { OnGoActive(); } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseObject::InputDisable( inputdata_t &inputdata ) { if ( !IsDisabled() ) { SetDisabled( true ); OnGoInactive(); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CBaseObject::GetMaxHealthForCurrentLevel( void ) { int iMaxHealth = IsMiniBuilding() ? GetMiniBuildingStartingHealth() : GetBaseHealth(); if ( GetOwner() && !m_bDisposableBuilding ) { CALL_ATTRIB_HOOK_INT_ON_OTHER( GetOwner(), iMaxHealth, mult_engy_building_health ); } if ( !IsMiniBuilding() && ( GetUpgradeLevel() > 1 ) ) { float flMultiplier = pow( UPGRADE_LEVEL_HEALTH_MULTIPLIER, GetUpgradeLevel() - 1 ); iMaxHealth = (int)( iMaxHealth * flMultiplier ); } return iMaxHealth; }