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 }