mirror of
https://github.com/DrBeef/JKXR.git
synced 2024-11-29 23:42:38 +00:00
492 lines
15 KiB
C++
492 lines
15 KiB
C++
/*
|
|
===========================================================================
|
|
Copyright (C) 2000 - 2013, Raven Software, Inc.
|
|
Copyright (C) 2001 - 2013, Activision, Inc.
|
|
Copyright (C) 2013 - 2015, OpenJK contributors
|
|
|
|
This file is part of the OpenJK source code.
|
|
|
|
OpenJK is free software; you can redistribute it and/or modify it
|
|
under the terms of the GNU General Public License version 2 as
|
|
published by the Free Software Foundation.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
===========================================================================
|
|
*/
|
|
|
|
#include "g_local.h"
|
|
#include "b_local.h"
|
|
#include "g_functions.h"
|
|
#include "wp_saber.h"
|
|
#include "w_local.h"
|
|
#include "bg_local.h"
|
|
#include <JKXR/VrClientInfo.h>
|
|
#include <JKXR/mathlib.h>
|
|
|
|
//---------------------
|
|
// Thermal Detonator
|
|
//---------------------
|
|
|
|
//---------------------------------------------------------
|
|
void thermalDetonatorExplode( gentity_t *ent )
|
|
//---------------------------------------------------------
|
|
{
|
|
if ( (ent->s.eFlags&EF_HELD_BY_SAND_CREATURE) )
|
|
{
|
|
ent->takedamage = qfalse; // don't allow double deaths!
|
|
|
|
G_Damage( ent->activator, ent, ent->owner, vec3_origin, ent->currentOrigin, weaponData[WP_THERMAL].altDamage, 0, MOD_EXPLOSIVE );
|
|
G_PlayEffect( "thermal/explosion", ent->currentOrigin );
|
|
G_PlayEffect( "thermal/shockwave", ent->currentOrigin );
|
|
|
|
G_FreeEntity( ent );
|
|
}
|
|
else if ( !ent->count )
|
|
{
|
|
G_Sound( ent, G_SoundIndex( "sound/weapons/thermal/warning.wav" ) );
|
|
ent->count = 1;
|
|
ent->nextthink = level.time + 800;
|
|
ent->svFlags |= SVF_BROADCAST;//so everyone hears/sees the explosion?
|
|
}
|
|
else
|
|
{
|
|
vec3_t pos;
|
|
|
|
VectorSet( pos, ent->currentOrigin[0], ent->currentOrigin[1], ent->currentOrigin[2] + 8 );
|
|
|
|
ent->takedamage = qfalse; // don't allow double deaths!
|
|
|
|
G_RadiusDamage( ent->currentOrigin, ent->owner, weaponData[WP_THERMAL].splashDamage, weaponData[WP_THERMAL].splashRadius, NULL, MOD_EXPLOSIVE_SPLASH );
|
|
|
|
G_PlayEffect( "thermal/explosion", ent->currentOrigin );
|
|
G_PlayEffect( "thermal/shockwave", ent->currentOrigin );
|
|
|
|
G_FreeEntity( ent );
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------------------
|
|
void thermal_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod, int dFlags, int hitLoc )
|
|
//-------------------------------------------------------------------------------------------------------------
|
|
{
|
|
thermalDetonatorExplode( self );
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
qboolean 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 )
|
|
//---------------------------------------------------------
|
|
{
|
|
float targetDist, shotSpeed, speedInc = 100, travelTime, impactDist, bestImpactDist = Q3_INFINITE;//fireSpeed,
|
|
vec3_t targetDir, shotVel, failCase = { 0.0f };
|
|
trace_t trace;
|
|
trajectory_t tr;
|
|
qboolean blocked;
|
|
int elapsedTime, skipNum, timeStep = 500, hitCount = 0, maxHits = 7;
|
|
vec3_t lastPos, testPos;
|
|
gentity_t *traceEnt;
|
|
|
|
if ( !idealSpeed )
|
|
{
|
|
idealSpeed = 300;
|
|
}
|
|
else if ( idealSpeed < speedInc )
|
|
{
|
|
idealSpeed = speedInc;
|
|
}
|
|
shotSpeed = idealSpeed;
|
|
skipNum = (idealSpeed-speedInc)/speedInc;
|
|
if ( !minSpeed )
|
|
{
|
|
minSpeed = 100;
|
|
}
|
|
if ( !maxSpeed )
|
|
{
|
|
maxSpeed = 900;
|
|
}
|
|
while ( hitCount < maxHits )
|
|
{
|
|
VectorSubtract( target, start, targetDir );
|
|
targetDist = VectorNormalize( targetDir );
|
|
|
|
VectorScale( targetDir, shotSpeed, shotVel );
|
|
travelTime = targetDist/shotSpeed;
|
|
shotVel[2] += travelTime * 0.5 * g_gravity->value;
|
|
|
|
if ( !hitCount )
|
|
{//save the first (ideal) one as the failCase (fallback value)
|
|
if ( !mustHit )
|
|
{//default is fine as a return value
|
|
VectorCopy( shotVel, failCase );
|
|
}
|
|
}
|
|
|
|
if ( tracePath )
|
|
{//do a rough trace of the path
|
|
blocked = qfalse;
|
|
|
|
VectorCopy( start, tr.trBase );
|
|
VectorCopy( shotVel, tr.trDelta );
|
|
tr.trType = TR_GRAVITY;
|
|
tr.trTime = level.time;
|
|
travelTime *= 1000.0f;
|
|
VectorCopy( start, lastPos );
|
|
|
|
//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?
|
|
for ( elapsedTime = timeStep; elapsedTime < floor(travelTime)+timeStep; elapsedTime += timeStep )
|
|
{
|
|
if ( (float)elapsedTime > travelTime )
|
|
{//cap it
|
|
elapsedTime = floor( travelTime );
|
|
}
|
|
EvaluateTrajectory( &tr, level.time + elapsedTime, testPos );
|
|
gi.trace( &trace, lastPos, mins, maxs, testPos, ignoreEntNum, clipmask, (EG2_Collision)0, 0 );
|
|
|
|
if ( trace.allsolid || trace.startsolid )
|
|
{
|
|
blocked = qtrue;
|
|
break;
|
|
}
|
|
if ( trace.fraction < 1.0f )
|
|
{//hit something
|
|
if ( trace.entityNum == enemyNum )
|
|
{//hit the enemy, that's perfect!
|
|
break;
|
|
}
|
|
else if ( trace.plane.normal[2] > 0.7 && DistanceSquared( trace.endpos, target ) < 4096 )//hit within 64 of desired location, should be okay
|
|
{//close enough!
|
|
break;
|
|
}
|
|
else
|
|
{//FIXME: maybe find the extents of this brush and go above or below it on next try somehow?
|
|
impactDist = DistanceSquared( trace.endpos, target );
|
|
if ( impactDist < bestImpactDist )
|
|
{
|
|
bestImpactDist = impactDist;
|
|
VectorCopy( shotVel, failCase );
|
|
}
|
|
blocked = qtrue;
|
|
//see if we should store this as the failCase
|
|
if ( trace.entityNum < ENTITYNUM_WORLD )
|
|
{//hit an ent
|
|
traceEnt = &g_entities[trace.entityNum];
|
|
if ( traceEnt && traceEnt->takedamage && !OnSameTeam( self, traceEnt ) )
|
|
{//hit something breakable, so that's okay
|
|
//we haven't found a clear shot yet so use this as the failcase
|
|
VectorCopy( shotVel, failCase );
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if ( elapsedTime == floor( travelTime ) )
|
|
{//reached end, all clear
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
//all clear, try next slice
|
|
VectorCopy( testPos, lastPos );
|
|
}
|
|
}
|
|
if ( blocked )
|
|
{//hit something, adjust speed (which will change arc)
|
|
hitCount++;
|
|
shotSpeed = idealSpeed + ((hitCount-skipNum) * speedInc);//from min to max (skipping ideal)
|
|
if ( hitCount >= skipNum )
|
|
{//skip ideal since that was the first value we tested
|
|
shotSpeed += speedInc;
|
|
}
|
|
}
|
|
else
|
|
{//made it!
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{//no need to check the path, go with first calc
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( hitCount >= maxHits )
|
|
{//NOTE: worst case scenario, use the one that impacted closest to the target (or just use the first try...?)
|
|
assert( (failCase[0] + failCase[1] + failCase[2]) > 0.0f );
|
|
VectorCopy( failCase, velocity );
|
|
return qfalse;
|
|
}
|
|
VectorCopy( shotVel, velocity );
|
|
return qtrue;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
void WP_ThermalThink( gentity_t *ent )
|
|
//---------------------------------------------------------
|
|
{
|
|
int count;
|
|
qboolean blow = qfalse;
|
|
|
|
// Thermal detonators for the player do occasional radius checks and blow up if there are entities in the blast radius
|
|
// This is done so that the main fire is actually useful as an attack. We explode anyway after delay expires.
|
|
|
|
if ( (ent->s.eFlags&EF_HELD_BY_SAND_CREATURE) )
|
|
{//blow once creature is underground (done with anim)
|
|
//FIXME: chance of being spit out? Especially if lots of delay left...
|
|
ent->e_TouchFunc = touchF_NULL;//don't impact on anything
|
|
if ( !ent->activator
|
|
|| !ent->activator->client
|
|
|| !ent->activator->client->ps.legsAnimTimer )
|
|
{//either something happened to the sand creature or it's done with it's attack anim
|
|
//blow!
|
|
ent->e_ThinkFunc = thinkF_thermalDetonatorExplode;
|
|
ent->nextthink = level.time + Q_irand( 50, 2000 );
|
|
}
|
|
else
|
|
{//keep checking
|
|
ent->nextthink = level.time + TD_THINK_TIME;
|
|
}
|
|
return;
|
|
}
|
|
else if ( ent->delay > level.time )
|
|
{
|
|
// Finally, we force it to bounce at least once before doing the special checks, otherwise it's just too easy for the player?
|
|
if ( ent->has_bounced )
|
|
{
|
|
count = G_RadiusList( ent->currentOrigin, TD_TEST_RAD, ent, qtrue, ent_list );
|
|
|
|
for ( int i = 0; i < count; i++ )
|
|
{
|
|
if ( ent_list[i]->s.number == 0 )
|
|
{
|
|
// avoid deliberately blowing up next to the player, no matter how close any enemy is..
|
|
// ...if the delay time expires though, there is no saving the player...muwhaaa haa ha
|
|
blow = qfalse;
|
|
break;
|
|
}
|
|
else if ( ent_list[i]->client
|
|
&& ent_list[i]->client->NPC_class != CLASS_SAND_CREATURE//ignore sand creatures
|
|
&& ent_list[i]->health > 0 )
|
|
{
|
|
//FIXME! sometimes the ent_list order changes, so we should make sure that the player isn't anywhere in this list
|
|
blow = qtrue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// our death time has arrived, even if nothing is near us
|
|
blow = qtrue;
|
|
}
|
|
|
|
if ( blow )
|
|
{
|
|
ent->e_ThinkFunc = thinkF_thermalDetonatorExplode;
|
|
ent->nextthink = level.time + 50;
|
|
}
|
|
else
|
|
{
|
|
// we probably don't need to do this thinking logic very often...maybe this is fast enough?
|
|
ent->nextthink = level.time + TD_THINK_TIME;
|
|
}
|
|
}
|
|
|
|
#define OLDEST_READING 5
|
|
#define NEWEST_READING 2
|
|
#define TD_REAL_THROW_VEL_MULT 4.4f
|
|
|
|
//---------------------------------------------------------
|
|
gentity_t *WP_FireThermalDetonator( gentity_t *ent, qboolean alt_fire )
|
|
//---------------------------------------------------------
|
|
{
|
|
gentity_t *bolt;
|
|
vec3_t dir, start;
|
|
float damageScale = 1.0f;
|
|
|
|
bolt = G_Spawn();
|
|
|
|
bool realThrow = false;
|
|
if ( BG_UseVRPosition(ent) )
|
|
{
|
|
vec3_t angs;
|
|
BG_CalculateVRWeaponPosition(start, angs);
|
|
|
|
//Caclulate speed between two controller position readings
|
|
float distance = VectorDistance(vr->weaponoffset_history[NEWEST_READING], vr->weaponoffset_history[OLDEST_READING]);
|
|
float t = vr->weaponoffset_history_timestamp[NEWEST_READING] - vr->weaponoffset_history_timestamp[OLDEST_READING];
|
|
float velocity = distance / (t/(float)1000.0);
|
|
|
|
//Calculate trajectory
|
|
VectorSubtract(vr->weaponoffset_history[NEWEST_READING], vr->weaponoffset_history[OLDEST_READING], dir);
|
|
VectorNormalize( dir );
|
|
BG_ConvertFromVR(dir, NULL, dir);
|
|
VectorScale( dir, velocity * TD_REAL_THROW_VEL_MULT, bolt->s.pos.trDelta );
|
|
realThrow = true;
|
|
}
|
|
else {
|
|
VectorCopy( forwardVec, dir );
|
|
VectorCopy( muzzle, start );
|
|
}
|
|
|
|
bolt->classname = "thermal_detonator";
|
|
|
|
if ( ent->s.number != 0 )
|
|
{
|
|
// If not the player, cut the damage a bit so we don't get pounded on so much
|
|
damageScale = TD_NPC_DAMAGE_CUT;
|
|
}
|
|
|
|
if ( !alt_fire && ent->s.number == 0 )
|
|
{
|
|
// Main fires for the players do a little bit of extra thinking
|
|
bolt->e_ThinkFunc = thinkF_WP_ThermalThink;
|
|
bolt->nextthink = level.time + TD_THINK_TIME;
|
|
bolt->delay = level.time + TD_TIME; // How long 'til she blows
|
|
}
|
|
else
|
|
{
|
|
bolt->e_ThinkFunc = thinkF_thermalDetonatorExplode;
|
|
bolt->nextthink = level.time + TD_TIME; // How long 'til she blows
|
|
}
|
|
|
|
bolt->mass = 10;
|
|
|
|
// How 'bout we give this thing a size...
|
|
VectorSet( bolt->mins, -4.0f, -4.0f, -4.0f );
|
|
VectorSet( bolt->maxs, 4.0f, 4.0f, 4.0f );
|
|
bolt->clipmask = MASK_SHOT;
|
|
bolt->clipmask &= ~CONTENTS_CORPSE;
|
|
bolt->contents = CONTENTS_SHOTCLIP;
|
|
bolt->takedamage = qtrue;
|
|
bolt->health = 15;
|
|
bolt->e_DieFunc = dieF_thermal_die;
|
|
|
|
WP_TraceSetStart( ent, start, bolt->mins, bolt->maxs );//make sure our start point isn't on the other side of a wall
|
|
|
|
float chargeAmount = 1.0f; // default of full charge
|
|
|
|
if ( ent->client )
|
|
{
|
|
chargeAmount = level.time - ent->client->ps.weaponChargeTime;
|
|
}
|
|
|
|
// get charge amount
|
|
chargeAmount = chargeAmount / (float)TD_VELOCITY;
|
|
|
|
if ( chargeAmount > 1.0f )
|
|
{
|
|
chargeAmount = 1.0f;
|
|
}
|
|
else if ( chargeAmount < TD_MIN_CHARGE )
|
|
{
|
|
chargeAmount = TD_MIN_CHARGE;
|
|
}
|
|
|
|
float thrownSpeed = TD_VELOCITY;
|
|
const qboolean thisIsAShooter = (qboolean)!Q_stricmp( "misc_weapon_shooter", ent->classname);
|
|
|
|
if (thisIsAShooter)
|
|
{
|
|
if (ent->delay != 0)
|
|
{
|
|
thrownSpeed = ent->delay;
|
|
}
|
|
}
|
|
|
|
// normal ones bounce, alt ones explode on impact
|
|
bolt->s.pos.trType = TR_GRAVITY;
|
|
bolt->owner = ent;
|
|
if (!realThrow) {
|
|
VectorScale( dir, thrownSpeed * chargeAmount, bolt->s.pos.trDelta );
|
|
}
|
|
|
|
if ( ent->health > 0 )
|
|
{
|
|
if (!realThrow) {
|
|
bolt->s.pos.trDelta[2] += 120;
|
|
}
|
|
|
|
if ( (ent->NPC || (ent->s.number && thisIsAShooter) )
|
|
&& ent->enemy )
|
|
{//NPC or misc_weapon_shooter
|
|
//FIXME: we're assuming he's actually facing this direction...
|
|
vec3_t target;
|
|
|
|
VectorCopy( ent->enemy->currentOrigin, target );
|
|
if ( target[2] <= start[2] )
|
|
{
|
|
vec3_t vec;
|
|
VectorSubtract( target, start, vec );
|
|
VectorNormalize( vec );
|
|
VectorMA( target, Q_flrand( 0, -32 ), vec, target );//throw a little short
|
|
}
|
|
|
|
target[0] += Q_flrand( -5, 5 )+(Q_flrand(-1.0f, 1.0f)*(6-ent->NPC->currentAim)*2);
|
|
target[1] += Q_flrand( -5, 5 )+(Q_flrand(-1.0f, 1.0f)*(6-ent->NPC->currentAim)*2);
|
|
target[2] += Q_flrand( -5, 5 )+(Q_flrand(-1.0f, 1.0f)*(6-ent->NPC->currentAim)*2);
|
|
|
|
WP_LobFire( ent, start, target, bolt->mins, bolt->maxs, bolt->clipmask, bolt->s.pos.trDelta, qtrue, ent->s.number, ent->enemy->s.number );
|
|
}
|
|
else if ( thisIsAShooter && ent->target && !VectorCompare( ent->pos1, vec3_origin ) )
|
|
{//misc_weapon_shooter firing at a position
|
|
WP_LobFire( ent, start, ent->pos1, bolt->mins, bolt->maxs, bolt->clipmask, bolt->s.pos.trDelta, qtrue, ent->s.number, ent->enemy->s.number );
|
|
}
|
|
}
|
|
|
|
if ( alt_fire )
|
|
{
|
|
bolt->alt_fire = qtrue;
|
|
}
|
|
else
|
|
{
|
|
bolt->s.eFlags |= EF_BOUNCE_HALF;
|
|
}
|
|
|
|
bolt->s.loopSound = G_SoundIndex( "sound/weapons/thermal/thermloop.wav" );
|
|
|
|
bolt->damage = weaponData[WP_THERMAL].damage * damageScale;
|
|
bolt->dflags = 0;
|
|
bolt->splashDamage = weaponData[WP_THERMAL].splashDamage * damageScale;
|
|
bolt->splashRadius = weaponData[WP_THERMAL].splashRadius;
|
|
|
|
bolt->s.eType = ET_MISSILE;
|
|
bolt->svFlags = SVF_USE_CURRENT_ORIGIN;
|
|
bolt->s.weapon = WP_THERMAL;
|
|
|
|
if ( alt_fire )
|
|
{
|
|
bolt->methodOfDeath = MOD_THERMAL_ALT;
|
|
bolt->splashMethodOfDeath = MOD_THERMAL_ALT;//? SPLASH;
|
|
}
|
|
else
|
|
{
|
|
bolt->methodOfDeath = MOD_THERMAL;
|
|
bolt->splashMethodOfDeath = MOD_THERMAL;//? SPLASH;
|
|
}
|
|
|
|
bolt->s.pos.trTime = level.time; // move a bit on the very first frame
|
|
VectorCopy( start, bolt->s.pos.trBase );
|
|
|
|
SnapVector( bolt->s.pos.trDelta ); // save net bandwidth
|
|
VectorCopy (start, bolt->currentOrigin);
|
|
|
|
VectorCopy( start, bolt->pos2 );
|
|
|
|
return bolt;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
gentity_t *WP_DropThermal( gentity_t *ent )
|
|
//---------------------------------------------------------
|
|
{
|
|
AngleVectors( ent->client->ps.viewangles, forwardVec, vrightVec, up );
|
|
CalcEntitySpot( ent, SPOT_WEAPON, muzzle );
|
|
return (WP_FireThermalDetonator( ent, qfalse ));
|
|
}
|