/* =========================================================================== Copyright (C) 2000 - 2013, Raven Software, Inc. Copyright (C) 2001 - 2013, Activision, Inc. Copyright (C) 2013 - 2015, OpenJK contributors This file is part of the OpenJK source code. OpenJK is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . =========================================================================== */ #include "b_local.h" #include "g_nav.h" #include "../cgame/cg_local.h" #include "g_functions.h" gentity_t *CreateMissile( vec3_t org, vec3_t dir, float vel, int life, gentity_t *owner, qboolean altFire = qfalse ); extern gitem_t *FindItemForAmmo( ammo_t ammo ); #define MIN_DISTANCE 256 #define MIN_DISTANCE_SQR ( MIN_DISTANCE * MIN_DISTANCE ) #define SENTRY_FORWARD_BASE_SPEED 10 #define SENTRY_FORWARD_MULTIPLIER 5 #define SENTRY_VELOCITY_DECAY 0.85f #define SENTRY_STRAFE_VEL 256 #define SENTRY_STRAFE_DIS 200 #define SENTRY_UPWARD_PUSH 32 #define SENTRY_HOVER_HEIGHT 24 //Local state enums enum { LSTATE_NONE = 0, LSTATE_ASLEEP, LSTATE_WAKEUP, LSTATE_ACTIVE, LSTATE_POWERING_UP, LSTATE_ATTACKING, }; /* ------------------------- NPC_Sentry_Precache ------------------------- */ void NPC_Sentry_Precache(void) { G_SoundIndex( "sound/chars/sentry/misc/sentry_explo" ); G_SoundIndex( "sound/chars/sentry/misc/sentry_pain" ); G_SoundIndex( "sound/chars/sentry/misc/sentry_shield_open" ); G_SoundIndex( "sound/chars/sentry/misc/sentry_shield_close" ); G_SoundIndex( "sound/chars/sentry/misc/sentry_hover_1_lp" ); G_SoundIndex( "sound/chars/sentry/misc/sentry_hover_2_lp" ); for ( int i = 1; i < 4; i++) { G_SoundIndex( va( "sound/chars/sentry/misc/talk%d", i ) ); } G_EffectIndex( "bryar/muzzle_flash"); G_EffectIndex( "env/med_explode"); RegisterItem( FindItemForAmmo( AMMO_BLASTER )); } /* ================ sentry_use ================ */ void sentry_use( gentity_t *self, gentity_t *other, gentity_t *activator) { G_ActivateBehavior(self,BSET_USE); self->flags &= ~FL_SHIELDED; NPC_SetAnim( self, SETANIM_BOTH, BOTH_POWERUP1, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD ); // self->NPC->localState = LSTATE_WAKEUP; self->NPC->localState = LSTATE_ACTIVE; } /* ------------------------- NPC_Sentry_Pain ------------------------- */ void NPC_Sentry_Pain( gentity_t *self, gentity_t *inflictor, gentity_t *other, const vec3_t point, int damage, int mod,int hitLoc ) { NPC_Pain( self, inflictor, other, point, damage, mod ); if ( mod == MOD_DEMP2 || mod == MOD_DEMP2_ALT ) { self->NPC->burstCount = 0; TIMER_Set( self, "attackDelay", Q_irand( 9000, 12000) ); self->flags |= FL_SHIELDED; NPC_SetAnim( self, SETANIM_BOTH, BOTH_FLY_SHIELDED, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD ); G_SoundOnEnt( self, CHAN_AUTO, "sound/chars/sentry/misc/sentry_pain" ); self->NPC->localState = LSTATE_ACTIVE; } // You got hit, go after the enemy // if (self->NPC->localState == LSTATE_ASLEEP) // { // G_Sound( self, G_SoundIndex("sound/chars/sentry/misc/shieldsopen.wav")); // // self->flags &= ~FL_SHIELDED; // NPC_SetAnim( self, SETANIM_BOTH, BOTH_POWERUP1, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD ); // self->NPC->localState = LSTATE_WAKEUP; // } } /* ------------------------- Sentry_Fire ------------------------- */ void Sentry_Fire (void) { vec3_t muzzle; static vec3_t forward, vright, up; gentity_t *missile; mdxaBone_t boltMatrix; int bolt; NPC->flags &= ~FL_SHIELDED; if ( NPCInfo->localState == LSTATE_POWERING_UP ) { if ( TIMER_Done( NPC, "powerup" )) { NPCInfo->localState = LSTATE_ATTACKING; NPC_SetAnim( NPC, SETANIM_BOTH, BOTH_ATTACK1, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD ); } else { // can't do anything right now return; } } else if ( NPCInfo->localState == LSTATE_ACTIVE ) { NPCInfo->localState = LSTATE_POWERING_UP; G_SoundOnEnt( NPC, CHAN_AUTO, "sound/chars/sentry/misc/sentry_shield_open" ); NPC_SetAnim( NPC, SETANIM_BOTH, BOTH_POWERUP1, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD ); TIMER_Set( NPC, "powerup", 250 ); return; } else if ( NPCInfo->localState != LSTATE_ATTACKING ) { // bad because we are uninitialized NPCInfo->localState = LSTATE_ACTIVE; return; } // Which muzzle to fire from? int which = NPCInfo->burstCount % 3; switch( which ) { case 0: bolt = NPC->genericBolt1; break; case 1: bolt = NPC->genericBolt2; break; case 2: default: bolt = NPC->genericBolt3; } gi.G2API_GetBoltMatrix( NPC->ghoul2, NPC->playerModel, bolt, &boltMatrix, NPC->currentAngles, NPC->currentOrigin, (cg.time?cg.time:level.time), NULL, NPC->s.modelScale ); gi.G2API_GiveMeVectorFromMatrix( boltMatrix, ORIGIN, muzzle ); AngleVectors( NPC->currentAngles, forward, vright, up ); // G_Sound( NPC, G_SoundIndex("sound/chars/sentry/misc/shoot.wav")); G_PlayEffect( "bryar/muzzle_flash", muzzle, forward ); missile = CreateMissile( muzzle, forward, 1600, 10000, NPC ); missile->classname = "bryar_proj"; missile->s.weapon = WP_BRYAR_PISTOL; missile->dflags = DAMAGE_DEATH_KNOCKBACK; missile->methodOfDeath = MOD_ENERGY; missile->clipmask = MASK_SHOT | CONTENTS_LIGHTSABER; NPCInfo->burstCount++; NPC->attackDebounceTime = level.time + 50; missile->damage = 5; // now scale for difficulty if ( g_spskill->integer == 0 ) { NPC->attackDebounceTime += 200; missile->damage = 1; } else if ( g_spskill->integer == 1 ) { NPC->attackDebounceTime += 100; missile->damage = 3; } } /* ------------------------- Sentry_MaintainHeight ------------------------- */ void Sentry_MaintainHeight( void ) { float dif; NPC->s.loopSound = G_SoundIndex( "sound/chars/sentry/misc/sentry_hover_1_lp" ); // Update our angles regardless NPC_UpdateAngles( qtrue, qtrue ); // If we have an enemy, we should try to hover at about enemy eye level if ( NPC->enemy ) { // Find the height difference dif = (NPC->enemy->currentOrigin[2]+NPC->enemy->maxs[2]) - NPC->currentOrigin[2]; // cap to prevent dramatic height shifts if ( fabs( dif ) > 8 ) { if ( fabs( dif ) > SENTRY_HOVER_HEIGHT ) { dif = ( dif < 0 ? -24 : 24 ); } NPC->client->ps.velocity[2] = (NPC->client->ps.velocity[2]+dif)/2; } } else { gentity_t *goal = NULL; if ( NPCInfo->goalEntity ) // Is there a goal? { goal = NPCInfo->goalEntity; } else { goal = NPCInfo->lastGoalEntity; } if (goal) { dif = goal->currentOrigin[2] - NPC->currentOrigin[2]; if ( fabs( dif ) > SENTRY_HOVER_HEIGHT ) { ucmd.upmove = ( ucmd.upmove < 0 ? -4 : 4 ); } else { if ( NPC->client->ps.velocity[2] ) { NPC->client->ps.velocity[2] *= SENTRY_VELOCITY_DECAY; if ( fabs( NPC->client->ps.velocity[2] ) < 2 ) { NPC->client->ps.velocity[2] = 0; } } } } // Apply friction to Z else if ( NPC->client->ps.velocity[2] ) { NPC->client->ps.velocity[2] *= SENTRY_VELOCITY_DECAY; if ( fabs( NPC->client->ps.velocity[2] ) < 1 ) { NPC->client->ps.velocity[2] = 0; } } } // Apply friction if ( NPC->client->ps.velocity[0] ) { NPC->client->ps.velocity[0] *= SENTRY_VELOCITY_DECAY; if ( fabs( NPC->client->ps.velocity[0] ) < 1 ) { NPC->client->ps.velocity[0] = 0; } } if ( NPC->client->ps.velocity[1] ) { NPC->client->ps.velocity[1] *= SENTRY_VELOCITY_DECAY; if ( fabs( NPC->client->ps.velocity[1] ) < 1 ) { NPC->client->ps.velocity[1] = 0; } } NPC_FaceEnemy( qtrue ); } /* ------------------------- Sentry_Idle ------------------------- */ void Sentry_Idle( void ) { Sentry_MaintainHeight(); // Is he waking up? if (NPCInfo->localState == LSTATE_WAKEUP) { if (NPC->client->ps.torsoAnimTimer<=0) { NPCInfo->scriptFlags |= SCF_LOOK_FOR_ENEMIES; NPCInfo->burstCount = 0; } } else { NPC_SetAnim( NPC, SETANIM_BOTH, BOTH_SLEEP1, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD ); NPC->flags |= FL_SHIELDED; NPC_BSIdle(); } } /* ------------------------- Sentry_Strafe ------------------------- */ void Sentry_Strafe( void ) { int dir; vec3_t end, right; trace_t tr; AngleVectors( NPC->client->renderInfo.eyeAngles, NULL, right, NULL ); // Pick a random strafe direction, then check to see if doing a strafe would be // reasonable valid dir = ( rand() & 1 ) ? -1 : 1; VectorMA( NPC->currentOrigin, SENTRY_STRAFE_DIS * dir, right, end ); gi.trace( &tr, NPC->currentOrigin, NULL, NULL, end, NPC->s.number, MASK_SOLID, (EG2_Collision)0, 0 ); // Close enough if ( tr.fraction > 0.9f ) { VectorMA( NPC->client->ps.velocity, SENTRY_STRAFE_VEL * dir, right, NPC->client->ps.velocity ); // Add a slight upward push NPC->client->ps.velocity[2] += SENTRY_UPWARD_PUSH; // Set the strafe start time so we can do a controlled roll NPC->fx_time = level.time; NPCInfo->standTime = level.time + 3000 + Q_flrand(0.0f, 1.0f) * 500; } } /* ------------------------- Sentry_Hunt ------------------------- */ void Sentry_Hunt( qboolean visible, qboolean advance ) { float speed; vec3_t forward; //If we're not supposed to stand still, pursue the player if ( NPCInfo->standTime < level.time ) { // Only strafe when we can see the player if ( visible ) { Sentry_Strafe(); return; } } //If we don't want to advance, stop here if ( !advance && visible ) return; //Only try and navigate if the player is visible if ( visible == qfalse ) { // Move towards our goal NPCInfo->goalEntity = NPC->enemy; NPCInfo->goalRadius = 12; NPC_MoveToGoal(qtrue); return; } else { VectorSubtract( NPC->enemy->currentOrigin, NPC->currentOrigin, forward ); /*distance = */VectorNormalize( forward ); } speed = SENTRY_FORWARD_BASE_SPEED + SENTRY_FORWARD_MULTIPLIER * g_spskill->integer; VectorMA( NPC->client->ps.velocity, speed, forward, NPC->client->ps.velocity ); } /* ------------------------- Sentry_RangedAttack ------------------------- */ void Sentry_RangedAttack( qboolean visible, qboolean advance ) { if ( TIMER_Done( NPC, "attackDelay" ) && NPC->attackDebounceTime < level.time && visible ) // Attack? { if ( NPCInfo->burstCount > 6 ) { if ( !NPC->fly_sound_debounce_time ) {//delay closing down to give the player an opening NPC->fly_sound_debounce_time = level.time + Q_irand( 500, 2000 ); } else if ( NPC->fly_sound_debounce_time < level.time ) { NPCInfo->localState = LSTATE_ACTIVE; NPC->fly_sound_debounce_time = NPCInfo->burstCount = 0; TIMER_Set( NPC, "attackDelay", Q_irand( 2000, 3500) ); NPC->flags |= FL_SHIELDED; NPC_SetAnim( NPC, SETANIM_BOTH, BOTH_FLY_SHIELDED, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD ); G_SoundOnEnt( NPC, CHAN_AUTO, "sound/chars/sentry/misc/sentry_shield_close" ); } } else { Sentry_Fire(); } } if ( NPCInfo->scriptFlags & SCF_CHASE_ENEMIES ) { Sentry_Hunt( visible, advance ); } } /* ------------------------- Sentry_AttackDecision ------------------------- */ void Sentry_AttackDecision( void ) { // Always keep a good height off the ground Sentry_MaintainHeight(); NPC->s.loopSound = G_SoundIndex( "sound/chars/sentry/misc/sentry_hover_2_lp" ); //randomly talk if ( TIMER_Done(NPC,"patrolNoise") ) { if (TIMER_Done(NPC,"angerNoise")) { G_SoundOnEnt( NPC, CHAN_AUTO, va("sound/chars/sentry/misc/talk%d", Q_irand(1, 3)) ); TIMER_Set( NPC, "patrolNoise", Q_irand( 4000, 10000 ) ); } } // He's dead. if (NPC->enemy->health<1) { NPC->enemy = NULL; Sentry_Idle(); return; } // If we don't have an enemy, just idle if ( NPC_CheckEnemyExt() == qfalse ) { Sentry_Idle(); return; } // Rate our distance to the target and visibilty float distance = (int) DistanceHorizontalSquared( NPC->currentOrigin, NPC->enemy->currentOrigin ); qboolean visible = NPC_ClearLOS( NPC->enemy ); qboolean advance = (qboolean)(distance > MIN_DISTANCE_SQR); // If we cannot see our target, move to see it if ( visible == qfalse ) { if ( NPCInfo->scriptFlags & SCF_CHASE_ENEMIES ) { Sentry_Hunt( visible, advance ); return; } } NPC_FaceEnemy( qtrue ); Sentry_RangedAttack( visible, advance ); } qboolean NPC_CheckPlayerTeamStealth( void ); /* ------------------------- NPC_Sentry_Patrol ------------------------- */ void NPC_Sentry_Patrol( void ) { Sentry_MaintainHeight(); //If we have somewhere to go, then do that if (!NPC->enemy) { if ( NPC_CheckPlayerTeamStealth() ) { //NPC_AngerSound(); NPC_UpdateAngles( qtrue, qtrue ); return; } if ( UpdateGoal() ) { //start loop sound once we move ucmd.buttons |= BUTTON_WALKING; NPC_MoveToGoal( qtrue ); } //randomly talk if (TIMER_Done(NPC,"patrolNoise")) { G_SoundOnEnt( NPC, CHAN_AUTO, va("sound/chars/sentry/misc/talk%d", Q_irand(1, 3)) ); TIMER_Set( NPC, "patrolNoise", Q_irand( 2000, 4000 ) ); } } NPC_UpdateAngles( qtrue, qtrue ); } /* ------------------------- NPC_BSSentry_Default ------------------------- */ void NPC_BSSentry_Default( void ) { if ( NPC->targetname ) { NPC->e_UseFunc = useF_sentry_use; } if (( NPC->enemy ) && (NPCInfo->localState != LSTATE_WAKEUP)) { // Don't attack if waking up or if no enemy Sentry_AttackDecision(); } else if ( NPCInfo->scriptFlags & SCF_LOOK_FOR_ENEMIES ) { NPC_Sentry_Patrol(); } else { Sentry_Idle(); } }