mirror of
https://github.com/DrBeef/JKXR.git
synced 2025-02-03 22:11:01 +00:00
0723c2335e
g_saberRealisticCombat should be set to "1" g_dismemberment should be set to "4" g_saberMoreRealistic is no longer used changed logic so that it should be the same approach for dealing damage as JKO with the above settings when TBDC is enabled fixed saber bounce in JKA too
14663 lines
468 KiB
C++
14663 lines
468 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 "anims.h"
|
|
#include "b_local.h"
|
|
#include "bg_local.h"
|
|
#include "g_functions.h"
|
|
#include "wp_saber.h"
|
|
#include "g_vehicles.h"
|
|
#include "../qcommon/tri_coll_test.h"
|
|
#include "../cgame/cg_local.h"
|
|
#include <JKXR/VrClientInfo.h>
|
|
|
|
#define JK2_RAGDOLL_GRIPNOHEALTH
|
|
|
|
#define MAX_SABER_VICTIMS 16
|
|
static int victimEntityNum[MAX_SABER_VICTIMS];
|
|
static float totalDmg[MAX_SABER_VICTIMS];
|
|
static vec3_t dmgDir[MAX_SABER_VICTIMS];
|
|
static vec3_t dmgNormal[MAX_SABER_VICTIMS];
|
|
static vec3_t dmgBladeVec[MAX_SABER_VICTIMS];
|
|
static vec3_t dmgSpot[MAX_SABER_VICTIMS];
|
|
static float dmgFraction[MAX_SABER_VICTIMS];
|
|
static int hitLoc[MAX_SABER_VICTIMS];
|
|
static qboolean hitDismember[MAX_SABER_VICTIMS];
|
|
static int hitDismemberLoc[MAX_SABER_VICTIMS];
|
|
static vec3_t saberHitLocation, saberHitNormal={0,0,1.0};
|
|
static float saberHitFraction;
|
|
static float sabersCrossed;
|
|
static int saberHitEntity;
|
|
static int numVictims = 0;
|
|
|
|
extern cvar_t *g_sex;
|
|
extern cvar_t *g_timescale;
|
|
extern cvar_t *g_dismemberment;
|
|
extern cvar_t *g_debugSaberLock;
|
|
extern cvar_t *g_saberLockRandomNess;
|
|
extern cvar_t *d_slowmodeath;
|
|
extern cvar_t *g_cheats;
|
|
extern cvar_t *g_debugMelee;
|
|
extern cvar_t *g_saberRestrictForce;
|
|
extern cvar_t *g_saberPickuppableDroppedSabers;
|
|
extern cvar_t *debug_subdivision;
|
|
|
|
|
|
extern qboolean WP_SaberBladeUseSecondBladeStyle( saberInfo_t *saber, int bladeNum );
|
|
extern qboolean WP_SaberBladeDoTransitionDamage( saberInfo_t *saber, int bladeNum );
|
|
extern qboolean Q3_TaskIDPending( gentity_t *ent, taskID_t taskType );
|
|
extern qboolean G_ClearViewEntity( gentity_t *ent );
|
|
extern void G_SetViewEntity( gentity_t *self, gentity_t *viewEntity );
|
|
extern qboolean G_ControlledByPlayer( gentity_t *self );
|
|
extern void G_AddVoiceEvent( gentity_t *self, int event, int speakDebounceTime );
|
|
extern void CG_ChangeWeapon( int num );
|
|
extern void CG_SaberDoWeaponHitMarks( gclient_t *client, gentity_t *saberEnt, gentity_t *hitEnt, int saberNum, int bladeNum, vec3_t hitPos, vec3_t hitDir, vec3_t uaxis, vec3_t splashBackDir, float sizeTimeScale );
|
|
extern void G_AngerAlert( gentity_t *self );
|
|
extern void G_ReflectMissile( gentity_t *ent, gentity_t *missile, vec3_t forward );
|
|
extern int G_CheckLedgeDive( gentity_t *self, float checkDist, const vec3_t checkVel, qboolean tryOpposite, qboolean tryPerp );
|
|
extern void G_BounceMissile( gentity_t *ent, trace_t *trace );
|
|
extern qboolean G_PointInBounds( const vec3_t point, const vec3_t mins, const vec3_t maxs );
|
|
extern void NPC_UseResponse( gentity_t *self, gentity_t *user, qboolean useWhenDone );
|
|
extern void WP_FireDreadnoughtBeam( gentity_t *ent );
|
|
extern void G_MissileImpacted( gentity_t *ent, gentity_t *other, vec3_t impactPos, vec3_t normal, int hitLoc=HL_NONE );
|
|
extern evasionType_t Jedi_SaberBlockGo( gentity_t *self, usercmd_t *cmd, vec3_t pHitloc, vec3_t phitDir, gentity_t *incoming, float dist = 0.0f );
|
|
extern void Jedi_RageStop( gentity_t *self );
|
|
extern int PM_PickAnim( gentity_t *self, int minAnim, int maxAnim );
|
|
extern void NPC_SetPainEvent( gentity_t *self );
|
|
extern qboolean PM_SwimmingAnim( int anim );
|
|
extern qboolean PM_InAnimForSaberMove( int anim, int saberMove );
|
|
extern qboolean PM_SpinningSaberAnim( int anim );
|
|
extern qboolean PM_SaberInSpecialAttack( int anim );
|
|
extern qboolean PM_SaberInAttack( int move );
|
|
extern qboolean PM_SaberInAttackPure( int move );
|
|
extern qboolean PM_SaberInTransition( int move );
|
|
extern qboolean PM_SaberInStart( int move );
|
|
extern qboolean PM_SaberInTransitionAny( int move );
|
|
extern qboolean PM_SaberInReturn( int move );
|
|
extern qboolean PM_SaberInBounce( int move );
|
|
extern qboolean PM_SaberInParry( int move );
|
|
extern qboolean PM_SaberInKnockaway( int move );
|
|
extern qboolean PM_SaberInBrokenParry( int move );
|
|
extern qboolean PM_SpinningSaberAnim( int anim );
|
|
extern saberMoveName_t PM_SaberBounceForAttack( int move );
|
|
extern saberMoveName_t PM_BrokenParryForAttack( int move );
|
|
extern saberMoveName_t PM_KnockawayForParry( int move );
|
|
extern qboolean PM_FlippingAnim( int anim );
|
|
extern qboolean PM_RollingAnim( int anim );
|
|
extern qboolean PM_CrouchAnim( int anim );
|
|
extern qboolean PM_SaberInIdle( int move );
|
|
extern qboolean PM_SaberInReflect( int move );
|
|
extern qboolean PM_InSpecialJump( int anim );
|
|
extern qboolean PM_InKnockDown( playerState_t *ps );
|
|
extern qboolean PM_ForceUsingSaberAnim( int anim );
|
|
extern qboolean PM_SuperBreakLoseAnim( int anim );
|
|
extern qboolean PM_SuperBreakWinAnim( int anim );
|
|
extern qboolean PM_SaberLockBreakAnim( int anim );
|
|
extern qboolean PM_InOnGroundAnim ( playerState_t *ps );
|
|
extern qboolean PM_KnockDownAnim( int anim );
|
|
extern qboolean PM_SaberInKata( saberMoveName_t saberMove );
|
|
extern qboolean PM_StabDownAnim( int anim );
|
|
extern int PM_PowerLevelForSaberAnim( playerState_t *ps, int saberNum = 0 );
|
|
extern void PM_VelocityForSaberMove( playerState_t *ps, vec3_t throwDir );
|
|
extern qboolean PM_VelocityForBlockedMove( playerState_t *ps, vec3_t throwDir );
|
|
extern qboolean PM_SaberCanInterruptMove( int move, int anim );
|
|
extern int Jedi_ReCalcParryTime( gentity_t *self, evasionType_t evasionType );
|
|
extern qboolean Jedi_DodgeEvasion( gentity_t *self, gentity_t *shooter, trace_t *tr, int hitLoc );
|
|
extern void Jedi_PlayDeflectSound( gentity_t *self );
|
|
extern void Jedi_PlayBlockedPushSound( gentity_t *self );
|
|
extern qboolean Jedi_WaitingAmbush( gentity_t *self );
|
|
extern void Jedi_Ambush( gentity_t *self );
|
|
extern qboolean Jedi_SaberBusy( gentity_t *self );
|
|
extern qboolean Jedi_CultistDestroyer( gentity_t *self );
|
|
extern qboolean Boba_Flying( gentity_t *self );
|
|
extern void JET_FlyStart( gentity_t *self );
|
|
extern void Boba_DoFlameThrower( gentity_t *self );
|
|
extern void Boba_StopFlameThrower( gentity_t *self );
|
|
|
|
extern Vehicle_t *G_IsRidingVehicle( gentity_t *ent );
|
|
extern int SaberDroid_PowerLevelForSaberAnim( gentity_t *self );
|
|
extern qboolean G_ValidEnemy( gentity_t *self, gentity_t *enemy );
|
|
extern void G_StartMatrixEffect( gentity_t *ent, int meFlags = 0, int length = 1000, float timeScale = 0.0f, int spinTime = 0 );
|
|
extern int PM_AnimLength( int index, animNumber_t anim );
|
|
extern void G_Knockdown( gentity_t *self, gentity_t *attacker, const vec3_t pushDir, float strength, qboolean breakSaberLock );
|
|
extern void G_KnockOffVehicle( gentity_t *pRider, gentity_t *self, qboolean bPull );
|
|
extern qboolean PM_LockedAnim( int anim );
|
|
extern qboolean Rosh_BeingHealed( gentity_t *self );
|
|
extern qboolean G_OkayToLean( playerState_t *ps, usercmd_t *cmd, qboolean interruptOkay );
|
|
|
|
int WP_AbsorbConversion(gentity_t *attacked, int atdAbsLevel, gentity_t *attacker, int atPower, int atPowerLevel, int atForceSpent);
|
|
void WP_ForcePowerStart( gentity_t *self, forcePowers_t forcePower, int overrideAmt );
|
|
void WP_ForcePowerStop( gentity_t *self, forcePowers_t forcePower );
|
|
qboolean WP_ForcePowerUsable( gentity_t *self, forcePowers_t forcePower, int overrideAmt );
|
|
void WP_SaberInFlightReflectCheck( gentity_t *self, usercmd_t *ucmd );
|
|
|
|
void WP_SaberDrop( gentity_t *self, gentity_t *saber );
|
|
qboolean WP_SaberLose( gentity_t *self, vec3_t throwDir );
|
|
void WP_SaberReturn( gentity_t *self, gentity_t *saber );
|
|
void WP_SaberBlockNonRandom( gentity_t *self, vec3_t hitloc, qboolean missileBlock );
|
|
qboolean WP_ForcePowerAvailable( gentity_t *self, forcePowers_t forcePower, int overrideAmt );
|
|
void WP_ForcePowerDrain( gentity_t *self, forcePowers_t forcePower, int overrideAmt );
|
|
void WP_DeactivateSaber( gentity_t *self, qboolean clearLength = qfalse );
|
|
qboolean FP_ForceDrainGrippableEnt( gentity_t *victim );
|
|
|
|
extern cvar_t *g_TeamBeefDirectorsCut;
|
|
|
|
extern cvar_t *g_saberAutoDeflect1stPerson;
|
|
extern cvar_t *g_saberAutoBlocking;
|
|
extern cvar_t *g_saberRealisticCombat;
|
|
extern cvar_t *g_saberDamageCapping;
|
|
extern cvar_t *g_saberNewControlScheme;
|
|
extern int g_crosshairEntNum;
|
|
|
|
qboolean g_saberNoEffects = qfalse;
|
|
qboolean g_noClashFlare = qfalse;
|
|
int g_saberFlashTime = 0;
|
|
vec3_t g_saberFlashPos = {0,0,0};
|
|
|
|
int forcePowerDarkLight[NUM_FORCE_POWERS] = //0 == neutral
|
|
{ //nothing should be usable at rank 0..
|
|
FORCE_LIGHTSIDE,//FP_HEAL,//instant
|
|
0,//FP_LEVITATION,//hold/duration
|
|
0,//FP_SPEED,//duration
|
|
0,//FP_PUSH,//hold/duration
|
|
0,//FP_PULL,//hold/duration
|
|
FORCE_LIGHTSIDE,//FP_TELEPATHY,//instant
|
|
FORCE_DARKSIDE,//FP_GRIP,//hold/duration
|
|
FORCE_DARKSIDE,//FP_LIGHTNING,//hold/duration
|
|
0,//FP_SABERATTACK,
|
|
0,//FP_SABERDEFEND,
|
|
0,//FP_SABERTHROW,
|
|
//new Jedi Academy powers
|
|
FORCE_DARKSIDE,//FP_RAGE,//duration
|
|
FORCE_LIGHTSIDE,//FP_PROTECT,//duration
|
|
FORCE_LIGHTSIDE,//FP_ABSORB,//duration
|
|
FORCE_DARKSIDE,//FP_DRAIN,//hold/duration
|
|
0,//FP_SEE,//duration
|
|
//NUM_FORCE_POWERS
|
|
};
|
|
|
|
int forcePowerNeeded[NUM_FORCE_POWERS] =
|
|
{
|
|
0,//FP_HEAL,//instant
|
|
10,//FP_LEVITATION,//hold/duration
|
|
50,//FP_SPEED,//duration
|
|
15,//FP_PUSH,//hold/duration
|
|
15,//FP_PULL,//hold/duration
|
|
20,//FP_TELEPATHY,//instant
|
|
1,//FP_GRIP,//hold/duration - FIXME: 30?
|
|
1,//FP_LIGHTNING,//hold/duration
|
|
20,//FP_SABERTHROW,
|
|
1,//FP_SABER_DEFENSE,
|
|
0,//FP_SABER_OFFENSE,
|
|
//new Jedi Academy powers
|
|
50,//FP_RAGE,//duration - speed, invincibility and extra damage for short period, drains your health and leaves you weak and slow afterwards.
|
|
30,//FP_PROTECT,//duration - protect against physical/energy (level 1 stops blaster/energy bolts, level 2 stops projectiles, level 3 protects against explosions)
|
|
30,//FP_ABSORB,//duration - protect against dark force powers (grip, lightning, drain)
|
|
1,//FP_DRAIN,//hold/duration - drain force power for health
|
|
20//FP_SEE,//duration - detect/see hidden enemies
|
|
//NUM_FORCE_POWERS
|
|
};
|
|
|
|
float forceJumpStrength[NUM_FORCE_POWER_LEVELS] =
|
|
{
|
|
JUMP_VELOCITY,//normal jump
|
|
420,
|
|
590,
|
|
840
|
|
};
|
|
|
|
float forceJumpHeight[NUM_FORCE_POWER_LEVELS] =
|
|
{
|
|
32,//normal jump (+stepheight+crouchdiff = 66)
|
|
96,//(+stepheight+crouchdiff = 130)
|
|
192,//(+stepheight+crouchdiff = 226)
|
|
384//(+stepheight+crouchdiff = 418)
|
|
};
|
|
|
|
float forceJumpHeightMax[NUM_FORCE_POWER_LEVELS] =
|
|
{
|
|
66,//normal jump (32+stepheight(18)+crouchdiff(24) = 74)
|
|
130,//(96+stepheight(18)+crouchdiff(24) = 138)
|
|
226,//(192+stepheight(18)+crouchdiff(24) = 234)
|
|
418//(384+stepheight(18)+crouchdiff(24) = 426)
|
|
};
|
|
|
|
float forcePushPullRadius[NUM_FORCE_POWER_LEVELS] =
|
|
{
|
|
0,//none
|
|
384,//256,
|
|
448,//384,
|
|
512
|
|
};
|
|
|
|
float forcePushCone[NUM_FORCE_POWER_LEVELS] =
|
|
{
|
|
1.0f,//none
|
|
1.0f,
|
|
0.8f,
|
|
0.6f
|
|
};
|
|
|
|
float forcePullCone[NUM_FORCE_POWER_LEVELS] =
|
|
{
|
|
1.0f,//none
|
|
1.0f,
|
|
1.0f,
|
|
0.8f
|
|
};
|
|
|
|
float forceSpeedValue[NUM_FORCE_POWER_LEVELS] =
|
|
{
|
|
1.0f,//none
|
|
0.75f,
|
|
0.5f,
|
|
0.25f
|
|
};
|
|
|
|
float forceSpeedRangeMod[NUM_FORCE_POWER_LEVELS] =
|
|
{
|
|
0.0f,//none
|
|
30.0f,
|
|
45.0f,
|
|
60.0f
|
|
};
|
|
|
|
float forceSpeedFOVMod[NUM_FORCE_POWER_LEVELS] =
|
|
{
|
|
0.0f,//none
|
|
20.0f,
|
|
30.0f,
|
|
40.0f
|
|
};
|
|
|
|
int forceGripDamage[NUM_FORCE_POWER_LEVELS] =
|
|
{
|
|
0,//none
|
|
0,
|
|
6,
|
|
9
|
|
};
|
|
|
|
int mindTrickTime[NUM_FORCE_POWER_LEVELS] =
|
|
{
|
|
0,//none
|
|
10000,//5000,
|
|
15000,//10000,
|
|
30000//15000
|
|
};
|
|
|
|
//NOTE: keep in synch with table below!!!
|
|
int saberThrowDist[NUM_FORCE_POWER_LEVELS] =
|
|
{
|
|
0,//none
|
|
256,
|
|
400,
|
|
400
|
|
};
|
|
|
|
//NOTE: keep in synch with table above!!!
|
|
int saberThrowDistSquared[NUM_FORCE_POWER_LEVELS] =
|
|
{
|
|
0,//none
|
|
65536,
|
|
160000,
|
|
160000
|
|
};
|
|
|
|
int parryDebounce[NUM_FORCE_POWER_LEVELS] =
|
|
{
|
|
500,//if don't even have defense, can't use defense!
|
|
300,
|
|
150,
|
|
50
|
|
};
|
|
|
|
float saberAnimSpeedMod[NUM_FORCE_POWER_LEVELS] =
|
|
{
|
|
0.0f,//if don't even have offense, can't use offense!
|
|
0.75f,
|
|
1.0f,
|
|
2.0f
|
|
};
|
|
|
|
stringID_table_t SaberStyleTable[] =
|
|
{
|
|
{ "NULL",SS_NONE },
|
|
ENUM2STRING(SS_FAST),
|
|
{ "fast",SS_FAST },
|
|
ENUM2STRING(SS_MEDIUM),
|
|
{ "medium",SS_MEDIUM },
|
|
ENUM2STRING(SS_STRONG),
|
|
{ "strong",SS_STRONG },
|
|
ENUM2STRING(SS_DESANN),
|
|
{ "desann",SS_DESANN },
|
|
ENUM2STRING(SS_TAVION),
|
|
{ "tavion",SS_TAVION },
|
|
ENUM2STRING(SS_DUAL),
|
|
{ "dual",SS_DUAL },
|
|
ENUM2STRING(SS_STAFF),
|
|
{ "staff",SS_STAFF },
|
|
{ "", 0 },
|
|
};
|
|
|
|
//SABER INITIALIZATION======================================================================
|
|
|
|
void G_CreateG2AttachedWeaponModel( gentity_t *ent, const char *psWeaponModel, int boltNum, int weaponNum )
|
|
{
|
|
if (!psWeaponModel)
|
|
{
|
|
assert (psWeaponModel);
|
|
return;
|
|
}
|
|
if ( ent->playerModel == -1 )
|
|
{
|
|
return;
|
|
}
|
|
if ( boltNum == -1 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( ent && ent->client && ent->client->NPC_class == CLASS_GALAKMECH )
|
|
{//hack for galakmech, no weaponmodel
|
|
ent->weaponModel[0] = ent->weaponModel[1] = -1;
|
|
return;
|
|
}
|
|
if ( weaponNum < 0 || weaponNum >= MAX_INHAND_WEAPONS )
|
|
{
|
|
return;
|
|
}
|
|
char weaponModel[64];
|
|
|
|
strcpy (weaponModel, psWeaponModel);
|
|
if (char *spot = strstr(weaponModel, ".md3") ) {
|
|
*spot = 0;
|
|
spot = strstr(weaponModel, "_w");//i'm using the in view weapon array instead of scanning the item list, so put the _w back on
|
|
if (!spot&&!strstr(weaponModel, "noweap"))
|
|
{
|
|
strcat (weaponModel, "_w");
|
|
}
|
|
strcat (weaponModel, ".glm"); //and change to ghoul2
|
|
}
|
|
|
|
// give us a saber model
|
|
int wModelIndex = G_ModelIndex( weaponModel );
|
|
if ( wModelIndex )
|
|
{
|
|
ent->weaponModel[weaponNum] = gi.G2API_InitGhoul2Model(ent->ghoul2, weaponModel, wModelIndex, NULL_HANDLE, NULL_HANDLE, 0, 0 );
|
|
if ( ent->weaponModel[weaponNum] != -1 )
|
|
{
|
|
// attach it to the hand
|
|
gi.G2API_AttachG2Model(&ent->ghoul2[ent->weaponModel[weaponNum]], &ent->ghoul2[ent->playerModel],
|
|
boltNum, ent->playerModel);
|
|
// set up a bolt on the end so we can get where the sabre muzzle is - we can assume this is always bolt 0
|
|
gi.G2API_AddBolt(&ent->ghoul2[ent->weaponModel[weaponNum]], "*flash");
|
|
//gi.G2API_SetLodBias( &ent->ghoul2[ent->weaponModel[weaponNum]], 0 );
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void WP_SaberAddG2SaberModels( gentity_t *ent, int specificSaberNum )
|
|
{
|
|
int saberNum = 0, maxSaber = 1;
|
|
if ( specificSaberNum != -1 && specificSaberNum <= maxSaber )
|
|
{
|
|
saberNum = maxSaber = specificSaberNum;
|
|
}
|
|
for ( ; saberNum <= maxSaber; saberNum++ )
|
|
{
|
|
if ( ent->weaponModel[saberNum] > 0 )
|
|
{//we already have a weapon model in this slot
|
|
//remove it
|
|
gi.G2API_SetSkin( &ent->ghoul2[ent->weaponModel[saberNum]], -1, 0 );
|
|
gi.G2API_RemoveGhoul2Model( ent->ghoul2, ent->weaponModel[saberNum] );
|
|
ent->weaponModel[saberNum] = -1;
|
|
}
|
|
if ( saberNum > 0 )
|
|
{//second saber
|
|
if ( !ent->client->ps.dualSabers
|
|
|| G_IsRidingVehicle( ent ) )
|
|
{//only have one saber or riding a vehicle and can only use one saber
|
|
return;
|
|
}
|
|
}
|
|
else if ( saberNum == 0 )
|
|
{//first saber
|
|
if ( ent->client->ps.saberInFlight )
|
|
{//it's still out there somewhere, don't add it
|
|
//FIXME: call it back?
|
|
continue;
|
|
}
|
|
}
|
|
int handBolt = ((saberNum == 0) ? ent->handRBolt : ent->handLBolt);
|
|
if ( (ent->client->ps.saber[saberNum].saberFlags&SFL_BOLT_TO_WRIST) )
|
|
{//special case, bolt to forearm
|
|
if ( saberNum == 0 )
|
|
{
|
|
handBolt = gi.G2API_AddBolt( &ent->ghoul2[ent->playerModel], "*r_hand_cap_r_arm" );
|
|
}
|
|
else
|
|
{
|
|
handBolt = gi.G2API_AddBolt( &ent->ghoul2[ent->playerModel], "*l_hand_cap_l_arm" );
|
|
}
|
|
}
|
|
G_CreateG2AttachedWeaponModel( ent, ent->client->ps.saber[saberNum].model, handBolt, saberNum );
|
|
|
|
if ( ent->client->ps.saber[saberNum].skin != NULL )
|
|
{//if this saber has a customSkin, use it
|
|
// lets see if it's out there
|
|
int saberSkin = gi.RE_RegisterSkin( ent->client->ps.saber[saberNum].skin );
|
|
if ( saberSkin )
|
|
{
|
|
// put it in the config strings
|
|
// and set the ghoul2 model to use it
|
|
gi.G2API_SetSkin( &ent->ghoul2[ent->weaponModel[saberNum]], G_SkinIndex( ent->client->ps.saber[saberNum].skin ), saberSkin );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------
|
|
void G_Throw( gentity_t *targ, const vec3_t newDir, float push )
|
|
//----------------------------------------------------------
|
|
{
|
|
vec3_t kvel;
|
|
float mass;
|
|
|
|
if ( targ
|
|
&& targ->client
|
|
&& ( targ->client->NPC_class == CLASS_ATST
|
|
|| targ->client->NPC_class == CLASS_RANCOR
|
|
|| targ->client->NPC_class == CLASS_SAND_CREATURE ) )
|
|
{//much to large to *ever* throw
|
|
return;
|
|
}
|
|
|
|
if ( targ->physicsBounce > 0 ) //overide the mass
|
|
{
|
|
mass = targ->physicsBounce;
|
|
}
|
|
else
|
|
{
|
|
mass = 200;
|
|
}
|
|
|
|
if ( g_gravity->value > 0 )
|
|
{
|
|
VectorScale( newDir, g_knockback->value * (float)push / mass * 0.8, kvel );
|
|
if ( !targ->client || targ->client->ps.groundEntityNum != ENTITYNUM_NONE )
|
|
{//give them some z lift to get them off the ground
|
|
kvel[2] = newDir[2] * g_knockback->value * (float)push / mass * 1.5;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
VectorScale( newDir, g_knockback->value * (float)push / mass, kvel );
|
|
}
|
|
|
|
if ( targ->client )
|
|
{
|
|
VectorAdd( targ->client->ps.velocity, kvel, targ->client->ps.velocity );
|
|
}
|
|
else if ( targ->s.pos.trType != TR_STATIONARY && targ->s.pos.trType != TR_LINEAR_STOP && targ->s.pos.trType != TR_NONLINEAR_STOP )
|
|
{
|
|
VectorAdd( targ->s.pos.trDelta, kvel, targ->s.pos.trDelta );
|
|
VectorCopy( targ->currentOrigin, targ->s.pos.trBase );
|
|
targ->s.pos.trTime = level.time;
|
|
}
|
|
|
|
// set the timer so that the other client can't cancel
|
|
// out the movement immediately
|
|
if ( targ->client && !targ->client->ps.pm_time )
|
|
{
|
|
int t;
|
|
|
|
t = push * 2;
|
|
|
|
if ( t < 50 )
|
|
{
|
|
t = 50;
|
|
}
|
|
if ( t > 200 )
|
|
{
|
|
t = 200;
|
|
}
|
|
targ->client->ps.pm_time = t;
|
|
targ->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
|
|
}
|
|
}
|
|
|
|
int WP_SetSaberModel( gclient_t *client, class_t npcClass )
|
|
{//FIXME: read from NPCs.cfg
|
|
if ( client )
|
|
{
|
|
switch ( npcClass )
|
|
{
|
|
case CLASS_DESANN://Desann
|
|
client->ps.saber[0].model = "models/weapons2/saber_desann/saber_w.glm";
|
|
break;
|
|
case CLASS_LUKE://Luke
|
|
client->ps.saber[0].model = "models/weapons2/saber_luke/saber_w.glm";
|
|
break;
|
|
case CLASS_PLAYER://Kyle NPC and player
|
|
case CLASS_KYLE://Kyle NPC and player
|
|
client->ps.saber[0].model = "models/weapons2/saber/saber_w.glm";
|
|
break;
|
|
default://reborn and tavion and everyone else
|
|
client->ps.saber[0].model = "models/weapons2/saber_reborn/saber_w.glm";
|
|
break;
|
|
}
|
|
return ( G_ModelIndex( client->ps.saber[0].model ) );
|
|
}
|
|
else
|
|
{
|
|
switch ( npcClass )
|
|
{
|
|
case CLASS_DESANN://Desann
|
|
return ( G_ModelIndex( "models/weapons2/saber_desann/saber_w.glm" ) );
|
|
break;
|
|
case CLASS_LUKE://Luke
|
|
return ( G_ModelIndex( "models/weapons2/saber_luke/saber_w.glm" ) );
|
|
break;
|
|
case CLASS_PLAYER://Kyle NPC and player
|
|
case CLASS_KYLE://Kyle NPC and player
|
|
return ( G_ModelIndex( "models/weapons2/saber/saber_w.glm" ) );
|
|
break;
|
|
default://reborn and tavion and everyone else
|
|
return ( G_ModelIndex( "models/weapons2/saber_reborn/saber_w.glm" ) );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void WP_SetSaberEntModelSkin( gentity_t *ent, gentity_t *saberent )
|
|
{
|
|
int saberModel = 0;
|
|
qboolean newModel = qfalse;
|
|
//FIXME: get saberModel from NPCs.cfg
|
|
if ( !ent->client->ps.saber[0].model )
|
|
{
|
|
saberModel = WP_SetSaberModel( ent->client, ent->client->NPC_class );
|
|
}
|
|
else
|
|
{
|
|
//got saberModel from NPCs.cfg
|
|
saberModel = G_ModelIndex( ent->client->ps.saber[0].model );
|
|
}
|
|
if ( saberModel && saberent->s.modelindex != saberModel )
|
|
{
|
|
if ( saberent->playerModel >= 0 )
|
|
{//remove the old one, if there is one
|
|
gi.G2API_RemoveGhoul2Model( saberent->ghoul2, saberent->playerModel );
|
|
}
|
|
//add the new one
|
|
saberent->playerModel = gi.G2API_InitGhoul2Model( saberent->ghoul2, ent->client->ps.saber[0].model, saberModel, NULL_HANDLE, NULL_HANDLE, 0, 0);
|
|
saberent->s.modelindex = saberModel;
|
|
newModel = qtrue;
|
|
}
|
|
//set skin, too
|
|
if ( ent->client->ps.saber[0].skin == NULL )
|
|
{
|
|
gi.G2API_SetSkin( &saberent->ghoul2[0], -1, 0 );
|
|
}
|
|
else
|
|
{//if this saber has a customSkin, use it
|
|
// lets see if it's out there
|
|
int saberSkin = gi.RE_RegisterSkin( ent->client->ps.saber[0].skin );
|
|
if ( saberSkin && (newModel || saberent->s.modelindex2 != saberSkin) )
|
|
{
|
|
// put it in the config strings
|
|
// and set the ghoul2 model to use it
|
|
gi.G2API_SetSkin( &saberent->ghoul2[0], G_SkinIndex( ent->client->ps.saber[0].skin ), saberSkin );
|
|
saberent->s.modelindex2 = saberSkin;
|
|
}
|
|
}
|
|
}
|
|
|
|
void WP_SaberFallSound( gentity_t *owner, gentity_t *saber )
|
|
{
|
|
if ( !saber )
|
|
{
|
|
return;
|
|
}
|
|
if ( owner && owner->client )
|
|
{//have an owner, use their data (assume saberNum is 0 because only the 0 saber can be thrown)
|
|
if ( owner->client->ps.saber[0].fallSound[0] )
|
|
{//have an override
|
|
G_Sound( saber, owner->client->ps.saber[0].fallSound[Q_irand( 0, 2 )] );
|
|
}
|
|
else if ( owner->client->ps.saber[0].type == SABER_SITH_SWORD )
|
|
{//is a sith sword
|
|
G_Sound( saber, G_SoundIndex( va( "sound/weapons/sword/fall%d.wav", Q_irand( 1, 7 ) ) ) );
|
|
}
|
|
else
|
|
{//normal saber
|
|
G_Sound( saber, G_SoundIndex( va( "sound/weapons/saber/bounce%d.wav", Q_irand( 1, 3 ) ) ) );
|
|
}
|
|
}
|
|
else if ( saber->NPC_type && saber->NPC_type[0] )
|
|
{//have a saber name to look up
|
|
saberInfo_t saberInfo;
|
|
if ( WP_SaberParseParms( saber->NPC_type, &saberInfo ) )
|
|
{//found it
|
|
if ( saberInfo.fallSound[0] )
|
|
{//have an override sound
|
|
G_Sound( saber, saberInfo.fallSound[Q_irand( 0, 2 )] );
|
|
}
|
|
else if ( saberInfo.type == SABER_SITH_SWORD )
|
|
{//is a sith sword
|
|
G_Sound( saber, G_SoundIndex( va( "sound/weapons/sword/fall%d.wav", Q_irand( 1, 7 ) ) ) );
|
|
}
|
|
else
|
|
{//normal saber
|
|
G_Sound( saber, G_SoundIndex( va( "sound/weapons/saber/bounce%d.wav", Q_irand( 1, 3 ) ) ) );
|
|
}
|
|
}
|
|
else
|
|
{//can't find it
|
|
G_Sound( saber, G_SoundIndex( va( "sound/weapons/saber/bounce%d.wav", Q_irand( 1, 3 ) ) ) );
|
|
}
|
|
}
|
|
else
|
|
{//no saber name specified
|
|
G_Sound( saber, G_SoundIndex( va( "sound/weapons/saber/bounce%d.wav", Q_irand( 1, 3 ) ) ) );
|
|
}
|
|
}
|
|
|
|
void WP_SaberSwingSound( gentity_t *ent, int saberNum, swingType_t swingType )
|
|
{
|
|
int index = 1;
|
|
if ( !ent || !ent->client )
|
|
{
|
|
return;
|
|
}
|
|
if ( swingType == SWING_FAST )
|
|
{
|
|
index = Q_irand( 1, 3 );
|
|
}
|
|
else if ( swingType == SWING_MEDIUM )
|
|
{
|
|
index = Q_irand( 4, 6 );
|
|
}
|
|
else if ( swingType == SWING_STRONG )
|
|
{
|
|
index = Q_irand( 7, 9 );
|
|
}
|
|
|
|
if ( ent->client->ps.saber[saberNum].swingSound[0] )
|
|
{
|
|
G_SoundIndexOnEnt( ent, CHAN_WEAPON, ent->client->ps.saber[saberNum].swingSound[Q_irand( 0, 2 )] );
|
|
}
|
|
else if ( ent->client->ps.saber[saberNum].type == SABER_SITH_SWORD )
|
|
{
|
|
G_SoundOnEnt( ent, CHAN_WEAPON, va( "sound/weapons/sword/swing%d.wav", Q_irand( 1, 4 ) ) );
|
|
}
|
|
else
|
|
{
|
|
G_SoundOnEnt( ent, CHAN_WEAPON, va( "sound/weapons/saber/saberhup%d.wav", index ) );
|
|
}
|
|
}
|
|
|
|
void WP_SaberHitSound( gentity_t *ent, int saberNum, int bladeNum )
|
|
{
|
|
int index = 1;
|
|
if ( !ent || !ent->client )
|
|
{
|
|
return;
|
|
}
|
|
index = Q_irand( 1, 3 );
|
|
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].hitSound[0] )
|
|
{
|
|
G_Sound( ent, ent->client->ps.saber[saberNum].hitSound[Q_irand( 0, 2 )] );
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].hit2Sound[0] )
|
|
{
|
|
G_Sound( ent, ent->client->ps.saber[saberNum].hit2Sound[Q_irand( 0, 2 )] );
|
|
}
|
|
else if ( ent->client->ps.saber[saberNum].type == SABER_SITH_SWORD )
|
|
{
|
|
G_Sound( ent, G_SoundIndex( va( "sound/weapons/sword/stab%d.wav", Q_irand( 1, 4 ) ) ) );
|
|
}
|
|
else
|
|
{
|
|
G_Sound( ent, G_SoundIndex( va( "sound/weapons/saber/saberhit%d.wav", index ) ) );
|
|
}
|
|
}
|
|
|
|
void WP_SaberBlockSound( gentity_t *ent, gentity_t *hitEnt, int saberNum, int bladeNum )
|
|
{
|
|
int index = 1;
|
|
if ( !ent || !ent->client )
|
|
{
|
|
return;
|
|
}
|
|
index = Q_irand( 1, 9 );
|
|
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].blockSound[0] )
|
|
{
|
|
G_Sound( ent, ent->client->ps.saber[saberNum].blockSound[Q_irand( 0, 2 )] );
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].block2Sound[0] )
|
|
{
|
|
G_Sound( ent, ent->client->ps.saber[saberNum].block2Sound[Q_irand( 0, 2 )] );
|
|
}
|
|
else
|
|
{
|
|
G_Sound( ent, G_SoundIndex( va( "sound/weapons/saber/saberblock%d.wav", index ) ) );
|
|
}
|
|
}
|
|
|
|
|
|
void WP_SaberBounceOnWallSound( gentity_t *ent, int saberNum, int bladeNum )
|
|
{
|
|
int index = 1;
|
|
if ( !ent || !ent->client )
|
|
{
|
|
return;
|
|
}
|
|
index = Q_irand( 1, 9 );
|
|
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].bounceSound[0] )
|
|
{
|
|
G_Sound( ent, ent->client->ps.saber[saberNum].bounceSound[Q_irand( 0, 2 )] );
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].bounce2Sound[0] )
|
|
{
|
|
G_Sound( ent, ent->client->ps.saber[saberNum].bounce2Sound[Q_irand( 0, 2 )] );
|
|
}
|
|
else if ( !WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].blockSound[0] )
|
|
{
|
|
G_Sound( ent, ent->client->ps.saber[saberNum].blockSound[Q_irand( 0, 2 )] );
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].block2Sound[0] )
|
|
{
|
|
G_Sound( ent, ent->client->ps.saber[saberNum].block2Sound[Q_irand( 0, 2 )] );
|
|
}
|
|
else
|
|
{
|
|
G_Sound( ent, G_SoundIndex( va( "sound/weapons/saber/saberblock%d.wav", index ) ) );
|
|
}
|
|
}
|
|
|
|
void WP_SaberBounceSound( gentity_t *ent, gentity_t *hitEnt, gentity_t *playOnEnt, int saberNum, int bladeNum, qboolean doForce )
|
|
{
|
|
int index = 1;
|
|
if ( !ent || !ent->client )
|
|
{
|
|
return;
|
|
}
|
|
index = Q_irand( 1, 3 );
|
|
|
|
if ( !playOnEnt )
|
|
{
|
|
playOnEnt = ent;
|
|
}
|
|
//NOTE: we don't allow overriding of the saberbounce sound, but since it's just a variant on the saberblock sound, we use that as the override
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].blockSound[0] )
|
|
{
|
|
G_Sound( playOnEnt, ent->client->ps.saber[saberNum].blockSound[Q_irand( 0, 2 )] );
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].block2Sound[0] )
|
|
{
|
|
G_Sound( playOnEnt, ent->client->ps.saber[saberNum].block2Sound[Q_irand( 0, 2 )] );
|
|
}
|
|
else
|
|
{
|
|
G_Sound( playOnEnt, G_SoundIndex( va( "sound/weapons/saber/saberbounce%d.wav", index ) ) );
|
|
}
|
|
}
|
|
|
|
int WP_SaberInitBladeData( gentity_t *ent )
|
|
{
|
|
if ( !ent->client )
|
|
{
|
|
return 0;
|
|
}
|
|
if ( 1 )
|
|
{
|
|
VectorClear( ent->client->renderInfo.muzzlePoint );
|
|
VectorClear( ent->client->renderInfo.muzzlePointOld );
|
|
//VectorClear( ent->client->renderInfo.muzzlePointNext );
|
|
VectorClear( ent->client->renderInfo.muzzleDir );
|
|
VectorClear( ent->client->renderInfo.muzzleDirOld );
|
|
//VectorClear( ent->client->renderInfo.muzzleDirNext );
|
|
for ( int saberNum = 0; saberNum < MAX_SABERS; saberNum++ )
|
|
{
|
|
for ( int bladeNum = 0; bladeNum < MAX_BLADES; bladeNum++ )
|
|
{
|
|
VectorClear( ent->client->ps.saber[saberNum].blade[bladeNum].muzzlePoint );
|
|
VectorClear( ent->client->ps.saber[saberNum].blade[bladeNum].muzzlePointOld );
|
|
VectorClear( ent->client->ps.saber[saberNum].blade[bladeNum].muzzleDir );
|
|
VectorClear( ent->client->ps.saber[saberNum].blade[bladeNum].muzzleDirOld );
|
|
ent->client->ps.saber[saberNum].blade[bladeNum].lengthOld = ent->client->ps.saber[saberNum].blade[bladeNum].length = 0;
|
|
if ( !ent->client->ps.saber[saberNum].blade[bladeNum].lengthMax )
|
|
{
|
|
if ( ent->client->NPC_class == CLASS_DESANN )
|
|
{//longer saber
|
|
ent->client->ps.saber[saberNum].blade[bladeNum].lengthMax = 48;
|
|
}
|
|
else if ( ent->client->NPC_class == CLASS_REBORN )
|
|
{//shorter saber
|
|
ent->client->ps.saber[saberNum].blade[bladeNum].lengthMax = 32;
|
|
}
|
|
else
|
|
{//standard saber length
|
|
ent->client->ps.saber[saberNum].blade[bladeNum].lengthMax = 40;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ent->client->ps.saberLockEnemy = ENTITYNUM_NONE;
|
|
ent->client->ps.saberLockTime = 0;
|
|
if ( ent->s.number )
|
|
{
|
|
if ( !ent->client->ps.saberAnimLevel )
|
|
{
|
|
if ( ent->client->NPC_class == CLASS_DESANN )
|
|
{
|
|
ent->client->ps.saberAnimLevel = SS_DESANN;
|
|
}
|
|
else if ( ent->client->NPC_class == CLASS_TAVION )
|
|
{
|
|
ent->client->ps.saberAnimLevel = SS_TAVION;
|
|
}
|
|
else if ( ent->client->NPC_class == CLASS_ALORA )
|
|
{
|
|
ent->client->ps.saberAnimLevel = SS_DUAL;
|
|
}
|
|
//FIXME: CLASS_CULTIST instead of this Q_stricmpn?
|
|
else if ( !Q_stricmpn( "cultist", ent->NPC_type, 7 ) )
|
|
{//should already be set in the .npc file
|
|
ent->client->ps.saberAnimLevel = Q_irand( SS_FAST, SS_STRONG );
|
|
}
|
|
else if ( ent->NPC && ent->client->playerTeam == TEAM_ENEMY && (ent->NPC->rank == RANK_CIVILIAN || ent->NPC->rank == RANK_LT_JG) )
|
|
{//grunt and fencer always uses quick attacks
|
|
ent->client->ps.saberAnimLevel = SS_FAST;
|
|
}
|
|
else if ( ent->NPC && ent->client->playerTeam == TEAM_ENEMY && (ent->NPC->rank == RANK_CREWMAN || ent->NPC->rank == RANK_ENSIGN) )
|
|
{//acrobat & force-users always use medium attacks
|
|
ent->client->ps.saberAnimLevel = SS_MEDIUM;
|
|
}
|
|
else if ( ent->client->playerTeam == TEAM_ENEMY && ent->client->NPC_class == CLASS_SHADOWTROOPER )
|
|
{//shadowtroopers
|
|
ent->client->ps.saberAnimLevel = Q_irand( SS_FAST, SS_STRONG );
|
|
}
|
|
else if ( ent->NPC && ent->client->playerTeam == TEAM_ENEMY && ent->NPC->rank == RANK_LT )
|
|
{//boss always starts with strong attacks
|
|
ent->client->ps.saberAnimLevel = SS_STRONG;
|
|
}
|
|
else if ( ent->client->NPC_class == CLASS_PLAYER )
|
|
{
|
|
ent->client->ps.saberAnimLevel = g_entities[0].client->ps.saberAnimLevel;
|
|
}
|
|
else
|
|
{//?
|
|
ent->client->ps.saberAnimLevel = Q_irand( SS_FAST, SS_STRONG );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( !ent->client->ps.saberAnimLevel )
|
|
{//initialize, but don't reset
|
|
if (ent->s.number < MAX_CLIENTS)
|
|
{
|
|
if (!ent->client->ps.saberStylesKnown)
|
|
{
|
|
ent->client->ps.saberStylesKnown = (1<<SS_MEDIUM);
|
|
}
|
|
|
|
|
|
if (ent->client->ps.saberStylesKnown & (1<<SS_FAST))
|
|
{
|
|
ent->client->ps.saberAnimLevel = SS_FAST;
|
|
}
|
|
else if (ent->client->ps.saberStylesKnown & (1<<SS_STRONG))
|
|
{
|
|
ent->client->ps.saberAnimLevel = SS_STRONG;
|
|
}
|
|
else
|
|
{
|
|
ent->client->ps.saberAnimLevel = SS_MEDIUM;
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
ent->client->ps.saberAnimLevel = SS_MEDIUM;
|
|
}
|
|
}
|
|
|
|
cg.saberAnimLevelPending = ent->client->ps.saberAnimLevel;
|
|
if ( ent->client->sess.missionStats.weaponUsed[WP_SABER] <= 0 )
|
|
{//let missionStats know that we actually do have the saber, even if we never use it
|
|
ent->client->sess.missionStats.weaponUsed[WP_SABER] = 1;
|
|
}
|
|
}
|
|
ent->client->ps.saberAttackChainCount = 0;
|
|
|
|
if ( ent->client->ps.saberEntityNum <= 0 || ent->client->ps.saberEntityNum >= ENTITYNUM_WORLD )
|
|
{//FIXME: if you do have a saber already, be sure to re-set the model if it's changed (say, via a script).
|
|
gentity_t *saberent = G_Spawn();
|
|
ent->client->ps.saberEntityNum = saberent->s.number;
|
|
saberent->classname = "lightsaber";
|
|
|
|
saberent->s.eType = ET_GENERAL;
|
|
saberent->svFlags = SVF_USE_CURRENT_ORIGIN;
|
|
saberent->s.weapon = WP_SABER;
|
|
saberent->owner = ent;
|
|
saberent->s.otherEntityNum = ent->s.number;
|
|
//clear the enemy
|
|
saberent->enemy = NULL;
|
|
|
|
saberent->clipmask = MASK_SOLID | CONTENTS_LIGHTSABER;
|
|
saberent->contents = CONTENTS_LIGHTSABER;//|CONTENTS_SHOTCLIP;
|
|
|
|
VectorSet( saberent->mins, -3.0f, -3.0f, -3.0f );
|
|
VectorSet( saberent->maxs, 3.0f, 3.0f, 3.0f );
|
|
saberent->mass = 10;//necc?
|
|
|
|
saberent->s.eFlags |= EF_NODRAW;
|
|
saberent->svFlags |= SVF_NOCLIENT;
|
|
/*
|
|
Ghoul2 Insert Start
|
|
*/
|
|
saberent->playerModel = -1;
|
|
WP_SetSaberEntModelSkin( ent, saberent );
|
|
|
|
// set up a bolt on the end so we can get where the sabre muzzle is - we can assume this is always bolt 0
|
|
gi.G2API_AddBolt( &saberent->ghoul2[0], "*flash" );
|
|
//gi.G2API_SetLodBias( &saberent->ghoul2[0], 0 );
|
|
if ( ent->client->ps.dualSabers )
|
|
{
|
|
int saber2 = G_ModelIndex( ent->client->ps.saber[1].model );
|
|
//gi.G2API_InitGhoul2Model( saberent->ghoul2, ent->client->ps.saber[1].model, saber2 );
|
|
// set up a bolt on the end so we can get where the sabre muzzle is - we can assume this is always bolt 0
|
|
//gi.G2API_AddBolt( &saberent->ghoul2[0], "*flash" );
|
|
//gi.G2API_SetLodBias( &saberent->ghoul2[0], 0 );
|
|
}
|
|
|
|
/*
|
|
Ghoul2 Insert End
|
|
*/
|
|
|
|
ent->client->ps.saberInFlight = qfalse;
|
|
ent->client->ps.saberEntityDist = 0;
|
|
ent->client->ps.saberEntityState = SES_LEAVING;
|
|
|
|
ent->client->ps.saberMove = ent->client->ps.saberMoveNext = LS_NONE;
|
|
|
|
//FIXME: need a think function to create alerts when turned on or is on, etc.
|
|
}
|
|
else
|
|
{//already have one, might just be changing sabers, register the model and skin and use them if different from what we're using now.
|
|
WP_SetSaberEntModelSkin( ent, &g_entities[ent->client->ps.saberEntityNum] );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ent->client->ps.saberEntityNum = ENTITYNUM_NONE;
|
|
ent->client->ps.saberInFlight = qfalse;
|
|
ent->client->ps.saberEntityDist = 0;
|
|
ent->client->ps.saberEntityState = SES_LEAVING;
|
|
}
|
|
|
|
if ( ent->client->ps.dualSabers )
|
|
{
|
|
return 2;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
void WP_SaberUpdateOldBladeData( gentity_t *ent )
|
|
{
|
|
if ( ent->client )
|
|
{
|
|
qboolean didEvent = qfalse;
|
|
for ( int saberNum = 0; saberNum < 2; saberNum++ )
|
|
{
|
|
for ( int bladeNum = 0; bladeNum < ent->client->ps.saber[saberNum].numBlades; bladeNum++ )
|
|
{
|
|
VectorCopy( ent->client->ps.saber[saberNum].blade[bladeNum].muzzlePoint, ent->client->ps.saber[saberNum].blade[bladeNum].muzzlePointOld );
|
|
VectorCopy( ent->client->ps.saber[saberNum].blade[bladeNum].muzzleDir, ent->client->ps.saber[saberNum].blade[bladeNum].muzzleDirOld );
|
|
if ( !didEvent )
|
|
{
|
|
if ( ent->client->ps.saber[saberNum].blade[bladeNum].lengthOld <= 0 && ent->client->ps.saber[saberNum].blade[bladeNum].length > 0 )
|
|
{//just turned on
|
|
//do sound event
|
|
vec3_t saberOrg;
|
|
VectorCopy( g_entities[ent->client->ps.saberEntityNum].currentOrigin, saberOrg );
|
|
if ( (!ent->client->ps.saberInFlight && ent->client->ps.groundEntityNum == ENTITYNUM_WORLD)//holding saber and on ground
|
|
|| g_entities[ent->client->ps.saberEntityNum].s.pos.trType == TR_STATIONARY )//saber out there somewhere and on ground
|
|
{//a ground alert
|
|
AddSoundEvent( ent, saberOrg, 256, AEL_SUSPICIOUS, qfalse, qtrue );
|
|
}
|
|
else
|
|
{//an in-air alert
|
|
AddSoundEvent( ent, saberOrg, 256, AEL_SUSPICIOUS );
|
|
}
|
|
didEvent = qtrue;
|
|
}
|
|
}
|
|
ent->client->ps.saber[saberNum].blade[bladeNum].lengthOld = ent->client->ps.saber[saberNum].blade[bladeNum].length;
|
|
}
|
|
}
|
|
VectorCopy( ent->client->renderInfo.muzzlePoint, ent->client->renderInfo.muzzlePointOld );
|
|
VectorCopy( ent->client->renderInfo.muzzleDir, ent->client->renderInfo.muzzleDirOld );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//SABER DAMAGE==============================================================================
|
|
//SABER DAMAGE==============================================================================
|
|
//SABER DAMAGE==============================================================================
|
|
//SABER DAMAGE==============================================================================
|
|
//SABER DAMAGE==============================================================================
|
|
//SABER DAMAGE==============================================================================
|
|
int WPDEBUG_SaberColor( saber_colors_t saberColor )
|
|
{
|
|
switch( (int)(saberColor) )
|
|
{
|
|
case SABER_RED:
|
|
return 0x000000ff;
|
|
break;
|
|
case SABER_ORANGE:
|
|
return 0x000088ff;
|
|
break;
|
|
case SABER_YELLOW:
|
|
return 0x0000ffff;
|
|
break;
|
|
case SABER_GREEN:
|
|
return 0x0000ff00;
|
|
break;
|
|
case SABER_BLUE:
|
|
return 0x00ff0000;
|
|
break;
|
|
case SABER_PURPLE:
|
|
return 0x00ff00ff;
|
|
break;
|
|
default:
|
|
return 0x00ffffff;//white
|
|
break;
|
|
}
|
|
}
|
|
|
|
qboolean WP_GetSaberDeflectionAngle( gentity_t *attacker, gentity_t *defender )
|
|
{
|
|
vec3_t temp, att_SaberBase, att_StartPos, saberMidNext, att_HitDir, att_HitPos, def_BladeDir;
|
|
float att_SaberHitLength, hitDot;
|
|
|
|
if ( !attacker || !attacker->client || attacker->client->ps.saberInFlight || attacker->client->ps.SaberLength() <= 0 )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( !defender || !defender->client || defender->client->ps.saberInFlight || defender->client->ps.SaberLength() <= 0 )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( PM_SuperBreakLoseAnim( attacker->client->ps.torsoAnim )
|
|
|| PM_SuperBreakWinAnim( attacker->client->ps.torsoAnim ) )
|
|
{
|
|
return qfalse;
|
|
}
|
|
attacker->client->ps.saberBounceMove = LS_NONE;
|
|
|
|
//get the attacker's saber base pos at time of impact
|
|
VectorSubtract( attacker->client->renderInfo.muzzlePoint, attacker->client->renderInfo.muzzlePointOld, temp );
|
|
VectorMA( attacker->client->renderInfo.muzzlePointOld, saberHitFraction, temp, att_SaberBase );
|
|
|
|
//get the position along the length of the blade where the hit occured
|
|
att_SaberHitLength = Distance( saberHitLocation, att_SaberBase )/attacker->client->ps.SaberLength();
|
|
|
|
//now get the start of that midpoint in the swing and the actual impact point in the swing (shouldn't the latter just be saberHitLocation?)
|
|
VectorMA( attacker->client->renderInfo.muzzlePointOld, att_SaberHitLength, attacker->client->renderInfo.muzzleDirOld, att_StartPos );
|
|
VectorMA( attacker->client->renderInfo.muzzlePoint, att_SaberHitLength, attacker->client->renderInfo.muzzleDir, saberMidNext );
|
|
VectorSubtract( saberMidNext, att_StartPos, att_HitDir );
|
|
VectorMA( att_StartPos, saberHitFraction, att_HitDir, att_HitPos );
|
|
VectorNormalize( att_HitDir );
|
|
|
|
//get the defender's saber dir at time of impact
|
|
VectorSubtract( defender->client->renderInfo.muzzleDirOld, defender->client->renderInfo.muzzleDir, temp );
|
|
VectorMA( defender->client->renderInfo.muzzleDirOld, saberHitFraction, temp, def_BladeDir );
|
|
|
|
//now compare
|
|
hitDot = DotProduct( att_HitDir, def_BladeDir );
|
|
if ( hitDot < 0.25f && hitDot > -0.25f )
|
|
{//hit pretty much perpendicular, pop straight back
|
|
attacker->client->ps.saberBounceMove = PM_SaberBounceForAttack( attacker->client->ps.saberMove );
|
|
return qfalse;
|
|
}
|
|
else
|
|
{//a deflection
|
|
vec3_t att_Right, att_Up, att_DeflectionDir;
|
|
float swingRDot, swingUDot;
|
|
|
|
//get the direction of the deflection
|
|
VectorScale( def_BladeDir, hitDot, att_DeflectionDir );
|
|
//get our bounce straight back direction
|
|
VectorScale( att_HitDir, -1.0f, temp );
|
|
//add the bounce back and deflection
|
|
VectorAdd( att_DeflectionDir, temp, att_DeflectionDir );
|
|
//normalize the result to determine what direction our saber should bounce back toward
|
|
VectorNormalize( att_DeflectionDir );
|
|
|
|
//need to know the direction of the deflectoin relative to the attacker's facing
|
|
VectorSet( temp, 0, attacker->client->ps.viewangles[YAW], 0 );//presumes no pitch!
|
|
AngleVectors( temp, NULL, att_Right, att_Up );
|
|
swingRDot = DotProduct( att_Right, att_DeflectionDir );
|
|
swingUDot = DotProduct( att_Up, att_DeflectionDir );
|
|
|
|
if ( swingRDot > 0.25f )
|
|
{//deflect to right
|
|
if ( swingUDot > 0.25f )
|
|
{//deflect to top
|
|
attacker->client->ps.saberBounceMove = LS_D1_TR;
|
|
}
|
|
else if ( swingUDot < -0.25f )
|
|
{//deflect to bottom
|
|
attacker->client->ps.saberBounceMove = LS_D1_BR;
|
|
}
|
|
else
|
|
{//deflect horizontally
|
|
attacker->client->ps.saberBounceMove = LS_D1__R;
|
|
}
|
|
}
|
|
else if ( swingRDot < -0.25f )
|
|
{//deflect to left
|
|
if ( swingUDot > 0.25f )
|
|
{//deflect to top
|
|
attacker->client->ps.saberBounceMove = LS_D1_TL;
|
|
}
|
|
else if ( swingUDot < -0.25f )
|
|
{//deflect to bottom
|
|
attacker->client->ps.saberBounceMove = LS_D1_BL;
|
|
}
|
|
else
|
|
{//deflect horizontally
|
|
attacker->client->ps.saberBounceMove = LS_D1__L;
|
|
}
|
|
}
|
|
else
|
|
{//deflect in middle
|
|
if ( swingUDot > 0.25f )
|
|
{//deflect to top
|
|
attacker->client->ps.saberBounceMove = LS_D1_T_;
|
|
}
|
|
else if ( swingUDot < -0.25f )
|
|
{//deflect to bottom
|
|
attacker->client->ps.saberBounceMove = LS_D1_B_;
|
|
}
|
|
else
|
|
{//deflect horizontally? Well, no such thing as straight back in my face, so use top
|
|
if ( swingRDot > 0 )
|
|
{
|
|
attacker->client->ps.saberBounceMove = LS_D1_TR;
|
|
}
|
|
else if ( swingRDot < 0 )
|
|
{
|
|
attacker->client->ps.saberBounceMove = LS_D1_TL;
|
|
}
|
|
else
|
|
{
|
|
attacker->client->ps.saberBounceMove = LS_D1_T_;
|
|
}
|
|
}
|
|
}
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer )
|
|
{
|
|
gi.Printf( S_COLOR_BLUE"%s deflected from %s to %s\n", attacker->targetname, saberMoveData[attacker->client->ps.saberMove].name, saberMoveData[attacker->client->ps.saberBounceMove].name );
|
|
}
|
|
#endif
|
|
return qtrue;
|
|
}
|
|
}
|
|
|
|
|
|
void WP_SaberClearDamageForEntNum( gentity_t *attacker, int entityNum, int saberNum, int bladeNum )
|
|
{
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer )
|
|
{
|
|
if ( entityNum )
|
|
{
|
|
Com_Printf( "clearing damage for entnum %d\n", entityNum );
|
|
}
|
|
}
|
|
#endif// FINAL_BUILD
|
|
if ( g_saberRealisticCombat->integer > 1 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
//FIXME: if hit their saber in WP_SaberDamageForTrace, need to still do knockback on them...
|
|
float knockBackScale = 0.0f;
|
|
if ( attacker && attacker->client )
|
|
{
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( &attacker->client->ps.saber[saberNum], bladeNum )
|
|
&& attacker->client->ps.saber[saberNum].knockbackScale > 0.0f )
|
|
{
|
|
knockBackScale = attacker->client->ps.saber[saberNum].knockbackScale;
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( &attacker->client->ps.saber[saberNum], bladeNum )
|
|
&& attacker->client->ps.saber[saberNum].knockbackScale2 > 0.0f )
|
|
{
|
|
knockBackScale = attacker->client->ps.saber[saberNum].knockbackScale2;
|
|
}
|
|
}
|
|
|
|
for ( int i = 0; i < numVictims; i++ )
|
|
{
|
|
if ( victimEntityNum[i] == entityNum )
|
|
{
|
|
//hold on a sec, let's still do any accumulated knockback
|
|
if ( knockBackScale )
|
|
{
|
|
gentity_t *victim = &g_entities[victimEntityNum[i]];
|
|
if ( victim && victim->client )
|
|
{
|
|
vec3_t center, dirToCenter;
|
|
float knockDownThreshHold, knockback = knockBackScale * totalDmg[i] * 0.5f;
|
|
|
|
VectorAdd( victim->absmin, victim->absmax, center );
|
|
VectorScale( center, 0.5, center );
|
|
VectorSubtract( victim->currentOrigin, saberHitLocation, dirToCenter );
|
|
VectorNormalize( dirToCenter );
|
|
G_Throw( victim, dirToCenter, knockback );
|
|
if ( victim->client->ps.groundEntityNum != ENTITYNUM_NONE
|
|
&& dirToCenter[2] <= 0 )
|
|
{//hit downward on someone who is standing on firm ground, so more likely to knock them down
|
|
knockDownThreshHold = Q_irand( 25, 50 );
|
|
}
|
|
else
|
|
{
|
|
knockDownThreshHold = Q_irand( 75, 125 );
|
|
}
|
|
|
|
if ( knockback > knockDownThreshHold )
|
|
{
|
|
G_Knockdown( victim, attacker, dirToCenter, 350, qtrue );
|
|
}
|
|
}
|
|
}
|
|
//now clear everything
|
|
totalDmg[i] = 0;//no damage
|
|
hitLoc[i] = HL_NONE;
|
|
hitDismemberLoc[i] = HL_NONE;
|
|
hitDismember[i] = qfalse;
|
|
victimEntityNum[i] = ENTITYNUM_NONE;//like we never hit him
|
|
}
|
|
}
|
|
}
|
|
|
|
extern float damageModifier[];
|
|
extern float hitLocHealthPercentage[];
|
|
qboolean WP_SaberApplyDamage( gentity_t *ent, float baseDamage, int baseDFlags,
|
|
qboolean brokenParry, int saberNum, int bladeNum, qboolean thrownSaber )
|
|
{
|
|
qboolean didDamage = qfalse;
|
|
gentity_t *victim;
|
|
int dFlags = baseDFlags;
|
|
float maxDmg;
|
|
saberType_t saberType = ent->client->ps.saber[saberNum].type;
|
|
|
|
if ( !numVictims )
|
|
{
|
|
return qfalse;
|
|
}
|
|
for ( int i = 0; i < numVictims; i++ )
|
|
{
|
|
dFlags = baseDFlags|DAMAGE_DEATH_KNOCKBACK|DAMAGE_NO_HIT_LOC;
|
|
if ( victimEntityNum[i] != ENTITYNUM_NONE && &g_entities[victimEntityNum[i]] != NULL )
|
|
{ // Don't bother with this damage if the fraction is higher than the saber's fraction
|
|
if ( dmgFraction[i] < saberHitFraction || brokenParry )
|
|
{
|
|
victim = &g_entities[victimEntityNum[i]];
|
|
if ( !victim )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( victim->e_DieFunc == dieF_maglock_die )
|
|
{//*sigh*, special check for maglocks
|
|
vec3_t testFrom;
|
|
if ( ent->client->ps.saberInFlight )
|
|
{
|
|
VectorCopy( g_entities[ent->client->ps.saberEntityNum].currentOrigin, testFrom );
|
|
}
|
|
else
|
|
{
|
|
VectorCopy( ent->currentOrigin, testFrom );
|
|
}
|
|
testFrom[2] = victim->currentOrigin[2];
|
|
trace_t testTrace;
|
|
gi.trace( &testTrace, testFrom, vec3_origin, vec3_origin, victim->currentOrigin, ent->s.number, MASK_SHOT, (EG2_Collision)0, 0 );
|
|
if ( testTrace.entityNum != victim->s.number )
|
|
{//can only damage maglocks if have a clear trace to the thing's origin
|
|
continue;
|
|
}
|
|
}
|
|
if ( totalDmg[i] > 0 )
|
|
{//actually want to do *some* damage here
|
|
if ( victim->client
|
|
&& victim->client->NPC_class==CLASS_WAMPA
|
|
&& victim->activator == ent )
|
|
{
|
|
}
|
|
else if ( PM_SuperBreakWinAnim( ent->client->ps.torsoAnim )
|
|
|| PM_StabDownAnim( ent->client->ps.torsoAnim ) )
|
|
{//never cap the superbreak wins
|
|
}
|
|
else
|
|
{
|
|
if ( victim->client
|
|
&& (victim->s.weapon == WP_SABER || (victim->client->NPC_class==CLASS_REBORN) || (victim->client->NPC_class==CLASS_WAMPA))
|
|
&& !g_saberRealisticCombat->integer )
|
|
{//dmg vs other saber fighters is modded by hitloc and capped
|
|
totalDmg[i] *= damageModifier[hitLoc[i]];
|
|
if ( hitLoc[i] == HL_NONE )
|
|
{
|
|
maxDmg = 33*baseDamage;
|
|
}
|
|
else
|
|
{
|
|
maxDmg = 50*hitLocHealthPercentage[hitLoc[i]]*baseDamage;//*victim->client->ps.stats[STAT_MAX_HEALTH]*2.0f;
|
|
}
|
|
if ( maxDmg < totalDmg[i] )
|
|
{
|
|
totalDmg[i] = maxDmg;
|
|
}
|
|
//dFlags |= DAMAGE_NO_HIT_LOC;
|
|
}
|
|
//clamp the dmg
|
|
if ( victim->s.weapon != WP_SABER )
|
|
{//clamp the dmg between 25 and maxhealth
|
|
/*
|
|
if ( totalDmg[i] > victim->max_health )
|
|
{
|
|
totalDmg[i] = victim->max_health;
|
|
}
|
|
else */if ( totalDmg[i] < 25 )
|
|
{
|
|
totalDmg[i] = 25;
|
|
}
|
|
if ( totalDmg[i] > 100 )//+(50*g_spskill->integer) )
|
|
{//clamp using same adjustment as in NPC_Begin
|
|
totalDmg[i] = 100;//+(50*g_spskill->integer);
|
|
}
|
|
}
|
|
else
|
|
{//clamp the dmg between 5 and 100
|
|
if ( !victim->s.number && totalDmg[i] > 50 )
|
|
{//never do more than half full health damage to player
|
|
//prevents one-hit kills
|
|
totalDmg[i] = 50;
|
|
}
|
|
else if ( totalDmg[i] > 100 )
|
|
{
|
|
totalDmg[i] = 100;
|
|
}
|
|
else
|
|
{
|
|
if ( totalDmg[i] < 5 )
|
|
{
|
|
totalDmg[i] = 5;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( totalDmg[i] > 0 )
|
|
{
|
|
gentity_t *inflictor = ent;
|
|
didDamage = qtrue;
|
|
qboolean vicWasDismembered = qtrue;
|
|
qboolean vicWasAlive = (qboolean)(victim->health>0);
|
|
|
|
|
|
//This will prevent damage being inflicted if no base damage happened at all
|
|
//this is different to JKO so don't do this in TBDC mode
|
|
if (!g_TeamBeefDirectorsCut->integer)
|
|
{
|
|
if (baseDamage <= 0.1f)
|
|
{//just get their attention?
|
|
dFlags |= DAMAGE_NO_DAMAGE;
|
|
}
|
|
}
|
|
|
|
if( victim->client )
|
|
{
|
|
if ( victim->client->ps.pm_time > 0 && victim->client->ps.pm_flags & PMF_TIME_KNOCKBACK && victim->client->ps.velocity[2] > 0 )
|
|
{//already being knocked around
|
|
dFlags |= DAMAGE_NO_KNOCKBACK;
|
|
}
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& (ent->client->ps.saber[saberNum].saberFlags2&SFL2_NO_DISMEMBERMENT) )
|
|
{//no dismemberment! (blunt/stabbing weapon?)
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& (ent->client->ps.saber[saberNum].saberFlags2&SFL2_NO_DISMEMBERMENT2) )
|
|
{//no dismemberment! (blunt/stabbing weapon?)
|
|
}
|
|
else
|
|
{
|
|
if ( debug_subdivision->integer || g_saberRealisticCombat->integer )
|
|
{
|
|
dFlags |= DAMAGE_DISMEMBER;
|
|
if ( hitDismember[i] )
|
|
{
|
|
victim->client->dismembered = false;
|
|
}
|
|
}
|
|
else if ( hitDismember[i] )
|
|
{
|
|
dFlags |= DAMAGE_DISMEMBER;
|
|
}
|
|
if ( !victim->client->dismembered )
|
|
{
|
|
vicWasDismembered = qfalse;
|
|
}
|
|
}
|
|
if ( baseDamage <= 1.0f )
|
|
{//very mild damage
|
|
if ( victim->s.number == 0 || victim->client->ps.weapon == WP_SABER || victim->client->NPC_class == CLASS_GALAKMECH )
|
|
{//if it's the player or a saber-user, don't kill them with this blow
|
|
dFlags |= DAMAGE_NO_KILL;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( victim->takedamage )
|
|
{//some other breakable thing
|
|
//create a flash here
|
|
if ( !g_noClashFlare )
|
|
{
|
|
g_saberFlashTime = level.time-50;
|
|
VectorCopy( dmgSpot[i], g_saberFlashPos );
|
|
}
|
|
}
|
|
}
|
|
if ( !PM_SuperBreakWinAnim( ent->client->ps.torsoAnim )
|
|
&& !PM_StabDownAnim( ent->client->ps.torsoAnim )
|
|
&& !g_saberRealisticCombat->integer
|
|
&& g_saberDamageCapping->integer )
|
|
{//never cap the superbreak wins
|
|
if ( victim->client
|
|
&& victim->s.number >= MAX_CLIENTS )
|
|
{
|
|
if ( victim->client->NPC_class == CLASS_SHADOWTROOPER
|
|
|| ( victim->NPC && (victim->NPC->aiFlags&NPCAI_BOSS_CHARACTER) ) )
|
|
{//hit a boss character
|
|
int maxDmg = ((3-g_spskill->integer)*5)+10;
|
|
if ( totalDmg[i] > maxDmg )
|
|
{
|
|
totalDmg[i] = maxDmg;
|
|
}
|
|
}
|
|
else if ( victim->client->ps.weapon == WP_SABER
|
|
|| victim->client->NPC_class == CLASS_REBORN
|
|
|| victim->client->NPC_class == CLASS_JEDI )
|
|
{//hit a non-boss saber-user
|
|
int maxDmg = ((3-g_spskill->integer)*15)+30;
|
|
if ( totalDmg[i] > maxDmg )
|
|
{
|
|
totalDmg[i] = maxDmg;
|
|
}
|
|
}
|
|
}
|
|
if ( victim->s.number < MAX_CLIENTS
|
|
&& ent->NPC )
|
|
{
|
|
if ( (ent->NPC->aiFlags&NPCAI_BOSS_CHARACTER)
|
|
|| (ent->NPC->aiFlags&NPCAI_SUBBOSS_CHARACTER)
|
|
|| ent->client->NPC_class == CLASS_SHADOWTROOPER )
|
|
{//player hit by a boss character
|
|
int maxDmg = ((g_spskill->integer+1)*4)+3;
|
|
if ( totalDmg[i] > maxDmg )
|
|
{
|
|
totalDmg[i] = maxDmg;
|
|
}
|
|
}
|
|
else if ( g_spskill->integer < 3 ) //was < 2
|
|
{//player hit by any enemy //on easy or medium?
|
|
int maxDmg = ((g_spskill->integer+1)*10)+20;
|
|
if ( totalDmg[i] > maxDmg )
|
|
{
|
|
totalDmg[i] = maxDmg;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//victim->hitLoc = hitLoc[i];
|
|
|
|
dFlags |= DAMAGE_NO_KNOCKBACK;//okay, let's try no knockback whatsoever...
|
|
dFlags &= ~DAMAGE_DEATH_KNOCKBACK;
|
|
if ( g_saberRealisticCombat->integer )
|
|
{
|
|
dFlags |= DAMAGE_NO_KNOCKBACK;
|
|
dFlags &= ~DAMAGE_DEATH_KNOCKBACK;
|
|
dFlags &= ~DAMAGE_NO_KILL;
|
|
}
|
|
if ( ent->client && !ent->s.number )
|
|
{
|
|
switch( hitLoc[i] )
|
|
{
|
|
case HL_FOOT_RT:
|
|
case HL_FOOT_LT:
|
|
case HL_LEG_RT:
|
|
case HL_LEG_LT:
|
|
ent->client->sess.missionStats.legAttacksCnt++;
|
|
break;
|
|
case HL_WAIST:
|
|
case HL_BACK_RT:
|
|
case HL_BACK_LT:
|
|
case HL_BACK:
|
|
case HL_CHEST_RT:
|
|
case HL_CHEST_LT:
|
|
case HL_CHEST:
|
|
ent->client->sess.missionStats.torsoAttacksCnt++;
|
|
break;
|
|
case HL_ARM_RT:
|
|
case HL_ARM_LT:
|
|
case HL_HAND_RT:
|
|
case HL_HAND_LT:
|
|
ent->client->sess.missionStats.armAttacksCnt++;
|
|
break;
|
|
default:
|
|
ent->client->sess.missionStats.otherAttacksCnt++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( saberType == SABER_SITH_SWORD )
|
|
{//do knockback
|
|
dFlags &= ~(DAMAGE_NO_KNOCKBACK|DAMAGE_DEATH_KNOCKBACK);
|
|
}
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].knockbackScale > 0.0f )
|
|
{
|
|
dFlags &= ~(DAMAGE_NO_KNOCKBACK|DAMAGE_DEATH_KNOCKBACK);
|
|
if ( saberNum < 1 )
|
|
{
|
|
dFlags |= DAMAGE_SABER_KNOCKBACK1;
|
|
}
|
|
else
|
|
{
|
|
dFlags |= DAMAGE_SABER_KNOCKBACK2;
|
|
}
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].knockbackScale2 > 0.0f )
|
|
{
|
|
dFlags &= ~(DAMAGE_NO_KNOCKBACK|DAMAGE_DEATH_KNOCKBACK);
|
|
if ( saberNum < 1 )
|
|
{
|
|
dFlags |= DAMAGE_SABER_KNOCKBACK1_B2;
|
|
}
|
|
else
|
|
{
|
|
dFlags |= DAMAGE_SABER_KNOCKBACK2_B2;
|
|
}
|
|
}
|
|
if ( thrownSaber )
|
|
{
|
|
inflictor = &g_entities[ent->client->ps.saberEntityNum];
|
|
}
|
|
int damage = 0;
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].damageScale != 1.0f )
|
|
{
|
|
damage = ceil(totalDmg[i]*ent->client->ps.saber[saberNum].damageScale);
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].damageScale2 != 1.0f )
|
|
{
|
|
damage = ceil(totalDmg[i]*ent->client->ps.saber[saberNum].damageScale2);
|
|
}
|
|
else
|
|
{
|
|
damage = ceil(totalDmg[i]);
|
|
}
|
|
G_Damage( victim, inflictor, ent, dmgDir[i], dmgSpot[i], damage, dFlags, MOD_SABER, hitDismemberLoc[i] );
|
|
if ( damage > 0 && cg.time )
|
|
{
|
|
float sizeTimeScale = 1.0f;
|
|
if ( (vicWasAlive
|
|
&& victim->health <= 0 )
|
|
|| (!vicWasDismembered
|
|
&& victim->client->dismembered
|
|
&& hitDismemberLoc[i] != HL_NONE
|
|
&& hitDismember[i]) )
|
|
{
|
|
sizeTimeScale = 3.0f;
|
|
}
|
|
//FIXME: if not hitting the first model on the enemy, don't do this!
|
|
CG_SaberDoWeaponHitMarks( ent->client,
|
|
(ent->client->ps.saberInFlight?&g_entities[ent->client->ps.saberEntityNum]:NULL),
|
|
victim,
|
|
saberNum,
|
|
bladeNum,
|
|
dmgSpot[i],
|
|
dmgDir[i],
|
|
dmgBladeVec[i],
|
|
dmgNormal[i],
|
|
sizeTimeScale );
|
|
}
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer )
|
|
{
|
|
if ( (dFlags&DAMAGE_NO_DAMAGE) )
|
|
{
|
|
gi.Printf( S_COLOR_RED"damage: fake, hitLoc %d\n", hitLoc[i] );
|
|
}
|
|
else
|
|
{
|
|
gi.Printf( S_COLOR_RED"damage: %4.2f, hitLoc %d\n", totalDmg[i], hitLoc[i] );
|
|
}
|
|
}
|
|
#endif
|
|
//do the effect
|
|
//vec3_t splashBackDir;
|
|
//VectorScale( dmgNormal[i], -1, splashBackDir );
|
|
//G_PlayEffect( G_EffectIndex( "blood_sparks" ), dmgSpot[i], splashBackDir );
|
|
if ( ent->s.number == 0 )
|
|
{
|
|
AddSoundEvent( victim->owner, dmgSpot[i], 256, AEL_DISCOVERED );
|
|
AddSightEvent( victim->owner, dmgSpot[i], 512, AEL_DISCOVERED, 50 );
|
|
}
|
|
if ( ent->client )
|
|
{
|
|
if ( ent->enemy && ent->enemy == victim )
|
|
{//just so Jedi knows that he hit his enemy
|
|
ent->client->ps.saberEventFlags |= SEF_HITENEMY;
|
|
}
|
|
else
|
|
{
|
|
ent->client->ps.saberEventFlags |= SEF_HITOBJECT;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return didDamage;
|
|
}
|
|
|
|
void WP_SaberDamageAdd( float trDmg, int trVictimEntityNum, vec3_t trDmgDir, vec3_t trDmgBladeVec, vec3_t trDmgNormal, vec3_t trDmgSpot, float dmg, float fraction, int trHitLoc, qboolean trDismember, int trDismemberLoc )
|
|
{
|
|
int curVictim = 0;
|
|
int i;
|
|
|
|
if ( trVictimEntityNum < 0 || trVictimEntityNum >= ENTITYNUM_WORLD )
|
|
{
|
|
return;
|
|
}
|
|
if ( trDmg * dmg < 10.0f )
|
|
{//too piddly an amount of damage to really count?
|
|
//FIXME: but already did the effect, didn't we... sigh...
|
|
//return;
|
|
}
|
|
if ( trDmg )
|
|
{//did some damage to something
|
|
for ( i = 0; i < numVictims; i++ )
|
|
{
|
|
if ( victimEntityNum[i] == trVictimEntityNum )
|
|
{//already hit this guy before
|
|
curVictim = i;
|
|
break;
|
|
}
|
|
}
|
|
if ( i == numVictims )
|
|
{//haven't hit his guy before
|
|
if ( numVictims + 1 >= MAX_SABER_VICTIMS )
|
|
{//can't add another victim at this time
|
|
return;
|
|
}
|
|
//add a new victim to the list
|
|
curVictim = numVictims;
|
|
victimEntityNum[numVictims++] = trVictimEntityNum;
|
|
}
|
|
|
|
float addDmg = trDmg*dmg;
|
|
if ( trHitLoc!=HL_NONE && (hitLoc[curVictim]==HL_NONE||hitLocHealthPercentage[trHitLoc]>hitLocHealthPercentage[hitLoc[curVictim]]) )
|
|
{//this hitLoc is more critical than the previous one this frame
|
|
hitLoc[curVictim] = trHitLoc;
|
|
}
|
|
|
|
totalDmg[curVictim] += addDmg;
|
|
if ( !VectorLengthSquared( dmgDir[curVictim] ) )
|
|
{
|
|
VectorCopy( trDmgDir, dmgDir[curVictim] );
|
|
}
|
|
if ( !VectorLengthSquared( dmgBladeVec[curVictim] ) )
|
|
{
|
|
VectorCopy( trDmgBladeVec, dmgBladeVec[curVictim] );
|
|
}
|
|
if ( !VectorLengthSquared( dmgNormal[curVictim] ) )
|
|
{
|
|
VectorCopy( trDmgNormal, dmgNormal[curVictim] );
|
|
}
|
|
if ( !VectorLengthSquared( dmgSpot[curVictim] ) )
|
|
{
|
|
VectorCopy( trDmgSpot, dmgSpot[curVictim] );
|
|
}
|
|
|
|
// Make sure we keep track of the fraction. Why?
|
|
// Well, if the saber hits something that stops it, the damage isn't done past that point.
|
|
dmgFraction[curVictim] = fraction;
|
|
if ( (trDismemberLoc != HL_NONE && hitDismemberLoc[curVictim] == HL_NONE)
|
|
|| (!hitDismember[curVictim] && trDismember) )
|
|
{//either this is the first dismember loc we got or we got a loc before, but it wasn't a dismember loc, so take the new one
|
|
hitDismemberLoc[curVictim] = trDismemberLoc;
|
|
}
|
|
if ( trDismember )
|
|
{//we scored a dismemberment hit...
|
|
hitDismember[curVictim] = trDismember;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
WP_SabersIntersect
|
|
|
|
Breaks the two saber paths into 2 tris each and tests each tri for the first saber path against each of the other saber path's tris
|
|
|
|
FIXME: subdivide the arc into a consistant increment
|
|
FIXME: test the intersection to see if the sabers really did intersect (weren't going in the same direction and/or passed through same point at different times)?
|
|
*/
|
|
extern qboolean tri_tri_intersect(vec3_t V0,vec3_t V1,vec3_t V2,vec3_t U0,vec3_t U1,vec3_t U2);
|
|
qboolean WP_SabersIntersect( gentity_t *ent1, int ent1SaberNum, int ent1BladeNum, gentity_t *ent2, qboolean checkDir )
|
|
{
|
|
vec3_t saberBase1, saberTip1, saberBaseNext1, saberTipNext1;
|
|
vec3_t saberBase2, saberTip2, saberBaseNext2, saberTipNext2;
|
|
int ent2SaberNum = 0, ent2BladeNum = 0;
|
|
vec3_t dir;
|
|
|
|
/*
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer )
|
|
{
|
|
gi.Printf( S_COLOR_GREEN"Doing precise saber intersection check\n" );
|
|
}
|
|
#endif
|
|
*/
|
|
|
|
if ( !ent1 || !ent2 )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( !ent1->client || !ent2->client )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( ent1->client->ps.SaberLength() <= 0 || ent2->client->ps.SaberLength() <= 0 )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
for ( ent2SaberNum = 0; ent2SaberNum < MAX_SABERS; ent2SaberNum++ )
|
|
{
|
|
for ( ent2BladeNum = 0; ent2BladeNum < ent2->client->ps.saber[ent2SaberNum].numBlades; ent2BladeNum++ )
|
|
{
|
|
if ( ent2->client->ps.saber[ent2SaberNum].type != SABER_NONE
|
|
&& ent2->client->ps.saber[ent2SaberNum].blade[ent2BladeNum].length > 0 )
|
|
{//valid saber and this blade is on
|
|
//if ( ent1->client->ps.saberInFlight )
|
|
{
|
|
VectorCopy( ent1->client->ps.saber[ent1SaberNum].blade[ent1BladeNum].muzzlePointOld, saberBase1 );
|
|
VectorCopy( ent1->client->ps.saber[ent1SaberNum].blade[ent1BladeNum].muzzlePoint, saberBaseNext1 );
|
|
|
|
VectorSubtract( ent1->client->ps.saber[ent1SaberNum].blade[ent1BladeNum].muzzlePoint, ent1->client->ps.saber[ent1SaberNum].blade[ent1BladeNum].muzzlePointOld, dir );
|
|
VectorNormalize( dir );
|
|
VectorMA( saberBaseNext1, SABER_EXTRAPOLATE_DIST, dir, saberBaseNext1 );
|
|
|
|
VectorMA( saberBase1, ent1->client->ps.saber[ent1SaberNum].blade[ent1BladeNum].length, ent1->client->ps.saber[ent1SaberNum].blade[ent1BladeNum].muzzleDirOld, saberTip1 );
|
|
VectorMA( saberBaseNext1, ent1->client->ps.saber[ent1SaberNum].blade[ent1BladeNum].length, ent1->client->ps.saber[ent1SaberNum].blade[ent1BladeNum].muzzleDir, saberTipNext1 );
|
|
|
|
VectorSubtract( saberTipNext1, saberTip1, dir );
|
|
VectorNormalize( dir );
|
|
VectorMA( saberTipNext1, SABER_EXTRAPOLATE_DIST, dir, saberTipNext1 );
|
|
}
|
|
/*
|
|
else
|
|
{
|
|
VectorCopy( ent1->client->ps.saber[ent1SaberNum].blade[ent1BladeNum].muzzlePoint, saberBase1 );
|
|
VectorCopy( ent1->client->ps.saber[ent1SaberNum].blade[ent1BladeNum].muzzlePointNext, saberBaseNext1 );
|
|
VectorMA( saberBase1, ent1->client->ps.saber[ent1SaberNum].blade[ent1BladeNum].length, ent1->client->ps.saber[ent1SaberNum].blade[ent1BladeNum].muzzleDir, saberTip1 );
|
|
VectorMA( saberBaseNext1, ent1->client->ps.saber[ent1SaberNum].blade[ent1BladeNum].length, ent1->client->ps.saber[ent1SaberNum].blade[ent1BladeNum].muzzleDirNext, saberTipNext1 );
|
|
}
|
|
*/
|
|
|
|
//if ( ent2->client->ps.saberInFlight )
|
|
{
|
|
VectorCopy( ent2->client->ps.saber[ent2SaberNum].blade[ent2BladeNum].muzzlePointOld, saberBase2 );
|
|
VectorCopy( ent2->client->ps.saber[ent2SaberNum].blade[ent2BladeNum].muzzlePoint, saberBaseNext2 );
|
|
|
|
VectorSubtract( ent2->client->ps.saber[ent2SaberNum].blade[ent2BladeNum].muzzlePoint, ent2->client->ps.saber[ent2SaberNum].blade[ent2BladeNum].muzzlePointOld, dir );
|
|
VectorNormalize( dir );
|
|
VectorMA( saberBaseNext2, SABER_EXTRAPOLATE_DIST, dir, saberBaseNext2 );
|
|
|
|
VectorMA( saberBase2, ent2->client->ps.saber[ent2SaberNum].blade[ent2BladeNum].length, ent2->client->ps.saber[ent2SaberNum].blade[ent2BladeNum].muzzleDirOld, saberTip2 );
|
|
VectorMA( saberBaseNext2, ent2->client->ps.saber[ent2SaberNum].blade[ent2BladeNum].length, ent2->client->ps.saber[ent2SaberNum].blade[ent2BladeNum].muzzleDir, saberTipNext2 );
|
|
|
|
VectorSubtract( saberTipNext2, saberTip2, dir );
|
|
VectorNormalize( dir );
|
|
VectorMA( saberTipNext2, SABER_EXTRAPOLATE_DIST, dir, saberTipNext2 );
|
|
}
|
|
/*
|
|
else
|
|
{
|
|
VectorCopy( ent2->client->ps.saber[ent2SaberNum].blade[ent2BladeNum].muzzlePoint, saberBase2 );
|
|
VectorCopy( ent2->client->ps.saber[ent2SaberNum].blade[ent2BladeNum].muzzlePointNext, saberBaseNext2 );
|
|
VectorMA( saberBase2, ent2->client->ps.saber[ent2SaberNum].blade[ent2BladeNum].length, ent2->client->ps.saber[ent2SaberNum].blade[ent2BladeNum].muzzleDir, saberTip2 );
|
|
VectorMA( saberBaseNext2, ent2->client->ps.saber[ent2SaberNum].blade[ent2BladeNum].length, ent2->client->ps.saber[ent2SaberNum].blade[ent2BladeNum].muzzleDirNext, saberTipNext2 );
|
|
}
|
|
*/
|
|
if ( checkDir )
|
|
{//check the direction of the two swings to make sure the sabers are swinging towards each other
|
|
vec3_t saberDir1, saberDir2;
|
|
|
|
VectorSubtract( saberTipNext1, saberTip1, saberDir1 );
|
|
VectorSubtract( saberTipNext2, saberTip2, saberDir2 );
|
|
VectorNormalize( saberDir1 );
|
|
VectorNormalize( saberDir2 );
|
|
if ( DotProduct( saberDir1, saberDir2 ) > 0.6f )
|
|
{//sabers moving in same dir, probably didn't actually hit
|
|
continue;
|
|
}
|
|
//now check orientation of sabers, make sure they're not parallel or close to it
|
|
float dot = DotProduct( ent1->client->ps.saber[ent1SaberNum].blade[ent1BladeNum].muzzleDir, ent2->client->ps.saber[ent2SaberNum].blade[ent2BladeNum].muzzleDir );
|
|
if ( dot > 0.9f || dot < -0.9f )
|
|
{//too parallel to really block effectively?
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ( tri_tri_intersect( saberBase1, saberTip1, saberBaseNext1, saberBase2, saberTip2, saberBaseNext2 ) )
|
|
{
|
|
return qtrue;
|
|
}
|
|
if ( tri_tri_intersect( saberBase1, saberTip1, saberBaseNext1, saberBase2, saberTip2, saberTipNext2 ) )
|
|
{
|
|
return qtrue;
|
|
}
|
|
if ( tri_tri_intersect( saberBase1, saberTip1, saberTipNext1, saberBase2, saberTip2, saberBaseNext2 ) )
|
|
{
|
|
return qtrue;
|
|
}
|
|
if ( tri_tri_intersect( saberBase1, saberTip1, saberTipNext1, saberBase2, saberTip2, saberTipNext2 ) )
|
|
{
|
|
return qtrue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
float WP_SabersDistance( gentity_t *ent1, gentity_t *ent2 )
|
|
{
|
|
vec3_t saberBaseNext1, saberTipNext1, saberPoint1;
|
|
vec3_t saberBaseNext2, saberTipNext2, saberPoint2;
|
|
|
|
/*
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer )
|
|
{
|
|
gi.Printf( S_COLOR_GREEN"Doing precise saber intersection check\n" );
|
|
}
|
|
#endif
|
|
*/
|
|
|
|
if ( !ent1 || !ent2 )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( !ent1->client || !ent2->client )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( ent1->client->ps.SaberLength() <= 0 || ent2->client->ps.SaberLength() <= 0 )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
//FIXME: UGH, how do we make this work for multiply-bladed sabers?
|
|
|
|
//if ( ent1->client->ps.saberInFlight )
|
|
{
|
|
VectorCopy( ent1->client->ps.saber[0].blade[0].muzzlePoint, saberBaseNext1 );
|
|
VectorMA( saberBaseNext1, ent1->client->ps.saber[0].blade[0].length, ent1->client->ps.saber[0].blade[0].muzzleDir, saberTipNext1 );
|
|
}
|
|
/*
|
|
else
|
|
{
|
|
VectorCopy( ent1->client->ps.saber[0].blade[0].muzzlePointNext, saberBaseNext1 );
|
|
VectorMA( saberBaseNext1, ent1->client->ps.saber[0].blade[0].length, ent1->client->ps.saber[0].blade[0].muzzleDirNext, saberTipNext1 );
|
|
}
|
|
*/
|
|
|
|
//if ( ent2->client->ps.saberInFlight )
|
|
{
|
|
VectorCopy( ent2->client->ps.saber[0].blade[0].muzzlePoint, saberBaseNext2 );
|
|
VectorMA( saberBaseNext2, ent2->client->ps.saber[0].blade[0].length, ent2->client->ps.saber[0].blade[0].muzzleDir, saberTipNext2 );
|
|
}
|
|
/*
|
|
else
|
|
{
|
|
VectorCopy( ent2->client->ps.saber[0].blade[0].muzzlePointNext, saberBaseNext2 );
|
|
VectorMA( saberBaseNext2, ent2->client->ps.saber[0].blade[0].length, ent2->client->ps.saber[0].blade[0].muzzleDirNext, saberTipNext2 );
|
|
}
|
|
*/
|
|
|
|
float sabersDist = ShortestLineSegBewteen2LineSegs( saberBaseNext1, saberTipNext1, saberBaseNext2, saberTipNext2, saberPoint1, saberPoint2 );
|
|
|
|
//okay, this is a super hack, but makes saber collisions look better from the player point of view
|
|
/*
|
|
if ( sabersDist < 16.0f )
|
|
{
|
|
vec3_t saberDistDir, saberMidPoint, camLookDir;
|
|
|
|
VectorSubtract( saberPoint2, saberPoint1, saberDistDir );
|
|
VectorMA( saberPoint1, 0.5f, saberDistDir, saberMidPoint );
|
|
VectorSubtract( saberMidPoint, cg.refdef.vieworg, camLookDir );
|
|
VectorNormalize( saberDistDir );
|
|
VectorNormalize( camLookDir );
|
|
float dot = fabs(DotProduct( camLookDir, saberDistDir ));
|
|
sabersDist -= 8.0f*dot;
|
|
if ( sabersDist < 0.0f )
|
|
{
|
|
sabersDist = 0.0f;
|
|
}
|
|
}
|
|
*/
|
|
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer > 2 )
|
|
{
|
|
G_DebugLine( saberPoint1, saberPoint2, FRAMETIME, 0x00ffffff, qtrue );
|
|
}
|
|
#endif
|
|
return sabersDist;
|
|
}
|
|
|
|
qboolean WP_SabersIntersection( gentity_t *ent1, gentity_t *ent2, vec3_t intersect )
|
|
{
|
|
vec3_t saberBaseNext1, saberTipNext1, saberPoint1;
|
|
vec3_t saberBaseNext2, saberTipNext2, saberPoint2;
|
|
int saberNum1, saberNum2, bladeNum1, bladeNum2;
|
|
float lineSegLength, bestLineSegLength = Q3_INFINITE;
|
|
|
|
if ( !ent1 || !ent2 )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( !ent1->client || !ent2->client )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( ent1->client->ps.SaberLength() <= 0 || ent2->client->ps.SaberLength() <= 0 )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
//UGH, had to make this work for multiply-bladed sabers
|
|
for ( saberNum1 = 0; saberNum1 < MAX_SABERS; saberNum1++ )
|
|
{
|
|
for ( bladeNum1 = 0; bladeNum1 < ent1->client->ps.saber[saberNum1].numBlades; bladeNum1++ )
|
|
{
|
|
if ( ent1->client->ps.saber[saberNum1].type != SABER_NONE
|
|
&& ent1->client->ps.saber[saberNum1].blade[bladeNum1].length > 0 )
|
|
{//valid saber and this blade is on
|
|
for ( saberNum2 = 0; saberNum2 < MAX_SABERS; saberNum2++ )
|
|
{
|
|
for ( bladeNum2 = 0; bladeNum2 < ent2->client->ps.saber[saberNum2].numBlades; bladeNum2++ )
|
|
{
|
|
if ( ent2->client->ps.saber[saberNum2].type != SABER_NONE
|
|
&& ent2->client->ps.saber[saberNum2].blade[bladeNum2].length > 0 )
|
|
{//valid saber and this blade is on
|
|
VectorCopy( ent1->client->ps.saber[saberNum1].blade[bladeNum1].muzzlePoint, saberBaseNext1 );
|
|
VectorMA( saberBaseNext1, ent1->client->ps.saber[saberNum1].blade[bladeNum1].length, ent1->client->ps.saber[saberNum1].blade[bladeNum1].muzzleDir, saberTipNext1 );
|
|
|
|
VectorCopy( ent2->client->ps.saber[saberNum2].blade[bladeNum2].muzzlePoint, saberBaseNext2 );
|
|
VectorMA( saberBaseNext2, ent2->client->ps.saber[saberNum2].blade[bladeNum2].length, ent2->client->ps.saber[saberNum2].blade[bladeNum2].muzzleDir, saberTipNext2 );
|
|
|
|
lineSegLength = ShortestLineSegBewteen2LineSegs( saberBaseNext1, saberTipNext1, saberBaseNext2, saberTipNext2, saberPoint1, saberPoint2 );
|
|
if ( lineSegLength < bestLineSegLength )
|
|
{
|
|
bestLineSegLength = lineSegLength;
|
|
VectorAdd( saberPoint1, saberPoint2, intersect );
|
|
VectorScale( intersect, 0.5, intersect );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
const char *hit_blood_sparks = "sparks/blood_sparks2"; // could have changed this effect directly, but this is just safer in case anyone anywhere else is using the old one for something?
|
|
const char *hit_sparks = "saber/saber_cut";
|
|
|
|
qboolean WP_SaberDamageEffects( trace_t *tr, const vec3_t start, float length, float dmg, vec3_t dmgDir, vec3_t bladeVec, int enemyTeam, saberType_t saberType, saberInfo_t *saber, int bladeNum )
|
|
{
|
|
|
|
int hitEntNum[MAX_G2_COLLISIONS];
|
|
for ( int hen = 0; hen < MAX_G2_COLLISIONS; hen++ )
|
|
{
|
|
hitEntNum[hen] = ENTITYNUM_NONE;
|
|
}
|
|
//NOTE: = {0} does NOT work on anything but bytes?
|
|
float hitEntDmgAdd[MAX_G2_COLLISIONS] = {0};
|
|
float hitEntDmgSub[MAX_G2_COLLISIONS] = {0};
|
|
vec3_t hitEntPoint[MAX_G2_COLLISIONS];
|
|
vec3_t hitEntNormal[MAX_G2_COLLISIONS];
|
|
vec3_t bladeDir;
|
|
float hitEntStartFrac[MAX_G2_COLLISIONS] = {0};
|
|
int trHitLoc[MAX_G2_COLLISIONS] = {HL_NONE};//same as 0
|
|
int trDismemberLoc[MAX_G2_COLLISIONS] = {HL_NONE};//same as 0
|
|
qboolean trDismember[MAX_G2_COLLISIONS] = {qfalse};//same as 0
|
|
int i,z;
|
|
int numHitEnts = 0;
|
|
float distFromStart,doDmg;
|
|
int hitEffect = 0;
|
|
const char *trSurfName;
|
|
gentity_t *hitEnt;
|
|
|
|
VectorNormalize2( bladeVec, bladeDir );
|
|
|
|
for (z=0; z < MAX_G2_COLLISIONS; z++)
|
|
{
|
|
if ( tr->G2CollisionMap[z].mEntityNum == -1 )
|
|
{//actually, completely break out of this for loop since nothing after this in the aray should ever be valid either
|
|
continue;//break;//
|
|
}
|
|
|
|
CCollisionRecord &coll = tr->G2CollisionMap[z];
|
|
//distFromStart = Distance( start, coll.mCollisionPosition );
|
|
distFromStart = coll.mDistance;
|
|
|
|
/*
|
|
//FIXME: (distFromStart/length) is not guaranteed to be from 0 to 1... *sigh*...
|
|
if ( length && saberHitFraction < 1.0f && (distFromStart/length) < 1.0f && (distFromStart/length) > saberHitFraction )
|
|
{//a saber was hit before this point, don't count it
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer )
|
|
{
|
|
gi.Printf( S_COLOR_MAGENTA"rejecting G2 collision- %4.2f farther than saberHitFraction %4.2f\n", (distFromStart/length), saberHitFraction );
|
|
}
|
|
#endif
|
|
continue;
|
|
}
|
|
*/
|
|
|
|
for ( i = 0; i < numHitEnts; i++ )
|
|
{
|
|
if ( hitEntNum[i] == coll.mEntityNum )
|
|
{//we hit this ent before
|
|
//we'll want to add this dist
|
|
hitEntDmgAdd[i] = distFromStart;
|
|
break;
|
|
}
|
|
}
|
|
if ( i == numHitEnts )
|
|
{//first time we hit this ent
|
|
if ( numHitEnts == MAX_G2_COLLISIONS )
|
|
{//hit too many damn ents!
|
|
continue;
|
|
}
|
|
hitEntNum[numHitEnts] = coll.mEntityNum;
|
|
if ( !coll.mFlags )
|
|
{//hmm, we came out first, so we must have started inside
|
|
//we'll want to subtract this dist
|
|
hitEntDmgAdd[numHitEnts] = distFromStart;
|
|
}
|
|
else
|
|
{//we're entering the model
|
|
//we'll want to subtract this dist
|
|
hitEntDmgSub[numHitEnts] = distFromStart;
|
|
}
|
|
//keep track of how far in the damage was done
|
|
hitEntStartFrac[numHitEnts] = hitEntDmgSub[numHitEnts]/length;
|
|
//remember the entrance point
|
|
VectorCopy( coll.mCollisionPosition, hitEntPoint[numHitEnts] );
|
|
//remember the normal of the face we hit
|
|
VectorCopy( coll.mCollisionNormal, hitEntNormal[numHitEnts] );
|
|
VectorNormalize( hitEntNormal[numHitEnts] );
|
|
|
|
//do the effect
|
|
|
|
//FIXME: check material rather than team?
|
|
hitEnt = &g_entities[hitEntNum[numHitEnts]];
|
|
if ( hitEnt
|
|
&& hitEnt->client
|
|
&& coll.mModelIndex > 0 )
|
|
{//hit a submodel on the enemy, not their actual body!
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( saber, bladeNum )
|
|
&& saber->hitOtherEffect )
|
|
{
|
|
hitEffect = saber->hitOtherEffect;
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( saber, bladeNum )
|
|
&& saber->hitOtherEffect2 )
|
|
{
|
|
hitEffect = saber->hitOtherEffect2;
|
|
}
|
|
else
|
|
{
|
|
hitEffect = G_EffectIndex( hit_sparks );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( saber, bladeNum )
|
|
&& saber->hitPersonEffect )
|
|
{
|
|
hitEffect = saber->hitPersonEffect;
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( saber, bladeNum )
|
|
&& saber->hitPersonEffect2 )
|
|
{
|
|
hitEffect = saber->hitPersonEffect2;
|
|
}
|
|
else
|
|
{
|
|
hitEffect = G_EffectIndex( hit_blood_sparks );
|
|
}
|
|
}
|
|
if ( hitEnt != NULL )
|
|
{
|
|
if ( hitEnt->client )
|
|
{
|
|
class_t npc_class = hitEnt->client->NPC_class;
|
|
if ( npc_class == CLASS_SEEKER || npc_class == CLASS_PROBE || npc_class == CLASS_MOUSE || npc_class == CLASS_REMOTE ||
|
|
npc_class == CLASS_GONK || npc_class == CLASS_R2D2 || npc_class == CLASS_R5D2 ||
|
|
npc_class == CLASS_PROTOCOL || npc_class == CLASS_MARK1 || npc_class == CLASS_MARK2 ||
|
|
npc_class == CLASS_INTERROGATOR || npc_class == CLASS_ATST || npc_class == CLASS_SENTRY )
|
|
{
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( saber, bladeNum )
|
|
&& saber->hitOtherEffect )
|
|
{
|
|
hitEffect = saber->hitOtherEffect;
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( saber, bladeNum )
|
|
&& saber->hitOtherEffect2 )
|
|
{
|
|
hitEffect = saber->hitOtherEffect2;
|
|
}
|
|
else
|
|
{
|
|
hitEffect = G_EffectIndex( hit_sparks );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// So sue me, this is the easiest way to check to see if this is the turbo laser from t2_wedge,
|
|
// in which case I don't want the saber effects goin off on it.
|
|
if ( (hitEnt->flags&FL_DMG_BY_HEAVY_WEAP_ONLY)
|
|
&& hitEnt->takedamage == qfalse
|
|
&& Q_stricmp( hitEnt->classname, "misc_turret" ) == 0 )
|
|
{
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
if ( dmg )
|
|
{//only do these effects if actually trying to damage the thing...
|
|
if ( (hitEnt->svFlags&SVF_BBRUSH)//a breakable brush
|
|
&& ( (hitEnt->spawnflags&1)//INVINCIBLE
|
|
||(hitEnt->flags&FL_DMG_BY_HEAVY_WEAP_ONLY) )//HEAVY weapon damage only
|
|
)
|
|
{//no hit effect (besides regular client-side one)
|
|
hitEffect = 0;
|
|
}
|
|
else
|
|
{
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( saber, bladeNum )
|
|
&& saber->hitOtherEffect )
|
|
{
|
|
hitEffect = saber->hitOtherEffect;
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( saber, bladeNum )
|
|
&& saber->hitOtherEffect2 )
|
|
{
|
|
hitEffect = saber->hitOtherEffect2;
|
|
}
|
|
else
|
|
{
|
|
hitEffect = G_EffectIndex( hit_sparks );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//FIXME: play less if damage is less?
|
|
if ( !g_saberNoEffects )
|
|
{
|
|
if ( hitEffect != 0 )
|
|
{
|
|
//FIXME: when you have multiple blades hitting someone for many sequential server frames, this can get a bit chuggy!
|
|
G_PlayEffect( hitEffect, coll.mCollisionPosition, coll.mCollisionNormal );
|
|
}
|
|
}
|
|
|
|
//Get the hit location based on surface name
|
|
if ( (hitLoc[numHitEnts] == HL_NONE && trHitLoc[numHitEnts] == HL_NONE)
|
|
|| (hitDismemberLoc[numHitEnts] == HL_NONE && trDismemberLoc[numHitEnts] == HL_NONE)
|
|
|| (!hitDismember[numHitEnts] && !trDismember[numHitEnts]) )
|
|
{//no hit loc set for this ent this damage cycle yet
|
|
//FIXME: find closest impact surf *first* (per ent), then call G_GetHitLocFromSurfName?
|
|
//FIXED: if hit multiple ents in this collision record, these trSurfName, trDismember and trDismemberLoc will get stomped/confused over the multiple ents I hit
|
|
trSurfName = gi.G2API_GetSurfaceName( &g_entities[coll.mEntityNum].ghoul2[coll.mModelIndex], coll.mSurfaceIndex );
|
|
trDismember[numHitEnts] = G_GetHitLocFromSurfName( &g_entities[coll.mEntityNum], trSurfName, &trHitLoc[numHitEnts], coll.mCollisionPosition, dmgDir, bladeDir, MOD_SABER, saberType );
|
|
if ( trDismember[numHitEnts] )
|
|
{
|
|
trDismemberLoc[numHitEnts] = trHitLoc[numHitEnts];
|
|
}
|
|
/*
|
|
if ( trDismember[numHitEnts] )
|
|
{
|
|
Com_Printf( S_COLOR_RED"Okay to dismember %s on ent %d\n", hitLocName[trDismemberLoc[numHitEnts]], hitEntNum[numHitEnts] );
|
|
}
|
|
else
|
|
{
|
|
Com_Printf( "Hit (no dismember) %s on ent %d\n", hitLocName[trHitLoc[numHitEnts]], hitEntNum[numHitEnts] );
|
|
}
|
|
*/
|
|
}
|
|
numHitEnts++;
|
|
}
|
|
}
|
|
|
|
//now go through all the ents we hit and do the damage
|
|
for ( i = 0; i < numHitEnts; i++ )
|
|
{
|
|
doDmg = dmg;
|
|
if ( hitEntNum[i] != ENTITYNUM_NONE )
|
|
{
|
|
if ( doDmg < 10 )
|
|
{//base damage is less than 10
|
|
if ( hitEntNum[i] != 0 )
|
|
{//not the player
|
|
hitEnt = &g_entities[hitEntNum[i]];
|
|
if ( !hitEnt->client || (hitEnt->client->ps.weapon!=WP_SABER&&hitEnt->client->NPC_class!=CLASS_GALAKMECH&&hitEnt->client->playerTeam==enemyTeam) )
|
|
{//did *not* hit a jedi and did *not* hit the player
|
|
//make sure the base damage is high against non-jedi, feels better
|
|
doDmg = 10;
|
|
}
|
|
}
|
|
}
|
|
if ( !hitEntDmgAdd[i] && !hitEntDmgSub[i] )
|
|
{//spent entire time in model
|
|
//NOTE: will we even get a collision then?
|
|
doDmg *= length;
|
|
}
|
|
else if ( hitEntDmgAdd[i] && hitEntDmgSub[i] )
|
|
{//we did enter and exit
|
|
doDmg *= hitEntDmgAdd[i] - hitEntDmgSub[i];
|
|
}
|
|
else if ( !hitEntDmgAdd[i] )
|
|
{//we didn't exit, just entered
|
|
doDmg *= length - hitEntDmgSub[i];
|
|
}
|
|
else if ( !hitEntDmgSub[i] )
|
|
{//we didn't enter, only exited
|
|
doDmg *= hitEntDmgAdd[i];
|
|
}
|
|
if ( doDmg > 0 )
|
|
{
|
|
WP_SaberDamageAdd( 1.0, hitEntNum[i], dmgDir, bladeVec, hitEntNormal[i], hitEntPoint[i], ceil(doDmg), hitEntStartFrac[i], trHitLoc[i], trDismember[i], trDismemberLoc[i] );
|
|
}
|
|
}
|
|
}
|
|
return (qboolean)(numHitEnts>0);
|
|
}
|
|
|
|
void WP_SaberBlockEffect( gentity_t *attacker, int saberNum, int bladeNum, vec3_t position, vec3_t normal, qboolean cutNotBlock )
|
|
{
|
|
saberInfo_t *saber = NULL;
|
|
|
|
if ( attacker && attacker->client )
|
|
{
|
|
saber = &attacker->client->ps.saber[saberNum];
|
|
}
|
|
|
|
if ( saber
|
|
&& !WP_SaberBladeUseSecondBladeStyle( saber, bladeNum )
|
|
&& saber->blockEffect )
|
|
{
|
|
if ( normal )
|
|
{
|
|
G_PlayEffect( saber->blockEffect, position, normal );
|
|
}
|
|
else
|
|
{
|
|
G_PlayEffect( saber->blockEffect, position );
|
|
}
|
|
}
|
|
else if ( saber
|
|
&& WP_SaberBladeUseSecondBladeStyle( saber, bladeNum )
|
|
&& saber->blockEffect2 )
|
|
{
|
|
if ( normal )
|
|
{
|
|
G_PlayEffect( saber->blockEffect2, position, normal );
|
|
}
|
|
else
|
|
{
|
|
G_PlayEffect( saber->blockEffect2, position );
|
|
}
|
|
}
|
|
else if ( cutNotBlock )
|
|
{
|
|
if ( normal )
|
|
{
|
|
G_PlayEffect( "saber/saber_cut", position, normal );
|
|
}
|
|
else
|
|
{
|
|
G_PlayEffect( "saber/saber_cut", position );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
|
|
if ( normal )
|
|
{
|
|
G_PlayEffect( "saber/saber_block", position, normal );
|
|
}
|
|
else
|
|
{
|
|
G_PlayEffect( "saber/saber_block", position );
|
|
}
|
|
}
|
|
}
|
|
|
|
void WP_SaberKnockaway( gentity_t *attacker, trace_t *tr )
|
|
{
|
|
WP_SaberDrop( attacker, &g_entities[attacker->client->ps.saberEntityNum] );
|
|
WP_SaberBlockSound( attacker, NULL, 0, 0 );
|
|
//G_Sound( &g_entities[attacker->client->ps.saberEntityNum], G_SoundIndex( va( "sound/weapons/saber/saberblock%d.wav", Q_irand(1, 9) ) ) );
|
|
WP_SaberBlockEffect( attacker, 0, 0, tr->endpos, NULL, qfalse );
|
|
saberHitFraction = tr->fraction;
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer )
|
|
{
|
|
gi.Printf( S_COLOR_MAGENTA"WP_SaberKnockaway: saberHitFraction %4.2f\n", saberHitFraction );
|
|
}
|
|
#endif
|
|
VectorCopy( tr->endpos, saberHitLocation );
|
|
saberHitEntity = tr->entityNum;
|
|
if ( !g_noClashFlare )
|
|
{
|
|
g_saberFlashTime = level.time-50;
|
|
VectorCopy( saberHitLocation, g_saberFlashPos );
|
|
}
|
|
|
|
//FIXME: make hitEnt play an attack anim or some other special anim when this happens
|
|
//gentity_t *hitEnt = &g_entities[tr->entityNum];
|
|
//NPC_SetAnim( hitEnt, SETANIM_BOTH, BOTH_KNOCKSABER, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
|
|
qboolean G_InCinematicSaberAnim( gentity_t *self )
|
|
{
|
|
if ( self->NPC
|
|
&& self->NPC->behaviorState == BS_CINEMATIC
|
|
&& (self->client->ps.torsoAnim == BOTH_CIN_16 ||self->client->ps.torsoAnim == BOTH_CIN_17) )
|
|
{
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
#define SABER_COLLISION_DIST 6//was 2//was 4//was 8//was 16
|
|
extern qboolean InFront( vec3_t spot, vec3_t from, vec3_t fromAngles, float threshHold = 0.0f );
|
|
qboolean WP_SaberDamageForTrace( int ignore, vec3_t start, vec3_t end, float dmg,
|
|
vec3_t bladeDir, qboolean noGhoul, int attackStrength,
|
|
saberType_t saberType, qboolean extrapolate,
|
|
int saberNum, int bladeNum )
|
|
{
|
|
trace_t tr;
|
|
vec3_t dir;
|
|
int mask = (MASK_SHOT|CONTENTS_LIGHTSABER);
|
|
gentity_t *attacker = &g_entities[ignore];
|
|
|
|
vec3_t end2;
|
|
VectorCopy( end, end2 );
|
|
if ( extrapolate )
|
|
{
|
|
//NOTE: since we can no longer use the predicted point, extrapolate the trace some.
|
|
// this may allow saber hits that aren't actually hits, but it doesn't look too bad
|
|
vec3_t diff;
|
|
VectorSubtract( end, start, diff );
|
|
VectorNormalize( diff );
|
|
VectorMA( end2, SABER_EXTRAPOLATE_DIST, diff, end2 );
|
|
}
|
|
|
|
if ( !noGhoul )
|
|
{
|
|
float useRadiusForDamage = 0;
|
|
|
|
if ( attacker
|
|
&& attacker->client )
|
|
{//see if we're not drawing the blade, if so, do a trace based on radius of blade (because the radius is being used to simulate a larger/smaller piece of a solid weapon)...
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( &attacker->client->ps.saber[saberNum], bladeNum )
|
|
&& (attacker->client->ps.saber[saberNum].saberFlags2&SFL2_NO_BLADE) )
|
|
{//not drawing blade
|
|
useRadiusForDamage = attacker->client->ps.saber[saberNum].blade[bladeNum].radius;
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( &attacker->client->ps.saber[saberNum], bladeNum )
|
|
&& (attacker->client->ps.saber[saberNum].saberFlags2&SFL2_NO_BLADE2) )
|
|
{//not drawing blade
|
|
useRadiusForDamage = attacker->client->ps.saber[saberNum].blade[bladeNum].radius;
|
|
}
|
|
}
|
|
if ( !useRadiusForDamage )
|
|
{//do normal check for larger-size saber traces
|
|
if ( !attacker->s.number
|
|
|| (attacker->client
|
|
&& (attacker->client->playerTeam==TEAM_PLAYER
|
|
|| attacker->client->NPC_class==CLASS_SHADOWTROOPER
|
|
|| attacker->client->NPC_class==CLASS_ALORA
|
|
|| (attacker->NPC && (attacker->NPC->aiFlags&NPCAI_BOSS_CHARACTER))
|
|
)
|
|
)
|
|
)//&&attackStrength==FORCE_LEVEL_3)
|
|
{
|
|
useRadiusForDamage = 2;
|
|
}
|
|
}
|
|
|
|
if ( useRadiusForDamage > 0 )//&&attackStrength==FORCE_LEVEL_3)
|
|
{//player,. player allies, shadowtroopers, tavion and desann use larger traces
|
|
vec3_t traceMins = {-useRadiusForDamage,-useRadiusForDamage,-useRadiusForDamage}, traceMaxs = {useRadiusForDamage,useRadiusForDamage,useRadiusForDamage};
|
|
gi.trace( &tr, start, traceMins, traceMaxs, end2, ignore, mask, G2_COLLIDE, 10 );//G2_SUPERSIZEDBBOX
|
|
}
|
|
/*
|
|
else if ( !attacker->s.number )
|
|
{
|
|
vec3_t traceMins = {-1,-1,-1}, traceMaxs = {1,1,1};
|
|
gi.trace( &tr, start, traceMins, traceMaxs, end2, ignore, mask, G2_COLLIDE, 10 );//G2_SUPERSIZEDBBOX
|
|
}
|
|
*/
|
|
else
|
|
{//reborn use smaller traces
|
|
gi.trace( &tr, start, NULL, NULL, end2, ignore, mask, G2_COLLIDE, 10 );//G2_SUPERSIZEDBBOX
|
|
}
|
|
}
|
|
else
|
|
{
|
|
gi.trace( &tr, start, NULL, NULL, end2, ignore, mask, G2_NOCOLLIDE, 10 );
|
|
}
|
|
|
|
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer > 1 )
|
|
{
|
|
if ( attacker != NULL && attacker->client != NULL )
|
|
{
|
|
G_DebugLine(start, end2, FRAMETIME, WPDEBUG_SaberColor( attacker->client->ps.saber[0].blade[0].color ), qtrue);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if ( tr.entityNum == ENTITYNUM_NONE )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
if ( tr.entityNum == ENTITYNUM_WORLD )
|
|
{
|
|
if ( attacker && attacker->client && (attacker->client->ps.saber[saberNum].saberFlags&SFL_BOUNCE_ON_WALLS) )
|
|
{
|
|
VectorCopy( tr.endpos, saberHitLocation );
|
|
VectorCopy( tr.plane.normal, saberHitNormal );
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
if ( &g_entities[tr.entityNum] )
|
|
{
|
|
gentity_t *hitEnt = &g_entities[tr.entityNum];
|
|
gentity_t *owner = g_entities[tr.entityNum].owner;
|
|
if ( hitEnt->contents & CONTENTS_LIGHTSABER )
|
|
{
|
|
if ( attacker && attacker->client && attacker->client->ps.saberInFlight )
|
|
{//thrown saber hit something
|
|
if ( owner
|
|
&& owner->s.number
|
|
&& owner->client
|
|
&& owner->NPC
|
|
&& owner->health > 0 )
|
|
{
|
|
if ( owner->client->NPC_class == CLASS_ALORA )
|
|
{//alora takes less damage
|
|
dmg *= 0.25f;
|
|
}
|
|
else if ( owner->client->NPC_class == CLASS_TAVION
|
|
/*|| (owner->client->NPC_class == CLASS_SHADOWTROOPER && !Q_irand( 0, g_spskill->integer*3 ))
|
|
|| (Q_irand( -5, owner->NPC->rank ) > RANK_CIVILIAN && !Q_irand( 0, g_spskill->integer*3 ))*/ )
|
|
{//Tavion can toss a blocked thrown saber aside
|
|
WP_SaberKnockaway( attacker, &tr );
|
|
Jedi_PlayDeflectSound( owner );
|
|
return qfalse;
|
|
}
|
|
}
|
|
}
|
|
//FIXME: take target FP_SABER_DEFENSE and attacker FP_SABER_OFFENSE into account here somehow?
|
|
qboolean sabersIntersect = WP_SabersIntersect( attacker, saberNum, bladeNum, owner, qfalse );//qtrue );
|
|
float sabersDist;
|
|
if ( attacker && attacker->client && attacker->client->ps.saberInFlight
|
|
&& owner && owner->s.number == 0 && (g_saberAutoBlocking->integer||attacker->client->ps.saberBlockingTime>level.time) )//NPC flying saber hit player's saber bounding box
|
|
{//players have g_saberAutoBlocking, do the more generous check against flying sabers
|
|
//FIXME: instead of hitting the player's saber bounding box
|
|
//and picking an anim afterwards, have him use AI similar
|
|
//to the AI the jedi use for picking a saber melee block...?
|
|
sabersDist = 0;
|
|
}
|
|
else
|
|
{//sabers must actually collide with the attacking saber
|
|
sabersDist = WP_SabersDistance( attacker, owner );
|
|
if ( attacker && attacker->client && attacker->client->ps.saberInFlight )
|
|
{
|
|
sabersDist /= 2.0f;
|
|
if ( sabersDist <= 16.0f )
|
|
{
|
|
sabersIntersect = qtrue;
|
|
}
|
|
}
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer > 1 )
|
|
{
|
|
gi.Printf( "sabersDist: %4.2f\n", sabersDist );
|
|
}
|
|
#endif//FINAL_BUILD
|
|
}
|
|
if ( sabersCrossed == -1 || sabersCrossed > sabersDist )
|
|
{
|
|
sabersCrossed = sabersDist;
|
|
}
|
|
float collisionDist;
|
|
if ( g_saberRealisticCombat->integer )
|
|
{
|
|
collisionDist = SABER_COLLISION_DIST;
|
|
}
|
|
else
|
|
{
|
|
collisionDist = SABER_COLLISION_DIST+6+g_spskill->integer*4;
|
|
}
|
|
{
|
|
if ( G_InCinematicSaberAnim( owner )
|
|
&& G_InCinematicSaberAnim( attacker ) )
|
|
{
|
|
sabersIntersect = qtrue;
|
|
}
|
|
}
|
|
if ( owner && owner->client && (attacker != NULL)
|
|
&& (sabersDist > collisionDist )//|| !InFront( attacker->currentOrigin, owner->currentOrigin, owner->client->ps.viewangles, 0.35f ))
|
|
&& !sabersIntersect )//was qtrue, but missed too much?
|
|
{//swing came from behind and/or was not stopped by a lightsaber
|
|
//re-try the trace without checking for lightsabers
|
|
gi.trace ( &tr, start, NULL, NULL, end2, ignore, mask&~CONTENTS_LIGHTSABER, G2_NOCOLLIDE, 10 );
|
|
if ( tr.entityNum == ENTITYNUM_WORLD )
|
|
{
|
|
return qtrue;
|
|
}
|
|
if ( tr.entityNum == ENTITYNUM_NONE || &g_entities[tr.entityNum] == NULL )
|
|
{//didn't hit the owner
|
|
/*
|
|
if ( attacker
|
|
&& attacker->client
|
|
&& (PM_SaberInAttack( attacker->client->ps.saberMove ) || PM_SaberInStart( attacker->client->ps.saberMove ))
|
|
&& DistanceSquared( tr.endpos, owner->currentOrigin ) < 10000 )
|
|
{
|
|
if ( owner->NPC
|
|
&& !owner->client->ps.saberInFlight
|
|
&& owner->client->ps.saberBlocked != BLOCKED_PARRY_BROKEN
|
|
&& !Jedi_SaberBusy( owner ) )
|
|
{//owner parried, just make sure they're saber is in the right spot - only does this if they're not already doing something with saber
|
|
if ( g_spskill->integer && (g_spskill->integer > 1 || Q_irand( 0, 1 )))
|
|
{//if on easy, they don't cheat like this, if on medium, they cheat 50% of the time, if on hard, they always cheat
|
|
//FIXME: also take into account the owner's FP_DEFENSE?
|
|
if ( Q_irand( 0, owner->NPC->rank ) >= RANK_CIVILIAN )
|
|
{//lower-rank Jedi aren't as good blockers
|
|
vec3_t attDir;
|
|
VectorSubtract( end2, start, attDir );
|
|
VectorNormalize( attDir );
|
|
Jedi_SaberBlockGo( owner, owner->NPC->last_ucmd, start, attDir, NULL );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
return qfalse; // Exit, but we didn't hit the wall.
|
|
}
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer > 1 )
|
|
{
|
|
if ( !attacker->s.number )
|
|
{
|
|
gi.Printf( S_COLOR_MAGENTA"%d saber hit owner through saber %4.2f, dist = %4.2f\n", level.time, saberHitFraction, sabersDist );
|
|
}
|
|
}
|
|
#endif//FINAL_BUILD
|
|
hitEnt = &g_entities[tr.entityNum];
|
|
owner = g_entities[tr.entityNum].owner;
|
|
}
|
|
else
|
|
{//hit a lightsaber
|
|
if ( (tr.fraction < saberHitFraction || tr.startsolid)
|
|
&& sabersDist < (8.0f+g_spskill->value)*4.0f// 50.0f//16.0f
|
|
&& (sabersIntersect || sabersDist < (4.0f+g_spskill->value)*2.0f) )//32.0f) )
|
|
{ // This saber hit closer than the last one.
|
|
if ( (tr.allsolid || tr.startsolid) && owner && owner->client )
|
|
{//tr.fraction will be 0, unreliable... so calculate actual
|
|
float dist = Distance( start, end2 );
|
|
if ( dist )
|
|
{
|
|
float hitFrac = WP_SabersDistance( attacker, owner )/dist;
|
|
if ( hitFrac > 1.0f )
|
|
{//umm... minimum distance between sabers was longer than trace...?
|
|
hitFrac = 1.0f;
|
|
}
|
|
if ( hitFrac < saberHitFraction )
|
|
{
|
|
saberHitFraction = hitFrac;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
saberHitFraction = 0.0f;
|
|
}
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer > 1 )
|
|
{
|
|
if ( !attacker->s.number )
|
|
{
|
|
gi.Printf( S_COLOR_GREEN"%d saber hit saber dist %4.2f allsolid %4.2f\n", level.time, sabersDist, saberHitFraction );
|
|
}
|
|
}
|
|
#endif//FINAL_BUILD
|
|
}
|
|
else
|
|
{
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer > 1 )
|
|
{
|
|
if ( !attacker->s.number )
|
|
{
|
|
gi.Printf( S_COLOR_BLUE"%d saber hit saber dist %4.2f, frac %4.2f\n", level.time, sabersDist, saberHitFraction );
|
|
}
|
|
saberHitFraction = tr.fraction;
|
|
}
|
|
#endif//FINAL_BUILD
|
|
}
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer )
|
|
{
|
|
gi.Printf( S_COLOR_MAGENTA"hit saber: saberHitFraction %4.2f, allsolid %d, startsolid %d\n", saberHitFraction, tr.allsolid, tr.startsolid );
|
|
}
|
|
#endif//FINAL_BUILD
|
|
VectorCopy(tr.endpos, saberHitLocation);
|
|
saberHitEntity = tr.entityNum;
|
|
}
|
|
/*
|
|
if ( owner
|
|
&& owner->client
|
|
&& attacker
|
|
&& attacker->client
|
|
&& (PM_SaberInAttack( attacker->client->ps.saberMove ) || PM_SaberInStart( attacker->client->ps.saberMove ))
|
|
&& DistanceSquared( tr.endpos, owner->currentOrigin ) < 10000 )
|
|
{
|
|
if ( owner->NPC
|
|
&& !owner->client->ps.saberInFlight
|
|
&& owner->client->ps.saberBlocked != BLOCKED_PARRY_BROKEN
|
|
&& !Jedi_SaberBusy( owner ) )
|
|
{//owner parried, just make sure they're saber is in the right spot - only does this if they're not already doing something with saber
|
|
if ( g_spskill->integer && (g_spskill->integer > 1 || Q_irand( 0, 1 )))
|
|
{//if on easy, they don't cheat like this, if on medium, they cheat 50% of the time, if on hard, they always cheat
|
|
//FIXME: also take into account the owner's FP_DEFENSE?
|
|
if ( Q_irand( 0, owner->NPC->rank ) >= RANK_CIVILIAN )
|
|
{//lower-rank Jedi aren't as good blockers
|
|
vec3_t attDir;
|
|
VectorSubtract( end2, start, attDir );
|
|
VectorNormalize( attDir );
|
|
Jedi_SaberBlockGo( owner, owner->NPC->last_ucmd, start, attDir, NULL );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
//FIXME: check to see if we broke the saber
|
|
// go through the impacted surfaces and call WP_BreakSaber
|
|
// PROBLEM: saberEnt doesn't actually have a saber g2 model
|
|
// and/or isn't in same location as saber model attached
|
|
// to the client. We'd have to fake it somehow...
|
|
return qfalse; // Exit, but we didn't hit the wall.
|
|
}
|
|
}
|
|
else
|
|
{
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer > 1 )
|
|
{
|
|
if ( !attacker->s.number )
|
|
{
|
|
gi.Printf( S_COLOR_RED"%d saber hit owner directly %4.2f\n", level.time, saberHitFraction );
|
|
}
|
|
}
|
|
#endif//FINAL_BUILD
|
|
}
|
|
|
|
if ( attacker && attacker->client && attacker->client->ps.saberInFlight )
|
|
{//thrown saber hit something
|
|
if ( ( hitEnt && hitEnt->client && hitEnt->health > 0 && ( hitEnt->client->NPC_class == CLASS_DESANN || !Q_stricmp("Yoda",hitEnt->NPC_type) || hitEnt->client->NPC_class == CLASS_LUKE || hitEnt->client->NPC_class == CLASS_BOBAFETT || (hitEnt->client->ps.powerups[PW_GALAK_SHIELD] > 0) ) ) ||
|
|
( owner && owner->client && owner->health > 0 && ( owner->client->NPC_class == CLASS_DESANN || !Q_stricmp("Yoda",owner->NPC_type) || owner->client->NPC_class == CLASS_LUKE || (owner->client->ps.powerups[PW_GALAK_SHIELD] > 0) ) ) )
|
|
{//Luke and Desann slap thrown sabers aside
|
|
//FIXME: control the direction of the thrown saber... if hit Galak's shield, bounce directly away from his origin?
|
|
WP_SaberKnockaway( attacker, &tr );
|
|
if ( hitEnt->client )
|
|
{
|
|
Jedi_PlayDeflectSound( hitEnt );
|
|
}
|
|
else
|
|
{
|
|
Jedi_PlayDeflectSound( owner );
|
|
}
|
|
return qfalse; // Exit, but we didn't hit the wall.
|
|
}
|
|
}
|
|
|
|
if ( hitEnt->takedamage )
|
|
{
|
|
//no team damage: if ( !hitEnt->client || attacker == NULL || !attacker->client || (hitEnt->client->playerTeam != attacker->client->playerTeam) )
|
|
{
|
|
vec3_t bladeVec={0};
|
|
if ( attacker && attacker->client )
|
|
{
|
|
VectorScale( bladeDir, attacker->client->ps.saber[saberNum].blade[bladeNum].length, bladeVec );
|
|
}
|
|
//multiply the damage by the total distance of the swipe
|
|
VectorSubtract( end2, start, dir );
|
|
float len = VectorNormalize( dir );//VectorLength( dir );
|
|
if ( noGhoul || !hitEnt->ghoul2.size() )
|
|
{//we weren't doing a ghoul trace
|
|
int hitEffect = 0;
|
|
if ( dmg >= 1.0 && hitEnt->bmodel )
|
|
{
|
|
dmg = 1.0;
|
|
}
|
|
if ( len > 1 )
|
|
{
|
|
dmg *= len;
|
|
}
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer > 1 )
|
|
{
|
|
if ( !(hitEnt->contents & CONTENTS_LIGHTSABER) )
|
|
{
|
|
gi.Printf( S_COLOR_GREEN"Hit ent, but no ghoul collisions\n" );
|
|
}
|
|
}
|
|
#endif
|
|
float trFrac, dmgFrac;
|
|
if ( tr.allsolid )
|
|
{//totally inside them
|
|
trFrac = 1.0;
|
|
dmgFrac = 0.0;
|
|
}
|
|
else if ( tr.startsolid )
|
|
{//started inside them
|
|
//we don't know how much was inside, we know it's less than all, so use half?
|
|
trFrac = 0.5;
|
|
dmgFrac = 0.0;
|
|
}
|
|
else
|
|
{//started outside them and hit them
|
|
//yeah. this doesn't account for coming out the other wide, but we can worry about that later (use ghoul2)
|
|
trFrac = (1.0f - tr.fraction);
|
|
dmgFrac = tr.fraction;
|
|
}
|
|
vec3_t backdir;
|
|
VectorScale( dir, -1, backdir );
|
|
WP_SaberDamageAdd( trFrac, tr.entityNum, dir, bladeVec, backdir, tr.endpos, dmg, dmgFrac, HL_NONE, qfalse, HL_NONE );
|
|
if ( !tr.allsolid && !tr.startsolid )
|
|
{
|
|
VectorScale( dir, -1, dir );
|
|
}
|
|
if ( hitEnt != NULL )
|
|
{
|
|
if ( hitEnt->client )
|
|
{
|
|
//don't do blood sparks on non-living things
|
|
class_t npc_class = hitEnt->client->NPC_class;
|
|
if ( npc_class == CLASS_SEEKER || npc_class == CLASS_PROBE || npc_class == CLASS_MOUSE ||
|
|
npc_class == CLASS_GONK || npc_class == CLASS_R2D2 || npc_class == CLASS_R5D2 || npc_class == CLASS_REMOTE ||
|
|
npc_class == CLASS_PROTOCOL || npc_class == CLASS_MARK1 || npc_class == CLASS_MARK2 ||
|
|
npc_class == CLASS_INTERROGATOR || npc_class == CLASS_ATST || npc_class == CLASS_SENTRY )
|
|
{
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( &attacker->client->ps.saber[saberNum], bladeNum )
|
|
&& attacker->client->ps.saber[saberNum].hitOtherEffect )
|
|
{
|
|
hitEffect = attacker->client->ps.saber[saberNum].hitOtherEffect;
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( &attacker->client->ps.saber[saberNum], bladeNum )
|
|
&& attacker->client->ps.saber[saberNum].hitOtherEffect2 )
|
|
{
|
|
hitEffect = attacker->client->ps.saber[saberNum].hitOtherEffect2;
|
|
}
|
|
else
|
|
{
|
|
hitEffect = G_EffectIndex( hit_sparks );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( dmg )
|
|
{//only do these effects if actually trying to damage the thing...
|
|
if ( (hitEnt->svFlags&SVF_BBRUSH)//a breakable brush
|
|
&& ( (hitEnt->spawnflags&1)//INVINCIBLE
|
|
||(hitEnt->flags&FL_DMG_BY_HEAVY_WEAP_ONLY)//HEAVY weapon damage only
|
|
||(hitEnt->NPC_targetname&&attacker&&attacker->targetname&&Q_stricmp(attacker->targetname,hitEnt->NPC_targetname)) ) )//only breakable by an entity who is not the attacker
|
|
{//no hit effect (besides regular client-side one)
|
|
}
|
|
else
|
|
{
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( &attacker->client->ps.saber[saberNum], bladeNum )
|
|
&& attacker->client->ps.saber[saberNum].hitOtherEffect )
|
|
{
|
|
hitEffect = attacker->client->ps.saber[saberNum].hitOtherEffect;
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( &attacker->client->ps.saber[saberNum], bladeNum )
|
|
&& attacker->client->ps.saber[saberNum].hitOtherEffect2 )
|
|
{
|
|
hitEffect = attacker->client->ps.saber[saberNum].hitOtherEffect2;
|
|
}
|
|
else
|
|
{
|
|
hitEffect = G_EffectIndex( hit_sparks );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( !g_saberNoEffects && hitEffect != 0 )
|
|
{
|
|
G_PlayEffect( hitEffect, tr.endpos, dir );//"saber_cut"
|
|
}
|
|
}
|
|
else
|
|
{//we were doing a ghoul trace
|
|
if ( !attacker
|
|
|| !attacker->client
|
|
|| attacker->client->ps.saberLockTime < level.time )
|
|
{
|
|
if ( !WP_SaberDamageEffects( &tr, start, len, dmg, dir, bladeVec, attacker->client->enemyTeam, saberType, &attacker->client->ps.saber[saberNum], bladeNum ) )
|
|
{//didn't hit a ghoul ent
|
|
/*
|
|
if ( && hitEnt->ghoul2.size() )
|
|
{//it was a ghoul2 model so we should have hit it
|
|
return qfalse;
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return qfalse;
|
|
}
|
|
|
|
#define LOCK_IDEAL_DIST_TOP 32.0f
|
|
#define LOCK_IDEAL_DIST_CIRCLE 48.0f
|
|
#define LOCK_IDEAL_DIST_JKA 46.0f//all of the new saberlocks are 46.08 from each other because Richard Lico is da MAN
|
|
extern void PM_SetAnimFrame( gentity_t *gent, int frame, qboolean torso, qboolean legs );
|
|
extern qboolean ValidAnimFileIndex ( int index );
|
|
int G_SaberLockAnim( int attackerSaberStyle, int defenderSaberStyle, int topOrSide, int lockOrBreakOrSuperBreak, int winOrLose )
|
|
{
|
|
int baseAnim = -1;
|
|
if ( lockOrBreakOrSuperBreak == SABERLOCK_LOCK )
|
|
{//special case: if we're using the same style and locking
|
|
if ( attackerSaberStyle == defenderSaberStyle
|
|
|| (attackerSaberStyle>=SS_FAST&&attackerSaberStyle<=SS_TAVION&&defenderSaberStyle>=SS_FAST&&defenderSaberStyle<=SS_TAVION) )
|
|
{//using same style
|
|
if ( winOrLose == SABERLOCK_LOSE )
|
|
{//you want the defender's stance...
|
|
switch ( defenderSaberStyle )
|
|
{
|
|
case SS_DUAL:
|
|
if ( topOrSide == SABERLOCK_TOP )
|
|
{
|
|
baseAnim = BOTH_LK_DL_DL_T_L_2;
|
|
}
|
|
else
|
|
{
|
|
baseAnim = BOTH_LK_DL_DL_S_L_2;
|
|
}
|
|
break;
|
|
case SS_STAFF:
|
|
if ( topOrSide == SABERLOCK_TOP )
|
|
{
|
|
baseAnim = BOTH_LK_ST_ST_T_L_2;
|
|
}
|
|
else
|
|
{
|
|
baseAnim = BOTH_LK_ST_ST_S_L_2;
|
|
}
|
|
break;
|
|
default:
|
|
if ( topOrSide == SABERLOCK_TOP )
|
|
{
|
|
baseAnim = BOTH_LK_S_S_T_L_2;
|
|
}
|
|
else
|
|
{
|
|
baseAnim = BOTH_LK_S_S_S_L_2;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ( baseAnim == -1 )
|
|
{
|
|
switch ( attackerSaberStyle )
|
|
{
|
|
case SS_DUAL:
|
|
switch ( defenderSaberStyle )
|
|
{
|
|
case SS_DUAL:
|
|
baseAnim = BOTH_LK_DL_DL_S_B_1_L;
|
|
break;
|
|
case SS_STAFF:
|
|
baseAnim = BOTH_LK_DL_ST_S_B_1_L;
|
|
break;
|
|
default://single
|
|
baseAnim = BOTH_LK_DL_S_S_B_1_L;
|
|
break;
|
|
}
|
|
break;
|
|
case SS_STAFF:
|
|
switch ( defenderSaberStyle )
|
|
{
|
|
case SS_DUAL:
|
|
baseAnim = BOTH_LK_ST_DL_S_B_1_L;
|
|
break;
|
|
case SS_STAFF:
|
|
baseAnim = BOTH_LK_ST_ST_S_B_1_L;
|
|
break;
|
|
default://single
|
|
baseAnim = BOTH_LK_ST_S_S_B_1_L;
|
|
break;
|
|
}
|
|
break;
|
|
default://single
|
|
switch ( defenderSaberStyle )
|
|
{
|
|
case SS_DUAL:
|
|
baseAnim = BOTH_LK_S_DL_S_B_1_L;
|
|
break;
|
|
case SS_STAFF:
|
|
baseAnim = BOTH_LK_S_ST_S_B_1_L;
|
|
break;
|
|
default://single
|
|
baseAnim = BOTH_LK_S_S_S_B_1_L;
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
//side lock or top lock?
|
|
if ( topOrSide == SABERLOCK_TOP )
|
|
{
|
|
baseAnim += 5;
|
|
}
|
|
//lock, break or superbreak?
|
|
if ( lockOrBreakOrSuperBreak == SABERLOCK_LOCK )
|
|
{
|
|
baseAnim += 2;
|
|
}
|
|
else
|
|
{//a break or superbreak
|
|
if ( lockOrBreakOrSuperBreak == SABERLOCK_SUPERBREAK )
|
|
{
|
|
baseAnim += 3;
|
|
}
|
|
//winner or loser?
|
|
if ( winOrLose == SABERLOCK_WIN )
|
|
{
|
|
baseAnim += 1;
|
|
}
|
|
}
|
|
}
|
|
return baseAnim;
|
|
}
|
|
|
|
qboolean G_CheckIncrementLockAnim( int anim, int winOrLose )
|
|
{
|
|
qboolean increment = qfalse;//???
|
|
//RULE: if you are the first style in the lock anim, you advance from LOSING position to WINNING position
|
|
// if you are the second style in the lock anim, you advance from WINNING position to LOSING position
|
|
switch ( anim )
|
|
{
|
|
//increment to win:
|
|
case BOTH_LK_DL_DL_S_L_1: //lock if I'm using dual vs. dual and I initiated
|
|
case BOTH_LK_DL_DL_S_L_2: //lock if I'm using dual vs. dual and other initiated
|
|
case BOTH_LK_DL_DL_T_L_1: //lock if I'm using dual vs. dual and I initiated
|
|
case BOTH_LK_DL_DL_T_L_2: //lock if I'm using dual vs. dual and other initiated
|
|
case BOTH_LK_DL_S_S_L_1: //lock if I'm using dual vs. a single
|
|
case BOTH_LK_DL_S_T_L_1: //lock if I'm using dual vs. a single
|
|
case BOTH_LK_DL_ST_S_L_1: //lock if I'm using dual vs. a staff
|
|
case BOTH_LK_DL_ST_T_L_1: //lock if I'm using dual vs. a staff
|
|
case BOTH_LK_S_S_S_L_1: //lock if I'm using single vs. a single and I initiated
|
|
case BOTH_LK_S_S_T_L_2: //lock if I'm using single vs. a single and other initiated
|
|
case BOTH_LK_ST_S_S_L_1: //lock if I'm using staff vs. a single
|
|
case BOTH_LK_ST_S_T_L_1: //lock if I'm using staff vs. a single
|
|
case BOTH_LK_ST_ST_T_L_1: //lock if I'm using staff vs. a staff and I initiated
|
|
case BOTH_LK_ST_ST_T_L_2: //lock if I'm using staff vs. a staff and other initiated
|
|
if ( winOrLose == SABERLOCK_WIN )
|
|
{
|
|
increment = qtrue;
|
|
}
|
|
else
|
|
{
|
|
increment = qfalse;
|
|
}
|
|
break;
|
|
|
|
//decrement to win:
|
|
case BOTH_LK_S_DL_S_L_1: //lock if I'm using single vs. a dual
|
|
case BOTH_LK_S_DL_T_L_1: //lock if I'm using single vs. a dual
|
|
case BOTH_LK_S_S_S_L_2: //lock if I'm using single vs. a single and other intitiated
|
|
case BOTH_LK_S_S_T_L_1: //lock if I'm using single vs. a single and I initiated
|
|
case BOTH_LK_S_ST_S_L_1: //lock if I'm using single vs. a staff
|
|
case BOTH_LK_S_ST_T_L_1: //lock if I'm using single vs. a staff
|
|
case BOTH_LK_ST_DL_S_L_1: //lock if I'm using staff vs. dual
|
|
case BOTH_LK_ST_DL_T_L_1: //lock if I'm using staff vs. dual
|
|
case BOTH_LK_ST_ST_S_L_1: //lock if I'm using staff vs. a staff and I initiated
|
|
case BOTH_LK_ST_ST_S_L_2: //lock if I'm using staff vs. a staff and other initiated
|
|
if ( winOrLose == SABERLOCK_WIN )
|
|
{
|
|
increment = qfalse;
|
|
}
|
|
else
|
|
{
|
|
increment = qtrue;
|
|
}
|
|
break;
|
|
default:
|
|
#ifndef FINAL_BUILD
|
|
Com_Printf( S_COLOR_RED"ERROR: unknown Saber Lock Anim: %s!!!\n", animTable[anim].name );
|
|
#endif
|
|
break;
|
|
}
|
|
return increment;
|
|
}
|
|
|
|
qboolean WP_SabersCheckLock2( gentity_t *attacker, gentity_t *defender, sabersLockMode_t lockMode )
|
|
{
|
|
animation_t *anim;
|
|
int attAnim, defAnim, advance = 0;
|
|
float attStart = 0.5f, defStart = 0.5f;
|
|
float idealDist = 48.0f;
|
|
//FIXME: this distances need to be modified by the lengths of the sabers involved...
|
|
//MATCH ANIMS
|
|
if ( lockMode == LOCK_KYLE_GRAB1
|
|
|| lockMode == LOCK_KYLE_GRAB2
|
|
|| lockMode == LOCK_KYLE_GRAB3 )
|
|
{
|
|
float numSpins = 1.0f;
|
|
idealDist = 46.0f;//42.0f;
|
|
attStart = defStart = 0.0f;
|
|
|
|
switch ( lockMode )
|
|
{
|
|
default:
|
|
case LOCK_KYLE_GRAB1:
|
|
attAnim = BOTH_KYLE_PA_1;
|
|
defAnim = BOTH_PLAYER_PA_1;
|
|
numSpins = 2.0f;
|
|
break;
|
|
case LOCK_KYLE_GRAB2:
|
|
attAnim = BOTH_KYLE_PA_3;
|
|
defAnim = BOTH_PLAYER_PA_3;
|
|
numSpins = 1.0f;
|
|
break;
|
|
case LOCK_KYLE_GRAB3:
|
|
attAnim = BOTH_KYLE_PA_2;
|
|
defAnim = BOTH_PLAYER_PA_2;
|
|
defender->forcePushTime = level.time + PM_AnimLength( defender->client->clientInfo.animFileIndex, BOTH_PLAYER_PA_2 );
|
|
numSpins = 3.0f;
|
|
break;
|
|
}
|
|
attacker->client->ps.SaberDeactivate();
|
|
defender->client->ps.SaberDeactivate();
|
|
if ( d_slowmodeath->integer > 3
|
|
&& ( defender->s.number < MAX_CLIENTS
|
|
|| attacker->s.number < MAX_CLIENTS ) )
|
|
{
|
|
if( ValidAnimFileIndex( attacker->client->clientInfo.animFileIndex ) )
|
|
{
|
|
int effectTime = PM_AnimLength( attacker->client->clientInfo.animFileIndex, (animNumber_t)attAnim );
|
|
int spinTime = floor((float)effectTime/numSpins);
|
|
int meFlags = (MEF_MULTI_SPIN);//MEF_NO_TIMESCALE|MEF_NO_VERTBOB|
|
|
if ( Q_irand( 0, 1 ) )
|
|
{
|
|
meFlags |= MEF_REVERSE_SPIN;
|
|
}
|
|
G_StartMatrixEffect( attacker, meFlags, effectTime, 0.75f, spinTime );
|
|
}
|
|
}
|
|
}
|
|
else if ( lockMode == LOCK_FORCE_DRAIN )
|
|
{
|
|
idealDist = 46.0f;//42.0f;
|
|
attStart = defStart = 0.0f;
|
|
|
|
attAnim = BOTH_FORCE_DRAIN_GRAB_START;
|
|
defAnim = BOTH_FORCE_DRAIN_GRABBED;
|
|
attacker->client->ps.SaberDeactivate();
|
|
defender->client->ps.SaberDeactivate();
|
|
}
|
|
else
|
|
{
|
|
if ( lockMode == LOCK_RANDOM )
|
|
{
|
|
lockMode = (sabersLockMode_t)Q_irand( (int)LOCK_FIRST, (int)(LOCK_RANDOM)-1 );
|
|
}
|
|
//FIXME: attStart% and idealDist will change per saber lock anim pairing... do we need a big table like in bg_panimate.cpp?
|
|
if ( attacker->client->ps.saberAnimLevel >= SS_FAST
|
|
&& attacker->client->ps.saberAnimLevel <= SS_TAVION
|
|
&& defender->client->ps.saberAnimLevel >= SS_FAST
|
|
&& defender->client->ps.saberAnimLevel <= SS_TAVION )
|
|
{//2 single sabers? Just do it the old way...
|
|
switch ( lockMode )
|
|
{
|
|
case LOCK_TOP:
|
|
attAnim = BOTH_BF2LOCK;// - starts in middle
|
|
defAnim = BOTH_BF1LOCK;// - starts in middle
|
|
attStart = defStart = 0.5f;
|
|
idealDist = LOCK_IDEAL_DIST_TOP;
|
|
break;
|
|
case LOCK_DIAG_TR:
|
|
attAnim = BOTH_CCWCIRCLELOCK; //- starts in middle
|
|
defAnim = BOTH_CWCIRCLELOCK;// - starts in middle
|
|
attStart = defStart = 0.5f;
|
|
idealDist = LOCK_IDEAL_DIST_CIRCLE;
|
|
break;
|
|
case LOCK_DIAG_TL:
|
|
attAnim = BOTH_CWCIRCLELOCK;// - starts in middle
|
|
defAnim = BOTH_CCWCIRCLELOCK;// - starts in middle
|
|
attStart = defStart = 0.5f;
|
|
idealDist = LOCK_IDEAL_DIST_CIRCLE;
|
|
break;
|
|
case LOCK_DIAG_BR:
|
|
attAnim = BOTH_CWCIRCLELOCK;// - starts on left, to right
|
|
defAnim = BOTH_CCWCIRCLELOCK;// - starts on right, to left
|
|
attStart = defStart = 0.85f;//move to end of anim
|
|
idealDist = LOCK_IDEAL_DIST_CIRCLE;
|
|
break;
|
|
case LOCK_DIAG_BL:
|
|
attAnim = BOTH_CCWCIRCLELOCK;// - starts on right, to left
|
|
defAnim = BOTH_CWCIRCLELOCK;// - starts on left, to right
|
|
attStart = defStart = 0.85f;//move to end of anim
|
|
idealDist = LOCK_IDEAL_DIST_CIRCLE;
|
|
break;
|
|
case LOCK_R:
|
|
attAnim = BOTH_CWCIRCLELOCK;// - starts on left, to right
|
|
defAnim = BOTH_CCWCIRCLELOCK;// - starts on right, to left
|
|
attStart = defStart = 0.75f;//move to end of anim
|
|
idealDist = LOCK_IDEAL_DIST_CIRCLE;
|
|
break;
|
|
case LOCK_L:
|
|
attAnim = BOTH_CCWCIRCLELOCK;// - starts on right, to left
|
|
defAnim = BOTH_CWCIRCLELOCK;// - starts on left, to right
|
|
attStart = defStart = 0.75f;//move to end of anim
|
|
idealDist = LOCK_IDEAL_DIST_CIRCLE;
|
|
break;
|
|
default:
|
|
return qfalse;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{//use the new system
|
|
idealDist = LOCK_IDEAL_DIST_JKA;//all of the new saberlocks are 46.08 from each other because Richard Lico is da MAN
|
|
if ( lockMode == LOCK_TOP )
|
|
{//top lock
|
|
attAnim = G_SaberLockAnim( attacker->client->ps.saberAnimLevel, defender->client->ps.saberAnimLevel, SABERLOCK_TOP, SABERLOCK_LOCK, SABERLOCK_WIN );
|
|
defAnim = G_SaberLockAnim( defender->client->ps.saberAnimLevel, attacker->client->ps.saberAnimLevel, SABERLOCK_TOP, SABERLOCK_LOCK, SABERLOCK_LOSE );
|
|
attStart = defStart = 0.5f;
|
|
}
|
|
else
|
|
{//side lock
|
|
switch ( lockMode )
|
|
{
|
|
case LOCK_DIAG_TR:
|
|
attAnim = G_SaberLockAnim( attacker->client->ps.saberAnimLevel, defender->client->ps.saberAnimLevel, SABERLOCK_SIDE, SABERLOCK_LOCK, SABERLOCK_WIN );
|
|
defAnim = G_SaberLockAnim( defender->client->ps.saberAnimLevel, attacker->client->ps.saberAnimLevel, SABERLOCK_SIDE, SABERLOCK_LOCK, SABERLOCK_LOSE );
|
|
attStart = defStart = 0.5f;
|
|
break;
|
|
case LOCK_DIAG_TL:
|
|
attAnim = G_SaberLockAnim( attacker->client->ps.saberAnimLevel, defender->client->ps.saberAnimLevel, SABERLOCK_SIDE, SABERLOCK_LOCK, SABERLOCK_LOSE );
|
|
defAnim = G_SaberLockAnim( defender->client->ps.saberAnimLevel, attacker->client->ps.saberAnimLevel, SABERLOCK_SIDE, SABERLOCK_LOCK, SABERLOCK_WIN );
|
|
attStart = defStart = 0.5f;
|
|
break;
|
|
case LOCK_DIAG_BR:
|
|
attAnim = G_SaberLockAnim( attacker->client->ps.saberAnimLevel, defender->client->ps.saberAnimLevel, SABERLOCK_SIDE, SABERLOCK_LOCK, SABERLOCK_WIN );
|
|
defAnim = G_SaberLockAnim( defender->client->ps.saberAnimLevel, attacker->client->ps.saberAnimLevel, SABERLOCK_SIDE, SABERLOCK_LOCK, SABERLOCK_LOSE );
|
|
if ( G_CheckIncrementLockAnim( attAnim, SABERLOCK_WIN ) )
|
|
{
|
|
attStart = 0.85f;//move to end of anim
|
|
}
|
|
else
|
|
{
|
|
attStart = 0.15f;//start at beginning of anim
|
|
}
|
|
if ( G_CheckIncrementLockAnim( defAnim, SABERLOCK_LOSE ) )
|
|
{
|
|
defStart = 0.85f;//start at end of anim
|
|
}
|
|
else
|
|
{
|
|
defStart = 0.15f;//start at beginning of anim
|
|
}
|
|
break;
|
|
case LOCK_DIAG_BL:
|
|
attAnim = G_SaberLockAnim( attacker->client->ps.saberAnimLevel, defender->client->ps.saberAnimLevel, SABERLOCK_SIDE, SABERLOCK_LOCK, SABERLOCK_LOSE );
|
|
defAnim = G_SaberLockAnim( defender->client->ps.saberAnimLevel, attacker->client->ps.saberAnimLevel, SABERLOCK_SIDE, SABERLOCK_LOCK, SABERLOCK_WIN );
|
|
if ( G_CheckIncrementLockAnim( attAnim, SABERLOCK_WIN ) )
|
|
{
|
|
attStart = 0.85f;//move to end of anim
|
|
}
|
|
else
|
|
{
|
|
attStart = 0.15f;//start at beginning of anim
|
|
}
|
|
if ( G_CheckIncrementLockAnim( defAnim, SABERLOCK_LOSE ) )
|
|
{
|
|
defStart = 0.85f;//start at end of anim
|
|
}
|
|
else
|
|
{
|
|
defStart = 0.15f;//start at beginning of anim
|
|
}
|
|
break;
|
|
case LOCK_R:
|
|
attAnim = G_SaberLockAnim( attacker->client->ps.saberAnimLevel, defender->client->ps.saberAnimLevel, SABERLOCK_SIDE, SABERLOCK_LOCK, SABERLOCK_LOSE );
|
|
defAnim = G_SaberLockAnim( defender->client->ps.saberAnimLevel, attacker->client->ps.saberAnimLevel, SABERLOCK_SIDE, SABERLOCK_LOCK, SABERLOCK_WIN );
|
|
if ( G_CheckIncrementLockAnim( attAnim, SABERLOCK_WIN ) )
|
|
{
|
|
attStart = 0.75f;//move to end of anim
|
|
}
|
|
else
|
|
{
|
|
attStart = 0.25f;//start at beginning of anim
|
|
}
|
|
if ( G_CheckIncrementLockAnim( defAnim, SABERLOCK_LOSE ) )
|
|
{
|
|
defStart = 0.75f;//start at end of anim
|
|
}
|
|
else
|
|
{
|
|
defStart = 0.25f;//start at beginning of anim
|
|
}
|
|
break;
|
|
case LOCK_L:
|
|
attAnim = G_SaberLockAnim( attacker->client->ps.saberAnimLevel, defender->client->ps.saberAnimLevel, SABERLOCK_SIDE, SABERLOCK_LOCK, SABERLOCK_WIN );
|
|
defAnim = G_SaberLockAnim( defender->client->ps.saberAnimLevel, attacker->client->ps.saberAnimLevel, SABERLOCK_SIDE, SABERLOCK_LOCK, SABERLOCK_LOSE );
|
|
//attacker starts with advantage
|
|
if ( G_CheckIncrementLockAnim( attAnim, SABERLOCK_WIN ) )
|
|
{
|
|
attStart = 0.75f;//move to end of anim
|
|
}
|
|
else
|
|
{
|
|
attStart = 0.25f;//start at beginning of anim
|
|
}
|
|
if ( G_CheckIncrementLockAnim( defAnim, SABERLOCK_LOSE ) )
|
|
{
|
|
defStart = 0.75f;//start at end of anim
|
|
}
|
|
else
|
|
{
|
|
defStart = 0.25f;//start at beginning of anim
|
|
}
|
|
break;
|
|
default:
|
|
return qfalse;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//set the proper anims
|
|
NPC_SetAnim( attacker, SETANIM_BOTH, attAnim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
NPC_SetAnim( defender, SETANIM_BOTH, defAnim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
//don't let them store a kick for the whole saberlock....
|
|
attacker->client->ps.saberMoveNext = defender->client->ps.saberMoveNext = LS_NONE;
|
|
//
|
|
if ( attStart > 0.0f )
|
|
{
|
|
if( ValidAnimFileIndex( attacker->client->clientInfo.animFileIndex ) )
|
|
{
|
|
anim = &level.knownAnimFileSets[attacker->client->clientInfo.animFileIndex].animations[attAnim];
|
|
advance = floor( anim->numFrames*attStart );
|
|
PM_SetAnimFrame( attacker, anim->firstFrame + advance, qtrue, qtrue );
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer )
|
|
{
|
|
Com_Printf( "%s starting saber lock, anim = %s, %d frames to go!\n", attacker->NPC_type, animTable[attAnim].name, anim->numFrames-advance );
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
if ( defStart > 0.0f )
|
|
{
|
|
if( ValidAnimFileIndex( defender->client->clientInfo.animFileIndex ) )
|
|
{
|
|
anim = &level.knownAnimFileSets[defender->client->clientInfo.animFileIndex].animations[defAnim];
|
|
advance = ceil( anim->numFrames*defStart );
|
|
PM_SetAnimFrame( defender, anim->firstFrame + advance, qtrue, qtrue );//was anim->firstFrame + anim->numFrames - advance, but that's wrong since they are matched anims
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer )
|
|
{
|
|
Com_Printf( "%s starting saber lock, anim = %s, %d frames to go!\n", defender->NPC_type, animTable[defAnim].name, advance );
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
VectorClear( attacker->client->ps.velocity );
|
|
VectorClear( attacker->client->ps.moveDir );
|
|
VectorClear( defender->client->ps.velocity );
|
|
VectorClear( defender->client->ps.moveDir );
|
|
if ( lockMode == LOCK_KYLE_GRAB1
|
|
|| lockMode == LOCK_KYLE_GRAB2
|
|
|| lockMode == LOCK_KYLE_GRAB3
|
|
|| lockMode == LOCK_FORCE_DRAIN )
|
|
{//not a real lock, just freeze them both in place
|
|
//can't move or attack
|
|
attacker->client->ps.pm_time = attacker->client->ps.weaponTime = attacker->client->ps.legsAnimTimer;
|
|
attacker->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
|
|
attacker->painDebounceTime = level.time + attacker->client->ps.pm_time;
|
|
if ( lockMode != LOCK_FORCE_DRAIN )
|
|
{
|
|
defender->client->ps.torsoAnimTimer += 200;
|
|
defender->client->ps.legsAnimTimer += 200;
|
|
}
|
|
defender->client->ps.pm_time = defender->client->ps.weaponTime = defender->client->ps.legsAnimTimer;
|
|
defender->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
|
|
if ( lockMode != LOCK_FORCE_DRAIN )
|
|
{
|
|
attacker->aimDebounceTime = level.time + attacker->client->ps.pm_time;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
attacker->client->ps.saberLockTime = defender->client->ps.saberLockTime = level.time + SABER_LOCK_TIME;
|
|
attacker->client->ps.legsAnimTimer = attacker->client->ps.torsoAnimTimer = defender->client->ps.legsAnimTimer = defender->client->ps.torsoAnimTimer = SABER_LOCK_TIME;
|
|
//attacker->client->ps.weaponTime = defender->client->ps.weaponTime = SABER_LOCK_TIME;
|
|
attacker->client->ps.saberLockEnemy = defender->s.number;
|
|
defender->client->ps.saberLockEnemy = attacker->s.number;
|
|
}
|
|
|
|
//MATCH ANGLES
|
|
if ( lockMode == LOCK_KYLE_GRAB1
|
|
|| lockMode == LOCK_KYLE_GRAB2
|
|
|| lockMode == LOCK_KYLE_GRAB3 )
|
|
{//not a real lock, just set pitch to 0
|
|
attacker->client->ps.viewangles[PITCH] = defender->client->ps.viewangles[PITCH] = 0;
|
|
}
|
|
else
|
|
{
|
|
//FIXME: if zDiff in elevation, make lower look up and upper look down and move them closer?
|
|
float defPitchAdd = 0, zDiff = ((attacker->currentOrigin[2]+attacker->client->standheight)-(defender->currentOrigin[2]+defender->client->standheight));
|
|
if ( zDiff > 24 )
|
|
{
|
|
defPitchAdd = -30;
|
|
}
|
|
else if ( zDiff < -24 )
|
|
{
|
|
defPitchAdd = 30;
|
|
}
|
|
else
|
|
{
|
|
defPitchAdd = zDiff/24.0f*-30.0f;
|
|
}
|
|
if ( attacker->NPC && defender->NPC )
|
|
{//if 2 NPCs, just set pitch to 0
|
|
attacker->client->ps.viewangles[PITCH] = -defPitchAdd;
|
|
defender->client->ps.viewangles[PITCH] = defPitchAdd;
|
|
}
|
|
else
|
|
{//if a player is involved, clamp player's pitch and match NPC's to player
|
|
if ( !attacker->s.number )
|
|
{
|
|
//clamp to defPitch
|
|
if ( attacker->client->ps.viewangles[PITCH] > -defPitchAdd + 10 )
|
|
{
|
|
attacker->client->ps.viewangles[PITCH] = -defPitchAdd + 10;
|
|
}
|
|
else if ( attacker->client->ps.viewangles[PITCH] < -defPitchAdd-10 )
|
|
{
|
|
attacker->client->ps.viewangles[PITCH] = -defPitchAdd-10;
|
|
}
|
|
//clamp to sane numbers
|
|
if ( attacker->client->ps.viewangles[PITCH] > 50 )
|
|
{
|
|
attacker->client->ps.viewangles[PITCH] = 50;
|
|
}
|
|
else if ( attacker->client->ps.viewangles[PITCH] < -50 )
|
|
{
|
|
attacker->client->ps.viewangles[PITCH] = -50;
|
|
}
|
|
defender->client->ps.viewangles[PITCH] = attacker->client->ps.viewangles[PITCH]*-1;
|
|
defPitchAdd = defender->client->ps.viewangles[PITCH];
|
|
}
|
|
else if ( !defender->s.number )
|
|
{
|
|
//clamp to defPitch
|
|
if ( defender->client->ps.viewangles[PITCH] > defPitchAdd + 10 )
|
|
{
|
|
defender->client->ps.viewangles[PITCH] = defPitchAdd + 10;
|
|
}
|
|
else if ( defender->client->ps.viewangles[PITCH] < defPitchAdd-10 )
|
|
{
|
|
defender->client->ps.viewangles[PITCH] = defPitchAdd-10;
|
|
}
|
|
//clamp to sane numbers
|
|
if ( defender->client->ps.viewangles[PITCH] > 50 )
|
|
{
|
|
defender->client->ps.viewangles[PITCH] = 50;
|
|
}
|
|
else if ( defender->client->ps.viewangles[PITCH] < -50 )
|
|
{
|
|
defender->client->ps.viewangles[PITCH] = -50;
|
|
}
|
|
defPitchAdd = defender->client->ps.viewangles[PITCH];
|
|
attacker->client->ps.viewangles[PITCH] = defender->client->ps.viewangles[PITCH]*-1;
|
|
}
|
|
}
|
|
}
|
|
vec3_t attAngles, defAngles, defDir;
|
|
VectorSubtract( defender->currentOrigin, attacker->currentOrigin, defDir );
|
|
VectorCopy( attacker->client->ps.viewangles, attAngles );
|
|
attAngles[YAW] = vectoyaw( defDir );
|
|
SetClientViewAngle( attacker, attAngles );
|
|
defAngles[PITCH] = attAngles[PITCH]*-1;
|
|
defAngles[YAW] = AngleNormalize180( attAngles[YAW] + 180);
|
|
defAngles[ROLL] = 0;
|
|
SetClientViewAngle( defender, defAngles );
|
|
|
|
//MATCH POSITIONS
|
|
vec3_t newOrg;
|
|
/*
|
|
idealDist -= fabs(defPitchAdd)/8.0f;
|
|
*/
|
|
float scale = (attacker->s.modelScale[0]+attacker->s.modelScale[1])*0.5f;
|
|
if ( scale && scale != 1.0f )
|
|
{
|
|
idealDist += 8*(scale-1.0f);
|
|
}
|
|
scale = (defender->s.modelScale[0]+defender->s.modelScale[1])*0.5f;
|
|
if ( scale && scale != 1.0f )
|
|
{
|
|
idealDist += 8*(scale-1.0f);
|
|
}
|
|
|
|
float diff = VectorNormalize( defDir ) - idealDist;//diff will be the total error in dist
|
|
//try to move attacker half the diff towards the defender
|
|
VectorMA( attacker->currentOrigin, diff*0.5f, defDir, newOrg );
|
|
trace_t trace;
|
|
gi.trace( &trace, attacker->currentOrigin, attacker->mins, attacker->maxs, newOrg, attacker->s.number, attacker->clipmask, (EG2_Collision)0, 0 );
|
|
if ( !trace.startsolid && !trace.allsolid )
|
|
{
|
|
G_SetOrigin( attacker, trace.endpos );
|
|
gi.linkentity( attacker );
|
|
}
|
|
//now get the defender's dist and do it for him too
|
|
vec3_t attDir;
|
|
VectorSubtract( attacker->currentOrigin, defender->currentOrigin, attDir );
|
|
diff = VectorNormalize( attDir ) - idealDist;//diff will be the total error in dist
|
|
//try to move defender all of the remaining diff towards the attacker
|
|
VectorMA( defender->currentOrigin, diff, attDir, newOrg );
|
|
gi.trace( &trace, defender->currentOrigin, defender->mins, defender->maxs, newOrg, defender->s.number, defender->clipmask, (EG2_Collision)0, 0 );
|
|
if ( !trace.startsolid && !trace.allsolid )
|
|
{
|
|
G_SetOrigin( defender, trace.endpos );
|
|
gi.linkentity( defender );
|
|
}
|
|
|
|
//DONE!
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
qboolean WP_SabersCheckLock( gentity_t *ent1, gentity_t *ent2 )
|
|
{
|
|
if ( ent1->client->playerTeam == ent2->client->playerTeam )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( ent1->client->NPC_class == CLASS_SABER_DROID
|
|
|| ent2->client->NPC_class == CLASS_SABER_DROID )
|
|
{//they don't have saberlock anims
|
|
return qfalse;
|
|
}
|
|
if ( ent1->client->ps.groundEntityNum == ENTITYNUM_NONE ||
|
|
ent2->client->ps.groundEntityNum == ENTITYNUM_NONE )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( (ent1->client->ps.saber[0].saberFlags&SFL_NOT_LOCKABLE)
|
|
|| (ent2->client->ps.saber[0].saberFlags&SFL_NOT_LOCKABLE) )
|
|
{//one of these sabers cannot lock (like a lance)
|
|
return qfalse;
|
|
}
|
|
if ( ent1->client->ps.dualSabers
|
|
&& ent1->client->ps.saber[1].Active()
|
|
&& (ent1->client->ps.saber[1].saberFlags&SFL_NOT_LOCKABLE) )
|
|
{//one of these sabers cannot lock (like a lance)
|
|
return qfalse;
|
|
}
|
|
if ( ent2->client->ps.dualSabers
|
|
&& ent2->client->ps.saber[1].Active()
|
|
&& (ent2->client->ps.saber[1].saberFlags&SFL_NOT_LOCKABLE) )
|
|
{//one of these sabers cannot lock (like a lance)
|
|
return qfalse;
|
|
}
|
|
if ( ent1->painDebounceTime > level.time-1000 || ent2->painDebounceTime > level.time-1000 )
|
|
{//can't saberlock if you're not ready
|
|
return qfalse;
|
|
}
|
|
if ( fabs( ent1->currentOrigin[2]-ent2->currentOrigin[2]) > 18 )
|
|
{
|
|
return qfalse;
|
|
}
|
|
float dist = DistanceSquared(ent1->currentOrigin,ent2->currentOrigin);
|
|
if ( dist < 64 || dist > 6400 )//( dist < 128 || dist > 2304 )
|
|
{//between 8 and 80 from each other//was 16 and 48
|
|
return qfalse;
|
|
}
|
|
if ( !InFOV( ent1, ent2, 40, 180 ) || !InFOV( ent2, ent1, 40, 180 ) )
|
|
{
|
|
return qfalse;
|
|
}
|
|
//Check for certain anims that *cannot* lock
|
|
//FIXME: there should probably be a whole *list* of these, but I'll put them in as they come up
|
|
if ( ent1->client->ps.torsoAnim == BOTH_A2_STABBACK1 && ent1->client->ps.torsoAnimTimer > 300 )
|
|
{//can't lock when saber is behind you
|
|
return qfalse;
|
|
}
|
|
if ( ent2->client->ps.torsoAnim == BOTH_A2_STABBACK1 && ent2->client->ps.torsoAnimTimer > 300 )
|
|
{//can't lock when saber is behind you
|
|
return qfalse;
|
|
}
|
|
if ( PM_LockedAnim( ent1->client->ps.torsoAnim )
|
|
|| PM_LockedAnim( ent2->client->ps.torsoAnim ) )
|
|
{//stuck doing something else
|
|
return qfalse;
|
|
}
|
|
if ( PM_SaberLockBreakAnim( ent1->client->ps.torsoAnim )
|
|
|| PM_SaberLockBreakAnim( ent2->client->ps.torsoAnim ) )
|
|
{//still finishing the last lock break!
|
|
return qfalse;
|
|
}
|
|
//BR to TL lock
|
|
if ( ent1->client->ps.torsoAnim == BOTH_A1_BR_TL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A2_BR_TL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A3_BR_TL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A4_BR_TL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A5_BR_TL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A6_BR_TL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A7_BR_TL )
|
|
{//ent1 is attacking in the opposite diagonal
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_DIAG_BR );
|
|
}
|
|
if ( ent2->client->ps.torsoAnim == BOTH_A1_BR_TL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A2_BR_TL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A3_BR_TL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A4_BR_TL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A5_BR_TL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A6_BR_TL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A7_BR_TL )
|
|
{//ent2 is attacking in the opposite diagonal
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_DIAG_BR );
|
|
}
|
|
//BL to TR lock
|
|
if ( ent1->client->ps.torsoAnim == BOTH_A1_BL_TR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A2_BL_TR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A3_BL_TR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A4_BL_TR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A5_BL_TR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A6_BL_TR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A7_BL_TR )
|
|
{//ent1 is attacking in the opposite diagonal
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_DIAG_BL );
|
|
}
|
|
if ( ent2->client->ps.torsoAnim == BOTH_A1_BL_TR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A2_BL_TR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A3_BL_TR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A4_BL_TR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A5_BL_TR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A6_BL_TR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A7_BL_TR )
|
|
{//ent2 is attacking in the opposite diagonal
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_DIAG_BL );
|
|
}
|
|
//L to R lock
|
|
if ( ent1->client->ps.torsoAnim == BOTH_A1__L__R ||
|
|
ent1->client->ps.torsoAnim == BOTH_A2__L__R ||
|
|
ent1->client->ps.torsoAnim == BOTH_A3__L__R ||
|
|
ent1->client->ps.torsoAnim == BOTH_A4__L__R ||
|
|
ent1->client->ps.torsoAnim == BOTH_A5__L__R ||
|
|
ent1->client->ps.torsoAnim == BOTH_A6__L__R ||
|
|
ent1->client->ps.torsoAnim == BOTH_A7__L__R )
|
|
{//ent1 is attacking l to r
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_L );
|
|
/*
|
|
if ( ent2BlockingPlayer )
|
|
{//player will block this anyway
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_L );
|
|
}
|
|
if ( ent2->client->ps.torsoAnim == BOTH_A1_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A2_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A3_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A4_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A5_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A6_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A7_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_P1_S1_TR ||
|
|
ent2->client->ps.torsoAnim == BOTH_P1_S1_BL )
|
|
{//ent2 is attacking or blocking on the r
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_L );
|
|
}
|
|
if ( ent2Boss && !Q_irand( 0, 3 ) )
|
|
{
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_L );
|
|
}
|
|
*/
|
|
}
|
|
if ( ent2->client->ps.torsoAnim == BOTH_A1__L__R ||
|
|
ent2->client->ps.torsoAnim == BOTH_A2__L__R ||
|
|
ent2->client->ps.torsoAnim == BOTH_A3__L__R ||
|
|
ent2->client->ps.torsoAnim == BOTH_A4__L__R ||
|
|
ent2->client->ps.torsoAnim == BOTH_A5__L__R ||
|
|
ent2->client->ps.torsoAnim == BOTH_A6__L__R ||
|
|
ent2->client->ps.torsoAnim == BOTH_A7__L__R )
|
|
{//ent2 is attacking l to r
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_L );
|
|
/*
|
|
if ( ent1BlockingPlayer )
|
|
{//player will block this anyway
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_L );
|
|
}
|
|
if ( ent1->client->ps.torsoAnim == BOTH_A1_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A2_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A3_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A4_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A5_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A6_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A7_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_P1_S1_TR ||
|
|
ent1->client->ps.torsoAnim == BOTH_P1_S1_BL )
|
|
{//ent1 is attacking or blocking on the r
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_L );
|
|
}
|
|
if ( ent1Boss && !Q_irand( 0, 3 ) )
|
|
{
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_L );
|
|
}
|
|
*/
|
|
}
|
|
//R to L lock
|
|
if ( ent1->client->ps.torsoAnim == BOTH_A1__R__L ||
|
|
ent1->client->ps.torsoAnim == BOTH_A2__R__L ||
|
|
ent1->client->ps.torsoAnim == BOTH_A3__R__L ||
|
|
ent1->client->ps.torsoAnim == BOTH_A4__R__L ||
|
|
ent1->client->ps.torsoAnim == BOTH_A5__R__L ||
|
|
ent1->client->ps.torsoAnim == BOTH_A6__R__L ||
|
|
ent1->client->ps.torsoAnim == BOTH_A7__R__L )
|
|
{//ent1 is attacking r to l
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_R );
|
|
/*
|
|
if ( ent2BlockingPlayer )
|
|
{//player will block this anyway
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_R );
|
|
}
|
|
if ( ent2->client->ps.torsoAnim == BOTH_A1_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A2_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A3_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A4_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A5_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A6_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A7_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_P1_S1_TL ||
|
|
ent2->client->ps.torsoAnim == BOTH_P1_S1_BR )
|
|
{//ent2 is attacking or blocking on the l
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_R );
|
|
}
|
|
if ( ent2Boss && !Q_irand( 0, 3 ) )
|
|
{
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_R );
|
|
}
|
|
*/
|
|
}
|
|
if ( ent2->client->ps.torsoAnim == BOTH_A1__R__L ||
|
|
ent2->client->ps.torsoAnim == BOTH_A2__R__L ||
|
|
ent2->client->ps.torsoAnim == BOTH_A3__R__L ||
|
|
ent2->client->ps.torsoAnim == BOTH_A4__R__L ||
|
|
ent2->client->ps.torsoAnim == BOTH_A5__R__L ||
|
|
ent2->client->ps.torsoAnim == BOTH_A6__R__L ||
|
|
ent2->client->ps.torsoAnim == BOTH_A7__R__L )
|
|
{//ent2 is attacking r to l
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_R );
|
|
/*
|
|
if ( ent1BlockingPlayer )
|
|
{//player will block this anyway
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_R );
|
|
}
|
|
if ( ent1->client->ps.torsoAnim == BOTH_A1_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A2_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A3_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A4_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A5_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A6_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A7_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_P1_S1_TL ||
|
|
ent1->client->ps.torsoAnim == BOTH_P1_S1_BR )
|
|
{//ent1 is attacking or blocking on the l
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_R );
|
|
}
|
|
if ( ent1Boss && !Q_irand( 0, 3 ) )
|
|
{
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_R );
|
|
}
|
|
*/
|
|
}
|
|
//TR to BL lock
|
|
if ( ent1->client->ps.torsoAnim == BOTH_A1_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A2_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A3_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A4_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A5_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A6_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A7_TR_BL )
|
|
{//ent1 is attacking diagonally
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_DIAG_TR );
|
|
/*
|
|
if ( ent2BlockingPlayer )
|
|
{//player will block this anyway
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_DIAG_TR );
|
|
}
|
|
if ( ent2->client->ps.torsoAnim == BOTH_A1_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A2_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A3_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A4_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A5_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A6_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A7_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_P1_S1_TL )
|
|
{//ent2 is attacking in the opposite diagonal
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_DIAG_TR );
|
|
}
|
|
if ( ent2->client->ps.torsoAnim == BOTH_A1_BR_TL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A2_BR_TL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A3_BR_TL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A4_BR_TL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A5_BR_TL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A6_BR_TL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A7_BR_TL ||
|
|
ent2->client->ps.torsoAnim == BOTH_P1_S1_BL )
|
|
{//ent2 is attacking in the opposite diagonal
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_DIAG_BL );
|
|
}
|
|
if ( ent2Boss && !Q_irand( 0, 3 ) )
|
|
{
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_DIAG_TR );
|
|
}
|
|
*/
|
|
}
|
|
|
|
if ( ent2->client->ps.torsoAnim == BOTH_A1_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A2_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A3_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A4_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A5_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A6_TR_BL ||
|
|
ent2->client->ps.torsoAnim == BOTH_A7_TR_BL )
|
|
{//ent2 is attacking diagonally
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_DIAG_TR );
|
|
/*
|
|
if ( ent1BlockingPlayer )
|
|
{//player will block this anyway
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_DIAG_TR );
|
|
}
|
|
if ( ent1->client->ps.torsoAnim == BOTH_A1_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A2_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A3_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A4_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A5_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A6_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A7_TR_BL ||
|
|
ent1->client->ps.torsoAnim == BOTH_P1_S1_TL )
|
|
{//ent1 is attacking in the opposite diagonal
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_DIAG_TR );
|
|
}
|
|
if ( ent1->client->ps.torsoAnim == BOTH_A1_BR_TL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A2_BR_TL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A3_BR_TL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A4_BR_TL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A5_BR_TL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A6_BR_TL ||
|
|
ent1->client->ps.torsoAnim == BOTH_A7_BR_TL ||
|
|
ent1->client->ps.torsoAnim == BOTH_P1_S1_BL )
|
|
{//ent1 is attacking in the opposite diagonal
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_DIAG_BL );
|
|
}
|
|
if ( ent1Boss && !Q_irand( 0, 3 ) )
|
|
{
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_DIAG_TR );
|
|
}
|
|
*/
|
|
}
|
|
|
|
//TL to BR lock
|
|
if ( ent1->client->ps.torsoAnim == BOTH_A1_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A2_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A3_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A4_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A5_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A6_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A7_TL_BR )
|
|
{//ent1 is attacking diagonally
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_DIAG_TL );
|
|
/*
|
|
if ( ent2BlockingPlayer )
|
|
{//player will block this anyway
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_DIAG_TL );
|
|
}
|
|
if ( ent2->client->ps.torsoAnim == BOTH_A1_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A2_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A3_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A4_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A5_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A6_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A7_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_P1_S1_TR )
|
|
{//ent2 is attacking in the opposite diagonal
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_DIAG_TL );
|
|
}
|
|
if ( ent2->client->ps.torsoAnim == BOTH_A1_BL_TR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A2_BL_TR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A3_BL_TR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A4_BL_TR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A5_BL_TR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A6_BL_TR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A7_BL_TR ||
|
|
ent2->client->ps.torsoAnim == BOTH_P1_S1_BR )
|
|
{//ent2 is attacking in the opposite diagonal
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_DIAG_BR );
|
|
}
|
|
if ( ent2Boss && !Q_irand( 0, 3 ) )
|
|
{
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_DIAG_TL );
|
|
}
|
|
*/
|
|
}
|
|
|
|
if ( ent2->client->ps.torsoAnim == BOTH_A1_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A2_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A3_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A4_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A5_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A6_TL_BR ||
|
|
ent2->client->ps.torsoAnim == BOTH_A7_TL_BR )
|
|
{//ent2 is attacking diagonally
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_DIAG_TL );
|
|
/*
|
|
if ( ent1BlockingPlayer )
|
|
{//player will block this anyway
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_DIAG_TL );
|
|
}
|
|
if ( ent1->client->ps.torsoAnim == BOTH_A1_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A2_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A3_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A4_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A5_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A6_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A7_TL_BR ||
|
|
ent1->client->ps.torsoAnim == BOTH_P1_S1_TR )
|
|
{//ent1 is attacking in the opposite diagonal
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_DIAG_TL );
|
|
}
|
|
if ( ent1->client->ps.torsoAnim == BOTH_A1_BL_TR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A2_BL_TR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A3_BL_TR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A4_BL_TR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A5_BL_TR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A6_BL_TR ||
|
|
ent1->client->ps.torsoAnim == BOTH_A7_BL_TR ||
|
|
ent1->client->ps.torsoAnim == BOTH_P1_S1_BR )
|
|
{//ent1 is attacking in the opposite diagonal
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_DIAG_BR );
|
|
}
|
|
if ( ent1Boss && !Q_irand( 0, 3 ) )
|
|
{
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_DIAG_TL );
|
|
}
|
|
*/
|
|
}
|
|
//T to B lock
|
|
if ( ent1->client->ps.torsoAnim == BOTH_A1_T__B_ ||
|
|
ent1->client->ps.torsoAnim == BOTH_A2_T__B_ ||
|
|
ent1->client->ps.torsoAnim == BOTH_A3_T__B_ ||
|
|
ent1->client->ps.torsoAnim == BOTH_A4_T__B_ ||
|
|
ent1->client->ps.torsoAnim == BOTH_A5_T__B_ ||
|
|
ent1->client->ps.torsoAnim == BOTH_A6_T__B_ ||
|
|
ent1->client->ps.torsoAnim == BOTH_A7_T__B_ )
|
|
{//ent1 is attacking top-down
|
|
/*
|
|
if ( ent2->client->ps.torsoAnim == BOTH_P1_S1_T_ ||
|
|
ent2->client->ps.torsoAnim == BOTH_K1_S1_T_ )
|
|
*/
|
|
{//ent2 is blocking at top
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_TOP );
|
|
}
|
|
}
|
|
|
|
if ( ent2->client->ps.torsoAnim == BOTH_A1_T__B_ ||
|
|
ent2->client->ps.torsoAnim == BOTH_A2_T__B_ ||
|
|
ent2->client->ps.torsoAnim == BOTH_A3_T__B_ ||
|
|
ent2->client->ps.torsoAnim == BOTH_A4_T__B_ ||
|
|
ent2->client->ps.torsoAnim == BOTH_A5_T__B_ ||
|
|
ent2->client->ps.torsoAnim == BOTH_A6_T__B_ ||
|
|
ent2->client->ps.torsoAnim == BOTH_A7_T__B_ )
|
|
{//ent2 is attacking top-down
|
|
/*
|
|
if ( ent1->client->ps.torsoAnim == BOTH_P1_S1_T_ ||
|
|
ent1->client->ps.torsoAnim == BOTH_K1_S1_T_ )
|
|
*/
|
|
{//ent1 is blocking at top
|
|
return WP_SabersCheckLock2( ent2, ent1, LOCK_TOP );
|
|
}
|
|
}
|
|
/*
|
|
if ( !Q_irand( 0, 10 ) )
|
|
{
|
|
return WP_SabersCheckLock2( ent1, ent2, LOCK_RANDOM );
|
|
}
|
|
*/
|
|
return qfalse;
|
|
}
|
|
|
|
qboolean WP_SaberParry( gentity_t *victim, gentity_t *attacker, int saberNum, int bladeNum )
|
|
{
|
|
if ( !victim || !victim->client || !attacker )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( Rosh_BeingHealed( victim ) )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( G_InCinematicSaberAnim( victim ) )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( PM_SuperBreakLoseAnim( victim->client->ps.torsoAnim )
|
|
|| PM_SuperBreakWinAnim( victim->client->ps.torsoAnim ) )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( victim->s.number || g_saberAutoBlocking->integer || victim->client->ps.saberBlockingTime > level.time )
|
|
{//either an NPC or a player who is blocking
|
|
if ( !PM_SaberInTransitionAny( victim->client->ps.saberMove )
|
|
&& !PM_SaberInBounce( victim->client->ps.saberMove )
|
|
&& !PM_SaberInKnockaway( victim->client->ps.saberMove ) )
|
|
{//I'm not attacking, in transition or in a bounce or knockaway, so play a parry
|
|
WP_SaberBlockNonRandom( victim, saberHitLocation, qfalse );
|
|
}
|
|
victim->client->ps.saberEventFlags |= SEF_PARRIED;
|
|
|
|
//since it was parried, take away any damage done
|
|
//FIXME: what if the damage was done before the parry?
|
|
WP_SaberClearDamageForEntNum( attacker, victim->s.number, saberNum, bladeNum );
|
|
|
|
//tell the victim to get mad at me
|
|
if ( victim->enemy != attacker && victim->client->playerTeam != attacker->client->playerTeam )
|
|
{//they're not mad at me and they're not on my team
|
|
G_ClearEnemy( victim );
|
|
G_SetEnemy( victim, attacker );
|
|
}
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
qboolean WP_BrokenParryKnockDown( gentity_t *victim )
|
|
{
|
|
if ( !victim || !victim->client )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( PM_SuperBreakLoseAnim( victim->client->ps.torsoAnim )
|
|
|| PM_SuperBreakWinAnim( victim->client->ps.torsoAnim ) )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( victim->client->ps.saberMove == LS_PARRY_UP
|
|
|| victim->client->ps.saberMove == LS_PARRY_UR
|
|
|| victim->client->ps.saberMove == LS_PARRY_UL
|
|
|| victim->client->ps.saberMove == LS_H1_BR
|
|
|| victim->client->ps.saberMove == LS_H1_B_
|
|
|| victim->client->ps.saberMove == LS_H1_BL )
|
|
{//knock their asses down!
|
|
int knockAnim = BOTH_KNOCKDOWN1;
|
|
if ( PM_CrouchAnim( victim->client->ps.legsAnim ) )
|
|
{
|
|
knockAnim = BOTH_KNOCKDOWN4;
|
|
}
|
|
NPC_SetAnim( victim, SETANIM_BOTH, knockAnim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
G_AddEvent( victim, EV_PAIN, victim->health );
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
qboolean G_TryingKataAttack( gentity_t *self, usercmd_t *cmd )
|
|
{
|
|
if ( g_saberNewControlScheme->integer )
|
|
{//use the new control scheme: force focus button
|
|
if ( (cmd->buttons&BUTTON_FORCE_FOCUS) )
|
|
{
|
|
return qtrue;
|
|
}
|
|
else
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
else //if ( self && self->client )
|
|
{//use the old control scheme
|
|
if ( (cmd->buttons&BUTTON_ALT_ATTACK) )
|
|
{//pressing alt-attack
|
|
//if ( !(self->client->ps.pm_flags&PMF_ALT_ATTACK_HELD) )
|
|
{//haven't been holding alt-attack
|
|
if ( (cmd->buttons&BUTTON_ATTACK) )
|
|
{//pressing attack
|
|
//If player is not in 3rd person then we don't perform the kata attack
|
|
if ( self && self->client && self->client->ps.clientNum == 0 && !cg.renderingThirdPerson)
|
|
return qfalse;
|
|
|
|
return qtrue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
qboolean G_TryingPullAttack( gentity_t *self, usercmd_t *cmd, qboolean amPulling )
|
|
{
|
|
if ( g_saberNewControlScheme->integer )
|
|
{//use the new control scheme: force focus button
|
|
if ( (cmd->buttons&BUTTON_FORCE_FOCUS) )
|
|
{
|
|
if ( self && self->client )
|
|
{
|
|
if ( self->client->ps.forcePowerLevel[FP_PULL] >= FORCE_LEVEL_3 )
|
|
{//force pull 3
|
|
if ( amPulling
|
|
|| (self->client->ps.forcePowersActive&(1<<FP_PULL))
|
|
|| self->client->ps.forcePowerDebounce[FP_PULL] > level.time ) //force-pulling
|
|
{//pulling
|
|
return qtrue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
else
|
|
{//use the old control scheme
|
|
if ( (cmd->buttons&BUTTON_ATTACK) )
|
|
{//pressing attack
|
|
if ( self && self->client )
|
|
{
|
|
if ( self->client->ps.forcePowerLevel[FP_PULL] >= FORCE_LEVEL_3 )
|
|
{//force pull 3
|
|
if ( amPulling
|
|
|| (self->client->ps.forcePowersActive&(1<<FP_PULL))
|
|
|| self->client->ps.forcePowerDebounce[FP_PULL] > level.time ) //force-pulling
|
|
{//pulling
|
|
return qtrue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
qboolean G_TryingCartwheel( gentity_t *self, usercmd_t *cmd )
|
|
{
|
|
if ( g_saberNewControlScheme->integer )
|
|
{//use the new control scheme: force focus button
|
|
if ( (cmd->buttons&BUTTON_FORCE_FOCUS) )
|
|
{
|
|
return qtrue;
|
|
}
|
|
else
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
else
|
|
{//use the old control scheme
|
|
if ( (cmd->buttons&BUTTON_ATTACK) )
|
|
{//pressing attack
|
|
if ( cmd->rightmove )
|
|
{
|
|
if ( self && self->client )
|
|
{
|
|
if ( cmd->upmove>0 //)
|
|
&& self->client->ps.groundEntityNum != ENTITYNUM_NONE )
|
|
{//on ground, pressing jump
|
|
return qtrue;
|
|
}
|
|
else
|
|
{//just jumped?
|
|
if ( self->client->ps.groundEntityNum == ENTITYNUM_NONE
|
|
&& level.time - self->client->ps.lastOnGround <= 50//250
|
|
&& (self->client->ps.pm_flags&PMF_JUMPING) )//jumping
|
|
{//just jumped this or last frame
|
|
return qtrue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
qboolean G_TryingSpecial( gentity_t *self, usercmd_t *cmd )
|
|
{
|
|
if ( g_saberNewControlScheme->integer )
|
|
{//use the new control scheme: force focus button
|
|
if ( (cmd->buttons&BUTTON_FORCE_FOCUS) )
|
|
{
|
|
return qtrue;
|
|
}
|
|
else
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
else
|
|
{//use the old control scheme
|
|
return qfalse;
|
|
}
|
|
}
|
|
|
|
qboolean G_TryingJumpAttack( gentity_t *self, usercmd_t *cmd )
|
|
{
|
|
if ( g_saberNewControlScheme->integer )
|
|
{//use the new control scheme: force focus button
|
|
if ( (cmd->buttons&BUTTON_FORCE_FOCUS) )
|
|
{
|
|
return qtrue;
|
|
}
|
|
else
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
else
|
|
{//use the old control scheme
|
|
if ( (cmd->buttons&BUTTON_ATTACK) )
|
|
{//pressing attack
|
|
if ( cmd->upmove>0 )
|
|
{//pressing jump
|
|
return qtrue;
|
|
}
|
|
else if ( self && self->client )
|
|
{//just jumped?
|
|
if ( self->client->ps.groundEntityNum == ENTITYNUM_NONE
|
|
&& level.time - self->client->ps.lastOnGround <= 250
|
|
&& (self->client->ps.pm_flags&PMF_JUMPING) )//jumping
|
|
{//jumped within the last quarter second
|
|
return qtrue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
qboolean G_TryingJumpForwardAttack( gentity_t *self, usercmd_t *cmd )
|
|
{
|
|
if ( g_saberNewControlScheme->integer )
|
|
{//use the new control scheme: force focus button
|
|
if ( (cmd->buttons&BUTTON_FORCE_FOCUS) )
|
|
{
|
|
return qtrue;
|
|
}
|
|
else
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
else
|
|
{//use the old control scheme
|
|
if ( (cmd->buttons&BUTTON_ATTACK) )
|
|
{//pressing attack
|
|
if ( cmd->forwardmove > 0 )
|
|
{//moving forward
|
|
if ( self && self->client )
|
|
{
|
|
if ( cmd->upmove>0
|
|
&& self->client->ps.groundEntityNum != ENTITYNUM_NONE )
|
|
{//pressing jump
|
|
return qtrue;
|
|
}
|
|
else
|
|
{//no slop on forward jumps - must be precise!
|
|
if ( self->client->ps.groundEntityNum == ENTITYNUM_NONE
|
|
&& level.time - self->client->ps.lastOnGround <= 50
|
|
&& (self->client->ps.pm_flags&PMF_JUMPING) )//jumping
|
|
{//just jumped this or last frame
|
|
return qtrue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
qboolean G_TryingLungeAttack( gentity_t *self, usercmd_t *cmd )
|
|
{
|
|
if ( g_saberNewControlScheme->integer )
|
|
{//use the new control scheme: force focus button
|
|
if ( (cmd->buttons&BUTTON_FORCE_FOCUS) )
|
|
{
|
|
return qtrue;
|
|
}
|
|
else
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
else
|
|
{//use the old control scheme
|
|
if ( (cmd->buttons&BUTTON_ATTACK) )
|
|
{//pressing attack
|
|
if ( cmd->upmove<0 )
|
|
{//pressing crouch
|
|
return qtrue;
|
|
}
|
|
else if ( self && self->client )
|
|
{//just unducked?
|
|
if ( (self->client->ps.pm_flags&PMF_DUCKED) )
|
|
{//just unducking
|
|
return qtrue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
|
|
//FIXME: for these below funcs, maybe in the old control scheme some moves should still cost power... if so, pass in the saberMove and use a switch statement
|
|
qboolean G_EnoughPowerForSpecialMove( int forcePower, int cost, qboolean kataMove )
|
|
{
|
|
if ( g_saberNewControlScheme->integer || kataMove )
|
|
{//special moves cost power
|
|
if ( forcePower >= cost )
|
|
{
|
|
return qtrue;
|
|
}
|
|
else
|
|
{
|
|
cg.forceHUDTotalFlashTime = level.time + 1000;
|
|
return qfalse;
|
|
}
|
|
}
|
|
else
|
|
{//old control scheme: uses no power, so just do it
|
|
return qtrue;
|
|
}
|
|
}
|
|
|
|
void G_DrainPowerForSpecialMove( gentity_t *self, forcePowers_t fp, int cost, qboolean kataMove )
|
|
{
|
|
if ( !self || !self->client || self->s.number >= MAX_CLIENTS )
|
|
{
|
|
return;
|
|
}
|
|
if ( g_saberNewControlScheme->integer || kataMove )
|
|
{//special moves cost power
|
|
WP_ForcePowerDrain( self, fp, cost );//drain the required force power
|
|
}
|
|
else
|
|
{//old control scheme: uses no power, so just do it
|
|
}
|
|
}
|
|
|
|
int G_CostForSpecialMove( int cost, qboolean kataMove )
|
|
{
|
|
if ( g_saberNewControlScheme->integer || kataMove )
|
|
{//special moves cost power
|
|
return cost;
|
|
}
|
|
else
|
|
{//old control scheme: uses no power, so just do it
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
extern qboolean G_EntIsBreakable( int entityNum, gentity_t *breaker );
|
|
void WP_SaberRadiusDamage( gentity_t *ent, vec3_t point, float radius, int damage, float knockBack )
|
|
{
|
|
if ( !ent || !ent->client )
|
|
{
|
|
return;
|
|
}
|
|
else if ( radius <= 0.0f || (damage <= 0 && knockBack <= 0) )
|
|
{
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
vec3_t mins, maxs, entDir;
|
|
gentity_t *radiusEnts[128];
|
|
int numEnts, i;
|
|
float dist;
|
|
|
|
//Setup the bbox to search in
|
|
for ( i = 0; i < 3; i++ )
|
|
{
|
|
mins[i] = point[i] - radius;
|
|
maxs[i] = point[i] + radius;
|
|
}
|
|
|
|
//Get the number of entities in a given space
|
|
numEnts = gi.EntitiesInBox( mins, maxs, radiusEnts, 128 );
|
|
|
|
for ( i = 0; i < numEnts; i++ )
|
|
{
|
|
if ( !radiusEnts[i]->inuse )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( radiusEnts[i] == ent )
|
|
{//Skip myself
|
|
continue;
|
|
}
|
|
|
|
if ( radiusEnts[i]->client == NULL )
|
|
{//must be a client
|
|
if ( G_EntIsBreakable( radiusEnts[i]->s.number, ent ) )
|
|
{//damage breakables within range, but not as much
|
|
G_Damage( radiusEnts[i], ent, ent, vec3_origin, radiusEnts[i]->currentOrigin, 10, 0, MOD_EXPLOSIVE_SPLASH );
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ( (radiusEnts[i]->client->ps.eFlags&EF_HELD_BY_RANCOR)
|
|
|| (radiusEnts[i]->client->ps.eFlags&EF_HELD_BY_WAMPA) )
|
|
{//can't be one being held
|
|
continue;
|
|
}
|
|
|
|
VectorSubtract( radiusEnts[i]->currentOrigin, point, entDir );
|
|
dist = VectorNormalize( entDir );
|
|
if ( dist <= radius )
|
|
{//in range
|
|
if ( damage > 0 )
|
|
{//do damage
|
|
int points = ceil((float)damage*dist/radius);
|
|
G_Damage( radiusEnts[i], ent, ent, vec3_origin, radiusEnts[i]->currentOrigin, points, DAMAGE_NO_KNOCKBACK, MOD_EXPLOSIVE_SPLASH );
|
|
}
|
|
if ( knockBack > 0 )
|
|
{//do knockback
|
|
if ( radiusEnts[i]->client
|
|
&& radiusEnts[i]->client->NPC_class != CLASS_RANCOR
|
|
&& radiusEnts[i]->client->NPC_class != CLASS_ATST
|
|
&& !(radiusEnts[i]->flags&FL_NO_KNOCKBACK) )//don't throw them back
|
|
{
|
|
float knockbackStr = knockBack*dist/radius;
|
|
entDir[2] += 0.1f;
|
|
VectorNormalize( entDir );
|
|
G_Throw( radiusEnts[i], entDir, knockbackStr );
|
|
if ( radiusEnts[i]->health > 0 )
|
|
{//still alive
|
|
if ( knockbackStr > 50 )
|
|
{//close enough and knockback high enough to possibly knock down
|
|
if ( dist < (radius*0.5f)
|
|
|| radiusEnts[i]->client->ps.groundEntityNum != ENTITYNUM_NONE )
|
|
{//within range of my fist or within ground-shaking range and not in the air
|
|
G_Knockdown( radiusEnts[i], ent, entDir, 500, qtrue );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
---------------------------------------------------------
|
|
void WP_SaberDamageTrace( gentity_t *ent, int saberNum, int bladeNum )
|
|
|
|
Constantly trace from the old blade pos to new, down the saber beam and do damage
|
|
|
|
FIXME: if the dot product of the old muzzle dir and the new muzzle dir is < 0.75, subdivide it and do multiple traces so we don't flatten out the arc!
|
|
---------------------------------------------------------
|
|
*/
|
|
#define MAX_SABER_SWING_INC 0.33f
|
|
void WP_SaberDamageTrace( gentity_t *ent, int saberNum, int bladeNum )
|
|
{
|
|
vec3_t mp1, mp2, md1, md2, baseOld, baseNew, baseDiff, endOld, endNew, bladePointOld, bladePointNew;
|
|
float tipDmgMod = 1.0f;
|
|
float baseDamage;
|
|
int baseDFlags = 0;
|
|
qboolean hit_wall = qfalse;
|
|
qboolean brokenParry = qfalse;
|
|
|
|
for ( int ven = 0; ven < MAX_SABER_VICTIMS; ven++ )
|
|
{
|
|
victimEntityNum[ven] = ENTITYNUM_NONE;
|
|
}
|
|
memset( totalDmg, 0, sizeof( totalDmg) );
|
|
memset( dmgDir, 0, sizeof( dmgDir ) );
|
|
memset( dmgNormal, 0, sizeof( dmgNormal ) );
|
|
memset( dmgSpot, 0, sizeof( dmgSpot ) );
|
|
memset( dmgFraction, 0, sizeof( dmgFraction ) );
|
|
memset( hitLoc, HL_NONE, sizeof( hitLoc ) );
|
|
memset( hitDismemberLoc, HL_NONE, sizeof( hitDismemberLoc ) );
|
|
memset( hitDismember, qfalse, sizeof( hitDismember ) );
|
|
numVictims = 0;
|
|
VectorClear(saberHitLocation);
|
|
VectorClear(saberHitNormal);
|
|
saberHitFraction = 1.0; // Closest saber hit. The saber can do no damage past this point.
|
|
saberHitEntity = ENTITYNUM_NONE;
|
|
sabersCrossed = -1;
|
|
|
|
if ( !ent->client )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( !ent->s.number )
|
|
{//player never uses these
|
|
ent->client->ps.saberEventFlags &= ~SEF_EVENTS;
|
|
}
|
|
|
|
if ( ent->client->ps.saber[saberNum].blade[bladeNum].length <= 1 )//cen get down to 1 when in a wall
|
|
{//saber is not on
|
|
return;
|
|
}
|
|
|
|
if ( VectorCompare( ent->client->renderInfo.muzzlePointOld, vec3_origin ) || VectorCompare( ent->client->renderInfo.muzzleDirOld, vec3_origin ) )
|
|
{
|
|
//just started up the saber?
|
|
return;
|
|
}
|
|
|
|
int saberContents = 0;
|
|
if ( !(ent->client->ps.saber[saberNum].saberFlags&SFL_ON_IN_WATER) )
|
|
{//saber can't stay on underwater
|
|
saberContents = gi.pointcontents( ent->client->renderInfo.muzzlePoint, ent->client->ps.saberEntityNum );
|
|
}
|
|
if ( (saberContents&CONTENTS_WATER)||
|
|
(saberContents&CONTENTS_SLIME)||
|
|
(saberContents&CONTENTS_LAVA) )
|
|
{//um... turn off? Or just set length to 1?
|
|
//FIXME: short-out effect/sound?
|
|
ent->client->ps.saber[saberNum].blade[bladeNum].active = qfalse;
|
|
return;
|
|
}
|
|
else if (!g_saberNoEffects && gi.WE_IsOutside(ent->client->renderInfo.muzzlePoint))
|
|
{
|
|
float chanceOfFizz = gi.WE_GetChanceOfSaberFizz();
|
|
if (chanceOfFizz>0 && Q_flrand(0.0f, 1.0f)<chanceOfFizz)
|
|
{
|
|
vec3_t end; /*normal = {0,0,1};//FIXME: opposite of rain angles?*/
|
|
VectorMA( ent->client->ps.saber[saberNum].blade[bladeNum].muzzlePoint, ent->client->ps.saber[saberNum].blade[bladeNum].length*Q_flrand(0, 1), ent->client->ps.saber[saberNum].blade[bladeNum].muzzleDir, end );
|
|
G_PlayEffect( "saber/fizz", end );
|
|
}
|
|
}
|
|
|
|
//FIXMEFIXMEFIXME: When in force speed (esp. lvl 3), need to interpolate this because
|
|
// we animate so much faster that the arc is pretty much flat...
|
|
|
|
int entPowerLevel = 0;
|
|
if ( ent->client->NPC_class == CLASS_SABER_DROID )
|
|
{
|
|
entPowerLevel = SaberDroid_PowerLevelForSaberAnim( ent );
|
|
}
|
|
else if ( !ent->s.number && (ent->client->ps.forcePowersActive&(1<<FP_SPEED)) )
|
|
{
|
|
entPowerLevel = FORCE_LEVEL_3;
|
|
}
|
|
else
|
|
{
|
|
entPowerLevel = PM_PowerLevelForSaberAnim( &ent->client->ps, saberNum );
|
|
}
|
|
|
|
if ( entPowerLevel )
|
|
{
|
|
if ( ent->client->ps.forceRageRecoveryTime > level.time )
|
|
{
|
|
entPowerLevel = FORCE_LEVEL_1;
|
|
}
|
|
else if ( ent->client->ps.forcePowersActive & (1 << FP_RAGE) )
|
|
{
|
|
entPowerLevel += ent->client->ps.forcePowerLevel[FP_RAGE];
|
|
}
|
|
}
|
|
|
|
if ( ent->client->ps.saberInFlight )
|
|
{//flying sabers are much more deadly
|
|
//unless you're dead
|
|
if ( ent->health <= 0 && g_saberRealisticCombat->integer < 2 )
|
|
{//so enemies don't keep trying to block it
|
|
//FIXME: still do damage, just not to humanoid clients who should try to avoid it
|
|
//baseDamage = 0.0f;
|
|
return;
|
|
}
|
|
//or unless returning
|
|
else if ( ent->client->ps.saberEntityState == SES_RETURNING
|
|
&& !(ent->client->ps.saber[0].saberFlags&SFL_RETURN_DAMAGE) )//type != SABER_STAR )
|
|
{//special case, since we're returning, chances are if we hit something
|
|
//it's going to be butt-first. So do less damage.
|
|
baseDamage = 0.1f;
|
|
}
|
|
else
|
|
{
|
|
if ( !ent->s.number )
|
|
{//cheat for player
|
|
baseDamage = 10.0f;
|
|
}
|
|
else
|
|
{
|
|
baseDamage = 2.5f;
|
|
}
|
|
}
|
|
//Use old to current since can't predict it
|
|
VectorCopy( ent->client->ps.saber[saberNum].blade[bladeNum].muzzlePointOld, mp1 );
|
|
VectorCopy( ent->client->ps.saber[saberNum].blade[bladeNum].muzzleDirOld, md1 );
|
|
VectorCopy( ent->client->ps.saber[saberNum].blade[bladeNum].muzzlePoint, mp2 );
|
|
VectorCopy( ent->client->ps.saber[saberNum].blade[bladeNum].muzzleDir, md2 );
|
|
}
|
|
else
|
|
{
|
|
if ( ent->client->ps.torsoAnim == BOTH_A7_HILT )
|
|
{//no effects, no damage
|
|
return;
|
|
}
|
|
else if ( G_InCinematicSaberAnim( ent ) )
|
|
{
|
|
baseDFlags = DAMAGE_NO_KILL;
|
|
baseDamage = 0.1f;
|
|
}
|
|
else if ( ent->client->ps.saberMove == LS_READY
|
|
&& !PM_SaberInSpecialAttack( ent->client->ps.torsoAnim ) )
|
|
{//just do effects
|
|
if ( g_saberRealisticCombat->integer < 2 )
|
|
{//don't kill with this hit
|
|
baseDFlags = DAMAGE_NO_KILL;
|
|
}
|
|
|
|
//This bit isn't in JKO, so only enable it if not in TBDC
|
|
if ( !g_TeamBeefDirectorsCut->integer)
|
|
{
|
|
if ((!WP_SaberBladeUseSecondBladeStyle(&ent->client->ps.saber[saberNum], bladeNum)
|
|
&& (ent->client->ps.saber[saberNum].saberFlags2 & SFL2_NO_IDLE_EFFECT))
|
|
|| (WP_SaberBladeUseSecondBladeStyle(&ent->client->ps.saber[saberNum], bladeNum)
|
|
&& (ent->client->ps.saber[saberNum].saberFlags2 & SFL2_NO_IDLE_EFFECT2))
|
|
)
|
|
{//do nothing at all when idle
|
|
return;
|
|
}
|
|
}
|
|
|
|
baseDamage = 0;
|
|
}
|
|
else if ( ent->client->ps.saberLockTime > level.time )
|
|
{//just do effects
|
|
//This bit isn't in JKO, so only enable it if not in TBDC
|
|
if ( !g_TeamBeefDirectorsCut->integer)
|
|
{
|
|
if ((!WP_SaberBladeUseSecondBladeStyle(&ent->client->ps.saber[saberNum], bladeNum)
|
|
&& (ent->client->ps.saber[saberNum].saberFlags2 & SFL2_NO_IDLE_EFFECT))
|
|
|| (WP_SaberBladeUseSecondBladeStyle(&ent->client->ps.saber[saberNum], bladeNum)
|
|
&& (ent->client->ps.saber[saberNum].saberFlags2 & SFL2_NO_IDLE_EFFECT2))
|
|
)
|
|
{//do nothing at all when idle
|
|
return;
|
|
}
|
|
}
|
|
|
|
baseDamage = 0;
|
|
}
|
|
else if ( !WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].damageScale <= 0.0f
|
|
&& ent->client->ps.saber[saberNum].knockbackScale <= 0.0f )
|
|
{//this blade does no damage and no knockback (only for blocking?)
|
|
baseDamage = 0;
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].damageScale2 <= 0.0f
|
|
&& ent->client->ps.saber[saberNum].knockbackScale2 <= 0.0f )
|
|
{//this blade does no damage and no knockback (only for blocking?)
|
|
baseDamage = 0;
|
|
}
|
|
else if ( ent->client->ps.saberBlocked > BLOCKED_NONE
|
|
|| ( !PM_SaberInAttack( ent->client->ps.saberMove )
|
|
&& !PM_SaberInSpecialAttack( ent->client->ps.torsoAnim )
|
|
&& !PM_SaberInTransitionAny( ent->client->ps.saberMove )
|
|
)
|
|
)
|
|
{//don't do damage if parrying/reflecting/bouncing/deflecting or not actually attacking or in a transition to/from/between attacks
|
|
if ( (!WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& (ent->client->ps.saber[saberNum].saberFlags2&SFL2_NO_IDLE_EFFECT) )
|
|
|| ( WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& (ent->client->ps.saber[saberNum].saberFlags2&SFL2_NO_IDLE_EFFECT2) )
|
|
)
|
|
{//do nothing at all when idle
|
|
return;
|
|
}
|
|
baseDamage = 0;
|
|
}
|
|
else
|
|
{//okay, in a saberMove that does damage
|
|
//make sure we're in the right anim
|
|
if ( !PM_SaberInSpecialAttack( ent->client->ps.torsoAnim )
|
|
&& !PM_InAnimForSaberMove( ent->client->ps.torsoAnim, ent->client->ps.saberMove ) )
|
|
{//forced into some other animation somehow, like a pain or death?
|
|
if ( (!WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& (ent->client->ps.saber[saberNum].saberFlags2&SFL2_NO_IDLE_EFFECT) )
|
|
|| ( WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& (ent->client->ps.saber[saberNum].saberFlags2&SFL2_NO_IDLE_EFFECT2) )
|
|
)
|
|
{//do nothing at all when idle
|
|
return;
|
|
}
|
|
baseDamage = 0;
|
|
}
|
|
else if ( ent->client->ps.weaponstate == WEAPON_FIRING && ent->client->ps.saberBlocked == BLOCKED_NONE &&
|
|
( PM_SaberInAttack(ent->client->ps.saberMove) || PM_SaberInSpecialAttack( ent->client->ps.torsoAnim ) || PM_SpinningSaberAnim(ent->client->ps.torsoAnim) || entPowerLevel > FORCE_LEVEL_2 || (WP_SaberBladeDoTransitionDamage( &ent->client->ps.saber[saberNum], bladeNum )&&PM_SaberInTransitionAny(ent->client->ps.saberMove)) ) )// || ent->client->ps.saberAnimLevel == SS_STAFF ) )
|
|
{//normal attack swing swinging/spinning (or if using strong set), do normal damage //FIXME: or if using staff?
|
|
//FIXME: more damage for higher attack power levels?
|
|
// More damage based on length/color of saber?
|
|
//FIXME: Desann does double damage?
|
|
if ( g_saberRealisticCombat->integer )
|
|
{
|
|
switch ( entPowerLevel )
|
|
{
|
|
default:
|
|
case FORCE_LEVEL_3:
|
|
baseDamage = 10.0f;
|
|
break;
|
|
case FORCE_LEVEL_2:
|
|
baseDamage = 5.0f;
|
|
break;
|
|
case FORCE_LEVEL_0:
|
|
case FORCE_LEVEL_1:
|
|
baseDamage = 2.5f;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( g_spskill->integer > 0
|
|
&& ent->s.number < MAX_CLIENTS
|
|
&& ( ent->client->ps.torsoAnim == BOTH_ROLL_STAB
|
|
|| ent->client->ps.torsoAnim == BOTH_SPINATTACK6
|
|
|| ent->client->ps.torsoAnim == BOTH_SPINATTACK7
|
|
|| ent->client->ps.torsoAnim == BOTH_LUNGE2_B__T_ ) )
|
|
{//*sigh*, these anim do less damage since they're so easy to do
|
|
baseDamage = 2.5f;
|
|
}
|
|
else
|
|
{
|
|
baseDamage = 2.5f * (float)entPowerLevel;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{//saber is transitioning, defending or idle, don't do as much damage
|
|
//FIXME: strong attacks and returns should do damage and be unblockable
|
|
if ( g_timescale->value < 1.0 )
|
|
{//in slow mo or force speed, we need to do damage during the transitions
|
|
if ( g_saberRealisticCombat->integer )
|
|
{
|
|
switch ( entPowerLevel )
|
|
{
|
|
case FORCE_LEVEL_3:
|
|
baseDamage = 10.0f;
|
|
break;
|
|
case FORCE_LEVEL_2:
|
|
baseDamage = 5.0f;
|
|
break;
|
|
default:
|
|
case FORCE_LEVEL_1:
|
|
baseDamage = 2.5f;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
baseDamage = 2.5f * (float)entPowerLevel;
|
|
}
|
|
}
|
|
else// if ( !ent->s.number )
|
|
{//I have to do *some* damage in transitions or else you feel like a total gimp
|
|
baseDamage = 0.1f;
|
|
}
|
|
/*
|
|
else
|
|
{
|
|
baseDamage = 0;//was 1.0f;//was 0.25
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
|
|
//Use current to next since can predict it
|
|
//FIXME: if they're closer than the saber blade start, we don't want the
|
|
// arm to pass through them without any damage... so check the radius
|
|
// and push them away (do pain & knockback)
|
|
//FIXME: if going into/coming from a parry/reflection or going into a deflection, don't use old mp & dir? Otherwise, deflections will cut through?
|
|
//VectorCopy( ent->client->renderInfo.muzzlePoint, mp1 );
|
|
//VectorCopy( ent->client->renderInfo.muzzleDir, md1 );
|
|
//VectorCopy( ent->client->renderInfo.muzzlePointNext, mp2 );
|
|
//VectorCopy( ent->client->renderInfo.muzzleDirNext, md2 );
|
|
//prediction was causing gaps in swing (G2 problem) so *don't* predict
|
|
if ( ent->client->ps.saberDamageDebounceTime > level.time )
|
|
{//really only used when a saber attack start anim starts, not actually for stopping damage
|
|
//we just want to not use the old position to trace the attack from...
|
|
VectorCopy( ent->client->ps.saber[saberNum].blade[bladeNum].muzzlePoint, ent->client->ps.saber[saberNum].blade[bladeNum].muzzlePointOld );
|
|
VectorCopy( ent->client->ps.saber[saberNum].blade[bladeNum].muzzleDir, ent->client->ps.saber[saberNum].blade[bladeNum].muzzleDirOld );
|
|
}
|
|
//do the damage trace from the last position...
|
|
VectorCopy( ent->client->ps.saber[saberNum].blade[bladeNum].muzzlePointOld, mp1 );
|
|
VectorCopy( ent->client->ps.saber[saberNum].blade[bladeNum].muzzleDirOld, md1 );
|
|
//...to the current one.
|
|
VectorCopy( ent->client->ps.saber[saberNum].blade[bladeNum].muzzlePoint, mp2 );
|
|
VectorCopy( ent->client->ps.saber[saberNum].blade[bladeNum].muzzleDir, md2 );
|
|
|
|
//NOTE: this is a test, may not be necc, as I can still swing right through someone without hitting them, somehow...
|
|
//see if anyone is so close that they're within the dist from my origin to the start of the saber
|
|
if ( ent->health > 0 && !ent->client->ps.saberLockTime && saberNum == 0 && bladeNum == 0
|
|
&& !G_InCinematicSaberAnim( ent ) )
|
|
{//only do once - for first blade
|
|
trace_t trace;
|
|
gi.trace( &trace, ent->currentOrigin, vec3_origin, vec3_origin, mp1, ent->s.number, (MASK_SHOT&~(CONTENTS_CORPSE|CONTENTS_ITEM)), (EG2_Collision)0, 0 );
|
|
if ( trace.entityNum < ENTITYNUM_WORLD && (trace.entityNum > 0||ent->client->NPC_class == CLASS_DESANN) )//NPCs don't push player away, unless it's Desann
|
|
{//a valid ent
|
|
gentity_t *traceEnt = &g_entities[trace.entityNum];
|
|
if ( traceEnt
|
|
&& traceEnt->client
|
|
&& traceEnt->client->NPC_class != CLASS_RANCOR
|
|
&& traceEnt->client->NPC_class != CLASS_ATST
|
|
&& traceEnt->client->NPC_class != CLASS_WAMPA
|
|
&& traceEnt->client->NPC_class != CLASS_SAND_CREATURE
|
|
&& traceEnt->health > 0
|
|
&& traceEnt->client->playerTeam != ent->client->playerTeam
|
|
&& !PM_SuperBreakLoseAnim( traceEnt->client->ps.legsAnim )
|
|
&& !PM_SuperBreakLoseAnim( traceEnt->client->ps.torsoAnim )
|
|
&& !PM_SuperBreakWinAnim( traceEnt->client->ps.legsAnim )
|
|
&& !PM_SuperBreakWinAnim( traceEnt->client->ps.torsoAnim )
|
|
&& !PM_InKnockDown( &traceEnt->client->ps )
|
|
&& !PM_LockedAnim( traceEnt->client->ps.legsAnim )
|
|
&& !PM_LockedAnim( traceEnt->client->ps.torsoAnim )
|
|
&& !G_InCinematicSaberAnim( traceEnt ))
|
|
{//enemy client, push them away
|
|
if ( !traceEnt->client->ps.saberLockTime
|
|
&& !traceEnt->message
|
|
&& !(traceEnt->flags&FL_NO_KNOCKBACK)
|
|
&& (!traceEnt->NPC||traceEnt->NPC->jumpState!=JS_JUMPING) )
|
|
{//don't push people in saberlock or with security keys or who are in BS_JUMP
|
|
vec3_t hitDir;
|
|
VectorSubtract( trace.endpos, ent->currentOrigin, hitDir );
|
|
float totalDist = Distance( mp1, ent->currentOrigin );
|
|
float knockback = (totalDist-VectorNormalize( hitDir ))/totalDist * 200.0f;
|
|
hitDir[2] = 0;
|
|
//FIXME: do we need to call G_Throw? Seems unfair to put actual knockback on them, stops the attack
|
|
//G_Throw( traceEnt, hitDir, knockback );
|
|
VectorMA( traceEnt->client->ps.velocity, knockback, hitDir, traceEnt->client->ps.velocity );
|
|
traceEnt->client->ps.pm_time = 200;
|
|
traceEnt->client->ps.pm_flags |= PMF_TIME_NOFRICTION;
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer )
|
|
{
|
|
gi.Printf( "%s pushing away %s at %s\n", ent->NPC_type, traceEnt->NPC_type, vtos( traceEnt->client->ps.velocity ) );
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//the thicker the blade, the more damage... the thinner, the less damage
|
|
baseDamage *= ent->client->ps.saber[saberNum].blade[bladeNum].radius/SABER_RADIUS_STANDARD;
|
|
|
|
if ( g_saberRealisticCombat->integer > 1 )
|
|
{//always do damage, and lots of it
|
|
if ( g_saberRealisticCombat->integer > 2 )
|
|
{//always do damage, and lots of it
|
|
baseDamage = 25.0f;
|
|
}
|
|
else if ( baseDamage > 0.1f )
|
|
{//only do super damage if we would have done damage according to normal rules
|
|
baseDamage = 25.0f;
|
|
}
|
|
}
|
|
else if ( ((!ent->s.number&&ent->client->ps.forcePowersActive&(1<<FP_SPEED))||ent->client->ps.forcePowersActive&(1<<FP_RAGE))
|
|
&& g_timescale->value < 1.0f )
|
|
{
|
|
baseDamage *= (1.0f-g_timescale->value);
|
|
}
|
|
if ( baseDamage > 0.1f )
|
|
{
|
|
if ( (ent->client->ps.forcePowersActive&(1<<FP_RAGE)) )
|
|
{//add some damage if raged
|
|
baseDamage += ent->client->ps.forcePowerLevel[FP_RAGE] * 5.0f;
|
|
}
|
|
else if ( ent->client->ps.forceRageRecoveryTime )
|
|
{//halve it if recovering
|
|
baseDamage *= 0.5f;
|
|
}
|
|
}
|
|
// Get the old state of the blade
|
|
VectorCopy( mp1, baseOld );
|
|
VectorMA( baseOld, ent->client->ps.saber[saberNum].blade[bladeNum].length, md1, endOld );
|
|
// Get the future state of the blade
|
|
VectorCopy( mp2, baseNew );
|
|
VectorMA( baseNew, ent->client->ps.saber[saberNum].blade[bladeNum].length, md2, endNew );
|
|
|
|
sabersCrossed = -1;
|
|
if ( VectorCompare2( baseOld, baseNew ) && VectorCompare2( endOld, endNew ) )
|
|
{
|
|
hit_wall = WP_SaberDamageForTrace( ent->s.number, mp2, endNew, baseDamage*4, md2,
|
|
qfalse, entPowerLevel, ent->client->ps.saber[saberNum].type, qfalse,
|
|
saberNum, bladeNum );
|
|
}
|
|
else
|
|
{
|
|
float aveLength, step = 8, stepsize = 8;
|
|
vec3_t ma1, ma2, md2ang, curBase1, curBase2;
|
|
int xx;
|
|
//do the trace at the base first
|
|
hit_wall = WP_SaberDamageForTrace( ent->s.number, baseOld, baseNew, baseDamage, md2,
|
|
qfalse, entPowerLevel, ent->client->ps.saber[saberNum].type, qtrue,
|
|
saberNum, bladeNum );
|
|
|
|
//if hit a saber, shorten rest of traces to match
|
|
if ( saberHitFraction < 1.0 )
|
|
{
|
|
//adjust muzzleDir...
|
|
vec3_t ma1, ma2;
|
|
vectoangles( md1, ma1 );
|
|
vectoangles( md2, ma2 );
|
|
for ( xx = 0; xx < 3; xx++ )
|
|
{
|
|
md2ang[xx] = LerpAngle( ma1[xx], ma2[xx], saberHitFraction );
|
|
}
|
|
AngleVectors( md2ang, md2, NULL, NULL );
|
|
//shorten the base pos
|
|
VectorSubtract( mp2, mp1, baseDiff );
|
|
VectorMA( mp1, saberHitFraction, baseDiff, baseNew );
|
|
VectorMA( baseNew, ent->client->ps.saber[saberNum].blade[bladeNum].length, md2, endNew );
|
|
}
|
|
|
|
//If the angle diff in the blade is high, need to do it in chunks of 33 to avoid flattening of the arc
|
|
float dirInc, curDirFrac;
|
|
if ( PM_SaberInAttack( ent->client->ps.saberMove )
|
|
|| PM_SaberInSpecialAttack( ent->client->ps.torsoAnim )
|
|
|| PM_SpinningSaberAnim( ent->client->ps.torsoAnim )
|
|
|| PM_InSpecialJump( ent->client->ps.torsoAnim )
|
|
|| (g_timescale->value<1.0f&&PM_SaberInTransitionAny( ent->client->ps.saberMove )) )
|
|
{
|
|
curDirFrac = DotProduct( md1, md2 );
|
|
}
|
|
else
|
|
{
|
|
curDirFrac = 1.0f;
|
|
}
|
|
//NOTE: if saber spun at least 180 degrees since last damage trace, this is not reliable...!
|
|
if ( fabs(curDirFrac) < 1.0f - MAX_SABER_SWING_INC )
|
|
{//the saber blade spun more than 33 degrees since the last damage trace
|
|
curDirFrac = dirInc = 1.0f/((1.0f - curDirFrac)/MAX_SABER_SWING_INC);
|
|
}
|
|
else
|
|
{
|
|
curDirFrac = 1.0f;
|
|
dirInc = 0.0f;
|
|
}
|
|
qboolean hit_saber = qfalse;
|
|
|
|
vectoangles( md1, ma1 );
|
|
vectoangles( md2, ma2 );
|
|
|
|
vec3_t curMD1, curMD2;//, mdDiff, dirDiff;
|
|
//VectorSubtract( md2, md1, mdDiff );
|
|
VectorCopy( md1, curMD2 );
|
|
VectorCopy( baseOld, curBase2 );
|
|
|
|
while ( 1 )
|
|
{
|
|
VectorCopy( curMD2, curMD1 );
|
|
VectorCopy( curBase2, curBase1 );
|
|
if ( curDirFrac >= 1.0f )
|
|
{
|
|
VectorCopy( md2, curMD2 );
|
|
VectorCopy( baseNew, curBase2 );
|
|
}
|
|
else
|
|
{
|
|
for ( xx = 0; xx < 3; xx++ )
|
|
{
|
|
md2ang[xx] = LerpAngle( ma1[xx], ma2[xx], curDirFrac );
|
|
}
|
|
AngleVectors( md2ang, curMD2, NULL, NULL );
|
|
//VectorMA( md1, curDirFrac, mdDiff, curMD2 );
|
|
VectorSubtract( baseNew, baseOld, baseDiff );
|
|
VectorMA( baseOld, curDirFrac, baseDiff, curBase2 );
|
|
}
|
|
// Move up the blade in intervals of stepsize
|
|
for ( step = stepsize; step < ent->client->ps.saber[saberNum].blade[bladeNum].length && step < ent->client->ps.saber[saberNum].blade[bladeNum].lengthOld; step+=12 )
|
|
{
|
|
VectorMA( curBase1, step, curMD1, bladePointOld );
|
|
VectorMA( curBase2, step, curMD2, bladePointNew );
|
|
if ( WP_SaberDamageForTrace( ent->s.number, bladePointOld, bladePointNew, baseDamage, curMD2,
|
|
qfalse, entPowerLevel, ent->client->ps.saber[saberNum].type, qtrue,
|
|
saberNum, bladeNum ) )
|
|
{
|
|
hit_wall = qtrue;
|
|
}
|
|
|
|
//if hit a saber, shorten rest of traces to match
|
|
if ( saberHitFraction < 1.0 )
|
|
{
|
|
//adjust muzzle endpoint
|
|
VectorSubtract( mp2, mp1, baseDiff );
|
|
VectorMA( mp1, saberHitFraction, baseDiff, baseNew );
|
|
VectorMA( baseNew, ent->client->ps.saber[saberNum].blade[bladeNum].length, curMD2, endNew );
|
|
//adjust muzzleDir...
|
|
vec3_t curMA1, curMA2;
|
|
vectoangles( curMD1, curMA1 );
|
|
vectoangles( curMD2, curMA2 );
|
|
for ( xx = 0; xx < 3; xx++ )
|
|
{
|
|
md2ang[xx] = LerpAngle( curMA1[xx], curMA2[xx], saberHitFraction );
|
|
}
|
|
AngleVectors( md2ang, curMD2, NULL, NULL );
|
|
/*
|
|
VectorSubtract( curMD2, curMD1, dirDiff );
|
|
VectorMA( curMD1, saberHitFraction, dirDiff, curMD2 );
|
|
*/
|
|
hit_saber = qtrue;
|
|
}
|
|
if (hit_wall)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
if ( hit_wall || hit_saber )
|
|
{
|
|
break;
|
|
}
|
|
if ( curDirFrac >= 1.0f )
|
|
{
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
curDirFrac += dirInc;
|
|
if ( curDirFrac >= 1.0f )
|
|
{
|
|
curDirFrac = 1.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
//do the trace at the end last
|
|
//Special check- adjust for length of blade not being a multiple of 12
|
|
aveLength = (ent->client->ps.saber[saberNum].blade[bladeNum].lengthOld + ent->client->ps.saber[saberNum].blade[bladeNum].length)/2;
|
|
if ( step > aveLength )
|
|
{//less dmg if the last interval was not stepsize
|
|
tipDmgMod = (stepsize-(step-aveLength))/stepsize;
|
|
}
|
|
//NOTE: since this is the tip, we do not extrapolate the extra 16
|
|
if ( WP_SaberDamageForTrace( ent->s.number, endOld, endNew, tipDmgMod*baseDamage, md2,
|
|
qfalse, entPowerLevel, ent->client->ps.saber[saberNum].type, qfalse,
|
|
saberNum, bladeNum ) )
|
|
{
|
|
hit_wall = qtrue;
|
|
}
|
|
}
|
|
|
|
if ( (saberHitFraction < 1.0f||(sabersCrossed>=0&&sabersCrossed<=32.0f)) && (ent->client->ps.weaponstate == WEAPON_FIRING || ent->client->ps.saberInFlight || G_InCinematicSaberAnim( ent ) ) )
|
|
{// The saber (in-hand) hit another saber, mano.
|
|
qboolean inFlightSaberBlocked = qfalse;
|
|
qboolean collisionResolved = qfalse;
|
|
qboolean deflected = qfalse;
|
|
|
|
gentity_t *hitEnt = &g_entities[saberHitEntity];
|
|
gentity_t *hitOwner = NULL;
|
|
int hitOwnerPowerLevel = FORCE_LEVEL_0;
|
|
|
|
if ( hitEnt )
|
|
{
|
|
hitOwner = hitEnt->owner;
|
|
}
|
|
if ( hitOwner && hitOwner->client )
|
|
{
|
|
hitOwnerPowerLevel = PM_PowerLevelForSaberAnim( &hitOwner->client->ps );
|
|
/*
|
|
if ( entPowerLevel >= FORCE_LEVEL_3
|
|
&& PM_SaberInSpecialAttack( ent->client->ps.torsoAnim ) )
|
|
{//a special "unblockable" attack
|
|
if ( hitOwner->client->NPC_class == CLASS_ALORA
|
|
|| hitOwner->client->NPC_class == CLASS_SHADOWTROOPER
|
|
|| (hitOwner->NPC&&(hitOwner->NPC->aiFlags&NPCAI_BOSS_CHARACTER)) )
|
|
{//these masters can even block unblockables (stops cheap kills)
|
|
entPowerLevel = FORCE_LEVEL_2;
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
|
|
//FIXME: check for certain anims, facing, etc, to make them lock into a sabers-locked pose
|
|
//SEF_LOCKED
|
|
|
|
if ( ent->client->ps.saberInFlight && saberNum == 0 &&
|
|
ent->client->ps.saber[saberNum].blade[bladeNum].active &&
|
|
ent->client->ps.saberEntityNum != ENTITYNUM_NONE &&
|
|
ent->client->ps.saberEntityState != SES_RETURNING )
|
|
{//saber was blocked, return it
|
|
inFlightSaberBlocked = qtrue;
|
|
}
|
|
|
|
//FIXME: based on strength, position and angle of attack & defense, decide if:
|
|
// defender and attacker lock sabers
|
|
// *defender's parry should hold and attack bounces (or deflects, based on angle of sabers)
|
|
// *defender's parry is somewhat broken and both bounce (or deflect)
|
|
// *defender's parry is broken and they bounce while attacker's attack deflects or carries through (especially if they're dead)
|
|
// defender is knocked down and attack goes through
|
|
|
|
//Check deflections and broken parries
|
|
if ( hitOwner && hitOwner->health > 0 && ent->health > 0 //both are alive
|
|
&& !inFlightSaberBlocked && hitOwner->client && !hitOwner->client->ps.saberInFlight && !ent->client->ps.saberInFlight//both have sabers in-hand
|
|
&& ent->client->ps.saberBlocked != BLOCKED_PARRY_BROKEN
|
|
&& ent->client->ps.saberLockTime < level.time
|
|
&& hitOwner->client->ps.saberLockTime < level.time )
|
|
{//2 in-hand sabers hit
|
|
//FIXME: defender should not parry or block at all if not in a saber anim... like, if in a roll or knockdown...
|
|
if ( baseDamage )
|
|
{//there is damage involved, not just effects
|
|
qboolean entAttacking = qfalse;
|
|
qboolean hitOwnerAttacking = qfalse;
|
|
qboolean entDefending = qfalse;
|
|
qboolean hitOwnerDefending = qfalse;
|
|
qboolean forceLock = qfalse;
|
|
|
|
if ( (ent->client->NPC_class == CLASS_KYLE && (ent->spawnflags&1) && hitOwner->s.number < MAX_CLIENTS )
|
|
|| (hitOwner->client->NPC_class == CLASS_KYLE && (hitOwner->spawnflags&1) && ent->s.number < MAX_CLIENTS ) )
|
|
{//Player vs. Kyle Boss == lots of saberlocks
|
|
if ( !Q_irand( 0, 2 ) )
|
|
{
|
|
forceLock = qtrue;
|
|
}
|
|
}
|
|
|
|
if ( PM_SaberInAttack( ent->client->ps.saberMove ) || PM_SaberInSpecialAttack( ent->client->ps.torsoAnim ) )
|
|
{
|
|
entAttacking = qtrue;
|
|
}
|
|
else if ( entPowerLevel > FORCE_LEVEL_2 )
|
|
{//stronger styles count as attacking even if in a transition
|
|
if ( PM_SaberInTransitionAny( ent->client->ps.saberMove ) )
|
|
{
|
|
entAttacking = qtrue;
|
|
}
|
|
}
|
|
if ( PM_SaberInParry( ent->client->ps.saberMove )
|
|
|| ent->client->ps.saberMove == LS_READY )
|
|
{
|
|
entDefending = qtrue;
|
|
}
|
|
|
|
if ( ent->client->ps.torsoAnim == BOTH_A1_SPECIAL
|
|
|| ent->client->ps.torsoAnim == BOTH_A2_SPECIAL
|
|
|| ent->client->ps.torsoAnim == BOTH_A3_SPECIAL )
|
|
{//parry/block/break-parry bonus for single-style kata moves
|
|
entPowerLevel++;
|
|
}
|
|
if ( entAttacking )
|
|
{//add twoHanded bonus and breakParryBonus to entPowerLevel here
|
|
//This makes staff too powerful
|
|
if ( (ent->client->ps.saber[saberNum].saberFlags&SFL_TWO_HANDED) )
|
|
{
|
|
entPowerLevel++;
|
|
}
|
|
//FIXME: what if dualSabers && both sabers are hitting at same time?
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum ) )
|
|
{
|
|
entPowerLevel += ent->client->ps.saber[saberNum].breakParryBonus;
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum ) )
|
|
{
|
|
entPowerLevel += ent->client->ps.saber[saberNum].breakParryBonus2;
|
|
}
|
|
}
|
|
else if ( entDefending )
|
|
{//add twoHanded bonus and dualSaber bonus and parryBonus to entPowerLevel here
|
|
if ( (ent->client->ps.saber[saberNum].saberFlags&SFL_TWO_HANDED)
|
|
|| (ent->client->ps.dualSabers && ent->client->ps.saber[1].Active()) )
|
|
{
|
|
entPowerLevel++;
|
|
}
|
|
//FIXME: what about second saber if dualSabers?
|
|
entPowerLevel += ent->client->ps.saber[saberNum].parryBonus;
|
|
}
|
|
|
|
if ( PM_SaberInAttack( hitOwner->client->ps.saberMove ) || PM_SaberInSpecialAttack( hitOwner->client->ps.torsoAnim ) )
|
|
{
|
|
hitOwnerAttacking = qtrue;
|
|
}
|
|
else if ( hitOwnerPowerLevel > FORCE_LEVEL_2 )
|
|
{//stronger styles count as attacking even if in a transition
|
|
if ( PM_SaberInTransitionAny( hitOwner->client->ps.saberMove ) )
|
|
{
|
|
hitOwnerAttacking = qtrue;
|
|
}
|
|
}
|
|
if ( PM_SaberInParry( hitOwner->client->ps.saberMove )
|
|
|| hitOwner->client->ps.saberMove == LS_READY )
|
|
{
|
|
hitOwnerDefending = qtrue;
|
|
}
|
|
|
|
if ( hitOwner->client->ps.torsoAnim == BOTH_A1_SPECIAL
|
|
|| hitOwner->client->ps.torsoAnim == BOTH_A2_SPECIAL
|
|
|| hitOwner->client->ps.torsoAnim == BOTH_A3_SPECIAL )
|
|
{//parry/block/break-parry bonus for single-style kata moves
|
|
hitOwnerPowerLevel++;
|
|
}
|
|
if ( hitOwnerAttacking )
|
|
{//add twoHanded bonus and breakParryBonus to entPowerLevel here
|
|
if ( (hitOwner->client->ps.saber[0].saberFlags&SFL_TWO_HANDED) )
|
|
{
|
|
hitOwnerPowerLevel++;
|
|
}
|
|
hitOwnerPowerLevel += hitOwner->client->ps.saber[0].breakParryBonus;
|
|
if ( hitOwner->client->ps.dualSabers && Q_irand( 0, 1 ) )
|
|
{//FIXME: assumes both sabers are hitting at same time...?
|
|
hitOwnerPowerLevel += 1 + hitOwner->client->ps.saber[1].breakParryBonus;
|
|
}
|
|
}
|
|
else if ( hitOwnerDefending )
|
|
{//add twoHanded bonus and dualSaber bonus and parryBonus to entPowerLevel here
|
|
if ( (hitOwner->client->ps.saber[0].saberFlags&SFL_TWO_HANDED)
|
|
|| (hitOwner->client->ps.dualSabers && hitOwner->client->ps.saber[1].Active()) )
|
|
{
|
|
hitOwnerPowerLevel++;
|
|
}
|
|
hitOwnerPowerLevel += hitOwner->client->ps.saber[0].parryBonus;
|
|
if ( hitOwner->client->ps.dualSabers && Q_irand( 0, 1 ) )
|
|
{//FIXME: assumes both sabers are defending at same time...?
|
|
hitOwnerPowerLevel += 1 + hitOwner->client->ps.saber[1].parryBonus;
|
|
}
|
|
}
|
|
|
|
if ( PM_SuperBreakLoseAnim( ent->client->ps.torsoAnim )
|
|
|| PM_SuperBreakWinAnim( ent->client->ps.torsoAnim )
|
|
|| PM_SuperBreakLoseAnim( hitOwner->client->ps.torsoAnim )
|
|
|| PM_SuperBreakWinAnim( hitOwner->client->ps.torsoAnim ) )
|
|
{//don't mess with this
|
|
collisionResolved = qtrue;
|
|
}
|
|
else if ( entAttacking
|
|
&& hitOwnerAttacking
|
|
&& !Q_irand( 0, g_saberLockRandomNess->integer )
|
|
&& ( g_debugSaberLock->integer || forceLock
|
|
|| entPowerLevel == hitOwnerPowerLevel
|
|
|| (entPowerLevel > FORCE_LEVEL_2 && hitOwnerPowerLevel > FORCE_LEVEL_2 )
|
|
|| (entPowerLevel < FORCE_LEVEL_3 && hitOwnerPowerLevel < FORCE_LEVEL_3 && hitOwner->client->ps.forcePowerLevel[FP_SABER_OFFENSE] > FORCE_LEVEL_2 && Q_irand( 0, 3 ))
|
|
|| (entPowerLevel < FORCE_LEVEL_2 && hitOwnerPowerLevel < FORCE_LEVEL_3 && hitOwner->client->ps.forcePowerLevel[FP_SABER_OFFENSE] > FORCE_LEVEL_1 && Q_irand( 0, 2 ))
|
|
|| (hitOwnerPowerLevel < FORCE_LEVEL_3 && entPowerLevel < FORCE_LEVEL_3 && ent->client->ps.forcePowerLevel[FP_SABER_OFFENSE] > FORCE_LEVEL_2 && !Q_irand( 0, 1 ))
|
|
|| (hitOwnerPowerLevel < FORCE_LEVEL_2 && entPowerLevel < FORCE_LEVEL_3 && ent->client->ps.forcePowerLevel[FP_SABER_OFFENSE] > FORCE_LEVEL_1 && !Q_irand( 0, 1 )))
|
|
&& WP_SabersCheckLock( ent, hitOwner ) )
|
|
{
|
|
collisionResolved = qtrue;
|
|
}
|
|
else if ( hitOwnerAttacking
|
|
&& entDefending
|
|
&& !Q_irand( 0, g_saberLockRandomNess->integer*3 )
|
|
&& (g_debugSaberLock->integer || forceLock ||
|
|
((ent->client->ps.saberMove != LS_READY || (hitOwnerPowerLevel-ent->client->ps.forcePowerLevel[FP_SABER_DEFENSE]) < Q_irand( -6, 0 ) )
|
|
&& ((hitOwnerPowerLevel < FORCE_LEVEL_3 && ent->client->ps.forcePowerLevel[FP_SABER_DEFENSE] > FORCE_LEVEL_2 )||
|
|
(hitOwnerPowerLevel < FORCE_LEVEL_2 && ent->client->ps.forcePowerLevel[FP_SABER_DEFENSE] > FORCE_LEVEL_1 )||
|
|
(hitOwnerPowerLevel < FORCE_LEVEL_3 && ent->client->ps.forcePowerLevel[FP_SABER_DEFENSE] > FORCE_LEVEL_0 && !Q_irand( 0, (hitOwnerPowerLevel-ent->client->ps.forcePowerLevel[FP_SABER_DEFENSE]+1)*2 ))) ))
|
|
&& WP_SabersCheckLock( hitOwner, ent ) )
|
|
{
|
|
collisionResolved = qtrue;
|
|
}
|
|
else if ( entAttacking && hitOwnerDefending )
|
|
{//I'm attacking hit, they're parrying
|
|
qboolean activeDefense = (qboolean)(hitOwner->s.number||g_saberAutoBlocking->integer||hitOwner->client->ps.saberBlockingTime > level.time);
|
|
if ( !Q_irand( 0, g_saberLockRandomNess->integer*3 )
|
|
&& activeDefense
|
|
&& (g_debugSaberLock->integer || forceLock ||
|
|
((hitOwner->client->ps.saberMove != LS_READY || (entPowerLevel-hitOwner->client->ps.forcePowerLevel[FP_SABER_DEFENSE]) < Q_irand( -6, 0 ) )
|
|
&& ( ( entPowerLevel < FORCE_LEVEL_3 && hitOwner->client->ps.forcePowerLevel[FP_SABER_DEFENSE] > FORCE_LEVEL_2 )
|
|
|| ( entPowerLevel < FORCE_LEVEL_2 && hitOwner->client->ps.forcePowerLevel[FP_SABER_DEFENSE] > FORCE_LEVEL_1 )
|
|
|| ( entPowerLevel < FORCE_LEVEL_3 && hitOwner->client->ps.forcePowerLevel[FP_SABER_DEFENSE] > FORCE_LEVEL_0 && !Q_irand( 0, (entPowerLevel-hitOwner->client->ps.forcePowerLevel[FP_SABER_DEFENSE]+1)*2 )) ) ))
|
|
&& WP_SabersCheckLock( ent, hitOwner ) )
|
|
{
|
|
collisionResolved = qtrue;
|
|
}
|
|
else if ( saberHitFraction < 1.0f )
|
|
{//an actual collision
|
|
if ( entPowerLevel < FORCE_LEVEL_3 && activeDefense )
|
|
{//strong attacks cannot be deflected
|
|
//based on angle of attack & angle of defensive saber, see if I should deflect off in another dir rather than bounce back
|
|
deflected = WP_GetSaberDeflectionAngle( ent, hitOwner );
|
|
//just so Jedi knows that he was blocked
|
|
ent->client->ps.saberEventFlags |= SEF_BLOCKED;
|
|
}
|
|
//base parry breaks on animation (saber attack level), not FP_SABER_OFFENSE
|
|
if ( entPowerLevel < FORCE_LEVEL_3
|
|
//&& ent->client->ps.forcePowerLevel[FP_SABER_OFFENSE] < FORCE_LEVEL_3//if you have high saber offense, you cannot have your attack knocked away, regardless of what style you're using?
|
|
//&& hitOwner->client->ps.saberAnimLevel != FORCE_LEVEL_5
|
|
&& activeDefense
|
|
&& (hitOwnerPowerLevel > FORCE_LEVEL_2||(hitOwner->client->ps.forcePowerLevel[FP_SABER_DEFENSE]>FORCE_LEVEL_2&&Q_irand(0,hitOwner->client->ps.forcePowerLevel[FP_SABER_OFFENSE]))) )
|
|
{//knockaways can make fast-attacker go into a broken parry anim if the ent is using fast or med (but not Tavion)
|
|
//make me parry
|
|
WP_SaberParry( hitOwner, ent, saberNum, bladeNum );
|
|
//turn the parry into a knockaway
|
|
hitOwner->client->ps.saberBounceMove = PM_KnockawayForParry( hitOwner->client->ps.saberBlocked );
|
|
//make them go into a broken parry
|
|
ent->client->ps.saberBounceMove = PM_BrokenParryForAttack( ent->client->ps.saberMove );
|
|
ent->client->ps.saberBlocked = BLOCKED_PARRY_BROKEN;
|
|
if ( saberNum == 0 )
|
|
{//FIXME: can only lose right-hand saber for now
|
|
if ( !(ent->client->ps.saber[saberNum].saberFlags&SFL_NOT_DISARMABLE)
|
|
&& ent->client->ps.forcePowerLevel[FP_SABER_DEFENSE] < FORCE_LEVEL_2
|
|
//&& (ent->s.number||g_saberRealisticCombat->integer)
|
|
&& Q_irand( 0, hitOwner->client->ps.SaberDisarmBonus( 0 ) ) > 0
|
|
&& (hitOwner->s.number || g_saberAutoBlocking->integer || !Q_irand( 0, 2 )) )//if player defending and autoblocking is on, this is less likely to happen, so don't do the random check
|
|
{//knocked the saber right out of his hand! (never happens to player)
|
|
//Get a good velocity to send the saber in based on my parry move
|
|
vec3_t throwDir;
|
|
if ( !PM_VelocityForBlockedMove( &hitOwner->client->ps, throwDir ) )
|
|
{
|
|
PM_VelocityForSaberMove( &ent->client->ps, throwDir );
|
|
}
|
|
WP_SaberLose( ent, throwDir );
|
|
}
|
|
}
|
|
//just so Jedi knows that he was blocked
|
|
ent->client->ps.saberEventFlags |= SEF_BLOCKED;
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer )
|
|
{
|
|
gi.Printf( S_COLOR_RED"%s knockaway %s's attack, new move = %s, anim = %s\n", hitOwner->NPC_type, ent->NPC_type, saberMoveData[ent->client->ps.saberBounceMove].name, animTable[saberMoveData[ent->client->ps.saberBounceMove].animToUse].name );
|
|
}
|
|
#endif
|
|
}
|
|
else if ( !activeDefense//they're not defending
|
|
|| (entPowerLevel > FORCE_LEVEL_2 //I hit hard
|
|
&& hitOwnerPowerLevel < entPowerLevel)//they are defending, but their defense strength is lower than my attack...
|
|
|| (!deflected && Q_irand( 0, Q_max(0, PM_PowerLevelForSaberAnim( &ent->client->ps, saberNum ) - hitOwner->client->ps.forcePowerLevel[FP_SABER_DEFENSE])/*PM_PowerLevelForSaberAnim( &hitOwner->client->ps )*/ ) > 0 ) )
|
|
{//broke their parry altogether
|
|
if ( entPowerLevel > FORCE_LEVEL_2 || Q_irand( 0, Q_max(0, ent->client->ps.forcePowerLevel[FP_SABER_OFFENSE] - hitOwner->client->ps.forcePowerLevel[FP_SABER_DEFENSE]) ) )
|
|
{//chance of continuing with the attack (not bouncing back)
|
|
ent->client->ps.saberEventFlags &= ~SEF_BLOCKED;
|
|
ent->client->ps.saberBounceMove = LS_NONE;
|
|
brokenParry = qtrue;
|
|
}
|
|
//do some time-consuming saber-knocked-aside broken parry anim
|
|
hitOwner->client->ps.saberBlocked = BLOCKED_PARRY_BROKEN;
|
|
hitOwner->client->ps.saberBounceMove = LS_NONE;
|
|
//FIXME: for now, you always disarm the right-hand saber
|
|
if ( !(hitOwner->client->ps.saber[0].saberFlags&SFL_NOT_DISARMABLE)
|
|
&& hitOwner->client->ps.forcePowerLevel[FP_SABER_DEFENSE] < FORCE_LEVEL_2
|
|
//&& (ent->s.number||g_saberRealisticCombat->integer)
|
|
&& Q_irand( 0, 2-ent->client->ps.SaberDisarmBonus( bladeNum ) ) <= 0 )
|
|
{//knocked the saber right out of his hand!
|
|
//get the right velocity for my attack direction
|
|
vec3_t throwDir;
|
|
PM_VelocityForSaberMove( &ent->client->ps, throwDir );
|
|
WP_SaberLose( hitOwner, throwDir );
|
|
if ( (ent->client->ps.saberAnimLevel == SS_STRONG && !Q_irand(0,3) )
|
|
|| ( ent->client->ps.saberAnimLevel==SS_DESANN&&!Q_irand(0,1) ) )
|
|
{// a strong attack
|
|
if ( WP_BrokenParryKnockDown( hitOwner ) )
|
|
{
|
|
hitOwner->client->ps.saberBlocked = BLOCKED_NONE;
|
|
hitOwner->client->ps.saberBounceMove = LS_NONE;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( (ent->client->ps.saberAnimLevel == SS_STRONG && !Q_irand(0,5) )
|
|
|| ( ent->client->ps.saberAnimLevel==SS_DESANN&&!Q_irand(0,3) ) )
|
|
{// a strong attack
|
|
if ( WP_BrokenParryKnockDown( hitOwner ) )
|
|
{
|
|
hitOwner->client->ps.saberBlocked = BLOCKED_NONE;
|
|
hitOwner->client->ps.saberBounceMove = LS_NONE;
|
|
}
|
|
}
|
|
}
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer )
|
|
{
|
|
if ( ent->client->ps.saberEventFlags&SEF_BLOCKED )
|
|
{
|
|
gi.Printf( S_COLOR_RED"%s parry broken (bounce/deflect)!\n", hitOwner->targetname );
|
|
}
|
|
else
|
|
{
|
|
gi.Printf( S_COLOR_RED"%s parry broken (follow-through)!\n", hitOwner->targetname );
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
else
|
|
{//just a parry, possibly the hitOwner can knockaway the ent
|
|
WP_SaberParry( hitOwner, ent, saberNum, bladeNum );
|
|
if ( PM_SaberInBounce( ent->client->ps.saberMove ) //FIXME: saberMove not set until pmove!
|
|
&& activeDefense
|
|
&& hitOwner->client->ps.saberAnimLevel != SS_FAST //&& hitOwner->client->ps.saberAnimLevel != FORCE_LEVEL_5
|
|
&& hitOwner->client->ps.forcePowerLevel[FP_SABER_DEFENSE] > FORCE_LEVEL_2 )
|
|
{//attacker bounced off, and defender has ability to do knockaways, so do one unless we're using fast attacks
|
|
//turn the parry into a knockaway
|
|
hitOwner->client->ps.saberBounceMove = PM_KnockawayForParry( hitOwner->client->ps.saberBlocked );
|
|
}
|
|
else if ( (ent->client->ps.saberAnimLevel == SS_STRONG && !Q_irand(0,6) )
|
|
|| ( ent->client->ps.saberAnimLevel==SS_DESANN && !Q_irand(0,3) ) )
|
|
{// a strong attack can sometimes do a knockdown
|
|
//HMM... maybe only if they're moving backwards?
|
|
if ( WP_BrokenParryKnockDown( hitOwner ) )
|
|
{
|
|
hitOwner->client->ps.saberBlocked = BLOCKED_NONE;
|
|
hitOwner->client->ps.saberBounceMove = LS_NONE;
|
|
}
|
|
}
|
|
}
|
|
collisionResolved = qtrue;
|
|
}
|
|
}
|
|
/*
|
|
else if ( entDefending && hitOwnerAttacking )
|
|
{//I'm parrying, they're attacking
|
|
if ( hitOwnerPowerLevel < FORCE_LEVEL_3 )
|
|
{//strong attacks cannot be deflected
|
|
//based on angle of attack & angle of defensive saber, see if I should deflect off in another dir rather than bounce back
|
|
deflected = WP_GetSaberDeflectionAngle( hitOwner, ent );
|
|
//just so Jedi knows that he was blocked
|
|
hitOwner->client->ps.saberEventFlags |= SEF_BLOCKED;
|
|
}
|
|
//FIXME: base parry breaks on animation (saber attack level), not FP_SABER_OFFENSE
|
|
if ( hitOwnerPowerLevel > FORCE_LEVEL_2 || (!deflected && Q_irand( 0, PM_PowerLevelForSaberAnim( &hitOwner->client->ps ) - ent->client->ps.forcePowerLevel[FP_SABER_DEFENSE] ) > 0 ) )
|
|
{//broke my parry altogether
|
|
if ( hitOwnerPowerLevel > FORCE_LEVEL_2 || Q_irand( 0, hitOwner->client->ps.forcePowerLevel[FP_SABER_OFFENSE] - ent->client->ps.forcePowerLevel[FP_SABER_DEFENSE] ) )
|
|
{//chance of continuing with the attack (not bouncing back)
|
|
hitOwner->client->ps.saberEventFlags &= ~SEF_BLOCKED;
|
|
hitOwner->client->ps.saberBounceMove = LS_NONE;
|
|
}
|
|
//do some time-consuming saber-knocked-aside broken parry anim
|
|
ent->client->ps.saberBlocked = BLOCKED_PARRY_BROKEN;
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer )
|
|
{
|
|
if ( hitOwner->client->ps.saberEventFlags&SEF_BLOCKED )
|
|
{
|
|
gi.Printf( S_COLOR_RED"%s parry broken (bounce/deflect)!\n", ent->targetname );
|
|
}
|
|
else
|
|
{
|
|
gi.Printf( S_COLOR_RED"%s parry broken (follow-through)!\n", ent->targetname );
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
WP_SaberParry( ent, hitOwner, saberNum, bladeNum );
|
|
}
|
|
collisionResolved = qtrue;
|
|
}
|
|
*/
|
|
else
|
|
{//some other kind of in-hand saber collision
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{//some kind of in-flight collision
|
|
}
|
|
|
|
if ( saberHitFraction < 1.0f )
|
|
{
|
|
if ( !collisionResolved && baseDamage )
|
|
{//some other kind of in-hand saber collision
|
|
//handle my reaction
|
|
if ( !ent->client->ps.saberInFlight
|
|
&& ent->client->ps.saberLockTime < level.time )
|
|
{//my saber is in hand
|
|
if ( ent->client->ps.saberBlocked != BLOCKED_PARRY_BROKEN )
|
|
{
|
|
if ( PM_SaberInAttack( ent->client->ps.saberMove ) || PM_SaberInSpecialAttack( ent->client->ps.torsoAnim ) ||
|
|
(entPowerLevel > FORCE_LEVEL_2&&!PM_SaberInIdle(ent->client->ps.saberMove)&&!PM_SaberInParry(ent->client->ps.saberMove)&&!PM_SaberInReflect(ent->client->ps.saberMove)) )
|
|
{//in the middle of attacking
|
|
if ( entPowerLevel < FORCE_LEVEL_3 && hitOwner->health > 0 )
|
|
{//don't deflect/bounce in strong attack or when enemy is dead
|
|
WP_GetSaberDeflectionAngle( ent, hitOwner );
|
|
ent->client->ps.saberEventFlags |= SEF_BLOCKED;
|
|
//since it was blocked/deflected, take away any damage done
|
|
//FIXME: what if the damage was done before the parry?
|
|
WP_SaberClearDamageForEntNum( ent, hitOwner->s.number, saberNum, bladeNum );
|
|
}
|
|
}
|
|
else
|
|
{//saber collided when not attacking, parry it
|
|
//since it was blocked/deflected, take away any damage done
|
|
//FIXME: what if the damage was done before the parry?
|
|
WP_SaberClearDamageForEntNum( ent, hitOwner->s.number, saberNum, bladeNum );
|
|
/*
|
|
if ( ent->s.number || g_saberAutoBlocking->integer || ent->client->ps.saberBlockingTime > level.time )
|
|
{//either an NPC or a player who has blocking
|
|
if ( !PM_SaberInTransitionAny( ent->client->ps.saberMove ) && !PM_SaberInBounce( ent->client->ps.saberMove ) )
|
|
{//I'm not attacking, in transition or in a bounce, so play a parry
|
|
//just so Jedi knows that he parried something
|
|
WP_SaberBlockNonRandom( ent, saberHitLocation, qfalse );
|
|
}
|
|
ent->client->ps.saberEventFlags |= SEF_PARRIED;
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//since it was blocked/deflected, take away any damage done
|
|
//FIXME: what if the damage was done before the parry?
|
|
WP_SaberClearDamageForEntNum( ent, hitOwner->s.number, saberNum, bladeNum );
|
|
}
|
|
}
|
|
else
|
|
{//nothing happens to *me* when my inFlight saber hits something
|
|
}
|
|
//handle their reaction
|
|
if ( hitOwner
|
|
&& hitOwner->health > 0
|
|
&& hitOwner->client
|
|
&& !hitOwner->client->ps.saberInFlight
|
|
&& hitOwner->client->ps.saberLockTime < level.time )
|
|
{//their saber is in hand
|
|
if ( PM_SaberInAttack( hitOwner->client->ps.saberMove ) || PM_SaberInSpecialAttack( hitOwner->client->ps.torsoAnim ) ||
|
|
(hitOwner->client->ps.saberAnimLevel > SS_MEDIUM&&!PM_SaberInIdle(hitOwner->client->ps.saberMove)&&!PM_SaberInParry(hitOwner->client->ps.saberMove)&&!PM_SaberInReflect(hitOwner->client->ps.saberMove)) )
|
|
{//in the middle of attacking
|
|
/*
|
|
if ( hitOwner->client->ps.saberAnimLevel < SS_STRONG )
|
|
{//don't deflect/bounce in strong attack
|
|
WP_GetSaberDeflectionAngle( hitOwner, ent );
|
|
hitOwner->client->ps.saberEventFlags |= SEF_BLOCKED;
|
|
}
|
|
*/
|
|
}
|
|
else
|
|
{//saber collided when not attacking, parry it
|
|
if ( !PM_SaberInBrokenParry( hitOwner->client->ps.saberMove ) )
|
|
{//not currently in a broken parry
|
|
if ( !WP_SaberParry( hitOwner, ent, saberNum, bladeNum ) )
|
|
{//FIXME: hitOwner can't parry, do some time-consuming saber-knocked-aside broken parry anim?
|
|
//hitOwner->client->ps.saberBlocked = BLOCKED_PARRY_BROKEN;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{//nothing happens to *hitOwner* when their inFlight saber hits something
|
|
}
|
|
}
|
|
|
|
//collision must have been handled by now
|
|
//Set the blocked attack bounce value in saberBlocked so we actually play our saberBounceMove anim
|
|
if ( ent->client->ps.saberEventFlags & SEF_BLOCKED )
|
|
{
|
|
if ( ent->client->ps.saberBlocked != BLOCKED_PARRY_BROKEN )
|
|
{
|
|
ent->client->ps.saberBlocked = BLOCKED_ATK_BOUNCE;
|
|
}
|
|
}
|
|
/*
|
|
if ( hitOwner && hitOwner->client->ps.saberEventFlags & SEF_BLOCKED )
|
|
{
|
|
hitOwner->client->ps.saberBlocked = BLOCKED_ATK_BOUNCE;
|
|
}
|
|
*/
|
|
}
|
|
|
|
if ( saberHitFraction < 1.0f || collisionResolved )
|
|
{//either actually hit or locked
|
|
if ( ent->client->ps.saberLockTime < level.time )
|
|
{
|
|
if ( inFlightSaberBlocked )
|
|
{//FIXME: never hear this sound
|
|
WP_SaberBounceSound( ent, hitOwner, &g_entities[ent->client->ps.saberEntityNum], 0, 0, qfalse );
|
|
}
|
|
else
|
|
{
|
|
if ( deflected )
|
|
{
|
|
WP_SaberBounceSound( ent, hitOwner, NULL, saberNum, bladeNum, qtrue );
|
|
}
|
|
else
|
|
{
|
|
WP_SaberBlockSound( ent, hitOwner, saberNum, bladeNum );
|
|
}
|
|
}
|
|
if ( !g_saberNoEffects )
|
|
{
|
|
WP_SaberBlockEffect( ent, saberNum, bladeNum, saberHitLocation, saberHitNormal, qfalse );
|
|
}
|
|
}
|
|
// Set the little screen flash - only when an attack is blocked
|
|
if ( !g_noClashFlare )
|
|
{
|
|
g_saberFlashTime = level.time-50;
|
|
VectorCopy( saberHitLocation, g_saberFlashPos );
|
|
}
|
|
}
|
|
|
|
if ( saberHitFraction < 1.0f )
|
|
{
|
|
if ( inFlightSaberBlocked )
|
|
{//we threw a saber and it was blocked, do any effects, etc.
|
|
int knockAway = 5;
|
|
if ( hitEnt
|
|
&& hitOwner
|
|
&& hitOwner->client
|
|
&& (PM_SaberInAttack( hitOwner->client->ps.saberMove ) || PM_SaberInSpecialAttack( hitOwner->client->ps.torsoAnim ) || PM_SpinningSaberAnim( hitOwner->client->ps.torsoAnim )) )
|
|
{//if hit someone who was in an attack or spin anim, more likely to have in-flight saber knocked away
|
|
if ( hitOwnerPowerLevel > FORCE_LEVEL_2 )
|
|
{//strong attacks almost always knock it aside!
|
|
knockAway = 1;
|
|
}
|
|
else
|
|
{//33% chance
|
|
knockAway = 2;
|
|
}
|
|
knockAway -= hitOwner->client->ps.SaberDisarmBonus( 0 );
|
|
}
|
|
if ( Q_irand( 0, knockAway ) <= 0 || //random
|
|
( hitOwner
|
|
&& hitOwner->client
|
|
&& hitOwner->NPC
|
|
&& (hitOwner->NPC->aiFlags&NPCAI_BOSS_CHARACTER)
|
|
) //or if blocked by a Boss character FIXME: or base on defense level?
|
|
)//FIXME: player should not auto-block a flying saber, let him override the parry with an attack to knock the saber from the air, rather than this random chance
|
|
{//knock it aside and turn it off
|
|
if ( !g_saberNoEffects )
|
|
{
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].hitOtherEffect )
|
|
{
|
|
G_PlayEffect( ent->client->ps.saber[saberNum].hitOtherEffect, saberHitLocation, saberHitNormal );
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].hitOtherEffect2 )
|
|
{
|
|
G_PlayEffect( ent->client->ps.saber[saberNum].hitOtherEffect2, saberHitLocation, saberHitNormal );
|
|
}
|
|
else
|
|
{
|
|
G_PlayEffect( "saber/saber_cut", saberHitLocation, saberHitNormal );
|
|
}
|
|
}
|
|
if ( hitEnt )
|
|
{
|
|
vec3_t newDir;
|
|
|
|
VectorSubtract( g_entities[ent->client->ps.saberEntityNum].currentOrigin, hitEnt->currentOrigin, newDir );
|
|
VectorNormalize( newDir );
|
|
G_ReflectMissile( ent, &g_entities[ent->client->ps.saberEntityNum], newDir );
|
|
}
|
|
Jedi_PlayDeflectSound( hitOwner );
|
|
WP_SaberDrop( ent, &g_entities[ent->client->ps.saberEntityNum] );
|
|
}
|
|
else
|
|
{
|
|
if ( !Q_irand( 0, 2 ) && hitEnt )
|
|
{
|
|
vec3_t newDir;
|
|
VectorSubtract( g_entities[ent->client->ps.saberEntityNum].currentOrigin, hitEnt->currentOrigin, newDir );
|
|
VectorNormalize( newDir );
|
|
G_ReflectMissile( ent, &g_entities[ent->client->ps.saberEntityNum], newDir );
|
|
}
|
|
WP_SaberReturn( ent, &g_entities[ent->client->ps.saberEntityNum] );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( ent->client->ps.saberLockTime > level.time )
|
|
{
|
|
if ( ent->s.number < ent->client->ps.saberLockEnemy
|
|
&& !Q_irand( 0, 3 ) )
|
|
{//need to make some kind of effect
|
|
vec3_t hitNorm = {0,0,1};
|
|
if ( WP_SabersIntersection( ent, &g_entities[ent->client->ps.saberLockEnemy], g_saberFlashPos ) )
|
|
{
|
|
if ( Q_irand( 0, 10 ) )
|
|
{
|
|
if ( !g_saberNoEffects )
|
|
{
|
|
WP_SaberBlockEffect( ent, saberNum, bladeNum, g_saberFlashPos, hitNorm, qfalse );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( !g_noClashFlare )
|
|
{
|
|
g_saberFlashTime = level.time-50;
|
|
}
|
|
if ( !g_saberNoEffects )
|
|
{
|
|
WP_SaberBlockEffect( ent, saberNum, bladeNum, g_saberFlashPos, hitNorm, qtrue );
|
|
}
|
|
}
|
|
WP_SaberBlockSound( ent, &g_entities[ent->client->ps.saberLockEnemy], 0, 0 );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( hit_wall
|
|
&& (ent->client->ps.saber[saberNum].saberFlags&SFL_BOUNCE_ON_WALLS)
|
|
&& (PM_SaberInAttackPure( ent->client->ps.saberMove ) //only in a normal attack anim
|
|
|| ent->client->ps.saberMove == LS_A_JUMP_T__B_ ) //or in the strong jump-fwd-attack "death from above" move
|
|
)
|
|
{//bounce off walls
|
|
//do anim
|
|
ent->client->ps.saberBlocked = BLOCKED_ATK_BOUNCE;
|
|
ent->client->ps.saberBounceMove = LS_D1_BR+(saberMoveData[ent->client->ps.saberMove].startQuad-Q_BR);
|
|
//do bounce sound & force feedback
|
|
WP_SaberBounceOnWallSound( ent, saberNum, bladeNum );
|
|
//do hit effect
|
|
if ( !g_saberNoEffects )
|
|
{
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].hitOtherEffect )
|
|
{
|
|
G_PlayEffect( ent->client->ps.saber[saberNum].hitOtherEffect, saberHitLocation, saberHitNormal );
|
|
}
|
|
else if ( WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum )
|
|
&& ent->client->ps.saber[saberNum].hitOtherEffect2 )
|
|
{
|
|
G_PlayEffect( ent->client->ps.saber[saberNum].hitOtherEffect2, saberHitLocation, saberHitNormal );
|
|
}
|
|
else
|
|
{
|
|
G_PlayEffect( "saber/saber_cut", saberHitLocation, saberHitNormal );
|
|
}
|
|
}
|
|
//do radius damage/knockback, if any
|
|
if ( !WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[saberNum], bladeNum ) )
|
|
{
|
|
WP_SaberRadiusDamage( ent, saberHitLocation, ent->client->ps.saber[saberNum].splashRadius, ent->client->ps.saber[saberNum].splashDamage, ent->client->ps.saber[saberNum].splashKnockback );
|
|
}
|
|
else
|
|
{
|
|
WP_SaberRadiusDamage( ent, saberHitLocation, ent->client->ps.saber[saberNum].splashRadius2, ent->client->ps.saber[saberNum].splashDamage2, ent->client->ps.saber[saberNum].splashKnockback2 );
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( WP_SaberApplyDamage( ent, baseDamage, baseDFlags, brokenParry, saberNum, bladeNum, (qboolean)(saberNum==0&&ent->client->ps.saberInFlight) ) )
|
|
{//actually did damage to something
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer )
|
|
{
|
|
gi.Printf( "base damage was %4.2f\n", baseDamage );
|
|
}
|
|
#endif
|
|
WP_SaberHitSound( ent, saberNum, bladeNum );
|
|
}
|
|
|
|
if ( hit_wall )
|
|
{
|
|
//just so Jedi knows that he hit a wall
|
|
ent->client->ps.saberEventFlags |= SEF_HITWALL;
|
|
if ( ent->s.number == 0 )
|
|
{
|
|
AddSoundEvent( ent, ent->currentOrigin, 128, AEL_DISCOVERED, qfalse, qtrue );//FIXME: is this impact on ground or not?
|
|
AddSightEvent( ent, ent->currentOrigin, 256, AEL_DISCOVERED, 50 );
|
|
}
|
|
}
|
|
}
|
|
|
|
void WP_SabersDamageTrace( gentity_t *ent, qboolean noEffects )
|
|
{
|
|
if ( !ent->client )
|
|
{
|
|
return;
|
|
}
|
|
if ( PM_SuperBreakLoseAnim( ent->client->ps.torsoAnim ) )
|
|
{
|
|
return;
|
|
}
|
|
// Saber 1.
|
|
g_saberNoEffects = noEffects;
|
|
for ( int i = 0; i < ent->client->ps.saber[0].numBlades; i++ )
|
|
{
|
|
// If the Blade is not active and the length is 0, don't trace it, try the next blade...
|
|
if ( !ent->client->ps.saber[0].blade[i].active && ent->client->ps.saber[0].blade[i].length == 0 )
|
|
continue;
|
|
|
|
if ( i != 0 )
|
|
{//not first blade
|
|
if ( ent->client->ps.saber[0].type == SABER_BROAD ||
|
|
ent->client->ps.saber[0].type == SABER_SAI ||
|
|
ent->client->ps.saber[0].type == SABER_CLAW )
|
|
{
|
|
g_saberNoEffects = qtrue;
|
|
}
|
|
}
|
|
g_noClashFlare = qfalse;
|
|
if ( (!WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[0], i ) && (ent->client->ps.saber[0].saberFlags2&SFL2_NO_CLASH_FLARE) )
|
|
|| ( WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[0], i ) && (ent->client->ps.saber[0].saberFlags2&SFL2_NO_CLASH_FLARE2) ) )
|
|
{
|
|
g_noClashFlare = qtrue;
|
|
}
|
|
WP_SaberDamageTrace( ent, 0, i );
|
|
}
|
|
// Saber 2.
|
|
g_saberNoEffects = noEffects;
|
|
if ( ent->client->ps.dualSabers )
|
|
{
|
|
for ( int i = 0; i < ent->client->ps.saber[1].numBlades; i++ )
|
|
{
|
|
// If the Blade is not active and the length is 0, don't trace it, try the next blade...
|
|
if ( !ent->client->ps.saber[1].blade[i].active && ent->client->ps.saber[1].blade[i].length == 0 )
|
|
continue;
|
|
|
|
if ( i != 0 )
|
|
{//not first blade
|
|
if ( ent->client->ps.saber[1].type == SABER_BROAD ||
|
|
ent->client->ps.saber[1].type == SABER_SAI ||
|
|
ent->client->ps.saber[1].type == SABER_CLAW )
|
|
{
|
|
g_saberNoEffects = qtrue;
|
|
}
|
|
}
|
|
g_noClashFlare = qfalse;
|
|
if ( (!WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[1], i ) && (ent->client->ps.saber[1].saberFlags2&SFL2_NO_CLASH_FLARE) )
|
|
|| ( WP_SaberBladeUseSecondBladeStyle( &ent->client->ps.saber[1], i ) && (ent->client->ps.saber[1].saberFlags2&SFL2_NO_CLASH_FLARE2) ) )
|
|
{
|
|
g_noClashFlare = qtrue;
|
|
}
|
|
WP_SaberDamageTrace( ent, 1, i );
|
|
}
|
|
}
|
|
g_saberNoEffects = qfalse;
|
|
g_noClashFlare = qfalse;
|
|
}
|
|
|
|
//SABER THROWING============================================================================
|
|
//SABER THROWING============================================================================
|
|
//SABER THROWING============================================================================
|
|
//SABER THROWING============================================================================
|
|
//SABER THROWING============================================================================
|
|
//SABER THROWING============================================================================
|
|
|
|
/*
|
|
================
|
|
WP_SaberImpact
|
|
|
|
================
|
|
*/
|
|
void WP_SaberImpact( gentity_t *owner, gentity_t *saber, trace_t *trace )
|
|
{
|
|
gentity_t *other;
|
|
|
|
other = &g_entities[trace->entityNum];
|
|
|
|
if ( other->takedamage && (other->svFlags&SVF_BBRUSH) )
|
|
{//a breakable brush? break it!
|
|
if ( (other->spawnflags&1)//INVINCIBLE
|
|
||(other->flags&FL_DMG_BY_HEAVY_WEAP_ONLY) )//HEAVY weapon damage only
|
|
{//can't actually break it
|
|
//no hit effect (besides regular client-side one)
|
|
}
|
|
else if ( other->NPC_targetname &&
|
|
(!owner||!owner->targetname||Q_stricmp(owner->targetname,other->NPC_targetname)) )
|
|
{//only breakable by an entity who is not the attacker
|
|
//no hit effect (besides regular client-side one)
|
|
}
|
|
else
|
|
{
|
|
vec3_t dir;
|
|
VectorCopy( saber->s.pos.trDelta, dir );
|
|
VectorNormalize( dir );
|
|
|
|
int dmg = other->health*2;
|
|
if ( other->health > 50 && dmg > 20 && !(other->svFlags&SVF_GLASS_BRUSH) )
|
|
{
|
|
dmg = 20;
|
|
}
|
|
G_Damage( other, saber, owner, dir, trace->endpos, dmg, 0, MOD_SABER );
|
|
if ( owner
|
|
&& owner->client
|
|
&& owner->client->ps.saber[0].hitOtherEffect )
|
|
{
|
|
G_PlayEffect( owner->client->ps.saber[0].hitOtherEffect, trace->endpos, dir );
|
|
}
|
|
else
|
|
{
|
|
G_PlayEffect( "saber/saber_cut", trace->endpos, dir );
|
|
}
|
|
if ( owner->s.number == 0 )
|
|
{
|
|
AddSoundEvent( owner, trace->endpos, 256, AEL_DISCOVERED );
|
|
AddSightEvent( owner, trace->endpos, 512, AEL_DISCOVERED, 50 );
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ( saber->s.pos.trType == TR_LINEAR )
|
|
{
|
|
//hit a wall? send it back
|
|
WP_SaberReturn( saber->owner, saber );
|
|
}
|
|
|
|
if ( other && !other->client && (other->contents&CONTENTS_LIGHTSABER) )//&& other->s.weapon == WP_SABER )
|
|
{//2 in-flight sabers collided!
|
|
//Big flash
|
|
//FIXME: bigger effect/sound?
|
|
//FIXME: STILL DOESNT WORK!!!
|
|
WP_SaberBlockSound( saber->owner, NULL, 0, 0 );
|
|
//G_Sound( saber, G_SoundIndex( va( "sound/weapons/saber/saberblock%d.wav", Q_irand(1, 9) ) ) );
|
|
WP_SaberBlockEffect( saber->owner, 0, 0, trace->endpos, NULL, qfalse);
|
|
qboolean noFlare = qfalse;
|
|
if ( saber->owner
|
|
&& saber->owner->client
|
|
&& (saber->owner->client->ps.saber[0].saberFlags2&SFL2_NO_CLASH_FLARE) )
|
|
{
|
|
noFlare = qtrue;
|
|
}
|
|
if ( !noFlare )
|
|
{
|
|
g_saberFlashTime = level.time-50;
|
|
VectorCopy( trace->endpos, g_saberFlashPos );
|
|
}
|
|
}
|
|
|
|
if ( owner && owner->s.number == 0 && owner->client )
|
|
{
|
|
//Add the event
|
|
if ( owner->client->ps.SaberLength() > 0 )
|
|
{//saber is on, very suspicious
|
|
if ( (!owner->client->ps.saberInFlight && owner->client->ps.groundEntityNum == ENTITYNUM_WORLD)//holding saber and on ground
|
|
|| saber->s.pos.trType == TR_STATIONARY )//saber out there somewhere and on ground
|
|
{//an on-ground alert
|
|
AddSoundEvent( owner, saber->currentOrigin, 128, AEL_DISCOVERED, qfalse, qtrue );
|
|
}
|
|
else
|
|
{//an in-air alert
|
|
AddSoundEvent( owner, saber->currentOrigin, 128, AEL_DISCOVERED );
|
|
}
|
|
AddSightEvent( owner, saber->currentOrigin, 256, AEL_DISCOVERED, 50 );
|
|
}
|
|
else
|
|
{//saber is off, not as suspicious
|
|
AddSoundEvent( owner, saber->currentOrigin, 128, AEL_SUSPICIOUS );
|
|
AddSightEvent( owner, saber->currentOrigin, 256, AEL_SUSPICIOUS );
|
|
}
|
|
}
|
|
|
|
// check for bounce
|
|
if ( !other->takedamage && ( saber->s.eFlags & ( EF_BOUNCE | EF_BOUNCE_HALF ) ) )
|
|
{
|
|
// Check to see if there is a bounce count
|
|
if ( saber->bounceCount ) {
|
|
// decrement number of bounces and then see if it should be done bouncing
|
|
if ( --saber->bounceCount <= 0 ) {
|
|
// He (or she) will bounce no more (after this current bounce, that is).
|
|
saber->s.eFlags &= ~( EF_BOUNCE | EF_BOUNCE_HALF );
|
|
if ( saber->s.pos.trType == TR_LINEAR && owner && owner->client && owner->client->ps.saberEntityState == SES_RETURNING )
|
|
{
|
|
WP_SaberDrop( saber->owner, saber );
|
|
}
|
|
return;
|
|
}
|
|
else
|
|
{//bounced and still have bounces left
|
|
if ( saber->s.pos.trType == TR_LINEAR && owner && owner->client && owner->client->ps.saberEntityState == SES_RETURNING )
|
|
{//under telekinetic control
|
|
if ( !gi.inPVS( saber->currentOrigin, owner->client->renderInfo.handRPoint ) )
|
|
{//not in the PVS of my master
|
|
saber->bounceCount -= 25;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( saber->s.pos.trType == TR_LINEAR && owner && owner->client && owner->client->ps.saberEntityState == SES_RETURNING )
|
|
{
|
|
//don't home for a few frames so we can get around this thing
|
|
trace_t bounceTr;
|
|
vec3_t end;
|
|
float owner_dist = Distance( owner->client->renderInfo.handRPoint, saber->currentOrigin );
|
|
|
|
VectorMA( saber->currentOrigin, 10, trace->plane.normal, end );
|
|
gi.trace( &bounceTr, saber->currentOrigin, saber->mins, saber->maxs, end, owner->s.number, saber->clipmask, (EG2_Collision)0, 0 );
|
|
VectorCopy( bounceTr.endpos, saber->currentOrigin );
|
|
if ( owner_dist > 0 )
|
|
{
|
|
if ( owner_dist > 50 )
|
|
{
|
|
owner->client->ps.saberEntityDist = owner_dist-50;
|
|
}
|
|
else
|
|
{
|
|
owner->client->ps.saberEntityDist = 0;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
G_BounceMissile( saber, trace );
|
|
|
|
if ( saber->s.pos.trType == TR_GRAVITY )
|
|
{//bounced
|
|
//play a bounce sound
|
|
WP_SaberFallSound( owner, saber );
|
|
//change rotation
|
|
VectorCopy( saber->currentAngles, saber->s.apos.trBase );
|
|
saber->s.apos.trType = TR_LINEAR;
|
|
saber->s.apos.trTime = level.time;
|
|
VectorSet( saber->s.apos.trDelta, Q_irand( -300, 300 ), Q_irand( -300, 300 ), Q_irand( -300, 300 ) );
|
|
}
|
|
//see if we stopped
|
|
else if ( saber->s.pos.trType == TR_STATIONARY )
|
|
{//stopped
|
|
//play a bounce sound
|
|
WP_SaberFallSound( owner, saber );
|
|
//stop rotation
|
|
VectorClear( saber->s.apos.trDelta );
|
|
pitch_roll_for_slope( saber, trace->plane.normal, saber->currentAngles );
|
|
saber->currentAngles[0] += SABER_PITCH_HACK;
|
|
VectorCopy( saber->currentAngles, saber->s.apos.trBase );
|
|
//remember when it fell so it can return automagically
|
|
saber->aimDebounceTime = level.time;
|
|
}
|
|
}
|
|
else if ( other->client && other->health > 0
|
|
&& ( (other->NPC && (other->NPC->aiFlags&NPCAI_BOSS_CHARACTER))
|
|
//|| other->client->NPC_class == CLASS_ALORA
|
|
|| other->client->NPC_class == CLASS_BOBAFETT
|
|
|| ( other->client->ps.powerups[PW_GALAK_SHIELD] > 0 ) ) )
|
|
{//Luke, Desann and Tavion slap thrown sabers aside
|
|
WP_SaberDrop( owner, saber );
|
|
WP_SaberBlockSound( owner, other, 0, 0 );
|
|
//G_Sound( saber, G_SoundIndex( va( "sound/weapons/saber/saberblock%d.wav", Q_irand(1, 9) ) ) );
|
|
WP_SaberBlockEffect( owner, 0, 0, trace->endpos, NULL, qfalse );
|
|
qboolean noFlare = qfalse;
|
|
if ( owner
|
|
&& owner->client
|
|
&& (owner->client->ps.saber[0].saberFlags2&SFL2_NO_CLASH_FLARE) )
|
|
{
|
|
noFlare = qtrue;
|
|
}
|
|
if ( !noFlare )
|
|
{
|
|
g_saberFlashTime = level.time-50;
|
|
VectorCopy( trace->endpos, g_saberFlashPos );
|
|
}
|
|
//FIXME: make Luke/Desann/Tavion play an attack anim or some other special anim when this happens
|
|
Jedi_PlayDeflectSound( other );
|
|
}
|
|
}
|
|
|
|
extern float G_PointDistFromLineSegment( const vec3_t start, const vec3_t end, const vec3_t from );
|
|
void WP_SaberInFlightReflectCheck( gentity_t *self, usercmd_t *ucmd )
|
|
{
|
|
gentity_t *ent;
|
|
gentity_t *entityList[MAX_GENTITIES];
|
|
gentity_t *missile_list[MAX_GENTITIES];
|
|
int numListedEntities;
|
|
vec3_t mins, maxs;
|
|
int i, e, numSabers;
|
|
int ent_count = 0;
|
|
int radius = 180;
|
|
vec3_t center;
|
|
vec3_t tip;
|
|
vec3_t up = {0,0,1};
|
|
qboolean willHit = qfalse;
|
|
|
|
if ( self->NPC && (self->NPC->scriptFlags&SCF_IGNORE_ALERTS) )
|
|
{//don't react to things flying at me...
|
|
return;
|
|
}
|
|
//sanity checks: make sure we actually have a saberent
|
|
if ( self->client->ps.weapon != WP_SABER )
|
|
{
|
|
return;
|
|
}
|
|
if ( !self->client->ps.saberInFlight )
|
|
{
|
|
return;
|
|
}
|
|
if ( !self->client->ps.SaberLength() )
|
|
{
|
|
return;
|
|
}
|
|
if ( self->client->ps.saberEntityNum == ENTITYNUM_NONE )
|
|
{
|
|
return;
|
|
}
|
|
gentity_t *saberent = &g_entities[self->client->ps.saberEntityNum];
|
|
if ( !saberent )
|
|
{
|
|
return;
|
|
}
|
|
//okay, enough damn sanity checks
|
|
|
|
VectorCopy( saberent->currentOrigin, center );
|
|
|
|
for ( i = 0 ; i < 3 ; i++ )
|
|
{
|
|
mins[i] = center[i] - radius;
|
|
maxs[i] = center[i] + radius;
|
|
}
|
|
|
|
numListedEntities = gi.EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
|
|
|
|
//FIXME: check visibility?
|
|
for ( e = 0 ; e < numListedEntities ; e++ )
|
|
{
|
|
ent = entityList[ e ];
|
|
|
|
if (ent == self)
|
|
continue;
|
|
if (ent->owner == self)
|
|
continue;
|
|
if ( !(ent->inuse) )
|
|
continue;
|
|
if ( ent->s.eType != ET_MISSILE )
|
|
{
|
|
if ( ent->client || ent->s.weapon != WP_SABER )
|
|
{//FIXME: wake up bad guys?
|
|
continue;
|
|
}
|
|
if ( ent->s.eFlags & EF_NODRAW )
|
|
{
|
|
continue;
|
|
}
|
|
if ( Q_stricmp( "lightsaber", ent->classname ) != 0 )
|
|
{//not a lightsaber
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{//FIXME: make exploding missiles explode?
|
|
if ( ent->s.pos.trType == TR_STATIONARY )
|
|
{//nothing you can do with a stationary missile
|
|
continue;
|
|
}
|
|
if ( ent->splashDamage || ent->splashRadius )
|
|
{//can't deflect exploding missiles
|
|
if ( DistanceSquared( ent->currentOrigin, center ) < 256 )//16 squared
|
|
{
|
|
G_MissileImpacted( ent, saberent, ent->currentOrigin, up );
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
//don't deflect it if it's not within 16 units of the blade
|
|
//do this for all blades
|
|
willHit = qfalse;
|
|
numSabers = 1;
|
|
if ( self->client->ps.dualSabers )
|
|
{
|
|
numSabers = 2;
|
|
}
|
|
for ( int saberNum = 0; saberNum < numSabers; saberNum++ )
|
|
{
|
|
for ( int bladeNum = 0; bladeNum < self->client->ps.saber[saberNum].numBlades; bladeNum++ )
|
|
{
|
|
VectorMA( self->client->ps.saber[saberNum].blade[bladeNum].muzzlePoint, self->client->ps.saber[saberNum].blade[bladeNum].length, self->client->ps.saber[saberNum].blade[bladeNum].muzzleDir, tip );
|
|
|
|
if( G_PointDistFromLineSegment( self->client->ps.saber[saberNum].blade[bladeNum].muzzlePoint, tip, ent->currentOrigin ) <= 32 )
|
|
{
|
|
willHit = qtrue;
|
|
break;
|
|
}
|
|
}
|
|
if ( willHit )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
if ( !willHit )
|
|
{
|
|
continue;
|
|
}
|
|
// ok, we are within the radius, add us to the incoming list
|
|
missile_list[ent_count] = ent;
|
|
ent_count++;
|
|
|
|
}
|
|
|
|
if ( ent_count )
|
|
{
|
|
vec3_t fx_dir;
|
|
// we are done, do we have any to deflect?
|
|
if ( ent_count )
|
|
{
|
|
for ( int x = 0; x < ent_count; x++ )
|
|
{
|
|
if ( missile_list[x]->s.weapon == WP_SABER )
|
|
{//just send it back
|
|
if ( missile_list[x]->owner && missile_list[x]->owner->client && missile_list[x]->owner->client->ps.saber[0].Active() && missile_list[x]->s.pos.trType == TR_LINEAR && missile_list[x]->owner->client->ps.saberEntityState != SES_RETURNING )
|
|
{//it's on and being controlled
|
|
//FIXME: prevent it from damaging me?
|
|
WP_SaberReturn( missile_list[x]->owner, missile_list[x] );
|
|
VectorNormalize2( missile_list[x]->s.pos.trDelta, fx_dir );
|
|
WP_SaberBlockEffect( self, 0, 0, missile_list[x]->currentOrigin, fx_dir, qfalse );
|
|
if ( missile_list[x]->owner->client->ps.saberInFlight && self->client->ps.saberInFlight )
|
|
{
|
|
WP_SaberBlockSound( self, missile_list[x]->owner, 0, 0 );
|
|
//G_Sound( missile_list[x], G_SoundIndex( va( "sound/weapons/saber/saberblock%d.wav", Q_irand(1, 9) ) ) );
|
|
qboolean noFlare = qfalse;
|
|
if ( (missile_list[x]->owner->client->ps.saber[0].saberFlags2&SFL2_NO_CLASH_FLARE)
|
|
&& (self->client->ps.saber[0].saberFlags2&SFL2_NO_CLASH_FLARE) )
|
|
{
|
|
noFlare = qtrue;
|
|
}
|
|
if ( !noFlare )
|
|
{
|
|
g_saberFlashTime = level.time-50;
|
|
gentity_t *saber = &g_entities[self->client->ps.saberEntityNum];
|
|
vec3_t org;
|
|
VectorSubtract( missile_list[x]->currentOrigin, saber->currentOrigin, org );
|
|
VectorMA( saber->currentOrigin, 0.5, org, org );
|
|
VectorCopy( org, g_saberFlashPos );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{//bounce it
|
|
vec3_t reflectAngle, forward;
|
|
if ( self->client && !self->s.number )
|
|
{
|
|
self->client->sess.missionStats.saberBlocksCnt++;
|
|
}
|
|
VectorCopy( saberent->s.apos.trBase, reflectAngle );
|
|
reflectAngle[PITCH] = Q_flrand( -90, 90 );
|
|
AngleVectors( reflectAngle, forward, NULL, NULL );
|
|
|
|
G_ReflectMissile( self, missile_list[x], forward );
|
|
//do an effect
|
|
VectorNormalize2( missile_list[x]->s.pos.trDelta, fx_dir );
|
|
G_PlayEffect( "blaster/deflect", missile_list[x]->currentOrigin, fx_dir );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
qboolean WP_SaberValidateEnemy( gentity_t *self, gentity_t *enemy )
|
|
{
|
|
if ( !enemy )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
if ( !enemy || enemy == self || !enemy->inuse || !enemy->client )
|
|
{//not valid
|
|
return qfalse;
|
|
}
|
|
|
|
if ( enemy->health <= 0 )
|
|
{//corpse
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
if ( enemy->client->ps.weapon == WP_SABER
|
|
&& enemy->client->ps.SaberActive() )
|
|
{//not other saber-users?
|
|
return qfalse;
|
|
}
|
|
*/
|
|
if ( enemy->s.number >= MAX_CLIENTS )
|
|
{//NPCs can cheat and use the homing saber throw 3 on the player
|
|
if ( enemy->client->ps.forcePowersKnown )
|
|
{//not other jedi?
|
|
return qfalse;
|
|
}
|
|
}
|
|
|
|
if ( DistanceSquared( self->client->renderInfo.handRPoint, enemy->currentOrigin ) > saberThrowDistSquared[self->client->ps.forcePowerLevel[FP_SABERTHROW]] )
|
|
{//too far
|
|
return qfalse;
|
|
}
|
|
|
|
if ( (!InFront( enemy->currentOrigin, self->currentOrigin, self->client->ps.viewangles, 0.0f) || !G_ClearLOS( self, self->client->renderInfo.eyePoint, enemy ) )
|
|
&& ( DistanceHorizontalSquared( enemy->currentOrigin, self->currentOrigin ) > 65536 || fabs(enemy->currentOrigin[2]-self->currentOrigin[2]) > 384 ) )
|
|
{//(not in front or not clear LOS) & greater than 256 away
|
|
return qfalse;
|
|
}
|
|
|
|
if ( enemy->client->playerTeam == self->client->playerTeam )
|
|
{//on same team
|
|
return qfalse;
|
|
}
|
|
|
|
//LOS?
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
float WP_SaberRateEnemy( gentity_t *enemy, vec3_t center, vec3_t forward, float radius )
|
|
{
|
|
float rating;
|
|
vec3_t dir;
|
|
|
|
VectorSubtract( enemy->currentOrigin, center, dir );
|
|
rating = (1.0f-(VectorNormalize( dir )/radius));
|
|
rating *= DotProduct( forward, dir );
|
|
return rating;
|
|
}
|
|
|
|
gentity_t *WP_SaberFindEnemy( gentity_t *self, gentity_t *saber )
|
|
{
|
|
//FIXME: should be a more intelligent way of doing this, like auto aim?
|
|
//closest, most in front... did damage to... took damage from? How do we know who the player is focusing on?
|
|
gentity_t *ent, *bestEnt = NULL;
|
|
gentity_t *entityList[MAX_GENTITIES];
|
|
int numListedEntities;
|
|
vec3_t center, mins, maxs, fwdangles, forward;
|
|
int i, e;
|
|
float radius = 400;
|
|
float rating, bestRating = 0.0f;
|
|
|
|
//FIXME: no need to do this in 1st person?
|
|
fwdangles[1] = self->client->ps.viewangles[1];
|
|
AngleVectors( fwdangles, forward, NULL, NULL );
|
|
|
|
VectorCopy( saber->currentOrigin, center );
|
|
|
|
for ( i = 0 ; i < 3 ; i++ )
|
|
{
|
|
mins[i] = center[i] - radius;
|
|
maxs[i] = center[i] + radius;
|
|
}
|
|
|
|
//if the saber has an enemy from the last time it looked, init to that one
|
|
if ( WP_SaberValidateEnemy( self, saber->enemy ) )
|
|
{
|
|
if ( gi.inPVS( self->currentOrigin, saber->enemy->currentOrigin ) )
|
|
{//potentially visible
|
|
if ( G_ClearLOS( self, self->client->renderInfo.eyePoint, saber->enemy ) )
|
|
{//can see him
|
|
bestEnt = saber->enemy;
|
|
bestRating = WP_SaberRateEnemy( bestEnt, center, forward, radius );
|
|
}
|
|
}
|
|
}
|
|
|
|
//If I have an enemy, see if that's even better
|
|
if ( WP_SaberValidateEnemy( self, self->enemy ) )
|
|
{
|
|
float myEnemyRating = WP_SaberRateEnemy( self->enemy, center, forward, radius );
|
|
if ( myEnemyRating > bestRating )
|
|
{
|
|
if ( gi.inPVS( self->currentOrigin, self->enemy->currentOrigin ) )
|
|
{//potentially visible
|
|
if ( G_ClearLOS( self, self->client->renderInfo.eyePoint, self->enemy ) )
|
|
{//can see him
|
|
bestEnt = self->enemy;
|
|
bestRating = myEnemyRating;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
numListedEntities = gi.EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
|
|
|
|
if ( !numListedEntities )
|
|
{//should we clear the enemy?
|
|
return bestEnt;
|
|
}
|
|
|
|
for ( e = 0 ; e < numListedEntities ; e++ )
|
|
{
|
|
ent = entityList[ e ];
|
|
|
|
if ( ent == self || ent == saber || ent == bestEnt )
|
|
{
|
|
continue;
|
|
}
|
|
if ( !WP_SaberValidateEnemy( self, ent ) )
|
|
{//doesn't meet criteria of valid look enemy (don't check current since we would have done that before this func's call
|
|
continue;
|
|
}
|
|
if ( !gi.inPVS( self->currentOrigin, ent->currentOrigin ) )
|
|
{//not even potentially visible
|
|
continue;
|
|
}
|
|
if ( !G_ClearLOS( self, self->client->renderInfo.eyePoint, ent ) )
|
|
{//can't see him
|
|
continue;
|
|
}
|
|
//rate him based on how close & how in front he is
|
|
rating = WP_SaberRateEnemy( ent, center, forward, radius );
|
|
if ( rating > bestRating )
|
|
{
|
|
bestEnt = ent;
|
|
bestRating = rating;
|
|
}
|
|
}
|
|
return bestEnt;
|
|
}
|
|
|
|
void WP_RunSaber( gentity_t *self, gentity_t *saber )
|
|
{
|
|
vec3_t origin, oldOrg;
|
|
trace_t tr;
|
|
|
|
VectorCopy( saber->currentOrigin, oldOrg );
|
|
// get current position
|
|
EvaluateTrajectory( &saber->s.pos, level.time, origin );
|
|
// get current angles
|
|
EvaluateTrajectory( &saber->s.apos, level.time, saber->currentAngles );
|
|
|
|
// trace a line from the previous position to the current position,
|
|
// ignoring interactions with the missile owner
|
|
int clipmask = saber->clipmask;
|
|
if ( !self || !self->client || self->client->ps.SaberLength() <= 0 )
|
|
{//don't keep hitting other sabers when turned off
|
|
clipmask &= ~CONTENTS_LIGHTSABER;
|
|
}
|
|
gi.trace( &tr, saber->currentOrigin, saber->mins, saber->maxs, origin,
|
|
saber->owner ? saber->owner->s.number : ENTITYNUM_NONE, clipmask, (EG2_Collision)0, 0 );
|
|
|
|
VectorCopy( tr.endpos, saber->currentOrigin );
|
|
|
|
if ( self->client->ps.SaberActive() )
|
|
{
|
|
if ( self->client->ps.saberInFlight || (self->client->ps.weaponTime&&!Q_irand( 0, 100 )) )
|
|
{//make enemies run from a lit saber in flight or from me when I'm attacking
|
|
if ( !Q_irand( 0, 10 ) )
|
|
{//not so often...
|
|
AddSightEvent( self, saber->currentOrigin, self->client->ps.SaberLength()*3, AEL_DANGER, 100 );
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( tr.startsolid )
|
|
{
|
|
tr.fraction = 0;
|
|
}
|
|
|
|
gi.linkentity( saber );
|
|
|
|
//touch push triggers?
|
|
|
|
if ( tr.fraction != 1 )
|
|
{
|
|
WP_SaberImpact( self, saber, &tr );
|
|
}
|
|
|
|
if ( saber->s.pos.trType == TR_LINEAR )
|
|
{//home
|
|
//figure out where saber should be
|
|
vec3_t forward, saberHome, saberDest, fwdangles = {0};
|
|
|
|
if (self->client->ps.clientNum == 0 && !cg.renderingThirdPerson)
|
|
{
|
|
BG_CalculateVRWeaponPosition(saberHome, fwdangles);
|
|
|
|
//Ignore roll
|
|
fwdangles[ROLL] = 0;
|
|
}
|
|
else {
|
|
VectorCopy(self->client->ps.viewangles, fwdangles);
|
|
}
|
|
|
|
if ( self->s.number )
|
|
{
|
|
fwdangles[0] -= 8;
|
|
}
|
|
else if ( cg.renderingThirdPerson )
|
|
{
|
|
fwdangles[0] -= 5;
|
|
}
|
|
|
|
//For now make FORCE_LEVEL_1 saber throw do the same as FORCE_LEVEL_2, otherwise it is impossible to use
|
|
if ( self->client->ps.forcePowerLevel[FP_SABERTHROW] >= FORCE_LEVEL_1
|
|
|| self->client->ps.saberEntityState == SES_RETURNING
|
|
|| VectorCompare( saber->s.pos.trDelta, vec3_origin ) )
|
|
{//control if it's returning or just starting
|
|
float saberSpeed = self->client->ps.forcePowerLevel[FP_SABERTHROW] == FORCE_LEVEL_1 ? 300 : 500;
|
|
float dist;
|
|
gentity_t *enemy = NULL;
|
|
|
|
AngleVectors( fwdangles, forward, NULL, NULL );
|
|
|
|
//Always use right hand as saber home
|
|
VectorCopy( self->client->renderInfo.handRPoint, saberHome );
|
|
VectorMA( saberHome, self->client->ps.saberEntityDist, forward, saberDest );
|
|
|
|
if ( self->client->ps.forcePowerLevel[FP_SABERTHROW] > FORCE_LEVEL_2
|
|
&& self->client->ps.saberEntityState == SES_LEAVING )
|
|
{//max level
|
|
if ( self->enemy &&
|
|
!WP_SaberValidateEnemy( self, self->enemy ) )
|
|
{//if my enemy isn't valid to auto-aim at, don't autoaim
|
|
}
|
|
else
|
|
{
|
|
//pick an enemy
|
|
enemy = WP_SaberFindEnemy( self, saber );
|
|
if ( enemy )
|
|
{//home in on enemy
|
|
float enemyDist = Distance( self->client->renderInfo.handRPoint, enemy->currentOrigin );
|
|
VectorCopy( enemy->currentOrigin, saberDest );
|
|
saberDest[2] += enemy->maxs[2]/2.0f;//FIXME: when in a knockdown anim, the saber float above them... do we care?
|
|
self->client->ps.saberEntityDist = enemyDist;
|
|
//once you pick an enemy, stay with it!
|
|
saber->enemy = enemy;
|
|
//FIXME: lock onto that enemy for a minimum amount of time (unless they become invalid?)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//Make the saber head there
|
|
VectorSubtract( saberDest, saber->currentOrigin, saber->s.pos.trDelta );
|
|
dist = VectorNormalize( saber->s.pos.trDelta );
|
|
if ( self->client->ps.forcePowerLevel[FP_SABERTHROW] > FORCE_LEVEL_2 && self->client->ps.saberEntityState == SES_LEAVING && !enemy )
|
|
{
|
|
if ( dist < 200 )
|
|
{
|
|
saberSpeed = 400 - (dist*2);
|
|
}
|
|
}
|
|
else if ( self->client->ps.saberEntityState == SES_LEAVING && dist < 50 )
|
|
{
|
|
saberSpeed = dist * 2 + 30;
|
|
if ( (enemy && dist > enemy->maxs[0]) || (!enemy && dist > 24) )
|
|
{//auto-tracking an enemy and we can't hit him
|
|
if ( saberSpeed < 120 )
|
|
{//clamp to a minimum speed
|
|
saberSpeed = 120;
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
if ( self->client->ps.saberEntityState == SES_RETURNING )
|
|
{//FIXME: if returning, move faster?
|
|
saberSpeed = 800;
|
|
if ( dist < 200 )
|
|
{
|
|
saberSpeed -= 400 - (dist*2);
|
|
}
|
|
}
|
|
*/
|
|
VectorScale( saber->s.pos.trDelta, saberSpeed, saber->s.pos.trDelta );
|
|
//SnapVector( saber->s.pos.trDelta ); // save net bandwidth
|
|
VectorCopy( saber->currentOrigin, saber->s.pos.trBase );
|
|
saber->s.pos.trTime = level.time;
|
|
saber->s.pos.trType = TR_LINEAR;
|
|
}
|
|
else
|
|
{
|
|
VectorCopy( saber->currentOrigin, saber->s.pos.trBase );
|
|
saber->s.pos.trTime = level.time;
|
|
saber->s.pos.trType = TR_LINEAR;
|
|
}
|
|
|
|
//if it's heading back, point it's base at us
|
|
if ( self->client->ps.saberEntityState == SES_RETURNING
|
|
&& !(self->client->ps.saber[0].saberFlags&SFL_RETURN_DAMAGE) )//type != SABER_STAR )
|
|
{
|
|
fwdangles[0] += SABER_PITCH_HACK;
|
|
VectorCopy( fwdangles, saber->s.apos.trBase );
|
|
saber->s.apos.trTime = level.time;
|
|
saber->s.apos.trType = TR_INTERPOLATE;
|
|
VectorClear( saber->s.apos.trDelta );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
qboolean WP_SaberLaunch( gentity_t *self, gentity_t *saber, qboolean thrown, qboolean noFail = qfalse )
|
|
{//FIXME: probably need a debounce time
|
|
vec3_t saberMins={-3.0f,-3.0f,-3.0f};
|
|
vec3_t saberMaxs={3.0f,3.0f,3.0f};
|
|
trace_t trace;
|
|
|
|
if ( self->client->NPC_class == CLASS_SABER_DROID )
|
|
{//saber droids can't drop their saber
|
|
return qfalse;
|
|
}
|
|
if ( !noFail )
|
|
{
|
|
if ( thrown )
|
|
{//this is a regular throw, so see if it's legal
|
|
if ( self->client->ps.forcePowerLevel[FP_SABERTHROW] > FORCE_LEVEL_2 )
|
|
{
|
|
if ( !WP_ForcePowerUsable( self, FP_SABERTHROW, 20 ) )
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( !WP_ForcePowerUsable( self, FP_SABERTHROW, 0 ) )
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
}
|
|
if ( !self->s.number && (cg.zoomMode || in_camera) )
|
|
{//can't saber throw when zoomed in or in cinematic
|
|
return qfalse;
|
|
}
|
|
//make sure it won't start in solid
|
|
gi.trace( &trace, self->client->renderInfo.handRPoint, saberMins, saberMaxs, self->client->renderInfo.handRPoint, saber->s.number, MASK_SOLID, (EG2_Collision)0, 0 );
|
|
if ( trace.startsolid || trace.allsolid )
|
|
{
|
|
return qfalse;
|
|
}
|
|
//make sure I'm not throwing it on the other side of a door or wall or whatever
|
|
gi.trace( &trace, self->currentOrigin, vec3_origin, vec3_origin, self->client->renderInfo.handRPoint, self->s.number, MASK_SOLID, (EG2_Collision)0, 0 );
|
|
if ( trace.startsolid || trace.allsolid || trace.fraction < 1.0f )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
if ( thrown )
|
|
{//this is a regular throw, so take force power
|
|
if ( self->client->ps.forcePowerLevel[FP_SABERTHROW] > FORCE_LEVEL_2 )
|
|
{//at max skill, the cost increases as keep it out
|
|
WP_ForcePowerStart( self, FP_SABERTHROW, 10 );
|
|
}
|
|
else
|
|
{
|
|
WP_ForcePowerStart( self, FP_SABERTHROW, 0 );
|
|
}
|
|
}
|
|
}
|
|
//clear the enemy
|
|
saber->enemy = NULL;
|
|
|
|
//===FIXME!!!==============================================================================================
|
|
//We should copy the right-hand saber's g2 instance to the thrown saber
|
|
//Then back again when you catch it!!!
|
|
//===FIXME!!!==============================================================================================
|
|
|
|
//draw it
|
|
saber->s.eFlags &= ~EF_NODRAW;
|
|
saber->svFlags |= SVF_BROADCAST;
|
|
saber->svFlags &= ~SVF_NOCLIENT;
|
|
|
|
//place it
|
|
VectorCopy( self->client->renderInfo.handRPoint, saber->currentOrigin );//muzzlePoint
|
|
VectorCopy( saber->currentOrigin, saber->s.pos.trBase );
|
|
saber->s.pos.trTime = level.time;
|
|
saber->s.pos.trType = TR_LINEAR;
|
|
VectorClear( saber->s.pos.trDelta );
|
|
gi.linkentity( saber );
|
|
|
|
//spin it
|
|
VectorClear( saber->s.apos.trBase );
|
|
saber->s.apos.trTime = level.time;
|
|
saber->s.apos.trType = TR_LINEAR;
|
|
if ( self->health > 0 && thrown )
|
|
{//throwing it
|
|
saber->s.apos.trBase[1] = self->client->ps.viewangles[1];
|
|
saber->s.apos.trBase[0] = SABER_PITCH_HACK;
|
|
}
|
|
else
|
|
{//dropping it
|
|
vectoangles( self->client->renderInfo.muzzleDir, saber->s.apos.trBase );
|
|
}
|
|
VectorClear( saber->s.apos.trDelta );
|
|
|
|
switch ( self->client->ps.forcePowerLevel[FP_SABERTHROW] )
|
|
{//FIXME: make a table?
|
|
default:
|
|
case FORCE_LEVEL_1:
|
|
saber->s.apos.trDelta[1] = 600;
|
|
break;
|
|
case FORCE_LEVEL_2:
|
|
saber->s.apos.trDelta[1] = 800;
|
|
break;
|
|
case FORCE_LEVEL_3:
|
|
saber->s.apos.trDelta[1] = 1200;
|
|
break;
|
|
}
|
|
|
|
//Take it out of my hand
|
|
self->client->ps.saberInFlight = qtrue;
|
|
self->client->ps.saberEntityState = SES_LEAVING;
|
|
self->client->ps.saberEntityDist = saberThrowDist[self->client->ps.forcePowerLevel[FP_SABERTHROW]];
|
|
self->client->ps.saberThrowTime = level.time;
|
|
//if ( self->client->ps.forcePowerLevel[FP_SABERTHROW] > FORCE_LEVEL_2 )
|
|
{
|
|
self->client->ps.forcePowerDebounce[FP_SABERTHROW] = level.time + 1000;//so we can keep it out for a minimum amount of time
|
|
}
|
|
|
|
if ( thrown )
|
|
{//this is a regular throw, so turn the saber on
|
|
//turn saber on
|
|
if ( (self->client->ps.saber[0].saberFlags&SFL_SINGLE_BLADE_THROWABLE) )//SaberStaff() )
|
|
{//only first blade can be on
|
|
if ( !self->client->ps.saber[0].blade[0].active )
|
|
{//turn on first one
|
|
self->client->ps.SaberBladeActivate( 0, 0 );
|
|
}
|
|
for ( int i = 1; i < self->client->ps.saber[0].numBlades; i++ )
|
|
{//turn off all others
|
|
if ( self->client->ps.saber[0].blade[i].active )
|
|
{
|
|
self->client->ps.SaberBladeActivate( 0, i, qfalse );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{//turn the sabers, all blades...?
|
|
self->client->ps.saber[0].Activate();
|
|
//self->client->ps.SaberActivate();
|
|
}
|
|
//turn on the saber trail
|
|
self->client->ps.saber[0].ActivateTrail( 150 );
|
|
}
|
|
|
|
//reset the mins
|
|
VectorCopy( saberMins, saber->mins );
|
|
VectorCopy( saberMaxs, saber->maxs );
|
|
saber->contents = 0;//CONTENTS_LIGHTSABER;
|
|
saber->clipmask = MASK_SOLID | CONTENTS_LIGHTSABER;
|
|
|
|
// remove the ghoul2 right-hand saber model on the player
|
|
if ( self->weaponModel[0] > 0 )
|
|
{
|
|
gi.G2API_RemoveGhoul2Model(self->ghoul2, self->weaponModel[0]);
|
|
self->weaponModel[0] = -1;
|
|
}
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
qboolean WP_SaberLose( gentity_t *self, vec3_t throwDir )
|
|
{
|
|
if ( !self || !self->client || self->client->ps.saberEntityNum <= 0 )
|
|
{//WTF?!! We lost it already?
|
|
return qfalse;
|
|
}
|
|
if ( self->client->NPC_class == CLASS_SABER_DROID )
|
|
{//saber droids can't drop their saber
|
|
return qfalse;
|
|
}
|
|
gentity_t *dropped = &g_entities[self->client->ps.saberEntityNum];
|
|
if ( !self->client->ps.saberInFlight )
|
|
{//not alreay in air
|
|
/*
|
|
qboolean noForceThrow = qfalse;
|
|
//make it so we can throw it
|
|
self->client->ps.forcePowersKnown |= (1<<FP_SABERTHROW);
|
|
if ( self->client->ps.forcePowerLevel[FP_SABERTHROW] < FORCE_LEVEL_1 )
|
|
{
|
|
noForceThrow = qtrue;
|
|
self->client->ps.forcePowerLevel[FP_SABERTHROW] = FORCE_LEVEL_1;
|
|
}
|
|
*/
|
|
//throw it
|
|
if ( !WP_SaberLaunch( self, dropped, qfalse ) )
|
|
{//couldn't throw it
|
|
return qfalse;
|
|
}
|
|
/*
|
|
if ( noForceThrow )
|
|
{
|
|
self->client->ps.forcePowerLevel[FP_SABERTHROW] = FORCE_LEVEL_0;
|
|
}
|
|
*/
|
|
}
|
|
if ( self->client->ps.saber[0].Active() )
|
|
{//on
|
|
//drop it instantly
|
|
WP_SaberDrop( self, dropped );
|
|
}
|
|
//optionally give it some thrown velocity
|
|
if ( throwDir && !VectorCompare( throwDir, vec3_origin ) )
|
|
{
|
|
VectorCopy( throwDir, dropped->s.pos.trDelta );
|
|
}
|
|
//don't pull it back on the next frame
|
|
if ( self->NPC )
|
|
{
|
|
self->NPC->last_ucmd.buttons &= ~BUTTON_ATTACK;
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
void WP_SetSaberOrigin( gentity_t *self, vec3_t newOrg )
|
|
{
|
|
if ( !self || !self->client )
|
|
{
|
|
return;
|
|
}
|
|
if ( self->client->ps.saberEntityNum <= 0 || self->client->ps.saberEntityNum >= ENTITYNUM_WORLD )
|
|
{//no saber ent to reposition
|
|
return;
|
|
}
|
|
if ( self->client->NPC_class == CLASS_SABER_DROID )
|
|
{//saber droids can't drop their saber
|
|
return;
|
|
}
|
|
gentity_t *dropped = &g_entities[self->client->ps.saberEntityNum];
|
|
if ( !self->client->ps.saberInFlight )
|
|
{//not already in air
|
|
qboolean noForceThrow = qfalse;
|
|
//make it so we can throw it
|
|
self->client->ps.forcePowersKnown |= (1<<FP_SABERTHROW);
|
|
if ( self->client->ps.forcePowerLevel[FP_SABERTHROW] < FORCE_LEVEL_1 )
|
|
{
|
|
noForceThrow = qtrue;
|
|
self->client->ps.forcePowerLevel[FP_SABERTHROW] = FORCE_LEVEL_1;
|
|
}
|
|
//throw it
|
|
if ( !WP_SaberLaunch( self, dropped, qfalse, qtrue ) )
|
|
{//couldn't throw it
|
|
return;
|
|
}
|
|
if ( noForceThrow )
|
|
{
|
|
self->client->ps.forcePowerLevel[FP_SABERTHROW] = FORCE_LEVEL_0;
|
|
}
|
|
}
|
|
VectorCopy( newOrg, dropped->s.origin );
|
|
VectorCopy( newOrg, dropped->currentOrigin );
|
|
VectorCopy( newOrg, dropped->s.pos.trBase );
|
|
//drop it instantly
|
|
WP_SaberDrop( self, dropped );
|
|
//don't pull it back on the next frame
|
|
if ( self->NPC )
|
|
{
|
|
self->NPC->last_ucmd.buttons &= ~BUTTON_ATTACK;
|
|
}
|
|
}
|
|
|
|
void WP_SaberCatch( gentity_t *self, gentity_t *saber, qboolean switchToSaber )
|
|
{//FIXME: probably need a debounce time
|
|
if ( self->health > 0 && !PM_SaberInBrokenParry( self->client->ps.saberMove ) && self->client->ps.saberBlocked != BLOCKED_PARRY_BROKEN )
|
|
{
|
|
//clear the enemy
|
|
saber->enemy = NULL;
|
|
//===FIXME!!!==============================================================================================
|
|
//We should copy the thrown saber's g2 instance to the right-hand saber
|
|
//When you catch it, and vice-versa when you throw it!!!
|
|
//===FIXME!!!==============================================================================================
|
|
//don't draw it
|
|
saber->s.eFlags |= EF_NODRAW;
|
|
saber->svFlags &= SVF_BROADCAST;
|
|
saber->svFlags |= SVF_NOCLIENT;
|
|
|
|
//take off any gravity stuff if we'd dropped it
|
|
saber->s.pos.trType = TR_LINEAR;
|
|
saber->s.eFlags &= ~EF_BOUNCE_HALF;
|
|
|
|
//Put it in my hand
|
|
self->client->ps.saberInFlight = qfalse;
|
|
self->client->ps.saberEntityState = SES_LEAVING;
|
|
|
|
//turn off the saber trail
|
|
self->client->ps.saber[0].DeactivateTrail( 75 );
|
|
|
|
//reset its contents/clipmask
|
|
saber->contents = CONTENTS_LIGHTSABER;// | CONTENTS_SHOTCLIP;
|
|
saber->clipmask = MASK_SHOT | CONTENTS_LIGHTSABER;
|
|
|
|
//play catch sound
|
|
G_Sound( saber, G_SoundIndex( "sound/weapons/saber/saber_catch.wav" ) );
|
|
//FIXME: if an NPC, don't turn it back on if no enemy or enemy is dead...
|
|
//if it's not our current weapon, make it our current weapon
|
|
if ( self->client->ps.weapon == WP_SABER )
|
|
{//only do the first saber since we only throw the first one
|
|
WP_SaberAddG2SaberModels( self, qfalse );
|
|
}
|
|
if ( switchToSaber )
|
|
{
|
|
if ( self->client->ps.weapon != WP_SABER )
|
|
{
|
|
CG_ChangeWeapon( WP_SABER );
|
|
}
|
|
else
|
|
{//if it's not active, turn it on
|
|
if ( (self->client->ps.saber[0].saberFlags&SFL_SINGLE_BLADE_THROWABLE) )//SaberStaff() )
|
|
{//only first blade can be on
|
|
if ( !self->client->ps.saber[0].blade[0].active )
|
|
{//only turn it on if first blade is off, otherwise, leave as-is
|
|
self->client->ps.saber[0].Activate();
|
|
}
|
|
}
|
|
else
|
|
{//turn all blades on
|
|
self->client->ps.saber[0].Activate();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void WP_SaberReturn( gentity_t *self, gentity_t *saber )
|
|
{
|
|
if ( PM_SaberInBrokenParry( self->client->ps.saberMove ) || self->client->ps.saberBlocked == BLOCKED_PARRY_BROKEN )
|
|
{
|
|
return;
|
|
}
|
|
if ( self && self->client )
|
|
{//still alive and stuff
|
|
//FIXME: when it's returning, flies butt first, but seems to do a lot of damage when going through people... hmm...
|
|
self->client->ps.saberEntityState = SES_RETURNING;
|
|
//turn down the saber trail
|
|
if ( !(self->client->ps.saber[0].saberFlags&SFL_RETURN_DAMAGE) )//type != SABER_STAR )
|
|
{
|
|
self->client->ps.saber[0].DeactivateTrail( 75 );
|
|
}
|
|
}
|
|
if ( !(saber->s.eFlags&EF_BOUNCE) )
|
|
{
|
|
saber->s.eFlags |= EF_BOUNCE;
|
|
saber->bounceCount = 300;
|
|
}
|
|
}
|
|
|
|
|
|
void WP_SaberDrop( gentity_t *self, gentity_t *saber )
|
|
{
|
|
//clear the enemy
|
|
saber->enemy = NULL;
|
|
saber->s.eFlags &= ~EF_BOUNCE;
|
|
saber->bounceCount = 0;
|
|
//make it fall
|
|
saber->s.pos.trType = TR_GRAVITY;
|
|
//make it bounce some
|
|
saber->s.eFlags |= EF_BOUNCE_HALF;
|
|
//make it spin
|
|
VectorCopy( saber->currentAngles, saber->s.apos.trBase );
|
|
saber->s.apos.trType = TR_LINEAR;
|
|
saber->s.apos.trTime = level.time;
|
|
VectorSet( saber->s.apos.trDelta, Q_irand( -300, 300 ), saber->s.apos.trDelta[1], Q_irand( -300, 300 ) );
|
|
if ( !saber->s.apos.trDelta[1] )
|
|
{
|
|
saber->s.apos.trDelta[1] = Q_irand( -300, 300 );
|
|
}
|
|
//force it to be ready to return
|
|
self->client->ps.saberEntityDist = 0;
|
|
self->client->ps.saberEntityState = SES_RETURNING;
|
|
//turn it off
|
|
self->client->ps.saber[0].Deactivate();
|
|
//turn off the saber trail
|
|
self->client->ps.saber[0].DeactivateTrail( 75 );
|
|
//play the saber turning off sound
|
|
G_SoundIndexOnEnt( saber, CHAN_AUTO, self->client->ps.saber[0].soundOff );
|
|
|
|
if ( self->health <= 0 )
|
|
{//owner is dead!
|
|
saber->s.time = level.time;//will make us free ourselves after a time
|
|
}
|
|
}
|
|
|
|
|
|
void WP_SaberPull( gentity_t *self, gentity_t *saber )
|
|
{
|
|
if ( PM_SaberInBrokenParry( self->client->ps.saberMove ) || self->client->ps.saberBlocked == BLOCKED_PARRY_BROKEN )
|
|
{
|
|
return;
|
|
}
|
|
if ( self->health > 0 )
|
|
{
|
|
//take off gravity
|
|
saber->s.pos.trType = TR_LINEAR;
|
|
//take off bounce
|
|
saber->s.eFlags &= EF_BOUNCE_HALF;
|
|
//play sound
|
|
G_Sound( self, G_SoundIndex( "sound/weapons/force/pull.wav" ) );
|
|
}
|
|
}
|
|
|
|
const char *saberColorStringForColor[SABER_PURPLE+1] =
|
|
{
|
|
"red",//SABER_RED
|
|
"orange",//SABER_ORANGE
|
|
"yellow",//SABER_YELLOW
|
|
"green",//SABER_GREEN
|
|
"blue",//SABER_BLUE
|
|
"purple"//SABER_PURPLE
|
|
};
|
|
|
|
// Check if we are throwing it, launch it if needed, update position if needed.
|
|
void WP_SaberThrow( gentity_t *self, usercmd_t *ucmd )
|
|
{
|
|
vec3_t saberDiff;
|
|
trace_t tr;
|
|
//static float SABER_SPEED = 10;
|
|
|
|
gentity_t *saberent;
|
|
|
|
if ( self->client->ps.saberEntityNum <= 0 || self->client->ps.saberEntityNum >= ENTITYNUM_WORLD )
|
|
{//WTF?!! We lost it?
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.torsoAnim == BOTH_LOSE_SABER )
|
|
{//can't catch it while it's being yanked from your hand!
|
|
return;
|
|
}
|
|
|
|
if ( !g_saberNewControlScheme->integer )
|
|
{
|
|
if ( PM_SaberInKata( (saberMoveName_t)self->client->ps.saberMove ) )
|
|
{//don't throw saber when in special attack (alt+attack)
|
|
return;
|
|
}
|
|
if ( (ucmd->buttons&BUTTON_ATTACK)
|
|
&& (ucmd->buttons&BUTTON_ALT_ATTACK)
|
|
&& !self->client->ps.saberInFlight
|
|
&& self->client->ps.clientNum == 0
|
|
&& cg.renderingThirdPerson)
|
|
{//trying to do special attack, don't throw it
|
|
return;
|
|
}
|
|
else if ( self->client->ps.torsoAnim == BOTH_A1_SPECIAL
|
|
|| self->client->ps.torsoAnim == BOTH_A2_SPECIAL
|
|
|| self->client->ps.torsoAnim == BOTH_A3_SPECIAL )
|
|
{//don't throw in these anims!
|
|
return;
|
|
}
|
|
}
|
|
saberent = &g_entities[self->client->ps.saberEntityNum];
|
|
|
|
VectorSubtract( self->client->renderInfo.handRPoint, saberent->currentOrigin, saberDiff );
|
|
|
|
//is our saber in flight?
|
|
if ( !self->client->ps.saberInFlight )
|
|
{//saber is not in flight right now
|
|
if ( self->client->ps.weapon != WP_SABER )
|
|
{//don't even have it out
|
|
return;
|
|
}
|
|
else if ( (ucmd->buttons&BUTTON_ALT_ATTACK) && !(self->client->ps.pm_flags&PMF_ALT_ATTACK_HELD) )
|
|
{//still holding it, not still holding attack from a previous throw, so throw it.
|
|
if ( !(self->client->ps.saberEventFlags&SEF_INWATER) && WP_SaberLaunch( self, saberent, qtrue ) )
|
|
{
|
|
if ( self->client && !self->s.number )
|
|
{
|
|
self->client->sess.missionStats.saberThrownCnt++;
|
|
}
|
|
//need to recalc this because we just moved it
|
|
VectorSubtract( self->client->renderInfo.handRPoint, saberent->currentOrigin, saberDiff );
|
|
}
|
|
else
|
|
{//couldn't throw it
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{//holding it, don't want to throw it, go away.
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{//inflight
|
|
//is our saber currently on it's way back to us?
|
|
if ( self->client->ps.saberEntityState == SES_RETURNING )
|
|
{//see if we're close enough to pick it up
|
|
if ( VectorLengthSquared( saberDiff ) <= 256 )//16 squared//G_BoundsOverlap( self->absmin, self->absmax, saberent->absmin, saberent->absmax ) )//
|
|
{//caught it
|
|
vec3_t axisPoint;
|
|
trace_t trace;
|
|
VectorCopy( self->currentOrigin, axisPoint );
|
|
axisPoint[2] = self->client->renderInfo.handRPoint[2];
|
|
gi.trace( &trace, axisPoint, vec3_origin, vec3_origin, self->client->renderInfo.handRPoint, self->s.number, MASK_SOLID, (EG2_Collision)0, 0 );
|
|
if ( !trace.startsolid && trace.fraction >= 1.0f )
|
|
{//our hand isn't through a wall
|
|
WP_SaberCatch( self, saberent, qtrue );
|
|
//NPC_SetAnim( self, SETANIM_TORSO, TORSO_HANDRETRACT1, SETANIM_FLAG_OVERRIDE );
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ( saberent->s.pos.trType != TR_STATIONARY )
|
|
{//saber is in flight, lerp it
|
|
if ( self->health <= 0 )//&& level.time > saberent->s.time + 5000 )
|
|
{//make us free ourselves after a time
|
|
if ( g_saberPickuppableDroppedSabers->integer
|
|
&& G_DropSaberItem( self->client->ps.saber[0].name, self->client->ps.saber[0].blade[0].color, saberent->currentOrigin, saberent->s.pos.trDelta, saberent->currentAngles ) != NULL )
|
|
{//dropped it
|
|
//free it
|
|
G_FreeEntity( saberent );
|
|
//forget it
|
|
self->client->ps.saberEntityNum = ENTITYNUM_NONE;
|
|
return;
|
|
}
|
|
}
|
|
WP_RunSaber( self, saberent );
|
|
}
|
|
else
|
|
{//it fell on the ground
|
|
if ( self->health <= 0 )//&& level.time > saberent->s.time + 5000 )
|
|
{//make us free ourselves after a time
|
|
if ( g_saberPickuppableDroppedSabers->integer )
|
|
{//spawn an item
|
|
G_DropSaberItem( self->client->ps.saber[0].name, self->client->ps.saber[0].blade[0].color, saberent->currentOrigin, saberent->s.pos.trDelta, saberent->currentAngles );
|
|
}
|
|
//free it
|
|
G_FreeEntity( saberent );
|
|
//forget it
|
|
self->client->ps.saberEntityNum = ENTITYNUM_NONE;
|
|
return;
|
|
}
|
|
if ( (!self->s.number && level.time - saberent->aimDebounceTime > 15000)
|
|
|| (self->s.number && level.time - saberent->aimDebounceTime > 5000) )
|
|
{//(only for player) been missing for 15 seconds, automagicially return
|
|
WP_SaberCatch( self, saberent, qfalse );
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
//are we still trying to use the saber?
|
|
if ( self->client->ps.weapon != WP_SABER )
|
|
{//switched away
|
|
if ( !self->client->ps.saberInFlight )
|
|
{//wasn't throwing saber
|
|
return;
|
|
}
|
|
else if ( saberent->s.pos.trType == TR_LINEAR )
|
|
{//switched away while controlling it, just drop the saber
|
|
WP_SaberDrop( self, saberent );
|
|
return;
|
|
}
|
|
else
|
|
{//it's on the ground, see if it's inside us (touching)
|
|
if ( G_PointInBounds( saberent->currentOrigin, self->absmin, self->absmax ) )
|
|
{//it's in us, pick it up automatically
|
|
WP_SaberPull( self, saberent );
|
|
}
|
|
}
|
|
}
|
|
else if ( saberent->s.pos.trType != TR_LINEAR )
|
|
{//weapon is saber and not flying
|
|
if ( self->client->ps.saberInFlight )
|
|
{//we dropped it
|
|
if ( ucmd->buttons & BUTTON_ATTACK )//|| self->client->ps.weaponstate == WEAPON_RAISING )//ucmd->buttons & BUTTON_ALT_ATTACK ||
|
|
{//we actively want to pick it up or we just switched to it, so pull it back
|
|
gi.trace( &tr, saberent->currentOrigin, saberent->mins, saberent->maxs, self->client->renderInfo.handRPoint, self->s.number, MASK_SOLID, (EG2_Collision)0, 0 );
|
|
|
|
if ( tr.allsolid || tr.startsolid || tr.fraction < 1.0f )
|
|
{//can't pick it up yet, no LOS
|
|
return;
|
|
}
|
|
//clear LOS, pick it up
|
|
WP_SaberPull( self, saberent );
|
|
}
|
|
else
|
|
{//see if it's inside us (touching)
|
|
if ( G_PointInBounds( saberent->currentOrigin, self->absmin, self->absmax ) )
|
|
{//it's in us, pick it up automatically
|
|
WP_SaberPull( self, saberent );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if ( self->health <= 0 && self->client->ps.saberInFlight )
|
|
{//we died, drop it
|
|
WP_SaberDrop( self, saberent );
|
|
return;
|
|
}
|
|
else if ( !self->client->ps.saber[0].Active() && self->client->ps.saberEntityState != SES_RETURNING )
|
|
{//we turned it off, drop it
|
|
WP_SaberDrop( self, saberent );
|
|
return;
|
|
}
|
|
|
|
//TODO: if deactivate saber in flight, should it drop?
|
|
|
|
if ( saberent->s.pos.trType != TR_LINEAR )
|
|
{//don't home
|
|
return;
|
|
}
|
|
|
|
float saberDist = VectorLength( saberDiff );
|
|
if ( self->client->ps.saberEntityState == SES_LEAVING )
|
|
{//saber still flying forward
|
|
if ( self->client->ps.forcePowerLevel[FP_SABERTHROW] > FORCE_LEVEL_2 )
|
|
{//still holding it out
|
|
if ( !(ucmd->buttons&BUTTON_ALT_ATTACK) && self->client->ps.forcePowerDebounce[FP_SABERTHROW] < level.time )
|
|
{//done throwing, return to me
|
|
if ( self->client->ps.saber[0].Active() )
|
|
{//still on
|
|
WP_SaberReturn( self, saberent );
|
|
}
|
|
}
|
|
else if ( level.time - self->client->ps.saberThrowTime >= 100 )
|
|
{
|
|
if ( WP_ForcePowerAvailable( self, FP_SABERTHROW, 1 ) )
|
|
{
|
|
WP_ForcePowerDrain( self, FP_SABERTHROW, 1 );
|
|
self->client->ps.saberThrowTime = level.time;
|
|
}
|
|
else
|
|
{//out of force power, return to me
|
|
WP_SaberReturn( self, saberent );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( !(ucmd->buttons&BUTTON_ALT_ATTACK) && self->client->ps.forcePowerDebounce[FP_SABERTHROW] < level.time )
|
|
{//not holding button and has been out at least 1 second, return to me
|
|
if ( self->client->ps.saber[0].Active() )
|
|
{//still on
|
|
WP_SaberReturn( self, saberent );
|
|
}
|
|
}
|
|
else if ( level.time - self->client->ps.saberThrowTime > 3000
|
|
|| (self->client->ps.forcePowerLevel[FP_SABERTHROW]==FORCE_LEVEL_1&&saberDist>=self->client->ps.saberEntityDist) )
|
|
{//been out too long, or saber throw 1 went too far, return to me
|
|
if ( self->client->ps.saber[0].Active() )
|
|
{//still on
|
|
WP_SaberReturn( self, saberent );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ( self->client->ps.saberEntityState == SES_RETURNING )
|
|
{
|
|
if ( self->client->ps.saberEntityDist > 0 )
|
|
{
|
|
self->client->ps.saberEntityDist -= 25;
|
|
}
|
|
if ( self->client->ps.saberEntityDist < 0 )
|
|
{
|
|
self->client->ps.saberEntityDist = 0;
|
|
}
|
|
else if ( saberDist < self->client->ps.saberEntityDist )
|
|
{//if it's coming back to me, never push it away
|
|
self->client->ps.saberEntityDist = saberDist;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//SABER BLOCKING============================================================================
|
|
//SABER BLOCKING============================================================================
|
|
//SABER BLOCKING============================================================================
|
|
//SABER BLOCKING============================================================================
|
|
//SABER BLOCKING============================================================================
|
|
int WP_MissileBlockForBlock( int saberBlock )
|
|
{
|
|
switch( saberBlock )
|
|
{
|
|
case BLOCKED_UPPER_RIGHT:
|
|
return BLOCKED_UPPER_RIGHT_PROJ;
|
|
break;
|
|
case BLOCKED_UPPER_LEFT:
|
|
return BLOCKED_UPPER_LEFT_PROJ;
|
|
break;
|
|
case BLOCKED_LOWER_RIGHT:
|
|
return BLOCKED_LOWER_RIGHT_PROJ;
|
|
break;
|
|
case BLOCKED_LOWER_LEFT:
|
|
return BLOCKED_LOWER_LEFT_PROJ;
|
|
break;
|
|
case BLOCKED_TOP:
|
|
return BLOCKED_TOP_PROJ;
|
|
break;
|
|
}
|
|
return saberBlock;
|
|
}
|
|
|
|
void WP_SaberBlockNonRandom( gentity_t *self, vec3_t hitloc, qboolean missileBlock )
|
|
{
|
|
vec3_t diff, fwdangles={0,0,0}, right;
|
|
float rightdot;
|
|
float zdiff;
|
|
|
|
if ( self->client->ps.weaponstate == WEAPON_DROPPING ||
|
|
self->client->ps.weaponstate == WEAPON_RAISING )
|
|
{//don't block while changing weapons
|
|
return;
|
|
}
|
|
if ( PM_SuperBreakLoseAnim( self->client->ps.torsoAnim )
|
|
|| PM_SuperBreakWinAnim( self->client->ps.torsoAnim ) )
|
|
{
|
|
return;
|
|
}
|
|
//NPCs don't auto-block
|
|
if ( !missileBlock && self->s.number != 0 && self->client->ps.saberBlocked != BLOCKED_NONE )
|
|
{
|
|
return;
|
|
}
|
|
|
|
VectorSubtract( hitloc, self->client->renderInfo.eyePoint, diff );
|
|
diff[2] = 0;
|
|
VectorNormalize( diff );
|
|
|
|
fwdangles[1] = self->client->ps.viewangles[1];
|
|
// Ultimately we might care if the shot was ahead or behind, but for now, just quadrant is fine.
|
|
AngleVectors( fwdangles, NULL, right, NULL );
|
|
|
|
rightdot = DotProduct(right, diff);
|
|
zdiff = hitloc[2] - self->client->renderInfo.eyePoint[2];
|
|
|
|
//FIXME: take torsoAngles into account?
|
|
if ( zdiff > -5 )//0 )//40 )
|
|
{
|
|
if ( rightdot > 0.3 )
|
|
{
|
|
self->client->ps.saberBlocked = BLOCKED_UPPER_RIGHT;
|
|
}
|
|
else if ( rightdot < -0.3 )
|
|
{
|
|
self->client->ps.saberBlocked = BLOCKED_UPPER_LEFT;
|
|
}
|
|
else
|
|
{
|
|
self->client->ps.saberBlocked = BLOCKED_TOP;
|
|
}
|
|
}
|
|
else if ( zdiff > -22 )//-20 )//20 )
|
|
{
|
|
if ( zdiff < -10 )//30 )
|
|
{//hmm, pretty low, but not low enough to use the low block, so we need to duck
|
|
//NPC should duck, but NPC should never get here
|
|
}
|
|
if ( rightdot > 0.1 )
|
|
{
|
|
self->client->ps.saberBlocked = BLOCKED_UPPER_RIGHT;
|
|
}
|
|
else if ( rightdot < -0.1 )
|
|
{
|
|
self->client->ps.saberBlocked = BLOCKED_UPPER_LEFT;
|
|
}
|
|
else
|
|
{//FIXME: this looks really weird if the shot is too low!
|
|
self->client->ps.saberBlocked = BLOCKED_TOP;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( rightdot >= 0 )
|
|
{
|
|
self->client->ps.saberBlocked = BLOCKED_LOWER_RIGHT;
|
|
}
|
|
else
|
|
{
|
|
self->client->ps.saberBlocked = BLOCKED_LOWER_LEFT;
|
|
}
|
|
}
|
|
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer )
|
|
{
|
|
if ( !self->s.number )
|
|
{
|
|
gi.Printf( "EyeZ: %4.2f HitZ: %4.2f zdiff: %4.2f rdot: %4.2f\n", self->client->renderInfo.eyePoint[2], hitloc[2], zdiff, rightdot );
|
|
switch ( self->client->ps.saberBlocked )
|
|
{
|
|
case BLOCKED_TOP:
|
|
gi.Printf( "BLOCKED_TOP\n" );
|
|
break;
|
|
case BLOCKED_UPPER_RIGHT:
|
|
gi.Printf( "BLOCKED_UPPER_RIGHT\n" );
|
|
break;
|
|
case BLOCKED_UPPER_LEFT:
|
|
gi.Printf( "BLOCKED_UPPER_LEFT\n" );
|
|
break;
|
|
case BLOCKED_LOWER_RIGHT:
|
|
gi.Printf( "BLOCKED_LOWER_RIGHT\n" );
|
|
break;
|
|
case BLOCKED_LOWER_LEFT:
|
|
gi.Printf( "BLOCKED_LOWER_LEFT\n" );
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if ( missileBlock )
|
|
{
|
|
self->client->ps.saberBlocked = WP_MissileBlockForBlock( self->client->ps.saberBlocked );
|
|
}
|
|
|
|
if ( self->client->ps.saberBlocked != BLOCKED_NONE )
|
|
{
|
|
int parryReCalcTime = Jedi_ReCalcParryTime( self, EVASION_PARRY );
|
|
if ( self->client->ps.forcePowerDebounce[FP_SABER_DEFENSE] < level.time + parryReCalcTime )
|
|
{
|
|
self->client->ps.forcePowerDebounce[FP_SABER_DEFENSE] = level.time + parryReCalcTime;
|
|
}
|
|
}
|
|
}
|
|
|
|
void WP_SaberStartMissileBlockCheck( gentity_t *self, usercmd_t *ucmd )
|
|
{
|
|
float dist;
|
|
gentity_t *ent, *incoming = NULL;
|
|
gentity_t *entityList[MAX_GENTITIES];
|
|
int numListedEntities;
|
|
vec3_t mins, maxs;
|
|
int i, e;
|
|
float closestDist, radius = 256;
|
|
vec3_t forward, dir, missile_dir, fwdangles = {0};
|
|
trace_t trace;
|
|
vec3_t traceTo, entDir;
|
|
qboolean dodgeOnlySabers = qfalse;
|
|
|
|
|
|
if ( self->NPC && (self->NPC->scriptFlags&SCF_IGNORE_ALERTS) )
|
|
{//don't react to things flying at me...
|
|
return;
|
|
}
|
|
if ( self->health <= 0 )
|
|
{//dead don't try to block (NOTE: actual deflection happens in missile code)
|
|
return;
|
|
}
|
|
|
|
if ( PM_InKnockDown( &self->client->ps ) )
|
|
{//can't block when knocked down
|
|
return;
|
|
}
|
|
|
|
if ( PM_SuperBreakLoseAnim( self->client->ps.torsoAnim )
|
|
|| PM_SuperBreakWinAnim( self->client->ps.torsoAnim ) )
|
|
{//can't block while in break anim
|
|
return;
|
|
}
|
|
|
|
if ( Rosh_BeingHealed( self ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( self->client->NPC_class == CLASS_ROCKETTROOPER )
|
|
{//rockettrooper
|
|
if ( self->client->ps.groundEntityNum != ENTITYNUM_NONE )
|
|
{//must be in air
|
|
return;
|
|
}
|
|
if ( Q_irand( 0, 4-(g_spskill->integer*2) ) )
|
|
{//easier level guys do this less
|
|
return;
|
|
}
|
|
if ( Q_irand( 0, 3 ) )
|
|
{//base level: 25% chance of looking for something to dodge
|
|
if ( Q_irand( 0, 1 ) )
|
|
{//dodge sabers twice as frequently as other projectiles
|
|
dodgeOnlySabers = qtrue;
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( self->client->NPC_class == CLASS_BOBAFETT )
|
|
{//Boba doesn't dodge quite as much
|
|
if ( Q_irand( 0, 2-g_spskill->integer) )
|
|
{//easier level guys do this less
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ( self->client->NPC_class != CLASS_BOBAFETT
|
|
&& (self->client->NPC_class != CLASS_REBORN || self->s.weapon == WP_SABER)
|
|
&& (self->client->NPC_class != CLASS_ROCKETTROOPER||!self->NPC||self->NPC->rank<RANK_LT)//if a rockettrooper, but not an officer, do these normal checks
|
|
)
|
|
{
|
|
if ( g_debugMelee->integer
|
|
&& (ucmd->buttons & BUTTON_USE)
|
|
&& cg.renderingThirdPerson
|
|
&& G_OkayToLean( &self->client->ps, ucmd, qfalse )
|
|
&& (self->client->ps.forcePowersActive&(1<<FP_SPEED)) )
|
|
{
|
|
}
|
|
else
|
|
{
|
|
if ( self->client->ps.weapon != WP_SABER )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.saberInFlight )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( self->s.number < MAX_CLIENTS )
|
|
{
|
|
if ( !self->client->ps.SaberLength() )
|
|
{//player doesn't auto-activate
|
|
return;
|
|
}
|
|
|
|
if (cg_thirdPerson.integer) {
|
|
if (!self->s.number && !g_saberAutoBlocking->integer &&
|
|
self->client->ps.saberBlockingTime < level.time) {
|
|
return;
|
|
}
|
|
} else // first person
|
|
{
|
|
if (!self->s.number && !g_saberAutoDeflect1stPerson->integer &&
|
|
self->client->ps.saberBlockingTime < level.time) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( (self->client->ps.saber[0].saberFlags&SFL_NOT_ACTIVE_BLOCKING) )
|
|
{//can't actively block with this saber type
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ( !self->s.number )
|
|
{//don't do this if already attacking!
|
|
if ( ucmd->buttons & BUTTON_ATTACK
|
|
|| PM_SaberInAttack( self->client->ps.saberMove )
|
|
|| PM_SaberInSpecialAttack( self->client->ps.torsoAnim )
|
|
|| PM_SaberInTransitionAny( self->client->ps.saberMove ))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ( self->client->ps.forcePowerLevel[FP_SABER_DEFENSE] < FORCE_LEVEL_1 )
|
|
{//you have not the SKILLZ
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.forcePowerDebounce[FP_SABER_DEFENSE] > level.time )
|
|
{//can't block while already blocking
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.forcePowersActive&(1<<FP_LIGHTNING) )
|
|
{//can't block while zapping
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.forcePowersActive&(1<<FP_DRAIN) )
|
|
{//can't block while draining
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.forcePowersActive&(1<<FP_PUSH) )
|
|
{//can't block while shoving
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.forcePowersActive&(1<<FP_GRIP) )
|
|
{//can't block while gripping (FIXME: or should it break the grip? Pain should break the grip, I think...)
|
|
return;
|
|
}
|
|
}
|
|
|
|
fwdangles[1] = self->client->ps.viewangles[1];
|
|
AngleVectors( fwdangles, forward, NULL, NULL );
|
|
|
|
for ( i = 0 ; i < 3 ; i++ )
|
|
{
|
|
mins[i] = self->currentOrigin[i] - radius;
|
|
maxs[i] = self->currentOrigin[i] + radius;
|
|
}
|
|
|
|
numListedEntities = gi.EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
|
|
|
|
closestDist = radius;
|
|
|
|
for ( e = 0 ; e < numListedEntities ; e++ )
|
|
{
|
|
ent = entityList[ e ];
|
|
|
|
if (ent == self)
|
|
continue;
|
|
if (ent->owner == self)
|
|
continue;
|
|
if ( !(ent->inuse) )
|
|
continue;
|
|
if ( dodgeOnlySabers )
|
|
{//only care about thrown sabers
|
|
if ( ent->client
|
|
|| ent->s.weapon != WP_SABER
|
|
|| !ent->classname
|
|
|| !ent->classname[0]
|
|
|| Q_stricmp( "lightsaber", ent->classname ) )
|
|
{//not a lightsaber, ignore it
|
|
continue;
|
|
}
|
|
}
|
|
if ( ent->s.eType != ET_MISSILE && !(ent->s.eFlags&EF_MISSILE_STICK) )
|
|
{//not a normal projectile
|
|
if ( ent->client || ent->s.weapon != WP_SABER )
|
|
{//FIXME: wake up bad guys?
|
|
continue;
|
|
}
|
|
if ( ent->s.eFlags & EF_NODRAW )
|
|
{
|
|
continue;
|
|
}
|
|
if ( Q_stricmp( "lightsaber", ent->classname ) != 0 )
|
|
{//not a lightsaber
|
|
//FIXME: what about general objects that are small in size- like rocks, etc...
|
|
continue;
|
|
}
|
|
//a lightsaber.. make sure it's on and inFlight
|
|
if ( !ent->owner || !ent->owner->client )
|
|
{
|
|
continue;
|
|
}
|
|
if ( !ent->owner->client->ps.saberInFlight )
|
|
{//not in flight
|
|
continue;
|
|
}
|
|
if ( ent->owner->client->ps.SaberLength() <= 0 )
|
|
{//not on
|
|
continue;
|
|
}
|
|
if ( ent->owner->health <= 0 && g_saberRealisticCombat->integer < 2 )
|
|
{//it's not doing damage, so ignore it
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( ent->s.pos.trType == TR_STATIONARY && !self->s.number )
|
|
{//nothing you can do with a stationary missile if you're the player
|
|
continue;
|
|
}
|
|
}
|
|
|
|
float dot1, dot2;
|
|
//see if they're in front of me
|
|
VectorSubtract( ent->currentOrigin, self->currentOrigin, dir );
|
|
dist = VectorNormalize( dir );
|
|
//FIXME: handle detpacks, proximity mines and tripmines
|
|
if ( ent->s.weapon == WP_THERMAL )
|
|
{//thermal detonator!
|
|
if ( self->NPC && dist < ent->splashRadius )
|
|
{
|
|
if ( dist < ent->splashRadius &&
|
|
ent->nextthink < level.time + 600 &&
|
|
ent->count &&
|
|
self->client->ps.groundEntityNum != ENTITYNUM_NONE &&
|
|
(ent->s.pos.trType == TR_STATIONARY||
|
|
ent->s.pos.trType == TR_INTERPOLATE||
|
|
(dot1 = DotProduct( dir, forward )) < SABER_REFLECT_MISSILE_CONE||
|
|
!WP_ForcePowerUsable( self, FP_PUSH, 0 )) )
|
|
{//TD is close enough to hurt me, I'm on the ground and the thing is at rest or behind me and about to blow up, or I don't have force-push so force-jump!
|
|
//FIXME: sometimes this might make me just jump into it...?
|
|
self->client->ps.forceJumpCharge = 480;
|
|
}
|
|
else if ( self->client->NPC_class != CLASS_BOBAFETT
|
|
&& (self->client->NPC_class != CLASS_REBORN || self->s.weapon == WP_SABER)
|
|
&& self->client->NPC_class != CLASS_ROCKETTROOPER )
|
|
{//FIXME: check forcePushRadius[NPC->client->ps.forcePowerLevel[FP_PUSH]]
|
|
if ( !ent->owner || !OnSameTeam( self, ent->owner ) )
|
|
{
|
|
ForceThrow( self, qfalse );
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
else if ( ent->splashDamage && ent->splashRadius )
|
|
{//exploding missile
|
|
//FIXME: handle tripmines and detpacks somehow...
|
|
// maybe do a force-gesture that makes them explode?
|
|
// But what if we're within it's splashradius?
|
|
if ( !self->s.number )
|
|
{//players don't auto-handle these at all
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
if ( self->client->NPC_class == CLASS_BOBAFETT
|
|
|| self->client->NPC_class == CLASS_ROCKETTROOPER )
|
|
{
|
|
/*
|
|
if ( ent->s.pos.trType == TR_STATIONARY && (ent->s.eFlags&EF_MISSILE_STICK) )
|
|
{//sorry, you're scrooged here
|
|
//FIXME: maybe jump or go up if on ground?
|
|
continue;
|
|
}
|
|
//else it's a rocket, try to evade it
|
|
*/
|
|
//HMM... let's see what happens if these guys try to avoid tripmines and detpacks, too...?
|
|
}
|
|
else
|
|
{//normal Jedi
|
|
if ( ent->s.pos.trType == TR_STATIONARY && (ent->s.eFlags&EF_MISSILE_STICK)
|
|
&& (self->client->NPC_class != CLASS_REBORN || self->s.weapon == WP_SABER) )
|
|
{//a placed explosive like a tripmine or detpack
|
|
if ( InFOV( ent->currentOrigin, self->client->renderInfo.eyePoint, self->client->ps.viewangles, 90, 90 ) )
|
|
{//in front of me
|
|
if ( G_ClearLOS( self, ent ) )
|
|
{//can see it
|
|
vec3_t throwDir;
|
|
//make the gesture
|
|
ForceThrow( self, qfalse );
|
|
//take it off the wall and toss it
|
|
ent->s.pos.trType = TR_GRAVITY;
|
|
ent->s.eType = ET_MISSILE;
|
|
ent->s.eFlags &= ~EF_MISSILE_STICK;
|
|
ent->s.eFlags |= EF_BOUNCE_HALF;
|
|
AngleVectors( ent->currentAngles, throwDir, NULL, NULL );
|
|
VectorMA( ent->currentOrigin, ent->maxs[0]+4, throwDir, ent->currentOrigin );
|
|
VectorCopy( ent->currentOrigin, ent->s.pos.trBase );
|
|
VectorScale( throwDir, 300, ent->s.pos.trDelta );
|
|
ent->s.pos.trDelta[2] += 150;
|
|
VectorMA( ent->s.pos.trDelta, 800, dir, ent->s.pos.trDelta );
|
|
ent->s.pos.trTime = level.time; // move a bit on the very first frame
|
|
VectorCopy( ent->currentOrigin, ent->s.pos.trBase );
|
|
ent->owner = self;
|
|
// make it explode, but with less damage
|
|
ent->splashDamage /= 3;
|
|
ent->splashRadius /= 3;
|
|
ent->e_ThinkFunc = thinkF_WP_Explode;
|
|
ent->nextthink = level.time + Q_irand( 500, 3000 );
|
|
}
|
|
}
|
|
}
|
|
else if ( dist < ent->splashRadius
|
|
&& self->client->ps.groundEntityNum != ENTITYNUM_NONE
|
|
&& ( DotProduct( dir, forward ) < SABER_REFLECT_MISSILE_CONE
|
|
|| !WP_ForcePowerUsable( self, FP_PUSH, 0 ) ) )
|
|
{//NPCs try to evade it
|
|
self->client->ps.forceJumpCharge = 480;
|
|
}
|
|
else if ( (self->client->NPC_class != CLASS_REBORN || self->s.weapon == WP_SABER) )
|
|
{//else, try to force-throw it away
|
|
if ( !ent->owner || !OnSameTeam( self, ent->owner ) )
|
|
{
|
|
//FIXME: check forcePushRadius[NPC->client->ps.forcePowerLevel[FP_PUSH]]
|
|
ForceThrow( self, qfalse );
|
|
}
|
|
}
|
|
//otherwise, can't block it, so we're screwed
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( ent->s.weapon != WP_SABER )
|
|
{//only block shots coming from behind
|
|
if ( (dot1 = DotProduct( dir, forward )) < SABER_REFLECT_MISSILE_CONE )
|
|
continue;
|
|
}
|
|
else if ( !self->s.number )
|
|
{//player never auto-blocks thrown sabers
|
|
continue;
|
|
}//NPCs always try to block sabers coming from behind!
|
|
|
|
//see if they're heading towards me
|
|
VectorCopy( ent->s.pos.trDelta, missile_dir );
|
|
VectorNormalize( missile_dir );
|
|
if ( (dot2 = DotProduct( dir, missile_dir )) > 0 )
|
|
continue;
|
|
|
|
//FIXME: must have a clear trace to me, too...
|
|
if ( dist < closestDist )
|
|
{
|
|
VectorCopy( self->currentOrigin, traceTo );
|
|
traceTo[2] = self->absmax[2] - 4;
|
|
gi.trace( &trace, ent->currentOrigin, ent->mins, ent->maxs, traceTo, ent->s.number, ent->clipmask, (EG2_Collision)0, 0 );
|
|
if ( trace.allsolid || trace.startsolid || (trace.fraction < 1.0f && trace.entityNum != self->s.number && trace.entityNum != self->client->ps.saberEntityNum) )
|
|
{//okay, try one more check
|
|
VectorNormalize2( ent->s.pos.trDelta, entDir );
|
|
VectorMA( ent->currentOrigin, radius, entDir, traceTo );
|
|
gi.trace( &trace, ent->currentOrigin, ent->mins, ent->maxs, traceTo, ent->s.number, ent->clipmask, (EG2_Collision)0, 0 );
|
|
if ( trace.allsolid || trace.startsolid || (trace.fraction < 1.0f && trace.entityNum != self->s.number && trace.entityNum != self->client->ps.saberEntityNum) )
|
|
{//can't hit me, ignore it
|
|
continue;
|
|
}
|
|
}
|
|
if ( self->s.number != 0 )
|
|
{//An NPC
|
|
if ( self->NPC && !self->enemy && ent->owner )
|
|
{
|
|
if ( ent->owner->health >= 0 && (!ent->owner->client || ent->owner->client->playerTeam != self->client->playerTeam) )
|
|
{
|
|
G_SetEnemy( self, ent->owner );
|
|
}
|
|
}
|
|
}
|
|
//FIXME: if NPC, predict the intersection between my current velocity/path and the missile's, see if it intersects my bounding box (+/-saberLength?), don't try to deflect unless it does?
|
|
closestDist = dist;
|
|
incoming = ent;
|
|
}
|
|
}
|
|
|
|
if ( incoming )
|
|
{
|
|
if ( self->NPC && !G_ControlledByPlayer( self ) )
|
|
{
|
|
if ( Jedi_WaitingAmbush( self ) )
|
|
{
|
|
Jedi_Ambush( self );
|
|
}
|
|
if ( ( self->client->NPC_class == CLASS_BOBAFETT || self->client->NPC_class == CLASS_ROCKETTROOPER )
|
|
&& self->client->moveType == MT_FLYSWIM
|
|
&& incoming->methodOfDeath != MOD_ROCKET_ALT )
|
|
{//a hovering Boba Fett, not a tracking rocket
|
|
if ( !Q_irand( 0, 1 ) )
|
|
{//strafe
|
|
self->NPC->standTime = 0;
|
|
self->client->ps.forcePowerDebounce[FP_SABER_DEFENSE] = level.time + Q_irand( 1000, 2000 );
|
|
}
|
|
if ( !Q_irand( 0, 1 ) )
|
|
{//go up/down
|
|
TIMER_Set( self, "heightChange", Q_irand( 1000, 3000 ) );
|
|
self->client->ps.forcePowerDebounce[FP_SABER_DEFENSE] = level.time + Q_irand( 1000, 2000 );
|
|
}
|
|
}
|
|
else if ( self->client->NPC_class != CLASS_ROCKETTROOPER
|
|
&& Jedi_SaberBlockGo( self, &self->NPC->last_ucmd, NULL, NULL, incoming ) != EVASION_NONE )
|
|
{//make sure to turn on your saber if it's not on
|
|
if ( self->client->NPC_class != CLASS_BOBAFETT
|
|
&& (self->client->NPC_class != CLASS_REBORN || self->s.weapon == WP_SABER) )
|
|
{
|
|
self->client->ps.SaberActivate();
|
|
}
|
|
}
|
|
}
|
|
else//player
|
|
{
|
|
if ( !(ucmd->buttons & BUTTON_USE) )//self->s.weapon == WP_SABER && self->client->ps.SaberActive() )
|
|
{
|
|
WP_SaberBlockNonRandom( self, incoming->currentOrigin, qtrue );
|
|
}
|
|
else
|
|
{
|
|
vec3_t diff, start, end;
|
|
float dist;
|
|
VectorSubtract( incoming->currentOrigin, self->currentOrigin, diff );
|
|
dist = VectorLength( diff );
|
|
VectorNormalize2( incoming->s.pos.trDelta, entDir );
|
|
VectorMA( incoming->currentOrigin, dist, entDir, start );
|
|
VectorCopy( self->currentOrigin, end );
|
|
end[2] += self->maxs[2]*0.75f;
|
|
gi.trace( &trace, start, incoming->mins, incoming->maxs, end, incoming->s.number, MASK_SHOT, G2_COLLIDE, 10 );
|
|
|
|
Jedi_DodgeEvasion( self, incoming->owner, &trace, HL_NONE );
|
|
}
|
|
if ( incoming->owner && incoming->owner->client && (!self->enemy || self->enemy->s.weapon != WP_SABER) )//keep enemy jedi over shooters
|
|
{
|
|
self->enemy = incoming->owner;
|
|
NPC_SetLookTarget( self, incoming->owner->s.number, level.time+1000 );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//GENERAL SABER============================================================================
|
|
//GENERAL SABER============================================================================
|
|
//GENERAL SABER============================================================================
|
|
//GENERAL SABER============================================================================
|
|
//GENERAL SABER============================================================================
|
|
|
|
void WP_SetSaberMove(gentity_t *self, short blocked)
|
|
{
|
|
self->client->ps.saberBlocked = blocked;
|
|
}
|
|
|
|
extern void CG_CubeOutline( vec3_t mins, vec3_t maxs, int time, unsigned int color, float alpha );
|
|
void WP_SaberUpdate( gentity_t *self, usercmd_t *ucmd )
|
|
{
|
|
//float swap;
|
|
float minsize = 16;
|
|
|
|
if(0)// if ( self->s.number != 0 )
|
|
{//for now only the player can do this // not anymore
|
|
return;
|
|
}
|
|
|
|
if ( !self->client )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.saberEntityNum < 0 || self->client->ps.saberEntityNum >= ENTITYNUM_WORLD )
|
|
{//never got one
|
|
return;
|
|
}
|
|
|
|
// Check if we are throwing it, launch it if needed, update position if needed.
|
|
WP_SaberThrow(self, ucmd);
|
|
|
|
|
|
//vec3_t saberloc;
|
|
//vec3_t sabermins={-8,-8,-8}, sabermaxs={8,8,8};
|
|
|
|
gentity_t *saberent;
|
|
|
|
if (self->client->ps.saberEntityNum <= 0)
|
|
{//WTF?!! We lost it?
|
|
return;
|
|
}
|
|
|
|
saberent = &g_entities[self->client->ps.saberEntityNum];
|
|
|
|
//FIXME: Based on difficulty level/jedi saber combat skill, make this bounding box fatter/smaller
|
|
if ( self->client->ps.saberBlocked != BLOCKED_NONE )
|
|
{//we're blocking, increase min size
|
|
minsize = 32;
|
|
}
|
|
|
|
if ( G_InCinematicSaberAnim( self ) )
|
|
{//fake some blocking
|
|
self->client->ps.saberBlocking = BLK_TIGHT;
|
|
if ( self->client->ps.saber[0].Active() )
|
|
{
|
|
self->client->ps.saber[0].ActivateTrail( 150 );
|
|
}
|
|
if ( self->client->ps.saber[1].Active() )
|
|
{
|
|
self->client->ps.saber[1].ActivateTrail( 150 );
|
|
}
|
|
}
|
|
|
|
//is our saber in flight?
|
|
if ( !self->client->ps.saberInFlight )
|
|
{ // It isn't, which means we can update its position as we will.
|
|
qboolean alwaysBlock[MAX_SABERS][MAX_BLADES];
|
|
qboolean forceBlock = qfalse;
|
|
qboolean noBlocking = qfalse;
|
|
|
|
//clear out last frame's numbers
|
|
VectorClear(saberent->mins);
|
|
VectorClear(saberent->maxs);
|
|
|
|
Vehicle_t *pVeh = G_IsRidingVehicle( self );
|
|
if ( !self->client->ps.SaberActive()
|
|
|| !self->client->ps.saberBlocking
|
|
|| PM_InKnockDown( &self->client->ps )
|
|
|| PM_SuperBreakLoseAnim( self->client->ps.torsoAnim )
|
|
|| (pVeh && pVeh->m_pVehicleInfo && pVeh->m_pVehicleInfo->type != VH_ANIMAL && pVeh->m_pVehicleInfo->type != VH_FLIER) )//riding a vehicle that you cannot block shots on
|
|
{//can't block if saber isn't on
|
|
int i, j;
|
|
for ( i = 0; i < MAX_SABERS; i++ )
|
|
{
|
|
//initialize to not blocking
|
|
for ( j = 0; j < MAX_BLADES; j++ )
|
|
{
|
|
alwaysBlock[i][j] = qfalse;
|
|
}
|
|
if ( i > 0 && !self->client->ps.dualSabers )
|
|
{//not using a second saber, leave it not blocking
|
|
}
|
|
else
|
|
{
|
|
if ( (self->client->ps.saber[i].saberFlags2&SFL2_ALWAYS_BLOCK) )
|
|
{
|
|
for ( j = 0; j < self->client->ps.saber[i].numBlades; j++ )
|
|
{
|
|
alwaysBlock[i][j] = qtrue;
|
|
forceBlock = qtrue;
|
|
}
|
|
}
|
|
if ( self->client->ps.saber[i].bladeStyle2Start > 0 )
|
|
{
|
|
for ( j = self->client->ps.saber[i].bladeStyle2Start; j < self->client->ps.saber[i].numBlades; j++ )
|
|
{
|
|
if ( (self->client->ps.saber[i].saberFlags2&SFL2_ALWAYS_BLOCK2) )
|
|
{
|
|
alwaysBlock[i][j] = qtrue;
|
|
forceBlock = qtrue;
|
|
}
|
|
else
|
|
{
|
|
alwaysBlock[i][j] = qfalse;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ( !forceBlock )
|
|
{
|
|
noBlocking = qtrue;
|
|
}
|
|
else if ( !self->client->ps.saberBlocking )
|
|
{//turn blocking on!
|
|
self->client->ps.saberBlocking = BLK_TIGHT;
|
|
}
|
|
}
|
|
if ( noBlocking )
|
|
{
|
|
//VectorClear(saberent->mins);
|
|
//VectorClear(saberent->maxs);
|
|
G_SetOrigin(saberent, self->currentOrigin);
|
|
}
|
|
else if ( self->client->ps.saberBlocking == BLK_TIGHT
|
|
|| self->client->ps.saberBlocking == BLK_WIDE )
|
|
{//FIXME: keep bbox in front of player, even when wide?
|
|
bool autoBlocking = (cg_thirdPerson.integer && g_saberAutoBlocking->integer) ||
|
|
(!cg_thirdPerson.integer && g_saberAutoDeflect1stPerson->integer);
|
|
vec3_t saberOrg;
|
|
if ( !forceBlock
|
|
&& ( (self->s.number&&!Jedi_SaberBusy(self)&&!g_saberRealisticCombat->integer) ||
|
|
(self->s.number == 0 && self->client->ps.saberBlocking == BLK_WIDE && (autoBlocking||self->client->ps.saberBlockingTime>level.time)) )
|
|
&& self->client->ps.weaponTime <= 0
|
|
&& !G_InCinematicSaberAnim( self ) )
|
|
{//full-size blocking for non-attacking player with g_saberAutoBlocking on
|
|
vec3_t saberang={0,0,0}, fwd, sabermins={-8,-8,-8}, sabermaxs={8,8,8};
|
|
|
|
saberang[YAW] = self->client->ps.viewangles[YAW];
|
|
AngleVectors( saberang, fwd, NULL, NULL );
|
|
|
|
VectorMA( self->currentOrigin, 12, fwd, saberOrg );
|
|
|
|
VectorAdd( self->mins, sabermins, saberent->mins );
|
|
VectorAdd( self->maxs, sabermaxs, saberent->maxs );
|
|
|
|
saberent->contents = CONTENTS_LIGHTSABER;
|
|
|
|
G_SetOrigin( saberent, saberOrg );
|
|
}
|
|
else
|
|
{
|
|
vec3_t saberBase, saberTip;
|
|
int numSabers = 1;
|
|
if ( self->client->ps.dualSabers )
|
|
{
|
|
numSabers = 2;
|
|
}
|
|
for ( int saberNum = 0; saberNum < numSabers; saberNum++ )
|
|
{
|
|
for ( int bladeNum = 0; bladeNum < self->client->ps.saber[saberNum].numBlades; bladeNum++ )
|
|
{
|
|
if ( self->client->ps.saber[saberNum].blade[bladeNum].length <= 0.0f )
|
|
{//don't include blades that are not on...
|
|
continue;
|
|
}
|
|
if ( forceBlock )
|
|
{//doing blade-specific bbox-sizing only, see if this blade should be counted
|
|
if ( !alwaysBlock[saberNum][bladeNum] )
|
|
{//this blade doesn't count right now
|
|
continue;
|
|
}
|
|
}
|
|
VectorCopy( self->client->ps.saber[saberNum].blade[bladeNum].muzzlePoint, saberBase );
|
|
VectorMA( saberBase, self->client->ps.saber[saberNum].blade[bladeNum].length, self->client->ps.saber[saberNum].blade[bladeNum].muzzleDir, saberTip );
|
|
VectorMA( saberBase, self->client->ps.saber[saberNum].blade[bladeNum].length*0.5, self->client->ps.saber[saberNum].blade[bladeNum].muzzleDir, saberOrg );
|
|
for ( int i = 0; i < 3; i++ )
|
|
{
|
|
/*
|
|
if ( saberTip[i] > self->client->ps.saber[saberNum].blade[bladeNum].muzzlePoint[i] )
|
|
{
|
|
saberent->maxs[i] = saberTip[i] - saberOrg[i] + 8;//self->client->renderInfo.muzzlePoint[i];
|
|
saberent->mins[i] = self->client->ps.saber[saberNum].blade[bladeNum].muzzlePoint[i] - saberOrg[i] - 8;
|
|
}
|
|
else //if ( saberTip[i] < self->client->renderInfo.muzzlePoint[i] )
|
|
{
|
|
saberent->maxs[i] = self->client->ps.saber[saberNum].blade[bladeNum].muzzlePoint[i] - saberOrg[i] + 8;
|
|
saberent->mins[i] = saberTip[i] - saberOrg[i] - 8;//self->client->ps.saber[saberNum].blade[bladeNum].muzzlePoint[i];
|
|
}
|
|
*/
|
|
float newSizeTip = (saberTip[i] - saberOrg[i]);
|
|
newSizeTip += (newSizeTip>=0)?8:-8;
|
|
float newSizeBase = (saberBase[i] - saberOrg[i]);
|
|
newSizeBase += (newSizeBase>=0)?8:-8;
|
|
if ( newSizeTip > saberent->maxs[i] )
|
|
{
|
|
saberent->maxs[i] = newSizeTip;
|
|
}
|
|
if ( newSizeBase > saberent->maxs[i] )
|
|
{
|
|
saberent->maxs[i] = newSizeBase;
|
|
}
|
|
if ( newSizeTip < saberent->mins[i] )
|
|
{
|
|
saberent->mins[i] = newSizeTip;
|
|
}
|
|
if ( newSizeBase < saberent->mins[i] )
|
|
{
|
|
saberent->mins[i] = newSizeBase;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ( !forceBlock )
|
|
{//not doing special "alwaysBlock" bbox
|
|
if ( self->client->ps.weaponTime > 0
|
|
|| self->s.number
|
|
|| g_saberAutoBlocking->integer
|
|
|| self->client->ps.saberBlockingTime > level.time )
|
|
{//if attacking or blocking (or an NPC), inflate to a minimum size
|
|
for ( int i = 0; i < 3; i++ )
|
|
{
|
|
if ( saberent->maxs[i] < minsize )
|
|
{
|
|
saberent->maxs[i] = minsize;
|
|
}
|
|
if ( saberent->mins[i] > -minsize )
|
|
{
|
|
saberent->mins[i] = -minsize;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
saberent->contents = CONTENTS_LIGHTSABER;
|
|
if (self->client->ps.clientNum == 0 &&
|
|
self->client->ps.saber[0].numBlades > 1)
|
|
{
|
|
vec3_t angles;
|
|
BG_CalculateVRSaberPosition(0, saberOrg, angles);
|
|
G_SetOrigin(saberent, saberOrg);
|
|
}
|
|
else
|
|
{
|
|
G_SetOrigin(saberent, saberOrg);
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
else if (self->client->ps.saberBlocking == BLK_WIDE)
|
|
{ // Assuming that we are not swinging, the saber's bounding box should be around the player.
|
|
vec3_t saberang={0,0,0}, fwd;
|
|
|
|
saberang[YAW] = self->client->ps.viewangles[YAW];
|
|
AngleVectors( saberang, fwd, NULL, NULL );
|
|
|
|
VectorMA(self->currentOrigin, 12, fwd, saberloc);
|
|
|
|
VectorAdd(self->mins, sabermins, saberent->mins);
|
|
VectorAdd(self->maxs, sabermaxs, saberent->maxs);
|
|
|
|
saberent->contents = CONTENTS_LIGHTSABER;
|
|
|
|
G_SetOrigin( saberent, saberloc);
|
|
}
|
|
else if (self->client->ps.saberBlocking == BLK_TIGHT)
|
|
{ // If the player is swinging, the bbox is around just the saber
|
|
VectorCopy(self->client->renderInfo.muzzlePoint, sabermins);
|
|
// Put the limits of the bbox around the saber size.
|
|
VectorMA(sabermins, self->client->ps.saberLength, self->client->renderInfo.muzzleDir, sabermaxs);
|
|
|
|
// Now make the mins into mins and the maxs into maxs
|
|
if (sabermins[0] > sabermaxs[0])
|
|
{
|
|
swap = sabermins[0];
|
|
sabermins[0] = sabermaxs[0];
|
|
sabermaxs[0] = swap;
|
|
}
|
|
if (sabermins[1] > sabermaxs[1])
|
|
{
|
|
swap = sabermins[1];
|
|
sabermins[1] = sabermaxs[1];
|
|
sabermaxs[1] = swap;
|
|
}
|
|
if (sabermins[2] > sabermaxs[2])
|
|
{
|
|
swap = sabermins[2];
|
|
sabermins[2] = sabermaxs[2];
|
|
sabermaxs[2] = swap;
|
|
}
|
|
|
|
// Now the loc is halfway between the (absolute) mins and maxs
|
|
VectorAdd(sabermins, sabermaxs, saberloc);
|
|
VectorScale(saberloc, 0.5, saberloc);
|
|
|
|
// Finally, turn the mins and maxs, which are absolute, into relative mins and maxs.
|
|
VectorSubtract(sabermins, saberloc, saberent->mins);
|
|
VectorSubtract(sabermaxs, saberloc, saberent->maxs);
|
|
|
|
saberent->contents = CONTENTS_LIGHTSABER;// | CONTENTS_SHOTCLIP;
|
|
|
|
G_SetOrigin( saberent, saberloc);
|
|
}
|
|
*/
|
|
else
|
|
{ // Otherwise there is no blocking possible.
|
|
//VectorClear(saberent->mins);
|
|
//VectorClear(saberent->maxs);
|
|
G_SetOrigin(saberent, self->currentOrigin);
|
|
}
|
|
saberent->clipmask = MASK_SHOT | CONTENTS_LIGHTSABER;
|
|
gi.linkentity(saberent);
|
|
}
|
|
else
|
|
{
|
|
WP_SaberInFlightReflectCheck( self, ucmd );
|
|
}
|
|
#ifndef FINAL_BUILD
|
|
if ( d_saberCombat->integer > 2 )
|
|
{
|
|
CG_CubeOutline( saberent->absmin, saberent->absmax, 50, WPDEBUG_SaberColor( self->client->ps.saber[0].blade[0].color ), 1 );
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#define MAX_RADIUS_ENTS 256 //NOTE: This can cause entities to be lost
|
|
qboolean G_CheckEnemyPresence( gentity_t *ent, int dir, float radius, float tolerance )
|
|
{
|
|
gentity_t *radiusEnts[ MAX_RADIUS_ENTS ];
|
|
vec3_t mins, maxs;
|
|
int numEnts;
|
|
vec3_t checkDir, dir2checkEnt;
|
|
float dist;
|
|
int i;
|
|
|
|
switch( dir )
|
|
{
|
|
case DIR_RIGHT:
|
|
AngleVectors( ent->currentAngles, NULL, checkDir, NULL );
|
|
break;
|
|
case DIR_LEFT:
|
|
AngleVectors( ent->currentAngles, NULL, checkDir, NULL );
|
|
VectorScale( checkDir, -1, checkDir );
|
|
break;
|
|
case DIR_FRONT:
|
|
AngleVectors( ent->currentAngles, checkDir, NULL, NULL );
|
|
break;
|
|
case DIR_BACK:
|
|
AngleVectors( ent->currentAngles, checkDir, NULL, NULL );
|
|
VectorScale( checkDir, -1, checkDir );
|
|
break;
|
|
}
|
|
//Get all ents in range, see if they're living clients and enemies, then check dot to them...
|
|
|
|
//Setup the bbox to search in
|
|
for ( i = 0; i < 3; i++ )
|
|
{
|
|
mins[i] = ent->currentOrigin[i] - radius;
|
|
maxs[i] = ent->currentOrigin[i] + radius;
|
|
}
|
|
|
|
//Get a number of entities in a given space
|
|
numEnts = gi.EntitiesInBox( mins, maxs, radiusEnts, MAX_RADIUS_ENTS );
|
|
|
|
for ( i = 0; i < numEnts; i++ )
|
|
{
|
|
//Don't consider self
|
|
if ( radiusEnts[i] == ent )
|
|
continue;
|
|
|
|
//Must be valid
|
|
if ( G_ValidEnemy( ent, radiusEnts[i] ) == qfalse )
|
|
continue;
|
|
|
|
VectorSubtract( radiusEnts[i]->currentOrigin, ent->currentOrigin, dir2checkEnt );
|
|
dist = VectorNormalize( dir2checkEnt );
|
|
if ( dist <= radius
|
|
&& DotProduct( dir2checkEnt, checkDir ) >= tolerance )
|
|
{
|
|
//stop on the first one
|
|
return qtrue;
|
|
}
|
|
}
|
|
|
|
return qfalse;
|
|
}
|
|
//OTHER JEDI POWERS=========================================================================
|
|
//OTHER JEDI POWERS=========================================================================
|
|
//OTHER JEDI POWERS=========================================================================
|
|
//OTHER JEDI POWERS=========================================================================
|
|
//OTHER JEDI POWERS=========================================================================
|
|
extern gentity_t *TossClientItems( gentity_t *self );
|
|
extern void ChangeWeapon( gentity_t *ent, int newWeapon );
|
|
void WP_DropWeapon( gentity_t *dropper, vec3_t velocity )
|
|
{
|
|
if ( !dropper || !dropper->client )
|
|
{
|
|
return;
|
|
}
|
|
int replaceWeap = WP_NONE;
|
|
int oldWeap = dropper->s.weapon;
|
|
gentity_t *weapon = TossClientItems( dropper );
|
|
if ( oldWeap == WP_THERMAL && dropper->NPC )
|
|
{//Hmm, maybe all NPCs should go into melee? Not too many, though, or they mob you and look silly
|
|
replaceWeap = WP_MELEE;
|
|
}
|
|
if ( dropper->ghoul2.IsValid() )
|
|
{
|
|
if ( dropper->weaponModel[0] > 0 )
|
|
{//NOTE: guess you never drop the left-hand weapon, eh?
|
|
gi.G2API_RemoveGhoul2Model( dropper->ghoul2, dropper->weaponModel[0] );
|
|
dropper->weaponModel[0] = -1;
|
|
}
|
|
}
|
|
//FIXME: does this work on the player?
|
|
dropper->client->ps.stats[STAT_WEAPONS] |= ( 1 << replaceWeap );
|
|
if ( !dropper->s.number )
|
|
{
|
|
if ( oldWeap == WP_THERMAL )
|
|
{
|
|
dropper->client->ps.ammo[weaponData[oldWeap].ammoIndex] -= weaponData[oldWeap].energyPerShot;
|
|
}
|
|
else
|
|
{
|
|
dropper->client->ps.stats[STAT_WEAPONS] &= ~( 1 << oldWeap );
|
|
}
|
|
CG_ChangeWeapon( replaceWeap );
|
|
}
|
|
else
|
|
{
|
|
dropper->client->ps.stats[STAT_WEAPONS] &= ~( 1 << oldWeap );
|
|
}
|
|
ChangeWeapon( dropper, replaceWeap );
|
|
dropper->s.weapon = replaceWeap;
|
|
if ( dropper->NPC )
|
|
{
|
|
dropper->NPC->last_ucmd.weapon = replaceWeap;
|
|
}
|
|
if ( weapon != NULL && velocity && !VectorCompare( velocity, vec3_origin ) )
|
|
{//weapon should have a direction to it's throw
|
|
VectorScale( velocity, 3, weapon->s.pos.trDelta );//NOTE: Presumes it is moving already...?
|
|
if ( weapon->s.pos.trDelta[2] < 150 )
|
|
{//this is presuming you don't want them to drop the weapon down on you...
|
|
weapon->s.pos.trDelta[2] = 150;
|
|
}
|
|
//FIXME: gets stuck inside it's former owner...
|
|
weapon->forcePushTime = level.time + 600; // let the push effect last for 600 ms
|
|
}
|
|
}
|
|
|
|
void WP_KnockdownTurret( gentity_t *self, gentity_t *pas )
|
|
{
|
|
//knock it over
|
|
VectorCopy( pas->currentOrigin, pas->s.pos.trBase );
|
|
pas->s.pos.trType = TR_LINEAR_STOP;
|
|
pas->s.pos.trDuration = 250;
|
|
pas->s.pos.trTime = level.time;
|
|
pas->s.pos.trDelta[2] = ( 12.0f / ( pas->s.pos.trDuration * 0.001f ) );
|
|
|
|
VectorCopy( pas->currentAngles, pas->s.apos.trBase );
|
|
pas->s.apos.trType = TR_LINEAR_STOP;
|
|
pas->s.apos.trDuration = 250;
|
|
pas->s.apos.trTime = level.time;
|
|
//FIXME: pick pitch/roll that always tilts it directly away from pusher
|
|
pas->s.apos.trDelta[PITCH] = ( 100.0f / ( pas->s.apos.trDuration * 0.001f ) );
|
|
|
|
//kill it
|
|
pas->count = 0;
|
|
pas->nextthink = -1;
|
|
G_Sound( pas, G_SoundIndex( "sound/chars/turret/shutdown.wav" ));
|
|
//push effect?
|
|
pas->forcePushTime = level.time + 600; // let the push effect last for 600 ms
|
|
}
|
|
|
|
void WP_ForceThrowHazardTrooper( gentity_t *self, gentity_t *trooper, qboolean pull )
|
|
{
|
|
if ( !self || !self->client )
|
|
{
|
|
return;
|
|
}
|
|
if ( !trooper || !trooper->client )
|
|
{
|
|
return;
|
|
}
|
|
|
|
//all levels: see effect on them, they notice us
|
|
trooper->forcePushTime = level.time + 600; // let the push effect last for 600 ms
|
|
|
|
if ( (pull&&self->client->ps.forcePowerLevel[FP_PULL]>FORCE_LEVEL_1)
|
|
|| (!pull&&self->client->ps.forcePowerLevel[FP_PUSH]>FORCE_LEVEL_1) )
|
|
{//level 2: they stop for a couple seconds and make a sound
|
|
trooper->painDebounceTime = level.time + Q_irand( 1500, 2500 );
|
|
G_AddVoiceEvent( trooper, Q_irand(EV_PUSHED1, EV_PUSHED3), Q_irand( 1000, 3000 ) );
|
|
GEntity_PainFunc( trooper, self, self, trooper->currentOrigin, 0, MOD_MELEE );
|
|
|
|
if ( (pull&&self->client->ps.forcePowerLevel[FP_PULL]>FORCE_LEVEL_2)
|
|
|| (!pull&&self->client->ps.forcePowerLevel[FP_PUSH]>FORCE_LEVEL_2) )
|
|
{//level 3: they actually play a pushed anim and stumble a bit
|
|
vec3_t hazAngles = {0,trooper->currentAngles[YAW],0};
|
|
int anim = -1;
|
|
if ( InFront( self->currentOrigin, trooper->currentOrigin, hazAngles ) )
|
|
{//I'm on front of him
|
|
if ( pull )
|
|
{
|
|
anim = BOTH_PAIN4;
|
|
}
|
|
else
|
|
{
|
|
anim = BOTH_PAIN1;
|
|
}
|
|
}
|
|
else
|
|
{//I'm behind him
|
|
if ( pull )
|
|
{
|
|
anim = BOTH_PAIN1;
|
|
}
|
|
else
|
|
{
|
|
anim = BOTH_PAIN4;
|
|
}
|
|
}
|
|
if ( anim != -1 )
|
|
{
|
|
if ( anim == BOTH_PAIN1 )
|
|
{//make them take a couple steps back
|
|
AngleVectors( hazAngles, trooper->client->ps.velocity, NULL, NULL );
|
|
VectorScale( trooper->client->ps.velocity, -40.0f, trooper->client->ps.velocity );
|
|
trooper->client->ps.pm_flags |= PMF_TIME_NOFRICTION;
|
|
}
|
|
else if ( anim == BOTH_PAIN4 )
|
|
{//make them stumble forward
|
|
AngleVectors( hazAngles, trooper->client->ps.velocity, NULL, NULL );
|
|
VectorScale( trooper->client->ps.velocity, 80.0f, trooper->client->ps.velocity );
|
|
trooper->client->ps.pm_flags |= PMF_TIME_NOFRICTION;
|
|
}
|
|
NPC_SetAnim( trooper, SETANIM_BOTH, anim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
trooper->painDebounceTime += trooper->client->ps.torsoAnimTimer;
|
|
trooper->client->ps.pm_time = trooper->client->ps.torsoAnimTimer;
|
|
}
|
|
}
|
|
if ( trooper->NPC )
|
|
{
|
|
if ( trooper->NPC->shotTime < trooper->painDebounceTime )
|
|
{
|
|
trooper->NPC->shotTime = trooper->painDebounceTime;
|
|
}
|
|
}
|
|
trooper->client->ps.weaponTime = trooper->painDebounceTime-level.time;
|
|
}
|
|
else
|
|
{//level 1: no pain reaction, but they should still notice
|
|
if ( trooper->enemy == NULL//not mad at anyone
|
|
&& trooper->client->playerTeam != self->client->playerTeam//not on our team
|
|
&& !(trooper->svFlags&SVF_LOCKEDENEMY)//not locked on an enemy
|
|
&& !(trooper->svFlags&SVF_IGNORE_ENEMIES)//not ignoring enemie
|
|
&& !(self->flags&FL_NOTARGET) )//I'm not in notarget
|
|
{//not already mad at them and can get mad at them, do so
|
|
G_SetEnemy( trooper, self );
|
|
}
|
|
}
|
|
}
|
|
|
|
void WP_ResistForcePush( gentity_t *self, gentity_t *pusher, qboolean noPenalty )
|
|
{
|
|
int parts;
|
|
qboolean runningResist = qfalse;
|
|
|
|
if ( !self || self->health <= 0 || !self->client || !pusher || !pusher->client )
|
|
{
|
|
return;
|
|
}
|
|
|
|
//NOTE: don't interrupt big anims with this!
|
|
if ( !PM_SaberCanInterruptMove( self->client->ps.saberMove, self->client->ps.torsoAnim ) )
|
|
{//can't interrupt my current torso anim/sabermove with this, so ignore it entirely!
|
|
return;
|
|
}
|
|
|
|
if ( (!self->s.number
|
|
||( self->NPC && (self->NPC->aiFlags&NPCAI_BOSS_CHARACTER) )
|
|
||( self->client && self->client->NPC_class == CLASS_SHADOWTROOPER )
|
|
/*
|
|
|| self->client->NPC_class == CLASS_DESANN
|
|
|| !Q_stricmp("Yoda",self->NPC_type)
|
|
|| self->client->NPC_class == CLASS_LUKE*/
|
|
)
|
|
&& (VectorLengthSquared( self->client->ps.velocity ) > 10000 || self->client->ps.forcePowerLevel[FP_PUSH] >= FORCE_LEVEL_3 || self->client->ps.forcePowerLevel[FP_PULL] >= FORCE_LEVEL_3 ) )
|
|
{
|
|
runningResist = qtrue;
|
|
}
|
|
|
|
if ( !runningResist
|
|
&& self->client->ps.groundEntityNum != ENTITYNUM_NONE
|
|
&& !PM_SpinningSaberAnim( self->client->ps.legsAnim )
|
|
&& !PM_FlippingAnim( self->client->ps.legsAnim )
|
|
&& !PM_RollingAnim( self->client->ps.legsAnim )
|
|
&& !PM_InKnockDown( &self->client->ps )
|
|
&& !PM_CrouchAnim( self->client->ps.legsAnim ))
|
|
{//if on a surface and not in a spin or flip, play full body resist
|
|
parts = SETANIM_BOTH;
|
|
}
|
|
else
|
|
{//play resist just in torso
|
|
parts = SETANIM_TORSO;
|
|
}
|
|
NPC_SetAnim( self, parts, BOTH_RESISTPUSH, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
|
|
if ( !noPenalty )
|
|
{
|
|
if ( !runningResist )
|
|
{
|
|
VectorClear( self->client->ps.velocity );
|
|
//still stop them from attacking or moving for a bit, though
|
|
//FIXME: maybe push just a little (like, slide)?
|
|
self->client->ps.weaponTime = 1000;
|
|
if ( self->client->ps.forcePowersActive&(1<<FP_SPEED) )
|
|
{
|
|
self->client->ps.weaponTime = floor( self->client->ps.weaponTime * g_timescale->value );
|
|
}
|
|
self->client->ps.pm_time = self->client->ps.weaponTime;
|
|
self->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
|
|
//play the full body push effect on me
|
|
self->forcePushTime = level.time + 600; // let the push effect last for 600 ms
|
|
}
|
|
else
|
|
{
|
|
self->client->ps.weaponTime = 600;
|
|
if ( self->client->ps.forcePowersActive&(1<<FP_SPEED) )
|
|
{
|
|
self->client->ps.weaponTime = floor( self->client->ps.weaponTime * g_timescale->value );
|
|
}
|
|
}
|
|
}
|
|
//play my force push effect on my hand
|
|
//self->client->ps.powerups[PW_FORCE_PUSH] = level.time + self->client->ps.torsoAnimTimer + 500;
|
|
|
|
//reset to 0 in case it's still > 0 from a previous push
|
|
//self->client->pushEffectFadeTime = 0;
|
|
if ( !pusher //???
|
|
|| pusher == self->enemy//my enemy tried to push me
|
|
|| (pusher->client && pusher->client->playerTeam != self->client->playerTeam) )//someone not on my team tried to push me
|
|
{
|
|
Jedi_PlayBlockedPushSound( self );
|
|
}
|
|
}
|
|
|
|
extern qboolean Boba_StopKnockdown( gentity_t *self, gentity_t *pusher, const vec3_t pushDir, qboolean forceKnockdown );
|
|
extern qboolean Jedi_StopKnockdown( gentity_t *self, gentity_t *pusher, const vec3_t pushDir );
|
|
void WP_ForceKnockdown( gentity_t *self, gentity_t *pusher, qboolean pull, qboolean strongKnockdown, qboolean breakSaberLock )
|
|
{
|
|
if ( !self || !self->client || !pusher || !pusher->client )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( self->client->NPC_class == CLASS_ROCKETTROOPER )
|
|
{
|
|
return;
|
|
}
|
|
else if ( PM_LockedAnim( self->client->ps.legsAnim ) )
|
|
{//stuck doing something else
|
|
return;
|
|
}
|
|
else if ( Rosh_BeingHealed( self ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
//break out of a saberLock?
|
|
if ( self->client->ps.saberLockTime > level.time )
|
|
{
|
|
if ( breakSaberLock
|
|
|| (pusher && self->client->ps.saberLockEnemy == pusher->s.number) )
|
|
{
|
|
self->client->ps.saberLockTime = 0;
|
|
self->client->ps.saberLockEnemy = ENTITYNUM_NONE;
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ( self->health > 0 )
|
|
{
|
|
if ( !self->s.number )
|
|
{
|
|
NPC_SetPainEvent( self );
|
|
}
|
|
else
|
|
{
|
|
GEntity_PainFunc( self, pusher, pusher, self->currentOrigin, 0, MOD_MELEE );
|
|
}
|
|
|
|
vec3_t pushDir;
|
|
if ( pull )
|
|
{
|
|
VectorSubtract( pusher->currentOrigin, self->currentOrigin, pushDir );
|
|
}
|
|
else
|
|
{
|
|
VectorSubtract( self->currentOrigin, pusher->currentOrigin, pushDir );
|
|
}
|
|
|
|
//FIXME: sometimes do this for some NPC force-users, too!
|
|
if ( Boba_StopKnockdown( self, pusher, pushDir, qtrue ) )
|
|
{//He can backflip instead of be knocked down
|
|
return;
|
|
}
|
|
else if ( Jedi_StopKnockdown( self, pusher, pushDir ) )
|
|
{//They can backflip instead of be knocked down
|
|
return;
|
|
}
|
|
|
|
G_CheckLedgeDive( self, 72, pushDir, qfalse, qfalse );
|
|
|
|
if ( !PM_SpinningSaberAnim( self->client->ps.legsAnim )
|
|
&& !PM_FlippingAnim( self->client->ps.legsAnim )
|
|
&& !PM_RollingAnim( self->client->ps.legsAnim )
|
|
&& !PM_InKnockDown( &self->client->ps ) )
|
|
{
|
|
int knockAnim = BOTH_KNOCKDOWN1;//default knockdown
|
|
if ( pusher->client->NPC_class == CLASS_DESANN && self->client->NPC_class != CLASS_LUKE )
|
|
{//desann always knocks down, unless you're Luke
|
|
strongKnockdown = qtrue;
|
|
}
|
|
if ( !self->s.number
|
|
&& !strongKnockdown
|
|
&& ( (!pull&&(self->client->ps.forcePowerLevel[FP_PUSH]>FORCE_LEVEL_1||!g_spskill->integer)) || (pull&&(self->client->ps.forcePowerLevel[FP_PULL]>FORCE_LEVEL_1||!g_spskill->integer)) ) )
|
|
{//player only knocked down if pushed *hard*
|
|
if ( self->s.weapon == WP_SABER )
|
|
{//temp HACK: these are the only 2 pain anims that look good when holding a saber
|
|
knockAnim = PM_PickAnim( self, BOTH_PAIN2, BOTH_PAIN3 );
|
|
}
|
|
else
|
|
{
|
|
knockAnim = PM_PickAnim( self, BOTH_PAIN1, BOTH_PAIN18 );
|
|
}
|
|
}
|
|
else if ( PM_CrouchAnim( self->client->ps.legsAnim ) )
|
|
{//crouched knockdown
|
|
knockAnim = BOTH_KNOCKDOWN4;
|
|
}
|
|
else
|
|
{//plain old knockdown
|
|
vec3_t pLFwd, pLAngles = {0,self->client->ps.viewangles[YAW],0};
|
|
vec3_t sFwd, sAngles = {0,pusher->client->ps.viewangles[YAW],0};
|
|
AngleVectors( pLAngles, pLFwd, NULL, NULL );
|
|
AngleVectors( sAngles, sFwd, NULL, NULL );
|
|
if ( DotProduct( sFwd, pLFwd ) > 0.2f )
|
|
{//pushing him from behind
|
|
//FIXME: check to see if we're aiming below or above the waist?
|
|
if ( pull )
|
|
{
|
|
knockAnim = BOTH_KNOCKDOWN1;
|
|
}
|
|
else
|
|
{
|
|
knockAnim = BOTH_KNOCKDOWN3;
|
|
}
|
|
}
|
|
else
|
|
{//pushing him from front
|
|
if ( pull )
|
|
{
|
|
knockAnim = BOTH_KNOCKDOWN3;
|
|
}
|
|
else
|
|
{
|
|
knockAnim = BOTH_KNOCKDOWN1;
|
|
}
|
|
}
|
|
}
|
|
if ( knockAnim == BOTH_KNOCKDOWN1 && strongKnockdown )
|
|
{//push *hard*
|
|
knockAnim = BOTH_KNOCKDOWN2;
|
|
}
|
|
NPC_SetAnim( self, SETANIM_BOTH, knockAnim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
if ( self->s.number >= MAX_CLIENTS )
|
|
{//randomize getup times - but not for boba
|
|
int addTime;
|
|
if ( self->client->NPC_class == CLASS_BOBAFETT )
|
|
{
|
|
addTime = Q_irand( -500, 0 );
|
|
}
|
|
else
|
|
{
|
|
addTime = Q_irand( -300, 300 );
|
|
}
|
|
self->client->ps.legsAnimTimer += addTime;
|
|
self->client->ps.torsoAnimTimer += addTime;
|
|
}
|
|
else
|
|
{//player holds extra long so you have more time to decide to do the quick getup
|
|
if ( PM_KnockDownAnim( self->client->ps.legsAnim ) )
|
|
{
|
|
self->client->ps.legsAnimTimer += PLAYER_KNOCKDOWN_HOLD_EXTRA_TIME;
|
|
self->client->ps.torsoAnimTimer += PLAYER_KNOCKDOWN_HOLD_EXTRA_TIME;
|
|
}
|
|
}
|
|
//
|
|
if ( pusher->NPC && pusher->enemy == self )
|
|
{//pushed pushed down his enemy
|
|
G_AddVoiceEvent( pusher, Q_irand( EV_GLOAT1, EV_GLOAT3 ), 3000 );
|
|
pusher->NPC->blockedSpeechDebounceTime = level.time + 3000;
|
|
}
|
|
}
|
|
}
|
|
self->forcePushTime = level.time + 600; // let the push effect last for 600 ms
|
|
}
|
|
|
|
qboolean WP_ForceThrowable( gentity_t *ent, gentity_t *forwardEnt, gentity_t *self, qboolean pull, float cone, float radius, vec3_t forward )
|
|
{
|
|
if (ent == self)
|
|
return qfalse;
|
|
if ( ent->owner == self && ent->s.weapon != WP_THERMAL )//can push your own thermals
|
|
return qfalse;
|
|
if ( !(ent->inuse) )
|
|
return qfalse;
|
|
if ( ent->NPC && ent->NPC->scriptFlags & SCF_NO_FORCE )
|
|
{
|
|
if ( ent->s.weapon == WP_SABER )
|
|
{//Hmm, should jedi do the resist behavior? If this is on, perhaps it's because of a cinematic?
|
|
WP_ResistForcePush( ent, self, qtrue );
|
|
}
|
|
return qfalse;
|
|
}
|
|
if ( (ent->flags&FL_FORCE_PULLABLE_ONLY) && !pull )
|
|
{//simple HACK: cannot force-push ammo rack items (because they may start in solid)
|
|
return qfalse;
|
|
}
|
|
//FIXME: don't push it if I already pushed it a little while ago
|
|
if ( ent->s.eType != ET_MISSILE )
|
|
{
|
|
if ( ent->client )
|
|
{
|
|
if ( ent->client->ps.pullAttackTime > level.time )
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
if ( cone >= 1.0f )
|
|
{//must be pointing right at them
|
|
if ( ent != forwardEnt )
|
|
{//must be the person I'm looking right at
|
|
if ( ent->client && !pull
|
|
&& ent->client->ps.forceGripEntityNum == self->s.number
|
|
&& (self->s.eFlags&EF_FORCE_GRIPPED) )
|
|
{//this is the guy that's force-gripping me, use a wider cone regardless of force power level
|
|
}
|
|
else
|
|
{
|
|
if ( ent->client && !pull
|
|
&& ent->client->ps.forceDrainEntityNum == self->s.number
|
|
&& (self->s.eFlags&EF_FORCE_DRAINED) )
|
|
{//this is the guy that's force-draining me, use a wider cone regardless of force power level
|
|
}
|
|
else
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ( ent->s.eType != ET_ITEM && ent->e_ThinkFunc != thinkF_G_RunObject )//|| !(ent->flags&FL_DROPPED_ITEM) )//was only dropped items
|
|
{
|
|
//FIXME: need pushable objects
|
|
if ( ent->s.eFlags & EF_NODRAW )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( !ent->client )
|
|
{
|
|
if ( Q_stricmp( "lightsaber", ent->classname ) != 0 )
|
|
{//not a lightsaber
|
|
if ( !(ent->svFlags&SVF_GLASS_BRUSH) )
|
|
{//and not glass
|
|
if ( Q_stricmp( "func_door", ent->classname ) != 0 || !(ent->spawnflags & 2/*MOVER_FORCE_ACTIVATE*/) )
|
|
{//not a force-usable door
|
|
if ( Q_stricmp( "func_static", ent->classname ) != 0 || (!(ent->spawnflags&1/*F_PUSH*/)&&!(ent->spawnflags&2/*F_PULL*/)) || (ent->spawnflags&32/*SOLITARY*/) )
|
|
{//not a force-usable func_static or, it is one, but it's solitary, so you only press it when looking right at it
|
|
if ( Q_stricmp( "limb", ent->classname ) )
|
|
{//not a limb
|
|
if ( ent->s.weapon == WP_TURRET && !Q_stricmp( "PAS", ent->classname ) && ent->s.apos.trType == TR_STATIONARY )
|
|
{//can knock over placed turrets
|
|
if ( !self->s.number || self->enemy != ent )
|
|
{//only NPCs who are actively mad at this turret can push it over
|
|
return qfalse;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if ( ent->moverState != MOVER_POS1 && ent->moverState != MOVER_POS2 )
|
|
{//not at rest
|
|
return qfalse;
|
|
}
|
|
}
|
|
}
|
|
//return qfalse;
|
|
}
|
|
else if ( ent->client->NPC_class == CLASS_MARK1 )
|
|
{//can't push Mark1 unless push 3
|
|
if ( pull || self->client->ps.forcePowerLevel[FP_PUSH] < FORCE_LEVEL_3 )
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
else if ( ent->client->NPC_class == CLASS_GALAKMECH
|
|
|| ent->client->NPC_class == CLASS_ATST
|
|
|| ent->client->NPC_class == CLASS_RANCOR
|
|
|| ent->client->NPC_class == CLASS_WAMPA
|
|
|| ent->client->NPC_class == CLASS_SAND_CREATURE )
|
|
{//can't push ATST or Galak or Rancor or Wampa
|
|
return qfalse;
|
|
}
|
|
else if ( ent->s.weapon == WP_EMPLACED_GUN )
|
|
{//FIXME: maybe can pull them out?
|
|
return qfalse;
|
|
}
|
|
else if ( ent->client->playerTeam == self->client->playerTeam && self->enemy && self->enemy != ent )
|
|
{//can't accidently push a teammate while in combat
|
|
return qfalse;
|
|
}
|
|
else if ( G_IsRidingVehicle( ent )
|
|
&& (ent->s.eFlags&EF_NODRAW) )
|
|
{//can't push/pull anyone riding *inside* vehicle
|
|
return qfalse;
|
|
}
|
|
}
|
|
else if ( ent->s.eType == ET_ITEM )
|
|
{
|
|
if ( (ent->flags&FL_NO_KNOCKBACK) )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( ent->item
|
|
&& ent->item->giType == IT_HOLDABLE
|
|
&& ent->item->giTag == INV_SECURITY_KEY )
|
|
//&& (ent->flags&FL_DROPPED_ITEM) ???
|
|
{//dropped security keys can't be pushed? But placed ones can...? does this make any sense?
|
|
if ( !pull || self->s.number )
|
|
{//can't push, NPC's can't do anything to it
|
|
return qfalse;
|
|
}
|
|
else
|
|
{
|
|
if ( g_crosshairEntNum != ent->s.number )
|
|
{//player can pull it if looking *right* at it
|
|
if ( cone >= 1.0f )
|
|
{//we did a forwardEnt trace
|
|
if ( forwardEnt != ent )
|
|
{//must be pointing right at them
|
|
return qfalse;
|
|
}
|
|
}
|
|
else if ( forward )
|
|
{//do a forwardEnt trace
|
|
trace_t tr;
|
|
vec3_t end;
|
|
VectorMA( self->client->renderInfo.eyePoint, radius, forward, end );
|
|
gi.trace( &tr, self->client->renderInfo.eyePoint, vec3_origin, vec3_origin, end, self->s.number, MASK_OPAQUE|CONTENTS_SOLID|CONTENTS_BODY|CONTENTS_ITEM|CONTENTS_CORPSE, (EG2_Collision)0, 0 );//was MASK_SHOT, changed to match crosshair trace
|
|
if ( tr.entityNum != ent->s.number )
|
|
{//last chance
|
|
return qfalse;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
switch ( ent->s.weapon )
|
|
{//only missiles with mass are force-pushable
|
|
case WP_SABER:
|
|
case WP_FLECHETTE:
|
|
case WP_ROCKET_LAUNCHER:
|
|
case WP_CONCUSSION:
|
|
case WP_THERMAL:
|
|
case WP_TRIP_MINE:
|
|
case WP_DET_PACK:
|
|
break;
|
|
//only alt-fire of this weapon is force-pushable
|
|
case WP_REPEATER:
|
|
if ( ent->methodOfDeath != MOD_REPEATER_ALT )
|
|
{//not an alt-fire missile
|
|
return qfalse;
|
|
}
|
|
break;
|
|
//everything else cannot be pushed
|
|
case WP_ATST_SIDE:
|
|
if ( ent->methodOfDeath != MOD_EXPLOSIVE )
|
|
{//not a rocket
|
|
return qfalse;
|
|
}
|
|
break;
|
|
default:
|
|
return qfalse;
|
|
break;
|
|
}
|
|
|
|
if ( ent->s.pos.trType == TR_STATIONARY && (ent->s.eFlags&EF_MISSILE_STICK) )
|
|
{//can't force-push/pull stuck missiles (detpacks, tripmines)
|
|
return qfalse;
|
|
}
|
|
if ( ent->s.pos.trType == TR_STATIONARY && ent->s.weapon != WP_THERMAL )
|
|
{//only thermal detonators can be pushed once stopped
|
|
return qfalse;
|
|
}
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
static qboolean ShouldPlayerResistForceThrow( gentity_t *player, gentity_t *attacker, qboolean pull )
|
|
{
|
|
if ( player->health <= 0 )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
if ( !player->client )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
if ( player->client->ps.forceRageRecoveryTime >= level.time )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
//wasn't trying to grip/drain anyone
|
|
if ( player->client->ps.torsoAnim == BOTH_FORCEGRIP_HOLD ||
|
|
player->client->ps.torsoAnim == BOTH_FORCE_DRAIN_GRAB_START ||
|
|
player->client->ps.torsoAnim == BOTH_FORCE_DRAIN_GRAB_HOLD )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
//only 30% chance of resisting a Desann or yoda push
|
|
if ( (attacker->client->NPC_class == CLASS_DESANN || Q_stricmp("Yoda",attacker->NPC_type) == 0) && Q_irand( 0, 2 ) > 0 )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
//on the ground
|
|
if ( player->client->ps.groundEntityNum == ENTITYNUM_NONE )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
//not knocked down already
|
|
if ( PM_InKnockDown( &player->client->ps ) )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
//not involved in a saberLock
|
|
if ( player->client->ps.saberLockTime >= level.time )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
//not attacking or otherwise busy
|
|
if ( player->client->ps.weaponTime >= level.time )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
//using saber or fists
|
|
if ( player->client->ps.weapon != WP_SABER && player->client->ps.weapon != WP_MELEE )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
forcePowers_t forcePower = (pull ? FP_PULL : FP_PUSH);
|
|
int attackingForceLevel = attacker->client->ps.forcePowerLevel[forcePower];
|
|
int defendingForceLevel = player->client->ps.forcePowerLevel[forcePower];
|
|
|
|
if ( player->client->ps.powerups[PW_FORCE_PUSH] > level.time ||
|
|
Q_irand( 0, Q_max(0, defendingForceLevel - attackingForceLevel)*2 + 1 ) > 0 )
|
|
{
|
|
// player was pushing, or player's force push/pull is high enough to try to stop me
|
|
if ( InFront( attacker->currentOrigin, player->client->renderInfo.eyePoint, player->client->ps.viewangles, 0.3f ) )
|
|
{
|
|
//I'm in front of player
|
|
return qtrue;
|
|
}
|
|
}
|
|
|
|
return qfalse;
|
|
}
|
|
|
|
|
|
void ForceThrowEx( gentity_t *self, qboolean pull, qboolean fake, qboolean aimByViewAngles );
|
|
void ForceThrow( gentity_t *self, qboolean pull, qboolean fake )
|
|
{
|
|
ForceThrowEx(self, pull, fake, qfalse);
|
|
}
|
|
|
|
void ForceThrowEx( gentity_t *self, qboolean pull, qboolean fake, qboolean aimByViewAngles )
|
|
{//FIXME: pass in a target ent so we (an NPC) can push/pull just one targeted ent.
|
|
//shove things in front of you away
|
|
float dist;
|
|
gentity_t *ent, *forwardEnt = NULL;
|
|
gentity_t *entityList[MAX_GENTITIES];
|
|
gentity_t *push_list[MAX_GENTITIES];
|
|
int numListedEntities = 0;
|
|
vec3_t mins, maxs;
|
|
vec3_t v;
|
|
int i, e;
|
|
int ent_count = 0;
|
|
int radius;
|
|
vec3_t center, ent_org, size, forward, right, end, dir, fwdangles = {0};
|
|
float dot1, cone;
|
|
trace_t tr;
|
|
int anim, hold, soundIndex, cost, actualCost;
|
|
qboolean noResist = qfalse;
|
|
|
|
if ( self->health <= 0 )
|
|
{
|
|
return;
|
|
}
|
|
if ( self->client->ps.leanofs )
|
|
{//can't force-throw while leaning
|
|
return;
|
|
}
|
|
if ( self->client->ps.forcePowerDebounce[FP_PUSH] > level.time )
|
|
{//already pushing- now you can't haul someone across the room, sorry
|
|
return;
|
|
}
|
|
if ( self->client->ps.forcePowerDebounce[FP_PULL] > level.time )
|
|
{//already pulling- now you can't haul someone across the room, sorry
|
|
return;
|
|
}
|
|
if ( self->client->ps.pullAttackTime > level.time )
|
|
{//already pull-attacking
|
|
return;
|
|
}
|
|
if ( !self->s.number && (cg.zoomMode || in_camera) )
|
|
{//can't force throw/pull when zoomed in or in cinematic
|
|
return;
|
|
}
|
|
if ( self->client->ps.saberLockTime > level.time )
|
|
{
|
|
if ( pull || self->client->ps.forcePowerLevel[FP_PUSH] < FORCE_LEVEL_3 )
|
|
{//this can be a way to break out
|
|
return;
|
|
}
|
|
//else, I'm breaking my half of the saberlock
|
|
self->client->ps.saberLockTime = 0;
|
|
self->client->ps.saberLockEnemy = ENTITYNUM_NONE;
|
|
}
|
|
|
|
if ( self->client->ps.legsAnim == BOTH_KNOCKDOWN3
|
|
|| (self->client->ps.torsoAnim == BOTH_FORCE_GETUP_F1 && self->client->ps.torsoAnimTimer > 400)
|
|
|| (self->client->ps.torsoAnim == BOTH_FORCE_GETUP_F2 && self->client->ps.torsoAnimTimer > 900)
|
|
|| (self->client->ps.torsoAnim == BOTH_GETUP3 && self->client->ps.torsoAnimTimer > 500)
|
|
|| (self->client->ps.torsoAnim == BOTH_GETUP4 && self->client->ps.torsoAnimTimer > 300)
|
|
|| (self->client->ps.torsoAnim == BOTH_GETUP5 && self->client->ps.torsoAnimTimer > 500) )
|
|
{//we're face-down, so we'd only be force-push/pulling the floor
|
|
return;
|
|
}
|
|
if ( pull )
|
|
{
|
|
radius = forcePushPullRadius[self->client->ps.forcePowerLevel[FP_PULL]];
|
|
}
|
|
else
|
|
{
|
|
radius = forcePushPullRadius[self->client->ps.forcePowerLevel[FP_PUSH]];
|
|
}
|
|
|
|
if ( !radius )
|
|
{//no ability to do this yet
|
|
return;
|
|
}
|
|
|
|
if ( pull )
|
|
{
|
|
cost = forcePowerNeeded[FP_PULL];
|
|
if ( !WP_ForcePowerUsable( self, FP_PULL, cost ) )
|
|
{
|
|
return;
|
|
}
|
|
//make sure this plays and that you cannot press fire for about 200ms after this
|
|
anim = BOTH_FORCEPULL;
|
|
soundIndex = G_SoundIndex( "sound/weapons/force/pull.wav" );
|
|
hold = 200;
|
|
}
|
|
else
|
|
{
|
|
cost = forcePowerNeeded[FP_PUSH];
|
|
if ( !WP_ForcePowerUsable( self, FP_PUSH, cost ) )
|
|
{
|
|
return;
|
|
}
|
|
//make sure this plays and that you cannot press fire for about 1 second after this
|
|
anim = BOTH_FORCEPUSH;
|
|
soundIndex = G_SoundIndex( "sound/weapons/force/push.wav" );
|
|
hold = 650;
|
|
}
|
|
|
|
int parts = SETANIM_TORSO;
|
|
if ( !PM_InKnockDown( &self->client->ps ) )
|
|
{
|
|
if ( self->client->ps.saberLockTime > level.time )
|
|
{
|
|
self->client->ps.saberLockTime = 0;
|
|
self->painDebounceTime = level.time + 2000;
|
|
hold += 1000;
|
|
parts = SETANIM_BOTH;
|
|
}
|
|
else if ( !VectorLengthSquared( self->client->ps.velocity ) && !(self->client->ps.pm_flags&PMF_DUCKED))
|
|
{
|
|
parts = SETANIM_BOTH;
|
|
}
|
|
}
|
|
|
|
if (self->client->ps.clientNum == 0)
|
|
{
|
|
//Handle this here so it is refreshed on every frame, not just when the lightning gun is first fired
|
|
cgi_HapticEvent("RTCWQuest:fire_tesla", 0, (vr->right_handed ? 2 : 1), 100, 0, 0);
|
|
}
|
|
|
|
|
|
NPC_SetAnim( self, parts, anim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD|SETANIM_FLAG_RESTART );
|
|
self->client->ps.saberMove = self->client->ps.saberBounceMove = LS_READY;//don't finish whatever saber anim you may have been in
|
|
self->client->ps.saberBlocked = BLOCKED_NONE;
|
|
if ( self->client->ps.forcePowersActive&(1<<FP_SPEED) )
|
|
{
|
|
hold = floor( hold*g_timescale->value );
|
|
}
|
|
self->client->ps.weaponTime = hold;//was 1000, but want to swing sooner
|
|
//do effect... FIXME: build-up or delay this until in proper part of anim
|
|
self->client->ps.powerups[PW_FORCE_PUSH] = level.time + self->client->ps.torsoAnimTimer + 500;
|
|
//reset to 0 in case it's still > 0 from a previous push
|
|
self->client->pushEffectFadeTime = 0;
|
|
|
|
G_Sound( self, soundIndex );
|
|
|
|
vec3_t origin, angles;
|
|
if (self->client->ps.clientNum == 0 && !cg.renderingThirdPerson && !aimByViewAngles)
|
|
{
|
|
BG_CalculateVROffHandPosition(origin, fwdangles);
|
|
AngleVectors( fwdangles, forward, right, NULL );
|
|
VectorCopy( origin, center );
|
|
}
|
|
else
|
|
{
|
|
VectorCopy( self->client->ps.viewangles, fwdangles );
|
|
VectorCopy( self->client->renderInfo.eyePoint, origin );
|
|
AngleVectors( fwdangles, forward, right, NULL );
|
|
VectorCopy( self->currentOrigin, center );
|
|
}
|
|
|
|
if ( (!pull && self->client->ps.forcePowersForced&(1<<FP_PUSH))
|
|
|| (pull && self->client->ps.forcePowersForced&(1<<FP_PULL))
|
|
|| (pull&&self->client->NPC_class==CLASS_KYLE&&(self->spawnflags&1)&&TIMER_Done( self, "kyleTakesSaber" )) )
|
|
{
|
|
noResist = qtrue;
|
|
}
|
|
|
|
if ( pull )
|
|
{
|
|
cone = forcePullCone[self->client->ps.forcePowerLevel[FP_PULL]];
|
|
}
|
|
else
|
|
{
|
|
cone = forcePushCone[self->client->ps.forcePowerLevel[FP_PUSH]];
|
|
}
|
|
|
|
// if ( cone >= 1.0f )
|
|
{//must be pointing right at them
|
|
VectorMA( origin, radius, forward, end );
|
|
gi.trace( &tr, origin, vec3_origin, vec3_origin, end, self->s.number, MASK_OPAQUE|CONTENTS_SOLID|CONTENTS_BODY|CONTENTS_ITEM|CONTENTS_CORPSE, (EG2_Collision)0, 0 );//was MASK_SHOT, changed to match crosshair trace
|
|
if ( tr.entityNum < ENTITYNUM_WORLD )
|
|
{//found something right in front of self,
|
|
forwardEnt = &g_entities[tr.entityNum];
|
|
if ( !forwardEnt->client && !Q_stricmp( "func_static", forwardEnt->classname ) )
|
|
{
|
|
if ( (forwardEnt->spawnflags&1/*F_PUSH*/)||(forwardEnt->spawnflags&2/*F_PULL*/) )
|
|
{//push/pullable
|
|
if ( (forwardEnt->spawnflags&32/*SOLITARY*/) )
|
|
{//can only push/pull ME, ignore all others
|
|
if ( forwardEnt->NPC_targetname == NULL
|
|
|| (self->targetname&&Q_stricmp( forwardEnt->NPC_targetname, self->targetname ) == 0) )
|
|
{//anyone can push it or only 1 person can push it and it's me
|
|
push_list[0] = forwardEnt;
|
|
ent_count = numListedEntities = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( forwardEnt )
|
|
{
|
|
if ( G_TryingPullAttack( self, &self->client->usercmd, qtrue ) )
|
|
{//we're going to try to do a pull attack on our forwardEnt
|
|
if ( WP_ForceThrowable( forwardEnt, forwardEnt, self, pull, cone, radius, forward ) )
|
|
{//we will actually pull-attack him, so don't pull him or anything else here
|
|
//activate the power, here, though, so the later check that actually does the pull attack knows we tried to pull
|
|
self->client->ps.forcePowersActive |= (1<<FP_PULL);
|
|
self->client->ps.forcePowerDebounce[FP_PULL] = level.time + 100; //force-pulling
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( !numListedEntities )
|
|
{
|
|
for ( i = 0 ; i < 3 ; i++ )
|
|
{
|
|
mins[i] = center[i] - radius;
|
|
maxs[i] = center[i] + radius;
|
|
}
|
|
|
|
numListedEntities = gi.EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
|
|
|
|
for ( e = 0 ; e < numListedEntities ; e++ )
|
|
{
|
|
ent = entityList[ e ];
|
|
|
|
if ( !WP_ForceThrowable( ent, forwardEnt, self, pull, cone, radius, forward ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
//this is all to see if we need to start a saber attack, if it's in flight, this doesn't matter
|
|
// find the distance from the edge of the bounding box
|
|
for ( i = 0 ; i < 3 ; i++ )
|
|
{
|
|
if ( center[i] < ent->absmin[i] )
|
|
{
|
|
v[i] = ent->absmin[i] - center[i];
|
|
} else if ( center[i] > ent->absmax[i] )
|
|
{
|
|
v[i] = center[i] - ent->absmax[i];
|
|
} else
|
|
{
|
|
v[i] = 0;
|
|
}
|
|
}
|
|
|
|
VectorSubtract( ent->absmax, ent->absmin, size );
|
|
VectorMA( ent->absmin, 0.5, size, ent_org );
|
|
|
|
//see if they're in front of me
|
|
VectorSubtract( ent_org, center, dir );
|
|
VectorNormalize( dir );
|
|
if ( cone < 1.0f )
|
|
{//must be within the forward cone
|
|
if ( ent->client && !pull
|
|
&& ent->client->ps.forceGripEntityNum == self->s.number
|
|
&& (self->s.eFlags&EF_FORCE_GRIPPED) )
|
|
{//this is the guy that's force-gripping me, use a wider cone regardless of force power level
|
|
if ( (dot1 = DotProduct( dir, forward )) < cone-0.3f )
|
|
continue;
|
|
}
|
|
else if ( ent->client && !pull
|
|
&& ent->client->ps.forceDrainEntityNum == self->s.number
|
|
&& (self->s.eFlags&EF_FORCE_DRAINED) )
|
|
{//this is the guy that's force-draining me, use a wider cone regardless of force power level
|
|
if ( (dot1 = DotProduct( dir, forward )) < cone-0.3f )
|
|
continue;
|
|
}
|
|
else if ( ent->s.eType == ET_MISSILE )//&& ent->s.eType != ET_ITEM && ent->e_ThinkFunc != thinkF_G_RunObject )
|
|
{//missiles are easier to force-push, never require direct trace (FIXME: maybe also items and general physics objects)
|
|
if ( (dot1 = DotProduct( dir, forward )) < cone-0.3f )
|
|
continue;
|
|
}
|
|
else if ( (dot1 = DotProduct( dir, forward )) < cone )
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else if ( ent->s.eType == ET_MISSILE )
|
|
{//a missile and we're at force level 1... just use a small cone, but not ridiculously small
|
|
if ( (dot1 = DotProduct( dir, forward )) < 0.75f )
|
|
{
|
|
continue;
|
|
}
|
|
}//else is an NPC or brush entity that our forward trace would have to hit
|
|
|
|
dist = VectorLength( v );
|
|
|
|
//Now check and see if we can actually deflect it
|
|
//method1
|
|
//if within a certain range, deflect it
|
|
if ( ent->s.eType == ET_MISSILE && cone >= 1.0f )
|
|
{//smaller radius on missile checks at force push 1
|
|
if ( dist >= 192 )
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else if ( dist >= radius )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
//in PVS?
|
|
if ( !ent->bmodel && !gi.inPVS( ent_org, origin ) )
|
|
{//must be in PVS
|
|
continue;
|
|
}
|
|
|
|
if ( ent != forwardEnt )
|
|
{//don't need to trace against forwardEnt again
|
|
//really should have a clear LOS to this thing...
|
|
gi.trace( &tr, origin, vec3_origin, vec3_origin, ent_org, self->s.number, MASK_FORCE_PUSH, (EG2_Collision)0, 0 );//was MASK_SHOT, but changed to match above trace and crosshair trace
|
|
if ( tr.fraction < 1.0f && tr.entityNum != ent->s.number )
|
|
{//must have clear LOS
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// ok, we are within the radius, add us to the incoming list
|
|
push_list[ent_count] = ent;
|
|
ent_count++;
|
|
}
|
|
}
|
|
|
|
if ( ent_count )
|
|
{
|
|
for ( int x = 0; x < ent_count; x++ )
|
|
{
|
|
if ( push_list[x]->client )
|
|
{
|
|
vec3_t pushDir;
|
|
float knockback = pull?0:200;
|
|
|
|
//SIGH band-aid...
|
|
if ( push_list[x]->s.number >= MAX_CLIENTS
|
|
&& self->s.number < MAX_CLIENTS )
|
|
{
|
|
if ( (push_list[x]->client->ps.forcePowersActive&(1<<FP_GRIP))
|
|
//&& push_list[x]->client->ps.forcePowerDebounce[FP_GRIP] < level.time
|
|
&& push_list[x]->client->ps.forceGripEntityNum == self->s.number )
|
|
{
|
|
WP_ForcePowerStop( push_list[x], FP_GRIP );
|
|
}
|
|
if ( (push_list[x]->client->ps.forcePowersActive&(1<<FP_DRAIN))
|
|
//&& push_list[x]->client->ps.forcePowerDebounce[FP_DRAIN] < level.time
|
|
&& push_list[x]->client->ps.forceDrainEntityNum == self->s.number )
|
|
{
|
|
WP_ForcePowerStop( push_list[x], FP_DRAIN );
|
|
}
|
|
}
|
|
|
|
if ( Rosh_BeingHealed( push_list[x] ) )
|
|
{
|
|
continue;
|
|
}
|
|
if ( push_list[x]->client->NPC_class == CLASS_HAZARD_TROOPER
|
|
&& push_list[x]->health > 0 )
|
|
{//living hazard troopers resist push/pull
|
|
WP_ForceThrowHazardTrooper( self, push_list[x], pull );
|
|
continue;
|
|
}
|
|
if ( fake )
|
|
{//always resist
|
|
WP_ResistForcePush( push_list[x], self, qfalse );
|
|
continue;
|
|
}
|
|
//FIXMEFIXMEFIXMEFIXMEFIXME: extern a lot of this common code when I have the time!!!
|
|
int powerLevel, powerUse;
|
|
if (pull)
|
|
{
|
|
powerLevel = self->client->ps.forcePowerLevel[FP_PULL];
|
|
powerUse = FP_PULL;
|
|
}
|
|
else
|
|
{
|
|
powerLevel = self->client->ps.forcePowerLevel[FP_PUSH];
|
|
powerUse = FP_PUSH;
|
|
}
|
|
int modPowerLevel = WP_AbsorbConversion( push_list[x], push_list[x]->client->ps.forcePowerLevel[FP_ABSORB], self, powerUse, powerLevel, forcePowerNeeded[self->client->ps.forcePowerLevel[powerUse]] );
|
|
if (push_list[x]->client->NPC_class==CLASS_ASSASSIN_DROID ||
|
|
push_list[x]->client->NPC_class==CLASS_HAZARD_TROOPER)
|
|
{
|
|
modPowerLevel = 0; // devides throw by 10
|
|
}
|
|
|
|
//First, if this is the player we're push/pulling, see if he can counter it
|
|
if ( modPowerLevel != -1
|
|
&& !noResist
|
|
&& InFront( center, push_list[x]->client->renderInfo.eyePoint, push_list[x]->client->ps.viewangles, 0.3f ) )
|
|
{//absorbed and I'm in front of them
|
|
//counter it
|
|
if ( push_list[x]->client->ps.forcePowerLevel[FP_ABSORB] > FORCE_LEVEL_2 )
|
|
{//no reaction at all
|
|
}
|
|
else
|
|
{
|
|
WP_ResistForcePush( push_list[x], self, qfalse );
|
|
push_list[x]->client->ps.saberMove = push_list[x]->client->ps.saberBounceMove = LS_READY;//don't finish whatever saber anim you may have been in
|
|
push_list[x]->client->ps.saberBlocked = BLOCKED_NONE;
|
|
}
|
|
continue;
|
|
}
|
|
else if ( !push_list[x]->s.number )
|
|
{//player
|
|
if ( !noResist && ShouldPlayerResistForceThrow(push_list[x], self, pull) )
|
|
{
|
|
WP_ResistForcePush( push_list[x], self, qfalse );
|
|
push_list[x]->client->ps.saberMove = push_list[x]->client->ps.saberBounceMove = LS_READY;//don't finish whatever saber anim you may have been in
|
|
push_list[x]->client->ps.saberBlocked = BLOCKED_NONE;
|
|
continue;
|
|
}
|
|
}
|
|
else if ( push_list[x]->client && Jedi_WaitingAmbush( push_list[x] ) )
|
|
{
|
|
WP_ForceKnockdown( push_list[x], self, pull, qtrue, qfalse );
|
|
continue;
|
|
}
|
|
|
|
G_KnockOffVehicle( push_list[x], self, pull );
|
|
|
|
if ( !pull
|
|
&& push_list[x]->client->ps.forceDrainEntityNum == self->s.number
|
|
&& (self->s.eFlags&EF_FORCE_DRAINED) )
|
|
{//stop them from draining me now, dammit!
|
|
WP_ForcePowerStop( push_list[x], FP_DRAIN );
|
|
}
|
|
|
|
//okay, everyone else (or player who couldn't resist it)...
|
|
if ( ((self->s.number == 0 && Q_irand( 0, 2 ) ) || Q_irand( 0, 2 ) ) && push_list[x]->client && push_list[x]->health > 0 //a living client
|
|
&& push_list[x]->client->ps.weapon == WP_SABER //Jedi
|
|
&& push_list[x]->health > 0 //alive
|
|
&& push_list[x]->client->ps.forceRageRecoveryTime < level.time //not recobering from rage
|
|
&& ((self->client->NPC_class != CLASS_DESANN&&Q_stricmp("Yoda",self->NPC_type)) || !Q_irand( 0, 2 ) )//only 30% chance of resisting a Desann push
|
|
&& push_list[x]->client->ps.groundEntityNum != ENTITYNUM_NONE //on the ground
|
|
&& InFront( center, push_list[x]->currentOrigin, push_list[x]->client->ps.viewangles, 0.3f ) //I'm in front of him
|
|
&& ( push_list[x]->client->ps.powerups[PW_FORCE_PUSH] > level.time ||//he's pushing too
|
|
(push_list[x]->s.number != 0 && push_list[x]->client->ps.weaponTime < level.time)//not the player and not attacking (NPC jedi auto-defend against pushes)
|
|
)
|
|
)
|
|
{//Jedi don't get pushed, they resist as long as they aren't already attacking and are on the ground
|
|
if ( push_list[x]->client->ps.saberLockTime > level.time )
|
|
{//they're in a lock
|
|
if ( push_list[x]->client->ps.saberLockEnemy != self->s.number )
|
|
{//they're not in a lock with me
|
|
continue;
|
|
}
|
|
else if ( pull || self->client->ps.forcePowerLevel[FP_PUSH] < FORCE_LEVEL_3 ||
|
|
push_list[x]->client->ps.forcePowerLevel[FP_PUSH] > FORCE_LEVEL_2 )
|
|
{//they're in a lock with me, but my push is too weak
|
|
continue;
|
|
}
|
|
else
|
|
{//we will knock them down
|
|
self->painDebounceTime = 0;
|
|
self->client->ps.weaponTime = 500;
|
|
if ( self->client->ps.forcePowersActive&(1<<FP_SPEED) )
|
|
{
|
|
self->client->ps.weaponTime = floor( self->client->ps.weaponTime * g_timescale->value );
|
|
}
|
|
}
|
|
}
|
|
int resistChance = Q_irand(0, 2);
|
|
if ( push_list[x]->s.number >= MAX_CLIENTS )
|
|
{//NPC
|
|
if ( g_spskill->integer == 1 )
|
|
{//stupid tweak for graham
|
|
resistChance = Q_irand(0, 3);
|
|
}
|
|
}
|
|
if ( noResist ||
|
|
( !pull
|
|
&& modPowerLevel == -1
|
|
&& self->client->ps.forcePowerLevel[FP_PUSH] > FORCE_LEVEL_2
|
|
&& !resistChance
|
|
&& push_list[x]->client->ps.forcePowerLevel[FP_PUSH] < FORCE_LEVEL_3 )
|
|
)
|
|
{//a level 3 push can even knock down a jedi
|
|
if ( PM_InKnockDown( &push_list[x]->client->ps ) )
|
|
{//can't knock them down again
|
|
continue;
|
|
}
|
|
WP_ForceKnockdown( push_list[x], self, pull, qfalse, qtrue );
|
|
}
|
|
else
|
|
{
|
|
WP_ResistForcePush( push_list[x], self, qfalse );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//UGH: FIXME: for enemy jedi, they should probably always do force pull 3, and not your weapon (if player?)!
|
|
//shove them
|
|
if ( push_list[x]->NPC
|
|
&& push_list[x]->NPC->jumpState == JS_JUMPING )
|
|
{//don't interrupt a scripted jump
|
|
//WP_ResistForcePush( push_list[x], self, qfalse );
|
|
push_list[x]->forcePushTime = level.time + 600; // let the push effect last for 600 ms
|
|
continue;
|
|
}
|
|
|
|
if ( push_list[x]->s.number
|
|
&& (push_list[x]->message || (push_list[x]->flags&FL_NO_KNOCKBACK)) )
|
|
{//an NPC who has a key
|
|
//don't push me... FIXME: maybe can pull the key off me?
|
|
WP_ForceKnockdown( push_list[x], self, pull, qfalse, qfalse );
|
|
continue;
|
|
}
|
|
if ( pull )
|
|
{
|
|
VectorSubtract( center, push_list[x]->currentOrigin, pushDir );
|
|
if ( self->client->ps.forcePowerLevel[FP_PULL] >= FORCE_LEVEL_3
|
|
&& self->client->NPC_class == CLASS_KYLE
|
|
&& (self->spawnflags&1)
|
|
&& TIMER_Done( self, "kyleTakesSaber" )
|
|
&& push_list[x]->client
|
|
&& push_list[x]->client->ps.weapon == WP_SABER
|
|
&& !push_list[x]->client->ps.saberInFlight
|
|
&& push_list[x]->client->ps.saberEntityNum < ENTITYNUM_WORLD
|
|
&& !PM_InOnGroundAnim( &push_list[x]->client->ps ) )
|
|
{
|
|
vec3_t throwVec;
|
|
VectorScale( pushDir, 10.0f, throwVec );
|
|
WP_SaberLose( push_list[x], throwVec );
|
|
NPC_SetAnim( push_list[x], SETANIM_BOTH, BOTH_LOSE_SABER, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
push_list[x]->client->ps.torsoAnimTimer += 500;
|
|
push_list[x]->client->ps.pm_time = push_list[x]->client->ps.weaponTime = push_list[x]->client->ps.torsoAnimTimer;
|
|
push_list[x]->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
|
|
push_list[x]->client->ps.saberMove = LS_NONE;
|
|
push_list[x]->aimDebounceTime = level.time + push_list[x]->client->ps.torsoAnimTimer;
|
|
VectorClear( push_list[x]->client->ps.velocity );
|
|
VectorClear( push_list[x]->client->ps.moveDir );
|
|
//Kyle will stand around for a bit, too...
|
|
self->client->ps.pm_time = self->client->ps.weaponTime = 2000;
|
|
self->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
|
|
self->painDebounceTime = level.time + self->client->ps.weaponTime;
|
|
TIMER_Set( self, "kyleTakesSaber", Q_irand( 60000, 180000 ) );//don't do this again for a while
|
|
G_AddVoiceEvent( self, Q_irand(EV_TAUNT1,EV_TAUNT3), Q_irand( 4000, 6000 ) );
|
|
VectorClear( self->client->ps.velocity );
|
|
VectorClear( self->client->ps.moveDir );
|
|
continue;
|
|
}
|
|
else if ( push_list[x]->NPC
|
|
&& (push_list[x]->NPC->scriptFlags&SCF_DONT_FLEE) )
|
|
{//*SIGH*... if an NPC can't flee, they can't run after and pick up their weapon, do don't drop it
|
|
}
|
|
else if ( self->client->ps.forcePowerLevel[FP_PULL] > FORCE_LEVEL_1
|
|
&& push_list[x]->client->NPC_class != CLASS_ROCKETTROOPER//rockettroopers never drop their weapon
|
|
&& push_list[x]->client->NPC_class != CLASS_VEHICLE
|
|
&& push_list[x]->client->NPC_class != CLASS_BOBAFETT
|
|
&& push_list[x]->client->NPC_class != CLASS_TUSKEN
|
|
&& push_list[x]->client->NPC_class != CLASS_HAZARD_TROOPER
|
|
&& push_list[x]->client->NPC_class != CLASS_ASSASSIN_DROID
|
|
&& push_list[x]->s.weapon != WP_SABER
|
|
&& push_list[x]->s.weapon != WP_MELEE
|
|
&& push_list[x]->s.weapon != WP_THERMAL
|
|
&& push_list[x]->s.weapon != WP_CONCUSSION // so rax can't drop his
|
|
)
|
|
{//yank the weapon - NOTE: level 1 just knocks them down, not take weapon
|
|
//FIXME: weapon yank anim if not a knockdown?
|
|
if ( InFront( center, push_list[x]->currentOrigin, push_list[x]->client->ps.viewangles, 0.0f ) )
|
|
{//enemy has to be facing me, too...
|
|
WP_DropWeapon( push_list[x], pushDir );
|
|
}
|
|
}
|
|
knockback += VectorNormalize( pushDir );
|
|
if ( knockback > 200 )
|
|
{
|
|
knockback = 200;
|
|
}
|
|
if ( self->client->ps.forcePowerLevel[FP_PULL] < FORCE_LEVEL_3 )
|
|
{//maybe just knock them down
|
|
knockback /= 3;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
VectorSubtract( push_list[x]->currentOrigin, center, pushDir );
|
|
knockback -= VectorNormalize( pushDir );
|
|
if ( knockback < 100 )
|
|
{
|
|
knockback = 100;
|
|
}
|
|
//scale for push level
|
|
if ( self->client->ps.forcePowerLevel[FP_PUSH] < FORCE_LEVEL_2 )
|
|
{//maybe just knock them down
|
|
knockback /= 3;
|
|
}
|
|
else if ( self->client->ps.forcePowerLevel[FP_PUSH] > FORCE_LEVEL_2 )
|
|
{//super-hard push
|
|
//Hmm, maybe in this case can even nudge/knockdown a jedi? Especially if close?
|
|
//knockback *= 5;
|
|
}
|
|
}
|
|
|
|
if ( modPowerLevel != -1 )
|
|
{
|
|
if ( !modPowerLevel )
|
|
{
|
|
knockback /= 10.0f;
|
|
}
|
|
else if ( modPowerLevel == 1 )
|
|
{
|
|
knockback /= 6.0f;
|
|
}
|
|
else// if ( modPowerLevel == 2 )
|
|
{
|
|
knockback /= 2.0f;
|
|
}
|
|
}
|
|
//actually push/pull the enemy
|
|
G_Throw( push_list[x], pushDir, knockback );
|
|
//make it so they don't actually hurt me when pulled at me...
|
|
push_list[x]->forcePuller = self->s.number;
|
|
|
|
if ( push_list[x]->client->ps.groundEntityNum != ENTITYNUM_NONE )
|
|
{//if on the ground, make sure they get shoved up some
|
|
if ( push_list[x]->client->ps.velocity[2] < knockback )
|
|
{
|
|
push_list[x]->client->ps.velocity[2] = knockback;
|
|
}
|
|
}
|
|
|
|
if ( push_list[x]->health > 0 )
|
|
{//target is still alive
|
|
if ( (push_list[x]->s.number||(cg.renderingThirdPerson&&!cg.zoomMode)) //NPC or 3rd person player
|
|
&& ((!pull&&self->client->ps.forcePowerLevel[FP_PUSH] < FORCE_LEVEL_2 && push_list[x]->client->ps.forcePowerLevel[FP_PUSH] < FORCE_LEVEL_1) //level 1 push
|
|
|| (pull && self->client->ps.forcePowerLevel[FP_PULL] < FORCE_LEVEL_2 && push_list[x]->client->ps.forcePowerLevel[FP_PULL] < FORCE_LEVEL_1)) )//level 1 pull
|
|
{//NPC or third person player (without force push/pull skill), and force push/pull level is at 1
|
|
WP_ForceKnockdown( push_list[x], self, pull, (qboolean)(!pull&&knockback>150), qfalse );
|
|
}
|
|
else if ( !push_list[x]->s.number )
|
|
{//player, have to force an anim on him
|
|
WP_ForceKnockdown( push_list[x], self, pull, (qboolean)(!pull&&knockback>150), qfalse );
|
|
}
|
|
else
|
|
{//NPC and force-push/pull at level 2 or higher
|
|
WP_ForceKnockdown( push_list[x], self, pull, (qboolean)(!pull&&knockback>100), qfalse );
|
|
}
|
|
}
|
|
push_list[x]->forcePushTime = level.time + 600; // let the push effect last for 600 ms
|
|
}
|
|
}
|
|
else if ( !fake )
|
|
{//not a fake push/pull
|
|
if ( push_list[x]->s.weapon == WP_SABER && (push_list[x]->contents&CONTENTS_LIGHTSABER) )
|
|
{//a thrown saber, just send it back
|
|
/*
|
|
if ( pull )
|
|
{//steal it?
|
|
}
|
|
else */if ( push_list[x]->owner && push_list[x]->owner->client && push_list[x]->owner->client->ps.SaberActive() && push_list[x]->s.pos.trType == TR_LINEAR && push_list[x]->owner->client->ps.saberEntityState != SES_RETURNING )
|
|
{//it's on and being controlled
|
|
//FIXME: prevent it from damaging me?
|
|
if ( self->s.number == 0 || Q_irand( 0, 2 ) )
|
|
{//certain chance of throwing it aside and turning it off?
|
|
//give it some velocity away from me
|
|
//FIXME: maybe actually push or pull it?
|
|
if ( Q_irand( 0, 1 ) )
|
|
{
|
|
VectorScale( right, -1, right );
|
|
}
|
|
G_ReflectMissile( self, push_list[x], right );
|
|
//FIXME: isn't turning off!!!
|
|
WP_SaberDrop( push_list[x]->owner, push_list[x] );
|
|
}
|
|
else
|
|
{
|
|
WP_SaberReturn( push_list[x]->owner, push_list[x] );
|
|
}
|
|
//different effect?
|
|
}
|
|
}
|
|
else if ( push_list[x]->s.eType == ET_MISSILE
|
|
&& push_list[x]->s.pos.trType != TR_STATIONARY
|
|
&& (push_list[x]->s.pos.trType != TR_INTERPOLATE||push_list[x]->s.weapon != WP_THERMAL) )//rolling and stationary thermal detonators are dealt with below
|
|
{
|
|
vec3_t dir2Me;
|
|
VectorSubtract( center, push_list[x]->currentOrigin, dir2Me );
|
|
float dot = DotProduct( push_list[x]->s.pos.trDelta, dir2Me );
|
|
if ( pull )
|
|
{//deflect rather than reflect?
|
|
}
|
|
else
|
|
{
|
|
if ( push_list[x]->s.eFlags&EF_MISSILE_STICK )
|
|
{//caught a sticky in-air
|
|
push_list[x]->s.eType = ET_MISSILE;
|
|
push_list[x]->s.eFlags &= ~EF_MISSILE_STICK;
|
|
push_list[x]->s.eFlags |= EF_BOUNCE_HALF;
|
|
push_list[x]->splashDamage /= 3;
|
|
push_list[x]->splashRadius /= 3;
|
|
push_list[x]->e_ThinkFunc = thinkF_WP_Explode;
|
|
push_list[x]->nextthink = level.time + Q_irand( 500, 3000 );
|
|
}
|
|
if ( dot >= 0 )
|
|
{//it's heading towards me
|
|
G_ReflectMissile( self, push_list[x], forward );
|
|
}
|
|
else
|
|
{
|
|
VectorScale( push_list[x]->s.pos.trDelta, 1.25f, push_list[x]->s.pos.trDelta );
|
|
}
|
|
//deflect sound
|
|
//G_Sound( push_list[x], G_SoundIndex( va("sound/weapons/blaster/reflect%d.wav", Q_irand( 1, 3 ) ) ) );
|
|
//push_list[x]->forcePushTime = level.time + 600; // let the push effect last for 600 ms
|
|
}
|
|
if ( push_list[x]->s.eType == ET_MISSILE
|
|
&& push_list[x]->s.weapon == WP_ROCKET_LAUNCHER
|
|
&& push_list[x]->damage < 60 )
|
|
{//pushing away a rocket raises it's damage to the max for NPCs
|
|
push_list[x]->damage = 60;
|
|
}
|
|
}
|
|
else if ( push_list[x]->svFlags & SVF_GLASS_BRUSH )
|
|
{//break the glass
|
|
trace_t tr;
|
|
vec3_t pushDir;
|
|
float damage = 800;
|
|
|
|
AngleVectors( fwdangles, forward, NULL, NULL );
|
|
VectorNormalize( forward );
|
|
VectorMA( origin, radius, forward, end );
|
|
gi.trace( &tr, origin, vec3_origin, vec3_origin, end, self->s.number, MASK_SHOT, (EG2_Collision)0, 0 );
|
|
if ( tr.entityNum != push_list[x]->s.number || tr.fraction == 1.0 || tr.allsolid || tr.startsolid )
|
|
{//must be pointing right at it
|
|
continue;
|
|
}
|
|
|
|
if ( pull )
|
|
{
|
|
VectorSubtract( origin, tr.endpos, pushDir );
|
|
}
|
|
else
|
|
{
|
|
VectorSubtract( tr.endpos, origin, pushDir );
|
|
}
|
|
/*
|
|
VectorSubtract( push_list[x]->absmax, push_list[x]->absmin, size );
|
|
VectorMA( push_list[x]->absmin, 0.5, size, center );
|
|
if ( pull )
|
|
{
|
|
VectorSubtract( origin, center, pushDir );
|
|
}
|
|
else
|
|
{
|
|
VectorSubtract( center, origin, pushDir );
|
|
}
|
|
*/
|
|
damage -= VectorNormalize( pushDir );
|
|
if ( damage < 200 )
|
|
{
|
|
damage = 200;
|
|
}
|
|
VectorScale( pushDir, damage, pushDir );
|
|
|
|
G_Damage( push_list[x], self, self, pushDir, tr.endpos, damage, 0, MOD_UNKNOWN );
|
|
}
|
|
else if ( !Q_stricmp( "func_static", push_list[x]->classname ) )
|
|
{//force-usable func_static
|
|
if ( !pull && (push_list[x]->spawnflags&1/*F_PUSH*/) )
|
|
{
|
|
if ( push_list[x]->NPC_targetname == NULL
|
|
|| (self->targetname&&Q_stricmp( push_list[x]->NPC_targetname, self->targetname ) == 0) )
|
|
{//anyone can pull it or only 1 person can push it and it's me
|
|
GEntity_UseFunc( push_list[x], self, self );
|
|
}
|
|
}
|
|
else if ( pull && (push_list[x]->spawnflags&2/*F_PULL*/) )
|
|
{
|
|
if ( push_list[x]->NPC_targetname == NULL
|
|
|| (self->targetname&&Q_stricmp( push_list[x]->NPC_targetname, self->NPC_targetname ) == 0) )
|
|
{//anyone can push it or only 1 person can push it and it's me
|
|
GEntity_UseFunc( push_list[x], self, self );
|
|
}
|
|
}
|
|
}
|
|
else if ( !Q_stricmp( "func_door", push_list[x]->classname ) && (push_list[x]->spawnflags&2/*MOVER_FORCE_ACTIVATE*/) )
|
|
{//push/pull the door
|
|
vec3_t pos1, pos2;
|
|
|
|
AngleVectors( fwdangles, forward, NULL, NULL );
|
|
VectorNormalize( forward );
|
|
VectorMA( origin, radius, forward, end );
|
|
gi.trace( &tr, origin, vec3_origin, vec3_origin, end, self->s.number, MASK_SHOT, (EG2_Collision)0, 0 );
|
|
if ( tr.entityNum != push_list[x]->s.number || tr.fraction == 1.0 || tr.allsolid || tr.startsolid )
|
|
{//must be pointing right at it
|
|
continue;
|
|
}
|
|
|
|
if ( VectorCompare( vec3_origin, push_list[x]->s.origin ) )
|
|
{//does not have an origin brush, so pos1 & pos2 are relative to world origin, need to calc center
|
|
VectorSubtract( push_list[x]->absmax, push_list[x]->absmin, size );
|
|
VectorMA( push_list[x]->absmin, 0.5, size, center );
|
|
if ( (push_list[x]->spawnflags&1) && push_list[x]->moverState == MOVER_POS1 )
|
|
{//if at pos1 and started open, make sure we get the center where it *started* because we're going to add back in the relative values pos1 and pos2
|
|
VectorSubtract( center, push_list[x]->pos1, center );
|
|
}
|
|
else if ( !(push_list[x]->spawnflags&1) && push_list[x]->moverState == MOVER_POS2 )
|
|
{//if at pos2, make sure we get the center where it *started* because we're going to add back in the relative values pos1 and pos2
|
|
VectorSubtract( center, push_list[x]->pos2, center );
|
|
}
|
|
VectorAdd( center, push_list[x]->pos1, pos1 );
|
|
VectorAdd( center, push_list[x]->pos2, pos2 );
|
|
}
|
|
else
|
|
{//actually has an origin, pos1 and pos2 are absolute
|
|
VectorCopy( push_list[x]->currentOrigin, center );
|
|
VectorCopy( push_list[x]->pos1, pos1 );
|
|
VectorCopy( push_list[x]->pos2, pos2 );
|
|
}
|
|
|
|
if ( Distance( pos1, origin ) < Distance( pos2, origin ) )
|
|
{//pos1 is closer
|
|
if ( push_list[x]->moverState == MOVER_POS1 )
|
|
{//at the closest pos
|
|
if ( pull )
|
|
{//trying to pull, but already at closest point, so screw it
|
|
continue;
|
|
}
|
|
}
|
|
else if ( push_list[x]->moverState == MOVER_POS2 )
|
|
{//at farthest pos
|
|
if ( !pull )
|
|
{//trying to push, but already at farthest point, so screw it
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{//pos2 is closer
|
|
if ( push_list[x]->moverState == MOVER_POS1 )
|
|
{//at the farthest pos
|
|
if ( !pull )
|
|
{//trying to push, but already at farthest point, so screw it
|
|
continue;
|
|
}
|
|
}
|
|
else if ( push_list[x]->moverState == MOVER_POS2 )
|
|
{//at closest pos
|
|
if ( pull )
|
|
{//trying to pull, but already at closest point, so screw it
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
GEntity_UseFunc( push_list[x], self, self );
|
|
}
|
|
else if ( push_list[x]->s.eType == ET_MISSILE/*thermal resting on ground*/
|
|
|| push_list[x]->s.eType == ET_ITEM
|
|
|| push_list[x]->e_ThinkFunc == thinkF_G_RunObject || Q_stricmp( "limb", push_list[x]->classname ) == 0 )
|
|
{//general object, toss it
|
|
vec3_t pushDir, kvel;
|
|
float knockback = pull?0:200;
|
|
float mass = 200;
|
|
|
|
if ( pull )
|
|
{
|
|
if ( push_list[x]->s.eType == ET_ITEM )
|
|
{//pull it to a little higher point
|
|
vec3_t adjustedOrg;
|
|
VectorCopy( center, adjustedOrg );
|
|
adjustedOrg[2] += self->maxs[2]/3;
|
|
VectorSubtract( adjustedOrg, push_list[x]->currentOrigin, pushDir );
|
|
}
|
|
else if ( self->enemy //I have an enemy
|
|
//&& push_list[x]->s.eType != ET_ITEM //not an item
|
|
&& self->client->ps.forcePowerLevel[FP_PUSH] > FORCE_LEVEL_2 //have push 3 or greater
|
|
&& InFront(push_list[x]->currentOrigin, center, self->currentAngles, 0.25f)//object is generally in front of me
|
|
&& InFront(self->enemy->currentOrigin, center, self->currentAngles, 0.75f)//enemy is pretty much right in front of me
|
|
&& !InFront(push_list[x]->currentOrigin, self->enemy->currentOrigin, self->enemy->currentAngles, -0.25f)//object is generally behind enemy
|
|
//FIXME: check dist to enemy and clear LOS to enemy and clear Path between object and enemy?
|
|
&& ( (self->NPC&&(noResist||Q_irand(0,RANK_CAPTAIN)<self->NPC->rank) )//NPC with enough skill
|
|
||( self->s.number<MAX_CLIENTS ) )
|
|
)
|
|
{//if I have an auto-enemy & he's in front of me, push it toward him!
|
|
/*
|
|
if ( targetedObjectMassTotal + push_list[x]->mass > TARGETED_OBJECT_PUSH_MASS_MAX )
|
|
{//already pushed too many things
|
|
//FIXME: pick closest?
|
|
continue;
|
|
}
|
|
targetedObjectMassTotal += push_list[x]->mass;
|
|
*/
|
|
VectorSubtract( self->enemy->currentOrigin, push_list[x]->currentOrigin, pushDir );
|
|
}
|
|
else
|
|
{
|
|
VectorSubtract( center, push_list[x]->currentOrigin, pushDir );
|
|
}
|
|
knockback += VectorNormalize( pushDir );
|
|
if ( knockback > 200 )
|
|
{
|
|
knockback = 200;
|
|
}
|
|
if ( push_list[x]->s.eType == ET_ITEM
|
|
&& push_list[x]->item
|
|
&& push_list[x]->item->giType == IT_HOLDABLE
|
|
&& push_list[x]->item->giTag == INV_SECURITY_KEY )
|
|
{//security keys are pulled with less enthusiasm
|
|
if ( knockback > 100 )
|
|
{
|
|
knockback = 100;
|
|
}
|
|
}
|
|
else if ( knockback > 200 )
|
|
{
|
|
knockback = 200;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( self->enemy //I have an enemy
|
|
&& push_list[x]->s.eType != ET_ITEM //not an item
|
|
&& self->client->ps.forcePowerLevel[FP_PUSH] > FORCE_LEVEL_2 //have push 3 or greater
|
|
&& InFront(push_list[x]->currentOrigin, center, self->currentAngles, 0.25f)//object is generally in front of me
|
|
&& InFront(self->enemy->currentOrigin, center, self->currentAngles, 0.75f)//enemy is pretty much right in front of me
|
|
&& InFront(push_list[x]->currentOrigin, self->enemy->currentOrigin, self->enemy->currentAngles, 0.25f)//object is generally in front of enemy
|
|
//FIXME: check dist to enemy and clear LOS to enemy and clear Path between object and enemy?
|
|
&& ( (self->NPC&&(noResist||Q_irand(0,RANK_CAPTAIN)<self->NPC->rank) )//NPC with enough skill
|
|
||( self->s.number<MAX_CLIENTS ) )
|
|
)
|
|
{//if I have an auto-enemy & he's in front of me, push it toward him!
|
|
/*
|
|
if ( targetedObjectMassTotal + push_list[x]->mass > TARGETED_OBJECT_PUSH_MASS_MAX )
|
|
{//already pushed too many things
|
|
//FIXME: pick closest?
|
|
continue;
|
|
}
|
|
targetedObjectMassTotal += push_list[x]->mass;
|
|
*/
|
|
VectorSubtract( self->enemy->currentOrigin, push_list[x]->currentOrigin, pushDir );
|
|
}
|
|
else
|
|
{
|
|
VectorSubtract( push_list[x]->currentOrigin, center, pushDir );
|
|
}
|
|
knockback -= VectorNormalize( pushDir );
|
|
if ( knockback < 100 )
|
|
{
|
|
knockback = 100;
|
|
}
|
|
}
|
|
//FIXME: if pull a FL_FORCE_PULLABLE_ONLY, clear the flag, assuming it's no longer in solid? or check?
|
|
VectorCopy( push_list[x]->currentOrigin, push_list[x]->s.pos.trBase );
|
|
push_list[x]->s.pos.trTime = level.time; // move a bit on the very first frame
|
|
if ( push_list[x]->s.pos.trType != TR_INTERPOLATE )
|
|
{//don't do this to rolling missiles
|
|
push_list[x]->s.pos.trType = TR_GRAVITY;
|
|
}
|
|
|
|
if ( push_list[x]->e_ThinkFunc == thinkF_G_RunObject && push_list[x]->physicsBounce )
|
|
{//it's a pushable misc_model_breakable, use it's mass instead of our one-size-fits-all mass
|
|
mass = push_list[x]->physicsBounce;//same as push_list[x]->mass, right?
|
|
}
|
|
if ( mass < 50 )
|
|
{//???
|
|
mass = 50;
|
|
}
|
|
if ( g_gravity->value > 0 )
|
|
{
|
|
VectorScale( pushDir, g_knockback->value * knockback / mass * 0.8, kvel );
|
|
kvel[2] = pushDir[2] * g_knockback->value * knockback / mass * 1.5;
|
|
}
|
|
else
|
|
{
|
|
VectorScale( pushDir, g_knockback->value * knockback / mass, kvel );
|
|
}
|
|
VectorAdd( push_list[x]->s.pos.trDelta, kvel, push_list[x]->s.pos.trDelta );
|
|
if ( g_gravity->value > 0 )
|
|
{
|
|
if ( push_list[x]->s.pos.trDelta[2] < knockback )
|
|
{
|
|
push_list[x]->s.pos.trDelta[2] = knockback;
|
|
}
|
|
}
|
|
//no trDuration?
|
|
if ( push_list[x]->e_ThinkFunc != thinkF_G_RunObject )
|
|
{//objects spin themselves?
|
|
//spin it
|
|
//FIXME: messing with roll ruins the rotational center???
|
|
push_list[x]->s.apos.trTime = level.time;
|
|
push_list[x]->s.apos.trType = TR_LINEAR;
|
|
VectorClear( push_list[x]->s.apos.trDelta );
|
|
push_list[x]->s.apos.trDelta[1] = Q_irand( -800, 800 );
|
|
}
|
|
|
|
if ( Q_stricmp( "limb", push_list[x]->classname ) == 0 )
|
|
{//make sure it runs it's physics
|
|
push_list[x]->e_ThinkFunc = thinkF_LimbThink;
|
|
push_list[x]->nextthink = level.time + FRAMETIME;
|
|
}
|
|
push_list[x]->forcePushTime = level.time + 600; // let the push effect last for 600 ms
|
|
push_list[x]->forcePuller = self->s.number;//remember this regardless
|
|
if ( push_list[x]->item && push_list[x]->item->giTag == INV_SECURITY_KEY )
|
|
{
|
|
AddSightEvent( player, push_list[x]->currentOrigin, 128, AEL_DISCOVERED );//security keys are more important
|
|
}
|
|
else
|
|
{
|
|
AddSightEvent( player, push_list[x]->currentOrigin, 128, AEL_SUSPICIOUS );//hmm... or should this always be discovered?
|
|
}
|
|
}
|
|
else if ( push_list[x]->s.weapon == WP_TURRET
|
|
&& !Q_stricmp( "PAS", push_list[x]->classname )
|
|
&& push_list[x]->s.apos.trType == TR_STATIONARY )
|
|
{//a portable turret
|
|
WP_KnockdownTurret( self, push_list[x] );
|
|
}
|
|
}
|
|
}
|
|
if ( pull )
|
|
{
|
|
if ( self->client->ps.forcePowerLevel[FP_PULL] > FORCE_LEVEL_2 )
|
|
{//at level 3, can pull multiple, so it costs more
|
|
actualCost = forcePowerNeeded[FP_PULL]*ent_count;
|
|
if ( actualCost > 50 )
|
|
{
|
|
actualCost = 50;
|
|
}
|
|
else if ( actualCost < cost )
|
|
{
|
|
actualCost = cost;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
actualCost = cost;
|
|
}
|
|
WP_ForcePowerStart( self, FP_PULL, actualCost );
|
|
}
|
|
else
|
|
{
|
|
if ( self->client->ps.forcePowerLevel[FP_PUSH] > FORCE_LEVEL_2 )
|
|
{//at level 3, can push multiple, so costs more
|
|
actualCost = forcePowerNeeded[FP_PUSH]*ent_count;
|
|
if ( actualCost > 50 )
|
|
{
|
|
actualCost = 50;
|
|
}
|
|
else if ( actualCost < cost )
|
|
{
|
|
actualCost = cost;
|
|
}
|
|
}
|
|
else if ( self->client->ps.forcePowerLevel[FP_PUSH] > FORCE_LEVEL_1 )
|
|
{//at level 2, can push multiple, so costs more
|
|
actualCost = floor(forcePowerNeeded[FP_PUSH]*ent_count/1.5f);
|
|
if ( actualCost > 50 )
|
|
{
|
|
actualCost = 50;
|
|
}
|
|
else if ( actualCost < cost )
|
|
{
|
|
actualCost = cost;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
actualCost = cost;
|
|
}
|
|
WP_ForcePowerStart( self, FP_PUSH, actualCost );
|
|
}
|
|
}
|
|
else
|
|
{//didn't push or pull anything? don't penalize them too much
|
|
if ( pull )
|
|
{
|
|
WP_ForcePowerStart( self, FP_PULL, 5 );
|
|
}
|
|
else
|
|
{
|
|
WP_ForcePowerStart( self, FP_PUSH, 5 );
|
|
}
|
|
}
|
|
if ( pull )
|
|
{
|
|
if ( self->NPC )
|
|
{//NPCs can push more often
|
|
//FIXME: vary by rank and game skill?
|
|
self->client->ps.forcePowerDebounce[FP_PULL] = level.time + 200;
|
|
}
|
|
else
|
|
{
|
|
self->client->ps.forcePowerDebounce[FP_PULL] = level.time + self->client->ps.torsoAnimTimer + 500;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( self->NPC )
|
|
{//NPCs can push more often
|
|
//FIXME: vary by rank and game skill?
|
|
self->client->ps.forcePowerDebounce[FP_PUSH] = level.time + 200;
|
|
}
|
|
else
|
|
{
|
|
self->client->ps.forcePowerDebounce[FP_PUSH] = level.time + self->client->ps.torsoAnimTimer + 500;
|
|
}
|
|
}
|
|
}
|
|
|
|
void WP_DebounceForceDeactivateTime( gentity_t *self )
|
|
{
|
|
//FIXME: if these are interruptable, should they also drain power at a constant rate
|
|
// rather than just taking one lump sum of force power upfront?
|
|
if ( self && self->client )
|
|
{
|
|
if ( self->client->ps.forcePowersActive&(1<<FP_SPEED)
|
|
|| self->client->ps.forcePowersActive&(1<<FP_PROTECT)
|
|
|| self->client->ps.forcePowersActive&(1<<FP_ABSORB)
|
|
|| self->client->ps.forcePowersActive&(1<<FP_RAGE)
|
|
|| self->client->ps.forcePowersActive&(1<<FP_SEE) )
|
|
{//already running another power that can be manually, stopped don't debounce so long
|
|
self->client->ps.forceAllowDeactivateTime = level.time + 500;
|
|
}
|
|
else
|
|
{//not running one of the interruptable powers
|
|
//FIXME: this should be shorter for force speed and rage (because of timescaling)
|
|
self->client->ps.forceAllowDeactivateTime = level.time + 1500;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ForceSpeed( gentity_t *self, int duration )
|
|
{
|
|
if ( self->health <= 0 )
|
|
{
|
|
return;
|
|
}
|
|
if (self->client->ps.forceAllowDeactivateTime < level.time &&
|
|
(self->client->ps.forcePowersActive & (1 << FP_SPEED)) )
|
|
{//stop using it
|
|
WP_ForcePowerStop( self, FP_SPEED );
|
|
return;
|
|
}
|
|
if ( !WP_ForcePowerUsable( self, FP_SPEED, 0 ) )
|
|
{
|
|
return;
|
|
}
|
|
if ( self->client->ps.saberLockTime > level.time )
|
|
{//FIXME: can this be a way to break out?
|
|
return;
|
|
}
|
|
|
|
WP_DebounceForceDeactivateTime( self );
|
|
|
|
WP_ForcePowerStart( self, FP_SPEED, 0 );
|
|
if ( duration )
|
|
{
|
|
self->client->ps.forcePowerDuration[FP_SPEED] = level.time + duration;
|
|
}
|
|
G_Sound( self, G_SoundIndex( "sound/weapons/force/speed.wav" ) );
|
|
}
|
|
|
|
void WP_StartForceHealEffects( gentity_t *self )
|
|
{
|
|
if ( self->ghoul2.size() )
|
|
{
|
|
if ( self->chestBolt != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "force/heal2" ), self->playerModel, self->chestBolt, self->s.number, self->currentOrigin, 3000, qtrue );
|
|
}
|
|
/*
|
|
if ( self->headBolt != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "force/heal_joint" ), self->playerModel, self->headBolt, self->s.number, self->currentOrigin, 3000, qtrue );
|
|
}
|
|
if ( self->cervicalBolt != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "force/heal_joint" ), self->playerModel, self->cervicalBolt, self->s.number, self->currentOrigin, 3000, qtrue );
|
|
}
|
|
if ( self->chestBolt != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "force/heal_joint" ), self->playerModel, self->chestBolt, self->s.number, self->currentOrigin, 3000, qtrue );
|
|
}
|
|
if ( self->gutBolt != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "force/heal_joint" ), self->playerModel, self->gutBolt, self->s.number, self->currentOrigin, 3000, qtrue );
|
|
}
|
|
if ( self->kneeLBolt != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "force/heal_joint" ), self->playerModel, self->kneeLBolt, self->s.number, self->currentOrigin, 3000, qtrue );
|
|
}
|
|
if ( self->kneeRBolt != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "force/heal_joint" ), self->playerModel, self->kneeRBolt, self->s.number, self->currentOrigin, 3000, qtrue );
|
|
}
|
|
if ( self->elbowLBolt != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "force/heal_joint" ), self->playerModel, self->elbowLBolt, self->s.number, self->currentOrigin, 3000, qtrue );
|
|
}
|
|
if ( self->elbowRBolt != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "force/heal_joint" ), self->playerModel, self->elbowRBolt, self->s.number, self->currentOrigin, 3000, qtrue );
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
|
|
void WP_StopForceHealEffects( gentity_t *self )
|
|
{
|
|
if ( self->ghoul2.size() )
|
|
{
|
|
if ( self->chestBolt != -1 )
|
|
{
|
|
G_StopEffect( G_EffectIndex( "force/heal2" ), self->playerModel, self->chestBolt, self->s.number );
|
|
}
|
|
/*
|
|
if ( self->headBolt != -1 )
|
|
{
|
|
G_StopEffect( G_EffectIndex( "force/heal_joint" ), self->playerModel, self->headBolt, self->s.number );
|
|
}
|
|
if ( self->cervicalBolt != -1 )
|
|
{
|
|
G_StopEffect( G_EffectIndex( "force/heal_joint" ), self->playerModel, self->cervicalBolt, self->s.number );
|
|
}
|
|
if ( self->chestBolt != -1 )
|
|
{
|
|
G_StopEffect( G_EffectIndex( "force/heal_joint" ), self->playerModel, self->chestBolt, self->s.number );
|
|
}
|
|
if ( self->gutBolt != -1 )
|
|
{
|
|
G_StopEffect( G_EffectIndex( "force/heal_joint" ), self->playerModel, self->gutBolt, self->s.number );
|
|
}
|
|
if ( self->kneeLBolt != -1 )
|
|
{
|
|
G_StopEffect( G_EffectIndex( "force/heal_joint" ), self->playerModel, self->kneeLBolt, self->s.number );
|
|
}
|
|
if ( self->kneeRBolt != -1 )
|
|
{
|
|
G_StopEffect( G_EffectIndex( "force/heal_joint" ), self->playerModel, self->kneeRBolt, self->s.number );
|
|
}
|
|
if ( self->elbowLBolt != -1 )
|
|
{
|
|
G_StopEffect( G_EffectIndex( "force/heal_joint" ), self->playerModel, self->elbowLBolt, self->s.number );
|
|
}
|
|
if ( self->elbowRBolt != -1 )
|
|
{
|
|
G_StopEffect( G_EffectIndex( "force/heal_joint" ), self->playerModel, self->elbowRBolt, self->s.number );
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
|
|
int FP_MaxForceHeal( gentity_t *self )
|
|
{
|
|
if ( self->s.number >= MAX_CLIENTS )
|
|
{
|
|
return MAX_FORCE_HEAL_HARD;
|
|
}
|
|
switch ( g_spskill->integer )
|
|
{
|
|
case 0://easy
|
|
return MAX_FORCE_HEAL_EASY;
|
|
break;
|
|
case 1://medium
|
|
return MAX_FORCE_HEAL_MEDIUM;
|
|
break;
|
|
case 2://hard
|
|
default:
|
|
return MAX_FORCE_HEAL_HARD;
|
|
break;
|
|
}
|
|
}
|
|
|
|
int FP_ForceHealInterval( gentity_t *self )
|
|
{
|
|
return (self->client->ps.forcePowerLevel[FP_HEAL]>FORCE_LEVEL_2)?50:FORCE_HEAL_INTERVAL;
|
|
}
|
|
|
|
void ForceHeal( gentity_t *self )
|
|
{
|
|
if ( self->health <= 0 || self->client->ps.stats[STAT_MAX_HEALTH] <= self->health )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( !WP_ForcePowerUsable( self, FP_HEAL, 20 ) )
|
|
{//must have enough force power for at least 5 points of health
|
|
return;
|
|
}
|
|
|
|
if ( self->painDebounceTime > level.time || (self->client->ps.weaponTime&&self->client->ps.weapon!=WP_NONE) )
|
|
{//can't initiate a heal while taking pain or attacking
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.saberLockTime > level.time )
|
|
{//FIXME: can this be a way to break out?
|
|
return;
|
|
}
|
|
/*
|
|
if ( self->client->ps.forcePowerLevel[FP_HEAL] > FORCE_LEVEL_2 )
|
|
{//instant heal
|
|
//no more than available force power
|
|
int max = self->client->ps.forcePower;
|
|
if ( max > MAX_FORCE_HEAL )
|
|
{//no more than max allowed
|
|
max = MAX_FORCE_HEAL;
|
|
}
|
|
if ( max > self->client->ps.stats[STAT_MAX_HEALTH] - self->health )
|
|
{//no more than what's missing
|
|
max = self->client->ps.stats[STAT_MAX_HEALTH] - self->health;
|
|
}
|
|
self->health += max;
|
|
WP_ForcePowerDrain( self, FP_HEAL, max );
|
|
G_SoundOnEnt( self, CHAN_VOICE, va( "sound/weapons/force/heal%d.mp3", Q_irand( 1, 4 ) ) );
|
|
}
|
|
else
|
|
*/
|
|
{
|
|
//start health going up
|
|
//NPC_SetAnim( self, SETANIM_TORSO, ?, SETANIM_FLAG_OVERRIDE );
|
|
WP_ForcePowerStart( self, FP_HEAL, 0 );
|
|
if ( self->client->ps.forcePowerLevel[FP_HEAL] < FORCE_LEVEL_2 )
|
|
{//must meditate
|
|
//FIXME: holster weapon (select WP_NONE?)
|
|
//FIXME: BOTH_FORCEHEAL_START
|
|
NPC_SetAnim( self, SETANIM_BOTH, BOTH_FORCEHEAL_START, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
self->client->ps.saberMove = self->client->ps.saberBounceMove = LS_READY;//don't finish whatever saber anim you may have been in
|
|
self->client->ps.saberBlocked = BLOCKED_NONE;
|
|
self->client->ps.torsoAnimTimer = self->client->ps.legsAnimTimer = FP_ForceHealInterval(self)*FP_MaxForceHeal(self) + 2000;//???
|
|
WP_DeactivateSaber( self );//turn off saber when meditating
|
|
}
|
|
else
|
|
{//just a quick gesture
|
|
/*
|
|
//Can't get an anim that looks good...
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCEHEAL_QUICK, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
self->client->ps.saberMove = self->client->ps.saberBounceMove = LS_READY;//don't finish whatever saber anim you may have been in
|
|
self->client->ps.saberBlocked = BLOCKED_NONE;
|
|
*/
|
|
}
|
|
}
|
|
|
|
//FIXME: always play healing effect
|
|
G_SoundOnEnt( self, CHAN_ITEM, "sound/weapons/force/heal.mp3" );
|
|
}
|
|
|
|
extern void NPC_PlayConfusionSound( gentity_t *self );
|
|
extern void NPC_Jedi_PlayConfusionSound( gentity_t *self );
|
|
qboolean WP_CheckBreakControl( gentity_t *self )
|
|
{
|
|
if ( !self )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( !self->s.number )
|
|
{//player
|
|
if ( self->client && self->client->ps.forcePowerLevel[FP_TELEPATHY] > FORCE_LEVEL_3 )
|
|
{//control-level
|
|
if ( self->client->ps.viewEntity > 0 && self->client->ps.viewEntity < ENTITYNUM_WORLD )
|
|
{//we are in a viewentity
|
|
gentity_t *controlled = &g_entities[self->client->ps.viewEntity];
|
|
if ( controlled->NPC && controlled->NPC->controlledTime > level.time )
|
|
{//it is an NPC we controlled
|
|
//clear it and return
|
|
G_ClearViewEntity( self );
|
|
return qtrue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{//NPC
|
|
if ( self->NPC && self->NPC->controlledTime > level.time )
|
|
{//being controlled
|
|
gentity_t *controller = &g_entities[0];
|
|
if ( controller->client && controller->client->ps.viewEntity == self->s.number )
|
|
{//we are being controlled by player
|
|
if ( controller->client->ps.forcePowerLevel[FP_TELEPATHY] > FORCE_LEVEL_3 )
|
|
{//control-level mind trick
|
|
//clear the control and return
|
|
G_ClearViewEntity( controller );
|
|
return qtrue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
extern bool Pilot_AnyVehiclesRegistered();
|
|
|
|
void ForceTelepathy( gentity_t *self )
|
|
{
|
|
trace_t tr;
|
|
vec3_t end, forward;
|
|
gentity_t *traceEnt;
|
|
qboolean targetLive = qfalse;
|
|
|
|
if ( WP_CheckBreakControl( self ) )
|
|
{
|
|
return;
|
|
}
|
|
if ( self->health <= 0 )
|
|
{
|
|
return;
|
|
}
|
|
//FIXME: if mind trick 3 and aiming at an enemy need more force power
|
|
if ( !WP_ForcePowerUsable( self, FP_TELEPATHY, 0 ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.weaponTime >= 800 )
|
|
{//just did one!
|
|
return;
|
|
}
|
|
if ( self->client->ps.saberLockTime > level.time )
|
|
{//FIXME: can this be a way to break out?
|
|
return;
|
|
}
|
|
|
|
vec3_t origin, angles;
|
|
if (self->client->ps.clientNum == 0 && !cg.renderingThirdPerson)
|
|
{
|
|
BG_CalculateVROffHandPosition(origin, angles);
|
|
AngleVectors(angles, forward, NULL, NULL);
|
|
}
|
|
else
|
|
{
|
|
AngleVectors(self->client->ps.viewangles, forward, NULL, NULL);
|
|
VectorCopy(self->client->ps.viewangles, angles);
|
|
VectorCopy(self->client->renderInfo.eyePoint, origin);
|
|
}
|
|
VectorNormalize( forward );
|
|
VectorMA( origin, 2048, forward, end );
|
|
|
|
//Cause a distraction if enemy is not fighting
|
|
gi.trace( &tr, self->client->renderInfo.eyePoint, vec3_origin, vec3_origin, end, self->s.number, MASK_OPAQUE|CONTENTS_BODY, (EG2_Collision)0, 0 );
|
|
if ( tr.entityNum == ENTITYNUM_NONE || tr.fraction == 1.0 || tr.allsolid || tr.startsolid )
|
|
{
|
|
return;
|
|
}
|
|
|
|
traceEnt = &g_entities[tr.entityNum];
|
|
|
|
if( traceEnt->NPC && traceEnt->NPC->scriptFlags & SCF_NO_FORCE )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( traceEnt && traceEnt->client )
|
|
{
|
|
switch ( traceEnt->client->NPC_class )
|
|
{
|
|
case CLASS_GALAKMECH://cant grip him, he's in armor
|
|
case CLASS_ATST://much too big to grip!
|
|
//no droids either
|
|
case CLASS_PROBE:
|
|
case CLASS_GONK:
|
|
case CLASS_R2D2:
|
|
case CLASS_R5D2:
|
|
case CLASS_MARK1:
|
|
case CLASS_MARK2:
|
|
case CLASS_MOUSE:
|
|
case CLASS_SEEKER:
|
|
case CLASS_REMOTE:
|
|
case CLASS_PROTOCOL:
|
|
case CLASS_ASSASSIN_DROID:
|
|
case CLASS_SABER_DROID:
|
|
case CLASS_BOBAFETT:
|
|
break;
|
|
case CLASS_RANCOR:
|
|
if ( !(traceEnt->spawnflags&1) )
|
|
{
|
|
targetLive = qtrue;
|
|
}
|
|
break;
|
|
default:
|
|
targetLive = qtrue;
|
|
break;
|
|
}
|
|
}
|
|
if ( targetLive
|
|
&& traceEnt->NPC
|
|
&& traceEnt->health > 0 )
|
|
{//hit an organic non-player
|
|
if ( G_ActivateBehavior( traceEnt, BSET_MINDTRICK ) )
|
|
{//activated a script on him
|
|
//FIXME: do the visual sparkles effect on their heads, still?
|
|
WP_ForcePowerStart( self, FP_TELEPATHY, 0 );
|
|
}
|
|
else if ( traceEnt->client->playerTeam != self->client->playerTeam )
|
|
{//an enemy
|
|
int override = 0;
|
|
if ( (traceEnt->NPC->scriptFlags&SCF_NO_MIND_TRICK) )
|
|
{
|
|
if ( traceEnt->client->NPC_class == CLASS_GALAKMECH )
|
|
{
|
|
G_AddVoiceEvent( traceEnt, Q_irand( EV_CONFUSE1, EV_CONFUSE3 ), Q_irand( 3000, 5000 ) );
|
|
}
|
|
}
|
|
else if ( self->client->ps.forcePowerLevel[FP_TELEPATHY] > FORCE_LEVEL_3 )
|
|
{//control them, even jedi
|
|
G_SetViewEntity( self, traceEnt );
|
|
traceEnt->NPC->controlledTime = level.time + 30000;
|
|
}
|
|
else if ( traceEnt->s.weapon != WP_SABER
|
|
&& traceEnt->client->NPC_class != CLASS_REBORN )
|
|
{//haha! Jedi aren't easily confused!
|
|
if ( self->client->ps.forcePowerLevel[FP_TELEPATHY] > FORCE_LEVEL_2
|
|
&& traceEnt->s.weapon != WP_NONE //don't charm people who aren't capable of fighting... like ugnaughts and droids, just confuse them
|
|
&& traceEnt->client->NPC_class != CLASS_TUSKEN//don't charm them, just confuse them
|
|
&& traceEnt->client->NPC_class != CLASS_NOGHRI//don't charm them, just confuse them
|
|
&& !Pilot_AnyVehiclesRegistered() //also, don't charm guys when bikes are near
|
|
)
|
|
{//turn them to our side
|
|
//if mind trick 3 and aiming at an enemy need more force power
|
|
override = 50;
|
|
if ( self->client->ps.forcePower < 50 )
|
|
{
|
|
if (self->client->ps.clientNum == 0)
|
|
{
|
|
CG_CenterPrint("Force Power Low", 440);
|
|
}
|
|
return;
|
|
}
|
|
if ( traceEnt->enemy )
|
|
{
|
|
G_ClearEnemy( traceEnt );
|
|
}
|
|
if ( traceEnt->NPC )
|
|
{
|
|
//traceEnt->NPC->tempBehavior = BS_FOLLOW_LEADER;
|
|
traceEnt->client->leader = self;
|
|
}
|
|
//FIXME: maybe pick an enemy right here?
|
|
//FIXME: does nothing to TEAM_FREE and TEAM_NEUTRALs!!!
|
|
team_t saveTeam = traceEnt->client->enemyTeam;
|
|
traceEnt->client->enemyTeam = traceEnt->client->playerTeam;
|
|
traceEnt->client->playerTeam = saveTeam;
|
|
//FIXME: need a *charmed* timer on this...? Or do TEAM_PLAYERS assume that "confusion" means they should switch to team_enemy when done?
|
|
traceEnt->NPC->charmedTime = level.time + mindTrickTime[self->client->ps.forcePowerLevel[FP_TELEPATHY]];
|
|
if ( traceEnt->ghoul2.size() && traceEnt->headBolt != -1 )
|
|
{//FIXME: what if already playing effect?
|
|
G_PlayEffect( G_EffectIndex( "force/confusion" ), traceEnt->playerModel, traceEnt->headBolt, traceEnt->s.number, traceEnt->currentOrigin, mindTrickTime[self->client->ps.forcePowerLevel[FP_TELEPATHY]], qtrue );
|
|
}
|
|
}
|
|
else
|
|
{//just confuse them
|
|
//somehow confuse them? Set don't fire to true for a while? Drop their aggression? Maybe just take their enemy away and don't let them pick one up for a while unless shot?
|
|
traceEnt->NPC->confusionTime = level.time + mindTrickTime[self->client->ps.forcePowerLevel[FP_TELEPATHY]];//confused for about 10 seconds
|
|
if ( traceEnt->ghoul2.size() && traceEnt->headBolt != -1 )
|
|
{//FIXME: what if already playing effect?
|
|
G_PlayEffect( G_EffectIndex( "force/confusion" ), traceEnt->playerModel, traceEnt->headBolt, traceEnt->s.number, traceEnt->currentOrigin, mindTrickTime[self->client->ps.forcePowerLevel[FP_TELEPATHY]], qtrue );
|
|
}
|
|
NPC_PlayConfusionSound( traceEnt );
|
|
if ( traceEnt->enemy )
|
|
{
|
|
G_ClearEnemy( traceEnt );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NPC_Jedi_PlayConfusionSound( traceEnt );
|
|
}
|
|
WP_ForcePowerStart( self, FP_TELEPATHY, override );
|
|
}
|
|
else if ( traceEnt->client->playerTeam == self->client->playerTeam )
|
|
{//an ally
|
|
//maybe just have him look at you? Respond? Take your enemy?
|
|
if ( traceEnt->client->ps.pm_type < PM_DEAD && traceEnt->NPC!=NULL && !(traceEnt->NPC->scriptFlags&SCF_NO_RESPONSE) )
|
|
{
|
|
NPC_UseResponse( traceEnt, self, qfalse );
|
|
WP_ForcePowerStart( self, FP_TELEPATHY, 1 );
|
|
}
|
|
}//NOTE: no effect on TEAM_NEUTRAL?
|
|
vec3_t eyeDir;
|
|
AngleVectors( traceEnt->client->renderInfo.eyeAngles, eyeDir, NULL, NULL );
|
|
VectorNormalize( eyeDir );
|
|
G_PlayEffect( "force/force_touch", traceEnt->client->renderInfo.eyePoint, eyeDir );
|
|
|
|
//make sure this plays and that you cannot press fire for about 1 second after this
|
|
//FIXME: BOTH_FORCEMINDTRICK or BOTH_FORCEDISTRACT
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_MINDTRICK1, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_RESTART|SETANIM_FLAG_HOLD );
|
|
//FIXME: build-up or delay this until in proper part of anim
|
|
}
|
|
else
|
|
{
|
|
if ( self->client->ps.forcePowerLevel[FP_TELEPATHY] > FORCE_LEVEL_1 && tr.fraction * 2048 > 64 )
|
|
{//don't create a diversion less than 64 from you of if at power level 1
|
|
//use distraction anim instead
|
|
G_PlayEffect( G_EffectIndex( "force/force_touch" ), tr.endpos, tr.plane.normal );
|
|
//FIXME: these events don't seem to always be picked up...?
|
|
AddSoundEvent( self, tr.endpos, 512, AEL_SUSPICIOUS, qtrue, qtrue );
|
|
AddSightEvent( self, tr.endpos, 512, AEL_SUSPICIOUS, 50 );
|
|
WP_ForcePowerStart( self, FP_TELEPATHY, 0 );
|
|
}
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_MINDTRICK2, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_RESTART|SETANIM_FLAG_HOLD );
|
|
}
|
|
self->client->ps.saberMove = self->client->ps.saberBounceMove = LS_READY;//don't finish whatever saber anim you may have been in
|
|
self->client->ps.saberBlocked = BLOCKED_NONE;
|
|
self->client->ps.weaponTime = 1000;
|
|
if ( self->client->ps.forcePowersActive&(1<<FP_SPEED) )
|
|
{
|
|
self->client->ps.weaponTime = floor( self->client->ps.weaponTime * g_timescale->value );
|
|
}
|
|
}
|
|
|
|
//rww - RAGDOLL_BEGIN
|
|
//#define JK2_RAGDOLL_GRIPNOHEALTH
|
|
//rww - RAGDOLL_END
|
|
|
|
void ForceGrip( gentity_t *self )
|
|
{//FIXME: make enemy Jedi able to use this
|
|
trace_t tr;
|
|
vec3_t end, forward;
|
|
gentity_t *traceEnt = NULL;
|
|
|
|
if ( self->health <= 0 )
|
|
{
|
|
return;
|
|
}
|
|
if ( !self->s.number && (cg.zoomMode || in_camera) )
|
|
{//can't force grip when zoomed in or in cinematic
|
|
return;
|
|
}
|
|
if ( self->client->ps.leanofs )
|
|
{//can't force-grip while leaning
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.forceGripEntityNum <= ENTITYNUM_WORLD )
|
|
{//already gripping
|
|
if ( self->client->ps.forcePowerLevel[FP_GRIP] > FORCE_LEVEL_1 )
|
|
{
|
|
self->client->ps.forcePowerDuration[FP_GRIP] = level.time + 100;
|
|
self->client->ps.weaponTime = 1000;
|
|
if ( self->client->ps.forcePowersActive&(1<<FP_SPEED) )
|
|
{
|
|
self->client->ps.weaponTime = floor( self->client->ps.weaponTime * g_timescale->value );
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ( !WP_ForcePowerUsable( self, FP_GRIP, 0 ) )
|
|
{//can't use it right now
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.forcePower < 26 )
|
|
{//need 20 to start, 6 to hold it for any decent amount of time...
|
|
if (self->client->ps.clientNum == 0)
|
|
{
|
|
CG_CenterPrint("Force Power Low", 440);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.weaponTime )
|
|
{//busy
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.saberLockTime > level.time )
|
|
{//FIXME: can this be a way to break out?
|
|
return;
|
|
}
|
|
//Cause choking anim + health drain in ent in front of me
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCEGRIP_HOLD, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
self->client->ps.saberMove = self->client->ps.saberBounceMove = LS_READY;//don't finish whatever saber anim you may have been in
|
|
self->client->ps.saberBlocked = BLOCKED_NONE;
|
|
|
|
self->client->ps.weaponTime = 1000;
|
|
if ( self->client->ps.forcePowersActive&(1<<FP_SPEED) )
|
|
{
|
|
self->client->ps.weaponTime = floor( self->client->ps.weaponTime * g_timescale->value );
|
|
}
|
|
|
|
vec3_t origin, angles;
|
|
if (self->client->ps.clientNum == 0 && !cg.renderingThirdPerson)
|
|
{
|
|
BG_CalculateVROffHandPosition(origin, angles);
|
|
AngleVectors(angles, forward, NULL, NULL);
|
|
}
|
|
else
|
|
{
|
|
AngleVectors(self->client->ps.viewangles, forward, NULL, NULL);
|
|
VectorCopy(self->client->ps.viewangles, angles);
|
|
VectorCopy(self->client->renderInfo.eyePoint, origin);
|
|
}
|
|
|
|
VectorNormalize( forward );
|
|
VectorMA( self->client->renderInfo.handLPoint, FORCE_GRIP_DIST, forward, end );
|
|
|
|
if ( self->enemy )
|
|
{//I have an enemy
|
|
if ( !self->enemy->message
|
|
&& !(self->flags&FL_NO_KNOCKBACK) )
|
|
{//don't auto-pickup guys with keys
|
|
if ( DistanceSquared( self->enemy->currentOrigin, self->currentOrigin ) < FORCE_GRIP_DIST_SQUARED )
|
|
{//close enough to grab
|
|
float minDot = 0.5f;
|
|
if ( self->s.number < MAX_CLIENTS )
|
|
{//player needs to be facing more directly
|
|
minDot = 0.2f;
|
|
}
|
|
if ( InFront( self->enemy->currentOrigin, origin, angles, minDot ) ) //self->s.number || //NPCs can always lift enemy since we assume they're looking at them...?
|
|
{//need to be facing the enemy
|
|
if ( gi.inPVS( self->enemy->currentOrigin, origin ) )
|
|
{//must be in PVS
|
|
gi.trace( &tr, origin, vec3_origin, vec3_origin, self->enemy->currentOrigin, self->s.number, MASK_SHOT, (EG2_Collision)0, 0 );
|
|
if ( tr.fraction == 1.0f || tr.entityNum == self->enemy->s.number )
|
|
{//must have clear LOS
|
|
traceEnt = self->enemy;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ( !traceEnt )
|
|
{//okay, trace straight ahead and see what's there
|
|
gi.trace( &tr, self->client->renderInfo.handLPoint, vec3_origin, vec3_origin, end, self->s.number, MASK_SHOT, (EG2_Collision)0, 0 );
|
|
if ( tr.entityNum >= ENTITYNUM_WORLD || tr.fraction == 1.0 || tr.allsolid || tr.startsolid )
|
|
{
|
|
return;
|
|
}
|
|
|
|
traceEnt = &g_entities[tr.entityNum];
|
|
}
|
|
//rww - RAGDOLL_BEGIN
|
|
#ifdef JK2_RAGDOLL_GRIPNOHEALTH
|
|
if ( !traceEnt || traceEnt == self/*???*/ || traceEnt->bmodel || (traceEnt->NPC && traceEnt->NPC->scriptFlags & SCF_NO_FORCE) )
|
|
{
|
|
return;
|
|
}
|
|
#else
|
|
//rww - RAGDOLL_END
|
|
if ( !traceEnt || traceEnt == self/*???*/ || traceEnt->bmodel || (traceEnt->health <= 0 && traceEnt->takedamage) || (traceEnt->NPC && traceEnt->NPC->scriptFlags & SCF_NO_FORCE) )
|
|
{
|
|
return;
|
|
}
|
|
//rww - RAGDOLL_BEGIN
|
|
#endif
|
|
//rww - RAGDOLL_END
|
|
|
|
if ( traceEnt->m_pVehicle != NULL )
|
|
{//is it a vehicle
|
|
//grab pilot if there is one
|
|
if ( traceEnt->m_pVehicle->m_pPilot != NULL
|
|
&& traceEnt->m_pVehicle->m_pPilot->client != NULL )
|
|
{//grip the pilot
|
|
traceEnt = traceEnt->m_pVehicle->m_pPilot;
|
|
}
|
|
else
|
|
{//can't grip a vehicle
|
|
return;
|
|
}
|
|
}
|
|
if ( traceEnt->client )
|
|
{
|
|
if ( traceEnt->client->ps.forceJumpZStart )
|
|
{//can't catch them in mid force jump - FIXME: maybe base it on velocity?
|
|
return;
|
|
}
|
|
if ( traceEnt->client->ps.pullAttackTime > level.time )
|
|
{//can't grip someone who is being pull-attacked or is pull-attacking
|
|
return;
|
|
}
|
|
if ( !Q_stricmp("Yoda",traceEnt->NPC_type) )
|
|
{
|
|
Jedi_PlayDeflectSound( traceEnt );
|
|
ForceThrowEx( traceEnt, qfalse, qfalse, qtrue );
|
|
return;
|
|
}
|
|
|
|
if ( G_IsRidingVehicle( traceEnt )
|
|
&& (traceEnt->s.eFlags&EF_NODRAW) )
|
|
{//riding *inside* vehicle
|
|
return;
|
|
}
|
|
|
|
switch ( traceEnt->client->NPC_class )
|
|
{
|
|
case CLASS_GALAKMECH://cant grip him, he's in armor
|
|
G_AddVoiceEvent( traceEnt, Q_irand(EV_PUSHED1, EV_PUSHED3), Q_irand( 3000, 5000 ) );
|
|
return;
|
|
break;
|
|
case CLASS_HAZARD_TROOPER://cant grip him, he's in armor
|
|
return;
|
|
break;
|
|
case CLASS_ATST://much too big to grip!
|
|
case CLASS_RANCOR://much too big to grip!
|
|
case CLASS_WAMPA://much too big to grip!
|
|
case CLASS_SAND_CREATURE://much too big to grip!
|
|
return;
|
|
break;
|
|
//no droids either...?
|
|
case CLASS_GONK:
|
|
case CLASS_R2D2:
|
|
case CLASS_R5D2:
|
|
case CLASS_MARK1:
|
|
case CLASS_MARK2:
|
|
case CLASS_MOUSE://?
|
|
case CLASS_PROTOCOL:
|
|
//*sigh*... in JK3, you'll be able to grab and move *anything*...
|
|
return;
|
|
break;
|
|
//not even combat droids? (No animation for being gripped...)
|
|
case CLASS_SABER_DROID:
|
|
case CLASS_ASSASSIN_DROID:
|
|
//*sigh*... in JK3, you'll be able to grab and move *anything*...
|
|
return;
|
|
break;
|
|
case CLASS_PROBE:
|
|
case CLASS_SEEKER:
|
|
case CLASS_REMOTE:
|
|
case CLASS_SENTRY:
|
|
case CLASS_INTERROGATOR:
|
|
//*sigh*... in JK3, you'll be able to grab and move *anything*...
|
|
return;
|
|
break;
|
|
case CLASS_DESANN://Desann cannot be gripped, he just pushes you back instantly
|
|
case CLASS_KYLE:
|
|
case CLASS_TAVION:
|
|
case CLASS_LUKE:
|
|
Jedi_PlayDeflectSound( traceEnt );
|
|
ForceThrowEx( traceEnt, qfalse, qfalse, qtrue );
|
|
return;
|
|
break;
|
|
case CLASS_REBORN:
|
|
case CLASS_SHADOWTROOPER:
|
|
case CLASS_ALORA:
|
|
case CLASS_JEDI:
|
|
if ( traceEnt->NPC && traceEnt->NPC->rank > RANK_CIVILIAN && self->client->ps.forcePowerLevel[FP_GRIP] < FORCE_LEVEL_2 )
|
|
{
|
|
Jedi_PlayDeflectSound( traceEnt );
|
|
ForceThrowEx( traceEnt, qfalse, qfalse, qtrue );
|
|
return;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if ( traceEnt->s.weapon == WP_EMPLACED_GUN )
|
|
{//FIXME: maybe can pull them out?
|
|
return;
|
|
}
|
|
if ( self->enemy && traceEnt != self->enemy && traceEnt->client->playerTeam == self->client->playerTeam )
|
|
{//can't accidently grip your teammate in combat
|
|
return;
|
|
}
|
|
//=CHECKABSORB===
|
|
if ( -1 != WP_AbsorbConversion( traceEnt, traceEnt->client->ps.forcePowerLevel[FP_ABSORB], self, FP_GRIP, self->client->ps.forcePowerLevel[FP_GRIP], forcePowerNeeded[self->client->ps.forcePowerLevel[FP_GRIP]]) )
|
|
{
|
|
//WP_ForcePowerStop( self, FP_GRIP );
|
|
return;
|
|
}
|
|
//===============
|
|
}
|
|
else
|
|
{//can't grip non-clients... right?
|
|
//FIXME: Make it so objects flagged as "grabbable" are let through
|
|
//if ( Q_stricmp( "misc_model_breakable", traceEnt->classname ) || !(traceEnt->s.eFlags&EF_BOUNCE_HALF) || !traceEnt->physicsBounce )
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Make sure to turn off Force Protection and Force Absorb.
|
|
if (self->client->ps.forcePowersActive & (1 << FP_PROTECT) )
|
|
{
|
|
WP_ForcePowerStop( self, FP_PROTECT );
|
|
}
|
|
if (self->client->ps.forcePowersActive & (1 << FP_ABSORB) )
|
|
{
|
|
WP_ForcePowerStop( self, FP_ABSORB );
|
|
}
|
|
|
|
WP_ForcePowerStart( self, FP_GRIP, 20 );
|
|
//FIXME: rule out other things?
|
|
//FIXME: Jedi resist, like the push and pull?
|
|
self->client->ps.forceGripEntityNum = traceEnt->s.number;
|
|
if ( traceEnt->client )
|
|
{
|
|
Vehicle_t *pVeh;
|
|
if ( ( pVeh = G_IsRidingVehicle( traceEnt ) ) != NULL )
|
|
{//riding vehicle? pull him off!
|
|
//FIXME: if in an AT-ST or X-Wing, shouldn't do this... :)
|
|
//pull him off of it
|
|
//((CVehicleNPC *)traceEnt->NPC)->Eject( traceEnt );
|
|
pVeh->m_pVehicleInfo->Eject( pVeh, traceEnt, qtrue );
|
|
//G_DriveVehicle( traceEnt, NULL, NULL );
|
|
}
|
|
G_AddVoiceEvent( traceEnt, Q_irand(EV_PUSHED1, EV_PUSHED3), 2000 );
|
|
if ( self->client->ps.forcePowerLevel[FP_GRIP] > FORCE_LEVEL_2 || traceEnt->s.weapon == WP_SABER )
|
|
{//if we pick up & carry, drop their weap
|
|
if ( traceEnt->s.weapon
|
|
&& traceEnt->client->NPC_class != CLASS_ROCKETTROOPER
|
|
&& traceEnt->client->NPC_class != CLASS_VEHICLE
|
|
&& traceEnt->client->NPC_class != CLASS_HAZARD_TROOPER
|
|
&& traceEnt->client->NPC_class != CLASS_TUSKEN
|
|
&& traceEnt->client->NPC_class != CLASS_BOBAFETT
|
|
&& traceEnt->client->NPC_class != CLASS_ASSASSIN_DROID
|
|
&& traceEnt->s.weapon != WP_CONCUSSION // so rax can't drop his
|
|
)
|
|
{
|
|
if ( traceEnt->client->NPC_class == CLASS_BOBAFETT )
|
|
{//he doesn't drop them, just puts it away
|
|
ChangeWeapon( traceEnt, WP_MELEE );
|
|
}
|
|
else if ( traceEnt->s.weapon == WP_MELEE )
|
|
{//they can't take that away from me, oh no...
|
|
}
|
|
else if ( traceEnt->NPC
|
|
&& (traceEnt->NPC->scriptFlags&SCF_DONT_FLEE) )
|
|
{//*SIGH*... if an NPC can't flee, they can't run after and pick up their weapon, do don't drop it
|
|
}
|
|
else if ( traceEnt->s.weapon != WP_SABER )
|
|
{
|
|
WP_DropWeapon( traceEnt, NULL );
|
|
}
|
|
else
|
|
{
|
|
//turn it off?
|
|
traceEnt->client->ps.SaberDeactivate();
|
|
G_SoundOnEnt( traceEnt, CHAN_WEAPON, "sound/weapons/saber/saberoffquick.wav" );
|
|
}
|
|
}
|
|
}
|
|
//else FIXME: need a one-armed choke if we're not on a high enough level to make them drop their gun
|
|
VectorCopy( traceEnt->client->renderInfo.headPoint, self->client->ps.forceGripOrg );
|
|
}
|
|
else
|
|
{
|
|
VectorCopy( traceEnt->currentOrigin, self->client->ps.forceGripOrg );
|
|
}
|
|
self->client->ps.forceGripOrg[2] += 48;//FIXME: define?
|
|
if ( self->client->ps.forcePowerLevel[FP_GRIP] < FORCE_LEVEL_2 )
|
|
{//just a duration
|
|
self->client->ps.forcePowerDebounce[FP_GRIP] = level.time + 250;
|
|
self->client->ps.forcePowerDuration[FP_GRIP] = level.time + 5000;
|
|
|
|
if ( self->m_pVehicle && self->m_pVehicle->m_pVehicleInfo->Inhabited( self->m_pVehicle ) )
|
|
{//empty vehicles don't make gripped noise
|
|
traceEnt->s.loopSound = G_SoundIndex( "sound/weapons/force/grip.mp3" );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( self->client->ps.forcePowerLevel[FP_GRIP] == FORCE_LEVEL_2 )
|
|
{//lifting sound? or always?
|
|
}
|
|
//if ( traceEnt->s.number )
|
|
{//picks them up for a second first
|
|
self->client->ps.forcePowerDebounce[FP_GRIP] = level.time + 1000;
|
|
}
|
|
/*
|
|
else
|
|
{//player should take damage right away
|
|
self->client->ps.forcePowerDebounce[FP_GRIP] = level.time + 250;
|
|
}
|
|
*/
|
|
// force grip sound should only play when the target is alive?
|
|
// if (traceEnt->health>0)
|
|
// {
|
|
self->s.loopSound = G_SoundIndex( "sound/weapons/force/grip.mp3" );
|
|
// }
|
|
}
|
|
}
|
|
|
|
qboolean ForceLightningCheck2Handed( gentity_t *self )
|
|
{
|
|
if ( self && self->client )
|
|
{
|
|
if ( self->s.weapon == WP_NONE
|
|
|| self->s.weapon == WP_MELEE
|
|
|| (self->s.weapon == WP_SABER && !self->client->ps.SaberActive()) )
|
|
{
|
|
return qtrue;
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
void ForceLightningAnim( gentity_t *self )
|
|
{
|
|
if ( !self || !self->client )
|
|
{
|
|
return;
|
|
}
|
|
|
|
//one-handed lightning 2 and above
|
|
int startAnim = BOTH_FORCELIGHTNING_START;
|
|
int holdAnim = BOTH_FORCELIGHTNING_HOLD;
|
|
|
|
if ( self->client->ps.forcePowerLevel[FP_LIGHTNING] >= FORCE_LEVEL_3
|
|
&& ForceLightningCheck2Handed( self ) )
|
|
{//empty handed lightning 3
|
|
startAnim = BOTH_FORCE_2HANDEDLIGHTNING_START;
|
|
holdAnim = BOTH_FORCE_2HANDEDLIGHTNING_HOLD;
|
|
}
|
|
|
|
//FIXME: if standing still, play on whole body? Especially 2-handed version
|
|
if ( self->client->ps.torsoAnim == startAnim )
|
|
{
|
|
if ( !self->client->ps.torsoAnimTimer )
|
|
{
|
|
NPC_SetAnim( self, SETANIM_TORSO, holdAnim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
else
|
|
{
|
|
NPC_SetAnim( self, SETANIM_TORSO, startAnim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NPC_SetAnim( self, SETANIM_TORSO, holdAnim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
}
|
|
|
|
void ForceLightning( gentity_t *self )
|
|
{
|
|
if ( self->health <= 0 )
|
|
{
|
|
return;
|
|
}
|
|
if ( !self->s.number && (cg.zoomMode || in_camera) )
|
|
{//can't force lightning when zoomed in or in cinematic
|
|
return;
|
|
}
|
|
if ( self->client->ps.leanofs )
|
|
{//can't force-lightning while leaning
|
|
return;
|
|
}
|
|
if ( self->client->ps.forcePower < 25 || !WP_ForcePowerUsable( self, FP_LIGHTNING, 0 ) )
|
|
{
|
|
if (self->client->ps.clientNum == 0)
|
|
{
|
|
CG_CenterPrint("Force Power Low", 440);
|
|
}
|
|
return;
|
|
}
|
|
if ( self->client->ps.forcePowerDebounce[FP_LIGHTNING] > level.time )
|
|
{//stops it while using it and also after using it, up to 3 second delay
|
|
return;
|
|
}
|
|
if ( self->client->ps.saberLockTime > level.time )
|
|
{//FIXME: can this be a way to break out?
|
|
return;
|
|
}
|
|
// Make sure to turn off Force Protection and Force Absorb.
|
|
if (self->client->ps.forcePowersActive & (1 << FP_PROTECT) )
|
|
{
|
|
WP_ForcePowerStop( self, FP_PROTECT );
|
|
}
|
|
if (self->client->ps.forcePowersActive & (1 << FP_ABSORB) )
|
|
{
|
|
WP_ForcePowerStop( self, FP_ABSORB );
|
|
}
|
|
//Shoot lightning from hand
|
|
//make sure this plays and that you cannot press fire for about 1 second after this
|
|
if ( self->client->ps.forcePowerLevel[FP_LIGHTNING] < FORCE_LEVEL_2 )
|
|
{
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCELIGHTNING, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
else
|
|
{
|
|
ForceLightningAnim( self );
|
|
/*
|
|
if ( ForceLightningCheck2Handed( self ) )
|
|
{//empty handed lightning 3
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCE_2HANDEDLIGHTNING_START, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
else
|
|
{//one-handed lightning 3
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCELIGHTNING_START, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
*/
|
|
}
|
|
self->client->ps.saberMove = self->client->ps.saberBounceMove = LS_READY;//don't finish whatever saber anim you may have been in
|
|
self->client->ps.saberBlocked = BLOCKED_NONE;
|
|
|
|
G_SoundOnEnt( self, CHAN_BODY, "sound/weapons/force/lightning.wav" );
|
|
if ( self->client->ps.forcePowerLevel[FP_LIGHTNING] < FORCE_LEVEL_2 )
|
|
{//short burst
|
|
//G_SoundOnEnt( self, CHAN_BODY, "sound/weapons/force/lightning.wav" );
|
|
}
|
|
else
|
|
{//holding it
|
|
self->s.loopSound = G_SoundIndex( "sound/weapons/force/lightning2.wav" );
|
|
}
|
|
|
|
//FIXME: build-up or delay this until in proper part of anim
|
|
self->client->ps.weaponTime = self->client->ps.torsoAnimTimer;
|
|
WP_ForcePowerStart( self, FP_LIGHTNING, self->client->ps.torsoAnimTimer );
|
|
}
|
|
|
|
void ForceLightningDamage( gentity_t *self, gentity_t *traceEnt, vec3_t dir, float dist, float dot, vec3_t impactPoint )
|
|
{
|
|
if( traceEnt->NPC && traceEnt->NPC->scriptFlags & SCF_NO_FORCE )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( traceEnt && traceEnt->takedamage )
|
|
{
|
|
if ( !traceEnt->client || traceEnt->client->playerTeam != self->client->playerTeam || self->enemy == traceEnt || traceEnt->enemy == self )
|
|
{//an enemy or object
|
|
int dmg;
|
|
//FIXME: check for client using FP_ABSORB
|
|
if ( self->client->ps.forcePowerLevel[FP_LIGHTNING] > FORCE_LEVEL_2 )
|
|
{//more damage if closer and more in front
|
|
dmg = 1;
|
|
if ( self->client->NPC_class == CLASS_REBORN
|
|
&& self->client->ps.weapon == WP_NONE )
|
|
{//Cultist: looks fancy, but does less damage
|
|
}
|
|
else
|
|
{
|
|
if ( dist < 100 )
|
|
{
|
|
dmg += 2;
|
|
}
|
|
else if ( dist < 200 )
|
|
{
|
|
dmg += 1;
|
|
}
|
|
if ( dot > 0.9f )
|
|
{
|
|
dmg += 2;
|
|
}
|
|
else if ( dot > 0.7f )
|
|
{
|
|
dmg += 1;
|
|
}
|
|
}
|
|
if ( self->client->ps.torsoAnim == BOTH_FORCE_2HANDEDLIGHTNING
|
|
|| self->client->ps.torsoAnim == BOTH_FORCE_2HANDEDLIGHTNING_START
|
|
|| self->client->ps.torsoAnim == BOTH_FORCE_2HANDEDLIGHTNING_HOLD
|
|
|| self->client->ps.torsoAnim == BOTH_FORCE_2HANDEDLIGHTNING_RELEASE )
|
|
{//jackin' 'em up, Palpatine-style
|
|
dmg *= 2;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dmg = Q_irand( 1, 3 );//*self->client->ps.forcePowerLevel[FP_LIGHTNING];
|
|
}
|
|
|
|
if ( traceEnt->client
|
|
&& traceEnt->health > 0
|
|
&& traceEnt->NPC
|
|
&& (traceEnt->NPC->aiFlags&NPCAI_BOSS_CHARACTER) )
|
|
{//Luke, Desann Tavion and Kyle can shield themselves from the attack
|
|
//FIXME: shield effect or something?
|
|
int parts;
|
|
if ( traceEnt->client->ps.groundEntityNum != ENTITYNUM_NONE && !PM_SpinningSaberAnim( traceEnt->client->ps.legsAnim ) && !PM_FlippingAnim( traceEnt->client->ps.legsAnim ) && !PM_RollingAnim( traceEnt->client->ps.legsAnim ) )
|
|
{//if on a surface and not in a spin or flip, play full body resist
|
|
parts = SETANIM_BOTH;
|
|
}
|
|
else
|
|
{//play resist just in torso
|
|
parts = SETANIM_TORSO;
|
|
}
|
|
//FIXME: don't interrupt big anims with this!
|
|
NPC_SetAnim( traceEnt, parts, BOTH_RESISTPUSH, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
Jedi_PlayDeflectSound( traceEnt );
|
|
dmg = Q_irand(0,1);
|
|
}
|
|
else if ( traceEnt->s.weapon == WP_SABER )
|
|
{//saber can block lightning
|
|
if ( traceEnt->client //a client
|
|
&& !traceEnt->client->ps.saberInFlight//saber in hand
|
|
&& ( traceEnt->client->ps.saberMove == LS_READY || PM_SaberInParry( traceEnt->client->ps.saberMove ) || PM_SaberInReturn( traceEnt->client->ps.saberMove ) )//not attacking with saber
|
|
&& InFOV( self->currentOrigin, traceEnt->currentOrigin, traceEnt->client->ps.viewangles, 20, 35 ) //I'm in front of them
|
|
&& !PM_InKnockDown( &traceEnt->client->ps ) //they're not in a knockdown
|
|
&& !PM_SuperBreakLoseAnim( traceEnt->client->ps.torsoAnim )
|
|
&& !PM_SuperBreakWinAnim( traceEnt->client->ps.torsoAnim )
|
|
&& !PM_SaberInSpecialAttack( traceEnt->client->ps.torsoAnim )
|
|
&& !PM_InSpecialJump( traceEnt->client->ps.torsoAnim )
|
|
&& (!traceEnt->s.number||(traceEnt->NPC&&traceEnt->NPC->rank>=RANK_LT_COMM)) )//the player or a tough jedi/reborn
|
|
{
|
|
if ( Q_irand( 0, traceEnt->client->ps.forcePowerLevel[FP_SABER_DEFENSE]*3 ) > 0 )//more of a chance of defending if saber defense is high
|
|
{
|
|
dmg = 0;
|
|
}
|
|
if ( (traceEnt->client->ps.forcePowersActive&(1<<FP_ABSORB))
|
|
&& traceEnt->client->ps.forcePowerLevel[FP_ABSORB] > FORCE_LEVEL_2 )
|
|
{//no parry, just absorb
|
|
}
|
|
else
|
|
{
|
|
//make them do a parry
|
|
traceEnt->client->ps.saberBlocked = BLOCKED_UPPER_LEFT;
|
|
int parryReCalcTime = Jedi_ReCalcParryTime( traceEnt, EVASION_PARRY );
|
|
if ( traceEnt->client->ps.forcePowerDebounce[FP_SABER_DEFENSE] < level.time + parryReCalcTime )
|
|
{
|
|
traceEnt->client->ps.forcePowerDebounce[FP_SABER_DEFENSE] = level.time + parryReCalcTime;
|
|
}
|
|
traceEnt->client->ps.weaponTime = Q_irand( 100, 300 );//hold this move - can't attack! - FIXME: unless dual sabers?
|
|
}
|
|
}
|
|
else if ( Q_irand( 0, 1 ) )
|
|
{//jedi less likely to be damaged
|
|
dmg = 0;
|
|
}
|
|
else
|
|
{
|
|
dmg = 1;
|
|
}
|
|
}
|
|
if ( traceEnt && traceEnt->client && traceEnt->client->ps.powerups[PW_GALAK_SHIELD] )
|
|
{
|
|
//has shield up
|
|
dmg = 0;
|
|
}
|
|
int modPowerLevel = -1;
|
|
|
|
if (traceEnt->client)
|
|
{
|
|
modPowerLevel = WP_AbsorbConversion(traceEnt, traceEnt->client->ps.forcePowerLevel[FP_ABSORB], self, FP_LIGHTNING, self->client->ps.forcePowerLevel[FP_LIGHTNING], 1);
|
|
}
|
|
|
|
if (modPowerLevel != -1)
|
|
{
|
|
if ( !modPowerLevel )
|
|
{
|
|
dmg = 0;
|
|
}
|
|
else if ( modPowerLevel == 1 )
|
|
{
|
|
dmg = floor((float)dmg/4.0f);
|
|
}
|
|
else if ( modPowerLevel == 2 )
|
|
{
|
|
dmg = floor((float)dmg/2.0f);
|
|
}
|
|
}
|
|
//FIXME: if ForceDrain, sap force power and add health to self, use different sound & effects
|
|
if ( dmg )
|
|
{
|
|
G_Damage( traceEnt, self, self, dir, impactPoint, dmg, 0, MOD_FORCE_LIGHTNING );
|
|
}
|
|
if ( traceEnt->client )
|
|
{
|
|
if ( !Q_irand( 0, 2 ) )
|
|
{
|
|
G_Sound( traceEnt, G_SoundIndex( va( "sound/weapons/force/lightninghit%d.wav", Q_irand( 1, 3 ) ) ) );
|
|
}
|
|
traceEnt->s.powerups |= ( 1 << PW_SHOCKED );
|
|
|
|
// If we are dead or we are a bot, we can do the full version
|
|
class_t npc_class = traceEnt->client->NPC_class;
|
|
if ( traceEnt->health <= 0 || ( npc_class == CLASS_SEEKER || npc_class == CLASS_PROBE ||
|
|
npc_class == CLASS_MOUSE || npc_class == CLASS_GONK || npc_class == CLASS_R2D2 || npc_class == CLASS_REMOTE ||
|
|
npc_class == CLASS_R5D2 || npc_class == CLASS_PROTOCOL || npc_class == CLASS_MARK1 ||
|
|
npc_class == CLASS_MARK2 || npc_class == CLASS_INTERROGATOR || npc_class == CLASS_ATST ) ||
|
|
npc_class == CLASS_SENTRY )
|
|
{
|
|
traceEnt->client->ps.powerups[PW_SHOCKED] = level.time + 4000;
|
|
}
|
|
else //short version
|
|
{
|
|
traceEnt->client->ps.powerups[PW_SHOCKED] = level.time + 500;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ForceShootLightning( gentity_t *self )
|
|
{
|
|
trace_t tr;
|
|
vec3_t end, forward;
|
|
gentity_t *traceEnt;
|
|
|
|
if ( self->health <= 0 )
|
|
{
|
|
return;
|
|
}
|
|
if ( !self->s.number && cg.zoomMode )
|
|
{//can't force lightning when zoomed in
|
|
return;
|
|
}
|
|
|
|
if (self->client->ps.clientNum == 0 && !cg.renderingThirdPerson)
|
|
{
|
|
vec3_t origin, angles;
|
|
BG_CalculateVROffHandPosition(origin, angles);
|
|
AngleVectors(angles, forward, NULL, NULL);
|
|
}
|
|
else
|
|
{
|
|
AngleVectors(self->client->ps.viewangles, forward, NULL, NULL);
|
|
}
|
|
|
|
VectorNormalize( forward );
|
|
|
|
if (self->client->ps.clientNum == 0)
|
|
{
|
|
//Handle this here so it is refreshed on every frame, not just when the lightning gun is first fired
|
|
cgi_HapticEvent("RTCWQuest:fire_tesla", 0, (vr->right_handed ? 2 : 1), 100, 0, 0);
|
|
}
|
|
|
|
//FIXME: if lightning hits water, do water-only-flagged radius damage from that point
|
|
if ( self->client->ps.forcePowerLevel[FP_LIGHTNING] > FORCE_LEVEL_2 )
|
|
{//arc
|
|
vec3_t center, mins, maxs, dir, ent_org, size, v;
|
|
float radius = 512, dot, dist;
|
|
gentity_t *entityList[MAX_GENTITIES];
|
|
int e, numListedEntities, i;
|
|
|
|
VectorCopy( self->currentOrigin, center );
|
|
for ( i = 0 ; i < 3 ; i++ )
|
|
{
|
|
mins[i] = center[i] - radius;
|
|
maxs[i] = center[i] + radius;
|
|
}
|
|
numListedEntities = gi.EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
|
|
|
|
for ( e = 0 ; e < numListedEntities ; e++ )
|
|
{
|
|
traceEnt = entityList[e];
|
|
|
|
if ( !traceEnt )
|
|
continue;
|
|
if ( traceEnt == self )
|
|
continue;
|
|
if ( traceEnt->owner == self && traceEnt->s.weapon != WP_THERMAL )//can push your own thermals
|
|
continue;
|
|
if ( !traceEnt->inuse )
|
|
continue;
|
|
if ( !traceEnt->takedamage )
|
|
continue;
|
|
/*
|
|
if ( traceEnt->health <= 0 )//no torturing corpses
|
|
continue;
|
|
*/
|
|
//this is all to see if we need to start a saber attack, if it's in flight, this doesn't matter
|
|
// find the distance from the edge of the bounding box
|
|
for ( i = 0 ; i < 3 ; i++ )
|
|
{
|
|
if ( center[i] < traceEnt->absmin[i] )
|
|
{
|
|
v[i] = traceEnt->absmin[i] - center[i];
|
|
} else if ( center[i] > traceEnt->absmax[i] )
|
|
{
|
|
v[i] = center[i] - traceEnt->absmax[i];
|
|
} else
|
|
{
|
|
v[i] = 0;
|
|
}
|
|
}
|
|
|
|
VectorSubtract( traceEnt->absmax, traceEnt->absmin, size );
|
|
VectorMA( traceEnt->absmin, 0.5, size, ent_org );
|
|
|
|
//see if they're in front of me
|
|
//must be within the forward cone
|
|
VectorSubtract( ent_org, center, dir );
|
|
VectorNormalize( dir );
|
|
if ( (dot = DotProduct( dir, forward )) < 0.5 )
|
|
continue;
|
|
|
|
//must be close enough
|
|
dist = VectorLength( v );
|
|
if ( dist >= radius )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
//in PVS?
|
|
if ( !traceEnt->bmodel && !gi.inPVS( ent_org, self->client->renderInfo.handLPoint ) )
|
|
{//must be in PVS
|
|
continue;
|
|
}
|
|
|
|
//Now check and see if we can actually hit it
|
|
gi.trace( &tr, self->client->renderInfo.handLPoint, vec3_origin, vec3_origin, ent_org, self->s.number, MASK_SHOT, (EG2_Collision)0, 0 );
|
|
if ( tr.fraction < 1.0f && tr.entityNum != traceEnt->s.number )
|
|
{//must have clear LOS
|
|
continue;
|
|
}
|
|
|
|
// ok, we are within the radius, add us to the incoming list
|
|
//FIXME: maybe add up the ents and do more damage the less ents there are
|
|
// as if we're spreading out the damage?
|
|
ForceLightningDamage( self, traceEnt, dir, dist, dot, ent_org );
|
|
}
|
|
|
|
}
|
|
else
|
|
{//trace-line
|
|
int ignore = self->s.number;
|
|
int traces = 0;
|
|
vec3_t start;
|
|
|
|
VectorCopy( self->client->renderInfo.handLPoint, start );
|
|
VectorMA( self->client->renderInfo.handLPoint, 2048, forward, end );
|
|
|
|
while ( traces < 10 )
|
|
{//need to loop this in case we hit a Jedi who dodges the shot
|
|
gi.trace( &tr, start, vec3_origin, vec3_origin, end, ignore, MASK_SHOT, G2_RETURNONHIT, 10 );
|
|
if ( tr.entityNum == ENTITYNUM_NONE || tr.fraction == 1.0 || tr.allsolid || tr.startsolid )
|
|
{
|
|
return;
|
|
}
|
|
|
|
traceEnt = &g_entities[tr.entityNum];
|
|
//NOTE: only NPCs do this auto-dodge
|
|
if ( traceEnt
|
|
&& traceEnt->s.number >= MAX_CLIENTS
|
|
&& traceEnt->client
|
|
&& traceEnt->client->ps.forcePowerLevel[FP_LEVITATION] > FORCE_LEVEL_0 )//&& traceEnt->NPC
|
|
{//FIXME: need a more reliable way to know we hit a jedi?
|
|
if ( !Jedi_DodgeEvasion( traceEnt, self, &tr, HL_NONE ) )
|
|
{//act like we didn't even hit him
|
|
VectorCopy( tr.endpos, start );
|
|
ignore = tr.entityNum;
|
|
traces++;
|
|
continue;
|
|
}
|
|
}
|
|
//a Jedi is not dodging this shot
|
|
break;
|
|
}
|
|
|
|
traceEnt = &g_entities[tr.entityNum];
|
|
ForceLightningDamage( self, traceEnt, forward, 0, 0, tr.endpos );
|
|
}
|
|
}
|
|
|
|
void WP_DeactivateSaber( gentity_t *self, qboolean clearLength )
|
|
{
|
|
if ( !self || !self->client )
|
|
{
|
|
return;
|
|
}
|
|
//keep my saber off!
|
|
if ( self->client->ps.SaberActive() )
|
|
{
|
|
self->client->ps.SaberDeactivate();
|
|
if ( clearLength )
|
|
{
|
|
self->client->ps.SetSaberLength( 0 );
|
|
}
|
|
G_SoundIndexOnEnt( self, CHAN_WEAPON, self->client->ps.saber[0].soundOff );
|
|
}
|
|
}
|
|
|
|
static void ForceShootDrain( gentity_t *self );
|
|
|
|
void ForceDrainGrabStart( gentity_t *self )
|
|
{
|
|
NPC_SetAnim( self, SETANIM_BOTH, BOTH_FORCE_DRAIN_GRAB_START, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
self->client->ps.saberMove = self->client->ps.saberBounceMove = LS_READY;//don't finish whatever saber anim you may have been in
|
|
self->client->ps.saberBlocked = BLOCKED_NONE;
|
|
|
|
self->client->ps.weaponTime = 1000;
|
|
if ( self->client->ps.forcePowersActive&(1<<FP_SPEED) )
|
|
{
|
|
self->client->ps.weaponTime = floor( self->client->ps.weaponTime * g_timescale->value );
|
|
}
|
|
//actually grabbing someone, so turn off the saber!
|
|
WP_DeactivateSaber( self, qtrue );
|
|
}
|
|
|
|
qboolean ForceDrain2( gentity_t *self )
|
|
{//FIXME: make enemy Jedi able to use this
|
|
trace_t tr;
|
|
vec3_t end, forward;
|
|
gentity_t *traceEnt = NULL;
|
|
|
|
if ( self->health <= 0 )
|
|
{
|
|
return qtrue;
|
|
}
|
|
|
|
if ( !self->s.number && (cg.zoomMode || in_camera) )
|
|
{//can't force grip when zoomed in or in cinematic
|
|
return qtrue;
|
|
}
|
|
|
|
if ( self->client->ps.leanofs )
|
|
{//can't force-drain while leaning
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
if ( self->client->ps.SaberLength() > 0 )
|
|
{//can't do this if saber is on!
|
|
return qfalse;
|
|
}
|
|
*/
|
|
|
|
if ( self->client->ps.forceDrainEntityNum <= ENTITYNUM_WORLD )
|
|
{//already draining
|
|
//keep my saber off!
|
|
WP_DeactivateSaber( self, qtrue );
|
|
if ( self->client->ps.forcePowerLevel[FP_DRAIN] > FORCE_LEVEL_1 )
|
|
{
|
|
self->client->ps.forcePowerDuration[FP_DRAIN] = level.time + 100;
|
|
self->client->ps.weaponTime = 1000;
|
|
if ( self->client->ps.forcePowersActive&(1<<FP_SPEED) )
|
|
{
|
|
self->client->ps.weaponTime = floor( self->client->ps.weaponTime * g_timescale->value );
|
|
}
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
if ( self->client->ps.forcePowerDebounce[FP_DRAIN] > level.time )
|
|
{//stops it while using it and also after using it, up to 3 second delay
|
|
return qtrue;
|
|
}
|
|
|
|
if ( self->client->ps.weaponTime > 0 )
|
|
{//busy
|
|
return qtrue;
|
|
}
|
|
|
|
if ( self->client->ps.forcePower < 25 || !WP_ForcePowerUsable( self, FP_DRAIN, 0 ) )
|
|
{
|
|
if (self->client->ps.clientNum == 0)
|
|
{
|
|
CG_CenterPrint("Force Power Low", 440);
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
if ( self->client->ps.saberLockTime > level.time )
|
|
{//in saberlock
|
|
return qtrue;
|
|
}
|
|
|
|
//NOTE: from here on, if it fails, it's okay to try a normal drain, so return qfalse
|
|
if ( self->client->ps.groundEntityNum == ENTITYNUM_NONE )
|
|
{//in air
|
|
return qfalse;
|
|
}
|
|
|
|
//Cause choking anim + health drain in ent in front of me
|
|
vec3_t origin, angles;
|
|
if (self->client->ps.clientNum == 0 && !cg.renderingThirdPerson)
|
|
{
|
|
BG_CalculateVROffHandPosition(origin, angles);
|
|
AngleVectors(angles, forward, NULL, NULL);
|
|
}
|
|
else
|
|
{
|
|
AngleVectors(self->client->ps.viewangles, forward, NULL, NULL);
|
|
VectorCopy(self->client->ps.viewangles, angles);
|
|
VectorCopy(self->client->renderInfo.eyePoint, origin);
|
|
}
|
|
VectorNormalize( forward );
|
|
VectorMA( origin, FORCE_DRAIN_DIST, forward, end );
|
|
|
|
//okay, trace straight ahead and see what's there
|
|
gi.trace( &tr, origin, vec3_origin, vec3_origin, end, self->s.number, MASK_SHOT, (EG2_Collision)0, 0 );
|
|
if ( tr.entityNum >= ENTITYNUM_WORLD || tr.fraction == 1.0 || tr.allsolid || tr.startsolid )
|
|
{
|
|
return qfalse;
|
|
}
|
|
traceEnt = &g_entities[tr.entityNum];
|
|
if ( !traceEnt || traceEnt == self/*???*/ || traceEnt->bmodel || (traceEnt->health <= 0 && traceEnt->takedamage) || (traceEnt->NPC && traceEnt->NPC->scriptFlags & SCF_NO_FORCE) )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
if ( traceEnt->client )
|
|
{
|
|
if ( traceEnt->client->ps.forceJumpZStart )
|
|
{//can't catch them in mid force jump - FIXME: maybe base it on velocity?
|
|
return qfalse;
|
|
}
|
|
if ( traceEnt->client->ps.groundEntityNum == ENTITYNUM_NONE )
|
|
{//can't catch them in mid air
|
|
return qfalse;
|
|
}
|
|
if ( !Q_stricmp("Yoda",traceEnt->NPC_type) )
|
|
{
|
|
Jedi_PlayDeflectSound( traceEnt );
|
|
ForceThrowEx( traceEnt, qfalse, qfalse, qtrue );
|
|
return qtrue;
|
|
}
|
|
switch ( traceEnt->client->NPC_class )
|
|
{
|
|
case CLASS_GALAKMECH://cant grab him, he's in armor
|
|
G_AddVoiceEvent( traceEnt, Q_irand(EV_PUSHED1, EV_PUSHED3), Q_irand( 3000, 5000 ) );
|
|
return qfalse;
|
|
break;
|
|
case CLASS_ROCKETTROOPER://cant grab him, he's in armor
|
|
case CLASS_HAZARD_TROOPER://cant grab him, he's in armor
|
|
return qfalse;
|
|
break;
|
|
case CLASS_ATST://much too big to grab!
|
|
return qfalse;
|
|
break;
|
|
//no droids either
|
|
case CLASS_GONK:
|
|
case CLASS_R2D2:
|
|
case CLASS_R5D2:
|
|
case CLASS_MARK1:
|
|
case CLASS_MARK2:
|
|
case CLASS_MOUSE:
|
|
case CLASS_PROTOCOL:
|
|
case CLASS_SABER_DROID:
|
|
case CLASS_ASSASSIN_DROID:
|
|
return qfalse;
|
|
break;
|
|
case CLASS_PROBE:
|
|
case CLASS_SEEKER:
|
|
case CLASS_REMOTE:
|
|
case CLASS_SENTRY:
|
|
case CLASS_INTERROGATOR:
|
|
return qfalse;
|
|
break;
|
|
case CLASS_DESANN://Desann cannot be gripped, he just pushes you back instantly
|
|
case CLASS_KYLE:
|
|
case CLASS_TAVION:
|
|
case CLASS_LUKE:
|
|
Jedi_PlayDeflectSound( traceEnt );
|
|
ForceThrowEx( traceEnt, qfalse, qfalse, qtrue );
|
|
return qtrue;
|
|
break;
|
|
case CLASS_REBORN:
|
|
case CLASS_SHADOWTROOPER:
|
|
//case CLASS_ALORA:
|
|
case CLASS_JEDI:
|
|
if ( traceEnt->NPC
|
|
&& traceEnt->NPC->rank > RANK_CIVILIAN
|
|
&& self->client->ps.forcePowerLevel[FP_DRAIN] < FORCE_LEVEL_2
|
|
&& traceEnt->client->ps.weaponTime <= 0 )
|
|
{
|
|
ForceDrainGrabStart( self );
|
|
Jedi_PlayDeflectSound( traceEnt );
|
|
ForceThrowEx( traceEnt, qfalse, qfalse, qtrue );
|
|
return qtrue;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if ( traceEnt->s.weapon == WP_EMPLACED_GUN )
|
|
{//FIXME: maybe can pull them out?
|
|
return qfalse;
|
|
}
|
|
if ( traceEnt != self->enemy && OnSameTeam(self, traceEnt) )
|
|
{//can't accidently grip-drain your teammate
|
|
return qfalse;
|
|
}
|
|
//=CHECKABSORB===
|
|
/*
|
|
if ( -1 != WP_AbsorbConversion( traceEnt, traceEnt->client->ps.forcePowerLevel[FP_ABSORB], self, FP_DRAIN, self->client->ps.forcePowerLevel[FP_DRAIN], forcePowerNeeded[self->client->ps.forcePowerLevel[FP_DRAIN]]) )
|
|
{
|
|
//WP_ForcePowerStop( self, FP_DRAIN );
|
|
return;
|
|
}
|
|
*/
|
|
//===============
|
|
if ( !FP_ForceDrainGrippableEnt( traceEnt ) )
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
else
|
|
{//can't drain non-clients
|
|
return qfalse;
|
|
}
|
|
|
|
ForceDrainGrabStart( self );
|
|
|
|
WP_ForcePowerStart( self, FP_DRAIN, 10 );
|
|
self->client->ps.forceDrainEntityNum = traceEnt->s.number;
|
|
|
|
// G_AddVoiceEvent( traceEnt, Q_irand(EV_PUSHED1, EV_PUSHED3), 2000 );
|
|
G_AddVoiceEvent( traceEnt, Q_irand(EV_CHOKE1, EV_CHOKE3), 2000 );
|
|
if ( /*self->client->ps.forcePowerLevel[FP_DRAIN] > FORCE_LEVEL_2 ||*/ traceEnt->s.weapon == WP_SABER )
|
|
{//if we pick up, turn off their weapon
|
|
WP_DeactivateSaber( traceEnt, qtrue );
|
|
}
|
|
|
|
/*
|
|
if ( self->client->ps.forcePowerLevel[FP_DRAIN] < FORCE_LEVEL_2 )
|
|
{//just a duration
|
|
self->client->ps.forcePowerDebounce[FP_DRAIN] = level.time + 250;
|
|
self->client->ps.forcePowerDuration[FP_DRAIN] = level.time + 5000;
|
|
}
|
|
*/
|
|
|
|
G_SoundOnEnt( self, CHAN_BODY, "sound/weapons/force/drain.mp3" );
|
|
|
|
// NPC_SetAnim( traceEnt, SETANIM_BOTH, BOTH_HUGGEE1, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
NPC_SetAnim( traceEnt, SETANIM_BOTH, BOTH_FORCE_DRAIN_GRABBED, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
|
|
WP_SabersCheckLock2( self, traceEnt, LOCK_FORCE_DRAIN );
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
void ForceDrain( gentity_t *self, qboolean triedDrain2 )
|
|
{
|
|
if ( self->health <= 0 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( !triedDrain2 && self->client->ps.weaponTime > 0 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.forcePower < 25 || !WP_ForcePowerUsable( self, FP_DRAIN, 0 ) )
|
|
{
|
|
if (self->client->ps.clientNum == 0)
|
|
{
|
|
CG_CenterPrint("Force Power Low", 440);
|
|
}
|
|
return;
|
|
}
|
|
if ( self->client->ps.forcePowerDebounce[FP_DRAIN] > level.time )
|
|
{//stops it while using it and also after using it, up to 3 second delay
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.saberLockTime > level.time )
|
|
{//FIXME: can this be a way to break out?
|
|
return;
|
|
}
|
|
|
|
// Make sure to turn off Force Protection and Force Absorb.
|
|
if ( self->client->ps.forcePowersActive & (1 << FP_PROTECT) )
|
|
{
|
|
WP_ForcePowerStop( self, FP_PROTECT );
|
|
}
|
|
if ( self->client->ps.forcePowersActive & (1 << FP_ABSORB) )
|
|
{
|
|
WP_ForcePowerStop( self, FP_ABSORB );
|
|
}
|
|
|
|
G_SoundOnEnt( self, CHAN_BODY, "sound/weapons/force/drain.mp3" );
|
|
|
|
WP_ForcePowerStart( self, FP_DRAIN, 0 );
|
|
}
|
|
|
|
|
|
qboolean FP_ForceDrainableEnt( gentity_t *victim )
|
|
{
|
|
if ( !victim || !victim->client )
|
|
{
|
|
return qfalse;
|
|
}
|
|
switch ( victim->client->NPC_class )
|
|
{
|
|
case CLASS_SAND_CREATURE://??
|
|
case CLASS_ATST: // technically droid...
|
|
case CLASS_GONK: // droid
|
|
case CLASS_INTERROGATOR: // droid
|
|
case CLASS_MARK1: // droid
|
|
case CLASS_MARK2: // droid
|
|
case CLASS_GALAKMECH: // droid
|
|
case CLASS_MINEMONSTER:
|
|
case CLASS_MOUSE: // droid
|
|
case CLASS_PROBE: // droid
|
|
case CLASS_PROTOCOL: // droid
|
|
case CLASS_R2D2: // droid
|
|
case CLASS_R5D2: // droid
|
|
case CLASS_REMOTE:
|
|
case CLASS_SEEKER: // droid
|
|
case CLASS_SENTRY:
|
|
case CLASS_SABER_DROID:
|
|
case CLASS_ASSASSIN_DROID:
|
|
case CLASS_VEHICLE:
|
|
return qfalse;
|
|
default:
|
|
break;
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
qboolean FP_ForceDrainGrippableEnt( gentity_t *victim )
|
|
{
|
|
if ( !victim || !victim->client )
|
|
{
|
|
return qfalse;
|
|
}
|
|
if ( !FP_ForceDrainableEnt( victim ) )
|
|
{
|
|
return qfalse;
|
|
}
|
|
switch ( victim->client->NPC_class )
|
|
{
|
|
case CLASS_RANCOR:
|
|
case CLASS_SAND_CREATURE:
|
|
case CLASS_WAMPA:
|
|
case CLASS_LIZARD:
|
|
case CLASS_MINEMONSTER:
|
|
case CLASS_MURJJ:
|
|
case CLASS_SWAMP:
|
|
case CLASS_ROCKETTROOPER:
|
|
case CLASS_HAZARD_TROOPER:
|
|
return qfalse;
|
|
default:
|
|
break;
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
void ForceDrainDamage( gentity_t *self, gentity_t *traceEnt, vec3_t dir, vec3_t impactPoint )
|
|
{
|
|
if ( traceEnt
|
|
&& traceEnt->health > 0
|
|
&& traceEnt->takedamage
|
|
&& FP_ForceDrainableEnt( traceEnt ) )
|
|
{
|
|
if ( traceEnt->client
|
|
&& (!OnSameTeam(self, traceEnt)||self->enemy==traceEnt)//don't drain an ally unless that is actually my current enemy
|
|
&& self->client->ps.forceDrainTime < level.time )
|
|
{//an enemy or object
|
|
int modPowerLevel = -1;
|
|
int dmg = self->client->ps.forcePowerLevel[FP_DRAIN] + 1;
|
|
int dflags = (DAMAGE_NO_ARMOR|DAMAGE_NO_KNOCKBACK|DAMAGE_NO_HIT_LOC);//|DAMAGE_NO_KILL);
|
|
if ( traceEnt->s.number == self->client->ps.forceDrainEntityNum )
|
|
{//grabbing hold of them does more damage/drains more, and can actually kill them
|
|
dmg += 3;
|
|
dflags |= DAMAGE_IGNORE_TEAM;
|
|
//dflags &= ~DAMAGE_NO_KILL;
|
|
}
|
|
|
|
if (traceEnt->client)
|
|
{
|
|
//check for client using FP_ABSORB
|
|
modPowerLevel = WP_AbsorbConversion(traceEnt, traceEnt->client->ps.forcePowerLevel[FP_ABSORB], self, FP_DRAIN, self->client->ps.forcePowerLevel[FP_DRAIN], 0);
|
|
//Since this is drain, don't absorb any power, but nullify the affect it has
|
|
}
|
|
|
|
if ( modPowerLevel != -1 )
|
|
{
|
|
if ( !modPowerLevel )
|
|
{
|
|
dmg = 0;
|
|
}
|
|
else if ( modPowerLevel == 1 )
|
|
{
|
|
dmg = 1;
|
|
}
|
|
else if ( modPowerLevel == 2 )
|
|
{
|
|
dmg = 2;
|
|
}
|
|
}
|
|
|
|
if ( dmg )
|
|
{
|
|
int drain = 0;
|
|
if ( traceEnt->client->ps.forcePower )
|
|
{
|
|
if ( dmg > traceEnt->client->ps.forcePower )
|
|
{
|
|
drain = traceEnt->client->ps.forcePower;
|
|
dmg -= drain;
|
|
traceEnt->client->ps.forcePower = 0;
|
|
}
|
|
else
|
|
{
|
|
drain = dmg;
|
|
traceEnt->client->ps.forcePower -= (dmg);
|
|
dmg = 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
if ( (dflags&DAMAGE_NO_KILL) )
|
|
{//must cap damage
|
|
if ( traceEnt->health <= 1 )
|
|
{//can't drain more than they have
|
|
dmg = 0;
|
|
}
|
|
else if ( dmg >= traceEnt->health )
|
|
{//no more than they have, leaving one for them
|
|
dmg = traceEnt->health-1;
|
|
}
|
|
}
|
|
*/
|
|
|
|
int maxHealth = self->client->ps.stats[STAT_MAX_HEALTH];
|
|
if ( self->client->ps.forcePowerLevel[FP_DRAIN] > FORCE_LEVEL_2 )
|
|
{//overcharge health
|
|
maxHealth = floor( (float)self->client->ps.stats[STAT_MAX_HEALTH] * 1.25f );
|
|
}
|
|
if (self->client->ps.stats[STAT_HEALTH] < maxHealth &&
|
|
self->health > 0 && self->client->ps.stats[STAT_HEALTH] > 0)
|
|
{
|
|
self->health += (drain+dmg);
|
|
if (self->health > maxHealth )
|
|
{
|
|
self->health = maxHealth;
|
|
}
|
|
self->client->ps.stats[STAT_HEALTH] = self->health;
|
|
if ( self->health > self->client->ps.stats[STAT_MAX_HEALTH] )
|
|
{
|
|
self->flags |= FL_OVERCHARGED_HEALTH;
|
|
}
|
|
}
|
|
|
|
if ( dmg )
|
|
{//do damage, too
|
|
G_Damage( traceEnt, self, self, dir, impactPoint, dmg, dflags, MOD_FORCE_DRAIN );
|
|
}
|
|
else if ( drain )
|
|
{
|
|
/*
|
|
if ( traceEnt->s.number == self->client->ps.forceDrainEntityNum
|
|
|| traceEnt->s.number < MAX_CLIENTS )
|
|
{//grip-draining (or player - only does sound)
|
|
*/
|
|
NPC_SetPainEvent( traceEnt );
|
|
/*
|
|
}
|
|
else
|
|
{
|
|
GEntity_PainFunc( traceEnt, self, self, impactPoint, 0, MOD_FORCE_DRAIN );
|
|
}
|
|
*/
|
|
}
|
|
|
|
if ( !Q_irand( 0, 2 ) )
|
|
{
|
|
G_Sound( traceEnt, G_SoundIndex( "sound/weapons/force/drained.mp3" ) );
|
|
}
|
|
|
|
traceEnt->client->ps.forcePowerRegenDebounceTime = level.time + 800; //don't let the client being drained get force power back right away
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
qboolean WP_CheckForceDraineeStopMe( gentity_t *self, gentity_t *drainee )
|
|
{
|
|
if ( drainee->NPC
|
|
&& drainee->client
|
|
&& (drainee->client->ps.forcePowersKnown&(1<<FP_PUSH))
|
|
&& level.time-(self->client->ps.forcePowerDebounce[FP_DRAIN]>self->client->ps.forcePowerLevel[FP_DRAIN]*500)//at level 1, I always get at least 500ms of drain, at level 3 I get 1500ms
|
|
&& !Q_irand( 0, 100-(drainee->NPC->stats.evasion*10)-(g_spskill->integer*12) ) )
|
|
{//a jedi who broke free
|
|
ForceThrowEx( drainee, qfalse, qfalse, qtrue );
|
|
//FIXME: I need to go into some pushed back anim...
|
|
WP_ForcePowerStop( self, FP_DRAIN );
|
|
//can't drain again for 2 seconds
|
|
self->client->ps.forcePowerDebounce[FP_DRAIN] = level.time + 4000;
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
void ForceShootDrain( gentity_t *self )
|
|
{
|
|
trace_t tr;
|
|
vec3_t end, forward;
|
|
gentity_t *traceEnt;
|
|
int numDrained = 0;
|
|
|
|
if ( self->health <= 0 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
vec3_t origin, angles;
|
|
if (self->client->ps.clientNum == 0 && !cg.renderingThirdPerson)
|
|
{
|
|
BG_CalculateVROffHandPosition(origin, angles);
|
|
AngleVectors(angles, forward, NULL, NULL);
|
|
}
|
|
else
|
|
{
|
|
AngleVectors( self->client->ps.viewangles, forward, NULL, NULL);
|
|
VectorCopy( self->client->ps.origin, origin );
|
|
}
|
|
VectorNormalize( forward );
|
|
|
|
if ( self->client->ps.forcePowerDebounce[FP_DRAIN] <= level.time )
|
|
{
|
|
if ( self->client->ps.forcePowerLevel[FP_DRAIN] > FORCE_LEVEL_2 )
|
|
{//arc
|
|
vec3_t center, mins, maxs, dir, ent_org, size, v;
|
|
float radius = MAX_DRAIN_DISTANCE, dot, dist;
|
|
gentity_t *entityList[MAX_GENTITIES];
|
|
int e, numListedEntities, i;
|
|
|
|
VectorCopy( origin, center );
|
|
for ( i = 0 ; i < 3 ; i++ )
|
|
{
|
|
mins[i] = center[i] - radius;
|
|
maxs[i] = center[i] + radius;
|
|
}
|
|
numListedEntities = gi.EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
|
|
|
|
for ( e = 0 ; e < numListedEntities ; e++ )
|
|
{
|
|
traceEnt = entityList[e];
|
|
|
|
if ( !traceEnt )
|
|
continue;
|
|
if ( traceEnt == self )
|
|
continue;
|
|
if ( !traceEnt->inuse )
|
|
continue;
|
|
if ( !traceEnt->takedamage )
|
|
continue;
|
|
if ( traceEnt->health <= 0 )//no torturing corpses
|
|
continue;
|
|
if ( !traceEnt->client )
|
|
continue;
|
|
/*
|
|
if ( !traceEnt->client->ps.forcePower )
|
|
continue;
|
|
*/
|
|
// if (traceEnt->client->ps.forceSide == FORCE_DARKSIDE) // We no longer care if the victim is dark or light
|
|
// continue;
|
|
if (self->enemy != traceEnt//not my enemy
|
|
&& OnSameTeam(self, traceEnt))//on my team
|
|
continue;
|
|
//this is all to see if we need to start a saber attack, if it's in flight, this doesn't matter
|
|
// find the distance from the edge of the bounding box
|
|
for ( i = 0 ; i < 3 ; i++ )
|
|
{
|
|
if ( center[i] < traceEnt->absmin[i] )
|
|
{
|
|
v[i] = traceEnt->absmin[i] - center[i];
|
|
} else if ( center[i] > traceEnt->absmax[i] )
|
|
{
|
|
v[i] = center[i] - traceEnt->absmax[i];
|
|
} else
|
|
{
|
|
v[i] = 0;
|
|
}
|
|
}
|
|
|
|
VectorSubtract( traceEnt->absmax, traceEnt->absmin, size );
|
|
VectorMA( traceEnt->absmin, 0.5, size, ent_org );
|
|
|
|
//see if they're in front of me
|
|
//must be within the forward cone
|
|
VectorSubtract( ent_org, center, dir );
|
|
VectorNormalize( dir );
|
|
if ( (dot = DotProduct( dir, forward )) < 0.5 )
|
|
continue;
|
|
|
|
//must be close enough
|
|
dist = VectorLength( v );
|
|
if ( dist >= radius )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
//in PVS?
|
|
if ( !traceEnt->bmodel && !gi.inPVS( ent_org, self->client->renderInfo.handLPoint ) )
|
|
{//must be in PVS
|
|
continue;
|
|
}
|
|
|
|
//Now check and see if we can actually hit it
|
|
gi.trace( &tr, origin, vec3_origin, vec3_origin, ent_org, self->s.number, MASK_SHOT, G2_RETURNONHIT, 10 );
|
|
if ( tr.fraction < 1.0f && tr.entityNum != traceEnt->s.number )
|
|
{//must have clear LOS
|
|
continue;
|
|
}
|
|
|
|
if ( traceEnt
|
|
&& traceEnt->s.number >= MAX_CLIENTS
|
|
&& traceEnt->client
|
|
&& traceEnt->client->ps.forcePowerLevel[FP_LEVITATION] > FORCE_LEVEL_0 )//&& traceEnt->NPC
|
|
{
|
|
if ( !Q_irand( 0, 4 ) && !Jedi_DodgeEvasion( traceEnt, self, &tr, HL_NONE ) )
|
|
{//act like we didn't even hit him
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// ok, we are within the radius, add us to the incoming list
|
|
if ( WP_CheckForceDraineeStopMe( self, traceEnt ) )
|
|
{
|
|
continue;
|
|
}
|
|
ForceDrainDamage( self, traceEnt, dir, ent_org );
|
|
numDrained++;
|
|
}
|
|
|
|
}
|
|
else
|
|
{//trace-line
|
|
int ignore = self->s.number;
|
|
int traces = 0;
|
|
vec3_t start;
|
|
|
|
VectorCopy( self->client->renderInfo.handLPoint, start );
|
|
VectorMA( start, MAX_DRAIN_DISTANCE, forward, end );
|
|
|
|
while ( traces < 10 )
|
|
{//need to loop this in case we hit a Jedi who dodges the shot
|
|
gi.trace( &tr, start, vec3_origin, vec3_origin, end, ignore, MASK_SHOT, G2_RETURNONHIT, 10 );
|
|
if ( tr.entityNum == ENTITYNUM_NONE || tr.fraction == 1.0 || tr.allsolid || tr.startsolid )
|
|
{
|
|
//always take 1 force point per frame that we're shooting this
|
|
WP_ForcePowerDrain( self, FP_DRAIN, 1 );
|
|
return;
|
|
}
|
|
|
|
traceEnt = &g_entities[tr.entityNum];
|
|
//NOTE: only NPCs do this auto-dodge
|
|
if ( traceEnt
|
|
&& traceEnt->s.number >= MAX_CLIENTS
|
|
&& traceEnt->client
|
|
&& traceEnt->client->ps.forcePowerLevel[FP_LEVITATION] > FORCE_LEVEL_0 )//&& traceEnt->NPC
|
|
{
|
|
if ( !Q_irand( 0, 2 ) && !Jedi_DodgeEvasion( traceEnt, self, &tr, HL_NONE ) )
|
|
{//act like we didn't even hit him
|
|
VectorCopy( tr.endpos, start );
|
|
ignore = tr.entityNum;
|
|
traces++;
|
|
continue;
|
|
}
|
|
}
|
|
//a Jedi is not dodging this shot
|
|
break;
|
|
}
|
|
traceEnt = &g_entities[tr.entityNum];
|
|
if ( !WP_CheckForceDraineeStopMe( self, traceEnt ) )
|
|
{
|
|
ForceDrainDamage( self, traceEnt, forward, tr.endpos );
|
|
}
|
|
numDrained = 1;
|
|
}
|
|
|
|
self->client->ps.forcePowerDebounce[FP_DRAIN] = level.time + 200;//so we don't drain so damn fast!
|
|
}
|
|
self->client->ps.forcePowerRegenDebounceTime = level.time + 500;
|
|
|
|
if ( !numDrained )
|
|
{//always take 1 force point per frame that we're shooting this
|
|
WP_ForcePowerDrain( self, FP_DRAIN, 1 );
|
|
}
|
|
else
|
|
{
|
|
WP_ForcePowerDrain( self, FP_DRAIN, numDrained );//was 2, but...
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
void ForceDrainEnt( gentity_t *self, gentity_t *drainEnt )
|
|
{
|
|
if ( self->health <= 0 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
vec3_t origin, angles, fwd;
|
|
if (self->client->ps.clientNum == 0 && !cg.renderingThirdPerson)
|
|
{
|
|
BG_CalculateVROffHandPosition(origin, angles);
|
|
AngleVectors(angles, fwd, NULL, NULL);
|
|
}
|
|
else
|
|
{
|
|
AngleVectors( self->client->ps.viewangles, fwd, NULL, NULL);
|
|
VectorCopy( self->client->ps.origin, origin );
|
|
}
|
|
VectorNormalize( fwd );
|
|
|
|
if ( self->client->ps.forcePowerDebounce[FP_DRAIN] <= level.time )
|
|
{
|
|
if ( !drainEnt )
|
|
return;
|
|
if ( drainEnt == self )
|
|
return;
|
|
if ( !drainEnt->inuse )
|
|
return;
|
|
if ( !drainEnt->takedamage )
|
|
return;
|
|
if ( drainEnt->health <= 0 )//no torturing corpses
|
|
return;
|
|
if ( !drainEnt->client )
|
|
return;
|
|
if (OnSameTeam(self, drainEnt))
|
|
return;
|
|
|
|
drainEnt->painDebounceTime = 0;
|
|
ForceDrainDamage( self, drainEnt, fwd, drainEnt->currentOrigin );
|
|
drainEnt->painDebounceTime = level.time + 2000;
|
|
|
|
if ( drainEnt->s.number )
|
|
{
|
|
if ( self->client->ps.forcePowerLevel[FP_DRAIN] > FORCE_LEVEL_2 )
|
|
{//do damage faster at level 3
|
|
self->client->ps.forcePowerDebounce[FP_DRAIN] = level.time + Q_irand( 100, 500 );
|
|
}
|
|
else
|
|
{
|
|
self->client->ps.forcePowerDebounce[FP_DRAIN] = level.time + Q_irand( 200, 800 );
|
|
}
|
|
}
|
|
else
|
|
{//player takes damage faster
|
|
self->client->ps.forcePowerDebounce[FP_DRAIN] = level.time + Q_irand( 100, 500 );
|
|
}
|
|
}
|
|
|
|
self->client->ps.forcePowerRegenDebounceTime = level.time + 500;
|
|
}
|
|
|
|
void ForceSeeing( gentity_t *self )
|
|
{
|
|
if ( self->health <= 0 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self->client->ps.forceAllowDeactivateTime < level.time &&
|
|
(self->client->ps.forcePowersActive & (1 << FP_SEE)) )
|
|
{
|
|
WP_ForcePowerStop( self, FP_SEE );
|
|
return;
|
|
}
|
|
|
|
if ( !WP_ForcePowerUsable( self, FP_SEE, 0 ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
WP_DebounceForceDeactivateTime( self );
|
|
|
|
WP_ForcePowerStart( self, FP_SEE, 0 );
|
|
|
|
G_SoundOnEnt( self, CHAN_ITEM, "sound/weapons/force/see.wav" );
|
|
}
|
|
|
|
void ForceProtect( gentity_t *self )
|
|
{
|
|
if ( self->health <= 0 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self->client->ps.forceAllowDeactivateTime < level.time &&
|
|
(self->client->ps.forcePowersActive & (1 << FP_PROTECT)) )
|
|
{
|
|
WP_ForcePowerStop( self, FP_PROTECT );
|
|
return;
|
|
}
|
|
|
|
if ( !WP_ForcePowerUsable( self, FP_PROTECT, 0 ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Make sure to turn off Force Rage and Force Absorb.
|
|
if (self->client->ps.forcePowersActive & (1 << FP_RAGE) )
|
|
{
|
|
WP_ForcePowerStop( self, FP_RAGE );
|
|
}
|
|
|
|
WP_DebounceForceDeactivateTime( self );
|
|
|
|
WP_ForcePowerStart( self, FP_PROTECT, 0 );
|
|
|
|
if ( self->client->ps.saberLockTime < level.time )
|
|
{
|
|
if ( self->client->ps.forcePowerLevel[FP_PROTECT] < FORCE_LEVEL_3 )
|
|
{//animate
|
|
int parts = SETANIM_BOTH;
|
|
int anim = BOTH_FORCE_PROTECT;
|
|
if ( self->client->ps.forcePowerLevel[FP_PROTECT] > FORCE_LEVEL_1 )
|
|
{//level 2 only does it on torso (can keep running)
|
|
parts = SETANIM_TORSO;
|
|
anim = BOTH_FORCE_PROTECT_FAST;
|
|
}
|
|
else
|
|
{
|
|
if ( self->client->ps.groundEntityNum != ENTITYNUM_NONE )
|
|
{
|
|
VectorClear( self->client->ps.velocity );
|
|
}
|
|
if ( self->NPC )
|
|
{
|
|
VectorClear( self->client->ps.moveDir );
|
|
self->client->ps.speed = 0;
|
|
}
|
|
//FIXME: what if in air?
|
|
}
|
|
NPC_SetAnim( self, parts, anim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
//don't move or attack during this anim
|
|
if ( self->client->ps.forcePowerLevel[FP_PROTECT] < FORCE_LEVEL_2 )
|
|
{
|
|
self->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
|
|
self->client->ps.pm_time = self->client->ps.weaponTime = self->client->ps.torsoAnimTimer;
|
|
if ( self->s.number )
|
|
{//NPC
|
|
self->painDebounceTime = level.time + self->client->ps.torsoAnimTimer;
|
|
}
|
|
else
|
|
{//player
|
|
self->aimDebounceTime = level.time + self->client->ps.torsoAnimTimer;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self->client->ps.weaponTime = self->client->ps.torsoAnimTimer;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ForceAbsorb( gentity_t *self )
|
|
{
|
|
if ( self->health <= 0 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self->client->ps.forceAllowDeactivateTime < level.time &&
|
|
(self->client->ps.forcePowersActive & (1 << FP_ABSORB)) )
|
|
{
|
|
WP_ForcePowerStop( self, FP_ABSORB );
|
|
return;
|
|
}
|
|
|
|
if ( !WP_ForcePowerUsable( self, FP_ABSORB, 0 ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Make sure to turn off Force Rage and Force Protection.
|
|
if (self->client->ps.forcePowersActive & (1 << FP_RAGE) )
|
|
{
|
|
WP_ForcePowerStop( self, FP_RAGE );
|
|
}
|
|
|
|
WP_DebounceForceDeactivateTime( self );
|
|
|
|
WP_ForcePowerStart( self, FP_ABSORB, 0 );
|
|
|
|
if ( self->client->ps.saberLockTime < level.time )
|
|
{
|
|
if ( self->client->ps.forcePowerLevel[FP_ABSORB] < FORCE_LEVEL_3 )
|
|
{//must animate
|
|
int parts = SETANIM_BOTH;
|
|
if ( self->client->ps.forcePowerLevel[FP_ABSORB] > FORCE_LEVEL_1 )
|
|
{//level 2 only does it on torso (can keep running)
|
|
parts = SETANIM_TORSO;
|
|
}
|
|
else
|
|
{
|
|
if ( self->client->ps.groundEntityNum != ENTITYNUM_NONE )
|
|
{
|
|
VectorClear( self->client->ps.velocity );
|
|
}
|
|
if ( self->NPC )
|
|
{
|
|
VectorClear( self->client->ps.moveDir );
|
|
self->client->ps.speed = 0;
|
|
}
|
|
//FIXME: what if in air?
|
|
}
|
|
/*
|
|
//if in air, only do on torso - NOTE: or moving?
|
|
if ( self->client->ps.groundEntityNum == ENTITYNUM_NONE )//|| !VectorCompare( self->client->ps.velocity, vec3_origin ) )
|
|
{
|
|
parts = SETANIM_TORSO;
|
|
}
|
|
*/
|
|
NPC_SetAnim( self, parts, BOTH_FORCE_ABSORB, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
self->client->ps.weaponTime = self->client->ps.torsoAnimTimer;
|
|
if ( parts == SETANIM_BOTH )
|
|
{//can't move
|
|
self->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
|
|
self->client->ps.pm_time = self->client->ps.legsAnimTimer = self->client->ps.torsoAnimTimer;// = self->client->ps.forcePowerDuration[FP_ABSORB];
|
|
if ( self->s.number )
|
|
{//NPC
|
|
self->painDebounceTime = level.time + self->client->ps.pm_time;//self->client->ps.forcePowerDuration[FP_ABSORB];
|
|
}
|
|
else
|
|
{//player
|
|
self->aimDebounceTime = level.time + self->client->ps.pm_time;//self->client->ps.forcePowerDuration[FP_ABSORB];
|
|
}
|
|
}
|
|
//stop saber
|
|
//WP_DeactivateSaber( self );//turn off saber when meditating
|
|
self->client->ps.saberMove = self->client->ps.saberBounceMove = LS_READY;//don't finish whatever saber anim you may have been in
|
|
self->client->ps.saberBlocked = BLOCKED_NONE;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ForceRage( gentity_t *self )
|
|
{
|
|
if ( self->health <= 0 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
//FIXME: prevent them from using any other force powers when raging?
|
|
|
|
if (self->client->ps.forceAllowDeactivateTime < level.time &&
|
|
(self->client->ps.forcePowersActive & (1 << FP_RAGE)) )
|
|
{
|
|
WP_ForcePowerStop( self, FP_RAGE );
|
|
return;
|
|
}
|
|
|
|
if ( !WP_ForcePowerUsable( self, FP_RAGE, 0 ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self->client->ps.forceRageRecoveryTime >= level.time)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( self->s.number < MAX_CLIENTS
|
|
&& self->health < 25 )
|
|
{//have to have at least 25 health to start it
|
|
return;
|
|
}
|
|
|
|
if (self->health < 10)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Make sure to turn off Force Protection and Force Absorb.
|
|
if (self->client->ps.forcePowersActive & (1 << FP_PROTECT) )
|
|
{
|
|
WP_ForcePowerStop( self, FP_PROTECT );
|
|
}
|
|
if (self->client->ps.forcePowersActive & (1 << FP_ABSORB) )
|
|
{
|
|
WP_ForcePowerStop( self, FP_ABSORB );
|
|
}
|
|
|
|
WP_DebounceForceDeactivateTime( self );
|
|
|
|
WP_ForcePowerStart( self, FP_RAGE, 0 );
|
|
|
|
if ( self->client->ps.saberLockTime < level.time )
|
|
{
|
|
if ( self->client->ps.forcePowerLevel[FP_RAGE] < FORCE_LEVEL_3 )
|
|
{//must animate
|
|
if ( self->client->ps.forcePowerLevel[FP_RAGE] < FORCE_LEVEL_2 )
|
|
{//have to stand still for whole length of anim
|
|
//FIXME: if in air, only do on torso?
|
|
NPC_SetAnim( self, SETANIM_BOTH, BOTH_FORCE_RAGE, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
//don't attack during this anim
|
|
self->client->ps.weaponTime = self->client->ps.torsoAnimTimer;
|
|
self->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
|
|
self->client->ps.pm_time = self->client->ps.torsoAnimTimer;
|
|
if ( self->s.number )
|
|
{//NPC
|
|
self->painDebounceTime = level.time + self->client->ps.torsoAnimTimer;
|
|
}
|
|
else
|
|
{//player
|
|
self->aimDebounceTime = level.time + self->client->ps.torsoAnimTimer;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCE_RAGE, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
//don't attack during this anim
|
|
self->client->ps.weaponTime = self->client->ps.torsoAnimTimer;
|
|
}
|
|
//stop saber
|
|
self->client->ps.saberMove = self->client->ps.saberBounceMove = LS_READY;//don't finish whatever saber anim you may have been in
|
|
self->client->ps.saberBlocked = BLOCKED_NONE;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ForceJumpCharge( gentity_t *self, usercmd_t *ucmd )
|
|
{
|
|
float forceJumpChargeInterval = forceJumpStrength[0] / (FORCE_JUMP_CHARGE_TIME/FRAMETIME);
|
|
|
|
if ( self->health <= 0 )
|
|
{
|
|
return;
|
|
}
|
|
if ( !self->s.number && cg.zoomMode )
|
|
{//can't force jump when zoomed in
|
|
return;
|
|
}
|
|
|
|
//need to play sound
|
|
if ( !self->client->ps.forceJumpCharge )
|
|
{//FIXME: this should last only as long as the actual charge-up
|
|
G_SoundOnEnt( self, CHAN_BODY, "sound/weapons/force/jumpbuild.wav" );
|
|
}
|
|
//Increment
|
|
self->client->ps.forceJumpCharge += forceJumpChargeInterval;
|
|
|
|
//clamp to max strength for current level
|
|
if ( self->client->ps.forceJumpCharge > forceJumpStrength[self->client->ps.forcePowerLevel[FP_LEVITATION]] )
|
|
{
|
|
self->client->ps.forceJumpCharge = forceJumpStrength[self->client->ps.forcePowerLevel[FP_LEVITATION]];
|
|
}
|
|
|
|
//clamp to max available force power
|
|
if ( self->client->ps.forceJumpCharge/forceJumpChargeInterval/(FORCE_JUMP_CHARGE_TIME/FRAMETIME)*forcePowerNeeded[FP_LEVITATION] > self->client->ps.forcePower )
|
|
{//can't use more than you have
|
|
self->client->ps.forceJumpCharge = self->client->ps.forcePower*forceJumpChargeInterval/(FORCE_JUMP_CHARGE_TIME/FRAMETIME);
|
|
}
|
|
//FIXME: a simple tap should always do at least a normal height's jump?
|
|
}
|
|
|
|
int WP_GetVelocityForForceJump( gentity_t *self, vec3_t jumpVel, usercmd_t *ucmd )
|
|
{
|
|
float pushFwd = 0, pushRt = 0;
|
|
vec3_t view, forward, right;
|
|
VectorCopy( self->client->ps.viewangles, view );
|
|
view[0] = 0;
|
|
AngleVectors( view, forward, right, NULL );
|
|
if ( ucmd->forwardmove && ucmd->rightmove )
|
|
{
|
|
if ( ucmd->forwardmove > 0 )
|
|
{
|
|
pushFwd = 50;
|
|
}
|
|
else
|
|
{
|
|
pushFwd = -50;
|
|
}
|
|
if ( ucmd->rightmove > 0 )
|
|
{
|
|
pushRt = 50;
|
|
}
|
|
else
|
|
{
|
|
pushRt = -50;
|
|
}
|
|
}
|
|
else if ( ucmd->forwardmove || ucmd->rightmove )
|
|
{
|
|
if ( ucmd->forwardmove > 0 )
|
|
{
|
|
pushFwd = 100;
|
|
}
|
|
else if ( ucmd->forwardmove < 0 )
|
|
{
|
|
pushFwd = -100;
|
|
}
|
|
else if ( ucmd->rightmove > 0 )
|
|
{
|
|
pushRt = 100;
|
|
}
|
|
else if ( ucmd->rightmove < 0 )
|
|
{
|
|
pushRt = -100;
|
|
}
|
|
}
|
|
VectorMA( self->client->ps.velocity, pushFwd, forward, jumpVel );
|
|
VectorMA( self->client->ps.velocity, pushRt, right, jumpVel );
|
|
jumpVel[2] += self->client->ps.forceJumpCharge;//forceJumpStrength;
|
|
if ( pushFwd > 0 && self->client->ps.forceJumpCharge > 200 )
|
|
{
|
|
return FJ_FORWARD;
|
|
}
|
|
else if ( pushFwd < 0 && self->client->ps.forceJumpCharge > 200 )
|
|
{
|
|
return FJ_BACKWARD;
|
|
}
|
|
else if ( pushRt > 0 && self->client->ps.forceJumpCharge > 200 )
|
|
{
|
|
return FJ_RIGHT;
|
|
}
|
|
else if ( pushRt < 0 && self->client->ps.forceJumpCharge > 200 )
|
|
{
|
|
return FJ_LEFT;
|
|
}
|
|
else
|
|
{//FIXME: jump straight up anim
|
|
return FJ_UP;
|
|
}
|
|
}
|
|
|
|
void ForceJump( gentity_t *self, usercmd_t *ucmd )
|
|
{
|
|
if ( self->client->ps.forcePowerDuration[FP_LEVITATION] > level.time )
|
|
{
|
|
return;
|
|
}
|
|
if ( !WP_ForcePowerUsable( self, FP_LEVITATION, 0 ) )
|
|
{
|
|
return;
|
|
}
|
|
if ( self->s.groundEntityNum == ENTITYNUM_NONE )
|
|
{
|
|
return;
|
|
}
|
|
if ( self->client->ps.pm_flags&PMF_JUMP_HELD )
|
|
{
|
|
return;
|
|
}
|
|
if ( self->health <= 0 )
|
|
{
|
|
return;
|
|
}
|
|
if ( !self->s.number && (cg.zoomMode || in_camera) )
|
|
{//can't force jump when zoomed in or in cinematic
|
|
return;
|
|
}
|
|
if ( self->client->ps.saberLockTime > level.time )
|
|
{//FIXME: can this be a way to break out?
|
|
return;
|
|
}
|
|
|
|
if ( self->client->NPC_class == CLASS_BOBAFETT
|
|
|| self->client->NPC_class == CLASS_ROCKETTROOPER )
|
|
{
|
|
if ( self->client->ps.forceJumpCharge > 300 )
|
|
{
|
|
JET_FlyStart(NPC);
|
|
}
|
|
else
|
|
{
|
|
G_AddEvent( self, EV_JUMP, 0 );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
G_SoundOnEnt( self, CHAN_BODY, "sound/weapons/force/jump.wav" );
|
|
}
|
|
|
|
float forceJumpChargeInterval = forceJumpStrength[self->client->ps.forcePowerLevel[FP_LEVITATION]]/(FORCE_JUMP_CHARGE_TIME/FRAMETIME);
|
|
|
|
int anim;
|
|
vec3_t jumpVel;
|
|
|
|
switch( WP_GetVelocityForForceJump( self, jumpVel, ucmd ) )
|
|
{
|
|
case FJ_FORWARD:
|
|
if ( ((self->client->NPC_class == CLASS_BOBAFETT||self->client->NPC_class == CLASS_ROCKETTROOPER) && self->client->ps.forceJumpCharge > 300 )
|
|
|| (self->client->ps.saber[0].saberFlags&SFL_NO_FLIPS)
|
|
|| (self->client->ps.dualSabers && (self->client->ps.saber[1].saberFlags&SFL_NO_FLIPS) )
|
|
|| ( self->NPC &&
|
|
self->NPC->rank != RANK_CREWMAN &&
|
|
self->NPC->rank <= RANK_LT_JG ) )
|
|
{//can't do acrobatics
|
|
anim = BOTH_FORCEJUMP1;
|
|
}
|
|
else
|
|
{
|
|
if ( self->client->NPC_class == CLASS_ALORA && Q_irand( 0, 3 ) )
|
|
{
|
|
anim = Q_irand( BOTH_ALORA_FLIP_1, BOTH_ALORA_FLIP_3 );
|
|
}
|
|
else
|
|
{
|
|
anim = BOTH_FLIP_F;
|
|
}
|
|
}
|
|
break;
|
|
case FJ_BACKWARD:
|
|
if ( ((self->client->NPC_class == CLASS_BOBAFETT||self->client->NPC_class == CLASS_ROCKETTROOPER) && self->client->ps.forceJumpCharge > 300 )
|
|
|| (self->client->ps.saber[0].saberFlags&SFL_NO_FLIPS)
|
|
|| (self->client->ps.dualSabers && (self->client->ps.saber[1].saberFlags&SFL_NO_FLIPS) )
|
|
|| ( self->NPC &&
|
|
self->NPC->rank != RANK_CREWMAN &&
|
|
self->NPC->rank <= RANK_LT_JG ) )
|
|
{//can't do acrobatics
|
|
anim = BOTH_FORCEJUMPBACK1;
|
|
}
|
|
else
|
|
{
|
|
anim = BOTH_FLIP_B;
|
|
}
|
|
break;
|
|
case FJ_RIGHT:
|
|
if ( ((self->client->NPC_class == CLASS_BOBAFETT||self->client->NPC_class == CLASS_ROCKETTROOPER) && self->client->ps.forceJumpCharge > 300 )
|
|
|| (self->client->ps.saber[0].saberFlags&SFL_NO_FLIPS)
|
|
|| (self->client->ps.dualSabers && (self->client->ps.saber[1].saberFlags&SFL_NO_FLIPS) )
|
|
|| ( self->NPC &&
|
|
self->NPC->rank != RANK_CREWMAN &&
|
|
self->NPC->rank <= RANK_LT_JG ) )
|
|
{//can't do acrobatics
|
|
anim = BOTH_FORCEJUMPRIGHT1;
|
|
}
|
|
else
|
|
{
|
|
anim = BOTH_FLIP_R;
|
|
}
|
|
break;
|
|
case FJ_LEFT:
|
|
if ( ((self->client->NPC_class == CLASS_BOBAFETT||self->client->NPC_class == CLASS_ROCKETTROOPER) && self->client->ps.forceJumpCharge > 300 )
|
|
|| (self->client->ps.saber[0].saberFlags&SFL_NO_FLIPS)
|
|
|| (self->client->ps.dualSabers && (self->client->ps.saber[1].saberFlags&SFL_NO_FLIPS) )
|
|
|| ( self->NPC &&
|
|
self->NPC->rank != RANK_CREWMAN &&
|
|
self->NPC->rank <= RANK_LT_JG ) )
|
|
{//can't do acrobatics
|
|
anim = BOTH_FORCEJUMPLEFT1;
|
|
}
|
|
else
|
|
{
|
|
anim = BOTH_FLIP_L;
|
|
}
|
|
break;
|
|
default:
|
|
case FJ_UP:
|
|
anim = BOTH_JUMP1;
|
|
break;
|
|
}
|
|
|
|
int parts = SETANIM_BOTH;
|
|
if ( self->client->ps.weaponTime )
|
|
{//FIXME: really only care if we're in a saber attack anim.. maybe trail length?
|
|
parts = SETANIM_LEGS;
|
|
}
|
|
|
|
NPC_SetAnim( self, parts, anim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
|
|
//FIXME: sound effect
|
|
self->client->ps.forceJumpZStart = self->currentOrigin[2];//remember this for when we land
|
|
VectorCopy( jumpVel, self->client->ps.velocity );
|
|
//wasn't allowing them to attack when jumping, but that was annoying
|
|
//self->client->ps.weaponTime = self->client->ps.torsoAnimTimer;
|
|
|
|
WP_ForcePowerStart( self, FP_LEVITATION, self->client->ps.forceJumpCharge/forceJumpChargeInterval/(FORCE_JUMP_CHARGE_TIME/FRAMETIME)*forcePowerNeeded[FP_LEVITATION] );
|
|
//self->client->ps.forcePowerDuration[FP_LEVITATION] = level.time + self->client->ps.weaponTime;
|
|
self->client->ps.forceJumpCharge = 0;
|
|
}
|
|
|
|
int WP_AbsorbConversion(gentity_t *attacked, int atdAbsLevel, gentity_t *attacker, int atPower, int atPowerLevel, int atForceSpent)
|
|
{
|
|
int getLevel = 0;
|
|
int addTot = 0;
|
|
|
|
if (atPower != FP_LIGHTNING &&
|
|
atPower != FP_DRAIN &&
|
|
atPower != FP_GRIP &&
|
|
atPower != FP_PUSH &&
|
|
atPower != FP_PULL)
|
|
{ //Only these powers can be absorbed
|
|
return -1;
|
|
}
|
|
|
|
if (!atdAbsLevel)
|
|
{ //looks like attacker doesn't have any absorb power
|
|
return -1;
|
|
}
|
|
|
|
if (!(attacked->client->ps.forcePowersActive & (1 << FP_ABSORB)))
|
|
{ //absorb is not active
|
|
return -1;
|
|
}
|
|
|
|
//Subtract absorb power level from the offensive force power
|
|
getLevel = atPowerLevel;
|
|
getLevel -= atdAbsLevel;
|
|
|
|
if (getLevel < 0)
|
|
{
|
|
getLevel = 0;
|
|
}
|
|
|
|
//let the attacker absorb an amount of force used in this attack based on his level of absorb
|
|
addTot = (atForceSpent/3)*attacked->client->ps.forcePowerLevel[FP_ABSORB];
|
|
|
|
if (addTot < 1 && atForceSpent >= 1)
|
|
{
|
|
addTot = 1;
|
|
}
|
|
attacked->client->ps.forcePower += addTot;
|
|
if (attacked->client->ps.forcePower > attacked->client->ps.forcePowerMax)
|
|
{
|
|
attacked->client->ps.forcePower = attacked->client->ps.forcePowerMax;
|
|
}
|
|
|
|
G_SoundOnEnt( attacked, CHAN_ITEM, "sound/weapons/force/absorbhit.wav" );
|
|
|
|
return getLevel;
|
|
}
|
|
|
|
void WP_ForcePowerRegenerate( gentity_t *self, int overrideAmt )
|
|
{
|
|
if ( !self->client )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.forcePower < self->client->ps.forcePowerMax )
|
|
{
|
|
if ( overrideAmt )
|
|
{
|
|
self->client->ps.forcePower += overrideAmt;
|
|
}
|
|
else
|
|
{
|
|
self->client->ps.forcePower++;
|
|
}
|
|
if ( self->client->ps.forcePower > self->client->ps.forcePowerMax )
|
|
{
|
|
self->client->ps.forcePower = self->client->ps.forcePowerMax;
|
|
}
|
|
}
|
|
}
|
|
|
|
void WP_ForcePowerDrain( gentity_t *self, forcePowers_t forcePower, int overrideAmt )
|
|
{
|
|
if ( self->NPC )
|
|
{//For now, NPCs have infinite force power
|
|
return;
|
|
}
|
|
//take away the power
|
|
int drain = overrideAmt;
|
|
if ( !drain )
|
|
{
|
|
drain = forcePowerNeeded[forcePower];
|
|
}
|
|
if ( !drain )
|
|
{
|
|
return;
|
|
}
|
|
self->client->ps.forcePower -= drain;
|
|
if ( self->client->ps.forcePower < 0 )
|
|
{
|
|
self->client->ps.forcePower = 0;
|
|
}
|
|
}
|
|
|
|
void WP_ForcePowerStart( gentity_t *self, forcePowers_t forcePower, int overrideAmt )
|
|
{
|
|
int duration = 0;
|
|
|
|
//FIXME: debounce some of these?
|
|
self->client->ps.forcePowerDebounce[forcePower] = 0;
|
|
|
|
//and it in
|
|
//set up duration time
|
|
switch( (int)forcePower )
|
|
{
|
|
case FP_HEAL:
|
|
self->client->ps.forcePowersActive |= ( 1 << forcePower );
|
|
self->client->ps.forceHealCount = 0;
|
|
WP_StartForceHealEffects( self );
|
|
break;
|
|
case FP_LEVITATION:
|
|
self->client->ps.forcePowersActive |= ( 1 << forcePower );
|
|
break;
|
|
case FP_SPEED:
|
|
//duration is always 5 seconds, player time
|
|
duration = ceil(FORCE_SPEED_DURATION*forceSpeedValue[self->client->ps.forcePowerLevel[FP_SPEED]]);//FIXME: because the timescale scales down (not instant), this doesn't end up being exactly right...
|
|
self->client->ps.forcePowersActive |= ( 1 << forcePower );
|
|
self->s.loopSound = G_SoundIndex( "sound/weapons/force/speedloop.wav" );
|
|
if ( self->client->ps.forcePowerLevel[FP_SPEED] > FORCE_LEVEL_2 )
|
|
{//HACK: just using this as a timestamp for when the power started, setting debounce to current time shouldn't adversely affect anything else
|
|
self->client->ps.forcePowerDebounce[FP_SPEED] = level.time;
|
|
}
|
|
break;
|
|
case FP_PUSH:
|
|
break;
|
|
case FP_PULL:
|
|
self->client->ps.forcePowersActive |= ( 1 << forcePower );
|
|
break;
|
|
case FP_TELEPATHY:
|
|
break;
|
|
case FP_GRIP:
|
|
duration = 1000;
|
|
self->client->ps.forcePowersActive |= ( 1 << forcePower );
|
|
//HACK: just using this as a timestamp for when the power started, setting debounce to current time shouldn't adversely affect anything else
|
|
//self->client->ps.forcePowerDebounce[forcePower] = level.time;
|
|
break;
|
|
case FP_LIGHTNING:
|
|
duration = overrideAmt;
|
|
overrideAmt = 0;
|
|
self->client->ps.forcePowersActive |= ( 1 << forcePower );
|
|
break;
|
|
//new Jedi Academy force powers
|
|
case FP_RAGE:
|
|
//duration is always 5 seconds, player time
|
|
duration = ceil(FORCE_RAGE_DURATION*forceSpeedValue[self->client->ps.forcePowerLevel[FP_RAGE]-1]);//FIXME: because the timescale scales down (not instant), this doesn't end up being exactly right...
|
|
self->client->ps.forcePowersActive |= ( 1 << forcePower );
|
|
G_SoundOnEnt( self, CHAN_ITEM, "sound/weapons/force/rage.mp3" );
|
|
self->s.loopSound = G_SoundIndex( "sound/weapons/force/rageloop.wav" );
|
|
if ( self->chestBolt != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "force/rage2" ), self->playerModel, self->chestBolt, self->s.number, self->currentOrigin, duration, qtrue );
|
|
}
|
|
break;
|
|
case FP_DRAIN:
|
|
if ( self->client->ps.forcePowerLevel[forcePower] > FORCE_LEVEL_1
|
|
&& self->client->ps.forceDrainEntityNum >= ENTITYNUM_WORLD )
|
|
{
|
|
duration = overrideAmt;
|
|
overrideAmt = 0;
|
|
//HACK: just using this as a timestamp for when the power started, setting debounce to current time shouldn't adversely affect anything else
|
|
self->client->ps.forcePowerDebounce[forcePower] = level.time;
|
|
}
|
|
else
|
|
{
|
|
duration = 1000;
|
|
}
|
|
self->client->ps.forcePowersActive |= ( 1 << forcePower );
|
|
break;
|
|
case FP_PROTECT:
|
|
switch ( self->client->ps.forcePowerLevel[FP_PROTECT] )
|
|
{
|
|
case FORCE_LEVEL_3:
|
|
duration = 20000;
|
|
break;
|
|
case FORCE_LEVEL_2:
|
|
duration = 15000;
|
|
break;
|
|
case FORCE_LEVEL_1:
|
|
default:
|
|
|
|
duration = 10000;
|
|
break;
|
|
}
|
|
self->client->ps.forcePowersActive |= ( 1 << forcePower );
|
|
G_SoundOnEnt( self, CHAN_ITEM, "sound/weapons/force/protect.mp3" );
|
|
self->s.loopSound = G_SoundIndex( "sound/weapons/force/protectloop.wav" );
|
|
break;
|
|
case FP_ABSORB:
|
|
duration = 20000;
|
|
self->client->ps.forcePowersActive |= ( 1 << forcePower );
|
|
G_SoundOnEnt( self, CHAN_ITEM, "sound/weapons/force/absorb.mp3" );
|
|
self->s.loopSound = G_SoundIndex( "sound/weapons/force/absorbloop.wav" );
|
|
break;
|
|
case FP_SEE:
|
|
if ( self->client->ps.forcePowerLevel[FP_SEE] == FORCE_LEVEL_1 )
|
|
{
|
|
duration = 5000;
|
|
}
|
|
else if ( self->client->ps.forcePowerLevel[FP_SEE] == FORCE_LEVEL_2 )
|
|
{
|
|
duration = 10000;
|
|
}
|
|
else// if ( self->client->ps.forcePowerLevel[FP_SEE] == FORCE_LEVEL_3 )
|
|
{
|
|
duration = 20000;
|
|
}
|
|
|
|
self->client->ps.forcePowersActive |= ( 1 << forcePower );
|
|
G_SoundOnEnt( self, CHAN_ITEM, "sound/weapons/force/see.mp3" );
|
|
self->s.loopSound = G_SoundIndex( "sound/weapons/force/seeloop.wav" );
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if ( duration )
|
|
{
|
|
self->client->ps.forcePowerDuration[forcePower] = level.time + duration;
|
|
}
|
|
else
|
|
{
|
|
self->client->ps.forcePowerDuration[forcePower] = 0;
|
|
}
|
|
|
|
WP_ForcePowerDrain( self, forcePower, overrideAmt );
|
|
|
|
if ( !self->s.number )
|
|
{
|
|
self->client->sess.missionStats.forceUsed[(int)forcePower]++;
|
|
}
|
|
}
|
|
|
|
void CG_CenterPrint( const char *str, int y, int delayOverride);
|
|
qboolean WP_ForcePowerAvailable( gentity_t *self, forcePowers_t forcePower, int overrideAmt )
|
|
{
|
|
if ( forcePower == FP_LEVITATION )
|
|
{
|
|
return qtrue;
|
|
}
|
|
int drain = overrideAmt?overrideAmt:forcePowerNeeded[forcePower];
|
|
if ( !drain )
|
|
{
|
|
return qtrue;
|
|
}
|
|
if ( self->client->ps.forcePower < drain )
|
|
{
|
|
// Only show low power warning for force powers that aren't duration based
|
|
if (drain > 1 && self->client->ps.clientNum == 0)
|
|
{
|
|
CG_CenterPrint("Force Power Low", 440);
|
|
}
|
|
return qfalse;
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
extern void CG_PlayerLockedWeaponSpeech( int jumping );
|
|
extern qboolean Rosh_TwinNearBy( gentity_t *self );
|
|
qboolean WP_ForcePowerUsable( gentity_t *self, forcePowers_t forcePower, int overrideAmt )
|
|
{
|
|
if ( !(self->client->ps.forcePowersKnown & ( 1 << forcePower )) )
|
|
{//don't know this power
|
|
return qfalse;
|
|
}
|
|
else if ( self->NPC && (self->NPC->aiFlags&NPCAI_ROSH) )
|
|
{
|
|
if ( ((1<<forcePower)&FORCE_POWERS_ROSH_FROM_TWINS) )
|
|
{//this is a force power we can only use when a twin is near us
|
|
if ( !Rosh_TwinNearBy( self ) )
|
|
{
|
|
return qfalse;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( self->client->ps.forcePowerLevel[forcePower] <= 0 )
|
|
{//can't use this power
|
|
return qfalse;
|
|
}
|
|
|
|
if( (self->flags&FL_LOCK_PLAYER_WEAPONS) ) // yes this locked weapons check also includes force powers, if we need a separate check later I'll make one
|
|
{
|
|
if ( self->s.number < MAX_CLIENTS )
|
|
{
|
|
CG_PlayerLockedWeaponSpeech( qfalse );
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
if ( in_camera && self->s.number < MAX_CLIENTS )
|
|
{//player can't turn on force powers duing cinematics
|
|
return qfalse;
|
|
}
|
|
|
|
if ( PM_LockedAnim( self->client->ps.torsoAnim ) && self->client->ps.torsoAnimTimer )
|
|
{//no force powers during these special anims
|
|
return qfalse;
|
|
}
|
|
if ( PM_SuperBreakLoseAnim( self->client->ps.torsoAnim )
|
|
|| PM_SuperBreakWinAnim( self->client->ps.torsoAnim ) )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
if ( (self->client->ps.forcePowersActive & ( 1 << forcePower )) )
|
|
{//already using this power
|
|
return qfalse;
|
|
}
|
|
/*
|
|
if ( !self->client->ps.forcePowerLevel[(int)(forcePower)] )
|
|
{
|
|
return qfalse;
|
|
}
|
|
*/
|
|
if ( self->client->NPC_class == CLASS_ATST )
|
|
{//Doh! No force powers in an AT-ST!
|
|
return qfalse;
|
|
}
|
|
Vehicle_t *pVeh = NULL;
|
|
if ( (pVeh = G_IsRidingVehicle( self )) != NULL )
|
|
{//Doh! No force powers when flying a vehicle!
|
|
if ( pVeh->m_pVehicleInfo->numHands > 1 )
|
|
{//if in a two-handed vehicle
|
|
return qfalse;
|
|
}
|
|
}
|
|
if ( self->client->ps.viewEntity > 0 && self->client->ps.viewEntity < ENTITYNUM_WORLD )
|
|
{//Doh! No force powers when controlling an NPC
|
|
return qfalse;
|
|
}
|
|
if ( self->client->ps.eFlags & EF_LOCKED_TO_WEAPON )
|
|
{//Doh! No force powers when in an emplaced gun!
|
|
return qfalse;
|
|
}
|
|
|
|
if ( (self->client->ps.saber[0].saberFlags&SFL_SINGLE_BLADE_THROWABLE)//SaberStaff() //using staff
|
|
&& !self->client->ps.dualSabers //only 1, in right hand
|
|
&& !self->client->ps.saber[0].blade[1].active )//only first blade is on
|
|
{//allow power
|
|
//FIXME: externalize this condition seperately?
|
|
}
|
|
else
|
|
{
|
|
if ( forcePower == FP_SABERTHROW && (self->client->ps.saber[0].saberFlags&SFL_NOT_THROWABLE) )
|
|
{//cannot throw this kind of saber
|
|
return qfalse;
|
|
}
|
|
|
|
if ( self->client->ps.saber[0].Active() )
|
|
{
|
|
if ( (self->client->ps.saber[0].saberFlags&SFL_TWO_HANDED) )
|
|
{
|
|
if ( g_saberRestrictForce->integer )
|
|
{
|
|
switch ( forcePower )
|
|
{
|
|
case FP_PUSH:
|
|
case FP_PULL:
|
|
case FP_TELEPATHY:
|
|
case FP_GRIP:
|
|
case FP_LIGHTNING:
|
|
case FP_DRAIN:
|
|
return qfalse;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if ( (self->client->ps.saber[0].saberFlags&SFL_TWO_HANDED)
|
|
|| (self->client->ps.dualSabers && self->client->ps.saber[1].Active()) )
|
|
{//this saber requires the use of two hands OR our other hand is using an active saber too
|
|
if ( (self->client->ps.saber[0].forceRestrictions&(1<<forcePower)) )
|
|
{//this power is verboten when using this saber
|
|
return qfalse;
|
|
}
|
|
}
|
|
}
|
|
if ( self->client->ps.dualSabers && self->client->ps.saber[1].Active() )
|
|
{
|
|
if ( g_saberRestrictForce->integer )
|
|
{
|
|
switch ( forcePower )
|
|
{
|
|
case FP_PUSH:
|
|
case FP_PULL:
|
|
case FP_TELEPATHY:
|
|
case FP_GRIP:
|
|
case FP_LIGHTNING:
|
|
case FP_DRAIN:
|
|
return qfalse;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
if ( (self->client->ps.saber[1].forceRestrictions&(1<<forcePower)) )
|
|
{//this power is verboten when using this saber
|
|
return qfalse;
|
|
}
|
|
}
|
|
}
|
|
|
|
return WP_ForcePowerAvailable( self, forcePower, overrideAmt );
|
|
}
|
|
|
|
void WP_ForcePowerStop( gentity_t *self, forcePowers_t forcePower )
|
|
{
|
|
gentity_t *gripEnt;
|
|
gentity_t *drainEnt;
|
|
|
|
if ( !(self->client->ps.forcePowersActive&(1<<forcePower)) )
|
|
{//umm, wasn't doing it, so...
|
|
return;
|
|
}
|
|
|
|
self->client->ps.forcePowersActive &= ~( 1 << forcePower );
|
|
|
|
switch( (int)forcePower )
|
|
{
|
|
case FP_HEAL:
|
|
//if ( self->client->ps.forcePowerLevel[FP_HEAL] < FORCE_LEVEL_3 )
|
|
{//wasn't an instant heal and heal is now done
|
|
if ( self->client->ps.forcePowerLevel[FP_HEAL] < FORCE_LEVEL_2 )
|
|
{//if in meditation pose, must come out of it
|
|
//FIXME: BOTH_FORCEHEAL_STOP
|
|
if ( self->client->ps.legsAnim == BOTH_FORCEHEAL_START )
|
|
{
|
|
NPC_SetAnim( self, SETANIM_LEGS, BOTH_FORCEHEAL_STOP, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
if ( self->client->ps.torsoAnim == BOTH_FORCEHEAL_START )
|
|
{
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCEHEAL_STOP, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
self->client->ps.saberMove = self->client->ps.saberBounceMove = LS_READY;//don't finish whatever saber anim you may have been in
|
|
self->client->ps.saberBlocked = BLOCKED_NONE;
|
|
}
|
|
}
|
|
WP_StopForceHealEffects( self );
|
|
if (self->health >= self->client->ps.stats[STAT_MAX_HEALTH]/3)
|
|
{
|
|
gi.G2API_ClearSkinGore(self->ghoul2);
|
|
}
|
|
break;
|
|
case FP_LEVITATION:
|
|
self->client->ps.forcePowerDebounce[FP_LEVITATION] = 0;
|
|
break;
|
|
case FP_SPEED:
|
|
if ( !self->s.number )
|
|
{//player using force speed
|
|
if ( g_timescale->value != 1.0 )
|
|
{
|
|
if ( !(self->client->ps.forcePowersActive&(1<<FP_RAGE))||self->client->ps.forcePowerLevel[FP_RAGE] < FORCE_LEVEL_2 )
|
|
{//not slowed down because of force rage
|
|
gi.cvar_set("timescale", "1");
|
|
}
|
|
}
|
|
}
|
|
//FIXME: reset my current anim, keeping current frame, but with proper anim speed
|
|
// otherwise, the anim will continue playing at high speed
|
|
self->s.loopSound = 0;
|
|
break;
|
|
case FP_PUSH:
|
|
break;
|
|
case FP_PULL:
|
|
break;
|
|
case FP_TELEPATHY:
|
|
break;
|
|
case FP_GRIP:
|
|
if ( self->NPC )
|
|
{
|
|
TIMER_Set( self, "gripping", -level.time );
|
|
}
|
|
if ( self->client->ps.forceGripEntityNum < ENTITYNUM_WORLD )
|
|
{
|
|
gripEnt = &g_entities[self->client->ps.forceGripEntityNum];
|
|
if ( gripEnt )
|
|
{
|
|
gripEnt->s.loopSound = 0;
|
|
if ( gripEnt->client )
|
|
{
|
|
gripEnt->client->ps.eFlags &= ~EF_FORCE_GRIPPED;
|
|
if ( self->client->ps.forcePowerLevel[FP_GRIP] > FORCE_LEVEL_1 )
|
|
{//sanity-cap the velocity
|
|
float gripVel = VectorNormalize( gripEnt->client->ps.velocity );
|
|
if ( gripVel > 500.0f )
|
|
{
|
|
gripVel = 500.0f;
|
|
}
|
|
VectorScale( gripEnt->client->ps.velocity, gripVel, gripEnt->client->ps.velocity );
|
|
}
|
|
|
|
//FIXME: they probably dropped their weapon, should we make them flee? Or should AI handle no-weapon behavior?
|
|
//rww - RAGDOLL_BEGIN
|
|
#ifndef JK2_RAGDOLL_GRIPNOHEALTH
|
|
//rww - RAGDOLL_END
|
|
if ( gripEnt->health > 0 )
|
|
//rww - RAGDOLL_BEGIN
|
|
#endif
|
|
//rww - RAGDOLL_END
|
|
{
|
|
int holdTime = 500;
|
|
if ( gripEnt->health > 0 )
|
|
{
|
|
G_AddEvent( gripEnt, EV_WATER_CLEAR, 0 );
|
|
}
|
|
if ( gripEnt->client->ps.forcePowerDebounce[FP_PUSH] > level.time )
|
|
{//they probably pushed out of it
|
|
holdTime = 0;
|
|
}
|
|
else if ( gripEnt->s.weapon == WP_SABER )
|
|
{//jedi recover faster
|
|
holdTime = self->client->ps.forcePowerLevel[FP_GRIP]*200;
|
|
}
|
|
else
|
|
{
|
|
holdTime = self->client->ps.forcePowerLevel[FP_GRIP]*500;
|
|
}
|
|
//stop the anims soon, keep them locked in place for a bit
|
|
if ( gripEnt->client->ps.torsoAnim == BOTH_CHOKE1 || gripEnt->client->ps.torsoAnim == BOTH_CHOKE3 )
|
|
{//stop choking anim on torso
|
|
if ( gripEnt->client->ps.torsoAnimTimer > holdTime )
|
|
{
|
|
gripEnt->client->ps.torsoAnimTimer = holdTime;
|
|
}
|
|
}
|
|
if ( gripEnt->client->ps.legsAnim == BOTH_CHOKE1 || gripEnt->client->ps.legsAnim == BOTH_CHOKE3 )
|
|
{//stop choking anim on legs
|
|
gripEnt->client->ps.legsAnimTimer = 0;
|
|
if ( holdTime )
|
|
{
|
|
//lock them in place for a bit
|
|
gripEnt->client->ps.pm_time = gripEnt->client->ps.torsoAnimTimer;
|
|
gripEnt->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
|
|
if ( gripEnt->s.number )
|
|
{//NPC
|
|
gripEnt->painDebounceTime = level.time + gripEnt->client->ps.torsoAnimTimer;
|
|
}
|
|
else
|
|
{//player
|
|
gripEnt->aimDebounceTime = level.time + gripEnt->client->ps.torsoAnimTimer;
|
|
}
|
|
}
|
|
}
|
|
if ( gripEnt->NPC )
|
|
{
|
|
if ( !(gripEnt->NPC->aiFlags&NPCAI_DIE_ON_IMPACT) )
|
|
{//not falling to their death
|
|
gripEnt->NPC->nextBStateThink = level.time + holdTime;
|
|
}
|
|
//if still alive after stopped gripping, let them wake others up
|
|
if ( gripEnt->health > 0 )
|
|
{
|
|
G_AngerAlert( gripEnt );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
gripEnt->s.eFlags &= ~EF_FORCE_GRIPPED;
|
|
if ( gripEnt->s.eType == ET_MISSILE )
|
|
{//continue normal movement
|
|
if ( gripEnt->s.weapon == WP_THERMAL )
|
|
{
|
|
gripEnt->s.pos.trType = TR_INTERPOLATE;
|
|
}
|
|
else
|
|
{
|
|
gripEnt->s.pos.trType = TR_LINEAR;//FIXME: what about gravity-effected projectiles?
|
|
}
|
|
VectorCopy( gripEnt->currentOrigin, gripEnt->s.pos.trBase );
|
|
gripEnt->s.pos.trTime = level.time;
|
|
}
|
|
else
|
|
{//drop it
|
|
gripEnt->e_ThinkFunc = thinkF_G_RunObject;
|
|
gripEnt->nextthink = level.time + FRAMETIME;
|
|
gripEnt->s.pos.trType = TR_GRAVITY;
|
|
VectorCopy( gripEnt->currentOrigin, gripEnt->s.pos.trBase );
|
|
gripEnt->s.pos.trTime = level.time;
|
|
}
|
|
}
|
|
}
|
|
self->s.loopSound = 0;
|
|
self->client->ps.forceGripEntityNum = ENTITYNUM_NONE;
|
|
self->client->ps.forceGripEntityInitialDist = ENTITYNUM_NONE;
|
|
}
|
|
if ( self->client->ps.torsoAnim == BOTH_FORCEGRIP_HOLD )
|
|
{
|
|
NPC_SetAnim( self, SETANIM_BOTH, BOTH_FORCEGRIP_RELEASE, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
break;
|
|
case FP_LIGHTNING:
|
|
if ( self->NPC )
|
|
{
|
|
TIMER_Set( self, "holdLightning", -level.time );
|
|
}
|
|
if ( self->client->ps.torsoAnim == BOTH_FORCELIGHTNING_HOLD
|
|
|| self->client->ps.torsoAnim == BOTH_FORCELIGHTNING_START )
|
|
{
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCELIGHTNING_RELEASE, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
else if ( self->client->ps.torsoAnim == BOTH_FORCE_2HANDEDLIGHTNING_HOLD
|
|
|| self->client->ps.torsoAnim == BOTH_FORCE_2HANDEDLIGHTNING_START )
|
|
{
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCE_2HANDEDLIGHTNING_RELEASE, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
if ( self->client->ps.forcePowerLevel[FP_LIGHTNING] < FORCE_LEVEL_2 )
|
|
{//don't do it again for 3 seconds, minimum... FIXME: this should be automatic once regeneration is slower (normal)
|
|
self->client->ps.forcePowerDebounce[FP_LIGHTNING] = level.time + 3000;//FIXME: define?
|
|
}
|
|
else
|
|
{//stop the looping sound
|
|
self->client->ps.forcePowerDebounce[FP_LIGHTNING] = level.time + 1000;//FIXME: define?
|
|
self->s.loopSound = 0;
|
|
}
|
|
break;
|
|
case FP_RAGE:
|
|
self->client->ps.forceRageRecoveryTime = level.time + 10000;//recover for 10 seconds
|
|
if ( self->client->ps.forcePowerDuration[FP_RAGE] > level.time )
|
|
{//still had time left, we cut it short
|
|
self->client->ps.forceRageRecoveryTime -= (self->client->ps.forcePowerDuration[FP_RAGE] - level.time);//minus however much time you had left when you cut it short
|
|
}
|
|
if ( !self->s.number )
|
|
{//player using force speed
|
|
if ( g_timescale->value != 1.0 )
|
|
{
|
|
if ( !(self->client->ps.forcePowersActive&(1<<FP_SPEED)) )
|
|
{//not slowed down because of force speed
|
|
gi.cvar_set("timescale", "1");
|
|
}
|
|
}
|
|
}
|
|
//FIXME: reset my current anim, keeping current frame, but with proper anim speed
|
|
// otherwise, the anim will continue playing at high speed
|
|
self->s.loopSound = 0;
|
|
if ( self->NPC )
|
|
{
|
|
Jedi_RageStop( self );
|
|
}
|
|
if ( self->chestBolt != -1 )
|
|
{
|
|
G_StopEffect("force/rage2", self->playerModel, self->chestBolt, self->s.number );
|
|
}
|
|
break;
|
|
case FP_DRAIN:
|
|
if ( self->NPC )
|
|
{
|
|
TIMER_Set( self, "draining", -level.time );
|
|
}
|
|
if ( self->client->ps.forcePowerLevel[FP_DRAIN] < FORCE_LEVEL_2 )
|
|
{//don't do it again for 3 seconds, minimum... FIXME: this should be automatic once regeneration is slower (normal)
|
|
self->client->ps.forcePowerDebounce[FP_DRAIN] = level.time + 3000;//FIXME: define?
|
|
}
|
|
else
|
|
{//stop the looping sound
|
|
self->client->ps.forcePowerDebounce[FP_DRAIN] = level.time + 1000;//FIXME: define?
|
|
self->s.loopSound = 0;
|
|
}
|
|
//drop them
|
|
if ( self->client->ps.forceDrainEntityNum < ENTITYNUM_WORLD )
|
|
{
|
|
drainEnt = &g_entities[self->client->ps.forceDrainEntityNum];
|
|
if ( drainEnt )
|
|
{
|
|
if ( drainEnt->client )
|
|
{
|
|
drainEnt->client->ps.eFlags &= ~EF_FORCE_DRAINED;
|
|
//VectorClear( drainEnt->client->ps.velocity );
|
|
if ( drainEnt->health > 0 )
|
|
{
|
|
if ( drainEnt->client->ps.forcePowerDebounce[FP_PUSH] > level.time )
|
|
{//they probably pushed out of it
|
|
}
|
|
else
|
|
{
|
|
//NPC_SetAnim( drainEnt, SETANIM_BOTH, BOTH_HUGGEESTOP1, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
if ( drainEnt->client->ps.torsoAnim != BOTH_FORCEPUSH )
|
|
{//don't stop the push
|
|
drainEnt->client->ps.torsoAnimTimer = 0;
|
|
}
|
|
drainEnt->client->ps.legsAnimTimer = 0;
|
|
}
|
|
if ( drainEnt->NPC )
|
|
{//if still alive after stopped draining, let them wake others up
|
|
G_AngerAlert( drainEnt );
|
|
}
|
|
}
|
|
else
|
|
{//leave the effect playing on them for a few seconds
|
|
//drainEnt->client->ps.eFlags |= EF_FORCE_DRAINED;
|
|
drainEnt->s.powerups |= ( 1 << PW_DRAINED );
|
|
drainEnt->client->ps.powerups[PW_DRAINED] = level.time + Q_irand( 1000, 4000 );
|
|
}
|
|
}
|
|
}
|
|
self->client->ps.forceDrainEntityNum = ENTITYNUM_NONE;
|
|
}
|
|
if ( self->client->ps.torsoAnim == BOTH_HUGGER1 )
|
|
{//old anim
|
|
NPC_SetAnim( self, SETANIM_BOTH, BOTH_HUGGERSTOP1, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
else if ( self->client->ps.torsoAnim == BOTH_FORCE_DRAIN_GRAB_START
|
|
|| self->client->ps.torsoAnim == BOTH_FORCE_DRAIN_GRAB_HOLD )
|
|
{//new anim
|
|
NPC_SetAnim( self, SETANIM_BOTH, BOTH_FORCE_DRAIN_GRAB_END, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
else if ( self->client->ps.torsoAnim == BOTH_FORCE_DRAIN_HOLD
|
|
|| self->client->ps.torsoAnim == BOTH_FORCE_DRAIN_START )
|
|
{
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCE_DRAIN_RELEASE, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
break;
|
|
case FP_PROTECT:
|
|
self->s.loopSound = 0;
|
|
break;
|
|
case FP_ABSORB:
|
|
self->s.loopSound = 0;
|
|
if ( self->client->ps.legsAnim == BOTH_FORCE_ABSORB_START )
|
|
{
|
|
NPC_SetAnim( self, SETANIM_LEGS, BOTH_FORCE_ABSORB_END, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
if ( self->client->ps.torsoAnim == BOTH_FORCE_ABSORB_START )
|
|
{
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCE_ABSORB_END, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
if ( self->client->ps.forcePowerLevel[FP_ABSORB] < FORCE_LEVEL_2 )
|
|
{//was stuck, free us in case we interrupted it or something
|
|
self->client->ps.weaponTime = 0;
|
|
self->client->ps.pm_flags &= ~PMF_TIME_KNOCKBACK;
|
|
self->client->ps.pm_time = 0;
|
|
if ( self->s.number )
|
|
{//NPC
|
|
self->painDebounceTime = 0;
|
|
}
|
|
else
|
|
{//player
|
|
self->aimDebounceTime = 0;
|
|
}
|
|
}
|
|
break;
|
|
case FP_SEE:
|
|
self->s.loopSound = 0;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void WP_ForceForceThrow( gentity_t *thrower )
|
|
{
|
|
if ( !thrower || !thrower->client )
|
|
{
|
|
return;
|
|
}
|
|
qboolean relock = qfalse;
|
|
if ( !(thrower->client->ps.forcePowersKnown&(1<<FP_PUSH)) )
|
|
{//give them push just for this specific purpose
|
|
thrower->client->ps.forcePowersKnown |= (1<<FP_PUSH);
|
|
thrower->client->ps.forcePowerLevel[FP_PUSH] = FORCE_LEVEL_1;
|
|
}
|
|
|
|
if ( thrower->NPC
|
|
&& (thrower->NPC->aiFlags&NPCAI_HEAL_ROSH)
|
|
&& (thrower->flags&FL_LOCK_PLAYER_WEAPONS) )
|
|
{
|
|
thrower->flags &= ~FL_LOCK_PLAYER_WEAPONS;
|
|
relock = qtrue;
|
|
}
|
|
|
|
ForceThrowEx( thrower, qfalse, qfalse, qtrue );
|
|
|
|
if ( relock )
|
|
{
|
|
thrower->flags |= FL_LOCK_PLAYER_WEAPONS;
|
|
}
|
|
|
|
if ( thrower )
|
|
{//take it back off
|
|
thrower->client->ps.forcePowersKnown &= ~(1<<FP_PUSH);
|
|
thrower->client->ps.forcePowerLevel[FP_PUSH] = FORCE_LEVEL_0;
|
|
}
|
|
}
|
|
|
|
extern qboolean PM_ForceJumpingUp( gentity_t *gent );
|
|
static void WP_ForcePowerRun( gentity_t *self, forcePowers_t forcePower, usercmd_t *cmd )
|
|
{
|
|
float speed, newSpeed;
|
|
gentity_t *gripEnt, *drainEnt;
|
|
vec3_t angles, dir, gripOrg, gripEntOrg;
|
|
float dist;
|
|
extern usercmd_t ucmd;
|
|
|
|
switch( (int)forcePower )
|
|
{
|
|
case FP_HEAL:
|
|
if ( self->client->ps.forceHealCount >= FP_MaxForceHeal(self) || self->health >= self->client->ps.stats[STAT_MAX_HEALTH] )
|
|
{//fully healed or used up all 25
|
|
if ( !Q3_TaskIDPending( self, TID_CHAN_VOICE ) )
|
|
{
|
|
int index = Q_irand( 1, 4 );
|
|
if ( self->s.number < MAX_CLIENTS )
|
|
{
|
|
G_SoundOnEnt( self, CHAN_VOICE, va( "sound/weapons/force/heal%d_%c.mp3", index, g_sex->string[0] ) );
|
|
}
|
|
else if ( self->NPC )
|
|
{
|
|
if ( self->NPC->blockedSpeechDebounceTime <= level.time )
|
|
{//enough time has passed since our last speech
|
|
if ( Q3_TaskIDPending( self, TID_CHAN_VOICE ) )
|
|
{//not playing a scripted line
|
|
//say "Ahhh...."
|
|
if ( self->NPC->stats.sex == SEX_MALE
|
|
|| self->NPC->stats.sex == SEX_NEUTRAL )
|
|
{
|
|
G_SoundOnEnt( self, CHAN_VOICE, va( "sound/weapons/force/heal%d_m.mp3", index ) );
|
|
}
|
|
else//all other sexes use female sounds
|
|
{
|
|
G_SoundOnEnt( self, CHAN_VOICE, va( "sound/weapons/force/heal%d_f.mp3", index ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
WP_ForcePowerStop( self, forcePower );
|
|
}
|
|
else if ( self->client->ps.forcePowerLevel[FP_HEAL] < FORCE_LEVEL_3 && ( (cmd->buttons&BUTTON_ATTACK) || (cmd->buttons&BUTTON_ALT_ATTACK) || self->painDebounceTime > level.time || (self->client->ps.weaponTime&&self->client->ps.weapon!=WP_NONE) ) )
|
|
{//attacked or was hit while healing...
|
|
//stop healing
|
|
WP_ForcePowerStop( self, forcePower );
|
|
}
|
|
else if ( self->client->ps.forcePowerLevel[FP_HEAL] < FORCE_LEVEL_2 && ( cmd->rightmove || cmd->forwardmove || cmd->upmove > 0 ) )
|
|
{//moved while healing... FIXME: also, in WP_ForcePowerStart, stop healing if any other force power is used
|
|
//stop healing
|
|
WP_ForcePowerStop( self, forcePower );
|
|
}
|
|
else if ( self->client->ps.forcePowerDebounce[FP_HEAL] < level.time )
|
|
{//time to heal again
|
|
if ( WP_ForcePowerAvailable( self, forcePower, 4 ) )
|
|
{//have available power
|
|
int healInterval = FP_ForceHealInterval( self );
|
|
int healAmount = 1;//hard, normal healing rate
|
|
if ( self->s.number < MAX_CLIENTS )
|
|
{
|
|
if ( g_spskill->integer == 1 )
|
|
{//medium, heal twice as fast
|
|
healAmount *= 2;
|
|
}
|
|
else if ( g_spskill->integer == 0 )
|
|
{//easy, heal 3 times as fast...
|
|
healAmount *= 3;
|
|
}
|
|
}
|
|
if ( self->health + healAmount > self->client->ps.stats[STAT_MAX_HEALTH] )
|
|
{
|
|
healAmount = self->client->ps.stats[STAT_MAX_HEALTH] - self->health;
|
|
}
|
|
self->health += healAmount;
|
|
self->client->ps.forceHealCount += healAmount;
|
|
self->client->ps.forcePowerDebounce[FP_HEAL] = level.time + healInterval;
|
|
WP_ForcePowerDrain( self, forcePower, 4 );
|
|
}
|
|
else
|
|
{//stop
|
|
WP_ForcePowerStop( self, forcePower );
|
|
}
|
|
}
|
|
break;
|
|
case FP_LEVITATION:
|
|
if ( self->client->ps.groundEntityNum != ENTITYNUM_NONE && !self->client->ps.forceJumpZStart )
|
|
{//done with jump
|
|
WP_ForcePowerStop( self, forcePower );
|
|
}
|
|
else
|
|
{
|
|
if ( PM_ForceJumpingUp( self ) )
|
|
{//holding jump in air
|
|
if ( cmd->upmove > 10 )
|
|
{//still trying to go up
|
|
if ( WP_ForcePowerAvailable( self, FP_LEVITATION, 1 ) )
|
|
{
|
|
if ( self->client->ps.forcePowerDebounce[FP_LEVITATION] < level.time )
|
|
{
|
|
WP_ForcePowerDrain( self, FP_LEVITATION, 5 );
|
|
self->client->ps.forcePowerDebounce[FP_LEVITATION] = level.time + 100;
|
|
}
|
|
self->client->ps.forcePowersActive |= ( 1 << FP_LEVITATION );
|
|
self->client->ps.forceJumpCharge = 1;//just used as a flag for the player, cleared when he lands
|
|
}
|
|
else
|
|
{//cut the jump short
|
|
WP_ForcePowerStop( self, forcePower );
|
|
}
|
|
}
|
|
else
|
|
{//cut the jump short
|
|
WP_ForcePowerStop( self, forcePower );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
WP_ForcePowerStop( self, forcePower );
|
|
}
|
|
}
|
|
break;
|
|
case FP_SPEED:
|
|
speed = forceSpeedValue[self->client->ps.forcePowerLevel[FP_SPEED]];
|
|
if ( !self->s.number )
|
|
{//player using force speed
|
|
if ( !(self->client->ps.forcePowersActive&(1<<FP_RAGE))
|
|
|| self->client->ps.forcePowerLevel[FP_SPEED] >= self->client->ps.forcePowerLevel[FP_RAGE] )
|
|
{//either not using rage or rage is at a lower level than speed
|
|
gi.cvar_set("timescale", va("%4.2f", speed));
|
|
if ( g_timescale->value > speed )
|
|
{
|
|
newSpeed = g_timescale->value - 0.05;
|
|
if ( newSpeed < speed )
|
|
{
|
|
newSpeed = speed;
|
|
}
|
|
gi.cvar_set("timescale", va("%4.2f", newSpeed));
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case FP_PUSH:
|
|
break;
|
|
case FP_PULL:
|
|
break;
|
|
case FP_TELEPATHY:
|
|
break;
|
|
case FP_GRIP:
|
|
if ( !WP_ForcePowerAvailable( self, FP_GRIP, 0 )
|
|
|| (self->client->ps.forcePowerLevel[FP_GRIP]>FORCE_LEVEL_1&&!self->s.number&&!(cmd->buttons&BUTTON_FORCEGRIP)) )
|
|
{
|
|
WP_ForcePowerStop( self, FP_GRIP );
|
|
return;
|
|
}
|
|
else if ( self->client->ps.forceGripEntityNum >= 0 && self->client->ps.forceGripEntityNum < ENTITYNUM_WORLD )
|
|
{
|
|
gripEnt = &g_entities[self->client->ps.forceGripEntityNum];
|
|
|
|
if ( !gripEnt || !gripEnt->inuse )
|
|
{//invalid or freed ent
|
|
WP_ForcePowerStop( self, FP_GRIP );
|
|
return;
|
|
}
|
|
else
|
|
//rww - RAGDOLL_BEGIN
|
|
#ifndef JK2_RAGDOLL_GRIPNOHEALTH
|
|
//rww - RAGDOLL_END
|
|
if ( gripEnt->health <= 0 && gripEnt->takedamage )//FIXME: what about things that never had health or lose takedamage when they die?
|
|
{//either invalid ent, or dead ent
|
|
WP_ForcePowerStop( self, FP_GRIP );
|
|
return;
|
|
}
|
|
else
|
|
//rww - RAGDOLL_BEGIN
|
|
#endif
|
|
//rww - RAGDOLL_END
|
|
if ( self->client->ps.forcePowerLevel[FP_GRIP] == FORCE_LEVEL_1
|
|
&& gripEnt->client
|
|
&& gripEnt->client->ps.groundEntityNum == ENTITYNUM_NONE
|
|
&& gripEnt->client->moveType != MT_FLYSWIM )
|
|
{
|
|
WP_ForcePowerStop( self, FP_GRIP );
|
|
return;
|
|
}
|
|
else if ( gripEnt->client && gripEnt->client->moveType == MT_FLYSWIM && VectorLengthSquared( gripEnt->client->ps.velocity ) > (300*300) )
|
|
{//flying creature broke free
|
|
WP_ForcePowerStop( self, FP_GRIP );
|
|
return;
|
|
}
|
|
else if ( gripEnt->client
|
|
&& gripEnt->health>0 //dead dudes don't fly
|
|
&& (gripEnt->client->NPC_class == CLASS_BOBAFETT || gripEnt->client->NPC_class == CLASS_ROCKETTROOPER)
|
|
&& self->client->ps.forcePowerDebounce[FP_GRIP] < level.time
|
|
&& !Q_irand( 0, 3 )
|
|
)
|
|
{//boba fett - fly away!
|
|
gripEnt->client->ps.forceJumpCharge = 0;//so we don't play the force flip anim
|
|
gripEnt->client->ps.velocity[2] = 250;
|
|
gripEnt->client->ps.forceJumpZStart = gripEnt->currentOrigin[2];//so we don't take damage if we land at same height
|
|
gripEnt->client->ps.pm_flags |= PMF_JUMPING;
|
|
G_AddEvent( gripEnt, EV_JUMP, 0 );
|
|
JET_FlyStart( gripEnt );
|
|
WP_ForcePowerStop( self, FP_GRIP );
|
|
return;
|
|
}
|
|
else if ( gripEnt->NPC
|
|
&& gripEnt->client
|
|
&& gripEnt->client->ps.forcePowersKnown
|
|
&& (gripEnt->client->NPC_class==CLASS_REBORN||gripEnt->client->ps.weapon==WP_SABER)
|
|
&& !Jedi_CultistDestroyer(gripEnt)
|
|
&& !Q_irand( 0, 100-(gripEnt->NPC->stats.evasion*8)-(g_spskill->integer*20) ) )
|
|
{//a jedi who broke free FIXME: maybe have some minimum grip length- a reaction time?
|
|
WP_ForceForceThrow( gripEnt );
|
|
//FIXME: I need to go into some pushed back anim...
|
|
WP_ForcePowerStop( self, FP_GRIP );
|
|
return;
|
|
}
|
|
else if ( PM_SaberInAttack( self->client->ps.saberMove )
|
|
|| PM_SaberInStart( self->client->ps.saberMove ) )
|
|
{//started an attack
|
|
WP_ForcePowerStop( self, FP_GRIP );
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
int gripLevel = self->client->ps.forcePowerLevel[FP_GRIP];
|
|
if ( gripEnt->client )
|
|
{
|
|
gripLevel = WP_AbsorbConversion( gripEnt, gripEnt->client->ps.forcePowerLevel[FP_ABSORB], self, FP_GRIP, self->client->ps.forcePowerLevel[FP_GRIP], forcePowerNeeded[gripLevel] );
|
|
}
|
|
if ( !gripLevel )
|
|
{
|
|
WP_ForcePowerStop( self, forcePower );
|
|
return;
|
|
}
|
|
|
|
if ( self->client->ps.forcePowerLevel[FP_GRIP] > FORCE_LEVEL_1 )
|
|
{//holding it
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCEGRIP_HOLD, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
if ( self->client->ps.torsoAnimTimer < 100 ){//we were already playing this anim, we didn't want to restart it, but we want to hold it for at least 100ms, sooo....
|
|
|
|
self->client->ps.torsoAnimTimer = 100;
|
|
}
|
|
}
|
|
|
|
bool isFirstPersonPlayer = (self->client->ps.clientNum == 0 && !cg.renderingThirdPerson);
|
|
//get their org
|
|
if (isFirstPersonPlayer)
|
|
{
|
|
vec3_t origin;
|
|
BG_CalculateVROffHandPosition(origin, angles);
|
|
}
|
|
else
|
|
{
|
|
VectorCopy( self->client->ps.viewangles, angles );
|
|
}
|
|
|
|
angles[0] -= 10;
|
|
AngleVectors( angles, dir, NULL, NULL );
|
|
if ( gripEnt->client )
|
|
{//move
|
|
VectorCopy( gripEnt->client->renderInfo.headPoint, gripEntOrg );
|
|
}
|
|
else
|
|
{
|
|
VectorCopy( gripEnt->currentOrigin, gripEntOrg );
|
|
}
|
|
|
|
if (isFirstPersonPlayer &&
|
|
self->client->ps.forcePowerLevel[FP_GRIP] > FORCE_LEVEL_2 &&
|
|
self->client->ps.forceGripEntityInitialDist == ENTITYNUM_NONE)
|
|
{
|
|
vec3_t diff;
|
|
VectorSubtract(vr->offhandposition[0], vr->hmdposition, diff);
|
|
self->client->ps.forceGripEntityInitialDist = VectorLength(diff);
|
|
}
|
|
|
|
//how far are they
|
|
dist = Distance( self->client->renderInfo.handLPoint, gripEntOrg );
|
|
if ( self->client->ps.forcePowerLevel[FP_GRIP] == FORCE_LEVEL_2 &&
|
|
(!InFront( gripEntOrg, self->client->renderInfo.handLPoint, self->client->ps.viewangles, 0.3f ) ||
|
|
DistanceSquared( gripEntOrg, self->client->renderInfo.handLPoint ) > FORCE_GRIP_DIST_SQUARED))
|
|
{//must face them
|
|
WP_ForcePowerStop( self, FP_GRIP );
|
|
return;
|
|
}
|
|
|
|
//check for lift or carry
|
|
if ( self->client->ps.forcePowerLevel[FP_GRIP] > FORCE_LEVEL_2
|
|
&& (!gripEnt->client || (!gripEnt->message&&!(gripEnt->flags&FL_NO_KNOCKBACK))) )
|
|
{//carry
|
|
if (isFirstPersonPlayer) {
|
|
vec3_t diff;
|
|
VectorSubtract(vr->offhandposition[0], vr->hmdposition, diff);
|
|
float length = VectorLength(diff);
|
|
float movedLength = (length - self->client->ps.forceGripEntityInitialDist) * cg_worldScale.value;
|
|
if (fabs(movedLength) > 1.0f) {
|
|
dist += movedLength * 5.0f;
|
|
}
|
|
if (dist > 384) {
|
|
dist = 384;
|
|
} else if (dist < 32) {
|
|
dist = 32;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//cap dist
|
|
if (dist > FORCE_GRIP_3_MAX_DIST)
|
|
{
|
|
dist = FORCE_GRIP_3_MAX_DIST;
|
|
}
|
|
else if (dist < FORCE_GRIP_3_MIN_DIST)
|
|
{
|
|
dist = FORCE_GRIP_3_MIN_DIST;
|
|
}
|
|
}
|
|
|
|
VectorMA( self->client->renderInfo.handLPoint, dist, dir, gripOrg );
|
|
}
|
|
else if ( self->client->ps.forcePowerLevel[FP_GRIP] > FORCE_LEVEL_1 )
|
|
{//just lift
|
|
VectorCopy( self->client->ps.forceGripOrg, gripOrg );
|
|
}
|
|
else
|
|
{
|
|
VectorCopy( gripEnt->currentOrigin, gripOrg );
|
|
}
|
|
if ( self->client->ps.forcePowerLevel[FP_GRIP] > FORCE_LEVEL_1 )
|
|
{//if holding him, make sure there's a clear LOS between my hand and him
|
|
trace_t gripTrace;
|
|
gi.trace( &gripTrace, self->client->renderInfo.handLPoint, NULL, NULL, gripEntOrg, ENTITYNUM_NONE, MASK_FORCE_PUSH, (EG2_Collision)0, 0 );
|
|
if ( gripTrace.startsolid
|
|
|| gripTrace.allsolid
|
|
|| gripTrace.fraction < 1.0f )
|
|
{//no clear trace, drop them
|
|
WP_ForcePowerStop( self, FP_GRIP );
|
|
return;
|
|
}
|
|
}
|
|
//now move them
|
|
if ( gripEnt->client )
|
|
{
|
|
if ( self->client->ps.forcePowerLevel[FP_GRIP] > FORCE_LEVEL_1 )
|
|
{//level 1 just holds them
|
|
VectorSubtract( gripOrg, gripEntOrg, gripEnt->client->ps.velocity );
|
|
if ( self->client->ps.forcePowerLevel[FP_GRIP] > FORCE_LEVEL_2
|
|
&& (!gripEnt->client || (!gripEnt->message&&!(gripEnt->flags&FL_NO_KNOCKBACK)) ) )
|
|
{//level 2 just lifts them
|
|
float gripDist = VectorNormalize( gripEnt->client->ps.velocity )/3.0f;
|
|
if ( gripDist < 20.0f )
|
|
{
|
|
if (gripDist<2.0f)
|
|
{
|
|
VectorClear(gripEnt->client->ps.velocity);
|
|
}
|
|
else
|
|
{
|
|
VectorScale( gripEnt->client->ps.velocity, (gripDist*gripDist), gripEnt->client->ps.velocity );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
VectorScale( gripEnt->client->ps.velocity, (gripDist*gripDist), gripEnt->client->ps.velocity );
|
|
}
|
|
}
|
|
}
|
|
//stop them from thinking
|
|
gripEnt->client->ps.pm_time = 2000;
|
|
gripEnt->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
|
|
if ( gripEnt->NPC )
|
|
{
|
|
if ( !(gripEnt->NPC->aiFlags&NPCAI_DIE_ON_IMPACT) )
|
|
{//not falling to their death
|
|
gripEnt->NPC->nextBStateThink = level.time + 2000;
|
|
}
|
|
if ( self->client->ps.forcePowerLevel[FP_GRIP] > FORCE_LEVEL_1 )
|
|
{//level 1 just holds them
|
|
vectoangles( dir, angles );
|
|
gripEnt->NPC->desiredYaw = AngleNormalize180(angles[YAW]+180);
|
|
gripEnt->NPC->desiredPitch = -angles[PITCH];
|
|
SaveNPCGlobals();
|
|
SetNPCGlobals( gripEnt );
|
|
NPC_UpdateAngles( qtrue, qtrue );
|
|
gripEnt->NPC->last_ucmd.angles[0] = ucmd.angles[0];
|
|
gripEnt->NPC->last_ucmd.angles[1] = ucmd.angles[1];
|
|
gripEnt->NPC->last_ucmd.angles[2] = ucmd.angles[2];
|
|
RestoreNPCGlobals();
|
|
//FIXME: why does he turn back to his original angles once he dies or is let go?
|
|
}
|
|
}
|
|
else if ( !gripEnt->s.number )
|
|
{
|
|
//vectoangles( dir, angles );
|
|
//gripEnt->client->ps.viewangles[0] = -angles[0];
|
|
//gripEnt->client->ps.viewangles[1] = AngleNormalize180(angles[YAW]+180);
|
|
gripEnt->enemy = self;
|
|
NPC_SetLookTarget( gripEnt, self->s.number, level.time+1000 );
|
|
}
|
|
|
|
gripEnt->client->ps.eFlags |= EF_FORCE_GRIPPED;
|
|
//dammit! Make sure that saber stays off!
|
|
WP_DeactivateSaber( gripEnt );
|
|
}
|
|
else
|
|
{//move
|
|
if ( self->client->ps.forcePowerLevel[FP_GRIP] > FORCE_LEVEL_1 )
|
|
{//level 1 just holds them
|
|
VectorCopy( gripEnt->currentOrigin, gripEnt->s.pos.trBase );
|
|
VectorSubtract( gripOrg, gripEntOrg, gripEnt->s.pos.trDelta );
|
|
if ( self->client->ps.forcePowerLevel[FP_GRIP] > FORCE_LEVEL_2
|
|
&& (!gripEnt->client || (!gripEnt->message&&!(gripEnt->flags&FL_NO_KNOCKBACK))) )
|
|
{//level 2 just lifts them
|
|
VectorScale( gripEnt->s.pos.trDelta, 10, gripEnt->s.pos.trDelta );
|
|
}
|
|
gripEnt->s.pos.trType = TR_LINEAR;
|
|
gripEnt->s.pos.trTime = level.time;
|
|
}
|
|
|
|
gripEnt->s.eFlags |= EF_FORCE_GRIPPED;
|
|
}
|
|
|
|
//Shouldn't this be discovered?
|
|
//AddSightEvent( self, gripOrg, 128, AEL_DANGER, 20 );
|
|
AddSightEvent( self, gripOrg, 128, AEL_DISCOVERED, 20 );
|
|
|
|
if ( self->client->ps.forcePowerDebounce[FP_GRIP] < level.time )
|
|
{
|
|
//GEntity_PainFunc( gripEnt, self, self, gripOrg, 0, MOD_CRUSH );
|
|
if ( !gripEnt->client
|
|
|| gripEnt->client->NPC_class != CLASS_VEHICLE
|
|
|| (gripEnt->m_pVehicle
|
|
&& gripEnt->m_pVehicle->m_pVehicleInfo
|
|
&& gripEnt->m_pVehicle->m_pVehicleInfo->type == VH_ANIMAL) )
|
|
{//we don't damage the empty vehicle
|
|
gripEnt->painDebounceTime = 0;
|
|
int gripDmg = forceGripDamage[self->client->ps.forcePowerLevel[FP_GRIP]];
|
|
if ( gripLevel != -1 )
|
|
{
|
|
if ( gripLevel == 1 )
|
|
{
|
|
gripDmg = floor((float)gripDmg/3.0f);
|
|
}
|
|
else //if ( gripLevel == 2 )
|
|
{
|
|
gripDmg = floor((float)gripDmg/1.5f);
|
|
}
|
|
}
|
|
G_Damage( gripEnt, self, self, dir, gripOrg, gripDmg, DAMAGE_NO_ARMOR, MOD_CRUSH );//MOD_???
|
|
}
|
|
if ( gripEnt->s.number )
|
|
{
|
|
if ( self->client->ps.forcePowerLevel[FP_GRIP] > FORCE_LEVEL_2 )
|
|
{//do damage faster at level 3
|
|
self->client->ps.forcePowerDebounce[FP_GRIP] = level.time + Q_irand( 150, 750 );
|
|
}
|
|
else
|
|
{
|
|
self->client->ps.forcePowerDebounce[FP_GRIP] = level.time + Q_irand( 250, 1000 );
|
|
}
|
|
}
|
|
else
|
|
{//player takes damage faster
|
|
self->client->ps.forcePowerDebounce[FP_GRIP] = level.time + Q_irand( 100, 600 );
|
|
}
|
|
if ( forceGripDamage[self->client->ps.forcePowerLevel[FP_GRIP]] > 0 )
|
|
{//no damage at level 1
|
|
WP_ForcePowerDrain( self, FP_GRIP, 3 );
|
|
}
|
|
if ( self->client->NPC_class == CLASS_KYLE
|
|
&& (self->spawnflags&1) )
|
|
{//"Boss" Kyle
|
|
if ( gripEnt->client )
|
|
{
|
|
if ( !Q_irand( 0, 2 ) )
|
|
{//toss him aside!
|
|
vec3_t vRt;
|
|
AngleVectors( self->currentAngles, NULL, vRt, NULL );
|
|
//stop gripping
|
|
TIMER_Set( self, "gripping", -level.time );
|
|
WP_ForcePowerStop( self, FP_GRIP );
|
|
//now toss him
|
|
if ( Q_irand( 0, 1 ) )
|
|
{//throw him to my left
|
|
NPC_SetAnim( self, SETANIM_BOTH, BOTH_TOSS1, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
VectorScale( vRt, -1500.0f, gripEnt->client->ps.velocity );
|
|
G_Knockdown( gripEnt, self, vRt, 500, qfalse );
|
|
}
|
|
else
|
|
{//throw him to my right
|
|
NPC_SetAnim( self, SETANIM_BOTH, BOTH_TOSS2, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
VectorScale( vRt, 1500.0f, gripEnt->client->ps.velocity );
|
|
G_Knockdown( gripEnt, self, vRt, 500, qfalse );
|
|
}
|
|
//don't do anything for a couple seconds
|
|
self->client->ps.weaponTime = self->client->ps.torsoAnimTimer + 2000;
|
|
self->painDebounceTime = level.time + self->client->ps.weaponTime;
|
|
//stop moving
|
|
VectorClear( self->client->ps.velocity );
|
|
VectorClear( self->client->ps.moveDir );
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//WP_ForcePowerDrain( self, FP_GRIP, 0 );
|
|
if ( !gripEnt->enemy )
|
|
{
|
|
if ( gripEnt->client
|
|
&& gripEnt->client->playerTeam == TEAM_PLAYER
|
|
&& self->s.number < MAX_CLIENTS
|
|
&& self->client
|
|
&& self->client->playerTeam == TEAM_PLAYER )
|
|
{//this shouldn't make allies instantly turn on you, let the damage->pain routine determine how allies should react to this
|
|
}
|
|
else
|
|
{
|
|
G_SetEnemy( gripEnt, self );
|
|
}
|
|
}
|
|
}
|
|
if ( gripEnt->client && gripEnt->health > 0 )
|
|
{
|
|
int anim = BOTH_CHOKE3; //left-handed choke
|
|
if ( gripEnt->client->ps.weapon == WP_NONE || gripEnt->client->ps.weapon == WP_MELEE )
|
|
{
|
|
anim = BOTH_CHOKE1; //two-handed choke
|
|
}
|
|
if ( self->client->ps.forcePowerLevel[FP_GRIP] < FORCE_LEVEL_2 )
|
|
{//still on ground, only set anim on torso
|
|
NPC_SetAnim( gripEnt, SETANIM_TORSO, anim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
else
|
|
{//in air, set on whole body
|
|
NPC_SetAnim( gripEnt, SETANIM_BOTH, anim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
gripEnt->painDebounceTime = level.time + 2000;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case FP_LIGHTNING:
|
|
if ( self->client->ps.forcePowerLevel[FP_LIGHTNING] > FORCE_LEVEL_1 )
|
|
{//higher than level 1
|
|
if ( cmd->buttons & BUTTON_FORCE_LIGHTNING )
|
|
{//holding it keeps it going
|
|
self->client->ps.forcePowerDuration[FP_LIGHTNING] = level.time + 500;
|
|
ForceLightningAnim( self );
|
|
}
|
|
}
|
|
if ( !WP_ForcePowerAvailable( self, forcePower, 0 ) )
|
|
{
|
|
WP_ForcePowerStop( self, forcePower );
|
|
}
|
|
else
|
|
{
|
|
ForceShootLightning( self );
|
|
if ( self->client->ps.torsoAnim == BOTH_FORCE_2HANDEDLIGHTNING
|
|
|| self->client->ps.torsoAnim == BOTH_FORCE_2HANDEDLIGHTNING_START
|
|
|| self->client->ps.torsoAnim == BOTH_FORCE_2HANDEDLIGHTNING_HOLD
|
|
|| self->client->ps.torsoAnim == BOTH_FORCE_2HANDEDLIGHTNING_RELEASE )
|
|
{//jackin' 'em up, Palpatine-style
|
|
//extra cost
|
|
WP_ForcePowerDrain( self, forcePower, 0 );
|
|
}
|
|
WP_ForcePowerDrain( self, forcePower, 0 );
|
|
}
|
|
break;
|
|
//new Jedi Academy force powers
|
|
case FP_RAGE:
|
|
if (self->health < 1)
|
|
{
|
|
WP_ForcePowerStop(self, forcePower);
|
|
break;
|
|
}
|
|
if (self->client->ps.forceRageDrainTime < level.time)
|
|
{
|
|
int addTime = 400;
|
|
|
|
self->health -= 2;
|
|
|
|
if (self->client->ps.forcePowerLevel[FP_RAGE] == FORCE_LEVEL_1)
|
|
{
|
|
addTime = 100;
|
|
}
|
|
else if (self->client->ps.forcePowerLevel[FP_RAGE] == FORCE_LEVEL_2)
|
|
{
|
|
addTime = 250;
|
|
}
|
|
else if (self->client->ps.forcePowerLevel[FP_RAGE] == FORCE_LEVEL_3)
|
|
{
|
|
addTime = 500;
|
|
}
|
|
self->client->ps.forceRageDrainTime = level.time + addTime;
|
|
}
|
|
|
|
if ( self->health < 1 )
|
|
{
|
|
self->health = 1;
|
|
//WP_ForcePowerStop( self, forcePower );
|
|
}
|
|
else
|
|
{
|
|
self->client->ps.stats[STAT_HEALTH] = self->health;
|
|
|
|
speed = forceSpeedValue[self->client->ps.forcePowerLevel[FP_RAGE]-1];
|
|
if ( !self->s.number )
|
|
{//player using force rage
|
|
if ( !(self->client->ps.forcePowersActive&(1<<FP_SPEED))
|
|
|| self->client->ps.forcePowerLevel[FP_RAGE] > self->client->ps.forcePowerLevel[FP_SPEED]+1 )
|
|
{//either not using speed or speed is at a lower level than rage
|
|
gi.cvar_set("timescale", va("%4.2f", speed));
|
|
if ( g_timescale->value > speed )
|
|
{
|
|
newSpeed = g_timescale->value - 0.05;
|
|
if ( newSpeed < speed )
|
|
{
|
|
newSpeed = speed;
|
|
}
|
|
gi.cvar_set("timescale", va("%4.2f", newSpeed));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case FP_DRAIN:
|
|
if ( cmd->buttons & BUTTON_FORCE_DRAIN )
|
|
{//holding it keeps it going
|
|
self->client->ps.forcePowerDuration[FP_DRAIN] = level.time + 500;
|
|
}
|
|
if ( !WP_ForcePowerAvailable( self, forcePower, 0 ) )
|
|
{//no more force power, stop
|
|
WP_ForcePowerStop( self, forcePower );
|
|
}
|
|
else if ( self->client->ps.forceDrainEntityNum >= 0 && self->client->ps.forceDrainEntityNum < ENTITYNUM_WORLD )
|
|
{//holding someone
|
|
if ( !WP_ForcePowerAvailable( self, FP_DRAIN, 0 )
|
|
|| (self->client->ps.forcePowerLevel[FP_DRAIN]>FORCE_LEVEL_1
|
|
&& !self->s.number
|
|
&& !(cmd->buttons&BUTTON_FORCE_DRAIN)
|
|
&& self->client->ps.forcePowerDuration[FP_DRAIN]<level.time) )
|
|
{
|
|
WP_ForcePowerStop( self, FP_DRAIN );
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
drainEnt = &g_entities[self->client->ps.forceDrainEntityNum];
|
|
|
|
if ( !drainEnt )
|
|
{//invalid ent
|
|
WP_ForcePowerStop( self, FP_DRAIN );
|
|
return;
|
|
}
|
|
else if ( (drainEnt->health <= 0&&drainEnt->takedamage) )//FIXME: what about things that never had health or lose takedamage when they die?
|
|
{//dead ent
|
|
WP_ForcePowerStop( self, FP_DRAIN );
|
|
return;
|
|
}
|
|
else if ( drainEnt->client && drainEnt->client->moveType == MT_FLYSWIM && VectorLengthSquared( NPC->client->ps.velocity ) > (300*300) )
|
|
{//flying creature broke free
|
|
WP_ForcePowerStop( self, FP_DRAIN );
|
|
return;
|
|
}
|
|
else if ( drainEnt->client
|
|
&& drainEnt->health>0 //dead dudes don't fly
|
|
&& (drainEnt->client->NPC_class == CLASS_BOBAFETT || drainEnt->client->NPC_class == CLASS_ROCKETTROOPER)
|
|
&& self->client->ps.forcePowerDebounce[FP_DRAIN] < level.time
|
|
&& !Q_irand( 0, 10 ) )
|
|
{//boba fett - fly away!
|
|
drainEnt->client->ps.forceJumpCharge = 0;//so we don't play the force flip anim
|
|
drainEnt->client->ps.velocity[2] = 250;
|
|
drainEnt->client->ps.forceJumpZStart = drainEnt->currentOrigin[2];//so we don't take damage if we land at same height
|
|
drainEnt->client->ps.pm_flags |= PMF_JUMPING;
|
|
G_AddEvent( drainEnt, EV_JUMP, 0 );
|
|
JET_FlyStart( drainEnt );
|
|
WP_ForcePowerStop( self, FP_DRAIN );
|
|
return;
|
|
}
|
|
else if ( drainEnt->NPC
|
|
&& drainEnt->client
|
|
&& drainEnt->client->ps.forcePowersKnown
|
|
&& (drainEnt->client->NPC_class==CLASS_REBORN||drainEnt->client->ps.weapon==WP_SABER)
|
|
&& !Jedi_CultistDestroyer(drainEnt)
|
|
&& level.time-(self->client->ps.forcePowerDebounce[FP_DRAIN]>self->client->ps.forcePowerLevel[FP_DRAIN]*500)//at level 1, I always get at least 500ms of drain, at level 3 I get 1500ms
|
|
&& !Q_irand( 0, 100-(drainEnt->NPC->stats.evasion*8)-(g_spskill->integer*15) ) )
|
|
{//a jedi who broke free FIXME: maybe have some minimum grip length- a reaction time?
|
|
WP_ForceForceThrow( drainEnt );
|
|
//FIXME: I need to go into some pushed back anim...
|
|
WP_ForcePowerStop( self, FP_DRAIN );
|
|
//can't drain again for 2 seconds
|
|
self->client->ps.forcePowerDebounce[FP_DRAIN] = level.time + 4000;
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
int drainLevel = WP_AbsorbConversion( drainEnt, drainEnt->client->ps.forcePowerLevel[FP_ABSORB], self, FP_DRAIN, self->client->ps.forcePowerLevel[FP_DRAIN], forcePowerNeeded[self->client->ps.forcePowerLevel[FP_DRAIN]] );
|
|
if ( !drainLevel )
|
|
{
|
|
WP_ForcePowerStop( self, forcePower );
|
|
return;
|
|
}
|
|
*/
|
|
|
|
//NPC_SetAnim( self, SETANIM_BOTH, BOTH_HUGGER1, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
if ( self->client->ps.torsoAnim != BOTH_FORCE_DRAIN_GRAB_START
|
|
|| !self->client->ps.torsoAnimTimer )
|
|
{
|
|
NPC_SetAnim( self, SETANIM_BOTH, BOTH_FORCE_DRAIN_GRAB_HOLD, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
if ( self->handLBolt != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "force/drain_hand" ), self->playerModel, self->handLBolt, self->s.number, self->currentOrigin, 200, qtrue );
|
|
}
|
|
if ( self->handRBolt != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "force/drain_hand" ), self->playerModel, self->handRBolt, self->s.number, self->currentOrigin, 200, qtrue );
|
|
}
|
|
|
|
//how far are they
|
|
dist = Distance( self->client->renderInfo.eyePoint, drainEnt->currentOrigin );
|
|
if ( DistanceSquared( drainEnt->currentOrigin, self->currentOrigin ) > FORCE_DRAIN_DIST_SQUARED )
|
|
{//must be close, got away somehow!
|
|
WP_ForcePowerStop( self, FP_DRAIN );
|
|
return;
|
|
}
|
|
|
|
//keep my saber off!
|
|
WP_DeactivateSaber( self, qtrue );
|
|
if ( drainEnt->client )
|
|
{
|
|
//now move them
|
|
VectorCopy( self->client->ps.viewangles, angles );
|
|
angles[0] = 0;
|
|
AngleVectors( angles, dir, NULL, NULL );
|
|
/*
|
|
VectorMA( self->currentOrigin, self->maxs[0], dir, drainEnt->client->ps.forceDrainOrg );
|
|
trace_t trace;
|
|
gi.trace( &trace, drainEnt->currentOrigin, drainEnt->mins, drainEnt->maxs, drainEnt->client->ps.forceDrainOrg, drainEnt->s.number, drainEnt->clipmask );
|
|
if ( !trace.startsolid && !trace.allsolid )
|
|
{
|
|
G_SetOrigin( drainEnt, trace.endpos );
|
|
gi.linkentity( drainEnt );
|
|
VectorClear( drainEnt->client->ps.velocity );
|
|
}
|
|
VectorMA( self->currentOrigin, self->maxs[0]*0.5f, dir, drainEnt->client->ps.forceDrainOrg );
|
|
*/
|
|
//stop them from thinking
|
|
drainEnt->client->ps.pm_time = 2000;
|
|
drainEnt->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
|
|
if ( drainEnt->NPC )
|
|
{
|
|
if ( !(drainEnt->NPC->aiFlags&NPCAI_DIE_ON_IMPACT) )
|
|
{//not falling to their death
|
|
drainEnt->NPC->nextBStateThink = level.time + 2000;
|
|
}
|
|
vectoangles( dir, angles );
|
|
drainEnt->NPC->desiredYaw = AngleNormalize180(angles[YAW]+180);
|
|
drainEnt->NPC->desiredPitch = -angles[PITCH];
|
|
SaveNPCGlobals();
|
|
SetNPCGlobals( drainEnt );
|
|
NPC_UpdateAngles( qtrue, qtrue );
|
|
drainEnt->NPC->last_ucmd.angles[0] = ucmd.angles[0];
|
|
drainEnt->NPC->last_ucmd.angles[1] = ucmd.angles[1];
|
|
drainEnt->NPC->last_ucmd.angles[2] = ucmd.angles[2];
|
|
RestoreNPCGlobals();
|
|
//FIXME: why does he turn back to his original angles once he dies or is let go?
|
|
}
|
|
else if ( !drainEnt->s.number )
|
|
{
|
|
drainEnt->enemy = self;
|
|
NPC_SetLookTarget( drainEnt, self->s.number, level.time+1000 );
|
|
}
|
|
|
|
drainEnt->client->ps.eFlags |= EF_FORCE_DRAINED;
|
|
//dammit! Make sure that saber stays off!
|
|
WP_DeactivateSaber( drainEnt, qtrue );
|
|
}
|
|
//Shouldn't this be discovered?
|
|
AddSightEvent( self, drainEnt->currentOrigin, 128, AEL_DISCOVERED, 20 );
|
|
|
|
if ( self->client->ps.forcePowerDebounce[FP_DRAIN] < level.time )
|
|
{
|
|
int drainLevel = WP_AbsorbConversion( drainEnt, drainEnt->client->ps.forcePowerLevel[FP_ABSORB], self, FP_DRAIN, self->client->ps.forcePowerLevel[FP_DRAIN], forcePowerNeeded[self->client->ps.forcePowerLevel[FP_DRAIN]] );
|
|
if ( (drainLevel && drainLevel == -1)
|
|
|| Q_irand( drainLevel, 3 ) < 3 )
|
|
{//the drain is being absorbed
|
|
ForceDrainEnt( self, drainEnt );
|
|
}
|
|
WP_ForcePowerDrain( self, FP_DRAIN, 3 );
|
|
}
|
|
else
|
|
{
|
|
if ( !Q_irand( 0, 4 ) )
|
|
{
|
|
WP_ForcePowerDrain( self, FP_DRAIN, 1 );
|
|
}
|
|
if ( !drainEnt->enemy )
|
|
{
|
|
G_SetEnemy( drainEnt, self );
|
|
}
|
|
}
|
|
if ( drainEnt->health > 0 )
|
|
{//still alive
|
|
//NPC_SetAnim( drainEnt, SETANIM_BOTH, BOTH_HUGGEE1, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
NPC_SetAnim( drainEnt, SETANIM_BOTH, BOTH_FORCE_DRAIN_GRABBED, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if ( self->client->ps.forcePowerLevel[forcePower] > FORCE_LEVEL_1 )
|
|
{//regular distance-drain
|
|
if ( cmd->buttons & BUTTON_FORCE_DRAIN )
|
|
{//holding it keeps it going
|
|
self->client->ps.forcePowerDuration[FP_DRAIN] = level.time + 500;
|
|
if ( self->client->ps.torsoAnim == BOTH_FORCE_DRAIN_START )
|
|
{
|
|
if ( !self->client->ps.torsoAnimTimer )
|
|
{
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCE_DRAIN_HOLD, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
else
|
|
{
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCE_DRAIN_START, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCE_DRAIN_HOLD, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
}
|
|
if ( !WP_ForcePowerAvailable( self, forcePower, 0 ) )
|
|
{
|
|
WP_ForcePowerStop( self, forcePower );
|
|
}
|
|
else
|
|
{
|
|
ForceShootDrain( self );
|
|
}
|
|
}
|
|
break;
|
|
case FP_PROTECT:
|
|
break;
|
|
case FP_ABSORB:
|
|
break;
|
|
case FP_SEE:
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void WP_CheckForcedPowers( gentity_t *self, usercmd_t *ucmd )
|
|
{
|
|
for ( int forcePower = FP_FIRST; forcePower < NUM_FORCE_POWERS; forcePower++ )
|
|
{
|
|
if ( (self->client->ps.forcePowersForced&(1<<forcePower)) )
|
|
{
|
|
switch ( forcePower )
|
|
{
|
|
case FP_HEAL:
|
|
ForceHeal( self );
|
|
//do only once
|
|
self->client->ps.forcePowersForced &= ~(1<<forcePower);
|
|
break;
|
|
case FP_LEVITATION:
|
|
//nothing
|
|
break;
|
|
case FP_SPEED:
|
|
ForceSpeed( self );
|
|
//do only once
|
|
self->client->ps.forcePowersForced &= ~(1<<forcePower);
|
|
break;
|
|
case FP_PUSH:
|
|
ForceThrowEx( self, qfalse, qfalse, qtrue );
|
|
//do only once
|
|
self->client->ps.forcePowersForced &= ~(1<<forcePower);
|
|
break;
|
|
case FP_PULL:
|
|
ForceThrowEx( self, qtrue, qfalse, qtrue );
|
|
//do only once
|
|
self->client->ps.forcePowersForced &= ~(1<<forcePower);
|
|
break;
|
|
case FP_TELEPATHY:
|
|
//FIXME: target at enemy?
|
|
ForceTelepathy( self );
|
|
//do only once
|
|
self->client->ps.forcePowersForced &= ~(1<<forcePower);
|
|
break;
|
|
case FP_GRIP:
|
|
ucmd->buttons &= ~(BUTTON_ATTACK|BUTTON_ALT_ATTACK|BUTTON_FORCE_FOCUS|BUTTON_FORCE_DRAIN|BUTTON_FORCE_LIGHTNING);
|
|
ucmd->buttons |= BUTTON_FORCEGRIP;
|
|
//holds until cleared
|
|
break;
|
|
case FP_LIGHTNING:
|
|
ucmd->buttons &= ~(BUTTON_ATTACK|BUTTON_ALT_ATTACK|BUTTON_FORCE_FOCUS|BUTTON_FORCEGRIP|BUTTON_FORCE_DRAIN);
|
|
ucmd->buttons |= BUTTON_FORCE_LIGHTNING;
|
|
//holds until cleared
|
|
break;
|
|
case FP_SABERTHROW:
|
|
ucmd->buttons &= ~(BUTTON_ATTACK|BUTTON_FORCE_FOCUS|BUTTON_FORCEGRIP|BUTTON_FORCE_DRAIN|BUTTON_FORCE_LIGHTNING);
|
|
ucmd->buttons |= BUTTON_ALT_ATTACK;
|
|
//holds until cleared?
|
|
break;
|
|
case FP_SABER_DEFENSE:
|
|
//nothing
|
|
break;
|
|
case FP_SABER_OFFENSE:
|
|
//nothing
|
|
break;
|
|
case FP_RAGE:
|
|
ForceRage( self );
|
|
//do only once
|
|
self->client->ps.forcePowersForced &= ~(1<<forcePower);
|
|
break;
|
|
case FP_PROTECT:
|
|
ForceProtect( self );
|
|
//do only once
|
|
self->client->ps.forcePowersForced &= ~(1<<forcePower);
|
|
break;
|
|
case FP_ABSORB:
|
|
ForceAbsorb( self );
|
|
//do only once
|
|
self->client->ps.forcePowersForced &= ~(1<<forcePower);
|
|
break;
|
|
case FP_DRAIN:
|
|
ucmd->buttons &= ~(BUTTON_ATTACK|BUTTON_ALT_ATTACK|BUTTON_FORCE_FOCUS|BUTTON_FORCEGRIP|BUTTON_FORCE_LIGHTNING);
|
|
ucmd->buttons |= BUTTON_FORCE_DRAIN;
|
|
//holds until cleared
|
|
break;
|
|
case FP_SEE:
|
|
//nothing
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void WP_ForcePowersUpdate( gentity_t *self, usercmd_t *ucmd )
|
|
{
|
|
qboolean usingForce = qfalse;
|
|
int i;
|
|
//see if any force powers are running
|
|
if ( !self )
|
|
{
|
|
return;
|
|
}
|
|
if ( !self->client )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( self->health <= 0 )
|
|
{//if dead, deactivate any active force powers
|
|
for ( i = 0; i < NUM_FORCE_POWERS; i++ )
|
|
{
|
|
if ( self->client->ps.forcePowerDuration[i] || (self->client->ps.forcePowersActive&( 1 << i )) )
|
|
{
|
|
WP_ForcePowerStop( self, (forcePowers_t)i );
|
|
self->client->ps.forcePowerDuration[i] = 0;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
WP_CheckForcedPowers( self, ucmd );
|
|
|
|
if ( !self->s.number )
|
|
{//player uses different kind of force-jump
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
if ( ucmd->buttons & BUTTON_FORCEJUMP )
|
|
{//just charging up
|
|
ForceJumpCharge( self, ucmd );
|
|
}
|
|
else */
|
|
if ( self->client->ps.forceJumpCharge )
|
|
{//let go of charge button, have charge
|
|
//if leave the ground by some other means, cancel the force jump so we don't suddenly jump when we land.
|
|
if ( self->client->ps.groundEntityNum == ENTITYNUM_NONE
|
|
&& !PM_SwimmingAnim( self->client->ps.legsAnim ) )
|
|
{//FIXME: stop sound?
|
|
//self->client->ps.forceJumpCharge = 0;
|
|
//FIXME: actually, we want this to still be cleared... don't clear it if the button isn't being pressed, but clear it if not holding button and not on ground.
|
|
}
|
|
else
|
|
{//still on ground, so jump
|
|
ForceJump( self, ucmd );
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( ucmd->buttons & BUTTON_FORCEGRIP )
|
|
{
|
|
ForceGrip( self );
|
|
}
|
|
|
|
if ( !self->s.number
|
|
&& self->client->NPC_class == CLASS_BOBAFETT )
|
|
{//Boba Fett
|
|
if ( ucmd->buttons & BUTTON_FORCE_LIGHTNING )
|
|
{//start flamethrower
|
|
Boba_DoFlameThrower( self );
|
|
return;
|
|
}
|
|
else if ( self->client->ps.forcePowerDuration[FP_LIGHTNING] )
|
|
{
|
|
self->client->ps.forcePowerDuration[FP_LIGHTNING] = 0;
|
|
Boba_StopFlameThrower( self );
|
|
return;
|
|
}
|
|
}
|
|
else if ( ucmd->buttons & BUTTON_FORCE_LIGHTNING )
|
|
{
|
|
ForceLightning( self );
|
|
}
|
|
|
|
if ( ucmd->buttons & BUTTON_FORCE_DRAIN )
|
|
{
|
|
if ( !ForceDrain2( self ) )
|
|
{//can't drain-grip someone right in front
|
|
if ( self->client->ps.forcePowerLevel[FP_DRAIN] > FORCE_LEVEL_1 )
|
|
{//try ranged
|
|
ForceDrain( self, qtrue );
|
|
}
|
|
}
|
|
}
|
|
|
|
for ( i = 0; i < NUM_FORCE_POWERS; i++ )
|
|
{
|
|
if ( self->client->ps.forcePowerDuration[i] )
|
|
{
|
|
if ( self->client->ps.forcePowerDuration[i] < level.time )
|
|
{
|
|
if ( (self->client->ps.forcePowersActive&( 1 << i )) )
|
|
{//turn it off
|
|
WP_ForcePowerStop( self, (forcePowers_t)i );
|
|
}
|
|
self->client->ps.forcePowerDuration[i] = 0;
|
|
}
|
|
}
|
|
if ( (self->client->ps.forcePowersActive&( 1 << i )) )
|
|
{
|
|
usingForce = qtrue;
|
|
WP_ForcePowerRun( self, (forcePowers_t)i, ucmd );
|
|
}
|
|
}
|
|
if ( self->client->ps.saberInFlight )
|
|
{//don't regen force power while throwing saber
|
|
if ( self->client->ps.saberEntityNum < ENTITYNUM_NONE && self->client->ps.saberEntityNum > 0 )//player is 0
|
|
{//
|
|
if ( &g_entities[self->client->ps.saberEntityNum] != NULL && g_entities[self->client->ps.saberEntityNum].s.pos.trType == TR_LINEAR )
|
|
{//fell to the ground and we're trying to pull it back
|
|
usingForce = qtrue;
|
|
}
|
|
}
|
|
}
|
|
if ( PM_ForceUsingSaberAnim( self->client->ps.torsoAnim ) )
|
|
{
|
|
usingForce = qtrue;
|
|
}
|
|
if ( !usingForce )
|
|
{//when not using the force, regenerate at 10 points per second
|
|
if ( self->client->ps.forcePowerRegenDebounceTime < level.time )
|
|
{
|
|
WP_ForcePowerRegenerate( self, self->client->ps.forcePowerRegenAmount );
|
|
self->client->ps.forcePowerRegenDebounceTime = level.time + self->client->ps.forcePowerRegenRate;
|
|
if ( self->client->ps.forceRageRecoveryTime >= level.time )
|
|
{//regen half as fast
|
|
self->client->ps.forcePowerRegenDebounceTime += self->client->ps.forcePowerRegenRate;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void WP_InitForcePowers( gentity_t *ent )
|
|
{
|
|
if ( !ent || !ent->client )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( !ent->client->ps.forcePowerMax )
|
|
{
|
|
ent->client->ps.forcePowerMax = FORCE_POWER_MAX;
|
|
}
|
|
if ( !ent->client->ps.forcePowerRegenRate )
|
|
{
|
|
ent->client->ps.forcePowerRegenRate = 100;
|
|
}
|
|
ent->client->ps.forcePower = ent->client->ps.forcePowerMax;
|
|
ent->client->ps.forcePowerRegenDebounceTime = level.time;
|
|
|
|
ent->client->ps.forceGripEntityInitialDist = ent->client->ps.forceGripEntityNum = ent->client->ps.forceDrainEntityNum = ent->client->ps.pullAttackEntNum = ENTITYNUM_NONE;
|
|
ent->client->ps.forceRageRecoveryTime = 0;
|
|
ent->client->ps.forceDrainTime = 0;
|
|
ent->client->ps.pullAttackTime = 0;
|
|
|
|
if ( ent->s.number < MAX_CLIENTS )
|
|
{//player
|
|
if ( !g_cheats->integer )//devmaps give you all the FP
|
|
{
|
|
ent->client->ps.forcePowerLevel[FP_SABER_DEFENSE] = FORCE_LEVEL_1;
|
|
ent->client->ps.forcePowerLevel[FP_SABER_OFFENSE] = FORCE_LEVEL_1;
|
|
}
|
|
else
|
|
{
|
|
ent->client->ps.forcePowersKnown = ( 1 << FP_HEAL )|( 1 << FP_LEVITATION )|( 1 << FP_SPEED )|( 1 << FP_PUSH )|( 1 << FP_PULL )|( 1 << FP_TELEPATHY )|( 1 << FP_GRIP )|( 1 << FP_LIGHTNING)|( 1 << FP_SABERTHROW)|( 1 << FP_SABER_DEFENSE )|( 1 << FP_SABER_OFFENSE )|( 1<< FP_RAGE )|( 1<< FP_DRAIN )|( 1<< FP_PROTECT )|( 1<< FP_ABSORB )|( 1<< FP_SEE );
|
|
ent->client->ps.forcePowerLevel[FP_HEAL] = FORCE_LEVEL_2;
|
|
ent->client->ps.forcePowerLevel[FP_LEVITATION] = FORCE_LEVEL_2;
|
|
ent->client->ps.forcePowerLevel[FP_PUSH] = FORCE_LEVEL_1;
|
|
ent->client->ps.forcePowerLevel[FP_PULL] = FORCE_LEVEL_1;
|
|
ent->client->ps.forcePowerLevel[FP_SABERTHROW] = FORCE_LEVEL_2;
|
|
ent->client->ps.forcePowerLevel[FP_SPEED] = FORCE_LEVEL_2;
|
|
ent->client->ps.forcePowerLevel[FP_LIGHTNING] = FORCE_LEVEL_1;
|
|
ent->client->ps.forcePowerLevel[FP_TELEPATHY] = FORCE_LEVEL_2;
|
|
|
|
ent->client->ps.forcePowerLevel[FP_RAGE] = FORCE_LEVEL_1;
|
|
ent->client->ps.forcePowerLevel[FP_PROTECT] = FORCE_LEVEL_1;
|
|
ent->client->ps.forcePowerLevel[FP_ABSORB] = FORCE_LEVEL_1;
|
|
ent->client->ps.forcePowerLevel[FP_DRAIN] = FORCE_LEVEL_1;
|
|
ent->client->ps.forcePowerLevel[FP_SEE] = FORCE_LEVEL_1;
|
|
|
|
ent->client->ps.forcePowerLevel[FP_SABER_DEFENSE] = FORCE_LEVEL_3;
|
|
ent->client->ps.forcePowerLevel[FP_SABER_OFFENSE] = FORCE_LEVEL_3;
|
|
ent->client->ps.forcePowerLevel[FP_GRIP] = FORCE_LEVEL_2;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool WP_DoingMoronicForcedAnimationForForcePowers(gentity_t *ent)
|
|
{
|
|
// :P --eez
|
|
if( !ent->client ) return false;
|
|
if( ent->client->ps.legsAnim == BOTH_FORCE_ABSORB_START ||
|
|
ent->client->ps.legsAnim == BOTH_FORCE_ABSORB_END ||
|
|
ent->client->ps.legsAnim == BOTH_FORCE_ABSORB ||
|
|
ent->client->ps.torsoAnim == BOTH_FORCE_RAGE ||
|
|
ent->client->ps.legsAnim == BOTH_FORCE_PROTECT )
|
|
return true;
|
|
return false;
|
|
}
|