diff --git a/cfg/monster_precache.cfg b/cfg/monster_precache.cfg index afed89c..ad29b3e 100755 --- a/cfg/monster_precache.cfg +++ b/cfg/monster_precache.cfg @@ -30,3 +30,4 @@ //monster_gonome //monster_male_assassin //monster_otis +//monster_pitdrone diff --git a/cfg/monster_skill.cfg b/cfg/monster_skill.cfg index 37c3d53..8d5a539 100755 --- a/cfg/monster_skill.cfg +++ b/cfg/monster_skill.cfg @@ -103,6 +103,11 @@ sk_massassin_kick 25 // Otis sk_otis_health 35 +// Pit Drone +sk_pitdrone_health 40 +sk_pitdrone_dmg_bite 25 +sk_pitdrone_dmg_whip 35 +sk_pitdrone_dmg_spit 10 // MONSTER WEAPON DAMAGE sk_9mm_bullet 5 diff --git a/src/dlls/Makefile b/src/dlls/Makefile index 523b9bb..a773a65 100644 --- a/src/dlls/Makefile +++ b/src/dlls/Makefile @@ -37,6 +37,7 @@ OBJ = \ monsterstate.o \ nodes.o \ otis.o \ + pitdrone.o \ scientist.o \ skill.o \ sound.o \ diff --git a/src/dlls/cmbasemonster.h b/src/dlls/cmbasemonster.h index e9ae5c4..b5f3fbd 100644 --- a/src/dlls/cmbasemonster.h +++ b/src/dlls/cmbasemonster.h @@ -213,7 +213,7 @@ public: void AdvanceRoute ( float distance ); virtual BOOL FTriangulate ( const Vector &vecStart , const Vector &vecEnd, float flDist, edict_t *pTargetEnt, Vector *pApex ); void MakeIdealYaw( Vector vecTarget ); - virtual void SetYawSpeed ( void ) { return; };// allows different yaw_speeds for each activity + virtual void SetYawSpeed ( void ) { return; }; // allows different yaw_speeds for each activity BOOL BuildRoute ( const Vector &vecGoal, int iMoveFlag, edict_t *pTarget ); virtual BOOL BuildNearestRoute ( Vector vecThreat, Vector vecViewOffset, float flMinDist, float flMaxDist ); int RouteClassify( int iMoveFlag ); @@ -1257,7 +1257,7 @@ public: }; //========================================================= -// CGonome +// Gonome //========================================================= class CMGonome : public CMBaseMonster { @@ -1358,4 +1358,62 @@ public: int bodystate; }; +//========================================================= +// Pit Drone's spit projectile +//========================================================= +class CPitdroneSpike : public CMBaseEntity +{ +public: + void Spawn(void); + void EXPORT SpikeTouch(edict_t *pOther); + void EXPORT StartTrail(); + static edict_t *Shoot(entvars_t *pevOwner, Vector vecStart, Vector vecVelocity, Vector vecAngles); +}; + +//========================================================= +// Pit Drone +//========================================================= +class CMPitdrone : public CMBaseMonster +{ +public: + void Spawn(void); + void Precache(void); + void HandleAnimEvent(MonsterEvent_t *pEvent); + void SetYawSpeed(void); + int ISoundMask(); + void KeyValue(KeyValueData *pkvd); + + int Classify(void); + + BOOL CheckMeleeAttack1(float flDot, float flDist); + BOOL CheckRangeAttack1(float flDot, float flDist); + void IdleSound(void); + void PainSound(void); + void AlertSound(void); + void DeathSound(void); + void BodyChange(float spikes); + int TakeDamage(entvars_t *pevInflictor, entvars_t *pevAttacker, float flDamage, int bitsDamageType); + int IgnoreConditions(void); + Schedule_t* GetSchedule(void); + Schedule_t* GetScheduleOfType(int Type); + void StartTask(Task_t *pTask); + void RunTask(Task_t *pTask); + void RunAI(void); + void CheckAmmo(); + void GibMonster(); + CUSTOM_SCHEDULES; + + float m_flLastHurtTime; + float m_flNextSpitTime;// last time the PitDrone used the spit attack. + float m_flNextFlinch; + int m_iInitialAmmo; + bool shouldAttackWithLeftClaw; + + static const char *pIdleSounds[]; + static const char *pAlertSounds[]; + static const char *pPainSounds[]; + static const char *pDieSounds[]; + static const char *pAttackMissSounds[]; +}; + #endif // BASEMONSTER_H diff --git a/src/dlls/combat.cpp b/src/dlls/combat.cpp index c40e8b8..0fe4d4a 100644 --- a/src/dlls/combat.cpp +++ b/src/dlls/combat.cpp @@ -177,7 +177,15 @@ void CMGib :: SpawnHeadGib( entvars_t *pevVictim ) pGib->LimitVelocity(); } +// Overload void CMGib :: SpawnRandomGibs( entvars_t *pevVictim, int cGibs, int human ) +{ + if ( human ) + CMGib::SpawnRandomGibs( pevVictim, cGibs, "models/hgibs.mdl", human ); + else + CMGib::SpawnRandomGibs( pevVictim, cGibs, "models/agibs.mdl", human ); +} +void CMGib :: SpawnRandomGibs( entvars_t *pevVictim, int cGibs, const char *pGibModel, int human ) { int cSplat; @@ -191,13 +199,13 @@ void CMGib :: SpawnRandomGibs( entvars_t *pevVictim, int cGibs, int human ) if ( human ) { // human pieces - pGib->Spawn( "models/hgibs.mdl" ); + pGib->Spawn( pGibModel ); pGib->pev->body = RANDOM_LONG(1,HUMAN_GIB_COUNT-1);// start at one to avoid throwing random amounts of skulls (0th gib) } else { // aliens - pGib->Spawn( "models/agibs.mdl" ); + pGib->Spawn( pGibModel ); pGib->pev->body = RANDOM_LONG(0,ALIEN_GIB_COUNT-1); } diff --git a/src/dlls/dllapi.cpp b/src/dlls/dllapi.cpp index 441ab4d..985df8e 100644 --- a/src/dlls/dllapi.cpp +++ b/src/dlls/dllapi.cpp @@ -155,6 +155,7 @@ monster_type_t monster_types[]= "monster_gonome", FALSE, // Opposing Force Monsters "monster_male_assassin", FALSE, "monster_otis", FALSE, + "monster_pitdrone", FALSE, "info_node", FALSE, // Nodes "info_node_air", FALSE, "", FALSE @@ -618,6 +619,7 @@ bool spawn_monster(int monster_type, Vector origin, Vector angles, int respawn_i case 18: monsters[monster_index].pMonster = CreateClassPtr((CMGonome *)NULL); break; case 19: monsters[monster_index].pMonster = CreateClassPtr((CMMassn *)NULL); break; case 20: monsters[monster_index].pMonster = CreateClassPtr((CMOtis *)NULL); break; + case 21: monsters[monster_index].pMonster = CreateClassPtr((CMPitdrone *)NULL); break; } if (monsters[monster_index].pMonster == NULL) @@ -1296,6 +1298,7 @@ void mmServerActivate( edict_t *pEdictList, int edictCount, int clientMax ) CMGonome gonome; CMMassn massn; CMOtis otis; + CMPitdrone pitdrone; g_psv_gravity = CVAR_GET_POINTER( "sv_gravity" ); @@ -1314,7 +1317,7 @@ void mmServerActivate( edict_t *pEdictList, int edictCount, int clientMax ) switch (index) { case 0: agrunt.Precache(); break; - case 1: apache.Precache(); break; + case 1: apache.Precache(); break; case 2: barney.Precache(); break; case 3: bigmomma.Precache(); break; case 4: bullsquid.Precache(); break; @@ -1334,6 +1337,7 @@ void mmServerActivate( edict_t *pEdictList, int edictCount, int clientMax ) case 18: gonome.Precache(); break; case 19: massn.Precache(); break; case 20: otis.Precache(); break; + case 21: pitdrone.Precache(); break; } } } diff --git a/src/dlls/monsters.h b/src/dlls/monsters.h index 6f4fac8..fe8e632 100644 --- a/src/dlls/monsters.h +++ b/src/dlls/monsters.h @@ -150,9 +150,10 @@ public: void LimitVelocity( void ); virtual int ObjectCaps( void ) { return (CMBaseEntity :: ObjectCaps() & ~FCAP_ACROSS_TRANSITION) | FCAP_DONT_SAVE; } - static void SpawnHeadGib( entvars_t *pevVictim ); - static void SpawnRandomGibs( entvars_t *pevVictim, int cGibs, int human ); - static void SpawnStickyGibs( entvars_t *pevVictim, Vector vecOrigin, int cGibs ); + static void SpawnHeadGib( entvars_t *pevVictim ); + static void SpawnRandomGibs( entvars_t *pevVictim, int cGibs, int human ); + static void SpawnRandomGibs( entvars_t *pevVictim, int cGibs, const char *pGibModel, int human ); + static void SpawnStickyGibs( entvars_t *pevVictim, Vector vecOrigin, int cGibs ); int m_bloodColor; int m_cBloodDecals; diff --git a/src/dlls/pitdrone.cpp b/src/dlls/pitdrone.cpp new file mode 100644 index 0000000..f7e8ea0 --- /dev/null +++ b/src/dlls/pitdrone.cpp @@ -0,0 +1,1064 @@ +// HUGE thanks to DrBeef for his hlsdk-xash3d-opfor repository! + +/*** +* +* Copyright (c) 1996-2001, Valve LLC. All rights reserved. +* +* This product contains software technology licensed from Id +* Software, Inc. ("Id Technology"). Id Technology (c) 1996 Id Software, Inc. +* All Rights Reserved. +* +* Use, distribution, and modification of this source code and/or resulting +* object code is restricted to non-commercial enhancements to products from +* Valve LLC. All other use, distribution, or modification is prohibited +* without written permission from Valve LLC. +* +****/ + +#include "extdll.h" +#include "util.h" +#include "cmbase.h" +#include "cmbasemonster.h" +#include "monsters.h" +#include "schedule.h" +#include "nodes.h" +#include "effects.h" +#include "decals.h" + +/* + * In Opposing Force pitdrone spawned via monstermaker did not have spikes + * That's probably a bug, because number of spikes is set in level editor, + * so spawned pitdrones always had 0 spikes. + * Having no spikes after spawn also prevented spike reloading. + * Those who want to keep original Opposing Force behavior can set this constant to zero. + */ +#define FEATURE_PITDRONE_SPAWN_WITH_SPIKES 1 + +// Disable this feature if you don't want to include spike_trail.spr in your mod +#define FEATURE_PITDRONE_SPIKE_TRAIL 1 + +#if FEATURE_PITDRONE_SPIKE_TRAIL +int iSpikeTrail; +#endif +int iPitdroneSpitSprite; + +void CPitdroneSpike::Spawn(void) +{ + pev->movetype = MOVETYPE_FLY; + pev->classname = MAKE_STRING("pitdronespike"); + + pev->solid = SOLID_BBOX; + pev->rendermode = kRenderTransAlpha; + pev->renderamt = 255; + + SET_MODEL(ENT(pev), "models/pit_drone_spike.mdl"); + pev->frame = 0; + pev->scale = 0.5; + + UTIL_SetSize(pev, Vector(-4, -4, -4), Vector(4, 4, 4)); +} + +void CPitdroneSpike::SpikeTouch(edict_t *pOther) +{ + int iPitch; + + // splat sound + iPitch = RANDOM_FLOAT(115, 125); + + if (!pOther->v.takedamage) + { + EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, "weapons/xbow_hit1.wav", 1, ATTN_NORM, 0, iPitch); + + SetThink(&CMBaseEntity::SUB_Remove); + pev->nextthink = gpGlobals->time; + + if (FClassnameIs(pOther, "worldspawn")) + { + // if what we hit is static architecture, can stay around for a while. + Vector vecDir = pev->velocity.Normalize(); + UTIL_SetOrigin(pev, pev->origin - vecDir * 12); + pev->angles = UTIL_VecToAngles(vecDir); + pev->solid = SOLID_NOT; + pev->movetype = MOVETYPE_FLY; + pev->velocity = Vector(0, 0, 0); + pev->avelocity.z = 0; + pev->angles.z = RANDOM_LONG(0, 360); + pev->nextthink = gpGlobals->time + 10.0; + } + } + else + { + entvars_t *pevOwner = VARS(pev->owner); + + if ( UTIL_IsPlayer( pOther ) ) + UTIL_TakeDamage( pOther, pev, pevOwner, gSkillData.pitdroneDmgSpit, DMG_GENERIC | DMG_NEVERGIB ); + else if ( pOther->v.euser4 != NULL ) + { + CMBaseMonster *pMonster = GetClassPtr((CMBaseMonster *)VARS(pOther)); + pMonster->TakeDamage( pev, pevOwner, gSkillData.pitdroneDmgSpit, DMG_GENERIC | DMG_NEVERGIB ); + } + + if (RANDOM_LONG(0,1)) + EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, "weapons/xbow_hitbod1.wav", 1, ATTN_NORM, 0, iPitch); + else + EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, "weapons/xbow_hitbod2.wav", 1, ATTN_NORM, 0, iPitch); + + SetThink( &CMBaseEntity::SUB_Remove ); + pev->nextthink = gpGlobals->time; + } +} + +void CPitdroneSpike::StartTrail() +{ +#if FEATURE_PITDRONE_SPIKE_TRAIL + MESSAGE_BEGIN( MSG_BROADCAST, SVC_TEMPENTITY ); + WRITE_BYTE( TE_BEAMFOLLOW ); + WRITE_SHORT( entindex() ); + WRITE_SHORT( iSpikeTrail ); // model + WRITE_BYTE(2); // life + WRITE_BYTE(1); // width + WRITE_BYTE(197); // r + WRITE_BYTE(194); // g + WRITE_BYTE(11); // b + WRITE_BYTE(192); //brigtness + MESSAGE_END(); +#endif + SetTouch(&CPitdroneSpike::SpikeTouch); +} + +edict_t *CPitdroneSpike::Shoot(entvars_t *pevOwner, Vector vecStart, Vector vecVelocity, Vector vecAngles) +{ + CPitdroneSpike *pSpit = CreateClassPtr( (CPitdroneSpike *)NULL ); + + if (pSpit == NULL) + return NULL; + + pSpit->Spawn(); + + UTIL_SetOrigin( pSpit->pev, vecStart ); + pSpit->pev->velocity = vecVelocity; + pSpit->pev->angles = vecAngles; + pSpit->pev->owner = ENT( pevOwner ); + + pSpit->SetThink(&CPitdroneSpike::StartTrail); + pSpit->pev->nextthink = gpGlobals->time + 0.1; + + return pSpit->edict(); +} + +// +// PitDrone, main part. +// +#define HORNGROUP 1 +#define PITDRONE_HORNS0 0 +#define PITDRONE_HORNS1 1 +#define PITDRONE_HORNS2 2 +#define PITDRONE_HORNS3 3 +#define PITDRONE_HORNS4 4 +#define PITDRONE_HORNS5 5 +#define PITDRONE_HORNS6 6 +#define PITDRONE_SPRINT_DIST 255 +#define PITDRONE_FLINCH_DELAY 2 // at most one flinch every n secs +#define PITDRONE_MAX_HORNS 6 +#define PITDRONE_GIB_COUNT 7 + +//========================================================= +// monster-specific schedule types +//========================================================= +enum +{ + SCHED_PDRONE_HURTHOP = LAST_COMMON_SCHEDULE + 1, + SCHED_PDRONE_SMELLFOOD, + SCHED_PDRONE_EAT, + SCHED_PDRONE_SNIFF_AND_EAT, + SCHED_PDRONE_COVER_AND_RELOAD +}; + +//========================================================= +// monster-specific tasks +//========================================================= +enum +{ + TASK_PDRONE_HOPTURN = LAST_COMMON_TASK + 1 +}; + +//========================================================= +// Monster's Anim Events Go Here +//========================================================= +#define PIT_DRONE_AE_SPIT ( 1 ) +// not sure what it is. It happens twice when pitdrone uses two claws at the same time +// once before 'throw' event and once after +#define PIT_DRONE_AE_ATTACK ( 2 ) +#define PIT_DRONE_AE_SLASH ( 4 ) +#define PIT_DRONE_AE_HOP ( 5 ) +#define PIT_DRONE_AE_THROW ( 6 ) +#define PIT_DRONE_AE_RELOAD ( 7 ) + +void CMPitdrone::KeyValue(KeyValueData *pkvd) +{ + if (FStrEq(pkvd->szKeyName, "initammo")) + { + m_iInitialAmmo = atoi(pkvd->szValue); + pkvd->fHandled = TRUE; + } + else + CMBaseMonster::KeyValue(pkvd); +} + +//========================================================= +// IgnoreConditions +//========================================================= +int CMPitdrone::IgnoreConditions(void) +{ + int iIgnore = CMBaseMonster::IgnoreConditions(); + + if ((m_Activity == ACT_MELEE_ATTACK1) || (m_Activity == ACT_MELEE_ATTACK2)) + { + if (m_flNextFlinch >= gpGlobals->time) + iIgnore |= (bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE); + } + + if ((m_Activity == ACT_SMALL_FLINCH) || (m_Activity == ACT_BIG_FLINCH)) + { + if (m_flNextFlinch < gpGlobals->time) + m_flNextFlinch = gpGlobals->time + PITDRONE_FLINCH_DELAY; + } + + return iIgnore; + +} + +const char *CMPitdrone::pAttackMissSounds[] = +{ + "zombie/claw_miss1.wav", + "zombie/claw_miss2.wav", +}; + +const char *CMPitdrone::pIdleSounds[] = +{ + "pitdrone/pit_drone_idle1.wav", + "pitdrone/pit_drone_idle2.wav", + "pitdrone/pit_drone_idle3.wav", + +}; + +const char *CMPitdrone::pAlertSounds[] = +{ + "pitdrone/pit_drone_alert1.wav", + "pitdrone/pit_drone_alert2.wav", + "pitdrone/pit_drone_alert3.wav", +}; + +const char *CMPitdrone::pPainSounds[] = +{ + "pitdrone/pit_drone_pain1.wav", + "pitdrone/pit_drone_pain2.wav", + "pitdrone/pit_drone_pain3.wav", + "pitdrone/pit_drone_pain4.wav", +}; + +const char *CMPitdrone::pDieSounds[] = +{ + "pitdrone/pit_drone_die1.wav", + "pitdrone/pit_drone_die2.wav", + "pitdrone/pit_drone_die3.wav", +}; + +//========================================================= +// TakeDamage - overridden for pitdrone so we can keep track +// of how much time has passed since it was last injured +//========================================================= +int CMPitdrone::TakeDamage(entvars_t *pevInflictor, entvars_t *pevAttacker, float flDamage, int bitsDamageType) +{ + float flDist; + Vector vecApex; + + // if the pitdrone is running, has an enemy, was hurt by the enemy, and isn't too close to the enemy, + // it will swerve. (whew). + if (m_hEnemy != 0 && IsMoving() && pevAttacker == VARS(m_hEnemy)) + { + flDist = (pev->origin - m_hEnemy->v.origin).Length2D(); + + if (flDist > PITDRONE_SPRINT_DIST) + { + flDist = (pev->origin - m_Route[m_iRouteIndex].vecLocation).Length2D();// reusing flDist. + + if (FTriangulate(pev->origin, m_Route[m_iRouteIndex].vecLocation, flDist * 0.5, m_hEnemy, &vecApex)) + { + InsertWaypoint(vecApex, bits_MF_TO_DETOUR | bits_MF_DONT_SIMPLIFY); + } + } + } + + m_flLastHurtTime = gpGlobals->time; + + return CMBaseMonster::TakeDamage(pevInflictor, pevAttacker, flDamage, bitsDamageType); +} + +//========================================================= +// CheckMeleeAttack1 - attack with both claws at the same time +//========================================================= +BOOL CMPitdrone::CheckMeleeAttack1(float flDot, float flDist) +{ + // Give a better chance for MeleeAttack2 + if (RANDOM_LONG(0,2) == 0) { + return CMBaseMonster::CheckMeleeAttack1(flDot, flDist); + } + return FALSE; +} + +//========================================================= +// CheckRangeAttack1 - spike attack +//========================================================= +BOOL CMPitdrone::CheckRangeAttack1(float flDot, float flDist) +{ + if (m_cAmmoLoaded <= 0) + { + return FALSE; + } + if (IsMoving() && flDist >= 512) + { + // pitdone will far too far behind if he stops running to spit at this distance from the enemy. + return FALSE; + } + + if (flDist > 64 && flDist <= 784 && flDot >= 0.5 && gpGlobals->time >= m_flNextSpitTime) + { + + if (IsMoving()) + { + // don't spit again for a long time, resume chasing enemy. + m_flNextSpitTime = gpGlobals->time + 5; + } + else + { + // not moving, so spit again pretty soon. + m_flNextSpitTime = gpGlobals->time + 1; + } + + return TRUE; + } + + return FALSE; + +} + +//========================================================= +// SetYawSpeed - allows each sequence to have a different +// turn rate associated with it. +//========================================================= +void CMPitdrone::SetYawSpeed(void) +{ + int ys; + + ys = 0; + + switch (m_Activity) + { + case ACT_WALK: ys = 120; break; + case ACT_RUN: ys = 120; break; + case ACT_IDLE: ys = 120; break; + case ACT_RANGE_ATTACK1: ys = 120; break; + default: + ys = 120; + break; + } + + pev->yaw_speed = ys; +} + +//========================================================= +// ISoundMask - 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 CMPitdrone::ISoundMask( void ) +{ + return 0; +} + +void CMPitdrone::HandleAnimEvent(MonsterEvent_t *pEvent) +{ + switch (pEvent->event) + { + case PIT_DRONE_AE_ATTACK: + break; + case PIT_DRONE_AE_THROW: + { + // SOUND HERE (in the pitdrone model) + edict_t *pHurt = CheckTraceHullAttack( 70, gSkillData.pitdroneDmgWhip, DMG_SLASH ); + + if( pHurt ) + { + // croonchy bite sound + const int iPitch = RANDOM_FLOAT( 110, 120 ); + switch( RANDOM_LONG( 0, 1 ) ) + { + case 0: + EMIT_SOUND_DYN( ENT( pev ), CHAN_WEAPON, "bullchicken/bc_bite2.wav", 0.7, ATTN_NORM, 0, iPitch ); + break; + case 1: + EMIT_SOUND_DYN( ENT( pev ), CHAN_WEAPON, "bullchicken/bc_bite3.wav", 0.7, ATTN_NORM, 0, iPitch ); + break; + } + + // screeshake transforms the viewmodel as well as the viewangle. No problems with seeing the ends of the viewmodels. + UTIL_ScreenShake( pHurt->v.origin, 15.0, 1.5, 0.7, 2 ); + + pHurt->v.velocity = pHurt->v.velocity + gpGlobals->v_forward * 100; + pHurt->v.velocity = pHurt->v.velocity + gpGlobals->v_up * 200; + } + else + EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, pAttackMissSounds[RANDOM_LONG(0, ARRAYSIZE(pAttackMissSounds) - 1)], 1.0, ATTN_NORM, 0, 100 + RANDOM_LONG(-5, 5)); + } + break; + + case PIT_DRONE_AE_SLASH: + { + /* The same event is reused for both right and left claw attacks. + * Pitdrone always starts the attack with the right claw so we use shouldAttackWithLeftClaw to check which claw is used now. + */ + // SOUND HERE (in the pitdrone model) + edict_t *pHurt = CheckTraceHullAttack(70, gSkillData.pitdroneDmgBite, DMG_SLASH); + if (pHurt) + { + if (pHurt->v.flags & (FL_MONSTER | FL_CLIENT)) + { + pHurt->v.punchangle.z = shouldAttackWithLeftClaw ? 18 : -18; + pHurt->v.punchangle.x = 5; + pHurt->v.velocity = pHurt->v.velocity + gpGlobals->v_right * ( shouldAttackWithLeftClaw ? 100 : -100 ); + } + } + else // Play a random attack miss sound + EMIT_SOUND_DYN(ENT(pev), CHAN_WEAPON, pAttackMissSounds[RANDOM_LONG(0, ARRAYSIZE(pAttackMissSounds) - 1)], 1.0, ATTN_NORM, 0, 100 + RANDOM_LONG(-5, 5)); + shouldAttackWithLeftClaw = !shouldAttackWithLeftClaw; + } + break; + + case PIT_DRONE_AE_RELOAD: + { + if (m_iInitialAmmo >= 0) + m_cAmmoLoaded = PITDRONE_MAX_HORNS; + else + m_cAmmoLoaded = 0; + BodyChange(m_cAmmoLoaded); + ClearConditions(bits_COND_NO_AMMO_LOADED); + } + break; + + case PIT_DRONE_AE_HOP: + { + float flGravity = g_psv_gravity->value; + + // throw the squid up into the air on this frame. + if( FBitSet( pev->flags, FL_ONGROUND ) ) + { + pev->flags -= FL_ONGROUND; + } + + // jump into air for 0.8 (24/30) seconds + pev->velocity.z += ( 0.625 * flGravity ) * 0.5; + } + break; + + case PIT_DRONE_AE_SPIT: + { + m_cAmmoLoaded--; + BodyChange(m_cAmmoLoaded); + + Vector vecSpitOffset; + Vector vecSpitDir; + + UTIL_MakeAimVectors(pev->angles); + + // !!!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 = (gpGlobals->v_forward * 15 + gpGlobals->v_up * 36); + vecSpitOffset = (pev->origin + vecSpitOffset); + //vecSpitDir = ((m_hEnemy->pev->origin + m_hEnemy->pev->view_ofs) - vecSpitOffset).Normalize(); + Vector vecEnemyPosition; + if (m_hEnemy != 0) + vecEnemyPosition = UTIL_BodyTarget(m_hEnemy, pev->origin); + else + vecEnemyPosition = m_vecEnemyLKP; + vecSpitDir = (vecEnemyPosition - vecSpitOffset).Normalize(); + + vecSpitDir.x += RANDOM_FLOAT(-0.01, 0.01); + vecSpitDir.y += RANDOM_FLOAT(-0.01, 0.01); + vecSpitDir.z += RANDOM_FLOAT(-0.01, 0); + + // SOUND HERE! (in the pitdrone model) + + CPitdroneSpike::Shoot(pev, vecSpitOffset, vecSpitDir * 900, UTIL_VecToAngles(vecSpitDir)); + + // spew the spittle temporary ents. + MESSAGE_BEGIN( MSG_PVS, SVC_TEMPENTITY, vecSpitOffset ); + WRITE_BYTE( TE_SPRITE_SPRAY ); + WRITE_COORD( vecSpitOffset.x ); // pos + WRITE_COORD( vecSpitOffset.y ); + WRITE_COORD( vecSpitOffset.z ); + WRITE_COORD( vecSpitDir.x ); // dir + WRITE_COORD( vecSpitDir.y ); + WRITE_COORD( vecSpitDir.z ); + WRITE_SHORT( iPitdroneSpitSprite ); // model + WRITE_BYTE( 15 ); // count + WRITE_BYTE( 210 ); // speed + WRITE_BYTE( 25 ); // noise ( client will divide by 100 ) + MESSAGE_END(); + } + break; + + + default: + CMBaseMonster::HandleAnimEvent(pEvent); + } +} + +int CMPitdrone::Classify(void) +{ + if ( m_iClassifyOverride == -1 ) // helper + return CLASS_NONE; + else if ( m_iClassifyOverride > 0 ) + return m_iClassifyOverride; // override + + return CLASS_RACEX_PITDRONE; +} + +void CMPitdrone::BodyChange(float horns) +{ + if (horns <= 0) + SetBodygroup(HORNGROUP, PITDRONE_HORNS0); + // pev->body = PITDRONE_HORNS0; + + if (horns == 1) + SetBodygroup(HORNGROUP, PITDRONE_HORNS6); + // pev->body = PITDRONE_HORNS6; + + if (horns == 2) + SetBodygroup(HORNGROUP, PITDRONE_HORNS5); + // pev->body = PITDRONE_HORNS5; + + if (horns == 3) + SetBodygroup(HORNGROUP, PITDRONE_HORNS4); + // pev->body = PITDRONE_HORNS4; + + if (horns == 4) + SetBodygroup(HORNGROUP, PITDRONE_HORNS3); + // pev->body = PITDRONE_HORNS3; + + if (horns == 5) + SetBodygroup(HORNGROUP, PITDRONE_HORNS2); + // pev->body = PITDRONE_HORNS2; + + if (horns >= 6) + SetBodygroup(HORNGROUP, PITDRONE_HORNS1); + // pev->body = PITDRONE_HORNS1; + + return; +} +//========================================================= +// Spawn +//========================================================= +void CMPitdrone::Spawn() +{ + Precache(); + + SET_MODEL( ENT(pev), "models/pit_drone.mdl" ); + UTIL_SetSize(pev, Vector(-16, -16, 0), Vector(16, 16, 48)); + + pev->solid = SOLID_SLIDEBOX; + pev->movetype = MOVETYPE_STEP; + m_bloodColor = BLOOD_COLOR_GREEN; + pev->effects = 0; + pev->health = gSkillData.pitdroneHealth; + m_flFieldOfView = 0.2;// indicates the width of this monster's forward view cone ( as a dotproduct result ) + m_MonsterState = MONSTERSTATE_NONE; + + m_flNextSpitTime = gpGlobals->time; + + if (m_iInitialAmmo >= 0) + { + m_cAmmoLoaded = min(m_iInitialAmmo, PITDRONE_MAX_HORNS); +#if FEATURE_PITDRONE_SPAWN_WITH_SPIKES + if (!m_cAmmoLoaded) { + m_cAmmoLoaded = PITDRONE_MAX_HORNS; + } +#endif + } + BodyChange(m_cAmmoLoaded); + MonsterInit(); + + pev->classname = MAKE_STRING( "monster_pitdrone" ); + if ( strlen( STRING( m_szMonsterName ) ) == 0 ) + { + // default name + m_szMonsterName = MAKE_STRING( "Pit Drone" ); + } +} + +//========================================================= +// Precache - precaches all resources this monster needs +//========================================================= +void CMPitdrone::Precache() +{ + PRECACHE_MODEL("models/pit_drone.mdl"); + PRECACHE_MODEL("models/pit_drone_gibs.mdl"); + iPitdroneSpitSprite = PRECACHE_MODEL("sprites/tinyspit.spr");// client side spittle. + + PRECACHE_SOUND_ARRAY(pAttackMissSounds); + PRECACHE_SOUND_ARRAY(pIdleSounds); + PRECACHE_SOUND_ARRAY(pDieSounds); + PRECACHE_SOUND_ARRAY(pPainSounds); + PRECACHE_SOUND_ARRAY(pAlertSounds); + + PRECACHE_SOUND("bullchicken/bc_bite2.wav"); + PRECACHE_SOUND("bullchicken/bc_bite3.wav"); + + PRECACHE_SOUND("pitdrone/pit_drone_melee_attack1.wav"); + PRECACHE_SOUND("pitdrone/pit_drone_melee_attack2.wav"); + + PRECACHE_SOUND("pitdrone/pit_drone_attack_spike1.wav"); + PRECACHE_SOUND("pitdrone/pit_drone_attack_spike2.wav"); + + PRECACHE_SOUND("pitdrone/pit_drone_communicate1.wav"); + PRECACHE_SOUND("pitdrone/pit_drone_communicate2.wav"); + PRECACHE_SOUND("pitdrone/pit_drone_communicate3.wav"); + PRECACHE_SOUND("pitdrone/pit_drone_communicate4.wav"); + + PRECACHE_SOUND("pitdrone/pit_drone_eat.wav"); + PRECACHE_SOUND("pitdrone/pit_drone_hunt1.wav"); + PRECACHE_SOUND("pitdrone/pit_drone_hunt2.wav"); + PRECACHE_SOUND("pitdrone/pit_drone_hunt3.wav"); + + PRECACHE_SOUND("weapons/xbow_hitbod1.wav"); + PRECACHE_SOUND("weapons/xbow_hitbod2.wav"); + PRECACHE_SOUND("weapons/xbow_hit1.wav"); + + //UTIL_PrecacheOther("pitdronespike"); + PRECACHE_MODEL("models/pit_drone_spike.mdl");// spit projectile + PRECACHE_SOUND("weapons/xbow_hitbod1.wav"); + PRECACHE_SOUND("weapons/xbow_hitbod2.wav"); + PRECACHE_SOUND("weapons/xbow_hit1.wav"); +#if FEATURE_PITDRONE_SPIKE_TRAIL + iSpikeTrail = PRECACHE_MODEL("sprites/spike_trail.spr"); +#endif +} + +//========================================================= +// IdleSound +//========================================================= +#define PITDRONE_ATTN_IDLE (float)1.5 +void CMPitdrone::IdleSound(void) +{ + EMIT_SOUND(ENT(pev), CHAN_VOICE, pIdleSounds[RANDOM_LONG(0, ARRAYSIZE(pIdleSounds)-1)], 1, PITDRONE_ATTN_IDLE); +} + +//========================================================= +// PainSound +//========================================================= +void CMPitdrone::PainSound(void) +{ + int iPitch = RANDOM_LONG(85, 120); + EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, pPainSounds[RANDOM_LONG(0, ARRAYSIZE(pPainSounds)-1)], 1, ATTN_NORM, 0, iPitch); +} + +//========================================================= +// AlertSound +//========================================================= +void CMPitdrone::AlertSound(void) +{ + int iPitch = RANDOM_LONG(140, 160); + EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, pAlertSounds[RANDOM_LONG(0, ARRAYSIZE(pAlertSounds)-1)], 1, ATTN_NORM, 0, iPitch); +} +//========================================================= +// DeathSound +//========================================================= +void CMPitdrone::DeathSound(void) +{ + EMIT_SOUND(ENT(pev), CHAN_VOICE, pDieSounds[RANDOM_LONG(0, ARRAYSIZE(pDieSounds)-1)], 1, ATTN_NORM); +} + +void CMPitdrone::RunAI(void) +{ + // first, do base class stuff + CMBaseMonster::RunAI(); + + if (m_hEnemy != 0 && m_Activity == ACT_RUN) + { + // chasing enemy. Sprint for last bit + if ((pev->origin - m_hEnemy->v.origin).Length2D() < PITDRONE_SPRINT_DIST) + { + pev->framerate = 1.25; + } + } +} + +void CMPitdrone::CheckAmmo( void ) +{ + if( m_cAmmoLoaded <= 0 && m_iInitialAmmo >= 0 ) + { + SetConditions( bits_COND_NO_AMMO_LOADED ); + } +} + +void CMPitdrone::GibMonster() +{ + EMIT_SOUND( ENT( pev ), CHAN_WEAPON, "common/bodysplat.wav", 1, ATTN_NORM ); + + if( CVAR_GET_FLOAT( "violence_agibs" ) != 0 ) // Should never get here, but someone might call it directly + { + CMGib::SpawnRandomGibs( pev, PITDRONE_GIB_COUNT, "models/pit_drone_gibs.mdl", 0 ); // Throw alien gibs + } + SetThink( &CMBaseEntity::SUB_Remove ); + pev->nextthink = gpGlobals->time; +} + +//======================================================== +// AI Schedules Specific to this monster +//========================================================= + +// primary range attack +Task_t tlPDroneRangeAttack1[] = +{ + { TASK_STOP_MOVING, 0 }, + { TASK_FACE_IDEAL, (float)0 }, + { TASK_RANGE_ATTACK1, (float)0 }, + { TASK_SET_ACTIVITY, (float)ACT_IDLE }, +}; + +Schedule_t slPDroneRangeAttack1[] = +{ + { + tlPDroneRangeAttack1, + ARRAYSIZE(tlPDroneRangeAttack1), + bits_COND_NEW_ENEMY | + bits_COND_ENEMY_DEAD | + bits_COND_HEAVY_DAMAGE | + bits_COND_ENEMY_OCCLUDED | + bits_COND_NO_AMMO_LOADED, + 0, + "PDrone Range Attack1" + }, +}; + +// Chase enemy schedule +Task_t tlPDroneChaseEnemy1[] = +{ + { TASK_SET_FAIL_SCHEDULE, (float)SCHED_RANGE_ATTACK1 },// !!!OEM - this will stop nasty PitDrone oscillation. + { TASK_GET_PATH_TO_ENEMY, (float)0 }, + { TASK_RUN_PATH, (float)0 }, + { TASK_WAIT_FOR_MOVEMENT, (float)0 }, +}; + +Schedule_t slPDroneChaseEnemy[] = +{ + { + tlPDroneChaseEnemy1, + ARRAYSIZE(tlPDroneChaseEnemy1), + bits_COND_NEW_ENEMY | + bits_COND_ENEMY_DEAD | + bits_COND_SMELL_FOOD | + bits_COND_CAN_RANGE_ATTACK1 | + bits_COND_CAN_MELEE_ATTACK1 | + bits_COND_CAN_MELEE_ATTACK2 | + bits_COND_TASK_FAILED | + bits_COND_HEAR_SOUND, + + 0, + "PDrone Chase Enemy" + }, +}; + +Task_t tlPDroneHurtHop[] = +{ + { TASK_STOP_MOVING, (float)0 }, + { TASK_SOUND_WAKE, (float)0 }, + { TASK_PDRONE_HOPTURN, (float)0 }, + { TASK_FACE_ENEMY, (float)0 },// in case squid didn't turn all the way in the air. +}; + +Schedule_t slPDroneHurtHop[] = +{ + { + tlPDroneHurtHop, + ARRAYSIZE( tlPDroneHurtHop ), + 0, + 0, + "PDroneHurtHop" + } +}; + + +// PitDrone walks to something tasty and eats it. +Task_t tlPDroneEat[] = +{ + { TASK_STOP_MOVING, (float)0 }, + { TASK_EAT, (float)10 },// this is in case the PitDrone can't get to the food + { TASK_STORE_LASTPOSITION, (float)0 }, + { TASK_GET_PATH_TO_BESTSCENT, (float)0 }, + { TASK_WALK_PATH, (float)0 }, + { TASK_WAIT_FOR_MOVEMENT, (float)0 }, + { TASK_PLAY_SEQUENCE, (float)ACT_EAT }, + { TASK_PLAY_SEQUENCE, (float)ACT_EAT }, + { TASK_PLAY_SEQUENCE, (float)ACT_EAT }, + { TASK_EAT, (float)50 }, + { TASK_GET_PATH_TO_LASTPOSITION, (float)0 }, + { TASK_WALK_PATH, (float)0 }, + { TASK_WAIT_FOR_MOVEMENT, (float)0 }, + { TASK_CLEAR_LASTPOSITION, (float)0 }, +}; + +Schedule_t slPDroneEat[] = +{ + { + tlPDroneEat, + ARRAYSIZE(tlPDroneEat), + bits_COND_LIGHT_DAMAGE | + bits_COND_HEAVY_DAMAGE | + bits_COND_NEW_ENEMY, + + // even though HEAR_SOUND/SMELL FOOD doesn't break this schedule, we need this mask + // here or the monster won't detect these sounds at ALL while running this schedule. + 0, + "PDroneEat" + } +}; + +// this is a bit different than just Eat. We use this schedule when the food is far away, occluded, or behind +// the PitDrone. This schedule plays a sniff animation before going to the source of food. +Task_t tlPDroneSniffAndEat[] = +{ + { TASK_STOP_MOVING, (float)0 }, + { TASK_EAT, (float)10 },// this is in case the PitDrone can't get to the food + { TASK_STORE_LASTPOSITION, (float)0 }, + { TASK_GET_PATH_TO_BESTSCENT, (float)0 }, + { TASK_RUN_PATH, (float)0 }, + { TASK_WAIT_FOR_MOVEMENT, (float)0 }, + { TASK_PLAY_SEQUENCE, (float)ACT_EAT }, + { TASK_PLAY_SEQUENCE, (float)ACT_EAT }, + { TASK_PLAY_SEQUENCE, (float)ACT_EAT }, + { TASK_EAT, (float)50 }, + { TASK_GET_PATH_TO_LASTPOSITION, (float)0 }, + { TASK_WALK_PATH, (float)0 }, + { TASK_WAIT_FOR_MOVEMENT, (float)0 }, + { TASK_CLEAR_LASTPOSITION, (float)0 }, +}; + +Schedule_t slPDroneSniffAndEat[] = +{ + { + tlPDroneSniffAndEat, + ARRAYSIZE(tlPDroneSniffAndEat), + bits_COND_LIGHT_DAMAGE | + bits_COND_HEAVY_DAMAGE | + bits_COND_NEW_ENEMY, + + // even though HEAR_SOUND/SMELL FOOD doesn't break this schedule, we need this mask + // here or the monster won't detect these sounds at ALL while running this schedule. + 0, + "PDroneSniffAndEat" + } +}; + +Task_t tlPDroneHideReload[] = +{ + { TASK_STOP_MOVING, (float)0 }, + { TASK_SET_FAIL_SCHEDULE, (float)SCHED_RELOAD }, + { TASK_FIND_COVER_FROM_ENEMY, (float)0 }, + { TASK_RUN_PATH, (float)0 }, + { TASK_WAIT_FOR_MOVEMENT, (float)0 }, + { TASK_REMEMBER, (float)bits_MEMORY_INCOVER }, + { TASK_FACE_ENEMY, (float)0 }, + { TASK_PLAY_SEQUENCE, (float)ACT_RELOAD }, +}; + +Schedule_t slPDroneHideReload[] = +{ + { + tlPDroneHideReload, + ARRAYSIZE( tlPDroneHideReload ), + bits_COND_HEAVY_DAMAGE | + bits_COND_HEAR_SOUND, + 0, + "PDroneHideReload" + } +}; + +DEFINE_CUSTOM_SCHEDULES(CMPitdrone) +{ + slPDroneRangeAttack1, + slPDroneChaseEnemy, + slPDroneHurtHop, + slPDroneEat, + slPDroneSniffAndEat, + slPDroneHideReload +}; + +IMPLEMENT_CUSTOM_SCHEDULES(CMPitdrone, CMBaseMonster) + +//========================================================= +// GetSchedule +//========================================================= +Schedule_t *CMPitdrone::GetSchedule(void) +{ + switch (m_MonsterState) + { + case MONSTERSTATE_ALERT: + { + if( HasConditions( bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE ) ) + { + return GetScheduleOfType( SCHED_PDRONE_HURTHOP ); + } + + if (HasConditions(bits_COND_SMELL_FOOD)) + { + // Just go (try) get it. + return GetScheduleOfType(SCHED_PDRONE_EAT); + } + + break; + } + case MONSTERSTATE_COMBAT: + { + // dead enemy + if (HasConditions(bits_COND_ENEMY_DEAD)) + { + // call base class, all code to handle dead enemies is centralized there. + return CMBaseMonster::GetSchedule(); + } + + if (HasConditions(bits_COND_NEW_ENEMY)) + { + return GetScheduleOfType(SCHED_WAKE_ANGRY); + } + + if (HasConditions(bits_COND_SMELL_FOOD)) + { + // Just go (try) get it. + return GetScheduleOfType(SCHED_PDRONE_EAT); + } + + if( HasConditions( bits_COND_NO_AMMO_LOADED ) && (m_iInitialAmmo >= 0) ) + { + return GetScheduleOfType( SCHED_PDRONE_COVER_AND_RELOAD ); + } + + if (HasConditions(bits_COND_CAN_RANGE_ATTACK1)) + { + return GetScheduleOfType(SCHED_RANGE_ATTACK1); + } + + if (HasConditions(bits_COND_CAN_MELEE_ATTACK1)) + { + return GetScheduleOfType(SCHED_MELEE_ATTACK1); + } + + if (HasConditions(bits_COND_CAN_MELEE_ATTACK2)) + { + return GetScheduleOfType(SCHED_MELEE_ATTACK2); + } + + return GetScheduleOfType(SCHED_CHASE_ENEMY); + + break; + } + } + + return CMBaseMonster::GetSchedule(); +} + +//========================================================= +// GetScheduleOfType +//========================================================= +Schedule_t* CMPitdrone::GetScheduleOfType(int Type) +{ + switch (Type) + { + case SCHED_RANGE_ATTACK1: + return &slPDroneRangeAttack1[0]; + break; + case SCHED_PDRONE_HURTHOP: + return &slPDroneHurtHop[0]; + break; + case SCHED_PDRONE_EAT: + return &slPDroneEat[0]; + break; + case SCHED_PDRONE_SNIFF_AND_EAT: + return &slPDroneSniffAndEat[0]; + break; + case SCHED_CHASE_ENEMY: + return &slPDroneChaseEnemy[0]; + break; + case SCHED_PDRONE_COVER_AND_RELOAD: + return &slPDroneHideReload[0]; + break; + } + + return CMBaseMonster::GetScheduleOfType(Type); +} + +//========================================================= +// Start task - selects the correct activity and performs +// any necessary calculations to start the next task on the +// schedule. OVERRIDDEN for PitDrone because it needs to +// know explicitly when the last attempt to chase the enemy +// failed, since that impacts its attack choices. +//========================================================= +void CMPitdrone::StartTask(Task_t *pTask) +{ + m_iTaskStatus = TASKSTATUS_RUNNING; + + switch (pTask->iTask) + { + case TASK_PDRONE_HOPTURN: + { + SetActivity( ACT_HOP ); + MakeIdealYaw( m_vecEnemyLKP ); + break; + } + case TASK_GET_PATH_TO_ENEMY: + { + if (BuildRoute(m_hEnemy->v.origin, bits_MF_TO_ENEMY, m_hEnemy)) + { + m_iTaskStatus = TASKSTATUS_COMPLETE; + } + else + { + ALERT(at_aiconsole, "GetPathToEnemy failed!!\n"); + TaskFail(); + } + break; + } + default: + { + CMBaseMonster::StartTask(pTask); + break; + } + } +} + +//========================================================= +// RunTask +//========================================================= +void CMPitdrone::RunTask(Task_t *pTask) +{ + switch( pTask->iTask ) + { + case TASK_PDRONE_HOPTURN: + { + MakeIdealYaw( m_vecEnemyLKP ); + ChangeYaw( pev->yaw_speed ); + + if( m_fSequenceFinished ) + { + m_iTaskStatus = TASKSTATUS_COMPLETE; + } + break; + } + default: + { + CMBaseMonster::RunTask( pTask ); + break; + } + } +} diff --git a/src/dlls/skill.cpp b/src/dlls/skill.cpp index d9a5308..502bcd1 100644 --- a/src/dlls/skill.cpp +++ b/src/dlls/skill.cpp @@ -79,6 +79,10 @@ skill_cfg_t skill_cfg[] = { {"sk_massassin_health", &gSkillData.massnHealth}, {"sk_massassin_kick", &gSkillData.massnDmgKick}, {"sk_otis_health", &gSkillData.otisHealth}, + {"sk_pitdrone_health", &gSkillData.pitdroneHealth}, + {"sk_pitdrone_dmg_spit", &gSkillData.pitdroneDmgSpit}, + {"sk_pitdrone_dmg_whip", &gSkillData.pitdroneDmgWhip}, + {"sk_pitdrone_dmg_bite", &gSkillData.pitdroneDmgBite}, {"sk_12mm_bullet", &gSkillData.monDmg9MM}, {"sk_9mmAR_bullet", &gSkillData.monDmgMP5}, {"sk_9mm_bullet", &gSkillData.monDmg12MM}, @@ -234,6 +238,12 @@ void monster_skill_init(void) // Otis gSkillData.otisHealth = 35.0f; + // Pit Drone + gSkillData.pitdroneHealth = 40.0f; + gSkillData.pitdroneDmgSpit = 10.0f; + gSkillData.pitdroneDmgWhip = 35.0f; + gSkillData.pitdroneDmgBite = 25.0f; + // MONSTER WEAPONS gSkillData.monDmg9MM = 5.0f; gSkillData.monDmgMP5 = 4.0f; diff --git a/src/dlls/skill.h b/src/dlls/skill.h index b08b7bc..5747996 100644 --- a/src/dlls/skill.h +++ b/src/dlls/skill.h @@ -97,6 +97,11 @@ struct skilldata_t float otisHealth; + float pitdroneHealth; + float pitdroneDmgSpit; + float pitdroneDmgWhip; + float pitdroneDmgBite; + // weapons shared by monsters float monDmg9MM; float monDmgMP5;