diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/server/hl1 | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'game/server/hl1')
80 files changed, 40059 insertions, 0 deletions
diff --git a/game/server/hl1/hl1_ai_basenpc.cpp b/game/server/hl1/hl1_ai_basenpc.cpp new file mode 100644 index 0000000..f97faf0 --- /dev/null +++ b/game/server/hl1/hl1_ai_basenpc.cpp @@ -0,0 +1,215 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "hl1_ai_basenpc.h" +#include "scripted.h" +#include "soundent.h" +#include "animation.h" +#include "entitylist.h" +#include "ai_navigator.h" +#include "ai_motor.h" +#include "player.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "npcevent.h" + +#include "effect_dispatch_data.h" +#include "te_effect_dispatch.h" +#include "cplane.h" +#include "ai_squad.h" + +#define HUMAN_GIBS 1 +#define ALIEN_GIBS 2 + +//========================================================= +// NoFriendlyFire - checks for possibility of friendly fire +// +// Builds a large box in front of the grunt and checks to see +// if any squad members are in that box. +//========================================================= +bool CHL1BaseNPC::NoFriendlyFire( void ) +{ + if ( !m_pSquad ) + { + return true; + } + + CPlane backPlane; + CPlane leftPlane; + CPlane rightPlane; + + Vector vecLeftSide; + Vector vecRightSide; + Vector v_left; + + Vector vForward, vRight, vUp; + QAngle vAngleToEnemy; + + if ( GetEnemy() != NULL ) + { + //!!!BUGBUG - to fix this, the planes must be aligned to where the monster will be firing its gun, not the direction it is facing!!! + VectorAngles( ( GetEnemy()->WorldSpaceCenter() - GetAbsOrigin() ), vAngleToEnemy ); + + AngleVectors ( vAngleToEnemy, &vForward, &vRight, &vUp ); + } + else + { + // if there's no enemy, pretend there's a friendly in the way, so the grunt won't shoot. + return false; + } + + vecLeftSide = GetAbsOrigin() - ( vRight * ( WorldAlignSize().x * 1.5 ) ); + vecRightSide = GetAbsOrigin() + ( vRight * ( WorldAlignSize().x * 1.5 ) ); + v_left = vRight * -1; + + leftPlane.InitializePlane ( vRight, vecLeftSide ); + rightPlane.InitializePlane ( v_left, vecRightSide ); + backPlane.InitializePlane ( vForward, GetAbsOrigin() ); + + AISquadIter_t iter; + for ( CAI_BaseNPC *pSquadMember = m_pSquad->GetFirstMember( &iter ); pSquadMember; pSquadMember = m_pSquad->GetNextMember( &iter ) ) + { + if ( pSquadMember == NULL ) + continue; + + if ( pSquadMember == this ) + continue; + + if ( backPlane.PointInFront ( pSquadMember->GetAbsOrigin() ) && + leftPlane.PointInFront ( pSquadMember->GetAbsOrigin() ) && + rightPlane.PointInFront ( pSquadMember->GetAbsOrigin()) ) + { + // this guy is in the check volume! Don't shoot! + return false; + } + } + + return true; +} + +void CHL1BaseNPC::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + if ( info.GetDamage() >= 1.0 && !(info.GetDamageType() & DMG_SHOCK ) ) + { + UTIL_BloodSpray( ptr->endpos, vecDir, BloodColor(), 4, FX_BLOODSPRAY_ALL ); + } + + BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator ); +} + + +bool CHL1BaseNPC::ShouldGib( const CTakeDamageInfo &info ) +{ + if ( info.GetDamageType() & DMG_NEVERGIB ) + return false; + + if ( ( g_pGameRules->Damage_ShouldGibCorpse( info.GetDamageType() ) && m_iHealth < GIB_HEALTH_VALUE ) || ( info.GetDamageType() & DMG_ALWAYSGIB ) ) + return true; + + return false; + +} + +bool CHL1BaseNPC::HasHumanGibs( void ) +{ + Class_T myClass = Classify(); + + if ( myClass == CLASS_HUMAN_MILITARY || + myClass == CLASS_PLAYER_ALLY || + myClass == CLASS_HUMAN_PASSIVE || + myClass == CLASS_PLAYER ) + + return true; + + return false; +} + + +bool CHL1BaseNPC::HasAlienGibs( void ) +{ + Class_T myClass = Classify(); + + if ( myClass == CLASS_ALIEN_MILITARY || + myClass == CLASS_ALIEN_MONSTER || + myClass == CLASS_INSECT || + myClass == CLASS_ALIEN_PREDATOR || + myClass == CLASS_ALIEN_PREY ) + + return true; + + return false; +} + + +void CHL1BaseNPC::Precache( void ) +{ + PrecacheModel( "models/gibs/agibs.mdl" ); + PrecacheModel( "models/gibs/hgibs.mdl" ); + + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CHL1BaseNPC::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 ); + + if ( HasAlienGibs() ) + data.m_nMaterial = ALIEN_GIBS; + else if ( HasHumanGibs() ) + data.m_nMaterial = HUMAN_GIBS; + + data.m_nColor = BloodColor(); + + DispatchEffect( "HL1Gib", data ); + + CSoundEnt::InsertSound( SOUND_MEAT, GetAbsOrigin(), 256, 0.5f, this ); + +/// BaseClass::CorpseGib( info ); + + return true; +} + +int CHL1BaseNPC::IRelationPriority( CBaseEntity *pTarget ) +{ + return BaseClass::IRelationPriority( pTarget ); +} + +void CHL1BaseNPC::EjectShell( const Vector &vecOrigin, const Vector &vecVelocity, float rotation, int iType ) +{ + CEffectData data; + data.m_vStart = vecVelocity; + data.m_vOrigin = vecOrigin; + data.m_vAngles = QAngle( 0, rotation, 0 ); + data.m_fFlags = iType; + + DispatchEffect( "HL1ShellEject", data ); +} + +// HL1 version - never return Ragdoll as the automatic schedule at the end of a +// scripted sequence +int CHL1BaseNPC::SelectDeadSchedule() +{ + // Alread dead (by animation event maybe?) + // Is it safe to set it to SCHED_NONE? + if ( m_lifeState == LIFE_DEAD ) + return SCHED_NONE; + + CleanupOnDeath(); + return SCHED_DIE; +} diff --git a/game/server/hl1/hl1_ai_basenpc.h b/game/server/hl1/hl1_ai_basenpc.h new file mode 100644 index 0000000..0b11ce1 --- /dev/null +++ b/game/server/hl1/hl1_ai_basenpc.h @@ -0,0 +1,51 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Base combat character with no AI +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// + +#ifndef HL1_AI_BASENPC_H +#define HL1_AI_BASENPC_H + +#ifdef _WIN32 +#pragma once +#endif + + +#include "ai_basenpc.h" +#include "ai_motor.h" +//============================================================================= +// >> CHL1NPCTalker +//============================================================================= + +class CHL1BaseNPC : public CAI_BaseNPC +{ + DECLARE_CLASS( CHL1BaseNPC, CAI_BaseNPC ); + +public: + CHL1BaseNPC( void ) + { + + } + + void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + bool ShouldGib( const CTakeDamageInfo &info ); + bool CorpseGib( const CTakeDamageInfo &info ); + + bool HasAlienGibs( void ); + bool HasHumanGibs( void ); + + void Precache( void ); + + int IRelationPriority( CBaseEntity *pTarget ); + bool NoFriendlyFire( void ); + + void EjectShell( const Vector &vecOrigin, const Vector &vecVelocity, float rotation, int iType ); + + virtual int SelectDeadSchedule(); +}; + +#endif //HL1_AI_BASENPC_H diff --git a/game/server/hl1/hl1_basecombatweapon.cpp b/game/server/hl1/hl1_basecombatweapon.cpp new file mode 100644 index 0000000..39a4707 --- /dev/null +++ b/game/server/hl1/hl1_basecombatweapon.cpp @@ -0,0 +1,86 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + + +#include "cbase.h" +#include "hl1_basecombatweapon_shared.h" +#include "effect_dispatch_data.h" +#include "te_effect_dispatch.h" + + +BEGIN_DATADESC( CBaseHL1CombatWeapon ) + DEFINE_THINKFUNC( FallThink ), +END_DATADESC(); + + +void CBaseHL1CombatWeapon::Precache() +{ + BaseClass::Precache(); + + PrecacheScriptSound( "BaseCombatWeapon.WeaponDrop" ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseHL1CombatWeapon::FallInit( void ) +{ + SetModel( GetWorldModel() ); + SetMoveType( MOVETYPE_FLYGRAVITY, MOVECOLLIDE_FLY_BOUNCE ); + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_TRIGGER ); + AddSolidFlags( FSOLID_NOT_SOLID ); + + SetPickupTouch(); + + SetThink( &CBaseHL1CombatWeapon::FallThink ); + + SetNextThink( gpGlobals->curtime + 0.1f ); + + // HACKHACK - On ground isn't always set, so look for ground underneath + trace_t tr; + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() - Vector(0,0,2), MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); + + if ( tr.fraction < 1.0 ) + { + SetGroundEntity( tr.m_pEnt ); + } + + SetViewOffset( Vector(0,0,8) ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Items that have just spawned run this think to catch them when +// they hit the ground. Once we're sure that the object is grounded, +// we change its solid type to trigger and set it in a large box that +// helps the player get it. +//----------------------------------------------------------------------------- +void CBaseHL1CombatWeapon::FallThink ( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1f ); + + if ( GetFlags() & FL_ONGROUND ) + { + // clatter if we have an owner (i.e., dropped by someone) + // don't clatter if the gun is waiting to respawn (if it's waiting, it is invisible!) + if ( GetOwnerEntity() ) + { + EmitSound( "BaseCombatWeapon.WeaponDrop" ); + } + + // lie flat + QAngle ang = GetAbsAngles(); + ang.x = 0; + ang.z = 0; + SetAbsAngles( ang ); + + Materialize(); + + SetSize( Vector( -24, -24, 0 ), Vector( 24, 24, 16 ) ); + } +} + diff --git a/game/server/hl1/hl1_basegrenade.cpp b/game/server/hl1/hl1_basegrenade.cpp new file mode 100644 index 0000000..8762ae4 --- /dev/null +++ b/game/server/hl1/hl1_basegrenade.cpp @@ -0,0 +1,116 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + + +#include "cbase.h" +#include "decals.h" +#include "basecombatcharacter.h" +#include "shake.h" +#include "engine/IEngineSound.h" +#include "soundent.h" +#include "entitylist.h" +#include "hl1_basegrenade.h" + + +extern short g_sModelIndexFireball; // (in combatweapon.cpp) holds the index for the fireball +extern short g_sModelIndexWExplosion; // (in combatweapon.cpp) holds the index for the underwater explosion + +unsigned int CHL1BaseGrenade::PhysicsSolidMaskForEntity( void ) const +{ + return BaseClass::PhysicsSolidMaskForEntity() | CONTENTS_HITBOX; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CHL1BaseGrenade::Precache() +{ + BaseClass::Precache(); + + PrecacheScriptSound( "BaseGrenade.Explode" ); +} + + +void CHL1BaseGrenade::Explode( trace_t *pTrace, int bitsDamageType ) +{ + float flRndSound;// sound randomizer + + SetModelName( NULL_STRING );//invisible + AddSolidFlags( FSOLID_NOT_SOLID ); + + m_takedamage = DAMAGE_NO; + + // Pull out of the wall a bit + if ( pTrace->fraction != 1.0 ) + { + SetLocalOrigin( pTrace->endpos + (pTrace->plane.normal * 0.6) ); + } + + Vector vecAbsOrigin = GetAbsOrigin(); + int contents = UTIL_PointContents ( vecAbsOrigin ); + + if ( pTrace->fraction != 1.0 ) + { + Vector vecNormal = pTrace->plane.normal; + const surfacedata_t *pdata = physprops->GetSurfaceData( pTrace->surface.surfaceProps ); + CPASFilter filter( vecAbsOrigin ); + te->Explosion( filter, 0.0, + &vecAbsOrigin, + !( contents & MASK_WATER ) ? g_sModelIndexFireball : g_sModelIndexWExplosion, + m_DmgRadius * .03, + 25, + TE_EXPLFLAG_NONE, + m_DmgRadius, + m_flDamage, + &vecNormal, + (char) pdata->game.material ); + } + else + { + CPASFilter filter( vecAbsOrigin ); + te->Explosion( filter, 0.0, + &vecAbsOrigin, + !( contents & MASK_WATER ) ? g_sModelIndexFireball : g_sModelIndexWExplosion, + m_DmgRadius * .03, + 25, + TE_EXPLFLAG_NONE, + m_DmgRadius, + m_flDamage ); + } + + CSoundEnt::InsertSound ( SOUND_COMBAT, GetAbsOrigin(), BASEGRENADE_EXPLOSION_VOLUME, 3.0 ); + + // Use the owner's position as the reported position + Vector vecReported = GetThrower() ? GetThrower()->GetAbsOrigin() : vec3_origin; + + CTakeDamageInfo info( this, GetThrower(), GetBlastForce(), GetAbsOrigin(), m_flDamage, bitsDamageType, 0, &vecReported ); + + RadiusDamage( info, GetAbsOrigin(), m_DmgRadius, CLASS_NONE, NULL ); + + UTIL_DecalTrace( pTrace, "Scorch" ); + + flRndSound = random->RandomFloat( 0 , 1 ); + + EmitSound( "BaseGrenade.Explode" ); + + SetTouch( NULL ); + + AddEffects( EF_NODRAW ); + SetAbsVelocity( vec3_origin ); + + SetThink( &CBaseGrenade::Smoke ); + SetNextThink( gpGlobals->curtime + 0.3); + + if ( GetWaterLevel() == 0 ) + { + int sparkCount = random->RandomInt( 0,3 ); + QAngle angles; + VectorAngles( pTrace->plane.normal, angles ); + + for ( int i = 0; i < sparkCount; i++ ) + Create( "spark_shower", GetAbsOrigin(), angles, NULL ); + } +} diff --git a/game/server/hl1/hl1_basegrenade.h b/game/server/hl1/hl1_basegrenade.h new file mode 100644 index 0000000..1f88846 --- /dev/null +++ b/game/server/hl1/hl1_basegrenade.h @@ -0,0 +1,42 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef HL1_BASEGRENADE_H +#define HL1_BASEGRENADE_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "basegrenade_shared.h" + + +class CHL1BaseGrenade : public CBaseGrenade +{ + DECLARE_CLASS( CHL1BaseGrenade, CBaseGrenade ); +public: + + virtual void Precache(); + + void Explode( trace_t *pTrace, int bitsDamageType ); + unsigned int PhysicsSolidMaskForEntity( void ) const; +}; + +class CHandGrenade : public CHL1BaseGrenade +{ +public: + DECLARE_CLASS( CHandGrenade, CHL1BaseGrenade ); + DECLARE_DATADESC(); + + void Spawn( void ); + void Precache( void ); + void BounceSound( void ); + void BounceTouch( CBaseEntity *pOther ); + + void ShootTimed( CBaseCombatCharacter *pOwner, Vector vecVelocity, float flTime ); +}; + +#endif // HL1_BASEGRENADE_H diff --git a/game/server/hl1/hl1_client.cpp b/game/server/hl1/hl1_client.cpp new file mode 100644 index 0000000..7473e10 --- /dev/null +++ b/game/server/hl1/hl1_client.cpp @@ -0,0 +1,199 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +/* + +===== tf_client.cpp ======================================================== + + HL1 client/server game specific stuff + +*/ + +#include "cbase.h" +#include "hl1_player.h" +#include "hl1mp_player.h" +#include "hl1_gamerules.h" +#include "gamerules.h" +#include "teamplay_gamerules.h" +#include "entitylist.h" +#include "physics.h" +#include "game.h" +#include "player_resource.h" +#include "engine/IEngineSound.h" + +#include "tier0/vprof.h" + +void Host_Say( edict_t *pEdict, bool teamonly ); + +extern CBaseEntity* FindPickerEntityClass( CBasePlayer *pPlayer, char *classname ); +extern bool g_fGameOver; + +/* +=========== +ClientPutInServer + +called each time a player is spawned into the game +============ +*/ +void ClientPutInServer( edict_t *pEdict, const char *playername ) +{ + CHL1_Player *pPlayer = NULL; + + // Allocate a CBasePlayer for pev, and call spawn + if ( g_pGameRules->IsMultiplayer() ) + pPlayer = CHL1_Player::CreatePlayer( "player_mp", pEdict ); + else + pPlayer = CHL1_Player::CreatePlayer( "player", pEdict ); + + pPlayer->SetPlayerName( playername ); +} + + +void ClientActive( edict_t *pEdict, bool bLoadGame ) +{ + CHL1_Player *pPlayer = dynamic_cast< CHL1_Player* >( CBaseEntity::Instance( pEdict ) ); + + pPlayer->InitialSpawn(); + + if ( !bLoadGame ) + { + pPlayer->Spawn(); + } +} + + +/* +=============== +const char *GetGameDescription() + +Returns the descriptive name of this .dll. E.g., Half-Life, or Team Fortress 2 +=============== +*/ +const char *GetGameDescription() +{ + if ( g_pGameRules ) // this function may be called before the world has spawned, and the game rules initialized + return g_pGameRules->GetGameDescription(); + else + return "Half-Life 1"; +} + +//----------------------------------------------------------------------------- +// Purpose: Given a player and optional name returns the entity of that +// classname that the player is nearest facing +// +// Input : +// Output : +//----------------------------------------------------------------------------- +CBaseEntity* FindEntity( edict_t *pEdict, char *classname) +{ + // If no name was given set bits based on the picked + if (FStrEq(classname,"")) + { + return (FindPickerEntityClass( static_cast<CBasePlayer*>(GetContainingEntity(pEdict)), classname )); + } + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Precache game-specific models & sounds +//----------------------------------------------------------------------------- +void ClientGamePrecache( void ) +{ + // Multiplayer uses different models, and more of them. + if ( g_pGameRules->IsMultiplayer() ) + { + CBaseEntity::PrecacheModel("models/player/mp/barney/barney.mdl"); + CBaseEntity::PrecacheModel("models/player/mp/gina/gina.mdl"); + CBaseEntity::PrecacheModel("models/player/mp/gman/gman.mdl"); + CBaseEntity::PrecacheModel("models/player/mp/gordon/gordon.mdl"); + CBaseEntity::PrecacheModel("models/player/mp/helmet/helmet.mdl"); + CBaseEntity::PrecacheModel("models/player/mp/hgrunt/hgrunt.mdl"); + CBaseEntity::PrecacheModel("models/player/mp/robo/robo.mdl"); + CBaseEntity::PrecacheModel("models/player/mp/scientist/scientist.mdl"); + CBaseEntity::PrecacheModel("models/player/mp/zombie/zombie.mdl"); + CBaseEntity::PrecacheModel("models/player.mdl" ); + } + else + { + CBaseEntity::PrecacheModel("models/player.mdl" ); + } + + CBaseEntity::PrecacheModel( "models/gibs/agibs.mdl" ); + + CBaseEntity::PrecacheScriptSound( "Player.UseDeny" ); +} + + +// called by ClientKill and DeadThink +void respawn( CBaseEntity *pEdict, bool fCopyCorpse ) +{ + if (gpGlobals->coop || gpGlobals->deathmatch) + { + if ( fCopyCorpse ) + { + // make a copy of the dead body for appearances sake + ((CHL1MP_Player *)pEdict)->CreateCorpse(); + } + + // respawn player + pEdict->Spawn(); + } + else + { // restart the entire server + engine->ServerCommand("reload\n"); + } +} + +void GameStartFrame( void ) +{ + VPROF("GameStartFrame()"); + + if ( g_fGameOver ) + return; + + gpGlobals->teamplay = (teamplay.GetInt() != 0); + +#ifdef DEBUG + extern void Bot_RunAll(); + Bot_RunAll(); +#endif +} + +//========================================================= +// instantiate the proper game rules object +//========================================================= +void InstallGameRules() +{ + engine->ServerCommand( "exec game.cfg\n" ); + engine->ServerExecute( ); + + if ( !gpGlobals->deathmatch ) + { + // generic half-life + CreateGameRulesObject( "CHalfLife1" ); + return; + } + else + { + CreateGameRulesObject( "CHL1MPRules" ); + return; + + if ( teamplay.GetInt() > 0 ) + { + // teamplay + CreateGameRulesObject( "CTeamplayRules" ); + return; + } + + // vanilla deathmatch + CreateGameRulesObject( "CMultiplayRules" ); + return; + } + + CreateGameRulesObject( "CHalfLife1" ); +} + diff --git a/game/server/hl1/hl1_ents.cpp b/game/server/hl1/hl1_ents.cpp new file mode 100644 index 0000000..c5ab56e --- /dev/null +++ b/game/server/hl1/hl1_ents.cpp @@ -0,0 +1,1654 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "ai_basenpc.h" +#include "trains.h" +#include "ndebugoverlay.h" +#include "entitylist.h" +#include "engine/IEngineSound.h" +#include "hl1_ents.h" +#include "doors.h" +#include "soundent.h" +#include "hl1_basegrenade.h" +#include "shake.h" +#include "globalstate.h" +#include "soundscape.h" +#include "buttons.h" +#include "Sprite.h" +#include "actanimating.h" +#include "npcevent.h" +#include "func_break.h" +#include "hl1_shareddefs.h" +#include "eventqueue.h" + + +// +// TRIGGERS: trigger_auto, trigger_relay, multimanager: replaced in src by logic_auto and logic_relay +// + +// This trigger will fire when the level spawns (or respawns if not fire once) +// It will check a global state before firing. +#define SF_AUTO_FIREONCE 0x0001 + +class CAutoTrigger : public CBaseEntity +{ + DECLARE_CLASS( CAutoTrigger, CBaseEntity ); +public: + void Spawn( void ); + void Precache( void ); + void Think( void ); + + int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + + DECLARE_DATADESC(); + +private: + + COutputEvent m_OnTrigger; + string_t m_globalstate; +}; +LINK_ENTITY_TO_CLASS( trigger_auto, CAutoTrigger ); + +BEGIN_DATADESC( CAutoTrigger ) + DEFINE_KEYFIELD( m_globalstate, FIELD_STRING, "globalstate" ), + + // Outputs + DEFINE_OUTPUT(m_OnTrigger, "OnTrigger"), +END_DATADESC() + + +void CAutoTrigger::Spawn( void ) +{ + Precache(); +} + + +void CAutoTrigger::Precache( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1f ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Checks the global state and fires targets if the global state is set. +//----------------------------------------------------------------------------- +void CAutoTrigger::Think( void ) +{ + if ( !m_globalstate || GlobalEntity_GetState( m_globalstate ) == GLOBAL_ON ) + { + m_OnTrigger.FireOutput(NULL, this); + + if ( m_spawnflags & SF_AUTO_FIREONCE ) + UTIL_Remove( this ); + } +} + + +#define SF_RELAY_FIREONCE 0x0001 + +class CTriggerRelay : public CBaseEntity +{ + DECLARE_CLASS( CTriggerRelay, CBaseEntity ); +public: + + CTriggerRelay( void ); + + bool KeyValue( const char *szKeyName, const char *szValue ); + void Spawn( void ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void RefireThink( void ); + + int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + + DECLARE_DATADESC(); + +private: + USE_TYPE triggerType; + float m_flRefireInterval; + float m_flRefireDuration; + float m_flTimeRefireDone; + + USE_TYPE m_TargetUseType; + float m_flTargetValue; + + COutputEvent m_OnTrigger; +}; +LINK_ENTITY_TO_CLASS( trigger_relay, CTriggerRelay ); + +BEGIN_DATADESC( CTriggerRelay ) + DEFINE_FIELD( triggerType, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_flRefireInterval, FIELD_FLOAT, "repeatinterval" ), + DEFINE_KEYFIELD( m_flRefireDuration, FIELD_FLOAT, "repeatduration" ), + DEFINE_FIELD( m_flTimeRefireDone, FIELD_TIME ), + DEFINE_FIELD( m_TargetUseType, FIELD_INTEGER ), + DEFINE_FIELD( m_flTargetValue, FIELD_FLOAT ), + + // Function Pointers + DEFINE_FUNCTION( RefireThink ), + + // Outputs + DEFINE_OUTPUT(m_OnTrigger, "OnTrigger"), +END_DATADESC() + +CTriggerRelay::CTriggerRelay( void ) +{ + m_flRefireInterval = -1; + m_flRefireDuration = -1; + m_flTimeRefireDone = -1; +} + +bool CTriggerRelay::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "triggerstate")) + { + int type = atoi( szValue ); + switch( type ) + { + case 0: + triggerType = USE_OFF; + break; + case 2: + triggerType = USE_TOGGLE; + break; + default: + triggerType = USE_ON; + break; + } + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + + +void CTriggerRelay::Spawn( void ) +{ +} + + +void CTriggerRelay::RefireThink( void ) +{ + // sending this as Activator and Caller right now. Seems the safest thing + // since whatever fired the relay the first time may no longer exist. + Use( this, this, m_TargetUseType, m_flTargetValue ); + + if( gpGlobals->curtime > m_flTimeRefireDone ) + { + UTIL_Remove( this ); + } + else + { + SetNextThink( gpGlobals->curtime + m_flRefireInterval ); + } +} + +void CTriggerRelay::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + m_OnTrigger.FireOutput(pActivator, this); + + if ( m_spawnflags & SF_RELAY_FIREONCE ) + { + UTIL_Remove( this ); + } + + else if( m_flRefireDuration != -1 && m_flTimeRefireDone == -1 ) + { + // Set up to refire this target automatically + m_TargetUseType = useType; + m_flTargetValue = value; + + m_flTimeRefireDone = gpGlobals->curtime + m_flRefireDuration; + SetThink( &CTriggerRelay::RefireThink ); + SetNextThink( gpGlobals->curtime + m_flRefireInterval ); + } +} + + +// The Multimanager Entity - when fired, will fire up to 16 targets +// at specified times. + +class CMultiManager : public CPointEntity +{ + DECLARE_CLASS( CMultiManager, CPointEntity ); +public: + + bool KeyValue( const char *szKeyName, const char *szValue ); + void Spawn ( void ); + void ManagerThink ( void ); + void ManagerUse ( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + +#if _DEBUG + void ManagerReport( void ); +#endif + + bool HasTarget( string_t targetname ); + + int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + + DECLARE_DATADESC(); + + int m_cTargets; // the total number of targets in this manager's fire list. + int m_index; // Current target + float m_flWait; + EHANDLE m_hActivator; + float m_startTime;// Time we started firing + string_t m_iTargetName [ MAX_MULTI_TARGETS ];// list if indexes into global string array + float m_flTargetDelay [ MAX_MULTI_TARGETS ];// delay (in seconds) from time of manager fire to target fire + + COutputEvent m_OnTrigger; + + void InputManagerTrigger( inputdata_t &data ); +}; +LINK_ENTITY_TO_CLASS( multi_manager, CMultiManager ); + +// Global Savedata for multi_manager +BEGIN_DATADESC( CMultiManager ) + DEFINE_KEYFIELD( m_flWait, FIELD_FLOAT, "wait" ), + + DEFINE_FIELD( m_cTargets, FIELD_INTEGER ), + DEFINE_FIELD( m_index, FIELD_INTEGER ), + DEFINE_FIELD( m_hActivator, FIELD_EHANDLE ), + DEFINE_FIELD( m_startTime, FIELD_TIME ), + DEFINE_ARRAY( m_iTargetName, FIELD_STRING, MAX_MULTI_TARGETS ), + DEFINE_ARRAY( m_flTargetDelay, FIELD_FLOAT, MAX_MULTI_TARGETS ), + + // Function Pointers + DEFINE_FUNCTION( ManagerThink ), + DEFINE_FUNCTION( ManagerUse ), +#if _DEBUG + DEFINE_FUNCTION( ManagerReport ), +#endif + + // Outputs + DEFINE_OUTPUT(m_OnTrigger, "OnTrigger"), + DEFINE_INPUTFUNC( FIELD_VOID, "Trigger", InputManagerTrigger ), +END_DATADESC() + +void CMultiManager::InputManagerTrigger( inputdata_t &data ) +{ + ManagerUse ( NULL, NULL, USE_TOGGLE, 0 ); +} + +bool CMultiManager::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( BaseClass::KeyValue( szKeyName, szValue ) ) + { + return true; + } + else // add this field to the target list + { + // this assumes that additional fields are targetnames and their values are delay values. + if ( m_cTargets < MAX_MULTI_TARGETS ) + { + char tmp[128]; + + UTIL_StripToken( szKeyName, tmp, Q_ARRAYSIZE( tmp ) ); + m_iTargetName [ m_cTargets ] = AllocPooledString( tmp ); + m_flTargetDelay [ m_cTargets ] = atof (szValue); + m_cTargets++; + } + else + { + return false; + } + } + + return true; +} + + +void CMultiManager::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + SetUse ( &CMultiManager::ManagerUse ); + SetThink ( &CMultiManager::ManagerThink); + + // Sort targets + // Quick and dirty bubble sort + int swapped = 1; + + while ( swapped ) + { + swapped = 0; + for ( int i = 1; i < m_cTargets; i++ ) + { + if ( m_flTargetDelay[i] < m_flTargetDelay[i-1] ) + { + // Swap out of order elements + string_t name = m_iTargetName[i]; + float delay = m_flTargetDelay[i]; + m_iTargetName[i] = m_iTargetName[i-1]; + m_flTargetDelay[i] = m_flTargetDelay[i-1]; + m_iTargetName[i-1] = name; + m_flTargetDelay[i-1] = delay; + swapped = 1; + } + } + } +} + + +bool CMultiManager::HasTarget( string_t targetname ) +{ + for ( int i = 0; i < m_cTargets; i++ ) + if ( FStrEq(STRING(targetname), STRING(m_iTargetName[i])) ) + return true; + + return false; +} + + +// Designers were using this to fire targets that may or may not exist -- +// so I changed it to use the standard target fire code, made it a little simpler. +void CMultiManager::ManagerThink ( void ) +{ + float t; + + t = gpGlobals->curtime - m_startTime; + + while ( m_index < m_cTargets && m_flTargetDelay[ m_index ] <= t ) + { + FireTargets( STRING( m_iTargetName[ m_index ] ), m_hActivator, this, USE_TOGGLE, 0 ); + m_index++; + } + + if ( m_index >= m_cTargets )// have we fired all targets? + { + SetThink( NULL ); + SetUse ( &CMultiManager::ManagerUse );// allow manager re-use + } + else + { + SetNextThink( m_startTime + m_flTargetDelay[ m_index ] ); + } +} + + +// The USE function builds the time table and starts the entity thinking. +void CMultiManager::ManagerUse ( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + m_hActivator = pActivator; + m_index = 0; + m_startTime = gpGlobals->curtime; + + m_OnTrigger.FireOutput(pActivator, this); + + // Calculate the time to re-enable the multimanager - just after the last output is fired. + // dvsents2: need to disable multimanager until last output is fired + //m_fEnableTime = gpGlobals->curtime + m_OnTrigger.GetMaxDelay(); + + SetUse( NULL );// disable use until all targets have fired + + SetThink ( &CMultiManager::ManagerThink ); + SetNextThink( gpGlobals->curtime ); +} + + +#if _DEBUG +void CMultiManager::ManagerReport ( void ) +{ + int cIndex; + + for ( cIndex = 0 ; cIndex < m_cTargets ; cIndex++ ) + { + Msg( "%s %f\n", STRING(m_iTargetName[cIndex]), m_flTargetDelay[cIndex] ); + } +} +#endif + + +// +// Pendulum +// + +#define SF_PENDULUM_SWING 2 + +LINK_ENTITY_TO_CLASS( func_pendulum, CPendulum ); + +BEGIN_DATADESC( CPendulum ) + DEFINE_FIELD( m_flAccel, FIELD_FLOAT ), + DEFINE_FIELD( m_flTime, FIELD_TIME ), + DEFINE_FIELD( m_flMaxSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_flDampSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_vCenter, FIELD_VECTOR ), + DEFINE_FIELD( m_vStart, FIELD_VECTOR ), + + DEFINE_KEYFIELD( m_flMoveDistance, FIELD_FLOAT, "pendistance" ), + DEFINE_KEYFIELD( m_flDamp, FIELD_FLOAT, "damp" ), + DEFINE_KEYFIELD( m_flBlockDamage, FIELD_FLOAT, "dmg" ), + + DEFINE_FUNCTION( PendulumUse ), + DEFINE_FUNCTION( Swing ), + DEFINE_FUNCTION( Stop ), + DEFINE_FUNCTION( RopeTouch ), + + DEFINE_FIELD( m_hEnemy, FIELD_EHANDLE ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Activate", InputActivate ), +END_DATADESC() + +void CPendulum::Spawn( void ) +{ + CBaseToggle::AxisDir(); + + m_flDamp *= 0.001; + + if ( FBitSet ( m_spawnflags, SF_DOOR_PASSABLE ) ) + SetSolid( SOLID_NONE ); + else + SetSolid( SOLID_BBOX ); + + SetMoveType( MOVETYPE_PUSH ); + SetModel( STRING(GetModelName()) ); + + if ( m_flMoveDistance != 0 ) + { + if ( m_flSpeed == 0 ) + m_flSpeed = 100; + + m_flAccel = ( m_flSpeed * m_flSpeed ) / ( 2 * fabs( m_flMoveDistance )); // Calculate constant acceleration from speed and distance + m_flMaxSpeed = m_flSpeed; + m_vStart = GetAbsAngles(); + m_vCenter = GetAbsAngles() + ( m_flMoveDistance * 0.05 ) * m_vecMoveAng; + + if ( FBitSet( m_spawnflags, SF_BRUSH_ROTATE_START_ON ) ) + { + SetThink( &CBaseEntity::SUB_CallUseToggle ); + SetNextThink( gpGlobals->curtime + 0.1f ); + } + + m_flSpeed = 0; + SetUse( &CPendulum::PendulumUse ); + + VPhysicsInitShadow( false, false ); + ///VPhysicsGetObject()->SetPosition( GetAbsOrigin(), pev->absangles ); + } + + if ( FBitSet( m_spawnflags, SF_PENDULUM_SWING ) ) + { + SetTouch ( &CPendulum::RopeTouch ); + } +} + + +void CPendulum::PendulumUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( m_flSpeed ) // Pendulum is moving, stop it and auto-return if necessary + { + if ( FBitSet( m_spawnflags, SF_BRUSH_ROTATE_START_ON ) ) + { + float delta; + + delta = CBaseToggle::AxisDelta( m_spawnflags, GetAbsAngles(), m_vStart ); + + SetLocalAngularVelocity( m_flMaxSpeed * m_vecMoveAng ); + SetNextThink( gpGlobals->curtime + delta / m_flMaxSpeed); + SetThink( &CPendulum::Stop ); + } + else + { + m_flSpeed = 0; // Dead stop + SetThink( NULL ); + SetLocalAngularVelocity( QAngle( 0, 0, 0 ) ); + } + } + else + { + SetNextThink( gpGlobals->curtime + 0.1f ); // Start the pendulum moving + m_flTime = gpGlobals->curtime; // Save time to calculate dt + SetThink( &CPendulum::Swing ); + m_flDampSpeed = m_flMaxSpeed; + } +} + +void CPendulum::InputActivate( inputdata_t &inputdata ) +{ + SetNextThink( gpGlobals->curtime + 0.1f ); // Start the pendulum moving + m_flTime = gpGlobals->curtime; // Save time to calculate dt + SetThink( &CPendulum::Swing ); + m_flDampSpeed = m_flMaxSpeed; +} + +void CPendulum::Stop( void ) +{ + SetAbsAngles( m_vStart ); + m_flSpeed = 0; + SetThink( NULL ); + SetLocalAngularVelocity( QAngle ( 0, 0, 0 ) ); +} + + +void CPendulum::Blocked( CBaseEntity *pOther ) +{ + m_flTime = gpGlobals->curtime; +} + +void CPendulum::Swing( void ) +{ + float delta, dt; + + delta = CBaseToggle::AxisDelta( m_spawnflags, GetAbsAngles(), m_vCenter ); + dt = gpGlobals->curtime - m_flTime; // How much time has passed? + m_flTime = gpGlobals->curtime; // Remember the last time called + + if ( delta > 0 && m_flAccel > 0 ) + m_flSpeed -= m_flAccel * dt; // Integrate velocity + else + m_flSpeed += m_flAccel * dt; + + if ( m_flSpeed > m_flMaxSpeed ) + m_flSpeed = m_flMaxSpeed; + else if ( m_flSpeed < -m_flMaxSpeed ) + m_flSpeed = -m_flMaxSpeed; + + // scale the destdelta vector by the time spent traveling to get velocity + SetLocalAngularVelocity( m_flSpeed * m_vecMoveAng ); + + // Call this again + SetNextThink( gpGlobals->curtime + 0.1f ); + SetMoveDoneTime( 0.1 ); + + if ( m_flDamp ) + { + m_flDampSpeed -= m_flDamp * m_flDampSpeed * dt; + if ( m_flDampSpeed < 30.0 ) + { + SetAbsAngles( m_vCenter ); + m_flSpeed = 0; + SetThink( NULL ); + SetLocalAngularVelocity( QAngle( 0, 0, 0 ) ); + } + else if ( m_flSpeed > m_flDampSpeed ) + m_flSpeed = m_flDampSpeed; + else if ( m_flSpeed < -m_flDampSpeed ) + m_flSpeed = -m_flDampSpeed; + } +} + +void CPendulum::Touch ( CBaseEntity *pOther ) +{ + if ( m_flBlockDamage <= 0 ) + return; + + // we can't hurt this thing, so we're not concerned with it + if ( !pOther->m_takedamage ) + return; + + // calculate damage based on rotation speed + float damage = m_flBlockDamage * m_flSpeed * 0.01; + + if ( damage < 0 ) + damage = -damage; + + pOther->TakeDamage( CTakeDamageInfo( this, this, damage, DMG_CRUSH ) ); + + Vector vNewVel = (pOther->GetAbsOrigin() - GetAbsOrigin()); + + VectorNormalize( vNewVel ); + + pOther->SetAbsVelocity( vNewVel * damage ); +} + +void CPendulum::RopeTouch ( CBaseEntity *pOther ) +{ + if ( !pOther->IsPlayer() ) + {// not a player! + DevMsg ( 2, "Not a client\n" ); + return; + } + + if ( pOther == GetEnemy() ) + return; + + m_hEnemy = pOther; + pOther->SetAbsVelocity( Vector ( 0, 0, 0 ) ); + pOther->SetMoveType( MOVETYPE_NONE ); +} + + +// +// MORTARS +// + +class CFuncMortarField : public CBaseToggle +{ + DECLARE_CLASS( CFuncMortarField, CBaseToggle ); +public: + void Spawn( void ); + void Precache( void ); + void KeyValue( KeyValueData *pkvd ); + + // Bmodels don't go across transitions + virtual int ObjectCaps( void ) { return CBaseToggle::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + +/* virtual int Save( CSave &save ); + virtual int Restore( CRestore &restore ); + + static TYPEDESCRIPTION m_SaveData[];*/ + + //void EXPORT FieldUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + void InputTrigger ( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + + string_t m_iszXController; + string_t m_iszYController; + float m_flSpread; + int m_iCount; + int m_fControl; +}; + +LINK_ENTITY_TO_CLASS( func_mortar_field, CFuncMortarField ); + +BEGIN_DATADESC( CFuncMortarField ) + DEFINE_KEYFIELD( m_iszXController, FIELD_STRING, "m_iszXController" ), + DEFINE_KEYFIELD( m_iszYController, FIELD_STRING, "m_iszYController" ), + DEFINE_KEYFIELD( m_flSpread, FIELD_FLOAT, "m_flSpread" ), + DEFINE_KEYFIELD( m_iCount, FIELD_INTEGER, "m_iCount" ), + DEFINE_KEYFIELD( m_fControl, FIELD_INTEGER, "m_fControl" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Trigger", InputTrigger ), +END_DATADESC() + + +// Drop bombs from above +void CFuncMortarField::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + SetModel( STRING(GetModelName()) ); // set size and link into world + SetMoveType( MOVETYPE_NONE ); + AddEffects( EF_NODRAW ); +// SetUse( FieldUse ); + Precache(); +} + + +void CFuncMortarField::Precache( void ) +{ + PrecacheModel( "sprites/lgtning.vmt" ); + + PrecacheScriptSound( "MortarField.Trigger" ); +} + +void CFuncMortarField::InputTrigger( inputdata_t &inputdata ) +{ + Vector vecStart; + CollisionProp()->RandomPointInBounds( Vector( 0, 0, 1 ), Vector( 1, 1, 1 ), &vecStart ); + + switch( m_fControl ) + { + case 0: // random + break; + case 1: // Trigger Activator + if (inputdata.pActivator != NULL) + { + vecStart.x = inputdata.pActivator->GetAbsOrigin().x; + vecStart.y = inputdata.pActivator->GetAbsOrigin().y; + } + break; + case 2: // table + { + CBaseEntity *pController; + + if ( m_iszXController != NULL_STRING ) + { + pController = gEntList.FindEntityByName( NULL, STRING(m_iszXController) ); + if (pController != NULL) + { + if ( FClassnameIs( pController, "momentary_rot_button" ) ) + { + CMomentaryRotButton *pXController = static_cast<CMomentaryRotButton*>( pController ); + Vector vecNormalizedPos( pXController->GetPos( pXController->GetLocalAngles() ), 0.0f, 0.0f ); + Vector vecWorldSpace; + CollisionProp()->NormalizedToWorldSpace( vecNormalizedPos, &vecWorldSpace ); + vecStart.x = vecWorldSpace.x; + } + else + { + DevMsg( "func_mortarfield has X controller that isn't a momentary_rot_button.\n" ); + } + } + } + if ( m_iszYController != NULL_STRING ) + { + pController = gEntList.FindEntityByName( NULL, STRING(m_iszYController) ); + if (pController != NULL) + { + if ( FClassnameIs( pController, "momentary_rot_button" ) ) + { + CMomentaryRotButton *pYController = static_cast<CMomentaryRotButton*>( pController ); + Vector vecNormalizedPos( 0.0f, pYController->GetPos( pYController->GetLocalAngles() ), 0.0f ); + Vector vecWorldSpace; + CollisionProp()->NormalizedToWorldSpace( vecNormalizedPos, &vecWorldSpace ); + vecStart.y = vecWorldSpace.y; + } + else + { + DevMsg( "func_mortarfield has Y controller that isn't a momentary_rot_button.\n" ); + } + } + } + } + break; + } + + CPASAttenuationFilter filter( this, ATTN_NONE ); + EmitSound( filter, entindex(), "MortarField.Trigger" ); + + float t = 2.5; + for (int i = 0; i < m_iCount; i++) + { + Vector vecSpot = vecStart; + vecSpot.x += random->RandomFloat( -m_flSpread, m_flSpread ); + vecSpot.y += random->RandomFloat( -m_flSpread, m_flSpread ); + + trace_t tr; + UTIL_TraceLine( vecSpot, vecSpot + Vector( 0, 0, -1 ) * MAX_TRACE_LENGTH, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); + + CBaseEntity *pMortar = Create( "monster_mortar", tr.endpos, QAngle( 0, 0, 0 ), inputdata.pActivator ); + pMortar->SetNextThink( gpGlobals->curtime + t ); + t += random->RandomFloat( 0.2, 0.5 ); + + if (i == 0) + CSoundEnt::InsertSound ( SOUND_DANGER, tr.endpos, 400, 0.3 ); + } +} + +#ifdef HL1_DLL + +class CMortar : public CHL1BaseGrenade +{ + DECLARE_CLASS( CMortar, CHL1BaseGrenade ); + +public: + void Spawn( void ); + void Precache( void ); + + void MortarExplode( void ); + + int m_spriteTexture; + + DECLARE_DATADESC(); +}; + +BEGIN_DATADESC( CMortar ) + DEFINE_THINKFUNC( MortarExplode ), + //DEFINE_FIELD( m_spriteTexture, FIELD_INTEGER ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( monster_mortar, CMortar ); + +void CMortar::Spawn( ) +{ + SetMoveType( MOVETYPE_NONE ); + SetSolid( SOLID_NONE ); + + SetDamage( 200 ); + SetDamageRadius( GetDamage() * 2.5 ); + + SetThink( &CMortar::MortarExplode ); + SetNextThink( TICK_NEVER_THINK ); + + Precache( ); +} + + +void CMortar::Precache( ) +{ + m_spriteTexture = PrecacheModel( "sprites/lgtning.vmt" ); +} + +void CMortar::MortarExplode( void ) +{ + Vector vecStart = GetAbsOrigin(); + Vector vecEnd = vecStart; + vecEnd.z += 1024; + + UTIL_Beam( vecStart, vecEnd, m_spriteTexture, 0, 0, 0, 0.5, 4.0, 4.0, 100, 0, 255, 160, 100, 128, 0 ); + + trace_t tr; + UTIL_TraceLine( GetAbsOrigin() + Vector( 0, 0, 1024 ), GetAbsOrigin() - Vector( 0, 0, 1024 ), MASK_ALL, this, COLLISION_GROUP_NONE, &tr ); + + + Explode( &tr, DMG_BLAST | DMG_MISSILEDEFENSE ); + UTIL_ScreenShake( tr.endpos, 25.0, 150.0, 1.0, 750, SHAKE_START ); +} + +#endif + +//========================================================= +// Dead HEV suit prop +//========================================================= +class CNPC_DeadHEV : public CAI_BaseNPC +{ + DECLARE_CLASS( CNPC_DeadHEV, CAI_BaseNPC ); +public: + void Spawn( void ); + Class_T Classify ( void ) { return CLASS_NONE; } + float MaxYawSpeed( void ) { return 8.0f; } + + bool KeyValue( const char *szKeyName, const char *szValue ); + + int m_iPose;// which sequence to display -- temporary, don't need to save + static char *m_szPoses[4]; +}; + +char *CNPC_DeadHEV::m_szPoses[] = { "deadback", "deadsitting", "deadstomach", "deadtable" }; + +bool CNPC_DeadHEV::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "pose" ) ) + m_iPose = atoi( szValue ); + else + CAI_BaseNPC::KeyValue( szKeyName, szValue ); + + return true; +} + +LINK_ENTITY_TO_CLASS( monster_hevsuit_dead, CNPC_DeadHEV ); + +//========================================================= +// ********** DeadHEV SPAWN ********** +//========================================================= +void CNPC_DeadHEV::Spawn( void ) +{ + PrecacheModel("models/player.mdl"); + SetModel( "models/player.mdl" ); + + ClearEffects(); + SetSequence( 0 ); + m_nBody = 1; + m_bloodColor = BLOOD_COLOR_RED; + + SetSequence( LookupSequence( m_szPoses[m_iPose] ) ); + + if ( GetSequence() == -1 ) + { + Msg ( "Dead hevsuit with bad pose\n" ); + SetSequence( 0 ); + ClearEffects(); + AddEffects( EF_BRIGHTLIGHT ); + } + + // Corpses have less health + m_iHealth = 8; + + NPCInitDead(); +} + + +// +// Render parameters trigger +// +// This entity will copy its render parameters (renderfx, rendermode, rendercolor, renderamt) +// to its targets when triggered. +// + + +// Flags to indicate masking off various render parameters that are normally copied to the targets +#define SF_RENDER_MASKFX (1<<0) +#define SF_RENDER_MASKAMT (1<<1) +#define SF_RENDER_MASKMODE (1<<2) +#define SF_RENDER_MASKCOLOR (1<<3) + +class CRenderFxManager : public CBaseEntity +{ + DECLARE_CLASS( CRenderFxManager, CBaseEntity ); +public: + void Spawn( void ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + // Input handlers. + void InputActivate( inputdata_t &inputdata ); + + DECLARE_DATADESC(); +}; + +BEGIN_DATADESC( CRenderFxManager ) + DEFINE_INPUTFUNC( FIELD_VOID, "Activate", InputActivate ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( env_render, CRenderFxManager ); + + +void CRenderFxManager::Spawn( void ) +{ + AddSolidFlags( FSOLID_NOT_SOLID ); + SetMoveType( MOVETYPE_NONE ); + AddEffects( EF_NODRAW ); +} + + +void CRenderFxManager::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( m_target != NULL_STRING ) + { + CBaseEntity *pEntity = NULL; + while ( ( pEntity = gEntList.FindEntityByName( pEntity, STRING( m_target ) ) ) != NULL ) + { + if ( !HasSpawnFlags( SF_RENDER_MASKFX ) ) + pEntity->m_nRenderFX = m_nRenderFX; + if ( !HasSpawnFlags( SF_RENDER_MASKAMT ) ) + pEntity->SetRenderColorA( GetRenderColor().a ); + if ( !HasSpawnFlags( SF_RENDER_MASKMODE ) ) + pEntity->m_nRenderMode = m_nRenderMode; + if ( !HasSpawnFlags( SF_RENDER_MASKCOLOR ) ) + pEntity->m_clrRender = m_clrRender; + } + } +} + + +void CRenderFxManager::InputActivate( inputdata_t &inputdata ) +{ + Use( inputdata.pActivator, inputdata.pCaller, USE_ON, 0 ); +} + + +// Link env_sound to soundscape system +LINK_ENTITY_TO_CLASS( env_sound, CEnvSoundscape ); + + +/////////////////////// +//XEN! +////////////////////// + + +#define XEN_PLANT_GLOW_SPRITE "sprites/flare3.spr" +#define XEN_PLANT_HIDE_TIME 5 + +class CXenPLight : public CActAnimating +{ + DECLARE_CLASS( CXenPLight, CActAnimating ); + +public: + + DECLARE_DATADESC(); + + void Spawn( void ); + void Precache( void ); + void Touch( CBaseEntity *pOther ); + void Think( void ); + + void LightOn( void ); + void LightOff( void ); + + float m_flDmgTime; + + +private: + CSprite *m_pGlow; +}; + +LINK_ENTITY_TO_CLASS( xen_plantlight, CXenPLight ); + +BEGIN_DATADESC( CXenPLight ) + DEFINE_FIELD( m_pGlow, FIELD_CLASSPTR ), + DEFINE_FIELD( m_flDmgTime, FIELD_FLOAT ), +END_DATADESC() + +void CXenPLight::Spawn( void ) +{ + Precache(); + + SetModel( "models/light.mdl" ); + + SetMoveType( MOVETYPE_NONE ); + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_TRIGGER | FSOLID_NOT_SOLID ); + + UTIL_SetSize( this, Vector(-80,-80,0), Vector(80,80,32)); + SetActivity( ACT_IDLE ); + SetNextThink( gpGlobals->curtime + 0.1 ); + SetCycle( random->RandomFloat(0,1) ); + + m_pGlow = CSprite::SpriteCreate( XEN_PLANT_GLOW_SPRITE, GetLocalOrigin() + Vector(0,0,(WorldAlignMins().z+WorldAlignMaxs().z)*0.5), FALSE ); + m_pGlow->SetTransparency( kRenderGlow, GetRenderColor().r, GetRenderColor().g, GetRenderColor().b, GetRenderColor().a, m_nRenderFX ); + m_pGlow->SetAttachment( this, 1 ); +} + + +void CXenPLight::Precache( void ) +{ + PrecacheModel( "models/light.mdl" ); + PrecacheModel( XEN_PLANT_GLOW_SPRITE ); +} + + +void CXenPLight::Think( void ) +{ + StudioFrameAdvance(); + SetNextThink( gpGlobals->curtime + 0.1 ); + + switch( GetActivity() ) + { + case ACT_CROUCH: + if ( IsSequenceFinished() ) + { + SetActivity( ACT_CROUCHIDLE ); + LightOff(); + } + break; + + case ACT_CROUCHIDLE: + if ( gpGlobals->curtime > m_flDmgTime ) + { + SetActivity( ACT_STAND ); + LightOn(); + } + break; + + case ACT_STAND: + if ( IsSequenceFinished() ) + SetActivity( ACT_IDLE ); + break; + + case ACT_IDLE: + default: + break; + } +} + + +void CXenPLight::Touch( CBaseEntity *pOther ) +{ + if ( pOther->IsPlayer() ) + { + m_flDmgTime = gpGlobals->curtime + XEN_PLANT_HIDE_TIME; + if ( GetActivity() == ACT_IDLE || GetActivity() == ACT_STAND ) + { + SetActivity( ACT_CROUCH ); + } + } +} + + +void CXenPLight::LightOn( void ) +{ + variant_t Value; + g_EventQueue.AddEvent( STRING( m_target ), "TurnOn", Value, 0, this, this ); + + if ( m_pGlow ) + m_pGlow->RemoveEffects( EF_NODRAW ); +} + + +void CXenPLight::LightOff( void ) +{ + variant_t Value; + g_EventQueue.AddEvent( STRING( m_target ), "TurnOff", Value, 0, this, this ); + + if ( m_pGlow ) + m_pGlow->AddEffects( EF_NODRAW ); +} + + +class CXenHair : public CActAnimating +{ + DECLARE_CLASS( CXenHair, CActAnimating ); +public: + void Spawn( void ); + void Precache( void ); + void Think( void ); +}; + +LINK_ENTITY_TO_CLASS( xen_hair, CXenHair ); + +#define SF_HAIR_SYNC 0x0001 + +void CXenHair::Spawn( void ) +{ + Precache(); + SetModel( "models/hair.mdl" ); + UTIL_SetSize( this, Vector(-4,-4,0), Vector(4,4,32)); + SetSequence( 0 ); + + if ( !HasSpawnFlags( SF_HAIR_SYNC ) ) + { + SetCycle( random->RandomFloat( 0,1) ); + m_flPlaybackRate = random->RandomFloat( 0.7, 1.4 ); + } + ResetSequenceInfo( ); + + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); + SetNextThink( gpGlobals->curtime + random->RandomFloat( 0.1, 0.4 ) ); // Load balance these a bit +} + + +void CXenHair::Think( void ) +{ + StudioFrameAdvance(); + SetNextThink( gpGlobals->curtime + 0.1 ); +} + + +void CXenHair::Precache( void ) +{ + PrecacheModel( "models/hair.mdl" ); +} + +class CXenTreeTrigger : public CBaseEntity +{ + DECLARE_CLASS( CXenTreeTrigger, CBaseEntity ); +public: + void Touch( CBaseEntity *pOther ); + static CXenTreeTrigger *TriggerCreate( CBaseEntity *pOwner, const Vector &position ); +}; +LINK_ENTITY_TO_CLASS( xen_ttrigger, CXenTreeTrigger ); + +CXenTreeTrigger *CXenTreeTrigger::TriggerCreate( CBaseEntity *pOwner, const Vector &position ) +{ + CXenTreeTrigger *pTrigger = CREATE_ENTITY( CXenTreeTrigger, "xen_ttrigger" ); + pTrigger->SetAbsOrigin( position ); + + pTrigger->SetSolid( SOLID_BBOX ); + pTrigger->AddSolidFlags( FSOLID_TRIGGER | FSOLID_NOT_SOLID ); + pTrigger->SetMoveType( MOVETYPE_NONE ); + pTrigger->SetOwnerEntity( pOwner ); + + return pTrigger; +} + + +void CXenTreeTrigger::Touch( CBaseEntity *pOther ) +{ + if ( GetOwnerEntity() ) + { + GetOwnerEntity()->Touch( pOther ); + } +} + +#define TREE_AE_ATTACK 1 + +class CXenTree : public CActAnimating +{ + DECLARE_CLASS( CXenTree, CActAnimating ); +public: + void Spawn( void ); + void Precache( void ); + void Touch( CBaseEntity *pOther ); + void Think( void ); + int OnTakeDamage( const CTakeDamageInfo &info ) { Attack(); return 0; } + void HandleAnimEvent( animevent_t *pEvent ); + void Attack( void ); + Class_T Classify( void ) { return CLASS_ALIEN_PREDATOR; } + + DECLARE_DATADESC(); + +private: + CXenTreeTrigger *m_pTrigger; +}; + +LINK_ENTITY_TO_CLASS( xen_tree, CXenTree ); + +BEGIN_DATADESC( CXenTree ) + DEFINE_FIELD( m_pTrigger, FIELD_CLASSPTR ), +END_DATADESC() + +void CXenTree::Spawn( void ) +{ + Precache(); + + SetModel( "models/tree.mdl" ); + SetMoveType( MOVETYPE_NONE ); + SetSolid ( SOLID_BBOX ); + + m_takedamage = DAMAGE_YES; + + UTIL_SetSize( this, Vector(-30,-30,0), Vector(30,30,188)); + SetActivity( ACT_IDLE ); + SetNextThink( gpGlobals->curtime + 0.1 ); + SetCycle( random->RandomFloat( 0,1 ) ); + m_flPlaybackRate = random->RandomFloat( 0.7, 1.4 ); + + Vector triggerPosition, vForward; + + AngleVectors( GetAbsAngles(), &vForward ); + triggerPosition = GetAbsOrigin() + (vForward * 64); + + // Create the trigger + m_pTrigger = CXenTreeTrigger::TriggerCreate( this, triggerPosition ); + UTIL_SetSize( m_pTrigger, Vector( -24, -24, 0 ), Vector( 24, 24, 128 ) ); +} + +void CXenTree::Precache( void ) +{ + PrecacheModel( "models/tree.mdl" ); + PrecacheModel( XEN_PLANT_GLOW_SPRITE ); + + PrecacheScriptSound( "XenTree.AttackMiss" ); + PrecacheScriptSound( "XenTree.AttackHit" ); +} + + +void CXenTree::Touch( CBaseEntity *pOther ) +{ + if ( !pOther->IsPlayer() && FClassnameIs( pOther, "monster_bigmomma" ) ) + return; + + Attack(); +} + + +void CXenTree::Attack( void ) +{ + if ( GetActivity() == ACT_IDLE ) + { + SetActivity( ACT_MELEE_ATTACK1 ); + m_flPlaybackRate = random->RandomFloat( 1.0, 1.4 ); + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "XenTree.AttackMiss" ); + } +} + + +void CXenTree::HandleAnimEvent( animevent_t *pEvent ) +{ + switch( pEvent->event ) + { + case TREE_AE_ATTACK: + { + CBaseEntity *pList[8]; + BOOL sound = FALSE; + int count = UTIL_EntitiesInBox( pList, 8, m_pTrigger->GetAbsOrigin() + m_pTrigger->WorldAlignMins(), m_pTrigger->GetAbsOrigin() + m_pTrigger->WorldAlignMaxs(), FL_NPC|FL_CLIENT ); + + Vector forward; + AngleVectors( GetAbsAngles(), &forward ); + + for ( int i = 0; i < count; i++ ) + { + if ( pList[i] != this ) + { + if ( pList[i]->GetOwnerEntity() != this ) + { + sound = true; + pList[i]->TakeDamage( CTakeDamageInfo(this, this, 25, DMG_CRUSH | DMG_SLASH ) ); + pList[i]->ViewPunch( QAngle( 15, 0, 18 ) ); + + pList[i]->SetAbsVelocity( pList[i]->GetAbsVelocity() + forward * 100 ); + } + } + } + + if ( sound ) + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "XenTree.AttackHit" ); + } + } + return; + } + + BaseClass::HandleAnimEvent( pEvent ); +} + +void CXenTree::Think( void ) +{ + StudioFrameAdvance(); + SetNextThink( gpGlobals->curtime + 0.1 ); + DispatchAnimEvents( this ); + + switch( GetActivity() ) + { + case ACT_MELEE_ATTACK1: + if ( IsSequenceFinished() ) + { + SetActivity( ACT_IDLE ); + m_flPlaybackRate = random->RandomFloat( 0.6f, 1.4f ); + } + break; + + default: + case ACT_IDLE: + break; + + } +} + +class CXenSpore : public CActAnimating +{ + DECLARE_CLASS( CXenSpore, CActAnimating ); +public: + void Spawn( void ); + void Precache( void ); + void Touch( CBaseEntity *pOther ); +// void HandleAnimEvent( MonsterEvent_t *pEvent ); + void Attack( void ) {} + + static const char *pModelNames[]; +}; + +class CXenSporeSmall : public CXenSpore +{ + DECLARE_CLASS( CXenSporeSmall, CXenSpore ); + void Spawn( void ); +}; + +class CXenSporeMed : public CXenSpore +{ + DECLARE_CLASS( CXenSporeMed, CXenSpore ); + void Spawn( void ); +}; + +class CXenSporeLarge : public CXenSpore +{ + DECLARE_CLASS( CXenSporeLarge, CXenSpore ); + void Spawn( void ); + + static const Vector m_hullSizes[]; +}; + +// Fake collision box for big spores +class CXenHull : public CPointEntity +{ + DECLARE_CLASS( CXenHull, CPointEntity ); +public: + static CXenHull *CreateHull( CBaseEntity *source, const Vector &mins, const Vector &maxs, const Vector &offset ); + Class_T Classify( void ) { return CLASS_ALIEN_PREDATOR; } +}; + +CXenHull *CXenHull::CreateHull( CBaseEntity *source, const Vector &mins, const Vector &maxs, const Vector &offset ) +{ + CXenHull *pHull = CREATE_ENTITY( CXenHull, "xen_hull" ); + + UTIL_SetOrigin( pHull, source->GetAbsOrigin() + offset ); + pHull->SetSolid( SOLID_BBOX ); + pHull->SetMoveType( MOVETYPE_NONE ); + pHull->SetOwnerEntity( source ); + UTIL_SetSize( pHull, mins, maxs ); + pHull->SetRenderColorA( 0 ); + pHull->m_nRenderMode = kRenderTransTexture; + return pHull; +} + + +LINK_ENTITY_TO_CLASS( xen_spore_small, CXenSporeSmall ); +LINK_ENTITY_TO_CLASS( xen_spore_medium, CXenSporeMed ); +LINK_ENTITY_TO_CLASS( xen_spore_large, CXenSporeLarge ); +LINK_ENTITY_TO_CLASS( xen_hull, CXenHull ); + +void CXenSporeSmall::Spawn( void ) +{ + m_nSkin = 0; + CXenSpore::Spawn(); + UTIL_SetSize( this, Vector(-16,-16,0), Vector(16,16,64)); +} +void CXenSporeMed::Spawn( void ) +{ + m_nSkin = 1; + CXenSpore::Spawn(); + UTIL_SetSize( this, Vector(-40,-40,0), Vector(40,40,120)); +} + + +// I just eyeballed these -- fill in hulls for the legs +const Vector CXenSporeLarge::m_hullSizes[] = +{ + Vector( 90, -25, 0 ), + Vector( 25, 75, 0 ), + Vector( -15, -100, 0 ), + Vector( -90, -35, 0 ), + Vector( -90, 60, 0 ), +}; + +void CXenSporeLarge::Spawn( void ) +{ + m_nSkin = 2; + CXenSpore::Spawn(); + UTIL_SetSize( this, Vector(-48,-48,110), Vector(48,48,240)); + + Vector forward, right; + + AngleVectors( GetAbsAngles(), &forward, &right, NULL ); + + // Rotate the leg hulls into position + for ( int i = 0; i < ARRAYSIZE(m_hullSizes); i++ ) + { + CXenHull::CreateHull( this, Vector(-12, -12, 0 ), Vector( 12, 12, 120 ), (m_hullSizes[i].x * forward) + (m_hullSizes[i].y * right) ); + } +} + +void CXenSpore::Spawn( void ) +{ + Precache(); + + SetModel( pModelNames[m_nSkin] ); + SetMoveType( MOVETYPE_NONE ); + SetSolid( SOLID_BBOX ); + m_takedamage = DAMAGE_NO; + +// SetActivity( ACT_IDLE ); + SetSequence( 0 ); + SetCycle( random->RandomFloat( 0.0f, 1.0f ) ); + m_flPlaybackRate = random->RandomFloat( 0.7f, 1.4f ); + ResetSequenceInfo( ); + SetNextThink( gpGlobals->curtime + random->RandomFloat( 0.1f, 0.4f ) ); // Load balance these a bit +} + +const char *CXenSpore::pModelNames[] = +{ + "models/fungus(small).mdl", + "models/fungus.mdl", + "models/fungus(large).mdl", +}; + + +void CXenSpore::Precache( void ) +{ + PrecacheModel( (char *)pModelNames[m_nSkin] ); +} + + +void CXenSpore::Touch( CBaseEntity *pOther ) +{ +} + + + +//========================================================= +// WaitTillLand - in order to emit their meaty scent from +// the proper location, gibs should wait until they stop +// bouncing to emit their scent. That's what this function +// does. +//========================================================= +void CHL1Gib::WaitTillLand ( void ) +{ + if ( !IsInWorld() ) + { + UTIL_Remove( this ); + return; + } + + if ( GetAbsVelocity() == vec3_origin ) + { + /* SetRenderColorA( 255 ); + m_nRenderMode = kRenderTransTexture; + AddSolidFlags( FSOLID_NOT_SOLID );*/ + + SetNextThink( gpGlobals->curtime + m_lifeTime ); + SetThink ( &CBaseEntity::SUB_FadeOut ); + + // If you bleed, you stink! + /* if ( m_bloodColor != DONT_BLEED ) + { + // ok, start stinkin! + CSoundEnt::InsertSound ( bits_SOUND_MEAT, pev->origin, 384, 25 ); + }*/ + } + else + { + // wait and check again in another half second. + SetNextThink( gpGlobals->curtime + 0.5 ); + } +} + +// +// Gib bounces on the ground or wall, sponges some blood down, too! +// +void CHL1Gib::BounceGibTouch ( CBaseEntity *pOther ) +{ + Vector vecSpot; + trace_t tr; + + if ( GetFlags() & FL_ONGROUND) + { + SetAbsVelocity( GetAbsVelocity() * 0.9 ); + + SetAbsAngles( QAngle( 0, GetAbsAngles().y, 0 ) ); + SetLocalAngularVelocity( QAngle( 0, GetLocalAngularVelocity().y, 0 ) ); + } + else + { + if ( m_cBloodDecals > 0 && m_bloodColor != DONT_BLEED ) + { + vecSpot = GetAbsOrigin() + Vector ( 0 , 0 , 8 );//move up a bit, and trace down. + UTIL_TraceLine ( vecSpot, vecSpot + Vector ( 0, 0, -24 ), MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr); + + UTIL_BloodDecalTrace( &tr, m_bloodColor ); + + m_cBloodDecals--; + } + + if ( m_material != matNone && random->RandomInt( 0, 2 ) == 0 ) + { + float volume; + float zvel = fabs( GetAbsVelocity().z ); + + volume = 0.8 * MIN(1.0, ((float)zvel) / 450.0); + + CBreakable::MaterialSoundRandom( entindex(), (Materials)m_material, volume ); + } + } +} + +// +// Sticky gib puts blood on the wall and stays put. +// +void CHL1Gib::StickyGibTouch ( CBaseEntity *pOther ) +{ + Vector vecSpot; + trace_t tr; + + SetThink ( &CHL1Gib::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 10 ); + + if ( !FClassnameIs( pOther, "worldspawn" ) ) + { + SetNextThink( gpGlobals->curtime ); + return; + } + + UTIL_TraceLine ( GetAbsOrigin(), GetAbsOrigin() + GetAbsVelocity() * 32, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); + + UTIL_BloodDecalTrace( &tr, m_bloodColor ); + + SetAbsVelocity( tr.plane.normal * -1 ); + + QAngle qAngle; + + VectorAngles( GetAbsVelocity(), qAngle ); + SetAbsAngles( qAngle ); + + SetAbsVelocity( vec3_origin ); + SetLocalAngularVelocity( QAngle( 0, 0, 0 ) ); + SetMoveType( MOVETYPE_NONE ); +} + +// +// Throw a chunk +// +void CHL1Gib::Spawn( const char *szGibModel ) +{ + SetMoveType( MOVETYPE_FLYGRAVITY, MOVECOLLIDE_FLY_BOUNCE ); + + SetFriction( 0.55 ); // deading the bounce a bit + + // sometimes an entity inherits the edict from a former piece of glass, + // and will spawn using the same render FX or rendermode! bad! + SetRenderColorA( 255 ); + m_nRenderMode = kRenderNormal; + m_nRenderFX = kRenderFxNone; + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetClassname( "gib" ); + + SetModel( szGibModel ); + UTIL_SetSize( this, Vector( 0, 0, 0), Vector(0, 0, 0)); + + SetNextThink( gpGlobals->curtime + 4 ); + + m_lifeTime = 25; + + SetThink ( &CHL1Gib::WaitTillLand ); + SetTouch ( &CHL1Gib::BounceGibTouch ); + + m_material = matNone; + m_cBloodDecals = 5;// how many blood decals this gib can place (1 per bounce until none remain). +} + +LINK_ENTITY_TO_CLASS( hl1gib, CHL1Gib ); + +BEGIN_DATADESC( CHL1Gib ) + // Function Pointers + DEFINE_FUNCTION( BounceGibTouch ), + DEFINE_FUNCTION( StickyGibTouch ), + DEFINE_FUNCTION( WaitTillLand ), + + DEFINE_FIELD( m_bloodColor, FIELD_INTEGER ), + DEFINE_FIELD( m_cBloodDecals, FIELD_INTEGER ), + DEFINE_FIELD( m_material, FIELD_INTEGER ), + DEFINE_FIELD( m_lifeTime, FIELD_FLOAT ), +END_DATADESC() + +#define SF_ENDSECTION_USEONLY 0x0001 + +class CTriggerEndSection : public CBaseEntity +{ + DECLARE_CLASS( CTriggerEndSection, CBaseEntity ); + +public: + void Spawn( void ); + void InputEndSection( inputdata_t &data ); + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( trigger_endsection, CTriggerEndSection ); + +BEGIN_DATADESC( CTriggerEndSection ) + DEFINE_INPUTFUNC( FIELD_VOID, "EndSection", InputEndSection ), +END_DATADESC() + +void CTriggerEndSection::Spawn( void ) +{ + if ( gpGlobals->deathmatch ) + { + UTIL_Remove( this ); + return; + } +} + +void CTriggerEndSection::InputEndSection( inputdata_t &data ) +{ + CBaseEntity *pPlayer = UTIL_GetLocalPlayer(); + + if ( pPlayer ) + { + //HACKY MCHACK - This works, but it's nasty. Alfred is going to fix a + //bug in gameui that prevents you from dropping to the main menu after + // calling disconnect. + engine->ClientCommand ( pPlayer->edict(), "toggleconsole;disconnect\n"); + } + + UTIL_Remove( this ); +} diff --git a/game/server/hl1/hl1_ents.h b/game/server/hl1/hl1_ents.h new file mode 100644 index 0000000..a5eb6fe --- /dev/null +++ b/game/server/hl1/hl1_ents.h @@ -0,0 +1,73 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef HL1_ENTS_H +#define HL1_ENTS_H +#ifdef _WIN32 +#pragma once +#endif + + +/********************** + Pendulum +/**********************/ + +class CPendulum : public CBaseToggle +{ + DECLARE_CLASS( CPendulum, CBaseToggle ); +public: + void Spawn ( void ); + void KeyValue( KeyValueData *pkvd ); + void Swing( void ); + void PendulumUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void Stop( void ); + void Touch( CBaseEntity *pOther ); + void RopeTouch ( CBaseEntity *pOther );// this touch func makes the pendulum a rope + void Blocked( CBaseEntity *pOther ); + + // Input handlers. + void InputActivate( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + + float m_flAccel; // Acceleration + float m_flTime; + float m_flDamp; + float m_flMaxSpeed; + float m_flDampSpeed; + QAngle m_vCenter; + QAngle m_vStart; + float m_flBlockDamage; + + EHANDLE m_hEnemy; +}; + +class CHL1Gib : public CBaseEntity +{ + DECLARE_CLASS( CHL1Gib, CBaseEntity ); + +public: + void Spawn( const char *szGibModel ); + void BounceGibTouch ( CBaseEntity *pOther ); + void StickyGibTouch ( CBaseEntity *pOther ); + void WaitTillLand( void ); + void LimitVelocity( void ); + + virtual int ObjectCaps( void ) { return (CBaseEntity::ObjectCaps() & ~FCAP_ACROSS_TRANSITION) | FCAP_DONT_SAVE; } + static void SpawnHeadGib( CBaseEntity *pVictim ); + static void SpawnRandomGibs( CBaseEntity *pVictim, int cGibs, int human ); + static void SpawnStickyGibs( CBaseEntity *pVictim, Vector vecOrigin, int cGibs ); + + int m_bloodColor; + int m_cBloodDecals; + int m_material; + float m_lifeTime; + + DECLARE_DATADESC(); +}; + + +#endif // HL1_ENTS_H diff --git a/game/server/hl1/hl1_env_speaker.cpp b/game/server/hl1/hl1_env_speaker.cpp new file mode 100644 index 0000000..97bf22c --- /dev/null +++ b/game/server/hl1/hl1_env_speaker.cpp @@ -0,0 +1,219 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#include "cbase.h" +#include "player.h" +#include "mathlib/mathlib.h" +#include "ai_speech.h" +#include "stringregistry.h" +#include "gamerules.h" +#include "game.h" +#include <ctype.h> +#include "entitylist.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ndebugoverlay.h" +#include "soundscape.h" + +#define SPEAKER_START_SILENT 1 // wait for trigger 'on' to start announcements + +// =================================================================================== +// +// Speaker class. Used for announcements per level, for door lock/unlock spoken voice. +// + +class CSpeaker : public CPointEntity +{ + DECLARE_CLASS( CSpeaker, CPointEntity ); +public: + bool KeyValue( const char *szKeyName, const char *szValue ); + void Spawn( void ); + void Precache( void ); + void ToggleUse ( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void SpeakerThink( void ); + + virtual int ObjectCaps( void ) { return (CBaseEntity::ObjectCaps() & ~FCAP_ACROSS_TRANSITION); } + + int m_preset; // preset number + string_t m_iszMessage; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( speaker, CSpeaker ); + +BEGIN_DATADESC( CSpeaker ) + DEFINE_FIELD( m_preset, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_iszMessage, FIELD_STRING, "message" ), + DEFINE_THINKFUNC( SpeakerThink ), + DEFINE_USEFUNC( ToggleUse ), +END_DATADESC() + +// +// ambient_generic - general-purpose user-defined static sound +// +void CSpeaker::Spawn( void ) +{ + char* szSoundFile = (char*) STRING( m_iszMessage ); + + if ( !m_preset && ( m_iszMessage == NULL_STRING || strlen( szSoundFile ) < 1 ) ) + { + Msg( "SPEAKER with no Level/Sentence! at: %f, %f, %f\n", GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z ); + SetNextThink( gpGlobals->curtime + 0.1 ); + SetThink( &CSpeaker::SUB_Remove ); + return; + } + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); + + + SetThink(&CSpeaker::SpeakerThink); + SetNextThink( TICK_NEVER_THINK ); + + // allow on/off switching via 'use' function. + SetUse ( &CSpeaker::ToggleUse ); + + Precache( ); +} + +#define ANNOUNCE_MINUTES_MIN 0.25 +#define ANNOUNCE_MINUTES_MAX 2.25 + +void CSpeaker::Precache( void ) +{ + if ( !FBitSet ( GetSpawnFlags(), SPEAKER_START_SILENT ) ) + // set first announcement time for random n second + SetNextThink( gpGlobals->curtime + random->RandomFloat( 5.0, 15.0 ) ); +} +void CSpeaker::SpeakerThink( void ) +{ + char* szSoundFile = NULL; + float flvolume = m_iHealth * 0.1; + int flags = 0; + int pitch = 100; + + + // Wait for the talking characters to finish first. + if ( !g_AIFriendliesTalkSemaphore.IsAvailable( this ) || !g_AIFoesTalkSemaphore.IsAvailable( this ) ) + { + float releaseTime = MAX( g_AIFriendliesTalkSemaphore.GetReleaseTime(), g_AIFoesTalkSemaphore.GetReleaseTime() ); + SetNextThink( gpGlobals->curtime + releaseTime + random->RandomFloat( 5, 10 ) ); + return; + } + + if (m_preset) + { + // go lookup preset text, assign szSoundFile + switch (m_preset) + { + case 1: szSoundFile = "C1A0_"; break; + case 2: szSoundFile = "C1A1_"; break; + case 3: szSoundFile = "C1A2_"; break; + case 4: szSoundFile = "C1A3_"; break; + case 5: szSoundFile = "C1A4_"; break; + case 6: szSoundFile = "C2A1_"; break; + case 7: szSoundFile = "C2A2_"; break; + case 8: szSoundFile = "C2A3_"; break; + case 9: szSoundFile = "C2A4_"; break; + case 10: szSoundFile = "C2A5_"; break; + case 11: szSoundFile = "C3A1_"; break; + case 12: szSoundFile = "C3A2_"; break; + } + } else + szSoundFile = (char*) STRING( m_iszMessage ); + + if (szSoundFile[0] == '!') + { + // play single sentence, one shot + UTIL_EmitAmbientSound ( GetSoundSourceIndex(), GetAbsOrigin(), szSoundFile, + flvolume, SNDLVL_120dB, flags, pitch); + + // shut off and reset + SetNextThink( TICK_NEVER_THINK ); + } + else + { + // make random announcement from sentence group + + if ( SENTENCEG_PlayRndSz( edict(), szSoundFile, flvolume, SNDLVL_120dB, flags, pitch) < 0 ) + Msg( "Level Design Error!\nSPEAKER has bad sentence group name: %s\n",szSoundFile); + + // set next announcement time for random 5 to 10 minute delay + SetNextThink ( gpGlobals->curtime + + random->RandomFloat( ANNOUNCE_MINUTES_MIN * 60.0, ANNOUNCE_MINUTES_MAX * 60.0 ) ); + + // time delay until it's ok to speak: used so that two NPCs don't talk at once + g_AIFriendliesTalkSemaphore.Acquire( 5, this ); + g_AIFoesTalkSemaphore.Acquire( 5, this ); + } + + return; +} + + +// +// ToggleUse - if an announcement is pending, cancel it. If no announcement is pending, start one. +// +void CSpeaker::ToggleUse ( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + int fActive = (GetNextThink() > 0.0); + + // fActive is TRUE only if an announcement is pending + + if ( useType != USE_TOGGLE ) + { + // ignore if we're just turning something on that's already on, or + // turning something off that's already off. + if ( (fActive && useType == USE_ON) || (!fActive && useType == USE_OFF) ) + return; + } + + if ( useType == USE_ON ) + { + // turn on announcements + SetNextThink( gpGlobals->curtime + 0.1 ); + return; + } + + if ( useType == USE_OFF ) + { + // turn off announcements + SetNextThink( TICK_NEVER_THINK ); + return; + + } + + // Toggle announcements + + + if ( fActive ) + { + // turn off announcements + SetNextThink( TICK_NEVER_THINK ); + } + else + { + // turn on announcements + SetNextThink( gpGlobals->curtime + 0.1 ); + } +} + +// KeyValue - load keyvalue pairs into member data +// NOTE: called BEFORE spawn! + +bool CSpeaker::KeyValue( const char *szKeyName, const char *szValue ) +{ + // preset + if (FStrEq(szKeyName, "preset")) + { + m_preset = atoi(szValue); + return true; + } + else + return BaseClass::KeyValue( szKeyName, szValue ); +} diff --git a/game/server/hl1/hl1_eventlog.cpp b/game/server/hl1/hl1_eventlog.cpp new file mode 100644 index 0000000..4c33d64 --- /dev/null +++ b/game/server/hl1/hl1_eventlog.cpp @@ -0,0 +1,55 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "../EventLog.h" + +class CHL1EventLog : public CEventLog +{ +private: + typedef CEventLog BaseClass; + +public: + virtual ~CHL1EventLog() {}; + +public: + bool PrintEvent( IGameEvent * event ) // override virtual function + { + if ( BaseClass::PrintEvent( event ) ) + { + return true; + } + + if ( Q_strcmp(event->GetName(), "hl1_") == 0 ) + { + return PrintHL1Event( event ); + } + + return false; + } + +protected: + + bool PrintHL1Event( IGameEvent * event ) // print Mod specific logs + { + // const char * name = event->GetName() + Q_strlen("hl1_"); // remove prefix + + return false; + } + +}; + +CHL1EventLog g_HL1EventLog; + +//----------------------------------------------------------------------------- +// Singleton access +//----------------------------------------------------------------------------- +IGameSystem* GameLogSystem() +{ + return &g_HL1EventLog; +} + diff --git a/game/server/hl1/hl1_func_recharge.cpp b/game/server/hl1/hl1_func_recharge.cpp new file mode 100644 index 0000000..d27b5bf --- /dev/null +++ b/game/server/hl1/hl1_func_recharge.cpp @@ -0,0 +1,239 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +/* + +===== h_battery.cpp ======================================================== + + battery-related code + +*/ + +#include "cbase.h" +#include "gamerules.h" +#include "player.h" +#include "engine/IEngineSound.h" +#include "in_buttons.h" + +ConVar sk_suitcharger( "sk_suitcharger","0" ); +#define HL1_MAX_ARMOR 100 + +class CRecharge : public CBaseToggle +{ +public: + DECLARE_CLASS( CRecharge, CBaseToggle ); + + void Spawn( ); + + virtual void Precache(); + + bool CreateVPhysics(); + void Off(void); + void Recharge(void); + bool KeyValue( const char *szKeyName, const char *szValue ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + virtual int ObjectCaps( void ) { return (BaseClass::ObjectCaps() | m_iCaps ); } + + DECLARE_DATADESC(); + + float m_flNextCharge; + int m_iReactivate ; // DeathMatch Delay until reactvated + int m_iJuice; + int m_iOn; // 0 = off, 1 = startup, 2 = going + float m_flSoundTime; + + int m_iCaps; + + COutputFloat m_OutRemainingCharge; +}; + +BEGIN_DATADESC( CRecharge ) + + DEFINE_FIELD( m_flNextCharge, FIELD_TIME ), + DEFINE_FIELD( m_iReactivate, FIELD_INTEGER), + DEFINE_FIELD( m_iJuice, FIELD_INTEGER), + DEFINE_FIELD( m_iOn, FIELD_INTEGER), + DEFINE_FIELD( m_flSoundTime, FIELD_TIME ), + DEFINE_FIELD( m_iCaps, FIELD_INTEGER ), + + // Function Pointers + DEFINE_FUNCTION( Off ), + DEFINE_FUNCTION( Recharge ), + + DEFINE_OUTPUT(m_OutRemainingCharge, "OutRemainingCharge"), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS(func_recharge, CRecharge); + + +bool CRecharge::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq(szKeyName, "style") || + FStrEq(szKeyName, "height") || + FStrEq(szKeyName, "value1") || + FStrEq(szKeyName, "value2") || + FStrEq(szKeyName, "value3")) + { + } + else if (FStrEq(szKeyName, "dmdelay")) + { + m_iReactivate = atoi(szValue); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + +void CRecharge::Spawn() +{ + Precache( ); + + SetSolid( SOLID_BSP ); + SetMoveType( MOVETYPE_PUSH ); + + SetModel( STRING( GetModelName() ) ); + m_iJuice = sk_suitcharger.GetFloat(); + SetTextureFrameIndex( 0 ); + + m_iCaps = FCAP_CONTINUOUS_USE; + + CreateVPhysics(); +} + +void CRecharge::Precache() +{ + BaseClass::Precache(); + + PrecacheScriptSound( "SuitRecharge.Deny" ); + PrecacheScriptSound( "SuitRecharge.Start" ); + PrecacheScriptSound( "SuitRecharge.ChargingLoop" ); +} + +bool CRecharge::CreateVPhysics() +{ + VPhysicsInitStatic(); + return true; +} + +void CRecharge::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + // Make sure that we have a caller + if (!pActivator) + return; + + // if it's not a player, ignore + if ( !pActivator->IsPlayer() ) + return; + + CBasePlayer *pPlayer = dynamic_cast<CBasePlayer *>( pActivator ); + + if ( pPlayer == NULL ) + return; + + // Reset to a state of continuous use. + m_iCaps = FCAP_CONTINUOUS_USE; + + // if there is no juice left, turn it off + if (m_iJuice <= 0) + { + SetTextureFrameIndex( 1 ); + Off(); + } + + // if the player doesn't have the suit, or there is no juice left, make the deny noise + if ( m_iJuice <= 0 ) + { + if (m_flSoundTime <= gpGlobals->curtime) + { + m_flSoundTime = gpGlobals->curtime + 0.62; + EmitSound( "SuitRecharge.Deny" ); + } + return; + } + + // If we're over our limit, debounce our keys + if ( pPlayer->ArmorValue() >= HL1_MAX_ARMOR) + { + // Make the user re-use me to get started drawing health. + pPlayer->m_afButtonPressed &= ~IN_USE; + m_iCaps = FCAP_IMPULSE_USE; + + EmitSound( "SuitRecharge.Deny" ); + return; + } + + SetNextThink( gpGlobals->curtime + 0.25 ); + SetThink(&CRecharge::Off); + + // Time to recharge yet? + + if (m_flNextCharge >= gpGlobals->curtime) + return; + + m_hActivator = pActivator; + + + // Play the on sound or the looping charging sound + if (!m_iOn) + { + m_iOn++; + EmitSound( "SuitRecharge.Start" ); + m_flSoundTime = 0.56 + gpGlobals->curtime; + } + if ((m_iOn == 1) && (m_flSoundTime <= gpGlobals->curtime)) + { + m_iOn++; + CPASAttenuationFilter filter( this, "SuitRecharge.ChargingLoop" ); + filter.MakeReliable(); + EmitSound( filter, entindex(), "SuitRecharge.ChargingLoop" ); + } + + CBasePlayer *pl = (CBasePlayer *) m_hActivator.Get(); + + // charge the player + if (pl->ArmorValue() < HL1_MAX_ARMOR) + { + m_iJuice--; + pl->IncrementArmorValue( 1, HL1_MAX_ARMOR ); + } + + // Send the output. + float flRemaining = m_iJuice / sk_suitcharger.GetFloat(); + m_OutRemainingCharge.Set(flRemaining, pActivator, this); + + // govern the rate of charge + m_flNextCharge = gpGlobals->curtime + 0.1; +} + +void CRecharge::Recharge(void) +{ + m_iJuice = sk_suitcharger.GetFloat(); + SetTextureFrameIndex( 0 ); + SetThink( &CBaseEntity::SUB_DoNothing ); +} + +void CRecharge::Off(void) +{ + // Stop looping sound. + if (m_iOn > 1) + { + StopSound( "SuitRecharge.ChargingLoop" ); + } + + m_iOn = 0; + + if ((!m_iJuice) && ( ( m_iReactivate = g_pGameRules->FlHEVChargerRechargeTime() ) > 0) ) + { + SetNextThink( gpGlobals->curtime + m_iReactivate ); + SetThink(&CRecharge::Recharge); + } + else + SetThink( NULL ); +}
\ No newline at end of file diff --git a/game/server/hl1/hl1_func_tank.cpp b/game/server/hl1/hl1_func_tank.cpp new file mode 100644 index 0000000..8f89819 --- /dev/null +++ b/game/server/hl1/hl1_func_tank.cpp @@ -0,0 +1,1700 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "Sprite.h" +#include "EnvLaser.h" +#include "basecombatweapon.h" +#include "explode.h" +#include "eventqueue.h" +#include "gamerules.h" +#include "ammodef.h" +#include "in_buttons.h" +#include "soundent.h" +#include "ndebugoverlay.h" +#include "grenade_beam.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "triggers.h" +#include "physics_cannister.h" + +#include "player.h" +#include "entitylist.h" + + +#define SF_TANK_ACTIVE 0x0001 +#define SF_TANK_PLAYER 0x0002 +#define SF_TANK_HUMANS 0x0004 +#define SF_TANK_ALIENS 0x0008 +#define SF_TANK_LINEOFSIGHT 0x0010 +#define SF_TANK_CANCONTROL 0x0020 +#define SF_TANK_DAMAGE_KICK 0x0040 // Kick when take damage +#define SF_TANK_AIM_AT_POS 0x0080 // Aim at a particular position +#define SF_TANK_SOUNDON 0x8000 + + +enum TANKBULLET +{ + TANK_BULLET_NONE = 0, + TANK_BULLET_SMALL = 1, + TANK_BULLET_MEDIUM = 2, + TANK_BULLET_LARGE = 3, +}; + +#define FUNCTANK_FIREVOLUME 1000 + + +// Custom damage +// env_laser (duration is 0.5 rate of fire) +// rockets +// explosion? + +class CFuncTank : public CBaseEntity +{ + DECLARE_CLASS( CFuncTank, CBaseEntity ); +public: + ~CFuncTank( void ); + void Spawn( void ); + void Activate( void ); + void Precache( void ); + bool CreateVPhysics( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + + int ObjectCaps( void ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void Think( void ); + void TrackTarget( void ); + int DrawDebugTextOverlays(void); + + virtual void FiringSequence( const Vector &barrelEnd, const Vector &forward, CBaseEntity *pAttacker ); + virtual void Fire( int bulletCount, const Vector &barrelEnd, const Vector &forward, CBaseEntity *pAttacker ); + + void StartRotSound( void ); + void StopRotSound( void ); + + inline bool IsActive( void ) { return (m_spawnflags & SF_TANK_ACTIVE)?TRUE:FALSE; } + + // Input handlers. + void InputActivate( inputdata_t &inputdata ); + void InputDeactivate( inputdata_t &inputdata ); + void InputSetFireRate( inputdata_t &inputdata ); + void InputSetTargetPosition( inputdata_t &inputdata ); + void InputSetTargetEntityName( inputdata_t &inputdata ); + void InputSetTargetEntity( inputdata_t &inputdata ); + + void TankActivate(void); + void TankDeactivate(void); + + inline bool CanFire( void ); + bool InRange( float range ); + + void TankTrace( const Vector &vecStart, const Vector &vecForward, const Vector &vecSpread, trace_t &tr ); + void TraceAttack( CBaseEntity *pAttacker, float flDamage, const Vector &vecDir, trace_t *ptr, int bitsDamageType); + + Vector WorldBarrelPosition( void ) + { + EntityMatrix tmp; + tmp.InitFromEntity( this ); + return tmp.LocalToWorld( m_barrelPos ); + } + + void UpdateMatrix( void ) + { + m_parentMatrix.InitFromEntity( GetParent() ? GetParent() : NULL ); + } + QAngle AimBarrelAt( const Vector &parentTarget ); + + bool ShouldSavePhysics() { return false; } + + DECLARE_DATADESC(); + + bool OnControls( CBaseEntity *pTest ); + bool StartControl( CBasePlayer *pController ); + void StopControl( void ); + void ControllerPostFrame( void ); + + CBaseEntity *FindTarget( string_t targetName, CBaseEntity *pActivator ); + +protected: + CBasePlayer* m_pController; + float m_flNextAttack; + Vector m_vecControllerUsePos; + + float m_yawCenter; // "Center" yaw + float m_yawRate; // Max turn rate to track targets + float m_yawRange; // Range of turning motion (one-sided: 30 is +/- 30 degress from center) + // Zero is full rotation + float m_yawTolerance; // Tolerance angle + + float m_pitchCenter; // "Center" pitch + float m_pitchRate; // Max turn rate on pitch + float m_pitchRange; // Range of pitch motion as above + float m_pitchTolerance; // Tolerance angle + + float m_fireLast; // Last time I fired + float m_fireRate; // How many rounds/second + float m_lastSightTime;// Last time I saw target + float m_persist; // Persistence of firing (how long do I shoot when I can't see) + float m_persist2; // Secondary persistence of firing (randomly shooting when I can't see) + float m_persist2burst;// How long secondary persistence burst lasts + float m_minRange; // Minimum range to aim/track + float m_maxRange; // Max range to aim/track + + Vector m_barrelPos; // Length of the freakin barrel + float m_spriteScale; // Scale of any sprites we shoot + string_t m_iszSpriteSmoke; + string_t m_iszSpriteFlash; + TANKBULLET m_bulletType; // Bullet type + int m_iBulletDamage; // 0 means use Bullet type's default damage + + Vector m_sightOrigin; // Last sight of target + int m_spread; // firing spread + string_t m_iszMaster; // Master entity (game_team_master or multisource) + + int m_iSmallAmmoType; + int m_iMediumAmmoType; + int m_iLargeAmmoType; + + string_t m_soundStartRotate; + string_t m_soundStopRotate; + string_t m_soundLoopRotate; + + string_t m_targetEntityName; + EHANDLE m_hTarget; + Vector m_vTargetPosition; + EntityMatrix m_parentMatrix; + + COutputEvent m_OnFire; + COutputEvent m_OnLoseTarget; + COutputEvent m_OnAquireTarget; + + CHandle<CBaseTrigger> m_hControlVolume; + string_t m_iszControlVolume; +}; + + +BEGIN_DATADESC( CFuncTank ) + + DEFINE_KEYFIELD( m_yawRate, FIELD_FLOAT, "yawrate" ), + DEFINE_KEYFIELD( m_yawRange, FIELD_FLOAT, "yawrange" ), + DEFINE_KEYFIELD( m_yawTolerance, FIELD_FLOAT, "yawtolerance" ), + DEFINE_KEYFIELD( m_pitchRate, FIELD_FLOAT, "pitchrate" ), + DEFINE_KEYFIELD( m_pitchRange, FIELD_FLOAT, "pitchrange" ), + DEFINE_KEYFIELD( m_pitchTolerance, FIELD_FLOAT, "pitchtolerance" ), + DEFINE_KEYFIELD( m_fireRate, FIELD_FLOAT, "firerate" ), + DEFINE_KEYFIELD( m_persist, FIELD_FLOAT, "persistence" ), + DEFINE_KEYFIELD( m_persist2, FIELD_FLOAT, "persistence2" ), + DEFINE_KEYFIELD( m_minRange, FIELD_FLOAT, "minRange" ), + DEFINE_KEYFIELD( m_maxRange, FIELD_FLOAT, "maxRange" ), + DEFINE_KEYFIELD( m_spriteScale, FIELD_FLOAT, "spritescale" ), + DEFINE_KEYFIELD( m_iszSpriteSmoke, FIELD_STRING, "spritesmoke" ), + DEFINE_KEYFIELD( m_iszSpriteFlash, FIELD_STRING, "spriteflash" ), + DEFINE_KEYFIELD( m_bulletType, FIELD_INTEGER, "bullet" ), + DEFINE_KEYFIELD( m_spread, FIELD_INTEGER, "firespread" ), + DEFINE_KEYFIELD( m_iBulletDamage, FIELD_INTEGER, "bullet_damage" ), + DEFINE_KEYFIELD( m_iszMaster, FIELD_STRING, "master" ), + DEFINE_KEYFIELD( m_soundStartRotate, FIELD_SOUNDNAME, "rotatestartsound" ), + DEFINE_KEYFIELD( m_soundStopRotate, FIELD_SOUNDNAME, "rotatestopsound" ), + DEFINE_KEYFIELD( m_soundLoopRotate, FIELD_SOUNDNAME, "rotatesound" ), + DEFINE_KEYFIELD( m_iszControlVolume, FIELD_STRING, "control_volume" ), + + DEFINE_FIELD( m_yawCenter, FIELD_FLOAT ), + DEFINE_FIELD( m_pitchCenter, FIELD_FLOAT ), + DEFINE_FIELD( m_fireLast, FIELD_TIME ), + DEFINE_FIELD( m_lastSightTime, FIELD_TIME ), + DEFINE_FIELD( m_barrelPos, FIELD_VECTOR ), + DEFINE_FIELD( m_sightOrigin, FIELD_VECTOR ), + DEFINE_FIELD( m_pController, FIELD_CLASSPTR ), + DEFINE_FIELD( m_vecControllerUsePos, FIELD_VECTOR ), + DEFINE_FIELD( m_flNextAttack, FIELD_TIME ), + DEFINE_FIELD( m_targetEntityName, FIELD_STRING ), + DEFINE_FIELD( m_vTargetPosition, FIELD_VECTOR ), + DEFINE_FIELD( m_persist2burst, FIELD_FLOAT), + //DEFINE_FIELD( m_parentMatrix, FIELD_MATRIX ), // DON'T SAVE + DEFINE_FIELD( m_hTarget, FIELD_EHANDLE ), + DEFINE_FIELD( m_hControlVolume, FIELD_EHANDLE ), + + + // Do not save these. + //DEFINE_FIELD( m_iSmallAmmoType, FIELD_INTEGER ), + //DEFINE_FIELD( m_iMediumAmmoType, FIELD_INTEGER ), + //DEFINE_FIELD( m_iLargeAmmoType, FIELD_INTEGER ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Activate", InputActivate ), + DEFINE_INPUTFUNC( FIELD_VOID, "Deactivate", InputDeactivate ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFireRate", InputSetFireRate ), + DEFINE_INPUTFUNC( FIELD_VECTOR, "SetTargetPosition", InputSetTargetPosition ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetTargetEntityName", InputSetTargetEntityName ), + DEFINE_INPUTFUNC( FIELD_EHANDLE, "SetTargetEntity", InputSetTargetEntity ), + + // Outputs + DEFINE_OUTPUT(m_OnFire, "OnFire"), + DEFINE_OUTPUT(m_OnLoseTarget, "OnLoseTarget"), + DEFINE_OUTPUT(m_OnAquireTarget, "OnAquireTarget"), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CFuncTank::~CFuncTank( void ) +{ + if ( m_soundLoopRotate != NULL_STRING && (m_spawnflags & SF_TANK_SOUNDON) ) + { + StopSound( entindex(), CHAN_STATIC, STRING(m_soundLoopRotate) ); + } +} + + +int CFuncTank::ObjectCaps( void ) +{ + int iCaps = BaseClass::ObjectCaps(); + + if ( m_spawnflags & SF_TANK_CANCONTROL ) + { + iCaps |= FCAP_IMPULSE_USE; + } + + return iCaps; +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +inline bool CFuncTank::CanFire( void ) +{ + float flTimeDelay = gpGlobals->curtime - m_lastSightTime; + // Fire when can't see enemy if time is less that persistence time + if ( flTimeDelay <= m_persist) + { + return true; + } + // Fire when I'm in a persistence2 burst + else if (flTimeDelay <= m_persist2burst) + { + return true; + } + // If less than persistence2, occasionally do another burst + else if (flTimeDelay <= m_persist2) + { + if (random->RandomInt(0,30)==0) + { + m_persist2burst = flTimeDelay+0.5; + return true; + } + } + return false; +} + + +//------------------------------------------------------------------------------ +// Purpose: Input handler for activating the tank. +//------------------------------------------------------------------------------ +void CFuncTank::InputActivate( inputdata_t &inputdata ) +{ + TankActivate(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFuncTank::TankActivate(void) +{ + m_spawnflags |= SF_TANK_ACTIVE; + SetNextThink( gpGlobals->curtime + 0.1f ); + m_fireLast = 0; +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for deactivating the tank. +//----------------------------------------------------------------------------- +void CFuncTank::InputDeactivate( inputdata_t &inputdata ) +{ + TankDeactivate(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFuncTank::TankDeactivate(void) +{ + m_spawnflags &= ~SF_TANK_ACTIVE; + m_fireLast = 0; + StopRotSound(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for changing the name of the tank's target entity. +//----------------------------------------------------------------------------- +void CFuncTank::InputSetTargetEntityName( inputdata_t &inputdata ) +{ + m_targetEntityName = inputdata.value.StringID(); + m_hTarget = FindTarget( m_targetEntityName, inputdata.pActivator ); + + // No longer aim at target position if have one + m_spawnflags &= ~SF_TANK_AIM_AT_POS; +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for setting a new target entity by ehandle. +//----------------------------------------------------------------------------- +void CFuncTank::InputSetTargetEntity( inputdata_t &inputdata ) +{ + if (inputdata.value.Entity() != NULL) + { + m_targetEntityName = inputdata.value.Entity()->GetEntityName(); + } + else + { + m_targetEntityName = NULL_STRING; + } + m_hTarget = inputdata.value.Entity(); + + // No longer aim at target position if have one + m_spawnflags &= ~SF_TANK_AIM_AT_POS; +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for setting the rate of fire in shots per second. +//----------------------------------------------------------------------------- +void CFuncTank::InputSetFireRate( inputdata_t &inputdata ) +{ + m_fireRate = inputdata.value.Float(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for setting the target as a position. +//----------------------------------------------------------------------------- +void CFuncTank::InputSetTargetPosition( inputdata_t &inputdata ) +{ + m_spawnflags |= SF_TANK_AIM_AT_POS; + m_hTarget = NULL; + + inputdata.value.Vector3D(m_vTargetPosition); +} + + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CFuncTank::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + // -------------- + // State + // -------------- + char tempstr[255]; + if (IsActive()) + { + Q_strncpy(tempstr,"State: Active",sizeof(tempstr)); + } + else + { + Q_strncpy(tempstr,"State: Inactive",sizeof(tempstr)); + } + EntityText(text_offset,tempstr,0); + text_offset++; + + // ------------------- + // Print Firing Speed + // -------------------- + Q_snprintf(tempstr,sizeof(tempstr),"Fire Rate: %f",m_fireRate); + + EntityText(text_offset,tempstr,0); + text_offset++; + + // -------------- + // Print Target + // -------------- + if (m_hTarget!=NULL) + { + Q_snprintf(tempstr,sizeof(tempstr),"Target: %s",m_hTarget->GetDebugName()); + } + else + { + Q_snprintf(tempstr,sizeof(tempstr),"Target: - "); + } + EntityText(text_offset,tempstr,0); + text_offset++; + + // -------------- + // Target Pos + // -------------- + if (m_spawnflags & SF_TANK_AIM_AT_POS) + { + Q_snprintf(tempstr,sizeof(tempstr),"Aim Pos: %3.0f %3.0f %3.0f",m_vTargetPosition.x,m_vTargetPosition.y,m_vTargetPosition.z); + } + else + { + Q_snprintf(tempstr,sizeof(tempstr),"Aim Pos: - "); + } + EntityText(text_offset,tempstr,0); + text_offset++; + + } + return text_offset; +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pAttacker - +// flDamage - +// vecDir - +// ptr - +// bitsDamageType - +//----------------------------------------------------------------------------- +void CFuncTank::TraceAttack( CBaseEntity *pAttacker, float flDamage, const Vector &vecDir, trace_t *ptr, int bitsDamageType) +{ + if (m_spawnflags & SF_TANK_DAMAGE_KICK) + { + // Deflect the func_tank + // Only adjust yaw for now + if (pAttacker) + { + Vector vFromAttacker = (pAttacker->EyePosition()-GetAbsOrigin()); + vFromAttacker.z = 0; + VectorNormalize(vFromAttacker); + + Vector vFromAttacker2 = (ptr->endpos-GetAbsOrigin()); + vFromAttacker2.z = 0; + VectorNormalize(vFromAttacker2); + + + Vector vCrossProduct; + CrossProduct(vFromAttacker,vFromAttacker2, vCrossProduct); + Msg( "%f\n",vCrossProduct.z); + QAngle angles; + angles = GetLocalAngles(); + if (vCrossProduct.z > 0) + { + angles.y += 10; + } + else + { + angles.y -= 10; + } + + // Limit against range in y + if ( angles.y > m_yawCenter + m_yawRange ) + { + angles.y = m_yawCenter + m_yawRange; + } + else if ( angles.y < (m_yawCenter - m_yawRange) ) + { + angles.y = (m_yawCenter - m_yawRange); + } + + SetLocalAngles( angles ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : targetName - +// pActivator - +//----------------------------------------------------------------------------- +CBaseEntity *CFuncTank::FindTarget( string_t targetName, CBaseEntity *pActivator ) +{ + return gEntList.FindEntityGenericNearest( STRING( targetName ), GetAbsOrigin(), 0, this, pActivator ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Caches entity key values until spawn is called. +// Input : szKeyName - +// szValue - +// Output : +//----------------------------------------------------------------------------- +bool CFuncTank::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "barrel")) + { + m_barrelPos.x = atof(szValue); + } + else if (FStrEq(szKeyName, "barrely")) + { + m_barrelPos.y = atof(szValue); + } + else if (FStrEq(szKeyName, "barrelz")) + { + m_barrelPos.z = atof(szValue); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + + +static Vector gTankSpread[] = +{ + Vector( 0, 0, 0 ), // perfect + Vector( 0.025, 0.025, 0.025 ), // small cone + Vector( 0.05, 0.05, 0.05 ), // medium cone + Vector( 0.1, 0.1, 0.1 ), // large cone + Vector( 0.25, 0.25, 0.25 ), // extra-large cone +}; +#define MAX_FIRING_SPREADS ARRAYSIZE(gTankSpread) + + +void CFuncTank::Spawn( void ) +{ + Precache(); + + SetMoveType( MOVETYPE_PUSH ); // so it doesn't get pushed by anything + SetSolid( SOLID_VPHYSICS ); + SetModel( STRING( GetModelName() ) ); + + m_yawCenter = GetLocalAngles().y; + m_pitchCenter = GetLocalAngles().x; + m_vTargetPosition = vec3_origin; + + if ( IsActive() ) + SetNextThink( gpGlobals->curtime + 1.0f ); + + UpdateMatrix(); + + m_sightOrigin = WorldBarrelPosition(); // Point at the end of the barrel + + if ( m_spread > MAX_FIRING_SPREADS ) + { + m_spread = 0; + } + + // No longer aim at target position if have one + m_spawnflags &= ~SF_TANK_AIM_AT_POS; + + if (m_spawnflags & SF_TANK_DAMAGE_KICK) + { + m_takedamage = DAMAGE_YES; + } + + // UNDONE: Do this? + //m_targetEntityName = m_target; + CreateVPhysics(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFuncTank::Activate( void ) +{ + BaseClass::Activate(); + + m_hControlVolume = NULL; + + // Find our control volume + if ( m_iszControlVolume != NULL_STRING ) + { + m_hControlVolume = dynamic_cast<CBaseTrigger*>( gEntList.FindEntityByName( NULL, m_iszControlVolume ) ); + } + + if (( !m_hControlVolume ) && (m_spawnflags & SF_TANK_CANCONTROL)) + { + Msg( "ERROR: Couldn't find control volume for player-controllable func_tank %s.\n", STRING(GetEntityName()) ); + UTIL_Remove( this ); + } +} + +bool CFuncTank::CreateVPhysics() +{ + VPhysicsInitShadow( false, false ); + return true; +} + + +void CFuncTank::Precache( void ) +{ + m_iSmallAmmoType = GetAmmoDef()->Index("9mmRound"); + m_iMediumAmmoType = GetAmmoDef()->Index("9mmRound"); + m_iLargeAmmoType = GetAmmoDef()->Index("12mmRound"); + + if ( m_iszSpriteSmoke != NULL_STRING ) + PrecacheModel( STRING(m_iszSpriteSmoke) ); + if ( m_iszSpriteFlash != NULL_STRING ) + PrecacheModel( STRING(m_iszSpriteFlash) ); + + if ( m_soundStartRotate != NULL_STRING ) + PrecacheScriptSound( STRING(m_soundStartRotate) ); + if ( m_soundStopRotate != NULL_STRING ) + PrecacheScriptSound( STRING(m_soundStopRotate) ); + if ( m_soundLoopRotate != NULL_STRING ) + PrecacheScriptSound( STRING(m_soundLoopRotate) ); +} + + + +//================================================================================== +// TANK CONTROLLING +bool CFuncTank::OnControls( CBaseEntity *pTest ) +{ + if ( !(m_spawnflags & SF_TANK_CANCONTROL) ) + return FALSE; + + Vector offset = pTest->GetLocalOrigin() - GetLocalOrigin(); + + if ( (m_vecControllerUsePos - pTest->GetLocalOrigin()).Length() < 30 ) + return TRUE; + + return FALSE; +} + +bool CFuncTank::StartControl( CBasePlayer *pController ) +{ + if ( m_pController != NULL ) + return FALSE; + + // Team only or disabled? + if ( m_iszMaster != NULL_STRING ) + { + if ( !UTIL_IsMasterTriggered( m_iszMaster, pController ) ) + return FALSE; + } + + // Holster player's weapon + m_pController = pController; + if ( m_pController->GetActiveWeapon() ) + { + m_pController->GetActiveWeapon()->Holster(); + } + + m_pController->m_Local.m_iHideHUD |= HIDEHUD_WEAPONSELECTION; + m_vecControllerUsePos = m_pController->GetLocalOrigin(); + + SetNextThink( gpGlobals->curtime + 0.1f ); + + return TRUE; +} + +void CFuncTank::StopControl() +{ + // TODO: bring back the controllers current weapon + if ( !m_pController ) + return; + + if ( m_pController->GetActiveWeapon() ) + m_pController->GetActiveWeapon()->Deploy(); + + m_pController->m_Local.m_iHideHUD &= ~HIDEHUD_WEAPONSELECTION; + SetNextThink( TICK_NEVER_THINK ); + m_pController = NULL; + + if ( IsActive() ) + SetNextThink( gpGlobals->curtime + 1.0f ); +} + +// Called each frame by the player's ItemPostFrame +void CFuncTank::ControllerPostFrame( void ) +{ + Assert(m_pController != NULL); + + if ( gpGlobals->curtime < m_flNextAttack ) + return; + + Vector forward; + AngleVectors( GetAbsAngles(), &forward ); + if ( m_pController->m_nButtons & IN_ATTACK ) + { + m_fireLast = gpGlobals->curtime - (1/m_fireRate) - 0.01; // to make sure the gun doesn't fire too many bullets + + int bulletCount = (gpGlobals->curtime - m_fireLast) * m_fireRate; + + Fire( bulletCount, WorldBarrelPosition(), forward, m_pController ); + + // HACKHACK -- make some noise (that the AI can hear) + CSoundEnt::InsertSound( SOUND_COMBAT, WorldSpaceCenter(), FUNCTANK_FIREVOLUME, 0.2 ); + + m_flNextAttack = gpGlobals->curtime + (1/m_fireRate); + } +} + + +void CFuncTank::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( m_spawnflags & SF_TANK_CANCONTROL ) + { + // player controlled turret + CBasePlayer *pPlayer = ToBasePlayer( pActivator ); + if ( !pPlayer ) + return; + + if ( value == 2 && useType == USE_SET ) + { + ControllerPostFrame(); + } + else if ( !m_pController && useType != USE_OFF ) + { + // The player must be within the func_tank controls + Assert( m_hControlVolume ); + if ( !m_hControlVolume->IsTouching( pPlayer ) ) + return; + + pPlayer->SetUseEntity( this ); + StartControl( pPlayer ); + } + else + { + StopControl(); + } + } + else + { + if ( !ShouldToggle( useType, IsActive() ) ) + return; + + if ( IsActive() ) + { + TankDeactivate(); + } + else + { + TankActivate(); + } + } +} + + +bool CFuncTank::InRange( float range ) +{ + if ( range < m_minRange ) + return FALSE; + if ( m_maxRange > 0 && range > m_maxRange ) + return FALSE; + + return TRUE; +} + + +void CFuncTank::Think( void ) +{ + // refresh the matrix + UpdateMatrix(); + + SetLocalAngularVelocity( vec3_angle ); + TrackTarget(); + + if ( fabs(GetLocalAngularVelocity().x) > 1 || fabs(GetLocalAngularVelocity().y) > 1 ) + StartRotSound(); + else + StopRotSound(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Aim the offset barrel at a position in parent space +// Input : parentTarget - the position of the target in parent space +// Output : Vector - angles in local space +//----------------------------------------------------------------------------- +QAngle CFuncTank::AimBarrelAt( const Vector &parentTarget ) +{ + Vector target = parentTarget - GetLocalOrigin(); + float quadTarget = target.LengthSqr(); + float quadTargetXY = target.x*target.x + target.y*target.y; + + // Target is too close! Can't aim at it + if ( quadTarget <= m_barrelPos.LengthSqr() ) + { + return GetLocalAngles(); + } + else + { + // We're trying to aim the offset barrel at an arbitrary point. + // To calculate this, I think of the target as being on a sphere with + // it's center at the origin of the gun. + // The rotation we need is the opposite of the rotation that moves the target + // along the surface of that sphere to intersect with the gun's shooting direction + // To calculate that rotation, we simply calculate the intersection of the ray + // coming out of the barrel with the target sphere (that's the new target position) + // and use atan2() to get angles + + // angles from target pos to center + float targetToCenterYaw = atan2( target.y, target.x ); + float centerToGunYaw = atan2( m_barrelPos.y, sqrt( quadTarget - (m_barrelPos.y*m_barrelPos.y) ) ); + + float targetToCenterPitch = atan2( target.z, sqrt( quadTargetXY ) ); + float centerToGunPitch = atan2( -m_barrelPos.z, sqrt( quadTarget - (m_barrelPos.z*m_barrelPos.z) ) ); + return QAngle( -RAD2DEG(targetToCenterPitch+centerToGunPitch), RAD2DEG( targetToCenterYaw - centerToGunYaw ), 0 ); + } +} + + +void CFuncTank::TrackTarget( void ) +{ + trace_t tr; + bool updateTime = FALSE, lineOfSight; + QAngle angles; + Vector barrelEnd; + CBaseEntity *pTarget = NULL; + + barrelEnd.Init(); + + // Get a position to aim for + if (m_pController) + { + // Tanks attempt to mirror the player's angles + angles = m_pController->EyeAngles(); + SetNextThink( gpGlobals->curtime + 0.05 ); + } + else + { + if ( IsActive() ) + { + SetNextThink( gpGlobals->curtime + 0.1f ); + } + else + { + return; + } + + // ----------------------------------- + // Get world target position + // ----------------------------------- + barrelEnd = WorldBarrelPosition(); + Vector worldTargetPosition; + if (m_spawnflags & SF_TANK_AIM_AT_POS) + { + worldTargetPosition = m_vTargetPosition; + } + else + { + CBaseEntity *pEntity = (CBaseEntity *)m_hTarget; + if ( !pEntity || ( pEntity->GetFlags() & FL_NOTARGET ) ) + { + if ( m_targetEntityName != NULL_STRING ) // New HL2 behavior + { + m_hTarget = FindTarget( m_targetEntityName, NULL ); + } + else // HL1 style + { + m_hTarget = ToBasePlayer( GetContainingEntity( UTIL_FindClientInPVS( edict() ) ) ); + } + + if ( m_hTarget != NULL ) + { + SetNextThink( gpGlobals->curtime ); // Think again immediately + } + else + { + if ( IsActive() ) + { + SetNextThink( gpGlobals->curtime + 2 ); // Wait 2 secs + } + + if ( m_fireLast !=0 ) + { + m_OnLoseTarget.FireOutput(this, this); + m_fireLast = 0; + } + } + + return; + } + pTarget = pEntity; + + // Calculate angle needed to aim at target + worldTargetPosition = pEntity->EyePosition(); + } + + float range = (worldTargetPosition - barrelEnd).Length(); + + if ( !InRange( range ) ) + { + m_fireLast = 0; + return; + } + + UTIL_TraceLine( barrelEnd, worldTargetPosition, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + + if (m_spawnflags & SF_TANK_AIM_AT_POS) + { + updateTime = TRUE; + m_sightOrigin = m_vTargetPosition; + } + else + { + lineOfSight = FALSE; + // No line of sight, don't track + if ( tr.fraction == 1.0 || tr.m_pEnt == pTarget ) + { + lineOfSight = TRUE; + + CBaseEntity *pInstance = pTarget; + if ( InRange( range ) && pInstance && pInstance->IsAlive() ) + { + updateTime = TRUE; + + // Sight position is BodyTarget with no noise (so gun doesn't bob up and down) + m_sightOrigin = pInstance->BodyTarget( GetLocalOrigin(), false ); + } + } + } + + // Convert targetPosition to parent + angles = AimBarrelAt( m_parentMatrix.WorldToLocal( m_sightOrigin ) ); + } + + // Force the angles to be relative to the center position + float offsetY = UTIL_AngleDistance( angles.y, m_yawCenter ); + float offsetX = UTIL_AngleDistance( angles.x, m_pitchCenter ); + angles.y = m_yawCenter + offsetY; + angles.x = m_pitchCenter + offsetX; + + // Limit against range in y + + // MDB - don't check pitch! If two func_tanks are meant to align, + // and one can pitch and the other cannot, this can lead to them getting + // different values for angles.y. Nothing is lost by not updating yaw + // because the target is not in pitch range. + + bool bOutsideYawRange = ( fabs( offsetY ) > m_yawRange + m_yawTolerance ); + bool bOutsidePitchRange = ( fabs( offsetX ) > m_pitchRange + m_pitchTolerance ); + + Vector vecToTarget = m_sightOrigin - GetLocalOrigin(); + + // if target is outside yaw range + if ( bOutsideYawRange ) + { + if ( angles.y > m_yawCenter + m_yawRange ) + { + angles.y = m_yawCenter + m_yawRange; + } + else if ( angles.y < (m_yawCenter - m_yawRange) ) + { + angles.y = (m_yawCenter - m_yawRange); + } + } + + if ( bOutsidePitchRange || bOutsideYawRange || ( vecToTarget.Length() < ( barrelEnd - GetAbsOrigin() ).Length() ) ) + { + // Don't update if you saw the player, but out of range + updateTime = false; + } + + if ( updateTime ) + { + m_lastSightTime = gpGlobals->curtime; + m_persist2burst = 0; + } + + // Move toward target at rate or less + float distY = UTIL_AngleDistance( angles.y, GetLocalAngles().y ); + + QAngle vecAngVel = GetLocalAngularVelocity(); + vecAngVel.y = distY * 10; + vecAngVel.y = clamp( vecAngVel.y, -m_yawRate, m_yawRate ); + + // Limit against range in x + angles.x = clamp( angles.x, m_pitchCenter - m_pitchRange, m_pitchCenter + m_pitchRange ); + + // Move toward target at rate or less + float distX = UTIL_AngleDistance( angles.x, GetLocalAngles().x ); + vecAngVel.x = distX * 10; + vecAngVel.x = clamp( vecAngVel.x, -m_pitchRate, m_pitchRate ); + SetLocalAngularVelocity( vecAngVel ); + + SetMoveDoneTime( 0.1 ); + if ( m_pController ) + return; + + if ( CanFire() && ( (fabs(distX) < m_pitchTolerance && fabs(distY) < m_yawTolerance) || (m_spawnflags & SF_TANK_LINEOFSIGHT) ) ) + { + bool fire = FALSE; + Vector forward; + AngleVectors( GetLocalAngles(), &forward ); + forward = m_parentMatrix.ApplyRotation( forward ); + + + if ( m_spawnflags & SF_TANK_LINEOFSIGHT ) + { + float length = (m_maxRange > 0) ? m_maxRange : MAX_TRACE_LENGTH; + UTIL_TraceLine( barrelEnd, barrelEnd + forward * length, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + + if ( tr.m_pEnt == pTarget ) + fire = TRUE; + } + else + fire = TRUE; + + if ( fire ) + { + if (m_fireLast == 0) + { + m_OnAquireTarget.FireOutput(this, this); + } + FiringSequence( barrelEnd, forward, this ); + } + else + { + if (m_fireLast !=0) + { + m_OnLoseTarget.FireOutput(this, this); + } + m_fireLast = 0; + } + } + else + { + if (m_fireLast !=0) + { + m_OnLoseTarget.FireOutput(this, this); + } + m_fireLast = 0; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Start of firing sequence. By default, just fire now. +// Input : &barrelEnd - +// &forward - +// *pAttacker - +//----------------------------------------------------------------------------- +void CFuncTank::FiringSequence( const Vector &barrelEnd, const Vector &forward, CBaseEntity *pAttacker ) +{ + if ( m_fireLast != 0 ) + { + int bulletCount = (gpGlobals->curtime - m_fireLast) * m_fireRate; + + if ( bulletCount > 0 ) + { + Fire( bulletCount, barrelEnd, forward, pAttacker ); + m_fireLast = gpGlobals->curtime; + } + } + else + { + m_fireLast = gpGlobals->curtime; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Fire targets and spawn sprites. +// Input : bulletCount - +// barrelEnd - +// forward - +// pAttacker - +//----------------------------------------------------------------------------- +void CFuncTank::Fire( int bulletCount, const Vector &barrelEnd, const Vector &forward, CBaseEntity *pAttacker ) +{ + if ( m_iszSpriteSmoke != NULL_STRING ) + { + CSprite *pSprite = CSprite::SpriteCreate( STRING(m_iszSpriteSmoke), barrelEnd, TRUE ); + pSprite->AnimateAndDie( random->RandomFloat( 15.0, 20.0 ) ); + pSprite->SetTransparency( kRenderTransAlpha, m_clrRender->r, m_clrRender->g, m_clrRender->b, 255, kRenderFxNone ); + + Vector vecVelocity( 0, 0, random->RandomFloat(40, 80) ); + pSprite->SetAbsVelocity( vecVelocity ); + pSprite->SetScale( m_spriteScale ); + } + if ( m_iszSpriteFlash != NULL_STRING ) + { + CSprite *pSprite = CSprite::SpriteCreate( STRING(m_iszSpriteFlash), barrelEnd, TRUE ); + pSprite->AnimateAndDie( 60 ); + pSprite->SetTransparency( kRenderTransAdd, 255, 255, 255, 255, kRenderFxNoDissipation ); + pSprite->SetScale( m_spriteScale ); + } + + m_OnFire.FireOutput(this, this); +} + + +void CFuncTank::TankTrace( const Vector &vecStart, const Vector &vecForward, const Vector &vecSpread, trace_t &tr ) +{ + Vector forward, right, up; + + AngleVectors( GetAbsAngles(), &forward, &right, &up ); + // get circular gaussian spread + float x, y, z; + do { + x = random->RandomFloat(-0.5,0.5) + random->RandomFloat(-0.5,0.5); + y = random->RandomFloat(-0.5,0.5) + random->RandomFloat(-0.5,0.5); + z = x*x+y*y; + } while (z > 1); + Vector vecDir = vecForward + + x * vecSpread.x * right + + y * vecSpread.y * up; + Vector vecEnd; + + vecEnd = vecStart + vecDir * MAX_TRACE_LENGTH; + UTIL_TraceLine( vecStart, vecEnd, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); +} + + +void CFuncTank::StartRotSound( void ) +{ + if ( m_spawnflags & SF_TANK_SOUNDON ) + return; + m_spawnflags |= SF_TANK_SOUNDON; + + if ( m_soundLoopRotate != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + filter.MakeReliable(); + + EmitSound_t ep; + ep.m_nChannel = CHAN_STATIC; + ep.m_pSoundName = (char*)STRING(m_soundLoopRotate); + ep.m_flVolume = 0.85f; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + if ( m_soundStartRotate != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_BODY; + ep.m_pSoundName = (char*)STRING(m_soundStartRotate); + ep.m_flVolume = 1.0f; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } +} + + +void CFuncTank::StopRotSound( void ) +{ + if ( m_spawnflags & SF_TANK_SOUNDON ) + { + if ( m_soundLoopRotate != NULL_STRING ) + { + StopSound( entindex(), CHAN_STATIC, (char*)STRING(m_soundLoopRotate) ); + } + if ( m_soundStopRotate != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_BODY; + ep.m_pSoundName = (char*)STRING(m_soundStopRotate); + ep.m_flVolume = 1.0f; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + } + m_spawnflags &= ~SF_TANK_SOUNDON; +} + +// ############################################################################# +// CFuncTankGun +// ############################################################################# +class CFuncTankGun : public CFuncTank +{ +public: + DECLARE_CLASS( CFuncTankGun, CFuncTank ); + + void Fire( int bulletCount, const Vector &barrelEnd, const Vector &forward, CBaseEntity *pAttacker ); +}; +LINK_ENTITY_TO_CLASS( func_tank, CFuncTankGun ); + +void CFuncTankGun::Fire( int bulletCount, const Vector &barrelEnd, const Vector &forward, CBaseEntity *pAttacker ) +{ + int i; + + for ( i = 0; i < bulletCount; i++ ) + { + switch( m_bulletType ) + { + case TANK_BULLET_SMALL: + FireBullets( 1, barrelEnd, forward, gTankSpread[m_spread], MAX_TRACE_LENGTH, m_iSmallAmmoType, 1, -1, -1, m_iBulletDamage, pAttacker ); + break; + + case TANK_BULLET_MEDIUM: + FireBullets( 1, barrelEnd, forward, gTankSpread[m_spread], MAX_TRACE_LENGTH, m_iMediumAmmoType, 1, -1, -1, m_iBulletDamage, pAttacker ); + break; + + case TANK_BULLET_LARGE: + FireBullets( 1, barrelEnd, forward, gTankSpread[m_spread], MAX_TRACE_LENGTH, m_iLargeAmmoType, 1, -1, -1, m_iBulletDamage, pAttacker ); + break; + default: + case TANK_BULLET_NONE: + break; + } + } + CFuncTank::Fire( bulletCount, barrelEnd, forward, pAttacker ); +} + +// ############################################################################# +// CFuncTankPulseLaser +// ############################################################################# +class CFuncTankPulseLaser : public CFuncTankGun +{ +public: + DECLARE_CLASS( CFuncTankPulseLaser, CFuncTankGun ); + DECLARE_DATADESC(); + + void Precache(); + void Fire( int bulletCount, const Vector &barrelEnd, const Vector &forward, CBaseEntity *pAttacker ); + + float m_flPulseSpeed; + float m_flPulseWidth; + color32 m_flPulseColor; + float m_flPulseLife; + float m_flPulseLag; + string_t m_sPulseFireSound; +}; +LINK_ENTITY_TO_CLASS( func_tankpulselaser, CFuncTankPulseLaser ); + +BEGIN_DATADESC( CFuncTankPulseLaser ) + + DEFINE_KEYFIELD( m_flPulseSpeed, FIELD_FLOAT, "PulseSpeed" ), + DEFINE_KEYFIELD( m_flPulseWidth, FIELD_FLOAT, "PulseWidth" ), + DEFINE_KEYFIELD( m_flPulseColor, FIELD_COLOR32, "PulseColor" ), + DEFINE_KEYFIELD( m_flPulseLife, FIELD_FLOAT, "PulseLife" ), + DEFINE_KEYFIELD( m_flPulseLag, FIELD_FLOAT, "PulseLag" ), + DEFINE_KEYFIELD( m_sPulseFireSound, FIELD_SOUNDNAME, "PulseFireSound" ), + +END_DATADESC() + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CFuncTankPulseLaser::Precache(void) +{ + UTIL_PrecacheOther( "grenade_beam" ); + + if ( m_sPulseFireSound != NULL_STRING ) + { + PrecacheScriptSound( STRING(m_sPulseFireSound) ); + } + BaseClass::Precache(); +} +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CFuncTankPulseLaser::Fire( int bulletCount, const Vector &barrelEnd, const Vector &vecForward, CBaseEntity *pAttacker ) +{ + // -------------------------------------------------- + // Get direction vectors for spread + // -------------------------------------------------- + Vector vecUp = Vector(0,0,1); + Vector vecRight; + CrossProduct ( vecForward, vecUp, vecRight ); + CrossProduct ( vecForward, -vecRight, vecUp ); + + for ( int i = 0; i < bulletCount; i++ ) + { + // get circular gaussian spread + float x, y, z; + do { + x = random->RandomFloat(-0.5,0.5) + random->RandomFloat(-0.5,0.5); + y = random->RandomFloat(-0.5,0.5) + random->RandomFloat(-0.5,0.5); + z = x*x+y*y; + } while (z > 1); + + Vector vecDir = vecForward + x * gTankSpread[m_spread].x * vecRight + y * gTankSpread[m_spread].y * vecUp; + + CGrenadeBeam *pPulse = CGrenadeBeam::Create( pAttacker, barrelEnd); + pPulse->Format(m_flPulseColor, m_flPulseWidth); + pPulse->Shoot(vecDir,m_flPulseSpeed,m_flPulseLife,m_flPulseLag,m_iBulletDamage); + + if ( m_sPulseFireSound != NULL_STRING ) + { + CPASAttenuationFilter filter( this, 0.6f ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_WEAPON; + ep.m_pSoundName = (char*)STRING(m_sPulseFireSound); + ep.m_flVolume = 1.0f; + ep.m_SoundLevel = ATTN_TO_SNDLVL( 0.6 ); + + EmitSound( filter, entindex(), ep ); + } + + } + CFuncTank::Fire( bulletCount, barrelEnd, vecForward, pAttacker ); +} + +// ############################################################################# +// CFuncTankLaser +// ############################################################################# +class CFuncTankLaser : public CFuncTank +{ + DECLARE_CLASS( CFuncTankLaser, CFuncTank ); +public: + void Activate( void ); + void Fire( int bulletCount, const Vector &barrelEnd, const Vector &forward, CBaseEntity *pAttacker ); + void Think( void ); + CEnvLaser *GetLaser( void ); + void UpdateOnRemove( void ); + + DECLARE_DATADESC(); + +private: + CEnvLaser *m_pLaser; + float m_laserTime; + string_t m_iszLaserName; +}; +LINK_ENTITY_TO_CLASS( func_tanklaser, CFuncTankLaser ); + +BEGIN_DATADESC( CFuncTankLaser ) + + DEFINE_KEYFIELD( m_iszLaserName, FIELD_STRING, "laserentity" ), + + DEFINE_FIELD( m_pLaser, FIELD_CLASSPTR ), + DEFINE_FIELD( m_laserTime, FIELD_TIME ), + +END_DATADESC() + + +void CFuncTankLaser::Activate( void ) +{ + BaseClass::Activate(); + + if ( !GetLaser() ) + { + UTIL_Remove(this); + Warning( "Laser tank with no env_laser!\n" ); + } + else + { + m_pLaser->TurnOff(); + } +} + +void CFuncTankLaser::UpdateOnRemove( void ) +{ + if( GetLaser() ) + { + m_pLaser->TurnOff(); + } + + BaseClass::UpdateOnRemove(); +} + +CEnvLaser *CFuncTankLaser::GetLaser( void ) +{ + if ( m_pLaser ) + return m_pLaser; + + CBaseEntity *pLaser = gEntList.FindEntityByName( NULL, m_iszLaserName ); + while ( pLaser ) + { + // Found the landmark + if ( FClassnameIs( pLaser, "env_laser" ) ) + { + m_pLaser = (CEnvLaser *)pLaser; + break; + } + else + { + pLaser = gEntList.FindEntityByName( pLaser, m_iszLaserName ); + } + } + + return m_pLaser; +} + + +void CFuncTankLaser::Think( void ) +{ + if ( m_pLaser && (gpGlobals->curtime > m_laserTime) ) + m_pLaser->TurnOff(); + + CFuncTank::Think(); +} + + +void CFuncTankLaser::Fire( int bulletCount, const Vector &barrelEnd, const Vector &forward, CBaseEntity *pAttacker ) +{ + int i; + trace_t tr; + + if ( GetLaser() ) + { + for ( i = 0; i < bulletCount; i++ ) + { + m_pLaser->SetLocalOrigin( barrelEnd ); + TankTrace( barrelEnd, forward, gTankSpread[m_spread], tr ); + + m_laserTime = gpGlobals->curtime; + m_pLaser->TurnOn(); + m_pLaser->SetFireTime( gpGlobals->curtime - 1.0 ); + m_pLaser->FireAtPoint( tr ); + m_pLaser->SetNextThink( TICK_NEVER_THINK ); + } + CFuncTank::Fire( bulletCount, barrelEnd, forward, this ); + } +} + +class CFuncTankRocket : public CFuncTank +{ +public: + DECLARE_CLASS( CFuncTankRocket, CFuncTank ); + + void Precache( void ); + void Fire( int bulletCount, const Vector &barrelEnd, const Vector &forward, CBaseEntity *pAttacker ); +}; +LINK_ENTITY_TO_CLASS( func_tankrocket, CFuncTankRocket ); + +void CFuncTankRocket::Precache( void ) +{ + UTIL_PrecacheOther( "rpg_rocket" ); + CFuncTank::Precache(); +} + + + +void CFuncTankRocket::Fire( int bulletCount, const Vector &barrelEnd, const Vector &forward, CBaseEntity *pAttacker ) +{ + int i; + + for ( i = 0; i < bulletCount; i++ ) + { + CBaseEntity *pRocket = CBaseEntity::Create( "rpg_rocket", barrelEnd, GetAbsAngles(), this ); + pRocket->SetAbsAngles( GetAbsAngles() ); + } + CFuncTank::Fire( bulletCount, barrelEnd, forward, this ); +} + + +class CFuncTankMortar : public CFuncTank +{ +public: + DECLARE_CLASS( CFuncTankMortar, CFuncTank ); + + void Precache( void ); + void FiringSequence( const Vector &barrelEnd, const Vector &forward, CBaseEntity *pAttacker ); + void Fire( int bulletCount, const Vector &barrelEnd, const Vector &vecForward, CBaseEntity *pAttacker ); + void ShootGun(void); + + // Input handlers. + void InputShootGun( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + + int m_Magnitude; + float m_fireDelay; + string_t m_fireStartSound; + string_t m_fireEndSound; + + // store future firing event + CBaseEntity *m_pAttacker; +}; + +LINK_ENTITY_TO_CLASS( func_tankmortar, CFuncTankMortar ); + +BEGIN_DATADESC( CFuncTankMortar ) + + DEFINE_KEYFIELD( m_Magnitude, FIELD_INTEGER, "iMagnitude" ), + DEFINE_KEYFIELD( m_fireDelay, FIELD_FLOAT, "firedelay" ), + DEFINE_KEYFIELD( m_fireStartSound, FIELD_STRING, "firestartsound" ), + DEFINE_KEYFIELD( m_fireEndSound, FIELD_STRING, "fireendsound" ), + + DEFINE_FIELD( m_pAttacker, FIELD_CLASSPTR ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "ShootGun", InputShootGun ), + +END_DATADESC() + + + +void CFuncTankMortar::Precache( void ) +{ + if ( m_fireStartSound != NULL_STRING ) + PrecacheScriptSound( STRING(m_fireStartSound) ); + if ( m_fireEndSound != NULL_STRING ) + PrecacheScriptSound( STRING(m_fireEndSound) ); + BaseClass::Precache(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler to make the tank shoot. +//----------------------------------------------------------------------------- +void CFuncTankMortar::InputShootGun( inputdata_t &inputdata ) +{ + ShootGun(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFuncTankMortar::ShootGun( void ) +{ + Vector forward; + AngleVectors( GetLocalAngles(), &forward ); + UpdateMatrix(); + forward = m_parentMatrix.ApplyRotation( forward ); + + // use cached firing state + Fire( 1, WorldBarrelPosition(), forward, m_pAttacker ); +} + + +void CFuncTankMortar::FiringSequence( const Vector &barrelEnd, const Vector &forward, CBaseEntity *pAttacker ) +{ + if ( m_fireLast != 0 ) + { + int bulletCount = (gpGlobals->curtime - m_fireLast) * m_fireRate; + // Only create 1 explosion + if ( bulletCount > 0 ) + { + // fire in "firedelay" seconds + if ( m_fireStartSound != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_WEAPON; + ep.m_pSoundName = (char*)STRING(m_fireStartSound); + ep.m_flVolume = 1.0f; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + if ( m_fireDelay != 0 ) + { + g_EventQueue.AddEvent( this, "ShootGun", m_fireDelay, pAttacker, this, 0 ); + } + else + { + ShootGun(); + } + m_fireLast = gpGlobals->curtime; + } + } + else + { + m_fireLast = gpGlobals->curtime; + } +} + +void CFuncTankMortar::Fire( int bulletCount, const Vector &barrelEnd, const Vector &vecForward, CBaseEntity *pAttacker ) +{ + if ( m_fireEndSound != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_WEAPON; + ep.m_pSoundName = STRING(m_fireEndSound); + ep.m_flVolume = 1.0f; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + trace_t tr; + + TankTrace( barrelEnd, vecForward, gTankSpread[m_spread], tr ); + + const CBaseEntity * ent = NULL; + if ( g_pGameRules->IsMultiplayer() ) + { + // temp remove suppress host + ent = te->GetSuppressHost(); + te->SetSuppressHost( NULL ); + } + + ExplosionCreate( tr.endpos, GetAbsAngles(), this, m_Magnitude, 0, true ); + + if ( g_pGameRules->IsMultiplayer() ) + { + te->SetSuppressHost( (CBaseEntity *) ent ); + } + + BaseClass::Fire( bulletCount, barrelEnd, vecForward, this ); +} + +//----------------------------------------------------------------------------- +// Purpose: Func tank that fires physics cannisters placed on it +//----------------------------------------------------------------------------- +class CFuncTankPhysCannister : public CFuncTank +{ +public: + DECLARE_CLASS( CFuncTankPhysCannister, CFuncTank ); + DECLARE_DATADESC(); + + void Activate( void ); + void Fire( int bulletCount, const Vector &barrelEnd, const Vector &forward, CBaseEntity *pAttacker ); + +protected: + string_t m_iszBarrelVolume; + CHandle<CBaseTrigger> m_hBarrelVolume; +}; + +LINK_ENTITY_TO_CLASS( func_tankphyscannister, CFuncTankPhysCannister ); + +BEGIN_DATADESC( CFuncTankPhysCannister ) + + DEFINE_KEYFIELD( m_iszBarrelVolume, FIELD_STRING, "barrel_volume" ), + DEFINE_FIELD( m_hBarrelVolume, FIELD_EHANDLE ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFuncTankPhysCannister::Activate( void ) +{ + BaseClass::Activate(); + + m_hBarrelVolume = NULL; + + // Find our barrel volume + if ( m_iszBarrelVolume != NULL_STRING ) + { + m_hBarrelVolume = dynamic_cast<CBaseTrigger*>( gEntList.FindEntityByName( NULL, m_iszBarrelVolume ) ); + } + + if ( !m_hBarrelVolume ) + { + Msg("ERROR: Couldn't find barrel volume for func_tankphyscannister %s.\n", STRING(GetEntityName()) ); + UTIL_Remove( this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFuncTankPhysCannister::Fire( int bulletCount, const Vector &barrelEnd, const Vector &forward, CBaseEntity *pAttacker ) +{ + Assert( m_hBarrelVolume ); + + // Do we have a cannister in our barrel volume? + CPhysicsCannister *pCannister = (CPhysicsCannister *)m_hBarrelVolume->GetTouchedEntityOfType( "physics_cannister" ); + if ( !pCannister ) + { + // Play a no-ammo sound + return; + } + + // Fire the cannister! + pCannister->CannisterFire( pAttacker ); +} + diff --git a/game/server/hl1/hl1_grenade_mp5.cpp b/game/server/hl1/hl1_grenade_mp5.cpp new file mode 100644 index 0000000..9a72c9f --- /dev/null +++ b/game/server/hl1/hl1_grenade_mp5.cpp @@ -0,0 +1,160 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "hl1_grenade_mp5.h" +#include "hl1mp_weapon_mp5.h" +#include "soundent.h" +#include "decals.h" +#include "shake.h" +#include "smoke_trail.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "world.h" + +extern short g_sModelIndexFireball; +extern short g_sModelIndexWExplosion; + +extern ConVar sk_plr_dmg_mp5_grenade; +extern ConVar sk_max_mp5_grenade; +extern ConVar sk_mp5_grenade_radius; + +BEGIN_DATADESC( CGrenadeMP5 ) + // SR-BUGBUG: These are borked!!!! +// float m_fSpawnTime; + + // Function pointers + DEFINE_ENTITYFUNC( GrenadeMP5Touch ), + + DEFINE_FIELD( m_fSpawnTime, FIELD_TIME ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( grenade_mp5, CGrenadeMP5 ); + +void CGrenadeMP5::Spawn( void ) +{ + Precache( ); + SetSolid( SOLID_BBOX ); + SetMoveType( MOVETYPE_FLY ); + AddFlag( FL_GRENADE ); + + SetModel( "models/grenade.mdl" ); + //UTIL_SetSize(this, Vector(-3, -3, -3), Vector(3, 3, 3)); + UTIL_SetSize(this, Vector(0, 0, 0), Vector(0, 0, 0)); + + SetUse( &CBaseGrenade::DetonateUse ); + SetTouch( &CGrenadeMP5::GrenadeMP5Touch ); + SetNextThink( gpGlobals->curtime + 0.1f ); + + m_flDamage = sk_plr_dmg_mp5_grenade.GetFloat(); + m_DmgRadius = sk_mp5_grenade_radius.GetFloat(); + m_takedamage = DAMAGE_YES; + m_bIsLive = true; + m_iHealth = 1; + + SetGravity( UTIL_ScaleForGravity( 400 ) ); // use a lower gravity for grenades to make them easier to see + SetFriction( 0.8 ); + + SetSequence( 0 ); + + m_fSpawnTime = gpGlobals->curtime; +} + + +void CGrenadeMP5::Event_Killed( CBaseEntity *pInflictor, CBaseEntity *pAttacker, float flDamage, int bitsDamageType ) +{ + Detonate( ); +} + + +void CGrenadeMP5::GrenadeMP5Touch( CBaseEntity *pOther ) +{ + if ( !pOther->IsSolid() ) + return; + + // If I'm live go ahead and blow up + if (m_bIsLive) + { + Detonate(); + } + else + { + // If I'm not live, only blow up if I'm hitting an chacter that + // is not the owner of the weapon + CBaseCombatCharacter *pBCC = ToBaseCombatCharacter( pOther ); + if (pBCC && GetThrower() != pBCC) + { + m_bIsLive = true; + Detonate(); + } + } +} + +void CGrenadeMP5::Detonate(void) +{ + if (!m_bIsLive) + { + return; + } + m_bIsLive = false; + m_takedamage = DAMAGE_NO; + + CPASFilter filter( GetAbsOrigin() ); + + te->Explosion( filter, 0.0, + &GetAbsOrigin(), + GetWaterLevel() == 0 ? g_sModelIndexFireball : g_sModelIndexWExplosion, + (m_flDamage - 50) * .60, + 15, + TE_EXPLFLAG_NONE, + m_DmgRadius, + m_flDamage ); + + trace_t tr; + tr = CBaseEntity::GetTouchTrace(); + + if ( (tr.m_pEnt != GetWorldEntity()) || (tr.hitbox != 0) ) + { + // non-world needs smaller decals + UTIL_DecalTrace( &tr, "SmallScorch"); + } + else + { + UTIL_DecalTrace( &tr, "Scorch" ); + } + + CSoundEnt::InsertSound ( SOUND_COMBAT, GetAbsOrigin(), BASEGRENADE_EXPLOSION_VOLUME, 3.0 ); + + RadiusDamage ( CTakeDamageInfo( this, GetThrower(), m_flDamage, DMG_BLAST ), GetAbsOrigin(), m_flDamage * 2.5, CLASS_NONE, NULL ); + + CPASAttenuationFilter filter2( this ); + EmitSound( filter2, entindex(), "GrenadeMP5.Detonate" ); + + if ( GetWaterLevel() == 0 ) + { + int sparkCount = random->RandomInt( 0,3 ); + QAngle angles; + VectorAngles( tr.plane.normal, angles ); + + for ( int i = 0; i < sparkCount; i++ ) + Create( "spark_shower", GetAbsOrigin(), angles, NULL ); + } + + UTIL_Remove( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGrenadeMP5::Precache( void ) +{ + BaseClass::Precache(); + + PrecacheModel( "models/grenade.mdl" ); + + PrecacheScriptSound( "GrenadeMP5.Detonate" ); +} diff --git a/game/server/hl1/hl1_grenade_mp5.h b/game/server/hl1/hl1_grenade_mp5.h new file mode 100644 index 0000000..8d5c592 --- /dev/null +++ b/game/server/hl1/hl1_grenade_mp5.h @@ -0,0 +1,42 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Projectile shot from the MP5 +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef GRENADEMP5_H +#define GRENADEMP5_H + +#include "hl1_basegrenade.h" + +#define MAX_MP5_NO_COLLIDE_TIME 0.2 + +class SmokeTrail; +class CWeaponMP5; + +class CGrenadeMP5 : public CHL1BaseGrenade +{ + DECLARE_CLASS( CGrenadeMP5, CHL1BaseGrenade ); +public: + + float m_fSpawnTime; + + void Spawn( void ); + void Precache( void ); + void GrenadeMP5Touch( CBaseEntity *pOther ); + void Event_Killed( CBaseEntity *pInflictor, CBaseEntity *pAttacker, float flDamage, int bitsDamageType ); + +public: + void EXPORT Detonate(void); + + DECLARE_DATADESC(); +}; + +#endif //GRENADEMP5_H diff --git a/game/server/hl1/hl1_grenade_spit.cpp b/game/server/hl1/hl1_grenade_spit.cpp new file mode 100644 index 0000000..1dc1d84 --- /dev/null +++ b/game/server/hl1/hl1_grenade_spit.cpp @@ -0,0 +1,171 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "hl1_grenade_spit.h" +#include "soundent.h" +#include "decals.h" + +#include "smoke_trail.h" +#include "hl2_shareddefs.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" + +ConVar sk_bullsquid_dmg_spit ( "sk_bullsquid_dmg_spit", "0" ); + +BEGIN_DATADESC( CGrenadeSpit ) + + // Function pointers + DEFINE_THINKFUNC( SpitThink ), + DEFINE_ENTITYFUNC( GrenadeSpitTouch ), + + //DEFINE_FIELD( m_nSquidSpitSprite, FIELD_INTEGER ), + + DEFINE_FIELD( m_fSpitDeathTime, FIELD_TIME ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( grenade_spit, CGrenadeSpit ); + +void CGrenadeSpit::Spawn( void ) +{ + Precache( ); + SetSolid( SOLID_BBOX ); + SetMoveType( MOVETYPE_FLYGRAVITY ); + + // FIXME, if these is a sprite, then we need a base class derived from CSprite rather than + // CBaseAnimating. pev->scale becomes m_flSpriteScale in that case. + SetModel( "models/spitball_large.mdl" ); + UTIL_SetSize(this, Vector(-3, -3, -3), Vector(3, 3, 3)); + + m_nRenderMode = kRenderTransAdd; + SetRenderColor( 255, 255, 255, 255 ); + m_nRenderFX = kRenderFxNone; + + SetThink( &CGrenadeSpit::SpitThink ); + SetUse( &CBaseGrenade::DetonateUse ); + SetTouch( &CGrenadeSpit::GrenadeSpitTouch ); + SetNextThink( gpGlobals->curtime + 0.1f ); + + m_flDamage = sk_bullsquid_dmg_spit.GetFloat(); + m_DmgRadius = 60.0f; + m_takedamage = DAMAGE_YES; + m_iHealth = 1; + + SetGravity( SPIT_GRAVITY ); + SetFriction( 0.8 ); + SetSequence( 1 ); + + SetCollisionGroup( HL2COLLISION_GROUP_SPIT ); +} + + +void CGrenadeSpit::SetSpitSize(int nSize) +{ + switch (nSize) + { + case SPIT_LARGE: + { + SetModel( "models/spitball_large.mdl" ); + break; + } + case SPIT_MEDIUM: + { + SetModel( "models/spitball_medium.mdl" ); + break; + } + case SPIT_SMALL: + { + SetModel( "models/spitball_small.mdl" ); + break; + } + } +} + +void CGrenadeSpit::Event_Killed( const CTakeDamageInfo &info ) +{ + Detonate( ); +} + +void CGrenadeSpit::GrenadeSpitTouch( CBaseEntity *pOther ) +{ + if (m_fSpitDeathTime != 0) + { + return; + } + if ( pOther->GetCollisionGroup() == HL2COLLISION_GROUP_SPIT) + { + return; + } + if ( !pOther->m_takedamage ) + { + + // make a splat on the wall + trace_t tr; + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + GetAbsVelocity() * 10, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); + UTIL_DecalTrace(&tr, "BeerSplash" ); + + // make some flecks + CPVSFilter filter( tr.endpos ); + te->SpriteSpray( filter, 0.0, + &tr.endpos, &tr.plane.normal, m_nSquidSpitSprite, random->RandomInt( 90, 160 ), 50, random->RandomInt ( 5, 15 ) ); + } + else + { + RadiusDamage ( CTakeDamageInfo( this, GetThrower(), m_flDamage, DMG_BLAST ), GetAbsOrigin(), m_DmgRadius, CLASS_NONE, NULL ); + } + + Detonate(); +} + +void CGrenadeSpit::SpitThink( void ) +{ + if (m_fSpitDeathTime != 0 && + m_fSpitDeathTime < gpGlobals->curtime) + { + UTIL_Remove( this ); + } + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +void CGrenadeSpit::Detonate(void) +{ + m_takedamage = DAMAGE_NO; + + int iPitch; + + // splat sound + iPitch = random->RandomFloat( 90, 110 ); + + EmitSound( "GrenadeSpit.Acid" ); + EmitSound( "GrenadeSpit.Hit" ); + + UTIL_Remove( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGrenadeSpit::Precache( void ) +{ + m_nSquidSpitSprite = PrecacheModel("sprites/bigspit.vmt");// client side spittle. + + PrecacheModel("models/spitball_large.mdl"); + PrecacheModel("models/spitball_medium.mdl"); + PrecacheModel("models/spitball_small.mdl"); + + PrecacheScriptSound( "GrenadeSpit.Acid" ); + PrecacheScriptSound( "GrenadeSpit.Hit" ); + +} + + +CGrenadeSpit::CGrenadeSpit(void) +{ +} diff --git a/game/server/hl1/hl1_grenade_spit.h b/game/server/hl1/hl1_grenade_spit.h new file mode 100644 index 0000000..70ff6c7 --- /dev/null +++ b/game/server/hl1/hl1_grenade_spit.h @@ -0,0 +1,49 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Projectile shot by bullsquid +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef GRENADESPIT_H +#define GRENADESPIT_H + +#include "hl1_basegrenade.h" + +enum SpitSize_e +{ + SPIT_SMALL, + SPIT_MEDIUM, + SPIT_LARGE, +}; + +#define SPIT_GRAVITY 0.9 + +class CGrenadeSpit : public CHL1BaseGrenade +{ +public: + DECLARE_CLASS( CGrenadeSpit, CHL1BaseGrenade ); + + void Spawn( void ); + void Precache( void ); + void SpitThink( void ); + void GrenadeSpitTouch( CBaseEntity *pOther ); + void Event_Killed( const CTakeDamageInfo &info ); + void SetSpitSize(int nSize); + + int m_nSquidSpitSprite; + float m_fSpitDeathTime; // If non-zero won't detonate + + void EXPORT Detonate(void); + CGrenadeSpit(void); + + DECLARE_DATADESC(); +}; + +#endif //GRENADESPIT_H diff --git a/game/server/hl1/hl1_item_ammo.cpp b/game/server/hl1/hl1_item_ammo.cpp new file mode 100644 index 0000000..e5d44bc --- /dev/null +++ b/game/server/hl1/hl1_item_ammo.cpp @@ -0,0 +1,410 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Ammo boxes for HL1 +// +//=============================================================================// + + +#include "cbase.h" +#include "player.h" +#include "gamerules.h" +#include "items.h" +#include "hl1_items.h" + + +// ======================================================================== +// >> Crossbow bolts +// ======================================================================== +#define AMMO_CROSSBOW_GIVE 5 +#define AMMO_CROSSBOW_MODEL "models/w_crossbow_clip.mdl" + +class CCrossbowAmmo : public CHL1Item +{ +public: + DECLARE_CLASS( CCrossbowAmmo, CHL1Item ); + + void Spawn( void ) + { + Precache(); + SetModel( AMMO_CROSSBOW_MODEL ); + BaseClass::Spawn(); + } + void Precache( void ) + { + PrecacheModel( AMMO_CROSSBOW_MODEL ); + } + bool MyTouch( CBasePlayer *pPlayer ) + { + if (pPlayer->GiveAmmo( AMMO_CROSSBOW_GIVE, "XBowBolt" ) ) + { + if ( g_pGameRules->ItemShouldRespawn( this ) == GR_ITEM_RESPAWN_NO ) + { + UTIL_Remove( this ); + } + return true; + } + return false; + } +}; +LINK_ENTITY_TO_CLASS(ammo_crossbow, CCrossbowAmmo); +PRECACHE_REGISTER(ammo_crossbow); + + +// ======================================================================== +// >> Egon ammo +// ======================================================================== +#define AMMO_EGON_GIVE 20 +#define AMMO_EGON_MODEL "models/w_chainammo.mdl" + +class CEgonAmmo : public CHL1Item +{ +public: + DECLARE_CLASS( CEgonAmmo, CHL1Item ); + + void Spawn( void ) + { + Precache(); + SetModel( AMMO_EGON_MODEL ); + BaseClass::Spawn(); + } + void Precache( void ) + { + PrecacheModel ( AMMO_EGON_MODEL ); + } + bool MyTouch( CBasePlayer *pPlayer ) + { + if (pPlayer->GiveAmmo( AMMO_EGON_GIVE, "Uranium" ) ) + { + if ( g_pGameRules->ItemShouldRespawn( this ) == GR_ITEM_RESPAWN_NO ) + { + UTIL_Remove( this ); + } + return true; + } + return false; + } +}; +LINK_ENTITY_TO_CLASS(ammo_egonclip, CEgonAmmo); +PRECACHE_REGISTER(ammo_egonclip); + + +// ======================================================================== +// >> Gauss ammo +// ======================================================================== +#define AMMO_GAUSS_GIVE 20 +#define AMMO_GAUSS_MODEL "models/w_gaussammo.mdl" + +class CGaussAmmo : public CHL1Item +{ +public: + DECLARE_CLASS( CGaussAmmo, CHL1Item ); + + void Spawn( void ) + { + Precache(); + SetModel( AMMO_GAUSS_MODEL ); + BaseClass::Spawn(); + } + void Precache( void ) + { + PrecacheModel ( AMMO_GAUSS_MODEL ); + } + bool MyTouch( CBasePlayer *pPlayer ) + { + if (pPlayer->GiveAmmo( AMMO_GAUSS_GIVE, "Uranium" ) ) + { + if ( g_pGameRules->ItemShouldRespawn( this ) == GR_ITEM_RESPAWN_NO ) + { + UTIL_Remove( this ); + } + return true; + } + return false; + } +}; +LINK_ENTITY_TO_CLASS(ammo_gaussclip, CGaussAmmo); +PRECACHE_REGISTER(ammo_gaussclip); + + +// ======================================================================== +// >> Glock ammo +// ======================================================================== +#define AMMO_GLOCK_GIVE 18 +#define AMMO_GLOCK_MODEL "models/w_9mmclip.mdl" + +class CGlockAmmo : public CHL1Item +{ +public: + DECLARE_CLASS( CGlockAmmo, CHL1Item ); + + void Spawn( void ) + { + Precache(); + SetModel( AMMO_GLOCK_MODEL ); + BaseClass::Spawn(); + } + void Precache( void ) + { + PrecacheModel ( AMMO_GLOCK_MODEL ); + } + bool MyTouch( CBasePlayer *pPlayer ) + { + if (pPlayer->GiveAmmo( AMMO_GLOCK_GIVE, "9mmRound" ) ) + { + if ( g_pGameRules->ItemShouldRespawn( this ) == GR_ITEM_RESPAWN_NO ) + { + UTIL_Remove( this ); + } + return true; + } + return false; + } +}; +LINK_ENTITY_TO_CLASS(ammo_glockclip, CGlockAmmo); +PRECACHE_REGISTER(ammo_glockclip); +LINK_ENTITY_TO_CLASS(ammo_9mmclip, CGlockAmmo); +PRECACHE_REGISTER(ammo_9mmclip); + + +// ======================================================================== +// >> MP5 ammo +// ======================================================================== +#define AMMO_MP5_GIVE 50 +#define AMMO_MP5_MODEL "models/w_9mmARclip.mdl" + +class CMP5AmmoClip : public CHL1Item +{ +public: + DECLARE_CLASS( CMP5AmmoClip, CHL1Item ); + + void Spawn( void ) + { + Precache(); + SetModel( AMMO_MP5_MODEL ); + BaseClass::Spawn(); + } + void Precache( void ) + { + PrecacheModel ( AMMO_MP5_MODEL ); + } + bool MyTouch( CBasePlayer *pPlayer ) + { + if (pPlayer->GiveAmmo( AMMO_MP5_GIVE, "9mmRound" ) ) + { + if ( g_pGameRules->ItemShouldRespawn( this ) == GR_ITEM_RESPAWN_NO ) + { + UTIL_Remove( this ); + } + return true; + } + return false; + } +}; +LINK_ENTITY_TO_CLASS(ammo_mp5clip, CMP5AmmoClip); +PRECACHE_REGISTER(ammo_mp5clip); +LINK_ENTITY_TO_CLASS(ammo_9mmar, CMP5AmmoClip); +PRECACHE_REGISTER(ammo_9mmar); + + +// ======================================================================== +// >> MP5Chain (?) ammo +// ======================================================================== +#define AMMO_MP5CHAIN_GIVE 200 +#define AMMO_MP5CHAIN_MODEL "models/w_chainammo.mdl" + +class CMP5Chainammo : public CHL1Item +{ +public: + DECLARE_CLASS( CMP5Chainammo, CHL1Item ); + + void Spawn( void ) + { + Precache(); + SetModel( AMMO_MP5CHAIN_MODEL ); + BaseClass::Spawn(); + } + void Precache( void ) + { + PrecacheModel ( AMMO_MP5CHAIN_MODEL ); + } + bool MyTouch( CBasePlayer *pPlayer ) + { + if (pPlayer->GiveAmmo( AMMO_MP5CHAIN_GIVE, "9mmRound" ) ) + { + if ( g_pGameRules->ItemShouldRespawn( this ) == GR_ITEM_RESPAWN_NO ) + { + UTIL_Remove( this ); + } + return true; + } + return false; + } +}; +LINK_ENTITY_TO_CLASS(ammo_9mmbox, CMP5Chainammo); +PRECACHE_REGISTER(ammo_9mmbox); + + +// ======================================================================== +// >> MP5 grenades +// ======================================================================== +#define AMMO_MP5GRENADE_GIVE 2 +#define AMMO_MP5GRENADE_MODEL "models/w_ARgrenade.mdl" + +class CMP5AmmoGrenade : public CHL1Item +{ +public: + DECLARE_CLASS( CMP5AmmoGrenade, CHL1Item ); + + void Spawn( void ) + { + Precache(); + SetModel( AMMO_MP5GRENADE_MODEL ); + BaseClass::Spawn(); + } + void Precache( void ) + { + PrecacheModel ( AMMO_MP5GRENADE_MODEL ); + } + bool MyTouch( CBasePlayer *pPlayer ) + { + if (pPlayer->GiveAmmo( AMMO_MP5GRENADE_GIVE, "MP5_Grenade" ) ) + { + if ( g_pGameRules->ItemShouldRespawn( this ) == GR_ITEM_RESPAWN_NO ) + { + UTIL_Remove( this ); + } + return true; + } + return false; + } +}; +LINK_ENTITY_TO_CLASS(ammo_mp5grenades, CMP5AmmoGrenade); +PRECACHE_REGISTER(ammo_mp5grenades); +LINK_ENTITY_TO_CLASS(ammo_argrenades, CMP5AmmoGrenade); +PRECACHE_REGISTER(ammo_argrenades); + + +// ======================================================================== +// >> 357 ammo +// ======================================================================== +#define AMMO_357_GIVE 6 +#define AMMO_357_MODEL "models/w_357ammobox.mdl" + +class CPythonAmmo : public CHL1Item +{ +public: + DECLARE_CLASS( CPythonAmmo, CHL1Item ); + + void Spawn( void ) + { + Precache(); + SetModel( AMMO_357_MODEL ); + BaseClass::Spawn(); + } + void Precache( void ) + { + PrecacheModel ( AMMO_357_MODEL ); + } + bool MyTouch( CBasePlayer *pPlayer ) + { + if (pPlayer->GiveAmmo( AMMO_357_GIVE, "357Round" ) ) + { + if ( g_pGameRules->ItemShouldRespawn( this ) == GR_ITEM_RESPAWN_NO ) + { + UTIL_Remove( this ); + } + return true; + } + return false; + } +}; +LINK_ENTITY_TO_CLASS(ammo_357, CPythonAmmo); +PRECACHE_REGISTER(ammo_357); + + +// ======================================================================== +// >> RPG rockets +// ======================================================================== +#define AMMO_RPG_GIVE 1 +#define AMMO_RPG_MODEL "models/w_rpgammo.mdl" + +class CRpgAmmo : public CHL1Item +{ +public: + DECLARE_CLASS( CRpgAmmo, CHL1Item ); + + void Spawn( void ) + { + Precache(); + SetModel( AMMO_RPG_MODEL ); + BaseClass::Spawn(); + } + void Precache( void ) + { + PrecacheModel ( AMMO_RPG_MODEL ); + } + bool MyTouch( CBasePlayer *pPlayer ) + { + int nGive; + + if ( g_pGameRules->IsMultiplayer() ) + { + // hand out more ammo per rocket in multiplayer. + nGive = AMMO_RPG_GIVE * 2; + } + else + { + nGive = AMMO_RPG_GIVE; + } + + if (pPlayer->GiveAmmo( nGive, "RPG_Rocket" ) ) + { + if ( g_pGameRules->ItemShouldRespawn( this ) == GR_ITEM_RESPAWN_NO ) + { + UTIL_Remove( this ); + } + return true; + } + return false; + } +}; +LINK_ENTITY_TO_CLASS(ammo_rpgclip, CRpgAmmo); +PRECACHE_REGISTER(ammo_rpgclip); + + +// ======================================================================== +// >> Shotgun ammo +// ======================================================================== +#define AMMO_SHOTGUN_GIVE 12 +#define AMMO_SHOTGUN_MODEL "models/w_shotbox.mdl" + +class CShotgunAmmo : public CHL1Item +{ +public: + DECLARE_CLASS( CShotgunAmmo, CHL1Item ); + + void Spawn( void ) + { + Precache(); + SetModel( AMMO_SHOTGUN_MODEL ); + BaseClass::Spawn(); + } + void Precache( void ) + { + PrecacheModel ( AMMO_SHOTGUN_MODEL ); + } + bool MyTouch( CBasePlayer *pPlayer ) + { + if (pPlayer->GiveAmmo( AMMO_SHOTGUN_GIVE, "Buckshot" ) ) + { + if ( g_pGameRules->ItemShouldRespawn( this ) == GR_ITEM_RESPAWN_NO ) + { + UTIL_Remove( this ); + } + return true; + } + return false; + } +}; +LINK_ENTITY_TO_CLASS(ammo_buckshot, CShotgunAmmo); +PRECACHE_REGISTER(ammo_buckshot); diff --git a/game/server/hl1/hl1_item_battery.cpp b/game/server/hl1/hl1_item_battery.cpp new file mode 100644 index 0000000..6cddacb --- /dev/null +++ b/game/server/hl1/hl1_item_battery.cpp @@ -0,0 +1,78 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Handling for the suit batteries. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "player.h" +#include "basecombatweapon.h" +#include "gamerules.h" +#include "items.h" +#include "engine/IEngineSound.h" +#include "hl1_items.h" + + +#define BATTERY_MODEL "models/w_battery.mdl" + +ConVar sk_battery( "sk_battery","0" ); + +class CItemBattery : public CHL1Item +{ +public: + DECLARE_CLASS( CItemBattery, CHL1Item ); + + void Spawn( void ) + { + Precache( ); + SetModel( BATTERY_MODEL ); + BaseClass::Spawn( ); + } + void Precache( void ) + { + PrecacheModel( BATTERY_MODEL ); + + PrecacheScriptSound( "Item.Pickup" ); + } + + bool MyTouch( CBasePlayer *pPlayer ) + { + if ((pPlayer->ArmorValue() < MAX_NORMAL_BATTERY) && pPlayer->IsSuitEquipped()) + { + int pct; + char szcharge[64]; + + pPlayer->IncrementArmorValue( sk_battery.GetFloat(), MAX_NORMAL_BATTERY ); + + CPASAttenuationFilter filter( pPlayer, "Item.Pickup" ); + EmitSound( filter, pPlayer->entindex(), "Item.Pickup" ); + + CSingleUserRecipientFilter user( pPlayer ); + user.MakeReliable(); + + UserMessageBegin( user, "ItemPickup" ); + WRITE_STRING( GetClassname() ); + MessageEnd(); + + + // Suit reports new power level + // For some reason this wasn't working in release build -- round it. + pct = (int)( (float)(pPlayer->ArmorValue() * 100.0) * (1.0/MAX_NORMAL_BATTERY) + 0.5); + pct = (pct / 5); + if (pct > 0) + pct--; + + Q_snprintf( szcharge,sizeof(szcharge),"!HEV_%1dP", pct ); + + //UTIL_EmitSoundSuit(edict(), szcharge); + pPlayer->SetSuitUpdate(szcharge, FALSE, SUIT_NEXT_IN_30SEC); + return true; + } + return false; + } +}; + +LINK_ENTITY_TO_CLASS(item_battery, CItemBattery); +PRECACHE_REGISTER(item_battery); + diff --git a/game/server/hl1/hl1_item_healthkit.cpp b/game/server/hl1/hl1_item_healthkit.cpp new file mode 100644 index 0000000..a5834d9 --- /dev/null +++ b/game/server/hl1/hl1_item_healthkit.cpp @@ -0,0 +1,384 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "gamerules.h" +#include "player.h" +#include "items.h" +#include "engine/IEngineSound.h" +#include "hl1_items.h" +#include "in_buttons.h" + + +ConVar sk_healthkit( "sk_healthkit","0" ); +ConVar sk_healthvial( "sk_healthvial","0" ); +ConVar sk_healthcharger( "sk_healthcharger","0" ); + +//----------------------------------------------------------------------------- +// Small health kit. Heals the player when picked up. +//----------------------------------------------------------------------------- +class CHealthKit : public CHL1Item +{ +public: + DECLARE_CLASS( CHealthKit, CHL1Item ); + + void Spawn( void ); + void Precache( void ); + bool MyTouch( CBasePlayer *pPlayer ); +}; + +LINK_ENTITY_TO_CLASS( item_healthkit, CHealthKit ); +PRECACHE_REGISTER(item_healthkit); + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CHealthKit::Spawn( void ) +{ + Precache(); + SetModel( "models/w_medkit.mdl" ); + + BaseClass::Spawn(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CHealthKit::Precache( void ) +{ + PrecacheModel("models/w_medkit.mdl"); + + PrecacheScriptSound( "HealthKit.Touch" ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPlayer - +// Output : +//----------------------------------------------------------------------------- +bool CHealthKit::MyTouch( CBasePlayer *pPlayer ) +{ + if ( pPlayer->TakeHealth( sk_healthkit.GetFloat(), DMG_GENERIC ) ) + { + CSingleUserRecipientFilter user( pPlayer ); + user.MakeReliable(); + + UserMessageBegin( user, "ItemPickup" ); + WRITE_STRING( GetClassname() ); + MessageEnd(); + + CPASAttenuationFilter filter( pPlayer, "HealthKit.Touch" ); + EmitSound( filter, pPlayer->entindex(), "HealthKit.Touch" ); + + if ( g_pGameRules->ItemShouldRespawn( this ) ) + { + Respawn(); + } + else + { + UTIL_Remove(this); + } + + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Small dynamically dropped health kit +//----------------------------------------------------------------------------- + +class CHealthVial : public CHL1Item +{ +public: + DECLARE_CLASS( CHealthVial, CHL1Item ); + + void Spawn( void ) + { + Precache(); + SetModel( "models/healthvial.mdl" ); + + BaseClass::Spawn(); + } + + void Precache( void ) + { + PrecacheModel("models/healthvial.mdl"); + + PrecacheScriptSound( "HealthVial.Touch" ); + } + + bool MyTouch( CBasePlayer *pPlayer ) + { + if ( pPlayer->TakeHealth( sk_healthvial.GetFloat(), DMG_GENERIC ) ) + { + CSingleUserRecipientFilter user( pPlayer ); + user.MakeReliable(); + + UserMessageBegin( user, "ItemPickup" ); + WRITE_STRING( GetClassname() ); + MessageEnd(); + + CPASAttenuationFilter filter( pPlayer, "HealthVial.Touch" ); + EmitSound( filter, pPlayer->entindex(), "HealthVial.Touch" ); + + if ( g_pGameRules->ItemShouldRespawn( this ) ) + { + Respawn(); + } + else + { + UTIL_Remove(this); + } + + return true; + } + + return false; + } +}; + +LINK_ENTITY_TO_CLASS( item_healthvial, CHealthVial ); +PRECACHE_REGISTER( item_healthvial ); + +//----------------------------------------------------------------------------- +// Wall mounted health kit. Heals the player when used. +//----------------------------------------------------------------------------- +class CWallHealth : public CBaseToggle +{ +public: + DECLARE_CLASS( CWallHealth, CBaseToggle ); + + void Spawn( ); + void Precache( void ); + bool CreateVPhysics(void); + void Off(void); + void Recharge(void); + bool KeyValue( const char *szKeyName, const char *szValue ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() | m_iCaps; } + + float m_flNextCharge; + int m_iReactivate ; // DeathMatch Delay until reactvated + int m_iJuice; + int m_iOn; // 0 = off, 1 = startup, 2 = going + float m_flSoundTime; + int m_iCaps; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(func_healthcharger, CWallHealth); + + +BEGIN_DATADESC( CWallHealth ) + + DEFINE_FIELD( m_flNextCharge, FIELD_TIME), + DEFINE_FIELD( m_iReactivate, FIELD_INTEGER), + DEFINE_FIELD( m_iJuice, FIELD_INTEGER), + DEFINE_FIELD( m_iOn, FIELD_INTEGER), + DEFINE_FIELD( m_flSoundTime, FIELD_TIME), + DEFINE_FIELD( m_iCaps, FIELD_INTEGER ), + + // Function Pointers + DEFINE_FUNCTION( Off ), + DEFINE_FUNCTION( Recharge ), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pkvd - +//----------------------------------------------------------------------------- +bool CWallHealth::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "style") || + FStrEq(szKeyName, "height") || + FStrEq(szKeyName, "value1") || + FStrEq(szKeyName, "value2") || + FStrEq(szKeyName, "value3")) + { + return(true); + } + else if (FStrEq(szKeyName, "dmdelay")) + { + m_iReactivate = atoi(szValue); + return(true); + } + + return(BaseClass::KeyValue( szKeyName, szValue )); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWallHealth::Spawn(void) +{ + Precache( ); + + SetSolid( SOLID_BSP ); + SetMoveType( MOVETYPE_PUSH ); + + SetModel( STRING( GetModelName() ) ); + + m_iJuice = sk_healthcharger.GetFloat(); + SetTextureFrameIndex( 0 ); + + m_iCaps = FCAP_CONTINUOUS_USE; + + CreateVPhysics(); +} + +//----------------------------------------------------------------------------- + +bool CWallHealth::CreateVPhysics(void) +{ + VPhysicsInitStatic(); + return true; + +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWallHealth::Precache(void) +{ + PrecacheScriptSound( "WallHealth.Deny" ); + PrecacheScriptSound( "WallHealth.Start" ); + PrecacheScriptSound( "WallHealth.LoopingContinueCharge" ); + PrecacheScriptSound( "WallHealth.Recharge" ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pActivator - +// *pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CWallHealth::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + // Make sure that we have a caller + if (!pActivator) + return; + // if it's not a player, ignore + if ( !pActivator->IsPlayer() ) + return; + + // Reset to a state of continuous use. + m_iCaps = FCAP_CONTINUOUS_USE; + + // if there is no juice left, turn it off + if (m_iJuice <= 0) + { + Off(); + SetTextureFrameIndex( 1 ); + } + + CBasePlayer *pPlayer = ToBasePlayer( pActivator ); + + // if the player doesn't have the suit, or there is no juice left, make the deny noise. + if ((m_iJuice <= 0) || (!(pPlayer->m_Local.m_bWearingSuit))) + { + if (m_flSoundTime <= gpGlobals->curtime) + { + m_flSoundTime = gpGlobals->curtime + 0.62; + EmitSound( "WallHealth.Deny" ); + } + return; + } + + if( pActivator->GetHealth() >= pActivator->GetMaxHealth() ) + { + CBasePlayer *pPlayer = dynamic_cast<CBasePlayer *>(pActivator); + + if( pPlayer ) + { + pPlayer->m_afButtonPressed &= ~IN_USE; + } + + // Make the user re-use me to get started drawing health. + m_iCaps = FCAP_IMPULSE_USE; + + EmitSound( "WallHealth.Deny" ); + return; + } + + SetNextThink( gpGlobals->curtime + 0.25 ); + SetThink(&CWallHealth::Off); + + // Time to recharge yet? + + if (m_flNextCharge >= gpGlobals->curtime) + return; + + // Play the on sound or the looping charging sound + if (!m_iOn) + { + m_iOn++; + EmitSound( "WallHealth.Start" ); + m_flSoundTime = 0.56 + gpGlobals->curtime; + } + if ((m_iOn == 1) && (m_flSoundTime <= gpGlobals->curtime)) + { + m_iOn++; + CPASAttenuationFilter filter( this, "WallHealth.LoopingContinueCharge" ); + filter.MakeReliable(); + EmitSound( filter, entindex(), "WallHealth.LoopingContinueCharge" ); + } + + + // charge the player + if ( pActivator->TakeHealth( 1, DMG_GENERIC ) ) + { + m_iJuice--; + } + + // govern the rate of charge + m_flNextCharge = gpGlobals->curtime + 0.1; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWallHealth::Recharge(void) +{ + EmitSound( "WallHealth.Recharge" ); + m_iJuice = sk_healthcharger.GetFloat(); + SetTextureFrameIndex( 0 ); + SetThink( NULL ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWallHealth::Off(void) +{ + // Stop looping sound. + if (m_iOn > 1) + StopSound( "WallHealth.LoopingContinueCharge" ); + + m_iOn = 0; + + if ((!m_iJuice) && ( ( m_iReactivate = g_pGameRules->FlHealthChargerRechargeTime() ) > 0) ) + { + SetNextThink( gpGlobals->curtime + m_iReactivate ); + SetThink(&CWallHealth::Recharge); + } + else + SetThink( NULL ); +} + diff --git a/game/server/hl1/hl1_item_longjump.cpp b/game/server/hl1/hl1_item_longjump.cpp new file mode 100644 index 0000000..3ecfd9a --- /dev/null +++ b/game/server/hl1/hl1_item_longjump.cpp @@ -0,0 +1,61 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + + +#include "cbase.h" +#include "player.h" +#include "gamerules.h" +#include "items.h" +#include "hl1_items.h" +#include "hl1_player.h" + + +class CItemLongJump : public CHL1Item +{ +public: + DECLARE_CLASS( CItemLongJump, CHL1Item ); + + void Spawn( void ) + { + Precache( ); + SetModel( "models/w_longjump.mdl" ); + BaseClass::Spawn( ); + + CollisionProp()->UseTriggerBounds( true, 16.0f ); + } + void Precache( void ) + { + PrecacheModel ("models/w_longjump.mdl"); + } + bool MyTouch( CBasePlayer *pPlayer ) + { + CHL1_Player *pHL1Player = (CHL1_Player*)pPlayer; + + if ( pHL1Player->m_bHasLongJump == true ) + { + return false; + } + + if ( pHL1Player->IsSuitEquipped() ) + { + pHL1Player->m_bHasLongJump = true;// player now has longjump module + + CSingleUserRecipientFilter user( pHL1Player ); + user.MakeReliable(); + + UserMessageBegin( user, "ItemPickup" ); + WRITE_STRING( STRING(m_iClassname) ); + MessageEnd(); + + UTIL_EmitSoundSuit( pHL1Player->edict(), "!HEV_A1" ); // Play the longjump sound UNDONE: Kelly? correct sound? + return true; + } + return false; + } +}; + +LINK_ENTITY_TO_CLASS( item_longjump, CItemLongJump ); +PRECACHE_REGISTER(item_longjump); diff --git a/game/server/hl1/hl1_item_suit.cpp b/game/server/hl1/hl1_item_suit.cpp new file mode 100644 index 0000000..83644fe --- /dev/null +++ b/game/server/hl1/hl1_item_suit.cpp @@ -0,0 +1,61 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// +/* +===== item_suit.cpp ======================================================== + + handling for the player's suit. +*/ + +#include "cbase.h" +#include "player.h" +#include "gamerules.h" +#include "items.h" +#include "hl1_items.h" + + +#define SF_SUIT_SHORTLOGON 0x0001 + +#define SUIT_MODEL "models/w_suit.mdl" + +extern int gEvilImpulse101; + +class CItemSuit : public CHL1Item +{ +public: + DECLARE_CLASS( CItemSuit, CHL1Item ); + + void Spawn( void ) + { + Precache( ); + SetModel( SUIT_MODEL ); + BaseClass::Spawn( ); + + CollisionProp()->UseTriggerBounds( true, 12.0f ); + } + void Precache( void ) + { + PrecacheModel( SUIT_MODEL ); + } + bool MyTouch( CBasePlayer *pPlayer ) + { + if ( pPlayer->IsSuitEquipped() ) + return false; + + if( !gEvilImpulse101 ) + { + if ( HasSpawnFlags( SF_SUIT_SHORTLOGON ) ) + UTIL_EmitSoundSuit(pPlayer->edict(), "!HEV_A0"); // short version of suit logon, + else + UTIL_EmitSoundSuit(pPlayer->edict(), "!HEV_AAx"); // long version of suit logon + } + + pPlayer->EquipSuit(); + return true; + } +}; + +LINK_ENTITY_TO_CLASS(item_suit, CItemSuit); +PRECACHE_REGISTER(item_suit); diff --git a/game/server/hl1/hl1_items.cpp b/game/server/hl1/hl1_items.cpp new file mode 100644 index 0000000..3024a27 --- /dev/null +++ b/game/server/hl1/hl1_items.cpp @@ -0,0 +1,45 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + + +#include "cbase.h" +#include "player.h" +#include "items.h" +#include "gamerules.h" +#include "hl1_items.h" + + +void CHL1Item::Spawn( void ) +{ + SetMoveType( MOVETYPE_FLYGRAVITY ); + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE | FSOLID_TRIGGER ); + CollisionProp()->UseTriggerBounds( true, 24.0f ); + + SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + + SetTouch( &CItem::ItemTouch ); + +#ifdef HL1_DLL + if ( g_pGameRules->IsMultiplayer() ) + AddEffects( EF_NOSHADOW ); +#endif + + +} + + +void CHL1Item::Activate( void ) +{ + BaseClass::Activate(); + + if ( UTIL_DropToFloor( this, MASK_SOLID ) == 0 ) + { + Warning( "Item %s fell out of level at %f,%f,%f\n", GetClassname(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z); + UTIL_Remove( this ); + return; + } +} diff --git a/game/server/hl1/hl1_items.h b/game/server/hl1/hl1_items.h new file mode 100644 index 0000000..92685d8 --- /dev/null +++ b/game/server/hl1/hl1_items.h @@ -0,0 +1,26 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef HL1_ITEMS_H +#define HL1_ITEMS_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "items.h" + +class CHL1Item : public CItem +{ +public: + DECLARE_CLASS( CHL1Item, CItem ); + + void Spawn( void ); + void Activate( void ); +}; + + +#endif // HL1_ITEMS_H diff --git a/game/server/hl1/hl1_monstermaker.cpp b/game/server/hl1/hl1_monstermaker.cpp new file mode 100644 index 0000000..c7d37d2 --- /dev/null +++ b/game/server/hl1/hl1_monstermaker.cpp @@ -0,0 +1,294 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: An entity that creates NPCs in the game. +// +//=============================================================================// + +#include "cbase.h" +#include "entityapi.h" +#include "entityoutput.h" +#include "ai_basenpc.h" +#include "hl1_monstermaker.h" +#include "mapentities.h" + + +BEGIN_DATADESC( CNPCMaker ) + + DEFINE_KEYFIELD( m_iMaxNumNPCs, FIELD_INTEGER, "monstercount" ), + DEFINE_KEYFIELD( m_iMaxLiveChildren, FIELD_INTEGER, "MaxLiveChildren" ), + DEFINE_KEYFIELD( m_flSpawnFrequency, FIELD_FLOAT, "delay" ), + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + DEFINE_KEYFIELD( m_iszNPCClassname, FIELD_STRING, "monstertype" ), + + DEFINE_FIELD( m_cLiveChildren, FIELD_INTEGER ), + DEFINE_FIELD( m_flGround, FIELD_FLOAT ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Spawn", InputSpawnNPC ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + + // Outputs + DEFINE_OUTPUT( m_OnSpawnNPC, "OnSpawnNPC" ), + + // Function Pointers + DEFINE_THINKFUNC( MakerThink ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( monstermaker, CNPCMaker ); + + +//----------------------------------------------------------------------------- +// Purpose: Spawn +//----------------------------------------------------------------------------- +void CNPCMaker::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + m_cLiveChildren = 0; + Precache(); + + // If I can make an infinite number of NPC, force them to fade + if ( m_spawnflags & SF_NPCMAKER_INF_CHILD ) + { + m_spawnflags |= SF_NPCMAKER_FADE; + } + + //Start on? + if ( m_bDisabled == false ) + { + SetThink ( &CNPCMaker::MakerThink ); + SetNextThink( gpGlobals->curtime + m_flSpawnFrequency ); + } + else + { + //wait to be activated. + SetThink ( &CBaseEntity::SUB_DoNothing ); + } + m_flGround = 0; +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns whether or not it is OK to make an NPC at this instant. +//----------------------------------------------------------------------------- +bool CNPCMaker::CanMakeNPC( void ) +{ + if ( m_iMaxLiveChildren > 0 && m_cLiveChildren >= m_iMaxLiveChildren ) + {// not allowed to make a new one yet. Too many live ones out right now. + return false; + } + + if ( !m_flGround ) + { + // set altitude. Now that I'm activated, any breakables, etc should be out from under me. + trace_t tr; + + UTIL_TraceLine ( GetAbsOrigin(), GetAbsOrigin() - Vector ( 0, 0, 2048 ), MASK_NPCSOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); + m_flGround = tr.endpos.z; + } + + Vector mins = GetAbsOrigin() - Vector( 34, 34, 0 ); + Vector maxs = GetAbsOrigin() + Vector( 34, 34, 0 ); + maxs.z = GetAbsOrigin().z; + + //Only adjust for the ground if we want it + if ( ( m_spawnflags & SF_NPCMAKER_NO_DROP ) == false ) + { + mins.z = m_flGround; + } + + CBaseEntity *pList[128]; + + int count = UTIL_EntitiesInBox( pList, 128, mins, maxs, FL_CLIENT|FL_NPC ); + if ( count ) + { + //Iterate through the list and check the results + for ( int i = 0; i < count; i++ ) + { + //Don't build on top of another entity + if ( pList[i] == NULL ) + continue; + + //If one of the entities is solid, then we can't spawn now + if ( ( pList[i]->GetSolidFlags() & FSOLID_NOT_SOLID ) == false ) + return false; + } + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: If this had a finite number of children, return true if they've all +// been created. +//----------------------------------------------------------------------------- +bool CNPCMaker::IsDepleted() +{ + if ( (m_spawnflags & SF_NPCMAKER_INF_CHILD) || m_iMaxNumNPCs > 0 ) + return false; + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Toggle the spawner's state +//----------------------------------------------------------------------------- +void CNPCMaker::Toggle( void ) +{ + if ( m_bDisabled ) + { + Enable(); + } + else + { + Disable(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Start the spawner +//----------------------------------------------------------------------------- +void CNPCMaker::Enable( void ) +{ + // can't be enabled once depleted + if ( IsDepleted() ) + return; + + m_bDisabled = false; + SetThink ( &CNPCMaker::MakerThink ); + SetNextThink( gpGlobals->curtime ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Stop the spawner +//----------------------------------------------------------------------------- +void CNPCMaker::Disable( void ) +{ + m_bDisabled = true; + SetThink ( NULL ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that spawns an NPC. +//----------------------------------------------------------------------------- +void CNPCMaker::InputSpawnNPC( inputdata_t &inputdata ) +{ + MakeNPC(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input hander that starts the spawner +//----------------------------------------------------------------------------- +void CNPCMaker::InputEnable( inputdata_t &inputdata ) +{ + Enable(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input hander that stops the spawner +//----------------------------------------------------------------------------- +void CNPCMaker::InputDisable( inputdata_t &inputdata ) +{ + Disable(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input hander that toggles the spawner +//----------------------------------------------------------------------------- +void CNPCMaker::InputToggle( inputdata_t &inputdata ) +{ + Toggle(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Precache the target NPC +//----------------------------------------------------------------------------- +void CNPCMaker::Precache( void ) +{ + BaseClass::Precache(); + UTIL_PrecacheOther( STRING( m_iszNPCClassname ) ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Creates the NPC. +//----------------------------------------------------------------------------- +void CNPCMaker::MakeNPC( void ) +{ + if (!CanMakeNPC()) + { + return; + } + + CBaseEntity *pent = (CBaseEntity*)CreateEntityByName( STRING(m_iszNPCClassname) ); + + if ( !pent ) + { + Warning("NULL Ent in NPCMaker!\n" ); + return; + } + + m_OnSpawnNPC.FireOutput( this, this ); + + pent->SetLocalOrigin( GetAbsOrigin() ); + pent->SetLocalAngles( GetAbsAngles() ); + + pent->AddSpawnFlags( SF_NPC_FALL_TO_GROUND ); + + if ( m_spawnflags & SF_NPCMAKER_FADE ) + { + pent->AddSpawnFlags( SF_NPC_FADE_CORPSE ); + } + + + DispatchSpawn( pent ); + pent->SetOwnerEntity( this ); + + m_cLiveChildren++;// count this NPC + + if (!(m_spawnflags & SF_NPCMAKER_INF_CHILD)) + { + m_iMaxNumNPCs--; + + if ( IsDepleted() ) + { + // Disable this forever. Don't kill it because it still gets death notices + SetThink( NULL ); + SetUse( NULL ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Creates a new NPC every so often. +//----------------------------------------------------------------------------- +void CNPCMaker::MakerThink ( void ) +{ + SetNextThink( gpGlobals->curtime + m_flSpawnFrequency ); + + MakeNPC(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pVictim - +//----------------------------------------------------------------------------- +void CNPCMaker::DeathNotice( CBaseEntity *pVictim ) +{ + // ok, we've gotten the deathnotice from our child, now clear out its owner if we don't want it to fade. + m_cLiveChildren--; +} diff --git a/game/server/hl1/hl1_monstermaker.h b/game/server/hl1/hl1_monstermaker.h new file mode 100644 index 0000000..2646020 --- /dev/null +++ b/game/server/hl1/hl1_monstermaker.h @@ -0,0 +1,71 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef MONSTERMAKER_H +#define MONSTERMAKER_H +#ifdef _WIN32 +#pragma once +#endif + +#include "cbase.h" + + +//----------------------------------------------------------------------------- +// Spawnflags +//----------------------------------------------------------------------------- +#define SF_NPCMAKER_START_ON 1 // start active ( if has targetname ) +#define SF_NPCMAKER_NPCCLIP 8 // Children are blocked by NPCclip +#define SF_NPCMAKER_FADE 16 // Children's corpses fade +#define SF_NPCMAKER_INF_CHILD 32 // Infinite number of children +#define SF_NPCMAKER_NO_DROP 64 // Do not adjust for the ground's position when checking for spawn + + +class CNPCMaker : public CBaseEntity +{ +public: + DECLARE_CLASS( CNPCMaker, CBaseEntity ); + + CNPCMaker(void) {} + + void Spawn( void ); + void Precache( void ); + + void MakerThink( void ); + bool CanMakeNPC( void ); + + void DeathNotice( CBaseEntity *pChild );// NPC maker children use this to tell the NPC maker that they have died. + void MakeNPC( void ); + + // Input handlers + void InputSpawnNPC( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + + // State changers + void Toggle( void ); + void Enable( void ); + void Disable( void ); + + bool IsDepleted(); + + DECLARE_DATADESC(); + + int m_iMaxNumNPCs; // max number of NPCs this ent can create + float m_flSpawnFrequency; // delay (in secs) between spawns + int m_iMaxLiveChildren; // max number of NPCs that this maker may have out at one time. + string_t m_iszNPCClassname; // classname of the NPC(s) that will be created. + + COutputEvent m_OnSpawnNPC; + + int m_cLiveChildren;// how many NPCs made by this NPC maker that are currently alive + + float m_flGround; // z coord of the ground under me, used to make sure no NPCs are under the maker when it drops a new child + bool m_bDisabled; +}; + + +#endif // MONSTERMAKER_H diff --git a/game/server/hl1/hl1_npc_aflock.cpp b/game/server/hl1/hl1_npc_aflock.cpp new file mode 100644 index 0000000..01b1c76 --- /dev/null +++ b/game/server/hl1/hl1_npc_aflock.cpp @@ -0,0 +1,858 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Bullseyes act as targets for other NPC's to attack and to trigger +// events +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_route.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "basecombatweapon.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "hl1_ai_basenpc.h" + +#define AFLOCK_MAX_RECRUIT_RADIUS 1024 +#define AFLOCK_FLY_SPEED 125 +#define AFLOCK_TURN_RATE 75 +#define AFLOCK_ACCELERATE 10 +#define AFLOCK_CHECK_DIST 192 +#define AFLOCK_TOO_CLOSE 100 +#define AFLOCK_TOO_FAR 256 + +//========================================================= +//========================================================= +class CNPC_FlockingFlyerFlock : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_FlockingFlyerFlock, CHL1BaseNPC ); +public: + + void Spawn( void ); + void Precache( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + void SpawnFlock( void ); + + // Sounds are shared by the flock + static void PrecacheFlockSounds( void ); + + DECLARE_DATADESC(); + + int m_cFlockSize; + float m_flFlockRadius; +}; + +BEGIN_DATADESC( CNPC_FlockingFlyerFlock ) + DEFINE_FIELD( m_cFlockSize, FIELD_INTEGER ), + DEFINE_FIELD( m_flFlockRadius, FIELD_FLOAT ), +END_DATADESC() + +class CNPC_FlockingFlyer : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_FlockingFlyer, CHL1BaseNPC ); +public: + void Spawn( void ); + void Precache( void ); + void SpawnCommonCode( void ); + void IdleThink( void ); + void BoidAdvanceFrame( void ); + void Start( void ); + bool FPathBlocked( void ); + void FlockLeaderThink( void ); + void SpreadFlock( void ); + void SpreadFlock2( void ); + void MakeSound( void ); + void FlockFollowerThink( void ); + void Event_Killed( const CTakeDamageInfo &info ); + void FallHack( void ); + //void Poop ( void ); Adrian - wtf?! + + + + int IsLeader( void ) { return m_pSquadLeader == this; } + int InSquad( void ) { return m_pSquadLeader != NULL; } + int SquadCount( void ); + void SquadRemove( CNPC_FlockingFlyer *pRemove ); + void SquadUnlink( void ); + void SquadAdd( CNPC_FlockingFlyer *pAdd ); + void SquadDisband( void ); + + CNPC_FlockingFlyer *m_pSquadLeader; + CNPC_FlockingFlyer *m_pSquadNext; + bool m_fTurning;// is this boid turning? + bool m_fCourseAdjust;// followers set this flag TRUE to override flocking while they avoid something + bool m_fPathBlocked;// TRUE if there is an obstacle ahead + Vector m_vecReferencePoint;// last place we saw leader + Vector m_vecAdjustedVelocity;// adjusted velocity (used when fCourseAdjust is TRUE) + float m_flGoalSpeed; + float m_flLastBlockedTime; + float m_flFakeBlockedTime; + float m_flAlertTime; + float m_flFlockNextSoundTime; + float m_flTempVar; + + DECLARE_DATADESC(); +}; + +BEGIN_DATADESC( CNPC_FlockingFlyer ) + DEFINE_FIELD( m_pSquadLeader, FIELD_CLASSPTR ), + DEFINE_FIELD( m_pSquadNext, FIELD_CLASSPTR ), + DEFINE_FIELD( m_fTurning, FIELD_BOOLEAN ), + DEFINE_FIELD( m_fCourseAdjust, FIELD_BOOLEAN ), + DEFINE_FIELD( m_fPathBlocked, FIELD_BOOLEAN ), + DEFINE_FIELD( m_vecReferencePoint, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecAdjustedVelocity, FIELD_VECTOR ), + DEFINE_FIELD( m_flGoalSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_flLastBlockedTime, FIELD_TIME ), + DEFINE_FIELD( m_flFakeBlockedTime, FIELD_TIME ), + DEFINE_FIELD( m_flAlertTime, FIELD_TIME ), + DEFINE_THINKFUNC( IdleThink ), + DEFINE_THINKFUNC( Start ), + DEFINE_THINKFUNC( FlockLeaderThink ), + DEFINE_THINKFUNC( FlockFollowerThink ), + DEFINE_THINKFUNC( FallHack ), + + DEFINE_FIELD( m_flFlockNextSoundTime, FIELD_TIME ), + DEFINE_FIELD( m_flTempVar, FIELD_FLOAT ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( monster_flyer, CNPC_FlockingFlyer ); +LINK_ENTITY_TO_CLASS( monster_flyer_flock, CNPC_FlockingFlyerFlock ); + +bool CNPC_FlockingFlyerFlock::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "iFlockSize" ) ) + { + m_cFlockSize = atoi( szValue ); + return true; + } + else if ( FStrEq( szKeyName, "flFlockRadius" ) ) + { + m_flFlockRadius = atof( szValue ); + return true; + } + else + BaseClass::KeyValue( szKeyName, szValue ); + + return false; +} + +//========================================================= +//========================================================= +void CNPC_FlockingFlyerFlock::Spawn( void ) +{ + Precache( ); + + SetRenderColor( 255, 255, 255, 255 ); + SpawnFlock(); + + + SetThink( &CBaseEntity::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +//========================================================= +//========================================================= +void CNPC_FlockingFlyerFlock::Precache( void ) +{ + //PRECACHE_MODEL("models/aflock.mdl"); + PrecacheModel("models/boid.mdl"); + + PrecacheFlockSounds(); +} + +void CNPC_FlockingFlyerFlock::SpawnFlock( void ) +{ + float R = m_flFlockRadius; + int iCount; + Vector vecSpot; + CNPC_FlockingFlyer *pBoid, *pLeader; + + pLeader = pBoid = NULL; + + for ( iCount = 0 ; iCount < m_cFlockSize ; iCount++ ) + { + pBoid = CREATE_ENTITY( CNPC_FlockingFlyer, "monster_flyer" ); + + if ( !pLeader ) + { + // make this guy the leader. + pLeader = pBoid; + + pLeader->m_pSquadLeader = pLeader; + pLeader->m_pSquadNext = NULL; + } + + vecSpot.x = random->RandomFloat( -R, R ); + vecSpot.y = random->RandomFloat( -R, R ); + vecSpot.z = random->RandomFloat( 0, 16 ); + vecSpot = GetAbsOrigin() + vecSpot; + + UTIL_SetOrigin( pBoid, vecSpot); + pBoid->SetMoveType( MOVETYPE_FLY ); + pBoid->SpawnCommonCode(); + pBoid->SetGroundEntity( NULL ); + pBoid->SetAbsVelocity( Vector ( 0, 0, 0 ) ); + pBoid->SetAbsAngles( GetAbsAngles() ); + + pBoid->SetCycle( 0 ); + pBoid->SetThink( &CNPC_FlockingFlyer::IdleThink ); + pBoid->SetNextThink( gpGlobals->curtime + 0.2 ); + + if ( pBoid != pLeader ) + { + pLeader->SquadAdd( pBoid ); + } + } +} + + +void CNPC_FlockingFlyerFlock::PrecacheFlockSounds( void ) +{ +} + +//========================================================= +//========================================================= +void CNPC_FlockingFlyer::Spawn( ) +{ + Precache( ); + SpawnCommonCode(); + + SetCycle( 0 ); + SetNextThink( gpGlobals->curtime + 0.1f ); + SetThink( &CNPC_FlockingFlyer::IdleThink ); +} + +//========================================================= +//========================================================= +void CNPC_FlockingFlyer::SpawnCommonCode( ) +{ + m_lifeState = LIFE_ALIVE; + SetClassname( "monster_flyer" ); + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_FLY ); + m_takedamage = DAMAGE_NO; + m_iHealth = 1; + + m_fPathBlocked = FALSE;// obstacles will be detected + m_flFieldOfView = 0.2; + m_flTempVar = 0; + + //SET_MODEL(ENT(pev), "models/aflock.mdl"); + SetModel( "models/boid.mdl" ); + +// UTIL_SetSize(this, Vector(0,0,0), Vector(0,0,0)); + UTIL_SetSize(this, Vector(-5,-5,0), Vector(5,5,2)); +} + +//========================================================= +//========================================================= +void CNPC_FlockingFlyer::Precache( ) +{ + //PRECACHE_MODEL("models/aflock.mdl"); + PrecacheModel("models/boid.mdl"); + CNPC_FlockingFlyerFlock::PrecacheFlockSounds(); + + PrecacheScriptSound( "FlockingFlyer.Alert" ); + PrecacheScriptSound( "FlockingFlyer.Idle" ); +} + +//========================================================= +//========================================================= +void CNPC_FlockingFlyer::IdleThink( void ) +{ + SetNextThink( gpGlobals->curtime + 0.2 ); + + // see if there's a client in the same pvs as the monster + if ( !FNullEnt( UTIL_FindClientInPVS( edict() ) ) ) + { + SetThink( &CNPC_FlockingFlyer::Start ); + SetNextThink( gpGlobals->curtime + 0.1f ); + } +} + +//========================================================= +// +// SquadUnlink(), Unlink the squad pointers. +// +//========================================================= +void CNPC_FlockingFlyer::SquadUnlink( void ) +{ + m_pSquadLeader = NULL; + m_pSquadNext = NULL; +} + +//========================================================= +// +// SquadAdd(), add pAdd to my squad +// +//========================================================= +void CNPC_FlockingFlyer::SquadAdd( CNPC_FlockingFlyer *pAdd ) +{ + ASSERT( pAdd!=NULL ); + ASSERT( !pAdd->InSquad() ); + ASSERT( this->IsLeader() ); + + pAdd->m_pSquadNext = m_pSquadNext; + m_pSquadNext = pAdd; + pAdd->m_pSquadLeader = this; +} +//========================================================= +// +// SquadRemove(), remove pRemove from my squad. +// If I am pRemove, promote m_pSquadNext to leader +// +//========================================================= +void CNPC_FlockingFlyer::SquadRemove( CNPC_FlockingFlyer *pRemove ) +{ + ASSERT( pRemove!=NULL ); + ASSERT( this->IsLeader() ); + ASSERT( pRemove->m_pSquadLeader == this ); + + if ( SquadCount() > 2 ) + { + // Removing the leader, promote m_pSquadNext to leader + if ( pRemove == this ) + { + CNPC_FlockingFlyer *pLeader = m_pSquadNext; + + // copy the enemy LKP to the new leader + + // if ( GetEnemy() ) + // pLeader->m_vecEnemyLKP = m_vecEnemyLKP; + + if ( pLeader ) + { + CNPC_FlockingFlyer *pList = pLeader; + + while ( pList ) + { + pList->m_pSquadLeader = pLeader; + pList = pList->m_pSquadNext; + } + + } + SquadUnlink(); + } + else // removing a node + { + CNPC_FlockingFlyer *pList = this; + + // Find the node before pRemove + while ( pList->m_pSquadNext != pRemove ) + { + // assert to test valid list construction + ASSERT( pList->m_pSquadNext != NULL ); + pList = pList->m_pSquadNext; + } + // List validity + ASSERT( pList->m_pSquadNext == pRemove ); + + // Relink without pRemove + pList->m_pSquadNext = pRemove->m_pSquadNext; + + // Unlink pRemove + pRemove->SquadUnlink(); + } + } + else + SquadDisband(); +} +//========================================================= +// +// SquadCount(), return the number of members of this squad +// callable from leaders & followers +// +//========================================================= +int CNPC_FlockingFlyer::SquadCount( void ) +{ + CNPC_FlockingFlyer *pList = m_pSquadLeader; + int squadCount = 0; + while ( pList ) + { + squadCount++; + pList = pList->m_pSquadNext; + } + + return squadCount; +} + +//========================================================= +// +// SquadDisband(), Unlink all squad members +// +//========================================================= +void CNPC_FlockingFlyer::SquadDisband( void ) +{ + CNPC_FlockingFlyer *pList = m_pSquadLeader; + CNPC_FlockingFlyer *pNext; + + while ( pList ) + { + pNext = pList->m_pSquadNext; + pList->SquadUnlink(); + pList = pNext; + } +} + +//========================================================= +// Start - player enters the pvs, so get things going. +//========================================================= +void CNPC_FlockingFlyer::Start( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1f ); + + if ( IsLeader() ) + { + SetThink( &CNPC_FlockingFlyer::FlockLeaderThink ); + } + else + { + SetThink( &CNPC_FlockingFlyer::FlockFollowerThink ); + } + + SetActivity ( ACT_FLY ); + ResetSequenceInfo( ); + BoidAdvanceFrame( ); + + m_flSpeed = AFLOCK_FLY_SPEED;// no delay! +} + +//========================================================= +//========================================================= +void CNPC_FlockingFlyer::BoidAdvanceFrame ( void ) +{ + float flapspeed = ( m_flSpeed - m_flTempVar ) / AFLOCK_ACCELERATE; + m_flTempVar = m_flTempVar * .8 + m_flSpeed * .2; + + if (flapspeed < 0) flapspeed = -flapspeed; + if (flapspeed < 0.25) flapspeed = 0.25; + if (flapspeed > 1.9) flapspeed = 1.9; + + m_flPlaybackRate = flapspeed; + + QAngle angVel = GetLocalAngularVelocity(); + + // lean + angVel.x = - GetAbsAngles().x + flapspeed * 5; + + // bank + angVel.z = - GetAbsAngles().z + angVel.y; + + SetLocalAngularVelocity( angVel ); + + // pev->framerate = flapspeed; + StudioFrameAdvance(); +} + +//========================================================= +// Leader boids use this think every tenth +//========================================================= +void CNPC_FlockingFlyer::FlockLeaderThink( void ) +{ + trace_t tr; + Vector vecDist;// used for general measurements + Vector vecDir;// used for general measurements + float flLeftSide; + float flRightSide; + Vector vForward, vRight, vUp; + + + SetNextThink( gpGlobals->curtime + 0.1f ); + + AngleVectors ( GetAbsAngles(), &vForward, &vRight, &vUp ); + + // is the way ahead clear? + if ( !FPathBlocked () ) + { + // if the boid is turning, stop the trend. + if ( m_fTurning ) + { + m_fTurning = FALSE; + + QAngle angVel = GetLocalAngularVelocity(); + angVel.y = 0; + SetLocalAngularVelocity( angVel ); + } + + m_fPathBlocked = FALSE; + + if ( m_flSpeed <= AFLOCK_FLY_SPEED ) + m_flSpeed += 5; + + SetAbsVelocity( vForward * m_flSpeed ); + + BoidAdvanceFrame( ); + + return; + } + + // IF we get this far in the function, the leader's path is blocked! + m_fPathBlocked = TRUE; + + if ( !m_fTurning)// something in the way and boid is not already turning to avoid + { + // measure clearance on left and right to pick the best dir to turn + UTIL_TraceLine(GetAbsOrigin(), GetAbsOrigin() + vRight * AFLOCK_CHECK_DIST, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr); + vecDist = (tr.endpos - GetAbsOrigin()); + flRightSide = vecDist.Length(); + + UTIL_TraceLine(GetAbsOrigin(), GetAbsOrigin() - vRight * AFLOCK_CHECK_DIST, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr); + vecDist = (tr.endpos - GetAbsOrigin()); + flLeftSide = vecDist.Length(); + + // turn right if more clearance on right side + if ( flRightSide > flLeftSide ) + { + QAngle angVel = GetLocalAngularVelocity(); + angVel.y = -AFLOCK_TURN_RATE; + SetLocalAngularVelocity( angVel ); + + m_fTurning = TRUE; + } + // default to left turn :) + else if ( flLeftSide > flRightSide ) + { + QAngle angVel = GetLocalAngularVelocity(); + angVel.y = AFLOCK_TURN_RATE; + SetLocalAngularVelocity( angVel ); + + m_fTurning = TRUE; + } + else + { + // equidistant. Pick randomly between left and right. + m_fTurning = TRUE; + + QAngle angVel = GetLocalAngularVelocity(); + + if ( random->RandomInt( 0, 1 ) == 0 ) + { + angVel.y = AFLOCK_TURN_RATE; + } + else + { + angVel.y = -AFLOCK_TURN_RATE; + } + + SetLocalAngularVelocity( angVel ); + } + } + + SpreadFlock( ); + + SetAbsVelocity( vForward * m_flSpeed ); + + // check and make sure we aren't about to plow into the ground, don't let it happen + UTIL_TraceLine(GetAbsOrigin(), GetAbsOrigin() - vUp * 16, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr); + if (tr.fraction != 1.0 && GetAbsVelocity().z < 0 ) + { + Vector vecVel = GetAbsVelocity(); + vecVel.z = 0; + SetAbsVelocity( vecVel ); + } + + // maybe it did, though. + if ( GetFlags() & FL_ONGROUND ) + { + UTIL_SetOrigin( this, GetAbsOrigin() + Vector ( 0 , 0 , 1 ) ); + Vector vecVel = GetAbsVelocity(); + vecVel.z = 0; + SetAbsVelocity( vecVel ); + } + + if ( m_flFlockNextSoundTime < gpGlobals->curtime ) + { +// MakeSound(); + m_flFlockNextSoundTime = gpGlobals->curtime + random->RandomFloat( 1, 3 ); + } + + BoidAdvanceFrame( ); + + return; +} + +//========================================================= +// FBoidPathBlocked - returns TRUE if there is an obstacle ahead +//========================================================= +bool CNPC_FlockingFlyer::FPathBlocked( void ) +{ + trace_t tr; + Vector vecDist;// used for general measurements + Vector vecDir;// used for general measurements + bool fBlocked; + Vector vForward, vRight, vUp; + + if ( m_flFakeBlockedTime > gpGlobals->curtime ) + { + m_flLastBlockedTime = gpGlobals->curtime; + return TRUE; + } + + // use VELOCITY, not angles, not all boids point the direction they are flying + //vecDir = UTIL_VecToAngles( pevBoid->velocity ); + AngleVectors ( GetAbsAngles(), &vForward, &vRight, &vUp ); + + fBlocked = FALSE;// assume the way ahead is clear + + // check for obstacle ahead + UTIL_TraceLine(GetAbsOrigin(), GetAbsOrigin() + vForward * AFLOCK_CHECK_DIST, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr); + + if (tr.fraction != 1.0) + { + m_flLastBlockedTime = gpGlobals->curtime; + fBlocked = TRUE; + } + + // extra wide checks + UTIL_TraceLine(GetAbsOrigin() + vRight * 12, GetAbsOrigin() + vRight * 12 + vForward * AFLOCK_CHECK_DIST, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr); + + if (tr.fraction != 1.0) + { + m_flLastBlockedTime = gpGlobals->curtime; + fBlocked = TRUE; + } + + UTIL_TraceLine(GetAbsOrigin() - vRight * 12, GetAbsOrigin() - vRight * 12 + vForward * AFLOCK_CHECK_DIST, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr); + + if (tr.fraction != 1.0) + { + m_flLastBlockedTime = gpGlobals->curtime; + fBlocked = TRUE; + } + + if ( !fBlocked && gpGlobals->curtime - m_flLastBlockedTime > 6 ) + { + // not blocked, and it's been a few seconds since we've actually been blocked. + m_flFakeBlockedTime = gpGlobals->curtime + random->RandomInt(1, 3); + } + + return fBlocked; +} + +//========================================================= +// Searches for boids that are too close and pushes them away +//========================================================= +void CNPC_FlockingFlyer::SpreadFlock( ) +{ + Vector vecDir; + float flSpeed;// holds vector magnitude while we fiddle with the direction + + CNPC_FlockingFlyer *pList = m_pSquadLeader; + while ( pList ) + { + if ( pList != this && ( GetAbsOrigin() - pList->GetAbsOrigin() ).Length() <= AFLOCK_TOO_CLOSE ) + { + // push the other away + vecDir = ( pList->GetAbsOrigin() - GetAbsOrigin() ); + VectorNormalize( vecDir ); + + // store the magnitude of the other boid's velocity, and normalize it so we + // can average in a course that points away from the leader. + flSpeed = pList->GetAbsVelocity().Length(); + + Vector vecVel = pList->GetAbsVelocity(); + VectorNormalize( vecVel ); + pList->SetAbsVelocity( ( vecVel + vecDir ) * 0.5 * flSpeed ); + } + + pList = pList->m_pSquadNext; + } +} + +//========================================================= +// Alters the caller's course if he's too close to others +// +// This function should **ONLY** be called when Caller's velocity is normalized!! +//========================================================= +void CNPC_FlockingFlyer::SpreadFlock2 ( ) +{ + Vector vecDir; + + CNPC_FlockingFlyer *pList = m_pSquadLeader; + + while ( pList ) + { + if ( pList != this && ( GetAbsOrigin() - pList->GetAbsOrigin() ).Length() <= AFLOCK_TOO_CLOSE ) + { + vecDir = ( GetAbsOrigin() - pList->GetAbsOrigin() ); + VectorNormalize( vecDir ); + + SetAbsVelocity( ( GetAbsVelocity() + vecDir ) ); + } + + pList = pList->m_pSquadNext; + } +} + +//========================================================= +//========================================================= +void CNPC_FlockingFlyer::MakeSound( void ) +{ + if ( m_flAlertTime > gpGlobals->curtime ) + { + CPASAttenuationFilter filter1( this ); + + // make agitated sounds + EmitSound( filter1, entindex(), "FlockingFlyer.Alert" ); + return; + } + + // make normal sound + CPASAttenuationFilter filter2( this ); + + EmitSound( filter2, entindex(), "FlockingFlyer.Idle" ); +} + +//========================================================= +// follower boids execute this code when flocking +//========================================================= +void CNPC_FlockingFlyer::FlockFollowerThink( void ) +{ + Vector vecDist; + Vector vecDir; + Vector vecDirToLeader; + float flDistToLeader; + + SetNextThink( gpGlobals->curtime + 0.1f ); + + if ( IsLeader() || !InSquad() ) + { + // the leader has been killed and this flyer suddenly finds himself the leader. + SetThink ( &CNPC_FlockingFlyer::FlockLeaderThink ); + return; + } + + vecDirToLeader = ( m_pSquadLeader->GetAbsOrigin() - GetAbsOrigin() ); + flDistToLeader = vecDirToLeader.Length(); + + // match heading with leader + SetAbsAngles( m_pSquadLeader->GetAbsAngles() ); + + // + // We can see the leader, so try to catch up to it + // + if ( FInViewCone ( m_pSquadLeader ) ) + { + // if we're too far away, speed up + if ( flDistToLeader > AFLOCK_TOO_FAR ) + { + m_flGoalSpeed = m_pSquadLeader->GetAbsVelocity().Length() * 1.5; + } + + // if we're too close, slow down + else if ( flDistToLeader < AFLOCK_TOO_CLOSE ) + { + m_flGoalSpeed = m_pSquadLeader->GetAbsVelocity().Length() * 0.5; + } + } + else + { + // wait up! the leader isn't out in front, so we slow down to let him pass + m_flGoalSpeed = m_pSquadLeader->GetAbsVelocity().Length() * 0.5; + } + + SpreadFlock2(); + + Vector vecVel = GetAbsVelocity(); + m_flSpeed = vecVel.Length(); + VectorNormalize( vecVel ); + + // if we are too far from leader, average a vector towards it into our current velocity + if ( flDistToLeader > AFLOCK_TOO_FAR ) + { + VectorNormalize( vecDirToLeader ); + vecVel = (vecVel + vecDirToLeader) * 0.5; + } + + // clamp speeds and handle acceleration + if ( m_flGoalSpeed > AFLOCK_FLY_SPEED * 2 ) + { + m_flGoalSpeed = AFLOCK_FLY_SPEED * 2; + } + + if ( m_flSpeed < m_flGoalSpeed ) + { + m_flSpeed += AFLOCK_ACCELERATE; + } + else if ( m_flSpeed > m_flGoalSpeed ) + { + m_flSpeed -= AFLOCK_ACCELERATE; + } + + SetAbsVelocity( vecVel * m_flSpeed ); + + BoidAdvanceFrame( ); +} + +//========================================================= +//========================================================= +void CNPC_FlockingFlyer::Event_Killed( const CTakeDamageInfo &info ) +{ + CNPC_FlockingFlyer *pSquad; + + pSquad = (CNPC_FlockingFlyer *)m_pSquadLeader; + + while ( pSquad ) + { + pSquad->m_flAlertTime = gpGlobals->curtime + 15; + pSquad = (CNPC_FlockingFlyer *)pSquad->m_pSquadNext; + } + + if ( m_pSquadLeader ) + { + m_pSquadLeader->SquadRemove( this ); + } + + m_lifeState = LIFE_DEAD; + + m_flPlaybackRate = 0; + IncrementInterpolationFrame(); + + UTIL_SetSize( this, Vector(0,0,0), Vector(0,0,0) ); + SetMoveType( MOVETYPE_FLYGRAVITY ); + + SetThink ( &CNPC_FlockingFlyer::FallHack ); + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +void CNPC_FlockingFlyer::FallHack( void ) +{ + if ( GetFlags() & FL_ONGROUND ) + { + CBaseEntity *groundentity = GetContainingEntity( GetGroundEntity()->edict() ); + + if ( !FClassnameIs ( groundentity, "worldspawn" ) ) + { + SetGroundEntity( NULL ); + SetNextThink( gpGlobals->curtime + 0.1f ); + } + else + { + SetAbsVelocity( Vector( 0, 0, 0 ) ); + SetThink( NULL ); + } + } +} diff --git a/game/server/hl1/hl1_npc_agrunt.cpp b/game/server/hl1/hl1_npc_agrunt.cpp new file mode 100644 index 0000000..484448a --- /dev/null +++ b/game/server/hl1/hl1_npc_agrunt.cpp @@ -0,0 +1,1127 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Bullseyes act as targets for other NPC's to attack and to trigger +// events +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "ai_squadslot.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "basecombatweapon.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "te.h" +#include "hl1_ai_basenpc.h" + +ConVar sk_agrunt_health( "sk_agrunt_health", "0" ); +ConVar sk_agrunt_dmg_punch( "sk_agrunt_dmg_punch", "0" ); + +//========================================================= +// Monster's Anim Events Go Here +//========================================================= +#define AGRUNT_AE_HORNET1 ( 1 ) +#define AGRUNT_AE_HORNET2 ( 2 ) +#define AGRUNT_AE_HORNET3 ( 3 ) +#define AGRUNT_AE_HORNET4 ( 4 ) +#define AGRUNT_AE_HORNET5 ( 5 ) +// some events are set up in the QC file that aren't recognized by the code yet. +#define AGRUNT_AE_PUNCH ( 6 ) +#define AGRUNT_AE_BITE ( 7 ) + +#define AGRUNT_AE_LEFT_FOOT ( 10 ) +#define AGRUNT_AE_RIGHT_FOOT ( 11 ) + +#define AGRUNT_AE_LEFT_PUNCH ( 12 ) +#define AGRUNT_AE_RIGHT_PUNCH ( 13 ) + +#define AGRUNT_MELEE_DIST 100 + +int iAgruntMuzzleFlash; +int ACT_THREAT_DISPLAY; + +// ----------------------------------------------- +// > Squad slots +// ----------------------------------------------- +enum AGruntSquadSlot_T +{ + AGRUNT_SQUAD_SLOT_HORNET1 = LAST_SHARED_SQUADSLOT, + AGRUNT_SQUAD_SLOT_HORNET2, + AGRUNT_SQUAD_SLOT_CHASE, +}; + +enum +{ + SCHED_AGRUNT_FAIL = LAST_SHARED_SCHEDULE, + SCHED_AGRUNT_COMBAT_FAIL, + SCHED_AGRUNT_STANDOFF, + SCHED_AGRUNT_SUPPRESS_HORNET, + SCHED_AGRUNT_RANGE_ATTACK, + SCHED_AGRUNT_HIDDEN_RANGE_ATTACK, + SCHED_AGRUNT_TAKE_COVER_FROM_ENEMY, + SCHED_AGRUNT_VICTORY_DANCE, + SCHED_AGRUNT_THREAT_DISPLAY, +}; + +//========================================================= +// monster-specific tasks +//========================================================= +enum +{ + TASK_AGRUNT_SETUP_HIDE_ATTACK = LAST_SHARED_TASK, + TASK_AGRUNT_GET_PATH_TO_ENEMY_CORPSE, + TASK_AGRUNT_RANGE_ATTACK1_NOTURN, +}; + + +class CNPC_AlienGrunt : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_AlienGrunt, CHL1BaseNPC ); +public: + + void Spawn( void ); + void Precache( void ); + + float MaxYawSpeed( void ); + Class_T Classify ( void ){ return CLASS_ALIEN_MILITARY; } + int GetSoundInterests ( void ); + void HandleAnimEvent( animevent_t *pEvent ); + + void AlertSound( void ); + void DeathSound( const CTakeDamageInfo &info ); + void PainSound( const CTakeDamageInfo &info ); + void AttackSound( void ); + + bool ShouldSpeak( void ); + void PrescheduleThink ( void ); + + bool FCanCheckAttacks ( void ); + + int MeleeAttack1Conditions ( float flDot, float flDist ); + int RangeAttack1Conditions ( float flDot, float flDist ); + + void StopTalking ( void ); + + void StartTask( const Task_t *pTask ); + void RunTask( const Task_t *pTask ); + + int TranslateSchedule( int scheduleType ); //GetScheduleOfType + int SelectSchedule( void ); // GetSchedule + + void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + int IRelationPriority( CBaseEntity *pTarget ); +/* + int IRelationship( CBaseEntity *pTarget ); +*/ +public: + DECLARE_DATADESC(); + DEFINE_CUSTOM_AI; + + bool m_fCanHornetAttack; + float m_flNextHornetAttackCheck; + + float m_flNextPainTime; + + // three hacky fields for speech stuff. These don't really need to be saved. + float m_flNextSpeakTime; + float m_flNextWordTime; + float m_flDamageTime; +}; + +LINK_ENTITY_TO_CLASS( monster_alien_grunt, CNPC_AlienGrunt ); + +BEGIN_DATADESC( CNPC_AlienGrunt ) + DEFINE_FIELD( m_fCanHornetAttack, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flNextHornetAttackCheck, FIELD_TIME ), + DEFINE_FIELD( m_flNextPainTime, FIELD_TIME ), + DEFINE_FIELD( m_flNextSpeakTime, FIELD_TIME ), + DEFINE_FIELD( m_flNextWordTime, FIELD_TIME ), + DEFINE_FIELD( m_flDamageTime, FIELD_TIME ), +END_DATADESC() + +int CNPC_AlienGrunt::IRelationPriority( CBaseEntity *pTarget ) +{ + //I hate grunts more than anything. + if ( pTarget->Classify() == CLASS_HUMAN_MILITARY ) + { + if ( FClassnameIs( pTarget, "monster_human_grunt" ) ) + { + return ( BaseClass::IRelationPriority ( pTarget ) + 1 ); + } + } + + return BaseClass::IRelationPriority( pTarget ); +} + +void CNPC_AlienGrunt::Spawn() +{ + Precache(); + + SetModel( "models/agrunt.mdl"); + UTIL_SetSize( this, Vector( -32, -32, 0 ), Vector( 32, 32, 64 ) ); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + + Vector vecSurroundingMins( -32, -32, 0 ); + Vector vecSurroundingMaxs( 32, 32, 85 ); + CollisionProp()->SetSurroundingBoundsType( USE_SPECIFIED_BOUNDS, &vecSurroundingMins, &vecSurroundingMaxs ); + + SetMoveType( MOVETYPE_STEP ); + m_bloodColor = BLOOD_COLOR_GREEN; + ClearEffects(); + m_iHealth = sk_agrunt_health.GetFloat(); + m_flFieldOfView = 0.2;// indicates the width of this monster's forward view cone ( as a dotproduct result ) + m_NPCState = NPC_STATE_NONE; + + CapabilitiesClear(); + CapabilitiesAdd ( bits_CAP_SQUAD | bits_CAP_MOVE_GROUND ); + + CapabilitiesAdd(bits_CAP_INNATE_RANGE_ATTACK1 ); + + // Innate range attack for kicking + CapabilitiesAdd(bits_CAP_INNATE_MELEE_ATTACK1 ); + + m_HackedGunPos = Vector( 24, 64, 48 ); + + m_flNextSpeakTime = m_flNextWordTime = gpGlobals->curtime + 10 + random->RandomInt( 0, 10 ); + + SetHullType(HULL_WIDE_HUMAN); + SetHullSizeNormal(); + + SetRenderColor( 255, 255, 255, 255 ); + + NPCInit(); + + BaseClass::Spawn(); +} + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CNPC_AlienGrunt::Precache() +{ + PrecacheModel("models/agrunt.mdl"); + + iAgruntMuzzleFlash = PrecacheModel( "sprites/muz4.vmt" ); + + UTIL_PrecacheOther( "hornet" ); + + PrecacheScriptSound( "Weapon_Hornetgun.Single" ); + PrecacheScriptSound( "AlienGrunt.LeftFoot" ); + PrecacheScriptSound( "AlienGrunt.RightFoot" ); + PrecacheScriptSound( "AlienGrunt.AttackHit" ); + PrecacheScriptSound( "AlienGrunt.AttackMiss" ); + PrecacheScriptSound( "AlienGrunt.Die" ); + PrecacheScriptSound( "AlienGrunt.Alert" ); + PrecacheScriptSound( "AlienGrunt.Attack" ); + PrecacheScriptSound( "AlienGrunt.Pain" ); + PrecacheScriptSound( "AlienGrunt.Idle" ); +} + +float CNPC_AlienGrunt::MaxYawSpeed( void ) +{ + float ys; + + switch ( GetActivity() ) + { + case ACT_TURN_LEFT: + case ACT_TURN_RIGHT: + ys = 110; + break; + default: + ys = 100; + } + + return ys; +} + +int CNPC_AlienGrunt::GetSoundInterests ( void ) +{ + return SOUND_WORLD | + SOUND_COMBAT | + SOUND_PLAYER | + SOUND_DANGER; +} + +//========================================================= +// HandleAnimEvent - catches the monster-specific messages +// that occur when tagged animation frames are played. +// +// Returns number of events handled, 0 if none. +//========================================================= +void CNPC_AlienGrunt::HandleAnimEvent( animevent_t *pEvent ) +{ + switch( pEvent->event ) + { + case AGRUNT_AE_HORNET1: + case AGRUNT_AE_HORNET2: + case AGRUNT_AE_HORNET3: + case AGRUNT_AE_HORNET4: + case AGRUNT_AE_HORNET5: + { + // m_vecEnemyLKP should be center of enemy body + Vector vecArmPos; + QAngle angArmDir; + Vector vecDirToEnemy; + QAngle angDir; + + if (HasCondition( COND_SEE_ENEMY) && GetEnemy()) + { + Vector vecEnemyLKP = GetEnemy()->GetAbsOrigin(); + + vecDirToEnemy = ( ( vecEnemyLKP ) - GetAbsOrigin() ); + VectorAngles( vecDirToEnemy, angDir ); + VectorNormalize( vecDirToEnemy ); + } + else + { + angDir = GetAbsAngles(); + angDir.x = -angDir.x; + + Vector vForward; + AngleVectors( angDir, &vForward ); + vecDirToEnemy = vForward; + } + + DoMuzzleFlash(); + + // make angles +-180 + if (angDir.x > 180) + { + angDir.x = angDir.x - 360; + } + + // SetBlending( 0, angDir.x ); + GetAttachment( "0", vecArmPos, angArmDir ); + + vecArmPos = vecArmPos + vecDirToEnemy * 32; + + CPVSFilter filter( GetAbsOrigin() ); + te->Sprite( filter, 0.0, + &vecArmPos, iAgruntMuzzleFlash, random->RandomFloat( 0.4, 0.8 ), 128 ); + + CBaseEntity *pHornet = CBaseEntity::Create( "hornet", vecArmPos, QAngle( 0, 0, 0 ), this ); + + Vector vForward; + AngleVectors( angDir, &vForward ); + + pHornet->SetAbsVelocity( vForward * 300 ); + pHornet->SetOwnerEntity( this ); + + EmitSound( "Weapon_Hornetgun.Single" ); + + CHL1BaseNPC *pHornetMonster = (CHL1BaseNPC *)pHornet->MyNPCPointer(); + + if ( pHornetMonster ) + { + pHornetMonster->SetEnemy( GetEnemy() ); + } + } + break; + + case AGRUNT_AE_LEFT_FOOT: + // left foot + { + CPASAttenuationFilter filter2( this ); + EmitSound( filter2, entindex(), "AlienGrunt.LeftFoot" ); + } + break; + case AGRUNT_AE_RIGHT_FOOT: + // right foot + { + CPASAttenuationFilter filter3( this ); + EmitSound( filter3, entindex(), "AlienGrunt.RightFoot" ); + } + break; + + case AGRUNT_AE_LEFT_PUNCH: + { + Vector vecMins = GetHullMins(); + Vector vecMaxs = GetHullMaxs(); + vecMins.z = vecMins.x; + vecMaxs.z = vecMaxs.x; + + CBaseEntity *pHurt = CheckTraceHullAttack( AGRUNT_MELEE_DIST, vecMins, vecMaxs, sk_agrunt_dmg_punch.GetFloat(), DMG_CLUB ); + CPASAttenuationFilter filter4( this ); + + if ( pHurt ) + { + if ( pHurt->GetFlags() & ( FL_NPC | FL_CLIENT ) ) + pHurt->ViewPunch( QAngle( -25, 8, 0) ); + + Vector vRight; + AngleVectors( GetAbsAngles(), NULL, &vRight, NULL ); + + // OK to use gpGlobals without calling MakeVectors, cause CheckTraceHullAttack called it above. + if ( pHurt->IsPlayer() ) + { + // this is a player. Knock him around. + pHurt->SetAbsVelocity( pHurt->GetAbsVelocity() + vRight * 250 ); + } + + EmitSound(filter4, entindex(), "AlienGrunt.AttackHit" ); + + Vector vecArmPos; + QAngle angArmAng; + GetAttachment( 0, vecArmPos, angArmAng ); + SpawnBlood(vecArmPos, g_vecAttackDir, pHurt->BloodColor(), 25);// a little surface blood. + } + else + { + // Play a random attack miss sound + EmitSound(filter4, entindex(), "AlienGrunt.AttackMiss" ); + } + } + break; + + case AGRUNT_AE_RIGHT_PUNCH: + { + Vector vecMins = GetHullMins(); + Vector vecMaxs = GetHullMaxs(); + vecMins.z = vecMins.x; + vecMaxs.z = vecMaxs.x; + + CBaseEntity *pHurt = CheckTraceHullAttack( AGRUNT_MELEE_DIST, vecMins, vecMaxs, sk_agrunt_dmg_punch.GetFloat(), DMG_CLUB ); + CPASAttenuationFilter filter5( this ); + + if ( pHurt ) + { + if ( pHurt->GetFlags() & ( FL_NPC | FL_CLIENT ) ) + pHurt->ViewPunch( QAngle( 25, 8, 0) ); + + // OK to use gpGlobals without calling MakeVectors, cause CheckTraceHullAttack called it above. + if ( pHurt->IsPlayer() ) + { + // this is a player. Knock him around. + Vector vRight; + AngleVectors( GetAbsAngles(), NULL, &vRight, NULL ); + pHurt->SetAbsVelocity( pHurt->GetAbsVelocity() + vRight * -250 ); + } + + EmitSound( filter5, entindex(), "AlienGrunt.AttackHit" ); + + Vector vecArmPos; + QAngle angArmAng; + GetAttachment( 0, vecArmPos, angArmAng ); + SpawnBlood(vecArmPos, g_vecAttackDir, pHurt->BloodColor(), 25);// a little surface blood. + } + else + { + // Play a random attack miss sound + EmitSound( filter5, entindex(), "AlienGrunt.AttackMiss" ); + } + } + break; + + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } +} + + +//========================================================= +// DieSound +//========================================================= +void CNPC_AlienGrunt::DeathSound( const CTakeDamageInfo &info ) +{ + StopTalking(); + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "AlienGrunt.Die" ); +} + +//========================================================= +// AlertSound +//========================================================= +void CNPC_AlienGrunt::AlertSound( void ) +{ + StopTalking(); + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "AlienGrunt.Alert" ); +} + +//========================================================= +// AttackSound +//========================================================= +void CNPC_AlienGrunt::AttackSound( void ) +{ + StopTalking(); + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "AlienGrunt.Attack" ); +} + +//========================================================= +// PainSound +//========================================================= +void CNPC_AlienGrunt::PainSound( const CTakeDamageInfo &info ) +{ + if ( m_flNextPainTime > gpGlobals->curtime ) + { + return; + } + + m_flNextPainTime = gpGlobals->curtime + 0.6; + + StopTalking(); + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(),"AlienGrunt.Pain" ); +} + +//========================================================= +// ShouldSpeak - Should this agrunt be talking? +//========================================================= +bool CNPC_AlienGrunt::ShouldSpeak( void ) +{ + if ( m_flNextSpeakTime > gpGlobals->curtime ) + { + // my time to talk is still in the future. + return FALSE; + } + + if ( m_spawnflags & SF_NPC_GAG ) + { + if ( m_NPCState != NPC_STATE_COMBAT ) + { + // if gagged, don't talk outside of combat. + // if not going to talk because of this, put the talk time + // into the future a bit, so we don't talk immediately after + // going into combat + m_flNextSpeakTime = gpGlobals->curtime + 3; + return FALSE; + } + } + + return TRUE; +} + +//========================================================= +// PrescheduleThink +//========================================================= +void CNPC_AlienGrunt::PrescheduleThink ( void ) +{ + BaseClass::PrescheduleThink(); + + if ( ShouldSpeak() ) + { + if ( m_flNextWordTime < gpGlobals->curtime ) + { + // play a new sound + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "AlienGrunt.Idle" ); + + // is this word our last? + if ( random->RandomInt( 1, 10 ) <= 1 ) + { + // stop talking. + StopTalking(); + } + else + { + m_flNextWordTime = gpGlobals->curtime + random->RandomFloat( 0.5, 1 ); + } + } + } +} + +//========================================================= +// FCanCheckAttacks - this is overridden for alien grunts +// because they can use their smart weapons against unseen +// enemies. Base class doesn't attack anyone it can't see. +//========================================================= +bool CNPC_AlienGrunt::FCanCheckAttacks ( void ) +{ + if ( !HasCondition( COND_ENEMY_TOO_FAR ) ) + return true; + else + return false; +} + +//========================================================= +// CheckMeleeAttack1 - alien grunts zap the crap out of +// any enemy that gets too close. +//========================================================= +int CNPC_AlienGrunt::MeleeAttack1Conditions ( float flDot, float flDist ) +{ + if ( flDist > AGRUNT_MELEE_DIST ) + return COND_NONE; + + if ( flDot < 0.6 ) + return COND_NONE; + + if ( HasCondition ( COND_SEE_ENEMY ) && GetEnemy() != NULL ) + return COND_CAN_MELEE_ATTACK1; + + return COND_NONE; +} + +//========================================================= +// CheckRangeAttack1 +// +// !!!LATER - we may want to load balance this. Several +// tracelines are done, so we may not want to do this every +// server frame. Definitely not while firing. +//========================================================= +int CNPC_AlienGrunt::RangeAttack1Conditions ( float flDot, float flDist ) +{ + if ( gpGlobals->curtime < m_flNextHornetAttackCheck ) + { + if ( HasCondition( COND_SEE_ENEMY ) ) + { + if ( m_fCanHornetAttack == true ) + { + return COND_CAN_RANGE_ATTACK1; + } + else + { + return COND_NONE; + } + } + else + return COND_NONE; + } + + if ( flDist < AGRUNT_MELEE_DIST ) + return COND_NONE; + + if ( flDist > 1024 ) + return COND_NONE; + + if ( flDot < 0.5 ) + return COND_NONE; + + if ( HasCondition( COND_SEE_ENEMY ) ) + { + trace_t tr; + Vector vecArmPos; + QAngle angArmDir; + + // verify that a shot fired from the gun will hit the enemy before the world. + // !!!LATER - we may wish to do something different for projectile weapons as opposed to instant-hit + GetAttachment( "0", vecArmPos, angArmDir ); + UTIL_TraceLine( vecArmPos, GetEnemy()->BodyTarget( vecArmPos ), MASK_SOLID, this, COLLISION_GROUP_NONE, &tr); + + if ( tr.fraction == 1.0 || tr.m_pEnt == GetEnemy() ) + { + m_flNextHornetAttackCheck = gpGlobals->curtime + random->RandomFloat( 2, 5 ); + m_fCanHornetAttack = true; + + return COND_CAN_RANGE_ATTACK1; + } + } + + m_flNextHornetAttackCheck = gpGlobals->curtime + 0.2;// don't check for half second if this check wasn't successful + m_fCanHornetAttack = false; + return COND_NONE; +} + +//========================================================= +// StopTalking - won't speak again for 10-20 seconds. +//========================================================= +void CNPC_AlienGrunt::StopTalking( void ) +{ + m_flNextWordTime = m_flNextSpeakTime = gpGlobals->curtime + 10 + random->RandomInt(0, 10); +} + + +void CNPC_AlienGrunt::StartTask ( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_AGRUNT_RANGE_ATTACK1_NOTURN: + { + SetLastAttackTime( gpGlobals->curtime ); + ResetIdealActivity( ACT_RANGE_ATTACK1 ); + } + break; + + case TASK_AGRUNT_GET_PATH_TO_ENEMY_CORPSE: + { + Vector forward; + AngleVectors( GetAbsAngles(), &forward ); + Vector flEnemyLKP = GetEnemyLKP(); + + GetNavigator()->SetGoal( flEnemyLKP - forward * 64, AIN_CLEAR_TARGET); + + if ( GetNavigator()->SetGoal( flEnemyLKP - forward * 64, AIN_CLEAR_TARGET) ) + { + TaskComplete(); + } + else + { + Msg ( "AGruntGetPathToEnemyCorpse failed!!\n" ); + TaskFail( FAIL_NO_ROUTE ); + } + } + break; + + case TASK_AGRUNT_SETUP_HIDE_ATTACK: + // alien grunt shoots hornets back out into the open from a concealed location. + // try to find a spot to throw that gives the smart weapon a good chance of finding the enemy. + // ideally, this spot is along a line that is perpendicular to a line drawn from the agrunt to the enemy. + + CHL1BaseNPC *pEnemyMonsterPtr; + + pEnemyMonsterPtr = (CHL1BaseNPC *)GetEnemy()->MyNPCPointer(); + + if ( pEnemyMonsterPtr ) + { + Vector vecCenter, vForward, vRight, vecEnemyLKP; + QAngle angTmp; + trace_t tr; + BOOL fSkip; + + fSkip = FALSE; + vecCenter = WorldSpaceCenter(); + vecEnemyLKP = GetEnemyLKP(); + + VectorAngles( vecEnemyLKP - GetAbsOrigin(), angTmp ); + SetAbsAngles( angTmp ); + AngleVectors( GetAbsAngles(), &vForward, &vRight, NULL ); + + UTIL_TraceLine( WorldSpaceCenter() + vForward * 128, vecEnemyLKP, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr); + if ( tr.fraction == 1.0 ) + { + GetMotor()->SetIdealYawToTargetAndUpdate ( GetAbsOrigin() + vRight * 128 ); + fSkip = TRUE; + TaskComplete(); + } + + if ( !fSkip ) + { + UTIL_TraceLine( WorldSpaceCenter() - vForward * 128, vecEnemyLKP, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr); + if ( tr.fraction == 1.0 ) + { + GetMotor()->SetIdealYawToTargetAndUpdate ( GetAbsOrigin() - vRight * 128 ); + fSkip = TRUE; + TaskComplete(); + } + } + + if ( !fSkip ) + { + UTIL_TraceLine( WorldSpaceCenter() + vForward * 256, vecEnemyLKP, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr); + if ( tr.fraction == 1.0 ) + { + GetMotor()->SetIdealYawToTargetAndUpdate ( GetAbsOrigin() + vRight * 256 ); + fSkip = TRUE; + TaskComplete(); + } + } + + if ( !fSkip ) + { + UTIL_TraceLine( WorldSpaceCenter() - vForward * 256, vecEnemyLKP, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr); + if ( tr.fraction == 1.0 ) + { + GetMotor()->SetIdealYawToTargetAndUpdate ( GetAbsOrigin() - vRight * 256 ); + fSkip = TRUE; + TaskComplete(); + } + } + + if ( !fSkip ) + { + TaskFail( FAIL_NO_COVER ); + } + } + else + { + Msg ( "AGRunt - no enemy monster ptr!!!\n" ); + TaskFail( FAIL_NO_ENEMY ); + } + break; + + default: + BaseClass::StartTask ( pTask ); + break; + } +} + + +void CNPC_AlienGrunt::RunTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + // NOTE: This is obsolete. Don't use it for HL2 code + case TASK_AGRUNT_RANGE_ATTACK1_NOTURN: + { + AutoMovement( ); + + if ( IsActivityFinished() ) + { + TaskComplete(); + } + break; + } + + default: + BaseClass::RunTask( pTask ); + } +} + + + +//========================================================= +// GetSchedule - Decides which type of schedule best suits +// the monster's current state and conditions. Then calls +// monster's member function to get a pointer to a schedule +// of the proper type. +//========================================================= +int CNPC_AlienGrunt::SelectSchedule( void ) +{ + if ( HasCondition( COND_HEAR_DANGER ) ) + { + return SCHED_TAKE_COVER_FROM_BEST_SOUND; + } + + switch ( m_NPCState ) + { + case NPC_STATE_COMBAT: + { + // dead enemy + if ( HasCondition( COND_ENEMY_DEAD ) ) + { + // call base class, all code to handle dead enemies is centralized there. + return BaseClass::SelectSchedule(); + } + + if ( HasCondition( COND_NEW_ENEMY) ) + { + return SCHED_WAKE_ANGRY; + } + + // zap player! + if ( HasCondition ( COND_CAN_MELEE_ATTACK1 ) ) + { + AttackSound();// this is a total hack. Should be parto f the schedule + return SCHED_MELEE_ATTACK1; + } + + if ( HasCondition ( COND_HEAVY_DAMAGE ) ) + { + return SCHED_SMALL_FLINCH; + } + + // can attack + if ( HasCondition ( COND_CAN_RANGE_ATTACK1 ) && OccupyStrategySlotRange( AGRUNT_SQUAD_SLOT_HORNET1, AGRUNT_SQUAD_SLOT_HORNET2 ) ) + { + return SCHED_RANGE_ATTACK1; + } + + if ( OccupyStrategySlot ( AGRUNT_SQUAD_SLOT_CHASE ) ) + { + return SCHED_CHASE_ENEMY; + } + + return SCHED_STANDOFF; + } + } + + return BaseClass::SelectSchedule(); +} + +int CNPC_AlienGrunt::TranslateSchedule( int scheduleType ) +{ + switch ( scheduleType ) + { + case SCHED_TAKE_COVER_FROM_ENEMY: + return SCHED_AGRUNT_TAKE_COVER_FROM_ENEMY; + break; + + /*case SCHED_RANGE_ATTACK1: + if ( HasCondition( COND_SEE_ENEMY ) ) + return SCHED_AGRUNT_RANGE_ATTACK; + // else + // return SCHED_AGRUNT_HIDDEN_RANGE_ATTACK; + break;*/ + + case SCHED_STANDOFF: + return SCHED_AGRUNT_STANDOFF; + break; + + case SCHED_VICTORY_DANCE: + return SCHED_AGRUNT_VICTORY_DANCE; + break; + case SCHED_FAIL: + // no fail schedule specified, so pick a good generic one. + { + if ( GetEnemy() != NULL ) + { + // I have an enemy + // !!!LATER - what if this enemy is really far away and i'm chasing him? + // this schedule will make me stop, face his last known position for 2 + // seconds, and then try to move again + return SCHED_AGRUNT_COMBAT_FAIL; + } + + return SCHED_AGRUNT_FAIL; + } + break; + } + + return BaseClass::TranslateSchedule( scheduleType ); +} + +void CNPC_AlienGrunt::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + CTakeDamageInfo ainfo = info; + + float flDamage = ainfo.GetDamage(); + + if ( ptr->hitgroup == 10 && (ainfo.GetDamageType() & (DMG_BULLET | DMG_SLASH | DMG_CLUB))) + { + // hit armor + if ( m_flDamageTime != gpGlobals->curtime || (random->RandomInt(0,10) < 1) ) + { + CPVSFilter filter( ptr->endpos ); + te->ArmorRicochet( filter, 0.0, &ptr->endpos, &ptr->plane.normal ); + m_flDamageTime = gpGlobals->curtime; + } + + if ( random->RandomInt( 0, 1 ) == 0 ) + { + Vector vecTracerDir = vecDir; + + vecTracerDir.x += random->RandomFloat( -0.3, 0.3 ); + vecTracerDir.y += random->RandomFloat( -0.3, 0.3 ); + vecTracerDir.z += random->RandomFloat( -0.3, 0.3 ); + + vecTracerDir = vecTracerDir * -512; + + Vector vEndPos = ptr->endpos + vecTracerDir; + + UTIL_Tracer( ptr->endpos, vEndPos, ENTINDEX( edict() ) ); + } + + flDamage -= 20; + if (flDamage <= 0) + flDamage = 0.1;// don't hurt the monster much, but allow bits_COND_LIGHT_DAMAGE to be generated + + ainfo.SetDamage( flDamage ); + } + else + { + SpawnBlood( ptr->endpos, vecDir, BloodColor(), flDamage);// a little surface blood. + TraceBleed( flDamage, vecDir, ptr, ainfo.GetDamageType() ); + } + + AddMultiDamage( ainfo, this ); +} + + +//========================================================= +// AI Schedules Specific to this monster +//========================================================= +AI_BEGIN_CUSTOM_NPC( monster_alien_grunt, CNPC_AlienGrunt ) + + DECLARE_ACTIVITY( ACT_THREAT_DISPLAY ) + + DECLARE_TASK ( TASK_AGRUNT_SETUP_HIDE_ATTACK ) + DECLARE_TASK ( TASK_AGRUNT_GET_PATH_TO_ENEMY_CORPSE ) + DECLARE_TASK ( TASK_AGRUNT_RANGE_ATTACK1_NOTURN ) + + DECLARE_SQUADSLOT( AGRUNT_SQUAD_SLOT_HORNET1 ) + DECLARE_SQUADSLOT( AGRUNT_SQUAD_SLOT_HORNET2 ) + DECLARE_SQUADSLOT( AGRUNT_SQUAD_SLOT_CHASE ) + + //========================================================= + // Fail Schedule + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_AGRUNT_FAIL, + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT 2" + " TASK_WAIT_PVS 0" + " " + " Interrupts" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_MELEE_ATTACK1" + ) + + //========================================================= + // Combat Fail Schedule + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_AGRUNT_COMBAT_FAIL, + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT_FACE_ENEMY 2" + " TASK_WAIT_PVS 0" + " " + " Interrupts" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_MELEE_ATTACK1" + ) + + //========================================================= + // Standoff schedule. Used in combat when a monster is + // hiding in cover or the enemy has moved out of sight. + // Should we look around in this schedule? + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_AGRUNT_STANDOFF, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT_FACE_ENEMY 2" + " " + " Interrupts" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_MELEE_ATTACK1" + " COND_SEE_ENEMY" + " COND_NEW_ENEMY" + " COND_HEAR_DANGER" + ) + + //========================================================= + // Suppress + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_AGRUNT_SUPPRESS_HORNET, + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_RANGE_ATTACK1 0" + ) + + //========================================================= + // primary range attacks + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_AGRUNT_RANGE_ATTACK, + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_RANGE_ATTACK1 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_HEAVY_DAMAGE" + ) + + DEFINE_SCHEDULE + ( + SCHED_AGRUNT_HIDDEN_RANGE_ATTACK, + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_AGRUNT_STANDOFF" + " TASK_AGRUNT_SETUP_HIDE_ATTACK 0" + " TASK_STOP_MOVING 0" + " TASK_FACE_IDEAL 0" + " TASK_AGRUNT_RANGE_ATTACK1_NOTURN 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_HEAVY_DAMAGE" + " COND_HEAR_DANGER" + ) + + //========================================================= + // Take cover from enemy! Tries lateral cover before node + // cover! + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_AGRUNT_TAKE_COVER_FROM_ENEMY, + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_WAIT 0.2" + " TASK_FIND_COVER_FROM_ENEMY 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_REMEMBER MEMORY:INCOVER" + " TASK_FACE_ENEMY 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + ) + + //========================================================= + // Victory dance! + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_AGRUNT_VICTORY_DANCE, + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_AGRUNT_THREAT_DISPLAY" + " TASK_WAIT 0.2" + " TASK_AGRUNT_GET_PATH_TO_ENEMY_CORPSE 0" + " TASK_WALK_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_FACE_ENEMY 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_CROUCH" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_VICTORY_DANCE" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_VICTORY_DANCE" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_STAND" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_THREAT_DISPLAY" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_CROUCH" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_VICTORY_DANCE" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_VICTORY_DANCE" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_VICTORY_DANCE" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_VICTORY_DANCE" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_VICTORY_DANCE" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_STAND" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + ) + + //========================================================= + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_AGRUNT_THREAT_DISPLAY, + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_THREAT_DISPLAY" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_HEAR_PLAYER" + " COND_HEAR_COMBAT" + " COND_HEAR_WORLD" + ) + +AI_END_CUSTOM_NPC() diff --git a/game/server/hl1/hl1_npc_apache.cpp b/game/server/hl1/hl1_npc_apache.cpp new file mode 100644 index 0000000..ee48d73 --- /dev/null +++ b/game/server/hl1/hl1_npc_apache.cpp @@ -0,0 +1,766 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "hl1_basegrenade.h" +#include "animation.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "soundenvelope.h" +#include "hl1_CBaseHelicopter.h" +#include "ndebugoverlay.h" +#include "smoke_trail.h" +#include "beam_shared.h" +#include "grenade_homer.h" + +#define HOMER_TRAIL0_LIFE 0.1 +#define HOMER_TRAIL1_LIFE 0.2 +#define HOMER_TRAIL2_LIFE 3.0// 1.0 + +#define SF_NOTRANSITION 128 + +extern short g_sModelIndexFireball; + +class CNPC_Apache : public CBaseHelicopter +{ + DECLARE_CLASS( CNPC_Apache, CBaseHelicopter ); +public: + DECLARE_DATADESC(); + + void Spawn( void ); + void Precache( void ); + + int BloodColor( void ) { return DONT_BLEED; } + Class_T Classify( void ) { return CLASS_HUMAN_MILITARY; }; + void InitializeRotorSound( void ); + void LaunchRocket( Vector &viewDir, int damage, int radius, Vector vecLaunchPoint ); + + void Flight( void ); + + bool FireGun( void ); + void AimRocketGun( void ); + void FireRocket( void ); + void DyingThink( void ); + + int ObjectCaps( void ); + + void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + +/* bool OnInternalDrawModel( ClientModelRenderInfo_t *pInfo ) + { + BaseClass::OnInteralDrawModel( pInfo ); + Vector origin = GetAbsOrigin(); + origin.z += 32; + SetAbsOrigin( origin ); + }*/ + +/*( void SetAbsOrigin( const Vector& absOrigin ) + { + ((Vector&)absOrigin).z += 32; + BaseClass::SetAbsOrigin( absOrigin ); + }*/ + + + + /* int Save( CSave &save ); + int Restore( CRestore &restore ); + static TYPEDESCRIPTION m_SaveData[]; + + void Spawn( void ); + void Precache( void ); + + + void Killed( entvars_t *pevAttacker, int iGib ); + void GibMonster( void ); + + void EXPORT HuntThink( void ); + void EXPORT FlyTouch( CBaseEntity *pOther ); + void EXPORT CrashTouch( CBaseEntity *pOther ); + void EXPORT DyingThink( void ); + void EXPORT StartupUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void EXPORT NullThink( void ); + + void ShowDamage( void ); + void Flight( void ); + void FireRocket( void ); + BOOL FireGun( void ); + + int TakeDamage( entvars_t* pevInflictor, entvars_t* pevAttacker, float flDamage, int bitsDamageType ); + void TraceAttack( entvars_t *pevAttacker, float flDamage, Vector vecDir, TraceResult *ptr, int bitsDamageType);*/ + + int m_iRockets; + float m_flForce; + float m_flNextRocket; + + int m_iAmmoType; + + Vector m_vecTarget; + Vector m_posTarget; + + Vector m_vecDesired; + Vector m_posDesired; + + Vector m_vecGoal; + + QAngle m_angGun; + + int m_iSoundState; // don't save this + + int m_iExplode; + int m_iBodyGibs; + int m_nDebrisModel; + + float m_flGoalSpeed; + + CHandle<SmokeTrail> m_hSmoke; + CBeam *m_pBeam; +}; + +BEGIN_DATADESC( CNPC_Apache ) + DEFINE_FIELD( m_iRockets, FIELD_INTEGER ), + DEFINE_FIELD( m_flForce, FIELD_FLOAT ), + DEFINE_FIELD( m_flNextRocket, FIELD_TIME ), + DEFINE_FIELD( m_vecTarget, FIELD_VECTOR ), + DEFINE_FIELD( m_posTarget, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecDesired, FIELD_VECTOR ), + DEFINE_FIELD( m_posDesired, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecGoal, FIELD_VECTOR ), + DEFINE_FIELD( m_angGun, FIELD_VECTOR ), + DEFINE_FIELD( m_flLastSeen, FIELD_TIME ), + DEFINE_FIELD( m_flPrevSeen, FIELD_TIME ), +// DEFINE_FIELD( m_iSoundState, FIELD_INTEGER ), +// DEFINE_FIELD( m_iSpriteTexture, FIELD_INTEGER ), +// DEFINE_FIELD( m_iExplode, FIELD_INTEGER ), +// DEFINE_FIELD( m_iBodyGibs, FIELD_INTEGER ), + DEFINE_FIELD( m_pBeam, FIELD_CLASSPTR ), + DEFINE_FIELD( m_flGoalSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_hSmoke, FIELD_EHANDLE ), +END_DATADESC() + +ConVar sk_apache_health( "sk_apache_health","100"); + +static Vector s_vecSurroundingMins( -300, -300, -172); +static Vector s_vecSurroundingMaxs(300, 300, 8); + + +void CNPC_Apache::Spawn( void ) +{ + Precache( ); + SetModel( "models/apache.mdl" ); + + BaseClass::Spawn(); + + AddFlag( FL_NPC ); + SetSolid( SOLID_BBOX ); + SetMoveType( MOVETYPE_STEP ); + AddFlag( FL_FLY ); + + + m_iHealth = sk_apache_health.GetFloat(); + + m_flFieldOfView = -0.707; // 270 degrees + + m_fHelicopterFlags = BITS_HELICOPTER_MISSILE_ON | BITS_HELICOPTER_GUN_ON; + + InitBoneControllers(); + + SetRenderColor( 255, 255, 255, 255 ); + + m_iRockets = 10; + + UTIL_SetSize( this, Vector( -32, -32, -32 ), Vector( 32, 32, 32 ) ); + + //CollisionProp()->SetSurroundingBoundsType( USE_SPECIFIED_BOUNDS, &s_vecSurroundingMins, &s_vecSurroundingMaxs ); + //AddSolidFlags( FSOLID_CUSTOMRAYTEST | FSOLID_CUSTOMBOXTEST ); + + m_hSmoke = NULL; +} + +LINK_ENTITY_TO_CLASS ( monster_apache, CNPC_Apache ); + +int CNPC_Apache::ObjectCaps( void ) +{ + if ( GetSpawnFlags() & SF_NOTRANSITION ) + return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; + else + return BaseClass::ObjectCaps(); +} + +void CNPC_Apache::Precache( void ) +{ + // Get to tha chopper! + PrecacheModel( "models/apache.mdl" ); + PrecacheScriptSound( "Apache.Rotor" ); + m_nDebrisModel = PrecacheModel( "models/metalplategibs_green.mdl" ); + + // Gun + PrecacheScriptSound( "Apache.FireGun" ); + m_iAmmoType = GetAmmoDef()->Index("9mmRound"); + + // Rockets + UTIL_PrecacheOther( "grenade_homer" ); + PrecacheScriptSound( "Apache.RPG" ); + PrecacheModel( "models/weapons/w_missile.mdl" ); + + BaseClass::Precache(); +} + +void CNPC_Apache::InitializeRotorSound( void ) +{ + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + + CPASAttenuationFilter filter( this ); + m_pRotorSound = controller.SoundCreate( filter, entindex(), "Apache.Rotor" ); + + BaseClass::InitializeRotorSound(); +} + +void CNPC_Apache::Flight( void ) +{ + StudioFrameAdvance( ); + + float flDistToDesiredPosition = (GetAbsOrigin() - m_vecDesiredPosition).Length(); + // NDebugOverlay::Line(GetAbsOrigin(), m_vecDesiredPosition, 0,0,255, true, 0.1); + + if (m_flGoalSpeed < 800 ) + m_flGoalSpeed += GetAcceleration(); + + +// Vector vecGoalOrientation; + if (flDistToDesiredPosition > 250) // 500 + { + Vector v1 = (m_vecTargetPosition - GetAbsOrigin()); + Vector v2 = (m_vecDesiredPosition - GetAbsOrigin()); + + VectorNormalize( v1 ); + VectorNormalize( v2 ); + + if (m_flLastSeen + 90 > gpGlobals->curtime && DotProduct( v1, v2 ) > 0.25) + { + m_vecGoalOrientation = ( m_vecTargetPosition - GetAbsOrigin()); + } + else + { + m_vecGoalOrientation = (m_vecDesiredPosition - GetAbsOrigin()); + } + VectorNormalize( m_vecGoalOrientation ); + } + else + { + AngleVectors( GetGoalEnt()->GetAbsAngles(), &m_vecGoalOrientation ); + } +// SetGoalOrientation( vecGoalOrientation ); + + + if (GetGoalEnt()) + { + // ALERT( at_console, "%.0f\n", flLength ); + if ( HasReachedTarget() ) + { + // If we get this close to the desired position, it's assumed that we've reached + // the desired position, so move on. + + // Fire target that I've reached my goal + m_AtTarget.FireOutput( GetGoalEnt(), this ); + + OnReachedTarget( GetGoalEnt() ); + + SetGoalEnt( gEntList.FindEntityByName( NULL, GetGoalEnt()->m_target ) ); + + if (GetGoalEnt()) + { + m_vecDesiredPosition = GetGoalEnt()->GetAbsOrigin(); + +// Vector vecGoalOrientation; + AngleVectors( GetGoalEnt()->GetAbsAngles(), &m_vecGoalOrientation ); + +// SetGoalOrientation( vecGoalOrientation ); + + flDistToDesiredPosition = (GetAbsOrigin() - m_vecDesiredPosition).Length(); + } + } + } + else + { + // If we can't find a new target, just stay where we are. + m_vecDesiredPosition = GetAbsOrigin(); + } + + // tilt model 5 degrees + QAngle angAdj = QAngle( 5.0, 0, 0 ); + + // estimate where I'll be facing in one seconds + Vector forward, right, up; + AngleVectors( GetAbsAngles() + GetLocalAngularVelocity() * 2 + angAdj, &forward, &right, &up ); + // Vector vecEst1 = GetAbsOrigin() + pev->velocity + gpGlobals->v_up * m_flForce - Vector( 0, 0, 384 ); + // float flSide = DotProduct( m_posDesired - vecEst1, gpGlobals->v_right ); + + + QAngle angVel = GetLocalAngularVelocity(); + float flSide = DotProduct( m_vecGoalOrientation, right ); + + if (flSide < 0) + { + if ( angVel.y < 60) + { + angVel.y += 8; // 9 * (3.0/2.0); + } + } + else + { + if ( angVel.y > -60) + { + angVel.y -= 8; // 9 * (3.0/2.0); + } + } + angVel.y *= 0.98; + SetLocalAngularVelocity( angVel ); + + Vector vecVel = GetAbsVelocity(); + + // estimate where I'll be in two seconds + AngleVectors( GetAbsAngles() + GetLocalAngularVelocity() * 1 + angAdj, &forward, &right, &up ); + Vector vecEst = GetAbsOrigin() + vecVel * 2.0 + up * m_flForce * 20 - Vector( 0, 0, 384 * 2 ); + + // add immediate force + AngleVectors( GetAbsAngles() + angAdj, &forward, &right, &up ); + + vecVel.x += up.x * m_flForce; + vecVel.y += up.y * m_flForce; + vecVel.z += up.z * m_flForce; + // add gravity + vecVel.z -= 38.4; // 32ft/sec + + float flSpeed = vecVel.Length(); + float flDir = DotProduct( Vector( forward.x, forward.y, 0 ), Vector( vecVel.x, vecVel.y, 0 ) ); + if (flDir < 0) + flSpeed = -flSpeed; + + float flDist = DotProduct( m_vecDesiredPosition - vecEst, forward ); + + // float flSlip = DotProduct( pev->velocity, gpGlobals->v_right ); + float flSlip = -DotProduct( m_vecDesiredPosition - vecEst, right ); + + angVel = GetLocalAngularVelocity(); + // fly sideways + if (flSlip > 0) + { + if (GetAbsAngles().z > -30 && angVel.z > -15) + angVel.z -= 4; + else + angVel.z += 2; + } + else + { + + if (GetAbsAngles().z < 30 && angVel.z < 15) + angVel.z += 4; + else + angVel.z -= 2; + } + SetLocalAngularVelocity( angVel ); + + // sideways drag + vecVel.x = vecVel.x * (1.0 - fabs( right.x ) * 0.05); + vecVel.y = vecVel.y * (1.0 - fabs( right.y ) * 0.05); + vecVel.z = vecVel.z * (1.0 - fabs( right.z ) * 0.05); + + // general drag + vecVel = vecVel * 0.995; + + // Set final computed velocity + SetAbsVelocity( vecVel ); + + // apply power to stay correct height + if (m_flForce < 80 && vecEst.z < m_vecDesiredPosition.z) + { + m_flForce += 12; + } + else if (m_flForce > 30) + { + if (vecEst.z > m_vecDesiredPosition.z) + m_flForce -= 8; + } + + + angVel = GetLocalAngularVelocity(); + // pitch forward or back to get to target + if (flDist > 0 && flSpeed < m_flGoalSpeed && GetAbsAngles().x + angVel.x < 40) + { + // ALERT( at_console, "F " ); + // lean forward + angVel.x += 12.0; + } + else if (flDist < 0 && flSpeed > -50 && GetAbsAngles().x + angVel.x > -20) + { + // ALERT( at_console, "B " ); + // lean backward + angVel.x -= 12.0; + } + else if (GetAbsAngles().x + angVel.x < 0) + { + // ALERT( at_console, "f " ); + angVel.x += 4.0; + } + else if (GetAbsAngles().x + angVel.x > 0) + { + // ALERT( at_console, "b " ); + angVel.x -= 4.0; + } + + // Set final computed angular velocity + SetLocalAngularVelocity( angVel ); + + // ALERT( at_console, "%.0f %.0f : %.0f %.0f : %.0f %.0f : %.0f\n", GetAbsOrigin().x, pev->velocity.x, flDist, flSpeed, GetAbsAngles().x, pev->avelocity.x, m_flForce ); + // ALERT( at_console, "%.0f %.0f : %.0f %0.f : %.0f\n", GetAbsOrigin().z, pev->velocity.z, vecEst.z, m_posDesired.z, m_flForce ); +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ + +#define CHOPPER_AP_GUN_TIP 0 +#define CHOPPER_AP_GUN_BASE 1 + +#define CHOPPER_BC_GUN_YAW 0 +#define CHOPPER_BC_GUN_PITCH 1 +#define CHOPPER_BC_POD_PITCH 2 + +bool CNPC_Apache::FireGun( ) +{ + if ( !GetEnemy() ) + return false; + + Vector vForward, vRight, vUp; + + AngleVectors( GetAbsAngles(), &vForward, &vUp, &vRight ); + + Vector posGun; + QAngle angGun; + GetAttachment( 1, posGun, angGun ); + + Vector vecTarget = (m_vecTargetPosition - posGun); + + VectorNormalize( vecTarget ); + + Vector vecOut; + + vecOut.x = DotProduct( vForward, vecTarget ); + vecOut.y = -DotProduct( vUp, vecTarget ); + vecOut.z = DotProduct( vRight, vecTarget ); + + QAngle angles; + + VectorAngles( vecOut, angles ); + + angles.y = AngleNormalize(angles.y); + angles.x = AngleNormalize(angles.x); + + if (angles.x > m_angGun.x) + m_angGun.x = MIN( angles.x, m_angGun.x + 12 ); + if (angles.x < m_angGun.x) + m_angGun.x = MAX( angles.x, m_angGun.x - 12 ); + if (angles.y > m_angGun.y) + m_angGun.y = MIN( angles.y, m_angGun.y + 12 ); + if (angles.y < m_angGun.y) + m_angGun.y = MAX( angles.y, m_angGun.y - 12 ); + + // hacks - shouldn't be hardcoded, oh well. + // limit it so it doesn't pop if you try to set it to the max value + m_angGun.y = clamp( m_angGun.y, -89.9, 89.9 ); + m_angGun.x = clamp( m_angGun.x, -9.9, 44.9 ); + + m_angGun.y = SetBoneController( 0, m_angGun.y ); + m_angGun.x = SetBoneController( 1, m_angGun.x ); + + Vector posBarrel; + QAngle angBarrel; + GetAttachment( 0, posBarrel, angBarrel ); + + Vector forward; + AngleVectors( angBarrel + m_angGun, &forward ); + + Vector2D vec2LOS = ( GetEnemy()->GetAbsOrigin() - GetAbsOrigin() ).AsVector2D(); + vec2LOS.NormalizeInPlace(); + + float flDot = vec2LOS.Dot( forward.AsVector2D() ); + + //forward +// NDebugOverlay::Line( GetAbsOrigin(), GetAbsOrigin() + ( forward * 200 ), 255,0,0, false, 0.1); + //LOS +// NDebugOverlay::Line( posGun, m_vecTargetPosition , 0,0,255, false, 0.1); +// NDebugOverlay::Box( GetAbsOrigin(), s_vecSurroundingMins, s_vecSurroundingMaxs, 0, 255,0, false, 0.1); + + if ( flDot > 0.98 ) + { + CPASAttenuationFilter filter( this, 0.2f ); + + EmitSound( filter, entindex(), "Apache.FireGun" );//<<TEMP>>temp sound + + // gun is a bit dodgy, just fire at the target if we are close + FireBullets( 1, posGun, vecTarget, VECTOR_CONE_4DEGREES, 8192, m_iAmmoType, 2 ); + + return true; + } + + return false; +} + +void CNPC_Apache::FireRocket( void ) +{ + static float side = 1.0; + static int count; + Vector vForward, vRight, vUp; + + + AngleVectors( GetAbsAngles(), &vForward, &vRight, &vUp ); + Vector vecSrc = GetAbsOrigin() + 1.5 * ( vForward * 21 + vRight * 70 * side + vUp * -79 ); + + // pick firing pod to launch from + switch( m_iRockets % 5) + { + case 0: vecSrc = vecSrc + vRight * 10; break; + case 1: vecSrc = vecSrc - vRight * 10; break; + case 2: vecSrc = vecSrc + vUp * 10; break; + case 3: vecSrc = vecSrc - vUp * 10; break; + case 4: break; + } + + Vector vTargetDir = m_vecTargetPosition - GetAbsOrigin(); + VectorNormalize ( vTargetDir ); + LaunchRocket( vTargetDir, 100, 150, vecSrc); + + m_iRockets--; + + side = - side; +} +void CNPC_Apache::AimRocketGun( void ) +{ + Vector vForward, vRight, vUp; + + if (m_iRockets <= 0) + return; + + Vector vTargetDir = m_vecTargetPosition - GetAbsOrigin(); + VectorNormalize ( vTargetDir ); + + AngleVectors( GetAbsAngles(), &vForward, &vRight, &vUp ); + Vector vecEst = ( vForward * 800 + GetAbsVelocity()); + VectorNormalize ( vecEst ); + + trace_t tr1; + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + vecEst * 4096, MASK_ALL, this, COLLISION_GROUP_NONE, &tr1); + +// NDebugOverlay::Line(GetAbsOrigin(), tr1.endpos, 255,255,0, false, 0.1); + + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + vTargetDir * 4096, MASK_ALL, this, COLLISION_GROUP_NONE, &tr1); + +// NDebugOverlay::Line(GetAbsOrigin(), tr1.endpos, 0,255,0, false, 0.1); + + // ALERT( at_console, "%d %d %d %4.2f\n", GetAbsAngles().x < 0, DotProduct( pev->velocity, gpGlobals->v_forward ) > -100, m_flNextRocket < gpGlobals->curtime, DotProduct( m_vecTarget, vecEst ) ); + + if ((m_iRockets % 2) == 1) + { + FireRocket( ); + m_flNextRocket = gpGlobals->curtime + 0.5; + if (m_iRockets <= 0) + { + m_flNextRocket = gpGlobals->curtime + 10; + m_iRockets = 10; + } + } + else if (DotProduct( GetAbsVelocity(), vForward ) > -100 && m_flNextRocket < gpGlobals->curtime) + { + if (m_flLastSeen + 60 > gpGlobals->curtime) + { + if (GetEnemy() != NULL) + { + // make sure it's a good shot + //if (DotProduct( vTargetDir, vecEst) > 0.5) + { + trace_t tr; + + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + vecEst * 4096, MASK_ALL, this, COLLISION_GROUP_NONE, &tr); + + // NDebugOverlay::Line(GetAbsOrigin(), tr.endpos, 255,0,255, false, 5); + +// if ((tr.endpos - m_vecTargetPosition).Length() < 512) + if ((tr.endpos - m_vecTargetPosition).Length() < 1024) + FireRocket( ); + } + } + else + { + trace_t tr; + + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + vecEst * 4096, MASK_ALL, this, COLLISION_GROUP_NONE, &tr); + // just fire when close + if ((tr.endpos - m_vecTargetPosition).Length() < 512) + FireRocket( ); + } + } + } +} + +#define MISSILE_HOMING_STRENGTH 0.3 +#define MISSILE_HOMING_DELAY 5.0 +#define MISSILE_HOMING_RAMP_UP 0.5 +#define MISSILE_HOMING_DURATION 1.0 +#define MISSILE_HOMING_RAMP_DOWN 0.5 + +void CNPC_Apache::LaunchRocket( Vector &viewDir, int damage, int radius, Vector vecLaunchPoint ) +{ + + CGrenadeHomer *pGrenade = CGrenadeHomer::CreateGrenadeHomer( + MAKE_STRING("models/weapons/w_missile.mdl"), + MAKE_STRING( "Apache.RPG" ), + vecLaunchPoint, vec3_angle, edict() ); + pGrenade->Spawn( ); + pGrenade->SetHoming(MISSILE_HOMING_STRENGTH, MISSILE_HOMING_DELAY, + MISSILE_HOMING_RAMP_UP, MISSILE_HOMING_DURATION, + MISSILE_HOMING_RAMP_DOWN); + pGrenade->SetDamage( damage ); + pGrenade->m_DmgRadius = radius; + + pGrenade->Launch( this, GetEnemy(), viewDir * 1500, 500, 0, HOMER_SMOKE_TRAIL_ON ); + +} + + +//----------------------------------------------------------------------------- +// Purpose: Lame, temporary death +//----------------------------------------------------------------------------- +void CNPC_Apache::DyingThink( void ) +{ + StudioFrameAdvance( ); + SetNextThink( gpGlobals->curtime + 0.1f ); + + if( gpGlobals->curtime > m_flNextCrashExplosion ) + { + CPASFilter pasFilter( GetAbsOrigin() ); + Vector pos; + + pos = GetAbsOrigin(); + pos.x += random->RandomFloat( -150, 150 ); + pos.y += random->RandomFloat( -150, 150 ); + pos.z += random->RandomFloat( -150, -50 ); + + te->Explosion( pasFilter, 0.0, &pos, g_sModelIndexFireball, 10, 15, TE_EXPLFLAG_NONE, 100, 0 ); + + Vector vecSize = Vector( 500, 500, 60 ); + CPVSFilter pvsFilter( GetAbsOrigin() ); + + te->BreakModel( pvsFilter, 0.0, GetAbsOrigin(), vec3_angle, vecSize, vec3_origin, + m_nDebrisModel, 100, 0, 2.5, BREAK_METAL ); + + m_flNextCrashExplosion = gpGlobals->curtime + random->RandomFloat( 0.3, 0.5 ); + } + + QAngle angVel = GetLocalAngularVelocity(); + if( angVel.y < 400 ) + { + angVel.y *= 1.1; + SetLocalAngularVelocity( angVel ); + } + + Vector vecImpulse( 0, 0, -38.4 ); // gravity - 32ft/sec + ApplyAbsVelocityImpulse( vecImpulse ); + + if( m_hSmoke ) + { + m_hSmoke->SetLifetime(0.1f); + m_hSmoke = NULL; + } + +} + + + +void CNPC_Apache::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + + CTakeDamageInfo dmgInfo = info; + + // HITGROUPS don't work currently. + // ignore blades + //if (ptr->hitgroup == 6 && (info.GetDamageType() & (DMG_ENERGYBEAM|DMG_BULLET|DMG_CLUB))) + // return; + + // hit hard, hits cockpit + if (info.GetDamage() > 50 || ptr->hitgroup == 1 || ptr->hitgroup == 2 || ptr->hitgroup == 3 ) + { + // ALERT( at_console, "%map .0f\n", flDamage ); + AddMultiDamage( dmgInfo, this ); + + if ( info.GetDamage() > 50 ) + { + if ( m_hSmoke == NULL && (m_hSmoke = SmokeTrail::CreateSmokeTrail()) != NULL ) + { + m_hSmoke->m_Opacity = 1.0f; + m_hSmoke->m_SpawnRate = 60; + m_hSmoke->m_ParticleLifetime = 1.3f; + m_hSmoke->m_StartColor.Init( 0.65f, 0.65f , 0.65f ); + m_hSmoke->m_EndColor.Init( 0.65f, 0.65f, 0.65f ); + m_hSmoke->m_StartSize = 12; + m_hSmoke->m_EndSize = 64; + m_hSmoke->m_SpawnRadius = 8; + m_hSmoke->m_MinSpeed = 2; + m_hSmoke->m_MaxSpeed = 24; + + m_hSmoke->SetLifetime( 1e6 ); + m_hSmoke->FollowEntity( this ); + + } + } + } + else + { + // do half damage in the body + dmgInfo.ScaleDamage(0.5); + AddMultiDamage( dmgInfo, this ); + g_pEffects->Ricochet( ptr->endpos, ptr->plane.normal ); + } + + if ( m_iHealth < 10 ) + { + if ( m_hSmoke == NULL && (m_hSmoke = SmokeTrail::CreateSmokeTrail()) != NULL ) + { + m_hSmoke->m_Opacity = 1.0f; + m_hSmoke->m_SpawnRate = 60; + m_hSmoke->m_ParticleLifetime = 1.3f; + m_hSmoke->m_StartColor.Init( 0.65f, 0.65f , 0.65f ); + m_hSmoke->m_EndColor.Init( 0.65f, 0.65f, 0.65f ); + m_hSmoke->m_StartSize = 12; + m_hSmoke->m_EndSize = 64; + m_hSmoke->m_SpawnRadius = 8; + m_hSmoke->m_MinSpeed = 2; + m_hSmoke->m_MaxSpeed = 24; + + m_hSmoke->SetLifetime( 1e6 ); + m_hSmoke->FollowEntity( this ); + + } + } +} diff --git a/game/server/hl1/hl1_npc_barnacle.cpp b/game/server/hl1/hl1_npc_barnacle.cpp new file mode 100644 index 0000000..05524cb --- /dev/null +++ b/game/server/hl1/hl1_npc_barnacle.cpp @@ -0,0 +1,516 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: barnacle - stationary ceiling mounted 'fishing' monster +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "hl1_npc_barnacle.h" +#include "npcevent.h" +#include "gib.h" +#include "ai_default.h" +#include "activitylist.h" +#include "hl2_player.h" +#include "vstdlib/random.h" +#include "physics_saverestore.h" +#include "vcollide_parse.h" +#include "engine/IEngineSound.h" + +ConVar sk_barnacle_health( "sk_barnacle_health","25"); + +//----------------------------------------------------------------------------- +// Private activities. +//----------------------------------------------------------------------------- +static int ACT_EAT = 0; + +//----------------------------------------------------------------------------- +// Interactions +//----------------------------------------------------------------------------- +int g_interactionBarnacleVictimDangle = 0; +int g_interactionBarnacleVictimReleased = 0; +int g_interactionBarnacleVictimGrab = 0; + +LINK_ENTITY_TO_CLASS( monster_barnacle, CNPC_Barnacle ); +IMPLEMENT_CUSTOM_AI( monster_barnacle, CNPC_Barnacle ); + +//----------------------------------------------------------------------------- +// Purpose: Initialize the custom schedules +// Input : +// Output : +//----------------------------------------------------------------------------- +void CNPC_Barnacle::InitCustomSchedules(void) +{ + INIT_CUSTOM_AI(CNPC_Barnacle); + + ADD_CUSTOM_ACTIVITY(CNPC_Barnacle, ACT_EAT); + + g_interactionBarnacleVictimDangle = CBaseCombatCharacter::GetInteractionID(); + g_interactionBarnacleVictimReleased = CBaseCombatCharacter::GetInteractionID(); + g_interactionBarnacleVictimGrab = CBaseCombatCharacter::GetInteractionID(); +} + + +BEGIN_DATADESC( CNPC_Barnacle ) + + DEFINE_FIELD( m_flAltitude, FIELD_FLOAT ), + DEFINE_FIELD( m_flKillVictimTime, FIELD_TIME ), + DEFINE_FIELD( m_cGibs, FIELD_INTEGER ),// barnacle loads up on gibs each time it kills something. + DEFINE_FIELD( m_fLiftingPrey, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flTongueAdj, FIELD_FLOAT ), + DEFINE_FIELD( m_flIgnoreTouchesUntil, FIELD_TIME ), + + // Function pointers + DEFINE_THINKFUNC( BarnacleThink ), + DEFINE_THINKFUNC( WaitTillDead ), +END_DATADESC() + + +//========================================================= +// Classify - indicates this monster's place in the +// relationship table. +//========================================================= +Class_T CNPC_Barnacle::Classify ( void ) +{ + return CLASS_ALIEN_MONSTER; +} + +//========================================================= +// HandleAnimEvent - catches the monster-specific messages +// that occur when tagged animation frames are played. +// +// Returns number of events handled, 0 if none. +//========================================================= +void CNPC_Barnacle::HandleAnimEvent( animevent_t *pEvent ) +{ + switch( pEvent->event ) + { + case BARNACLE_AE_PUKEGIB: + CGib::SpawnRandomGibs( this, 1, GIB_HUMAN ); + break; + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } +} + +//========================================================= +// Spawn +//========================================================= +void CNPC_Barnacle::Spawn() +{ + Precache( ); + + SetModel( "models/barnacle.mdl" ); + UTIL_SetSize( this, Vector(-16, -16, -32), Vector(16, 16, 0) ); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_NONE ); + SetBloodColor( BLOOD_COLOR_GREEN ); + m_iHealth = sk_barnacle_health.GetFloat(); + m_flFieldOfView = 0.5;// indicates the width of this monster's forward view cone ( as a dotproduct result ) + m_NPCState = NPC_STATE_NONE; + m_flKillVictimTime = 0; + m_cGibs = 0; + m_fLiftingPrey = FALSE; + m_takedamage = DAMAGE_YES; + + InitBoneControllers(); + InitTonguePosition(); + + // set eye position + SetDefaultEyeOffset(); + + SetActivity ( ACT_IDLE ); + + SetThink ( &CNPC_Barnacle::BarnacleThink ); + SetNextThink( gpGlobals->curtime + 0.5f ); + //Do not have a shadow + AddEffects( EF_NOSHADOW ); + + m_flIgnoreTouchesUntil = gpGlobals->curtime; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +int CNPC_Barnacle::OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ) +{ + CTakeDamageInfo info = inputInfo; + if ( info.GetDamageType() & DMG_CLUB ) + { + info.SetDamage( m_iHealth ); + } + + return BaseClass::OnTakeDamage_Alive( info ); +} + +//----------------------------------------------------------------------------- +// Purpose: Initialize tongue position when first spawned +// Input : +// Output : +//----------------------------------------------------------------------------- +void CNPC_Barnacle::InitTonguePosition( void ) +{ + CBaseEntity *pTouchEnt; + float flLength; + + pTouchEnt = TongueTouchEnt( &flLength ); + m_flAltitude = flLength; + + Vector origin; + QAngle angle; + + GetAttachment( "TongueEnd", origin, angle ); + + m_flTongueAdj = origin.z - GetAbsOrigin().z; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Barnacle::BarnacleThink ( void ) +{ + CBaseEntity *pTouchEnt; + float flLength; + + SetNextThink( gpGlobals->curtime + 0.1f ); + + if (CAI_BaseNPC::m_nDebugBits & bits_debugDisableAI) + { + // AI Disabled, don't do anything + } + else if ( GetEnemy() != NULL ) + { +// barnacle has prey. + + if ( !GetEnemy()->IsAlive() ) + { + // someone (maybe even the barnacle) killed the prey. Reset barnacle. + m_fLiftingPrey = FALSE;// indicate that we're not lifting prey. + SetEnemy( NULL ); + return; + } + + CBaseCombatCharacter* pVictim = GetEnemyCombatCharacterPointer(); + Assert( pVictim ); + + if ( m_fLiftingPrey ) + { + + if ( GetEnemy() != NULL && pVictim->m_lifeState == LIFE_DEAD ) + { + // crap, someone killed the prey on the way up. + SetEnemy( NULL ); + m_fLiftingPrey = FALSE; + return; + } + + // still pulling prey. + Vector vecNewEnemyOrigin = GetEnemy()->GetLocalOrigin(); + vecNewEnemyOrigin.x = GetLocalOrigin().x; + vecNewEnemyOrigin.y = GetLocalOrigin().y; + + // guess as to where their neck is + // FIXME: remove, ask victim where their neck is + vecNewEnemyOrigin.x -= 6 * cos(GetEnemy()->GetLocalAngles().y * M_PI/180.0); + vecNewEnemyOrigin.y -= 6 * sin(GetEnemy()->GetLocalAngles().y * M_PI/180.0); + + m_flAltitude -= BARNACLE_PULL_SPEED; + vecNewEnemyOrigin.z += BARNACLE_PULL_SPEED; + + if ( fabs( GetLocalOrigin().z - ( vecNewEnemyOrigin.z + GetEnemy()->GetViewOffset().z ) ) < BARNACLE_BODY_HEIGHT ) + { + // prey has just been lifted into position ( if the victim origin + eye height + 8 is higher than the bottom of the barnacle, it is assumed that the head is within barnacle's body ) + m_fLiftingPrey = FALSE; + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Barnacle.Bite"); + + // Take a while to kill the player + m_flKillVictimTime = gpGlobals->curtime + 10; + + if ( pVictim ) + { + pVictim->DispatchInteraction( g_interactionBarnacleVictimDangle, NULL, this ); + SetActivity ( (Activity)ACT_EAT ); + } + } + + CBaseEntity *pEnemy = GetEnemy(); + + trace_t trace; + UTIL_TraceEntity( pEnemy, pEnemy->GetAbsOrigin(), vecNewEnemyOrigin, MASK_SOLID_BRUSHONLY, pEnemy, COLLISION_GROUP_NONE, &trace ); + + if( trace.fraction != 1.0 ) + { + // The victim cannot be moved from their current origin to this new origin. So drop them. + SetEnemy( NULL ); + m_fLiftingPrey = FALSE; + + if( pEnemy->MyCombatCharacterPointer() ) + { + pEnemy->MyCombatCharacterPointer()->DispatchInteraction( g_interactionBarnacleVictimReleased, NULL, this ); + } + + // Ignore touches long enough to let the victim move away. + m_flIgnoreTouchesUntil = gpGlobals->curtime + 1.5; + + SetActivity( ACT_IDLE ); + + return; + } + + UTIL_SetOrigin ( GetEnemy(), vecNewEnemyOrigin ); + } + else + { + // prey is lifted fully into feeding position and is dangling there. + + if ( m_flKillVictimTime != -1 && gpGlobals->curtime > m_flKillVictimTime ) + { + // kill! + if ( pVictim ) + { + // DMG_CRUSH added so no physics force is generated + pVictim->TakeDamage( CTakeDamageInfo( this, this, pVictim->m_iHealth, DMG_SLASH | DMG_ALWAYSGIB | DMG_CRUSH ) ); + m_cGibs = 3; + } + + return; + } + + // bite prey every once in a while + if ( pVictim && ( random->RandomInt( 0, 49 ) == 0 ) ) + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Barnacle.Chew" ); + + if ( pVictim ) + { + pVictim->DispatchInteraction( g_interactionBarnacleVictimDangle, NULL, this ); + } + } + } + } + else + { +// barnacle has no prey right now, so just idle and check to see if anything is touching the tongue. + + // If idle and no nearby client, don't think so often. Client should be out of PVS and not within 50 feet. + if ( !UTIL_FindClientInPVS(edict()) ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex(1); + + if( pPlayer ) + { + Vector vecDist = pPlayer->GetAbsOrigin() - GetAbsOrigin(); + + if( vecDist.Length2DSqr() >= Square(600.0f) ) + { + SetNextThink( gpGlobals->curtime + 1.5f ); + } + } + } + + if ( IsActivityFinished() ) + {// this is done so barnacle will fidget. + SetActivity ( ACT_IDLE ); + } + + if ( m_cGibs && random->RandomInt(0,99) == 1 ) + { + // cough up a gib. + CGib::SpawnRandomGibs( this, 1, GIB_HUMAN ); + m_cGibs--; + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Barnacle.Chew" ); + } + + pTouchEnt = TongueTouchEnt( &flLength ); + + //NDebugOverlay::Box( GetAbsOrigin() - Vector( 0, 0, flLength ), Vector( -2, -2, -2 ), Vector( 2, 2, 2 ), 255,0,0, 0, 0.1 ); + + if ( pTouchEnt != NULL ) + { + // tongue is fully extended, and is touching someone. + CBaseCombatCharacter* pBCC = (CBaseCombatCharacter *)pTouchEnt; + + // FIXME: humans should return neck position + Vector vecGrabPos = pTouchEnt->GetAbsOrigin(); + + if ( pBCC && pBCC->DispatchInteraction( g_interactionBarnacleVictimGrab, &vecGrabPos, this ) ) + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Barnacle.Alert" ); + + SetSequenceByName ( "attack1" ); + + SetEnemy( pTouchEnt ); + + pTouchEnt->SetMoveType( MOVETYPE_FLY ); + pTouchEnt->SetAbsVelocity( vec3_origin ); + pTouchEnt->SetBaseVelocity( vec3_origin ); + Vector origin = GetAbsOrigin(); + origin.z = pTouchEnt->GetAbsOrigin().z; + pTouchEnt->SetLocalOrigin( origin ); + + m_fLiftingPrey = TRUE;// indicate that we should be lifting prey. + m_flKillVictimTime = -1;// set this to a bogus time while the victim is lifted. + + m_flAltitude = (GetAbsOrigin().z - vecGrabPos.z); + } + } + else + { + // calculate a new length for the tongue to be clear of anything else that moves under it. + if ( m_flAltitude < flLength ) + { + // if tongue is higher than is should be, lower it kind of slowly. + m_flAltitude += BARNACLE_PULL_SPEED; + } + else + { + m_flAltitude = flLength; + } + + } + + } + + // ALERT( at_console, "tounge %f\n", m_flAltitude + m_flTongueAdj ); + //NDebugOverlay::Box( GetAbsOrigin() - Vector( 0, 0, m_flAltitude ), Vector( -2, -2, -2 ), Vector( 2, 2, 2 ), 255,255,255, 0, 0.1 ); + + SetBoneController( 0, -(m_flAltitude + m_flTongueAdj) ); + StudioFrameAdvance(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Barnacle::Event_Killed( const CTakeDamageInfo &info ) +{ + AddSolidFlags( FSOLID_NOT_SOLID ); + m_takedamage = DAMAGE_NO; + m_lifeState = LIFE_DEAD; + if ( GetEnemy() != NULL ) + { + CBaseCombatCharacter *pVictim = GetEnemyCombatCharacterPointer(); + + if ( pVictim ) + { + pVictim->DispatchInteraction( g_interactionBarnacleVictimReleased, NULL, this ); + } + } + + CGib::SpawnRandomGibs( this, 4, GIB_HUMAN ); + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Barnacle.Die" ); + + SetActivity ( ACT_DIESIMPLE ); + SetBoneController( 0, 0 ); + + StudioFrameAdvance(); + + SetNextThink( gpGlobals->curtime + 0.1f ); + SetThink ( &CNPC_Barnacle::WaitTillDead ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Barnacle::WaitTillDead ( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1f ); + + StudioFrameAdvance(); + DispatchAnimEvents ( this ); + + if ( IsActivityFinished() ) + { + // death anim finished. + StopAnimation(); + SetThink ( NULL ); + } +} + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CNPC_Barnacle::Precache() +{ + PrecacheModel("models/barnacle.mdl"); + + PrecacheScriptSound( "Barnacle.Bite" ); + PrecacheScriptSound( "Barnacle.Chew" ); + PrecacheScriptSound( "Barnacle.Alert" ); + PrecacheScriptSound( "Barnacle.Die" ); + + BaseClass::Precache(); +} + +//========================================================= +// TongueTouchEnt - does a trace along the barnacle's tongue +// to see if any entity is touching it. Also stores the length +// of the trace in the int pointer provided. +//========================================================= +#define BARNACLE_CHECK_SPACING 8 +CBaseEntity *CNPC_Barnacle::TongueTouchEnt ( float *pflLength ) +{ + trace_t tr; + float length; + + // trace once to hit architecture and see if the tongue needs to change position. + UTIL_TraceLine ( GetAbsOrigin(), GetAbsOrigin() - Vector ( 0 , 0 , 2048 ), + MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); + + length = fabs( GetAbsOrigin().z - tr.endpos.z ); + // Pull it up a tad + length -= 16; + if ( pflLength ) + { + *pflLength = length; + } + + // Don't try to touch any prey. + if ( m_flIgnoreTouchesUntil > gpGlobals->curtime ) + return NULL; + + Vector delta = Vector( BARNACLE_CHECK_SPACING, BARNACLE_CHECK_SPACING, 0 ); + Vector mins = GetAbsOrigin() - delta; + Vector maxs = GetAbsOrigin() + delta; + maxs.z = GetAbsOrigin().z; + + // Take our current tongue's length or a point higher if we hit a wall + // NOTENOTE: (this relieves the need to know if the tongue is currently moving) + mins.z -= MIN( m_flAltitude, length ); + + CBaseEntity *pList[10]; + int count = UTIL_EntitiesInBox( pList, 10, mins, maxs, (FL_CLIENT|FL_NPC) ); + if ( count ) + { + for ( int i = 0; i < count; i++ ) + { + CBaseCombatCharacter *pVictim = ToBaseCombatCharacter( pList[ i ] ); + + bool bCanHurt = false; + + if ( IRelationType( pList[i] ) == D_HT || IRelationType( pList[i] ) == D_FR ) + bCanHurt = true; + + if ( pList[i] != this && bCanHurt == true && pVictim->m_lifeState == LIFE_ALIVE ) + { + return pList[i]; + } + } + } + + return NULL; +} diff --git a/game/server/hl1/hl1_npc_barnacle.h b/game/server/hl1/hl1_npc_barnacle.h new file mode 100644 index 0000000..3877857 --- /dev/null +++ b/game/server/hl1/hl1_npc_barnacle.h @@ -0,0 +1,63 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Base combat character with no AI +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef HL1_NPC_BARNACLE_H +#define HL1_NPC_BARNACLE_H + +#include "hl1_ai_basenpc.h" +#include "rope_physics.h" + +#define BARNACLE_BODY_HEIGHT 44 // how 'tall' the barnacle's model is. +#define BARNACLE_PULL_SPEED 8 +#define BARNACLE_KILL_VICTIM_DELAY 5 // how many seconds after pulling prey in to gib them. + +//========================================================= +// Monster's Anim Events Go Here +//========================================================= +#define BARNACLE_AE_PUKEGIB 2 + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CNPC_Barnacle : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_Barnacle, CHL1BaseNPC ); + +public: + void Spawn( void ); + void InitTonguePosition( void ); + void Precache( void ); + CBaseEntity* TongueTouchEnt ( float *pflLength ); + Class_T Classify ( void ); + void HandleAnimEvent( animevent_t *pEvent ); + void BarnacleThink ( void ); + void WaitTillDead ( void ); + void Event_Killed( const CTakeDamageInfo &info ); + int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + + DECLARE_DATADESC(); + + float m_flAltitude; + + float m_flKillVictimTime; + int m_cGibs;// barnacle loads up on gibs each time it kills something. + bool m_fLiftingPrey; + float m_flTongueAdj; + float m_flIgnoreTouchesUntil; + +public: + DEFINE_CUSTOM_AI; +}; + +#endif //HL1_NPC_BARNACLE_H diff --git a/game/server/hl1/hl1_npc_barney.cpp b/game/server/hl1/hl1_npc_barney.cpp new file mode 100644 index 0000000..1ff032c --- /dev/null +++ b/game/server/hl1/hl1_npc_barney.cpp @@ -0,0 +1,938 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Bullseyes act as targets for other NPC's to attack and to trigger +// events +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "hl1_npc_barney.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "basecombatweapon.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "ai_behavior_follow.h" +#include "AI_Criteria.h" +#include "SoundEmitterSystem/isoundemittersystembase.h" + +#define BA_ATTACK "BA_ATTACK" +#define BA_MAD "BA_MAD" +#define BA_SHOT "BA_SHOT" +#define BA_KILL "BA_KILL" +#define BA_POK "BA_POK" + +ConVar sk_barney_health( "sk_barney_health","35"); + +//========================================================= +// Monster's Anim Events Go Here +//========================================================= +// first flag is barney dying for scripted sequences? +#define BARNEY_AE_DRAW ( 2 ) +#define BARNEY_AE_SHOOT ( 3 ) +#define BARNEY_AE_HOLSTER ( 4 ) + +#define BARNEY_BODY_GUNHOLSTERED 0 +#define BARNEY_BODY_GUNDRAWN 1 +#define BARNEY_BODY_GUNGONE 2 + + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CNPC_Barney ) + DEFINE_FIELD( m_fGunDrawn, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flPainTime, FIELD_TIME ), + DEFINE_FIELD( m_flCheckAttackTime, FIELD_TIME ), + DEFINE_FIELD( m_fLastAttackCheck, FIELD_BOOLEAN ), + + DEFINE_THINKFUNC( SUB_LVFadeOut ), + + //DEFINE_FIELD( m_iAmmoType, FIELD_INTEGER ), +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( monster_barney, CNPC_Barney ); + + +static BOOL IsFacing( CBaseEntity *pevTest, const Vector &reference ) +{ + Vector vecDir = (reference - pevTest->GetAbsOrigin()); + vecDir.z = 0; + VectorNormalize( vecDir ); + Vector forward; + QAngle angle; + angle = pevTest->GetAbsAngles(); + angle.x = 0; + AngleVectors( angle, &forward ); + // He's facing me, he meant it + if ( DotProduct( forward, vecDir ) > 0.96 ) // +/- 15 degrees or so + { + return TRUE; + } + return FALSE; +} + +//========================================================= +// Spawn +//========================================================= +void CNPC_Barney::Spawn() +{ + Precache( ); + + SetModel( "models/barney.mdl"); + + SetRenderColor( 255, 255, 255, 255 ); + + SetHullType(HULL_HUMAN); + SetHullSizeNormal(); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + m_bloodColor = BLOOD_COLOR_RED; + m_iHealth = sk_barney_health.GetFloat(); + SetViewOffset( Vector ( 0, 0, 100 ) );// position of the eyes relative to monster's origin. + m_flFieldOfView = VIEW_FIELD_WIDE; // NOTE: we need a wide field of view so npc will notice player and say hello + m_NPCState = NPC_STATE_NONE; + + SetBodygroup( 1, 0 ); + + m_fGunDrawn = false; + + CapabilitiesClear(); + CapabilitiesAdd( bits_CAP_MOVE_GROUND | bits_CAP_OPEN_DOORS | bits_CAP_AUTO_DOORS | bits_CAP_USE | bits_CAP_DOORS_GROUP); + CapabilitiesAdd( bits_CAP_INNATE_RANGE_ATTACK1 | bits_CAP_TURN_HEAD | bits_CAP_ANIMATEDFACE ); + + NPCInit(); + + SetUse( &CNPC_Barney::FollowerUse ); +} + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CNPC_Barney::Precache() +{ + m_iAmmoType = GetAmmoDef()->Index("9mmRound"); + + PrecacheModel("models/barney.mdl"); + + PrecacheScriptSound( "Barney.FirePistol" ); + PrecacheScriptSound( "Barney.Pain" ); + PrecacheScriptSound( "Barney.Die" ); + + // every new barney must call this, otherwise + // when a level is loaded, nobody will talk (time is reset to 0) + TalkInit(); + BaseClass::Precache(); +} + +void CNPC_Barney::ModifyOrAppendCriteria( AI_CriteriaSet& criteriaSet ) +{ + BaseClass::ModifyOrAppendCriteria( criteriaSet ); + + bool predisaster = FBitSet( m_spawnflags, SF_NPC_PREDISASTER ) ? true : false; + + criteriaSet.AppendCriteria( "disaster", predisaster ? "[disaster::pre]" : "[disaster::post]" ); +} + +// Init talk data +void CNPC_Barney::TalkInit() +{ + BaseClass::TalkInit(); + + // get voice for head - just one barney voice for now + GetExpresser()->SetVoicePitch( 100 ); +} + + +//========================================================= +// GetSoundInterests - returns a bit mask indicating which types +// of sounds this monster regards. +//========================================================= +int CNPC_Barney::GetSoundInterests ( void) +{ + return SOUND_WORLD | + SOUND_COMBAT | + SOUND_CARCASS | + SOUND_MEAT | + SOUND_GARBAGE | + SOUND_DANGER | + SOUND_PLAYER; +} + +//========================================================= +// Classify - indicates this monster's place in the +// relationship table. +//========================================================= +Class_T CNPC_Barney::Classify ( void ) +{ + return CLASS_PLAYER_ALLY; +} + +//========================================================= +// ALertSound - barney says "Freeze!" +//========================================================= +void CNPC_Barney::AlertSound( void ) +{ + if ( GetEnemy() != NULL ) + { + if ( IsOkToSpeak() ) + { + Speak( BA_ATTACK ); + } + } + +} +//========================================================= +// SetYawSpeed - allows each sequence to have a different +// turn rate associated with it. +//========================================================= +void CNPC_Barney::SetYawSpeed ( void ) +{ + int ys; + + ys = 0; + + switch ( GetActivity() ) + { + case ACT_IDLE: + ys = 70; + break; + case ACT_WALK: + ys = 70; + break; + case ACT_RUN: + ys = 90; + break; + default: + ys = 70; + break; + } + + GetMotor()->SetYawSpeed( ys ); +} + +//========================================================= +// CheckRangeAttack1 +//========================================================= +bool CNPC_Barney::CheckRangeAttack1 ( float flDot, float flDist ) +{ + if ( gpGlobals->curtime > m_flCheckAttackTime ) + { + trace_t tr; + + Vector shootOrigin = GetAbsOrigin() + Vector( 0, 0, 55 ); + CBaseEntity *pEnemy = GetEnemy(); + Vector shootTarget = ( (pEnemy->BodyTarget( shootOrigin ) - pEnemy->GetAbsOrigin()) + GetEnemyLKP() ); + + UTIL_TraceLine ( shootOrigin, shootTarget, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr); + m_flCheckAttackTime = gpGlobals->curtime + 1; + if ( tr.fraction == 1.0 || ( tr.m_pEnt != NULL && tr.m_pEnt == pEnemy) ) + m_fLastAttackCheck = TRUE; + else + m_fLastAttackCheck = FALSE; + + m_flCheckAttackTime = gpGlobals->curtime + 1.5; + } + + return m_fLastAttackCheck; +} + +//------------------------------------------------------------------------------ +// Purpose : For innate range attack +// Input : +// Output : +//------------------------------------------------------------------------------ +int CNPC_Barney::RangeAttack1Conditions( float flDot, float flDist ) +{ + if (GetEnemy() == NULL) + { + return( COND_NONE ); + } + + else if ( flDist > 1024 ) + { + return( COND_TOO_FAR_TO_ATTACK ); + } + else if ( flDot < 0.5 ) + { + return( COND_NOT_FACING_ATTACK ); + } + + if ( CheckRangeAttack1 ( flDot, flDist ) ) + return( COND_CAN_RANGE_ATTACK1 ); + + return COND_NONE; +} + + +//========================================================= +// BarneyFirePistol - shoots one round from the pistol at +// the enemy barney is facing. +//========================================================= +void CNPC_Barney::BarneyFirePistol ( void ) +{ + Vector vecShootOrigin; + + vecShootOrigin = GetAbsOrigin() + Vector( 0, 0, 55 ); + Vector vecShootDir = GetShootEnemyDir( vecShootOrigin ); + + QAngle angDir; + + VectorAngles( vecShootDir, angDir ); +// SetBlending( 0, angDir.x ); + DoMuzzleFlash(); + + FireBullets(1, vecShootOrigin, vecShootDir, VECTOR_CONE_2DEGREES, 1024, m_iAmmoType ); + + int pitchShift = random->RandomInt( 0, 20 ); + + // Only shift about half the time + if ( pitchShift > 10 ) + pitchShift = 0; + else + pitchShift -= 5; + + CPASAttenuationFilter filter( this ); + + EmitSound_t params; + params.m_pSoundName = "Barney.FirePistol"; + params.m_flVolume = 1; + params.m_nChannel= CHAN_WEAPON; + params.m_SoundLevel = SNDLVL_NORM; + params.m_nPitch = 100 + pitchShift; + EmitSound( filter, entindex(), params ); + + CSoundEnt::InsertSound ( SOUND_COMBAT, GetAbsOrigin(), 384, 0.3 ); + + // UNDONE: Reload? + m_cAmmoLoaded--;// take away a bullet! +} + +int CNPC_Barney::OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ) +{ + // make sure friends talk about it if player hurts talkmonsters... + int ret = BaseClass::OnTakeDamage_Alive( inputInfo ); + + if ( !IsAlive() || m_lifeState == LIFE_DYING ) + return ret; + + if ( m_NPCState != NPC_STATE_PRONE && ( inputInfo.GetAttacker()->GetFlags() & FL_CLIENT ) ) + { + // This is a heurstic to determine if the player intended to harm me + // If I have an enemy, we can't establish intent (may just be crossfire) + if ( GetEnemy() == NULL ) + { + // If the player was facing directly at me, or I'm already suspicious, get mad + if ( HasMemory( bits_MEMORY_SUSPICIOUS ) || IsFacing( inputInfo.GetAttacker(), GetAbsOrigin() ) ) + { + // Alright, now I'm pissed! + Speak( BA_MAD ); + + Remember( bits_MEMORY_PROVOKED ); + StopFollowing(); + } + else + { + // Hey, be careful with that + Speak( BA_SHOT ); + Remember( bits_MEMORY_SUSPICIOUS ); + } + } + else if ( !(GetEnemy()->IsPlayer()) && m_lifeState == LIFE_ALIVE ) + { + Speak( BA_SHOT ); + } + } + + return ret; +} + +//========================================================= +// PainSound +//========================================================= +void CNPC_Barney::PainSound( const CTakeDamageInfo &info ) +{ + if (gpGlobals->curtime < m_flPainTime) + return; + + m_flPainTime = gpGlobals->curtime + random->RandomFloat( 0.5, 0.75 ); + + CPASAttenuationFilter filter( this ); + + CSoundParameters params; + if ( GetParametersForSound( "Barney.Pain", params, NULL ) ) + { + params.pitch = GetExpresser()->GetVoicePitch(); + + EmitSound_t ep( params ); + + EmitSound( filter, entindex(), ep ); + } +} + +//========================================================= +// DeathSound +//========================================================= +void CNPC_Barney::DeathSound( const CTakeDamageInfo &info ) +{ + CPASAttenuationFilter filter( this ); + + CSoundParameters params; + if ( GetParametersForSound( "Barney.Die", params, NULL ) ) + { + params.pitch = GetExpresser()->GetVoicePitch(); + + EmitSound_t ep( params ); + + EmitSound( filter, entindex(), ep ); + } +} + +void CNPC_Barney::TraceAttack( const CTakeDamageInfo &inputInfo, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + CTakeDamageInfo info = inputInfo; + + switch( ptr->hitgroup ) + { + case HITGROUP_CHEST: + case HITGROUP_STOMACH: + if ( info.GetDamageType() & (DMG_BULLET | DMG_SLASH | DMG_BLAST) ) + { + info.ScaleDamage( 0.5f ); + } + break; + case 10: + if ( info.GetDamageType() & (DMG_BULLET | DMG_SLASH | DMG_CLUB) ) + { + info.SetDamage( info.GetDamage() - 20 ); + if ( info.GetDamage() <= 0 ) + { + g_pEffects->Ricochet( ptr->endpos, ptr->plane.normal ); + info.SetDamage( 0.01 ); + } + } + // always a head shot + ptr->hitgroup = HITGROUP_HEAD; + break; + } + + BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator ); +} + +void CNPC_Barney::Event_Killed( const CTakeDamageInfo &info ) +{ + if ( m_nBody < BARNEY_BODY_GUNGONE ) + { + // drop the gun! + Vector vecGunPos; + QAngle angGunAngles; + CBaseEntity *pGun = NULL; + + SetBodygroup( 1, BARNEY_BODY_GUNGONE); + + GetAttachment( "0", vecGunPos, angGunAngles ); + + angGunAngles.y += 180; + pGun = DropItem( "weapon_glock", vecGunPos, angGunAngles ); + } + + SetUse( NULL ); + BaseClass::Event_Killed( info ); + + if ( UTIL_IsLowViolence() ) + { + SUB_StartLVFadeOut( 0.0f ); + } +} + +void CNPC_Barney::SUB_StartLVFadeOut( float delay, bool notSolid ) +{ + SetThink( &CNPC_Barney::SUB_LVFadeOut ); + SetNextThink( gpGlobals->curtime + delay ); + SetRenderColorA( 255 ); + m_nRenderMode = kRenderNormal; + + if ( notSolid ) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + SetLocalAngularVelocity( vec3_angle ); + } +} + +void CNPC_Barney::SUB_LVFadeOut( void ) +{ + if( VPhysicsGetObject() ) + { + if( VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_PLAYER_HELD || GetEFlags() & EFL_IS_BEING_LIFTED_BY_BARNACLE ) + { + // Try again in a few seconds. + SetNextThink( gpGlobals->curtime + 5 ); + SetRenderColorA( 255 ); + return; + } + } + + float dt = gpGlobals->frametime; + if ( dt > 0.1f ) + { + dt = 0.1f; + } + m_nRenderMode = kRenderTransTexture; + int speed = MAX(3,256*dt); // fade out over 3 seconds + SetRenderColorA( UTIL_Approach( 0, m_clrRender->a, speed ) ); + NetworkStateChanged(); + + if ( m_clrRender->a == 0 ) + { + UTIL_Remove(this); + } + else + { + SetNextThink( gpGlobals->curtime ); + } +} + +void CNPC_Barney::StartTask( const Task_t *pTask ) +{ + BaseClass::StartTask( pTask ); +} + +void CNPC_Barney::RunTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_RANGE_ATTACK1: + if (GetEnemy() != NULL && (GetEnemy()->IsPlayer())) + { + m_flPlaybackRate = 1.5; + } + BaseClass::RunTask( pTask ); + break; + default: + BaseClass::RunTask( pTask ); + break; + } +} + +//========================================================= +// HandleAnimEvent - catches the monster-specific messages +// that occur when tagged animation frames are played. +// +// Returns number of events handled, 0 if none. +//========================================================= +void CNPC_Barney::HandleAnimEvent( animevent_t *pEvent ) +{ + switch( pEvent->event ) + { + case BARNEY_AE_SHOOT: + BarneyFirePistol(); + break; + + case BARNEY_AE_DRAW: + // barney's bodygroup switches here so he can pull gun from holster + SetBodygroup( 1, BARNEY_BODY_GUNDRAWN); + m_fGunDrawn = true; + break; + + case BARNEY_AE_HOLSTER: + // change bodygroup to replace gun in holster + SetBodygroup( 1, BARNEY_BODY_GUNHOLSTERED); + m_fGunDrawn = false; + break; + + default: + BaseClass::HandleAnimEvent( pEvent ); + } +} + + +//========================================================= +// AI Schedules Specific to this monster +//========================================================= + +int CNPC_Barney::TranslateSchedule( int scheduleType ) +{ + switch( scheduleType ) + { + case SCHED_ARM_WEAPON: + if ( GetEnemy() != NULL ) + { + // face enemy, then draw. + return SCHED_BARNEY_ENEMY_DRAW; + } + break; + + // Hook these to make a looping schedule + case SCHED_TARGET_FACE: + { + int baseType; + + // call base class default so that scientist will talk + // when 'used' + baseType = BaseClass::TranslateSchedule( scheduleType ); + + if ( baseType == SCHED_IDLE_STAND ) + return SCHED_BARNEY_FACE_TARGET; + else + return baseType; + } + break; + + case SCHED_TARGET_CHASE: + { + return SCHED_BARNEY_FOLLOW; + break; + } + + case SCHED_IDLE_STAND: + { + int baseType; + + // call base class default so that scientist will talk + // when 'used' + baseType = BaseClass::TranslateSchedule( scheduleType ); + + if ( baseType == SCHED_IDLE_STAND ) + return SCHED_BARNEY_IDLE_STAND; + else + return baseType; + } + break; + + case SCHED_TAKE_COVER_FROM_ENEMY: + case SCHED_CHASE_ENEMY: + { + if ( HasCondition( COND_HEAVY_DAMAGE ) ) + return SCHED_TAKE_COVER_FROM_ENEMY; + + // No need to take cover since I can see him + // SHOOT! + if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) && m_fGunDrawn ) + return SCHED_RANGE_ATTACK1; + } + break; + } + + return BaseClass::TranslateSchedule( scheduleType ); +} + +//========================================================= +// SelectSchedule - Decides which type of schedule best suits +// the monster's current state and conditions. Then calls +// monster's member function to get a pointer to a schedule +// of the proper type. +//========================================================= +int CNPC_Barney::SelectSchedule( void ) +{ + if ( m_NPCState == NPC_STATE_COMBAT || GetEnemy() != NULL ) + { + // Priority action! + if (!m_fGunDrawn ) + return SCHED_ARM_WEAPON; + } + + if ( GetFollowTarget() == NULL ) + { + if ( HasCondition( COND_PLAYER_PUSHING ) && !(GetSpawnFlags() & SF_NPC_PREDISASTER ) ) // Player wants me to move + return SCHED_HL1TALKER_FOLLOW_MOVE_AWAY; + } + + if ( BehaviorSelectSchedule() ) + return BaseClass::SelectSchedule(); + + if ( HasCondition( COND_HEAR_DANGER ) ) + { + CSound *pSound; + pSound = GetBestSound(); + + ASSERT( pSound != NULL ); + + if ( pSound && pSound->IsSoundType( SOUND_DANGER ) ) + return SCHED_TAKE_COVER_FROM_BEST_SOUND; + } + if ( HasCondition( COND_ENEMY_DEAD ) && IsOkToSpeak() ) + { + Speak( BA_KILL ); + } + + switch( m_NPCState ) + { + case NPC_STATE_COMBAT: + { + // dead enemy + if ( HasCondition( COND_ENEMY_DEAD ) ) + return BaseClass::SelectSchedule(); // call base class, all code to handle dead enemies is centralized there. + + // always act surprized with a new enemy + if ( HasCondition( COND_NEW_ENEMY ) && HasCondition( COND_LIGHT_DAMAGE) ) + return SCHED_SMALL_FLINCH; + + if ( HasCondition( COND_HEAVY_DAMAGE ) ) + return SCHED_TAKE_COVER_FROM_ENEMY; + + if ( !HasCondition(COND_SEE_ENEMY) ) + { + // we can't see the enemy + if ( !HasCondition(COND_ENEMY_OCCLUDED) ) + { + // enemy is unseen, but not occluded! + // turn to face enemy + return SCHED_COMBAT_FACE; + } + else + { + return SCHED_CHASE_ENEMY; + } + } + } + break; + + case NPC_STATE_ALERT: + case NPC_STATE_IDLE: + if ( HasCondition( COND_LIGHT_DAMAGE ) || HasCondition( COND_HEAVY_DAMAGE ) ) + { + // flinch if hurt + return SCHED_SMALL_FLINCH; + } + + if ( GetEnemy() == NULL && GetFollowTarget() ) + { + if ( !GetFollowTarget()->IsAlive() ) + { + // UNDONE: Comment about the recently dead player here? + StopFollowing(); + break; + } + else + { + + return SCHED_TARGET_FACE; + } + } + + // try to say something about smells + TrySmellTalk(); + break; + } + + return BaseClass::SelectSchedule(); +} + +NPC_STATE CNPC_Barney::SelectIdealState ( void ) +{ + return BaseClass::SelectIdealState(); +} + +void CNPC_Barney::DeclineFollowing( void ) +{ + if ( CanSpeakAfterMyself() ) + { + Speak( BA_POK ); + } +} + +bool CNPC_Barney::CanBecomeRagdoll( void ) +{ + if ( UTIL_IsLowViolence() ) + { + return false; + } + + return BaseClass::CanBecomeRagdoll(); +} + +bool CNPC_Barney::ShouldGib( const CTakeDamageInfo &info ) +{ + if ( UTIL_IsLowViolence() ) + { + return false; + } + + return BaseClass::ShouldGib( info ); +} + +//------------------------------------------------------------------------------ +// +// Schedules +// +//------------------------------------------------------------------------------ + +AI_BEGIN_CUSTOM_NPC( monster_barney, CNPC_Barney ) + + //========================================================= + // > SCHED_BARNEY_FOLLOW + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_BARNEY_FOLLOW, + + " Tasks" +// " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_BARNEY_STOP_FOLLOWING" + " TASK_GET_PATH_TO_TARGET 0" + " TASK_MOVE_TO_TARGET_RANGE 180" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_TARGET_FACE" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_HEAR_DANGER" + " COND_PROVOKED" + ) + + //========================================================= + // > SCHED_BARNEY_ENEMY_DRAW + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_BARNEY_ENEMY_DRAW, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_ARM" + " " + " Interrupts" + ) + + //========================================================= + // > SCHED_BARNEY_FACE_TARGET + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_BARNEY_FACE_TARGET, + + " Tasks" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_FACE_TARGET 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_BARNEY_FOLLOW" + " " + " Interrupts" + " COND_GIVE_WAY" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_PROVOKED" + " COND_HEAR_DANGER" + ) + + //========================================================= + // > SCHED_BARNEY_IDLE_STAND + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_BARNEY_IDLE_STAND, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT 2" + " TASK_TALKER_HEADRESET 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_PROVOKED" + " COND_HEAR_COMBAT" + " COND_SMELL" + ) + +AI_END_CUSTOM_NPC() + + +//========================================================= +// DEAD BARNEY PROP +// +// Designer selects a pose in worldcraft, 0 through num_poses-1 +// this value is added to what is selected as the 'first dead pose' +// among the monster's normal animations. All dead poses must +// appear sequentially in the model file. Be sure and set +// the m_iFirstPose properly! +// +//========================================================= +class CNPC_DeadBarney : public CAI_BaseNPC +{ + DECLARE_CLASS( CNPC_DeadBarney, CAI_BaseNPC ); +public: + + void Spawn( void ); + Class_T Classify ( void ) { return CLASS_NONE; } + + bool KeyValue( const char *szKeyName, const char *szValue ); + float MaxYawSpeed ( void ) { return 8.0f; } + + int m_iPose;// which sequence to display -- temporary, don't need to save + int m_iDesiredSequence; + static char *m_szPoses[3]; + + DECLARE_DATADESC(); +}; + +char *CNPC_DeadBarney::m_szPoses[] = { "lying_on_back", "lying_on_side", "lying_on_stomach" }; + +bool CNPC_DeadBarney::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "pose" ) ) + m_iPose = atoi( szValue ); + else + BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + +LINK_ENTITY_TO_CLASS( monster_barney_dead, CNPC_DeadBarney ); + +BEGIN_DATADESC( CNPC_DeadBarney ) +END_DATADESC() + +//========================================================= +// ********** DeadBarney SPAWN ********** +//========================================================= +void CNPC_DeadBarney::Spawn( void ) +{ + PrecacheModel("models/barney.mdl"); + SetModel( "models/barney.mdl"); + + ClearEffects(); + SetSequence( 0 ); + m_bloodColor = BLOOD_COLOR_RED; + + SetRenderColor( 255, 255, 255, 255 ); + + SetSequence( m_iDesiredSequence = LookupSequence( m_szPoses[m_iPose] ) ); + if ( GetSequence() == -1 ) + { + Msg ( "Dead barney with bad pose\n" ); + } + // Corpses have less health + m_iHealth = 0.0;//gSkillData.barneyHealth; + + NPCInitDead(); +} diff --git a/game/server/hl1/hl1_npc_barney.h b/game/server/hl1/hl1_npc_barney.h new file mode 100644 index 0000000..442fb02 --- /dev/null +++ b/game/server/hl1/hl1_npc_barney.h @@ -0,0 +1,81 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef NPC_BARNEY_H +#define NPC_BARNEY_H + +#include "hl1_npc_talker.h" + +//========================================================= +//========================================================= +class CNPC_Barney : public CHL1NPCTalker +{ + DECLARE_CLASS( CNPC_Barney, CHL1NPCTalker ); +public: + + DECLARE_DATADESC(); + + virtual void ModifyOrAppendCriteria( AI_CriteriaSet& set ); + + void Precache( void ); + void Spawn( void ); + void TalkInit( void ); + + void StartTask( const Task_t *pTask ); + void RunTask( const Task_t *pTask ); + + int GetSoundInterests ( void ); + Class_T Classify ( void ); + void AlertSound( void ); + void SetYawSpeed ( void ); + + bool CheckRangeAttack1 ( float flDot, float flDist ); + void BarneyFirePistol ( void ); + + int OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ); + void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + void Event_Killed( const CTakeDamageInfo &info ); + + void PainSound( const CTakeDamageInfo &info ); + void DeathSound( const CTakeDamageInfo &info ); + + void HandleAnimEvent( animevent_t *pEvent ); + int TranslateSchedule( int scheduleType ); + int SelectSchedule( void ); + + void DeclineFollowing( void ); + + bool CanBecomeRagdoll( void ); + bool ShouldGib( const CTakeDamageInfo &info ); + + int RangeAttack1Conditions( float flDot, float flDist ); + + void SUB_StartLVFadeOut( float delay = 10.0f, bool bNotSolid = true ); + void SUB_LVFadeOut( void ); + + NPC_STATE SelectIdealState ( void ); + + bool m_fGunDrawn; + float m_flPainTime; + float m_flCheckAttackTime; + bool m_fLastAttackCheck; + + int m_iAmmoType; + + enum + { + SCHED_BARNEY_FOLLOW = BaseClass::NEXT_SCHEDULE, + SCHED_BARNEY_ENEMY_DRAW, + SCHED_BARNEY_FACE_TARGET, + SCHED_BARNEY_IDLE_STAND, + SCHED_BARNEY_STOP_FOLLOWING, + }; + + DEFINE_CUSTOM_AI; +}; + +#endif //NPC_BARNEY_H
\ No newline at end of file diff --git a/game/server/hl1/hl1_npc_bigmomma.cpp b/game/server/hl1/hl1_npc_bigmomma.cpp new file mode 100644 index 0000000..c769375 --- /dev/null +++ b/game/server/hl1/hl1_npc_bigmomma.cpp @@ -0,0 +1,1313 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "basecombatweapon.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "hl1_ai_basenpc.h" +#include "ai_navigator.h" +#include "decals.h" +#include "effect_dispatch_data.h" +#include "te_effect_dispatch.h" +#include "Sprite.h" + +ConVar sk_bigmomma_health_factor( "sk_bigmomma_health_factor", "1" ); +ConVar sk_bigmomma_dmg_slash( "sk_bigmomma_dmg_slash", "50" ); +ConVar sk_bigmomma_dmg_blast( "sk_bigmomma_dmg_blast", "100" ); +ConVar sk_bigmomma_radius_blast( "sk_bigmomma_radius_blast", "250" ); + +float GetCurrentGravity( void ); + + +//========================================================= +// Monster's Anim Events Go Here +//========================================================= +#define BIG_AE_STEP1 1 // Footstep left +#define BIG_AE_STEP2 2 // Footstep right +#define BIG_AE_STEP3 3 // Footstep back left +#define BIG_AE_STEP4 4 // Footstep back right +#define BIG_AE_SACK 5 // Sack slosh +#define BIG_AE_DEATHSOUND 6 // Death sound + +#define BIG_AE_MELEE_ATTACKBR 8 // Leg attack +#define BIG_AE_MELEE_ATTACKBL 9 // Leg attack +#define BIG_AE_MELEE_ATTACK1 10 // Leg attack +#define BIG_AE_MORTAR_ATTACK1 11 // Launch a mortar +#define BIG_AE_LAY_CRAB 12 // Lay a headcrab +#define BIG_AE_JUMP_FORWARD 13 // Jump up and forward +#define BIG_AE_SCREAM 14 // alert sound +#define BIG_AE_PAIN_SOUND 15 // pain sound +#define BIG_AE_ATTACK_SOUND 16 // attack sound +#define BIG_AE_BIRTH_SOUND 17 // birth sound +#define BIG_AE_EARLY_TARGET 50 // Fire target early + + +enum +{ + SCHED_NODE_FAIL = LAST_SHARED_SCHEDULE, + SCHED_BIG_NODE, +}; + +enum +{ + TASK_MOVE_TO_NODE_RANGE = LAST_SHARED_TASK, // Move within node range + TASK_FIND_NODE, // Find my next node + TASK_PLAY_NODE_PRESEQUENCE, // Play node pre-script + TASK_PLAY_NODE_SEQUENCE, // Play node script + TASK_PROCESS_NODE, // Fire targets, etc. + TASK_WAIT_NODE, // Wait at the node + TASK_NODE_DELAY, // Delay walking toward node for a bit. You've failed to get there + TASK_NODE_YAW, // Get the best facing direction for this node + TASK_CHECK_NODE_PROXIMITY, +}; + +// User defined conditions +//#define bits_COND_NODE_SEQUENCE ( COND_SPECIAL1 ) // pev->netname contains the name of a sequence to play + +// Attack distance constants +#define BIG_ATTACKDIST 170 +#define BIG_MORTARDIST 800 +#define BIG_MAXCHILDREN 6 // Max # of live headcrab children + + +#define bits_MEMORY_CHILDPAIR (bits_MEMORY_CUSTOM1) +#define bits_MEMORY_ADVANCE_NODE (bits_MEMORY_CUSTOM2) +#define bits_MEMORY_COMPLETED_NODE (bits_MEMORY_CUSTOM3) +#define bits_MEMORY_FIRED_NODE (bits_MEMORY_CUSTOM4) + +int gSpitSprite, gSpitDebrisSprite; + + +Vector VecCheckSplatToss( CBaseEntity *pEntity, const Vector &vecSpot1, Vector vecSpot2, float maxHeight ); +void MortarSpray( const Vector &position, const Vector &direction, int spriteModel, int count ); + +#define SF_INFOBM_RUN 0x0001 +#define SF_INFOBM_WAIT 0x0002 + +//========================================================= +// Mortar shot entity +//========================================================= +class CBMortar : public CBaseAnimating +{ + DECLARE_CLASS( CBMortar, CBaseAnimating ); +public: + void Spawn( void ); + + virtual void Precache(); + + static CBMortar *Shoot( CBaseEntity *pOwner, Vector vecStart, Vector vecVelocity ); + void Touch( CBaseEntity *pOther ); + void Animate( void ); + + float m_flDmgTime; + + DECLARE_DATADESC(); + + int m_maxFrame; + int m_iFrame; + + CSprite* pSprite; +}; + +LINK_ENTITY_TO_CLASS( bmortar, CBMortar ); + +BEGIN_DATADESC( CBMortar ) + DEFINE_FIELD( m_maxFrame, FIELD_INTEGER ), + DEFINE_FIELD( m_flDmgTime, FIELD_FLOAT ), + DEFINE_FUNCTION( Animate ), + DEFINE_FIELD( m_iFrame, FIELD_INTEGER ), + DEFINE_FIELD( pSprite, FIELD_CLASSPTR ), +END_DATADESC() + + +// AI Nodes for Big Momma +class CInfoBM : public CPointEntity +{ + DECLARE_CLASS( CInfoBM, CPointEntity ); +public: + void Spawn( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + + // name in pev->targetname + // next in pev->target + // radius in pev->scale + // health in pev->health + // Reach target in pev->message + // Reach delay in pev->speed + // Reach sequence in pev->netname + + DECLARE_DATADESC(); + + float m_flRadius; + float m_flDelay; + string_t m_iszReachTarget; + string_t m_iszReachSequence; + string_t m_iszPreSequence; + + COutputEvent m_OnAnimationEvent; +}; + +LINK_ENTITY_TO_CLASS( info_bigmomma, CInfoBM ); + +BEGIN_DATADESC( CInfoBM ) + DEFINE_FIELD( m_flRadius, FIELD_FLOAT ), + DEFINE_FIELD( m_flDelay, FIELD_FLOAT ), + DEFINE_KEYFIELD( m_iszReachTarget, FIELD_STRING, "reachtarget" ), + DEFINE_KEYFIELD( m_iszReachSequence, FIELD_STRING, "reachsequence" ), + DEFINE_KEYFIELD( m_iszPreSequence, FIELD_STRING, "presequence" ), + DEFINE_OUTPUT( m_OnAnimationEvent, "OnAnimationEvent" ), +END_DATADESC() + + +void CInfoBM::Spawn( void ) +{ + BaseClass::Spawn(); + +// Msg( "Name %s\n", STRING( GetEntityName() ) ); +} + +bool CInfoBM::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq( szKeyName, "radius")) + { + m_flRadius = atof( szValue ); + return true; + } + else if (FStrEq( szKeyName, "reachdelay" )) + { + m_flDelay = atof( szValue); + return true; + } + else if (FStrEq( szKeyName, "health" )) + { + m_iHealth = atoi( szValue ); + return true; + } + + return BaseClass::KeyValue(szKeyName, szValue ); +} + +// UNDONE: +// +#define BIG_CHILDCLASS "monster_babycrab" + + +class CNPC_BigMomma : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_BigMomma, CHL1BaseNPC ); +public: + + void Spawn( void ); + void Precache( void ); + + Class_T Classify( void ) { return CLASS_ALIEN_MONSTER; }; + void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + int OnTakeDamage( const CTakeDamageInfo &info ); + void HandleAnimEvent( animevent_t *pEvent ); + void LayHeadcrab( void ); + void LaunchMortar( void ); + void DeathNotice( CBaseEntity *pevChild ); + + int MeleeAttack1Conditions( float flDot, float flDist ); // Slash + int MeleeAttack2Conditions( float flDot, float flDist ); // Lay a crab + int RangeAttack1Conditions( float flDot, float flDist ); // Mortar launch + + + BOOL CanLayCrab( void ) + { + if ( m_crabTime < gpGlobals->curtime && m_crabCount < BIG_MAXCHILDREN ) + { + // Don't spawn crabs inside each other + Vector mins = GetAbsOrigin() - Vector( 32, 32, 0 ); + Vector maxs = GetAbsOrigin() + Vector( 32, 32, 0 ); + + CBaseEntity *pList[2]; + int count = UTIL_EntitiesInBox( pList, 2, mins, maxs, FL_NPC ); + for ( int i = 0; i < count; i++ ) + { + if ( pList[i] != this ) // Don't hurt yourself! + return COND_NONE; + } + return COND_CAN_MELEE_ATTACK2; + } + + return COND_NONE; + } + + void Activate ( void ); + + void NodeReach( void ); + void NodeStart( string_t iszNextNode ); + bool ShouldGoToNode( void ); + + const char *GetNodeSequence( void ) + { + CInfoBM *pTarget = (CInfoBM*)GetTarget(); + + if ( pTarget && FClassnameIs( pTarget, "info_bigmomma" ) ) + { + return STRING( pTarget->m_iszReachSequence ); // netname holds node sequence + } + + return NULL; + } + + + const char *GetNodePresequence( void ) + { + CInfoBM *pTarget = (CInfoBM *)GetTarget(); + + if ( pTarget && FClassnameIs( pTarget, "info_bigmomma" ) ) + { + return STRING( pTarget->m_iszPreSequence ); + } + return NULL; + } + + float GetNodeDelay( void ) + { + CInfoBM *pTarget = (CInfoBM *)GetTarget(); + + if ( pTarget && FClassnameIs( pTarget, "info_bigmomma" ) ) + { + return pTarget->m_flDelay; // Speed holds node delay + } + return 0; + } + + float GetNodeRange( void ) + { + CInfoBM *pTarget = (CInfoBM *)GetTarget(); + + if ( pTarget && FClassnameIs( pTarget, "info_bigmomma" ) ) + { + return pTarget->m_flRadius; // Scale holds node delay + } + + return 1e6; + } + + float GetNodeYaw( void ) + { + CBaseEntity *pTarget = GetTarget(); + + if ( pTarget ) + { + if ( pTarget->GetAbsAngles().y != 0 ) + return pTarget->GetAbsAngles().y; + } + + return GetAbsAngles().y; + } + + // Restart the crab count on each new level + void OnRestore( void ) + { + BaseClass::OnRestore(); + m_crabCount = 0; + } + + int SelectSchedule( void ); + void StartTask( const Task_t *pTask ); + void RunTask( const Task_t *pTask ); + + float MaxYawSpeed( void ); + + DECLARE_DATADESC(); + + DEFINE_CUSTOM_AI; + + /* + void RunTask( Task_t *pTask ); + void StartTask( Task_t *pTask ); + Schedule_t *GetSchedule( void ); + Schedule_t *GetScheduleOfType( int Type ); + void SetYawSpeed( void ); + + CUSTOM_SCHEDULES; +*/ + +private: + float m_nodeTime; + float m_crabTime; + float m_mortarTime; + float m_painSoundTime; + int m_crabCount; + float m_flDmgTime; + + bool m_bDoneWithPath; + + string_t m_iszTarget; + string_t m_iszNetName; + float m_flWait; + + Vector m_vTossDir; + + +}; + + +BEGIN_DATADESC( CNPC_BigMomma ) + DEFINE_FIELD( m_nodeTime, FIELD_TIME ), + DEFINE_FIELD( m_crabTime, FIELD_TIME ), + DEFINE_FIELD( m_mortarTime, FIELD_TIME ), + DEFINE_FIELD( m_painSoundTime, FIELD_TIME ), + DEFINE_KEYFIELD( m_iszNetName, FIELD_STRING, "netname" ), + DEFINE_FIELD( m_flWait, FIELD_TIME ), + DEFINE_FIELD( m_iszTarget, FIELD_STRING ), + + DEFINE_FIELD( m_crabCount, FIELD_INTEGER ), + DEFINE_FIELD( m_flDmgTime, FIELD_TIME ), + DEFINE_FIELD( m_bDoneWithPath, FIELD_BOOLEAN ), + DEFINE_FIELD( m_vTossDir, FIELD_VECTOR ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS ( monster_bigmomma, CNPC_BigMomma ); + +//========================================================= +// Spawn +//========================================================= +void CNPC_BigMomma::Spawn() +{ + Precache( ); + + SetModel( "models/big_mom.mdl" ); + UTIL_SetSize( this, Vector( -32, -32, 0 ), Vector( 32, 32, 64 ) ); + + Vector vecSurroundingMins( -95, -95, 0 ); + Vector vecSurroundingMaxs( 95, 95, 190 ); + CollisionProp()->SetSurroundingBoundsType( USE_SPECIFIED_BOUNDS, &vecSurroundingMins, &vecSurroundingMaxs ); + + SetNavType( NAV_GROUND ); + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + + m_bloodColor = BLOOD_COLOR_GREEN; + m_iHealth = 150 * sk_bigmomma_health_factor.GetFloat(); + + SetHullType( HULL_WIDE_HUMAN ); + SetHullSizeNormal(); + + CapabilitiesAdd( bits_CAP_MOVE_GROUND ); + CapabilitiesAdd( bits_CAP_INNATE_RANGE_ATTACK1 | bits_CAP_INNATE_MELEE_ATTACK1 | bits_CAP_INNATE_MELEE_ATTACK2 ); + +// pev->view_ofs = Vector ( 0, 0, 128 );// position of the eyes relative to monster's origin. + m_flFieldOfView = 0.3;// indicates the width of this monster's forward view cone ( as a dotproduct result ) + m_NPCState = NPC_STATE_NONE; + + SetRenderColor( 255, 255, 255, 255 ); + + m_bDoneWithPath = false; + + m_nodeTime = 0.0f; + + m_iszTarget = m_iszNetName; + + NPCInit(); + + BaseClass::Spawn(); +} + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CNPC_BigMomma::Precache() +{ + PrecacheModel("models/big_mom.mdl"); + + UTIL_PrecacheOther( BIG_CHILDCLASS ); + + // TEMP: Squid + PrecacheModel("sprites/mommaspit.vmt");// spit projectile. + gSpitSprite = PrecacheModel("sprites/mommaspout.vmt");// client side spittle. + gSpitDebrisSprite = PrecacheModel("sprites/mommablob.vmt" ); + + PrecacheScriptSound( "BigMomma.Pain" ); + PrecacheScriptSound( "BigMomma.Attack" ); + PrecacheScriptSound( "BigMomma.AttackHit" ); + PrecacheScriptSound( "BigMomma.Alert" ); + PrecacheScriptSound( "BigMomma.Birth" ); + PrecacheScriptSound( "BigMomma.Sack" ); + PrecacheScriptSound( "BigMomma.Die" ); + PrecacheScriptSound( "BigMomma.FootstepLeft" ); + PrecacheScriptSound( "BigMomma.FootstepRight" ); + PrecacheScriptSound( "BigMomma.LayHeadcrab" ); + PrecacheScriptSound( "BigMomma.ChildDie" ); + PrecacheScriptSound( "BigMomma.LaunchMortar" ); +} + +//========================================================= +// SetYawSpeed - allows each sequence to have a different +// turn rate associated with it. +//========================================================= +float CNPC_BigMomma::MaxYawSpeed ( void ) +{ + float ys = 90.0f; + + switch ( GetActivity() ) + { + case ACT_IDLE: + ys = 100.0f; + break; + default: + ys = 90.0f; + } + + return ys; +} + +void CNPC_BigMomma::Activate( void ) +{ + if ( GetTarget() == NULL ) + Remember( bits_MEMORY_ADVANCE_NODE ); // Start 'er up + + BaseClass::Activate(); +} + +void CNPC_BigMomma::NodeStart( string_t iszNextNode ) +{ + m_iszTarget = iszNextNode; + + const char *pTargetName = STRING( m_iszTarget ); + + CBaseEntity *pTarget = NULL; + + if ( pTargetName ) + pTarget = gEntList.FindEntityByName( NULL, pTargetName ); + + if ( pTarget == NULL ) + { + //Msg( "BM: Finished the path!!\n" ); + m_bDoneWithPath = true; + return; + } + + SetTarget( pTarget ); +} + + +void CNPC_BigMomma::NodeReach( void ) +{ + CInfoBM *pTarget = (CInfoBM*)GetTarget(); + + Forget( bits_MEMORY_ADVANCE_NODE ); + + if ( !pTarget ) + return; + + if ( pTarget->m_iHealth >= 1 ) + m_iMaxHealth = m_iHealth = pTarget->m_iHealth * sk_bigmomma_health_factor.GetFloat(); + + if ( !HasMemory( bits_MEMORY_FIRED_NODE ) ) + { + if ( pTarget ) + { + pTarget->m_OnAnimationEvent.FireOutput( this, this ); + } + } + + Forget( bits_MEMORY_FIRED_NODE ); + + m_iszTarget = pTarget->m_target; + + if ( pTarget->m_iHealth == 0 ) + Remember( bits_MEMORY_ADVANCE_NODE ); // Move on if no health at this node + else + { + GetNavigator()->ClearGoal(); + } +} + + +void CNPC_BigMomma::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + CTakeDamageInfo dmgInfo = info; + + if ( ptr->hitbox <= 9 ) + { + // didn't hit the sack? + if ( m_flDmgTime != gpGlobals->curtime || (random->RandomInt( 0, 10 ) < 1) ) + { + g_pEffects->Ricochet( ptr->endpos, ptr->plane.normal ); + m_flDmgTime = gpGlobals->curtime; + } + + // don't hurt the monster much, but allow bits_COND_LIGHT_DAMAGE to be generated + dmgInfo.SetDamage( 0.1 ); + } + else + { + SpawnBlood( ptr->endpos + ptr->plane.normal * 15, vecDir, m_bloodColor, 100 ); + + if ( gpGlobals->curtime > m_painSoundTime ) + { + m_painSoundTime = gpGlobals->curtime + random->RandomInt(1, 3); + EmitSound( "BigMomma.Pain" ); + } + } + + BaseClass::TraceAttack( dmgInfo, vecDir, ptr, pAccumulator ); +} + + +int CNPC_BigMomma::OnTakeDamage( const CTakeDamageInfo &info ) +{ + CTakeDamageInfo newInfo = info; + + // Don't take any acid damage -- BigMomma's mortar is acid + if ( newInfo.GetDamageType() & DMG_ACID ) + { + newInfo.SetDamage( 0 ); + } + + // never die from damage, just advance to the next node + if ( ( GetHealth() - newInfo.GetDamage() ) < 1 ) + { + newInfo.SetDamage( 0 ); + Remember( bits_MEMORY_ADVANCE_NODE ); + DevMsg( 2, "BM: Finished node health!!!\n" ); + } + + DevMsg( 2, "BM Health: %f\n", GetHealth() - newInfo.GetDamage() ); + + return BaseClass::OnTakeDamage( newInfo ); +} + +bool CNPC_BigMomma::ShouldGoToNode( void ) +{ + if ( HasMemory( bits_MEMORY_ADVANCE_NODE ) ) + { + if ( m_nodeTime < gpGlobals->curtime ) + return true; + } + return false; +} + + +int CNPC_BigMomma::SelectSchedule( void ) +{ + if ( ShouldGoToNode() ) + { + return SCHED_BIG_NODE; + } + + return BaseClass::SelectSchedule(); +} + + +void CNPC_BigMomma::StartTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_CHECK_NODE_PROXIMITY: + { + + } + + break; + case TASK_FIND_NODE: + { + CBaseEntity *pTarget = GetTarget(); + + if ( !HasMemory( bits_MEMORY_ADVANCE_NODE ) ) + { + if ( pTarget ) + m_iszTarget = pTarget->m_target; + } + + NodeStart( m_iszTarget ); + TaskComplete(); + //Msg( "BM: Found node %s\n", STRING( m_iszTarget ) ); + } + break; + + case TASK_NODE_DELAY: + m_nodeTime = gpGlobals->curtime + pTask->flTaskData; + TaskComplete(); + //Msg( "BM: FAIL! Delay %.2f\n", pTask->flTaskData ); + break; + + case TASK_PROCESS_NODE: + //Msg( "BM: Reached node %s\n", STRING( m_iszTarget ) ); + NodeReach(); + TaskComplete(); + break; + + case TASK_PLAY_NODE_PRESEQUENCE: + case TASK_PLAY_NODE_SEQUENCE: + { + const char *pSequence = NULL; + int iSequence; + + if ( pTask->iTask == TASK_PLAY_NODE_SEQUENCE ) + pSequence = GetNodeSequence(); + else + pSequence = GetNodePresequence(); + + //Msg( "BM: Playing node sequence %s\n", pSequence ); + + if ( pSequence ) //ugh + { + iSequence = LookupSequence( pSequence ); + + if ( iSequence != -1 ) + { + SetIdealActivity( ACT_DO_NOT_DISTURB ); + SetSequence( iSequence ); + SetCycle( 0.0f ); + + ResetSequenceInfo(); + //Msg( "BM: Sequence %s %f\n", GetNodeSequence(), gpGlobals->curtime ); + return; + } + } + TaskComplete(); + } + break; + + case TASK_NODE_YAW: + GetMotor()->SetIdealYaw( GetNodeYaw() ); + TaskComplete(); + break; + + case TASK_WAIT_NODE: + m_flWait = gpGlobals->curtime + GetNodeDelay(); + + /*if ( GetTarget() && GetTarget()->GetSpawnFlags() & SF_INFOBM_WAIT ) + Msg( "BM: Wait at node %s forever\n", STRING( m_iszTarget) ); + else + Msg( "BM: Wait at node %s for %.2f\n", STRING( m_iszTarget ), GetNodeDelay() );*/ + break; + + + case TASK_MOVE_TO_NODE_RANGE: + { + CBaseEntity *pTarget = GetTarget(); + + if ( !pTarget ) + TaskFail( FAIL_NO_TARGET ); + else + { + if ( ( pTarget->GetAbsOrigin() - GetAbsOrigin() ).Length() < GetNodeRange() ) + TaskComplete(); + else + { + Activity act = ACT_WALK; + if ( pTarget->GetSpawnFlags() & SF_INFOBM_RUN ) + act = ACT_RUN; + + AI_NavGoal_t goal( GOALTYPE_TARGETENT, vec3_origin, act ); + + if ( !GetNavigator()->SetGoal( goal ) ) + { + TaskFail( NO_TASK_FAILURE ); + } + } + } + } + //Msg( "BM: Moving to node %s\n", STRING( m_iszTarget ) ); + + break; + + case TASK_MELEE_ATTACK1: + { + + // Play an attack sound here + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "BigMomma.Attack" ); + + BaseClass::StartTask( pTask ); + } + + break; + + default: + BaseClass::StartTask( pTask ); + break; + } +} + +//========================================================= +// RunTask +//========================================================= +void CNPC_BigMomma::RunTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_CHECK_NODE_PROXIMITY: + { + float distance; + + if ( GetTarget() == NULL ) + TaskFail( FAIL_NO_TARGET ); + else + { + if ( GetNavigator()->IsGoalActive() ) + { + distance = ( GetTarget()->GetAbsOrigin() - GetAbsOrigin() ).Length2D(); + // Set the appropriate activity based on an overlapping range + // overlap the range to prevent oscillation + if ( distance < GetNodeRange() ) + { + //Msg( "BM: Reached node PROXIMITY!!\n" ); + TaskComplete(); + GetNavigator()->ClearGoal(); // Stop moving + } + } + else + TaskComplete(); + } + } + + break; + + case TASK_WAIT_NODE: + if ( GetTarget() != NULL && (GetTarget()->GetSpawnFlags() & SF_INFOBM_WAIT) ) + return; + + if ( gpGlobals->curtime > m_flWaitFinished ) + TaskComplete(); + //Msg( "BM: The WAIT is over!\n" ); + break; + + case TASK_PLAY_NODE_PRESEQUENCE: + case TASK_PLAY_NODE_SEQUENCE: + + if ( IsSequenceFinished() ) + { + CBaseEntity *pTarget = NULL; + + if ( GetTarget() ) + pTarget = gEntList.FindEntityByName( NULL, STRING( GetTarget()->m_target ) ); + + if ( pTarget ) + { + SetActivity( ACT_IDLE ); + TaskComplete(); + } + } + + break; + + default: + BaseClass::RunTask( pTask ); + break; + } +} + +//========================================================= +// HandleAnimEvent - catches the monster-specific messages +// that occur when tagged animation frames are played. +// +// Returns number of events handled, 0 if none. +//========================================================= +void CNPC_BigMomma::HandleAnimEvent( animevent_t *pEvent ) +{ + CPASAttenuationFilter filter( this ); + + Vector vecFwd, vecRight, vecUp; + QAngle angles; + angles = GetAbsAngles(); + AngleVectors( angles, &vecFwd, &vecRight, &vecUp ); + + switch( pEvent->event ) + { + case BIG_AE_MELEE_ATTACKBR: + case BIG_AE_MELEE_ATTACKBL: + case BIG_AE_MELEE_ATTACK1: + { + Vector center = GetAbsOrigin() + vecFwd * 128; + Vector mins = center - Vector( 64, 64, 0 ); + Vector maxs = center + Vector( 64, 64, 64 ); + + CBaseEntity *pList[8]; + int count = UTIL_EntitiesInBox( pList, 8, mins, maxs, FL_NPC | FL_CLIENT ); + CBaseEntity *pHurt = NULL; + + for ( int i = 0; i < count && !pHurt; i++ ) + { + if ( pList[i] != this ) + { + if ( pList[i]->GetOwnerEntity() != this ) + { + pHurt = pList[i]; + } + } + } + + if ( pHurt ) + { + CTakeDamageInfo info( this, this, 15, DMG_CLUB | DMG_SLASH ); + CalculateMeleeDamageForce( &info, (pHurt->GetAbsOrigin() - GetAbsOrigin()), pHurt->GetAbsOrigin() ); + pHurt->TakeDamage( info ); + QAngle newAngles = angles; + newAngles.x = 15; + if ( pHurt->IsPlayer() ) + { + ((CBasePlayer *)pHurt)->SetPunchAngle( newAngles ); + } + switch( pEvent->event ) + { + case BIG_AE_MELEE_ATTACKBR: +// pHurt->pev->velocity = pHurt->pev->velocity + (vecFwd * 150) + Vector(0,0,250) - (vecRight * 200); + pHurt->SetAbsVelocity( pHurt->GetAbsVelocity() + (vecFwd * 150) + Vector(0,0,250) - (vecRight * 200) ); + break; + + case BIG_AE_MELEE_ATTACKBL: + pHurt->SetAbsVelocity( pHurt->GetAbsVelocity() + (vecFwd * 150) + Vector(0,0,250) + (vecRight * 200) ); + break; + + case BIG_AE_MELEE_ATTACK1: + pHurt->SetAbsVelocity( pHurt->GetAbsVelocity() + (vecFwd * 220) + Vector(0,0,200) ); + break; + } + + pHurt->SetGroundEntity( NULL ); + EmitSound( filter, entindex(), "BigMomma.AttackHit" ); + } + } + break; + + case BIG_AE_SCREAM: + EmitSound( filter, entindex(), "BigMomma.Alert" ); + break; + + case BIG_AE_PAIN_SOUND: + EmitSound( filter, entindex(), "BigMomma.Pain" ); + break; + + case BIG_AE_ATTACK_SOUND: + EmitSound( filter, entindex(), "BigMomma.Attack" ); + break; + + case BIG_AE_BIRTH_SOUND: + EmitSound( filter, entindex(), "BigMomma.Birth" ); + break; + + case BIG_AE_SACK: + if ( RandomInt(0,100) < 30 ) + { + EmitSound( filter, entindex(), "BigMomma.Sack" ); + } + break; + + case BIG_AE_DEATHSOUND: + EmitSound( filter, entindex(), "BigMomma.Die" ); + break; + + case BIG_AE_STEP1: // Footstep left + case BIG_AE_STEP3: // Footstep back left + EmitSound( filter, entindex(), "BigMomma.FootstepLeft" ); + break; + + case BIG_AE_STEP4: // Footstep back right + case BIG_AE_STEP2: // Footstep right + EmitSound( filter, entindex(), "BigMomma.FootstepRight" ); + break; + + case BIG_AE_MORTAR_ATTACK1: + LaunchMortar(); + break; + + case BIG_AE_LAY_CRAB: + LayHeadcrab(); + break; + + case BIG_AE_JUMP_FORWARD: + SetGroundEntity( NULL ); + SetAbsOrigin(GetAbsOrigin() + Vector ( 0 , 0 , 1) );// take him off ground so engine doesn't instantly reset onground + SetAbsVelocity(vecFwd * 200 + vecUp * 500 ); + break; + + case BIG_AE_EARLY_TARGET: + { + CInfoBM *pTarget = (CInfoBM*) GetTarget(); + + if ( pTarget ) + { + pTarget->m_OnAnimationEvent.FireOutput( this, this ); + } + + Remember( bits_MEMORY_FIRED_NODE ); + } + break; + + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } +} + + +void CNPC_BigMomma::LayHeadcrab( void ) +{ + CBaseEntity *pChild = CBaseEntity::Create( BIG_CHILDCLASS, GetAbsOrigin(), GetAbsAngles(), this ); + + pChild->AddSpawnFlags( SF_NPC_FALL_TO_GROUND ); + + pChild->SetOwnerEntity( this ); + + // Is this the second crab in a pair? + if ( HasMemory( bits_MEMORY_CHILDPAIR ) ) + { + m_crabTime = gpGlobals->curtime + RandomFloat( 5, 10 ); + Forget( bits_MEMORY_CHILDPAIR ); + } + else + { + m_crabTime = gpGlobals->curtime + RandomFloat( 0.5, 2.5 ); + Remember( bits_MEMORY_CHILDPAIR ); + } + + trace_t tr; + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() - Vector(0,0,100), MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); + UTIL_DecalTrace( &tr, "Splash" ); + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "BigMomma.LayHeadcrab" ); + + m_crabCount++; +} + +void CNPC_BigMomma::DeathNotice( CBaseEntity *pevChild ) +{ + if ( m_crabCount > 0 ) // Some babies may cross a transition, but we reset the count then + { + m_crabCount--; + } + if ( IsAlive() ) + { + // Make the "my baby's dead" noise! + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "BigMomma.ChildDie" ); + } +} + + +void CNPC_BigMomma::LaunchMortar( void ) +{ + m_mortarTime = gpGlobals->curtime + RandomFloat( 2, 15 ); + + Vector startPos = GetAbsOrigin(); + startPos.z += 180; + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "BigMomma.LaunchMortar" ); + + CBMortar *pBomb = CBMortar::Shoot( this, startPos, m_vTossDir ); + pBomb->SetGravity( 1.0 ); + MortarSpray( startPos, Vector(0,0,10), gSpitSprite, 24 ); +} + +int CNPC_BigMomma::MeleeAttack1Conditions( float flDot, float flDist ) +{ + if (flDot >= 0.7) + { + if ( flDist > BIG_ATTACKDIST ) + return COND_TOO_FAR_TO_ATTACK; + else + return COND_CAN_MELEE_ATTACK1; + } + else + { + return COND_NOT_FACING_ATTACK; + } + + return COND_NONE; +} + + +// Lay a crab +int CNPC_BigMomma::MeleeAttack2Conditions( float flDot, float flDist ) +{ + return CanLayCrab(); +} + + +Vector VecCheckSplatToss( CBaseEntity *pEnt, const Vector &vecSpot1, Vector vecSpot2, float maxHeight ) +{ + trace_t tr; + Vector vecMidPoint;// halfway point between Spot1 and Spot2 + Vector vecApex;// highest point + Vector vecScale; + Vector vecGrenadeVel; + Vector vecTemp; + float flGravity = GetCurrentGravity(); + + // calculate the midpoint and apex of the 'triangle' + vecMidPoint = vecSpot1 + (vecSpot2 - vecSpot1) * 0.5; + UTIL_TraceLine(vecMidPoint, vecMidPoint + Vector(0,0,maxHeight), MASK_SOLID_BRUSHONLY, pEnt, COLLISION_GROUP_NONE, &tr ); + vecApex = tr.endpos; + + UTIL_TraceLine(vecSpot1, vecApex, MASK_SOLID, pEnt, COLLISION_GROUP_NONE, &tr ); + if (tr.fraction != 1.0) + { + // fail! + return vec3_origin; + } + + // Don't worry about actually hitting the target, this won't hurt us! + + // How high should the grenade travel (subtract 15 so the grenade doesn't hit the ceiling)? + float height = (vecApex.z - vecSpot1.z) - 15; + + //HACK HACK + if ( height < 0 ) + height *= -1; + + // How fast does the grenade need to travel to reach that height given gravity? + float speed = sqrt( 2 * flGravity * height ); + + // How much time does it take to get there? + float time = speed / flGravity; + vecGrenadeVel = (vecSpot2 - vecSpot1); + vecGrenadeVel.z = 0; + + // Travel half the distance to the target in that time (apex is at the midpoint) + vecGrenadeVel = vecGrenadeVel * ( 0.5 / time ); + // Speed to offset gravity at the desired height + vecGrenadeVel.z = speed; + + return vecGrenadeVel; +} + +// Mortar launch +int CNPC_BigMomma::RangeAttack1Conditions( float flDot, float flDist ) +{ + if ( flDist > BIG_MORTARDIST ) + return COND_TOO_FAR_TO_ATTACK; + + if ( flDist <= BIG_MORTARDIST && m_mortarTime < gpGlobals->curtime ) + { + CBaseEntity *pEnemy = GetEnemy(); + + if ( pEnemy ) + { + Vector startPos = GetAbsOrigin(); + startPos.z += 180; + + m_vTossDir = VecCheckSplatToss( this, startPos, pEnemy->BodyTarget( GetAbsOrigin() ), random->RandomFloat( 150, 500 ) ); + + if ( m_vTossDir != vec3_origin ) + return COND_CAN_RANGE_ATTACK1; + } + } + + return COND_NONE; +} + +// --------------------------------- +// +// Mortar +// +// --------------------------------- +void MortarSpray( const Vector &position, const Vector &direction, int spriteModel, int count ) +{ + CPVSFilter filter( position ); + + te->SpriteSpray( filter, 0.0, &position, &direction, spriteModel, 200, 80, count ); +} + + +// UNDONE: right now this is pretty much a copy of the squid spit with minor changes to the way it does damage +void CBMortar:: Spawn( void ) +{ + SetMoveType( MOVETYPE_FLYGRAVITY ); + SetClassname( "bmortar" ); + + SetSolid( SOLID_BBOX ); + + pSprite = CSprite::SpriteCreate( "sprites/mommaspit.vmt", GetAbsOrigin(), true ); + + if ( pSprite ) + { + pSprite->SetAttachment( this, 0 ); + pSprite->m_flSpriteFramerate = 5; + + pSprite->m_nRenderMode = kRenderTransAlpha; + pSprite->SetBrightness( 255 ); + + m_iFrame = 0; + + pSprite->SetScale( 2.5f ); + } + + UTIL_SetSize( this, Vector( 0, 0, 0), Vector(0, 0, 0) ); + + m_maxFrame = (float)modelinfo->GetModelFrameCount( GetModel() ) - 1; + m_flDmgTime = gpGlobals->curtime + 0.4; +} + +void CBMortar::Animate( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1 ); + + Vector vVelocity = GetAbsVelocity(); + + VectorNormalize( vVelocity ); + + if ( gpGlobals->curtime > m_flDmgTime ) + { + m_flDmgTime = gpGlobals->curtime + 0.2; + MortarSpray( GetAbsOrigin() + Vector( 0, 0, 15 ), -vVelocity, gSpitSprite, 3 ); + } + if ( m_iFrame++ ) + { + if ( m_iFrame > m_maxFrame ) + { + m_iFrame = 0; + } + } +} + +CBMortar *CBMortar::Shoot( CBaseEntity *pOwner, Vector vecStart, Vector vecVelocity ) +{ + CBMortar *pSpit = CREATE_ENTITY( CBMortar, "bmortar" ); + pSpit->Spawn(); + + UTIL_SetOrigin( pSpit, vecStart ); + pSpit->SetAbsVelocity( vecVelocity ); + pSpit->SetOwnerEntity( pOwner ); + pSpit->SetThink ( &CBMortar::Animate ); + pSpit->SetNextThink( gpGlobals->curtime + 0.1 ); + + return pSpit; +} + +void CBMortar::Precache() +{ + BaseClass::Precache(); + + PrecacheScriptSound( "NPC_BigMomma.SpitTouch1" ); + PrecacheScriptSound( "NPC_BigMomma.SpitHit1" ); + PrecacheScriptSound( "NPC_BigMomma.SpitHit2" ); +} + +void CBMortar::Touch( CBaseEntity *pOther ) +{ + trace_t tr; + int iPitch; + + // splat sound + iPitch = random->RandomFloat( 90, 110 ); + + EmitSound( "NPC_BigMomma.SpitTouch1" ); + + switch ( random->RandomInt( 0, 1 ) ) + { + case 0: + EmitSound( "NPC_BigMomma.SpitHit1" ); + break; + case 1: + EmitSound( "NPC_BigMomma.SpitHit2" ); + break; + } + + if ( pOther->IsBSPModel() ) + { + // make a splat on the wall + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + GetAbsVelocity() * 10, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); + UTIL_DecalTrace( &tr, "Splash" ); + } + else + { + tr.endpos = GetAbsOrigin(); + + Vector vVelocity = GetAbsVelocity(); + VectorNormalize( vVelocity ); + + tr.plane.normal = -1 * vVelocity; + } + // make some flecks + MortarSpray( tr.endpos + Vector( 0, 0, 15 ), tr.plane.normal, gSpitSprite, 24 ); + + CBaseEntity *pOwner = GetOwnerEntity(); + + RadiusDamage( CTakeDamageInfo( this, pOwner, sk_bigmomma_dmg_blast.GetFloat(), DMG_ACID ), GetAbsOrigin(), sk_bigmomma_radius_blast.GetFloat(), CLASS_NONE, NULL ); + + UTIL_Remove( pSprite ); + UTIL_Remove( this ); +} + + +AI_BEGIN_CUSTOM_NPC( monster_bigmomma, CNPC_BigMomma ) + + DECLARE_TASK( TASK_MOVE_TO_NODE_RANGE ) + DECLARE_TASK( TASK_FIND_NODE ) + DECLARE_TASK( TASK_PLAY_NODE_PRESEQUENCE ) + DECLARE_TASK( TASK_PLAY_NODE_SEQUENCE ) + DECLARE_TASK( TASK_PROCESS_NODE ) + DECLARE_TASK( TASK_WAIT_NODE ) + DECLARE_TASK( TASK_NODE_DELAY ) + DECLARE_TASK( TASK_NODE_YAW ) + DECLARE_TASK( TASK_CHECK_NODE_PROXIMITY ) + + + //========================================================= + // > SCHED_BIG_NODE + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_NODE_FAIL, + + " Tasks" + " TASK_NODE_DELAY 3" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " " + " Interrupts" + ) + + //========================================================= + // > SCHED_BIG_NODE + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_BIG_NODE, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_NODE_FAIL" + " TASK_STOP_MOVING 0" + " TASK_FIND_NODE 0" + " TASK_PLAY_NODE_PRESEQUENCE 0" + " TASK_MOVE_TO_NODE_RANGE 0" + " TASK_CHECK_NODE_PROXIMITY 0" + " TASK_STOP_MOVING 0" + " TASK_NODE_YAW 0" + " TASK_FACE_IDEAL 0" + " TASK_WAIT_NODE 0" + " TASK_PLAY_NODE_SEQUENCE 0" + " TASK_PROCESS_NODE 0" + + " " + " Interrupts" + ) + +AI_END_CUSTOM_NPC() + + diff --git a/game/server/hl1/hl1_npc_bloater.cpp b/game/server/hl1/hl1_npc_bloater.cpp new file mode 100644 index 0000000..b5c57c9 --- /dev/null +++ b/game/server/hl1/hl1_npc_bloater.cpp @@ -0,0 +1,86 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "basecombatweapon.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "hl1_ai_basenpc.h" + +class CNPC_Bloater : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_Bloater, CHL1BaseNPC ); +public: + + void Spawn( void ); + void Precache( void ); + /*void SetYawSpeed( void ); + int Classify ( void ); + void HandleAnimEvent( MonsterEvent_t *pEvent ); + + void PainSound( const CTakeDamageInfo &info ); + void AlertSound( void ); + void IdleSound( void ); + void AttackSnd( void ); + + // No range attacks + BOOL CheckRangeAttack1 ( float flDot, float flDist ) { return FALSE; } + BOOL CheckRangeAttack2 ( float flDot, float flDist ) { return FALSE; } + int TakeDamage( entvars_t *pevInflictor, entvars_t *pevAttacker, float flDamage, int bitsDamageType );*/ +}; + +LINK_ENTITY_TO_CLASS( monster_bloater, CNPC_Bloater ); + +//========================================================= +// Spawn +//========================================================= +void CNPC_Bloater::Spawn() +{ + Precache( ); + + SetModel( "models/floater.mdl"); +// UTIL_SetSize( this, VEC_HUMAN_HULL_MIN, VEC_HUMAN_HULL_MAX ); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_FLY ); + m_spawnflags |= FL_FLY; + m_bloodColor = BLOOD_COLOR_GREEN; + m_iHealth = 40; +// pev->view_ofs = VEC_VIEW;// position of the eyes relative to monster's origin. + m_flFieldOfView = 0.5;// indicates the width of this monster's forward view cone ( as a dotproduct result ) + m_NPCState = NPC_STATE_NONE; + + SetRenderColor( 255, 255, 255, 255 ); + + NPCInit(); +} + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CNPC_Bloater::Precache() +{ + PrecacheModel("models/floater.mdl"); +} diff --git a/game/server/hl1/hl1_npc_bullsquid.cpp b/game/server/hl1/hl1_npc_bullsquid.cpp new file mode 100644 index 0000000..373278d --- /dev/null +++ b/game/server/hl1/hl1_npc_bullsquid.cpp @@ -0,0 +1,1167 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Cute hound like Alien. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "game.h" +#include "ai_default.h" +#include "ai_schedule.h" +#include "ai_hull.h" +#include "ai_route.h" +#include "ai_hint.h" +#include "ai_navigator.h" +#include "ai_senses.h" +#include "npcevent.h" +#include "animation.h" +#include "hl1_npc_bullsquid.h" +#include "gib.h" +#include "soundent.h" +#include "ndebugoverlay.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "hl1_grenade_spit.h" +#include "util.h" +#include "shake.h" +#include "movevars_shared.h" +#include "decals.h" +#include "hl2_shareddefs.h" +#include "ammodef.h" + +#define SQUID_SPRINT_DIST 256 // how close the squid has to get before starting to sprint and refusing to swerve + +ConVar sk_bullsquid_health ( "sk_bullsquid_health", "0" ); +ConVar sk_bullsquid_dmg_bite ( "sk_bullsquid_dmg_bite", "0" ); +ConVar sk_bullsquid_dmg_whip ( "sk_bullsquid_dmg_whip", "0" ); +extern ConVar sk_bullsquid_dmg_spit; + +//========================================================= +// monster-specific schedule types +//========================================================= +enum +{ + SCHED_SQUID_HURTHOP = LAST_SHARED_SCHEDULE, + SCHED_SQUID_SEECRAB, + SCHED_SQUID_EAT, + SCHED_SQUID_SNIFF_AND_EAT, + SCHED_SQUID_WALLOW, + SCHED_SQUID_CHASE_ENEMY, +}; + +//========================================================= +// monster-specific tasks +//========================================================= +enum +{ + TASK_SQUID_HOPTURN = LAST_SHARED_TASK, + TASK_SQUID_EAT, +}; + +//----------------------------------------------------------------------------- +// Squid Conditions +//----------------------------------------------------------------------------- +enum +{ + COND_SQUID_SMELL_FOOD = LAST_SHARED_CONDITION + 1, +}; + + +//========================================================= +// Monster's Anim Events Go Here +//========================================================= +#define BSQUID_AE_SPIT ( 1 ) +#define BSQUID_AE_BITE ( 2 ) +#define BSQUID_AE_BLINK ( 3 ) +#define BSQUID_AE_TAILWHIP ( 4 ) +#define BSQUID_AE_HOP ( 5 ) +#define BSQUID_AE_THROW ( 6 ) + +LINK_ENTITY_TO_CLASS( monster_bullchicken, CNPC_Bullsquid ); + +int ACT_SQUID_EXCITED; +int ACT_SQUID_EAT; +int ACT_SQUID_DETECT_SCENT; +int ACT_SQUID_INSPECT_FLOOR; + +//========================================================= +// Bullsquid's spit projectile +//========================================================= +class CSquidSpit : public CBaseEntity +{ + DECLARE_CLASS( CSquidSpit, CBaseEntity ); +public: + void Spawn( void ); + void Precache( void ); + + static void Shoot( CBaseEntity *pOwner, Vector vecStart, Vector vecVelocity ); + void Touch( CBaseEntity *pOther ); + void Animate( void ); + + int m_nSquidSpitSprite; + + DECLARE_DATADESC(); + + void SetSprite( CBaseEntity *pSprite ) + { + m_hSprite = pSprite; + } + + CBaseEntity *GetSprite( void ) + { + return m_hSprite.Get(); + } + +private: + EHANDLE m_hSprite; + + +}; + +LINK_ENTITY_TO_CLASS( squidspit, CSquidSpit ); + +BEGIN_DATADESC( CSquidSpit ) + DEFINE_FIELD( m_nSquidSpitSprite, FIELD_INTEGER ), + DEFINE_FIELD( m_hSprite, FIELD_EHANDLE ), +END_DATADESC() + + +void CSquidSpit::Precache( void ) +{ + m_nSquidSpitSprite = PrecacheModel("sprites/bigspit.vmt");// client side spittle. + + PrecacheScriptSound( "NPC_BigMomma.SpitTouch1" ); + PrecacheScriptSound( "NPC_BigMomma.SpitHit1" ); + PrecacheScriptSound( "NPC_BigMomma.SpitHit2" ); +} + +void CSquidSpit:: Spawn( void ) +{ + Precache(); + + SetMoveType ( MOVETYPE_FLY ); + SetClassname( "squidspit" ); + + SetSolid( SOLID_BBOX ); + + m_nRenderMode = kRenderTransAlpha; + SetRenderColorA( 255 ); + SetModel( "" ); + + SetSprite( CSprite::SpriteCreate( "sprites/bigspit.vmt", GetAbsOrigin(), true ) ); + + UTIL_SetSize( this, Vector( 0, 0, 0), Vector(0, 0, 0) ); + + SetCollisionGroup( HL2COLLISION_GROUP_SPIT ); +} + +void CSquidSpit::Shoot( CBaseEntity *pOwner, Vector vecStart, Vector vecVelocity ) +{ + CSquidSpit *pSpit = CREATE_ENTITY( CSquidSpit, "squidspit" ); + pSpit->Spawn(); + + UTIL_SetOrigin( pSpit, vecStart ); + pSpit->SetAbsVelocity( vecVelocity ); + pSpit->SetOwnerEntity( pOwner ); + + CSprite *pSprite = (CSprite*)pSpit->GetSprite(); + + if ( pSprite ) + { + pSprite->SetAttachment( pSpit, 0 ); + pSprite->SetOwnerEntity( pSpit ); + + pSprite->SetScale( 0.5 ); + pSprite->SetTransparency( pSpit->m_nRenderMode, pSpit->m_clrRender->r, pSpit->m_clrRender->g, pSpit->m_clrRender->b, pSpit->m_clrRender->a, pSpit->m_nRenderFX ); + } + + + CPVSFilter filter( vecStart ); + + VectorNormalize( vecVelocity ); + te->SpriteSpray( filter, 0.0, &vecStart , &vecVelocity, pSpit->m_nSquidSpitSprite, 210, 25, 15 ); +} + +void CSquidSpit::Touch ( CBaseEntity *pOther ) +{ + trace_t tr; + int iPitch; + + if ( pOther->GetSolidFlags() & FSOLID_TRIGGER ) + return; + + if ( pOther->GetCollisionGroup() == HL2COLLISION_GROUP_SPIT) + { + return; + } + + // splat sound + iPitch = random->RandomFloat( 90, 110 ); + + EmitSound( "NPC_BigMomma.SpitTouch1" ); + + switch ( random->RandomInt( 0, 1 ) ) + { + case 0: + EmitSound( "NPC_BigMomma.SpitHit1" ); + break; + case 1: + EmitSound( "NPC_BigMomma.SpitHit2" ); + break; + } + + if ( !pOther->m_takedamage ) + { + // make a splat on the wall + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + GetAbsVelocity() * 10, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); + UTIL_DecalTrace(&tr, "BeerSplash" ); + + // make some flecks + CPVSFilter filter( tr.endpos ); + + te->SpriteSpray( filter, 0.0, &tr.endpos, &tr.plane.normal, m_nSquidSpitSprite, 30, 8, 5 ); + + } + else + { + CTakeDamageInfo info( this, this, sk_bullsquid_dmg_spit.GetFloat(), DMG_BULLET ); + CalculateBulletDamageForce( &info, GetAmmoDef()->Index("9mmRound"), GetAbsVelocity(), GetAbsOrigin() ); + pOther->TakeDamage( info ); + } + + UTIL_Remove( m_hSprite ); + UTIL_Remove( this ); +} + + +BEGIN_DATADESC( CNPC_Bullsquid ) + DEFINE_FIELD( m_fCanThreatDisplay, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flLastHurtTime, FIELD_TIME ), + DEFINE_FIELD( m_flNextSpitTime, FIELD_TIME ), + + DEFINE_FIELD( m_flHungryTime, FIELD_TIME ), +END_DATADESC() + +//========================================================= +// Spawn +//========================================================= +void CNPC_Bullsquid::Spawn() +{ + Precache( ); + + SetModel( "models/bullsquid.mdl"); + SetHullType(HULL_WIDE_SHORT); + SetHullSizeNormal(); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + m_bloodColor = BLOOD_COLOR_GREEN; + + SetRenderColor( 255, 255, 255, 255 ); + + m_iHealth = sk_bullsquid_health.GetFloat(); + m_flFieldOfView = 0.2;// indicates the width of this monster's forward view cone ( as a dotproduct result ) + m_NPCState = NPC_STATE_NONE; + + CapabilitiesClear(); + CapabilitiesAdd( bits_CAP_MOVE_GROUND | bits_CAP_INNATE_RANGE_ATTACK1 | bits_CAP_INNATE_MELEE_ATTACK1 | bits_CAP_INNATE_MELEE_ATTACK2 ); + + m_fCanThreatDisplay = TRUE; + m_flNextSpitTime = gpGlobals->curtime; + + NPCInit(); + + m_flDistTooFar = 784; +} + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CNPC_Bullsquid::Precache() +{ + BaseClass::Precache(); + + PrecacheModel("models/bullsquid.mdl"); + + PrecacheModel("sprites/bigspit.vmt");// spit projectile. + + PrecacheScriptSound( "Bullsquid.Idle" ); + PrecacheScriptSound( "Bullsquid.Pain" ); + PrecacheScriptSound( "Bullsquid.Alert" ); + PrecacheScriptSound( "Bullsquid.Die" ); + PrecacheScriptSound( "Bullsquid.Attack" ); + PrecacheScriptSound( "Bullsquid.Bite" ); + PrecacheScriptSound( "Bullsquid.Growl" ); +} + + +int CNPC_Bullsquid::TranslateSchedule( int scheduleType ) +{ + switch ( scheduleType ) + { + case SCHED_CHASE_ENEMY: + return SCHED_SQUID_CHASE_ENEMY; + break; + } + + return BaseClass::TranslateSchedule( scheduleType ); +} + +//----------------------------------------------------------------------------- +// Purpose: Indicates this monster's place in the relationship table. +// Output : +//----------------------------------------------------------------------------- +Class_T CNPC_Bullsquid::Classify( void ) +{ + return CLASS_ALIEN_PREDATOR; +} + +//========================================================= +// IdleSound +//========================================================= +#define SQUID_ATTN_IDLE (float)1.5 +void CNPC_Bullsquid::IdleSound( void ) +{ + CPASAttenuationFilter filter( this, SQUID_ATTN_IDLE ); + EmitSound( filter, entindex(), "Bullsquid.Idle" ); +} + +//========================================================= +// PainSound +//========================================================= +void CNPC_Bullsquid::PainSound( const CTakeDamageInfo &info ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Bullsquid.Pain" ); +} + +//========================================================= +// AlertSound +//========================================================= +void CNPC_Bullsquid::AlertSound( void ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Bullsquid.Alert" ); +} + +//========================================================= +// DeathSound +//========================================================= +void CNPC_Bullsquid::DeathSound( const CTakeDamageInfo &info ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Bullsquid.Die" ); +} + +//========================================================= +// AttackSound +//========================================================= +void CNPC_Bullsquid::AttackSound( void ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Bullsquid.Attack" ); +} + +//========================================================= +// SetYawSpeed - allows each sequence to have a different +// turn rate associated with it. +//========================================================= +float CNPC_Bullsquid::MaxYawSpeed( void ) +{ + float flYS = 0; + + switch ( GetActivity() ) + { + case ACT_WALK: flYS = 90; break; + case ACT_RUN: flYS = 90; break; + case ACT_IDLE: flYS = 90; break; + case ACT_RANGE_ATTACK1: flYS = 90; break; + default: + flYS = 90; + break; + } + + return flYS; +} + +//========================================================= +// HandleAnimEvent - catches the monster-specific messages +// that occur when tagged animation frames are played. +//========================================================= +void CNPC_Bullsquid::HandleAnimEvent( animevent_t *pEvent ) +{ + switch( pEvent->event ) + { + case BSQUID_AE_SPIT: + { + if ( GetEnemy() ) + { + Vector vecSpitOffset; + Vector vecSpitDir; + Vector vRight, vUp, vForward; + + AngleVectors ( GetAbsAngles(), &vForward, &vRight, &vUp ); + + // !!!HACKHACK - the spot at which the spit originates (in front of the mouth) was measured in 3ds and hardcoded here. + // we should be able to read the position of bones at runtime for this info. + vecSpitOffset = ( vRight * 8 + vForward * 60 + vUp * 50 ); + vecSpitOffset = ( GetAbsOrigin() + vecSpitOffset ); + vecSpitDir = ( ( GetEnemy()->BodyTarget( GetAbsOrigin() ) ) - vecSpitOffset ); + + VectorNormalize( vecSpitDir ); + + vecSpitDir.x += random->RandomFloat( -0.05, 0.05 ); + vecSpitDir.y += random->RandomFloat( -0.05, 0.05 ); + vecSpitDir.z += random->RandomFloat( -0.05, 0 ); + + AttackSound(); + + CSquidSpit::Shoot( this, vecSpitOffset, vecSpitDir * 900 ); + } + } + break; + + case BSQUID_AE_BITE: + { + // SOUND HERE! + CBaseEntity *pHurt = CheckTraceHullAttack( 70, Vector(-16,-16,-16), Vector(16,16,16), sk_bullsquid_dmg_bite.GetFloat(), DMG_SLASH ); + if ( pHurt ) + { + Vector forward, up; + AngleVectors( GetAbsAngles(), &forward, NULL, &up ); + pHurt->SetAbsVelocity( pHurt->GetAbsVelocity() - (forward * 100) ); + pHurt->SetAbsVelocity( pHurt->GetAbsVelocity() + (up * 100) ); + pHurt->SetGroundEntity( NULL ); + } + } + break; + + case BSQUID_AE_TAILWHIP: + { + CBaseEntity *pHurt = CheckTraceHullAttack( 70, Vector(-16,-16,-16), Vector(16,16,16), sk_bullsquid_dmg_whip.GetFloat(), DMG_SLASH | DMG_ALWAYSGIB ); + if ( pHurt ) + { + Vector right, up; + AngleVectors( GetAbsAngles(), NULL, &right, &up ); + + if ( pHurt->GetFlags() & ( FL_NPC | FL_CLIENT ) ) + pHurt->ViewPunch( QAngle( 20, 0, -20 ) ); + + pHurt->SetAbsVelocity( pHurt->GetAbsVelocity() + (right * 200) ); + pHurt->SetAbsVelocity( pHurt->GetAbsVelocity() + (up * 100) ); + } + } + break; + + case BSQUID_AE_BLINK: + { + // close eye. + m_nSkin = 1; + } + break; + + case BSQUID_AE_HOP: + { + float flGravity = GetCurrentGravity(); + + // throw the squid up into the air on this frame. + if ( GetFlags() & FL_ONGROUND ) + { + SetGroundEntity( NULL ); + } + + // jump into air for 0.8 (24/30) seconds + Vector vecVel = GetAbsVelocity(); + vecVel.z += ( 0.625 * flGravity ) * 0.5; + SetAbsVelocity( vecVel ); + } + break; + + case BSQUID_AE_THROW: + { + // squid throws its prey IF the prey is a client. + CBaseEntity *pHurt = CheckTraceHullAttack( 70, Vector(-16,-16,-16), Vector(16,16,16), 0, 0 ); + + + if ( pHurt ) + { + // croonchy bite sound + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Bullsquid.Bite" ); + + // screeshake transforms the viewmodel as well as the viewangle. No problems with seeing the ends of the viewmodels. + UTIL_ScreenShake( pHurt->GetAbsOrigin(), 25.0, 1.5, 0.7, 2, SHAKE_START ); + + if ( pHurt->IsPlayer() ) + { + Vector forward, up; + AngleVectors( GetAbsAngles(), &forward, NULL, &up ); + + pHurt->SetAbsVelocity( pHurt->GetAbsVelocity() + forward * 300 + up * 300 ); + } + } + } + break; + + default: + BaseClass::HandleAnimEvent( pEvent ); + } +} + +int CNPC_Bullsquid::RangeAttack1Conditions( float flDot, float flDist ) +{ + if ( IsMoving() && flDist >= 512 ) + { + // squid will far too far behind if he stops running to spit at this distance from the enemy. + return ( COND_NONE ); + } + + if ( flDist > 85 && flDist <= 784 && flDot >= 0.5 && gpGlobals->curtime >= m_flNextSpitTime ) + { + if ( GetEnemy() != NULL ) + { + if ( fabs( GetAbsOrigin().z - GetEnemy()->GetAbsOrigin().z ) > 256 ) + { + // don't try to spit at someone up really high or down really low. + return( COND_NONE ); + } + } + + if ( IsMoving() ) + { + // don't spit again for a long time, resume chasing enemy. + m_flNextSpitTime = gpGlobals->curtime + 5; + } + else + { + // not moving, so spit again pretty soon. + m_flNextSpitTime = gpGlobals->curtime + 0.5; + } + + return( COND_CAN_RANGE_ATTACK1 ); + } + + return( COND_NONE ); +} + +//========================================================= +// MeleeAttack2Conditions - bullsquid is a big guy, so has a longer +// melee range than most monsters. This is the tailwhip attack +//========================================================= +int CNPC_Bullsquid::MeleeAttack1Conditions( float flDot, float flDist ) +{ + if ( GetEnemy()->m_iHealth <= sk_bullsquid_dmg_whip.GetFloat() && flDist <= 85 && flDot >= 0.7 ) + { + return ( COND_CAN_MELEE_ATTACK1 ); + } + + return( COND_NONE ); +} + +//========================================================= +// MeleeAttack2Conditions - bullsquid is a big guy, so has a longer +// melee range than most monsters. This is the bite attack. +// this attack will not be performed if the tailwhip attack +// is valid. +//========================================================= +int CNPC_Bullsquid::MeleeAttack2Conditions( float flDot, float flDist ) +{ + if ( flDist <= 85 && flDot >= 0.7 && !HasCondition( COND_CAN_MELEE_ATTACK1 ) ) // The player & bullsquid can be as much as their bboxes + return ( COND_CAN_MELEE_ATTACK2 ); + + return( COND_NONE ); +} + +bool CNPC_Bullsquid::FValidateHintType ( CAI_Hint *pHint ) +{ + if ( pHint->HintType() == HINT_HL1_WORLD_HUMAN_BLOOD ) + return true; + + Msg ( "Couldn't validate hint type" ); + + return false; +} + +void CNPC_Bullsquid::RemoveIgnoredConditions( void ) +{ + if ( m_flHungryTime > gpGlobals->curtime ) + ClearCondition( COND_SQUID_SMELL_FOOD ); + + if ( gpGlobals->curtime - m_flLastHurtTime <= 20 ) + { + // haven't been hurt in 20 seconds, so let the squid care about stink. + ClearCondition( COND_SMELL ); + } + + if ( GetEnemy() != NULL ) + { + // ( Unless after a tasty headcrab, yumm ^_^ ) + if ( FClassnameIs( GetEnemy(), "monster_headcrab" ) ) + ClearCondition( COND_SMELL ); + } +} + +Disposition_t CNPC_Bullsquid::IRelationType( CBaseEntity *pTarget ) +{ + if ( gpGlobals->curtime - m_flLastHurtTime < 5 && FClassnameIs ( pTarget, "monster_headcrab" ) ) + { + // if squid has been hurt in the last 5 seconds, and is getting relationship for a headcrab, + // tell squid to disregard crab. + return D_NU; + } + + return BaseClass::IRelationType ( pTarget ); +} + +//========================================================= +// TakeDamage - overridden for bullsquid so we can keep track +// of how much time has passed since it was last injured +//========================================================= +int CNPC_Bullsquid::OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ) +{ + +#if 0 //Fix later. + + float flDist; + Vector vecApex, vOffset; + + // if the squid is running, has an enemy, was hurt by the enemy, hasn't been hurt in the last 3 seconds, and isn't too close to the enemy, + // it will swerve. (whew). + if ( GetEnemy() != NULL && IsMoving() && pevAttacker == GetEnemy() && gpGlobals->curtime - m_flLastHurtTime > 3 ) + { + flDist = ( GetAbsOrigin() - GetEnemy()->GetAbsOrigin() ).Length2D(); + + if ( flDist > SQUID_SPRINT_DIST ) + { + AI_Waypoint_t* pRoute = GetNavigator()->GetPath()->Route(); + + if ( pRoute ) + { + flDist = ( GetAbsOrigin() - pRoute[ pRoute->iNodeID ].vecLocation ).Length2D();// reusing flDist. + + if ( GetNavigator()->GetPath()->BuildTriangulationRoute( GetAbsOrigin(), pRoute[ pRoute->iNodeID ].vecLocation, flDist * 0.5, GetEnemy(), &vecApex, &vOffset, NAV_GROUND ) ) + { + GetNavigator()->PrependWaypoint( vecApex, bits_WP_TO_DETOUR | bits_WP_DONT_SIMPLIFY ); + } + } + } + } +#endif + + if ( !FClassnameIs ( inputInfo.GetAttacker(), "monster_headcrab" ) ) + { + // don't forget about headcrabs if it was a headcrab that hurt the squid. + m_flLastHurtTime = gpGlobals->curtime; + } + + return BaseClass::OnTakeDamage_Alive ( inputInfo ); +} + +//========================================================= +// GetSoundInterests - returns a bit mask indicating which types +// of sounds this monster regards. In the base class implementation, +// monsters care about all sounds, but no scents. +//========================================================= +int CNPC_Bullsquid::GetSoundInterests ( void ) +{ + return SOUND_WORLD | + SOUND_COMBAT | + SOUND_CARCASS | + SOUND_MEAT | + SOUND_GARBAGE | + SOUND_PLAYER; +} + +//========================================================= +// OnListened - monsters dig through the active sound list for +// any sounds that may interest them. (smells, too!) +//========================================================= +void CNPC_Bullsquid::OnListened( void ) +{ + AISoundIter_t iter; + + CSound *pCurrentSound; + + static int conditionsToClear[] = + { + COND_SQUID_SMELL_FOOD, + }; + + ClearConditions( conditionsToClear, ARRAYSIZE( conditionsToClear ) ); + + pCurrentSound = GetSenses()->GetFirstHeardSound( &iter ); + + while ( pCurrentSound ) + { + // the npc cares about this sound, and it's close enough to hear. + int condition = COND_NONE; + + if ( !pCurrentSound->FIsSound() ) + { + // if not a sound, must be a smell - determine if it's just a scent, or if it's a food scent + if ( pCurrentSound->IsSoundType( SOUND_MEAT | SOUND_CARCASS ) ) + { + // the detected scent is a food item + condition = COND_SQUID_SMELL_FOOD; + } + } + + if ( condition != COND_NONE ) + SetCondition( condition ); + + pCurrentSound = GetSenses()->GetNextHeardSound( &iter ); + } + + BaseClass::OnListened(); +} + +//======================================================== +// RunAI - overridden for bullsquid because there are things +// that need to be checked every think. +//======================================================== +void CNPC_Bullsquid::RunAI ( void ) +{ + // first, do base class stuff + BaseClass::RunAI(); + + if ( m_nSkin != 0 ) + { + // close eye if it was open. + m_nSkin = 0; + } + + if ( random->RandomInt( 0,39 ) == 0 ) + { + m_nSkin = 1; + } + + if ( GetEnemy() != NULL && GetActivity() == ACT_RUN ) + { + // chasing enemy. Sprint for last bit + if ( (GetAbsOrigin() - GetEnemy()->GetAbsOrigin()).Length2D() < SQUID_SPRINT_DIST ) + { + m_flPlaybackRate = 1.25; + } + } + +} + +//========================================================= +// GetSchedule +//========================================================= +int CNPC_Bullsquid::SelectSchedule( void ) +{ + switch ( m_NPCState ) + { + case NPC_STATE_ALERT: + { + if ( HasCondition( COND_LIGHT_DAMAGE ) || HasCondition( COND_HEAVY_DAMAGE ) ) + { + return SCHED_SQUID_HURTHOP; + } + + if ( HasCondition( COND_SQUID_SMELL_FOOD ) ) + { + CSound *pSound; + + pSound = GetBestScent(); + + if ( pSound && (!FInViewCone ( pSound->GetSoundOrigin() ) || !FVisible ( pSound->GetSoundOrigin() )) ) + { + // scent is behind or occluded + return SCHED_SQUID_SNIFF_AND_EAT; + } + + // food is right out in the open. Just go get it. + return SCHED_SQUID_EAT; + } + + if ( HasCondition( COND_SMELL ) ) + { + // there's something stinky. + CSound *pSound; + + pSound = GetBestScent(); + if ( pSound ) + return SCHED_SQUID_WALLOW; + } + + break; + } + case NPC_STATE_COMBAT: + { +// dead enemy + if ( HasCondition( COND_ENEMY_DEAD ) ) + { + // call base class, all code to handle dead enemies is centralized there. + return BaseClass::SelectSchedule(); + } + + if ( HasCondition( COND_NEW_ENEMY ) ) + { + if ( m_fCanThreatDisplay && IRelationType( GetEnemy() ) == D_HT && FClassnameIs( GetEnemy(), "monster_headcrab" ) ) + { + // this means squid sees a headcrab! + m_fCanThreatDisplay = FALSE;// only do the headcrab dance once per lifetime. + return SCHED_SQUID_SEECRAB; + } + else + { + return SCHED_WAKE_ANGRY; + } + } + + if ( HasCondition( COND_SQUID_SMELL_FOOD ) ) + { + CSound *pSound; + + pSound = GetBestScent(); + + if ( pSound && (!FInViewCone ( pSound->GetSoundOrigin() ) || !FVisible ( pSound->GetSoundOrigin() )) ) + { + // scent is behind or occluded + return SCHED_SQUID_SNIFF_AND_EAT; + } + + // food is right out in the open. Just go get it. + return SCHED_SQUID_EAT; + } + + if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) ) + { + return SCHED_RANGE_ATTACK1; + } + + if ( HasCondition( COND_CAN_MELEE_ATTACK1 ) ) + { + return SCHED_MELEE_ATTACK1; + } + + if ( HasCondition( COND_CAN_MELEE_ATTACK2 ) ) + { + return SCHED_MELEE_ATTACK2; + } + + return SCHED_CHASE_ENEMY; + + break; + } + } + + return BaseClass::SelectSchedule(); +} + +//========================================================= +// FInViewCone - returns true is the passed vector is in +// the caller's forward view cone. The dot product is performed +// in 2d, making the view cone infinitely tall. +//========================================================= +bool CNPC_Bullsquid::FInViewCone ( Vector pOrigin ) +{ + Vector los = ( pOrigin - GetAbsOrigin() ); + + // do this in 2D + los.z = 0; + VectorNormalize( los ); + + Vector facingDir = EyeDirection2D( ); + + float flDot = DotProduct( los, facingDir ); + + if ( flDot > m_flFieldOfView ) + return true; + + return false; +} + +//========================================================= +// FVisible - returns true if a line can be traced from +// the caller's eyes to the target vector +//========================================================= +bool CNPC_Bullsquid::FVisible ( Vector vecOrigin ) +{ + trace_t tr; + Vector vecLookerOrigin; + + vecLookerOrigin = EyePosition();//look through the caller's 'eyes' + UTIL_TraceLine(vecLookerOrigin, vecOrigin, MASK_BLOCKLOS, this/*pentIgnore*/, COLLISION_GROUP_NONE, &tr); + + if ( tr.fraction != 1.0 ) + return false; // Line of sight is not established + else + return true;// line of sight is valid. +} + +//========================================================= +// Start task - selects the correct activity and performs +// any necessary calculations to start the next task on the +// schedule. OVERRIDDEN for bullsquid because it needs to +// know explicitly when the last attempt to chase the enemy +// failed, since that impacts its attack choices. +//========================================================= +void CNPC_Bullsquid::StartTask ( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_MELEE_ATTACK2: + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Bullsquid.Growl" ); + BaseClass::StartTask ( pTask ); + break; + } + case TASK_SQUID_HOPTURN: + { + SetActivity ( ACT_HOP ); + + if ( GetEnemy() ) + { + Vector vecFacing = ( GetEnemy()->GetAbsOrigin() - GetAbsOrigin() ); + VectorNormalize( vecFacing ); + + GetMotor()->SetIdealYaw( vecFacing ); + } + + break; + } + case TASK_SQUID_EAT: + { + m_flHungryTime = gpGlobals->curtime + pTask->flTaskData; + break; + } + + default: + { + BaseClass::StartTask ( pTask ); + break; + } + } +} + +//========================================================= +// RunTask +//========================================================= +void CNPC_Bullsquid::RunTask ( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_SQUID_HOPTURN: + { + if ( GetEnemy() ) + { + Vector vecFacing = ( GetEnemy()->GetAbsOrigin() - GetAbsOrigin() ); + VectorNormalize( vecFacing ); + GetMotor()->SetIdealYaw( vecFacing ); + } + + if ( IsSequenceFinished() ) + { + TaskComplete(); + } + break; + } + default: + { + BaseClass::RunTask( pTask ); + break; + } + } +} + +//========================================================= +// GetIdealState - Overridden for Bullsquid to deal with +// the feature that makes it lose interest in headcrabs for +// a while if something injures it. +//========================================================= +NPC_STATE CNPC_Bullsquid::SelectIdealState ( void ) +{ + // If no schedule conditions, the new ideal state is probably the reason we're in here. + switch ( m_NPCState ) + { + case NPC_STATE_COMBAT: + /* + COMBAT goes to ALERT upon death of enemy + */ + { + if ( GetEnemy() != NULL && ( HasCondition( COND_LIGHT_DAMAGE ) || HasCondition ( COND_HEAVY_DAMAGE ) ) && FClassnameIs( GetEnemy(), "monster_headcrab" ) ) + { + // if the squid has a headcrab enemy and something hurts it, it's going to forget about the crab for a while. + SetEnemy( NULL ); + return NPC_STATE_ALERT; + } + break; + } + } + + return BaseClass::SelectIdealState(); +} + + +//------------------------------------------------------------------------------ +// +// Schedules +// +//------------------------------------------------------------------------------ + +AI_BEGIN_CUSTOM_NPC( monster_bullchicken, CNPC_Bullsquid ) + + DECLARE_TASK ( TASK_SQUID_HOPTURN ) + DECLARE_TASK ( TASK_SQUID_EAT ) + + DECLARE_CONDITION( COND_SQUID_SMELL_FOOD ) + + DECLARE_ACTIVITY( ACT_SQUID_EXCITED ) + DECLARE_ACTIVITY( ACT_SQUID_EAT ) + DECLARE_ACTIVITY( ACT_SQUID_DETECT_SCENT ) + DECLARE_ACTIVITY( ACT_SQUID_INSPECT_FLOOR ) + + //========================================================= + // > SCHED_SQUID_HURTHOP + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SQUID_HURTHOP, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SOUND_WAKE 0" + " TASK_SQUID_HOPTURN 0" + " TASK_FACE_ENEMY 0" + " " + " Interrupts" + ) + + //========================================================= + // > SCHED_SQUID_SEECRAB + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SQUID_SEECRAB, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SOUND_WAKE 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_SQUID_EXCITED" + " TASK_FACE_ENEMY 0" + " " + " Interrupts" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + ) + + //========================================================= + // > SCHED_SQUID_EAT + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SQUID_EAT, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SQUID_EAT 10" + " TASK_STORE_LASTPOSITION 0" + " TASK_GET_PATH_TO_BESTSCENT 0" + " TASK_WALK_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_SQUID_EAT" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_SQUID_EAT" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_SQUID_EAT" + " TASK_SQUID_EAT 50" + " TASK_GET_PATH_TO_LASTPOSITION 0" + " TASK_WALK_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_CLEAR_LASTPOSITION 0" + " " + " Interrupts" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_NEW_ENEMY" + " COND_SMELL" + ) + + //========================================================= + // > SCHED_SQUID_SNIFF_AND_EAT + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SQUID_SNIFF_AND_EAT, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SQUID_EAT 10" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_SQUID_DETECT_SCENT" + " TASK_STORE_LASTPOSITION 0" + " TASK_GET_PATH_TO_BESTSCENT 0" + " TASK_WALK_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_SQUID_EAT" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_SQUID_EAT" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_SQUID_EAT" + " TASK_SQUID_EAT 50" + " TASK_GET_PATH_TO_LASTPOSITION 0" + " TASK_WALK_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_CLEAR_LASTPOSITION 0" + " " + " Interrupts" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_NEW_ENEMY" + " COND_SMELL" + ) + + //========================================================= + // > SCHED_SQUID_WALLOW + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SQUID_WALLOW, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SQUID_EAT 10" + " TASK_STORE_LASTPOSITION 0" + " TASK_GET_PATH_TO_BESTSCENT 0" + " TASK_WALK_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_SQUID_INSPECT_FLOOR" + " TASK_SQUID_EAT 50" + " TASK_GET_PATH_TO_LASTPOSITION 0" + " TASK_WALK_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_CLEAR_LASTPOSITION 0" + " " + " Interrupts" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_NEW_ENEMY" + ) + + //========================================================= + // > SCHED_SQUID_CHASE_ENEMY + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SQUID_CHASE_ENEMY, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_RANGE_ATTACK1" + " TASK_GET_PATH_TO_ENEMY 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " " + " Interrupts" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_SMELL" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_MELEE_ATTACK2" + " COND_TASK_FAILED" + ) + +AI_END_CUSTOM_NPC() diff --git a/game/server/hl1/hl1_npc_bullsquid.h b/game/server/hl1/hl1_npc_bullsquid.h new file mode 100644 index 0000000..1f7a52d --- /dev/null +++ b/game/server/hl1/hl1_npc_bullsquid.h @@ -0,0 +1,68 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef NPC_BULLSQUID_H +#define NPC_BULLSQUID_H + +#include "hl1_ai_basenpc.h" + + +class CNPC_Bullsquid : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_Bullsquid, CHL1BaseNPC ); + +public: + void Spawn( void ); + void Precache( void ); + Class_T Classify( void ); + + void IdleSound( void ); + void PainSound( const CTakeDamageInfo &info ); + void AlertSound( void ); + void DeathSound( const CTakeDamageInfo &info ); + void AttackSound( void ); + + float MaxYawSpeed( void ); + + void HandleAnimEvent( animevent_t *pEvent ); + + int RangeAttack1Conditions( float flDot, float flDist ); + int MeleeAttack1Conditions( float flDot, float flDist ); + int MeleeAttack2Conditions( float flDot, float flDist ); + + bool FValidateHintType ( CAI_Hint *pHint ); + void RemoveIgnoredConditions( void ); + Disposition_t IRelationType( CBaseEntity *pTarget ); + int OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ); + + int GetSoundInterests ( void ); + void RunAI ( void ); + virtual void OnListened ( void ); + + int SelectSchedule( void ); + int TranslateSchedule( int scheduleType ); + + bool FInViewCone ( Vector pOrigin ); + bool FVisible ( Vector vecOrigin ); + + void StartTask ( const Task_t *pTask ); + void RunTask ( const Task_t *pTask ); + + NPC_STATE SelectIdealState ( void ); + + DEFINE_CUSTOM_AI; + DECLARE_DATADESC() + +private: + + bool m_fCanThreatDisplay;// this is so the squid only does the "I see a headcrab!" dance one time. + float m_flLastHurtTime;// we keep track of this, because if something hurts a squid, it will forget about its love of headcrabs for a while. + float m_flNextSpitTime;// last time the bullsquid used the spit attack. + float m_flHungryTime;// set this is a future time to stop the monster from eating for a while. + +}; +#endif // NPC_BULLSQUID_H
\ No newline at end of file diff --git a/game/server/hl1/hl1_npc_controller.cpp b/game/server/hl1/hl1_npc_controller.cpp new file mode 100644 index 0000000..bc27aa0 --- /dev/null +++ b/game/server/hl1/hl1_npc_controller.cpp @@ -0,0 +1,1313 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Bullseyes act as targets for other NPC's to attack and to trigger +// events +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +//#include "hl1_npc_controller.h" +#include "ai_basenpc_flyer.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "basecombatweapon.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "Sprite.h" +#include "ai_moveprobe.h" + + +//========================================================= +// Monster's Anim Events Go Here +//========================================================= +#define CONTROLLER_AE_HEAD_OPEN 1 +#define CONTROLLER_AE_BALL_SHOOT 2 +#define CONTROLLER_AE_SMALL_SHOOT 3 +#define CONTROLLER_AE_POWERUP_FULL 4 +#define CONTROLLER_AE_POWERUP_HALF 5 + +#define CONTROLLER_FLINCH_DELAY 2 // at most one flinch every n secs + +#define DIST_TO_CHECK 200 + +ConVar sk_controller_health ( "sk_controller_health", "60" ); +ConVar sk_controller_dmgzap ( "sk_controller_dmgzap", "15" ); +ConVar sk_controller_speedball ( "sk_controller_speedball", "650" ); +ConVar sk_controller_dmgball ( "sk_controller_dmgball", "3" ); + +int ACT_CONTROLLER_UP; +int ACT_CONTROLLER_DOWN; +int ACT_CONTROLLER_LEFT; +int ACT_CONTROLLER_RIGHT; +int ACT_CONTROLLER_FORWARD; +int ACT_CONTROLLER_BACKWARD; + +class CSprite; +class CNPC_Controller; + +enum +{ + TASK_CONTROLLER_CHASE_ENEMY = LAST_SHARED_TASK, + TASK_CONTROLLER_STRAFE, + TASK_CONTROLLER_TAKECOVER, + TASK_CONTROLLER_FAIL, +}; + +enum +{ + SCHED_CONTROLLER_CHASE_ENEMY = LAST_SHARED_SCHEDULE, + SCHED_CONTROLLER_STRAFE, + SCHED_CONTROLLER_TAKECOVER, + SCHED_CONTROLLER_FAIL, +}; + +class CControllerNavigator : public CAI_ComponentWithOuter<CNPC_Controller, CAI_Navigator> +{ + typedef CAI_ComponentWithOuter<CNPC_Controller, CAI_Navigator> BaseClass; +public: + CControllerNavigator( CNPC_Controller *pOuter ) + : BaseClass( pOuter ) + { + } + + bool ActivityIsLocomotive( Activity activity ) { return true; } +}; + +class CNPC_Controller : public CAI_BaseFlyingBot +{ +public: + + DECLARE_CLASS( CNPC_Controller, CAI_BaseFlyingBot ); + DEFINE_CUSTOM_AI; + DECLARE_DATADESC(); + + void Spawn( void ); + void Precache( void ); + + float MaxYawSpeed( void ) { return 120.0f; } + Class_T Classify ( void ) { return CLASS_ALIEN_MILITARY; } + + void HandleAnimEvent( animevent_t *pEvent ); + + void RunAI( void ); + + int RangeAttack1Conditions ( float flDot, float flDist ); // balls + int RangeAttack2Conditions ( float flDot, float flDist ); // head + int MeleeAttack1Conditions ( float flDot, float flDist ) { return COND_NONE; } + int MeleeAttack2Conditions ( float flDot, float flDist ) { return COND_NONE; } + + int TranslateSchedule( int scheduleType ); + void StartTask ( const Task_t *pTask ); + void RunTask ( const Task_t *pTask ); + + void Stop( void ); + bool OverridePathMove( float flInterval ); + bool OverrideMove( float flInterval ); + + void MoveToTarget( float flInterval, const Vector &vecMoveTarget ); + + Activity NPC_TranslateActivity( Activity eNewActivity ); + void SetActivity ( Activity NewActivity ); + bool ShouldAdvanceRoute( float flWaypointDist ); + int LookupFloat( ); + + friend class CControllerNavigator; + CAI_Navigator *CreateNavigator() + { + return new CControllerNavigator( this ); + } + + bool ShouldGib( const CTakeDamageInfo &info ); + bool HasAlienGibs( void ) { return true; } + bool HasHumanGibs( void ) { return false; } + + float m_flShootTime; + float m_flShootEnd; + + void PainSound( const CTakeDamageInfo &info ); + void AlertSound( void ); + void IdleSound( void ); + void AttackSound( void ); + void DeathSound( const CTakeDamageInfo &info ); + + int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + void Event_Killed( const CTakeDamageInfo &info ); + + CSprite *m_pBall[2]; // hand balls + int m_iBall[2]; // how bright it should be + float m_iBallTime[2]; // when it should be that color + int m_iBallCurrent[2]; // current brightness + + Vector m_vecEstVelocity; + + Vector m_velocity; + bool m_fInCombat; + + void SetSequence( int nSequence ); + + int IRelationPriority( CBaseEntity *pTarget ); +}; + +class CNPC_ControllerHeadBall : public CAI_BaseNPC +{ +public: + DECLARE_CLASS( CNPC_ControllerHeadBall, CAI_BaseNPC ); + + DECLARE_DATADESC(); + + void Spawn( void ); + void Precache( void ); + + void EXPORT HuntThink( void ); + void EXPORT KillThink( void ); + void EXPORT BounceTouch( CBaseEntity *pOther ); + void MovetoTarget( Vector vecTarget ); + + float m_flSpawnTime; + Vector m_vecIdeal; + EHANDLE m_hOwner; + + CSprite *m_pSprite; +}; + +class CNPC_ControllerZapBall : public CAI_BaseNPC +{ +public: + DECLARE_CLASS( CNPC_ControllerHeadBall, CAI_BaseNPC ); + + DECLARE_DATADESC(); + + void Spawn( void ); + void Precache( void ); + + void EXPORT AnimateThink( void ); + void EXPORT ExplodeTouch( CBaseEntity *pOther ); + + void Kill( void ); + + EHANDLE m_hOwner; + float m_flSpawnTime; + + CSprite *m_pSprite; +}; + +LINK_ENTITY_TO_CLASS( monster_alien_controller, CNPC_Controller ); + +BEGIN_DATADESC( CNPC_Controller ) + + DEFINE_ARRAY( m_pBall, FIELD_CLASSPTR, 2 ), + DEFINE_ARRAY( m_iBall, FIELD_INTEGER, 2 ), + DEFINE_ARRAY( m_iBallTime, FIELD_TIME, 2 ), + DEFINE_ARRAY( m_iBallCurrent, FIELD_INTEGER, 2 ), + DEFINE_FIELD( m_vecEstVelocity, FIELD_VECTOR ), + DEFINE_FIELD( m_velocity, FIELD_VECTOR ), + DEFINE_FIELD( m_fInCombat, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_flShootTime, FIELD_TIME ), + DEFINE_FIELD( m_flShootEnd, FIELD_TIME ), + +END_DATADESC() + + +void CNPC_Controller::Spawn() +{ + Precache( ); + + SetModel( "models/controller.mdl" ); + UTIL_SetSize( this, Vector( -32, -32, 0 ), Vector( 32, 32, 64 )); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + + SetMoveType( MOVETYPE_STEP ); + SetGravity(0.001); + + + m_bloodColor = BLOOD_COLOR_GREEN; + m_iHealth = sk_controller_health.GetFloat(); + + m_flFieldOfView = VIEW_FIELD_FULL;// indicates the width of this monster's forward view cone ( as a dotproduct result ) + m_NPCState = NPC_STATE_NONE; + + SetRenderColor( 255, 255, 255, 255 ); + + CapabilitiesClear(); + + AddFlag( FL_FLY ); + SetNavType( NAV_FLY ); + + CapabilitiesAdd( bits_CAP_MOVE_FLY | bits_CAP_INNATE_RANGE_ATTACK1 | bits_CAP_INNATE_RANGE_ATTACK2 | bits_CAP_MOVE_SHOOT); + + NPCInit(); + + + SetDefaultEyeOffset(); +} + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CNPC_Controller::Precache() +{ + PrecacheModel("models/controller.mdl"); + + PrecacheModel( "sprites/xspark4.vmt"); + + UTIL_PrecacheOther( "controller_energy_ball" ); + UTIL_PrecacheOther( "controller_head_ball" ); + + PrecacheScriptSound( "Controller.Pain" ); + PrecacheScriptSound( "Controller.Alert" ); + PrecacheScriptSound( "Controller.Die" ); + PrecacheScriptSound( "Controller.Idle" ); + PrecacheScriptSound( "Controller.Attack" ); + +} + +//========================================================= +// TakeDamage - +//========================================================= +int CNPC_Controller::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + PainSound( info ); + return BaseClass::OnTakeDamage_Alive( info ); +} + +bool CNPC_Controller::ShouldGib( const CTakeDamageInfo &info ) +{ + if ( info.GetDamageType() & DMG_NEVERGIB ) + return false; + + if ( ( g_pGameRules->Damage_ShouldGibCorpse( info.GetDamageType() ) && m_iHealth < GIB_HEALTH_VALUE ) || ( info.GetDamageType() & DMG_ALWAYSGIB ) ) + return true; + + return false; + +} + +int CNPC_Controller::IRelationPriority( CBaseEntity *pTarget ) +{ + if ( pTarget->Classify() == CLASS_PLAYER ) + { + return BaseClass::IRelationPriority ( pTarget ) + 1; + } + + return BaseClass::IRelationPriority( pTarget ); +} + +void CNPC_Controller::Event_Killed( const CTakeDamageInfo &info ) +{ + if( ShouldGib(info) ) + { + //remove the balls + if (m_pBall[0]) + { + UTIL_Remove( m_pBall[0] ); + m_pBall[0] = NULL; + } + if (m_pBall[1]) + { + UTIL_Remove( m_pBall[1] ); + m_pBall[1] = NULL; + } + } + else + { + // fade balls + if (m_pBall[0]) + { + m_pBall[0]->FadeAndDie( 2 ); + m_pBall[0] = NULL; + } + if (m_pBall[1]) + { + m_pBall[1]->FadeAndDie( 2 ); + m_pBall[1] = NULL; + } + } + + BaseClass::Event_Killed( info ); +} + +void CNPC_Controller::PainSound( const CTakeDamageInfo &info ) +{ + if (random->RandomInt(0,5) < 2) + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Controller.Pain" ); + } +} + +void CNPC_Controller::AlertSound( void ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Controller.Alert" ); +} + +void CNPC_Controller::IdleSound( void ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Controller.Idle" ); +} + +void CNPC_Controller::AttackSound( void ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Controller.Attack" ); +} + +void CNPC_Controller::DeathSound( const CTakeDamageInfo &info ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Controller.Die" ); +} + +//========================================================= +// HandleAnimEvent - catches the monster-specific messages +// that occur when tagged animation frames are played. +//========================================================= +void CNPC_Controller::HandleAnimEvent( animevent_t *pEvent ) +{ + switch( pEvent->event ) + { + case CONTROLLER_AE_HEAD_OPEN: + { + Vector vecStart; + QAngle angleGun; + + GetAttachment( 0, vecStart, angleGun ); + + // BUGBUG - attach to attachment point! + + CBroadcastRecipientFilter filter; + te->DynamicLight( filter, 0.0, &vecStart, 255, 192, 64, 0, 1 /*radius*/, 0.2, -32 ); + + m_iBall[0] = 192; + m_iBallTime[0] = gpGlobals->curtime + atoi( pEvent->options ) / 15.0; + m_iBall[1] = 255; + m_iBallTime[1] = gpGlobals->curtime + atoi( pEvent->options ) / 15.0; + + } + break; + + case CONTROLLER_AE_BALL_SHOOT: + { + Vector vecStart; + QAngle angleGun; + + GetAttachment( 1, vecStart, angleGun ); + + CBroadcastRecipientFilter filter; + te->DynamicLight( filter, 0.0, &vecStart, 255, 192, 64, 0, 1 /*radius*/, 0.1, 32 ); + + CAI_BaseNPC *pBall = (CAI_BaseNPC*)Create( "controller_head_ball", vecStart, angleGun ); + + pBall->SetAbsVelocity( Vector(0,0,32) ); + pBall->SetEnemy( GetEnemy() ); + +// DevMsg( 1, "controller shooting head ball\n" ); + + m_iBall[0] = 0; + m_iBall[1] = 0; + } + break; + + case CONTROLLER_AE_SMALL_SHOOT: + { + AttackSound( ); + m_flShootTime = gpGlobals->curtime; + m_flShootEnd = m_flShootTime + atoi( pEvent->options ) / 15.0; + } + break; + case CONTROLLER_AE_POWERUP_FULL: + { + m_iBall[0] = 255; + m_iBallTime[0] = gpGlobals->curtime + atoi( pEvent->options ) / 15.0; + m_iBall[1] = 255; + m_iBallTime[1] = gpGlobals->curtime + atoi( pEvent->options ) / 15.0; + } + break; + case CONTROLLER_AE_POWERUP_HALF: + { + m_iBall[0] = 192; + m_iBallTime[0] = gpGlobals->curtime + atoi( pEvent->options ) / 15.0; + m_iBall[1] = 192; + m_iBallTime[1] = gpGlobals->curtime + atoi( pEvent->options ) / 15.0; + } + break; + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } +} + + +//========================================================= +// AI Schedules Specific to this monster +//========================================================= + +AI_BEGIN_CUSTOM_NPC( monster_alien_controller, CNPC_Controller ) + + //declare our tasks + DECLARE_TASK( TASK_CONTROLLER_CHASE_ENEMY ) + DECLARE_TASK( TASK_CONTROLLER_STRAFE ) + DECLARE_TASK( TASK_CONTROLLER_TAKECOVER ) + DECLARE_TASK( TASK_CONTROLLER_FAIL ) + + DECLARE_ACTIVITY( ACT_CONTROLLER_UP ) + DECLARE_ACTIVITY( ACT_CONTROLLER_DOWN ) + DECLARE_ACTIVITY( ACT_CONTROLLER_LEFT ) + DECLARE_ACTIVITY( ACT_CONTROLLER_RIGHT ) + DECLARE_ACTIVITY( ACT_CONTROLLER_FORWARD ) + DECLARE_ACTIVITY( ACT_CONTROLLER_BACKWARD ) + + //========================================================= + // > SCHED_CONTROLLER_CHASE_ENEMY + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_CONTROLLER_CHASE_ENEMY, + + " Tasks" + " TASK_GET_PATH_TO_ENEMY 128" + " TASK_WAIT_FOR_MOVEMENT 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_TASK_FAILED" + + + ) + + //========================================================= + // > SCHED_CONTROLLER_STRAFE + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_CONTROLLER_STRAFE, + + " Tasks" + " TASK_WAIT 0.2" + " TASK_GET_PATH_TO_ENEMY 128" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_WAIT 1" + " " + " Interrupts" + " COND_NEW_ENEMY" + ) + + //========================================================= + // > SCHED_CONTROLLER_TAKECOVER + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_CONTROLLER_TAKECOVER, + + " Tasks" + " TASK_WAIT 0.2" + " TASK_FIND_COVER_FROM_ENEMY 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_WAIT 1" + " " + " Interrupts" + " COND_NEW_ENEMY" + ) + + //========================================================= + // > SCHED_CONTROLLER_FAIL + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_CONTROLLER_FAIL, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT 2" + " TASK_WAIT_PVS 0" + ) + +AI_END_CUSTOM_NPC() + +//========================================================= +// StartTask +//========================================================= +void CNPC_Controller::StartTask( const Task_t *pTask ) +{ + BaseClass::StartTask( pTask ); +} + +Vector Intersect( Vector vecSrc, Vector vecDst, Vector vecMove, float flSpeed ) +{ + Vector vecTo = vecDst - vecSrc; + + float a = DotProduct( vecMove, vecMove ) - flSpeed * flSpeed; + float b = 0 * DotProduct(vecTo, vecMove); // why does this work? + float c = DotProduct( vecTo, vecTo ); + + float t; + if (a == 0) + { + t = c / (flSpeed * flSpeed); + } + else + { + t = b * b - 4 * a * c; + t = sqrt( t ) / (2.0 * a); + float t1 = -b +t; + float t2 = -b -t; + + if (t1 < 0 || t2 < t1) + t = t2; + else + t = t1; + } + + if (t < 0.1) + t = 0.1; + if (t > 10.0) + t = 10.0; + + Vector vecHit = vecTo + vecMove * t; + VectorNormalize( vecHit ); + return vecHit * flSpeed; +} + + +int CNPC_Controller::LookupFloat( ) +{ + if (m_velocity.Length( ) < 32.0) + { + return ACT_CONTROLLER_UP; + } + + Vector vecForward, vecRight, vecUp; + AngleVectors( GetAbsAngles(), &vecForward, &vecRight, &vecUp ); + + float x = DotProduct( vecForward, m_velocity ); + float y = DotProduct( vecRight, m_velocity ); + float z = DotProduct( vecUp, m_velocity ); + + if (fabs(x) > fabs(y) && fabs(x) > fabs(z)) + { + if (x > 0) + return ACT_CONTROLLER_FORWARD; + else + return ACT_CONTROLLER_BACKWARD; + } + else if (fabs(y) > fabs(z)) + { + if (y > 0) + return ACT_CONTROLLER_RIGHT; + else + return ACT_CONTROLLER_LEFT; + } + else + { + if (z > 0) + return ACT_CONTROLLER_UP; + else + return ACT_CONTROLLER_DOWN; + } +} + + +//========================================================= +// RunTask +//========================================================= +void CNPC_Controller::RunTask ( const Task_t *pTask ) +{ + if (m_flShootEnd > gpGlobals->curtime) + { + Vector vecHand; + QAngle vecAngle; + + GetAttachment( 2, vecHand, vecAngle ); + + while (m_flShootTime < m_flShootEnd && m_flShootTime < gpGlobals->curtime) + { + Vector vecSrc = vecHand + GetAbsVelocity() * (m_flShootTime - gpGlobals->curtime); + Vector vecDir; + + if (GetEnemy() != NULL) + { + if (HasCondition( COND_SEE_ENEMY )) + { + m_vecEstVelocity = m_vecEstVelocity * 0.5 + GetEnemy()->GetAbsVelocity() * 0.5; + } + else + { + m_vecEstVelocity = m_vecEstVelocity * 0.8; + } + vecDir = Intersect( vecSrc, GetEnemy()->BodyTarget( GetAbsOrigin() ), m_vecEstVelocity, sk_controller_speedball.GetFloat() ); + + float delta = 0.03490; // +-2 degree + vecDir = vecDir + Vector( random->RandomFloat( -delta, delta ), random->RandomFloat( -delta, delta ), random->RandomFloat( -delta, delta ) ) * sk_controller_speedball.GetFloat(); + + vecSrc = vecSrc + vecDir * (gpGlobals->curtime - m_flShootTime); + CAI_BaseNPC *pBall = (CAI_BaseNPC*)Create( "controller_energy_ball", vecSrc, GetAbsAngles(), this ); + pBall->SetAbsVelocity( vecDir ); + +// DevMsg( 2, "controller shooting energy ball\n" ); + } + + m_flShootTime += 0.2; + } + + if (m_flShootTime > m_flShootEnd) + { + m_iBall[0] = 64; + m_iBallTime[0] = m_flShootEnd; + m_iBall[1] = 64; + m_iBallTime[1] = m_flShootEnd; + m_fInCombat = FALSE; + } + } + + switch ( pTask->iTask ) + { + case TASK_WAIT_FOR_MOVEMENT: + case TASK_WAIT: + case TASK_WAIT_FACE_ENEMY: + case TASK_WAIT_PVS: + { + if( GetEnemy() ) + { + float idealYaw = UTIL_VecToYaw( GetEnemy()->GetAbsOrigin() - GetAbsOrigin() ); + GetMotor()->SetIdealYawAndUpdate( idealYaw ); + } + + if ( IsSequenceFinished() || GetActivity() == ACT_IDLE) + { + m_fInCombat = false; + } + + BaseClass::RunTask ( pTask ); + + if (!m_fInCombat) + { + if( HasCondition( COND_CAN_RANGE_ATTACK1 )) + { + SetActivity( ACT_RANGE_ATTACK1 ); + SetCycle( 0 ); + ResetSequenceInfo( ); + m_fInCombat = true; + } + else if( HasCondition( COND_CAN_RANGE_ATTACK2 ) ) + { + SetActivity( ACT_RANGE_ATTACK2 ); + SetCycle( 0 ); + ResetSequenceInfo( ); + m_fInCombat = true; + } + else + { + int iFloatActivity = LookupFloat(); + if( IsSequenceFinished() || iFloatActivity != GetActivity() ) + { + SetActivity( (Activity)iFloatActivity ); + } + } + } + } + break; + default: + BaseClass::RunTask ( pTask ); + break; + } +} + +void CNPC_Controller::SetSequence( int nSequence ) +{ + BaseClass::SetSequence( nSequence ); +} + + +//========================================================= +//========================================================= +int CNPC_Controller::TranslateSchedule( int scheduleType ) +{ + switch ( scheduleType ) + { + case SCHED_CHASE_ENEMY: + return SCHED_CONTROLLER_CHASE_ENEMY; + case SCHED_RANGE_ATTACK1: + return SCHED_CONTROLLER_STRAFE; + case SCHED_RANGE_ATTACK2: + case SCHED_MELEE_ATTACK1: + case SCHED_MELEE_ATTACK2: + case SCHED_TAKE_COVER_FROM_ENEMY: + return SCHED_CONTROLLER_TAKECOVER; + case SCHED_FAIL: + return SCHED_CONTROLLER_FAIL; + + default: + break; + } + + return BaseClass::TranslateSchedule( scheduleType ); +} + + +//========================================================= +// CheckRangeAttack1 - shoot a bigass energy ball out of their head +//========================================================= +int CNPC_Controller::RangeAttack1Conditions ( float flDot, float flDist ) +{ + if( flDist > 2048 ) + { + return COND_TOO_FAR_TO_ATTACK; + } + + if( flDist <= 256 ) + { + return COND_TOO_CLOSE_TO_ATTACK; + } + +// if( flDot <= 0.5 ) +// { +// return COND_NOT_FACING_ATTACK; +// } + + return COND_CAN_RANGE_ATTACK1; +} + +//========================================================= +// CheckRangeAttack1 - head +//========================================================= +int CNPC_Controller::RangeAttack2Conditions ( float flDot, float flDist ) +{ + if( flDist > 2048 ) + { + return COND_TOO_FAR_TO_ATTACK; + } + + if( flDist <= 64 ) + { + return COND_TOO_CLOSE_TO_ATTACK; + } + +// if( flDot <= 0.5 ) +// { +// return COND_NOT_FACING_ATTACK; +// } + + return COND_CAN_RANGE_ATTACK2; +} + +//========================================================= +//========================================================= +Activity CNPC_Controller::NPC_TranslateActivity( Activity eNewActivity ) +{ + switch ( eNewActivity) + { + case ACT_IDLE: + return (Activity)LookupFloat(); + break; + + default: + return BaseClass::NPC_TranslateActivity( eNewActivity ); + } +} + +//========================================================= +// SetActivity - +//========================================================= +void CNPC_Controller::SetActivity ( Activity NewActivity ) +{ + BaseClass::SetActivity( NewActivity ); + m_flGroundSpeed = 100; +} + +//========================================================= +// RunAI +//========================================================= +void CNPC_Controller::RunAI( void ) +{ + BaseClass::RunAI(); + + Vector vecStart; + QAngle angleGun; + + //some kind of hack in hl1 ? +// if ( HasMemory( bits_MEMORY_KILLED ) ) + //use this instead + if( !IsAlive() ) + return; + + for (int i = 0; i < 2; i++) + { + if (m_pBall[i] == NULL) + { + m_pBall[i] = CSprite::SpriteCreate( "sprites/xspark4.vmt", GetAbsOrigin(), TRUE ); + m_pBall[i]->SetTransparency( kRenderGlow, 255, 255, 255, 255, kRenderFxNoDissipation ); + m_pBall[i]->SetAttachment( this, (i + 3) ); + m_pBall[i]->SetScale( 1.0 ); + } + + float t = m_iBallTime[i] - gpGlobals->curtime; + if (t > 0.1) + t = 0.1 / t; + else + t = 1.0; + + m_iBallCurrent[i] += (m_iBall[i] - m_iBallCurrent[i]) * t; + + m_pBall[i]->SetBrightness( m_iBallCurrent[i] ); + + GetAttachment( i + 2, vecStart, angleGun ); + m_pBall[i]->SetAbsOrigin( vecStart ); + + CBroadcastRecipientFilter filter; + GetAttachment( i + 3, vecStart, angleGun ); + te->DynamicLight( filter, 0.0, &vecStart, 255, 192, 64, 0/*exponent*/, m_iBallCurrent[i] / 8 /*radius*/, 0.5, 0 ); + } +} + +//========================================================= +// Stop - +//========================================================= +void CNPC_Controller::Stop( void ) +{ + SetIdealActivity( GetStoppedActivity() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Handles movement towards the last move target. +// Input : flInterval - +//----------------------------------------------------------------------------- +bool CNPC_Controller::OverridePathMove( float flInterval ) +{ + CBaseEntity *pMoveTarget = (GetTarget()) ? GetTarget() : GetEnemy(); + Vector waypointDir = GetNavigator()->GetCurWaypointPos() - GetLocalOrigin(); + + float flWaypointDist = waypointDir.Length2D(); + VectorNormalize(waypointDir); + + // cut corner? + if (flWaypointDist < 128) + { + if (m_flGroundSpeed > 100) + m_flGroundSpeed -= 40; + } + else + { + if (m_flGroundSpeed < 400) + m_flGroundSpeed += 10; + } + + m_velocity = m_velocity * 0.8 + m_flGroundSpeed * waypointDir * 0.5; + SetAbsVelocity( m_velocity ); + + // ----------------------------------------------------------------- + // Check route is blocked + // ------------------------------------------------------------------ + Vector checkPos = GetLocalOrigin() + (waypointDir * (m_flGroundSpeed * flInterval)); + + AIMoveTrace_t moveTrace; + GetMoveProbe()->MoveLimit( NAV_FLY, GetLocalOrigin(), checkPos, MASK_NPCSOLID|CONTENTS_WATER, + pMoveTarget, &moveTrace); + if (IsMoveBlocked( moveTrace )) + { + TaskFail(FAIL_NO_ROUTE); + GetNavigator()->ClearGoal(); + return true; + } + + // ---------------------------------------------- + + Vector lastPatrolDir = GetNavigator()->GetCurWaypointPos() - GetLocalOrigin(); + + if ( ProgressFlyPath( flInterval, pMoveTarget, MASK_NPCSOLID, false, 64 ) == AINPP_COMPLETE ) + { + { + m_vLastPatrolDir = lastPatrolDir; + VectorNormalize(m_vLastPatrolDir); + } + return true; + } + return false; +} + +bool CNPC_Controller::OverrideMove( float flInterval ) +{ + if (m_flGroundSpeed == 0) + { + m_flGroundSpeed = 100; + } + + // ---------------------------------------------- + // Select move target + // ---------------------------------------------- + CBaseEntity *pMoveTarget = NULL; + if (GetTarget() != NULL ) + { + pMoveTarget = GetTarget(); + } + else if (GetEnemy() != NULL ) + { + pMoveTarget = GetEnemy(); + } + + // ---------------------------------------------- + // Select move target position + // ---------------------------------------------- + Vector vMoveTargetPos(0,0,0); + if (GetTarget()) + { + vMoveTargetPos = GetTarget()->GetAbsOrigin(); + } + else if (GetEnemy() != NULL) + { + vMoveTargetPos = GetEnemy()->GetAbsOrigin(); + } + + // ----------------------------------------- + // See if we can fly there directly + // ----------------------------------------- + if (pMoveTarget /*|| HaveInspectTarget()*/) + { + trace_t tr; + + if (pMoveTarget) + { + UTIL_TraceEntity( this, GetAbsOrigin(), vMoveTargetPos, + MASK_NPCSOLID_BRUSHONLY, pMoveTarget, GetCollisionGroup(), &tr); + } + else + { + UTIL_TraceEntity( this, GetAbsOrigin(), vMoveTargetPos, MASK_NPCSOLID_BRUSHONLY, &tr); + } +/* + float fTargetDist = (1-tr.fraction)*(GetAbsOrigin() - vMoveTargetPos).Length(); + if (fTargetDist > 50) + { + //SetCondition( COND_SCANNER_FLY_BLOCKED ); + } + else + { + //SetCondition( COND_SCANNER_FLY_CLEAR ); + } +*/ + } + + // ----------------------------------------------------------------- + // If I have a route, keep it updated and move toward target + // ------------------------------------------------------------------ + if (GetNavigator()->IsGoalActive()) + { + if ( OverridePathMove( flInterval ) ) + return true; + } + else + { + //do nothing + Stop(); + TaskComplete(); + } + + return true; +} + +void CNPC_Controller::MoveToTarget( float flInterval, const Vector &vecMoveTarget ) +{ + const float myAccel = 300.0; + const float myDecay = 9.0; + + //TurnHeadToTarget( flInterval, MoveTarget ); + MoveToLocation( flInterval, vecMoveTarget, myAccel, (2 * myAccel), myDecay ); +} + +//========================================================= +// Controller bouncy ball attack +//========================================================= + +LINK_ENTITY_TO_CLASS( controller_head_ball, CNPC_ControllerHeadBall ); + +BEGIN_DATADESC( CNPC_ControllerHeadBall ) + + DEFINE_THINKFUNC( HuntThink ), + DEFINE_THINKFUNC( KillThink ), + DEFINE_ENTITYFUNC( BounceTouch ), + + DEFINE_FIELD( m_pSprite, FIELD_CLASSPTR ), + + DEFINE_FIELD( m_flSpawnTime, FIELD_TIME ), + DEFINE_FIELD( m_vecIdeal, FIELD_VECTOR ), + DEFINE_FIELD( m_hOwner, FIELD_EHANDLE ), + +END_DATADESC() + + +void CNPC_ControllerHeadBall::Spawn( void ) +{ + Precache( ); + // motor + SetMoveType( MOVETYPE_FLY ); + SetSolid( SOLID_BBOX ); + SetSize( vec3_origin, vec3_origin ); + + m_pSprite = CSprite::SpriteCreate( "sprites/xspark4.vmt", GetAbsOrigin(), FALSE ); + m_pSprite->SetTransparency( kRenderTransAdd, 255, 255, 255, 255, kRenderFxNoDissipation ); + m_pSprite->SetAttachment( this, 0 ); + m_pSprite->SetScale( 2.0 ); + + UTIL_SetSize( this, Vector( 0, 0, 0), Vector(0, 0, 0) ); + UTIL_SetOrigin( this, GetAbsOrigin() ); + + SetThink( &CNPC_ControllerHeadBall::HuntThink ); + SetTouch( &CNPC_ControllerHeadBall::BounceTouch ); + +// m_vecIdeal = vec3_origin; //(0,0,0) + + SetNextThink( gpGlobals->curtime + 0.1 ); + + m_hOwner = GetOwnerEntity(); + + m_flSpawnTime = gpGlobals->curtime; +} + + +void CNPC_ControllerHeadBall::Precache( void ) +{ + PrecacheModel( "sprites/xspark4.vmt"); +} + +extern short g_sModelIndexLaser; + +void CNPC_ControllerHeadBall::HuntThink( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1 ); + + if( !m_pSprite ) + { + Assert(0); + return; + } + + m_pSprite->SetBrightness( m_pSprite->GetBrightness() - 5, 0.1f ); + + CBroadcastRecipientFilter filter; + te->DynamicLight( filter, 0.0, &GetAbsOrigin(), 255, 255, 255, 0, m_pSprite->GetBrightness() / 16, 0.2, 0 ); + + // check world boundaries + if (gpGlobals->curtime - m_flSpawnTime > 5 || m_pSprite->GetBrightness() < 64 /*|| GetEnemy() == NULL || m_hOwner == NULL*/ || !IsInWorld() ) + { + SetTouch( NULL ); + SetThink( &CNPC_ControllerHeadBall::KillThink ); + SetNextThink( gpGlobals->curtime ); + return; + } + + if( !GetEnemy() ) + return; + + MovetoTarget( GetEnemy()->GetAbsOrigin() ); + + if ((GetEnemy()->WorldSpaceCenter() - GetAbsOrigin()).Length() < 64) + { + trace_t tr; + + UTIL_TraceLine( GetAbsOrigin(), GetEnemy()->WorldSpaceCenter(), MASK_ALL, this, COLLISION_GROUP_NONE, &tr ); + + CBaseEntity *pEntity = tr.m_pEnt; + if (pEntity != NULL && pEntity->m_takedamage == DAMAGE_YES) + { + ClearMultiDamage( ); + Vector dir = GetAbsVelocity(); + VectorNormalize( dir ); + CTakeDamageInfo info( this, this, sk_controller_dmgball.GetFloat(), DMG_SHOCK ); + CalculateMeleeDamageForce( &info, dir, tr.endpos ); + pEntity->DispatchTraceAttack( info, dir, &tr ); + ApplyMultiDamage(); + + int haloindex = 0; + int fadelength = 0; + int amplitude = 0; + const Vector vecEnd = tr.endpos; + te->BeamEntPoint( filter, 0.0, entindex(), NULL, 0, &(tr.m_pEnt->GetAbsOrigin()), + g_sModelIndexLaser, haloindex /* no halo */, 0, 10, 3, 20, 20, fadelength, + amplitude, 255, 255, 255, 255, 10 ); + + } + + UTIL_EmitAmbientSound( GetSoundSourceIndex(), GetAbsOrigin(), "Controller.ElectroSound", 0.5, SNDLVL_NORM, 0, 100 ); + + SetNextAttack( gpGlobals->curtime + 3.0 ); + + SetThink( &CNPC_ControllerHeadBall::KillThink ); + SetNextThink( gpGlobals->curtime + 0.3 ); + } +} + +void CNPC_ControllerHeadBall::MovetoTarget( Vector vecTarget ) +{ + // accelerate + float flSpeed = m_vecIdeal.Length(); + if (flSpeed == 0) + { + m_vecIdeal = GetAbsVelocity(); + flSpeed = m_vecIdeal.Length(); + } + + if (flSpeed > 400) + { + VectorNormalize( m_vecIdeal ); + m_vecIdeal = m_vecIdeal * 400; + } + + Vector t = vecTarget - GetAbsOrigin(); + VectorNormalize(t); + m_vecIdeal = m_vecIdeal + t * 100; + SetAbsVelocity(m_vecIdeal); +} + +void CNPC_ControllerHeadBall::BounceTouch( CBaseEntity *pOther ) +{ + Vector vecDir = m_vecIdeal; + VectorNormalize( vecDir ); + + trace_t tr; + tr = CBaseEntity::GetTouchTrace( ); + + float n = -DotProduct(tr.plane.normal, vecDir); + + vecDir = 2.0 * tr.plane.normal * n + vecDir; + + m_vecIdeal = vecDir * m_vecIdeal.Length(); +} + +void CNPC_ControllerHeadBall::KillThink( void ) +{ + UTIL_Remove( m_pSprite ); + UTIL_Remove( this ); +} + + +//========================================================= +// Controller Zap attack +//========================================================= + +LINK_ENTITY_TO_CLASS( controller_energy_ball, CNPC_ControllerZapBall ); + +BEGIN_DATADESC( CNPC_ControllerZapBall ) + + DEFINE_THINKFUNC( AnimateThink ), + DEFINE_ENTITYFUNC( ExplodeTouch ), + + DEFINE_FIELD( m_hOwner, FIELD_EHANDLE ), + DEFINE_FIELD( m_flSpawnTime, FIELD_TIME ), + DEFINE_FIELD( m_pSprite, FIELD_CLASSPTR ), + +END_DATADESC() + + +void CNPC_ControllerZapBall::Spawn( void ) +{ + Precache( ); + // motor + SetMoveType( MOVETYPE_FLY ); +// SetSolid( SOLID_CUSTOM ); + SetSolid( SOLID_BBOX ); + SetSize( vec3_origin, vec3_origin ); + + m_pSprite = CSprite::SpriteCreate( "sprites/xspark4.vmt", GetAbsOrigin(), FALSE ); + m_pSprite->SetTransparency( kRenderTransAdd, 255, 255, 255, 255, kRenderFxNoDissipation ); + m_pSprite->SetAttachment( this, 0 ); + m_pSprite->SetScale( 0.5 ); + + UTIL_SetSize( this, Vector( 0, 0, 0), Vector(0, 0, 0) ); + UTIL_SetOrigin( this, GetAbsOrigin() ); + + SetThink( &CNPC_ControllerZapBall::AnimateThink ); + SetTouch( &CNPC_ControllerZapBall::ExplodeTouch ); + + m_hOwner = GetOwnerEntity(); + + m_flSpawnTime = gpGlobals->curtime; // keep track of when ball spawned + SetNextThink( gpGlobals->curtime + 0.1 ); +} + + +void CNPC_ControllerZapBall::Precache( void ) +{ + PrecacheModel( "sprites/xspark4.vmt"); +} + + +void CNPC_ControllerZapBall::AnimateThink( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1 ); + + SetCycle( ((int)GetCycle() + 1) % 11 ); + + if (gpGlobals->curtime - m_flSpawnTime > 5 || GetAbsVelocity().Length() < 10) + { + SetTouch( NULL ); + Kill(); + } +} + + +void CNPC_ControllerZapBall::ExplodeTouch( CBaseEntity *pOther ) +{ + if (m_takedamage = DAMAGE_YES ) + { + trace_t tr; + tr = GetTouchTrace( ); + + ClearMultiDamage( ); + + Vector vecAttackDir = GetAbsVelocity(); + VectorNormalize( vecAttackDir ); + + if (m_hOwner != NULL) + { + CTakeDamageInfo info( this, m_hOwner, sk_controller_dmgball.GetFloat(), DMG_ENERGYBEAM ); + CalculateMeleeDamageForce( &info, vecAttackDir, tr.endpos ); + pOther->DispatchTraceAttack( info, vecAttackDir, &tr ); + } + else + { + CTakeDamageInfo info( this, this, sk_controller_dmgball.GetFloat(), DMG_ENERGYBEAM ); + CalculateMeleeDamageForce( &info, vecAttackDir, tr.endpos ); + pOther->DispatchTraceAttack( info, vecAttackDir, &tr ); + } + + ApplyMultiDamage(); + + // void UTIL_EmitAmbientSound( CBaseEntity *entity, const Vector &vecOrigin, const char *samp, float vol, soundlevel_t soundlevel, int fFlags, int pitch, float soundtime /*= 0.0f*/ ) + + UTIL_EmitAmbientSound( GetSoundSourceIndex(), tr.endpos, "Controller.ElectroSound", 0.3, SNDLVL_NORM, 0, random->RandomInt( 90, 99 ) ); + } + + Kill(); +} + +void CNPC_ControllerZapBall::Kill( void ) +{ + UTIL_Remove( m_pSprite ); + UTIL_Remove( this ); +} diff --git a/game/server/hl1/hl1_npc_controller.h b/game/server/hl1/hl1_npc_controller.h new file mode 100644 index 0000000..c012063 --- /dev/null +++ b/game/server/hl1/hl1_npc_controller.h @@ -0,0 +1,170 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef NPC_CONTROLLER_H +#define NPC_CONTROLLER_H +#pragma once + +#include "ai_basenpc_flyer.h" + +class CSprite; +class CNPC_Controller; + +enum +{ + TASK_CONTROLLER_CHASE_ENEMY = LAST_SHARED_TASK, + TASK_CONTROLLER_STRAFE, + TASK_CONTROLLER_TAKECOVER, + TASK_CONTROLLER_FAIL, +}; + +enum +{ + SCHED_CONTROLLER_CHASE_ENEMY = LAST_SHARED_SCHEDULE, + SCHED_CONTROLLER_STRAFE, + SCHED_CONTROLLER_TAKECOVER, + SCHED_CONTROLLER_FAIL, +}; + +class CControllerNavigator : public CAI_ComponentWithOuter<CNPC_Controller, CAI_Navigator> +{ + typedef CAI_ComponentWithOuter<CNPC_Controller, CAI_Navigator> BaseClass; +public: + CControllerNavigator( CNPC_Controller *pOuter ) + : BaseClass( pOuter ) + { + } + + bool ActivityIsLocomotive( Activity activity ) { return true; } +}; + +class CNPC_Controller : public CAI_BaseFlyingBot +{ +public: + + DECLARE_CLASS( CNPC_Controller, CAI_BaseFlyingBot ); + DEFINE_CUSTOM_AI; + DECLARE_DATADESC(); + + void Spawn( void ); + void Precache( void ); + + float MaxYawSpeed( void ) { return 120.0f; } + Class_T Classify ( void ) { return CLASS_ALIEN_MILITARY; } + + void HandleAnimEvent( animevent_t *pEvent ); + + void RunAI( void ); + + int RangeAttack1Conditions ( float flDot, float flDist ); // balls + int RangeAttack2Conditions ( float flDot, float flDist ); // head + int MeleeAttack1Conditions ( float flDot, float flDist ) { return COND_NONE; } + int MeleeAttack2Conditions ( float flDot, float flDist ) { return COND_NONE; } + + int TranslateSchedule( int scheduleType ); + void StartTask ( const Task_t *pTask ); + void RunTask ( const Task_t *pTask ); + + void Stop( void ); + bool OverridePathMove( float flInterval ); + bool OverrideMove( float flInterval ); + + void MoveToTarget( float flInterval, const Vector &vecMoveTarget ); + + void SetActivity ( Activity NewActivity ); + bool ShouldAdvanceRoute( float flWaypointDist ); + int LookupFloat( ); + + friend class CControllerNavigator; + CAI_Navigator *CreateNavigator() + { + return new CControllerNavigator( this ); + } + + bool ShouldGib( const CTakeDamageInfo &info ); + bool HasAlienGibs( void ) { return true; } + bool HasHumanGibs( void ) { return false; } + + float m_flNextFlinch; + + float m_flShootTime; + float m_flShootEnd; + + void PainSound( void ); + void AlertSound( void ); + void IdleSound( void ); + void AttackSound( void ); + void DeathSound( void ); + + static const char *pAttackSounds[]; + static const char *pIdleSounds[]; + static const char *pAlertSounds[]; + static const char *pPainSounds[]; + static const char *pDeathSounds[]; + + int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + void Event_Killed( const CTakeDamageInfo &info ); + + CSprite *m_pBall[2]; // hand balls + int m_iBall[2]; // how bright it should be + float m_iBallTime[2]; // when it should be that color + int m_iBallCurrent[2]; // current brightness + + Vector m_vecEstVelocity; + + Vector m_velocity; + bool m_fInCombat; + + void SetSequence( int nSequence ); +}; + +class CNPC_ControllerHeadBall : public CAI_BaseNPC +{ +public: + DECLARE_CLASS( CNPC_ControllerHeadBall, CAI_BaseNPC ); + + DECLARE_DATADESC(); + + void Spawn( void ); + void Precache( void ); + + void EXPORT HuntThink( void ); + void EXPORT KillThink( void ); + void EXPORT BounceTouch( CBaseEntity *pOther ); + void MovetoTarget( Vector vecTarget ); + + int m_iTrail; + int m_flNextAttack; + float m_flSpawnTime; + Vector m_vecIdeal; + EHANDLE m_hOwner; + + CSprite *m_pSprite; +}; + +class CNPC_ControllerZapBall : public CAI_BaseNPC +{ +public: + DECLARE_CLASS( CNPC_ControllerHeadBall, CAI_BaseNPC ); + + DECLARE_DATADESC(); + + void Spawn( void ); + void Precache( void ); + + void EXPORT AnimateThink( void ); + void EXPORT ExplodeTouch( CBaseEntity *pOther ); + + void Kill( void ); + + EHANDLE m_hOwner; + float m_flSpawnTime; + + CSprite *m_pSprite; +}; + +#endif //NPC_CONTROLLER_H
\ No newline at end of file diff --git a/game/server/hl1/hl1_npc_gargantua.cpp b/game/server/hl1/hl1_npc_gargantua.cpp new file mode 100644 index 0000000..c5d8b2c --- /dev/null +++ b/game/server/hl1/hl1_npc_gargantua.cpp @@ -0,0 +1,1174 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Bullseyes act as targets for other NPC's to attack and to trigger +// events +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "beam_shared.h" +#include "Sprite.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "hl1_npc_gargantua.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "basecombatweapon.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "shake.h" +#include "decals.h" +#include "particle_smokegrenade.h" +#include "gib.h" +#include "func_break.h" +#include "hl1_shareddefs.h" + + +extern short g_sModelIndexFireball; +int gGargGibModel; + +//========================================================= +// Gargantua Monster +//========================================================= +#define GARG_ATTACKDIST 120.0f + +// Garg animation events +#define GARG_AE_SLASH_LEFT 1 +//#define GARG_AE_BEAM_ATTACK_RIGHT 2 // No longer used +#define GARG_AE_LEFT_FOOT 3 +#define GARG_AE_RIGHT_FOOT 4 +#define GARG_AE_STOMP 5 +#define GARG_AE_BREATHE 6 + + +// Gargantua is immune to any damage but this +#define GARG_DAMAGE ( DMG_ENERGYBEAM | DMG_CRUSH | DMG_MISSILEDEFENSE | DMG_BLAST ) +#define GARG_EYE_SPRITE_NAME "sprites/gargeye1.vmt" +#define GARG_BEAM_SPRITE_NAME "sprites/xbeam3.vmt" +#define GARG_BEAM_SPRITE2 "sprites/xbeam3.vmt" +#define GARG_STOMP_SPRITE_NAME "sprites/gargeye1.vmt" +#define GARG_FLAME_LENGTH 330 +#define GARG_GIB_MODEL "models/metalplategibs.mdl" + +#define STOMP_SPRITE_COUNT 10 + + +#define ATTACH_EYE 1 + +ConVar sk_gargantua_health ( "sk_gargantua_health", "800" ); +ConVar sk_gargantua_dmg_slash( "sk_gargantua_dmg_slash", "10" ); +ConVar sk_gargantua_dmg_fire ( "sk_gargantua_dmg_fire", "3" ); +ConVar sk_gargantua_dmg_stomp( "sk_gargantua_dmg_stomp", "50" ); + +enum +{ + TASK_SOUND_ATTACK = LAST_SHARED_TASK, + TASK_FLAME_SWEEP, +}; + +enum +{ + SCHED_GARG_FLAME = LAST_SHARED_SCHEDULE, + SCHED_GARG_SWIPE, + SCHED_GARG_CHASE_ENEMY, + SCHED_GARG_CHASE_ENEMY_FAILED, +}; + +LINK_ENTITY_TO_CLASS( monster_gargantua, CNPC_Gargantua ); + +BEGIN_DATADESC( CNPC_Gargantua ) + DEFINE_FIELD( m_pEyeGlow, FIELD_CLASSPTR ), + DEFINE_FIELD( m_eyeBrightness, FIELD_INTEGER ), + DEFINE_FIELD( m_seeTime, FIELD_TIME ), + DEFINE_FIELD( m_flameTime, FIELD_TIME ), + DEFINE_FIELD( m_streakTime, FIELD_TIME ), + DEFINE_ARRAY( m_pFlame, FIELD_CLASSPTR, 4 ), + DEFINE_FIELD( m_flameX, FIELD_FLOAT ), + DEFINE_FIELD( m_flameY, FIELD_FLOAT ), + DEFINE_FIELD( m_flDmgTime, FIELD_TIME ), + DEFINE_FIELD( m_painSoundTime, FIELD_TIME ), +END_DATADESC() + +static void MoveToGround( Vector *position, CBaseEntity *ignore, const Vector &mins, const Vector &maxs ) +{ + trace_t tr; + // Find point on floor where enemy would stand at chasePosition + Vector floor = *position; + floor.z -= 1024; + UTIL_TraceHull( *position, floor, mins, maxs, MASK_NPCSOLID, ignore, COLLISION_GROUP_NONE, &tr ); + if ( tr.fraction < 1 ) + { + position->z = tr.endpos.z; + } +} + +class CStomp : public CBaseEntity +{ + DECLARE_CLASS( CStomp, CBaseEntity ); + +public: + DECLARE_DATADESC(); + + virtual void Precache(); + + void Spawn( void ); + void Think( void ); + static CStomp *StompCreate( Vector &origin, Vector &end, float speed, CBaseEntity* pOwner ); + +private: + Vector m_vecMoveDir; + float m_flScale; + float m_flSpeed; + unsigned int m_uiFramerate; + float m_flDmgTime; + CBaseEntity* m_pOwner; + +// UNDONE: re-use this sprite list instead of creating new ones all the time +// CSprite *m_pSprites[ STOMP_SPRITE_COUNT ]; +}; + +BEGIN_DATADESC(CStomp) + DEFINE_FIELD( m_vecMoveDir, FIELD_VECTOR ), + DEFINE_FIELD( m_flScale, FIELD_FLOAT ), + DEFINE_FIELD( m_flSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_uiFramerate, FIELD_INTEGER ), + DEFINE_FIELD( m_flDmgTime, FIELD_TIME ), + DEFINE_FIELD( m_pOwner, FIELD_CLASSPTR ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( garg_stomp, CStomp ); +CStomp *CStomp::StompCreate( Vector &origin, Vector &end, float speed, CBaseEntity* pOwner ) +{ + CStomp *pStomp = (CStomp*)CreateEntityByName( "garg_stomp" ); + + pStomp->SetAbsOrigin( origin ); + Vector dir = (end - origin); +// pStomp->m_flScale = dir.Length(); + pStomp->m_flScale = 2048; + pStomp->m_vecMoveDir = dir; + VectorNormalize( pStomp->m_vecMoveDir ); + pStomp->m_flSpeed = speed; + pStomp->m_pOwner = pOwner; + pStomp->Spawn(); + + return pStomp; + +} + +void CStomp::Precache() +{ + BaseClass::Precache(); + + PrecacheScriptSound( "Garg.Stomp" ); + PrecacheModel( GARG_STOMP_SPRITE_NAME ); +} + + +void CStomp::Spawn( void ) +{ + Precache(); + + SetNextThink( gpGlobals->curtime ); + SetClassname( "garg_stomp" ); + m_flDmgTime = gpGlobals->curtime; + + m_uiFramerate = 30; +// pev->rendermode = kRenderTransTexture; +// SetBrightness( 0 ); + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Garg.Stomp" ); + +} + + +#define STOMP_INTERVAL 0.025 + +void CStomp::Think( void ) +{ + trace_t tr; + + SetNextThink( gpGlobals->curtime + 0.1 ); + + // Do damage for this frame + Vector vecStart = GetAbsOrigin(); + vecStart.z += 30; + Vector vecEnd = vecStart + (m_vecMoveDir * m_flSpeed * gpGlobals->frametime); + + UTIL_TraceHull( vecStart, vecEnd, Vector(-32, -32, -32), Vector(32, 32, 32), MASK_SOLID, m_pOwner, COLLISION_GROUP_NONE, &tr ); +// NDebugOverlay::Line( vecStart, vecEnd, 0, 255, 0, false, 10.0f ); + + if ( tr.m_pEnt ) + { + CBaseEntity *pEntity = tr.m_pEnt; + CTakeDamageInfo info( this, this, 50, DMG_SONIC ); + CalculateMeleeDamageForce( &info, m_vecMoveDir, tr.endpos ); + pEntity->TakeDamage( info ); + } + + // Accelerate the effect + m_flSpeed += (gpGlobals->frametime) * m_uiFramerate; + m_uiFramerate += (gpGlobals->frametime) * 2000; + + // Move and spawn trails + if ( gpGlobals->curtime - m_flDmgTime > 0.2f ) + { + m_flDmgTime = gpGlobals->curtime - 0.2f; + } + + while ( gpGlobals->curtime - m_flDmgTime > STOMP_INTERVAL ) + { + SetAbsOrigin( GetAbsOrigin() + m_vecMoveDir * m_flSpeed * STOMP_INTERVAL ); + for ( int i = 0; i < 2; i++ ) + { + CSprite *pSprite = CSprite::SpriteCreate( GARG_STOMP_SPRITE_NAME, GetAbsOrigin(), TRUE ); + if ( pSprite ) + { + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() - Vector(0,0,500), MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); + pSprite->SetAbsOrigin( tr.endpos ); +// pSprite->pev->velocity = Vector(RandomFloat(-200,200),RandomFloat(-200,200),175); + pSprite->SetNextThink( gpGlobals->curtime + 0.3 ); + pSprite->SetThink( &CSprite::SUB_Remove ); + pSprite->SetTransparency( kRenderTransAdd, 255, 255, 255, 255, kRenderFxFadeFast ); + } + g_pEffects->EnergySplash( tr.endpos, tr.plane.normal ); + } + m_flDmgTime += STOMP_INTERVAL; + // Scale has the "life" of this effect + m_flScale -= STOMP_INTERVAL * m_flSpeed; + if ( m_flScale <= 0 ) + { + // Life has run out + UTIL_Remove(this); + CPASAttenuationFilter filter( this ); + StopSound( entindex(), CHAN_STATIC, "Garg.Stomp" ); + } + } +} + + +//========================================================================= +// Gargantua +//========================================================================= + +//========================================================= +// Spawn +//========================================================= +void CNPC_Gargantua::Spawn() +{ + Precache( ); + + SetModel( "models/garg.mdl" ); + + SetNavType(NAV_GROUND); + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + + Vector vecSurroundingMins( -80, -80, 0 ); + Vector vecSurroundingMaxs( 80, 80, 214 ); + CollisionProp()->SetSurroundingBoundsType( USE_SPECIFIED_BOUNDS, &vecSurroundingMins, &vecSurroundingMaxs ); + + m_bloodColor = BLOOD_COLOR_GREEN; + m_iHealth = sk_gargantua_health.GetFloat(); + SetViewOffset( Vector ( 0, 0, 96 ) );// taken from mdl file + m_flFieldOfView = -0.2;// width of forward view cone ( as a dotproduct result ) + m_NPCState = NPC_STATE_NONE; + + CapabilitiesAdd( bits_CAP_MOVE_GROUND ); + CapabilitiesAdd( bits_CAP_INNATE_RANGE_ATTACK1 | bits_CAP_INNATE_MELEE_ATTACK1 | bits_CAP_INNATE_MELEE_ATTACK2 ); + + SetHullType( HULL_LARGE ); + SetHullSizeNormal(); + + m_pEyeGlow = CSprite::SpriteCreate( GARG_EYE_SPRITE_NAME, GetAbsOrigin(), FALSE ); + m_pEyeGlow->SetTransparency( kRenderGlow, 255, 255, 255, 0, kRenderFxNoDissipation ); + m_pEyeGlow->SetAttachment( this, 1 ); + EyeOff(); + + m_seeTime = gpGlobals->curtime + 5; + m_flameTime = gpGlobals->curtime + 2; + + NPCInit(); + + BaseClass::Spawn(); + + // Give garg a healthy free knowledge. + GetEnemies()->SetFreeKnowledgeDuration( 59.0f ); +} + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CNPC_Gargantua::Precache() +{ + PrecacheModel("models/garg.mdl"); + PrecacheModel( GARG_EYE_SPRITE_NAME ); + PrecacheModel( GARG_BEAM_SPRITE_NAME ); + PrecacheModel( GARG_BEAM_SPRITE2 ); + //gStompSprite = PRECACHE_MODEL( GARG_STOMP_SPRITE_NAME ); + gGargGibModel = PrecacheModel( GARG_GIB_MODEL ); + + PrecacheScriptSound( "Garg.AttackHit" ); + PrecacheScriptSound( "Garg.AttackMiss" ); + PrecacheScriptSound( "Garg.Footstep" ); + PrecacheScriptSound( "Garg.Breath" ); + PrecacheScriptSound( "Garg.Attack" ); + PrecacheScriptSound( "Garg.Pain" ); + PrecacheScriptSound( "Garg.BeamAttackOn" ); + PrecacheScriptSound( "Garg.BeamAttackRun" ); + PrecacheScriptSound( "Garg.BeamAttackOff" ); + PrecacheScriptSound( "Garg.StompSound" ); +} + + +Class_T CNPC_Gargantua::Classify ( void ) +{ + return CLASS_ALIEN_MONSTER; +} + +void CNPC_Gargantua::PrescheduleThink( void ) +{ + if ( !HasCondition( COND_SEE_ENEMY ) ) + { + m_seeTime = gpGlobals->curtime + 5; + EyeOff(); + } + else + { + EyeOn( 200 ); + } + + EyeUpdate(); +} + +float CNPC_Gargantua::MaxYawSpeed ( void ) +{ + float ys = 60; + + switch ( GetActivity() ) + { + case ACT_IDLE: + ys = 60; + break; + case ACT_TURN_LEFT: + case ACT_TURN_RIGHT: + ys = 180; + break; + case ACT_WALK: + case ACT_RUN: + ys = 60; + break; + + default: + ys = 60; + break; + } + + return ys; +} + +int CNPC_Gargantua::MeleeAttack1Conditions( float flDot, float flDist ) +{ + if (flDot >= 0.7) + { + if ( flDist <= GARG_ATTACKDIST ) + { + return COND_CAN_MELEE_ATTACK1; + } + } + + return COND_NONE; +} + + +// Flame thrower madness! +int CNPC_Gargantua::MeleeAttack2Conditions( float flDot, float flDist ) +{ + if ( gpGlobals->curtime > m_flameTime ) + { + if ( flDot >= 0.8 ) + { + if ( flDist > GARG_ATTACKDIST ) + { + if ( flDist <= GARG_FLAME_LENGTH ) + return COND_CAN_MELEE_ATTACK2; + } + } + } + + return COND_NONE; +} + +//========================================================= +// CheckRangeAttack1 +// flDot is the cos of the angle of the cone within which +// the attack can occur. +//========================================================= +// +// Stomp attack +// +//========================================================= +int CNPC_Gargantua::RangeAttack1Conditions( float flDot, float flDist ) +{ + if ( gpGlobals->curtime > m_seeTime ) + { + if ( flDot >= 0.7 ) + { + if ( flDist > GARG_ATTACKDIST ) + { + return COND_CAN_RANGE_ATTACK1; + } + } + } + + return COND_NONE; +} + +//========================================================= +// CheckTraceHullAttack - expects a length to trace, amount +// of damage to do, and damage type. Returns a pointer to +// the damaged entity in case the monster wishes to do +// other stuff to the victim (punchangle, etc) +// Used for many contact-range melee attacks. Bites, claws, etc. + +// Overridden for Gargantua because his swing starts lower as +// a percentage of his height (otherwise he swings over the +// players head) +//========================================================= +CBaseEntity* CNPC_Gargantua::GargantuaCheckTraceHullAttack(float flDist, int iDamage, int iDmgType) +{ + trace_t tr; + + Vector vForward, vUp; + AngleVectors( GetAbsAngles(), &vForward, NULL, &vUp ); + + Vector vecStart = GetAbsOrigin(); + vecStart.z += 64; + Vector vecEnd = vecStart + ( vForward * flDist) - ( vUp * flDist * 0.3); + + //UTIL_TraceHull( vecStart, vecEnd, dont_ignore_monsters, head_hull, ENT(pev), &tr ); + + UTIL_TraceEntity( this, GetAbsOrigin(), vecEnd, MASK_SOLID, &tr ); + + if ( tr.m_pEnt ) + { + CBaseEntity *pEntity = tr.m_pEnt; + + if ( iDamage > 0 ) + { + CTakeDamageInfo info( this, this, iDamage, iDmgType ); + CalculateMeleeDamageForce( &info, vForward, tr.endpos ); + pEntity->TakeDamage( info ); + } + + return pEntity; + } + + return NULL; +} + +void CNPC_Gargantua::HandleAnimEvent( animevent_t *pEvent ) +{ + CPASAttenuationFilter filter( this ); + + switch( pEvent->event ) + { + case GARG_AE_SLASH_LEFT: + { + // HACKHACK!!! + CBaseEntity *pHurt = GargantuaCheckTraceHullAttack( GARG_ATTACKDIST + 10.0, sk_gargantua_dmg_slash.GetFloat(), DMG_SLASH ); + + if (pHurt) + { + if ( pHurt->GetFlags() & ( FL_NPC | FL_CLIENT ) ) + { + pHurt->ViewPunch( QAngle( -30, -30, 30 ) ); + + Vector vRight; + AngleVectors( GetAbsAngles(), NULL, &vRight, NULL ); + pHurt->SetAbsVelocity( pHurt->GetAbsVelocity() - vRight * 100 ); + } + + EmitSound( filter, entindex(), "Garg.AttackHit" ); + } + else // Play a random attack miss sound + { + EmitSound( filter, entindex(),"Garg.AttackMiss" ); + } + } + break; + + case GARG_AE_RIGHT_FOOT: + case GARG_AE_LEFT_FOOT: + + UTIL_ScreenShake( GetAbsOrigin(), 4.0, 3.0, 1.0, 1500, SHAKE_START ); + EmitSound( filter, entindex(), "Garg.Footstep" ); + break; + + case GARG_AE_STOMP: + StompAttack(); + m_seeTime = gpGlobals->curtime + 12; + break; + + case GARG_AE_BREATHE: + EmitSound( filter, entindex(), "Garg.Breath" ); + break; + + default: + BaseClass::HandleAnimEvent(pEvent); + break; + } +} + +int CNPC_Gargantua::TranslateSchedule( int scheduleType ) +{ + //TEMP TEMP + if ( FlameIsOn() ) + FlameDestroy(); + + switch( scheduleType ) + { + case SCHED_MELEE_ATTACK2: + return SCHED_GARG_FLAME; + case SCHED_MELEE_ATTACK1: + return SCHED_GARG_SWIPE; + + case SCHED_CHASE_ENEMY: + return SCHED_GARG_CHASE_ENEMY; + + case SCHED_CHASE_ENEMY_FAILED: + return SCHED_GARG_CHASE_ENEMY_FAILED; + + case SCHED_ALERT_STAND: + return SCHED_CHASE_ENEMY; + + break; + } + + return BaseClass::TranslateSchedule( scheduleType ); +} + +void CNPC_Gargantua::StartTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_FLAME_SWEEP: + + //TEMP TEMP + FlameCreate(); + m_flWaitFinished = gpGlobals->curtime + pTask->flTaskData; + m_flameTime = gpGlobals->curtime + 6; + m_flameX = 0; + m_flameY = 0; + break; + + case TASK_SOUND_ATTACK: + + if ( random->RandomInt(0,100) < 30 ) + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Garg.Attack" ); + } + + TaskComplete(); + break; + + case TASK_DIE: + m_flWaitFinished = gpGlobals->curtime + 1.6; + DeathEffect(); + // FALL THROUGH + default: + BaseClass::StartTask( pTask ); + break; + } +} + +bool CNPC_Gargantua::ShouldGib( const CTakeDamageInfo &info ) +{ + return false; +} + +//========================================================= +// RunTask +//========================================================= +void CNPC_Gargantua::RunTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_DIE: + + if ( gpGlobals->curtime > m_flWaitFinished ) + { + //TEMP TEMP + m_nRenderFX = kRenderFxExplode; + SetRenderColor( 255, 0, 0 , 255 ); + StopAnimation(); + SetNextThink( gpGlobals->curtime + 0.15 ); + SetThink( &CBaseEntity::SUB_Remove ); + + int i; + + int parts = modelinfo->GetModelFrameCount( modelinfo->GetModel( gGargGibModel ) ); + + for ( i = 0; i < 10; i++ ) + { + CGib *pGib = CREATE_ENTITY( CGib, "gib" ); + + pGib->Spawn( GARG_GIB_MODEL); + + int bodyPart = 0; + + if ( parts > 1 ) + bodyPart = random->RandomInt( 0, parts-1 ); + + pGib->SetBodygroup( 0, bodyPart ); + pGib->SetBloodColor( BLOOD_COLOR_YELLOW ); + pGib->m_material = matNone; + pGib->SetAbsOrigin( GetAbsOrigin() ); + pGib->SetAbsVelocity( UTIL_RandomBloodVector() * random->RandomFloat( 300, 500 ) ); + + pGib->SetNextThink( gpGlobals->curtime + 1.25 ); + pGib->SetThink( &CBaseEntity::SUB_FadeOut ); + } + + Vector vecSize = Vector( 200, 200, 128 ); + CPVSFilter filter( GetAbsOrigin() ); + te->BreakModel( filter, 0.0, GetAbsOrigin(), vec3_angle, vecSize, vec3_origin, + gGargGibModel, 200, 50, 3.0, BREAK_FLESH ); + + return; + } + else + BaseClass::RunTask( pTask ); + break; + + case TASK_FLAME_SWEEP: + if ( gpGlobals->curtime > m_flWaitFinished ) + { + //TEMP TEMP + FlameDestroy(); + TaskComplete(); + FlameControls( 0, 0 ); + SetBoneController( 0, 0 ); + SetBoneController( 1, 0 ); + } + else + { + bool cancel = false; + + QAngle angles = QAngle( 0, 0, 0 ); + + //TEMP TEMP + FlameUpdate(); + CBaseEntity *pEnemy = GetEnemy(); + + if ( pEnemy ) + { + Vector org = GetAbsOrigin(); + org.z += 64; + Vector dir = pEnemy->BodyTarget(org) - org; + + VectorAngles( dir, angles ); + angles.x = -angles.x; + angles.y -= GetAbsAngles().y; + + if ( dir.Length() > 400 ) + cancel = true; + } + if ( fabs(angles.y) > 60 ) + cancel = true; + + if ( cancel ) + { + m_flWaitFinished -= 0.5; + m_flameTime -= 0.5; + } + + //TEMP TEMP + //FlameControls( angles.x + 2 * sin(gpGlobals->curtime*8), angles.y + 28 * sin(gpGlobals->curtime*8.5) ); + FlameControls( angles.x, angles.y ); + } + break; + + default: + BaseClass::RunTask( pTask ); + break; + } +} + +void CNPC_Gargantua::FlameCreate( void ) +{ + int i; + Vector posGun; + QAngle angleGun; + + trace_t trace; + + Vector vForward; + + AngleVectors( GetAbsAngles(), &vForward ); + + for ( i = 0; i < 4; i++ ) + { + if ( i < 2 ) + m_pFlame[i] = CBeam::BeamCreate( GARG_BEAM_SPRITE_NAME, 24.0 ); + else + m_pFlame[i] = CBeam::BeamCreate( GARG_BEAM_SPRITE2, 14.0 ); + if ( m_pFlame[i] ) + { + int attach = i%2; + // attachment is 0 based in GetAttachment + GetAttachment( attach+1, posGun, angleGun ); + + Vector vecEnd = ( vForward * GARG_FLAME_LENGTH) + posGun; + //UTIL_TraceLine( posGun, vecEnd, dont_ignore_monsters, edict(), &trace ); + + UTIL_TraceLine ( posGun, vecEnd, MASK_SOLID, this, COLLISION_GROUP_NONE, &trace); +// NDebugOverlay::Line( posGun, vecEnd, 255, 255, 255, false, 10.0f ); + + m_pFlame[i]->PointEntInit( trace.endpos, this ); + if ( i < 2 ) + m_pFlame[i]->SetColor( 255, 130, 90 ); + else + m_pFlame[i]->SetColor( 0, 120, 255 ); + m_pFlame[i]->SetBrightness( 190 ); + m_pFlame[i]->SetBeamFlags( FBEAM_SHADEIN ); + m_pFlame[i]->SetScrollRate( 20 ); + // attachment is 1 based in SetEndAttachment + m_pFlame[i]->SetEndAttachment( attach + 2 ); + CSoundEnt::InsertSound( SOUND_COMBAT, posGun, 384, 0.3 ); + } + } + + CPASAttenuationFilter filter4( this ); + EmitSound( filter4, entindex(), "Garg.BeamAttackOn" ); + EmitSound( filter4, entindex(), "Garg.BeamAttackRun" ); +} + + +void CNPC_Gargantua::FlameControls( float angleX, float angleY ) +{ + if ( angleY < -180 ) + angleY += 360; + else if ( angleY > 180 ) + angleY -= 360; + + if ( angleY < -45 ) + angleY = -45; + else if ( angleY > 45 ) + angleY = 45; + + m_flameX = UTIL_ApproachAngle( angleX, m_flameX, 4 ); + m_flameY = UTIL_ApproachAngle( angleY, m_flameY, 8 ); + SetBoneController( 0, m_flameY ); + SetBoneController( 1, m_flameX ); +} + + +void CNPC_Gargantua::FlameUpdate( void ) +{ + int i; + static float offset[2] = { 60, -60 }; + trace_t trace; + Vector vecStart; + QAngle angleGun; + BOOL streaks = FALSE; + + Vector vForward; + + for ( i = 0; i < 2; i++ ) + { + if ( m_pFlame[i] ) + { + QAngle vecAim = GetAbsAngles(); + vecAim.x += -m_flameX; + vecAim.y += m_flameY; + + AngleVectors( vecAim, &vForward ); + + GetAttachment( i + 2, vecStart, angleGun ); + Vector vecEnd = vecStart + ( vForward * GARG_FLAME_LENGTH); // - offset[i] * gpGlobals->v_right; + + UTIL_TraceLine ( vecStart, vecEnd, MASK_SOLID, this, COLLISION_GROUP_NONE, &trace); + + m_pFlame[i]->SetStartPos( trace.endpos ); + m_pFlame[i+2]->SetStartPos( (vecStart * 0.6) + (trace.endpos * 0.4) ); + + if ( trace.fraction != 1.0 && gpGlobals->curtime > m_streakTime ) + { + g_pEffects->Sparks( trace.endpos, 1, 1, &trace.plane.normal ); + streaks = TRUE; + UTIL_DecalTrace( &trace, "SmallScorch" ); + } + // RadiusDamage( trace.vecEndPos, pev, pev, gSkillData.gargantuaDmgFire, CLASS_ALIEN_MONSTER, DMG_BURN ); + FlameDamage( vecStart, trace.endpos, this, this, sk_gargantua_dmg_fire.GetFloat(), CLASS_ALIEN_MONSTER, DMG_BURN ); + + CBroadcastRecipientFilter filter; + GetAttachment(i + 2, vecStart, angleGun); + te->DynamicLight( filter, 0.0, &vecStart, 255, 0, 0, 0, 48, 0.2, 150 ); + } + } + if ( streaks ) + m_streakTime = gpGlobals->curtime; +} + + + +void CNPC_Gargantua::FlameDamage( Vector vecStart, Vector vecEnd, CBaseEntity *pevInflictor, CBaseEntity *pevAttacker, float flDamage, int iClassIgnore, int bitsDamageType ) +{ + CBaseEntity *pEntity = NULL; + trace_t tr; + float flAdjustedDamage; + Vector vecSpot; + + Vector vecMid = (vecStart + vecEnd) * 0.5; + +// float searchRadius = (vecStart - vecMid).Length(); + float searchRadius = GARG_FLAME_LENGTH; + float maxDamageRadius = searchRadius / 2.0; + + Vector vecAim = (vecEnd - vecStart); + + VectorNormalize( vecAim ); + + // iterate on all entities in the vicinity. + while ((pEntity = gEntList.FindEntityInSphere( pEntity, GetAbsOrigin(), searchRadius )) != NULL) + { + + if ( pEntity->m_takedamage != DAMAGE_NO ) + { + // UNDONE: this should check a damage mask, not an ignore + if ( iClassIgnore != CLASS_NONE && pEntity->Classify() == iClassIgnore ) + {// houndeyes don't hurt other houndeyes with their attack + continue; + } + + vecSpot = pEntity->BodyTarget( vecMid ); + + float dist = DotProduct( vecAim, vecSpot - vecMid ); + if (dist > searchRadius) + dist = searchRadius; + else if (dist < -searchRadius) + dist = searchRadius; + + Vector vecSrc = vecMid + dist * vecAim; + + UTIL_TraceLine ( vecStart, vecSpot, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr); +// NDebugOverlay::Line( vecStart, vecSpot, 0, 255, 0, false, 10.0f ); + + if ( tr.fraction == 1.0 || tr.m_pEnt == pEntity ) + {// the explosion can 'see' this entity, so hurt them! + // decrease damage for an ent that's farther from the flame. + dist = ( vecSrc - tr.endpos ).Length(); + + if (dist > maxDamageRadius) + { + flAdjustedDamage = flDamage - (dist - maxDamageRadius) * 0.4; + if (flAdjustedDamage <= 0) + continue; + } + else + { + flAdjustedDamage = flDamage; + } + + // ALERT( at_console, "hit %s\n", STRING( pEntity->pev->classname ) ); + if (tr.fraction != 1.0) + { + ClearMultiDamage( ); + + Vector vDir = (tr.endpos - vecSrc); + VectorNormalize( vDir ); + + CTakeDamageInfo info( pevInflictor, pevAttacker, flAdjustedDamage, bitsDamageType ); + CalculateMeleeDamageForce( &info, vDir, tr.endpos ); + pEntity->DispatchTraceAttack( info, vDir, &tr ); + ApplyMultiDamage(); + } + else + { + pEntity->TakeDamage( CTakeDamageInfo( pevInflictor, pevAttacker, flAdjustedDamage, bitsDamageType ) ); + } + } + } + } +} + + +void CNPC_Gargantua::FlameDestroy( void ) +{ + int i; + + CPASAttenuationFilter filter4( this ); + EmitSound( filter4, entindex(), "Garg.BeamAttackOff" ); + + for ( i = 0; i < 4; i++ ) + { + if ( m_pFlame[i] ) + { + UTIL_Remove( m_pFlame[i] ); + m_pFlame[i] = NULL; + } + } +} + + +void CNPC_Gargantua::EyeOn( int level ) +{ + m_eyeBrightness = level; +} + +void CNPC_Gargantua::EyeOff( void ) +{ + m_eyeBrightness = 0; +} + +void CNPC_Gargantua::EyeUpdate( void ) +{ + if ( m_pEyeGlow ) + { + m_pEyeGlow->SetBrightness( UTIL_Approach( m_eyeBrightness, m_pEyeGlow->GetBrightness(), 26 ), 0.5f ); + if ( m_pEyeGlow->GetBrightness() == 0 ) + { + m_pEyeGlow->AddEffects( EF_NODRAW ); + } + else + { + m_pEyeGlow->RemoveEffects( EF_NODRAW ); + } + } +} + + +void CNPC_Gargantua::StompAttack( void ) +{ + trace_t trace; + + Vector vecForward; + AngleVectors(GetAbsAngles(), &vecForward ); + Vector vecStart = GetAbsOrigin() + Vector(0,0,60) + 35 * vecForward; + + CBaseEntity* pPlayer = GetEnemy(); + if ( !pPlayer ) + return; + + Vector vecAim = pPlayer->GetAbsOrigin() - GetAbsOrigin(); + VectorNormalize( vecAim ); + Vector vecEnd = (vecAim * 1024) + vecStart; + + UTIL_TraceLine( vecStart, vecEnd, MASK_SOLID, this, COLLISION_GROUP_NONE, &trace ); +// NDebugOverlay::Line( vecStart, vecEnd, 255, 0, 0, false, 10.0f ); + + CStomp::StompCreate( vecStart, trace.endpos, 0, this ); + UTIL_ScreenShake( GetAbsOrigin(), 12.0, 100.0, 2.0, 1000, SHAKE_START ); + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Garg.StompSound" ); + + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() - Vector(0,0,20), MASK_SOLID, this, COLLISION_GROUP_NONE, &trace ); + if ( trace.fraction < 1.0 ) + { + UTIL_DecalTrace( &trace, "SmallScorch" ); + } +} + +void CNPC_Gargantua::DeathEffect( void ) +{ + int i; + + Vector vForward; + AngleVectors( GetAbsAngles(), &vForward ); + Vector deathPos = GetAbsOrigin() + vForward * 100; + + Vector position = GetAbsOrigin(); + position.z += 32; + + CPASFilter filter( GetAbsOrigin() ); + for ( i = 0; i < 7; i++) + { + te->Explosion( filter, i * 0.2, &GetAbsOrigin(), g_sModelIndexFireball, 10, 15, TE_EXPLFLAG_NONE, 100, 0 ); + position.z += 15; + } + + UTIL_Smoke(GetAbsOrigin(),random->RandomInt(10, 15), 10); + UTIL_ScreenShake( GetAbsOrigin(), 25.0, 100.0, 5.0, 1000, SHAKE_START ); + +} + +void CNPC_Gargantua::Event_Killed( const CTakeDamageInfo &info ) +{ + EyeOff(); + UTIL_Remove( m_pEyeGlow ); + m_pEyeGlow = NULL; + BaseClass::Event_Killed( info ); + m_takedamage = DAMAGE_NO; +} + +void CNPC_Gargantua::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + CTakeDamageInfo subInfo = info; + + if ( !IsAlive() ) + { + BaseClass::TraceAttack( subInfo, vecDir, ptr, pAccumulator ); + return; + } + + // UNDONE: Hit group specific damage? + if ( subInfo.GetDamageType() & ( GARG_DAMAGE | DMG_BLAST ) ) + { + if ( m_painSoundTime < gpGlobals->curtime ) + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Garg.Pain" ); + + m_painSoundTime = gpGlobals->curtime + random->RandomFloat( 2.5, 4 ); + } + } + + int bitsDamageType = subInfo.GetDamageType(); + + bitsDamageType &= GARG_DAMAGE; + + subInfo.SetDamageType( bitsDamageType ); + + if ( subInfo.GetDamageType() == 0 ) + { + if ( m_flDmgTime != gpGlobals->curtime || (random->RandomInt( 0, 100 ) < 20) ) + { + g_pEffects->Ricochet(ptr->endpos, -vecDir ); + m_flDmgTime = gpGlobals->curtime; + } + + subInfo.SetDamage( 0 ); + } + + BaseClass::TraceAttack( subInfo, vecDir, ptr, pAccumulator ); +} + +int CNPC_Gargantua::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + if( GetState() == NPC_STATE_SCRIPT ) + { + // Invulnerable while scripted. This fixes the problem where garg wouldn't + // explode in C2A1 because for some reason he wouldn't die while scripted, he'd + // only freeze in place. Now he's just immune until he gets to the script and stops. + return 0; + } + + CTakeDamageInfo subInfo = info; + + float flDamage = subInfo.GetDamage(); + + if ( IsAlive() ) + { + if ( !(subInfo.GetDamageType() & GARG_DAMAGE) ) + { + flDamage *= 0.01; + subInfo.SetDamage( flDamage ); + } + if ( subInfo.GetDamageType() & DMG_BLAST ) + { + SetCondition( COND_LIGHT_DAMAGE ); + } + } + + return BaseClass::OnTakeDamage_Alive( subInfo ); +} + +AI_BEGIN_CUSTOM_NPC( monster_gargantua, CNPC_Gargantua ) + +DECLARE_TASK ( TASK_SOUND_ATTACK ) +DECLARE_TASK ( TASK_FLAME_SWEEP ) + + //========================================================= + // > SCHED_GARG_FLAME + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GARG_FLAME, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_SOUND_ATTACK 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_MELEE_ATTACK2" + " TASK_FLAME_SWEEP 4.5" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + ) + + //========================================================= + // > SCHED_GARG_SWIPE + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GARG_SWIPE, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_MELEE_ATTACK1 0" + " " + " Interrupts" + " COND_CAN_MELEE_ATTACK2" + ) + + DEFINE_SCHEDULE + ( + SCHED_GARG_CHASE_ENEMY, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_GARG_CHASE_ENEMY_FAILED" + " TASK_GET_CHASE_PATH_TO_ENEMY 300" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_FACE_ENEMY 0" + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_ENEMY_UNREACHABLE" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + " COND_CAN_MELEE_ATTACK2" + " COND_TOO_CLOSE_TO_ATTACK" + " COND_LOST_ENEMY" + ); + + DEFINE_SCHEDULE + ( + SCHED_GARG_CHASE_ENEMY_FAILED, + + " Tasks" + " TASK_SET_ROUTE_SEARCH_TIME 2" // Spend 2 seconds trying to build a path if stuck + " TASK_GET_PATH_TO_RANDOM_NODE 180" + " TASK_WALK_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + " COND_CAN_MELEE_ATTACK2" + ); + +AI_END_CUSTOM_NPC() diff --git a/game/server/hl1/hl1_npc_gargantua.h b/game/server/hl1/hl1_npc_gargantua.h new file mode 100644 index 0000000..f86484e --- /dev/null +++ b/game/server/hl1/hl1_npc_gargantua.h @@ -0,0 +1,86 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef NPC_GARGANTUA_H +#define NPC_GARGANTUA_H + +#include "hl1_ai_basenpc.h" + +class CNPC_Gargantua : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_Gargantua, CHL1BaseNPC ); +public: + + void Spawn( void ); + void Precache( void ); + Class_T Classify ( void ); + + float MaxYawSpeed ( void ); + + int MeleeAttack1Conditions( float flDot, float flDist ); + int MeleeAttack2Conditions( float flDot, float flDist ); + int RangeAttack1Conditions( float flDot, float flDist ); + + void HandleAnimEvent( animevent_t *pEvent ); + int TranslateSchedule( int scheduleType ); + + void StartTask( const Task_t *pTask ); + void RunTask ( const Task_t *pTask ); + + bool CanBecomeRagdoll() { return false; } + + void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + +/* int TakeDamage( entvars_t *pevInflictor, entvars_t *pevAttacker, float flDamage, int bitsDamageType ); + void TraceAttack( entvars_t *pevAttacker, float flDamage, Vector vecDir, TraceResult *ptr, int bitsDamageType ); + + Schedule_t *GetScheduleOfType( int Type ); + void StartTask( Task_t *pTask ); + void RunTask( Task_t *pTask ); +*/ + void PrescheduleThink( void ); + void Event_Killed( const CTakeDamageInfo &info ); + void DeathEffect( void ); + + bool ShouldGib( const CTakeDamageInfo &info ); + + void EyeOff( void ); + void EyeOn( int level ); + void EyeUpdate( void ); +// void Leap( void ); + void StompAttack( void ); + void FlameCreate( void ); + void FlameUpdate( void ); + void FlameControls( float angleX, float angleY ); + void FlameDestroy( void ); + inline BOOL FlameIsOn( void ) { return m_pFlame[0] != NULL; } + + void FlameDamage( Vector vecStart, Vector vecEnd, CBaseEntity *pevInflictor, CBaseEntity *pevAttacker, float flDamage, int iClassIgnore, int bitsDamageType ); + + + DEFINE_CUSTOM_AI; + DECLARE_DATADESC(); + +private: + CBaseEntity* GargantuaCheckTraceHullAttack(float flDist, int iDamage, int iDmgType); + + CSprite *m_pEyeGlow; // Glow around the eyes + CBeam *m_pFlame[4]; // Flame beams + + int m_eyeBrightness; // Brightness target + float m_seeTime; // Time to attack (when I see the enemy, I set this) + float m_flameTime; // Time of next flame attack + float m_painSoundTime; // Time of next pain sound + float m_streakTime; // streak timer (don't send too many) + float m_flameX; // Flame thrower aim + float m_flameY; + + float m_flDmgTime; +}; + +#endif //NPC_GARGANTUA_H
\ No newline at end of file diff --git a/game/server/hl1/hl1_npc_gman.cpp b/game/server/hl1/hl1_npc_gman.cpp new file mode 100644 index 0000000..5ea95cd --- /dev/null +++ b/game/server/hl1/hl1_npc_gman.cpp @@ -0,0 +1,232 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +//========================================================= +// GMan - misunderstood servant of the people +//========================================================= + +#include "cbase.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "ai_baseactor.h" + +//========================================================= +// Monster's Anim Events Go Here +//========================================================= + +class CNPC_GMan : public CAI_BaseActor +{ + DECLARE_CLASS( CNPC_GMan, CAI_BaseActor ); +public: + + void Spawn( void ); + void Precache( void ); + float MaxYawSpeed( void ){ return 90.0f; } + Class_T Classify ( void ); + void HandleAnimEvent( animevent_t *pEvent ); + int GetSoundInterests ( void ); + + bool IsInC5A1(); + + void StartTask( const Task_t *pTask ); + void RunTask( const Task_t *pTask ); + int OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ); + void TraceAttack( CBaseEntity *pAttacker, float flDamage, const Vector &vecDir, trace_t *ptr, int bitsDamageType); + + virtual int PlayScriptedSentence( const char *pszSentence, float duration, float volume, soundlevel_t soundlevel, bool bConcurrent, CBaseEntity *pListener ); + + EHANDLE m_hPlayer; + EHANDLE m_hTalkTarget; + float m_flTalkTime; +}; + +LINK_ENTITY_TO_CLASS( monster_gman, CNPC_GMan ); + +//========================================================= +// Hack that tells us whether the GMan is in the final map +//========================================================= +bool CNPC_GMan::IsInC5A1() +{ + const char *pMapName = STRING(gpGlobals->mapname); + + if( pMapName ) + { + return !Q_strnicmp( pMapName, "c5a1", 4 ); + } + + return false; +} + +//========================================================= +// Classify - indicates this monster's place in the +// relationship table. +//========================================================= +Class_T CNPC_GMan::Classify ( void ) +{ + return CLASS_NONE; +} + + +//========================================================= +// HandleAnimEvent - catches the monster-specific messages +// that occur when tagged animation frames are played. +//========================================================= +void CNPC_GMan::HandleAnimEvent( animevent_t *pEvent ) +{ + switch( pEvent->event ) + { + case 1: + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } +} + +//========================================================= +// GetSoundInterests - generic monster can't hear. +//========================================================= +int CNPC_GMan::GetSoundInterests ( void ) +{ + return NULL; +} + +//========================================================= +// Spawn +//========================================================= +void CNPC_GMan::Spawn() +{ + Precache(); + + BaseClass::Spawn(); + + SetModel( "models/gman.mdl" ); + + SetHullType(HULL_HUMAN); + SetHullSizeNormal(); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + SetBloodColor( BLOOD_COLOR_MECH ); + m_iHealth = 8; + m_flFieldOfView = 0.5;// indicates the width of this NPC's forward view cone ( as a dotproduct result ) + m_NPCState = NPC_STATE_NONE; + + CapabilitiesAdd( bits_CAP_MOVE_GROUND | bits_CAP_OPEN_DOORS | bits_CAP_USE_WEAPONS | bits_CAP_ANIMATEDFACE | bits_CAP_TURN_HEAD); + + NPCInit(); +} + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CNPC_GMan::Precache() +{ + PrecacheModel( "models/gman.mdl" ); +} + + +//========================================================= +// AI Schedules Specific to this monster +//========================================================= + + +void CNPC_GMan::StartTask( const Task_t *pTask ) +{ + switch( pTask->iTask ) + { + case TASK_WAIT: + if (m_hPlayer == NULL) + { + m_hPlayer = gEntList.FindEntityByClassname( NULL, "player" ); + } + break; + } + + BaseClass::StartTask( pTask ); +} + +void CNPC_GMan::RunTask( const Task_t *pTask ) +{ + switch( pTask->iTask ) + { + case TASK_WAIT: + // look at who I'm talking to + if (m_flTalkTime > gpGlobals->curtime && m_hTalkTarget != NULL) + { + AddLookTarget( m_hTalkTarget->GetAbsOrigin(), 1.0, 2.0 ); + } + // look at player, but only if playing a "safe" idle animation + else if (m_hPlayer != NULL && (GetSequence() == 0 || IsInC5A1()) ) + { + AddLookTarget( m_hPlayer->EyePosition(), 1.0, 3.0 ); + } + else + { + // Just center the head forward. + Vector forward; + GetVectors( &forward, NULL, NULL ); + + AddLookTarget( GetAbsOrigin() + forward * 12.0f, 1.0, 1.0 ); + SetBoneController( 0, 0 ); + } + BaseClass::RunTask( pTask ); + break; + } + + SetBoneController( 0, 0 ); + BaseClass::RunTask( pTask ); +} + + +//========================================================= +// Override all damage +//========================================================= +int CNPC_GMan::OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ) +{ + m_iHealth = m_iMaxHealth / 2; // always trigger the 50% damage aitrigger + + if ( inputInfo.GetDamage() > 0 ) + SetCondition( COND_LIGHT_DAMAGE ); + + if ( inputInfo.GetDamage() >= 20 ) + SetCondition( COND_HEAVY_DAMAGE ); + + return TRUE; +} + + +void CNPC_GMan::TraceAttack( CBaseEntity *pAttacker, float flDamage, const Vector &vecDir, trace_t *ptr, int bitsDamageType) +{ + g_pEffects->Ricochet( ptr->endpos, ptr->plane.normal ); +// AddMultiDamage( pevAttacker, this, flDamage, bitsDamageType ); +} + +int CNPC_GMan::PlayScriptedSentence( const char *pszSentence, float delay, float volume, soundlevel_t soundlevel, bool bConcurrent, CBaseEntity *pListener ) +{ + BaseClass::PlayScriptedSentence( pszSentence, delay, volume, soundlevel, bConcurrent, pListener ); + + m_flTalkTime = gpGlobals->curtime + delay; + m_hTalkTarget = pListener; + + return 1; +} diff --git a/game/server/hl1/hl1_npc_hassassin.cpp b/game/server/hl1/hl1_npc_hassassin.cpp new file mode 100644 index 0000000..fa978fe --- /dev/null +++ b/game/server/hl1/hl1_npc_hassassin.cpp @@ -0,0 +1,951 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "basecombatweapon.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "util.h" +#include "hl1_ai_basenpc.h" +#include "hl1_basegrenade.h" +#include "movevars_shared.h" +#include "ai_basenpc.h" + + +ConVar sk_hassassin_health( "sk_hassassin_health", "50" ); + +//========================================================= +// monster-specific schedule types +//========================================================= +enum +{ + SCHED_ASSASSIN_EXPOSED = LAST_SHARED_SCHEDULE,// cover was blown. + SCHED_ASSASSIN_JUMP, // fly through the air + SCHED_ASSASSIN_JUMP_ATTACK, // fly through the air and shoot + SCHED_ASSASSIN_JUMP_LAND, // hit and run away + SCHED_ASSASSIN_FAIL, + SCHED_ASSASSIN_TAKE_COVER_FROM_ENEMY1, + SCHED_ASSASSIN_TAKE_COVER_FROM_ENEMY2, + SCHED_ASSASSIN_TAKE_COVER_FROM_BEST_SOUND, + SCHED_ASSASSIN_HIDE, + SCHED_ASSASSIN_HUNT, +}; + +Activity ACT_ASSASSIN_FLY_UP; +Activity ACT_ASSASSIN_FLY_ATTACK; +Activity ACT_ASSASSIN_FLY_DOWN; + +//========================================================= +// monster-specific tasks +//========================================================= + +enum +{ + TASK_ASSASSIN_FALL_TO_GROUND = LAST_SHARED_TASK + 1, // falling and waiting to hit ground +}; + + +//========================================================= +// Monster's Anim Events Go Here +//========================================================= +#define ASSASSIN_AE_SHOOT1 1 +#define ASSASSIN_AE_TOSS1 2 +#define ASSASSIN_AE_JUMP 3 + + +#define MEMORY_BADJUMP bits_MEMORY_CUSTOM1 + +class CNPC_HAssassin : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_HAssassin, CHL1BaseNPC ); + +public: + void Spawn( void ); + void Precache( void ); + + int TranslateSchedule( int scheduleType ); + + void HandleAnimEvent( animevent_t *pEvent ); + float MaxYawSpeed() { return 360.0f; } + + void Shoot ( void ); + + int MeleeAttack1Conditions ( float flDot, float flDist ); + int RangeAttack1Conditions ( float flDot, float flDist ); + int RangeAttack2Conditions ( float flDot, float flDist ); + + int SelectSchedule ( void ); + + void RunTask ( const Task_t *pTask ); + void StartTask ( const Task_t *pTask ); + + Class_T Classify ( void ); + + int GetSoundInterests( void ); + + void RunAI( void ); + + float m_flLastShot; + float m_flDiviation; + + float m_flNextJump; + Vector m_vecJumpVelocity; + + float m_flNextGrenadeCheck; + Vector m_vecTossVelocity; + bool m_fThrowGrenade; + + int m_iTargetRanderamt; + + int m_iFrustration; + + int m_iAmmoType; + +public: + DECLARE_DATADESC(); + DEFINE_CUSTOM_AI; + +}; + +LINK_ENTITY_TO_CLASS( monster_human_assassin, CNPC_HAssassin ); + +BEGIN_DATADESC( CNPC_HAssassin ) + DEFINE_FIELD( m_flLastShot, FIELD_TIME ), + DEFINE_FIELD( m_flDiviation, FIELD_FLOAT ), + + DEFINE_FIELD( m_flNextJump, FIELD_TIME ), + DEFINE_FIELD( m_vecJumpVelocity, FIELD_VECTOR ), + + DEFINE_FIELD( m_flNextGrenadeCheck, FIELD_TIME ), + DEFINE_FIELD( m_vecTossVelocity, FIELD_VECTOR ), + DEFINE_FIELD( m_fThrowGrenade, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_iTargetRanderamt, FIELD_INTEGER ), + DEFINE_FIELD( m_iFrustration, FIELD_INTEGER ), + + //DEFINE_FIELD( m_iAmmoType, FIELD_INTEGER ), + +END_DATADESC() + +//========================================================= +// Spawn +//========================================================= +void CNPC_HAssassin::Spawn() +{ + Precache( ); + + SetModel( "models/hassassin.mdl"); + + SetHullType(HULL_HUMAN); + SetHullSizeNormal(); + + + SetNavType ( NAV_GROUND ); + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + m_bloodColor = BLOOD_COLOR_RED; + ClearEffects(); + m_iHealth = sk_hassassin_health.GetFloat(); + m_flFieldOfView = VIEW_FIELD_WIDE; // indicates the width of this monster's forward view cone ( as a dotproduct result ) + m_NPCState = NPC_STATE_NONE; + + m_HackedGunPos = Vector( 0, 24, 48 ); + + m_iTargetRanderamt = 20; + SetRenderColor( 255, 255, 255, 20 ); + m_nRenderMode = kRenderTransTexture; + + CapabilitiesClear(); + CapabilitiesAdd( bits_CAP_MOVE_GROUND ); + CapabilitiesAdd( bits_CAP_INNATE_RANGE_ATTACK1 | bits_CAP_INNATE_RANGE_ATTACK2 | bits_CAP_INNATE_MELEE_ATTACK1 ); + + NPCInit(); +} + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CNPC_HAssassin::Precache() +{ + m_iAmmoType = GetAmmoDef()->Index("9mmRound"); + + PrecacheModel("models/hassassin.mdl"); + + UTIL_PrecacheOther( "npc_handgrenade" ); + + PrecacheScriptSound( "HAssassin.Shot" ); + PrecacheScriptSound( "HAssassin.Beamsound" ); + PrecacheScriptSound( "HAssassin.Footstep" ); +} + +int CNPC_HAssassin::GetSoundInterests( void ) +{ + return SOUND_WORLD | + SOUND_COMBAT | + SOUND_PLAYER | + SOUND_DANGER; +} + +Class_T CNPC_HAssassin::Classify ( void ) +{ + return CLASS_HUMAN_MILITARY; +} + +//========================================================= +// CheckMeleeAttack1 - jump like crazy if the enemy gets too close. +//========================================================= +int CNPC_HAssassin::MeleeAttack1Conditions ( float flDot, float flDist ) +{ + if ( m_flNextJump < gpGlobals->curtime && ( flDist <= 128 || HasMemory( MEMORY_BADJUMP )) && GetEnemy() != NULL ) + { + trace_t tr; + + Vector vecMin = Vector( random->RandomFloat( 0, -64), random->RandomFloat( 0, -64 ), 0 ); + Vector vecMax = Vector( random->RandomFloat( 0, 64), random->RandomFloat( 0, 64 ), 160 ); + + Vector vecDest = GetAbsOrigin() + Vector( random->RandomFloat( -64, 64), random->RandomFloat( -64, 64 ), 160 ); + + UTIL_TraceHull( GetAbsOrigin() + Vector( 0, 0, 36 ), GetAbsOrigin() + Vector( 0, 0, 36 ), vecMin, vecMax, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); + + //NDebugOverlay::Box( GetAbsOrigin() + Vector( 0, 0, 36 ), vecMin, vecMax, 0,0, 255, 0, 2.0 ); + + if ( tr.startsolid || tr.fraction < 1.0) + { + return COND_TOO_CLOSE_TO_ATTACK; + } + + float flGravity = GetCurrentGravity(); + + float time = sqrt( 160 / (0.5 * flGravity)); + float speed = flGravity * time / 160; + m_vecJumpVelocity = ( vecDest - GetAbsOrigin() ) * speed; + + return COND_CAN_MELEE_ATTACK1; + } + + if ( flDist > 128 ) + return COND_TOO_FAR_TO_ATTACK; + + return COND_NONE; +} + +//========================================================= +// CheckRangeAttack1 - drop a cap in their ass +// +//========================================================= +int CNPC_HAssassin::RangeAttack1Conditions ( float flDot, float flDist ) +{ + if ( !HasCondition( COND_ENEMY_OCCLUDED ) && flDist > 64 && flDist <= 2048 ) + { + trace_t tr; + + Vector vecSrc = GetAbsOrigin() + m_HackedGunPos; + + // verify that a bullet fired from the gun will hit the enemy before the world. + UTIL_TraceLine( vecSrc, GetEnemy()->BodyTarget(vecSrc), MASK_SOLID, this, COLLISION_GROUP_NONE, &tr); + + if ( tr.fraction == 1.0 || tr.m_pEnt == GetEnemy() ) + { + return COND_CAN_RANGE_ATTACK1; + } + } + + return COND_NONE; +} + +//========================================================= +// CheckRangeAttack2 - toss grenade is enemy gets in the way and is too close. +//========================================================= +int CNPC_HAssassin::RangeAttack2Conditions ( float flDot, float flDist ) +{ + m_fThrowGrenade = false; + if ( !FBitSet ( GetEnemy()->GetFlags(), FL_ONGROUND ) ) + { + // don't throw grenades at anything that isn't on the ground! + return COND_NONE; + } + + // don't get grenade happy unless the player starts to piss you off + if ( m_iFrustration <= 2) + return COND_NONE; + + if ( m_flNextGrenadeCheck < gpGlobals->curtime && !HasCondition( COND_ENEMY_OCCLUDED ) && flDist <= 512 ) + { + Vector vTossPos; + QAngle vAngles; + + GetAttachment( "grenadehand", vTossPos, vAngles ); + + Vector vecToss = VecCheckThrow( this, vTossPos, GetEnemy()->WorldSpaceCenter(), flDist, 0.5 ); // use dist as speed to get there in 1 second + + if ( vecToss != vec3_origin ) + { + m_vecTossVelocity = vecToss; + + // throw a hand grenade + m_fThrowGrenade = TRUE; + + return COND_CAN_RANGE_ATTACK2; + } + } + + return COND_NONE; +} + +//========================================================= +// StartTask +//========================================================= +void CNPC_HAssassin::StartTask ( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_RANGE_ATTACK2: + if (!m_fThrowGrenade) + { + TaskComplete( ); + } + else + { + BaseClass::StartTask ( pTask ); + } + break; + case TASK_ASSASSIN_FALL_TO_GROUND: + m_flWaitFinished = gpGlobals->curtime + 2.0f; + break; + default: + BaseClass::StartTask ( pTask ); + break; + } +} + + +//========================================================= +// RunTask +//========================================================= +void CNPC_HAssassin::RunTask ( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_ASSASSIN_FALL_TO_GROUND: + GetMotor()->SetIdealYawAndUpdate( GetEnemyLKP() ); + + if ( IsSequenceFinished() ) + { + if ( GetAbsVelocity().z > 0) + { + SetActivity( ACT_ASSASSIN_FLY_UP ); + } + else if ( HasCondition ( COND_SEE_ENEMY )) + { + SetActivity( ACT_ASSASSIN_FLY_ATTACK ); + SetCycle( 0 ); + } + else + { + SetActivity( ACT_ASSASSIN_FLY_DOWN ); + SetCycle( 0 ); + } + + ResetSequenceInfo( ); + } + + if ( GetFlags() & FL_ONGROUND) + { + TaskComplete( ); + } + else if( gpGlobals->curtime > m_flWaitFinished || GetAbsVelocity().z == 0.0 ) + { + // I've waited two seconds and haven't hit the ground. Try to force it. + trace_t trace; + UTIL_TraceEntity( this, GetAbsOrigin(), GetAbsOrigin() - Vector( 0, 0, 1 ), MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &trace ); + + if( trace.DidHitWorld() ) + { + SetGroundEntity( trace.m_pEnt ); + } + else + { + // Try again in a couple of seconds. + m_flWaitFinished = gpGlobals->curtime + 2.0f; + } + } + break; + default: + BaseClass::RunTask ( pTask ); + break; + } +} + +//========================================================= +// GetSchedule - Decides which type of schedule best suits +// the monster's current state and conditions. Then calls +// monster's member function to get a pointer to a schedule +// of the proper type. +//========================================================= +int CNPC_HAssassin::SelectSchedule ( void ) +{ + switch ( m_NPCState ) + { + case NPC_STATE_IDLE: + case NPC_STATE_ALERT: + { + if ( HasCondition ( COND_HEAR_DANGER ) || HasCondition ( COND_HEAR_COMBAT ) ) + { + if ( HasCondition ( COND_HEAR_DANGER ) ) + return SCHED_TAKE_COVER_FROM_BEST_SOUND; + + else + return SCHED_INVESTIGATE_SOUND; + } + } + break; + + case NPC_STATE_COMBAT: + { + // dead enemy + if ( HasCondition( COND_ENEMY_DEAD ) ) + { + // call base class, all code to handle dead enemies is centralized there. + return BaseClass::SelectSchedule(); + } + + // flying? + if ( GetMoveType() == MOVETYPE_FLYGRAVITY ) + { + if ( GetFlags() & FL_ONGROUND ) + { + //Msg( "landed\n" ); + // just landed + SetMoveType( MOVETYPE_STEP ); + return SCHED_ASSASSIN_JUMP_LAND; + } + else + { + //Msg("jump\n"); + // jump or jump/shoot + if ( m_NPCState == NPC_STATE_COMBAT ) + return SCHED_ASSASSIN_JUMP; + else + return SCHED_ASSASSIN_JUMP_ATTACK; + } + } + + if ( HasCondition ( COND_HEAR_DANGER ) ) + { + return SCHED_TAKE_COVER_FROM_BEST_SOUND; + } + + if ( HasCondition ( COND_LIGHT_DAMAGE ) ) + { + m_iFrustration++; + } + if ( HasCondition ( COND_HEAVY_DAMAGE ) ) + { + m_iFrustration++; + } + + // jump player! + if ( HasCondition ( COND_CAN_MELEE_ATTACK1 ) ) + { + //Msg( "melee attack 1\n"); + return SCHED_MELEE_ATTACK1; + } + + // throw grenade + if ( HasCondition ( COND_CAN_RANGE_ATTACK2 ) ) + { + //Msg( "range attack 2\n"); + return SCHED_RANGE_ATTACK2; + } + + // spotted + if ( HasCondition ( COND_SEE_ENEMY ) && HasCondition ( COND_ENEMY_FACING_ME ) ) + { + //Msg("exposed\n"); + m_iFrustration++; + return SCHED_ASSASSIN_EXPOSED; + } + + // can attack + if ( HasCondition ( COND_CAN_RANGE_ATTACK1 ) ) + { + //Msg( "range attack 1\n" ); + m_iFrustration = 0; + return SCHED_RANGE_ATTACK1; + } + + if ( HasCondition ( COND_SEE_ENEMY ) ) + { + //Msg( "face\n"); + return SCHED_COMBAT_FACE; + } + + // new enemy + if ( HasCondition ( COND_NEW_ENEMY ) ) + { + //Msg( "take cover\n"); + return SCHED_TAKE_COVER_FROM_ENEMY; + } + + // ALERT( at_console, "stand\n"); + return SCHED_ALERT_STAND; + } + break; + } + + return BaseClass::SelectSchedule(); +} + +//========================================================= +// HandleAnimEvent - catches the monster-specific messages +// that occur when tagged animation frames are played. +// +// Returns number of events handled, 0 if none. +//========================================================= +void CNPC_HAssassin::HandleAnimEvent( animevent_t *pEvent ) +{ + switch( pEvent->event ) + { + case ASSASSIN_AE_SHOOT1: + Shoot( ); + break; + case ASSASSIN_AE_TOSS1: + { + Vector vTossPos; + QAngle vAngles; + + GetAttachment( "grenadehand", vTossPos, vAngles ); + + CHandGrenade *pGrenade = (CHandGrenade*)Create( "grenade_hand", vTossPos, vec3_angle ); + if ( pGrenade ) + { + pGrenade->ShootTimed( this, m_vecTossVelocity, 2.0 ); + } + + m_flNextGrenadeCheck = gpGlobals->curtime + 6;// wait six seconds before even looking again to see if a grenade can be thrown. + m_fThrowGrenade = FALSE; + // !!!LATER - when in a group, only try to throw grenade if ordered. + } + break; + case ASSASSIN_AE_JUMP: + { + SetMoveType( MOVETYPE_FLYGRAVITY ); + SetGroundEntity( NULL ); + SetAbsVelocity( m_vecJumpVelocity ); + m_flNextJump = gpGlobals->curtime + 3.0; + } + return; + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } +} + + + +//========================================================= +// Shoot +//========================================================= +void CNPC_HAssassin::Shoot ( void ) +{ + Vector vForward, vRight, vUp; + Vector vecShootOrigin; + QAngle vAngles; + + if ( GetEnemy() == NULL) + { + return; + } + + GetAttachment( "guntip", vecShootOrigin, vAngles ); + + Vector vecShootDir = GetShootEnemyDir( vecShootOrigin ); + + if (m_flLastShot + 2 < gpGlobals->curtime) + { + m_flDiviation = 0.10; + } + else + { + m_flDiviation -= 0.01; + if (m_flDiviation < 0.02) + m_flDiviation = 0.02; + } + m_flLastShot = gpGlobals->curtime; + + AngleVectors( GetAbsAngles(), &vForward, &vRight, &vUp ); + + Vector vecShellVelocity = vRight * random->RandomFloat(40,90) + vUp * random->RandomFloat(75,200) + vForward * random->RandomFloat(-40, 40); + EjectShell( GetAbsOrigin() + vUp * 32 + vForward * 12, vecShellVelocity, GetAbsAngles().y, 0 ); + FireBullets( 1, vecShootOrigin, vecShootDir, Vector( m_flDiviation, m_flDiviation, m_flDiviation ), 2048, m_iAmmoType ); // shoot +-8 degrees + + //NDebugOverlay::Line( vecShootOrigin, vecShootOrigin + vecShootDir * 2048, 255, 0, 0, true, 2.0 ); + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "HAssassin.Shot" ); + + DoMuzzleFlash(); + + VectorAngles( vecShootDir, vAngles ); + SetPoseParameter( "shoot", vecShootDir.x ); + + m_cAmmoLoaded--; +} + +//========================================================= +//========================================================= +int CNPC_HAssassin::TranslateSchedule ( int scheduleType ) +{ +// Msg( "%d\n", m_iFrustration ); + switch ( scheduleType ) + { + case SCHED_TAKE_COVER_FROM_ENEMY: + + if ( m_iHealth > 30 ) + return SCHED_ASSASSIN_TAKE_COVER_FROM_ENEMY1; + else + return SCHED_ASSASSIN_TAKE_COVER_FROM_ENEMY2; + + case SCHED_TAKE_COVER_FROM_BEST_SOUND: + return SCHED_ASSASSIN_TAKE_COVER_FROM_BEST_SOUND; + case SCHED_FAIL: + + if ( m_NPCState == NPC_STATE_COMBAT ) + return SCHED_ASSASSIN_FAIL; + + break; + case SCHED_ALERT_STAND: + + if ( m_NPCState == NPC_STATE_COMBAT ) + return SCHED_ASSASSIN_HIDE; + + break; + //case SCHED_CHASE_ENEMY: + // return SCHED_ASSASSIN_HUNT; + + case SCHED_MELEE_ATTACK1: + + if ( GetFlags() & FL_ONGROUND) + { + if (m_flNextJump > gpGlobals->curtime) + { + // can't jump yet, go ahead and fail + return SCHED_ASSASSIN_FAIL; + } + else + { + return SCHED_ASSASSIN_JUMP; + } + } + else + { + return SCHED_ASSASSIN_JUMP_ATTACK; + } + } + + return BaseClass::TranslateSchedule( scheduleType ); +} + +//========================================================= +// RunAI +//========================================================= +void CNPC_HAssassin::RunAI( void ) +{ + BaseClass::RunAI(); + + // always visible if moving + // always visible is not on hard + if (g_iSkillLevel != SKILL_HARD || GetEnemy() == NULL || m_lifeState == LIFE_DEAD || GetActivity() == ACT_RUN || GetActivity() == ACT_WALK || !(GetFlags() & FL_ONGROUND)) + m_iTargetRanderamt = 255; + else + m_iTargetRanderamt = 20; + + CPASAttenuationFilter filter( this ); + + if ( GetRenderColor().a > m_iTargetRanderamt) + { + if ( GetRenderColor().a == 255) + { + EmitSound( filter, entindex(), "HAssassin.Beamsound" ); + } + + SetRenderColorA( MAX( GetRenderColor().a - 50, m_iTargetRanderamt ) ); + m_nRenderMode = kRenderTransTexture; + } + else if ( GetRenderColor().a < m_iTargetRanderamt) + { + SetRenderColorA ( MIN( GetRenderColor().a + 50, m_iTargetRanderamt ) ); + if (GetRenderColor().a == 255) + m_nRenderMode = kRenderNormal; + } + + if ( GetActivity() == ACT_RUN || GetActivity() == ACT_WALK) + { + static int iStep = 0; + iStep = ! iStep; + if (iStep) + { + EmitSound( filter, entindex(), "HAssassin.Footstep" ); + } + } +} + +AI_BEGIN_CUSTOM_NPC( monster_human_assassin, CNPC_HAssassin ) + + DECLARE_TASK( TASK_ASSASSIN_FALL_TO_GROUND ) + + DECLARE_ACTIVITY( ACT_ASSASSIN_FLY_UP ) + DECLARE_ACTIVITY( ACT_ASSASSIN_FLY_ATTACK ) + DECLARE_ACTIVITY( ACT_ASSASSIN_FLY_DOWN ) + + //========================================================= + // AI Schedules Specific to this monster + //========================================================= + + //========================================================= + // Enemy exposed assasin's cover + //========================================================= + + //========================================================= + // > SCHED_ASSASSIN_EXPOSED + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_ASSASSIN_EXPOSED, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_RANGE_ATTACK1 0" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ASSASSIN_JUMP" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_TAKE_COVER_FROM_ENEMY" + " " + " Interrupts" + " COND_CAN_MELEE_ATTACK1" + + ) + + //========================================================= + // > SCHED_ASSASSIN_JUMP + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_ASSASSIN_JUMP, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_HOP" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_ASSASSIN_JUMP_ATTACK" + " " + " Interrupts" + ) + + //========================================================= + // > SCHED_ASSASSIN_JUMP_ATTACK + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_ASSASSIN_JUMP_ATTACK, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ASSASSIN_JUMP_LAND" + " TASK_ASSASSIN_FALL_TO_GROUND 0" + " " + " Interrupts" + ) + + //========================================================= + // > SCHED_ASSASSIN_JUMP_LAND + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_ASSASSIN_JUMP_LAND, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ASSASSIN_EXPOSED" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_REMEMBER MEMORY:CUSTOM1" + " TASK_FIND_NODE_COVER_FROM_ENEMY 0" + " TASK_RUN_PATH 0" + " TASK_FORGET MEMORY:CUSTOM1" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_REMEMBER MEMORY:INCOVER" + " TASK_FACE_ENEMY 0" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_RANGE_ATTACK1" + + " " + " Interrupts" + ) + + //========================================================= + // Fail Schedule + //========================================================= + + //========================================================= + // > SCHED_ASSASSIN_FAIL + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_ASSASSIN_FAIL, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT_FACE_ENEMY 2" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_CHASE_ENEMY" + " " + " Interrupts" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + " COND_CAN_MELEE_ATTACK1" + " COND_HEAR_DANGER" + " COND_HEAR_PLAYER" + ) + + + + + //========================================================= + // > SCHED_ASSASSIN_TAKE_COVER_FROM_ENEMY1 + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_ASSASSIN_TAKE_COVER_FROM_ENEMY1, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_WAIT 0.2" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_RANGE_ATTACK1" + " TASK_FIND_COVER_FROM_ENEMY 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_REMEMBER MEMORY:INCOVER" + " TASK_FACE_ENEMY 0" + " " + " Interrupts" + " COND_CAN_MELEE_ATTACK1" + " COND_NEW_ENEMY" + " COND_HEAR_DANGER" + + ) + + //========================================================= + // > SCHED_ASSASSIN_TAKE_COVER_FROM_ENEMY2 + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_ASSASSIN_TAKE_COVER_FROM_ENEMY2, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_WAIT 0.2" + " TASK_FACE_ENEMY 0" + " TASK_RANGE_ATTACK1 0" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_RANGE_ATTACK1" + " TASK_FIND_COVER_FROM_ENEMY 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_REMEMBER MEMORY:INCOVER" + " TASK_FACE_ENEMY 0" + " " + " Interrupts" + " COND_CAN_MELEE_ATTACK1" + " COND_NEW_ENEMY" + " COND_HEAR_DANGER" + + ) + + + + //========================================================= + // hide from the loudest sound source + //========================================================= + + //========================================================= + // > SCHED_ASSASSIN_TAKE_COVER_FROM_BEST_SOUND + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_ASSASSIN_TAKE_COVER_FROM_BEST_SOUND, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_MELEE_ATTACK1" + " TASK_STOP_MOVING 0" + " TASK_FIND_COVER_FROM_BEST_SOUND 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_REMEMBER MEMORY:INCOVER" + " TASK_TURN_LEFT 179" + " " + " Interrupts" + " COND_NEW_ENEMY" + + ) + + //========================================================= + // > SCHED_ASSASSIN_HIDE + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_ASSASSIN_HIDE, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT 2.0" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_CHASE_ENEMY" + + " Interrupts" + " COND_NEW_ENEMY" + " COND_SEE_ENEMY" + " COND_SEE_FEAR" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_PROVOKED" + " COND_HEAR_DANGER" + ) + + //========================================================= + // > SCHED_ASSASSIN_HUNT + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_ASSASSIN_HUNT, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ASSASSIN_TAKE_COVER_FROM_ENEMY2" + " TASK_GET_PATH_TO_ENEMY 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + + " Interrupts" + " COND_NEW_ENEMY" + " COND_CAN_RANGE_ATTACK1" + " COND_HEAR_DANGER" + ) + +AI_END_CUSTOM_NPC() diff --git a/game/server/hl1/hl1_npc_headcrab.cpp b/game/server/hl1/hl1_npc_headcrab.cpp new file mode 100644 index 0000000..ed41a93 --- /dev/null +++ b/game/server/hl1/hl1_npc_headcrab.cpp @@ -0,0 +1,698 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements the headcrab, a tiny, jumpy alien parasite. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "game.h" +#include "ai_default.h" +#include "ai_schedule.h" +#include "ai_hull.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "npcevent.h" +#include "hl1_npc_headcrab.h" +#include "gib.h" +//#include "AI_Interactions.h" +#include "ndebugoverlay.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "movevars_shared.h" +#include "SoundEmitterSystem/isoundemittersystembase.h" + +extern void ClearMultiDamage(void); +extern void ApplyMultiDamage( void ); + +ConVar sk_headcrab_health( "sk_headcrab_health","20"); +ConVar sk_headcrab_dmg_bite( "sk_headcrab_dmg_bite","10"); + +#define CRAB_ATTN_IDLE (float)1.5 +#define HEADCRAB_GUTS_GIB_COUNT 1 +#define HEADCRAB_LEGS_GIB_COUNT 3 +#define HEADCRAB_ALL_GIB_COUNT 5 + +#define HEADCRAB_MAX_JUMP_DIST 256 + +#define HEADCRAB_RUNMODE_ACCELERATE 1 +#define HEADCRAB_RUNMODE_IDLE 2 +#define HEADCRAB_RUNMODE_DECELERATE 3 +#define HEADCRAB_RUNMODE_FULLSPEED 4 +#define HEADCRAB_RUNMODE_PAUSE 5 + +#define HEADCRAB_RUN_MINSPEED 0.5 +#define HEADCRAB_RUN_MAXSPEED 1.0 + +#define HC_AE_JUMPATTACK ( 2 ) + +BEGIN_DATADESC( CNPC_Headcrab ) + // m_nGibCount - don't save + + // Function Pointers + DEFINE_ENTITYFUNC( LeapTouch ), + DEFINE_FIELD( m_vecJumpVel, FIELD_VECTOR ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( monster_headcrab, CNPC_Headcrab ); + + +enum +{ + SCHED_HEADCRAB_RANGE_ATTACK1 = LAST_SHARED_SCHEDULE, + SCHED_FAST_HEADCRAB_RANGE_ATTACK1, +}; + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +void CNPC_Headcrab::Spawn( void ) +{ + Precache(); + + SetRenderColor( 255, 255, 255, 255 ); + + SetModel( "models/headcrab.mdl" ); + m_iHealth = sk_headcrab_health.GetFloat(); + + SetHullType(HULL_TINY); + SetHullSizeNormal(); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + SetViewOffset( Vector(6, 0, 11) ); // Position of the eyes relative to NPC's origin. + + m_bloodColor = BLOOD_COLOR_GREEN; + m_flFieldOfView = 0.5; + m_NPCState = NPC_STATE_NONE; + m_nGibCount = HEADCRAB_ALL_GIB_COUNT; + + CapabilitiesClear(); + CapabilitiesAdd( bits_CAP_MOVE_GROUND | bits_CAP_INNATE_RANGE_ATTACK1 ); + + NPCInit(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +void CNPC_Headcrab::Precache( void ) +{ + PrecacheModel( "models/headcrab.mdl" ); +// PrecacheModel( "models/hc_squashed01.mdl" ); +// PrecacheModel( "models/gibs/hc_gibs.mdl" ); + + PrecacheScriptSound( "Headcrab.Bite" ); + PrecacheScriptSound( "Headcrab.Attack" ); + PrecacheScriptSound( "Headcrab.Idle" ); + PrecacheScriptSound( "Headcrab.Die" ); + PrecacheScriptSound( "Headcrab.Alert" ); + PrecacheScriptSound( "Headcrab.Pain" ); + + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Headcrab::IdleSound() +{ + HeadCrabSound( "Headcrab.Idle" ); +} + + +void CNPC_Headcrab::AlertSound() +{ + HeadCrabSound( "Headcrab.Alert" ); +} + + +void CNPC_Headcrab::PainSound( const CTakeDamageInfo &info ) +{ + HeadCrabSound( "Headcrab.Pain" ); +} + + +void CNPC_Headcrab::DeathSound( const CTakeDamageInfo &info ) +{ + HeadCrabSound( "Headcrab.Die" ); +} + +void CNPC_Headcrab::HeadCrabSound( const char *pchSound ) +{ + CPASAttenuationFilter filter( this, ATTN_IDLE ); + + CSoundParameters params; + if ( GetParametersForSound( pchSound, params, NULL ) ) + { + EmitSound_t ep( params ); + + ep.m_flVolume = GetSoundVolume(); + ep.m_nPitch = GetVoicePitch(); + + EmitSound( filter, entindex(), ep ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pTask - +//----------------------------------------------------------------------------- +void CNPC_Headcrab::StartTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_RANGE_ATTACK1: + { + SetIdealActivity( ACT_RANGE_ATTACK1 ); + SetTouch( &CNPC_Headcrab::LeapTouch ); + break; + } + + default: + { + BaseClass::StartTask( pTask ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pTask - +//----------------------------------------------------------------------------- +void CNPC_Headcrab::RunTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_RANGE_ATTACK1: + case TASK_RANGE_ATTACK2: + { + if ( IsSequenceFinished() ) + { + TaskComplete(); + SetTouch( NULL ); + SetIdealActivity( ACT_IDLE ); + } + break; + } + + default: + { + CAI_BaseNPC::RunTask( pTask ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : +//----------------------------------------------------------------------------- +int CNPC_Headcrab::SelectSchedule( void ) +{ + switch ( m_NPCState ) + { + case NPC_STATE_ALERT: + { + if (HasCondition( COND_LIGHT_DAMAGE ) || HasCondition( COND_HEAVY_DAMAGE )) + { + if ( fabs( GetMotor()->DeltaIdealYaw() ) < ( 1.0 - m_flFieldOfView) * 60 ) // roughly in the correct direction + { + return SCHED_TAKE_COVER_FROM_ORIGIN; + } + else if ( SelectWeightedSequence( ACT_SMALL_FLINCH ) != -1 ) + { + return SCHED_SMALL_FLINCH; + } + } + else if (HasCondition( COND_HEAR_DANGER ) || + HasCondition( COND_HEAR_PLAYER ) || + HasCondition( COND_HEAR_WORLD ) || + HasCondition( COND_HEAR_COMBAT )) + { + return SCHED_ALERT_FACE_BESTSOUND; + } + else + { + return SCHED_PATROL_WALK; + } + break; + } + } + + // no special cases here, call the base class + return BaseClass::SelectSchedule(); +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CNPC_Headcrab::Touch( CBaseEntity *pOther ) +{ + // If someone has smacked me into a wall then gib! +/* if (m_NPCState == NPC_STATE_DEAD) + { + if (GetAbsVelocity().Length() > 250) + { + trace_t tr; + Vector vecDir = GetAbsVelocity(); + VectorNormalize(vecDir); + UTIL_TraceLine(GetAbsOrigin(), GetAbsOrigin() + vecDir * 100, + MASK_SOLID_BRUSHONLY, pev, COLLISION_GROUP_NONE, &tr); + float dotPr = DotProduct(vecDir,tr.plane.normal); + if ((tr.fraction != 1.0) && + (dotPr < -0.8) ) + { + Event_Gibbed(); + // Throw headcrab guts + CGib::SpawnSpecificGibs( this, HEADCRAB_GUTS_GIB_COUNT, 300, 400, "models/gibs/hc_gibs.mdl"); + } + + } + }*/ + BaseClass::Touch(pOther); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pevInflictor - +// pevAttacker - +// flDamage - +// bitsDamageType - +// Output : +//----------------------------------------------------------------------------- +int CNPC_Headcrab::OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ) +{ + CTakeDamageInfo info = inputInfo; + + // + // Don't take any acid damage. + // + if ( info.GetDamageType() & DMG_ACID ) + { + return 0; + } + + return BaseClass::OnTakeDamage_Alive( info ); +} + +float CNPC_Headcrab::GetDamageAmount( void ) +{ + return sk_headcrab_dmg_bite.GetFloat(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : Type - +// Output : CAI_Schedule * +//----------------------------------------------------------------------------- +int CNPC_Headcrab::TranslateSchedule( int scheduleType ) +{ + switch( scheduleType ) + { + case SCHED_RANGE_ATTACK1: + return SCHED_HEADCRAB_RANGE_ATTACK1; + + case SCHED_FAIL_TAKE_COVER: + return SCHED_ALERT_FACE; + } + + return BaseClass::TranslateSchedule( scheduleType ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Headcrab::PrescheduleThink( void ) +{ + BaseClass::PrescheduleThink(); + + // + // Make the crab coo a little bit in combat state. + // + if (( m_NPCState == NPC_STATE_COMBAT ) && ( random->RandomFloat( 0, 5 ) < 0.1 )) + { + IdleSound(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: For innate melee attack +// Input : +// Output : +//----------------------------------------------------------------------------- +int CNPC_Headcrab::RangeAttack1Conditions ( float flDot, float flDist ) +{ + if ( gpGlobals->curtime < m_flNextAttack ) + { + return( 0 ); + } + + if ( !(GetFlags() & FL_ONGROUND) ) + { + return( 0 ); + } + + if ( flDist > 256 ) + { + return( COND_TOO_FAR_TO_ATTACK ); + } + else if ( flDot < 0.65 ) + { + return( COND_NOT_FACING_ATTACK ); + } + + return( COND_CAN_RANGE_ATTACK1 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Indicates this monster's place in the relationship table. +// Output : +//----------------------------------------------------------------------------- +Class_T CNPC_Headcrab::Classify( void ) +{ + return CLASS_ALIEN_PREY; +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns the real center of the monster. The bounding box is much larger +// than the actual creature so this is needed for targetting. +// Output : Vector +//----------------------------------------------------------------------------- +Vector CNPC_Headcrab::Center( void ) +{ + return Vector( GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z + 6 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &posSrc - +// Output : Vector +//----------------------------------------------------------------------------- +Vector CNPC_Headcrab::BodyTarget( const Vector &posSrc, bool bNoisy ) +{ + return( Center() ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +float CNPC_Headcrab::MaxYawSpeed ( void ) +{ + switch ( GetActivity() ) + { + case ACT_IDLE: + return 30; + break; + + case ACT_RUN: + case ACT_WALK: + return 20; + break; + + case ACT_TURN_LEFT: + case ACT_TURN_RIGHT: + return 15; + break; + + case ACT_RANGE_ATTACK1: + return 30; + break; + + default: + return 30; + break; + } + + return BaseClass::MaxYawSpeed(); +} + + +//----------------------------------------------------------------------------- +// Purpose: LeapTouch - this is the headcrab's touch function when it is in the air. +// Input : *pOther - +//----------------------------------------------------------------------------- +void CNPC_Headcrab::LeapTouch( CBaseEntity *pOther ) +{ + if ( pOther->Classify() == Classify() ) + { + return; + } + + // Don't hit if back on ground + if ( !(GetFlags() & FL_ONGROUND) && ( pOther->IsNPC() || pOther->IsPlayer() ) ) + { + BiteSound(); + TouchDamage( pOther ); + } + + SetTouch( NULL ); +} + +//----------------------------------------------------------------------------- +// Purpose: Make the sound of this headcrab chomping a target. +// Input : +//----------------------------------------------------------------------------- +void CNPC_Headcrab::BiteSound( void ) +{ + HeadCrabSound( "Headcrab.Bite" ); +} + +//----------------------------------------------------------------------------- +// Purpose: Deal the damage from the headcrab's touch attack. +//----------------------------------------------------------------------------- +void CNPC_Headcrab::TouchDamage( CBaseEntity *pOther ) +{ + CTakeDamageInfo info( this, this, GetDamageAmount(), DMG_SLASH ); + CalculateMeleeDamageForce( &info, GetAbsVelocity(), GetAbsOrigin() ); + pOther->TakeDamage( info ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Catches the monster-specific messages that occur when tagged +// animation frames are played. +// Input : *pEvent - +//----------------------------------------------------------------------------- +void CNPC_Headcrab::HandleAnimEvent( animevent_t *pEvent ) +{ + switch ( pEvent->event ) + { + case HC_AE_JUMPATTACK: + { + SetGroundEntity( NULL ); + + // + // Take him off ground so engine doesn't instantly reset FL_ONGROUND. + // + UTIL_SetOrigin( this, GetAbsOrigin() + Vector( 0 , 0 , 1 )); + + Vector vecJumpDir; + CBaseEntity *pEnemy = GetEnemy(); + if ( pEnemy ) + { + Vector vecEnemyEyePos = pEnemy->EyePosition(); + + float gravity = GetCurrentGravity(); + if ( gravity <= 1 ) + { + gravity = 1; + } + + // + // How fast does the headcrab need to travel to reach my enemy's eyes given gravity? + // + float height = ( vecEnemyEyePos.z - GetAbsOrigin().z ); + if ( height < 16 ) + { + height = 16; + } + else if ( height > 120 ) + { + height = 120; + } + float speed = sqrt( 2 * gravity * height ); + float time = speed / gravity; + + // + // Scale the sideways velocity to get there at the right time + // + vecJumpDir = vecEnemyEyePos - GetAbsOrigin(); + vecJumpDir = vecJumpDir / time; + + // + // Speed to offset gravity at the desired height. + // + vecJumpDir.z = speed; + + // + // Don't jump too far/fast. + // + float distance = vecJumpDir.Length(); + if ( distance > 650 ) + { + vecJumpDir = vecJumpDir * ( 650.0 / distance ); + } + } + else + { + // + // Jump hop, don't care where. + // + Vector forward, up; + AngleVectors( GetAbsAngles(), &forward, NULL, &up ); + vecJumpDir = Vector( forward.x, forward.y, up.z ) * 350; + } + + int iSound = random->RandomInt( 0 , 2 ); + if ( iSound != 0 ) + { + AttackSound(); + } + + SetAbsVelocity( vecJumpDir ); + m_flNextAttack = gpGlobals->curtime + 2; + break; + } + + default: + { + CAI_BaseNPC::HandleAnimEvent( pEvent ); + break; + } + } +} + +void CNPC_Headcrab::AttackSound( void ) +{ + HeadCrabSound( "Headcrab.Attack" ); +} + + +//------------------------------------------------------------------------------ +// +// Schedules +// +//------------------------------------------------------------------------------ + +AI_BEGIN_CUSTOM_NPC( monster_headcrab, CNPC_Headcrab ) + + //========================================================= + // > SCHED_HEADCRAB_RANGE_ATTACK1 + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HEADCRAB_RANGE_ATTACK1, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_IDEAL 0" + " TASK_RANGE_ATTACK1 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_FACE_IDEAL 0" + " TASK_WAIT_RANDOM 0.5" + " " + " Interrupts" + " COND_ENEMY_OCCLUDED" + " COND_NO_PRIMARY_AMMO" + ) + + //========================================================= + // > SCHED_FAST_HEADCRAB_RANGE_ATTACK1 + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_FAST_HEADCRAB_RANGE_ATTACK1, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_IDEAL 0" + " TASK_RANGE_ATTACK1 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " " + " Interrupts" + " COND_ENEMY_OCCLUDED" + " COND_NO_PRIMARY_AMMO" + ) + +AI_END_CUSTOM_NPC() + + +class CNPC_BabyCrab : public CNPC_Headcrab +{ + DECLARE_CLASS( CNPC_BabyCrab, CNPC_Headcrab ); +public: + void Spawn( void ); + void Precache( void ); + + unsigned int PhysicsSolidMaskForEntity( void ) const; + + int RangeAttack1Conditions ( float flDot, float flDist ); + float MaxYawSpeed( void ){ return 120.0f; } + float GetDamageAmount( void ); + + virtual int GetVoicePitch( void ) { return PITCH_NORM + random->RandomInt( 40,50 ); } + virtual float GetSoundVolume( void ) { return 0.8; } +}; +LINK_ENTITY_TO_CLASS( monster_babycrab, CNPC_BabyCrab ); + +unsigned int CNPC_BabyCrab::PhysicsSolidMaskForEntity( void ) const +{ + unsigned int iMask = BaseClass::PhysicsSolidMaskForEntity(); + + iMask &= ~CONTENTS_MONSTERCLIP; + + return iMask; +} + +void CNPC_BabyCrab::Spawn( void ) +{ + CNPC_Headcrab::Spawn(); + SetModel( "models/baby_headcrab.mdl" ); + m_nRenderMode = kRenderTransTexture; + + SetRenderColor( 255, 255, 255, 192 ); + + UTIL_SetSize(this, Vector(-12, -12, 0), Vector(12, 12, 24)); + + m_iHealth = sk_headcrab_health.GetFloat() * 0.25; // less health than full grown +} + +void CNPC_BabyCrab::Precache( void ) +{ + PrecacheModel( "models/baby_headcrab.mdl" ); + CNPC_Headcrab::Precache(); +} + +int CNPC_BabyCrab::RangeAttack1Conditions( float flDot, float flDist ) +{ + if ( GetFlags() & FL_ONGROUND ) + { + if ( GetGroundEntity() && ( GetGroundEntity()->GetFlags() & ( FL_CLIENT | FL_NPC ) ) ) + return COND_CAN_RANGE_ATTACK1; + + // A little less accurate, but jump from closer + if ( flDist <= 180 && flDot >= 0.55 ) + return COND_CAN_RANGE_ATTACK1; + } + + return COND_NONE; +} + +float CNPC_BabyCrab::GetDamageAmount( void ) +{ + return sk_headcrab_dmg_bite.GetFloat() * 0.3; +} diff --git a/game/server/hl1/hl1_npc_headcrab.h b/game/server/hl1/hl1_npc_headcrab.h new file mode 100644 index 0000000..c047415 --- /dev/null +++ b/game/server/hl1/hl1_npc_headcrab.h @@ -0,0 +1,63 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef NPC_HEADCRAB_H +#define NPC_HEADCRAB_H +#pragma once + + +#include "hl1_ai_basenpc.h" + +class CNPC_Headcrab : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_Headcrab, CHL1BaseNPC ); +public: + + void Spawn( void ); + void Precache( void ); + + void RunTask ( const Task_t *pTask ); + void StartTask ( const Task_t *pTask ); + void SetYawSpeed ( void ); + Vector Center( void ); + Vector BodyTarget( const Vector &posSrc, bool bNoisy = true ); + + float MaxYawSpeed( void ); + Class_T Classify( void ); + + void LeapTouch ( CBaseEntity *pOther ); + void BiteSound( void ); + void AttackSound( void ); + void TouchDamage( CBaseEntity *pOther ); + void HandleAnimEvent( animevent_t *pEvent ); + int SelectSchedule( void ); + void Touch( CBaseEntity *pOther ); + int OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ); + int TranslateSchedule( int scheduleType ); + void PrescheduleThink( void ); + int RangeAttack1Conditions ( float flDot, float flDist ); + float GetDamageAmount( void ); + virtual void PainSound( const CTakeDamageInfo &info ); + virtual void DeathSound( const CTakeDamageInfo &info ); + virtual void IdleSound(); + virtual void AlertSound(); + + virtual int GetVoicePitch( void ) { return 100; } + virtual float GetSoundVolume( void ) { return 1.0; } + + int m_nGibCount; + + DEFINE_CUSTOM_AI; + DECLARE_DATADESC(); + +protected: + void HeadCrabSound( const char *pchSound ); + + Vector m_vecJumpVel; +}; + +#endif //NPC_HEADCRAB_H
\ No newline at end of file diff --git a/game/server/hl1/hl1_npc_hgrunt.cpp b/game/server/hl1/hl1_npc_hgrunt.cpp new file mode 100644 index 0000000..375ddf7 --- /dev/null +++ b/game/server/hl1/hl1_npc_hgrunt.cpp @@ -0,0 +1,2645 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Bullseyes act as targets for other NPC's to attack and to trigger +// events +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "beam_shared.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "hl1_npc_hgrunt.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "basecombatweapon.h" +#include "hl1_basegrenade.h" +#include "ai_interactions.h" +#include "scripted.h" +#include "hl1_basegrenade.h" +#include "hl1_grenade_mp5.h" + +ConVar sk_hgrunt_health( "sk_hgrunt_health","0"); +ConVar sk_hgrunt_kick ( "sk_hgrunt_kick", "0" ); +ConVar sk_hgrunt_pellets ( "sk_hgrunt_pellets", "0" ); +ConVar sk_hgrunt_gspeed ( "sk_hgrunt_gspeed", "0" ); + +extern ConVar sk_plr_dmg_grenade; +extern ConVar sk_plr_dmg_mp5_grenade; + +#define SF_GRUNT_LEADER ( 1 << 5 ) + +int g_fGruntQuestion; // true if an idle grunt asked a question. Cleared when someone answers. +int g_iSquadIndex = 0; + +#define HGRUNT_GUN_SPREAD 0.08716f + +//========================================================= +// monster-specific DEFINE's +//========================================================= +#define GRUNT_CLIP_SIZE 36 // how many bullets in a clip? - NOTE: 3 round burst sound, so keep as 3 * x! +#define GRUNT_VOL 0.35 // volume of grunt sounds +#define GRUNT_SNDLVL SNDLVL_NORM // soundlevel of grunt sentences +#define HGRUNT_LIMP_HEALTH 20 +#define HGRUNT_DMG_HEADSHOT ( DMG_BULLET | DMG_CLUB ) // damage types that can kill a grunt with a single headshot. +#define HGRUNT_NUM_HEADS 2 // how many grunt heads are there? +#define HGRUNT_MINIMUM_HEADSHOT_DAMAGE 15 // must do at least this much damage in one shot to head to score a headshot kill +#define HGRUNT_SENTENCE_VOLUME (float)0.35 // volume of grunt sentences + +#define HGRUNT_9MMAR ( 1 << 0) +#define HGRUNT_HANDGRENADE ( 1 << 1) +#define HGRUNT_GRENADELAUNCHER ( 1 << 2) +#define HGRUNT_SHOTGUN ( 1 << 3) + +#define HEAD_GROUP 1 +#define HEAD_GRUNT 0 +#define HEAD_COMMANDER 1 +#define HEAD_SHOTGUN 2 +#define HEAD_M203 3 +#define GUN_GROUP 2 +#define GUN_MP5 0 +#define GUN_SHOTGUN 1 +#define GUN_NONE 2 + +//========================================================= +// Monster's Anim Events Go Here +//========================================================= +#define HGRUNT_AE_RELOAD ( 2 ) +#define HGRUNT_AE_KICK ( 3 ) +#define HGRUNT_AE_BURST1 ( 4 ) +#define HGRUNT_AE_BURST2 ( 5 ) +#define HGRUNT_AE_BURST3 ( 6 ) +#define HGRUNT_AE_GREN_TOSS ( 7 ) +#define HGRUNT_AE_GREN_LAUNCH ( 8 ) +#define HGRUNT_AE_GREN_DROP ( 9 ) +#define HGRUNT_AE_CAUGHT_ENEMY ( 10) // grunt established sight with an enemy (player only) that had previously eluded the squad. +#define HGRUNT_AE_DROP_GUN ( 11) // grunt (probably dead) is dropping his mp5. + + +const char *CNPC_HGrunt::pGruntSentences[] = +{ + "HG_GREN", // grenade scared grunt + "HG_ALERT", // sees player + "HG_MONSTER", // sees monster + "HG_COVER", // running to cover + "HG_THROW", // about to throw grenade + "HG_CHARGE", // running out to get the enemy + "HG_TAUNT", // say rude things +}; + +enum HGRUNT_SENTENCE_TYPES +{ + HGRUNT_SENT_NONE = -1, + HGRUNT_SENT_GREN = 0, + HGRUNT_SENT_ALERT, + HGRUNT_SENT_MONSTER, + HGRUNT_SENT_COVER, + HGRUNT_SENT_THROW, + HGRUNT_SENT_CHARGE, + HGRUNT_SENT_TAUNT, +} ; + +LINK_ENTITY_TO_CLASS( monster_human_grunt, CNPC_HGrunt ); + +//========================================================= +// monster-specific schedule types +//========================================================= +enum +{ + SCHED_GRUNT_FAIL = LAST_SHARED_SCHEDULE, + SCHED_GRUNT_COMBAT_FAIL, + SCHED_GRUNT_VICTORY_DANCE, + SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE, + SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE_RETRY, + SCHED_GRUNT_FOUND_ENEMY, + SCHED_GRUNT_COMBAT_FACE, + SCHED_GRUNT_SIGNAL_SUPPRESS, + SCHED_GRUNT_SUPPRESS, + SCHED_GRUNT_WAIT_IN_COVER, + SCHED_GRUNT_TAKE_COVER, + SCHED_GRUNT_GRENADE_COVER, + SCHED_GRUNT_TOSS_GRENADE_COVER, + SCHED_GRUNT_HIDE_RELOAD, + SCHED_GRUNT_SWEEP, + SCHED_GRUNT_RANGE_ATTACK1A, + SCHED_GRUNT_RANGE_ATTACK1B, + SCHED_GRUNT_RANGE_ATTACK2, + SCHED_GRUNT_REPEL, + SCHED_GRUNT_REPEL_ATTACK, + SCHED_GRUNT_REPEL_LAND, + SCHED_GRUNT_TAKE_COVER_FAILED, + SCHED_GRUNT_RELOAD, + SCHED_GRUNT_TAKE_COVER_FROM_ENEMY, + SCHED_GRUNT_BARNACLE_HIT, + SCHED_GRUNT_BARNACLE_PULL, + SCHED_GRUNT_BARNACLE_CHOMP, + SCHED_GRUNT_BARNACLE_CHEW, +}; + +//========================================================= +// monster-specific tasks +//========================================================= +enum +{ + TASK_GRUNT_FACE_TOSS_DIR = LAST_SHARED_TASK + 1, + TASK_GRUNT_SPEAK_SENTENCE, + TASK_GRUNT_CHECK_FIRE, +}; + + +//========================================================= +// monster-specific conditions +//========================================================= +enum +{ + COND_GRUNT_NOFIRE = LAST_SHARED_CONDITION + 1, +}; + +// ----------------------------------------------- +// > Squad slots +// ----------------------------------------------- +enum SquadSlot_T +{ + SQUAD_SLOT_GRENADE1 = LAST_SHARED_SQUADSLOT, + SQUAD_SLOT_GRENADE2, + SQUAD_SLOT_ENGAGE1, + SQUAD_SLOT_ENGAGE2, +}; + + +int ACT_GRUNT_LAUNCH_GRENADE; +int ACT_GRUNT_TOSS_GRENADE; +int ACT_GRUNT_MP5_STANDING; +int ACT_GRUNT_MP5_CROUCHING; +int ACT_GRUNT_SHOTGUN_STANDING; +int ACT_GRUNT_SHOTGUN_CROUCHING; + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CNPC_HGrunt ) + DEFINE_FIELD( m_flNextGrenadeCheck, FIELD_TIME ), + DEFINE_FIELD( m_flNextPainTime, FIELD_TIME ), + DEFINE_FIELD( m_flCheckAttackTime, FIELD_FLOAT ), + DEFINE_FIELD( m_vecTossVelocity, FIELD_VECTOR ), + DEFINE_FIELD( m_iLastGrenadeCondition, FIELD_INTEGER ), + DEFINE_FIELD( m_fStanding, FIELD_BOOLEAN ), + DEFINE_FIELD( m_fFirstEncounter, FIELD_BOOLEAN ), + DEFINE_FIELD( m_iClipSize, FIELD_INTEGER ), + DEFINE_FIELD( m_voicePitch, FIELD_INTEGER ), + DEFINE_FIELD( m_iSentence, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_iWeapons, FIELD_INTEGER, "weapons" ), + DEFINE_KEYFIELD( m_SquadName, FIELD_STRING, "netname" ), + + DEFINE_FIELD( m_bInBarnacleMouth, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flLastEnemySightTime, FIELD_TIME ), + DEFINE_FIELD( m_flTalkWaitTime, FIELD_TIME ), + //DEFINE_FIELD( m_iAmmoType, FIELD_INTEGER ), + +END_DATADESC() + + +//========================================================= +// Spawn +//========================================================= +void CNPC_HGrunt::Spawn() +{ + Precache( ); + + SetModel( "models/hgrunt.mdl" ); + + SetHullType(HULL_HUMAN); + SetHullSizeNormal(); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + m_bloodColor = BLOOD_COLOR_RED; + ClearEffects(); + m_iHealth = sk_hgrunt_health.GetFloat(); + m_flFieldOfView = 0.2;// indicates the width of this monster's forward view cone ( as a dotproduct result ) + m_NPCState = NPC_STATE_NONE; + m_flNextGrenadeCheck = gpGlobals->curtime + 1; + m_flNextPainTime = gpGlobals->curtime; + m_iSentence = HGRUNT_SENT_NONE; + + CapabilitiesClear(); + CapabilitiesAdd ( bits_CAP_SQUAD | bits_CAP_TURN_HEAD | bits_CAP_DOORS_GROUP | bits_CAP_MOVE_GROUND ); + + CapabilitiesAdd(bits_CAP_INNATE_RANGE_ATTACK1 ); + + // Innate range attack for grenade + CapabilitiesAdd(bits_CAP_INNATE_RANGE_ATTACK2 ); + // Innate range attack for kicking + CapabilitiesAdd(bits_CAP_INNATE_MELEE_ATTACK1 ); + + m_fFirstEncounter = true;// this is true when the grunt spawns, because he hasn't encountered an enemy yet. + + m_HackedGunPos = Vector ( 0, 0, 55 ); + + if ( m_iWeapons == 0) + { + // initialize to original values + m_iWeapons = HGRUNT_9MMAR | HGRUNT_HANDGRENADE; + // pev->weapons = HGRUNT_SHOTGUN; + // pev->weapons = HGRUNT_9MMAR | HGRUNT_GRENADELAUNCHER; + } + + if (FBitSet( m_iWeapons, HGRUNT_SHOTGUN )) + { + SetBodygroup( GUN_GROUP, GUN_SHOTGUN ); + m_iClipSize = 8; + } + else + { + m_iClipSize = GRUNT_CLIP_SIZE; + } + m_cAmmoLoaded = m_iClipSize; + + if ( random->RandomInt( 0, 99 ) < 80) + m_nSkin = 0; // light skin + else + m_nSkin = 1; // dark skin + + if (FBitSet( m_iWeapons, HGRUNT_SHOTGUN )) + { + SetBodygroup( HEAD_GROUP, HEAD_SHOTGUN); + } + else if (FBitSet( m_iWeapons, HGRUNT_GRENADELAUNCHER )) + { + SetBodygroup( HEAD_GROUP, HEAD_M203 ); + m_nSkin = 1; // alway dark skin + } + + m_flTalkWaitTime = 0; + + + //HACK + g_iSquadIndex = 0; + + BaseClass::Spawn(); + + NPCInit(); +} + +int CNPC_HGrunt::IRelationPriority( CBaseEntity *pTarget ) +{ + //I hate alien grunts more than anything. + if ( pTarget->Classify() == CLASS_ALIEN_MILITARY ) + { + if ( FClassnameIs( pTarget, "monster_alien_grunt" ) ) + { + return ( BaseClass::IRelationPriority ( pTarget ) + 1 ); + } + } + + return BaseClass::IRelationPriority( pTarget ); +} + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CNPC_HGrunt::Precache() +{ + m_iAmmoType = GetAmmoDef()->Index("9mmRound"); + + PrecacheModel("models/hgrunt.mdl"); + + // get voice pitch + if ( random->RandomInt(0,1)) + m_voicePitch = 109 + random->RandomInt(0,7); + else + m_voicePitch = 100; + + PrecacheScriptSound( "HGrunt.Reload" ); + PrecacheScriptSound( "HGrunt.GrenadeLaunch" ); + PrecacheScriptSound( "HGrunt.9MM" ); + PrecacheScriptSound( "HGrunt.Shotgun" ); + PrecacheScriptSound( "HGrunt.Pain" ); + PrecacheScriptSound( "HGrunt.Die" ); + + BaseClass::Precache(); + + UTIL_PrecacheOther( "grenade_hand" ); + UTIL_PrecacheOther( "grenade_mp5" ); +} + +//========================================================= +// someone else is talking - don't speak +//========================================================= +bool CNPC_HGrunt::FOkToSpeak( void ) +{ +// if someone else is talking, don't speak + if ( gpGlobals->curtime <= m_flTalkWaitTime ) + return FALSE; + + if ( m_spawnflags & SF_NPC_GAG ) + { + if ( m_NPCState != NPC_STATE_COMBAT ) + { + // no talking outside of combat if gagged. + return FALSE; + } + } + + return TRUE; +} + + +//========================================================= +// Speak Sentence - say your cued up sentence. +// +// Some grunt sentences (take cover and charge) rely on actually +// being able to execute the intended action. It's really lame +// when a grunt says 'COVER ME' and then doesn't move. The problem +// is that the sentences were played when the decision to TRY +// to move to cover was made. Now the sentence is played after +// we know for sure that there is a valid path. The schedule +// may still fail but in most cases, well after the grunt has +// started moving. +//========================================================= +void CNPC_HGrunt::SpeakSentence( void ) +{ + if ( m_iSentence == HGRUNT_SENT_NONE ) + { + // no sentence cued up. + return; + } + + if ( FOkToSpeak() ) + { + SENTENCEG_PlayRndSz( edict(), pGruntSentences[ m_iSentence ], HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + JustSpoke(); + } +} + +//========================================================= +//========================================================= +void CNPC_HGrunt::JustSpoke( void ) +{ + m_flTalkWaitTime = gpGlobals->curtime + random->RandomFloat( 1.5f, 2.0f ); + m_iSentence = HGRUNT_SENT_NONE; +} + +//========================================================= +// PrescheduleThink - this function runs after conditions +// are collected and before scheduling code is run. +//========================================================= +void CNPC_HGrunt::PrescheduleThink ( void ) +{ + BaseClass::PrescheduleThink(); + + if ( m_pSquad && GetEnemy() != NULL ) + { + if ( m_pSquad->GetLeader() == NULL ) + return; + + CNPC_HGrunt *pSquadLeader = (CNPC_HGrunt*)m_pSquad->GetLeader()->MyNPCPointer(); + + if ( pSquadLeader == NULL ) + return; //Paranoid, so making sure it's ok. + + if ( HasCondition ( COND_SEE_ENEMY ) ) + { + // update the squad's last enemy sighting time. + pSquadLeader->m_flLastEnemySightTime = gpGlobals->curtime; + } + else + { + if ( gpGlobals->curtime - pSquadLeader->m_flLastEnemySightTime > 5 ) + { + // been a while since we've seen the enemy + pSquadLeader->GetEnemies()->MarkAsEluded( GetEnemy() ); + } + } + } +} + +Class_T CNPC_HGrunt::Classify ( void ) +{ + return CLASS_HUMAN_MILITARY; +} + +//========================================================= +// +// SquadRecruit(), get some monsters of my classification and +// link them as a group. returns the group size +// +//========================================================= +int CNPC_HGrunt::SquadRecruit( int searchRadius, int maxMembers ) +{ + int squadCount; + int iMyClass = Classify();// cache this monster's class + + if ( maxMembers < 2 ) + return 0; + + // I am my own leader + squadCount = 1; + + CBaseEntity *pEntity = NULL; + + if ( m_SquadName != NULL_STRING ) + { + // I have a netname, so unconditionally recruit everyone else with that name. + pEntity = gEntList.FindEntityByClassname( pEntity, "monster_human_grunt" ); + + while ( pEntity ) + { + CNPC_HGrunt *pRecruit = (CNPC_HGrunt*)pEntity->MyNPCPointer(); + + if ( pRecruit ) + { + if ( !pRecruit->m_pSquad && pRecruit->Classify() == iMyClass && pRecruit != this ) + { + // minimum protection here against user error.in worldcraft. + if ( pRecruit->m_SquadName != NULL_STRING && FStrEq( STRING( m_SquadName ), STRING( pRecruit->m_SquadName ) ) ) + { + pRecruit->InitSquad(); + squadCount++; + } + } + } + + pEntity = gEntList.FindEntityByClassname( pEntity, "monster_human_grunt" ); + } + + return squadCount; + } + else + { + char szSquadName[64]; + Q_snprintf( szSquadName, sizeof( szSquadName ), "squad%d\n", g_iSquadIndex ); + + m_SquadName = MAKE_STRING( szSquadName ); + + while ( ( pEntity = gEntList.FindEntityInSphere( pEntity, GetAbsOrigin(), searchRadius ) ) != NULL ) + { + if ( !FClassnameIs ( pEntity, "monster_human_grunt" ) ) + continue; + + CNPC_HGrunt *pRecruit = (CNPC_HGrunt*)pEntity->MyNPCPointer(); + + if ( pRecruit && pRecruit != this && pRecruit->IsAlive() && !pRecruit->m_hCine ) + { + // Can we recruit this guy? + if ( !pRecruit->m_pSquad && pRecruit->Classify() == iMyClass && + ( (iMyClass != CLASS_ALIEN_MONSTER) || FClassnameIs( this, pRecruit->GetClassname() ) ) && + !pRecruit->m_SquadName ) + { + trace_t tr; + UTIL_TraceLine( GetAbsOrigin() + GetViewOffset(), pRecruit->GetAbsOrigin() + GetViewOffset(), MASK_NPCSOLID_BRUSHONLY, pRecruit, COLLISION_GROUP_NONE, &tr );// try to hit recruit with a traceline. + + if ( tr.fraction == 1.0 ) + { + //We're ready to recruit people, so start a squad if I don't have one. + if ( !m_pSquad ) + { + InitSquad(); + } + + pRecruit->m_SquadName = m_SquadName; + + pRecruit->CapabilitiesAdd ( bits_CAP_SQUAD ); + pRecruit->InitSquad(); + + squadCount++; + } + } + } + } + + if ( squadCount > 1 ) + { + g_iSquadIndex++; + } + } + + return squadCount; +} + +void CNPC_HGrunt::StartNPC ( void ) +{ + if ( !m_pSquad ) + { + if ( m_SquadName != NULL_STRING ) + { + // if I have a groupname, I can only recruit if I'm flagged as leader + if ( GetSpawnFlags() & SF_GRUNT_LEADER ) + { + InitSquad(); + + // try to form squads now. + int iSquadSize = SquadRecruit( 1024, 4 ); + + if ( iSquadSize ) + { + Msg ( "Squad of %d %s formed\n", iSquadSize, GetClassname() ); + } + } + else + { + + //Hacky. + //Revisit me later. + const char *pSquadName = STRING( m_SquadName ); + + m_SquadName = NULL_STRING; + + BaseClass::StartNPC(); + + m_SquadName = MAKE_STRING( pSquadName ); + + return; + } + } + else + { + int iSquadSize = SquadRecruit( 1024, 4 ); + + if ( iSquadSize ) + { + Msg ( "Squad of %d %s formed\n", iSquadSize, GetClassname() ); + } + } + } + + BaseClass::StartNPC(); + + if ( m_pSquad && m_pSquad->IsLeader( this ) ) + { + SetBodygroup( 1, 1 ); // UNDONE: truly ugly hack + m_nSkin = 0; + } +} + +//========================================================= +// CheckMeleeAttack1 +//========================================================= +int CNPC_HGrunt::MeleeAttack1Conditions ( float flDot, float flDist ) +{ + if (flDist > 64) + return COND_TOO_FAR_TO_ATTACK; + else if (flDot < 0.7) + return COND_NOT_FACING_ATTACK; + + return COND_CAN_MELEE_ATTACK1; +} + +//========================================================= +// CheckRangeAttack1 - overridden for HGrunt, cause +// FCanCheckAttacks() doesn't disqualify all attacks based +// on whether or not the enemy is occluded because unlike +// the base class, the HGrunt can attack when the enemy is +// occluded (throw grenade over wall, etc). We must +// disqualify the machine gun attack if the enemy is occluded. +//========================================================= +int CNPC_HGrunt::RangeAttack1Conditions ( float flDot, float flDist ) +{ + if ( !HasCondition( COND_ENEMY_OCCLUDED ) && flDist <= 2048 && flDot >= 0.5 && NoFriendlyFire() ) + { + trace_t tr; + + if ( !GetEnemy()->IsPlayer() && flDist <= 64 ) + { + // kick nonclients, but don't shoot at them. + return COND_NONE; + } + + Vector vecSrc; + QAngle angAngles; + + GetAttachment( "0", vecSrc, angAngles ); + + //NDebugOverlay::Line( GetAbsOrigin() + GetViewOffset(), GetEnemy()->BodyTarget(GetAbsOrigin() + GetViewOffset()), 255, 0, 0, false, 0.1 ); + // verify that a bullet fired from the gun will hit the enemy before the world. + UTIL_TraceLine( GetAbsOrigin() + GetViewOffset(), GetEnemy()->BodyTarget(GetAbsOrigin() + GetViewOffset()), MASK_SHOT, this/*pentIgnore*/, COLLISION_GROUP_NONE, &tr); + + if ( tr.fraction == 1.0 || tr.m_pEnt == GetEnemy() ) + { + //NDebugOverlay::Line( tr.startpos, tr.endpos, 0, 255, 0, false, 1.0 ); + return COND_CAN_RANGE_ATTACK1; + } + + //NDebugOverlay::Line( tr.startpos, tr.endpos, 255, 0, 0, false, 1.0 ); + } + + + if ( !NoFriendlyFire() ) + return COND_WEAPON_BLOCKED_BY_FRIEND; //err =| + + return COND_NONE; +} + +int CNPC_HGrunt::RangeAttack2Conditions( float flDot, float flDist ) +{ + m_iLastGrenadeCondition = GetGrenadeConditions( flDot, flDist ); + return m_iLastGrenadeCondition; +} + +int CNPC_HGrunt::GetGrenadeConditions( float flDot, float flDist ) +{ + if ( !FBitSet( m_iWeapons, ( HGRUNT_HANDGRENADE | HGRUNT_GRENADELAUNCHER ) ) ) + return COND_NONE; + + // assume things haven't changed too much since last time + if (gpGlobals->curtime < m_flNextGrenadeCheck ) + return m_iLastGrenadeCondition; + + if ( m_flGroundSpeed != 0 ) + return COND_NONE; + + CBaseEntity *pEnemy = GetEnemy(); + + if (!pEnemy) + return COND_NONE; + + Vector flEnemyLKP = GetEnemyLKP(); + if ( !(pEnemy->GetFlags() & FL_ONGROUND) && pEnemy->GetWaterLevel() == 0 && flEnemyLKP.z > (GetAbsOrigin().z + WorldAlignMaxs().z) ) + { + //!!!BUGBUG - we should make this check movetype and make sure it isn't FLY? Players who jump a lot are unlikely to + // be grenaded. + // don't throw grenades at anything that isn't on the ground! + return COND_NONE; + } + + Vector vecTarget; + + if (FBitSet( m_iWeapons, HGRUNT_HANDGRENADE)) + { + // find feet + if ( random->RandomInt( 0,1 ) ) + { + // magically know where they are + pEnemy->CollisionProp()->NormalizedToWorldSpace( Vector( 0.5f, 0.5f, 0.0f ), &vecTarget ); + } + else + { + // toss it to where you last saw them + vecTarget = flEnemyLKP; + } + } + else + { + // find target + // vecTarget = GetEnemy()->BodyTarget( GetAbsOrigin() ); + vecTarget = GetEnemy()->GetAbsOrigin() + (GetEnemy()->BodyTarget( GetAbsOrigin() ) - GetEnemy()->GetAbsOrigin()); + // estimate position + if ( HasCondition( COND_SEE_ENEMY)) + { + vecTarget = vecTarget + ((vecTarget - GetAbsOrigin()).Length() / sk_hgrunt_gspeed.GetFloat()) * GetEnemy()->GetAbsVelocity(); + } + } + + // are any of my squad members near the intended grenade impact area? + if ( m_pSquad ) + { + if ( m_pSquad->SquadMemberInRange( vecTarget, 256 ) ) + { + // crap, I might blow my own guy up. Don't throw a grenade and don't check again for a while. + m_flNextGrenadeCheck = gpGlobals->curtime + 1; // one full second. + return COND_NONE; + } + } + + if ( ( vecTarget - GetAbsOrigin() ).Length2D() <= 256 ) + { + // crap, I don't want to blow myself up + m_flNextGrenadeCheck = gpGlobals->curtime + 1; // one full second. + return COND_NONE; + } + + + if (FBitSet( m_iWeapons, HGRUNT_HANDGRENADE)) + { + Vector vGunPos; + QAngle angGunAngles; + GetAttachment( "0", vGunPos, angGunAngles ); + + + Vector vecToss = VecCheckToss( this, vGunPos, vecTarget, -1, 0.5, false ); + + if ( vecToss != vec3_origin ) + { + m_vecTossVelocity = vecToss; + + // don't check again for a while. + m_flNextGrenadeCheck = gpGlobals->curtime + 0.3; // 1/3 second. + + return COND_CAN_RANGE_ATTACK2; + } + else + { + // don't check again for a while. + m_flNextGrenadeCheck = gpGlobals->curtime + 1; // one full second. + + return COND_NONE; + } + } + else + { + Vector vGunPos; + QAngle angGunAngles; + GetAttachment( "0", vGunPos, angGunAngles ); + + Vector vecToss = VecCheckThrow( this, vGunPos, vecTarget, sk_hgrunt_gspeed.GetFloat(), 0.5 ); + + if ( vecToss != vec3_origin ) + { + m_vecTossVelocity = vecToss; + + // don't check again for a while. + m_flNextGrenadeCheck = gpGlobals->curtime + 0.3; // 1/3 second. + + return COND_CAN_RANGE_ATTACK2; + } + else + { + // don't check again for a while. + m_flNextGrenadeCheck = gpGlobals->curtime + 1; // one full second. + + return COND_NONE; + } + } +} + + +//========================================================= +// FCanCheckAttacks - this is overridden for human grunts +// because they can throw/shoot grenades when they can't see their +// target and the base class doesn't check attacks if the monster +// cannot see its enemy. +// +// !!!BUGBUG - this gets called before a 3-round burst is fired +// which means that a friendly can still be hit with up to 2 rounds. +// ALSO, grenades will not be tossed if there is a friendly in front, +// this is a bad bug. Friendly machine gun fire avoidance +// will unecessarily prevent the throwing of a grenade as well. +//========================================================= +bool CNPC_HGrunt::FCanCheckAttacks( void ) +{ + // This condition set when too close to a grenade to blow it up + if ( !HasCondition( COND_TOO_CLOSE_TO_ATTACK ) ) + { + return true; + } + else + { + return false; + } +} + +int CNPC_HGrunt::GetSoundInterests( void ) +{ + return SOUND_WORLD | + SOUND_COMBAT | + SOUND_PLAYER | + SOUND_BULLET_IMPACT | + SOUND_DANGER; +} + + +//========================================================= +// TraceAttack - make sure we're not taking it in the helmet +//========================================================= +void CNPC_HGrunt::TraceAttack( const CTakeDamageInfo &inputInfo, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + CTakeDamageInfo info = inputInfo; + + // check for helmet shot + if (ptr->hitgroup == 11) + { + // make sure we're wearing one + if ( GetBodygroup( 1 ) == HEAD_GRUNT && (info.GetDamageType() & (DMG_BULLET | DMG_SLASH | DMG_BLAST | DMG_CLUB))) + { + // absorb damage + info.SetDamage( info.GetDamage() - 20 ); + if ( info.GetDamage() <= 0 ) + info.SetDamage( 0.01 ); + } + // it's head shot anyways + ptr->hitgroup = HITGROUP_HEAD; + } + BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator ); +} + + +//========================================================= +// TakeDamage - overridden for the grunt because the grunt +// needs to forget that he is in cover if he's hurt. (Obviously +// not in a safe place anymore). +//========================================================= +int CNPC_HGrunt::OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ) +{ + Forget( bits_MEMORY_INCOVER ); + + return BaseClass::OnTakeDamage_Alive ( inputInfo ); +} + + +//========================================================= +// SetYawSpeed - allows each sequence to have a different +// turn rate associated with it. +//========================================================= +float CNPC_HGrunt::MaxYawSpeed( void ) +{ + float flYS; + + switch ( GetActivity() ) + { + case ACT_IDLE: + flYS = 150; + break; + case ACT_RUN: + flYS = 150; + break; + case ACT_WALK: + flYS = 180; + break; + case ACT_RANGE_ATTACK1: + flYS = 120; + break; + case ACT_RANGE_ATTACK2: + flYS = 120; + break; + case ACT_MELEE_ATTACK1: + flYS = 120; + break; + case ACT_MELEE_ATTACK2: + flYS = 120; + break; + case ACT_TURN_LEFT: + case ACT_TURN_RIGHT: + flYS = 180; + break; + case ACT_GLIDE: + case ACT_FLY: + flYS = 30; + break; + default: + flYS = 90; + break; + } + + // Yaw speed is handled differently now! + return flYS * 0.5f; +} + +void CNPC_HGrunt::IdleSound( void ) +{ + if (FOkToSpeak() && ( g_fGruntQuestion || random->RandomInt( 0,1 ) ) ) + { + if (!g_fGruntQuestion) + { + // ask question or make statement + switch ( random->RandomInt( 0,2 ) ) + { + case 0: // check in + SENTENCEG_PlayRndSz( edict(), "HG_CHECK", HGRUNT_SENTENCE_VOLUME, SNDLVL_NORM, 0, m_voicePitch); + g_fGruntQuestion = 1; + break; + case 1: // question + SENTENCEG_PlayRndSz( edict(), "HG_QUEST", HGRUNT_SENTENCE_VOLUME, SNDLVL_NORM, 0, m_voicePitch); + g_fGruntQuestion = 2; + break; + case 2: // statement + SENTENCEG_PlayRndSz( edict(), "HG_IDLE", HGRUNT_SENTENCE_VOLUME, SNDLVL_NORM, 0, m_voicePitch); + break; + } + } + else + { + switch (g_fGruntQuestion) + { + case 1: // check in + SENTENCEG_PlayRndSz( edict(), "HG_CLEAR", HGRUNT_SENTENCE_VOLUME, SNDLVL_NORM, 0, m_voicePitch); + break; + case 2: // question + SENTENCEG_PlayRndSz( edict(), "HG_ANSWER", HGRUNT_SENTENCE_VOLUME, SNDLVL_NORM, 0, m_voicePitch); + break; + } + g_fGruntQuestion = 0; + } + JustSpoke(); + } +} + +bool CNPC_HGrunt::HandleInteraction(int interactionType, void *data, CBaseCombatCharacter* sourceEnt) +{ + if (interactionType == g_interactionBarnacleVictimDangle) + { + // Force choosing of a new schedule + ClearSchedule( "Soldier being eaten by a barnacle" ); + m_bInBarnacleMouth = true; + return true; + } + else if ( interactionType == g_interactionBarnacleVictimReleased ) + { + SetState ( NPC_STATE_IDLE ); + m_bInBarnacleMouth = false; + SetAbsVelocity( vec3_origin ); + SetMoveType( MOVETYPE_STEP ); + return true; + } + else if ( interactionType == g_interactionBarnacleVictimGrab ) + { + if ( GetFlags() & FL_ONGROUND ) + { + SetGroundEntity( NULL ); + } + + //Maybe this will break something else. + if ( GetState() == NPC_STATE_SCRIPT ) + { + m_hCine->CancelScript(); + ClearSchedule( "Soldier grabbed by a barnacle" ); + } + + SetState( NPC_STATE_PRONE ); + + CTakeDamageInfo info; + PainSound( info ); + return true; + } + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Combine needs to check ammo +// Input : +// Output : +//----------------------------------------------------------------------------- +void CNPC_HGrunt::CheckAmmo ( void ) +{ + if ( m_cAmmoLoaded <= 0 ) + SetCondition( COND_NO_PRIMARY_AMMO ); + +} + +//========================================================= +//========================================================= +CBaseEntity *CNPC_HGrunt::Kick( void ) +{ + trace_t tr; + + Vector forward; + AngleVectors( GetAbsAngles(), &forward ); + Vector vecStart = GetAbsOrigin(); + vecStart.z += WorldAlignSize().z * 0.5; + Vector vecEnd = vecStart + (forward * 70); + + UTIL_TraceHull( vecStart, vecEnd, Vector(-16,-16,-18), Vector(16,16,18), MASK_SHOT_HULL, this, COLLISION_GROUP_NONE, &tr ); + + if ( tr.m_pEnt ) + { + CBaseEntity *pEntity = tr.m_pEnt; + return pEntity; + } + + return NULL; +} + +Vector CNPC_HGrunt::Weapon_ShootPosition( void ) +{ + if ( m_fStanding ) + return GetAbsOrigin() + Vector( 0, 0, 60 ); + else + return GetAbsOrigin() + Vector( 0, 0, 48 ); +} + +void CNPC_HGrunt::Event_Killed( const CTakeDamageInfo &info ) +{ + Vector vecGunPos; + QAngle vecGunAngles; + + GetAttachment( "0", vecGunPos, vecGunAngles ); + + // switch to body group with no gun. + SetBodygroup( GUN_GROUP, GUN_NONE ); + + // If the gun would drop into a wall, spawn it at our origin + if( UTIL_PointContents( vecGunPos ) & CONTENTS_SOLID ) + { + vecGunPos = GetAbsOrigin(); + } + + // now spawn a gun. + if (FBitSet( m_iWeapons, HGRUNT_SHOTGUN )) + { + DropItem( "weapon_shotgun", vecGunPos, vecGunAngles ); + } + else + { + DropItem( "weapon_mp5", vecGunPos, vecGunAngles ); + } + + if (FBitSet( m_iWeapons, HGRUNT_GRENADELAUNCHER )) + { + DropItem( "ammo_ARgrenades", BodyTarget( GetAbsOrigin() ), vecGunAngles ); + } + + BaseClass::Event_Killed( info ); +} + +//========================================================= +// HandleAnimEvent - catches the monster-specific messages +// that occur when tagged animation frames are played. +//========================================================= +void CNPC_HGrunt::HandleAnimEvent( animevent_t *pEvent ) +{ + Vector vecShootDir; + Vector vecShootOrigin; + + switch( pEvent->event ) + { + case HGRUNT_AE_RELOAD: + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "HGrunt.Reload" ); + + m_cAmmoLoaded = m_iClipSize; + ClearCondition( COND_NO_PRIMARY_AMMO); + } + break; + + case HGRUNT_AE_GREN_TOSS: + { + CHandGrenade *pGrenade = (CHandGrenade*)Create( "grenade_hand", GetAbsOrigin() + Vector(0,0,60), vec3_angle ); + if ( pGrenade ) + { + pGrenade->ShootTimed( this, m_vecTossVelocity, 3.5 ); + } + + m_iLastGrenadeCondition = COND_NONE; + m_flNextGrenadeCheck = gpGlobals->curtime + 6;// wait six seconds before even looking again to see if a grenade can be thrown. + + Msg( "Tossing a grenade to flush you out!\n" ); + } + break; + + case HGRUNT_AE_GREN_LAUNCH: + { + CPASAttenuationFilter filter2( this ); + EmitSound( filter2, entindex(), "HGrunt.GrenadeLaunch" ); + + Vector vecSrc; + QAngle angAngles; + + GetAttachment( "0", vecSrc, angAngles ); + + CGrenadeMP5 * m_pMyGrenade = (CGrenadeMP5*)Create( "grenade_mp5", vecSrc, angAngles, this ); + m_pMyGrenade->SetAbsVelocity( m_vecTossVelocity ); + m_pMyGrenade->SetLocalAngularVelocity( QAngle( random->RandomFloat( -100, -500 ), 0, 0 ) ); + m_pMyGrenade->SetMoveType( MOVETYPE_FLYGRAVITY ); + m_pMyGrenade->SetThrower( this ); + m_pMyGrenade->SetDamage( sk_plr_dmg_mp5_grenade.GetFloat() ); + + if (g_iSkillLevel == SKILL_HARD) + m_flNextGrenadeCheck = gpGlobals->curtime + random->RandomFloat( 2, 5 );// wait a random amount of time before shooting again + else + m_flNextGrenadeCheck = gpGlobals->curtime + 6;// wait six seconds before even looking again to see if a grenade can be thrown. + + m_iLastGrenadeCondition = COND_NONE; + + Msg( "Using grenade launcer to flush you out!\n" ); + } + break; + + case HGRUNT_AE_GREN_DROP: + { + CHandGrenade *pGrenade = (CHandGrenade*)Create( "grenade_hand", Weapon_ShootPosition(), vec3_angle ); + if ( pGrenade ) + { + pGrenade->ShootTimed( this, m_vecTossVelocity, 3.5 ); + } + + m_iLastGrenadeCondition = COND_NONE; + Msg( "Dropping a grenade!\n" ); + } + break; + + case HGRUNT_AE_BURST1: + { + if ( FBitSet( m_iWeapons, HGRUNT_9MMAR ) ) + { + Shoot(); + + CPASAttenuationFilter filter3( this ); + // the first round of the three round burst plays the sound and puts a sound in the world sound list. + EmitSound( filter3, entindex(), "HGrunt.9MM" ); + } + else + { + Shotgun( ); + + CPASAttenuationFilter filter4( this ); + EmitSound( filter4, entindex(), "HGrunt.Shotgun" ); + } + + CSoundEnt::InsertSound ( SOUND_COMBAT, GetAbsOrigin(), 384, 0.3 ); + } + break; + + case HGRUNT_AE_BURST2: + case HGRUNT_AE_BURST3: + Shoot(); + break; + + case HGRUNT_AE_KICK: + { + CBaseEntity *pHurt = Kick(); + + if ( pHurt ) + { + // SOUND HERE! + Vector forward, up; + AngleVectors( GetAbsAngles(), &forward, NULL, &up ); + + if ( pHurt->GetFlags() & ( FL_NPC | FL_CLIENT ) ) + pHurt->ViewPunch( QAngle( 15, 0, 0) ); + + // Don't give velocity or damage to the world + if( pHurt->entindex() > 0 ) + { + pHurt->ApplyAbsVelocityImpulse( forward * 100 + up * 50 ); + + CTakeDamageInfo info( this, this, sk_hgrunt_kick.GetFloat(), DMG_CLUB ); + CalculateMeleeDamageForce( &info, forward, pHurt->GetAbsOrigin() ); + pHurt->TakeDamage( info ); + } + } + } + break; + + case HGRUNT_AE_CAUGHT_ENEMY: + { + if ( FOkToSpeak() ) + { + SENTENCEG_PlayRndSz( edict(), "HG_ALERT", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + JustSpoke(); + } + + } + + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } +} + +void CNPC_HGrunt::SetAim( const Vector &aimDir ) +{ + QAngle angDir; + VectorAngles( aimDir, angDir ); + + float curPitch = GetPoseParameter( "XR" ); + float newPitch = curPitch + UTIL_AngleDiff( UTIL_ApproachAngle( angDir.x, curPitch, 60 ), curPitch ); + + SetPoseParameter( "XR", -newPitch ); +} + +//========================================================= +// Shoot +//========================================================= +void CNPC_HGrunt::Shoot ( void ) +{ + if ( GetEnemy() == NULL ) + return; + + Vector vecShootOrigin = Weapon_ShootPosition(); + Vector vecShootDir = GetShootEnemyDir( vecShootOrigin ); + + Vector forward, right, up; + AngleVectors( GetAbsAngles(), &forward, &right, &up ); + + Vector vecShellVelocity = right * random->RandomFloat(40,90) + up * random->RandomFloat( 75,200 ) + forward * random->RandomFloat( -40, 40 ); + EjectShell( vecShootOrigin - vecShootDir * 24, vecShellVelocity, GetAbsAngles().y, 0 ); + FireBullets(1, vecShootOrigin, vecShootDir, VECTOR_CONE_10DEGREES, 2048, m_iAmmoType ); // shoot +-5 degrees + + DoMuzzleFlash(); + + m_cAmmoLoaded--;// take away a bullet! + + SetAim( vecShootDir ); +} + +//========================================================= +// Shoot +//========================================================= +void CNPC_HGrunt::Shotgun ( void ) +{ + if ( GetEnemy() == NULL ) + return; + + Vector vecShootOrigin = Weapon_ShootPosition(); + Vector vecShootDir = GetShootEnemyDir( vecShootOrigin ); + + Vector forward, right, up; + AngleVectors( GetAbsAngles(), &forward, &right, &up ); + + Vector vecShellVelocity = right * random->RandomFloat(40,90) + up * random->RandomFloat( 75,200 ) + forward * random->RandomFloat( -40, 40 ); + EjectShell( vecShootOrigin - vecShootDir * 24, vecShellVelocity, GetAbsAngles().y, 1 ); + FireBullets( sk_hgrunt_pellets.GetFloat(), vecShootOrigin, vecShootDir, VECTOR_CONE_15DEGREES, 2048, m_iAmmoType, 0 ); // shoot +-7.5 degrees + + DoMuzzleFlash(); + + m_cAmmoLoaded--;// take away a bullet! + + SetAim( vecShootDir ); +} + +//========================================================= +// start task +//========================================================= +void CNPC_HGrunt::StartTask ( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_GRUNT_CHECK_FIRE: + if ( !NoFriendlyFire() ) + { + SetCondition( COND_WEAPON_BLOCKED_BY_FRIEND ); + } + TaskComplete(); + break; + + case TASK_GRUNT_SPEAK_SENTENCE: + SpeakSentence(); + TaskComplete(); + break; + + case TASK_WALK_PATH: + case TASK_RUN_PATH: + // grunt no longer assumes he is covered if he moves + Forget( bits_MEMORY_INCOVER ); + BaseClass ::StartTask( pTask ); + break; + + case TASK_RELOAD: + SetIdealActivity( ACT_RELOAD ); + break; + + case TASK_GRUNT_FACE_TOSS_DIR: + break; + + case TASK_FACE_IDEAL: + case TASK_FACE_ENEMY: + BaseClass::StartTask( pTask ); + if (GetMoveType() == MOVETYPE_FLYGRAVITY) + { + SetIdealActivity( ACT_GLIDE ); + } + break; + + default: + BaseClass::StartTask( pTask ); + break; + } +} + +//========================================================= +// RunTask +//========================================================= +void CNPC_HGrunt::RunTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_GRUNT_FACE_TOSS_DIR: + { + // project a point along the toss vector and turn to face that point. + GetMotor()->SetIdealYawToTargetAndUpdate( GetAbsOrigin() + m_vecTossVelocity * 64, AI_KEEP_YAW_SPEED ); + + if ( FacingIdeal() ) + { + TaskComplete(); + } + break; + } + default: + { + BaseClass::RunTask( pTask ); + break; + } + } +} + +//========================================================= +// PainSound +//========================================================= +void CNPC_HGrunt::PainSound( const CTakeDamageInfo &info ) +{ + if ( gpGlobals->curtime > m_flNextPainTime ) + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "HGrunt.Pain" ); + + m_flNextPainTime = gpGlobals->curtime + 1; + } +} + +//========================================================= +// DeathSound +//========================================================= +void CNPC_HGrunt::DeathSound( const CTakeDamageInfo &info ) +{ + CPASAttenuationFilter filter( this, ATTN_IDLE ); + EmitSound( filter, entindex(), "HGrunt.Die" ); +} + +//========================================================= +// SetActivity +//========================================================= +Activity CNPC_HGrunt::NPC_TranslateActivity( Activity eNewActivity ) +{ + switch ( eNewActivity) + { + case ACT_RANGE_ATTACK1: + // grunt is either shooting standing or shooting crouched + if (FBitSet( m_iWeapons, HGRUNT_9MMAR)) + { + if ( m_fStanding ) + { + // get aimable sequence + return (Activity)ACT_GRUNT_MP5_STANDING; + } + else + { + // get crouching shoot + return (Activity)ACT_GRUNT_MP5_CROUCHING; + } + } + else + { + if ( m_fStanding ) + { + // get aimable sequence + return (Activity)ACT_GRUNT_SHOTGUN_STANDING; + } + else + { + // get crouching shoot + return (Activity)ACT_GRUNT_SHOTGUN_CROUCHING; + } + } + break; + case ACT_RANGE_ATTACK2: + // grunt is going to a secondary long range attack. This may be a thrown + // grenade or fired grenade, we must determine which and pick proper sequence + if ( m_iWeapons & HGRUNT_HANDGRENADE ) + { + // get toss anim + return (Activity)ACT_GRUNT_TOSS_GRENADE; + } + else + { + // get launch anim + return (Activity)ACT_GRUNT_LAUNCH_GRENADE; + } + break; + case ACT_RUN: + if ( m_iHealth <= HGRUNT_LIMP_HEALTH ) + { + // limp! + return ACT_RUN_HURT; + } + else + { + return eNewActivity; + } + break; + case ACT_WALK: + if ( m_iHealth <= HGRUNT_LIMP_HEALTH ) + { + // limp! + return ACT_WALK_HURT; + } + else + { + return eNewActivity; + } + break; + case ACT_IDLE: + if ( m_NPCState == NPC_STATE_COMBAT ) + { + eNewActivity = ACT_IDLE_ANGRY; + } + + break; + } + + return BaseClass::NPC_TranslateActivity( eNewActivity ); +} + +void CNPC_HGrunt::ClearAttackConditions( void ) +{ + bool fCanRangeAttack2 = HasCondition( COND_CAN_RANGE_ATTACK2 ); + + // Call the base class. + BaseClass::ClearAttackConditions(); + + if( fCanRangeAttack2 ) + { + // We don't allow the base class to clear this condition because we + // don't sense for it every frame. + SetCondition( COND_CAN_RANGE_ATTACK2 ); + } +} + +int CNPC_HGrunt::SelectSchedule( void ) +{ + // clear old sentence + m_iSentence = HGRUNT_SENT_NONE; + + // flying? If PRONE, barnacle has me. IF not, it's assumed I am rapelling. + if ( GetMoveType() == MOVETYPE_FLYGRAVITY && m_NPCState != NPC_STATE_PRONE ) + { + if (GetFlags() & FL_ONGROUND) + { + // just landed + SetMoveType( MOVETYPE_STEP ); + SetGravity( 1.0 ); + return SCHED_GRUNT_REPEL_LAND; + } + else + { + // repel down a rope, + if ( m_NPCState == NPC_STATE_COMBAT ) + return SCHED_GRUNT_REPEL_ATTACK; + else + return SCHED_GRUNT_REPEL; + } + } + + // grunts place HIGH priority on running away from danger sounds. + if ( HasCondition ( COND_HEAR_DANGER ) ) + { + // dangerous sound nearby! + + //!!!KELLY - currently, this is the grunt's signal that a grenade has landed nearby, + // and the grunt should find cover from the blast + // good place for "SHIT!" or some other colorful verbal indicator of dismay. + // It's not safe to play a verbal order here "Scatter", etc cause + // this may only affect a single individual in a squad. + + if (FOkToSpeak()) + { + SENTENCEG_PlayRndSz( edict(), "HG_GREN", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + JustSpoke(); + } + + return SCHED_TAKE_COVER_FROM_BEST_SOUND; + } + + switch ( m_NPCState ) + { + + case NPC_STATE_PRONE: + { + if (m_bInBarnacleMouth) + { + return SCHED_GRUNT_BARNACLE_CHOMP; + } + else + { + return SCHED_GRUNT_BARNACLE_HIT; + } + } + + case NPC_STATE_COMBAT: + { +// dead enemy + if ( HasCondition( COND_ENEMY_DEAD ) ) + { + // call base class, all code to handle dead enemies is centralized there. + return BaseClass::SelectSchedule(); + } + +// new enemy + if ( HasCondition( COND_NEW_ENEMY) ) + { + if ( m_pSquad ) + { + if ( !m_pSquad->IsLeader( this ) ) + { + return SCHED_TAKE_COVER_FROM_ENEMY; + } + else + { + //!!!KELLY - the leader of a squad of grunts has just seen the player or a + // monster and has made it the squad's enemy. You + // can check pev->flags for FL_CLIENT to determine whether this is the player + // or a monster. He's going to immediately start + // firing, though. If you'd like, we can make an alternate "first sight" + // schedule where the leader plays a handsign anim + // that gives us enough time to hear a short sentence or spoken command + // before he starts pluggin away. + if (FOkToSpeak())// && RANDOM_LONG(0,1)) + { + if ((GetEnemy() != NULL) && GetEnemy()->IsPlayer()) + // player + SENTENCEG_PlayRndSz( edict(), "HG_ALERT", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + else if ((GetEnemy() != NULL) && + (GetEnemy()->Classify() != CLASS_PLAYER_ALLY) && + (GetEnemy()->Classify() != CLASS_HUMAN_PASSIVE) && + (GetEnemy()->Classify() != CLASS_MACHINE) ) + // monster + SENTENCEG_PlayRndSz( edict(), "HG_MONST", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + + JustSpoke(); + } + + if ( HasCondition ( COND_CAN_RANGE_ATTACK1 ) ) + { + return SCHED_GRUNT_SUPPRESS; + } + else + { + return SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE; + } + } + } + } +// no ammo + else if ( HasCondition ( COND_NO_PRIMARY_AMMO ) ) + { + //!!!KELLY - this individual just realized he's out of bullet ammo. + // He's going to try to find cover to run to and reload, but rarely, if + // none is available, he'll drop and reload in the open here. + return SCHED_GRUNT_HIDE_RELOAD; + } + +// damaged just a little + else if ( HasCondition( COND_LIGHT_DAMAGE ) ) + { + // if hurt: + // 90% chance of taking cover + // 10% chance of flinch. + int iPercent = random->RandomInt(0,99); + + if ( iPercent <= 90 && GetEnemy() != NULL ) + { + // only try to take cover if we actually have an enemy! + + //!!!KELLY - this grunt was hit and is going to run to cover. + if (FOkToSpeak()) // && RANDOM_LONG(0,1)) + { + //SENTENCEG_PlayRndSz( ENT(pev), "HG_COVER", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + m_iSentence = HGRUNT_SENT_COVER; + //JustSpoke(); + } + return SCHED_TAKE_COVER_FROM_ENEMY; + } + else + { + return SCHED_SMALL_FLINCH; + } + } +// can kick + else if ( HasCondition( COND_CAN_MELEE_ATTACK1 ) ) + { + return SCHED_MELEE_ATTACK1; + } +// can grenade launch + + else if ( FBitSet( m_iWeapons, HGRUNT_GRENADELAUNCHER) && HasCondition ( COND_CAN_RANGE_ATTACK2 ) && OccupyStrategySlotRange( SQUAD_SLOT_GRENADE1, SQUAD_SLOT_GRENADE2 ) ) + { + // shoot a grenade if you can + return SCHED_RANGE_ATTACK2; + } +// can shoot + else if ( HasCondition ( COND_CAN_RANGE_ATTACK1 ) ) + { + if ( m_pSquad ) + { + if ( m_pSquad->GetLeader() != NULL ) + { + + CAI_BaseNPC *pSquadLeader = m_pSquad->GetLeader()->MyNPCPointer(); + + // if the enemy has eluded the squad and a squad member has just located the enemy + // and the enemy does not see the squad member, issue a call to the squad to waste a + // little time and give the player a chance to turn. + if ( pSquadLeader && pSquadLeader->EnemyHasEludedMe() && !HasCondition ( COND_ENEMY_FACING_ME ) ) + { + return SCHED_GRUNT_FOUND_ENEMY; + } + } + } + + if ( OccupyStrategySlotRange ( SQUAD_SLOT_ENGAGE1, SQUAD_SLOT_ENGAGE2 ) ) + { + // try to take an available ENGAGE slot + return SCHED_RANGE_ATTACK1; + } + else if ( HasCondition ( COND_CAN_RANGE_ATTACK2 ) && OccupyStrategySlotRange( SQUAD_SLOT_GRENADE1, SQUAD_SLOT_GRENADE2 ) ) + { + // throw a grenade if can and no engage slots are available + return SCHED_RANGE_ATTACK2; + } + else + { + // hide! + return SCHED_TAKE_COVER_FROM_ENEMY; + } + } +// can't see enemy + else if ( HasCondition( COND_ENEMY_OCCLUDED ) ) + { + if ( HasCondition( COND_CAN_RANGE_ATTACK2 ) && OccupyStrategySlotRange( SQUAD_SLOT_GRENADE1, SQUAD_SLOT_GRENADE2 ) ) + { + //!!!KELLY - this grunt is about to throw or fire a grenade at the player. Great place for "fire in the hole" "frag out" etc + if (FOkToSpeak()) + { + SENTENCEG_PlayRndSz( edict(), "HG_THROW", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + JustSpoke(); + } + return SCHED_RANGE_ATTACK2; + } + else if ( OccupyStrategySlotRange ( SQUAD_SLOT_ENGAGE1, SQUAD_SLOT_ENGAGE2 ) ) + { + //!!!KELLY - grunt cannot see the enemy and has just decided to + // charge the enemy's position. + if (FOkToSpeak())// && RANDOM_LONG(0,1)) + { + //SENTENCEG_PlayRndSz( ENT(pev), "HG_CHARGE", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + m_iSentence = HGRUNT_SENT_CHARGE; + //JustSpoke(); + } + + return SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE; + } + else + { + //!!!KELLY - grunt is going to stay put for a couple seconds to see if + // the enemy wanders back out into the open, or approaches the + // grunt's covered position. Good place for a taunt, I guess? + if (FOkToSpeak() && random->RandomInt(0,1)) + { + SENTENCEG_PlayRndSz( edict(), "HG_TAUNT", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + JustSpoke(); + } + return SCHED_STANDOFF; + } + } + + if ( HasCondition( COND_SEE_ENEMY ) && !HasCondition ( COND_CAN_RANGE_ATTACK1 ) ) + { + return SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE; + } + } + case NPC_STATE_ALERT: + if ( HasCondition( COND_ENEMY_DEAD ) && SelectWeightedSequence( ACT_VICTORY_DANCE ) != ACTIVITY_NOT_AVAILABLE ) + { + // Scan around for new enemies + return SCHED_VICTORY_DANCE; + } + break; + } + + return BaseClass::SelectSchedule(); +} + +int CNPC_HGrunt::TranslateSchedule( int scheduleType ) +{ + + if ( scheduleType == SCHED_CHASE_ENEMY_FAILED ) + { + return SCHED_ESTABLISH_LINE_OF_FIRE; + } + switch ( scheduleType ) + { + case SCHED_TAKE_COVER_FROM_ENEMY: + { + if ( m_pSquad ) + { + if ( g_iSkillLevel == SKILL_HARD && HasCondition( COND_CAN_RANGE_ATTACK2 ) && OccupyStrategySlotRange( SQUAD_SLOT_GRENADE1, SQUAD_SLOT_GRENADE2 ) ) + { + if (FOkToSpeak()) + { + SENTENCEG_PlayRndSz( edict(), "HG_THROW", HGRUNT_SENTENCE_VOLUME, GRUNT_SNDLVL, 0, m_voicePitch); + JustSpoke(); + } + return SCHED_GRUNT_TOSS_GRENADE_COVER; + } + else + { + return SCHED_GRUNT_TAKE_COVER; + } + } + else + { + if ( random->RandomInt(0,1) ) + { + return SCHED_GRUNT_TAKE_COVER; + } + else + { + return SCHED_GRUNT_GRENADE_COVER; + } + } + } + case SCHED_GRUNT_TAKE_COVER_FAILED: + { + if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) && OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) ) + { + return SCHED_RANGE_ATTACK1; + } + + return SCHED_FAIL; + } + break; + + case SCHED_RANGE_ATTACK1: + { + // randomly stand or crouch + if ( random->RandomInt( 0,9 ) == 0) + { + m_fStanding = random->RandomInt( 0, 1 ) != 0; + } + + if ( m_fStanding ) + return SCHED_GRUNT_RANGE_ATTACK1B; + else + return SCHED_GRUNT_RANGE_ATTACK1A; + } + + case SCHED_RANGE_ATTACK2: + { + return SCHED_GRUNT_RANGE_ATTACK2; + } + case SCHED_VICTORY_DANCE: + { + if ( m_pSquad ) + { + if ( !m_pSquad->IsLeader( this ) ) + { + return SCHED_GRUNT_FAIL; + } + } + + return SCHED_GRUNT_VICTORY_DANCE; + } + case SCHED_GRUNT_SUPPRESS: + { + if ( GetEnemy()->IsPlayer() && m_fFirstEncounter ) + { + m_fFirstEncounter = FALSE;// after first encounter, leader won't issue handsigns anymore when he has a new enemy + return SCHED_GRUNT_SIGNAL_SUPPRESS; + } + else + { + return SCHED_GRUNT_SUPPRESS; + } + } + case SCHED_FAIL: + { + if ( GetEnemy() != NULL ) + { + // grunt has an enemy, so pick a different default fail schedule most likely to help recover. + return SCHED_GRUNT_COMBAT_FAIL; + } + + return SCHED_GRUNT_FAIL; + } + case SCHED_GRUNT_REPEL: + { + Vector vecVel = GetAbsVelocity(); + if ( vecVel.z > -128 ) + { + vecVel.z -= 32; + SetAbsVelocity( vecVel ); + } + + return SCHED_GRUNT_REPEL; + } + case SCHED_GRUNT_REPEL_ATTACK: + { + Vector vecVel = GetAbsVelocity(); + if ( vecVel.z > -128 ) + { + vecVel.z -= 32; + SetAbsVelocity( vecVel ); + } + + return SCHED_GRUNT_REPEL_ATTACK; + } + default: + { + return BaseClass::TranslateSchedule( scheduleType ); + } + } +} + +//========================================================= +// CHGruntRepel - when triggered, spawns a monster_human_grunt +// repelling down a line. +//========================================================= + +class CNPC_HGruntRepel:public CAI_BaseNPC +{ + DECLARE_CLASS( CNPC_HGruntRepel, CAI_BaseNPC ); +public: + void Spawn( void ); + void Precache( void ); + void RepelUse ( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + int m_iSpriteTexture; // Don't save, precache + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( monster_grunt_repel, CNPC_HGruntRepel ); + + + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CNPC_HGruntRepel ) + DEFINE_USEFUNC( RepelUse ), + //DEFINE_FIELD( m_iSpriteTexture, FIELD_INTEGER ), +END_DATADESC() + +void CNPC_HGruntRepel::Spawn( void ) +{ + Precache( ); + SetSolid( SOLID_NONE ); + + SetUse( &CNPC_HGruntRepel::RepelUse ); +} + +void CNPC_HGruntRepel::Precache( void ) +{ + UTIL_PrecacheOther( "monster_human_grunt" ); + m_iSpriteTexture = PrecacheModel( "sprites/rope.vmt" ); +} + +void CNPC_HGruntRepel::RepelUse ( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + trace_t tr; + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + Vector( 0, 0, -4096.0), MASK_NPCSOLID, this,COLLISION_GROUP_NONE, &tr); + + CBaseEntity *pEntity = Create( "monster_human_grunt", GetAbsOrigin(), GetAbsAngles() ); + CAI_BaseNPC *pGrunt = pEntity->MyNPCPointer( ); + pGrunt->SetMoveType( MOVETYPE_FLYGRAVITY ); + pGrunt->SetGravity( 0.001 ); + pGrunt->SetAbsVelocity( Vector( 0, 0, random->RandomFloat( -196, -128 ) ) ); + pGrunt->SetActivity( ACT_GLIDE ); + // UNDONE: position? + pGrunt->m_vecLastPosition = tr.endpos; + + CBeam *pBeam = CBeam::BeamCreate( "sprites/rope.vmt", 10 ); + pBeam->PointEntInit( GetAbsOrigin() + Vector(0,0,112), pGrunt ); + pBeam->SetBeamFlags( FBEAM_SOLID ); + pBeam->SetColor( 255, 255, 255 ); + pBeam->SetThink( &CBaseEntity::SUB_Remove ); + SetNextThink( gpGlobals->curtime + -4096.0 * tr.fraction / pGrunt->GetAbsVelocity().z + 0.5 ); + + UTIL_Remove( this ); +} + + +//------------------------------------------------------------------------------ +// +// Schedules +// +//------------------------------------------------------------------------------ +AI_BEGIN_CUSTOM_NPC( monster_human_grunt, CNPC_HGrunt ) + + DECLARE_ACTIVITY( ACT_GRUNT_LAUNCH_GRENADE ) + DECLARE_ACTIVITY( ACT_GRUNT_TOSS_GRENADE ) + DECLARE_ACTIVITY( ACT_GRUNT_MP5_STANDING ); + DECLARE_ACTIVITY( ACT_GRUNT_MP5_CROUCHING ); + DECLARE_ACTIVITY( ACT_GRUNT_SHOTGUN_STANDING ); + DECLARE_ACTIVITY( ACT_GRUNT_SHOTGUN_CROUCHING ); + + DECLARE_CONDITION( COND_GRUNT_NOFIRE ) + + DECLARE_TASK( TASK_GRUNT_FACE_TOSS_DIR ) + DECLARE_TASK( TASK_GRUNT_SPEAK_SENTENCE ) + DECLARE_TASK( TASK_GRUNT_CHECK_FIRE ) + + DECLARE_SQUADSLOT( SQUAD_SLOT_GRENADE1 ) + DECLARE_SQUADSLOT( SQUAD_SLOT_GRENADE2 ) + DECLARE_SQUADSLOT( SQUAD_SLOT_ENGAGE1 ) + DECLARE_SQUADSLOT( SQUAD_SLOT_ENGAGE2 ) + + //========================================================= + // > SCHED_GRUNT_FAIL + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_FAIL, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT 0" + " TASK_WAIT_PVS 0" + " " + " Interrupts" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_MELEE_ATTACK1" + ) + + //========================================================= + // > SCHED_GRUNT_COMBAT_FAIL + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_COMBAT_FAIL, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT_FACE_ENEMY 2" + " TASK_WAIT_PVS 0" + " " + " Interrupts" + " COND_CAN_RANGE_ATTACK1" + ) + + //========================================================= + // > SCHED_GRUNT_VICTORY_DANCE + // Victory dance! + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_VICTORY_DANCE, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_WAIT 1.5" + " TASK_GET_PATH_TO_ENEMY_CORPSE 0" + " TASK_WALK_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_FACE_ENEMY 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_VICTORY_DANCE" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + ) + + //========================================================= + // > SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE + // Establish line of fire - move to a position that allows + // the grunt to attack. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE_RETRY" + " TASK_GET_PATH_TO_ENEMY 0" + " TASK_GRUNT_SPEAK_SENTENCE 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + " COND_CAN_MELEE_ATTACK2" + " COND_HEAR_DANGER" + ) + + //========================================================= + // This is a schedule I added that borrows some HL2 technology + // to be smarter in cases where HL1 was pretty dumb. I've wedged + // this between ESTABLISH_LINE_OF_FIRE and TAKE_COVER_FROM_ENEMY (sjb) + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE_RETRY, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_GRUNT_TAKE_COVER_FROM_ENEMY" + " TASK_GET_PATH_TO_ENEMY_LKP_LOS 0" + " TASK_GRUNT_SPEAK_SENTENCE 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + " COND_CAN_MELEE_ATTACK2" + " COND_HEAR_DANGER" + ) + + //========================================================= + // > SCHED_GRUNT_FOUND_ENEMY + // Grunt established sight with an enemy + // that was hiding from the squad. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_FOUND_ENEMY, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_SIGNAL1" + " " + " Interrupts" + " COND_HEAR_DANGER" + ) + + + //========================================================= + // > SCHED_GRUNT_COMBAT_FACE + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_COMBAT_FACE, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_FACE_ENEMY 0" + " TASK_WAIT 1.5" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_GRUNT_SWEEP" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + ) + + + //========================================================= + // > SCHED_GRUNT_SIGNAL_SUPPRESS + // Suppressing fire - don't stop shooting until the clip is + // empty or grunt gets hurt. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_SIGNAL_SUPPRESS, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_IDEAL 0" + " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_SIGNAL2" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " " + " Interrupts" + " COND_ENEMY_DEAD" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_GRUNT_NOFIRE" + " COND_NO_PRIMARY_AMMO" + " COND_HEAR_DANGER" + ) + + //========================================================= + // > SCHED_GRUNT_SUPPRESS + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_SUPPRESS, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " " + " Interrupts" + " COND_ENEMY_DEAD" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_GRUNT_NOFIRE" + " COND_NO_PRIMARY_AMMO" + " COND_HEAR_DANGER" + ) + + //========================================================= + // > SCHED_GRUNT_WAIT_IN_COVER + // grunt wait in cover - we don't allow danger or the ability + // to attack to break a grunt's run to cover schedule, but + // when a grunt is in cover, we do want them to attack if they can. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_WAIT_IN_COVER, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT_FACE_ENEMY 1" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_HEAR_DANGER" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_MELEE_ATTACK2" + ) + + + //========================================================= + // > SCHED_GRUNT_TAKE_COVER + // !!!BUGBUG - set a decent fail schedule here. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_TAKE_COVER, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_GRUNT_TAKE_COVER_FAILED" + " TASK_WAIT 0.2" + " TASK_FIND_COVER_FROM_ENEMY 0" + " TASK_GRUNT_SPEAK_SENTENCE 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_REMEMBER MEMORY:INCOVER" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_GRUNT_WAIT_IN_COVER" + " " + " Interrupts" + ) + + + //========================================================= + // > SCHED_GRUNT_GRENADE_COVER + // drop grenade then run to cover. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_GRENADE_COVER, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FIND_COVER_FROM_ENEMY 99" + " TASK_FIND_FAR_NODE_COVER_FROM_ENEMY 384" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_SPECIAL_ATTACK1" + " TASK_CLEAR_MOVE_WAIT 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_GRUNT_WAIT_IN_COVER" + " " + " Interrupts" + ) + + //========================================================= + // > SCHED_GRUNT_TOSS_GRENADE_COVER + // drop grenade then run to cover. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_TOSS_GRENADE_COVER, + + " Tasks" + " TASK_FACE_ENEMY 0" + " TASK_RANGE_ATTACK2 0" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_GRUNT_TAKE_COVER_FROM_ENEMY" + " " + " Interrupts" + ) + + //========================================================= + // > SCHED_GRUNT_HIDE_RELOAD + // Grunt reload schedule + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_HIDE_RELOAD, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_GRUNT_RELOAD" + " TASK_FIND_COVER_FROM_ENEMY 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_REMEMBER MEMORY:INCOVER" + " TASK_FACE_ENEMY 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_RELOAD" + " " + " Interrupts" + " COND_HEAVY_DAMAGE" + " COND_HEAR_DANGER" + ) + + //========================================================= + // > SCHED_GRUNT_SWEEP + // Do a turning sweep of the area + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_SWEEP, + + " Tasks" + " TASK_TURN_LEFT 179" + " TASK_WAIT 1" + " TASK_TURN_LEFT 179" + " TASK_WAIT 1" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + " COND_HEAR_WORLD" + " COND_HEAR_DANGER" + " COND_HEAR_PLAYER" + ) + + //========================================================= + // > SCHED_GRUNT_RANGE_ATTACK1A + // primary range attack. Overriden because base class stops attacking when the enemy is occluded. + // grunt's grenade toss requires the enemy be occluded. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_RANGE_ATTACK1A, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_CROUCH" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_HEAVY_DAMAGE" + " COND_ENEMY_OCCLUDED" + " COND_HEAR_DANGER" + " COND_GRUNT_NOFIRE" + " COND_NO_PRIMARY_AMMO" + ) + + //========================================================= + // > SCHED_GRUNT_RANGE_ATTACK1B + // primary range attack. Overriden because base class stops attacking when the enemy is occluded. + // grunt's grenade toss requires the enemy be occluded. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_RANGE_ATTACK1B, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_IDLE_ANGRY" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " TASK_FACE_ENEMY 0" + " TASK_GRUNT_CHECK_FIRE 0" + " TASK_RANGE_ATTACK1 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_HEAVY_DAMAGE" + " COND_ENEMY_OCCLUDED" + " COND_HEAR_DANGER" + " COND_GRUNT_NOFIRE" + " COND_NO_PRIMARY_AMMO" + ) + + //========================================================= + // > SCHED_GRUNT_RANGE_ATTACK2 + // secondary range attack. Overriden because base class stops attacking when the enemy is occluded. + // grunt's grenade toss requires the enemy be occluded. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_RANGE_ATTACK2, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_GRUNT_FACE_TOSS_DIR 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_RANGE_ATTACK2" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_GRUNT_WAIT_IN_COVER" // don't run immediately after throwing grenade. + " " + " Interrupts" + ) + + //========================================================= + // > SCHED_GRUNT_REPEL + // secondary range attack. Overriden because base class stops attacking when the enemy is occluded. + // grunt's grenade toss requires the enemy be occluded. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_REPEL, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_IDEAL 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_GLIDE" + " " + " Interrupts" + " COND_SEE_ENEMY" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_HEAR_DANGER" + " COND_HEAR_PLAYER" + " COND_HEAR_COMBAT" + ) + + //========================================================= + // > SCHED_GRUNT_REPEL_ATTACK + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_REPEL_ATTACK, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_FLY" + " " + " Interrupts" + " COND_ENEMY_OCCLUDED" + ) + + //========================================================= + // > SCHED_GRUNT_REPEL_LAND + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_REPEL_LAND, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_LAND" + " TASK_GET_PATH_TO_LASTPOSITION 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_CLEAR_LASTPOSITION 0" + " " + " Interrupts" + " COND_SEE_ENEMY" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_HEAR_DANGER" + " COND_HEAR_COMBAT" + " COND_HEAR_PLAYER" + ) + + //========================================================= + // > SCHED_GRUNT_RELOAD + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_RELOAD, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_RELOAD" + " " + " Interrupts" + " COND_HEAVY_DAMAGE" + ) + + //========================================================= + // > SCHED_GRUNT_TAKE_COVER_FROM_ENEMY + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_TAKE_COVER_FROM_ENEMY, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_WAIT 0.2" + " TASK_FIND_COVER_FROM_ENEMY 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_REMEMBER MEMORY:INCOVER" + " TASK_FACE_ENEMY 0" + " TASK_WAIT 1" + " " + " Interrupts" + " COND_NEW_ENEMY" + ) + + //========================================================= + // > SCHED_GRUNT_TAKE_COVER_FAILED + // special schedule type that forces analysis of conditions and picks + // the best possible schedule to recover from this type of failure. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_TAKE_COVER_FAILED, + " Tasks" + " Interrupts" + ) + + //========================================================= + // > SCHED_GRUNT_BARNACLE_HIT + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_BARNACLE_HIT, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_BARNACLE_HIT" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_GRUNT_BARNACLE_PULL" + "" + " Interrupts" + ) + + //========================================================= + // > SCHED_GRUNT_BARNACLE_PULL + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_BARNACLE_PULL, + + " Tasks" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_BARNACLE_PULL" + "" + " Interrupts" + ) + + //========================================================= + // > SCHED_GRUNT_BARNACLE_CHOMP + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_BARNACLE_CHOMP, + + " Tasks" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_BARNACLE_CHOMP" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_GRUNT_BARNACLE_CHEW" + "" + " Interrupts" + ) + + //========================================================= + // > SCHED_GRUNT_BARNACLE_CHEW + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_GRUNT_BARNACLE_CHEW, + + " Tasks" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_BARNACLE_CHEW" + ) + +AI_END_CUSTOM_NPC() + + +//========================================================= +// DEAD HGRUNT PROP +//========================================================= +class CNPC_DeadHGrunt : public CAI_BaseNPC +{ + DECLARE_CLASS( CNPC_DeadHGrunt, CAI_BaseNPC ); +public: + void Spawn( void ); + Class_T Classify ( void ) { return CLASS_HUMAN_MILITARY; } + float MaxYawSpeed( void ) { return 8.0f; } + + bool KeyValue( const char *szKeyName, const char *szValue ); + + int m_iPose;// which sequence to display -- temporary, don't need to save + static char *m_szPoses[3]; +}; + +char *CNPC_DeadHGrunt::m_szPoses[] = { "deadstomach", "deadside", "deadsitting" }; + +bool CNPC_DeadHGrunt::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "pose" ) ) + m_iPose = atoi( szValue ); + else + CAI_BaseNPC::KeyValue( szKeyName, szValue ); + + return true; +} + +LINK_ENTITY_TO_CLASS( monster_hgrunt_dead, CNPC_DeadHGrunt ); + +//========================================================= +// ********** DeadHGrunt SPAWN ********** +//========================================================= +void CNPC_DeadHGrunt::Spawn( void ) +{ + PrecacheModel("models/hgrunt.mdl"); + SetModel( "models/hgrunt.mdl" ); + + ClearEffects(); + SetSequence( 0 ); + m_bloodColor = BLOOD_COLOR_RED; + + SetSequence( LookupSequence( m_szPoses[m_iPose] ) ); + + if ( GetSequence() == -1 ) + { + Msg ( "Dead hgrunt with bad pose\n" ); + } + + // Corpses have less health + m_iHealth = 8; + + // map old bodies onto new bodies + switch( m_nBody ) + { + case 0: // Grunt with Gun + m_nBody = 0; + m_nSkin = 0; + SetBodygroup( HEAD_GROUP, HEAD_GRUNT ); + SetBodygroup( GUN_GROUP, GUN_MP5 ); + break; + case 1: // Commander with Gun + m_nBody = 0; + m_nSkin = 0; + SetBodygroup( HEAD_GROUP, HEAD_COMMANDER ); + SetBodygroup( GUN_GROUP, GUN_MP5 ); + break; + case 2: // Grunt no Gun + m_nBody = 0; + m_nSkin = 0; + SetBodygroup( HEAD_GROUP, HEAD_GRUNT ); + SetBodygroup( GUN_GROUP, GUN_NONE ); + break; + case 3: // Commander no Gun + m_nBody = 0; + m_nSkin = 0; + SetBodygroup( HEAD_GROUP, HEAD_COMMANDER ); + SetBodygroup( GUN_GROUP, GUN_NONE ); + break; + } + + NPCInitDead(); +} diff --git a/game/server/hl1/hl1_npc_hgrunt.h b/game/server/hl1/hl1_npc_hgrunt.h new file mode 100644 index 0000000..af82ebd --- /dev/null +++ b/game/server/hl1/hl1_npc_hgrunt.h @@ -0,0 +1,117 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef NPC_HGRUNT_H +#define NPC_HGRUNT_H + +#include "ai_squad.h" +#include "hl1_ai_basenpc.h" + +class CNPC_HGrunt : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_HGrunt, CHL1BaseNPC ); +public: + + void Precache( void ); + void Spawn( void ); + + void JustSpoke( void ); + void SpeakSentence( void ); + void PrescheduleThink ( void ); + + bool FOkToSpeak( void ); + + Class_T Classify ( void ); + int RangeAttack1Conditions ( float flDot, float flDist ); + int MeleeAttack1Conditions ( float flDot, float flDist ); + int RangeAttack2Conditions ( float flDot, float flDist ); + + Activity NPC_TranslateActivity( Activity eNewActivity ); + + void ClearAttackConditions( void ); + + int IRelationPriority( CBaseEntity *pTarget ); + + int GetGrenadeConditions ( float flDot, float flDist ); + + bool FCanCheckAttacks( void ); + + int GetSoundInterests ( void ); + + void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + int OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ); + + float MaxYawSpeed( void ); + + void IdleSound( void ); + + void CheckAmmo ( void ); + + CBaseEntity *Kick( void ); + + Vector Weapon_ShootPosition( void ); + void HandleAnimEvent( animevent_t *pEvent ); + + void Shoot ( void ); + void Shotgun( void ); + + void StartTask ( const Task_t *pTask ); + void RunTask ( const Task_t *pTask ); + + int SelectSchedule( void ); + int TranslateSchedule( int scheduleType ); + + + void PainSound( const CTakeDamageInfo &info ); + void DeathSound( const CTakeDamageInfo &info ); + void SetAim( const Vector &aimDir ); + + bool HandleInteraction(int interactionType, void *data, CBaseCombatCharacter* sourceEnt); + + void StartNPC ( void ); + + int SquadRecruit( int searchRadius, int maxMembers ); + + void Event_Killed( const CTakeDamageInfo &info ); + + + static const char *pGruntSentences[]; + + bool m_bInBarnacleMouth; + +public: + DECLARE_DATADESC(); + DEFINE_CUSTOM_AI; + +private: + + // checking the feasibility of a grenade toss is kind of costly, so we do it every couple of seconds, + // not every server frame. + float m_flNextGrenadeCheck; + float m_flNextPainTime; + float m_flLastEnemySightTime; + float m_flTalkWaitTime; + + Vector m_vecTossVelocity; + + int m_iLastGrenadeCondition; + bool m_fStanding; + bool m_fFirstEncounter;// only put on the handsign show in the squad's first encounter. + int m_iClipSize; + + int m_voicePitch; + + int m_iSentence; + + float m_flCheckAttackTime; + + int m_iAmmoType; + + int m_iWeapons; +}; + +#endif
\ No newline at end of file diff --git a/game/server/hl1/hl1_npc_hornet.cpp b/game/server/hl1/hl1_npc_hornet.cpp new file mode 100644 index 0000000..d2b09ee --- /dev/null +++ b/game/server/hl1/hl1_npc_hornet.cpp @@ -0,0 +1,400 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "ai_senses.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "basecombatweapon.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "te.h" +#include "hl1_npc_hornet.h" + +int iHornetTrail; +int iHornetPuff; + +LINK_ENTITY_TO_CLASS( hornet, CNPC_Hornet ); + +extern ConVar sk_npc_dmg_hornet; +extern ConVar sk_plr_dmg_hornet; + +BEGIN_DATADESC( CNPC_Hornet ) + DEFINE_FIELD( m_flStopAttack, FIELD_TIME ), + DEFINE_FIELD( m_iHornetType, FIELD_INTEGER ), + DEFINE_FIELD( m_flFlySpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_flDamage, FIELD_INTEGER ), + DEFINE_FIELD( m_vecEnemyLKP, FIELD_POSITION_VECTOR ), + + + DEFINE_ENTITYFUNC( DieTouch ), + DEFINE_THINKFUNC( StartDart ), + DEFINE_THINKFUNC( StartTrack ), + DEFINE_ENTITYFUNC( DartTouch ), + DEFINE_ENTITYFUNC( TrackTouch ), + DEFINE_THINKFUNC( TrackTarget ), +END_DATADESC() + +//========================================================= +//========================================================= +void CNPC_Hornet::Spawn( void ) +{ + Precache(); + + SetMoveType( MOVETYPE_FLY ); + SetSolid( SOLID_BBOX ); + m_takedamage = DAMAGE_YES; + AddFlag( FL_NPC ); + m_iHealth = 1;// weak! + m_bloodColor = DONT_BLEED; + + if ( g_pGameRules->IsMultiplayer() ) + { + // hornets don't live as long in multiplayer + m_flStopAttack = gpGlobals->curtime + 3.5; + } + else + { + m_flStopAttack = gpGlobals->curtime + 5.0; + } + + m_flFieldOfView = 0.9; // +- 25 degrees + + if ( random->RandomInt ( 1, 5 ) <= 2 ) + { + m_iHornetType = HORNET_TYPE_RED; + m_flFlySpeed = HORNET_RED_SPEED; + } + else + { + m_iHornetType = HORNET_TYPE_ORANGE; + m_flFlySpeed = HORNET_ORANGE_SPEED; + } + + SetModel( "models/hornet.mdl" ); + UTIL_SetSize( this, Vector( -4, -4, -4 ), Vector( 4, 4, 4 ) ); + + SetTouch( &CNPC_Hornet::DieTouch ); + SetThink( &CNPC_Hornet::StartTrack ); + + if ( GetOwnerEntity() && (GetOwnerEntity()->GetFlags() & FL_CLIENT) ) + { + m_flDamage = sk_plr_dmg_hornet.GetFloat(); + } + else + { + // no real owner, or owner isn't a client. + m_flDamage = sk_npc_dmg_hornet.GetFloat(); + } + + SetNextThink( gpGlobals->curtime + 0.1f ); + ResetSequenceInfo(); + + m_vecEnemyLKP = vec3_origin; +} + + +void CNPC_Hornet::Precache() +{ + PrecacheModel("models/hornet.mdl"); + + iHornetPuff = PrecacheModel( "sprites/muz1.vmt" ); + iHornetTrail = PrecacheModel("sprites/laserbeam.vmt"); + + PrecacheScriptSound( "Hornet.Die" ); + PrecacheScriptSound( "Hornet.Buzz" ); +} + +//========================================================= +// hornets will never get mad at each other, no matter who the owner is. +//========================================================= +Disposition_t CNPC_Hornet::IRelationType( CBaseEntity *pTarget ) +{ + if ( pTarget->GetModelIndex() == GetModelIndex() ) + { + return D_NU; + } + + return BaseClass::IRelationType( pTarget ); +} + +//========================================================= +// ID's Hornet as their owner +//========================================================= +Class_T CNPC_Hornet::Classify ( void ) +{ + if ( GetOwnerEntity() && (GetOwnerEntity()->GetFlags() & FL_CLIENT) ) + { + return CLASS_PLAYER_BIOWEAPON; + } + + return CLASS_ALIEN_BIOWEAPON; +} + +//========================================================= +// StartDart - starts a hornet out just flying straight. +//========================================================= +void CNPC_Hornet::StartDart ( void ) +{ + IgniteTrail(); + + SetTouch( &CNPC_Hornet::DartTouch ); + + SetThink( &CBaseEntity::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 4 ); +} + + +void CNPC_Hornet::DieTouch ( CBaseEntity *pOther ) +{ + if ( !pOther || !pOther->IsSolid() || pOther->IsSolidFlagSet(FSOLID_VOLUME_CONTENTS) ) + { + return; + } + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Hornet.Die" ); + + CTakeDamageInfo info( this, GetOwnerEntity(), m_flDamage, DMG_BULLET ); + CalculateBulletDamageForce( &info, GetAmmoDef()->Index("Hornet"), GetAbsVelocity(), GetAbsOrigin() ); + pOther->TakeDamage( info ); + + m_takedamage = DAMAGE_NO; + + AddEffects( EF_NODRAW ); + + AddSolidFlags( FSOLID_NOT_SOLID );// intangible + + UTIL_Remove( this ); + SetTouch( NULL ); +} + + +//========================================================= +// StartTrack - starts a hornet out tracking its target +//========================================================= +void CNPC_Hornet:: StartTrack ( void ) +{ + IgniteTrail(); + + SetTouch( &CNPC_Hornet::TrackTouch ); + SetThink( &CNPC_Hornet::TrackTarget ); + + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +void TE_BeamFollow( IRecipientFilter& filter, float delay, + int iEntIndex, int modelIndex, int haloIndex, float life, float width, float endWidth, + float fadeLength,float r, float g, float b, float a ); + +void CNPC_Hornet::IgniteTrail( void ) +{ + Vector vColor; + + if ( m_iHornetType == HORNET_TYPE_RED ) + vColor = Vector ( 179, 39, 14 ); + else + vColor = Vector ( 255, 128, 0 ); + + CBroadcastRecipientFilter filter; + TE_BeamFollow( filter, 0.0, + entindex(), + iHornetTrail, + 0, + 1, + 2, + 0.5, + 0.5, + vColor.x, + vColor.y, + vColor.z, + 128 ); +} + + +unsigned int CNPC_Hornet::PhysicsSolidMaskForEntity( void ) const +{ + unsigned int iMask = BaseClass::PhysicsSolidMaskForEntity(); + + iMask &= ~CONTENTS_MONSTERCLIP; + + return iMask; +} + +//========================================================= +// Tracking Hornet hit something +//========================================================= +void CNPC_Hornet::TrackTouch ( CBaseEntity *pOther ) +{ + if ( !pOther->IsSolid() || pOther->IsSolidFlagSet(FSOLID_VOLUME_CONTENTS) ) + { + return; + } + + if ( pOther == GetOwnerEntity() || pOther->GetModelIndex() == GetModelIndex() ) + {// bumped into the guy that shot it. + //SetSolid( SOLID_NOT ); + return; + } + + int nRelationship = IRelationType( pOther ); + if ( (nRelationship == D_FR || nRelationship == D_NU || nRelationship == D_LI) ) + { + // hit something we don't want to hurt, so turn around. + Vector vecVel = GetAbsVelocity(); + + VectorNormalize( vecVel ); + + vecVel.x *= -1; + vecVel.y *= -1; + + SetAbsOrigin( GetAbsOrigin() + vecVel * 4 ); // bounce the hornet off a bit. + SetAbsVelocity( vecVel * m_flFlySpeed ); + + return; + } + + DieTouch( pOther ); +} + +void CNPC_Hornet::DartTouch( CBaseEntity *pOther ) +{ + DieTouch( pOther ); +} + +//========================================================= +// Hornet is flying, gently tracking target +//========================================================= +void CNPC_Hornet::TrackTarget ( void ) +{ + Vector vecFlightDir; + Vector vecDirToEnemy; + float flDelta; + + StudioFrameAdvance( ); + + if (gpGlobals->curtime > m_flStopAttack) + { + SetTouch( NULL ); + SetThink( &CBaseEntity::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.1f ); + return; + } + + // UNDONE: The player pointer should come back after returning from another level + if ( GetEnemy() == NULL ) + {// enemy is dead. + GetSenses()->Look( 1024 ); + SetEnemy( BestEnemy() ); + } + + if ( GetEnemy() != NULL && FVisible( GetEnemy() )) + { + m_vecEnemyLKP = GetEnemy()->BodyTarget( GetAbsOrigin() ); + } + else + { + m_vecEnemyLKP = m_vecEnemyLKP + GetAbsVelocity() * m_flFlySpeed * 0.1; + } + + vecDirToEnemy = m_vecEnemyLKP - GetAbsOrigin(); + VectorNormalize( vecDirToEnemy ); + + if ( GetAbsVelocity().Length() < 0.1 ) + vecFlightDir = vecDirToEnemy; + else + { + vecFlightDir = GetAbsVelocity(); + VectorNormalize( vecFlightDir ); + } + + SetAbsVelocity( vecFlightDir + vecDirToEnemy ); + + // measure how far the turn is, the wider the turn, the slow we'll go this time. + flDelta = DotProduct ( vecFlightDir, vecDirToEnemy ); + + if ( flDelta < 0.5 ) + {// hafta turn wide again. play sound + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Hornet.Buzz" ); + } + + if ( flDelta <= 0 && m_iHornetType == HORNET_TYPE_RED ) + {// no flying backwards, but we don't want to invert this, cause we'd go fast when we have to turn REAL far. + flDelta = 0.25; + } + + Vector vecVel = vecFlightDir + vecDirToEnemy; + VectorNormalize( vecVel ); + + if ( GetOwnerEntity() && (GetOwnerEntity()->GetFlags() & FL_NPC) ) + { + // random pattern only applies to hornets fired by monsters, not players. + + vecVel.x += random->RandomFloat ( -0.10, 0.10 );// scramble the flight dir a bit. + vecVel.y += random->RandomFloat ( -0.10, 0.10 ); + vecVel.z += random->RandomFloat ( -0.10, 0.10 ); + } + + switch ( m_iHornetType ) + { + case HORNET_TYPE_RED: + SetAbsVelocity( vecVel * ( m_flFlySpeed * flDelta ) );// scale the dir by the ( speed * width of turn ) + SetNextThink( gpGlobals->curtime + random->RandomFloat( 0.1, 0.3 ) ); + break; + default: + Assert( false ); //fall through if release + case HORNET_TYPE_ORANGE: + SetAbsVelocity( vecVel * m_flFlySpeed );// do not have to slow down to turn. + SetNextThink( gpGlobals->curtime + 0.1f );// fixed think time + break; + } + + QAngle angNewAngles; + VectorAngles( GetAbsVelocity(), angNewAngles ); + SetAbsAngles( angNewAngles ); + + SetSolid( SOLID_BBOX ); + + // if hornet is close to the enemy, jet in a straight line for a half second. + // (only in the single player game) + if ( GetEnemy() != NULL && !g_pGameRules->IsMultiplayer() ) + { + if ( flDelta >= 0.4 && ( GetAbsOrigin() - m_vecEnemyLKP ).Length() <= 300 ) + { + CPVSFilter filter( GetAbsOrigin() ); + te->Sprite( filter, 0.0, + &GetAbsOrigin(), // pos + iHornetPuff, // model + 0.2, //size + 128 // brightness + ); + + CPASAttenuationFilter filter2( this ); + EmitSound( filter2, entindex(), "Hornet.Buzz" ); + SetAbsVelocity( GetAbsVelocity() * 2 ); + SetNextThink( gpGlobals->curtime + 1.0f ); + // don't attack again + m_flStopAttack = gpGlobals->curtime; + } + } +} diff --git a/game/server/hl1/hl1_npc_hornet.h b/game/server/hl1/hl1_npc_hornet.h new file mode 100644 index 0000000..0f8ccc6 --- /dev/null +++ b/game/server/hl1/hl1_npc_hornet.h @@ -0,0 +1,76 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef NPC_HORNET_H +#define NPC_HORNET_H + +#include "hl1_ai_basenpc.h" + +//========================================================= +// Hornets +//========================================================= + +//========================================================= +// Hornet Defines +//========================================================= +#define HORNET_TYPE_RED 0 +#define HORNET_TYPE_ORANGE 1 +#define HORNET_RED_SPEED (float)600 +#define HORNET_ORANGE_SPEED (float)800 + +extern int iHornetPuff; + +//========================================================= +// Hornet - this is the projectile that the Alien Grunt fires. +//========================================================= +class CNPC_Hornet : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_Hornet, CHL1BaseNPC ); +public: + + void Spawn( void ); + void Precache( void ); + Class_T Classify ( void ); + Disposition_t IRelationType(CBaseEntity *pTarget); + + void DieTouch ( CBaseEntity *pOther ); + void DartTouch( CBaseEntity *pOther ); + void TrackTouch ( CBaseEntity *pOther ); + void TrackTarget ( void ); + void StartDart ( void ); + void IgniteTrail( void ); + void StartTrack(void); + + + virtual unsigned int PhysicsSolidMaskForEntity( void ) const; + virtual bool ShouldGib( const CTakeDamageInfo &info ) { return false; } + + /* virtual int Save( CSave &save ); + virtual int Restore( CRestore &restore ); + static TYPEDESCRIPTION m_SaveData[]; + + + void EXPORT StartTrack ( void ); + + void EXPORT TrackTarget ( void ); + void EXPORT TrackTouch ( CBaseEntity *pOther ); + void EXPORT DartTouch( CBaseEntity *pOther ); + + + int TakeDamage( entvars_t *pevInflictor, entvars_t *pevAttacker, float flDamage, int bitsDamageType );*/ + + float m_flStopAttack; + int m_iHornetType; + float m_flFlySpeed; + int m_flDamage; + + DECLARE_DATADESC(); + + Vector m_vecEnemyLKP; +}; + +#endif //NPC_HORNET_H
\ No newline at end of file diff --git a/game/server/hl1/hl1_npc_houndeye.cpp b/game/server/hl1/hl1_npc_houndeye.cpp new file mode 100644 index 0000000..10ae189 --- /dev/null +++ b/game/server/hl1/hl1_npc_houndeye.cpp @@ -0,0 +1,1287 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Cute hound like Alien. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "game.h" +#include "ai_default.h" +#include "ai_schedule.h" +#include "ai_hull.h" +#include "ai_navigator.h" +#include "ai_route.h" +#include "ai_squad.h" +#include "ai_squadslot.h" +#include "ai_hint.h" +#include "npcevent.h" +#include "animation.h" +#include "hl1_npc_houndeye.h" +#include "gib.h" +#include "soundent.h" +#include "ndebugoverlay.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "movevars_shared.h" + +// houndeye does 20 points of damage spread over a sphere 384 units in diameter, and each additional +// squad member increases the BASE damage by 110%, per the spec. +#define HOUNDEYE_MAX_SQUAD_SIZE 4 +#define HOUNDEYE_SQUAD_BONUS (float)1.1 + +#define HOUNDEYE_EYE_FRAMES 4 // how many different switchable maps for the eye + +#define HOUNDEYE_SOUND_STARTLE_VOLUME 128 // how loud a sound has to be to badly scare a sleeping houndeye + +#define HOUNDEYE_TOP_MASS 300.0f + +ConVar sk_houndeye_health ( "sk_houndeye_health", "20" ); +ConVar sk_houndeye_dmg_blast ( "sk_houndeye_dmg_blast", "15" ); + +static int s_iSquadIndex = 0; + +//========================================================= +// Monster's Anim Events Go Here +//========================================================= +#define HOUND_AE_WARN 1 +#define HOUND_AE_STARTATTACK 2 +#define HOUND_AE_THUMP 3 +#define HOUND_AE_ANGERSOUND1 4 +#define HOUND_AE_ANGERSOUND2 5 +#define HOUND_AE_HOPBACK 6 +#define HOUND_AE_CLOSE_EYE 7 + +BEGIN_DATADESC( CNPC_Houndeye ) + DEFINE_FIELD( m_iSpriteTexture, FIELD_INTEGER ), + DEFINE_FIELD( m_fAsleep, FIELD_BOOLEAN ), + DEFINE_FIELD( m_fDontBlink, FIELD_BOOLEAN ), + DEFINE_FIELD( m_vecPackCenter, FIELD_POSITION_VECTOR ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( monster_houndeye, CNPC_Houndeye ); + +//========================================================= +// monster-specific tasks +//========================================================= +enum +{ + TASK_HOUND_CLOSE_EYE = LAST_SHARED_TASK, + TASK_HOUND_OPEN_EYE, + TASK_HOUND_THREAT_DISPLAY, + TASK_HOUND_FALL_ASLEEP, + TASK_HOUND_WAKE_UP, + TASK_HOUND_HOP_BACK, +}; + +//========================================================= +// monster-specific schedule types +//========================================================= +enum +{ + SCHED_HOUND_AGITATED = LAST_SHARED_SCHEDULE, + SCHED_HOUND_HOP_RETREAT, + SCHED_HOUND_YELL1, + SCHED_HOUND_YELL2, + SCHED_HOUND_RANGEATTACK, + SCHED_HOUND_SLEEP, + SCHED_HOUND_WAKE_LAZY, + SCHED_HOUND_WAKE_URGENT, + SCHED_HOUND_SPECIALATTACK, + SCHED_HOUND_COMBAT_FAIL_PVS, + SCHED_HOUND_COMBAT_FAIL_NOPVS, +// SCHED_HOUND_FAIL, +}; + +enum HoundEyeSquadSlots +{ + SQUAD_SLOTS_HOUND_ATTACK = LAST_SHARED_SQUADSLOT, +}; + + +//========================================================= +// Spawn +//========================================================= +void CNPC_Houndeye::Spawn() +{ + Precache( ); + + SetRenderColor( 255, 255, 255, 255 ); + + SetModel( "models/houndeye.mdl" ); + + SetHullType(HULL_TINY); + SetHullSizeNormal(); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + m_bloodColor = BLOOD_COLOR_YELLOW; + ClearEffects(); + m_iHealth = sk_houndeye_health.GetFloat(); + m_flFieldOfView = 0.5;// indicates the width of this monster's forward view cone ( as a dotproduct result ) + m_NPCState = NPC_STATE_NONE; + m_fAsleep = FALSE; // everyone spawns awake + m_fDontBlink = FALSE; + + CapabilitiesClear(); + + CapabilitiesAdd( bits_CAP_MOVE_GROUND | bits_CAP_INNATE_RANGE_ATTACK1 ); + CapabilitiesAdd( bits_CAP_SQUAD); + + NPCInit(); +} + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CNPC_Houndeye::Precache() +{ + PrecacheModel("models/houndeye.mdl"); + + m_iSpriteTexture = PrecacheModel( "sprites/shockwave.vmt" ); + + PrecacheScriptSound( "HoundEye.Idle" ); + PrecacheScriptSound( "HoundEye.Warn" ); + PrecacheScriptSound( "HoundEye.Hunt" ); + PrecacheScriptSound( "HoundEye.Alert" ); + PrecacheScriptSound( "HoundEye.Die" ); + PrecacheScriptSound( "HoundEye.Pain" ); + PrecacheScriptSound( "HoundEye.Anger1" ); + PrecacheScriptSound( "HoundEye.Anger2" ); + PrecacheScriptSound( "HoundEye.Sonic" ); + + BaseClass::Precache(); +} + +void CNPC_Houndeye::Event_Killed( const CTakeDamageInfo &info ) +{ + // Close the eye to make death more obvious + m_nSkin = 1; + BaseClass::Event_Killed( info ); +} + +int CNPC_Houndeye::RangeAttack1Conditions ( float flDot, float flDist ) +{ + // I'm not allowed to attack if standing in another hound eye + // (note houndeyes allowed to interpenetrate) + trace_t tr; + UTIL_TraceHull( GetAbsOrigin(), GetAbsOrigin() + Vector(0,0,0.1), + GetHullMins(), GetHullMaxs(), + MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr ); + if (tr.startsolid) + { + CBaseEntity *pEntity = tr.m_pEnt; + if (pEntity->Classify() == CLASS_ALIEN_MONSTER) + { + return( COND_NONE ); + } + } + + // If I'm really close to my enemy allow me to attack if + // I'm facing regardless of next attack time + if (flDist < 100 && flDot >= 0.3) + { + return COND_CAN_RANGE_ATTACK1; + } + if ( gpGlobals->curtime < m_flNextAttack ) + { + return( COND_NONE ); + } + if (flDist > ( HOUNDEYE_MAX_ATTACK_RADIUS * 0.5 )) + { + return COND_TOO_FAR_TO_ATTACK; + } + if (flDot < 0.3) + { + return COND_NOT_FACING_ATTACK; + } + return COND_CAN_RANGE_ATTACK1; +} + +//========================================================= +// IdleSound +//========================================================= +void CNPC_Houndeye::IdleSound ( void ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "HoundEye.Idle" ); +} + +//========================================================= +// IdleSound +//========================================================= +void CNPC_Houndeye::WarmUpSound ( void ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(),"HoundEye.Warn" ); +} + +//========================================================= +// WarnSound +//========================================================= +void CNPC_Houndeye::WarnSound ( void ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "HoundEye.Hunt" ); +} + +//========================================================= +// AlertSound +//========================================================= +void CNPC_Houndeye::AlertSound ( void ) +{ + + if ( m_pSquad && !m_pSquad->IsLeader( this ) ) + return; // only leader makes ALERT sound. + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "HoundEye.Alert" ); +} + +//========================================================= +// DeathSound +//========================================================= +void CNPC_Houndeye::DeathSound( const CTakeDamageInfo &info ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "HoundEye.Die" ); +} + +//========================================================= +// PainSound +//========================================================= +void CNPC_Houndeye::PainSound ( const CTakeDamageInfo &info ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "HoundEye.Pain" ); +} + +//========================================================= +// MaxYawSpeed - allows each sequence to have a different +// turn rate associated with it. +//========================================================= +float CNPC_Houndeye::MaxYawSpeed ( void ) +{ + int flYS; + + flYS = 90; + + switch ( GetActivity() ) + { + case ACT_CROUCHIDLE://sleeping! + flYS = 0; + break; + case ACT_IDLE: + flYS = 60; + break; + case ACT_WALK: + flYS = 90; + break; + case ACT_RUN: + flYS = 90; + break; + case ACT_TURN_LEFT: + case ACT_TURN_RIGHT: + flYS = 90; + break; + } + + return flYS; +} + +//========================================================= +// Classify - indicates this monster's place in the +// relationship table. +//========================================================= +Class_T CNPC_Houndeye::Classify ( void ) +{ + return CLASS_ALIEN_MONSTER; +} + +//========================================================= +// HandleAnimEvent - catches the monster-specific messages +// that occur when tagged animation frames are played. +//========================================================= +void CNPC_Houndeye::HandleAnimEvent( animevent_t *pEvent ) +{ + switch ( pEvent->event ) + { + case HOUND_AE_WARN: + // do stuff for this event. + WarnSound(); + break; + + case HOUND_AE_STARTATTACK: + WarmUpSound(); + break; + + case HOUND_AE_HOPBACK: + { + float flGravity = GetCurrentGravity(); + Vector v_forward; + GetVectors( &v_forward, NULL, NULL ); + + SetGroundEntity( NULL ); + + Vector vecVel = v_forward * -200; + vecVel.z += ( 0.6 * flGravity ) * 0.5; + SetAbsVelocity( vecVel ); + + break; + } + + case HOUND_AE_THUMP: + // emit the shockwaves + SonicAttack(); + break; + + case HOUND_AE_ANGERSOUND1: + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "HoundEye.Anger1" ); + } + break; + + case HOUND_AE_ANGERSOUND2: + { + CPASAttenuationFilter filter2( this ); + EmitSound( filter2, entindex(), "HoundEye.Anger2" ); + } + break; + + case HOUND_AE_CLOSE_EYE: + if ( !m_fDontBlink ) + { + m_nSkin = HOUNDEYE_EYE_FRAMES - 1; + } + break; + + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } +} + +//========================================================= +// SonicAttack +//========================================================= +void CNPC_Houndeye::SonicAttack ( void ) +{ + float flAdjustedDamage; + float flDist; + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "HoundEye.Sonic"); + + CBroadcastRecipientFilter filter2; + te->BeamRingPoint( filter2, 0.0, + GetAbsOrigin(), //origin + 16, //start radius + HOUNDEYE_MAX_ATTACK_RADIUS,//end radius + m_iSpriteTexture, //texture + 0, //halo index + 0, //start frame + 0, //framerate + 0.2, //life + 24, //width + 16, //spread + 0, //amplitude + WriteBeamColor().x, //r + WriteBeamColor().y, //g + WriteBeamColor().z, //b + 192, //a + 0 //speed + ); + + CBroadcastRecipientFilter filter3; + te->BeamRingPoint( filter3, 0.0, + GetAbsOrigin(), //origin + 16, //start radius + HOUNDEYE_MAX_ATTACK_RADIUS / 2, //end radius + m_iSpriteTexture, //texture + 0, //halo index + 0, //start frame + 0, //framerate + 0.2, //life + 24, //width + 16, //spread + 0, //amplitude + WriteBeamColor().x, //r + WriteBeamColor().y, //g + WriteBeamColor().z, //b + 192, //a + 0 //speed + ); + + CBaseEntity *pEntity = NULL; + // iterate on all entities in the vicinity. + while ((pEntity = gEntList.FindEntityInSphere( pEntity, GetAbsOrigin(), HOUNDEYE_MAX_ATTACK_RADIUS )) != NULL) + { + if ( pEntity->m_takedamage != DAMAGE_NO ) + { + if ( !FClassnameIs(pEntity, "monster_houndeye") ) + {// houndeyes don't hurt other houndeyes with their attack + + // houndeyes do FULL damage if the ent in question is visible. Half damage otherwise. + // This means that you must get out of the houndeye's attack range entirely to avoid damage. + // Calculate full damage first + + if ( m_pSquad && m_pSquad->NumMembers() > 1 ) + { + // squad gets attack bonus. + flAdjustedDamage = sk_houndeye_dmg_blast.GetFloat() + sk_houndeye_dmg_blast.GetFloat() * ( HOUNDEYE_SQUAD_BONUS * ( m_pSquad->NumMembers() - 1 ) ); + } + else + { + // solo + flAdjustedDamage =sk_houndeye_dmg_blast.GetFloat(); + } + + flDist = (pEntity->WorldSpaceCenter() - GetAbsOrigin()).Length(); + + flAdjustedDamage -= ( flDist / HOUNDEYE_MAX_ATTACK_RADIUS ) * flAdjustedDamage; + + if ( !FVisible( pEntity ) ) + { + if ( pEntity->IsPlayer() ) + { + // if this entity is a client, and is not in full view, inflict half damage. We do this so that players still + // take the residual damage if they don't totally leave the houndeye's effective radius. We restrict it to clients + // so that monsters in other parts of the level don't take the damage and get pissed. + flAdjustedDamage *= 0.5; + } + else if ( !FClassnameIs( pEntity, "func_breakable" ) && !FClassnameIs( pEntity, "func_pushable" ) ) + { + // do not hurt nonclients through walls, but allow damage to be done to breakables + flAdjustedDamage = 0; + } + } + + //ALERT ( at_aiconsole, "Damage: %f\n", flAdjustedDamage ); + + if (flAdjustedDamage > 0 ) + { + CTakeDamageInfo info( this, this, flAdjustedDamage, DMG_SONIC | DMG_ALWAYSGIB ); + CalculateExplosiveDamageForce( &info, (pEntity->GetAbsOrigin() - GetAbsOrigin()), pEntity->GetAbsOrigin() ); + + pEntity->TakeDamage( info ); + + if ( (pEntity->GetAbsOrigin() - GetAbsOrigin()).Length2D() <= HOUNDEYE_MAX_ATTACK_RADIUS ) + { + if ( pEntity->GetMoveType() == MOVETYPE_VPHYSICS || (pEntity->VPhysicsGetObject() && !pEntity->IsPlayer()) ) + { + IPhysicsObject *pPhysObject = pEntity->VPhysicsGetObject(); + + if ( pPhysObject ) + { + float flMass = pPhysObject->GetMass(); + + if ( flMass <= HOUNDEYE_TOP_MASS ) + { + // Increase the vertical lift of the force + Vector vecForce = info.GetDamageForce(); + vecForce.z *= 2.0f; + info.SetDamageForce( vecForce ); + + pEntity->VPhysicsTakeDamage( info ); + } + } + } + } + } + } + } + } +} + +//========================================================= +// WriteBeamColor - writes a color vector to the network +// based on the size of the group. +//========================================================= +Vector CNPC_Houndeye::WriteBeamColor ( void ) +{ + BYTE bRed, bGreen, bBlue; + + if ( m_pSquad ) + { + switch ( m_pSquad->NumMembers() ) + { + case 1: + // solo houndeye - weakest beam + bRed = 188; + bGreen = 220; + bBlue = 255; + break; + case 2: + bRed = 101; + bGreen = 133; + bBlue = 221; + break; + case 3: + bRed = 67; + bGreen = 85; + bBlue = 255; + break; + case 4: + bRed = 62; + bGreen = 33; + bBlue = 211; + break; + default: + Msg ( "Unsupported Houndeye SquadSize!\n" ); + bRed = 188; + bGreen = 220; + bBlue = 255; + break; + } + } + else + { + // solo houndeye - weakest beam + bRed = 188; + bGreen = 220; + bBlue = 255; + } + + + return Vector ( bRed, bGreen, bBlue ); +} + +bool CNPC_Houndeye::ShouldGoToIdleState( void ) +{ + if ( m_pSquad ) + { + AISquadIter_t iter; + for (CAI_BaseNPC *pMember = m_pSquad->GetFirstMember( &iter ); pMember; pMember = m_pSquad->GetNextMember( &iter ) ) + { + if ( pMember != this && pMember->GetHintNode() && pMember->GetHintNode()->HintType() != NO_NODE ) + return true; + } + + return true; + } + + return true; +} + +bool CNPC_Houndeye::FValidateHintType ( CAI_Hint *pHint ) +{ + switch( pHint->HintType() ) + { + case HINT_HL1_WORLD_MACHINERY: + return true; + break; + case HINT_HL1_WORLD_BLINKING_LIGHT: + return true; + break; + case HINT_HL1_WORLD_HUMAN_BLOOD: + return true; + break; + case HINT_HL1_WORLD_ALIEN_BLOOD: + return true; + break; + } + + Msg ( "Couldn't validate hint type" ); + + return false; +} + +//========================================================= +// SetActivity +//========================================================= +void CNPC_Houndeye::SetActivity ( Activity NewActivity ) +{ + int iSequence; + + if ( NewActivity == GetActivity() ) + return; + + if ( m_NPCState == NPC_STATE_COMBAT && NewActivity == ACT_IDLE && random->RandomInt( 0, 1 ) ) + { + // play pissed idle. + iSequence = LookupSequence( "madidle" ); + + SetActivity( NewActivity ); // Go ahead and set this so it doesn't keep trying when the anim is not present + + // In case someone calls this with something other than the ideal activity + SetIdealActivity( GetActivity() ); + + // Set to the desired anim, or default anim if the desired is not present + if ( iSequence > ACTIVITY_NOT_AVAILABLE ) + { + SetSequence( iSequence ); // Set to the reset anim (if it's there) + SetCycle( 0 ); // FIX: frame counter shouldn't be reset when its the same activity as before + ResetSequenceInfo(); + } + } + else + { + BaseClass::SetActivity ( NewActivity ); + } +} + +//========================================================= +// start task +//========================================================= +void CNPC_Houndeye::StartTask ( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_HOUND_FALL_ASLEEP: + { + m_fAsleep = TRUE; // signal that hound is lying down (must stand again before doing anything else!) + TaskComplete(); + break; + } + case TASK_HOUND_WAKE_UP: + { + m_fAsleep = FALSE; // signal that hound is standing again + TaskComplete(); + break; + } + case TASK_HOUND_OPEN_EYE: + { + m_fDontBlink = FALSE; // turn blinking back on and that code will automatically open the eye + TaskComplete(); + break; + } + case TASK_HOUND_CLOSE_EYE: + { + m_nSkin = 0; + m_fDontBlink = TRUE; // tell blink code to leave the eye alone. + break; + } + case TASK_HOUND_THREAT_DISPLAY: + { + SetIdealActivity( ACT_IDLE_ANGRY ); + break; + } + case TASK_HOUND_HOP_BACK: + { + SetIdealActivity( ACT_LEAP ); + break; + } + case TASK_RANGE_ATTACK1: + { + SetIdealActivity( ACT_RANGE_ATTACK1 ); + break; + } + case TASK_SPECIAL_ATTACK1: + { + SetIdealActivity( ACT_SPECIAL_ATTACK1 ); + break; + } + default: + { + BaseClass::StartTask(pTask); + break; + } + } +} + +//========================================================= +// RunTask +//========================================================= +void CNPC_Houndeye::RunTask ( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_HOUND_THREAT_DISPLAY: + { + if ( GetEnemy() ) + { + float idealYaw; + idealYaw = UTIL_VecToYaw( GetEnemy()->GetAbsOrigin() - GetAbsOrigin() ); + GetMotor()->SetIdealYawAndUpdate( idealYaw ); + } + + if ( IsSequenceFinished() ) + { + TaskComplete(); + } + + break; + } + case TASK_HOUND_CLOSE_EYE: + { + if ( m_nSkin < HOUNDEYE_EYE_FRAMES - 1 ) + m_nSkin++; + break; + } + case TASK_HOUND_HOP_BACK: + { + if ( IsSequenceFinished() ) + { + TaskComplete(); + } + break; + } + case TASK_SPECIAL_ATTACK1: + { + m_nSkin = random->RandomInt(0, HOUNDEYE_EYE_FRAMES - 1); + + float idealYaw; + idealYaw = UTIL_VecToYaw( GetNavigator()->GetGoalPos() ); + GetMotor()->SetIdealYawAndUpdate( idealYaw ); + + float life; + life = ((255 - GetCycle()) / ( m_flPlaybackRate * m_flPlaybackRate)); + if (life < 0.1) + { + life = 0.1; + } + + /* MessageBegin( MSG_PAS, SVC_TEMPENTITY, GetAbsOrigin() ); + WRITE_BYTE( TE_IMPLOSION); + WRITE_COORD( GetAbsOrigin().x); + WRITE_COORD( GetAbsOrigin().y); + WRITE_COORD( GetAbsOrigin().z + 16); + WRITE_BYTE( 50 * life + 100); // radius + WRITE_BYTE( pev->frame / 25.0 ); // count + WRITE_BYTE( life * 10 ); // life + MessageEnd();*/ + + if ( IsSequenceFinished() ) + { + SonicAttack(); + TaskComplete(); + } + + break; + } + default: + { + BaseClass::RunTask(pTask); + break; + } + } +} + +//========================================================= +// PrescheduleThink +//========================================================= +void CNPC_Houndeye::PrescheduleThink ( void ) +{ + BaseClass::PrescheduleThink(); + + // if the hound is mad and is running, make hunt noises. + if ( m_NPCState == NPC_STATE_COMBAT && GetActivity() == ACT_RUN && random->RandomFloat( 0, 1 ) < 0.2 ) + { + WarnSound(); + } + + // at random, initiate a blink if not already blinking or sleeping + if ( !m_fDontBlink ) + { + if ( ( m_nSkin == 0 ) && random->RandomInt(0,0x7F) == 0 ) + {// start blinking! + m_nSkin = HOUNDEYE_EYE_FRAMES - 1; + } + else if ( m_nSkin != 0 ) + {// already blinking + m_nSkin--; + } + } + + // if you are the leader, average the origins of each pack member to get an approximate center. + if ( m_pSquad && m_pSquad->IsLeader( this ) ) + { + int iSquadCount = 0; + + AISquadIter_t iter; + for (CAI_BaseNPC *pSquadMember = m_pSquad->GetFirstMember( &iter ); pSquadMember; pSquadMember = m_pSquad->GetNextMember( &iter ) ) + { + iSquadCount++; + m_vecPackCenter = m_vecPackCenter + pSquadMember->GetAbsOrigin(); + } + + m_vecPackCenter = m_vecPackCenter / iSquadCount; + } +} + +//========================================================= +// GetScheduleOfType +//========================================================= +int CNPC_Houndeye::TranslateSchedule( int scheduleType ) +{ + if ( m_fAsleep ) + { + // if the hound is sleeping, must wake and stand! + if ( HasCondition( COND_HEAR_DANGER ) || HasCondition( COND_HEAR_THUMPER ) || HasCondition( COND_HEAR_COMBAT ) || + HasCondition( COND_HEAR_WORLD ) || HasCondition( COND_HEAR_PLAYER ) || HasCondition( COND_HEAR_BULLET_IMPACT ) ) + { + CSound *pWakeSound; + + pWakeSound = GetBestSound(); + ASSERT( pWakeSound != NULL ); + if ( pWakeSound ) + { + GetMotor()->SetIdealYawToTarget ( pWakeSound->GetSoundOrigin() ); + + if ( FLSoundVolume ( pWakeSound ) >= HOUNDEYE_SOUND_STARTLE_VOLUME ) + { + // awakened by a loud sound + return SCHED_HOUND_WAKE_URGENT; + } + } + // sound was not loud enough to scare the bejesus out of houndeye + return SCHED_HOUND_WAKE_LAZY; + } + else if ( HasCondition( COND_NEW_ENEMY ) ) + { + // get up fast, to fight. + return SCHED_HOUND_WAKE_URGENT; + } + + else + { + // hound is waking up on its own + return SCHED_HOUND_WAKE_LAZY; + } + } + switch ( scheduleType ) + { + case SCHED_IDLE_STAND: + { + // we may want to sleep instead of stand! + if ( m_pSquad && !m_pSquad->IsLeader( this ) && !m_fAsleep && random->RandomInt( 0,29 ) < 1 ) + { + return SCHED_HOUND_SLEEP; + } + else + { + return BaseClass::TranslateSchedule( scheduleType ); + } + } + + case SCHED_RANGE_ATTACK1: + return SCHED_HOUND_RANGEATTACK; + case SCHED_SPECIAL_ATTACK1: + return SCHED_HOUND_SPECIALATTACK; + + case SCHED_FAIL: + { + if ( m_NPCState == NPC_STATE_COMBAT ) + { + if ( !FNullEnt( UTIL_FindClientInPVS( edict() ) ) ) + { + // client in PVS + return SCHED_HOUND_COMBAT_FAIL_PVS; + } + else + { + // client has taken off! + return SCHED_HOUND_COMBAT_FAIL_NOPVS; + } + } + else + { + return BaseClass::TranslateSchedule ( scheduleType ); + } + } + default: + { + return BaseClass::TranslateSchedule ( scheduleType ); + } + } +} + +int CNPC_Houndeye::SelectSchedule( void ) +{ + switch ( m_NPCState ) + { + case NPC_STATE_COMBAT: + { +// dead enemy + if ( HasCondition( COND_ENEMY_DEAD ) ) + { + // call base class, all code to handle dead enemies is centralized there. + return BaseClass::SelectSchedule(); + } + + if ( HasCondition( COND_LIGHT_DAMAGE ) || HasCondition( COND_HEAVY_DAMAGE ) ) + { + if ( random->RandomFloat( 0 , 1 ) <= 0.4 ) + { + trace_t trace; + Vector v_forward; + GetVectors( &v_forward, NULL, NULL ); + UTIL_TraceEntity( this, GetAbsOrigin(), GetAbsOrigin() + v_forward * -128, MASK_SOLID, &trace ); + + if ( trace.fraction == 1.0 ) + { + // it's clear behind, so the hound will jump + return SCHED_HOUND_HOP_RETREAT; + } + } + + return SCHED_TAKE_COVER_FROM_ENEMY; + } + + if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) ) + { + // don't constraint attacks based on squad slots, just let em all go for it +// if ( OccupyStrategySlot ( SQUAD_SLOTS_HOUND_ATTACK ) ) + return SCHED_RANGE_ATTACK1; + } + break; + } + } + + return BaseClass::SelectSchedule(); +} + + +//========================================================= +// FLSoundVolume - subtracts the volume of the given sound +// from the distance the sound source is from the caller, +// and returns that value, which is considered to be the 'local' +// volume of the sound. +//========================================================= +float CNPC_Houndeye::FLSoundVolume( CSound *pSound ) +{ + return ( pSound->Volume() - ( ( pSound->GetSoundOrigin() - GetAbsOrigin() ).Length() ) ); +} + + +//========================================================= +// +// SquadRecruit(), get some monsters of my classification and +// link them as a group. returns the group size +// +//========================================================= +int CNPC_Houndeye::SquadRecruit( int searchRadius, int maxMembers ) +{ + int squadCount; + int iMyClass = Classify();// cache this monster's class + + if ( maxMembers < 2 ) + return 0; + + // I am my own leader + squadCount = 1; + + CBaseEntity *pEntity = NULL; + + if ( m_SquadName != NULL_STRING ) + { + // I have a netname, so unconditionally recruit everyone else with that name. + pEntity = gEntList.FindEntityByClassname( pEntity, "monster_houndeye" ); + + while ( pEntity && squadCount < maxMembers ) + { + CNPC_Houndeye *pRecruit = (CNPC_Houndeye*)pEntity->MyNPCPointer(); + + if ( pRecruit ) + { + if ( !pRecruit->m_pSquad && pRecruit->Classify() == iMyClass && pRecruit != this ) + { + // minimum protection here against user error.in worldcraft. + if ( pRecruit->m_SquadName != NULL_STRING && FStrEq( STRING( m_SquadName ), STRING( pRecruit->m_SquadName ) ) ) + { + pRecruit->InitSquad(); + squadCount++; + } + } + } + + pEntity = gEntList.FindEntityByClassname( pEntity, "monster_houndeye" ); + } + + return squadCount; + } + else + { + char szSquadName[64]; + Q_snprintf( szSquadName, sizeof( szSquadName ), "squad%d\n", s_iSquadIndex ); + + m_SquadName = MAKE_STRING( szSquadName ); + + while ( ( pEntity = gEntList.FindEntityInSphere( pEntity, GetAbsOrigin(), searchRadius ) ) != NULL && squadCount < maxMembers ) + { + if ( !FClassnameIs ( pEntity, "monster_houndeye" ) ) + continue; + + CNPC_Houndeye *pRecruit = (CNPC_Houndeye*)pEntity->MyNPCPointer(); + + if ( pRecruit && pRecruit != this && pRecruit->IsAlive() && !pRecruit->m_hCine ) + { + // Can we recruit this guy? + if ( !pRecruit->m_pSquad && pRecruit->Classify() == iMyClass && + ( (iMyClass != CLASS_ALIEN_MONSTER) || FClassnameIs( this, pRecruit->GetClassname() ) ) && + !pRecruit->m_SquadName ) + { + trace_t tr; + UTIL_TraceLine( GetAbsOrigin() + GetViewOffset(), pRecruit->GetAbsOrigin() + GetViewOffset(), MASK_NPCSOLID_BRUSHONLY, pRecruit, COLLISION_GROUP_NONE, &tr );// try to hit recruit with a traceline. + + if ( tr.fraction == 1.0 ) + { + //We're ready to recruit people, so start a squad if I don't have one. + if ( !m_pSquad ) + { + InitSquad(); + } + + pRecruit->m_SquadName = m_SquadName; + + pRecruit->CapabilitiesAdd ( bits_CAP_SQUAD ); + pRecruit->InitSquad(); + + squadCount++; + } + } + } + } + + if ( squadCount > 1 ) + { + s_iSquadIndex++; + } + } + + return squadCount; +} + + + +void CNPC_Houndeye::StartNPC ( void ) +{ + if ( !m_pSquad ) + { + if ( m_SquadName != NULL_STRING ) + { + BaseClass::StartNPC(); + return; + } + else + { + int iSquadSize = SquadRecruit( 1024, 4 ); + + if ( iSquadSize ) + { + Msg ( "Squad of %d %s formed\n", iSquadSize, GetClassname() ); + } + } + } + + BaseClass::StartNPC(); +} + + + +//------------------------------------------------------------------------------ +// +// Schedules +// +//------------------------------------------------------------------------------ + +AI_BEGIN_CUSTOM_NPC( monster_houndeye, CNPC_Houndeye ) + + DECLARE_TASK ( TASK_HOUND_CLOSE_EYE ) + DECLARE_TASK ( TASK_HOUND_OPEN_EYE ) + DECLARE_TASK ( TASK_HOUND_THREAT_DISPLAY ) + DECLARE_TASK ( TASK_HOUND_FALL_ASLEEP ) + DECLARE_TASK ( TASK_HOUND_WAKE_UP ) + DECLARE_TASK ( TASK_HOUND_HOP_BACK ) + + //========================================================= + // > SCHED_HOUND_RANGEATTACK + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HOUND_RANGEATTACK, + + " Tasks" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_HOUND_YELL1" + " " + " Interrupts" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + ) + + //========================================================= + // > SCHED_HOUND_AGITATED + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HOUND_AGITATED, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_HOUND_THREAT_DISPLAY 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_HEAVY_DAMAGE" + ) + + //========================================================= + // > SCHED_HOUND_HOP_RETREAT + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HOUND_HOP_RETREAT, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_HOUND_HOP_BACK 0" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_TAKE_COVER_FROM_ENEMY" + " " + " Interrupts" + ) + + //========================================================= + // > SCHED_HOUND_YELL1 + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HOUND_YELL1, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_IDEAL 0" + " TASK_RANGE_ATTACK1 0" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_HOUND_AGITATED" + " " + " Interrupts" + ) + + //========================================================= + // > SCHED_HOUND_YELL2 + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HOUND_YELL2, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_IDEAL 0" + " TASK_RANGE_ATTACK1 0" + " " + " Interrupts" + ) + + + //========================================================= + // > SCHED_HOUND_SLEEP + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HOUND_SLEEP, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT_RANDOM 5" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_CROUCH" + " TASK_SET_ACTIVITY ACTIVITY:ACT_CROUCHIDLE" + " TASK_HOUND_FALL_ASLEEP 0" + " TASK_WAIT_RANDOM 25" + " TASK_HOUND_CLOSE_EYE 0" + " " + " Interrupts" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_NEW_ENEMY" + " COND_HEAR_COMBAT" + " COND_HEAR_DANGER" + " COND_HEAR_PLAYER" + " COND_HEAR_WORLD" + ) + + //========================================================= + // > SCHED_HOUND_WAKE_LAZY + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HOUND_WAKE_LAZY, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_HOUND_OPEN_EYE 0" + " TASK_WAIT_RANDOM 2.5" + " TASK_PLAY_SEQUENCE ACT_STAND" + " TASK_HOUND_WAKE_UP 0" + " " + " Interrupts" + ) + + //========================================================= + // > SCHED_HOUND_WAKE_URGENT + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HOUND_WAKE_URGENT, + + " Tasks" + " TASK_HOUND_OPEN_EYE 0" + " TASK_PLAY_SEQUENCE ACT_HOP" + " TASK_FACE_IDEAL 0" + " TASK_HOUND_WAKE_UP 0" + " " + " Interrupts" + ) + + //========================================================= + // > SCHED_HOUND_SPECIALATTACK + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HOUND_SPECIALATTACK, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_IDEAL 0" + " TASK_SPECIAL_ATTACK1 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_IDLE_ANGRY" + " " + " Interrupts" + " " + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_ENEMY_OCCLUDED" + ) + + //========================================================= + // > SCHED_HOUND_COMBAT_FAIL_PVS + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HOUND_COMBAT_FAIL_PVS, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_HOUND_THREAT_DISPLAY 0" + " TASK_WAIT_FACE_ENEMY 1" + " " + " Interrupts" + " " + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + ) + + //========================================================= + // > SCHED_HOUND_COMBAT_FAIL_NOPVS + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HOUND_COMBAT_FAIL_NOPVS, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_HOUND_THREAT_DISPLAY 0" + " TASK_WAIT_FACE_ENEMY 1" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT_PVS 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + ) + +AI_END_CUSTOM_NPC() diff --git a/game/server/hl1/hl1_npc_houndeye.h b/game/server/hl1/hl1_npc_houndeye.h new file mode 100644 index 0000000..dfe7495 --- /dev/null +++ b/game/server/hl1/hl1_npc_houndeye.h @@ -0,0 +1,73 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef NPC_HOUNDEYE_H +#define NPC_HOUNDEYE_H +#pragma once + +#include "hl1_ai_basenpc.h" +#define HOUNDEYE_MAX_ATTACK_RADIUS 384 + +class CNPC_Houndeye : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_Houndeye, CHL1BaseNPC ); + +public: + void Spawn( void ); + void Precache( void ); + + void Event_Killed( const CTakeDamageInfo &info ); + + void WarmUpSound ( void ); + void AlertSound( void ); + void DeathSound( const CTakeDamageInfo &info ); + void WarnSound( void ); + void PainSound( const CTakeDamageInfo &info ); + void IdleSound( void ); + + float MaxYawSpeed ( void ); + + Class_T Classify ( void ); + + void HandleAnimEvent( animevent_t *pEvent ); + + void SonicAttack ( void ); + + Vector WriteBeamColor( void ); + bool ShouldGoToIdleState( void ); + bool FValidateHintType ( CAI_Hint *pHint ); + + void SetActivity ( Activity NewActivity ); + + void StartTask( const Task_t *pTask ); + void RunTask ( const Task_t *pTask ); + void PrescheduleThink ( void ); + + int TranslateSchedule( int scheduleType ); + int SelectSchedule( void ); + + float FLSoundVolume( CSound *pSound ); + int RangeAttack1Conditions ( float flDot, float flDist ); + + void StartNPC ( void ); + + virtual float InnateRange1MinRange( void ) { return 0.0f; } + virtual float InnateRange1MaxRange( void ) { return HOUNDEYE_MAX_ATTACK_RADIUS; } + + DEFINE_CUSTOM_AI; + DECLARE_DATADESC(); + +private: + int SquadRecruit( int searchRadius, int maxMembers ); + + int m_iSpriteTexture; + bool m_fAsleep;// some houndeyes sleep in idle mode if this is set, the houndeye is lying down + bool m_fDontBlink;// don't try to open/close eye if this bit is set! + Vector m_vecPackCenter; // the center of the pack. The leader maintains this by averaging the origins of all pack members. +}; + +#endif // NPC_HOUNDEYE_H
\ No newline at end of file diff --git a/game/server/hl1/hl1_npc_ichthyosaur.cpp b/game/server/hl1/hl1_npc_ichthyosaur.cpp new file mode 100644 index 0000000..1e17a2d --- /dev/null +++ b/game/server/hl1/hl1_npc_ichthyosaur.cpp @@ -0,0 +1,1024 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Ichthyosaur - buh bum... buh bum... +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "beam_shared.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "activitylist.h" +#include "game.h" +#include "npcevent.h" +#include "player.h" +#include "entitylist.h" +#include "soundenvelope.h" +#include "shake.h" +#include "ndebugoverlay.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "hl1_npc_ichthyosaur.h" + +ConVar sk_ichthyosaur_health ( "sk_ichthyosaur_health", "0" ); +ConVar sk_ichthyosaur_shake ( "sk_ichthyosaur_shake", "0" ); + +#define ICH_SWIM_SPEED_WALK 150 +#define ICH_SWIM_SPEED_RUN 400 +#define PROBE_LENGTH 150 + +enum IchthyosaurMoveType_t +{ + ICH_MOVETYPE_SEEK = 0, // Fly through the target without stopping. + ICH_MOVETYPE_ARRIVE // Slow down and stop at target. +}; + +enum +{ + SCHED_SWIM_AROUND = LAST_SHARED_SCHEDULE + 1, + SCHED_SWIM_AGITATED, + SCHED_CIRCLE_ENEMY, + SCHED_TWITCH_DIE, +}; + + +//========================================================= +// monster-specific tasks and states +//========================================================= +enum +{ + TASK_ICHTHYOSAUR_CIRCLE_ENEMY = LAST_SHARED_TASK + 1, + TASK_ICHTHYOSAUR_SWIM, + TASK_ICHTHYOSAUR_FLOAT, +}; + + +LINK_ENTITY_TO_CLASS( monster_ichthyosaur, CNPC_Ichthyosaur ); + +BEGIN_DATADESC( CNPC_Ichthyosaur ) + + // Function Pointers + DEFINE_ENTITYFUNC( BiteTouch ), + + DEFINE_FIELD( m_SaveVelocity, FIELD_VECTOR ), + DEFINE_FIELD( m_idealDist, FIELD_FLOAT ), + DEFINE_FIELD( m_flBlink, FIELD_TIME ), + DEFINE_FIELD( m_flEnemyTouched, FIELD_TIME ), + DEFINE_FIELD( m_bOnAttack, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flMaxSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_flMinSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_flMaxDist, FIELD_FLOAT ), + DEFINE_FIELD( m_flNextAlert, FIELD_TIME ), + DEFINE_FIELD( m_flMaxSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_vecLastMoveTarget, FIELD_VECTOR ), + DEFINE_FIELD( m_bHasMoveTarget, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flFlyingSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_flLastAttackSound, FIELD_TIME ), + + DEFINE_INPUTFUNC( FIELD_VOID, "StartCombat", InputStartCombat ), + DEFINE_INPUTFUNC( FIELD_VOID, "EndCombat", InputEndCombat ), + +END_DATADESC() + +//========================================================= +// AI Schedules Specific to this monster +//========================================================= + + +AI_BEGIN_CUSTOM_NPC( monster_ichthyosaur, CNPC_Ichthyosaur ) + +DECLARE_TASK ( TASK_ICHTHYOSAUR_SWIM ) +DECLARE_TASK ( TASK_ICHTHYOSAUR_CIRCLE_ENEMY ) +DECLARE_TASK ( TASK_ICHTHYOSAUR_FLOAT ) + + //========================================================= + // > SCHED_SWIM_AROUND + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SWIM_AROUND, + + " Tasks" + " TASK_SET_ACTIVITY ACTIVITY:ACT_GLIDE" + " TASK_ICHTHYOSAUR_SWIM 0.0" + " " + " Interrupts" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_SEE_ENEMY" + " COND_NEW_ENEMY" + " COND_HEAR_PLAYER" + " COND_HEAR_COMBAT" + ) + //========================================================= + // > SCHED_SWIM_AGITATED + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SWIM_AGITATED, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_SWIM" + " TASK_WAIT 2.0" + " " + ) + //========================================================= + // > SCHED_CIRCLE_ENEMY + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_CIRCLE_ENEMY, + + " Tasks" + " TASK_SET_ACTIVITY ACTIVITY:ACT_GLIDE" + " TASK_ICHTHYOSAUR_CIRCLE_ENEMY 0.0" + " " + " Interrupts" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_NEW_ENEMY" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_RANGE_ATTACK1" + ) + //========================================================= + // > SCHED_TWITCH_DIE + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_TWITCH_DIE, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SOUND_DIE 0" + " TASK_DIE 0" + " TASK_ICHTHYOSAUR_FLOAT 0" + " " + ) + +AI_END_CUSTOM_NPC() + + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CNPC_Ichthyosaur::Precache() +{ + PrecacheModel("models/icky.mdl"); + + PrecacheModel("sprites/lgtning.vmt"); + + PrecacheScriptSound( "Ichthyosaur.Bite" ); + PrecacheScriptSound( "Ichthyosaur.Alert" ); + PrecacheScriptSound( "Ichthyosaur.Pain" ); + PrecacheScriptSound( "Ichthyosaur.Die" ); + PrecacheScriptSound( "Ichthyosaur.Idle" ); + PrecacheScriptSound( "Ichthyosaur.Attack" ); + + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Ichthyosaur::Spawn( void ) +{ + Precache( ); + + SetModel( "models/icky.mdl"); + UTIL_SetSize( this, Vector( -32, -32, -32 ), Vector( 32, 32, 32 ) ); + + SetHullType(HULL_LARGE_CENTERED); + SetHullSizeNormal(); + SetDefaultEyeOffset(); + + // Use our hitboxes to determine our render bounds + CollisionProp()->SetSurroundingBoundsType( USE_HITBOXES ); + + SetNavType( NAV_FLY ); + m_NPCState = NPC_STATE_NONE; + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + AddFlag( FL_FLY | FL_STEPMOVEMENT ); + + m_flGroundSpeed = ICH_SWIM_SPEED_RUN; + + m_bloodColor = BLOOD_COLOR_YELLOW; + m_iHealth = sk_ichthyosaur_health.GetFloat(); + m_iMaxHealth = m_iHealth; + m_flFieldOfView = -0.707; // 270 degrees + + AddFlag( FL_SWIM ); + + m_flFlyingSpeed = ICHTHYOSAUR_SPEED; + + SetDistLook( 1024 ); + + + SetTouch( &CNPC_Ichthyosaur::BiteTouch ); + + m_idealDist = 384; + m_flMinSpeed = 80; + m_flMaxSpeed = 400; + m_flMaxDist = 384; + m_flLastAttackSound = gpGlobals->curtime; + + Vector vforward; + AngleVectors(GetAbsAngles(), &vforward ); + VectorNormalize ( vforward ); + SetAbsVelocity( m_flFlyingSpeed * vforward ); + m_SaveVelocity = GetAbsVelocity(); + + CapabilitiesClear(); + CapabilitiesAdd( bits_CAP_MOVE_FLY | bits_CAP_INNATE_MELEE_ATTACK1 | bits_CAP_INNATE_RANGE_ATTACK1 ); + + m_bOnAttack = false; + + NPCInit(); +} + + +//========================================================= +//========================================================= +int CNPC_Ichthyosaur::TranslateSchedule( int scheduleType ) +{ + switch ( scheduleType ) + { + case SCHED_IDLE_WALK: + return SCHED_SWIM_AROUND; + case SCHED_STANDOFF: + return SCHED_CIRCLE_ENEMY; + case SCHED_FAIL: + return SCHED_SWIM_AGITATED; + case SCHED_DIE: + return SCHED_TWITCH_DIE; + case SCHED_CHASE_ENEMY: + + if ( m_flLastAttackSound < gpGlobals->curtime ) + { + AttackSound(); + m_flLastAttackSound = gpGlobals->curtime + random->RandomFloat( 2.0f, 4.0f ); + } + + break; + } + + return BaseClass::TranslateSchedule( scheduleType ); +} + +int CNPC_Ichthyosaur::SelectSchedule() +{ + switch(m_NPCState) + { + case NPC_STATE_IDLE: + m_flFlyingSpeed = 80; + m_flMaxSpeed = 80; + return TranslateSchedule( SCHED_IDLE_WALK ); + + case NPC_STATE_ALERT: + m_flFlyingSpeed = 150; + m_flMaxSpeed = 150; + return TranslateSchedule( SCHED_IDLE_WALK ); + + case NPC_STATE_COMBAT: + m_flMaxSpeed = 400; + + // eat them + if ( HasCondition( COND_CAN_MELEE_ATTACK1 ) ) + { + return TranslateSchedule( SCHED_MELEE_ATTACK1 ); + } + + // chase them down and eat them + if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) ) + { + return TranslateSchedule( SCHED_CHASE_ENEMY ); + } + + if ( HasCondition( COND_HEAVY_DAMAGE ) ) + { + m_bOnAttack = true; + } + + if ( GetHealth() < GetMaxHealth() - 20 ) + { + m_bOnAttack = true; + } + + return TranslateSchedule( SCHED_STANDOFF ); + } + + return BaseClass::SelectSchedule(); +} + + +bool CNPC_Ichthyosaur::OverrideMove( float flInterval ) +{ + if ( m_lifeState == LIFE_ALIVE ) + { + if ( m_NPCState != NPC_STATE_SCRIPT) + { + MoveExecute_Alive( flInterval ); + } + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Ichthyosaur::MoveExecute_Alive(float flInterval) +{ + Vector vStart = GetAbsOrigin(); + Vector vForward, vRight, vUp; + + if (GetNavigator()->IsGoalActive()) + { + Vector vecDir = ( GetNavigator()->GetPath()->CurWaypointPos() - GetAbsOrigin()); + VectorNormalize( vecDir ); + + m_SaveVelocity = vecDir * m_flFlyingSpeed; + } + + // If we're attacking, accelerate to max speed + if (m_bOnAttack && m_flFlyingSpeed < m_flMaxSpeed) + { + m_flFlyingSpeed = MIN( m_flMaxSpeed, m_flFlyingSpeed+40 ); + } + + if (m_flFlyingSpeed < 180) + { + if (GetIdealActivity() == ACT_SWIM) + SetActivity( ACT_GLIDE ); + if (GetIdealActivity() == ACT_GLIDE) + m_flPlaybackRate = m_flFlyingSpeed / 150.0; + } + else + { + if (GetIdealActivity() == ACT_GLIDE) + SetActivity( ACT_SWIM ); + if (GetIdealActivity() == ACT_SWIM) + m_flPlaybackRate = m_flFlyingSpeed / 300.0; + } + + // Steering + QAngle angSaveAngles; + VectorAngles( m_SaveVelocity, angSaveAngles ); + AngleVectors(angSaveAngles, &vForward, &vRight, &vUp); + + Vector z; + float frac; + + Vector f, u, l, r, d; + f = DoProbe(vStart + (PROBE_LENGTH * vForward) ); + r = DoProbe(vStart + ((PROBE_LENGTH/3) * (vForward + vRight)) ); + l = DoProbe(vStart + ((PROBE_LENGTH/3) * (vForward - vRight)) ); + u = DoProbe(vStart + ((PROBE_LENGTH/3) * (vForward + vUp)) ); + d = DoProbe(vStart + ((PROBE_LENGTH/3) * (vForward - vUp)) ); + + Vector SteeringVector = f+r+l+u+d; + + if( ProbeZ( vStart + vForward*50, vUp*50, &frac ) ) + { + // reflect off the water surface + m_SaveVelocity.z = -10; + } + + m_SaveVelocity += SteeringVector/32; + VectorNormalize( m_SaveVelocity ); + + angSaveAngles = GetAbsAngles(); + + AngleVectors( angSaveAngles, &vForward, &vRight, &vUp ); + + float flDot = DotProduct( vForward, m_SaveVelocity ); + if (flDot > 0.5) + m_SaveVelocity = m_SaveVelocity * m_flFlyingSpeed; + else if (flDot > 0) + m_SaveVelocity = m_SaveVelocity * m_flFlyingSpeed * (flDot + 0.5); + else + m_SaveVelocity = m_SaveVelocity * 80; + + SetAbsVelocity( m_SaveVelocity ); + + VectorAngles( m_SaveVelocity, angSaveAngles ); + + // + // Smooth Pitch + // + if (angSaveAngles.x > 180) + angSaveAngles.x = angSaveAngles.x - 360; + + QAngle angAbsAngles = GetAbsAngles(); + + angAbsAngles.x = clamp( UTIL_Approach(angSaveAngles.x, angAbsAngles.x, 10 ), -60, 60 ); + + // + // Smooth Yaw and generate Roll + // + float turn = 360; + + if (fabs(angSaveAngles.y - angAbsAngles.y) < fabs(turn)) + { + turn = angSaveAngles.y - angAbsAngles.y; + } + if (fabs(angSaveAngles.y - angAbsAngles.y + 360) < fabs(turn)) + { + turn = angSaveAngles.y - angAbsAngles.y + 360; + } + if (fabs(angSaveAngles.y - angAbsAngles.y - 360) < fabs(turn)) + { + turn = angSaveAngles.y - angAbsAngles.y - 360; + } + + float speed = m_flFlyingSpeed * 0.4; + + if (fabs(turn) > speed) + { + if (turn < 0.0) + { + turn = -speed; + } + else + { + turn = speed; + } + } + angAbsAngles.y += turn; + angAbsAngles.z -= turn; + angAbsAngles.y = fmod((angAbsAngles.y + 360.0), 360.0); + + // don't touch bone controller, makes swim animation look funky with all these hard turns. +// static float yaw_adj; +// yaw_adj = yaw_adj * 0.8 + turn; +// SetBoneController( 0, -yaw_adj / 4.0 ); + + // + // Roll Smoothing + // + turn = 360; + float flTempRoll = angAbsAngles.z; + + if (fabs(angSaveAngles.z - angAbsAngles.z) < fabs(turn)) + { + turn = angSaveAngles.z - angAbsAngles.z; + } + if (fabs(angSaveAngles.z - angAbsAngles.z + 360) < fabs(turn)) + { + turn = angSaveAngles.z - angAbsAngles.z + 360; + } + if (fabs(angSaveAngles.z - angAbsAngles.z - 360) < fabs(turn)) + { + turn = angSaveAngles.z - angAbsAngles.z - 360; + } + speed = m_flFlyingSpeed/2 * 0.1; + if (fabs(turn) < speed) + { + flTempRoll += turn; + } + else + { + if (turn < 0.0) + { + flTempRoll -= speed; + } + else + { + flTempRoll += speed; + } + } + + angAbsAngles.z = clamp( UTIL_Approach(flTempRoll, angAbsAngles.z, 5 ), -20, 20 ); + + SetAbsAngles( angAbsAngles ); + + //Move along the current velocity vector + if ( WalkMove( m_SaveVelocity * flInterval, MASK_NPCSOLID ) == false ) + { + //Attempt a half-step + if ( WalkMove( (m_SaveVelocity*0.5f) * flInterval, MASK_NPCSOLID) == false ) + { + //Restart the velocity + //VectorNormalize( m_vecVelocity ); + m_SaveVelocity *= 0.25f; + } + else + { + //Cut our velocity in half + m_SaveVelocity *= 0.5f; + } + } + + SetAbsVelocity( m_SaveVelocity ); +} + +void CNPC_Ichthyosaur::InputStartCombat( inputdata_t &input ) +{ + m_bOnAttack = true; +} + +void CNPC_Ichthyosaur::InputEndCombat( inputdata_t &input ) +{ + m_bOnAttack = false; +} + +//========================================================= +// Start task - selects the correct activity and performs +// any necessary calculations to start the next task on the +// schedule. +//========================================================= +void CNPC_Ichthyosaur::StartTask(const Task_t *pTask) +{ + switch (pTask->iTask) + { + case TASK_ICHTHYOSAUR_CIRCLE_ENEMY: + break; + case TASK_ICHTHYOSAUR_SWIM: + break; + case TASK_SMALL_FLINCH: + if (m_idealDist > 128) + { + m_flMaxDist = 512; + m_idealDist = 512; + } + else + { + m_bOnAttack = true; + } + BaseClass::StartTask(pTask); + break; + + case TASK_ICHTHYOSAUR_FLOAT: + m_nSkin = EYE_BASE; + SetSequenceByName( "bellyup" ); + break; + + default: + BaseClass::StartTask(pTask); + break; + } +} + +void CNPC_Ichthyosaur::RunTask(const Task_t *pTask ) +{ + QAngle angles = GetAbsAngles(); + + switch ( pTask->iTask ) + { + case TASK_ICHTHYOSAUR_CIRCLE_ENEMY: + + if (GetEnemy() == NULL ) + { + TaskComplete( ); + } + else if (FVisible( GetEnemy() )) + { + Vector vecFrom = GetEnemy()->EyePosition( ); + + Vector vecDelta = GetAbsOrigin() - vecFrom; + VectorNormalize( vecDelta ); + Vector vecSwim = CrossProduct( vecDelta, Vector( 0, 0, 1 ) ); + VectorNormalize( vecSwim ); + + if (DotProduct( vecSwim, m_SaveVelocity ) < 0) + { + vecSwim = vecSwim * -1.0; + } + + Vector vecPos = vecFrom + vecDelta * m_idealDist + vecSwim * 32; + + trace_t tr; + +// UTIL_TraceHull( vecFrom, vecPos, ignore_monsters, large_hull, m_hEnemy->edict(), &tr ); + UTIL_TraceEntity( this, vecFrom, vecPos, MASK_NPCSOLID, &tr ); + + if (tr.fraction > 0.5) + { + vecPos = tr.endpos; + } + + Vector vecNorm = vecPos - GetAbsOrigin(); + VectorNormalize( vecNorm ); + m_SaveVelocity = m_SaveVelocity * 0.8 + 0.2 * vecNorm * m_flFlyingSpeed; + + if (HasCondition( COND_ENEMY_FACING_ME ) && GetEnemy()->FVisible( this )) + { + m_flNextAlert -= 0.1; + + if (m_idealDist < m_flMaxDist) + { + m_idealDist += 4; + } + if (m_flFlyingSpeed > m_flMinSpeed) + { + m_flFlyingSpeed -= 2; + } + else if (m_flFlyingSpeed < m_flMinSpeed) + { + m_flFlyingSpeed += 2; + } + if (m_flMinSpeed < m_flMaxSpeed) + { + m_flMinSpeed += 0.5; + } + } + else + { + m_flNextAlert += 0.1; + + if (m_idealDist > 128) + { + m_idealDist -= 4; + } + if (m_flFlyingSpeed < m_flMaxSpeed) + { + m_flFlyingSpeed += 4; + } + } + } + else + { + m_flNextAlert = gpGlobals->curtime + 0.2; + } + + if (m_flNextAlert < gpGlobals->curtime) + { + // ALERT( at_console, "AlertSound()\n"); + AlertSound( ); + m_flNextAlert = gpGlobals->curtime + RandomFloat( 3, 5 ); + } + + break; + case TASK_ICHTHYOSAUR_SWIM: + if ( IsSequenceFinished() ) + { + TaskComplete( ); + } + break; + case TASK_DIE: + if ( IsSequenceFinished() ) + { +// pev->deadflag = DEAD_DEAD; + + TaskComplete( ); + } + break; + + case TASK_ICHTHYOSAUR_FLOAT: + angles.x = UTIL_ApproachAngle( 0, angles.x, 20 ); + SetAbsAngles( angles ); + +// SetAbsVelocity( GetAbsVelocity() * 0.8 ); +// if (pev->waterlevel > 1 && GetAbsVelocity().z < 64) +// { +// pev->velocity.z += 8; +// } +// else +// { +// pev->velocity.z -= 8; +// } + // ALERT( at_console, "%f\n", m_vecAbsVelocity.z ); + break; + + default: + BaseClass::RunTask( pTask ); + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Get our conditions for a melee attack +// Input : flDot - +// flDist - +// Output : int +//----------------------------------------------------------------------------- +int CNPC_Ichthyosaur::MeleeAttack1Conditions( float flDot, float flDist ) +{ + // Enemy must be submerged with us + if ( GetEnemy() && GetEnemy()->GetWaterLevel() != GetWaterLevel() ) + return COND_NONE; + + Vector predictedDir = ( (GetEnemy()->GetAbsOrigin()+(GetEnemy()->GetSmoothedVelocity())) - GetAbsOrigin() ); + float flPredictedDist = VectorNormalize( predictedDir ); + + Vector vBodyDir; + GetVectors( &vBodyDir, NULL, NULL ); + + float flPredictedDot = DotProduct( predictedDir, vBodyDir ); + + if ( flPredictedDot < 0.8f ) + return COND_NOT_FACING_ATTACK; + + if ( ( flPredictedDist > ( GetAbsVelocity().Length() * 0.5f) ) && ( flDist > 128.0f ) ) + return COND_TOO_FAR_TO_ATTACK; + + return COND_CAN_MELEE_ATTACK1; +} + +//========================================================= +// RangeAttack1Conditions +//========================================================= +int CNPC_Ichthyosaur::RangeAttack1Conditions( float flDot, float flDist ) +{ + CBaseEntity *pEnemy = GetEnemy(); + + if( pEnemy && pEnemy->GetWaterLevel() != GetWaterLevel() ) + { + return COND_NONE; + } + + if ( flDot > -0.7 && (m_bOnAttack || ( flDist <= 192 && m_idealDist <= 192))) + { + return COND_CAN_RANGE_ATTACK1; + } + + return COND_NONE; +} + +void CNPC_Ichthyosaur::BiteTouch( CBaseEntity *pOther ) +{ + // bite if we hit who we want to eat + if ( pOther == GetEnemy() ) + { + m_flEnemyTouched = gpGlobals->curtime + 0.2f; + m_bOnAttack = true; + } +} + +#define ICHTHYOSAUR_AE_SHAKE_RIGHT 1 +#define ICHTHYOSAUR_AE_SHAKE_LEFT 2 + +//========================================================= +// HandleAnimEvent - catches the monster-specific messages +// that occur when tagged animation frames are played. +//========================================================= +void CNPC_Ichthyosaur::HandleAnimEvent( animevent_t *pEvent ) +{ + int bDidAttack = FALSE; + Vector vForward, vRight; + QAngle angles = GetAbsAngles(); + AngleVectors( angles, &vForward, &vRight, NULL ); + + switch( pEvent->event ) + { + case ICHTHYOSAUR_AE_SHAKE_RIGHT: + case ICHTHYOSAUR_AE_SHAKE_LEFT: + { + CBaseEntity* hEnemy = GetEnemy(); + + if (hEnemy != NULL && FVisible( hEnemy )) + { + CBaseEntity *pHurt = GetEnemy(); + + if ( m_flEnemyTouched > gpGlobals->curtime && (pHurt->BodyTarget( GetAbsOrigin() ) - GetAbsOrigin()).Length() > (32+16+32) ) + break; + + Vector vecShootOrigin = Weapon_ShootPosition(); + Vector vecShootDir = GetShootEnemyDir( vecShootOrigin ); + + if (DotProduct( vecShootDir, vForward ) > 0.707) + { + angles = pHurt->GetAbsAngles(); + m_bOnAttack = true; + pHurt->SetAbsVelocity( pHurt->GetAbsVelocity() - vRight * 300 ); + if (pHurt->IsPlayer()) + { + angles.x += RandomFloat( -35, 35 ); + angles.y += RandomFloat( -90, 90 ); + angles.z = 0; + ((CBasePlayer*) pHurt)->SetPunchAngle( angles ); + } + + CTakeDamageInfo info( this, this, sk_ichthyosaur_shake.GetInt(), DMG_SLASH ); + CalculateMeleeDamageForce( &info, vForward, pHurt->GetAbsOrigin() ); + pHurt->TakeDamage( info ); + } + } + + // Do our bite sound + BiteSound(); + + bDidAttack = TRUE; + } + break; + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } + // make bubbles when he attacks + if (bDidAttack) + { + Vector vecSrc = GetAbsOrigin() + vForward * 32; + UTIL_Bubbles( vecSrc - Vector( 8, 8, 8 ), vecSrc + Vector( 8, 8, 8 ), 16 ); + } +} + +//========================================================= +// Classify - indicates this monster's place in the +// relationship table. +//========================================================= +Class_T CNPC_Ichthyosaur::Classify ( void ) +{ + return CLASS_ALIEN_MONSTER; +} + +void CNPC_Ichthyosaur::NPCThink ( void ) +{ + // blink the eye + if (m_flBlink < gpGlobals->curtime) + { + m_nSkin = EYE_CLOSED; + if (m_flBlink + 0.2 < gpGlobals->curtime) + { + m_flBlink = gpGlobals->curtime + random->RandomFloat( 3, 4 ); + if (m_bOnAttack) + m_nSkin = EYE_MAD; + else + m_nSkin = EYE_BASE; + } + } + + BaseClass::NPCThink( ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : speed to move at +//----------------------------------------------------------------------------- +float CNPC_Ichthyosaur::GetGroundSpeed( void ) +{ + if ( GetIdealActivity() == ACT_GLIDE ) + return ICH_SWIM_SPEED_WALK; + + return ICH_SWIM_SPEED_RUN; +} + +Vector CNPC_Ichthyosaur::DoProbe( const Vector &Probe ) +{ + Vector WallNormal = Vector(0,0,-1); // WATER normal is Straight Down for fish. + float frac = 1.0; + bool bBumpedSomething = false; // = ProbeZ(GetAbsOrigin(), Probe, &frac); + + trace_t tr; + UTIL_TraceEntity( this, GetAbsOrigin(), Probe, MASK_NPCSOLID, &tr ); + if ( tr.allsolid || tr.fraction < 0.99 ) + { + if (tr.fraction < 0.0) tr.fraction = 0.0; + if (tr.fraction > 1.0) tr.fraction = 1.0; + if (tr.fraction < frac) + { + frac = tr.fraction; + bBumpedSomething = true; + WallNormal = tr.plane.normal; + } + } + //NOTENOTE: Debug start + //NDebugOverlay::Line( tr.startpos, tr.endpos, 255.0f*(1.0f-tr.fraction), 255.0f * tr.fraction, 0.0f, true, 0.05f ); + //NOTENOTE: Debug end + + if (bBumpedSomething && (GetEnemy() == NULL || !tr.m_pEnt || tr.m_pEnt->entindex() != GetEnemy()->entindex())) + { + Vector ProbeDir = Probe - GetAbsOrigin(); + + Vector NormalToProbeAndWallNormal = CrossProduct(ProbeDir, WallNormal); + Vector SteeringVector = CrossProduct( NormalToProbeAndWallNormal, ProbeDir); + + VectorNormalize( WallNormal ); + VectorNormalize( m_SaveVelocity ); + + float SteeringForce = m_flFlyingSpeed * (1-frac) * ( DotProduct( WallNormal, m_SaveVelocity ) ); + if (SteeringForce < 0.0) + { + SteeringForce = -SteeringForce; + } + + Vector vSteering = SteeringVector; + + VectorNormalize( vSteering ); + SteeringVector = SteeringForce * vSteering; + + //NDebugOverlay::Line( GetAbsOrigin(), GetAbsOrigin() + (SteeringVector*4.0f), 0, 255, 255, true, 0.1f ); + + return SteeringVector; + } + return Vector(0, 0, 0); +} + +bool CNPC_Ichthyosaur::ProbeZ( const Vector &position, const Vector &probe, float *pFraction) +{ + int iPositionContents = UTIL_PointContents( position ); + int iProbeContents = UTIL_PointContents( position ); + + if( !(iPositionContents & MASK_WATER) ) + { + // we're not in the water anymore + *pFraction = 0.0; + return true; // We hit a water boundary because we are where we don't belong. + } + if( iProbeContents == iPositionContents ) + { + // The probe is entirely inside the water + *pFraction = 1.0; + return false; + } + + Vector ProbeUnit = (probe-position); + VectorNormalize( ProbeUnit ); + float ProbeLength = (probe-position).Length(); + float maxProbeLength = ProbeLength; + float minProbeLength = 0; + + float diff = maxProbeLength - minProbeLength; + while (diff > 1.0) + { + float midProbeLength = minProbeLength + diff/2.0; + Vector midProbeVec = midProbeLength * ProbeUnit; + if (UTIL_PointContents(position+midProbeVec) == iPositionContents) + { + minProbeLength = midProbeLength; + } + else + { + maxProbeLength = midProbeLength; + } + diff = maxProbeLength - minProbeLength; + } + *pFraction = minProbeLength/ProbeLength; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pEntity - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CNPC_Ichthyosaur::FVisible( CBaseEntity *pEntity, int traceMask, CBaseEntity **ppBlocker ) +{ + // Can't see entities that aren't in water + if ( pEntity->GetWaterLevel() < 1 ) + return false; + + return BaseClass::FVisible( pEntity, traceMask, ppBlocker ); +} + +void CNPC_Ichthyosaur::IdleSound( void ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Ichthyosaur.Idle" ); +} + +void CNPC_Ichthyosaur::AlertSound( void ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Ichthyosaur.Alert" ); +} + +void CNPC_Ichthyosaur::AttackSound( void ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Ichthyosaur.Attack" ); +} + +void CNPC_Ichthyosaur::BiteSound( void ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Ichthyosaur.Bite" ); +} + +void CNPC_Ichthyosaur::DeathSound( const CTakeDamageInfo &info ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Ichthyosaur.Die" ); +} + +void CNPC_Ichthyosaur::PainSound( const CTakeDamageInfo &info ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Ichthyosaur.Pain" ); +} + +//----------------------------------------------------------------------------- +void CNPC_Ichthyosaur::GatherEnemyConditions( CBaseEntity *pEnemy ) +{ + // Do the base class + BaseClass::GatherEnemyConditions( pEnemy ); + + if ( HasCondition( COND_ENEMY_UNREACHABLE ) == false ) + { + if( pEnemy == NULL || pEnemy->GetWaterLevel() != GetWaterLevel() ) + { + SetCondition( COND_ENEMY_UNREACHABLE ); + } + } +} diff --git a/game/server/hl1/hl1_npc_ichthyosaur.h b/game/server/hl1/hl1_npc_ichthyosaur.h new file mode 100644 index 0000000..0f05c4d --- /dev/null +++ b/game/server/hl1/hl1_npc_ichthyosaur.h @@ -0,0 +1,97 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef NPC_ICHTHYOSAUR_H +#define NPC_ICHTHYOSAUR_H + + +#include "hl1_ai_basenpc.h" + +#define SEARCH_RETRY 16 + +#define ICHTHYOSAUR_SPEED 150 + +#define EYE_MAD 0 +#define EYE_BASE 1 +#define EYE_CLOSED 2 +#define EYE_BACK 3 +#define EYE_LOOK 4 + + +// +// CNPC_Ichthyosaur +// + +class CNPC_Ichthyosaur : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_Ichthyosaur, CHL1BaseNPC ); +public: + + void Precache( void ); + void Spawn( void ); + Class_T Classify ( void ); + void NPCThink ( void ); + void Swim ( void ); + void StartTask(const Task_t *pTask); + void RunTask( const Task_t *pTask ); + int RangeAttack1Conditions( float flDot, float flDist ); + int MeleeAttack1Conditions ( float flDot, float flDist ); + void BiteTouch( CBaseEntity *pOther ); + void HandleAnimEvent( animevent_t *pEvent ); + int TranslateSchedule( int scheduleType ); + int SelectSchedule(); + virtual bool FVisible ( CBaseEntity *pEntity, int traceMask = MASK_BLOCKLOS, CBaseEntity **ppBlocker = NULL ); + + Vector DoProbe( const Vector &Probe ); + bool ProbeZ( const Vector &position, const Vector &probe, float *pFraction); + + float GetGroundSpeed ( void ); + + bool OverrideMove( float flInterval ); + void MoveExecute_Alive(float flInterval); + + void InputStartCombat( inputdata_t &input ); + void InputEndCombat( inputdata_t &input ); + + virtual void IdleSound( void ); + virtual void AlertSound( void ); + virtual void DeathSound( const CTakeDamageInfo &info ); + virtual void PainSound( const CTakeDamageInfo &info ); + + void AttackSound( void ); + void BiteSound( void ); + + virtual void GatherEnemyConditions( CBaseEntity *pEnemy ); + + DEFINE_CUSTOM_AI; + DECLARE_DATADESC(); + +private: + Vector m_SaveVelocity; + float m_idealDist; + + float m_flBlink; + + float m_flEnemyTouched; + bool m_bOnAttack; + + float m_flMaxSpeed; + float m_flMinSpeed; + float m_flMaxDist; + + float m_flNextAlert; + float m_flLastAttackSound; + + //Save the info from that run + Vector m_vecLastMoveTarget; + bool m_bHasMoveTarget; + + float m_flFlyingSpeed; +}; + + +#endif //NPC_ICHTHYOSAUR_H diff --git a/game/server/hl1/hl1_npc_leech.cpp b/game/server/hl1/hl1_npc_leech.cpp new file mode 100644 index 0000000..9e00fc0 --- /dev/null +++ b/game/server/hl1/hl1_npc_leech.cpp @@ -0,0 +1,724 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "basecombatweapon.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "hl1_ai_basenpc.h" +#include "ai_senses.h" + +// Animation events +#define LEECH_AE_ATTACK 1 +#define LEECH_AE_FLOP 2 + +//#define DEBUG_BEAMS 0 + +ConVar sk_leech_health( "sk_leech_health", "2" ); +ConVar sk_leech_dmg_bite( "sk_leech_dmg_bite", "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 70 +#define LEECH_SIZEX 10 +#define LEECH_FRAMETIME 0.1 + +class CNPC_Leech : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_Leech, CHL1BaseNPC ); +public: + + DECLARE_DATADESC(); + + void Spawn( void ); + void Precache( void ); + + static const char *pAlertSounds[]; + + void SwimThink( void ); + void DeadThink( void ); + + void SwitchLeechState( void ); + float ObstacleDistance( CBaseEntity *pTarget ); + void UpdateMotion( void ); + + void RecalculateWaterlevel( void ); + void Touch( CBaseEntity *pOther ); + + Disposition_t IRelationType(CBaseEntity *pTarget); + + void HandleAnimEvent( animevent_t *pEvent ); + + void AttackSound( void ); + void AlertSound( void ); + + void Activate( void ); + + Class_T Classify( void ) { return CLASS_INSECT; }; + + void Event_Killed( const CTakeDamageInfo &info ); + + + bool ShouldGib( const CTakeDamageInfo &info ); + + +/* // Base entity functions + void Killed( entvars_t *pevAttacker, int iGib ); + int TakeDamage( entvars_t *pevInflictor, entvars_t *pevAttacker, float flDamage, int bitsDamageType ); +*/ + +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; + Vector m_oldOrigin; +}; + +LINK_ENTITY_TO_CLASS( monster_leech, CNPC_Leech ); + +BEGIN_DATADESC( CNPC_Leech ) + DEFINE_FIELD( m_flTurning, FIELD_FLOAT ), + DEFINE_FIELD( m_fPathBlocked, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flAccelerate, FIELD_FLOAT ), + DEFINE_FIELD( m_obstacle, FIELD_FLOAT ), + DEFINE_FIELD( m_top, FIELD_FLOAT ), + DEFINE_FIELD( m_bottom, FIELD_FLOAT ), + DEFINE_FIELD( m_height, FIELD_FLOAT ), + DEFINE_FIELD( m_waterTime, FIELD_TIME ), + DEFINE_FIELD( m_sideTime, FIELD_TIME ), + DEFINE_FIELD( m_zTime, FIELD_TIME ), + DEFINE_FIELD( m_stateTime, FIELD_TIME ), + DEFINE_FIELD( m_attackSoundTime, FIELD_TIME ), + DEFINE_FIELD( m_oldOrigin, FIELD_VECTOR ), + + DEFINE_THINKFUNC( SwimThink ), + DEFINE_THINKFUNC( DeadThink ), +END_DATADESC() + + +bool CNPC_Leech::ShouldGib( const CTakeDamageInfo &info ) +{ + return false; +} + +void CNPC_Leech::Spawn( void ) +{ + Precache(); + SetModel( "models/leech.mdl" ); + + SetHullType(HULL_TINY_CENTERED); + SetHullSizeNormal(); + + UTIL_SetSize( this, Vector(-1,-1,0), Vector(1,1,2)); + + Vector vecSurroundingMins(-8,-8,0); + Vector vecSurroundingMaxs(8,8,2); + CollisionProp()->SetSurroundingBoundsType( USE_SPECIFIED_BOUNDS, &vecSurroundingMins, &vecSurroundingMaxs ); + + // Don't push the minz down too much or the water check will fail because this entity is really point-sized + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_FLY ); + AddFlag( FL_SWIM ); + m_iHealth = sk_leech_health.GetInt(); + + m_flFieldOfView = -0.5; // 180 degree FOV + SetDistLook( 750 ); + NPCInit(); + SetThink( &CNPC_Leech::SwimThink ); + SetUse( NULL ); + SetTouch( NULL ); + SetViewOffset( vec3_origin ); + + m_flTurning = 0; + m_fPathBlocked = FALSE; + SetActivity( ACT_SWIM ); + SetState( NPC_STATE_IDLE ); + m_stateTime = gpGlobals->curtime + random->RandomFloat( 1, 5 ); + + SetRenderColor( 255, 255, 255, 255 ); + + m_bloodColor = DONT_BLEED; + SetCollisionGroup( COLLISION_GROUP_DEBRIS ); +} + +void CNPC_Leech::Activate( void ) +{ + RecalculateWaterlevel(); + + BaseClass::Activate(); +} + +void CNPC_Leech::DeadThink( void ) +{ + if ( IsSequenceFinished() ) + { + if ( GetActivity() == ACT_DIEFORWARD ) + { + SetThink( NULL ); + StopAnimation(); + return; + } + else if ( GetFlags() & FL_ONGROUND ) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + SetActivity( ACT_DIEFORWARD ); + } + } + StudioFrameAdvance(); + SetNextThink( gpGlobals->curtime + 0.1 ); + + // Apply damage velocity, but keep out of the walls + if ( GetAbsVelocity().x != 0 || GetAbsVelocity().y != 0 ) + { + trace_t tr; + + // Look 0.5 seconds ahead + UTIL_TraceLine( GetLocalOrigin(), GetLocalOrigin() + GetAbsVelocity() * 0.5, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr); + if (tr.fraction != 1.0) + { + Vector vVelocity = GetAbsVelocity(); + + vVelocity.x = 0; + vVelocity.y = 0; + + SetAbsVelocity( vVelocity ); + } + } +} + + +Disposition_t CNPC_Leech::IRelationType( CBaseEntity *pTarget ) +{ + if ( pTarget->IsPlayer() ) + return D_HT; + + return BaseClass::IRelationType( pTarget ); +} + +void CNPC_Leech::Touch( CBaseEntity *pOther ) +{ + if ( !pOther->IsPlayer() ) + return; + + if ( pOther == GetTouchTrace().m_pEnt ) + { + if ( pOther->GetAbsVelocity() == vec3_origin ) + return; + + SetBaseVelocity( pOther->GetAbsVelocity() ); + AddFlag( FL_BASEVELOCITY ); + } +} + +void CNPC_Leech::HandleAnimEvent( animevent_t *pEvent ) +{ + CBaseEntity *pEnemy = GetEnemy(); + + switch( pEvent->event ) + { + case LEECH_AE_FLOP: + // Play flop sound + break; + + case LEECH_AE_ATTACK: + AttackSound(); + + if ( pEnemy != NULL ) + { + Vector dir, face; + + AngleVectors( GetAbsAngles(), &face ); + + face.z = 0; + dir = (pEnemy->GetLocalOrigin() - GetLocalOrigin() ); + dir.z = 0; + + VectorNormalize( dir ); + VectorNormalize( face ); + + if ( DotProduct(dir, face) > 0.9 ) // Only take damage if the leech is facing the prey + { + CTakeDamageInfo info( this, this, sk_leech_dmg_bite.GetInt(), DMG_SLASH ); + CalculateMeleeDamageForce( &info, dir, pEnemy->GetAbsOrigin() ); + pEnemy->TakeDamage( info ); + } + } + m_stateTime -= 2; + break; + + + + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } +} + + +void CNPC_Leech::Precache( void ) +{ + PrecacheModel("models/leech.mdl"); + + PrecacheScriptSound( "Leech.Attack" ); + PrecacheScriptSound( "Leech.Alert" ); +} + + +void CNPC_Leech::AttackSound( void ) +{ + if ( gpGlobals->curtime > m_attackSoundTime ) + { + CPASAttenuationFilter filter( this ); + + EmitSound(filter, entindex(), "Leech.Attack" ); + m_attackSoundTime = gpGlobals->curtime + 0.5; + } +} + + +void CNPC_Leech::AlertSound( void ) +{ + CPASAttenuationFilter filter( this ); + EmitSound(filter, entindex(), "Leech.Alert" ); +} + +void CNPC_Leech::SwitchLeechState( void ) +{ + m_stateTime = gpGlobals->curtime + random->RandomFloat( 3, 6 ); + if ( m_NPCState == NPC_STATE_COMBAT ) + { + SetEnemy ( NULL ); + SetState( NPC_STATE_IDLE ); + // We may be up against the player, so redo the side checks + m_sideTime = 0; + } + else + { + GetSenses()->Look( GetSenses()->GetDistLook() ); + CBaseEntity *pEnemy = BestEnemy(); + if ( pEnemy && pEnemy->GetWaterLevel() != 0 ) + { + SetEnemy ( pEnemy ); + SetState( NPC_STATE_COMBAT ); + m_stateTime = gpGlobals->curtime + random->RandomFloat( 18, 25 ); + AlertSound(); + } + } +} + +void CNPC_Leech::RecalculateWaterlevel( void ) +{ + // Calculate boundaries + Vector vecTest = GetLocalOrigin() - Vector(0,0,400); + + trace_t tr; + + UTIL_TraceLine( GetLocalOrigin(), vecTest, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr); + + if ( tr.fraction != 1.0 ) + m_bottom = tr.endpos.z + 1; + else + m_bottom = vecTest.z; + + m_top = UTIL_WaterLevel( GetLocalOrigin(), GetLocalOrigin().z, GetLocalOrigin().z + 400 ) - 1; + +#if DEBUG_BEAMS + NDebugOverlay::Line( GetLocalOrigin(), GetLocalOrigin() + Vector( 0, 0, m_bottom ), 0, 255, 0, false, 0.1f ); + NDebugOverlay::Line( GetLocalOrigin(), GetLocalOrigin() + Vector( 0, 0, m_top ), 0, 255, 255, false, 0.1f ); +#endif + + // 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->RandomFloat( m_bottom, m_top ); + m_waterTime = gpGlobals->curtime + random->RandomFloat( 5, 7 ); +} + +void CNPC_Leech::SwimThink( void ) +{ + trace_t tr; + float flLeftSide; + float flRightSide; + float targetSpeed; + float targetYaw = 0; + CBaseEntity *pTarget; + + /*if ( !UTIL_FindClientInPVS( edict() ) ) + { + m_flNextThink = gpGlobals->curtime + random->RandomFloat( 1.0f, 1.5f ); + SetAbsVelocity( vec3_origin ); + return; + } + else*/ + SetNextThink( gpGlobals->curtime + 0.1 ); + + targetSpeed = LEECH_SWIM_SPEED; + + if ( m_waterTime < gpGlobals->curtime ) + RecalculateWaterlevel(); + + if ( m_stateTime < gpGlobals->curtime ) + SwitchLeechState(); + + ClearCondition( COND_CAN_MELEE_ATTACK1 ); + + switch( m_NPCState ) + { + case NPC_STATE_COMBAT: + pTarget = GetEnemy(); + if ( !pTarget ) + SwitchLeechState(); + else + { + // Chase the enemy's eyes + m_height = pTarget->GetLocalOrigin().z + pTarget->GetViewOffset().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->GetLocalOrigin() - GetLocalOrigin(); + location.z += (pTarget->GetViewOffset().z); + if ( location.Length() < 80 ) + SetCondition( COND_CAN_MELEE_ATTACK1 ); + // Turn towards target ent + targetYaw = UTIL_VecToYaw( location ); + + QAngle vTestAngle = GetAbsAngles(); + + targetYaw = UTIL_AngleDiff( targetYaw, UTIL_AngleMod( GetAbsAngles().y ) ); + + if ( targetYaw < (-LEECH_TURN_RATE) ) + targetYaw = (-LEECH_TURN_RATE); + else if ( targetYaw > (LEECH_TURN_RATE) ) + targetYaw = (LEECH_TURN_RATE); + else + targetSpeed *= 2; + } + + break; + + default: + if ( m_zTime < gpGlobals->curtime ) + { + float newHeight = random->RandomFloat( m_bottom, m_top ); + m_height = 0.5 * m_height + 0.5 * newHeight; + m_zTime = gpGlobals->curtime + random->RandomFloat( 1, 4 ); + } + if ( random->RandomInt( 0, 100 ) < 10 ) + targetYaw = random->RandomInt( -30, 30 ); + pTarget = NULL; + // oldorigin test + if ( ( GetLocalOrigin() - m_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 ); + m_oldOrigin = GetLocalOrigin(); + if ( m_obstacle < 0.1 ) + m_obstacle = 0.1; + + Vector vForward, vRight; + + AngleVectors( GetAbsAngles(), &vForward, &vRight, NULL ); + + // 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; + m_flSpeed = UTIL_Approach( targetSpeed, m_flSpeed, LEECH_SWIM_ACCEL * LEECH_FRAMETIME ); + SetAbsVelocity( vForward * m_flSpeed ); + + } + 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 = GetLocalOrigin() + ( vRight * LEECH_SIZEX) + ( vForward * LEECH_CHECK_DIST); + UTIL_TraceLine( GetLocalOrigin(), vecTest, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr); + flRightSide = tr.fraction; + + vecTest = GetLocalOrigin() + ( vRight * -LEECH_SIZEX) + ( vForward * LEECH_CHECK_DIST); + UTIL_TraceLine( GetLocalOrigin(), vecTest, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr); + + flLeftSide = tr.fraction; + + // turn left, right or random depending on clearance ratio + float delta = (flRightSide - flLeftSide); + if ( delta > 0.1 || (delta > -0.1 && random->RandomInt( 0,100 ) < 50 ) ) + m_flTurning = -LEECH_TURN_RATE; + else + m_flTurning = LEECH_TURN_RATE; + } + + m_flSpeed = UTIL_Approach( -(LEECH_SWIM_SPEED*0.5), m_flSpeed, LEECH_SWIM_DECEL * LEECH_FRAMETIME * m_obstacle ); + SetAbsVelocity( vForward * m_flSpeed ); + } + + GetMotor()->SetIdealYaw( m_flTurning + targetYaw ); + UpdateMotion(); +} + +// +// ObstacleDistance - returns normalized distance to obstacle +// +float CNPC_Leech::ObstacleDistance( CBaseEntity *pTarget ) +{ + trace_t tr; + Vector vecTest; + Vector vForward, vRight; + + // use VELOCITY, not angles, not all boids point the direction they are flying + //Vector vecDir = UTIL_VecToAngles( pev->velocity ); + QAngle tmp = GetAbsAngles(); + tmp.x = -tmp.x; + AngleVectors ( tmp, &vForward, &vRight, NULL ); + + // check for obstacle ahead + vecTest = GetLocalOrigin() + vForward * LEECH_CHECK_DIST; + UTIL_TraceLine( GetLocalOrigin(), vecTest, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr); + + if ( tr.startsolid ) + { + m_flSpeed = -LEECH_SWIM_SPEED * 0.5; + } + + if ( tr.fraction != 1.0 ) + { + if ( (pTarget == NULL || tr.m_pEnt != pTarget ) ) + { + return tr.fraction; + } + else + { + if ( fabs( m_height - GetLocalOrigin().z ) > 10 ) + return tr.fraction; + } + } + + if ( m_sideTime < gpGlobals->curtime ) + { + // extra wide checks + vecTest = GetLocalOrigin() + vRight * LEECH_SIZEX * 2 + vForward * LEECH_CHECK_DIST; + UTIL_TraceLine( GetLocalOrigin(), vecTest, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr); + + if (tr.fraction != 1.0) + return tr.fraction; + + vecTest = GetLocalOrigin() - vRight * LEECH_SIZEX * 2 + vForward * LEECH_CHECK_DIST; + UTIL_TraceLine( GetLocalOrigin(), vecTest, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr); + if (tr.fraction != 1.0) + return tr.fraction; + + // Didn't hit either side, so stop testing for another 0.5 - 1 seconds + m_sideTime = gpGlobals->curtime + random->RandomFloat(0.5,1); + } + + return 1.0; +} + +void CNPC_Leech::UpdateMotion( void ) +{ + float flapspeed = ( m_flSpeed - m_flAccelerate) / LEECH_ACCELERATE; + m_flAccelerate = m_flAccelerate * 0.8 + m_flSpeed * 0.2; + + if (flapspeed < 0) + flapspeed = -flapspeed; + flapspeed += 1.0; + if (flapspeed < 0.5) + flapspeed = 0.5; + if (flapspeed > 1.9) + flapspeed = 1.9; + + m_flPlaybackRate = flapspeed; + + QAngle vAngularVelocity = GetLocalAngularVelocity(); + QAngle vAngles = GetLocalAngles(); + + if ( !m_fPathBlocked ) + vAngularVelocity.y = GetMotor()->GetIdealYaw(); + else + vAngularVelocity.y = GetMotor()->GetIdealYaw() * m_obstacle; + + if ( vAngularVelocity.y > 150 ) + SetIdealActivity( ACT_TURN_LEFT ); + else if ( vAngularVelocity.y < -150 ) + SetIdealActivity( ACT_TURN_RIGHT ); + else + SetIdealActivity( ACT_SWIM ); + + // lean + float targetPitch, delta; + delta = m_height - GetLocalOrigin().z; + +/* if ( delta < -10 ) + targetPitch = -30; + else if ( delta > 10 ) + targetPitch = 30; + else*/ + targetPitch = 0; + + vAngles.x = UTIL_Approach( targetPitch, vAngles.x, 60 * LEECH_FRAMETIME ); + + // bank + vAngularVelocity.z = - ( vAngles.z + (vAngularVelocity.y * 0.25)); + + if ( m_NPCState == NPC_STATE_COMBAT && HasCondition( COND_CAN_MELEE_ATTACK1 ) ) + SetIdealActivity( ACT_MELEE_ATTACK1 ); + + // Out of water check + if ( !GetWaterLevel() ) + { + SetMoveType( MOVETYPE_FLYGRAVITY ); + SetIdealActivity( ACT_HOP ); + SetAbsVelocity( vec3_origin ); + + // Animation will intersect the floor if either of these is non-zero + vAngles.z = 0; + vAngles.x = 0; + + m_flPlaybackRate = random->RandomFloat( 0.8, 1.2 ); + } + else if ( GetMoveType() == MOVETYPE_FLYGRAVITY ) + { + SetMoveType( MOVETYPE_FLY ); + SetGroundEntity( NULL ); + + // TODO + RecalculateWaterlevel(); + m_waterTime = gpGlobals->curtime + 2; // Recalc again soon, water may be rising + } + + if ( GetActivity() != GetIdealActivity() ) + { + SetActivity ( GetIdealActivity() ); + } + StudioFrameAdvance(); + + DispatchAnimEvents ( this ); + + SetLocalAngles( vAngles ); + SetLocalAngularVelocity( vAngularVelocity ); + + Vector vForward, vRight; + + AngleVectors( vAngles, &vForward, &vRight, NULL ); + +#if DEBUG_BEAMS + if ( m_fPathBlocked ) + { + float color = m_obstacle * 30; + if ( m_obstacle == 1.0 ) + color = 0; + if ( color > 255 ) + color = 255; + NDebugOverlay::Line( GetLocalOrigin(), GetLocalOrigin() + vForward * LEECH_CHECK_DIST, 255, color, color, false, 0.1f ); + } + else + NDebugOverlay::Line( GetLocalOrigin(), GetLocalOrigin() + vForward * LEECH_CHECK_DIST, 255, 255, 0, false, 0.1f ); + + NDebugOverlay::Line( GetLocalOrigin(), GetLocalOrigin() + vRight * (vAngularVelocity.y*0.25), 0, 0, 255, false, 0.1f ); +#endif + +} + +void CNPC_Leech::Event_Killed( const CTakeDamageInfo &info ) +{ + Vector vecSplatDir; + trace_t tr; + + //ALERT(at_aiconsole, "Leech: killed\n"); + // tell owner ( if any ) that we're dead.This is mostly for MonsterMaker functionality. + CBaseEntity *pOwner = GetOwnerEntity(); + if (pOwner) + pOwner->DeathNotice( this ); + + // When we hit the ground, play the "death_end" activity + if ( GetWaterLevel() ) + { + QAngle qAngles = GetAbsAngles(); + QAngle qAngularVel = GetLocalAngularVelocity(); + Vector vOrigin = GetLocalOrigin(); + + qAngles.z = 0; + qAngles.x = 0; + + vOrigin.z += 1; + + SetAbsVelocity( vec3_origin ); + + if ( random->RandomInt( 0, 99 ) < 70 ) + qAngularVel.y = random->RandomInt( -720, 720 ); + + SetAbsAngles( qAngles ); + SetLocalAngularVelocity( qAngularVel ); + SetAbsOrigin( vOrigin ); + + + SetGravity ( 0.02 ); + SetGroundEntity( NULL ); + SetActivity( ACT_DIESIMPLE ); + } + else + SetActivity( ACT_DIEFORWARD ); + + SetMoveType( MOVETYPE_FLYGRAVITY ); + m_takedamage = DAMAGE_NO; + + SetThink( &CNPC_Leech::DeadThink ); +} diff --git a/game/server/hl1/hl1_npc_nihilanth.cpp b/game/server/hl1/hl1_npc_nihilanth.cpp new file mode 100644 index 0000000..a045a27 --- /dev/null +++ b/game/server/hl1/hl1_npc_nihilanth.cpp @@ -0,0 +1,1745 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "basecombatweapon.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "Sprite.h" +#include "hl1_ai_basenpc.h" +#include "ai_senses.h" +#include "Sprite.h" +#include "beam_shared.h" +#include "logicrelay.h" +#include "ai_navigator.h" + + +#define N_SCALE 15 +#define N_SPHERES 20 + +ConVar sk_nihilanth_health( "sk_nihilanth_health", "800" ); +ConVar sk_nihilanth_zap( "sk_nihilanth_zap", "30" ); + +class CNPC_Nihilanth : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_Nihilanth, CHL1BaseNPC ); +public: + void Spawn( void ); + void Precache( void ); + + Class_T Classify( void ) { return CLASS_ALIEN_MILITARY; }; + + /* void Killed( entvars_t *pevAttacker, int iGib ); + void GibMonster( void ); + + void TargetSphere( USE_TYPE useType, float value ); + CBaseEntity *RandomTargetname( const char *szName ); + void MakeFriend( Vector vecPos ); + + int TakeDamage( entvars_t* pevInflictor, entvars_t* pevAttacker, float flDamage, int bitsDamageType ); + void TraceAttack( entvars_t *pevAttacker, float flDamage, Vector vecDir, TraceResult *ptr, int bitsDamageType); + */ + + int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + bool ShouldGib( const CTakeDamageInfo &info ) { return false; } + + void PainSound( const CTakeDamageInfo &info ); + void DeathSound( const CTakeDamageInfo &info ); + + void StartupThink( void ); + void NullThink( void ); + + void HuntThink( void ); + void DyingThink( void ); + + void Flight( void ); + void NextActivity( void ); + void FloatSequence( void ); + void HandleAnimEvent( animevent_t *pEvent ); + bool EmitSphere( void ); + + void ShootBalls( void ); + bool AbsorbSphere( void ); + + void MakeFriend( Vector vecStart ); + + void InputTurnBabyOn( inputdata_t &inputdata ); + void InputTurnBabyOff( inputdata_t &inputdata ); + + float m_flForce; + + float m_flNextPainSound; + + Vector m_velocity; + Vector m_avelocity; + + Vector m_vecTarget; + Vector m_posTarget; + + Vector m_vecDesired; + Vector m_posDesired; + + float m_flMinZ; + float m_flMaxZ; + + Vector m_vecGoal; + + float m_flLastSeen; + float m_flPrevSeen; + + int m_irritation; + + int m_iLevel; + int m_iTeleport; + + EHANDLE m_hRecharger; + + EHANDLE m_hSphere[N_SPHERES]; + int m_iActiveSpheres; + + float m_flAdj; + + CSprite *m_pBall; + + char m_szRechargerTarget[64]; + char m_szDrawUse[64]; + char m_szTeleportUse[64]; + char m_szTeleportTouch[64]; + char m_szDeadUse[64]; + char m_szDeadTouch[64]; + + float m_flShootEnd; + float m_flShootTime; + + EHANDLE m_hFriend[3]; + + bool m_bDead; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( monster_nihilanth, CNPC_Nihilanth ); + +BEGIN_DATADESC( CNPC_Nihilanth ) + DEFINE_FIELD( m_flForce, FIELD_FLOAT ), + DEFINE_FIELD( m_flNextPainSound, FIELD_TIME ), + DEFINE_FIELD( m_velocity, FIELD_VECTOR ), + DEFINE_FIELD( m_avelocity, FIELD_VECTOR ), + DEFINE_FIELD( m_vecTarget, FIELD_VECTOR ), + DEFINE_FIELD( m_posTarget, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecDesired, FIELD_VECTOR ), + DEFINE_FIELD( m_posDesired, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_flMinZ, FIELD_FLOAT ), + DEFINE_FIELD( m_flMaxZ, FIELD_FLOAT ), + DEFINE_FIELD( m_vecGoal, FIELD_VECTOR ), + DEFINE_FIELD( m_flLastSeen, FIELD_TIME ), + DEFINE_FIELD( m_flPrevSeen, FIELD_TIME ), + DEFINE_FIELD( m_irritation, FIELD_INTEGER ), + DEFINE_FIELD( m_iLevel, FIELD_INTEGER ), + DEFINE_FIELD( m_iTeleport, FIELD_INTEGER ), + DEFINE_FIELD( m_hRecharger, FIELD_EHANDLE ), + DEFINE_ARRAY( m_hSphere, FIELD_EHANDLE, N_SPHERES ), + DEFINE_FIELD( m_iActiveSpheres, FIELD_INTEGER ), + DEFINE_FIELD( m_flAdj, FIELD_FLOAT ), + DEFINE_FIELD( m_pBall, FIELD_CLASSPTR ), + DEFINE_ARRAY( m_szRechargerTarget, FIELD_CHARACTER, 64 ), + DEFINE_ARRAY( m_szDrawUse, FIELD_CHARACTER, 64 ), + DEFINE_ARRAY( m_szTeleportUse, FIELD_CHARACTER, 64 ), + DEFINE_ARRAY( m_szTeleportTouch, FIELD_CHARACTER, 64 ), + DEFINE_ARRAY( m_szDeadUse, FIELD_CHARACTER, 64 ), + DEFINE_ARRAY( m_szDeadTouch, FIELD_CHARACTER, 64 ), + DEFINE_FIELD( m_flShootEnd, FIELD_TIME ), + DEFINE_FIELD( m_flShootTime, FIELD_TIME ), + DEFINE_ARRAY( m_hFriend, FIELD_EHANDLE, 3 ), + DEFINE_FIELD( m_bDead, FIELD_BOOLEAN ), + DEFINE_THINKFUNC( NullThink ), + DEFINE_THINKFUNC( StartupThink ), + DEFINE_THINKFUNC( HuntThink ), + DEFINE_THINKFUNC( DyingThink ), + + DEFINE_INPUTFUNC( FIELD_VOID, "TurnBabyOn", InputTurnBabyOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnBabyOff", InputTurnBabyOff ), + +END_DATADESC() + +class CNihilanthHVR : public CAI_BaseNPC +{ + DECLARE_CLASS( CNihilanthHVR, CAI_BaseNPC ); +public: + void Spawn( void ); + void Precache( void ); + + void CircleInit( CBaseEntity *pTarget ); + void AbsorbInit( void ); + void GreenBallInit( void ); + + + void RemoveTouch( CBaseEntity *pOther ); + + /*void Zap( void ); + void Teleport( void );*/ + + void HoverThink( void ); + bool CircleTarget( Vector vecTarget ); + void BounceTouch( CBaseEntity *pOther ); + + void ZapThink( void ); + void ZapInit( CBaseEntity *pEnemy ); + void ZapTouch( CBaseEntity *pOther ); + + void TeleportThink( void ); + void TeleportTouch( CBaseEntity *pOther ); + + void MovetoTarget( Vector vecTarget ); + + void DissipateThink( void ); + + CSprite *SpriteInit( const char *pSpriteName, CNihilanthHVR *pOwner ); + + void TeleportInit( CNPC_Nihilanth *pOwner, CBaseEntity *pEnemy, CBaseEntity *pTarget, CBaseEntity *pTouch ); + + float m_flIdealVel; + Vector m_vecIdeal; + CNPC_Nihilanth *m_pNihilanth; + EHANDLE m_hTouch; + + + float m_flBallScale; + + void SetSprite( CBaseEntity *pSprite ) + { + m_hSprite = pSprite; + } + + CBaseEntity *GetSprite( void ) + { + return m_hSprite.Get(); + } + + void SetBeam( CBaseEntity *pBeam ) + { + m_hBeam = pBeam; + } + + CBaseEntity *GetBeam( void ) + { + return m_hBeam.Get(); + } + +private: + + EHANDLE m_hSprite; + EHANDLE m_hBeam; + + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( nihilanth_energy_ball, CNihilanthHVR ); + + +BEGIN_DATADESC( CNihilanthHVR ) + DEFINE_FIELD( m_flIdealVel, FIELD_FLOAT ), + DEFINE_FIELD( m_flBallScale, FIELD_FLOAT ), + DEFINE_FIELD( m_vecIdeal, FIELD_VECTOR ), + DEFINE_FIELD( m_pNihilanth, FIELD_CLASSPTR ), + DEFINE_FIELD( m_hTouch, FIELD_EHANDLE ), + DEFINE_FIELD( m_hSprite, FIELD_EHANDLE ), + DEFINE_FIELD( m_hBeam, FIELD_EHANDLE ), + + DEFINE_THINKFUNC( HoverThink ), + DEFINE_ENTITYFUNC( BounceTouch ), + DEFINE_THINKFUNC( ZapThink ), + DEFINE_ENTITYFUNC( ZapTouch ), + DEFINE_THINKFUNC( DissipateThink ), + DEFINE_THINKFUNC( TeleportThink ), + DEFINE_ENTITYFUNC( TeleportTouch ), + DEFINE_ENTITYFUNC( RemoveTouch ), +END_DATADESC() + + +//========================================================= +// Nihilanth, final Boss monster +//========================================================= + +void CNPC_Nihilanth::Spawn( void ) +{ + Precache( ); + // motor + SetMoveType( MOVETYPE_FLY ); + SetSolid( SOLID_BBOX ); + + SetModel( "models/nihilanth.mdl" ); + //UTIL_SetSize( this, Vector( -300, -300, 0), Vector(300, 300, 512)); + //UTIL_SetSize(this, Vector( -32, -32, 0), Vector(32, 32, 64 )); + + UTIL_SetSize(this, Vector( -16 * N_SCALE, -16 * N_SCALE, -48 * N_SCALE ), Vector( 16 * N_SCALE, 16 * N_SCALE, 28 * N_SCALE ) ); + + Vector vecSurroundingMins( -16 * N_SCALE, -16 * N_SCALE, -48 * N_SCALE ); + Vector vecSurroundingMaxs( 16 * N_SCALE, 16 * N_SCALE, 28 * N_SCALE ); + CollisionProp()->SetSurroundingBoundsType( USE_SPECIFIED_BOUNDS, &vecSurroundingMins, &vecSurroundingMaxs ); + + UTIL_SetOrigin( this, GetAbsOrigin() - Vector( 0, 0, 64 ) ); + + AddFlag( FL_NPC ); + m_takedamage = DAMAGE_AIM; + m_iHealth = sk_nihilanth_health.GetFloat(); + SetViewOffset ( Vector( 0, 0, 300 ) ); + + m_flFieldOfView = -1; // 360 degrees + + SetSequence( 0 ); + ResetSequenceInfo( ); + + InitBoneControllers(); + + SetThink( &CNPC_Nihilanth::StartupThink ); + SetNextThink( gpGlobals->curtime + 0.1 ); + + m_vecDesired = Vector( 1, 0, 0 ); + m_posDesired = Vector( GetAbsOrigin().x, GetAbsOrigin().y, 512 ); + + m_iLevel = 1; + m_iTeleport = 1; + + if (m_szRechargerTarget[0] == '\0') Q_strncpy( m_szRechargerTarget, "n_recharger", sizeof( m_szRechargerTarget ) ); + if (m_szDrawUse[0] == '\0') Q_strncpy( m_szDrawUse, "n_draw", sizeof( m_szDrawUse ) ); + if (m_szTeleportUse[0] == '\0') Q_strncpy( m_szTeleportUse, "n_leaving", sizeof( m_szTeleportUse ) ); + if (m_szTeleportTouch[0] == '\0') Q_strncpy( m_szTeleportTouch, "n_teleport", sizeof( m_szTeleportTouch ) ); + if (m_szDeadUse[0] == '\0') Q_strncpy( m_szDeadUse, "n_dead", sizeof( m_szDeadUse ) ); + if (m_szDeadTouch[0] == '\0') Q_strncpy( m_szDeadTouch, "n_ending", sizeof( m_szDeadTouch ) ); + + SetBloodColor( BLOOD_COLOR_YELLOW ); +} + + +void CNPC_Nihilanth::Precache( void ) +{ + PrecacheModel("models/nihilanth.mdl"); + PrecacheModel("sprites/lgtning.vmt"); + UTIL_PrecacheOther( "nihilanth_energy_ball" ); + UTIL_PrecacheOther( "monster_alien_controller" ); + UTIL_PrecacheOther( "monster_alien_slave" ); + + PrecacheScriptSound( "Nihilanth.PainLaugh" ); + PrecacheScriptSound( "Nihilanth.Pain" ); + PrecacheScriptSound( "Nihilanth.Die" ); + PrecacheScriptSound( "Nihilanth.FriendBeam" ); + PrecacheScriptSound( "Nihilanth.Attack" ); + PrecacheScriptSound( "Nihilanth.BallAttack" ); + PrecacheScriptSound( "Nihilanth.Recharge" ); + +} + +void CNPC_Nihilanth::PainSound( const CTakeDamageInfo &info ) +{ + if (m_flNextPainSound > gpGlobals->curtime) + return; + + m_flNextPainSound = gpGlobals->curtime + random->RandomFloat( 2, 5 ); + + if ( m_iHealth > sk_nihilanth_health.GetFloat() / 2 ) + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Nihilanth.PainLaugh" ); + } + else if (m_irritation >= 2) + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Nihilanth.Pain" ); + } +} + +void CNPC_Nihilanth::DeathSound( const CTakeDamageInfo &info ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Nihilanth.Die" ); +} + +int CNPC_Nihilanth::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + if ( info.GetInflictor() == this ) + return 0; + + if ( m_bDead ) + return 0; + + if ( info.GetDamage() >= m_iHealth ) + { + m_iHealth = 1; + if ( m_irritation != 3 ) + return 0; + } + + PainSound( info ); + + m_iHealth -= info.GetDamage(); + + if( m_iHealth < 0 ) + { + m_iHealth = 1; + m_bDead = true; + m_takedamage = DAMAGE_NO; + } + + return 0; +} + +void CNPC_Nihilanth::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + if (m_irritation == 3) + m_irritation = 2; + + if (m_irritation == 2 && ptr->hitgroup == 0 && info.GetDamage() > 2) + m_irritation = 3; + + if (m_irritation != 3) + { + Vector vecBlood = (ptr->endpos - GetAbsOrigin() ); + + VectorNormalize( vecBlood ); + + UTIL_BloodStream( ptr->endpos, vecBlood, BloodColor(), info.GetDamage() + (100 - 100 * (m_iHealth / sk_nihilanth_health.GetFloat() ))); + } + + // SpawnBlood(ptr->vecEndPos, BloodColor(), flDamage * 5.0);// a little surface blood. + AddMultiDamage( info, this ); +} + +bool CNPC_Nihilanth::EmitSphere( void ) +{ + m_iActiveSpheres = 0; + int empty = 0; + + for (int i = 0; i < N_SPHERES; i++) + { + if (m_hSphere[i] != NULL) + { + m_iActiveSpheres++; + } + else + { + empty = i; + } + } + + if (m_iActiveSpheres >= N_SPHERES) + return false; + + Vector vecSrc = m_hRecharger->GetAbsOrigin(); + CNihilanthHVR *pEntity = (CNihilanthHVR *)CREATE_ENTITY( CNihilanthHVR, "nihilanth_energy_ball" ); + + + pEntity->SetAbsOrigin( vecSrc ); + pEntity->SetAbsAngles( GetAbsAngles() ); + pEntity->SetOwnerEntity( this ); + pEntity->Spawn(); + + pEntity->SetAbsVelocity( GetAbsOrigin() - vecSrc ); + pEntity->CircleInit( this ); + m_hSphere[empty] = pEntity; + + return true; +} + +void CNPC_Nihilanth::NullThink( void ) +{ + StudioFrameAdvance( ); + SetNextThink( gpGlobals->curtime + 0.5 ); +} + +void CNPC_Nihilanth::StartupThink( void ) +{ + m_irritation = 0; + m_flAdj = 512; + + CBaseEntity *pEntity; + + pEntity = gEntList.FindEntityByName( NULL, "n_min" ); + if (pEntity) + m_flMinZ = pEntity->GetAbsOrigin().z; + else + m_flMinZ = -4096; + + pEntity = gEntList.FindEntityByName( NULL, "n_max" ); + if (pEntity) + m_flMaxZ = pEntity->GetAbsOrigin().z; + else + m_flMaxZ = 4096; + + m_hRecharger = this; + + //TODO + for (int i = 0; i < N_SPHERES; i++) + { + EmitSphere(); + } + + m_hRecharger = NULL; + + SetUse( NULL ); + SetThink( &CNPC_Nihilanth::HuntThink); + + SetNextThink( gpGlobals->curtime + 0.1 ); +} + +void CNPC_Nihilanth::InputTurnBabyOn( inputdata_t &inputdata ) +{ + if ( m_irritation == 0 ) + { + m_irritation = 1; + } +} + +void CNPC_Nihilanth::InputTurnBabyOff( inputdata_t &inputdata ) +{ + CBaseEntity *pTouch = gEntList.FindEntityByName( NULL, m_szDeadTouch ); + + if ( pTouch && GetEnemy() != NULL ) + pTouch->Touch( GetEnemy() ); +} + +bool CNPC_Nihilanth::AbsorbSphere( void ) +{ + for (int i = 0; i < N_SPHERES; i++) + { + if (m_hSphere[i] != NULL) + { + CNihilanthHVR *pSphere = (CNihilanthHVR *)m_hSphere[i].Get(); + pSphere->AbsorbInit(); + m_hSphere[i] = NULL; + m_iActiveSpheres--; + return TRUE; + } + } + return FALSE; +} + +void CNPC_Nihilanth::HuntThink( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1 ); + DispatchAnimEvents( this ); + StudioFrameAdvance( ); + + ShootBalls(); + + // if dead, force cancelation of current animation + if ( m_bDead ) + { + SetThink( &CNPC_Nihilanth::DyingThink ); + SetCycle( 1.0f ); + + StudioFrameAdvance(); + return; + } + + // ALERT( at_console, "health %.0f\n", pev->health ); + + // if damaged, try to abosorb some spheres + if ( m_iHealth < sk_nihilanth_health.GetFloat() && AbsorbSphere() ) + { + m_iHealth += sk_nihilanth_health.GetFloat() / N_SPHERES; + } + + // get new sequence + if ( IsSequenceFinished() ) + { + SetCycle( 0 ); + + NextActivity( ); + ResetSequenceInfo( ); + m_flPlaybackRate = 2.0 - 1.0 * ( m_iHealth / sk_nihilanth_health.GetFloat() ); + } + + // look for current enemy + if ( GetEnemy() != NULL && m_hRecharger == NULL) + { + if (FVisible( GetEnemy() )) + { + if (m_flLastSeen < gpGlobals->curtime - 5) + m_flPrevSeen = gpGlobals->curtime; + + m_flLastSeen = gpGlobals->curtime; + m_posTarget = GetEnemy()->GetAbsOrigin(); + m_vecTarget = m_posTarget - GetAbsOrigin(); + + VectorNormalize( m_vecTarget ); + + m_vecDesired = m_vecTarget; + m_posDesired = Vector( GetAbsOrigin().x, GetAbsOrigin().y, m_posTarget.z + m_flAdj ); + } + else + { + m_flAdj = MIN( m_flAdj + 10, 1000 ); + } + } + + // don't go too high + if (m_posDesired.z > m_flMaxZ) + m_posDesired.z = m_flMaxZ; + + // don't go too low + if (m_posDesired.z < m_flMinZ) + m_posDesired.z = m_flMinZ; + + Flight( ); +} + +void CNPC_Nihilanth::Flight( void ) +{ + Vector vForward, vRight, vUp; + + QAngle vAngle = QAngle( GetAbsAngles().x + m_avelocity.x, GetAbsAngles().y + m_avelocity.y, GetAbsAngles().z + m_avelocity.z ); + + AngleVectors( vAngle, &vForward, &vRight, &vUp ); + float flSide = DotProduct( m_vecDesired, vRight ); + + if (flSide < 0) + { + if (m_avelocity.y < 180) + { + m_avelocity.y += 6; // 9 * (3.0/2.0); + } + } + else + { + if (m_avelocity.y > -180) + { + m_avelocity.y -= 6; // 9 * (3.0/2.0); + } + } + m_avelocity.y *= 0.98; + + // estimate where I'll be in two seconds + Vector vecEst = GetAbsOrigin() + m_velocity * 2.0 + vUp * m_flForce * 20; + + // add immediate force + AngleVectors( GetAbsAngles(), &vForward, &vRight, &vUp ); + + m_velocity.x += vUp.x * m_flForce; + m_velocity.y += vUp.y * m_flForce; + m_velocity.z += vUp.z * m_flForce; + + + float flSpeed = m_velocity.Length(); + float flDir = DotProduct( Vector( vForward.x, vForward.y, 0 ), Vector( m_velocity.x, m_velocity.y, 0 ) ); + if (flDir < 0) + flSpeed = -flSpeed; + + // sideways drag + m_velocity.x = m_velocity.x * (1.0 - fabs( vRight.x ) * 0.05); + m_velocity.y = m_velocity.y * (1.0 - fabs( vRight.y ) * 0.05); + m_velocity.z = m_velocity.z * (1.0 - fabs( vRight.z ) * 0.05); + + // general drag + m_velocity = m_velocity * 0.995; + + // apply power to stay correct height + if (m_flForce < 100 && vecEst.z < m_posDesired.z) + { + m_flForce += 10; + } + else if (m_flForce > -100 && vecEst.z > m_posDesired.z) + { + if (vecEst.z > m_posDesired.z) + m_flForce -= 10; + } + + SetAbsVelocity( m_velocity ); + + vAngle = QAngle( GetAbsAngles().x + m_avelocity.x * 0.1, GetAbsAngles().y + m_avelocity.y * 0.1, GetAbsAngles().z + m_avelocity.z * 0.1 ); + + SetAbsAngles( vAngle ); + + // ALERT( at_console, "%5.0f %5.0f : %4.0f : %3.0f : %2.0f\n", m_posDesired.z, pev->origin.z, m_velocity.z, m_avelocity.y, m_flForce ); +} + +void CNPC_Nihilanth::NextActivity( ) +{ + Vector vForward, vRight, vUp; + + SetIdealActivity( ACT_DO_NOT_DISTURB ); + + AngleVectors( GetAbsAngles(), &vForward, &vRight, &vUp ); + + if (m_irritation >= 2) + { + if (m_pBall == NULL) + { + m_pBall = CSprite::SpriteCreate( "sprites/tele1.vmt", GetAbsOrigin(), true ); + if (m_pBall) + { + m_pBall->SetTransparency( kRenderTransAdd, 255, 255, 255, 255, kRenderFxNoDissipation ); + m_pBall->SetAttachment( this, 1 ); + m_pBall->SetScale( 4.0 ); + m_pBall->m_flSpriteFramerate = 10.0f; + m_pBall->TurnOn( ); + } + } + + if (m_pBall) + { + CBroadcastRecipientFilter filterlight; + Vector vOrigin; + QAngle vAngle; + + GetAttachment( 2, vOrigin, vAngle ); + + te->DynamicLight( filterlight, 0.0, &vOrigin, 255, 192, 64, 0, 256, 20, 0 ); + } + } + + + if (( m_iHealth < sk_nihilanth_health.GetFloat() / 2 || m_iActiveSpheres < N_SPHERES / 2) && m_hRecharger == NULL && m_iLevel <= 9) + { + char szName[64]; + + CBaseEntity *pEnt = NULL; + CBaseEntity *pRecharger = NULL; + float flDist = 8192; + + Q_snprintf(szName, sizeof( szName ), "%s%d", m_szRechargerTarget, m_iLevel ); + + while ((pEnt = gEntList.FindEntityByName( pEnt, szName )) != NULL ) + { + float flLocal = (pEnt->GetAbsOrigin() - GetAbsOrigin() ).Length(); + + if ( flLocal < flDist ) + { + flDist = flLocal; + pRecharger = pEnt; + } + } + + if (pRecharger) + { + m_hRecharger = pRecharger; + m_posDesired = Vector( GetAbsOrigin().x, GetAbsOrigin().y, pRecharger->GetAbsOrigin().z ); + m_vecDesired = pRecharger->GetAbsOrigin() - m_posDesired; + + VectorNormalize( m_vecDesired ); + + m_vecDesired.z = 0; + + VectorNormalize( m_vecDesired ); + } + else + { + m_hRecharger = NULL; + Msg( "nihilanth can't find %s\n", szName ); + + m_iLevel++; + + if ( m_iLevel > 9 ) + m_irritation = 2; + } + } + + float flDist = ( m_posDesired - GetAbsOrigin() ).Length(); + float flDot = DotProduct( m_vecDesired, vForward ); + + if (m_hRecharger != NULL) + { + // at we at power up yet? + if (flDist < 128.0) + { + int iseq = LookupSequence( "recharge" ); + + if (iseq != GetSequence()) + { + char szText[64]; + + Q_snprintf( szText, sizeof( szText ), "%s%d", m_szDrawUse, m_iLevel ); + FireTargets( szText, this, this, USE_ON, 1.0 ); + + Msg( "fireing %s\n", szText ); + } + SetSequence ( LookupSequence( "recharge" ) ); + } + else + { + FloatSequence( ); + } + return; + } + + if (GetEnemy() != NULL && !GetEnemy()->IsAlive()) + { + SetEnemy( NULL ); + } + + if (m_flLastSeen + 15 < gpGlobals->curtime) + { + SetEnemy( NULL ); + } + + if ( GetEnemy() == NULL) + { + GetSenses()->Look( 4096 ); + SetEnemy( BestEnemy() ); + } + + if ( GetEnemy() != NULL && m_irritation != 0) + { + if (m_flLastSeen + 5 > gpGlobals->curtime && flDist < 256 && flDot > 0) + { + if (m_irritation >= 2 && m_iHealth < sk_nihilanth_health.GetFloat() / 2.0) + { + SetSequence( LookupSequence( "attack1_open" ) ); + } + else + { + if ( random->RandomInt(0, 1 ) == 0) + { + SetSequence( LookupSequence( "attack1" ) ); // zap + } + else + { + char szText[64]; + + Q_snprintf( szText, sizeof( szText ), "%s%d", m_szTeleportTouch, m_iTeleport ); + CBaseEntity *pTouch = gEntList.FindEntityByName( NULL, szText ); + + Q_snprintf( szText, sizeof( szText ), "%s%d", m_szTeleportUse, m_iTeleport ); + CBaseEntity *pTrigger = gEntList.FindEntityByName( NULL, szText ); + + if (pTrigger != NULL || pTouch != NULL) + { + SetSequence( LookupSequence( "attack2" ) ); // teleport + } + else + { + m_iTeleport++; + SetSequence( LookupSequence( "attack1" ) ); // zap + } + } + } + return; + } + } + + FloatSequence( ); +} + +void CNPC_Nihilanth::MakeFriend( Vector vecStart ) +{ + int i; + + for (i = 0; i < 3; i++) + { + if (m_hFriend[i] != NULL && !m_hFriend[i]->IsAlive()) + { + if ( m_nRenderMode == kRenderNormal) // don't do it if they are already fading + m_hFriend[i]->MyNPCPointer()->CorpseFade(); + + m_hFriend[i] = NULL; + } + + if (m_hFriend[i] == NULL) + { + if ( random->RandomInt( 0, 1 ) == 0) + { + int iNode = GetNavigator()->GetNetwork()->NearestNodeToPoint( vecStart ); + CAI_Node *pNode = GetNavigator()->GetNetwork()->GetNode( iNode ); + + if ( pNode && pNode->GetType() == NODE_AIR ) + { + trace_t tr; + Vector vNodeOrigin = pNode->GetOrigin(); + + UTIL_TraceHull( vNodeOrigin + Vector( 0, 0, 32 ), vNodeOrigin + Vector( 0, 0, 32 ), Vector(-40,-40, 0), Vector(40, 40, 100), MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); + + if ( tr.startsolid == 0 ) + m_hFriend[i] = Create("monster_alien_controller", vNodeOrigin, GetAbsAngles() ); + } + } + else + { + int iNode = GetNavigator()->GetNetwork()->NearestNodeToPoint( vecStart ); + CAI_Node *pNode = GetNavigator()->GetNetwork()->GetNode( iNode ); + + if ( pNode && ( pNode->GetType() == NODE_GROUND || pNode->GetType() == NODE_WATER ) ) + { + trace_t tr; + Vector vNodeOrigin = pNode->GetOrigin(); + + UTIL_TraceHull( vNodeOrigin + Vector( 0, 0, 36 ), vNodeOrigin + Vector( 0, 0, 36 ), Vector( -15, -15, 0), Vector( 20, 15, 72 ), MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); + + if (tr.startsolid == 0) + m_hFriend[i] = Create("monster_alien_slave", vNodeOrigin, GetAbsAngles() ); + } + } + if (m_hFriend[i] != NULL) + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, m_hFriend[i]->entindex(), "Nihilanth.FriendBeam" ); + } + + return; + } + } +} + +void CNPC_Nihilanth::ShootBalls( void ) +{ + if (m_flShootEnd > gpGlobals->curtime) + { + Vector vecHand; + QAngle vecAngle; + + while (m_flShootTime < m_flShootEnd && m_flShootTime < gpGlobals->curtime) + { + if ( GetEnemy() != NULL) + { + Vector vecSrc, vecDir; + CNihilanthHVR *pEntity = NULL; + + GetAttachment( 3, vecHand, vecAngle ); + vecSrc = vecHand + GetAbsVelocity() * (m_flShootTime - gpGlobals->curtime); + vecDir = m_posTarget - GetAbsOrigin(); + VectorNormalize( vecDir ); + vecSrc = vecSrc + vecDir * (gpGlobals->curtime - m_flShootTime); + + pEntity = (CNihilanthHVR *)CREATE_ENTITY( CNihilanthHVR, "nihilanth_energy_ball" ); + + pEntity->SetAbsOrigin( vecSrc ); + pEntity->SetAbsAngles( vecAngle ); + pEntity->SetOwnerEntity( this ); + pEntity->Spawn(); + + pEntity->SetAbsVelocity( vecDir * 200.0 ); + pEntity->ZapInit( GetEnemy() ); + + GetAttachment( 4, vecHand, vecAngle ); + vecSrc = vecHand + GetAbsVelocity() * (m_flShootTime - gpGlobals->curtime); + vecDir = m_posTarget - GetAbsOrigin(); + VectorNormalize( vecDir ); + + vecSrc = vecSrc + vecDir * (gpGlobals->curtime - m_flShootTime); + + pEntity = (CNihilanthHVR *)CREATE_ENTITY( CNihilanthHVR, "nihilanth_energy_ball" ); + + pEntity->SetAbsOrigin( vecSrc ); + pEntity->SetAbsAngles( vecAngle ); + pEntity->SetOwnerEntity( this ); + pEntity->Spawn(); + + pEntity->SetAbsVelocity( vecDir * 200.0 ); + pEntity->ZapInit( GetEnemy() ); + + } + + m_flShootTime += 0.2; + } + } +} + +void CNPC_Nihilanth::FloatSequence( void ) +{ + if (m_irritation >= 2) + { + SetSequence( LookupSequence( "float_open" ) ); + } + else if (m_avelocity.y > 30) + { + SetSequence( LookupSequence( "walk_r" ) ); + } + else if (m_avelocity.y < -30) + { + SetSequence( LookupSequence( "walk_l" ) ); + } + else if (m_velocity.z > 30) + { + SetSequence( LookupSequence( "walk_u" ) ); + } + else if (m_velocity.z < -30) + { + SetSequence( LookupSequence( "walk_d" ) ); + } + else + { + SetSequence( LookupSequence( "float" ) ); + } +} + +void CNPC_Nihilanth::DyingThink( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1 ); + DispatchAnimEvents( this ); + StudioFrameAdvance( ); + + if ( m_lifeState == LIFE_ALIVE ) + { + CTakeDamageInfo info; + DeathSound( info ); + m_lifeState = LIFE_DYING; + + m_posDesired.z = m_flMaxZ; + } + + if ( GetAbsOrigin().z < m_flMaxZ && m_lifeState == LIFE_DEAD ) + { + SetAbsOrigin( Vector( GetAbsOrigin().x, GetAbsOrigin().y, m_flMaxZ ) ); + SetAbsVelocity( Vector( 0, 0, 0 ) ); + } + + if ( m_lifeState == LIFE_DYING ) + { + Flight( ); + + if (fabs( GetAbsOrigin().z - m_flMaxZ ) < 16) + { + CBaseEntity *pTrigger = NULL; + + SetAbsVelocity( Vector( 0, 0, 0 ) ); + SetGravity( 0 ); + + while( ( pTrigger = gEntList.FindEntityByName( pTrigger, m_szDeadUse ) ) != NULL ) + { + CLogicRelay *pRelay = (CLogicRelay*)pTrigger; + pRelay->m_OnTrigger.FireOutput( this, this ); + } + + m_lifeState = LIFE_DEAD; + } + } + + if ( IsSequenceFinished() ) + { + QAngle qAngularVel = GetLocalAngularVelocity(); + + qAngularVel.y += random->RandomFloat( -100, 100 ); + + if ( qAngularVel.y < -100) + qAngularVel.y = -100; + if ( qAngularVel.y > 100) + qAngularVel.y = 100; + + SetLocalAngularVelocity( qAngularVel ); + SetSequence( LookupSequence( "die1" ) ); + } + + if ( m_pBall ) + { + if (m_pBall->GetBrightness() > 0) + { + m_pBall->SetBrightness( MAX( 0, m_pBall->GetBrightness() - 7 ), 0 ); + } + else + { + UTIL_Remove( m_pBall ); + m_pBall = NULL; + } + } + + Vector vecDir, vecSrc; + QAngle vecAngles; + Vector vForward, vRight, vUp; + + AngleVectors( GetAbsAngles(), &vForward, &vRight, &vUp ); + + int iAttachment = random->RandomInt( 1, 4 ); + + do { + vecDir = Vector( random->RandomFloat( -1, 1 ), random->RandomFloat( -1, 1 ), random->RandomFloat( -1, 1 ) ); + } while (DotProduct( vecDir, vecDir) > 1.0); + + switch( random->RandomInt( 1, 4 )) + { + case 1: // head + vecDir.z = fabs( vecDir.z ) * 0.5; + vecDir = vecDir + 2 * vUp; + break; + case 2: // eyes + if (DotProduct( vecDir, vForward ) < 0) + vecDir = vecDir * -1; + + vecDir = vecDir + 2 * vForward; + break; + case 3: // left hand + if (DotProduct( vecDir, vRight ) > 0) + vecDir = vecDir * -1; + vecDir = vecDir - 2 * vRight; + break; + case 4: // right hand + if (DotProduct( vecDir, vRight ) < 0) + vecDir = vecDir * -1; + vecDir = vecDir + 2 * vRight; + break; + } + + GetAttachment( iAttachment, vecSrc, vecAngles ); + + trace_t tr; + + UTIL_TraceLine( vecSrc, vecSrc + vecDir * 4096, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); + + CBeam *pBeam = CBeam::BeamCreate( "sprites/laserbeam.vmt", 16 ); + + if ( pBeam == NULL ) + return; + + pBeam->PointEntInit( tr.endpos, this ); + pBeam->SetEndAttachment( iAttachment ); + pBeam->SetColor( 64, 128, 255 ); + pBeam->SetFadeLength( 50 ); + pBeam->SetBrightness( 255 ); + pBeam->SetNoise( 12 ); + pBeam->SetScrollRate( 1.0 ); + pBeam->LiveForTime( 0.5 ); + + GetAttachment( 2, vecSrc, vecAngles ); + CNihilanthHVR *pEntity = (CNihilanthHVR *)CREATE_ENTITY( CNihilanthHVR, "nihilanth_energy_ball" ); + + pEntity->SetAbsOrigin( vecSrc ); + pEntity->SetAbsAngles( GetAbsAngles() ); + pEntity->SetOwnerEntity( this ); + pEntity->SetAbsVelocity( Vector ( random->RandomFloat( -0.7, 0.7 ), random->RandomFloat( -0.7, 0.7 ), 1.0 ) * 600.0 ); + pEntity->Spawn(); + + pEntity->GreenBallInit(); + + return; +} + + +void CNPC_Nihilanth::HandleAnimEvent( animevent_t *pEvent ) +{ + switch( pEvent->event ) + { + case 1: // shoot + break; + case 2: // zen + if ( GetEnemy() != NULL) + { + Vector vOrigin; + QAngle vAngle; + CPASAttenuationFilter filter( this ); + + if ( random->RandomInt(0,4) == 0) + EmitSound( filter, entindex(), "Nihilanth.Attack" ); + + EmitSound( filter, entindex(), "Nihilanth.BallAttack" ); + + GetAttachment( 2, vOrigin, vAngle); + + CBroadcastRecipientFilter filterlight; + + te->DynamicLight( filterlight, 0.0, &vOrigin, 128, 128, 255, 0, 256, 1.0f, 128 ); + + GetAttachment( 3, vOrigin, vAngle); + te->DynamicLight( filterlight, 0.0, &vOrigin, 128, 128, 255, 0, 256, 1.0f, 128 ); + + m_flShootTime = gpGlobals->curtime; + m_flShootEnd = gpGlobals->curtime + 1.0; + } + break; + case 3: // prayer + if (GetEnemy() != NULL) + { + char szText[32]; + + Q_snprintf( szText, sizeof( szText ), "%s%d", m_szTeleportTouch, m_iTeleport ); + CBaseEntity *pTouch = gEntList.FindEntityByName( NULL, szText ); + + Q_snprintf( szText, sizeof( szText ), "%s%d", m_szTeleportUse, m_iTeleport ); + CBaseEntity *pTrigger = gEntList.FindEntityByName( NULL, szText ); + + if (pTrigger != NULL || pTouch != NULL) + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Nihilanth.Attack" ); + + Vector vecSrc; + QAngle vecAngles; + + GetAttachment( 2, vecSrc, vecAngles ); + + CNihilanthHVR *pEntity = (CNihilanthHVR *)CREATE_ENTITY( CNihilanthHVR, "nihilanth_energy_ball" ); + + pEntity->SetAbsOrigin( vecSrc ); + pEntity->SetAbsAngles( vecAngles ); + pEntity->SetOwnerEntity( this ); + pEntity->Spawn(); + + pEntity->TeleportInit( this, GetEnemy(), pTrigger, pTouch ); + + pEntity->SetAbsVelocity( GetAbsOrigin() - vecSrc ); + pEntity->SetAbsVelocity( Vector( GetAbsVelocity().x, GetAbsVelocity().y, GetAbsVelocity().z * 0.2 ) ); + + } + else + { + Vector vOrigin; + QAngle vAngle; + + m_iTeleport++; // unexpected failure + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Nihilanth.BallAttack" ); + + Msg( "nihilanth can't target %s\n", szText ); + + GetAttachment( 2, vOrigin, vAngle); + + CBroadcastRecipientFilter filterlight; + + te->DynamicLight( filterlight, 0.0, &vOrigin, 128, 128, 255, 0, 256, 1.0f, 128 ); + + GetAttachment( 3, vOrigin, vAngle); + te->DynamicLight( filterlight, 0.0, &vOrigin, 128, 128, 255, 0, 256, 1.0f, 128 ); + + m_flShootTime = gpGlobals->curtime; + m_flShootEnd = gpGlobals->curtime + 1.0; + } + } + break; + case 4: // get a sphere + { + if (m_hRecharger != NULL) + { + if (!EmitSphere( )) + { + m_hRecharger = NULL; + } + } + } + break; + case 5: // start up sphere machine + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Nihilanth.Recharge" ); + } + break; + case 6: + if ( GetEnemy() != NULL) + { + Vector vecSrc; + QAngle vecAngles; + GetAttachment( 3, vecSrc, vecAngles ); + + CNihilanthHVR *pEntity = (CNihilanthHVR *)CREATE_ENTITY( CNihilanthHVR, "nihilanth_energy_ball" ); + + pEntity->SetAbsOrigin( vecSrc ); + pEntity->SetAbsAngles( vecAngles ); + pEntity->SetOwnerEntity( this ); + pEntity->Spawn(); + + pEntity->SetAbsVelocity( GetAbsOrigin() - vecSrc ); + pEntity->ZapInit( GetEnemy() ); + } + break; + case 7: + /* + Vector vecSrc, vecAngles; + GetAttachment( 0, vecSrc, vecAngles ); + CNihilanthHVR *pEntity = (CNihilanthHVR *)Create( "nihilanth_energy_ball", vecSrc, pev->angles, edict() ); + pEntity->pev->velocity = Vector ( RANDOM_FLOAT( -0.7, 0.7 ), RANDOM_FLOAT( -0.7, 0.7 ), 1.0 ) * 600.0; + pEntity->GreenBallInit( ); + */ + break; + } +} + + +//========================================================= +// Controller bouncy ball attack +//========================================================= + + + +void CNihilanthHVR::Spawn( void ) +{ + Precache( ); +} + + +void CNihilanthHVR::Precache( void ) +{ + PrecacheModel("sprites/flare6.vmt"); + PrecacheModel("sprites/nhth1.vmt"); + PrecacheModel("sprites/exit1.vmt"); + PrecacheModel("sprites/tele1.vmt"); + PrecacheModel("sprites/animglow01.vmt"); + PrecacheModel("sprites/xspark4.vmt"); + PrecacheModel("sprites/muzzleflash3.vmt"); + + PrecacheModel("sprites/laserbeam.vmt"); + + PrecacheScriptSound( "NihilanthHVR.Zap" ); + PrecacheScriptSound( "NihilanthHVR.TeleAttack" ); +} + +void CNihilanthHVR::CircleInit( CBaseEntity *pTarget ) +{ + SetMoveType( MOVETYPE_FLY ); + SetSolid( SOLID_NONE ); + + UTIL_SetSize( this, Vector( 0, 0, 0), Vector(0, 0, 0)); + UTIL_SetOrigin( this, GetAbsOrigin() ); + + SetThink( &CNihilanthHVR::HoverThink ); + SetTouch( &CNihilanthHVR::BounceTouch ); + SetNextThink( gpGlobals->curtime + 0.1 ); + + CSprite *pSprite = SpriteInit( "sprites/muzzleflash3.vmt", this ); + + if ( pSprite ) + { + m_flBallScale = 2.0f; + pSprite->SetScale( 2.0 ); + pSprite->SetTransparency( kRenderTransAdd, 255, 224, 192, 255, kRenderFxNone ); + } + + SetTarget( pTarget ); +} + +CSprite *CNihilanthHVR::SpriteInit( const char *pSpriteName, CNihilanthHVR *pOwner ) +{ + pOwner->SetSprite( CSprite::SpriteCreate( pSpriteName, pOwner->GetAbsOrigin(), true ) ); + + CSprite *pSprite = (CSprite*)pOwner->GetSprite(); + + if ( pSprite ) + { + pSprite->SetAttachment( pOwner, 0 ); + pSprite->SetOwnerEntity( pOwner ); + pSprite->AnimateForTime( 5, 9999 ); + } + + return pSprite; +} + +void CNihilanthHVR::HoverThink( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1 ); + + if ( GetTarget() != NULL ) + { + CircleTarget( GetTarget()->GetAbsOrigin() + Vector( 0, 0, 16 * N_SCALE ) ); + } + else + { + UTIL_Remove( GetSprite() ); + UTIL_Remove( this ); + } +} + +void CNihilanthHVR::BounceTouch( CBaseEntity *pOther ) +{ + Vector vecDir = m_vecIdeal; + + VectorNormalize( vecDir ); + + const trace_t &tr = GetTouchTrace(); + + float n = -DotProduct(tr.plane.normal, vecDir); + + vecDir = 2.0 * tr.plane.normal * n + vecDir; + + m_vecIdeal = vecDir * m_vecIdeal.Length(); +} + +bool CNihilanthHVR::CircleTarget( Vector vecTarget ) +{ + bool fClose = false; + + vecTarget = vecTarget + Vector( 0, 0, 64 ); + + Vector vecDest = vecTarget; + Vector vecEst = GetAbsOrigin() + GetAbsVelocity() * 0.5; + Vector vecSrc = GetAbsOrigin(); + vecDest.z = 0; + vecEst.z = 0; + vecSrc.z = 0; + float d1 = (vecDest - vecSrc).Length() - 24 * N_SCALE; + float d2 = (vecDest - vecEst).Length() - 24 * N_SCALE; + + if ( m_vecIdeal == vec3_origin ) + { + m_vecIdeal = GetAbsVelocity(); + } + + if (d1 < 0 && d2 <= d1) + { + // ALERT( at_console, "too close\n"); + Vector vTemp = (vecDest - vecSrc); + + VectorNormalize( vTemp ); + + m_vecIdeal = m_vecIdeal - vTemp * 50; + } + + else if (d1 > 0 && d2 >= d1) + { + Vector vTemp = (vecDest - vecSrc); + + VectorNormalize( vTemp ); + + m_vecIdeal = m_vecIdeal + vTemp * 50; + } + + SetLocalAngularVelocity( QAngle( GetLocalAngularVelocity().x, GetLocalAngularVelocity().y, d1 * 20 ) ); + + if (d1 < 32) + { + fClose = true; + } + + m_vecIdeal = m_vecIdeal + Vector( random->RandomFloat( -2, 2 ), random->RandomFloat( -2, 2 ), random->RandomFloat( -2, 2 )); + + float flIdealZ = m_vecIdeal.z; + + m_vecIdeal = Vector( m_vecIdeal.x, m_vecIdeal.y, 0 ); + + VectorNormalize( m_vecIdeal ); + + m_vecIdeal = (m_vecIdeal * 200) + Vector( 0, 0, flIdealZ ); + + // move up/down + d1 = vecTarget.z - GetAbsOrigin().z; + if (d1 > 0 && m_vecIdeal.z < 200) + m_vecIdeal.z += 200; + else if (d1 < 0 && m_vecIdeal.z > -200) + m_vecIdeal.z -= 200; + + SetAbsVelocity( m_vecIdeal ); + + // ALERT( at_console, "%.0f %.0f %.0f\n", m_vecIdeal.x, m_vecIdeal.y, m_vecIdeal.z ); + return fClose; +} + + +void CNihilanthHVR::ZapInit( CBaseEntity *pEnemy ) +{ + SetMoveType( MOVETYPE_FLY ); + SetSolid( SOLID_BBOX ); + + CSprite *pSprite = SpriteInit( "sprites/nhth1.vmt", this ); + + if ( pSprite ) + { + m_flBallScale = 2.0f; + pSprite->SetScale( 2.0 ); + pSprite->SetTransparency( kRenderTransAdd, 255, 255, 255, 255, kRenderFxNone ); + } + + + Vector vVelocity = pEnemy->GetAbsOrigin() - GetAbsOrigin(); + VectorNormalize( vVelocity ); + SetAbsVelocity ( vVelocity * 300 ); + + SetEnemy( pEnemy ); + SetThink( &CNihilanthHVR::ZapThink ); + SetTouch( &CNihilanthHVR::ZapTouch ); + SetNextThink( gpGlobals->curtime + 0.1 ); + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "NihilanthHVR.Zap" ); +} + +void CNihilanthHVR::ZapThink( void ) +{ + SetNextThink( gpGlobals->curtime + 0.05 ); + + // check world boundaries + if ( GetEnemy() == NULL || GetAbsOrigin().x < -4096 || GetAbsOrigin().x > 4096 || GetAbsOrigin().y < -4096 || GetAbsOrigin().y > 4096 || GetAbsOrigin().z < -4096 || GetAbsOrigin().z > 4096) + { + SetTouch( NULL ); + UTIL_Remove( GetSprite() ); + UTIL_Remove( this ); + return; + } + + if ( GetAbsVelocity().Length() < 2000) + { + SetAbsVelocity( GetAbsVelocity() * 1.2 ); + } + + if (( GetEnemy()->WorldSpaceCenter() - GetAbsOrigin()).Length() < 256) + { + trace_t tr; + + UTIL_TraceLine( GetAbsOrigin(), GetEnemy()->WorldSpaceCenter(), MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); + + CBaseEntity *pEntity = tr.m_pEnt; + + if (pEntity != NULL && pEntity->m_takedamage ) + { + ClearMultiDamage( ); + CTakeDamageInfo info( this, this, sk_nihilanth_zap.GetFloat(), DMG_SHOCK ); + CalculateMeleeDamageForce( &info, (tr.endpos - tr.startpos), tr.endpos ); + pEntity->DispatchTraceAttack( info, GetAbsVelocity(), &tr ); + ApplyMultiDamage(); + } + + CBeam *pBeam = CBeam::BeamCreate( "sprites/laserbeam.vmt", 2.0f ); + + if ( pBeam == NULL ) + return; + + pBeam->PointEntInit( tr.endpos, this ); + pBeam->SetColor( 64, 196, 255 ); + pBeam->SetBrightness( 255 ); + pBeam->SetNoise( 7.2 ); + pBeam->SetScrollRate( 10 ); + pBeam->LiveForTime( 0.1 ); + + UTIL_EmitAmbientSound( GetSoundSourceIndex(), tr.endpos, "Controller.ElectroSound", 0.5, SNDLVL_NORM, 0, random->RandomInt( 140, 160 ) ); + + SetTouch( NULL ); + GetSprite()->SetThink( &CBaseEntity::SUB_Remove ); + SetThink( &CBaseEntity::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.2 ); + GetSprite()->SetNextThink( gpGlobals->curtime + 0.2 ); + return; + } + + CBroadcastRecipientFilter filterlight; + te->DynamicLight( filterlight, 0.0, &GetAbsOrigin(), 128, 128, 255, 0, 128, 10, 128 ); +} + + +void CNihilanthHVR::ZapTouch( CBaseEntity *pOther ) +{ + UTIL_EmitAmbientSound( GetSoundSourceIndex(), GetAbsOrigin(), "Controller.ElectroSound", 1.0, SNDLVL_NORM, 0, random->RandomInt( 90, 95 ) ); + + RadiusDamage( CTakeDamageInfo( this, this, 50, DMG_SHOCK ), GetAbsOrigin(), 125, CLASS_NONE, NULL ); + SetAbsVelocity( GetAbsVelocity() * 0 ); + + SetTouch( NULL ); + UTIL_Remove( GetSprite() ); + UTIL_Remove( this ); + SetNextThink( gpGlobals->curtime + 0.2 ); +} + +void CNihilanthHVR::AbsorbInit( void ) +{ + CBroadcastRecipientFilter filter; + + SetThink( &CNihilanthHVR::DissipateThink ); + SetRenderColorA( 255 ); + + SetBeam( CBeam::BeamCreate( "sprites/laserbeam.vmt", 8.0f ) ); + + CBeam *pBeam = (CBeam*)GetBeam(); + + if ( pBeam == NULL ) + return; + + pBeam->EntsInit( this, GetTarget() ); + pBeam->SetEndAttachment( 1 ); + pBeam->SetColor( 255, 128, 64 ); + pBeam->SetBrightness( 255 ); + pBeam->SetNoise( 18 ); +} + +void CNihilanthHVR::DissipateThink( void ) +{ + CSprite *pSprite = (CSprite*)GetSprite(); + + SetNextThink ( gpGlobals->curtime + 0.1 ); + + if ( m_flBallScale > 5.0) + { + UTIL_Remove( this ); + UTIL_Remove( GetSprite() ); + UTIL_Remove( GetBeam() ); + } + + pSprite->SetBrightness( pSprite->GetBrightness() - 7, 0 ); + + m_flBallScale += 0.1; + pSprite->SetScale( m_flBallScale ); + + if ( GetTarget() != NULL) + { + CircleTarget( GetTarget()->GetAbsOrigin() + Vector( 0, 0, 4096 ) ); + } + else + { + UTIL_Remove( this ); + UTIL_Remove( GetSprite() ); + UTIL_Remove( GetBeam() ); + } + +/* MESSAGE_BEGIN( MSG_BROADCAST, SVC_TEMPENTITY ); + WRITE_BYTE( TE_ELIGHT ); + WRITE_SHORT( entindex( ) ); // entity, attachment + WRITE_COORD( pev->origin.x ); // origin + WRITE_COORD( pev->origin.y ); + WRITE_COORD( pev->origin.z ); + WRITE_COORD( pev->renderamt ); // radius + WRITE_BYTE( 255 ); // R + WRITE_BYTE( 192 ); // G + WRITE_BYTE( 64 ); // B + WRITE_BYTE( 2 ); // life * 10 + WRITE_COORD( 0 ); // decay + MESSAGE_END();*/ +} + +void CNihilanthHVR::TeleportInit( CNPC_Nihilanth *pOwner, CBaseEntity *pEnemy, CBaseEntity *pTarget, CBaseEntity *pTouch ) +{ + SetMoveType( MOVETYPE_FLY ); + SetSolid( SOLID_BBOX ); + + SetModel( "" ); + + CSprite *pSprite = SpriteInit( "sprites/exit1.vmt", this ); + + if ( pSprite ) + { + m_flBallScale = 2.0f; + pSprite->SetScale( 2.0 ); + pSprite->SetTransparency( kRenderTransAdd, 255, 255, 255, 255, kRenderFxNone ); + } + + + m_pNihilanth = pOwner; + SetEnemy( pEnemy ); + SetTarget( pTarget ); + m_hTouch = pTouch; + + SetThink( &CNihilanthHVR::TeleportThink ); + SetTouch( &CNihilanthHVR::TeleportTouch ); + SetNextThink( gpGlobals->curtime + 0.1 ); + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "NihilanthHVR.TeleAttack" ); +} + +void CNihilanthHVR::MovetoTarget( Vector vecTarget ) +{ + if ( m_vecIdeal == vec3_origin ) + { + m_vecIdeal = GetAbsVelocity(); + } + + // accelerate + float flSpeed = m_vecIdeal.Length(); + + if (flSpeed > 300) + { + VectorNormalize( m_vecIdeal ); + m_vecIdeal= m_vecIdeal * 300; + } + + Vector vTemp = vecTarget - GetAbsOrigin(); + VectorNormalize( vTemp ); + + m_vecIdeal = m_vecIdeal + vTemp * 300; + SetAbsVelocity( m_vecIdeal ); +} + +void CNihilanthHVR::TeleportThink( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1 ); + + // check world boundaries + if ( GetEnemy() == NULL || !GetEnemy()->IsAlive() || GetAbsOrigin().x < -4096 || GetAbsOrigin().x > 4096 || GetAbsOrigin().y < -4096 || GetAbsOrigin().y > 4096 || GetAbsOrigin().z < -4096 || GetAbsOrigin().z > 4096) + { + StopSound( entindex(), "NihilanthHVR.TeleAttack" ); + UTIL_Remove( this ); + UTIL_Remove( GetSprite() ); + return; + } + + if (( GetEnemy()->WorldSpaceCenter() - GetAbsOrigin() ).Length() < 128) + { + StopSound( entindex(), "NihilanthHVR.TeleAttack" ); + UTIL_Remove( this ); + UTIL_Remove( GetSprite() ); + + if ( GetTarget() != NULL) + { + CLogicRelay *pRelay = (CLogicRelay*)GetTarget(); + pRelay->m_OnTrigger.FireOutput( this, this ); + } + + if ( m_hTouch != NULL && GetEnemy() != NULL ) + m_hTouch->Touch( GetEnemy() ); + } + else + { + MovetoTarget( GetEnemy()->WorldSpaceCenter( ) ); + } + + CBroadcastRecipientFilter filterlight; + te->DynamicLight( filterlight, 0.0, &GetAbsOrigin(), 0, 255, 0, 0, 256, 1.0, 256 ); +} + +void CNihilanthHVR::TeleportTouch( CBaseEntity *pOther ) +{ + CBaseEntity *pEnemy = GetEnemy(); + + if (pOther == pEnemy) + { + if (GetTarget() != NULL) + { + if ( GetTarget() != NULL) + { + CLogicRelay *pRelay = (CLogicRelay*)GetTarget(); + pRelay->m_OnTrigger.FireOutput( this, this ); + } + } + + if (m_hTouch != NULL && pEnemy != NULL ) + m_hTouch->Touch( pEnemy ); + } + else + { + m_pNihilanth->MakeFriend( GetAbsOrigin() ); + } + + SetTouch( NULL ); + StopSound( entindex(), "NihilanthHVR.TeleAttack" ); + UTIL_Remove( this ); + UTIL_Remove( GetSprite() ); +} + +void CNihilanthHVR::GreenBallInit( ) +{ + SetMoveType( MOVETYPE_FLY ); + SetSolid( SOLID_BBOX ); + + SetModel( "" ); + + CSprite *pSprite = SpriteInit( "sprites/exit1.spr", this ); + + if ( pSprite ) + { + pSprite->SetScale( 1.0 ); + pSprite->SetTransparency( kRenderTransAdd, 255, 255, 255, 255, kRenderFxNone ); + } + + SetTouch( &CNihilanthHVR::RemoveTouch ); +} + +void CNihilanthHVR::RemoveTouch( CBaseEntity *pOther ) +{ + StopSound( entindex(), "NihilanthHVR.TeleAttack" ); + UTIL_Remove( this ); + UTIL_Remove( GetSprite() ); +} diff --git a/game/server/hl1/hl1_npc_osprey.cpp b/game/server/hl1/hl1_npc_osprey.cpp new file mode 100644 index 0000000..65b2de8 --- /dev/null +++ b/game/server/hl1/hl1_npc_osprey.cpp @@ -0,0 +1,1589 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "beam_shared.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "ai_senses.h" +#include "hl1_npc_hgrunt.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "basecombatweapon.h" +#include "hl1_basegrenade.h" +#include "soundenvelope.h" +#include "hl1_CBaseHelicopter.h" +#include "IEffects.h" +#include "smoke_trail.h" + +extern short g_sModelIndexFireball; + +typedef struct +{ + int isValid; + EHANDLE hGrunt; + Vector vecOrigin; + Vector vecAngles; +} t_ospreygrunt; + +#define LOADED_WITH_GRUNTS 0 //WORST NAME EVER! +#define UNLOADING_GRUNTS 1 +#define GRUNTS_DEPLOYED 2 //Waiting for them to finish repelin +#define SF_WAITFORTRIGGER 0x40 +#define MAX_CARRY 24 +#define DEFAULT_SPEED 250 +#define HELICOPTER_THINK_INTERVAL 0.1 + +class CNPC_Osprey : public CBaseHelicopter +{ + DECLARE_CLASS( CNPC_Osprey, CBaseHelicopter ); +public: + + int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + + void Spawn( void ); + void Precache( void ); + Class_T Classify( void ) { return CLASS_NONE; }; + int BloodColor( void ) { return DONT_BLEED; } + + void FindAllThink( void ); + void DeployThink( void ); + bool HasDead( void ); + void Flight( void ); + void HoverThink( void ); + CAI_BaseNPC *MakeGrunt( Vector vecSrc ); + + void InitializeRotorSound( void ); + void PrescheduleThink( void ); + + void DyingThink( void ); + + void CrashTouch( CBaseEntity *pOther ); + +/* + + void CrashTouch( CBaseEntity *pOther ); + void DyingThink( void ); + void CommandUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); +*/ + void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + + float m_startTime; + + float m_flIdealtilt; + float m_flRotortilt; + + float m_flRightHealth; + float m_flLeftHealth; + + int m_iUnits; + EHANDLE m_hGrunt[MAX_CARRY]; + Vector m_vecOrigin[MAX_CARRY]; + EHANDLE m_hRepel[4]; + + int m_iSoundState; + int m_iSpriteTexture; + + int m_iPitch; + + int m_iExplode; + int m_iTailGibs; + int m_iBodyGibs; + int m_iEngineGibs; + + int m_iDoLeftSmokePuff; + int m_iDoRightSmokePuff; + + int m_iRepelState; + float m_flPrevGoalVel; + + int m_iRotorAngle; + int m_nDebrisModel; + + CHandle<SmokeTrail> m_hLeftSmoke; + CHandle<SmokeTrail> m_hRightSmoke; + + DECLARE_DATADESC(); + + int m_iNextCrashModel; //which gib to explode with next +}; + +LINK_ENTITY_TO_CLASS( monster_osprey, CNPC_Osprey ); + +BEGIN_DATADESC( CNPC_Osprey ) + DEFINE_FIELD( m_startTime, FIELD_TIME ), + + DEFINE_FIELD( m_flIdealtilt, FIELD_FLOAT ), + DEFINE_FIELD( m_flRotortilt, FIELD_FLOAT ), + + DEFINE_FIELD( m_flRightHealth, FIELD_FLOAT ), + DEFINE_FIELD( m_flLeftHealth, FIELD_FLOAT ), + + DEFINE_FIELD( m_iRepelState, FIELD_INTEGER ), + + DEFINE_FIELD( m_iUnits, FIELD_INTEGER ), + DEFINE_ARRAY( m_hGrunt, FIELD_EHANDLE, MAX_CARRY ), + DEFINE_ARRAY( m_vecOrigin, FIELD_POSITION_VECTOR, MAX_CARRY ), + DEFINE_ARRAY( m_hRepel, FIELD_EHANDLE, 4 ), + + // DEFINE_FIELD( m_iTailGibs, FIELD_INTEGER ), + // DEFINE_FIELD( m_iBodyGibs, FIELD_INTEGER ), + // DEFINE_FIELD( m_iEngineGibs, FIELD_INTEGER ), + // DEFINE_FIELD( m_nDebrisModel, FIELD_INTEGER ), + + // DEFINE_FIELD( m_iSoundState, FIELD_INTEGER ), + // DEFINE_FIELD( m_iSpriteTexture, FIELD_INTEGER ), + // DEFINE_FIELD( m_iPitch, FIELD_INTEGER ), + + DEFINE_FIELD( m_flPrevGoalVel, FIELD_FLOAT ), + DEFINE_FIELD( m_iRotorAngle, FIELD_INTEGER ), + + DEFINE_FIELD( m_hLeftSmoke, FIELD_EHANDLE ), + DEFINE_FIELD( m_hRightSmoke, FIELD_EHANDLE ), + + DEFINE_FIELD( m_iNextCrashModel, FIELD_INTEGER ), + + DEFINE_FIELD( m_iDoLeftSmokePuff, FIELD_INTEGER ), + DEFINE_FIELD( m_iDoRightSmokePuff, FIELD_INTEGER ), + + //DEFINE_FIELD( m_iExplode, FIELD_INTEGER ), + + DEFINE_THINKFUNC( FindAllThink ), + DEFINE_THINKFUNC( DeployThink ), + + DEFINE_ENTITYFUNC( CrashTouch ), + +/* DEFINE_FUNCTION ( HoverThink ), + DEFINE_FUNCTION ( DyingThink ), + DEFINE_FUNCTION ( CommandUse ),*/ +END_DATADESC() + + +void CNPC_Osprey::Spawn( void ) +{ + Precache( ); + // motor + SetModel( "models/osprey.mdl" ); + + BaseClass::Spawn(); + + Vector mins, maxs; + ExtractBbox( 0, mins, maxs ); + UTIL_SetSize( this, mins, maxs ); + UTIL_SetOrigin( this, GetAbsOrigin() ); + + AddFlag( FL_NPC ); + m_takedamage = DAMAGE_YES; + m_flRightHealth = 200; + m_flLeftHealth = 200; + m_iHealth = 400; + + m_flFieldOfView = 0; // 180 degrees + + SetSequence( 0 ); + ResetSequenceInfo( ); + SetCycle( random->RandomInt( 0,0xFF ) ); + +// InitBoneControllers(); + + m_startTime = gpGlobals->curtime + 1; + + //FindAllThink(); +// SetUse( CommandUse ); + +/* if (!( m_spawnflags & SF_WAITFORTRIGGER)) + { + SetThink( gpGlobals->curtime + 1.0 ); + }*/ + + m_flMaxSpeed = (float)BASECHOPPER_MAX_SPEED / 2; + + m_iRepelState = LOADED_WITH_GRUNTS; + m_flPrevGoalVel = 9999; + + m_iRotorAngle = -1; + SetBoneController( 0, m_iRotorAngle ); + + m_hLeftSmoke = NULL; + m_hRightSmoke = NULL; +} + + +void CNPC_Osprey::Precache( void ) +{ + UTIL_PrecacheOther( "monster_human_grunt" ); + + PrecacheModel("models/osprey.mdl"); + PrecacheModel("models/HVR.mdl"); + + m_iSpriteTexture = PrecacheModel( "sprites/rope.vmt" ); + + m_iExplode = PrecacheModel( "sprites/fexplo.vmt" ); + m_iTailGibs = PrecacheModel( "models/osprey_tailgibs.mdl" ); + m_iBodyGibs = PrecacheModel( "models/osprey_bodygibs.mdl" ); + m_iEngineGibs = PrecacheModel( "models/osprey_enginegibs.mdl" ); + + m_nDebrisModel = PrecacheModel( "models/mechgibs.mdl" ); + + PrecacheScriptSound( "Apache.RotorSpinup" ); + + BaseClass::Precache(); +} + +void CNPC_Osprey::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + float flDamage = info.GetDamage(); + + // Hit right engine + if (ptr->hitgroup == 3 ) + { + if( m_flRightHealth <= 0 ) + return; + else + m_flRightHealth -= flDamage; + + if( m_flRightHealth <= 0 ) + { + Assert( m_hRightSmoke == NULL ); + + if ( (m_hRightSmoke = SmokeTrail::CreateSmokeTrail()) != NULL ) + { + m_hRightSmoke->m_Opacity = 1.0f; + m_hRightSmoke->m_SpawnRate = 60; + m_hRightSmoke->m_ParticleLifetime = 1.3f; + m_hRightSmoke->m_StartColor.Init( 0.65f, 0.65f , 0.65f ); + m_hRightSmoke->m_EndColor.Init( 0.65f, 0.65f, 0.65f ); + m_hRightSmoke->m_StartSize = 12; + m_hRightSmoke->m_EndSize = 80; + m_hRightSmoke->m_SpawnRadius = 8; + m_hRightSmoke->m_MinSpeed = 2; + m_hRightSmoke->m_MaxSpeed = 24; + + m_hRightSmoke->SetLifetime( 1e6 ); + m_hRightSmoke->FollowEntity( this, "right" ); + } + } + } + + // Hit left engine + if (ptr->hitgroup == 2 ) + { + if( m_flLeftHealth <= 0 ) + return; + else + m_flLeftHealth -= flDamage; + + if( m_flLeftHealth <= 0 ) + { + //create smoke trail + Assert( m_hLeftSmoke == NULL ); + + if ( (m_hLeftSmoke = SmokeTrail::CreateSmokeTrail()) != NULL ) + { + m_hLeftSmoke->m_Opacity = 1.0f; + m_hLeftSmoke->m_SpawnRate = 60; + m_hLeftSmoke->m_ParticleLifetime = 1.3f; + m_hLeftSmoke->m_StartColor.Init( 0.65f, 0.65f , 0.65f ); + m_hLeftSmoke->m_EndColor.Init( 0.65f, 0.65f, 0.65f ); + m_hLeftSmoke->m_StartSize = 12; + m_hLeftSmoke->m_EndSize = 64; + m_hLeftSmoke->m_SpawnRadius = 8; + m_hLeftSmoke->m_MinSpeed = 2; + m_hLeftSmoke->m_MaxSpeed = 24; + + m_hLeftSmoke->SetLifetime( 1e6 ); + m_hLeftSmoke->FollowEntity( this, "left" ); + } + } + } + + // hit hard, hits cockpit, hits engines + if (flDamage > 50 || ptr->hitgroup == 1 || ptr->hitgroup == 2 || ptr->hitgroup == 3) + { + AddMultiDamage( info, this ); + } + else + { + g_pEffects->Sparks( ptr->endpos ); + } +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CNPC_Osprey::InitializeRotorSound( void ) +{ + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + + CPASAttenuationFilter filter( this ); + m_pRotorSound = controller.SoundCreate( filter, entindex(), CHAN_STATIC, "Apache.RotorSpinup", 0.2 ); + + BaseClass::InitializeRotorSound(); +} + +void CNPC_Osprey::FindAllThink( void ) +{ + CBaseEntity *pEntity = NULL; + + m_iUnits = 0; + while ( ( pEntity = gEntList.FindEntityByClassname( pEntity, "monster_human_grunt" ) ) != NULL) + { + if ( m_iUnits > MAX_CARRY ) + break; + + if (pEntity->IsAlive()) + { + m_hGrunt[m_iUnits] = pEntity; + m_vecOrigin[m_iUnits] = pEntity->GetAbsOrigin(); + m_iUnits++; + } + } + + if (m_iUnits == 0) + { + Msg( "osprey error: no grunts to resupply\n"); + UTIL_Remove( this ); + return; + } + + m_startTime = 0.0f; +} + +void CNPC_Osprey::DeployThink( void ) +{ + Vector vecForward; + Vector vecRight; + Vector vecUp; + Vector vecSrc; + + SetLocalAngularVelocity( QAngle ( 0, 0, 0 ) ); + SetAbsVelocity( Vector ( 0, 0, 0 ) ); + + AngleVectors( GetAbsAngles(), &vecForward, &vecRight, &vecUp ); + + trace_t tr; + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + Vector( 0, 0, -4096.0), MASK_SOLID_BRUSHONLY, this,COLLISION_GROUP_NONE, &tr); + CSoundEnt::InsertSound ( SOUND_DANGER, tr.endpos, 400, 0.3 ); + + vecSrc = GetAbsOrigin() + vecForward * 32 + vecRight * 100 + vecUp * -96; + m_hRepel[0] = MakeGrunt( vecSrc ); + + vecSrc = GetAbsOrigin() + vecForward * -64 + vecRight * 100 + vecUp * -96; + m_hRepel[1] = MakeGrunt( vecSrc ); + + vecSrc = GetAbsOrigin() + vecForward * 32 + vecRight * -100 + vecUp * -96; + m_hRepel[2] = MakeGrunt( vecSrc ); + + vecSrc = GetAbsOrigin() + vecForward * -64 + vecRight * -100 + vecUp * -96; + m_hRepel[3] = MakeGrunt( vecSrc ); + + m_iRepelState = GRUNTS_DEPLOYED; + + HoverThink(); +} + +bool CNPC_Osprey::HasDead( ) +{ + for (int i = 0; i < m_iUnits; i++) + { + if (m_hGrunt[i] == NULL || !m_hGrunt[i]->IsAlive()) + { + return TRUE; + } + else + { + m_vecOrigin[i] = m_hGrunt[i]->GetAbsOrigin(); // send them to where they died + } + } + return FALSE; +} + +void CNPC_Osprey::HoverThink( void ) +{ + int i = 0; + for (i = 0; i < 4; i++) + { + CBaseEntity *pRepel = (CBaseEntity*)m_hRepel[i]; + + if ( pRepel != NULL && pRepel->m_iHealth > 0 && pRepel->GetMoveType() == MOVETYPE_FLYGRAVITY ) + { + break; + } + } + + if ( i == 4 ) + m_iRepelState = LOADED_WITH_GRUNTS; + + if( m_iRepelState != LOADED_WITH_GRUNTS ) + { + // angle of engines should approach vertical + m_iRotorAngle = UTIL_Approach(0, m_iRotorAngle, 5); + } +} + +CAI_BaseNPC *CNPC_Osprey::MakeGrunt( Vector vecSrc ) +{ + CBaseEntity *pEntity; + CAI_BaseNPC *pGrunt; + + trace_t tr; + UTIL_TraceLine( vecSrc, vecSrc + Vector( 0, 0, -4096.0), MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr); + + if ( tr.m_pEnt && tr.m_pEnt->GetSolid() != SOLID_BSP) + return NULL; + + for (int i = 0; i < m_iUnits; i++) + { + if (m_hGrunt[i] == NULL || !m_hGrunt[i]->IsAlive()) + { + if (m_hGrunt[i] != NULL && m_hGrunt[i]->m_nRenderMode == kRenderNormal) + { + m_hGrunt[i]->SUB_StartFadeOut( ); + } + pEntity = Create( "monster_human_grunt", vecSrc, GetAbsAngles() ); + pGrunt = pEntity->MyNPCPointer( ); + pGrunt->SetMoveType( MOVETYPE_FLYGRAVITY ); + pGrunt->SetGravity( 0.0001 ); + + Vector spd = Vector( 0, 0, random->RandomFloat( -196, -128 ) ); + pGrunt->SetLocalVelocity( spd ); + pGrunt->SetActivity( ACT_GLIDE ); + pGrunt->SetGroundEntity( NULL ); + + pGrunt->SetOwnerEntity(this); + + + CBeam *pBeam = CBeam::BeamCreate( "sprites/rope.vmt", 1.0 ); + pBeam->PointEntInit( vecSrc + Vector(0,0,112), pGrunt ); + pBeam->SetBeamFlags( FBEAM_SOLID ); + pBeam->SetColor( 255, 255, 255 ); + pBeam->SetThink( &CBaseEntity::SUB_Remove ); + pBeam->SetNextThink( gpGlobals->curtime + -4096.0 * tr.fraction / pGrunt->GetAbsVelocity().z + 0.5 ); + + + // ALERT( at_console, "%d at %.0f %.0f %.0f\n", i, m_vecOrigin[i].x, m_vecOrigin[i].y, m_vecOrigin[i].z ); + pGrunt->m_vecLastPosition = m_vecOrigin[i]; + m_hGrunt[i] = pGrunt; + return pGrunt; + } + } + // ALERT( at_console, "none dead\n"); + return NULL; +} + +void CNPC_Osprey::Flight( void ) +{ + if ( m_iRepelState == LOADED_WITH_GRUNTS ) + { + BaseClass::Flight(); + + // adjust angle of osprey rotors + if ( m_angleVelocity > 0 ) + { + m_iRotorAngle = UTIL_Approach(-45, m_iRotorAngle, 5 * (m_angleVelocity / 10)); + } + else + { + m_iRotorAngle = UTIL_Approach(-1, m_iRotorAngle, 5); + } + SetBoneController( 0, m_iRotorAngle ); + + } +} + +void CNPC_Osprey::PrescheduleThink( void ) +{ + BaseClass::PrescheduleThink(); + + StudioFrameAdvance( ); + + if ( m_startTime != 0.0 && m_startTime <= gpGlobals->curtime ) + FindAllThink(); + + if ( GetGoalEnt() ) + { + if ( m_flPrevGoalVel != GetGoalEnt()->m_flSpeed ) + { + if ( m_flPrevGoalVel == 0 && GetGoalEnt()->m_flSpeed != 0 ) + { + if ( HasDead() && m_iRepelState == LOADED_WITH_GRUNTS ) + m_iRepelState = UNLOADING_GRUNTS; + } + + m_flPrevGoalVel = GetGoalEnt()->m_flSpeed; + } + } + + if ( m_iRepelState == UNLOADING_GRUNTS ) + DeployThink(); + else if ( m_iRepelState == GRUNTS_DEPLOYED ) + HoverThink(); +} + + + +//----------------------------------------------------------------------------- +// Purpose: Lame, temporary death +//----------------------------------------------------------------------------- +void CNPC_Osprey::DyingThink( void ) +{ + StudioFrameAdvance( ); + SetNextThink( gpGlobals->curtime + 0.1f ); + + if( gpGlobals->curtime > m_flNextCrashExplosion ) + { + CPASFilter filter( GetAbsOrigin() ); + Vector pos; + QAngle dummy; + + int rand = RandomInt(0,10); + + if( rand < 4 ) + { + int iAttach = LookupAttachment( rand % 2 ? "left" : "right" ); + GetAttachment( iAttach, pos, dummy ); + } + else + { + pos = GetAbsOrigin(); + pos.x += random->RandomFloat( -150, 150 ); + pos.y += random->RandomFloat( -150, 150 ); + pos.z += random->RandomFloat( -150, -50 ); + } + + te->Explosion( filter, 0.0, &pos, g_sModelIndexFireball, 10, 15, TE_EXPLFLAG_NONE, 100, 0 ); + m_flNextCrashExplosion = gpGlobals->curtime + random->RandomFloat( 0.4, 0.7 ); + + Vector vecSize = Vector( 500, 500, 60 ); + + switch( m_iNextCrashModel ) + { + case 0: + { + te->BreakModel( filter, 0.0, GetAbsOrigin(), vec3_angle, + vecSize, vec3_origin, m_iTailGibs, 100, 0, 2.5, BREAK_METAL ); + break; + } + case 1: + { + te->BreakModel( filter, 0.0, GetAbsOrigin(), vec3_angle, + vecSize, vec3_origin, m_iBodyGibs, 100, 0, 2.5, BREAK_METAL ); + break; + } + case 2: + { + te->BreakModel( filter, 0.0, GetAbsOrigin(), vec3_angle, + vecSize, vec3_origin, m_iEngineGibs, 100, 0, 2.5, BREAK_METAL ); + break; + } + case 3: + { + te->BreakModel( filter, 0.0, GetAbsOrigin(), vec3_angle, + vecSize, vec3_origin, m_nDebrisModel, 100, 0, 2.5, BREAK_METAL ); + break; + } + } + + m_iNextCrashModel++; + if( m_iNextCrashModel > 3 ) m_iNextCrashModel = 0; + } + + QAngle angVel = GetLocalAngularVelocity(); + if( angVel.y < 400 ) + { + angVel.y *= 1.1; + SetLocalAngularVelocity( angVel ); + } + Vector vecImpulse( 0, 0, 0 ); + // add gravity + vecImpulse.z -= 38.4; // 32ft/sec + ApplyAbsVelocityImpulse( vecImpulse ); + +} + +void CNPC_Osprey::CrashTouch( CBaseEntity *pOther ) +{ + Vector vecSize = Vector( 120, 120, 30 ); + CPVSFilter filter( GetAbsOrigin() ); + + te->BreakModel( filter, 0.0, GetAbsOrigin(), vec3_angle, + vecSize, vec3_origin, m_iTailGibs, 100, 0, 2.5, BREAK_METAL ); + + if( m_hLeftSmoke ) + { + m_hLeftSmoke->SetLifetime(0.1f); + m_hLeftSmoke = NULL; + } + + if( m_hRightSmoke ) + { + m_hRightSmoke->SetLifetime(0.1f); + m_hRightSmoke = NULL; + } + + BaseClass::CrashTouch( pOther ); +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +BEGIN_DATADESC( CBaseHelicopter ) + + DEFINE_THINKFUNC( HelicopterThink ), + DEFINE_THINKFUNC( CallDyingThink ), + DEFINE_ENTITYFUNC( CrashTouch ), + DEFINE_ENTITYFUNC( FlyTouch ), + + DEFINE_SOUNDPATCH( m_pRotorSound ), + + DEFINE_FIELD( m_flForce, FIELD_FLOAT ), + DEFINE_FIELD( m_fHelicopterFlags, FIELD_INTEGER), + DEFINE_FIELD( m_vecDesiredFaceDir, FIELD_VECTOR ), + DEFINE_FIELD( m_vecDesiredPosition,FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecGoalOrientation,FIELD_VECTOR ), + DEFINE_FIELD( m_flLastSeen, FIELD_TIME ), + DEFINE_FIELD( m_flPrevSeen, FIELD_TIME ), +// DEFINE_FIELD( m_iSoundState, FIELD_INTEGER ), // Don't save, precached + DEFINE_FIELD( m_vecTarget, FIELD_VECTOR ), + DEFINE_FIELD( m_vecTargetPosition, FIELD_POSITION_VECTOR ), + + DEFINE_FIELD( m_angleVelocity, FIELD_FLOAT ), + DEFINE_FIELD( m_flNextCrashExplosion, FIELD_TIME ), + + DEFINE_FIELD( m_flMaxSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_flMaxSpeedFiring, FIELD_FLOAT ), + DEFINE_FIELD( m_flGoalSpeed, FIELD_FLOAT ), + DEFINE_KEYFIELD( m_flInitialSpeed, FIELD_FLOAT, "InitialSpeed" ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_STRING, "ChangePathCorner", InputChangePathCorner), + DEFINE_INPUTFUNC( FIELD_VOID, "Activate", InputActivate), + + // Outputs + DEFINE_OUTPUT(m_AtTarget, "AtPathCorner" ), + DEFINE_OUTPUT(m_LeaveTarget, "LeavePathCorner" ),//<<TEMP>> Undone + +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CBaseHelicopter, DT_BaseHelicopter ) +END_SEND_TABLE() + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +// Notes : Have your derived Helicopter's Spawn() function call this one FIRST +//------------------------------------------------------------------------------ +void CBaseHelicopter::Precache( void ) +{ +} + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +// Notes : Have your derived Helicopter's Spawn() function call this one FIRST +//------------------------------------------------------------------------------ +void CBaseHelicopter::Spawn( void ) +{ + Precache( ); + + SetSolid( SOLID_BBOX ); + SetMoveType( MOVETYPE_STEP ); + AddFlag( FL_FLY ); + + m_lifeState = LIFE_ALIVE; + + // This base class assumes the helicopter has no guns or missiles. + // Set the appropriate flags in your derived class' Spawn() function. + m_fHelicopterFlags &= ~BITS_HELICOPTER_MISSILE_ON; + m_fHelicopterFlags &= ~BITS_HELICOPTER_GUN_ON; + + m_pRotorSound = NULL; + + SetCycle( 0 ); + ResetSequenceInfo(); + + AddFlag( FL_NPC ); + + m_flMaxSpeed = BASECHOPPER_MAX_SPEED; + m_flMaxSpeedFiring = BASECHOPPER_MAX_FIRING_SPEED; + m_takedamage = DAMAGE_AIM; + + // Don't start up if the level designer has asked the + // helicopter to start disabled. + if ( !(m_spawnflags & SF_AWAITINPUT) ) + { + Startup(); + SetNextThink( gpGlobals->curtime + 1.0f ); + } + +} + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +bool CBaseHelicopter::FireGun( void ) +{ + return true; +} + + +//------------------------------------------------------------------------------ +// Purpose : The main think function for the helicopters +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBaseHelicopter::HelicopterThink( void ) +{ + SetNextThink( gpGlobals->curtime + HELICOPTER_THINK_INTERVAL ); + + // Don't keep this around for more than one frame. + ClearCondition( COND_ENEMY_DEAD ); + + // Animate and dispatch animation events. + DispatchAnimEvents( this ); + + PrescheduleThink(); + + ShowDamage( ); + + // ----------------------------------------------- + // If AI is disabled, kill any motion and return + // ----------------------------------------------- + if (CAI_BaseNPC::m_nDebugBits & bits_debugDisableAI) + { + SetAbsVelocity( vec3_origin ); + SetLocalAngularVelocity( vec3_angle ); + SetNextThink( gpGlobals->curtime + HELICOPTER_THINK_INTERVAL ); + return; + } + + Hunt(); + + HelicopterPostThink(); +} + + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBaseHelicopter::Hunt( void ) +{ + FlyPathCorners( ); + + if( HasCondition( COND_ENEMY_DEAD ) ) + { + SetEnemy( NULL ); + } + + // Look for my best enemy. If I change enemies, + // be sure and change my prevseen/lastseen timers. + if( m_lifeState == LIFE_ALIVE ) + { + GetSenses()->Look( 4092 ); + + ChooseEnemy(); + + if( HasEnemy() ) + { + CheckEnemy( GetEnemy() ); + + if (FVisible( GetEnemy() )) + { + if (m_flLastSeen < gpGlobals->curtime - 2) + { + m_flPrevSeen = gpGlobals->curtime; + } + + m_flLastSeen = gpGlobals->curtime; + m_vecTargetPosition = GetEnemy()->WorldSpaceCenter(); + } + } + else + { + // look at where we're going instead + m_vecTargetPosition = m_vecDesiredPosition; + } + } + else + { + // If we're dead or dying, forget our enemy and don't look for new ones(sjb) + SetEnemy( NULL ); + } + + if ( 1 ) + { + Vector targetDir = m_vecTargetPosition - GetAbsOrigin(); + Vector desiredDir = m_vecDesiredPosition - GetAbsOrigin(); + + VectorNormalize( targetDir ); + VectorNormalize( desiredDir ); + + if ( !IsCrashing() && m_flLastSeen + 5 > gpGlobals->curtime ) //&& DotProduct( targetDir, desiredDir) > 0.25) + { + // If we've seen the target recently, face the target. + //Msg( "Facing Target \n" ); + m_vecDesiredFaceDir = targetDir; + } + else + { + // Face our desired position. + // Msg( "Facing Position\n" ); + m_vecDesiredFaceDir = desiredDir; + } + } + else + { + // Face the way the path corner tells us to. + //Msg( "Facing my path corner\n" ); + m_vecDesiredFaceDir = m_vecGoalOrientation; + } + + Flight(); + + UpdatePlayerDopplerShift( ); + + // ALERT( at_console, "%.0f %.0f %.0f\n", gpGlobals->curtime, m_flLastSeen, m_flPrevSeen ); + if (m_fHelicopterFlags & BITS_HELICOPTER_GUN_ON) + { + if ( (m_flLastSeen + 1 > gpGlobals->curtime) && (m_flPrevSeen + 2 < gpGlobals->curtime) ) + { + if (FireGun( )) + { + // slow down if we're firing + if (m_flGoalSpeed > m_flMaxSpeedFiring ) + { + m_flGoalSpeed = m_flMaxSpeedFiring; + } + } + } + } + + if (m_fHelicopterFlags & BITS_HELICOPTER_MISSILE_ON) + { + AimRocketGun(); + } + + // Finally, forget dead enemies. + if( GetEnemy() != NULL && !GetEnemy()->IsAlive() ) + { + SetEnemy( NULL ); + } +} + + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBaseHelicopter::FlyPathCorners( void ) +{ + + if ( GetGoalEnt() == NULL && m_target != NULL_STRING )// this monster has a target + { + SetGoalEnt( gEntList.FindEntityByName( NULL, m_target ) ); + if (GetGoalEnt()) + { + m_vecDesiredPosition = GetGoalEnt()->GetLocalOrigin(); + + // FIXME: orienation removed from path_corners! + AngleVectors( GetGoalEnt()->GetLocalAngles(), &m_vecGoalOrientation ); + } + } + + // walk route + if (GetGoalEnt()) + { + // ALERT( at_console, "%.0f\n", flLength ); + if ( HasReachedTarget( ) ) + { + // If we get this close to the desired position, it's assumed that we've reached + // the desired position, so move on. + + // Fire target that I've reached my goal + m_AtTarget.FireOutput( GetGoalEnt(), this ); + + OnReachedTarget( GetGoalEnt() ); + + SetGoalEnt( gEntList.FindEntityByName( NULL, GetGoalEnt()->m_target ) ); + + if (GetGoalEnt()) + { + m_vecDesiredPosition = GetGoalEnt()->GetAbsOrigin(); + + // FIXME: orienation removed from path_corners! + AngleVectors( GetGoalEnt()->GetLocalAngles(), &m_vecGoalOrientation ); + + // NDebugOverlay::Box( m_vecDesiredPosition, Vector( -16, -16, -16 ), Vector( 16, 16, 16 ), 0,255,0, false, 30.0); + } + } + } + else + { + // If we can't find a new target, just stay where we are. + m_vecDesiredPosition = GetAbsOrigin(); + } + +} + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBaseHelicopter::UpdatePlayerDopplerShift( ) +{ + // ----------------------------- + // make rotor, engine sounds + // ----------------------------- + if (m_iSoundState == 0) + { + // Sound startup. + InitializeRotorSound(); + } + else + { + CBaseEntity *pPlayer = NULL; + + // UNDONE: this needs to send different sounds to every player for multiplayer. + // FIXME: this isn't the correct way to find a player!!! + pPlayer = gEntList.FindEntityByName( NULL, "!player" ); + if (pPlayer) + { + Vector dir = pPlayer->GetLocalOrigin() - GetLocalOrigin(); + VectorNormalize(dir); + + float velReceiver = -DotProduct( pPlayer->GetAbsVelocity(), dir ); + float velTransmitter = -DotProduct( GetAbsVelocity(), dir ); + // speed of sound == 13049in/s + int iPitch = 100 * ((1 - velReceiver / 13049) / (1 + velTransmitter / 13049)); + + // clamp pitch shifts + if (iPitch > 250) + iPitch = 250; + if (iPitch < 50) + iPitch = 50; + + // Msg( "Pitch:%d\n", iPitch ); + + UpdateRotorSoundPitch( iPitch ); + //Msg( "%.0f\n", pitch ); + //Msg( "%.0f\n", flVol ); + } + else + { + Msg( "Chopper didn't find a player!\n" ); + } + } +} + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void CBaseHelicopter::Flight( void ) +{ + if( GetFlags() & FL_ONGROUND ) + { + //This would be really bad. + SetGroundEntity( NULL ); + } + + // Generic speed up + if (m_flGoalSpeed < m_flMaxSpeed) + { + m_flGoalSpeed += GetAcceleration(); + } + +// NDebugOverlay::Line(GetAbsOrigin(), m_vecDesiredPosition, 0,0,255, true, 0.1); + + // estimate where I'll be facing in one seconds + Vector forward, right, up; + AngleVectors( GetLocalAngles() + GetLocalAngularVelocity() * 2, &forward, &right, &up ); + + QAngle angVel = GetLocalAngularVelocity(); + float flSide = DotProduct( m_vecDesiredFaceDir, right ); + if (flSide < 0) + { + if ( angVel.y < 8 ) + { + angVel.y += 2; + } + else if (angVel.y < 60) + { + angVel.y += 8; + } + } + else + { + if ( angVel.y > -8 ) + { + angVel.y -= 2; + } + else if (angVel.y > -60) + { + angVel.y -= 8; + } + } + + angVel.y *= ( 0.98 ); // why?! (sjb) + + // estimate where I'll be in two seconds + AngleVectors( GetLocalAngles() + angVel, NULL, NULL, &up ); + Vector vecEst = GetAbsOrigin() + GetAbsVelocity() * 2.0 + up * m_flForce * 20 - Vector( 0, 0, 384 * 2 ); + + // add immediate force + AngleVectors( GetLocalAngles(), &forward, &right, &up ); + + Vector vecImpulse( 0, 0, 0 ); + vecImpulse.x += up.x * m_flForce; + vecImpulse.y += up.y * m_flForce; + vecImpulse.z += up.z * m_flForce; + + // add gravity + vecImpulse.z -= 38.4; // 32ft/sec + ApplyAbsVelocityImpulse( vecImpulse ); + + float flSpeed = GetAbsVelocity().Length(); + float flDir = DotProduct( Vector( forward.x, forward.y, 0 ), Vector( GetAbsVelocity().x, GetAbsVelocity().y, 0 ) ); + if (flDir < 0) + { + flSpeed = -flSpeed; + } + + float flDist = DotProduct( m_vecDesiredPosition - vecEst, forward ); + +// float flDist = (m_vecDesiredPosition - vecEst).Length(); + + // float flSlip = DotProduct( GetAbsVelocity(), right ); + float flSlip = -DotProduct( m_vecDesiredPosition - vecEst, right ); + + // fly sideways + if (flSlip > 0) + { + if (GetLocalAngles().z > -30 && angVel.z > -15) + angVel.z -= 4; + else + angVel.z += 2; + } + else + { + if (GetLocalAngles().z < 30 && angVel.z < 15) + angVel.z += 4; + else + angVel.z -= 2; + } + + // These functions contain code Ken wrote that used to be right here as part of the flight model, + // but we want different helicopter vehicles to have different drag characteristics, so I made + // them virtual functions (sjb) + ApplySidewaysDrag( right ); + ApplyGeneralDrag(); + + // apply power to stay correct height + // FIXME: these need to be per class variables +#define MAX_FORCE 80 +#define FORCE_POSDELTA 12 +#define FORCE_NEGDELTA 8 + + if (m_flForce < MAX_FORCE && vecEst.z < m_vecDesiredPosition.z) + { + m_flForce += FORCE_POSDELTA; + } + else if (m_flForce > 30) + { + if (vecEst.z > m_vecDesiredPosition.z) + m_flForce -= FORCE_NEGDELTA; + } + + // pitch forward or back to get to target + //----------------------------------------- + // Pitch is reversed since Half-Life! (sjb) + //----------------------------------------- + + + // when we're way out, lean forward up to 40 degrees to accelerate to target + // not exceeding our goal speed. +#if 0 + float nodeSpeed = GetGoalEnt()->m_flSpeed; + + if ( flDist > 300 && flSpeed < m_flGoalSpeed && GetLocalAngles().x + angVel.x < 25 ) + { + angVel.x += 8.0; //lean forward + } + else if ( flDist < 500 && GetLocalAngles().x + angVel.x > -30 && nodeSpeed == 0) + { + angVel.x -= 8.0; // lean backwards as we approach the target + } + else if (GetLocalAngles().x + angVel.x < -20 ) + { + // ALERT( at_console, "f " ); + angVel.x += 2.0; + } + else if (GetLocalAngles().x + angVel.x > 20 ) + { + // ALERT( at_console, "b " ); + angVel.x -= 2.0; + } +#else + m_angleVelocity = GetLocalAngles().x + angVel.x; + float angleX = angVel.x; + +// Msg("AngVel %f, %f\n", m_angleVelocity, flDist); + if (flDist > 128 && flSpeed < m_flGoalSpeed && m_angleVelocity < 30) + { + // ALERT( at_console, "F " ); + // lean forward + angleX += 6; + } + else if (flDist < -128 && flSpeed > -50 && m_angleVelocity > -20) + { + // ALERT( at_console, "B " ); + // lean backward + angleX -= 12.0; + } + else if ( (m_angleVelocity < -20) || (m_angleVelocity < 0 && flDist < 128) ) + { + // ALERT( at_console, "f " ); + if ( abs(m_angleVelocity) < 5 ) + { + angleX += 1.0; + } + else + { + angleX += 4.0; + } + } + else if ( (m_angleVelocity > 20) || (m_angleVelocity > 0 && flDist < 128) ) + { + // ALERT( at_console, "b " ); + if ( abs(m_angleVelocity) < 5 ) + { + angleX -= 1.0; + } + else + { + angleX -= 4.0; + } + } + + angVel.x = angleX; + //Msg("AngVel.x %f %f\n", angVel.x, angleX ); + +#endif + + SetLocalAngularVelocity( angVel ); + // ALERT( at_console, "%.0f %.0f : %.0f %.0f : %.0f %.0f : %.0f\n", GetAbsOrigin().x, GetAbsVelocity().x, flDist, flSpeed, GetLocalAngles().x, m_vecAngVelocity.x, m_flForce ); + // ALERT( at_console, "%.0f %.0f : %.0f %0.f : %.0f\n", GetAbsOrigin().z, GetAbsVelocity().z, vecEst.z, m_vecDesiredPosition.z, m_flForce ); +} + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBaseHelicopter::InitializeRotorSound( void ) +{ + if (m_pRotorSound) + { + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + + // Get the rotor sound started up. + controller.Play( m_pRotorSound, 0.0, 100 ); + controller.SoundChangeVolume(m_pRotorSound, GetRotorVolume(), 2.0); + } + + m_iSoundState = SND_CHANGE_PITCH; // hack for going through level transitions +} + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBaseHelicopter::UpdateRotorSoundPitch( int iPitch ) +{ + if (m_pRotorSound) + { + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + controller.SoundChangePitch( m_pRotorSound, iPitch, 0.1 ); + controller.SoundChangeVolume( m_pRotorSound, GetRotorVolume(), 0.1 ); + } +} + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBaseHelicopter::FlyTouch( CBaseEntity *pOther ) +{ + // bounce if we hit something solid + if ( pOther->GetSolid() == SOLID_BSP) + { + const trace_t &tr = CBaseEntity::GetTouchTrace( ); + + // UNDONE, do a real bounce + ApplyAbsVelocityImpulse( tr.plane.normal * (GetAbsVelocity().Length() + 200) ); + } +} + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBaseHelicopter::CrashTouch( CBaseEntity *pOther ) +{ + // only crash if we hit something solid + if ( pOther->GetSolid() == SOLID_BSP) + { + SetTouch( NULL ); + SetNextThink( gpGlobals->curtime ); + + CPASFilter filter( GetAbsOrigin() ); + for (int i = 0; i < 5; i++) + { + Vector pos = GetAbsOrigin(); + + pos.x += random->RandomFloat( -150, 150 ); + pos.y += random->RandomFloat( -150, 150 ); + pos.z += random->RandomFloat( -150, -50 ); + te->Explosion( filter, MIN( 0.99, i * 0.2 ), &pos, g_sModelIndexFireball, 10, 15, TE_EXPLFLAG_NONE, 100, 0 ); + } + + UTIL_Remove( this ); + } +} + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBaseHelicopter::DyingThink( void ) +{ + StudioFrameAdvance( ); + SetNextThink( gpGlobals->curtime + 0.1f ); + + SetLocalAngularVelocity( GetLocalAngularVelocity() * 1.02 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +int CBaseHelicopter::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ +#if 0 + // This code didn't port easily. WTF does it do? (sjb) + if (pevInflictor->m_owner == pev) + return 0; +#endif + + /* + if ( (bitsDamageType & DMG_BULLET) && flDamage > 50) + { + // clip bullet damage at 50 + flDamage = 50; + } + */ + + return BaseClass::OnTakeDamage_Alive( info ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Override base class to add display of fly direction +// Input : +// Output : +//----------------------------------------------------------------------------- +void CBaseHelicopter::DrawDebugGeometryOverlays(void) +{ + if (m_pfnThink!= NULL) + { + // ------------------------------ + // Draw route if requested + // ------------------------------ + if (m_debugOverlays & OVERLAY_NPC_ROUTE_BIT) + { + NDebugOverlay::Line(GetAbsOrigin(), m_vecDesiredPosition, 0,0,255, true, 0); + } + } + BaseClass::DrawDebugGeometryOverlays(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +void CBaseHelicopter::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + CTakeDamageInfo dmgInfo = info; + + // ALERT( at_console, "%d %.0f\n", ptr->iHitgroup, flDamage ); + + // HITGROUPS don't work currently. + // ignore blades +// if (ptr->hitgroup == 6 && (info.GetDamageType() & (DMG_ENERGYBEAM|DMG_BULLET|DMG_CLUB))) +// return; + + // hit hard, hits cockpit + if (info.GetDamage() > 50 || ptr->hitgroup == 11 ) + { + // ALERT( at_console, "%map .0f\n", flDamage ); + AddMultiDamage( dmgInfo, this ); +// m_iDoSmokePuff = 3 + (info.GetDamage() / 5.0); + } + else + { + // do half damage in the body + dmgInfo.ScaleDamage(0.5); + AddMultiDamage( dmgInfo, this ); + g_pEffects->Ricochet( ptr->endpos, ptr->plane.normal ); + } + +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBaseHelicopter::NullThink( void ) +{ + StudioFrameAdvance( ); + SetNextThink( gpGlobals->curtime + 0.5f ); +} + + +void CBaseHelicopter::Startup( void ) +{ + m_flGoalSpeed = m_flInitialSpeed; + SetThink( &CBaseHelicopter::HelicopterThink ); + SetTouch( &CBaseHelicopter::FlyTouch ); + SetNextThink( gpGlobals->curtime + 0.1f ); +} + + +void CBaseHelicopter::Event_Killed( const CTakeDamageInfo &info ) +{ + m_lifeState = LIFE_DYING; + + SetMoveType( MOVETYPE_FLYGRAVITY ); + SetGravity( UTIL_ScaleForGravity( 240 ) ); // use a lower gravity + + // Kill the rotor sound. + + UTIL_SetSize( this, Vector( -32, -32, -64), Vector( 32, 32, 0) ); + SetThink( &CBaseHelicopter::CallDyingThink ); + SetTouch( &CBaseHelicopter::CrashTouch ); + + m_flNextCrashExplosion = gpGlobals->curtime + 0.0f; + + SetNextThink( gpGlobals->curtime + 0.1f ); + m_iHealth = 0; + m_takedamage = DAMAGE_NO; + +/* + if (m_spawnflags & SF_NOWRECKAGE) + { + m_flNextRocket = gpGlobals->curtime + 4.0; + } + else + { + m_flNextRocket = gpGlobals->curtime + 15.0; + } +*/ + m_OnDeath.FireOutput( info.GetAttacker(), this ); +} + + +void CBaseHelicopter::StopLoopingSounds() +{ + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + controller.SoundDestroy( m_pRotorSound ); + m_pRotorSound = NULL; + + BaseClass::StopLoopingSounds(); +} + +void CBaseHelicopter::GibMonster( void ) +{ +} + + +void CBaseHelicopter::ChangePathCorner( const char *pszName ) +{ + if( m_lifeState != LIFE_ALIVE ) + { + // Disregard this if dead or dying. + return; + } + + if (GetGoalEnt()) + { + SetGoalEnt( gEntList.FindEntityByName( NULL, pszName ) ); + + // I don't think we need to do this. The FLIGHT() code will do it for us (sjb) + if (GetGoalEnt()) + { + m_vecDesiredPosition = GetGoalEnt()->GetLocalOrigin(); + AngleVectors( GetGoalEnt()->GetLocalAngles(), &m_vecGoalOrientation ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Slams the chopper's current path corner and sends it to a new one. +// This code does NOT check that the path from the current position +// to the wished path corner is clear! +//----------------------------------------------------------------------------- +void CBaseHelicopter::InputChangePathCorner( inputdata_t &inputdata ) +{ + ChangePathCorner( inputdata.value.String() ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Call Startup for a helicopter that's been flagged to start disabled +//----------------------------------------------------------------------------- +void CBaseHelicopter::InputActivate( inputdata_t &inputdata ) +{ + if( m_spawnflags & SF_AWAITINPUT ) + { + Startup(); + + // Now clear the spawnflag to protect from + // subsequent calls. + m_spawnflags &= ~SF_AWAITINPUT; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- + +void CBaseHelicopter::ApplySidewaysDrag( const Vector &vecRight ) +{ + Vector vecNewVelocity = GetAbsVelocity(); + vecNewVelocity.x *= 1.0 - fabs( vecRight.x ) * 0.05; + vecNewVelocity.y *= 1.0 - fabs( vecRight.y ) * 0.05; + vecNewVelocity.z *= 1.0 - fabs( vecRight.z ) * 0.05; + SetAbsVelocity( vecNewVelocity ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +void CBaseHelicopter::ApplyGeneralDrag( void ) +{ + Vector vecNewVelocity = GetAbsVelocity(); + vecNewVelocity *= 0.995; + SetAbsVelocity( vecNewVelocity ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +bool CBaseHelicopter::ChooseEnemy( void ) +{ + // See if there's a new enemy. + CBaseEntity *pNewEnemy; + + pNewEnemy = BestEnemy(); + + if( ( pNewEnemy != GetEnemy() ) && pNewEnemy != NULL ) + { + //New enemy! Clear the timers and set conditions. + SetEnemy( pNewEnemy ); + m_flLastSeen = m_flPrevSeen = gpGlobals->curtime; + return true; + } + else + { + ClearCondition( COND_NEW_ENEMY ); + return false; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +void CBaseHelicopter::CheckEnemy( CBaseEntity *pEnemy ) +{ + // ------------------- + // If enemy is dead + // ------------------- + if ( !pEnemy->IsAlive() ) + { + SetCondition( COND_ENEMY_DEAD ); + ClearCondition( COND_SEE_ENEMY ); + ClearCondition( COND_ENEMY_OCCLUDED ); + return; + } +} + +bool CBaseHelicopter::HasReachedTarget( void ) +{ + float flDist = (WorldSpaceCenter() - m_vecDesiredPosition).Length(); + + if( GetGoalEnt()->m_flSpeed <= 0 ) + return ( flDist < 145 ); + else + return( flDist < 512 ); +} + diff --git a/game/server/hl1/hl1_npc_roach.cpp b/game/server/hl1/hl1_npc_roach.cpp new file mode 100644 index 0000000..d5a4be7 --- /dev/null +++ b/game/server/hl1/hl1_npc_roach.cpp @@ -0,0 +1,471 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "hl1_ai_basenpc.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "ai_senses.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "basecombatweapon.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "ai_behavior_follow.h" +#include "ai_navigator.h" +#include "decals.h" + + +#define ROACH_IDLE 0 +#define ROACH_BORED 1 +#define ROACH_SCARED_BY_ENT 2 +#define ROACH_SCARED_BY_LIGHT 3 +#define ROACH_SMELL_FOOD 4 +#define ROACH_EAT 5 + +//========================================================= +// Monster's Anim Events Go Here +//========================================================= +class CNPC_Roach : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_Roach, CHL1BaseNPC ); + +public: + + void Spawn( void ); + void Precache( void ); + float MaxYawSpeed( void ); + +// DECLARE_DATADESC(); + + void NPCThink ( void ); + void PickNewDest ( int iCondition ); + void Look ( int iDistance ); + void Move ( float flInterval ); + + Class_T Classify( void ) { return CLASS_INSECT; } + + void Touch ( CBaseEntity *pOther ); + + void Event_Killed( const CTakeDamageInfo &info ); + int GetSoundInterests ( void ); + + void Eat( float flFullDuration ); + bool ShouldEat( void ); + + bool ShouldGib( const CTakeDamageInfo &info ) { return false; } + + float m_flLastLightLevel; + float m_flNextSmellTime; + + // UNDONE: These don't necessarily need to be save/restored, but if we add more data, it may + bool m_fLightHacked; + int m_iMode; + + float m_flHungryTime; + // ----------------------------- +}; + +LINK_ENTITY_TO_CLASS( monster_cockroach, CNPC_Roach ); + +//BEGIN_DATADESC( CNPC_Roach ) + +// DEFINE_FUNCTION( RoachTouch ), + +//END_DATADESC() + + +//========================================================= +// Spawn +//========================================================= +void CNPC_Roach::Spawn() +{ + Precache( ); + + SetModel( "models/roach.mdl" ); + UTIL_SetSize( this, Vector( -1, -1, 0 ), Vector( 1, 1, 2 ) ); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + m_bloodColor = BLOOD_COLOR_YELLOW; + ClearEffects(); + m_iHealth = 1; + m_flFieldOfView = 0.5;// indicates the width of this monster's forward view cone ( as a dotproduct result ) + m_NPCState = NPC_STATE_NONE; + + SetRenderColor( 255, 255, 255, 255 ); + + NPCInit(); + SetActivity ( ACT_IDLE ); + + SetViewOffset ( Vector ( 0, 0, 1 ) );// position of the eyes relative to monster's origin. + m_takedamage = DAMAGE_YES; + m_fLightHacked = FALSE; + m_flLastLightLevel = -1; + m_iMode = ROACH_IDLE; + m_flNextSmellTime = gpGlobals->curtime; + + AddEffects( EF_NOSHADOW ); +} + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CNPC_Roach::Precache() +{ + PrecacheModel("models/roach.mdl"); + + PrecacheScriptSound( "Roach.Walk" ); + PrecacheScriptSound( "Roach.Die" ); + PrecacheScriptSound( "Roach.Smash" ); +} + +float CNPC_Roach::MaxYawSpeed( void ) +{ + return 120.0f; +} + +void CNPC_Roach::Eat( float flFullDuration ) +{ + m_flHungryTime = gpGlobals->curtime + flFullDuration; +} + +bool CNPC_Roach::ShouldEat( void ) +{ + if ( m_flHungryTime > gpGlobals->curtime ) + { + return false; + } + + return true; +} + +//========================================================= +// MonsterThink, overridden for roaches. +//========================================================= +void CNPC_Roach::NPCThink( void ) +{ + if ( FNullEnt( UTIL_FindClientInPVS( edict() ) ) ) + SetNextThink( gpGlobals->curtime + random->RandomFloat( 1.0f , 1.5f ) ); + else + SetNextThink( gpGlobals->curtime + 0.1f );// keep monster thinking + + float flInterval = gpGlobals->curtime - GetLastThink(); + + StudioFrameAdvance( ); // animate + + if ( !m_fLightHacked ) + { + // if light value hasn't been collection for the first time yet, + // suspend the creature for a second so the world finishes spawning, then we'll collect the light level. + SetNextThink( gpGlobals->curtime + 1 ); + m_fLightHacked = TRUE; + return; + } + else if ( m_flLastLightLevel < 0 ) + { + // collect light level for the first time, now that all of the lightmaps in the roach's area have been calculated. + m_flLastLightLevel = 0; + } + + switch ( m_iMode ) + { + case ROACH_IDLE: + case ROACH_EAT: + { + // if not moving, sample environment to see if anything scary is around. Do a radius search 'look' at random. + if ( random->RandomInt( 0, 3 ) == 1 ) + { + Look( 150 ); + + if ( HasCondition( COND_SEE_FEAR ) ) + { + // if see something scary + //ALERT ( at_aiconsole, "Scared\n" ); + Eat( 30 + ( random->RandomInt( 0, 14 ) ) );// roach will ignore food for 30 to 45 seconds + PickNewDest( ROACH_SCARED_BY_ENT ); + SetActivity ( ACT_WALK ); + } + else if ( random->RandomInt( 0,149 ) == 1 ) + { + // if roach doesn't see anything, there's still a chance that it will move. (boredom) + //ALERT ( at_aiconsole, "Bored\n" ); + PickNewDest( ROACH_BORED ); + SetActivity ( ACT_WALK ); + + if ( m_iMode == ROACH_EAT ) + { + // roach will ignore food for 30 to 45 seconds if it got bored while eating. + Eat( 30 + ( random->RandomInt(0,14) ) ); + } + } + } + + // don't do this stuff if eating! + if ( m_iMode == ROACH_IDLE ) + { + if ( ShouldEat() ) + { + GetSenses()->Listen(); + } + + if ( 0 > m_flLastLightLevel ) + { + // someone turned on lights! + //ALERT ( at_console, "Lights!\n" ); + PickNewDest( ROACH_SCARED_BY_LIGHT ); + SetActivity ( ACT_WALK ); + } + else if ( HasCondition( COND_SMELL ) ) + { + CSound *pSound = GetLoudestSoundOfType( ALL_SOUNDS ); + + // roach smells food and is just standing around. Go to food unless food isn't on same z-plane. + if ( pSound && abs( pSound->GetSoundOrigin().z - GetAbsOrigin().z ) <= 3 ) + { + PickNewDest( ROACH_SMELL_FOOD ); + SetActivity ( ACT_WALK ); + } + } + } + + break; + } + case ROACH_SCARED_BY_LIGHT: + { + // if roach was scared by light, then stop if we're over a spot at least as dark as where we started! + if ( 0 <= m_flLastLightLevel ) + { + SetActivity ( ACT_IDLE ); + m_flLastLightLevel = 0;// make this our new light level. + } + break; + } + } + + if ( GetActivity() != ACT_IDLE ) + { + Move( flInterval ); + } +} + +void CNPC_Roach::PickNewDest ( int iCondition ) +{ + Vector vecNewDir; + Vector vecDest; + float flDist; + + m_iMode = iCondition; + + GetNavigator()->ClearGoal(); + + if ( m_iMode == ROACH_SMELL_FOOD ) + { + // find the food and go there. + CSound *pSound = GetLoudestSoundOfType( ALL_SOUNDS ); + + if ( pSound ) + { + GetNavigator()->SetRandomGoal( 3 - random->RandomInt( 0,5 ) ); + return; + } + } + + do + { + // picks a random spot, requiring that it be at least 128 units away + // else, the roach will pick a spot too close to itself and run in + // circles. this is a hack but buys me time to work on the real monsters. + vecNewDir.x = random->RandomInt( -1, 1 ); + vecNewDir.y = random->RandomInt( -1, 1 ); + flDist = 256 + ( random->RandomInt(0,255) ); + vecDest = GetAbsOrigin() + vecNewDir * flDist; + + } while ( ( vecDest - GetAbsOrigin() ).Length2D() < 128 ); + + Vector vecLocation; + + vecLocation.x = vecDest.x; + vecLocation.y = vecDest.y; + vecLocation.z = GetAbsOrigin().z; + + AI_NavGoal_t goal( GOALTYPE_LOCATION, vecLocation, ACT_WALK ); + + GetNavigator()->SetGoal( goal ); + + if ( random->RandomInt( 0, 9 ) == 1 ) + { + // every once in a while, a roach will play a skitter sound when they decide to run + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Roach.Walk" ); + } +} + +//========================================================= +// Look - overriden for the roach, which can virtually see +// 360 degrees. +//========================================================= +void CNPC_Roach::Look ( int iDistance ) +{ + CBaseEntity *pSightEnt = NULL;// the current visible entity that we're dealing with + + // DON'T let visibility information from last frame sit around! + ClearCondition( COND_SEE_HATE | COND_SEE_DISLIKE | COND_SEE_ENEMY | COND_SEE_FEAR ); + + // don't let monsters outside of the player's PVS act up, or most of the interesting + // things will happen before the player gets there! + if ( FNullEnt( UTIL_FindClientInPVS( edict() ) ) ) + { + return; + } + + // Does sphere also limit itself to PVS? + // Examine all entities within a reasonable radius + // !!!PERFORMANCE - let's trivially reject the ent list before radius searching! + + for ( CEntitySphereQuery sphere( GetAbsOrigin(), iDistance ); ( pSightEnt = sphere.GetCurrentEntity() ) != NULL; sphere.NextEntity() ) + { + // only consider ents that can be damaged. !!!temporarily only considering other monsters and clients + if ( pSightEnt->IsPlayer() || FBitSet ( pSightEnt->GetFlags(), FL_NPC ) ) + { + if ( /*FVisible( pSightEnt ) &&*/ !FBitSet( pSightEnt->GetFlags(), FL_NOTARGET ) && pSightEnt->m_iHealth > 0 ) + { + // don't add the Enemy's relationship to the conditions. We only want to worry about conditions when + // we see monsters other than the Enemy. + switch ( IRelationType ( pSightEnt ) ) + { + case D_FR: + SetCondition( COND_SEE_FEAR ); + break; + case D_NU: + break; + default: + Msg ( "%s can't asses %s\n", GetClassname(), pSightEnt->GetClassname() ); + break; + } + } + } + } +} + +//========================================================= +// roach's move function +//========================================================= +void CNPC_Roach::Move ( float flInterval ) +{ + float flWaypointDist; + Vector vecApex; + + // local move to waypoint. + flWaypointDist = ( GetNavigator()->GetGoalPos() - GetAbsOrigin() ).Length2D(); + + GetMotor()->SetIdealYawToTargetAndUpdate( GetNavigator()->GetGoalPos() ); + + float speed = 150 * flInterval; + + Vector vToTarget = GetNavigator()->GetGoalPos() - GetAbsOrigin(); + vToTarget.NormalizeInPlace(); + Vector vMovePos = vToTarget * speed; + + if ( random->RandomInt( 0,7 ) == 1 ) + { + // randomly change direction + PickNewDest( m_iMode ); + } + + if( !WalkMove( vMovePos, MASK_NPCSOLID ) ) + { + PickNewDest( m_iMode ); + } + + // if the waypoint is closer than step size, then stop after next step (ok for roach to overshoot) + if ( flWaypointDist <= m_flGroundSpeed * flInterval ) + { + // take truncated step and stop + + SetActivity ( ACT_IDLE ); + m_flLastLightLevel = 0;// this is roach's new comfortable light level + + if ( m_iMode == ROACH_SMELL_FOOD ) + { + m_iMode = ROACH_EAT; + } + else + { + m_iMode = ROACH_IDLE; + } + } + + if ( random->RandomInt( 0,149 ) == 1 && m_iMode != ROACH_SCARED_BY_LIGHT && m_iMode != ROACH_SMELL_FOOD ) + { + // random skitter while moving as long as not on a b-line to get out of light or going to food + PickNewDest( FALSE ); + } +} + +void CNPC_Roach::Touch ( CBaseEntity *pOther ) +{ + Vector vecSpot; + trace_t tr; + + if ( pOther->GetAbsVelocity() == vec3_origin || !pOther->IsPlayer() ) + { + return; + } + + vecSpot = GetAbsOrigin() + Vector ( 0 , 0 , 8 );//move up a bit, and trace down. + //UTIL_TraceLine ( vecSpot, vecSpot + Vector ( 0, 0, -24 ), ignore_monsters, ENT(pev), & tr); + + UTIL_TraceLine ( vecSpot, vecSpot + Vector ( 0, 0, -24 ), MASK_ALL, this, COLLISION_GROUP_NONE, &tr); + + // This isn't really blood. So you don't have to screen it out based on violence levels (UTIL_ShouldShowBlood()) + UTIL_DecalTrace( &tr, "YellowBlood" ); + + // DMG_GENERIC because we don't want any physics force generated + TakeDamage( CTakeDamageInfo( pOther, pOther, m_iHealth, DMG_GENERIC ) ); +} + +void CNPC_Roach::Event_Killed( const CTakeDamageInfo &info ) +{ + RemoveSolidFlags( FSOLID_NOT_SOLID ); + + CPASAttenuationFilter filter( this ); + + //random sound + if ( random->RandomInt( 0,4 ) == 1 ) + { + EmitSound( filter, entindex(), "Roach.Die" ); + } + else + { + EmitSound( filter, entindex(), "Roach.Smash" ); + } + + CSoundEnt::InsertSound ( SOUND_WORLD, GetAbsOrigin(), 128, 1 ); + + UTIL_Remove( this ); +} + +int CNPC_Roach::GetSoundInterests ( void) +{ + return SOUND_CARCASS | + SOUND_MEAT; +} diff --git a/game/server/hl1/hl1_npc_scientist.cpp b/game/server/hl1/hl1_npc_scientist.cpp new file mode 100644 index 0000000..d4f0bd9 --- /dev/null +++ b/game/server/hl1/hl1_npc_scientist.cpp @@ -0,0 +1,1410 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Bullseyes act as targets for other NPC's to attack and to trigger +// events +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "hl1_npc_scientist.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "engine/IEngineSound.h" +#include "ai_navigator.h" +#include "ai_behavior_follow.h" +#include "AI_Criteria.h" +#include "SoundEmitterSystem/isoundemittersystembase.h" + +#define SC_PLFEAR "SC_PLFEAR" +#define SC_FEAR "SC_FEAR" +#define SC_HEAL "SC_HEAL" +#define SC_SCREAM "SC_SCREAM" +#define SC_POK "SC_POK" + +ConVar sk_scientist_health( "sk_scientist_health","20"); +ConVar sk_scientist_heal( "sk_scientist_heal","25"); + +#define NUM_SCIENTIST_HEADS 4 // four heads available for scientist model +enum { HEAD_GLASSES = 0, HEAD_EINSTEIN = 1, HEAD_LUTHER = 2, HEAD_SLICK = 3 }; + + +int ACT_EXCITED; + +//========================================================= +// Makes it fast to check barnacle classnames in +// IsValidEnemy() +//========================================================= +string_t s_iszBarnacleClassname; + +//========================================================= +// Monster's Anim Events Go Here +//========================================================= +#define SCIENTIST_AE_HEAL ( 1 ) +#define SCIENTIST_AE_NEEDLEON ( 2 ) +#define SCIENTIST_AE_NEEDLEOFF ( 3 ) + +//======================================================= +// Scientist +//======================================================= + +LINK_ENTITY_TO_CLASS( monster_scientist, CNPC_Scientist ); + +//IMPLEMENT_SERVERCLASS_ST( CNPC_Scientist, DT_NPC_Scientist ) +//END_SEND_TABLE() + + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CNPC_Scientist ) + DEFINE_FIELD( m_flFearTime, FIELD_TIME ), + DEFINE_FIELD( m_flHealTime, FIELD_TIME ), + DEFINE_FIELD( m_flPainTime, FIELD_TIME ), + + DEFINE_THINKFUNC( SUB_LVFadeOut ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: +// +// +//----------------------------------------------------------------------------- +void CNPC_Scientist::Precache( void ) +{ + PrecacheModel( "models/scientist.mdl" ); + + PrecacheScriptSound( "Scientist.Pain" ); + + TalkInit(); + + BaseClass::Precache(); +} + +void CNPC_Scientist::ModifyOrAppendCriteria( AI_CriteriaSet& criteriaSet ) +{ + BaseClass::ModifyOrAppendCriteria( criteriaSet ); + + bool predisaster = FBitSet( m_spawnflags, SF_NPC_PREDISASTER ) ? true : false; + + criteriaSet.AppendCriteria( "disaster", predisaster ? "[disaster::pre]" : "[disaster::post]" ); +} + +// Init talk data +void CNPC_Scientist::TalkInit() +{ + + BaseClass::TalkInit(); + + // scientist will try to talk to friends in this order: + + m_szFriends[0] = "monster_scientist"; + m_szFriends[1] = "monster_sitting_scientist"; + m_szFriends[2] = "monster_barney"; + + // get voice for head + switch (m_nBody % 3) + { + default: + case HEAD_GLASSES: GetExpresser()->SetVoicePitch( 105 ); break; //glasses + case HEAD_EINSTEIN: GetExpresser()->SetVoicePitch( 100 ); break; //einstein + case HEAD_LUTHER: GetExpresser()->SetVoicePitch( 95 ); break; //luther + case HEAD_SLICK: GetExpresser()->SetVoicePitch( 100 ); break;//slick + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// +// +//----------------------------------------------------------------------------- +void CNPC_Scientist::Spawn( void ) +{ + + //Select the body first if it's going to be random cause we set his voice pitch in Precache. + if ( m_nBody == -1 ) + m_nBody = random->RandomInt( 0, NUM_SCIENTIST_HEADS-1 );// pick a head, any head + + + SetRenderColor( 255, 255, 255, 255 ); + + Precache(); + + SetModel( "models/scientist.mdl" ); + + SetHullType(HULL_HUMAN); + SetHullSizeNormal(); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + m_bloodColor = BLOOD_COLOR_RED; + ClearEffects(); + m_iHealth = sk_scientist_health.GetFloat(); + m_flFieldOfView = VIEW_FIELD_WIDE; + m_NPCState = NPC_STATE_NONE; + + CapabilitiesClear(); + CapabilitiesAdd( bits_CAP_MOVE_GROUND | bits_CAP_OPEN_DOORS | bits_CAP_AUTO_DOORS | bits_CAP_USE ); + CapabilitiesAdd( bits_CAP_TURN_HEAD | bits_CAP_ANIMATEDFACE ); + + // White hands + m_nSkin = 0; + + + // Luther is black, make his hands black + if ( m_nBody == HEAD_LUTHER ) + m_nSkin = 1; + + NPCInit(); + + SetUse( &CNPC_Scientist::FollowerUse ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Scientist::Activate() +{ + s_iszBarnacleClassname = FindPooledString( "monster_barnacle" ); + BaseClass::Activate(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// +// +// Output : +//----------------------------------------------------------------------------- +Class_T CNPC_Scientist::Classify( void ) +{ + return CLASS_HUMAN_PASSIVE; +} + +int CNPC_Scientist::GetSoundInterests ( void ) +{ + return SOUND_WORLD | + SOUND_COMBAT | + SOUND_DANGER | + SOUND_PLAYER; +} + +//========================================================= +// HandleAnimEvent - catches the monster-specific messages +// that occur when tagged animation frames are played. +//========================================================= +void CNPC_Scientist::HandleAnimEvent( animevent_t *pEvent ) +{ + switch( pEvent->event ) + { + case SCIENTIST_AE_HEAL: // Heal my target (if within range) + Heal(); + break; + case SCIENTIST_AE_NEEDLEON: + { + int oldBody = m_nBody; + m_nBody = (oldBody % NUM_SCIENTIST_HEADS) + NUM_SCIENTIST_HEADS * 1; + } + break; + case SCIENTIST_AE_NEEDLEOFF: + { + int oldBody = m_nBody; + m_nBody = (oldBody % NUM_SCIENTIST_HEADS) + NUM_SCIENTIST_HEADS * 0; + } + break; + + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } +} + +void CNPC_Scientist::DeclineFollowing( void ) +{ + if ( CanSpeakAfterMyself() ) + { + Speak( SC_POK ); + } +} + +bool CNPC_Scientist::CanBecomeRagdoll( void ) +{ + if ( UTIL_IsLowViolence() ) + { + return false; + } + + return BaseClass::CanBecomeRagdoll(); +} + +bool CNPC_Scientist::ShouldGib( const CTakeDamageInfo &info ) +{ + if ( UTIL_IsLowViolence() ) + { + return false; + } + + return BaseClass::ShouldGib( info ); +} + +void CNPC_Scientist::SUB_StartLVFadeOut( float delay, bool notSolid ) +{ + SetThink( &CNPC_Scientist::SUB_LVFadeOut ); + SetNextThink( gpGlobals->curtime + delay ); + SetRenderColorA( 255 ); + m_nRenderMode = kRenderNormal; + + if ( notSolid ) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + SetLocalAngularVelocity( vec3_angle ); + } +} + +void CNPC_Scientist::SUB_LVFadeOut( void ) +{ + if( VPhysicsGetObject() ) + { + if( VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_PLAYER_HELD || GetEFlags() & EFL_IS_BEING_LIFTED_BY_BARNACLE ) + { + // Try again in a few seconds. + SetNextThink( gpGlobals->curtime + 5 ); + SetRenderColorA( 255 ); + return; + } + } + + float dt = gpGlobals->frametime; + if ( dt > 0.1f ) + { + dt = 0.1f; + } + m_nRenderMode = kRenderTransTexture; + int speed = MAX(3,256*dt); // fade out over 3 seconds + SetRenderColorA( UTIL_Approach( 0, m_clrRender->a, speed ) ); + NetworkStateChanged(); + + if ( m_clrRender->a == 0 ) + { + UTIL_Remove(this); + } + else + { + SetNextThink( gpGlobals->curtime ); + } +} + +void CNPC_Scientist::Scream( void ) +{ + if ( IsOkToSpeak() ) + { + GetExpresser()->BlockSpeechUntil( gpGlobals->curtime + 10 ); + SetSpeechTarget( GetEnemy() ); + Speak( SC_SCREAM ); + } +} + +Activity CNPC_Scientist::GetStoppedActivity( void ) +{ + if ( GetEnemy() != NULL ) + return (Activity)ACT_EXCITED; + + return BaseClass::GetStoppedActivity(); +} + +float CNPC_Scientist::MaxYawSpeed( void ) +{ + switch( GetActivity() ) + { + case ACT_TURN_LEFT: + case ACT_TURN_RIGHT: + return 160; + break; + case ACT_RUN: + return 160; + break; + default: + return 60; + break; + } +} + +void CNPC_Scientist::StartTask( const Task_t *pTask ) +{ + switch( pTask->iTask ) + { + case TASK_SAY_HEAL: + + GetExpresser()->BlockSpeechUntil( gpGlobals->curtime + 2 ); + SetSpeechTarget( GetTarget() ); + Speak( SC_HEAL ); + + TaskComplete(); + break; + + case TASK_SCREAM: + Scream(); + TaskComplete(); + break; + + case TASK_RANDOM_SCREAM: + if ( random->RandomFloat( 0, 1 ) < pTask->flTaskData ) + Scream(); + TaskComplete(); + break; + + case TASK_SAY_FEAR: + if ( IsOkToSpeak() ) + { + GetExpresser()->BlockSpeechUntil( gpGlobals->curtime + 2 ); + SetSpeechTarget( GetEnemy() ); + if ( GetEnemy() && GetEnemy()->IsPlayer() ) + Speak( SC_PLFEAR ); + else + Speak( SC_FEAR ); + } + TaskComplete(); + break; + + case TASK_HEAL: + SetIdealActivity( ACT_MELEE_ATTACK1 ); + break; + + case TASK_RUN_PATH_SCARED: + GetNavigator()->SetMovementActivity( ACT_RUN_SCARED ); + break; + + case TASK_MOVE_TO_TARGET_RANGE_SCARED: + { + if ( GetTarget() == NULL) + { + TaskFail(FAIL_NO_TARGET); + } + else if ( (GetTarget()->GetAbsOrigin() - GetAbsOrigin()).Length() < 1 ) + { + TaskComplete(); + } + } + break; + + default: + BaseClass::StartTask( pTask ); + break; + } +} + +void CNPC_Scientist::RunTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_RUN_PATH_SCARED: + if ( !IsMoving() ) + TaskComplete(); + if ( random->RandomInt(0,31) < 8 ) + Scream(); + break; + + case TASK_MOVE_TO_TARGET_RANGE_SCARED: + { + float distance; + + if ( GetTarget() == NULL ) + { + TaskFail(FAIL_NO_TARGET); + } + else + { + distance = ( GetNavigator()->GetPath()->ActualGoalPosition() - GetAbsOrigin() ).Length2D(); + // Re-evaluate when you think your finished, or the target has moved too far + if ( (distance < pTask->flTaskData) || (GetNavigator()->GetPath()->ActualGoalPosition() - GetTarget()->GetAbsOrigin()).Length() > pTask->flTaskData * 0.5 ) + { + GetNavigator()->GetPath()->ResetGoalPosition(GetTarget()->GetAbsOrigin()); + distance = ( GetNavigator()->GetPath()->ActualGoalPosition() - GetAbsOrigin() ).Length2D(); +// GetNavigator()->GetPath()->Find(); + GetNavigator()->SetGoal( GOALTYPE_TARGETENT ); + } + + // Set the appropriate activity based on an overlapping range + // overlap the range to prevent oscillation + // BUGBUG: this is checking linear distance (ie. through walls) and not path distance or even visibility + if ( distance < pTask->flTaskData ) + { + TaskComplete(); + GetNavigator()->GetPath()->Clear(); // Stop moving + } + else + { + if ( distance < 190 && GetNavigator()->GetMovementActivity() != ACT_WALK_SCARED ) + GetNavigator()->SetMovementActivity( ACT_WALK_SCARED ); + else if ( distance >= 270 && GetNavigator()->GetMovementActivity() != ACT_RUN_SCARED ) + GetNavigator()->SetMovementActivity( ACT_RUN_SCARED ); + } + } + } + break; + + case TASK_HEAL: + if ( IsSequenceFinished() ) + { + TaskComplete(); + } + else + { + if ( TargetDistance() > 90 ) + TaskComplete(); + + if ( GetTarget() ) + GetMotor()->SetIdealYaw( UTIL_VecToYaw( GetTarget()->GetAbsOrigin() - GetAbsOrigin() ) ); + + //GetMotor()->SetYawSpeed( m_YawSpeed ); + } + break; + default: + BaseClass::RunTask( pTask ); + break; + } +} + +int CNPC_Scientist::OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ) +{ + + if ( inputInfo.GetInflictor() && inputInfo.GetInflictor()->GetFlags() & FL_CLIENT ) + { + Remember( bits_MEMORY_PROVOKED ); + StopFollowing(); + } + + // make sure friends talk about it if player hurts scientist... + return BaseClass::OnTakeDamage_Alive( inputInfo ); +} + +void CNPC_Scientist::Event_Killed( const CTakeDamageInfo &info ) +{ + SetUse( NULL ); + BaseClass::Event_Killed( info ); + + if ( UTIL_IsLowViolence() ) + { + SUB_StartLVFadeOut( 0.0f ); + } +} + +bool CNPC_Scientist::CanHeal( void ) +{ + CBaseEntity *pTarget = GetFollowTarget(); + + if ( pTarget == NULL ) + return false; + + if ( pTarget->IsPlayer() == false ) + return false; + + if ( (m_flHealTime > gpGlobals->curtime) || (pTarget->m_iHealth > (pTarget->m_iMaxHealth * 0.5)) ) + return false; + + return true; +} + +//========================================================= +// PainSound +//========================================================= +void CNPC_Scientist::PainSound ( const CTakeDamageInfo &info ) +{ + if (gpGlobals->curtime < m_flPainTime ) + return; + + m_flPainTime = gpGlobals->curtime + random->RandomFloat( 0.5, 0.75 ); + + CPASAttenuationFilter filter( this ); + + CSoundParameters params; + if ( GetParametersForSound( "Scientist.Pain", params, NULL ) ) + { + EmitSound_t ep( params ); + params.pitch = GetExpresser()->GetVoicePitch(); + + EmitSound( filter, entindex(), ep ); + } +} + +//========================================================= +// DeathSound +//========================================================= +void CNPC_Scientist::DeathSound( const CTakeDamageInfo &info ) +{ + PainSound( info ); +} + + +void CNPC_Scientist::Heal( void ) +{ + if ( !CanHeal() ) + return; + + Vector target = GetFollowTarget()->GetAbsOrigin() - GetAbsOrigin(); + if ( target.Length() > 100 ) + return; + + GetTarget()->TakeHealth( sk_scientist_heal.GetFloat(), DMG_GENERIC ); + // Don't heal again for 1 minute + m_flHealTime = gpGlobals->curtime + 60; +} + +int CNPC_Scientist::TranslateSchedule( int scheduleType ) +{ + switch( scheduleType ) + { + // Hook these to make a looping schedule + case SCHED_TARGET_FACE: + { + int baseType; + + // call base class default so that scientist will talk + // when 'used' + baseType = BaseClass::TranslateSchedule( scheduleType ); + + if (baseType == SCHED_IDLE_STAND) + return SCHED_TARGET_FACE; // override this for different target face behavior + else + return baseType; + } + break; + + case SCHED_TARGET_CHASE: + return SCHED_SCI_FOLLOWTARGET; + break; + + case SCHED_IDLE_STAND: + { + int baseType; + + baseType = BaseClass::TranslateSchedule( scheduleType ); + + if (baseType == SCHED_IDLE_STAND) + return SCHED_SCI_IDLESTAND; // override this for different target face behavior + else + return baseType; + } + break; + } + + return BaseClass::TranslateSchedule( scheduleType ); +} + +Activity CNPC_Scientist::NPC_TranslateActivity( Activity newActivity ) +{ + if ( GetFollowTarget() && GetEnemy() ) + { + CBaseEntity *pEnemy = GetEnemy(); + + int relationship = D_NU; + + // Nothing scary, just me and the player + if ( pEnemy != NULL ) + relationship = IRelationType( pEnemy ); + + if ( relationship == D_HT || relationship == D_FR ) + { + if ( newActivity == ACT_WALK ) + return ACT_WALK_SCARED; + else if ( newActivity == ACT_RUN ) + return ACT_RUN_SCARED; + } + } + + return BaseClass::NPC_TranslateActivity( newActivity ); +} + +int CNPC_Scientist::SelectSchedule( void ) +{ + if( m_NPCState == NPC_STATE_PRONE ) + { + // Immediately call up to the talker code. Barnacle death is priority schedule. + return BaseClass::SelectSchedule(); + } + + // so we don't keep calling through the EHANDLE stuff + CBaseEntity *pEnemy = GetEnemy(); + + if ( GetFollowTarget() ) + { + // so we don't keep calling through the EHANDLE stuff + CBaseEntity *pEnemy = GetEnemy(); + + int relationship = D_NU; + + // Nothing scary, just me and the player + if ( pEnemy != NULL ) + relationship = IRelationType( pEnemy ); + + if ( relationship != D_HT && relationship != D_FR ) + { + // If I'm already close enough to my target + if ( TargetDistance() <= 128 ) + { + if ( CanHeal() ) // Heal opportunistically + { + SetTarget( GetFollowTarget() ); + return SCHED_SCI_HEAL; + } + } + } + } + else if ( HasCondition( COND_PLAYER_PUSHING ) && !(GetSpawnFlags() & SF_NPC_PREDISASTER ) ) + { // Player wants me to move + return SCHED_HL1TALKER_FOLLOW_MOVE_AWAY; + } + + if ( BehaviorSelectSchedule() ) + { + return BaseClass::SelectSchedule(); + } + + + + if ( HasCondition( COND_HEAR_DANGER ) && m_NPCState != NPC_STATE_PRONE ) + { + CSound *pSound; + pSound = GetBestSound(); + + if ( pSound && pSound->IsSoundType(SOUND_DANGER) ) + return SCHED_TAKE_COVER_FROM_BEST_SOUND; + } + + switch( m_NPCState ) + { + + case NPC_STATE_ALERT: + case NPC_STATE_IDLE: + + if ( pEnemy ) + { + if ( HasCondition( COND_SEE_ENEMY ) ) + m_flFearTime = gpGlobals->curtime; + else if ( DisregardEnemy( pEnemy ) ) // After 15 seconds of being hidden, return to alert + { + SetEnemy( NULL ); + pEnemy = NULL; + } + } + + if ( HasCondition( COND_LIGHT_DAMAGE ) || HasCondition( COND_HEAVY_DAMAGE ) ) + { + // flinch if hurt + return SCHED_SMALL_FLINCH; + } + + // Cower when you hear something scary + if ( HasCondition( COND_HEAR_DANGER ) || HasCondition( COND_HEAR_COMBAT ) ) + { + CSound *pSound; + pSound = GetBestSound(); + + if ( pSound ) + { + if ( pSound->IsSoundType(SOUND_DANGER | SOUND_COMBAT) ) + { + if ( gpGlobals->curtime - m_flFearTime > 3 ) // Only cower every 3 seconds or so + { + m_flFearTime = gpGlobals->curtime; // Update last fear + return SCHED_SCI_STARTLE; // This will just duck for a second + } + } + } + } + + if ( GetFollowTarget() ) + { + if ( !GetFollowTarget()->IsAlive() ) + { + // UNDONE: Comment about the recently dead player here? + StopFollowing(); + break; + } + + int relationship = D_NU; + + // Nothing scary, just me and the player + if ( pEnemy != NULL ) + relationship = IRelationType( pEnemy ); + + if ( relationship != D_HT ) + { + return SCHED_TARGET_FACE; // Just face and follow. + } + else // UNDONE: When afraid, scientist won't move out of your way. Keep This? If not, write move away scared + { + if ( HasCondition( COND_NEW_ENEMY ) ) // I just saw something new and scary, react + return SCHED_SCI_FEAR; // React to something scary + return SCHED_SCI_FACETARGETSCARED; // face and follow, but I'm scared! + } + } + + // try to say something about smells + TrySmellTalk(); + break; + + + case NPC_STATE_COMBAT: + + if ( HasCondition( COND_NEW_ENEMY ) ) + return SCHED_SCI_FEAR; // Point and scream! + if ( HasCondition( COND_SEE_ENEMY ) ) + return SCHED_SCI_COVER; // Take Cover + + if ( HasCondition( COND_HEAR_COMBAT ) || HasCondition( COND_HEAR_DANGER ) ) + return SCHED_TAKE_COVER_FROM_BEST_SOUND; // Cower and panic from the scary sound! + + return SCHED_SCI_COVER; // Run & Cower + break; + } + + return BaseClass::SelectSchedule(); +} + +NPC_STATE CNPC_Scientist::SelectIdealState ( void ) +{ + switch ( m_NPCState ) + { + case NPC_STATE_ALERT: + case NPC_STATE_IDLE: + if ( HasCondition( COND_NEW_ENEMY ) ) + { + if ( GetFollowTarget() && GetEnemy() ) + { + int relationship = IRelationType( GetEnemy() ); + if ( relationship != D_FR || relationship != D_HT && ( !HasCondition( COND_LIGHT_DAMAGE ) || !HasCondition( COND_HEAVY_DAMAGE ) ) ) + { + // Don't go to combat if you're following the player + return NPC_STATE_ALERT; + } + StopFollowing(); + } + } + else if ( HasCondition( COND_LIGHT_DAMAGE ) || HasCondition( COND_HEAVY_DAMAGE ) ) + { + // Stop following if you take damage + if ( GetFollowTarget() ) + StopFollowing(); + } + break; + + case NPC_STATE_COMBAT: + { + CBaseEntity *pEnemy = GetEnemy(); + if ( pEnemy != NULL ) + { + if ( DisregardEnemy( pEnemy ) ) // After 15 seconds of being hidden, return to alert + { + // Strip enemy when going to alert + SetEnemy( NULL ); + return NPC_STATE_ALERT; + } + // Follow if only scared a little + if ( GetFollowTarget() ) + { + return NPC_STATE_ALERT; + } + + if ( HasCondition( COND_SEE_ENEMY ) ) + { + m_flFearTime = gpGlobals->curtime; + return NPC_STATE_COMBAT; + } + + } + } + break; + } + + return BaseClass::SelectIdealState(); +} + +int CNPC_Scientist::FriendNumber( int arrayNumber ) +{ + static int array[3] = { 1, 2, 0 }; + if ( arrayNumber < 3 ) + return array[ arrayNumber ]; + return arrayNumber; +} + +float CNPC_Scientist::TargetDistance( void ) +{ + CBaseEntity *pFollowTarget = GetFollowTarget(); + + // If we lose the player, or he dies, return a really large distance + if ( pFollowTarget == NULL || !pFollowTarget->IsAlive() ) + return 1e6; + + return (pFollowTarget->WorldSpaceCenter() - WorldSpaceCenter()).Length(); +} + +bool CNPC_Scientist::IsValidEnemy( CBaseEntity *pEnemy ) +{ + if( pEnemy->m_iClassname == s_iszBarnacleClassname ) + { + // Scientists ignore barnacles rather than freak out.(sjb) + return false; + } + + return BaseClass::IsValidEnemy(pEnemy); +} + + +//========================================================= +// Dead Scientist PROP +//========================================================= +class CNPC_DeadScientist : public CAI_BaseNPC +{ + DECLARE_CLASS( CNPC_DeadScientist, CAI_BaseNPC ); +public: + + void Spawn( void ); + Class_T Classify ( void ) { return CLASS_NONE; } + + bool KeyValue( const char *szKeyName, const char *szValue ); + float MaxYawSpeed ( void ) { return 8.0f; } + + int m_iPose;// which sequence to display -- temporary, don't need to save + int m_iDesiredSequence; + static char *m_szPoses[7]; +}; + + +char *CNPC_DeadScientist::m_szPoses[] = { "lying_on_back", "lying_on_stomach", "dead_sitting", "dead_hang", "dead_table1", "dead_table2", "dead_table3" }; + +bool CNPC_DeadScientist::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "pose" ) ) + m_iPose = atoi( szValue ); + else + BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + +LINK_ENTITY_TO_CLASS( monster_scientist_dead, CNPC_DeadScientist ); + +// +// ********** DeadScientist SPAWN ********** +// +void CNPC_DeadScientist::Spawn( void ) +{ + PrecacheModel("models/scientist.mdl"); + SetModel( "models/scientist.mdl" ); + + ClearEffects(); + SetSequence( 0 ); + m_bloodColor = BLOOD_COLOR_RED; + + SetRenderColor( 255, 255, 255, 255 ); + + if ( m_nBody == -1 ) + {// -1 chooses a random head + m_nBody = random->RandomInt( 0, NUM_SCIENTIST_HEADS-1);// pick a head, any head + } + // Luther is black, make his hands black + if ( m_nBody == HEAD_LUTHER ) + m_nSkin = 1; + else + m_nSkin = 0; + + SetSequence( LookupSequence( m_szPoses[m_iPose] ) ); + + if ( GetSequence() == -1) + { + Msg ( "Dead scientist with bad pose\n" ); + } + + m_iHealth = 0.0;//gSkillData.barneyHealth; + + NPCInitDead(); + +} + + +//========================================================= +// Sitting Scientist PROP +//========================================================= + + +LINK_ENTITY_TO_CLASS( monster_sitting_scientist, CNPC_SittingScientist ); + +//IMPLEMENT_CUSTOM_AI( monster_sitting_scientist, CNPC_SittingScientist ); + +//IMPLEMENT_SERVERCLASS_ST( CNPC_SittingScientist, DT_NPC_SittingScientist ) +//END_SEND_TABLE() + + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CNPC_SittingScientist ) + DEFINE_FIELD( m_iHeadTurn, FIELD_INTEGER ), + DEFINE_FIELD( m_flResponseDelay, FIELD_FLOAT ), + //DEFINE_FIELD( m_baseSequence, FIELD_INTEGER ), + + DEFINE_THINKFUNC( SittingThink ), +END_DATADESC() + + +// animation sequence aliases +typedef enum +{ +SITTING_ANIM_sitlookleft, +SITTING_ANIM_sitlookright, +SITTING_ANIM_sitscared, +SITTING_ANIM_sitting2, +SITTING_ANIM_sitting3 +} SITTING_ANIM; + + +// +// ********** Scientist SPAWN ********** +// +void CNPC_SittingScientist::Spawn( ) +{ + PrecacheModel("models/scientist.mdl"); + SetModel("models/scientist.mdl"); + Precache(); + + InitBoneControllers(); + + SetHullType(HULL_HUMAN); + SetHullSizeNormal(); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + m_iHealth = 50; + + m_bloodColor = BLOOD_COLOR_RED; + m_flFieldOfView = VIEW_FIELD_WIDE; // indicates the width of this monster's forward view cone ( as a dotproduct result ) + + m_NPCState = NPC_STATE_NONE; + + CapabilitiesClear(); + CapabilitiesAdd( bits_CAP_TURN_HEAD ); + + m_spawnflags |= SF_NPC_PREDISASTER; // predisaster only! + + if ( m_nBody == -1 ) + {// -1 chooses a random head + m_nBody = random->RandomInt( 0, NUM_SCIENTIST_HEADS-1 );// pick a head, any head + } + // Luther is black, make his hands black + if ( m_nBody == HEAD_LUTHER ) + m_nBody = 1; + + UTIL_DropToFloor( this,MASK_SOLID ); + + NPCInit(); + + SetThink (&CNPC_SittingScientist::SittingThink); + SetNextThink( gpGlobals->curtime + 0.1f ); + + m_baseSequence = LookupSequence( "sitlookleft" ); + SetSequence( m_baseSequence + random->RandomInt(0,4) ); + ResetSequenceInfo( ); +} + +void CNPC_SittingScientist::Precache( void ) +{ + m_baseSequence = LookupSequence( "sitlookleft" ); + TalkInit(); +} + +int CNPC_SittingScientist::FriendNumber( int arrayNumber ) +{ + static int array[3] = { 2, 1, 0 }; + if ( arrayNumber < 3 ) + return array[ arrayNumber ]; + return arrayNumber; +} + +//========================================================= +// sit, do stuff +//========================================================= +void CNPC_SittingScientist::SittingThink( void ) +{ + CBaseEntity *pent; + + StudioFrameAdvance( ); + + // try to greet player + //FIXMEFIXME + + //MB - don't greet, done by base talker + if ( 0 && GetExpresser()->CanSpeakConcept( TLK_HELLO ) ) + { + pent = FindNearestFriend(true); + if (pent) + { + float yaw = VecToYaw(pent->GetAbsOrigin() - GetAbsOrigin()) - GetAbsAngles().y; + + if (yaw > 180) yaw -= 360; + if (yaw < -180) yaw += 360; + + if (yaw > 0) + SetSequence( m_baseSequence + SITTING_ANIM_sitlookleft ); + else + SetSequence ( m_baseSequence + SITTING_ANIM_sitlookright ); + + ResetSequenceInfo( ); + SetCycle( 0 ); + SetBoneController( 0, 0 ); + + GetExpresser()->Speak( TLK_HELLO ); + } + } + else if ( IsSequenceFinished() ) + { + int i = random->RandomInt(0,99); + m_iHeadTurn = 0; + + if (m_flResponseDelay && gpGlobals->curtime > m_flResponseDelay) + { + // respond to question + GetExpresser()->Speak( TLK_QUESTION ); + SetSequence( m_baseSequence + SITTING_ANIM_sitscared ); + m_flResponseDelay = 0; + } + else if (i < 30) + { + SetSequence( m_baseSequence + SITTING_ANIM_sitting3 ); + + // turn towards player or nearest friend and speak + + //FIXME + /*/ if (!FBitSet(m_nSpeak, bit_saidHelloPlayer)) + pent = FindNearestFriend(TRUE); + else*/ + pent = FindNamedEntity( "!nearestfriend" ); + + if (!FIdleSpeak() || !pent) + { + m_iHeadTurn = random->RandomInt(0,8) * 10 - 40; + SetSequence( m_baseSequence + SITTING_ANIM_sitting3 ); + } + else + { + // only turn head if we spoke + float yaw = VecToYaw(pent->GetAbsOrigin() - GetAbsOrigin()) - GetAbsAngles().y; + + if (yaw > 180) yaw -= 360; + if (yaw < -180) yaw += 360; + + if (yaw > 0) + SetSequence( m_baseSequence + SITTING_ANIM_sitlookleft ); + else + SetSequence( m_baseSequence + SITTING_ANIM_sitlookright ); + + //ALERT(at_console, "sitting speak\n"); + } + } + else if (i < 60) + { + SetSequence( m_baseSequence + SITTING_ANIM_sitting3 ); + m_iHeadTurn = random->RandomInt(0,8) * 10 - 40; + if ( random->RandomInt(0,99) < 5) + { + //ALERT(at_console, "sitting speak2\n"); + FIdleSpeak(); + } + } + else if (i < 80) + { + SetSequence( m_baseSequence + SITTING_ANIM_sitting2 ); + } + else if (i < 100) + { + SetSequence( m_baseSequence + SITTING_ANIM_sitscared ); + } + + ResetSequenceInfo( ); + SetCycle( 0 ); + SetBoneController( 0, m_iHeadTurn ); + } + + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +// prepare sitting scientist to answer a question +void CNPC_SittingScientist::SetAnswerQuestion( CNPCSimpleTalker *pSpeaker ) +{ + m_flResponseDelay = gpGlobals->curtime + random->RandomFloat(3, 4); + SetSpeechTarget( (CNPCSimpleTalker *)pSpeaker ); +} + +//------------------------------------------------------------------------------ +// +// Schedules +// +//------------------------------------------------------------------------------ + +AI_BEGIN_CUSTOM_NPC( monster_scientist, CNPC_Scientist ) + + DECLARE_TASK( TASK_SAY_HEAL ) + DECLARE_TASK( TASK_HEAL ) + DECLARE_TASK( TASK_SAY_FEAR ) + DECLARE_TASK( TASK_RUN_PATH_SCARED ) + DECLARE_TASK( TASK_SCREAM ) + DECLARE_TASK( TASK_RANDOM_SCREAM ) + DECLARE_TASK( TASK_MOVE_TO_TARGET_RANGE_SCARED ) + + DECLARE_ACTIVITY( ACT_EXCITED ) + + //========================================================= + // > SCHED_SCI_HEAL + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SCI_HEAL, + + " Tasks" + " TASK_GET_PATH_TO_TARGET 0" + " TASK_MOVE_TO_TARGET_RANGE 50" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_SCI_FOLLOWTARGET" + " TASK_FACE_IDEAL 0" + " TASK_SAY_HEAL 0" + " TASK_PLAY_SEQUENCE_FACE_TARGET ACTIVITY:ACT_ARM" + " TASK_HEAL 0" + " TASK_PLAY_SEQUENCE_FACE_TARGET ACTIVITY:ACT_DISARM" + " " + " Interrupts" + ) + + //========================================================= + // > SCHED_SCI_FOLLOWTARGET + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SCI_FOLLOWTARGET, + + " Tasks" +// " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_SCI_STOPFOLLOWING" + " TASK_GET_PATH_TO_TARGET 0" + " TASK_MOVE_TO_TARGET_RANGE 128" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_TARGET_FACE" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_HEAR_DANGER" + " COND_HEAR_COMBAT" + ) + + //========================================================= + // > SCHED_SCI_STOPFOLLOWING + //========================================================= +// DEFINE_SCHEDULE +// ( +// SCHED_SCI_STOPFOLLOWING, +// +// " Tasks" +// " TASK_TALKER_CANT_FOLLOW 0" +// " " +// " Interrupts" +// ) + + //========================================================= + // > SCHED_SCI_FACETARGET + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SCI_FACETARGET, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_TARGET 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_SCI_FOLLOWTARGET" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_HEAR_DANGER" + " COND_HEAR_COMBAT" + " COND_GIVE_WAY" + ) + + //========================================================= + // > SCHED_SCI_COVER + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SCI_COVER, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_SCI_PANIC" + " TASK_STOP_MOVING 0" + " TASK_FIND_COVER_FROM_ENEMY 0" + " TASK_RUN_PATH_SCARED 0" + " TASK_TURN_LEFT 179" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_SCI_HIDE" + " " + " Interrupts" + " COND_NEW_ENEMY" + ) + + //========================================================= + // > SCHED_SCI_HIDE + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SCI_HIDE, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_SCI_PANIC" + " TASK_STOP_MOVING 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_CROUCHIDLE" + " TASK_SET_ACTIVITY ACTIVITY:ACT_CROUCHIDLE" + " TASK_WAIT_RANDOM 10" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_SEE_ENEMY" + " COND_SEE_HATE" + " COND_SEE_FEAR" + " COND_SEE_DISLIKE" + " COND_HEAR_DANGER" + ) + + //========================================================= + // > SCHED_SCI_IDLESTAND + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SCI_IDLESTAND, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT 2" + " TASK_TALKER_HEADRESET 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_SMELL" + " COND_PROVOKED" + " COND_HEAR_COMBAT" + " COND_GIVE_WAY" + ) + + //========================================================= + // > SCHED_SCI_PANIC + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SCI_PANIC, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_SCREAM 0" + " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_EXCITED" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " " + " Interrupts" + ) + + //========================================================= + // > SCHED_SCI_FOLLOWSCARED + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SCI_FOLLOWSCARED, + + " Tasks" + " TASK_GET_PATH_TO_TARGET 0" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_SCI_FOLLOWTARGET" + " TASK_MOVE_TO_TARGET_RANGE_SCARED 128" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_HEAR_DANGER" + ) + + //========================================================= + // > SCHED_SCI_FACETARGETSCARED + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SCI_FACETARGETSCARED, + + " Tasks" + " TASK_FACE_TARGET 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_CROUCHIDLE" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_SCI_FOLLOWSCARED" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_HEAR_DANGER" + ) + + //========================================================= + // > SCHED_FEAR + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SCI_FEAR, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_SAY_FEAR 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + ) + + //========================================================= + // > SCHED_SCI_STARTLE + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_SCI_STARTLE, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_SCI_PANIC" + " TASK_RANDOM_SCREAM 0.3" + " TASK_STOP_MOVING 0" + " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_CROUCH" + " TASK_RANDOM_SCREAM 0.1" + " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_CROUCHIDLE" + " TASK_WAIT_RANDOM 1" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_SEE_ENEMY" + " COND_SEE_HATE" + " COND_SEE_FEAR" + " COND_SEE_DISLIKE" + ) + +AI_END_CUSTOM_NPC() diff --git a/game/server/hl1/hl1_npc_scientist.h b/game/server/hl1/hl1_npc_scientist.h new file mode 100644 index 0000000..858883c --- /dev/null +++ b/game/server/hl1/hl1_npc_scientist.h @@ -0,0 +1,139 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef NPC_SCIENTIST_H +#define NPC_SCIENTIST_H + +#include "hl1_npc_talker.h" + +//========================================================= +//========================================================= +class CNPC_Scientist : public CHL1NPCTalker +{ + DECLARE_CLASS( CNPC_Scientist, CHL1NPCTalker ); +public: + +// DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + + void Precache( void ); + void Spawn( void ); + void Activate(); + Class_T Classify( void ); + int GetSoundInterests ( void ); + + virtual void ModifyOrAppendCriteria( AI_CriteriaSet& set ); + + virtual int ObjectCaps( void ) { return UsableNPCObjectCaps(BaseClass::ObjectCaps()); } + float MaxYawSpeed( void ); + + float TargetDistance( void ); + bool IsValidEnemy( CBaseEntity *pEnemy ); + + + int OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ); + void Event_Killed( const CTakeDamageInfo &info ); + + void Heal( void ); + bool CanHeal( void ); + + int TranslateSchedule( int scheduleType ); + void HandleAnimEvent( animevent_t *pEvent ); + int SelectSchedule( void ); + void StartTask( const Task_t *pTask ); + void RunTask( const Task_t *pTask ); + + NPC_STATE SelectIdealState ( void ); + + int FriendNumber( int arrayNumber ); + + bool DisregardEnemy( CBaseEntity *pEnemy ) { return !pEnemy->IsAlive() || (gpGlobals->curtime - m_flFearTime) > 15; } + + void TalkInit( void ); + + void DeclineFollowing( void ); + + bool CanBecomeRagdoll( void ); + bool ShouldGib( const CTakeDamageInfo &info ); + + void SUB_StartLVFadeOut( float delay = 10.0f, bool bNotSolid = true ); + void SUB_LVFadeOut( void ); + + void Scream( void ); + + Activity GetStoppedActivity( void ); + Activity NPC_TranslateActivity( Activity newActivity ); + + void PainSound( const CTakeDamageInfo &info ); + void DeathSound( const CTakeDamageInfo &info ); + + enum + { + SCHED_SCI_HEAL = BaseClass::NEXT_SCHEDULE, + SCHED_SCI_FOLLOWTARGET, + SCHED_SCI_STOPFOLLOWING, + SCHED_SCI_FACETARGET, + SCHED_SCI_COVER, + SCHED_SCI_HIDE, + SCHED_SCI_IDLESTAND, + SCHED_SCI_PANIC, + SCHED_SCI_FOLLOWSCARED, + SCHED_SCI_FACETARGETSCARED, + SCHED_SCI_FEAR, + SCHED_SCI_STARTLE, + }; + + enum + { + TASK_SAY_HEAL = BaseClass::NEXT_TASK, + TASK_HEAL, + TASK_SAY_FEAR, + TASK_RUN_PATH_SCARED, + TASK_SCREAM, + TASK_RANDOM_SCREAM, + TASK_MOVE_TO_TARGET_RANGE_SCARED, + }; + + DEFINE_CUSTOM_AI; + +private: + + float m_flFearTime; + float m_flHealTime; + float m_flPainTime; + //float m_flResponseDelay; +}; + +//========================================================= +// Sitting Scientist PROP +//========================================================= + +class CNPC_SittingScientist : public CNPC_Scientist // kdb: changed from public CBaseMonster so he can speak +{ + DECLARE_CLASS( CNPC_SittingScientist, CNPC_Scientist ); +public: + +// DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + void Spawn( void ); + void Precache( void ); + + int FriendNumber( int arrayNumber ); + + void SittingThink( void ); + + virtual void SetAnswerQuestion( CNPCSimpleTalker *pSpeaker ); + int m_baseSequence; + int m_iHeadTurn; + float m_flResponseDelay; + + //DEFINE_CUSTOM_AI; +}; + +#endif // NPC_SCIENTIST_H
\ No newline at end of file diff --git a/game/server/hl1/hl1_npc_snark.cpp b/game/server/hl1/hl1_npc_snark.cpp new file mode 100644 index 0000000..cb33749 --- /dev/null +++ b/game/server/hl1/hl1_npc_snark.cpp @@ -0,0 +1,527 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Projectile shot from the MP5 +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + + +#include "cbase.h" +#include "soundent.h" +#include "engine/IEngineSound.h" +#include "ai_senses.h" +#include "hl1_npc_snark.h" +#include "SoundEmitterSystem/isoundemittersystembase.h" + + +ConVar sk_snark_health ( "sk_snark_health", "0" ); +ConVar sk_snark_dmg_bite ( "sk_snark_dmg_bite", "0" ); +ConVar sk_snark_dmg_pop ( "sk_snark_dmg_pop", "0" ); + + +LINK_ENTITY_TO_CLASS( monster_snark, CSnark); + + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CSnark ) + DEFINE_FIELD( m_flDie, FIELD_TIME ), + DEFINE_FIELD( m_vecTarget, FIELD_VECTOR ), + DEFINE_FIELD( m_flNextHunt, FIELD_TIME ), + DEFINE_FIELD( m_flNextHit, FIELD_TIME ), + DEFINE_FIELD( m_posPrev, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_hOwner, FIELD_EHANDLE ), + DEFINE_FIELD( m_iMyClass, FIELD_INTEGER ), + + DEFINE_ENTITYFUNC( SuperBounceTouch ), + DEFINE_THINKFUNC( HuntThink ), +END_DATADESC() + + +#define SQUEEK_DETONATE_DELAY 15.0 +#define SNARK_EXPLOSION_VOLUME 512 + + +enum w_squeak_e { + WSQUEAK_IDLE1 = 0, + WSQUEAK_FIDGET, + WSQUEAK_JUMP, + WSQUEAK_RUN, +}; + +float CSnark::m_flNextBounceSoundTime = 0; + +void CSnark::Precache( void ) +{ + BaseClass::Precache(); + + PrecacheModel( "models/w_squeak2.mdl" ); + + PrecacheScriptSound( "Snark.Die" ); + PrecacheScriptSound( "Snark.Gibbed" ); + PrecacheScriptSound( "Snark.Squeak" ); + PrecacheScriptSound( "Snark.Deploy" ); + PrecacheScriptSound( "Snark.Bounce" ); + +} + + +void CSnark::Spawn( void ) +{ + Precache(); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_FLYGRAVITY, MOVECOLLIDE_FLY_CUSTOM ); + SetFriction(1.0); + + SetModel( "models/w_squeak2.mdl" ); + UTIL_SetSize( this, Vector( -4, -4, 0 ), Vector( 4, 4, 8 ) ); + + SetBloodColor( BLOOD_COLOR_YELLOW ); + + SetTouch( &CSnark::SuperBounceTouch ); + SetThink( &CSnark::HuntThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); + + m_flNextHit = gpGlobals->curtime; + m_flNextHunt = gpGlobals->curtime + 1E6; + m_flNextBounceSoundTime = gpGlobals->curtime; + + AddFlag( FL_AIMTARGET | FL_NPC ); + m_takedamage = DAMAGE_YES; + + m_iHealth = sk_snark_health.GetFloat(); + m_iMaxHealth = m_iHealth; + + SetGravity( UTIL_ScaleForGravity( 400 ) ); // use a lower gravity for snarks + SetFriction( 0.5 ); + + SetDamage( sk_snark_dmg_pop.GetFloat() ); + + m_flDie = gpGlobals->curtime + SQUEEK_DETONATE_DELAY; + + m_flFieldOfView = 0; // 180 degrees + + if ( GetOwnerEntity() ) + m_hOwner = GetOwnerEntity(); + + m_flNextBounceSoundTime = gpGlobals->curtime;// reset each time a snark is spawned. + + SetSequence( WSQUEAK_RUN ); + ResetSequenceInfo( ); + + m_iMyClass = CLASS_NONE; + + m_posPrev = Vector( 0, 0, 0 ); +} + + +Class_T CSnark::Classify( void ) +{ + if ( m_iMyClass != CLASS_NONE ) + return m_iMyClass; // protect against recursion + + if ( GetEnemy() != NULL ) + { + m_iMyClass = CLASS_INSECT; // no one cares about it + switch( GetEnemy()->Classify( ) ) + { + case CLASS_PLAYER: + case CLASS_HUMAN_PASSIVE: + case CLASS_HUMAN_MILITARY: + m_iMyClass = CLASS_NONE; + return CLASS_ALIEN_MILITARY; // barney's get mad, grunts get mad at it + } + m_iMyClass = CLASS_NONE; + } + + return CLASS_ALIEN_BIOWEAPON; +} + + +void CSnark::Event_Killed( const CTakeDamageInfo &inputInfo ) +{ +// pev->model = iStringNull;// make invisible + SetThink( &CSnark::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.1f ); + SetTouch( NULL ); + + // since squeak grenades never leave a body behind, clear out their takedamage now. + // Squeaks do a bit of radius damage when they pop, and that radius damage will + // continue to call this function unless we acknowledge the Squeak's death now. (sjb) + m_takedamage = DAMAGE_NO; + + // play squeek blast + CPASAttenuationFilter filter( this, 0.5 ); + EmitSound( filter, entindex(), "Snark.Die" ); + + CSoundEnt::InsertSound( SOUND_COMBAT, GetAbsOrigin(), SNARK_EXPLOSION_VOLUME, 3.0 ); + + UTIL_BloodDrips( WorldSpaceCenter(), Vector( 0, 0, 0 ), BLOOD_COLOR_YELLOW, 80 ); + + if ( m_hOwner != NULL ) + { + RadiusDamage( CTakeDamageInfo( this, m_hOwner, GetDamage(), DMG_BLAST ), GetAbsOrigin(), GetDamage() * 2.5, CLASS_NONE, NULL ); + } + else + { + RadiusDamage( CTakeDamageInfo( this, this, GetDamage(), DMG_BLAST ), GetAbsOrigin(), GetDamage() * 2.5, CLASS_NONE, NULL ); + } + + // reset owner so death message happens + if ( m_hOwner != NULL ) + SetOwnerEntity( m_hOwner ); + + CTakeDamageInfo info = inputInfo; + int iGibDamage = g_pGameRules->Damage_GetShouldGibCorpse(); + info.SetDamageType( iGibDamage ); + + BaseClass::Event_Killed( info ); +} + + +bool CSnark::Event_Gibbed( const CTakeDamageInfo &info ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Snark.Gibbed" ); + + return BaseClass::Event_Gibbed( info ); +} + + +void CSnark::HuntThink( void ) +{ + if (!IsInWorld()) + { + SetTouch( NULL ); + UTIL_Remove( this ); + return; + } + + StudioFrameAdvance( ); + SetNextThink( gpGlobals->curtime + 0.1f ); + + //FIXME: There's a problem in this movetype that causes it to set a ground entity but never recheck to clear it + // For now, we stomp it clear and force it to revalidate -- jdw + + SetGroundEntity( NULL ); + PhysicsStepRecheckGround(); + + // explode when ready + if ( gpGlobals->curtime >= m_flDie ) + { + g_vecAttackDir = GetAbsVelocity(); + VectorNormalize( g_vecAttackDir ); + m_iHealth = -1; + CTakeDamageInfo info( this, this, 1, DMG_GENERIC ); + Event_Killed( info ); + return; + } + + // float + if ( GetWaterLevel() != 0) + { + if ( GetMoveType() == MOVETYPE_FLYGRAVITY ) + { + SetMoveType( MOVETYPE_FLY, MOVECOLLIDE_FLY_CUSTOM ); + } + + Vector vecVel = GetAbsVelocity(); + vecVel *= 0.9; + vecVel.z += 8.0; + SetAbsVelocity( vecVel ); + } + else if ( GetMoveType() == MOVETYPE_FLY ) + { + SetMoveType( MOVETYPE_FLYGRAVITY, MOVECOLLIDE_FLY_CUSTOM ); + } + + // return if not time to hunt + if ( m_flNextHunt > gpGlobals->curtime ) + return; + + m_flNextHunt = gpGlobals->curtime + 2.0; + + Vector vecFlat = GetAbsVelocity(); + vecFlat.z = 0; + VectorNormalize( vecFlat ); + + if ( GetEnemy() == NULL || !GetEnemy()->IsAlive() ) + { + // find target, bounce a bit towards it. + GetSenses()->Look( 1024 ); + SetEnemy( BestEnemy() ); + } + + // squeek if it's about time blow up + if ( (m_flDie - gpGlobals->curtime <= 0.5) && (m_flDie - gpGlobals->curtime >= 0.3) ) + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Snark.Squeak" ); + CSoundEnt::InsertSound( SOUND_COMBAT, GetAbsOrigin(), 256, 0.25 ); + } + + // higher pitch as squeeker gets closer to detonation time + float flpitch = 155.0 - 60.0 * ( (m_flDie - gpGlobals->curtime) / SQUEEK_DETONATE_DELAY ); + if ( flpitch < 80 ) + flpitch = 80; + + if ( GetEnemy() != NULL ) + { + if ( FVisible( GetEnemy() ) ) + { + m_vecTarget = GetEnemy()->EyePosition() - GetAbsOrigin(); + VectorNormalize( m_vecTarget ); + } + + float flVel = GetAbsVelocity().Length(); + float flAdj = 50.0 / ( flVel + 10.0 ); + + if ( flAdj > 1.2 ) + flAdj = 1.2; + + // ALERT( at_console, "think : enemy\n"); + + // ALERT( at_console, "%.0f %.2f %.2f %.2f\n", flVel, m_vecTarget.x, m_vecTarget.y, m_vecTarget.z ); + + SetAbsVelocity( GetAbsVelocity() * flAdj + (m_vecTarget * 300) ); + } + + if ( GetFlags() & FL_ONGROUND ) + { + SetLocalAngularVelocity( QAngle( 0, 0, 0 ) ); + } + else + { + QAngle angVel = GetLocalAngularVelocity(); + if ( angVel == QAngle( 0, 0, 0 ) ) + { + angVel.x = random->RandomFloat( -100, 100 ); + angVel.z = random->RandomFloat( -100, 100 ); + SetLocalAngularVelocity( angVel ); + } + } + + if ( ( GetAbsOrigin() - m_posPrev ).Length() < 1.0 ) + { + Vector vecVel = GetAbsVelocity(); + vecVel.x = random->RandomFloat( -100, 100 ); + vecVel.y = random->RandomFloat( -100, 100 ); + SetAbsVelocity( vecVel ); + } + + m_posPrev = GetAbsOrigin(); + + QAngle angles; + VectorAngles( GetAbsVelocity(), angles ); + angles.z = 0; + angles.x = 0; + SetAbsAngles( angles ); +} + +unsigned int CSnark::PhysicsSolidMaskForEntity( void ) const +{ + unsigned int iMask = BaseClass::PhysicsSolidMaskForEntity(); + + iMask &= ~CONTENTS_MONSTERCLIP; + + return iMask; +} + + +// Custom collision that provides a good bounce when we hit walls +// and also gives gravity velocity so the snarks fall off of edges. +void CSnark::ResolveFlyCollisionCustom( trace_t &trace, Vector &vecVelocity ) +{ + // Get the impact surface's friction. + float flSurfaceFriction; + physprops->GetPhysicsProperties( trace.surface.surfaceProps, NULL, NULL, &flSurfaceFriction, NULL ); + + Vector vecAbsVelocity = GetAbsVelocity(); + + // If we hit a wall + if ( trace.plane.normal.z <= 0.7 ) // Floor + { + Vector vecDir = vecAbsVelocity; + + float speed = vecDir.Length(); + + VectorNormalize( vecDir ); + + float hitDot = DotProduct( trace.plane.normal, -vecDir ); + + Vector vReflection = 2.0f * trace.plane.normal * hitDot + vecDir; + + SetAbsVelocity( vReflection * speed * 0.6f ); + + return; + } + + // Stop if on ground. + // Get the total velocity (player + conveyors, etc.) + VectorAdd( vecAbsVelocity, GetBaseVelocity(), vecVelocity ); + float flSpeedSqr = DotProduct( vecVelocity, vecVelocity ); + + // Verify that we have an entity. + CBaseEntity *pEntity = trace.m_pEnt; + Assert( pEntity ); + + if ( vecVelocity.z < ( 800 * gpGlobals->frametime ) ) + { + vecAbsVelocity.z = 0.0f; + + // Recompute speedsqr based on the new absvel + VectorAdd( vecAbsVelocity, GetBaseVelocity(), vecVelocity ); + flSpeedSqr = DotProduct( vecVelocity, vecVelocity ); + } + SetAbsVelocity( vecAbsVelocity ); + + if ( flSpeedSqr < ( 30 * 30 ) ) + { + if ( pEntity->IsStandable() ) + { + SetGroundEntity( pEntity ); + } + + // Reset velocities. + SetAbsVelocity( vec3_origin ); + SetLocalAngularVelocity( vec3_angle ); + } + else + { + vecAbsVelocity += GetBaseVelocity(); + vecAbsVelocity *= ( 1.0f - trace.fraction ) * gpGlobals->frametime * flSurfaceFriction; + PhysicsPushEntity( vecAbsVelocity, &trace ); + } +} + +void CSnark::SuperBounceTouch( CBaseEntity *pOther ) +{ + float flpitch; + trace_t tr; + tr = CBaseEntity::GetTouchTrace( ); + + // don't hit the guy that launched this grenade + if ( GetOwnerEntity() && ( pOther == GetOwnerEntity() ) ) + return; + + // at least until we've bounced once + SetOwnerEntity( NULL ); + + QAngle angles = GetAbsAngles(); + angles.x = 0; + angles.z = 0; + SetAbsAngles( angles ); + + // avoid bouncing too much + if ( m_flNextHit > gpGlobals->curtime) + return; + + // higher pitch as squeeker gets closer to detonation time + flpitch = 155.0 - 60.0 * ( ( m_flDie - gpGlobals->curtime ) / SQUEEK_DETONATE_DELAY ); + + if ( pOther->m_takedamage && m_flNextAttack < gpGlobals->curtime ) + { + // attack! + + // make sure it's me who has touched them + if ( tr.m_pEnt == pOther ) + { + // and it's not another squeakgrenade + if ( tr.m_pEnt->GetModelIndex() != GetModelIndex() ) + { + // ALERT( at_console, "hit enemy\n"); + ClearMultiDamage(); + + Vector vecForward; + AngleVectors( GetAbsAngles(), &vecForward ); + + if ( m_hOwner != NULL ) + { + CTakeDamageInfo info( this, m_hOwner, sk_snark_dmg_bite.GetFloat(), DMG_SLASH ); + CalculateMeleeDamageForce( &info, vecForward, tr.endpos ); + pOther->DispatchTraceAttack( info, vecForward, &tr ); + } + else + { + CTakeDamageInfo info( this, this, sk_snark_dmg_bite.GetFloat(), DMG_SLASH ); + CalculateMeleeDamageForce( &info, vecForward, tr.endpos ); + pOther->DispatchTraceAttack( info, vecForward, &tr ); + } + + ApplyMultiDamage(); + + SetDamage( GetDamage() + sk_snark_dmg_pop.GetFloat() ); // add more explosion damage + // m_flDie += 2.0; // add more life + + // make bite sound + CPASAttenuationFilter filter( this ); + CSoundParameters params; + if ( GetParametersForSound( "Snark.Deploy", params, NULL ) ) + { + EmitSound_t ep( params ); + ep.m_nPitch = (int)flpitch; + + EmitSound( filter, entindex(), ep ); + } + m_flNextAttack = gpGlobals->curtime + 0.5; + } + } + else + { + // ALERT( at_console, "been hit\n"); + } + } + + m_flNextHit = gpGlobals->curtime + 0.1; + m_flNextHunt = gpGlobals->curtime; + + if ( g_pGameRules->IsMultiplayer() ) + { + // in multiplayer, we limit how often snarks can make their bounce sounds to prevent overflows. + if ( gpGlobals->curtime < m_flNextBounceSoundTime ) + { + // too soon! + return; + } + } + + if ( !( GetFlags() & FL_ONGROUND ) ) + { + // play bounce sound + CPASAttenuationFilter filter2( this ); + + CSoundParameters params; + if ( GetParametersForSound( "Snark.Bounce", params, NULL ) ) + { + EmitSound_t ep( params ); + ep.m_nPitch = (int)flpitch; + + EmitSound( filter2, entindex(), ep ); + } + + CSoundEnt::InsertSound( SOUND_COMBAT, GetAbsOrigin(), 256, 0.25 ); + } + else + { + // skittering sound + CSoundEnt::InsertSound( SOUND_COMBAT, GetAbsOrigin(), 100, 0.1 ); + } + + m_flNextBounceSoundTime = gpGlobals->curtime + 0.5;// half second. +} + + +bool CSnark::IsValidEnemy( CBaseEntity *pEnemy ) +{ + return CHL1BaseNPC::IsValidEnemy( pEnemy ); +} + diff --git a/game/server/hl1/hl1_npc_snark.h b/game/server/hl1/hl1_npc_snark.h new file mode 100644 index 0000000..d28b489 --- /dev/null +++ b/game/server/hl1/hl1_npc_snark.h @@ -0,0 +1,57 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Projectile shot from the MP5 +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + + +#ifndef NPC_SNARK_H +#define NPC_SNARK_H + + +#include "hl1_ai_basenpc.h" + + +class CSnark : public CHL1BaseNPC +{ + DECLARE_CLASS( CSnark, CHL1BaseNPC ); +public: + + DECLARE_DATADESC(); + + void Precache( void ); + void Spawn( void ); + Class_T Classify( void ); + void Event_Killed( const CTakeDamageInfo &info ); + bool Event_Gibbed( const CTakeDamageInfo &info ); + void HuntThink( void ); + void SuperBounceTouch( CBaseEntity *pOther ); + + virtual void ResolveFlyCollisionCustom( trace_t &trace, Vector &vecVelocity ); + + virtual unsigned int PhysicsSolidMaskForEntity( void ) const; + + virtual bool ShouldGib( const CTakeDamageInfo &info ) { return false; } + static float m_flNextBounceSoundTime; + + virtual bool IsValidEnemy( CBaseEntity *pEnemy ); + +private: + Class_T m_iMyClass; + float m_flDie; + Vector m_vecTarget; + float m_flNextHunt; + float m_flNextHit; + Vector m_posPrev; + EHANDLE m_hOwner; +}; + + +#endif // NPC_SNARK_H diff --git a/game/server/hl1/hl1_npc_talker.cpp b/game/server/hl1/hl1_npc_talker.cpp new file mode 100644 index 0000000..2797cec --- /dev/null +++ b/game/server/hl1/hl1_npc_talker.cpp @@ -0,0 +1,728 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "hl1_npc_talker.h" +#include "scripted.h" +#include "soundent.h" +#include "animation.h" +#include "entitylist.h" +#include "ai_navigator.h" +#include "ai_motor.h" +#include "player.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "npcevent.h" +#include "ai_interactions.h" +#include "doors.h" + +#include "effect_dispatch_data.h" +#include "te_effect_dispatch.h" +#include "hl1_ai_basenpc.h" +#include "SoundEmitterSystem/isoundemittersystembase.h" + +ConVar hl1_debug_sentence_volume( "hl1_debug_sentence_volume", "0" ); +ConVar hl1_fixup_sentence_sndlevel( "hl1_fixup_sentence_sndlevel", "1" ); + +//#define TALKER_LOOK 0 + +BEGIN_DATADESC( CHL1NPCTalker ) + + DEFINE_ENTITYFUNC( Touch ), + DEFINE_FIELD( m_bInBarnacleMouth, FIELD_BOOLEAN ), + DEFINE_USEFUNC( FollowerUse ), + +END_DATADESC() + +void CHL1NPCTalker::RunTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_HL1TALKER_FOLLOW_WALK_PATH_FOR_UNITS: + { + float distance; + + distance = (m_vecLastPosition - GetLocalOrigin()).Length2D(); + + // Walk path until far enough away + if ( distance > pTask->flTaskData || + GetNavigator()->GetGoalType() == GOALTYPE_NONE ) + { + TaskComplete(); + GetNavigator()->ClearGoal(); // Stop moving + } + break; + } + + + + case TASK_TALKER_CLIENT_STARE: + case TASK_TALKER_LOOK_AT_CLIENT: + { + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + + // track head to the client for a while. + if ( m_NPCState == NPC_STATE_IDLE && + !IsMoving() && + !GetExpresser()->IsSpeaking() ) + { + + if ( pPlayer ) + { + IdleHeadTurn( pPlayer ); + } + } + else + { + // started moving or talking + TaskFail( "moved away" ); + return; + } + + if ( pTask->iTask == TASK_TALKER_CLIENT_STARE ) + { + // fail out if the player looks away or moves away. + if ( ( pPlayer->GetAbsOrigin() - GetAbsOrigin() ).Length2D() > TALKER_STARE_DIST ) + { + // player moved away. + TaskFail( NO_TASK_FAILURE ); + } + + Vector vForward; + AngleVectors( GetAbsAngles(), &vForward ); + if ( UTIL_DotPoints( pPlayer->GetAbsOrigin(), GetAbsOrigin(), vForward ) < m_flFieldOfView ) + { + // player looked away + TaskFail( "looked away" ); + } + } + + if ( gpGlobals->curtime > m_flWaitFinished ) + { + TaskComplete( NO_TASK_FAILURE ); + } + + break; + } + + case TASK_WAIT_FOR_MOVEMENT: + { + if ( GetExpresser()->IsSpeaking() && GetSpeechTarget() != NULL) + { + // ALERT(at_console, "walking, talking\n"); + IdleHeadTurn( GetSpeechTarget(), GetExpresser()->GetTimeSpeechComplete() - gpGlobals->curtime ); + } + else if ( GetEnemy() ) + { + IdleHeadTurn( GetEnemy() ); + } + + BaseClass::RunTask( pTask ); + + break; + } + + case TASK_FACE_PLAYER: + { + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + + if ( pPlayer ) + { + //GetMotor()->SetIdealYaw( pPlayer->GetAbsOrigin() ); + IdleHeadTurn( pPlayer ); + if ( gpGlobals->curtime > m_flWaitFinished && GetMotor()->DeltaIdealYaw() < 10 ) + { + TaskComplete(); + } + } + else + { + TaskFail( FAIL_NO_PLAYER ); + } + + break; + } + + case TASK_TALKER_EYECONTACT: + { + if (!IsMoving() && GetExpresser()->IsSpeaking() && GetSpeechTarget() != NULL) + { + // ALERT( at_console, "waiting %f\n", m_flStopTalkTime - gpGlobals->time ); + IdleHeadTurn( GetSpeechTarget(), GetExpresser()->GetTimeSpeechComplete() - gpGlobals->curtime ); + } + + BaseClass::RunTask( pTask ); + + break; + + } + + + default: + { + if ( GetExpresser()->IsSpeaking() && GetSpeechTarget() != NULL) + { + IdleHeadTurn( GetSpeechTarget(), GetExpresser()->GetTimeSpeechComplete() - gpGlobals->curtime ); + } + else if ( GetEnemy() && m_NPCState == NPC_STATE_COMBAT ) + { + IdleHeadTurn( GetEnemy() ); + } + else if ( GetFollowTarget() ) + { + IdleHeadTurn( GetFollowTarget() ); + } + + BaseClass::RunTask( pTask ); + break; + } + } +} + +bool CHL1NPCTalker::ShouldGib( const CTakeDamageInfo &info ) +{ + if ( info.GetDamageType() & DMG_NEVERGIB ) + return false; + + if ( ( g_pGameRules->Damage_ShouldGibCorpse( info.GetDamageType() ) && m_iHealth < GIB_HEALTH_VALUE ) || ( info.GetDamageType() & DMG_ALWAYSGIB ) ) + return true; + + return false; + +} + +void CHL1NPCTalker::StartTask( const Task_t *pTask ) +{ + switch( pTask->iTask ) + { + case TASK_HL1TALKER_FOLLOW_WALK_PATH_FOR_UNITS: + { + GetNavigator()->SetMovementActivity( ACT_WALK ); + break; + } + case TASK_TALKER_SPEAK: + // ask question or make statement + FIdleSpeak(); + TaskComplete(); + break; + default: + BaseClass::StartTask( pTask ); + break; + } +} + +//========================================================= +// FIdleSpeak +// ask question of nearby friend, or make statement +//========================================================= +int CHL1NPCTalker::FIdleSpeak ( void ) +{ + if (!IsOkToSpeak()) + return FALSE; + + // if there is a friend nearby to speak to, play sentence, set friend's response time, return + // try to talk to any standing or sitting scientists nearby + CBaseEntity *pentFriend = FindNearestFriend( false ); + CHL1NPCTalker *pentTalker = dynamic_cast<CHL1NPCTalker *>( pentFriend ); + if (pentTalker && random->RandomInt(0,1) ) + { + Speak( TLK_QUESTION ); + SetSpeechTarget( pentFriend ); + + pentTalker->SetSpeechTarget( this ); + pentTalker->SetCondition( COND_TALKER_RESPOND_TO_QUESTION ); + pentTalker->SetSchedule( SCHED_TALKER_IDLE_RESPONSE ); + pentTalker->GetExpresser()->BlockSpeechUntil( GetExpresser()->GetTimeSpeechComplete() ); + + GetExpresser()->BlockSpeechUntil( gpGlobals->curtime + random->RandomFloat(4.8, 5.2) ); + + //DevMsg( "Asking some question!\n" ); + return TRUE; + } + else if ( random->RandomInt(0,1)) // otherwise, play an idle statement + { + //DevMsg( "Making idle statement!\n" ); + + Speak( TLK_IDLE ); + // set global min delay for next conversation + GetExpresser()->BlockSpeechUntil( gpGlobals->curtime + random->RandomFloat(4.8, 5.2) ); + return TRUE; + } + + // never spoke + GetExpresser()->BlockSpeechUntil( 0 ); + m_flNextIdleSpeechTime = gpGlobals->curtime + 3; + return FALSE; +} + + + +bool CHL1NPCTalker::IsValidSpeechTarget( int flags, CBaseEntity *pEntity ) +{ + if ( pEntity == this ) + return false; + + CHL1NPCTalker *pentTarget = dynamic_cast<CHL1NPCTalker *>( pEntity ); + if ( pentTarget ) + { + if ( !(flags & AIST_IGNORE_RELATIONSHIP) ) + { + if ( pEntity->IsPlayer() ) + { + if ( !IsPlayerAlly( (CBasePlayer *)pEntity ) ) + return false; + } + else + { + if ( IRelationType( pEntity ) != D_LI ) + return false; + } + } + + if ( !pEntity->IsAlive() ) + // don't dead people + return false; + + // Ignore no-target entities + if ( pEntity->GetFlags() & FL_NOTARGET ) + return false; + + CAI_BaseNPC *pNPC = pEntity->MyNPCPointer(); + if ( pNPC ) + { + // If not a NPC for some reason, or in a script. + //if ( (pNPC->m_NPCState == NPC_STATE_SCRIPT || pNPC->m_NPCState == NPC_STATE_PRONE)) + // return false; + + if ( pNPC->IsInAScript() ) + return false; + + // Don't bother people who don't want to be bothered + if ( !pNPC->CanBeUsedAsAFriend() ) + return false; + } + + if ( flags & AIST_FACING_TARGET ) + { + if ( pEntity->IsPlayer() ) + return HasCondition( COND_SEE_PLAYER ); + else if ( !FInViewCone( pEntity ) ) + return false; + } + + return FVisible( pEntity ); + } + else + return BaseClass::IsValidSpeechTarget( flags, pEntity ); +} + + +int CHL1NPCTalker::SelectSchedule ( void ) +{ + switch( m_NPCState ) + { + case NPC_STATE_PRONE: + { + if (m_bInBarnacleMouth) + { + return SCHED_HL1TALKER_BARNACLE_CHOMP; + } + else + { + return SCHED_HL1TALKER_BARNACLE_HIT; + } + } + } + + return BaseClass::SelectSchedule(); +} + +void CHL1NPCTalker::Precache() +{ + BaseClass::Precache(); + + PrecacheScriptSound( "Barney.Close" ); +} + +bool CHL1NPCTalker::HandleInteraction(int interactionType, void *data, CBaseCombatCharacter* sourceEnt) +{ + if (interactionType == g_interactionBarnacleVictimDangle) + { + // Force choosing of a new schedule + ClearSchedule( "NPC talker being eaten by a barnacle" ); + m_bInBarnacleMouth = true; + return true; + } + else if ( interactionType == g_interactionBarnacleVictimReleased ) + { + SetState ( NPC_STATE_IDLE ); + + CPASAttenuationFilter filter( this ); + + CSoundParameters params; + + if ( GetParametersForSound( "Barney.Close", params, NULL ) ) + { + EmitSound_t ep( params ); + ep.m_nPitch = GetExpresser()->GetVoicePitch(); + + EmitSound( filter, entindex(), ep ); + } + + m_bInBarnacleMouth = false; + SetAbsVelocity( vec3_origin ); + SetMoveType( MOVETYPE_STEP ); + return true; + } + else if ( interactionType == g_interactionBarnacleVictimGrab ) + { + if ( GetFlags() & FL_ONGROUND ) + { + SetGroundEntity( NULL ); + } + + if ( GetState() == NPC_STATE_SCRIPT ) + { + if ( m_hCine ) + { + m_hCine->CancelScript(); + } + } + + SetState( NPC_STATE_PRONE ); + ClearSchedule( "NPC talker grabbed by a barnacle" ); + + CTakeDamageInfo info; + PainSound( info ); + return true; + } + return false; +} + +void CHL1NPCTalker::StartFollowing( CBaseEntity *pLeader ) +{ + if ( !HasSpawnFlags( SF_NPC_GAG ) ) + { + if ( m_iszUse != NULL_STRING ) + { + PlaySentence( STRING( m_iszUse ), 0.0f ); + } + else + { + Speak( TLK_STARTFOLLOW ); + } + + SetSpeechTarget( pLeader ); + } + + BaseClass::StartFollowing( pLeader ); +} + +int CHL1NPCTalker::PlayScriptedSentence( const char *pszSentence, float delay, float volume, soundlevel_t soundlevel, bool bConcurrent, CBaseEntity *pListener ) +{ + if( hl1_debug_sentence_volume.GetBool() ) + { + Msg( "SENTENCE: %s Vol:%f SndLevel:%d\n", GetDebugName(), volume, soundlevel ); + } + + if( hl1_fixup_sentence_sndlevel.GetBool() ) + { + if( soundlevel < SNDLVL_TALKING ) + { + soundlevel = SNDLVL_TALKING; + } + } + + return BaseClass::PlayScriptedSentence( pszSentence, delay, volume, soundlevel, bConcurrent, pListener ); +} + +Disposition_t CHL1NPCTalker::IRelationType( CBaseEntity *pTarget ) +{ + if ( pTarget->IsPlayer() ) + { + if ( HasMemory( bits_MEMORY_PROVOKED ) ) + { + return D_HT; + } + } + + return BaseClass::IRelationType( pTarget ); +} + +void CHL1NPCTalker::Touch( CBaseEntity *pOther ) +{ + if ( m_NPCState == NPC_STATE_SCRIPT ) + return; + + BaseClass::Touch(pOther); +} + +void CHL1NPCTalker::StopFollowing( void ) +{ + if ( !(m_afMemory & bits_MEMORY_PROVOKED) ) + { + if ( !HasSpawnFlags( SF_NPC_GAG ) ) + { + if ( m_iszUnUse != NULL_STRING ) + { + PlaySentence( STRING( m_iszUnUse ), 0.0f ); + } + else + { + Speak( TLK_STOPFOLLOW ); + } + + SetSpeechTarget( GetFollowTarget() ); + } + } + + BaseClass::StopFollowing(); +} + +void CHL1NPCTalker::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + if ( info.GetDamage() >= 1.0 && !(info.GetDamageType() & DMG_SHOCK ) ) + { + UTIL_BloodImpact( ptr->endpos, vecDir, BloodColor(), 4 ); + } + + BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator ); +} + +void CHL1NPCTalker::FollowerUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + // Don't allow use during a scripted_sentence + if ( GetUseTime() > gpGlobals->curtime ) + return; + + if ( m_hCine && !m_hCine->CanInterrupt() ) + return; + + if ( pCaller != NULL && pCaller->IsPlayer() ) + { + // Pre-disaster followers can't be used + if ( m_spawnflags & SF_NPC_PREDISASTER ) + { + SetSpeechTarget( pCaller ); + DeclineFollowing(); + return; + } + } + + BaseClass::FollowerUse( pActivator, pCaller, useType, value ); +} + +int CHL1NPCTalker::TranslateSchedule( int scheduleType ) +{ + return BaseClass::TranslateSchedule( scheduleType ); +} + +float CHL1NPCTalker::PickLookTarget( bool bExcludePlayers, float minTime, float maxTime ) +{ + return random->RandomFloat( 5.0f, 10.0f ); +} + +void CHL1NPCTalker::IdleHeadTurn( CBaseEntity *pTarget, float flDuration, float flImportance ) +{ + // Must be able to turn our head + if (!(CapabilitiesGet() & bits_CAP_TURN_HEAD)) + return; + + // If the target is invalid, or we're in a script, do nothing + if ( ( !pTarget ) || ( m_NPCState == NPC_STATE_SCRIPT ) ) + return; + + // Fill in a duration if we haven't specified one + if ( flDuration == 0.0f ) + { + flDuration = random->RandomFloat( 2.0, 4.0 ); + } + + // Add a look target + AddLookTarget( pTarget, 1.0, flDuration ); +} + +void CHL1NPCTalker::SetHeadDirection( const Vector &vTargetPos, float flInterval) +{ +#ifdef TALKER_LOOK + // Draw line in body, head, and eye directions + Vector vEyePos = EyePosition(); + Vector vHeadDir = HeadDirection3D(); + Vector vBodyDir = BodyDirection2D(); + + //UNDONE <<TODO>> + // currently eye dir just returns head dir, so use vTargetPos for now + //Vector vEyeDir; w + //EyeDirection3D(&vEyeDir); + NDebugOverlay::Line( vEyePos, vEyePos+(50*vHeadDir), 255, 0, 0, false, 0.1 ); + NDebugOverlay::Line( vEyePos, vEyePos+(50*vBodyDir), 0, 255, 0, false, 0.1 ); + NDebugOverlay::Line( vEyePos, vTargetPos, 0, 0, 255, false, 0.1 ); +#endif + +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CHL1NPCTalker::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_nMaterial = 1; + data.m_nHitBox = -m_iHealth; + + data.m_nColor = BloodColor(); + + DispatchEffect( "HL1Gib", data ); + + CSoundEnt::InsertSound( SOUND_MEAT, GetAbsOrigin(), 256, 0.5f, this ); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CHL1NPCTalker::OnObstructingDoor( AILocalMoveGoal_t *pMoveGoal, CBaseDoor *pDoor, float distClear, AIMoveResult_t *pResult ) +{ + // If we can't get through the door, try and open it + if ( BaseClass::OnObstructingDoor( pMoveGoal, pDoor, distClear, pResult ) ) + { + if ( IsMoveBlocked( *pResult ) && pMoveGoal->directTrace.vHitNormal != vec3_origin ) + { + // Can't do anything if the door's locked + if ( !pDoor->m_bLocked && !pDoor->HasSpawnFlags(SF_DOOR_NONPCS) ) + { + // Tell the door to open + variant_t emptyVariant; + pDoor->AcceptInput( "Open", this, this, emptyVariant, USE_TOGGLE ); + *pResult = AIMR_OK; + } + } + return true; + } + + return false; +} + +// HL1 version - never return Ragdoll as the automatic schedule at the end of a +// scripted sequence +int CHL1NPCTalker::SelectDeadSchedule() +{ + // Alread dead (by animation event maybe?) + // Is it safe to set it to SCHED_NONE? + if ( m_lifeState == LIFE_DEAD ) + return SCHED_NONE; + + CleanupOnDeath(); + return SCHED_DIE; +} + + +AI_BEGIN_CUSTOM_NPC( monster_hl1talker, CHL1NPCTalker ) + + DECLARE_TASK( TASK_HL1TALKER_FOLLOW_WALK_PATH_FOR_UNITS ) + + //========================================================= + // > SCHED_HL1TALKER_MOVE_AWAY_FOLLOW + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HL1TALKER_FOLLOW_MOVE_AWAY, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_TARGET_FACE" + " TASK_STORE_LASTPOSITION 0" + " TASK_MOVE_AWAY_PATH 100" + " TASK_HL1TALKER_FOLLOW_WALK_PATH_FOR_UNITS 100" + " TASK_STOP_MOVING 0" + " TASK_FACE_PLAYER 0" + " TASK_SET_ACTIVITY ACT_IDLE" + "" + " Interrupts" + ) + + //========================================================= + // > SCHED_HL1TALKER_IDLE_SPEAK_WAIT + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HL1TALKER_IDLE_SPEAK_WAIT, + + " Tasks" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" // Stop and talk + " TASK_FACE_PLAYER 0" + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_HEAR_DANGER" + ) + + //========================================================= + // > SCHED_HL1TALKER_BARNACLE_HIT + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HL1TALKER_BARNACLE_HIT, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_BARNACLE_HIT" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_HL1TALKER_BARNACLE_PULL" + "" + " Interrupts" + ) + + //========================================================= + // > SCHED_HL1TALKER_BARNACLE_PULL + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HL1TALKER_BARNACLE_PULL, + + " Tasks" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_BARNACLE_PULL" + "" + " Interrupts" + ) + + //========================================================= + // > SCHED_HL1TALKER_BARNACLE_CHOMP + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HL1TALKER_BARNACLE_CHOMP, + + " Tasks" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_BARNACLE_CHOMP" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_HL1TALKER_BARNACLE_CHEW" + "" + " Interrupts" + ) + + //========================================================= + // > SCHED_HL1TALKER_BARNACLE_CHEW + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HL1TALKER_BARNACLE_CHEW, + + " Tasks" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_BARNACLE_CHEW" + ) + +AI_END_CUSTOM_NPC() diff --git a/game/server/hl1/hl1_npc_talker.h b/game/server/hl1/hl1_npc_talker.h new file mode 100644 index 0000000..f3d883b --- /dev/null +++ b/game/server/hl1/hl1_npc_talker.h @@ -0,0 +1,125 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Base combat character with no AI +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// + +#ifndef HL1TALKNPC_H +#define HL1TALKNPC_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "soundflags.h" + +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_default.h" +#include "ai_speech.h" +#include "ai_basenpc.h" +#include "ai_behavior.h" +#include "ai_behavior_follow.h" +#include "npc_talker.h" + + +#define SF_NPC_PREDISASTER ( 1 << 16 ) // This is a predisaster scientist or barney. Influences how they speak. + + + +//========================================================= +// Talking NPC base class +// Used for scientists and barneys +//========================================================= + +//============================================================================= +// >> CHL1NPCTalker +//============================================================================= + +class CHL1NPCTalker : public CNPCSimpleTalker +{ + DECLARE_CLASS( CHL1NPCTalker, CNPCSimpleTalker ); + +public: + CHL1NPCTalker( void ) + { + } + + virtual void Precache(); + + void StartTask( const Task_t *pTask ); + void RunTask( const Task_t *pTask ); + int SelectSchedule ( void ); + bool HandleInteraction(int interactionType, void *data, CBaseCombatCharacter* sourceEnt); + bool ShouldGib( const CTakeDamageInfo &info ); + + int TranslateSchedule( int scheduleType ); + void IdleHeadTurn( CBaseEntity *pTarget, float flDuration = 0.0, float flImportance = 1.0f ); + void SetHeadDirection( const Vector &vTargetPos, float flInterval); + bool CorpseGib( const CTakeDamageInfo &info ); + + Disposition_t IRelationType( CBaseEntity *pTarget ); + + void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + + void StartFollowing( CBaseEntity *pLeader ); + void StopFollowing( void ); + int PlayScriptedSentence( const char *pszSentence, float delay, float volume, soundlevel_t soundlevel, bool bConcurrent, CBaseEntity *pListener ); + + + void Touch( CBaseEntity *pOther ); + + float PickLookTarget( bool bExcludePlayers = false, float minTime = 1.5, float maxTime = 2.5 ); + + bool OnObstructingDoor( AILocalMoveGoal_t *pMoveGoal, CBaseDoor *pDoor, float distClear, AIMoveResult_t *pResult ); + + // Hacks! HL2 has a system for avoiding the player, we don't + // This ensures that we fall back to the real player avoidance + // Essentially does the opposite of what it says + virtual bool ShouldPlayerAvoid( void ) { return false; } + + bool IsValidSpeechTarget( int flags, CBaseEntity *pEntity ); + +protected: + virtual void FollowerUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + int FIdleSpeak ( void ); + +private: + virtual void DeclineFollowing( void ) {} + + virtual int SelectDeadSchedule( void ); + +public: + + bool m_bInBarnacleMouth; + + + enum + { + SCHED_HL1TALKER_FOLLOW_MOVE_AWAY = BaseClass::NEXT_SCHEDULE, + SCHED_HL1TALKER_IDLE_SPEAK_WAIT, + SCHED_HL1TALKER_BARNACLE_HIT, + SCHED_HL1TALKER_BARNACLE_PULL, + SCHED_HL1TALKER_BARNACLE_CHOMP, + SCHED_HL1TALKER_BARNACLE_CHEW, + + NEXT_SCHEDULE, + }; + + enum + { + TASK_HL1TALKER_FOLLOW_WALK_PATH_FOR_UNITS = BaseClass::NEXT_TASK, + + NEXT_TASK, + }; + + DECLARE_DATADESC(); + DEFINE_CUSTOM_AI; +}; + + + +#endif //HL1TALKNPC_H diff --git a/game/server/hl1/hl1_npc_tentacle.cpp b/game/server/hl1/hl1_npc_tentacle.cpp new file mode 100644 index 0000000..c4cc8d1 --- /dev/null +++ b/game/server/hl1/hl1_npc_tentacle.cpp @@ -0,0 +1,1012 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Bullseyes act as targets for other NPC's to attack and to trigger +// events +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "ai_senses.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "basecombatweapon.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "hl1_ai_basenpc.h" +#include "studio.h" //hitbox parsing +#include "collisionutils.h" //ComputeSeparatingPlane + +#include "physics_bone_follower.h" //For BoneFollowerManager + +#define ACT_T_IDLE 1010 +Activity ACT_1010; +Activity ACT_1011; +Activity ACT_1012; +Activity ACT_1013; + +#define ACT_T_TAP 1020 +Activity ACT_1020; +Activity ACT_1021; +Activity ACT_1022; +Activity ACT_1023; + +#define ACT_T_STRIKE 1030 +Activity ACT_1030; +Activity ACT_1031; +Activity ACT_1032; +Activity ACT_1033; + +#define ACT_T_REARIDLE 1040 +Activity ACT_1040; +Activity ACT_1041; +Activity ACT_1042; +Activity ACT_1043; +Activity ACT_1044; + +class CNPC_Tentacle : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_Tentacle, CHL1BaseNPC ); +public: + + CNPC_Tentacle(); + + void Spawn( ); + void Precache( ); + bool KeyValue( const char *szKeyName, const char *szValue ); + + bool QueryHearSound( CSound *pSound ) { return true; } // Tentacle isn't picky. + + + int Level( float dz ); + int MyLevel( void ); + float MyHeight( void ); + + // Don't allow the tentacle to go across transitions!!! + virtual int ObjectCaps( void ) { return CAI_BaseNPC::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + + void Start ( void ); + void Cycle ( void ); + void HitTouch( CBaseEntity *pOther ); + + void HandleAnimEvent( animevent_t *pEvent ); + float HearingSensitivity( void ) { return 2.0; }; + + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + + bool CreateVPhysics( void ); + + void UpdateOnRemove( void ); + + float m_flInitialYaw; + int m_iGoalAnim; + int m_iLevel; + int m_iDir; + float m_flFramerateAdj; + float m_flSoundYaw; + int m_iSoundLevel; + float m_flSoundTime; + float m_flSoundRadius; + int m_iHitDmg; + float m_flHitTime; + + float m_flTapRadius; + + float m_flNextSong; + static int g_fFlySound; + static int g_fSquirmSound; + + float m_flMaxYaw; + int m_iTapSound; + + Vector m_vecPrevSound; + float m_flPrevSoundTime; + + float MaxYawSpeed( void ) { return 18.0f; } + + bool HeardAnything( void ); + + Class_T Classify ( void ); + + CBoneFollowerManager m_BoneFollowerManager; + + DECLARE_DATADESC(); + DEFINE_CUSTOM_AI; +}; + +// Crane bones that have physics followers +static const char *pTentacleFollowerBoneNames[] = +{ + "Bone08", + "Bone09" +}; + +int CNPC_Tentacle::g_fFlySound; +int CNPC_Tentacle::g_fSquirmSound; + +LINK_ENTITY_TO_CLASS( monster_tentacle, CNPC_Tentacle ); + +// stike sounds +#define TE_NONE -1 +#define TE_SILO 0 +#define TE_DIRT 1 +#define TE_WATER 2 + +// animation sequence aliases +typedef enum +{ + TENTACLE_ANIM_Pit_Idle, + + TENTACLE_ANIM_rise_to_Temp1, + TENTACLE_ANIM_Temp1_to_Floor, + TENTACLE_ANIM_Floor_Idle, + TENTACLE_ANIM_Floor_Fidget_Pissed, + TENTACLE_ANIM_Floor_Fidget_SmallRise, + TENTACLE_ANIM_Floor_Fidget_Wave, + TENTACLE_ANIM_Floor_Strike, + TENTACLE_ANIM_Floor_Tap, + TENTACLE_ANIM_Floor_Rotate, + TENTACLE_ANIM_Floor_Rear, + TENTACLE_ANIM_Floor_Rear_Idle, + TENTACLE_ANIM_Floor_to_Lev1, + + TENTACLE_ANIM_Lev1_Idle, + TENTACLE_ANIM_Lev1_Fidget_Claw, + TENTACLE_ANIM_Lev1_Fidget_Shake, + TENTACLE_ANIM_Lev1_Fidget_Snap, + TENTACLE_ANIM_Lev1_Strike, + TENTACLE_ANIM_Lev1_Tap, + TENTACLE_ANIM_Lev1_Rotate, + TENTACLE_ANIM_Lev1_Rear, + TENTACLE_ANIM_Lev1_Rear_Idle, + TENTACLE_ANIM_Lev1_to_Lev2, + + TENTACLE_ANIM_Lev2_Idle, + TENTACLE_ANIM_Lev2_Fidget_Shake, + TENTACLE_ANIM_Lev2_Fidget_Swing, + TENTACLE_ANIM_Lev2_Fidget_Tut, + TENTACLE_ANIM_Lev2_Strike, + TENTACLE_ANIM_Lev2_Tap, + TENTACLE_ANIM_Lev2_Rotate, + TENTACLE_ANIM_Lev2_Rear, + TENTACLE_ANIM_Lev2_Rear_Idle, + TENTACLE_ANIM_Lev2_to_Lev3, + + TENTACLE_ANIM_Lev3_Idle, + TENTACLE_ANIM_Lev3_Fidget_Shake, + TENTACLE_ANIM_Lev3_Fidget_Side, + TENTACLE_ANIM_Lev3_Fidget_Swipe, + TENTACLE_ANIM_Lev3_Strike, + TENTACLE_ANIM_Lev3_Tap, + TENTACLE_ANIM_Lev3_Rotate, + TENTACLE_ANIM_Lev3_Rear, + TENTACLE_ANIM_Lev3_Rear_Idle, + + TENTACLE_ANIM_Lev1_Door_reach, + + TENTACLE_ANIM_Lev3_to_Engine, + TENTACLE_ANIM_Engine_Idle, + TENTACLE_ANIM_Engine_Sway, + TENTACLE_ANIM_Engine_Swat, + TENTACLE_ANIM_Engine_Bob, + TENTACLE_ANIM_Engine_Death1, + TENTACLE_ANIM_Engine_Death2, + TENTACLE_ANIM_Engine_Death3, + + TENTACLE_ANIM_none +} TENTACLE_ANIM; + +BEGIN_DATADESC( CNPC_Tentacle ) + DEFINE_FIELD( m_flInitialYaw, FIELD_FLOAT ), + DEFINE_FIELD( m_iGoalAnim, FIELD_INTEGER ), + DEFINE_FIELD( m_iLevel, FIELD_INTEGER ), + DEFINE_FIELD( m_iDir, FIELD_INTEGER ), + DEFINE_FIELD( m_flFramerateAdj, FIELD_FLOAT ), + DEFINE_FIELD( m_flSoundYaw, FIELD_FLOAT ), + DEFINE_FIELD( m_iSoundLevel, FIELD_INTEGER ), + DEFINE_FIELD( m_flSoundTime, FIELD_TIME ), + DEFINE_FIELD( m_flSoundRadius, FIELD_FLOAT ), + DEFINE_FIELD( m_iHitDmg, FIELD_INTEGER ), + DEFINE_FIELD( m_flHitTime, FIELD_TIME ), + DEFINE_FIELD( m_flTapRadius, FIELD_FLOAT ), + DEFINE_FIELD( m_flNextSong, FIELD_TIME ), + DEFINE_FIELD( m_iTapSound, FIELD_INTEGER ), + DEFINE_FIELD( m_flMaxYaw, FIELD_FLOAT ), + DEFINE_FIELD( m_vecPrevSound, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_flPrevSoundTime, FIELD_TIME ), + + DEFINE_EMBEDDED( m_BoneFollowerManager ), + + DEFINE_THINKFUNC( Start ), + DEFINE_THINKFUNC( Cycle ), + DEFINE_ENTITYFUNC( HitTouch ), +END_DATADESC() + +Class_T CNPC_Tentacle::Classify ( void ) +{ + return CLASS_ALIEN_MONSTER; +} + + +CNPC_Tentacle::CNPC_Tentacle() +{ + m_flMaxYaw = 65; + m_iTapSound = 0; +} + +bool CNPC_Tentacle::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "sweeparc") ) + { + m_flMaxYaw = atof( szValue ) / 2.0; + return true; + } + else if (FStrEq( szKeyName, "sound")) + { + m_iTapSound = atoi( szValue ); + return true; + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return false; +} + +// +// Tentacle Spawn +// +void CNPC_Tentacle::Spawn( ) +{ + Precache( ); + + SetSolid( SOLID_BBOX ); + + //Necessary for TestCollision to be called for hitbox ray hits + AddSolidFlags( FSOLID_CUSTOMRAYTEST ); + + SetMoveType( MOVETYPE_NONE ); + ClearEffects(); + m_iHealth = 75; + SetSequence( 0 ); + + SetModel( "models/tentacle2.mdl" ); + UTIL_SetSize( this, Vector( -32, -32, 0 ), Vector( 32, 32, 64 ) ); + + // Use our hitboxes to determine our render bounds + CollisionProp()->SetSurroundingBoundsType( USE_HITBOXES ); + + m_takedamage = DAMAGE_AIM; + AddFlag( FL_NPC ); + + m_bloodColor = BLOOD_COLOR_GREEN; + + ResetSequenceInfo( ); + m_iDir = 1; + + SetThink( &CNPC_Tentacle::Start ); + SetNextThink( gpGlobals->curtime + 0.2 ); + + SetTouch( &CNPC_Tentacle::HitTouch ); + + m_flInitialYaw = GetAbsAngles().y; + GetMotor()->SetIdealYawAndUpdate( m_flInitialYaw ); + + g_fFlySound = FALSE; + g_fSquirmSound = FALSE; + + m_iHitDmg = 200; + + if (m_flMaxYaw <= 0) + m_flMaxYaw = 65; + + m_NPCState = NPC_STATE_IDLE; + + UTIL_SetOrigin( this, GetAbsOrigin() ); + + CreateVPhysics(); + + AddEffects( EF_NOSHADOW ); +} + +void CNPC_Tentacle::UpdateOnRemove( void ) +{ + m_BoneFollowerManager.DestroyBoneFollowers(); + BaseClass::UpdateOnRemove(); +} + +void CNPC_Tentacle::Precache( ) +{ + PrecacheModel("models/tentacle2.mdl"); + + PrecacheScriptSound( "Tentacle.Flies" ); + PrecacheScriptSound( "Tentacle.Squirm" ); + PrecacheScriptSound( "Tentacle.Sing" ); + PrecacheScriptSound( "Tentacle.HitDirt" ); + PrecacheScriptSound( "Tentacle.Swing" ); + PrecacheScriptSound( "Tentacle.Search" ); + PrecacheScriptSound( "Tentacle.Roar" ); + PrecacheScriptSound( "Tentacle.Alert" ); + + BaseClass::Precache(); +} + + +int CNPC_Tentacle::Level( float dz ) +{ + if (dz < 216) + return 0; + if (dz < 408) + return 1; + if (dz < 600) + return 2; + return 3; +} + + +float CNPC_Tentacle::MyHeight( ) +{ + switch ( MyLevel( ) ) + { + case 1: + return 256; + case 2: + return 448; + case 3: + return 640; + } + return 0; +} + + +int CNPC_Tentacle::MyLevel( ) +{ + switch( GetSequence() ) + { + case TENTACLE_ANIM_Pit_Idle: + return -1; + + case TENTACLE_ANIM_rise_to_Temp1: + case TENTACLE_ANIM_Temp1_to_Floor: + case TENTACLE_ANIM_Floor_to_Lev1: + return 0; + + case TENTACLE_ANIM_Floor_Idle: + case TENTACLE_ANIM_Floor_Fidget_Pissed: + case TENTACLE_ANIM_Floor_Fidget_SmallRise: + case TENTACLE_ANIM_Floor_Fidget_Wave: + case TENTACLE_ANIM_Floor_Strike: + case TENTACLE_ANIM_Floor_Tap: + case TENTACLE_ANIM_Floor_Rotate: + case TENTACLE_ANIM_Floor_Rear: + case TENTACLE_ANIM_Floor_Rear_Idle: + return 0; + + case TENTACLE_ANIM_Lev1_Idle: + case TENTACLE_ANIM_Lev1_Fidget_Claw: + case TENTACLE_ANIM_Lev1_Fidget_Shake: + case TENTACLE_ANIM_Lev1_Fidget_Snap: + case TENTACLE_ANIM_Lev1_Strike: + case TENTACLE_ANIM_Lev1_Tap: + case TENTACLE_ANIM_Lev1_Rotate: + case TENTACLE_ANIM_Lev1_Rear: + case TENTACLE_ANIM_Lev1_Rear_Idle: + return 1; + + case TENTACLE_ANIM_Lev1_to_Lev2: + return 1; + + case TENTACLE_ANIM_Lev2_Idle: + case TENTACLE_ANIM_Lev2_Fidget_Shake: + case TENTACLE_ANIM_Lev2_Fidget_Swing: + case TENTACLE_ANIM_Lev2_Fidget_Tut: + case TENTACLE_ANIM_Lev2_Strike: + case TENTACLE_ANIM_Lev2_Tap: + case TENTACLE_ANIM_Lev2_Rotate: + case TENTACLE_ANIM_Lev2_Rear: + case TENTACLE_ANIM_Lev2_Rear_Idle: + return 2; + + case TENTACLE_ANIM_Lev2_to_Lev3: + return 2; + + case TENTACLE_ANIM_Lev3_Idle: + case TENTACLE_ANIM_Lev3_Fidget_Shake: + case TENTACLE_ANIM_Lev3_Fidget_Side: + case TENTACLE_ANIM_Lev3_Fidget_Swipe: + case TENTACLE_ANIM_Lev3_Strike: + case TENTACLE_ANIM_Lev3_Tap: + case TENTACLE_ANIM_Lev3_Rotate: + case TENTACLE_ANIM_Lev3_Rear: + case TENTACLE_ANIM_Lev3_Rear_Idle: + return 3; + + case TENTACLE_ANIM_Lev1_Door_reach: + return -1; + } + return -1; +} + +void CNPC_Tentacle::Start( void ) +{ + SetThink( &CNPC_Tentacle::Cycle ); + + CPASAttenuationFilter filter( this ); + + if ( !g_fFlySound ) + { + EmitSound( filter, entindex(), "Tentacle.Flies" ); + g_fFlySound = TRUE; + } + else if ( !g_fSquirmSound ) + { + EmitSound( filter, entindex(), "Tentacle.Squirm" ); + g_fSquirmSound = TRUE; + } + + SetNextThink( gpGlobals->curtime + 0.1 ); +} + +bool CNPC_Tentacle::HeardAnything( void ) +{ + if ( HasCondition( COND_HEAR_DANGER ) || // I remove a bunch of sounds from here on purpose. Talk to me if you're changing this!(sjb) + HasCondition( COND_HEAR_COMBAT ) || + HasCondition( COND_HEAR_WORLD ) || + HasCondition( COND_HEAR_PLAYER ) ) + return true; + + return false; +} + +void CNPC_Tentacle::Cycle( void ) +{ + //NDebugOverlay::Cross3D( EarPosition(), 32, 255, 0, 0, false, 0.1 ); + + // ALERT( at_console, "%s %.2f %d %d\n", STRING( pev->targetname ), pev->origin.z, m_MonsterState, m_IdealMonsterState ); + SetNextThink( gpGlobals->curtime + 0.1 ); + + // ALERT( at_console, "%s %d %d %d %f %f\n", STRING( pev->targetname ), pev->sequence, m_iGoalAnim, m_iDir, pev->framerate, pev->health ); + + if ( m_NPCState == NPC_STATE_SCRIPT || GetIdealState() == NPC_STATE_SCRIPT) + { + SetAbsAngles( QAngle( GetAbsAngles().x, m_flInitialYaw, GetAbsAngles().z ) ); + GetMotor()->SetIdealYaw( m_flInitialYaw ); + RemoveIgnoredConditions(); + NPCThink( ); + m_iGoalAnim = TENTACLE_ANIM_Pit_Idle; + return; + } + + StudioFrameAdvance(); + DispatchAnimEvents( this ); + + GetMotor()->UpdateYaw( MaxYawSpeed() ); + + CSound *pSound = NULL; + + GetSenses()->Listen(); + m_BoneFollowerManager.UpdateBoneFollowers(this); + + // Listen will set this if there's something in my sound list + if ( HeardAnything() ) + pSound = GetSenses()->GetClosestSound( false, (SOUND_DANGER|SOUND_COMBAT|SOUND_WORLD|SOUND_PLAYER) ); + else + pSound = NULL; + + if ( pSound ) + { + //NDebugOverlay::Line( EarPosition(), pSound->GetSoundOrigin(), 0, 255, 0, false, 0.2 ); + + Vector vecDir; + if ( gpGlobals->curtime - m_flPrevSoundTime < 0.5 ) + { + float dt = gpGlobals->curtime - m_flPrevSoundTime; + vecDir = pSound->GetSoundOrigin() + (pSound->GetSoundOrigin() - m_vecPrevSound) / dt - GetAbsOrigin(); + } + else + { + vecDir = pSound->GetSoundOrigin() - GetAbsOrigin(); + } + + m_flPrevSoundTime = gpGlobals->curtime; + m_vecPrevSound = pSound->GetSoundOrigin(); + + m_flSoundYaw = VecToYaw ( vecDir ) - m_flInitialYaw; + + m_iSoundLevel = Level( vecDir.z ); + + if (m_flSoundYaw < -180) + m_flSoundYaw += 360; + if (m_flSoundYaw > 180) + m_flSoundYaw -= 360; + + // ALERT( at_console, "sound %d %.0f\n", m_iSoundLevel, m_flSoundYaw ); + if (m_flSoundTime < gpGlobals->curtime) + { + // play "I hear new something" sound + UTIL_EmitAmbientSound( GetSoundSourceIndex(), GetAbsOrigin() + Vector( 0, 0, MyHeight()), "Tentacle.Alert", 1.0, SNDLVL_GUNFIRE, 0, 100); + } + m_flSoundTime = gpGlobals->curtime + random->RandomFloat( 5.0, 10.0 ); + } + + // clip ideal_yaw + float dy = m_flSoundYaw; + switch( GetSequence() ) + { + case TENTACLE_ANIM_Floor_Rear: + case TENTACLE_ANIM_Floor_Rear_Idle: + case TENTACLE_ANIM_Lev1_Rear: + case TENTACLE_ANIM_Lev1_Rear_Idle: + case TENTACLE_ANIM_Lev2_Rear: + case TENTACLE_ANIM_Lev2_Rear_Idle: + case TENTACLE_ANIM_Lev3_Rear: + case TENTACLE_ANIM_Lev3_Rear_Idle: + if (dy < 0 && dy > -m_flMaxYaw) + dy = -m_flMaxYaw; + if (dy > 0 && dy < m_flMaxYaw) + dy = m_flMaxYaw; + break; + default: + if (dy < -m_flMaxYaw) + dy = -m_flMaxYaw; + if (dy > m_flMaxYaw) + dy = m_flMaxYaw; + } + GetMotor()->SetIdealYaw( m_flInitialYaw + dy ); + + if ( IsSequenceFinished() ) + { + // ALERT( at_console, "%s done %d %d\n", STRING( pev->targetname ), pev->sequence, m_iGoalAnim ); + if ( m_iHealth <= 1) + { + m_iGoalAnim = TENTACLE_ANIM_Pit_Idle; + + if ( GetSequence() == TENTACLE_ANIM_Pit_Idle) + { + m_iHealth = 75; + } + } + else if ( m_flSoundTime > gpGlobals->curtime ) + { + if (m_flSoundYaw >= -(m_flMaxYaw + 30) && m_flSoundYaw <= (m_flMaxYaw + 30)) + { + // strike + switch ( m_iSoundLevel ) + { + case 0: + m_iGoalAnim = SelectWeightedSequence ( ACT_1030 ); + break; + case 1: + m_iGoalAnim = SelectWeightedSequence ( ACT_1031 ); + break; + case 2: + m_iGoalAnim = SelectWeightedSequence ( ACT_1032 ); + break; + case 3: + m_iGoalAnim = SelectWeightedSequence ( ACT_1033 ); + break; + } + } + else if (m_flSoundYaw >= -m_flMaxYaw * 2 && m_flSoundYaw <= m_flMaxYaw * 2) + { + // tap + switch ( m_iSoundLevel ) + { + case 0: + m_iGoalAnim = SelectWeightedSequence ( ACT_1020 ); + break; + case 1: + m_iGoalAnim = SelectWeightedSequence ( ACT_1021 ); + break; + case 2: + m_iGoalAnim = SelectWeightedSequence ( ACT_1022 ); + break; + case 3: + m_iGoalAnim = SelectWeightedSequence ( ACT_1023 ); + break; + } + } + else + { + // go into rear idle + switch ( m_iSoundLevel ) + { + case 0: + m_iGoalAnim = SelectWeightedSequence ( ACT_1040 ); + break; + case 1: + m_iGoalAnim = SelectWeightedSequence ( ACT_1041 ); + break; + case 2: + m_iGoalAnim = SelectWeightedSequence ( ACT_1042 ); + break; + case 3: + m_iGoalAnim = SelectWeightedSequence ( ACT_1043 ); + break; + case 4: + m_iGoalAnim = SelectWeightedSequence ( ACT_1044 ); + break; + } + } + } + else if ( GetSequence() == TENTACLE_ANIM_Pit_Idle) + { + // stay in pit until hear noise + m_iGoalAnim = TENTACLE_ANIM_Pit_Idle; + } + else if ( GetSequence() == m_iGoalAnim) + { + if ( MyLevel() >= 0 && gpGlobals->curtime < m_flSoundTime) + { + if ( random->RandomInt(0,9) < m_flSoundTime - gpGlobals->curtime ) + { + // continue stike + switch ( m_iSoundLevel ) + { + case 0: + m_iGoalAnim = SelectWeightedSequence ( ACT_1030 ); + break; + case 1: + m_iGoalAnim = SelectWeightedSequence ( ACT_1031 ); + break; + case 2: + m_iGoalAnim = SelectWeightedSequence ( ACT_1032 ); + break; + case 3: + m_iGoalAnim = SelectWeightedSequence ( ACT_1033 ); + break; + } + } + else + { + // tap + switch ( m_iSoundLevel ) + { + case 0: + m_iGoalAnim = SelectWeightedSequence ( ACT_1020 ); + break; + case 1: + m_iGoalAnim = SelectWeightedSequence ( ACT_1021 ); + break; + case 2: + m_iGoalAnim = SelectWeightedSequence ( ACT_1022 ); + break; + case 3: + m_iGoalAnim = SelectWeightedSequence ( ACT_1023 ); + break; + } + } + } + else if ( MyLevel( ) < 0 ) + { + m_iGoalAnim = SelectWeightedSequence( ACT_1010 ); + } + else + { + if (m_flNextSong < gpGlobals->curtime) + { + // play "I hear new something" sound + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Tentacle.Sing" ); + + m_flNextSong = gpGlobals->curtime + random->RandomFloat( 10, 20 ); + } + + if (random->RandomInt(0,15) == 0) + { + // idle on new level + switch ( random->RandomInt( 0, 3 ) ) + { + case 0: + m_iGoalAnim = SelectWeightedSequence ( ACT_1010 ); + break; + case 1: + m_iGoalAnim = SelectWeightedSequence ( ACT_1011 ); + break; + case 2: + m_iGoalAnim = SelectWeightedSequence ( ACT_1012 ); + break; + case 3: + m_iGoalAnim = SelectWeightedSequence ( ACT_1013 ); + break; + } + } + else if ( random->RandomInt( 0, 3 ) == 0 ) + { + // tap + switch ( MyLevel() ) + { + case 0: + m_iGoalAnim = SelectWeightedSequence ( ACT_1020 ); + break; + case 1: + m_iGoalAnim = SelectWeightedSequence ( ACT_1021 ); + break; + case 2: + m_iGoalAnim = SelectWeightedSequence ( ACT_1022 ); + break; + case 3: + m_iGoalAnim = SelectWeightedSequence ( ACT_1023 ); + break; + } + } + else + { + // idle + switch ( MyLevel() ) + { + case 0: + m_iGoalAnim = SelectWeightedSequence ( ACT_1010 ); + break; + case 1: + m_iGoalAnim = SelectWeightedSequence ( ACT_1011 ); + break; + case 2: + m_iGoalAnim = SelectWeightedSequence ( ACT_1012 ); + break; + case 3: + m_iGoalAnim = SelectWeightedSequence ( ACT_1013 ); + break; + } + } + } + if (m_flSoundYaw < 0) + m_flSoundYaw += random->RandomFloat( 2, 8 ); + else + m_flSoundYaw -= random->RandomFloat( 2, 8 ); + } + + SetSequence( FindTransitionSequence( GetSequence(), m_iGoalAnim, &m_iDir ) ); + + + if (m_iDir > 0) + { + SetCycle( 0 ); + } + else + { + m_iDir = -1; // just to safe + SetCycle( 1.0f ); + } + + ResetSequenceInfo( ); + + m_flFramerateAdj = random->RandomFloat( -0.2, 0.2 ); + m_flPlaybackRate = m_iDir * 1.0 + m_flFramerateAdj; + + switch( GetSequence() ) + { + case TENTACLE_ANIM_Floor_Tap: + case TENTACLE_ANIM_Lev1_Tap: + case TENTACLE_ANIM_Lev2_Tap: + case TENTACLE_ANIM_Lev3_Tap: + { + Vector vecSrc, v_forward; + AngleVectors( GetAbsAngles(), &v_forward ); + + trace_t tr1, tr2; + + vecSrc = GetAbsOrigin() + Vector( 0, 0, MyHeight() - 4); + UTIL_TraceLine( vecSrc, vecSrc + v_forward * 512, MASK_NPCSOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr1 ); + + vecSrc = GetAbsOrigin() + Vector( 0, 0, MyHeight() + 8); + UTIL_TraceLine( vecSrc, vecSrc + v_forward * 512, MASK_NPCSOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr2 ); + + // ALERT( at_console, "%f %f\n", tr1.flFraction * 512, tr2.flFraction * 512 ); + + m_flTapRadius = SetPoseParameter( 0, random->RandomFloat( tr1.fraction * 512, tr2.fraction * 512 ) ); + } + break; + default: + m_flTapRadius = 336; // 400 - 64 + break; + } + SetViewOffset( Vector( 0, 0, MyHeight() ) ); + // ALERT( at_console, "seq %d\n", pev->sequence ); + } + + if (m_flPrevSoundTime + 2.0 > gpGlobals->curtime) + { + // 1.5 normal speed if hears sounds + m_flPlaybackRate = m_iDir * 1.5 + m_flFramerateAdj; + } + else if (m_flPrevSoundTime + 5.0 > gpGlobals->curtime) + { + // slowdown to normal + m_flPlaybackRate = m_iDir + m_iDir * (5 - (gpGlobals->curtime - m_flPrevSoundTime)) / 2 + m_flFramerateAdj; + } +} + +void CNPC_Tentacle::HandleAnimEvent( animevent_t *pEvent ) +{ + switch( pEvent->event ) + { + case 1: // bang + { + Vector vecSrc; + QAngle angAngles; + GetAttachment( "0", vecSrc, angAngles ); + + // Vector vecSrc = GetAbsOrigin() + m_flTapRadius * Vector( cos( GetAbsAngles().y * (3.14192653 / 180.0) ), sin( GetAbsAngles().y * (M_PI / 180.0) ), 0.0 ); + + // vecSrc.z += MyHeight( ); + + switch( m_iTapSound ) + { + case TE_SILO: + UTIL_EmitAmbientSound( GetSoundSourceIndex(), vecSrc, "Tentacle.HitSilo", 1.0, SNDLVL_GUNFIRE, 0, 100); + break; + case TE_NONE: + break; + case TE_DIRT: + UTIL_EmitAmbientSound( GetSoundSourceIndex(), vecSrc, "Tentacle.HitDirt", 1.0, SNDLVL_GUNFIRE, 0, 100); + break; + case TE_WATER: + UTIL_EmitAmbientSound( GetSoundSourceIndex(), vecSrc, "Tentacle.HitWater", 1.0, SNDLVL_GUNFIRE, 0, 100); + break; + } + } + break; + + case 3: // start killing swing + m_iHitDmg = 200; + break; + + case 4: // end killing swing + m_iHitDmg = 25; + break; + + case 5: // just "whoosh" sound + break; + + case 2: // tap scrape + case 6: // light tap + { + Vector vecSrc = GetAbsOrigin() + m_flTapRadius * Vector( cos( GetAbsAngles().y * (M_PI / 180.0) ), sin( GetAbsAngles().y * (M_PI / 180.0) ), 0.0 ); + + vecSrc.z += MyHeight( ); + + float flVol = random->RandomFloat( 0.3, 0.5 ); + + switch( m_iTapSound ) + { + case TE_SILO: + UTIL_EmitAmbientSound( GetSoundSourceIndex(), vecSrc, "Tentacle.HitSilo", flVol, SNDLVL_GUNFIRE, 0, 100); + break; + case TE_NONE: + break; + case TE_DIRT: + UTIL_EmitAmbientSound( GetSoundSourceIndex(), vecSrc, "Tentacle.HitDirt", flVol, SNDLVL_GUNFIRE, 0, 100); + break; + case TE_WATER: + UTIL_EmitAmbientSound( GetSoundSourceIndex(), vecSrc, "Tentacle.HitWater", flVol, SNDLVL_GUNFIRE, 0, 100); + break; + } + } + break; + + + case 7: // roar + UTIL_EmitAmbientSound( GetSoundSourceIndex(), GetAbsOrigin() + Vector( 0, 0, MyHeight()), "Tentacle.Roar", 1.0, SNDLVL_GUNFIRE, 0, 100); + break; + + case 8: // search + UTIL_EmitAmbientSound( GetSoundSourceIndex(), GetAbsOrigin() + Vector( 0, 0, MyHeight()), "Tentacle.Search", 1.0, SNDLVL_GUNFIRE, 0, 100); + break; + + case 9: // swing + UTIL_EmitAmbientSound( GetSoundSourceIndex(), GetAbsOrigin() + Vector( 0, 0, MyHeight()), "Tentacle.Swing", 1.0, SNDLVL_GUNFIRE, 0, 100); + break; + default: + BaseClass::HandleAnimEvent( pEvent ); + } +} + +void CNPC_Tentacle::HitTouch( CBaseEntity *pOther ) +{ + if (m_flHitTime > gpGlobals->curtime) + return; + + // only look at the ones where the player hit me + if( pOther == NULL || pOther->GetModelIndex() == GetModelIndex() || ( pOther->GetSolidFlags() & FSOLID_TRIGGER ) ) + return; + + //Right now the BoneFollower will always be hit in box 0, and + //will pass that to us. Make *any* touch by the physics objects a kill + //as the ragdoll only covers the top portion of the tentacle. + if ( pOther->m_takedamage ) + { + CTakeDamageInfo info( this, this, m_iHitDmg, DMG_CLUB ); + + Vector vDamageForce = pOther->GetAbsOrigin() - GetAbsOrigin(); + VectorNormalize( vDamageForce ); + + CalculateMeleeDamageForce( &info, vDamageForce, pOther->GetAbsOrigin() ); + pOther->TakeDamage( info ); + + m_flHitTime = gpGlobals->curtime + 0.5; + } +} + +int CNPC_Tentacle::OnTakeDamage( const CTakeDamageInfo &info ) +{ + CTakeDamageInfo i = info; + + //Don't allow the tentacle to die. Instead set health to 1, so we can do our own death and rebirth + if( (int)i.GetDamage() >= m_iHealth ) + { + i.SetDamage( 0.0f ); + m_iHealth = 1; + } + + return BaseClass::OnTakeDamage( i ); +} + +bool CNPC_Tentacle::CreateVPhysics( void ) +{ + BaseClass::CreateVPhysics(); + + IPhysicsObject *pPhysics = VPhysicsGetObject(); + if( pPhysics ) + { + unsigned short flags = pPhysics->GetCallbackFlags(); + + flags |= CALLBACK_GLOBAL_TOUCH; + + pPhysics->SetCallbackFlags( flags ); + } + m_BoneFollowerManager.InitBoneFollowers( this, ARRAYSIZE(pTentacleFollowerBoneNames), pTentacleFollowerBoneNames ); + + return true; +} + +//------------------------------------------------------------------------------ +// +// Schedules +// +//------------------------------------------------------------------------------ + +AI_BEGIN_CUSTOM_NPC( monster_tentacle, CNPC_Tentacle ) + + DECLARE_ACTIVITY( ACT_1010 ) + DECLARE_ACTIVITY( ACT_1011 ) + DECLARE_ACTIVITY( ACT_1012 ) + DECLARE_ACTIVITY( ACT_1013 ) + + DECLARE_ACTIVITY( ACT_1020 ) + DECLARE_ACTIVITY( ACT_1021 ) + DECLARE_ACTIVITY( ACT_1022 ) + DECLARE_ACTIVITY( ACT_1023 ) + + DECLARE_ACTIVITY( ACT_1030 ) + DECLARE_ACTIVITY( ACT_1031 ) + DECLARE_ACTIVITY( ACT_1032 ) + DECLARE_ACTIVITY( ACT_1033 ) + + DECLARE_ACTIVITY( ACT_1040 ) + DECLARE_ACTIVITY( ACT_1041 ) + DECLARE_ACTIVITY( ACT_1042 ) + DECLARE_ACTIVITY( ACT_1043 ) + DECLARE_ACTIVITY( ACT_1044 ) + +AI_END_CUSTOM_NPC() diff --git a/game/server/hl1/hl1_npc_turret.cpp b/game/server/hl1/hl1_npc_turret.cpp new file mode 100644 index 0000000..9084b57 --- /dev/null +++ b/game/server/hl1/hl1_npc_turret.cpp @@ -0,0 +1,1509 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "ai_default.h" +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "ai_hint.h" +#include "ai_memory.h" +#include "ai_route.h" +#include "ai_motor.h" +#include "ai_senses.h" +#include "soundent.h" +#include "game.h" +#include "npcevent.h" +#include "entitylist.h" +#include "activitylist.h" +#include "animation.h" +#include "basecombatweapon.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "Sprite.h" + +#define TURRET_SHOTS 2 +#define TURRET_RANGE (100 * 12) +#define TURRET_SPREAD Vector( 0, 0, 0 ) +#define TURRET_TURNRATE 30 //angles per 0.1 second +#define TURRET_MAXWAIT 15 // seconds turret will stay active w/o a target +#define TURRET_MAXSPIN 5 // seconds turret barrel will spin w/o a target + +typedef enum +{ +// TURRET_ANIM_NONE = 0, + TURRET_ANIM_FIRE = 0, + TURRET_ANIM_SPIN, + TURRET_ANIM_DEPLOY, + TURRET_ANIM_RETIRE, + TURRET_ANIM_DIE, +} TURRET_ANIM; + +#define SF_MONSTER_TURRET_AUTOACTIVATE 32 +#define SF_MONSTER_TURRET_STARTINACTIVE 64 + +#define TURRET_GLOW_SPRITE "sprites/flare3.vmt" + +#define TURRET_ORIENTATION_FLOOR 0 +#define TURRET_ORIENTATION_CEILING 1 + +class CNPC_BaseTurret : public CAI_BaseNPC +{ + DECLARE_CLASS( CNPC_BaseTurret, CAI_BaseNPC ); +public: + void Spawn(void); + virtual void Precache(void); + void EXPORT TurretUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + virtual void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ); + + Class_T Classify( void ); + + // Think functions + void EXPORT ActiveThink(void); + void EXPORT SearchThink(void); + void EXPORT AutoSearchThink(void); + void EXPORT TurretDeath(void); + + virtual void EXPORT SpinDownCall(void) { m_iSpin = 0; } + virtual void EXPORT SpinUpCall(void) { m_iSpin = 1; } + + void EXPORT Deploy(void); + void EXPORT Retire(void); + + void EXPORT Initialize(void); + + virtual void Ping(void); + virtual void EyeOn(void); + virtual void EyeOff(void); + + + void InputActivate( inputdata_t &inputdata ); + void InputDeactivate( inputdata_t &inputdata ); + + void Event_Killed( const CTakeDamageInfo &info ); + virtual bool ShouldFadeOnDeath( void ) { return false; } + bool ShouldGib( const CTakeDamageInfo &info ) { return false; } + + // other functions + void SetTurretAnim(TURRET_ANIM anim); + int MoveTurret(void); + virtual void Shoot(Vector &vecSrc, Vector &vecDirToEnemy) { }; + + float m_flMaxSpin; // Max time to spin the barrel w/o a target + int m_iSpin; + + CSprite *m_pEyeGlow; + int m_eyeBrightness; + + int m_iDeployHeight; + int m_iRetractHeight; + int m_iMinPitch; + + int m_iBaseTurnRate; // angles per second + float m_fTurnRate; // actual turn rate + int m_iOrientation; // 0 = floor, 1 = Ceiling + int m_iOn; + int m_fBeserk; // Sometimes this bitch will just freak out + int m_iAutoStart; // true if the turret auto deploys when a target + // enters its range + + Vector m_vecLastSight; + float m_flLastSight; // Last time we saw a target + float m_flMaxWait; // Max time to seach w/o a target + int m_iSearchSpeed; // Not Used! + + // movement + float m_flStartYaw; + QAngle m_vecCurAngles; + Vector m_vecGoalAngles; + + + float m_flPingTime; // Time until the next ping, used when searching + float m_flSpinUpTime; // Amount of time until the barrel should spin down when searching + + float m_flDamageTime; + + int m_iAmmoType; + + COutputEvent m_OnActivate; + COutputEvent m_OnDeactivate; + + //DEFINE_CUSTOM_AI; + DECLARE_DATADESC(); +}; + +BEGIN_DATADESC( CNPC_BaseTurret ) + + //FIELDS + DEFINE_FIELD( m_flMaxSpin, FIELD_FLOAT ), + DEFINE_FIELD( m_iSpin, FIELD_INTEGER ), + + DEFINE_FIELD( m_pEyeGlow, FIELD_CLASSPTR ), + DEFINE_FIELD( m_eyeBrightness, FIELD_INTEGER ), + DEFINE_FIELD( m_iDeployHeight, FIELD_INTEGER ), + DEFINE_FIELD( m_iRetractHeight, FIELD_INTEGER ), + DEFINE_FIELD( m_iMinPitch, FIELD_INTEGER ), + + DEFINE_FIELD( m_fTurnRate, FIELD_FLOAT ), + DEFINE_FIELD( m_iOn, FIELD_INTEGER ), + DEFINE_FIELD( m_fBeserk, FIELD_INTEGER ), + DEFINE_FIELD( m_iAutoStart, FIELD_INTEGER ), + + DEFINE_FIELD( m_vecLastSight, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_flLastSight, FIELD_TIME ), + + DEFINE_FIELD( m_flStartYaw, FIELD_FLOAT ), + DEFINE_FIELD( m_vecCurAngles, FIELD_VECTOR ), + DEFINE_FIELD( m_vecGoalAngles, FIELD_VECTOR ), + + DEFINE_FIELD( m_flPingTime, FIELD_TIME ), + DEFINE_FIELD( m_flSpinUpTime, FIELD_TIME ), + + DEFINE_FIELD( m_flDamageTime, FIELD_TIME ), + //DEFINE_FIELD( m_iAmmoType, FIELD_INTEGER ), + + //KEYFIELDS + DEFINE_KEYFIELD( m_flMaxWait, FIELD_FLOAT, "maxsleep" ), + DEFINE_KEYFIELD( m_iOrientation, FIELD_INTEGER, "orientation" ), + DEFINE_KEYFIELD( m_iSearchSpeed, FIELD_INTEGER, "searchspeed" ), + DEFINE_KEYFIELD( m_iBaseTurnRate, FIELD_INTEGER, "turnrate" ), + + //Use + DEFINE_USEFUNC( TurretUse ), + + //Thinks + DEFINE_THINKFUNC( ActiveThink ), + DEFINE_THINKFUNC( SearchThink ), + DEFINE_THINKFUNC( AutoSearchThink ), + DEFINE_THINKFUNC( TurretDeath ), + DEFINE_THINKFUNC( SpinDownCall ), + DEFINE_THINKFUNC( SpinUpCall ), + DEFINE_THINKFUNC( Deploy ), + DEFINE_THINKFUNC( Retire ), + DEFINE_THINKFUNC( Initialize ), + + //Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Activate", InputActivate ), + DEFINE_INPUTFUNC( FIELD_VOID, "Deactivate", InputDeactivate ), + + //Outputs + DEFINE_OUTPUT( m_OnActivate, "OnActivate"), + DEFINE_OUTPUT( m_OnDeactivate, "OnDeactivate"), + +END_DATADESC() + + +void CNPC_BaseTurret::Spawn() +{ + Precache( ); + SetNextThink( gpGlobals->curtime + 1 ); + SetMoveType( MOVETYPE_FLY ); + SetSequence( 0 ); + SetCycle( 0 ); + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + m_takedamage = DAMAGE_YES; + AddFlag( FL_AIMTARGET ); + + AddFlag( FL_NPC ); + SetUse( &CNPC_BaseTurret::TurretUse ); + + if (( m_spawnflags & SF_MONSTER_TURRET_AUTOACTIVATE ) + && !( m_spawnflags & SF_MONSTER_TURRET_STARTINACTIVE )) + { + m_iAutoStart = true; + } + + ResetSequenceInfo( ); + + SetBoneController(0, 0); + SetBoneController(1, 0); + + m_flFieldOfView = VIEW_FIELD_FULL; + + m_bloodColor = DONT_BLEED; + m_flDamageTime = 0; + + if ( GetSpawnFlags() & SF_MONSTER_TURRET_STARTINACTIVE ) + { + SetTurretAnim( TURRET_ANIM_RETIRE ); + SetCycle( 0.0f ); + m_flPlaybackRate = 0.0f; + } +} + + +void CNPC_BaseTurret::Precache() +{ + m_iAmmoType = GetAmmoDef()->Index("12mmRound"); + + PrecacheScriptSound( "Turret.Alert" ); + PrecacheScriptSound( "Turret.Die" ); + PrecacheScriptSound( "Turret.Deploy" ); + PrecacheScriptSound( "Turret.Undeploy" ); + PrecacheScriptSound( "Turret.Ping" ); + PrecacheScriptSound( "Turret.Shoot" ); +} + +Class_T CNPC_BaseTurret::Classify( void ) +{ + if (m_iOn || m_iAutoStart) + return CLASS_MACHINE; + return CLASS_NONE; +} + +//========================================================= +// TraceAttack - being attacked +//========================================================= +void CNPC_BaseTurret::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + CTakeDamageInfo ainfo = info; + + if ( ptr->hitgroup == 10 ) + { + // hit armor + if ( m_flDamageTime != gpGlobals->curtime || (random->RandomInt(0,10) < 1) ) + { + g_pEffects->Ricochet( ptr->endpos, ptr->plane.normal ); + m_flDamageTime = gpGlobals->curtime; + } + + ainfo.SetDamage( 0.1 );// don't hurt the monster much, but allow bits_COND_LIGHT_DAMAGE to be generated + } + + if ( m_takedamage == DAMAGE_NO ) + return; + + //DevMsg( 1, "traceattack: %f\n", ainfo.GetDamage() ); + + AddMultiDamage( info, this ); +} + +//========================================================= +// TakeDamage - take damage. +//========================================================= +int CNPC_BaseTurret::OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ) +{ + if ( m_takedamage == DAMAGE_NO ) + return 0; + + float flDamage = inputInfo.GetDamage(); + + if (!m_iOn) + flDamage /= 10.0; + + m_iHealth -= flDamage; + if (m_iHealth <= 0) + { + m_iHealth = 0; + m_takedamage = DAMAGE_NO; + m_flDamageTime = gpGlobals->curtime; + +// ClearBits (pev->flags, FL_MONSTER); // why are they set in the first place??? + + SetUse(NULL); + SetThink(&CNPC_BaseTurret::TurretDeath); + SetNextThink( gpGlobals->curtime + 0.1 ); + + m_OnDeactivate.FireOutput(this, this); + + return 0; + } + + if (m_iHealth <= 10) + { + if (m_iOn) + { + m_fBeserk = 1; + SetThink(&CNPC_BaseTurret::SearchThink); + } + } + return 1; +} + +int CNPC_BaseTurret::OnTakeDamage( const CTakeDamageInfo &info ) +{ + int retVal = 0; + + if (!m_takedamage) + return 0; + + switch( m_lifeState ) + { + case LIFE_ALIVE: + retVal = OnTakeDamage_Alive( info ); + if ( m_iHealth <= 0 ) + { + IPhysicsObject *pPhysics = VPhysicsGetObject(); + if ( pPhysics ) + { + pPhysics->EnableCollisions( false ); + } + + Event_Killed( info ); + Event_Dying(); + } + return retVal; + break; + + case LIFE_DYING: + return OnTakeDamage_Dying( info ); + + default: + case LIFE_DEAD: + return OnTakeDamage_Dead( info ); + } +} + +void CNPC_BaseTurret::SetTurretAnim( TURRET_ANIM anim ) +{ + /* + if (GetSequence() != anim) + { + switch(anim) + { + case TURRET_ANIM_FIRE: + case TURRET_ANIM_SPIN: + if (GetSequence() != TURRET_ANIM_FIRE && GetSequence() != TURRET_ANIM_SPIN) + { + m_flCycle = 0; + } + break; + default: + m_flCycle = 0; + break; + } + + SetSequence( anim ); + + ResetSequenceInfo( ); + + switch(anim) + { + case TURRET_ANIM_RETIRE: + m_flCycle = 255; + m_flPlaybackRate = -1.0; //play the animation backwards + break; + case TURRET_ANIM_DIE: + m_flPlaybackRate = 1.0; + break; + } + //ALERT(at_console, "Turret anim #%d\n", anim); + } + */ + + if (GetSequence() != anim) + { + SetSequence( anim ); + + ResetSequenceInfo( ); + + switch(anim) + { + case TURRET_ANIM_FIRE: + case TURRET_ANIM_SPIN: + if (GetSequence() != TURRET_ANIM_FIRE && GetSequence() != TURRET_ANIM_SPIN) + { + SetCycle( 0 ); + } + break; + case TURRET_ANIM_RETIRE: + SetCycle( 1.0 ); + m_flPlaybackRate = -1.0; //play the animation backwards + break; + case TURRET_ANIM_DIE: + SetCycle( 0.0 ); + m_flPlaybackRate = 1.0; + break; + default: + SetCycle( 0 ); + break; + } + + + } +} + +//========================================================= +// Initialize - set up the turret, initial think +//========================================================= +void CNPC_BaseTurret::Initialize(void) +{ + m_iOn = 0; + m_fBeserk = 0; + m_iSpin = 0; + + SetBoneController( 0, 0 ); + SetBoneController( 1, 0 ); + + if (m_iBaseTurnRate == 0) m_iBaseTurnRate = TURRET_TURNRATE; + if (m_flMaxWait == 0) m_flMaxWait = TURRET_MAXWAIT; + + QAngle angles = GetAbsAngles(); + m_flStartYaw = angles.y; + if (m_iOrientation == TURRET_ORIENTATION_CEILING) + { + angles.x = 180; + angles.y += 180; + if( angles.y > 360 ) + angles.y -= 360; + SetAbsAngles( angles ); + +// pev->idealpitch = 180; //not used? + + Vector view_ofs = GetViewOffset(); + view_ofs.z = -view_ofs.z; + SetViewOffset( view_ofs ); + +// pev->effects |= EF_INVLIGHT; //no need + } + + m_vecGoalAngles.x = 0; + + if (m_iAutoStart) + { + m_flLastSight = gpGlobals->curtime + m_flMaxWait; + SetThink(&CNPC_BaseTurret::AutoSearchThink); + + SetNextThink( gpGlobals->curtime + 0.1 ); + } + else + { + SetThink( &CBaseEntity::SUB_DoNothing ); + } +} + +//========================================================= +// ActiveThink - +//========================================================= +void CNPC_BaseTurret::ActiveThink(void) +{ + int fAttack = 0; + + SetNextThink( gpGlobals->curtime + 0.1 ); + StudioFrameAdvance( ); + + if ( (!m_iOn) || (GetEnemy() == NULL) ) + { + SetEnemy( NULL ); + m_flLastSight = gpGlobals->curtime + m_flMaxWait; + SetThink(&CNPC_BaseTurret::SearchThink); + return; + } + + // if it's dead, look for something new + if ( !GetEnemy()->IsAlive() ) + { + if (!m_flLastSight) + { + m_flLastSight = gpGlobals->curtime + 0.5; // continue-shooting timeout + } + else + { + if (gpGlobals->curtime > m_flLastSight) + { + SetEnemy( NULL ); + m_flLastSight = gpGlobals->curtime + m_flMaxWait; + SetThink(&CNPC_BaseTurret::SearchThink); + return; + } + } + } + + Vector vecMid = EyePosition(); + Vector vecMidEnemy = GetEnemy()->BodyTarget(vecMid, false); + + // Look for our current enemy + int fEnemyVisible = FInViewCone( GetEnemy() ) && FVisible( GetEnemy() ); + + //We want to look at the enemy's eyes so we don't jitter + Vector vecDirToEnemyEyes = vecMidEnemy - vecMid; +// NDebugOverlay::Line( vecMid, vecMidEnemy, 0, 255, 0, false, 1.0 ); + + float flDistToEnemy = vecDirToEnemyEyes.Length(); + + VectorNormalize( vecDirToEnemyEyes ); + + QAngle vecAnglesToEnemy; + VectorAngles( vecDirToEnemyEyes, vecAnglesToEnemy ); + + // Current enmey is not visible. + if (!fEnemyVisible || (flDistToEnemy > TURRET_RANGE)) + { + if (!m_flLastSight) + m_flLastSight = gpGlobals->curtime + 0.5; + else + { + // Should we look for a new target? + if (gpGlobals->curtime > m_flLastSight) + { + SetEnemy( NULL ); + m_flLastSight = gpGlobals->curtime + m_flMaxWait; + SetThink(&CNPC_BaseTurret::SearchThink); + return; + } + } + fEnemyVisible = 0; + } + else + { + m_vecLastSight = vecMidEnemy; + } + + Vector forward; + AngleVectors( m_vecCurAngles, &forward ); + + Vector2D vec2LOS = ( GetEnemy()->GetAbsOrigin() - GetAbsOrigin() ).AsVector2D(); + vec2LOS.NormalizeInPlace(); + + float flDot = vec2LOS.Dot( forward.AsVector2D() ); + + if ( flDot <= 0.866 ) + fAttack = FALSE; + else + fAttack = TRUE; + + //forward + //NDebugOverlay::Line(vecMuzzle, vecMid + ( forward ), 255,0,0, false, 0.1); + //LOS + //NDebugOverlay::Line(vecMuzzle, vecMid + ( vecDirToEnemyEyes * 200 ), 0,0,255, false, 0.1); + + // fire the gun + if (m_iSpin && ((fAttack) || (m_fBeserk))) + { + Shoot(vecMid, forward ); + SetTurretAnim(TURRET_ANIM_FIRE); + } + else + { + SetTurretAnim(TURRET_ANIM_SPIN); + } + + //move the gun + if (m_fBeserk) + { + if (random->RandomInt(0,9) == 0) + { + m_vecGoalAngles.y = random->RandomFloat(0,360); + m_vecGoalAngles.x = random->RandomFloat(0,90) - 90 * m_iOrientation; + + CTakeDamageInfo info; + info.SetAttacker(this); + info.SetInflictor(this); + info.SetDamage( 1 ); + info.SetDamageType( DMG_GENERIC ); + + TakeDamage( info ); // don't beserk forever + return; + } + } + else if (fEnemyVisible) + { + if (vecAnglesToEnemy.y > 360) + vecAnglesToEnemy.y -= 360; + + if (vecAnglesToEnemy.y < 0) + vecAnglesToEnemy.y += 360; + + //ALERT(at_console, "[%.2f]", vec.x); + + if (vecAnglesToEnemy.x < -180) + vecAnglesToEnemy.x += 360; + + if (vecAnglesToEnemy.x > 180) + vecAnglesToEnemy.x -= 360; + + // now all numbers should be in [1...360] + // pin to turret limitations to [-90...14] + + if (m_iOrientation == TURRET_ORIENTATION_FLOOR) + { + if (vecAnglesToEnemy.x > 90) + vecAnglesToEnemy.x = 90; + else if (vecAnglesToEnemy.x < m_iMinPitch) + vecAnglesToEnemy.x = m_iMinPitch; + } + else + { + if (vecAnglesToEnemy.x < -90) + vecAnglesToEnemy.x = -90; + else if (vecAnglesToEnemy.x > -m_iMinPitch) + vecAnglesToEnemy.x = -m_iMinPitch; + } + + //DevMsg( 1, "->[%.2f]\n", vec.x); + + m_vecGoalAngles.y = vecAnglesToEnemy.y; + m_vecGoalAngles.x = vecAnglesToEnemy.x; + + } + + SpinUpCall(); + MoveTurret(); +} + +//========================================================= +// SearchThink +// This search function will sit with the turret deployed and look for a new target. +// After a set amount of time, the barrel will spin down. After m_flMaxWait, the turret will +// retact. +//========================================================= +void CNPC_BaseTurret::SearchThink(void) +{ + // ensure rethink + SetTurretAnim(TURRET_ANIM_SPIN); + StudioFrameAdvance( ); + SetNextThink( gpGlobals->curtime + 0.1 ); + + if (m_flSpinUpTime == 0 && m_flMaxSpin) + m_flSpinUpTime = gpGlobals->curtime + m_flMaxSpin; + + Ping( ); + + CBaseEntity *pEnemy = GetEnemy(); + + // If we have a target and we're still healthy + if (pEnemy != NULL) + { + if (!pEnemy->IsAlive() ) + pEnemy = NULL;// Dead enemy forces a search for new one + } + + + // Acquire Target + if (pEnemy == NULL) + { + GetSenses()->Look(TURRET_RANGE); + pEnemy = BestEnemy(); + + if ( pEnemy && !FVisible( pEnemy ) ) + pEnemy = NULL; + } + + // If we've found a target, spin up the barrel and start to attack + if (pEnemy != NULL) + { + m_flLastSight = 0; + m_flSpinUpTime = 0; + SetThink(&CNPC_BaseTurret::ActiveThink); + } + else + { + // Are we out of time, do we need to retract? + if (gpGlobals->curtime > m_flLastSight) + { + //Before we retrace, make sure that we are spun down. + m_flLastSight = 0; + m_flSpinUpTime = 0; + SetThink(&CNPC_BaseTurret::Retire); + } + // should we stop the spin? + else if ((m_flSpinUpTime) && (gpGlobals->curtime > m_flSpinUpTime)) + { + SpinDownCall(); + } + + // generic hunt for new victims + m_vecGoalAngles.y = (m_vecGoalAngles.y + 0.1 * m_fTurnRate); + if (m_vecGoalAngles.y >= 360) + m_vecGoalAngles.y -= 360; + MoveTurret(); + } + + SetEnemy( pEnemy ); +} + +//========================================================= +// AutoSearchThink - +//========================================================= +void CNPC_BaseTurret::AutoSearchThink(void) +{ + // ensure rethink + StudioFrameAdvance( ); + SetNextThink( gpGlobals->curtime + 0.3 ); + + // If we have a target and we're still healthy + + CBaseEntity *pEnemy = GetEnemy(); + + if (pEnemy != NULL) + { + if (!pEnemy->IsAlive() ) + { + pEnemy = NULL; + } + } + + // Acquire Target + + if (pEnemy == NULL) + { + GetSenses()->Look( TURRET_RANGE ); + pEnemy = BestEnemy(); + + if ( pEnemy && !FVisible( pEnemy ) ) + pEnemy = NULL; + } + + if (pEnemy != NULL) + { + SetThink(&CNPC_BaseTurret::Deploy); + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Turret.Alert" ); + } + + SetEnemy( pEnemy ); +} + +extern short g_sModelIndexSmoke; + +//========================================================= +// TurretDeath - I die as I have lived, beyond my means +//========================================================= +void CNPC_BaseTurret::TurretDeath(void) +{ + StudioFrameAdvance( ); + SetNextThink( gpGlobals->curtime + 0.1 ); + + if (m_lifeState != LIFE_DEAD) + { + m_lifeState = LIFE_DEAD; + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Turret.Die" ); + + StopSound( entindex(), "Turret.Spinup" ); + + if (m_iOrientation == TURRET_ORIENTATION_FLOOR) + m_vecGoalAngles.x = -14; + else + m_vecGoalAngles.x = 90;//-90; + + SetTurretAnim(TURRET_ANIM_DIE); + + EyeOn( ); + } + + EyeOff( ); + + if (m_flDamageTime + random->RandomFloat( 0, 2 ) > gpGlobals->curtime) + { + // lots of smoke + Vector pos; + CollisionProp()->RandomPointInBounds( vec3_origin, Vector( 1, 1, 1 ), &pos ); + pos.z = CollisionProp()->GetCollisionOrigin().z; + + CBroadcastRecipientFilter filter; + te->Smoke( filter, 0.0, &pos, + g_sModelIndexSmoke, + 2.5, + 10 ); + } + + if (m_flDamageTime + random->RandomFloat( 0, 5 ) > gpGlobals->curtime) + { + Vector vecSrc; + CollisionProp()->RandomPointInBounds( vec3_origin, Vector( 1, 1, 1 ), &vecSrc ); + g_pEffects->Sparks( vecSrc ); + } + + if (IsSequenceFinished() && !MoveTurret() && m_flDamageTime + 5 < gpGlobals->curtime) + { + m_flPlaybackRate = 0; + SetThink( NULL ); + } +} + +//========================================================= +// Deploy - go active +//========================================================= +void CNPC_BaseTurret::Deploy(void) +{ + SetNextThink( gpGlobals->curtime + 0.1 ); + StudioFrameAdvance( ); + + if (GetSequence() != TURRET_ANIM_DEPLOY) + { + m_iOn = 1; + SetTurretAnim(TURRET_ANIM_DEPLOY); + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Turret.Deploy" ); + m_OnActivate.FireOutput(this, this); + } + + if (IsSequenceFinished()) + { + Vector curmins, curmaxs; + curmins = WorldAlignMins(); + curmaxs = WorldAlignMaxs(); + + curmaxs.z = m_iDeployHeight; + curmins.z = -m_iDeployHeight; + + SetCollisionBounds( curmins, curmaxs ); + + m_vecCurAngles.x = 0; + + QAngle angles = GetAbsAngles(); + + if (m_iOrientation == TURRET_ORIENTATION_CEILING) + { + m_vecCurAngles.y = UTIL_AngleMod( angles.y + 180 ); + } + else + { + m_vecCurAngles.y = UTIL_AngleMod( angles.y ); + } + + SetTurretAnim(TURRET_ANIM_SPIN); + m_flPlaybackRate = 0; + SetThink(&CNPC_BaseTurret::SearchThink); + } + + m_flLastSight = gpGlobals->curtime + m_flMaxWait; +} + +//========================================================= +// Retire - stop being active +//========================================================= +void CNPC_BaseTurret::Retire(void) +{ + // make the turret level + m_vecGoalAngles.x = 0; + m_vecGoalAngles.y = m_flStartYaw; + + SetNextThink( gpGlobals->curtime + 0.1 ); + + StudioFrameAdvance( ); + + EyeOff( ); + + if (!MoveTurret()) + { + if (m_iSpin) + { + SpinDownCall(); + } + else if (GetSequence() != TURRET_ANIM_RETIRE) + { + SetTurretAnim(TURRET_ANIM_RETIRE); + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Turret.Undeploy" ); + m_OnDeactivate.FireOutput(this, this); + } + //else if (IsSequenceFinished()) + else if( GetSequence() == TURRET_ANIM_RETIRE && GetCycle() <= 0.0 ) + { + m_iOn = 0; + m_flLastSight = 0; + //SetTurretAnim(TURRET_ANIM_NONE); + + Vector curmins, curmaxs; + curmins = WorldAlignMins(); + curmaxs = WorldAlignMaxs(); + + curmaxs.z = m_iRetractHeight; + curmins.z = -m_iRetractHeight; + + SetCollisionBounds( curmins, curmaxs ); + if (m_iAutoStart) + { + SetThink(&CNPC_BaseTurret::AutoSearchThink); + SetNextThink( gpGlobals->curtime + 0.1 ); + } + else + { + SetThink( &CBaseEntity::SUB_DoNothing ); + } + } + } + else + { + SetTurretAnim(TURRET_ANIM_SPIN); + } +} + +//========================================================= +// Ping - make the pinging noise every second while searching +//========================================================= +void CNPC_BaseTurret::Ping(void) +{ + if (m_flPingTime == 0) + m_flPingTime = gpGlobals->curtime + 1; + else if (m_flPingTime <= gpGlobals->curtime) + { + m_flPingTime = gpGlobals->curtime + 1; + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Turret.Ping" ); + + EyeOn( ); + } + else if (m_eyeBrightness > 0) + { + EyeOff( ); + } +} + +//========================================================= +// MoveTurret - handle turret rotation +// returns 1 if the turret moved. +//========================================================= +int CNPC_BaseTurret::MoveTurret(void) +{ + int bMoved = 0; + + if (m_vecCurAngles.x != m_vecGoalAngles.x) + { + float flDir = m_vecGoalAngles.x > m_vecCurAngles.x ? 1 : -1 ; + + m_vecCurAngles.x += 0.1 * m_fTurnRate * flDir; + + // if we started below the goal, and now we're past, peg to goal + if (flDir == 1) + { + if (m_vecCurAngles.x > m_vecGoalAngles.x) + m_vecCurAngles.x = m_vecGoalAngles.x; + } + else + { + if (m_vecCurAngles.x < m_vecGoalAngles.x) + m_vecCurAngles.x = m_vecGoalAngles.x; + } + + if (m_iOrientation == TURRET_ORIENTATION_FLOOR) + SetBoneController(1, m_vecCurAngles.x); + else + SetBoneController(1, -m_vecCurAngles.x); + + bMoved = 1; + } + + if (m_vecCurAngles.y != m_vecGoalAngles.y) + { + float flDir = m_vecGoalAngles.y > m_vecCurAngles.y ? 1 : -1 ; + float flDist = fabs(m_vecGoalAngles.y - m_vecCurAngles.y); + + if (flDist > 180) + { + flDist = 360 - flDist; + flDir = -flDir; + } + if (flDist > 30) + { + if (m_fTurnRate < m_iBaseTurnRate * 10) + { + m_fTurnRate += m_iBaseTurnRate; + } + } + else if (m_fTurnRate > 45) + { + m_fTurnRate -= m_iBaseTurnRate; + } + else + { + m_fTurnRate += m_iBaseTurnRate; + } + + m_vecCurAngles.y += 0.1 * m_fTurnRate * flDir; + + if (m_vecCurAngles.y < 0) + m_vecCurAngles.y += 360; + else if (m_vecCurAngles.y >= 360) + m_vecCurAngles.y -= 360; + + if (flDist < (0.05 * m_iBaseTurnRate)) + m_vecCurAngles.y = m_vecGoalAngles.y; + + QAngle angles = GetAbsAngles(); + + //ALERT(at_console, "%.2f -> %.2f\n", m_vecCurAngles.y, y); + + if (m_iOrientation == TURRET_ORIENTATION_FLOOR) + SetBoneController(0, m_vecCurAngles.y - angles.y ); + else + SetBoneController(0, angles.y - 180 - m_vecCurAngles.y ); + bMoved = 1; + } + + if (!bMoved) + m_fTurnRate = m_iBaseTurnRate; + + //DevMsg(1, "(%.2f, %.2f)->(%.2f, %.2f)\n", m_vecCurAngles.x, + // m_vecCurAngles.y, m_vecGoalAngles.x, m_vecGoalAngles.y); + + return bMoved; +} + +void CNPC_BaseTurret::TurretUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( !ShouldToggle( useType, m_iOn ) ) + return; + + if (m_iOn) + { + SetEnemy( NULL ); + SetNextThink( gpGlobals->curtime + 0.1 ); + m_iAutoStart = FALSE;// switching off a turret disables autostart + //!!!! this should spin down first!!BUGBUG + SetThink(&CNPC_BaseTurret::Retire); + } + else + { + SetNextThink( gpGlobals->curtime + 0.1 ); // turn on delay + + // if the turret is flagged as an autoactivate turret, re-enable it's ability open self. + if ( m_spawnflags & SF_MONSTER_TURRET_AUTOACTIVATE ) + { + m_iAutoStart = TRUE; + } + + SetThink(&CNPC_BaseTurret::Deploy); + } +} + +void CNPC_BaseTurret::InputDeactivate( inputdata_t &inputdata ) +{ + if( m_iOn && m_lifeState == LIFE_ALIVE ) + { + SetEnemy( NULL ); + SetNextThink( gpGlobals->curtime + 0.1 ); + m_iAutoStart = FALSE;// switching off a turret disables autostart + //!!!! this should spin down first!!BUGBUG + SetThink(&CNPC_BaseTurret::Retire); + } +} + +void CNPC_BaseTurret::InputActivate( inputdata_t &inputdata ) +{ + if( !m_iOn && m_lifeState == LIFE_ALIVE ) + { + SetNextThink( gpGlobals->curtime + 0.1 ); // turn on delay + + // if the turret is flagged as an autoactivate turret, re-enable it's ability open self. + if ( m_spawnflags & SF_MONSTER_TURRET_AUTOACTIVATE ) + { + m_iAutoStart = TRUE; + } + + SetThink(&CNPC_BaseTurret::Deploy); + } +} + + +//========================================================= +// EyeOn - turn on light on the turret +//========================================================= +void CNPC_BaseTurret::EyeOn(void) +{ + if (m_pEyeGlow) + { + if (m_eyeBrightness != 255) + { + m_eyeBrightness = 255; + } + m_pEyeGlow->SetBrightness( m_eyeBrightness ); + } +} + +//========================================================= +// EyeOn - turn off light on the turret +//========================================================= +void CNPC_BaseTurret::EyeOff(void) +{ + if (m_pEyeGlow) + { + if (m_eyeBrightness > 0) + { + m_eyeBrightness = MAX( 0, m_eyeBrightness - 30 ); + m_pEyeGlow->SetBrightness( m_eyeBrightness ); + } + } +} + +void CNPC_BaseTurret::Event_Killed( const CTakeDamageInfo &info ) +{ + BaseClass::Event_Killed( info ); + + SetMoveType( MOVETYPE_FLY ); +} + +//=== + +class CNPC_MiniTurret : public CNPC_BaseTurret +{ + DECLARE_CLASS( CNPC_MiniTurret, CNPC_BaseTurret ); + +public: + void Spawn( ); + void Precache(void); + + // other functions + void Shoot(Vector &vecSrc, Vector &vecDirToEnemy); +}; + +class CNPC_Turret : public CNPC_BaseTurret +{ + DECLARE_CLASS( CNPC_Turret, CNPC_BaseTurret ); + +public: + DECLARE_DATADESC(); + + void Spawn(void); + void Precache(void); + + // Think functions + void SpinUpCall(void); + void SpinDownCall(void); + + // other functions + void Shoot(Vector &vecSrc, Vector &vecDirToEnemy); + +private: + int m_iStartSpin; +}; + +BEGIN_DATADESC(CNPC_Turret) + DEFINE_FIELD( m_iStartSpin, FIELD_INTEGER ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( monster_turret, CNPC_Turret ); +LINK_ENTITY_TO_CLASS( monster_miniturret, CNPC_MiniTurret ); + +ConVar sk_turret_health ( "sk_turret_health","50"); +ConVar sk_miniturret_health ( "sk_miniturret_health","40"); +ConVar sk_sentry_health ( "sk_sentry_health","40"); + +void CNPC_Turret::Spawn() +{ + Precache( ); + SetModel( "models/turret.mdl" ); + m_iHealth = sk_turret_health.GetFloat(); + m_HackedGunPos = Vector( 0, 0, 12.75 ); + m_flMaxSpin = TURRET_MAXSPIN; + + Vector view_ofs( 0, 0, 12.75 ); + SetViewOffset( view_ofs ); + + CNPC_BaseTurret::Spawn( ); + + m_iRetractHeight = 16; + m_iDeployHeight = 32; + m_iMinPitch = -90; + UTIL_SetSize(this, Vector(-32, -32, -m_iRetractHeight), Vector(32, 32, m_iRetractHeight)); + + SetThink(&CNPC_BaseTurret::Initialize); + + m_pEyeGlow = CSprite::SpriteCreate( TURRET_GLOW_SPRITE, GetAbsOrigin(), FALSE ); + m_pEyeGlow->SetTransparency( kRenderGlow, 255, 0, 0, 0, kRenderFxNoDissipation ); + m_pEyeGlow->SetAttachment( this, 2 ); + + m_eyeBrightness = 0; + + SetNextThink( gpGlobals->curtime + 0.3 ); +} + +void CNPC_Turret::Precache() +{ + CNPC_BaseTurret::Precache( ); + PrecacheModel ("models/turret.mdl"); + PrecacheModel (TURRET_GLOW_SPRITE); + + PrecacheModel( "sprites/xspark4.vmt" ); + + PrecacheScriptSound( "Turret.Shoot" ); + + PrecacheScriptSound( "Turret.SpinUpCall" ); + PrecacheScriptSound( "Turret.Spinup" ); + PrecacheScriptSound( "Turret.SpinDownCall" ); + //precache sounds +} + +void CNPC_Turret::Shoot(Vector &vecSrc, Vector &vecDirToEnemy) +{ + CPASAttenuationFilter filter( this ); + + FireBullets( 1, vecSrc, vecDirToEnemy, TURRET_SPREAD, TURRET_RANGE, m_iAmmoType, 1 ); + + EmitSound( filter, entindex(), "Turret.Shoot" ); + + DoMuzzleFlash(); +} + +void CNPC_Turret::SpinUpCall(void) +{ + StudioFrameAdvance( ); + SetNextThink( gpGlobals->curtime + 0.1 ); + + // Are we already spun up? If not start the two stage process. + if (!m_iSpin) + { + SetTurretAnim(TURRET_ANIM_SPIN); + // for the first pass, spin up the the barrel + if (!m_iStartSpin) + { + SetNextThink( gpGlobals->curtime + 1.0 ); //spinup delay + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Turret.SpinUpCall" ); + m_iStartSpin = 1; + m_flPlaybackRate = 0.1; + } + // after the barrel is spun up, turn on the hum + else if (m_flPlaybackRate >= 1.0) + { + SetNextThink( gpGlobals->curtime + 0.1 );// retarget delay + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Turret.Spinup" ); + SetThink(&CNPC_BaseTurret::ActiveThink); + m_iStartSpin = 0; + m_iSpin = 1; + } + else + { + m_flPlaybackRate += 0.075; + } + } + + if (m_iSpin) + { + SetThink(&CNPC_BaseTurret::ActiveThink); + } +} + +void CNPC_Turret::SpinDownCall(void) +{ + if (m_iSpin) + { + CPASAttenuationFilter filter( this ); + + SetTurretAnim(TURRET_ANIM_SPIN); + + if ( m_flPlaybackRate == 1.0) + { + StopSound( entindex(), "Turret.Spinup" ); + EmitSound( filter, entindex(), "Turret.SpinDownCall" ); + } + m_flPlaybackRate -= 0.02; + if (m_flPlaybackRate <= 0) + { + m_flPlaybackRate = 0; + m_iSpin = 0; + } + } +} + +void CNPC_MiniTurret::Spawn() +{ + Precache( ); + + SetModel( "models/miniturret.mdl" ); + m_iHealth = sk_miniturret_health.GetFloat(); + m_HackedGunPos = Vector( 0, 0, 12.75 ); + m_flMaxSpin = 0; + + Vector view_ofs( 0, 0, 12.75 ); + SetViewOffset( view_ofs ); + + CNPC_BaseTurret::Spawn( ); + m_iRetractHeight = 16; + m_iDeployHeight = 32; + m_iMinPitch = -90; + UTIL_SetSize(this, Vector(-16, -16, -m_iRetractHeight), Vector(16, 16, m_iRetractHeight)); + + SetThink(&CNPC_MiniTurret::Initialize); + SetNextThink(gpGlobals->curtime + 0.3); + + if (( m_spawnflags & SF_MONSTER_TURRET_AUTOACTIVATE ) && !( m_spawnflags & SF_MONSTER_TURRET_STARTINACTIVE )) + { + m_iAutoStart = true; + } +} + + +void CNPC_MiniTurret::Precache() +{ + CNPC_BaseTurret::Precache( ); + + m_iAmmoType = GetAmmoDef()->Index("9mmRound"); + + PrecacheScriptSound( "Turret.Shoot" ); + + PrecacheModel ("models/miniturret.mdl"); +} + +void CNPC_MiniTurret::Shoot(Vector &vecSrc, Vector &vecDirToEnemy) +{ + FireBullets( 1, vecSrc, vecDirToEnemy, TURRET_SPREAD, TURRET_RANGE, m_iAmmoType, 1 ); + + CPASAttenuationFilter filter( this ); + + EmitSound( filter, entindex(), "Turret.Shoot" ); + + DoMuzzleFlash(); +} + +//========================================================= +// Sentry gun - smallest turret, placed near grunt entrenchments +//========================================================= +class CNPC_Sentry : public CNPC_BaseTurret +{ + DECLARE_CLASS( CNPC_Sentry, CNPC_BaseTurret ); + +public: + void Spawn( ); + void Precache(void); + + // other functions + void Shoot(Vector &vecSrc, Vector &vecDirToEnemy); + int OnTakeDamage_Alive(const CTakeDamageInfo &info); + void Event_Killed( const CTakeDamageInfo &info ); + void SentryTouch( CBaseEntity *pOther ); + + DECLARE_DATADESC(); + +private: + bool m_bStartedDeploy; //set to true when the turret begins its deploy +}; + +BEGIN_DATADESC( CNPC_Sentry ) + DEFINE_ENTITYFUNC( SentryTouch ), + DEFINE_FIELD( m_bStartedDeploy, FIELD_BOOLEAN ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( monster_sentry, CNPC_Sentry ); + +void CNPC_Sentry::Precache() +{ + BaseClass::Precache( ); + + m_iAmmoType = GetAmmoDef()->Index("9mmRound"); + + PrecacheScriptSound( "Sentry.Shoot" ); + PrecacheScriptSound( "Sentry.Die" ); + + PrecacheModel ("models/sentry.mdl"); +} + +void CNPC_Sentry::Spawn() +{ + Precache( ); + SetModel( "models/sentry.mdl" ); + m_iHealth = sk_sentry_health.GetFloat(); + m_HackedGunPos = Vector( 0, 0, 48 ); + + + SetViewOffset( Vector(0,0,48) ); + + m_flMaxWait = 1E6; + m_flMaxSpin = 1E6; + + BaseClass::Spawn(); + + SetSequence( TURRET_ANIM_RETIRE ); + SetCycle( 0.0 ); + m_flPlaybackRate = 0.0; + + m_iRetractHeight = 64; + m_iDeployHeight = 64; + m_iMinPitch = -60; + + UTIL_SetSize(this, Vector(-16, -16, -m_iRetractHeight), Vector(16, 16, m_iRetractHeight)); + + SetTouch(&CNPC_Sentry::SentryTouch); + SetThink(&CNPC_Sentry::Initialize); + + SetNextThink(gpGlobals->curtime + 0.3); + + m_bStartedDeploy = false; +} + +void CNPC_Sentry::Shoot(Vector &vecSrc, Vector &vecDirToEnemy) +{ + FireBullets( 1, vecSrc, vecDirToEnemy, TURRET_SPREAD, TURRET_RANGE, m_iAmmoType, 1 ); + + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Sentry.Shoot" ); + + DoMuzzleFlash(); +} + +int CNPC_Sentry::OnTakeDamage_Alive(const CTakeDamageInfo &info) +{ + if ( m_takedamage == DAMAGE_NO ) + return 0; + + if (!m_iOn && !m_bStartedDeploy) + { + m_bStartedDeploy = true; + SetThink( &CNPC_Sentry::Deploy ); + SetUse( NULL ); + SetNextThink( gpGlobals->curtime + 0.1 ); + } + + m_iHealth -= info.GetDamage(); + if (m_iHealth <= 0) + { + m_iHealth = 0; + m_takedamage = DAMAGE_NO; + m_flDamageTime = gpGlobals->curtime; + + SetUse(NULL); + SetThink(&CNPC_BaseTurret::TurretDeath); //should be SentryDeath ? + SetNextThink( gpGlobals->curtime + 0.1 ); + + m_OnDeactivate.FireOutput(this, this); + + return 0; + } + + return 1; +} + +void CNPC_Sentry::Event_Killed( const CTakeDamageInfo &info ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Sentry.Die" ); + + StopSound( entindex(), "Turret.Spinup" ); + + AddSolidFlags( FSOLID_NOT_STANDABLE ); + + Vector vecSrc; + QAngle vecAng; + GetAttachment( 2, vecSrc, vecAng ); + + te->Smoke( filter, 0.0, &vecSrc, + g_sModelIndexSmoke, + 2.5, + 10 ); + + g_pEffects->Sparks( vecSrc ); + + BaseClass::Event_Killed( info ); +} + + +void CNPC_Sentry::SentryTouch( CBaseEntity *pOther ) +{ + //trigger the sentry to turn on if a monster or player touches it + if ( pOther && (pOther->IsPlayer() || FBitSet ( pOther->GetFlags(), FL_NPC )) ) + { + CTakeDamageInfo info; + info.SetAttacker( pOther ); + info.SetInflictor( pOther ); + info.SetDamage( 0 ); + + TakeDamage(info); + } +} + diff --git a/game/server/hl1/hl1_npc_vortigaunt.cpp b/game/server/hl1/hl1_npc_vortigaunt.cpp new file mode 100644 index 0000000..a6d6283 --- /dev/null +++ b/game/server/hl1/hl1_npc_vortigaunt.cpp @@ -0,0 +1,743 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Alien slave monster +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "beam_shared.h" +#include "game.h" +#include "ai_default.h" +#include "ai_schedule.h" +#include "ai_hull.h" +#include "ai_route.h" +#include "ai_squad.h" +#include "npcevent.h" +#include "gib.h" +//#include "AI_Interactions.h" +#include "ndebugoverlay.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "hl1_npc_vortigaunt.h" +#include "soundent.h" +#include "player.h" +#include "IEffects.h" +#include "basecombatweapon.h" +#include "SoundEmitterSystem/isoundemittersystembase.h" + +//========================================================= +// Monster's Anim Events Go Here +//========================================================= +#define ISLAVE_AE_CLAW ( 1 ) +#define ISLAVE_AE_CLAWRAKE ( 2 ) +#define ISLAVE_AE_ZAP_POWERUP ( 3 ) +#define ISLAVE_AE_ZAP_SHOOT ( 4 ) +#define ISLAVE_AE_ZAP_DONE ( 5 ) + + +ConVar sk_islave_health( "sk_islave_health","50"); +ConVar sk_islave_dmg_claw( "sk_islave_dmg_claw","8"); +ConVar sk_islave_dmg_clawrake( "sk_islave_dmg_clawrake","25"); +ConVar sk_islave_dmg_zap( "sk_islave_dmg_zap","15"); + +LINK_ENTITY_TO_CLASS( monster_alien_slave, CNPC_Vortigaunt ); + +BEGIN_DATADESC( CNPC_Vortigaunt ) + DEFINE_FIELD( m_iBravery, FIELD_INTEGER ), + DEFINE_ARRAY( m_pBeam, FIELD_CLASSPTR, VORTIGAUNT_MAX_BEAMS ), + DEFINE_FIELD( m_iBeams, FIELD_INTEGER ), + DEFINE_FIELD( m_flNextAttack, FIELD_TIME ), + DEFINE_FIELD( m_iVoicePitch, FIELD_INTEGER ), + DEFINE_FIELD( m_hDead, FIELD_EHANDLE ), +END_DATADESC() + +enum +{ + SCHED_VORTIGAUNT_ATTACK = LAST_SHARED_SCHEDULE, +}; + +#define VORTIGAUNT_IGNORE_PLAYER 64 + +//========================================================= +// Spawn +//========================================================= +void CNPC_Vortigaunt::Spawn() +{ + Precache( ); + + SetModel( "models/islave.mdl" ); + + SetRenderColor( 255, 255, 255, 255 ); + + SetHullType(HULL_HUMAN); + SetHullSizeNormal(); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + m_bloodColor = BLOOD_COLOR_GREEN; + ClearEffects(); + m_iHealth = sk_islave_health.GetFloat(); + //pev->view_ofs = VEC_VIEW;// position of the eyes relative to monster's origin. + m_flFieldOfView = VIEW_FIELD_WIDE; + m_NPCState = NPC_STATE_NONE; + + m_iVoicePitch = random->RandomInt( 85, 110 ); + + CapabilitiesClear(); + CapabilitiesAdd( bits_CAP_MOVE_GROUND ); + CapabilitiesAdd( bits_CAP_SQUAD ); + + CapabilitiesAdd( bits_CAP_TURN_HEAD | bits_CAP_DOORS_GROUP ); + + CapabilitiesAdd ( bits_CAP_INNATE_RANGE_ATTACK1 ); + CapabilitiesAdd ( bits_CAP_INNATE_MELEE_ATTACK1 ); + + m_iBravery = 0; + + NPCInit(); + + BaseClass::Spawn(); +} + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CNPC_Vortigaunt::Precache() +{ + BaseClass::Precache(); + + PrecacheModel("models/islave.mdl"); + PrecacheModel("sprites/lgtning.vmt"); + + PrecacheScriptSound( "Vortigaunt.Pain" ); + PrecacheScriptSound( "Vortigaunt.Die" ); + PrecacheScriptSound( "Vortigaunt.AttackHit" ); + PrecacheScriptSound( "Vortigaunt.AttackMiss" ); + PrecacheScriptSound( "Vortigaunt.ZapPowerup" ); + PrecacheScriptSound( "Vortigaunt.ZapShoot" ); +} + +Disposition_t CNPC_Vortigaunt::IRelationType ( CBaseEntity *pTarget ) +{ + if ( (pTarget->IsPlayer()) ) + { + if ( (GetSpawnFlags() & VORTIGAUNT_IGNORE_PLAYER ) && !HasMemory( bits_MEMORY_PROVOKED ) ) + return D_NU; + } + + return BaseClass::IRelationType( pTarget ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +Class_T CNPC_Vortigaunt::Classify ( void ) +{ + return CLASS_ALIEN_MILITARY; +} + +void CNPC_Vortigaunt::CallForHelp( char *szClassname, float flDist, CBaseEntity * pEnemy, Vector &vecLocation ) +{ + // ALERT( at_aiconsole, "help " ); + + // skip ones not on my netname + if ( !m_pSquad ) + return; + + AISquadIter_t iter; + for (CAI_BaseNPC *pSquadMember = m_pSquad->GetFirstMember( &iter ); pSquadMember; pSquadMember = m_pSquad->GetNextMember( &iter ) ) + { + float d = ( GetAbsOrigin() - pSquadMember->GetAbsOrigin() ).Length(); + + if ( d < flDist ) + { + pSquadMember->Remember( bits_MEMORY_PROVOKED ); + pSquadMember->UpdateEnemyMemory( pEnemy, vecLocation ); + } + } +} + + +//========================================================= +// ALertSound - scream +//========================================================= +void CNPC_Vortigaunt::AlertSound( void ) +{ + if ( GetEnemy() != NULL ) + { + SENTENCEG_PlayRndSz( edict(), "SLV_ALERT", 0.85, SNDLVL_NORM, 0, m_iVoicePitch ); + + Vector vecTmp = GetEnemy()->GetAbsOrigin(); + CallForHelp( "monster_alien_slave", 512, GetEnemy(), vecTmp ); + } +} + +//========================================================= +// IdleSound +//========================================================= +void CNPC_Vortigaunt::IdleSound( void ) +{ + if ( random->RandomInt( 0, 2 ) == 0) + SENTENCEG_PlayRndSz( edict(), "SLV_IDLE", 0.85, SNDLVL_NORM, 0, m_iVoicePitch); +} + +//========================================================= +// PainSound +//========================================================= +void CNPC_Vortigaunt::PainSound( const CTakeDamageInfo &info ) +{ + if ( random->RandomInt( 0, 2 ) == 0) + { + CPASAttenuationFilter filter( this ); + + CSoundParameters params; + if ( GetParametersForSound( "Vortigaunt.Pain", params, NULL ) ) + { + EmitSound_t ep( params ); + params.pitch = m_iVoicePitch; + + EmitSound( filter, entindex(), ep ); + } + } +} + +//========================================================= +// DieSound +//========================================================= + +void CNPC_Vortigaunt::DeathSound( const CTakeDamageInfo &info ) +{ + CPASAttenuationFilter filter( this ); + CSoundParameters params; + if ( GetParametersForSound( "Vortigaunt.Die", params, NULL ) ) + { + EmitSound_t ep( params ); + params.pitch = m_iVoicePitch; + + EmitSound( filter, entindex(), ep ); + } +} + +int CNPC_Vortigaunt::GetSoundInterests ( void ) +{ + return SOUND_WORLD | + SOUND_COMBAT | + SOUND_DANGER | + SOUND_PLAYER; +} + +void CNPC_Vortigaunt::Event_Killed( const CTakeDamageInfo &info ) +{ + ClearBeams( ); + BaseClass::Event_Killed( info ); +} + +//========================================================= +// SetYawSpeed - allows each sequence to have a different +// turn rate associated with it. +//========================================================= +float CNPC_Vortigaunt::MaxYawSpeed ( void ) +{ + float flYS; + + switch ( GetActivity() ) + { + case ACT_WALK: + flYS = 50; + break; + case ACT_RUN: + flYS = 70; + break; + case ACT_IDLE: + flYS = 50; + break; + default: + flYS = 90; + break; + } + + return flYS; +} + +//========================================================= +// HandleAnimEvent - catches the monster-specific messages +// that occur when tagged animation frames are played. +// +// Returns number of events handled, 0 if none. +//========================================================= +void CNPC_Vortigaunt::HandleAnimEvent( animevent_t *pEvent ) +{ + // ALERT( at_console, "event %d : %f\n", pEvent->event, pev->frame ); + switch( pEvent->event ) + { + case ISLAVE_AE_CLAW: + { + // SOUND HERE! + CBaseEntity *pHurt = CheckTraceHullAttack( 40, Vector(-10,-10,-10), Vector(10,10,10), sk_islave_dmg_claw.GetFloat(), DMG_SLASH ); + CPASAttenuationFilter filter( this ); + if ( pHurt ) + { + if ( pHurt->GetFlags() & ( FL_NPC | FL_CLIENT ) ) + pHurt->ViewPunch( QAngle( 5, 0, -18 ) ); + + // Play a random attack hit sound + CSoundParameters params; + if ( GetParametersForSound( "Vortigaunt.AttackHit", params, NULL ) ) + { + EmitSound_t ep( params ); + params.pitch = m_iVoicePitch; + + EmitSound( filter, entindex(), ep ); + } + } + else + { + // Play a random attack miss sound + CSoundParameters params; + if ( GetParametersForSound( "Vortigaunt.AttackMiss", params, NULL ) ) + { + EmitSound_t ep( params ); + params.pitch = m_iVoicePitch; + + EmitSound( filter, entindex(), ep ); + } + } + } + break; + + case ISLAVE_AE_CLAWRAKE: + { + CBaseEntity *pHurt = CheckTraceHullAttack( 40, Vector(-10,-10,-10), Vector(10,10,10), sk_islave_dmg_clawrake.GetFloat(), DMG_SLASH ); + CPASAttenuationFilter filter2( this ); + if ( pHurt ) + { + if ( pHurt->GetFlags() & ( FL_NPC | FL_CLIENT ) ) + pHurt->ViewPunch( QAngle( 5, 0, 18 ) ); + + CSoundParameters params; + if ( GetParametersForSound( "Vortigaunt.AttackHit", params, NULL ) ) + { + EmitSound_t ep( params ); + params.pitch = m_iVoicePitch; + + EmitSound( filter2, entindex(), ep ); + } + } + else + { + CSoundParameters params; + if ( GetParametersForSound( "Vortigaunt.AttackMiss", params, NULL ) ) + { + EmitSound_t ep( params ); + params.pitch = m_iVoicePitch; + + EmitSound( filter2, entindex(), ep ); + } + } + } + break; + + case ISLAVE_AE_ZAP_POWERUP: + { + // speed up attack when on hard + if ( g_iSkillLevel == SKILL_HARD ) + m_flPlaybackRate = 1.5; + + Vector v_forward; + GetVectors( &v_forward, NULL, NULL ); + + CBroadcastRecipientFilter filter; + te->DynamicLight( filter, 0.0, &GetAbsOrigin(), 125, 200, 100, 2, 120, 0.2 / m_flPlaybackRate, 0 ); + + if ( m_hDead != NULL ) + { + WackBeam( -1, m_hDead ); + WackBeam( 1, m_hDead ); + } + else + { + ArmBeam( -1 ); + ArmBeam( 1 ); + BeamGlow( ); + } + + CPASAttenuationFilter filter3( this ); + CSoundParameters params; + if ( GetParametersForSound( "Vortigaunt.ZapPowerup", params, NULL ) ) + { + EmitSound_t ep( params ); + ep.m_nPitch = 100 + m_iBeams * 10; + EmitSound( filter3, entindex(), ep ); + } + +// Huh? Model doesn't have multiple texturegroups, commented this out. -LH +// m_nSkin = m_iBeams / 2; + } + break; + + case ISLAVE_AE_ZAP_SHOOT: + { + ClearBeams( ); + + if ( m_hDead != NULL ) + { + Vector vecDest = m_hDead->GetAbsOrigin() + Vector( 0, 0, 38 ); + trace_t trace; + UTIL_TraceHull( vecDest, vecDest, GetHullMins(), GetHullMaxs(),MASK_SOLID, m_hDead, COLLISION_GROUP_NONE, &trace ); + + if ( !trace.startsolid ) + { + CBaseEntity *pNew = Create( "monster_alien_slave", m_hDead->GetAbsOrigin(), m_hDead->GetAbsAngles() ); + + pNew->AddSpawnFlags( 1 ); + WackBeam( -1, pNew ); + WackBeam( 1, pNew ); + UTIL_Remove( m_hDead ); + break; + } + } + + ClearMultiDamage(); + + ZapBeam( -1 ); + ZapBeam( 1 ); + + CPASAttenuationFilter filter4( this ); + EmitSound( filter4, entindex(), "Vortigaunt.ZapShoot" ); + ApplyMultiDamage(); + + m_flNextAttack = gpGlobals->curtime + random->RandomFloat( 0.5, 4.0 ); + } + break; + + case ISLAVE_AE_ZAP_DONE: + { + ClearBeams(); + } + break; + + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } +} + +//------------------------------------------------------------------------------ +// Purpose : For innate range attack +// Input : +// Output : +//------------------------------------------------------------------------------ +int CNPC_Vortigaunt::RangeAttack1Conditions( float flDot, float flDist ) +{ + if ( GetEnemy() == NULL ) + return( COND_LOST_ENEMY ); + + if ( gpGlobals->curtime < m_flNextAttack ) + return COND_NONE; + + if ( HasCondition( COND_CAN_MELEE_ATTACK1 ) ) + return COND_NONE; + + return COND_CAN_RANGE_ATTACK1; +} + + +void CNPC_Vortigaunt::StartTask( const Task_t *pTask ) +{ + ClearBeams(); + BaseClass::StartTask( pTask ); +} + +//========================================================= +// TakeDamage - get provoked when injured +//========================================================= + +int CNPC_Vortigaunt::OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ) +{ + // don't slash one of your own + if ( ( inputInfo.GetDamageType() & DMG_SLASH ) && inputInfo.GetAttacker() && IRelationType( inputInfo.GetAttacker() ) == D_NU ) + return 0; + + Remember( bits_MEMORY_PROVOKED ); + + return BaseClass::OnTakeDamage_Alive( inputInfo ); +} + + +void CNPC_Vortigaunt::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + if ( info.GetDamageType() & DMG_SHOCK ) + return; + + BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator ); +} + +//========================================================= +//========================================================= +int CNPC_Vortigaunt::SelectSchedule( void ) +{ + ClearBeams(); + + switch ( m_NPCState ) + { + case NPC_STATE_COMBAT: +// dead enemy + if ( HasCondition( COND_ENEMY_DEAD ) ) + { + // call base class, all code to handle dead enemies is centralized there. + return BaseClass::SelectSchedule(); + } + + if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) ) + return SCHED_RANGE_ATTACK1; + + if ( m_iHealth < 20 || m_iBravery < 0) + { + if ( !HasCondition( COND_CAN_MELEE_ATTACK1 ) ) + { + SetDefaultFailSchedule( SCHED_CHASE_ENEMY ); + if ( HasCondition( COND_LIGHT_DAMAGE ) || HasCondition( COND_HEAVY_DAMAGE ) ) + return SCHED_TAKE_COVER_FROM_ENEMY; + if ( HasCondition ( COND_SEE_ENEMY ) && HasCondition ( COND_ENEMY_FACING_ME ) ) + return SCHED_TAKE_COVER_FROM_ENEMY; + } + } + break; + } + return BaseClass::SelectSchedule( ); +} + +int CNPC_Vortigaunt::TranslateSchedule( int scheduleType ) +{ + //Oops can't get to my enemy. + if ( scheduleType == SCHED_CHASE_ENEMY_FAILED ) + { + return SCHED_ESTABLISH_LINE_OF_FIRE; + } + + switch ( scheduleType ) + { + case SCHED_FAIL: + + if ( HasCondition( COND_CAN_MELEE_ATTACK1 ) ) + { + return ( SCHED_MELEE_ATTACK1 ); + } + + break; + + case SCHED_RANGE_ATTACK1: + { + //Adrian - HACK HACK! This should've been done up there ^^^^ + if ( HasCondition( COND_CAN_MELEE_ATTACK1 ) ) + { + return ( SCHED_MELEE_ATTACK1 ); + } + + return SCHED_VORTIGAUNT_ATTACK; + } + + break; + } + + return BaseClass::TranslateSchedule( scheduleType ); +} + +//========================================================= +// ArmBeam - small beam from arm to nearby geometry +//========================================================= +void CNPC_Vortigaunt::ArmBeam( int side ) +{ + trace_t tr; + float flDist = 1.0; + + if ( m_iBeams >= VORTIGAUNT_MAX_BEAMS ) + return; + + Vector forward, right, up; + Vector vecAim; + AngleVectors( GetAbsAngles(), &forward, &right, &up ); + Vector vecSrc = GetAbsOrigin() + up * 36 + right * side * 16 + forward * 32; + + for (int i = 0; i < 3; i++) + { + vecAim = right * side * random->RandomFloat( 0, 1 ) + up * random->RandomFloat( -1, 1 ); + trace_t tr1; + UTIL_TraceLine ( vecSrc, vecSrc + vecAim * 512, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr1); + if (flDist > tr1.fraction) + { + tr = tr1; + flDist = tr.fraction; + } + } + + // Couldn't find anything close enough + if ( flDist == 1.0 ) + return; + + if( tr.m_pEnt && tr.m_pEnt->m_takedamage && !tr.m_pEnt->IsNPC() ) + { + CTakeDamageInfo info( this, this, 10, DMG_SHOCK ); + CalculateMeleeDamageForce( &info, vecAim, tr.endpos ); + + tr.m_pEnt->TakeDamage( info ); + } + + UTIL_DecalTrace( &tr, "FadingScorch" ); + + m_pBeam[m_iBeams] = CBeam::BeamCreate( "sprites/lgtning.vmt", 3.0f ); + + if ( m_pBeam[m_iBeams] == NULL ) + return; + + m_pBeam[m_iBeams]->PointEntInit( tr.endpos, this ); + m_pBeam[m_iBeams]->SetEndAttachment( side < 0 ? 2 : 1 ); + + m_pBeam[m_iBeams]->SetColor( 96, 128, 16 ); + + m_pBeam[m_iBeams]->SetBrightness( 64 ); + m_pBeam[m_iBeams]->SetNoise( 12.8 ); + m_pBeam[m_iBeams]->AddSpawnFlags( SF_BEAM_TEMPORARY ); + + m_iBeams++; +} + +//========================================================= +// BeamGlow - brighten all beams +//========================================================= +void CNPC_Vortigaunt::BeamGlow( ) +{ + int b = m_iBeams * 32; + + if ( b > 255 ) + b = 255; + + for ( int i = 0; i < m_iBeams; i++ ) + { + if ( m_pBeam[i] != NULL ) + { + if ( m_pBeam[i]->GetBrightness() != 255 ) + m_pBeam[i]->SetBrightness( b ); + } + } +} + +//========================================================= +// WackBeam - regenerate dead colleagues +//========================================================= +void CNPC_Vortigaunt::WackBeam( int side, CBaseEntity *pEntity ) +{ + Vector vecDest; + + if ( m_iBeams >= VORTIGAUNT_MAX_BEAMS ) + return; + + if ( pEntity == NULL ) + return; + + m_pBeam[m_iBeams] = CBeam::BeamCreate( "sprites/lgtning.vmt", 3.0f ); + if ( m_pBeam[m_iBeams] == NULL ) + return; + + m_pBeam[m_iBeams]->PointEntInit( pEntity->WorldSpaceCenter(), this ); + m_pBeam[m_iBeams]->SetEndAttachment( side < 0 ? 2 : 1 ); + m_pBeam[m_iBeams]->SetColor( 180, 255, 96 ); + m_pBeam[m_iBeams]->SetBrightness( 255 ); + m_pBeam[m_iBeams]->SetNoise( 12.8 ); + m_pBeam[m_iBeams]->AddSpawnFlags( SF_BEAM_TEMPORARY ); + m_iBeams++; +} + +//========================================================= +// ZapBeam - heavy damage directly forward +//========================================================= +void CNPC_Vortigaunt::ZapBeam( int side ) +{ + Vector vecSrc, vecAim; + trace_t tr; + CBaseEntity *pEntity; + + if ( m_iBeams >= VORTIGAUNT_MAX_BEAMS ) + return; + + Vector forward, right, up; + AngleVectors( GetAbsAngles(), &forward, &right, &up ); + + vecSrc = GetAbsOrigin() + up * 36; + vecAim = GetShootEnemyDir( vecSrc ); + float deflection = 0.01; + vecAim = vecAim + side * right * random->RandomFloat( 0, deflection ) + up * random->RandomFloat( -deflection, deflection ); + UTIL_TraceLine ( vecSrc, vecSrc + vecAim * 1024, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr); + + m_pBeam[m_iBeams] = CBeam::BeamCreate( "sprites/lgtning.vmt", 5.0f ); + if ( m_pBeam[m_iBeams] == NULL ) + return; + + m_pBeam[m_iBeams]->PointEntInit( tr.endpos, this ); + m_pBeam[m_iBeams]->SetEndAttachment( side < 0 ? 2 : 1 ); + m_pBeam[m_iBeams]->SetColor( 180, 255, 96 ); + m_pBeam[m_iBeams]->SetBrightness( 255 ); + m_pBeam[m_iBeams]->SetNoise( 3.2f ); + m_pBeam[m_iBeams]->AddSpawnFlags( SF_BEAM_TEMPORARY ); + m_iBeams++; + + pEntity = tr.m_pEnt; + + if ( pEntity != NULL && m_takedamage ) + { + CTakeDamageInfo info( this, this, sk_islave_dmg_zap.GetFloat(), DMG_SHOCK ); + CalculateMeleeDamageForce( &info, vecAim, tr.endpos ); + pEntity->DispatchTraceAttack( info, vecAim, &tr ); + } +} + +//========================================================= +// ClearBeams - remove all beams +//========================================================= +void CNPC_Vortigaunt::ClearBeams( ) +{ + for (int i = 0; i < VORTIGAUNT_MAX_BEAMS; i++) + { + if (m_pBeam[i]) + { + UTIL_Remove( m_pBeam[i] ); + m_pBeam[i] = NULL; + } + } + + m_iBeams = 0; + m_nSkin = 0; +} + + +//------------------------------------------------------------------------------ +// +// Schedules +// +//------------------------------------------------------------------------------ + +AI_BEGIN_CUSTOM_NPC( monster_alien_slave, CNPC_Vortigaunt ) + + //========================================================= + // > SCHED_VORTIGAUNT_ATTACK + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_VORTIGAUNT_ATTACK, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_IDEAL 0" + " TASK_RANGE_ATTACK1 0" + " " + " Interrupts" + " COND_CAN_MELEE_ATTACK1" + " COND_HEAVY_DAMAGE" + " COND_HEAR_DANGER" + ) + +AI_END_CUSTOM_NPC() diff --git a/game/server/hl1/hl1_npc_vortigaunt.h b/game/server/hl1/hl1_npc_vortigaunt.h new file mode 100644 index 0000000..63cdb66 --- /dev/null +++ b/game/server/hl1/hl1_npc_vortigaunt.h @@ -0,0 +1,74 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef NPC_VORTIGAUNT_H +#define NPC_VORTIGAUNT_H + +#define VORTIGAUNT_MAX_BEAMS 8 + +#include "hl1_ai_basenpc.h" +//========================================================= +//========================================================= +class CNPC_Vortigaunt : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_Vortigaunt, CHL1BaseNPC ); +public: + + void Spawn( void ); + void Precache( void ); + Class_T Classify ( void ); + + void AlertSound( void ); + void IdleSound( void ); + void PainSound( const CTakeDamageInfo &info ); + void DeathSound( const CTakeDamageInfo &info ); + + int GetSoundInterests ( void ); + + float MaxYawSpeed ( void ); + + void Event_Killed( const CTakeDamageInfo &info ); + void CallForHelp( char *szClassname, float flDist, CBaseEntity * pEnemy, Vector &vecLocation ); + + int RangeAttack1Conditions( float flDot, float flDist ); + + int OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ); + void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + + void StartTask( const Task_t *pTask ); + + int SelectSchedule( void ); + int TranslateSchedule( int scheduleType ); + + void ArmBeam( int side ); + void BeamGlow( void ); + void WackBeam( int side, CBaseEntity *pEntity ); + void ZapBeam( int side ); + void ClearBeams( void ); + + void HandleAnimEvent( animevent_t *pEvent ); + + virtual Disposition_t IRelationType ( CBaseEntity *pTarget ); + + DEFINE_CUSTOM_AI; + DECLARE_DATADESC(); + +private: + int m_iVoicePitch; + int m_iBeams; + + int m_iBravery; + + CBeam *m_pBeam[VORTIGAUNT_MAX_BEAMS]; + + float m_flNextAttack; + + EHANDLE m_hDead; +}; + + +#endif //NPC_VORTIGAUNT_
\ No newline at end of file diff --git a/game/server/hl1/hl1_npc_zombie.cpp b/game/server/hl1/hl1_npc_zombie.cpp new file mode 100644 index 0000000..12874bb --- /dev/null +++ b/game/server/hl1/hl1_npc_zombie.cpp @@ -0,0 +1,307 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: A slow-moving, once-human headcrab victim with only melee attacks. +// +// UNDONE: Make head take 100% damage, body take 30% damage. +// UNDONE: Don't flinch every time you get hit. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "game.h" +#include "ai_default.h" +#include "ai_schedule.h" +#include "ai_hull.h" +#include "ai_route.h" +#include "npcevent.h" +#include "hl1_npc_zombie.h" +#include "gib.h" +//#include "AI_Interactions.h" +#include "ndebugoverlay.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" + +ConVar sk_zombie_health( "sk_zombie_health","50"); +ConVar sk_zombie_dmg_one_slash( "sk_zombie_dmg_one_slash", "20" ); +ConVar sk_zombie_dmg_both_slash( "sk_zombie_dmg_both_slash", "40" ); + + + +LINK_ENTITY_TO_CLASS( monster_zombie, CNPC_Zombie ); + + +//========================================================= +// Spawn +//========================================================= +void CNPC_Zombie::Spawn() +{ + Precache( ); + + SetModel( "models/zombie.mdl" ); + + SetRenderColor( 255, 255, 255, 255 ); + + SetHullType(HULL_HUMAN); + SetHullSizeNormal(); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + m_bloodColor = BLOOD_COLOR_GREEN; + m_iHealth = sk_zombie_health.GetFloat(); + //pev->view_ofs = VEC_VIEW;// position of the eyes relative to monster's origin. + m_flFieldOfView = 0.5; + m_NPCState = NPC_STATE_NONE; + CapabilitiesClear(); + CapabilitiesAdd( bits_CAP_MOVE_GROUND | bits_CAP_INNATE_MELEE_ATTACK1 | bits_CAP_DOORS_GROUP ); + + NPCInit(); +} + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CNPC_Zombie::Precache() +{ + PrecacheModel( "models/zombie.mdl" ); + + PrecacheScriptSound( "Zombie.AttackHit" ); + PrecacheScriptSound( "Zombie.AttackMiss" ); + PrecacheScriptSound( "Zombie.Pain" ); + PrecacheScriptSound( "Zombie.Idle" ); + PrecacheScriptSound( "Zombie.Alert" ); + PrecacheScriptSound( "Zombie.Attack" ); + + BaseClass::Precache(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns this monster's place in the relationship table. +//----------------------------------------------------------------------------- +Class_T CNPC_Zombie::Classify( void ) +{ + return CLASS_ALIEN_MONSTER; +} + +//========================================================= +// HandleAnimEvent - catches the monster-specific messages +// that occur when tagged animation frames are played. +//========================================================= +void CNPC_Zombie::HandleAnimEvent( animevent_t *pEvent ) +{ + Vector v_forward, v_right; + switch( pEvent->event ) + { + case ZOMBIE_AE_ATTACK_RIGHT: + { + // do stuff for this event. + // ALERT( at_console, "Slash right!\n" ); + + Vector vecMins = GetHullMins(); + Vector vecMaxs = GetHullMaxs(); + vecMins.z = vecMins.x; + vecMaxs.z = vecMaxs.x; + + CBaseEntity *pHurt = CheckTraceHullAttack( 70, vecMins, vecMaxs, sk_zombie_dmg_one_slash.GetFloat(), DMG_SLASH ); + CPASAttenuationFilter filter( this ); + if ( pHurt ) + { + if ( pHurt->GetFlags() & ( FL_NPC | FL_CLIENT ) ) + { + pHurt->ViewPunch( QAngle( 5, 0, 18 ) ); + + GetVectors( &v_forward, &v_right, NULL ); + + pHurt->SetAbsVelocity( pHurt->GetAbsVelocity() - v_right * 100 ); + } + // Play a random attack hit sound + EmitSound( filter, entindex(), "Zombie.AttackHit" ); + } + else // Play a random attack miss sound + EmitSound( filter, entindex(), "Zombie.AttackMiss" ); + + if ( random->RandomInt( 0, 1 ) ) + AttackSound(); + } + break; + + case ZOMBIE_AE_ATTACK_LEFT: + { + // do stuff for this event. + // ALERT( at_console, "Slash left!\n" ); + Vector vecMins = GetHullMins(); + Vector vecMaxs = GetHullMaxs(); + vecMins.z = vecMins.x; + vecMaxs.z = vecMaxs.x; + + CBaseEntity *pHurt = CheckTraceHullAttack( 70, vecMins, vecMaxs, sk_zombie_dmg_one_slash.GetFloat(), DMG_SLASH ); + + CPASAttenuationFilter filter2( this ); + if ( pHurt ) + { + if ( pHurt->GetFlags() & ( FL_NPC | FL_CLIENT ) ) + { + pHurt->ViewPunch( QAngle ( 5, 0, -18 ) ); + + GetVectors( &v_forward, &v_right, NULL ); + + pHurt->SetAbsVelocity( pHurt->GetAbsVelocity() - v_right * 100 ); + } + EmitSound( filter2, entindex(), "Zombie.AttackHit" ); + } + else + { + EmitSound( filter2, entindex(), "Zombie.AttackMiss" ); + } + + if ( random->RandomInt( 0,1 ) ) + AttackSound(); + } + break; + + case ZOMBIE_AE_ATTACK_BOTH: + { + // do stuff for this event. + Vector vecMins = GetHullMins(); + Vector vecMaxs = GetHullMaxs(); + vecMins.z = vecMins.x; + vecMaxs.z = vecMaxs.x; + + CBaseEntity *pHurt = CheckTraceHullAttack( 70, vecMins, vecMaxs, sk_zombie_dmg_both_slash.GetFloat(), DMG_SLASH ); + + + CPASAttenuationFilter filter3( this ); + if ( pHurt ) + { + if ( pHurt->GetFlags() & ( FL_NPC | FL_CLIENT ) ) + { + pHurt->ViewPunch( QAngle ( 5, 0, 0 ) ); + + GetVectors( &v_forward, &v_right, NULL ); + pHurt->SetAbsVelocity( pHurt->GetAbsVelocity() - v_right * 100 ); + } + EmitSound( filter3, entindex(), "Zombie.AttackHit" ); + } + else + EmitSound( filter3, entindex(),"Zombie.AttackMiss" ); + + if ( random->RandomInt( 0,1 ) ) + AttackSound(); + } + break; + + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } +} + + +static float DamageForce( const Vector &size, float damage ) +{ + float force = damage * ((32 * 32 * 72.0) / (size.x * size.y * size.z)) * 5; + + if ( force > 1000.0) + { + force = 1000.0; + } + + return force; +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pInflictor - +// pAttacker - +// flDamage - +// bitsDamageType - +// Output : int +//----------------------------------------------------------------------------- +int CNPC_Zombie::OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ) +{ + CTakeDamageInfo info = inputInfo; + + // Take 30% damage from bullets + if ( info.GetDamageType() == DMG_BULLET ) + { + Vector vecDir = GetAbsOrigin() - info.GetInflictor()->WorldSpaceCenter(); + VectorNormalize( vecDir ); + float flForce = DamageForce( WorldAlignSize(), info.GetDamage() ); + SetAbsVelocity( GetAbsVelocity() + vecDir * flForce ); + info.ScaleDamage( 0.3f ); + } + + // HACK HACK -- until we fix this. + if ( IsAlive() ) + PainSound( info ); + + return BaseClass::OnTakeDamage_Alive( info ); +} + +void CNPC_Zombie::PainSound( const CTakeDamageInfo &info ) +{ + if ( random->RandomInt(0,5) < 2) + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Zombie.Pain" ); + } +} + +void CNPC_Zombie::AlertSound( void ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Zombie.Alert" ); +} + +void CNPC_Zombie::IdleSound( void ) +{ + // Play a random idle sound + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Zombie.Idle" ); +} + +void CNPC_Zombie::AttackSound( void ) +{ + // Play a random attack sound + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Zombie.Attack" ); +} + +int CNPC_Zombie::MeleeAttack1Conditions ( float flDot, float flDist ) +{ + if ( flDist > 64) + { + return COND_TOO_FAR_TO_ATTACK; + } + else if (flDot < 0.7) + { + return 0; + } + else if (GetEnemy() == NULL) + { + return 0; + } + + return COND_CAN_MELEE_ATTACK1; +} + +void CNPC_Zombie::RemoveIgnoredConditions ( void ) +{ + if ( GetActivity() == ACT_MELEE_ATTACK1 ) + { + // Nothing stops an attacking zombie. + ClearCondition( COND_LIGHT_DAMAGE ); + ClearCondition( COND_HEAVY_DAMAGE ); + } + + if (( GetActivity() == ACT_SMALL_FLINCH ) || ( GetActivity() == ACT_BIG_FLINCH )) + { + if (m_flNextFlinch < gpGlobals->curtime) + m_flNextFlinch = gpGlobals->curtime + ZOMBIE_FLINCH_DELAY; + } + + BaseClass::RemoveIgnoredConditions(); +} diff --git a/game/server/hl1/hl1_npc_zombie.h b/game/server/hl1/hl1_npc_zombie.h new file mode 100644 index 0000000..73298a8 --- /dev/null +++ b/game/server/hl1/hl1_npc_zombie.h @@ -0,0 +1,52 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef NPC_ZOMBIE_H +#define NPC_ZOMBIE_H + + +#include "hl1_ai_basenpc.h" +//========================================================= +// Monster's Anim Events Go Here +//========================================================= +#define ZOMBIE_AE_ATTACK_RIGHT 0x01 +#define ZOMBIE_AE_ATTACK_LEFT 0x02 +#define ZOMBIE_AE_ATTACK_BOTH 0x03 + +#define ZOMBIE_FLINCH_DELAY 2 // at most one flinch every n secs + +//========================================================= +//========================================================= +class CNPC_Zombie : public CHL1BaseNPC +{ + DECLARE_CLASS( CNPC_Zombie, CHL1BaseNPC ); +public: + + void Spawn( void ); + void Precache( void ); + float MaxYawSpeed( void ) { return 120.0f; }; + Class_T Classify( void ); + void HandleAnimEvent( animevent_t *pEvent ); +// int IgnoreConditions ( void ); + + float m_flNextFlinch; + + void PainSound( const CTakeDamageInfo &info ); + void AlertSound( void ); + void IdleSound( void ); + void AttackSound( void ); + + // No range attacks + BOOL CheckRangeAttack1 ( float flDot, float flDist ) { return FALSE; } + BOOL CheckRangeAttack2 ( float flDot, float flDist ) { return FALSE; } + int OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ); + + void RemoveIgnoredConditions ( void ); + int MeleeAttack1Conditions ( float flDot, float flDist ); +}; + +#endif //NPC_ZOMBIE_H
\ No newline at end of file diff --git a/game/server/hl1/hl1_player.cpp b/game/server/hl1/hl1_player.cpp new file mode 100644 index 0000000..b47cafe --- /dev/null +++ b/game/server/hl1/hl1_player.cpp @@ -0,0 +1,2110 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Player for HL1. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "hl1_player.h" +#include "gamerules.h" +#include "trains.h" +#include "hl1_basecombatweapon_shared.h" +#include "vcollide_parse.h" +#include "in_buttons.h" +#include "igamemovement.h" +#include "ai_hull.h" +#include "hl2_shareddefs.h" +#include "info_camera_link.h" +#include "point_camera.h" +#include "ndebugoverlay.h" +#include "globals.h" +#include "ai_interactions.h" +#include "engine/IEngineSound.h" +#include "vphysics/player_controller.h" +#include "vphysics/constraints.h" +#include "predicted_viewmodel.h" +#include "physics_saverestore.h" +#include "gamestats.h" + +#define DMG_FREEZE DMG_VEHICLE +#define DMG_SLOWFREEZE DMG_DISSOLVE + +// HL1_DMG_SHOWNHUD: Add DMG_VEHICLE because HL2 hijacked those bits from DMG_FREEZE, which is what they are in HL1 +// HL1_DMG_SHOWNHUD: Add DMG_DISSOLVE because HL2 hijacked those bits from DMG_SLOWFREEZE, which is what they are in HL1 +// See Halflife1 GameRules - Damage_GetShowOnHud() +//#define HL1_DMG_SHOWNHUD (DMG_POISON | DMG_ACID | DMG_FREEZE | DMG_SLOWFREEZE | DMG_DROWN | DMG_BURN | DMG_SLOWBURN | DMG_NERVEGAS | DMG_RADIATION | DMG_SHOCK) + +// TIME BASED DAMAGE AMOUNT +// tweak these values based on gameplay feedback: +#define PARALYZE_DURATION 2 // number of 2 second intervals to take damage +#define PARALYZE_DAMAGE 1.0 // damage to take each 2 second interval + +#define NERVEGAS_DURATION 2 +#define NERVEGAS_DAMAGE 5.0 + +#define POISON_DURATION 5 +#define POISON_DAMAGE 2.0 + +#define RADIATION_DURATION 2 +#define RADIATION_DAMAGE 1.0 + +#define ACID_DURATION 2 +#define ACID_DAMAGE 5.0 + +#define SLOWBURN_DURATION 2 +#define SLOWBURN_DAMAGE 1.0 + +#define SLOWFREEZE_DURATION 2 +#define SLOWFREEZE_DAMAGE 1.0 + +#define FLASH_DRAIN_TIME 1.2 //100 units/3 minutes +#define FLASH_CHARGE_TIME 0.2 // 100 units/20 seconds (seconds per unit) + +ConVar player_showpredictedposition( "player_showpredictedposition", "0" ); +ConVar player_showpredictedposition_timestep( "player_showpredictedposition_timestep", "1.0" ); + +ConVar sv_hl1_allowpickup( "sv_hl1_allowpickup", "0" ); + +LINK_ENTITY_TO_CLASS( player, CHL1_Player ); +PRECACHE_REGISTER(player); + +BEGIN_DATADESC( CHL1_Player ) + DEFINE_FIELD( m_nControlClass, FIELD_INTEGER ), + DEFINE_AUTO_ARRAY( m_vecMissPositions, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_nNumMissPositions, FIELD_INTEGER ), + + DEFINE_FIELD( m_flIdleTime, FIELD_TIME ), + DEFINE_FIELD( m_flMoveTime, FIELD_TIME ), + DEFINE_FIELD( m_flLastDamageTime, FIELD_TIME ), + DEFINE_FIELD( m_flTargetFindTime, FIELD_TIME ), + DEFINE_FIELD( m_bHasLongJump, FIELD_BOOLEAN ), + DEFINE_FIELD( m_nFlashBattery, FIELD_INTEGER ), + DEFINE_FIELD( m_flFlashLightTime, FIELD_TIME ), + + DEFINE_FIELD( m_flStartCharge, FIELD_FLOAT ), + DEFINE_FIELD( m_flAmmoStartCharge, FIELD_FLOAT ), + DEFINE_FIELD( m_flPlayAftershock, FIELD_FLOAT ), + DEFINE_FIELD( m_flNextAmmoBurn, FIELD_FLOAT ), + + DEFINE_PHYSPTR( m_pPullConstraint ), + DEFINE_FIELD( m_hPullObject, FIELD_EHANDLE ), + DEFINE_FIELD( m_bIsPullingObject, FIELD_BOOLEAN ), + +END_DATADESC() + + +IMPLEMENT_SERVERCLASS_ST( CHL1_Player, DT_HL1Player ) + SendPropInt( SENDINFO( m_bHasLongJump ), 1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_nFlashBattery ), 8, SPROP_UNSIGNED ), + SendPropBool( SENDINFO( m_bIsPullingObject ) ), + + SendPropFloat( SENDINFO( m_flStartCharge ) ), + SendPropFloat( SENDINFO( m_flAmmoStartCharge ) ), + SendPropFloat( SENDINFO( m_flPlayAftershock ) ), + SendPropFloat( SENDINFO( m_flNextAmmoBurn ) ) + +END_SEND_TABLE() + +CHL1_Player::CHL1_Player() +{ + m_nNumMissPositions = 0; +} + +void CHL1_Player::Precache( void ) +{ + BaseClass::Precache(); + + PrecacheScriptSound( "Player.FlashlightOn" ); + PrecacheScriptSound( "Player.FlashlightOff" ); + PrecacheScriptSound( "Player.UseTrain" ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Allow pre-frame adjustments on the player +//----------------------------------------------------------------------------- +void CHL1_Player::PreThink(void) +{ + if ( player_showpredictedposition.GetBool() ) + { + Vector predPos; + + UTIL_PredictedPosition( this, player_showpredictedposition_timestep.GetFloat(), &predPos ); + + NDebugOverlay::Box( predPos, NAI_Hull::Mins( GetHullType() ), NAI_Hull::Maxs( GetHullType() ), 0, 255, 0, 0, 0.01f ); + NDebugOverlay::Line( GetAbsOrigin(), predPos, 0, 255, 0, 0, 0.01f ); + } + + int buttonsChanged; + buttonsChanged = m_afButtonPressed | m_afButtonReleased; + + UpdatePullingObject(); + + g_pGameRules->PlayerThink( this ); + + if ( g_fGameOver || IsPlayerLockedInPlace() ) + return; // intermission or finale + + ItemPreFrame( ); + WaterMove(); + + if ( g_pGameRules && g_pGameRules->FAllowFlashlight() ) + m_Local.m_iHideHUD &= ~HIDEHUD_FLASHLIGHT; + else + m_Local.m_iHideHUD |= HIDEHUD_FLASHLIGHT; + + + // checks if new client data (for HUD and view control) needs to be sent to the client + UpdateClientData(); + + CheckTimeBasedDamage(); + + CheckSuitUpdate(); + + if (m_lifeState >= LIFE_DYING) + { + PlayerDeathThink(); + return; + } + + // So the correct flags get sent to client asap. + // + if ( m_afPhysicsFlags & PFLAG_DIROVERRIDE ) + AddFlag( FL_ONTRAIN ); + else + RemoveFlag( FL_ONTRAIN ); + + // Train speed control + if ( m_afPhysicsFlags & PFLAG_DIROVERRIDE ) + { + CBaseEntity *pTrain = GetGroundEntity(); + float vel; + + if ( pTrain ) + { + if ( !(pTrain->ObjectCaps() & FCAP_DIRECTIONAL_USE) ) + pTrain = NULL; + } + + if ( !pTrain ) + { + if ( GetActiveWeapon() && (GetActiveWeapon()->ObjectCaps() & FCAP_DIRECTIONAL_USE) ) + { + m_iTrain = TRAIN_ACTIVE | TRAIN_NEW; + + if ( m_nButtons & IN_FORWARD ) + { + m_iTrain |= TRAIN_FAST; + } + else if ( m_nButtons & IN_BACK ) + { + m_iTrain |= TRAIN_BACK; + } + else + { + m_iTrain |= TRAIN_NEUTRAL; + } + return; + } + else + { + trace_t trainTrace; + // Maybe this is on the other side of a level transition + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + Vector(0,0,-38), + MASK_PLAYERSOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &trainTrace ); + + if ( trainTrace.fraction != 1.0 && trainTrace.m_pEnt ) + pTrain = trainTrace.m_pEnt; + + + if ( !pTrain || !(pTrain->ObjectCaps() & FCAP_DIRECTIONAL_USE) || !pTrain->OnControls(this) ) + { +// Warning( "In train mode with no train!\n" ); + m_afPhysicsFlags &= ~PFLAG_DIROVERRIDE; + m_iTrain = TRAIN_NEW|TRAIN_OFF; + return; + } + } + } + else if ( !( GetFlags() & FL_ONGROUND ) || pTrain->HasSpawnFlags( SF_TRACKTRAIN_NOCONTROL ) || (m_nButtons & (IN_MOVELEFT|IN_MOVERIGHT) ) ) + { + // Turn off the train if you jump, strafe, or the train controls go dead + m_afPhysicsFlags &= ~PFLAG_DIROVERRIDE; + m_iTrain = TRAIN_NEW|TRAIN_OFF; + return; + } + + SetAbsVelocity( vec3_origin ); + vel = 0; + if ( m_afButtonPressed & IN_FORWARD ) + { + vel = 1; + pTrain->Use( this, this, USE_SET, (float)vel ); + } + else if ( m_afButtonPressed & IN_BACK ) + { + vel = -1; + pTrain->Use( this, this, USE_SET, (float)vel ); + } + else if ( m_afButtonPressed & IN_JUMP ) + { + m_afPhysicsFlags &= ~PFLAG_DIROVERRIDE; + m_iTrain = TRAIN_NEW|TRAIN_OFF; + } + + if (vel) + { + m_iTrain = TrainSpeed(pTrain->m_flSpeed, ((CFuncTrackTrain*)pTrain)->GetMaxSpeed()); + m_iTrain |= TRAIN_ACTIVE|TRAIN_NEW; + } + } + else if (m_iTrain & TRAIN_ACTIVE) + { + m_iTrain = TRAIN_NEW; // turn off train + } + + // THIS CODE DOESN'T SEEM TO DO ANYTHING!!! + // WHY IS IT STILL HERE? (sjb) + if (m_nButtons & IN_JUMP) + { + // If on a ladder, jump off the ladder + // else Jump + if( IsPullingObject() ) + { + StopPullingObject(); + } + + Jump(); + } + + // If trying to duck, already ducked, or in the process of ducking + if ((m_nButtons & IN_DUCK) || (GetFlags() & FL_DUCKING) || (m_afPhysicsFlags & PFLAG_DUCKING) ) + Duck(); + + // + // If we're not on the ground, we're falling. Update our falling velocity. + // + if ( !( GetFlags() & FL_ONGROUND ) ) + { + m_Local.m_flFallVelocity = -GetAbsVelocity().z; + } + + if ( m_afPhysicsFlags & PFLAG_ONBARNACLE ) + { + SetAbsVelocity( vec3_origin ); + } + // StudioFrameAdvance( );//!!!HACKHACK!!! Can't be hit by traceline when not animating? + + //Find targets for NPC to shoot if they decide to miss us + FindMissTargets(); +} + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +Class_T CHL1_Player::Classify ( void ) +{ + // If player controlling another entity? If so, return this class + if (m_nControlClass != CLASS_NONE) + { + return m_nControlClass; + } + else + { + return CLASS_PLAYER; + } +} + +//----------------------------------------------------------------------------- +// Purpose: This is a generic function (to be implemented by sub-classes) to +// handle specific interactions between different types of characters +// (For example the barnacle grabbing an NPC) +// Input : Constant for the type of interaction +// Output : true - if sub-class has a response for the interaction +// false - if sub-class has no response +//----------------------------------------------------------------------------- +bool CHL1_Player::HandleInteraction(int interactionType, void *data, CBaseCombatCharacter* sourceEnt) +{ + if ( interactionType == g_interactionBarnacleVictimDangle ) + { + TakeDamage ( CTakeDamageInfo( sourceEnt, sourceEnt, m_iHealth + ArmorValue(), DMG_SLASH | DMG_ALWAYSGIB ) ); + return true; + } + + if (interactionType == g_interactionBarnacleVictimReleased) + { + m_afPhysicsFlags &= ~PFLAG_ONBARNACLE; + SetMoveType( MOVETYPE_WALK ); + return true; + } + else if (interactionType == g_interactionBarnacleVictimGrab) + { + m_afPhysicsFlags |= PFLAG_ONBARNACLE; + ClearUseEntity(); + return true; + } + return false; +} + + +void CHL1_Player::PlayerRunCommand(CUserCmd *ucmd, IMoveHelper *moveHelper) +{ + // Handle FL_FROZEN. + if ( m_afPhysicsFlags & PFLAG_ONBARNACLE ) + { + ucmd->forwardmove = 0; + ucmd->sidemove = 0; + ucmd->upmove = 0; + } + + //Update our movement information + if ( ( ucmd->forwardmove != 0 ) || ( ucmd->sidemove != 0 ) || ( ucmd->upmove != 0 ) ) + { + m_flIdleTime -= TICK_INTERVAL * 2.0f; + + if ( m_flIdleTime < 0.0f ) + { + m_flIdleTime = 0.0f; + } + + m_flMoveTime += TICK_INTERVAL; + + if ( m_flMoveTime > 4.0f ) + { + m_flMoveTime = 4.0f; + } + } + else + { + m_flIdleTime += TICK_INTERVAL; + + if ( m_flIdleTime > 4.0f ) + { + m_flIdleTime = 4.0f; + } + + m_flMoveTime -= TICK_INTERVAL * 2.0f; + + if ( m_flMoveTime < 0.0f ) + { + m_flMoveTime = 0.0f; + } + } + + //Msg("Player time: [ACTIVE: %f]\t[IDLE: %f]\n", m_flMoveTime, m_flIdleTime ); + + BaseClass::PlayerRunCommand( ucmd, moveHelper ); +} + +//----------------------------------------------------------------------------- +// Purpose: Create and give the named item to the player. Then return it. +//----------------------------------------------------------------------------- +CBaseEntity *CHL1_Player::GiveNamedItem( const char *pszName, int iSubType ) +{ + // If I already own this type don't create one + if ( Weapon_OwnsThisType(pszName, iSubType) ) + return NULL; + + // Msg( "giving %s\n", pszName ); + + EHANDLE pent; + + pent = CreateEntityByName(pszName); + if ( pent == NULL ) + { + Msg( "NULL Ent in GiveNamedItem!\n" ); + return NULL; + } + + pent->SetLocalOrigin( GetLocalOrigin() ); + pent->AddSpawnFlags( SF_NORESPAWN ); + + if ( iSubType ) + { + CBaseCombatWeapon *pWeapon = dynamic_cast<CBaseCombatWeapon*>( (CBaseEntity*)pent ); + if ( pWeapon ) + { + pWeapon->SetSubType( iSubType ); + } + } + + DispatchSpawn( pent ); + + if ( pent != NULL && !(pent->IsMarkedForDeletion()) ) + { + pent->Touch( this ); + } + + return pent; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CHL1_Player::StartPullingObject( CBaseEntity *pObject ) +{ + if ( pObject->VPhysicsGetObject() == NULL || VPhysicsGetObject() == NULL ) + { + return; + } + + if( !(GetFlags()&FL_ONGROUND) ) + { + //Msg("Can't grab in air!\n"); + return; + } + + if( GetGroundEntity() == pObject ) + { + //Msg("Can't grab something you're standing on!\n"); + return; + } + + constraint_ballsocketparams_t ballsocket; + ballsocket.Defaults(); + ballsocket.constraint.Defaults(); + ballsocket.constraint.forceLimit = lbs2kg(1000); + ballsocket.constraint.torqueLimit = lbs2kg(1000); + ballsocket.InitWithCurrentObjectState( VPhysicsGetObject(), pObject->VPhysicsGetObject(), WorldSpaceCenter() ); + m_pPullConstraint = physenv->CreateBallsocketConstraint( VPhysicsGetObject(), pObject->VPhysicsGetObject(), NULL, ballsocket ); + + m_hPullObject.Set(pObject); + m_bIsPullingObject = true; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CHL1_Player::StopPullingObject() +{ + if( m_pPullConstraint ) + { + physenv->DestroyConstraint( m_pPullConstraint ); + } + + m_hPullObject.Set(NULL); + m_pPullConstraint = NULL; + m_bIsPullingObject = false; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CHL1_Player::UpdatePullingObject() +{ + if( !IsPullingObject() ) + return; + + CBaseEntity *pObject= m_hPullObject.Get(); + + if( !pObject || !pObject->VPhysicsGetObject() ) + { + // Object broke or otherwise vanished. + StopPullingObject(); + return; + } + + if( m_afButtonReleased & IN_USE ) + { + // Player released +USE + StopPullingObject(); + return; + } + + + float flMaxDistSqr = Square(PLAYER_USE_RADIUS + 1.0f); + + Vector objectPos; + QAngle angle; + + pObject->VPhysicsGetObject()->GetPosition( &objectPos, &angle ); + + if( !FInViewCone(objectPos) ) + { + // Player turned away. + StopPullingObject(); + } + else if( objectPos.DistToSqr(WorldSpaceCenter()) > flMaxDistSqr ) + { + // Object got caught up on something and left behind + StopPullingObject(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Sets HL1specific defaults. +//----------------------------------------------------------------------------- +void CHL1_Player::Spawn(void) +{ + // In multiplayer ,this is handled in the super class + if ( !g_pGameRules->IsMultiplayer () ) + SetModel( "models/player.mdl" ); + + BaseClass::Spawn(); + + // + // Our player movement speed is set once here. This will override the cl_xxxx + // cvars unless they are set to be lower than this. + // + SetMaxSpeed( 1000 ); + + SetDefaultFOV( 0 ); + + m_nFlashBattery = 99; + m_flFlashLightTime = 1; + + m_flFieldOfView = 0.5; + + StopPullingObject(); + + m_Local.m_iHideHUD = 0; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CHL1_Player::Event_Killed( const CTakeDamageInfo &info ) +{ + StopPullingObject(); + BaseClass::Event_Killed(info); +} + +void CHL1_Player::CheckTimeBasedDamage( void ) +{ + int i; + byte bDuration = 0; + + static float gtbdPrev = 0.0; + + // If we don't have any time based damage return. + if ( !g_pGameRules->Damage_IsTimeBased( m_bitsDamageType ) ) + return; + + // only check for time based damage approx. every 2 seconds + if (abs(gpGlobals->curtime - m_tbdPrev) < 2.0) + return; + + m_tbdPrev = gpGlobals->curtime; + + for (i = 0; i < CDMG_TIMEBASED; i++) + { + // Make sure the damage type is really time-based. + // This is kind of hacky but necessary until we setup DamageType as an enum. + int iDamage = ( DMG_PARALYZE << i ); + if ( !g_pGameRules->Damage_IsTimeBased( iDamage ) ) + continue; + + // make sure bit is set for damage type + if ( m_bitsDamageType & iDamage ) + { + switch (i) + { + case itbd_Paralyze: + // UNDONE - flag movement as half-speed + bDuration = PARALYZE_DURATION; + break; + case itbd_NerveGas: +// OnTakeDamage(pev, pev, NERVEGAS_DAMAGE, DMG_GENERIC); + bDuration = NERVEGAS_DURATION; + break; + case itbd_PoisonRecover: + OnTakeDamage( CTakeDamageInfo( this, this, POISON_DAMAGE, DMG_GENERIC ) ); + bDuration = POISON_DURATION; + break; + case itbd_Radiation: +// OnTakeDamage(pev, pev, RADIATION_DAMAGE, DMG_GENERIC); + bDuration = RADIATION_DURATION; + break; + case itbd_DrownRecover: + // NOTE: this hack is actually used to RESTORE health + // after the player has been drowning and finally takes a breath + if (m_idrowndmg > m_idrownrestored) + { + int idif = MIN(m_idrowndmg - m_idrownrestored, 10); + + TakeHealth(idif, DMG_GENERIC); + m_idrownrestored += idif; + } + bDuration = 4; // get up to 5*10 = 50 points back + break; + + case itbd_Acid: +// OnTakeDamage(pev, pev, ACID_DAMAGE, DMG_GENERIC); + bDuration = ACID_DURATION; + break; + case itbd_SlowBurn: +// OnTakeDamage(pev, pev, SLOWBURN_DAMAGE, DMG_GENERIC); + bDuration = SLOWBURN_DURATION; + break; + case itbd_SlowFreeze: +// OnTakeDamage(pev, pev, SLOWFREEZE_DAMAGE, DMG_GENERIC); + bDuration = SLOWFREEZE_DURATION; + break; + default: + bDuration = 0; + } + + if (m_rgbTimeBasedDamage[i]) + { + // decrement damage duration, detect when done. + if (!m_rgbTimeBasedDamage[i] || --m_rgbTimeBasedDamage[i] == 0) + { + m_rgbTimeBasedDamage[i] = 0; + // if we're done, clear damage bits + m_bitsDamageType &= ~(DMG_PARALYZE << i); + } + } + else + // first time taking this damage type - init damage duration + m_rgbTimeBasedDamage[i] = bDuration; + } + } +} + + +class CPhysicsPlayerCallback : public IPhysicsPlayerControllerEvent +{ +public: + int ShouldMoveTo( IPhysicsObject *pObject, const Vector &position ) + { + CHL1_Player *pPlayer = (CHL1_Player *)pObject->GetGameData(); + if ( pPlayer ) + { + if ( pPlayer->TouchedPhysics() ) + { + return 0; + } + } + return 1; + } +}; + +static CPhysicsPlayerCallback playerCallback; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CHL1_Player::InitVCollision( const Vector &vecAbsOrigin, const Vector &vecAbsVelocity ) +{ + BaseClass::InitVCollision( vecAbsOrigin, vecAbsVelocity ); + + // Setup the HL2 specific callback. + GetPhysicsController()->SetEventHandler( &playerCallback ); +} + + +CHL1_Player::~CHL1_Player( void ) +{ +} + +extern int gEvilImpulse101; +void CHL1_Player::CheatImpulseCommands( int iImpulse ) +{ + if ( !sv_cheats->GetBool() ) + { + return; + } + + switch( iImpulse ) + { + case 101: + gEvilImpulse101 = true; + + GiveNamedItem( "item_suit" ); + GiveNamedItem( "item_battery" ); + GiveNamedItem( "weapon_crowbar" ); + GiveNamedItem( "weapon_glock" ); + GiveNamedItem( "ammo_9mmclip" ); + GiveNamedItem( "weapon_shotgun" ); + GiveNamedItem( "ammo_buckshot" ); + GiveNamedItem( "weapon_mp5" ); + GiveNamedItem( "ammo_9mmar" ); + GiveNamedItem( "ammo_argrenades" ); + GiveNamedItem( "weapon_handgrenade" ); + GiveNamedItem( "weapon_tripmine" ); + GiveNamedItem( "weapon_357" ); + GiveNamedItem( "ammo_357" ); + GiveNamedItem( "weapon_crossbow" ); + GiveNamedItem( "ammo_crossbow" ); + GiveNamedItem( "weapon_egon" ); + GiveNamedItem( "weapon_gauss" ); + GiveNamedItem( "ammo_gaussclip" ); + GiveNamedItem( "weapon_rpg" ); + GiveNamedItem( "ammo_rpgclip" ); + GiveNamedItem( "weapon_satchel" ); + GiveNamedItem( "weapon_snark" ); + GiveNamedItem( "weapon_hornetgun" ); + + gEvilImpulse101 = false; + break; + + case 0: + default: + BaseClass::CheatImpulseCommands( iImpulse ); + } +} + +void CHL1_Player::SetupVisibility( CBaseEntity *pViewEntity, unsigned char *pvs, int pvssize ) +{ + int area = pViewEntity ? pViewEntity->NetworkProp()->AreaNum() : NetworkProp()->AreaNum(); + BaseClass::SetupVisibility( pViewEntity, pvs, pvssize ); + PointCameraSetupVisibility( this, area, pvs, pvssize ); +} + + +#define ARMOR_RATIO 0.2 // Armor Takes 80% of the damage +#define ARMOR_BONUS 0.5 // Each Point of Armor is work 1/x points of health + + +int CHL1_Player::OnTakeDamage( const CTakeDamageInfo &inputInfo ) +{ + // have suit diagnose the problem - ie: report damage type + int bitsDamage = inputInfo.GetDamageType(); + int ffound = true; + int fmajor; + int fcritical; + int fTookDamage; + int ftrivial; + float flRatio; + float flBonus; + float flHealthPrev = m_iHealth; + + CTakeDamageInfo info = inputInfo; + + if ( info.GetDamage() > 0.0f ) + { + m_flLastDamageTime = gpGlobals->curtime; + } + + flBonus = ARMOR_BONUS; + flRatio = ARMOR_RATIO; + + if ( ( info.GetDamageType() & DMG_BLAST ) && g_pGameRules->IsMultiplayer() ) + { + // blasts damage armor more. + flBonus *= 2; + } + + // Already dead + if ( !IsAlive() ) + return 0; + // go take the damage first + + + if ( !g_pGameRules->FPlayerCanTakeDamage( this, info.GetAttacker(), info ) ) + { + // Refuse the damage + return 0; + } + + // keep track of amount of damage last sustained + m_lastDamageAmount = info.GetDamage(); + + // Armor. + if ( ArmorValue() && + !(info.GetDamageType() & (DMG_FALL | DMG_DROWN | DMG_POISON)) && // armor doesn't protect against fall or drown damage! + !(GetFlags() & FL_GODMODE) ) + { + float flNew = info.GetDamage() * flRatio; + + float flArmor; + + flArmor = (info.GetDamage() - flNew) * flBonus; + + // Does this use more armor than we have? + if ( flArmor > ArmorValue() ) + { + flArmor = ArmorValue(); + flArmor *= (1/flBonus); + flNew = info.GetDamage() - flArmor; + SetArmorValue( 0 ); + } + else + SetArmorValue( ArmorValue() - flArmor ); + + info.SetDamage( flNew ); + } + + // this cast to INT is critical!!! If a player ends up with 0.5 health, the engine will get that + // as an int (zero) and think the player is dead! (this will incite a clientside screentilt, etc) + info.SetDamage( (int)info.GetDamage() ); + fTookDamage = CBaseCombatCharacter::OnTakeDamage( info ); // Bypass CBasePlayer's + + if ( fTookDamage ) + { + // add to the damage total for clients, which will be sent as a single + // message at the end of the frame + // todo: remove after combining shotgun blasts? + if ( info.GetInflictor() && info.GetInflictor()->edict() ) + m_DmgOrigin = info.GetInflictor()->GetAbsOrigin(); + + m_DmgTake += (int)info.GetDamage(); + } + + // Reset damage time countdown for each type of time based damage player just sustained + for (int i = 0; i < CDMG_TIMEBASED; i++) + { + // Make sure the damage type is really time-based. + // This is kind of hacky but necessary until we setup DamageType as an enum. + int iDamage = ( DMG_PARALYZE << i ); + if ( ( info.GetDamageType() & iDamage ) && g_pGameRules->Damage_IsTimeBased( iDamage ) ) + { + m_rgbTimeBasedDamage[i] = 0; + } + } + + // Display any effect associate with this damage type + DamageEffect(info.GetDamage(),bitsDamage); + + // how bad is it, doc? + ftrivial = (m_iHealth > 75 || m_lastDamageAmount < 5); + fmajor = (m_lastDamageAmount > 25); + fcritical = (m_iHealth < 30); + + // handle all bits set in this damage message, + // let the suit give player the diagnosis + + // UNDONE: add sounds for types of damage sustained (ie: burn, shock, slash ) + + // UNDONE: still need to record damage and heal messages for the following types + + // DMG_BURN + // DMG_FREEZE + // DMG_BLAST + // DMG_SHOCK + + m_bitsDamageType |= bitsDamage; // Save this so we can report it to the client + m_bitsHUDDamage = -1; // make sure the damage bits get resent + + bool bTimeBasedDamage = g_pGameRules->Damage_IsTimeBased( bitsDamage ); + while (fTookDamage && (!ftrivial || bTimeBasedDamage) && ffound && bitsDamage) + { + ffound = false; + + if (bitsDamage & DMG_CLUB) + { + if (fmajor) + SetSuitUpdate("!HEV_DMG4", false, SUIT_NEXT_IN_30SEC); // minor fracture + bitsDamage &= ~DMG_CLUB; + ffound = true; + } + if (bitsDamage & (DMG_FALL | DMG_CRUSH)) + { + if (fmajor) + SetSuitUpdate("!HEV_DMG5", false, SUIT_NEXT_IN_30SEC); // major fracture + else + SetSuitUpdate("!HEV_DMG4", false, SUIT_NEXT_IN_30SEC); // minor fracture + + bitsDamage &= ~(DMG_FALL | DMG_CRUSH); + ffound = true; + } + + if (bitsDamage & DMG_BULLET) + { + if (m_lastDamageAmount > 5) + SetSuitUpdate("!HEV_DMG6", false, SUIT_NEXT_IN_30SEC); // blood loss detected + //else + // SetSuitUpdate("!HEV_DMG0", false, SUIT_NEXT_IN_30SEC); // minor laceration + + bitsDamage &= ~DMG_BULLET; + ffound = true; + } + + if (bitsDamage & DMG_SLASH) + { + if (fmajor) + SetSuitUpdate("!HEV_DMG1", false, SUIT_NEXT_IN_30SEC); // major laceration + else + SetSuitUpdate("!HEV_DMG0", false, SUIT_NEXT_IN_30SEC); // minor laceration + + bitsDamage &= ~DMG_SLASH; + ffound = true; + } + + if (bitsDamage & DMG_SONIC) + { + if (fmajor) + SetSuitUpdate("!HEV_DMG2", false, SUIT_NEXT_IN_1MIN); // internal bleeding + bitsDamage &= ~DMG_SONIC; + ffound = true; + } + + if (bitsDamage & (DMG_POISON | DMG_PARALYZE)) + { + if (bitsDamage & DMG_POISON) + { + m_nPoisonDmg += info.GetDamage(); + m_tbdPrev = gpGlobals->curtime; + m_rgbTimeBasedDamage[itbd_PoisonRecover] = 0; + } + + SetSuitUpdate("!HEV_DMG3", false, SUIT_NEXT_IN_1MIN); // blood toxins detected + bitsDamage &= ~( DMG_POISON | DMG_PARALYZE ); + ffound = true; + } + + if (bitsDamage & DMG_ACID) + { + SetSuitUpdate("!HEV_DET1", false, SUIT_NEXT_IN_1MIN); // hazardous chemicals detected + bitsDamage &= ~DMG_ACID; + ffound = true; + } + + if (bitsDamage & DMG_NERVEGAS) + { + SetSuitUpdate("!HEV_DET0", false, SUIT_NEXT_IN_1MIN); // biohazard detected + bitsDamage &= ~DMG_NERVEGAS; + ffound = true; + } + + if (bitsDamage & DMG_RADIATION) + { + SetSuitUpdate("!HEV_DET2", false, SUIT_NEXT_IN_1MIN); // radiation detected + bitsDamage &= ~DMG_RADIATION; + ffound = true; + } + if (bitsDamage & DMG_SHOCK) + { + bitsDamage &= ~DMG_SHOCK; + ffound = true; + } + } + + m_Local.m_vecPunchAngle.SetX( -2 ); + + if (fTookDamage && !ftrivial && fmajor && flHealthPrev >= 75) + { + // first time we take major damage... + // turn automedic on if not on + SetSuitUpdate("!HEV_MED1", false, SUIT_NEXT_IN_30MIN); // automedic on + + // give morphine shot if not given recently + SetSuitUpdate("!HEV_HEAL7", false, SUIT_NEXT_IN_30MIN); // morphine shot + } + + if (fTookDamage && !ftrivial && fcritical && flHealthPrev < 75) + { + + // already took major damage, now it's critical... + if (m_iHealth < 6) + SetSuitUpdate("!HEV_HLTH3", false, SUIT_NEXT_IN_10MIN); // near death + else if (m_iHealth < 20) + SetSuitUpdate("!HEV_HLTH2", false, SUIT_NEXT_IN_10MIN); // health critical + + // give critical health warnings + if (!random->RandomInt(0,3) && flHealthPrev < 50) + SetSuitUpdate("!HEV_DMG7", false, SUIT_NEXT_IN_5MIN); //seek medical attention + } + + // if we're taking time based damage, warn about its continuing effects + if (fTookDamage && g_pGameRules->Damage_IsTimeBased( info.GetDamageType() ) && flHealthPrev < 75) + { + if (flHealthPrev < 50) + { + if (!random->RandomInt(0,3)) + SetSuitUpdate("!HEV_DMG7", false, SUIT_NEXT_IN_5MIN); //seek medical attention + } + else + SetSuitUpdate("!HEV_HLTH1", false, SUIT_NEXT_IN_10MIN); // health dropping + } + + // Do special explosion damage effect + if ( bitsDamage & DMG_BLAST ) + { + OnDamagedByExplosion( info ); + } + + gamestats->Event_PlayerDamage( this, info ); + + return fTookDamage; +} + + +int CHL1_Player::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + int nRet; + int nSavedHealth = m_iHealth; + + // Drown + if( info.GetDamageType() & DMG_DROWN ) + { + if( m_idrowndmg == m_idrownrestored ) + { + EmitSound( "Player.DrownStart" ); + } + else + { + EmitSound( "Player.DrownContinue" ); + } + } + + nRet = BaseClass::OnTakeDamage_Alive( info ); + + if ( GetFlags() & FL_GODMODE ) + { + m_iHealth = nSavedHealth; + } + else if (m_debugOverlays & OVERLAY_BUDDHA_MODE) + { + if ( m_iHealth <= 0 ) + { + m_iHealth = 1; + } + } + + return nRet; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CHL1_Player::FindMissTargets( void ) +{ + if ( m_flTargetFindTime > gpGlobals->curtime ) + return; + + m_flTargetFindTime = gpGlobals->curtime + 1.0f; + m_nNumMissPositions = 0; + + CBaseEntity *pEnts[256]; + Vector radius( 80, 80, 80); + + int numEnts = UTIL_EntitiesInBox( pEnts, 256, GetAbsOrigin()-radius, GetAbsOrigin()+radius, 0 ); + + for ( int i = 0; i < numEnts; i++ ) + { + if ( pEnts[i] == NULL ) + continue; + + if ( m_nNumMissPositions >= 16 ) + return; + + //See if it's a good target candidate + if ( FClassnameIs( pEnts[i], "prop_dynamic" ) || + FClassnameIs( pEnts[i], "dynamic_prop" ) || + FClassnameIs( pEnts[i], "prop_physics" ) || + FClassnameIs( pEnts[i], "physics_prop" ) ) + { + //NDebugOverlay::Cross3D( pEnts[i]->WorldSpaceCenter(), -Vector(4,4,4), Vector(4,4,4), 0, 255, 0, true, 1.0f ); + + m_vecMissPositions[m_nNumMissPositions++] = pEnts[i]->WorldSpaceCenter(); + continue; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Good position to shoot at +//----------------------------------------------------------------------------- +bool CHL1_Player::GetMissPosition( Vector *position ) +{ + if ( m_nNumMissPositions == 0 ) + return false; + + (*position) = m_vecMissPositions[ random->RandomInt( 0, m_nNumMissPositions-1 ) ]; + + return true; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CHL1_Player::FlashlightIsOn( void ) +{ + return IsEffectActive( EF_DIMLIGHT); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CHL1_Player::FlashlightTurnOn( void ) +{ + if ( IsSuitEquipped() ) + { + AddEffects( EF_DIMLIGHT ); + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Player.FlashlightOn" ); + + m_flFlashLightTime = FLASH_DRAIN_TIME + gpGlobals->curtime; + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CHL1_Player::FlashlightTurnOff( void ) +{ + RemoveEffects( EF_DIMLIGHT ); + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Player.FlashlightOff" ); + m_flFlashLightTime = FLASH_CHARGE_TIME + gpGlobals->curtime; +} + + +void CHL1_Player::UpdateClientData( void ) +{ + if (m_DmgTake || m_DmgSave || m_bitsHUDDamage != m_bitsDamageType) + { + // Comes from inside me if not set + Vector damageOrigin = GetLocalOrigin(); + // send "damage" message + // causes screen to flash, and pain compass to show direction of damage + damageOrigin = m_DmgOrigin; + + // only send down damage type that have hud art + int iShowHudDamage = g_pGameRules->Damage_GetShowOnHud(); + int visibleDamageBits = m_bitsDamageType & iShowHudDamage; + + m_DmgTake = clamp( m_DmgTake, 0, 255 ); + m_DmgSave = clamp( m_DmgSave, 0, 255 ); + + CSingleUserRecipientFilter user( this ); + user.MakeReliable(); + UserMessageBegin( user, "Damage" ); + WRITE_BYTE( m_DmgSave ); + WRITE_BYTE( m_DmgTake ); + WRITE_LONG( visibleDamageBits ); + WRITE_FLOAT( damageOrigin.x ); //BUG: Should be fixed point (to hud) not floats + WRITE_FLOAT( damageOrigin.y ); //BUG: However, the HUD does _not_ implement bitfield messages (yet) + WRITE_FLOAT( damageOrigin.z ); //BUG: We use WRITE_VEC3COORD for everything else + MessageEnd(); + + m_DmgTake = 0; + m_DmgSave = 0; + m_bitsHUDDamage = m_bitsDamageType; + + // Clear off non-time-based damage indicators + int iDamage = g_pGameRules->Damage_GetTimeBased(); + m_bitsDamageType &= iDamage; + } + + // Update Flashlight + if ( ( m_flFlashLightTime ) && ( m_flFlashLightTime <= gpGlobals->curtime ) ) + { + if ( FlashlightIsOn() ) + { + if ( m_nFlashBattery ) + { + m_flFlashLightTime = FLASH_DRAIN_TIME + gpGlobals->curtime; + m_nFlashBattery--; + + if ( !m_nFlashBattery ) + FlashlightTurnOff(); + } + } + else + { + if ( m_nFlashBattery < 100 ) + { + m_flFlashLightTime = FLASH_CHARGE_TIME + gpGlobals->curtime; + m_nFlashBattery++; + } + else + m_flFlashLightTime = 0; + } + } + + BaseClass::UpdateClientData(); +} + +void CHL1_Player::OnSave( IEntitySaveUtils *pUtils ) +{ + // If I'm pulling a box, stop. + StopPullingObject(); + + BaseClass::OnSave(pUtils); +} + +void CHL1_Player::CreateViewModel( int index /*=0*/ ) +{ + Assert( index >= 0 && index < MAX_VIEWMODELS ); + + if ( GetViewModel( index ) ) + return; + + CPredictedViewModel *vm = ( CPredictedViewModel * )CreateEntityByName( "predicted_viewmodel" ); + if ( vm ) + { + vm->SetAbsOrigin( GetAbsOrigin() ); + vm->SetOwner( this ); + vm->SetIndex( index ); + DispatchSpawn( vm ); + vm->FollowEntity( this ); + m_hViewModel.Set( index, vm ); + } +} + +void CHL1_Player::OnRestore( void ) +{ + BaseClass::OnRestore(); + + // If we are controlling a train, resend our train status + if( !FBitSet( m_iTrain, TRAIN_OFF ) ) + { + m_iTrain |= TRAIN_NEW; + } +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +static void MatrixOrthogonalize( matrix3x4_t &matrix, int column ) +{ + Vector columns[3]; + int i; + + for ( i = 0; i < 3; i++ ) + { + MatrixGetColumn( matrix, i, columns[i] ); + } + + int index0 = column; + int index1 = (column+1)%3; + int index2 = (column+2)%3; + + columns[index2] = CrossProduct( columns[index0], columns[index1] ); + columns[index1] = CrossProduct( columns[index2], columns[index0] ); + VectorNormalize( columns[index2] ); + VectorNormalize( columns[index1] ); + MatrixSetColumn( columns[index1], index1, matrix ); + MatrixSetColumn( columns[index2], index2, matrix ); +} + +#define SIGN(x) ( (x) < 0 ? -1 : 1 ) + +static QAngle AlignAngles( const QAngle &angles, float cosineAlignAngle ) +{ + matrix3x4_t alignMatrix; + AngleMatrix( angles, alignMatrix ); + + for ( int j = 0; j < 3; j++ ) + { + Vector vec; + MatrixGetColumn( alignMatrix, j, vec ); + for ( int i = 0; i < 3; i++ ) + { + if ( fabs(vec[i]) > cosineAlignAngle ) + { + vec[i] = SIGN(vec[i]); + vec[(i+1)%3] = 0; + vec[(i+2)%3] = 0; + MatrixSetColumn( vec, j, alignMatrix ); + MatrixOrthogonalize( alignMatrix, j ); + break; + } + } + } + + QAngle out; + MatrixAngles( alignMatrix, out ); + return out; +} + + +static void TraceCollideAgainstBBox( const CPhysCollide *pCollide, const Vector &start, const Vector &end, const QAngle &angles, const Vector &boxOrigin, const Vector &mins, const Vector &maxs, trace_t *ptr ) +{ + physcollision->TraceBox( boxOrigin, boxOrigin + (start-end), mins, maxs, pCollide, start, angles, ptr ); + + if ( ptr->DidHit() ) + { + ptr->endpos = start * (1-ptr->fraction) + end * ptr->fraction; + ptr->startpos = start; + ptr->plane.dist = -ptr->plane.dist; + ptr->plane.normal *= -1; + } +} + +//--------------------------------------------- + +#include "player_pickup.h" +#include "props.h" +#include "vphysics/friction.h" +#include "physics_saverestore.h" +ConVar hl2_normspeed( "hl2_normspeed", "190" ); +ConVar player_throwforce( "player_throwforce", "1000" ); +ConVar physcannon_maxmass( "physcannon_maxmass", "250" ); + +// derive from this so we can add save/load data to it +struct game_shadowcontrol_params_t : public hlshadowcontrol_params_t +{ + DECLARE_SIMPLE_DATADESC(); +}; + +BEGIN_SIMPLE_DATADESC( game_shadowcontrol_params_t ) + + DEFINE_FIELD( targetPosition, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( targetRotation, FIELD_VECTOR ), + DEFINE_FIELD( maxAngular, FIELD_FLOAT ), + DEFINE_FIELD( maxDampAngular, FIELD_FLOAT ), + DEFINE_FIELD( maxSpeed, FIELD_FLOAT ), + DEFINE_FIELD( maxDampSpeed, FIELD_FLOAT ), + DEFINE_FIELD( dampFactor, FIELD_FLOAT ), + DEFINE_FIELD( teleportDistance, FIELD_FLOAT ), + +END_DATADESC() + +// when looking level, hold bottom of object 8 inches below eye level +#define PLAYER_HOLD_LEVEL_EYES -8 + +// when looking down, hold bottom of object 0 inches from feet +#define PLAYER_HOLD_DOWN_FEET 2 + +// when looking up, hold bottom of object 24 inches above eye level +#define PLAYER_HOLD_UP_EYES 24 + +// use a +/-30 degree range for the entire range of motion of pitch +#define PLAYER_LOOK_PITCH_RANGE 30 + +// player can reach down 2ft below his feet (otherwise he'll hold the object above the bottom) +#define PLAYER_REACH_DOWN_DISTANCE 24 + +static void ComputePlayerMatrix( CBasePlayer *pPlayer, matrix3x4_t &out ) +{ + if ( !pPlayer ) + return; + + QAngle angles = pPlayer->EyeAngles(); + Vector origin = pPlayer->EyePosition(); + + // 0-360 / -180-180 + //angles.x = init ? 0 : AngleDistance( angles.x, 0 ); + //angles.x = clamp( angles.x, -PLAYER_LOOK_PITCH_RANGE, PLAYER_LOOK_PITCH_RANGE ); + angles.x = 0; + + float feet = pPlayer->GetAbsOrigin().z + pPlayer->WorldAlignMins().z; + float eyes = origin.z; + float zoffset = 0; + // moving up (negative pitch is up) + if ( angles.x < 0 ) + { + zoffset = RemapVal( angles.x, 0, -PLAYER_LOOK_PITCH_RANGE, PLAYER_HOLD_LEVEL_EYES, PLAYER_HOLD_UP_EYES ); + } + else + { + zoffset = RemapVal( angles.x, 0, PLAYER_LOOK_PITCH_RANGE, PLAYER_HOLD_LEVEL_EYES, PLAYER_HOLD_DOWN_FEET + (feet - eyes) ); + } + origin.z += zoffset; + angles.x = 0; + AngleMatrix( angles, origin, out ); +} + +//----------------------------------------------------------------------------- +class CGrabController : public IMotionEvent +{ + DECLARE_SIMPLE_DATADESC(); + +public: + + CGrabController( void ); + ~CGrabController( void ); + void AttachEntity( CBasePlayer *pPlayer, CBaseEntity *pEntity, IPhysicsObject *pPhys, bool bIsMegaPhysCannon = false ); + void DetachEntity(); + void OnRestore(); + + bool UpdateObject( CBasePlayer *pPlayer, float flError ); + + void SetTargetPosition( const Vector &target, const QAngle &targetOrientation ); + float ComputeError(); + float GetLoadWeight( void ) const { return m_flLoadWeight; } + void SetAngleAlignment( float alignAngleCosine ) { m_angleAlignment = alignAngleCosine; } + void SetIgnorePitch( bool bIgnore ) { m_bIgnoreRelativePitch = bIgnore; } + QAngle TransformAnglesToPlayerSpace( const QAngle &anglesIn, CBasePlayer *pPlayer ); + QAngle TransformAnglesFromPlayerSpace( const QAngle &anglesIn, CBasePlayer *pPlayer ); + + CBaseEntity *GetAttached() { return (CBaseEntity *)m_attachedEntity; } + + IMotionEvent::simresult_e Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ); + float GetSavedMass( IPhysicsObject *pObject ); + +private: + // Compute the max speed for an attached object + void ComputeMaxSpeed( CBaseEntity *pEntity, IPhysicsObject *pPhysics ); + + game_shadowcontrol_params_t m_shadow; + float m_timeToArrive; + float m_errorTime; + float m_error; + float m_contactAmount; + float m_angleAlignment; + bool m_bCarriedEntityBlocksLOS; + bool m_bIgnoreRelativePitch; + + float m_flLoadWeight; + float m_savedRotDamping[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + float m_savedMass[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + EHANDLE m_attachedEntity; + QAngle m_vecPreferredCarryAngles; + bool m_bHasPreferredCarryAngles; + + QAngle m_attachedAnglesPlayerSpace; + Vector m_attachedPositionObjectSpace; + + IPhysicsMotionController *m_controller; + + friend class CWeaponPhysCannon; +}; + +BEGIN_SIMPLE_DATADESC( CGrabController ) + + DEFINE_EMBEDDED( m_shadow ), + + DEFINE_FIELD( m_timeToArrive, FIELD_FLOAT ), + DEFINE_FIELD( m_errorTime, FIELD_FLOAT ), + DEFINE_FIELD( m_error, FIELD_FLOAT ), + DEFINE_FIELD( m_contactAmount, FIELD_FLOAT ), + DEFINE_AUTO_ARRAY( m_savedRotDamping, FIELD_FLOAT ), + DEFINE_AUTO_ARRAY( m_savedMass, FIELD_FLOAT ), + DEFINE_FIELD( m_flLoadWeight, FIELD_FLOAT ), + DEFINE_FIELD( m_bCarriedEntityBlocksLOS, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bIgnoreRelativePitch, FIELD_BOOLEAN ), + DEFINE_FIELD( m_attachedEntity, FIELD_EHANDLE ), + DEFINE_FIELD( m_angleAlignment, FIELD_FLOAT ), + DEFINE_FIELD( m_vecPreferredCarryAngles, FIELD_VECTOR ), + DEFINE_FIELD( m_bHasPreferredCarryAngles, FIELD_BOOLEAN ), + DEFINE_FIELD( m_attachedAnglesPlayerSpace, FIELD_VECTOR ), + DEFINE_FIELD( m_attachedPositionObjectSpace, FIELD_VECTOR ), + + // Physptrs can't be inside embedded classes + // DEFINE_PHYSPTR( m_controller ), + +END_DATADESC() + +const float DEFAULT_MAX_ANGULAR = 360.0f * 10.0f; +const float REDUCED_CARRY_MASS = 1.0f; + +CGrabController::CGrabController( void ) +{ + m_shadow.dampFactor = 1.0; + m_shadow.teleportDistance = 0; + m_errorTime = 0; + m_error = 0; + // make this controller really stiff! + m_shadow.maxSpeed = 1000; + m_shadow.maxAngular = DEFAULT_MAX_ANGULAR; + m_shadow.maxDampSpeed = m_shadow.maxSpeed*2; + m_shadow.maxDampAngular = m_shadow.maxAngular; + m_attachedEntity = NULL; + m_vecPreferredCarryAngles = vec3_angle; + m_bHasPreferredCarryAngles = false; +} + +CGrabController::~CGrabController( void ) +{ + DetachEntity(); +} + +void CGrabController::OnRestore() +{ + if ( m_controller ) + { + m_controller->SetEventHandler( this ); + } +} + +void CGrabController::SetTargetPosition( const Vector &target, const QAngle &targetOrientation ) +{ + m_shadow.targetPosition = target; + m_shadow.targetRotation = targetOrientation; + + m_timeToArrive = gpGlobals->frametime; + + CBaseEntity *pAttached = GetAttached(); + if ( pAttached ) + { + IPhysicsObject *pObj = pAttached->VPhysicsGetObject(); + + if ( pObj != NULL ) + { + pObj->Wake(); + } + else + { + DetachEntity(); + } + } +} + +float CGrabController::ComputeError() +{ + if ( m_errorTime <= 0 ) + return 0; + + CBaseEntity *pAttached = GetAttached(); + if ( pAttached ) + { + Vector pos; + IPhysicsObject *pObj = pAttached->VPhysicsGetObject(); + if ( pObj ) + { + pObj->GetShadowPosition( &pos, NULL ); + float error = (m_shadow.targetPosition - pos).Length(); + if ( m_errorTime > 0 ) + { + if ( m_errorTime > 1 ) + { + m_errorTime = 1; + } + float speed = error / m_errorTime; + if ( speed > m_shadow.maxSpeed ) + { + error *= 0.5; + } + m_error = (1-m_errorTime) * m_error + error * m_errorTime; + } + } + else + { + DevMsg( "Object attached to Physcannon has no physics object\n" ); + DetachEntity(); + return 9999; // force detach + } + } + + if ( pAttached->IsEFlagSet( EFL_IS_BEING_LIFTED_BY_BARNACLE ) ) + { + m_error *= 3.0f; + } + + m_errorTime = 0; + + return m_error; +} + + +#define MASS_SPEED_SCALE 60 +#define MAX_MASS 40 + + +void CGrabController::ComputeMaxSpeed( CBaseEntity *pEntity, IPhysicsObject *pPhysics ) +{ + m_shadow.maxSpeed = 1000; + m_shadow.maxAngular = DEFAULT_MAX_ANGULAR; + + // Compute total mass... + float flMass = PhysGetEntityMass( pEntity ); + float flMaxMass = physcannon_maxmass.GetFloat(); + if ( flMass <= flMaxMass ) + return; + + float flLerpFactor = clamp( flMass, flMaxMass, 500.0f ); + flLerpFactor = SimpleSplineRemapVal( flLerpFactor, flMaxMass, 500.0f, 0.0f, 1.0f ); + + float invMass = pPhysics->GetInvMass(); + float invInertia = pPhysics->GetInvInertia().Length(); + + float invMaxMass = 1.0f / MAX_MASS; + float ratio = invMaxMass / invMass; + invMass = invMaxMass; + invInertia *= ratio; + + float maxSpeed = invMass * MASS_SPEED_SCALE * 200; + float maxAngular = invInertia * MASS_SPEED_SCALE * 360; + + m_shadow.maxSpeed = Lerp( flLerpFactor, m_shadow.maxSpeed, maxSpeed ); + m_shadow.maxAngular = Lerp( flLerpFactor, m_shadow.maxAngular, maxAngular ); +} + + +QAngle CGrabController::TransformAnglesToPlayerSpace( const QAngle &anglesIn, CBasePlayer *pPlayer ) +{ + if ( m_bIgnoreRelativePitch ) + { + matrix3x4_t test; + QAngle angleTest = pPlayer->EyeAngles(); + angleTest.x = 0; + AngleMatrix( angleTest, test ); + return TransformAnglesToLocalSpace( anglesIn, test ); + } + return TransformAnglesToLocalSpace( anglesIn, pPlayer->EntityToWorldTransform() ); +} + +QAngle CGrabController::TransformAnglesFromPlayerSpace( const QAngle &anglesIn, CBasePlayer *pPlayer ) +{ + if ( m_bIgnoreRelativePitch ) + { + matrix3x4_t test; + QAngle angleTest = pPlayer->EyeAngles(); + angleTest.x = 0; + AngleMatrix( angleTest, test ); + return TransformAnglesToWorldSpace( anglesIn, test ); + } + return TransformAnglesToWorldSpace( anglesIn, pPlayer->EntityToWorldTransform() ); +} + + +void CGrabController::AttachEntity( CBasePlayer *pPlayer, CBaseEntity *pEntity, IPhysicsObject *pPhys, bool bIsMegaPhysCannon ) +{ + // play the impact sound of the object hitting the player + // used as feedback to let the player know he picked up the object + PhysicsImpactSound( pPlayer, pPhys, CHAN_STATIC, pPhys->GetMaterialIndex(), pPlayer->VPhysicsGetObject()->GetMaterialIndex(), 1.0, 64 ); + Vector position; + QAngle angles; + pPhys->GetPosition( &position, &angles ); + // If it has a preferred orientation, use that instead. + Pickup_GetPreferredCarryAngles( pEntity, pPlayer, pPlayer->EntityToWorldTransform(), angles ); + +// ComputeMaxSpeed( pEntity, pPhys ); + + // Carried entities can never block LOS + m_bCarriedEntityBlocksLOS = pEntity->BlocksLOS(); + pEntity->SetBlocksLOS( false ); + m_controller = physenv->CreateMotionController( this ); + m_controller->AttachObject( pPhys, true ); + // Don't do this, it's causing trouble with constraint solvers. + //m_controller->SetPriority( IPhysicsMotionController::HIGH_PRIORITY ); + + pPhys->Wake(); + PhysSetGameFlags( pPhys, FVPHYSICS_PLAYER_HELD ); + SetTargetPosition( position, angles ); + m_attachedEntity = pEntity; + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int count = pEntity->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + m_flLoadWeight = 0; + float damping = 10; + float flFactor = count / 7.5f; + if ( flFactor < 1.0f ) + { + flFactor = 1.0f; + } + for ( int i = 0; i < count; i++ ) + { + float mass = pList[i]->GetMass(); + pList[i]->GetDamping( NULL, &m_savedRotDamping[i] ); + m_flLoadWeight += mass; + m_savedMass[i] = mass; + + // reduce the mass to prevent the player from adding crazy amounts of energy to the system + pList[i]->SetMass( REDUCED_CARRY_MASS / flFactor ); + pList[i]->SetDamping( NULL, &damping ); + } + + // Give extra mass to the phys object we're actually picking up + pPhys->SetMass( REDUCED_CARRY_MASS ); + pPhys->EnableDrag( false ); + + m_errorTime = bIsMegaPhysCannon ? -1.5f : -1.0f; // 1 seconds until error starts accumulating + m_error = 0; + m_contactAmount = 0; + + m_attachedAnglesPlayerSpace = TransformAnglesToPlayerSpace( angles, pPlayer ); + if ( m_angleAlignment != 0 ) + { + m_attachedAnglesPlayerSpace = AlignAngles( m_attachedAnglesPlayerSpace, m_angleAlignment ); + } + + VectorITransform( pEntity->WorldSpaceCenter(), pEntity->EntityToWorldTransform(), m_attachedPositionObjectSpace ); + + // If it's a prop, see if it has desired carry angles + CPhysicsProp *pProp = dynamic_cast<CPhysicsProp *>(pEntity); + if ( pProp ) + { + m_bHasPreferredCarryAngles = pProp->GetPropDataAngles( "preferred_carryangles", m_vecPreferredCarryAngles ); + } + else + { + m_bHasPreferredCarryAngles = false; + } +} + +static void ClampPhysicsVelocity( IPhysicsObject *pPhys, float linearLimit, float angularLimit ) +{ + Vector vel; + AngularImpulse angVel; + pPhys->GetVelocity( &vel, &angVel ); + float speed = VectorNormalize(vel) - linearLimit; + float angSpeed = VectorNormalize(angVel) - angularLimit; + speed = speed < 0 ? 0 : -speed; + angSpeed = angSpeed < 0 ? 0 : -angSpeed; + vel *= speed; + angVel *= angSpeed; + pPhys->AddVelocity( &vel, &angVel ); +} + +void CGrabController::DetachEntity() +{ + CBaseEntity *pEntity = GetAttached(); + if ( pEntity ) + { + // Restore the LS blocking state + pEntity->SetBlocksLOS( m_bCarriedEntityBlocksLOS ); + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int count = pEntity->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + for ( int i = 0; i < count; i++ ) + { + IPhysicsObject *pPhys = pList[i]; + if ( !pPhys ) + continue; + + // on the odd chance that it's gone to sleep while under anti-gravity + pPhys->EnableDrag( true ); + pPhys->Wake(); + pPhys->SetMass( m_savedMass[i] ); + pPhys->SetDamping( NULL, &m_savedRotDamping[i] ); + PhysClearGameFlags( pPhys, FVPHYSICS_PLAYER_HELD ); + if ( pPhys->GetContactPoint( NULL, NULL ) ) + { + PhysForceClearVelocity( pPhys ); + } + else + { + ClampPhysicsVelocity( pPhys, hl2_normspeed.GetFloat() * 1.5f, 2.0f * 360.0f ); + } + + } + } + + m_attachedEntity = NULL; + physenv->DestroyMotionController( m_controller ); + m_controller = NULL; +} + +static bool InContactWithHeavyObject( IPhysicsObject *pObject, float heavyMass ) +{ + bool contact = false; + IPhysicsFrictionSnapshot *pSnapshot = pObject->CreateFrictionSnapshot(); + while ( pSnapshot->IsValid() ) + { + IPhysicsObject *pOther = pSnapshot->GetObject( 1 ); + if ( !pOther->IsMoveable() || pOther->GetMass() > heavyMass ) + { + contact = true; + break; + } + pSnapshot->NextFrictionData(); + } + pObject->DestroyFrictionSnapshot( pSnapshot ); + return contact; +} + +IMotionEvent::simresult_e CGrabController::Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ) +{ + game_shadowcontrol_params_t shadowParams = m_shadow; + if ( InContactWithHeavyObject( pObject, GetLoadWeight() ) ) + { + m_contactAmount = Approach( 0.1f, m_contactAmount, deltaTime*2.0f ); + } + else + { + m_contactAmount = Approach( 1.0f, m_contactAmount, deltaTime*2.0f ); + } + shadowParams.maxAngular = m_shadow.maxAngular * m_contactAmount * m_contactAmount * m_contactAmount; + m_timeToArrive = pObject->ComputeShadowControl( shadowParams, m_timeToArrive, deltaTime ); + + // Slide along the current contact points to fix bouncing problems + Vector velocity; + AngularImpulse angVel; + pObject->GetVelocity( &velocity, &angVel ); + PhysComputeSlideDirection( pObject, velocity, angVel, &velocity, &angVel, GetLoadWeight() ); + pObject->SetVelocityInstantaneous( &velocity, NULL ); + + linear.Init(); + angular.Init(); + m_errorTime += deltaTime; + + return SIM_LOCAL_ACCELERATION; +} + +float CGrabController::GetSavedMass( IPhysicsObject *pObject ) +{ + CBaseEntity *pHeld = m_attachedEntity; + if ( pHeld ) + { + if ( pObject->GetGameData() == (void*)pHeld ) + { + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int count = pHeld->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + for ( int i = 0; i < count; i++ ) + { + if ( pList[i] == pObject ) + return m_savedMass[i]; + } + } + } + return 0.0f; +} + +bool CGrabController::UpdateObject( CBasePlayer *pPlayer, float flError ) +{ + CBaseEntity *pEntity = GetAttached(); + if ( !pEntity || ComputeError() > flError || pPlayer->GetGroundEntity() == pEntity || !pEntity->VPhysicsGetObject() ) + { + return false; + } + + //Adrian: Oops, our object became motion disabled, let go! + IPhysicsObject *pPhys = pEntity->VPhysicsGetObject(); + if ( pPhys && pPhys->IsMoveable() == false ) + { + return false; + } + + Vector forward, right, up; + QAngle playerAngles = pPlayer->EyeAngles(); + float pitch = AngleDistance(playerAngles.x,0); + playerAngles.x = clamp( pitch, -75, 75 ); + AngleVectors( playerAngles, &forward, &right, &up ); + + // Now clamp a sphere of object radius at end to the player's bbox + Vector radial = physcollision->CollideGetExtent( pPhys->GetCollide(), vec3_origin, pEntity->GetAbsAngles(), -forward ); + Vector player2d = pPlayer->CollisionProp()->OBBMaxs(); + float playerRadius = player2d.Length2D(); + float radius = playerRadius + fabs(DotProduct( forward, radial )); + + float distance = 24 + ( radius * 2.0f ); + + Vector start = pPlayer->Weapon_ShootPosition(); + Vector end = start + ( forward * distance ); + + trace_t tr; + CTraceFilterSkipTwoEntities traceFilter( pPlayer, pEntity, COLLISION_GROUP_NONE ); + Ray_t ray; + ray.Init( start, end ); + enginetrace->TraceRay( ray, MASK_SOLID_BRUSHONLY, &traceFilter, &tr ); + + if ( tr.fraction < 0.5 ) + { + end = start + forward * (radius*0.5f); + } + else if ( tr.fraction <= 1.0f ) + { + end = start + forward * ( distance - radius ); + } + Vector playerMins, playerMaxs, nearest; + pPlayer->CollisionProp()->WorldSpaceAABB( &playerMins, &playerMaxs ); + Vector playerLine = pPlayer->CollisionProp()->WorldSpaceCenter(); + CalcClosestPointOnLine( end, playerLine+Vector(0,0,playerMins.z), playerLine+Vector(0,0,playerMaxs.z), nearest, NULL ); + + Vector delta = end - nearest; + float len = VectorNormalize(delta); + if ( len < radius ) + { + end = nearest + radius * delta; + } +/* + //Show overlays of radius + if ( g_debug_physcannon.GetBool() ) + { + NDebugOverlay::Box( end, -Vector( 2,2,2 ), Vector(2,2,2), 0, 255, 0, true, 0 ); + + NDebugOverlay::Box( GetAttached()->WorldSpaceCenter(), + -Vector( radius, radius, radius), + Vector( radius, radius, radius ), + 255, 0, 0, + true, + 0.0f ); + } +*/ + + QAngle angles = TransformAnglesFromPlayerSpace( m_attachedAnglesPlayerSpace, pPlayer ); + + // If it has a preferred orientation, update to ensure we're still oriented correctly. + Pickup_GetPreferredCarryAngles( pEntity, pPlayer, pPlayer->EntityToWorldTransform(), angles ); + + // We may be holding a prop that has preferred carry angles + if ( m_bHasPreferredCarryAngles ) + { + matrix3x4_t tmp; + ComputePlayerMatrix( pPlayer, tmp ); + angles = TransformAnglesToWorldSpace( m_vecPreferredCarryAngles, tmp ); + } + + matrix3x4_t attachedToWorld; + Vector offset; + AngleMatrix( angles, attachedToWorld ); + VectorRotate( m_attachedPositionObjectSpace, attachedToWorld, offset ); + + SetTargetPosition( end - offset, angles ); + return true; +} + +//----------------------------------------------------------------------------- +// Player pickup controller +//----------------------------------------------------------------------------- + +class CPlayerPickupController : public CBaseEntity +{ + DECLARE_DATADESC(); + DECLARE_CLASS( CPlayerPickupController, CBaseEntity ); +public: + void Init( CBasePlayer *pPlayer, CBaseEntity *pObject ); + void Shutdown( bool bThrown = false ); + bool OnControls( CBaseEntity *pControls ) { return true; } + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void OnRestore() + { + m_grabController.OnRestore(); + } + void VPhysicsUpdate( IPhysicsObject *pPhysics ){} + void VPhysicsShadowUpdate( IPhysicsObject *pPhysics ) {} + + bool IsHoldingEntity( CBaseEntity *pEnt ); + CGrabController &GetGrabController() { return m_grabController; } + +private: + CGrabController m_grabController; + CBasePlayer *m_pPlayer; +}; + +LINK_ENTITY_TO_CLASS( player_pickup, CPlayerPickupController ); + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CPlayerPickupController ) + + DEFINE_EMBEDDED( m_grabController ), + + // Physptrs can't be inside embedded classes + DEFINE_PHYSPTR( m_grabController.m_controller ), + + DEFINE_FIELD( m_pPlayer, FIELD_CLASSPTR ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPlayer - +// *pObject - +//----------------------------------------------------------------------------- +void CPlayerPickupController::Init( CBasePlayer *pPlayer, CBaseEntity *pObject ) +{ + // Holster player's weapon + if ( pPlayer->GetActiveWeapon() ) + { + if ( !pPlayer->GetActiveWeapon()->Holster() ) + { + Shutdown(); + return; + } + } + + // If the target is debris, convert it to non-debris + if ( pObject->GetCollisionGroup() == COLLISION_GROUP_DEBRIS ) + { + // Interactive debris converts back to debris when it comes to rest + pObject->SetCollisionGroup( COLLISION_GROUP_INTERACTIVE_DEBRIS ); + } + + // done so I'll go across level transitions with the player + SetParent( pPlayer ); + m_grabController.SetIgnorePitch( true ); + m_grabController.SetAngleAlignment( DOT_30DEGREE ); + m_pPlayer = pPlayer; + IPhysicsObject *pPhysics = pObject->VPhysicsGetObject(); + Pickup_OnPhysGunPickup( pObject, m_pPlayer ); + m_grabController.AttachEntity( pPlayer, pObject, pPhysics ); + + m_pPlayer->m_Local.m_iHideHUD |= HIDEHUD_WEAPONSELECTION; + m_pPlayer->SetUseEntity( this ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : bool - +//----------------------------------------------------------------------------- +void CPlayerPickupController::Shutdown( bool bThrown ) +{ + CBaseEntity *pObject = m_grabController.GetAttached(); + + m_grabController.DetachEntity(); + + if ( pObject != NULL ) + { + Pickup_OnPhysGunDrop( pObject, m_pPlayer, bThrown ? THROWN_BY_PLAYER : DROPPED_BY_PLAYER ); + } + + if ( m_pPlayer ) + { + m_pPlayer->SetUseEntity( NULL ); + if ( m_pPlayer->GetActiveWeapon() ) + { + m_pPlayer->GetActiveWeapon()->Deploy(); + } + + m_pPlayer->m_Local.m_iHideHUD &= ~HIDEHUD_WEAPONSELECTION; + } + Remove(); +} + + +void CPlayerPickupController::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( ToBasePlayer(pActivator) == m_pPlayer ) + { + CBaseEntity *pAttached = m_grabController.GetAttached(); + + // UNDONE: Use vphysics stress to decide to drop objects + // UNDONE: Must fix case of forcing objects into the ground you're standing on (causes stress) before that will work + if ( !pAttached || useType == USE_OFF || (m_pPlayer->m_nButtons & IN_ATTACK2) || m_grabController.ComputeError() > 12 ) + { + Shutdown(); + return; + } + + //Adrian: Oops, our object became motion disabled, let go! + IPhysicsObject *pPhys = pAttached->VPhysicsGetObject(); + if ( pPhys && pPhys->IsMoveable() == false ) + { + Shutdown(); + return; + } + +#if STRESS_TEST + vphysics_objectstress_t stress; + CalculateObjectStress( pAttached->VPhysicsGetObject(), pAttached, &stress ); + if ( stress.exertedStress > 250 ) + { + Shutdown(); + return; + } +#endif + // +ATTACK will throw phys objects + if ( m_pPlayer->m_nButtons & IN_ATTACK ) + { + Shutdown( true ); + Vector vecLaunch; + m_pPlayer->EyeVectors( &vecLaunch ); + // JAY: Scale this with mass because some small objects really go flying + float massFactor = clamp( pAttached->VPhysicsGetObject()->GetMass(), 0.5, 15 ); + massFactor = RemapVal( massFactor, 0.5, 15, 0.5, 4 ); + vecLaunch *= player_throwforce.GetFloat() * massFactor; + + pAttached->VPhysicsGetObject()->ApplyForceCenter( vecLaunch ); + AngularImpulse aVel = RandomAngularImpulse( -10, 10 ) * massFactor; + pAttached->VPhysicsGetObject()->ApplyTorqueCenter( aVel ); + return; + } + + if ( useType == USE_SET ) + { + // update position + m_grabController.UpdateObject( m_pPlayer, 12 ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pEnt - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CPlayerPickupController::IsHoldingEntity( CBaseEntity *pEnt ) +{ + return ( m_grabController.GetAttached() == pEnt ); +} + +ConVar hl1_new_pull( "hl1_new_pull", "1" ); +void PlayerPickupObject( CBasePlayer *pPlayer, CBaseEntity *pObject ) +{ + if( hl1_new_pull.GetBool() ) + { + CHL1_Player *pHL1Player = dynamic_cast<CHL1_Player*>(pPlayer); + if( pHL1Player && !pHL1Player->IsPullingObject() ) + { + pHL1Player->StartPullingObject(pObject); + } + } +} diff --git a/game/server/hl1/hl1_player.h b/game/server/hl1/hl1_player.h new file mode 100644 index 0000000..03ddf9d --- /dev/null +++ b/game/server/hl1/hl1_player.h @@ -0,0 +1,167 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Player for HL1. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef HL1_PLAYER_H +#define HL1_PLAYER_H +#pragma once + + +#include "player.h" + +extern int TrainSpeed(int iSpeed, int iMax); +extern void CopyToBodyQue( CBaseAnimating *pCorpse ); + +enum HL1PlayerPhysFlag_e +{ + // 1 -- 5 are used by enum PlayerPhysFlag_e in player.h + + PFLAG_ONBARNACLE = ( 1<<6 ) // player is hangning from the barnalce +}; + +class IPhysicsPlayerController; + + +//============================================================================= +//============================================================================= +class CSuitPowerDevice +{ +public: + CSuitPowerDevice( int bitsID, float flDrainRate ) { m_bitsDeviceID = bitsID; m_flDrainRate = flDrainRate; } +private: + int m_bitsDeviceID; // tells what the device is. DEVICE_SPRINT, DEVICE_FLASHLIGHT, etc. BITMASK!!!!! + float m_flDrainRate; // how quickly does this device deplete suit power? ( percent per second ) + +public: + int GetDeviceID( void ) const { return m_bitsDeviceID; } + float GetDeviceDrainRate( void ) const { return m_flDrainRate; } +}; + +//============================================================================= +// >> HL1_PLAYER +//============================================================================= +class CHL1_Player : public CBasePlayer +{ + DECLARE_CLASS( CHL1_Player, CBasePlayer ); + DECLARE_SERVERCLASS(); +public: + + DECLARE_DATADESC(); + + CHL1_Player(); + ~CHL1_Player( void ); + + static CHL1_Player *CreatePlayer( const char *className, edict_t *ed ) + { + CHL1_Player::s_PlayerEdict = ed; + return (CHL1_Player*)CreateEntityByName( className ); + } + + void CreateCorpse( void ) { CopyToBodyQue( this ); }; + + void Precache( void ); + void Spawn(void); + void Event_Killed( const CTakeDamageInfo &info ); + void CheatImpulseCommands( int iImpulse ); + void PlayerRunCommand( CUserCmd *ucmd, IMoveHelper *moveHelper ); + void UpdateClientData( void ); + void OnSave( IEntitySaveUtils *pUtils ); + + void CheckTimeBasedDamage( void ); + + // from cbasecombatcharacter + void InitVCollision( const Vector &vecAbsOrigin, const Vector &vecAbsVelocity ); + + Class_T Classify ( void ); + Class_T m_nControlClass; // Class when player is controlling another entity + + // from CBasePlayer + void SetupVisibility( CBaseEntity *pViewEntity, unsigned char *pvs, int pvssize ); + + // Aiming heuristics accessors + float GetIdleTime( void ) const { return ( m_flIdleTime - m_flMoveTime ); } + float GetMoveTime( void ) const { return ( m_flMoveTime - m_flIdleTime ); } + float GetLastDamageTime( void ) const { return m_flLastDamageTime; } + bool IsDucking( void ) const { return !!( GetFlags() & FL_DUCKING ); } + + int OnTakeDamage( const CTakeDamageInfo &info ); + int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + void FindMissTargets( void ); + bool GetMissPosition( Vector *position ); + + void OnDamagedByExplosion( const CTakeDamageInfo &info ) { }; + void PlayerPickupObject( CBasePlayer *pPlayer, CBaseEntity *pObject ); + + virtual void CreateViewModel( int index /*=0*/ ); + + virtual CBaseEntity *GiveNamedItem( const char *pszName, int iSubType = 0 ); + + virtual void OnRestore( void ); + + bool IsPullingObject() { return m_bIsPullingObject; } + void StartPullingObject( CBaseEntity *pObject ); + void StopPullingObject(); + void UpdatePullingObject(); + + +protected: + void PreThink( void ); + bool HandleInteraction(int interactionType, void *data, CBaseCombatCharacter* sourceEnt); + +private: + Vector m_vecMissPositions[16]; + int m_nNumMissPositions; + + // Aiming heuristics code + float m_flIdleTime; //Amount of time we've been motionless + float m_flMoveTime; //Amount of time we've been in motion + float m_flLastDamageTime; //Last time we took damage + float m_flTargetFindTime; + + EHANDLE m_hPullObject; + IPhysicsConstraint *m_pPullConstraint; + + +public: + + // Flashlight Device + int FlashlightIsOn( void ); + void FlashlightTurnOn( void ); + void FlashlightTurnOff( void ); + float m_flFlashLightTime; // Time until next battery draw/Recharge + CNetworkVar( int, m_nFlashBattery ); // Flashlight Battery Draw + + // For gauss weapon +// float m_flStartCharge; +// float m_flAmmoStartCharge; +// float m_flPlayAftershock; +// float m_flNextAmmoBurn; // while charging, when to absorb another unit of player's ammo? + + CNetworkVar( float, m_flStartCharge ); + CNetworkVar( float, m_flAmmoStartCharge ); + CNetworkVar( float, m_flPlayAftershock ); + CNetworkVar( float, m_flNextAmmoBurn ); // while charging, when to absorb another unit of player's ammo? + + CNetworkVar( bool, m_bHasLongJump ); + CNetworkVar( bool, m_bIsPullingObject ); +}; + + +//----------------------------------------------------------------------------- +// Converts an entity to a HL1 player +//----------------------------------------------------------------------------- +inline CHL1_Player *ToHL1Player( CBaseEntity *pEntity ) +{ + if ( !pEntity || !pEntity->IsPlayer() ) + return NULL; +#if _DEBUG + return dynamic_cast<CHL1_Player *>( pEntity ); +#else + return static_cast<CHL1_Player *>( pEntity ); +#endif +} + +#endif //HL1_PLAYER_H diff --git a/game/server/hl1/hl1_playermove.cpp b/game/server/hl1/hl1_playermove.cpp new file mode 100644 index 0000000..5ec5a64 --- /dev/null +++ b/game/server/hl1/hl1_playermove.cpp @@ -0,0 +1,76 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "player_command.h" +#include "igamemovement.h" +#include "in_buttons.h" +#include "ipredictionsystem.h" +#include "hl1_player.h" + + +static CMoveData g_MoveData; +CMoveData *g_pMoveData = &g_MoveData; + +IPredictionSystem *IPredictionSystem::g_pPredictionSystems = NULL; + + +//----------------------------------------------------------------------------- +// Sets up the move data for Halflife 1 +//----------------------------------------------------------------------------- +class CHL1PlayerMove : public CPlayerMove +{ +DECLARE_CLASS( CHL1PlayerMove, CPlayerMove ); + +public: + virtual void StartCommand( CBasePlayer *player, CUserCmd *cmd ); + virtual void SetupMove( CBasePlayer *player, CUserCmd *ucmd, IMoveHelper *pHelper, CMoveData *move ); + virtual void FinishMove( CBasePlayer *player, CUserCmd *ucmd, CMoveData *move ); +}; + +// PlayerMove Interface +static CHL1PlayerMove g_PlayerMove; + +//----------------------------------------------------------------------------- +// Singleton accessor +//----------------------------------------------------------------------------- +CPlayerMove *PlayerMove() +{ + return &g_PlayerMove; +} + +//----------------------------------------------------------------------------- +// Main setup, finish +//----------------------------------------------------------------------------- + +void CHL1PlayerMove::StartCommand( CBasePlayer *player, CUserCmd *cmd ) +{ + BaseClass::StartCommand( player, cmd ); +} + +//----------------------------------------------------------------------------- +// Purpose: This is called pre player movement and copies all the data necessary +// from the player for movement. (Server-side, the client-side version +// of this code can be found in prediction.cpp.) +//----------------------------------------------------------------------------- +void CHL1PlayerMove::SetupMove( CBasePlayer *player, CUserCmd *ucmd, IMoveHelper *pHelper, CMoveData *move ) +{ + BaseClass::SetupMove( player, ucmd, pHelper, move ); +} + + +//----------------------------------------------------------------------------- +// Purpose: This is called post player movement to copy back all data that +// movement could have modified and that is necessary for future +// movement. (Server-side, the client-side version of this code can +// be found in prediction.cpp.) +//----------------------------------------------------------------------------- +void CHL1PlayerMove::FinishMove( CBasePlayer *player, CUserCmd *ucmd, CMoveData *move ) +{ + // Call the default FinishMove code. + BaseClass::FinishMove( player, ucmd, move ); +} diff --git a/game/server/hl1/hl1_weapon_crowbar.cpp b/game/server/hl1/hl1_weapon_crowbar.cpp new file mode 100644 index 0000000..f11f640 --- /dev/null +++ b/game/server/hl1/hl1_weapon_crowbar.cpp @@ -0,0 +1,379 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Crowbar - an old favorite +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "hl1mp_basecombatweapon_shared.h" + +#ifdef CLIENT_DLL +#include "c_baseplayer.h" +#include "fx_impact.h" +#include "fx.h" +#else +#include "player.h" +#include "soundent.h" +#endif + +#include "gamerules.h" +#include "ammodef.h" +#include "mathlib/mathlib.h" +#include "in_buttons.h" + +#include "vstdlib/random.h" + +extern ConVar sk_plr_dmg_crowbar; + +#define CROWBAR_RANGE 64.0f +#define CROWBAR_REFIRE_MISS 0.5f +#define CROWBAR_REFIRE_HIT 0.25f + + +#ifdef CLIENT_DLL +#define CWeaponCrowbar C_WeaponCrowbar +#endif + +//----------------------------------------------------------------------------- +// CWeaponCrowbar +//----------------------------------------------------------------------------- + +class CWeaponCrowbar : public CBaseHL1MPCombatWeapon +{ + DECLARE_CLASS( CWeaponCrowbar, CBaseHL1MPCombatWeapon ); +public: + DECLARE_NETWORKCLASS(); + DECLARE_PREDICTABLE(); +#ifndef CLIENT_DLL + DECLARE_DATADESC(); +#endif + + CWeaponCrowbar(); + + void Precache( void ); + virtual void ItemPostFrame( void ); + void PrimaryAttack( void ); + +public: + trace_t m_traceHit; + Activity m_nHitActivity; + +private: + virtual void Swing( void ); + virtual void Hit( void ); + virtual void ImpactEffect( void ); + void ImpactSound( CBaseEntity *pHitEntity ); + virtual Activity ChooseIntersectionPointAndActivity( trace_t &hitTrace, const Vector &mins, const Vector &maxs, CBasePlayer *pOwner ); + +public: + +}; + +IMPLEMENT_NETWORKCLASS_ALIASED( WeaponCrowbar, DT_WeaponCrowbar ); + +BEGIN_NETWORK_TABLE( CWeaponCrowbar, DT_WeaponCrowbar ) +/// what +END_NETWORK_TABLE() + +BEGIN_PREDICTION_DATA( CWeaponCrowbar ) +END_PREDICTION_DATA() + +LINK_ENTITY_TO_CLASS( weapon_crowbar, CWeaponCrowbar ); +PRECACHE_WEAPON_REGISTER( weapon_crowbar ); + +#ifndef CLIENT_DLL +BEGIN_DATADESC( CWeaponCrowbar ) + + // DEFINE_FIELD( m_trLineHit, trace_t ), + // DEFINE_FIELD( m_trHullHit, trace_t ), + // DEFINE_FIELD( m_nHitActivity, FIELD_INTEGER ), + // DEFINE_FIELD( m_traceHit, trace_t ), + + // Class CWeaponCrowbar: + // DEFINE_FIELD( m_nHitActivity, FIELD_INTEGER ), + + // Function Pointers + DEFINE_FUNCTION( Hit ), + +END_DATADESC() +#endif + +#define BLUDGEON_HULL_DIM 16 + +static const Vector g_bludgeonMins(-BLUDGEON_HULL_DIM,-BLUDGEON_HULL_DIM,-BLUDGEON_HULL_DIM); +static const Vector g_bludgeonMaxs(BLUDGEON_HULL_DIM,BLUDGEON_HULL_DIM,BLUDGEON_HULL_DIM); + +//----------------------------------------------------------------------------- +// Constructor +//----------------------------------------------------------------------------- +CWeaponCrowbar::CWeaponCrowbar() +{ + m_bFiresUnderwater = true; +} + +//----------------------------------------------------------------------------- +// Purpose: Precache the weapon +//----------------------------------------------------------------------------- +void CWeaponCrowbar::Precache( void ) +{ + //Call base class first + BaseClass::Precache(); +} + +//------------------------------------------------------------------------------ +// Purpose : Update weapon +//------------------------------------------------------------------------------ +void CWeaponCrowbar::ItemPostFrame( void ) +{ + CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); + + if ( pOwner == NULL ) + return; + + if ( (pOwner->m_nButtons & IN_ATTACK) && (m_flNextPrimaryAttack <= gpGlobals->curtime) ) + { + PrimaryAttack(); + } + else + { + WeaponIdle(); + return; + } +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CWeaponCrowbar::PrimaryAttack() +{ + Swing(); +} + + +//------------------------------------------------------------------------------ +// Purpose: Implement impact function +//------------------------------------------------------------------------------ +void CWeaponCrowbar::Hit( void ) +{ + //Make sound for the AI +#ifndef CLIENT_DLL + + CBasePlayer *pPlayer = ToBasePlayer( GetOwner() ); + + CSoundEnt::InsertSound( SOUND_BULLET_IMPACT, m_traceHit.endpos, 400, 0.2f, pPlayer ); + + CBaseEntity *pHitEntity = m_traceHit.m_pEnt; + + //Apply damage to a hit target + if ( pHitEntity != NULL ) + { + Vector hitDirection; + pPlayer->EyeVectors( &hitDirection, NULL, NULL ); + VectorNormalize( hitDirection ); + + ClearMultiDamage(); + CTakeDamageInfo info( GetOwner(), GetOwner(), sk_plr_dmg_crowbar.GetFloat(), DMG_CLUB ); + CalculateMeleeDamageForce( &info, hitDirection, m_traceHit.endpos ); + pHitEntity->DispatchTraceAttack( info, hitDirection, &m_traceHit ); + ApplyMultiDamage(); + + // Now hit all triggers along the ray that... + TraceAttackToTriggers( CTakeDamageInfo( GetOwner(), GetOwner(), sk_plr_dmg_crowbar.GetFloat(), DMG_CLUB ), m_traceHit.startpos, m_traceHit.endpos, hitDirection ); + + //Play an impact sound + ImpactSound( pHitEntity ); + } +#endif + + //Apply an impact effect + ImpactEffect(); +} + +//----------------------------------------------------------------------------- +// Purpose: Play the impact sound +// Input : pHitEntity - entity that we hit +// assumes pHitEntity is not null +//----------------------------------------------------------------------------- +void CWeaponCrowbar::ImpactSound( CBaseEntity *pHitEntity ) +{ + bool bIsWorld = ( pHitEntity->entindex() == 0 ); +#ifndef CLIENT_DLL + if ( !bIsWorld ) + { + bIsWorld |= pHitEntity->Classify() == CLASS_NONE || pHitEntity->Classify() == CLASS_MACHINE; + } +#endif + + if( bIsWorld ) + { + WeaponSound( MELEE_HIT_WORLD ); + } + else + { + WeaponSound( MELEE_HIT ); + } +} + +Activity CWeaponCrowbar::ChooseIntersectionPointAndActivity( trace_t &hitTrace, const Vector &mins, const Vector &maxs, CBasePlayer *pOwner ) +{ + int i, j, k; + float distance; + const float *minmaxs[2] = {mins.Base(), maxs.Base()}; + trace_t tmpTrace; + Vector vecHullEnd = hitTrace.endpos; + Vector vecEnd; + + distance = 1e6f; + Vector vecSrc = hitTrace.startpos; + + vecHullEnd = vecSrc + ((vecHullEnd - vecSrc)*2); + UTIL_TraceLine( vecSrc, vecHullEnd, MASK_SHOT_HULL, pOwner, COLLISION_GROUP_NONE, &tmpTrace ); + if ( tmpTrace.fraction == 1.0 ) + { + for ( i = 0; i < 2; i++ ) + { + for ( j = 0; j < 2; j++ ) + { + for ( k = 0; k < 2; k++ ) + { + vecEnd.x = vecHullEnd.x + minmaxs[i][0]; + vecEnd.y = vecHullEnd.y + minmaxs[j][1]; + vecEnd.z = vecHullEnd.z + minmaxs[k][2]; + + UTIL_TraceLine( vecSrc, vecEnd, MASK_SHOT_HULL, pOwner, COLLISION_GROUP_NONE, &tmpTrace ); + if ( tmpTrace.fraction < 1.0 ) + { + float thisDistance = (tmpTrace.endpos - vecSrc).Length(); + if ( thisDistance < distance ) + { + hitTrace = tmpTrace; + distance = thisDistance; + } + } + } + } + } + } + else + { + hitTrace = tmpTrace; + } + + + return ACT_VM_HITCENTER; +} + +#ifdef HL1MP_CLIENT_DLL +//----------------------------------------------------------------------------- +// Purpose: Handle jeep impacts +//----------------------------------------------------------------------------- +void ImpactCrowbarCallback( const CEffectData &data ) +{ + trace_t tr; + Vector vecOrigin, vecStart, vecShotDir; + int iMaterial, iDamageType, iHitbox; + short nSurfaceProp; + C_BaseEntity *pEntity = ParseImpactData( data, &vecOrigin, &vecStart, &vecShotDir, nSurfaceProp, iMaterial, iDamageType, iHitbox ); + + bool bIsWorld = ( pEntity->entindex() == 0 ); + + if ( !pEntity ) + { + // This happens for impacts that occur on an object that's then destroyed. + // Clear out the fraction so it uses the server's data + tr.fraction = 1.0; + GetActiveWeapon()->WeaponSound( bIsWorld ? MELEE_HIT_WORLD : MELEE_HIT ); + return; + } + + // If we hit, perform our custom effects and play the sound + if ( Impact( vecOrigin, vecStart, iMaterial, iDamageType, iHitbox, pEntity, tr ) ) + { + // Check for custom effects based on the Decal index + PerformCustomEffects( vecOrigin, tr, vecShotDir, iMaterial, 2 ); + } + + GetActiveWeapon()->WeaponSound( bIsWorld ? MELEE_HIT_WORLD : MELEE_HIT ); +} + +DECLARE_CLIENT_EFFECT( "ImpactCrowbar", ImpactCrowbarCallback ); + +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponCrowbar::ImpactEffect( void ) +{ + //FIXME: need new decals +#ifdef HL1MP_CLIENT_DLL + // in hl1mp force the basic crowbar sound + UTIL_ImpactTrace( &m_traceHit, DMG_CLUB, "ImpactCrowbar" ); +#else + UTIL_ImpactTrace( &m_traceHit, DMG_CLUB ); +#endif +} + +//------------------------------------------------------------------------------ +// Purpose : Starts the swing of the weapon and determines the animation +//------------------------------------------------------------------------------ +void CWeaponCrowbar::Swing( void ) +{ + // Try a ray + CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); + if ( !pOwner ) + return; + + Vector swingStart = pOwner->Weapon_ShootPosition( ); + Vector forward; + + pOwner->EyeVectors( &forward, NULL, NULL ); + + Vector swingEnd = swingStart + forward * CROWBAR_RANGE; + + UTIL_TraceLine( swingStart, swingEnd, MASK_SHOT_HULL, pOwner, COLLISION_GROUP_NONE, &m_traceHit ); + m_nHitActivity = ACT_VM_HITCENTER; + + if ( m_traceHit.fraction == 1.0 ) + { + float bludgeonHullRadius = 1.732f * BLUDGEON_HULL_DIM; // hull is +/- 16, so use cuberoot of 2 to determine how big the hull is from center to the corner point + + // Back off by hull "radius" + swingEnd -= forward * bludgeonHullRadius; + + UTIL_TraceHull( swingStart, swingEnd, g_bludgeonMins, g_bludgeonMaxs, MASK_SHOT_HULL, pOwner, COLLISION_GROUP_NONE, &m_traceHit ); + if ( m_traceHit.fraction < 1.0 ) + { + m_nHitActivity = ChooseIntersectionPointAndActivity( m_traceHit, g_bludgeonMins, g_bludgeonMaxs, pOwner ); + } + } + + + // ------------------------- + // Miss + // ------------------------- + if ( m_traceHit.fraction == 1.0f ) + { + m_nHitActivity = ACT_VM_MISSCENTER; + + //Play swing sound + WeaponSound( SINGLE ); + + //Setup our next attack times + m_flNextPrimaryAttack = gpGlobals->curtime + CROWBAR_REFIRE_MISS; + } + else + { + Hit(); + + //Setup our next attack times + m_flNextPrimaryAttack = gpGlobals->curtime + CROWBAR_REFIRE_HIT; + } + + //Send the anim + SendWeaponAnim( m_nHitActivity ); + pOwner->SetAnimation( PLAYER_ATTACK1 ); +} diff --git a/game/server/hl1/hl1_weapon_snark.cpp b/game/server/hl1/hl1_weapon_snark.cpp new file mode 100644 index 0000000..9339297 --- /dev/null +++ b/game/server/hl1/hl1_weapon_snark.cpp @@ -0,0 +1,208 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Snark +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "npcevent.h" +#include "hl1_basecombatweapon_shared.h" +#include "basecombatcharacter.h" +#include "ai_basenpc.h" +#include "player.h" +#include "gamerules.h" +#include "in_buttons.h" +#include "soundent.h" +#include "game.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "hl1_npc_snark.h" +#include "beam_shared.h" + + +//----------------------------------------------------------------------------- +// CWeaponSnark +//----------------------------------------------------------------------------- + + +#define SNARK_NEST_MODEL "models/w_sqknest.mdl" + + +class CWeaponSnark : public CBaseHL1CombatWeapon +{ + DECLARE_CLASS( CWeaponSnark, CBaseHL1CombatWeapon ); +public: + + CWeaponSnark( void ); + + void Precache( void ); + void PrimaryAttack( void ); + void WeaponIdle( void ); + bool Deploy( void ); + bool Holster( CBaseCombatWeapon *pSwitchingTo = NULL ); + + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + +private: + bool m_bJustThrown; +}; + +LINK_ENTITY_TO_CLASS( weapon_snark, CWeaponSnark ); + +PRECACHE_WEAPON_REGISTER( weapon_snark ); + +IMPLEMENT_SERVERCLASS_ST( CWeaponSnark, DT_WeaponSnark ) +END_SEND_TABLE() + +BEGIN_DATADESC( CWeaponSnark ) + DEFINE_FIELD( m_bJustThrown, FIELD_BOOLEAN ), +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: Constructor +//----------------------------------------------------------------------------- +CWeaponSnark::CWeaponSnark( void ) +{ + m_bReloadsSingly = false; + m_bFiresUnderwater = true; + m_bJustThrown = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponSnark::Precache( void ) +{ + BaseClass::Precache(); + + PrecacheScriptSound( "WpnSnark.PrimaryAttack" ); + PrecacheScriptSound( "WpnSnark.Deploy" ); + + UTIL_PrecacheOther("monster_snark"); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponSnark::PrimaryAttack( void ) +{ + // Only the player fires this way so we can cast + CBasePlayer *pPlayer = ToBasePlayer( GetOwner() ); + + if ( !pPlayer ) + { + return; + } + + if ( pPlayer->GetAmmoCount( m_iPrimaryAmmoType ) <= 0 ) + return; + + Vector vecForward; + pPlayer->EyeVectors( &vecForward ); + + // find place to toss monster + // Does this need to consider a crouched player? + Vector vecStart = pPlayer->WorldSpaceCenter() + (vecForward * 20); + Vector vecEnd = vecStart + (vecForward * 44); + trace_t tr; + UTIL_TraceLine( vecStart, vecEnd, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); + if ( tr.allsolid || tr.startsolid || tr.fraction <= 0.25 ) + return; + + // player "shoot" animation + SendWeaponAnim( ACT_VM_PRIMARYATTACK ); + pPlayer->SetAnimation( PLAYER_ATTACK1 ); + + CSnark *pSnark = (CSnark*)Create( "monster_snark", tr.endpos, pPlayer->EyeAngles(), GetOwner() ); + if ( pSnark ) + { + pSnark->SetAbsVelocity( vecForward * 200 + pPlayer->GetAbsVelocity() ); + } + + // play hunt sound + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "WpnSnark.PrimaryAttack" ); + + CSoundEnt::InsertSound( SOUND_DANGER, GetAbsOrigin(), 200, 0.2 ); + + pPlayer->RemoveAmmo( 1, m_iPrimaryAmmoType ); + + m_bJustThrown = true; + + m_flNextPrimaryAttack = gpGlobals->curtime + 0.3; + SetWeaponIdleTime( gpGlobals->curtime + 1.0 ); +} + +void CWeaponSnark::WeaponIdle( void ) +{ + CBasePlayer *pPlayer = ToBasePlayer( GetOwner() ); + + if ( !pPlayer ) + { + return; + } + + if ( !HasWeaponIdleTimeElapsed() ) + return; + + if ( m_bJustThrown ) + { + m_bJustThrown = false; + + if ( pPlayer->GetAmmoCount( m_iPrimaryAmmoType ) <= 0 ) + { + if ( !pPlayer->SwitchToNextBestWeapon( pPlayer->GetActiveWeapon() ) ) + Holster(); + } + else + { + SendWeaponAnim( ACT_VM_DRAW ); + SetWeaponIdleTime( gpGlobals->curtime + random->RandomFloat( 10, 15 ) ); + } + } + else + { + if ( random->RandomFloat( 0, 1 ) <= 0.75 ) + { + SendWeaponAnim( ACT_VM_IDLE ); + } + else + { + SendWeaponAnim( ACT_VM_FIDGET ); + } + } +} + +bool CWeaponSnark::Deploy( void ) +{ + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "WpnSnark.Deploy" ); + + return BaseClass::Deploy(); +} + +bool CWeaponSnark::Holster( CBaseCombatWeapon *pSwitchingTo ) +{ + CBasePlayer *pPlayer = ToBasePlayer( GetOwner() ); + if ( !pPlayer ) + { + return false; + } + + if ( !BaseClass::Holster( pSwitchingTo ) ) + { + return false; + } + + if ( pPlayer->GetAmmoCount( m_iPrimaryAmmoType ) <= 0 ) + { + SetThink( &CWeaponSnark::DestroyItem ); + SetNextThink( gpGlobals->curtime + 0.1 ); + } + + pPlayer->SetNextAttack( gpGlobals->curtime + 0.5 ); + + return true; +} diff --git a/game/server/hl1/hl1_weapon_tripmine.cpp b/game/server/hl1/hl1_weapon_tripmine.cpp new file mode 100644 index 0000000..8818cf7 --- /dev/null +++ b/game/server/hl1/hl1_weapon_tripmine.cpp @@ -0,0 +1,573 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Tripmine +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "npcevent.h" +#include "hl1_basecombatweapon_shared.h" +#include "basecombatcharacter.h" +#include "ai_basenpc.h" +#include "player.h" +#include "gamerules.h" +#include "in_buttons.h" +#include "soundent.h" +#include "game.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "hl1_player.h" +#include "hl1_basegrenade.h" +#include "beam_shared.h" + +extern ConVar sk_plr_dmg_tripmine; + + +//----------------------------------------------------------------------------- +// CWeaponTripMine +//----------------------------------------------------------------------------- + + +#define TRIPMINE_MODEL "models/w_tripmine.mdl" + + +class CWeaponTripMine : public CBaseHL1CombatWeapon +{ + DECLARE_CLASS( CWeaponTripMine, CBaseHL1CombatWeapon ); +public: + + CWeaponTripMine( void ); + + void Spawn( void ); + void Precache( void ); + void Equip( CBaseCombatCharacter *pOwner ); + void PrimaryAttack( void ); + void WeaponIdle( void ); + bool Holster( CBaseCombatWeapon *pSwitchingTo = NULL ); + + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + +private: + int m_iGroundIndex; + int m_iPickedUpIndex; +}; + +LINK_ENTITY_TO_CLASS( weapon_tripmine, CWeaponTripMine ); + +PRECACHE_WEAPON_REGISTER( weapon_tripmine ); + +IMPLEMENT_SERVERCLASS_ST( CWeaponTripMine, DT_WeaponTripMine ) +END_SEND_TABLE() + +BEGIN_DATADESC( CWeaponTripMine ) +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: Constructor +//----------------------------------------------------------------------------- +CWeaponTripMine::CWeaponTripMine( void ) +{ + m_bReloadsSingly = false; + m_bFiresUnderwater = true; +} + +void CWeaponTripMine::Spawn( void ) +{ + BaseClass::Spawn(); + + m_iWorldModelIndex = m_iGroundIndex; + SetModel( TRIPMINE_MODEL ); + + SetActivity( ACT_TRIPMINE_GROUND ); + ResetSequenceInfo( ); + m_flPlaybackRate = 0; + + if ( !g_pGameRules->IsDeathmatch() ) + { + UTIL_SetSize( this, Vector(-16, -16, 0), Vector(16, 16, 28) ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponTripMine::Precache( void ) +{ + BaseClass::Precache(); + + m_iGroundIndex = PrecacheModel( TRIPMINE_MODEL ); + m_iPickedUpIndex = PrecacheModel( GetWorldModel() ); + + UTIL_PrecacheOther( "monster_tripmine" ); +} + +void CWeaponTripMine::Equip( CBaseCombatCharacter *pOwner ) +{ + m_iWorldModelIndex = m_iPickedUpIndex; + + BaseClass::Equip( pOwner ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponTripMine::PrimaryAttack( void ) +{ + CHL1_Player *pPlayer = ToHL1Player( GetOwner() ); + if ( !pPlayer ) + { + return; + } + + if ( pPlayer->GetAmmoCount( m_iPrimaryAmmoType ) <= 0 ) + return; + + Vector vecAiming = pPlayer->GetAutoaimVector( 0 ); + Vector vecSrc = pPlayer->Weapon_ShootPosition( ); + + trace_t tr; + + UTIL_TraceLine( vecSrc, vecSrc + vecAiming * 64, MASK_SHOT, pPlayer, COLLISION_GROUP_NONE, &tr ); + + if ( tr.fraction < 1.0 ) + { + CBaseEntity *pEntity = tr.m_pEnt; + if ( pEntity && !( pEntity->GetFlags() & FL_CONVEYOR ) ) + { + QAngle angles; + VectorAngles( tr.plane.normal, angles ); + + CBaseEntity::Create( "monster_tripmine", tr.endpos + tr.plane.normal * 2, angles, pPlayer ); + + pPlayer->RemoveAmmo( 1, m_iPrimaryAmmoType ); + + pPlayer->SetAnimation( PLAYER_ATTACK1 ); + + if ( pPlayer->GetAmmoCount( m_iPrimaryAmmoType ) <= 0 ) + { + if ( !pPlayer->SwitchToNextBestWeapon( pPlayer->GetActiveWeapon() ) ) + Holster(); + } + else + { + SendWeaponAnim( ACT_VM_DRAW ); + SetWeaponIdleTime( gpGlobals->curtime + random->RandomFloat( 10, 15 ) ); + } + + m_flNextPrimaryAttack = gpGlobals->curtime + 0.5; + + SetWeaponIdleTime( gpGlobals->curtime ); // MO curtime correct ? + } + } + else + { + SetWeaponIdleTime( m_flTimeWeaponIdle = gpGlobals->curtime + random->RandomFloat( 10, 15 ) ); + } +} + +void CWeaponTripMine::WeaponIdle( void ) +{ + CBasePlayer *pPlayer = ToBasePlayer( GetOwner() ); + + if ( !pPlayer ) + { + return; + } + + if ( !HasWeaponIdleTimeElapsed() ) + return; + + int iAnim; + + if ( random->RandomFloat( 0, 1 ) <= 0.75 ) + { + iAnim = ACT_VM_IDLE; + } + else + { + iAnim = ACT_VM_FIDGET; + } + + SendWeaponAnim( iAnim ); +} + +bool CWeaponTripMine::Holster( CBaseCombatWeapon *pSwitchingTo ) +{ + CBasePlayer *pPlayer = ToBasePlayer( GetOwner() ); + if ( !pPlayer ) + { + return false; + } + + if ( !BaseClass::Holster( pSwitchingTo ) ) + { + return false; + } + + if ( pPlayer->GetAmmoCount( m_iPrimaryAmmoType ) <= 0 ) + { + SetThink( &CWeaponTripMine::DestroyItem ); + SetNextThink( gpGlobals->curtime + 0.1 ); + } + + pPlayer->SetNextAttack( gpGlobals->curtime + 0.5 ); + + return true; +} + + +//----------------------------------------------------------------------------- +// CTripmineGrenade +//----------------------------------------------------------------------------- + +#define TRIPMINE_BEAM_SPRITE "sprites/laserbeam.vmt" + +class CTripmineGrenade : public CHL1BaseGrenade +{ + DECLARE_CLASS( CTripmineGrenade, CHL1BaseGrenade ); +public: + CTripmineGrenade(); + void Spawn( void ); + void Precache( void ); + + int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + + void WarningThink( void ); + void PowerupThink( void ); + void BeamBreakThink( void ); + void DelayDeathThink( void ); + void Event_Killed( const CTakeDamageInfo &info ); + + DECLARE_DATADESC(); + +private: + void MakeBeam( void ); + void KillBeam( void ); + +private: + float m_flPowerUp; + Vector m_vecDir; + Vector m_vecEnd; + float m_flBeamLength; + + CHandle<CBaseEntity> m_hRealOwner; + CHandle<CBeam> m_hBeam; + + CHandle<CBaseEntity> m_hStuckOn; + Vector m_posStuckOn; + QAngle m_angStuckOn; + + int m_iLaserModel; +}; + +LINK_ENTITY_TO_CLASS( monster_tripmine, CTripmineGrenade ); + +BEGIN_DATADESC( CTripmineGrenade ) + DEFINE_FIELD( m_flPowerUp, FIELD_TIME ), + DEFINE_FIELD( m_vecDir, FIELD_VECTOR ), + DEFINE_FIELD( m_vecEnd, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_flBeamLength, FIELD_FLOAT ), +// DEFINE_FIELD( m_hBeam, FIELD_EHANDLE ), + DEFINE_FIELD( m_hRealOwner, FIELD_EHANDLE ), + DEFINE_FIELD( m_hStuckOn, FIELD_EHANDLE ), + DEFINE_FIELD( m_posStuckOn, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_angStuckOn, FIELD_VECTOR ), + //DEFINE_FIELD( m_iLaserModel, FIELD_INTEGER ), + + // Function Pointers + DEFINE_THINKFUNC( WarningThink ), + DEFINE_THINKFUNC( PowerupThink ), + DEFINE_THINKFUNC( BeamBreakThink ), + DEFINE_THINKFUNC( DelayDeathThink ), +END_DATADESC() + + +CTripmineGrenade::CTripmineGrenade() +{ + m_vecDir.Init(); + m_vecEnd.Init(); +} + +void CTripmineGrenade::Spawn( void ) +{ + Precache( ); + // motor + SetMoveType( MOVETYPE_FLY ); + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_SOLID ); + SetModel( TRIPMINE_MODEL ); + + // Don't collide with the player (the beam will still be tripped by one, however) + SetCollisionGroup( COLLISION_GROUP_WEAPON ); + + SetCycle( 0 ); + SetSequence( SelectWeightedSequence( ACT_TRIPMINE_WORLD ) ); + ResetSequenceInfo(); + m_flPlaybackRate = 0; + + UTIL_SetSize( this, Vector( -8, -8, -8), Vector(8, 8, 8) ); + + m_flDamage = sk_plr_dmg_tripmine.GetFloat(); + m_DmgRadius = m_flDamage * 2.5; + + if ( m_spawnflags & 1 ) + { + // power up quickly + m_flPowerUp = gpGlobals->curtime + 1.0; + } + else + { + // power up in 2.5 seconds + m_flPowerUp = gpGlobals->curtime + 2.5; + } + + SetThink( &CTripmineGrenade::PowerupThink ); + SetNextThink( gpGlobals->curtime + 0.2 ); + + m_takedamage = DAMAGE_YES; + + m_iHealth = 1; + + if ( GetOwnerEntity() != NULL ) + { + // play deploy sound + EmitSound( "TripmineGrenade.Deploy" ); + EmitSound( "TripmineGrenade.Charge" ); + + m_hRealOwner = GetOwnerEntity(); + } + AngleVectors( GetAbsAngles(), &m_vecDir ); + m_vecEnd = GetAbsOrigin() + m_vecDir * MAX_TRACE_LENGTH; +} + + +void CTripmineGrenade::Precache( void ) +{ + PrecacheModel( TRIPMINE_MODEL ); + m_iLaserModel = PrecacheModel( TRIPMINE_BEAM_SPRITE ); + + PrecacheScriptSound( "TripmineGrenade.Deploy" ); + PrecacheScriptSound( "TripmineGrenade.Charge" ); + PrecacheScriptSound( "TripmineGrenade.Activate" ); + +} + + +void CTripmineGrenade::WarningThink( void ) +{ + // set to power up + SetThink( &CTripmineGrenade::PowerupThink ); + SetNextThink( gpGlobals->curtime + 1.0f ); +} + + +void CTripmineGrenade::PowerupThink( void ) +{ + if ( m_hStuckOn == NULL ) + { + trace_t tr; + CBaseEntity *pOldOwner = GetOwnerEntity(); + + // don't explode if the player is standing in front of the laser + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + m_vecDir * 32, MASK_SHOT, NULL, COLLISION_GROUP_NONE, &tr ); + + if( tr.m_pEnt && pOldOwner && + ( tr.m_pEnt == pOldOwner ) && pOldOwner->IsPlayer() ) + { + m_flPowerUp += 0.1; //delay the arming + SetNextThink( gpGlobals->curtime + 0.1f ); + return; + } + + // find out what we've been stuck on + SetOwnerEntity( NULL ); + + UTIL_TraceLine( GetAbsOrigin() + m_vecDir * 8, GetAbsOrigin() - m_vecDir * 32, MASK_SHOT, pOldOwner, COLLISION_GROUP_NONE, &tr ); + + if ( tr.startsolid ) + { + SetOwnerEntity( pOldOwner ); + m_flPowerUp += 0.1; + SetNextThink( gpGlobals->curtime + 0.1f ); + return; + } + if ( tr.fraction < 1.0 ) + { + SetOwnerEntity( tr.m_pEnt ); + m_hStuckOn = tr.m_pEnt; + m_posStuckOn = m_hStuckOn->GetAbsOrigin(); + m_angStuckOn = m_hStuckOn->GetAbsAngles(); + } + else + { + // somehow we've been deployed on nothing, or something that was there, but now isn't. + // remove ourselves + + StopSound( "TripmineGrenade.Deploy" ); + StopSound( "TripmineGrenade.Charge" ); + SetThink( &CBaseEntity::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.1f ); +// ALERT( at_console, "WARNING:Tripmine at %.0f, %.0f, %.0f removed\n", pev->origin.x, pev->origin.y, pev->origin.z ); + KillBeam(); + return; + } + } + else if ( (m_posStuckOn != m_hStuckOn->GetAbsOrigin()) || (m_angStuckOn != m_hStuckOn->GetAbsAngles()) ) + { + // what we were stuck on has moved, or rotated. Create a tripmine weapon and drop to ground + + StopSound( "TripmineGrenade.Deploy" ); + StopSound( "TripmineGrenade.Charge" ); + CBaseEntity *pMine = Create( "weapon_tripmine", GetAbsOrigin() + m_vecDir * 24, GetAbsAngles() ); + pMine->AddSpawnFlags( SF_NORESPAWN ); + + SetThink( &CBaseEntity::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.1f ); + KillBeam(); + return; + } + + if ( gpGlobals->curtime > m_flPowerUp ) + { + MakeBeam( ); + RemoveSolidFlags( FSOLID_NOT_SOLID ); + + m_bIsLive = true; + + // play enabled sound + EmitSound( "TripmineGrenade.Activate" ); + } + + SetNextThink( gpGlobals->curtime + 0.1f ); +} + + +void CTripmineGrenade::KillBeam( void ) +{ + if ( m_hBeam ) + { + UTIL_Remove( m_hBeam ); + m_hBeam = NULL; + } +} + + +void CTripmineGrenade::MakeBeam( void ) +{ + trace_t tr; + + UTIL_TraceLine( GetAbsOrigin(), m_vecEnd, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + + m_flBeamLength = tr.fraction; + + // set to follow laser spot + SetThink( &CTripmineGrenade::BeamBreakThink ); + + SetNextThink( gpGlobals->curtime + 1.0f ); + + Vector vecTmpEnd = GetAbsOrigin() + m_vecDir * MAX_TRACE_LENGTH * m_flBeamLength; + + m_hBeam = CBeam::BeamCreate( TRIPMINE_BEAM_SPRITE, 1.0 ); + m_hBeam->PointEntInit( vecTmpEnd, this ); + m_hBeam->SetColor( 0, 214, 198 ); + m_hBeam->SetScrollRate( 25.5 ); + m_hBeam->SetBrightness( 64 ); + m_hBeam->AddSpawnFlags( SF_BEAM_TEMPORARY ); // so it won't save and come back to haunt us later.. +} + + +void CTripmineGrenade::BeamBreakThink( void ) +{ + bool bBlowup = false; + trace_t tr; + + // NOT MASK_SHOT because we want only simple hit boxes + UTIL_TraceLine( GetAbsOrigin(), m_vecEnd, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); + + // ALERT( at_console, "%f : %f\n", tr.flFraction, m_flBeamLength ); + + // respawn detect. + if ( !m_hBeam ) + { + MakeBeam(); + + trace_t stuckOnTrace; + Vector forward; + GetVectors( &forward, NULL, NULL ); + + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() - forward * 12.0f, MASK_SOLID, this, COLLISION_GROUP_NONE, &stuckOnTrace ); + + if ( stuckOnTrace.m_pEnt ) + { + m_hStuckOn = stuckOnTrace.m_pEnt; // reset stuck on ent too + } + } + + CBaseEntity *pEntity = tr.m_pEnt; + CBaseCombatCharacter *pBCC = ToBaseCombatCharacter( pEntity ); + + if ( pBCC || fabs( m_flBeamLength - tr.fraction ) > 0.001 ) + { + bBlowup = true; + } + else + { + if ( m_hStuckOn == NULL ) + bBlowup = true; + else if ( m_posStuckOn != m_hStuckOn->GetAbsOrigin() ) + bBlowup = true; + else if ( m_angStuckOn != m_hStuckOn->GetAbsAngles() ) + bBlowup = true; + } + + if ( bBlowup ) + { + SetOwnerEntity( m_hRealOwner ); + m_iHealth = 0; + Event_Killed( CTakeDamageInfo( this, m_hRealOwner, 100, GIB_NORMAL ) ); + return; + } + + SetNextThink( gpGlobals->curtime + 0.1 ); +} +/* +int CTripmineGrenade::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + if (gpGlobals->curtime < m_flPowerUp && info.GetDamage() < m_iHealth) + { + // disable + // Create( "weapon_tripmine", GetLocalOrigin() + m_vecDir * 24, GetAngles() ); + SetThink( &CBaseEntity::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.1f ); + KillBeam(); + return 0; + } + return BaseClass::OnTakeDamage_Alive( info ); +}*/ + +void CTripmineGrenade::Event_Killed( const CTakeDamageInfo &info ) +{ + m_takedamage = DAMAGE_NO; + + if ( info.GetAttacker() && ( info.GetAttacker()->GetFlags() & FL_CLIENT ) ) + { + // some client has destroyed this mine, he'll get credit for any kills + SetOwnerEntity( info.GetAttacker() ); + } + + SetThink( &CTripmineGrenade::DelayDeathThink ); + SetNextThink( gpGlobals->curtime + random->RandomFloat( 0.1, 0.3 ) ); + + StopSound( "TripmineGrenade.Charge" ); +} + +void CTripmineGrenade::DelayDeathThink( void ) +{ + KillBeam(); + trace_t tr; + UTIL_TraceLine ( GetAbsOrigin() + m_vecDir * 8, GetAbsOrigin() - m_vecDir * 64, MASK_SOLID, this, COLLISION_GROUP_NONE, & tr); + + Explode( &tr, DMG_BLAST ); +} diff --git a/game/server/hl1/hl1_weaponbox.cpp b/game/server/hl1/hl1_weaponbox.cpp new file mode 100644 index 0000000..07a745e --- /dev/null +++ b/game/server/hl1/hl1_weaponbox.cpp @@ -0,0 +1,242 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + + +#include "cbase.h" +#include "hl1_items.h" +#include "ammodef.h" + + +#define WEAPONBOX_MODEL "models/w_weaponbox.mdl" + + +class CWeaponBox : public CHL1Item +{ +public: + DECLARE_CLASS( CWeaponBox, CHL1Item ); + + void Spawn( void ); + void Precache( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + void BoxTouch( CBaseEntity *pPlayer ); + + DECLARE_DATADESC(); + +private: + bool PackAmmo( char *szName, int iCount ); + int GiveAmmo( int iCount, char *szName, int iMax, int *pIndex = NULL ); + + int m_cAmmoTypes; // how many ammo types packed into this box (if packed by a level designer) + string_t m_rgiszAmmo[MAX_AMMO_SLOTS]; // ammo names + int m_rgAmmo[MAX_AMMO_SLOTS]; // ammo quantities +}; +LINK_ENTITY_TO_CLASS(weaponbox, CWeaponBox); +PRECACHE_REGISTER(weaponbox); + +BEGIN_DATADESC( CWeaponBox ) + DEFINE_ARRAY( m_rgiszAmmo, FIELD_STRING, MAX_AMMO_SLOTS ), + DEFINE_ARRAY( m_rgAmmo, FIELD_INTEGER, MAX_AMMO_SLOTS ), + DEFINE_FIELD( m_cAmmoTypes, FIELD_INTEGER ), + + DEFINE_ENTITYFUNC( BoxTouch ), +END_DATADESC() + + +bool CWeaponBox::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( m_cAmmoTypes < MAX_AMMO_SLOTS ) + { + if ( PackAmmo( (char *)szKeyName, atoi( szValue ) ) ) + { + m_cAmmoTypes++;// count this new ammo type. + + return true; + } + } + else + { + Warning( "WeaponBox too full! only %d ammotypes allowed\n", MAX_AMMO_SLOTS ); + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + +void CWeaponBox::Spawn( void ) +{ + Precache(); + SetModel( WEAPONBOX_MODEL ); + BaseClass::Spawn(); + + PrecacheScriptSound( "Item.Pickup" ); + + SetTouch( &CWeaponBox::BoxTouch ); +} + + +void CWeaponBox::Precache( void ) +{ + PrecacheModel( WEAPONBOX_MODEL ); +} + + +void CWeaponBox::BoxTouch( CBaseEntity *pOther ) +{ + if ( !( GetFlags() & FL_ONGROUND ) ) + { + return; + } + + if ( !pOther->IsPlayer() ) + { + // only players may touch a weaponbox. + return; + } + + if ( !pOther->IsAlive() ) + { + // no dead guys. + return; + } + + CBasePlayer *pPlayer = (CBasePlayer *)pOther; + int i; + +// dole out ammo + for ( i = 0 ; i < MAX_AMMO_SLOTS ; i++ ) + { + if ( m_rgiszAmmo[ i ] != NULL_STRING ) + { + // there's some ammo of this type. + pPlayer->GiveAmmo( m_rgAmmo[ i ], (char *)STRING( m_rgiszAmmo[ i ] ) ); + + // now empty the ammo from the weaponbox since we just gave it to the player + m_rgiszAmmo[ i ] = NULL_STRING; + m_rgAmmo[ i ] = 0; + } + } + + CPASAttenuationFilter filter( pOther, "Item.Pickup" ); + EmitSound( filter, pOther->entindex(), "Item.Pickup" ); + + SetTouch(NULL); + if ( g_pGameRules->ItemShouldRespawn( this ) == GR_ITEM_RESPAWN_NO ) + { + UTIL_Remove( this ); + } +} + + +bool CWeaponBox::PackAmmo( char *szName, int iCount ) +{ + char szConvertedName[ 32 ]; + + if ( FStrEq( szName, "" ) ) + { + // error here + Warning( "NULL String in PackAmmo!\n" ); + return false; + } + + Q_snprintf( szConvertedName, sizeof( szConvertedName ), "%s", szName ); + if ( !stricmp( szName, "bolts" ) ) + { + Q_snprintf( szConvertedName, sizeof( szConvertedName ), "XBowBolt" ); + } + if ( !stricmp( szName, "uranium" ) ) + { + Q_snprintf( szConvertedName, sizeof( szConvertedName ), "Uranium" ); + } + if ( !stricmp( szName, "9mm" ) ) + { + Q_snprintf( szConvertedName, sizeof( szConvertedName ), "9mmRound" ); + } + if ( !stricmp( szName, "Hand Grenade" ) ) + { + Q_snprintf( szConvertedName, sizeof( szConvertedName ), "Grenade" ); + } + if ( !stricmp( szName, "Hornets" ) ) + { + Q_snprintf( szConvertedName, sizeof( szConvertedName ), "Hornet" ); + } + if ( !stricmp( szName, "ARgrenades" ) ) + { + Q_snprintf( szConvertedName, sizeof( szConvertedName ), "MP5_Grenade" ); + } + if ( !stricmp( szName, "357" ) ) + { + Q_snprintf( szConvertedName, sizeof( szConvertedName ), "357Round" ); + } + if ( !stricmp( szName, "rockets" ) ) + { + Q_snprintf( szConvertedName, sizeof( szConvertedName ), "RPG_Rocket" ); + } + if ( !stricmp( szName, "Satchel Charge" ) ) + { + Q_snprintf( szConvertedName, sizeof( szConvertedName ), "Satchel" ); + } + if ( !stricmp( szName, "buckshot" ) ) + { + Q_snprintf( szConvertedName, sizeof( szConvertedName ), "Buckshot" ); + } + if ( !stricmp( szName, "Snarks" ) ) + { + Q_snprintf( szConvertedName, sizeof( szConvertedName ), "Snark" ); + } + if ( !stricmp( szName, "Trip Mine" ) ) + { + Q_snprintf( szConvertedName, sizeof( szConvertedName ), "TripMine" ); + } + + int iMaxCarry = GetAmmoDef()->MaxCarry( GetAmmoDef()->Index( szConvertedName ) ); + + if ( iMaxCarry > 0 && iCount > 0 ) + { + //ALERT ( at_console, "Packed %d rounds of %s\n", iCount, STRING(iszName) ); + GiveAmmo( iCount, szConvertedName, iMaxCarry ); + return true; + } + + return false; +} + +//========================================================= +// CWeaponBox - GiveAmmo +//========================================================= +int CWeaponBox::GiveAmmo( int iCount, char *szName, int iMax, int *pIndex ) +{ + int i; + + for ( i = 1; ( i < MAX_AMMO_SLOTS ) && ( m_rgiszAmmo[i] != NULL_STRING ); i++ ) + { + if ( stricmp( szName, STRING( m_rgiszAmmo[i] ) ) == 0 ) + { + if (pIndex) + *pIndex = i; + + int iAdd = MIN( iCount, iMax - m_rgAmmo[i]); + if (iCount == 0 || iAdd > 0) + { + m_rgAmmo[i] += iAdd; + + return i; + } + return -1; + } + } + + if (i < MAX_AMMO_SLOTS) + { + if (pIndex) + *pIndex = i; + + m_rgiszAmmo[i] = AllocPooledString( szName ); + m_rgAmmo[i] = iCount; + + return i; + } + Warning( "out of named ammo slots\n"); + return i; +} diff --git a/game/server/hl1/hl1mp_bot_temp.cpp b/game/server/hl1/hl1mp_bot_temp.cpp new file mode 100644 index 0000000..d5eae12 --- /dev/null +++ b/game/server/hl1/hl1mp_bot_temp.cpp @@ -0,0 +1,432 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Basic BOT handling. +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + + +#include "cbase.h" +#include "player.h" +#include "hl1mp_player.h" +#include "in_buttons.h" +#include "movehelper_server.h" + +void ClientPutInServer( edict_t *pEdict, const char *playername ); +void Bot_Think( CHL1MP_Player *pBot ); + +#ifdef DEBUG + +ConVar bot_forcefireweapon( "bot_forcefireweapon", "", 0, "Force bots with the specified weapon to fire." ); +ConVar bot_forceattack2( "bot_forceattack2", "0", 0, "When firing, use attack2." ); +ConVar bot_forceattackon( "bot_forceattackon", "0", 0, "When firing, don't tap fire, hold it down." ); +ConVar bot_flipout( "bot_flipout", "0", 0, "When on, all bots fire their guns." ); +ConVar bot_defend( "bot_defend", "0", 0, "Set to a team number, and that team will all keep their combat shields raised." ); +ConVar bot_changeclass( "bot_changeclass", "0", 0, "Force all bots to change to the specified class." ); +ConVar bot_zombie( "bot_zombie", "0", 0, "Brraaaaaiiiins." ); +static ConVar bot_mimic( "bot_mimic", "0", 0, "Bot uses usercmd of player by index." ); +static ConVar bot_mimic_yaw_offset( "bot_mimic_yaw_offset", "0", 0, "Offsets the bot yaw." ); +ConVar bot_attack( "bot_attack", "1", 0, "Shoot!" ); + +ConVar bot_sendcmd( "bot_sendcmd", "", 0, "Forces bots to send the specified command." ); + +ConVar bot_crouch( "bot_crouch", "0", 0, "Bot crouches" ); + +static int BotNumber = 1; +static int g_iNextBotTeam = -1; +static int g_iNextBotClass = -1; + +typedef struct +{ + bool backwards; + + float nextturntime; + bool lastturntoright; + + float nextstrafetime; + float sidemove; + + QAngle forwardAngle; + QAngle lastAngles; + + float m_flJoinTeamTime; + int m_WantedTeam; + int m_WantedClass; +} botdata_t; + +static botdata_t g_BotData[ MAX_PLAYERS ]; + + +//----------------------------------------------------------------------------- +// Purpose: Create a new Bot and put it in the game. +// Output : Pointer to the new Bot, or NULL if there's no free clients. +//----------------------------------------------------------------------------- +CBasePlayer *BotPutInServer( bool bFrozen, int iTeam ) +{ + g_iNextBotTeam = iTeam; + + char botname[ 64 ]; + Q_snprintf( botname, sizeof( botname ), "Bot%02i", BotNumber ); + + // This is an evil hack, but we use it to prevent sv_autojointeam from kicking in. + + edict_t *pEdict = engine->CreateFakeClient( botname ); + + if (!pEdict) + { + Msg( "Failed to create Bot.\n"); + return NULL; + } + + // Allocate a CBasePlayer for the bot, and call spawn + //ClientPutInServer( pEdict, botname ); + CHL1MP_Player *pPlayer = ((CHL1MP_Player *)CBaseEntity::Instance( pEdict )); + pPlayer->ClearFlags(); + pPlayer->AddFlag( FL_CLIENT | FL_FAKECLIENT ); + + if ( bFrozen ) + pPlayer->AddEFlags( EFL_BOT_FROZEN ); + + char szReturnString[512]; + + Q_snprintf( szReturnString, sizeof (szReturnString ), "cl_playermodel %s\n", "gman" ); + engine->ClientCommand ( pPlayer->edict(), szReturnString ); + + BotNumber++; + + g_BotData[pPlayer->entindex()-1].m_WantedTeam = iTeam; + g_BotData[pPlayer->entindex()-1].m_flJoinTeamTime = gpGlobals->curtime + 0.3; + + return pPlayer; +} + +//----------------------------------------------------------------------------- +// Purpose: Run through all the Bots in the game and let them think. +//----------------------------------------------------------------------------- +void Bot_RunAll( void ) +{ + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CHL1MP_Player *pPlayer = ToHL1MPPlayer( UTIL_PlayerByIndex( i ) ); + + if ( pPlayer && (pPlayer->GetFlags() & FL_FAKECLIENT) ) + { + Bot_Think( pPlayer ); + } + } +} + +bool RunMimicCommand( CUserCmd& cmd ) +{ + if ( bot_mimic.GetInt() <= 0 ) + return false; + + if ( bot_mimic.GetInt() > gpGlobals->maxClients ) + return false; + + + CBasePlayer *pPlayer = UTIL_PlayerByIndex( bot_mimic.GetInt() ); + if ( !pPlayer ) + return false; + + if ( !pPlayer->GetLastUserCommand() ) + return false; + + cmd = *pPlayer->GetLastUserCommand(); + cmd.viewangles[YAW] += bot_mimic_yaw_offset.GetFloat(); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Simulates a single frame of movement for a player +// Input : *fakeclient - +// *viewangles - +// forwardmove - +// sidemove - +// upmove - +// buttons - +// impulse - +// msec - +// Output : virtual void +//----------------------------------------------------------------------------- +static void RunPlayerMove( CHL1MP_Player *fakeclient, const QAngle& viewangles, float forwardmove, float sidemove, float upmove, unsigned short buttons, byte impulse, float frametime ) +{ + if ( !fakeclient ) + return; + + CUserCmd cmd; + + // Store off the globals.. they're gonna get whacked + float flOldFrametime = gpGlobals->frametime; + float flOldCurtime = gpGlobals->curtime; + + float flTimeBase = gpGlobals->curtime + gpGlobals->frametime - frametime; + fakeclient->SetTimeBase( flTimeBase ); + + Q_memset( &cmd, 0, sizeof( cmd ) ); + + if ( !RunMimicCommand( cmd ) && !bot_zombie.GetBool() ) + { + VectorCopy( viewangles, cmd.viewangles ); + cmd.forwardmove = forwardmove; + cmd.sidemove = sidemove; + cmd.upmove = upmove; + cmd.buttons = buttons; + cmd.impulse = impulse; + cmd.random_seed = random->RandomInt( 0, 0x7fffffff ); + } + + if( bot_crouch.GetInt() ) + cmd.buttons |= IN_DUCK; + + if ( bot_attack.GetBool() ) + cmd.buttons |= IN_ATTACK; + + MoveHelperServer()->SetHost( fakeclient ); + fakeclient->PlayerRunCommand( &cmd, MoveHelperServer() ); + + // save off the last good usercmd + fakeclient->SetLastUserCommand( cmd ); + + // Clear out any fixangle that has been set + fakeclient->pl.fixangle = FIXANGLE_NONE; + + // Restore the globals.. + gpGlobals->frametime = flOldFrametime; + gpGlobals->curtime = flOldCurtime; +} + +//----------------------------------------------------------------------------- +// Purpose: Run this Bot's AI for one frame. +//----------------------------------------------------------------------------- +void Bot_Think( CHL1MP_Player *pBot ) +{ + // Make sure we stay being a bot + pBot->AddFlag( FL_FAKECLIENT ); + + botdata_t *botdata = &g_BotData[ ENTINDEX( pBot->edict() ) - 1 ]; + + QAngle vecViewAngles; + float forwardmove = 0.0; + float sidemove = botdata->sidemove; + float upmove = 0.0; + unsigned short buttons = 0; + byte impulse = 0; + float frametime = gpGlobals->frametime; + + vecViewAngles = pBot->GetLocalAngles(); + + + // Create some random values + if ( pBot->IsAlive() && (pBot->GetSolid() == SOLID_BBOX) ) + { + trace_t trace; + + // Stop when shot + if ( !pBot->IsEFlagSet(EFL_BOT_FROZEN) ) + { + if ( pBot->m_iHealth == 100 ) + { + forwardmove = 600 * ( botdata->backwards ? -1 : 1 ); + if ( botdata->sidemove != 0.0f ) + { + forwardmove *= random->RandomFloat( 0.1, 1.0f ); + } + } + else + { + forwardmove = 0; + } + } + + // Only turn if I haven't been hurt + if ( !pBot->IsEFlagSet(EFL_BOT_FROZEN) && pBot->m_iHealth == 100 ) + { + Vector vecEnd; + Vector forward; + + QAngle angle; + float angledelta = 15.0; + + int maxtries = (int)360.0/angledelta; + + if ( botdata->lastturntoright ) + { + angledelta = -angledelta; + } + + angle = pBot->GetLocalAngles(); + + Vector vecSrc; + while ( --maxtries >= 0 ) + { + AngleVectors( angle, &forward ); + + vecSrc = pBot->GetLocalOrigin() + Vector( 0, 0, 36 ); + + vecEnd = vecSrc + forward * 10; + + UTIL_TraceHull( vecSrc, vecEnd, VEC_HULL_MIN_SCALED( pBot ), VEC_HULL_MAX_SCALED( pBot ), + MASK_PLAYERSOLID, pBot, COLLISION_GROUP_NONE, &trace ); + + if ( trace.fraction == 1.0 ) + { + if ( gpGlobals->curtime < botdata->nextturntime ) + { + break; + } + } + + angle.y += angledelta; + + if ( angle.y > 180 ) + angle.y -= 360; + else if ( angle.y < -180 ) + angle.y += 360; + + botdata->nextturntime = gpGlobals->curtime + 2.0; + botdata->lastturntoright = random->RandomInt( 0, 1 ) == 0 ? true : false; + + botdata->forwardAngle = angle; + botdata->lastAngles = angle; + + } + + + if ( gpGlobals->curtime >= botdata->nextstrafetime ) + { + botdata->nextstrafetime = gpGlobals->curtime + 1.0f; + + if ( random->RandomInt( 0, 5 ) == 0 ) + { + botdata->sidemove = -600.0f + 1200.0f * random->RandomFloat( 0, 2 ); + } + else + { + botdata->sidemove = 0; + } + sidemove = botdata->sidemove; + + if ( random->RandomInt( 0, 20 ) == 0 ) + { + botdata->backwards = true; + } + else + { + botdata->backwards = false; + } + } + + pBot->SetLocalAngles( angle ); + vecViewAngles = angle; + } + + // Is my team being forced to defend? + if ( bot_defend.GetInt() == pBot->GetTeamNumber() ) + { + buttons |= IN_ATTACK2; + } + // If bots are being forced to fire a weapon, see if I have it + else if ( bot_forcefireweapon.GetString() ) + { + CBaseCombatWeapon *pWeapon = pBot->Weapon_OwnsThisType( bot_forcefireweapon.GetString() ); + if ( pWeapon ) + { + // Switch to it if we don't have it out + CBaseCombatWeapon *pActiveWeapon = pBot->GetActiveWeapon(); + + // Switch? + if ( pActiveWeapon != pWeapon ) + { + pBot->Weapon_Switch( pWeapon ); + } + else + { + // Start firing + // Some weapons require releases, so randomise firing + if ( bot_forceattackon.GetBool() || (RandomFloat(0.0,1.0) > 0.5) ) + { + buttons |= bot_forceattack2.GetBool() ? IN_ATTACK2 : IN_ATTACK; + } + } + } + } + + if ( bot_flipout.GetInt() ) + { + if ( bot_forceattackon.GetBool() || (RandomFloat(0.0,1.0) > 0.5) ) + { + buttons |= bot_forceattack2.GetBool() ? IN_ATTACK2 : IN_ATTACK; + } + } + + if ( strlen( bot_sendcmd.GetString() ) > 0 ) + { + //send the cmd from this bot + CCommand args; + args.Tokenize( bot_sendcmd.GetString() ); + pBot->ClientCommand( args ); + + bot_sendcmd.SetValue(""); + } + } + else + { + // Wait for Reinforcement wave + if ( !pBot->IsAlive() ) + { + // Try hitting my buttons occasionally + if ( random->RandomInt( 0, 100 ) > 80 ) + { + // Respawn the bot + if ( random->RandomInt( 0, 1 ) == 0 ) + { + buttons |= IN_JUMP; + } + else + { + buttons = 0; + } + } + } + } + + if ( bot_flipout.GetInt() >= 2 ) + { + + QAngle angOffset = RandomAngle( -1, 1 ); + + botdata->lastAngles += angOffset; + + for ( int i = 0 ; i < 2; i++ ) + { + if ( fabs( botdata->lastAngles[ i ] - botdata->forwardAngle[ i ] ) > 15.0f ) + { + if ( botdata->lastAngles[ i ] > botdata->forwardAngle[ i ] ) + { + botdata->lastAngles[ i ] = botdata->forwardAngle[ i ] + 15; + } + else + { + botdata->lastAngles[ i ] = botdata->forwardAngle[ i ] - 15; + } + } + } + + botdata->lastAngles[ 2 ] = 0; + + pBot->SetLocalAngles( botdata->lastAngles ); + } + + RunPlayerMove( pBot, pBot->GetLocalAngles(), forwardmove, sidemove, upmove, buttons, impulse, frametime ); +} + +#endif + diff --git a/game/server/hl1/hl1mp_bot_temp.h b/game/server/hl1/hl1mp_bot_temp.h new file mode 100644 index 0000000..f78e63e --- /dev/null +++ b/game/server/hl1/hl1mp_bot_temp.h @@ -0,0 +1,19 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// +#ifndef BOT_BASE_H +#define BOT_BASE_H +#ifdef _WIN32 +#pragma once +#endif + + +// If iTeam or iClass is -1, then a team or class is randomly chosen. +CBasePlayer *BotPutInServer( bool bFrozen, int iTeam ); + + +#endif // BOT_BASE_H + diff --git a/game/server/hl1/hl1mp_gameinterface.cpp b/game/server/hl1/hl1mp_gameinterface.cpp new file mode 100644 index 0000000..afbe164 --- /dev/null +++ b/game/server/hl1/hl1mp_gameinterface.cpp @@ -0,0 +1,28 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "gameinterface.h" +#include "mapentities.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +void CServerGameClients::GetPlayerLimits( int& minplayers, int& maxplayers, int &defaultMaxPlayers ) const +{ + minplayers = defaultMaxPlayers = 8; + maxplayers = MAX_PLAYERS - 1; +} + + +// -------------------------------------------------------------------------------------------- // +// Mod-specific CServerGameDLL implementation. +// -------------------------------------------------------------------------------------------- // + +void CServerGameDLL::LevelInit_ParseAllEntities( const char *pMapEntities ) +{ + MapEntity_ParseAllEntities( pMapEntities, NULL ); +} diff --git a/game/server/hl1/hl1mp_player.cpp b/game/server/hl1/hl1mp_player.cpp new file mode 100644 index 0000000..84adbfe --- /dev/null +++ b/game/server/hl1/hl1mp_player.cpp @@ -0,0 +1,636 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Multiplayer Player for HL1. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "hl1mp_player.h" +#include "client.h" +#include "team.h" + +class CTEPlayerAnimEvent : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEPlayerAnimEvent, CBaseTempEntity ); + DECLARE_SERVERCLASS(); + + CTEPlayerAnimEvent( const char *name ) : CBaseTempEntity( name ) + { + } + + CNetworkHandle( CBasePlayer, m_hPlayer ); + CNetworkVar( int, m_iEvent ); + CNetworkVar( int, m_nData ); +}; + +IMPLEMENT_SERVERCLASS_ST_NOBASE( CTEPlayerAnimEvent, DT_TEPlayerAnimEvent ) + SendPropEHandle( SENDINFO( m_hPlayer ) ), + SendPropInt( SENDINFO( m_iEvent ), Q_log2( PLAYERANIMEVENT_COUNT ) + 1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_nData ), 32 ) +END_SEND_TABLE() + +static CTEPlayerAnimEvent g_TEPlayerAnimEvent( "PlayerAnimEvent" ); + +void TE_PlayerAnimEvent( CBasePlayer *pPlayer, PlayerAnimEvent_t event, int nData ) +{ + CPVSFilter filter( pPlayer->EyePosition() ); + + // The player himself doesn't need to be sent his animation events + // unless cs_showanimstate wants to show them. +// if ( !ToolsEnabled() && ( cl_showanimstate.GetInt() == pPlayer->entindex() ) ) + { +// filter.RemoveRecipient( pPlayer ); + } + + g_TEPlayerAnimEvent.m_hPlayer = pPlayer; + g_TEPlayerAnimEvent.m_iEvent = event; + g_TEPlayerAnimEvent.m_nData = nData; + g_TEPlayerAnimEvent.Create( filter, 0 ); +} + + +////////////////////////////////////////////////////////////////////////////////////////// + +extern int gEvilImpulse101; + +LINK_ENTITY_TO_CLASS( player_mp, CHL1MP_Player ); +PRECACHE_REGISTER( player_mp ); + +IMPLEMENT_SERVERCLASS_ST( CHL1MP_Player, DT_HL1MP_PLAYER ) + SendPropExclude( "DT_BaseAnimating", "m_flPoseParameter" ), + SendPropExclude( "DT_BaseAnimating", "m_flPlaybackRate" ), + SendPropExclude( "DT_BaseAnimating", "m_nSequence" ), + SendPropExclude( "DT_BaseEntity", "m_angRotation" ), + SendPropExclude( "DT_BaseAnimatingOverlay", "overlay_vars" ), + + // cs_playeranimstate and clientside animation takes care of these on the client +// SendPropExclude( "DT_ServerAnimationData" , "m_flCycle" ), + SendPropExclude( "DT_AnimTimeMustBeFirst" , "m_flAnimTime" ), + + SendPropAngle( SENDINFO_VECTORELEM(m_angEyeAngles, 0), 11 ), + SendPropAngle( SENDINFO_VECTORELEM(m_angEyeAngles, 1), 11 ), + + SendPropEHandle( SENDINFO( m_hRagdoll ) ), + SendPropInt( SENDINFO( m_iSpawnInterpCounter), 4 ), + SendPropInt( SENDINFO( m_iRealSequence ), 9 ), + + +// SendPropDataTable( SENDINFO_DT( m_Shared ), &REFERENCE_SEND_TABLE( DT_TFCPlayerShared ) ) +END_SEND_TABLE() + +void cc_CreatePredictionError_f() +{ + CBaseEntity *pEnt = CBaseEntity::Instance( 1 ); + pEnt->SetAbsOrigin( pEnt->GetAbsOrigin() + Vector( 63, 0, 0 ) ); +} + +ConCommand cc_CreatePredictionError( "CreatePredictionError", cc_CreatePredictionError_f, "Create a prediction error", FCVAR_CHEAT ); + +static const char * s_szModelPath = "models/player/mp/"; + +CHL1MP_Player::CHL1MP_Player() +{ + m_PlayerAnimState = CreatePlayerAnimState( this ); +// item_list = 0; + + UseClientSideAnimation(); + m_angEyeAngles.Init(); +// m_pCurStateInfo = NULL; + m_lifeState = LIFE_DEAD; // Start "dead". + + m_iSpawnInterpCounter = 0; + m_flNextModelChangeTime = 0; + m_flNextTeamChangeTime = 0; + +// SetViewOffset( TFC_PLAYER_VIEW_OFFSET ); + +// SetContextThink( &CTFCPlayer::TFCPlayerThink, gpGlobals->curtime, "TFCPlayerThink" ); +} + +CHL1MP_Player::~CHL1MP_Player() +{ + m_PlayerAnimState->Release(); +} + +void CHL1MP_Player::PostThink( void ) +{ + BaseClass::PostThink(); + + QAngle angles = GetLocalAngles(); + angles[PITCH] = 0; + SetLocalAngles( angles ); + + // Store the eye angles pitch so the client can compute its animation state correctly. + m_angEyeAngles = EyeAngles(); + + m_PlayerAnimState->Update( m_angEyeAngles[YAW], m_angEyeAngles[PITCH] ); +} + +void CHL1MP_Player::Spawn( void ) +{ + if ( !IsObserver() ) + { + RemoveEffects( EF_NODRAW ); + SetMoveType( MOVETYPE_WALK ); + RemoveSolidFlags( FSOLID_NOT_SOLID ); + + // if no model, force one + if ( !GetModelPtr() ) + SetModel( "models/player/mp/gordon/gordon.mdl" ); + } + + m_flNextModelChangeTime = 0; + m_flNextTeamChangeTime = 0; + + BaseClass::Spawn(); + + if ( !IsObserver() ) + { + GiveDefaultItems(); + SetPlayerModel(); + } + + m_bHasLongJump = false; + + m_iSpawnInterpCounter = (m_iSpawnInterpCounter + 1) % 8; +} + +void CHL1MP_Player::DoAnimationEvent( PlayerAnimEvent_t event, int nData ) +{ + m_PlayerAnimState->DoAnimationEvent( event, nData ); + TE_PlayerAnimEvent( this, event, nData ); // Send to any clients who can see this guy. +} + +void CHL1MP_Player::GiveDefaultItems( void ) +{ + GiveNamedItem( "weapon_crowbar" ); + GiveNamedItem( "weapon_glock" ); + + CBasePlayer::GiveAmmo( 68, "9mmRound" ); +} + +void CHL1MP_Player::UpdateOnRemove( void ) +{ + if ( m_hRagdoll ) + { + UTIL_RemoveImmediate( m_hRagdoll ); + m_hRagdoll = NULL; + } + + BaseClass::UpdateOnRemove(); +} + + +void CHL1MP_Player::DetonateSatchelCharges( void ) +{ + CBaseEntity *pSatchel = NULL; + + while ( (pSatchel = gEntList.FindEntityByClassname( pSatchel, "monster_satchel" ) ) != NULL) + { + if ( pSatchel->GetOwnerEntity() == this ) + { + pSatchel->Use( this, this, USE_ON, 0 ); + } + } +} + +void CHL1MP_Player::Event_Killed( const CTakeDamageInfo &info ) +{ + DoAnimationEvent( PLAYERANIMEVENT_DIE ); +// SetNumAnimOverlays( 0 ); + + + // Note: since we're dead, it won't draw us on the client, but we don't set EF_NODRAW + // because we still want to transmit to the clients in our PVS. + if ( !IsHLTV() ) + CreateRagdollEntity(); + + DetonateSatchelCharges(); + + BaseClass::Event_Killed( info ); + + m_lifeState = LIFE_DEAD; + RemoveEffects( EF_NODRAW ); // still draw player body +} + + +void CHL1MP_Player::SetAnimation( PLAYER_ANIM playerAnim ) +{ +// BaseClass::SetAnimation( playerAnim ); + if ( playerAnim == PLAYER_ATTACK1 ) + { + DoAnimationEvent( PLAYERANIMEVENT_FIRE_GUN ); + } + + int animDesired; + char szAnim[64]; + + float speed; + + speed = GetAbsVelocity().Length2D(); + + if (GetFlags() & (FL_FROZEN|FL_ATCONTROLS)) + { + speed = 0; + playerAnim = PLAYER_IDLE; + } + + if ( playerAnim == PLAYER_ATTACK1 ) + { + if ( speed > 0 ) + { + playerAnim = PLAYER_WALK; + } + else + { + playerAnim = PLAYER_IDLE; + } + } + + Activity idealActivity = ACT_WALK;// TEMP!!!!! + + // This could stand to be redone. Why is playerAnim abstracted from activity? (sjb) + if (playerAnim == PLAYER_JUMP) + { + idealActivity = ACT_HOP; + } + else if (playerAnim == PLAYER_SUPERJUMP) + { + idealActivity = ACT_LEAP; + } + else if (playerAnim == PLAYER_DIE) + { + if ( m_lifeState == LIFE_ALIVE ) + { + idealActivity = ACT_DIERAGDOLL; + } + } + else if (playerAnim == PLAYER_ATTACK1) + { + if ( GetActivity() == ACT_HOVER || + GetActivity() == ACT_SWIM || + GetActivity() == ACT_HOP || + GetActivity() == ACT_LEAP || + GetActivity() == ACT_DIESIMPLE ) + { + idealActivity = GetActivity(); + } + else + { + idealActivity = ACT_RANGE_ATTACK1; + } + } + else if (playerAnim == PLAYER_IDLE || playerAnim == PLAYER_WALK) + { + if ( !( GetFlags() & FL_ONGROUND ) && (GetActivity() == ACT_HOP || GetActivity() == ACT_LEAP) ) // Still jumping + { + idealActivity = GetActivity(); + } + else if ( GetWaterLevel() > 1 ) + { + if ( speed == 0 ) + idealActivity = ACT_HOVER; + else + idealActivity = ACT_SWIM; + } + else if ( speed > 0 ) + { + idealActivity = ACT_WALK; + } + else + { + idealActivity = ACT_IDLE; + } + } + + + if (idealActivity == ACT_RANGE_ATTACK1) + { + if ( GetFlags() & FL_DUCKING ) // crouching + { + Q_strncpy( szAnim, "crouch_shoot_" ,sizeof(szAnim)); + } + else + { + Q_strncpy( szAnim, "ref_shoot_" ,sizeof(szAnim)); + } + Q_strncat( szAnim, m_szAnimExtension ,sizeof(szAnim), COPY_ALL_CHARACTERS ); + animDesired = LookupSequence( szAnim ); + if (animDesired == -1) + animDesired = 0; + + if ( GetSequence() != animDesired || !SequenceLoops() ) + { + SetCycle( 0 ); + } + + // Tracker 24588: In single player when firing own weapon this causes eye and punchangle to jitter + //if (!SequenceLoops()) + //{ + // IncrementInterpolationFrame(); + //} + + SetActivity( idealActivity ); + ResetSequence( animDesired ); + } + else if (idealActivity == ACT_IDLE) + { + if ( GetFlags() & FL_DUCKING ) + { + animDesired = LookupSequence( "crouch_idle" ); + } + else + { + animDesired = LookupSequence( "look_idle" ); + } + if (animDesired == -1) + animDesired = 0; + + SetActivity( ACT_IDLE ); + } + else if ( idealActivity == ACT_WALK ) + { + if ( GetFlags() & FL_DUCKING ) + { + animDesired = SelectWeightedSequence( ACT_CROUCH ); + SetActivity( ACT_CROUCH ); + } + else + { + animDesired = SelectWeightedSequence( ACT_RUN ); + SetActivity( ACT_RUN ); + } + + } + else + { + if ( GetActivity() == idealActivity) + return; + + SetActivity( idealActivity ); + + animDesired = SelectWeightedSequence( GetActivity() ); + + // Already using the desired animation? + if (GetSequence() == animDesired) + return; + + m_iRealSequence = animDesired; + ResetSequence( animDesired ); + SetCycle( 0 ); + return; + } + + // Already using the desired animation? + if (GetSequence() == animDesired) + return; + + m_iRealSequence = animDesired; + + //Msg( "Set animation to %d\n", animDesired ); + // Reset to first frame of desired animation + ResetSequence( animDesired ); + SetCycle( 0 ); +} + +static ConVar sv_debugweaponpickup( "sv_debugweaponpickup", "0", FCVAR_CHEAT, "Prints descriptive reasons as to why pickup did not work." ); + +// correct respawning of weapons +bool CHL1MP_Player::BumpWeapon( CBaseCombatWeapon *pWeapon ) +{ CBaseCombatCharacter *pOwner = pWeapon->GetOwner(); + + // Can I have this weapon type? + if ( !IsAllowedToPickupWeapons() ) + { + if ( sv_debugweaponpickup.GetBool() ) + Msg("sv_debugweaponpickup: IsAllowedToPickupWeapons() returned false\n"); + + return false; + } + + if ( pOwner || !Weapon_CanUse( pWeapon ) || !g_pGameRules->CanHavePlayerItem( this, pWeapon ) ) + { + if ( sv_debugweaponpickup.GetBool() && pOwner ) + Msg("sv_debugweaponpickup: pOwner\n"); + + if ( sv_debugweaponpickup.GetBool() && !Weapon_CanUse( pWeapon ) ) + Msg("sv_debugweaponpickup: Can't use weapon\n"); + + if ( sv_debugweaponpickup.GetBool() && !g_pGameRules->CanHavePlayerItem( this, pWeapon ) ) + Msg("sv_debugweaponpickup: Gamerules says player can't have item\n"); + + if ( gEvilImpulse101 ) + { + UTIL_Remove( pWeapon ); + } + return false; + } + + // Don't let the player fetch weapons through walls (use MASK_SOLID so that you can't pickup through windows) + if( !pWeapon->FVisible( this, MASK_SOLID ) && !(GetFlags() & FL_NOTARGET) ) + { + if ( sv_debugweaponpickup.GetBool() && !FVisible( this, MASK_SOLID ) ) + Msg("sv_debugweaponpickup: Can't fetch weapon through a wall\n"); + + if ( sv_debugweaponpickup.GetBool() && !(GetFlags() & FL_NOTARGET) ) + Msg("sv_debugweaponpickup: NoTarget\n"); + + return false; + } + + bool bOwnsWeaponAlready = !!Weapon_OwnsThisType( pWeapon->GetClassname(), pWeapon->GetSubType()); + + if ( bOwnsWeaponAlready == true ) + { + //If we have room for the ammo, then "take" the weapon too. + if ( Weapon_EquipAmmoOnly( pWeapon ) ) + { + pWeapon->CheckRespawn(); + + UTIL_Remove( pWeapon ); + + if ( sv_debugweaponpickup.GetBool() ) + Msg("sv_debugweaponpickup: Picking up weapon\n"); + + return true; + } + else + { + if ( sv_debugweaponpickup.GetBool() ) + Msg("sv_debugweaponpickup: Owns weapon already\n"); + + return false; + } + } + + pWeapon->CheckRespawn(); + Weapon_Equip( pWeapon ); + + if ( sv_debugweaponpickup.GetBool() ) + Msg("sv_debugweaponpickup: Picking up weapon\n"); + + return true; +} + + +void CHL1MP_Player::ChangeTeam( int iTeamNum ) +{ + bool bKill = false; + + if ( g_pGameRules->IsTeamplay() == true ) + { + if ( iTeamNum != GetTeamNumber() && GetTeamNumber() != TEAM_UNASSIGNED ) + { + bKill = true; + } + } + + BaseClass::ChangeTeam( iTeamNum ); + + m_flNextTeamChangeTime = gpGlobals->curtime + 5; + + if ( g_pGameRules->IsTeamplay() == true ) + { + SetPlayerTeamModel(); + } + else + { + SetPlayerModel(); + } + + if ( bKill == true ) + { + CommitSuicide(); + } +} + +void CHL1MP_Player::SetPlayerTeamModel( void ) +{ + int iTeamNum = GetTeamNumber(); + + if ( iTeamNum <= TEAM_SPECTATOR ) + return; + + CTeam * pTeam = GetGlobalTeam( iTeamNum ); + + char szModelName[256]; + Q_snprintf( szModelName, 256, "%s%s/%s.mdl", s_szModelPath, pTeam->GetName(), pTeam->GetName() ); + + // Check to see if the model was properly precached, do not error out if not. + int i = modelinfo->GetModelIndex( szModelName ); + if ( i == -1 ) + { + Warning("Model %s does not exist.\n", szModelName ); + return; + } + + SetModel( szModelName ); + m_flNextModelChangeTime = gpGlobals->curtime + 5; +} + + +void CHL1MP_Player::SetPlayerModel( void ) +{ + char szBaseName[128]; + Q_FileBase( engine->GetClientConVarValue( engine->IndexOfEdict( edict() ), "cl_playermodel" ), szBaseName, 128 ); + + // Don't let it be 'none'; default to Barney + if ( Q_stricmp( "none", szBaseName ) == 0 ) + { + Q_strcpy( szBaseName, "gordon" ); + } + + char szModelName[256]; + Q_snprintf( szModelName, 256, "%s%s/%s.mdl", s_szModelPath, szBaseName, szBaseName ); + + // Check to see if the model was properly precached, do not error out if not. + int i = modelinfo->GetModelIndex( szModelName ); + if ( i == -1 ) + { + SetModel( "models/player/mp/gordon/gordon.mdl" ); + engine->ClientCommand ( edict(), "cl_playermodel models/gordon.mdl\n" ); + return; + } + + SetModel( szModelName ); + + m_flNextModelChangeTime = gpGlobals->curtime + 5; +} + + +// -------------------------------------------------------------------------------- // +// Ragdoll entities. +// -------------------------------------------------------------------------------- // + +class CHL1MPRagdoll : public CBaseAnimatingOverlay +{ +public: + DECLARE_CLASS( CHL1MPRagdoll, CBaseAnimatingOverlay ); + DECLARE_SERVERCLASS(); + + // Transmit ragdolls to everyone. + virtual int UpdateTransmitState() + { + return SetTransmitState( FL_EDICT_ALWAYS ); + } + +public: + // In case the client has the player entity, we transmit the player index. + // In case the client doesn't have it, we transmit the player's model index, origin, and angles + // so they can create a ragdoll in the right place. + CNetworkHandle( CBaseEntity, m_hPlayer ); // networked entity handle + CNetworkVector( m_vecRagdollVelocity ); + CNetworkVector( m_vecRagdollOrigin ); +}; + +LINK_ENTITY_TO_CLASS( hl1mp_ragdoll, CHL1MPRagdoll ); + +IMPLEMENT_SERVERCLASS_ST_NOBASE( CHL1MPRagdoll, DT_HL1MPRagdoll ) + SendPropVector ( SENDINFO( m_vecRagdollOrigin), -1, SPROP_COORD ), + SendPropEHandle ( SENDINFO( m_hPlayer ) ), + SendPropModelIndex( SENDINFO( m_nModelIndex ) ), + SendPropInt ( SENDINFO( m_nForceBone), 8, 0 ), + SendPropVector ( SENDINFO( m_vecForce), -1, SPROP_NOSCALE ), + SendPropVector ( SENDINFO( m_vecRagdollVelocity ) ) +END_SEND_TABLE() + + +void CHL1MP_Player::CreateRagdollEntity( void ) +{ + if ( m_hRagdoll ) + { + UTIL_RemoveImmediate( m_hRagdoll ); + m_hRagdoll = NULL; + } + + // If we already have a ragdoll, don't make another one. + CHL1MPRagdoll *pRagdoll = dynamic_cast< CHL1MPRagdoll* >(m_hRagdoll.Get() ); + + if ( !pRagdoll ) + { + // Create a new one + pRagdoll = dynamic_cast< CHL1MPRagdoll* >( CreateEntityByName( "hl1mp_ragdoll" ) ); + } + + if ( pRagdoll ) + { + pRagdoll->m_hPlayer = this; + pRagdoll->m_vecRagdollOrigin = GetAbsOrigin(); + pRagdoll->m_vecRagdollVelocity = GetAbsVelocity(); + pRagdoll->m_nModelIndex = m_nModelIndex; + pRagdoll->m_nForceBone = m_nForceBone; + //pRagdoll->m_vecForce = m_vecTotalBulletForce; + pRagdoll->SetAbsOrigin( GetAbsOrigin() ); + + } + + m_hRagdoll = pRagdoll; +} + +void CHL1MP_Player::CreateCorpse( void ) +{ + +} + diff --git a/game/server/hl1/hl1mp_player.h b/game/server/hl1/hl1mp_player.h new file mode 100644 index 0000000..5ada43f --- /dev/null +++ b/game/server/hl1/hl1mp_player.h @@ -0,0 +1,87 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#ifndef HL1MP_PLAYER_H +#define HL1MP_PLAYER_H +#pragma once + +#include "cbase.h" +#include "hl1_player_shared.h" +#include "hl1_player.h" +#include "takedamageinfo.h" + + +class CHL1MP_Player; + + +//============================================================================= +// >> HL1MP_Player +//============================================================================= +class CHL1MP_Player : public CHL1_Player +{ +public: + DECLARE_CLASS( CHL1MP_Player, CHL1_Player ); + DECLARE_SERVERCLASS(); + + CHL1MP_Player(); + ~CHL1MP_Player( void ); + + virtual void Event_Killed( const CTakeDamageInfo &info ); + virtual void Spawn( void ); + virtual void PostThink( void ); + virtual void SetAnimation( PLAYER_ANIM playerAnim ); + void GiveDefaultItems( void ); + void CreateRagdollEntity( void ); + void UpdateOnRemove( void ); + virtual bool BecomeRagdollOnClient( const Vector &force ) { return true; }; + virtual void CreateCorpse( void ); + + virtual bool BumpWeapon( CBaseCombatWeapon *pWeapon ); + + virtual void ChangeTeam( int iTeamNum ) OVERRIDE; + + void SetPlayerTeamModel( void ); + + float GetNextModelChangeTime( void ) { return m_flNextModelChangeTime; } + float GetNextTeamChangeTime( void ) { return m_flNextTeamChangeTime; } + + void SetPlayerModel( void ); + + void DoAnimationEvent( PlayerAnimEvent_t event, int nData = 0 ); + + virtual bool StartObserverMode (int mode) + { + if ( !IsHLTV() ) + return false; + return BaseClass::StartObserverMode( mode ); + } + + void DetonateSatchelCharges( void ); + + CNetworkVar( int, m_iRealSequence ); + +private: + CNetworkHandle( CBaseEntity, m_hRagdoll ); + CNetworkVar( int, m_iSpawnInterpCounter ); + CNetworkQAngle( m_angEyeAngles ); + + IHL1MPPlayerAnimState* m_PlayerAnimState; + float m_flNextModelChangeTime; + float m_flNextTeamChangeTime; +}; + +inline CHL1MP_Player *ToHL1MPPlayer( CBaseEntity *pEntity ) +{ + if ( !pEntity || !pEntity->IsPlayer() ) + return NULL; + + return dynamic_cast<CHL1MP_Player*>( pEntity ); +} + + +#endif //HL1MP_PLAYER_H |