diff options
Diffstat (limited to 'game/server/tf/halloween')
84 files changed, 12401 insertions, 0 deletions
diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_approach_target.cpp b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_approach_target.cpp new file mode 100644 index 0000000..a9bfd2f --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_approach_target.cpp @@ -0,0 +1,81 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_approach_target.cpp +// The 2011 Halloween Boss +// Michael Booth, October 2011 + +#include "cbase.h" + +#include "../eyeball_boss.h" +#include "eyeball_boss_approach_target.h" +#include "eyeball_boss_launch_rockets.h" + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossApproachTarget::OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ) +{ + m_giveUpTimer.Start( 5.0f ); + m_minChaseTimer.Start( 0.5f ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossApproachTarget::Update( CEyeballBoss *me, float interval ) +{ + CBaseCombatCharacter *victim = me->GetVictim(); + CBaseCombatCharacter *closestVictim = me->FindClosestVisibleVictim(); + + if ( victim != closestVictim && m_minChaseTimer.IsElapsed() ) + { + return Done( "Noticed better victim" ); + } + + if ( !victim || !victim->IsAlive() ) + { + return Done( "Victim gone" ); + } + + if ( m_giveUpTimer.IsElapsed() ) + { + return Done( "Giving up" ); + } + + bool isVictimVisible = me->IsLineOfSightClear( victim, CBaseCombatCharacter::IGNORE_ACTORS ); + + if ( !isVictimVisible ) + { + if ( m_lingerTimer.IsElapsed() ) + { + return Done( "Lost victim" ); + } + + // wait a bit to see if we catch a glimpse of our victim again + return Continue(); + } + + m_lingerTimer.Start( 1.0f ); + + float attackRange = tf_eyeball_boss_attack_range.GetFloat(); + if ( me->IsEnraged() ) + { + attackRange *= 2.0f; + } + + if ( me->IsRangeLessThan( victim, attackRange ) ) + { + return ChangeTo( new CEyeballBossLaunchRockets, "Rocket attack!" ); + } + + // approach victim + me->GetLocomotionInterface()->SetDesiredSpeed( tf_eyeball_boss_speed.GetFloat() ); + me->GetLocomotionInterface()->Approach( victim->WorldSpaceCenter() ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CEyeballBossApproachTarget::OnEnd( CEyeballBoss *me, Action< CEyeballBoss > *nextAction ) +{ +} diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_approach_target.h b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_approach_target.h new file mode 100644 index 0000000..5c09a7b --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_approach_target.h @@ -0,0 +1,26 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_approach_target.h +// The 2011 Halloween Boss +// Michael Booth, October 2011 + +#ifndef EYEBALL_BOSS_APPROACH_TARGET_H +#define EYEBALL_BOSS_APPROACH_TARGET_H + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CEyeballBossApproachTarget : public Action< CEyeballBoss > +{ +public: + virtual ActionResult< CEyeballBoss > OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ); + virtual ActionResult< CEyeballBoss > Update( CEyeballBoss *me, float interval ); + virtual void OnEnd( CEyeballBoss *me, Action< CEyeballBoss > *nextAction ); + + virtual const char *GetName( void ) const { return "ApproachTarget"; } // return name of this action + +private: + CountdownTimer m_lingerTimer; + CountdownTimer m_giveUpTimer; + CountdownTimer m_minChaseTimer; +}; + +#endif // EYEBALL_BOSS_APPROACH_TARGET_H diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_behavior.cpp b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_behavior.cpp new file mode 100644 index 0000000..1875526 --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_behavior.cpp @@ -0,0 +1,201 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_behavior.cpp +// The 2011 Halloween Boss' top level behavior, containing all other actions as children +// Michael Booth, October 2011 + +#include "cbase.h" + +#include "tf_player.h" +#include "tf_gamerules.h" +#include "particle_parse.h" +#include "tf/halloween/eyeball_boss/teleport_vortex.h" + +#include "../eyeball_boss.h" +#include "eyeball_boss_behavior.h" +#include "eyeball_boss_emerge.h" +#include "eyeball_boss_stunned.h" + + +//--------------------------------------------------------------------------------------------- +Action< CEyeballBoss > *CEyeballBossBehavior::InitialContainedAction( CEyeballBoss *me ) +{ + return new CEyeballBossEmerge; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossBehavior::OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ) +{ + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossBehavior::Update( CEyeballBoss *me, float interval ) +{ + if ( tf_eyeball_boss_debug.GetBool() ) + { + DevMsg( "%3.2f: DPS = %3.2f\n", gpGlobals->curtime, me->GetInjuryRate() ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CEyeballBoss > CEyeballBossBehavior::OnInjured( CEyeballBoss *me, const CTakeDamageInfo &info ) +{ + CTFPlayer *attacker = ToTFPlayer( info.GetAttacker() ); + if ( attacker ) + { + if ( attacker->HasPurgatoryBuff() && m_stunCooldownTimer.IsElapsed() ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "eyeball_boss_stunned" ); + if ( event ) + { + event->SetInt( "level", me->GetLevel() ); + event->SetInt( "player_entindex", attacker->entindex() ); + gameeventmanager->FireEvent( event ); + } + + me->LogPlayerInteraction( "eyeball_stunned", attacker ); + + m_stunCooldownTimer.Start( 10.0f ); + + return TrySuspendFor( new CEyeballBossStunned, RESULT_IMPORTANT, "Hurt by Purgatory Buff!" ); + } + + // critz piss me off + if ( info.GetDamageType() & DMG_CRITICAL ) + { + me->BecomeEnraged( 5.0f ); + } + + // heavy DPS pisses me off + if ( me->GetInjuryRate() > 300.0f ) + { + me->BecomeEnraged( 5.0f ); + } + } + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CEyeballBoss > CEyeballBossBehavior::OnKilled( CEyeballBoss *me, const CTakeDamageInfo &info ) +{ + // award achievement to everyone who injured me within the last few seconds + const float deathTime = 5.0f; + const CUtlVector< CEyeballBoss::AttackerInfo > &attackerVector = me->GetAttackerVector(); + for( int i=0; i<attackerVector.Count(); ++i ) + { + if ( attackerVector[i].m_attacker != NULL && + gpGlobals->curtime - attackerVector[i].m_timestamp < deathTime ) + { + if ( !me->IsSpell() ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "eyeball_boss_killer" ); + if ( event ) + { + event->SetInt( "level", me->GetLevel() ); + event->SetInt( "player_entindex", attackerVector[i].m_attacker->entindex() ); + gameeventmanager->FireEvent( event ); + } + } + + if ( TFGameRules() && TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_VIADUCT ) ) + { + if ( !me->WasSpawnedByCheats() ) + { + attackerVector[i].m_attacker->AwardAchievement( ACHIEVEMENT_TF_HALLOWEEN_EYEBOSS_KILL ); + } + } + + me->LogPlayerInteraction( "eyeball_killer", attackerVector[i].m_attacker ); + } + } + + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_RED ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, false, APPEND_PLAYERS ); + + UTIL_LogPrintf( "HALLOWEEN: eyeball_death (max_dps %3.2f) (max_health %d) (player_count %d) (level %d)\n", me->GetMaxInjuryRate(), me->GetMaxHealth(), playerVector.Count(), me->GetLevel() ); + + return TryChangeTo( new CEyeballBossDead, RESULT_CRITICAL, "I died!" ); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossDead::OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ) +{ + int animSequence = me->LookupSequence( "death" ); + if ( animSequence ) + { + me->SetSequence( animSequence ); + me->SetPlaybackRate( 1.0f ); + me->SetCycle( 0 ); + me->ResetSequenceInfo(); + } + + me->EmitSound( "Halloween.EyeballBossStunned" ); + + m_giveUpTimer.Start( 10.0f ); + + if ( tf_eyeball_boss_debug.GetBool() ) + { + DevMsg( "Max Eyeball DPS taken = %3.2f\n", me->GetMaxInjuryRate() ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossDead::Update( CEyeballBoss *me, float interval ) +{ + float ground; + TheNavMesh->GetSimpleGroundHeight( me->WorldSpaceCenter(), &ground ); + + if ( m_giveUpTimer.IsElapsed() || ( me->WorldSpaceCenter().z - ground ) < 100.0f ) + { + // we're on the ground - pop + DispatchParticleEffect( "eyeboss_death", me->GetAbsOrigin(), me->GetAbsAngles() ); + + me->EmitSound( "Cart.Explode" ); + me->EmitSound( "Halloween.EyeballBossDie" ); + + UTIL_ScreenShake( me->GetAbsOrigin(), 25.0f, 5.0f, 5.0f, 1000.0f, SHAKE_START ); + + UTIL_Remove( me ); + + me->SetVictim( NULL ); + + if ( !me->IsSpell() ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "eyeball_boss_killed" ); + if ( event ) + { + event->SetInt( "level", me->GetLevel() ); + gameeventmanager->FireEvent( event ); + } + + // next time, I'll be tougher! + me->GainLevel(); + } + + // coat nearby players with goo + const float gooRange = 750.0f; + me->JarateNearbyPlayers( gooRange ); + + // create vortex to loot + CTeleportVortex *vortex = (CTeleportVortex *)CBaseEntity::Create( "teleport_vortex", me->GetAbsOrigin(), vec3_angle ); + if ( vortex ) + { + vortex->SetupVortex( true ); + } + } + + return Continue(); +} diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_behavior.h b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_behavior.h new file mode 100644 index 0000000..87f5649 --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_behavior.h @@ -0,0 +1,46 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_behavior.h +// The 2011 Halloween Boss +// Michael Booth, October 2011 + +#ifndef EYEBALL_BOSS_BEHAVIOR_H +#define EYEBALL_BOSS_BEHAVIOR_H + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CEyeballBossBehavior : public Action< CEyeballBoss > +{ +public: + virtual Action< CEyeballBoss > *InitialContainedAction( CEyeballBoss *me ); + + virtual ActionResult< CEyeballBoss > OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ); + virtual ActionResult< CEyeballBoss > Update( CEyeballBoss *me, float interval ); + + virtual EventDesiredResult< CEyeballBoss > OnInjured( CEyeballBoss *me, const CTakeDamageInfo &info ); + virtual EventDesiredResult< CEyeballBoss > OnKilled( CEyeballBoss *me, const CTakeDamageInfo &info ); + + virtual const char *GetName( void ) const { return "Behavior"; } // return name of this action + +private: + CountdownTimer m_stunCooldownTimer; +}; + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CEyeballBossDead : public Action< CEyeballBoss > +{ +public: + virtual ActionResult< CEyeballBoss > OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ); + virtual ActionResult< CEyeballBoss > Update( CEyeballBoss *me, float interval ); + + virtual const char *GetName( void ) const { return "Dead"; } // return name of this action + +private: + CountdownTimer m_giveUpTimer; +}; + + + +#endif // EYEBALL_BOSS_BEHAVIOR_H diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_emerge.cpp b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_emerge.cpp new file mode 100644 index 0000000..1578b81 --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_emerge.cpp @@ -0,0 +1,172 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_emerge.cpp +// The Halloween Boss emerging from the ground +// Michael Booth, October 2011 + +#include "cbase.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "nav_mesh/tf_nav_area.h" +#include "particle_parse.h" + +#include "../eyeball_boss.h" +#include "eyeball_boss_emerge.h" +#include "eyeball_boss_idle.h" + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossEmerge::OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ) +{ + if ( me->IsSpell() ) + { + // just do teleport in code + me->RemoveEffects( EF_NOINTERP | EF_NODRAW ); + + DispatchParticleEffect( "eyeboss_tp_normal", me->GetAbsOrigin(), me->GetAbsAngles() ); + + int animSequence = me->LookupSequence( "teleport_in" ); + if ( animSequence ) + { + me->SetSequence( animSequence ); + me->SetPlaybackRate( 1.0f ); + me->SetCycle( 0 ); + me->ResetSequenceInfo(); + } + } + else + { + int animSequence = me->LookupSequence( "arrives" ); + if ( animSequence ) + { + me->SetSequence( animSequence ); + me->SetPlaybackRate( 1.0f ); + me->SetCycle( 0 ); + me->ResetSequenceInfo(); + } + + DispatchParticleEffect( "halloween_boss_summon", me->GetAbsOrigin(), me->GetAbsAngles() ); + + m_groundPos = me->GetAbsOrigin(); + m_riseTimer.Start( 3.0f ); + m_emergePos = me->GetAbsOrigin() + Vector( 0, 0, 100.0f ); + + m_height = 150.0f; + me->SetAbsOrigin( m_emergePos + Vector( 0, 0, -m_height ) ); + me->EmitSound( "Halloween.HeadlessBossSpawnRumble" ); + + IGameEvent *event = gameeventmanager->CreateEvent( "eyeball_boss_summoned" ); + if ( event ) + { + event->SetInt( "level", me->GetLevel() ); + gameeventmanager->FireEvent( event ); + } + + m_killTimer.Start( 2.0f ); + + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_RED ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, false, APPEND_PLAYERS ); + + UTIL_LogPrintf( "HALLOWEEN: eyeball_spawn (max_health %d) (player_count %d) (level %d)\n", me->GetMaxHealth(), playerVector.Count(), me->GetLevel() ); + } + + return Continue(); +} + + +//---------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossEmerge::Update( CEyeballBoss *me, float interval ) +{ + if ( !m_riseTimer.IsElapsed() ) + { + me->SetAbsOrigin( m_emergePos + Vector( 0, 0, -m_height * m_riseTimer.GetRemainingTime() / m_riseTimer.GetCountdownDuration() ) ); + + if ( m_rumbleTimer.IsElapsed() ) + { + m_rumbleTimer.Start( 0.25f ); + + // shake nearby players' screens. + UTIL_ScreenShake( me->GetAbsOrigin(), 15.0f, 5.0f, 1.0f, 1000.0f, SHAKE_START ); + } + } + + if ( me->IsActivityFinished() ) + { + return ChangeTo( new CEyeballBossIdle, "Here I am!" ); + } + + // don't do any kill players or remove pipe bomb code below if I'm a spell + if ( me->IsSpell() ) + { + return Continue(); + } + + // push players away to avoid penetration issues + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + const float pushRange = 250.0f; + const float pushForce = 200.0f; + + const float deathRange = me->GetLevel() > 1 ? 200.0f : 100.0f; + + for( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *player = playerVector[i]; + + Vector toPlayer = player->EyePosition() - m_groundPos; + float range = toPlayer.NormalizeInPlace(); + + if ( range < pushRange ) + { + // make sure we push players up and away + toPlayer.z = 0.0f; + toPlayer.NormalizeInPlace(); + toPlayer.z = 1.0f; + + Vector push = pushForce * toPlayer; + + player->RemoveFlag( FL_ONGROUND ); + player->ApplyAbsVelocityImpulse( push ); + } + + // kill anyone touching the summon portal + if ( !m_killTimer.IsElapsed() ) + { + if ( range < deathRange ) + { + CTakeDamageInfo info( me, me, 2.0f * player->GetMaxHealth(), DMG_BLAST, TF_DMG_CUSTOM_PLASMA ); + player->TakeDamage( info ); + } + } + } + + // leveled-up boss fizzles any grenades/etc near his summon portal + if ( !m_killTimer.IsElapsed() && me->GetLevel() > 1 ) + { + Vector vecSize = Vector( 256, 256, 256 ); + + const int maxCollectedEntities = 1024; + CBaseEntity *pObjects[ maxCollectedEntities ]; + int count = UTIL_EntitiesInBox( pObjects, maxCollectedEntities, m_groundPos - vecSize, m_groundPos + vecSize, FL_GRENADE ); + + for( int i = 0; i < count; ++i ) + { + if ( pObjects[i] == NULL ) + continue; + + if ( pObjects[i]->IsPlayer() ) + continue; + + // Remove the enemy pipe + pObjects[i]->SetThink( &CBaseEntity::SUB_Remove ); + pObjects[i]->SetNextThink( gpGlobals->curtime ); + pObjects[i]->SetTouch( NULL ); + pObjects[i]->AddEffects( EF_NODRAW ); + } + } + + return Continue(); +} diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_emerge.h b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_emerge.h new file mode 100644 index 0000000..765e96c --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_emerge.h @@ -0,0 +1,28 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_emerge.h +// The Halloween Boss emerging from the ground +// Michael Booth, October 2011 + +#ifndef EYEBALL_BOSS_EMERGE_H +#define EYEBALL_BOSS_EMERGE_H + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CEyeballBossEmerge : public Action< CEyeballBoss > +{ +public: + virtual ActionResult< CEyeballBoss > OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ); + virtual ActionResult< CEyeballBoss > Update( CEyeballBoss *me, float interval ); + virtual const char *GetName( void ) const { return "Emerge"; } // return name of this action + +private: + CountdownTimer m_riseTimer; + CountdownTimer m_rumbleTimer; + CountdownTimer m_killTimer; + Vector m_emergePos; + Vector m_groundPos; + float m_height; +}; + + +#endif // EYEBALL_BOSS_EMERGE_H diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_emote.cpp b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_emote.cpp new file mode 100644 index 0000000..ebd9825 --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_emote.cpp @@ -0,0 +1,56 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_emote.cpp +// The 2011 Halloween Boss - play an animation +// Michael Booth, October 2011 + +#include "cbase.h" + +#include "../eyeball_boss.h" +#include "eyeball_boss_emote.h" + + +//--------------------------------------------------------------------------------------------- +CEyeballBossEmote::CEyeballBossEmote( int animationSequence, const char *soundName, Action< CEyeballBoss > *nextAction ) +{ + m_animationSequence = animationSequence; + m_soundName = soundName; + m_nextAction = nextAction; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossEmote::OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ) +{ + if ( m_animationSequence ) + { + me->SetSequence( m_animationSequence ); + me->SetPlaybackRate( 1.0f ); + me->SetCycle( 0 ); + me->ResetSequenceInfo(); + } + + if ( m_soundName ) + { + me->EmitSound( m_soundName ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossEmote::Update( CEyeballBoss *me, float interval ) +{ + if ( me->IsSequenceFinished() ) + { + if ( m_nextAction ) + { + return ChangeTo( m_nextAction ); + } + + return Done(); + } + + return Continue(); +} + diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_emote.h b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_emote.h new file mode 100644 index 0000000..889b3b9 --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_emote.h @@ -0,0 +1,28 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_emote.h +// The 2011 Halloween Boss - play an animation +// Michael Booth, October 2011 + +#ifndef EYEBALL_BOSS_EMOTE_H +#define EYEBALL_BOSS_EMOTE_H + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CEyeballBossEmote : public Action< CEyeballBoss > +{ +public: + CEyeballBossEmote( int animationSequence, const char *soundName, Action< CEyeballBoss > *nextAction = NULL ); + virtual ~CEyeballBossEmote() { } + + virtual ActionResult< CEyeballBoss > OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ); + virtual ActionResult< CEyeballBoss > Update( CEyeballBoss *me, float interval ); + + virtual const char *GetName( void ) const { return "Emote"; } // return name of this action + +private: + int m_animationSequence; + const char *m_soundName; + Action< CEyeballBoss > *m_nextAction; +}; + +#endif // EYEBALL_BOSS_EMOTE_H diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_idle.cpp b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_idle.cpp new file mode 100644 index 0000000..6bae53e --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_idle.cpp @@ -0,0 +1,242 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_idle.cpp +// The 2011 Halloween Boss +// Michael Booth, October 2011 + +#include "cbase.h" + +#include "particle_parse.h" +#include "tf_gamerules.h" + +#include "../eyeball_boss.h" +#include "eyeball_boss_idle.h" +#include "eyeball_boss_teleport.h" +#include "eyeball_boss_notice.h" +#include "eyeball_boss_emote.h" + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossIdle::OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ) +{ + m_attacker = NULL; + + m_talkTimer.Start( RandomFloat( 3.0f, 5.0f ) ); + + m_lifeTimer.Start( me->IsSpell() ? tf_eyeball_boss_lifetime_spell.GetFloat() : tf_eyeball_boss_lifetime.GetFloat() ); + + m_moveTimer.Start( RandomFloat( 10.0f, 15.0f ) ); + + m_lastWarnTime = 0.0f; + m_isLaughReady = false; + m_lastHealth = me->GetHealth(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossIdle::Update( CEyeballBoss *me, float interval ) +{ + if ( tf_eyeball_boss_debug_orientation.GetBool() ) + { + CBaseCombatCharacter *target = me->FindClosestVisibleVictim(); + + if ( target ) + { + me->GetBodyInterface()->AimHeadTowards( target ); + } + + if ( me->GetInjuryRate() > 0.0001f ) + { + DevMsg( "%3.2f: DPS = %3.2f, Max DPS = %3.2f\n", gpGlobals->curtime, me->GetInjuryRate(), me->GetMaxInjuryRate() ); + } + + return Continue(); + } + + if ( !me->IsSpell() ) + { + if ( m_lifeTimer.GetRemainingTime() < 10.0f && m_lastWarnTime > 10.0f ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "eyeball_boss_escape_imminent" ); + if ( event ) + { + event->SetInt( "level", me->GetLevel() ); + event->SetInt( "time_remaining", 10 ); + gameeventmanager->FireEvent( event ); + } + } + else if ( m_lifeTimer.GetRemainingTime() < 30.0f && m_lastWarnTime > 30.0f ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "eyeball_boss_escape_imminent" ); + if ( event ) + { + event->SetInt( "level", me->GetLevel() ); + event->SetInt( "time_remaining", 30 ); + gameeventmanager->FireEvent( event ); + } + } + else if ( m_lifeTimer.GetRemainingTime() < 60.0f && m_lastWarnTime > 60.0f ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "eyeball_boss_escape_imminent" ); + if ( event ) + { + event->SetInt( "level", me->GetLevel() ); + event->SetInt( "time_remaining", 60 ); + gameeventmanager->FireEvent( event ); + } + } + m_lastWarnTime = m_lifeTimer.GetRemainingTime(); + } + + // just leave + if ( me->IsSpell() && m_lifeTimer.IsElapsed() ) + { + return ChangeTo( new CEyeballBossEscape, "Escaping..." ); + } + + // teleport to a new place + if ( m_moveTimer.IsElapsed() && !me->IsSpell() ) + { + // get low + me->GetLocomotionInterface()->SetDesiredAltitude( 0.0f ); + + // only teleport if we're low enough for players to reach the vortex we leave behind + Vector ground = me->WorldSpaceCenter(); + if ( TheNavMesh->GetSimpleGroundHeight( me->WorldSpaceCenter(), &ground.z ) ) + { + const float maxTeleportHeight = 300.0f; + + // make sure the ground is actually reachable by the players + CNavArea *area = TheNavMesh->GetNearestNavArea( ground, true, maxTeleportHeight * 1.5f ); + + if ( area ) + { + if ( me->WorldSpaceCenter().z - ground.z < maxTeleportHeight ) + { + if ( m_lifeTimer.IsElapsed() ) + { + return ChangeTo( new CEyeballBossEscape, "Escaping..." ); + } + + m_moveTimer.Start( RandomFloat( 10.0f, 15.0f ) ); + + return SuspendFor( new CEyeballBossTeleport, "Moving..." ); + } + } + } + } + else + { + // resume hovering + me->GetLocomotionInterface()->SetDesiredAltitude( tf_eyeball_boss_hover_height.GetFloat() ); + } + + CBaseCombatCharacter *victim = me->FindClosestVisibleVictim(); + if ( victim ) + { + me->SetVictim( victim ); + + return SuspendFor( new CEyeballBossNotice, "Target found..." ); + } + + if ( m_attacker != NULL ) + { + // look at attacker that just injured us + me->GetBodyInterface()->AimHeadTowards( m_attacker ); + + m_attacker = NULL; + m_lookAroundTimer.Start( RandomFloat( 0.5f, 2.0f ) ); + } + else + { + if ( m_lookAroundTimer.IsElapsed() ) + { + // look around + m_lookAroundTimer.Start( RandomFloat( 2.0f, 4.0f ) ); + + Vector target; + + SinCos( RandomFloat( -3.141592f, 3.141592f ), &target.y, &target.x ); + target.z = 0.0f; + + me->GetBodyInterface()->AimHeadTowards( me->GetAbsOrigin() + 100.0f * target ); + } + + // we have no target - if someone died recently, laugh + if ( m_isLaughReady ) + { + m_isLaughReady = false; + return SuspendFor( new CEyeballBossEmote( me->LookupSequence( "laugh" ), "Halloween.EyeballBossLaugh" ), "Taunt our victim" ); + } + } + + int animSequence = 0; + + if ( me->IsEnraged() ) + { + animSequence = me->LookupSequence( "lookaround3" ); + } + else if ( me->IsGrumpy() ) + { + animSequence = me->LookupSequence( "lookaround2" ); + } + else + { + animSequence = me->LookupSequence( "lookaround1" ); + } + + if ( animSequence ) + { + if ( me->GetSequence() != animSequence || me->IsSequenceFinished() ) + { + me->SetSequence( animSequence ); + me->SetPlaybackRate( 1.0f ); + me->SetCycle( 0 ); + me->ResetSequenceInfo(); + } + } + + if ( m_talkTimer.IsElapsed() ) + { + if ( me->IsEnraged() ) + { + me->EmitSound( "Halloween.EyeballBossRage" ); + m_talkTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + } + else + { + me->EmitSound( "Halloween.EyeballBossIdle" ); + m_talkTimer.Start( RandomFloat( 3.0f, 5.0f ) ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossIdle::OnResume( CEyeballBoss *me, Action< CEyeballBoss > *interruptingAction ) +{ + m_talkTimer.Start( RandomFloat( 3.0f, 5.0f ) ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CEyeballBoss > CEyeballBossIdle::OnInjured( CEyeballBoss *me, const CTakeDamageInfo &info ) +{ + m_attacker = info.GetAttacker(); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CEyeballBoss > CEyeballBossIdle::OnOtherKilled( CEyeballBoss *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info ) +{ + m_isLaughReady = true; + return TryContinue(); +} + diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_idle.h b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_idle.h new file mode 100644 index 0000000..99f853e --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_idle.h @@ -0,0 +1,37 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_idle.h +// The 2011 Halloween Boss +// Michael Booth, October 2011 + +#ifndef EYEBALL_BOSS_IDLE_H +#define EYEBALL_BOSS_IDLE_H + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CEyeballBossIdle : public Action< CEyeballBoss > +{ +public: + virtual ActionResult< CEyeballBoss > OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ); + virtual ActionResult< CEyeballBoss > Update( CEyeballBoss *me, float interval ); + + virtual ActionResult< CEyeballBoss > OnResume( CEyeballBoss *me, Action< CEyeballBoss > *interruptingAction ); + + virtual EventDesiredResult< CEyeballBoss > OnInjured( CEyeballBoss *me, const CTakeDamageInfo &info ); + virtual EventDesiredResult< CEyeballBoss > OnOtherKilled( CEyeballBoss *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info ); + + virtual const char *GetName( void ) const { return "Idle"; } // return name of this action + +private: + CountdownTimer m_lookAroundTimer; + PathFollower m_path; + CountdownTimer m_repathTimer; + CHandle< CBaseEntity > m_attacker; + CountdownTimer m_talkTimer; + CountdownTimer m_lifeTimer; + CountdownTimer m_moveTimer; + float m_lastWarnTime; + int m_lastHealth; + bool m_isLaughReady; +}; + +#endif // EYEBALL_BOSS_IDLE_H
\ No newline at end of file diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_launch_rockets.cpp b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_launch_rockets.cpp new file mode 100644 index 0000000..1f41288 --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_launch_rockets.cpp @@ -0,0 +1,182 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_launch_rockets.cpp +// The 2011 Halloween Boss +// Michael Booth, October 2011 + +#include "cbase.h" + +#include "tf_projectile_rocket.h" +#include "tf_obj_sentrygun.h" + +#include "../eyeball_boss.h" +#include "eyeball_boss_launch_rockets.h" + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossLaunchRockets::OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ) +{ + if ( me->GetVictim() == NULL ) + { + return Done( "No target" ); + } + + m_lastTargetPosition = me->GetVictim()->GetAbsOrigin(); + + const char *firingAnimation = NULL; + + if ( me->IsEnraged() ) + { + m_rocketsLeft = 3; + m_initialDelayTimer.Start( 0.25f ); + firingAnimation = "firing3"; + + me->EmitSound( "Halloween.EyeballBossRage" ); + } + else if ( me->IsGrumpy() ) + { + m_rocketsLeft = 3; + m_initialDelayTimer.Start( 0.25f ); + firingAnimation = "firing2"; + } + else + { + m_rocketsLeft = 1; + m_initialDelayTimer.Start( 0.5f ); + firingAnimation = "firing1"; + } + + int animSequence = me->LookupSequence( firingAnimation ); + if ( animSequence ) + { + me->SetSequence( animSequence ); + me->SetPlaybackRate( 1.0f ); + me->SetCycle( 0 ); + me->ResetSequenceInfo(); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossLaunchRockets::Update( CEyeballBoss *me, float interval ) +{ + if ( me->GetVictim() && me->GetVictim()->IsAlive() ) + { + m_lastTargetPosition = me->GetVictim()->GetAbsOrigin(); + + // target upper part of sentry guns + if ( me->GetVictim()->IsBaseObject() ) + { + CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( const_cast< CBaseCombatCharacter * >( me->GetVictim() ) ); + if ( sentry ) + { + m_lastTargetPosition = sentry->EyePosition(); + } + } + + // circle around them + Vector forward, right; + me->GetVectors( &forward, &right, NULL ); + + me->GetLocomotionInterface()->SetDesiredSpeed( tf_eyeball_boss_speed.GetFloat() ); + + if ( !me->IsSpell() ) + { + if ( me->GetVictim()->entindex() & 0x1 ) + { + me->GetLocomotionInterface()->Approach( 100.0f * right + me->WorldSpaceCenter() ); + } + else + { + me->GetLocomotionInterface()->Approach( -100.0f * right + me->WorldSpaceCenter() ); + } + } + + me->GetBodyInterface()->AimHeadTowards( me->GetVictim() ); + } + else + { + me->GetBodyInterface()->AimHeadTowards( m_lastTargetPosition ); + } + + // fire rockets + if ( m_initialDelayTimer.IsElapsed() && m_launchTimer.IsElapsed() ) + { + --m_rocketsLeft; + m_launchTimer.Start( 0.3f ); + + const float rocketDamage = 50.0f; + + // if I'm enraged, shoot full speed rockets, otherwise shoot slow ones + float speedFactor = me->IsEnraged() ? 1.0f : 0.3f; + float rocketSpeed = 1100.0f * speedFactor; + + Vector targetSpot = m_lastTargetPosition; + + if ( me->IsEnraged() && me->GetVictim() ) + { + // lead our target + float rangeBetween = me->GetRangeTo( targetSpot ); + + const float veryCloseRange = 150.0f; + if ( rangeBetween > veryCloseRange ) + { + float timeToTravel = rangeBetween / rocketSpeed; + + Vector leadOffset = timeToTravel * me->GetVictim()->GetAbsVelocity(); + + CTraceFilterNoNPCsOrPlayer filter( me, COLLISION_GROUP_NONE ); + trace_t result; + + UTIL_TraceLine( me->WorldSpaceCenter(), me->GetVictim()->GetAbsOrigin() + leadOffset, MASK_SOLID_BRUSHONLY, &filter, &result ); + + if ( result.DidHit() ) + { + const float errorTolerance = 300.0f; + if ( ( result.endpos - targetSpot ).IsLengthGreaterThan( errorTolerance ) ) + { + // rocket is going to hit an obstruction not near our victim - just aim right for them and hope for splash + leadOffset = vec3_origin; + } + } + + targetSpot = me->GetVictim()->GetAbsOrigin() + leadOffset; + } + } + + QAngle launchAngles; + Vector toTarget = targetSpot - me->WorldSpaceCenter(); + VectorAngles( toTarget, launchAngles ); + + CBaseEntity *pScorer = me->GetOwnerEntity() ? me->GetOwnerEntity() : me; + + CTFProjectile_Rocket *pRocket = CTFProjectile_Rocket::Create( me, me->WorldSpaceCenter(), launchAngles, me, pScorer ); + if ( pRocket ) + { + pRocket->SetModel( "models/props_halloween/eyeball_projectile.mdl" ); + + Vector forward; + AngleVectors( launchAngles, &forward, NULL, NULL ); + + Vector vecVelocity = forward * rocketSpeed; + pRocket->SetAbsVelocity( vecVelocity ); + pRocket->SetupInitialTransmittedGrenadeVelocity( vecVelocity ); + + pRocket->EmitSound( "Weapon_RPG.SingleCrit" ); + pRocket->SetDamage( rocketDamage ); + pRocket->SetCritical( !me->IsSpell() ); + pRocket->SetEyeBallRocket( !me->IsSpell() ); + pRocket->SetSpell( me->IsSpell() ); + pRocket->ChangeTeam( me->GetTeamNumber() ); + } + + if ( !m_rocketsLeft ) + { + return Done(); + } + } + + return Continue(); +} + diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_launch_rockets.h b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_launch_rockets.h new file mode 100644 index 0000000..f2b2772 --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_launch_rockets.h @@ -0,0 +1,30 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_launch_rockets.h +// The 2011 Halloween Boss +// Michael Booth, October 2011 + +#ifndef EYEBALL_BOSS_LAUNCH_ROCKETS_H +#define EYEBALL_BOSS_LAUNCH_ROCKETS_H + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CEyeballBossLaunchRockets : public Action< CEyeballBoss > +{ +public: + virtual ActionResult< CEyeballBoss > OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ); + virtual ActionResult< CEyeballBoss > Update( CEyeballBoss *me, float interval ); + + virtual const char *GetName( void ) const { return "LaunchRockets"; } // return name of this action + +private: + CountdownTimer m_initialDelayTimer; + + CountdownTimer m_launchTimer; + int m_rocketsLeft; + + Vector m_lastTargetPosition; +}; + + +#endif // EYEBALL_BOSS_LAUNCH_ROCKETS_H diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_notice.cpp b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_notice.cpp new file mode 100644 index 0000000..85e284d --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_notice.cpp @@ -0,0 +1,43 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_notice.cpp +// The 2011 Halloween Boss +// Michael Booth, October 2011 + +#include "cbase.h" + +#include "../eyeball_boss.h" +#include "eyeball_boss_notice.h" +#include "eyeball_boss_approach_target.h" + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossNotice::OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ) +{ + m_timer.Start( 0.25f ); + + me->EmitSound( "Halloween.EyeballBossBecomeAlert" ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossNotice::Update( CEyeballBoss *me, float interval ) +{ + CBaseEntity *victim = me->GetVictim(); + + if ( !victim ) + { + return Done( "Victim gone" ); + } + + me->GetBodyInterface()->AimHeadTowards( victim ); + + if ( m_timer.IsElapsed() ) + { + return ChangeTo( new CEyeballBossApproachTarget, "Chasing victim" ); + } + + return Continue(); +} + diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_notice.h b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_notice.h new file mode 100644 index 0000000..658cbf2 --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_notice.h @@ -0,0 +1,23 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_notice.h +// The 2011 Halloween Boss +// Michael Booth, October 2011 + +#ifndef EYEBALL_BOSS_NOTICE_H +#define EYEBALL_BOSS_NOTICE_H + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CEyeballBossNotice : public Action< CEyeballBoss > +{ +public: + virtual ActionResult< CEyeballBoss > OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ); + virtual ActionResult< CEyeballBoss > Update( CEyeballBoss *me, float interval ); + + virtual const char *GetName( void ) const { return "Notice"; } // return name of this action + +private: + CountdownTimer m_timer; +}; + +#endif // EYEBALL_BOSS_NOTICE_H diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_stunned.cpp b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_stunned.cpp new file mode 100644 index 0000000..8cfc342 --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_stunned.cpp @@ -0,0 +1,60 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_stunned.cpp +// The 2011 Halloween Boss +// Michael Booth, October 2011 + +#include "cbase.h" + +#include "../eyeball_boss.h" +#include "eyeball_boss_stunned.h" + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossStunned::OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ) +{ + m_stunTimer.Start( 5.0f ); + + int animSequence = me->LookupSequence( "stunned" ); + if ( animSequence ) + { + me->SetSequence( animSequence ); + me->SetPlaybackRate( 1.0f ); + me->SetCycle( 0 ); + me->ResetSequenceInfo(); + } + + me->EmitSound( "Halloween.EyeballBossStunned" ); + + // limit the total amount of damage we can take while stunned + me->SetDamageLimit( me->GetMaxHealth() / 3 ); + + // sink + me->GetLocomotionInterface()->SetDesiredAltitude( 0.0f ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossStunned::Update( CEyeballBoss *me, float interval ) +{ + if ( m_stunTimer.IsElapsed() ) + { + // get mad and retaliate + me->BecomeEnraged( 20.0f ); + + return Done(); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CEyeballBossStunned::OnEnd( CEyeballBoss *me, Action< CEyeballBoss > *nextAction ) +{ + me->RemoveDamageLimit(); + + // resume hovering + me->GetLocomotionInterface()->SetDesiredAltitude( tf_eyeball_boss_hover_height.GetFloat() ); +} diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_stunned.h b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_stunned.h new file mode 100644 index 0000000..9bf26f2 --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_stunned.h @@ -0,0 +1,31 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_stunned.h +// The 2011 Halloween Boss +// Michael Booth, October 2011 + +#ifndef EYEBALL_BOSS_STUNNED_H +#define EYEBALL_BOSS_STUNNED_H + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CEyeballBossStunned : public Action< CEyeballBoss > +{ +public: + virtual ActionResult< CEyeballBoss > OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ); + virtual ActionResult< CEyeballBoss > Update( CEyeballBoss *me, float interval ); + virtual void OnEnd( CEyeballBoss *me, Action< CEyeballBoss > *nextAction ); + + virtual EventDesiredResult< CEyeballBoss > OnInjured( CEyeballBoss *me, const CTakeDamageInfo &info ) + { + // don't get stunned while stunned + return TryToSustain( RESULT_CRITICAL ); + } + + virtual const char *GetName( void ) const { return "Stunned"; } // return name of this action + +private: + CountdownTimer m_stunTimer; + float m_spinRate; +}; + +#endif // EYEBALL_BOSS_STUNNED_H diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_teleport.cpp b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_teleport.cpp new file mode 100644 index 0000000..859e5f5 --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_teleport.cpp @@ -0,0 +1,148 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_teleport.cpp +// The 2011 Halloween Boss +// Michael Booth, October 2011 + +#include "cbase.h" + +#include "particle_parse.h" +#include "tf/halloween/eyeball_boss/teleport_vortex.h" +#include "../eyeball_boss.h" +#include "eyeball_boss_teleport.h" +#include "player_vs_environment/monster_resource.h" + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossTeleport::OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ) +{ + m_state = TELEPORTING_OUT; + + int animSequence = me->LookupSequence( "teleport_out" ); + if ( animSequence ) + { + me->SetSequence( animSequence ); + me->SetPlaybackRate( 1.0f ); + me->SetCycle( 0 ); + me->ResetSequenceInfo(); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossTeleport::Update( CEyeballBoss *me, float interval ) +{ + if ( me->IsSequenceFinished() ) + { + switch( m_state ) + { + case TELEPORTING_OUT: + { + CTeleportVortex *vortex = (CTeleportVortex *)CBaseEntity::Create( "teleport_vortex", me->GetAbsOrigin(), vec3_angle ); + if ( vortex ) + { + vortex->SetupVortex( false ); + } + + DispatchParticleEffect( "eyeboss_tp_normal", me->GetAbsOrigin(), me->GetAbsAngles() ); + + me->EmitSound( "Halloween.EyeballBossTeleport" ); + + me->AddEffects( EF_NOINTERP | EF_NODRAW ); + + me->SetAbsOrigin( me->PickNewSpawnSpot() + Vector( 0, 0, 75.0f ) ); + + // wait on the other side for a moment + m_state = TELEPORTING_IN; + } + break; + + case TELEPORTING_IN: + { + me->RemoveEffects( EF_NOINTERP | EF_NODRAW ); + + DispatchParticleEffect( "eyeboss_tp_normal", me->GetAbsOrigin(), me->GetAbsAngles() ); + + int animSequence = me->LookupSequence( "teleport_in" ); + if ( animSequence ) + { + me->SetSequence( animSequence ); + me->SetPlaybackRate( 1.0f ); + me->SetCycle( 0 ); + me->ResetSequenceInfo(); + } + + m_state = DONE; + } + break; + + case DONE: + return Done(); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossEscape::OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ) +{ + int animSequence = me->LookupSequence( "escape" ); + if ( animSequence ) + { + me->SetSequence( animSequence ); + me->SetPlaybackRate( 1.0f ); + me->SetCycle( 0 ); + me->ResetSequenceInfo(); + } + + me->EmitSound( "Halloween.EyeballBossLaugh" ); + + UTIL_LogPrintf( "HALLOWEEN: eyeball_escaped (max_dps %3.2f) (health %d) (level %d)\n", me->GetMaxInjuryRate(), me->GetHealth(), me->GetLevel() ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CEyeballBoss > CEyeballBossEscape::Update( CEyeballBoss *me, float interval ) +{ + if ( me->IsSequenceFinished() ) + { + if ( me->IsSpell() ) + { + me->EmitSound( "Halloween.spell_spawn_boss_disappear" ); + } + + DispatchParticleEffect( "eyeboss_tp_escape", me->GetAbsOrigin(), me->GetAbsAngles() ); + + if ( g_pMonsterResource ) + { + g_pMonsterResource->HideBossHealthMeter(); + } + + UTIL_Remove( me ); + + me->SetVictim( NULL ); + + if ( !me->IsSpell() ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "eyeball_boss_escaped" ); + if ( event ) + { + event->SetInt( "level", me->GetLevel() ); + gameeventmanager->FireEvent( event ); + } + } + + // reset back to normal level + me->ResetLevel(); + + return Done(); + } + + return Continue(); +} diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_teleport.h b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_teleport.h new file mode 100644 index 0000000..03cd088 --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_teleport.h @@ -0,0 +1,45 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss_teleport.h +// The 2011 Halloween Boss +// Michael Booth, October 2011 + +#ifndef EYEBALL_BOSS_TELEPORT_H +#define EYEBALL_BOSS_TELEPORT_H + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CEyeballBossTeleport : public Action< CEyeballBoss > +{ +public: + virtual ActionResult< CEyeballBoss > OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ); + virtual ActionResult< CEyeballBoss > Update( CEyeballBoss *me, float interval ); + + virtual const char *GetName( void ) const { return "Teleport"; } // return name of this action + +private: + enum TeleportState + { + TELEPORTING_OUT, + TELEPORTING_IN, + DONE + }; + TeleportState m_state; +}; + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CEyeballBossEscape : public Action< CEyeballBoss > +{ +public: + virtual ActionResult< CEyeballBoss > OnStart( CEyeballBoss *me, Action< CEyeballBoss > *priorAction ); + virtual ActionResult< CEyeballBoss > Update( CEyeballBoss *me, float interval ); + + virtual const char *GetName( void ) const { return "Escape"; } // return name of this action + +private: + CountdownTimer m_timer; +}; + + +#endif // EYEBALL_BOSS_TELEPORT_H diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_boss.cpp b/game/server/tf/halloween/eyeball_boss/eyeball_boss.cpp new file mode 100644 index 0000000..1aff63b --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_boss.cpp @@ -0,0 +1,1050 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss.cpp +// The 2011 Halloween Boss +// Michael Booth, October 2011 + +#include "cbase.h" + +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "tf_projectile_arrow.h" +#include "tf_weapon_grenade_pipebomb.h" +#include "tf_ammo_pack.h" +#include "nav_mesh/tf_nav_area.h" +#include "NextBot/Path/NextBotChasePath.h" +#include "econ_wearable.h" +#include "team_control_point_master.h" +#include "particle_parse.h" +#include "nav_mesh/tf_path_follower.h" +#include "tf_obj_sentrygun.h" +#include "bot/map_entities/tf_spawner.h" +#include "tf_fx.h" +#include "player_vs_environment/monster_resource.h" + +#include "eyeball_boss.h" +#include "eyeball_behavior/eyeball_boss_behavior.h" +#include "halloween/zombie/zombie.h" + + +ConVar tf_eyeball_boss_debug( "tf_eyeball_boss_debug", "0", FCVAR_CHEAT ); +ConVar tf_eyeball_boss_debug_orientation( "tf_eyeball_boss_debug_orientation", "0", FCVAR_CHEAT ); + +ConVar tf_eyeball_boss_lifetime( "tf_eyeball_boss_lifetime", "120", FCVAR_CHEAT ); +ConVar tf_eyeball_boss_lifetime_spell( "tf_eyeball_boss_lifetime_spell", "8", FCVAR_CHEAT ); + +ConVar tf_eyeball_boss_speed( "tf_eyeball_boss_speed", "250", FCVAR_CHEAT ); + +ConVar tf_eyeball_boss_hover_height( "tf_eyeball_boss_hover_height", "200", FCVAR_CHEAT ); + +ConVar tf_eyeball_boss_acceleration( "tf_eyeball_boss_acceleration", "500", FCVAR_CHEAT ); +ConVar tf_eyeball_boss_horiz_damping( "tf_eyeball_boss_horiz_damping", "2", FCVAR_CHEAT ); +ConVar tf_eyeball_boss_vert_damping( "tf_eyeball_boss_vert_damping", "1", FCVAR_CHEAT ); + +ConVar tf_eyeball_boss_attack_range( "tf_eyeball_boss_attack_range", "750", FCVAR_CHEAT ); + +ConVar tf_eyeball_boss_health_base( "tf_eyeball_boss_health_base", "8000", FCVAR_CHEAT ); +ConVar tf_eyeball_boss_health_per_player( "tf_eyeball_boss_health_per_player", "400", FCVAR_CHEAT ); +extern ConVar tf_halloween_bot_min_player_count; + +ConVar tf_eyeball_boss_health_at_level_2( "tf_eyeball_boss_health_at_level_2", "17000", FCVAR_CHEAT ); +ConVar tf_eyeball_boss_health_per_level( "tf_eyeball_boss_health_per_level", "3000", FCVAR_CHEAT ); + + +LINK_ENTITY_TO_CLASS( eyeball_boss, CEyeballBoss ); + +IMPLEMENT_SERVERCLASS_ST( CEyeballBoss, DT_EyeballBoss ) + + SendPropExclude( "DT_BaseEntity", "m_angRotation" ), // client has its own orientation logic + SendPropExclude( "DT_BaseEntity", "m_angAbsRotation" ), // client has its own orientation logic + SendPropVector( SENDINFO( m_lookAtSpot ), 0, SPROP_COORD ), + SendPropInt( SENDINFO( m_attitude ) ), + +END_SEND_TABLE() + + +int CEyeballBoss::m_level = 1; + +IMPLEMENT_AUTO_LIST( IEyeballBossAutoList ); + +//----------------------------------------------------------------------------------------------------- +//----------------------------------------------------------------------------------------------------- +CEyeballBoss::CEyeballBoss() +{ + ALLOCATE_INTENTION_INTERFACE( CEyeballBoss ); + + m_locomotor = new CEyeballBossLocomotion( this ); + m_body = new CEyeballBossBody( this ); + m_vision = new CDisableVision( this ); + + m_eyeOffset = vec3_origin; + m_target = NULL; + m_rageTimer.Invalidate(); + m_victim = NULL; + m_lookAtSpot = vec3_origin; + m_attitude = EYEBALL_CALM; + m_damageLimit = -1; +} + + +//----------------------------------------------------------------------------------------------------- +CEyeballBoss::~CEyeballBoss() +{ + DEALLOCATE_INTENTION_INTERFACE; + + if ( m_vision ) + delete m_vision; + + if ( m_body ) + delete m_body; + + if ( m_locomotor ) + delete m_locomotor; + + IGameEvent *event = gameeventmanager->CreateEvent( "recalculate_truce" ); + if ( event ) + { + gameeventmanager->FireEvent( event, true ); + } +} + +void CEyeballBoss::PrecacheEyeballBoss() +{ + PrecacheModel( "models/props_halloween/halloween_demoeye.mdl" ); + PrecacheModel( "models/props_halloween/eyeball_projectile.mdl" ); + + PrecacheScriptSound( "Halloween.EyeballBossIdle" ); + PrecacheScriptSound( "Halloween.EyeballBossBecomeAlert" ); + PrecacheScriptSound( "Halloween.EyeballBossAcquiredVictim" ); + PrecacheScriptSound( "Halloween.EyeballBossStunned" ); + PrecacheScriptSound( "Halloween.EyeballBossStunRecover" ); + PrecacheScriptSound( "Halloween.EyeballBossLaugh" ); + PrecacheScriptSound( "Halloween.EyeballBossBigLaugh" ); + PrecacheScriptSound( "Halloween.EyeballBossDie" ); + PrecacheScriptSound( "Halloween.EyeballBossEscapeSoon" ); + PrecacheScriptSound( "Halloween.EyeballBossEscapeImminent" ); + PrecacheScriptSound( "Halloween.EyeballBossEscaped" ); + PrecacheScriptSound( "Halloween.EyeballBossDie" ); + PrecacheScriptSound( "Halloween.EyeballBossTeleport" ); + PrecacheScriptSound( "Halloween.HeadlessBossSpawnRumble" ); + + PrecacheScriptSound( "Halloween.EyeballBossBecomeEnraged" ); + PrecacheScriptSound( "Halloween.EyeballBossRage" ); + PrecacheScriptSound( "Halloween.EyeballBossCalmDown" ); + PrecacheScriptSound( "Halloween.spell_spawn_boss_disappear" ); + + PrecacheScriptSound( "Halloween.MonoculusBossSpawn" ); + PrecacheScriptSound( "Halloween.MonoculusBossDeath" ); + + PrecacheParticleSystem( "eyeboss_death" ); + PrecacheParticleSystem( "eyeboss_aura_angry" ); + PrecacheParticleSystem( "eyeboss_aura_grumpy" ); + PrecacheParticleSystem( "eyeboss_aura_calm" ); + PrecacheParticleSystem( "eyeboss_aura_stunned" ); + PrecacheParticleSystem( "eyeboss_tp_normal" ); + PrecacheParticleSystem( "eyeboss_tp_escape" ); + PrecacheParticleSystem( "eyeboss_team_red" ); + PrecacheParticleSystem( "eyeboss_team_blue" ); +} + +//----------------------------------------------------------------------------------------------------- +void CEyeballBoss::Precache() +{ + BaseClass::Precache(); + + // always allow late precaching, so we don't pay the cost of the + // Halloween Boss for the entire year + + bool bAllowPrecache = CBaseEntity::IsPrecacheAllowed(); + CBaseEntity::SetAllowPrecache( true ); + + PrecacheEyeballBoss(); + + CBaseEntity::SetAllowPrecache( bAllowPrecache ); +} + + +//----------------------------------------------------------------------------------------------------- +void CEyeballBoss::Spawn( void ) +{ + Precache(); + + BaseClass::Spawn(); + + SetModel( "models/props_halloween/halloween_demoeye.mdl" ); + + int health = tf_eyeball_boss_health_base.GetInt(); + + if ( m_level > 1 ) + { + // the Boss was defeated last time - he's tougher this time + health = tf_eyeball_boss_health_at_level_2.GetInt(); + + health += tf_eyeball_boss_health_per_level.GetInt() * ( m_level - 2 ); + } + else + { + // scale the boss' health with the player count + int totalPlayers = GetGlobalTFTeam( TF_TEAM_BLUE )->GetNumPlayers() + GetGlobalTFTeam( TF_TEAM_RED )->GetNumPlayers(); + + if ( totalPlayers > tf_halloween_bot_min_player_count.GetInt() ) + { + health += ( totalPlayers - tf_halloween_bot_min_player_count.GetInt() ) * tf_eyeball_boss_health_per_player.GetInt(); + } + } + + SetHealth( health ); + SetMaxHealth( health ); + + m_homePos = GetAbsOrigin(); + + Vector mins( -50, -50, -50 ); + Vector maxs( 50, 50, 50 ); + CollisionProp()->SetSurroundingBoundsType( USE_SPECIFIED_BOUNDS, &mins, &maxs ); + CollisionProp()->SetCollisionBounds( mins, maxs ); + + m_lookAtSpot = vec3_origin; + + CBaseEntity *spawnPoint = NULL; + while( ( spawnPoint = gEntList.FindEntityByClassname( spawnPoint, "info_target" ) ) != NULL ) + { + if ( FStrEq( STRING( spawnPoint->GetEntityName() ), "spawn_boss_alt" ) ) + { + m_spawnSpotVector.AddToTail( spawnPoint ); + } + } + + if ( m_spawnSpotVector.Count() == 0 ) + { + Warning( "No info_target entities named 'spawn_boss_alt' found!\n" ); + } + + // show Boss' health meter on HUD + if ( IsSpell() ) + { + // this will force particle effect on the boss + m_attitude = GetTeamNumber() == TF_TEAM_RED ? EYEBALL_ANGRY : EYEBALL_CALM; + } + else + { + if ( g_pMonsterResource ) + { + g_pMonsterResource->SetBossHealthPercentage( 1.0f ); + } + + m_attitude = EYEBALL_CALM; + } + + IGameEvent *event = gameeventmanager->CreateEvent( "recalculate_truce" ); + if ( event ) + { + gameeventmanager->FireEvent( event, true ); + } +} + + +//--------------------------------------------------------------------------------------------- +void CEyeballBoss::Update( void ) +{ + BaseClass::Update(); + + m_attitude = EYEBALL_CALM; + + if ( IsEnraged() ) + { + if ( IsSpell() ) + { + m_attitude = GetTeamNumber() == TF_TEAM_RED ? EYEBALL_ANGRY : EYEBALL_CALM; + m_nSkin = GetTeamNumber() == TF_TEAM_RED ? EYEBALL_TEAM_RED : EYEBALL_TEAM_BLUE; + } + else + { + m_nSkin = EYEBALL_RED_SKIN; + } + + int angryPoseParameter = LookupPoseParameter( "anger" ); + if ( angryPoseParameter >= 0 ) + { + SetPoseParameter( angryPoseParameter, 1 ); + } + } + else if ( IsGrumpy() ) + { + m_nSkin = EYEBALL_NORMAL_SKIN; + + int angryPoseParameter = LookupPoseParameter( "anger" ); + if ( angryPoseParameter >= 0 ) + { + SetPoseParameter( angryPoseParameter, 0.4f ); + } + } + else + { + m_nSkin = EYEBALL_NORMAL_SKIN; + + int angryPoseParameter = LookupPoseParameter( "anger" ); + if ( angryPoseParameter >= 0 ) + { + SetPoseParameter( angryPoseParameter, 0 ); + } + } +} + + +//--------------------------------------------------------------------------------------------- +void CEyeballBoss::UpdateOnRemove( void ) +{ + // In regular TF gameplay, g_pMonsterResource should always be non-null. The null check helps some server plugins though. + Assert( g_pMonsterResource != NULL ); + if ( g_pMonsterResource ) + { + g_pMonsterResource->HideBossHealthMeter(); + } + + BaseClass::UpdateOnRemove(); +} + + +//--------------------------------------------------------------------------------------------- +void CEyeballBoss::JarateNearbyPlayers( float range ) +{ + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + for( int i=0; i<playerVector.Count(); ++i ) + { + if ( IsRangeLessThan( playerVector[i], range ) && + IsLineOfSightClear( playerVector[i], CBaseCombatCharacter::IGNORE_ACTORS ) ) + { + playerVector[i]->m_Shared.AddCond( TF_COND_URINE, 10.0f ); + } + } +} + + +//--------------------------------------------------------------------------------------------- +float EyeballBossModifyDamage( const CTakeDamageInfo &info ) +{ + CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase * >( info.GetWeapon() ); + CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( info.GetInflictor() ); + CTFProjectile_SentryRocket *sentryRocket = dynamic_cast< CTFProjectile_SentryRocket * >( info.GetInflictor() ); + + if ( sentry || sentryRocket ) + { + return info.GetDamage() * 0.25f; + } + else if ( pWeapon ) + { + switch( pWeapon->GetWeaponID() ) + { + case TF_WEAPON_FLAMETHROWER: + return info.GetDamage() * 0.5f; + + case TF_WEAPON_MINIGUN: + return info.GetDamage() * 0.25f; + } + } + + // unmodified + return info.GetDamage(); +} + + +//--------------------------------------------------------------------------------------------- +int CEyeballBoss::OnTakeDamage_Alive( const CTakeDamageInfo &rawInfo ) +{ + CTakeDamageInfo info = rawInfo; + + if ( IsSelf( info.GetAttacker() ) ) + { + // don't injure myself + return 0; + } + + // have we reached our damage limit? + if ( m_damageLimit == 0 ) + { + return 0; + } + + if ( IsSpell() ) + { + return 0; + } + + int beforeHealth = GetHealth(); + + info.SetDamage( EyeballBossModifyDamage( info ) ); + + int result = BaseClass::OnTakeDamage_Alive( info ); + + // update boss health meter + float healthPercentage = (float)GetHealth() / (float)GetMaxHealth(); + + if ( g_pMonsterResource ) + { + if ( healthPercentage <= 0.0f ) + { + g_pMonsterResource->HideBossHealthMeter(); + } + else + { + g_pMonsterResource->SetBossHealthPercentage( healthPercentage ); + } + } + + // do we have a damage limit? + if ( m_damageLimit >= 0 ) + { + int actualDamage = beforeHealth - GetHealth(); + + m_damageLimit -= actualDamage; + + if ( m_damageLimit < 0 ) + { + m_damageLimit = 0; + } + } + + return result; +} + + +//--------------------------------------------------------------------------------------------- +bool CEyeballBoss::ShouldCollide( int collisionGroup, int contentsMask ) const +{ + return BaseClass::ShouldCollide( collisionGroup, contentsMask ); +} + + + +//----------------------------------------------------------------------------- +// Update our last known nav area directly underneath us (since we fly) +//----------------------------------------------------------------------------- +void CEyeballBoss::UpdateLastKnownArea( void ) +{ + if ( TheNavMesh->IsGenerating() ) + { + ClearLastKnownArea(); + return; + } + + // find the area we are directly standing in + CNavArea *area = TheNavMesh->GetNearestNavArea( this, GETNAVAREA_CHECK_LOS, 500.0f ); + if ( !area ) + return; + + // make sure we can actually use this area - if not, consider ourselves off the mesh + if ( !IsAreaTraversable( area ) ) + return; + + if ( area != m_lastNavArea ) + { + // player entered a new nav area + if ( m_lastNavArea ) + { + m_lastNavArea->DecrementPlayerCount( m_registeredNavTeam, entindex() ); + m_lastNavArea->OnExit( this, area ); + } + + m_registeredNavTeam = GetTeamNumber(); + area->IncrementPlayerCount( m_registeredNavTeam, entindex() ); + area->OnEnter( this, m_lastNavArea ); + + OnNavAreaChanged( area, m_lastNavArea ); + + m_lastNavArea = area; + } +} + + +//--------------------------------------------------------------------------------------------- +CBaseCombatCharacter *CEyeballBoss::GetVictim( void ) const +{ + if ( m_victim == NULL ) + return NULL; + + if ( !m_victim->IsAlive() ) + return NULL; + + if ( IsInPurgatory( m_victim ) ) + return NULL; + + return m_victim; +} + + +//--------------------------------------------------------------------------------------------- +CBaseCombatCharacter *CEyeballBoss::FindClosestVisibleVictim( void ) +{ + CBaseCombatCharacter *victim = NULL; + float victimRangeSq = FLT_MAX; + + CUtlVector< CTFPlayer * > playerVector; + int nTargetTeam = TEAM_ANY; + if ( IsSpell() ) + { + nTargetTeam = GetTeamNumber() == TF_TEAM_RED ? TF_TEAM_BLUE : TF_TEAM_RED; + + for ( int i=0; i<TFGameRules()->GetBossCount(); ++i ) + { + CBaseCombatCharacter *pBoss = TFGameRules()->GetActiveBoss( i ); + if ( pBoss && !IsSelf( pBoss ) && pBoss->GetTeamNumber() != GetTeamNumber() ) + { + float rangeSq = ( pBoss->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr(); + if ( rangeSq < victimRangeSq ) + { + if ( IsLineOfSightClear( pBoss ) ) + { + victim = pBoss; + victimRangeSq = rangeSq; + } + } + } + } + } + CollectPlayers( &playerVector, nTargetTeam, COLLECT_ONLY_LIVING_PLAYERS ); + + for( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *player = playerVector[i]; + + if ( IsInPurgatory( player ) ) + continue; + + if ( player->m_Shared.IsStealthed() ) + { + if ( !player->m_Shared.InCond( TF_COND_BURNING ) && + !player->m_Shared.InCond( TF_COND_URINE ) && + !player->m_Shared.InCond( TF_COND_STEALTHED_BLINK ) && + !player->m_Shared.InCond( TF_COND_BLEEDING ) ) + { + // cloaked spies are invisible to us + continue; + } + } + + // ignore player who disguises as my team + if ( player->m_Shared.InCond( TF_COND_DISGUISED ) && player->m_Shared.GetDisguiseTeam() == GetTeamNumber() ) + { + continue; + } + + // ignore ghost players + if ( player->m_Shared.InCond( TF_COND_HALLOWEEN_GHOST_MODE ) ) + { + continue; + } + + float rangeSq = ( player->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr(); + if ( rangeSq < victimRangeSq ) + { + if ( IsLineOfSightClear( player ) ) + { + victim = player; + victimRangeSq = rangeSq; + } + } + } + + for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i ) + { + CBaseObject* pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] ); + if ( pObj->GetTeamNumber() == GetTeamNumber() ) + { + continue; + } + + if ( pObj->ObjectType() == OBJ_SENTRYGUN ) + { + float rangeSq = ( pObj->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr(); + if ( rangeSq < victimRangeSq ) + { + if ( IsLineOfSightClear( pObj ) ) + { + victim = pObj; + victimRangeSq = rangeSq; + } + } + } + } + + // find closest zombie + for ( int i=0; i<IZombieAutoList::AutoList().Count(); ++i ) + { + CZombie* pZombie = static_cast< CZombie* >( IZombieAutoList::AutoList()[i] ); + if ( pZombie->GetTeamNumber() == GetTeamNumber() ) + { + continue; + } + + float rangeSq = GetRangeSquaredTo( pZombie ); + if ( rangeSq < victimRangeSq ) + { + if ( IsLineOfSightClear( pZombie ) ) + { + victim = pZombie; + victimRangeSq = rangeSq; + } + } + } + + return victim; +} + + +//--------------------------------------------------------------------------------------------- +const Vector &CEyeballBoss::PickNewSpawnSpot( void ) const +{ + static Vector spot; + + if ( m_spawnSpotVector.Count() == 0 ) + { + spot = GetAbsOrigin(); + } + else + { + spot = m_spawnSpotVector[ RandomInt( 0, m_spawnSpotVector.Count()-1 ) ]->GetAbsOrigin(); + } + + return spot; +} + + +//--------------------------------------------------------------------------------------------- +void CEyeballBoss::BecomeEnraged( float duration ) +{ + if ( !IsEnraged() ) + { + EmitSound( "Halloween.EyeballBossBecomeEnraged" ); + } + + m_rageTimer.Start( duration ); +} + + +//--------------------------------------------------------------------------------------------- +void CEyeballBoss::LogPlayerInteraction( const char *verb, CTFPlayer *player ) +{ + if ( !player || !verb ) + return; + + if ( !player->GetTeam() ) + return; + + CTFWeaponBase *weapon = player->GetActiveTFWeapon(); + const char *weaponLogName = NULL; + + if ( weapon ) + { + weaponLogName = WeaponIdToAlias( weapon->GetWeaponID() ); + + CEconItemView *pItem = weapon->GetAttributeContainer()->GetItem(); + + if ( pItem && pItem->GetStaticData() ) + { + if ( pItem->GetStaticData()->GetLogClassname() ) + { + weaponLogName = pItem->GetStaticData()->GetLogClassname(); + } + } + } + + UTIL_LogPrintf( "HALLOWEEN: \"%s<%i><%s><%s>\" %s with \"%s\" (attacker_position \"%d %d %d\")\n", + player->GetPlayerName(), + player->GetUserID(), + player->GetNetworkIDString(), + player->GetTeam()->GetName(), + verb, + weaponLogName ? weaponLogName : "NoWeapon", + (int)player->GetAbsOrigin().x, + (int)player->GetAbsOrigin().y, + (int)player->GetAbsOrigin().z ); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +IMPLEMENT_INTENTION_INTERFACE( CEyeballBoss, CEyeballBossBehavior ); + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +CEyeballBossLocomotion::CEyeballBossLocomotion( INextBot *bot ) : ILocomotion( bot ) +{ + Reset(); +} + + +//--------------------------------------------------------------------------------------------- +CEyeballBossLocomotion::~CEyeballBossLocomotion() +{ +} + + +//--------------------------------------------------------------------------------------------- +// (EXTEND) reset to initial state +void CEyeballBossLocomotion::Reset( void ) +{ + m_velocity = vec3_origin; + m_acceleration = vec3_origin; + m_desiredSpeed = 0.0f; + m_currentSpeed = 0.0f; + m_forward = vec3_origin; + m_desiredAltitude = tf_eyeball_boss_hover_height.GetFloat(); +} + + +#ifdef LOW_FLOAT_BUT_HANGS_UP_ON_LEDGES +//--------------------------------------------------------------------------------------------- +void CEyeballBossLocomotion::MaintainAltitude( void ) +{ + CBaseCombatCharacter *me = GetBot()->GetEntity(); + + trace_t result; + CTraceFilterSimpleClassnameList filter( me, COLLISION_GROUP_NONE ); + filter.AddClassnameToIgnore( "eyeball_boss" ); + + UTIL_TraceLine( me->GetAbsOrigin(), me->GetAbsOrigin() + Vector( 0, 0, -2000.0f ), MASK_PLAYERSOLID_BRUSHONLY, &filter, &result ); + + float groundZ = result.endpos.z; + + float currentAltitude = me->GetAbsOrigin().z - groundZ; + + float desiredAltitude = GetDesiredAltitude(); + + float error = desiredAltitude - currentAltitude; + + float accelZ = clamp( error, -tf_eyeball_boss_acceleration.GetFloat(), tf_eyeball_boss_acceleration.GetFloat() ); + + m_acceleration.z += accelZ; +} +#endif + +#define HI_FLOATING +#ifdef HI_FLOATING +//--------------------------------------------------------------------------------------------- +void CEyeballBossLocomotion::MaintainAltitude( void ) +{ + CBaseCombatCharacter *me = GetBot()->GetEntity(); + + if ( !me->IsAlive() ) + { + m_acceleration.x = 0.0f; + m_acceleration.y = 0.0f; + m_acceleration.z = -300.0f; + + return; + } + + trace_t result; + CTraceFilterSimpleClassnameList filter( me, COLLISION_GROUP_NONE ); + filter.AddClassnameToIgnore( "eyeball_boss" ); + + // find ceiling + TraceHull( me->GetAbsOrigin(), me->GetAbsOrigin() + Vector( 0, 0, 1000.0f ), + me->WorldAlignMins(), me->WorldAlignMaxs(), + GetBot()->GetBodyInterface()->GetSolidMask(), &filter, &result ); + + float ceiling = result.endpos.z - me->GetAbsOrigin().z; + + Vector aheadXY; + + if ( IsAttemptingToMove() ) + { + aheadXY.x = m_forward.x; + aheadXY.y = m_forward.y; + aheadXY.z = 0.0f; + aheadXY.NormalizeInPlace(); + } + else + { + aheadXY = vec3_origin; + } + + TraceHull( me->GetAbsOrigin() + Vector( 0, 0, ceiling ) + aheadXY * 50.0f, + me->GetAbsOrigin() + Vector( 0, 0, -2000.0f ) + aheadXY * 50.0f, + Vector( 1.25f * me->WorldAlignMins().x, 1.25f * me->WorldAlignMins().y, me->WorldAlignMins().z ), + Vector( 1.25f * me->WorldAlignMaxs().x, 1.25f * me->WorldAlignMaxs().y, me->WorldAlignMaxs().z ), + GetBot()->GetBodyInterface()->GetSolidMask(), &filter, &result ); + + float groundZ = result.endpos.z; + + float currentAltitude = me->GetAbsOrigin().z - groundZ; + + float desiredAltitude = GetDesiredAltitude(); + + float error = desiredAltitude - currentAltitude; + + float accelZ = clamp( error, -tf_eyeball_boss_acceleration.GetFloat(), tf_eyeball_boss_acceleration.GetFloat() ); + + m_acceleration.z += accelZ; +} +#endif + +//--------------------------------------------------------------------------------------------- +// (EXTEND) update internal state +void CEyeballBossLocomotion::Update( void ) +{ + CBaseCombatCharacter *me = GetBot()->GetEntity(); + const float deltaT = GetUpdateInterval(); + + Vector pos = me->GetAbsOrigin(); + + // always maintain altitude, even if not trying to move (ie: no Approach call) + MaintainAltitude(); + + m_forward = m_velocity; + m_currentSpeed = m_forward.NormalizeInPlace(); + + Vector damping( tf_eyeball_boss_horiz_damping.GetFloat(), tf_eyeball_boss_horiz_damping.GetFloat(), tf_eyeball_boss_vert_damping.GetFloat() ); + Vector totalAccel = m_acceleration - m_velocity * damping; + + m_velocity += totalAccel * deltaT; + me->SetAbsVelocity( m_velocity ); + + pos += m_velocity * deltaT; + + // check for collisions along move + trace_t result; + CTraceFilterSkipClassname filter( me, "eyeball_boss", COLLISION_GROUP_NONE ); + Vector from = me->GetAbsOrigin(); + Vector to = pos; + Vector desiredGoal = to; + Vector resolvedGoal; + int recursionLimit = 3; + + int hitCount = 0; + Vector surfaceNormal = vec3_origin; + + bool didHitWorld = false; + + while( true ) + { + TraceHull( from, desiredGoal, me->WorldAlignMins(), me->WorldAlignMaxs(), GetBot()->GetBodyInterface()->GetSolidMask(), &filter, &result ); + + if ( !result.DidHit() ) + { + resolvedGoal = pos; + break; + } + + if ( result.DidHitWorld() ) + { + didHitWorld = true; + } + + ++hitCount; + surfaceNormal += result.plane.normal; + + // If we hit really close to our target, then stop + if ( !result.startsolid && desiredGoal.DistToSqr( result.endpos ) < 1.0f ) + { + resolvedGoal = result.endpos; + break; + } + + if ( result.startsolid ) + { + // stuck inside solid; don't move + resolvedGoal = me->GetAbsOrigin(); + break; + } + + if ( --recursionLimit <= 0 ) + { + // reached recursion limit, no more adjusting allowed + resolvedGoal = result.endpos; + break; + } + + // slide off of surface we hit + Vector fullMove = desiredGoal - from; + Vector leftToMove = fullMove * ( 1.0f - result.fraction ); + + float blocked = DotProduct( result.plane.normal, leftToMove ); + + Vector unconstrained = fullMove - blocked * result.plane.normal; + + // check for collisions along remainder of move + // But don't bother if we're not going to deflect much + Vector remainingMove = from + unconstrained; + if ( remainingMove.DistToSqr( result.endpos ) < 1.0f ) + { + resolvedGoal = result.endpos; + break; + } + + desiredGoal = remainingMove; + } + + if ( hitCount > 0 ) + { + surfaceNormal.NormalizeInPlace(); + + // bounce + m_velocity = m_velocity - 2.0f * DotProduct( m_velocity, surfaceNormal ) * surfaceNormal; + + if ( didHitWorld ) + { + //me->EmitSound( "Minion.Bounce" ); + } + } + + GetBot()->GetEntity()->SetAbsOrigin( result.endpos ); + + m_acceleration = vec3_origin; +} + + +//--------------------------------------------------------------------------------------------- +// (EXTEND) move directly towards the given position +void CEyeballBossLocomotion::Approach( const Vector &goalPos, float goalWeight ) +{ + Vector flyGoal = goalPos; + flyGoal.z += m_desiredAltitude; + + Vector toGoal = flyGoal - GetBot()->GetEntity()->GetAbsOrigin(); + // altitude is handled in Update() + toGoal.z = 0.0f; + toGoal.NormalizeInPlace(); + + m_acceleration += tf_eyeball_boss_acceleration.GetFloat() * toGoal; +} + + +//--------------------------------------------------------------------------------------------- +void CEyeballBossLocomotion::SetDesiredSpeed( float speed ) +{ + m_desiredSpeed = speed; +} + + +//--------------------------------------------------------------------------------------------- +float CEyeballBossLocomotion::GetDesiredSpeed( void ) const +{ + return m_desiredSpeed; +} + + +//--------------------------------------------------------------------------------------------- +void CEyeballBossLocomotion::SetDesiredAltitude( float height ) +{ + m_desiredAltitude = height; +} + + +//--------------------------------------------------------------------------------------------- +float CEyeballBossLocomotion::GetDesiredAltitude( void ) const +{ + return m_desiredAltitude; +} + + +//--------------------------------------------------------------------------------------------- +// Face along path. Since we float, only face horizontally. +void CEyeballBossLocomotion::FaceTowards( const Vector &target ) +{ + CBaseCombatCharacter *me = GetBot()->GetEntity(); + + Vector toTarget = target - me->WorldSpaceCenter(); + toTarget.z = 0.0f; + + QAngle angles; + VectorAngles( toTarget, angles ); + + me->SetAbsAngles( angles ); +} + + +//--------------------------------------------------------------------------------------------- +// return position of "feet" - the driving point where the bot contacts the ground +// for this floating boss, "feet" refers to the ground directly underneath him +const Vector &CEyeballBossLocomotion::GetFeet( void ) const +{ + static Vector feet; + CBaseCombatCharacter *me = GetBot()->GetEntity(); + + trace_t result; + CTraceFilterSimpleClassnameList filter( me, COLLISION_GROUP_NONE ); + filter.AddClassnameToIgnore( "eyeball_boss" ); + + feet = me->GetAbsOrigin(); + + UTIL_TraceLine( feet, feet + Vector( 0, 0, -2000.0f ), MASK_PLAYERSOLID_BRUSHONLY, &filter, &result ); + + feet.z = result.endpos.z; + + return feet; +} + + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +CEyeballBossBody::CEyeballBossBody( INextBot *bot ) : CBotNPCBody( bot ) +{ + m_leftRightPoseParameter = -1; + m_upDownPoseParameter = -1; + m_lookAtSpot = vec3_origin; +} + + +//--------------------------------------------------------------------------------------------- +void CEyeballBossBody::Update( void ) +{ + CBaseCombatCharacter *me = GetBot()->GetEntity(); + + // track client-side rotation + Vector myForward; + me->GetVectors( &myForward, NULL, NULL ); + + const float myApproachRate = 3.0f; // 1.0f; + + Vector toTarget = m_lookAtSpot - me->WorldSpaceCenter(); + toTarget.NormalizeInPlace(); + + myForward += toTarget * myApproachRate * GetUpdateInterval(); + myForward.NormalizeInPlace(); + + QAngle myNewAngles; + VectorAngles( myForward, myNewAngles ); + + me->SetAbsAngles( myNewAngles ); + + if ( tf_eyeball_boss_debug.GetBool() ) + { + NDebugOverlay::Line( me->WorldSpaceCenter(), me->WorldSpaceCenter() + 150.0f * myForward, 255, 255, 0, true, 0.1f ); + } + + // move the animation ahead in time + me->StudioFrameAdvance(); + me->DispatchAnimEvents( me ); +} + + +//--------------------------------------------------------------------------------------------- +// Aim the bot's head towards the given goal +void CEyeballBossBody::AimHeadTowards( const Vector &lookAtPos, LookAtPriorityType priority, float duration, INextBotReply *replyWhenAimed, const char *reason ) +{ + CEyeballBoss *me = (CEyeballBoss *)GetBot()->GetEntity(); + + m_lookAtSpot = lookAtPos; + me->SetLookAtTarget( lookAtPos ); +} + + +//--------------------------------------------------------------------------------------------- +// Continually aim the bot's head towards the given subject +void CEyeballBossBody::AimHeadTowards( CBaseEntity *subject, LookAtPriorityType priority, float duration, INextBotReply *replyWhenAimed, const char *reason ) +{ + CEyeballBoss *me = (CEyeballBoss *)GetBot()->GetEntity(); + + me->SetLookAtTarget( subject->EyePosition() ); + + if ( !subject ) + return; + + m_lookAtSpot = subject->EyePosition(); +} diff --git a/game/server/tf/halloween/eyeball_boss/eyeball_boss.h b/game/server/tf/halloween/eyeball_boss/eyeball_boss.h new file mode 100644 index 0000000..3ef481b --- /dev/null +++ b/game/server/tf/halloween/eyeball_boss/eyeball_boss.h @@ -0,0 +1,338 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// eyeball_boss.h +// The 2011 Halloween Boss +// Michael Booth, October 2011 + +#ifndef EYEBALL_BOSS_H +#define EYEBALL_BOSS_H + +#include "NextBot.h" +#include "NextBotBehavior.h" +#include "Path/NextBotPathFollow.h" +#include "bot_npc/bot_npc_body.h" +#include "../halloween_base_boss.h" + +#define EYEBALL_RADIUS 100.0f + +#define EYEBALL_NORMAL_SKIN 0 +#define EYEBALL_RED_SKIN 1 +#define EYEBALL_TEAM_RED 2 +#define EYEBALL_TEAM_BLUE 3 + +#define PURGATORY_Z -1152 + +#define EYEBALL_ANGRY 2 +#define EYEBALL_GRUMPY 1 +#define EYEBALL_CALM 0 + +extern ConVar tf_eyeball_boss_debug; +extern ConVar tf_eyeball_boss_debug_orientation; +extern ConVar tf_eyeball_boss_lifetime; +extern ConVar tf_eyeball_boss_lifetime_spell; +extern ConVar tf_eyeball_boss_speed; +extern ConVar tf_eyeball_boss_hover_height; +extern ConVar tf_eyeball_boss_acceleration; +extern ConVar tf_eyeball_boss_horiz_damping; +extern ConVar tf_eyeball_boss_vert_damping; +extern ConVar tf_eyeball_boss_attack_range; +extern ConVar tf_eyeball_boss_health_base; +extern ConVar tf_eyeball_boss_health_per_player; +extern ConVar tf_halloween_bot_min_player_count; + + +//---------------------------------------------------------------------------- +class CEyeballBossBody : public CBotNPCBody +{ +public: + CEyeballBossBody( INextBot *bot ); + virtual ~CEyeballBossBody() { } + + virtual void Update( void ); + + virtual void AimHeadTowards( const Vector &lookAtPos, + LookAtPriorityType priority = BORING, + float duration = 0.0f, + INextBotReply *replyWhenAimed = NULL, + const char *reason = NULL ); // aim the bot's head towards the given goal + virtual void AimHeadTowards( CBaseEntity *subject, + LookAtPriorityType priority = BORING, + float duration = 0.0f, + INextBotReply *replyWhenAimed = NULL, + const char *reason = NULL ); // continually aim the bot's head towards the given subject + + virtual float GetMaxHeadAngularVelocity( void ) const // return max turn rate of head in degrees/second + { + return 3000.0f; + } + +private: + int m_leftRightPoseParameter; + int m_upDownPoseParameter; + + Vector m_lookAtSpot; +}; + + +//---------------------------------------------------------------------------- +// Bypass vision system +class CDisableVision : public IVision +{ +public: + CDisableVision( INextBot *bot ) : IVision( bot ) { } + virtual ~CDisableVision() { } + + virtual void Reset( void ) { } + virtual void Update( void ) { } +}; + + +//---------------------------------------------------------------------------- +class CEyeballBossLocomotion : public ILocomotion +{ +public: + CEyeballBossLocomotion( INextBot *bot ); + virtual ~CEyeballBossLocomotion(); + + virtual void Reset( void ); // (EXTEND) reset to initial state + virtual void Update( void ); // (EXTEND) update internal state + + virtual void Approach( const Vector &goalPos, float goalWeight = 1.0f ); // (EXTEND) move directly towards the given position + + virtual void SetDesiredSpeed( float speed ); // set desired speed for locomotor movement + virtual float GetDesiredSpeed( void ) const; // returns the current desired speed + + virtual float GetStepHeight( void ) const; // if delta Z is greater than this, we have to jump to get up + virtual float GetMaxJumpHeight( void ) const; // return maximum height of a jump + virtual float GetDeathDropHeight( void ) const; // distance at which we will die if we fall + + virtual void SetDesiredAltitude( float height ); // how high above our Approach goal do we float? + virtual float GetDesiredAltitude( void ) const; + + virtual const Vector &GetGroundNormal( void ) const; // surface normal of the ground we are in contact with + + virtual const Vector &GetVelocity( void ) const; // return current world space velocity + void SetVelocity( const Vector &velocity ); + + virtual void FaceTowards( const Vector &target ); // rotate body to face towards "target" + + // return position of "feet" - the driving point where the bot contacts the ground + // for this floating boss, "feet" refers to the ground directly underneath him + virtual const Vector &GetFeet( void ) const; + +protected: + float m_desiredSpeed; + float m_currentSpeed; + Vector m_forward; + + float m_desiredAltitude; + void MaintainAltitude( void ); + + Vector m_velocity; + Vector m_acceleration; +}; + + +inline float CEyeballBossLocomotion::GetStepHeight( void ) const +{ + return 50.0f; +} + +inline float CEyeballBossLocomotion::GetMaxJumpHeight( void ) const +{ + return 100.0f; +} + +inline float CEyeballBossLocomotion::GetDeathDropHeight( void ) const +{ + return 999.9f; +} + +inline const Vector &CEyeballBossLocomotion::GetGroundNormal( void ) const +{ + static Vector up( 0, 0, 1.0f ); + + return up; +} + +inline const Vector &CEyeballBossLocomotion::GetVelocity( void ) const +{ + return m_velocity; +} + +inline void CEyeballBossLocomotion::SetVelocity( const Vector &velocity ) +{ + m_velocity = velocity; +} + +DECLARE_AUTO_LIST( IEyeballBossAutoList ); + +//---------------------------------------------------------------------------- +class CEyeballBoss : public CHalloweenBaseBoss, public IEyeballBossAutoList +{ +public: + DECLARE_CLASS( CEyeballBoss, CHalloweenBaseBoss ); + DECLARE_SERVERCLASS(); + + CEyeballBoss(); + virtual ~CEyeballBoss(); + + static void PrecacheEyeballBoss(); + virtual void Precache(); + virtual void Spawn( void ); + virtual void UpdateOnRemove( void ); + + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + + virtual void UpdateLastKnownArea( void ); // invoke this to update our last known nav area (since there is no think method chained to CBaseCombatCharacter) + + // INextBot + DECLARE_INTENTION_INTERFACE( CEyeballBoss ); + virtual CEyeballBossLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; } + virtual CEyeballBossBody *GetBodyInterface( void ) const { return m_body; } + virtual CDisableVision *GetVisionInterface( void ) const { return m_vision; } + + virtual void Update( void ); + + virtual Vector EyePosition( void ); + + virtual bool ShouldCollide( int collisionGroup, int contentsMask ) const; + + virtual float GetCritInjuryMultiplier( void ) const; // when we are hit by a crit, damage is mutiplied by this + + const Vector &GetHomePosition( void ) const; + + void BecomeEnraged( float duration ); + bool IsEnraged( void ) const; + + bool IsGrumpy( void ) const; + + void SetLookAtTarget( const Vector &spot ); + + void SetVictim( CBaseCombatCharacter *victim ); + CBaseCombatCharacter *GetVictim( void ) const; + + bool IsInPurgatory( CBaseEntity *entity ) const; + + CBaseCombatCharacter *FindClosestVisibleVictim( void ); + + const Vector &PickNewSpawnSpot( void ) const; + + void JarateNearbyPlayers( float range ); + + void SetDamageLimit( int limit ); + void RemoveDamageLimit( void ); + + void LogPlayerInteraction( const char *verb, CTFPlayer *player ); + + void GainLevel( void ); + void ResetLevel( void ); + virtual int GetLevel( void ) const OVERRIDE; + + virtual HalloweenBossType GetBossType() const { return HALLOWEEN_BOSS_MONOCULUS; } + +private: + CEyeballBossLocomotion *m_locomotor; + CEyeballBossBody *m_body; + CDisableVision *m_vision; + + Vector m_eyeOffset; + Vector m_homePos; + + CTFPlayer *m_target; + + CountdownTimer m_invulnTimer; + + CNetworkVector( m_lookAtSpot ); + CNetworkVar( int, m_attitude ); + + CountdownTimer m_rageTimer; + + CHandle< CBaseCombatCharacter > m_victim; + + CUtlVector< CHandle< CBaseEntity > > m_spawnSpotVector; + + int m_damageLimit; + + static int m_level; +}; + +inline int CEyeballBoss::GetLevel( void ) const +{ + return m_level; +} + +inline void CEyeballBoss::GainLevel( void ) +{ + ++m_level; +} + +inline void CEyeballBoss::ResetLevel( void ) +{ + m_level = 1; +} + +inline void CEyeballBoss::SetDamageLimit( int limit ) +{ + m_damageLimit = limit; +} + +inline void CEyeballBoss::RemoveDamageLimit( void ) +{ + m_damageLimit = -1; +} + +inline float CEyeballBoss::GetCritInjuryMultiplier( void ) const +{ + return 2.0f; +} + +inline bool CEyeballBoss::IsInPurgatory( CBaseEntity *entity ) const +{ + if ( IsSpell() ) + return false; + + return ( entity->GetAbsOrigin().z < PURGATORY_Z ); +} + +inline void CEyeballBoss::SetVictim( CBaseCombatCharacter *victim ) +{ + m_victim = victim; +} + +inline bool CEyeballBoss::IsEnraged( void ) const +{ + // always enrage if I'm a spell + if ( IsSpell() ) + return true; + + // being near death always makes me mad + if ( GetHealth() < GetMaxHealth()/3 ) + return true; + + return m_rageTimer.HasStarted() && !m_rageTimer.IsElapsed(); +} + +inline bool CEyeballBoss::IsGrumpy( void ) const +{ + if ( IsEnraged() ) + return false; + + return ( GetHealth() < 2*GetMaxHealth()/3 ); +} + +inline const Vector &CEyeballBoss::GetHomePosition( void ) const +{ + return m_homePos; +} + +inline Vector CEyeballBoss::EyePosition( void ) +{ + return GetAbsOrigin() + m_eyeOffset; +} + +inline void CEyeballBoss::SetLookAtTarget( const Vector &spot ) +{ + m_lookAtSpot = spot; +} + +#endif // EYEBALL_BOSS_H diff --git a/game/server/tf/halloween/ghost/ghost.cpp b/game/server/tf/halloween/ghost/ghost.cpp new file mode 100644 index 0000000..d28fb35 --- /dev/null +++ b/game/server/tf/halloween/ghost/ghost.cpp @@ -0,0 +1,331 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// ghost.cpp +// A spooky halloween ghost bot +// Michael Booth, October 2011 + +#include "cbase.h" + +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "tf_projectile_arrow.h" +#include "tf_weapon_grenade_pipebomb.h" +#include "nav_mesh/tf_nav_area.h" +#include "ghost.h" + +#include "NextBot/Path/NextBotChasePath.h" +#include "econ_wearable.h" +#include "team_control_point_master.h" +#include "particle_parse.h" +#include "CRagdollMagnet.h" +#include "NextBot/Behavior/BehaviorMoveTo.h" + + +void CC_GhostSpawn( const CCommand& args ) +{ + MDLCACHE_CRITICAL_SECTION(); + + CBaseEntity *entity = CreateEntityByName( "ghost" ); + if ( entity ) + { + entity->Precache(); + DispatchSpawn( entity ); + + // Now attempt to drop into the world + CBasePlayer* pPlayer = UTIL_GetCommandClient(); + trace_t tr; + Vector forward; + pPlayer->EyeVectors( &forward ); + UTIL_TraceLine(pPlayer->EyePosition(), + pPlayer->EyePosition() + forward * MAX_TRACE_LENGTH,MASK_SOLID, + pPlayer, COLLISION_GROUP_NONE, &tr ); + + if ( tr.fraction != 1.0 ) + { + // Raise the end position a little up off the floor, place the npc and drop him down + tr.endpos.z += 12; + entity->Teleport( &tr.endpos, NULL, NULL ); + } + } +} +static ConCommand ghost_spawn( "ghost_spawn", CC_GhostSpawn, "Spawns a Ghost where the player is looking.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//----------------------------------------------------------------------------------------------------- +CGhost *SpawnGhost( const Vector &spot, const QAngle &angles, float lifetime ) +{ + CGhost *ghost = (CGhost *)CreateEntityByName( "ghost" ); + if ( ghost ) + { + DispatchSpawn( ghost ); + + ghost->SetAbsOrigin( spot ); + ghost->SetLocalAngles( angles ); + ghost->SetLifetime( lifetime ); + + return ghost; + } + + return NULL; +} + + +//----------------------------------------------------------------------------------------------------- +// The Ghost +//----------------------------------------------------------------------------------------------------- +LINK_ENTITY_TO_CLASS( ghost, CGhost ); + + +//----------------------------------------------------------------------------------------------------- +CGhost::CGhost() +{ + ALLOCATE_INTENTION_INTERFACE( CGhost ); + + m_locomotor = new CGhostLocomotion( this ); + + m_eyeOffset = vec3_origin; + m_lifetime = 10.0f; +} + + +//----------------------------------------------------------------------------------------------------- +CGhost::~CGhost() +{ + DEALLOCATE_INTENTION_INTERFACE; + + if ( m_locomotor ) + delete m_locomotor; +} + +//----------------------------------------------------------------------------------------------------- +void CGhost::PrecacheGhost() +{ + PrecacheModel( "models/props_halloween/ghost_no_hat.mdl" ); + PrecacheParticleSystem( "ghost_appearation" ); + PrecacheScriptSound( "Halloween.GhostMoan" ); + PrecacheScriptSound( "Halloween.GhostBoo" ); + PrecacheScriptSound( "Halloween.Haunted" ); +} + +//----------------------------------------------------------------------------------------------------- +void CGhost::Precache() +{ + BaseClass::Precache(); + + // always allow late precaching, so we don't pay the cost of the + // Halloween Ghost for the entire year + + bool bAllowPrecache = CBaseEntity::IsPrecacheAllowed(); + CBaseEntity::SetAllowPrecache( true ); + + PrecacheGhost(); + + CBaseEntity::SetAllowPrecache( bAllowPrecache ); +} + + +//----------------------------------------------------------------------------------------------------- +void CGhost::Spawn( void ) +{ + Precache(); + + BaseClass::Spawn(); + + SetCollisionGroup( COLLISION_GROUP_NONE ); + SetSolid( SOLID_NONE ); + AddSolidFlags( FSOLID_NOT_SOLID ); + + SetModel( "models/props_halloween/ghost_no_hat.mdl" ); +} + + +//--------------------------------------------------------------------------------------------- +bool CGhost::ShouldCollide( int collisionGroup, int contentsMask ) const +{ + return false; + //return BaseClass::ShouldCollide( collisionGroup, contentsMask ); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CGhostBehavior : public Action< CGhost > +{ +public: + //--------------------------------------------------------------------------------------------- + virtual ActionResult< CGhost > OnStart( CGhost *me, Action< CGhost > *priorAction ) + { + m_lifeTimer.Start(); + m_stuckAnchor = me->GetAbsOrigin(); + m_stuckTimer.Start( 1.0f ); + + me->GetVectors( &m_forward, NULL, NULL ); + + DispatchParticleEffect( "ghost_appearation", me->WorldSpaceCenter(), me->GetAbsAngles() ); + + return Continue(); + } + + + //--------------------------------------------------------------------------------------------- + virtual ActionResult< CGhost > Update( CGhost *me, float interval ) + { + if ( m_lifeTimer.IsGreaterThen( me->GetLifetime() ) || m_stuckTimer.IsElapsed() ) + { + DispatchParticleEffect( "ghost_appearation", me->WorldSpaceCenter(), me->GetAbsAngles() ); + + me->EmitSound( "Halloween.Haunted" ); + + UTIL_Remove( me ); + return Done(); + } + + if ( m_moanTimer.IsElapsed() ) + { + me->EmitSound( "Halloween.GhostMoan" ); + m_moanTimer.Start( RandomFloat( 5.0f, 7.0f ) ); + } + + DriftAroundAndAvoidObstacles( me ); + ScareNearbyPlayers( me ); + + return Continue(); + } + + + //--------------------------------------------------------------------------------------------- + void DriftAroundAndAvoidObstacles( CGhost *me ) + { + const float feelerRange = 150.0f; + + Vector left( -m_forward.y, m_forward.x, 0.0f ); + Vector right( m_forward.y, -m_forward.x, 0.0f ); + + CTraceFilterNoNPCsOrPlayer traceFilter( me, COLLISION_GROUP_NONE ); + trace_t resultLeft; + UTIL_TraceLine( me->WorldSpaceCenter(), me->WorldSpaceCenter() + feelerRange * ( m_forward + left ), MASK_PLAYERSOLID, &traceFilter, &resultLeft ); + //NDebugOverlay::Line( me->WorldSpaceCenter(), me->WorldSpaceCenter() + feelerRange * ( m_forward + left ), 0, 0, 255, true, interval ); + + trace_t resultRight; + UTIL_TraceLine( me->WorldSpaceCenter(), me->WorldSpaceCenter() + feelerRange * ( m_forward + right ), MASK_PLAYERSOLID, &traceFilter, &resultRight ); + //NDebugOverlay::Line( me->WorldSpaceCenter(), me->WorldSpaceCenter() + feelerRange * ( m_forward + right ), 255, 0, 0, true, interval ); + + const float turnRate = 0.2f; + + if ( resultLeft.DidHit() ) + { + if ( resultRight.DidHit() ) + { + // both sides hit + if ( resultLeft.fraction < resultRight.fraction ) + { + // left hit closer - turn right + m_forward += turnRate * right; + } + else + { + // right hit closer - turn left + m_forward += turnRate * left; + } + } + else + { + // left hit - turn right + m_forward += turnRate * right; + } + } + else if ( resultRight.DidHit() ) + { + // right hit - turn left + m_forward += turnRate * left; + } + + m_forward.NormalizeInPlace(); + + Vector goal = 100.0f * m_forward + me->GetAbsOrigin(); + + me->GetLocomotionInterface()->Approach( goal ); + me->GetLocomotionInterface()->FaceTowards( goal ); + me->GetLocomotionInterface()->Run(); + + if ( me->IsRangeGreaterThan( m_stuckAnchor, 50.0f ) ) + { + m_stuckAnchor = me->GetAbsOrigin(); + m_stuckTimer.Reset(); + } + } + + + //--------------------------------------------------------------------------------------------- + void ScareNearbyPlayers( CGhost *me ) + { + if ( m_scareTimer.IsElapsed() ) + { + m_scareTimer.Start( 1.0f ); + + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + for( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *victim = playerVector[i]; + + if ( victim && !victim->HasPurgatoryBuff() ) + { + if ( me->IsRangeLessThan( victim, GHOST_SCARE_RADIUS ) ) + { + if ( me->IsLineOfSightClear( victim ) ) + { + // scare them! + const float scareTime = 2.0f; + const float speedReduction = 0.0f; + + // "stun by trigger" results in the Halloween "yikes" effects + int stunFlags = TF_STUN_LOSER_STATE | TF_STUN_BY_TRIGGER; + victim->m_Shared.StunPlayer( scareTime, speedReduction, stunFlags, NULL ); + } + } + } + } + } + } + + virtual const char *GetName( void ) const { return "Behavior"; } // return name of this action + +private: + IntervalTimer m_lifeTimer; + CountdownTimer m_moanTimer; + CountdownTimer m_scareTimer; + Vector m_forward; + Vector m_stuckAnchor; + CountdownTimer m_stuckTimer; +}; + + +IMPLEMENT_INTENTION_INTERFACE( CGhost, CGhostBehavior ); + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +// Get maximum running speed +float CGhostLocomotion::GetRunSpeed( void ) const +{ + return 90.0f; +} + + +//--------------------------------------------------------------------------------------------- +// Return maximum acceleration of locomotor +float CGhostLocomotion::GetMaxAcceleration( void ) const +{ + return 500.0f; +} + + +//--------------------------------------------------------------------------------------------- +// Return maximum deceleration of locomotor +float CGhostLocomotion::GetMaxDeceleration( void ) const +{ + return 500.0f; +} diff --git a/game/server/tf/halloween/ghost/ghost.h b/game/server/tf/halloween/ghost/ghost.h new file mode 100644 index 0000000..23e9e36 --- /dev/null +++ b/game/server/tf/halloween/ghost/ghost.h @@ -0,0 +1,86 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// ghost.h +// A spooky halloween ghost bot +// Michael Booth, October 2011 + +#ifndef GHOST_H +#define GHOST_H + +#include "NextBot.h" +#include "NextBotBehavior.h" +#include "NextBotGroundLocomotion.h" +#include "Path/NextBotPathFollow.h" + +#define GHOST_SPEED 90 +#define GHOST_SCARE_RADIUS 192 + +class CTFPlayer; + +//---------------------------------------------------------------------------- +class CGhostLocomotion : public NextBotGroundLocomotion +{ +public: + DECLARE_CLASS( CGhostLocomotion, NextBotGroundLocomotion ); + + CGhostLocomotion( INextBot *bot ) : NextBotGroundLocomotion( bot ) { } + virtual ~CGhostLocomotion() { } + + virtual float GetRunSpeed( void ) const; // get maximum running speed + + virtual float GetMaxAcceleration( void ) const; // return maximum acceleration of locomotor + virtual float GetMaxDeceleration( void ) const; // return maximum deceleration of locomotor +}; + + +//---------------------------------------------------------------------------- +class CGhost : public NextBotCombatCharacter +{ +public: + DECLARE_CLASS( CGhost, NextBotCombatCharacter ); + + CGhost(); + virtual ~CGhost(); + + static void PrecacheGhost(); + virtual void Precache(); + virtual void Spawn( void ); + + void SetLifetime( float duration ); + float GetLifetime( void ) const; + + // INextBot + DECLARE_INTENTION_INTERFACE( CGhost ); + virtual CGhostLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; } + + virtual Vector EyePosition( void ); + + virtual bool ShouldCollide( int collisionGroup, int contentsMask ) const; + + +private: + CGhostLocomotion *m_locomotor; + Vector m_eyeOffset; + Vector m_homePos; + float m_lifetime; +}; + + +inline void CGhost::SetLifetime( float duration ) +{ + m_lifetime = duration; +} + +inline float CGhost::GetLifetime( void ) const +{ + return m_lifetime; +} + +inline Vector CGhost::EyePosition( void ) +{ + return GetAbsOrigin() + m_eyeOffset; +} + + +extern CGhost *SpawnGhost( const Vector &spot, const QAngle &angles, float lifetime = 10.0f ); + +#endif // GHOST_H diff --git a/game/server/tf/halloween/halloween_base_boss.cpp b/game/server/tf/halloween/halloween_base_boss.cpp new file mode 100644 index 0000000..014bde7 --- /dev/null +++ b/game/server/tf/halloween/halloween_base_boss.cpp @@ -0,0 +1,313 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// halloween_boss_base.cpp +// Shared code for the Halloween Bosses +// Michael Booth, October 2011 + +#include "cbase.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "halloween_base_boss.h" +#include "tf_gamestats.h" + + +//----------------------------------------------------------------------------------------------------- +CHalloweenBaseBoss::CHalloweenBaseBoss() +{ + m_wasSpawnedByCheats = false; +} + + +//----------------------------------------------------------------------------------------------------- +CHalloweenBaseBoss::~CHalloweenBaseBoss() +{ +} + + +//----------------------------------------------------------------------------------------------------- +void CHalloweenBaseBoss::Spawn( void ) +{ + BaseClass::Spawn(); + + ConVarRef sv_cheats( "sv_cheats" ); + if ( sv_cheats.IsValid() && sv_cheats.GetBool() ) + { + // remember we spawned with a cheat command + m_wasSpawnedByCheats = true; + } + + m_damagePerSecond = 0.0f; + m_maxDamagePerSecond = 0.0f; + + if ( TFGameRules() ) + { + TFGameRules()->AddActiveBoss( this ); + } + + // track how many players were playing when boss spawned + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_RED ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, false, APPEND_PLAYERS ); + + // status + CTF_GameStats.Event_HalloweenBossEvent( GetBossType(), GetLevel(), HALLOWEEN_EVENT_BOSS_SPAWN, playerVector.Count(), 0.0f ); +} + + +//----------------------------------------------------------------------------------------------------- +void CHalloweenBaseBoss::Update( void ) +{ + BaseClass::Update(); + + UpdateDamagePerSecond(); +} + +//----------------------------------------------------------------------------------------------------- +void CHalloweenBaseBoss::UpdateOnRemove() +{ + if ( TFGameRules() ) + { + TFGameRules()->RemoveActiveBoss( this ); + } + + BaseClass::UpdateOnRemove(); +} + +//----------------------------------------------------------------------------------------------------- +int CHalloweenBaseBoss::OnTakeDamage( const CTakeDamageInfo &info ) +{ + if ( info.GetDamage() && ( info.GetAttacker() != this ) ) + { + CTFPlayer *pAttacker = ToTFPlayer( info.GetAttacker() ); + + if ( pAttacker && info.GetWeapon() ) + { + CTFWeaponBase *pWeapon = dynamic_cast<CTFWeaponBase *>( info.GetWeapon() ); + if ( pWeapon ) + { + pWeapon->ApplyOnHitAttributes( this, pAttacker, info ); + } + } + } + + return BaseClass::OnTakeDamage( info ); +} + +//----------------------------------------------------------------------------------------------------- +int CHalloweenBaseBoss::OnTakeDamage_Alive( const CTakeDamageInfo &rawInfo ) +{ + CTakeDamageInfo info = rawInfo; + + if ( info.GetAttacker() && info.GetAttacker()->GetTeamNumber() == GetTeamNumber() ) + { + return 0; + } + + if ( TFGameRules()->RoundHasBeenWon() ) + { + info.SetDamage( 0.0f ); + } + + if ( info.GetDamageType() & DMG_CRITICAL ) + { + // do the critical damage increase + info.SetDamage( info.GetDamage() * GetCritInjuryMultiplier() ); + } + + // keep a list of everyone who hurt me, and when + CTFPlayer *playerAttacker = ToTFPlayer( info.GetAttacker() ); + if ( playerAttacker ) + { + CTFWeaponBase *attackerWeapon = assert_cast< CTFWeaponBase * >( info.GetWeapon() ); + bool isMeleeAttack = attackerWeapon && attackerWeapon->IsMeleeWeapon(); + + RememberAttacker( playerAttacker, isMeleeAttack, info.GetDamage() ); + + for( int i=0; i<playerAttacker->m_Shared.GetNumHealers(); ++i ) + { + CTFPlayer *medic = ToTFPlayer( playerAttacker->m_Shared.GetHealerByIndex( i ) ); + if ( medic ) + { + // medics healing my attacker are also considered attackers + // they do zero damage to keep DPS calculations sane + RememberAttacker( medic, isMeleeAttack, 0.0f ); + } + } + } + + // fire event for client combat text, beep, etc. + IGameEvent *event = gameeventmanager->CreateEvent( "npc_hurt" ); + if ( event ) + { + event->SetInt( "entindex", entindex() ); + event->SetInt( "health", MAX( 0, GetHealth() ) ); + event->SetInt( "damageamount", info.GetDamage() ); + event->SetBool( "crit", ( info.GetDamageType() & DMG_CRITICAL ) ? true : false ); + event->SetInt( "boss", GetBossType() ); + + CTFPlayer *attackerPlayer = ToTFPlayer( info.GetAttacker() ); + if ( attackerPlayer ) + { + event->SetInt( "attacker_player", attackerPlayer->GetUserID() ); + + if ( attackerPlayer->GetActiveTFWeapon() ) + { + event->SetInt( "weaponid", attackerPlayer->GetActiveTFWeapon()->GetWeaponID() ); + } + else + { + event->SetInt( "weaponid", 0 ); + } + } + else + { + // hurt by world + event->SetInt( "attacker_player", 0 ); + event->SetInt( "weaponid", 0 ); + } + + gameeventmanager->FireEvent( event ); + } + + return BaseClass::OnTakeDamage_Alive( info ); +} + +void CHalloweenBaseBoss::Event_Killed( const CTakeDamageInfo &info ) +{ + const CUtlVector< AttackerInfo > &attackerVector = GetAttackerVector(); + for( int i=0; i<attackerVector.Count(); ++i ) + { + if ( attackerVector[i].m_attacker != NULL ) + { + IGameEvent *pEvent = gameeventmanager->CreateEvent( "halloween_boss_killed" ); + if ( pEvent ) + { + pEvent->SetInt( "boss", GetBossType() ); + pEvent->SetInt( "killer", attackerVector[i].m_attacker->GetUserID() ); + gameeventmanager->FireEvent( pEvent, true ); + } + + // Shoot a huge soul at players that have damaged the boss + TFGameRules()->DropHalloweenSoulPack( 25, EyePosition(), attackerVector[i].m_attacker, TEAM_SPECTATOR ); + } + } + + BaseClass::Event_Killed( info ); +} + +//--------------------------------------------------------------------------------------------- +void CHalloweenBaseBoss::RememberAttacker( CTFPlayer *playerAttacker, bool wasMeleeHit, float damage ) +{ + // record the damage for dps calculations + DamageRateInfo info; + info.m_damage = damage; + info.m_timestamp = gpGlobals->curtime; + m_damageVector.AddToTail( info ); + + int i; + + // has this player hurt me before + for( i=0; i<m_attackerVector.Count(); ++i ) + { + if ( m_attackerVector[i].m_attacker && m_attackerVector[i].m_attacker->entindex() == playerAttacker->entindex() ) + { + // this player is hurting me again + m_attackerVector[i].m_timestamp = gpGlobals->curtime; + m_attackerVector[i].m_wasLastHitFromMeleeWeapon = wasMeleeHit; + return; + } + } + + // new attacker + AttackerInfo attackerInfo; + + attackerInfo.m_attacker = playerAttacker; + attackerInfo.m_timestamp = gpGlobals->curtime; + attackerInfo.m_wasLastHitFromMeleeWeapon = wasMeleeHit; + + m_attackerVector.AddToTail( attackerInfo ); +} + + +//--------------------------------------------------------------------------------------------- +void CHalloweenBaseBoss::UpdateDamagePerSecond( void ) +{ + const float windowDuration = 5.0f; + int i; + + m_damagePerSecond = 0.0f; + + for( i=0; i<m_damageVector.Count(); ++i ) + { + float age = gpGlobals->curtime - m_damageVector[i].m_timestamp; + + if ( age > windowDuration ) + { + // too old + continue; + } + + m_damagePerSecond += m_damageVector[i].m_damage; + } + + m_damagePerSecond /= windowDuration; + + if ( m_damagePerSecond > m_maxDamagePerSecond ) + { + m_maxDamagePerSecond = m_damagePerSecond; + } + +// if ( m_damagePerSecond > 0.0001f ) +// { +// DevMsg( "%3.2f: dps = %3.2f\n", gpGlobals->curtime, m_damagePerSecond ); +// } +} + + +//--------------------------------------------------------------------------------------------- +void CHalloweenBaseBoss::Break( void ) +{ + CPVSFilter filter( GetAbsOrigin() ); + UserMessageBegin( filter, "BreakModel" ); + WRITE_SHORT( GetModelIndex() ); + WRITE_VEC3COORD( GetAbsOrigin() ); + WRITE_ANGLES( GetAbsAngles() ); + WRITE_SHORT( m_nSkin ); + MessageEnd(); +} + + +//--------------------------------------------------------------------------------------------- +/*static*/ CHalloweenBaseBoss* CHalloweenBaseBoss::SpawnBossAtPos( HalloweenBossType bossType, const Vector& vSpawnPos, int nTeam /*= TF_TEAM_HALLOWEEN*/, CBaseEntity* pOwner /*= NULL*/ ) +{ + const char* pszBossType = NULL; + switch ( bossType ) + { + case HALLOWEEN_BOSS_HHH: + pszBossType = "headless_hatman"; + break; + case HALLOWEEN_BOSS_MONOCULUS: + pszBossType = "eyeball_boss"; + break; + case HALLOWEEN_BOSS_MERASMUS: + pszBossType = "merasmus"; + break; + default: + AssertMsg( 0, "Invalid Halloween Boss Type" ); + } + + CHalloweenBaseBoss *pBoss = NULL; + if ( pszBossType ) + { + pBoss = dynamic_cast< CHalloweenBaseBoss * >( CreateEntityByName( pszBossType ) ); + if ( pBoss ) + { + pBoss->SetAbsOrigin( vSpawnPos + Vector( 0, 0, 10.0f ) ); + pBoss->ChangeTeam( nTeam ); + pBoss->SetOwnerEntity( pOwner ); + + DispatchSpawn( pBoss ); + } + } + + return pBoss; +} diff --git a/game/server/tf/halloween/halloween_base_boss.h b/game/server/tf/halloween/halloween_base_boss.h new file mode 100644 index 0000000..3cb6c03 --- /dev/null +++ b/game/server/tf/halloween/halloween_base_boss.h @@ -0,0 +1,109 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// halloween_boss_base.h +// Shared code for the Halloween Bosses +// Michael Booth, October 2011 + +#ifndef HALLOWEEN_BOSS_BASE_H +#define HALLOWEEN_BOSS_BASE_H + +#include "tf_shareddefs.h" +#include "NextBot.h" +#include "NextBotBehavior.h" +#include "NextBotGroundLocomotion.h" +#include "headless_hatman_body.h" +#include "Path/NextBotPathFollow.h" + + +class CTFPlayer; + + +//---------------------------------------------------------------------------- +class CHalloweenBaseBoss : public NextBotCombatCharacter +{ +public: + DECLARE_CLASS( CHalloweenBaseBoss, NextBotCombatCharacter ); + + CHalloweenBaseBoss(); + virtual ~CHalloweenBaseBoss(); + + virtual void Spawn( void ); + virtual int OnTakeDamage( const CTakeDamageInfo &rawInfo ) OVERRIDE; + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + virtual void Event_Killed( const CTakeDamageInfo &info ) OVERRIDE; + virtual void UpdateOnRemove(); + + virtual void Update( void ); + + void Break( void ); // bust into gibs + + struct AttackerInfo + { + CHandle< CTFPlayer > m_attacker; + float m_timestamp; + bool m_wasLastHitFromMeleeWeapon; + }; + const CUtlVector< AttackerInfo > &GetAttackerVector( void ) const; + void RememberAttacker( CTFPlayer *player, bool wasMeleeHit, float damage ); + + bool WasSpawnedByCheats( void ) const; + + virtual float GetCritInjuryMultiplier( void ) const; // when we are hit by a crit, damage is mutiplied by this + + float GetInjuryRate( void ) const; // return average recent damage taken per second + float GetMaxInjuryRate( void ) const; // return maximum damage taken per second + + virtual int GetLevel() const { return 0; } + + virtual HalloweenBossType GetBossType() const { return HALLOWEEN_BOSS_INVALID; } + static CHalloweenBaseBoss* SpawnBossAtPos( HalloweenBossType bossType, const Vector& vSpawnPos, int nTeam = TF_TEAM_HALLOWEEN, CBaseEntity* pOwner = NULL ); + + bool IsSpell() const { return GetTeamNumber() != TF_TEAM_HALLOWEEN; } + + enum HalloweenStatsEventType + { + HALLOWEEN_EVENT_BOSS_SPAWN = 0, + }; + +private: + CUtlVector< AttackerInfo > m_attackerVector; // list of everyone who injured me, and when + + void UpdateDamagePerSecond( void ); + struct DamageRateInfo + { + float m_timestamp; + float m_damage; + }; + CUtlVector< DamageRateInfo > m_damageVector; + + float m_damagePerSecond; + float m_maxDamagePerSecond; + + bool m_wasSpawnedByCheats; +}; + +inline float CHalloweenBaseBoss::GetInjuryRate( void ) const +{ + return m_damagePerSecond; +} + +inline float CHalloweenBaseBoss::GetMaxInjuryRate( void ) const +{ + return m_maxDamagePerSecond; +} + +inline float CHalloweenBaseBoss::GetCritInjuryMultiplier( void ) const +{ + return TF_DAMAGE_CRIT_MULTIPLIER; +} + +inline bool CHalloweenBaseBoss::WasSpawnedByCheats( void ) const +{ + return m_wasSpawnedByCheats; +} + +inline const CUtlVector< CHalloweenBaseBoss::AttackerInfo > &CHalloweenBaseBoss::GetAttackerVector( void ) const +{ + return m_attackerVector; +} + +#endif // HALLOWEEN_BOSS_BASE_H diff --git a/game/server/tf/halloween/halloween_behavior/crybaby_boss_attack.cpp b/game/server/tf/halloween/halloween_behavior/crybaby_boss_attack.cpp new file mode 100644 index 0000000..e2decd4 --- /dev/null +++ b/game/server/tf/halloween/halloween_behavior/crybaby_boss_attack.cpp @@ -0,0 +1,269 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// crybaby_boss_attack.cpp +// Halloween Boss 2011 chase and attack behavior +// Michael Booth, October 2011 + +#include "cbase.h" + +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "nav_mesh/tf_nav_area.h" +#include "particle_parse.h" +#include "team_control_point_master.h" + +#include "../headless_hatman.h" +#include "crybaby_boss_attack.h" + + +//---------------------------------------------------------------------------------- +CCryBabyBossAttack::CCryBabyBossAttack( CTFPlayer *victim ) +{ + m_victim = victim; +} + + +//---------------------------------------------------------------------------------- +ActionResult< CHeadlessHatman > CCryBabyBossAttack::OnStart( CHeadlessHatman *me, Action< CHeadlessHatman > *priorAction ) +{ + // start animation + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_ITEM1 ); + + m_axeSwingTimer.Invalidate(); + m_attackTimer.Invalidate(); + + m_attackTarget = NULL; + m_attackTargetFocusTimer.Invalidate(); + + return Continue(); +} + + +//---------------------------------------------------------------------------------- +bool CCryBabyBossAttack::IsSwingingAxe( void ) const +{ + return !m_axeSwingTimer.IsElapsed(); +} + + +//---------------------------------------------------------------------------------- +void CCryBabyBossAttack::UpdateAxeSwing( CHeadlessHatman *me ) +{ + if ( m_axeSwingTimer.HasStarted() ) + { + // continue axe swing + if ( m_axeSwingTimer.IsElapsed() ) + { + // moment of impact - did axe swing hit? + m_axeSwingTimer.Invalidate(); + + if ( m_attackTarget != NULL ) + { + Vector forward; + me->GetVectors( &forward, NULL, NULL ); + + Vector toVictim = m_attackTarget->WorldSpaceCenter() - me->WorldSpaceCenter(); + toVictim.NormalizeInPlace(); + + // looser tolerance as victim gets closer + const float closeRange = 100.0f; + float range = me->GetRangeTo( m_attackTarget ); + float closeness = ( range < closeRange ) ? 0.0f : ( range - closeRange ) / ( tf_halloween_bot_attack_range.GetFloat() - closeRange ); + float hitAngle = 0.0f + closeness * 0.27f; + + if ( DotProduct( forward, toVictim ) > hitAngle ) + { + if ( me->IsRangeLessThan( m_attackTarget, 0.9f * tf_halloween_bot_attack_range.GetFloat() ) ) + { + if ( me->IsLineOfSightClear( m_attackTarget ) ) + { + // CHOP! + CTakeDamageInfo info( me, me, 2.0f * m_attackTarget->GetHealth(), DMG_CLUB, TF_DMG_CUSTOM_DECAPITATION_BOSS ); + CalculateMeleeDamageForce( &info, toVictim, me->WorldSpaceCenter(), 5.0f ); + m_attackTarget->TakeDamage( info ); + me->EmitSound( "Halloween.HeadlessBossAxeHitFlesh" ); + } + } + } + } + + // always playe the axe-hit-world impact sound, since it carries through the world better + me->EmitSound( "Halloween.HeadlessBossAxeHitWorld" ); + UTIL_ScreenShake( me->GetAbsOrigin(), 15.0f, 5.0f, 1.0f, 1000.0f, SHAKE_START ); + + Vector bladePos; + QAngle bladeAngle; + if ( me->GetAxe() && me->GetAxe()->GetAttachment( "axe_blade", bladePos, bladeAngle ) ) + { + DispatchParticleEffect( "halloween_boss_axe_hit_world", bladePos, bladeAngle ); + } + } + } +} + + +//---------------------------------------------------------------------------------- +// Validate that our victim is still alive and someplace we can actually get to +bool CCryBabyBossAttack::IsVictimChaseable( CHeadlessHatman *me ) +{ + if ( m_victim == NULL ) + { + return false; + } + + if ( !m_victim->IsAlive() ) + { + return false; + } + + CTFNavArea *victimArea = (CTFNavArea *)m_victim->GetLastKnownArea(); + if ( !victimArea || victimArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED ) ) + { + // unreachable + return false; + } + + if ( m_victim->GetGroundEntity() != NULL ) + { + Vector victimAreaPos; + victimArea->GetClosestPointOnArea( m_victim->GetAbsOrigin(), &victimAreaPos ); + if ( ( m_victim->GetAbsOrigin() - victimAreaPos ).AsVector2D().IsLengthGreaterThan( 50.0f ) ) + { + // off the mesh and unreachable + return false; + } + } + + return true; +} + + +//---------------------------------------------------------------------------------- +ActionResult< CHeadlessHatman > CCryBabyBossAttack::Update( CHeadlessHatman *me, float interval ) +{ + if ( !me->IsAlive() ) + { + return Done(); + } + + if ( !IsVictimChaseable( me ) ) + { + return Done( "Victim is dead or unreachable" ); + } + + if ( !IsSwingingAxe() && m_laughTimer.IsElapsed() ) + { + me->EmitSound( "Halloween.HeadlessBossLaugh" ); + m_laughTimer.Start( RandomFloat( 3.0f, 5.0f ) ); + } + + // chase after our chase victim + const float standAndSwingRange = 100.0f; + + if ( me->IsRangeGreaterThan( m_victim, standAndSwingRange ) || !me->IsLineOfSightClear( m_victim ) ) + { + CHeadlessHatmanPathCost cost( me ); + m_chasePath.Update( me, m_victim, cost ); + } + + if ( m_attackTargetFocusTimer.IsElapsed() || m_attackTarget == NULL ) + { + m_attackTarget = m_victim.Get(); + } + + // swing our axe at our attack target if they are in range + if ( m_attackTarget != NULL && m_attackTarget->IsAlive() ) + { + if ( me->IsRangeLessThan( m_attackTarget, tf_halloween_bot_attack_range.GetFloat() ) ) + { + if ( m_attackTimer.IsElapsed() ) + { + if ( !IsSwingingAxe() ) + { + me->AddGesture( ACT_MP_ATTACK_STAND_ITEM1 ); + m_axeSwingTimer.Start( 0.58f ); + me->EmitSound( "Halloween.HeadlessBossAttack" ); + + m_attackTimer.Start( 1.0f ); + } + } + + me->GetLocomotionInterface()->FaceTowards( m_attackTarget->WorldSpaceCenter() ); + } + } + + // finish axe swing once it has begun + UpdateAxeSwing( me ); + + if ( me->GetLocomotionInterface()->IsAttemptingToMove() ) + { + // play running animation + int healthRatio = 100 * me->GetHealth() / me->GetMaxHealth(); + + if ( healthRatio > 50 ) + { + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_RUN_ITEM1 ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_ITEM1 ); + } + } + else + { + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_RUN_MELEE ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE ); + } + } + + if ( m_footfallTimer.IsElapsed() ) + { + m_footfallTimer.Start( 0.25f ); + + // periodically shake nearby players' screens when we're moving + UTIL_ScreenShake( me->GetAbsOrigin(), 15.0f, 5.0f, 1.0f, 1000.0f, SHAKE_START ); + } + } + else + { + // standing still + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_STAND_ITEM1 ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_ITEM1 ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CHeadlessHatman > CCryBabyBossAttack::OnStuck( CHeadlessHatman *me ) +{ + // we're stuck - just warp to the our next path goal + if ( m_chasePath.GetCurrentGoal() ) + { + me->SetAbsOrigin( m_chasePath.GetCurrentGoal()->pos + Vector( 0, 0, 10.0f ) ); + } + + return TryContinue( RESULT_TRY ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CHeadlessHatman > CCryBabyBossAttack::OnContact( CHeadlessHatman *me, CBaseEntity *other, CGameTrace *result ) +{ + if ( other ) + { + CBaseCombatCharacter *combatChar = dynamic_cast< CBaseCombatCharacter * >( other ); + if ( combatChar && combatChar->IsAlive() ) + { + // force attack the thing we bumped into + // this prevents us from being stuck on dispensers, for example + m_attackTarget = combatChar; + m_attackTargetFocusTimer.Start( 3.0f ); + } + } + + return TryContinue( RESULT_TRY ); +} + diff --git a/game/server/tf/halloween/halloween_behavior/crybaby_boss_attack.h b/game/server/tf/halloween/halloween_behavior/crybaby_boss_attack.h new file mode 100644 index 0000000..8c2cff4 --- /dev/null +++ b/game/server/tf/halloween/halloween_behavior/crybaby_boss_attack.h @@ -0,0 +1,47 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// crybaby_boss_attack.h +// Halloween Boss 2011 chase and attack behavior +// Michael Booth, October 2011 + +#ifndef CRYBABY_BOSS_ATTACK_H +#define CRYBABY_BOSS_ATTACK_H + +#include "NextBot/Path/NextBotChasePath.h" + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CCryBabyBossAttack : public Action< CHeadlessHatman > +{ +public: + CCryBabyBossAttack( CTFPlayer *victim ); + virtual ~CCryBabyBossAttack() { } + + virtual ActionResult< CHeadlessHatman > OnStart( CHeadlessHatman *me, Action< CHeadlessHatman > *priorAction ); + virtual ActionResult< CHeadlessHatman > Update( CHeadlessHatman *me, float interval ); + + virtual EventDesiredResult< CHeadlessHatman > OnStuck( CHeadlessHatman *me ); + virtual EventDesiredResult< CHeadlessHatman > OnContact( CHeadlessHatman *me, CBaseEntity *other, CGameTrace *result = NULL ); + + virtual const char *GetName( void ) const { return "CryBabyBossAttack"; } // return name of this action + +private: + ChasePath m_chasePath; + CHandle< CTFPlayer > m_victim; + + CountdownTimer m_axeSwingTimer; + CountdownTimer m_attackTimer; + CountdownTimer m_laughTimer; + CountdownTimer m_footfallTimer; + + void UpdateAxeSwing( CHeadlessHatman *me ); + bool IsSwingingAxe( void ) const; + + bool IsVictimChaseable( CHeadlessHatman *me ); + + CHandle< CBaseCombatCharacter > m_attackTarget; // the victim I'm momentarily attacking + CountdownTimer m_attackTargetFocusTimer; +}; + + + +#endif // CRYBABY_BOSS_ATTACK_H diff --git a/game/server/tf/halloween/halloween_behavior/crybaby_boss_escape.cpp b/game/server/tf/halloween/halloween_behavior/crybaby_boss_escape.cpp new file mode 100644 index 0000000..ba64933 --- /dev/null +++ b/game/server/tf/halloween/halloween_behavior/crybaby_boss_escape.cpp @@ -0,0 +1,132 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// crybaby_boss_escape.cpp +// Crybaby Boss runs to the pit in Mann Manor to escape +// Michael Booth, October 2011 + +#include "cbase.h" + +#include "nav_mesh.h" +#include "tf_player.h" + +#include "../headless_hatman.h" +#include "crybaby_boss_escape.h" +#include "crybaby_boss_attack.h" + +extern ConVar tf_bot_path_lookahead_range; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CHeadlessHatman > CCryBabyBossEscape::OnStart( CHeadlessHatman *me, Action< CHeadlessHatman > *priorAction ) +{ + m_path.SetMinLookAheadDistance( tf_bot_path_lookahead_range.GetFloat() ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CHeadlessHatman > CCryBabyBossEscape::Update( CHeadlessHatman *me, float interval ) +{ + // any nearby player that taunts me, enrages me and provokes me to attack them! + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TEAM_ANY, COLLECT_ONLY_LIVING_PLAYERS ); + + for( int i=0; i<playerVector.Count(); ++i ) + { + if ( playerVector[i]->m_Shared.InCond( TF_COND_TAUNTING ) ) + { + const float noticeTauntRange = 750.0f; + if ( me->IsRangeLessThan( playerVector[i], noticeTauntRange ) ) + { + if ( playerVector[i]->IsLookingTowards( me ) ) + { + if ( me->IsLineOfSightClear( playerVector[i] ) ) + { + return SuspendFor( new CCryBabyBossAttack( playerVector[i] ), "DON'T LAUGH AT ME!!!" ); + } + } + } + } + } + + // push players away to avoid penetration issues + const float pushRange = 250.0f; + const float pushForce = 200.0f; + + for( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *player = playerVector[i]; + + Vector toPlayer = player->EyePosition() - me->GetAbsOrigin(); + float range = toPlayer.NormalizeInPlace(); + + if ( range < pushRange ) + { + // make sure we push players up and away + toPlayer.z = 0.0f; + toPlayer.NormalizeInPlace(); + toPlayer.z = 1.0f; + + Vector push = pushForce * toPlayer; + + player->ApplyAbsVelocityImpulse( push ); + } + } + + // run to the pit and escape + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CHeadlessHatmanPathCost cost( me ); + + // hack for now + CNavArea *goalArea = TheNavMesh->GetNavAreaByID( 27 ); + if ( !goalArea ) + { + return Done( "No goal area!" ); + } + + m_path.Compute( me, goalArea->GetCenter(), cost ); + } + + m_path.Update( me ); + + // animation state + if ( me->GetLocomotionInterface()->IsAttemptingToMove() ) + { + // play running animation + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_RUN_MELEE ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE ); + } + + if ( m_footfallTimer.IsElapsed() ) + { + m_footfallTimer.Start( 0.25f ); + + // periodically shake nearby players' screens when we're moving + UTIL_ScreenShake( me->GetAbsOrigin(), 15.0f, 5.0f, 1.0f, 1000.0f, SHAKE_START ); + } + } + else + { + // standing still + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_STAND_ITEM1 ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_ITEM1 ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CHeadlessHatman > CCryBabyBossEscape::OnMoveToSuccess( CHeadlessHatman *me, const Path *path ) +{ + UTIL_Remove( me ); + + return TryDone( RESULT_CRITICAL, "Reached escape pit" ); +} + diff --git a/game/server/tf/halloween/halloween_behavior/crybaby_boss_escape.h b/game/server/tf/halloween/halloween_behavior/crybaby_boss_escape.h new file mode 100644 index 0000000..7f16d39 --- /dev/null +++ b/game/server/tf/halloween/halloween_behavior/crybaby_boss_escape.h @@ -0,0 +1,28 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// crybaby_boss_escape.h +// Crybaby Boss runs to the pit in Mann Manor to escape +// Michael Booth, October 2011 + +#ifndef CRYBABY_ESCAPE_H +#define CRYBABY_ESCAPE_H + +#include "Path/NextBotPathFollow.h" + +class CCryBabyBossEscape : public Action< CHeadlessHatman > +{ +public: + virtual ActionResult< CHeadlessHatman > OnStart( CHeadlessHatman *me, Action< CHeadlessHatman > *priorAction ); + virtual ActionResult< CHeadlessHatman > Update( CHeadlessHatman *me, float interval ); + + virtual EventDesiredResult< CHeadlessHatman > OnMoveToSuccess( CHeadlessHatman *me, const Path *path ); + + virtual const char *GetName( void ) const { return "CryBabyBossEscape"; }; + +private: + PathFollower m_path; + CountdownTimer m_repathTimer; + CountdownTimer m_footfallTimer; +}; + + +#endif // CRYBABY_ESCAPE_H diff --git a/game/server/tf/halloween/halloween_behavior/headless_hatman_attack.cpp b/game/server/tf/halloween/halloween_behavior/headless_hatman_attack.cpp new file mode 100644 index 0000000..014991c --- /dev/null +++ b/game/server/tf/halloween/halloween_behavior/headless_hatman_attack.cpp @@ -0,0 +1,543 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// headless_hatman_attack.cpp +// Halloween Boss chase and attack behavior +// Michael Booth, October 2010 + +#include "cbase.h" + +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "nav_mesh/tf_nav_area.h" +#include "particle_parse.h" +#include "team_control_point_master.h" + +#include "../headless_hatman.h" +#include "headless_hatman_attack.h" +#include "headless_hatman_terrify.h" + + +ConVar tf_halloween_hhh_attack_kart_radius( "tf_halloween_hhh_attack_kart_radius", "300", FCVAR_CHEAT ); + +//---------------------------------------------------------------------------------- +ActionResult< CHeadlessHatman > CHeadlessHatmanAttack::OnStart( CHeadlessHatman *me, Action< CHeadlessHatman > *priorAction ) +{ + // smooth out the bot's path following by moving toward a point farther down the path + m_path.SetMinLookAheadDistance( 100.0f ); + + // start animation + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_ITEM1 ); + + m_axeSwingTimer.Invalidate(); + m_attackTimer.Invalidate(); + + m_closestVisible = NULL; + TFGameRules()->SetIT( NULL ); + m_lastIT = NULL; + m_chaseVictimTimer.Invalidate(); + + m_attackTarget = NULL; + m_attackTargetFocusTimer.Invalidate(); + + m_scareTimer.Start( 20.0f ); + + m_homePos = me->GetAbsOrigin(); + m_homePosRecalcTimer.Start( 3.0f ); + + return Continue(); +} + + +//---------------------------------------------------------------------------------- +bool CHeadlessHatmanAttack::IsSwingingAxe( void ) const +{ + return !m_axeSwingTimer.IsElapsed(); +} + + +//---------------------------------------------------------------------------------- +bool CHeadlessHatmanAttack::IsPotentiallyChaseable( CHeadlessHatman *me, CTFPlayer *victim ) +{ + if ( !victim ) + { + return false; + } + + if ( !victim->IsAlive() ) + { + // victim is dead - pick a new one + return false; + } + + if ( victim->m_Shared.InCond( TF_COND_HALLOWEEN_GHOST_MODE ) ) + { + // don't attack ghost + return false; + } + + CTFNavArea *victimArea = (CTFNavArea *)victim->GetLastKnownArea(); + if ( !victimArea || victimArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED ) ) + { + // unreachable - pick a new victim + return false; + } + + if ( victim->GetGroundEntity() != NULL ) + { + Vector victimAreaPos; + victimArea->GetClosestPointOnArea( victim->GetAbsOrigin(), &victimAreaPos ); + if ( ( victim->GetAbsOrigin() - victimAreaPos ).AsVector2D().IsLengthGreaterThan( 50.0f ) ) + { + // off the mesh and unreachable - pick a new victim + return false; + } + } + + if ( victim->m_Shared.IsInvulnerable() ) + { + // invulnerable - pick a new victim + return false; + } + + Vector toHome = m_homePos - victim->GetAbsOrigin(); + if ( !victim->m_Shared.InCond( TF_COND_HALLOWEEN_KART ) ) + { + if ( toHome.IsLengthGreaterThan( tf_halloween_bot_quit_range.GetFloat() ) ) + { + // too far from home - pick a new victim + return false; + } + } + + return true; +} + + +//---------------------------------------------------------------------------------- +void CHeadlessHatmanAttack::ValidateChaseVictim( CHeadlessHatman *me ) +{ + CTFPlayer *victim = ToTFPlayer( TFGameRules()->GetIT() ); + + if ( victim && m_lastIT != victim ) + { + // something has changed our victim + m_chaseVictimTimer.Start( tf_halloween_bot_chase_duration.GetFloat() ); + m_lastIT = victim; + } + + if ( !IsPotentiallyChaseable( me, victim ) ) + { + // we can no longer reach this victim + TFGameRules()->SetIT( NULL ); + } +} + + +//---------------------------------------------------------------------------------- +void CHeadlessHatmanAttack::SelectVictim( CHeadlessHatman *me ) +{ + ValidateChaseVictim( me ); + + m_closestVisible = ToTFPlayer( TFGameRules()->GetIT() ); + + if ( TFGameRules()->GetIT() == NULL ) + { + // pick a new victim to chase + CTFPlayer *newVictim = NULL; + CUtlVector< CTFPlayer * > playerVector; + + // collect everyone + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + float victimRangeSq = FLT_MAX; + float visibleVictimRangeSq = FLT_MAX; + + for( int i=0; i<playerVector.Count(); ++i ) + { + if ( !IsPotentiallyChaseable( me, playerVector[i] ) ) + { + continue; + } + + float rangeSq = me->GetRangeSquaredTo( playerVector[i] ); + if ( rangeSq < visibleVictimRangeSq ) + { + if ( me->IsLineOfSightClear( playerVector[i] ) ) + { + // track closest visible victim so we can look at players out of attack range + m_closestVisible = playerVector[i]; + visibleVictimRangeSq = rangeSq; + } + } + + // check distance to home - don't pick victims too far away from home + if ( !playerVector[i]->m_Shared.InCond( TF_COND_HALLOWEEN_KART ) ) + { + Vector toHome = m_homePos - playerVector[i]->GetAbsOrigin(); + if ( toHome.IsLengthGreaterThan( tf_halloween_bot_chase_range.GetFloat() ) ) + { + continue; + } + } + + if ( rangeSq < victimRangeSq ) + { + newVictim = playerVector[i]; + victimRangeSq = rangeSq; + } + } + + if ( newVictim ) + { + // we have a new victim + m_chaseVictimTimer.Start( tf_halloween_bot_chase_duration.GetFloat() ); + + TFGameRules()->SetIT( newVictim ); + } + } + + // attack the "IT" player unless we're focusing on something else + if ( m_attackTarget == NULL || m_attackTargetFocusTimer.IsElapsed() || !m_attackTarget->IsAlive() ) + { + m_attackTarget = ToTFPlayer( TFGameRules()->GetIT() ); + } +} + +//---------------------------------------------------------------------------------- +void CHeadlessHatmanAttack::AttackTarget( CHeadlessHatman *me, CBaseCombatCharacter *pTarget, float flAttackRange ) +{ + // out of range? don't do anything + if ( !me->IsRangeLessThan( pTarget, flAttackRange ) ) + return; + + Vector forward; + me->GetVectors( &forward, NULL, NULL ); + + Vector toVictim = pTarget->WorldSpaceCenter() - me->WorldSpaceCenter(); + toVictim.NormalizeInPlace(); + + CTFPlayer *pPlayer = pTarget->IsPlayer() ? ToTFPlayer( pTarget ) : NULL; + // push kart player or ghost + if ( pPlayer && ( pPlayer->m_Shared.InCond( TF_COND_HALLOWEEN_KART ) || pPlayer->m_Shared.InCond( TF_COND_HALLOWEEN_GHOST_MODE ) ) ) + { + // Apply Huge Force + Vector vecDir = toVictim; + vecDir.NormalizeInPlace(); + vecDir.z += 0.7f; + + vecDir *= 1300.0f; + + if ( pPlayer->m_Shared.InCond( TF_COND_HALLOWEEN_KART ) ) + { + pPlayer->AddHalloweenKartPushEvent( NULL, NULL, NULL, vecDir, 100.0f, TF_DMG_CUSTOM_DECAPITATION_BOSS ); + } + else + { + pPlayer->ApplyAbsVelocityImpulse( vecDir ); + } + } + else + { + // looser tolerance as victim gets closer + const float closeRange = 0.5f * flAttackRange; + float range = me->GetRangeTo( pTarget ); + float closeness = ( range < closeRange ) ? 0.0f : ( range - closeRange ) / ( flAttackRange - closeRange ); + float hitAngle = 0.0f + closeness * 0.27f; + + // is target in front? + if ( DotProduct( forward, toVictim ) > hitAngle ) + { + if ( me->IsLineOfSightClear( pTarget ) ) + { + // CHOP! + CTakeDamageInfo info( me, me, m_attackTarget->GetMaxHealth() * 0.8f, DMG_CLUB, TF_DMG_CUSTOM_DECAPITATION_BOSS ); + CalculateMeleeDamageForce( &info, toVictim, me->WorldSpaceCenter(), 5.0f ); + m_attackTarget->TakeDamage( info ); + me->EmitSound( "Halloween.HeadlessBossAxeHitFlesh" ); + } + } + } +} + + +//---------------------------------------------------------------------------------- +void CHeadlessHatmanAttack::UpdateAxeSwing( CHeadlessHatman *me ) +{ + if ( m_axeSwingTimer.HasStarted() ) + { + // continue axe swing + if ( m_axeSwingTimer.IsElapsed() ) + { + // moment of impact - did axe swing hit? + m_axeSwingTimer.Invalidate(); + + if ( TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_DOOMSDAY ) ) + { + CUtlVector< CTFPlayer* > playerVector; + CollectPlayers( &playerVector, TEAM_ANY ); + for( CTFPlayer* pPlayer : playerVector ) + { + AttackTarget( me, pPlayer, tf_halloween_hhh_attack_kart_radius.GetFloat() ); + } + + DispatchParticleEffect( "hammer_impact_button", me->GetAbsOrigin(), me->GetAbsAngles() ); + + me->EmitSound( "Halloween.HammerImpact" ); + + // after HHH punt off a kart target, pick a new target right away + TFGameRules()->SetIT( NULL ); + } + else if ( m_attackTarget != NULL ) + { + AttackTarget( me, m_attackTarget, tf_halloween_bot_attack_range.GetFloat() ); + } + + // always playe the axe-hit-world impact sound, since it carries through the world better + me->EmitSound( "Halloween.HeadlessBossAxeHitWorld" ); + if ( !TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_DOOMSDAY ) ) + { + UTIL_ScreenShake( me->GetAbsOrigin(), 15.0f, 5.0f, 1.0f, 1000.0f, SHAKE_START ); + } + + Vector bladePos; + QAngle bladeAngle; + if ( me->GetAxe() && me->GetAxe()->GetAttachment( "axe_blade", bladePos, bladeAngle ) ) + { + DispatchParticleEffect( "halloween_boss_axe_hit_world", bladePos, bladeAngle ); + } + } + } +} + + +//---------------------------------------------------------------------------------- +ActionResult< CHeadlessHatman > CHeadlessHatmanAttack::Update( CHeadlessHatman *me, float interval ) +{ + if ( !me->IsAlive() ) + { + return Done(); + } + + SelectVictim( me ); + RecomputeHomePosition(); + + if ( !IsSwingingAxe() && m_laughTimer.IsElapsed() ) + { + me->EmitSound( "Halloween.HeadlessBossLaugh" ); + m_laughTimer.Start( RandomFloat( 3.0f, 5.0f ) ); + + if ( TFGameRules()->GetIT() == NULL ) + { + // if we have no victim, show our frustration + int r = RandomInt( 0, 100 ); + if ( r < 25 ) + { + me->AddGesture( ACT_MP_GESTURE_VC_FISTPUMP_MELEE ); + } + else if ( r < 50 ) + { + me->AddGesture( ACT_MP_GESTURE_VC_FINGERPOINT_MELEE ); + } + } + } + + if ( TFGameRules()->GetIT() == NULL ) + { + // go home + const float atHomeRange = 50.0f; + if ( me->IsRangeGreaterThan( m_homePos, atHomeRange ) ) + { + if ( m_path.GetAge() > 3.0f ) + { + CHeadlessHatmanPathCost cost( me ); + m_path.Compute( me, m_homePos, cost ); + } + + m_path.Update( me ); + } + else // at home + { + if ( m_closestVisible != NULL ) + { + // look at visible victim out of range + me->GetLocomotionInterface()->FaceTowards( m_closestVisible->WorldSpaceCenter() ); + } + } + } + else + { + // chase after our chase victim + const float standAndSwingRange = 100.0f; + CTFPlayer *chaseVictim = ToTFPlayer( TFGameRules()->GetIT() ); + + if ( chaseVictim && m_warnITTimer.IsElapsed() && !TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_DOOMSDAY ) ) + { + m_warnITTimer.Start( 7.0f ); + ClientPrint( chaseVictim, HUD_PRINTCENTER, "#TF_HALLOWEEN_BOSS_WARN_VICTIM", chaseVictim->GetPlayerName() ); + } + + if ( me->IsRangeGreaterThan( chaseVictim, standAndSwingRange ) || !me->IsLineOfSightClear( chaseVictim ) ) + { + if ( m_path.GetAge() > 1.0f ) + { + CHeadlessHatmanPathCost cost( me ); + m_path.Compute( me, chaseVictim, cost ); + } + + m_path.Update( me ); + } + } + + // swing our axe at our attack target if they are in range + if ( m_attackTarget != NULL && m_attackTarget->IsAlive() ) + { + if ( me->IsRangeLessThan( m_attackTarget, tf_halloween_bot_attack_range.GetFloat() ) ) + { + if ( m_scareTimer.IsElapsed() && m_attackTarget->IsPlayer() ) + { + m_scareTimer.Reset(); + return SuspendFor( new CHeadlessHatmanTerrify, "Boo!" ); + } + + if ( m_attackTimer.IsElapsed() ) + { + if ( !IsSwingingAxe() ) + { + if ( TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_DOOMSDAY ) ) + { + me->AddGesture( ACT_MP_ATTACK_STAND_ITEM2 ); + } + else + { + me->AddGesture( ACT_MP_ATTACK_STAND_ITEM1 ); + } + + m_axeSwingTimer.Start( 0.58f ); + me->EmitSound( "Halloween.HeadlessBossAttack" ); + + m_attackTimer.Start( 1.0f ); + } + } + + me->GetLocomotionInterface()->FaceTowards( m_attackTarget->WorldSpaceCenter() ); + } + } + + // finish axe swing once it has begun + UpdateAxeSwing( me ); + + if ( me->GetLocomotionInterface()->IsAttemptingToMove() ) + { + // play running animation + int healthRatio = 100 * me->GetHealth() / me->GetMaxHealth(); + + if ( healthRatio > 50 ) + { + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_RUN_ITEM1 ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_ITEM1 ); + } + } + else + { + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_RUN_MELEE ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE ); + } + } + + if ( !TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_DOOMSDAY ) ) + { + if ( m_footfallTimer.IsElapsed() ) + { + m_footfallTimer.Start( 0.25f ); + + // periodically shake nearby players' screens when we're moving + UTIL_ScreenShake( me->GetAbsOrigin(), 15.0f, 5.0f, 1.0f, 1000.0f, SHAKE_START ); + } + } + } + else + { + // standing still + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_STAND_ITEM1 ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_ITEM1 ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CHeadlessHatman > CHeadlessHatmanAttack::OnStuck( CHeadlessHatman *me ) +{ + // we're stuck - just warp to the our next path goal + if ( m_path.GetCurrentGoal() ) + { + me->SetAbsOrigin( m_path.GetCurrentGoal()->pos + Vector( 0, 0, 10.0f ) ); + } + + return TryContinue( RESULT_TRY ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CHeadlessHatman > CHeadlessHatmanAttack::OnContact( CHeadlessHatman *me, CBaseEntity *other, CGameTrace *result ) +{ + if ( other ) + { + CBaseCombatCharacter *combatChar = dynamic_cast< CBaseCombatCharacter * >( other ); + if ( combatChar && combatChar->IsAlive() ) + { + // force attack the thing we bumped into + // this prevents us from being stuck on dispensers, for example + m_attackTarget = combatChar; + m_attackTargetFocusTimer.Start( 3.0f ); + } + } + + return TryContinue( RESULT_TRY ); +} + + +//--------------------------------------------------------------------------------------------- +void CHeadlessHatmanAttack::RecomputeHomePosition( void ) +{ + if ( !m_homePosRecalcTimer.IsElapsed() ) + { + return; + } + + m_homePosRecalcTimer.Reset(); + + CTeamControlPoint *contestedPoint = NULL; + CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + if ( pMaster ) + { + for( int i=0; i<pMaster->GetNumPoints(); ++i ) + { + contestedPoint = pMaster->GetControlPoint( i ); + if ( contestedPoint && pMaster->IsInRound( contestedPoint ) ) + { + if ( ObjectiveResource()->GetOwningTeam( contestedPoint->GetPointIndex() ) == TF_TEAM_BLUE ) + continue; + + // blue are the invaders + if ( !TeamplayGameRules()->TeamMayCapturePoint( TF_TEAM_BLUE, contestedPoint->GetPointIndex() ) ) + continue; + + break; + } + } + } + + if ( contestedPoint ) + { + m_homePos = contestedPoint->GetAbsOrigin(); + } +} + + diff --git a/game/server/tf/halloween/halloween_behavior/headless_hatman_attack.h b/game/server/tf/halloween/halloween_behavior/headless_hatman_attack.h new file mode 100644 index 0000000..d54ae80 --- /dev/null +++ b/game/server/tf/halloween/halloween_behavior/headless_hatman_attack.h @@ -0,0 +1,54 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// headless_hatman_attack.h +// Halloween Boss chase and attack behavior +// Michael Booth, October 2010 + +#ifndef HEADLESS_HATMAN_ATTACK_H +#define HEADLESS_HATMAN_ATTACK_H + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CHeadlessHatmanAttack : public Action< CHeadlessHatman > +{ +public: + virtual ActionResult< CHeadlessHatman > OnStart( CHeadlessHatman *me, Action< CHeadlessHatman > *priorAction ); + virtual ActionResult< CHeadlessHatman > Update( CHeadlessHatman *me, float interval ); + + virtual EventDesiredResult< CHeadlessHatman > OnStuck( CHeadlessHatman *me ); + virtual EventDesiredResult< CHeadlessHatman > OnContact( CHeadlessHatman *me, CBaseEntity *other, CGameTrace *result = NULL ); + + virtual const char *GetName( void ) const { return "Attack"; } // return name of this action + +private: + PathFollower m_path; + + Vector m_homePos; + CountdownTimer m_homePosRecalcTimer; + void RecomputeHomePosition( void ); + + CountdownTimer m_axeSwingTimer; + CountdownTimer m_attackTimer; + CountdownTimer m_laughTimer; + CountdownTimer m_scareTimer; + CountdownTimer m_warnITTimer; + CountdownTimer m_footfallTimer; + + void AttackTarget( CHeadlessHatman *me, CBaseCombatCharacter *pTarget, float flAttackRange ); + void UpdateAxeSwing( CHeadlessHatman *me ); + bool IsSwingingAxe( void ) const; + + CHandle< CTFPlayer > m_closestVisible; + + CHandle< CTFPlayer > m_lastIT; + CountdownTimer m_chaseVictimTimer; + void ValidateChaseVictim( CHeadlessHatman *me ); + bool IsPotentiallyChaseable( CHeadlessHatman *me, CTFPlayer *victim ); + + CHandle< CBaseCombatCharacter > m_attackTarget; // the victim I'm momentarily attacking + CountdownTimer m_attackTargetFocusTimer; + void SelectVictim( CHeadlessHatman *me ); +}; + + + +#endif // HEADLESS_HATMAN_ATTACK_H diff --git a/game/server/tf/halloween/halloween_behavior/headless_hatman_dying.cpp b/game/server/tf/halloween/halloween_behavior/headless_hatman_dying.cpp new file mode 100644 index 0000000..5a4a0ea --- /dev/null +++ b/game/server/tf/halloween/halloween_behavior/headless_hatman_dying.cpp @@ -0,0 +1,44 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// headless_hatman_dying.cpp +// The HHH in the process of dying +// Michael Booth, October 2010 + +#include "cbase.h" + +#include "particle_parse.h" + +#include "../headless_hatman.h" +#include "headless_hatman_dying.h" + + +//--------------------------------------------------------------------------------------------- +ActionResult< CHeadlessHatman > CHeadlessHatmanDying::OnStart( CHeadlessHatman *me, Action< CHeadlessHatman > *priorAction ) +{ + me->GetBodyInterface()->StartActivity( ACT_DIESIMPLE ); + me->EmitSound( "Halloween.HeadlessBossDying" ); + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CHeadlessHatman > CHeadlessHatmanDying::Update( CHeadlessHatman *me, float interval ) +{ + if ( me->IsActivityFinished() ) + { + me->Break(); + DispatchParticleEffect( "halloween_boss_death", me->GetAbsOrigin(), me->GetAbsAngles() ); + + UTIL_Remove( me ); + + IGameEvent *event = gameeventmanager->CreateEvent( "pumpkin_lord_killed" ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } + + return Done(); + } + + return Continue(); +} + diff --git a/game/server/tf/halloween/halloween_behavior/headless_hatman_dying.h b/game/server/tf/halloween/halloween_behavior/headless_hatman_dying.h new file mode 100644 index 0000000..ea4b32e --- /dev/null +++ b/game/server/tf/halloween/halloween_behavior/headless_hatman_dying.h @@ -0,0 +1,20 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// headless_hatman_dying.h +// The HHH in the process of dying +// Michael Booth, October 2010 + +#ifndef HEADLESS_HATMAN_DYING_H +#define HEADLESS_HATMAN_DYING_H + + +//--------------------------------------------------------------------------------------------- +class CHeadlessHatmanDying : public Action< CHeadlessHatman > +{ +public: + virtual ActionResult< CHeadlessHatman > OnStart( CHeadlessHatman *me, Action< CHeadlessHatman > *priorAction ); + virtual ActionResult< CHeadlessHatman > Update( CHeadlessHatman *me, float interval ); + virtual const char *GetName( void ) const { return "Dying"; } // return name of this action +}; + + +#endif // HEADLESS_HATMAN_DYING_H diff --git a/game/server/tf/halloween/halloween_behavior/headless_hatman_emerge.cpp b/game/server/tf/halloween/halloween_behavior/headless_hatman_emerge.cpp new file mode 100644 index 0000000..a45b679 --- /dev/null +++ b/game/server/tf/halloween/halloween_behavior/headless_hatman_emerge.cpp @@ -0,0 +1,133 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// headless_hatman_emerge.cpp +// The Halloween Boss emerging from the ground +// Michael Booth, October 2010 + +#include "cbase.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "nav_mesh/tf_nav_area.h" +#include "particle_parse.h" + +#include "../headless_hatman.h" +#include "headless_hatman_emerge.h" +#include "headless_hatman_attack.h" +#include "crybaby_boss_escape.h" + + +//--------------------------------------------------------------------------------------------- +ActionResult< CHeadlessHatman > CHeadlessHatmanEmerge::OnStart( CHeadlessHatman *me, Action< CHeadlessHatman > *priorAction ) +{ + me->GetBodyInterface()->StartActivity( ACT_TRANSITION ); + + DispatchParticleEffect( "halloween_boss_summon", me->GetAbsOrigin(), me->GetAbsAngles() ); + + m_riseTimer.Start( 3.0f ); + m_emergePos = me->GetAbsOrigin() + Vector( 0, 0, 10.0f ); + + m_height = 200.0f; + me->SetAbsOrigin( m_emergePos + Vector( 0, 0, -m_height ) ); + me->EmitSound( "Halloween.HeadlessBossSpawnRumble" ); + + // face towards a nearby player + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + float closeRangeSq = FLT_MAX; + CTFPlayer *close = NULL; + + for( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *player = playerVector[i]; + + float rangeSq = me->GetRangeSquaredTo( player ); + if ( rangeSq < closeRangeSq ) + { + closeRangeSq = rangeSq; + close = player; + } + } + + QAngle facingAngle; + + if ( close ) + { + Vector toPlayer = close->GetAbsOrigin() - me->GetAbsOrigin(); + toPlayer.z = 0.0f; + toPlayer.NormalizeInPlace(); + + VectorAngles( toPlayer, Vector(0,0,1), facingAngle ); + } + else + { + facingAngle.x = 0.0f; + facingAngle.y = RandomFloat( 0.0f, 360.0f ); + facingAngle.z = 0.0f; + } + + me->SetAbsAngles( facingAngle ); + + + IGameEvent *event = gameeventmanager->CreateEvent( "pumpkin_lord_summoned" ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } + + return Continue(); +} + + +//---------------------------------------------------------------------------------- +ActionResult< CHeadlessHatman > CHeadlessHatmanEmerge::Update( CHeadlessHatman *me, float interval ) +{ + if ( !m_riseTimer.IsElapsed() ) + { + me->SetAbsOrigin( m_emergePos + Vector( 0, 0, -m_height * m_riseTimer.GetRemainingTime() / m_riseTimer.GetCountdownDuration() ) ); + + if ( m_rumbleTimer.IsElapsed() ) + { + m_rumbleTimer.Start( 0.25f ); + + // shake nearby players' screens. + UTIL_ScreenShake( me->GetAbsOrigin(), 15.0f, 5.0f, 1.0f, 1000.0f, SHAKE_START ); + } + } + + if ( me->IsActivityFinished() ) + { + return ChangeTo( new CHeadlessHatmanAttack, "Here I come!" ); + } + + // push players away to avoid penetration issues + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + const float pushRange = 250.0f; + const float pushForce = 200.0f; + + for( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *player = playerVector[i]; + + Vector toPlayer = player->EyePosition() - m_emergePos; + float range = toPlayer.NormalizeInPlace(); + + if ( range < pushRange ) + { + // make sure we push players up and away + toPlayer.z = 0.0f; + toPlayer.NormalizeInPlace(); + toPlayer.z = 1.0f; + + Vector push = pushForce * toPlayer; + + player->ApplyAbsVelocityImpulse( push ); + } + } + + return Continue(); +} diff --git a/game/server/tf/halloween/halloween_behavior/headless_hatman_emerge.h b/game/server/tf/halloween/halloween_behavior/headless_hatman_emerge.h new file mode 100644 index 0000000..e4bda2a --- /dev/null +++ b/game/server/tf/halloween/halloween_behavior/headless_hatman_emerge.h @@ -0,0 +1,26 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// headless_hatman_emerge.h +// The Halloween Boss emerging from the ground +// Michael Booth, October 2010 + +#ifndef HEADLESS_HATMAN_EMERGE_H +#define HEADLESS_HATMAN_EMERGE_H + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CHeadlessHatmanEmerge : public Action< CHeadlessHatman > +{ +public: + virtual ActionResult< CHeadlessHatman > OnStart( CHeadlessHatman *me, Action< CHeadlessHatman > *priorAction ); + virtual ActionResult< CHeadlessHatman > Update( CHeadlessHatman *me, float interval ); + virtual const char *GetName( void ) const { return "Emerge"; } // return name of this action + +private: + CountdownTimer m_riseTimer; + CountdownTimer m_rumbleTimer; + Vector m_emergePos; + float m_height; +}; + + +#endif // HEADLESS_HATMAN_EMERGE_H diff --git a/game/server/tf/halloween/halloween_behavior/headless_hatman_terrify.cpp b/game/server/tf/halloween/halloween_behavior/headless_hatman_terrify.cpp new file mode 100644 index 0000000..eb855ad --- /dev/null +++ b/game/server/tf/halloween/halloween_behavior/headless_hatman_terrify.cpp @@ -0,0 +1,95 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// headless_hatman_terrify.cpp +// The Halloween Boss leans over and yells "Boo!", terrifying nearby victims +// Michael Booth, October 2010 + +#include "cbase.h" + +#include "tf_player.h" +#include "tf_team.h" + +#include "../headless_hatman.h" +#include "headless_hatman_terrify.h" + + +//--------------------------------------------------------------------------------------------- +ActionResult< CHeadlessHatman > CHeadlessHatmanTerrify::OnStart( CHeadlessHatman *me, Action< CHeadlessHatman > *priorAction ) +{ + me->AddGesture( ACT_MP_GESTURE_VC_HANDMOUTH_ITEM1 ); + + m_booTimer.Start( 0.25f ); + m_scareTimer.Start( 0.75f ); + m_timer.Start( 1.25f ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CHeadlessHatman > CHeadlessHatmanTerrify::Update( CHeadlessHatman *me, float interval ) +{ + if ( m_timer.IsElapsed() ) + { + return Done(); + } + + if ( m_booTimer.HasStarted() && m_booTimer.IsElapsed() ) + { + m_booTimer.Invalidate(); + me->EmitSound( "Halloween.HeadlessBossBoo" ); + } + + if ( m_scareTimer.IsElapsed() ) + { + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + for( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *victim = playerVector[i]; + + if ( me->IsRangeLessThan( victim, tf_halloween_bot_terrify_radius.GetFloat() ) ) + { + if ( !IsWearingPumpkinHeadOrSaxtonMask( victim ) && me->IsLineOfSightClear( victim ) ) + { + // scare them! + const float scareTime = 2.0f; + const float speedReduction = 0.0f; + + // "stun by trigger" results in the Halloween "yikes" effects + int stunFlags = TF_STUN_LOSER_STATE | TF_STUN_BY_TRIGGER; + victim->m_Shared.StunPlayer( scareTime, speedReduction, stunFlags, NULL ); + } + } + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +bool CHeadlessHatmanTerrify::IsWearingPumpkinHeadOrSaxtonMask( CTFPlayer *player ) +{ + const int pumpkinHeadHat = 278; + const int saxtonMask = 277; + + for( int i=0; i<player->GetNumWearables(); ++i ) + { + CEconWearable *wearable = player->GetWearable( i ); + if ( wearable && wearable->GetAttributeContainer() ) + { + CEconItemView *item = wearable->GetAttributeContainer()->GetItem(); + if ( item && item->IsValid() ) + { + if ( item->GetItemDefIndex() == pumpkinHeadHat || item->GetItemDefIndex() == saxtonMask ) + { + return true; + } + } + } + } + + return false; +} diff --git a/game/server/tf/halloween/halloween_behavior/headless_hatman_terrify.h b/game/server/tf/halloween/halloween_behavior/headless_hatman_terrify.h new file mode 100644 index 0000000..a105fb5 --- /dev/null +++ b/game/server/tf/halloween/halloween_behavior/headless_hatman_terrify.h @@ -0,0 +1,26 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// headless_hatman_terrify.h +// The Halloween Boss leans over and yells "Boo!", terrifying nearby victims +// Michael Booth, October 2010 + +#ifndef HEADLESS_HATMAN_TERRIFY_H +#define HEADLESS_HATMAN_TERRIFY_H + +//--------------------------------------------------------------------------------------------- +class CHeadlessHatmanTerrify : public Action< CHeadlessHatman > +{ +public: + virtual ActionResult< CHeadlessHatman > OnStart( CHeadlessHatman *me, Action< CHeadlessHatman > *priorAction ); + virtual ActionResult< CHeadlessHatman > Update( CHeadlessHatman *me, float interval ); + virtual const char *GetName( void ) const { return "Terrify"; } // return name of this action + +private: + CountdownTimer m_booTimer; + CountdownTimer m_scareTimer; + CountdownTimer m_timer; + + bool IsWearingPumpkinHeadOrSaxtonMask( CTFPlayer *player ); +}; + + +#endif // HEADLESS_HATMAN_TERRIFY_H diff --git a/game/server/tf/halloween/halloween_gift_spawn_locations.cpp b/game/server/tf/halloween/halloween_gift_spawn_locations.cpp new file mode 100644 index 0000000..0cbd4d2 --- /dev/null +++ b/game/server/tf/halloween/halloween_gift_spawn_locations.cpp @@ -0,0 +1,292 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// memdbgon must be the last include file in a .cpp file!!! +#include "cbase.h" +#include "utlvector.h" +#include "util.h" +#include "halloween/halloween_gift_spawn_locations.h" +#include "tier0/memdbgon.h" +// ------------------------------------------------------------------------- + +struct valid_item_pos +{ + float x, y, z; +}; + +static valid_item_pos kValidPositions_MannManor[] = { + { -725.000000, 1362.500000, -639.968750 }, + { -3137.500000, 1500.000000, -1023.968750 }, + { -1212.500000, 1187.500000, -959.968750 }, + { -3150.000000, 1375.000000, -791.968750 }, + { -2237.500000, 2150.000000, -1015.968750 }, + { -1837.500000, 2250.000000, -895.968750 }, + { -1162.500000, 1837.500000, -959.968750 }, + { -3662.500000, 1212.500000, -1024.431152 }, + { -912.500000, 3937.500000, -1039.968750 }, + { -175.000000, 1837.500000, -1007.968750 }, + { -1575.000000, 1762.500000, -959.968750 }, + { -1625.000000, 1387.500000, -959.968750 }, + { -850.000000, 1025.000000, -639.468750 }, + { -2625.000000, 3750.000000, -1086.180908 }, + { -450.000000, 1850.000000, -1006.910278 }, + { -2775.000000, 1425.000000, -1023.968750 }, + { -2037.500000, 1625.000000, -895.968750 }, + { -637.500000, 2475.000000, -767.968750 }, + { -3637.500000, 2862.500000, -903.968750 }, + { -2812.500000, 1325.000000, -791.968750 }, + { -1212.500000, 2162.500000, -959.968750 }, + { -1525.000000, 2662.500000, -895.968750 }, + { -2837.500000, 2537.500000, -1085.712402 }, + { -1312.500000, 3787.500000, -1079.968750 }, + { -2450.000000, 1637.500000, -895.968750 }, + { -625.000000, 1037.500000, -639.968750 }, + { -3625.000000, 2587.500000, -903.968750 }, + { -2800.000000, 3362.500000, -1087.954102 }, + { -2250.000000, 1625.000000, -895.968750 }, + { -1775.000000, 2825.000000, -783.968750 }, + { -3712.500000, 2100.000000, -671.968750 }, + { -3500.000000, 2100.000000, -863.968750 }, + { -1400.000000, 3325.000000, -1088.116577 }, + { -800.000000, 2825.000000, -767.968750 }, + { -1787.500000, 2575.000000, -767.968750 }, + { -2962.500000, 3187.500000, -668.908264 }, + { -1962.500000, 1900.000000, -895.968750 }, + { -712.500000, 2262.500000, -767.968750 }, + { -587.500000, 3887.500000, -1099.458862 }, + { -3737.500000, 2112.500000, -863.968750 }, + { -3212.500000, 1237.500000, -1023.968750 }, + { -1325.000000, 3987.500000, -1079.968750 }, + { -737.500000, 1887.500000, -1007.968750 }, + { -2837.500000, 2137.500000, -1083.385742 }, + { -3112.500000, 1137.500000, -791.968750 }, + { -2275.000000, 3187.500000, -767.968750 }, + { -1475.000000, 2962.500000, -847.968750 }, + { -3425.000000, 2137.500000, -671.968750 }, + { -3450.000000, 2912.500000, -903.968750 }, + { -1725.000000, 3737.500000, -1050.131104 }, + { -850.000000, 3237.500000, -1031.956299 }, + { -2650.000000, 2512.500000, -1085.018555 }, + { -2487.500000, 2037.500000, -1050.676147 }, + { -1600.000000, 1175.000000, -959.968750 }, + { -650.000000, 1937.500000, -743.482178 }, + { -650.000000, 1775.000000, -692.995728 }, + { -1012.500000, 1500.000000, -639.968750 }, + { -3700.000000, 1275.000000, -811.867065 }, + { -3112.500000, 1600.000000, -791.968750 }, + { -2950.000000, 1850.000000, -1053.853516 }, + { -1225.000000, 3475.000000, -1086.870605 }, + { -1150.000000, 3300.000000, -1070.466187 }, + { -925.000000, 3425.000000, -1050.419189 }, + { -650.000000, 3300.000000, -1043.805176 }, + { -625.000000, 2800.000000, -767.968750 }, + { -2775.000000, 1650.000000, -1023.968750 }, + { -3575.000000, 1100.000000, -808.125854 }, + { -1212.500000, 1425.000000, -959.968750 }, + { -1737.500000, 3000.000000, -783.968750 }, + { -1462.500000, 2437.500000, -895.968750 }, + { -862.500000, 2200.000000, -767.968750 }, + { -2962.500000, 3550.000000, -671.968750 }, + { -3462.500000, 2487.500000, -903.968750 }, + { -1012.500000, 3150.000000, -1048.620605 }, + { -562.500000, 3725.000000, -1087.434692 }, + { -2062.500000, 3250.000000, -984.070923 }, + { -2675.000000, 2112.500000, -1082.104248 }, + { -2487.500000, 2400.000000, -1051.597046 }, + { -2037.500000, 3137.500000, -838.662476 }, + { -3062.500000, 1262.500000, -1023.968750 }, + { -2887.500000, 2812.500000, -694.111816 }, + { -2987.500000, 3250.000000, -1086.727539 }, + { -2812.500000, 3762.500000, -1086.203857 }, + { -2350.000000, 3537.500000, -1082.763184 }, + { -2162.500000, 3837.500000, -967.968750 }, + { -1562.500000, 3312.500000, -1079.254028 }, + { -1375.000000, 3500.000000, -1085.550903 }, + { -887.500000, 3700.000000, -1059.968750 }, + { -2812.500000, 2287.500000, -1081.588135 }, + { -2687.500000, 2362.500000, -1081.903198 }, + { -2337.500000, 2412.500000, -1023.968750 }, + { -1950.000000, 1437.500000, -895.968750 }, + { -2850.000000, 1112.500000, -872.148926 }, + { -1350.000000, 1862.500000, -959.968750 }, + { -1600.000000, 2425.000000, -830.815613 }, + { -3487.500000, 1262.500000, -809.627197 }, + { -3475.000000, 1237.500000, -1023.968750 }, + { -2875.000000, 2787.500000, -1085.673584 }, + { -3012.500000, 2062.500000, -1073.808105 }, + { -2550.000000, 3962.500000, -1087.083984 }, + { -1512.500000, 3812.500000, -1079.644287 }, + { -1100.000000, 3437.500000, -1073.367554 }, + { -750.000000, 3562.500000, -1059.989502 }, + { -850.000000, 2987.500000, -767.968750 }, + { -575.000000, 2287.500000, -767.968750 }, + { -1175.000000, 2337.500000, -959.968750 }, + { -1850.000000, 3175.000000, -916.482300 }, + { -2675.000000, 1125.000000, -954.482300 }, + { -1562.500000, 1575.000000, -959.968750 }, + { -600.000000, 525.000000, -759.968750 }, + { -2962.500000, 2975.000000, -671.584595 }, + { -3687.500000, 2275.000000, -751.524414 }, + { -3437.500000, 2250.000000, -863.968750 }, + { -2750.000000, 1500.000000, -791.968750 }, + { -2862.500000, 3025.000000, -1088.080444 }, + { -2650.000000, 3400.000000, -1086.712158 }, + { -2037.500000, 3850.000000, -967.968750 }, + { -2050.000000, 3537.500000, -1039.457031 }, + { -1262.500000, 3625.000000, -1079.968750 }, + { -1100.000000, 3725.000000, -1079.968750 }, + { -1137.500000, 2975.000000, -1064.579102 }, + { -1100.000000, 2800.000000, -1035.468262 }, + { -1000.000000, 3300.000000, -1043.383545 }, + { -900.000000, 3550.000000, -1069.937378 }, + { -675.000000, 3437.500000, -1038.987061 }, + { -625.000000, 3162.500000, -1048.860962 }, + { -2825.000000, 1975.000000, -1071.794189 }, + { -2875.000000, 1250.000000, -1023.968750 }, + { -2625.000000, 1950.000000, -1080.477051 }, + { -2512.500000, 2600.000000, -1060.309570 }, + { -2675.000000, 1250.000000, -990.329102 }, + { -1025.000000, 1262.500000, -618.168762 }, + { -2850.000000, 3175.000000, -1091.269287 }, + { -2275.000000, 3862.500000, -993.140991 }, + { -1987.500000, 3650.000000, -1038.143188 }, + { -1937.500000, 3475.000000, -1009.303223 }, + { -1625.000000, 3925.000000, -1057.784912 }, + { -575.000000, 3562.500000, -1064.084229 }, + { -2450.000000, 3400.000000, -1092.235474 }, + { -1675.000000, 3537.500000, -1058.036133 }, + { -1187.500000, 2562.500000, -990.317749 }, + { -1612.500000, 2975.000000, -815.968750 }, + { -1175.000000, 4000.000000, -1059.968750 }, + { -2200.000000, 2425.000000, -1023.968750 }, + { -2162.500000, 3425.000000, -1036.134521 }, + { -1450.000000, 2187.500000, -767.968750 }, + { -1100.000000, 2187.500000, -767.968750 }, +}; + +const float g_flPlayerEyeHeight = 70.0f; + +static valid_item_pos kValidPositions_Viaduct[] = +{ + { -1056.508423, 120.524803, 307.972595f - g_flPlayerEyeHeight }, + { -1245.718750, -371.585114, 316.206665f - g_flPlayerEyeHeight }, + { -1755.105469, -602.121094, 211.415222f - g_flPlayerEyeHeight }, + { -1866.556030, -11.241785, 311.038757f - g_flPlayerEyeHeight }, + { -1801.210083, 567.631775, 221.211823f - g_flPlayerEyeHeight }, + { -2254.645996, -501.424133, 323.972595f - g_flPlayerEyeHeight }, + { -2210.340576, 376.084320, 323.972595f - g_flPlayerEyeHeight }, + { -1888.754272, 639.861633, 204.341263f - g_flPlayerEyeHeight }, + { -1903.123413, -641.635986, 203.944336f - g_flPlayerEyeHeight }, + { -1212.720703, 990.511353, 227.972595f - g_flPlayerEyeHeight }, + { -1225.367554, -966.919922, 227.972595f - g_flPlayerEyeHeight }, + { -2486.856934, -1201.215576, 194.879059f - g_flPlayerEyeHeight }, + { -2568.440674, 1470.992432, 195.961288f - g_flPlayerEyeHeight }, + { -1681.012573, -1190.597534, 131.972595f - g_flPlayerEyeHeight }, + { -1668.416504, 1226.122070, 131.972595f - g_flPlayerEyeHeight }, + { -2459.953125, 394.793518, 323.972595f - g_flPlayerEyeHeight }, + { -2495.843994, -505.581940, 323.972595f - g_flPlayerEyeHeight }, + { -1507.193359, -632.548096, 207.798416f - g_flPlayerEyeHeight }, + { -1515.489746, 637.124756, 207.442322f - g_flPlayerEyeHeight }, + { -1533.132202, 6.273134, 299.123474f - g_flPlayerEyeHeight }, + { -1533.132202, 6.273134, 299.123474f - g_flPlayerEyeHeight }, + { -1533.132202, 6.273134, 299.123474f - g_flPlayerEyeHeight }, + { -1533.132202, 6.273134, 299.123474f - g_flPlayerEyeHeight }, + { -1533.132202, 6.273134, 299.123474f - g_flPlayerEyeHeight }, + { -1533.132202, 6.273134, 299.123474f - g_flPlayerEyeHeight }, + { -1533.132202, 6.273134, 299.123474f - g_flPlayerEyeHeight }, + { -1767.763306, -31.788916, 304.332611f - g_flPlayerEyeHeight }, + { -1314.115601, 1.664654, 304.332611f - g_flPlayerEyeHeight }, +}; + +static valid_item_pos kValidPositions_Lakeside[] = +{ + { 440.514740, 19.020391, 52.031319f - g_flPlayerEyeHeight }, + { -439.044678, -0.653890, 52.031319f - g_flPlayerEyeHeight }, + { 14.226428, -860.007385, 116.031319f - g_flPlayerEyeHeight }, + { 536.307800, -35.320126, -110.660393f - g_flPlayerEyeHeight }, + { -522.902588, -15.580125, -107.159370f - g_flPlayerEyeHeight }, + { -13.246658, 556.848022, 84.031319f - g_flPlayerEyeHeight }, + { 199.859665, -560.373779, 52.031319f - g_flPlayerEyeHeight }, + { -206.504196, -524.416443, 52.031319f - g_flPlayerEyeHeight }, + + { -572.280029, -854.871521, -102.842537f - g_flPlayerEyeHeight }, + { 570.159973, -833.295898, -104.718376f - g_flPlayerEyeHeight }, + { -10.444493, -1814.084106, 91.031319f - g_flPlayerEyeHeight }, + { -1117.456177, -1821.980591, 81.080040f - g_flPlayerEyeHeight }, + { 1122.193481, -1760.623291, 84.117996f - g_flPlayerEyeHeight }, + { 934.447083, -14.523724, -153.873962f - g_flPlayerEyeHeight }, + { -935.006165, 0.899750, -154.107819f - g_flPlayerEyeHeight }, + { -831.039368, 1028.812988, -132.968689f - g_flPlayerEyeHeight }, + { 838.370667, 1039.730347, -132.968689f - g_flPlayerEyeHeight }, + { 330.726898, -1061.698975, 91.031319f - g_flPlayerEyeHeight }, + { -347.198090, -1074.667480, 91.031319f - g_flPlayerEyeHeight }, + { -970.985229, -1151.937866, -129.290192f - g_flPlayerEyeHeight }, + { 964.682556, -1112.505127, -132.963379f - g_flPlayerEyeHeight }, + { -310.233612, -548.355957, -68.615242f - g_flPlayerEyeHeight }, + { 310.885254, -545.554932, -68.772285f - g_flPlayerEyeHeight }, +}; + +static valid_item_pos kValidPositions_Hightower[] = +{ + { 6126.312500, 7590.515137, 435.031311f - g_flPlayerEyeHeight }, + { 7961.887207, 7579.561523, -35.332085f - g_flPlayerEyeHeight }, + { 6192.0f, 7592.0f, 373.0f, }, + { 7634.951660, 7592.539063, 130.810715f - g_flPlayerEyeHeight }, + { 6472.569336, 7593.163086, -149.912628f - g_flPlayerEyeHeight }, // Cayle says to use this one three times + { 6472.569336, 7593.163086, -149.912628f - g_flPlayerEyeHeight }, + { 6472.569336, 7593.163086, -149.912628f - g_flPlayerEyeHeight }, + { 6333.593750, 7398.845703, 425.031311f - g_flPlayerEyeHeight }, + { 6338.365723, 7780.267090, 425.031311f - g_flPlayerEyeHeight }, + { 6111.854004, 7582.590820, 425.031311f - g_flPlayerEyeHeight }, + { 8269.034180, 8336.364258, -193.885345f - g_flPlayerEyeHeight }, + { 6152.998047, 6708.033203, -190.968689f - g_flPlayerEyeHeight }, + { 7947.068359, 7592.274902, -45.030327f - g_flPlayerEyeHeight }, + { 8690.697266, 7519.280273, -134.968689f - g_flPlayerEyeHeight }, + { 5184.807617, 7460.308594, 65.031311f - g_flPlayerEyeHeight }, + { 5176.026367, 7762.865234, 65.031311f - g_flPlayerEyeHeight }, + { 5665.124512, 7912.830078, 186.027237f - g_flPlayerEyeHeight }, + { 5629.083984, 7272.486816, 186.031311f - g_flPlayerEyeHeight }, + { 7569.272461, 7356.835938, 30.543331f - g_flPlayerEyeHeight }, + { 7620.356445, 7806.569824, 43.632362f - g_flPlayerEyeHeight }, + { 10225.334961, 7462.086914, -366.968689f - g_flPlayerEyeHeight }, + { 10208.725586, 7768.193848, -366.968689f - g_flPlayerEyeHeight }, +}; + +struct halloween_map_info +{ + const char *m_pszMapName; + valid_item_pos *m_pPositions; + int m_pPositionCount; +}; + +static halloween_map_info g_HalloweenMapInfo[] = +{ + { "cp_manor_event", kValidPositions_MannManor, ARRAYSIZE( kValidPositions_MannManor ) }, + { "koth_viaduct_event", kValidPositions_Viaduct, ARRAYSIZE( kValidPositions_Viaduct ) }, + { "koth_lakeside_event", kValidPositions_Lakeside, ARRAYSIZE( kValidPositions_Lakeside ) }, + { "plr_hightower_event", kValidPositions_Hightower, ARRAYSIZE( kValidPositions_Hightower ) }, +}; + +// +void AddHalloweenGiftPositionsForMap( const char *pszMapName, CUtlVector<Vector> &vLocations ) +{ + for ( int i = 0; i < ARRAYSIZE( g_HalloweenMapInfo ); i++ ) + { + if ( FStrEq( pszMapName, g_HalloweenMapInfo[i].m_pszMapName ) ) + { + for ( int iPoint = 0; iPoint < g_HalloweenMapInfo[i].m_pPositionCount; iPoint++ ) + { + vLocations.AddToTail( Vector( + g_HalloweenMapInfo[ i ].m_pPositions[ iPoint ].x, + g_HalloweenMapInfo[ i ].m_pPositions[ iPoint ].y, + g_HalloweenMapInfo[ i ].m_pPositions[ iPoint ].z + ) ); + } + return; + } + } +} diff --git a/game/server/tf/halloween/halloween_gift_spawn_locations.h b/game/server/tf/halloween/halloween_gift_spawn_locations.h new file mode 100644 index 0000000..fd78929 --- /dev/null +++ b/game/server/tf/halloween/halloween_gift_spawn_locations.h @@ -0,0 +1,16 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// memdbgon must be the last include file in a .cpp file!!! +#include "cbase.h" +#include "utlvector.h" +#include "util.h" +#include "tier0/memdbgon.h" + +// +void AddHalloweenGiftPositionsForMap( const char *pszMapName, CUtlVector<Vector> &vLocations ); +// diff --git a/game/server/tf/halloween/headless_hatman.cpp b/game/server/tf/halloween/headless_hatman.cpp new file mode 100644 index 0000000..d4aef78 --- /dev/null +++ b/game/server/tf/halloween/headless_hatman.cpp @@ -0,0 +1,330 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// headless_hatman.cpp +// An NPC that spawns in the Halloween map and wreaks havok +// Michael Booth, October 2010 + +#include "cbase.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "nav_mesh/tf_nav_area.h" +#include "headless_hatman.h" +#include "NextBot/Path/NextBotChasePath.h" +#include "econ_wearable.h" +#include "team_control_point_master.h" +#include "particle_parse.h" +#include "ghost/ghost.h" + +#include "halloween_behavior/headless_hatman_emerge.h" +#include "halloween_behavior/headless_hatman_dying.h" + + +ConVar tf_halloween_bot_health_base( "tf_halloween_bot_health_base", "3000", FCVAR_CHEAT ); +ConVar tf_halloween_bot_health_per_player( "tf_halloween_bot_health_per_player", "200", FCVAR_CHEAT ); +ConVar tf_halloween_bot_min_player_count( "tf_halloween_bot_min_player_count", "10", FCVAR_CHEAT ); + +ConVar tf_halloween_bot_speed( "tf_halloween_bot_speed", "400", FCVAR_CHEAT ); +ConVar tf_halloween_bot_attack_range( "tf_halloween_bot_attack_range", "200", FCVAR_CHEAT ); +ConVar tf_halloween_bot_speed_recovery_rate( "tf_halloween_bot_speed_recovery_rate", "100", FCVAR_CHEAT, "Movement units/second" ); +ConVar tf_halloween_bot_chase_duration( "tf_halloween_bot_chase_duration", "30", FCVAR_CHEAT ); +ConVar tf_halloween_bot_terrify_radius( "tf_halloween_bot_terrify_radius", "500", FCVAR_CHEAT ); +ConVar tf_halloween_bot_chase_range( "tf_halloween_bot_chase_range", "1500", FCVAR_CHEAT ); +ConVar tf_halloween_bot_quit_range( "tf_halloween_bot_quit_range", "2000", FCVAR_CHEAT ); + + +//----------------------------------------------------------------------------------------------------- +// The Horseless Headless Horseman +//----------------------------------------------------------------------------------------------------- +LINK_ENTITY_TO_CLASS( headless_hatman, CHeadlessHatman ); + +IMPLEMENT_SERVERCLASS_ST( CHeadlessHatman, DT_HeadlessHatman ) +END_SEND_TABLE() + + +//----------------------------------------------------------------------------------------------------- +CHeadlessHatman::CHeadlessHatman() +{ + m_intention = new CHeadlessHatmanIntention( this ); + m_locomotor = new CHeadlessHatmanLocomotion( this ); + m_body = new CHeadlessHatmanBody( this ); +} + + +//----------------------------------------------------------------------------------------------------- +CHeadlessHatman::~CHeadlessHatman() +{ + if ( m_intention ) + delete m_intention; + + if ( m_locomotor ) + delete m_locomotor; + + if ( m_body ) + delete m_body; +} + + +void CHeadlessHatman::PrecacheHeadlessHatman() +{ + int model = PrecacheModel( "models/bots/headless_hatman.mdl" ); + PrecacheGibsForModel( model ); + + if ( TFGameRules() && TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_DOOMSDAY ) ) + { + PrecacheModel( "models/weapons/c_models/c_big_mallet/c_big_mallet.mdl" ); + PrecacheParticleSystem( "hammer_impact_button" ); + PrecacheScriptSound( "Halloween.HammerImpact" ); + } + else + { + PrecacheModel( "models/weapons/c_models/c_bigaxe/c_bigaxe.mdl" ); + } + + PrecacheScriptSound( "Halloween.HeadlessBossSpawn" ); + PrecacheScriptSound( "Halloween.HeadlessBossSpawnRumble" ); + PrecacheScriptSound( "Halloween.HeadlessBossAttack" ); + PrecacheScriptSound( "Halloween.HeadlessBossAlert" ); + PrecacheScriptSound( "Halloween.HeadlessBossBoo" ); + PrecacheScriptSound( "Halloween.HeadlessBossPain" ); + PrecacheScriptSound( "Halloween.HeadlessBossLaugh" ); + PrecacheScriptSound( "Halloween.HeadlessBossDying" ); + PrecacheScriptSound( "Halloween.HeadlessBossDeath" ); + PrecacheScriptSound( "Halloween.HeadlessBossAxeHitFlesh" ); + PrecacheScriptSound( "Halloween.HeadlessBossAxeHitWorld" ); + PrecacheScriptSound( "Halloween.HeadlessBossFootfalls" ); + PrecacheScriptSound( "Player.IsNowIt" ); + PrecacheScriptSound( "Player.YouAreIt" ); + PrecacheScriptSound( "Player.TaggedOtherIt" ); + + PrecacheParticleSystem( "halloween_boss_summon" ); + PrecacheParticleSystem( "halloween_boss_axe_hit_world" ); + PrecacheParticleSystem( "halloween_boss_injured" ); + PrecacheParticleSystem( "halloween_boss_death" ); + PrecacheParticleSystem( "halloween_boss_foot_impact" ); + PrecacheParticleSystem( "halloween_boss_eye_glow" ); +} + +//----------------------------------------------------------------------------------------------------- +void CHeadlessHatman::Precache() +{ + BaseClass::Precache(); + + // always allow late precaching, so we don't pay the cost of the + // Halloween Boss for the entire year + + bool bAllowPrecache = CBaseEntity::IsPrecacheAllowed(); + CBaseEntity::SetAllowPrecache( true ); + + PrecacheHeadlessHatman(); + + CBaseEntity::SetAllowPrecache( bAllowPrecache ); +} + + +//----------------------------------------------------------------------------------------------------- +void CHeadlessHatman::Spawn( void ) +{ + Precache(); + + BaseClass::Spawn(); + + SetModel( "models/bots/headless_hatman.mdl" ); + + m_axe = (CBaseAnimating *)CreateEntityByName( "prop_dynamic" ); + if ( m_axe ) + { + m_axe->SetModel( GetWeaponModel() ); + + // bonemerge the axe into our model + m_axe->FollowEntity( this, true ); + } + + // scale the boss' health with the player count + int totalPlayers = GetGlobalTFTeam( TF_TEAM_BLUE )->GetNumPlayers() + GetGlobalTFTeam( TF_TEAM_RED )->GetNumPlayers(); + + int health = tf_halloween_bot_health_base.GetInt(); + if ( totalPlayers > tf_halloween_bot_min_player_count.GetInt() ) + { + health += ( totalPlayers - tf_halloween_bot_min_player_count.GetInt() ) * tf_halloween_bot_health_per_player.GetInt(); + } + + SetHealth( health ); + SetMaxHealth( health ); + + m_homePos = GetAbsOrigin(); + + m_damagePoseParameter = -1; + + SetBloodColor( DONT_BLEED ); +} + + +//----------------------------------------------------------------------------------------------------- +int CHeadlessHatman::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + DispatchParticleEffect( "halloween_boss_injured", info.GetDamagePosition(), GetAbsAngles() ); + + return BaseClass::OnTakeDamage_Alive( info ); +} + + +//--------------------------------------------------------------------------------------------- +void CHeadlessHatman::Update( void ) +{ + BaseClass::Update(); + + if ( m_damagePoseParameter < 0 ) + { + m_damagePoseParameter = LookupPoseParameter( "damage" ); + } + + if ( m_damagePoseParameter >= 0 ) + { + SetPoseParameter( m_damagePoseParameter, 1.0f - ( (float)GetHealth() / (float)GetMaxHealth() ) ); + } +} + + +//--------------------------------------------------------------------------------------------- +const char *CHeadlessHatman::GetWeaponModel() const +{ + if ( TFGameRules() && TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_DOOMSDAY ) ) + { + return "models/weapons/c_models/c_big_mallet/c_big_mallet.mdl"; + } + else + { + return "models/weapons/c_models/c_bigaxe/c_bigaxe.mdl"; + } +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CHeadlessHatmanBehavior : public Action< CHeadlessHatman > +{ +public: + virtual Action< CHeadlessHatman > *InitialContainedAction( CHeadlessHatman *me ) + { + return new CHeadlessHatmanEmerge; + } + + virtual ActionResult< CHeadlessHatman > Update( CHeadlessHatman *me, float interval ) + { + if ( !me->IsAlive() ) + { + if ( !me->WasSpawnedByCheats() ) + { + // award achievement to everyone who injured me within the last few seconds + const float deathTime = 5.0f; + const CUtlVector< CHeadlessHatman::AttackerInfo > &attackerVector = me->GetAttackerVector(); + for( int i=0; i<attackerVector.Count(); ++i ) + { + if ( attackerVector[i].m_attacker != NULL && + gpGlobals->curtime - attackerVector[i].m_timestamp < deathTime ) + { + CReliableBroadcastRecipientFilter filter; + UTIL_SayText2Filter( filter, attackerVector[i].m_attacker, false, "#TF_Halloween_Boss_Killers", attackerVector[i].m_attacker->GetPlayerName() ); + + if ( TFGameRules() && TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_MANN_MANOR ) ) + { + // killing the boss with a melee weapon is a separate achievement + if ( attackerVector[i].m_wasLastHitFromMeleeWeapon ) + { + attackerVector[i].m_attacker->AwardAchievement( ACHIEVEMENT_TF_HALLOWEEN_BOSS_KILL_MELEE ); + } + + attackerVector[i].m_attacker->AwardAchievement( ACHIEVEMENT_TF_HALLOWEEN_BOSS_KILL ); + } + } + } + } + + // nobody is IT any longer + TFGameRules()->SetIT( NULL ); + + return ChangeTo( new CHeadlessHatmanDying, "I am dead!" ); + } + + return Continue(); + } + + virtual const char *GetName( void ) const { return "Attack"; } // return name of this action +}; + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +CHeadlessHatmanIntention::CHeadlessHatmanIntention( CHeadlessHatman *me ) : IIntention( me ) +{ + m_behavior = new Behavior< CHeadlessHatman >( new CHeadlessHatmanBehavior ); +} + +CHeadlessHatmanIntention::~CHeadlessHatmanIntention() +{ + delete m_behavior; +} + +void CHeadlessHatmanIntention::Reset( void ) +{ + delete m_behavior; + m_behavior = new Behavior< CHeadlessHatman >( new CHeadlessHatmanBehavior ); +} + +void CHeadlessHatmanIntention::Update( void ) +{ + m_behavior->Update( static_cast< CHeadlessHatman * >( GetBot() ), GetUpdateInterval() ); +} + +// is this a place we can be? +QueryResultType CHeadlessHatmanIntention::IsPositionAllowed( const INextBot *meBot, const Vector &pos ) const +{ + return ANSWER_YES; +} + + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +float CHeadlessHatmanLocomotion::GetRunSpeed( void ) const +{ + return tf_halloween_bot_speed.GetFloat(); +} + + +//--------------------------------------------------------------------------------------------- +// if delta Z is greater than this, we have to jump to get up +float CHeadlessHatmanLocomotion::GetStepHeight( void ) const +{ + return 18.0f; +} + + +//--------------------------------------------------------------------------------------------- +// return maximum height of a jump +float CHeadlessHatmanLocomotion::GetMaxJumpHeight( void ) const +{ + return 18.0f; +} + + +//--------------------------------------------------------------------------------------------- +// Return max rate of yaw rotation +float CHeadlessHatmanLocomotion::GetMaxYawRate( void ) const +{ + return 200.0f; +} + + +//--------------------------------------------------------------------------------------------- +// Should we collide with this entity? +bool CHeadlessHatmanLocomotion::ShouldCollideWith( const CBaseEntity *object ) const +{ + // don't collide with player in doomsday event + if ( TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_DOOMSDAY ) && object->IsPlayer() ) + { + return false; + } + + return BaseClass::ShouldCollideWith( object ); +} diff --git a/game/server/tf/halloween/headless_hatman.h b/game/server/tf/halloween/headless_hatman.h new file mode 100644 index 0000000..e97e792 --- /dev/null +++ b/game/server/tf/halloween/headless_hatman.h @@ -0,0 +1,205 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// headless_hatman.h +// An NPC that spawns in the Halloween map and wreaks havok +// Michael Booth, October 2010 + +#ifndef HEADLESS_HATMAN_H +#define HEADLESS_HATMAN_H + +#include "NextBot.h" +#include "NextBotBehavior.h" +#include "NextBotGroundLocomotion.h" +#include "headless_hatman_body.h" +#include "Path/NextBotPathFollow.h" +#include "halloween_base_boss.h" + +extern ConVar tf_halloween_bot_health_base; +extern ConVar tf_halloween_bot_health_per_player; +extern ConVar tf_halloween_bot_min_player_count; + +extern ConVar tf_halloween_bot_speed; +extern ConVar tf_halloween_bot_attack_range; +extern ConVar tf_halloween_bot_speed_recovery_rate; +extern ConVar tf_halloween_bot_speed_penalty; +extern ConVar tf_halloween_bot_chase_duration; +extern ConVar tf_halloween_bot_terrify_radius; +extern ConVar tf_halloween_bot_chase_range; +extern ConVar tf_halloween_bot_quit_range; + +class CTFPlayer; +class CHeadlessHatman; + + +//---------------------------------------------------------------------------- +class CHeadlessHatmanLocomotion : public NextBotGroundLocomotion +{ +public: + CHeadlessHatmanLocomotion( INextBot *bot ) : NextBotGroundLocomotion( bot ) { } + virtual ~CHeadlessHatmanLocomotion() { } + + virtual float GetRunSpeed( void ) const; // get maximum running speed + virtual float GetStepHeight( void ) const; // if delta Z is greater than this, we have to jump to get up + virtual float GetMaxJumpHeight( void ) const; // return maximum height of a jump + + /** + * Should we collide with this entity? + */ + virtual bool ShouldCollideWith( const CBaseEntity *object ) const; + +private: + virtual float GetMaxYawRate( void ) const; // return max rate of yaw rotation +}; + + +//---------------------------------------------------------------------------- +class CHeadlessHatmanIntention : public IIntention +{ +public: + CHeadlessHatmanIntention( CHeadlessHatman *me ); + virtual ~CHeadlessHatmanIntention(); + + virtual void Reset( void ); + virtual void Update( void ); + + virtual QueryResultType IsPositionAllowed( const INextBot *me, const Vector &pos ) const; // is the a place we can be? + + virtual INextBotEventResponder *FirstContainedResponder( void ) const { return m_behavior; } + virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const { return NULL; } + +private: + Behavior< CHeadlessHatman > *m_behavior; +}; + + +//---------------------------------------------------------------------------- +class CHeadlessHatman : public CHalloweenBaseBoss +{ +public: + DECLARE_CLASS( CHeadlessHatman, CHalloweenBaseBoss ); + DECLARE_SERVERCLASS(); + + CHeadlessHatman(); + virtual ~CHeadlessHatman(); + + static void PrecacheHeadlessHatman(); + virtual void Precache(); + virtual void Spawn( void ); + + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + + // INextBot + virtual CHeadlessHatmanIntention *GetIntentionInterface( void ) const { return m_intention; } + virtual CHeadlessHatmanLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; } + virtual CHeadlessHatmanBody *GetBodyInterface( void ) const { return m_body; } + + virtual void Update( void ); + + const Vector &GetHomePosition( void ) const; + + CBaseAnimating *GetAxe( void ) const; + + virtual HalloweenBossType GetBossType() const { return HALLOWEEN_BOSS_HHH; } + +private: + const char *GetWeaponModel() const; + + CHeadlessHatmanIntention *m_intention; + CHeadlessHatmanLocomotion *m_locomotor; + CHeadlessHatmanBody *m_body; + + CBaseAnimating *m_axe; + + CUtlVector< AttackerInfo > m_attackerVector; // list of everyone who injured me, and when + + CountdownTimer m_painTimer; + + Vector m_homePos; + int m_damagePoseParameter; +}; + + +inline CBaseAnimating *CHeadlessHatman::GetAxe( void ) const +{ + return m_axe; +} + + +inline const Vector &CHeadlessHatman::GetHomePosition( void ) const +{ + return m_homePos; +} + + +//-------------------------------------------------------------------------------------------------------------- +class CHeadlessHatmanPathCost : public IPathCost +{ +public: + CHeadlessHatmanPathCost( CHeadlessHatman *me ) + { + m_me = me; + } + + // return the cost (weighted distance between) of moving from "fromArea" to "area", or -1 if the move is not allowed + virtual float operator()( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) const + { + if ( fromArea == NULL ) + { + // first area in path, no cost + return 0.0f; + } + else + { + if ( !m_me->GetLocomotionInterface()->IsAreaTraversable( area ) ) + { + // our locomotor says we can't move here + return -1.0f; + } + + // compute distance traveled along path so far + float dist; + + if ( ladder ) + { + dist = ladder->m_length; + } + else if ( length > 0.0 ) + { + // optimization to avoid recomputing length + dist = length; + } + else + { + dist = ( area->GetCenter() - fromArea->GetCenter() ).Length(); + } + + float cost = dist + fromArea->GetCostSoFar(); + + // check height change + float deltaZ = fromArea->ComputeAdjacentConnectionHeightChange( area ); + if ( deltaZ >= m_me->GetLocomotionInterface()->GetStepHeight() ) + { + if ( deltaZ >= m_me->GetLocomotionInterface()->GetMaxJumpHeight() ) + { + // too high to reach + return -1.0f; + } + + // jumping is slower than flat ground + const float jumpPenalty = 5.0f; + cost += jumpPenalty * dist; + } + else if ( deltaZ < -m_me->GetLocomotionInterface()->GetDeathDropHeight() ) + { + // too far to drop + return -1.0f; + } + + return cost; + } + } + + CHeadlessHatman *m_me; +}; + + +#endif // HEADLESS_HATMAN_H diff --git a/game/server/tf/halloween/headless_hatman_body.cpp b/game/server/tf/halloween/headless_hatman_body.cpp new file mode 100644 index 0000000..a7ac031 --- /dev/null +++ b/game/server/tf/halloween/headless_hatman_body.cpp @@ -0,0 +1,114 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#include "cbase.h" + +#include "NextBot.h" +#include "headless_hatman.h" +#include "headless_hatman_body.h" + + +//------------------------------------------------------------------------------------------- +CHeadlessHatmanBody::CHeadlessHatmanBody( INextBot *bot ) : IBody( bot ) +{ + m_moveXPoseParameter = -1; + m_moveYPoseParameter = -1; + m_currentActivity = -1; +} + + +//------------------------------------------------------------------------------------------- +bool CHeadlessHatmanBody::StartActivity( Activity act, unsigned int flags ) +{ + CBaseCombatCharacter *me = (CBaseCombatCharacter *)GetBot()->GetEntity(); + + int animSequence = ::SelectWeightedSequence( me->GetModelPtr(), act, me->GetSequence() ); + + if ( animSequence ) + { + m_currentActivity = act; + me->SetSequence( animSequence ); + me->SetPlaybackRate( 1.0f ); + me->SetCycle( 0 ); + me->ResetSequenceInfo(); + + return true; + } + + return false; +} + + +//------------------------------------------------------------------------------------------- +void CHeadlessHatmanBody::Update( void ) +{ + CBaseCombatCharacter *me = (CBaseCombatCharacter *)GetBot()->GetEntity(); + + if ( m_moveXPoseParameter < 0 ) + { + m_moveXPoseParameter = me->LookupPoseParameter( "move_x" ); + } + + if ( m_moveYPoseParameter < 0 ) + { + m_moveYPoseParameter = me->LookupPoseParameter( "move_y" ); + } + + + // Update the pose parameters + float speed = GetBot()->GetLocomotionInterface()->GetGroundSpeed(); // me->GetAbsVelocity().Length(); + + if ( speed < 0.01f ) + { + // stopped + if ( m_moveXPoseParameter >= 0 ) + { + me->SetPoseParameter( m_moveXPoseParameter, 0.0f ); + } + + if ( m_moveYPoseParameter >= 0 ) + { + me->SetPoseParameter( m_moveYPoseParameter, 0.0f ); + } + } + else + { + Vector forward, right, up; + me->GetVectors( &forward, &right, &up ); + + const Vector &motionVector = GetBot()->GetLocomotionInterface()->GetGroundMotionVector(); + + // move_x == 1.0 at full forward motion and -1.0 in full reverse + if ( m_moveXPoseParameter >= 0 ) + { + float forwardVel = DotProduct( motionVector, forward ); + + me->SetPoseParameter( m_moveXPoseParameter, forwardVel ); + } + + if ( m_moveYPoseParameter >= 0 ) + { + float sideVel = DotProduct( motionVector, right ); + + me->SetPoseParameter( m_moveYPoseParameter, sideVel ); + } + } + + // adjust animation speed to actual movement speed + if ( me->m_flGroundSpeed > 0.0f ) + { + // Clamp playback rate to avoid datatable warnings. Anything faster would look silly, anyway. + float playbackRate = clamp( speed / me->m_flGroundSpeed, -4.f, 12.f ); + me->SetPlaybackRate( playbackRate ); + } + + // move the animation ahead in time + me->StudioFrameAdvance(); + me->DispatchAnimEvents( me ); +} + + +//--------------------------------------------------------------------------------------------- +// return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface) +unsigned int CHeadlessHatmanBody::GetSolidMask( void ) const +{ + return MASK_NPCSOLID | CONTENTS_PLAYERCLIP; +} diff --git a/game/server/tf/halloween/headless_hatman_body.h b/game/server/tf/halloween/headless_hatman_body.h new file mode 100644 index 0000000..ff37495 --- /dev/null +++ b/game/server/tf/halloween/headless_hatman_body.h @@ -0,0 +1,47 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#ifndef HEADLESS_HATMAN_BODY_H +#define HEADLESS_HATMAN_BODY_H + +#include "animation.h" +#include "NextBotBodyInterface.h" + +class INextBot; + + +//---------------------------------------------------------------------------------------------------------------- +/** + * The interface for control and information about the bot's body state (posture, animation state, etc) + */ +class CHeadlessHatmanBody : public IBody +{ +public: + CHeadlessHatmanBody( INextBot *bot ); + virtual ~CHeadlessHatmanBody() { } + + virtual void Update( void ); + + virtual bool StartActivity( Activity act, unsigned int flags = 0 ); + virtual Activity GetActivity( void ) const; // return currently animating activity + virtual bool IsActivity( Activity act ) const; // return true if currently animating activity matches the given one + + virtual unsigned int GetSolidMask( void ) const; // return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface) + +private: + int m_currentActivity; + int m_moveXPoseParameter; + int m_moveYPoseParameter; +}; + + +inline Activity CHeadlessHatmanBody::GetActivity( void ) const +{ + return (Activity)m_currentActivity; +} + +inline bool CHeadlessHatmanBody::IsActivity( Activity act ) const +{ + return act == m_currentActivity ? true : false; +} + + +#endif // HEADLESS_HATMAN_BODY_H diff --git a/game/server/tf/halloween/merasmus/merasmus.cpp b/game/server/tf/halloween/merasmus/merasmus.cpp new file mode 100644 index 0000000..93f42d2 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus.cpp @@ -0,0 +1,1609 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "nav_mesh/tf_nav_area.h" +#include "NextBot/Path/NextBotChasePath.h" +#include "econ_wearable.h" +#include "team_control_point_master.h" +#include "particle_parse.h" +#include "tf_weaponbase_merasmus_grenade.h" +#include "merasmus_dancer.h" +#include "tf_wheel_of_doom.h" +#include "soundenvelope.h" +#include "util.h" +#include "tf_obj_sentrygun.h" +#include "logicrelay.h" +#include "steamworks_gamestats.h" + +#include "tf/halloween/ghost/ghost.h" + +#include "player_vs_environment/monster_resource.h" + +#include "merasmus_trick_or_treat_prop.h" +#include "merasmus.h" +#include "merasmus_behavior/merasmus_disguise.h" +#include "merasmus_behavior/merasmus_dying.h" +#include "merasmus_behavior/merasmus_reveal.h" +#include "merasmus_behavior/merasmus_teleport.h" + +#include "rtime.h" +#include "gc_clientsystem.h" +#include "tf_gcmessages.h" + +#include "tf_fx.h" + +ConVar tf_merasmus_health_base( "tf_merasmus_health_base", "33750", FCVAR_CHEAT ); +ConVar tf_merasmus_health_per_player( "tf_merasmus_health_per_player", "2500", FCVAR_CHEAT ); +ConVar tf_merasmus_min_player_count( "tf_merasmus_min_player_count", "10", FCVAR_CHEAT ); + +ConVar tf_merasmus_lifetime( "tf_merasmus_lifetime", "120", FCVAR_CHEAT ); + +ConVar tf_merasmus_speed( "tf_merasmus_speed", "600", FCVAR_CHEAT ); +ConVar tf_merasmus_speed_recovery_rate( "tf_merasmus_speed_recovery_rate", "100", FCVAR_CHEAT, "Movement units/second" ); +ConVar tf_merasmus_chase_duration( "tf_merasmus_chase_duration", "7", FCVAR_CHEAT ); +ConVar tf_merasmus_chase_range( "tf_merasmus_chase_range", "2000", FCVAR_CHEAT ); + +ConVar tf_merasmus_should_disguise_threshold( "tf_merasmus_should_disguise_threshold", "0.45f", FCVAR_CHEAT ); +ConVar tf_merasmus_min_props_to_reveal( "tf_merasmus_min_props_to_reveal", "0.7f", FCVAR_CHEAT, "Percentage of total fake props players have to destroy before Merasmus reveals himself"); + +ConVar tf_merasmus_attack_range( "tf_merasmus_attack_range", "200", FCVAR_CHEAT ); + +ConVar tf_merasmus_health_regen_rate( "tf_merasmus_health_regen_rate", "0.001f", FCVAR_CHEAT, "Percentage of Max HP per sec that Merasmus will regenerate while in disguise" ); + +ConVar tf_merasmus_bomb_head_duration( "tf_merasmus_bomb_head_duration", "15.f", FCVAR_CHEAT ); +ConVar tf_merasmus_bomb_head_per_team( "tf_merasmus_bomb_head_per_team", "1", FCVAR_CHEAT ); + +ConVar tf_merasmus_stun_duration( "tf_merasmus_stun_duration", "2.f", FCVAR_CHEAT ); + +extern ConVar tf_merasmus_spawn_interval; +extern ConVar tf_merasmus_spawn_interval_variation; + +#define MERASMUS_MODEL_NAME "models/bots/merasmus/merasmus.mdl" +#define MERASMUS_BOMB_MODEL "models/props_lakeside_event/bomb_temp.mdl" + +static const char* s_pszDisguiseProps[] = +{ + // "models/props_hydro/dumptruck.mdl", // 265 + "models/props_halloween/pumpkin_02.mdl", + "models/props_halloween/pumpkin_03.mdl", + "models/egypt/palm_tree/palm_tree.mdl", + "models/props_spytech/control_room_console01.mdl", + "models/props_spytech/work_table001.mdl", + // "models/egypt/tent/tent.mdl", // 248 + "models/props_coalmines/boulder1.mdl", + "models/props_coalmines/boulder2.mdl", + "models/props_farm/concrete_block001.mdl", // 152 + // "models/props_farm/tractor_tire001.mdl", // requires offset + "models/props_farm/welding_machine01.mdl", + "models/props_medieval/medieval_resupply.mdl", + "models/props_medieval/target/target.mdl", + "models/props_swamp/picnic_table.mdl", + "models/props_manor/baby_grand_01.mdl", // 154 + "models/props_manor/bookcase_132_02.mdl", + "models/props_manor/chair_01.mdl", + "models/props_manor/couch_01.mdl", + "models/props_manor/grandfather_clock_01.mdl", + // "models/props_manor/tractor_01.mdl", // 227 + // "models/props_gameplay/haybale.mdl", // requires offset + "models/props_viaduct_event/coffin_simple_closed.mdl", + // "models/props_farm/wooden_barrel.mdl", // requires offset + "models/props_2fort/miningcrate001.mdl", + "models/props_gameplay/resupply_locker.mdl", + "models/props_2fort/oildrum.mdl", + // "models/props_farm/wood_pile.mdl", // requires offset + "models/props_lakeside/wood_crate_01.mdl", + // "models/props_farm/pallet001.mdl", // requires offset + "models/props_well/hand_truck01.mdl", + "models/props_vehicles/mining_car_metal.mdl", + "models/props_2fort/tire002.mdl", + "models/props_well/computer_cart01.mdl", + "models/egypt/palm_tree/palm_tree.mdl" +}; + + +//----------------------------------------------------------------------------------------------------- +// The Horseless Headless Horseman +//----------------------------------------------------------------------------------------------------- +LINK_ENTITY_TO_CLASS( merasmus, CMerasmus ); + +IMPLEMENT_SERVERCLASS_ST( CMerasmus, DT_Merasmus ) + SendPropBool( SENDINFO( m_bRevealed ) ), + SendPropBool( SENDINFO( m_bIsDoingAOEAttack ) ), + SendPropBool( SENDINFO( m_bStunned ) ), +END_SEND_TABLE() + + +int CMerasmus::m_level = 1; + +IMPLEMENT_AUTO_LIST( IMerasmusAutoList ); + +//----------------------------------------------------------------------------------------------------- +CMerasmus::CMerasmus() +{ + m_intention = new CMerasmusIntention( this ); + m_locomotor = new CMerasmusLocomotion( this ); + m_flyingLocomotor = new CMerasmusFlyingLocomotion( this ); + m_body = new CMerasmusBody( this ); + m_bRevealed = false; + m_bIsDoingAOEAttack = false; + + m_wheel = NULL; + + m_stunTimer.Invalidate(); + m_bStunned = false; + + m_nBombHitCount = 0; + + m_hMerasmusRevealer = NULL; + + m_isFlying = false; + m_isHiding=false; + + m_hHealthBar = g_pMonsterResource; + ListenForGameEvent( "player_death" ); +} + + +//----------------------------------------------------------------------------------------------------- +CMerasmus::~CMerasmus() +{ + if ( m_intention ) + delete m_intention; + + if ( m_locomotor ) + delete m_locomotor; + + if ( m_flyingLocomotor ) + delete m_flyingLocomotor; + + if ( m_body ) + delete m_body; + + // Make sure the health meter goes away + if( m_hHealthBar.Get() ) + { + m_hHealthBar->HideBossHealthMeter(); + } + + IGameEvent *event = gameeventmanager->CreateEvent( "recalculate_truce" ); + if ( event ) + { + gameeventmanager->FireEvent( event, true ); + } +} + +//----------------------------------------------------------------------------------------------------- +void CMerasmus::Precache() +{ + BaseClass::Precache(); + + // always allow late precaching, so we don't pay the cost of the + // Halloween Boss for the entire year + + bool bAllowPrecache = CBaseEntity::IsPrecacheAllowed(); + CBaseEntity::SetAllowPrecache( true ); + + PrecacheMerasmus(); + + CBaseEntity::SetAllowPrecache( bAllowPrecache ); +} + + +void CMerasmus::PrecacheMerasmus() +{ + int model = PrecacheModel( MERASMUS_MODEL_NAME ); + PrecacheGibsForModel( model ); + + // precache disguise prop list + for ( int i=0; i<ARRAYSIZE( s_pszDisguiseProps ); ++i ) + { + PrecacheModel( s_pszDisguiseProps[i] ); + } + + PrecacheModel( MERASMUS_BOMB_MODEL ); + PrecacheModel( "models/props_lakeside_event/bomb_temp_hat.mdl" ); // bomb head on player + PrecacheModel( "models/props_halloween/bombonomicon.mdl" ); // bombonomicon hint to player + + // Boss VOs + PrecacheScriptSound( "Halloween.MerasmusAppears" ); + PrecacheScriptSound( "Halloween.MerasmusBanish" ); + //PrecacheScriptSound( "Halloween.MerasmusCastBleedingSpell" ); + PrecacheScriptSound( "Halloween.MerasmusCastFireSpell" ); + PrecacheScriptSound( "Halloween.MerasmusCastJarateSpell" ); + PrecacheScriptSound( "Halloween.MerasmusCastJarateSpellRare" ); + PrecacheScriptSound( "Halloween.MerasmusLaunchSpell" ); + PrecacheScriptSound( "Halloween.MerasmusControlPoint" ); + PrecacheScriptSound( "Halloween.MerasmusDepart" ); + PrecacheScriptSound( "Halloween.MerasmusDepartRare" ); + PrecacheScriptSound( "Halloween.MerasmusDiscovered" ); + PrecacheScriptSound( "Halloween.MerasmusGrenadeThrow" ); + PrecacheScriptSound( "Halloween.MerasmusHidden" ); + PrecacheScriptSound( "Halloween.MerasmusHiddenRare" ); + PrecacheScriptSound( "Halloween.MerasmusHitByBomb" ); + PrecacheScriptSound( "Halloween.MerasmusHitByBombRare" ); + PrecacheScriptSound( "Halloween.MerasmusInitiateHiding" ); + PrecacheScriptSound( "Halloween.MerasmusStaffAttack" ); + PrecacheScriptSound( "Halloween.MerasmusStaffAttackRare" ); + PrecacheScriptSound( "Halloween.MerasmusTauntFakeProp" ); + + //PrecacheScriptSound( "Halloween.MerasmusTeleport" ); + + // Boss event sound effects + PrecacheScriptSound( "Halloween.MerasmusBossSpawn" ); + PrecacheScriptSound( "Halloween.Merasmus_Death" ); + PrecacheScriptSound( "Halloween.EyeballBossEscapeSoon" ); + PrecacheScriptSound( "Halloween.EyeballBossEscapeImminent" ); + PrecacheScriptSound( "Halloween.EyeballBossEscaped" ); + PrecacheScriptSound( "Halloween.Merasmus_Float" ); + PrecacheScriptSound( "Halloween.Merasmus_Stun" ); + PrecacheScriptSound( "Halloween.Merasmus_Spell" ); + PrecacheScriptSound( "Halloween.Merasmus_Hiding_Explode" ); + + PrecacheParticleSystem( "merasmus_spawn" ); // spawn + PrecacheParticleSystem( "merasmus_tp" ); // puff effect + PrecacheParticleSystem( "merasmus_blood" ); // when he takes damage + PrecacheParticleSystem( "merasmus_blood_bits" ); // when he takes damage while stunned + PrecacheParticleSystem( "merasmus_ambient_body" ); // glow around the body + PrecacheParticleSystem( "merasmus_shoot" ); // when he casts spell + PrecacheParticleSystem( "merasmus_book_attack" ); // big attack + PrecacheParticleSystem( "merasmus_object_spawn" ); // object spawn + PrecacheParticleSystem( "merasmus_zap" ); // zap! + PrecacheParticleSystem( "merasmus_dazed" ); // stunned + PrecacheParticleSystem( "merasmus_dazed_explosion" ); // bomb head explode + + // TEMP + PrecacheScriptSound( "Halloween.HeadlessBossAxeHitFlesh" ); +} + + +//----------------------------------------------------------------------------------------------------- +void CMerasmus::Spawn( void ) +{ + Precache(); + + SetModel( MERASMUS_MODEL_NAME ); + + BaseClass::Spawn(); + + PlayHighPrioritySound("Halloween.MerasmusAppears"); + + m_isHiding=false; + + // scale the boss' health with the player count + int totalPlayers = GetGlobalTFTeam( TF_TEAM_BLUE )->GetNumPlayers() + GetGlobalTFTeam( TF_TEAM_RED )->GetNumPlayers(); + + int health = tf_merasmus_health_base.GetInt(); + if ( totalPlayers > tf_merasmus_min_player_count.GetInt() ) + { + health += ( totalPlayers - tf_merasmus_min_player_count.GetInt() ) * tf_merasmus_health_per_player.GetInt(); + } + + CBaseEntity *pWheel = gEntList.FindEntityByName( NULL, "wheel_of_fortress" ); + if ( pWheel ) + { + m_wheel = assert_cast< CWheelOfDoom* >( pWheel ); + } + + SetHealth( health ); + SetMaxHealth( health ); + + m_homePos = GetAbsOrigin(); + + m_damagePoseParameter = -1; + + SetBloodColor( DONT_BLEED ); + + if ( m_pIdleSound ) + { + CSoundEnvelopeController::GetController().SoundDestroy( m_pIdleSound ); + m_pIdleSound = NULL; + } + + // Collect all of the players that are alive at this moment. These are the players who will get + // their hat leveled up when Merasmus dies. We only want to give credit to these players to discourage + // the strategy of spawning Merasmus with 10 people so his health is scaled for 10 people, + // then have 22 other people connect and destroy him. + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector ); + FOR_EACH_VEC( playerVector, i ) + { + m_startingAttackersVector.AddToTail( playerVector[i] ); + } + + CPVSFilter filter( GetAbsOrigin() ); + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + m_pIdleSound = controller.SoundCreate( filter, entindex(), "Halloween.Merasmus_Float" ); + controller.Play( m_pIdleSound, 1.0, 100 ); + + m_solidType = GetSolid(); + m_solidFlags = GetSolidFlags(); + + const float flLifeTime = tf_merasmus_lifetime.GetFloat(); + m_lifeTimer.Start( flLifeTime ); + m_flLastWarnTime = 0; + + IGameEvent *event = gameeventmanager->CreateEvent( "merasmus_summoned" ); + if ( event ) + { + event->SetInt( "level", GetLevel() ); + gameeventmanager->FireEvent( event ); + } + TriggerLogicRelay( "boss_enter_relay", true ); + + DispatchParticleEffect( "merasmus_spawn", GetAbsOrigin(), GetAbsAngles() ); + + m_bossStats.ResetStats(); + + event = gameeventmanager->CreateEvent( "recalculate_truce" ); + if ( event ) + { + gameeventmanager->FireEvent( event, true ); + } +} + + +//--------------------------------------------------------------------------------------------- +void RemoveAllBombHeadFromPlayers() +{ + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + for ( int i=0; i<playerVector.Count(); ++i ) + { + if ( playerVector[i]->m_Shared.InCond( TF_COND_HALLOWEEN_BOMB_HEAD ) ) + { + playerVector[i]->m_Shared.RemoveCond( TF_COND_HALLOWEEN_BOMB_HEAD ); + } + } +} + + +void CMerasmus::UpdateOnRemove() +{ + Assert( TFGameRules() ); + if ( m_hHealthBar ) + { + m_hHealthBar->HideBossHealthMeter(); + } + + if ( m_pIdleSound ) + { + CSoundEnvelopeController::GetController().SoundDestroy( m_pIdleSound ); + m_pIdleSound = NULL; + } + + // the boss is NULL, remove bomb head condition won't give players power play + RemoveAllBombHeadFromPlayers(); + + RemoveAllFakeProps(); + + BaseClass::UpdateOnRemove(); + + // Report Stats + SW_ReportMerasmusStats(); +} + + +//--------------------------------------------------------------------------------------------- +float MerasmusModifyDamage( const CTakeDamageInfo &info ) +{ + CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase * >( info.GetWeapon() ); + CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( info.GetInflictor() ); + CTFProjectile_SentryRocket *sentryRocket = dynamic_cast< CTFProjectile_SentryRocket * >( info.GetInflictor() ); + + if ( sentry || sentryRocket ) + { + return info.GetDamage() * 0.5f; + } + else if ( pWeapon ) + { + switch( pWeapon->GetWeaponID() ) + { + case TF_WEAPON_MINIGUN: + return info.GetDamage() * 0.5f; + + case TF_WEAPON_SODA_POPPER: + return info.GetDamage() * 1.5f; + + case TF_WEAPON_HANDGUN_SCOUT_PRIMARY: + return info.GetDamage() * 1.75; + + case TF_WEAPON_SCATTERGUN: + case TF_WEAPON_REVOLVER: + return info.GetDamage() * 2.f; + + case TF_WEAPON_SNIPERRIFLE: + case TF_WEAPON_SNIPERRIFLE_DECAP: + case TF_WEAPON_SNIPERRIFLE_CLASSIC: + case TF_WEAPON_COMPOUND_BOW: + case TF_WEAPON_KNIFE: + case TF_WEAPON_PEP_BRAWLER_BLASTER: + return info.GetDamage() * 3.f; + } + } + + // unmodified + return info.GetDamage(); +} + + +//----------------------------------------------------------------------------------------------------- +int CMerasmus::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + if ( !IsRevealed() ) + { + return 0; + } + + if ( IsSelf( info.GetAttacker() ) ) + { + // don't injure myself + return 0; + } + + CTakeDamageInfo modifiedInfo = info; + + if ( RandomInt( 0, 30 ) == 0 ) + { + //EmitSound( "Halloween.MerasmusHurt" ); << add new sound entry + } + + if ( IsStunned() ) + { + DispatchParticleEffect( "merasmus_blood", modifiedInfo.GetDamagePosition(), GetAbsAngles() ); + } + else + { + DispatchParticleEffect( "merasmus_blood_bits", modifiedInfo.GetDamagePosition(), GetAbsAngles() ); + } + + modifiedInfo.SetDamage( MerasmusModifyDamage( modifiedInfo ) ); + if ( m_bIsDoingAOEAttack || m_bStunned ) + { + modifiedInfo.AddDamageType( DMG_CRITICAL ); + } + + int result = BaseClass::OnTakeDamage_Alive( modifiedInfo ); + + // update boss health meter + float healthPercentage = (float)GetHealth() / (float)GetMaxHealth(); + + if ( m_hHealthBar ) + { + if ( healthPercentage <= 0.0f ) + { + m_hHealthBar->HideBossHealthMeter(); + } + else + { + m_hHealthBar->SetBossHealthPercentage( healthPercentage ); + } + } + + // Stats Tracking + CTFPlayer *pAttacker = ToTFPlayer( info.GetAttacker() ); + if ( pAttacker ) + { + int iClass = pAttacker->GetPlayerClass()->GetClassIndex(); + if ( iClass > TF_CLASS_UNDEFINED && iClass < TF_LAST_NORMAL_CLASS ) + { + m_bossStats.m_arrClassDamage[ iClass ] += info.GetDamage(); + } + } + return result; +} + +//--------------------------------------------------------------------------------------------- +void CMerasmus::FireGameEvent( IGameEvent *event) +{ + if ( !V_strcmp( event->GetName(), "player_death" ) ) + { + // Collect Data + int nDmgType = event->GetInt( "customkill", -1 ); + if ( nDmgType == TF_DMG_CUSTOM_MERASMUS_DECAPITATION || nDmgType == TF_DMG_CUSTOM_MERASMUS_ZAP ) + { + m_bossStats.m_nStaffKills++; + } + else if ( nDmgType == TF_DMG_CUSTOM_MERASMUS_GRENADE || nDmgType == TF_DMG_CUSTOM_MERASMUS_PLAYER_BOMB ) + { + m_bossStats.m_nBombKills++; + } + else + { + // Treat as a PvPKill + m_bossStats.m_nPvpKills++; + } + } +} + +//--------------------------------------------------------------------------------------------- +void CMerasmus::Update( void ) +{ + BaseClass::Update(); + + if ( m_damagePoseParameter < 0 ) + { + m_damagePoseParameter = LookupPoseParameter( "damage" ); + } + + if ( m_damagePoseParameter >= 0 ) + { + SetPoseParameter( m_damagePoseParameter, 1.0f - ( (float)GetHealth() / (float)GetMaxHealth() ) ); + } +} + + +Vector CMerasmus::GetCastPosition() const +{ + Vector vForward, vRight, vUp; + AngleVectors( GetAbsAngles(), &vForward, &vRight, &vUp ); + return WorldSpaceCenter() + vForward * 60.f + 30.f * vRight + 60.f * vUp; +} + + +void RemoveAllGrenades( CMerasmus *me ) +{ + const int maxCollectedEntities = 1024; + CBaseEntity *pObjects[ maxCollectedEntities ]; + int count = UTIL_EntitiesInSphere( pObjects, maxCollectedEntities, me->GetAbsOrigin(), 400, FL_GRENADE ); + + for( int i = 0; i < count; ++i ) + { + if ( pObjects[i] == NULL ) + continue; + + if ( pObjects[i]->IsPlayer() ) + continue; + + // Remove the enemy pipe + pObjects[i]->SetThink( &CBaseEntity::SUB_Remove ); + pObjects[i]->SetNextThink( gpGlobals->curtime ); + pObjects[i]->SetTouch( NULL ); + pObjects[i]->AddEffects( EF_NODRAW ); + } +} + +void CMerasmus::OnRevealed(bool bPlaySound) +{ + RecordDisguiseTime(); + m_bRevealed = true; + m_isHiding = false; + m_nRevealedHealth = GetHealth(); + + if (bPlaySound) + { + PlayHighPrioritySound( "Halloween.MerasmusDiscovered" ); + } + + RemoveEffects( EF_NOINTERP | EF_NODRAW ); + + DispatchParticleEffect( "merasmus_spawn", WorldSpaceCenter(), GetAbsAngles() ); + + // don't collide with anything, we do our own collision detection in our think method + SetSolid( m_solidType ); + SetSolidFlags( m_solidFlags ); + + RemoveAllFakeProps(); + TFGameRules()->PushAllPlayersAway( GetAbsOrigin(), 400.f, 500.f, TF_TEAM_RED ); + TFGameRules()->PushAllPlayersAway( GetAbsOrigin(), 400.f, 500.f, TF_TEAM_BLUE ); + RemoveAllGrenades( this ); + + // give player who found merasmus buff + if ( m_hMerasmusRevealer ) + { + // condition was removed by blowing up merasmus, you get buff award + const float buffDuration = 10.0f; + m_hMerasmusRevealer->m_Shared.AddCond( TF_COND_CRITBOOSTED_PUMPKIN, buffDuration ); + m_hMerasmusRevealer->m_Shared.AddCond( TF_COND_SPEED_BOOST, buffDuration ); + m_hMerasmusRevealer->m_Shared.AddCond( TF_COND_INVULNERABLE, buffDuration ); + } + m_hMerasmusRevealer = NULL; + + // show Boss' health meter on HUD + if ( m_hHealthBar ) + { + float healthPercentage = (float)GetHealth() / (float)GetMaxHealth(); + m_hHealthBar->SetBossHealthPercentage( healthPercentage ); + } + + // face towards a nearby player + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + float closeRangeSq = FLT_MAX; + CTFPlayer *close = NULL; + + for( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *player = playerVector[i]; + + float rangeSq = GetRangeSquaredTo( player ); + if ( rangeSq < closeRangeSq ) + { + closeRangeSq = rangeSq; + close = player; + } + } + + QAngle facingAngle; + + if ( close ) + { + Vector toPlayer = close->GetAbsOrigin() - GetAbsOrigin(); + toPlayer.z = 0.0f; + toPlayer.NormalizeInPlace(); + + VectorAngles( toPlayer, Vector(0,0,1), facingAngle ); + } + else + { + facingAngle.x = 0.0f; + facingAngle.y = RandomFloat( 0.0f, 360.0f ); + facingAngle.z = 0.0f; + } + + SetAbsAngles( facingAngle ); +} + + +bool CMerasmus::ShouldReveal() const +{ + int nDestroyedProps = 0; + for ( int i=0; i<m_fakePropVector.Count(); ++i ) + { + if ( m_fakePropVector[i] == NULL ) + { + nDestroyedProps++; + } + } + return nDestroyedProps >= m_nDestroyedPropsToReveal; +} + + +bool CMerasmus::IsNextKilledPropMerasmus() const +{ + int nDestroyedProps = 0; + for ( int i=0; i<m_fakePropVector.Count(); ++i ) + { + if ( m_fakePropVector[i] == NULL ) + { + nDestroyedProps++; + } + } + return nDestroyedProps + 1 == m_nDestroyedPropsToReveal; +} + + +void CMerasmus::OnDisguise() +{ + m_flStartDisguiseTime = gpGlobals->curtime; + m_bRevealed = false; + m_isHiding = true; + + StopAOEAttack(); + + AddEffects( EF_NOINTERP | EF_NODRAW ); + + DispatchParticleEffect( "merasmus_tp", WorldSpaceCenter(), GetAbsAngles() ); + + // don't collide with anything, we do our own collision detection in our think method + SetSolid( SOLID_NONE ); + SetSolidFlags( FSOLID_NOT_SOLID ); + + // fake randomness of when Merasmus should reveal + m_nDestroyedPropsToReveal = RandomInt( MAX( 1, tf_merasmus_min_props_to_reveal.GetFloat() * m_fakePropVector.Count() ), m_fakePropVector.Count() ); +} + + +bool CMerasmus::ShouldDisguise() const +{ + if ( GetHealth() <= 0 ) + { + return false; + } + + float flLostHealthPercentage = (float)( m_nRevealedHealth - GetHealth() ) / (float)GetMaxHealth(); + return flLostHealthPercentage > tf_merasmus_should_disguise_threshold.GetFloat(); +} + + +CTFWeaponBaseGrenadeProj* CMerasmus::CreateMerasmusGrenade( const Vector& vPosition, const Vector& vVelocity, CBaseCombatCharacter* pOwner, float fScale ) +{ + QAngle qAngles = RandomAngle( 0, 360 ); + CTFWeaponBaseMerasmusGrenade *pGrenade = static_cast<CTFWeaponBaseMerasmusGrenade*>( CBaseEntity::Create( "tf_weaponbase_merasmus_grenade", vPosition, qAngles, pOwner ) ); + if ( pGrenade ) + { + pGrenade->SetModel( MERASMUS_BOMB_MODEL ); + DispatchSpawn( pGrenade ); + pGrenade->InitGrenade( vVelocity, AngularImpulse( 600, random->RandomInt( -1200, 1200 ), 0 ), pOwner, 50 * fScale, 300.f * fScale ); + pGrenade->SetDetonateTimerLength( 2.f ); + pGrenade->SetModelScale( fScale ); + pGrenade->SetCollisionGroup( TFCOLLISION_GROUP_ROCKETS ); // we want to use collision_group_rockets so we don't ever collide with players + } + + return pGrenade; +} + + +const char* CMerasmus::GetRandomPropModelName() +{ + int which = RandomInt( 0, ARRAYSIZE( s_pszDisguiseProps ) - 1 ); + return s_pszDisguiseProps[ which ]; +} + + +void CMerasmus::PushPlayer( CTFPlayer* pPlayer, float flPushForce ) const +{ + // send the player flying + // make sure we push players up and away + Vector toPlayer = pPlayer->EyePosition() - GetAbsOrigin(); + toPlayer.z = 0.0f; + toPlayer.NormalizeInPlace(); + toPlayer.z = 1.0f; + + Vector push = flPushForce * toPlayer; + + pPlayer->ApplyAbsVelocityImpulse( push ); +} + + +void CMerasmus::AddStun( CTFPlayer* pPlayer ) +{ + if ( !IsRevealed() ) + { + // don't let bomb head player explode on merasmus while disguise + return; + } + + // first stun + if ( !IsStunned() ) + { + CPVSFilter filter( WorldSpaceCenter() ); + if (RandomInt( 1, 10) == 9 ) + { + PlayLowPrioritySound( filter, "Halloween.MerasmusHitByBombRare" ); + } + else + { + PlayLowPrioritySound( filter, "Halloween.MerasmusHitByBomb" ); + } + } + + // buff the player that stunned me + const float buffDuration = 10.0f; + pPlayer->m_Shared.AddCond( TF_COND_CRITBOOSTED_PUMPKIN, buffDuration ); + pPlayer->m_Shared.AddCond( TF_COND_SPEED_BOOST, buffDuration ); + pPlayer->m_Shared.AddCond( TF_COND_INVULNERABLE, buffDuration ); + + pPlayer->m_Shared.RemoveCond( TF_COND_STUNNED ); + pPlayer->m_Shared.RemoveCond( TF_COND_HALLOWEEN_BOMB_HEAD ); + pPlayer->MerasmusPlayerBombExplode( false ); + + PushPlayer( pPlayer, 300.f ); + DispatchParticleEffect( "merasmus_dazed_explosion", WorldSpaceCenter(), GetAbsAngles() ); + + IGameEvent *pEvent = gameeventmanager->CreateEvent( "merasmus_stunned" ); + if ( pEvent ) + { + pEvent->SetInt( "player", pPlayer->GetUserID() ); + gameeventmanager->FireEvent( pEvent, true ); + } + + // don't stun while doing AOE + if ( !m_bIsDoingAOEAttack ) + { + m_nBombHitCount++; + m_stunTimer.Start( tf_merasmus_stun_duration.GetFloat() ); + } +} + + +void CMerasmus::OnBeginStun() +{ + EmitSound( "Halloween.Merasmus_Stun" ); + + m_bStunned = true; +} + + +void CMerasmus::OnEndStun() +{ + m_stunTimer.Invalidate(); + m_bStunned = false; +} + + +void CMerasmus::AddFakeProp( CTFMerasmusTrickOrTreatProp* pFakeProp ) +{ + m_fakePropVector.AddToTail( pFakeProp ); +} + + +void CMerasmus::RemoveAllFakeProps() +{ + for ( int i=0; i<m_fakePropVector.Count(); ++i ) + { + if ( m_fakePropVector[i] != NULL ) + { + UTIL_Remove( m_fakePropVector[i] ); + } + } + m_fakePropVector.RemoveAll(); +} + + +void BombHeadForTeam( int nTeam, int nBombHeadPlayers ) +{ + // decrease nBombHeadPlayers by the number of existing bomb heads + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, nTeam, COLLECT_ONLY_LIVING_PLAYERS ); + + if ( playerVector.Count() <= 0 ) + { + // everyone on this team is dead + return; + } + + for( int n=0; n<nBombHeadPlayers; ++n ) + { + // find the living player who was a bombhead the longest time ago and give them the bomb + CTFPlayer *pVictim = NULL; + float oldestTimeStamp = -1.0f; + + for( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *pPlayer = playerVector[i]; + + if ( pPlayer->GetTimeSinceWasBombHead() > oldestTimeStamp && + !pPlayer->m_Shared.InCond( TF_COND_HALLOWEEN_BOMB_HEAD ) && + pPlayer->GetLastKnownArea() && + pPlayer->GetLastKnownArea()->HasFuncNavPrefer() ) + { + pVictim = pPlayer; + oldestTimeStamp = pPlayer->GetTimeSinceWasBombHead(); + } + } + + if ( !pVictim ) + { + // no victims available - try again next time + return; + } + + // give this victim the bomb + float flBuffDuration = tf_merasmus_bomb_head_duration.GetFloat(); + pVictim->m_Shared.StunPlayer( tf_merasmus_bomb_head_duration.GetFloat(), 0.f, TF_STUN_LOSER_STATE ); + pVictim->m_Shared.AddCond( TF_COND_HALLOWEEN_BOMB_HEAD, flBuffDuration ); + pVictim->m_Shared.AddCond( TF_COND_SPEED_BOOST, flBuffDuration ); + + pVictim->SetBombHeadTimestamp(); + + // notify player they are a bomb + ClientPrint( pVictim, HUD_PRINTCENTER, "#TF_HALLOWEEN_MERASMUS_YOU_ARE_BOMB", pVictim->GetPlayerName() ); + } +} + + +void CMerasmus::BombHeadMode() +{ + int nBombHeadPlayers = tf_merasmus_bomb_head_per_team.GetInt(); + BombHeadForTeam( TF_TEAM_RED, nBombHeadPlayers ); + BombHeadForTeam( TF_TEAM_BLUE, nBombHeadPlayers ); +} + + +bool CMerasmus::ShouldLeave() const +{ + return m_lifeTimer.IsElapsed(); +} + + +void CMerasmus::LeaveWarning() +{ + if ( m_lifeTimer.GetRemainingTime() < 10.0f && m_flLastWarnTime > 10.0f ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "merasmus_escape_warning" ); + if ( event ) + { + event->SetInt( "level", GetLevel() ); + event->SetInt( "time_remaining", 10 ); + gameeventmanager->FireEvent( event ); + } + } + else if ( m_lifeTimer.GetRemainingTime() < 30.0f && m_flLastWarnTime > 30.0f ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "merasmus_escape_warning" ); + if ( event ) + { + event->SetInt( "level", GetLevel() ); + event->SetInt( "time_remaining", 30 ); + gameeventmanager->FireEvent( event ); + } + } + else if ( m_lifeTimer.GetRemainingTime() < 60.0f && m_flLastWarnTime > 60.0f ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "merasmus_escape_warning" ); + if ( event ) + { + event->SetInt( "level", GetLevel() ); + event->SetInt( "time_remaining", 60 ); + gameeventmanager->FireEvent( event ); + } + } + m_flLastWarnTime = m_lifeTimer.GetRemainingTime(); +} + + +void CMerasmus::OnLeaveWhileInPropForm() +{ + CUtlVector< CBaseEntity* > validProps; + for ( int i=0; i<m_fakePropVector.Count(); ++i ) + { + if ( m_fakePropVector[i] != NULL ) + { + validProps.AddToTail( m_fakePropVector[i] ); + } + } + + if ( validProps.Count() ) + { + int which = RandomInt( 0, validProps.Count() -1 ); + SetAbsOrigin( validProps[ which ]->GetAbsOrigin() ); + } +} + + +void CMerasmus::TriggerLogicRelay( const char* pszLogicRelayName, bool bSpawn /*= false*/ ) +{ + CLogicRelay* pLogicRelay = assert_cast< CLogicRelay* >( gEntList.FindEntityByName( NULL, pszLogicRelayName ) ); + if ( pLogicRelay ) + { + inputdata_t data; + data.pCaller = this; + data.pActivator = this; + pLogicRelay->InputTrigger( data ); + + if ( bSpawn ) + { + SetAbsOrigin( pLogicRelay->GetAbsOrigin() ); + } + } +} + + +void CMerasmus::PlayLowPrioritySound( IRecipientFilter &filter, const char* pszSoundEntryName ) +{ + CSoundParameters params; + if ( CBaseEntity::GetParametersForSound( pszSoundEntryName, params, NULL ) ) + { + EmitSound_t es( params ); + es.m_nFlags |= SND_DO_NOT_OVERWRITE_EXISTING_ON_CHANNEL; + EmitSound( filter, entindex(), es ); + } +} + +void CMerasmus::PlayHighPrioritySound( const char* pszSoundEntryName ) +{ + CBroadcastRecipientFilter filter; + CSoundParameters params; + if ( CBaseEntity::GetParametersForSound( pszSoundEntryName, params, NULL ) ) + { + EmitSound_t es( params ); + EmitSound( filter, entindex(), es ); + } +} + +//--------------------------------------------------------------------------------------------- +void CMerasmus::RecordDisguiseTime( ) +{ + if ( m_flStartDisguiseTime == 0 ) + return; + + float flTime = ( gpGlobals->curtime - m_flStartDisguiseTime ); + + if ( m_bossStats.m_flPropHuntTime1 == 0 ) + { + m_bossStats.m_flPropHuntTime1 = flTime; + } + else + { + m_bossStats.m_flPropHuntTime2 = flTime; + } + + m_flStartDisguiseTime = 0; +} + +void CMerasmus::StartRespawnTimer() const +{ + if( TFGameRules() ) + { + if( GetLevel() <= 3 ) + { + TFGameRules()->StartHalloweenBossTimer( tf_merasmus_spawn_interval.GetFloat() ,tf_merasmus_spawn_interval_variation.GetFloat() ); + } + else + { + TFGameRules()->StartHalloweenBossTimer( 60.f ); + } + } +} + +//--------------------------------------------------------------------------------------------- +void CMerasmus::SW_ReportMerasmusStats( void ) +{ + if ( !GCClientSystem() ) + return; + + static uint8 unEventCounter = 0; + + GCSDK::CProtoBufMsg<CMsgHalloween_Merasmus2012> msg( k_EMsgGC_Halloween_Merasmus2012 ); + msg.Body().set_time_submitted( CRTime::RTime32TimeCur() ); + msg.Body().set_is_valve_server( false ); + msg.Body().set_boss_level( GetLevel() ); + msg.Body().set_spawned_health( GetMaxHealth() ); + msg.Body().set_remaining_health( GetHealth() ); + msg.Body().set_life_time( (int)m_lifeTimer.GetElapsedTime() ); // Amount of time in seconds, boss was alive for + msg.Body().set_bomb_kills( m_bossStats.m_nBombKills ); // Kills from Bombs + msg.Body().set_staff_kills( m_bossStats.m_nStaffKills ); // kills from staff attack + msg.Body().set_pvp_kills( m_bossStats.m_nPvpKills ); // Number of kills from players while Boss is out (Jerk factor) + msg.Body().set_prophunt_time1( m_bossStats.m_flPropHuntTime1 ); + msg.Body().set_prophunt_time2( m_bossStats.m_flPropHuntTime2 ); + + msg.Body().set_dmg_scout( m_bossStats.m_arrClassDamage[ TF_CLASS_SCOUT ] ); // Amount of damage done by each class + msg.Body().set_dmg_sniper( m_bossStats.m_arrClassDamage[ TF_CLASS_SNIPER] ); + msg.Body().set_dmg_soldier( m_bossStats.m_arrClassDamage[ TF_CLASS_SOLDIER] ); + msg.Body().set_dmg_demo( m_bossStats.m_arrClassDamage[ TF_CLASS_DEMOMAN] ); + msg.Body().set_dmg_medic( m_bossStats.m_arrClassDamage[ TF_CLASS_MEDIC ] ); + msg.Body().set_dmg_heavy( m_bossStats.m_arrClassDamage[ TF_CLASS_HEAVYWEAPONS ] ); + msg.Body().set_dmg_pyro( m_bossStats.m_arrClassDamage[ TF_CLASS_PYRO ] ); + msg.Body().set_dmg_spy( m_bossStats.m_arrClassDamage[ TF_CLASS_SPY ] ); + msg.Body().set_dmg_engineer( m_bossStats.m_arrClassDamage[ TF_CLASS_ENGINEER ] ); + + // Class counts + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector ); + + int nClassCounts[ TF_LAST_NORMAL_CLASS ]; + V_memset( nClassCounts, 0, sizeof( nClassCounts ) ); + FOR_EACH_VEC( playerVector, index ) + { + int iClass = playerVector[index]->GetPlayerClass()->GetClassIndex(); + if ( iClass > TF_CLASS_UNDEFINED && iClass < TF_LAST_NORMAL_CLASS ) + { + nClassCounts[iClass]++; + } + } + + msg.Body().set_scout_count( nClassCounts[TF_CLASS_SCOUT] ); // Class and player break down at the point of boss despawn + msg.Body().set_sniper_count( nClassCounts[TF_CLASS_SNIPER] ); + msg.Body().set_solider_count( nClassCounts[TF_CLASS_SOLDIER] ); + msg.Body().set_demo_count( nClassCounts[TF_CLASS_DEMOMAN] ); + msg.Body().set_medic_count( nClassCounts[TF_CLASS_MEDIC] ); + msg.Body().set_heavy_count( nClassCounts[TF_CLASS_HEAVYWEAPONS] ); + msg.Body().set_pyro_count( nClassCounts[TF_CLASS_PYRO] ); + msg.Body().set_spy_count( nClassCounts[TF_CLASS_SPY] ); + msg.Body().set_engineer_count( nClassCounts[TF_CLASS_ENGINEER] ); + + GCClientSystem()->BSendMessage( msg ); + + +// OGS Version +// +//#if !defined(NO_STEAM) +// KeyValues* pKVData = new KeyValues( "TF2Halloween2012MerasmusBossStats" ); +// +// // Auto Values +// // ID +// // SessionID +// // TimeSubmitted +// //pKVData->SetBool( "IsValveServer", false ); +// +// pKVData->SetInt( "BossLevel", GetLevel() ); +// pKVData->SetInt( "SpawnedHealth", GetMaxHealth() ); +// pKVData->SetInt( "RemainingHealth", GetHealth() ); // 0 == Boss was killed +// +// pKVData->SetInt( "LifeTime", (int)m_lifeTimer.GetElapsedTime() ); // Amount of time in seconds, boss was alive for +// pKVData->SetInt( "BombKills", m_bossStats.m_nBombKills ); // Kills from Bombs +// pKVData->SetInt( "StaffKills", m_bossStats.m_nStaffKills ); // kills from staff account +// pKVData->SetInt( "PvPKills", m_bossStats.m_nPvpKills ); // Number of kills from players while Boss is out (Jerk factor) +// pKVData->SetInt( "PropHuntTime1", m_bossStats.m_flPropHuntTime1 ); +// pKVData->SetInt( "PropHuntTime2", m_bossStats.m_flPropHuntTime2 ); +// +// // Class Damage +// pKVData->SetInt( "DmgFromScout", m_bossStats.m_arrClassDamage[ TF_CLASS_SCOUT ] ); // Amount of damage done by each class +// pKVData->SetInt( "DmgFromSniper", m_bossStats.m_arrClassDamage[ TF_CLASS_SNIPER ] ); +// pKVData->SetInt( "DmgFromSoldier", m_bossStats.m_arrClassDamage[ TF_CLASS_SOLDIER ] ); +// pKVData->SetInt( "DmgFromDemo", m_bossStats.m_arrClassDamage[ TF_CLASS_DEMOMAN ] ); +// pKVData->SetInt( "DmgFromMedic", m_bossStats.m_arrClassDamage[ TF_CLASS_MEDIC ] ); +// pKVData->SetInt( "DmgFromHeavy", m_bossStats.m_arrClassDamage[ TF_CLASS_HEAVYWEAPONS ] ); +// pKVData->SetInt( "DmgFromPyro", m_bossStats.m_arrClassDamage[ TF_CLASS_PYRO ] ); +// pKVData->SetInt( "DmgFromSpy", m_bossStats.m_arrClassDamage[ TF_CLASS_SPY ] ); +// pKVData->SetInt( "DmgFromEngineer", m_bossStats.m_arrClassDamage[ TF_CLASS_ENGINEER ] ); +// +// // Class counts +// CUtlVector< CTFPlayer * > playerVector; +// CollectPlayers( &playerVector ); +// +// int nClassCounts[ TF_LAST_NORMAL_CLASS ]; +// V_memset( nClassCounts, 0, sizeof( nClassCounts ) ); +// FOR_EACH_VEC( playerVector, index ) +// { +// int iClass = playerVector[index]->GetPlayerClass()->GetClassIndex(); +// if ( iClass > TF_CLASS_UNDEFINED && iClass < TF_LAST_NORMAL_CLASS ) +// { +// nClassCounts[iClass]++; +// } +// } +// +// pKVData->SetInt( "ScoutCount", nClassCounts[TF_CLASS_SCOUT] ); // Class and player break down at the point of boss despawn +// pKVData->SetInt( "SniperCount", nClassCounts[TF_CLASS_SNIPER] ); +// pKVData->SetInt( "SoldierCount", nClassCounts[TF_CLASS_SOLDIER] ); +// pKVData->SetInt( "DemoCount", nClassCounts[TF_CLASS_DEMOMAN] ); +// pKVData->SetInt( "MedicCount", nClassCounts[TF_CLASS_MEDIC] ); +// pKVData->SetInt( "HeavyCount", nClassCounts[TF_CLASS_HEAVYWEAPONS] ); +// pKVData->SetInt( "PyroCount", nClassCounts[TF_CLASS_PYRO] ); +// pKVData->SetInt( "SpyCount", nClassCounts[TF_CLASS_SPY] ); +// pKVData->SetInt( "EngineerCount", nClassCounts[TF_CLASS_ENGINEER] ); +// +// //GetSteamWorksSGameStatsUploader().AddStatsForUpload( pKVData ); +//#endif + + m_bossStats.ResetStats(); +} + + +void CollectTargets( CBaseCombatCharacter *pCaster, float flSpellRange, int nTargetTeam, int nMaxTarget, CUtlVector< CHandle< CBaseEntity > > &vecTargets ) +{ + vecTargets.RemoveAll(); + + // collect everyone + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, nTargetTeam, COLLECT_ONLY_LIVING_PLAYERS ); + + CUtlVector< CTFPlayer * > candidateTargets; + for ( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *pPlayer = playerVector[i]; + Vector toPlayer = pCaster->EyePosition() - pPlayer->WorldSpaceCenter(); + if ( toPlayer.IsLengthLessThan( flSpellRange ) && pCaster->IsLineOfSightClear( pPlayer ) ) + { + candidateTargets.AddToTail( pPlayer ); + } + } + + while ( candidateTargets.Count() != 0 && vecTargets.Count() != nMaxTarget ) + { + int which = RandomInt( 0, candidateTargets.Count() - 1 ); + vecTargets.AddToTail( candidateTargets[ which ] ); + candidateTargets.FastRemove( which ); + } + + // find sentry in range + for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i ) + { + CBaseObject *pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] ); + if ( pObj->ObjectType() == OBJ_SENTRYGUN ) + { + Vector toSentry = pCaster->EyePosition() - pObj->WorldSpaceCenter(); + if ( toSentry.IsLengthLessThan( flSpellRange ) && pCaster->IsLineOfSightClear( pObj ) ) + { + vecTargets.AddToTail( pObj ); + } + } + } +} + + +void CastSpell( CBaseCombatCharacter* pCaster, const char* pszCastingAttachmentName, float flSpellRange, float flMinDamage, float flMaxDamage, CBaseEntity* pTarget ) +{ + float flSpellTime = 5.f; + + if ( pTarget->IsPlayer() ) + { + CTFPlayer *pPlayer = ToTFPlayer( pTarget ); + pPlayer->m_Shared.SelfBurn( flSpellTime ); + pPlayer->ApplyAbsVelocityImpulse( 1000.f * Vector( 0, 0, 1 ) ); + + Vector toPlayer = pCaster->EyePosition() - pPlayer->WorldSpaceCenter(); + float flDistSqr = toPlayer.LengthSqr(); + + float flDmg = RemapValClamped( flDistSqr, 100.f, Square( 0.5f * flSpellRange ), flMaxDamage, flMinDamage ); + CTakeDamageInfo info( pCaster, pCaster, flDmg, DMG_BURN, TF_DMG_CUSTOM_MERASMUS_ZAP ); + pPlayer->TakeDamage( info ); + } + else if ( pTarget->IsBaseObject() ) + { + CBaseObject *pObj = static_cast< CBaseObject* >( pTarget ); + pObj->DetonateObject(); + } + + // Shoot a beam at them + CReliableBroadcastRecipientFilter filter; + Vector vStartPos; + QAngle qStartAngles; + pCaster->GetAttachment( pszCastingAttachmentName, vStartPos, qStartAngles ); + Vector vEnd = pTarget->EyePosition(); + te_tf_particle_effects_control_point_t controlPoint = { PATTACH_ABSORIGIN, vEnd }; + TE_TFParticleEffectComplex( filter, 0.0f, "merasmus_zap", vStartPos, qStartAngles, NULL, &controlPoint, pCaster, PATTACH_CUSTOMORIGIN ); +} + + +/*static*/ bool CMerasmus::Zap( CBaseCombatCharacter *pCaster, const char* pszCastingAttachmentName, float flSpellRange, float flMinDamage, float flMaxDamage, int nMaxTarget, int nTargetTeam /*= TEAM_ANY*/ ) +{ + CUtlVector< CHandle< CBaseEntity > > vecTargets; + CollectTargets( pCaster, flSpellRange, nTargetTeam, nMaxTarget, vecTargets ); + + if ( vecTargets.Count() == 0 ) + return false; + + for ( int i=0; i<vecTargets.Count(); ++i ) + { + CBaseEntity *pTarget = vecTargets[i]; + if ( pTarget ) + { + CastSpell( pCaster, pszCastingAttachmentName, flSpellRange, flMinDamage, flMaxDamage, pTarget ); + } + } + + return true; +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CMerasmusBehavior : public Action< CMerasmus > +{ +public: + virtual Action< CMerasmus > *InitialContainedAction( CMerasmus *me ) + { + return new CMerasmusReveal; + } + + virtual ActionResult< CMerasmus > OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ) + { + return Continue(); + } + + virtual ActionResult< CMerasmus > Update( CMerasmus *me, float interval ) + { + if ( !me->IsAlive() ) + { + if ( !me->WasSpawnedByCheats() ) + { + // award achievement to everyone who injured me within the last few seconds + const float deathTime = 5.0f; + const CUtlVector< CMerasmus::AttackerInfo > &attackerVector = me->GetAttackerVector(); + for( int i=0; i<attackerVector.Count(); ++i ) + { + if ( attackerVector[i].m_attacker != NULL && + gpGlobals->curtime - attackerVector[i].m_timestamp < deathTime ) + { + CReliableBroadcastRecipientFilter filter; + UTIL_SayText2Filter( filter, attackerVector[i].m_attacker, false, "#TF_Halloween_Merasmus_Killers", attackerVector[i].m_attacker->GetPlayerName() ); + + if ( TFGameRules() && TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_LAKESIDE ) ) + { + attackerVector[i].m_attacker->AwardAchievement( ACHIEVEMENT_TF_HALLOWEEN_MERASMUS_KILL ); + } + } + } + + // Award hat levels based on Merasmus' level when he dies, but only to people who were + // around when Merasmus spawned. + const CUtlVector< CHandle<CTFPlayer> >& vecStartingAttackers = me->GetStartingAttackers(); + if ( GCClientSystem() ) + { + // GC message + // Notify the GC that this occurred to possibly level up your hat if you have one + GCSDK::CProtoBufMsg<CMsgUpdateHalloweenMerasmusLootLevel> msg( k_EMsgGC_Halloween_UpdateMerasmusLootLevel ); + msg.Body().set_merasmus_level( me->GetLevel() ); + + FOR_EACH_VEC( vecStartingAttackers, i ) + { + CTFPlayer* pPlayer = vecStartingAttackers[i]; + if ( pPlayer ) + { + CSteamID steamID; + if ( pPlayer->GetSteamID( &steamID ) && steamID.IsValid() && steamID.BIndividualAccount() ) + { + CMsgUpdateHalloweenMerasmusLootLevel_Player *pMsgPlayer = msg.Body().add_players(); + pMsgPlayer->set_steam_id( steamID.ConvertToUint64() ); + } + + } + } + GCClientSystem()->BSendMessage( msg ); + } + } + + // nobody is IT any longer + TFGameRules()->SetIT( NULL ); + + return ChangeTo( new CMerasmusDying, "I am dead!" ); + } + else + { + me->LeaveWarning(); + + if ( me->ShouldLeave() && !me->IsStunned() && !me->IsFlying() ) + { + return ChangeTo( new CMerasmusEscape, "Escaping..." ); + } + } + + return Continue(); + } + + + virtual EventDesiredResult< CMerasmus > OnInjured( CMerasmus *me, const CTakeDamageInfo &info ) + { + if ( me->ShouldDisguise() && me->IsRevealed() && !me->IsStunned() && !me->IsFlying() ) + { + return TrySuspendFor( new CMerasmusDisguise, RESULT_IMPORTANT, "Disguise" ); + } + + return TryContinue(); + } + + virtual const char *GetName( void ) const { return "Merasmus Behavior"; } // return name of this action + +private: + + +}; + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +CMerasmusIntention::CMerasmusIntention( CMerasmus *me ) : IIntention( me ) +{ + m_behavior = new Behavior< CMerasmus >( new CMerasmusBehavior ); +} + +CMerasmusIntention::~CMerasmusIntention() +{ + delete m_behavior; +} + +void CMerasmusIntention::Reset( void ) +{ + delete m_behavior; + m_behavior = new Behavior< CMerasmus >( new CMerasmusBehavior ); +} + +void CMerasmusIntention::Update( void ) +{ + m_behavior->Update( static_cast< CMerasmus * >( GetBot() ), GetUpdateInterval() ); +} + +// is this a place we can be? +QueryResultType CMerasmusIntention::IsPositionAllowed( const INextBot *meBot, const Vector &pos ) const +{ + return ANSWER_YES; +} + + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +void CMerasmusLocomotion::Update( void ) +{ + CMerasmus *me = (CMerasmus *)GetBot()->GetEntity(); + + if ( me->IsFlying() ) + { + // don't update this locomotor since the flying locomotor is active + return; + } + + NextBotGroundLocomotion::Update(); +} + +float CMerasmusLocomotion::GetRunSpeed( void ) const +{ + return tf_merasmus_speed.GetFloat(); +} + + +//--------------------------------------------------------------------------------------------- +// if delta Z is greater than this, we have to jump to get up +float CMerasmusLocomotion::GetStepHeight( void ) const +{ + return 18.0f; +} + + +//--------------------------------------------------------------------------------------------- +// return maximum height of a jump +float CMerasmusLocomotion::GetMaxJumpHeight( void ) const +{ + return 18.0f; +} + + +//--------------------------------------------------------------------------------------------- +// Return max rate of yaw rotation +float CMerasmusLocomotion::GetMaxYawRate( void ) const +{ + return 200.0f; +} + + +//--------------------------------------------------------------------------------------------- +bool CMerasmusLocomotion::ShouldCollideWith( const CBaseEntity *object ) const +{ + if ( !object ) + return false; + + // Don't collide with players + return object->IsPlayer() ? false : true; +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +#define MERASMUS_ACCELERATION 250.0f //500.0f + +CMerasmusFlyingLocomotion::CMerasmusFlyingLocomotion( INextBot *bot ) : ILocomotion( bot ) +{ + Reset(); +} + + +//--------------------------------------------------------------------------------------------- +CMerasmusFlyingLocomotion::~CMerasmusFlyingLocomotion() +{ +} + + +//--------------------------------------------------------------------------------------------- +// (EXTEND) reset to initial state +void CMerasmusFlyingLocomotion::Reset( void ) +{ + m_velocity = vec3_origin; + m_acceleration = vec3_origin; + m_currentSpeed = 0.0f; + m_forward = vec3_origin; + m_desiredAltitude = 50.0f; +} + + +//--------------------------------------------------------------------------------------------- +void CMerasmusFlyingLocomotion::MaintainAltitude( void ) +{ + CBaseCombatCharacter *me = GetBot()->GetEntity(); + + float groundZ; + TheNavMesh->GetSimpleGroundHeight( me->GetAbsOrigin(), &groundZ ); + + float currentAltitude = me->GetAbsOrigin().z - groundZ; + float error = m_desiredAltitude - currentAltitude; + float accelZ = clamp( error, -MERASMUS_ACCELERATION, MERASMUS_ACCELERATION ); + + m_acceleration.z += accelZ; +} + + +//--------------------------------------------------------------------------------------------- +// (EXTEND) update internal state +void CMerasmusFlyingLocomotion::Update( void ) +{ + CMerasmus *me = (CMerasmus *)GetBot()->GetEntity(); + const float deltaT = GetUpdateInterval(); + + if ( !me->IsFlying() ) + { + // not flying - let the other locomotor run + return; + } + + Vector pos = me->GetAbsOrigin(); + + // always maintain altitude, even if not trying to move (ie: no Approach call) + MaintainAltitude(); + + m_forward = m_velocity; + m_currentSpeed = m_forward.NormalizeInPlace(); + + Vector damping( 1.0f, 1.0f, 1.0f ); + Vector totalAccel = m_acceleration - m_velocity * damping; + + m_velocity += totalAccel * deltaT; + me->SetAbsVelocity( m_velocity ); + + pos += m_velocity * deltaT; + + // Merasmus doesn't collide with players and floats between valid nav areas + // so skip the collision checking + GetBot()->GetEntity()->SetAbsOrigin( pos ); + m_acceleration = vec3_origin; +} + + +//--------------------------------------------------------------------------------------------- +// (EXTEND) move directly towards the given position +void CMerasmusFlyingLocomotion::Approach( const Vector &goalPos, float goalWeight ) +{ + Vector flyGoal = goalPos; + flyGoal.z += m_desiredAltitude; + + Vector toGoal = flyGoal - GetBot()->GetEntity()->GetAbsOrigin(); + // altitude is handled in Update() + toGoal.z = 0.0f; + toGoal.NormalizeInPlace(); + + m_acceleration += MERASMUS_ACCELERATION * toGoal; +} + + +//--------------------------------------------------------------------------------------------- +float CMerasmusFlyingLocomotion::GetDesiredSpeed( void ) const +{ + return tf_merasmus_speed.GetFloat(); +} + + +//--------------------------------------------------------------------------------------------- +void CMerasmusFlyingLocomotion::SetDesiredAltitude( float height ) +{ + m_desiredAltitude = height; +} + + +//--------------------------------------------------------------------------------------------- +float CMerasmusFlyingLocomotion::GetDesiredAltitude( void ) const +{ + return m_desiredAltitude; +} + +//--------------------------------------------------------------------------------------------- +bool CMerasmusFlyingLocomotion::ShouldCollideWith( const CBaseEntity *object ) const +{ + if ( !object ) + return false; + + // Don't collide with players + return object->IsPlayer() ? false : true; +} + +//--------------------------------------------------------------------------------------------- +void CMerasmusFlyingLocomotion::FaceTowards( const Vector &target ) +{ + CMerasmus *me = (CMerasmus *)GetBot()->GetEntity(); + const float deltaT = GetUpdateInterval(); + + QAngle angles = me->GetLocalAngles(); + + float desiredYaw = UTIL_VecToYaw( target - GetFeet() ); + + float angleDiff = UTIL_AngleDiff( desiredYaw, angles.y ); + + const float maxYawRate = 100.0f; + float deltaYaw = maxYawRate * deltaT; + + if ( angleDiff < -deltaYaw ) + { + angles.y -= deltaYaw; + } + else if ( angleDiff > deltaYaw ) + { + angles.y += deltaYaw; + } + else + { + angles.y += angleDiff; + } + + me->SetLocalAngles( angles ); +} + + + diff --git a/game/server/tf/halloween/merasmus/merasmus.h b/game/server/tf/halloween/merasmus/merasmus.h new file mode 100644 index 0000000..2b3f4ea --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus.h @@ -0,0 +1,412 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef MERASMUS_H +#define MERASMUS_H + +#include "NextBot.h" +#include "NextBotBehavior.h" +#include "NextBotGroundLocomotion.h" +#include "merasmus_body.h" +#include "Path/NextBotPathFollow.h" +#include "../halloween_base_boss.h" + + +extern ConVar tf_merasmus_health_base; +extern ConVar tf_merasmus_health_per_player; +extern ConVar tf_merasmus_min_player_count; + +extern ConVar tf_merasmus_speed; +extern ConVar tf_merasmus_attack_range; +extern ConVar tf_merasmus_speed_recovery_rate; +extern ConVar tf_merasmus_speed_penalty; +extern ConVar tf_merasmus_chase_duration; +extern ConVar tf_merasmus_chase_range; + +extern ConVar tf_merasmus_health_regen_rate; + +extern ConVar tf_merasmus_bomb_head_duration; +extern ConVar tf_merasmus_bomb_head_per_team; + +class CTFPlayer; +class CWheelOfDoom; +class CMerasmus; +class CTFWeaponBaseGrenadeProj; +class CTFMerasmusTrickOrTreatProp; +class CMonsterResource; + +//---------------------------------------------------------------------------- +class CMerasmusSWStats +{ +public: + + void ResetStats () + { + V_memset( m_arrClassDamage, 0, sizeof( m_arrClassDamage ) ); + m_flPropHuntTime1 = 0; + m_flPropHuntTime2 = 0; + m_flLifeTime = 0; + m_nBombKills = 0; + m_nStaffKills = 0; + m_nPvpKills = 0; + } + + int m_arrClassDamage[ TF_LAST_NORMAL_CLASS ]; + float m_flPropHuntTime1; + float m_flPropHuntTime2; + float m_flLifeTime; + int m_nBombKills; + int m_nStaffKills; + int m_nPvpKills; +}; + +//---------------------------------------------------------------------------- +class CMerasmusLocomotion : public NextBotGroundLocomotion +{ +public: + CMerasmusLocomotion( INextBot *bot ) : NextBotGroundLocomotion( bot ) { } + virtual ~CMerasmusLocomotion() { } + + virtual void Update( void ); // (EXTEND) update internal state + + virtual float GetRunSpeed( void ) const; // get maximum running speed + virtual float GetStepHeight( void ) const; // if delta Z is greater than this, we have to jump to get up + virtual float GetMaxJumpHeight( void ) const; // return maximum height of a jump + + /** + * Should we collide with this entity? + */ + virtual bool ShouldCollideWith( const CBaseEntity *object ) const; + +private: + virtual float GetMaxYawRate( void ) const; // return max rate of yaw rotation +}; + + +//---------------------------------------------------------------------------- +class CMerasmusFlyingLocomotion : public ILocomotion +{ +public: + CMerasmusFlyingLocomotion( INextBot *bot ); + virtual ~CMerasmusFlyingLocomotion(); + + virtual void Reset( void ); // (EXTEND) reset to initial state + virtual void Update( void ); // (EXTEND) update internal state + + virtual void Approach( const Vector &goalPos, float goalWeight = 1.0f ); // (EXTEND) move directly towards the given position + + virtual float GetDesiredSpeed( void ) const; // returns the current desired speed + + virtual void SetDesiredAltitude( float height ); // how high above our Approach goal do we float? + virtual float GetDesiredAltitude( void ) const; + + virtual const Vector &GetGroundNormal( void ) const; // surface normal of the ground we are in contact with + + virtual const Vector &GetVelocity( void ) const; // return current world space velocity + void SetVelocity( const Vector &velocity ); + + virtual bool ShouldCollideWith( const CBaseEntity *object ) const; + + virtual void FaceTowards( const Vector &target ); // rotate body to face towards "target" + +protected: + float m_currentSpeed; + Vector m_forward; + + float m_desiredAltitude; + void MaintainAltitude( void ); + + Vector m_velocity; + Vector m_acceleration; +}; + +inline const Vector &CMerasmusFlyingLocomotion::GetGroundNormal( void ) const +{ + static Vector up( 0, 0, 1.0f ); + + return up; +} + +inline const Vector &CMerasmusFlyingLocomotion::GetVelocity( void ) const +{ + return m_velocity; +} + +inline void CMerasmusFlyingLocomotion::SetVelocity( const Vector &velocity ) +{ + m_velocity = velocity; +} + + +//---------------------------------------------------------------------------- +class CMerasmusIntention : public IIntention +{ +public: + CMerasmusIntention( CMerasmus *me ); + virtual ~CMerasmusIntention(); + + virtual void Reset( void ); + virtual void Update( void ); + + virtual QueryResultType IsPositionAllowed( const INextBot *me, const Vector &pos ) const; // is the a place we can be? + + virtual INextBotEventResponder *FirstContainedResponder( void ) const { return m_behavior; } + virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const { return NULL; } + +private: + Behavior< CMerasmus > *m_behavior; +}; + +DECLARE_AUTO_LIST( IMerasmusAutoList ); + +//---------------------------------------------------------------------------- +class CMerasmus : public CHalloweenBaseBoss, public CGameEventListener, public IMerasmusAutoList +{ +public: + DECLARE_CLASS( CMerasmus, CHalloweenBaseBoss ); + DECLARE_SERVERCLASS(); + + CMerasmus(); + virtual ~CMerasmus(); + + static void PrecacheMerasmus(); + virtual void Precache(); + virtual void Spawn( void ); + virtual void UpdateOnRemove(); + + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + + // CGameEventListener + virtual void FireGameEvent( IGameEvent *event ); + + // INextBot + virtual CMerasmusIntention *GetIntentionInterface( void ) const { return m_intention; } + virtual ILocomotion *GetLocomotionInterface( void ) const { if ( m_isFlying ) return m_flyingLocomotor; return m_locomotor; } + virtual CMerasmusBody *GetBodyInterface( void ) const { return m_body; } + + virtual void Update( void ); + + const Vector &GetHomePosition( void ) const; + + Vector GetCastPosition() const; + + bool IsRevealed() const { return m_bRevealed; } + void OnRevealed(bool bPlaySound = true); + bool ShouldReveal() const; + bool IsNextKilledPropMerasmus() const; + void SetRevealer( CTFPlayer* pPlayer ) { m_hMerasmusRevealer = pPlayer; } + + void OnDisguise(); + bool ShouldDisguise() const; + + void StartAOEAttack() { m_bIsDoingAOEAttack = true; } + void StopAOEAttack() { m_bIsDoingAOEAttack = false; } + bool IsDoingAOEAttack() const { return m_bIsDoingAOEAttack; } + + static CTFWeaponBaseGrenadeProj* CreateMerasmusGrenade( const Vector& vPosition, const Vector& vVelocity, CBaseCombatCharacter* pOwner, float fScale = 1.0f ); + + static const char* GetRandomPropModelName(); + + void PushPlayer( CTFPlayer* pPlayer, float flPushForce ) const; + int GetBombHitCount() const { return m_nBombHitCount; } + void ResetBombHitCount() { m_nBombHitCount = 0; } + void AddStun( CTFPlayer* pPlayer ); + void OnBeginStun(); + void OnEndStun(); + bool HasStunTimer() const { return !m_stunTimer.IsElapsed(); } + bool IsStunned() const { return m_bStunned; } + + void AddFakeProp( CTFMerasmusTrickOrTreatProp* pFakeProp ); + void RemoveAllFakeProps(); + + void BombHeadMode(); + + bool ShouldLeave() const; + void LeaveWarning(); + void OnLeaveWhileInPropForm(); + + void TriggerLogicRelay( const char* pszLogicRelayName, bool bSpawn = false ); + + void StartFlying( void ) { m_isFlying = true; } + void StopFlying( void ) { m_isFlying = false; } + bool IsFlying( void ) const { return m_isFlying; } + bool IsHiding( void ) const { return m_isHiding; } + + void PlayLowPrioritySound( IRecipientFilter &filter, const char* pszSoundEntryName ); + void PlayHighPrioritySound( const char* pszSoundEntryName ); + + void GainLevel( void ); + void ResetLevel( void ); + static int GetMerasmusLevel() { return m_level; } + virtual int GetLevel( void ) const OVERRIDE; + static void DBG_SetLevel( int nLevel ); + + virtual HalloweenBossType GetBossType() const { return HALLOWEEN_BOSS_MERASMUS; } + + void StartRespawnTimer() const; + + const CUtlVector< CHandle<CTFPlayer> >& GetStartingAttackers() const; + + static bool Zap( CBaseCombatCharacter *pCaster, const char* pszCastingAttachmentName, float flSpellRange, float flMinDamage, float flMaxDamage, int nMaxTarget, int nTargetTeam = TEAM_ANY ); + + // Stats + void RecordDisguiseTime( ); + void SW_ReportMerasmusStats( void ); +private: + + + CMerasmusIntention *m_intention; + CMerasmusLocomotion *m_locomotor; + CMerasmusFlyingLocomotion *m_flyingLocomotor; + CMerasmusBody *m_body; + + bool m_isFlying; + + bool m_isHiding; + + CUtlVector< AttackerInfo > m_attackerVector; // list of everyone who injured me, and when + CUtlVector< CHandle<CTFPlayer> > m_startingAttackersVector; + + CountdownTimer m_stunTimer; + int m_nBombHitCount; + + Vector m_homePos; + int m_damagePoseParameter; + + int m_nRevealedHealth; + + CHandle< CWheelOfDoom > m_wheel; + + CHandle< CMonsterResource > m_hHealthBar; + + CNetworkVar( bool, m_bRevealed ); + CNetworkVar( bool, m_bIsDoingAOEAttack ); + CNetworkVar( bool, m_bStunned ); + + CSoundPatch *m_pIdleSound; + + CUtlVector< CHandle< CTFMerasmusTrickOrTreatProp > > m_fakePropVector; + + int m_nDestroyedPropsToReveal; + + SolidType_t m_solidType; + int m_solidFlags; + + CHandle< CTFPlayer > m_hMerasmusRevealer; + + CountdownTimer m_lifeTimer; + float m_flLastWarnTime; + + // For Stats + float m_flStartDisguiseTime; + CMerasmusSWStats m_bossStats; + + static int m_level; +}; + + +inline int CMerasmus::GetLevel( void ) const +{ + return m_level; +} + +inline void CMerasmus::GainLevel( void ) +{ + ++m_level; +} + +inline void CMerasmus::ResetLevel( void ) +{ + m_level = 1; +} + +inline void CMerasmus::DBG_SetLevel( int nLevel ) +{ + m_level = nLevel; +} + +inline const Vector &CMerasmus::GetHomePosition( void ) const +{ + return m_homePos; +} + +inline const CUtlVector< CHandle<CTFPlayer> >& CMerasmus::GetStartingAttackers() const +{ + return m_startingAttackersVector; +} + +//-------------------------------------------------------------------------------------------------------------- +class CMerasmusPathCost : public IPathCost +{ +public: + CMerasmusPathCost( CMerasmus *me ) + { + m_me = me; + } + + // return the cost (weighted distance between) of moving from "fromArea" to "area", or -1 if the move is not allowed + virtual float operator()( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) const + { + if ( fromArea == NULL ) + { + // first area in path, no cost + return 0.0f; + } + else + { + if ( !m_me->GetLocomotionInterface()->IsAreaTraversable( area ) ) + { + // our locomotor says we can't move here + return -1.0f; + } + + // compute distance traveled along path so far + float dist; + + if ( ladder ) + { + dist = ladder->m_length; + } + else if ( length > 0.0 ) + { + // optimization to avoid recomputing length + dist = length; + } + else + { + dist = ( area->GetCenter() - fromArea->GetCenter() ).Length(); + } + + float cost = dist + fromArea->GetCostSoFar(); + + // check height change + float deltaZ = fromArea->ComputeAdjacentConnectionHeightChange( area ); + if ( deltaZ >= m_me->GetLocomotionInterface()->GetStepHeight() ) + { + if ( deltaZ >= m_me->GetLocomotionInterface()->GetMaxJumpHeight() ) + { + // too high to reach + return -1.0f; + } + + // jumping is slower than flat ground + const float jumpPenalty = 5.0f; + cost += jumpPenalty * dist; + } + else if ( deltaZ < -m_me->GetLocomotionInterface()->GetDeathDropHeight() ) + { + // too far to drop + return -1.0f; + } + + return cost; + } + } + + CMerasmus *m_me; +}; + + +#endif // MERASMUS_H diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_aoe_attack.cpp b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_aoe_attack.cpp new file mode 100644 index 0000000..6698be0 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_aoe_attack.cpp @@ -0,0 +1,248 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" + +#include "tf_gamerules.h" +#include "particle_parse.h" +#include "nav_mesh/tf_nav_area.h" +#include "nav_mesh/tf_nav_mesh.h" +#include "tf/halloween/eyeball_boss/teleport_vortex.h" +#include "tf_weaponbase_grenadeproj.h" +#include "sceneentity.h" + +#include "../merasmus.h" +#include "merasmus_aoe_attack.h" + +//--------------------------------------------------------------------------------------------- +#define MAX_BOMBS_PER_TICK 4 + +//--------------------------------------------------------------------------------------------- +ActionResult< CMerasmus > CMerasmusAOEAttack::OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ) +{ + m_aoeStartTimer.Start( 4.f ); + m_state = AOE_BEGIN; + + me->StartFlying(); + + CTFNavArea *pointArea = TheTFNavMesh()->GetControlPointCenterArea( 0 ); + + if ( !pointArea ) + { + return Done( "No control point!" ); + } + + const float surroundRange = 400.f; + CollectSurroundingAreas( &m_wanderAreaVector, pointArea, surroundRange, StepHeight, StepHeight ); + + if ( m_wanderAreaVector.Count() == 0 ) + { + return Done( "No nav areas near control point!" ); + } + + me->GetBodyInterface()->StartActivity( ACT_RANGE_ATTACK1 ); + + // This is only to play sound since animation doesn't work with the vcd + CFmtStr vcdName( "scenes/bot/merasmus/low/bomb_attack_00%d.vcd", RandomInt( 1, 9 ) ); + InstancedScriptedScene( me, vcdName.Get(), NULL, 0.0f, false, NULL, true ); + + m_wanderArea = NULL; + + return Continue(); +} + + +#define BOMB_SCALE 1.0f // 0.85f +#define BOMB_VERT_VEL 750.0f +#define BOMB_START_OFFSET Vector( 0.0f, 0.0f, 150.0f ) + +//--------------------------------------------------------------------------------------------- +void CMerasmusAOEAttack::QueueSingleGrenadeForLaunch( const Vector &vecVelocity ) +{ + m_vecGrenadesToCreate.AddToTail( MerasmusGrenadeCreateSpec_t( vecVelocity ) ); +} + +//--------------------------------------------------------------------------------------------- +void CMerasmusAOEAttack::ClearPendingGrenades() +{ + m_vecGrenadesToCreate.RemoveAll(); +} + +//--------------------------------------------------------------------------------------------- +void CMerasmusAOEAttack::LaunchPendingGrenades( CMerasmus *me ) +{ + int nNumGrenadesLeftToCreate = MIN( m_vecGrenadesToCreate.Count(), MAX_BOMBS_PER_TICK ); + while ( nNumGrenadesLeftToCreate > 0 ) + { + // Create the first one in the list + MerasmusGrenadeCreateSpec_t &info = m_vecGrenadesToCreate[0]; + CMerasmus::CreateMerasmusGrenade( me->WorldSpaceCenter() + BOMB_START_OFFSET, info.m_vecVelocity, me, BOMB_SCALE ); + + // Remove the first one in the list + m_vecGrenadesToCreate.Remove( 0 ); + + --nNumGrenadesLeftToCreate; + } +} + +//--------------------------------------------------------------------------------------------- +void CMerasmusAOEAttack::QueueBombRingsForLaunch( CMerasmus *me ) +{ + ClearPendingGrenades(); + + const float bombRingMinHorizVel = 100.0f; + const float bombRingMaxHorizVel = 2000.0f; + + QAngle myAngles = me->EyeAngles(); + + float deltaVel = bombRingMaxHorizVel - bombRingMinHorizVel; + const int ringCount = 2; + + for( int r=0; r<ringCount; ++r ) + { + float u = (float)(r+1)/(float)ringCount; + + float horizVel = bombRingMinHorizVel + u * deltaVel; + +// float angleDelta = 10.0f + 20.0f * ( 1.0f - u ); + float angleDelta = 20.0f + 30.0f * ( 1.0f - u ); + + for( float angle=0.0f; angle<360.0f; angle += angleDelta ) + { + Vector forward; + AngleVectors( myAngles, &forward ); + + Vector vecVelocity( horizVel * forward.x, horizVel * forward.y, BOMB_VERT_VEL ); + + QueueSingleGrenadeForLaunch( vecVelocity ); + + myAngles.y += angleDelta; + } + } +} + + +//--------------------------------------------------------------------------------------------- +void CMerasmusAOEAttack::QueueBombSpokesForLaunch( CMerasmus *me ) +{ + ClearPendingGrenades(); + + const float bombSpokeAngle = 45.0f; + const int bombSpokeCount = 4; // 10; + const float bombSpokeMinHorizVel = 100.0f; + const float bombSpokeMaxHorizVel = 2000.0f; + + float deltaVel = bombSpokeMaxHorizVel - bombSpokeMinHorizVel; + float angleDelta = bombSpokeAngle; + + QAngle myAngles = me->EyeAngles(); + + for( float angle=0.0f; angle<360.0f; angle += angleDelta ) + { + Vector forward; + AngleVectors( myAngles, &forward ); + + int spokeCount = bombSpokeCount; + + for( int i=0; i<spokeCount; ++i ) + { + float u = (float)(i+1)/(float)spokeCount; + + float horizVel = bombSpokeMinHorizVel + u * deltaVel; + + Vector vecVelocity( horizVel * forward.x, horizVel * forward.y, BOMB_VERT_VEL ); + + QueueSingleGrenadeForLaunch( vecVelocity ); + } + + myAngles.y += angleDelta; + } +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CMerasmus > CMerasmusAOEAttack::Update( CMerasmus *me, float interval ) +{ + if ( me->IsActivityFinished() ) + { + me->StopAOEAttack(); + + return Done(); + } + + switch ( m_state ) + { + case AOE_BEGIN: + { + if ( m_aoeStartTimer.IsElapsed() ) + { + m_launchTimer.Start( 0.5f ); + + m_state = AOE_FIRING; + } + } + break; + case AOE_FIRING: + { + // Start the AOE particles + me->StartAOEAttack(); + + CMerasmusFlyingLocomotion *fly = (CMerasmusFlyingLocomotion *)me->GetLocomotionInterface(); + + // float up and down a bit + fly->SetDesiredAltitude( 175.0f + 25.0f * FastCos( gpGlobals->curtime ) ); + + if ( m_launchTimer.IsElapsed() ) + { + if ( RandomInt( 1, 100 ) < 50 ) + { + QueueBombSpokesForLaunch( me ); + } + else + { + QueueBombRingsForLaunch( me ); + } + + m_launchTimer.Start( 2.0f ); + } + + // Go through and launch any pending grenades + LaunchPendingGrenades( me ); + + // wander among nav areas near the cap point + if ( m_wanderTimer.IsElapsed() || m_wanderArea == NULL ) + { + m_wanderTimer.Start( RandomFloat( 1.0f, 3.0f ) ); + + m_wanderArea = (CTFNavArea *)m_wanderAreaVector[ RandomInt( 0, m_wanderAreaVector.Count()-1 ) ]; + } + + if ( m_wanderArea ) + { + Vector flySpot = m_wanderArea->GetCenter(); + flySpot.z = me->GetAbsOrigin().z; + + me->GetLocomotionInterface()->Approach( flySpot ); + me->GetLocomotionInterface()->FaceTowards( flySpot ); + } + } + break; + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CMerasmusAOEAttack::OnEnd( CMerasmus *me, Action< CMerasmus > *nextAction ) +{ + me->StopFlying(); + + // The animation sometime doesn't turn off the bodygroup correctly. Slam it in code. + int staffBodyGroup = me->FindBodygroupByName( "staff" ); + me->SetBodygroup( staffBodyGroup, 0 ); + + me->ResetBombHitCount(); +} diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_aoe_attack.h b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_aoe_attack.h new file mode 100644 index 0000000..76c04cf --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_aoe_attack.h @@ -0,0 +1,51 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef MERASMUS_AOE_ATTACK_H +#define MERASMUS_AOE_ATTACK_H + +class CMerasmusAOEAttack : public Action< CMerasmus > +{ +public: + virtual ActionResult< CMerasmus > OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ); + virtual ActionResult< CMerasmus > Update( CMerasmus *me, float interval ); + virtual void OnEnd( CMerasmus *me, Action< CMerasmus > *nextAction ); + + virtual const char *GetName( void ) const { return "AOE Attack!"; } // return name of this action +private: + enum AOEState_t + { + AOE_BEGIN, + AOE_FIRING, + }; + AOEState_t m_state; + + CountdownTimer m_aoeStartTimer; + CountdownTimer m_launchTimer; + CountdownTimer m_flyTimer; + CUtlVector< CNavArea * > m_wanderAreaVector; + CountdownTimer m_wanderTimer; + CTFNavArea *m_wanderArea; + + // To save network perf, we don't create all bombs in a single tick. Rather, we fill up a queue of bombs and distribute the creation over time. + // I originally had another property (start position) as part of MerasmusGrenadeCreateSpec_t, but it didn't make sense, since we want to use + // his current position when we actually create -- not the position he was at whenever we actually filled the queue with grenades. I'm leaving + // the struct here, rather than making m_vecGrenadesToCreate a CUtlVector< Vector >, in case we want to add anything else. + struct MerasmusGrenadeCreateSpec_t + { + MerasmusGrenadeCreateSpec_t( const Vector &v ) : m_vecVelocity( v ) {} + Vector m_vecVelocity; + }; + CUtlVector< MerasmusGrenadeCreateSpec_t > m_vecGrenadesToCreate; + + void QueueSingleGrenadeForLaunch( const Vector &vecVelocity ); // Don't call directly - call QueueBombRingsForLaunch() or QueueBombSpokesForLaunch() + void ClearPendingGrenades(); + void LaunchPendingGrenades( CMerasmus *me ); + + void QueueBombRingsForLaunch( CMerasmus *me ); + void QueueBombSpokesForLaunch( CMerasmus *me ); +}; + +#endif // MERASMUS_TELEPORT_AOE_ATTACK_H diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_attack.cpp b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_attack.cpp new file mode 100644 index 0000000..9a40377 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_attack.cpp @@ -0,0 +1,386 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" + +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "nav_mesh/tf_nav_area.h" +#include "particle_parse.h" +#include "team_control_point_master.h" + +#include "../merasmus.h" +#include "../merasmus_trick_or_treat_prop.h" +#include "merasmus_attack.h" +#include "merasmus_staff_attack.h" +#include "merasmus_stunned.h" +#include "merasmus_throwing_grenade.h" +#include "merasmus_zap.h" + +//---------------------------------------------------------------------------------- +ActionResult< CMerasmus > CMerasmusAttack::OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ) +{ + // smooth out the bot's path following by moving toward a point farther down the path + m_path.SetMinLookAheadDistance( 100.0f ); + + // start animation + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_ITEM1 ); + + m_attackTimer.Invalidate(); + + m_attackTarget = NULL; + m_attackTargetFocusTimer.Start( 0.f ); + + RandomGrenadeTimer(); + RandomZapTimer(); + + m_homePos = me->GetAbsOrigin(); + m_homePosRecalcTimer.Start( 3.0f ); + + m_bombHeadTimer.Start( 10.f ); + + return Continue(); +} + + +//---------------------------------------------------------------------------------- +bool CMerasmusAttack::IsPotentiallyChaseable( CMerasmus *me, CTFPlayer *victim ) +{ + if ( !victim ) + { + return false; + } + + if ( !victim->IsAlive() ) + { + // victim is dead - pick a new one + return false; + } + + CTFNavArea *victimArea = (CTFNavArea *)victim->GetLastKnownArea(); + if ( !victimArea || victimArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED ) ) + { + // unreachable - pick a new victim + return false; + } + + if ( victim->GetGroundEntity() != NULL ) + { + Vector victimAreaPos; + victimArea->GetClosestPointOnArea( victim->GetAbsOrigin(), &victimAreaPos ); + if ( ( victim->GetAbsOrigin() - victimAreaPos ).AsVector2D().IsLengthGreaterThan( 50.0f ) ) + { + // off the mesh and unreachable - pick a new victim + return false; + } + } + + if ( victim->m_Shared.IsInvulnerable() ) + { + // invulnerable - pick a new victim + return false; + } + + Vector toHome = m_homePos - victim->GetAbsOrigin(); + if ( toHome.IsLengthGreaterThan( tf_merasmus_chase_range.GetFloat() ) ) + { + // too far from home - pick a new victim + return false; + } + + return true; +} + + +//---------------------------------------------------------------------------------- +void CMerasmusAttack::SelectVictim( CMerasmus *me ) +{ + if ( IsPotentiallyChaseable( me, m_attackTarget ) && !m_attackTargetFocusTimer.IsElapsed() ) + { + return; + } + + // pick a new victim to chase + CTFPlayer *newVictim = NULL; + CUtlVector< CTFPlayer * > playerVector; + + // collect everyone + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + float victimRangeSq = FLT_MAX; + for( int i=0; i<playerVector.Count(); ++i ) + { + if ( !IsPotentiallyChaseable( me, playerVector[i] ) ) + { + continue; + } + + float rangeSq = me->GetRangeSquaredTo( playerVector[i] ); + if ( rangeSq < victimRangeSq ) + { + newVictim = playerVector[i]; + victimRangeSq = rangeSq; + } + } + + if ( newVictim ) + { + // we have a new victim + m_attackTargetFocusTimer.Start( tf_merasmus_chase_duration.GetFloat() ); + } + + m_attackTarget = newVictim; +} + + +//---------------------------------------------------------------------------------- +ActionResult< CMerasmus > CMerasmusAttack::Update( CMerasmus *me, float interval ) +{ + if ( !me->IsAlive() ) + { + return Done(); + } + + if ( me->HasStunTimer() ) + { + return SuspendFor( new CMerasmusStunned, "Stunned!" ); + } + + SelectVictim( me ); + RecomputeHomePosition(); + + if ( m_attackTarget == NULL ) + { + // go home + const float atHomeRange = 50.0f; + if ( me->IsRangeGreaterThan( m_homePos, atHomeRange ) ) + { + if ( m_path.GetAge() > 3.0f ) + { + CMerasmusPathCost cost( me ); + m_path.Compute( me, m_homePos, cost ); + } + + m_path.Update( me ); + } +// else +// { +// // at home with nothing to do - taunt! +// if ( !me->IsMoving() && m_tauntTimer.IsElapsed() ) +// { +// m_tauntTimer.Start( RandomFloat( 3.0f, 5.0f ) ); +// +// return SuspendFor( new CMerasmusTaunt, "Taunting because I have nothing to do." ); +// } +// } + } + else + { + // chase after our chase victim + const float standAndSwingRange = 100.0f; + CTFPlayer *chaseVictim = m_attackTarget; + + if ( me->IsRangeGreaterThan( chaseVictim, standAndSwingRange ) || !me->IsLineOfSightClear( chaseVictim ) ) + { + if ( m_path.GetAge() > 1.0f ) + { + CMerasmusPathCost cost( me ); + m_path.Compute( me, chaseVictim, cost ); + } + + m_path.Update( me ); + } + } + + if ( m_bombHeadTimer.IsElapsed() ) + { + // bomb heads last 15 seconds - make sure we don't add more while existing ones are out + m_bombHeadTimer.Start( 16.0f ); + me->BombHeadMode(); + } + + // swing our axe at our attack target if they are in range + if ( m_attackTarget != NULL && m_attackTarget->IsAlive() ) + { + if ( m_zapTimer.IsElapsed() ) + { + RandomZapTimer(); + return SuspendFor( new CMerasmusZap, "Zap!" ); + } + + if ( me->IsRangeLessThan( m_attackTarget, tf_merasmus_attack_range.GetFloat() ) ) + { + if ( m_attackTimer.IsElapsed() ) + { + m_attackTimer.Start( 1.f ); + return SuspendFor( new CMerasmusStaffAttack( m_attackTarget ), "Whack!" ); + } + + me->GetLocomotionInterface()->FaceTowards( m_attackTarget->WorldSpaceCenter() ); + } + + if ( m_grenadeTimer.IsElapsed() ) + { + RandomGrenadeTimer(); + return SuspendFor( new CMerasmusThrowingGrenade( m_attackTarget ), "Fire in the hole!" ); + } + } + + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_RUN_MELEE ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE ); + } + + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CMerasmus > CMerasmusAttack::OnStuck( CMerasmus *me ) +{ + // we're stuck - just warp to the our next path goal + if ( m_path.GetCurrentGoal() ) + { + me->SetAbsOrigin( m_path.GetCurrentGoal()->pos + Vector( 0, 0, 10.0f ) ); + } + + return TryContinue( RESULT_TRY ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CMerasmus > CMerasmusAttack::OnContact( CMerasmus *me, CBaseEntity *other, CGameTrace *result ) +{ + if ( other->IsPlayer() ) + { + CTFPlayer *pTFPlayer = ToTFPlayer( other ); + if ( pTFPlayer ) + { + if ( pTFPlayer->IsAlive() ) + { + // force attack the thing we bumped into + // this prevents us from being stuck on dispensers, for example + m_attackTarget = pTFPlayer; + m_attackTargetFocusTimer.Start( tf_merasmus_chase_duration.GetFloat() ); + } + } + } + + return TryContinue( RESULT_TRY ); +} + + +//--------------------------------------------------------------------------------------------- +void CMerasmusAttack::RecomputeHomePosition( void ) +{ + if ( !m_homePosRecalcTimer.IsElapsed() ) + { + return; + } + + m_homePosRecalcTimer.Reset(); + + CTeamControlPoint *contestedPoint = NULL; + CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + if ( pMaster ) + { + for( int i=0; i<pMaster->GetNumPoints(); ++i ) + { + contestedPoint = pMaster->GetControlPoint( i ); + if ( contestedPoint && pMaster->IsInRound( contestedPoint ) ) + { + if ( ObjectiveResource()->GetOwningTeam( contestedPoint->GetPointIndex() ) == TF_TEAM_BLUE ) + continue; + + // blue are the invaders + if ( !TeamplayGameRules()->TeamMayCapturePoint( TF_TEAM_BLUE, contestedPoint->GetPointIndex() ) ) + continue; + + break; + } + } + } + + if ( contestedPoint ) + { + m_homePos = contestedPoint->GetAbsOrigin(); + } +} + + +void CMerasmusAttack::RandomGrenadeTimer() +{ + m_grenadeTimer.Start( RandomFloat( 2.f, 3.f ) ); +} + + +void CMerasmusAttack::RandomZapTimer() +{ + m_zapTimer.Start( RandomFloat( 3.f, 4.f ) ); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +ActionResult< CMerasmus > CMerasmusTaunt::OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ) +{ + m_timer.Start( 3.0f ); + + const char *taunts[] = + { + "gesture_melee_cheer", + "gesture_melee_go", + "taunt01", // wave + "taunt06", // thriller + "taunt_laugh", + NULL + }; + + // count the available taunts + int count = 0; + while( true ) + { + if ( taunts[ count ] == NULL ) + break; + + ++count; + } + + // pick one and play it + int which = RandomInt( 0, count-1 ); + me->AddGestureSequence( me->LookupSequence( taunts[ which ] ) ); + + int staffBodyGroup = me->FindBodygroupByName( "staff" ); + me->SetBodygroup( staffBodyGroup, 1 ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CMerasmus > CMerasmusTaunt::Update( CMerasmus *me, float interval ) +{ + if ( m_timer.IsElapsed() ) + { + return Done(); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CMerasmusTaunt::OnEnd( CMerasmus *me, Action< CMerasmus > *nextAction ) +{ + // turn the staff back on + int staffBodyGroup = me->FindBodygroupByName( "staff" ); + me->SetBodygroup( staffBodyGroup, 0 ); +} + + + diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_attack.h b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_attack.h new file mode 100644 index 0000000..dc19cdf --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_attack.h @@ -0,0 +1,63 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef MERASMUS_ATTACK_H +#define MERASMUS_ATTACK_H + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CMerasmusAttack : public Action< CMerasmus > +{ +public: + virtual ActionResult< CMerasmus > OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ); + virtual ActionResult< CMerasmus > Update( CMerasmus *me, float interval ); + + virtual EventDesiredResult< CMerasmus > OnStuck( CMerasmus *me ); + virtual EventDesiredResult< CMerasmus > OnContact( CMerasmus *me, CBaseEntity *other, CGameTrace *result = NULL ); + + virtual const char *GetName( void ) const { return "Attack"; } // return name of this action + +private: + PathFollower m_path; + + Vector m_homePos; + CountdownTimer m_homePosRecalcTimer; + void RecomputeHomePosition( void ); + + CountdownTimer m_attackTimer; + + CountdownTimer m_grenadeTimer; + void RandomGrenadeTimer(); + + CountdownTimer m_zapTimer; + void RandomZapTimer(); + + CountdownTimer m_bombHeadTimer; + CountdownTimer m_tauntTimer; + + CHandle< CTFPlayer > m_attackTarget; // the victim I'm momentarily attacking + CountdownTimer m_attackTargetFocusTimer; + bool IsPotentiallyChaseable( CMerasmus *me, CTFPlayer *victim ); + void SelectVictim( CMerasmus *me ); +}; + + + +//--------------------------------------------------------------------------------------------- +class CMerasmusTaunt : public Action< CMerasmus > +{ +public: + virtual ActionResult< CMerasmus > OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ); + virtual ActionResult< CMerasmus > Update( CMerasmus *me, float interval ); + virtual void OnEnd( CMerasmus *me, Action< CMerasmus > *nextAction ); + + virtual const char *GetName( void ) const { return "Taunt"; } // return name of this action + +private: + CountdownTimer m_timer; +}; + + +#endif // MERASMUS_ATTACK_H diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_disguise.cpp b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_disguise.cpp new file mode 100644 index 0000000..063f959 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_disguise.cpp @@ -0,0 +1,226 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "nav_mesh/tf_nav_area.h" +#include "particle_parse.h" +#include "player_vs_environment/monster_resource.h" + +#include "../merasmus.h" +#include "../merasmus_trick_or_treat_prop.h" +#include "merasmus_disguise.h" +#include "merasmus_reveal.h" + +ConVar tf_merasmus_disguise_debug( "tf_merasmus_disguise_debug", "0", FCVAR_CHEAT ); + +//--------------------------------------------------------------------------------------------- +ActionResult< CMerasmus > CMerasmusDisguise::OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ) +{ + m_bSpawnedProps = false; + + TryToDisguiseSpawn( me ); + + m_flStartRegenTime = gpGlobals->curtime; + m_nStartRegenHealth = me->GetHealth(); + + me->PlayHighPrioritySound( "Halloween.MerasmusInitiateHiding" ); + RandomDisguiseTauntTimer(); + + m_findPropsFailTimer.Start( 3 ); + + // set boss inactive + g_pMonsterResource->SetBossState( 1 ); + + return Continue(); +} + + +//---------------------------------------------------------------------------------- +ActionResult< CMerasmus > CMerasmusDisguise::Update( CMerasmus *me, float interval ) +{ + if ( me->ShouldLeave() ) + { + return Done(); + } + me->LeaveWarning(); + + if ( !m_bSpawnedProps ) + { + if ( m_findPropsFailTimer.HasStarted() && m_findPropsFailTimer.IsElapsed() ) + { + // Couldn't find props in time - skip + return Done(); + } + + if ( !m_findSpawnPositionTime.IsElapsed() ) + { + // not ready yet + return Continue(); + } + + TryToDisguiseSpawn( me ); + + return Continue(); + } + + if ( m_disguiseTauntTimer.IsElapsed() ) + { + if (RandomInt(0,10) == 0) + { + me->PlayHighPrioritySound( "Halloween.MerasmusHiddenRare" ); + } + else + { + me->PlayHighPrioritySound( "Halloween.MerasmusHidden" ); + } + + RandomDisguiseTauntTimer(); + } + + // regen health while disguise + if ( me->GetHealth() < me->GetMaxHealth() ) + { + float flHealthRegenPerSec = tf_merasmus_health_regen_rate.GetFloat() * me->GetMaxHealth() * ( me->GetLevel() - 1 ); + int nNewHealth = MIN( ( gpGlobals->curtime - m_flStartRegenTime ) * flHealthRegenPerSec + m_nStartRegenHealth, me->GetMaxHealth() ); + me->SetHealth( nNewHealth ); + + // show Boss' health meter on HUD + if ( g_pMonsterResource ) + { + float healthPercentage = (float)me->GetHealth() / (float)me->GetMaxHealth(); + g_pMonsterResource->SetBossHealthPercentage( healthPercentage ); + } + } + + // should I come out from disguise? + if ( me->ShouldReveal() ) + { + return Done( "Revealed!" ); + } + + return Continue(); +} + + +void CMerasmusDisguise::OnEnd( CMerasmus *me, Action< CMerasmus > *nextAction ) +{ + if ( me->ShouldLeave() ) + { + me->OnLeaveWhileInPropForm(); + } + + // set boss active + g_pMonsterResource->SetBossState( 0 ); + + me->OnRevealed(); +} + + +QAngle GetRandomPropAngles( CTFNavArea* pArea ) +{ + Vector vNormal; + pArea->ComputeNormal( &vNormal ); + Vector vForward = pArea->GetRandomPoint() - pArea->GetCenter(); + QAngle qAngles; + VectorAngles( vForward, vNormal, qAngles ); + + return qAngles; +} + + +void CMerasmusDisguise::TryToDisguiseSpawn( CMerasmus *me ) +{ + m_findSpawnPositionTime.Start( 1 ); + + // face towards a nearby player + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + // pick a random spot + CUtlVector< CTFNavArea * > candidateAreaVector; + for( int i=0; i<TheNavAreas.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)TheNavAreas[i]; + + if ( !area->HasFuncNavPrefer() ) + { + // don't spawn outside nav prefer + continue; + } + + // don't use small nav areas + const float goodSize = 150.f; + if ( area->GetSizeX() < goodSize || area->GetSizeY() < goodSize ) + { + continue; + } + + // don't use area containing player + if ( area->GetPlayerCount( TF_TEAM_BLUE ) || area->GetPlayerCount( TF_TEAM_RED ) ) + { + continue; + } + + // don't use slope area +// Vector vNormal; +// area->ComputeNormal( &vNormal ); +// if ( vNormal.z < 0.9f ) +// { +// continue; +// } + + candidateAreaVector.AddToTail( area ); + } + + if ( candidateAreaVector.Count() == 0 ) + { + // no place to spawn (!) + return; + } + + // spread out the area + CUtlVector< CTFNavArea * > spawnAreaVector; + SelectSeparatedShuffleSet< CTFNavArea >( 10, 500.f, candidateAreaVector, &spawnAreaVector ); + + if ( spawnAreaVector.Count() == 0 ) + { + // no place to spawn (!) + return; + } + + if ( tf_merasmus_disguise_debug.GetBool() ) + { + for ( int i=0; i<spawnAreaVector.Count(); ++i ) + { + // draw all potential areas + spawnAreaVector[i]->DrawFilled( 0, 255, 0, 0, 30.f ); + } + } + + // spawn random props + int nRandomTrickOrTreatProps = spawnAreaVector.Count(); + for ( int i=0; i<nRandomTrickOrTreatProps; ++i ) + { + int propSpawnID = RandomInt( 0, spawnAreaVector.Count()-1 ); + + CTFMerasmusTrickOrTreatProp* pFakeProp = CTFMerasmusTrickOrTreatProp::Create( spawnAreaVector[ propSpawnID ]->GetCenter(), GetRandomPropAngles( spawnAreaVector[ propSpawnID ] ) ); + me->AddFakeProp( pFakeProp ); + + spawnAreaVector.FastRemove( propSpawnID ); + } + + me->OnDisguise(); + m_bSpawnedProps = true; +} + + +void CMerasmusDisguise::RandomDisguiseTauntTimer() +{ + m_disguiseTauntTimer.Start( RandomFloat( 10.f, 25.f ) ); +} diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_disguise.h b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_disguise.h new file mode 100644 index 0000000..bfed85f --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_disguise.h @@ -0,0 +1,33 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef MERASMUS_DISGUISE_H +#define MERASMUS_DISGUISE_H + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CMerasmusDisguise : public Action< CMerasmus > +{ +public: + virtual ActionResult< CMerasmus > OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ); + virtual ActionResult< CMerasmus > Update( CMerasmus *me, float interval ); + virtual void OnEnd( CMerasmus *me, Action< CMerasmus > *nextAction ); + virtual const char *GetName( void ) const { return "Disguise"; } // return name of this action + +private: + void TryToDisguiseSpawn( CMerasmus *me ); + CountdownTimer m_findPropsFailTimer; + CountdownTimer m_findSpawnPositionTime; + bool m_bSpawnedProps; + + void RandomDisguiseTauntTimer(); + CountdownTimer m_disguiseTauntTimer; + + float m_flStartRegenTime; + int m_nStartRegenHealth; +}; + + +#endif // MERASMUS_DISGUISE_H diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_dying.cpp b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_dying.cpp new file mode 100644 index 0000000..216ca92 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_dying.cpp @@ -0,0 +1,60 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" + +#include "particle_parse.h" +#include "tf_gamerules.h" + +#include "../merasmus.h" +#include "merasmus_dying.h" +#include "tf/halloween/eyeball_boss/teleport_vortex.h" + +//--------------------------------------------------------------------------------------------- +ActionResult< CMerasmus > CMerasmusDying::OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ) +{ + me->GetBodyInterface()->StartActivity( ACT_DIESIMPLE ); + me->PlayHighPrioritySound( "Halloween.MerasmusBanish" ); + TFGameRules()->BroadcastSound( 255, "Halloween.Merasmus_Death" ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CMerasmus > CMerasmusDying::Update( CMerasmus *me, float interval ) +{ + if ( me->IsActivityFinished() ) + { + me->Break(); + DispatchParticleEffect( "merasmus_spawn", me->GetAbsOrigin(), me->GetAbsAngles() ); + + IGameEvent *event = gameeventmanager->CreateEvent( "merasmus_killed" ); + if ( event ) + { + event->SetInt( "level", me->GetLevel() ); + gameeventmanager->FireEvent( event ); + } + me->TriggerLogicRelay( "boss_dead_relay" ); + + // create vortex to loot + CTeleportVortex *vortex = (CTeleportVortex *)CBaseEntity::Create( "teleport_vortex", me->WorldSpaceCenter(), vec3_angle ); + if ( vortex ) + { + vortex->SetupVortex( true, true ); + } + + me->GainLevel(); + + me->StartRespawnTimer(); + + UTIL_Remove( me ); + + return Done(); + } + + return Continue(); +} + diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_dying.h b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_dying.h new file mode 100644 index 0000000..97e888c --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_dying.h @@ -0,0 +1,20 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef MERASMUS_DYING_H +#define MERASMUS_DYING_H + + +//--------------------------------------------------------------------------------------------- +class CMerasmusDying : public Action< CMerasmus > +{ +public: + virtual ActionResult< CMerasmus > OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ); + virtual ActionResult< CMerasmus > Update( CMerasmus *me, float interval ); + virtual const char *GetName( void ) const { return "Dying"; } // return name of this action +}; + + +#endif // MERASMUS_DYING_H diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_reveal.cpp b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_reveal.cpp new file mode 100644 index 0000000..1dc7a62 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_reveal.cpp @@ -0,0 +1,38 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "nav_mesh/tf_nav_area.h" +#include "particle_parse.h" + +#include "../merasmus.h" +#include "merasmus_reveal.h" +#include "merasmus_attack.h" + + +//--------------------------------------------------------------------------------------------- +ActionResult< CMerasmus > CMerasmusReveal::OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ) +{ + me->OnRevealed(false); + + me->GetBodyInterface()->StartActivity( ACT_SHIELD_UP ); + + return Continue(); +} + + +//---------------------------------------------------------------------------------- +ActionResult< CMerasmus > CMerasmusReveal::Update( CMerasmus *me, float interval ) +{ + if ( me->IsActivityFinished() ) + { + return ChangeTo( new CMerasmusAttack, "Here I come!" ); + } + + return Continue(); +} diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_reveal.h b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_reveal.h new file mode 100644 index 0000000..de56951 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_reveal.h @@ -0,0 +1,20 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef MERASMUS_EMERGE_H +#define MERASMUS_EMERGE_H + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CMerasmusReveal : public Action< CMerasmus > +{ +public: + virtual ActionResult< CMerasmus > OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ); + virtual ActionResult< CMerasmus > Update( CMerasmus *me, float interval ); + virtual const char *GetName( void ) const { return "Reveal"; } // return name of this action +}; + + +#endif // MERASMUS_EMERGE_H diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_staff_attack.cpp b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_staff_attack.cpp new file mode 100644 index 0000000..b15e4f7 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_staff_attack.cpp @@ -0,0 +1,117 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" + +#include "tf_gamerules.h" +#include "tf_player.h" + +#include "../merasmus.h" +#include "merasmus_staff_attack.h" +#include "merasmus_stunned.h" + +CMerasmusStaffAttack::CMerasmusStaffAttack( CTFPlayer* pTarget ) +{ + m_hTarget = pTarget; +} + + +ActionResult< CMerasmus > CMerasmusStaffAttack::OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ) +{ + // smooth out the bot's path following by moving toward a point farther down the path + m_path.SetMinLookAheadDistance( 100.0f ); + + int iLayer = me->AddGesture( ACT_MP_ATTACK_STAND_MELEE ); + float flDuration = me->GetLayerDuration( iLayer ); + m_staffSwingTimer.Start( flDuration ); + m_hitTimer.Start( 0.5f * flDuration ); + + if ( RandomInt( 0, 2 ) == 0 ) + { + CPVSFilter filter( me->WorldSpaceCenter() ); + if ( RandomInt( 1, 5 ) == 1 ) + { + me->PlayLowPrioritySound( filter, "Halloween.MerasmusStaffAttackRare" ); + } + else + { + me->PlayLowPrioritySound( filter, "Halloween.MerasmusStaffAttack" ); + } + } + + return Continue(); +} + + +ActionResult< CMerasmus > CMerasmusStaffAttack::Update( CMerasmus *me, float interval ) +{ + // Interupt if stunned + if ( me->HasStunTimer() ) + { + return ChangeTo( new CMerasmusStunned, "Stun Interupt!" ); + } + + if ( m_hitTimer.HasStarted() && m_hitTimer.IsElapsed() ) + { + m_hitTimer.Invalidate(); + + if ( m_hTarget != NULL ) + { + Vector forward; + me->GetVectors( &forward, NULL, NULL ); + + Vector toVictim = m_hTarget->WorldSpaceCenter() - me->WorldSpaceCenter(); + toVictim.NormalizeInPlace(); + + // looser tolerance as victim gets closer + const float closeRange = 100.0f; + float range = me->GetRangeTo( m_hTarget ); + float closeness = ( range < closeRange ) ? 0.0f : ( range - closeRange ) / ( tf_merasmus_attack_range.GetFloat() - closeRange ); + float hitAngle = 0.0f + closeness * 0.27f; + + if ( DotProduct( forward, toVictim ) > hitAngle ) + { + if ( me->IsRangeLessThan( m_hTarget, 0.9f * tf_merasmus_attack_range.GetFloat() ) ) + { + if ( me->IsLineOfSightClear( m_hTarget ) ) + { + // CHOP! + CTakeDamageInfo info( me, me, 70, DMG_CLUB, TF_DMG_CUSTOM_MERASMUS_DECAPITATION ); + CalculateMeleeDamageForce( &info, toVictim, me->WorldSpaceCenter(), 5.0f ); + m_hTarget->TakeDamage( info ); + + CPVSFilter filter( me->WorldSpaceCenter() ); + me->PlayLowPrioritySound( filter, "Halloween.HeadlessBossAxeHitFlesh" ); + + me->PushPlayer( m_hTarget, 500.f ); + } + } + } + } + } + + if ( m_hTarget ) + { + if ( me->IsRangeGreaterThan( m_hTarget, 100.f ) || !me->IsLineOfSightClear( m_hTarget ) ) + { + if ( m_path.GetAge() > 1.0f ) + { + CMerasmusPathCost cost( me ); + m_path.Compute( me, m_hTarget, cost ); + } + + m_path.Update( me ); + } + + me->GetLocomotionInterface()->FaceTowards( m_hTarget->WorldSpaceCenter() ); + } + + if ( m_staffSwingTimer.IsElapsed() ) + { + return Done(); + } + + return Continue(); +} diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_staff_attack.h b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_staff_attack.h new file mode 100644 index 0000000..60629fc --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_staff_attack.h @@ -0,0 +1,28 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef MERASMUS_STAFF_ATTACK_H +#define MERASMUS_STAFF_ATTACK_H + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CMerasmusStaffAttack : public Action< CMerasmus > +{ +public: + CMerasmusStaffAttack( CTFPlayer* pTarget ); + virtual ActionResult< CMerasmus > OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ); + virtual ActionResult< CMerasmus > Update( CMerasmus *me, float interval ); + + virtual const char *GetName( void ) const { return "Staff Attack"; } // return name of this action + +private: + CountdownTimer m_staffSwingTimer; + CountdownTimer m_hitTimer; + CHandle< CTFPlayer > m_hTarget; + + PathFollower m_path; +}; + +#endif // MERASMUS_STAFF_ATTACK_H
\ No newline at end of file diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_stunned.cpp b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_stunned.cpp new file mode 100644 index 0000000..8efc321 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_stunned.cpp @@ -0,0 +1,91 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" + +#include "../merasmus.h" +#include "merasmus_stunned.h" +#include "merasmus_teleport.h" + +#include "tf_player.h" + +ActionResult< CMerasmus > CMerasmusStunned::OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ) +{ + m_nStunStage = STUN_BEGIN; + + int iLayer = me->AddGesture( ACT_MP_STUN_BEGIN ); + float flDuration = me->GetLayerDuration( iLayer ); + m_stunAnimationTimer.Start( flDuration ); + + me->OnBeginStun(); + + return Continue(); +} + + +ActionResult< CMerasmus > CMerasmusStunned::Update( CMerasmus *me, float interval ) +{ + // finished? + if ( m_nStunStage == STUN_END && m_stunFinishTimer.IsElapsed() ) + { + if ( me->ShouldDisguise() ) + { + return Done(); + } + + if ( me->GetBombHitCount() >= 3 ) + { + return ChangeTo( new CMerasmusTeleport( true, true ), "Teleport AOE!" ); + } + else + { + return ChangeTo( new CMerasmusTeleport( false, false ), "Teleport to new area!" ); + } + } + + if ( m_stunAnimationTimer.IsElapsed() ) + { + bool bStunned = me->HasStunTimer(); + + // reset animation if stunned + if ( bStunned ) + { + m_nStunStage = STUN_BEGIN; + } + + switch ( m_nStunStage ) + { + case STUN_BEGIN: + { + int iLayer = me->AddGesture( ACT_MP_STUN_MIDDLE ); + float flDuration = me->GetLayerDuration( iLayer ); + m_stunAnimationTimer.Start( flDuration ); + if ( !bStunned ) + { + m_nStunStage = STUN_MID; + } + } + break; + case STUN_MID: + { + int iLayer = me->AddGesture( ACT_MP_STUN_END ); + float flDuration = me->GetLayerDuration( iLayer ); + m_stunAnimationTimer.Start( flDuration ); + + m_nStunStage = STUN_END; + m_stunFinishTimer.Start( flDuration + 0.5f ); + } + break; + } + } + + return Continue(); +} + + +void CMerasmusStunned::OnEnd( CMerasmus *me, Action< CMerasmus > *nextAction ) +{ + me->OnEndStun(); +} diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_stunned.h b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_stunned.h new file mode 100644 index 0000000..4f02145 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_stunned.h @@ -0,0 +1,29 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef TF_MERASMUS_STUNNED_H +#define TF_MERASMUS_STUNNED_H + +class CMerasmusStunned : public Action< CMerasmus > +{ +public: + virtual ActionResult< CMerasmus > OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ); + virtual ActionResult< CMerasmus > Update( CMerasmus *me, float interval ); + virtual void OnEnd( CMerasmus *me, Action< CMerasmus > *nextAction ); + + virtual const char *GetName( void ) const { return "Stunned!"; } // return name of this action +private: + enum StunStage_t + { + STUN_BEGIN, + STUN_MID, + STUN_END + }; + StunStage_t m_nStunStage; + CountdownTimer m_stunAnimationTimer; + CountdownTimer m_stunFinishTimer; +}; + +#endif //TF_MERASMUS_STUNNED_H diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_teleport.cpp b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_teleport.cpp new file mode 100644 index 0000000..538b77f --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_teleport.cpp @@ -0,0 +1,189 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" + +#include "particle_parse.h" +#include "tf/halloween/eyeball_boss/teleport_vortex.h" +#include "player_vs_environment/monster_resource.h" +#include "tf_gamerules.h" +#include "nav_mesh/tf_nav_area.h" + +#include "../merasmus.h" +#include "merasmus_teleport.h" +#include "merasmus_aoe_attack.h" + + +CMerasmusTeleport::CMerasmusTeleport( bool bShouldAOE, bool bGoToCap ) + : m_bShouldAOE( bShouldAOE ), m_bShouldGoToCap( bGoToCap ) +{ +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CMerasmus > CMerasmusTeleport::OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ) +{ + // teleport out + m_state = TELEPORTING_OUT; + me->GetBodyInterface()->StartActivity( ACT_SHIELD_DOWN ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CMerasmus > CMerasmusTeleport::Update( CMerasmus *me, float interval ) +{ + if ( me->IsActivityFinished() ) + { + switch( m_state ) + { + case TELEPORTING_OUT: + { + DispatchParticleEffect( "merasmus_tp", me->WorldSpaceCenter(), me->GetAbsAngles() ); + + me->AddEffects( EF_NOINTERP | EF_NODRAW ); + + me->SetAbsOrigin( GetTeleportPosition( me ) ); + + // wait on the other side for a moment + m_state = TELEPORTING_IN; + } + break; + + case TELEPORTING_IN: + { + me->RemoveEffects( EF_NOINTERP | EF_NODRAW ); + + DispatchParticleEffect( "merasmus_tp", me->WorldSpaceCenter(), me->GetAbsAngles() ); + + me->GetBodyInterface()->StartActivity( ACT_SHIELD_UP ); + + m_state = DONE; + } + break; + + case DONE: + { + if ( m_bShouldAOE ) + { + m_bShouldAOE = false; + return SuspendFor( new CMerasmusAOEAttack, "AOE Attack!" ); + } + } + return Done(); + } + } + + return Continue(); +} + + +Vector CMerasmusTeleport::GetTeleportPosition( CMerasmus *me ) const +{ + Vector vGroundOffset( 0, 0, 75.0f ); + if ( m_bShouldGoToCap ) + { + return me->GetHomePosition() + vGroundOffset; + } + else + { + // pick a random spot + const float goodSize = 100.f; + CUtlVector< CTFNavArea * > spawnAreaVector; + for( int i=0; i<TheNavAreas.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)TheNavAreas[i]; + + if ( !area->HasFuncNavPrefer() ) + { + // don't spawn outside nav prefer + continue; + } + + // don't use small nav areas + if ( area->GetSizeX() < goodSize || area->GetSizeY() < goodSize ) + { + continue; + } + + // don't use area containing player + if ( area->GetPlayerCount( TF_TEAM_BLUE ) || area->GetPlayerCount( TF_TEAM_RED ) ) + { + continue; + } + + spawnAreaVector.AddToTail( area ); + } + + if ( spawnAreaVector.Count() ) + { + int which = RandomInt( 0, spawnAreaVector.Count() - 1 ); + return spawnAreaVector[ which ]->GetCenter(); + } + else + { + return me->GetHomePosition() + vGroundOffset; + } + } +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +ActionResult< CMerasmus > CMerasmusEscape::OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ) +{ + me->GetBodyInterface()->StartActivity( ACT_FLY ); + + if (RandomInt(0,10) == 0) + { + me->PlayHighPrioritySound( "Halloween.MerasmusDepartRare" ); + } + else + { + me->PlayHighPrioritySound( "Halloween.MerasmusDepart" ); + } + + UTIL_LogPrintf( "HALLOWEEN: merasmus_escaped (max_dps %3.2f) (health %d) (level %d)\n", me->GetMaxInjuryRate(), me->GetHealth(), me->GetLevel() ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CMerasmus > CMerasmusEscape::Update( CMerasmus *me, float interval ) +{ + if ( me->IsActivityFinished() ) + { + Vector vPos; + QAngle qAngles; + me->GetAttachment( "effect_robe", vPos, qAngles ); + DispatchParticleEffect( "merasmus_tp", vPos, qAngles ); + + if ( g_pMonsterResource ) + { + g_pMonsterResource->HideBossHealthMeter(); + } + + IGameEvent *event = gameeventmanager->CreateEvent( "merasmus_escaped" ); + if ( event ) + { + event->SetInt( "level", me->GetLevel() ); + gameeventmanager->FireEvent( event ); + } + me->TriggerLogicRelay( "boss_exit_relay" ); + + // reset back to normal level + me->ResetLevel(); + + me->StartRespawnTimer(); + + UTIL_Remove( me ); + + return Done(); + } + + return Continue(); +} diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_teleport.h b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_teleport.h new file mode 100644 index 0000000..1bd5d67 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_teleport.h @@ -0,0 +1,49 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef MERASMUS_TELEPORT_H +#define MERASMUS_TELEPORT_H + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CMerasmusTeleport : public Action< CMerasmus > +{ +public: + CMerasmusTeleport( bool bShouldAOE, bool bGoToCap ); + + virtual ActionResult< CMerasmus > OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ); + virtual ActionResult< CMerasmus > Update( CMerasmus *me, float interval ); + + virtual const char *GetName( void ) const { return "Teleport"; } // return name of this action + +private: + enum TeleportState + { + TELEPORTING_OUT, + TELEPORTING_IN, + DONE + }; + TeleportState m_state; + + bool m_bShouldAOE; + bool m_bShouldGoToCap; + + Vector GetTeleportPosition( CMerasmus *me ) const; +}; + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CMerasmusEscape : public Action< CMerasmus > +{ +public: + virtual ActionResult< CMerasmus > OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ); + virtual ActionResult< CMerasmus > Update( CMerasmus *me, float interval ); + + virtual const char *GetName( void ) const { return "Escape"; } // return name of this action +}; + + +#endif // MERASMUS_TELEPORT_H diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_throwing_grenade.cpp b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_throwing_grenade.cpp new file mode 100644 index 0000000..f82fdc7 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_throwing_grenade.cpp @@ -0,0 +1,159 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" + +#include "tf_player.h" +#include "tf_gamerules.h" +#include "particle_parse.h" + +#include "../merasmus.h" +#include "merasmus_throwing_grenade.h" +#include "merasmus_stunned.h" + +CMerasmusThrowingGrenade::CMerasmusThrowingGrenade( CTFPlayer* pTarget ) +{ + m_hTarget = pTarget; +} + + +ActionResult< CMerasmus > CMerasmusThrowingGrenade::OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ) +{ + if ( m_hTarget == NULL ) + { + return Done( "No Target" ); + } + + if ( !me->IsLineOfSightClear( m_hTarget ) ) + { + CUtlVector< CTFPlayer * > playerVector; + + // collect everyone + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + CUtlVector< CTFPlayer * > newTargetVector; + for ( int i=0; i<playerVector.Count(); ++i ) + { + if ( playerVector[i] == m_hTarget ) + { + continue; + } + + if ( !me->IsLineOfSightClear( playerVector[i] ) ) + { + continue; + } + + newTargetVector.AddToTail( playerVector[i] ); + } + + if ( newTargetVector.Count() == 0 ) + { + m_hTarget = NULL; + } + else + { + int which = RandomInt( 0, newTargetVector.Count() - 1 ); + m_hTarget = newTargetVector[ which ]; + } + } + + if ( m_hTarget == NULL ) + { + return Done( "No Target" ); + } + + me->GetLocomotionInterface()->FaceTowards( m_hTarget->WorldSpaceCenter() ); + int iLayer = me->AddGesture( ACT_MP_ATTACK_STAND_ITEM1 ); + float flDuration = me->GetLayerDuration( iLayer ); + m_throwTimer.Start( flDuration ); + + // we want to release the grenade mid-animation + m_releaseGrenadeTimer.Start( 0.25f ); + + // hide his staff + int staffBodyGroup = me->FindBodygroupByName( "staff" ); + me->SetBodygroup( staffBodyGroup, 2 ); + + // smooth out the bot's path following by moving toward a point farther down the path + m_path.SetMinLookAheadDistance( 100.0f ); + + return Continue(); +} + + +ActionResult< CMerasmus > CMerasmusThrowingGrenade::Update( CMerasmus *me, float interval ) +{ + // Interupt if stunned + if ( me->HasStunTimer() ) + { + return ChangeTo( new CMerasmusStunned, "Stun Interupt!" ); + } + + if ( m_releaseGrenadeTimer.HasStarted() && m_releaseGrenadeTimer.IsElapsed() ) + { + m_releaseGrenadeTimer.Invalidate(); + + DispatchParticleEffect( "merasmus_shoot", PATTACH_ABSORIGIN_FOLLOW, me, "effect_hand_R" ); + + Vector vPos; + QAngle qAngles; + me->GetAttachment( "effect_hand_R", vPos, qAngles ); + + Vector vForward, vRight, vUp; + AngleVectors( me->EyeAngles(), &vForward, &vRight, &vUp ); + float flLaunchSpeed = RandomFloat( 1500.f, 2000.f ); + Vector vecVelocity = ( vForward * flLaunchSpeed ) + ( vUp * 200.0f ) + ( RandomFloat( -10.0f, 10.0f ) * vRight ) + ( RandomFloat( -10.0f, 10.0f ) * vUp ); + CTFWeaponBaseGrenadeProj* pGrenade = CMerasmus::CreateMerasmusGrenade( vPos, vecVelocity, me ); + if ( pGrenade ) + { + if ( RandomInt( 0, 6 ) == 0 ) + { + CPVSFilter filter( me->WorldSpaceCenter() ); + if ( RandomInt(1,10) == 1 ) + { + me->PlayLowPrioritySound( filter, "Halloween.MerasmusGrenadeThrowRare" ); + } + else + { + me->PlayLowPrioritySound( filter, "Halloween.MerasmusGrenadeThrow" ); + } + } + } + } + + if ( m_hTarget ) + { + if ( me->IsRangeGreaterThan( m_hTarget, 100.f ) || !me->IsLineOfSightClear( m_hTarget ) ) + { + if ( m_path.GetAge() > 1.0f ) + { + CMerasmusPathCost cost( me ); + m_path.Compute( me, m_hTarget, cost ); + } + + m_path.Update( me ); + } + + me->GetLocomotionInterface()->FaceTowards( m_hTarget->WorldSpaceCenter() ); + } + + + if ( m_throwTimer.IsElapsed() ) + { + return Done( "Fire in the hole!" ); + } + + return Continue(); +} + + +void CMerasmusThrowingGrenade::OnEnd( CMerasmus *me, Action< CMerasmus > *nextAction ) +{ + // turn his staff back on + int staffBodyGroup = me->FindBodygroupByName( "staff" ); + me->SetBodygroup( staffBodyGroup, 0 ); +} diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_throwing_grenade.h b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_throwing_grenade.h new file mode 100644 index 0000000..7a508cf --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_throwing_grenade.h @@ -0,0 +1,27 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef MERASMUS_ROCKET_H +#define MERASMUS_ROCKET_H + +class CMerasmusThrowingGrenade : public Action< CMerasmus > +{ +public: + CMerasmusThrowingGrenade( CTFPlayer* pTarget ); + + virtual ActionResult< CMerasmus > OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ); + virtual ActionResult< CMerasmus > Update( CMerasmus *me, float interval ); + virtual void OnEnd( CMerasmus *me, Action< CMerasmus > *nextAction ); + + virtual const char *GetName( void ) const { return "Rocket"; } // return name of this action +private: + CHandle< CTFPlayer > m_hTarget; + CountdownTimer m_throwTimer; + CountdownTimer m_releaseGrenadeTimer; + + PathFollower m_path; +}; + +#endif // MERASMUS_ROCKET_H diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_zap.cpp b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_zap.cpp new file mode 100644 index 0000000..1be6822 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_zap.cpp @@ -0,0 +1,73 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" + +#include "../merasmus.h" +#include "merasmus_zap.h" +#include "merasmus_stunned.h" + +ActionResult< CMerasmus > CMerasmusZap::OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ) +{ + me->GetBodyInterface()->StartActivity( ACT_RANGE_ATTACK2 ); + m_zapTimer.Start( 1.3f ); + + m_spellType = SpellType_t( RandomInt( 0, SPELL_COUNT - 1 ) ); + PlayCastSound( me ); + + return Continue(); +} + + +ActionResult< CMerasmus > CMerasmusZap::Update( CMerasmus *me, float interval ) +{ + // Interupt if stunned + if ( me->HasStunTimer() ) + { + return ChangeTo( new CMerasmusStunned, "Stun Interupt!" ); + } + + if ( m_zapTimer.HasStarted() && m_zapTimer.IsElapsed() ) + { + m_zapTimer.Invalidate(); + + const float flSpellRange = 600.f + 50.f * ( me->GetLevel() - 1 ); + const int nTargetCount = 6 + ( me->GetLevel() - 1 ); + const float flMaxDamage = 50.f + ( 5 * (me->GetLevel() - 1) ); + const float flMinDamage = 20.f + ( 5 * (me->GetLevel() - 1) ); + + if ( CMerasmus::Zap( me, "effect_staff", flSpellRange, flMinDamage, flMaxDamage, nTargetCount ) ) + { + me->EmitSound( "Halloween.Merasmus_Spell" ); + } + } + + if ( me->IsActivityFinished() ) + { + return Done( "Zapped!" ); + } + + return Continue(); +} + + +void CMerasmusZap::PlayCastSound( CMerasmus* me ) const +{ + CPVSFilter filter( me->WorldSpaceCenter() ); + switch ( m_spellType ) + { + case SPELL_FIRE: + { + me->PlayLowPrioritySound( filter, "Halloween.MerasmusCastFireSpell" ); + } + break; + case SPELL_LAUNCH: + { + me->PlayLowPrioritySound( filter, "Halloween.MerasmusLaunchSpell" ); + } + break; + } +} + diff --git a/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_zap.h b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_zap.h new file mode 100644 index 0000000..559d136 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_zap.h @@ -0,0 +1,32 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef TF_MERASMUS_ZAP_H +#define TF_MERASMUS_ZAP_H + +#include "tf_gamerules.h" + +class CMerasmusZap : public Action< CMerasmus > +{ +public: + virtual ActionResult< CMerasmus > OnStart( CMerasmus *me, Action< CMerasmus > *priorAction ); + virtual ActionResult< CMerasmus > Update( CMerasmus *me, float interval ); + + virtual const char *GetName( void ) const { return "Zap!"; } // return name of this action +private: + enum SpellType_t + { + SPELL_FIRE, + SPELL_LAUNCH, + + SPELL_COUNT + }; + SpellType_t m_spellType; + void PlayCastSound( CMerasmus* me ) const; + + CountdownTimer m_zapTimer; +}; + +#endif //TF_MERASMUS_ZAP_H diff --git a/game/server/tf/halloween/merasmus/merasmus_body.cpp b/game/server/tf/halloween/merasmus/merasmus_body.cpp new file mode 100644 index 0000000..a7ec353 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_body.cpp @@ -0,0 +1,118 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" + +#include "NextBot.h" +#include "merasmus.h" +#include "merasmus_body.h" + + +//------------------------------------------------------------------------------------------- +CMerasmusBody::CMerasmusBody( INextBot *bot ) : IBody( bot ) +{ + m_moveXPoseParameter = -1; + m_moveYPoseParameter = -1; + m_currentActivity = -1; +} + + +//------------------------------------------------------------------------------------------- +bool CMerasmusBody::StartActivity( Activity act, unsigned int flags ) +{ + CMerasmus *me = (CMerasmus *)GetBot()->GetEntity(); + + int animSequence = ::SelectWeightedSequence( me->GetModelPtr(), act, me->GetSequence() ); + + if ( animSequence ) + { + m_currentActivity = act; + me->SetSequence( animSequence ); + me->SetPlaybackRate( 1.0f ); + me->SetCycle( 0 ); + me->ResetSequenceInfo(); + + return true; + } + + return false; +} + + +//------------------------------------------------------------------------------------------- +void CMerasmusBody::Update( void ) +{ + CMerasmus *me = (CMerasmus *)GetBot()->GetEntity(); + + if ( m_moveXPoseParameter < 0 ) + { + m_moveXPoseParameter = me->LookupPoseParameter( "move_x" ); + } + + if ( m_moveYPoseParameter < 0 ) + { + m_moveYPoseParameter = me->LookupPoseParameter( "move_y" ); + } + + + // Update the pose parameters + float speed = me->GetLocomotionInterface()->GetGroundSpeed(); // me->GetAbsVelocity().Length(); + + if ( speed < 0.01f ) + { + // stopped + if ( m_moveXPoseParameter >= 0 ) + { + me->SetPoseParameter( m_moveXPoseParameter, 0.0f ); + } + + if ( m_moveYPoseParameter >= 0 ) + { + me->SetPoseParameter( m_moveYPoseParameter, 0.0f ); + } + } + else + { + Vector forward, right, up; + me->GetVectors( &forward, &right, &up ); + + const Vector &motionVector = me->GetLocomotionInterface()->GetGroundMotionVector(); + + // move_x == 1.0 at full forward motion and -1.0 in full reverse + if ( m_moveXPoseParameter >= 0 ) + { + float forwardVel = DotProduct( motionVector, forward ); + + me->SetPoseParameter( m_moveXPoseParameter, forwardVel ); + } + + if ( m_moveYPoseParameter >= 0 ) + { + float sideVel = DotProduct( motionVector, right ); + + me->SetPoseParameter( m_moveYPoseParameter, sideVel ); + } + } + + // adjust animation speed to actual movement speed + if ( me->m_flGroundSpeed > 0.0f ) + { + // Clamp playback rate to avoid datatable warnings. Anything faster would look silly, anyway. + float playbackRate = clamp( speed / me->m_flGroundSpeed, -4.f, 12.f ); + me->SetPlaybackRate( playbackRate ); + } + + // move the animation ahead in time + me->StudioFrameAdvance(); + me->DispatchAnimEvents( me ); +} + + +//--------------------------------------------------------------------------------------------- +// return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface) +unsigned int CMerasmusBody::GetSolidMask( void ) const +{ + return MASK_NPCSOLID | CONTENTS_PLAYERCLIP; +} diff --git a/game/server/tf/halloween/merasmus/merasmus_body.h b/game/server/tf/halloween/merasmus/merasmus_body.h new file mode 100644 index 0000000..b542b19 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_body.h @@ -0,0 +1,51 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef MERASMUS_BODY_H +#define MERASMUS_BODY_H + +#include "animation.h" +#include "NextBotBodyInterface.h" + +class INextBot; + + +//---------------------------------------------------------------------------------------------------------------- +/** + * The interface for control and information about the bot's body state (posture, animation state, etc) + */ +class CMerasmusBody : public IBody +{ +public: + CMerasmusBody( INextBot *bot ); + virtual ~CMerasmusBody() { } + + virtual void Update( void ); + + virtual bool StartActivity( Activity act, unsigned int flags = 0 ); + virtual Activity GetActivity( void ) const; // return currently animating activity + virtual bool IsActivity( Activity act ) const; // return true if currently animating activity matches the given one + + virtual unsigned int GetSolidMask( void ) const; // return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface) + +private: + int m_currentActivity; + int m_moveXPoseParameter; + int m_moveYPoseParameter; +}; + + +inline Activity CMerasmusBody::GetActivity( void ) const +{ + return (Activity)m_currentActivity; +} + +inline bool CMerasmusBody::IsActivity( Activity act ) const +{ + return act == m_currentActivity ? true : false; +} + + +#endif // MERASMUS_BODY_H diff --git a/game/server/tf/halloween/merasmus/merasmus_dancer.cpp b/game/server/tf/halloween/merasmus/merasmus_dancer.cpp new file mode 100644 index 0000000..5e9b7f4 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_dancer.cpp @@ -0,0 +1,172 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "tf_gamerules.h" +#include "merasmus_dancer.h" +#include "animation.h" + +//----------------------------------------------------------------------------- + +#include "tf_fx.h" + +//----------------------------------------------------------------------------- + +#define POOF_SOUND "Halloween.Merasmus_Hiding_Explode" + +//----------------------------------------------------------------------------- + +LINK_ENTITY_TO_CLASS( merasmus_dancer, CMerasmusDancer ); + +IMPLEMENT_SERVERCLASS_ST( CMerasmusDancer, DT_MerasmusDancer ) +END_SEND_TABLE() + +//----------------------------------------------------------------------------- + +#define MERASMUS_MODEL_NAME "models/bots/merasmus/merasmus.mdl" + +//----------------------------------------------------------------------------- + +CMerasmusDancer::CMerasmusDancer() +: m_bEmitParticleEffect( false ) +{ +} + +//----------------------------------------------------------------------------- + +CMerasmusDancer::~CMerasmusDancer() +{ +} + +//----------------------------------------------------------------------------- + +void CMerasmusDancer::Spawn() +{ + Precache(); + + m_DieCountdownTimer.Invalidate(); + + BaseClass::Spawn(); + + SetModel( MERASMUS_MODEL_NAME ); + UseClientSideAnimation(); + + SetThink( &CMerasmusDancer::DanceThink ); + SetNextThink( gpGlobals->curtime ); + + m_bEmitParticleEffect = true; +} + +//----------------------------------------------------------------------------- + +void CMerasmusDancer::PlaySequence( const char *pSeqName ) +{ + int iAnimSequence = LookupSequence( pSeqName ); // dance animation + if ( iAnimSequence ) + { + SetSequence( iAnimSequence ); + SetPlaybackRate( 1.0f ); + SetCycle( 0 ); + ResetSequenceInfo(); + + HideStaff(); + } +} + +//----------------------------------------------------------------------------- + +void CMerasmusDancer::PlayActivity( int iActivity ) +{ + int iAnimSequence = ::SelectWeightedSequence( GetModelPtr(), iActivity, GetSequence() ); + if ( iAnimSequence ) + { + SetSequence( iAnimSequence ); + SetPlaybackRate( 1.0f ); + SetCycle( 0 ); + ResetSequenceInfo(); + + HideStaff(); + } +} + +//----------------------------------------------------------------------------- + +void CMerasmusDancer::HideStaff() +{ + int nStaffBodyGroup = FindBodygroupByName( "staff" ); + SetBodygroup( nStaffBodyGroup, 2 ); +} + +//----------------------------------------------------------------------------- + +void CMerasmusDancer::Dance() +{ + PlaySequence( "taunt06" ); +} + +//----------------------------------------------------------------------------- + +void CMerasmusDancer::Vanish() +{ + m_bEmitParticleEffect = true; + m_DieCountdownTimer.Start( 0.0f ); +} + +//----------------------------------------------------------------------------- + +void CMerasmusDancer::BlastOff() +{ + m_bEmitParticleEffect = true; + m_DieCountdownTimer.Start( 0.3f ); + + PlayActivity( ACT_FLY ); +} + +//----------------------------------------------------------------------------- + +void CMerasmusDancer::Precache() +{ + BaseClass::Precache(); + + int model = PrecacheModel( MERASMUS_MODEL_NAME ); + PrecacheGibsForModel( model ); + PrecacheParticleSystem( "merasmus_tp" ); // puff effect + + PrecacheScriptSound( POOF_SOUND ); + + // We deliberately allow late precaches here. + bool bAllowPrecache = CBaseAnimating::IsPrecacheAllowed(); + CBaseAnimating::SetAllowPrecache( bAllowPrecache ); +} + +//----------------------------------------------------------------------------- + +bool CMerasmusDancer::ShouldDelete() const +{ + return m_DieCountdownTimer.HasStarted() && m_DieCountdownTimer.IsElapsed(); +} + +//----------------------------------------------------------------------------- + +void CMerasmusDancer::DanceThink() +{ + // Emit the initial effect here, rather than in Spawn(), since GetAbsOrigin() and GetAbsAngles() don't return useful values then. + if ( m_bEmitParticleEffect ) + { + DispatchParticleEffect( "merasmus_tp", GetAbsOrigin(), GetAbsAngles() ); + m_bEmitParticleEffect = false; + EmitSound( POOF_SOUND ); + } + + if ( ShouldDelete() ) + { + EmitSound( POOF_SOUND ); + UTIL_Remove( this ); + return; + } + + SetNextThink( gpGlobals->curtime ); +} diff --git a/game/server/tf/halloween/merasmus/merasmus_dancer.h b/game/server/tf/halloween/merasmus/merasmus_dancer.h new file mode 100644 index 0000000..305f589 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_dancer.h @@ -0,0 +1,49 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Teleport vortex for the Eyeball Boss +// +//=============================================================================// +#ifndef MERASMUS_DANCE_H +#define MERASMUS_DANCE_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "baseanimating.h" + +//============================================================================= +// +// Non-AI version of Merasmus that can be spawned during the dance spell. +// +class CMerasmusDancer : public CBaseAnimating +{ + DECLARE_CLASS( CMerasmusDancer, CBaseAnimating ); + DECLARE_SERVERCLASS(); + +public: + CMerasmusDancer(); + virtual ~CMerasmusDancer(); + + void Dance(); + void Vanish(); + void BlastOff(); + +private: + virtual void Spawn(); + virtual void Precache(); + + void HideStaff(); + void PlaySequence( const char *pSeqName ); + void PlayActivity( int iActivity ); + void DanceThink(); + + bool ShouldDelete() const; + + bool m_bEmitParticleEffect; + CountdownTimer m_DieCountdownTimer; +}; + +#endif // MERASMUS_DANCE_H + + diff --git a/game/server/tf/halloween/merasmus/merasmus_trick_or_treat_prop.cpp b/game/server/tf/halloween/merasmus/merasmus_trick_or_treat_prop.cpp new file mode 100644 index 0000000..f343d92 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_trick_or_treat_prop.cpp @@ -0,0 +1,178 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" + +#include "tf_ammo_pack.h" +#include "particle_parse.h" +#include "tf/halloween/ghost/ghost.h" +#include "tf_player.h" +#include "tf_gamerules.h" + +#include "merasmus.h" +#include "merasmus_trick_or_treat_prop.h" + +LINK_ENTITY_TO_CLASS( tf_merasmus_trick_or_treat_prop, CTFMerasmusTrickOrTreatProp ); + +IMPLEMENT_AUTO_LIST( ITFMerasmusTrickOrTreatProp ); + +ConVar tf_merasmus_prop_health( "tf_merasmus_prop_health", "150", FCVAR_CHEAT | FCVAR_GAMEDLL ); + +CTFMerasmusTrickOrTreatProp::CTFMerasmusTrickOrTreatProp() +{ +} + + +void CTFMerasmusTrickOrTreatProp::Spawn() +{ + Precache(); + + SetModel( CMerasmus::GetRandomPropModelName() ); + + BaseClass::Spawn(); + + SetMoveType( MOVETYPE_NONE ); + SetSolid( SOLID_VPHYSICS ); + m_takedamage = DAMAGE_YES; + SetHealth( tf_merasmus_prop_health.GetInt() ); + + DispatchParticleEffect( "merasmus_object_spawn", WorldSpaceCenter(), GetAbsAngles() ); +} + + +void CTFMerasmusTrickOrTreatProp::Event_Killed( const CTakeDamageInfo &info ) +{ + SpawnTrickOrTreatItem(); + + DispatchParticleEffect( "merasmus_object_spawn", WorldSpaceCenter(), GetAbsAngles() ); + EmitSound( "Halloween.Merasmus_Hiding_Explode" ); + + if ( TFGameRules()->GetActiveBoss() ) + { + CMerasmus* pMerasmus = assert_cast< CMerasmus* >( TFGameRules()->GetActiveBoss() ); + if ( pMerasmus ) + { + if ( pMerasmus->IsNextKilledPropMerasmus() ) + { + // move merasmus to the destroyed prop before we reveal him + pMerasmus->SetAbsOrigin( GetAbsOrigin() ); + + if ( info.GetAttacker() && info.GetAttacker()->IsPlayer() ) + { + CTFPlayer* pTFPlayer = assert_cast< CTFPlayer* >( info.GetAttacker() ); + if ( pTFPlayer ) + { + pMerasmus->SetRevealer( pTFPlayer ); + + IGameEvent *pEvent = gameeventmanager->CreateEvent( "merasmus_prop_found" ); + if ( pEvent ) + { + pEvent->SetInt( "player", pTFPlayer->GetUserID() ); + gameeventmanager->FireEvent( pEvent, true ); + } + } + } + } + else + { + CPVSFilter filter( pMerasmus->WorldSpaceCenter() ); + if (RandomInt(1,3) == 1) + { + pMerasmus->PlayLowPrioritySound( filter, "Halloween.MerasmusTauntFakeProp" ); + } + } + } + } + + BaseClass::Event_Killed( info ); +} + + +int CTFMerasmusTrickOrTreatProp::OnTakeDamage( const CTakeDamageInfo &info ) +{ + DispatchParticleEffect( "merasmus_blood", info.GetDamagePosition(), GetAbsAngles() ); + + CTakeDamageInfo newinfo = info; + + CTFPlayer *pTFPlayer = ToTFPlayer( newinfo.GetAttacker() ); + if ( pTFPlayer && ( pTFPlayer->IsPlayerClass( TF_CLASS_SOLDIER ) || pTFPlayer->IsPlayerClass( TF_CLASS_DEMOMAN ) ) && ( newinfo.GetDamageType() & DMG_BLAST ) ) + { + newinfo.SetDamage( GetHealth() * 2.f ); + } + + return BaseClass::OnTakeDamage( newinfo ); +} + + +void CTFMerasmusTrickOrTreatProp::Touch( CBaseEntity *pOther ) +{ + BaseClass::Touch( pOther ); + + if ( pOther && pOther->IsPlayer() ) + { + CTFPlayer *pPlayer = ToTFPlayer( pOther ); + if ( pPlayer->m_Shared.InCond( TF_COND_HALLOWEEN_BOMB_HEAD ) ) + { + pPlayer->m_Shared.RemoveCond( TF_COND_HALLOWEEN_BOMB_HEAD ); + pPlayer->m_Shared.RemoveCond( TF_COND_STUNNED ); + pPlayer->MerasmusPlayerBombExplode( false ); + + // force kill + CTakeDamageInfo info( pPlayer, pPlayer, 99999, DMG_BLAST, TF_DMG_CUSTOM_NONE ); + Event_Killed( info ); + } + } +} + + +CTFMerasmusTrickOrTreatProp* CTFMerasmusTrickOrTreatProp::Create( const Vector& vPosition, const QAngle& qAngles ) +{ + CTFMerasmusTrickOrTreatProp *pTrickOrTreatProp = static_cast<CTFMerasmusTrickOrTreatProp*>( CBaseEntity::Create( "tf_merasmus_trick_or_treat_prop", vPosition, qAngles, NULL ) ); + + // must be on a team different from player(s) in order for some + // weapons to hit (ie: pipe bombs) + if ( pTrickOrTreatProp ) + { + pTrickOrTreatProp->ChangeTeam( TF_TEAM_HALLOWEEN ); + } + + return pTrickOrTreatProp; +} + + +void CTFMerasmusTrickOrTreatProp::SpawnTrickOrTreatItem() +{ + int nNumAmmo = 1/*RandomInt( 1, 3 )*/; + for ( int i=0; i<nNumAmmo; ++i ) + { + // Create the ammo pack. + CTFAmmoPack *pAmmoPack = CTFAmmoPack::Create( WorldSpaceCenter(), vec3_angle, this, "models/items/ammopack_medium.mdl" ); + Assert( pAmmoPack ); + if ( pAmmoPack ) + { + pAmmoPack->MakeHolidayPack(); + pAmmoPack->SetBonusScale( 2.f ); + pAmmoPack->SetModelScale( 1.4f ); + + Vector vecRight, vecUp; + AngleVectors( EyeAngles(), NULL, &vecRight, &vecUp ); + + // Calculate the initial impulse on the weapon. + Vector vecImpulse = RandomVector( 40.f, 80.f ); + vecImpulse.z *= Sign( vecImpulse.z ); // always go up + + pAmmoPack->SetInitialVelocity( vecImpulse ); + pAmmoPack->ApplyAbsVelocityImpulse( vecImpulse ); + + + // Give the ammo pack some health, so that trains can destroy it. + pAmmoPack->SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + pAmmoPack->m_takedamage = DAMAGE_YES; + pAmmoPack->SetHealth( 900 ); + + pAmmoPack->SetBodygroup( 1, 1 ); + } + } +} diff --git a/game/server/tf/halloween/merasmus/merasmus_trick_or_treat_prop.h b/game/server/tf/halloween/merasmus/merasmus_trick_or_treat_prop.h new file mode 100644 index 0000000..b9d48c4 --- /dev/null +++ b/game/server/tf/halloween/merasmus/merasmus_trick_or_treat_prop.h @@ -0,0 +1,30 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef MERASMUS_TRICK_OR_TREAT_PROP_H +#define MERASMUS_TRICK_OR_TREAT_PROP_H + +DECLARE_AUTO_LIST( ITFMerasmusTrickOrTreatProp ); + +class CTFMerasmusTrickOrTreatProp : public CBaseAnimating, public ITFMerasmusTrickOrTreatProp +{ + DECLARE_CLASS( CTFMerasmusTrickOrTreatProp, CBaseAnimating ); + +public: + CTFMerasmusTrickOrTreatProp(); + ~CTFMerasmusTrickOrTreatProp() {} + + virtual void Spawn( void ); + virtual void Event_Killed( const CTakeDamageInfo &info ); + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + virtual void Touch( CBaseEntity *pOther ); + + static CTFMerasmusTrickOrTreatProp* Create( const Vector& vPosition, const QAngle& qAngles ); + +private: + void SpawnTrickOrTreatItem(); +}; + +#endif // MERASMUS_TRICK_OR_TREAT_PROP_H diff --git a/game/server/tf/halloween/spell/tf_spell_pickup.cpp b/game/server/tf/halloween/spell/tf_spell_pickup.cpp new file mode 100644 index 0000000..89d274c --- /dev/null +++ b/game/server/tf/halloween/spell/tf_spell_pickup.cpp @@ -0,0 +1,111 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Spell +// +//=============================================================================// +#include "cbase.h" + +#include "tf_spell_pickup.h" +#include "tf_player.h" +#include "halloween/tf_weapon_spellbook.h" +#include "tf_gamerules.h" + +LINK_ENTITY_TO_CLASS( tf_spell_pickup, CSpellPickup ); + +BEGIN_DATADESC( CSpellPickup ) + + // Keyfields. + DEFINE_KEYFIELD( m_nTier, FIELD_INTEGER, "tier" ), + +END_DATADESC(); + + +//----------------------------------------------------------------------------- +CSpellPickup::CSpellPickup() +{ + m_nTier = 0; +} + +//----------------------------------------------------------------------------- +void CSpellPickup::Spawn( void ) +{ + BaseClass::Spawn(); + m_nSkin = m_nTier; +} + +//----------------------------------------------------------------------------- +void CSpellPickup::Precache( void ) +{ + BaseClass::Precache(); + + PrecacheScriptSound( "Halloween.spell_pickup" ); + PrecacheScriptSound( "Halloween.spell_pickup_rare" ); +} + +//----------------------------------------------------------------------------- +bool CSpellPickup::MyTouch( CBasePlayer *pPlayer ) +{ + CTFPlayer *pTFPlayer = ToTFPlayer( pPlayer ); + if ( pTFPlayer ) + { + CTFSpellBook *pSpellBook = dynamic_cast< CTFSpellBook* >( pTFPlayer->GetEntityForLoadoutSlot( LOADOUT_POSITION_ACTION ) ); + if ( pSpellBook ) + { + pSpellBook->RollNewSpell( m_nTier ); + + CSingleUserRecipientFilter filter( pPlayer ); + const char *pszSoundName = ( m_nTier > 0 ) ? "Halloween.spell_pickup_rare" : "Halloween.spell_pickup"; + EmitSound( filter, entindex(), pszSoundName ); + } + } + + return true; +} + + +//----------------------------------------------------------------------------- +bool CSpellPickup::ItemCanBeTouchedByPlayer( CBasePlayer *pPlayer ) +{ + if ( IsDisabled() ) + return false; + + // Dont let them pick up new spells if they already have a spell unless its a tier 1 spell + CTFPlayer *pTFPlayer = ToTFPlayer( pPlayer ); + if ( pTFPlayer && m_nTier == 0 ) + { + CTFSpellBook *pSpellBook = dynamic_cast< CTFSpellBook* >( pTFPlayer->GetEntityForLoadoutSlot( LOADOUT_POSITION_ACTION ) ); + if ( !pSpellBook ) + { + // TEMP + ClientPrint( pPlayer, HUD_PRINTCENTER, "Equip a SpellBook in your ActionSlot to pick this up.", pPlayer->GetPlayerName() ); + return false; + } + + if ( pSpellBook->HasASpellWithCharges() ) + { + return false; + } + } + + return true; +} + +//----------------------------------------------------------------------------- +const char *CSpellPickup::GetPowerupModel( void ) +{ + if ( TFGameRules() && TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_DOOMSDAY ) ) + { + if ( m_nTier == 1 ) + { + return "models/items/crystal_ball_pickup_major.mdl"; + } + return "models/items/crystal_ball_pickup.mdl"; + } + + if ( m_nTier == 1 ) + { + return "models/props_halloween/hwn_spellbook_upright_major.mdl"; + } + + return BaseClass::GetPowerupModel(); +} diff --git a/game/server/tf/halloween/spell/tf_spell_pickup.h b/game/server/tf/halloween/spell/tf_spell_pickup.h new file mode 100644 index 0000000..1e8c4f2 --- /dev/null +++ b/game/server/tf/halloween/spell/tf_spell_pickup.h @@ -0,0 +1,37 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Spell. +// +//=============================================================================// +#ifndef TF_SPELL_PICKUP_H +#define TF_SPELL_PICKUP_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "tf_powerup.h" +class CSpellPickup : public CTFPowerup +{ + DECLARE_CLASS( CSpellPickup, CTFPowerup ) + DECLARE_DATADESC(); + +public: + CSpellPickup(); + + virtual void Spawn( void ) OVERRIDE; + virtual void Precache() OVERRIDE; + + virtual bool MyTouch( CBasePlayer *pPlayer ) OVERRIDE; + virtual const char *GetPowerupModel( void ) OVERRIDE; + virtual const char *GetDefaultPowerupModel( void ) OVERRIDE { return "models/props_halloween/hwn_spellbook_upright.mdl"; } + virtual bool ItemCanBeTouchedByPlayer( CBasePlayer *pPlayer ) OVERRIDE; + + void SetTier( int nTier ) { m_nTier = nTier; } + +private: + + int m_nTier; +}; + +#endif // TF_SPELL_PICKUP_H diff --git a/game/server/tf/halloween/zombie/zombie.cpp b/game/server/tf/halloween/zombie/zombie.cpp new file mode 100644 index 0000000..601b739 --- /dev/null +++ b/game/server/tf/halloween/zombie/zombie.cpp @@ -0,0 +1,599 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "nav_mesh/tf_nav_area.h" +#include "NextBot/Path/NextBotChasePath.h" +#include "particle_parse.h" + +#include "zombie.h" +#include "zombie_behavior/zombie_spawn.h" + +#include "halloween/tf_weapon_spellbook.h" + +#define SKELETON_MODEL "models/bots/skeleton_sniper/skeleton_sniper.mdl" +#define SKELETON_KING_MODEL "models/bots/skeleton_sniper_boss/skeleton_sniper_boss.mdl" +#define SKELETON_KING_CROWN_MODEL "models/player/items/demo/crown.mdl" + +ConVar tf_max_active_zombie( "tf_max_active_zombie", "30", FCVAR_CHEAT ); + +#ifdef STAGING_ONLY +ConVar tf_halloween_skeleton_test_hat( "tf_halloween_skeleton_test_hat", "-1", FCVAR_CHEAT ); +#endif // STAGING_ONLY + +//----------------------------------------------------------------------------------------------------- +// NPC Zombie versions of the players +//----------------------------------------------------------------------------------------------------- +LINK_ENTITY_TO_CLASS( tf_zombie, CZombie ); + +IMPLEMENT_SERVERCLASS_ST( CZombie, DT_Zombie ) + SendPropFloat( SENDINFO( m_flHeadScale ) ), +END_SEND_TABLE() + +IMPLEMENT_AUTO_LIST( IZombieAutoList ); + + +static const char *s_skeletonHatModels[] = +{ + "models/player/items/all_class/skull_scout.mdl", + "models/workshop/player/items/scout/hw2013_boston_bandy_mask/hw2013_boston_bandy_mask.mdl", + "models/workshop/player/items/demo/hw2013_blackguards_bicorn/hw2013_blackguards_bicorn.mdl", + "models/player/items/heavy/heavy_big_chief.mdl", +}; + + +//----------------------------------------------------------------------------------------------------- +CZombie::CZombie() +{ + m_intention = new CZombieIntention( this ); + m_locomotor = new CZombieLocomotion( this ); + m_body = new CHeadlessHatmanBody( this ); + + m_nType = SKELETON_NORMAL; + + m_flHeadScale = 1.f; + + m_flAttackRange = 50.f; + m_flAttackDamage = 30.f; + + m_bSpy = false; + m_bForceSuicide = false; +} + + +//----------------------------------------------------------------------------------------------------- +CZombie::~CZombie() +{ + if ( m_intention ) + delete m_intention; + + if ( m_locomotor ) + delete m_locomotor; + + if ( m_body ) + delete m_body; +} + + +void CZombie::PrecacheZombie() +{ + /*PrecacheModel( "models/player/items/scout/scout_zombie.mdl" ); + PrecacheModel( "models/player/items/sniper/sniper_zombie.mdl" ); + PrecacheModel( "models/player/items/soldier/soldier_zombie.mdl" ); + PrecacheModel( "models/player/items/demo/demo_zombie.mdl" ); + PrecacheModel( "models/player/items/medic/medic_zombie.mdl" ); + PrecacheModel( "models/player/items/heavy/heavy_zombie.mdl" ); + PrecacheModel( "models/player/items/pyro/pyro_zombie.mdl" ); + PrecacheModel( "models/player/items/spy/spy_zombie.mdl" ); + PrecacheModel( "models/player/items/engineer/engineer_zombie.mdl" );*/ + + int nSkeletonModel = PrecacheModel( SKELETON_MODEL ); + PrecacheGibsForModel( nSkeletonModel ); + + int nSkeletonKingModel = PrecacheModel( SKELETON_KING_MODEL ); + PrecacheGibsForModel( nSkeletonKingModel ); + + PrecacheModel( SKELETON_KING_CROWN_MODEL ); + + if( TFGameRules()->GetHalloweenScenario() == CTFGameRules::HALLOWEEN_SCENARIO_DOOMSDAY ) + { + for ( int i=0; i<ARRAYSIZE( s_skeletonHatModels ) ; ++i ) + { + PrecacheModel( s_skeletonHatModels[i] ); + } + } + + PrecacheParticleSystem( "bomibomicon_ring" ); + PrecacheParticleSystem( "spell_pumpkin_mirv_goop_red" ); + PrecacheParticleSystem( "spell_pumpkin_mirv_goop_blue" ); + PrecacheParticleSystem( "spell_skeleton_goop_green" ); + + PrecacheScriptSound( "Halloween.skeleton_break" ); + PrecacheScriptSound( "Halloween.skeleton_laugh_small" ); + PrecacheScriptSound( "Halloween.skeleton_laugh_medium" ); + PrecacheScriptSound( "Halloween.skeleton_laugh_giant" ); +} + + +//----------------------------------------------------------------------------------------------------- +void CZombie::Precache() +{ + BaseClass::Precache(); + + // These are player models which are already precached... + + bool bAllowPrecache = CBaseEntity::IsPrecacheAllowed(); + CBaseEntity::SetAllowPrecache( true ); + + PrecacheZombie(); + + CBaseEntity::SetAllowPrecache( bAllowPrecache ); +} + + +//----------------------------------------------------------------------------------------------------- +void CZombie::Spawn( void ) +{ + Precache(); + + /*int which = RandomInt( TF_CLASS_SCOUT, TF_CLASS_ENGINEER ); + const char *name = g_aRawPlayerClassNamesShort[ which ]; + + if ( FStrEq( name, "spy" ) ) + { + m_bSpy = true; + }*/ + + //SetModel( CFmtStr( "models/player/%s.mdl", name ) ); + + SetModel( SKELETON_MODEL ); + + BaseClass::Spawn(); + + const int health = 50; + SetHealth( health ); + SetMaxHealth( health ); + AddFlag( FL_NPC ); + + QAngle qAngle = vec3_angle; + qAngle[YAW] = RandomFloat( 0, 360 ); + SetAbsAngles( qAngle ); + + // Spawn Pos + GetBodyInterface()->StartActivity( ACT_TRANSITION ); + + //int iSkinIndex = GetTeamNumber() == TF_TEAM_RED ? 0 : 1; + + //m_zombieParts = (CBaseAnimating *)CreateEntityByName( "prop_dynamic" ); + //if ( m_zombieParts ) + //{ + // m_zombieParts->SetModel( CFmtStr( "models/player/items/%s/%s_zombie.mdl", name, name ) ); + // m_zombieParts->m_nSkin = iSkinIndex; + + // // bonemerge into our model + // m_zombieParts->FollowEntity( this, true ); + //} + + //if ( m_bSpy ) + //{ + // // Spy has a bunch of extra skins used to adjust the mask + // iSkinIndex += 22; + //} + //else + //{ + // // 4: red zombie + // // 5: blue zombie + // // 6: red zombie invuln + // // 7: blue zombie invuln + // iSkinIndex += 4; + //} + + switch ( GetTeamNumber() ) + { + case TF_TEAM_RED: + m_nSkin = 0; + break; + case TF_TEAM_BLUE: + m_nSkin = 1; + break; + default: + { + m_nSkin = 2; + // make sure I'm on TF_TEAM_HALLOWEEN + ChangeTeam( TF_TEAM_HALLOWEEN ); + } + } + + // force kill oldest skeletons in the level (except skeleton king) to keep the number of skeletons under the max active + int nForceKill = IZombieAutoList::AutoList().Count() - tf_max_active_zombie.GetInt(); + for ( int i=0; i<IZombieAutoList::AutoList().Count() && nForceKill > 0; ++i ) + { + CZombie *pZombie = static_cast< CZombie* >( IZombieAutoList::AutoList()[i] ); + if ( pZombie->GetSkeletonType() != SKELETON_KING ) + { + pZombie->ForceSuicide(); + nForceKill--; + } + } + Assert( nForceKill <= 0 ); +} + + +//----------------------------------------------------------------------------------------------------- +int CZombie::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + if ( info.GetAttacker() && info.GetAttacker()->GetTeamNumber() == GetTeamNumber() ) + return 0; + + if ( !IsPlayingGesture( ACT_MP_GESTURE_FLINCH_CHEST ) ) + { + AddGesture( ACT_MP_GESTURE_FLINCH_CHEST ); + } + + const char* pszEffectName; + if ( GetTeamNumber() == TF_TEAM_HALLOWEEN ) + { + pszEffectName = "spell_skeleton_goop_green"; + } + else + { + pszEffectName = GetTeamNumber() == TF_TEAM_RED ? "spell_pumpkin_mirv_goop_red" : "spell_pumpkin_mirv_goop_blue"; + } + + DispatchParticleEffect( pszEffectName, info.GetDamagePosition(), GetAbsAngles() ); + + return BaseClass::OnTakeDamage_Alive( info ); +} + + +//----------------------------------------------------------------------------------------------------- +void CZombie::Event_Killed( const CTakeDamageInfo &info ) +{ + EmitSound( "Halloween.skeleton_break" ); + + if ( TFGameRules() && TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_HIGHTOWER ) ) + { + CTFPlayer *pPlayerAttacker = NULL; + if ( info.GetAttacker() && info.GetAttacker()->IsPlayer() ) + { + pPlayerAttacker = ToTFPlayer( info.GetAttacker() ); + if ( pPlayerAttacker ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_HALLOWEEN_HELLTOWER_SKELETON_GRIND ); + + IGameEvent *pEvent = gameeventmanager->CreateEvent( "halloween_skeleton_killed" ); + if ( pEvent ) + { + pEvent->SetInt( "player", pPlayerAttacker->GetUserID() ); + gameeventmanager->FireEvent( pEvent, true ); + } + } + } + } + + BaseClass::Event_Killed( info ); +} + + +//----------------------------------------------------------------------------------------------------- +void CZombie::UpdateOnRemove() +{ + CPVSFilter filter( GetAbsOrigin() ); + UserMessageBegin( filter, "BreakModel" ); + WRITE_SHORT( GetModelIndex() ); + WRITE_VEC3COORD( GetAbsOrigin() ); + WRITE_ANGLES( GetAbsAngles() ); + WRITE_SHORT( m_nSkin ); + MessageEnd(); + + UTIL_Remove( m_hHat ); + + BaseClass::UpdateOnRemove(); +} + + +//--------------------------------------------------------------------------------------------- +/*static*/ CZombie* CZombie::SpawnAtPos( const Vector& vSpawnPos, float flLifeTime /*= 0.f*/, int nTeam /*= TF_TEAM_HALLOWEEN*/, CBaseEntity *pOwner /*= NULL*/, SkeletonType_t nSkeletonType /*= SKELETON_NORMAL*/ ) +{ + CZombie *pZombie = (CZombie *)CreateEntityByName( "tf_zombie" ); + if ( pZombie ) + { + pZombie->ChangeTeam( nTeam ); + + DispatchSpawn( pZombie ); + + pZombie->SetAbsOrigin( vSpawnPos ); + pZombie->SetOwnerEntity( pOwner ); + + if ( flLifeTime > 0.f ) + { + pZombie->StartLifeTimer( flLifeTime ); + } + + pZombie->SetSkeletonType( nSkeletonType ); + } + + return pZombie; +} + + +bool CZombie::ShouldSuicide() const +{ + // out of life time + if ( m_lifeTimer.HasStarted() && m_lifeTimer.IsElapsed() ) + return true; + + // owner changed team + if ( GetOwnerEntity() && GetOwnerEntity()->GetTeamNumber() != GetTeamNumber() ) + return true; + + return m_bForceSuicide; +} + + +//----------------------------------------------------------------------------------------------------- +void CZombie::SetSkeletonType( SkeletonType_t nType ) +{ + m_nType = nType; + // Skeleton King? + if ( nType == SKELETON_KING ) + { + SetModel( SKELETON_KING_MODEL ); + SetModelScale( 2.f ); + + const int health = 1000; + SetHealth( health ); + SetMaxHealth( health ); + + m_flAttackRange = 100.f; + m_flAttackDamage = 100.f; + + AddHat( SKELETON_KING_CROWN_MODEL ); + } + else if ( nType == SKELETON_MINI ) + { + SetModel( SKELETON_MODEL ); + SetModelScale( 0.5f ); + m_flHeadScale = 3.f; + + if( TFGameRules()->GetHalloweenScenario() == CTFGameRules::HALLOWEEN_SCENARIO_DOOMSDAY ) + { + int iModelIndex = RandomInt( 0, ARRAYSIZE( s_skeletonHatModels ) - 1 ); +#ifdef STAGING_ONLY + iModelIndex = tf_halloween_skeleton_test_hat.GetInt() > 0 ? tf_halloween_skeleton_test_hat.GetInt() : iModelIndex; +#endif // STAGING_ONLY + const char *pszHat = s_skeletonHatModels[ iModelIndex ]; + AddHat( pszHat ); + } + + m_flAttackRange = 40.f; + m_flAttackDamage = 20.f; + } +} + + +//----------------------------------------------------------------------------------------------------- +void CZombie::AddHat( const char *pszModel ) +{ + if ( !m_hHat ) + { + int iHead = LookupBone( "bip_head" ); + Assert( iHead != -1 ); + if ( iHead != -1 ) + { + m_hHat = (CBaseAnimating *)CreateEntityByName( "prop_dynamic" ); + if ( m_hHat ) + { + m_hHat->SetModel( pszModel ); + + Vector pos; + QAngle angles; + GetBonePosition( iHead, pos, angles ); + m_hHat->SetAbsOrigin( pos ); + m_hHat->SetAbsAngles( angles ); + m_hHat->FollowEntity( this, true ); + } + } + } +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CZombieBehavior : public Action< CZombie > +{ +public: + virtual Action< CZombie > *InitialContainedAction( CZombie *me ) + { + return new CZombieSpawn; + } + + virtual ActionResult< CZombie > OnStart( CZombie *me, Action< CZombie > *priorAction ) + { + return Continue(); + } + + virtual ActionResult< CZombie > Update( CZombie *me, float interval ) + { + if ( !me->IsAlive() || me->ShouldSuicide() ) + { + UTIL_Remove( me ); + return Done(); + } + + if ( ShouldLaugh( me ) ) + { + Laugh( me ); + } + + return Continue(); + } + + virtual EventDesiredResult< CZombie > OnKilled( CZombie *me, const CTakeDamageInfo &info ) + { + // bonemerged models don't ragdoll + //UTIL_Remove( me->m_zombieParts ); + + if ( info.GetAttacker() && dynamic_cast< CBaseCombatCharacter* >( info.GetAttacker() ) ) + { + if ( me->GetSkeletonType() == CZombie::SKELETON_NORMAL ) + { + // normal skeleton spawns 3 mini skeletons + CBaseCombatCharacter* pOwner = dynamic_cast< CBaseCombatCharacter* >( me->GetOwnerEntity() ); + pOwner = pOwner ? pOwner : me; + for ( int i=0; i<3; ++i ) + { + CreateSpellSpawnZombie( pOwner, me->GetAbsOrigin(), 2 ); + } + } + else if ( me->GetSkeletonType() == CZombie::SKELETON_KING ) + { + // skeleton king drops rare spell + TFGameRules()->DropSpellPickup( me->GetAbsOrigin(), 1 ); + } + } + + UTIL_Remove( me ); + + return TryDone(); + } + + virtual const char *GetName( void ) const { return "ZombieBehavior"; } // return name of this action + +private: + + bool ShouldLaugh( CZombie *me ) + { + if ( !m_laughTimer.HasStarted() ) + { + switch ( me->GetSkeletonType() ) + { + case CZombie::SKELETON_KING: + { + m_laughTimer.Start( RandomFloat( 6.f, 7.f ) ); + break; + } + case CZombie::SKELETON_MINI: + { + m_laughTimer.Start( RandomFloat( 2.f, 3.f ) ); + break; + } + default: + { + m_laughTimer.Start( RandomFloat( 4.f, 5.f ) ); + } + } + + return false; + } + + if ( m_laughTimer.HasStarted() && m_laughTimer.IsElapsed() ) + { + m_laughTimer.Invalidate(); + return true; + } + + return false; + } + + void Laugh( CZombie *me ) + { + const char *pszSoundName; + switch ( me->GetSkeletonType() ) + { + case CZombie::SKELETON_KING: + { + pszSoundName = "Halloween.skeleton_laugh_giant"; + break; + } + case CZombie::SKELETON_MINI: + { + pszSoundName = "Halloween.skeleton_laugh_small"; + break; + } + default: + { + pszSoundName = "Halloween.skeleton_laugh_medium"; + } + } + + me->EmitSound( pszSoundName ); + } + + CountdownTimer m_laughTimer; +}; + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +CZombieIntention::CZombieIntention( CZombie *me ) : IIntention( me ) +{ + m_behavior = new Behavior< CZombie >( new CZombieBehavior ); +} + +CZombieIntention::~CZombieIntention() +{ + delete m_behavior; +} + +void CZombieIntention::Reset( void ) +{ + delete m_behavior; + m_behavior = new Behavior< CZombie >( new CZombieBehavior ); +} + +void CZombieIntention::Update( void ) +{ + m_behavior->Update( static_cast< CZombie * >( GetBot() ), GetUpdateInterval() ); +} + +// is this a place we can be? +QueryResultType CZombieIntention::IsPositionAllowed( const INextBot *meBot, const Vector &pos ) const +{ + return ANSWER_YES; +} + + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +float CZombieLocomotion::GetRunSpeed( void ) const +{ + return 300.f; +} + + +//--------------------------------------------------------------------------------------------- +// if delta Z is greater than this, we have to jump to get up +float CZombieLocomotion::GetStepHeight( void ) const +{ + return 18.0f; +} + + +//--------------------------------------------------------------------------------------------- +// return maximum height of a jump +float CZombieLocomotion::GetMaxJumpHeight( void ) const +{ + return 18.0f; +} + + +//--------------------------------------------------------------------------------------------- +// Return max rate of yaw rotation +float CZombieLocomotion::GetMaxYawRate( void ) const +{ + return 200.0f; +} + + +//--------------------------------------------------------------------------------------------- +bool CZombieLocomotion::ShouldCollideWith( const CBaseEntity *object ) const +{ + return false; +} diff --git a/game/server/tf/halloween/zombie/zombie.h b/game/server/tf/halloween/zombie/zombie.h new file mode 100644 index 0000000..eb6e475 --- /dev/null +++ b/game/server/tf/halloween/zombie/zombie.h @@ -0,0 +1,196 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef ZOMBIE_H +#define ZOMBIE_H + +#include "NextBot.h" +#include "NextBotBehavior.h" +#include "NextBotGroundLocomotion.h" +#include "../headless_hatman_body.h" +#include "Path/NextBotPathFollow.h" + +class CZombie; + + +//---------------------------------------------------------------------------- +class CZombieLocomotion : public NextBotGroundLocomotion +{ +public: + CZombieLocomotion( INextBot *bot ) : NextBotGroundLocomotion( bot ) { } + virtual ~CZombieLocomotion() { } + + virtual float GetRunSpeed( void ) const; // get maximum running speed + virtual float GetStepHeight( void ) const; // if delta Z is greater than this, we have to jump to get up + virtual float GetMaxJumpHeight( void ) const; // return maximum height of a jump + + virtual bool ShouldCollideWith( const CBaseEntity *object ) const; + +private: + virtual float GetMaxYawRate( void ) const; // return max rate of yaw rotation +}; + + +//---------------------------------------------------------------------------- +class CZombieIntention : public IIntention +{ +public: + CZombieIntention( CZombie *me ); + virtual ~CZombieIntention(); + + virtual void Reset( void ); + virtual void Update( void ); + + virtual QueryResultType IsPositionAllowed( const INextBot *me, const Vector &pos ) const; // is the a place we can be? + + virtual INextBotEventResponder *FirstContainedResponder( void ) const { return m_behavior; } + virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const { return NULL; } + +private: + Behavior< CZombie > *m_behavior; +}; + + +//---------------------------------------------------------------------------- +DECLARE_AUTO_LIST( IZombieAutoList ); + +class CZombie : public NextBotCombatCharacter, public IZombieAutoList +{ +public: + DECLARE_CLASS( CZombie, NextBotCombatCharacter ); + DECLARE_SERVERCLASS(); + + CZombie(); + virtual ~CZombie(); + + static void PrecacheZombie(); + virtual void Precache(); + virtual void Spawn( void ); + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + virtual void Event_Killed( const CTakeDamageInfo &info ); + virtual void UpdateOnRemove(); + + // INextBot + virtual CZombieIntention *GetIntentionInterface( void ) const { return m_intention; } + virtual CZombieLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; } + virtual CHeadlessHatmanBody *GetBodyInterface( void ) const { return m_body; } + + CBaseAnimating *m_zombieParts; + + void StartLifeTimer( float flLifeTime ) { m_lifeTimer.Start( flLifeTime ); } + bool ShouldSuicide() const; + void ForceSuicide() { m_bForceSuicide = true; } + + enum SkeletonType_t + { + SKELETON_NORMAL = 0, + SKELETON_KING, + SKELETON_MINI + }; + SkeletonType_t GetSkeletonType() const { return m_nType; } + void SetSkeletonType( SkeletonType_t nType ); + void AddHat( const char *pszModel ); + + static CZombie* SpawnAtPos( const Vector& vSpawnPos, float flLifeTime = 0.f, int nTeam = TF_TEAM_HALLOWEEN, CBaseEntity *pOwner = NULL, SkeletonType_t nSkeletonType = SKELETON_NORMAL ); + + float GetAttackRange() const { return m_flAttackRange; } + float GetAttackDamage() const { return m_flAttackDamage; } +private: + CZombieIntention *m_intention; + CZombieLocomotion *m_locomotor; + CHeadlessHatmanBody *m_body; + + SkeletonType_t m_nType; + CNetworkVar( float, m_flHeadScale ); + + CHandle< CBaseAnimating > m_hHat; + + float m_flAttackRange; + float m_flAttackDamage; + + bool m_bSpy; + bool m_bForceSuicide; + CountdownTimer m_lifeTimer; +}; + + + +//-------------------------------------------------------------------------------------------------------------- +class CZombiePathCost : public IPathCost +{ +public: + CZombiePathCost( CZombie *me ) + { + m_me = me; + } + + // return the cost (weighted distance between) of moving from "fromArea" to "area", or -1 if the move is not allowed + virtual float operator()( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) const + { + if ( fromArea == NULL ) + { + // first area in path, no cost + return 0.0f; + } + else + { + if ( !m_me->GetLocomotionInterface()->IsAreaTraversable( area ) ) + { + // our locomotor says we can't move here + return -1.0f; + } + + // compute distance traveled along path so far + float dist; + + if ( ladder ) + { + dist = ladder->m_length; + } + else if ( length > 0.0 ) + { + // optimization to avoid recomputing length + dist = length; + } + else + { + dist = ( area->GetCenter() - fromArea->GetCenter() ).Length(); + } + + // check height change + float deltaZ = fromArea->ComputeAdjacentConnectionHeightChange( area ); + if ( deltaZ >= m_me->GetLocomotionInterface()->GetStepHeight() ) + { + if ( deltaZ >= m_me->GetLocomotionInterface()->GetMaxJumpHeight() ) + { + // too high to reach + return -1.0f; + } + + // jumping is slower than flat ground + const float jumpPenalty = 5.0f; + dist += jumpPenalty * dist; + } + else if ( deltaZ < -m_me->GetLocomotionInterface()->GetDeathDropHeight() ) + { + // too far to drop + return -1.0f; + } + + // this term causes the same bot to choose different routes over time, + // but keep the same route for a period in case of repaths + int timeMod = (int)( gpGlobals->curtime / 10.0f ) + 1; + float preference = 1.0f + 50.0f * ( 1.0f + FastCos( (float)( m_me->GetEntity()->entindex() * area->GetID() * timeMod ) ) ); + float cost = dist * preference; + + return cost + fromArea->GetCostSoFar();; + } + } + + CZombie *m_me; +}; + + +#endif // ZOMBIE_H diff --git a/game/server/tf/halloween/zombie/zombie_behavior/zombie_attack.cpp b/game/server/tf/halloween/zombie/zombie_behavior/zombie_attack.cpp new file mode 100644 index 0000000..0438e05 --- /dev/null +++ b/game/server/tf/halloween/zombie/zombie_behavior/zombie_attack.cpp @@ -0,0 +1,303 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" + +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "nav_mesh/tf_nav_area.h" + +#include "../zombie.h" +#include "zombie_attack.h" +#include "zombie_special_attack.h" + +#define ZOMBIE_CHASE_MIN_DURATION 3.0f + +ConVar tf_halloween_zombie_damage( "tf_halloween_zombie_damage", "10", FCVAR_CHEAT, "How much damage a zombie melee hit does." ); + + +//---------------------------------------------------------------------------------- +ActionResult< CZombie > CZombieAttack::OnStart( CZombie *me, Action< CZombie > *priorAction ) +{ + // smooth out the bot's path following by moving toward a point farther down the path + m_path.SetMinLookAheadDistance( 100.0f ); + + // start animation + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE ); + + m_specialAttackTimer.Start( RandomFloat( 5.f, 10.f ) ); + + return Continue(); +} + + +//---------------------------------------------------------------------------------- +bool CZombieAttack::IsPotentiallyChaseable( CZombie *me, CBaseCombatCharacter *victim ) +{ + if ( !victim ) + { + return false; + } + + if ( !victim->IsAlive() ) + { + // victim is dead - pick a new one + return false; + } + + CTFNavArea *victimArea = (CTFNavArea *)victim->GetLastKnownArea(); + if ( !victimArea || victimArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED ) ) + { + // unreachable - pick a new victim + return false; + } + + if ( victim->GetGroundEntity() != NULL ) + { + Vector victimAreaPos; + victimArea->GetClosestPointOnArea( victim->GetAbsOrigin(), &victimAreaPos ); + if ( ( victim->GetAbsOrigin() - victimAreaPos ).AsVector2D().IsLengthGreaterThan( 50.0f ) ) + { + // off the mesh and unreachable - pick a new victim + return false; + } + } + + return true; +} + + +//---------------------------------------------------------------------------------- +void CZombieAttack::SelectVictim( CZombie *me ) +{ + if ( IsPotentiallyChaseable( me, m_attackTarget ) && !m_attackTargetFocusTimer.IsElapsed() ) + { + // Continue chasing current target + return; + } + + // pick a new victim to chase + CBaseCombatCharacter *newVictim = NULL; + CUtlVector< CTFPlayer * > playerVector; + + // collect everyone + if ( me->GetTeamNumber() == TF_TEAM_RED ) + { + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS ); + } + else if ( me->GetTeamNumber() == TF_TEAM_BLUE ) + { + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + } + else + { + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + } + + float victimRangeSq = FLT_MAX; + // find closest player + for( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *pPlayer = playerVector[i]; + if ( !IsPotentiallyChaseable( me, pPlayer ) ) + { + continue; + } + + // ignore stealth player + if ( pPlayer->m_Shared.IsStealthed() ) + { + if ( !pPlayer->m_Shared.InCond( TF_COND_BURNING ) && + !pPlayer->m_Shared.InCond( TF_COND_URINE ) && + !pPlayer->m_Shared.InCond( TF_COND_STEALTHED_BLINK ) && + !pPlayer->m_Shared.InCond( TF_COND_BLEEDING ) ) + { + // cloaked spies are invisible to us + continue; + } + } + + // ignore player who disguises as my team + if ( pPlayer->m_Shared.InCond( TF_COND_DISGUISED ) && pPlayer->m_Shared.GetDisguiseTeam() == me->GetTeamNumber() ) + { + continue; + } + + // ignore ghost players + if ( pPlayer->m_Shared.InCond( TF_COND_HALLOWEEN_GHOST_MODE ) ) + { + continue; + } + + float rangeSq = me->GetRangeSquaredTo( pPlayer ); + if ( rangeSq < victimRangeSq ) + { + newVictim = pPlayer; + victimRangeSq = rangeSq; + } + } + + // find closest zombie + for ( int i=0; i<IZombieAutoList::AutoList().Count(); ++i ) + { + CZombie* pZombie = static_cast< CZombie* >( IZombieAutoList::AutoList()[i] ); + if ( pZombie->GetTeamNumber() == me->GetTeamNumber() ) + { + continue; + } + + if ( !IsPotentiallyChaseable( me, pZombie ) ) + { + continue; + } + + float rangeSq = me->GetRangeSquaredTo( pZombie ); + if ( rangeSq < victimRangeSq ) + { + newVictim = pZombie; + victimRangeSq = rangeSq; + } + } + + if ( newVictim ) + { + // we have a new victim + m_attackTargetFocusTimer.Start( ZOMBIE_CHASE_MIN_DURATION ); + } + + m_attackTarget = newVictim; +} + + +//---------------------------------------------------------------------------------- +ActionResult< CZombie > CZombieAttack::Update( CZombie *me, float interval ) +{ + if ( !me->IsAlive() ) + { + return Done(); + } + + if ( !m_tauntTimer.IsElapsed() ) + { + // wait for taunt to finish + return Continue(); + } + + SelectVictim( me ); + + if ( m_attackTarget == NULL || !m_attackTarget->IsAlive() ) + { + return Continue(); + } + + // chase after our chase victim + const float standAndSwingRange = 50.0f; + + bool isLineOfSightClear = me->IsLineOfSightClear( m_attackTarget ); + + if ( me->IsRangeGreaterThan( m_attackTarget, standAndSwingRange ) || !isLineOfSightClear ) + { + if ( m_path.GetAge() > 0.5f ) + { + CZombiePathCost cost( me ); + m_path.Compute( me, m_attackTarget, cost ); + } + + m_path.Update( me ); + } + + // claw at attack target if they are in range + const float zombieSwingRange = 150.0f; + if ( me->IsRangeLessThan( m_attackTarget, zombieSwingRange ) ) + { + me->GetLocomotionInterface()->FaceTowards( m_attackTarget->WorldSpaceCenter() ); + + // swing! + if ( !me->IsPlayingGesture( ACT_MP_ATTACK_STAND_MELEE ) ) + { + me->AddGesture( ACT_MP_ATTACK_STAND_MELEE ); + } + + const float zombieAttackRange = me->GetAttackRange(); + if ( me->IsRangeLessThan( m_attackTarget, zombieAttackRange ) ) + { + if ( me->GetSkeletonType() == 1 && m_specialAttackTimer.IsElapsed() ) + { + m_specialAttackTimer.Start( RandomFloat( 5.f, 10.f ) ); + return SuspendFor( new CZombieSpecialAttack, "Do Special Attack!" ); + } + + if ( m_attackTimer.IsElapsed() ) + { + m_attackTimer.Start( RandomFloat( 0.8f, 1.2f ) ); + + Vector toVictim = m_attackTarget->WorldSpaceCenter() - me->WorldSpaceCenter(); + toVictim.NormalizeInPlace(); + + // hit! + CBaseEntity *pAttacker = me->GetOwnerEntity() ? me->GetOwnerEntity() : me; + CTakeDamageInfo info( pAttacker, pAttacker, me->GetAttackDamage(), DMG_SLASH ); + info.SetDamageCustom( TF_DMG_CUSTOM_SPELL_SKELETON ); + CalculateMeleeDamageForce( &info, toVictim, me->WorldSpaceCenter(), 5.0f ); + m_attackTarget->TakeDamage( info ); + } + } + } + + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_RUN_MELEE ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CZombie > CZombieAttack::OnStuck( CZombie *me ) +{ + // if we're stuck just die + CTakeDamageInfo info( me, me, 99999.9f, DMG_SLASH ); + me->TakeDamage( info ); + + return TryContinue( RESULT_TRY ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CZombie > CZombieAttack::OnContact( CZombie *me, CBaseEntity *other, CGameTrace *result ) +{ + if ( other->IsPlayer() ) + { + CTFPlayer *pTFPlayer = ToTFPlayer( other ); + if ( pTFPlayer ) + { + if ( pTFPlayer->IsAlive() && me->GetTeamNumber() != TF_TEAM_HALLOWEEN && me->GetTeamNumber() != pTFPlayer->GetTeamNumber() ) + { + // force attack the thing we bumped into + // this prevents us from being stuck on dispensers, for example + m_attackTarget = pTFPlayer; + m_attackTargetFocusTimer.Start( ZOMBIE_CHASE_MIN_DURATION ); + } + } + } + + return TryContinue( RESULT_TRY ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CZombie > CZombieAttack::OnOtherKilled( CZombie *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info ) +{ + /*if ( victim && victim->IsPlayer() && me->GetLocomotionInterface()->IsOnGround() ) + { + me->AddGestureSequence( me->LookupSequence( "taunt06" ) ); + m_tauntTimer.Start( 3.0f ); + }*/ + + return TryContinue( RESULT_TRY ); +} diff --git a/game/server/tf/halloween/zombie/zombie_behavior/zombie_attack.h b/game/server/tf/halloween/zombie/zombie_behavior/zombie_attack.h new file mode 100644 index 0000000..476ffc0 --- /dev/null +++ b/game/server/tf/halloween/zombie/zombie_behavior/zombie_attack.h @@ -0,0 +1,36 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef ZOMBIE_ATTACK_H +#define ZOMBIE_ATTACK_H + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CZombieAttack : public Action< CZombie > +{ +public: + virtual ActionResult< CZombie > OnStart( CZombie *me, Action< CZombie > *priorAction ); + virtual ActionResult< CZombie > Update( CZombie *me, float interval ); + + virtual EventDesiredResult< CZombie > OnStuck( CZombie *me ); + virtual EventDesiredResult< CZombie > OnContact( CZombie *me, CBaseEntity *other, CGameTrace *result = NULL ); + virtual EventDesiredResult< CZombie > OnOtherKilled( CZombie *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info ); + + virtual const char *GetName( void ) const { return "Attack"; } // return name of this action + +private: + PathFollower m_path; + + CHandle< CBaseCombatCharacter > m_attackTarget; + CountdownTimer m_attackTimer; + CountdownTimer m_specialAttackTimer; + CountdownTimer m_attackTargetFocusTimer; + CountdownTimer m_tauntTimer; + + bool IsPotentiallyChaseable( CZombie *me, CBaseCombatCharacter *victim ); + void SelectVictim( CZombie *me ); +}; + +#endif // ZOMBIE_ATTACK_H diff --git a/game/server/tf/halloween/zombie/zombie_behavior/zombie_spawn.cpp b/game/server/tf/halloween/zombie/zombie_behavior/zombie_spawn.cpp new file mode 100644 index 0000000..87d8e43 --- /dev/null +++ b/game/server/tf/halloween/zombie/zombie_behavior/zombie_spawn.cpp @@ -0,0 +1,29 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" + +#include "tf_shareddefs.h" +#include "../zombie.h" +#include "zombie_attack.h" +#include "zombie_spawn.h" + +ActionResult< CZombie > CZombieSpawn::OnStart( CZombie *me, Action< CZombie > *priorAction ) +{ + me->GetBodyInterface()->StartActivity( ACT_TRANSITION ); + + return Continue(); +} + + +ActionResult< CZombie > CZombieSpawn::Update( CZombie *me, float interval ) +{ + if ( me->IsActivityFinished() ) + { + return ChangeTo( new CZombieAttack, "Start Attack!" ); + } + + return Continue(); +}
\ No newline at end of file diff --git a/game/server/tf/halloween/zombie/zombie_behavior/zombie_spawn.h b/game/server/tf/halloween/zombie/zombie_behavior/zombie_spawn.h new file mode 100644 index 0000000..863fee6 --- /dev/null +++ b/game/server/tf/halloween/zombie/zombie_behavior/zombie_spawn.h @@ -0,0 +1,22 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef ZOMBIE_SPAWN_H +#define ZOMBIE_SPAWN_H + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CZombieSpawn : public Action< CZombie > +{ +public: + virtual ActionResult< CZombie > OnStart( CZombie *me, Action< CZombie > *priorAction ); + virtual ActionResult< CZombie > Update( CZombie *me, float interval ); + + virtual const char *GetName( void ) const { return "Spawn"; } // return name of this action + +private: +}; + +#endif // ZOMBIE_SPAWN_H diff --git a/game/server/tf/halloween/zombie/zombie_behavior/zombie_special_attack.cpp b/game/server/tf/halloween/zombie/zombie_behavior/zombie_special_attack.cpp new file mode 100644 index 0000000..ee2ca3c --- /dev/null +++ b/game/server/tf/halloween/zombie/zombie_behavior/zombie_special_attack.cpp @@ -0,0 +1,68 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" + +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_fx.h" + +#include "../zombie.h" +#include "zombie_special_attack.h" + +ActionResult< CZombie > CZombieSpecialAttack::OnStart( CZombie *me, Action< CZombie > *priorAction ) +{ + me->GetBodyInterface()->StartActivity( ACT_SPECIAL_ATTACK1 ); + + m_stompTimer.Start( 1 ); + + return Continue(); +} + + +ActionResult< CZombie > CZombieSpecialAttack::Update( CZombie *me, float interval ) +{ + if ( m_stompTimer.HasStarted() && m_stompTimer.IsElapsed() ) + { + DoSpecialAttack( me ); + m_stompTimer.Invalidate(); + } + + if ( me->IsActivityFinished() ) + { + return Done(); + } + + return Continue(); +} + + +void CZombieSpecialAttack::DoSpecialAttack( CZombie *me ) +{ + CPVSFilter filter( me->GetAbsOrigin() ); + TE_TFParticleEffect( filter, 0.0, "bomibomicon_ring", me->GetAbsOrigin(), vec3_angle ); + + int nTargetTeam = TEAM_ANY; + if ( me->GetTeamNumber() != TF_TEAM_HALLOWEEN ) + { + nTargetTeam = me->GetTeamNumber() == TF_TEAM_RED ? TF_TEAM_BLUE : TF_TEAM_RED; + } + + CUtlVector< CTFPlayer* > pushedPlayers; + TFGameRules()->PushAllPlayersAway( me->GetAbsOrigin(), 200.f, 500.f, nTargetTeam, &pushedPlayers ); + + CBaseEntity *pAttacker = me->GetOwnerEntity() ? me->GetOwnerEntity() : me; + for ( int i=0; i<pushedPlayers.Count(); ++i ) + { + Vector toVictim = pushedPlayers[i]->WorldSpaceCenter() - me->WorldSpaceCenter(); + toVictim.NormalizeInPlace(); + + // hit! + CTakeDamageInfo info( pAttacker, pAttacker, me->GetAttackDamage(), DMG_SLASH ); + info.SetDamageCustom( TF_DMG_CUSTOM_SPELL_SKELETON ); + CalculateMeleeDamageForce( &info, toVictim, me->WorldSpaceCenter(), 5.0f ); + pushedPlayers[i]->TakeDamage( info ); + } +} diff --git a/game/server/tf/halloween/zombie/zombie_behavior/zombie_special_attack.h b/game/server/tf/halloween/zombie/zombie_behavior/zombie_special_attack.h new file mode 100644 index 0000000..b8689fb --- /dev/null +++ b/game/server/tf/halloween/zombie/zombie_behavior/zombie_special_attack.h @@ -0,0 +1,25 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef ZOMBIE_SPECIAL_ATTACK_H +#define ZOMBIE_SPECIAL_ATTACK_H + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CZombieSpecialAttack : public Action< CZombie > +{ +public: + virtual ActionResult< CZombie > OnStart( CZombie *me, Action< CZombie > *priorAction ); + virtual ActionResult< CZombie > Update( CZombie *me, float interval ); + + virtual const char *GetName( void ) const { return "Special Attack"; } // return name of this action +private: + + void DoSpecialAttack( CZombie *me ); + + CountdownTimer m_stompTimer; +}; + +#endif // ZOMBIE_SPECIAL_ATTACK_H diff --git a/game/server/tf/halloween/zombie/zombie_body.cpp b/game/server/tf/halloween/zombie/zombie_body.cpp new file mode 100644 index 0000000..67b13c4 --- /dev/null +++ b/game/server/tf/halloween/zombie/zombie_body.cpp @@ -0,0 +1,118 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" + +#include "NextBot.h" +#include "zombie.h" +#include "zombie_body.h" + + +//------------------------------------------------------------------------------------------- +CZombieBody::CZombieBody( INextBot *bot ) : IBody( bot ) +{ + m_moveXPoseParameter = -1; + m_moveYPoseParameter = -1; + m_currentActivity = -1; +} + + +//------------------------------------------------------------------------------------------- +bool CZombieBody::StartActivity( Activity act, unsigned int flags ) +{ + CZombie *me = (CZombie *)GetBot()->GetEntity(); + + int animSequence = ::SelectWeightedSequence( me->GetModelPtr(), act, me->GetSequence() ); + + if ( animSequence ) + { + m_currentActivity = act; + me->SetSequence( animSequence ); + me->SetPlaybackRate( 1.0f ); + me->SetCycle( 0 ); + me->ResetSequenceInfo(); + + return true; + } + + return false; +} + + +//------------------------------------------------------------------------------------------- +void CZombieBody::Update( void ) +{ + CZombie *me = (CZombie *)GetBot()->GetEntity(); + + if ( m_moveXPoseParameter < 0 ) + { + m_moveXPoseParameter = me->LookupPoseParameter( "move_x" ); + } + + if ( m_moveYPoseParameter < 0 ) + { + m_moveYPoseParameter = me->LookupPoseParameter( "move_y" ); + } + + + // Update the pose parameters + float speed = me->GetLocomotionInterface()->GetGroundSpeed(); // me->GetAbsVelocity().Length(); + + if ( speed < 0.01f ) + { + // stopped + if ( m_moveXPoseParameter >= 0 ) + { + me->SetPoseParameter( m_moveXPoseParameter, 0.0f ); + } + + if ( m_moveYPoseParameter >= 0 ) + { + me->SetPoseParameter( m_moveYPoseParameter, 0.0f ); + } + } + else + { + Vector forward, right, up; + me->GetVectors( &forward, &right, &up ); + + const Vector &motionVector = me->GetLocomotionInterface()->GetGroundMotionVector(); + + // move_x == 1.0 at full forward motion and -1.0 in full reverse + if ( m_moveXPoseParameter >= 0 ) + { + float forwardVel = DotProduct( motionVector, forward ); + + me->SetPoseParameter( m_moveXPoseParameter, forwardVel ); + } + + if ( m_moveYPoseParameter >= 0 ) + { + float sideVel = DotProduct( motionVector, right ); + + me->SetPoseParameter( m_moveYPoseParameter, sideVel ); + } + } + + // adjust animation speed to actual movement speed + if ( me->m_flGroundSpeed > 0.0f ) + { + // Clamp playback rate to avoid datatable warnings. Anything faster would look silly, anyway. + float playbackRate = clamp( speed / me->m_flGroundSpeed, -4.f, 12.f ); + me->SetPlaybackRate( playbackRate ); + } + + // move the animation ahead in time + me->StudioFrameAdvance(); + me->DispatchAnimEvents( me ); +} + + +//--------------------------------------------------------------------------------------------- +// return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface) +unsigned int CZombieBody::GetSolidMask( void ) const +{ + return MASK_NPCSOLID | CONTENTS_PLAYERCLIP; +} diff --git a/game/server/tf/halloween/zombie/zombie_body.h b/game/server/tf/halloween/zombie/zombie_body.h new file mode 100644 index 0000000..ba7ffcf --- /dev/null +++ b/game/server/tf/halloween/zombie/zombie_body.h @@ -0,0 +1,51 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef ZOMBIE_BODY_H +#define ZOMBIE_BODY_H + +#include "animation.h" +#include "NextBotBodyInterface.h" + +class INextBot; + + +//---------------------------------------------------------------------------------------------------------------- +/** + * The interface for control and information about the bot's body state (posture, animation state, etc) + */ +class CZombieBody : public IBody +{ +public: + CZombieBody( INextBot *bot ); + virtual ~CZombieBody() { } + + virtual void Update( void ); + + virtual bool StartActivity( Activity act, unsigned int flags = 0 ); + virtual Activity GetActivity( void ) const; // return currently animating activity + virtual bool IsActivity( Activity act ) const; // return true if currently animating activity matches the given one + + virtual unsigned int GetSolidMask( void ) const; // return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface) + +private: + int m_currentActivity; + int m_moveXPoseParameter; + int m_moveYPoseParameter; +}; + + +inline Activity CZombieBody::GetActivity( void ) const +{ + return (Activity)m_currentActivity; +} + +inline bool CZombieBody::IsActivity( Activity act ) const +{ + return act == m_currentActivity ? true : false; +} + + +#endif // ZOMBIE_BODY_H diff --git a/game/server/tf/halloween/zombie/zombie_spawner.cpp b/game/server/tf/halloween/zombie/zombie_spawner.cpp new file mode 100644 index 0000000..cf0e3cb --- /dev/null +++ b/game/server/tf/halloween/zombie/zombie_spawner.cpp @@ -0,0 +1,86 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" + +#include "tf_shareddefs.h" +#include "zombie.h" +#include "zombie_spawner.h" + +LINK_ENTITY_TO_CLASS( tf_zombie_spawner, CZombieSpawner ); + +BEGIN_DATADESC( CZombieSpawner ) + DEFINE_KEYFIELD( m_flZombieLifeTime, FIELD_FLOAT, "zombie_lifetime" ), + DEFINE_KEYFIELD( m_nMaxActiveZombies, FIELD_INTEGER, "max_zombies" ), + DEFINE_KEYFIELD( m_bInfiniteZombies, FIELD_BOOLEAN, "infinite_zombies" ), + DEFINE_KEYFIELD( m_nSkeletonType, FIELD_INTEGER, "zombie_type" ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetMaxActiveZombies", InputSetMaxActiveZombies ), +END_DATADESC() + +CZombieSpawner::CZombieSpawner() +{ + m_bEnabled = false; + m_bInfiniteZombies = false; + m_nMaxActiveZombies = 1; + m_flZombieLifeTime = 0; + m_nSkeletonType = 0; + m_nSpawned = 0; +} + + +void CZombieSpawner::Spawn() +{ + BaseClass::Spawn(); + + SetNextThink( gpGlobals->curtime ); +} + + +void CZombieSpawner::Think() +{ + m_activeZombies.FindAndFastRemove( NULL ); + + if ( m_bEnabled && ( ( m_bInfiniteZombies && m_activeZombies.Count() < m_nMaxActiveZombies ) || ( !m_bInfiniteZombies && m_nSpawned < m_nMaxActiveZombies ) ) ) + { + CZombie *pZombie = CZombie::SpawnAtPos( GetAbsOrigin(), m_flZombieLifeTime, TF_TEAM_HALLOWEEN, NULL, (CZombie::SkeletonType_t)m_nSkeletonType ); + if ( pZombie ) + { + m_nSpawned++; + m_activeZombies.AddToTail( pZombie ); + } + + SetNextThink( gpGlobals->curtime + RandomFloat( 1.5f, 3.f ) ); + return; + } + + SetNextThink( gpGlobals->curtime + 0.2f ); +} + + +void CZombieSpawner::InputEnable( inputdata_t &inputdata ) +{ + m_bEnabled = true; + + SetNextThink( gpGlobals->curtime ); +} + + +void CZombieSpawner::InputDisable( inputdata_t &inputdata ) +{ + m_bEnabled = false; + m_nSpawned = 0; + + m_activeZombies.Purge(); + + SetNextThink( gpGlobals->curtime ); +} + + +void CZombieSpawner::InputSetMaxActiveZombies( inputdata_t &inputdata ) +{ + m_nMaxActiveZombies = inputdata.value.Int(); +} diff --git a/game/server/tf/halloween/zombie/zombie_spawner.h b/game/server/tf/halloween/zombie/zombie_spawner.h new file mode 100644 index 0000000..ebbac9f --- /dev/null +++ b/game/server/tf/halloween/zombie/zombie_spawner.h @@ -0,0 +1,35 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef ZOMBIE_SPAWN_H +#define ZOMBIE_SPAWN_H + +class CZombieSpawner : public CPointEntity +{ + DECLARE_CLASS( CZombieSpawner, CPointEntity ); + DECLARE_DATADESC(); +public: + CZombieSpawner(); + + virtual void Spawn(); + virtual void Think(); + + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + void InputSetMaxActiveZombies( inputdata_t &inputdata ); + +private: + bool m_bEnabled; + bool m_bInfiniteZombies; + int m_nMaxActiveZombies; + float m_flZombieLifeTime; + int m_nSkeletonType; + + int m_nSpawned; + + CUtlVector< CHandle< CZombie > > m_activeZombies; +}; + +#endif // ZOMBIE_SPAWN_H |