You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
599 lines
15 KiB
599 lines
15 KiB
//========= 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; |
|
}
|
|
|