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 #include "g_headers.h"
23 
24 #include "b_local.h"
25 #include "g_nav.h"
26 
27 gentity_t *CreateMissile( vec3_t org, vec3_t dir, float vel, int life, gentity_t *owner, qboolean altFire = qfalse );
28 
29 void Seeker_Strafe( void );
30 
31 #define VELOCITY_DECAY		0.7f
32 
33 #define	MIN_MELEE_RANGE		320
34 #define	MIN_MELEE_RANGE_SQR	( MIN_MELEE_RANGE * MIN_MELEE_RANGE )
35 
36 #define MIN_DISTANCE		80
37 #define MIN_DISTANCE_SQR	( MIN_DISTANCE * MIN_DISTANCE )
38 
39 #define SEEKER_STRAFE_VEL	100
40 #define SEEKER_STRAFE_DIS	200
41 #define SEEKER_UPWARD_PUSH	32
42 
43 #define SEEKER_FORWARD_BASE_SPEED	10
44 #define SEEKER_FORWARD_MULTIPLIER	2
45 
46 #define SEEKER_SEEK_RADIUS			1024
47 
48 //------------------------------------
NPC_Seeker_Precache(void)49 void NPC_Seeker_Precache(void)
50 {
51 	G_SoundIndex("sound/chars/seeker/misc/fire.wav");
52 	G_SoundIndex( "sound/chars/seeker/misc/hiss.wav");
53 	G_EffectIndex( "env/small_explode");
54 }
55 
56 //------------------------------------
NPC_Seeker_Pain(gentity_t * self,gentity_t * inflictor,gentity_t * other,vec3_t point,int damage,int mod,int hitLoc)57 void NPC_Seeker_Pain( gentity_t *self, gentity_t *inflictor, gentity_t *other, vec3_t point, int damage, int mod,int hitLoc )
58 {
59 	if ( !(self->svFlags & SVF_CUSTOM_GRAVITY ))
60 	{//void G_Damage( gentity_t *targ, gentity_t *inflictor, gentity_t *attacker, vec3_t dir, vec3_t point, int damage, int dflags, int mod, int hitLoc=HL_NONE );
61 		G_Damage( self, NULL, NULL, (float*)vec3_origin, (float*)vec3_origin, 999, 0, MOD_FALLING );
62 	}
63 
64 	SaveNPCGlobals();
65 	SetNPCGlobals( self );
66 	Seeker_Strafe();
67 	RestoreNPCGlobals();
68 	NPC_Pain( self, inflictor, other, point, damage, mod );
69 }
70 
71 //------------------------------------
Seeker_MaintainHeight(void)72 void Seeker_MaintainHeight( void )
73 {
74 	float	dif;
75 
76 	// Update our angles regardless
77 	NPC_UpdateAngles( qtrue, qtrue );
78 
79 	// If we have an enemy, we should try to hover at or a little below enemy eye level
80 	if ( NPC->enemy )
81 	{
82 		if (TIMER_Done( NPC, "heightChange" ))
83 		{
84 			TIMER_Set( NPC,"heightChange",Q_irand( 1000, 3000 ));
85 
86 			// Find the height difference
87 			dif = (NPC->enemy->currentOrigin[2] +  Q_flrand( NPC->enemy->maxs[2]/2, NPC->enemy->maxs[2]+8 )) - NPC->currentOrigin[2];
88 
89 			// cap to prevent dramatic height shifts
90 			if ( fabs( dif ) > 2 )
91 			{
92 				if ( fabs( dif ) > 24 )
93 				{
94 					dif = ( dif < 0 ? -24 : 24 );
95 				}
96 
97 				NPC->client->ps.velocity[2] = (NPC->client->ps.velocity[2]+dif)/2;
98 			}
99 		}
100 	}
101 	else
102 	{
103 		gentity_t *goal = NULL;
104 
105 		if ( NPCInfo->goalEntity )	// Is there a goal?
106 		{
107 			goal = NPCInfo->goalEntity;
108 		}
109 		else
110 		{
111 			goal = NPCInfo->lastGoalEntity;
112 		}
113 		if ( goal )
114 		{
115 			dif = goal->currentOrigin[2] - NPC->currentOrigin[2];
116 
117 			if ( fabs( dif ) > 24 )
118 			{
119 				ucmd.upmove = ( ucmd.upmove < 0 ? -4 : 4 );
120 			}
121 			else
122 			{
123 				if ( NPC->client->ps.velocity[2] )
124 				{
125 					NPC->client->ps.velocity[2] *= VELOCITY_DECAY;
126 
127 					if ( fabs( NPC->client->ps.velocity[2] ) < 2 )
128 					{
129 						NPC->client->ps.velocity[2] = 0;
130 					}
131 				}
132 			}
133 		}
134 	}
135 
136 	// Apply friction
137 	if ( NPC->client->ps.velocity[0] )
138 	{
139 		NPC->client->ps.velocity[0] *= VELOCITY_DECAY;
140 
141 		if ( fabs( NPC->client->ps.velocity[0] ) < 1 )
142 		{
143 			NPC->client->ps.velocity[0] = 0;
144 		}
145 	}
146 
147 	if ( NPC->client->ps.velocity[1] )
148 	{
149 		NPC->client->ps.velocity[1] *= VELOCITY_DECAY;
150 
151 		if ( fabs( NPC->client->ps.velocity[1] ) < 1 )
152 		{
153 			NPC->client->ps.velocity[1] = 0;
154 		}
155 	}
156 }
157 
158 //------------------------------------
Seeker_Strafe(void)159 void Seeker_Strafe( void )
160 {
161 	int		side;
162 	vec3_t	end, right, dir;
163 	trace_t	tr;
164 
165 	if ( Q_flrand(0.0f, 1.0f) > 0.7f || !NPC->enemy || !NPC->enemy->client )
166 	{
167 		// Do a regular style strafe
168 		AngleVectors( NPC->client->renderInfo.eyeAngles, NULL, right, NULL );
169 
170 		// Pick a random strafe direction, then check to see if doing a strafe would be
171 		//	reasonably valid
172 		side = ( rand() & 1 ) ? -1 : 1;
173 		VectorMA( NPC->currentOrigin, SEEKER_STRAFE_DIS * side, right, end );
174 
175 		gi.trace( &tr, NPC->currentOrigin, NULL, NULL, end, NPC->s.number, MASK_SOLID, G2_NOCOLLIDE, 0 );
176 
177 		// Close enough
178 		if ( tr.fraction > 0.9f )
179 		{
180 			VectorMA( NPC->client->ps.velocity, SEEKER_STRAFE_VEL * side, right, NPC->client->ps.velocity );
181 
182 			G_Sound( NPC, G_SoundIndex( "sound/chars/seeker/misc/hiss" ));
183 
184 			// Add a slight upward push
185 			NPC->client->ps.velocity[2] += SEEKER_UPWARD_PUSH;
186 
187 			NPCInfo->standTime = level.time + 1000 + Q_flrand(0.0f, 1.0f) * 500;
188 		}
189 	}
190 	else
191 	{
192 		// Do a strafe to try and keep on the side of their enemy
193 		AngleVectors( NPC->enemy->client->renderInfo.eyeAngles, dir, right, NULL );
194 
195 		// Pick a random side
196 		side = ( rand() & 1 ) ? -1 : 1;
197 		VectorMA( NPC->enemy->currentOrigin, SEEKER_STRAFE_DIS * side, right, end );
198 
199 		// then add a very small bit of random in front of/behind the player action
200 		VectorMA( end, Q_flrand(-1.0f, 1.0f) * 25, dir, end );
201 
202 		gi.trace( &tr, NPC->currentOrigin, NULL, NULL, end, NPC->s.number, MASK_SOLID, G2_NOCOLLIDE, 0 );
203 
204 		// Close enough
205 		if ( tr.fraction > 0.9f )
206 		{
207 			VectorSubtract( tr.endpos, NPC->currentOrigin, dir );
208 			dir[2] *= 0.25; // do less upward change
209 			float dis = VectorNormalize( dir );
210 
211 			// Try to move the desired enemy side
212 			VectorMA( NPC->client->ps.velocity, dis, dir, NPC->client->ps.velocity );
213 
214 			G_Sound( NPC, G_SoundIndex( "sound/chars/seeker/misc/hiss" ));
215 
216 			// Add a slight upward push
217 			NPC->client->ps.velocity[2] += SEEKER_UPWARD_PUSH;
218 
219 			NPCInfo->standTime = level.time + 2500 + Q_flrand(0.0f, 1.0f) * 500;
220 		}
221 	}
222 }
223 
224 //------------------------------------
Seeker_Hunt(qboolean visible,qboolean advance)225 void Seeker_Hunt( qboolean visible, qboolean advance )
226 {
227 	float	distance, speed;
228 	vec3_t	forward;
229 
230 	NPC_FaceEnemy( qtrue );
231 
232 	// If we're not supposed to stand still, pursue the player
233 	if ( NPCInfo->standTime < level.time )
234 	{
235 		// Only strafe when we can see the player
236 		if ( visible )
237 		{
238 			Seeker_Strafe();
239 			return;
240 		}
241 	}
242 
243 	// If we don't want to advance, stop here
244 	if ( advance == qfalse )
245 	{
246 		return;
247 	}
248 
249 	// Only try and navigate if the player is visible
250 	if ( visible == qfalse )
251 	{
252 		// Move towards our goal
253 		NPCInfo->goalEntity = NPC->enemy;
254 		NPCInfo->goalRadius = 24;
255 
256 		// Get our direction from the navigator if we can't see our target
257 		if ( NPC_GetMoveDirection( forward, &distance ) == qfalse )
258 		{
259 			return;
260 		}
261 	}
262 	else
263 	{
264 		VectorSubtract( NPC->enemy->currentOrigin, NPC->currentOrigin, forward );
265 		/*distance = */VectorNormalize( forward );
266 	}
267 
268 	speed = SEEKER_FORWARD_BASE_SPEED + SEEKER_FORWARD_MULTIPLIER * g_spskill->integer;
269 	VectorMA( NPC->client->ps.velocity, speed, forward, NPC->client->ps.velocity );
270 }
271 
272 //------------------------------------
Seeker_Fire(void)273 void Seeker_Fire( void )
274 {
275 	vec3_t		dir, enemy_org, muzzle;
276 	gentity_t	*missile;
277 
278 	CalcEntitySpot( NPC->enemy, SPOT_HEAD, enemy_org );
279 	VectorSubtract( enemy_org, NPC->currentOrigin, dir );
280 	VectorNormalize( dir );
281 
282 	// move a bit forward in the direction we shall shoot in so that the bolt doesn't poke out the other side of the seeker
283 	VectorMA( NPC->currentOrigin, 15, dir, muzzle );
284 
285 	missile = CreateMissile( muzzle, dir, 1000, 10000, NPC );
286 
287 	G_PlayEffect( "blaster/muzzle_flash", NPC->currentOrigin, dir );
288 
289 	missile->classname = "blaster";
290 	missile->s.weapon = WP_BLASTER;
291 
292 	missile->damage = 5;
293 	missile->dflags = DAMAGE_DEATH_KNOCKBACK;
294 	missile->methodOfDeath = MOD_ENERGY;
295 	missile->clipmask = MASK_SHOT | CONTENTS_LIGHTSABER;
296 }
297 
298 //------------------------------------
Seeker_Ranged(qboolean visible,qboolean advance)299 void Seeker_Ranged( qboolean visible, qboolean advance )
300 {
301 	if ( NPC->count > 0 )
302 	{
303 		if ( TIMER_Done( NPC, "attackDelay" ))	// Attack?
304 		{
305 			TIMER_Set( NPC, "attackDelay", Q_irand( 250, 2500 ));
306 			Seeker_Fire();
307 			NPC->count--;
308 		}
309 	}
310 	else
311 	{
312 		// out of ammo, so let it die...give it a push up so it can fall more and blow up on impact
313 //		NPC->client->ps.gravity = 900;
314 //		NPC->svFlags &= ~SVF_CUSTOM_GRAVITY;
315 //		NPC->client->ps.velocity[2] += 16;
316 		G_Damage( NPC, NPC, NPC, NULL, NULL, 999, 0, MOD_UNKNOWN );
317 	}
318 
319 	if ( NPCInfo->scriptFlags & SCF_CHASE_ENEMIES )
320 	{
321 		Seeker_Hunt( visible, advance );
322 	}
323 }
324 
325 //------------------------------------
Seeker_Attack(void)326 void Seeker_Attack( void )
327 {
328 	// Always keep a good height off the ground
329 	Seeker_MaintainHeight();
330 
331 	// Rate our distance to the target, and our visibilty
332 	float		distance	= DistanceHorizontalSquared( NPC->currentOrigin, NPC->enemy->currentOrigin );
333 	qboolean	visible		= NPC_ClearLOS( NPC->enemy );
334 	qboolean	advance		= (qboolean)(distance > MIN_DISTANCE_SQR);
335 
336 	// If we cannot see our target, move to see it
337 	if ( visible == qfalse )
338 	{
339 		if ( NPCInfo->scriptFlags & SCF_CHASE_ENEMIES )
340 		{
341 			Seeker_Hunt( visible, advance );
342 			return;
343 		}
344 	}
345 
346 	Seeker_Ranged( visible, advance );
347 }
348 
349 //------------------------------------
Seeker_FindEnemy(void)350 void Seeker_FindEnemy( void )
351 {
352 	int			numFound;
353 	float		dis, bestDis = SEEKER_SEEK_RADIUS * SEEKER_SEEK_RADIUS + 1;
354 	vec3_t		mins, maxs;
355 	gentity_t	*entityList[MAX_GENTITIES], *ent, *best = NULL;
356 
357 	VectorSet( maxs, SEEKER_SEEK_RADIUS, SEEKER_SEEK_RADIUS, SEEKER_SEEK_RADIUS );
358 	VectorScale( maxs, -1, mins );
359 
360 	numFound = gi.EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
361 
362 	for ( int i = 0 ; i < numFound ; i++ )
363 	{
364 		ent = entityList[i];
365 
366 		if ( ent->s.number == NPC->s.number || !ent->client || !ent->NPC || ent->health <= 0 || !ent->inuse )
367 		{
368 			continue;
369 		}
370 
371 		if ( ent->client->playerTeam == NPC->client->playerTeam || ent->client->playerTeam == TEAM_NEUTRAL ) // don't attack same team or bots
372 		{
373 			continue;
374 		}
375 
376 		// try to find the closest visible one
377 		if ( !NPC_ClearLOS( ent ))
378 		{
379 			continue;
380 		}
381 
382 		dis = DistanceHorizontalSquared( NPC->currentOrigin, ent->currentOrigin );
383 
384 		if ( dis <= bestDis )
385 		{
386 			bestDis = dis;
387 			best = ent;
388 		}
389 	}
390 
391 	if ( best )
392 	{
393 		// used to offset seekers around a circle so they don't occupy the same spot.  This is not a fool-proof method.
394 		NPC->random = Q_flrand(0.0f, 1.0f) * 6.3f; // roughly 2pi
395 
396 		NPC->enemy = best;
397 	}
398 }
399 
400 //------------------------------------
Seeker_FollowPlayer(void)401 void Seeker_FollowPlayer( void )
402 {
403 	Seeker_MaintainHeight();
404 
405 	float	dis	= DistanceHorizontalSquared( NPC->currentOrigin, g_entities[0].currentOrigin );
406 	vec3_t	pt, dir;
407 
408 	if ( dis < MIN_DISTANCE_SQR )
409 	{
410 		// generally circle the player closely till we take an enemy..this is our target point
411 		pt[0] = g_entities[0].currentOrigin[0] + cos( level.time * 0.001f + NPC->random ) * 56;
412 		pt[1] = g_entities[0].currentOrigin[1] + sin( level.time * 0.001f + NPC->random ) * 56;
413 		pt[2] = g_entities[0].currentOrigin[2] + 40;
414 
415 		VectorSubtract( pt, NPC->currentOrigin, dir );
416 		VectorMA( NPC->client->ps.velocity, 0.8f, dir, NPC->client->ps.velocity );
417 	}
418 	else
419 	{
420 		if ( TIMER_Done( NPC, "seekerhiss" ))
421 		{
422 			TIMER_Set( NPC, "seekerhiss", 1000 + Q_flrand(0.0f, 1.0f) * 1000 );
423 			G_Sound( NPC, G_SoundIndex( "sound/chars/seeker/misc/hiss" ));
424 		}
425 
426 		// Hey come back!
427 		NPCInfo->goalEntity = &g_entities[0];
428 		NPCInfo->goalRadius = 32;
429 		NPC_MoveToGoal( qtrue );
430 		NPC->owner = &g_entities[0];
431 	}
432 
433 	if ( NPCInfo->enemyCheckDebounceTime < level.time )
434 	{
435 		// check twice a second to find a new enemy
436 		Seeker_FindEnemy();
437 		NPCInfo->enemyCheckDebounceTime = level.time + 500;
438 	}
439 
440 	NPC_UpdateAngles( qtrue, qtrue );
441 }
442 
443 //------------------------------------
NPC_BSSeeker_Default(void)444 void NPC_BSSeeker_Default( void )
445 {
446 	if ( in_camera )
447 	{
448 		// cameras make me commit suicide....
449 		G_Damage( NPC, NPC, NPC, NULL, NULL, 999, 0, MOD_UNKNOWN );
450 	}
451 
452 	if ( NPC->random == 0.0f )
453 	{
454 		// used to offset seekers around a circle so they don't occupy the same spot.  This is not a fool-proof method.
455 		NPC->random = Q_flrand(0.0f, 1.0f) * 6.3f; // roughly 2pi
456 	}
457 
458 	if ( NPC->enemy && NPC->enemy->health && NPC->enemy->inuse )
459 	{
460 		if ( NPC->enemy->s.number == 0 || ( NPC->enemy->client && NPC->enemy->client->NPC_class == CLASS_SEEKER ))
461 		{
462 			//hacked to never take the player as an enemy, even if the player shoots at it
463 			NPC->enemy = NULL;
464 		}
465 		else
466 		{
467 			Seeker_Attack();
468 			return;
469 		}
470 	}
471 
472 	// In all other cases, follow the player and look for enemies to take on
473 	Seeker_FollowPlayer();
474 }