1165 lines
30 KiB
C++
1165 lines
30 KiB
C++
/***
|
|
*
|
|
* Copyright (c) 1996-2001, 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.
|
|
*
|
|
****/
|
|
//=========================================================
|
|
// Agrunt - Dominant, warlike alien grunt monster
|
|
//=========================================================
|
|
|
|
#include "extdll.h"
|
|
#include "util.h"
|
|
#include "cbase.h"
|
|
#include "monsters.h"
|
|
#include "schedule.h"
|
|
#include "squadmonster.h"
|
|
#include "weapons.h"
|
|
#include "soundent.h"
|
|
#include "hornet.h"
|
|
|
|
//=========================================================
|
|
// monster-specific schedule types
|
|
//=========================================================
|
|
enum
|
|
{
|
|
SCHED_AGRUNT_SUPPRESS = LAST_COMMON_SCHEDULE + 1,
|
|
SCHED_AGRUNT_THREAT_DISPLAY,
|
|
};
|
|
|
|
//=========================================================
|
|
// monster-specific tasks
|
|
//=========================================================
|
|
enum
|
|
{
|
|
TASK_AGRUNT_SETUP_HIDE_ATTACK = LAST_COMMON_TASK + 1,
|
|
TASK_AGRUNT_GET_PATH_TO_ENEMY_CORPSE,
|
|
};
|
|
|
|
int iAgruntMuzzleFlash;
|
|
|
|
//=========================================================
|
|
// Monster's Anim Events Go Here
|
|
//=========================================================
|
|
#define AGRUNT_AE_HORNET1 (1)
|
|
#define AGRUNT_AE_HORNET2 (2)
|
|
#define AGRUNT_AE_HORNET3 (3)
|
|
#define AGRUNT_AE_HORNET4 (4)
|
|
#define AGRUNT_AE_HORNET5 (5)
|
|
// some events are set up in the QC file that aren't recognized by the code yet.
|
|
#define AGRUNT_AE_PUNCH (6)
|
|
#define AGRUNT_AE_BITE (7)
|
|
|
|
#define AGRUNT_AE_LEFT_FOOT (10)
|
|
#define AGRUNT_AE_RIGHT_FOOT (11)
|
|
|
|
#define AGRUNT_AE_LEFT_PUNCH (12)
|
|
#define AGRUNT_AE_RIGHT_PUNCH (13)
|
|
|
|
|
|
|
|
#define AGRUNT_MELEE_DIST 100
|
|
|
|
class CAGrunt : public CSquadMonster
|
|
{
|
|
public:
|
|
void Spawn() override;
|
|
void Precache() override;
|
|
void SetYawSpeed() override;
|
|
int Classify() override;
|
|
int ISoundMask() override;
|
|
void HandleAnimEvent(MonsterEvent_t* pEvent) override;
|
|
void SetObjectCollisionBox() override
|
|
{
|
|
pev->absmin = pev->origin + Vector(-32, -32, 0);
|
|
pev->absmax = pev->origin + Vector(32, 32, 85);
|
|
}
|
|
|
|
Schedule_t* GetSchedule() override;
|
|
Schedule_t* GetScheduleOfType(int Type) override;
|
|
bool FCanCheckAttacks() override;
|
|
bool CheckMeleeAttack1(float flDot, float flDist) override;
|
|
bool CheckRangeAttack1(float flDot, float flDist) override;
|
|
void StartTask(Task_t* pTask) override;
|
|
void AlertSound() override;
|
|
void DeathSound() override;
|
|
void PainSound() override;
|
|
void AttackSound();
|
|
void PrescheduleThink() override;
|
|
void TraceAttack(entvars_t* pevAttacker, float flDamage, Vector vecDir, TraceResult* ptr, int bitsDamageType) override;
|
|
int IRelationship(CBaseEntity* pTarget) override;
|
|
void StopTalking();
|
|
bool ShouldSpeak();
|
|
CUSTOM_SCHEDULES;
|
|
|
|
bool Save(CSave& save) override;
|
|
bool Restore(CRestore& restore) override;
|
|
static TYPEDESCRIPTION m_SaveData[];
|
|
|
|
static const char* pAttackHitSounds[];
|
|
static const char* pAttackMissSounds[];
|
|
static const char* pAttackSounds[];
|
|
static const char* pDieSounds[];
|
|
static const char* pPainSounds[];
|
|
static const char* pIdleSounds[];
|
|
static const char* pAlertSounds[];
|
|
|
|
bool m_fCanHornetAttack;
|
|
float m_flNextHornetAttackCheck;
|
|
|
|
float m_flNextPainTime;
|
|
|
|
// three hacky fields for speech stuff. These don't really need to be saved.
|
|
float m_flNextSpeakTime;
|
|
float m_flNextWordTime;
|
|
int m_iLastWord;
|
|
};
|
|
LINK_ENTITY_TO_CLASS(monster_alien_grunt, CAGrunt);
|
|
|
|
TYPEDESCRIPTION CAGrunt::m_SaveData[] =
|
|
{
|
|
DEFINE_FIELD(CAGrunt, m_fCanHornetAttack, FIELD_BOOLEAN),
|
|
DEFINE_FIELD(CAGrunt, m_flNextHornetAttackCheck, FIELD_TIME),
|
|
DEFINE_FIELD(CAGrunt, m_flNextPainTime, FIELD_TIME),
|
|
DEFINE_FIELD(CAGrunt, m_flNextSpeakTime, FIELD_TIME),
|
|
DEFINE_FIELD(CAGrunt, m_flNextWordTime, FIELD_TIME),
|
|
DEFINE_FIELD(CAGrunt, m_iLastWord, FIELD_INTEGER),
|
|
};
|
|
|
|
IMPLEMENT_SAVERESTORE(CAGrunt, CSquadMonster);
|
|
|
|
const char* CAGrunt::pAttackHitSounds[] =
|
|
{
|
|
"zombie/claw_strike1.wav",
|
|
"zombie/claw_strike2.wav",
|
|
"zombie/claw_strike3.wav",
|
|
};
|
|
|
|
const char* CAGrunt::pAttackMissSounds[] =
|
|
{
|
|
"zombie/claw_miss1.wav",
|
|
"zombie/claw_miss2.wav",
|
|
};
|
|
|
|
const char* CAGrunt::pAttackSounds[] =
|
|
{
|
|
"agrunt/ag_attack1.wav",
|
|
"agrunt/ag_attack2.wav",
|
|
"agrunt/ag_attack3.wav",
|
|
};
|
|
|
|
const char* CAGrunt::pDieSounds[] =
|
|
{
|
|
"agrunt/ag_die1.wav",
|
|
"agrunt/ag_die4.wav",
|
|
"agrunt/ag_die5.wav",
|
|
};
|
|
|
|
const char* CAGrunt::pPainSounds[] =
|
|
{
|
|
"agrunt/ag_pain1.wav",
|
|
"agrunt/ag_pain2.wav",
|
|
"agrunt/ag_pain3.wav",
|
|
"agrunt/ag_pain4.wav",
|
|
"agrunt/ag_pain5.wav",
|
|
};
|
|
|
|
const char* CAGrunt::pIdleSounds[] =
|
|
{
|
|
"agrunt/ag_idle1.wav",
|
|
"agrunt/ag_idle2.wav",
|
|
"agrunt/ag_idle3.wav",
|
|
"agrunt/ag_idle4.wav",
|
|
};
|
|
|
|
const char* CAGrunt::pAlertSounds[] =
|
|
{
|
|
"agrunt/ag_alert1.wav",
|
|
"agrunt/ag_alert3.wav",
|
|
"agrunt/ag_alert4.wav",
|
|
"agrunt/ag_alert5.wav",
|
|
};
|
|
|
|
//=========================================================
|
|
// IRelationship - overridden because Human Grunts are
|
|
// Alien Grunt's nemesis.
|
|
//=========================================================
|
|
int CAGrunt::IRelationship(CBaseEntity* pTarget)
|
|
{
|
|
if (FClassnameIs(pTarget->pev, "monster_human_grunt"))
|
|
{
|
|
return R_NM;
|
|
}
|
|
|
|
return CSquadMonster::IRelationship(pTarget);
|
|
}
|
|
|
|
//=========================================================
|
|
// ISoundMask
|
|
//=========================================================
|
|
int CAGrunt::ISoundMask()
|
|
{
|
|
return bits_SOUND_WORLD |
|
|
bits_SOUND_COMBAT |
|
|
bits_SOUND_PLAYER |
|
|
bits_SOUND_DANGER;
|
|
}
|
|
|
|
//=========================================================
|
|
// TraceAttack
|
|
//=========================================================
|
|
void CAGrunt::TraceAttack(entvars_t* pevAttacker, float flDamage, Vector vecDir, TraceResult* ptr, int bitsDamageType)
|
|
{
|
|
if (ptr->iHitgroup == 10 && (bitsDamageType & (DMG_BULLET | DMG_SLASH | DMG_CLUB)) != 0)
|
|
{
|
|
// hit armor
|
|
if (pev->dmgtime != gpGlobals->time || (RANDOM_LONG(0, 10) < 1))
|
|
{
|
|
UTIL_Ricochet(ptr->vecEndPos, RANDOM_FLOAT(1, 2));
|
|
pev->dmgtime = gpGlobals->time;
|
|
}
|
|
|
|
if (RANDOM_LONG(0, 1) == 0)
|
|
{
|
|
Vector vecTracerDir = vecDir;
|
|
|
|
vecTracerDir.x += RANDOM_FLOAT(-0.3, 0.3);
|
|
vecTracerDir.y += RANDOM_FLOAT(-0.3, 0.3);
|
|
vecTracerDir.z += RANDOM_FLOAT(-0.3, 0.3);
|
|
|
|
vecTracerDir = vecTracerDir * -512;
|
|
|
|
MESSAGE_BEGIN(MSG_PVS, SVC_TEMPENTITY, ptr->vecEndPos);
|
|
WRITE_BYTE(TE_TRACER);
|
|
WRITE_COORD(ptr->vecEndPos.x);
|
|
WRITE_COORD(ptr->vecEndPos.y);
|
|
WRITE_COORD(ptr->vecEndPos.z);
|
|
|
|
WRITE_COORD(vecTracerDir.x);
|
|
WRITE_COORD(vecTracerDir.y);
|
|
WRITE_COORD(vecTracerDir.z);
|
|
MESSAGE_END();
|
|
}
|
|
|
|
flDamage -= 20;
|
|
if (flDamage <= 0)
|
|
flDamage = 0.1; // don't hurt the monster much, but allow bits_COND_LIGHT_DAMAGE to be generated
|
|
}
|
|
else
|
|
{
|
|
SpawnBlood(ptr->vecEndPos, BloodColor(), flDamage); // a little surface blood.
|
|
TraceBleed(flDamage, vecDir, ptr, bitsDamageType);
|
|
}
|
|
|
|
AddMultiDamage(pevAttacker, this, flDamage, bitsDamageType);
|
|
}
|
|
|
|
//=========================================================
|
|
// StopTalking - won't speak again for 10-20 seconds.
|
|
//=========================================================
|
|
void CAGrunt::StopTalking()
|
|
{
|
|
m_flNextWordTime = m_flNextSpeakTime = gpGlobals->time + 10 + RANDOM_LONG(0, 10);
|
|
}
|
|
|
|
//=========================================================
|
|
// ShouldSpeak - Should this agrunt be talking?
|
|
//=========================================================
|
|
bool CAGrunt::ShouldSpeak()
|
|
{
|
|
if (m_flNextSpeakTime > gpGlobals->time)
|
|
{
|
|
// my time to talk is still in the future.
|
|
return false;
|
|
}
|
|
|
|
if ((pev->spawnflags & SF_MONSTER_GAG) != 0)
|
|
{
|
|
if (m_MonsterState != MONSTERSTATE_COMBAT)
|
|
{
|
|
// if gagged, don't talk outside of combat.
|
|
// if not going to talk because of this, put the talk time
|
|
// into the future a bit, so we don't talk immediately after
|
|
// going into combat
|
|
m_flNextSpeakTime = gpGlobals->time + 3;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//=========================================================
|
|
// PrescheduleThink
|
|
//=========================================================
|
|
void CAGrunt::PrescheduleThink()
|
|
{
|
|
if (ShouldSpeak())
|
|
{
|
|
if (m_flNextWordTime < gpGlobals->time)
|
|
{
|
|
int num = -1;
|
|
|
|
do
|
|
{
|
|
num = RANDOM_LONG(0, ARRAYSIZE(pIdleSounds) - 1);
|
|
} while (num == m_iLastWord);
|
|
|
|
m_iLastWord = num;
|
|
|
|
// play a new sound
|
|
EMIT_SOUND(ENT(pev), CHAN_VOICE, pIdleSounds[num], 1.0, ATTN_NORM);
|
|
|
|
// is this word our last?
|
|
if (RANDOM_LONG(1, 10) <= 1)
|
|
{
|
|
// stop talking.
|
|
StopTalking();
|
|
}
|
|
else
|
|
{
|
|
m_flNextWordTime = gpGlobals->time + RANDOM_FLOAT(0.5, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//=========================================================
|
|
// DieSound
|
|
//=========================================================
|
|
void CAGrunt::DeathSound()
|
|
{
|
|
StopTalking();
|
|
|
|
EMIT_SOUND(ENT(pev), CHAN_VOICE, RANDOM_SOUND_ARRAY(pDieSounds), 1.0, ATTN_NORM);
|
|
}
|
|
|
|
//=========================================================
|
|
// AlertSound
|
|
//=========================================================
|
|
void CAGrunt::AlertSound()
|
|
{
|
|
StopTalking();
|
|
|
|
EMIT_SOUND(ENT(pev), CHAN_VOICE, RANDOM_SOUND_ARRAY(pAlertSounds), 1.0, ATTN_NORM);
|
|
}
|
|
|
|
//=========================================================
|
|
// AttackSound
|
|
//=========================================================
|
|
void CAGrunt::AttackSound()
|
|
{
|
|
StopTalking();
|
|
|
|
EMIT_SOUND(ENT(pev), CHAN_VOICE, RANDOM_SOUND_ARRAY(pAttackSounds), 1.0, ATTN_NORM);
|
|
}
|
|
|
|
//=========================================================
|
|
// PainSound
|
|
//=========================================================
|
|
void CAGrunt::PainSound()
|
|
{
|
|
if (m_flNextPainTime > gpGlobals->time)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_flNextPainTime = gpGlobals->time + 0.6;
|
|
|
|
StopTalking();
|
|
|
|
EMIT_SOUND(ENT(pev), CHAN_VOICE, RANDOM_SOUND_ARRAY(pPainSounds), 1.0, ATTN_NORM);
|
|
}
|
|
|
|
//=========================================================
|
|
// Classify - indicates this monster's place in the
|
|
// relationship table.
|
|
//=========================================================
|
|
int CAGrunt::Classify()
|
|
{
|
|
return CLASS_ALIEN_MILITARY;
|
|
}
|
|
|
|
//=========================================================
|
|
// SetYawSpeed - allows each sequence to have a different
|
|
// turn rate associated with it.
|
|
//=========================================================
|
|
void CAGrunt::SetYawSpeed()
|
|
{
|
|
int ys;
|
|
|
|
switch (m_Activity)
|
|
{
|
|
case ACT_TURN_LEFT:
|
|
case ACT_TURN_RIGHT:
|
|
ys = 110;
|
|
break;
|
|
default:
|
|
ys = 100;
|
|
}
|
|
|
|
pev->yaw_speed = ys;
|
|
}
|
|
|
|
//=========================================================
|
|
// HandleAnimEvent - catches the monster-specific messages
|
|
// that occur when tagged animation frames are played.
|
|
//
|
|
// Returns number of events handled, 0 if none.
|
|
//=========================================================
|
|
void CAGrunt::HandleAnimEvent(MonsterEvent_t* pEvent)
|
|
{
|
|
switch (pEvent->event)
|
|
{
|
|
case AGRUNT_AE_HORNET1:
|
|
case AGRUNT_AE_HORNET2:
|
|
case AGRUNT_AE_HORNET3:
|
|
case AGRUNT_AE_HORNET4:
|
|
case AGRUNT_AE_HORNET5:
|
|
{
|
|
// m_vecEnemyLKP should be center of enemy body
|
|
Vector vecArmPos, vecArmDir;
|
|
Vector vecDirToEnemy;
|
|
Vector angDir;
|
|
|
|
if (HasConditions(bits_COND_SEE_ENEMY))
|
|
{
|
|
vecDirToEnemy = ((m_vecEnemyLKP)-pev->origin);
|
|
angDir = UTIL_VecToAngles(vecDirToEnemy);
|
|
vecDirToEnemy = vecDirToEnemy.Normalize();
|
|
}
|
|
else
|
|
{
|
|
angDir = pev->angles;
|
|
UTIL_MakeAimVectors(angDir);
|
|
vecDirToEnemy = gpGlobals->v_forward;
|
|
}
|
|
|
|
pev->effects = EF_MUZZLEFLASH;
|
|
|
|
// make angles +-180
|
|
if (angDir.x > 180)
|
|
{
|
|
angDir.x = angDir.x - 360;
|
|
}
|
|
|
|
SetBlending(0, angDir.x);
|
|
GetAttachment(0, vecArmPos, vecArmDir);
|
|
|
|
vecArmPos = vecArmPos + vecDirToEnemy * 32;
|
|
MESSAGE_BEGIN(MSG_PVS, SVC_TEMPENTITY, vecArmPos);
|
|
WRITE_BYTE(TE_SPRITE);
|
|
WRITE_COORD(vecArmPos.x); // pos
|
|
WRITE_COORD(vecArmPos.y);
|
|
WRITE_COORD(vecArmPos.z);
|
|
WRITE_SHORT(iAgruntMuzzleFlash); // model
|
|
WRITE_BYTE(6); // size * 10
|
|
WRITE_BYTE(128); // brightness
|
|
MESSAGE_END();
|
|
|
|
CBaseEntity* pHornet = CBaseEntity::Create("hornet", vecArmPos, UTIL_VecToAngles(vecDirToEnemy), edict());
|
|
UTIL_MakeVectors(pHornet->pev->angles);
|
|
pHornet->pev->velocity = gpGlobals->v_forward * 300;
|
|
|
|
|
|
|
|
switch (RANDOM_LONG(0, 2))
|
|
{
|
|
case 0:
|
|
EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, "agrunt/ag_fire1.wav", 1.0, ATTN_NORM, 0, 100);
|
|
break;
|
|
case 1:
|
|
EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, "agrunt/ag_fire2.wav", 1.0, ATTN_NORM, 0, 100);
|
|
break;
|
|
case 2:
|
|
EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, "agrunt/ag_fire3.wav", 1.0, ATTN_NORM, 0, 100);
|
|
break;
|
|
}
|
|
|
|
CBaseMonster* pHornetMonster = pHornet->MyMonsterPointer();
|
|
|
|
if (pHornetMonster)
|
|
{
|
|
pHornetMonster->m_hEnemy = m_hEnemy;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case AGRUNT_AE_LEFT_FOOT:
|
|
switch (RANDOM_LONG(0, 1))
|
|
{
|
|
// left foot
|
|
case 0:
|
|
EMIT_SOUND_DYN(ENT(pev), CHAN_BODY, "player/pl_ladder2.wav", 1, ATTN_NORM, 0, 70);
|
|
break;
|
|
case 1:
|
|
EMIT_SOUND_DYN(ENT(pev), CHAN_BODY, "player/pl_ladder4.wav", 1, ATTN_NORM, 0, 70);
|
|
break;
|
|
}
|
|
break;
|
|
case AGRUNT_AE_RIGHT_FOOT:
|
|
// right foot
|
|
switch (RANDOM_LONG(0, 1))
|
|
{
|
|
case 0:
|
|
EMIT_SOUND_DYN(ENT(pev), CHAN_BODY, "player/pl_ladder1.wav", 1, ATTN_NORM, 0, 70);
|
|
break;
|
|
case 1:
|
|
EMIT_SOUND_DYN(ENT(pev), CHAN_BODY, "player/pl_ladder3.wav", 1, ATTN_NORM, 0, 70);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case AGRUNT_AE_LEFT_PUNCH:
|
|
{
|
|
CBaseEntity* pHurt = CheckTraceHullAttack(AGRUNT_MELEE_DIST, gSkillData.agruntDmgPunch, DMG_CLUB);
|
|
|
|
if (pHurt)
|
|
{
|
|
pHurt->pev->punchangle.y = -25;
|
|
pHurt->pev->punchangle.x = 8;
|
|
|
|
// OK to use gpGlobals without calling MakeVectors, cause CheckTraceHullAttack called it above.
|
|
if (pHurt->IsPlayer())
|
|
{
|
|
// this is a player. Knock him around.
|
|
pHurt->pev->velocity = pHurt->pev->velocity + gpGlobals->v_right * 250;
|
|
}
|
|
|
|
EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, RANDOM_SOUND_ARRAY(pAttackHitSounds), 1.0, ATTN_NORM, 0, 100 + RANDOM_LONG(-5, 5));
|
|
|
|
Vector vecArmPos, vecArmAng;
|
|
GetAttachment(0, vecArmPos, vecArmAng);
|
|
SpawnBlood(vecArmPos, pHurt->BloodColor(), 25); // a little surface blood.
|
|
}
|
|
else
|
|
{
|
|
// Play a random attack miss sound
|
|
EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, RANDOM_SOUND_ARRAY(pAttackMissSounds), 1.0, ATTN_NORM, 0, 100 + RANDOM_LONG(-5, 5));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case AGRUNT_AE_RIGHT_PUNCH:
|
|
{
|
|
CBaseEntity* pHurt = CheckTraceHullAttack(AGRUNT_MELEE_DIST, gSkillData.agruntDmgPunch, DMG_CLUB);
|
|
|
|
if (pHurt)
|
|
{
|
|
pHurt->pev->punchangle.y = 25;
|
|
pHurt->pev->punchangle.x = 8;
|
|
|
|
// OK to use gpGlobals without calling MakeVectors, cause CheckTraceHullAttack called it above.
|
|
if (pHurt->IsPlayer())
|
|
{
|
|
// this is a player. Knock him around.
|
|
pHurt->pev->velocity = pHurt->pev->velocity + gpGlobals->v_right * -250;
|
|
}
|
|
|
|
EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, RANDOM_SOUND_ARRAY(pAttackHitSounds), 1.0, ATTN_NORM, 0, 100 + RANDOM_LONG(-5, 5));
|
|
|
|
Vector vecArmPos, vecArmAng;
|
|
GetAttachment(0, vecArmPos, vecArmAng);
|
|
SpawnBlood(vecArmPos, pHurt->BloodColor(), 25); // a little surface blood.
|
|
}
|
|
else
|
|
{
|
|
// Play a random attack miss sound
|
|
EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, RANDOM_SOUND_ARRAY(pAttackMissSounds), 1.0, ATTN_NORM, 0, 100 + RANDOM_LONG(-5, 5));
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
CSquadMonster::HandleAnimEvent(pEvent);
|
|
break;
|
|
}
|
|
}
|
|
|
|
//=========================================================
|
|
// Spawn
|
|
//=========================================================
|
|
void CAGrunt::Spawn()
|
|
{
|
|
Precache();
|
|
|
|
SET_MODEL(ENT(pev), "models/agrunt.mdl");
|
|
UTIL_SetSize(pev, Vector(-32, -32, 0), Vector(32, 32, 64));
|
|
|
|
pev->solid = SOLID_SLIDEBOX;
|
|
pev->movetype = MOVETYPE_STEP;
|
|
m_bloodColor = BLOOD_COLOR_GREEN;
|
|
pev->effects = 0;
|
|
pev->health = gSkillData.agruntHealth;
|
|
m_flFieldOfView = 0.2; // indicates the width of this monster's forward view cone ( as a dotproduct result )
|
|
m_MonsterState = MONSTERSTATE_NONE;
|
|
m_afCapability = 0;
|
|
m_afCapability |= bits_CAP_SQUAD;
|
|
|
|
m_HackedGunPos = Vector(24, 64, 48);
|
|
|
|
m_flNextSpeakTime = m_flNextWordTime = gpGlobals->time + 10 + RANDOM_LONG(0, 10);
|
|
|
|
|
|
MonsterInit();
|
|
}
|
|
|
|
//=========================================================
|
|
// Precache - precaches all resources this monster needs
|
|
//=========================================================
|
|
void CAGrunt::Precache()
|
|
{
|
|
PRECACHE_MODEL("models/agrunt.mdl");
|
|
|
|
PRECACHE_SOUND_ARRAY(pAttackHitSounds);
|
|
PRECACHE_SOUND_ARRAY(pAttackMissSounds);
|
|
PRECACHE_SOUND_ARRAY(pIdleSounds);
|
|
PRECACHE_SOUND_ARRAY(pDieSounds);
|
|
PRECACHE_SOUND_ARRAY(pPainSounds);
|
|
PRECACHE_SOUND_ARRAY(pAttackSounds);
|
|
PRECACHE_SOUND_ARRAY(pAlertSounds);
|
|
|
|
PRECACHE_SOUND("hassault/hw_shoot1.wav");
|
|
|
|
iAgruntMuzzleFlash = PRECACHE_MODEL("sprites/muz4.spr");
|
|
|
|
UTIL_PrecacheOther("hornet");
|
|
}
|
|
|
|
//=========================================================
|
|
// AI Schedules Specific to this monster
|
|
//=========================================================
|
|
|
|
//=========================================================
|
|
// Fail Schedule
|
|
//=========================================================
|
|
Task_t tlAGruntFail[] =
|
|
{
|
|
{TASK_STOP_MOVING, 0},
|
|
{TASK_SET_ACTIVITY, (float)ACT_IDLE},
|
|
{TASK_WAIT, (float)2},
|
|
{TASK_WAIT_PVS, (float)0},
|
|
};
|
|
|
|
Schedule_t slAGruntFail[] =
|
|
{
|
|
{tlAGruntFail,
|
|
ARRAYSIZE(tlAGruntFail),
|
|
bits_COND_CAN_RANGE_ATTACK1 |
|
|
bits_COND_CAN_MELEE_ATTACK1,
|
|
0,
|
|
"AGrunt Fail"},
|
|
};
|
|
|
|
//=========================================================
|
|
// Combat Fail Schedule
|
|
//=========================================================
|
|
Task_t tlAGruntCombatFail[] =
|
|
{
|
|
{TASK_STOP_MOVING, 0},
|
|
{TASK_SET_ACTIVITY, (float)ACT_IDLE},
|
|
{TASK_WAIT_FACE_ENEMY, (float)2},
|
|
{TASK_WAIT_PVS, (float)0},
|
|
};
|
|
|
|
Schedule_t slAGruntCombatFail[] =
|
|
{
|
|
{tlAGruntCombatFail,
|
|
ARRAYSIZE(tlAGruntCombatFail),
|
|
bits_COND_CAN_RANGE_ATTACK1 |
|
|
bits_COND_CAN_MELEE_ATTACK1,
|
|
0,
|
|
"AGrunt Combat Fail"},
|
|
};
|
|
|
|
//=========================================================
|
|
// Standoff schedule. Used in combat when a monster is
|
|
// hiding in cover or the enemy has moved out of sight.
|
|
// Should we look around in this schedule?
|
|
//=========================================================
|
|
Task_t tlAGruntStandoff[] =
|
|
{
|
|
{TASK_STOP_MOVING, (float)0},
|
|
{TASK_SET_ACTIVITY, (float)ACT_IDLE},
|
|
{TASK_WAIT_FACE_ENEMY, (float)2},
|
|
};
|
|
|
|
Schedule_t slAGruntStandoff[] =
|
|
{
|
|
{tlAGruntStandoff,
|
|
ARRAYSIZE(tlAGruntStandoff),
|
|
bits_COND_CAN_RANGE_ATTACK1 |
|
|
bits_COND_CAN_MELEE_ATTACK1 |
|
|
bits_COND_SEE_ENEMY |
|
|
bits_COND_NEW_ENEMY |
|
|
bits_COND_HEAR_SOUND,
|
|
|
|
bits_SOUND_DANGER,
|
|
"Agrunt Standoff"}};
|
|
|
|
//=========================================================
|
|
// Suppress
|
|
//=========================================================
|
|
Task_t tlAGruntSuppressHornet[] =
|
|
{
|
|
{TASK_STOP_MOVING, (float)0},
|
|
{TASK_RANGE_ATTACK1, (float)0},
|
|
};
|
|
|
|
Schedule_t slAGruntSuppress[] =
|
|
{
|
|
{
|
|
tlAGruntSuppressHornet,
|
|
ARRAYSIZE(tlAGruntSuppressHornet),
|
|
0,
|
|
0,
|
|
"AGrunt Suppress Hornet",
|
|
},
|
|
};
|
|
|
|
//=========================================================
|
|
// primary range attacks
|
|
//=========================================================
|
|
Task_t tlAGruntRangeAttack1[] =
|
|
{
|
|
{TASK_STOP_MOVING, (float)0},
|
|
{TASK_FACE_ENEMY, (float)0},
|
|
{TASK_RANGE_ATTACK1, (float)0},
|
|
};
|
|
|
|
Schedule_t slAGruntRangeAttack1[] =
|
|
{
|
|
{tlAGruntRangeAttack1,
|
|
ARRAYSIZE(tlAGruntRangeAttack1),
|
|
bits_COND_NEW_ENEMY |
|
|
bits_COND_ENEMY_DEAD |
|
|
bits_COND_HEAVY_DAMAGE,
|
|
|
|
0,
|
|
"AGrunt Range Attack1"},
|
|
};
|
|
|
|
|
|
Task_t tlAGruntHiddenRangeAttack1[] =
|
|
{
|
|
{TASK_SET_FAIL_SCHEDULE, (float)SCHED_STANDOFF},
|
|
{TASK_AGRUNT_SETUP_HIDE_ATTACK, 0},
|
|
{TASK_STOP_MOVING, 0},
|
|
{TASK_FACE_IDEAL, 0},
|
|
{TASK_RANGE_ATTACK1_NOTURN, (float)0},
|
|
};
|
|
|
|
Schedule_t slAGruntHiddenRangeAttack[] =
|
|
{
|
|
{tlAGruntHiddenRangeAttack1,
|
|
ARRAYSIZE(tlAGruntHiddenRangeAttack1),
|
|
bits_COND_NEW_ENEMY |
|
|
bits_COND_HEAVY_DAMAGE |
|
|
bits_COND_HEAR_SOUND,
|
|
|
|
bits_SOUND_DANGER,
|
|
"AGrunt Hidden Range Attack1"},
|
|
};
|
|
|
|
//=========================================================
|
|
// Take cover from enemy! Tries lateral cover before node
|
|
// cover!
|
|
//=========================================================
|
|
Task_t tlAGruntTakeCoverFromEnemy[] =
|
|
{
|
|
{TASK_STOP_MOVING, (float)0},
|
|
{TASK_WAIT, (float)0.2},
|
|
{TASK_FIND_COVER_FROM_ENEMY, (float)0},
|
|
{TASK_RUN_PATH, (float)0},
|
|
{TASK_WAIT_FOR_MOVEMENT, (float)0},
|
|
{TASK_REMEMBER, (float)bits_MEMORY_INCOVER},
|
|
{TASK_FACE_ENEMY, (float)0},
|
|
};
|
|
|
|
Schedule_t slAGruntTakeCoverFromEnemy[] =
|
|
{
|
|
{tlAGruntTakeCoverFromEnemy,
|
|
ARRAYSIZE(tlAGruntTakeCoverFromEnemy),
|
|
bits_COND_NEW_ENEMY,
|
|
0,
|
|
"AGruntTakeCoverFromEnemy"},
|
|
};
|
|
|
|
//=========================================================
|
|
// Victory dance!
|
|
//=========================================================
|
|
Task_t tlAGruntVictoryDance[] =
|
|
{
|
|
{TASK_STOP_MOVING, (float)0},
|
|
{TASK_SET_FAIL_SCHEDULE, (float)SCHED_AGRUNT_THREAT_DISPLAY},
|
|
{TASK_WAIT, (float)0.2},
|
|
{TASK_AGRUNT_GET_PATH_TO_ENEMY_CORPSE, (float)0},
|
|
{TASK_WALK_PATH, (float)0},
|
|
{TASK_WAIT_FOR_MOVEMENT, (float)0},
|
|
{TASK_FACE_ENEMY, (float)0},
|
|
{TASK_PLAY_SEQUENCE, (float)ACT_CROUCH},
|
|
{TASK_PLAY_SEQUENCE, (float)ACT_VICTORY_DANCE},
|
|
{TASK_PLAY_SEQUENCE, (float)ACT_VICTORY_DANCE},
|
|
{TASK_PLAY_SEQUENCE, (float)ACT_STAND},
|
|
{TASK_PLAY_SEQUENCE, (float)ACT_THREAT_DISPLAY},
|
|
{TASK_PLAY_SEQUENCE, (float)ACT_CROUCH},
|
|
{TASK_PLAY_SEQUENCE, (float)ACT_VICTORY_DANCE},
|
|
{TASK_PLAY_SEQUENCE, (float)ACT_VICTORY_DANCE},
|
|
{TASK_PLAY_SEQUENCE, (float)ACT_VICTORY_DANCE},
|
|
{TASK_PLAY_SEQUENCE, (float)ACT_VICTORY_DANCE},
|
|
{TASK_PLAY_SEQUENCE, (float)ACT_VICTORY_DANCE},
|
|
{TASK_PLAY_SEQUENCE, (float)ACT_STAND},
|
|
};
|
|
|
|
Schedule_t slAGruntVictoryDance[] =
|
|
{
|
|
{tlAGruntVictoryDance,
|
|
ARRAYSIZE(tlAGruntVictoryDance),
|
|
bits_COND_NEW_ENEMY |
|
|
bits_COND_LIGHT_DAMAGE |
|
|
bits_COND_HEAVY_DAMAGE,
|
|
0,
|
|
"AGruntVictoryDance"},
|
|
};
|
|
|
|
//=========================================================
|
|
//=========================================================
|
|
Task_t tlAGruntThreatDisplay[] =
|
|
{
|
|
{TASK_STOP_MOVING, (float)0},
|
|
{TASK_FACE_ENEMY, (float)0},
|
|
{TASK_PLAY_SEQUENCE, (float)ACT_THREAT_DISPLAY},
|
|
};
|
|
|
|
Schedule_t slAGruntThreatDisplay[] =
|
|
{
|
|
{tlAGruntThreatDisplay,
|
|
ARRAYSIZE(tlAGruntThreatDisplay),
|
|
bits_COND_NEW_ENEMY |
|
|
bits_COND_LIGHT_DAMAGE |
|
|
bits_COND_HEAVY_DAMAGE,
|
|
|
|
bits_SOUND_PLAYER |
|
|
bits_SOUND_COMBAT |
|
|
bits_SOUND_WORLD,
|
|
"AGruntThreatDisplay"},
|
|
};
|
|
|
|
DEFINE_CUSTOM_SCHEDULES(CAGrunt){
|
|
slAGruntFail,
|
|
slAGruntCombatFail,
|
|
slAGruntStandoff,
|
|
slAGruntSuppress,
|
|
slAGruntRangeAttack1,
|
|
slAGruntHiddenRangeAttack,
|
|
slAGruntTakeCoverFromEnemy,
|
|
slAGruntVictoryDance,
|
|
slAGruntThreatDisplay,
|
|
};
|
|
|
|
IMPLEMENT_CUSTOM_SCHEDULES(CAGrunt, CSquadMonster);
|
|
|
|
//=========================================================
|
|
// FCanCheckAttacks - this is overridden for alien grunts
|
|
// because they can use their smart weapons against unseen
|
|
// enemies. Base class doesn't attack anyone it can't see.
|
|
//=========================================================
|
|
bool CAGrunt::FCanCheckAttacks()
|
|
{
|
|
if (!HasConditions(bits_COND_ENEMY_TOOFAR))
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
//=========================================================
|
|
// CheckMeleeAttack1 - alien grunts zap the crap out of
|
|
// any enemy that gets too close.
|
|
//=========================================================
|
|
bool CAGrunt::CheckMeleeAttack1(float flDot, float flDist)
|
|
{
|
|
if (HasConditions(bits_COND_SEE_ENEMY) && flDist <= AGRUNT_MELEE_DIST && flDot >= 0.6 && m_hEnemy != NULL)
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//=========================================================
|
|
// CheckRangeAttack1
|
|
//
|
|
// !!!LATER - we may want to load balance this. Several
|
|
// tracelines are done, so we may not want to do this every
|
|
// server frame. Definitely not while firing.
|
|
//=========================================================
|
|
bool CAGrunt::CheckRangeAttack1(float flDot, float flDist)
|
|
{
|
|
if (gpGlobals->time < m_flNextHornetAttackCheck)
|
|
{
|
|
return m_fCanHornetAttack;
|
|
}
|
|
|
|
if (HasConditions(bits_COND_SEE_ENEMY) && flDist >= AGRUNT_MELEE_DIST && flDist <= 1024 && flDot >= 0.5 && NoFriendlyFire())
|
|
{
|
|
TraceResult tr;
|
|
Vector vecArmPos, vecArmDir;
|
|
|
|
// verify that a shot fired from the gun will hit the enemy before the world.
|
|
// !!!LATER - we may wish to do something different for projectile weapons as opposed to instant-hit
|
|
UTIL_MakeVectors(pev->angles);
|
|
GetAttachment(0, vecArmPos, vecArmDir);
|
|
// UTIL_TraceLine( vecArmPos, vecArmPos + gpGlobals->v_forward * 256, ignore_monsters, ENT(pev), &tr);
|
|
UTIL_TraceLine(vecArmPos, m_hEnemy->BodyTarget(vecArmPos), dont_ignore_monsters, ENT(pev), &tr);
|
|
|
|
if (tr.flFraction == 1.0 || tr.pHit == m_hEnemy->edict())
|
|
{
|
|
m_flNextHornetAttackCheck = gpGlobals->time + RANDOM_FLOAT(2, 5);
|
|
m_fCanHornetAttack = true;
|
|
return m_fCanHornetAttack;
|
|
}
|
|
}
|
|
|
|
m_flNextHornetAttackCheck = gpGlobals->time + 0.2; // don't check for half second if this check wasn't successful
|
|
m_fCanHornetAttack = false;
|
|
return m_fCanHornetAttack;
|
|
}
|
|
|
|
//=========================================================
|
|
// StartTask
|
|
//=========================================================
|
|
void CAGrunt::StartTask(Task_t* pTask)
|
|
{
|
|
switch (pTask->iTask)
|
|
{
|
|
case TASK_AGRUNT_GET_PATH_TO_ENEMY_CORPSE:
|
|
{
|
|
UTIL_MakeVectors(pev->angles);
|
|
if (BuildRoute(m_vecEnemyLKP - gpGlobals->v_forward * 50, bits_MF_TO_LOCATION, NULL))
|
|
{
|
|
TaskComplete();
|
|
}
|
|
else
|
|
{
|
|
ALERT(at_aiconsole, "AGruntGetPathToEnemyCorpse failed!!\n");
|
|
TaskFail();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case TASK_AGRUNT_SETUP_HIDE_ATTACK:
|
|
// alien grunt shoots hornets back out into the open from a concealed location.
|
|
// try to find a spot to throw that gives the smart weapon a good chance of finding the enemy.
|
|
// ideally, this spot is along a line that is perpendicular to a line drawn from the agrunt to the enemy.
|
|
|
|
CBaseMonster* pEnemyMonsterPtr;
|
|
|
|
pEnemyMonsterPtr = m_hEnemy->MyMonsterPointer();
|
|
|
|
if (pEnemyMonsterPtr)
|
|
{
|
|
Vector vecCenter;
|
|
TraceResult tr;
|
|
bool fSkip;
|
|
|
|
fSkip = false;
|
|
vecCenter = Center();
|
|
|
|
UTIL_VecToAngles(m_vecEnemyLKP - pev->origin);
|
|
|
|
UTIL_TraceLine(Center() + gpGlobals->v_forward * 128, m_vecEnemyLKP, ignore_monsters, ENT(pev), &tr);
|
|
if (tr.flFraction == 1.0)
|
|
{
|
|
MakeIdealYaw(pev->origin + gpGlobals->v_right * 128);
|
|
fSkip = true;
|
|
TaskComplete();
|
|
}
|
|
|
|
if (!fSkip)
|
|
{
|
|
UTIL_TraceLine(Center() - gpGlobals->v_forward * 128, m_vecEnemyLKP, ignore_monsters, ENT(pev), &tr);
|
|
if (tr.flFraction == 1.0)
|
|
{
|
|
MakeIdealYaw(pev->origin - gpGlobals->v_right * 128);
|
|
fSkip = true;
|
|
TaskComplete();
|
|
}
|
|
}
|
|
|
|
if (!fSkip)
|
|
{
|
|
UTIL_TraceLine(Center() + gpGlobals->v_forward * 256, m_vecEnemyLKP, ignore_monsters, ENT(pev), &tr);
|
|
if (tr.flFraction == 1.0)
|
|
{
|
|
MakeIdealYaw(pev->origin + gpGlobals->v_right * 256);
|
|
fSkip = true;
|
|
TaskComplete();
|
|
}
|
|
}
|
|
|
|
if (!fSkip)
|
|
{
|
|
UTIL_TraceLine(Center() - gpGlobals->v_forward * 256, m_vecEnemyLKP, ignore_monsters, ENT(pev), &tr);
|
|
if (tr.flFraction == 1.0)
|
|
{
|
|
MakeIdealYaw(pev->origin - gpGlobals->v_right * 256);
|
|
fSkip = true;
|
|
TaskComplete();
|
|
}
|
|
}
|
|
|
|
if (!fSkip)
|
|
{
|
|
TaskFail();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ALERT(at_aiconsole, "AGRunt - no enemy monster ptr!!!\n");
|
|
TaskFail();
|
|
}
|
|
break;
|
|
|
|
default:
|
|
CSquadMonster::StartTask(pTask);
|
|
break;
|
|
}
|
|
}
|
|
|
|
//=========================================================
|
|
// GetSchedule - Decides which type of schedule best suits
|
|
// the monster's current state and conditions. Then calls
|
|
// monster's member function to get a pointer to a schedule
|
|
// of the proper type.
|
|
//=========================================================
|
|
Schedule_t* CAGrunt::GetSchedule()
|
|
{
|
|
if (HasConditions(bits_COND_HEAR_SOUND))
|
|
{
|
|
CSound* pSound;
|
|
pSound = PBestSound();
|
|
|
|
ASSERT(pSound != NULL);
|
|
if (pSound && (pSound->m_iType & bits_SOUND_DANGER) != 0)
|
|
{
|
|
// dangerous sound nearby!
|
|
return GetScheduleOfType(SCHED_TAKE_COVER_FROM_BEST_SOUND);
|
|
}
|
|
}
|
|
|
|
switch (m_MonsterState)
|
|
{
|
|
case MONSTERSTATE_COMBAT:
|
|
{
|
|
// dead enemy
|
|
if (HasConditions(bits_COND_ENEMY_DEAD))
|
|
{
|
|
// call base class, all code to handle dead enemies is centralized there.
|
|
return CBaseMonster::GetSchedule();
|
|
}
|
|
|
|
if (HasConditions(bits_COND_NEW_ENEMY))
|
|
{
|
|
return GetScheduleOfType(SCHED_WAKE_ANGRY);
|
|
}
|
|
|
|
// zap player!
|
|
if (HasConditions(bits_COND_CAN_MELEE_ATTACK1))
|
|
{
|
|
AttackSound(); // this is a total hack. Should be parto f the schedule
|
|
return GetScheduleOfType(SCHED_MELEE_ATTACK1);
|
|
}
|
|
|
|
if (HasConditions(bits_COND_HEAVY_DAMAGE))
|
|
{
|
|
return GetScheduleOfType(SCHED_SMALL_FLINCH);
|
|
}
|
|
|
|
// can attack
|
|
if (HasConditions(bits_COND_CAN_RANGE_ATTACK1) && OccupySlot(bits_SLOTS_AGRUNT_HORNET))
|
|
{
|
|
return GetScheduleOfType(SCHED_RANGE_ATTACK1);
|
|
}
|
|
|
|
if (OccupySlot(bits_SLOT_AGRUNT_CHASE))
|
|
{
|
|
return GetScheduleOfType(SCHED_CHASE_ENEMY);
|
|
}
|
|
|
|
return GetScheduleOfType(SCHED_STANDOFF);
|
|
}
|
|
}
|
|
|
|
return CSquadMonster::GetSchedule();
|
|
}
|
|
|
|
//=========================================================
|
|
//=========================================================
|
|
Schedule_t* CAGrunt::GetScheduleOfType(int Type)
|
|
{
|
|
switch (Type)
|
|
{
|
|
case SCHED_TAKE_COVER_FROM_ENEMY:
|
|
return &slAGruntTakeCoverFromEnemy[0];
|
|
break;
|
|
|
|
case SCHED_RANGE_ATTACK1:
|
|
if (HasConditions(bits_COND_SEE_ENEMY))
|
|
{
|
|
//normal attack
|
|
return &slAGruntRangeAttack1[0];
|
|
}
|
|
else
|
|
{
|
|
// attack an unseen enemy
|
|
// return &slAGruntHiddenRangeAttack[ 0 ];
|
|
return &slAGruntRangeAttack1[0];
|
|
}
|
|
break;
|
|
|
|
case SCHED_AGRUNT_THREAT_DISPLAY:
|
|
return &slAGruntThreatDisplay[0];
|
|
break;
|
|
|
|
case SCHED_AGRUNT_SUPPRESS:
|
|
return &slAGruntSuppress[0];
|
|
break;
|
|
|
|
case SCHED_STANDOFF:
|
|
return &slAGruntStandoff[0];
|
|
break;
|
|
|
|
case SCHED_VICTORY_DANCE:
|
|
return &slAGruntVictoryDance[0];
|
|
break;
|
|
|
|
case SCHED_FAIL:
|
|
// no fail schedule specified, so pick a good generic one.
|
|
{
|
|
if (m_hEnemy != NULL)
|
|
{
|
|
// I have an enemy
|
|
// !!!LATER - what if this enemy is really far away and i'm chasing him?
|
|
// this schedule will make me stop, face his last known position for 2
|
|
// seconds, and then try to move again
|
|
return &slAGruntCombatFail[0];
|
|
}
|
|
|
|
return &slAGruntFail[0];
|
|
}
|
|
break;
|
|
}
|
|
|
|
return CSquadMonster::GetScheduleOfType(Type);
|
|
}
|