/* =========================================================================== 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" 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 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; //NPC_AimAdjust( -1 );//adjust aim worse longer we cannot see enemy } if ( NPC->client->ps.weapon == WP_NONE ) { shoot = qfalse; } else { 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, (EG2_Collision)0, 0 ); 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; } } } } } //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 if (self->NPC){ 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<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<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 ( Q_flrand(0.0f, 1.0f) > 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, (EG2_Collision)0, 0 ); // 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 + Q_flrand(0.0f, 1.0f) * 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, Q_flrand(-1.0f, 1.0f) * 25, dir, 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 ) { 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 + Q_flrand(0.0f, 1.0f) * 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 = Q_flrand(0.0f, 1.0f) * 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(); } }