codemp/game/NPC_AI_Jedi.c

Go to the documentation of this file.
00001 #include "b_local.h"
00002 #include "g_nav.h"
00003 #include "anims.h"
00004 #include "w_saber.h"
00005 
00006 #include "../namespace_begin.h"
00007 extern qboolean BG_SabersOff( playerState_t *ps );
00008 #include "../namespace_end.h"
00009 
00010 extern void CG_DrawAlert( vec3_t origin, float rating );
00011 extern void G_AddVoiceEvent( gentity_t *self, int event, int speakDebounceTime );
00012 extern void ForceJump( gentity_t *self, usercmd_t *ucmd );
00013 extern vmCvar_t g_saberRealisticCombat;
00014 extern vmCvar_t d_slowmodeath;
00015 
00016 void G_StartMatrixEffect( gentity_t *ent )
00017 { //perhaps write this at some point?
00018 
00019 }
00020 
00021 #define MAX_VIEW_DIST           2048
00022 #define MAX_VIEW_SPEED          100
00023 #define JEDI_MAX_LIGHT_INTENSITY 64
00024 #define JEDI_MIN_LIGHT_THRESHOLD 10
00025 #define JEDI_MAX_LIGHT_THRESHOLD 50
00026 
00027 #define DISTANCE_SCALE          0.25f
00028 //#define       DISTANCE_THRESHOLD      0.075f
00029 #define SPEED_SCALE                     0.25f
00030 #define FOV_SCALE                       0.5f
00031 #define LIGHT_SCALE                     0.25f
00032 
00033 #define REALIZE_THRESHOLD       0.6f
00034 #define CAUTIOUS_THRESHOLD      ( REALIZE_THRESHOLD * 0.3 )
00035 
00036 #define MAX_CHECK_THRESHOLD     1
00037 
00038 extern void NPC_ClearLookTarget( gentity_t *self );
00039 extern void NPC_SetLookTarget( gentity_t *self, int entNum, int clearTime );
00040 extern void NPC_TempLookTarget( gentity_t *self, int lookEntNum, int minLookTime, int maxLookTime );
00041 extern qboolean G_ExpandPointToBBox( vec3_t point, const vec3_t mins, const vec3_t maxs, int ignore, int clipmask );
00042 extern qboolean NPC_CheckEnemyStealth( void );
00043 extern void G_SoundOnEnt( gentity_t *ent, soundChannel_t channel, const char *soundPath );
00044 
00045 #include "../namespace_begin.h"
00046 extern gitem_t  *BG_FindItemForAmmo( ammo_t ammo );
00047 #include "../namespace_end.h"
00048 
00049 extern void ForceThrow( gentity_t *self, qboolean pull );
00050 extern void ForceLightning( gentity_t *self );
00051 extern void ForceHeal( gentity_t *self );
00052 extern void ForceRage( gentity_t *self );
00053 extern void ForceProtect( gentity_t *self );
00054 extern void ForceAbsorb( gentity_t *self );
00055 extern int WP_MissileBlockForBlock( int saberBlock );
00056 extern qboolean G_GetHitLocFromSurfName( gentity_t *ent, const char *surfName, int *hitLoc, vec3_t point, vec3_t dir, vec3_t bladeDir, int mod );
00057 extern qboolean WP_ForcePowerUsable( gentity_t *self, forcePowers_t forcePower, int overrideAmt );
00058 extern qboolean WP_ForcePowerAvailable( gentity_t *self, forcePowers_t forcePower, int overrideAmt );
00059 extern void WP_ForcePowerStop( gentity_t *self, forcePowers_t forcePower );
00060 extern void WP_DeactivateSaber( gentity_t *self, qboolean clearLength ); //clearLength = qfalse
00061 extern void WP_ActivateSaber( gentity_t *self );
00062 //extern void WP_SaberBlock(gentity_t *saber, vec3_t hitloc);
00063 
00064 #include "../namespace_begin.h"
00065 extern qboolean PM_SaberInStart( int move );
00066 extern qboolean BG_SaberInSpecialAttack( int anim );
00067 extern qboolean BG_SaberInAttack( int move );
00068 extern qboolean PM_SaberInBounce( int move );
00069 extern qboolean PM_SaberInParry( int move );
00070 extern qboolean PM_SaberInKnockaway( int move );
00071 extern qboolean PM_SaberInBrokenParry( int move );
00072 extern qboolean PM_SaberInDeflect( int move );
00073 extern qboolean BG_SpinningSaberAnim( int anim );
00074 extern qboolean BG_FlippingAnim( int anim );
00075 extern qboolean PM_RollingAnim( int anim );
00076 extern qboolean PM_InKnockDown( playerState_t *ps );
00077 extern qboolean BG_InRoll( playerState_t *ps, int anim );
00078 extern qboolean BG_CrouchAnim( int anim );
00079 #include "../namespace_end.h"
00080 
00081 extern qboolean NPC_SomeoneLookingAtMe(gentity_t *ent);
00082 
00083 extern int WP_GetVelocityForForceJump( gentity_t *self, vec3_t jumpVel, usercmd_t *ucmd );
00084 
00085 extern void G_TestLine(vec3_t start, vec3_t end, int color, int time);
00086 
00087 static void Jedi_Aggression( gentity_t *self, int change );
00088 qboolean Jedi_WaitingAmbush( gentity_t *self );
00089 
00090 #include "../namespace_begin.h"
00091 extern int bg_parryDebounce[];
00092 #include "../namespace_end.h"
00093 
00094 static int      jediSpeechDebounceTime[TEAM_NUM_TEAMS];//used to stop several jedi from speaking all at once
00095 //Local state enums
00096 enum
00097 {
00098         LSTATE_NONE = 0,
00099         LSTATE_UNDERFIRE,
00100         LSTATE_INVESTIGATE,
00101 };
00102 
00103 void NPC_ShadowTrooper_Precache( void )
00104 {
00105         RegisterItem( BG_FindItemForAmmo( AMMO_FORCE ) );
00106         G_SoundIndex( "sound/chars/shadowtrooper/cloak.wav" );
00107         G_SoundIndex( "sound/chars/shadowtrooper/decloak.wav" );
00108 }
00109 
00110 void Jedi_ClearTimers( gentity_t *ent )
00111 {
00112         TIMER_Set( ent, "roamTime", 0 );
00113         TIMER_Set( ent, "chatter", 0 );
00114         TIMER_Set( ent, "strafeLeft", 0 );
00115         TIMER_Set( ent, "strafeRight", 0 );
00116         TIMER_Set( ent, "noStrafe", 0 );
00117         TIMER_Set( ent, "walking", 0 );
00118         TIMER_Set( ent, "taunting", 0 );
00119         TIMER_Set( ent, "parryTime", 0 );
00120         TIMER_Set( ent, "parryReCalcTime", 0 );
00121         TIMER_Set( ent, "forceJumpChasing", 0 );
00122         TIMER_Set( ent, "jumpChaseDebounce", 0 );
00123         TIMER_Set( ent, "moveforward", 0 );
00124         TIMER_Set( ent, "moveback", 0 );
00125         TIMER_Set( ent, "movenone", 0 );
00126         TIMER_Set( ent, "moveright", 0 );
00127         TIMER_Set( ent, "moveleft", 0 );
00128         TIMER_Set( ent, "movecenter", 0 );
00129         TIMER_Set( ent, "saberLevelDebounce", 0 );
00130         TIMER_Set( ent, "noRetreat", 0 );
00131         TIMER_Set( ent, "holdLightning", 0 );
00132         TIMER_Set( ent, "gripping", 0 );
00133         TIMER_Set( ent, "draining", 0 );
00134         TIMER_Set( ent, "noturn", 0 );
00135 }
00136 
00137 void Jedi_PlayBlockedPushSound( gentity_t *self )
00138 {
00139         if ( !self->s.number )
00140         {
00141                 G_AddVoiceEvent( self, EV_PUSHFAIL, 3000 );
00142         }
00143         else if ( self->health > 0 && self->NPC && self->NPC->blockedSpeechDebounceTime < level.time )
00144         {
00145                 G_AddVoiceEvent( self, EV_PUSHFAIL, 3000 );
00146                 self->NPC->blockedSpeechDebounceTime = level.time + 3000;
00147         }
00148 }
00149 
00150 void Jedi_PlayDeflectSound( gentity_t *self )
00151 {
00152         if ( !self->s.number )
00153         {
00154                 G_AddVoiceEvent( self, Q_irand( EV_DEFLECT1, EV_DEFLECT3 ), 3000 );
00155         }
00156         else if ( self->health > 0 && self->NPC && self->NPC->blockedSpeechDebounceTime < level.time )
00157         {
00158                 G_AddVoiceEvent( self, Q_irand( EV_DEFLECT1, EV_DEFLECT3 ), 3000 );
00159                 self->NPC->blockedSpeechDebounceTime = level.time + 3000;
00160         }
00161 }
00162 
00163 void NPC_Jedi_PlayConfusionSound( gentity_t *self )
00164 {
00165         if ( self->health > 0 )
00166         {
00167                 if ( self->client && ( self->client->NPC_class == CLASS_TAVION || self->client->NPC_class == CLASS_DESANN ) )
00168                 {
00169                         G_AddVoiceEvent( self, Q_irand( EV_CONFUSE1, EV_CONFUSE3 ), 2000 );
00170                 }
00171                 else if ( Q_irand( 0, 1 ) )
00172                 {
00173                         G_AddVoiceEvent( self, Q_irand( EV_TAUNT1, EV_TAUNT3 ), 2000 );
00174                 }
00175                 else
00176                 {
00177                         G_AddVoiceEvent( self, Q_irand( EV_GLOAT1, EV_GLOAT3 ), 2000 );
00178                 }
00179         }
00180 }
00181 
00182 void Boba_Precache( void )
00183 {
00184         G_SoundIndex( "sound/boba/jeton.wav" );
00185         G_SoundIndex( "sound/boba/jethover.wav" );
00186         G_SoundIndex( "sound/effects/combustfire.mp3" );
00187         G_EffectIndex( "boba/jet" );
00188         G_EffectIndex( "boba/fthrw" );
00189 }
00190 
00191 extern void G_CreateG2AttachedWeaponModel( gentity_t *ent, const char *weaponModel, int boltNum, int weaponNum );
00192 extern void ChangeWeapon( gentity_t *ent, int newWeapon );
00193 void Boba_ChangeWeapon( int wp )
00194 {
00195         if ( NPC->s.weapon == wp )
00196         {
00197                 return;
00198         }
00199         NPC_ChangeWeapon( wp );
00200         G_AddEvent( NPC, EV_GENERAL_SOUND, G_SoundIndex( "sound/weapons/change.wav" ));
00201 }
00202 
00203 void WP_ResistForcePush( gentity_t *self, gentity_t *pusher, qboolean noPenalty )
00204 {
00205         int parts;
00206         qboolean runningResist = qfalse;
00207 
00208         if ( !self || self->health <= 0 || !self->client || !pusher || !pusher->client ) 
00209         {
00210                 return;
00211         }
00212         if ( (!self->s.number || self->client->NPC_class == CLASS_DESANN || !Q_stricmp("Yoda",self->NPC_type) || self->client->NPC_class == CLASS_LUKE)
00213                 && (VectorLengthSquared( self->client->ps.velocity ) > 10000 || self->client->ps.fd.forcePowerLevel[FP_PUSH] >= FORCE_LEVEL_3 || self->client->ps.fd.forcePowerLevel[FP_PULL] >= FORCE_LEVEL_3 ) )
00214         {
00215                 runningResist = qtrue;
00216         }
00217         if ( !runningResist
00218                 && self->client->ps.groundEntityNum != ENTITYNUM_NONE 
00219                 && !BG_SpinningSaberAnim( self->client->ps.legsAnim ) 
00220                 && !BG_FlippingAnim( self->client->ps.legsAnim ) 
00221                 && !PM_RollingAnim( self->client->ps.legsAnim ) 
00222                 && !PM_InKnockDown( &self->client->ps ) 
00223                 && !BG_CrouchAnim( self->client->ps.legsAnim ))
00224         {//if on a surface and not in a spin or flip, play full body resist
00225                 parts = SETANIM_BOTH;
00226         }
00227         else
00228         {//play resist just in torso
00229                 parts = SETANIM_TORSO;
00230         }
00231         NPC_SetAnim( self, parts, BOTH_RESISTPUSH, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
00232         if ( !noPenalty )
00233         {
00234                 char buf[128];
00235                 float tFVal = 0;
00236 
00237                 trap_Cvar_VariableStringBuffer("timescale", buf, sizeof(buf));
00238 
00239                 tFVal = atof(buf);
00240 
00241                 if ( !runningResist )
00242                 {
00243                         VectorClear( self->client->ps.velocity );
00244                         //still stop them from attacking or moving for a bit, though
00245                         //FIXME: maybe push just a little (like, slide)?
00246                         self->client->ps.weaponTime = 1000;
00247                         if ( self->client->ps.fd.forcePowersActive&(1<<FP_SPEED) )
00248                         {
00249                                 self->client->ps.weaponTime = floor( self->client->ps.weaponTime * tFVal );
00250                         }
00251                         self->client->ps.pm_time = self->client->ps.weaponTime;
00252                         self->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
00253                         //play the full body push effect on me
00254                         //self->forcePushTime = level.time + 600; // let the push effect last for 600 ms
00255                         //rwwFIXMEFIXME: Do this?
00256                 }
00257                 else
00258                 {
00259                         self->client->ps.weaponTime = 600;
00260                         if ( self->client->ps.fd.forcePowersActive&(1<<FP_SPEED) )
00261                         {
00262                                 self->client->ps.weaponTime = floor( self->client->ps.weaponTime * tFVal );
00263                         }
00264                 }
00265         }
00266         //play my force push effect on my hand
00267         self->client->ps.powerups[PW_DISINT_4] = level.time + self->client->ps.torsoTimer + 500;
00268         self->client->ps.powerups[PW_PULL] = 0;
00269         Jedi_PlayBlockedPushSound( self );
00270 }
00271 
00272 qboolean Boba_StopKnockdown( gentity_t *self, gentity_t *pusher, vec3_t pushDir, qboolean forceKnockdown ) //forceKnockdown = qfalse
00273 {
00274         vec3_t  pDir, fwd, right, ang;
00275         float   fDot, rDot;
00276         int             strafeTime;
00277 
00278         if ( self->client->NPC_class != CLASS_BOBAFETT )
00279         {
00280                 return qfalse;
00281         }
00282 
00283         if ( (self->client->ps.eFlags2&EF2_FLYING) )//moveType == MT_FLYSWIM )
00284         {//can't knock me down when I'm flying
00285                 return qtrue;
00286         }
00287 
00288         VectorSet(ang, 0, self->r.currentAngles[YAW], 0);
00289         strafeTime = Q_irand( 1000, 2000 );
00290 
00291         AngleVectors( ang, fwd, right, NULL );
00292         VectorNormalize2( pushDir, pDir );
00293         fDot = DotProduct( pDir, fwd );
00294         rDot = DotProduct( pDir, right );
00295 
00296         if ( Q_irand( 0, 2 ) )
00297         {//flip or roll with it
00298                 usercmd_t       tempCmd;
00299                 if ( fDot >= 0.4f )
00300                 {
00301                         tempCmd.forwardmove = 127;
00302                         TIMER_Set( self, "moveforward", strafeTime );
00303                 }
00304                 else if ( fDot <= -0.4f )
00305                 {
00306                         tempCmd.forwardmove = -127;
00307                         TIMER_Set( self, "moveback", strafeTime );
00308                 }
00309                 else if ( rDot > 0 )
00310                 {
00311                         tempCmd.rightmove = 127;
00312                         TIMER_Set( self, "strafeRight", strafeTime );
00313                         TIMER_Set( self, "strafeLeft", -1 );
00314                 }
00315                 else
00316                 {
00317                         tempCmd.rightmove = -127;
00318                         TIMER_Set( self, "strafeLeft", strafeTime );
00319                         TIMER_Set( self, "strafeRight", -1 );
00320                 }
00321                 G_AddEvent( self, EV_JUMP, 0 );
00322                 if ( !Q_irand( 0, 1 ) )
00323                 {//flip
00324                         self->client->ps.fd.forceJumpCharge = 280;//FIXME: calc this intelligently?
00325                         ForceJump( self, &tempCmd );
00326                 }
00327                 else
00328                 {//roll
00329                         TIMER_Set( self, "duck", strafeTime );
00330                 }
00331                 self->painDebounceTime = 0;//so we do something
00332         }
00333         else if ( !Q_irand( 0, 1 ) && forceKnockdown )
00334         {//resist
00335                 WP_ResistForcePush( self, pusher, qtrue );
00336         }
00337         else
00338         {//fall down
00339                 return qfalse;
00340         }
00341 
00342         return qtrue;
00343 }
00344 
00345 void Boba_FlyStart( gentity_t *self )
00346 {//switch to seeker AI for a while
00347         if ( TIMER_Done( self, "jetRecharge" ) )
00348         {
00349                 self->client->ps.gravity = 0;
00350                 if ( self->NPC )
00351                 {
00352                         self->NPC->aiFlags |= NPCAI_CUSTOM_GRAVITY;
00353                 }
00354                 self->client->ps.eFlags2 |= EF2_FLYING;//moveType = MT_FLYSWIM;
00355                 self->client->jetPackTime = level.time + Q_irand( 3000, 10000 );
00356                 //take-off sound
00357                 G_SoundOnEnt( self, CHAN_ITEM, "sound/boba/jeton.wav" );
00358                 //jet loop sound
00359                 self->s.loopSound = G_SoundIndex( "sound/boba/jethover.wav" );
00360                 if ( self->NPC )
00361                 {
00362                         self->count = Q3_INFINITE; // SEEKER shot ammo count
00363                 }
00364         }
00365 }
00366 
00367 void Boba_FlyStop( gentity_t *self )
00368 {
00369         self->client->ps.gravity = g_gravity.value;
00370         if ( self->NPC )
00371         {
00372                 self->NPC->aiFlags &= ~NPCAI_CUSTOM_GRAVITY;
00373         }
00374         self->client->ps.eFlags2 &= ~EF2_FLYING;
00375         self->client->jetPackTime = 0;
00376         //stop jet loop sound
00377         self->s.loopSound = 0;
00378         if ( self->NPC )
00379         {
00380                 self->count = 0; // SEEKER shot ammo count
00381                 TIMER_Set( self, "jetRecharge", Q_irand( 1000, 5000 ) );
00382                 TIMER_Set( self, "jumpChaseDebounce", Q_irand( 500, 2000 ) );
00383         }
00384 }
00385 
00386 qboolean Boba_Flying( gentity_t *self )
00387 {
00388         return ((qboolean)(self->client->ps.eFlags2&EF2_FLYING));//moveType==MT_FLYSWIM));
00389 }
00390 
00391 void Boba_FireFlameThrower( gentity_t *self )
00392 {
00393         int             damage  = Q_irand( 20, 30 );
00394         trace_t         tr;
00395         gentity_t       *traceEnt = NULL;
00396         mdxaBone_t      boltMatrix;
00397         vec3_t          start, end, dir, traceMins = {-4, -4, -4}, traceMaxs = {4, 4, 4};
00398 
00399         trap_G2API_GetBoltMatrix( self->ghoul2, 0, self->client->renderInfo.handLBolt,
00400                         &boltMatrix, self->r.currentAngles, self->r.currentOrigin, level.time,
00401                         NULL, self->modelScale );
00402 
00403         BG_GiveMeVectorFromMatrix( &boltMatrix, ORIGIN, start );
00404         BG_GiveMeVectorFromMatrix( &boltMatrix, NEGATIVE_Y, dir );
00405         //G_PlayEffect( "boba/fthrw", start, dir );
00406         VectorMA( start, 128, dir, end );
00407 
00408         trap_Trace( &tr, start, traceMins, traceMaxs, end, self->s.number, MASK_SHOT );
00409 
00410         traceEnt = &g_entities[tr.entityNum];
00411         if ( tr.entityNum < ENTITYNUM_WORLD && traceEnt->takedamage )
00412         {
00413                 G_Damage( traceEnt, self, self, dir, tr.endpos, damage, DAMAGE_NO_ARMOR|DAMAGE_NO_KNOCKBACK|/*DAMAGE_NO_HIT_LOC|*/DAMAGE_IGNORE_TEAM, MOD_LAVA );
00414                 //rwwFIXMEFIXME: add DAMAGE_NO_HIT_LOC?
00415         }
00416 }
00417 
00418 //extern void SP_fx_explosion_trail( gentity_t *ent );
00419 void Boba_StartFlameThrower( gentity_t *self )
00420 {
00421         int     flameTime = 4000;//Q_irand( 1000, 3000 );
00422         mdxaBone_t      boltMatrix;
00423         vec3_t          org, dir;
00424 
00425         self->client->ps.torsoTimer = flameTime;//+1000;
00426         if ( self->NPC )
00427         {
00428                 TIMER_Set( self, "nextAttackDelay", flameTime );
00429                 TIMER_Set( self, "walking", 0 );
00430         }
00431         TIMER_Set( self, "flameTime", flameTime );
00432         /*
00433         gentity_t *fire = G_Spawn();
00434         if ( fire != NULL )
00435         {
00436                 mdxaBone_t      boltMatrix;
00437                 vec3_t          org, dir, ang;
00438                 gi.G2API_GetBoltMatrix( NPC->ghoul2, NPC->playerModel, NPC->handRBolt,
00439                                 &boltMatrix, NPC->r.currentAngles, NPC->r.currentOrigin, (cg.time?cg.time:level.time),
00440                                 NULL, NPC->s.modelScale );
00441 
00442                 gi.G2API_GiveMeVectorFromMatrix( boltMatrix, ORIGIN, org );
00443                 gi.G2API_GiveMeVectorFromMatrix( boltMatrix, NEGATIVE_Y, dir );
00444                 vectoangles( dir, ang );
00445 
00446                 VectorCopy( org, fire->s.origin );
00447                 VectorCopy( ang, fire->s.angles );
00448 
00449                 fire->targetname = "bobafire";
00450                 SP_fx_explosion_trail( fire );
00451                 fire->damage = 1;
00452                 fire->radius = 10;
00453                 fire->speed = 200;
00454                 fire->fxID = G_EffectIndex( "boba/fthrw" );//"env/exp_fire_trail" );//"env/small_fire"
00455                 fx_explosion_trail_link( fire );
00456                 fx_explosion_trail_use( fire, NPC, NPC );
00457 
00458         }
00459         */
00460         G_SoundOnEnt( self, CHAN_WEAPON, "sound/effects/combustfire.mp3" );
00461 
00462         trap_G2API_GetBoltMatrix(NPC->ghoul2, 0, NPC->client->renderInfo.handRBolt, &boltMatrix, NPC->r.currentAngles,
00463                 NPC->r.currentOrigin, level.time, NULL, NPC->modelScale);
00464 
00465         BG_GiveMeVectorFromMatrix( &boltMatrix, ORIGIN, org );
00466         BG_GiveMeVectorFromMatrix( &boltMatrix, NEGATIVE_Y, dir );
00467 
00468         G_PlayEffectID( G_EffectIndex("boba/fthrw"), org, dir);
00469 }
00470 
00471 void Boba_DoFlameThrower( gentity_t *self )
00472 {
00473         NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCELIGHTNING_HOLD, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
00474         if ( TIMER_Done( self, "nextAttackDelay" ) && TIMER_Done( self, "flameTime" ) )
00475         {
00476                 Boba_StartFlameThrower( self );
00477         }
00478         Boba_FireFlameThrower( self );
00479 }
00480 
00481 void Boba_FireDecide( void )
00482 {
00483         qboolean enemyLOS = qfalse;
00484         qboolean enemyCS = qfalse;
00485         qboolean enemyInFOV = qfalse;
00486         //qboolean move = qtrue;
00487         qboolean faceEnemy = qfalse;
00488         qboolean shoot = qfalse;
00489         qboolean hitAlly = qfalse;
00490         vec3_t  impactPos;
00491         float   enemyDist;
00492         float   dot;
00493         vec3_t  enemyDir, shootDir;
00494 
00495         if ( NPC->client->ps.groundEntityNum == ENTITYNUM_NONE 
00496                 && NPC->client->ps.fd.forceJumpZStart
00497                 && !BG_FlippingAnim( NPC->client->ps.legsAnim )
00498                 && !Q_irand( 0, 10 ) )
00499         {//take off
00500                 Boba_FlyStart( NPC );
00501         }
00502 
00503         if ( !NPC->enemy )
00504         {
00505                 return;
00506         }
00507 
00508         /*
00509         if ( NPC->enemy->enemy != NPC && NPC->health == NPC->client->pers.maxHealth )
00510         {
00511                 NPCInfo->scriptFlags |= SCF_ALT_FIRE;
00512                 Boba_ChangeWeapon( WP_DISRUPTOR );
00513         }
00514         else */if ( NPC->enemy->s.weapon == WP_SABER )
00515         {
00516                 NPCInfo->scriptFlags &= ~SCF_ALT_FIRE;
00517                 Boba_ChangeWeapon( WP_ROCKET_LAUNCHER );
00518         }
00519         else
00520         {
00521                 if ( NPC->health < NPC->client->pers.maxHealth*0.5f )
00522                 {
00523                         NPCInfo->scriptFlags |= SCF_ALT_FIRE;
00524                         Boba_ChangeWeapon( WP_BLASTER );
00525                         NPCInfo->burstMin = 3;
00526                         NPCInfo->burstMean = 12;
00527                         NPCInfo->burstMax = 20;
00528                         NPCInfo->burstSpacing = Q_irand( 300, 750 );//attack debounce
00529                 }
00530                 else
00531                 {
00532                         NPCInfo->scriptFlags &= ~SCF_ALT_FIRE;
00533                         Boba_ChangeWeapon( WP_BLASTER );
00534                 }
00535         }
00536 
00537         VectorClear( impactPos );
00538         enemyDist = DistanceSquared( NPC->r.currentOrigin, NPC->enemy->r.currentOrigin );
00539 
00540         VectorSubtract( NPC->enemy->r.currentOrigin, NPC->r.currentOrigin, enemyDir );
00541         VectorNormalize( enemyDir );
00542         AngleVectors( NPC->client->ps.viewangles, shootDir, NULL, NULL );
00543         dot = DotProduct( enemyDir, shootDir );
00544         if ( dot > 0.5f ||( enemyDist * (1.0f-dot)) < 10000 )
00545         {//enemy is in front of me or they're very close and not behind me
00546                 enemyInFOV = qtrue;
00547         }
00548 
00549         if ( (enemyDist < (128*128)&&enemyInFOV) || !TIMER_Done( NPC, "flameTime" ) )
00550         {//flamethrower
00551                 Boba_DoFlameThrower( NPC );
00552                 enemyCS = qfalse;
00553                 shoot = qfalse;
00554                 NPCInfo->enemyLastSeenTime = level.time;
00555                 faceEnemy = qtrue;
00556                 ucmd.buttons &= ~(BUTTON_ATTACK|BUTTON_ALT_ATTACK);
00557         }
00558         else if ( enemyDist < MIN_ROCKET_DIST_SQUARED )//128
00559         {//enemy within 128
00560                 if ( (NPC->client->ps.weapon == WP_FLECHETTE || NPC->client->ps.weapon == WP_REPEATER) && 
00561                         (NPCInfo->scriptFlags & SCF_ALT_FIRE) )
00562                 {//shooting an explosive, but enemy too close, switch to primary fire
00563                         NPCInfo->scriptFlags &= ~SCF_ALT_FIRE;
00564                         //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...
00565                 }
00566         }
00567         else if ( enemyDist > 65536 )//256 squared
00568         {
00569                 if ( NPC->client->ps.weapon == WP_DISRUPTOR )
00570                 {//sniping... should be assumed
00571                         if ( !(NPCInfo->scriptFlags&SCF_ALT_FIRE) )
00572                         {//use primary fire
00573                                 NPCInfo->scriptFlags |= SCF_ALT_FIRE;
00574                                 //reset fire-timing variables
00575                                 NPC_ChangeWeapon( WP_DISRUPTOR );
00576                                 NPC_UpdateAngles( qtrue, qtrue );
00577                                 return;
00578                         }
00579                 }
00580         }
00581 
00582         //can we see our target?
00583         if ( TIMER_Done( NPC, "nextAttackDelay" ) && TIMER_Done( NPC, "flameTime" ) )
00584         {
00585                 if ( NPC_ClearLOS4( NPC->enemy ) )
00586                 {
00587                         NPCInfo->enemyLastSeenTime = level.time;
00588                         enemyLOS = qtrue;
00589 
00590                         if ( NPC->client->ps.weapon == WP_NONE )
00591                         {
00592                                 enemyCS = qfalse;//not true, but should stop us from firing
00593                         }
00594                         else
00595                         {//can we shoot our target?
00596                                 if ( (NPC->client->ps.weapon == WP_ROCKET_LAUNCHER || (NPC->client->ps.weapon == WP_FLECHETTE && (NPCInfo->scriptFlags&SCF_ALT_FIRE))) && enemyDist < MIN_ROCKET_DIST_SQUARED )//128*128
00597                                 {
00598                                         enemyCS = qfalse;//not true, but should stop us from firing
00599                                         hitAlly = qtrue;//us!
00600                                         //FIXME: if too close, run away!
00601                                 }
00602                                 else if ( enemyInFOV )
00603                                 {//if enemy is FOV, go ahead and check for shooting
00604                                         int hit = NPC_ShotEntity( NPC->enemy, impactPos );
00605                                         gentity_t *hitEnt = &g_entities[hit];
00606 
00607                                         if ( hit == NPC->enemy->s.number 
00608                                                 || ( hitEnt && hitEnt->client && hitEnt->client->playerTeam == NPC->client->enemyTeam )
00609                                                 || ( hitEnt && hitEnt->takedamage && ((hitEnt->r.svFlags&SVF_GLASS_BRUSH)||hitEnt->health < 40||NPC->s.weapon == WP_EMPLACED_GUN) ) )
00610                                         {//can hit enemy or enemy ally or will hit glass or other minor breakable (or in emplaced gun), so shoot anyway
00611                                                 enemyCS = qtrue;
00612                                                 //NPC_AimAdjust( 2 );//adjust aim better longer we have clear shot at enemy
00613                                                 VectorCopy( NPC->enemy->r.currentOrigin, NPCInfo->enemyLastSeenLocation );
00614                                         }
00615                                         else
00616                                         {//Hmm, have to get around this bastard
00617                                                 //NPC_AimAdjust( 1 );//adjust aim better longer we can see enemy
00618                                                 if ( hitEnt && hitEnt->client && hitEnt->client->playerTeam == NPC->client->playerTeam )
00619                                                 {//would hit an ally, don't fire!!!
00620                                                         hitAlly = qtrue;
00621                                                 }
00622                                                 else
00623                                                 {//Check and see where our shot *would* hit... if it's not close to the enemy (within 256?), then don't fire
00624                                                 }
00625                                         }
00626                                 }
00627                                 else
00628                                 {
00629                                         enemyCS = qfalse;//not true, but should stop us from firing
00630                                 }
00631                         }
00632                 }
00633                 else if ( trap_InPVS( NPC->enemy->r.currentOrigin, NPC->r.currentOrigin ) )
00634                 {
00635                         NPCInfo->enemyLastSeenTime = level.time;
00636                         faceEnemy = qtrue;
00637                         //NPC_AimAdjust( -1 );//adjust aim worse longer we cannot see enemy
00638                 }
00639 
00640                 if ( NPC->client->ps.weapon == WP_NONE )
00641                 {
00642                         faceEnemy = qfalse;
00643                         shoot = qfalse;
00644                 }
00645                 else
00646                 {
00647                         if ( enemyLOS )
00648                         {//FIXME: no need to face enemy if we're moving to some other goal and he's too far away to shoot?
00649                                 faceEnemy = qtrue;
00650                         }
00651                         if ( enemyCS )
00652                         {
00653                                 shoot = qtrue;
00654                         }
00655                 }
00656 
00657                 if ( !enemyCS )
00658                 {//if have a clear shot, always try
00659                         //See if we should continue to fire on their last position
00661                         if ( !hitAlly //we're not going to hit an ally
00662                                 && enemyInFOV //enemy is in our FOV //FIXME: or we don't have a clear LOS?
00663                                 && NPCInfo->enemyLastSeenTime > 0 )//we've seen the enemy
00664                         {
00665                                 if ( level.time - NPCInfo->enemyLastSeenTime < 10000 )//we have seem the enemy in the last 10 seconds
00666                                 {
00667                                         if ( !Q_irand( 0, 10 ) )
00668                                         {
00669                                                 //Fire on the last known position
00670                                                 vec3_t  muzzle, dir, angles;
00671                                                 qboolean tooClose = qfalse;
00672                                                 qboolean tooFar = qfalse;
00673                                                 float distThreshold;
00674                                                 float dist;
00675 
00676                                                 CalcEntitySpot( NPC, SPOT_HEAD, muzzle );
00677                                                 if ( VectorCompare( impactPos, vec3_origin ) )
00678                                                 {//never checked ShotEntity this frame, so must do a trace...
00679                                                         trace_t tr;
00680                                                         //vec3_t        mins = {-2,-2,-2}, maxs = {2,2,2};
00681                                                         vec3_t  forward, end;
00682                                                         AngleVectors( NPC->client->ps.viewangles, forward, NULL, NULL );
00683                                                         VectorMA( muzzle, 8192, forward, end );
00684                                                         trap_Trace( &tr, muzzle, vec3_origin, vec3_origin, end, NPC->s.number, MASK_SHOT );
00685                                                         VectorCopy( tr.endpos, impactPos );
00686                                                 }
00687 
00688                                                 //see if impact would be too close to me
00689                                                 distThreshold = 16384/*128*128*/;//default
00690                                                 switch ( NPC->s.weapon )
00691                                                 {
00692                                                 case WP_ROCKET_LAUNCHER:
00693                                                 case WP_FLECHETTE:
00694                                                 case WP_THERMAL:
00695                                                 case WP_TRIP_MINE:
00696                                                 case WP_DET_PACK:
00697                                                         distThreshold = 65536/*256*256*/;
00698                                                         break;
00699                                                 case WP_REPEATER:
00700                                                         if ( NPCInfo->scriptFlags&SCF_ALT_FIRE )
00701                                                         {
00702                                                                 distThreshold = 65536/*256*256*/;
00703                                                         }
00704                                                         break;
00705                                                 default:
00706                                                         break;
00707                                                 }
00708 
00709                                                 dist = DistanceSquared( impactPos, muzzle );
00710 
00711                                                 if ( dist < distThreshold )
00712                                                 {//impact would be too close to me
00713                                                         tooClose = qtrue;
00714                                                 }
00715                                                 else if ( level.time - NPCInfo->enemyLastSeenTime > 5000 ||
00716                                                         (NPCInfo->group && level.time - NPCInfo->group->lastSeenEnemyTime > 5000 ))
00717                                                 {//we've haven't seen them in the last 5 seconds
00718                                                         //see if it's too far from where he is
00719                                                         distThreshold = 65536/*256*256*/;//default
00720                                                         switch ( NPC->s.weapon )
00721                                                         {
00722                                                         case WP_ROCKET_LAUNCHER:
00723                                                         case WP_FLECHETTE:
00724                                                         case WP_THERMAL:
00725                                                         case WP_TRIP_MINE:
00726                                                         case WP_DET_PACK:
00727                                                                 distThreshold = 262144/*512*512*/;
00728                                                                 break;
00729                                                         case WP_REPEATER:
00730                                                                 if ( NPCInfo->scriptFlags&SCF_ALT_FIRE )
00731                                                                 {
00732                                                                         distThreshold = 262144/*512*512*/;
00733                                                                 }
00734                                                                 break;
00735                                                         default:
00736                                                                 break;
00737                                                         }
00738                                                         dist = DistanceSquared( impactPos, NPCInfo->enemyLastSeenLocation );
00739                                                         if ( dist > distThreshold )
00740                                                         {//impact would be too far from enemy
00741                                                                 tooFar = qtrue;
00742                                                         }
00743                                                 }
00744 
00745                                                 if ( !tooClose && !tooFar )
00746                                                 {//okay too shoot at last pos
00747                                                         VectorSubtract( NPCInfo->enemyLastSeenLocation, muzzle, dir );
00748                                                         VectorNormalize( dir );
00749                                                         vectoangles( dir, angles );
00750 
00751                                                         NPCInfo->desiredYaw             = angles[YAW];
00752                                                         NPCInfo->desiredPitch   = angles[PITCH];
00753 
00754                                                         shoot = qtrue;
00755                                                         faceEnemy = qfalse;
00756                                                 }
00757                                         }
00758                                 }
00759                         }
00760                 }
00761 
00762                 //FIXME: don't shoot right away!
00763                 if ( NPC->client->ps.weaponTime > 0 )
00764                 {
00765                         if ( NPC->s.weapon == WP_ROCKET_LAUNCHER )
00766                         {
00767                                 if ( !enemyLOS || !enemyCS )
00768                                 {//cancel it
00769                                         NPC->client->ps.weaponTime = 0;
00770                                 }
00771                                 else
00772                                 {//delay our next attempt
00773                                         TIMER_Set( NPC, "nextAttackDelay", Q_irand( 500, 1000 ) );
00774                                 }
00775                         }
00776                 }
00777                 else if ( shoot )
00778                 {//try to shoot if it's time
00779                         if ( TIMER_Done( NPC, "nextAttackDelay" ) )
00780                         {
00781                                 if( !(NPCInfo->scriptFlags & SCF_FIRE_WEAPON) ) // we've already fired, no need to do it again here
00782                                 {
00783                                         WeaponThink( qtrue );
00784                                 }
00785                                 //NASTY
00786                                 if ( NPC->s.weapon == WP_ROCKET_LAUNCHER 
00787                                         && (ucmd.buttons&BUTTON_ATTACK) 
00788                                         && !Q_irand( 0, 3 ) )
00789                                 {//every now and then, shoot a homing rocket
00790                                         ucmd.buttons &= ~BUTTON_ATTACK;
00791                                         ucmd.buttons |= BUTTON_ALT_ATTACK;
00792                                         NPC->client->ps.weaponTime = Q_irand( 500, 1500 );
00793                                 }
00794                         }
00795                 }
00796         }
00797 }
00798 
00799 void Jedi_Cloak( gentity_t *self )
00800 {
00801         if ( self )
00802         {
00803                 self->flags |= FL_NOTARGET;
00804                 if ( self->client )
00805                 {
00806                         if ( !self->client->ps.powerups[PW_CLOAKED] )
00807                         {//cloak
00808                                 self->client->ps.powerups[PW_CLOAKED] = Q3_INFINITE;
00809 
00810                                 //FIXME: debounce attacks?
00811                                 //FIXME: temp sound
00812                                 G_Sound( self, CHAN_ITEM, G_SoundIndex("sound/chars/shadowtrooper/cloak.wav") );
00813                         }
00814                 }
00815         }
00816 }
00817 
00818 void Jedi_Decloak( gentity_t *self )
00819 {
00820         if ( self )
00821         {
00822                 self->flags &= ~FL_NOTARGET;
00823                 if ( self->client )
00824                 {
00825                         if ( self->client->ps.powerups[PW_CLOAKED] )
00826                         {//Uncloak
00827                                 self->client->ps.powerups[PW_CLOAKED] = 0;
00828 
00829                                 G_Sound( self, CHAN_ITEM, G_SoundIndex("sound/chars/shadowtrooper/decloak.wav") );
00830                         }
00831                 }
00832         }
00833 }
00834 
00835 void Jedi_CheckCloak( void )
00836 {
00837         if ( NPC && NPC->client && NPC->client->NPC_class == CLASS_SHADOWTROOPER )
00838         {
00839                 if ( !NPC->client->ps.saberHolstered ||
00840                         NPC->health <= 0 || 
00841                         NPC->client->ps.saberInFlight ||
00842                 //      (NPC->client->ps.eFlags&EF_FORCE_GRIPPED) ||
00843                 //      (NPC->client->ps.eFlags&EF_FORCE_DRAINED) ||
00844                         NPC->painDebounceTime > level.time )
00845                 {//can't be cloaked if saber is on, or dead or saber in flight or taking pain or being gripped
00846                         Jedi_Decloak( NPC );
00847                 }
00848                 else if ( NPC->health > 0 
00849                         && !NPC->client->ps.saberInFlight 
00850                 //      && !(NPC->client->ps.eFlags&EF_FORCE_GRIPPED) 
00851                 //      && !(NPC->client->ps.eFlags&EF_FORCE_DRAINED) 
00852                         && NPC->painDebounceTime < level.time )
00853                 {//still alive, have saber in hand, not taking pain and not being gripped
00854                         Jedi_Cloak( NPC );
00855                 }
00856         }
00857 }
00858 /*
00859 ==========================================================================================
00860 AGGRESSION
00861 ==========================================================================================
00862 */
00863 static void Jedi_Aggression( gentity_t *self, int change )
00864 {
00865         int     upper_threshold, lower_threshold;
00866 
00867         self->NPC->stats.aggression += change;
00868         
00869         //FIXME: base this on initial NPC stats
00870         if ( self->client->playerTeam == NPCTEAM_PLAYER )
00871         {//good guys are less aggressive
00872                 upper_threshold = 7;
00873                 lower_threshold = 1;
00874         }
00875         else
00876         {//bad guys are more aggressive
00877                 if ( self->client->NPC_class == CLASS_DESANN )
00878                 {
00879                         upper_threshold = 20;
00880                         lower_threshold = 5;
00881                 }
00882                 else
00883                 {
00884                         upper_threshold = 10;
00885                         lower_threshold = 3;
00886                 }
00887         }
00888 
00889         if ( self->NPC->stats.aggression > upper_threshold )
00890         {
00891                 self->NPC->stats.aggression = upper_threshold;
00892         }
00893         else if ( self->NPC->stats.aggression < lower_threshold )
00894         {
00895                 self->NPC->stats.aggression = lower_threshold;
00896         }
00897         //Com_Printf( "(%d) %s agg %d change: %d\n", level.time, self->NPC_type, self->NPC->stats.aggression, change );
00898 }
00899 
00900 static void Jedi_AggressionErosion( int amt )
00901 {
00902         if ( TIMER_Done( NPC, "roamTime" ) )
00903         {//the longer we're not alerted and have no enemy, the more our aggression goes down
00904                 TIMER_Set( NPC, "roamTime", Q_irand( 2000, 5000 ) );
00905                 Jedi_Aggression( NPC, amt );
00906         }
00907         
00908         if ( NPCInfo->stats.aggression < 4 || (NPCInfo->stats.aggression < 6&&NPC->client->NPC_class == CLASS_DESANN))
00909         {//turn off the saber
00910                 WP_DeactivateSaber( NPC, qfalse );
00911         }
00912 }
00913 
00914 void NPC_Jedi_RateNewEnemy( gentity_t *self, gentity_t *enemy )
00915 {
00916         float healthAggression;
00917         float weaponAggression;
00918         int newAggression;
00919 
00920         switch( enemy->s.weapon )
00921         {
00922         case WP_SABER:
00923                 healthAggression = (float)self->health/200.0f*6.0f;
00924                 weaponAggression = 7;//go after him
00925                 break;
00926         case WP_BLASTER:
00927                 if ( DistanceSquared( self->r.currentOrigin, enemy->r.currentOrigin ) < 65536 )//256 squared
00928                 {
00929                         healthAggression = (float)self->health/200.0f*8.0f;
00930                         weaponAggression = 8;//go after him
00931                 }
00932                 else
00933                 {
00934                         healthAggression = 8.0f - ((float)self->health/200.0f*8.0f);
00935                         weaponAggression = 2;//hang back for a second
00936                 }
00937                 break;
00938         default:
00939                 healthAggression = (float)self->health/200.0f*8.0f;
00940                 weaponAggression = 6;//approach
00941                 break;
00942         }
00943         //Average these with current aggression
00944         newAggression = ceil( (healthAggression + weaponAggression + (float)self->NPC->stats.aggression )/3.0f);
00945         //Com_Printf( "(%d) new agg %d - new enemy\n", level.time, newAggression );
00946         Jedi_Aggression( self, newAggression - self->NPC->stats.aggression );
00947 
00948         //don't taunt right away
00949         TIMER_Set( self, "chatter", Q_irand( 4000, 7000 ) );
00950 }
00951 
00952 static void Jedi_Rage( void )
00953 {
00954         Jedi_Aggression( NPC, 10 - NPCInfo->stats.aggression + Q_irand( -2, 2 ) );
00955         TIMER_Set( NPC, "roamTime", 0 );
00956         TIMER_Set( NPC, "chatter", 0 );
00957         TIMER_Set( NPC, "walking", 0 );
00958         TIMER_Set( NPC, "taunting", 0 );
00959         TIMER_Set( NPC, "jumpChaseDebounce", 0 );
00960         TIMER_Set( NPC, "movenone", 0 );
00961         TIMER_Set( NPC, "movecenter", 0 );
00962         TIMER_Set( NPC, "noturn", 0 );
00963         ForceRage( NPC );
00964 }
00965 
00966 void Jedi_RageStop( gentity_t *self )
00967 {
00968         if ( self->NPC )
00969         {//calm down and back off
00970                 TIMER_Set( self, "roamTime", 0 );
00971                 Jedi_Aggression( self, Q_irand( -5, 0 ) );
00972         }
00973 }
00974 /*
00975 ==========================================================================================
00976 SPEAKING
00977 ==========================================================================================
00978 */
00979 
00980 static qboolean Jedi_BattleTaunt( void )
00981 {
00982         if ( TIMER_Done( NPC, "chatter" ) 
00983                 && !Q_irand( 0, 3 ) 
00984                 && NPCInfo->blockedSpeechDebounceTime < level.time 
00985                 && jediSpeechDebounceTime[NPC->client->playerTeam] < level.time )
00986         {
00987                 int event = -1;
00988                 if ( NPC->client->playerTeam == NPCTEAM_PLAYER 
00989                         && NPC->enemy && NPC->enemy->client && NPC->enemy->client->NPC_class == CLASS_JEDI )
00990                 {//a jedi fighting a jedi - training
00991                         if ( NPC->client->NPC_class ==