908 lines
No EOL
25 KiB
C++
908 lines
No EOL
25 KiB
C++
#include "g_headers.h"
|
|
#include "b_local.h"
|
|
//#include "g_nav.h"
|
|
//#include "anims.h"
|
|
//#include "wp_saber.h"
|
|
extern qboolean PM_FlippingAnim( int anim );
|
|
extern void NPC_BSST_Patrol( void );
|
|
|
|
extern void RT_FlyStart( gentity_t *self );
|
|
extern qboolean Q3_TaskIDPending( gentity_t *ent, taskID_t taskType );
|
|
|
|
#define VELOCITY_DECAY 0.7f
|
|
|
|
#define RT_FLYING_STRAFE_VEL 60
|
|
#define RT_FLYING_STRAFE_DIS 200
|
|
#define RT_FLYING_UPWARD_PUSH 150
|
|
|
|
#define RT_FLYING_FORWARD_BASE_SPEED 50
|
|
#define RT_FLYING_FORWARD_MULTIPLIER 10
|
|
|
|
void RT_Precache( void )
|
|
{
|
|
G_SoundIndex( "sound/chars/boba/bf_blast-off.wav" );
|
|
G_SoundIndex( "sound/chars/boba/bf_jetpack_lp.wav" );
|
|
G_SoundIndex( "sound/chars/boba/bf_land.wav" );
|
|
G_EffectIndex( "rockettrooper/flameNEW" );
|
|
G_EffectIndex( "rockettrooper/light_cone" );//extern this? At least use a different one
|
|
}
|
|
|
|
extern void NPC_BehaviorSet_Stormtrooper( int bState );
|
|
void RT_RunStormtrooperAI( void )
|
|
{
|
|
int bState;
|
|
//Execute our bState
|
|
if(NPCInfo->tempBehavior)
|
|
{//Overrides normal behavior until cleared
|
|
bState = NPCInfo->tempBehavior;
|
|
}
|
|
else
|
|
{
|
|
if(!NPCInfo->behaviorState)
|
|
NPCInfo->behaviorState = NPCInfo->defaultBehavior;
|
|
|
|
bState = NPCInfo->behaviorState;
|
|
}
|
|
NPC_BehaviorSet_Stormtrooper( bState );
|
|
}
|
|
|
|
void RT_FireDecide( void )
|
|
{
|
|
qboolean enemyLOS = qfalse;
|
|
qboolean enemyCS = qfalse;
|
|
qboolean enemyInFOV = qfalse;
|
|
//qboolean move = qtrue;
|
|
qboolean faceEnemy = qfalse;
|
|
qboolean shoot = qfalse;
|
|
qboolean hitAlly = qfalse;
|
|
vec3_t impactPos;
|
|
float enemyDist;
|
|
|
|
if ( NPC->client->ps.groundEntityNum == ENTITYNUM_NONE
|
|
&& NPC->client->ps.forceJumpZStart
|
|
&& !PM_FlippingAnim( NPC->client->ps.legsAnim )
|
|
&& !Q_irand( 0, 10 ) )
|
|
{//take off
|
|
RT_FlyStart( NPC );
|
|
}
|
|
|
|
if ( !NPC->enemy )
|
|
{
|
|
return;
|
|
}
|
|
|
|
VectorClear( impactPos );
|
|
enemyDist = DistanceSquared( NPC->currentOrigin, NPC->enemy->currentOrigin );
|
|
|
|
vec3_t enemyDir, shootDir;
|
|
VectorSubtract( NPC->enemy->currentOrigin, NPC->currentOrigin, enemyDir );
|
|
VectorNormalize( enemyDir );
|
|
AngleVectors( NPC->client->ps.viewangles, shootDir, NULL, NULL );
|
|
float dot = DotProduct( enemyDir, shootDir );
|
|
if ( dot > 0.5f ||( enemyDist * (1.0f-dot)) < 10000 )
|
|
{//enemy is in front of me or they're very close and not behind me
|
|
enemyInFOV = qtrue;
|
|
}
|
|
|
|
if ( enemyDist < MIN_ROCKET_DIST_SQUARED )//128
|
|
{//enemy within 128
|
|
if ( (NPC->client->ps.weapon == WP_FLECHETTE || NPC->client->ps.weapon == WP_REPEATER) &&
|
|
(NPCInfo->scriptFlags & SCF_ALT_FIRE) )
|
|
{//shooting an explosive, but enemy too close, switch to primary fire
|
|
NPCInfo->scriptFlags &= ~SCF_ALT_FIRE;
|
|
//FIXME: we can never go back to alt-fire this way since, after this, we don't know if we were initially supposed to use alt-fire or not...
|
|
}
|
|
}
|
|
|
|
//can we see our target?
|
|
if ( TIMER_Done( NPC, "nextAttackDelay" ) && TIMER_Done( NPC, "flameTime" ) )
|
|
{
|
|
if ( NPC_ClearLOS( NPC->enemy ) )
|
|
{
|
|
NPCInfo->enemyLastSeenTime = level.time;
|
|
enemyLOS = qtrue;
|
|
|
|
if ( NPC->client->ps.weapon == WP_NONE )
|
|
{
|
|
enemyCS = qfalse;//not true, but should stop us from firing
|
|
}
|
|
else
|
|
{//can we shoot our target?
|
|
if ( (NPC->client->ps.weapon == WP_ROCKET_LAUNCHER
|
|
|| (NPC->client->ps.weapon == WP_CONCUSSION && !(NPCInfo->scriptFlags&SCF_ALT_FIRE))
|
|
|| (NPC->client->ps.weapon == WP_FLECHETTE && (NPCInfo->scriptFlags&SCF_ALT_FIRE))) && enemyDist < MIN_ROCKET_DIST_SQUARED )//128*128
|
|
{
|
|
enemyCS = qfalse;//not true, but should stop us from firing
|
|
hitAlly = qtrue;//us!
|
|
//FIXME: if too close, run away!
|
|
}
|
|
else if ( enemyInFOV )
|
|
{//if enemy is FOV, go ahead and check for shooting
|
|
int hit = NPC_ShotEntity( NPC->enemy, impactPos );
|
|
gentity_t *hitEnt = &g_entities[hit];
|
|
|
|
if ( hit == NPC->enemy->s.number
|
|
|| ( hitEnt && hitEnt->client && hitEnt->client->playerTeam == NPC->client->enemyTeam )
|
|
|| ( hitEnt && hitEnt->takedamage && ((hitEnt->svFlags&SVF_GLASS_BRUSH)||hitEnt->health < 40||NPC->s.weapon == WP_EMPLACED_GUN) ) )
|
|
{//can hit enemy or enemy ally or will hit glass or other minor breakable (or in emplaced gun), so shoot anyway
|
|
enemyCS = qtrue;
|
|
//NPC_AimAdjust( 2 );//adjust aim better longer we have clear shot at enemy
|
|
VectorCopy( NPC->enemy->currentOrigin, NPCInfo->enemyLastSeenLocation );
|
|
}
|
|
else
|
|
{//Hmm, have to get around this bastard
|
|
//NPC_AimAdjust( 1 );//adjust aim better longer we can see enemy
|
|
if ( hitEnt && hitEnt->client && hitEnt->client->playerTeam == NPC->client->playerTeam )
|
|
{//would hit an ally, don't fire!!!
|
|
hitAlly = qtrue;
|
|
}
|
|
else
|
|
{//Check and see where our shot *would* hit... if it's not close to the enemy (within 256?), then don't fire
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
enemyCS = qfalse;//not true, but should stop us from firing
|
|
}
|
|
}
|
|
}
|
|
else if ( gi.inPVS( NPC->enemy->currentOrigin, NPC->currentOrigin ) )
|
|
{
|
|
NPCInfo->enemyLastSeenTime = level.time;
|
|
faceEnemy = qtrue;
|
|
//NPC_AimAdjust( -1 );//adjust aim worse longer we cannot see enemy
|
|
}
|
|
|
|
if ( NPC->client->ps.weapon == WP_NONE )
|
|
{
|
|
faceEnemy = qfalse;
|
|
shoot = qfalse;
|
|
}
|
|
else
|
|
{
|
|
if ( enemyLOS )
|
|
{//FIXME: no need to face enemy if we're moving to some other goal and he's too far away to shoot?
|
|
faceEnemy = qtrue;
|
|
}
|
|
if ( enemyCS )
|
|
{
|
|
shoot = qtrue;
|
|
}
|
|
}
|
|
|
|
if ( !enemyCS )
|
|
{//if have a clear shot, always try
|
|
//See if we should continue to fire on their last position
|
|
//!TIMER_Done( NPC, "stick" ) ||
|
|
if ( !hitAlly //we're not going to hit an ally
|
|
&& enemyInFOV //enemy is in our FOV //FIXME: or we don't have a clear LOS?
|
|
&& NPCInfo->enemyLastSeenTime > 0 )//we've seen the enemy
|
|
{
|
|
if ( level.time - NPCInfo->enemyLastSeenTime < 10000 )//we have seem the enemy in the last 10 seconds
|
|
{
|
|
if ( !Q_irand( 0, 10 ) )
|
|
{
|
|
//Fire on the last known position
|
|
vec3_t muzzle, dir, angles;
|
|
qboolean tooClose = qfalse;
|
|
qboolean tooFar = qfalse;
|
|
|
|
CalcEntitySpot( NPC, SPOT_HEAD, muzzle );
|
|
if ( VectorCompare( impactPos, vec3_origin ) )
|
|
{//never checked ShotEntity this frame, so must do a trace...
|
|
trace_t tr;
|
|
//vec3_t mins = {-2,-2,-2}, maxs = {2,2,2};
|
|
vec3_t forward, end;
|
|
AngleVectors( NPC->client->ps.viewangles, forward, NULL, NULL );
|
|
VectorMA( muzzle, 8192, forward, end );
|
|
gi.trace( &tr, muzzle, vec3_origin, vec3_origin, end, NPC->s.number, MASK_SHOT );
|
|
VectorCopy( tr.endpos, impactPos );
|
|
}
|
|
|
|
//see if impact would be too close to me
|
|
float distThreshold = 16384/*128*128*/;//default
|
|
switch ( NPC->s.weapon )
|
|
{
|
|
case WP_ROCKET_LAUNCHER:
|
|
case WP_FLECHETTE:
|
|
case WP_THERMAL:
|
|
case WP_TRIP_MINE:
|
|
case WP_DET_PACK:
|
|
distThreshold = 65536/*256*256*/;
|
|
break;
|
|
case WP_REPEATER:
|
|
if ( NPCInfo->scriptFlags&SCF_ALT_FIRE )
|
|
{
|
|
distThreshold = 65536/*256*256*/;
|
|
}
|
|
break;
|
|
case WP_CONCUSSION:
|
|
if ( !(NPCInfo->scriptFlags&SCF_ALT_FIRE) )
|
|
{
|
|
distThreshold = 65536/*256*256*/;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
float dist = DistanceSquared( impactPos, muzzle );
|
|
|
|
if ( dist < distThreshold )
|
|
{//impact would be too close to me
|
|
tooClose = qtrue;
|
|
}
|
|
else if ( level.time - NPCInfo->enemyLastSeenTime > 5000 ||
|
|
(NPCInfo->group && level.time - NPCInfo->group->lastSeenEnemyTime > 5000 ))
|
|
{//we've haven't seen them in the last 5 seconds
|
|
//see if it's too far from where he is
|
|
distThreshold = 65536/*256*256*/;//default
|
|
switch ( NPC->s.weapon )
|
|
{
|
|
case WP_ROCKET_LAUNCHER:
|
|
case WP_FLECHETTE:
|
|
case WP_THERMAL:
|
|
case WP_TRIP_MINE:
|
|
case WP_DET_PACK:
|
|
distThreshold = 262144/*512*512*/;
|
|
break;
|
|
case WP_REPEATER:
|
|
if ( NPCInfo->scriptFlags&SCF_ALT_FIRE )
|
|
{
|
|
distThreshold = 262144/*512*512*/;
|
|
}
|
|
break;
|
|
case WP_CONCUSSION:
|
|
if ( !(NPCInfo->scriptFlags&SCF_ALT_FIRE) )
|
|
{
|
|
distThreshold = 262144/*512*512*/;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
dist = DistanceSquared( impactPos, NPCInfo->enemyLastSeenLocation );
|
|
if ( dist > distThreshold )
|
|
{//impact would be too far from enemy
|
|
tooFar = qtrue;
|
|
}
|
|
}
|
|
|
|
if ( !tooClose && !tooFar )
|
|
{//okay too shoot at last pos
|
|
VectorSubtract( NPCInfo->enemyLastSeenLocation, muzzle, dir );
|
|
VectorNormalize( dir );
|
|
vectoangles( dir, angles );
|
|
|
|
NPCInfo->desiredYaw = angles[YAW];
|
|
NPCInfo->desiredPitch = angles[PITCH];
|
|
|
|
shoot = qtrue;
|
|
faceEnemy = qfalse;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//FIXME: don't shoot right away!
|
|
if ( NPC->client->fireDelay )
|
|
{
|
|
if ( NPC->s.weapon == WP_ROCKET_LAUNCHER
|
|
|| (NPC->s.weapon == WP_CONCUSSION&&!(NPCInfo->scriptFlags&SCF_ALT_FIRE)) )
|
|
{
|
|
if ( !enemyLOS || !enemyCS )
|
|
{//cancel it
|
|
NPC->client->fireDelay = 0;
|
|
}
|
|
else
|
|
{//delay our next attempt
|
|
TIMER_Set( NPC, "nextAttackDelay", Q_irand( 1000, 3000 ) );//FIXME: base on g_spskill
|
|
}
|
|
}
|
|
}
|
|
else if ( shoot )
|
|
{//try to shoot if it's time
|
|
if ( TIMER_Done( NPC, "nextAttackDelay" ) )
|
|
{
|
|
if( !(NPCInfo->scriptFlags & SCF_FIRE_WEAPON) ) // we've already fired, no need to do it again here
|
|
{
|
|
WeaponThink( qtrue );
|
|
}
|
|
//NASTY
|
|
int altChance = 6;//FIXME: base on g_spskill
|
|
if ( NPC->s.weapon == WP_ROCKET_LAUNCHER )
|
|
{
|
|
if ( (ucmd.buttons&BUTTON_ATTACK)
|
|
&& !Q_irand( 0, altChance ) )
|
|
{//every now and then, shoot a homing rocket
|
|
ucmd.buttons &= ~BUTTON_ATTACK;
|
|
ucmd.buttons |= BUTTON_ALT_ATTACK;
|
|
NPC->client->fireDelay = Q_irand( 1000, 3000 );//FIXME: base on g_spskill
|
|
}
|
|
}
|
|
else if ( NPC->s.weapon == WP_CONCUSSION )
|
|
{
|
|
if ( (ucmd.buttons&BUTTON_ATTACK)
|
|
&& Q_irand( 0, altChance*5 ) )
|
|
{//fire the beam shot
|
|
ucmd.buttons &= ~BUTTON_ATTACK;
|
|
ucmd.buttons |= BUTTON_ALT_ATTACK;
|
|
TIMER_Set( NPC, "nextAttackDelay", Q_irand( 1500, 2500 ) );//FIXME: base on g_spskill
|
|
}
|
|
else
|
|
{//fire the rocket-like shot
|
|
TIMER_Set( NPC, "nextAttackDelay", Q_irand( 3000, 5000 ) );//FIXME: base on g_spskill
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//=====================================================================================
|
|
//FLYING behavior
|
|
//=====================================================================================
|
|
qboolean RT_Flying( gentity_t *self )
|
|
{
|
|
return ((qboolean)(self->client->moveType==MT_FLYSWIM));
|
|
}
|
|
|
|
void RT_FlyStart( gentity_t *self )
|
|
{//switch to seeker AI for a while
|
|
if ( TIMER_Done( self, "jetRecharge" )
|
|
&& !RT_Flying( self ) )
|
|
{
|
|
self->client->ps.gravity = 0;
|
|
self->svFlags |= SVF_CUSTOM_GRAVITY;
|
|
self->client->moveType = MT_FLYSWIM;
|
|
//Inform NPC_HandleAIFlags we want to fly
|
|
self->NPC->aiFlags |= NPCAI_FLY;
|
|
self->lastInAirTime = level.time;
|
|
|
|
//start jet effect
|
|
self->client->jetPackTime = Q3_INFINITE;
|
|
if ( self->genericBolt1 != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "rockettrooper/flameNEW" ), self->playerModel, self->genericBolt1, self->s.number, self->currentOrigin, qtrue, qtrue );
|
|
}
|
|
if ( self->genericBolt2 != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "rockettrooper/flameNEW" ), self->playerModel, self->genericBolt2, self->s.number, self->currentOrigin, qtrue, qtrue );
|
|
}
|
|
|
|
//take-off sound
|
|
G_SoundOnEnt( self, CHAN_ITEM, "sound/chars/boba/bf_blast-off.wav" );
|
|
//jet loop sound
|
|
self->s.loopSound = G_SoundIndex( "sound/chars/boba/bf_jetpack_lp.wav" );
|
|
if ( self->NPC )
|
|
{
|
|
self->count = Q3_INFINITE; // SEEKER shot ammo count
|
|
}
|
|
}
|
|
}
|
|
|
|
void RT_FlyStop( gentity_t *self )
|
|
{
|
|
self->client->ps.gravity = g_gravity->value;
|
|
self->svFlags &= ~SVF_CUSTOM_GRAVITY;
|
|
self->client->moveType = MT_RUNJUMP;
|
|
//Stop the effect
|
|
self->client->jetPackTime = 0;
|
|
if ( self->genericBolt1 != -1 )
|
|
{
|
|
G_StopEffect("rockettrooper/flameNEW", self->playerModel, self->genericBolt1, self->s.number );
|
|
}
|
|
if ( self->genericBolt2 != -1 )
|
|
{
|
|
G_StopEffect("rockettrooper/flameNEW", self->playerModel, self->genericBolt2, self->s.number );
|
|
}
|
|
//stop jet loop sound
|
|
self->s.loopSound = 0;
|
|
G_SoundOnEnt( self, CHAN_ITEM, "sound/chars/boba/bf_land.wav" );
|
|
|
|
if ( self->NPC )
|
|
{
|
|
self->count = 0; // SEEKER shot ammo count
|
|
TIMER_Set( self, "jetRecharge", Q_irand( 1000, 5000 ) );
|
|
TIMER_Set( self, "jumpChaseDebounce", Q_irand( 500, 2000 ) );
|
|
}
|
|
}
|
|
|
|
void RT_JetPackEffect( int duration )
|
|
{
|
|
if ( NPC->genericBolt1 != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "rockettrooper/flameNEW" ), NPC->playerModel, NPC->genericBolt1, NPC->s.number, NPC->currentOrigin, duration, qtrue );
|
|
}
|
|
if ( NPC->genericBolt2 != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "rockettrooper/flameNEW" ), NPC->playerModel, NPC->genericBolt2, NPC->s.number, NPC->currentOrigin, duration, qtrue );
|
|
}
|
|
|
|
//take-off sound
|
|
G_SoundOnEnt( NPC, CHAN_ITEM, "sound/chars/boba/bf_blast-off.wav" );
|
|
}
|
|
|
|
void RT_Flying_ApplyFriction( float frictionScale )
|
|
{
|
|
if ( NPC->client->ps.velocity[0] )
|
|
{
|
|
NPC->client->ps.velocity[0] *= VELOCITY_DECAY;///frictionScale;
|
|
|
|
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] *= VELOCITY_DECAY;///frictionScale;
|
|
|
|
if ( fabs( NPC->client->ps.velocity[1] ) < 1 )
|
|
{
|
|
NPC->client->ps.velocity[1] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
void RT_Flying_MaintainHeight( void )
|
|
{
|
|
float dif = 0;
|
|
|
|
// Update our angles regardless
|
|
NPC_UpdateAngles( qtrue, qtrue );
|
|
|
|
if ( NPC->forcePushTime > level.time )
|
|
{//if being pushed, we don't have control over our movement
|
|
return;
|
|
}
|
|
|
|
if ( (NPC->client->ps.pm_flags&PMF_TIME_KNOCKBACK) )
|
|
{//don't slow down for a bit
|
|
if ( NPC->client->ps.pm_time > 0 )
|
|
{
|
|
VectorScale( NPC->client->ps.velocity, 0.9f, NPC->client->ps.velocity );
|
|
return;
|
|
}
|
|
}
|
|
|
|
/*
|
|
if ( (NPC->client->ps.eFlags&EF_FORCE_GRIPPED) )
|
|
{
|
|
RT_Flying_ApplyFriction( 3.0f );
|
|
return;
|
|
}
|
|
*/
|
|
// If we have an enemy, we should try to hover at or a little below enemy eye level
|
|
if ( NPC->enemy
|
|
&& (!Q3_TaskIDPending( NPC, TID_MOVE_NAV ) || !NPCInfo->goalEntity ) )
|
|
{
|
|
if (TIMER_Done( NPC, "heightChange" ))
|
|
{
|
|
TIMER_Set( NPC,"heightChange",Q_irand( 1000, 3000 ));
|
|
|
|
float enemyZHeight = NPC->enemy->currentOrigin[2];
|
|
if ( NPC->enemy->client
|
|
&& NPC->enemy->client->ps.groundEntityNum == ENTITYNUM_NONE
|
|
&& (NPC->enemy->client->ps.forcePowersActive&(1<<FP_LEVITATION)) )
|
|
{//so we don't go up when they force jump up at us
|
|
enemyZHeight = NPC->enemy->client->ps.forceJumpZStart;
|
|
}
|
|
|
|
// Find the height difference
|
|
dif = (enemyZHeight + Q_flrand( NPC->enemy->maxs[2]/2, NPC->enemy->maxs[2]+8 )) - NPC->currentOrigin[2];
|
|
|
|
float difFactor = 10.0f;
|
|
|
|
// cap to prevent dramatic height shifts
|
|
if ( fabs( dif ) > 2*difFactor )
|
|
{
|
|
if ( fabs( dif ) > 20*difFactor )
|
|
{
|
|
dif = ( dif < 0 ? -20*difFactor : 20*difFactor );
|
|
}
|
|
|
|
NPC->client->ps.velocity[2] = (NPC->client->ps.velocity[2]+dif)/2;
|
|
}
|
|
NPC->client->ps.velocity[2] *= Q_flrand( 0.85f, 1.25f );
|
|
}
|
|
else
|
|
{//don't get too far away from height of enemy...
|
|
float enemyZHeight = NPC->enemy->currentOrigin[2];
|
|
if ( NPC->enemy->client
|
|
&& NPC->enemy->client->ps.groundEntityNum == ENTITYNUM_NONE
|
|
&& (NPC->enemy->client->ps.forcePowersActive&(1<<FP_LEVITATION)) )
|
|
{//so we don't go up when they force jump up at us
|
|
enemyZHeight = NPC->enemy->client->ps.forceJumpZStart;
|
|
}
|
|
dif = NPC->currentOrigin[2] - (enemyZHeight+64);
|
|
float maxHeight = 200;
|
|
float hDist = DistanceHorizontal( NPC->enemy->currentOrigin, NPC->currentOrigin );
|
|
if ( hDist < 512 )
|
|
{
|
|
maxHeight *= hDist/512;
|
|
}
|
|
if ( dif > maxHeight )
|
|
{
|
|
if ( NPC->client->ps.velocity[2] > 0 )//FIXME: or: we can't see him anymore
|
|
{//slow down
|
|
if ( NPC->client->ps.velocity[2] )
|
|
{
|
|
NPC->client->ps.velocity[2] *= VELOCITY_DECAY;
|
|
|
|
if ( fabs( NPC->client->ps.velocity[2] ) < 2 )
|
|
{
|
|
NPC->client->ps.velocity[2] = 0;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{//start coming back down
|
|
NPC->client->ps.velocity[2] -= 4;
|
|
}
|
|
}
|
|
else if ( dif < -200 && NPC->client->ps.velocity[2] < 0 )//we're way below him
|
|
{
|
|
if ( NPC->client->ps.velocity[2] < 0 )//FIXME: or: we can't see him anymore
|
|
{//slow down
|
|
if ( NPC->client->ps.velocity[2] )
|
|
{
|
|
NPC->client->ps.velocity[2] *= VELOCITY_DECAY;
|
|
|
|
if ( fabs( NPC->client->ps.velocity[2] ) > -2 )
|
|
{
|
|
NPC->client->ps.velocity[2] = 0;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{//start going back up
|
|
NPC->client->ps.velocity[2] += 4;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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];
|
|
}
|
|
else if ( VectorCompare( NPC->pos1, vec3_origin ) )
|
|
{//have a starting position as a reference point
|
|
dif = NPC->pos1[2] - NPC->currentOrigin[2];
|
|
}
|
|
|
|
if ( fabs( dif ) > 24 )
|
|
{
|
|
ucmd.upmove = ( ucmd.upmove < 0 ? -4 : 4 );
|
|
}
|
|
else
|
|
{
|
|
if ( NPC->client->ps.velocity[2] )
|
|
{
|
|
NPC->client->ps.velocity[2] *= VELOCITY_DECAY;
|
|
|
|
if ( fabs( NPC->client->ps.velocity[2] ) < 2 )
|
|
{
|
|
NPC->client->ps.velocity[2] = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply friction
|
|
RT_Flying_ApplyFriction( 1.0f );
|
|
}
|
|
|
|
void RT_Flying_Strafe( void )
|
|
{
|
|
int side;
|
|
vec3_t end, right, dir;
|
|
trace_t tr;
|
|
|
|
if ( random() > 0.7f
|
|
|| !NPC->enemy
|
|
|| !NPC->enemy->client )
|
|
{
|
|
// Do a regular style strafe
|
|
AngleVectors( NPC->client->renderInfo.eyeAngles, NULL, right, NULL );
|
|
|
|
// Pick a random strafe direction, then check to see if doing a strafe would be
|
|
// reasonably valid
|
|
side = ( rand() & 1 ) ? -1 : 1;
|
|
VectorMA( NPC->currentOrigin, RT_FLYING_STRAFE_DIS * side, right, end );
|
|
|
|
gi.trace( &tr, NPC->currentOrigin, NULL, NULL, end, NPC->s.number, MASK_SOLID );
|
|
|
|
// Close enough
|
|
if ( tr.fraction > 0.9f )
|
|
{
|
|
float vel = RT_FLYING_STRAFE_VEL+Q_flrand(-20,20);
|
|
VectorMA( NPC->client->ps.velocity, vel*side, right, NPC->client->ps.velocity );
|
|
if ( !Q_irand( 0, 3 ) )
|
|
{
|
|
// Add a slight upward push
|
|
float upPush = RT_FLYING_UPWARD_PUSH;
|
|
if ( NPC->client->ps.velocity[2] < 300 )
|
|
{
|
|
if ( NPC->client->ps.velocity[2] < 300+upPush )
|
|
{
|
|
NPC->client->ps.velocity[2] += upPush;
|
|
}
|
|
else
|
|
{
|
|
NPC->client->ps.velocity[2] = 300;
|
|
}
|
|
}
|
|
}
|
|
|
|
NPCInfo->standTime = level.time + 1000 + random() * 500;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Do a strafe to try and keep on the side of their enemy
|
|
AngleVectors( NPC->enemy->client->renderInfo.eyeAngles, dir, right, NULL );
|
|
|
|
// Pick a random side
|
|
side = ( rand() & 1 ) ? -1 : 1;
|
|
float stDis = RT_FLYING_STRAFE_DIS*2.0f;
|
|
VectorMA( NPC->enemy->currentOrigin, stDis * side, right, end );
|
|
|
|
// then add a very small bit of random in front of/behind the player action
|
|
VectorMA( end, crandom() * 25, dir, end );
|
|
|
|
gi.trace( &tr, NPC->currentOrigin, NULL, NULL, end, NPC->s.number, MASK_SOLID );
|
|
|
|
// Close enough
|
|
if ( tr.fraction > 0.9f )
|
|
{
|
|
float vel = (RT_FLYING_STRAFE_VEL*4)+Q_flrand(-20,20);
|
|
VectorSubtract( tr.endpos, NPC->currentOrigin, dir );
|
|
dir[2] *= 0.25; // do less upward change
|
|
float dis = VectorNormalize( dir );
|
|
if ( dis > vel )
|
|
{
|
|
dis = vel;
|
|
}
|
|
// Try to move the desired enemy side
|
|
VectorMA( NPC->client->ps.velocity, dis, dir, NPC->client->ps.velocity );
|
|
|
|
if ( !Q_irand( 0, 3 ) )
|
|
{
|
|
float upPush = RT_FLYING_UPWARD_PUSH;
|
|
// Add a slight upward push
|
|
if ( NPC->client->ps.velocity[2] < 300 )
|
|
{
|
|
if ( NPC->client->ps.velocity[2] < 300+upPush )
|
|
{
|
|
NPC->client->ps.velocity[2] += upPush;
|
|
}
|
|
else
|
|
{
|
|
NPC->client->ps.velocity[2] = 300;
|
|
}
|
|
}
|
|
else if ( NPC->client->ps.velocity[2] > 300 )
|
|
{
|
|
NPC->client->ps.velocity[2] = 300;
|
|
}
|
|
}
|
|
|
|
NPCInfo->standTime = level.time + 2500 + random() * 500;
|
|
}
|
|
}
|
|
}
|
|
|
|
void RT_Flying_Hunt( qboolean visible, qboolean advance )
|
|
{
|
|
float distance, speed;
|
|
vec3_t forward;
|
|
|
|
if ( NPC->forcePushTime >= level.time )
|
|
//|| (NPC->client->ps.eFlags&EF_FORCE_GRIPPED) )
|
|
{//if being pushed, we don't have control over our movement
|
|
NPC->delay = 0;
|
|
return;
|
|
}
|
|
NPC_FaceEnemy( qtrue );
|
|
|
|
// 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 )
|
|
{
|
|
NPC->delay = 0;
|
|
RT_Flying_Strafe();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If we don't want to advance, stop here
|
|
if ( advance )
|
|
{
|
|
// Only try and navigate if the player is visible
|
|
if ( visible == qfalse )
|
|
{
|
|
// Move towards our goal
|
|
NPCInfo->goalEntity = NPC->enemy;
|
|
NPCInfo->goalRadius = 24;
|
|
|
|
NPC->delay = 0;
|
|
NPC_MoveToGoal(qtrue);
|
|
return;
|
|
|
|
}
|
|
}
|
|
//else move straight at/away from him
|
|
VectorSubtract( NPC->enemy->currentOrigin, NPC->currentOrigin, forward );
|
|
forward[2] *= 0.1f;
|
|
distance = VectorNormalize( forward );
|
|
|
|
speed = RT_FLYING_FORWARD_BASE_SPEED + RT_FLYING_FORWARD_MULTIPLIER * g_spskill->integer;
|
|
if ( advance && distance < Q_flrand( 256, 3096 ) )
|
|
{
|
|
NPC->delay = 0;
|
|
VectorMA( NPC->client->ps.velocity, speed, forward, NPC->client->ps.velocity );
|
|
}
|
|
else if ( distance < Q_flrand( 0, 128 ) )
|
|
{
|
|
if ( NPC->health <= 50 )
|
|
{//always back off
|
|
NPC->delay = 0;
|
|
}
|
|
else if ( !TIMER_Done( NPC, "backoffTime" ) )
|
|
{//still backing off from end of last delay
|
|
NPC->delay = 0;
|
|
}
|
|
else if ( !NPC->delay )
|
|
{//start a new delay
|
|
NPC->delay = Q_irand( 0, 10+(20*(2-g_spskill->integer)) );
|
|
}
|
|
else
|
|
{//continue the current delay
|
|
NPC->delay--;
|
|
}
|
|
if ( !NPC->delay )
|
|
{//delay done, now back off for a few seconds!
|
|
TIMER_Set( NPC, "backoffTime", Q_irand( 2000, 5000 ) );
|
|
VectorMA( NPC->client->ps.velocity, speed*-2, forward, NPC->client->ps.velocity );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NPC->delay = 0;
|
|
}
|
|
}
|
|
|
|
void RT_Flying_Ranged( qboolean visible, qboolean advance )
|
|
{
|
|
if ( NPCInfo->scriptFlags & SCF_CHASE_ENEMIES )
|
|
{
|
|
RT_Flying_Hunt( visible, advance );
|
|
}
|
|
}
|
|
|
|
void RT_Flying_Attack( void )
|
|
{
|
|
// Always keep a good height off the ground
|
|
RT_Flying_MaintainHeight();
|
|
|
|
// Rate our distance to the target, and our visibilty
|
|
float distance = DistanceHorizontalSquared( NPC->currentOrigin, NPC->enemy->currentOrigin );
|
|
qboolean visible = NPC_ClearLOS( NPC->enemy );
|
|
qboolean advance = (qboolean)(distance>(256.0f*256.0f));
|
|
|
|
// If we cannot see our target, move to see it
|
|
if ( visible == qfalse )
|
|
{
|
|
if ( NPCInfo->scriptFlags & SCF_CHASE_ENEMIES )
|
|
{
|
|
RT_Flying_Hunt( visible, advance );
|
|
return;
|
|
}
|
|
}
|
|
|
|
RT_Flying_Ranged( visible, advance );
|
|
}
|
|
|
|
void RT_Flying_Think( void )
|
|
{
|
|
if ( Q3_TaskIDPending( NPC, TID_MOVE_NAV )
|
|
&& UpdateGoal() )
|
|
{//being scripted to go to a certain spot, don't maintain height
|
|
if ( NPC_MoveToGoal( qtrue ) )
|
|
{//we could macro-nav to our goal
|
|
if ( NPC->enemy && NPC->enemy->health && NPC->enemy->inuse )
|
|
{
|
|
NPC_FaceEnemy( qtrue );
|
|
RT_FireDecide();
|
|
}
|
|
}
|
|
else
|
|
{//frick, no where to nav to, keep us in the air!
|
|
RT_Flying_MaintainHeight();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ( NPC->random == 0.0f )
|
|
{
|
|
// used to offset seekers around a circle so they don't occupy the same spot. This is not a fool-proof method.
|
|
NPC->random = random() * 6.3f; // roughly 2pi
|
|
}
|
|
|
|
if ( NPC->enemy && NPC->enemy->health && NPC->enemy->inuse )
|
|
{
|
|
RT_Flying_Attack();
|
|
RT_FireDecide();
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
RT_Flying_MaintainHeight();
|
|
RT_RunStormtrooperAI();
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
//=====================================================================================
|
|
//ON GROUND WITH ENEMY behavior
|
|
//=====================================================================================
|
|
|
|
|
|
//=====================================================================================
|
|
//DEFAULT behavior
|
|
//=====================================================================================
|
|
extern void RT_CheckJump( void );
|
|
void NPC_BSRT_Default( void )
|
|
{
|
|
//FIXME: custom pain and death funcs:
|
|
//pain3 is in air
|
|
//die in air is both_falldeath1
|
|
//attack1 is on ground, attack2 is in air
|
|
|
|
//FIXME: this doesn't belong here
|
|
if ( NPC->client->ps.groundEntityNum != ENTITYNUM_NONE )
|
|
{
|
|
if ( NPCInfo->rank >= RANK_LT )//&& !Q_irand( 0, 50 ) )
|
|
{//officers always stay in the air
|
|
NPC->client->ps.velocity[2] = Q_irand( 50, 125 );
|
|
NPC->NPC->aiFlags |= NPCAI_FLY; //fixme also, Inform NPC_HandleAIFlags we want to fly
|
|
}
|
|
}
|
|
|
|
if ( RT_Flying( NPC ) )
|
|
{//FIXME: only officers need do this, right?
|
|
RT_Flying_Think();
|
|
}
|
|
else if ( NPC->enemy != NULL )
|
|
{//rocketrooper on ground with enemy
|
|
UpdateGoal();
|
|
RT_RunStormtrooperAI();
|
|
RT_CheckJump();
|
|
//NPC_BSST_Default();//FIXME: add missile avoidance
|
|
//RT_Hunt();//NPC_BehaviorSet_Jedi( bState );
|
|
}
|
|
else
|
|
{//shouldn't have gotten in here
|
|
RT_RunStormtrooperAI();
|
|
//NPC_BSST_Patrol();
|
|
}
|
|
} |