//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: The Medic's Medikit weapon // // // $Workfile: $ // $Date: $ // $NoKeywords: $ //=============================================================================// #include "cbase.h" #include "in_buttons.h" #include "engine/IEngineSound.h" #include "tf_gamerules.h" #include "tf_item.h" #include "entity_capture_flag.h" #if defined( CLIENT_DLL ) #include #include #include "particles_simple.h" #include "c_tf_player.h" #include "soundenvelope.h" #include "tf_hud_mediccallers.h" #include "c_tf_playerresource.h" #include "prediction.h" #else #include "ndebugoverlay.h" #include "tf_player.h" #include "tf_team.h" #include "tf_gamestats.h" #include "ilagcompensationmanager.h" #include "tf_obj.h" #include "inetchannel.h" #include "IEffects.h" #include "baseprojectile.h" #include "soundenvelope.h" #include "effect_dispatch_data.h" #include "func_respawnroom.h" #endif #include "tf_revive.h" #include "tf_weapon_medigun.h" #include "tf_weapon_shovel.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" MedigunEffects_t g_MedigunEffects[MEDIGUN_NUM_CHARGE_TYPES] = { { TF_COND_INVULNERABLE, TF_COND_INVULNERABLE_WEARINGOFF, "TFPlayer.InvulnerableOn", "TFPlayer.InvulnerableOff" }, // MEDIGUN_CHARGE_INVULN = 0, { TF_COND_CRITBOOSTED, TF_COND_LAST, "TFPlayer.CritBoostOn", "TFPlayer.CritBoostOff" }, // MEDIGUN_CHARGE_CRITICALBOOST, { TF_COND_MEGAHEAL, TF_COND_LAST, "TFPlayer.QuickFixInvulnerableOn", "TFPlayer.MegaHealOff" }, // MEDIGUN_CHARGE_MEGAHEAL, { TF_COND_MEDIGUN_UBER_BULLET_RESIST, TF_COND_LAST, "WeaponMedigun_Vaccinator.InvulnerableOn", "WeaponMedigun_Vaccinator.InvulnerableOff" }, // TF_COND_MEDIGUN_UBER_BULLET_RESIST, { TF_COND_MEDIGUN_UBER_BLAST_RESIST, TF_COND_LAST, "WeaponMedigun_Vaccinator.InvulnerableOn", "WeaponMedigun_Vaccinator.InvulnerableOff" }, // TF_COND_MEDIGUN_UBER_BLAST_RESIST, { TF_COND_MEDIGUN_UBER_FIRE_RESIST, TF_COND_LAST, "WeaponMedigun_Vaccinator.InvulnerableOn", "WeaponMedigun_Vaccinator.InvulnerableOff" }, // TF_COND_MEDIGUN_UBER_FIRE_RESIST, }; struct MedigunResistConditions_t { medigun_resist_types_t eResistType; ETFCond passiveCond; ETFCond uberCond; }; MedigunResistConditions_t g_MedigunResistConditions[MEDIGUN_NUM_RESISTS] = { { MEDIGUN_BULLET_RESIST, TF_COND_MEDIGUN_SMALL_BULLET_RESIST, TF_COND_MEDIGUN_UBER_BULLET_RESIST }, { MEDIGUN_BLAST_RESIST, TF_COND_MEDIGUN_SMALL_BLAST_RESIST, TF_COND_MEDIGUN_UBER_BLAST_RESIST }, { MEDIGUN_FIRE_RESIST, TF_COND_MEDIGUN_SMALL_FIRE_RESIST, TF_COND_MEDIGUN_UBER_FIRE_RESIST } }; // Buff ranges ConVar weapon_medigun_damage_modifier( "weapon_medigun_damage_modifier", "1.5", FCVAR_CHEAT | FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY, "Scales the damage a player does while being healed with the medigun." ); ConVar weapon_medigun_construction_rate( "weapon_medigun_construction_rate", "10", FCVAR_CHEAT | FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY, "Constructing object health healed per second by the medigun." ); ConVar weapon_medigun_charge_rate( "weapon_medigun_charge_rate", "40", FCVAR_CHEAT | FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY, "Amount of time healing it takes to fully charge the medigun." ); ConVar weapon_medigun_chargerelease_rate( "weapon_medigun_chargerelease_rate", "8", FCVAR_CHEAT | FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY, "Amount of time it takes the a full charge of the medigun to be released." ); ConVar weapon_medigun_resist_num_chunks( "weapon_medigun_resist_num_chunks", "4", FCVAR_CHEAT | FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY, "How many uber bar chunks the vaccinator has." ); ConVar tf_vaccinator_uber_charge_rate_modifier( "tf_vaccinator_uber_charge_rate_modifier", "1.0", FCVAR_CHEAT | FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY , "Vaccinator uber charge rate." ); #if defined (CLIENT_DLL) ConVar tf_medigun_autoheal( "tf_medigun_autoheal", "0", FCVAR_CLIENTDLL | FCVAR_ARCHIVE | FCVAR_USERINFO, "Setting this to 1 will cause the Medigun's primary attack to be a toggle instead of needing to be held down." ); #endif #if !defined (CLIENT_DLL) ConVar tf_medigun_lagcomp( "tf_medigun_lagcomp", "1", FCVAR_DEVELOPMENTONLY ); #endif static const char *s_pszMedigunHealTargetThink = "MedigunHealTargetThink"; extern ConVar tf_invuln_time; #ifdef CLIENT_DLL //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void RecvProxy_HealingTarget( const CRecvProxyData *pData, void *pStruct, void *pOut ) { CWeaponMedigun *pMedigun = ((CWeaponMedigun*)(pStruct)); if ( pMedigun != NULL ) { pMedigun->ForceHealingTargetUpdate(); } RecvProxy_IntToEHandle( pData, pStruct, pOut ); } #endif LINK_ENTITY_TO_CLASS( tf_weapon_medigun, CWeaponMedigun ); PRECACHE_WEAPON_REGISTER( tf_weapon_medigun ); IMPLEMENT_NETWORKCLASS_ALIASED( WeaponMedigun, DT_WeaponMedigun ) #ifdef GAME_DLL void* SendProxy_SendActiveLocalWeaponDataTable( const SendProp *pProp, const void *pStruct, const void *pVarData, CSendProxyRecipients *pRecipients, int objectID ); void* SendProxy_SendNonLocalWeaponDataTable( const SendProp *pProp, const void *pStruct, const void *pVarData, CSendProxyRecipients *pRecipients, int objectID ); #endif //----------------------------------------------------------------------------- // Purpose: Only sent when a player's holding it. //----------------------------------------------------------------------------- BEGIN_NETWORK_TABLE_NOBASE( CWeaponMedigun, DT_LocalTFWeaponMedigunData ) #if defined( CLIENT_DLL ) RecvPropFloat( RECVINFO(m_flChargeLevel) ), #else SendPropFloat( SENDINFO(m_flChargeLevel), 0, SPROP_NOSCALE | SPROP_CHANGES_OFTEN ), #endif END_NETWORK_TABLE() //----------------------------------------------------------------------------- // Purpose: Variables sent at low precision to non-holding observers. //----------------------------------------------------------------------------- BEGIN_NETWORK_TABLE_NOBASE( CWeaponMedigun, DT_TFWeaponMedigunDataNonLocal ) #if defined( CLIENT_DLL ) RecvPropFloat( RECVINFO(m_flChargeLevel) ), #else SendPropFloat( SENDINFO(m_flChargeLevel), 12, SPROP_NOSCALE | SPROP_CHANGES_OFTEN, 0.0, 100.0f ), #endif END_NETWORK_TABLE() //----------------------------------------------------------------------------- // Purpose: Variables always sent //----------------------------------------------------------------------------- BEGIN_NETWORK_TABLE( CWeaponMedigun, DT_WeaponMedigun ) #if !defined( CLIENT_DLL ) // SendPropFloat( SENDINFO(m_flChargeLevel), 0, SPROP_NOSCALE | SPROP_CHANGES_OFTEN ), SendPropEHandle( SENDINFO( m_hHealingTarget ) ), SendPropBool( SENDINFO( m_bHealing ) ), SendPropBool( SENDINFO( m_bAttacking ) ), SendPropBool( SENDINFO( m_bChargeRelease ) ), SendPropBool( SENDINFO( m_bHolstered ) ), SendPropInt( SENDINFO( m_nChargeResistType ) ), SendPropDataTable("LocalTFWeaponMedigunData", 0, &REFERENCE_SEND_TABLE(DT_LocalTFWeaponMedigunData), SendProxy_SendLocalWeaponDataTable ), SendPropDataTable("NonLocalTFWeaponMedigunData", 0, &REFERENCE_SEND_TABLE(DT_TFWeaponMedigunDataNonLocal), SendProxy_SendNonLocalWeaponDataTable ), #else // RecvPropFloat( RECVINFO(m_flChargeLevel) ), RecvPropEHandle( RECVINFO( m_hHealingTarget ), RecvProxy_HealingTarget ), RecvPropBool( RECVINFO( m_bHealing ) ), RecvPropBool( RECVINFO( m_bAttacking ) ), RecvPropBool( RECVINFO( m_bChargeRelease ) ), RecvPropBool( RECVINFO( m_bHolstered ) ), RecvPropInt( RECVINFO( m_nChargeResistType ) ), RecvPropDataTable("LocalTFWeaponMedigunData", 0, 0, &REFERENCE_RECV_TABLE(DT_LocalTFWeaponMedigunData)), RecvPropDataTable("NonLocalTFWeaponMedigunData", 0, 0, &REFERENCE_RECV_TABLE(DT_TFWeaponMedigunDataNonLocal)), #endif END_NETWORK_TABLE() #ifdef CLIENT_DLL BEGIN_PREDICTION_DATA( CWeaponMedigun ) DEFINE_PRED_FIELD( m_bHealing, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE ), DEFINE_PRED_FIELD( m_bAttacking, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE ), DEFINE_PRED_FIELD( m_bHolstered, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE ), DEFINE_PRED_FIELD( m_hHealingTarget, FIELD_EHANDLE, FTYPEDESC_INSENDTABLE ), DEFINE_FIELD( m_bCanChangeTarget, FIELD_BOOLEAN ), DEFINE_FIELD( m_flHealEffectLifetime, FIELD_FLOAT ), DEFINE_PRED_FIELD( m_flChargeLevel, FIELD_FLOAT, FTYPEDESC_INSENDTABLE ), DEFINE_PRED_FIELD( m_bChargeRelease, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE ), // DEFINE_PRED_FIELD( m_bPlayingSound, FIELD_BOOLEAN ), // DEFINE_PRED_FIELD( m_bUpdateHealingTargets, FIELD_BOOLEAN ), END_PREDICTION_DATA() #endif #define PARTICLE_PATH_VEL 140.0 #define NUM_PATH_PARTICLES_PER_SEC 300.0f #define NUM_MEDIGUN_PATH_POINTS 8 extern ConVar tf_max_health_boost; //----------------------------------------------------------------------------- // Purpose: For HUD auto medic callers //----------------------------------------------------------------------------- #ifdef CLIENT_DLL ConVar hud_medicautocallers( "hud_medicautocallers", "0", FCVAR_ARCHIVE | FCVAR_ARCHIVE_XBOX ); ConVar hud_medicautocallersthreshold( "hud_medicautocallersthreshold", "75", FCVAR_ARCHIVE | FCVAR_ARCHIVE_XBOX ); ConVar hud_medichealtargetmarker ( "hud_medichealtargetmarker", "0", FCVAR_ARCHIVE | FCVAR_ARCHIVE_XBOX ); #endif const char *g_pszMedigunHealSounds[MEDIGUN_NUM_CHARGE_TYPES] = { "WeaponMedigun.Healing", // MEDIGUN_CHARGE_INVULN = 0, "WeaponMedigun.Healing", // MEDIGUN_CHARGE_CRITICALBOOST, "Weapon_Quick_Fix.Healing", // MEDIGUN_CHARGE_MEGAHEAL, "WeaponMedigun_Vaccinator.Healing", // MEDIGUN_CHARGE_RESIST, }; //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CWeaponMedigun::CWeaponMedigun( void ) { WeaponReset(); SetPredictionEligible( true ); } CWeaponMedigun::~CWeaponMedigun() { #ifdef CLIENT_DLL StopChargeEffect( true ); if ( m_pChargedSound ) { CSoundEnvelopeController::GetController().SoundDestroy( m_pChargedSound ); } if ( m_pDisruptSound ) { CSoundEnvelopeController::GetController().SoundDestroy( m_pDisruptSound ); } m_flAutoCallerCheckTime = 0.0f; #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::WeaponReset( void ) { BaseClass::WeaponReset(); CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( m_bHealing && pOwner && pOwner->m_Shared.InState( TF_STATE_DYING ) ) { m_bWasHealingBeforeDeath = true; } else { m_bWasHealingBeforeDeath = false; } m_flHealEffectLifetime = 0; m_bHealing = false; m_bAttacking = false; m_bChargeRelease = false; m_DetachedTargets.Purge(); m_flEndResistCharge = 0.f; m_bCanChangeTarget = true; m_flNextBuzzTime = 0; m_flReleaseStartedAt = 0; int iPreserveUber = 0; if ( TFGameRules()->State_Get() == GR_STATE_RND_RUNNING ) { CALL_ATTRIB_HOOK_INT_ON_OTHER( pOwner, iPreserveUber, preserve_ubercharge ); m_flChargeLevel = MIN( m_flChargeLevel, iPreserveUber / 100.f ); } else { m_flChargeLevel = 0; } RemoveHealingTarget( true ); m_bAttack2Down = false; m_bAttack3Down = false; m_bReloadDown = false; m_nChargeResistType = 0; #if defined( GAME_DLL ) StopHealingOwner(); m_hLastHealingTarget = NULL; RecalcEffectOnTarget( ToTFPlayer( GetOwnerEntity() ), true ); m_nHealTargetClass = 0; m_nChargesReleased = 0; #endif #if defined( CLIENT_DLL ) m_nOldChargeResistType = 0; m_bPlayingSound = false; m_bUpdateHealingTargets = false; m_bOldChargeRelease = false; UpdateEffects(); StopChargeEffect( true ); m_pChargeEffectOwner = NULL; m_pChargeEffect = NULL; m_pChargedSound = NULL; m_pDisruptSound = NULL; m_flDenySecondary = 0.f; #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::Precache() { BaseClass::Precache(); PrecacheModel( "models/weapons/c_models/c_proto_backpack/c_proto_backpack.mdl" ); PrecacheScriptSound( "WeaponMedigun.NoTarget" ); PrecacheScriptSound( "WeaponMedigun.Healing" ); PrecacheScriptSound( "Weapon_Quick_Fix.Healing" ); PrecacheScriptSound( "WeaponMedigun.Charged" ); PrecacheParticleSystem( "medicgun_invulnstatus_fullcharge_blue" ); PrecacheParticleSystem( "medicgun_invulnstatus_fullcharge_red" ); PrecacheParticleSystem( "medicgun_beam_red_invun" ); PrecacheParticleSystem( "medicgun_beam_red" ); PrecacheParticleSystem( "medicgun_beam_red_targeted" ); PrecacheParticleSystem( "medicgun_beam_blue_invun" ); PrecacheParticleSystem( "medicgun_beam_blue" ); PrecacheParticleSystem( "medicgun_beam_blue_targeted" ); PrecacheParticleSystem( "vaccinator_red_buff1" ); PrecacheParticleSystem( "vaccinator_red_buff2" ); PrecacheParticleSystem( "vaccinator_red_buff3" ); PrecacheParticleSystem( "vaccinator_blue_buff1" ); PrecacheParticleSystem( "vaccinator_blue_buff2" ); PrecacheParticleSystem( "vaccinator_blue_buff3" ); PrecacheParticleSystem( "drain_effect" ); PrecacheScriptSound( "WeaponMedigun_Vaccinator.Charged_tier_01"); PrecacheScriptSound( "WeaponMedigun_Vaccinator.Charged_tier_02"); PrecacheScriptSound( "WeaponMedigun_Vaccinator.Charged_tier_03"); PrecacheScriptSound( "WeaponMedigun_Vaccinator.Charged_tier_04"); PrecacheScriptSound( "WeaponMedigun.HealingDisrupt" ); // PrecacheParticleSystem( "medicgun_beam_machinery" ); for( int i=0; im_Shared.IsRageDraining() ) { CreateMedigunShield(); } // Resume healing self for Quick-Fix if we're still ubering and switch back to the Quick-Fix. if ( ( GetMedigunType() == MEDIGUN_QUICKFIX ) && m_bChargeRelease && !m_bHealingSelf ) { StartHealingTarget( pOwner ); m_bHealingSelf = true; } #endif #ifdef CLIENT_DLL ManageChargeEffect(); #endif m_flNextTargetCheckTime = gpGlobals->curtime; return true; } return false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CWeaponMedigun::Holster( CBaseCombatWeapon *pSwitchingTo ) { RemoveHealingTarget( true ); m_bAttacking = false; m_bHolstered = true; #ifdef GAME_DLL RecalcEffectOnTarget( ToTFPlayer( GetOwnerEntity() ), true ); StopHealingOwner(); #endif RemoveMedigunShield(); #ifdef CLIENT_DLL UpdateEffects(); ManageChargeEffect(); #endif return BaseClass::Holster( pSwitchingTo ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::UpdateOnRemove( void ) { RemoveHealingTarget( true ); m_bAttacking = false; m_bChargeRelease = false; #ifdef GAME_DLL RecalcEffectOnTarget( ToTFPlayer( GetOwnerEntity() ), true ); StopHealingOwner(); #endif #ifdef CLIENT_DLL if ( m_bPlayingSound ) { m_bPlayingSound = false; StopHealSound(); } UpdateEffects(); #endif RemoveMedigunShield(); BaseClass::UpdateOnRemove(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- float CWeaponMedigun::GetTargetRange( void ) { return (float)m_pWeaponInfo->GetWeaponData( m_iWeaponMode ).m_flRange; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- float CWeaponMedigun::GetStickRange( void ) { return (GetTargetRange() * 1.2); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- float CWeaponMedigun::GetHealRate( void ) { float flHealRate = m_pWeaponInfo->GetWeaponData( m_iWeaponMode ).m_nDamage; CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetOwnerEntity(), flHealRate, mult_medigun_healrate ); // This attribute represents a bucket of attributes. int iHealingMastery = 0; CALL_ATTRIB_HOOK_INT_ON_OTHER( GetOwnerEntity(), iHealingMastery, healing_mastery ); if ( iHealingMastery ) { float flPerc = RemapValClamped( (float)iHealingMastery, 1.f, 4.f, 1.25f, 2.f ); flHealRate *= flPerc; } if ( TFGameRules() && TFGameRules()->IsPowerupMode() ) { CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( pOwner && pOwner->m_Shared.GetCarryingRuneType() == RUNE_HASTE ) { flHealRate *= 2.f; } else if ( pOwner && ( pOwner->m_Shared.GetCarryingRuneType() == RUNE_KING || pOwner->m_Shared.InCond( TF_COND_KING_BUFFED ) ) ) { flHealRate *= 1.5f; } } return flHealRate; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CWeaponMedigun::HealingTarget( CBaseEntity *pTarget ) { if ( pTarget == m_hHealingTarget ) return true; return false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CWeaponMedigun::AllowedToHealTarget( CBaseEntity *pTarget ) { CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return false; if ( !pTarget ) return false; if ( pTarget->IsPlayer() ) { CTFPlayer *pTFPlayer = ToTFPlayer( pTarget ); if ( !pTFPlayer ) return false; if ( !pTFPlayer->IsAlive() ) return false; // We cannot heal teammates who are using the Equalizer. CTFWeaponBase *pTFWeapon = pTFPlayer->GetActiveTFWeapon(); int iWeaponBlocksHealing = 0; CALL_ATTRIB_HOOK_INT_ON_OTHER( pTFWeapon, iWeaponBlocksHealing, weapon_blocks_healing ); if ( iWeaponBlocksHealing == 1 ) { return false; } bool bStealthed = pTFPlayer->m_Shared.IsStealthed() && !pOwner->m_Shared.IsStealthed(); // Allow stealthed medics to heal stealthed targets bool bDisguised = pTFPlayer->m_Shared.InCond( TF_COND_DISGUISED ); // We can heal teammates and enemies that are disguised as teammates if ( !bStealthed && ( pTFPlayer->InSameTeam( pOwner ) || ( bDisguised && pTFPlayer->m_Shared.GetDisguiseTeam() == pOwner->GetTeamNumber() ) ) ) { return true; } } else { if ( !pTarget->InSameTeam( pOwner ) ) return false; #ifdef STAGING_ONLY if ( pTarget->IsBaseObject() && IsAllowedToTargetBuildings() ) return true; #else if ( pTarget->IsBaseObject() ) return false; #endif // STAGING_ONLY CTFReviveMarker *pReviveMarker = dynamic_cast< CTFReviveMarker* >( pTarget ); if ( pReviveMarker ) { m_hReviveMarker = pReviveMarker; // Store last marker we touched return true; } } return false; } // Now make sure there isn't something other than team players in the way. class CMedigunFilter : public CTraceFilterSimple { public: CMedigunFilter( CBaseEntity *pShooter ) : CTraceFilterSimple( pShooter, COLLISION_GROUP_WEAPON ) { m_pShooter = pShooter; } virtual bool ShouldHitEntity( IHandleEntity *pHandleEntity, int contentsMask ) { // If it hit an edict that isn't the target and is on our team, then the ray is blocked. CBaseEntity *pEnt = static_cast(pHandleEntity); // Ignore collisions with the shooter if ( pEnt == m_pShooter ) return false; if ( pEnt->GetTeam() == m_pShooter->GetTeam() ) return false; return CTraceFilterSimple::ShouldHitEntity( pHandleEntity, contentsMask ); } CBaseEntity *m_pShooter; }; //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::MaintainTargetInSlot() { CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return; CBaseEntity *pTarget = m_hHealingTarget; Assert( pTarget ); // Make sure the guy didn't go out of range. bool bLostTarget = true; Vector vecSrc = pOwner->Weapon_ShootPosition( ); Vector vecTargetPoint = pTarget->WorldSpaceCenter(); Vector vecPoint; // If it's brush built, use absmins/absmaxs pTarget->CollisionProp()->CalcNearestPoint( vecSrc, &vecPoint ); float flDistance = (vecPoint - vecSrc).Length(); if ( flDistance < GetStickRange() ) { if ( m_flNextTargetCheckTime > gpGlobals->curtime ) return; m_flNextTargetCheckTime = gpGlobals->curtime + 1.0f; CheckAchievementsOnHealTarget(); trace_t tr; CMedigunFilter drainFilter( pOwner ); Vector vecAiming; pOwner->EyeVectors( &vecAiming ); Vector vecEnd = vecSrc + vecAiming * GetTargetRange(); UTIL_TraceLine( vecSrc, vecEnd, (MASK_SHOT & ~CONTENTS_HITBOX), pOwner, DMG_GENERIC, &tr ); // Still visible? if ( tr.m_pEnt == pTarget ) return; UTIL_TraceLine( vecSrc, vecTargetPoint, MASK_SHOT, &drainFilter, &tr ); // Still visible? if (( tr.fraction == 1.0f) || (tr.m_pEnt == pTarget)) return; // If we failed, try the target's eye point as well UTIL_TraceLine( vecSrc, pTarget->EyePosition(), MASK_SHOT, &drainFilter, &tr ); if (( tr.fraction == 1.0f) || (tr.m_pEnt == pTarget)) return; } // We've lost this guy if ( bLostTarget ) { RemoveHealingTarget(); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::FindNewTargetForSlot() { CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return; Vector vecSrc = pOwner->Weapon_ShootPosition( ); if ( m_hHealingTarget ) { RemoveHealingTarget(); } // In Normal mode, we heal players under our crosshair Vector vecAiming; pOwner->EyeVectors( &vecAiming ); // Find a player in range of this player, and make sure they're healable. Vector vecEnd = vecSrc + vecAiming * GetTargetRange(); trace_t tr; UTIL_TraceLine( vecSrc, vecEnd, (MASK_SHOT & ~CONTENTS_HITBOX), pOwner, DMG_GENERIC, &tr ); if ( tr.fraction != 1.0 && tr.m_pEnt ) { if ( !HealingTarget( tr.m_pEnt ) && AllowedToHealTarget( tr.m_pEnt ) ) { #ifdef GAME_DLL pOwner->SpeakConceptIfAllowed( MP_CONCEPT_MEDIC_STARTEDHEALING ); if ( tr.m_pEnt->IsPlayer() ) { CTFPlayer *pTarget = ToTFPlayer( tr.m_pEnt ); pTarget->SpeakConceptIfAllowed( MP_CONCEPT_HEALTARGET_STARTEDHEALING ); } // Start the heal target thinking. SetContextThink( &CWeaponMedigun::HealTargetThink, gpGlobals->curtime, s_pszMedigunHealTargetThink ); m_hLastHealingTarget = tr.m_pEnt; #endif m_hHealingTarget.Set( tr.m_pEnt ); m_flNextTargetCheckTime = gpGlobals->curtime + 1.0f; } } } bool CWeaponMedigun::IsReleasingCharge( void ) const { return (m_bChargeRelease && !m_bHolstered); } int CWeaponMedigun::GetMedigunType( void ) const { int iMode = 0; CALL_ATTRIB_HOOK_INT( iMode, set_weapon_mode ); return iMode; } float CWeaponMedigun::GetMinChargeAmount( void ) const { if( GetMedigunType() == MEDIGUN_RESIST ) { return 1.f / weapon_medigun_resist_num_chunks.GetInt(); } else { return 1.f; } } medigun_charge_types CWeaponMedigun::GetChargeType( void ) const { int iTmp = MEDIGUN_CHARGE_INVULN; CALL_ATTRIB_HOOK_INT( iTmp, set_charge_type ); if( GetMedigunType() == MEDIGUN_RESIST ) { // If this is a resist-medigun, then the charge type needs to be within the resist types Assert( iTmp >= MEDIGUN_CHARGE_BULLET_RESIST && iTmp <= MEDIGUN_CHARGE_FIRE_RESIST ); Assert( m_nChargeResistType < MEDIGUN_NUM_RESISTS ); iTmp += m_nChargeResistType; } return (medigun_charge_types)iTmp; } void CWeaponMedigun::CycleResistType() { // Resist medigun only! if( GetMedigunType() != MEDIGUN_RESIST ) return; if( IsReleasingCharge() ) return; #ifdef GAME_DLL // When cycling resist we have to remove the current resist, then add the new resist. CTFPlayer *pTFHealingTarget = ToTFPlayer( m_hHealingTarget ); CTFPlayer *pOwner = ToTFPlayer( GetOwner() ); ETFCond cond = g_MedigunResistConditions[ GetResistType() ].passiveCond; // Remove from out healing target if( pTFHealingTarget) { CBaseEntity* pProvider = pTFHealingTarget->m_Shared.GetConditionProvider( cond ); // Remove from our healing target if we're the provider if( pProvider == pOwner || pProvider == NULL ) { pTFHealingTarget->m_Shared.RemoveCond( cond ); } } // Remove from ourselves if( pOwner ) { // Remove from ourselves if we're the provider CBaseEntity* pProvider = pOwner->m_Shared.GetConditionProvider( cond ); if( pProvider == pOwner || pProvider == NULL ) { pOwner->m_Shared.RemoveCond( cond ); } } #endif m_nChargeResistType += 1; m_nChargeResistType = m_nChargeResistType % MEDIGUN_NUM_RESISTS; #ifdef GAME_DLL CTFPlayer *pTFOwner = ToTFPlayer( GetOwnerEntity() ); if( pTFOwner ) { RecalcEffectOnTarget( pTFOwner ); } if( pTFHealingTarget ) { // Now add the new resist RecalcEffectOnTarget( pTFHealingTarget ); pTFHealingTarget->m_Shared.AddCond( g_MedigunResistConditions[ GetResistType() ].passiveCond, PERMANENT_CONDITION, pTFOwner ); pTFOwner->m_Shared.AddCond( g_MedigunResistConditions[ GetResistType() ].passiveCond, PERMANENT_CONDITION, pTFOwner ); } #else // Updates our particles ForceHealingTargetUpdate(); #endif } medigun_resist_types_t CWeaponMedigun::GetResistType() const { Assert( GetMedigunType() == MEDIGUN_RESIST ); int nCurrentActiveResist = ( GetChargeType() - MEDIGUN_CHARGE_BULLET_RESIST ); Assert( nCurrentActiveResist >= 0 && nCurrentActiveResist < MEDIGUN_NUM_RESISTS ); nCurrentActiveResist = nCurrentActiveResist % MEDIGUN_NUM_RESISTS; return medigun_resist_types_t(nCurrentActiveResist); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CWeaponMedigun::IsAllowedToTargetBuildings( void ) { #ifdef STAGING_ONLY if ( !TFGameRules() || !TFGameRules()->GameModeUsesUpgrades() ) return false; // See if we have the upgrade to heal buildings int iHealBuildings = 0; CALL_ATTRIB_HOOK_INT( iHealBuildings, medic_machinery_beam ); return iHealBuildings ? true : false; #else return false; #endif // STAGING_ONLY } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CWeaponMedigun::IsAttachedToBuilding( void ) { if ( !m_hHealingTarget ) return false; return m_hHealingTarget->IsBaseObject(); } #ifdef GAME_DLL //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::HealTargetThink( void ) { // Verify that we still have a valid heal target. CBaseEntity *pTarget = m_hHealingTarget; if ( !pTarget || !pTarget->IsAlive() ) { SetContextThink( NULL, 0, s_pszMedigunHealTargetThink ); return; } CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return; float flTime = gpGlobals->curtime - pOwner->GetTimeBase(); if ( flTime > 5.0f || !AllowedToHealTarget(pTarget) ) { RemoveHealingTarget( true ); } // Make sure our heal target hasn't changed classes while being healed CTFPlayer *pTFTarget = ToTFPlayer( pTarget ); if ( pTFTarget ) { int nPrevClass = m_nHealTargetClass; m_nHealTargetClass = pTFTarget->GetPlayerClass()->GetClassIndex(); if ( m_nHealTargetClass != nPrevClass ) { pOwner->TeamFortress_SetSpeed(); } } if ( !pTarget->IsPlayer() ) { #ifdef STAGING_ONLY if ( TFGameRules() && TFGameRules()->GameModeUsesUpgrades() ) { if ( IsAttachedToBuilding() ) { // Heal building if ( m_hHealingTarget->GetHealth() < m_hHealingTarget->GetMaxHealth() ) { CBaseEntity *pEntity = m_hHealingTarget; CBaseObject *pObject = dynamic_cast( pEntity ); if ( pObject ) { pObject->SetHealth( m_hHealingTarget->GetHealth() + ( GetHealRate() / 10.f ) ); } } } } #endif // STAGING_ONLY CTFReviveMarker *pReviveMarker = dynamic_cast< CTFReviveMarker* >( pTarget ); if ( pReviveMarker ) { CTFPlayer *pDeadPlayer = pReviveMarker->GetOwner(); if ( pDeadPlayer ) { pReviveMarker->SetReviver( pOwner ); // Instantly revive players when deploying uber float flHealRate = GetHealRate(); float flReviveRate = m_bChargeRelease ? flHealRate / 2.f : flHealRate / 8.f; pReviveMarker->AddMarkerHealth( flReviveRate ); // Set observer target to reviver so they know they're being revived if ( pDeadPlayer->GetObserverMode() > OBS_MODE_FREEZECAM ) { if ( pReviveMarker->GetReviver() && pDeadPlayer->GetObserverTarget() != pReviveMarker->GetReviver() ) { pDeadPlayer->SetObserverTarget( pReviveMarker->GetReviver() ); } } if ( !pReviveMarker->HasOwnerBeenPrompted() ) { // This will give them a messagebox that has a Cancel button pReviveMarker->PromptOwner(); } } } } SetNextThink( gpGlobals->curtime + 0.2f, s_pszMedigunHealTargetThink ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::StartHealingTarget( CBaseEntity *pTarget ) { CTFPlayer *pTFTarget = ToTFPlayer( pTarget ); if ( !pTFTarget ) return; CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return; // Handle bonuses as additive, penalties as percentage... float flOverhealBonus = tf_max_health_boost.GetFloat() - 1.0f; float flMod = 1.0f; CALL_ATTRIB_HOOK_FLOAT( flMod, mult_medigun_overheal_amount ); if ( flMod >= 1.0f ) { flOverhealBonus += flMod; } else if ( flMod < 1.0f && flOverhealBonus > 0.0f ) { flOverhealBonus *= flMod; flOverhealBonus += 1.0f; } // Safety net if ( flOverhealBonus <= 1.0f ) { flOverhealBonus = 1.0f; } float flOverhealDecayMult = 1.0; CALL_ATTRIB_HOOK_FLOAT( flOverhealDecayMult, mult_medigun_overheal_decay ); float flOverhealExpert = 0.f; CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pOwner, flOverhealExpert, overheal_expert ); flOverhealBonus = Max( flOverhealBonus, flOverhealBonus + ( flOverhealExpert / 4 ) ); flOverhealDecayMult = Max( flOverhealDecayMult, flOverhealDecayMult + ( flOverhealExpert / 2 ) ); pTFTarget->m_Shared.Heal( pOwner, GetHealRate(), flOverhealBonus, flOverhealDecayMult ); // If target is grappling, set ourselves as grappling them if ( pTFTarget->GetGrapplingHookTarget() ) { pOwner->SetGrapplingHookTarget( pTFTarget ); } // Add on the small passive resist when we attach onto a target if( GetMedigunType() == MEDIGUN_RESIST ) { pTFTarget->m_Shared.AddCond( g_MedigunResistConditions[ GetResistType() ].passiveCond, PERMANENT_CONDITION, pOwner ); pOwner->m_Shared.AddCond( g_MedigunResistConditions[ GetResistType() ].passiveCond, PERMANENT_CONDITION, pOwner ); } } //----------------------------------------------------------------------------- // Purpose: QuickFix uber heals the target and medic //----------------------------------------------------------------------------- void CWeaponMedigun::StopHealingOwner( void ) { if ( !m_bHealingSelf ) return; CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return; pOwner->m_Shared.StopHealing( pOwner ); m_bHealingSelf = false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::AddCharge( float flPercentage ) { m_flChargeLevel = MIN( m_flChargeLevel + flPercentage, 1.0 ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::SubtractCharge( float flPercentage ) { float flSubtractAmount = Max( flPercentage, 0.0f ); SubtractChargeAndUpdateDeployState( flSubtractAmount, true ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::RecalcEffectOnTarget( CTFPlayer *pPlayer, bool bInstantRemove ) { if ( !pPlayer ) return; pPlayer->m_Shared.RecalculateChargeEffects( bInstantRemove ); } #endif //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- const char *CWeaponMedigun::GetHealSound( void ) const { COMPILE_TIME_ASSERT( ARRAYSIZE(g_pszMedigunHealSounds) == MEDIGUN_NUM_CHARGE_TYPES ); return g_pszMedigunHealSounds[ GetMedigunType() ]; } #ifdef GAME_DLL void CWeaponMedigun::UberchargeChunkDeployed() { m_nChargesReleased++; if( m_nChargesReleased % weapon_medigun_resist_num_chunks.GetInt() == 0 ) { CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return; CTF_GameStats.Event_PlayerInvulnerable( pOwner ); EconEntity_OnOwnerKillEaterEvent( this, pOwner, ToTFPlayer( m_hHealingTarget ), kKillEaterEvent_UberActivated ); } } #endif //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::CreateMedigunShield( void ) { #ifdef GAME_DLL if ( m_hMedigunShield ) return; CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return; #ifdef STAGING_ONLY bool bHasPermanentShield = HasPermanentShield(); if ( !bHasPermanentShield ) #endif // STAGING_ONLY { // check if we can activate the shield if ( ( pOwner->m_Shared.GetRageMeter() < 100.f ) && !pOwner->m_Shared.IsRageDraining() ) return; } pOwner->SpeakConceptIfAllowed( MP_CONCEPT_MEDIC_HEAL_SHIELD ); m_hMedigunShield = CTFMedigunShield::Create( pOwner ); if ( m_hMedigunShield ) { pOwner->m_Shared.StartRageDrain(); #ifdef STAGING_ONLY m_hMedigunShield->SetPermanentShield( bHasPermanentShield ); #endif // STAGING_ONLY } #endif // GAME_DLL } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::RemoveMedigunShield( void ) { #ifdef GAME_DLL if ( m_hMedigunShield ) { m_hMedigunShield->RemoveShield(); m_hMedigunShield.Set( NULL ); } #endif // GAME_DLL } #ifdef STAGING_ONLY //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CWeaponMedigun::HasPermanentShield() const { if ( !TFGameRules()->IsMannVsMachineMode() ) return false; int iPermanentShield = 0; CALL_ATTRIB_HOOK_INT( iPermanentShield, permanent_medic_shield ); return iPermanentShield != 0; } #endif // STAGING_ONLY //----------------------------------------------------------------------------- // Purpose: Returns a pointer to a healable target //----------------------------------------------------------------------------- bool CWeaponMedigun::FindAndHealTargets( void ) { CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return false; #ifdef GAME_DLL if ( !pOwner->IsBot() ) { INetChannelInfo *pNetChanInfo = engine->GetPlayerNetInfo( pOwner->entindex() ); if ( !pNetChanInfo || pNetChanInfo->IsTimingOut() ) return false; } #endif // GAME_DLL bool bFound = false; // Maintaining beam to existing target? CBaseEntity *pTarget = m_hHealingTarget; if ( pTarget && pTarget->IsAlive() ) { MaintainTargetInSlot(); } else { FindNewTargetForSlot(); } CBaseEntity *pNewTarget = m_hHealingTarget; if ( pNewTarget && pNewTarget->IsAlive() ) { CTFPlayer *pTFPlayer = ToTFPlayer( pNewTarget ); #ifdef GAME_DLL // HACK: For now, just deal with players if ( pTFPlayer ) { if ( pTarget != pNewTarget ) { StartHealingTarget( pNewTarget ); } RecalcEffectOnTarget( pTFPlayer ); } #endif bFound = true; // Charge up our power if we're not releasing it, and our target // isn't receiving any benefit from our healing. if ( !m_bChargeRelease ) { float flChargeRate = weapon_medigun_charge_rate.GetFloat(); float flChargeAmount = gpGlobals->frametime / flChargeRate; if ( pTFPlayer && weapon_medigun_charge_rate.GetFloat() ) { #ifdef GAME_DLL int iBoostMax = floor( pTFPlayer->m_Shared.GetMaxBuffedHealth() * 0.95); float flChargeModifier = 1.f; // Reduced charge for healing fully healed guys if ( pNewTarget->GetHealth() >= iBoostMax && ( TFGameRules() && !(TFGameRules()->InSetup() && TFGameRules()->GetActiveRoundTimer() ) ) ) { flChargeModifier *= 0.5; } int iTotalHealers = pTFPlayer->m_Shared.GetNumHealers(); if ( iTotalHealers > 1 ) { flChargeModifier /= (float)iTotalHealers; } // The resist medigun has a uber charge rate flChargeAmount *= flChargeModifier; if ( pOwner->m_Shared.GetCarryingRuneType() == RUNE_HASTE ) { flChargeAmount *= 2.f; } else if ( pOwner->m_Shared.GetCarryingRuneType() == RUNE_KING || pOwner->m_Shared.InCond( TF_COND_KING_BUFFED ) ) { flChargeAmount *= 1.5f; } if ( pNewTarget->GetHealth() >= pNewTarget->GetMaxHealth() && ( TFGameRules() && !(TFGameRules()->InSetup() && TFGameRules()->GetActiveRoundTimer() ) ) ) { CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pOwner, flChargeAmount, mult_medigun_overheal_uberchargerate ); } CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pOwner, flChargeAmount, mult_medigun_uberchargerate ); // Apply any bonus our target gives us. if ( pTarget ) { bool bInRespawnRoom = PointInRespawnRoom( pTarget, WorldSpaceCenter() ) || PointInRespawnRoom( pOwner, WorldSpaceCenter() ); if ( !bInRespawnRoom ) { CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pTarget, flChargeAmount, mult_uberchargerate_for_healer ); } } if ( TFGameRules() ) { if ( TFGameRules()->IsQuickBuildTime() ) { flChargeAmount *= 4.f; } else if ( TFGameRules()->InSetup() && TFGameRules()->GetActiveRoundTimer() ) { flChargeAmount *= 3.f; } } #endif float flNewLevel = MIN( m_flChargeLevel + flChargeAmount, 1.0 ); float flMinChargeAmount = GetMinChargeAmount(); if ( flNewLevel >= flMinChargeAmount && m_flChargeLevel < flMinChargeAmount ) { #ifdef GAME_DLL pOwner->SpeakConceptIfAllowed( MP_CONCEPT_MEDIC_CHARGEREADY ); pTFPlayer->SpeakConceptIfAllowed( MP_CONCEPT_HEALTARGET_CHARGEREADY ); #else // send a message that we've got charge // if you change this from being a client-side only event, you have to // fix ACHIEVEMENT_TF_MEDIC_KILL_WHILE_CHARGED to check the medic userid. IGameEvent *event = gameeventmanager->CreateEvent( "localplayer_chargeready" ); if ( event ) { gameeventmanager->FireEventClientSide( event ); } #endif } #ifdef CLIENT_DLL if ( GetMedigunType() == MEDIGUN_RESIST ) { // Play a sound when we tick over to a new charge level int nChargeLevel = int(floor(flNewLevel/flMinChargeAmount)); float flNextChargeLevelAmount = nChargeLevel * flMinChargeAmount; if( flNewLevel >= flNextChargeLevelAmount && m_flChargeLevel < flNextChargeLevelAmount ) { const char* pzsSoundName = CFmtStr( "WeaponMedigun_Vaccinator.Charged_tier_0%d", nChargeLevel ); if ( nChargeLevel == 1 ) { if ( m_pChargedSound != NULL ) { CSoundEnvelopeController::GetController().SoundDestroy( m_pChargedSound ); m_pChargedSound = NULL; } CLocalPlayerFilter filter; CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); m_pChargedSound = controller.SoundCreate( filter, entindex(), pzsSoundName ); controller.Play( m_pChargedSound, 1.0, 100 ); } else { pOwner->EmitSound( pzsSoundName ); } } } #endif SetChargeLevel( flNewLevel ); } else if ( IsAttachedToBuilding() ) { m_flChargeLevel = MIN( m_flChargeLevel + flChargeAmount, 1.0 ); } } } return bFound; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::ItemHolsterFrame( void ) { BaseClass::ItemHolsterFrame(); DrainCharge(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::DrainCharge( void ) { // If we're in charge release mode, drain our charge if ( m_bChargeRelease ) { CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return; int flUberTime = weapon_medigun_chargerelease_rate.GetFloat(); CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pOwner, flUberTime, add_uber_time ); float flChargeAmount = gpGlobals->frametime / flUberTime; float flExtraPlayerCost = flChargeAmount * 0.5; // Drain faster the more targets we're applying to. Extra targets count for 50% drain to still reward juggling somewhat. for ( int i = m_DetachedTargets.Count()-1; i >= 0; i-- ) { if ( m_DetachedTargets[i].hTarget == NULL || m_DetachedTargets[i].hTarget.Get() == m_hHealingTarget.Get() || !m_DetachedTargets[i].hTarget->IsAlive() || m_DetachedTargets[i].flTime < (gpGlobals->curtime - tf_invuln_time.GetFloat()) ) { m_DetachedTargets.Remove(i); } else { flChargeAmount += flExtraPlayerCost; } } SubtractChargeAndUpdateDeployState( flChargeAmount, false ); } } void CWeaponMedigun::SubtractChargeAndUpdateDeployState( float flSubtractAmount, bool bForceDrain ) { CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return; float flNewCharge = Max( m_flChargeLevel - flSubtractAmount, 0.0f ); if( GetMedigunType() == MEDIGUN_RESIST ) { if( flNewCharge <= m_flEndResistCharge ) { // If the player is holding down ATTACK2 and they have a bar of Uber left, // let them burn straight into the next bar if( (m_flEndResistCharge > 0) && m_bAttack2Down ) { float flChunkSize = GetMinChargeAmount(); int nCurrentChunk = floor(m_flChargeLevel / flChunkSize); m_flEndResistCharge = flChunkSize * Max( 0, (nCurrentChunk - 1) ); #ifdef GAME_DLL UberchargeChunkDeployed(); #endif } else { // Make sure we don't cross over too far if this is a natural drain if( !bForceDrain ) { flNewCharge = m_flEndResistCharge; } m_flEndResistCharge = 0.f; // Stop deploying m_bChargeRelease = false; m_flReleaseStartedAt = 0; m_DetachedTargets.Purge(); #ifdef GAME_DLL pOwner->ClearPunchVictims(); RecalcEffectOnTarget( pOwner ); #endif } } } m_flChargeLevel = flNewCharge; if ( !m_flChargeLevel ) { m_bChargeRelease = false; m_flReleaseStartedAt = 0; m_DetachedTargets.Purge(); #ifdef GAME_DLL pOwner->ClearPunchVictims(); RecalcEffectOnTarget( pOwner ); StopHealingOwner(); // QuickFix uber heals the target and medic #endif } } //----------------------------------------------------------------------------- // Purpose: Overloaded to handle the hold-down healing //----------------------------------------------------------------------------- void CWeaponMedigun::ItemPostFrame( void ) { CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return; // If we're lowered, we're not allowed to fire if ( CanAttack() == false ) { RemoveHealingTarget( true ); return; } #if !defined( CLIENT_DLL ) if ( AppliesModifier() ) { m_DamageModifier.SetModifier( weapon_medigun_damage_modifier.GetFloat() ); } #endif // Try to start healing m_bAttacking = false; if ( pOwner->GetMedigunAutoHeal() ) { if ( pOwner->m_nButtons & IN_ATTACK ) { if ( m_bCanChangeTarget ) { RemoveHealingTarget(); #if defined( CLIENT_DLL ) if (prediction->IsFirstTimePredicted() ) { m_bPlayingSound = false; StopHealSound(); } #endif // can't change again until we release the attack button m_bCanChangeTarget = false; } } else { m_bCanChangeTarget = true; } if ( m_bHealing && ( m_iState != WEAPON_IS_ACTIVE || pOwner->IsTaunting() ) ) { RemoveHealingTarget(); } else if ( m_bHealing || ( pOwner->m_nButtons & IN_ATTACK ) ) { PrimaryAttack(); m_bAttacking = true; } } else { if ( /*m_bChargeRelease || */ pOwner->m_nButtons & IN_ATTACK ) { PrimaryAttack(); m_bAttacking = true; } else if ( m_bHealing ) { // Detach from the player if they release the attack button. RemoveHealingTarget(); } } if ( pOwner->m_nButtons & IN_ATTACK2 ) { SecondaryAttack(); } else { m_bAttack2Down = false; } if( (pOwner->m_nButtons & IN_ATTACK3) && !m_bAttack3Down ) { CreateMedigunShield(); m_bAttack3Down = true; } else if( !(pOwner->m_nButtons & IN_ATTACK3) && m_bAttack3Down ) { m_bAttack3Down = false; } if ( pOwner->m_nButtons & IN_RELOAD && !m_bReloadDown ) { #ifdef GAME_DLL CycleResistType(); #endif m_bReloadDown = true; } else if ( !(pOwner->m_nButtons & IN_RELOAD) && m_bReloadDown ) { m_bReloadDown = false; } WeaponIdle(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CWeaponMedigun::Lower( void ) { // Stop healing if we are if ( m_bHealing ) { RemoveHealingTarget( true ); m_bAttacking = false; #ifdef CLIENT_DLL UpdateEffects(); #endif } return BaseClass::Lower(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::RemoveHealingTarget( bool bStopHealingSelf ) { CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return; // If this guy is already in our detached target list, update the time. Otherwise, add him. if ( m_bChargeRelease ) { int i = 0; for ( i = 0; i < m_DetachedTargets.Count(); i++ ) { if ( m_DetachedTargets[i].hTarget == m_hHealingTarget ) { m_DetachedTargets[i].flTime = gpGlobals->curtime; break; } } if ( i == m_DetachedTargets.Count() ) { int iIdx = m_DetachedTargets.AddToTail(); m_DetachedTargets[iIdx].hTarget = m_hHealingTarget; m_DetachedTargets[iIdx].flTime = gpGlobals->curtime; } } #ifdef GAME_DLL int nMedigunType = GetMedigunType(); if ( m_hHealingTarget ) { // HACK: For now, just deal with players if ( m_hHealingTarget->IsPlayer() ) { CTFPlayer *pTFPlayer = ToTFPlayer( m_hHealingTarget ); CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( pTFPlayer && pOwner ) { pTFPlayer->m_Shared.StopHealing( pOwner ); pOwner->SpeakConceptIfAllowed( MP_CONCEPT_MEDIC_STOPPEDHEALING, pTFPlayer->IsAlive() ? "healtarget:alive" : "healtarget:dead" ); pTFPlayer->SpeakConceptIfAllowed( MP_CONCEPT_HEALTARGET_STOPPEDHEALING ); // If we're grappled to this player, drop if ( pOwner->GetGrapplingHookTarget() == pTFPlayer ) { pOwner->SetGrapplingHookTarget( NULL ); } // Remove our passive resist if( nMedigunType == MEDIGUN_RESIST ) { ETFCond cond = g_MedigunResistConditions[ GetResistType() ].passiveCond; CBaseEntity* pProvider = pTFPlayer->m_Shared.GetConditionProvider( cond ); // Remove from our healing target if we're the provider if( pProvider == pOwner || pProvider == NULL ) { pTFPlayer->m_Shared.RemoveCond( cond ); } // Remove from ourselves if we're the provider pProvider = pOwner->m_Shared.GetConditionProvider( cond ); if( pProvider == pOwner || pProvider == NULL ) { pOwner->m_Shared.RemoveCond( cond ); } } } } } // Stop thinking - we no longer have a heal target. SetContextThink( NULL, 0, s_pszMedigunHealTargetThink ); #endif m_hHealingTarget.Set( NULL ); #ifdef GAME_DLL // See if we have The QuickFix, which adjusts our move speed based on heal target pOwner->TeamFortress_SetSpeed(); #endif // Stop the welding animation if ( m_bHealing ) { SendWeaponAnim( ACT_MP_ATTACK_STAND_POSTFIRE ); pOwner->DoAnimationEvent( PLAYERANIMEVENT_ATTACK_POST ); } #ifndef CLIENT_DLL m_DamageModifier.RemoveModifier(); #endif m_bHealing = false; } //----------------------------------------------------------------------------- // Purpose: Attempt to heal any player within range of the medikit //----------------------------------------------------------------------------- void CWeaponMedigun::PrimaryAttack( void ) { CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return; if ( !CanAttack() ) return; #ifdef GAME_DLL /* // Start boosting ourself if we're not if ( m_bChargeRelease && !m_bHealingSelf ) { pOwner->m_Shared.Heal( pOwner, GetHealRate() * 2 ); m_bHealingSelf = true; } */ #endif #if !defined (CLIENT_DLL) if ( tf_medigun_lagcomp.GetBool() ) lagcompensation->StartLagCompensation( pOwner, pOwner->GetCurrentCommand() ); #endif if ( FindAndHealTargets() ) { // Start the animation if ( !m_bHealing ) { #ifdef GAME_DLL pOwner->SpeakWeaponFire(); #endif SendWeaponAnim( ACT_MP_ATTACK_STAND_PREFIRE ); pOwner->DoAnimationEvent( PLAYERANIMEVENT_ATTACK_PRE ); } m_bHealing = true; } else { RemoveHealingTarget(); } #if !defined (CLIENT_DLL) if ( tf_medigun_lagcomp.GetBool() ) lagcompensation->FinishLagCompensation( pOwner ); #endif } //----------------------------------------------------------------------------- // Purpose: Burn charge level to generate invulnerability //----------------------------------------------------------------------------- void CWeaponMedigun::SecondaryAttack( void ) { CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return; if ( !CanAttack() ) return; CTFPlayer *pTFPlayerPatient = NULL; if ( m_hHealingTarget && m_hHealingTarget->IsPlayer() ) { pTFPlayerPatient = ToTFPlayer( m_hHealingTarget ); } // STAGING_MEDIC // Resist gun early outs if the patient and medic both have the condition (or medic with no patient has condition) if ( GetMedigunType() == MEDIGUN_RESIST ) { ETFCond uberCond = g_MedigunResistConditions[GetResistType()].uberCond; if ( pOwner->m_Shared.InCond( uberCond ) ) { if ( !pTFPlayerPatient || pTFPlayerPatient->m_Shared.InCond( uberCond ) ) { return; } } } m_bAttack2Down = true; // If using standard-uber-model-medigun, ensure they have a full charge and are not already in charge release mode bool bDenyUse = GetMedigunType() != MEDIGUN_RESIST && (m_flChargeLevel < 1.0); // If using the resist-medigun, they can shoot sooner float flChunkSize = GetMinChargeAmount(); bDenyUse |= GetMedigunType() == MEDIGUN_RESIST && m_flChargeLevel < flChunkSize; if ( bDenyUse || m_bChargeRelease ) { #ifdef CLIENT_DLL // Deny, flash if ( !m_bChargeRelease && gpGlobals->curtime >= m_flDenySecondary ) { m_flDenySecondary = gpGlobals->curtime + 0.5f; pOwner->EmitSound( "Player.DenyWeaponSelection" ); } #endif return; } if ( !pOwner->m_Shared.CanRecieveMedigunChargeEffect( GetChargeType() ) ) { if ( pOwner->m_afButtonPressed & IN_ATTACK2 #ifdef CLIENT_DLL && prediction->IsFirstTimePredicted() #endif ) { #ifdef GAME_DLL CSingleUserRecipientFilter filter( pOwner ); TFGameRules()->SendHudNotification( filter, HUD_NOTIFY_NO_INVULN_WITH_FLAG ); #else pOwner->EmitSound( "Player.DenyWeaponSelection" ); #endif } return; } // Toggle super charge state m_bChargeRelease = true; m_flReleaseStartedAt = gpGlobals->curtime; #ifdef GAME_DLL if( GetMedigunType() == MEDIGUN_RESIST ) { // We dont want to give the user a point every time they deploy an uber with the resist medigun. // Instead we give them a point for every 4 deploys UberchargeChunkDeployed(); int nCurrentChunk = floor( m_flChargeLevel / flChunkSize ); Assert( nCurrentChunk >= 1 ); CPVSFilter filter( pOwner->WorldSpaceCenter() ); pOwner->EmitSound( filter, pOwner->entindex(), CFmtStr( "WeaponMedigun_Vaccinator.Charged_tier_0%d", nCurrentChunk ) ); pOwner->EmitSound( filter, pOwner->entindex(), g_MedigunEffects[MEDIGUN_CHARGE_BULLET_RESIST].pszChargeOnSound ); } else { // Award assist point CTF_GameStats.Event_PlayerInvulnerable( pOwner ); // Award strange assist score EconEntity_OnOwnerKillEaterEvent( this, pOwner, ToTFPlayer( m_hHealingTarget ), kKillEaterEvent_UberActivated ); } // STAGING_MEDIC if ( GetMedigunType() != MEDIGUN_RESIST ) { RecalcEffectOnTarget( pOwner ); } pOwner->SpeakConceptIfAllowed( MP_CONCEPT_MEDIC_CHARGEDEPLOYED ); if ( pTFPlayerPatient ) { // STAGING_MEDIC if ( GetMedigunType() != MEDIGUN_RESIST ) { RecalcEffectOnTarget( pTFPlayerPatient ); } pTFPlayerPatient->SpeakConceptIfAllowed( MP_CONCEPT_HEALTARGET_CHARGEDEPLOYED ); } IGameEvent * event = gameeventmanager->CreateEvent( "player_chargedeployed" ); if ( event ) { event->SetInt( "userid", pOwner->GetUserID() ); if ( m_hHealingTarget && m_hHealingTarget->IsPlayer() ) { event->SetInt( "targetid", ToTFPlayer(m_hHealingTarget)->GetUserID() ); } gameeventmanager->FireEvent( event ); } // Check for achievements // Simultaneous uber charge with teammates. CTeam *pTeam = pOwner->GetTeam(); if ( pTeam ) { CUtlVector aChargingMedics; aChargingMedics.AddToTail( pOwner ); for ( int i = 0; i < pTeam->GetNumPlayers(); i++ ) { CTFPlayer *pTeamPlayer = ToTFPlayer( pTeam->GetPlayer(i) ); if ( pTeamPlayer && pTeamPlayer->IsPlayerClass( TF_CLASS_MEDIC ) && pTeamPlayer != pOwner ) { CWeaponMedigun *pWeapon = dynamic_cast ( pTeamPlayer->GetActiveWeapon() ); if ( pWeapon && pWeapon->IsReleasingCharge() ) { aChargingMedics.AddToTail( pTeamPlayer ); } } } if ( aChargingMedics.Count() >= 3 ) { // Give the achievement to all the Medics for ( int i = 0; i < aChargingMedics.Count(); i++ ) { if ( aChargingMedics[i] ) { aChargingMedics[i]->AwardAchievement( ACHIEVEMENT_TF_MEDIC_SIMUL_CHARGE ); } } } } // reset this count pOwner->HandleAchievement_Medic_AssistHeavy( NULL ); // If using the QuickFix, heal the medic if ( GetMedigunType() == MEDIGUN_QUICKFIX ) { if ( IsReleasingCharge() && !m_bHealingSelf ) { StartHealingTarget( pOwner ); m_bHealingSelf = true; } } #endif // GAME_DLL // STAGING_MEDIC if ( GetMedigunType() == MEDIGUN_RESIST ) { // Remove charge immediately and just give target and yourself the conditions m_bChargeRelease = false; #ifdef GAME_DLL int iResistDuration = 3.0f; pOwner->m_Shared.AddCond( g_MedigunResistConditions[GetResistType()].uberCond, iResistDuration, pOwner ); m_flChargeLevel -= flChunkSize; if ( pTFPlayerPatient ) { pTFPlayerPatient->m_Shared.AddCond( g_MedigunResistConditions[GetResistType()].uberCond, iResistDuration, pOwner ); } if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) { CBaseEntity *pTarget = m_hHealingTarget; CTFReviveMarker *pReviveMarker = dynamic_cast( pTarget ); if ( pReviveMarker ) { CTFPlayer *pDeadPlayer = pReviveMarker->GetOwner(); if ( pDeadPlayer ) { pReviveMarker->SetReviver( pOwner ); // fill almost to max, give a small time period so patient has time to see notifications from regular revive code pReviveMarker->AddMarkerHealth( pReviveMarker->GetMaxHealth() * 0.9f ); } } } #endif } } //----------------------------------------------------------------------------- // Purpose: Idle tests to see if we're facing a valid target for the medikit // If so, move into the "heal-able" animation. // Otherwise, move into the "not-heal-able" animation. //----------------------------------------------------------------------------- void CWeaponMedigun::WeaponIdle( void ) { if ( HasWeaponIdleTimeElapsed() ) { // Loop the welding animation if ( m_bHealing ) { SendWeaponAnim( ACT_VM_PRIMARYATTACK ); return; } return BaseClass::WeaponIdle(); } } #if defined( CLIENT_DLL ) //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::StopHealSound( bool bStopHealingSound, bool bStopNoTargetSound ) { if ( bStopHealingSound ) { StopSound( GetHealSound() ); } if ( bStopNoTargetSound ) { StopSound( "WeaponMedigun.NoTarget" ); } if ( m_pDisruptSound ) { CSoundEnvelopeController::GetController().SoundDestroy( m_pDisruptSound ); m_pDisruptSound = NULL; } } void CWeaponMedigun::StopChargeEffect( bool bImmediately ) { // Either these should both be NULL or neither NULL Assert( ( m_pChargeEffect != NULL && m_pChargeEffectOwner != NULL ) || ( m_pChargeEffect == NULL && m_pChargeEffectOwner == NULL ) ); if ( m_pChargeEffect != NULL && m_pChargeEffectOwner != NULL ) { if( bImmediately ) { m_pChargeEffectOwner->ParticleProp()->StopEmissionAndDestroyImmediately( m_pChargeEffect ); } else { m_pChargeEffectOwner->ParticleProp()->StopEmission( m_pChargeEffect ); } m_pChargeEffect = NULL; m_pChargeEffectOwner = NULL; } if ( m_pChargedSound != NULL ) { CSoundEnvelopeController::GetController().SoundDestroy( m_pChargedSound ); m_pChargedSound = NULL; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::ManageChargeEffect( void ) { C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); C_BaseEntity *pEffectOwner = this; if ( pLocalPlayer == NULL ) return; if ( pLocalPlayer == GetTFPlayerOwner() ) { pEffectOwner = pLocalPlayer->GetRenderedWeaponModel(); if ( !pEffectOwner ) { return; } } bool bOwnerTaunting = false; if ( GetTFPlayerOwner() && GetTFPlayerOwner()->m_Shared.InCond( TF_COND_TAUNTING ) == true ) { bOwnerTaunting = true; } float flMinChargeToDeploy = GetMinChargeAmount(); if ( GetTFPlayerOwner() && bOwnerTaunting == false && m_bHolstered == false && ( m_flChargeLevel >= flMinChargeToDeploy || m_bChargeRelease == true ) ) { // Did we switch from 1st to 3rd or 3rd to 1st? Taunting does this. if( pEffectOwner != m_pChargeEffectOwner ) { // Stop the current effect so we can make a new one StopChargeEffect( m_bHolstered ); } if ( m_pChargeEffect == NULL ) { const char *pszEffectName = NULL; switch( GetTFPlayerOwner()->GetTeamNumber() ) { case TF_TEAM_BLUE: pszEffectName = "medicgun_invulnstatus_fullcharge_blue"; break; case TF_TEAM_RED: pszEffectName = "medicgun_invulnstatus_fullcharge_red"; break; default: pszEffectName = ""; break; } m_pChargeEffect = pEffectOwner->ParticleProp()->Create( pszEffectName, PATTACH_POINT_FOLLOW, "muzzle" ); m_pChargeEffectOwner = pEffectOwner; } if ( m_pChargedSound == NULL ) { CLocalPlayerFilter filter; CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); m_pChargedSound = controller.SoundCreate( filter, entindex(), "WeaponMedigun.Charged" ); controller.Play( m_pChargedSound, 1.0, 100 ); } } else { StopChargeEffect( m_bHolstered ); } } //----------------------------------------------------------------------------- // Purpose: // Input : updateType - //----------------------------------------------------------------------------- void CWeaponMedigun::OnDataChanged( DataUpdateType_t updateType ) { BaseClass::OnDataChanged( updateType ); if ( m_bUpdateHealingTargets ) { UpdateEffects(); m_bUpdateHealingTargets = false; } if ( m_nOldChargeResistType != m_nChargeResistType ) { m_nOldChargeResistType = m_nChargeResistType; C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); if( GetOwner() == pLocalPlayer && pLocalPlayer ) { // Sound effect pLocalPlayer->EmitSound( "WeaponMedigun_Vaccinator.Toggle" ); } } if ( m_bHealing ) { CTFPlayer *pTarget = ToTFPlayer( m_hHealingTarget ); if ( !m_pDisruptSound && pTarget && pTarget->m_Shared.InCond( TF_COND_HEALING_DEBUFF ) ) { CLocalPlayerFilter filter; CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); m_pDisruptSound = controller.SoundCreate( filter, entindex(), "WeaponMedigun.HealingDisrupt" ); controller.Play( m_pDisruptSound, 1.f, 100.f ); } else if ( m_pDisruptSound ) { CSoundEnvelopeController::GetController().SoundDestroy( m_pDisruptSound ); m_pDisruptSound = NULL; } } else { ClientThinkList()->SetNextClientThink( GetClientHandle(), CLIENT_THINK_NEVER ); m_bPlayingSound = false; StopHealSound( true, false ); // Are they holding the attack button but not healing anyone? Give feedback. if ( IsActiveByLocalPlayer() && GetOwner() && GetOwner()->IsAlive() && m_bAttacking && GetOwner() == C_BasePlayer::GetLocalPlayer() && CanAttack() == true ) { if ( gpGlobals->curtime >= m_flNextBuzzTime ) { CLocalPlayerFilter filter; EmitSound( filter, entindex(), "WeaponMedigun.NoTarget" ); m_flNextBuzzTime = gpGlobals->curtime + 0.5f; // only buzz every so often. } } else { StopHealSound( false, true ); // Stop the "no target" sound. } } // Think? if ( m_bHealing || IsCarriedByLocalPlayer() ) { ClientThinkList()->SetNextClientThink( GetClientHandle(), CLIENT_THINK_ALWAYS ); } ManageChargeEffect(); // Find teammates that need healing if ( IsCarriedByLocalPlayer() ) { C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); if ( !pLocalPlayer || !pLocalPlayer->IsPlayerClass( TF_CLASS_MEDIC ) ) { return; } if ( pLocalPlayer == GetOwner() && hud_medicautocallers.GetBool() ) { UpdateMedicAutoCallers(); } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::ClientThink() { C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); if ( !pLocalPlayer ) { return; } // Don't show it while the player is dead. Ideally, we'd respond to m_bHealing in OnDataChanged, // but it stops sending the weapon when it's holstered, and it gets holstered when the player dies. CTFPlayer *pFiringPlayer = ToTFPlayer( GetOwnerEntity() ); if ( !pFiringPlayer || pFiringPlayer->IsPlayerDead() || pFiringPlayer->IsDormant() ) { ClientThinkList()->SetNextClientThink( GetClientHandle(), CLIENT_THINK_NEVER ); m_bPlayingSound = false; StopHealSound(); return; } // If the local player is the guy getting healed, let him know // who's healing him, and their charge level. if( m_hHealingTarget != NULL ) { if ( pLocalPlayer == m_hHealingTarget ) { pLocalPlayer->SetHealer( pFiringPlayer, m_flChargeLevel ); } // Setup whether we were last healed by the local player or by someone else (used by replay system) // since GetHealer() gets cleared out every frame before player_death events get fired. See tf_replay.cpp. C_BaseEntity *pHealingTargetEnt = m_hHealingTarget; if ( pHealingTargetEnt && pHealingTargetEnt->IsPlayer() ) { C_TFPlayer *pHealingTargetPlayer = ToTFPlayer( pHealingTargetEnt ); pHealingTargetPlayer->SetWasHealedByLocalPlayer( pFiringPlayer == pLocalPlayer ); } if ( !m_bPlayingSound ) { m_bPlayingSound = true; CLocalPlayerFilter filter; EmitSound( filter, entindex(), GetHealSound() ); } } if ( m_bOldChargeRelease != m_bChargeRelease ) { m_bOldChargeRelease = m_bChargeRelease; ForceHealingTargetUpdate(); } // If the rendered weapon has changed, we need to update our particles if ( m_hHealingTargetEffect.pOwner && pFiringPlayer->GetRenderedWeaponModel() != m_hHealingTargetEffect.pOwner ) { ForceHealingTargetUpdate(); } if ( pFiringPlayer->m_Shared.IsEnteringOrExitingFullyInvisible() ) { UpdateEffects(); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::UpdateEffects( void ) { CTFPlayer *pFiringPlayer = ToTFPlayer( GetOwnerEntity() ); if ( !pFiringPlayer ) return; C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); C_BaseEntity *pEffectOwner = this; if ( pLocalPlayer == pFiringPlayer ) { pEffectOwner = pLocalPlayer->GetRenderedWeaponModel(); } // If we're still healing and our owner changed, then we did something // like changed bool bImmediate = pEffectOwner != m_hHealingTargetEffect.pOwner && m_bHealing; // Remove all the effects if ( m_hHealingTargetEffect.pOwner ) { if ( m_hHealingTargetEffect.pEffect ) { bImmediate ? m_hHealingTargetEffect.pOwner->ParticleProp()->StopEmissionAndDestroyImmediately( m_hHealingTargetEffect.pEffect ) : m_hHealingTargetEffect.pOwner->ParticleProp()->StopEmission( m_hHealingTargetEffect.pEffect ); } if ( m_hHealingTargetEffect.pCustomEffect ) { bImmediate ? m_hHealingTargetEffect.pOwner->ParticleProp()->StopEmissionAndDestroyImmediately( m_hHealingTargetEffect.pCustomEffect ) : m_hHealingTargetEffect.pOwner->ParticleProp()->StopEmission( m_hHealingTargetEffect.pCustomEffect ); } } else { if ( m_hHealingTargetEffect.pEffect ) { m_hHealingTargetEffect.pEffect->StopEmission(); } if ( m_hHealingTargetEffect.pCustomEffect ) { m_hHealingTargetEffect.pCustomEffect->StopEmission(); } } m_hHealingTargetEffect.pOwner = NULL; m_hHealingTargetEffect.pTarget = NULL; m_hHealingTargetEffect.pEffect = NULL; m_hHealingTargetEffect.pCustomEffect = NULL; // Don't add targets if the medic is dead if ( !pEffectOwner || pFiringPlayer->IsPlayerDead() || !pFiringPlayer->IsPlayerClass( TF_CLASS_MEDIC ) || pFiringPlayer->m_Shared.IsFullyInvisible() ) return; // Add our targets // Loops through the healing targets, and make sure we have an effect for each of them if ( m_hHealingTarget ) { if ( m_hHealingTargetEffect.pTarget == m_hHealingTarget ) return; bool bReviveMarker = m_hReviveMarker && m_hReviveMarker == m_hHealingTarget; // Hack to avoid another dynamic_cast here bool bHealTargetMarker = hud_medichealtargetmarker.GetBool() && !bReviveMarker; const char *pszEffectName; if ( IsAttachedToBuilding() ) { pszEffectName = "medicgun_beam_machinery"; } else if ( pFiringPlayer->GetTeamNumber() == TF_TEAM_RED ) { if ( m_bChargeRelease ) { pszEffectName = "medicgun_beam_red_invun"; } else { if ( bHealTargetMarker && pFiringPlayer == pLocalPlayer ) { pszEffectName = "medicgun_beam_red_targeted"; } else { pszEffectName = "medicgun_beam_red"; } } } else { if ( m_bChargeRelease ) { pszEffectName = "medicgun_beam_blue_invun"; } else { if ( bHealTargetMarker && pFiringPlayer == pLocalPlayer ) { pszEffectName = "medicgun_beam_blue_targeted"; } else { pszEffectName = "medicgun_beam_blue"; } } } // Attach differently if targeting a revive marker float flVecHeightOffset = bReviveMarker ? 0.f : 50.f; ParticleAttachment_t attachType = bReviveMarker ? PATTACH_POINT_FOLLOW : PATTACH_ABSORIGIN_FOLLOW; const char *pszAttachName = bReviveMarker ? "healbeam" : NULL; CNewParticleEffect *pEffect = pEffectOwner->ParticleProp()->Create( pszEffectName, PATTACH_POINT_FOLLOW, "muzzle" ); pEffectOwner->ParticleProp()->AddControlPoint( pEffect, 1, m_hHealingTarget, attachType, pszAttachName, Vector(0.f,0.f,flVecHeightOffset) ); m_hHealingTargetEffect.pTarget = m_hHealingTarget; m_hHealingTargetEffect.pEffect = pEffect; m_hHealingTargetEffect.pOwner = pEffectOwner; // See if we have a custom particle effect that wants to add to the beam CEconItemView *pItem = m_AttributeManager.GetItem(); int iSystems = pItem->GetStaticData()->GetNumAttachedParticles( GetTeamNumber() ); for ( int i = 0; i < iSystems; i++ ) { attachedparticlesystem_t *pSystem = pItem->GetStaticData()->GetAttachedParticleData( GetTeamNumber(),i ); if ( pSystem->iCustomType == 1 ) { pEffect = pEffectOwner->ParticleProp()->Create( pSystem->pszSystemName, PATTACH_POINT_FOLLOW, "muzzle" ); pEffectOwner->ParticleProp()->AddControlPoint( pEffect, 1, m_hHealingTarget, attachType, pszAttachName, Vector(0.f,0.f,flVecHeightOffset) ); m_hHealingTargetEffect.pCustomEffect = pEffect; } } } } //----------------------------------------------------------------------------- // Purpose: Look for teammates that need healing //----------------------------------------------------------------------------- void CWeaponMedigun::UpdateMedicAutoCallers( void ) { // Find teammates that need healing if ( gpGlobals->curtime > m_flAutoCallerCheckTime ) { if ( !g_TF_PR ) { return; } C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); for( int playerIndex = 1; playerIndex <= MAX_PLAYERS; playerIndex++ ) { if ( g_TF_PR->GetTeam( playerIndex ) == GetLocalPlayerTeam() ) { C_TFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( playerIndex ) ); // Don't do this for the local player if ( pPlayer == NULL || pPlayer == pLocalPlayer ) continue; if( m_hHealingTarget != NULL ) { // Don't do this for players the medic is healing if ( pPlayer == m_hHealingTarget ) continue; } if ( pPlayer->IsAlive() ) { int iHealth = float( pPlayer->GetHealth() ) / float( pPlayer->GetMaxHealth() ) * 100; int iHealthThreshold = hud_medicautocallersthreshold.GetInt(); // If it's a healthy teammate.... if ( iHealth > iHealthThreshold ) { // Make sure we don't have them in our list if previously hurt if ( m_iAutoCallers.Find( playerIndex ) != m_iAutoCallers.InvalidIndex() ) { m_iAutoCallers.FindAndRemove( playerIndex ); continue; } } // If it's a hurt teammate.... if ( iHealth <= iHealthThreshold ) { // Make sure we're not already tracking this if ( m_iAutoCallers.Find( playerIndex ) != m_iAutoCallers.InvalidIndex()) continue; // Distance check float flDistSq = pPlayer->GetAbsOrigin().DistToSqr( pLocalPlayer->GetAbsOrigin() ); if ( flDistSq >= 1000000 ) { continue; } // Now add auto-caller pPlayer->CreateSaveMeEffect( CALLER_TYPE_AUTO ); // And track the player so we don't re-add them m_iAutoCallers.AddToTail( playerIndex ); } } } } // Throttle this check m_flAutoCallerCheckTime = gpGlobals->curtime + 0.25f; } } #endif //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CWeaponMedigun::CheckAchievementsOnHealTarget( void ) { CTFPlayer *pTFPlayer = ToTFPlayer( m_hHealingTarget ); if ( !pTFPlayer ) return; #ifdef GAME_DLL // Check for "target under fire" achievement if ( pTFPlayer->m_AchievementData.CountDamagersWithinTime(3.0) >= 4 ) { if ( GetTFPlayerOwner() ) { GetTFPlayerOwner()->AwardAchievement( ACHIEVEMENT_TF_MEDIC_HEAL_UNDER_FIRE ); } } // Check for "Engineer repairing sentrygun" achievement if ( pTFPlayer->IsPlayerClass( TF_CLASS_ENGINEER ) ) { // Has Engineer worked on his sentrygun recently? CBaseObject *pSentry = pTFPlayer->GetObjectOfType( OBJ_SENTRYGUN ); if ( pSentry && pTFPlayer->m_AchievementData.IsTargetInHistory( pSentry, 4.0 ) ) { if ( pSentry->m_AchievementData.CountDamagersWithinTime(3.0) > 0 ) { CTFPlayer *pOwner = GetTFPlayerOwner(); if ( pOwner ) { // give to medic pOwner->AwardAchievement( ACHIEVEMENT_TF_MEDIC_HEAL_ENGINEER ); // give to the engineer! pTFPlayer->AwardAchievement( ACHIEVEMENT_TF_ENGINEER_REPAIR_SENTRY_W_MEDIC ); } } } } #else // check for ACHIEVEMENT_TF_MEDIC_HEAL_CALLERS if ( pTFPlayer->m_flSaveMeExpireTime > gpGlobals->curtime ) { IGameEvent *event = gameeventmanager->CreateEvent( "player_healedmediccall" ); if ( event ) { event->SetInt( "userid", pTFPlayer->GetUserID() ); gameeventmanager->FireEventClientSide( event ); } } #endif } //----------------------------------------------------------------------------- // Purpose: Our owner has become stunned. //----------------------------------------------------------------------------- void CWeaponMedigun::OnControlStunned( void ) { BaseClass::OnControlStunned(); // Interrupt auto healing. RemoveHealingTarget( true ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CWeaponMedigun::EffectMeterShouldFlash( void ) { CTFPlayer *pPlayer = GetTFPlayerOwner(); if ( !pPlayer ) return false; if ( pPlayer && ( pPlayer->m_Shared.GetRageMeter() >= 100.0f || pPlayer->m_Shared.IsRageDraining() ) ) return true; else return false; } //----------------------------------------------------------------------------- // Purpose: UI Progress //----------------------------------------------------------------------------- float CWeaponMedigun::GetProgress( void ) { CTFPlayer *pPlayer = GetTFPlayerOwner(); if ( !pPlayer ) return 0.f; return pPlayer->m_Shared.GetRageMeter() / 100.0f; } //----------------------------------------------------------------------------- // entity_medigun_shield //----------------------------------------------------------------------------- #define MEDIGUNSHIELD_THINK_CONTEXT "CTFMedigunShield_ShieldThink" LINK_ENTITY_TO_CLASS( entity_medigun_shield, CTFMedigunShield ); PRECACHE_WEAPON_REGISTER( entity_medigun_shield ); // Network IMPLEMENT_NETWORKCLASS_ALIASED( TFMedigunShield, DT_TFMedigunShield ) BEGIN_NETWORK_TABLE( CTFMedigunShield, DT_TFMedigunShield ) END_NETWORK_TABLE() BEGIN_PREDICTION_DATA( CTFMedigunShield ) END_PREDICTION_DATA() // Data BEGIN_DATADESC( CTFMedigunShield ) #ifdef GAME_DLL DEFINE_FUNCTION( ShieldTouch ), DEFINE_THINKFUNC( ShieldThink ), #endif // GAME_DLL END_DATADESC() #ifdef GAME_DLL static const float PROJECTILE_SHIELD_ALPHA_BASE = 150.f; static const float PROJECTILE_SHIELD_ENERGY_REFILL_PULSE = 10.f; static const float PROJECTILE_SHIELD_ENERGY_MAX = 1000.f; #endif // GAME_DLL //----------------------------------------------------------------------------- // //----------------------------------------------------------------------------- CTFMedigunShield::CTFMedigunShield() { m_nBlinkCount = 0; #ifdef GAME_DLL m_pTouchLoop = NULL; #ifdef STAGING_ONLY m_bPermanentShield = false; #endif // STAGING_ONLY #endif // GAME_DLL } //----------------------------------------------------------------------------- // //----------------------------------------------------------------------------- CTFMedigunShield::~CTFMedigunShield() { #ifdef GAME_DLL if ( m_pTouchLoop ) { CSoundEnvelopeController::GetController().SoundDestroy( m_pTouchLoop ); m_pTouchLoop = NULL; } #endif // GAME_DLL } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMedigunShield::Spawn() { BaseClass::Spawn(); SetModel( "models/props_mvm/mvm_player_shield.mdl" ); SetSolid( SOLID_VPHYSICS ); SetSolidFlags( FSOLID_TRIGGER ); SetCollisionGroup( TFCOLLISION_GROUP_COMBATOBJECT ); SetBlocksLOS( false ); AddEffects( EF_NOSHADOW ); m_takedamage = DAMAGE_EVENTS_ONLY; #ifdef GAME_DLL m_flShieldEnergyLevel = PROJECTILE_SHIELD_ENERGY_MAX; SetTouch( &CTFMedigunShield::ShieldTouch ); SetContextThink( &CTFMedigunShield::ShieldThink, gpGlobals->curtime, MEDIGUNSHIELD_THINK_CONTEXT ); #else SetNextClientThink( CLIENT_THINK_ALWAYS ); #endif // GAME_DLL } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMedigunShield::Precache() { PrecacheScriptSound( "WeaponMedi_Shield.Deploy" ); PrecacheScriptSound( "WeaponMedi_Shield.Protection" ); PrecacheScriptSound( "WeaponMedi_Shield.Retract" ); PrecacheScriptSound( "WeaponMedi_Shield.Burn" ); PrecacheScriptSound( "WeaponMedi_Shield.Burn_lp" ); PrecacheModel( "models/props_mvm/mvm_player_shield.mdl" ); PrecacheModel( "models/props_mvm/mvm_player_shield2.mdl" ); BaseClass::Precache(); } #ifdef CLIENT_DLL //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMedigunShield::ClientThink() { UpdateShieldPosition(); } #endif // CLIENT_DLL //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMedigunShield::UpdateShieldPosition( void ) { CBaseEntity *pOwner = GetOwnerEntity(); if ( !pOwner ) return; // Positioning logic Vector vecForward; AngleVectors( pOwner->EyeAngles(), &vecForward ); vecForward.z = 0.f; Vector vecShieldOrigin = pOwner->GetAbsOrigin() + vecForward * 145.f; SetAbsOrigin( vecShieldOrigin ); SetAbsAngles( QAngle( 0.f, pOwner->EyeAngles().y, 0.f ) ); // Ignore pitch } #ifdef GAME_DLL //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CTFMedigunShield *CTFMedigunShield::Create( CTFPlayer *pOwner ) { if ( pOwner ) { CTFMedigunShield *pShield = static_cast< CTFMedigunShield* >( CBaseEntity::Create( "entity_medigun_shield", pOwner->GetAbsOrigin() + Vector( 0, 0, 60 ), pOwner->GetAbsAngles() ) ); if ( pShield ) { pShield->SetOwnerEntity( pOwner ); pShield->ChangeTeam( pOwner->GetTeamNumber() ); int nShieldLevel = 0; CALL_ATTRIB_HOOK_INT_ON_OTHER( pOwner, nShieldLevel, generate_rage_on_heal ); if ( nShieldLevel > 1 ) { pShield->SetModel( "models/props_mvm/mvm_player_shield2.mdl" ); } pShield->m_nSkin = pShield->GetTeamNumber() == TF_TEAM_RED ? 0 : 1; CPVSFilter filter( pOwner->WorldSpaceCenter() ); pOwner->EmitSound( filter, pOwner->entindex(), "WeaponMedi_Shield.Deploy" ); return pShield; } } return NULL; } //------------------------------------------------------------------------------ // Purpose: //------------------------------------------------------------------------------ // bool CTFMedigunShield::TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ) // { // switch( GetTeamNumber() ) // { // case TF_TEAM_RED: // if ( ( mask & CONTENTS_REDTEAM ) ) // return false; // break; // // case TF_TEAM_BLUE: // if ( ( mask & CONTENTS_BLUETEAM ) ) // return false; // break; // } // // vcollide_t *pCollide = modelinfo->GetVCollide( GetModelIndex() ); // // UTIL_ClearTrace( trace ); // // physcollision->TraceBox( ray, pCollide->solids[0], GetAbsOrigin(), GetAbsAngles(), &trace ); // // if ( trace.fraction >= 1 ) // return false; // // // return owner as trace hit // trace.m_pEnt = this; // return true; // } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CTFMedigunShield::ShouldCollide( int collisionGroup, int contentsMask ) const { if ( collisionGroup == COLLISION_GROUP_PROJECTILE || collisionGroup == TFCOLLISION_GROUP_ROCKETS || collisionGroup == TFCOLLISION_GROUP_ROCKET_BUT_NOT_WITH_OTHER_ROCKETS ) { switch( GetTeamNumber() ) { case TF_TEAM_RED: if ( ( contentsMask & CONTENTS_BLUETEAM ) ) return false; break; case TF_TEAM_BLUE: if ( ( contentsMask & CONTENTS_REDTEAM ) ) return false; break; } } return BaseClass::ShouldCollide( collisionGroup, contentsMask ); } //----------------------------------------------------------------------------- // //----------------------------------------------------------------------------- void CTFMedigunShield::StartTouch( CBaseEntity *pOther ) { BaseClass::StartTouch( pOther ); CBaseEntity *pOwner = GetOwnerEntity(); if ( !pOwner ) return; if ( !InSameTeam( pOther ) ) { CPVSFilter filter( WorldSpaceCenter() ); pOwner->EmitSound( filter, entindex(), "WeaponMedi_Shield.Burn" ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMedigunShield::ShieldTouch( CBaseEntity *pOther ) { if ( !pOther ) return; if ( !InSameTeam( pOther ) ) { // Drip pan for unknown projectiles if ( !pOther->IsDeflectable() && pOther->GetCollisionGroup() == COLLISION_GROUP_PROJECTILE ) { UTIL_Remove( pOther ); return; } CTFPlayer *pTFOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pTFOwner ) return; if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && ( pTFOwner->GetTeamNumber() == TF_TEAM_PVE_INVADERS ) ) return; int nShieldLevel = 0; CALL_ATTRIB_HOOK_INT_ON_OTHER( pTFOwner, nShieldLevel, generate_rage_on_heal ); // Damage it float flDamage = ( nShieldLevel > 1 ) ? 2.f : 1.f; CTakeDamageInfo info; info.SetAttacker( pTFOwner ); info.SetInflictor( this ); info.SetWeapon( pTFOwner->GetActiveTFWeapon() ); info.SetDamage( flDamage ); info.SetDamageType( DMG_ENERGYBEAM ); info.SetDamageCustom( TF_DMG_CUSTOM_PLASMA ); info.SetDamagePosition( pOther->EyePosition() ); pOther->TakeDamage( info ); // Slow enemy players if ( pOther->IsPlayer() ) { CTFPlayer *pTFVictim = ToTFPlayer( pOther ); if ( !pTFVictim ) return; float flStun = ( nShieldLevel > 1 ) ? 0.5f : 0.3f; pTFVictim->m_Shared.StunPlayer( 0.5f, flStun, TF_STUN_MOVEMENT, pTFOwner ); } // Sound if ( !m_pTouchLoop ) { CPVSFilter filter( GetAbsOrigin() ); CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); m_pTouchLoop = controller.SoundCreate( filter, entindex(), "WeaponMedi_Shield.Burn_lp" ); controller.Play( m_pTouchLoop, 1.0, 100 ); } } } //----------------------------------------------------------------------------- // //----------------------------------------------------------------------------- void CTFMedigunShield::EndTouch( CBaseEntity *pOther ) { BaseClass::EndTouch( pOther ); if ( m_pTouchLoop ) { CSoundEnvelopeController::GetController().SoundDestroy( m_pTouchLoop ); m_pTouchLoop = NULL; } } //----------------------------------------------------------------------------- // //----------------------------------------------------------------------------- void CTFMedigunShield::ShieldThink( void ) { CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); #ifdef STAGING_ONLY if ( !pOwner || ( !pOwner->m_Shared.IsRageDraining() && !m_bPermanentShield ) ) #else if ( !pOwner || !pOwner->m_Shared.IsRageDraining() ) #endif { RemoveShield(); return; } UpdateShieldPosition(); // Regen shield if ( m_flShieldEnergyLevel < PROJECTILE_SHIELD_ENERGY_MAX ) { m_flShieldEnergyLevel += PROJECTILE_SHIELD_ENERGY_REFILL_PULSE; m_flShieldEnergyLevel = Min( m_flShieldEnergyLevel, PROJECTILE_SHIELD_ENERGY_MAX ); } // Visuals SetRenderMode( kRenderTransAlpha ); int nShieldOpacity = PROJECTILE_SHIELD_ALPHA_BASE; // Determine alpha float flFadePoint = PROJECTILE_SHIELD_ENERGY_MAX / 2.f; if ( m_flShieldEnergyLevel <= flFadePoint ) { nShieldOpacity = (int)RemapValClamped( m_flShieldEnergyLevel, 0.f, flFadePoint, 255.f, PROJECTILE_SHIELD_ALPHA_BASE ); } // Blink when we're about to expire #ifdef STAGING_ONLY if ( pOwner->m_Shared.GetRageMeter() <= 25.f && !m_bPermanentShield ) #else if ( pOwner->m_Shared.GetRageMeter() <= 25.f ) #endif { ++m_nBlinkCount; if ( m_nBlinkCount & 0x2 ) { SetRenderColorA( PROJECTILE_SHIELD_ALPHA_BASE - 100 ); } else { SetRenderColorA( nShieldOpacity ); } // Play a retract sound if ( m_nBlinkCount == 1 ) { CPVSFilter filter( pOwner->WorldSpaceCenter() ); pOwner->EmitSound( filter, entindex(), "WeaponMedi_Shield.Retract" ); } } else { SetRenderColorA( nShieldOpacity ); } SetContextThink( &CTFMedigunShield::ShieldThink, gpGlobals->curtime + 0.1f, MEDIGUNSHIELD_THINK_CONTEXT ); } //----------------------------------------------------------------------------- // //----------------------------------------------------------------------------- void CTFMedigunShield::RemoveShield( void ) { SetTouch( NULL ); AddEffects( EF_NODRAW ); UTIL_Remove( this ); } //----------------------------------------------------------------------------- // //----------------------------------------------------------------------------- void CTFMedigunShield::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) { if ( !InSameTeam( info.GetAttacker() ) ) { CEffectData data; data.m_vOrigin = ptr->endpos - (vecDir * 8); data.m_vNormal = -vecDir; data.m_flMagnitude = RemapValClamped( info.GetDamage(), 1.f, 50.f, 0.5f, 2.f ); CPVSFilter filter( GetAbsOrigin() ); te->DispatchEffect( filter, 0.f, data.m_vOrigin, "EnergyShieldImpact", data ); // g_pEffects->EnergySplash( ptr->endpos - (vecDir * 8), -vecDir, false ); } BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator ); } //----------------------------------------------------------------------------- // //----------------------------------------------------------------------------- int CTFMedigunShield::OnTakeDamage( const CTakeDamageInfo &info ) { if ( !InSameTeam( info.GetAttacker() ) ) { CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( pOwner ) { float flDamage = info.GetDamage(); m_flShieldEnergyLevel -= flDamage; m_flShieldEnergyLevel = Max( m_flShieldEnergyLevel, 0.f ); // Add to blocked damage stat CTF_GameStats.Event_PlayerBlockedDamage( pOwner, flDamage ); // CPVSFilter filter( pOwner->WorldSpaceCenter() ); // pOwner->EmitSound( filter, entindex(), "WeaponMedi_Shield.Protection" ); pOwner->PlayDamageResistSound( 100.f, RandomFloat( 0.85f, 1.f ) ); IGameEvent *event = gameeventmanager->CreateEvent( "medigun_shield_blocked_damage" ); if ( event ) { event->SetInt( "userid", pOwner->GetUserID() ); event->SetFloat( "damage", flDamage ); gameeventmanager->FireEvent( event ); } } } return 0; } #endif // GAME_DLL