twhl_talk_monsters_tutorial
folder goes in your Half-Life installation directory. Restart Steam if you do not see the test mod in your library.
Visually, the only differences you will see is the removal of grenades near the pouches and the black suit being replaced by a blue one (to fit with the Black Mesa Security staff like Barney and eventually Otis).<modelname>T.mdl
). Back in the late 90's and early 20's, "external textures" and "sequence group" (<modelname>0x.mdl
) model files made sense due to the low Internet speeds, it was more reliable and faster to download many small files rather than a huge one. Nowadays, all of this doesn't matter anymore and it just complicate things.ACT_RANGE_ATTACK2
activity which will be used for shooting the healing darts. Also, the animation event for shooting uses a different ID to distinguish between 9mm bullets and healing darts.CBaseMonster
as parent class instead of CTalkMonster
. Then, add her healing darts and the logic behind it and finish her code by reading the next page to "upgrade" her into a CTalkMonster
with some behavior similar to Barney.
So you want to try programming the basic Friendly Assassin and her healing darts on your own? That's nice!hassassin.cpp
) code for reference contains some information that will surely be of interest when programming our friendly version.ACT_MELEE_ATTACK1
is the entire jumping/flying mechanic. For our friendly version, we won't need it.ACT_RANGE_ATTACK2
concerns the grenade throw mechanic which you can also skip since it's going to be replaced by healing darts later.RunAI()
method, the entire footsteps code can be skipped since this is handled by the animation events that have been added in the model.bool CBaseEntity::TakeHealth(float flHealth, int bitsDamageType)
). The choice of editing the existing crossbow bolt code or duplicate it is yours to make (read this page for more details). Don't forget that crossbow bolts explodes in multiplayer if shot without using the crossbow's scope, so make sure that behavior does not happen for the healing darts.EHANDLE m_pHealTarget
in the Friendly Assassin's class (don't forget to update the save/restore table!).Schedule_t* GetSchedule()
to execute that search function early as possible regardless of the state (alert, combat, idle...) And if we have our ally (m_pHealTarget != nullptr
), we can return the SCHED_RANGE_ATTACK2
schedule that will play ACT_RANGE_ATTACK2
activity and thus play our duplicated shooting animation.#pragma once
// Animation event IDs
constexpr int FASSASSIN_AE_SHOOT = 1;
constexpr int FASSASSIN_AE_SHOOT_HEALING_DART = 2;
class CFAssassin : public CBaseMonster
{
public:
void Spawn() override;
void Precache() override;
int Classify() override;
int ISoundMask() override;
void SetYawSpeed() override;
bool CheckRangeAttack1(float flDot, float flDist) override;
void HandleAnimEvent(MonsterEvent_t* pEvent) override;
Schedule_t* GetSchedule() override;
Schedule_t* GetScheduleOfType(int Type) override;
void Killed(entvars_t* pevAttacker, int iGib) override;
bool Save(CSave& save) override;
bool Restore(CRestore& restore) override;
static TYPEDESCRIPTION m_SaveData[];
CUSTOM_SCHEDULES;
private:
// Search nearby for a friendly monster to heal and returns said monster if it exists. Otherwise, "nullptr" is returned
CBaseEntity* CheckHealTarget();
float m_flDiviation; // Accuracy code taken from Human Assassin
float m_flLastShot;
EHANDLE m_pHealTarget; // Target to heal
float m_flLastHealTime; // Healing cooldown
int m_iShell; // Brass shell
};
// The healing dart, a modified copy of the crossbow bolt
class CHealingDart : public CBaseEntity
{
public:
static CHealingDart* DartCreate();
void Spawn() override;
void Precache() override;
int Classify() override;
void EXPORT DartTouch(CBaseEntity* pOther);
void EXPORT BubbleThink();
};
And here's the source file:
#include "extdll.h"
#include "util.h"
#include "cbase.h"
#include "monsters.h"
#include "soundent.h"
#include "weapons.h"
#include "fassassin.h"
// Tasks and schedules
Task_t tlFAssassinHunt[] =
{
{TASK_GET_PATH_TO_ENEMY, 0.0f},
{TASK_RUN_PATH, 0.0f},
{TASK_WAIT_FOR_MOVEMENT, 0.0f},
};
Schedule_t slFAssassinHunt[] =
{
{tlFAssassinHunt,
ARRAYSIZE(tlFAssassinHunt),
bits_COND_NEW_ENEMY |
bits_COND_CAN_RANGE_ATTACK1 |
bits_COND_HEAR_SOUND,
bits_SOUND_DANGER,
"FAssassinHunt"},
};
Task_t tlFAssassinTakeCoverFromBestSound[] =
{
{TASK_STOP_MOVING, 0.0f},
{TASK_FIND_COVER_FROM_BEST_SOUND, 0.0f},
{TASK_RUN_PATH, 0.0f},
{TASK_WAIT_FOR_MOVEMENT, 0.0f},
{TASK_REMEMBER, (float)bits_MEMORY_INCOVER},
{TASK_TURN_LEFT, 179.0f},
};
Schedule_t slFAssassinTakeCoverFromBestSound[] =
{
{tlFAssassinTakeCoverFromBestSound,
ARRAYSIZE(tlFAssassinTakeCoverFromBestSound),
bits_COND_NEW_ENEMY,
0,
"FAssassinTakeCoverFromBestSound"},
};
Task_t tlFAssassinTakeCoverFromEnemy[] =
{
{TASK_STOP_MOVING, 0.0f},
{TASK_WAIT, 0.2f},
{TASK_SET_FAIL_SCHEDULE, (float)SCHED_RANGE_ATTACK1},
{TASK_FIND_COVER_FROM_ENEMY, 0.0f},
{TASK_RUN_PATH, 0.0f},
{TASK_WAIT_FOR_MOVEMENT, 0.0f},
{TASK_REMEMBER, (float)bits_MEMORY_INCOVER},
{TASK_FACE_ENEMY, 0.0f},
};
Schedule_t slFAssassinTakeCoverFromEnemy[] =
{
{tlFAssassinTakeCoverFromEnemy,
ARRAYSIZE(tlFAssassinTakeCoverFromEnemy),
bits_COND_NEW_ENEMY |
bits_COND_HEAR_SOUND,
bits_SOUND_DANGER,
"FAssassinTakeCoverFromEnemy"},
};
Task_t tlFAssassinTakeCoverFromEnemy2[] =
{
{TASK_STOP_MOVING, 0.0f},
{TASK_WAIT, 0.2f},
{TASK_FACE_ENEMY, 0.0f},
{TASK_RANGE_ATTACK1, 0.0f},
{TASK_FIND_FAR_NODE_COVER_FROM_ENEMY, 384.0f},
{TASK_RUN_PATH, 0.0f},
{TASK_WAIT_FOR_MOVEMENT, 0.0f},
{TASK_REMEMBER, (float)bits_MEMORY_INCOVER},
{TASK_FACE_ENEMY, 0.0f},
};
Schedule_t slFAssassinTakeCoverFromEnemy2[] =
{
{tlFAssassinTakeCoverFromEnemy2,
ARRAYSIZE(tlFAssassinTakeCoverFromEnemy2),
bits_COND_NEW_ENEMY |
bits_COND_HEAR_SOUND,
bits_SOUND_DANGER,
"FAssassinTakeCoverFromEnemy2"},
};
DEFINE_CUSTOM_SCHEDULES(CFAssassin) {
slFAssassinHunt,
slFAssassinTakeCoverFromBestSound,
slFAssassinTakeCoverFromEnemy,
slFAssassinTakeCoverFromEnemy2,
};
IMPLEMENT_CUSTOM_SCHEDULES(CFAssassin, CBaseMonster);
// Save/restore table
TYPEDESCRIPTION CFAssassin::m_SaveData[] =
{
DEFINE_FIELD(CFAssassin, m_flDiviation, FIELD_FLOAT),
DEFINE_FIELD(CFAssassin, m_flLastShot, FIELD_TIME),
DEFINE_FIELD(CFAssassin, m_pHealTarget, FIELD_EHANDLE),
DEFINE_FIELD(CFAssassin, m_flLastHealTime, FIELD_TIME),
};
IMPLEMENT_SAVERESTORE(CFAssassin, CBaseMonster);
LINK_ENTITY_TO_CLASS(monster_fassassin, CFAssassin);
void CFAssassin::Spawn()
{
Precache();
SET_MODEL(ENT(pev), "models/fassassin.mdl");
UTIL_SetSize(pev, VEC_HUMAN_HULL_MIN, VEC_HUMAN_HULL_MAX);
pev->solid = SOLID_SLIDEBOX;
pev->movetype = MOVETYPE_STEP;
m_bloodColor = BLOOD_COLOR_RED;
pev->health = gSkillData.hassassinHealth;
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 | bits_CAP_RANGE_ATTACK2;
// This is used by various methods like "ShootEnemy" to determine precisely the origin of the shots
m_HackedGunPos = Vector(0.0f, 24.0f, 48.0f);
MonsterInit();
}
void CFAssassin::Precache()
{
PRECACHE_MODEL("models/fassassin.mdl");
PRECACHE_SOUND("weapons/pl_gun1.wav");
PRECACHE_SOUND("weapons/pl_gun2.wav");
m_iShell = PRECACHE_MODEL("models/shell.mdl");
UTIL_PrecacheOther("healing_dart");
}
int CFAssassin::Classify()
{
return CLASS_PLAYER_ALLY;
}
int CFAssassin::ISoundMask()
{
return bits_SOUND_WORLD |
bits_SOUND_COMBAT |
bits_SOUND_CARCASS |
bits_SOUND_MEAT |
bits_SOUND_GARBAGE |
bits_SOUND_DANGER |
bits_SOUND_PLAYER;
}
void CFAssassin::SetYawSpeed()
{
pev->yaw_speed = 360.0f;
}
bool CFAssassin::CheckRangeAttack1(float flDot, float flDist)
{
if (HasConditions(bits_COND_ENEMY_OCCLUDED) || flDist <= 64.0f || flDist > 2048.0f)
return false;
TraceResult tr;
Vector vecSrc = GetGunPosition();
UTIL_TraceLine(vecSrc, m_hEnemy->BodyTarget(vecSrc), dont_ignore_monsters, ENT(pev), &tr);
return tr.flFraction == 1.0f || tr.pHit == m_hEnemy->edict();
}
void CFAssassin::HandleAnimEvent(MonsterEvent_t* pEvent)
{
switch (pEvent->event)
{
case FASSASSIN_AE_SHOOT:
{
Vector vecOrigin = GetGunPosition();
Vector vecDir = ShootAtEnemy(vecOrigin);
// Accuracy code taken from Human Assassin
if (m_flLastShot + 2.0f < gpGlobals->time)
{
m_flDiviation = 0.10f;
}
else
{
m_flDiviation -= 0.01f;
if (m_flDiviation < 0.02f)
m_flDiviation = 0.02f;
}
m_flLastShot = gpGlobals->time;
UTIL_MakeVectors(pev->angles);
Vector vecShellVelocity = gpGlobals->v_right * RANDOM_FLOAT(40.0f, 90.0f) + gpGlobals->v_up * RANDOM_FLOAT(75.0f, 200.0f) + gpGlobals->v_forward * RANDOM_FLOAT(-40.0f, 40.0f);
EjectBrass(pev->origin + gpGlobals->v_up * 32.0f + gpGlobals->v_forward * 12.0f, vecShellVelocity, pev->angles.y, m_iShell, TE_BOUNCE_SHELL);
FireBullets(1, vecOrigin, vecDir, Vector(m_flDiviation, m_flDiviation, m_flDiviation), 2048.0f, BULLET_MONSTER_9MM);
switch (RANDOM_LONG(0, 1))
{
case 0:
EMIT_SOUND(ENT(pev), CHAN_WEAPON, "weapons/pl_gun1.wav", RANDOM_FLOAT(0.6f, 0.8f), ATTN_NORM);
break;
case 1:
EMIT_SOUND(ENT(pev), CHAN_WEAPON, "weapons/pl_gun2.wav", RANDOM_FLOAT(0.6f, 0.8f), ATTN_NORM);
break;
}
pev->effects |= EF_MUZZLEFLASH;
Vector angDir = UTIL_VecToAngles(vecDir);
SetBlending(0, angDir.x);
}
break;
case FASSASSIN_AE_SHOOT_HEALING_DART:
{
Vector vecOrigin = GetGunPosition();
Vector vecDir = gpGlobals->v_forward;
if (m_pHealTarget)
{
MakeIdealYaw(m_pHealTarget->pev->origin);
ChangeYaw(pev->yaw_speed);
vecDir = ((m_pHealTarget->BodyTarget(vecOrigin) - m_pHealTarget->pev->origin) + m_pHealTarget->pev->origin - vecOrigin).Normalize();
}
// Spawn the healing dart (same as the crossbow code)
CHealingDart* pDart = CHealingDart::DartCreate();
pDart->pev->origin = vecOrigin;
pDart->pev->angles = UTIL_VecToAngles(vecDir);
pDart->pev->owner = edict();
pDart->pev->velocity = vecDir * 2048.0f;
pDart->pev->speed = 2048.0f;
pDart->pev->avelocity.z = 10.0f;
// Same sounds as shooting with the 9mm but with a higher pitch to make the difference
switch (RANDOM_LONG(0, 1))
{
case 0:
EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, "weapons/pl_gun1.wav", VOL_NORM, ATTN_NORM, 0, 150);
break;
case 1:
EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, "weapons/pl_gun2.wav", VOL_NORM, ATTN_NORM, 0, 150);
break;
}
pev->effects |= EF_MUZZLEFLASH;
Vector angDir = UTIL_VecToAngles(vecDir);
SetBlending(0, angDir.x);
m_flLastHealTime = gpGlobals->time + 10.0f; // Apply cooldown
m_pHealTarget = nullptr; // Forget target
}
break;
default:
CBaseMonster::HandleAnimEvent(pEvent);
}
}
Schedule_t* CFAssassin::GetSchedule()
{
if (HasConditions(bits_COND_HEAR_SOUND))
{
CSound* pSound = PBestSound();
if (pSound && (pSound->m_iType & bits_SOUND_DANGER) != 0)
return GetScheduleOfType(SCHED_TAKE_COVER_FROM_BEST_SOUND);
}
// Unless we are taking cover from a dangerous sound (i.e. grenade), check ASAP if we can heal a friend regardless of state (idle/alert/combat...)
m_pHealTarget = CheckHealTarget();
if (m_pHealTarget)
return GetScheduleOfType(SCHED_RANGE_ATTACK2);
if (m_MonsterState != MONSTERSTATE_COMBAT || HasConditions(bits_COND_ENEMY_DEAD))
return CBaseMonster::GetSchedule();
if (HasConditions(bits_COND_CAN_RANGE_ATTACK1))
return GetScheduleOfType(SCHED_RANGE_ATTACK1);
if (HasConditions(bits_COND_SEE_ENEMY))
return GetScheduleOfType(SCHED_COMBAT_FACE);
if (HasConditions(bits_COND_NEW_ENEMY))
return GetScheduleOfType(SCHED_TAKE_COVER_FROM_ENEMY);
return GetScheduleOfType(SCHED_ALERT_STAND);
}
Schedule_t* CFAssassin::GetScheduleOfType(int Type)
{
switch (Type)
{
case SCHED_TAKE_COVER_FROM_ENEMY:
return pev->health > 30 ? slFAssassinTakeCoverFromEnemy : slFAssassinTakeCoverFromEnemy2;
case SCHED_TAKE_COVER_FROM_BEST_SOUND:
return slFAssassinTakeCoverFromBestSound;
case SCHED_CHASE_ENEMY:
return slFAssassinHunt;
}
return CBaseMonster::GetScheduleOfType(Type);
}
void CFAssassin::Killed(entvars_t* pevAttacker, int iGib)
{
// Drop the gun
if (pev->body <= 0)
{
pev->body = 1;
Vector vecOrigin;
Vector vecAngles;
GetAttachment(0, vecOrigin, vecAngles);
DropItem("weapon_9mmhandgun", vecOrigin, vecAngles);
}
CBaseMonster::Killed(pevAttacker, iGib);
}
CBaseEntity* CFAssassin::CheckHealTarget()
{
// Cooldown is not over yet
if (m_flLastHealTime > gpGlobals->time)
return nullptr;
// Find all entities nearby
CBaseEntity* pTarget = nullptr;
while ((pTarget = UTIL_FindEntityInSphere(pTarget, pev->origin, 256.0f)) != nullptr)
{
// Check if it's a monster (including player) that is alive, an ally and not moving too fast (otherwise the healing dart goes somewhere else)
if (pTarget->MyMonsterPointer() == nullptr || !pTarget->IsAlive() || IRelationship(pTarget) != R_AL || pTarget->pev->velocity.Length() > 64.0f)
continue;
// For players, heal them if they have half of their health
if (pTarget->IsPlayer() && pTarget->pev->health <= pTarget->pev->max_health / 2.0f)
return pTarget;
// For the rest (friendly monsters), heal them if they have their below below a threshold
if (!pTarget->IsPlayer() && pTarget->pev->health <= gSkillData.scientistHeal)
return pTarget;
}
// Found no one eligible for healing
return nullptr;
}
LINK_ENTITY_TO_CLASS(healing_dart, CHealingDart);
CHealingDart* CHealingDart::DartCreate()
{
CHealingDart* pDart = GetClassPtr((CHealingDart*)nullptr);
pDart->pev->classname = MAKE_STRING("healing_dart");
pDart->Spawn();
return pDart;
}
void CHealingDart::Spawn()
{
Precache();
pev->movetype = MOVETYPE_FLY;
pev->solid = SOLID_BBOX;
pev->gravity = 0.5f;
SET_MODEL(ENT(pev), "models/crossbow_bolt.mdl"); // Could use a different model here if you have one and you wish
UTIL_SetOrigin(pev, pev->origin);
UTIL_SetSize(pev, Vector(0.0f, 0.0f, 0.0f), Vector(0.0f, 0.0f, 0.0f));
SetTouch(&CHealingDart::DartTouch);
SetThink(&CHealingDart::BubbleThink);
pev->nextthink = gpGlobals->time + 0.2f;
}
void CHealingDart::Precache()
{
PRECACHE_MODEL("models/crossbow_bolt.mdl");
PRECACHE_SOUND("weapons/xbow_hitbod1.wav");
PRECACHE_SOUND("weapons/xbow_hitbod2.wav");
PRECACHE_SOUND("weapons/xbow_hit1.wav");
}
int CHealingDart::Classify()
{
return CLASS_NONE;
}
void CHealingDart::DartTouch(CBaseEntity* pOther)
{
SetTouch(nullptr);
SetThink(nullptr);
if (0 != pOther->pev->takedamage)
{
TraceResult tr = UTIL_GetGlobalTrace();
entvars_t* pevOwner = VARS(pev->owner);
// Check if the target is alive first, we don't want to heal a dying or dead monster
if (pOther->IsAlive())
pOther->TakeHealth(gSkillData.scientistHeal, DMG_GENERIC);
pev->velocity = Vector(0.0f, 0.0f, 0.0f);
switch (RANDOM_LONG(0, 1))
{
case 0:
EMIT_SOUND(ENT(pev), CHAN_BODY, "weapons/xbow_hitbod1.wav", VOL_NORM, ATTN_NORM);
break;
case 1:
EMIT_SOUND(ENT(pev), CHAN_BODY, "weapons/xbow_hitbod2.wav", VOL_NORM, ATTN_NORM);
break;
}
Killed(pev, GIB_NEVER);
}
else
{
EMIT_SOUND_DYN(ENT(pev), CHAN_BODY, "weapons/xbow_hit1.wav", RANDOM_FLOAT(0.95f, 1.0f), ATTN_NORM, 0, 98 + RANDOM_LONG(0, 7));
SetThink(&CHealingDart::SUB_Remove);
pev->nextthink = gpGlobals->time;
if (FClassnameIs(pOther->pev, "worldspawn"))
{
Vector vecDir = pev->velocity.Normalize();
UTIL_SetOrigin(pev, pev->origin - vecDir * 12.0f);
pev->angles = UTIL_VecToAngles(vecDir);
pev->solid = SOLID_NOT;
pev->movetype = MOVETYPE_FLY;
pev->velocity = Vector(0.0f, 0.0f, 0.0f);
pev->avelocity.z = 0.0f;
pev->angles.z = RANDOM_LONG(0.0f, 360.0f);
pev->nextthink = gpGlobals->time + 10.0f;
}
if (UTIL_PointContents(pev->origin) != CONTENTS_WATER)
UTIL_Sparks(pev->origin);
}
}
void CHealingDart::BubbleThink()
{
pev->nextthink = gpGlobals->time + 0.1f;
if (pev->waterlevel == 0)
return;
UTIL_BubbleTrail(pev->origin - pev->velocity * 0.1f, pev->origin, 1);
}
You must log in to post a comment. You can login or register a new account.