1855 lines
44 KiB
C++
1855 lines
44 KiB
C++
//NPC_combat.cpp
|
|
#include "b_local.h"
|
|
|
|
extern void G_AddVoiceEvent( gentity_t *self, int event, int speakDebounceTime );
|
|
extern void G_SetEnemy( gentity_t *self, gentity_t *enemy );
|
|
extern qboolean NPC_CheckDisguise( gentity_t *ent );
|
|
extern qboolean NPC_CheckLookTarget( gentity_t *self );
|
|
extern void NPC_ClearLookTarget( gentity_t *self );
|
|
|
|
void G_ClearEnemy (gentity_t *self)
|
|
{
|
|
NPC_CheckLookTarget( self );
|
|
|
|
if ( self->enemy )
|
|
{
|
|
if( self->client && self->client->renderInfo.lookTarget == self->enemy->s.number )
|
|
{
|
|
NPC_ClearLookTarget( self );
|
|
}
|
|
|
|
if ( self->NPC && self->enemy == self->NPC->goalEntity )
|
|
{
|
|
self->NPC->goalEntity = NULL;
|
|
}
|
|
//FIXME: set last enemy?
|
|
}
|
|
|
|
self->enemy = NULL;
|
|
}
|
|
|
|
/*
|
|
-------------------------
|
|
NPC_AngerAlert
|
|
-------------------------
|
|
*/
|
|
|
|
#define ANGER_ALERT_RADIUS 512
|
|
#define ANGER_ALERT_SOUND_RADIUS 256
|
|
|
|
static void G_AngerAlert( gentity_t *self )
|
|
{
|
|
G_AlertTeam( self, self->enemy, ANGER_ALERT_RADIUS, ANGER_ALERT_SOUND_RADIUS );
|
|
}
|
|
|
|
/*
|
|
-------------------------
|
|
G_TeamEnemy
|
|
-------------------------
|
|
*/
|
|
|
|
qboolean G_TeamEnemy( gentity_t *self )
|
|
{//FIXME: Probably a better way to do this, is a linked list of your teammates already available?
|
|
int i;
|
|
gentity_t *ent;
|
|
|
|
if ( !self->client || self->client->playerTeam == TEAM_FREE )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
for( i = 1; i < MAX_GENTITIES; i++ )
|
|
{
|
|
ent = &g_entities[i];
|
|
|
|
if ( !ent->client )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( ent->client->playerTeam != self->client->playerTeam )
|
|
{//ent is not on my team
|
|
continue;
|
|
}
|
|
|
|
if ( ent->enemy )
|
|
{//they have an enemy
|
|
if ( !ent->enemy->client || ent->enemy->client->playerTeam != self->client->playerTeam )
|
|
{//the ent's enemy is either a normal ent or is a player/NPC that is not on my team
|
|
return qtrue;
|
|
}
|
|
}
|
|
}
|
|
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
-------------------------
|
|
G_SetEnemy
|
|
-------------------------
|
|
*/
|
|
|
|
void G_SetEnemy( gentity_t *self, gentity_t *enemy )
|
|
{
|
|
int event = 0;
|
|
|
|
//Must be valid
|
|
if ( enemy == NULL )
|
|
return;
|
|
|
|
//Must be valid
|
|
if ( enemy->inuse == 0 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
//Don't take the enemy if in notarget
|
|
if ( enemy->flags & FL_NOTARGET )
|
|
return;
|
|
|
|
if ( !self->NPC )
|
|
{
|
|
self->enemy = enemy;
|
|
return;
|
|
}
|
|
|
|
if ( enemy->client && enemy->client->playerTeam == TEAM_DISGUISE )
|
|
{//unmask the player
|
|
enemy->client->playerTeam = TEAM_STARFLEET;
|
|
}
|
|
|
|
if ( self->enemy == NULL )
|
|
{
|
|
//FIXME: Have to do this to prevent alert cascading
|
|
G_ClearEnemy( self );
|
|
self->enemy = enemy;
|
|
|
|
//Special case- if player is being hunted by his own people, set their enemy team correctly
|
|
if ( self->client->playerTeam == TEAM_STARFLEET && enemy->s.number == 0 )
|
|
{
|
|
self->client->enemyTeam = TEAM_PLAYER;
|
|
}
|
|
|
|
//If have an anger script, run that instead of yelling
|
|
if( self->behaviorSet[BSET_ANGER] )
|
|
{
|
|
G_ActivateBehavior( self, BSET_ANGER );
|
|
}
|
|
else if ( self->client && enemy->client && self->client->playerTeam != enemy->client->playerTeam )
|
|
{
|
|
//FIXME: Use anger when entire team has no enemy.
|
|
// Basically, you're first one to notice enemies
|
|
if ( !G_TeamEnemy( self ) )
|
|
{//team did not have an enemy previously
|
|
event = Q_irand(EV_ANGER1, EV_ANGER3);
|
|
}
|
|
else if ( !Q_irand( 0, 2 ) )
|
|
{//Otherwise, just picking up another enemy
|
|
event = Q_irand(EV_COMBAT1, EV_COMBAT3);
|
|
}
|
|
|
|
if ( event )
|
|
{//yell
|
|
G_AddVoiceEvent( self, event, 2000 );
|
|
}
|
|
}
|
|
|
|
//Alert anyone else in the area
|
|
if ( Q_stricmp( "desperado", self->NPC_type ) != 0 && Q_stricmp( "paladin", self->NPC_type ) != 0 )
|
|
{//special holodeck enemies exception
|
|
G_AngerAlert( self );
|
|
}
|
|
return;
|
|
}
|
|
|
|
//Otherwise, just picking up another enemy
|
|
|
|
if ( Q_irand( 0, 2 ) == 0 )
|
|
{//yell something
|
|
event = Q_irand(EV_COMBAT1, EV_COMBAT3);
|
|
}
|
|
|
|
if ( event )
|
|
{
|
|
G_AddVoiceEvent( self, event, 2000 );
|
|
}
|
|
|
|
//Take the enemy
|
|
G_ClearEnemy(self);
|
|
self->enemy = enemy;
|
|
}
|
|
|
|
/*
|
|
int ChooseBestWeapon( void )
|
|
{
|
|
int n;
|
|
int weapon;
|
|
|
|
// check weapons in the NPC's weapon preference order
|
|
for ( n = 0; n < MAX_WEAPONS; n++ )
|
|
{
|
|
weapon = NPCInfo->weaponOrder[n];
|
|
|
|
if ( weapon == WP_NONE )
|
|
{
|
|
break;
|
|
}
|
|
|
|
if ( !HaveWeapon( weapon ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( client->ps.ammo[weaponData[weapon].ammoIndex] )
|
|
{
|
|
return weapon;
|
|
}
|
|
}
|
|
|
|
// check weapons serially (mainly in case a weapon is not on the NPC's list)
|
|
for ( weapon = 1; weapon < WP_NUM_WEAPONS; weapon++ )
|
|
{
|
|
if ( !HaveWeapon( weapon ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( client->ps.ammo[weaponData[weapon].ammoIndex] )
|
|
{
|
|
return weapon;
|
|
}
|
|
}
|
|
|
|
return client->ps.weapon;
|
|
}
|
|
*/
|
|
|
|
void ChangeWeapon( gentity_t *ent, int newWeapon )
|
|
{
|
|
if ( !ent || !ent->client || !ent->NPC )
|
|
{
|
|
return;
|
|
}
|
|
|
|
ent->client->ps.weapon = newWeapon;
|
|
ent->NPC->shotTime = 0;
|
|
ent->NPC->burstCount = 0;
|
|
ent->NPC->attackHold = 0;
|
|
ent->NPC->currentAmmo = ent->client->ps.ammo[weaponData[newWeapon].ammoIndex];
|
|
|
|
switch ( newWeapon )
|
|
{
|
|
case WP_IMOD://IMOD
|
|
ent->NPC->aiFlags &= ~NPCAI_BURST_WEAPON;
|
|
ent->NPC->burstSpacing = 3000;
|
|
break;
|
|
|
|
case WP_BORG_DRILL:
|
|
case WP_BORG_ASSIMILATOR://Borg
|
|
ent->NPC->aiFlags &= ~NPCAI_BURST_WEAPON;
|
|
ent->NPC->burstSpacing = 5000;
|
|
ent->NPC->attackHold = 5000;
|
|
break;
|
|
|
|
case WP_BORG_TASER://Borg
|
|
ent->NPC->aiFlags |= NPCAI_BURST_WEAPON;
|
|
ent->NPC->burstMin = 1;//0.5 sec
|
|
ent->NPC->burstMean = 4;//1 second
|
|
ent->NPC->burstMax = 7;//3 seconds
|
|
ent->NPC->burstSpacing = 2000;//2 seconds
|
|
break;
|
|
|
|
case WP_BORG_WEAPON://Borg
|
|
ent->NPC->aiFlags &= ~NPCAI_BURST_WEAPON;
|
|
//ent->NPC->aiFlags |= NPCAI_BURST_WEAPON;
|
|
//ent->NPC->burstMin = 1;//0.5 sec
|
|
//ent->NPC->burstMean = 1;//1 second
|
|
//ent->NPC->burstMax = 2;//3 seconds
|
|
ent->NPC->burstSpacing = 2000;//2 seconds
|
|
break;
|
|
|
|
case WP_COMPRESSION_RIFLE://prifle
|
|
ent->NPC->aiFlags &= ~NPCAI_BURST_WEAPON;
|
|
ent->NPC->burstSpacing = 1000;//attackdebounce
|
|
break;
|
|
|
|
case WP_IMPERIAL_BLADE:
|
|
case WP_KLINGON_BLADE:
|
|
ent->NPC->aiFlags &= ~NPCAI_BURST_WEAPON;
|
|
ent->NPC->burstSpacing = 1000;//attackdebounce
|
|
break;
|
|
|
|
case WP_PHASER:
|
|
ent->NPC->aiFlags |= NPCAI_BURST_WEAPON;
|
|
ent->NPC->burstMin = 5;//0.5 sec
|
|
ent->NPC->burstMean = 10;//1 second
|
|
ent->NPC->burstMax = 20;//3 seconds
|
|
ent->NPC->burstSpacing = 2000;//2 seconds
|
|
ent->NPC->attackHold = 1000;//Hold attack button for a 1-second burst
|
|
break;
|
|
|
|
case WP_TRICORDER:
|
|
ent->NPC->aiFlags |= NPCAI_BURST_WEAPON;
|
|
ent->NPC->burstMin = 5;
|
|
ent->NPC->burstMean = 10;
|
|
ent->NPC->burstMax = 30;
|
|
ent->NPC->burstSpacing = 1000;
|
|
break;
|
|
|
|
case WP_SCAVENGER_RIFLE://Scav weapon
|
|
case WP_CHAOTICA_GUARD_GUN:
|
|
//Check for special tutorial mode shooters
|
|
if ( ( ent->client->playerTeam == TEAM_SCAVENGERS ) && ( ent->spawnflags & 2 ) )
|
|
{
|
|
ent->NPC->aiFlags &= ~NPCAI_BURST_WEAPON;
|
|
ent->NPC->burstMin = 1;
|
|
ent->NPC->burstMean = 1;
|
|
ent->NPC->burstMax = 1;
|
|
ent->NPC->burstSpacing = 1500;//attack debounce
|
|
}
|
|
else
|
|
{
|
|
ent->NPC->aiFlags |= NPCAI_BURST_WEAPON;
|
|
ent->NPC->burstMin = 3;
|
|
ent->NPC->burstMean = 3;
|
|
ent->NPC->burstMax = 3;
|
|
|
|
if ( g_spskill->integer == 0 )
|
|
ent->NPC->burstSpacing = 1500;//attack debounce
|
|
else if ( g_spskill->integer == 1 )
|
|
ent->NPC->burstSpacing = 1000;//attack debounce
|
|
else
|
|
ent->NPC->burstSpacing = 500;//attack debounce
|
|
}
|
|
|
|
break;
|
|
|
|
case WP_STASIS:
|
|
ent->NPC->aiFlags &= ~NPCAI_BURST_WEAPON;
|
|
ent->NPC->burstSpacing = 2000;//attack debounce
|
|
break;
|
|
|
|
case WP_DESPERADO://winchester
|
|
ent->NPC->aiFlags &= ~NPCAI_BURST_WEAPON;
|
|
ent->NPC->burstSpacing = 2000;//attackdebounce
|
|
break;
|
|
|
|
case WP_PALADIN://crossbow
|
|
ent->NPC->aiFlags &= ~NPCAI_BURST_WEAPON;
|
|
ent->NPC->burstSpacing = 2000;//attackdebounce
|
|
break;
|
|
|
|
default:
|
|
ent->NPC->aiFlags &= ~NPCAI_BURST_WEAPON;
|
|
break;
|
|
}
|
|
}
|
|
|
|
void NPC_ChangeWeapon( int newWeapon )
|
|
{
|
|
ChangeWeapon( NPC, newWeapon );
|
|
}
|
|
/*
|
|
void NPC_ApplyWeaponFireDelay(void)
|
|
How long, if at all, in msec the actual fire should delay from the time the attack was started
|
|
*/
|
|
void NPC_ApplyWeaponFireDelay(void)
|
|
{
|
|
if ( NPC->attackDebounceTime > level.time )
|
|
{//Just fired, if attacking again, must be a burst fire, so don't add delay
|
|
//NOTE: Borg AI uses attackDebounceTime "incorrectly", so this will always return for them!
|
|
return;
|
|
}
|
|
|
|
switch(client->ps.weapon)
|
|
{
|
|
case WP_BORG_WEAPON:
|
|
NPCInfo->burstCount = 0;
|
|
client->fireDelay = 500;
|
|
break;
|
|
|
|
case WP_BORG_TASER:
|
|
NPCInfo->burstCount = 0;
|
|
if ( Q_stricmp( "satan", NPC->NPC_type ) != 0 )
|
|
{//no fire delay for Satan's Robot
|
|
client->fireDelay = 900;
|
|
}
|
|
break;
|
|
|
|
case WP_BORG_DRILL:
|
|
case WP_BORG_ASSIMILATOR:
|
|
NPCInfo->burstCount = 0;
|
|
client->fireDelay = 400;
|
|
break;
|
|
|
|
case WP_FORGE_PSYCH:
|
|
NPCInfo->burstCount = 0;
|
|
//TODO: Play charge up sound
|
|
client->fireDelay = 700;
|
|
break;
|
|
|
|
case WP_FORGE_PROJ:
|
|
NPCInfo->burstCount = 0;
|
|
//TODO: Play charge up sound
|
|
client->fireDelay = 500;
|
|
break;
|
|
|
|
case WP_PARASITE:
|
|
NPCInfo->burstCount = 0;
|
|
//TODO: Play charge up sound
|
|
client->fireDelay = 400;
|
|
break;
|
|
|
|
case WP_STASIS_ATTACK:
|
|
NPCInfo->burstCount = 0;
|
|
client->fireDelay = 1000;
|
|
break;
|
|
|
|
case WP_IMPERIAL_BLADE:
|
|
case WP_KLINGON_BLADE:
|
|
NPCInfo->burstCount = 0;
|
|
client->fireDelay = 600;
|
|
break;
|
|
|
|
default:
|
|
client->fireDelay = 0;
|
|
break;
|
|
}
|
|
};
|
|
|
|
/*
|
|
-------------------------
|
|
ShootThink
|
|
-------------------------
|
|
*/
|
|
void ShootThink( void )
|
|
{
|
|
int delay;
|
|
|
|
ucmd.buttons &= ~BUTTON_ATTACK;
|
|
/*
|
|
if ( enemyVisibility != VIS_SHOOT)
|
|
return;
|
|
*/
|
|
|
|
if ( client->ps.weapon == WP_NONE )
|
|
return;
|
|
|
|
if ( client->ps.weaponstate != WEAPON_READY && client->ps.weaponstate != WEAPON_FIRING && client->ps.weaponstate != WEAPON_IDLE)
|
|
return;
|
|
|
|
if ( level.time < NPCInfo->shotTime )
|
|
{
|
|
return;
|
|
}
|
|
|
|
ucmd.buttons |= BUTTON_ATTACK;
|
|
|
|
NPCInfo->currentAmmo = client->ps.ammo[weaponData[client->ps.weapon].ammoIndex]; // checkme
|
|
|
|
NPC_ApplyWeaponFireDelay();
|
|
|
|
if ( NPCInfo->aiFlags & NPCAI_BURST_WEAPON )
|
|
{
|
|
if ( !NPCInfo->burstCount )
|
|
{
|
|
NPCInfo->burstCount = Q_irand( NPCInfo->burstMin, NPCInfo->burstMax );
|
|
/*
|
|
NPCInfo->burstCount = erandom( NPCInfo->burstMean );
|
|
if ( NPCInfo->burstCount < NPCInfo->burstMin )
|
|
{
|
|
NPCInfo->burstCount = NPCInfo->burstMin;
|
|
}
|
|
else if ( NPCInfo->burstCount > NPCInfo->burstMax )
|
|
{
|
|
NPCInfo->burstCount = NPCInfo->burstMax;
|
|
}
|
|
*/
|
|
delay = 0;
|
|
}
|
|
else
|
|
{
|
|
NPCInfo->burstCount--;
|
|
if ( NPCInfo->burstCount == 0 )
|
|
{
|
|
delay = NPCInfo->burstSpacing;
|
|
}
|
|
else
|
|
{
|
|
delay = 0;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
delay = NPCInfo->burstSpacing;
|
|
}
|
|
|
|
NPCInfo->shotTime = level.time + delay;
|
|
NPC->attackDebounceTime = level.time + NPC_AttackDebounceForWeapon();
|
|
}
|
|
|
|
/*
|
|
static void WeaponThink( qboolean inCombat )
|
|
FIXME makes this so there's a delay from event that caused us to check to actually doing it
|
|
|
|
Added: hacks for Borg
|
|
*/
|
|
void WeaponThink( qboolean inCombat )
|
|
{
|
|
if ( client->ps.weaponstate == WEAPON_RAISING || client->ps.weaponstate == WEAPON_DROPPING )
|
|
{
|
|
ucmd.weapon = client->ps.weapon;
|
|
ucmd.buttons &= ~BUTTON_ATTACK;
|
|
return;
|
|
}
|
|
|
|
//MCG - Begin
|
|
//For now, no-one runs out of ammo
|
|
if(NPC->client->ps.ammo[ weaponData[client->ps.weapon].ammoIndex ] < 10) // checkme
|
|
// if(NPC->client->ps.ammo[ client->ps.weapon ] < 10)
|
|
{
|
|
Add_Ammo (NPC, client->ps.weapon, 100);
|
|
}
|
|
|
|
/*if ( NPC->playerTeam == TEAM_BORG )
|
|
{//HACK!!!
|
|
if(!(NPC->client->ps.stats[STAT_WEAPONS] & ( 1 << WP_BORG_WEAPON )))
|
|
NPC->client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_BORG_WEAPON );
|
|
|
|
if ( client->ps.weapon != WP_BORG_WEAPON )
|
|
{
|
|
NPC_ChangeWeapon( WP_BORG_WEAPON );
|
|
Add_Ammo (NPC, client->ps.weapon, 10);
|
|
NPCInfo->currentAmmo = client->ps.ammo[client->ps.weapon];
|
|
}
|
|
}
|
|
else */
|
|
|
|
/*if ( NPC->client->playerTeam == TEAM_SCAVENGERS )
|
|
{//HACK!!!
|
|
if(!(NPC->client->ps.stats[STAT_WEAPONS] & ( 1 << WP_SCAVENGER_RIFLE )))
|
|
NPC->client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_SCAVENGER_RIFLE );
|
|
|
|
if ( client->ps.weapon != WP_SCAVENGER_RIFLE )
|
|
|
|
{
|
|
NPC_ChangeWeapon( WP_SCAVENGER_RIFLE );
|
|
Add_Ammo (NPC, client->ps.weapon, 10);
|
|
// NPCInfo->currentAmmo = client->ps.ammo[client->ps.weapon];
|
|
NPCInfo->currentAmmo = client->ps.ammo[weaponData[client->ps.weapon].ammoIndex]; // checkme
|
|
}
|
|
}
|
|
else*/
|
|
//MCG - End
|
|
{
|
|
// if the gun in our hands is out of ammo, we need to change
|
|
/*if ( client->ps.ammo[client->ps.weapon] == 0 )
|
|
{
|
|
NPCInfo->aiFlags |= NPCAI_CHECK_WEAPON;
|
|
}
|
|
|
|
if ( NPCInfo->aiFlags & NPCAI_CHECK_WEAPON )
|
|
{
|
|
NPCInfo->aiFlags &= ~NPCAI_CHECK_WEAPON;
|
|
bestWeapon = ChooseBestWeapon();
|
|
if ( bestWeapon != client->ps.weapon )
|
|
{
|
|
NPC_ChangeWeapon( bestWeapon );
|
|
}
|
|
}*/
|
|
}
|
|
|
|
ucmd.weapon = client->ps.weapon;
|
|
ShootThink();
|
|
}
|
|
|
|
/*
|
|
HaveWeapon
|
|
*/
|
|
|
|
qboolean HaveWeapon( int weapon )
|
|
{
|
|
return ( client->ps.stats[STAT_WEAPONS] & ( 1 << weapon ) );
|
|
}
|
|
|
|
qboolean EntIsGlass (gentity_t *check)
|
|
{
|
|
if(check->classname &&
|
|
!Q_stricmp("func_breakable", check->classname) &&
|
|
check->count == 1 && check->health <= 100)
|
|
{
|
|
return qtrue;
|
|
}
|
|
|
|
return qfalse;
|
|
}
|
|
|
|
qboolean ShotThroughGlass (trace_t *tr, gentity_t *target, vec3_t spot, int mask)
|
|
{
|
|
gentity_t *hit = &g_entities[ tr->entityNum ];
|
|
if(hit != target && EntIsGlass(hit))
|
|
{//ok to shoot through breakable glass
|
|
int skip = hit->s.number;
|
|
vec3_t muzzle;
|
|
|
|
VectorCopy(tr->endpos, muzzle);
|
|
gi.trace (tr, muzzle, NULL, NULL, spot, skip, mask );
|
|
return qtrue;
|
|
}
|
|
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
CanShoot
|
|
determine if NPC can directly target enemy
|
|
|
|
this function does not check teams, invulnerability, notarget, etc....
|
|
|
|
Added: If can't shoot center, try head, if not, see if it's close enough to try anyway.
|
|
*/
|
|
qboolean CanShoot ( gentity_t *ent, gentity_t *shooter )
|
|
{
|
|
trace_t tr;
|
|
vec3_t muzzle;
|
|
vec3_t spot, diff;
|
|
gentity_t *traceEnt;
|
|
|
|
CalcEntitySpot( shooter, SPOT_WEAPON, muzzle );
|
|
CalcEntitySpot( ent, SPOT_ORIGIN, spot ); //FIXME preferred target locations for some weapons (feet for R/L)
|
|
|
|
gi.trace ( &tr, muzzle, NULL, NULL, spot, shooter->s.number, MASK_SHOT );
|
|
traceEnt = &g_entities[ tr.entityNum ];
|
|
|
|
// point blank, baby!
|
|
if (tr.startsolid && (shooter->NPC) && (shooter->NPC->touchedByPlayer) )
|
|
{
|
|
traceEnt = shooter->NPC->touchedByPlayer;
|
|
}
|
|
|
|
if ( ShotThroughGlass( &tr, ent, spot, MASK_SHOT ) )
|
|
{
|
|
traceEnt = &g_entities[ tr.entityNum ];
|
|
}
|
|
|
|
// shot is dead on
|
|
if ( traceEnt == ent )
|
|
{
|
|
return qtrue;
|
|
}
|
|
//MCG - Begin
|
|
else
|
|
{//ok, can't hit them in center, try their head
|
|
CalcEntitySpot( ent, SPOT_HEAD, spot );
|
|
gi.trace ( &tr, muzzle, NULL, NULL, spot, shooter->s.number, MASK_SHOT );
|
|
traceEnt = &g_entities[ tr.entityNum ];
|
|
if ( traceEnt == ent)
|
|
{
|
|
return qtrue;
|
|
}
|
|
}
|
|
|
|
//Actually, we should just check to fire in dir we're facing and if it's close enough,
|
|
//and we didn't hit someone on our own team, shoot
|
|
VectorSubtract(spot, tr.endpos, diff);
|
|
if(VectorLength(diff) < random() * 32)
|
|
{
|
|
return qtrue;
|
|
}
|
|
//MCG - End
|
|
// shot would hit a non-client
|
|
if ( !traceEnt->client )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
// shot is blocked by another player
|
|
|
|
// he's already dead, so go ahead
|
|
if ( traceEnt->health <= 0 )
|
|
{
|
|
return qtrue;
|
|
}
|
|
|
|
// don't deliberately shoot a teammate
|
|
if ( traceEnt->client && ( traceEnt->client->playerTeam == shooter->client->playerTeam ) )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
// he's just in the wrong place, go ahead
|
|
return qtrue;
|
|
}
|
|
|
|
|
|
/*
|
|
void NPC_CheckPossibleEnemy( gentity_t *other, visibility_t vis )
|
|
|
|
Added: hacks for scripted NPCs
|
|
*/
|
|
void NPC_CheckPossibleEnemy( gentity_t *other, visibility_t vis )
|
|
{
|
|
// is he is already our enemy?
|
|
if ( other == NPC->enemy )
|
|
return;
|
|
|
|
if ( other->flags & FL_NOTARGET )
|
|
return;
|
|
|
|
// we already have an enemy and this guy is in our FOV, see if this guy would be better
|
|
if ( NPC->enemy && vis == VIS_FOV )
|
|
{
|
|
if ( NPCInfo->enemyLastSeenTime - level.time < 2000 )
|
|
{
|
|
return;
|
|
}
|
|
if ( enemyVisibility == VIS_UNKNOWN )
|
|
{
|
|
enemyVisibility = NPC_CheckVisibility ( NPC->enemy, CHECK_360|CHECK_FOV );
|
|
}
|
|
if ( enemyVisibility == VIS_FOV )
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ( !NPC->enemy )
|
|
{//only take an enemy if you don't have one yet
|
|
G_SetEnemy( NPC, other );
|
|
}
|
|
|
|
if ( vis == VIS_FOV )
|
|
{
|
|
NPCInfo->enemyLastSeenTime = level.time;
|
|
VectorCopy( other->currentOrigin, NPCInfo->enemyLastSeenLocation );
|
|
NPCInfo->enemyLastHeardTime = 0;
|
|
VectorClear( NPCInfo->enemyLastHeardLocation );
|
|
}
|
|
else
|
|
{
|
|
NPCInfo->enemyLastSeenTime = 0;
|
|
VectorClear( NPCInfo->enemyLastSeenLocation );
|
|
NPCInfo->enemyLastHeardTime = level.time;
|
|
VectorCopy( other->currentOrigin, NPCInfo->enemyLastHeardLocation );
|
|
}
|
|
}
|
|
|
|
|
|
//==========================================
|
|
//MCG Added functions:
|
|
//==========================================
|
|
|
|
/*
|
|
int NPC_AttackDebounceForWeapon (void)
|
|
|
|
DOES NOT control how fast you can fire
|
|
Only makes you keep your weapon up after you fire
|
|
|
|
*/
|
|
int NPC_AttackDebounceForWeapon (void)
|
|
{
|
|
switch ( NPC->client->ps.weapon )
|
|
{
|
|
case WP_SCAVENGER_RIFLE://scav rifle
|
|
case WP_CHAOTICA_GUARD_GUN:
|
|
return 500;
|
|
break;
|
|
|
|
case WP_IMOD:
|
|
return 3000;//IMOD
|
|
break;
|
|
|
|
case WP_BORG_WEAPON:
|
|
case WP_BORG_TASER:
|
|
return 1200;//BORG - guess- should be length of the attack anim
|
|
break;
|
|
|
|
case WP_BORG_ASSIMILATOR:
|
|
return 200;
|
|
break;
|
|
|
|
case WP_COMPRESSION_RIFLE://prifle
|
|
return 3000;
|
|
break;
|
|
|
|
case WP_PHASER:
|
|
return 100;
|
|
break;
|
|
|
|
case WP_TRICORDER:
|
|
return 0;//tricorder
|
|
break;
|
|
|
|
case WP_KLINGON_BLADE:
|
|
case WP_IMPERIAL_BLADE:
|
|
return 48;
|
|
break;
|
|
|
|
case WP_BOT_WELDER:
|
|
|
|
if ( g_spskill->integer == 0 )
|
|
return 2000;
|
|
|
|
if ( g_spskill->integer == 1 )
|
|
return 1500;
|
|
|
|
return 1000;
|
|
break;
|
|
|
|
default:
|
|
return 100;
|
|
break;
|
|
}
|
|
}
|
|
|
|
//FIXME: need a mindist for explosive weapons
|
|
float NPC_MaxDistSquaredForWeapon (void)
|
|
{
|
|
if(NPCInfo->stats.shootDistance > 0)
|
|
{//overrides default weapon dist
|
|
return NPCInfo->stats.shootDistance * NPCInfo->stats.shootDistance;
|
|
}
|
|
|
|
switch ( NPC->s.weapon )
|
|
{
|
|
case WP_SCAVENGER_RIFLE://scav rifle
|
|
case WP_CHAOTICA_GUARD_GUN:
|
|
return 1024 * 1024;//should be shorter?
|
|
break;
|
|
|
|
case WP_IMOD:
|
|
return 512 * 512;//IMOD
|
|
break;
|
|
|
|
case WP_BORG_WEAPON:
|
|
return 512 * 512;//BORG
|
|
break;
|
|
|
|
case WP_BORG_TASER:
|
|
return 256 * 256;//BORG
|
|
break;
|
|
|
|
case WP_BORG_DRILL:
|
|
case WP_BORG_ASSIMILATOR:
|
|
return 72 * 72;//BORG
|
|
break;
|
|
|
|
case WP_COMPRESSION_RIFLE://prifle
|
|
return 1024 * 1024;
|
|
break;
|
|
|
|
case WP_PHASER:
|
|
return 1024 * 1024;
|
|
break;
|
|
|
|
case WP_TRICORDER:
|
|
return 0;//tricorder
|
|
break;
|
|
|
|
case WP_IMPERIAL_BLADE:
|
|
case WP_KLINGON_BLADE:
|
|
return 64*64;
|
|
break;
|
|
|
|
default:
|
|
return 1024 * 1024;//was 0
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
-------------------------
|
|
ValidEnemy
|
|
-------------------------
|
|
*/
|
|
|
|
qboolean ValidEnemy(gentity_t *ent)
|
|
{
|
|
if ( ent == NULL )
|
|
return qfalse;
|
|
|
|
//if team_free, maybe everyone is an enemy?
|
|
if ( !NPC->client->enemyTeam )
|
|
return qfalse;
|
|
|
|
if( ent->client && !(ent->flags & FL_NOTARGET))
|
|
{
|
|
if( ent->health > 0 )
|
|
{
|
|
if( ent->client->playerTeam == NPC->client->enemyTeam )
|
|
{
|
|
return qtrue;
|
|
}
|
|
}
|
|
}
|
|
|
|
return qfalse;
|
|
}
|
|
|
|
qboolean NPC_EnemyTooFar(gentity_t *enemy, float dist, qboolean toShoot)
|
|
{
|
|
vec3_t vec;
|
|
|
|
|
|
if ( !toShoot )
|
|
{//Not trying to actually press fire button with this check
|
|
if ( ( NPC->client->ps.weapon == WP_BORG_ASSIMILATOR ) || ( NPC->client->ps.weapon == WP_BORG_DRILL ) )
|
|
{//Just have to get to him
|
|
return qfalse;
|
|
}
|
|
}
|
|
|
|
|
|
if(!dist)
|
|
{
|
|
VectorSubtract(NPC->currentOrigin, enemy->currentOrigin, vec);
|
|
dist = VectorLengthSquared(vec);
|
|
}
|
|
|
|
if(dist > NPC_MaxDistSquaredForWeapon())
|
|
return qtrue;
|
|
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
NPC_PickEnemy
|
|
|
|
Randomly picks a living enemy from the specified team and returns it
|
|
|
|
FIXME: For now, you MUST specify an enemy team
|
|
|
|
If you specify choose closest, it will find only the closest enemy
|
|
|
|
If you specify checkVis, it will return and enemy that is visible
|
|
|
|
If you specify findPlayersFirst, it will try to find players first
|
|
|
|
You can mix and match any of those options (example: find closest visible players first)
|
|
|
|
FIXME: this should go through the snapshot and find the closest enemy
|
|
*/
|
|
gentity_t *NPC_PickEnemy (gentity_t *closestTo, int enemyTeam, qboolean checkVis, qboolean findPlayersFirst, qboolean findClosest)
|
|
{
|
|
int num_choices = 0;
|
|
int choice[128];//FIXME: need a different way to determine how many choices?
|
|
gentity_t *newenemy = NULL;
|
|
gentity_t *closestEnemy = NULL;
|
|
int entNum;
|
|
vec3_t diff;
|
|
float relDist;
|
|
float bestDist = Q3_INFINITE;
|
|
qboolean failed = qfalse;
|
|
int visChecks = (CHECK_360|CHECK_FOV|CHECK_VISRANGE);
|
|
int minVis = VIS_FOV;
|
|
|
|
if (!enemyTeam)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
if ( NPCInfo->behaviorState == BS_FORMATION ||
|
|
NPCInfo->behaviorState == BS_STAND_AND_SHOOT ||
|
|
NPCInfo->behaviorState == BS_HUNT_AND_KILL ||
|
|
NPCInfo->behaviorState == BS_RUN_AND_SHOOT ||
|
|
NPCInfo->behaviorState == BS_POINT_COMBAT )
|
|
{//Formations guys don't require inFov to pick up a target
|
|
//These other behavior states are active battle states and should not
|
|
//use FOV. FOV checks are for enemies who are patrolling, guarding, etc.
|
|
visChecks &= ~CHECK_FOV;
|
|
minVis = VIS_360;
|
|
}
|
|
|
|
if( findPlayersFirst || enemyTeam == TEAM_PLAYER )
|
|
{//try to find a player first
|
|
newenemy = &g_entities[0];
|
|
if( newenemy->client && !(newenemy->flags & FL_NOTARGET) && !(newenemy->s.eFlags & EF_NODRAW))
|
|
{
|
|
if( newenemy->health > 0 )
|
|
{
|
|
if( enemyTeam == TEAM_PLAYER || newenemy->client->playerTeam == enemyTeam ||
|
|
( enemyTeam == TEAM_STARFLEET && !NPC_CheckDisguise( newenemy ) ) )
|
|
{//FIXME: check for range and FOV or vis?
|
|
if( newenemy != NPC->lastEnemy )
|
|
{//Make sure we're not just going back and forth here
|
|
if ( gi.inPVS(newenemy->currentOrigin, NPC->currentOrigin) )
|
|
{
|
|
if(NPCInfo->behaviorState == BS_INVESTIGATE || NPCInfo->behaviorState == BS_PATROL)
|
|
{
|
|
if(!NPC->enemy)
|
|
{
|
|
if(!InVisrange(newenemy))
|
|
{
|
|
failed = qtrue;
|
|
}
|
|
else if(NPC_CheckVisibility ( newenemy, CHECK_360|CHECK_FOV|CHECK_VISRANGE ) != VIS_FOV)
|
|
{
|
|
failed = qtrue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( !failed )
|
|
{
|
|
VectorSubtract( closestTo->currentOrigin, newenemy->currentOrigin, diff );
|
|
relDist = VectorLengthSquared(diff);
|
|
if ( newenemy->client->hiddenDist > 0 )
|
|
{
|
|
if( relDist > newenemy->client->hiddenDist*newenemy->client->hiddenDist )
|
|
{
|
|
//out of hidden range
|
|
if ( VectorLengthSquared( newenemy->client->hiddenDir ) )
|
|
{//They're only hidden from a certain direction, check
|
|
float dot;
|
|
VectorNormalize( diff );
|
|
dot = DotProduct( newenemy->client->hiddenDir, diff );
|
|
if ( dot > 0.5 )
|
|
{//I'm not looking in the right dir toward them to see them
|
|
failed = qtrue;
|
|
}
|
|
else
|
|
{
|
|
Debug_Printf(debugNPCAI, DEBUG_LEVEL_INFO, "%s saw %s trying to hide - hiddenDir %s targetDir %s dot %f\n", NPC->targetname, newenemy->targetname, vtos(newenemy->client->hiddenDir), vtos(diff), dot );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
failed = qtrue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug_Printf(debugNPCAI, DEBUG_LEVEL_INFO, "%s saw %s trying to hide - hiddenDist %f\n", NPC->targetname, newenemy->targetname, newenemy->client->hiddenDist );
|
|
}
|
|
}
|
|
|
|
if(!failed)
|
|
{
|
|
if(findClosest)
|
|
{
|
|
if(relDist < bestDist)
|
|
{
|
|
if(!NPC_EnemyTooFar(newenemy, relDist, qfalse))
|
|
{
|
|
if(checkVis)
|
|
{
|
|
if( NPC_CheckVisibility ( newenemy, visChecks ) == minVis )
|
|
{
|
|
bestDist = relDist;
|
|
closestEnemy = newenemy;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bestDist = relDist;
|
|
closestEnemy = newenemy;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if(!NPC_EnemyTooFar(newenemy, 0, qfalse))
|
|
{
|
|
if(checkVis)
|
|
{
|
|
if( NPC_CheckVisibility ( newenemy, CHECK_360|CHECK_FOV|CHECK_VISRANGE ) == VIS_FOV )
|
|
{
|
|
choice[num_choices++] = newenemy->s.number;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
choice[num_choices++] = newenemy->s.number;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (findClosest && closestEnemy)
|
|
{
|
|
return closestEnemy;
|
|
}
|
|
|
|
if (num_choices)
|
|
{
|
|
return &g_entities[ choice[rand() % num_choices] ];
|
|
}
|
|
|
|
if ( enemyTeam == TEAM_PLAYER )
|
|
{//couldn't find the player
|
|
return NULL;
|
|
}
|
|
|
|
num_choices = 0;
|
|
bestDist = Q3_INFINITE;
|
|
closestEnemy = NULL;
|
|
|
|
for ( entNum = 0; entNum < globals.num_entities; entNum++ )
|
|
{
|
|
newenemy = &g_entities[entNum];
|
|
|
|
if ( (newenemy->client || newenemy->svFlags & SVF_NONNPC_ENEMY) && !(newenemy->flags & FL_NOTARGET) && !(newenemy->s.eFlags & EF_NODRAW))
|
|
{
|
|
if ( newenemy->health > 0 )
|
|
{
|
|
if ( (newenemy->client && newenemy->client->playerTeam == enemyTeam) || (!newenemy->client && newenemy->noDamageTeam == enemyTeam) )
|
|
{//FIXME: check for range and FOV or vis?
|
|
if ( newenemy != NPC->lastEnemy )
|
|
{//Make sure we're not just going back and forth here
|
|
if(!gi.inPVS(newenemy->currentOrigin, NPC->currentOrigin))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( NPCInfo->behaviorState == BS_INVESTIGATE || NPCInfo->behaviorState == BS_PATROL )
|
|
{
|
|
if ( !NPC->enemy )
|
|
{
|
|
if ( !InVisrange( newenemy ) )
|
|
{
|
|
continue;
|
|
}
|
|
else if ( NPC_CheckVisibility ( newenemy, CHECK_360|CHECK_FOV|CHECK_VISRANGE ) != VIS_FOV )
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
VectorSubtract( closestTo->currentOrigin, newenemy->currentOrigin, diff );
|
|
relDist = VectorLengthSquared(diff);
|
|
if ( newenemy->client && newenemy->client->hiddenDist > 0 )
|
|
{
|
|
if( relDist > newenemy->client->hiddenDist*newenemy->client->hiddenDist )
|
|
{
|
|
//out of hidden range
|
|
if ( VectorLengthSquared( newenemy->client->hiddenDir ) )
|
|
{//They're only hidden from a certain direction, check
|
|
float dot;
|
|
|
|
VectorNormalize( diff );
|
|
dot = DotProduct( newenemy->client->hiddenDir, diff );
|
|
if ( dot > 0.5 )
|
|
{//I'm not looking in the right dir toward them to see them
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
Debug_Printf(debugNPCAI, DEBUG_LEVEL_INFO, "%s saw %s trying to hide - hiddenDir %s targetDir %s dot %f\n", NPC->targetname, newenemy->targetname, vtos(newenemy->client->hiddenDir), vtos(diff), dot );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug_Printf(debugNPCAI, DEBUG_LEVEL_INFO, "%s saw %s trying to hide - hiddenDist %f\n", NPC->targetname, newenemy->targetname, newenemy->client->hiddenDist );
|
|
}
|
|
}
|
|
|
|
if ( findClosest )
|
|
{
|
|
if ( relDist < bestDist )
|
|
{
|
|
if ( !NPC_EnemyTooFar( newenemy, relDist, qfalse ) )
|
|
{
|
|
if ( checkVis )
|
|
{
|
|
//FIXME: NPCs need to be able to pick up other NPCs behind them,
|
|
//but for now, commented out because it was picking up enemies it shouldn't
|
|
//if ( NPC_CheckVisibility ( newenemy, CHECK_360|CHECK_VISRANGE ) >= VIS_360 )
|
|
if ( NPC_CheckVisibility ( newenemy, visChecks ) == minVis )
|
|
{
|
|
bestDist = relDist;
|
|
closestEnemy = newenemy;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bestDist = relDist;
|
|
closestEnemy = newenemy;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if ( !NPC_EnemyTooFar( newenemy, 0, qfalse ) )
|
|
{
|
|
if ( checkVis )
|
|
{
|
|
//if( NPC_CheckVisibility ( newenemy, CHECK_360|CHECK_FOV|CHECK_VISRANGE ) == VIS_FOV )
|
|
if ( NPC_CheckVisibility ( newenemy, CHECK_360|CHECK_VISRANGE ) >= VIS_360 )
|
|
{
|
|
choice[num_choices++] = newenemy->s.number;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
choice[num_choices++] = newenemy->s.number;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (findClosest)
|
|
{//FIXME: you can pick up an enemy around a corner this way.
|
|
return closestEnemy;
|
|
}
|
|
|
|
if (!num_choices)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
return &g_entities[ choice[rand() % num_choices] ];
|
|
}
|
|
|
|
/*
|
|
gentity_t *NPC_PickAlly ( void )
|
|
|
|
Simply returns closest visible ally
|
|
*/
|
|
|
|
gentity_t *NPC_PickAlly ( qboolean facingEachOther, float range, qboolean ignoreGroup, qboolean movingOnly )
|
|
{
|
|
gentity_t *ally = NULL;
|
|
gentity_t *closestAlly = NULL;
|
|
int entNum;
|
|
vec3_t diff;
|
|
float relDist;
|
|
float bestDist = range;
|
|
|
|
for ( entNum = 0; entNum < globals.num_entities; entNum++ )
|
|
{
|
|
ally = &g_entities[entNum];
|
|
|
|
if ( ally->client )
|
|
{
|
|
if ( ally->health > 0 )
|
|
{
|
|
if ( ally->client && ( ally->client->playerTeam == NPC->client->playerTeam ||
|
|
( (NPC->client->playerTeam == TEAM_SCAVENGERS||NPC->client->playerTeam==TEAM_IMPERIAL) && ally->client->playerTeam == TEAM_DISGUISE ) ) )
|
|
{//if on same team or if player is disguised as your team
|
|
if ( ignoreGroup )
|
|
{
|
|
if ( ally == NPC->client->leader )
|
|
{
|
|
//reject
|
|
continue;
|
|
}
|
|
if ( ally->client && ally->client->leader && ally->client->leader == NPC )
|
|
{
|
|
//reject
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if(!gi.inPVS(ally->currentOrigin, NPC->currentOrigin))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( movingOnly && ally->client && NPC->client )
|
|
{//They have to be moving relative to each other
|
|
if ( !DistanceSquared( ally->client->ps.velocity, NPC->client->ps.velocity ) )
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
VectorSubtract( NPC->currentOrigin, ally->currentOrigin, diff );
|
|
relDist = VectorNormalize( diff );
|
|
if ( relDist < bestDist )
|
|
{
|
|
if ( facingEachOther )
|
|
{
|
|
vec3_t vf;
|
|
float dot;
|
|
|
|
AngleVectors( ally->client->ps.viewangles, vf, NULL, NULL );
|
|
VectorNormalize(vf);
|
|
dot = DotProduct(diff, vf);
|
|
|
|
if ( dot < 0.5 )
|
|
{//Not facing in dir to me
|
|
continue;
|
|
}
|
|
//He's facing me, am I facing him?
|
|
AngleVectors( NPC->client->ps.viewangles, vf, NULL, NULL );
|
|
VectorNormalize(vf);
|
|
dot = DotProduct(diff, vf);
|
|
|
|
if ( dot > -0.5 )
|
|
{//I'm not facing opposite of dir to me
|
|
continue;
|
|
}
|
|
//I am facing him
|
|
}
|
|
|
|
if ( NPC_CheckVisibility ( ally, CHECK_360|CHECK_VISRANGE ) >= VIS_360 )
|
|
{
|
|
bestDist = relDist;
|
|
closestAlly = ally;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
return closestAlly;
|
|
}
|
|
|
|
void NPC_CheckEnemy (qboolean findNew, qboolean tooFarOk)
|
|
{
|
|
qboolean forcefindNew = qfalse;
|
|
gentity_t *closestTo;
|
|
//FIXME: have a "NPCInfo->persistance" we can set to determine how long to try to shoot
|
|
//someone we can't hit? Rather than hard-coded 10?
|
|
|
|
//FIXME they shouldn't recognize enemy's death instantly
|
|
|
|
//TEMP FIX:
|
|
//if(NPC->enemy->client)
|
|
//{
|
|
// NPC->enemy->health = NPC->enemy->client->ps.stats[STAT_HEALTH];
|
|
//}
|
|
|
|
if ( NPC->enemy )
|
|
{
|
|
if ( !NPC->enemy->inuse )
|
|
{
|
|
G_ClearEnemy( NPC );
|
|
}
|
|
}
|
|
|
|
if ( NPC->svFlags & SVF_IGNORE_ENEMIES )
|
|
{//We're ignoring all enemies for now
|
|
G_ClearEnemy( NPC );
|
|
return;
|
|
}
|
|
|
|
if ( NPC->svFlags & SVF_LOCKEDENEMY )
|
|
{//keep this enemy until dead
|
|
if ( NPC->enemy )
|
|
{
|
|
if ( (!NPC->NPC && !(NPC->svFlags & SVF_NONNPC_ENEMY) ) || NPC->enemy->health > 0 )
|
|
{//Enemy never had health (a train or info_not_null, etc) or did and is now dead (NPCs, turrets, etc)
|
|
return;
|
|
}
|
|
}
|
|
NPC->svFlags &= ~SVF_LOCKEDENEMY;
|
|
}
|
|
|
|
if ( NPC->enemy )
|
|
{
|
|
if ( NPC_EnemyTooFar(NPC->enemy, 0, qfalse) )
|
|
{
|
|
if(findNew)
|
|
{//See if there is a close one and take it if so, else keep this one
|
|
forcefindNew = qtrue;
|
|
}
|
|
else if(!tooFarOk)//FIXME: don't need this extra bool any more
|
|
{
|
|
G_ClearEnemy( NPC );
|
|
}
|
|
}
|
|
else if ( !gi.inPVS(NPC->currentOrigin, NPC->enemy->currentOrigin ) )
|
|
{//FIXME: should this be a line-of site check?
|
|
//FIXME: a lot of things check PVS AGAIN when deciding whether
|
|
//or not to shoot, redundant!
|
|
//Should we lose the enemy?
|
|
//FIXME: if lose enemy, run lostenemyscript
|
|
if ( NPC->enemy->client && NPC->enemy->client->hiddenDist )
|
|
{//He ducked into shadow while we weren't looking
|
|
//Drop enemy and see if we should search for him
|
|
NPC_LostEnemyDecideChase();
|
|
}
|
|
else
|
|
{//If we're not chasing him, we need to lose him
|
|
switch( NPCInfo->behaviorState )
|
|
{
|
|
case BS_HUNT_AND_KILL:
|
|
case BS_COMBAT:
|
|
//Okay to lose PVS, we're chasing them
|
|
break;
|
|
case BS_RUN_AND_SHOOT:
|
|
//If he's not our goalEntity, we're running somewhere else, so lose him
|
|
if ( NPC->enemy != NPCInfo->goalEntity )
|
|
{
|
|
G_ClearEnemy( NPC );
|
|
}
|
|
break;
|
|
default:
|
|
//We're not chasing him, so lose him as an enemy
|
|
G_ClearEnemy( NPC );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( NPC->enemy )
|
|
{
|
|
if ( NPC->enemy->health <= 0 || NPC->enemy->flags & FL_NOTARGET )
|
|
{
|
|
G_ClearEnemy( NPC );
|
|
}
|
|
}
|
|
|
|
closestTo = NPC;
|
|
//FIXME: check your defendEnt, if you have one, see if their enemy is different
|
|
//than yours, or, if they don't have one, pick the closest enemy to THEM?
|
|
if ( NPCInfo->defendEnt )
|
|
{//Trying to protect someone
|
|
if ( NPCInfo->defendEnt->health > 0 )
|
|
{//Still alive, We presume we're close to them, navigation should handle this?
|
|
if ( NPCInfo->defendEnt->enemy )
|
|
{//They were shot or acquired an enemy
|
|
if ( NPC->enemy != NPCInfo->defendEnt->enemy )
|
|
{//They have a different enemy, take it!
|
|
G_SetEnemy( NPC, NPCInfo->defendEnt->enemy );
|
|
}
|
|
}
|
|
else if ( NPC->enemy == NULL )
|
|
{//We don't have an enemy, so find closest to defendEnt
|
|
closestTo = NPCInfo->defendEnt;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!NPC->enemy || ( NPC->enemy && (NPC->enemy->health <= 0 || NPC->cantHitEnemyCounter >= 100)) || forcefindNew )
|
|
{//FIXME: NPCs that are moving after an enemy should ignore the can't hit enemy counter- that should only be for NPCs that are standing still
|
|
//NOTE: cantHitEnemyCounter >= 100 means we couldn't hit enemy for a full
|
|
// 10 seconds, so give up. This means even if we're chasing him, we would
|
|
// try to find another enemy after 10 seconds (assuming the cantHitEnemyCounter
|
|
// is allowed to increment in a chasing bState)
|
|
gentity_t *newenemy;
|
|
qboolean foundenemy = qfalse;
|
|
|
|
if(!findNew)
|
|
{
|
|
NPC->lastEnemy = NPC->enemy;
|
|
G_ClearEnemy(NPC);
|
|
return;
|
|
}
|
|
|
|
//If enemy dead or unshootable, look for others on out enemy's team
|
|
if ( NPC->client->enemyTeam )
|
|
{
|
|
//NOTE: this only checks vis if can't hit enemy for 10 tries, which I suppose
|
|
// means they need to find one that in more than just PVS
|
|
//newenemy = NPC_PickEnemy( closestTo, NPC->client->enemyTeam, (NPC->cantHitEnemyCounter > 10), qfalse, qtrue );//3rd parm was (NPC->enemyTeam == TEAM_STARFLEET)
|
|
//For now, made it so you ALWAYS have to check VIS
|
|
newenemy = NPC_PickEnemy( closestTo, NPC->client->enemyTeam, qtrue, qfalse, qtrue );//3rd parm was (NPC->enemyTeam == TEAM_STARFLEET)
|
|
if(newenemy)
|
|
{
|
|
foundenemy = qtrue;
|
|
G_SetEnemy( NPC, newenemy );
|
|
}
|
|
}
|
|
|
|
if(!forcefindNew)
|
|
{
|
|
if(!foundenemy)
|
|
{
|
|
NPC->lastEnemy = NPC->enemy;
|
|
G_ClearEnemy(NPC);
|
|
}
|
|
|
|
NPC->cantHitEnemyCounter = 0;
|
|
}
|
|
//FIXME: if we can't find any at all, go into INdependant NPC AI, pursue and kill
|
|
}
|
|
|
|
if ( NPC->enemy && NPC->enemy->client )
|
|
{
|
|
if(NPC->enemy->client->playerTeam)
|
|
{
|
|
// assert( NPC->client->playerTeam != NPC->enemy->client->playerTeam);
|
|
if( NPC->client->playerTeam != NPC->enemy->client->playerTeam )
|
|
{
|
|
NPC->client->enemyTeam = NPC->enemy->client->playerTeam;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
NPC_CheckAttack
|
|
|
|
Simply checks aggression and returns true or false
|
|
*/
|
|
|
|
qboolean NPC_CheckAttack (float scale)
|
|
{
|
|
if(!scale)
|
|
scale = 1.0;
|
|
|
|
if(((float)NPCInfo->stats.aggression) * scale < Q_flrand(0, 4))
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
if(NPCInfo->shotTime > level.time)
|
|
return qfalse;
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
NPC_CheckDefend
|
|
|
|
Simply checks evasion and returns true or false
|
|
*/
|
|
|
|
qboolean NPC_CheckDefend (float scale)
|
|
{
|
|
if(!scale)
|
|
scale = 1.0;
|
|
|
|
if((float)(NPCInfo->stats.evasion) > random() * 4 * scale)
|
|
return qtrue;
|
|
|
|
return qfalse;
|
|
}
|
|
//========================================================================================
|
|
//OLD id-style hunt and kill
|
|
//========================================================================================
|
|
/*
|
|
IdealDistance
|
|
|
|
determines what the NPC's ideal distance from it's enemy should
|
|
be in the current situation
|
|
*/
|
|
float IdealDistance ( gentity_t *self )
|
|
{
|
|
float ideal;
|
|
|
|
ideal = 225 - 20 * NPCInfo->stats.aggression;
|
|
switch ( NPC->s.weapon )
|
|
{
|
|
case WP_QUANTUM_BURST:
|
|
ideal += 200;
|
|
break;
|
|
|
|
case WP_GRENADE_LAUNCHER:
|
|
ideal += 50;
|
|
break;
|
|
|
|
case WP_BORG_WEAPON:
|
|
ideal = 256;
|
|
break;
|
|
|
|
case WP_BORG_DRILL:
|
|
case WP_BORG_ASSIMILATOR:
|
|
ideal = 36;
|
|
break;
|
|
|
|
case WP_BORG_TASER:
|
|
ideal = 128;
|
|
break;
|
|
|
|
case WP_STASIS:
|
|
ideal = 128;
|
|
break;
|
|
|
|
case WP_TRICORDER:
|
|
ideal = 0;
|
|
break;
|
|
|
|
case WP_PHASER:
|
|
case WP_COMPRESSION_RIFLE:
|
|
case WP_IMOD:
|
|
case WP_SCAVENGER_RIFLE:
|
|
case WP_CHAOTICA_GUARD_GUN:
|
|
case WP_TETRION_DISRUPTOR:
|
|
case WP_DREADNOUGHT:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return ideal;
|
|
}
|
|
|
|
/*QUAKED point_combat (0.7 0 0.7) (-16 -16 -24) (16 16 28) DUCK FLEE INVESTIGATE SQUAD LEAN
|
|
NPCs in bState BS_COMBAT_POINT will find their closest empty combat_point
|
|
|
|
DUCK - NPC will duck and fire from this point
|
|
FLEE - Will choose this point when running
|
|
INVESTIGATE - Will look here if a sound is heard near it
|
|
*/
|
|
|
|
void SP_point_combat( gentity_t *self )
|
|
{
|
|
if(level.numCombatPoints >= MAX_COMBAT_POINTS)
|
|
{
|
|
gi.Printf(S_COLOR_RED"ERROR: Too many combat points, limit is %d\n", MAX_COMBAT_POINTS);
|
|
G_FreeEntity(self);
|
|
return;
|
|
}
|
|
|
|
G_SetOrigin(self, self->s.origin);
|
|
gi.linkentity(self);
|
|
|
|
VectorCopy(self->currentOrigin, level.combatPoints[level.numCombatPoints].origin);
|
|
|
|
level.combatPoints[level.numCombatPoints].flags = self->spawnflags;
|
|
level.combatPoints[level.numCombatPoints].occupied = qfalse;
|
|
|
|
level.numCombatPoints++;
|
|
|
|
G_FreeEntity(self);
|
|
};
|
|
|
|
/*
|
|
-------------------------
|
|
NPC_CollectCombatPoints
|
|
-------------------------
|
|
*/
|
|
|
|
typedef map< int, int > combatPoint_m;
|
|
|
|
int NPC_CollectCombatPoints( vec3_t origin, float radius, combatPoint_m &points )
|
|
{
|
|
float radiusSqr = (radius*radius);
|
|
float distance;
|
|
|
|
//Collect all nearest
|
|
for ( int i = 0; i < level.numCombatPoints; i++ )
|
|
{
|
|
distance = DistanceSquared( origin, level.combatPoints[i].origin );
|
|
|
|
if ( distance < radiusSqr )
|
|
{
|
|
//Using a map will sort nearest automatically
|
|
points[ distance ] = i;
|
|
}
|
|
}
|
|
|
|
return points.size();
|
|
}
|
|
|
|
/*
|
|
-------------------------
|
|
NPC_FindCombatPoint
|
|
-------------------------
|
|
*/
|
|
|
|
#define MIN_AVOID_DOT 0.75f
|
|
#define MIN_AVOID_DISTANCE 128
|
|
#define MIN_AVOID_DISTANCE_SQUARED ( MIN_AVOID_DISTANCE * MIN_AVOID_DISTANCE )
|
|
#define CP_COLLECT_RADIUS 512
|
|
|
|
int NPC_FindCombatPoint( vec3_t position, vec3_t avoidPosition, vec3_t enemyPosition, int flags )
|
|
{
|
|
combatPoint_m points;
|
|
|
|
//Collect our nearest points
|
|
NPC_CollectCombatPoints( position, CP_COLLECT_RADIUS, points );
|
|
|
|
int i;
|
|
combatPoint_m::iterator cpi;
|
|
STL_ITERATE( cpi, points )
|
|
{
|
|
i = (*cpi).second;
|
|
|
|
//Must be vacant
|
|
if ( level.combatPoints[i].occupied == (int) qtrue )
|
|
continue;
|
|
|
|
//If we want a duck space, make sure this is one
|
|
if ( ( flags & CP_DUCK ) && ( level.combatPoints[i].flags & CPF_DUCK ) )
|
|
continue;
|
|
|
|
//If we want a duck space, make sure this is one
|
|
if ( ( flags & CP_FLEE ) && ( level.combatPoints[i].flags & CPF_FLEE ) )
|
|
continue;
|
|
|
|
///Make sure this is an investigate combat point
|
|
if ( ( flags & CP_INVESTIGATE ) && ( level.combatPoints[i].flags & CPF_INVESTIGATE ) )
|
|
continue;
|
|
|
|
//Squad points are only valid if we're looking for them
|
|
if ( ( level.combatPoints[i].flags & CPF_SQUAD ) && ( ( flags & CP_SQUAD ) == qfalse ) )
|
|
continue;
|
|
|
|
//If we need a cover point, check this point
|
|
if ( ( flags & CP_COVER ) && ( NPC_ClearLOS( level.combatPoints[i].origin, NPC->enemy ) == qtrue ) )
|
|
continue;
|
|
|
|
//Need a clear LOS to our target
|
|
if ( ( flags & CP_CLEAR ) && ( NPC_ClearLOS( level.combatPoints[i].origin, NPC->enemy ) == qfalse ) )
|
|
continue;
|
|
|
|
//Avoid this position?
|
|
if ( ( flags & CP_AVOID ) && ( DistanceSquared( level.combatPoints[i].origin, enemyPosition ) < MIN_AVOID_DISTANCE_SQUARED ) )
|
|
continue;
|
|
|
|
//See if we're trying to avoid our enemy
|
|
if ( flags & CP_AVOID_ENEMY )
|
|
{
|
|
vec3_t eDir, gDir;
|
|
|
|
VectorSubtract( position, enemyPosition, eDir );
|
|
VectorNormalize( eDir );
|
|
|
|
VectorSubtract( position, level.combatPoints[i].origin, gDir );
|
|
VectorNormalize( gDir );
|
|
|
|
float dot = DotProduct( gDir, eDir );
|
|
|
|
//Don't want to run at enemy
|
|
if ( dot >= MIN_AVOID_DOT )
|
|
continue;
|
|
|
|
//Can't be too close to the enemy
|
|
if ( DistanceSquared( level.combatPoints[i].origin, enemyPosition ) < MIN_AVOID_DISTANCE_SQUARED )
|
|
continue;
|
|
}
|
|
|
|
return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
//Overload
|
|
|
|
int NPC_FindCombatPoint( int flags )
|
|
{
|
|
vec3_t avoid;
|
|
|
|
if ( NPC_ValidEnemy( NPC->enemy ) == qfalse )
|
|
{
|
|
flags &= ~CP_AVOID_ENEMY;
|
|
VectorClear( avoid );
|
|
}
|
|
else //FIXME: Hate else statements...
|
|
{
|
|
VectorCopy( NPC->enemy->currentOrigin, avoid );
|
|
}
|
|
|
|
return NPC_FindCombatPoint( NPC->currentOrigin, avoid, avoid, flags );
|
|
}
|
|
|
|
/*
|
|
-------------------------
|
|
NPC_FindSquadPoint
|
|
-------------------------
|
|
*/
|
|
|
|
int NPC_FindSquadPoint( vec3_t position )
|
|
{
|
|
unsigned int nearestDist = 99999999;
|
|
int nearestPoint = -1;
|
|
|
|
//float playerDist = DistanceSquared( g_entities[0].currentOrigin, NPC->currentOrigin );
|
|
|
|
for ( int i = 0; i < level.numCombatPoints; i++ )
|
|
{
|
|
//Squad points are only valid if we're looking for them
|
|
if ( ( level.combatPoints[i].flags & CPF_SQUAD ) == qfalse )
|
|
continue;
|
|
|
|
//Must be vacant
|
|
if ( level.combatPoints[i].occupied == qtrue )
|
|
continue;
|
|
|
|
unsigned int dist = DistanceSquared( position, level.combatPoints[i].origin );
|
|
|
|
//The point cannot take us past the player
|
|
//if ( dist > ( playerDist * DotProduct( dirToPlayer, playerDir ) ) ) //FIXME: Retain this
|
|
// continue;
|
|
|
|
//See if this is closer than the others
|
|
if ( dist < nearestDist )
|
|
{
|
|
nearestPoint = i;
|
|
nearestDist = dist;
|
|
}
|
|
}
|
|
|
|
return nearestPoint;
|
|
}
|
|
|
|
/*
|
|
-------------------------
|
|
NPC_ReserveCombatPoint
|
|
-------------------------
|
|
*/
|
|
|
|
qboolean NPC_ReserveCombatPoint( int combatPointID )
|
|
{
|
|
//Make sure it's valid
|
|
if ( combatPointID > level.numCombatPoints )
|
|
return qfalse;
|
|
|
|
//Make sure it's not already occupied
|
|
if ( level.combatPoints[combatPointID].occupied )
|
|
return qfalse;
|
|
|
|
//Reserve it
|
|
level.combatPoints[combatPointID].occupied = qtrue;
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
-------------------------
|
|
NPC_FreeCombatPoint
|
|
-------------------------
|
|
*/
|
|
|
|
qboolean NPC_FreeCombatPoint( int combatPointID )
|
|
{
|
|
//Make sure it's valid
|
|
if ( combatPointID > level.numCombatPoints )
|
|
return qfalse;
|
|
|
|
//Make sure it's currently occupied
|
|
if ( level.combatPoints[combatPointID].occupied == qfalse )
|
|
return qfalse;
|
|
|
|
//Free it
|
|
level.combatPoints[combatPointID].occupied = qfalse;
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
-------------------------
|
|
NPC_SetCombatPoint
|
|
-------------------------
|
|
*/
|
|
|
|
qboolean NPC_SetCombatPoint( int combatPointID )
|
|
{
|
|
//Free a combat point if we already have one
|
|
if ( NPCInfo->combatPoint != -1 )
|
|
{
|
|
NPC_FreeCombatPoint( NPCInfo->combatPoint );
|
|
}
|
|
|
|
if ( NPC_ReserveCombatPoint( combatPointID ) == qfalse )
|
|
return qfalse;
|
|
|
|
NPCInfo->combatPoint = combatPointID;
|
|
|
|
return qtrue;
|
|
}
|
|
|