halflife-photomode/dlls/leech.cpp

716 lines
17 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.
*
****/
//=========================================================
// leech - basic little swimming monster
//=========================================================
//
// UNDONE:
// DONE:Steering force model for attack
// DONE:Attack animation control / damage
// DONE:Establish range of up/down motion and steer around vertical obstacles
// DONE:Re-evaluate height periodically
// DONE:Fall (MOVETYPE_TOSS) and play different anim if out of water
// Test in complex room (c2a3?)
// DONE:Sounds? - Kelly will fix
// Blood cloud? Hurt effect?
// Group behavior?
// DONE:Save/restore
// Flop animation - just bind to ACT_TWITCH
// Fix fatal push into wall case
//
// Try this on a bird
// Try this on a model with hulls/tracehull?
//
#include "float.h"
#include "extdll.h"
#include "util.h"
#include "cbase.h"
#include "monsters.h"
// Animation events
#define LEECH_AE_ATTACK 1
#define LEECH_AE_FLOP 2
// Movement constants
#define LEECH_ACCELERATE 10
#define LEECH_CHECK_DIST 45
#define LEECH_SWIM_SPEED 50
#define LEECH_SWIM_ACCEL 80
#define LEECH_SWIM_DECEL 10
#define LEECH_TURN_RATE 90
#define LEECH_SIZEX 10
#define LEECH_FRAMETIME 0.1
#define DEBUG_BEAMS 0
#if DEBUG_BEAMS
#include "effects.h"
#endif
class CLeech : public CBaseMonster
{
public:
void Spawn() override;
void Precache() override;
void EXPORT SwimThink();
void EXPORT DeadThink();
void Touch(CBaseEntity* pOther) override
{
if (pOther->IsPlayer())
{
// If the client is pushing me, give me some base velocity
if (gpGlobals->trace_ent && gpGlobals->trace_ent == edict())
{
pev->basevelocity = pOther->pev->velocity;
pev->flags |= FL_BASEVELOCITY;
}
}
}
void SetObjectCollisionBox() override
{
pev->absmin = pev->origin + Vector(-8, -8, 0);
pev->absmax = pev->origin + Vector(8, 8, 2);
}
void AttackSound();
void AlertSound() override;
void UpdateMotion();
float ObstacleDistance(CBaseEntity* pTarget);
void MakeVectors();
void RecalculateWaterlevel();
void SwitchLeechState();
// Base entity functions
void HandleAnimEvent(MonsterEvent_t* pEvent) override;
int BloodColor() override { return DONT_BLEED; }
void Killed(entvars_t* pevAttacker, int iGib) override;
void Activate() override;
bool TakeDamage(entvars_t* pevInflictor, entvars_t* pevAttacker, float flDamage, int bitsDamageType) override;
int Classify() override { return CLASS_INSECT; }
int IRelationship(CBaseEntity* pTarget) override;
bool Save(CSave& save) override;
bool Restore(CRestore& restore) override;
static TYPEDESCRIPTION m_SaveData[];
static const char* pAttackSounds[];
static const char* pAlertSounds[];
private:
// UNDONE: Remove unused boid vars, do group behavior
float m_flTurning; // is this boid turning?
bool m_fPathBlocked; // true if there is an obstacle ahead
float m_flAccelerate;
float m_obstacle;
float m_top;
float m_bottom;
float m_height;
float m_waterTime;
float m_sideTime; // Timer to randomly check clearance on sides
float m_zTime;
float m_stateTime;
float m_attackSoundTime;
#if DEBUG_BEAMS
CBeam* m_pb;
CBeam* m_pt;
#endif
};
LINK_ENTITY_TO_CLASS(monster_leech, CLeech);
TYPEDESCRIPTION CLeech::m_SaveData[] =
{
DEFINE_FIELD(CLeech, m_flTurning, FIELD_FLOAT),
DEFINE_FIELD(CLeech, m_fPathBlocked, FIELD_BOOLEAN),
DEFINE_FIELD(CLeech, m_flAccelerate, FIELD_FLOAT),
DEFINE_FIELD(CLeech, m_obstacle, FIELD_FLOAT),
DEFINE_FIELD(CLeech, m_top, FIELD_FLOAT),
DEFINE_FIELD(CLeech, m_bottom, FIELD_FLOAT),
DEFINE_FIELD(CLeech, m_height, FIELD_FLOAT),
DEFINE_FIELD(CLeech, m_waterTime, FIELD_TIME),
DEFINE_FIELD(CLeech, m_sideTime, FIELD_TIME),
DEFINE_FIELD(CLeech, m_zTime, FIELD_TIME),
DEFINE_FIELD(CLeech, m_stateTime, FIELD_TIME),
DEFINE_FIELD(CLeech, m_attackSoundTime, FIELD_TIME),
};
IMPLEMENT_SAVERESTORE(CLeech, CBaseMonster);
const char* CLeech::pAttackSounds[] =
{
"leech/leech_bite1.wav",
"leech/leech_bite2.wav",
"leech/leech_bite3.wav",
};
const char* CLeech::pAlertSounds[] =
{
"leech/leech_alert1.wav",
"leech/leech_alert2.wav",
};
void CLeech::Spawn()
{
Precache();
SET_MODEL(ENT(pev), "models/leech.mdl");
// Just for fun
// SET_MODEL(ENT(pev), "models/icky.mdl");
// UTIL_SetSize( pev, g_vecZero, g_vecZero );
UTIL_SetSize(pev, Vector(-1, -1, 0), Vector(1, 1, 2));
// Don't push the minz down too much or the water check will fail because this entity is really point-sized
pev->solid = SOLID_SLIDEBOX;
pev->movetype = MOVETYPE_FLY;
SetBits(pev->flags, FL_SWIM);
pev->health = gSkillData.leechHealth;
m_flFieldOfView = -0.5; // 180 degree FOV
m_flDistLook = 750;
MonsterInit();
SetThink(&CLeech::SwimThink);
SetUse(NULL);
SetTouch(NULL);
pev->view_ofs = g_vecZero;
m_flTurning = 0;
m_fPathBlocked = false;
SetActivity(ACT_SWIM);
SetState(MONSTERSTATE_IDLE);
m_stateTime = gpGlobals->time + RANDOM_FLOAT(1, 5);
}
void CLeech::Activate()
{
RecalculateWaterlevel();
}
void CLeech::RecalculateWaterlevel()
{
// Calculate boundaries
Vector vecTest = pev->origin - Vector(0, 0, 400);
TraceResult tr;
UTIL_TraceLine(pev->origin, vecTest, missile, edict(), &tr);
if (tr.flFraction != 1.0)
m_bottom = tr.vecEndPos.z + 1;
else
m_bottom = vecTest.z;
m_top = UTIL_WaterLevel(pev->origin, pev->origin.z, pev->origin.z + 400) - 1;
// Chop off 20% of the outside range
float newBottom = m_bottom * 0.8 + m_top * 0.2;
m_top = m_bottom * 0.2 + m_top * 0.8;
m_bottom = newBottom;
m_height = RANDOM_FLOAT(m_bottom, m_top);
m_waterTime = gpGlobals->time + RANDOM_FLOAT(5, 7);
}
void CLeech::SwitchLeechState()
{
m_stateTime = gpGlobals->time + RANDOM_FLOAT(3, 6);
if (m_MonsterState == MONSTERSTATE_COMBAT)
{
m_hEnemy = NULL;
SetState(MONSTERSTATE_IDLE);
// We may be up against the player, so redo the side checks
m_sideTime = 0;
}
else
{
Look(m_flDistLook);
CBaseEntity* pEnemy = BestVisibleEnemy();
if (pEnemy && pEnemy->pev->waterlevel != 0)
{
m_hEnemy = pEnemy;
SetState(MONSTERSTATE_COMBAT);
m_stateTime = gpGlobals->time + RANDOM_FLOAT(18, 25);
AlertSound();
}
}
}
int CLeech::IRelationship(CBaseEntity* pTarget)
{
if (pTarget->IsPlayer())
return R_DL;
return CBaseMonster::IRelationship(pTarget);
}
void CLeech::AttackSound()
{
if (gpGlobals->time > m_attackSoundTime)
{
EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, RANDOM_SOUND_ARRAY(pAttackSounds), 1.0, ATTN_NORM, 0, PITCH_NORM);
m_attackSoundTime = gpGlobals->time + 0.5;
}
}
void CLeech::AlertSound()
{
EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, RANDOM_SOUND_ARRAY(pAlertSounds), 1.0, ATTN_NORM * 0.5, 0, PITCH_NORM);
}
void CLeech::Precache()
{
//PRECACHE_MODEL("models/icky.mdl");
PRECACHE_MODEL("models/leech.mdl");
PRECACHE_SOUND_ARRAY(pAttackSounds);
PRECACHE_SOUND_ARRAY(pAlertSounds);
}
bool CLeech::TakeDamage(entvars_t* pevInflictor, entvars_t* pevAttacker, float flDamage, int bitsDamageType)
{
pev->velocity = g_vecZero;
// Nudge the leech away from the damage
if (pevInflictor)
{
pev->velocity = (pev->origin - pevInflictor->origin).Normalize() * 25;
}
return CBaseMonster::TakeDamage(pevInflictor, pevAttacker, flDamage, bitsDamageType);
}
void CLeech::HandleAnimEvent(MonsterEvent_t* pEvent)
{
switch (pEvent->event)
{
case LEECH_AE_ATTACK:
AttackSound();
CBaseEntity* pEnemy;
pEnemy = m_hEnemy;
if (pEnemy != NULL)
{
Vector dir, face;
UTIL_MakeVectorsPrivate(pev->angles, face, NULL, NULL);
face.z = 0;
dir = (pEnemy->pev->origin - pev->origin);
dir.z = 0;
dir = dir.Normalize();
face = face.Normalize();
if (DotProduct(dir, face) > 0.9) // Only take damage if the leech is facing the prey
pEnemy->TakeDamage(pev, pev, gSkillData.leechDmgBite, DMG_SLASH);
}
m_stateTime -= 2;
break;
case LEECH_AE_FLOP:
// Play flop sound
break;
default:
CBaseMonster::HandleAnimEvent(pEvent);
break;
}
}
void CLeech::MakeVectors()
{
Vector tmp = pev->angles;
tmp.x = -tmp.x;
UTIL_MakeVectors(tmp);
}
//
// ObstacleDistance - returns normalized distance to obstacle
//
float CLeech::ObstacleDistance(CBaseEntity* pTarget)
{
TraceResult tr;
Vector vecTest;
// use VELOCITY, not angles, not all boids point the direction they are flying
//Vector vecDir = UTIL_VecToAngles( pev->velocity );
MakeVectors();
// check for obstacle ahead
vecTest = pev->origin + gpGlobals->v_forward * LEECH_CHECK_DIST;
UTIL_TraceLine(pev->origin, vecTest, missile, edict(), &tr);
if (0 != tr.fStartSolid)
{
pev->speed = -LEECH_SWIM_SPEED * 0.5;
// ALERT( at_console, "Stuck from (%f %f %f) to (%f %f %f)\n", pev->oldorigin.x, pev->oldorigin.y, pev->oldorigin.z, pev->origin.x, pev->origin.y, pev->origin.z );
// UTIL_SetOrigin( pev, pev->oldorigin );
}
if (tr.flFraction != 1.0)
{
if ((pTarget == NULL || tr.pHit != pTarget->edict()))
{
return tr.flFraction;
}
else
{
if (fabs(m_height - pev->origin.z) > 10)
return tr.flFraction;
}
}
if (m_sideTime < gpGlobals->time)
{
// extra wide checks
vecTest = pev->origin + gpGlobals->v_right * LEECH_SIZEX * 2 + gpGlobals->v_forward * LEECH_CHECK_DIST;
UTIL_TraceLine(pev->origin, vecTest, missile, edict(), &tr);
if (tr.flFraction != 1.0)
return tr.flFraction;
vecTest = pev->origin - gpGlobals->v_right * LEECH_SIZEX * 2 + gpGlobals->v_forward * LEECH_CHECK_DIST;
UTIL_TraceLine(pev->origin, vecTest, missile, edict(), &tr);
if (tr.flFraction != 1.0)
return tr.flFraction;
// Didn't hit either side, so stop testing for another 0.5 - 1 seconds
m_sideTime = gpGlobals->time + RANDOM_FLOAT(0.5, 1);
}
return 1.0;
}
void CLeech::DeadThink()
{
if (m_fSequenceFinished)
{
if (m_Activity == ACT_DIEFORWARD)
{
SetThink(NULL);
StopAnimation();
return;
}
else if ((pev->flags & FL_ONGROUND) != 0)
{
pev->solid = SOLID_NOT;
SetActivity(ACT_DIEFORWARD);
}
}
StudioFrameAdvance();
pev->nextthink = gpGlobals->time + 0.1;
// Apply damage velocity, but keep out of the walls
if (pev->velocity.x != 0 || pev->velocity.y != 0)
{
TraceResult tr;
// Look 0.5 seconds ahead
UTIL_TraceLine(pev->origin, pev->origin + pev->velocity * 0.5, missile, edict(), &tr);
if (tr.flFraction != 1.0)
{
pev->velocity.x = 0;
pev->velocity.y = 0;
}
}
}
void CLeech::UpdateMotion()
{
float flapspeed = (pev->speed - m_flAccelerate) / LEECH_ACCELERATE;
m_flAccelerate = m_flAccelerate * 0.8 + pev->speed * 0.2;
if (flapspeed < 0)
flapspeed = -flapspeed;
flapspeed += 1.0;
if (flapspeed < 0.5)
flapspeed = 0.5;
if (flapspeed > 1.9)
flapspeed = 1.9;
pev->framerate = flapspeed;
if (!m_fPathBlocked)
pev->avelocity.y = pev->ideal_yaw;
else
pev->avelocity.y = pev->ideal_yaw * m_obstacle;
if (pev->avelocity.y > 150)
m_IdealActivity = ACT_TURN_LEFT;
else if (pev->avelocity.y < -150)
m_IdealActivity = ACT_TURN_RIGHT;
else
m_IdealActivity = ACT_SWIM;
// lean
float targetPitch, delta;
delta = m_height - pev->origin.z;
if (delta < -10)
targetPitch = -30;
else if (delta > 10)
targetPitch = 30;
else
targetPitch = 0;
pev->angles.x = UTIL_Approach(targetPitch, pev->angles.x, 60 * LEECH_FRAMETIME);
// bank
pev->avelocity.z = -(pev->angles.z + (pev->avelocity.y * 0.25));
if (m_MonsterState == MONSTERSTATE_COMBAT && HasConditions(bits_COND_CAN_MELEE_ATTACK1))
m_IdealActivity = ACT_MELEE_ATTACK1;
// Out of water check
if (0 == pev->waterlevel)
{
pev->movetype = MOVETYPE_TOSS;
m_IdealActivity = ACT_TWITCH;
pev->velocity = g_vecZero;
// Animation will intersect the floor if either of these is non-zero
pev->angles.z = 0;
pev->angles.x = 0;
if (pev->framerate < 1.0)
pev->framerate = 1.0;
}
else if (pev->movetype == MOVETYPE_TOSS)
{
pev->movetype = MOVETYPE_FLY;
pev->flags &= ~FL_ONGROUND;
RecalculateWaterlevel();
m_waterTime = gpGlobals->time + 2; // Recalc again soon, water may be rising
}
if (m_Activity != m_IdealActivity)
{
SetActivity(m_IdealActivity);
}
float flInterval = StudioFrameAdvance();
DispatchAnimEvents(flInterval);
#if DEBUG_BEAMS
if (!m_pb)
m_pb = CBeam::BeamCreate("sprites/laserbeam.spr", 5);
if (!m_pt)
m_pt = CBeam::BeamCreate("sprites/laserbeam.spr", 5);
m_pb->PointsInit(pev->origin, pev->origin + gpGlobals->v_forward * LEECH_CHECK_DIST);
m_pt->PointsInit(pev->origin, pev->origin - gpGlobals->v_right * (pev->avelocity.y * 0.25));
if (m_fPathBlocked)
{
float color = m_obstacle * 30;
if (m_obstacle == 1.0)
color = 0;
if (color > 255)
color = 255;
m_pb->SetColor(255, (int)color, (int)color);
}
else
m_pb->SetColor(255, 255, 0);
m_pt->SetColor(0, 0, 255);
#endif
}
void CLeech::SwimThink()
{
TraceResult tr;
float flLeftSide;
float flRightSide;
float targetSpeed;
float targetYaw = 0;
CBaseEntity* pTarget;
if (FNullEnt(FIND_CLIENT_IN_PVS(edict())))
{
pev->nextthink = gpGlobals->time + RANDOM_FLOAT(1, 1.5);
pev->velocity = g_vecZero;
return;
}
else
pev->nextthink = gpGlobals->time + 0.1;
targetSpeed = LEECH_SWIM_SPEED;
if (m_waterTime < gpGlobals->time)
RecalculateWaterlevel();
if (m_stateTime < gpGlobals->time)
SwitchLeechState();
ClearConditions(bits_COND_CAN_MELEE_ATTACK1);
switch (m_MonsterState)
{
case MONSTERSTATE_COMBAT:
pTarget = m_hEnemy;
if (!pTarget)
SwitchLeechState();
else
{
// Chase the enemy's eyes
m_height = pTarget->pev->origin.z + pTarget->pev->view_ofs.z - 5;
// Clip to viable water area
if (m_height < m_bottom)
m_height = m_bottom;
else if (m_height > m_top)
m_height = m_top;
Vector location = pTarget->pev->origin - pev->origin;
location.z += (pTarget->pev->view_ofs.z);
if (location.Length() < 40)
SetConditions(bits_COND_CAN_MELEE_ATTACK1);
// Turn towards target ent
targetYaw = UTIL_VecToYaw(location);
targetYaw = UTIL_AngleDiff(targetYaw, UTIL_AngleMod(pev->angles.y));
if (targetYaw < (-LEECH_TURN_RATE * 0.75))
targetYaw = (-LEECH_TURN_RATE * 0.75);
else if (targetYaw > (LEECH_TURN_RATE * 0.75))
targetYaw = (LEECH_TURN_RATE * 0.75);
else
targetSpeed *= 2;
}
break;
default:
if (m_zTime < gpGlobals->time)
{
float newHeight = RANDOM_FLOAT(m_bottom, m_top);
m_height = 0.5 * m_height + 0.5 * newHeight;
m_zTime = gpGlobals->time + RANDOM_FLOAT(1, 4);
}
if (RANDOM_LONG(0, 100) < 10)
targetYaw = RANDOM_LONG(-30, 30);
pTarget = NULL;
// oldorigin test
if ((pev->origin - pev->oldorigin).Length() < 1)
{
// If leech didn't move, there must be something blocking it, so try to turn
m_sideTime = 0;
}
break;
}
m_obstacle = ObstacleDistance(pTarget);
pev->oldorigin = pev->origin;
if (m_obstacle < 0.1)
m_obstacle = 0.1;
// is the way ahead clear?
if (m_obstacle == 1.0)
{
// if the leech is turning, stop the trend.
if (m_flTurning != 0)
{
m_flTurning = 0;
}
m_fPathBlocked = false;
pev->speed = UTIL_Approach(targetSpeed, pev->speed, LEECH_SWIM_ACCEL * LEECH_FRAMETIME);
pev->velocity = gpGlobals->v_forward * pev->speed;
}
else
{
m_obstacle = 1.0 / m_obstacle;
// IF we get this far in the function, the leader's path is blocked!
m_fPathBlocked = true;
if (m_flTurning == 0) // something in the way and leech is not already turning to avoid
{
Vector vecTest;
// measure clearance on left and right to pick the best dir to turn
vecTest = pev->origin + (gpGlobals->v_right * LEECH_SIZEX) + (gpGlobals->v_forward * LEECH_CHECK_DIST);
UTIL_TraceLine(pev->origin, vecTest, missile, edict(), &tr);
flRightSide = tr.flFraction;
vecTest = pev->origin + (gpGlobals->v_right * -LEECH_SIZEX) + (gpGlobals->v_forward * LEECH_CHECK_DIST);
UTIL_TraceLine(pev->origin, vecTest, missile, edict(), &tr);
flLeftSide = tr.flFraction;
// turn left, right or random depending on clearance ratio
float delta = (flRightSide - flLeftSide);
if (delta > 0.1 || (delta > -0.1 && RANDOM_LONG(0, 100) < 50))
m_flTurning = -LEECH_TURN_RATE;
else
m_flTurning = LEECH_TURN_RATE;
}
pev->speed = UTIL_Approach(-(LEECH_SWIM_SPEED * 0.5), pev->speed, LEECH_SWIM_DECEL * LEECH_FRAMETIME * m_obstacle);
pev->velocity = gpGlobals->v_forward * pev->speed;
}
pev->ideal_yaw = m_flTurning + targetYaw;
UpdateMotion();
}
void CLeech::Killed(entvars_t* pevAttacker, int iGib)
{
Vector vecSplatDir;
TraceResult tr;
//ALERT(at_aiconsole, "Leech: killed\n");
// tell owner ( if any ) that we're dead.This is mostly for MonsterMaker functionality.
CBaseEntity* pOwner = CBaseEntity::Instance(pev->owner);
if (pOwner)
pOwner->DeathNotice(pev);
// When we hit the ground, play the "death_end" activity
if (0 != pev->waterlevel)
{
pev->angles.z = 0;
pev->angles.x = 0;
pev->origin.z += 1;
pev->avelocity = g_vecZero;
if (RANDOM_LONG(0, 99) < 70)
pev->avelocity.y = RANDOM_LONG(-720, 720);
pev->gravity = 0.02;
ClearBits(pev->flags, FL_ONGROUND);
SetActivity(ACT_DIESIMPLE);
}
else
SetActivity(ACT_DIEFORWARD);
pev->movetype = MOVETYPE_TOSS;
pev->takedamage = DAMAGE_NO;
SetThink(&CLeech::DeadThink);
}