jkxr/Projects/Android/jni/OpenJK/code/game/wp_thermal.cpp
2023-03-08 22:56:21 +00:00

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 ));
}