1 /*
2 ===========================================================================
3 Copyright (C) 2000 - 2013, Raven Software, Inc.
4 Copyright (C) 2001 - 2013, Activision, Inc.
5 Copyright (C) 2013 - 2015, OpenJK contributors
6
7 This file is part of the OpenJK source code.
8
9 OpenJK is free software; you can redistribute it and/or modify it
10 under the terms of the GNU General Public License version 2 as
11 published by the Free Software Foundation.
12
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, see <http://www.gnu.org/licenses/>.
20 ===========================================================================
21 */
22
23 ////////////////////////////////////////////////////////////////////////////////////////
24 // RAVEN SOFTWARE - STAR WARS: JK II
25 // (c) 2002 Activision
26 //
27 // Boba Fett
28 // ---------
29 // Ah yes, this file is pretty messy. I've tried to move everything in here, but in fact
30 // a lot of his AI occurs in the seeker and jedi AI files. Some of these functions
31 //
32 //
33 //
34 ////////////////////////////////////////////////////////////////////////////////////////
35 #include "b_local.h"
36 #include "../Ravl/CVec.h"
37
38
39 ////////////////////////////////////////////////////////////////////////////////////////
40 // Forward References Of Functions
41 ////////////////////////////////////////////////////////////////////////////////////////
42 void Boba_Precache( void );
43 void Boba_DustFallNear(const vec3_t origin, int dustcount);
44 void Boba_ChangeWeapon(int wp);
45 qboolean Boba_StopKnockdown(gentity_t *self, gentity_t *pusher, const vec3_t pushDir, qboolean forceKnockdown = qfalse);
46
47 // Flight Related Functions (also used by Rocket Trooper)
48 //--------------------------------------------------------
49 qboolean Boba_Flying( gentity_t *self );
50 void Boba_FlyStart( gentity_t *self );
51 void Boba_FlyStop( gentity_t *self );
52
53 // Called From NPC_Pain()
54 //-----------------------------
55 void Boba_Pain( gentity_t *self, gentity_t *inflictor, int damage, int mod);
56
57
58 // Local: Flame Thrower Weapon
59 //-----------------------------
60 void Boba_FireFlameThrower( gentity_t *self );
61 void Boba_StopFlameThrower( gentity_t *self );
62 void Boba_StartFlameThrower( gentity_t *self );
63 void Boba_DoFlameThrower( gentity_t *self );
64
65 // Local: Other Tactics
66 //----------------------
67 void Boba_DoAmbushWait( gentity_t *self);
68 void Boba_DoSniper( gentity_t *self);
69
70 // Local: Respawning
71 //-------------------
72 bool Boba_Respawn();
73
74 // Called From Within AI_Jedi && AI_Seeker
75 //-----------------------------------------
76 void Boba_Fire();
77 void Boba_FireDecide();
78
79 // Local: Called From Tactics()
80 //----------------------------
81 void Boba_TacticsSelect();
82 bool Boba_CanSeeEnemy( gentity_t *self );
83
84
85 // Called From NPC_RunBehavior()
86 //-------------------------------
87 void Boba_Update(); // Always Called First, Before Any Other Thinking
88 bool Boba_Tactics(); // If returns true, Jedi and Seeker AI not used
89 bool Boba_Flee(); // If returns true, Jedi and Seeker AI not used
90
91
92
93 ////////////////////////////////////////////////////////////////////////////////////////
94 // External Functions
95 ////////////////////////////////////////////////////////////////////////////////////////
96 extern void G_SoundAtSpot( vec3_t org, int soundIndex, qboolean broadcast );
97 extern void G_CreateG2AttachedWeaponModel( gentity_t *ent, const char *weaponModel, int boltNum, int weaponNum );
98 extern void ChangeWeapon( gentity_t *ent, int newWeapon );
99 extern void WP_ResistForcePush( gentity_t *self, gentity_t *pusher, qboolean noPenalty );
100 extern void ForceJump( gentity_t *self, usercmd_t *ucmd );
101 extern void G_Knockdown( gentity_t *self, gentity_t *attacker, const vec3_t pushDir, float strength, qboolean breakSaberLock );
102
103 extern void CG_DrawEdge( vec3_t start, vec3_t end, int type );
104
105 ////////////////////////////////////////////////////////////////////////////////////////
106 // External Data
107 ////////////////////////////////////////////////////////////////////////////////////////
108 extern cvar_t* g_bobaDebug;
109
110
111
112 ////////////////////////////////////////////////////////////////////////////////////////
113 // Boba Debug Output
114 ////////////////////////////////////////////////////////////////////////////////////////
115 #ifndef FINAL_BUILD
116 #if !defined(CTYPE_H_INC)
117 #include <ctype.h>
118 #define CTYPE_H_INC
119 #endif
120
121 #if !defined(STDARG_H_INC)
122 #include <stdarg.h>
123 #define STDARG_H_INC
124 #endif
125
126 #if !defined(STDIO_H_INC)
127 #include <stdio.h>
128 #define STDIO_H_INC
129 #endif
Boba_Printf(const char * format,...)130 void Boba_Printf(const char * format, ...)
131 {
132 if (g_bobaDebug->integer==0)
133 {
134 return;
135 }
136
137 static char string[2][1024]; // in case this is called by nested functions
138 static int index = 0;
139 static char nFormat[300];
140 char* buf;
141
142 // Tack On The Standard Format Around The Given Format
143 //-----------------------------------------------------
144 Com_sprintf(nFormat, sizeof(nFormat), "[BOBA %8d] %s\n", level.time, format);
145
146
147 // Resolve Remaining Elipsis Parameters Into Newly Formated String
148 //-----------------------------------------------------------------
149 buf = string[index & 1];
150 index++;
151
152 va_list argptr;
153 va_start (argptr, format);
154 Q_vsnprintf (buf, sizeof(*string), nFormat, argptr);
155 va_end (argptr);
156
157 // Print It To Debug Output Console
158 //----------------------------------
159 gi.Printf(buf);
160 }
161 #else
Boba_Printf(const char * format,...)162 void Boba_Printf(const char * format, ...)
163 {
164 }
165 #endif
166
167
168 ////////////////////////////////////////////////////////////////////////////////////////
169 // Defines
170 ////////////////////////////////////////////////////////////////////////////////////////
171 #define BOBA_FLAMEDURATION 3000
172 #define BOBA_FLAMETHROWRANGE 128
173 #define BOBA_FLAMETHROWSIZE 40
174 #define BOBA_FLAMETHROWDAMAGEMIN 1//10
175 #define BOBA_FLAMETHROWDAMAGEMAX 5//40
176 #define BOBA_ROCKETRANGEMIN 300
177 #define BOBA_ROCKETRANGEMAX 2000
178
179
180 ////////////////////////////////////////////////////////////////////////////////////////
181 // Global Data
182 ////////////////////////////////////////////////////////////////////////////////////////
183 bool BobaHadDeathScript = false;
184 bool BobaActive = false;
185 vec3_t BobaFootStepLoc;
186 int BobaFootStepCount = 0;
187
188 vec3_t AverageEnemyDirection;
189 int AverageEnemyDirectionSamples;
190
191
192 ////////////////////////////////////////////////////////////////////////////////////////
193 // Enums
194 ////////////////////////////////////////////////////////////////////////////////////////
195 enum EBobaTacticsState
196 {
197 BTS_NONE,
198
199 // Attack
200 //--------
201 BTS_RIFLE, // Uses Jedi / Seeker Movement
202 BTS_MISSILE, // Uses Jedi / Seeker Movement
203 BTS_SNIPER, // Uses Special Movement Internal To This File
204 BTS_FLAMETHROW, // Locked In Place
205
206 // Waiting
207 //---------
208 BTS_AMBUSHWAIT, // Goto CP & Wait
209
210 BTS_MAX
211 };
212
213
214
215
216
217
218
219
220
221 ////////////////////////////////////////////////////////////////////////////////////////
222 //
223 ////////////////////////////////////////////////////////////////////////////////////////
Boba_Precache(void)224 void Boba_Precache( void )
225 {
226 G_SoundIndex( "sound/chars/boba/bf_blast-off.wav" );
227 G_SoundIndex( "sound/chars/boba/bf_jetpack_lp.wav" );
228 G_SoundIndex( "sound/chars/boba/bf_land.wav" );
229 G_SoundIndex( "sound/weapons/boba/bf_flame.mp3" );
230 G_SoundIndex( "sound/player/footsteps/boot1" );
231 G_SoundIndex( "sound/player/footsteps/boot2" );
232 G_SoundIndex( "sound/player/footsteps/boot3" );
233 G_SoundIndex( "sound/player/footsteps/boot4" );
234 G_EffectIndex( "boba/jetSP" );
235 G_EffectIndex( "boba/fthrw" );
236 G_EffectIndex( "volumetric/black_smoke" );
237 G_EffectIndex( "chunks/dustFall" );
238
239 AverageEnemyDirectionSamples = 0;
240 VectorClear(AverageEnemyDirection);
241 BobaHadDeathScript = false;
242 BobaActive = true;
243 BobaFootStepCount = 0;
244 }
245
246 ////////////////////////////////////////////////////////////////////////////////////////
247 //
248 ////////////////////////////////////////////////////////////////////////////////////////
Boba_DustFallNear(const vec3_t origin,int dustcount)249 void Boba_DustFallNear(const vec3_t origin, int dustcount)
250 {
251 if (!BobaActive)
252 {
253 return;
254 }
255
256 trace_t testTrace;
257 vec3_t testDirection;
258 vec3_t testStartPos;
259 vec3_t testEndPos;
260
261 VectorCopy(origin, testStartPos);
262 for (int i=0; i<dustcount; i++)
263 {
264 testDirection[0] = (Q_flrand(0.0f, 1.0f) * 2.0f) - 1.0f;
265 testDirection[1] = (Q_flrand(0.0f, 1.0f) * 2.0f) - 1.0f;
266 testDirection[2] = 1.0f;
267
268 VectorMA(origin, 1000.0f, testDirection, testEndPos);
269 gi.trace (&testTrace, origin, NULL, NULL, testEndPos, (player && player->inuse)?(0):(ENTITYNUM_NONE), MASK_SHOT, (EG2_Collision)0, 0 );
270
271 if (!testTrace.startsolid &&
272 !testTrace.allsolid &&
273 testTrace.fraction>0.1f &&
274 testTrace.fraction<0.9f)
275 {
276 G_PlayEffect( "chunks/dustFall", testTrace.endpos, testTrace.plane.normal );
277 }
278 }
279 }
280
281 ////////////////////////////////////////////////////////////////////////////////////////
282 // This is just a super silly wrapper around NPC_Change Weapon
283 ////////////////////////////////////////////////////////////////////////////////////////
Boba_ChangeWeapon(int wp)284 void Boba_ChangeWeapon( int wp )
285 {
286 if ( NPC->s.weapon == wp )
287 {
288 return;
289 }
290 NPC_ChangeWeapon( wp );
291 G_AddEvent( NPC, EV_GENERAL_SOUND, G_SoundIndex( "sound/weapons/change.wav" ));
292 }
293
294 ////////////////////////////////////////////////////////////////////////////////////////
295 // Choose an "anti-knockdown" response
296 ////////////////////////////////////////////////////////////////////////////////////////
Boba_StopKnockdown(gentity_t * self,gentity_t * pusher,const vec3_t pushDir,qboolean forceKnockdown)297 qboolean Boba_StopKnockdown( gentity_t *self, gentity_t *pusher, const vec3_t pushDir, qboolean forceKnockdown )
298 {
299 if ( self->client->NPC_class != CLASS_BOBAFETT )
300 {
301 return qfalse;
302 }
303
304 if ( self->client->moveType == MT_FLYSWIM )
305 {//can't knock me down when I'm flying
306 return qtrue;
307 }
308
309 vec3_t pDir, fwd, right, ang = {0, self->currentAngles[YAW], 0};
310 float fDot, rDot;
311 int strafeTime = Q_irand( 1000, 2000 );
312
313 AngleVectors( ang, fwd, right, NULL );
314 VectorNormalize2( pushDir, pDir );
315 fDot = DotProduct( pDir, fwd );
316 rDot = DotProduct( pDir, right );
317
318 if ( Q_irand( 0, 2 ) )
319 {//flip or roll with it
320 usercmd_t tempCmd;
321 if ( fDot >= 0.4f )
322 {
323 tempCmd.forwardmove = 127;
324 TIMER_Set( self, "moveforward", strafeTime );
325 }
326 else if ( fDot <= -0.4f )
327 {
328 tempCmd.forwardmove = -127;
329 TIMER_Set( self, "moveback", strafeTime );
330 }
331 else if ( rDot > 0 )
332 {
333 tempCmd.rightmove = 127;
334 TIMER_Set( self, "strafeRight", strafeTime );
335 TIMER_Set( self, "strafeLeft", -1 );
336 }
337 else
338 {
339 tempCmd.rightmove = -127;
340 TIMER_Set( self, "strafeLeft", strafeTime );
341 TIMER_Set( self, "strafeRight", -1 );
342 }
343 G_AddEvent( self, EV_JUMP, 0 );
344 if ( !Q_irand( 0, 1 ) )
345 {//flip
346 self->client->ps.forceJumpCharge = 280;//FIXME: calc this intelligently?
347 ForceJump( self, &tempCmd );
348 }
349 else
350 {//roll
351 TIMER_Set( self, "duck", strafeTime );
352 }
353 self->painDebounceTime = 0;//so we do something
354 }
355 else if ( !Q_irand( 0, 1 ) && forceKnockdown )
356 {//resist
357 WP_ResistForcePush( self, pusher, qtrue );
358 }
359 else
360 {//fall down
361 return qfalse;
362 }
363
364 return qtrue;
365 }
366
367 ////////////////////////////////////////////////////////////////////////////////////////
368 // Is this entity flying
369 ////////////////////////////////////////////////////////////////////////////////////////
Boba_Flying(gentity_t * self)370 qboolean Boba_Flying( gentity_t *self )
371 {
372 assert(self && self->client && self->client->NPC_class==CLASS_BOBAFETT);//self->NPC &&
373 return ((qboolean)(self->client->moveType==MT_FLYSWIM));
374 }
375
376 ////////////////////////////////////////////////////////////////////////////////////////
377 //
378 ////////////////////////////////////////////////////////////////////////////////////////
Boba_CanSeeEnemy(gentity_t * self)379 bool Boba_CanSeeEnemy( gentity_t *self )
380 {
381 assert(self && self->NPC && self->client && self->client->NPC_class==CLASS_BOBAFETT);
382 return ((level.time - self->NPC->enemyLastSeenTime)<1000);
383 }
384
385 ////////////////////////////////////////////////////////////////////////////////////////
386 //
387 ////////////////////////////////////////////////////////////////////////////////////////
Boba_Pain(gentity_t * self,gentity_t * inflictor,int damage,int mod)388 void Boba_Pain( gentity_t *self, gentity_t *inflictor, int damage, int mod)
389 {
390 if (mod==MOD_SABER && !(NPCInfo->aiFlags&NPCAI_FLAMETHROW))
391 {
392 TIMER_Set( self, "Boba_TacticsSelect", 0); // Hurt By The Saber, Time To Try Something New
393 }
394 if (self->NPC->aiFlags&NPCAI_FLAMETHROW)
395 {
396 NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCELIGHTNING_HOLD, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
397 self->client->ps.torsoAnimTimer = level.time - TIMER_Get(self, "falmeTime");
398 }
399 }
400
401 ////////////////////////////////////////////////////////////////////////////////////////
402 //
403 ////////////////////////////////////////////////////////////////////////////////////////
Boba_FlyStart(gentity_t * self)404 void Boba_FlyStart( gentity_t *self )
405 {//switch to seeker AI for a while
406 if ( TIMER_Done( self, "jetRecharge" )
407 && !Boba_Flying( self ) )
408 {
409 self->client->ps.gravity = 0;
410 self->svFlags |= SVF_CUSTOM_GRAVITY;
411 self->client->moveType = MT_FLYSWIM;
412 //start jet effect
413 self->client->jetPackTime = level.time + Q_irand( 3000, 10000 );
414 if ( self->genericBolt1 != -1 )
415 {
416 G_PlayEffect( G_EffectIndex( "boba/jetSP" ), self->playerModel, self->genericBolt1, self->s.number, self->currentOrigin, qtrue, qtrue );
417 }
418 if ( self->genericBolt2 != -1 )
419 {
420 G_PlayEffect( G_EffectIndex( "boba/jetSP" ), self->playerModel, self->genericBolt2, self->s.number, self->currentOrigin, qtrue, qtrue );
421 }
422
423 //take-off sound
424 G_SoundOnEnt( self, CHAN_ITEM, "sound/chars/boba/bf_blast-off.wav" );
425 //jet loop sound
426 self->s.loopSound = G_SoundIndex( "sound/chars/boba/bf_jetpack_lp.wav" );
427 if ( self->NPC )
428 {
429 self->count = Q3_INFINITE; // SEEKER shot ammo count
430 }
431 }
432 }
433
434 ////////////////////////////////////////////////////////////////////////////////////////
435 //
436 ////////////////////////////////////////////////////////////////////////////////////////
Boba_FlyStop(gentity_t * self)437 void Boba_FlyStop( gentity_t *self )
438 {
439 self->client->ps.gravity = g_gravity->value;
440 self->svFlags &= ~SVF_CUSTOM_GRAVITY;
441 self->client->moveType = MT_RUNJUMP;
442 //Stop effect
443 self->client->jetPackTime = 0;
444 if ( self->genericBolt1 != -1 )
445 {
446 G_StopEffect( "boba/jetSP", self->playerModel, self->genericBolt1, self->s.number );
447 }
448 if ( self->genericBolt2 != -1 )
449 {
450 G_StopEffect( "boba/jetSP", self->playerModel, self->genericBolt2, self->s.number );
451 }
452
453 //stop jet loop sound
454 G_SoundOnEnt( self, CHAN_ITEM, "sound/chars/boba/bf_land.wav" );
455
456 self->s.loopSound = 0;
457 if ( self->NPC )
458 {
459 self->count = 0; // SEEKER shot ammo count
460 TIMER_Set( self, "jetRecharge", Q_irand( 1000, 5000 ) );
461 TIMER_Set( self, "jumpChaseDebounce", Q_irand( 500, 2000 ) );
462 }
463 }
464
465 ////////////////////////////////////////////////////////////////////////////////////////
466 // This func actually does the damage inflicting traces
467 ////////////////////////////////////////////////////////////////////////////////////////
Boba_FireFlameThrower(gentity_t * self)468 void Boba_FireFlameThrower( gentity_t *self )
469 {
470 trace_t tr;
471 vec3_t start, end, dir;
472 CVec3 traceMins(self->mins);
473 CVec3 traceMaxs(self->maxs);
474 gentity_t* traceEnt = NULL;
475 int damage = Q_irand( BOBA_FLAMETHROWDAMAGEMIN, BOBA_FLAMETHROWDAMAGEMAX );
476
477 AngleVectors(self->currentAngles, dir, 0, 0);
478 dir[2] = 0.0f;
479 VectorCopy(self->currentOrigin, start);
480 traceMins *= 0.5f;
481 traceMaxs *= 0.5f;
482 start[2] += 40.0f;
483
484 VectorMA( start, 150.0f, dir, end );
485
486 if (g_bobaDebug->integer)
487 {
488 CG_DrawEdge(start, end, EDGE_IMPACT_POSSIBLE);
489 }
490 gi.trace( &tr, start, self->mins, self->maxs, end, self->s.number, MASK_SHOT, (EG2_Collision)0, 0);
491
492 traceEnt = &g_entities[tr.entityNum];
493 if ( tr.entityNum < ENTITYNUM_WORLD && traceEnt->takedamage )
494 {
495 G_Damage( traceEnt, self, self, dir, tr.endpos, damage, DAMAGE_NO_ARMOR|DAMAGE_NO_KNOCKBACK|DAMAGE_NO_HIT_LOC|DAMAGE_IGNORE_TEAM, MOD_LAVA, HL_NONE );
496 if (traceEnt->health>0)
497 {
498 // G_Knockdown( traceEnt, self, dir, Q_irand(200, 330), qfalse);
499 G_Throw(traceEnt, dir, 30);
500 }
501 }
502 }
503
504 ////////////////////////////////////////////////////////////////////////////////////////
505 //
506 ////////////////////////////////////////////////////////////////////////////////////////
Boba_StopFlameThrower(gentity_t * self)507 void Boba_StopFlameThrower( gentity_t *self )
508 {
509 if ( self->s.number < MAX_CLIENTS )
510 {
511 self->client->ps.torsoAnimTimer = 0;
512 G_StopEffect( G_EffectIndex("boba/fthrw"), self->playerModel, self->genericBolt3, self->s.number);
513 return;
514 }
515 if ((NPCInfo->aiFlags&NPCAI_FLAMETHROW))
516 {
517 self->NPC->aiFlags &= ~NPCAI_FLAMETHROW;
518 self->client->ps.torsoAnimTimer = 0;
519
520 TIMER_Set( self, "flameTime", 0);
521 TIMER_Set( self, "nextAttackDelay", 0);
522 TIMER_Set( self, "Boba_TacticsSelect", 0);
523
524 // G_SoundOnEnt( self, CHAN_WEAPON, "sound/effects/flameoff.mp3" );
525 G_StopEffect( G_EffectIndex("boba/fthrw"), self->playerModel, self->genericBolt3, self->s.number);
526
527 Boba_Printf("FlameThrower OFF");
528 }
529 }
530
531 ////////////////////////////////////////////////////////////////////////////////////////
532 //
533 ////////////////////////////////////////////////////////////////////////////////////////
Boba_StartFlameThrower(gentity_t * self)534 void Boba_StartFlameThrower( gentity_t *self )
535 {
536 if (!(NPCInfo->aiFlags&NPCAI_FLAMETHROW))
537 {
538 NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCELIGHTNING_HOLD, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
539
540 self->NPC->aiFlags |= NPCAI_FLAMETHROW;
541 self->client->ps.torsoAnimTimer = BOBA_FLAMEDURATION;
542
543 TIMER_Set( self, "flameTime", BOBA_FLAMEDURATION);
544 TIMER_Set( self, "nextAttackDelay", BOBA_FLAMEDURATION);
545 TIMER_Set( self, "nextFlameDelay", BOBA_FLAMEDURATION*2);
546 TIMER_Set( self, "Boba_TacticsSelect", BOBA_FLAMEDURATION);
547
548 G_SoundOnEnt( self, CHAN_WEAPON, "sound/weapons/boba/bf_flame.mp3" );
549 G_PlayEffect( G_EffectIndex("boba/fthrw"), self->playerModel, self->genericBolt3, self->s.number, self->s.origin, 1 );
550
551 Boba_Printf("FlameThrower ON");
552 }
553 }
554
555 ////////////////////////////////////////////////////////////////////////////////////////
556 //
557 ////////////////////////////////////////////////////////////////////////////////////////
Boba_DoFlameThrower(gentity_t * self)558 void Boba_DoFlameThrower( gentity_t *self )
559 {
560 if ( self->s.number < MAX_CLIENTS )
561 {
562 if ( self->client )
563 {
564 if ( !self->client->ps.forcePowerDuration[FP_LIGHTNING] )
565 {
566 NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCELIGHTNING_HOLD, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
567 self->client->ps.torsoAnimTimer = BOBA_FLAMEDURATION;
568 G_SoundOnEnt( self, CHAN_WEAPON, "sound/weapons/boba/bf_flame.mp3" );
569 G_PlayEffect( G_EffectIndex("boba/fthrw"), self->playerModel, self->genericBolt3, self->s.number, self->s.origin, 1 );
570 self->client->ps.forcePowerDuration[FP_LIGHTNING] = 1;
571 }
572 Boba_FireFlameThrower( self );
573 }
574 return;
575 }
576 if (!(NPCInfo->aiFlags&NPCAI_FLAMETHROW) && TIMER_Done(self, "nextAttackDelay"))
577 {
578 Boba_StartFlameThrower( self );
579 }
580
581 if ( (NPCInfo->aiFlags&NPCAI_FLAMETHROW))
582 {
583 Boba_FireFlameThrower( self );
584 }
585 }
586
587 ////////////////////////////////////////////////////////////////////////////////////////
588 //
589 ////////////////////////////////////////////////////////////////////////////////////////
Boba_DoAmbushWait(gentity_t * self)590 void Boba_DoAmbushWait( gentity_t *self)
591 {
592 }
593
594 ////////////////////////////////////////////////////////////////////////////////////////
595 //
596 ////////////////////////////////////////////////////////////////////////////////////////
Boba_DoSniper(gentity_t * self)597 void Boba_DoSniper( gentity_t *self)
598 {
599 if (TIMER_Done(NPC, "PickNewSniperPoint"))
600 {
601 TIMER_Set(NPC, "PickNewSniperPoint", Q_irand(15000, 25000));
602 int SniperPoint = NPC_FindCombatPoint(NPC->currentOrigin, 0, NPC->currentOrigin, CP_SNIPE|CP_CLEAR|CP_HAS_ROUTE|CP_TRYFAR|CP_HORZ_DIST_COLL, 0, -1);
603 if (SniperPoint!=-1)
604 {
605 NPC_SetCombatPoint(SniperPoint);
606 NPC_SetMoveGoal( NPC, level.combatPoints[SniperPoint].origin, 20, qtrue, SniperPoint );
607 }
608 }
609
610 if (Distance(NPC->currentOrigin, level.combatPoints[NPCInfo->combatPoint].origin)<50.0f)
611 {
612 Boba_FireDecide();
613 }
614
615
616 bool IsOnAPath = !!NPC_MoveToGoal(qtrue);
617
618 // Resolve Blocked Problems
619 //--------------------------
620 if (NPCInfo->aiFlags&NPCAI_BLOCKED &&
621 NPC->client->moveType!=MT_FLYSWIM &&
622 ((level.time - NPCInfo->blockedDebounceTime)>3000)
623 )
624 {
625 Boba_Printf("BLOCKED: Attempting Jump");
626 if (IsOnAPath)
627 {
628 if (!NPC_TryJump(NPCInfo->blockedTargetPosition))
629 {
630 Boba_Printf(" Failed");
631 }
632 }
633 }
634
635 NPC_FaceEnemy(qtrue);
636 NPC_UpdateAngles( qtrue, qtrue );
637 }
638
639
640 ////////////////////////////////////////////////////////////////////////////////////////
641 // Call This function to make Boba actually shoot his current weapon
642 ////////////////////////////////////////////////////////////////////////////////////////
Boba_Fire()643 void Boba_Fire()
644 {
645 WeaponThink(qtrue);
646
647 // If Actually Fired, Decide To Apply Alt Fire And Calc Next Attack Delay
648 //------------------------------------------------------------------------
649 if (ucmd.buttons&BUTTON_ATTACK)
650 {
651 switch (NPC->s.weapon)
652 {
653 case WP_ROCKET_LAUNCHER:
654 TIMER_Set( NPC, "nextAttackDelay", Q_irand(1000, 2000));
655
656 // Occasionally Shoot A Homing Missile
657 //-------------------------------------
658 if (!Q_irand(0,3))
659 {
660 ucmd.buttons &= ~BUTTON_ATTACK;
661 ucmd.buttons |= BUTTON_ALT_ATTACK;
662 NPC->client->fireDelay = Q_irand( 1000, 3000 );
663 }
664 break;
665
666 case WP_DISRUPTOR:
667 TIMER_Set(NPC, "nextAttackDelay", Q_irand(1000, 4000));
668
669 // Occasionally Alt-Fire
670 //-----------------------
671 if (!Q_irand(0,3))
672 {
673 ucmd.buttons &= ~BUTTON_ATTACK;
674 ucmd.buttons |= BUTTON_ALT_ATTACK;
675 NPC->client->fireDelay = Q_irand( 1000, 3000 );
676 }
677 break;
678
679 case WP_BLASTER:
680
681 if (TIMER_Done(NPC, "nextBlasterAltFireDecide"))
682 {
683 if (Q_irand(0, (NPC->count*2)+3)>2)
684 {
685 TIMER_Set(NPC, "nextBlasterAltFireDecide", Q_irand(3000, 8000));
686 if (!(NPCInfo->scriptFlags&SCF_ALT_FIRE))
687 {
688 Boba_Printf("ALT FIRE On");
689 NPCInfo->scriptFlags |= SCF_ALT_FIRE;
690 NPC_ChangeWeapon(WP_BLASTER); // Update Delay Timers
691 }
692 }
693 else
694 {
695 TIMER_Set(NPC, "nextBlasterAltFireDecide", Q_irand(2000, 5000));
696 if ( (NPCInfo->scriptFlags&SCF_ALT_FIRE))
697 {
698 Boba_Printf("ALT FIRE Off");
699 NPCInfo->scriptFlags &=~SCF_ALT_FIRE;
700 NPC_ChangeWeapon(WP_BLASTER); // Update Delay Timers
701 }
702 }
703 }
704
705 // Occasionally Alt Fire
706 //-----------------------
707 if (NPCInfo->scriptFlags&SCF_ALT_FIRE)
708 {
709 ucmd.buttons &= ~BUTTON_ATTACK;
710 ucmd.buttons |= BUTTON_ALT_ATTACK;
711 }
712 break;
713 }
714 }
715 }
716
717
718 ////////////////////////////////////////////////////////////////////////////////////////
719 // Call this function to see if Fett should fire his current weapon
720 ////////////////////////////////////////////////////////////////////////////////////////
Boba_FireDecide(void)721 void Boba_FireDecide( void )
722 {
723 // Any Reason Not To Shoot?
724 //--------------------------
725 if (!NPC || // Only NPCs
726 !NPC->client || // Only Clients
727 NPC->client->NPC_class!=CLASS_BOBAFETT || // Only Boba
728 !NPC->enemy || // Only If There Is An Enemy
729 NPC->s.weapon==WP_NONE || // Only If Using A Valid Weapon
730 !TIMER_Done(NPC, "nextAttackDelay") || // Only If Ready To Shoot Again
731 !Boba_CanSeeEnemy(NPC) // Only If Enemy Recently Seen
732 )
733 {
734 return;
735 }
736
737 // Now Check Weapon Specific Parameters To See If We Should Shoot Or Not
738 //-----------------------------------------------------------------------
739 switch (NPC->s.weapon)
740 {
741 case WP_ROCKET_LAUNCHER:
742 if (Distance(NPC->currentOrigin, NPC->enemy->currentOrigin)>400.0f)
743 {
744 Boba_Fire();
745 }
746 break;
747
748 case WP_DISRUPTOR:
749 // TODO: Add Conditions Here
750 Boba_Fire();
751 break;
752
753 case WP_BLASTER:
754 // TODO: Add Conditions Here
755 Boba_Fire();
756 break;
757 }
758 }
759
760
761
762 ////////////////////////////////////////////////////////////////////////////////////////
763 // Tactics avaliable to Boba Fett:
764 // --------------------------------
765 // BTS_RIFLE, // Uses Jedi / Seeker Movement
766 // BTS_MISSILE, // Uses Jedi / Seeker Movement
767 // BTS_SNIPER, // Uses Special Movement Internal To This File
768 // BTS_FLAMETHROW, // Locked In Place
769 // BTS_AMBUSHWAIT, // Goto CP & Wait
770 //
771 //
772 // Weapons available to Boba Fett:
773 // --------------------------------
774 // WP_NONE (Flame Thrower)
775 // WP_ROCKET_LAUNCHER
776 // WP_BLASTER
777 // WP_DISRUPTOR
778 //
779 ////////////////////////////////////////////////////////////////////////////////////////
Boba_TacticsSelect()780 void Boba_TacticsSelect()
781 {
782 // Don't Change Tactics For A Little While
783 //------------------------------------------
784 TIMER_Set(NPC, "Boba_TacticsSelect", Q_irand(8000, 15000));
785 int nextState = NPCInfo->localState;
786
787
788 // Get Some Data That Will Help With The Selection Of The Next Tactic
789 //--------------------------------------------------------------------
790 bool enemyAlive = (NPC->enemy->health>0);
791 float enemyDistance = Distance(NPC->currentOrigin, NPC->enemy->currentOrigin);
792 bool enemyInFlameRange = (enemyDistance<BOBA_FLAMETHROWRANGE);
793 bool enemyInRocketRange = (enemyDistance>BOBA_ROCKETRANGEMIN && enemyDistance<BOBA_ROCKETRANGEMAX);
794 bool enemyRecentlySeen = Boba_CanSeeEnemy(NPC);
795
796
797 // Enemy Is Really Close
798 //-----------------------
799 if (!enemyAlive)
800 {
801 nextState = BTS_RIFLE;
802 }
803 else if (enemyInFlameRange)
804 {
805 // If It's Been Long Enough Since Our Last Flame Blast, Try To Torch The Enemy
806 //-----------------------------------------------------------------------------
807 if (TIMER_Done(NPC, "nextFlameDelay"))
808 {
809 nextState = BTS_FLAMETHROW;
810 }
811
812 // Otherwise, He's Probably Too Close, So Try To Get Clear Of Him
813 //----------------------------------------------------------------
814 else
815 {
816 nextState = BTS_RIFLE;
817 }
818 }
819
820 // Recently Saw The Enemy, Time For Some Good Ole Fighten!
821 //---------------------------------------------------------
822 else if (enemyRecentlySeen)
823 {
824 // At First, Boba will prefer to use his blaster against the player, but
825 // the more times he is driven away (NPC->count), he will be less likely to
826 // choose the blaster, and more likely to go for the missile launcher
827 nextState = (!enemyInRocketRange || Q_irand(0, NPC->count)<1)?(BTS_RIFLE):(BTS_MISSILE);
828 }
829
830 // Hmmm... Havn't Seen The Player In A While, We Might Want To Try Something Sneaky
831 //-----------------------------------------------------------------------------------
832 else
833 {
834 bool SnipePointsNear = false; // TODO
835 bool AmbushPointNear = false; // TODO
836
837 if (Q_irand(0, NPC->count)>0)
838 {
839 int SniperPoint = NPC_FindCombatPoint(NPC->currentOrigin, 0, NPC->currentOrigin, CP_SNIPE|CP_CLEAR|CP_HAS_ROUTE|CP_TRYFAR|CP_HORZ_DIST_COLL, 0, -1);
840 if (SniperPoint!=-1)
841 {
842 NPC_SetCombatPoint(SniperPoint);
843 NPC_SetMoveGoal( NPC, level.combatPoints[SniperPoint].origin, 20, qtrue, SniperPoint );
844 TIMER_Set(NPC, "PickNewSniperPoint", Q_irand(15000, 25000));
845 SnipePointsNear = true;
846 }
847 }
848
849
850 if (SnipePointsNear && TIMER_Done(NPC, "Boba_NoSniperTime"))
851 {
852 TIMER_Set(NPC, "Boba_NoSniperTime", 120000); // Don't snipe again for a while
853 TIMER_Set(NPC, "Boba_TacticsSelect", Q_irand(35000, 45000));// More patience here
854 nextState = BTS_SNIPER;
855 }
856 else if (AmbushPointNear)
857 {
858 TIMER_Set(NPC, "Boba_TacticsSelect", Q_irand(15000, 25000));// More patience here
859 nextState = BTS_AMBUSHWAIT;
860 }
861 else
862 {
863 nextState = (!enemyInRocketRange || Q_irand(0, NPC->count)<1)?(BTS_RIFLE):(BTS_MISSILE);
864 }
865 }
866
867
868
869 // The Next State Has Been Selected, Now Change Weapon If Necessary
870 //------------------------------------------------------------------
871 if (nextState!=NPCInfo->localState)
872 {
873 NPCInfo->localState = nextState;
874 switch (NPCInfo->localState)
875 {
876 case BTS_FLAMETHROW:
877 Boba_Printf("NEW TACTIC: Flame Thrower");
878 Boba_ChangeWeapon(WP_NONE);
879 Boba_DoFlameThrower(NPC);
880 break;
881
882 case BTS_RIFLE:
883 Boba_Printf("NEW TACTIC: Rifle");
884 Boba_ChangeWeapon(WP_BLASTER);
885 break;
886
887 case BTS_MISSILE:
888 Boba_Printf("NEW TACTIC: Rocket Launcher");
889 Boba_ChangeWeapon(WP_ROCKET_LAUNCHER);
890 break;
891
892 case BTS_SNIPER:
893 Boba_Printf("NEW TACTIC: Sniper");
894 Boba_ChangeWeapon(WP_DISRUPTOR);
895 break;
896
897 case BTS_AMBUSHWAIT:
898 Boba_Printf("NEW TACTIC: Ambush");
899 Boba_ChangeWeapon(WP_NONE);
900 break;
901 }
902 }
903 }
904
905
906 ////////////////////////////////////////////////////////////////////////////////////////
907 // Tactics
908 //
909 // This function is called right after Update()
910 // If returns true, Jedi and Seeker AI not used for movement
911 //
912 ////////////////////////////////////////////////////////////////////////////////////////
Boba_Tactics()913 bool Boba_Tactics()
914 {
915 if (!NPC->enemy)
916 {
917 return false;
918 }
919
920 // Think About Changing Tactics
921 //------------------------------
922 if (TIMER_Done(NPC, "Boba_TacticsSelect"))
923 {
924 Boba_TacticsSelect();
925 }
926
927 // These Tactics Require Seeker & Jedi Movement
928 //----------------------------------------------
929 if (!NPCInfo->localState ||
930 NPCInfo->localState==BTS_RIFLE ||
931 NPCInfo->localState==BTS_MISSILE)
932 {
933 return false;
934 }
935
936 // Flame Thrower - Locked In Place
937 //---------------------------------
938 if (NPCInfo->localState==BTS_FLAMETHROW)
939 {
940 Boba_DoFlameThrower( NPC );
941 }
942
943 // Sniper - Move Around, And Take Shots
944 //--------------------------------------
945 else if (NPCInfo->localState==BTS_SNIPER)
946 {
947 Boba_DoSniper( NPC );
948 }
949
950 // Ambush Wait
951 //------------
952 else if (NPCInfo->localState==BTS_AMBUSHWAIT)
953 {
954 Boba_DoAmbushWait( NPC );
955 }
956
957
958 NPC_FacePosition( NPC->enemy->currentOrigin, qtrue);
959 NPC_UpdateAngles(qtrue, qtrue);
960
961 return true; // Do Not Use Normal Jedi Or Seeker Movement
962 }
963
964
965 ////////////////////////////////////////////////////////////////////////////////////////
966 //
967 ////////////////////////////////////////////////////////////////////////////////////////
Boba_Respawn()968 bool Boba_Respawn()
969 {
970 int cp = -1;
971
972 // Try To Predict Where The Enemy Is Going
973 //-----------------------------------------
974 if (AverageEnemyDirectionSamples && NPC->behaviorSet[BSET_DEATH]==0)
975 {
976 vec3_t endPos;
977 VectorMA(NPC->enemy->currentOrigin, 1000.0f / (float)AverageEnemyDirectionSamples, AverageEnemyDirection, endPos);
978 cp = NPC_FindCombatPoint(endPos, 0, endPos, CP_FLEE|CP_TRYFAR|CP_HORZ_DIST_COLL, 0, -1);
979 Boba_Printf("Attempting Predictive Spawn Point");
980 }
981
982 // If That Failed, Try To Go Directly To The Enemy
983 //-------------------------------------------------
984 if (cp==-1)
985 {
986 cp = NPC_FindCombatPoint(NPC->enemy->currentOrigin, 0, NPC->enemy->currentOrigin, CP_FLEE|CP_TRYFAR|CP_HORZ_DIST_COLL, 0, -1);
987 Boba_Printf("Attempting Closest Current Spawn Point");
988 }
989
990 // If We've Found One, Go There
991 //------------------------------
992 if (cp!=-1)
993 {
994 NPC_SetCombatPoint( cp );
995 NPCInfo->surrenderTime = 0;
996 NPC->health = NPC->max_health;
997 NPC->svFlags &=~SVF_NOCLIENT;
998 NPC->count ++; // This is the number of times spawned
999 G_SetOrigin(NPC, level.combatPoints[cp].origin);
1000
1001 AverageEnemyDirectionSamples = 0;
1002 VectorClear(AverageEnemyDirection);
1003
1004 Boba_Printf("Found Spawn Point (%d)", cp);
1005 return true;
1006 }
1007
1008 assert(0); // Yea, that's bad...
1009 Boba_Printf("FAILED TO FIND SPAWN POINT");
1010 return false;
1011 }
1012
1013
1014 ////////////////////////////////////////////////////////////////////////////////////////
1015 //
1016 ////////////////////////////////////////////////////////////////////////////////////////
Boba_Update()1017 void Boba_Update()
1018 {
1019 // Never Forget The Player... Never.
1020 //-----------------------------------
1021 if (player && player->inuse && !NPC->enemy)
1022 {
1023 G_SetEnemy(NPC, player);
1024 NPC->svFlags |= SVF_LOCKEDENEMY; // Don't forget about the enemy once you've found him
1025 }
1026
1027 // Hey, This Is Boba, He Tests The Trace All The Time
1028 //----------------------------------------------------
1029 if (NPC->enemy)
1030 {
1031 if (!(NPC->svFlags&SVF_NOCLIENT))
1032 {
1033 trace_t testTrace;
1034 vec3_t eyes;
1035 CalcEntitySpot( NPC, SPOT_HEAD_LEAN, eyes );
1036 gi.trace (&testTrace, eyes, NULL, NULL, NPC->enemy->currentOrigin, NPC->s.number, MASK_SHOT, (EG2_Collision)0, 0);
1037
1038 bool wasSeen = Boba_CanSeeEnemy(NPC);
1039
1040 if (!testTrace.startsolid &&
1041 !testTrace.allsolid &&
1042 testTrace.entityNum == NPC->enemy->s.number)
1043 {
1044 NPCInfo->enemyLastSeenTime = level.time;
1045 NPCInfo->enemyLastHeardTime = level.time;
1046 VectorCopy(NPC->enemy->currentOrigin, NPCInfo->enemyLastSeenLocation);
1047 VectorCopy(NPC->enemy->currentOrigin, NPCInfo->enemyLastHeardLocation);
1048 }
1049 else if (gi.inPVS( NPC->enemy->currentOrigin, NPC->currentOrigin))
1050 {
1051 NPCInfo->enemyLastHeardTime = level.time;
1052 VectorCopy(NPC->enemy->currentOrigin, NPCInfo->enemyLastHeardLocation);
1053 }
1054
1055 if (g_bobaDebug->integer)
1056 {
1057 bool nowSeen = Boba_CanSeeEnemy(NPC);
1058 if (!wasSeen && nowSeen)
1059 {
1060 Boba_Printf("Enemy Seen");
1061 }
1062 if (wasSeen && !nowSeen)
1063 {
1064 Boba_Printf("Enemy Lost");
1065 }
1066 CG_DrawEdge(NPC->currentOrigin, NPC->enemy->currentOrigin, (nowSeen)?(EDGE_IMPACT_SAFE):(EDGE_IMPACT_POSSIBLE));
1067 }
1068 }
1069
1070 if (!NPCInfo->surrenderTime)
1071 {
1072 if ((level.time - NPCInfo->enemyLastSeenTime)>20000 && TIMER_Done(NPC, "TooLongGoneRespawn"))
1073 {
1074 TIMER_Set(NPC, "TooLongGoneRespawn", 30000); // Give him some time to get to you before trying again
1075 Boba_Printf("Gone Too Long, Attempting Respawn Even Though Not Hiding");
1076 Boba_Respawn();
1077 }
1078 }
1079 }
1080
1081
1082 // Make Sure He Always Appears In The Last Area With Full Health When His Death Script Is Turned On
1083 //--------------------------------------------------------------------------------------------------
1084 if (!BobaHadDeathScript && NPC->behaviorSet[BSET_DEATH]!=0)
1085 {
1086 if (!gi.inPVS(NPC->enemy->currentOrigin, NPC->currentOrigin))
1087 {
1088 Boba_Printf("Attempting Final Battle Spawn...");
1089 if (Boba_Respawn())
1090 {
1091 BobaHadDeathScript = true;
1092 }
1093 else
1094 {
1095 Boba_Printf("Failed");
1096 }
1097 }
1098 }
1099
1100
1101
1102 // Don't Forget To Turn Off That Flame Thrower, Mr. Fett - You're Waisting Precious Natural Gases
1103 //------------------------------------------------------------------------------------------------
1104 if ((NPCInfo->aiFlags&NPCAI_FLAMETHROW) && (TIMER_Done(NPC, "flameTime")))
1105 {
1106 Boba_StopFlameThrower(NPC);
1107 }
1108
1109
1110 // Occasionally A Jump Turns Into A Rocket Fly
1111 //---------------------------------------------
1112 if ( NPC->client->ps.groundEntityNum == ENTITYNUM_NONE
1113 && NPC->client->ps.forceJumpZStart
1114 && !Q_irand( 0, 10 ) )
1115 {//take off
1116 Boba_FlyStart( NPC );
1117 }
1118
1119
1120 // If Hurting, Try To Run Away
1121 //-----------------------------
1122 if (!NPCInfo->surrenderTime && (NPC->health<NPC->max_health/10))
1123 {
1124 Boba_Printf("Time To Surrender, Searching For Flee Point");
1125
1126
1127 // Find The Closest Flee Point That I Can Get To
1128 //-----------------------------------------------
1129 int cp = NPC_FindCombatPoint(NPC->currentOrigin, 0, NPC->currentOrigin, CP_FLEE|CP_HAS_ROUTE|CP_TRYFAR|CP_HORZ_DIST_COLL, 0, -1);
1130 if (cp!=-1)
1131 {
1132 NPC_SetCombatPoint( cp );
1133 NPC_SetMoveGoal( NPC, level.combatPoints[cp].origin, 8, qtrue, cp );
1134 if (NPC->count<6)
1135 {
1136 NPCInfo->surrenderTime = level.time + Q_irand(5000, 10000) + 1000*(6-NPC->count);
1137 }
1138 else
1139 {
1140 NPCInfo->surrenderTime = level.time + Q_irand(5000, 10000);
1141 }
1142 }
1143 else
1144 {
1145 Boba_Printf(" Failure");
1146 }
1147 }
1148 }
1149
1150
1151
1152 ////////////////////////////////////////////////////////////////////////////////////////
1153 //
1154 ////////////////////////////////////////////////////////////////////////////////////////
Boba_Flee()1155 bool Boba_Flee()
1156 {
1157 bool EnemyRecentlySeen = ((level.time - NPCInfo->enemyLastSeenTime)<10000);
1158 bool ReachedEscapePoint = (Distance(level.combatPoints[NPCInfo->combatPoint].origin, NPC->currentOrigin)<50.0f);
1159 bool HasBeenGoneEnough = (level.time>NPCInfo->surrenderTime || (level.time - NPCInfo->enemyLastSeenTime)>400000);
1160
1161
1162 // Is It Time To Come Back For Some More?
1163 //----------------------------------------
1164 if (!EnemyRecentlySeen || ReachedEscapePoint)
1165 {
1166 NPC->svFlags |= SVF_NOCLIENT;
1167 if (HasBeenGoneEnough)
1168 {
1169 if ((level.time - NPCInfo->enemyLastSeenTime)>400000)
1170 {
1171 Boba_Printf(" Gone Too Long, Attempting Respawn");
1172 }
1173
1174 if (Boba_Respawn())
1175 {
1176 return true;
1177 }
1178 }
1179 else if (ReachedEscapePoint && (NPCInfo->surrenderTime - level.time)>3000)
1180 {
1181 if (TIMER_Done(NPC, "SpookPlayerTimer"))
1182 {
1183 vec3_t testDirection;
1184 TIMER_Set(NPC, "SpookPlayerTimer", Q_irand(2000, 10000));
1185 switch(Q_irand(0, 1))
1186 {
1187 case 0:
1188 Boba_Printf("SPOOK: Dust");
1189 Boba_DustFallNear(NPC->enemy->currentOrigin, Q_irand(1,2));
1190 break;
1191
1192 case 1:
1193 Boba_Printf("SPOOK: Footsteps");
1194 testDirection[0] = (Q_flrand(0.0f, 1.0f) * 0.5f) - 1.0f;
1195 testDirection[0] += (testDirection[0]>0.0f)?(0.5f):(-0.5f);
1196 testDirection[1] = (Q_flrand(0.0f, 1.0f) * 0.5f) - 1.0f;
1197 testDirection[1] += (testDirection[1]>0.0f)?(0.5f):(-0.5f);
1198 testDirection[2] = 1.0f;
1199 VectorMA(NPC->enemy->currentOrigin, 400.0f, testDirection, BobaFootStepLoc);
1200
1201 BobaFootStepCount = Q_irand(3,8);
1202 break;
1203 }
1204 }
1205
1206 if (BobaFootStepCount && TIMER_Done(NPC, "BobaFootStepFakeTimer"))
1207 {
1208 TIMER_Set(NPC, "BobaFootStepFakeTimer", Q_irand(300, 800));
1209 BobaFootStepCount --;
1210 G_SoundAtSpot(BobaFootStepLoc, G_SoundIndex(va("sound/player/footsteps/boot%d", Q_irand(1,4))), qtrue);
1211 }
1212
1213 if (TIMER_Done(NPC, "ResampleEnemyDirection") && NPC->enemy->resultspeed>10.0f)
1214 {
1215 TIMER_Set(NPC, "ResampleEnemyDirection", Q_irand(500, 1000));
1216 AverageEnemyDirectionSamples ++;
1217
1218 vec3_t moveDir;
1219 VectorCopy(NPC->enemy->client->ps.velocity, moveDir);
1220 VectorNormalize(moveDir);
1221
1222 VectorAdd(AverageEnemyDirection, moveDir, AverageEnemyDirection);
1223 }
1224
1225 if (g_bobaDebug->integer && AverageEnemyDirectionSamples)
1226 {
1227 vec3_t endPos;
1228 VectorMA(NPC->enemy->currentOrigin, 500.0f / (float)AverageEnemyDirectionSamples, AverageEnemyDirection, endPos);
1229 CG_DrawEdge(NPC->enemy->currentOrigin, endPos, EDGE_IMPACT_POSSIBLE);
1230 }
1231 }
1232 }
1233 else
1234 {
1235 NPCInfo->surrenderTime += 100;
1236 }
1237
1238 // Finish The Flame Thrower First...
1239 //-----------------------------------
1240 if (NPCInfo->aiFlags&NPCAI_FLAMETHROW)
1241 {
1242 Boba_DoFlameThrower( NPC );
1243 NPC_FacePosition( NPC->enemy->currentOrigin, qtrue);
1244 NPC_UpdateAngles(qtrue, qtrue);
1245 return true;
1246 }
1247
1248 bool IsOnAPath = !!NPC_MoveToGoal(qtrue);
1249 if (!ReachedEscapePoint &&
1250 NPCInfo->aiFlags&NPCAI_BLOCKED &&
1251 NPC->client->moveType!=MT_FLYSWIM &&
1252 ((level.time - NPCInfo->blockedDebounceTime)>1000)
1253 )
1254 {
1255 if (!Boba_CanSeeEnemy(NPC) && Distance(NPC->currentOrigin, level.combatPoints[NPCInfo->combatPoint].origin)<200)
1256 {
1257 Boba_Printf("BLOCKED: Just Teleporting There");
1258 G_SetOrigin(NPC, level.combatPoints[NPCInfo->combatPoint].origin);
1259 }
1260 else
1261 {
1262 Boba_Printf("BLOCKED: Attempting Jump");
1263
1264 if (IsOnAPath)
1265 {
1266 if (NPC_TryJump(NPCInfo->blockedTargetPosition))
1267 {
1268 }
1269 else
1270 {
1271 Boba_Printf(" Failed");
1272 }
1273 }
1274 else if (EnemyRecentlySeen)
1275 {
1276 if (NPC_TryJump(NPCInfo->enemyLastSeenLocation))
1277 {
1278 }
1279 else
1280 {
1281 Boba_Printf(" Failed");
1282 }
1283 }
1284 }
1285 }
1286
1287
1288 NPC_UpdateAngles( qtrue, qtrue );
1289 return true;
1290 }