// A Swarm grub, bursts out of gooey things and crawls about looking icky (non-violent) #include "cbase.h" #include "asw_grub.h" #include "asw_marine.h" #include "te_effect_dispatch.h" #include "npc_antlion.h" #include "npc_bullseye.h" #include "npcevent.h" #include "asw_marine.h" #include "soundenvelope.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" #define SWARM_GRUB_MODEL "models/swarm/Grubs/Grub.mdl" const int ASW_GRUB_MIN_JUMP_DIST = 48; const int ASW_GRUB_MAX_JUMP_DIST = 170; #define PARASITE_IGNORE_WORLD_COLLISION_TIME 0.5 IMPLEMENT_SERVERCLASS_ST( CASW_Grub, DT_ASW_Grub ) END_SEND_TABLE() ConVar asw_grub_speedboost( "asw_grub_speedboost", "1.4", FCVAR_CHEAT , "boost speed for the grubs" ); ConVar asw_debug_grubs("asw_debug_grubs", "0", FCVAR_CHEAT, "If set, grubs will print debug messages for various things"); extern ConVar sv_gravity; int ACT_ASW_GRUB_IDLE; CASW_Grub::CASW_Grub( void )// : CASW_Alien() { m_bMidJump = false; m_bCommittedToJump = false; } LINK_ENTITY_TO_CLASS( asw_grub_advanced, CASW_Grub ); //IMPLEMENT_SERVERCLASS_ST( CASW_Grub, DT_ASW_Grub ) //END_SEND_TABLE() BEGIN_DATADESC( CASW_Grub ) DEFINE_FIELD( m_bCommittedToJump, FIELD_BOOLEAN ), DEFINE_FIELD( m_bMidJump, FIELD_BOOLEAN ), DEFINE_FIELD( m_vecCommittedJumpPos, FIELD_POSITION_VECTOR ), DEFINE_FIELD( m_flNextNPCThink, FIELD_TIME ), DEFINE_FIELD( m_flIgnoreWorldCollisionTime, FIELD_TIME ), DEFINE_FIELD( m_bJumpFromGoo, FIELD_BOOLEAN ), DEFINE_FIELD( m_flGooJumpDistance, FIELD_FLOAT ), DEFINE_FIELD( m_flGooJumpAngle, FIELD_FLOAT ), DEFINE_THINKFUNC( LeapThink ), DEFINE_ENTITYFUNC( LeapTouch ), DEFINE_ENTITYFUNC( NormalTouch ), END_DATADESC() enum { SCHED_GRUB_JUMP_FROM_GOO = LAST_ASW_ALIEN_SHARED_SCHEDULE, SCHED_ASW_GRUB_WANDER_ANGRILY, }; enum { TASK_GRUB_JUMP_FROM_GOO = LAST_ASW_ALIEN_SHARED_TASK, }; extern int AE_HEADCRAB_JUMPATTACK; void CASW_Grub::Spawn( void ) { SetModel( SWARM_GRUB_MODEL ); Precache(); BaseClass::Spawn(); SetModel( SWARM_GRUB_MODEL ); SetHullType(HULL_TINY); SetHullSizeNormal(); SetViewOffset( Vector(6, 0, 11) ) ; // Position of the eyes relative to NPC's origin. SetNavType( NAV_GROUND ); SetBloodColor( BLOOD_COLOR_GREEN ); m_NPCState = NPC_STATE_NONE; m_iHealth = 1; SetSolid( SOLID_BBOX ); AddSolidFlags( FSOLID_NOT_STANDABLE ); SetMoveType( MOVETYPE_STEP ); if ( m_SquadName != NULL_STRING ) { CapabilitiesAdd( bits_CAP_SQUAD ); } SetCollisionGroup( ASW_COLLISION_GROUP_GRUBS ); //SetCollisionGroup( COLLISION_GROUP_DEBRIS ); CapabilitiesAdd( bits_CAP_MOVE_GROUND ); //if ( HasSpawnFlags( SF_ANTLION_USE_GROUNDCHECKS ) == false ) CapabilitiesAdd( bits_CAP_SKIP_NAV_GROUND_CHECK ); NPCInit(); BaseClass::Spawn(); SetEfficiency( AIE_EFFICIENT ); SetCollisionGroup( ASW_COLLISION_GROUP_GRUBS ); //RemoveSolidFlags( FSOLID_NOT_STANDABLE ); SetBlocksLOS(false); AddFlag( FL_FLY ); } CASW_Grub::~CASW_Grub() { } void CASW_Grub::Precache( void ) { PrecacheModel( SWARM_GRUB_MODEL ); //PrecacheModel( "Models/Swarm/Grubs/GrubGib1.mdl" ); //PrecacheModel( "Models/Swarm/Grubs/GrubGib2.mdl" ); //PrecacheModel( "Models/Swarm/Grubs/GrubGib3.mdl" ); PrecacheModel( "Models/Swarm/Grubs/GrubGib4.mdl" ); //PrecacheModel( "Models/Swarm/Grubs/GrubGib5.mdl" ); //PrecacheModel( "Models/Swarm/Grubs/GrubGib6.mdl" ); PrecacheScriptSound("ASW_Parasite.Death"); PrecacheScriptSound("ASW_Parasite.Attack"); PrecacheScriptSound("ASW_Parasite.Idle"); BaseClass::Precache(); } float CASW_Grub::GetIdealSpeed() const { return asw_grub_speedboost.GetFloat() * BaseClass::GetIdealSpeed() * m_flPlaybackRate; } float CASW_Grub::GetIdealAccel( ) const { return GetIdealSpeed() * 1.5f; } float CASW_Grub::MaxYawSpeed( void ) { return 128.0f; switch ( GetActivity() ) { case ACT_IDLE: return 64.0f; break; case ACT_WALK: return 64.0f; break; default: case ACT_RUN: return 64.0f; break; } return 64.0f; } // don't collide with other grubs during jump bool CASW_Grub::ShouldCollide(int collisionGroup, int contentsMask) { /* if (m_bMidJump) return false; if (m_bMidJump && (collisionGroup == COLLISION_GROUP_NONE || collisionGroup == ASW_COLLISION_GROUP_GRUBS) ) { return false; } if (collisionGroup == ASW_COLLISION_GROUP_GRUBS) return false; */ return BaseClass::ShouldCollide(collisionGroup, contentsMask); } void CASW_Grub::AlertSound() { //EmitSound("ASW_Drone.Alert"); //IdleSound(); } void CASW_Grub::PainSound( const CTakeDamageInfo &info ) { if (gpGlobals->curtime > m_fNextPainSound) { EmitSound("ASW_Parasite.Death"); m_fNextPainSound = gpGlobals->curtime + 0.5f; } } void CASW_Grub::AttackSound() { //EmitSound("ASW_Parasite.Attack"); } void CASW_Grub::IdleSound() { } bool CASW_Grub::CorpseGib( const CTakeDamageInfo &info ) { CEffectData data; data.m_vOrigin = WorldSpaceCenter(); data.m_vNormal = data.m_vOrigin - info.GetDamagePosition(); VectorNormalize( data.m_vNormal ); data.m_flScale = RemapVal( m_iHealth, 0, -500, 1, 3 ); data.m_flScale = clamp( data.m_flScale, 1, 3 ); data.m_fFlags = (IsOnFire() || (info.GetDamageType() & DMG_BURN)) ? ASW_GIBFLAG_ON_FIRE : 0; Msg("grubgib flags: %d burn=%d dt=%d\n", data.m_fFlags, (info.GetDamageType() & DMG_BURN), info.GetDamageType()); DispatchEffect( "GrubGib", data ); //CSoundEnt::InsertSound( SOUND_PHYSICS_DANGER, GetAbsOrigin(), 256, 0.5f, this ); return true; } //----------------------------------------------------------------------------- // Purpose: Does a jump attack at the given position. // Input : bRandomJump - Just hop in a random direction. // vecPos - Position to jump at, ignored if bRandom is set to true. // bThrown - //----------------------------------------------------------------------------- void CASW_Grub::JumpAttack( bool bRandomJump, const Vector &vecPos, bool bThrown ) { Vector vecJumpVel; if ( !bRandomJump ) { float gravity = sv_gravity.GetFloat(); if ( gravity <= 1 ) { gravity = 1; } // How fast does the headcrab need to travel to reach the position given gravity? float flActualHeight = vecPos.z - GetAbsOrigin().z; float height = flActualHeight; if ( height < 16 ) { height = 60; //16; } else { float flMaxHeight = bThrown ? 400 : 120; if ( height > flMaxHeight ) { height = flMaxHeight; } } // overshoot the jump by an additional 8 inches // NOTE: This calculation jumps at a position INSIDE the box of the enemy (player) // so if you make the additional height too high, the crab can land on top of the // enemy's head. If we want to jump high, we'll need to move vecPos to the surface/outside // of the enemy's box. float additionalHeight = 0; if ( height < 32 ) { additionalHeight = 8; } height += additionalHeight; // NOTE: This equation here is from vf^2 = vi^2 + 2*a*d float speed = sqrt( 2 * gravity * height ); float time = speed / gravity; // add in the time it takes to fall the additional height // So the impact takes place on the downward slope at the original height time += sqrt( (2 * additionalHeight) / gravity ); // Scale the sideways velocity to get there at the right time VectorSubtract( vecPos, GetAbsOrigin(), vecJumpVel ); vecJumpVel /= time; // Speed to offset gravity at the desired height. vecJumpVel.z = speed; // Don't jump too far/fast. float flJumpSpeed = vecJumpVel.Length(); float flMaxSpeed = bThrown ? 1000.0f : 650.0f; if ( flJumpSpeed > flMaxSpeed ) { vecJumpVel *= flMaxSpeed / flJumpSpeed; } } else { // // Jump hop, don't care where. // Vector forward, up; QAngle angFacing = GetLocalAngles(); angFacing.y = m_flGooJumpAngle; AngleVectors( angFacing, &forward, NULL, &up ); vecJumpVel = Vector( forward.x, forward.y, forward.z) * random->RandomFloat(100, 150); // random->RandomFloat(0, 30) } AttackSound(); Leap( vecJumpVel ); } void CASW_Grub::Leap( const Vector &vecVel ) { SetTouch( &CASW_Grub::LeapTouch ); SetCondition( COND_FLOATING_OFF_GROUND ); SetGroundEntity( NULL ); m_flIgnoreWorldCollisionTime = gpGlobals->curtime + PARASITE_IGNORE_WORLD_COLLISION_TIME; if( HasHeadroom() ) { // Take him off ground so engine doesn't instantly reset FL_ONGROUND. UTIL_SetOrigin( this, GetLocalOrigin() + Vector( 0, 0, 1 ) ); } SetAbsVelocity( vecVel ); // Think every frame so the player sees the headcrab where he actually is... m_bMidJump = true; SetThink( &CASW_Grub::LeapThink ); SetNextThink( gpGlobals->curtime ); //SetMoveType( MOVETYPE_FLYGRAVITY, MOVECOLLIDE_FLY_BOUNCE ); } void CASW_Grub::LeapThink( void ) { if (gpGlobals->curtime > m_flNextNPCThink) { NPCThink(); m_flNextNPCThink = gpGlobals->curtime + 0.1; } if( GetFlags() & FL_ONGROUND ) { SetThink( &CASW_Grub::CallNPCThink ); SetNextThink( gpGlobals->curtime + 0.1 ); return; } SetNextThink( gpGlobals->curtime ); } void CASW_Grub::NormalTouch(CBaseEntity* pOther) { if (!pOther || !pOther->CollisionProp() || pOther->Classify() != CLASS_ASW_MARINE) // only get squashed by marines return; // are we within the marine's x/y? //Vector vecMyFlatPos = GetAbsOrigin(); //vecMyFlatPos.z = pOther->GetAbsOrigin().z; //if (!CollisionProp()->IsPointInBounds(vecMyFlatPos)) //return; // are we underneath the marine? float z_diff = pOther->CollisionProp()->OBBCenter().z - GetAbsOrigin().z; //float other_half_height = pOther->CollisionProp()->OBBSize().z * 0.5f; //if (z_diff > other_half_height) if (pOther->GetAbsOrigin().z > GetAbsOrigin().z) // is he higher than me? { // squash this if (asw_debug_grubs.GetBool()) { Msg("Squashed by a marine (my z=%f his z=%f diff=%f dist=%f)\n", GetAbsOrigin().z, pOther->GetAbsOrigin().z, z_diff, pOther->GetAbsOrigin().DistTo(GetAbsOrigin())); } Squash(pOther); } else { if (asw_debug_grubs.GetBool()) { Msg("Not squashed by a marine (my z=%f his z=%f diff=%f dist=%f)\n", GetAbsOrigin().z, pOther->GetAbsOrigin().z, z_diff, pOther->GetAbsOrigin().DistTo(GetAbsOrigin())); } } } void CASW_Grub::Squash(CBaseEntity* pSquasher) { Event_Killed( CTakeDamageInfo( pSquasher, pSquasher, 200, DMG_CRUSH ) ); EmitSound( "NPC_AntlionGrub.Squash" ); trace_t tr; Vector vecDir( 0, 0, -1.0f ); tr.endpos = GetLocalOrigin(); tr.endpos[2] += 8.0f; MakeDamageBloodDecal( 4, 0.8f, &tr, vecDir ); SetTouch( NULL ); m_iHealth = 0; CorpseGib( CTakeDamageInfo( pSquasher, pSquasher, 200, DMG_CRUSH ) ); UTIL_Remove(this); } //----------------------------------------------------------------------------- // Purpose: LeapTouch - this is the headcrab's touch function when it is in the air. // Input : *pOther - //----------------------------------------------------------------------------- void CASW_Grub::LeapTouch( CBaseEntity *pOther ) { m_bMidJump = false; if ( IRelationType( pOther ) == D_HT ) { // Don't hit if back on ground if ( !( GetFlags() & FL_ONGROUND ) ) { if ( pOther->m_takedamage != DAMAGE_NO ) { //BiteSound(); //TouchDamage( pOther ); } else { //ImpactSound(); } } else { //ImpactSound(); } } else if( !(GetFlags() & FL_ONGROUND) ) { // Still in the air... if( gpGlobals->curtime < m_flIgnoreWorldCollisionTime ) { // Headcrabs try to ignore the world, static props, and friends for a // fraction of a second after they jump. This is because they often brush // doorframes or props as they leap, and touching those objects turns off // this touch function, which can cause them to hit the player and not bite. // A timer probably isn't the best way to fix this, but it's one of our // safer options at this point (sjb). return; } if( !pOther->IsSolid() ) { // Touching a trigger or something. return; } // don't collide with other grubs if (pOther->Classify() == CLASS_ASW_GRUB) return; // don't collide with marines if (pOther->Classify() == CLASS_ASW_MARINE) return; } // make sure we're solid RemoveSolidFlags( FSOLID_NOT_SOLID ); // make it so we collide with various things again SetCollisionGroup(ASW_COLLISION_GROUP_GRUBS); m_takedamage = DAMAGE_YES; // doesn't take damage until he's landed // Shut off the touch function. SetTouch( &CASW_Grub::NormalTouch ); SetThink ( &CASW_Grub::CallNPCThink ); } bool CASW_Grub::HasHeadroom() { trace_t tr; UTIL_TraceEntity( this, GetAbsOrigin(), GetAbsOrigin() + Vector( 0, 0, 1 ), MASK_NPCSOLID, this, GetCollisionGroup(), &tr ); return (tr.fraction == 1.0); } void CASW_Grub::StartTask(const Task_t *pTask) { switch (pTask->iTask) { case TASK_GRUB_JUMP_FROM_GOO: { DoJumpFromGoo(); break; } default: { BaseClass::StartTask( pTask ); } } } void CASW_Grub::RunTask( const Task_t *pTask ) { switch ( pTask->iTask ) { case TASK_GRUB_JUMP_FROM_GOO: GetMotor()->UpdateYaw(); if ( FacingIdeal() ) { TaskComplete(); } break; default: BaseClass::RunTask( pTask ); break; } } int CASW_Grub::SelectSchedule() { if ( m_bJumpFromGoo ) { m_bJumpFromGoo = false; return SCHED_GRUB_JUMP_FROM_GOO; } return BaseClass::SelectSchedule(); } void CASW_Grub::SetJumpFromGoo(bool bDoJump, float flJumpAngle, float flJumpDistance) { m_bJumpFromGoo = true; m_flGooJumpDistance = flJumpDistance; m_flGooJumpAngle = flJumpAngle; } void CASW_Grub::DoJumpFromGoo() { Vector dir = vec3_origin; AngleVectors(GetAbsAngles(), &dir); Vector vecJumpPos = GetAbsOrigin() + dir * m_flGooJumpDistance; SetActivity( ACT_JUMP ); StudioFrameAdvanceManual( 0.0 ); SetParent( NULL ); RemoveFlag( FL_FLY ); AddEffects( EF_NOINTERP ); GetMotor()->SetIdealYaw( m_flGooJumpAngle ); // do a random jump forward JumpAttack( true, vecJumpPos, false ); } void CASW_Grub::IdleInGoo(bool b) { if (b) { SetActivity((Activity) ACT_ASW_GRUB_IDLE); } bDoGooIdle = b; } Activity CASW_Grub::TranslateActivity( Activity baseAct, Activity *pIdealWeaponActivity ) { Activity translated = BaseClass::TranslateActivity(baseAct, pIdealWeaponActivity); if (translated == ACT_IDLE && bDoGooIdle) { return (Activity) ACT_ASW_GRUB_IDLE; } return translated; } int CASW_Grub::TranslateSchedule( int scheduleType ) { switch( scheduleType ) { case SCHED_IDLE_STAND: case SCHED_IDLE_WALK: case SCHED_ALERT_STAND: case SCHED_ALERT_FACE: case SCHED_COMBAT_FACE: return SCHED_ASW_GRUB_WANDER_ANGRILY; } return BaseClass::TranslateSchedule( scheduleType ); } int CASW_Grub::SelectFailSchedule( int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode ) { if ( failedSchedule != SCHED_ASW_GRUB_WANDER_ANGRILY && ( failedSchedule == SCHED_TAKE_COVER_FROM_ENEMY || failedSchedule == SCHED_CHASE_ENEMY_FAILED ) ) { return SCHED_ASW_GRUB_WANDER_ANGRILY; } return BaseClass::SelectFailSchedule( failedSchedule, failedTask, taskFailCode ); } // since grubs are just decorative, make sure they're not wasting CPU void CASW_Grub::UpdateEfficiency( bool bInPVS ) { SetEfficiency( AIE_VERY_EFFICIENT ); //SetMoveEfficiency( AIME_EFFICIENT ); SetMoveEfficiency( AIME_NORMAL ); } bool CASW_Grub::BlocksLOS( void ) { return false; } AI_BEGIN_CUSTOM_NPC( asw_grub, CASW_Grub ) DECLARE_ANIMEVENT( AE_HEADCRAB_JUMPATTACK ) DECLARE_TASK( TASK_GRUB_JUMP_FROM_GOO ) DECLARE_ACTIVITY( ACT_ASW_GRUB_IDLE ) DEFINE_SCHEDULE ( SCHED_GRUB_JUMP_FROM_GOO, " Tasks" " TASK_GRUB_JUMP_FROM_GOO 0" "" " Interrupts" ) DEFINE_SCHEDULE ( SCHED_ASW_GRUB_WANDER_ANGRILY, " Tasks" " TASK_WANDER 480240" // 48 units to 240 units. " TASK_WALK_PATH 0" " TASK_WAIT_FOR_MOVEMENT 4" "" " Interrupts" ) AI_END_CUSTOM_NPC()