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 }