mirror of https://github.com/FWGS/hlsdk-xash3d
1952 lines
44 KiB
C++
Executable File
1952 lines
44 KiB
C++
Executable File
/***
|
|
*
|
|
* Copyright (c) 1999, 2000 Valve LLC. All rights reserved.
|
|
*
|
|
* This product contains software technology licensed from Id
|
|
* Software, Inc. ("Id Technology"). Id Technology (c) 1996 Id Software, Inc.
|
|
* All Rights Reserved.
|
|
*
|
|
* This source code contains proprietary and confidential information of
|
|
* Valve LLC and its suppliers. Access to this code is restricted to
|
|
* persons who have executed a written SDK license with Valve. Any access,
|
|
* use or distribution of this code by or to any unlicensed person is illegal.
|
|
*
|
|
****/
|
|
#if !defined( OEM_BUILD ) && !defined( HLDEMO_BUILD )
|
|
|
|
#include "extdll.h"
|
|
#include "util.h"
|
|
#include "cbase.h"
|
|
#include "monsters.h"
|
|
#include "schedule.h"
|
|
#include "flyingmonster.h"
|
|
#include "nodes.h"
|
|
#include "soundent.h"
|
|
#include "animation.h"
|
|
#include "effects.h"
|
|
#include "weapons.h"
|
|
#include "stukagrenade.h"
|
|
|
|
#include "Stukabat.h"
|
|
|
|
|
|
LINK_ENTITY_TO_CLASS( monster_stukabat, CStukabat );
|
|
LINK_ENTITY_TO_CLASS( info_node_stukabat, CStukabatNode );
|
|
|
|
|
|
//=========================================================
|
|
// stukabat nodes
|
|
//=========================================================
|
|
|
|
|
|
TYPEDESCRIPTION CStukabatNode::m_SaveData[] =
|
|
{
|
|
DEFINE_FIELD( CStukabatNode, mbInUse, FIELD_BOOLEAN ),
|
|
};
|
|
|
|
IMPLEMENT_SAVERESTORE( CStukabatNode, CBaseEntity );
|
|
|
|
//=========================================================
|
|
// nodes start out as ents in the world. As they are spawned,
|
|
// the node info is recorded then the ents are discarded.
|
|
//=========================================================
|
|
void CStukabatNode :: KeyValue( KeyValueData *pkvd )
|
|
{
|
|
CBaseEntity::KeyValue( pkvd );
|
|
}
|
|
|
|
//=========================================================
|
|
//=========================================================
|
|
void CStukabatNode :: Spawn( void )
|
|
{
|
|
pev->movetype = MOVETYPE_NONE;
|
|
pev->solid = SOLID_NOT;// always solid_not
|
|
mbInUse = false;
|
|
pev->classname = MAKE_STRING("info_node_stukabat");
|
|
}
|
|
|
|
|
|
//=========================================================
|
|
// stukabat
|
|
//=========================================================
|
|
|
|
#define SEARCH_RETRY 16
|
|
|
|
#define STUKABAT_SPEED 200
|
|
|
|
extern CGraph WorldGraph;
|
|
|
|
|
|
#define STUKABAT_AE_BOMB 1
|
|
|
|
//=========================================================
|
|
// Monster's Anim Events Go Here
|
|
//=========================================================
|
|
|
|
|
|
TYPEDESCRIPTION CStukabat::m_SaveData[] =
|
|
{
|
|
DEFINE_FIELD( CStukabat, m_SaveVelocity, FIELD_VECTOR ),
|
|
DEFINE_FIELD( CStukabat, m_idealDist, FIELD_FLOAT ),
|
|
DEFINE_FIELD( CStukabat, m_flNextMeleeAttack, FIELD_FLOAT ),
|
|
DEFINE_FIELD( CStukabat, m_flNextRangedAttack, FIELD_FLOAT ),
|
|
//DEFINE_FIELD( CStukabat, m_bOnAttack, FIELD_BOOLEAN ),
|
|
DEFINE_FIELD( CStukabat, m_flMaxSpeed, FIELD_FLOAT ),
|
|
DEFINE_FIELD( CStukabat, m_flMinSpeed, FIELD_FLOAT ),
|
|
DEFINE_FIELD( CStukabat, m_flMaxDist, FIELD_FLOAT ),
|
|
DEFINE_FIELD( CStukabat, m_flNextAlert, FIELD_TIME ),
|
|
DEFINE_FIELD( CStukabat, m_flLandTime, FIELD_TIME ),
|
|
DEFINE_FIELD( CStukabat, m_iMode, FIELD_INTEGER ),
|
|
DEFINE_FIELD( CStukabat, meMedium, FIELD_INTEGER ),
|
|
DEFINE_FIELD( CStukabat, mpCeilingNode, FIELD_CLASSPTR ),
|
|
};
|
|
|
|
IMPLEMENT_SAVERESTORE( CStukabat, CFlyingMonster );
|
|
|
|
|
|
const char *CStukabat::pIdleSounds[] =
|
|
{
|
|
"stukabat/stukabat_idle1.wav",
|
|
"stukabat/stukabat_idle2.wav",
|
|
"stukabat/stukabat_idle3.wav",
|
|
"stukabat/stukabat_idle4.wav",
|
|
};
|
|
|
|
const char *CStukabat::pAlertSounds[] =
|
|
{
|
|
"stukabat/ng_alert1.wav",
|
|
};
|
|
|
|
const char *CStukabat::pAttackSounds[] =
|
|
{
|
|
"stukabat/ng_attack1.wav",
|
|
};
|
|
|
|
const char *CStukabat::pSlashSounds[] =
|
|
{
|
|
"zombie/claw_strike1.wav",
|
|
"zombie/claw_strike2.wav",
|
|
"zombie/claw_strike3.wav",
|
|
};
|
|
|
|
const char *CStukabat::pPainSounds[] =
|
|
{
|
|
"stukabat/ng_pain1.wav",
|
|
"stukabat/ng_pain2.wav",
|
|
"stukabat/ng_pain3.wav",
|
|
};
|
|
|
|
const char *CStukabat::pDieSounds[] =
|
|
{
|
|
"stukabat/ng_die1.wav",
|
|
"stukabat/ng_die2.wav",
|
|
};
|
|
|
|
#define EMIT_STUKABAT_SOUND( chan, array ) \
|
|
EMIT_SOUND_DYN ( ENT(pev), chan , array [ RANDOM_LONG(0,ARRAYSIZE( array )-1) ], 1.0, 0.6, 0, RANDOM_LONG(95,105) );
|
|
|
|
|
|
void CStukabat :: IdleSound( void )
|
|
{
|
|
EMIT_STUKABAT_SOUND( CHAN_VOICE, pIdleSounds );
|
|
}
|
|
|
|
void CStukabat :: AlertSound( void )
|
|
{
|
|
EMIT_STUKABAT_SOUND( CHAN_VOICE, pAlertSounds );
|
|
}
|
|
|
|
void CStukabat :: AttackSound( void )
|
|
{
|
|
EMIT_STUKABAT_SOUND( CHAN_VOICE, pAttackSounds );
|
|
}
|
|
|
|
void CStukabat :: SlashSound( void )
|
|
{
|
|
EMIT_STUKABAT_SOUND( CHAN_WEAPON, pSlashSounds );
|
|
}
|
|
|
|
void CStukabat :: DeathSound( void )
|
|
{
|
|
EMIT_STUKABAT_SOUND( CHAN_VOICE, pDieSounds );
|
|
}
|
|
|
|
void CStukabat :: PainSound( void )
|
|
{
|
|
EMIT_STUKABAT_SOUND( CHAN_VOICE, pPainSounds );
|
|
}
|
|
|
|
//=========================================================
|
|
// monster-specific tasks and states
|
|
//=========================================================
|
|
enum
|
|
{
|
|
TASK_STUKABAT_CIRCLE_ENEMY = LAST_COMMON_TASK + 1,
|
|
TASK_STUKABAT_FLY,
|
|
TASK_STUKABAT_MOVETOGROUND,
|
|
TASK_STUKABAT_LANDGROUND,
|
|
TASK_STUKABAT_TAKEOFFGROUND,
|
|
TASK_STUKABAT_FIND_NODE,
|
|
TASK_STUKABAT_LANDCEILING,
|
|
TASK_STUKABAT_TAKEOFFCEILING,
|
|
TASK_STUKABAT_BOMB_OK,
|
|
TASK_STUKABAT_FALL
|
|
};
|
|
|
|
// Note: tried hovering, didn't like it
|
|
|
|
//=========================================================
|
|
// AI Schedules Specific to this monster
|
|
//=========================================================
|
|
|
|
static Task_t tlFlyAround[] =
|
|
{
|
|
{ TASK_SET_ACTIVITY, (float)ACT_FLY },
|
|
{ TASK_STUKABAT_FLY, 0.0 },
|
|
};
|
|
|
|
static Schedule_t slFlyAround[] =
|
|
{
|
|
{
|
|
tlFlyAround,
|
|
ARRAYSIZE(tlFlyAround),
|
|
bits_COND_LIGHT_DAMAGE |
|
|
bits_COND_HEAVY_DAMAGE |
|
|
bits_COND_SEE_ENEMY |
|
|
bits_COND_NEW_ENEMY |
|
|
bits_COND_HEAR_SOUND,
|
|
bits_SOUND_PLAYER |
|
|
bits_SOUND_COMBAT,
|
|
"FlyAround"
|
|
},
|
|
};
|
|
|
|
static Task_t tlFlyAgitated[] =
|
|
{
|
|
{ TASK_STOP_MOVING, (float) 0 },
|
|
{ TASK_SET_ACTIVITY, (float)ACT_RUN },
|
|
{ TASK_WAIT, (float)2.0 },
|
|
};
|
|
|
|
static Schedule_t slFlyAgitated[] =
|
|
{
|
|
{
|
|
tlFlyAgitated,
|
|
ARRAYSIZE(tlFlyAgitated),
|
|
0,
|
|
0,
|
|
"FlyAgitated"
|
|
},
|
|
};
|
|
|
|
|
|
static Task_t tlCircleEnemy[] =
|
|
{
|
|
{ TASK_SET_ACTIVITY, (float)ACT_FLY },
|
|
{ TASK_STUKABAT_CIRCLE_ENEMY, 0.0 },
|
|
};
|
|
|
|
static Schedule_t slCircleEnemy[] =
|
|
{
|
|
{
|
|
tlCircleEnemy,
|
|
ARRAYSIZE(tlCircleEnemy),
|
|
bits_COND_NEW_ENEMY |
|
|
bits_COND_LIGHT_DAMAGE |
|
|
bits_COND_HEAVY_DAMAGE |
|
|
bits_COND_CAN_MELEE_ATTACK1 |
|
|
bits_COND_CAN_RANGE_ATTACK1,
|
|
0,
|
|
"CircleEnemy"
|
|
},
|
|
};
|
|
|
|
|
|
Task_t tlStukabatTwitchDie[] =
|
|
{
|
|
{ TASK_STOP_MOVING, 0 },
|
|
{ TASK_SOUND_DIE, (float)0 },
|
|
{ TASK_STUKABAT_FALL, (float)0 },
|
|
{ TASK_DIE, (float)0 },
|
|
};
|
|
|
|
Schedule_t slStukabatTwitchDie[] =
|
|
{
|
|
{
|
|
tlStukabatTwitchDie,
|
|
ARRAYSIZE( tlStukabatTwitchDie ),
|
|
0,
|
|
0,
|
|
"Die"
|
|
},
|
|
};
|
|
|
|
static Task_t tlLandGround[] =
|
|
{
|
|
{ TASK_STUKABAT_MOVETOGROUND, 0 },
|
|
{ TASK_SET_ACTIVITY, (float)ACT_LAND },
|
|
{ TASK_STUKABAT_LANDGROUND, 0 },
|
|
{ TASK_SET_ACTIVITY, (float)ACT_CROUCHIDLE },
|
|
};
|
|
|
|
static Schedule_t slLandGround[] =
|
|
{
|
|
{
|
|
tlLandGround,
|
|
ARRAYSIZE(tlLandGround),
|
|
0,
|
|
0,
|
|
"LandGround"
|
|
},
|
|
};
|
|
|
|
static Task_t tlTakeOffGround[] =
|
|
{
|
|
{ TASK_SET_ACTIVITY, (float)ACT_LEAP },
|
|
{ TASK_STUKABAT_TAKEOFFGROUND, 0 },
|
|
{ TASK_SET_ACTIVITY, (float)ACT_FLY },
|
|
};
|
|
|
|
static Schedule_t slTakeOffGround[] =
|
|
{
|
|
{
|
|
tlTakeOffGround,
|
|
ARRAYSIZE(tlTakeOffGround),
|
|
0,
|
|
0,
|
|
"TakeOffGround"
|
|
},
|
|
};
|
|
|
|
static Task_t tlLandCeiling[] =
|
|
{
|
|
{ TASK_SET_FAIL_SCHEDULE, (float)SCHED_FAIL },
|
|
{ TASK_STUKABAT_FIND_NODE, 0 },
|
|
{ TASK_GET_PATH_TO_TARGET, (float)0 },
|
|
{ TASK_WALK_PATH, (float)0 },
|
|
{ TASK_WAIT_FOR_MOVEMENT, (float)0 },
|
|
{ TASK_SET_ACTIVITY, (float)ACT_STAND },
|
|
{ TASK_STUKABAT_LANDCEILING, 0 },
|
|
{ TASK_SET_ACTIVITY, (float)ACT_IDLE },
|
|
};
|
|
|
|
static Schedule_t slLandCeiling[] =
|
|
{
|
|
{
|
|
tlLandCeiling,
|
|
ARRAYSIZE(tlLandCeiling),
|
|
0,
|
|
0,
|
|
"LandCeiling"
|
|
},
|
|
};
|
|
|
|
static Task_t tlTakeOffCeiling[] =
|
|
{
|
|
{ TASK_STUKABAT_TAKEOFFCEILING, 0 },
|
|
{ TASK_SET_ACTIVITY, (float)ACT_FLY },
|
|
};
|
|
|
|
static Schedule_t slTakeOffCeiling[] =
|
|
{
|
|
{
|
|
tlTakeOffCeiling,
|
|
ARRAYSIZE(tlTakeOffCeiling),
|
|
0,
|
|
0,
|
|
"TakeOffCeiling"
|
|
},
|
|
};
|
|
|
|
static Task_t tlBombAttack[] =
|
|
{
|
|
{ TASK_SET_FAIL_SCHEDULE, (float)SCHED_FAIL },
|
|
{ TASK_GET_PATH_TO_SPOT, (float)0 }, // todo: overwrite this...
|
|
{ TASK_WALK_PATH, (float)0 },
|
|
{ TASK_WAIT_FOR_MOVEMENT, (float)0 },
|
|
{ TASK_STUKABAT_BOMB_OK, (float)0 }, // checks 2D length <= 128, z length <= 1024 && > 128, path is unobstructed
|
|
{ TASK_RANGE_ATTACK1, (float)0 },
|
|
};
|
|
|
|
static Schedule_t slBombAttack[] =
|
|
{
|
|
{
|
|
tlBombAttack,
|
|
ARRAYSIZE(tlBombAttack),
|
|
0,
|
|
0,
|
|
"BombAttack"
|
|
},
|
|
};
|
|
|
|
|
|
DEFINE_CUSTOM_SCHEDULES(CStukabat)
|
|
{
|
|
slFlyAround,
|
|
slFlyAgitated,
|
|
slCircleEnemy,
|
|
slStukabatTwitchDie,
|
|
slLandGround,
|
|
slTakeOffGround,
|
|
slLandCeiling,
|
|
slTakeOffCeiling,
|
|
slBombAttack,
|
|
};
|
|
|
|
IMPLEMENT_CUSTOM_SCHEDULES(CStukabat, CFlyingMonster);
|
|
|
|
//=========================================================
|
|
// Classify - indicates this monster's place in the
|
|
// relationship table.
|
|
//=========================================================
|
|
int CStukabat :: Classify ( void )
|
|
{
|
|
return m_iClass?m_iClass:CLASS_ALIEN_MONSTER;
|
|
}
|
|
|
|
|
|
//=========================================================
|
|
// CheckMeleeAttack1
|
|
//=========================================================
|
|
BOOL CStukabat :: CheckMeleeAttack1 ( float flDot, float flDist )
|
|
{
|
|
if ( flDist <= 64 && flDot >= 0.7 && m_flNextMeleeAttack <= gpGlobals->time )
|
|
{
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
/*
|
|
void CStukabat::SlashTouch( CBaseEntity *pOther )
|
|
{
|
|
// slash if we hit who we want to eat
|
|
if ( pOther == m_hEnemy )
|
|
{
|
|
m_flNextMeleeAttack = gpGlobals->time;
|
|
m_bOnAttack = TRUE;
|
|
}
|
|
}
|
|
*/
|
|
|
|
void CStukabat::CombatUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value )
|
|
{
|
|
//if ( !ShouldToggle( useType, m_bOnAttack ) )
|
|
if ( !ShouldToggle( useType ) )
|
|
return;
|
|
|
|
/*
|
|
if (m_bOnAttack)
|
|
{
|
|
m_bOnAttack = 0;
|
|
}
|
|
else
|
|
{
|
|
m_bOnAttack = 1;
|
|
}
|
|
*/
|
|
}
|
|
|
|
//=========================================================
|
|
// CheckRangeAttack1 - Fly in for a chomp
|
|
//
|
|
//=========================================================
|
|
BOOL CStukabat :: CheckRangeAttack1 ( float flDot, float flDist )
|
|
{
|
|
//if ( flDot > -0.7 && (m_bOnAttack || ( flDist <= 384 && m_idealDist <= 384)))
|
|
if ( flDot > -0.7 && flDist <= 2048 && m_flNextRangedAttack <= gpGlobals->time)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
//=========================================================
|
|
// CheckRangeAttack2 - Fly in for a chomp
|
|
//
|
|
//=========================================================
|
|
BOOL CStukabat :: CheckRangeAttack2 ( float flDot, float flDist )
|
|
{
|
|
//if ( flDot > -0.7 && (m_bOnAttack || ( flDist <= 384 && m_idealDist <= 384)))
|
|
if ( flDot > -0.7 && ( flDist <= 384 && m_idealDist <= 384) && m_flNextMeleeAttack <= gpGlobals->time)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
//=========================================================
|
|
// SetYawSpeed - allows each sequence to have a different
|
|
// turn rate associated with it.
|
|
//=========================================================
|
|
void CStukabat :: SetYawSpeed ( void )
|
|
{
|
|
pev->yaw_speed = 100;
|
|
}
|
|
|
|
|
|
|
|
//=========================================================
|
|
// Killed - overrides CFlyingMonster.
|
|
//
|
|
void CStukabat :: Killed( entvars_t *pevAttacker, int iGib )
|
|
{
|
|
pev->velocity = Vector(0,0,0);
|
|
pev->gravity = 1.0;
|
|
pev->angles.x = 0;
|
|
pev->deadflag = DEAD_DYING;
|
|
|
|
//CBaseMonster::Killed( pevAttacker, iGib );
|
|
CFlyingMonster::Killed( pevAttacker, iGib );
|
|
pev->movetype = MOVETYPE_TOSS;
|
|
}
|
|
|
|
void CStukabat::BecomeDead( void )
|
|
{
|
|
pev->takedamage = DAMAGE_YES;// don't let autoaim aim at corpses.
|
|
|
|
// give the corpse half of the monster's original maximum health.
|
|
pev->health = pev->max_health / 2;
|
|
pev->max_health = 5; // max_health now becomes a counter for how many blood decals the corpse can place.
|
|
}
|
|
|
|
void CStukabat :: Slash( void )
|
|
{
|
|
if (m_hEnemy != NULL && FVisible( m_hEnemy ))
|
|
{
|
|
CBaseEntity *pHurt = m_hEnemy;
|
|
|
|
Vector vecShootDir = ShootAtEnemy( pev->origin );
|
|
UTIL_MakeAimVectors ( pev->angles );
|
|
|
|
if (DotProduct( vecShootDir, gpGlobals->v_forward ) > 0.707)
|
|
{
|
|
//m_bOnAttack = TRUE;
|
|
pHurt->pev->punchangle.z = -18;
|
|
pHurt->pev->punchangle.x = 5;
|
|
pHurt->pev->velocity = pHurt->pev->velocity - gpGlobals->v_right * 100;
|
|
//pHurt->TakeDamage( pev, pev, gSkillData.stukabatDmgSlash, DMG_SLASH );
|
|
pHurt->TakeDamage( pev, pev, gSkillData.nightgauntDmgSlash, DMG_SLASH );
|
|
}
|
|
}
|
|
SlashSound();
|
|
}
|
|
|
|
//=========================================================
|
|
// HandleAnimEvent - catches the monster-specific messages
|
|
// that occur when tagged animation frames are played.
|
|
//=========================================================
|
|
void CStukabat :: HandleAnimEvent( MonsterEvent_t *pEvent )
|
|
{
|
|
switch( pEvent->event )
|
|
{
|
|
case STUKABAT_AE_BOMB:
|
|
{
|
|
Vector vecAngle;
|
|
UTIL_MakeVectors(vecAngle);
|
|
vecAngle.Normalize();
|
|
vecAngle = 20.0 * vecAngle;
|
|
|
|
EMIT_SOUND(ENT(pev), CHAN_WEAPON, "weapons/glauncher.wav", 0.8, ATTN_NORM);
|
|
|
|
// todo: need to set hackedgunposition
|
|
CStukaGrenade::ShootContact( pev, GetGunPosition(), vecAngle );
|
|
|
|
m_flNextRangedAttack = gpGlobals->time + RANDOM_FLOAT(7.0, 10.0);
|
|
}
|
|
break;
|
|
default:
|
|
CFlyingMonster::HandleAnimEvent( pEvent );
|
|
break;
|
|
}
|
|
}
|
|
|
|
//=========================================================
|
|
// Spawn
|
|
//=========================================================
|
|
void CStukabat :: Spawn()
|
|
{
|
|
Precache( );
|
|
|
|
if (pev->model)
|
|
SET_MODEL(ENT(pev), STRING(pev->model)); //LRC
|
|
else
|
|
SET_MODEL(ENT(pev), "models/stukabat.mdl");
|
|
UTIL_SetSize( pev, Vector( -16, -16, -2 ), Vector( 16, 16, 16 ) );
|
|
|
|
pev->solid = SOLID_BBOX;
|
|
pev->movetype = MOVETYPE_FLY;
|
|
m_bloodColor = BLOOD_COLOR_GREEN;
|
|
if (pev->health == 0)
|
|
// pev->health = gSkillData.stukabatHealth;
|
|
// pev->health = gSkillData.zombieHealth;
|
|
pev->health = 5;
|
|
pev->view_ofs = Vector ( 0, 0, 16 );
|
|
m_flFieldOfView = VIEW_FIELD_WIDE;
|
|
m_MonsterState = MONSTERSTATE_NONE;
|
|
SetBits(pev->flags, FL_FLY);
|
|
SetFlyingSpeed( STUKABAT_SPEED );
|
|
SetFlyingMomentum( 2.5 ); // Set momentum constant
|
|
|
|
m_afCapability = bits_CAP_RANGE_ATTACK1 | bits_CAP_RANGE_ATTACK2 | bits_CAP_FLY;
|
|
|
|
meMedium = SB_INAIR;
|
|
m_iMode = STUKABAT_IDLE;
|
|
|
|
MonsterInit();
|
|
|
|
// PhilG: hack to allow CheckMeleeAttack1 to be used
|
|
m_afCapability |= bits_CAP_MELEE_ATTACK1;
|
|
|
|
//SetTouch( SlashTouch );
|
|
m_flNextRangedAttack = gpGlobals->time;
|
|
m_flNextMeleeAttack = gpGlobals->time;
|
|
SetTouch( NULL );
|
|
SetUse( CombatUse );
|
|
|
|
m_idealDist = 384;
|
|
m_flMinSpeed = 80;
|
|
m_flMaxSpeed = 300;
|
|
m_flMaxDist = 384;
|
|
|
|
m_flLandTime = gpGlobals->time;
|
|
|
|
Vector Forward;
|
|
UTIL_MakeVectorsPrivate(pev->angles, Forward, 0, 0);
|
|
pev->velocity = m_flightSpeed * Forward.Normalize();
|
|
m_SaveVelocity = pev->velocity;
|
|
|
|
mpCeilingNode = NULL;
|
|
}
|
|
|
|
//=========================================================
|
|
// Precache - precaches all resources this monster needs
|
|
//=========================================================
|
|
void CStukabat :: Precache()
|
|
{
|
|
if (pev->model)
|
|
PRECACHE_MODEL((char*)STRING(pev->model)); //LRC
|
|
else
|
|
PRECACHE_MODEL("models/stukabat.mdl");
|
|
|
|
PRECACHE_SOUND_ARRAY( pIdleSounds );
|
|
PRECACHE_SOUND_ARRAY( pAlertSounds );
|
|
PRECACHE_SOUND_ARRAY( pAttackSounds );
|
|
PRECACHE_SOUND_ARRAY( pSlashSounds );
|
|
PRECACHE_SOUND_ARRAY( pDieSounds );
|
|
PRECACHE_SOUND_ARRAY( pPainSounds );
|
|
|
|
PRECACHE_SOUND("weapons/glauncher.wav");
|
|
}
|
|
|
|
void CStukabat :: SetActivity ( Activity NewActivity )
|
|
{
|
|
int iSequence = ACTIVITY_NOT_AVAILABLE;
|
|
void *pmodel = GET_MODEL_PTR( ENT(pev) );
|
|
|
|
switch ( NewActivity)
|
|
{
|
|
case ACT_RANGE_ATTACK2:
|
|
// get aimable sequence
|
|
iSequence = LookupSequence( "Attack_claw" );
|
|
default:
|
|
iSequence = LookupActivity ( NewActivity );
|
|
break;
|
|
}
|
|
|
|
// Set to the desired anim, or default anim if the desired is not present
|
|
if ( iSequence > ACTIVITY_NOT_AVAILABLE )
|
|
{
|
|
if ( pev->sequence != iSequence || !m_fSequenceLoops )
|
|
{
|
|
pev->frame = 0;
|
|
}
|
|
|
|
pev->sequence = iSequence; // Set to the reset anim (if it's there)
|
|
ResetSequenceInfo( );
|
|
SetYawSpeed();
|
|
}
|
|
else
|
|
{
|
|
// Not available try to get default anim
|
|
ALERT ( at_console, "%s has no sequence for act:%d\n", STRING(pev->classname), NewActivity );
|
|
pev->sequence = 0; // Set to the reset anim (if it's there)
|
|
}
|
|
|
|
m_Activity = NewActivity; // Go ahead and set this so it doesn't keep trying when the anim is not present
|
|
|
|
// In case someone calls this with something other than the ideal activity
|
|
m_IdealActivity = m_Activity;
|
|
}
|
|
|
|
//=========================================================
|
|
// GetSchedule
|
|
//=========================================================
|
|
Schedule_t* CStukabat::GetSchedule()
|
|
{
|
|
// ALERT( at_console, "GetSchedule( )\n" );
|
|
switch(m_MonsterState)
|
|
{
|
|
case MONSTERSTATE_IDLE:
|
|
m_flightSpeed = STUKABAT_SPEED / 4;
|
|
switch (meMedium)
|
|
{
|
|
case SB_ONGROUND:
|
|
return GetScheduleOfType( SCHED_IDLE_STAND );
|
|
break;
|
|
case SB_ONCEILING:
|
|
return GetScheduleOfType( SCHED_IDLE_STAND );
|
|
break;
|
|
case SB_INAIR:
|
|
default:
|
|
return GetScheduleOfType( SCHED_IDLE_WALK );
|
|
break;
|
|
}
|
|
|
|
case MONSTERSTATE_ALERT:
|
|
m_flightSpeed = STUKABAT_SPEED - 50;
|
|
return GetScheduleOfType( SCHED_IDLE_WALK );
|
|
|
|
case MONSTERSTATE_COMBAT:
|
|
switch (meMedium)
|
|
{
|
|
case SB_INAIR:
|
|
{
|
|
m_flMaxSpeed = STUKABAT_SPEED + 100;
|
|
|
|
// chase them down and eat them
|
|
if ( HasConditions( bits_COND_CAN_RANGE_ATTACK1 ) )
|
|
{
|
|
return GetScheduleOfType( SCHED_RANGE_ATTACK1 );
|
|
}
|
|
// can we claw? (which is defined as a range attack for some unknown reason)
|
|
if ( HasConditions( bits_COND_CAN_RANGE_ATTACK2 ) )
|
|
{
|
|
if ( HasConditions( bits_COND_CAN_MELEE_ATTACK1 ) )
|
|
{
|
|
return GetScheduleOfType( SCHED_RANGE_ATTACK2 );
|
|
}
|
|
else
|
|
{
|
|
return GetScheduleOfType( SCHED_CHASE_ENEMY );
|
|
}
|
|
}
|
|
/*
|
|
if ( HasConditions( bits_COND_HEAVY_DAMAGE ) )
|
|
{
|
|
m_bOnAttack = TRUE;
|
|
}
|
|
if ( pev->health < pev->max_health - 20 )
|
|
{
|
|
m_bOnAttack = TRUE;
|
|
}
|
|
*/
|
|
|
|
return GetScheduleOfType( SCHED_STANDOFF );
|
|
}
|
|
break;
|
|
case SB_ONGROUND:
|
|
case SB_ONCEILING:
|
|
default:
|
|
return GetScheduleOfType( SCHED_COMBAT_STAND );
|
|
break;
|
|
}
|
|
}
|
|
|
|
return CFlyingMonster :: GetSchedule();
|
|
}
|
|
|
|
|
|
//=========================================================
|
|
//=========================================================
|
|
Schedule_t* CStukabat :: GetScheduleOfType ( int Type )
|
|
{
|
|
int iRand;
|
|
|
|
// ALERT( at_console, "GetScheduleOfType( %d ) %d\n", Type, m_bOnAttack );
|
|
switch ( Type )
|
|
{
|
|
case SCHED_IDLE_STAND:
|
|
case SCHED_IDLE_WALK:
|
|
switch (meMedium)
|
|
{
|
|
case SB_ONGROUND:
|
|
// randomly want to take off, if we have been on ground for at least 10 secs
|
|
if (RANDOM_LONG(0,30) == 0 && gpGlobals->time > m_flLandTime + 10.0)
|
|
{
|
|
return slTakeOffGround;
|
|
}
|
|
return CBaseMonster :: GetScheduleOfType( Type );
|
|
break;
|
|
case SB_ONCEILING:
|
|
// randomly want to take off, if we have been on ground for at least 10 secs
|
|
if (RANDOM_LONG(0,10) == 0 && gpGlobals->time > m_flLandTime + 10.0)
|
|
{
|
|
return slTakeOffCeiling;
|
|
}
|
|
return CBaseMonster :: GetScheduleOfType( Type );
|
|
break;
|
|
case SB_INAIR:
|
|
default:
|
|
// randomly want to land if we have been in the air at least 10 secs
|
|
if (gpGlobals->time > m_flLandTime + 10.0)
|
|
{
|
|
iRand = RANDOM_LONG(0,10);
|
|
if (iRand == 1)
|
|
{
|
|
return slLandGround;
|
|
}
|
|
else if (iRand < 4)
|
|
{
|
|
return slLandCeiling;
|
|
}
|
|
}
|
|
return slFlyAround;
|
|
break;
|
|
}
|
|
case SCHED_STANDOFF:
|
|
return slCircleEnemy;
|
|
case SCHED_COMBAT_STAND:
|
|
switch (meMedium)
|
|
{
|
|
case SB_ONGROUND:
|
|
return slTakeOffGround;
|
|
break;
|
|
case SB_ONCEILING:
|
|
return slTakeOffCeiling;
|
|
break;
|
|
default:
|
|
case SB_INAIR:
|
|
// this should never happen
|
|
return slFlyAround;
|
|
break;
|
|
}
|
|
break;
|
|
case SCHED_RANGE_ATTACK1:
|
|
return slBombAttack;
|
|
case SCHED_FAIL:
|
|
return slFlyAgitated;
|
|
case SCHED_DIE:
|
|
return slStukabatTwitchDie;
|
|
case SCHED_CHASE_ENEMY:
|
|
AttackSound( );
|
|
}
|
|
|
|
return CBaseMonster :: GetScheduleOfType( Type );
|
|
}
|
|
|
|
|
|
|
|
//=========================================================
|
|
// Start task - selects the correct activity and performs
|
|
// any necessary calculations to start the next task on the
|
|
// schedule.
|
|
//=========================================================
|
|
void CStukabat::StartTask(Task_t *pTask)
|
|
{
|
|
switch (pTask->iTask)
|
|
{
|
|
case TASK_STUKABAT_CIRCLE_ENEMY:
|
|
break;
|
|
case TASK_STUKABAT_FLY:
|
|
break;
|
|
case TASK_STUKABAT_MOVETOGROUND:
|
|
// angle downwards
|
|
//if (pev->angles.x > -60) pev->angles.x = -60;
|
|
break;
|
|
case TASK_STUKABAT_LANDGROUND:
|
|
SetActivity(ACT_LAND);
|
|
LandGround();
|
|
break;
|
|
case TASK_STUKABAT_TAKEOFFGROUND:
|
|
SetActivity(ACT_LEAP);
|
|
TakeOffGround();
|
|
break;
|
|
case TASK_STUKABAT_FIND_NODE:
|
|
FindNode();
|
|
m_hTargetEnt = mpCeilingNode;
|
|
m_movementActivity = ACT_FLY;
|
|
break;
|
|
case TASK_STUKABAT_LANDCEILING:
|
|
// stop moving
|
|
m_flightSpeed = 0.0;
|
|
m_flGroundSpeed = 0.0;
|
|
pev->velocity = Vector(0,0,0);
|
|
|
|
SetActivity(ACT_STAND);
|
|
LandCeiling();
|
|
break;
|
|
case TASK_STUKABAT_TAKEOFFCEILING:
|
|
TakeOffGround();
|
|
break;
|
|
case TASK_STUKABAT_BOMB_OK:
|
|
{
|
|
bool bOK = false;
|
|
// do we have an enemy
|
|
if (m_hEnemy != NULL)
|
|
{
|
|
//CBaseEntity* pEnemy = CBaseEntity::Instance(m_hEnemy);
|
|
CBaseEntity* pEnemy = (m_hEnemy);
|
|
if (pEnemy && pEnemy->IsAlive())
|
|
{
|
|
// are we at least 128 (2D) away from enemy
|
|
if ((pev->origin - pEnemy->pev->origin).Length2D() <= 128)
|
|
{
|
|
float dz = pev->origin.z - pEnemy->pev->origin.z;
|
|
// are we a good height above
|
|
if (dz > 128 && dz <= 1024)
|
|
{
|
|
TraceResult tr;
|
|
|
|
UTIL_TraceLine( pev->origin, pEnemy->pev->origin, ignore_monsters, NULL, &tr );
|
|
// are we obstructed (at least before 95% of the distance
|
|
if (tr.flFraction > 0.95)
|
|
{
|
|
bOK = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (bOK)
|
|
{
|
|
TaskComplete();
|
|
}
|
|
else
|
|
{
|
|
TaskFail();
|
|
}
|
|
}
|
|
break;
|
|
case TASK_SMALL_FLINCH:
|
|
if (m_idealDist > 128)
|
|
{
|
|
m_flMaxDist = 512;
|
|
m_idealDist = 512;
|
|
}
|
|
else
|
|
{
|
|
//m_bOnAttack = TRUE;
|
|
}
|
|
CFlyingMonster::StartTask(pTask);
|
|
break;
|
|
|
|
case TASK_STUKABAT_FALL:
|
|
m_movementActivity = ACT_FALL;
|
|
//m_IdealActivity = ACT_FALL;
|
|
SetActivity ( ACT_FALL );
|
|
//SetSequenceByName( "fall_cycler" );
|
|
break;
|
|
|
|
// overridden task
|
|
case TASK_WALK_PATH:
|
|
{
|
|
switch (meMedium)
|
|
{
|
|
case SB_ONGROUND:
|
|
m_movementActivity = ACT_WALK;
|
|
break;
|
|
case SB_ONCEILING:
|
|
case SB_INAIR:
|
|
default:
|
|
m_movementActivity = ACT_FLY;
|
|
break;
|
|
}
|
|
TaskComplete();
|
|
break;
|
|
}
|
|
|
|
case TASK_GET_PATH_TO_SPOT:
|
|
{
|
|
// get the target location
|
|
if (m_hEnemy == NULL)
|
|
{
|
|
TaskFail();
|
|
break;
|
|
}
|
|
|
|
//CBaseEntity* pEnemy = CBaseEntity::Instance(m_hEnemy);
|
|
CBaseEntity* pEnemy = (m_hEnemy);
|
|
|
|
TraceResult tr;
|
|
|
|
// trace directly up for 1024
|
|
Vector up = pEnemy->pev->origin;
|
|
up.z += 1024;
|
|
|
|
UTIL_TraceLine( pEnemy->pev->origin, up, ignore_monsters, NULL, &tr );
|
|
|
|
// choose highest point (minus a bit)
|
|
m_vecMoveGoal = tr.vecEndPos;
|
|
m_vecMoveGoal.z -= 64;
|
|
|
|
// will we be too close to his head?
|
|
if ((pEnemy->pev->origin - m_vecMoveGoal).Length() < 128)
|
|
{
|
|
TaskFail();
|
|
break;
|
|
}
|
|
|
|
if ( BuildRoute ( m_vecMoveGoal, bits_MF_TO_LOCATION, NULL ) )
|
|
{
|
|
TaskComplete();
|
|
}
|
|
else
|
|
{
|
|
// no way to get there =(
|
|
ALERT ( at_aiconsole, "GetPathToSpot failed!!\n" );
|
|
TaskFail();
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
CFlyingMonster::StartTask(pTask);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void CStukabat :: RunTask ( Task_t *pTask )
|
|
{
|
|
switch ( pTask->iTask )
|
|
{
|
|
case TASK_STUKABAT_CIRCLE_ENEMY:
|
|
if (m_hEnemy == NULL)
|
|
{
|
|
TaskComplete( );
|
|
}
|
|
else if (FVisible( m_hEnemy ))
|
|
{
|
|
Vector vecFrom = m_hEnemy->EyePosition( );
|
|
|
|
Vector vecDelta = (pev->origin - vecFrom).Normalize( );
|
|
Vector vecFly = CrossProduct( vecDelta, Vector( 0, 0, 1 ) ).Normalize( );
|
|
|
|
if (DotProduct( vecFly, m_SaveVelocity ) < 0)
|
|
vecFly = vecFly * -1.0;
|
|
|
|
Vector vecPos = vecFrom + vecDelta * m_idealDist + vecFly * 32;
|
|
|
|
// ALERT( at_console, "vecPos %.0f %.0f %.0f\n", vecPos.x, vecPos.y, vecPos.z );
|
|
|
|
TraceResult tr;
|
|
|
|
UTIL_TraceHull( vecFrom, vecPos, ignore_monsters, large_hull, m_hEnemy->edict(), &tr );
|
|
|
|
if (tr.flFraction > 0.5)
|
|
vecPos = tr.vecEndPos;
|
|
|
|
m_SaveVelocity = m_SaveVelocity * 0.8 + 0.2 * (vecPos - pev->origin).Normalize() * m_flightSpeed;
|
|
|
|
// ALERT( at_console, "m_SaveVelocity %.2f %.2f %.2f\n", m_SaveVelocity.x, m_SaveVelocity.y, m_SaveVelocity.z );
|
|
|
|
if (HasConditions( bits_COND_ENEMY_FACING_ME ) && m_hEnemy->FVisible( this ))
|
|
{
|
|
m_flNextAlert -= 0.1;
|
|
|
|
if (m_idealDist < m_flMaxDist)
|
|
{
|
|
m_idealDist += 4;
|
|
}
|
|
if (m_flightSpeed > m_flMinSpeed)
|
|
{
|
|
m_flightSpeed -= 2;
|
|
}
|
|
else if (m_flightSpeed < m_flMinSpeed)
|
|
{
|
|
m_flightSpeed += 2;
|
|
}
|
|
if (m_flMinSpeed < m_flMaxSpeed)
|
|
{
|
|
m_flMinSpeed += 0.5;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_flNextAlert += 0.1;
|
|
|
|
if (m_idealDist > 128)
|
|
{
|
|
m_idealDist -= 4;
|
|
}
|
|
if (m_flightSpeed < m_flMaxSpeed)
|
|
{
|
|
m_flightSpeed += 4;
|
|
}
|
|
}
|
|
// ALERT( at_console, "%.0f\n", m_idealDist );
|
|
}
|
|
else
|
|
{
|
|
m_flNextAlert = gpGlobals->time + 0.2;
|
|
}
|
|
|
|
if (m_flNextAlert < gpGlobals->time)
|
|
{
|
|
// ALERT( at_console, "AlertSound()\n");
|
|
AlertSound( );
|
|
m_flNextAlert = gpGlobals->time + RANDOM_FLOAT( 3, 5 );
|
|
}
|
|
|
|
// PhilG: they will circle me forever unless I break it...
|
|
if (RANDOM_LONG(0,50) == 1)
|
|
{
|
|
TaskComplete();
|
|
}
|
|
|
|
break;
|
|
case TASK_STUKABAT_FLY:
|
|
//if (m_fSequenceFinished)
|
|
// the sequence loops, so it is very rare that the 'finished' flag is set
|
|
// set we randomly finish the task, which will recall the result in the
|
|
// getting of a new schedule (possibly the same one again)
|
|
if (RANDOM_LONG(0,20) == 1)
|
|
{
|
|
TaskComplete( );
|
|
}
|
|
break;
|
|
case TASK_STUKABAT_MOVETOGROUND:
|
|
{
|
|
// how far above the floor are we?
|
|
float fz = FloorZ(pev->origin);
|
|
if (pev->origin.z - fz < 6)
|
|
{
|
|
// we are very close to the floor, so drop the last little bit
|
|
DROP_TO_FLOOR(ENT(pev));
|
|
TaskComplete( );
|
|
}
|
|
else
|
|
{
|
|
int iContents = UTIL_PointContents(pev->origin + Vector(0,0,-6));
|
|
if ((iContents == CONTENTS_WATER) ||
|
|
(iContents == CONTENTS_LAVA) ||
|
|
(iContents == CONTENTS_SLIME))
|
|
{
|
|
TaskFail();
|
|
break;
|
|
}
|
|
// note: the stukabat origin is 2 units above the bottom of its hull
|
|
// hence if we are checking for 6 units (above), we can only safely push down 4 units
|
|
if ( CheckLocalMove ( pev->origin, pev->origin + Vector(0,0,-4), NULL, NULL ) == LOCALMOVE_VALID)
|
|
{
|
|
pev->origin.z -= 4;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case TASK_STUKABAT_LANDGROUND:
|
|
if (m_fSequenceFinished)
|
|
{
|
|
TaskComplete( );
|
|
}
|
|
break;
|
|
case TASK_STUKABAT_TAKEOFFGROUND:
|
|
if (m_fSequenceFinished)
|
|
{
|
|
TaskComplete( );
|
|
}
|
|
break;
|
|
case TASK_STUKABAT_FIND_NODE:
|
|
if (mpCeilingNode)
|
|
{
|
|
TaskComplete( );
|
|
}
|
|
else
|
|
{
|
|
TaskFail();
|
|
}
|
|
break;
|
|
case TASK_STUKABAT_LANDCEILING:
|
|
if (m_fSequenceFinished)
|
|
{
|
|
mpCeilingNode->mbInUse = true;
|
|
TaskComplete( );
|
|
}
|
|
break;
|
|
case TASK_STUKABAT_TAKEOFFCEILING:
|
|
{
|
|
mpCeilingNode->mbInUse = false;
|
|
mpCeilingNode = NULL;
|
|
TaskComplete( );
|
|
}
|
|
break;
|
|
case TASK_RANGE_ATTACK2:
|
|
{
|
|
MakeIdealYaw ( m_vecEnemyLKP );
|
|
ChangeYaw ( pev->yaw_speed );
|
|
|
|
if ( m_fSequenceFinished )
|
|
{
|
|
m_Activity = ACT_RESET;
|
|
TaskComplete();
|
|
}
|
|
if (gpGlobals->time > m_flNextMeleeAttack)
|
|
{
|
|
// do the attack
|
|
Slash();
|
|
// set the next melee attack time
|
|
m_flNextMeleeAttack = gpGlobals->time + RANDOM_FLOAT(3.0,6.0);
|
|
}
|
|
}
|
|
break;
|
|
case TASK_STUKABAT_FALL:
|
|
if (FBitSet(pev->flags, FL_ONGROUND))
|
|
{
|
|
if ( RANDOM_LONG(0,4) == 1 )
|
|
{
|
|
m_IdealActivity = ACT_DIEVIOLENT;
|
|
}
|
|
else
|
|
{
|
|
m_IdealActivity = ACT_DIESIMPLE;
|
|
}
|
|
TaskComplete( );
|
|
}
|
|
else
|
|
{
|
|
if (m_fSequenceFinished)
|
|
{
|
|
SetActivity ( ACT_FALL );
|
|
}
|
|
MoveExecute(NULL, Vector(0,0,-1), 0.1);
|
|
}
|
|
break;
|
|
case TASK_DIE:
|
|
if ( m_fSequenceFinished )
|
|
{
|
|
CFlyingMonster :: RunTask ( pTask );
|
|
|
|
pev->deadflag = DEAD_DEAD;
|
|
|
|
TaskComplete( );
|
|
}
|
|
break;
|
|
|
|
default:
|
|
CFlyingMonster :: RunTask ( pTask );
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
float CStukabat::VectorToPitch( const Vector &vec )
|
|
{
|
|
float pitch;
|
|
if (vec.z == 0 && vec.x == 0)
|
|
pitch = 0;
|
|
else
|
|
{
|
|
pitch = (int) (atan2(vec.z, sqrt(vec.x*vec.x+vec.y*vec.y)) * 180 / M_PI);
|
|
if (pitch < 0)
|
|
pitch += 360;
|
|
}
|
|
return pitch;
|
|
}
|
|
|
|
//=========================================================
|
|
void CStukabat::FindNode()
|
|
{
|
|
mpCeilingNode = NULL;
|
|
|
|
Vector vDistance;
|
|
const int MAX_NODE = 1000;
|
|
int iNode = 0;
|
|
CStukabatNode* pNodesInSphere[MAX_NODE];
|
|
CBaseEntity* pNode = UTIL_FindEntityByClassname(NULL, "info_node_stukabat");
|
|
while (pNode)
|
|
{
|
|
vDistance = pev->origin - pNode->pev->origin;
|
|
if (vDistance.Length() <= 1024.0 && !((CStukabatNode*)pNode)->mbInUse)
|
|
{
|
|
pNodesInSphere[iNode] = (CStukabatNode*)pNode;
|
|
iNode++;
|
|
}
|
|
|
|
pNode = UTIL_FindEntityByClassname(pNode, "info_node_stukabat");
|
|
}
|
|
|
|
// no viable nodes found
|
|
if (iNode == 0) return;
|
|
|
|
// choose a viable node at random
|
|
int iRand = RANDOM_LONG(0,iNode-1);
|
|
|
|
mpCeilingNode = pNodesInSphere[iRand];
|
|
}
|
|
|
|
int CStukabat :: CheckLocalMove ( const Vector &vecStart, const Vector &vecEnd, CBaseEntity *pTarget, float *pflDist )
|
|
{
|
|
int iContents = UTIL_PointContents(vecEnd);
|
|
if ((iContents == CONTENTS_WATER) ||
|
|
(iContents == CONTENTS_LAVA) ||
|
|
(iContents == CONTENTS_SLIME))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
TraceResult tr;
|
|
|
|
UTIL_TraceHull( vecStart, vecEnd, dont_ignore_monsters, head_hull, edict(), &tr );
|
|
|
|
// ALERT( at_console, "%.0f %.0f %.0f : ", vecStart.x, vecStart.y, vecStart.z );
|
|
// ALERT( at_console, "%.0f %.0f %.0f\n", vecEnd.x, vecEnd.y, vecEnd.z );
|
|
|
|
if (pflDist)
|
|
{
|
|
*pflDist = ( tr.vecEndPos - vecStart ).Length();// get the distance.
|
|
}
|
|
|
|
// ALERT( at_console, "check %d %d %f\n", tr.fStartSolid, tr.fAllSolid, tr.flFraction );
|
|
if (tr.fStartSolid || tr.flFraction < 1.0)
|
|
{
|
|
// PhilG
|
|
if (gpGlobals->trace_ent)
|
|
{
|
|
CBaseEntity* pBlocker = CBaseEntity::Instance(gpGlobals->trace_ent);
|
|
}
|
|
|
|
if ( tr.flFraction < 1.0 && pTarget && pTarget->edict() == gpGlobals->trace_ent )
|
|
return LOCALMOVE_VALID;
|
|
return LOCALMOVE_INVALID;
|
|
}
|
|
|
|
return LOCALMOVE_VALID;
|
|
}
|
|
|
|
void CStukabat::Move(float flInterval)
|
|
{
|
|
if (meMedium == SB_INAIR)
|
|
{
|
|
CFlyingMonster::Move( flInterval );
|
|
}
|
|
else
|
|
{
|
|
float flWaypointDist;
|
|
Vector vecApex;
|
|
|
|
// local move to waypoint.
|
|
flWaypointDist = ( m_Route[ m_iRouteIndex ].vecLocation - pev->origin ).Length2D();
|
|
MakeIdealYaw ( m_Route[ m_iRouteIndex ].vecLocation );
|
|
|
|
ChangeYaw ( pev->yaw_speed );
|
|
UTIL_MakeVectors( pev->angles );
|
|
|
|
if ( RANDOM_LONG(0,7) == 1 )
|
|
{
|
|
// randomly check for blocked path.(more random load balancing)
|
|
if ( !WALK_MOVE( ENT(pev), pev->ideal_yaw, 4, WALKMOVE_NORMAL ) )
|
|
{
|
|
// stuck, so just pick a new spot to run off to
|
|
PickNewDest( m_iMode );
|
|
}
|
|
}
|
|
|
|
WALK_MOVE( ENT(pev), pev->ideal_yaw, m_flGroundSpeed * flInterval, WALKMOVE_NORMAL );
|
|
|
|
// if the waypoint is closer than step size, then stop after next step (ok for rat to overshoot)
|
|
if ( flWaypointDist <= m_flGroundSpeed * flInterval )
|
|
{
|
|
// take truncated step and stop
|
|
|
|
SetActivity ( ACT_CROUCHIDLE );
|
|
|
|
if ( m_iMode == STUKABAT_SMELL_FOOD )
|
|
{
|
|
m_iMode = STUKABAT_EAT;
|
|
}
|
|
else
|
|
{
|
|
m_iMode = STUKABAT_IDLE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
float CStukabat::FlPitchDiff( void )
|
|
{
|
|
float flPitchDiff;
|
|
float flCurrentPitch;
|
|
|
|
flCurrentPitch = UTIL_AngleMod( pev->angles.z );
|
|
|
|
if ( flCurrentPitch == pev->idealpitch )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
flPitchDiff = pev->idealpitch - flCurrentPitch;
|
|
|
|
if ( pev->idealpitch > flCurrentPitch )
|
|
{
|
|
if (flPitchDiff >= 180)
|
|
flPitchDiff = flPitchDiff - 360;
|
|
}
|
|
else
|
|
{
|
|
if (flPitchDiff <= -180)
|
|
flPitchDiff = flPitchDiff + 360;
|
|
}
|
|
return flPitchDiff;
|
|
}
|
|
|
|
float CStukabat :: ChangePitch( int speed )
|
|
{
|
|
if ( pev->movetype == MOVETYPE_FLY )
|
|
{
|
|
float diff = FlPitchDiff();
|
|
float target = 0;
|
|
if ( m_IdealActivity != GetStoppedActivity() )
|
|
{
|
|
if (diff < -20)
|
|
target = 45;
|
|
else if (diff > 20)
|
|
target = -45;
|
|
}
|
|
pev->angles.x = UTIL_Approach(target, pev->angles.x, 220.0 * 0.1 );
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
float CStukabat::ChangeYaw( int speed )
|
|
{
|
|
if ( pev->movetype == MOVETYPE_FLY )
|
|
{
|
|
float diff = FlYawDiff();
|
|
float target = 0;
|
|
|
|
if ( m_IdealActivity != GetStoppedActivity() )
|
|
{
|
|
if ( diff < -20 )
|
|
target = 20;
|
|
else if ( diff > 20 )
|
|
target = -20;
|
|
}
|
|
pev->angles.z = UTIL_Approach( target, pev->angles.z, 220.0 * 0.1 );
|
|
}
|
|
return CFlyingMonster::ChangeYaw( speed );
|
|
}
|
|
|
|
|
|
Activity CStukabat:: GetStoppedActivity( void )
|
|
{
|
|
//if ( pev->movetype != MOVETYPE_FLY ) // UNDONE: Ground idle here, IDLE may be something else
|
|
// return ACT_IDLE;
|
|
//return ACT_WALK;
|
|
switch (meMedium)
|
|
{
|
|
case SB_ONGROUND:
|
|
return ACT_CROUCHIDLE;
|
|
break;
|
|
case SB_ONCEILING:
|
|
return ACT_IDLE;
|
|
break;
|
|
case SB_INAIR:
|
|
default:
|
|
return ACT_FLY;
|
|
break;
|
|
}
|
|
}
|
|
|
|
void CStukabat::MoveExecute( CBaseEntity *pTargetEnt, const Vector &vecDir, float flInterval )
|
|
{
|
|
m_SaveVelocity = vecDir * m_flightSpeed;
|
|
CFlyingMonster::MoveExecute(pTargetEnt, vecDir, flInterval);
|
|
}
|
|
|
|
|
|
void CStukabat::MonsterThink ( void )
|
|
{
|
|
float flInterval = 0.1;
|
|
|
|
if (!IsAlive() || pev->deadflag == DEAD_DYING) m_iMode = STUKABAT_DEAD;
|
|
|
|
if (pev->deadflag == DEAD_NO)
|
|
{
|
|
if (m_MonsterState != MONSTERSTATE_SCRIPT)
|
|
{
|
|
switch (meMedium)
|
|
{
|
|
case SB_ONCEILING:
|
|
MonsterThinkOnCeiling();
|
|
break;
|
|
case SB_ONGROUND:
|
|
MonsterThinkOnGround();
|
|
break;
|
|
case SB_INAIR:
|
|
default:
|
|
MonsterThinkInAir();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (pev->deadflag == DEAD_DYING)
|
|
{
|
|
if (m_MonsterState != MONSTERSTATE_SCRIPT)
|
|
{
|
|
SetNextThink( 0.1 );// keep monster thinking.
|
|
RunAI();
|
|
DispatchAnimEvents( );
|
|
StudioFrameAdvance( );
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void CStukabat::MonsterThinkInAir ( void )
|
|
{
|
|
// we just want to run the activity if we are doing any of the following
|
|
// taking off from the ground
|
|
// landing (ground or ceiling)
|
|
// attacking
|
|
if ((m_IdealActivity == ACT_LEAP) ||
|
|
(m_IdealActivity == ACT_LAND) ||
|
|
(m_IdealActivity == ACT_STAND))
|
|
{
|
|
SetNextThink( 0.1 );// keep monster thinking
|
|
RunAI();
|
|
StudioFrameAdvance( ); // animate
|
|
return;
|
|
}
|
|
|
|
CFlyingMonster::MonsterThink( );
|
|
|
|
// if we are not dead, in the air and not attacking
|
|
if (m_MonsterState != MONSTERSTATE_DEAD &&
|
|
meMedium == SB_INAIR &&
|
|
m_IdealActivity != ACT_RANGE_ATTACK1 &&
|
|
m_IdealActivity != ACT_RANGE_ATTACK2)
|
|
{
|
|
Fly();
|
|
}
|
|
}
|
|
|
|
void CStukabat::MonsterThinkOnCeiling ( void )
|
|
{
|
|
CBaseMonster::MonsterThink( );
|
|
}
|
|
|
|
void CStukabat::MonsterThinkOnGround ( void )
|
|
{
|
|
if ( FNullEnt( FIND_CLIENT_IN_PVS( edict() ) ) )
|
|
SetNextThink( RANDOM_FLOAT(1,1.5) );
|
|
else
|
|
SetNextThink( 0.1 );// keep monster thinking
|
|
|
|
RunAI();
|
|
|
|
if (m_IdealActivity == ACT_IDLE)
|
|
{
|
|
pev->sequence = LookupActivity ( ACT_CROUCHIDLE );
|
|
m_Activity = ACT_CROUCHIDLE;
|
|
m_IdealActivity = ACT_CROUCHIDLE;
|
|
}
|
|
if (m_IdealActivity == ACT_CROUCHIDLE && m_fSequenceFinished)
|
|
{
|
|
SetActivity(ACT_CROUCHIDLE);
|
|
}
|
|
|
|
float flInterval = StudioFrameAdvance( ); // animate
|
|
|
|
// if we are landing, do nothing
|
|
if (m_IdealActivity == ACT_LAND) return;
|
|
|
|
switch (m_iMode)
|
|
{
|
|
case STUKABAT_IDLE:
|
|
case STUKABAT_EAT:
|
|
{
|
|
// if not moving, sample environment to see if anything scary is around. Do a radius search 'look' at random.
|
|
if ( RANDOM_LONG(0,1) == 1 )
|
|
{
|
|
Look( 150 );
|
|
if (HasConditions(bits_COND_SEE_FEAR))
|
|
{
|
|
// if see something scary
|
|
//ALERT ( at_aiconsole, "Scared\n" );
|
|
Eat( 30 + ( RANDOM_LONG(0,14) ) );// rat will ignore food for 30 to 45 seconds
|
|
PickNewDest( STUKABAT_SCARED_BY_ENT );
|
|
SetActivity ( ACT_RUN );
|
|
}
|
|
else if ( RANDOM_LONG(0,1) == 1 )
|
|
{
|
|
// if rat doesn't see anything, there's still a chance that it will move. (boredom)
|
|
//ALERT ( at_aiconsole, "Bored\n" );
|
|
PickNewDest( STUKABAT_BORED );
|
|
SetActivity ( ACT_WALK );
|
|
|
|
if ( m_iMode == STUKABAT_EAT )
|
|
{
|
|
// rat will ignore food for 30 to 45 seconds if it got bored while eating.
|
|
Eat( 30 + ( RANDOM_LONG(0,14) ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
// don't do this stuff if eating!
|
|
if ( m_iMode == STUKABAT_IDLE )
|
|
{
|
|
if ( FShouldEat() )
|
|
{
|
|
Listen();
|
|
}
|
|
|
|
if ( HasConditions(bits_COND_SMELL_FOOD) )
|
|
{
|
|
CSound *pSound;
|
|
|
|
pSound = CSoundEnt::SoundPointerForIndex( m_iAudibleList );
|
|
|
|
// rat smells food and is just standing around. Go to food unless food isn't on same z-plane.
|
|
if ( pSound && abs( pSound->m_vecOrigin.z - pev->origin.z ) <= 3 )
|
|
{
|
|
PickNewDest( STUKABAT_SMELL_FOOD );
|
|
SetActivity ( ACT_WALK );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//SetActivity ( ACT_CROUCHIDLE );
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case STUKABAT_DEAD:
|
|
return;
|
|
break;
|
|
}
|
|
if ( m_flGroundSpeed != 0 )
|
|
{
|
|
Move( 0.1 );
|
|
}
|
|
}
|
|
|
|
void CStukabat :: Stop( void )
|
|
{
|
|
//if (!m_bOnAttack)
|
|
m_flightSpeed = 80.0;
|
|
}
|
|
|
|
void CStukabat::Fly( )
|
|
{
|
|
int retValue = 0;
|
|
|
|
Vector start = pev->origin;
|
|
|
|
Vector Angles;
|
|
Vector Forward, Right, Up;
|
|
|
|
/*
|
|
if (FBitSet( pev->flags, FL_ONGROUND))
|
|
{
|
|
pev->angles.x = 0;
|
|
pev->angles.y += RANDOM_FLOAT( -45, 45 );
|
|
ClearBits( pev->flags, FL_ONGROUND );
|
|
|
|
Angles = Vector( -pev->angles.x, pev->angles.y, pev->angles.z );
|
|
UTIL_MakeVectorsPrivate(Angles, Forward, Right, Up);
|
|
|
|
pev->velocity = Forward * 200 + Up * 200;
|
|
|
|
return;
|
|
}
|
|
*/
|
|
|
|
//if (meMedium == SB_ONGROUND)
|
|
//{
|
|
// pev->angles.x = 0;
|
|
// SetActivity( ACT_WALK );
|
|
// return;
|
|
//}
|
|
|
|
//if (m_bOnAttack && m_flightSpeed < m_flMaxSpeed)
|
|
if (m_flightSpeed < m_flMaxSpeed)
|
|
{
|
|
m_flightSpeed += 40;
|
|
}
|
|
|
|
SetActivity( ACT_FLY );
|
|
|
|
/*
|
|
if (m_flightSpeed < 180)
|
|
{
|
|
SetActivity( ACT_FLY );
|
|
//if (m_IdealActivity == ACT_RUN)
|
|
// SetActivity( ACT_WALK );
|
|
//if (m_IdealActivity == ACT_WALK)
|
|
// pev->framerate = m_flightSpeed / 150.0;
|
|
// ALERT( at_console, "walk %.2f\n", pev->framerate );
|
|
}
|
|
else
|
|
{
|
|
switch (meMedium)
|
|
{
|
|
case SB_ONGROUND:
|
|
//SetActivity( ACT_WALK );
|
|
break;
|
|
case SB_ONCEILING:
|
|
SetActivity( ACT_WALK );
|
|
break;
|
|
default:
|
|
SetActivity( ACT_FLY );
|
|
break;
|
|
}
|
|
//if (m_IdealActivity == ACT_WALK)
|
|
// SetActivity( ACT_RUN );
|
|
//if (m_IdealActivity == ACT_RUN)
|
|
// pev->framerate = m_flightSpeed / 150.0;
|
|
// ALERT( at_console, "run %.2f\n", pev->framerate );
|
|
}
|
|
*/
|
|
|
|
#define PROBE_LENGTH 150
|
|
|
|
Angles = UTIL_VecToAngles( m_SaveVelocity );
|
|
Angles.x = -Angles.x;
|
|
UTIL_MakeVectorsPrivate(Angles, Forward, Right, Up);
|
|
|
|
Vector f, u, l, r, d;
|
|
f = DoProbe(start + PROBE_LENGTH * Forward);
|
|
r = DoProbe(start + PROBE_LENGTH/3 * (Forward+Right));
|
|
l = DoProbe(start + PROBE_LENGTH/3 * (Forward-Right));
|
|
u = DoProbe(start + PROBE_LENGTH/3 * (Forward+Up));
|
|
d = DoProbe(start + PROBE_LENGTH/3 * (Forward-Up));
|
|
|
|
Vector SteeringVector = f+r+l+u+d;
|
|
m_SaveVelocity = (m_SaveVelocity + SteeringVector/2).Normalize();
|
|
|
|
Angles = Vector( -pev->angles.x, pev->angles.y, pev->angles.z );
|
|
UTIL_MakeVectorsPrivate(Angles, Forward, Right, Up);
|
|
// ALERT( at_console, "%f : %f\n", Angles.x, Forward.z );
|
|
|
|
float flDot = DotProduct( Forward, m_SaveVelocity );
|
|
if (flDot > 0.5)
|
|
pev->velocity = m_SaveVelocity = m_SaveVelocity * m_flightSpeed;
|
|
else if (flDot > 0)
|
|
pev->velocity = m_SaveVelocity = m_SaveVelocity * m_flightSpeed * (flDot + 0.5);
|
|
else
|
|
pev->velocity = m_SaveVelocity = m_SaveVelocity * 80;
|
|
|
|
|
|
Angles = UTIL_VecToAngles( m_SaveVelocity );
|
|
|
|
// Smooth Pitch
|
|
//
|
|
if (Angles.x > 180)
|
|
Angles.x = Angles.x - 360;
|
|
pev->angles.x = UTIL_Approach(Angles.x, pev->angles.x, 50 * 0.1 );
|
|
if (pev->angles.x < -80) pev->angles.x = -80;
|
|
if (pev->angles.x > 80) pev->angles.x = 80;
|
|
|
|
// Smooth Yaw and generate Roll
|
|
//
|
|
float turn = 360;
|
|
// ALERT( at_console, "Y %.0f %.0f\n", Angles.y, pev->angles.y );
|
|
|
|
if (fabs(Angles.y - pev->angles.y) < fabs(turn))
|
|
{
|
|
turn = Angles.y - pev->angles.y;
|
|
}
|
|
if (fabs(Angles.y - pev->angles.y + 360) < fabs(turn))
|
|
{
|
|
turn = Angles.y - pev->angles.y + 360;
|
|
}
|
|
if (fabs(Angles.y - pev->angles.y - 360) < fabs(turn))
|
|
{
|
|
turn = Angles.y - pev->angles.y - 360;
|
|
}
|
|
|
|
float speed = m_flightSpeed * 0.1;
|
|
|
|
// ALERT( at_console, "speed %.0f %f\n", turn, speed );
|
|
if (fabs(turn) > speed)
|
|
{
|
|
if (turn < 0.0)
|
|
{
|
|
turn = -speed;
|
|
}
|
|
else
|
|
{
|
|
turn = speed;
|
|
}
|
|
}
|
|
pev->angles.y += turn;
|
|
pev->angles.z -= turn;
|
|
pev->angles.y = fmod((pev->angles.y + 360.0), 360.0);
|
|
|
|
static float yaw_adj;
|
|
|
|
yaw_adj = yaw_adj * 0.8 + turn;
|
|
|
|
// ALERT( at_console, "yaw %f : %f\n", turn, yaw_adj );
|
|
|
|
SetBoneController( 0, -yaw_adj / 4.0 );
|
|
|
|
// Roll Smoothing
|
|
//
|
|
turn = 360;
|
|
if (fabs(Angles.z - pev->angles.z) < fabs(turn))
|
|
{
|
|
turn = Angles.z - pev->angles.z;
|
|
}
|
|
if (fabs(Angles.z - pev->angles.z + 360) < fabs(turn))
|
|
{
|
|
turn = Angles.z - pev->angles.z + 360;
|
|
}
|
|
if (fabs(Angles.z - pev->angles.z - 360) < fabs(turn))
|
|
{
|
|
turn = Angles.z - pev->angles.z - 360;
|
|
}
|
|
speed = m_flightSpeed/2 * 0.1;
|
|
if (fabs(turn) < speed)
|
|
{
|
|
pev->angles.z += turn;
|
|
}
|
|
else
|
|
{
|
|
if (turn < 0.0)
|
|
{
|
|
pev->angles.z -= speed;
|
|
}
|
|
else
|
|
{
|
|
pev->angles.z += speed;
|
|
}
|
|
}
|
|
if (pev->angles.z < -20) pev->angles.z = -20;
|
|
if (pev->angles.z > 20) pev->angles.z = 20;
|
|
|
|
UTIL_MakeVectorsPrivate( Vector( -Angles.x, Angles.y, Angles.z ), Forward, Right, Up);
|
|
|
|
}
|
|
|
|
|
|
void CStukabat::LandGround(void)
|
|
{
|
|
ClearBits( pev->flags, FL_FLY );
|
|
meMedium = SB_ONGROUND;
|
|
m_afCapability = bits_CAP_RANGE_ATTACK2;
|
|
pev->movetype = MOVETYPE_STEP;
|
|
pev->angles.x = 0;
|
|
//pev->angles.z = 0;
|
|
m_flLandTime = gpGlobals->time;
|
|
}
|
|
|
|
void CStukabat::LandCeiling(void)
|
|
{
|
|
meMedium = SB_ONCEILING;
|
|
m_afCapability = bits_CAP_FLY;
|
|
pev->movetype = MOVETYPE_FLY;
|
|
pev->angles.x = 0;
|
|
//pev->angles.z = 0;
|
|
m_flLandTime = gpGlobals->time;
|
|
}
|
|
|
|
void CStukabat::TakeOffGround(void)
|
|
{
|
|
SetBits( pev->flags, FL_FLY );
|
|
meMedium = SB_INAIR;
|
|
m_afCapability = bits_CAP_RANGE_ATTACK2 | bits_CAP_FLY;
|
|
pev->movetype = MOVETYPE_FLY;
|
|
m_flLandTime = gpGlobals->time;
|
|
}
|
|
|
|
Vector CStukabat::DoProbe(const Vector &Probe)
|
|
{
|
|
Vector WallNormal = Vector(0,0,1); // AIR normal is Straight Up for flying thing.
|
|
float frac;
|
|
BOOL bBumpedSomething = ProbeZ(pev->origin, Probe, &frac);
|
|
|
|
TraceResult tr;
|
|
TRACE_MONSTER_HULL(edict(), pev->origin, Probe, dont_ignore_monsters, edict(), &tr);
|
|
if ( tr.fAllSolid || tr.flFraction < 0.99 )
|
|
{
|
|
if (tr.flFraction < 0.0) tr.flFraction = 0.0;
|
|
if (tr.flFraction > 1.0) tr.flFraction = 1.0;
|
|
if (tr.flFraction < frac)
|
|
{
|
|
frac = tr.flFraction;
|
|
bBumpedSomething = TRUE;
|
|
WallNormal = tr.vecPlaneNormal;
|
|
}
|
|
}
|
|
|
|
if (bBumpedSomething && (m_hEnemy == NULL || tr.pHit != m_hEnemy->edict()))
|
|
{
|
|
Vector ProbeDir = Probe - pev->origin;
|
|
|
|
Vector NormalToProbeAndWallNormal = CrossProduct(ProbeDir, WallNormal);
|
|
Vector SteeringVector = CrossProduct( NormalToProbeAndWallNormal, ProbeDir);
|
|
|
|
float SteeringForce = m_flightSpeed * (1-frac) * (DotProduct(WallNormal.Normalize(), m_SaveVelocity.Normalize()));
|
|
if (SteeringForce < 0.0)
|
|
{
|
|
SteeringForce = -SteeringForce;
|
|
}
|
|
SteeringVector = SteeringForce * SteeringVector.Normalize();
|
|
|
|
return SteeringVector;
|
|
}
|
|
return Vector(0, 0, 0);
|
|
}
|
|
|
|
//=========================================================
|
|
// Picks a new spot for rat to run to.(
|
|
//=========================================================
|
|
void CStukabat :: PickNewDest ( int iCondition )
|
|
{
|
|
Vector vecNewDir;
|
|
Vector vecDest;
|
|
float flDist;
|
|
|
|
m_iMode = iCondition;
|
|
|
|
if ( m_iMode == STUKABAT_SMELL_FOOD )
|
|
{
|
|
// find the food and go there.
|
|
CSound *pSound;
|
|
|
|
pSound = CSoundEnt::SoundPointerForIndex( m_iAudibleList );
|
|
|
|
if ( pSound )
|
|
{
|
|
m_Route[ 0 ].vecLocation.x = pSound->m_vecOrigin.x + ( 3 - RANDOM_LONG(0,5) );
|
|
m_Route[ 0 ].vecLocation.y = pSound->m_vecOrigin.y + ( 3 - RANDOM_LONG(0,5) );
|
|
m_Route[ 0 ].vecLocation.z = pSound->m_vecOrigin.z;
|
|
m_Route[ 0 ].iType = bits_MF_TO_LOCATION;
|
|
m_movementGoal = RouteClassify( m_Route[ 0 ].iType );
|
|
return;
|
|
}
|
|
}
|
|
|
|
do
|
|
{
|
|
// picks a random spot, requiring that it be at least 128 units away
|
|
// else, the rat will pick a spot too close to itself and run in
|
|
// circles. this is a hack but buys me time to work on the real monsters.
|
|
vecNewDir.x = RANDOM_FLOAT( -1, 1 );
|
|
vecNewDir.y = RANDOM_FLOAT( -1, 1 );
|
|
flDist = 256 + ( RANDOM_LONG(0,255) );
|
|
vecDest = pev->origin + vecNewDir * flDist;
|
|
|
|
} while ( ( vecDest - pev->origin ).Length2D() < 128 );
|
|
|
|
m_Route[ 0 ].vecLocation.x = vecDest.x;
|
|
m_Route[ 0 ].vecLocation.y = vecDest.y;
|
|
m_Route[ 0 ].vecLocation.z = pev->origin.z;
|
|
m_Route[ 0 ].iType = bits_MF_TO_LOCATION;
|
|
m_movementGoal = RouteClassify( m_Route[ 0 ].iType );
|
|
|
|
if ( RANDOM_LONG(0,9) == 1 )
|
|
{
|
|
// every once in a while, a rat will play a skitter sound when they decide to run
|
|
EMIT_SOUND_DYN(ENT(pev), CHAN_BODY, "roach/rch_walk.wav", 1, ATTN_NORM, 0, 80 + RANDOM_LONG(0,39) );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
#endif |