summaryrefslogtreecommitdiff
path: root/game/server/tf/halloween
diff options
context:
space:
mode:
authorFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
committerFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
commit3bf9df6b2785fa6d951086978a3e66f49427166a (patch)
tree2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/server/tf/halloween
downloadarchived-source-engine-2018-hl2-src-master.tar.xz
archived-source-engine-2018-hl2-src-master.zip
Diffstat (limited to 'game/server/tf/halloween')
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_approach_target.cpp81
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_approach_target.h26
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_behavior.cpp201
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_behavior.h46
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_emerge.cpp172
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_emerge.h28
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_emote.cpp56
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_emote.h28
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_idle.cpp242
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_idle.h37
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_launch_rockets.cpp182
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_launch_rockets.h30
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_notice.cpp43
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_notice.h23
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_stunned.cpp60
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_stunned.h31
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_teleport.cpp148
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_behavior/eyeball_boss_teleport.h45
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_boss.cpp1050
-rw-r--r--game/server/tf/halloween/eyeball_boss/eyeball_boss.h338
-rw-r--r--game/server/tf/halloween/ghost/ghost.cpp331
-rw-r--r--game/server/tf/halloween/ghost/ghost.h86
-rw-r--r--game/server/tf/halloween/halloween_base_boss.cpp313
-rw-r--r--game/server/tf/halloween/halloween_base_boss.h109
-rw-r--r--game/server/tf/halloween/halloween_behavior/crybaby_boss_attack.cpp269
-rw-r--r--game/server/tf/halloween/halloween_behavior/crybaby_boss_attack.h47
-rw-r--r--game/server/tf/halloween/halloween_behavior/crybaby_boss_escape.cpp132
-rw-r--r--game/server/tf/halloween/halloween_behavior/crybaby_boss_escape.h28
-rw-r--r--game/server/tf/halloween/halloween_behavior/headless_hatman_attack.cpp543
-rw-r--r--game/server/tf/halloween/halloween_behavior/headless_hatman_attack.h54
-rw-r--r--game/server/tf/halloween/halloween_behavior/headless_hatman_dying.cpp44
-rw-r--r--game/server/tf/halloween/halloween_behavior/headless_hatman_dying.h20
-rw-r--r--game/server/tf/halloween/halloween_behavior/headless_hatman_emerge.cpp133
-rw-r--r--game/server/tf/halloween/halloween_behavior/headless_hatman_emerge.h26
-rw-r--r--game/server/tf/halloween/halloween_behavior/headless_hatman_terrify.cpp95
-rw-r--r--game/server/tf/halloween/halloween_behavior/headless_hatman_terrify.h26
-rw-r--r--game/server/tf/halloween/halloween_gift_spawn_locations.cpp292
-rw-r--r--game/server/tf/halloween/halloween_gift_spawn_locations.h16
-rw-r--r--game/server/tf/halloween/headless_hatman.cpp330
-rw-r--r--game/server/tf/halloween/headless_hatman.h205
-rw-r--r--game/server/tf/halloween/headless_hatman_body.cpp114
-rw-r--r--game/server/tf/halloween/headless_hatman_body.h47
-rw-r--r--game/server/tf/halloween/merasmus/merasmus.cpp1609
-rw-r--r--game/server/tf/halloween/merasmus/merasmus.h412
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_aoe_attack.cpp248
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_aoe_attack.h51
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_attack.cpp386
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_attack.h63
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_disguise.cpp226
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_disguise.h33
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_dying.cpp60
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_dying.h20
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_reveal.cpp38
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_reveal.h20
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_staff_attack.cpp117
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_staff_attack.h28
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_stunned.cpp91
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_stunned.h29
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_teleport.cpp189
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_teleport.h49
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_throwing_grenade.cpp159
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_throwing_grenade.h27
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_zap.cpp73
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_behavior/merasmus_zap.h32
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_body.cpp118
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_body.h51
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_dancer.cpp172
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_dancer.h49
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_trick_or_treat_prop.cpp178
-rw-r--r--game/server/tf/halloween/merasmus/merasmus_trick_or_treat_prop.h30
-rw-r--r--game/server/tf/halloween/spell/tf_spell_pickup.cpp111
-rw-r--r--game/server/tf/halloween/spell/tf_spell_pickup.h37
-rw-r--r--game/server/tf/halloween/zombie/zombie.cpp599
-rw-r--r--game/server/tf/halloween/zombie/zombie.h196
-rw-r--r--game/server/tf/halloween/zombie/zombie_behavior/zombie_attack.cpp303
-rw-r--r--game/server/tf/halloween/zombie/zombie_behavior/zombie_attack.h36
-rw-r--r--game/server/tf/halloween/zombie/zombie_behavior/zombie_spawn.cpp29
-rw-r--r--game/server/tf/halloween/zombie/zombie_behavior/zombie_spawn.h22
-rw-r--r--game/server/tf/halloween/zombie/zombie_behavior/zombie_special_attack.cpp68
-rw-r--r--game/server/tf/halloween/zombie/zombie_behavior/zombie_special_attack.h25
-rw-r--r--game/server/tf/halloween/zombie/zombie_body.cpp118
-rw-r--r--game/server/tf/halloween/zombie/zombie_body.h51
-rw-r--r--game/server/tf/halloween/zombie/zombie_spawner.cpp86
-rw-r--r--game/server/tf/halloween/zombie/zombie_spawner.h35
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