mirror of
https://github.com/DrBeef/JKXR.git
synced 2024-11-23 12:32:26 +00:00
4597b03873
Opens in Android Studio but haven't even tried to build it yet (it won't.. I know that much!)
1270 lines
40 KiB
C++
1270 lines
40 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_headers.h"
|
|
|
|
#include "b_local.h"
|
|
#include "g_nav.h"
|
|
#include "anims.h"
|
|
#include "wp_saber.h"
|
|
|
|
extern qboolean G_StandardHumanoid( const char *modelName );
|
|
extern void G_AddVoiceEvent( gentity_t *self, int event, int speakDebounceTime );
|
|
extern qboolean Q3_TaskIDPending( gentity_t *ent, taskID_t taskType );
|
|
extern void NPC_AimAdjust( int change );
|
|
extern qboolean WP_LobFire( gentity_t *self, vec3_t start, vec3_t target, vec3_t mins, vec3_t maxs, int clipmask,
|
|
vec3_t velocity, qboolean tracePath, int ignoreEntNum, int enemyNum,
|
|
float minSpeed, float maxSpeed, float idealSpeed, qboolean mustHit );
|
|
extern qboolean InFront( vec3_t spot, vec3_t from, vec3_t fromAngles, float threshHold = 0.0f );
|
|
extern void G_SoundAtSpot( vec3_t org, int soundIndex );
|
|
extern void G_SoundOnEnt (gentity_t *ent, soundChannel_t channel, const char *soundPath);
|
|
extern qboolean PM_CrouchAnim( int anim );
|
|
//extern void NPC_Mark1_Part_Explode(gentity_t *self,int bolt);
|
|
|
|
#define MELEE_DIST_SQUARED 6400//80*80
|
|
#define MIN_LOB_DIST_SQUARED 65536//256*256
|
|
#define MAX_LOB_DIST_SQUARED 200704//448*448
|
|
#define REPEATER_ALT_SIZE 3 // half of bbox size
|
|
#define GENERATOR_HEALTH 25
|
|
#define TURN_ON 0x00000000
|
|
#define TURN_OFF 0x00000100
|
|
#define GALAK_SHIELD_HEALTH 500
|
|
|
|
static vec3_t shieldMins = {-60, -60, -24 };
|
|
static vec3_t shieldMaxs = {60, 60, 80};
|
|
|
|
extern qboolean NPC_CheckPlayerTeamStealth( void );
|
|
|
|
static qboolean enemyLOS;
|
|
static qboolean enemyCS;
|
|
static qboolean hitAlly;
|
|
static qboolean faceEnemy;
|
|
static qboolean AImove;
|
|
static qboolean shoot;
|
|
static float enemyDist;
|
|
static vec3_t impactPos;
|
|
|
|
void NPC_GalakMech_Precache( void )
|
|
{
|
|
G_SoundIndex( "sound/weapons/galak/skewerhit.wav" );
|
|
G_SoundIndex( "sound/weapons/galak/lasercharge.wav" );
|
|
G_SoundIndex( "sound/weapons/galak/lasercutting.wav" );
|
|
G_SoundIndex( "sound/weapons/galak/laserdamage.wav" );
|
|
|
|
G_EffectIndex( "galak/trace_beam" );
|
|
G_EffectIndex( "galak/beam_warmup" );
|
|
// G_EffectIndex( "small_chunks");
|
|
G_EffectIndex( "env/med_explode2");
|
|
G_EffectIndex( "env/small_explode2");
|
|
G_EffectIndex( "galak/explode");
|
|
G_EffectIndex( "blaster/smoke_bolton");
|
|
// G_EffectIndex( "env/exp_trail_comp");
|
|
}
|
|
|
|
void NPC_GalakMech_Init( gentity_t *ent )
|
|
{
|
|
if (ent->NPC->behaviorState != BS_CINEMATIC)
|
|
{
|
|
ent->client->ps.stats[STAT_ARMOR] = GALAK_SHIELD_HEALTH;
|
|
ent->NPC->investigateCount = ent->NPC->investigateDebounceTime = 0;
|
|
ent->flags |= FL_SHIELDED;//reflect normal shots
|
|
ent->client->ps.powerups[PW_GALAK_SHIELD] = Q3_INFINITE;//temp, for effect
|
|
ent->fx_time = level.time;
|
|
VectorSet( ent->mins, -60, -60, -24 );
|
|
VectorSet( ent->maxs, 60, 60, 80 );
|
|
ent->flags |= FL_NO_KNOCKBACK;//don't get pushed
|
|
TIMER_Set( ent, "attackDelay", 0 ); //FIXME: Slant for difficulty levels
|
|
TIMER_Set( ent, "flee", 0 );
|
|
TIMER_Set( ent, "smackTime", 0 );
|
|
TIMER_Set( ent, "beamDelay", 0 );
|
|
TIMER_Set( ent, "noLob", 0 );
|
|
TIMER_Set( ent, "noRapid", 0 );
|
|
TIMER_Set( ent, "talkDebounce", 0 );
|
|
gi.G2API_SetSurfaceOnOff( &ent->ghoul2[ent->playerModel], "torso_shield_off", TURN_ON );
|
|
gi.G2API_SetSurfaceOnOff( &ent->ghoul2[ent->playerModel], "torso_galakface_off", TURN_OFF );
|
|
gi.G2API_SetSurfaceOnOff( &ent->ghoul2[ent->playerModel], "torso_galakhead_off", TURN_OFF );
|
|
gi.G2API_SetSurfaceOnOff( &ent->ghoul2[ent->playerModel], "torso_eyes_mouth_off", TURN_OFF );
|
|
gi.G2API_SetSurfaceOnOff( &ent->ghoul2[ent->playerModel], "torso_collar_off", TURN_OFF );
|
|
gi.G2API_SetSurfaceOnOff( &ent->ghoul2[ent->playerModel], "torso_galaktorso_off", TURN_OFF );
|
|
}
|
|
else
|
|
{
|
|
// gi.G2API_SetSurfaceOnOff( &ent->ghoul2[ent->playerModel], "helmet", TURN_OFF );
|
|
gi.G2API_SetSurfaceOnOff( &ent->ghoul2[ent->playerModel], "torso_shield_off", TURN_OFF );
|
|
gi.G2API_SetSurfaceOnOff( &ent->ghoul2[ent->playerModel], "torso_galakface_off", TURN_ON );
|
|
gi.G2API_SetSurfaceOnOff( &ent->ghoul2[ent->playerModel], "torso_galakhead_off", TURN_ON );
|
|
gi.G2API_SetSurfaceOnOff( &ent->ghoul2[ent->playerModel], "torso_eyes_mouth_off", TURN_ON );
|
|
gi.G2API_SetSurfaceOnOff( &ent->ghoul2[ent->playerModel], "torso_collar_off", TURN_ON );
|
|
gi.G2API_SetSurfaceOnOff( &ent->ghoul2[ent->playerModel], "torso_galaktorso_off", TURN_ON );
|
|
}
|
|
|
|
}
|
|
|
|
//-----------------------------------------------------------------
|
|
static void GM_CreateExplosion( gentity_t *self, const int boltID, qboolean doSmall = qfalse )
|
|
{
|
|
if ( boltID >=0 )
|
|
{
|
|
mdxaBone_t boltMatrix;
|
|
vec3_t org, dir;
|
|
|
|
gi.G2API_GetBoltMatrix( self->ghoul2, self->playerModel,
|
|
boltID,
|
|
&boltMatrix, self->currentAngles, self->currentOrigin, (cg.time?cg.time:level.time),
|
|
NULL, self->s.modelScale );
|
|
|
|
gi.G2API_GiveMeVectorFromMatrix( boltMatrix, ORIGIN, org );
|
|
gi.G2API_GiveMeVectorFromMatrix( boltMatrix, NEGATIVE_Y, dir );
|
|
|
|
if ( doSmall )
|
|
{
|
|
G_PlayEffect( "env/small_explode2", org, dir );
|
|
}
|
|
else
|
|
{
|
|
G_PlayEffect( "env/med_explode2", org, dir );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
-------------------------
|
|
GM_Dying
|
|
-------------------------
|
|
*/
|
|
|
|
void GM_Dying( gentity_t *self )
|
|
{
|
|
if ( level.time - self->s.time < 4000 )
|
|
{//FIXME: need a real effect
|
|
self->s.powerups |= ( 1 << PW_SHOCKED );
|
|
self->client->ps.powerups[PW_SHOCKED] = level.time + 1000;
|
|
if ( TIMER_Done( self, "dyingExplosion" ) )
|
|
{
|
|
int newBolt;
|
|
switch ( Q_irand( 1, 14 ) )
|
|
{
|
|
// Find place to generate explosion
|
|
case 1:
|
|
if (!gi.G2API_GetSurfaceRenderStatus( &self->ghoul2[self->playerModel], "r_hand" ))
|
|
{//r_hand still there
|
|
GM_CreateExplosion( self, self->handRBolt, qtrue );
|
|
gi.G2API_SetSurfaceOnOff( &self->ghoul2[self->playerModel], "r_hand", TURN_OFF );
|
|
}
|
|
else if (!gi.G2API_GetSurfaceRenderStatus( &self->ghoul2[self->playerModel], "r_arm_middle" ))
|
|
{//r_arm_middle still there
|
|
newBolt = gi.G2API_AddBolt( &self->ghoul2[self->playerModel], "*r_arm_elbow" );
|
|
gi.G2API_SetSurfaceOnOff( &self->ghoul2[self->playerModel], "r_arm_middle", TURN_OFF );
|
|
}
|
|
break;
|
|
case 2:
|
|
//FIXME: do only once?
|
|
if (!gi.G2API_GetSurfaceRenderStatus( &self->ghoul2[self->playerModel], "l_hand" ))
|
|
{//l_hand still there
|
|
GM_CreateExplosion( self, self->handLBolt );
|
|
gi.G2API_SetSurfaceOnOff( &self->ghoul2[self->playerModel], "l_hand", TURN_OFF );
|
|
}
|
|
else if (!gi.G2API_GetSurfaceRenderStatus( &self->ghoul2[self->playerModel], "l_arm_wrist" ))
|
|
{//l_arm_wrist still there
|
|
newBolt = gi.G2API_AddBolt( &self->ghoul2[self->playerModel], "*l_arm_cap_l_hand" );
|
|
gi.G2API_SetSurfaceOnOff( &self->ghoul2[self->playerModel], "l_arm_wrist", TURN_OFF );
|
|
}
|
|
else if (!gi.G2API_GetSurfaceRenderStatus( &self->ghoul2[self->playerModel], "l_arm_middle" ))
|
|
{//l_arm_middle still there
|
|
newBolt = gi.G2API_AddBolt( &self->ghoul2[self->playerModel], "*l_arm_cap_l_hand" );
|
|
gi.G2API_SetSurfaceOnOff( &self->ghoul2[self->playerModel], "l_arm_middle", TURN_OFF );
|
|
}
|
|
else if (!gi.G2API_GetSurfaceRenderStatus( &self->ghoul2[self->playerModel], "l_arm_augment" ))
|
|
{//l_arm_augment still there
|
|
newBolt = gi.G2API_AddBolt( &self->ghoul2[self->playerModel], "*l_arm_elbow" );
|
|
gi.G2API_SetSurfaceOnOff( &self->ghoul2[self->playerModel], "l_arm_augment", TURN_OFF );
|
|
}
|
|
break;
|
|
case 3:
|
|
case 4:
|
|
newBolt = gi.G2API_AddBolt( &self->ghoul2[self->playerModel], "*hip_fr" );
|
|
GM_CreateExplosion( self, newBolt );
|
|
break;
|
|
case 5:
|
|
case 6:
|
|
newBolt = gi.G2API_AddBolt( &self->ghoul2[self->playerModel], "*shldr_l" );
|
|
GM_CreateExplosion( self, newBolt );
|
|
break;
|
|
case 7:
|
|
case 8:
|
|
newBolt = gi.G2API_AddBolt( &self->ghoul2[self->playerModel], "*uchest_r" );
|
|
GM_CreateExplosion( self, newBolt );
|
|
break;
|
|
case 9:
|
|
case 10:
|
|
GM_CreateExplosion( self, self->headBolt );
|
|
break;
|
|
case 11:
|
|
newBolt = gi.G2API_AddBolt( &self->ghoul2[self->playerModel], "*l_leg_knee" );
|
|
GM_CreateExplosion( self, newBolt, qtrue );
|
|
break;
|
|
case 12:
|
|
newBolt = gi.G2API_AddBolt( &self->ghoul2[self->playerModel], "*r_leg_knee" );
|
|
GM_CreateExplosion( self, newBolt, qtrue );
|
|
break;
|
|
case 13:
|
|
newBolt = gi.G2API_AddBolt( &self->ghoul2[self->playerModel], "*l_leg_foot" );
|
|
GM_CreateExplosion( self, newBolt, qtrue );
|
|
break;
|
|
case 14:
|
|
newBolt = gi.G2API_AddBolt( &self->ghoul2[self->playerModel], "*r_leg_foot" );
|
|
GM_CreateExplosion( self, newBolt, qtrue );
|
|
break;
|
|
}
|
|
|
|
TIMER_Set( self, "dyingExplosion", Q_irand( 300, 1100 ) );
|
|
}
|
|
}
|
|
else
|
|
{//one final, huge explosion
|
|
G_PlayEffect( "galak/explode", self->currentOrigin );
|
|
// G_PlayEffect( "small_chunks", self->currentOrigin );
|
|
// G_PlayEffect( "env/exp_trail_comp", self->currentOrigin, self->currentAngles );
|
|
self->nextthink = level.time + FRAMETIME;
|
|
self->e_ThinkFunc = thinkF_G_FreeEntity;
|
|
}
|
|
}
|
|
|
|
/*
|
|
-------------------------
|
|
NPC_GM_Pain
|
|
-------------------------
|
|
*/
|
|
|
|
extern void NPC_SetPainEvent( gentity_t *self );
|
|
void NPC_GM_Pain( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, vec3_t point, int damage, int mod,int hitLoc )
|
|
{
|
|
if ( self->client->ps.powerups[PW_GALAK_SHIELD] == 0 )
|
|
{//shield is currently down
|
|
//FIXME: allow for radius damage?
|
|
if ( (hitLoc==HL_GENERIC1) && (self->locationDamage[HL_GENERIC1] > GENERATOR_HEALTH) )
|
|
{
|
|
int newBolt = gi.G2API_AddBolt( &self->ghoul2[self->playerModel], "*antenna_base" );
|
|
if ( newBolt != -1 )
|
|
{
|
|
GM_CreateExplosion( self, newBolt );
|
|
}
|
|
|
|
gi.G2API_SetSurfaceOnOff( &self->ghoul2[self->playerModel], "torso_shield_off", TURN_OFF );
|
|
gi.G2API_SetSurfaceOnOff( &self->ghoul2[self->playerModel], "torso_antenna", TURN_OFF );
|
|
gi.G2API_SetSurfaceOnOff( &self->ghoul2[self->playerModel], "torso_antenna_base_cap_off", TURN_ON );
|
|
self->client->ps.powerups[PW_GALAK_SHIELD] = 0;//temp, for effect
|
|
self->client->ps.stats[STAT_ARMOR] = 0;//no more armor
|
|
self->NPC->investigateDebounceTime = 0;//stop recharging
|
|
|
|
NPC_SetAnim( self, SETANIM_BOTH, BOTH_ALERT1, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
TIMER_Set( self, "attackDelay", self->client->ps.torsoAnimTimer );
|
|
G_AddEvent( self, Q_irand( EV_DEATH1, EV_DEATH3 ), self->health );
|
|
}
|
|
}
|
|
else
|
|
{//store the point for shield impact
|
|
if ( point )
|
|
{
|
|
VectorCopy( point, self->pos4 );
|
|
self->client->poisonTime = level.time;
|
|
}
|
|
}
|
|
|
|
if ( !self->lockCount && !self->client->ps.torsoAnimTimer )
|
|
{//don't interrupt laser sweep attack or other special attacks/moves
|
|
if ( self->count < 4 && self->health > 100 && hitLoc != HL_GENERIC1 )
|
|
{
|
|
if ( self->delay < level.time )
|
|
{
|
|
int speech;
|
|
switch( self->count )
|
|
{
|
|
default:
|
|
case 0:
|
|
speech = EV_PUSHED1;
|
|
break;
|
|
case 1:
|
|
speech = EV_PUSHED2;
|
|
break;
|
|
case 2:
|
|
speech = EV_PUSHED3;
|
|
break;
|
|
case 3:
|
|
speech = EV_DETECTED1;
|
|
break;
|
|
}
|
|
self->count++;
|
|
self->NPC->blockedSpeechDebounceTime = 0;
|
|
G_AddVoiceEvent( self, speech, Q_irand( 3000, 5000 ) );
|
|
self->delay = level.time + Q_irand( 5000, 7000 );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NPC_Pain( self, inflictor, attacker, point, damage, mod, hitLoc );
|
|
}
|
|
}
|
|
else if ( hitLoc == HL_GENERIC1 )
|
|
{
|
|
NPC_SetPainEvent( self );
|
|
self->s.powerups |= ( 1 << PW_SHOCKED );
|
|
self->client->ps.powerups[PW_SHOCKED] = level.time + Q_irand( 500, 2500 );
|
|
}
|
|
|
|
if ( inflictor && inflictor->lastEnemy == self )
|
|
{//He force-pushed my own lobfires back at me
|
|
if ( mod == MOD_REPEATER_ALT && !Q_irand( 0, 2 ) )
|
|
{
|
|
if ( TIMER_Done( self, "noRapid" ) )
|
|
{
|
|
self->NPC->scriptFlags &= ~SCF_ALT_FIRE;
|
|
self->alt_fire = qfalse;
|
|
TIMER_Set( self, "noLob", Q_irand( 2000, 6000 ) );
|
|
}
|
|
else
|
|
{//hopefully this will make us fire the laser
|
|
TIMER_Set( self, "noLob", Q_irand( 1000, 2000 ) );
|
|
}
|
|
}
|
|
else if ( mod == MOD_REPEATER && !Q_irand( 0, 5 ) )
|
|
{
|
|
if ( TIMER_Done( self, "noLob" ) )
|
|
{
|
|
self->NPC->scriptFlags |= SCF_ALT_FIRE;
|
|
self->alt_fire = qtrue;
|
|
TIMER_Set( self, "noRapid", Q_irand( 2000, 6000 ) );
|
|
}
|
|
else
|
|
{//hopefully this will make us fire the laser
|
|
TIMER_Set( self, "noRapid", Q_irand( 1000, 2000 ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
-------------------------
|
|
GM_HoldPosition
|
|
-------------------------
|
|
*/
|
|
|
|
static void GM_HoldPosition( void )
|
|
{
|
|
NPC_FreeCombatPoint( NPCInfo->combatPoint, qtrue );
|
|
if ( !Q3_TaskIDPending( NPC, TID_MOVE_NAV ) )
|
|
{//don't have a script waiting for me to get to my point, okay to stop trying and stand
|
|
NPCInfo->goalEntity = NULL;
|
|
}
|
|
}
|
|
|
|
/*
|
|
-------------------------
|
|
GM_Move
|
|
-------------------------
|
|
*/
|
|
static qboolean GM_Move( void )
|
|
{
|
|
NPCInfo->combatMove = qtrue;//always move straight toward our goal
|
|
|
|
qboolean moved = NPC_MoveToGoal( qtrue );
|
|
navInfo_t info;
|
|
|
|
//Get the move info
|
|
NAV_GetLastMove( info );
|
|
|
|
//FIXME: if we bump into another one of our guys and can't get around him, just stop!
|
|
//If we hit our target, then stop and fire!
|
|
if ( info.flags & NIF_COLLISION )
|
|
{
|
|
if ( info.blocker == NPC->enemy )
|
|
{
|
|
GM_HoldPosition();
|
|
}
|
|
}
|
|
|
|
//If our move failed, then reset
|
|
if ( moved == qfalse )
|
|
{//FIXME: if we're going to a combat point, need to pick a different one
|
|
if ( !Q3_TaskIDPending( NPC, TID_MOVE_NAV ) )
|
|
{//can't transfer movegoal or stop when a script we're running is waiting to complete
|
|
GM_HoldPosition();
|
|
}
|
|
}
|
|
|
|
return moved;
|
|
}
|
|
|
|
/*
|
|
-------------------------
|
|
NPC_BSGM_Patrol
|
|
-------------------------
|
|
*/
|
|
|
|
void NPC_BSGM_Patrol( void )
|
|
{
|
|
if ( NPC_CheckPlayerTeamStealth() )
|
|
{
|
|
NPC_UpdateAngles( qtrue, qtrue );
|
|
return;
|
|
}
|
|
|
|
//If we have somewhere to go, then do that
|
|
if ( UpdateGoal() )
|
|
{
|
|
ucmd.buttons |= BUTTON_WALKING;
|
|
NPC_MoveToGoal( qtrue );
|
|
}
|
|
|
|
NPC_UpdateAngles( qtrue, qtrue );
|
|
}
|
|
|
|
/*
|
|
-------------------------
|
|
GM_CheckMoveState
|
|
-------------------------
|
|
*/
|
|
|
|
static void GM_CheckMoveState( void )
|
|
{
|
|
if ( Q3_TaskIDPending( NPC, TID_MOVE_NAV ) )
|
|
{//moving toward a goal that a script is waiting on, so don't stop for anything!
|
|
AImove = qtrue;
|
|
}
|
|
|
|
//See if we're moving towards a goal, not the enemy
|
|
if ( ( NPCInfo->goalEntity != NPC->enemy ) && ( NPCInfo->goalEntity != NULL ) )
|
|
{
|
|
//Did we make it?
|
|
if ( NAV_HitNavGoal( NPC->currentOrigin, NPC->mins, NPC->maxs, NPCInfo->goalEntity->currentOrigin, 16, qfalse ) ||
|
|
( !Q3_TaskIDPending( NPC, TID_MOVE_NAV ) && enemyLOS && enemyDist <= 10000 ) )
|
|
{//either hit our navgoal or our navgoal was not a crucial (scripted) one (maybe a combat point) and we're scouting and found our enemy
|
|
NPC_ReachedGoal();
|
|
//don't attack right away
|
|
TIMER_Set( NPC, "attackDelay", Q_irand( 250, 500 ) ); //FIXME: Slant for difficulty levels
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
-------------------------
|
|
GM_CheckFireState
|
|
-------------------------
|
|
*/
|
|
|
|
static void GM_CheckFireState( void )
|
|
{
|
|
if ( enemyCS )
|
|
{//if have a clear shot, always try
|
|
return;
|
|
}
|
|
|
|
if ( !VectorCompare( NPC->client->ps.velocity, vec3_origin ) )
|
|
{//if moving at all, don't do this
|
|
return;
|
|
}
|
|
|
|
//See if we should continue to fire on their last position
|
|
if ( !hitAlly && NPCInfo->enemyLastSeenTime > 0 )
|
|
{
|
|
if ( level.time - NPCInfo->enemyLastSeenTime < 10000 )
|
|
{
|
|
if ( !Q_irand( 0, 10 ) )
|
|
{
|
|
//Fire on the last known position
|
|
vec3_t muzzle, dir, angles;
|
|
qboolean tooClose = qfalse;
|
|
qboolean tooFar = qfalse;
|
|
|
|
CalcEntitySpot( NPC, SPOT_HEAD, muzzle );
|
|
if ( VectorCompare( impactPos, vec3_origin ) )
|
|
{//never checked ShotEntity this frame, so must do a trace...
|
|
trace_t tr;
|
|
//vec3_t mins = {-2,-2,-2}, maxs = {2,2,2};
|
|
vec3_t forward, end;
|
|
AngleVectors( NPC->client->ps.viewangles, forward, NULL, NULL );
|
|
VectorMA( muzzle, 8192, forward, end );
|
|
gi.trace( &tr, muzzle, vec3_origin, vec3_origin, end, NPC->s.number, MASK_SHOT, G2_NOCOLLIDE, 0 );
|
|
VectorCopy( tr.endpos, impactPos );
|
|
}
|
|
|
|
//see if impact would be too close to me
|
|
float distThreshold = 16384/*128*128*/;//default
|
|
if ( NPC->s.weapon == WP_REPEATER )
|
|
{
|
|
if ( NPCInfo->scriptFlags&SCF_ALT_FIRE )
|
|
{
|
|
distThreshold = 65536/*256*256*/;
|
|
}
|
|
}
|
|
|
|
float dist = DistanceSquared( impactPos, muzzle );
|
|
|
|
if ( dist < distThreshold )
|
|
{//impact would be too close to me
|
|
tooClose = qtrue;
|
|
}
|
|
else if ( level.time - NPCInfo->enemyLastSeenTime > 5000 )
|
|
{//we've haven't seen them in the last 5 seconds
|
|
//see if it's too far from where he is
|
|
distThreshold = 65536/*256*256*/;//default
|
|
if ( NPC->s.weapon == WP_REPEATER )
|
|
{
|
|
if ( NPCInfo->scriptFlags&SCF_ALT_FIRE )
|
|
{
|
|
distThreshold = 262144/*512*512*/;
|
|
}
|
|
}
|
|
dist = DistanceSquared( impactPos, NPCInfo->enemyLastSeenLocation );
|
|
if ( dist > distThreshold )
|
|
{//impact would be too far from enemy
|
|
tooFar = qtrue;
|
|
}
|
|
}
|
|
|
|
if ( !tooClose && !tooFar )
|
|
{//okay too shoot at last pos
|
|
VectorSubtract( NPCInfo->enemyLastSeenLocation, muzzle, dir );
|
|
VectorNormalize( dir );
|
|
vectoangles( dir, angles );
|
|
|
|
NPCInfo->desiredYaw = angles[YAW];
|
|
NPCInfo->desiredPitch = angles[PITCH];
|
|
|
|
shoot = qtrue;
|
|
faceEnemy = qfalse;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void NPC_GM_StartLaser( void )
|
|
{
|
|
if ( !NPC->lockCount )
|
|
{//haven't already started a laser attack
|
|
//warm up for the beam attack
|
|
NPC_SetAnim( NPC, SETANIM_TORSO, TORSO_RAISEWEAP2, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
TIMER_Set( NPC, "beamDelay", NPC->client->ps.torsoAnimTimer );
|
|
TIMER_Set( NPC, "attackDelay", NPC->client->ps.torsoAnimTimer+3000 );
|
|
NPC->lockCount = 1;
|
|
//turn on warmup effect
|
|
G_PlayEffect( "galak/beam_warmup", NPC->s.number );
|
|
G_SoundOnEnt( NPC, CHAN_AUTO, "sound/weapons/galak/lasercharge.wav" );
|
|
}
|
|
}
|
|
|
|
void GM_StartGloat( void )
|
|
{
|
|
NPC->wait = 0;
|
|
gi.G2API_SetSurfaceOnOff( &NPC->ghoul2[NPC->playerModel], "torso_galakface_off", TURN_ON );
|
|
gi.G2API_SetSurfaceOnOff( &NPC->ghoul2[NPC->playerModel], "torso_galakhead_off", TURN_ON );
|
|
gi.G2API_SetSurfaceOnOff( &NPC->ghoul2[NPC->playerModel], "torso_eyes_mouth_off", TURN_ON );
|
|
gi.G2API_SetSurfaceOnOff( &NPC->ghoul2[NPC->playerModel], "torso_collar_off", TURN_ON );
|
|
gi.G2API_SetSurfaceOnOff( &NPC->ghoul2[NPC->playerModel], "torso_galaktorso_off", TURN_ON );
|
|
NPC_SetAnim( NPC, SETANIM_BOTH, BOTH_STAND2TO1, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
NPC->client->ps.legsAnimTimer += 500;
|
|
NPC->client->ps.torsoAnimTimer += 500;
|
|
}
|
|
/*
|
|
-------------------------
|
|
NPC_BSGM_Attack
|
|
-------------------------
|
|
*/
|
|
|
|
void NPC_BSGM_Attack( void )
|
|
{
|
|
//Don't do anything if we're hurt
|
|
if ( NPC->painDebounceTime > level.time )
|
|
{
|
|
NPC_UpdateAngles( qtrue, qtrue );
|
|
return;
|
|
}
|
|
|
|
//FIXME: if killed enemy, use victory anim
|
|
if ( NPC->enemy && NPC->enemy->health <= 0
|
|
&& !NPC->enemy->s.number )
|
|
{//my enemy is dead
|
|
if ( NPC->client->ps.torsoAnim == BOTH_STAND2TO1 )
|
|
{
|
|
if ( NPC->client->ps.torsoAnimTimer <= 500 )
|
|
{
|
|
G_AddVoiceEvent( NPC, Q_irand( EV_VICTORY1, EV_VICTORY3 ), 3000 );
|
|
NPC_SetAnim( NPC, SETANIM_BOTH, BOTH_TRIUMPHANT1START, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
NPC->client->ps.legsAnimTimer += 500;
|
|
NPC->client->ps.torsoAnimTimer += 500;
|
|
}
|
|
}
|
|
else if ( NPC->client->ps.torsoAnim == BOTH_TRIUMPHANT1START )
|
|
{
|
|
if ( NPC->client->ps.torsoAnimTimer <= 500 )
|
|
{
|
|
NPC_SetAnim( NPC, SETANIM_BOTH, BOTH_TRIUMPHANT1STARTGESTURE, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
NPC->client->ps.legsAnimTimer += 500;
|
|
NPC->client->ps.torsoAnimTimer += 500;
|
|
}
|
|
}
|
|
else if ( NPC->client->ps.torsoAnim == BOTH_TRIUMPHANT1STARTGESTURE )
|
|
{
|
|
if ( NPC->client->ps.torsoAnimTimer <= 500 )
|
|
{
|
|
NPC_SetAnim( NPC, SETANIM_BOTH, BOTH_TRIUMPHANT1STOP, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
NPC->client->ps.legsAnimTimer += 500;
|
|
NPC->client->ps.torsoAnimTimer += 500;
|
|
}
|
|
}
|
|
else if ( NPC->client->ps.torsoAnim == BOTH_TRIUMPHANT1STOP )
|
|
{
|
|
if ( NPC->client->ps.torsoAnimTimer <= 500 )
|
|
{
|
|
NPC_SetAnim( NPC, SETANIM_BOTH, BOTH_STAND1, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
NPC->client->ps.legsAnimTimer = -1;
|
|
NPC->client->ps.torsoAnimTimer = -1;
|
|
}
|
|
}
|
|
else if ( NPC->wait )
|
|
{
|
|
if ( TIMER_Done( NPC, "gloatTime" ) )
|
|
{
|
|
GM_StartGloat();
|
|
}
|
|
else if ( DistanceHorizontalSquared( NPC->client->renderInfo.eyePoint, NPC->enemy->currentOrigin ) > 4096 && (NPCInfo->scriptFlags&SCF_CHASE_ENEMIES) )//64 squared
|
|
{
|
|
NPCInfo->goalEntity = NPC->enemy;
|
|
GM_Move();
|
|
}
|
|
else
|
|
{//got there
|
|
GM_StartGloat();
|
|
}
|
|
}
|
|
NPC_FaceEnemy( qtrue );
|
|
NPC_UpdateAngles( qtrue, qtrue );
|
|
return;
|
|
}
|
|
//If we don't have an enemy, just idle
|
|
if ( NPC_CheckEnemyExt() == qfalse || !NPC->enemy )
|
|
{
|
|
NPC->enemy = NULL;
|
|
NPC_BSGM_Patrol();
|
|
return;
|
|
}
|
|
|
|
enemyLOS = enemyCS = qfalse;
|
|
AImove = qtrue;
|
|
faceEnemy = qfalse;
|
|
shoot = qfalse;
|
|
hitAlly = qfalse;
|
|
VectorClear( impactPos );
|
|
enemyDist = DistanceSquared( NPC->currentOrigin, NPC->enemy->currentOrigin );
|
|
|
|
if ( NPC->client->ps.torsoAnim == BOTH_ATTACK4 ||
|
|
NPC->client->ps.torsoAnim == BOTH_ATTACK5 )
|
|
{
|
|
shoot = qfalse;
|
|
if ( TIMER_Done( NPC, "smackTime" ) && !NPCInfo->blockedDebounceTime )
|
|
{//time to smack
|
|
//recheck enemyDist and InFront
|
|
if ( enemyDist < MELEE_DIST_SQUARED && InFront( NPC->enemy->currentOrigin, NPC->currentOrigin, NPC->client->ps.viewangles, 0.3f ) )
|
|
{
|
|
vec3_t smackDir;
|
|
VectorSubtract( NPC->enemy->currentOrigin, NPC->currentOrigin, smackDir );
|
|
smackDir[2] += 30;
|
|
VectorNormalize( smackDir );
|
|
//hurt them
|
|
G_Sound( NPC->enemy, G_SoundIndex( "sound/weapons/galak/skewerhit.wav" ) );
|
|
G_Damage( NPC->enemy, NPC, NPC, smackDir, NPC->currentOrigin, (g_spskill->integer+1)*Q_irand( 5, 10), DAMAGE_NO_ARMOR|DAMAGE_NO_KNOCKBACK, MOD_CRUSH );
|
|
if ( NPC->client->ps.torsoAnim == BOTH_ATTACK4 )
|
|
{//smackdown
|
|
int knockAnim = BOTH_KNOCKDOWN1;
|
|
if ( PM_CrouchAnim( NPC->enemy->client->ps.legsAnim ) )
|
|
{//knockdown from crouch
|
|
knockAnim = BOTH_KNOCKDOWN4;
|
|
}
|
|
//throw them
|
|
smackDir[2] = 1;
|
|
VectorNormalize( smackDir );
|
|
G_Throw( NPC->enemy, smackDir, 50 );
|
|
NPC_SetAnim( NPC->enemy, SETANIM_BOTH, knockAnim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
else
|
|
{//uppercut
|
|
//throw them
|
|
G_Throw( NPC->enemy, smackDir, 100 );
|
|
//make them backflip
|
|
NPC_SetAnim( NPC->enemy, SETANIM_BOTH, BOTH_KNOCKDOWN5, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
}
|
|
//done with the damage
|
|
NPCInfo->blockedDebounceTime = 1;
|
|
}
|
|
}
|
|
}
|
|
else if ( NPC->lockCount ) //already shooting laser
|
|
{//sometimes use the laser beam attack, but only after he's taken down our generator
|
|
shoot = qfalse;
|
|
if ( NPC->lockCount == 1 )
|
|
{//charging up
|
|
if ( TIMER_Done( NPC, "beamDelay" ) )
|
|
{//time to start the beam
|
|
int laserAnim;
|
|
if ( Q_irand( 0, 1 ) )
|
|
{
|
|
laserAnim = BOTH_ATTACK2;
|
|
}
|
|
else
|
|
{
|
|
laserAnim = BOTH_ATTACK7;
|
|
}
|
|
NPC_SetAnim( NPC, SETANIM_BOTH, laserAnim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
TIMER_Set( NPC, "attackDelay", NPC->client->ps.torsoAnimTimer + Q_irand( 1000, 3000 ) );
|
|
//turn on beam effect
|
|
NPC->lockCount = 2;
|
|
G_PlayEffect( "galak/trace_beam", NPC->s.number );
|
|
NPC->s.loopSound = G_SoundIndex( "sound/weapons/galak/lasercutting.wav" );
|
|
if ( !NPCInfo->coverTarg )
|
|
{//for moving looping sound at end of trace
|
|
NPCInfo->coverTarg = G_Spawn();
|
|
if ( NPCInfo->coverTarg )
|
|
{
|
|
G_SetOrigin( NPCInfo->coverTarg, NPC->client->renderInfo.muzzlePoint );
|
|
NPCInfo->coverTarg->svFlags |= SVF_BROADCAST;
|
|
NPCInfo->coverTarg->s.loopSound = G_SoundIndex( "sound/weapons/galak/lasercutting.wav" );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{//in the actual attack now
|
|
if ( !NPC->client->ps.torsoAnimTimer )
|
|
{//attack done!
|
|
NPC->lockCount = 0;
|
|
G_FreeEntity( NPCInfo->coverTarg );
|
|
NPC->s.loopSound = 0;
|
|
NPC_SetAnim( NPC, SETANIM_TORSO, TORSO_DROPWEAP2, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
TIMER_Set( NPC, "attackDelay", NPC->client->ps.torsoAnimTimer );
|
|
}
|
|
else
|
|
{//attack still going
|
|
//do the trace and damage
|
|
trace_t trace;
|
|
vec3_t end, mins={-3,-3,-3}, maxs={3,3,3};
|
|
VectorMA( NPC->client->renderInfo.muzzlePoint, 1024, NPC->client->renderInfo.muzzleDir, end );
|
|
gi.trace( &trace, NPC->client->renderInfo.muzzlePoint, mins, maxs, end, NPC->s.number, MASK_SHOT, G2_NOCOLLIDE, 0 );
|
|
if ( trace.allsolid || trace.startsolid )
|
|
{//oops, in a wall
|
|
if ( NPCInfo->coverTarg )
|
|
{
|
|
G_SetOrigin( NPCInfo->coverTarg, NPC->client->renderInfo.muzzlePoint );
|
|
}
|
|
}
|
|
else
|
|
{//clear
|
|
if ( trace.fraction < 1.0f )
|
|
{//hit something
|
|
gentity_t *traceEnt = &g_entities[trace.entityNum];
|
|
if ( traceEnt && traceEnt->takedamage )
|
|
{//damage it
|
|
G_SoundAtSpot( trace.endpos, G_SoundIndex( "sound/weapons/galak/laserdamage.wav" ) );
|
|
G_Damage( traceEnt, NPC, NPC, NPC->client->renderInfo.muzzleDir, trace.endpos, 10, 0, MOD_ENERGY );
|
|
}
|
|
}
|
|
if ( NPCInfo->coverTarg )
|
|
{
|
|
G_SetOrigin( NPCInfo->coverTarg, trace.endpos );
|
|
}
|
|
if ( !Q_irand( 0, 5 ) )
|
|
{
|
|
G_SoundAtSpot( trace.endpos, G_SoundIndex( "sound/weapons/galak/laserdamage.wav" ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{//Okay, we're not in a special attack, see if we should switch weapons or start a special attack
|
|
/*
|
|
if ( NPC->s.weapon == WP_REPEATER
|
|
&& !(NPCInfo->scriptFlags & SCF_ALT_FIRE)//using rapid-fire
|
|
&& NPC->enemy->s.weapon == WP_SABER //enemy using saber
|
|
&& NPC->client && (NPC->client->ps.saberEventFlags&SEF_DEFLECTED)
|
|
&& !Q_irand( 0, 50 ) )
|
|
{//he's deflecting my shots, switch to the laser or the lob fire for a while
|
|
TIMER_Set( NPC, "noRapid", Q_irand( 2000, 6000 ) );
|
|
NPCInfo->scriptFlags |= SCF_ALT_FIRE;
|
|
NPC->alt_fire = qtrue;
|
|
if ( NPC->locationDamage[HL_GENERIC1] > GENERATOR_HEALTH && (Q_irand( 0, 1 )||enemyDist < MAX_LOB_DIST_SQUARED) )
|
|
{//shield down, use laser
|
|
NPC_GM_StartLaser();
|
|
}
|
|
}
|
|
else*/
|
|
if ( !NPC->client->ps.powerups[PW_GALAK_SHIELD]
|
|
&& enemyDist < MELEE_DIST_SQUARED
|
|
&& InFront( NPC->enemy->currentOrigin, NPC->currentOrigin, NPC->client->ps.viewangles, 0.3f )
|
|
&& G_StandardHumanoid( NPC->enemy->NPC_type ) )//within 80 and in front
|
|
{//our shield is down, and enemy within 80, if very close, use melee attack to slap away
|
|
if ( TIMER_Done( NPC, "attackDelay" ) )
|
|
{
|
|
//animate me
|
|
int swingAnim;
|
|
if ( NPC->locationDamage[HL_GENERIC1] > GENERATOR_HEALTH )
|
|
{//generator down, use random melee
|
|
swingAnim = Q_irand( BOTH_ATTACK4, BOTH_ATTACK5 );//smackdown or uppercut
|
|
}
|
|
else
|
|
{//always knock-away
|
|
swingAnim = BOTH_ATTACK5;//uppercut
|
|
}
|
|
//FIXME: swing sound
|
|
NPC_SetAnim( NPC, SETANIM_BOTH, swingAnim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
TIMER_Set( NPC, "attackDelay", NPC->client->ps.torsoAnimTimer + Q_irand( 1000, 3000 ) );
|
|
//delay the hurt until the proper point in the anim
|
|
TIMER_Set( NPC, "smackTime", 600 );
|
|
NPCInfo->blockedDebounceTime = 0;
|
|
//FIXME: say something?
|
|
}
|
|
}
|
|
else if ( !NPC->lockCount && NPC->locationDamage[HL_GENERIC1] > GENERATOR_HEALTH
|
|
&& TIMER_Done( NPC, "attackDelay" )
|
|
&& InFront( NPC->enemy->currentOrigin, NPC->currentOrigin, NPC->client->ps.viewangles, 0.3f )
|
|
&& ((!Q_irand( 0, 10*(2-g_spskill->integer))&& enemyDist > MIN_LOB_DIST_SQUARED&& enemyDist < MAX_LOB_DIST_SQUARED)
|
|
||(!TIMER_Done( NPC, "noLob" )&&!TIMER_Done( NPC, "noRapid" )))
|
|
&& NPC->enemy->s.weapon != WP_TURRET )
|
|
{//sometimes use the laser beam attack, but only after he's taken down our generator
|
|
shoot = qfalse;
|
|
NPC_GM_StartLaser();
|
|
}
|
|
else if ( enemyDist < MIN_LOB_DIST_SQUARED
|
|
&& (NPC->enemy->s.weapon != WP_TURRET || Q_stricmp( "PAS", NPC->enemy->classname ))
|
|
&& TIMER_Done( NPC, "noRapid" ) )//256
|
|
{//enemy within 256
|
|
if ( (NPC->client->ps.weapon == WP_REPEATER) && (NPCInfo->scriptFlags & SCF_ALT_FIRE) )
|
|
{//shooting an explosive, but enemy too close, switch to primary fire
|
|
NPCInfo->scriptFlags &= ~SCF_ALT_FIRE;
|
|
NPC->alt_fire = qfalse;
|
|
//FIXME: use weap raise & lower anims
|
|
NPC_ChangeWeapon( WP_REPEATER );
|
|
}
|
|
}
|
|
else if ( (enemyDist > MAX_LOB_DIST_SQUARED || (NPC->enemy->s.weapon == WP_TURRET && !Q_stricmp( "PAS", NPC->enemy->classname )))
|
|
&& TIMER_Done( NPC, "noLob" ) )//448
|
|
{//enemy more than 448 away and we are ready to try lob fire again
|
|
if ( (NPC->client->ps.weapon == WP_REPEATER) && !(NPCInfo->scriptFlags & SCF_ALT_FIRE) )
|
|
{//enemy far enough away to use lobby explosives
|
|
NPCInfo->scriptFlags |= SCF_ALT_FIRE;
|
|
NPC->alt_fire = qtrue;
|
|
//FIXME: use weap raise & lower anims
|
|
NPC_ChangeWeapon( WP_REPEATER );
|
|
}
|
|
}
|
|
}
|
|
|
|
//can we see our target?
|
|
if ( NPC_ClearLOS( NPC->enemy ) )
|
|
{
|
|
NPCInfo->enemyLastSeenTime = level.time;//used here for aim debouncing, not always a clear LOS
|
|
enemyLOS = qtrue;
|
|
|
|
if ( NPC->client->ps.weapon == WP_NONE )
|
|
{
|
|
enemyCS = qfalse;//not true, but should stop us from firing
|
|
NPC_AimAdjust( -1 );//adjust aim worse longer we have no weapon
|
|
}
|
|
else
|
|
{//can we shoot our target?
|
|
if ( ((NPC->client->ps.weapon == WP_REPEATER && (NPCInfo->scriptFlags&SCF_ALT_FIRE))) && enemyDist < MIN_LOB_DIST_SQUARED )//256
|
|
{
|
|
enemyCS = qfalse;//not true, but should stop us from firing
|
|
hitAlly = qtrue;//us!
|
|
//FIXME: if too close, run away!
|
|
}
|
|
else
|
|
{
|
|
int hit = NPC_ShotEntity( NPC->enemy, impactPos );
|
|
gentity_t *hitEnt = &g_entities[hit];
|
|
if ( hit == NPC->enemy->s.number
|
|
|| ( hitEnt && hitEnt->client && hitEnt->client->playerTeam == NPC->client->enemyTeam )
|
|
|| ( hitEnt && hitEnt->takedamage ) )
|
|
{//can hit enemy or will hit glass or other breakable, so shoot anyway
|
|
enemyCS = qtrue;
|
|
NPC_AimAdjust( 2 );//adjust aim better longer we have clear shot at enemy
|
|
VectorCopy( NPC->enemy->currentOrigin, NPCInfo->enemyLastSeenLocation );
|
|
}
|
|
else
|
|
{//Hmm, have to get around this bastard
|
|
NPC_AimAdjust( 1 );//adjust aim better longer we can see enemy
|
|
if ( hitEnt && hitEnt->client && hitEnt->client->playerTeam == NPC->client->playerTeam )
|
|
{//would hit an ally, don't fire!!!
|
|
hitAlly = qtrue;
|
|
}
|
|
else
|
|
{//Check and see where our shot *would* hit... if it's not close to the enemy (within 256?), then don't fire
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if ( gi.inPVS( NPC->enemy->currentOrigin, NPC->currentOrigin ) )
|
|
{
|
|
if ( TIMER_Done( NPC, "talkDebounce" ) && !Q_irand( 0, 10 ) )
|
|
{
|
|
if ( NPCInfo->enemyCheckDebounceTime < 8 )
|
|
{
|
|
int speech = -1;
|
|
switch( NPCInfo->enemyCheckDebounceTime )
|
|
{
|
|
case 0:
|
|
case 1:
|
|
case 2:
|
|
speech = EV_CHASE1 + NPCInfo->enemyCheckDebounceTime;
|
|
break;
|
|
case 3:
|
|
case 4:
|
|
case 5:
|
|
speech = EV_COVER1 + NPCInfo->enemyCheckDebounceTime-3;
|
|
break;
|
|
case 6:
|
|
case 7:
|
|
speech = EV_ESCAPING1 + NPCInfo->enemyCheckDebounceTime-6;
|
|
break;
|
|
}
|
|
NPCInfo->enemyCheckDebounceTime++;
|
|
if ( speech != -1 )
|
|
{
|
|
G_AddVoiceEvent( NPC, speech, Q_irand( 3000, 5000 ) );
|
|
TIMER_Set( NPC, "talkDebounce", Q_irand( 5000, 7000 ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
NPCInfo->enemyLastSeenTime = level.time;
|
|
|
|
int hit = NPC_ShotEntity( NPC->enemy, impactPos );
|
|
gentity_t *hitEnt = &g_entities[hit];
|
|
if ( hit == NPC->enemy->s.number
|
|
|| ( hitEnt && hitEnt->client && hitEnt->client->playerTeam == NPC->client->enemyTeam )
|
|
|| ( hitEnt && hitEnt->takedamage ) )
|
|
{//can hit enemy or will hit glass or other breakable, so shoot anyway
|
|
enemyCS = qtrue;
|
|
}
|
|
else
|
|
{
|
|
faceEnemy = qtrue;
|
|
NPC_AimAdjust( -1 );//adjust aim worse longer we cannot see enemy
|
|
}
|
|
}
|
|
|
|
if ( enemyLOS )
|
|
{
|
|
faceEnemy = qtrue;
|
|
}
|
|
else
|
|
{
|
|
if ( !NPCInfo->goalEntity )
|
|
{
|
|
NPCInfo->goalEntity = NPC->enemy;
|
|
}
|
|
if ( NPCInfo->goalEntity == NPC->enemy )
|
|
{//for now, always chase the enemy
|
|
AImove = qtrue;
|
|
}
|
|
}
|
|
if ( enemyCS )
|
|
{
|
|
shoot = qtrue;
|
|
//NPCInfo->enemyCheckDebounceTime = level.time;//actually used here as a last actual LOS
|
|
}
|
|
else
|
|
{
|
|
if ( !NPCInfo->goalEntity )
|
|
{
|
|
NPCInfo->goalEntity = NPC->enemy;
|
|
}
|
|
if ( NPCInfo->goalEntity == NPC->enemy )
|
|
{//for now, always chase the enemy
|
|
AImove = qtrue;
|
|
}
|
|
}
|
|
|
|
//Check for movement to take care of
|
|
GM_CheckMoveState();
|
|
|
|
//See if we should override shooting decision with any special considerations
|
|
GM_CheckFireState();
|
|
|
|
if ( NPC->client->ps.weapon == WP_REPEATER && (NPCInfo->scriptFlags&SCF_ALT_FIRE) && shoot && TIMER_Done( NPC, "attackDelay" ) )
|
|
{
|
|
vec3_t muzzle;
|
|
vec3_t angles;
|
|
vec3_t target;
|
|
vec3_t velocity = {0,0,0};
|
|
vec3_t mins = {-REPEATER_ALT_SIZE,-REPEATER_ALT_SIZE,-REPEATER_ALT_SIZE}, maxs = {REPEATER_ALT_SIZE,REPEATER_ALT_SIZE,REPEATER_ALT_SIZE};
|
|
|
|
CalcEntitySpot( NPC, SPOT_WEAPON, muzzle );
|
|
|
|
VectorCopy( NPC->enemy->currentOrigin, target );
|
|
|
|
target[0] += Q_flrand( -5, 5 )+(Q_flrand(-1.0f, 1.0f)*(6-NPCInfo->currentAim)*2);
|
|
target[1] += Q_flrand( -5, 5 )+(Q_flrand(-1.0f, 1.0f)*(6-NPCInfo->currentAim)*2);
|
|
target[2] += Q_flrand( -5, 5 )+(Q_flrand(-1.0f, 1.0f)*(6-NPCInfo->currentAim)*2);
|
|
|
|
//Find the desired angles
|
|
qboolean clearshot = WP_LobFire( NPC, muzzle, target, mins, maxs, MASK_SHOT|CONTENTS_LIGHTSABER,
|
|
velocity, qtrue, NPC->s.number, NPC->enemy->s.number,
|
|
300, 1100, 1500, qtrue );
|
|
if ( VectorCompare( vec3_origin, velocity ) || (!clearshot&&enemyLOS&&enemyCS) )
|
|
{//no clear lob shot and no lob shot that will hit something breakable
|
|
if ( enemyLOS && enemyCS && TIMER_Done( NPC, "noRapid" ) )
|
|
{//have a clear straight shot, so switch to primary
|
|
NPCInfo->scriptFlags &= ~SCF_ALT_FIRE;
|
|
NPC->alt_fire = qfalse;
|
|
NPC_ChangeWeapon( WP_REPEATER );
|
|
//keep this weap for a bit
|
|
TIMER_Set( NPC, "noLob", Q_irand( 500, 1000 ) );
|
|
}
|
|
else
|
|
{
|
|
shoot = qfalse;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
vectoangles( velocity, angles );
|
|
|
|
NPCInfo->desiredYaw = AngleNormalize360( angles[YAW] );
|
|
NPCInfo->desiredPitch = AngleNormalize360( angles[PITCH] );
|
|
|
|
VectorCopy( velocity, NPC->client->hiddenDir );
|
|
NPC->client->hiddenDist = VectorNormalize ( NPC->client->hiddenDir );
|
|
}
|
|
}
|
|
else if ( faceEnemy )
|
|
{//face the enemy
|
|
NPC_FaceEnemy( qtrue );
|
|
}
|
|
|
|
if ( !TIMER_Done( NPC, "standTime" ) )
|
|
{
|
|
AImove = qfalse;
|
|
}
|
|
if ( !(NPCInfo->scriptFlags&SCF_CHASE_ENEMIES) )
|
|
{//not supposed to chase my enemies
|
|
if ( NPCInfo->goalEntity == NPC->enemy )
|
|
{//goal is my entity, so don't move
|
|
AImove = qfalse;
|
|
}
|
|
}
|
|
|
|
if ( AImove && !NPC->lockCount )
|
|
{//move toward goal
|
|
if ( NPCInfo->goalEntity
|
|
&& NPC->client->ps.legsAnim != BOTH_ALERT1
|
|
&& NPC->client->ps.legsAnim != BOTH_ATTACK2
|
|
&& NPC->client->ps.legsAnim != BOTH_ATTACK4
|
|
&& NPC->client->ps.legsAnim != BOTH_ATTACK5
|
|
&& NPC->client->ps.legsAnim != BOTH_ATTACK7 )
|
|
{
|
|
AImove = GM_Move();
|
|
}
|
|
else
|
|
{
|
|
AImove = qfalse;
|
|
}
|
|
}
|
|
|
|
if ( !TIMER_Done( NPC, "flee" ) )
|
|
{//running away
|
|
faceEnemy = qfalse;
|
|
}
|
|
|
|
//FIXME: check scf_face_move_dir here?
|
|
|
|
if ( !faceEnemy )
|
|
{//we want to face in the dir we're running
|
|
if ( !AImove )
|
|
{//if we haven't moved, we should look in the direction we last looked?
|
|
VectorCopy( NPC->client->ps.viewangles, NPCInfo->lastPathAngles );
|
|
}
|
|
if ( AImove )
|
|
{//don't run away and shoot
|
|
NPCInfo->desiredYaw = NPCInfo->lastPathAngles[YAW];
|
|
NPCInfo->desiredPitch = 0;
|
|
shoot = qfalse;
|
|
}
|
|
}
|
|
NPC_UpdateAngles( qtrue, qtrue );
|
|
|
|
if ( NPCInfo->scriptFlags & SCF_DONT_FIRE )
|
|
{
|
|
shoot = qfalse;
|
|
}
|
|
|
|
if ( NPC->enemy && NPC->enemy->enemy )
|
|
{
|
|
if ( NPC->enemy->s.weapon == WP_SABER && NPC->enemy->enemy->s.weapon == WP_SABER )
|
|
{//don't shoot at an enemy jedi who is fighting another jedi, for fear of injuring one or causing rogue blaster deflections (a la Obi Wan/Vader duel at end of ANH)
|
|
shoot = qfalse;
|
|
}
|
|
}
|
|
//FIXME: don't shoot right away!
|
|
if ( shoot )
|
|
{//try to shoot if it's time
|
|
if ( TIMER_Done( NPC, "attackDelay" ) )
|
|
{
|
|
if( !(NPCInfo->scriptFlags & SCF_FIRE_WEAPON) ) // we've already fired, no need to do it again here
|
|
{
|
|
WeaponThink( qtrue );
|
|
}
|
|
}
|
|
}
|
|
|
|
//also:
|
|
if ( NPC->enemy->s.weapon == WP_TURRET && !Q_stricmp( "PAS", NPC->enemy->classname ) )
|
|
{//crush turrets
|
|
if ( G_BoundsOverlap( NPC->absmin, NPC->absmax, NPC->enemy->absmin, NPC->enemy->absmax ) )
|
|
{//have to do this test because placed turrets are not solid to NPCs (so they don't obstruct navigation)
|
|
if ( NPC->client->ps.powerups[PW_GALAK_SHIELD] > 0 )
|
|
{
|
|
NPC->client->ps.powerups[PW_BATTLESUIT] = level.time + ARMOR_EFFECT_TIME;
|
|
G_Damage( NPC->enemy, NPC, NPC, NULL, NPC->currentOrigin, 100, DAMAGE_NO_KNOCKBACK, MOD_ELECTROCUTE );
|
|
}
|
|
else
|
|
{
|
|
G_Damage( NPC->enemy, NPC, NPC, NULL, NPC->currentOrigin, 100, DAMAGE_NO_KNOCKBACK, MOD_CRUSH );
|
|
}
|
|
}
|
|
}
|
|
else if ( NPCInfo->touchedByPlayer != NULL && NPCInfo->touchedByPlayer == NPC->enemy )
|
|
{//touched enemy
|
|
if ( NPC->client->ps.powerups[PW_GALAK_SHIELD] > 0 )
|
|
{//zap him!
|
|
//animate me
|
|
NPC_SetAnim( NPC, SETANIM_BOTH, BOTH_ATTACK6, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
TIMER_Set( NPC, "attackDelay", NPC->client->ps.torsoAnimTimer );
|
|
TIMER_Set( NPC, "standTime", NPC->client->ps.legsAnimTimer );
|
|
//FIXME: debounce this?
|
|
NPCInfo->touchedByPlayer = NULL;
|
|
//FIXME: some shield effect?
|
|
NPC->client->ps.powerups[PW_BATTLESUIT] = level.time + ARMOR_EFFECT_TIME;
|
|
vec3_t smackDir;
|
|
VectorSubtract( NPC->enemy->currentOrigin, NPC->currentOrigin, smackDir );
|
|
smackDir[2] += 30;
|
|
VectorNormalize( smackDir );
|
|
G_Damage( NPC->enemy, NPC, NPC, smackDir, NPC->currentOrigin, (g_spskill->integer+1)*Q_irand( 5, 10), DAMAGE_NO_KNOCKBACK, MOD_ELECTROCUTE );
|
|
//throw them
|
|
G_Throw( NPC->enemy, smackDir, 100 );
|
|
NPC->enemy->s.powerups |= ( 1 << PW_SHOCKED );
|
|
if ( NPC->enemy->client )
|
|
{
|
|
NPC->enemy->client->ps.powerups[PW_SHOCKED] = level.time + 1000;
|
|
}
|
|
//stop any attacks
|
|
ucmd.buttons = 0;
|
|
}
|
|
}
|
|
|
|
if ( NPCInfo->movementSpeech < 3 && NPCInfo->blockedSpeechDebounceTime <= level.time )
|
|
{
|
|
if ( NPC->enemy && NPC->enemy->health > 0 && NPC->enemy->painDebounceTime > level.time )
|
|
{
|
|
if ( NPC->enemy->health < 50 && NPCInfo->movementSpeech == 2 )
|
|
{
|
|
G_AddVoiceEvent( NPC, EV_ANGER2, Q_irand( 2000, 4000 ) );
|
|
NPCInfo->movementSpeech = 3;
|
|
}
|
|
else if ( NPC->enemy->health < 75 && NPCInfo->movementSpeech == 1 )
|
|
{
|
|
G_AddVoiceEvent( NPC, EV_ANGER1, Q_irand( 2000, 4000 ) );
|
|
NPCInfo->movementSpeech = 2;
|
|
}
|
|
else if ( NPC->enemy->health < 100 && NPCInfo->movementSpeech == 0 )
|
|
{
|
|
G_AddVoiceEvent( NPC, EV_ANGER3, Q_irand( 2000, 4000 ) );
|
|
NPCInfo->movementSpeech = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void NPC_BSGM_Default( void )
|
|
{
|
|
if( NPCInfo->scriptFlags & SCF_FIRE_WEAPON )
|
|
{
|
|
WeaponThink( qtrue );
|
|
}
|
|
|
|
if ( NPC->client->ps.stats[STAT_ARMOR] <= 0 )
|
|
{//armor gone
|
|
if ( !NPCInfo->investigateDebounceTime )
|
|
{//start regenerating the armor
|
|
gi.G2API_SetSurfaceOnOff( &NPC->ghoul2[NPC->playerModel], "torso_shield_off", TURN_OFF );
|
|
NPC->flags &= ~FL_SHIELDED;//no more reflections
|
|
VectorSet( NPC->mins, -20, -20, -24 );
|
|
VectorSet( NPC->maxs, 20, 20, 64 );
|
|
NPC->client->crouchheight = NPC->client->standheight = 64;
|
|
if ( NPC->locationDamage[HL_GENERIC1] < GENERATOR_HEALTH )
|
|
{//still have the generator bolt-on
|
|
if ( NPCInfo->investigateCount < 12 )
|
|
{
|
|
NPCInfo->investigateCount++;
|
|
}
|
|
NPCInfo->investigateDebounceTime = level.time + (NPCInfo->investigateCount * 5000);
|
|
}
|
|
}
|
|
else if ( NPCInfo->investigateDebounceTime < level.time )
|
|
{//armor regenerated, turn shield back on
|
|
//do a trace and make sure we can turn this back on?
|
|
trace_t tr;
|
|
gi.trace( &tr, NPC->currentOrigin, shieldMins, shieldMaxs, NPC->currentOrigin, NPC->s.number, NPC->clipmask, G2_NOCOLLIDE, 0 );
|
|
if ( !tr.startsolid )
|
|
{
|
|
VectorCopy( shieldMins, NPC->mins );
|
|
VectorCopy( shieldMaxs, NPC->maxs );
|
|
NPC->client->crouchheight = NPC->client->standheight = shieldMaxs[2];
|
|
NPC->client->ps.stats[STAT_ARMOR] = GALAK_SHIELD_HEALTH;
|
|
NPCInfo->investigateDebounceTime = 0;
|
|
NPC->flags |= FL_SHIELDED;//reflect normal shots
|
|
NPC->fx_time = level.time;
|
|
gi.G2API_SetSurfaceOnOff( &NPC->ghoul2[NPC->playerModel], "torso_shield_off", TURN_ON );
|
|
}
|
|
}
|
|
}
|
|
if ( NPC->client->ps.stats[STAT_ARMOR] > 0 )
|
|
{//armor present
|
|
NPC->client->ps.powerups[PW_GALAK_SHIELD] = Q3_INFINITE;//temp, for effect
|
|
gi.G2API_SetSurfaceOnOff( &NPC->ghoul2[NPC->playerModel], "torso_shield_off", TURN_ON );
|
|
}
|
|
else
|
|
{
|
|
gi.G2API_SetSurfaceOnOff( &NPC->ghoul2[NPC->playerModel], "torso_shield_off", TURN_OFF );
|
|
}
|
|
|
|
if( !NPC->enemy )
|
|
{//don't have an enemy, look for one
|
|
NPC_BSGM_Patrol();
|
|
}
|
|
else //if ( NPC->enemy )
|
|
{//have an enemy
|
|
NPC_BSGM_Attack();
|
|
}
|
|
}
|