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 #include "g_headers.h"
24 
25 #include "b_local.h"
26 #include "g_local.h"
27 #include "wp_saber.h"
28 #include "w_local.h"
29 #include "g_functions.h"
30 
31 //---------------------
32 //	Thermal Detonator
33 //---------------------
34 
35 //---------------------------------------------------------
thermalDetonatorExplode(gentity_t * ent)36 void thermalDetonatorExplode( gentity_t *ent )
37 //---------------------------------------------------------
38 {
39 	if ( !ent->count )
40 	{
41 		G_Sound( ent, G_SoundIndex( "sound/weapons/thermal/warning.wav" ) );
42 		ent->count = 1;
43 		ent->nextthink = level.time + 800;
44 		ent->svFlags |= SVF_BROADCAST;//so everyone hears/sees the explosion?
45 	}
46 	else
47 	{
48 		vec3_t	pos;
49 
50 		VectorSet( pos, ent->currentOrigin[0], ent->currentOrigin[1], ent->currentOrigin[2] + 8 );
51 
52 		ent->takedamage = qfalse; // don't allow double deaths!
53 
54 		G_RadiusDamage( ent->currentOrigin, ent->owner, weaponData[WP_THERMAL].splashDamage, weaponData[WP_THERMAL].splashRadius, NULL, MOD_EXPLOSIVE_SPLASH );
55 
56 		G_PlayEffect( "thermal/explosion", ent->currentOrigin );
57 		G_PlayEffect( "thermal/shockwave", ent->currentOrigin );
58 
59 		G_FreeEntity( ent );
60 	}
61 }
62 
63 //-------------------------------------------------------------------------------------------------------------
thermal_die(gentity_t * self,gentity_t * inflictor,gentity_t * attacker,int damage,int mod,int dFlags,int hitLoc)64 void thermal_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod, int dFlags, int hitLoc )
65 //-------------------------------------------------------------------------------------------------------------
66 {
67 	thermalDetonatorExplode( self );
68 }
69 
70 //---------------------------------------------------------
WP_LobFire(gentity_t * self,vec3_t start,vec3_t target,vec3_t mins,vec3_t maxs,int clipmask,vec3_t velocity,qboolean tracePath,int ignoreEntNum,int enemyNum,float minSpeed,float maxSpeed,float idealSpeed,qboolean mustHit)71 qboolean WP_LobFire( gentity_t *self, vec3_t start, vec3_t target, vec3_t mins, vec3_t maxs, int clipmask,
72 				vec3_t velocity, qboolean tracePath, int ignoreEntNum, int enemyNum,
73 				float minSpeed, float maxSpeed, float idealSpeed, qboolean mustHit )
74 //---------------------------------------------------------
75 {
76 	float	targetDist, shotSpeed, speedInc = 100, travelTime, impactDist, bestImpactDist = Q3_INFINITE;//fireSpeed,
77 	vec3_t	targetDir, shotVel, failCase = { 0.0f };
78 	trace_t	trace;
79 	trajectory_t	tr;
80 	qboolean	blocked;
81 	int		elapsedTime, skipNum, timeStep = 500, hitCount = 0, maxHits = 7;
82 	vec3_t	lastPos, testPos;
83 	gentity_t	*traceEnt;
84 
85 	if ( !idealSpeed )
86 	{
87 		idealSpeed = 300;
88 	}
89 	else if ( idealSpeed < speedInc )
90 	{
91 		idealSpeed = speedInc;
92 	}
93 	shotSpeed = idealSpeed;
94 	skipNum = (idealSpeed-speedInc)/speedInc;
95 	if ( !minSpeed )
96 	{
97 		minSpeed = 100;
98 	}
99 	if ( !maxSpeed )
100 	{
101 		maxSpeed = 900;
102 	}
103 	while ( hitCount < maxHits )
104 	{
105 		VectorSubtract( target, start, targetDir );
106 		targetDist = VectorNormalize( targetDir );
107 
108 		VectorScale( targetDir, shotSpeed, shotVel );
109 		travelTime = targetDist/shotSpeed;
110 		shotVel[2] += travelTime * 0.5 * g_gravity->value;
111 
112 		if ( !hitCount )
113 		{//save the first (ideal) one as the failCase (fallback value)
114 			if ( !mustHit )
115 			{//default is fine as a return value
116 				VectorCopy( shotVel, failCase );
117 			}
118 		}
119 
120 		if ( tracePath )
121 		{//do a rough trace of the path
122 			blocked = qfalse;
123 
124 			VectorCopy( start, tr.trBase );
125 			VectorCopy( shotVel, tr.trDelta );
126 			tr.trType = TR_GRAVITY;
127 			tr.trTime = level.time;
128 			travelTime *= 1000.0f;
129 			VectorCopy( start, lastPos );
130 
131 			//This may be kind of wasteful, especially on long throws... use larger steps?  Divide the travelTime into a certain hard number of slices?  Trace just to apex and down?
132 			for ( elapsedTime = timeStep; elapsedTime < floor(travelTime)+timeStep; elapsedTime += timeStep )
133 			{
134 				if ( (float)elapsedTime > travelTime )
135 				{//cap it
136 					elapsedTime = floor( travelTime );
137 				}
138 				EvaluateTrajectory( &tr, level.time + elapsedTime, testPos );
139 				gi.trace( &trace, lastPos, mins, maxs, testPos, ignoreEntNum, clipmask, G2_NOCOLLIDE, 0 );
140 
141 				if ( trace.allsolid || trace.startsolid )
142 				{
143 					blocked = qtrue;
144 					break;
145 				}
146 				if ( trace.fraction < 1.0f )
147 				{//hit something
148 					if ( trace.entityNum == enemyNum )
149 					{//hit the enemy, that's perfect!
150 						break;
151 					}
152 					else if ( trace.plane.normal[2] > 0.7 && DistanceSquared( trace.endpos, target ) < 4096 )//hit within 64 of desired location, should be okay
153 					{//close enough!
154 						break;
155 					}
156 					else
157 					{//FIXME: maybe find the extents of this brush and go above or below it on next try somehow?
158 						impactDist = DistanceSquared( trace.endpos, target );
159 						if ( impactDist < bestImpactDist )
160 						{
161 							bestImpactDist = impactDist;
162 							VectorCopy( shotVel, failCase );
163 						}
164 						blocked = qtrue;
165 						//see if we should store this as the failCase
166 						if ( trace.entityNum < ENTITYNUM_WORLD )
167 						{//hit an ent
168 							traceEnt = &g_entities[trace.entityNum];
169 							if ( traceEnt && traceEnt->takedamage && !OnSameTeam( self, traceEnt ) )
170 							{//hit something breakable, so that's okay
171 								//we haven't found a clear shot yet so use this as the failcase
172 								VectorCopy( shotVel, failCase );
173 							}
174 						}
175 						break;
176 					}
177 				}
178 				if ( elapsedTime == floor( travelTime ) )
179 				{//reached end, all clear
180 					break;
181 				}
182 				else
183 				{
184 					//all clear, try next slice
185 					VectorCopy( testPos, lastPos );
186 				}
187 			}
188 			if ( blocked )
189 			{//hit something, adjust speed (which will change arc)
190 				hitCount++;
191 				shotSpeed = idealSpeed + ((hitCount-skipNum) * speedInc);//from min to max (skipping ideal)
192 				if ( hitCount >= skipNum )
193 				{//skip ideal since that was the first value we tested
194 					shotSpeed += speedInc;
195 				}
196 			}
197 			else
198 			{//made it!
199 				break;
200 			}
201 		}
202 		else
203 		{//no need to check the path, go with first calc
204 			break;
205 		}
206 	}
207 
208 	if ( hitCount >= maxHits )
209 	{//NOTE: worst case scenario, use the one that impacted closest to the target (or just use the first try...?)
210 		VectorCopy( failCase, velocity );
211 		return qfalse;
212 	}
213 	VectorCopy( shotVel, velocity );
214 	return qtrue;
215 }
216 
217 //---------------------------------------------------------
WP_ThermalThink(gentity_t * ent)218 void WP_ThermalThink( gentity_t *ent )
219 //---------------------------------------------------------
220 {
221 	int			count;
222 	qboolean	blow = qfalse;
223 
224 	// Thermal detonators for the player do occasional radius checks and blow up if there are entities in the blast radius
225 	//	This is done so that the main fire is actually useful as an attack.  We explode anyway after delay expires.
226 	if ( ent->delay > level.time )
227 	{
228 		//	Finally, we force it to bounce at least once before doing the special checks, otherwise it's just too easy for the player?
229 		if ( ent->has_bounced )
230 		{
231 			count = G_RadiusList( ent->currentOrigin, TD_TEST_RAD, ent, qtrue, ent_list );
232 
233 			for ( int i = 0; i < count; i++ )
234 			{
235 				if ( ent_list[i]->s.number == 0 )
236 				{
237 					// avoid deliberately blowing up next to the player, no matter how close any enemy is..
238 					//	...if the delay time expires though, there is no saving the player...muwhaaa haa ha
239 					blow = qfalse;
240 					break;
241 				}
242 				else if ( ent_list[i]->client && ent_list[i]->health > 0 )
243 				{
244 					// sometimes the ent_list order changes, so we should make sure that the player isn't anywhere in this list
245 					blow = qtrue;
246 				}
247 			}
248 		}
249 	}
250 	else
251 	{
252 		// our death time has arrived, even if nothing is near us
253 		blow = qtrue;
254 	}
255 
256 	if ( blow )
257 	{
258 		ent->e_ThinkFunc = thinkF_thermalDetonatorExplode;
259 		ent->nextthink = level.time + 50;
260 	}
261 	else
262 	{
263 		// we probably don't need to do this thinking logic very often...maybe this is fast enough?
264 		ent->nextthink = level.time + TD_THINK_TIME;
265 	}
266 }
267 
268 //---------------------------------------------------------
WP_FireThermalDetonator(gentity_t * ent,qboolean alt_fire)269 gentity_t *WP_FireThermalDetonator( gentity_t *ent, qboolean alt_fire )
270 //---------------------------------------------------------
271 {
272 	gentity_t	*bolt;
273 	vec3_t		dir, start;
274 	float		damageScale = 1.0f;
275 
276 	VectorCopy( wpFwd, dir );
277 	VectorCopy( wpMuzzle, start );
278 
279 	bolt = G_Spawn();
280 
281 	bolt->classname = "thermal_detonator";
282 
283 	if ( ent->s.number != 0 )
284 	{
285 		// If not the player, cut the damage a bit so we don't get pounded on so much
286 		damageScale = TD_NPC_DAMAGE_CUT;
287 	}
288 
289 	if ( !alt_fire && ent->s.number == 0 )
290 	{
291 		// Main fires for the players do a little bit of extra thinking
292 		bolt->e_ThinkFunc = thinkF_WP_ThermalThink;
293 		bolt->nextthink = level.time + TD_THINK_TIME;
294 		bolt->delay = level.time + TD_TIME; // How long 'til she blows
295 	}
296 	else
297 	{
298 		bolt->e_ThinkFunc = thinkF_thermalDetonatorExplode;
299 		bolt->nextthink = level.time + TD_TIME; // How long 'til she blows
300 	}
301 
302 	bolt->mass = 10;
303 
304 	// How 'bout we give this thing a size...
305 	VectorSet( bolt->mins, -4.0f, -4.0f, -4.0f );
306 	VectorSet( bolt->maxs, 4.0f, 4.0f, 4.0f );
307 	bolt->clipmask = MASK_SHOT;
308 	bolt->clipmask &= ~CONTENTS_CORPSE;
309 	bolt->contents = CONTENTS_SHOTCLIP;
310 	bolt->takedamage = qtrue;
311 	bolt->health = 15;
312 	bolt->e_DieFunc = dieF_thermal_die;
313 
314 	WP_TraceSetStart( ent, start, bolt->mins, bolt->maxs );//make sure our start point isn't on the other side of a wall
315 
316 	float chargeAmount = 1.0f; // default of full charge
317 
318 	if ( ent->client )
319 	{
320 		chargeAmount = level.time - ent->client->ps.weaponChargeTime;
321 	}
322 
323 	// get charge amount
324 	chargeAmount = chargeAmount / (float)TD_VELOCITY;
325 
326 	if ( chargeAmount > 1.0f )
327 	{
328 		chargeAmount = 1.0f;
329 	}
330 	else if ( chargeAmount < TD_MIN_CHARGE )
331 	{
332 		chargeAmount = TD_MIN_CHARGE;
333 	}
334 
335 	// normal ones bounce, alt ones explode on impact
336 	bolt->s.pos.trType = TR_GRAVITY;
337 	bolt->owner = ent;
338 	VectorScale( dir, TD_VELOCITY * chargeAmount, bolt->s.pos.trDelta );
339 
340 	if ( ent->health > 0 )
341 	{
342 		bolt->s.pos.trDelta[2] += 120;
343 
344 		if ( ent->NPC && ent->enemy )
345 		{//FIXME: we're assuming he's actually facing this direction...
346 			vec3_t	target;
347 
348 			VectorCopy( ent->enemy->currentOrigin, target );
349 			if ( target[2] <= start[2] )
350 			{
351 				vec3_t	vec;
352 				VectorSubtract( target, start, vec );
353 				VectorNormalize( vec );
354 				VectorMA( target, Q_flrand( 0, -32 ), vec, target );//throw a little short
355 			}
356 
357 			target[0] += Q_flrand( -5, 5 )+(Q_flrand(-1.0f, 1.0f)*(6-ent->NPC->currentAim)*2);
358 			target[1] += Q_flrand( -5, 5 )+(Q_flrand(-1.0f, 1.0f)*(6-ent->NPC->currentAim)*2);
359 			target[2] += Q_flrand( -5, 5 )+(Q_flrand(-1.0f, 1.0f)*(6-ent->NPC->currentAim)*2);
360 
361 			WP_LobFire( ent, start, target, bolt->mins, bolt->maxs, bolt->clipmask, bolt->s.pos.trDelta, qtrue, ent->s.number, ent->enemy->s.number );
362 		}
363 	}
364 
365 	if ( alt_fire )
366 	{
367 		bolt->alt_fire = qtrue;
368 	}
369 	else
370 	{
371 		bolt->s.eFlags |= EF_BOUNCE_HALF;
372 	}
373 
374 	bolt->s.loopSound = G_SoundIndex( "sound/weapons/thermal/thermloop.wav" );
375 
376 	bolt->damage = weaponData[WP_THERMAL].damage * damageScale;
377 	bolt->dflags = 0;
378 	bolt->splashDamage = weaponData[WP_THERMAL].splashDamage * damageScale;
379 	bolt->splashRadius = weaponData[WP_THERMAL].splashRadius;
380 
381 	bolt->s.eType = ET_MISSILE;
382 	bolt->svFlags = SVF_USE_CURRENT_ORIGIN;
383 	bolt->s.weapon = WP_THERMAL;
384 
385 	if ( alt_fire )
386 	{
387 		bolt->methodOfDeath = MOD_THERMAL_ALT;
388 		bolt->splashMethodOfDeath = MOD_THERMAL_ALT;//? SPLASH;
389 	}
390 	else
391 	{
392 		bolt->methodOfDeath = MOD_THERMAL;
393 		bolt->splashMethodOfDeath = MOD_THERMAL;//? SPLASH;
394 	}
395 
396 	bolt->s.pos.trTime = level.time;		// move a bit on the very first frame
397 	VectorCopy( start, bolt->s.pos.trBase );
398 
399 	SnapVector( bolt->s.pos.trDelta );			// save net bandwidth
400 	VectorCopy (start, bolt->currentOrigin);
401 
402 	VectorCopy( start, bolt->pos2 );
403 
404 	return bolt;
405 }
406 
407 //---------------------------------------------------------
WP_DropThermal(gentity_t * ent)408 gentity_t *WP_DropThermal( gentity_t *ent )
409 //---------------------------------------------------------
410 {
411 	AngleVectors( ent->client->ps.viewangles, wpFwd, wpVright, wpUp );
412 	CalcEntitySpot( ent, SPOT_WEAPON, wpMuzzle );
413 	return (WP_FireThermalDetonator( ent, qfalse ));
414 }