1190 lines
32 KiB
C++
1190 lines
32 KiB
C++
// Swarm Civilian corrupted by SynTek chemicals into being a crazed zombie like thing
|
|
#include "cbase.h"
|
|
|
|
#include "doors.h"
|
|
|
|
#include "simtimer.h"
|
|
#include "npc_BaseZombie.h"
|
|
#include "ai_hull.h"
|
|
#include "ai_navigator.h"
|
|
#include "ai_memory.h"
|
|
#include "gib.h"
|
|
#include "soundenvelope.h"
|
|
#include "engine/IEngineSound.h"
|
|
#include "asw_zombie.h"
|
|
#include "asw_fx_shared.h"
|
|
#include "EntityFlame.h"
|
|
#include "asw_zombie.h"
|
|
#include "asw_gamerules.h"
|
|
#include "asw_burning.h"
|
|
|
|
// memdbgon must be the last include file in a .cpp file!!!
|
|
#include "tier0/memdbgon.h"
|
|
|
|
// ACT_FLINCH_PHYSICS
|
|
|
|
extern ConVar showhitlocation;
|
|
ConVar sk_asw_zombie_health( "sk_asw_zombie_health","100");
|
|
ConVar asw_zombie_eye_glow("asw_zombie_eye_glow", "1");
|
|
// asw - how much extra damage to do to burning aliens
|
|
ConVar asw_fire_zombie_damage_scale("asw_fire_zombie_damage_scale", "3.0", FCVAR_CHEAT );
|
|
|
|
#define ZOMBIE_GLOW_SPRITE "sprites/glow1.vmt"
|
|
|
|
envelopePoint_t envASWZombieMoanVolumeFast[] =
|
|
{
|
|
{ 7.0f, 7.0f,
|
|
0.1f, 0.1f,
|
|
},
|
|
{ 0.0f, 0.0f,
|
|
0.2f, 0.3f,
|
|
},
|
|
};
|
|
|
|
envelopePoint_t envASWZombieMoanVolume[] =
|
|
{
|
|
{ 1.0f, 1.0f,
|
|
0.1f, 0.1f,
|
|
},
|
|
{ 1.0f, 1.0f,
|
|
0.2f, 0.2f,
|
|
},
|
|
{ 0.0f, 0.0f,
|
|
0.3f, 0.4f,
|
|
},
|
|
};
|
|
|
|
envelopePoint_t envASWZombieMoanVolumeLong[] =
|
|
{
|
|
{ 1.0f, 1.0f,
|
|
0.3f, 0.5f,
|
|
},
|
|
{ 1.0f, 1.0f,
|
|
0.6f, 1.0f,
|
|
},
|
|
{ 0.0f, 0.0f,
|
|
0.3f, 0.4f,
|
|
},
|
|
};
|
|
|
|
envelopePoint_t envASWZombieMoanIgnited[] =
|
|
{
|
|
{ 1.0f, 1.0f,
|
|
0.5f, 1.0f,
|
|
},
|
|
{ 1.0f, 1.0f,
|
|
30.0f, 30.0f,
|
|
},
|
|
{ 0.0f, 0.0f,
|
|
0.5f, 1.0f,
|
|
},
|
|
};
|
|
|
|
|
|
//=============================================================================
|
|
//=============================================================================
|
|
|
|
LINK_ENTITY_TO_CLASS( asw_zombie, CASW_Zombie );
|
|
// asw: no placing just torsos
|
|
//LINK_ENTITY_TO_CLASS( asw_zombie_torso, CASW_Zombie );
|
|
|
|
//---------------------------------------------------------
|
|
//---------------------------------------------------------
|
|
const char *CASW_Zombie::pASWMoanSounds[] =
|
|
{
|
|
"ASW_Zombie.Moan1",
|
|
"ASW_Zombie.Moan2",
|
|
"ASW_Zombie.Moan3",
|
|
"ASW_Zombie.Moan4",
|
|
};
|
|
|
|
//=========================================================
|
|
// Conditions
|
|
//=========================================================
|
|
enum
|
|
{
|
|
COND_ASW_BLOCKED_BY_DOOR = LAST_BASE_ZOMBIE_CONDITION,
|
|
COND_ASW_DOOR_OPENED,
|
|
COND_ASW_ZOMBIE_CHARGE_TARGET_MOVED,
|
|
};
|
|
|
|
//=========================================================
|
|
// Schedules
|
|
//=========================================================
|
|
enum
|
|
{
|
|
SCHED_ASW_ZOMBIE_BASH_DOOR = LAST_BASE_ZOMBIE_SCHEDULE,
|
|
SCHED_ASW_ZOMBIE_WANDER_ANGRILY,
|
|
SCHED_ASW_ZOMBIE_CHARGE_ENEMY,
|
|
SCHED_ASW_ZOMBIE_FAIL,
|
|
};
|
|
|
|
//=========================================================
|
|
// Tasks
|
|
//=========================================================
|
|
enum
|
|
{
|
|
TASK_ASW_ZOMBIE_EXPRESS_ANGER = LAST_BASE_ZOMBIE_TASK,
|
|
TASK_ASW_ZOMBIE_YAW_TO_DOOR,
|
|
TASK_ASW_ZOMBIE_ATTACK_DOOR,
|
|
TASK_ASW_ZOMBIE_CHARGE_ENEMY,
|
|
};
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
int ACT_ASW_ZOMBIE_TANTRUM;
|
|
int ACT_ASW_ZOMBIE_WALLPOUND;
|
|
|
|
IMPLEMENT_SERVERCLASS_ST(CASW_Zombie, DT_ASW_Zombie)
|
|
SendPropBool(SENDINFO(m_bOnFire)),
|
|
END_SEND_TABLE()
|
|
|
|
BEGIN_DATADESC( CASW_Zombie )
|
|
DEFINE_FIELD( m_hBlockingDoor, FIELD_EHANDLE ),
|
|
DEFINE_FIELD( m_flDoorBashYaw, FIELD_FLOAT ),
|
|
DEFINE_EMBEDDED( m_DurationDoorBash ),
|
|
DEFINE_EMBEDDED( m_NextTimeToStartDoorBash ),
|
|
DEFINE_FIELD( m_vPositionCharged, FIELD_POSITION_VECTOR ),
|
|
DEFINE_FIELD( m_hSpawner, FIELD_EHANDLE ),
|
|
DEFINE_FIELD( m_bOnFire, FIELD_BOOLEAN ),
|
|
DEFINE_FIELD( m_bHoldoutAlien, FIELD_BOOLEAN ),
|
|
END_DATADESC()
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CASW_Zombie::Precache( void )
|
|
{
|
|
BaseClass::Precache();
|
|
|
|
PrecacheModel( "models/Swarm/CivilianZ/CivilianZ.mdl" );
|
|
PrecacheModel( "models/zombie/classic_torso.mdl" );
|
|
PrecacheModel( "models/zombie/classic_legs.mdl" );
|
|
|
|
PrecacheScriptSound( "Zombie.FootstepRight" );
|
|
PrecacheScriptSound( "Zombie.FootstepLeft" );
|
|
PrecacheScriptSound( "Zombie.FootstepLeft" );
|
|
PrecacheScriptSound( "Zombie.ScuffRight" );
|
|
PrecacheScriptSound( "Zombie.ScuffLeft" );
|
|
PrecacheScriptSound( "Zombie.AttackHit" );
|
|
PrecacheScriptSound( "Zombie.AttackMiss" );
|
|
PrecacheScriptSound( "Zombie.Pain" );
|
|
PrecacheScriptSound( "Zombie.Die" );
|
|
PrecacheScriptSound( "Zombie.Alert" );
|
|
PrecacheScriptSound( "Zombie.Idle" );
|
|
PrecacheScriptSound( "Zombie.Attack" );
|
|
|
|
PrecacheScriptSound( "NPC_BaseZombie.Moan1" );
|
|
PrecacheScriptSound( "NPC_BaseZombie.Moan2" );
|
|
PrecacheScriptSound( "NPC_BaseZombie.Moan3" );
|
|
PrecacheScriptSound( "NPC_BaseZombie.Moan4" );
|
|
|
|
PrecacheScriptSound( "ASW_Zombie.Pain" );
|
|
PrecacheScriptSound( "ASW_Zombie.Die" );
|
|
PrecacheScriptSound( "ASW_Zombie.Alert" );
|
|
PrecacheScriptSound( "ASW_Zombie.Idle" );
|
|
PrecacheScriptSound( "ASW_Zombie.Attack" );
|
|
PrecacheScriptSound( "ASW_Zombie.Moan1" );
|
|
PrecacheScriptSound( "ASW_Zombie.Moan2" );
|
|
PrecacheScriptSound( "ASW_Zombie.Moan3" );
|
|
PrecacheScriptSound( "ASW_Zombie.Moan4" );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//-----------------------------------------------------------------------------
|
|
void CASW_Zombie::Spawn( void )
|
|
{
|
|
Precache();
|
|
|
|
// asw: no placing just torsos
|
|
//if( FClassnameIs( this, "asw_zombie" ) )
|
|
//{
|
|
m_fIsTorso = false;
|
|
//}
|
|
//else
|
|
//{
|
|
// This was placed as an npc_zombie_torso
|
|
//m_fIsTorso = true;
|
|
//}
|
|
|
|
|
|
m_fIsHeadless = false;
|
|
|
|
SetBloodColor( BLOOD_COLOR_RED );
|
|
SetHealthByDifficultyLevel();
|
|
m_flFieldOfView = 0.2;
|
|
|
|
SetDistLook( 768.0f );
|
|
m_flDistTooFar = 1024.0f;
|
|
if ( HasSpawnFlags( SF_NPC_LONG_RANGE ) )
|
|
{
|
|
m_flDistTooFar = 2248.0f;
|
|
SetDistLook( 1200.0f );
|
|
}
|
|
|
|
CapabilitiesClear();
|
|
|
|
//GetNavigator()->SetRememberStaleNodes( false );
|
|
|
|
BaseClass::Spawn();
|
|
|
|
m_flNextMoanSound = gpGlobals->curtime + random->RandomFloat( 1.0, 4.0 );
|
|
|
|
if (asw_zombie_eye_glow.GetBool())
|
|
CreateEyeGlows();
|
|
}
|
|
|
|
void CASW_Zombie::UpdateOnRemove( void )
|
|
{
|
|
RemoveEyeGlows(0.0f);
|
|
BaseClass::UpdateOnRemove();
|
|
}
|
|
|
|
// give our zombies 360 degree vision
|
|
bool CASW_Zombie::FInViewCone( const Vector &vecSpot )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// player (and player controlled marines) always avoid zombies
|
|
bool CASW_Zombie::ShouldPlayerAvoid( void )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
float CASW_Zombie::GetIdealSpeed() const
|
|
{
|
|
return BaseClass::GetIdealSpeed() * m_flPlaybackRate;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//-----------------------------------------------------------------------------
|
|
void CASW_Zombie::PrescheduleThink( void )
|
|
{
|
|
if( gpGlobals->curtime > m_flNextMoanSound )
|
|
{
|
|
if( CanPlayMoanSound() )
|
|
{
|
|
// Classic guy idles instead of moans.
|
|
IdleSound();
|
|
|
|
m_flNextMoanSound = gpGlobals->curtime + random->RandomFloat( 2.0, 5.0 );
|
|
}
|
|
else
|
|
{
|
|
m_flNextMoanSound = gpGlobals->curtime + random->RandomFloat( 1.0, 2.0 );
|
|
}
|
|
}
|
|
|
|
BaseClass::PrescheduleThink();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//-----------------------------------------------------------------------------
|
|
int CASW_Zombie::SelectSchedule ( void )
|
|
{
|
|
if( HasCondition( COND_PHYSICS_DAMAGE ) && !m_ActBusyBehavior.IsActive() )
|
|
{
|
|
return SCHED_FLINCH_PHYSICS;
|
|
}
|
|
|
|
return BaseClass::SelectSchedule();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Sound of a footstep
|
|
//-----------------------------------------------------------------------------
|
|
void CASW_Zombie::FootstepSound( bool fRightFoot )
|
|
{
|
|
if( fRightFoot )
|
|
{
|
|
EmitSound( "Zombie.FootstepRight" );
|
|
}
|
|
else
|
|
{
|
|
EmitSound( "Zombie.FootstepLeft" );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Sound of a foot sliding/scraping
|
|
//-----------------------------------------------------------------------------
|
|
void CASW_Zombie::FootscuffSound( bool fRightFoot )
|
|
{
|
|
if( fRightFoot )
|
|
{
|
|
EmitSound( "Zombie.ScuffRight" );
|
|
}
|
|
else
|
|
{
|
|
EmitSound( "Zombie.ScuffLeft" );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Play a random attack hit sound
|
|
//-----------------------------------------------------------------------------
|
|
void CASW_Zombie::AttackHitSound( void )
|
|
{
|
|
EmitSound( "Zombie.AttackHit" );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Play a random attack miss sound
|
|
//-----------------------------------------------------------------------------
|
|
void CASW_Zombie::AttackMissSound( void )
|
|
{
|
|
// Play a random attack miss sound
|
|
EmitSound( "Zombie.AttackMiss" );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CASW_Zombie::PainSound( const CTakeDamageInfo &info )
|
|
{
|
|
// We're constantly taking damage when we are on fire. Don't make all those noises!
|
|
if ( IsOnFire() )
|
|
{
|
|
return;
|
|
}
|
|
|
|
EmitSound( "ASW_Zombie.Pain" );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//-----------------------------------------------------------------------------
|
|
void CASW_Zombie::DeathSound()
|
|
{
|
|
EmitSound( "ASW_Zombie.Die" );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CASW_Zombie::AlertSound( void )
|
|
{
|
|
EmitSound( "ASW_Zombie.Alert" );
|
|
|
|
// Don't let a moan sound cut off the alert sound.
|
|
m_flNextMoanSound += random->RandomFloat( 2.0, 4.0 );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Returns a moan sound for this class of zombie.
|
|
//-----------------------------------------------------------------------------
|
|
const char *CASW_Zombie::GetMoanSound( int nSound )
|
|
{
|
|
return pASWMoanSounds[ nSound % ARRAYSIZE( pASWMoanSounds ) ];
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Play a random idle sound.
|
|
//-----------------------------------------------------------------------------
|
|
void CASW_Zombie::IdleSound( void )
|
|
{
|
|
if( GetState() == NPC_STATE_IDLE && random->RandomFloat( 0, 1 ) == 0 )
|
|
{
|
|
// Moan infrequently in IDLE state.
|
|
return;
|
|
}
|
|
|
|
if( IsSlumped() )
|
|
{
|
|
// Sleeping zombies are quiet.
|
|
return;
|
|
}
|
|
|
|
EmitSound( "ASW_Zombie.Idle" );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Play a random attack sound.
|
|
//-----------------------------------------------------------------------------
|
|
void CASW_Zombie::AttackSound( void )
|
|
{
|
|
EmitSound( "ASW_Zombie.Attack" );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Returns the classname (ie "npc_headcrab") to spawn when our headcrab bails.
|
|
//-----------------------------------------------------------------------------
|
|
const char *CASW_Zombie::GetHeadcrabClassname( void )
|
|
{
|
|
return "npc_headcrab";
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//-----------------------------------------------------------------------------
|
|
const char *CASW_Zombie::GetHeadcrabModel( void )
|
|
{
|
|
return "models/headcrabclassic.mdl";
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
const char *CASW_Zombie::GetLegsModel( void )
|
|
{
|
|
return "models/zombie/classic_legs.mdl";
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//-----------------------------------------------------------------------------
|
|
const char *CASW_Zombie::GetTorsoModel( void )
|
|
{
|
|
return "models/zombie/classic_torso.mdl";
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
//---------------------------------------------------------
|
|
void CASW_Zombie::SetZombieModel( void )
|
|
{
|
|
Hull_t lastHull = GetHullType();
|
|
|
|
// asw: no placing just torsos
|
|
//if ( m_fIsTorso )
|
|
//{
|
|
//SetModel( "models/zombie/classic_torso.mdl" );
|
|
//SetHullType( HULL_TINY );
|
|
//}
|
|
//else
|
|
//{
|
|
SetModel( "models/Swarm/CivilianZ/CivilianZ.mdl" );
|
|
SetHullType( HULL_HUMAN );
|
|
//}
|
|
|
|
//SetBodygroup( ZOMBIE_BODYGROUP_HEADCRAB, !m_fIsHeadless );
|
|
|
|
SetHullSizeNormal( true );
|
|
SetDefaultEyeOffset();
|
|
SetActivity( ACT_IDLE );
|
|
|
|
// hull changed size, notify vphysics
|
|
// UNDONE: Solve this generally, systematically so other
|
|
// NPCs can change size
|
|
if ( lastHull != GetHullType() )
|
|
{
|
|
if ( VPhysicsGetObject() )
|
|
{
|
|
SetupVPhysicsHull();
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// Classic zombie only uses moan sound if on fire.
|
|
//---------------------------------------------------------
|
|
void CASW_Zombie::MoanSound( envelopePoint_t *pEnvelope, int iEnvelopeSize )
|
|
{
|
|
if( IsOnFire() )
|
|
{
|
|
BaseClass::MoanSound( pEnvelope, iEnvelopeSize );
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
//---------------------------------------------------------
|
|
bool CASW_Zombie::ShouldBecomeTorso( const CTakeDamageInfo &info, float flDamageThreshold )
|
|
{
|
|
// asw: no placing just torsos
|
|
return false;
|
|
|
|
if( IsSlumped() )
|
|
{
|
|
// Never break apart a slouched zombie. This is because the most fun
|
|
// slouched zombies to kill are ones sleeping leaning against explosive
|
|
// barrels. If you break them in half in the blast, the force of being
|
|
// so close to the explosion makes the body pieces fly at ridiculous
|
|
// velocities because the pieces weigh less than the whole.
|
|
return false;
|
|
}
|
|
|
|
return BaseClass::ShouldBecomeTorso( info, flDamageThreshold );
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
//---------------------------------------------------------
|
|
void CASW_Zombie::GatherConditions( void )
|
|
{
|
|
BaseClass::GatherConditions();
|
|
|
|
static int conditionsToClear[] =
|
|
{
|
|
COND_ASW_BLOCKED_BY_DOOR,
|
|
COND_ASW_DOOR_OPENED,
|
|
COND_ASW_ZOMBIE_CHARGE_TARGET_MOVED,
|
|
};
|
|
|
|
ClearConditions( conditionsToClear, ARRAYSIZE( conditionsToClear ) );
|
|
|
|
if ( m_hBlockingDoor == NULL ||
|
|
( m_hBlockingDoor->m_toggle_state == TS_AT_TOP ||
|
|
m_hBlockingDoor->m_toggle_state == TS_GOING_UP ) )
|
|
{
|
|
ClearCondition( COND_ASW_BLOCKED_BY_DOOR );
|
|
if ( m_hBlockingDoor != NULL )
|
|
{
|
|
SetCondition( COND_ASW_DOOR_OPENED );
|
|
m_hBlockingDoor = NULL;
|
|
}
|
|
}
|
|
else
|
|
SetCondition( COND_ASW_BLOCKED_BY_DOOR );
|
|
|
|
if ( ConditionInterruptsCurSchedule( COND_ASW_ZOMBIE_CHARGE_TARGET_MOVED ) )
|
|
{
|
|
if ( GetNavigator()->IsGoalActive() )
|
|
{
|
|
const float CHARGE_RESET_TOLERANCE = 60.0;
|
|
if ( !GetEnemy() ||
|
|
( m_vPositionCharged - GetEnemyLKP() ).Length() > CHARGE_RESET_TOLERANCE )
|
|
{
|
|
SetCondition( COND_ASW_ZOMBIE_CHARGE_TARGET_MOVED );
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
//---------------------------------------------------------
|
|
|
|
int CASW_Zombie::SelectFailSchedule( int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode )
|
|
{
|
|
if ( HasCondition( COND_ASW_BLOCKED_BY_DOOR ) && m_hBlockingDoor != NULL )
|
|
{
|
|
ClearCondition( COND_ASW_BLOCKED_BY_DOOR );
|
|
if ( m_NextTimeToStartDoorBash.Expired() && failedSchedule != SCHED_ASW_ZOMBIE_BASH_DOOR )
|
|
return SCHED_ASW_ZOMBIE_BASH_DOOR;
|
|
m_hBlockingDoor = NULL;
|
|
}
|
|
|
|
if ( failedSchedule != SCHED_ASW_ZOMBIE_CHARGE_ENEMY &&
|
|
IsPathTaskFailure( taskFailCode ) &&
|
|
random->RandomInt( 1, 100 ) < 50 )
|
|
{
|
|
return SCHED_ASW_ZOMBIE_CHARGE_ENEMY;
|
|
}
|
|
|
|
if ( failedSchedule != SCHED_ASW_ZOMBIE_WANDER_ANGRILY &&
|
|
( failedSchedule == SCHED_TAKE_COVER_FROM_ENEMY ||
|
|
failedSchedule == SCHED_CHASE_ENEMY_FAILED ) )
|
|
{
|
|
return SCHED_ASW_ZOMBIE_WANDER_ANGRILY;
|
|
}
|
|
|
|
return BaseClass::SelectFailSchedule( failedSchedule, failedTask, taskFailCode );
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
//---------------------------------------------------------
|
|
|
|
int CASW_Zombie::TranslateSchedule( int scheduleType )
|
|
{
|
|
if ( scheduleType == SCHED_COMBAT_FACE && IsUnreachable( GetEnemy() ) )
|
|
return SCHED_TAKE_COVER_FROM_ENEMY;
|
|
|
|
if ( !m_fIsTorso && scheduleType == SCHED_FAIL )
|
|
return SCHED_ASW_ZOMBIE_FAIL;
|
|
|
|
return BaseClass::TranslateSchedule( scheduleType );
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
|
|
Activity CASW_Zombie::NPC_TranslateActivity( Activity newActivity )
|
|
{
|
|
newActivity = BaseClass::NPC_TranslateActivity( newActivity );
|
|
|
|
if ( newActivity == ACT_RUN )
|
|
return ACT_WALK;
|
|
|
|
return newActivity;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
//---------------------------------------------------------
|
|
void CASW_Zombie::OnStateChange( NPC_STATE OldState, NPC_STATE NewState )
|
|
{
|
|
BaseClass::OnStateChange( OldState, NewState );
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
//---------------------------------------------------------
|
|
|
|
void CASW_Zombie::StartTask( const Task_t *pTask )
|
|
{
|
|
switch( pTask->iTask )
|
|
{
|
|
case TASK_ASW_ZOMBIE_EXPRESS_ANGER:
|
|
{
|
|
if ( random->RandomInt( 1, 4 ) == 2 )
|
|
{
|
|
SetIdealActivity( (Activity)ACT_ASW_ZOMBIE_TANTRUM );
|
|
}
|
|
else
|
|
{
|
|
TaskComplete();
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case TASK_ASW_ZOMBIE_YAW_TO_DOOR:
|
|
{
|
|
AssertMsg( m_hBlockingDoor != NULL, "Expected condition handling to break schedule before landing here" );
|
|
if ( m_hBlockingDoor != NULL )
|
|
{
|
|
GetMotor()->SetIdealYaw( m_flDoorBashYaw );
|
|
}
|
|
TaskComplete();
|
|
break;
|
|
}
|
|
|
|
case TASK_ASW_ZOMBIE_ATTACK_DOOR:
|
|
{
|
|
m_DurationDoorBash.Reset();
|
|
SetIdealActivity( SelectDoorBash() );
|
|
break;
|
|
}
|
|
|
|
case TASK_ASW_ZOMBIE_CHARGE_ENEMY:
|
|
{
|
|
if ( !GetEnemy() )
|
|
TaskFail( FAIL_NO_ENEMY );
|
|
else if ( GetNavigator()->SetVectorGoalFromTarget( GetEnemy()->GetLocalOrigin() ) )
|
|
{
|
|
m_vPositionCharged = GetEnemy()->GetLocalOrigin();
|
|
TaskComplete();
|
|
}
|
|
else
|
|
TaskFail( FAIL_NO_ROUTE );
|
|
break;
|
|
}
|
|
|
|
default:
|
|
BaseClass::StartTask( pTask );
|
|
break;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
//---------------------------------------------------------
|
|
|
|
void CASW_Zombie::RunTask( const Task_t *pTask )
|
|
{
|
|
switch( pTask->iTask )
|
|
{
|
|
case TASK_ASW_ZOMBIE_ATTACK_DOOR:
|
|
{
|
|
if ( IsActivityFinished() )
|
|
{
|
|
if ( m_DurationDoorBash.Expired() )
|
|
{
|
|
TaskComplete();
|
|
m_NextTimeToStartDoorBash.Reset();
|
|
}
|
|
else
|
|
ResetIdealActivity( SelectDoorBash() );
|
|
}
|
|
break;
|
|
}
|
|
|
|
case TASK_ASW_ZOMBIE_CHARGE_ENEMY:
|
|
{
|
|
break;
|
|
}
|
|
|
|
case TASK_ASW_ZOMBIE_EXPRESS_ANGER:
|
|
{
|
|
if ( IsActivityFinished() )
|
|
{
|
|
TaskComplete();
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
BaseClass::RunTask( pTask );
|
|
break;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
//---------------------------------------------------------
|
|
|
|
bool CASW_Zombie::OnObstructingDoor( AILocalMoveGoal_t *pMoveGoal, CBaseDoor *pDoor,
|
|
float distClear, AIMoveResult_t *pResult )
|
|
{
|
|
if ( BaseClass::OnObstructingDoor( pMoveGoal, pDoor, distClear, pResult ) )
|
|
{
|
|
if ( IsMoveBlocked( *pResult ) && pMoveGoal->directTrace.vHitNormal != vec3_origin )
|
|
{
|
|
m_hBlockingDoor = pDoor;
|
|
m_flDoorBashYaw = UTIL_VecToYaw( pMoveGoal->directTrace.vHitNormal * -1 );
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
//---------------------------------------------------------
|
|
|
|
Activity CASW_Zombie::SelectDoorBash()
|
|
{
|
|
if ( random->RandomInt( 1, 3 ) == 1 )
|
|
return ACT_MELEE_ATTACK1;
|
|
return (Activity)ACT_ASW_ZOMBIE_WALLPOUND;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// Zombies should scream continuously while burning, so long
|
|
// as they are alive.
|
|
//---------------------------------------------------------
|
|
void CASW_Zombie::Ignite( float flFlameLifetime, bool bNPCOnly, float flSize, bool bCalledByLevelDesigner )
|
|
{
|
|
return; // asw asw_ignite instead
|
|
/*
|
|
if( !IsOnFire() && IsAlive() )
|
|
{
|
|
BaseClass::Ignite( flFlameLifetime, bNPCOnly, flSize, bCalledByLevelDesigner );
|
|
|
|
RemoveSpawnFlags( SF_NPC_GAG );
|
|
|
|
MoanSound( envASWZombieMoanIgnited, ARRAYSIZE( envASWZombieMoanIgnited ) );
|
|
|
|
if( m_pMoanSound )
|
|
{
|
|
ENVELOPE_CONTROLLER.SoundChangePitch( m_pMoanSound, 120, 1.0 );
|
|
ENVELOPE_CONTROLLER.SoundChangeVolume( m_pMoanSound, 1, 1.0 );
|
|
}
|
|
}*/
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
//---------------------------------------------------------
|
|
int CASW_Zombie::OnTakeDamage_Alive( const CTakeDamageInfo &info )
|
|
{
|
|
#ifndef HL2_EPISODIC
|
|
if ( info.GetDamageType() & DMG_BUCKSHOT )
|
|
{
|
|
if( !m_fIsTorso && info.GetDamage() > (m_iMaxHealth/3) )
|
|
{
|
|
// Always flinch if damaged a lot by buckshot, even if not shot in the head.
|
|
// The reason for making sure we did at least 1/3rd of the zombie's max health
|
|
// is so the zombie doesn't flinch every time the odd shotgun pellet hits them,
|
|
// and so the maximum number of times you'll see a zombie flinch like this is 2.(sjb)
|
|
AddGesture( ACT_GESTURE_FLINCH_HEAD );
|
|
}
|
|
}
|
|
#endif // HL2_EPISODIC
|
|
// catching on fire
|
|
int result = 0;
|
|
|
|
// scale burning damage up
|
|
if (dynamic_cast<CEntityFlame*>(info.GetAttacker()))
|
|
{
|
|
CTakeDamageInfo newDamage = info;
|
|
newDamage.ScaleDamage(asw_fire_zombie_damage_scale.GetFloat());
|
|
result = BaseClass::OnTakeDamage_Alive(newDamage);
|
|
}
|
|
else
|
|
{
|
|
result = BaseClass::OnTakeDamage_Alive(info);
|
|
}
|
|
|
|
// if we take fire damage, catch on fire
|
|
if (result > 0 && (info.GetDamageType() & DMG_BURN))
|
|
ASW_Ignite(30.0f, 0, info.GetAttacker());
|
|
|
|
//CASW_Marine* pMarine = dynamic_cast<CASW_Marine*>(info.GetAttacker());
|
|
//if (pMarine)
|
|
//pMarine->HurtAlien(this);
|
|
|
|
return BaseClass::OnTakeDamage_Alive( info );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//-----------------------------------------------------------------------------
|
|
bool CASW_Zombie::IsHeavyDamage( const CTakeDamageInfo &info )
|
|
{
|
|
#ifdef HL2_EPISODIC
|
|
if ( info.GetDamageType() & DMG_BUCKSHOT )
|
|
{
|
|
if ( !m_fIsTorso && info.GetDamage() > (m_iMaxHealth/3) )
|
|
return true;
|
|
}
|
|
|
|
// Randomly treat all damage as heavy
|
|
if ( info.GetDamageType() & (DMG_BULLET | DMG_BUCKSHOT) )
|
|
{
|
|
// Don't randomly flinch if I'm melee attacking
|
|
if ( !HasCondition(COND_CAN_MELEE_ATTACK1) && (RandomFloat() > 0.5) )
|
|
{
|
|
// Randomly forget I've flinched, so that I'll be forced to play a big flinch
|
|
// If this doesn't happen, it means I may not fully flinch if I recently flinched
|
|
if ( RandomFloat() > 0.75 )
|
|
{
|
|
Forget(bits_MEMORY_FLINCHED);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
#endif // HL2_EPISODIC
|
|
|
|
return BaseClass::IsHeavyDamage(info);
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
//---------------------------------------------------------
|
|
void CASW_Zombie::BuildScheduleTestBits( void )
|
|
{
|
|
BaseClass::BuildScheduleTestBits();
|
|
|
|
if( !m_fIsTorso && !IsCurSchedule( SCHED_FLINCH_PHYSICS ) && !m_ActBusyBehavior.IsActive() )
|
|
{
|
|
SetCustomInterruptCondition( COND_PHYSICS_DAMAGE );
|
|
}
|
|
}
|
|
|
|
HeadcrabRelease_t CASW_Zombie::ShouldReleaseHeadcrab( const CTakeDamageInfo &info, float flDamageThreshold )
|
|
{
|
|
return RELEASE_NO;
|
|
}
|
|
|
|
void CASW_Zombie::StudioFrameAdvance()
|
|
{
|
|
// vary out playback rate for crazy zombie-ness
|
|
//m_flPlaybackRate = RandomFloat(2.0f, 3.0f);
|
|
m_flPlaybackRate = 1.5f;
|
|
BaseClass::StudioFrameAdvance();
|
|
}
|
|
|
|
// make the bleeding more pronounced
|
|
void CASW_Zombie::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr )
|
|
{
|
|
m_fNoDamageDecal = false;
|
|
if ( m_takedamage == DAMAGE_NO )
|
|
return;
|
|
|
|
CTakeDamageInfo subInfo = info;
|
|
|
|
SetLastHitGroup( ptr->hitgroup );
|
|
m_nForceBone = ptr->physicsbone; // save this bone for physics forces
|
|
|
|
Assert( m_nForceBone > -255 && m_nForceBone < 256 );
|
|
|
|
bool bDebug = showhitlocation.GetBool();
|
|
|
|
switch ( ptr->hitgroup )
|
|
{
|
|
case HITGROUP_GENERIC:
|
|
if( bDebug ) DevMsg("Hit Location: Generic\n");
|
|
break;
|
|
|
|
// hit gear, react but don't bleed
|
|
case HITGROUP_GEAR:
|
|
subInfo.SetDamage( 0.01 );
|
|
ptr->hitgroup = HITGROUP_GENERIC;
|
|
if( bDebug ) DevMsg("Hit Location: Gear\n");
|
|
break;
|
|
|
|
case HITGROUP_HEAD:
|
|
subInfo.ScaleDamage( GetHitgroupDamageMultiplier(ptr->hitgroup, info) );
|
|
if( bDebug ) DevMsg("Hit Location: Head\n");
|
|
break;
|
|
|
|
case HITGROUP_CHEST:
|
|
subInfo.ScaleDamage( GetHitgroupDamageMultiplier(ptr->hitgroup, info) );
|
|
if( bDebug ) DevMsg("Hit Location: Chest\n");
|
|
break;
|
|
|
|
case HITGROUP_STOMACH:
|
|
subInfo.ScaleDamage( GetHitgroupDamageMultiplier(ptr->hitgroup, info) );
|
|
if( bDebug ) DevMsg("Hit Location: Stomach\n");
|
|
break;
|
|
|
|
case HITGROUP_LEFTARM:
|
|
case HITGROUP_RIGHTARM:
|
|
subInfo.ScaleDamage( GetHitgroupDamageMultiplier(ptr->hitgroup, info) );
|
|
if( bDebug ) DevMsg("Hit Location: Left/Right Arm\n");
|
|
break
|
|
;
|
|
case HITGROUP_LEFTLEG:
|
|
case HITGROUP_RIGHTLEG:
|
|
subInfo.ScaleDamage( GetHitgroupDamageMultiplier(ptr->hitgroup, info) );
|
|
if( bDebug ) DevMsg("Hit Location: Left/Right Leg\n");
|
|
break;
|
|
|
|
default:
|
|
if( bDebug ) DevMsg("Hit Location: UNKNOWN\n");
|
|
break;
|
|
}
|
|
|
|
if ( subInfo.GetDamage() >= 1.0 && !(subInfo.GetDamageType() & DMG_SHOCK ) )
|
|
{
|
|
if( !IsPlayer() || ( IsPlayer() && gpGlobals->maxClients > 1 ) )
|
|
{
|
|
// NPC's always bleed. Players only bleed in multiplayer.
|
|
//SpawnBlood( ptr->endpos, vecDir, BloodColor(), subInfo.GetDamage() );// a little surface blood.
|
|
//UTIL_ASW_DroneBleed( ptr->endpos, vecDir, 4 );
|
|
UTIL_ASW_BloodDrips( GetAbsOrigin()+Vector(0,0,60)+vecDir*3, vecDir, BloodColor(), 5 );
|
|
}
|
|
|
|
TraceBleed( subInfo.GetDamage(), vecDir, ptr, subInfo.GetDamageType() );
|
|
|
|
if ( ptr->hitgroup == HITGROUP_HEAD && m_iHealth - subInfo.GetDamage() > 0 )
|
|
{
|
|
m_fNoDamageDecal = true;
|
|
}
|
|
}
|
|
|
|
// Airboat gun will impart major force if it's about to kill him....
|
|
if ( info.GetDamageType() & DMG_AIRBOAT )
|
|
{
|
|
if ( subInfo.GetDamage() >= GetHealth() )
|
|
{
|
|
float flMagnitude = subInfo.GetDamageForce().Length();
|
|
if ( (flMagnitude != 0.0f) && (flMagnitude < 400.0f * 65.0f) )
|
|
{
|
|
subInfo.ScaleDamageForce( 400.0f * 65.0f / flMagnitude );
|
|
}
|
|
}
|
|
}
|
|
|
|
if( info.GetInflictor() )
|
|
{
|
|
subInfo.SetInflictor( info.GetInflictor() );
|
|
}
|
|
else
|
|
{
|
|
subInfo.SetInflictor( info.GetAttacker() );
|
|
}
|
|
|
|
AddMultiDamage( subInfo, this );
|
|
}
|
|
|
|
void CASW_Zombie::OnRestore()
|
|
{
|
|
BaseClass::OnRestore();
|
|
|
|
CreateEyeGlows();
|
|
}
|
|
|
|
void CASW_Zombie::CreateEyeGlows( void )
|
|
{
|
|
//Create our Eye sprites
|
|
if ( m_pEyeGlow[0] == NULL )
|
|
{
|
|
m_pEyeGlow[0] = CSprite::SpriteCreate( ZOMBIE_GLOW_SPRITE, GetLocalOrigin(), false );
|
|
m_pEyeGlow[0]->SetAttachment( this, LookupAttachment( "LeftEye" ) );
|
|
|
|
m_pEyeGlow[0]->SetTransparency( kRenderTransAdd, 128, 0, 0, 128, kRenderFxNoDissipation );
|
|
m_pEyeGlow[0]->SetBrightness( 164, 0.1f );
|
|
m_pEyeGlow[0]->SetScale( 0.1f, 0.1f );
|
|
m_pEyeGlow[0]->SetColor( 128, 0, 0 );
|
|
m_pEyeGlow[0]->SetAsTemporary();
|
|
}
|
|
if ( m_pEyeGlow[1] == NULL )
|
|
{
|
|
m_pEyeGlow[1] = CSprite::SpriteCreate( ZOMBIE_GLOW_SPRITE, GetLocalOrigin(), false );
|
|
m_pEyeGlow[1]->SetAttachment( this, LookupAttachment( "RightEye" ) );
|
|
|
|
m_pEyeGlow[1]->SetTransparency( kRenderTransAdd, 128, 0, 0, 128, kRenderFxNoDissipation );
|
|
m_pEyeGlow[1]->SetBrightness( 164, 0.1f );
|
|
m_pEyeGlow[1]->SetScale( 0.1f, 0.1f );
|
|
m_pEyeGlow[1]->SetColor( 128, 0, 0 );
|
|
m_pEyeGlow[1]->SetAsTemporary();
|
|
}
|
|
}
|
|
|
|
void CASW_Zombie::RemoveEyeGlows( float flDelay )
|
|
{
|
|
if( m_pEyeGlow[0] )
|
|
{
|
|
m_pEyeGlow[0]->FadeAndDie( flDelay );
|
|
m_pEyeGlow[0] = NULL;
|
|
}
|
|
|
|
if( m_pEyeGlow[1] )
|
|
{
|
|
m_pEyeGlow[1]->FadeAndDie( flDelay );
|
|
m_pEyeGlow[1] = NULL;
|
|
}
|
|
}
|
|
|
|
void CASW_Zombie::Event_Killed( const CTakeDamageInfo &info )
|
|
{
|
|
RemoveEyeGlows( 0.0f );
|
|
|
|
BaseClass::Event_Killed( info );
|
|
}
|
|
|
|
void CASW_Zombie::SetSpawner(CASW_Base_Spawner* spawner)
|
|
{
|
|
m_hSpawner = spawner;
|
|
}
|
|
|
|
void CASW_Zombie::ASW_Ignite( float flFlameLifetime, float flSize, CBaseEntity *pAttacker, CBaseEntity *pDamagingWeapon /*= NULL */ )
|
|
{
|
|
if (AllowedToIgnite())
|
|
{
|
|
if( IsOnFire() )
|
|
return;
|
|
|
|
m_bOnFire = true;
|
|
if (ASWBurning())
|
|
ASWBurning()->BurnEntity(this, pAttacker, flFlameLifetime, 0.4f, 5.0f * 0.4f); // 5 dps, applied every 0.4 seconds
|
|
|
|
/*
|
|
CEntityFlame *pFlame = CEntityFlame::Create( this );
|
|
if (pFlame)
|
|
{
|
|
if (pAttacker)
|
|
pFlame->SetOwnerEntity(pAttacker);
|
|
pFlame->SetLifetime( flFlameLifetime );
|
|
AddFlag( FL_ONFIRE );
|
|
|
|
SetEffectEntity( pFlame );
|
|
|
|
if ( flSize > 0.0f )
|
|
{
|
|
pFlame->SetSize( flSize );
|
|
}
|
|
}
|
|
*/
|
|
|
|
m_OnIgnite.FireOutput( this, this );
|
|
|
|
RemoveSpawnFlags( SF_NPC_GAG );
|
|
|
|
MoanSound( envASWZombieMoanIgnited, ARRAYSIZE( envASWZombieMoanIgnited ) );
|
|
|
|
if( m_pMoanSound )
|
|
{
|
|
ENVELOPE_CONTROLLER.SoundChangePitch( m_pMoanSound, 120, 1.0 );
|
|
ENVELOPE_CONTROLLER.SoundChangeVolume( m_pMoanSound, 1, 1.0 );
|
|
}
|
|
}
|
|
}
|
|
|
|
void CASW_Zombie::Extinguish()
|
|
{
|
|
m_bOnFire = false;
|
|
if( m_pMoanSound )
|
|
{
|
|
ENVELOPE_CONTROLLER.SoundChangeVolume( m_pMoanSound, 0, 2.0 );
|
|
ENVELOPE_CONTROLLER.SoundChangePitch( m_pMoanSound, 100, 2.0 );
|
|
m_flNextMoanSound = gpGlobals->curtime + random->RandomFloat( 2.0, 4.0 );
|
|
}
|
|
|
|
if (ASWBurning())
|
|
ASWBurning()->Extinguish(this);
|
|
RemoveFlag( FL_ONFIRE );
|
|
}
|
|
|
|
|
|
void CASW_Zombie::SetHealthByDifficultyLevel()
|
|
{
|
|
SetHealth(ASWGameRules()->ModifyAlienHealthBySkillLevel(sk_asw_zombie_health.GetInt()));
|
|
}
|
|
|
|
//=============================================================================
|
|
|
|
AI_BEGIN_CUSTOM_NPC( asw_zombie, CASW_Zombie )
|
|
|
|
DECLARE_CONDITION( COND_ASW_BLOCKED_BY_DOOR )
|
|
DECLARE_CONDITION( COND_ASW_DOOR_OPENED )
|
|
DECLARE_CONDITION( COND_ASW_ZOMBIE_CHARGE_TARGET_MOVED )
|
|
|
|
DECLARE_TASK( TASK_ASW_ZOMBIE_EXPRESS_ANGER )
|
|
DECLARE_TASK( TASK_ASW_ZOMBIE_YAW_TO_DOOR )
|
|
DECLARE_TASK( TASK_ASW_ZOMBIE_ATTACK_DOOR )
|
|
DECLARE_TASK( TASK_ASW_ZOMBIE_CHARGE_ENEMY )
|
|
|
|
DECLARE_ACTIVITY( ACT_ASW_ZOMBIE_TANTRUM );
|
|
DECLARE_ACTIVITY( ACT_ASW_ZOMBIE_WALLPOUND );
|
|
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_ASW_ZOMBIE_BASH_DOOR,
|
|
|
|
" Tasks"
|
|
" TASK_SET_ACTIVITY ACTIVITY:ACT_ASW_ZOMBIE_TANTRUM"
|
|
" TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_TAKE_COVER_FROM_ENEMY"
|
|
" TASK_ASW_ZOMBIE_YAW_TO_DOOR 0"
|
|
" TASK_FACE_IDEAL 0"
|
|
" TASK_ASW_ZOMBIE_ATTACK_DOOR 0"
|
|
""
|
|
" Interrupts"
|
|
" COND_ZOMBIE_RELEASECRAB"
|
|
" COND_ENEMY_DEAD"
|
|
" COND_NEW_ENEMY"
|
|
" COND_ASW_DOOR_OPENED"
|
|
)
|
|
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_ASW_ZOMBIE_WANDER_ANGRILY,
|
|
|
|
" Tasks"
|
|
" TASK_WANDER 480240" // 48 units to 240 units.
|
|
" TASK_WALK_PATH 0"
|
|
" TASK_WAIT_FOR_MOVEMENT 4"
|
|
""
|
|
" Interrupts"
|
|
" COND_ZOMBIE_RELEASECRAB"
|
|
" COND_ENEMY_DEAD"
|
|
" COND_NEW_ENEMY"
|
|
" COND_ASW_DOOR_OPENED"
|
|
)
|
|
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_ASW_ZOMBIE_CHARGE_ENEMY,
|
|
|
|
|
|
" Tasks"
|
|
" TASK_ASW_ZOMBIE_CHARGE_ENEMY 0"
|
|
" TASK_WALK_PATH 0"
|
|
" TASK_WAIT_FOR_MOVEMENT 0"
|
|
" TASK_PLAY_SEQUENCE ACTIVITY:ACT_MELEE_ATTACK1" /* placeholder until frustration/rage/fence shake animation available */
|
|
""
|
|
" Interrupts"
|
|
" COND_ZOMBIE_RELEASECRAB"
|
|
" COND_ENEMY_DEAD"
|
|
" COND_NEW_ENEMY"
|
|
" COND_ASW_DOOR_OPENED"
|
|
" COND_ASW_ZOMBIE_CHARGE_TARGET_MOVED"
|
|
)
|
|
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_ASW_ZOMBIE_FAIL,
|
|
|
|
" Tasks"
|
|
" TASK_STOP_MOVING 0"
|
|
" TASK_SET_ACTIVITY ACTIVITY:ACT_ASW_ZOMBIE_TANTRUM"
|
|
" TASK_WAIT 1"
|
|
" TASK_WAIT_PVS 0"
|
|
""
|
|
" Interrupts"
|
|
" COND_CAN_RANGE_ATTACK1 "
|
|
" COND_CAN_RANGE_ATTACK2 "
|
|
" COND_CAN_MELEE_ATTACK1 "
|
|
" COND_CAN_MELEE_ATTACK2"
|
|
" COND_GIVE_WAY"
|
|
" COND_ASW_DOOR_OPENED"
|
|
)
|
|
|
|
AI_END_CUSTOM_NPC()
|
|
|
|
//=============================================================================
|