twhl_monsters_tutorial
folder goes in your Half-Life installation directory. Restart Steam if you do not see the test mod in your library.armorman.mdl
file with any Half-Life Model Viewer. Solokiller's Half-Life Asset Manager works best since it will give you more details without having to decompile the model itself. If you are using another Half-Life Model Viewer, you might need to decompile the model and read the QC file to gather the information explained here.ACT_IDLE
activity. Likewise, we also have animations for walking, running, turning left and right and various flinching and deaths tied to their matching activities. Those are the very basic sequences/activities used by all monsters so we do not need to program them again.SCRIPT_EVENT_SOUND
) is used to play footstep sounds. One thing worth mentioning is that we have custom footsteps sounds instead of the default ones from Half-Life (common/npc_stepX.wav
). Likewise, on the death animations, you will see animation event "2001" (MONSTER_EVENT_BODYDROP_LIGHT
) being used to play the body drop sound (those are default Half-Life sounds unlike the footsteps ones). Those are common to all monsters and therefore do not require extra programming.assasinated
and standing_idle
sequences are not tied to activities. They were supposed to be used in specific contexts (scripted sequences) in Half-Rats: Parasomnia but they ended up being unused. We are not going to use them.ACT_RANGE_ATTACK1
. They have two animation events: "5001" for the muzzle flash and "3" for the actual shoot. The muzzle flash animation event there in the default/common code but we need to program the shoot one since it is specific to the Armored Man.ACT_ARM
and ACT_DISARM
activities respectively and have the events "2" and "4" respectively. When you have finished reading this page and programming the Armored Man, an exercise for you would to be program that arm weapon behavior (look at Half-Life's Barney for inspiration) and probably finish Valve’s unfinished work by programming the holster one.SCRIPT_EVENT_SOUND
) for the shotgun sounds. One important animation event to note is "5" because it is the one responsible for completing the actual reload. We will need to program that reload logic.ACT_EXCITED
activity. If you read the important "disclaimer" about modifying activities, you know why its name does not match the action. Since the shield is specific to the Armored Man, we will have to program it.hgrunt.mdl
) and/or player (player.mdl
) models and compare the hitgroup values.monsters.h
):
Constant | Value | Body part |
---|---|---|
HITGROUP_GENERIC |
0 |
Generic. |
HITGROUP_HEAD |
1 |
Head. |
HITGROUP_CHEST |
2 |
Chest. |
HITGROUP_STOMACH |
3 |
Stomach. |
HITGROUP_LEFTARM |
4 |
Left arm. |
HITGROUP_RIGHTARM |
5 |
Right arm. |
HITGROUP_LEFTLEG |
6 |
Left leg. |
HITGROUP_RIGHTLEG |
7 |
Right leg. |
gun
bodygroup to control the visibility of the shotgun, we will have to handle this too when the Armored Man dies and drop it's shotgun.armorman.cpp
in the dlls
folder of your mod's source code. Do not forget to add it to the server project (hldll
for Windows/Visual Studio, linux/Makefile.hldll
for Linux Makefiles) so that it gets compiled when building the binaries.monsters.h
common to all monsters:
#include "extdll.h"
#include "util.h"
#include "cbase.h"
#include "monsters.h"
#include "armorman.h"
Notice we also have armorman.h
which will contain the class declaration of the Armored Man. Right now, this file does not exists and this will cause an error, so fix it by creating it in the same place as our source file. If you are using an IDE like Visual Studio, do not forget to add it in the project so you can access it more easily.#pragma once
class CArmorMan : public CBaseMonster
{
public:
void Spawn() override;
void Precache() override;
int Classify() override;
};
Go back to the source file. Do not forget to link the class to an entity by adding the LINK_ENTITY_TO_CLASS(monster_armorman, CArmorMan);
line below the includes.Spawn()
one, here is the code:
void CArmorMan::Spawn()
{
Precache();
SET_MODEL(ENT(pev), "models/armorman.mdl");
UTIL_SetSize(pev, Vector(-24.0f, -24.0f, 0.0f), Vector(24.0f, 24.0f, 108.0f));
pev->solid = SOLID_SLIDEBOX;
pev->movetype = MOVETYPE_STEP;
m_bloodColor = BLOOD_COLOR_RED;
pev->health = gSkillData.armormanHealth;
pev->view_ofs = Vector(0.0f, -10.0f, 110.0f);
m_flFieldOfView = VIEW_FIELD_WIDE;
m_MonsterState = MONSTERSTATE_NONE;
m_afCapability = bits_CAP_HEAR | bits_CAP_DOORS_GROUP | bits_CAP_TURN_HEAD | bits_CAP_RANGE_ATTACK1;
MonsterInit();
}
Now, the explanations as to why you put all these lines inside of it.UTIL_SetSize(entvars_t* pev, const Vector& vecMin, const Vector& vecMax)
function are offsets from the model's origin. The latter being at the center of its legs.VEC_HUMAN_HULL_MIN
and VEC_HUMAN_HULL_MAX
exists.gSkillData.armormanHealth
.
Program that gSkillData.armormanHealth
skill CVAR and do not forget to copy/paste and/or update the skill.cfg
file with proper values. If you get a compile error that gSkillData
is not defined, that means you forgot to include the skill.h
header file in the Armored Man source file.pev->view_ofs
defines the offset from the origin to the position of the monster's eyes. The Armored Man being taller than "normal humans" are, we need to use a custom offset. For "normal humans", you can use the handy constant VEC_VIEW
.m_flFieldOfView
defines the field of view of the Armored Man's eyes, VIEW_FIELD_WIDE
is a handy constant for 0.5f
meaning "180° radius".MONSTERSTATE_NONE
. We also tell that the Armored Man is capable of hearing sounds in the world, use doors, turn his head since it has a bone controller for that purpose and perform one kind of range attack. We don't need to define the attack capabilities because when we finalize the spawning process by calling the MonsterInit()
method from CBaseMonster
, the common monsters code will handle that for us (by querying the model's activities).Precache()
one. We are only using the model for now; we will add the rest later. If you followed the book's previous chapters (entity and weapons programming), you should be able to program this one easily on your own and that will be your first exercise on this chapter's page.void CArmorMan::Precache()
{
PRECACHE_MODEL("models/armorman.mdl");
}
The third method to override and implement is the int Classify()
one. The Armored Man will be on the same "faction" as Half-Life's human grunts. Here is your second exercise, implement this method by yourself. If you are stuck, read the "Classification" page again and give another go at the exercise.int CArmorMan::Classify()
{
return CLASS_HUMAN_MILITARY;
}
Time to see if the basics of the Armored Man works properly, compile the binaries and install them in the test mod. You can use the test map called test_armorman
provided with the assets assuming you did not changed the name of the entity when linking it with the class.@PointClass base(Monster, Sequence) size(-24 -24 0, 24 24 108) studio("models/armorman.mdl") = monster_armorman : "Armored Man" []
If you see the Armored Man, it means that you have done things correctly for now.SetYawSpeed()
method, do it and set his pev->yaw_speed
to 200.0f
for now.sv_cheats 1
), you can aim at the Armored Man and issue the impulse 103
command to write a small report of the monster in the console. You will get various information on its state, current schedule (if any), task and more. Give it a try or look at CBaseMonster::ReportAIState()
for more details.W_Precache
function in dlls/weapons.cpp
. If you do this, don't forget to remove that precache before shipping your mod to prevent "useless" precache. Another way is to copy and/or modify the "Grunt-o-matic" cheat (impulse 97
) in CBasePlayer::CheatImpulseCommands(int iImpulse)
.bool CheckRangeAttack1(float flDot, float flDist)
and HandleAnimEvent(MonsterEvent_t* pEvent)
methods.true
, this will notify the default/common code that the Armored Man is capable of doing the first kind of range attack (play sequences tied to the ACT_RANGE_ATTACK1
activity). By default, it returns false
for "he can't do it".#pragma once
constexpr int ARMORMAN_AE_SHOOT = 3;
#define ARMORMAN_SHOTGUN_OFFSET Vector(0.0f, 0.0f, 55.0f)
class CArmorMan : public CBaseMonster
{
public:
void Spawn() override;
void Precache() override;
int Classify() override;
void SetYawSpeed() override;
bool CheckRangeAttack1(float flDot, float flDist) override;
void HandleAnimEvent(MonsterEvent_t* pEvent) override;
bool Save(CSave& save) override;
bool Restore(CRestore& restore) override;
static TYPEDESCRIPTION m_SaveData[];
private:
bool m_bLastCheckAttackResult;
float m_flNextCheckAttackTime;
};
Why you did those changes? Here are the answers.ARMORMAN_AE_SHOOT
, AE
is a shortcut for "animation event". Why "3"? Because during our "technical analysis", we noticed that the shoot sequences calls animation events with the ID "3" for the actual shooting.ARMORMAN_SHOTGUN_OFFSET
is another constant which is the offset between the model's origin and the end of the shotgun's barrels. In other words, it is the starting position of shotgun shots.bool Save(CSave& save)
and bool Restore(CRestore& restore)
methods as well as the declaration of the save/restore table (TYPEDESCRIPTION m_SaveData[]
). We will save/restore the two extra variables m_bLastCheckAttackResult
and m_flNextCheckAttackTime
regarding that "range attack check delay" to prevent issues when loading and saving games.TYPEDESCRIPTION CArmorMan::m_SaveData[] =
{
DEFINE_FIELD(CArmorMan, m_bLastCheckAttackResult, FIELD_BOOLEAN),
DEFINE_FIELD(CArmorMan, m_flNextCheckAttackTime, FIELD_TIME)
};
IMPLEMENT_SAVERESTORE(CArmorMan, CBaseMonster);
Time to implement our overridden version of the bool CheckRangeAttack1(float flDot, float flDist)
method, this code is similar to Barney and human grunts from Half-Life 1 with minor modifications:
bool CArmorMan::CheckRangeAttack1(float flDot, float flDist)
{
if (flDist > 1024.0f || flDot < 0.5f)
return false;
if (gpGlobals->time <= m_flNextCheckAttackTime)
return m_bLastCheckAttackResult;
const Vector vecShootOrigin = pev->origin + ARMORMAN_SHOTGUN_OFFSET;
CBaseEntity* pEnemy = m_hEnemy;
const Vector vecShootTarget = pEnemy->BodyTarget(vecShootOrigin) - pEnemy->pev->origin + m_vecEnemyLKP;
TraceResult tr;
UTIL_TraceLine(vecShootOrigin, vecShootTarget, dont_ignore_monsters, ENT(pev), &tr);
if (tr.flFraction == 1.0f || tr.pHit && Instance(tr.pHit) == pEnemy)
m_bLastCheckAttackResult = true;
else
m_bLastCheckAttackResult = false;
m_flNextCheckAttackTime = gpGlobals->time + 1.5f;
return m_bLastCheckAttackResult;
}
Again, the explanations of why you are doing all of this.flDist
) or not facing (flDot
) the enemy, we are denying the attack. Remember that he has a shotgun, not a sniper rifle.true
to "validate" the attack. Otherwise, we return false
.HandleAnimEvent(HandleAnimEvent* pEvent)
to perform the actual shooting.void CArmorMan::HandleAnimEvent(MonsterEvent_t* pEvent)
{
if (pEvent->event != ARMORMAN_AE_SHOOT)
{
CBaseMonster::HandleAnimEvent(pEvent);
return;
}
UTIL_MakeVectors(pev->angles);
const Vector vecShootOrigin = pev->origin + ARMORMAN_SHOTGUN_OFFSET;
const Vector vecShootDir = ShootAtEnemy(vecShootOrigin);
const Vector vecAngDir = UTIL_VecToAngles(vecShootDir);
SetBlending(0, vecAngDir.x);
pev->effects = EF_MUZZLEFLASH;
if (m_hEnemy && (m_hEnemy->pev->origin - pev->origin).Length() <= 256.0f)
{
FireBullets(12, vecShootOrigin, vecShootDir, VECTOR_CONE_15DEGREES, 1024.0f, BULLET_PLAYER_BUCKSHOT);
EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, "weapons/dbarrel1.wav", VOL_NORM, ATTN_NORM, 0, PITCH_NORM);
}
else
{
FireBullets(6, vecShootOrigin, vecShootDir, VECTOR_CONE_10DEGREES, 1024.0f, BULLET_PLAYER_BUCKSHOT);
EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, "weapons/sbarrel1.wav", VOL_NORM, ATTN_NORM, 0, PITCH_NORM);
}
CSoundEnt::InsertSound(bits_SOUND_COMBAT, pev->origin, 384, 0.3f);
}
Your IDE will likely yell about CSoundEnt
, VECTOR_CONE_15DEGREES
and BULLET_PLAYER_BUCKSHOT
being undefined. First can be fixed by including soundent.h
and the two others with weapons.h
.bool CheckRangeAttack1(float flDist, float flDot)
.weapons/dbarrel1.wav
(PRECACHE_SOUND("weapons/dbarrel1.wav");
) and weapons/sbarrel1.wav
(PRECACHE_SOUND("weapons/sbarrel1.wav");
) sounds in the Precache()
method or you will not be able to hear them. Those are Half-Life's shotgun sounds ("d" for double, "s" for single).
The last line is the most interesting one: it places a "combat sound" with the size of 384 units sphere radius at the Armored Man's location for a duration of 0,3 seconds. In other words, all other monsters within that sphere will "hear" the shot and might react to it depending on their AI behavior.CheckAmmo()
, Schedule_t* GetSchedule()
, Schedule_t* GetScheduleOfType(int Type)
and RunTask(Task_t* pTask)
.CUSTOM_SCHEDULES
in the class declaration to declare and define our custom schedule and custom task in a similar fashion as the save/restore feature.#pragma once
constexpr int ARMORMAN_AE_SHOOT = 3;
constexpr int ARMORMAN_AE_RELOAD = 5;
#define ARMORMAN_SHOTGUN_OFFSET Vector(0.0f, 0.0f, 55.0f)
class CArmorMan : public CBaseMonster
{
public:
void Spawn() override;
void Precache() override;
int Classify() override;
void SetYawSpeed() override;
bool CheckRangeAttack1(float flDot, float flDist) override;
void CheckAmmo() override;
void HandleAnimEvent(MonsterEvent_t* pEvent) override;
Schedule_t* GetSchedule() override;
Schedule_t* GetScheduleOfType(int Type) override;
void RunTask(Task_t* pTask) override;
bool Save(CSave& save) override;
bool Restore(CRestore& restore) override;
static TYPEDESCRIPTION m_SaveData[];
CUSTOM_SCHEDULES;
private:
bool m_bLastCheckAttackResult;
float m_flNextCheckAttackTime;
};
We are going to take care of the infinite ammo problem first. In the Spawn()
method, you can initialize m_cAmmoLoaded
to 2 just before calling MonsterInit()
. The Armored Man will have a loaded shotgun instead of an empty one upon spawning.HandleAnimEvent(MonsterEvent_t* pEvent)
to subtract ammo during shots.m_cAmmoLoaded
is equals or higher than two. This prevents the Armored Man from performing a "double barrel shot" if he only has one buckshot loaded or his shotgun is empty.m_cAmmoLoaded
. Otherwise, we just subtract one.if (m_cAmmoLoaded >= 2 && m_hEnemy && (m_hEnemy->pev->origin - pev->origin).Length() <= 256.0f)
{
FireBullets(12, vecShootOrigin, vecShootDir, VECTOR_CONE_15DEGREES, 1024.0f, BULLET_PLAYER_BUCKSHOT);
EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, "weapons/dbarrel1.wav", VOL_NORM, ATTN_NORM, 0, PITCH_NORM);
m_cAmmoLoaded -= 2;
}
else
{
FireBullets(6, vecShootOrigin, vecShootDir, VECTOR_CONE_10DEGREES, 1024.0f, BULLET_PLAYER_BUCKSHOT);
EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, "weapons/sbarrel1.wav", VOL_NORM, ATTN_NORM, 0, PITCH_NORM);
m_cAmmoLoaded--;
}
Now that the infinite ammo problem is fixed, we need to take care of the reloading part.TASK_STOP_MOVING
, TASK_FACE_ENEMY
and TASK_PLAY_SEQUENCE
.Task_t tlArmorManReload[] =
{
{ TASK_STOP_MOVING, 0.0f },
{ TASK_FACE_ENEMY, 0.0f },
{ TASK_PLAY_SEQUENCE, (float)ACT_RELOAD }
};
Do note that you can improve this task by adding more "steps" such as "taking cover" and so on. Once you have finished implementing the reload logic, feel free to experiment a bit and discover by yourself.bits_COND_HEAVY_DAMAGE
) and/or hearing dangerous sounds (bits_COND_HEAD_SOUND
and bits_SOUND_DANGER
) can interrupt this schedule.Schedule_t slArmorManReload[] =
{
{
tlArmorManReload,
ARRAYSIZE(tlArmorManReload),
bits_COND_HEAVY_DAMAGE | bits_COND_HEAR_SOUND,
bits_SOUND_DANGER,
"ArmorMan Reload"
}
};
Once the schedule is setup, you can "register" it in a similar way as save/restore tables like this:
DEFINE_CUSTOM_SCHEDULES(CArmorMan)
{
slArmorManReload
};
IMPLEMENT_CUSTOM_SCHEDULES(CArmorMan, CBaseMonster);
Now that the task and schedules are setup properly, we need to tell the Armored Man to use them when he has to reload.CheckAmmo()
by checking if m_cAmmoLoaded
is less or equals than zero and if that is the case, we turn on the condition flag about "no ammo is loaded right now".void CArmorMan::CheckAmmo()
{
if (m_cAmmoLoaded <= 0)
SetConditions(bits_COND_NO_AMMO_LOADED);
}
Next method to implement the overridden version is Schedule_t* GetSchedule()
. We need to tell the Armored Man that if he is in combat (MONSTERSTATE_COMBAT
) and if he has to reload (bits_COND_NO_AMMO_LOADED
is set), then he has to reload his shotgun. For other cases, the default/common monsters code can handle them.Schedule_t* CArmorMan::GetSchedule()
{
if (m_MonsterState == MONSTERSTATE_COMBAT && HasConditions(bits_COND_NO_AMMO_LOADED))
return GetScheduleOfType(SCHED_RELOAD);
return CBaseMonster::GetSchedule();
}
However, it is not going to be enough. When the Armored Man's current schedule is to perform the reload (SCHED_RELOAD
), he need to use our custom schedule (slArmorManReload
) and this is where the overridden implementation of Schedule_t* CArmorMan::GetScheduleOfType( int Type )
intervenes.Schedule_t* CArmorMan::GetScheduleOfType(int Type)
{
if (Type == SCHED_RELOAD)
return slArmorManReload;
return CBaseMonster::GetScheduleOfType(Type);
}
Another thing that we need to be sure is that when the Armored Man is performing the reload (TASK_RELOAD
), he needs to actually play the reload activity (ACT_RELOAD
) in order to have the reload itself completed and prevent loops. That is why RunTask(Task_t* pTask)
has to be overridden and implemented.void CArmorMan::RunTask(Task_t* pTask)
{
if (pTask->iTask == TASK_RELOAD)
{
m_IdealActivity = ACT_RELOAD;
return;
}
CBaseMonster::RunTask(pTask);
}
There is one more thing to do: write the code of the animation event responsible for completing the reload. We need to clear the "no ammo is loaded" condition flag (bits_COND_NO_AMMO_LOADED
) and update the value of m_cAmmoLoaded
to two.HandleAnimEvent(MonsterEvent_t* pEvent)
method to use switch
and case
instead of the if
/`return` structure.void CArmorMan::HandleAnimEvent(MonsterEvent_t* pEvent)
{
switch (pEvent->event)
{
case ARMORMAN_AE_SHOOT:
{
// Shooting code goes here
}
break;
case ARMORMAN_AE_RELOAD:
m_cAmmoLoaded = 2;
ClearConditions(bits_COND_NO_AMMO_LOADED);
break;
default:
CBaseMonster::HandleAnimEvent(pEvent);
}
}
Before compiling the binaries, do not forget to precache the shotgun reload sounds included in the weapons
folders otherwise you will not hear the reloading sounds. After that, you can compile the binaries and test them. The Armored Man should now reload after performing a "double barrel shot" or after two "single barrel shots" before being able to shoot again.
TraceAttack(entvars_t* pevAttacker, float flDamage, Vector vecDir, TraceResult* ptr, int bitsDamageType)
, it takes several arguments which most of them are self-explanatory.TraceResult
structure has an iHitgroup
variable which we are going to use to detect if the damage is inflicted to a non-protected area (0) or a protected one (1).bitsDamageType
which you can check for these types of damage using their DMG_
constants.ptr->iHitgroup
to HITGROUP_LEFTARM
.flDamage
.void CArmorMan::TraceAttack(entvars_t* pevAttacker, float flDamage, Vector vecDir, TraceResult* ptr, int bitsDamageType)
{
if (ptr->iHitgroup == 1)
{
if (bitsDamageType & (DMG_BULLET | DMG_SLASH | DMG_BLAST | DMG_CLUB))
{
UTIL_Ricochet(ptr->vecEndPos, 1.0f);
flDamage = 0.01f;
}
ptr->iHitgroup = HITGROUP_LEFTARM;
}
CBaseMonster::TraceAttack(pevAttacker, flDamage, vecDir, ptr, bitsDamageType);
}
The UTIL_Ricochet(const Vector& position, float scale)
add a little spark effect and the ptr->iHitgroup = HITGROUP_LEFTARM
is there to attempt to force a left flinch animation to be played.0.01f
for flDamage
instead of 0. This is a "hack" to have the monster recognize he took damage (setting the condition flag bits_COND_LIGHT_DAMAGE
bit to true).Task_t tlArmorManShield[] =
{
{ TASK_STOP_MOVING, 0.0f },
{ TASK_PLAY_SEQUENCE_FACE_ENEMY, (float)ACT_EXCITED },
};
Schedule_t slArmorManShield[] =
{
{
tlArmorManShield,
ARRAYSIZE(tlArmorManShield),
0,
0,
"ArmorMan Shield"
}
};
Do not forget to register the custom schedule alongside the existing reload one.m_flNextShieldTime
so that the Armored Man does not use his shield every 5 seconds, add it and do not forget to add it to the save/restore table for saved games.Schedule_t* GetSchedule()
method which you might need to refactor a bit to make the code cleaner.Schedule_t* GetScheduleOfType(int Type)
: if the schedule is "cower" then we use our custom schedule. You might need to refactor this one too a bit.Schedule_t* CArmorMan::GetSchedule()
{
if (m_MonsterState != MONSTERSTATE_COMBAT)
return CBaseMonster::GetSchedule();
if (HasConditions(bits_COND_NO_AMMO_LOADED))
return GetScheduleOfType(SCHED_RELOAD);
if (m_hEnemy && pev->health <= (gSkillData.armormanHealth / 2.0f) && gpGlobals->time > m_flNextShieldTime)
{
m_flNextShieldTime = gpGlobals->time + 30.0f;
return GetScheduleOfType(SCHED_COWER);
}
return CBaseMonster::GetSchedule();
}
Schedule_t* CArmorMan::GetScheduleOfType(int Type)
{
switch (Type)
{
case SCHED_RELOAD:
return slArmorManReload;
case SCHED_COWER:
return slArmorManShield;
}
return CBaseMonster::GetScheduleOfType(Type);
}
You can now compile the binaries, put them in your mod folder, start the test map and try to shoot the shield, smash it with the crowbar and use explosives to see if the Armored Man does not take damage.PRECACHE_SOUND(const char* s)
but that would be tedious. The prefered way is to use sound arrays like existing Half-Life monsters.static const char* pAlertSounds[];
Then you define a set like this somewhere in the source file:
const char* CArmorMan::pAlertSounds[] =
{
"armorman/am_alert1.wav",
"armorman/am_alert2.wav",
"armorman/am_alert3.wav"
};
Then you use the handy PRECACHE_SOUND_ARRAY(a)
macro to precache the set.AlertSound()
method and use the EMIT_SOUND_DYN(edict_t* entity, int channel, const char* sample, float volume, float attenuation, int flags, int pitch)
method to play a random sound.void CArmorMan::AlertSound()
{
EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, pAlertSounds[RANDOM_LONG(0, ARRAYSIZE(pAlertSounds) - 1)], VOL_NORM, ATTN_NORM, 0, PITCH_NORM);
}
That is it, your exercise now is to do the same for the death, idle and pain sets.
Another exercise to practice Half-Life programming in general is to change the normal sound pitch by random ones. You can look at weapons and other monsters for inspirations.
GibMonster()
).Killed(entvars_t* pevAttacker, int iGib)
, check if the model body is less than 1 (0 = shotgun in hand, 1 = no shotgun) and if that's the case: change the body to 1 and drop an ammo_buckshot entity using the CBaseEntity* DropItem(char* pszItemName, const Vector& vecPos, const Vector& vecAng)
method from CBaseMonster
. Oh and do not forget to call the default code.void CArmorMan::Killed(entvars_t* pevAttacker, int iGib)
{
if (pev->body < 1) // Shotgun in hand
{
pev->body = 0; // No shotgun
// This part retrieves the position and angles of the attachment that is the shotgun
// You could just use "pev->origin" and "pev->angles" for "DropItem" but the dropped
// item would look like dropped from the stomach which would be a bit weird
Vector vecGunPos;
Vector vecGunAng;
GetAttachment(0, vecGunPos, vecGunAng);
vecGunAng.x = 0.0f;
vecGunAng.z = 0.0f;
DropItem("ammo_buckshot", vecGunPos, vecGunAng);
}
CBaseMonster::Killed(pevAttacker, iGib);
}
CBaseMonster
to CSquadMonster
, add the bits_CAP_SQUAD
capability flag to m_afCapability
in the Spawn()
method. You will need to include squadmonster.h
as well.PrescheduleThink()
to implement some interesting squad logic borrowed from Half-Life's human grunt:
void CArmorMan::PrescheduleThink()
{
if (!InSquad() || !m_hEnemy)
return;
if (HasConditions(bits_COND_SEE_ENEMY))
{
// update the squad's last enemy sighting time.
MySquadLeader()->m_flLastEnemySightTime = gpGlobals->time;
}
else
{
if (gpGlobals->time - MySquadLeader()->m_flLastEnemySightTime > 5.0f)
{
// been a while since we've seen the enemy
MySquadLeader()->m_fEnemyEluded = true;
}
}
}
Do not forget to update the FGD so that level designers can assign an Armored Man to a squad, look at monster_human_grunt for an exemple.squadmonster.cpp
contains useful information:
#define MAX_SQUAD_MEMBERS 5
macro, we can determine that a squad can only have 5 members maximum (the leader + 4 members).CSquadMonster* CSquadMonster::MySquadLeader()
method. Likewise, a squad member can be retrieved with CSquadMonster* CSquadMember::MySquadMember(int i)
where i
is an index between 0 and MAX_SQUAD_MEMBERS - 1
.CLASS_ALIEN_MONSTER
) and are within a 1024 Hammer units sphere radius, a squad is created with the first monster spawned being the leader.bool CSquadMonster::NoFriendlyFire()
method that is useful for range attacks checks. It basically returns false
if it detect a squad member between the monster and the target (using a tolerance of it's size multiplied by 1.5) or true
if no friendlies are on the way.You must log in to post a comment. You can login or register a new account.
link to solution:
https://twhl.info/thread/view/20840