mirror of
https://github.com/DrBeef/JKXR.git
synced 2024-11-29 23:42:38 +00:00
4597b03873
Opens in Android Studio but haven't even tried to build it yet (it won't.. I know that much!)
1290 lines
No EOL
38 KiB
C++
1290 lines
No EOL
38 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/>.
|
|
===========================================================================
|
|
*/
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
// RAVEN SOFTWARE - STAR WARS: JK II
|
|
// (c) 2002 Activision
|
|
//
|
|
// Boba Fett
|
|
// ---------
|
|
// Ah yes, this file is pretty messy. I've tried to move everything in here, but in fact
|
|
// a lot of his AI occurs in the seeker and jedi AI files. Some of these functions
|
|
//
|
|
//
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
#include "b_local.h"
|
|
#include "../Ravl/CVec.h"
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
// Forward References Of Functions
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
void Boba_Precache( void );
|
|
void Boba_DustFallNear(const vec3_t origin, int dustcount);
|
|
void Boba_ChangeWeapon(int wp);
|
|
qboolean Boba_StopKnockdown(gentity_t *self, gentity_t *pusher, const vec3_t pushDir, qboolean forceKnockdown = qfalse);
|
|
|
|
// Flight Related Functions (also used by Rocket Trooper)
|
|
//--------------------------------------------------------
|
|
qboolean Boba_Flying( gentity_t *self );
|
|
void Boba_FlyStart( gentity_t *self );
|
|
void Boba_FlyStop( gentity_t *self );
|
|
|
|
// Called From NPC_Pain()
|
|
//-----------------------------
|
|
void Boba_Pain( gentity_t *self, gentity_t *inflictor, int damage, int mod);
|
|
|
|
|
|
// Local: Flame Thrower Weapon
|
|
//-----------------------------
|
|
void Boba_FireFlameThrower( gentity_t *self );
|
|
void Boba_StopFlameThrower( gentity_t *self );
|
|
void Boba_StartFlameThrower( gentity_t *self );
|
|
void Boba_DoFlameThrower( gentity_t *self );
|
|
|
|
// Local: Other Tactics
|
|
//----------------------
|
|
void Boba_DoAmbushWait( gentity_t *self);
|
|
void Boba_DoSniper( gentity_t *self);
|
|
|
|
// Local: Respawning
|
|
//-------------------
|
|
bool Boba_Respawn();
|
|
|
|
// Called From Within AI_Jedi && AI_Seeker
|
|
//-----------------------------------------
|
|
void Boba_Fire();
|
|
void Boba_FireDecide();
|
|
|
|
// Local: Called From Tactics()
|
|
//----------------------------
|
|
void Boba_TacticsSelect();
|
|
bool Boba_CanSeeEnemy( gentity_t *self );
|
|
|
|
|
|
// Called From NPC_RunBehavior()
|
|
//-------------------------------
|
|
void Boba_Update(); // Always Called First, Before Any Other Thinking
|
|
bool Boba_Tactics(); // If returns true, Jedi and Seeker AI not used
|
|
bool Boba_Flee(); // If returns true, Jedi and Seeker AI not used
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
// External Functions
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
extern void G_SoundAtSpot( vec3_t org, int soundIndex, qboolean broadcast );
|
|
extern void G_CreateG2AttachedWeaponModel( gentity_t *ent, const char *weaponModel, int boltNum, int weaponNum );
|
|
extern void ChangeWeapon( gentity_t *ent, int newWeapon );
|
|
extern void WP_ResistForcePush( gentity_t *self, gentity_t *pusher, qboolean noPenalty );
|
|
extern void ForceJump( gentity_t *self, usercmd_t *ucmd );
|
|
extern void G_Knockdown( gentity_t *self, gentity_t *attacker, const vec3_t pushDir, float strength, qboolean breakSaberLock );
|
|
|
|
extern void CG_DrawEdge( vec3_t start, vec3_t end, int type );
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
// External Data
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
extern cvar_t* g_bobaDebug;
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
// Boba Debug Output
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
#ifndef FINAL_BUILD
|
|
#if !defined(CTYPE_H_INC)
|
|
#include <ctype.h>
|
|
#define CTYPE_H_INC
|
|
#endif
|
|
|
|
#if !defined(STDARG_H_INC)
|
|
#include <stdarg.h>
|
|
#define STDARG_H_INC
|
|
#endif
|
|
|
|
#if !defined(STDIO_H_INC)
|
|
#include <stdio.h>
|
|
#define STDIO_H_INC
|
|
#endif
|
|
void Boba_Printf(const char * format, ...)
|
|
{
|
|
if (g_bobaDebug->integer==0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
static char string[2][1024]; // in case this is called by nested functions
|
|
static int index = 0;
|
|
static char nFormat[300];
|
|
char* buf;
|
|
|
|
// Tack On The Standard Format Around The Given Format
|
|
//-----------------------------------------------------
|
|
Com_sprintf(nFormat, sizeof(nFormat), "[BOBA %8d] %s\n", level.time, format);
|
|
|
|
|
|
// Resolve Remaining Elipsis Parameters Into Newly Formated String
|
|
//-----------------------------------------------------------------
|
|
buf = string[index & 1];
|
|
index++;
|
|
|
|
va_list argptr;
|
|
va_start (argptr, format);
|
|
Q_vsnprintf (buf, sizeof(*string), nFormat, argptr);
|
|
va_end (argptr);
|
|
|
|
// Print It To Debug Output Console
|
|
//----------------------------------
|
|
gi.Printf(buf);
|
|
}
|
|
#else
|
|
void Boba_Printf(const char * format, ...)
|
|
{
|
|
}
|
|
#endif
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
// Defines
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
#define BOBA_FLAMEDURATION 3000
|
|
#define BOBA_FLAMETHROWRANGE 128
|
|
#define BOBA_FLAMETHROWSIZE 40
|
|
#define BOBA_FLAMETHROWDAMAGEMIN 1//10
|
|
#define BOBA_FLAMETHROWDAMAGEMAX 5//40
|
|
#define BOBA_ROCKETRANGEMIN 300
|
|
#define BOBA_ROCKETRANGEMAX 2000
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
// Global Data
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
bool BobaHadDeathScript = false;
|
|
bool BobaActive = false;
|
|
vec3_t BobaFootStepLoc;
|
|
int BobaFootStepCount = 0;
|
|
|
|
vec3_t AverageEnemyDirection;
|
|
int AverageEnemyDirectionSamples;
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
// Enums
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
enum EBobaTacticsState
|
|
{
|
|
BTS_NONE,
|
|
|
|
// Attack
|
|
//--------
|
|
BTS_RIFLE, // Uses Jedi / Seeker Movement
|
|
BTS_MISSILE, // Uses Jedi / Seeker Movement
|
|
BTS_SNIPER, // Uses Special Movement Internal To This File
|
|
BTS_FLAMETHROW, // Locked In Place
|
|
|
|
// Waiting
|
|
//---------
|
|
BTS_AMBUSHWAIT, // Goto CP & Wait
|
|
|
|
BTS_MAX
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
void Boba_Precache( void )
|
|
{
|
|
G_SoundIndex( "sound/chars/boba/bf_blast-off.wav" );
|
|
G_SoundIndex( "sound/chars/boba/bf_jetpack_lp.wav" );
|
|
G_SoundIndex( "sound/chars/boba/bf_land.wav" );
|
|
G_SoundIndex( "sound/weapons/boba/bf_flame.mp3" );
|
|
G_SoundIndex( "sound/player/footsteps/boot1" );
|
|
G_SoundIndex( "sound/player/footsteps/boot2" );
|
|
G_SoundIndex( "sound/player/footsteps/boot3" );
|
|
G_SoundIndex( "sound/player/footsteps/boot4" );
|
|
G_EffectIndex( "boba/jetSP" );
|
|
G_EffectIndex( "boba/fthrw" );
|
|
G_EffectIndex( "volumetric/black_smoke" );
|
|
G_EffectIndex( "chunks/dustFall" );
|
|
|
|
AverageEnemyDirectionSamples = 0;
|
|
VectorClear(AverageEnemyDirection);
|
|
BobaHadDeathScript = false;
|
|
BobaActive = true;
|
|
BobaFootStepCount = 0;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
void Boba_DustFallNear(const vec3_t origin, int dustcount)
|
|
{
|
|
if (!BobaActive)
|
|
{
|
|
return;
|
|
}
|
|
|
|
trace_t testTrace;
|
|
vec3_t testDirection;
|
|
vec3_t testStartPos;
|
|
vec3_t testEndPos;
|
|
|
|
VectorCopy(origin, testStartPos);
|
|
for (int i=0; i<dustcount; i++)
|
|
{
|
|
testDirection[0] = (Q_flrand(0.0f, 1.0f) * 2.0f) - 1.0f;
|
|
testDirection[1] = (Q_flrand(0.0f, 1.0f) * 2.0f) - 1.0f;
|
|
testDirection[2] = 1.0f;
|
|
|
|
VectorMA(origin, 1000.0f, testDirection, testEndPos);
|
|
gi.trace (&testTrace, origin, NULL, NULL, testEndPos, (player && player->inuse)?(0):(ENTITYNUM_NONE), MASK_SHOT, (EG2_Collision)0, 0 );
|
|
|
|
if (!testTrace.startsolid &&
|
|
!testTrace.allsolid &&
|
|
testTrace.fraction>0.1f &&
|
|
testTrace.fraction<0.9f)
|
|
{
|
|
G_PlayEffect( "chunks/dustFall", testTrace.endpos, testTrace.plane.normal );
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
// This is just a super silly wrapper around NPC_Change Weapon
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
void Boba_ChangeWeapon( int wp )
|
|
{
|
|
if ( NPC->s.weapon == wp )
|
|
{
|
|
return;
|
|
}
|
|
NPC_ChangeWeapon( wp );
|
|
G_AddEvent( NPC, EV_GENERAL_SOUND, G_SoundIndex( "sound/weapons/change.wav" ));
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
// Choose an "anti-knockdown" response
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
qboolean Boba_StopKnockdown( gentity_t *self, gentity_t *pusher, const vec3_t pushDir, qboolean forceKnockdown )
|
|
{
|
|
if ( self->client->NPC_class != CLASS_BOBAFETT )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
if ( self->client->moveType == MT_FLYSWIM )
|
|
{//can't knock me down when I'm flying
|
|
return qtrue;
|
|
}
|
|
|
|
vec3_t pDir, fwd, right, ang = {0, self->currentAngles[YAW], 0};
|
|
float fDot, rDot;
|
|
int strafeTime = Q_irand( 1000, 2000 );
|
|
|
|
AngleVectors( ang, fwd, right, NULL );
|
|
VectorNormalize2( pushDir, pDir );
|
|
fDot = DotProduct( pDir, fwd );
|
|
rDot = DotProduct( pDir, right );
|
|
|
|
if ( Q_irand( 0, 2 ) )
|
|
{//flip or roll with it
|
|
usercmd_t tempCmd;
|
|
if ( fDot >= 0.4f )
|
|
{
|
|
tempCmd.forwardmove = 127;
|
|
TIMER_Set( self, "moveforward", strafeTime );
|
|
}
|
|
else if ( fDot <= -0.4f )
|
|
{
|
|
tempCmd.forwardmove = -127;
|
|
TIMER_Set( self, "moveback", strafeTime );
|
|
}
|
|
else if ( rDot > 0 )
|
|
{
|
|
tempCmd.rightmove = 127;
|
|
TIMER_Set( self, "strafeRight", strafeTime );
|
|
TIMER_Set( self, "strafeLeft", -1 );
|
|
}
|
|
else
|
|
{
|
|
tempCmd.rightmove = -127;
|
|
TIMER_Set( self, "strafeLeft", strafeTime );
|
|
TIMER_Set( self, "strafeRight", -1 );
|
|
}
|
|
G_AddEvent( self, EV_JUMP, 0 );
|
|
if ( !Q_irand( 0, 1 ) )
|
|
{//flip
|
|
self->client->ps.forceJumpCharge = 280;//FIXME: calc this intelligently?
|
|
ForceJump( self, &tempCmd );
|
|
}
|
|
else
|
|
{//roll
|
|
TIMER_Set( self, "duck", strafeTime );
|
|
}
|
|
self->painDebounceTime = 0;//so we do something
|
|
}
|
|
else if ( !Q_irand( 0, 1 ) && forceKnockdown )
|
|
{//resist
|
|
WP_ResistForcePush( self, pusher, qtrue );
|
|
}
|
|
else
|
|
{//fall down
|
|
return qfalse;
|
|
}
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
// Is this entity flying
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
qboolean Boba_Flying( gentity_t *self )
|
|
{
|
|
assert(self && self->client && self->client->NPC_class==CLASS_BOBAFETT);//self->NPC &&
|
|
return ((qboolean)(self->client->moveType==MT_FLYSWIM));
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
bool Boba_CanSeeEnemy( gentity_t *self )
|
|
{
|
|
assert(self && self->NPC && self->client && self->client->NPC_class==CLASS_BOBAFETT);
|
|
return ((level.time - self->NPC->enemyLastSeenTime)<1000);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
void Boba_Pain( gentity_t *self, gentity_t *inflictor, int damage, int mod)
|
|
{
|
|
if (mod==MOD_SABER && !(NPCInfo->aiFlags&NPCAI_FLAMETHROW))
|
|
{
|
|
TIMER_Set( self, "Boba_TacticsSelect", 0); // Hurt By The Saber, Time To Try Something New
|
|
}
|
|
if (self->NPC->aiFlags&NPCAI_FLAMETHROW)
|
|
{
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCELIGHTNING_HOLD, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
self->client->ps.torsoAnimTimer = level.time - TIMER_Get(self, "falmeTime");
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
void Boba_FlyStart( gentity_t *self )
|
|
{//switch to seeker AI for a while
|
|
if ( TIMER_Done( self, "jetRecharge" )
|
|
&& !Boba_Flying( self ) )
|
|
{
|
|
self->client->ps.gravity = 0;
|
|
self->svFlags |= SVF_CUSTOM_GRAVITY;
|
|
self->client->moveType = MT_FLYSWIM;
|
|
//start jet effect
|
|
self->client->jetPackTime = level.time + Q_irand( 3000, 10000 );
|
|
if ( self->genericBolt1 != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "boba/jetSP" ), self->playerModel, self->genericBolt1, self->s.number, self->currentOrigin, qtrue, qtrue );
|
|
}
|
|
if ( self->genericBolt2 != -1 )
|
|
{
|
|
G_PlayEffect( G_EffectIndex( "boba/jetSP" ), self->playerModel, self->genericBolt2, self->s.number, self->currentOrigin, qtrue, qtrue );
|
|
}
|
|
|
|
//take-off sound
|
|
G_SoundOnEnt( self, CHAN_ITEM, "sound/chars/boba/bf_blast-off.wav" );
|
|
//jet loop sound
|
|
self->s.loopSound = G_SoundIndex( "sound/chars/boba/bf_jetpack_lp.wav" );
|
|
if ( self->NPC )
|
|
{
|
|
self->count = Q3_INFINITE; // SEEKER shot ammo count
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
void Boba_FlyStop( gentity_t *self )
|
|
{
|
|
self->client->ps.gravity = g_gravity->value;
|
|
self->svFlags &= ~SVF_CUSTOM_GRAVITY;
|
|
self->client->moveType = MT_RUNJUMP;
|
|
//Stop effect
|
|
self->client->jetPackTime = 0;
|
|
if ( self->genericBolt1 != -1 )
|
|
{
|
|
G_StopEffect( "boba/jetSP", self->playerModel, self->genericBolt1, self->s.number );
|
|
}
|
|
if ( self->genericBolt2 != -1 )
|
|
{
|
|
G_StopEffect( "boba/jetSP", self->playerModel, self->genericBolt2, self->s.number );
|
|
}
|
|
|
|
//stop jet loop sound
|
|
G_SoundOnEnt( self, CHAN_ITEM, "sound/chars/boba/bf_land.wav" );
|
|
|
|
self->s.loopSound = 0;
|
|
if ( self->NPC )
|
|
{
|
|
self->count = 0; // SEEKER shot ammo count
|
|
TIMER_Set( self, "jetRecharge", Q_irand( 1000, 5000 ) );
|
|
TIMER_Set( self, "jumpChaseDebounce", Q_irand( 500, 2000 ) );
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
// This func actually does the damage inflicting traces
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
void Boba_FireFlameThrower( gentity_t *self )
|
|
{
|
|
trace_t tr;
|
|
vec3_t start, end, dir;
|
|
CVec3 traceMins(self->mins);
|
|
CVec3 traceMaxs(self->maxs);
|
|
gentity_t* traceEnt = NULL;
|
|
int damage = Q_irand( BOBA_FLAMETHROWDAMAGEMIN, BOBA_FLAMETHROWDAMAGEMAX );
|
|
|
|
AngleVectors(self->currentAngles, dir, 0, 0);
|
|
dir[2] = 0.0f;
|
|
VectorCopy(self->currentOrigin, start);
|
|
traceMins *= 0.5f;
|
|
traceMaxs *= 0.5f;
|
|
start[2] += 40.0f;
|
|
|
|
VectorMA( start, 150.0f, dir, end );
|
|
|
|
if (g_bobaDebug->integer)
|
|
{
|
|
CG_DrawEdge(start, end, EDGE_IMPACT_POSSIBLE);
|
|
}
|
|
gi.trace( &tr, start, self->mins, self->maxs, end, self->s.number, MASK_SHOT, (EG2_Collision)0, 0);
|
|
|
|
traceEnt = &g_entities[tr.entityNum];
|
|
if ( tr.entityNum < ENTITYNUM_WORLD && traceEnt->takedamage )
|
|
{
|
|
G_Damage( traceEnt, self, self, dir, tr.endpos, damage, DAMAGE_NO_ARMOR|DAMAGE_NO_KNOCKBACK|DAMAGE_NO_HIT_LOC|DAMAGE_IGNORE_TEAM, MOD_LAVA, HL_NONE );
|
|
if (traceEnt->health>0)
|
|
{
|
|
// G_Knockdown( traceEnt, self, dir, Q_irand(200, 330), qfalse);
|
|
G_Throw(traceEnt, dir, 30);
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
void Boba_StopFlameThrower( gentity_t *self )
|
|
{
|
|
if ( self->s.number < MAX_CLIENTS )
|
|
{
|
|
self->client->ps.torsoAnimTimer = 0;
|
|
G_StopEffect( G_EffectIndex("boba/fthrw"), self->playerModel, self->genericBolt3, self->s.number);
|
|
return;
|
|
}
|
|
if ((NPCInfo->aiFlags&NPCAI_FLAMETHROW))
|
|
{
|
|
self->NPC->aiFlags &= ~NPCAI_FLAMETHROW;
|
|
self->client->ps.torsoAnimTimer = 0;
|
|
|
|
TIMER_Set( self, "flameTime", 0);
|
|
TIMER_Set( self, "nextAttackDelay", 0);
|
|
TIMER_Set( self, "Boba_TacticsSelect", 0);
|
|
|
|
// G_SoundOnEnt( self, CHAN_WEAPON, "sound/effects/flameoff.mp3" );
|
|
G_StopEffect( G_EffectIndex("boba/fthrw"), self->playerModel, self->genericBolt3, self->s.number);
|
|
|
|
Boba_Printf("FlameThrower OFF");
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
void Boba_StartFlameThrower( gentity_t *self )
|
|
{
|
|
if (!(NPCInfo->aiFlags&NPCAI_FLAMETHROW))
|
|
{
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCELIGHTNING_HOLD, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
|
|
self->NPC->aiFlags |= NPCAI_FLAMETHROW;
|
|
self->client->ps.torsoAnimTimer = BOBA_FLAMEDURATION;
|
|
|
|
TIMER_Set( self, "flameTime", BOBA_FLAMEDURATION);
|
|
TIMER_Set( self, "nextAttackDelay", BOBA_FLAMEDURATION);
|
|
TIMER_Set( self, "nextFlameDelay", BOBA_FLAMEDURATION*2);
|
|
TIMER_Set( self, "Boba_TacticsSelect", BOBA_FLAMEDURATION);
|
|
|
|
G_SoundOnEnt( self, CHAN_WEAPON, "sound/weapons/boba/bf_flame.mp3" );
|
|
G_PlayEffect( G_EffectIndex("boba/fthrw"), self->playerModel, self->genericBolt3, self->s.number, self->s.origin, 1 );
|
|
|
|
Boba_Printf("FlameThrower ON");
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
void Boba_DoFlameThrower( gentity_t *self )
|
|
{
|
|
if ( self->s.number < MAX_CLIENTS )
|
|
{
|
|
if ( self->client )
|
|
{
|
|
if ( !self->client->ps.forcePowerDuration[FP_LIGHTNING] )
|
|
{
|
|
NPC_SetAnim( self, SETANIM_TORSO, BOTH_FORCELIGHTNING_HOLD, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD );
|
|
self->client->ps.torsoAnimTimer = BOBA_FLAMEDURATION;
|
|
G_SoundOnEnt( self, CHAN_WEAPON, "sound/weapons/boba/bf_flame.mp3" );
|
|
G_PlayEffect( G_EffectIndex("boba/fthrw"), self->playerModel, self->genericBolt3, self->s.number, self->s.origin, 1 );
|
|
self->client->ps.forcePowerDuration[FP_LIGHTNING] = 1;
|
|
}
|
|
Boba_FireFlameThrower( self );
|
|
}
|
|
return;
|
|
}
|
|
if (!(NPCInfo->aiFlags&NPCAI_FLAMETHROW) && TIMER_Done(self, "nextAttackDelay"))
|
|
{
|
|
Boba_StartFlameThrower( self );
|
|
}
|
|
|
|
if ( (NPCInfo->aiFlags&NPCAI_FLAMETHROW))
|
|
{
|
|
Boba_FireFlameThrower( self );
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
void Boba_DoAmbushWait( gentity_t *self)
|
|
{
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
void Boba_DoSniper( gentity_t *self)
|
|
{
|
|
if (TIMER_Done(NPC, "PickNewSniperPoint"))
|
|
{
|
|
TIMER_Set(NPC, "PickNewSniperPoint", Q_irand(15000, 25000));
|
|
int SniperPoint = NPC_FindCombatPoint(NPC->currentOrigin, 0, NPC->currentOrigin, CP_SNIPE|CP_CLEAR|CP_HAS_ROUTE|CP_TRYFAR|CP_HORZ_DIST_COLL, 0, -1);
|
|
if (SniperPoint!=-1)
|
|
{
|
|
NPC_SetCombatPoint(SniperPoint);
|
|
NPC_SetMoveGoal( NPC, level.combatPoints[SniperPoint].origin, 20, qtrue, SniperPoint );
|
|
}
|
|
}
|
|
|
|
if (Distance(NPC->currentOrigin, level.combatPoints[NPCInfo->combatPoint].origin)<50.0f)
|
|
{
|
|
Boba_FireDecide();
|
|
}
|
|
|
|
|
|
bool IsOnAPath = !!NPC_MoveToGoal(qtrue);
|
|
|
|
// Resolve Blocked Problems
|
|
//--------------------------
|
|
if (NPCInfo->aiFlags&NPCAI_BLOCKED &&
|
|
NPC->client->moveType!=MT_FLYSWIM &&
|
|
((level.time - NPCInfo->blockedDebounceTime)>3000)
|
|
)
|
|
{
|
|
Boba_Printf("BLOCKED: Attempting Jump");
|
|
if (IsOnAPath)
|
|
{
|
|
if (!NPC_TryJump(NPCInfo->blockedTargetPosition))
|
|
{
|
|
Boba_Printf(" Failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
NPC_FaceEnemy(qtrue);
|
|
NPC_UpdateAngles( qtrue, qtrue );
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
// Call This function to make Boba actually shoot his current weapon
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
void Boba_Fire()
|
|
{
|
|
WeaponThink(qtrue);
|
|
|
|
// If Actually Fired, Decide To Apply Alt Fire And Calc Next Attack Delay
|
|
//------------------------------------------------------------------------
|
|
if (ucmd.buttons&BUTTON_ATTACK)
|
|
{
|
|
switch (NPC->s.weapon)
|
|
{
|
|
case WP_ROCKET_LAUNCHER:
|
|
TIMER_Set( NPC, "nextAttackDelay", Q_irand(1000, 2000));
|
|
|
|
// Occasionally Shoot A Homing Missile
|
|
//-------------------------------------
|
|
if (!Q_irand(0,3))
|
|
{
|
|
ucmd.buttons &= ~BUTTON_ATTACK;
|
|
ucmd.buttons |= BUTTON_ALT_ATTACK;
|
|
NPC->client->fireDelay = Q_irand( 1000, 3000 );
|
|
}
|
|
break;
|
|
|
|
case WP_DISRUPTOR:
|
|
TIMER_Set(NPC, "nextAttackDelay", Q_irand(1000, 4000));
|
|
|
|
// Occasionally Alt-Fire
|
|
//-----------------------
|
|
if (!Q_irand(0,3))
|
|
{
|
|
ucmd.buttons &= ~BUTTON_ATTACK;
|
|
ucmd.buttons |= BUTTON_ALT_ATTACK;
|
|
NPC->client->fireDelay = Q_irand( 1000, 3000 );
|
|
}
|
|
break;
|
|
|
|
case WP_BLASTER:
|
|
|
|
if (TIMER_Done(NPC, "nextBlasterAltFireDecide"))
|
|
{
|
|
if (Q_irand(0, (NPC->count*2)+3)>2)
|
|
{
|
|
TIMER_Set(NPC, "nextBlasterAltFireDecide", Q_irand(3000, 8000));
|
|
if (!(NPCInfo->scriptFlags&SCF_ALT_FIRE))
|
|
{
|
|
Boba_Printf("ALT FIRE On");
|
|
NPCInfo->scriptFlags |= SCF_ALT_FIRE;
|
|
NPC_ChangeWeapon(WP_BLASTER); // Update Delay Timers
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TIMER_Set(NPC, "nextBlasterAltFireDecide", Q_irand(2000, 5000));
|
|
if ( (NPCInfo->scriptFlags&SCF_ALT_FIRE))
|
|
{
|
|
Boba_Printf("ALT FIRE Off");
|
|
NPCInfo->scriptFlags &=~SCF_ALT_FIRE;
|
|
NPC_ChangeWeapon(WP_BLASTER); // Update Delay Timers
|
|
}
|
|
}
|
|
}
|
|
|
|
// Occasionally Alt Fire
|
|
//-----------------------
|
|
if (NPCInfo->scriptFlags&SCF_ALT_FIRE)
|
|
{
|
|
ucmd.buttons &= ~BUTTON_ATTACK;
|
|
ucmd.buttons |= BUTTON_ALT_ATTACK;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
// Call this function to see if Fett should fire his current weapon
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
void Boba_FireDecide( void )
|
|
{
|
|
// Any Reason Not To Shoot?
|
|
//--------------------------
|
|
if (!NPC || // Only NPCs
|
|
!NPC->client || // Only Clients
|
|
NPC->client->NPC_class!=CLASS_BOBAFETT || // Only Boba
|
|
!NPC->enemy || // Only If There Is An Enemy
|
|
NPC->s.weapon==WP_NONE || // Only If Using A Valid Weapon
|
|
!TIMER_Done(NPC, "nextAttackDelay") || // Only If Ready To Shoot Again
|
|
!Boba_CanSeeEnemy(NPC) // Only If Enemy Recently Seen
|
|
)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Now Check Weapon Specific Parameters To See If We Should Shoot Or Not
|
|
//-----------------------------------------------------------------------
|
|
switch (NPC->s.weapon)
|
|
{
|
|
case WP_ROCKET_LAUNCHER:
|
|
if (Distance(NPC->currentOrigin, NPC->enemy->currentOrigin)>400.0f)
|
|
{
|
|
Boba_Fire();
|
|
}
|
|
break;
|
|
|
|
case WP_DISRUPTOR:
|
|
// TODO: Add Conditions Here
|
|
Boba_Fire();
|
|
break;
|
|
|
|
case WP_BLASTER:
|
|
// TODO: Add Conditions Here
|
|
Boba_Fire();
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
// Tactics avaliable to Boba Fett:
|
|
// --------------------------------
|
|
// BTS_RIFLE, // Uses Jedi / Seeker Movement
|
|
// BTS_MISSILE, // Uses Jedi / Seeker Movement
|
|
// BTS_SNIPER, // Uses Special Movement Internal To This File
|
|
// BTS_FLAMETHROW, // Locked In Place
|
|
// BTS_AMBUSHWAIT, // Goto CP & Wait
|
|
//
|
|
//
|
|
// Weapons available to Boba Fett:
|
|
// --------------------------------
|
|
// WP_NONE (Flame Thrower)
|
|
// WP_ROCKET_LAUNCHER
|
|
// WP_BLASTER
|
|
// WP_DISRUPTOR
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
void Boba_TacticsSelect()
|
|
{
|
|
// Don't Change Tactics For A Little While
|
|
//------------------------------------------
|
|
TIMER_Set(NPC, "Boba_TacticsSelect", Q_irand(8000, 15000));
|
|
int nextState = NPCInfo->localState;
|
|
|
|
|
|
// Get Some Data That Will Help With The Selection Of The Next Tactic
|
|
//--------------------------------------------------------------------
|
|
bool enemyAlive = (NPC->enemy->health>0);
|
|
float enemyDistance = Distance(NPC->currentOrigin, NPC->enemy->currentOrigin);
|
|
bool enemyInFlameRange = (enemyDistance<BOBA_FLAMETHROWRANGE);
|
|
bool enemyInRocketRange = (enemyDistance>BOBA_ROCKETRANGEMIN && enemyDistance<BOBA_ROCKETRANGEMAX);
|
|
bool enemyRecentlySeen = Boba_CanSeeEnemy(NPC);
|
|
|
|
|
|
// Enemy Is Really Close
|
|
//-----------------------
|
|
if (!enemyAlive)
|
|
{
|
|
nextState = BTS_RIFLE;
|
|
}
|
|
else if (enemyInFlameRange)
|
|
{
|
|
// If It's Been Long Enough Since Our Last Flame Blast, Try To Torch The Enemy
|
|
//-----------------------------------------------------------------------------
|
|
if (TIMER_Done(NPC, "nextFlameDelay"))
|
|
{
|
|
nextState = BTS_FLAMETHROW;
|
|
}
|
|
|
|
// Otherwise, He's Probably Too Close, So Try To Get Clear Of Him
|
|
//----------------------------------------------------------------
|
|
else
|
|
{
|
|
nextState = BTS_RIFLE;
|
|
}
|
|
}
|
|
|
|
// Recently Saw The Enemy, Time For Some Good Ole Fighten!
|
|
//---------------------------------------------------------
|
|
else if (enemyRecentlySeen)
|
|
{
|
|
// At First, Boba will prefer to use his blaster against the player, but
|
|
// the more times he is driven away (NPC->count), he will be less likely to
|
|
// choose the blaster, and more likely to go for the missile launcher
|
|
nextState = (!enemyInRocketRange || Q_irand(0, NPC->count)<1)?(BTS_RIFLE):(BTS_MISSILE);
|
|
}
|
|
|
|
// Hmmm... Havn't Seen The Player In A While, We Might Want To Try Something Sneaky
|
|
//-----------------------------------------------------------------------------------
|
|
else
|
|
{
|
|
bool SnipePointsNear = false; // TODO
|
|
bool AmbushPointNear = false; // TODO
|
|
|
|
if (Q_irand(0, NPC->count)>0)
|
|
{
|
|
int SniperPoint = NPC_FindCombatPoint(NPC->currentOrigin, 0, NPC->currentOrigin, CP_SNIPE|CP_CLEAR|CP_HAS_ROUTE|CP_TRYFAR|CP_HORZ_DIST_COLL, 0, -1);
|
|
if (SniperPoint!=-1)
|
|
{
|
|
NPC_SetCombatPoint(SniperPoint);
|
|
NPC_SetMoveGoal( NPC, level.combatPoints[SniperPoint].origin, 20, qtrue, SniperPoint );
|
|
TIMER_Set(NPC, "PickNewSniperPoint", Q_irand(15000, 25000));
|
|
SnipePointsNear = true;
|
|
}
|
|
}
|
|
|
|
|
|
if (SnipePointsNear && TIMER_Done(NPC, "Boba_NoSniperTime"))
|
|
{
|
|
TIMER_Set(NPC, "Boba_NoSniperTime", 120000); // Don't snipe again for a while
|
|
TIMER_Set(NPC, "Boba_TacticsSelect", Q_irand(35000, 45000));// More patience here
|
|
nextState = BTS_SNIPER;
|
|
}
|
|
else if (AmbushPointNear)
|
|
{
|
|
TIMER_Set(NPC, "Boba_TacticsSelect", Q_irand(15000, 25000));// More patience here
|
|
nextState = BTS_AMBUSHWAIT;
|
|
}
|
|
else
|
|
{
|
|
nextState = (!enemyInRocketRange || Q_irand(0, NPC->count)<1)?(BTS_RIFLE):(BTS_MISSILE);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// The Next State Has Been Selected, Now Change Weapon If Necessary
|
|
//------------------------------------------------------------------
|
|
if (nextState!=NPCInfo->localState)
|
|
{
|
|
NPCInfo->localState = nextState;
|
|
switch (NPCInfo->localState)
|
|
{
|
|
case BTS_FLAMETHROW:
|
|
Boba_Printf("NEW TACTIC: Flame Thrower");
|
|
Boba_ChangeWeapon(WP_NONE);
|
|
Boba_DoFlameThrower(NPC);
|
|
break;
|
|
|
|
case BTS_RIFLE:
|
|
Boba_Printf("NEW TACTIC: Rifle");
|
|
Boba_ChangeWeapon(WP_BLASTER);
|
|
break;
|
|
|
|
case BTS_MISSILE:
|
|
Boba_Printf("NEW TACTIC: Rocket Launcher");
|
|
Boba_ChangeWeapon(WP_ROCKET_LAUNCHER);
|
|
break;
|
|
|
|
case BTS_SNIPER:
|
|
Boba_Printf("NEW TACTIC: Sniper");
|
|
Boba_ChangeWeapon(WP_DISRUPTOR);
|
|
break;
|
|
|
|
case BTS_AMBUSHWAIT:
|
|
Boba_Printf("NEW TACTIC: Ambush");
|
|
Boba_ChangeWeapon(WP_NONE);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
// Tactics
|
|
//
|
|
// This function is called right after Update()
|
|
// If returns true, Jedi and Seeker AI not used for movement
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
bool Boba_Tactics()
|
|
{
|
|
if (!NPC->enemy)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Think About Changing Tactics
|
|
//------------------------------
|
|
if (TIMER_Done(NPC, "Boba_TacticsSelect"))
|
|
{
|
|
Boba_TacticsSelect();
|
|
}
|
|
|
|
// These Tactics Require Seeker & Jedi Movement
|
|
//----------------------------------------------
|
|
if (!NPCInfo->localState ||
|
|
NPCInfo->localState==BTS_RIFLE ||
|
|
NPCInfo->localState==BTS_MISSILE)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Flame Thrower - Locked In Place
|
|
//---------------------------------
|
|
if (NPCInfo->localState==BTS_FLAMETHROW)
|
|
{
|
|
Boba_DoFlameThrower( NPC );
|
|
}
|
|
|
|
// Sniper - Move Around, And Take Shots
|
|
//--------------------------------------
|
|
else if (NPCInfo->localState==BTS_SNIPER)
|
|
{
|
|
Boba_DoSniper( NPC );
|
|
}
|
|
|
|
// Ambush Wait
|
|
//------------
|
|
else if (NPCInfo->localState==BTS_AMBUSHWAIT)
|
|
{
|
|
Boba_DoAmbushWait( NPC );
|
|
}
|
|
|
|
|
|
NPC_FacePosition( NPC->enemy->currentOrigin, qtrue);
|
|
NPC_UpdateAngles(qtrue, qtrue);
|
|
|
|
return true; // Do Not Use Normal Jedi Or Seeker Movement
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
bool Boba_Respawn()
|
|
{
|
|
int cp = -1;
|
|
|
|
// Try To Predict Where The Enemy Is Going
|
|
//-----------------------------------------
|
|
if (AverageEnemyDirectionSamples && NPC->behaviorSet[BSET_DEATH]==0)
|
|
{
|
|
vec3_t endPos;
|
|
VectorMA(NPC->enemy->currentOrigin, 1000.0f / (float)AverageEnemyDirectionSamples, AverageEnemyDirection, endPos);
|
|
cp = NPC_FindCombatPoint(endPos, 0, endPos, CP_FLEE|CP_TRYFAR|CP_HORZ_DIST_COLL, 0, -1);
|
|
Boba_Printf("Attempting Predictive Spawn Point");
|
|
}
|
|
|
|
// If That Failed, Try To Go Directly To The Enemy
|
|
//-------------------------------------------------
|
|
if (cp==-1)
|
|
{
|
|
cp = NPC_FindCombatPoint(NPC->enemy->currentOrigin, 0, NPC->enemy->currentOrigin, CP_FLEE|CP_TRYFAR|CP_HORZ_DIST_COLL, 0, -1);
|
|
Boba_Printf("Attempting Closest Current Spawn Point");
|
|
}
|
|
|
|
// If We've Found One, Go There
|
|
//------------------------------
|
|
if (cp!=-1)
|
|
{
|
|
NPC_SetCombatPoint( cp );
|
|
NPCInfo->surrenderTime = 0;
|
|
NPC->health = NPC->max_health;
|
|
NPC->svFlags &=~SVF_NOCLIENT;
|
|
NPC->count ++; // This is the number of times spawned
|
|
G_SetOrigin(NPC, level.combatPoints[cp].origin);
|
|
|
|
AverageEnemyDirectionSamples = 0;
|
|
VectorClear(AverageEnemyDirection);
|
|
|
|
Boba_Printf("Found Spawn Point (%d)", cp);
|
|
return true;
|
|
}
|
|
|
|
assert(0); // Yea, that's bad...
|
|
Boba_Printf("FAILED TO FIND SPAWN POINT");
|
|
return false;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
void Boba_Update()
|
|
{
|
|
// Never Forget The Player... Never.
|
|
//-----------------------------------
|
|
if (player && player->inuse && !NPC->enemy)
|
|
{
|
|
G_SetEnemy(NPC, player);
|
|
NPC->svFlags |= SVF_LOCKEDENEMY; // Don't forget about the enemy once you've found him
|
|
}
|
|
|
|
// Hey, This Is Boba, He Tests The Trace All The Time
|
|
//----------------------------------------------------
|
|
if (NPC->enemy)
|
|
{
|
|
if (!(NPC->svFlags&SVF_NOCLIENT))
|
|
{
|
|
trace_t testTrace;
|
|
vec3_t eyes;
|
|
CalcEntitySpot( NPC, SPOT_HEAD_LEAN, eyes );
|
|
gi.trace (&testTrace, eyes, NULL, NULL, NPC->enemy->currentOrigin, NPC->s.number, MASK_SHOT, (EG2_Collision)0, 0);
|
|
|
|
bool wasSeen = Boba_CanSeeEnemy(NPC);
|
|
|
|
if (!testTrace.startsolid &&
|
|
!testTrace.allsolid &&
|
|
testTrace.entityNum == NPC->enemy->s.number)
|
|
{
|
|
NPCInfo->enemyLastSeenTime = level.time;
|
|
NPCInfo->enemyLastHeardTime = level.time;
|
|
VectorCopy(NPC->enemy->currentOrigin, NPCInfo->enemyLastSeenLocation);
|
|
VectorCopy(NPC->enemy->currentOrigin, NPCInfo->enemyLastHeardLocation);
|
|
}
|
|
else if (gi.inPVS( NPC->enemy->currentOrigin, NPC->currentOrigin))
|
|
{
|
|
NPCInfo->enemyLastHeardTime = level.time;
|
|
VectorCopy(NPC->enemy->currentOrigin, NPCInfo->enemyLastHeardLocation);
|
|
}
|
|
|
|
if (g_bobaDebug->integer)
|
|
{
|
|
bool nowSeen = Boba_CanSeeEnemy(NPC);
|
|
if (!wasSeen && nowSeen)
|
|
{
|
|
Boba_Printf("Enemy Seen");
|
|
}
|
|
if (wasSeen && !nowSeen)
|
|
{
|
|
Boba_Printf("Enemy Lost");
|
|
}
|
|
CG_DrawEdge(NPC->currentOrigin, NPC->enemy->currentOrigin, (nowSeen)?(EDGE_IMPACT_SAFE):(EDGE_IMPACT_POSSIBLE));
|
|
}
|
|
}
|
|
|
|
if (!NPCInfo->surrenderTime)
|
|
{
|
|
if ((level.time - NPCInfo->enemyLastSeenTime)>20000 && TIMER_Done(NPC, "TooLongGoneRespawn"))
|
|
{
|
|
TIMER_Set(NPC, "TooLongGoneRespawn", 30000); // Give him some time to get to you before trying again
|
|
Boba_Printf("Gone Too Long, Attempting Respawn Even Though Not Hiding");
|
|
Boba_Respawn();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Make Sure He Always Appears In The Last Area With Full Health When His Death Script Is Turned On
|
|
//--------------------------------------------------------------------------------------------------
|
|
if (!BobaHadDeathScript && NPC->behaviorSet[BSET_DEATH]!=0)
|
|
{
|
|
if (!gi.inPVS(NPC->enemy->currentOrigin, NPC->currentOrigin))
|
|
{
|
|
Boba_Printf("Attempting Final Battle Spawn...");
|
|
if (Boba_Respawn())
|
|
{
|
|
BobaHadDeathScript = true;
|
|
}
|
|
else
|
|
{
|
|
Boba_Printf("Failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Don't Forget To Turn Off That Flame Thrower, Mr. Fett - You're Waisting Precious Natural Gases
|
|
//------------------------------------------------------------------------------------------------
|
|
if ((NPCInfo->aiFlags&NPCAI_FLAMETHROW) && (TIMER_Done(NPC, "flameTime")))
|
|
{
|
|
Boba_StopFlameThrower(NPC);
|
|
}
|
|
|
|
|
|
// Occasionally A Jump Turns Into A Rocket Fly
|
|
//---------------------------------------------
|
|
if ( NPC->client->ps.groundEntityNum == ENTITYNUM_NONE
|
|
&& NPC->client->ps.forceJumpZStart
|
|
&& !Q_irand( 0, 10 ) )
|
|
{//take off
|
|
Boba_FlyStart( NPC );
|
|
}
|
|
|
|
|
|
// If Hurting, Try To Run Away
|
|
//-----------------------------
|
|
if (!NPCInfo->surrenderTime && (NPC->health<NPC->max_health/10))
|
|
{
|
|
Boba_Printf("Time To Surrender, Searching For Flee Point");
|
|
|
|
|
|
// Find The Closest Flee Point That I Can Get To
|
|
//-----------------------------------------------
|
|
int cp = NPC_FindCombatPoint(NPC->currentOrigin, 0, NPC->currentOrigin, CP_FLEE|CP_HAS_ROUTE|CP_TRYFAR|CP_HORZ_DIST_COLL, 0, -1);
|
|
if (cp!=-1)
|
|
{
|
|
NPC_SetCombatPoint( cp );
|
|
NPC_SetMoveGoal( NPC, level.combatPoints[cp].origin, 8, qtrue, cp );
|
|
if (NPC->count<6)
|
|
{
|
|
NPCInfo->surrenderTime = level.time + Q_irand(5000, 10000) + 1000*(6-NPC->count);
|
|
}
|
|
else
|
|
{
|
|
NPCInfo->surrenderTime = level.time + Q_irand(5000, 10000);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Boba_Printf(" Failure");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
bool Boba_Flee()
|
|
{
|
|
bool EnemyRecentlySeen = ((level.time - NPCInfo->enemyLastSeenTime)<10000);
|
|
bool ReachedEscapePoint = (Distance(level.combatPoints[NPCInfo->combatPoint].origin, NPC->currentOrigin)<50.0f);
|
|
bool HasBeenGoneEnough = (level.time>NPCInfo->surrenderTime || (level.time - NPCInfo->enemyLastSeenTime)>400000);
|
|
|
|
|
|
// Is It Time To Come Back For Some More?
|
|
//----------------------------------------
|
|
if (!EnemyRecentlySeen || ReachedEscapePoint)
|
|
{
|
|
NPC->svFlags |= SVF_NOCLIENT;
|
|
if (HasBeenGoneEnough)
|
|
{
|
|
if ((level.time - NPCInfo->enemyLastSeenTime)>400000)
|
|
{
|
|
Boba_Printf(" Gone Too Long, Attempting Respawn");
|
|
}
|
|
|
|
if (Boba_Respawn())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
else if (ReachedEscapePoint && (NPCInfo->surrenderTime - level.time)>3000)
|
|
{
|
|
if (TIMER_Done(NPC, "SpookPlayerTimer"))
|
|
{
|
|
vec3_t testDirection;
|
|
TIMER_Set(NPC, "SpookPlayerTimer", Q_irand(2000, 10000));
|
|
switch(Q_irand(0, 1))
|
|
{
|
|
case 0:
|
|
Boba_Printf("SPOOK: Dust");
|
|
Boba_DustFallNear(NPC->enemy->currentOrigin, Q_irand(1,2));
|
|
break;
|
|
|
|
case 1:
|
|
Boba_Printf("SPOOK: Footsteps");
|
|
testDirection[0] = (Q_flrand(0.0f, 1.0f) * 0.5f) - 1.0f;
|
|
testDirection[0] += (testDirection[0]>0.0f)?(0.5f):(-0.5f);
|
|
testDirection[1] = (Q_flrand(0.0f, 1.0f) * 0.5f) - 1.0f;
|
|
testDirection[1] += (testDirection[1]>0.0f)?(0.5f):(-0.5f);
|
|
testDirection[2] = 1.0f;
|
|
VectorMA(NPC->enemy->currentOrigin, 400.0f, testDirection, BobaFootStepLoc);
|
|
|
|
BobaFootStepCount = Q_irand(3,8);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (BobaFootStepCount && TIMER_Done(NPC, "BobaFootStepFakeTimer"))
|
|
{
|
|
TIMER_Set(NPC, "BobaFootStepFakeTimer", Q_irand(300, 800));
|
|
BobaFootStepCount --;
|
|
G_SoundAtSpot(BobaFootStepLoc, G_SoundIndex(va("sound/player/footsteps/boot%d", Q_irand(1,4))), qtrue);
|
|
}
|
|
|
|
if (TIMER_Done(NPC, "ResampleEnemyDirection") && NPC->enemy->resultspeed>10.0f)
|
|
{
|
|
TIMER_Set(NPC, "ResampleEnemyDirection", Q_irand(500, 1000));
|
|
AverageEnemyDirectionSamples ++;
|
|
|
|
vec3_t moveDir;
|
|
VectorCopy(NPC->enemy->client->ps.velocity, moveDir);
|
|
VectorNormalize(moveDir);
|
|
|
|
VectorAdd(AverageEnemyDirection, moveDir, AverageEnemyDirection);
|
|
}
|
|
|
|
if (g_bobaDebug->integer && AverageEnemyDirectionSamples)
|
|
{
|
|
vec3_t endPos;
|
|
VectorMA(NPC->enemy->currentOrigin, 500.0f / (float)AverageEnemyDirectionSamples, AverageEnemyDirection, endPos);
|
|
CG_DrawEdge(NPC->enemy->currentOrigin, endPos, EDGE_IMPACT_POSSIBLE);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NPCInfo->surrenderTime += 100;
|
|
}
|
|
|
|
// Finish The Flame Thrower First...
|
|
//-----------------------------------
|
|
if (NPCInfo->aiFlags&NPCAI_FLAMETHROW)
|
|
{
|
|
Boba_DoFlameThrower( NPC );
|
|
NPC_FacePosition( NPC->enemy->currentOrigin, qtrue);
|
|
NPC_UpdateAngles(qtrue, qtrue);
|
|
return true;
|
|
}
|
|
|
|
bool IsOnAPath = !!NPC_MoveToGoal(qtrue);
|
|
if (!ReachedEscapePoint &&
|
|
NPCInfo->aiFlags&NPCAI_BLOCKED &&
|
|
NPC->client->moveType!=MT_FLYSWIM &&
|
|
((level.time - NPCInfo->blockedDebounceTime)>1000)
|
|
)
|
|
{
|
|
if (!Boba_CanSeeEnemy(NPC) && Distance(NPC->currentOrigin, level.combatPoints[NPCInfo->combatPoint].origin)<200)
|
|
{
|
|
Boba_Printf("BLOCKED: Just Teleporting There");
|
|
G_SetOrigin(NPC, level.combatPoints[NPCInfo->combatPoint].origin);
|
|
}
|
|
else
|
|
{
|
|
Boba_Printf("BLOCKED: Attempting Jump");
|
|
|
|
if (IsOnAPath)
|
|
{
|
|
if (NPC_TryJump(NPCInfo->blockedTargetPosition))
|
|
{
|
|
}
|
|
else
|
|
{
|
|
Boba_Printf(" Failed");
|
|
}
|
|
}
|
|
else if (EnemyRecentlySeen)
|
|
{
|
|
if (NPC_TryJump(NPCInfo->enemyLastSeenLocation))
|
|
{
|
|
}
|
|
else
|
|
{
|
|
Boba_Printf(" Failed");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
NPC_UpdateAngles( qtrue, qtrue );
|
|
return true;
|
|
} |