/*** * * Copyright (c) 1996-2002, 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.0f #define LEECH_SWIM_ACCEL 80.0f #define LEECH_SWIM_DECEL 10.0f #define LEECH_TURN_RATE 90 #define LEECH_SIZEX 10 #define LEECH_FRAMETIME 0.1f #define DEBUG_BEAMS 0 #if DEBUG_BEAMS #include "effects.h" #endif class CLeech : public CBaseMonster { public: void Spawn( void ); void Precache( void ); void EXPORT SwimThink( void ); void EXPORT DeadThink( void ); void Touch( CBaseEntity *pOther ) { 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( void ) { pev->absmin = pev->origin + Vector( -8, -8, 0 ); pev->absmax = pev->origin + Vector( 8, 8, 2 ); } void AttackSound( void ); void AlertSound( void ); void UpdateMotion( void ); float ObstacleDistance( CBaseEntity *pTarget ); void MakeVectors( void ); void RecalculateWaterlevel( void ); void SwitchLeechState( void ); // Base entity functions void HandleAnimEvent( MonsterEvent_t *pEvent ); int BloodColor( void ) { return DONT_BLEED; } void Killed( entvars_t *pevAttacker, int iGib ); void Activate( void ); int TakeDamage( entvars_t *pevInflictor, entvars_t *pevAttacker, float flDamage, int bitsDamageType ); int Classify( void ) { return CLASS_INSECT; } int IRelationship( CBaseEntity *pTarget ); virtual int Save( CSave &save ); virtual int Restore( CRestore &restore ); 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 ) LINK_ENTITY_TO_CLASS( monster_piranha, 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( void ) { 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( void ) { RecalculateWaterlevel(); } void CLeech::RecalculateWaterlevel( void ) { // Calculate boundaries Vector vecTest = pev->origin - Vector( 0, 0, 400 ); TraceResult tr; UTIL_TraceLine( pev->origin, vecTest, missile, edict(), &tr ); if( tr.flFraction != 1.0f ) m_bottom = tr.vecEndPos.z + 1.0f; 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.8f + m_top * 0.2f; m_top = m_bottom * 0.2f + m_top * 0.8f; m_bottom = newBottom; m_height = RANDOM_FLOAT( m_bottom, m_top ); m_waterTime = gpGlobals->time + RANDOM_FLOAT( 5, 7 ); } void CLeech::SwitchLeechState( void ) { 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( void ) { if( gpGlobals->time > m_attackSoundTime ) { EMIT_SOUND_DYN( ENT( pev ), CHAN_VOICE, pAttackSounds[RANDOM_LONG( 0, ARRAYSIZE( pAttackSounds ) - 1 )], 1.0f, ATTN_NORM, 0, PITCH_NORM ); m_attackSoundTime = gpGlobals->time + 0.5f; } } void CLeech::AlertSound( void ) { EMIT_SOUND_DYN( ENT( pev ), CHAN_VOICE, pAlertSounds[RANDOM_LONG( 0, ARRAYSIZE( pAlertSounds ) - 1 )], 1.0f, ATTN_NORM * 0.5f, 0, PITCH_NORM ); } void CLeech::Precache( void ) { size_t i; //PRECACHE_MODEL( "models/icky.mdl" ); PRECACHE_MODEL( "models/leech.mdl" ); for( i = 0; i < ARRAYSIZE( pAttackSounds ); i++ ) PRECACHE_SOUND( pAttackSounds[i] ); for( i = 0; i < ARRAYSIZE( pAlertSounds ); i++ ) PRECACHE_SOUND( pAlertSounds[i] ); } int 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.0f; } 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.9f ) // 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( void ) { 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( tr.fStartSolid ) { pev->speed = -LEECH_SWIM_SPEED * 0.5f; //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.0f ) { 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.0f ) 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.0f ) return tr.flFraction; // Didn't hit either side, so stop testing for another 0.5 - 1 seconds m_sideTime = gpGlobals->time + RANDOM_FLOAT( 0.5f, 1.0f ); } return 1.0; } void CLeech::DeadThink( void ) { if( m_fSequenceFinished ) { if( m_Activity == ACT_DIEFORWARD ) { SetThink( NULL ); StopAnimation(); return; } else if( pev->flags & FL_ONGROUND ) { pev->solid = SOLID_NOT; SetActivity(ACT_DIEFORWARD); } } StudioFrameAdvance(); pev->nextthink = gpGlobals->time + 0.1f; // 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.5f, missile, edict(), &tr ); if( tr.flFraction != 1.0f ) { pev->velocity.x = 0.0f; pev->velocity.y = 0.0f; } } } void CLeech::UpdateMotion( void ) { float flapspeed = ( pev->speed - m_flAccelerate ) / LEECH_ACCELERATE; m_flAccelerate = m_flAccelerate * 0.8f + pev->speed * 0.2f; if( flapspeed < 0 ) flapspeed = -flapspeed; flapspeed += 1.0f; if( flapspeed < 0.5f ) flapspeed = 0.5f; if( flapspeed > 1.9f ) flapspeed = 1.9f; 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.0f ) m_IdealActivity = ACT_TURN_LEFT; else if( pev->avelocity.y < -150.0f ) 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.0f * LEECH_FRAMETIME ); // bank pev->avelocity.z = -( pev->angles.z + ( pev->avelocity.y * 0.25f ) ); if( m_MonsterState == MONSTERSTATE_COMBAT && HasConditions( bits_COND_CAN_MELEE_ATTACK1 ) ) m_IdealActivity = ACT_MELEE_ATTACK1; // Out of water check if( !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.0f; pev->angles.x = 0.0f; if( pev->framerate < 1.0f ) pev->framerate = 1.0f; } 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.25f ) ); if( m_fPathBlocked ) { float color = m_obstacle * 30; if( m_obstacle == 1.0f ) 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( void ) { 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.0f, 1.5f ); pev->velocity = g_vecZero; return; } else pev->nextthink = gpGlobals->time + 0.1f; 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.75f ) ) targetYaw = ( -LEECH_TURN_RATE * 0.75f ); else if( targetYaw > ( LEECH_TURN_RATE * 0.75f ) ) targetYaw = ( LEECH_TURN_RATE * 0.75f ); else targetSpeed *= 2; } break; default: if( m_zTime < gpGlobals->time ) { float newHeight = RANDOM_FLOAT( m_bottom, m_top ); m_height = 0.5f * m_height + 0.5f * 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.1f ) m_obstacle = 0.1f; // is the way ahead clear? if( m_obstacle == 1.0f ) { // 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.0f / 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.1f || ( delta > -0.1f && RANDOM_LONG( 0, 100 ) < 50 ) ) m_flTurning = -LEECH_TURN_RATE; else m_flTurning = LEECH_TURN_RATE; } pev->speed = UTIL_Approach( -( LEECH_SWIM_SPEED * 0.5f ), 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( 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 ); }