/* =========================================================================== 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 . =========================================================================== */ //NPC_utils.cpp #include "b_local.h" #include "Q3_Interface.h" #include "g_navigator.h" #include "../cgame/cg_local.h" #include "g_nav.h" extern Vehicle_t *G_IsRidingVehicle( gentity_t *pEnt ); int teamNumbers[TEAM_NUM_TEAMS]; int teamStrength[TEAM_NUM_TEAMS]; int teamCounter[TEAM_NUM_TEAMS]; #define VALID_ATTACK_CONE 2.0f //Degrees void GetAnglesForDirection( const vec3_t p1, const vec3_t p2, vec3_t out ); /* void CalcEntitySpot ( gentity_t *ent, spot_t spot, vec3_t point ) Added: Uses shootAngles if a NPC has them */ extern void ViewHeightFix(const gentity_t *const ent); extern void AddLeanOfs(const gentity_t *const ent, vec3_t point); extern void SubtractLeanOfs(const gentity_t *const ent, vec3_t point); void CalcEntitySpot ( const gentity_t *ent, const spot_t spot, vec3_t point ) { vec3_t forward, up, right; vec3_t start, end; trace_t tr; if ( !ent ) { return; } ViewHeightFix(ent); switch ( spot ) { case SPOT_ORIGIN: if(VectorCompare(ent->currentOrigin, vec3_origin)) {//brush VectorSubtract(ent->absmax, ent->absmin, point);//size VectorMA(ent->absmin, 0.5, point, point); } else { VectorCopy ( ent->currentOrigin, point ); } break; case SPOT_CHEST: case SPOT_HEAD: if ( ent->client && VectorLengthSquared( ent->client->renderInfo.eyePoint ) && (ent->client->ps.viewEntity <= 0 || ent->client->ps.viewEntity >= ENTITYNUM_WORLD) ) {//Actual tag_head eyespot! //FIXME: Stasis aliens may have a problem here... VectorCopy( ent->client->renderInfo.eyePoint, point ); if ( ent->client->NPC_class == CLASS_ATST ) {//adjust up some point[2] += 28;//magic number :) } else if (!ent->client->ps.clientNum) { // When IRL crouch is used, view height is higher than model height // We need to lower "point" else enemies will be aiming into empty space int viewHeight = ent->client->ps.viewheight - STANDARD_VIEWHEIGHT_OFFSET; int realHeight = ent->maxs[2]; point[2] -= viewHeight - realHeight; } if ( ent->NPC ) {//always aim from the center of my bbox, so we don't wiggle when we lean forward or backwards point[0] = ent->currentOrigin[0]; point[1] = ent->currentOrigin[1]; } else if ( !ent->s.number ) { SubtractLeanOfs( ent, point ); } } else { VectorCopy ( ent->currentOrigin, point ); if ( ent->client ) { point[2] += ent->client->ps.viewheight; } } if ( spot == SPOT_CHEST && ent->client ) { if ( ent->client->NPC_class != CLASS_ATST ) {//adjust up some point[2] -= ent->maxs[2]*0.2f; } } break; case SPOT_HEAD_LEAN: if ( ent->client && VectorLengthSquared( ent->client->renderInfo.eyePoint ) && (ent->client->ps.viewEntity <= 0 || ent->client->ps.viewEntity >= ENTITYNUM_WORLD) ) {//Actual tag_head eyespot! //FIXME: Stasis aliens may have a problem here... VectorCopy( ent->client->renderInfo.eyePoint, point ); if ( ent->client->NPC_class == CLASS_ATST ) {//adjust up some point[2] += 28;//magic number :) } else if (!ent->client->ps.clientNum) { // When IRL crouch is used, view height is higher than model height // We need to lower "point" else enemies will be aiming into empty space int viewHeight = ent->client->ps.viewheight - STANDARD_VIEWHEIGHT_OFFSET; int realHeight = ent->maxs[2]; point[2] -= viewHeight - realHeight; } if ( ent->NPC ) {//always aim from the center of my bbox, so we don't wiggle when we lean forward or backwards point[0] = ent->currentOrigin[0]; point[1] = ent->currentOrigin[1]; } else if ( !ent->s.number ) { SubtractLeanOfs( ent, point ); } //NOTE: automatically takes leaning into account! } else { VectorCopy ( ent->currentOrigin, point ); if ( ent->client ) { point[2] += ent->client->ps.viewheight; } //AddLeanOfs ( ent, point ); } break; //FIXME: implement... //case SPOT_CHEST: //Returns point 3/4 from tag_torso to tag_head? //break; case SPOT_LEGS: VectorCopy ( ent->currentOrigin, point ); point[2] += (ent->mins[2] * 0.5); break; case SPOT_WEAPON: if( ent->NPC && !VectorCompare( ent->NPC->shootAngles, vec3_origin ) && !VectorCompare( ent->NPC->shootAngles, ent->client->ps.viewangles )) { AngleVectors( ent->NPC->shootAngles, forward, right, up ); } else { AngleVectors( ent->client->ps.viewangles, forward, right, up ); } CalcMuzzlePoint( (gentity_t*)ent, forward, right, up, point, 0 ); //NOTE: automatically takes leaning into account! break; case SPOT_GROUND: // if entity is on the ground, just use it's absmin if ( ent->s.groundEntityNum != -1 ) { VectorCopy( ent->currentOrigin, point ); point[2] = ent->absmin[2]; break; } // if it is reasonably close to the ground, give the point underneath of it VectorCopy( ent->currentOrigin, start ); start[2] = ent->absmin[2]; VectorCopy( start, end ); end[2] -= 64; gi.trace( &tr, start, ent->mins, ent->maxs, end, ent->s.number, MASK_PLAYERSOLID, (EG2_Collision)0, 0 ); if ( tr.fraction < 1.0 ) { VectorCopy( tr.endpos, point); break; } // otherwise just use the origin VectorCopy( ent->currentOrigin, point ); break; default: VectorCopy ( ent->currentOrigin, point ); break; } } //=================================================================================== /* qboolean NPC_UpdateAngles ( qboolean doPitch, qboolean doYaw ) Added: option to do just pitch or just yaw Does not include "aim" in it's calculations FIXME: stop compressing angles into shorts!!!! */ extern cvar_t *g_timescale; extern bool NPC_IsTrooper( gentity_t *ent ); qboolean NPC_UpdateAngles ( qboolean doPitch, qboolean doYaw ) { #if 1 float error; float decay; float targetPitch = 0; float targetYaw = 0; float yawSpeed; qboolean exact = qtrue; // if angle changes are locked; just keep the current angles // aimTime isn't even set anymore... so this code was never reached, but I need a way to lock NPC's yaw, so instead of making a new SCF_ flag, just use the existing render flag... - dmv if ( !NPC->enemy && ( (level.time < NPCInfo->aimTime) || NPC->client->renderInfo.renderFlags & RF_LOCKEDANGLE) ) { if(doPitch) targetPitch = NPCInfo->lockedDesiredPitch; if(doYaw) targetYaw = NPCInfo->lockedDesiredYaw; } else { // we're changing the lockedDesired Pitch/Yaw below so it's lost it's original meaning, get rid of the lock flag NPC->client->renderInfo.renderFlags &= ~RF_LOCKEDANGLE; if(doPitch) { targetPitch = NPCInfo->desiredPitch; NPCInfo->lockedDesiredPitch = NPCInfo->desiredPitch; } if(doYaw) { targetYaw = NPCInfo->desiredYaw; NPCInfo->lockedDesiredYaw = NPCInfo->desiredYaw; } } if ( NPC->s.weapon == WP_EMPLACED_GUN ) { // FIXME: this seems to do nothing, actually... yawSpeed = 20; } else { if ( NPC->client->NPC_class == CLASS_ROCKETTROOPER && !NPC->enemy ) {//just slowly lookin' around yawSpeed = 1; } else { yawSpeed = NPCInfo->stats.yawSpeed; } } if ( NPC->s.weapon == WP_SABER && NPC->client->ps.forcePowersActive&(1<value; } if (!NPC_IsTrooper(NPC) && NPC->enemy && !G_IsRidingVehicle( NPC ) && NPC->client->NPC_class != CLASS_VEHICLE ) { if (NPC->s.weapon==WP_BLASTER_PISTOL || NPC->s.weapon==WP_BLASTER || NPC->s.weapon==WP_BOWCASTER || NPC->s.weapon==WP_REPEATER || NPC->s.weapon==WP_FLECHETTE || NPC->s.weapon==WP_BRYAR_PISTOL || NPC->s.weapon==WP_NOGHRI_STICK) { yawSpeed *= 10.0f; } } if( doYaw ) { // decay yaw error error = AngleDelta ( NPC->client->ps.viewangles[YAW], targetYaw ); if( fabs(error) > MIN_ANGLE_ERROR ) { if ( error ) { exact = qfalse; decay = 60.0 + yawSpeed * 3; decay *= 50.0f / 1000.0f;//msec if ( error < 0.0 ) { error += decay; if ( error > 0.0 ) { error = 0.0; } } else { error -= decay; if ( error < 0.0 ) { error = 0.0; } } } } ucmd.angles[YAW] = ANGLE2SHORT( targetYaw + error ) - client->ps.delta_angles[YAW]; } //FIXME: have a pitchSpeed? if( doPitch ) { // decay pitch error error = AngleDelta ( NPC->client->ps.viewangles[PITCH], targetPitch ); if ( fabs(error) > MIN_ANGLE_ERROR ) { if ( error ) { exact = qfalse; decay = 60.0 + yawSpeed * 3; decay *= 50.0f / 1000.0f;//msec if ( error < 0.0 ) { error += decay; if ( error > 0.0 ) { error = 0.0; } } else { error -= decay; if ( error < 0.0 ) { error = 0.0; } } } } ucmd.angles[PITCH] = ANGLE2SHORT( targetPitch + error ) - client->ps.delta_angles[PITCH]; } ucmd.angles[ROLL] = ANGLE2SHORT ( NPC->client->ps.viewangles[ROLL] ) - client->ps.delta_angles[ROLL]; if ( exact && Q3_TaskIDPending( NPC, TID_ANGLE_FACE ) ) { Q3_TaskIDComplete( NPC, TID_ANGLE_FACE ); } return exact; #else float error; float decay; float targetPitch = 0; float targetYaw = 0; float yawSpeed; //float runningMod = NPCInfo->currentSpeed/100.0f; qboolean exact = qtrue; qboolean doSound = qfalse; // if angle changes are locked; just keep the current angles if ( level.time < NPCInfo->aimTime ) { if(doPitch) targetPitch = NPCInfo->lockedDesiredPitch; if(doYaw) targetYaw = NPCInfo->lockedDesiredYaw; } else { if(doPitch) targetPitch = NPCInfo->desiredPitch; if(doYaw) targetYaw = NPCInfo->desiredYaw; // NPCInfo->aimTime = level.time + 250; if(doPitch) NPCInfo->lockedDesiredPitch = NPCInfo->desiredPitch; if(doYaw) NPCInfo->lockedDesiredYaw = NPCInfo->desiredYaw; } yawSpeed = NPCInfo->stats.yawSpeed; if(doYaw) { // decay yaw error error = AngleDelta ( NPC->client->ps.viewangles[YAW], targetYaw ); if( fabs(error) > MIN_ANGLE_ERROR ) { /* if(NPC->client->playerTeam == TEAM_BORG&& NPCInfo->behaviorState != BS_FACE&&NPCInfo->tempBehavior!= BS_FACE) {//HACK - borg turn more jittery if ( error ) { exact = qfalse; decay = 60.0 + yawSpeed * 3; decay *= 50.0 / 1000.0;//msec //Snap to if(fabs(error) > 10) { if(Q_flrand(0.0f, 1.0f) > 0.6) { doSound = qtrue; } } if ( error < 0.0)//-10.0 ) { error += decay; if ( error > 0.0 ) { error = 0.0; } } else if ( error > 0.0)//10.0 ) { error -= decay; if ( error < 0.0 ) { error = 0.0; } } } } else*/ if ( error ) { exact = qfalse; decay = 60.0 + yawSpeed * 3; decay *= 50.0 / 1000.0;//msec if ( error < 0.0 ) { error += decay; if ( error > 0.0 ) { error = 0.0; } } else { error -= decay; if ( error < 0.0 ) { error = 0.0; } } } } ucmd.angles[YAW] = ANGLE2SHORT( targetYaw + error ) - client->ps.delta_angles[YAW]; } //FIXME: have a pitchSpeed? if(doPitch) { // decay pitch error error = AngleDelta ( NPC->client->ps.viewangles[PITCH], targetPitch ); if ( fabs(error) > MIN_ANGLE_ERROR ) { /* if(NPC->client->playerTeam == TEAM_BORG&& NPCInfo->behaviorState != BS_FACE&&NPCInfo->tempBehavior!= BS_FACE) {//HACK - borg turn more jittery if ( error ) { exact = qfalse; decay = 60.0 + yawSpeed * 3; decay *= 50.0 / 1000.0;//msec //Snap to if(fabs(error) > 10) { if(Q_flrand(0.0f, 1.0f) > 0.6) { doSound = qtrue; } } if ( error < 0.0)//-10.0 ) { error += decay; if ( error > 0.0 ) { error = 0.0; } } else if ( error > 0.0)//10.0 ) { error -= decay; if ( error < 0.0 ) { error = 0.0; } } } } else*/ if ( error ) { exact = qfalse; decay = 60.0 + yawSpeed * 3; decay *= 50.0 / 1000.0;//msec if ( error < 0.0 ) { error += decay; if ( error > 0.0 ) { error = 0.0; } } else { error -= decay; if ( error < 0.0 ) { error = 0.0; } } } } ucmd.angles[PITCH] = ANGLE2SHORT( targetPitch + error ) - client->ps.delta_angles[PITCH]; } ucmd.angles[ROLL] = ANGLE2SHORT ( NPC->client->ps.viewangles[ROLL] ) - client->ps.delta_angles[ROLL]; /* if(doSound) { G_Sound(NPC, G_SoundIndex(va("sound/enemies/borg/borgservo%d.wav", Q_irand(1, 8)))); } */ return exact; #endif } void NPC_AimWiggle( vec3_t enemy_org ) { //shoot for somewhere between the head and torso //NOTE: yes, I know this looks weird, but it works if ( NPCInfo->aimErrorDebounceTime < level.time ) { NPCInfo->aimOfs[0] = 0.3*Q_flrand(NPC->enemy->mins[0], NPC->enemy->maxs[0]); NPCInfo->aimOfs[1] = 0.3*Q_flrand(NPC->enemy->mins[1], NPC->enemy->maxs[1]); if ( NPC->enemy->maxs[2] > 0 ) { NPCInfo->aimOfs[2] = NPC->enemy->maxs[2]*Q_flrand(0.0f, -1.0f); } } VectorAdd( enemy_org, NPCInfo->aimOfs, enemy_org ); } /* qboolean NPC_UpdateFiringAngles ( qboolean doPitch, qboolean doYaw ) Includes aim when determining angles - so they don't always hit... */ qboolean NPC_UpdateFiringAngles ( qboolean doPitch, qboolean doYaw ) { #if 0 float diff; float error; float targetPitch = 0; float targetYaw = 0; qboolean exact = qtrue; if ( level.time < NPCInfo->aimTime ) { if( doPitch ) targetPitch = NPCInfo->lockedDesiredPitch; if( doYaw ) targetYaw = NPCInfo->lockedDesiredYaw; } else { if( doPitch ) { targetPitch = NPCInfo->desiredPitch; NPCInfo->lockedDesiredPitch = NPCInfo->desiredPitch; } if( doYaw ) { targetYaw = NPCInfo->desiredYaw; NPCInfo->lockedDesiredYaw = NPCInfo->desiredYaw; } } if( doYaw ) { // add yaw error based on NPCInfo->aim value error = ((float)(6 - NPCInfo->stats.aim)) * Q_flrand(-1, 1); if(Q_irand(0, 1)) error *= -1; diff = AngleDelta ( NPC->client->ps.viewangles[YAW], targetYaw ); if ( diff ) exact = qfalse; ucmd.angles[YAW] = ANGLE2SHORT( targetYaw + diff + error ) - client->ps.delta_angles[YAW]; } if( doPitch ) { // add pitch error based on NPCInfo->aim value error = ((float)(6 - NPCInfo->stats.aim)) * Q_flrand(-1, 1); diff = AngleDelta ( NPC->client->ps.viewangles[PITCH], targetPitch ); if ( diff ) exact = qfalse; ucmd.angles[PITCH] = ANGLE2SHORT( targetPitch + diff + error ) - client->ps.delta_angles[PITCH]; } ucmd.angles[ROLL] = ANGLE2SHORT ( NPC->client->ps.viewangles[ROLL] ) - client->ps.delta_angles[ROLL]; return exact; #else float error, diff; float decay; float targetPitch = 0; float targetYaw = 0; qboolean exact = qtrue; // if angle changes are locked; just keep the current angles if ( level.time < NPCInfo->aimTime ) { if(doPitch) targetPitch = NPCInfo->lockedDesiredPitch; if(doYaw) targetYaw = NPCInfo->lockedDesiredYaw; } else { if(doPitch) targetPitch = NPCInfo->desiredPitch; if(doYaw) targetYaw = NPCInfo->desiredYaw; // NPCInfo->aimTime = level.time + 250; if(doPitch) NPCInfo->lockedDesiredPitch = NPCInfo->desiredPitch; if(doYaw) NPCInfo->lockedDesiredYaw = NPCInfo->desiredYaw; } if ( NPCInfo->aimErrorDebounceTime < level.time ) { if ( Q_irand(0, 1 ) ) { NPCInfo->lastAimErrorYaw = ((float)(6 - NPCInfo->stats.aim)) * Q_flrand(-1, 1); } if ( Q_irand(0, 1 ) ) { NPCInfo->lastAimErrorPitch = ((float)(6 - NPCInfo->stats.aim)) * Q_flrand(-1, 1); } NPCInfo->aimErrorDebounceTime = level.time + Q_irand(250, 2000); } if(doYaw) { // decay yaw diff diff = AngleDelta ( NPC->client->ps.viewangles[YAW], targetYaw ); if ( diff) { exact = qfalse; decay = 60.0 + 80.0; decay *= 50.0f / 1000.0f;//msec if ( diff < 0.0 ) { diff += decay; if ( diff > 0.0 ) { diff = 0.0; } } else { diff -= decay; if ( diff < 0.0 ) { diff = 0.0; } } } // add yaw error based on NPCInfo->aim value error = NPCInfo->lastAimErrorYaw; /* if(Q_irand(0, 1)) { error *= -1; } */ ucmd.angles[YAW] = ANGLE2SHORT( targetYaw + diff + error ) - client->ps.delta_angles[YAW]; } if(doPitch) { // decay pitch diff diff = AngleDelta ( NPC->client->ps.viewangles[PITCH], targetPitch ); if ( diff) { exact = qfalse; decay = 60.0 + 80.0; decay *= 50.0f / 1000.0f;//msec if ( diff < 0.0 ) { diff += decay; if ( diff > 0.0 ) { diff = 0.0; } } else { diff -= decay; if ( diff < 0.0 ) { diff = 0.0; } } } error = NPCInfo->lastAimErrorPitch; ucmd.angles[PITCH] = ANGLE2SHORT( targetPitch + diff + error ) - client->ps.delta_angles[PITCH]; } ucmd.angles[ROLL] = ANGLE2SHORT ( NPC->client->ps.viewangles[ROLL] ) - client->ps.delta_angles[ROLL]; return exact; #endif } //=================================================================================== /* static void NPC_UpdateShootAngles (vec3_t angles, qboolean doPitch, qboolean doYaw ) Does update angles on shootAngles */ void NPC_UpdateShootAngles (vec3_t angles, qboolean doPitch, qboolean doYaw ) {//FIXME: shoot angles either not set right or not used! float error; float decay; float targetPitch = 0; float targetYaw = 0; if(doPitch) targetPitch = angles[PITCH]; if(doYaw) targetYaw = angles[YAW]; if(doYaw) { // decay yaw error error = AngleDelta ( NPCInfo->shootAngles[YAW], targetYaw ); if ( error ) { decay = 60.0 + 80.0 * NPCInfo->stats.aim; decay *= 100.0f / 1000.0f;//msec if ( error < 0.0 ) { error += decay; if ( error > 0.0 ) { error = 0.0; } } else { error -= decay; if ( error < 0.0 ) { error = 0.0; } } } NPCInfo->shootAngles[YAW] = targetYaw + error; } if(doPitch) { // decay pitch error error = AngleDelta ( NPCInfo->shootAngles[PITCH], targetPitch ); if ( error ) { decay = 60.0 + 80.0 * NPCInfo->stats.aim; decay *= 100.0f / 1000.0f;//msec if ( error < 0.0 ) { error += decay; if ( error > 0.0 ) { error = 0.0; } } else { error -= decay; if ( error < 0.0 ) { error = 0.0; } } } NPCInfo->shootAngles[PITCH] = targetPitch + error; } } /* void SetTeamNumbers (void) Sets the number of living clients on each team FIXME: Does not account for non-respawned players! FIXME: Don't include medics? */ void SetTeamNumbers (void) { gentity_t *found; int i; for( i = 0; i < TEAM_NUM_TEAMS; i++ ) { teamNumbers[i] = 0; teamStrength[i] = 0; } for( i = 0; i < 1 ; i++ ) { found = &g_entities[i]; if( found->client ) { if( found->health > 0 )//FIXME: or if a player! { teamNumbers[found->client->playerTeam]++; teamStrength[found->client->playerTeam] += found->health; } } } for( i = 0; i < TEAM_NUM_TEAMS; i++ ) {//Get the average health teamStrength[i] = floor( ((float)(teamStrength[i])) / ((float)(teamNumbers[i])) ); } } extern stringID_table_t BSTable[]; extern stringID_table_t BSETTable[]; qboolean G_ActivateBehavior (gentity_t *self, int bset ) { bState_t bSID = (bState_t)-1; char *bs_name = NULL; if ( !self ) { return qfalse; } bs_name = self->behaviorSet[bset]; if( !(VALIDSTRING( bs_name )) ) { return qfalse; } if ( self->NPC ) { bSID = (bState_t)(GetIDForString( BSTable, bs_name )); } if(bSID != (bState_t)-1) { self->NPC->tempBehavior = BS_DEFAULT; self->NPC->behaviorState = bSID; if ( bSID == BS_SEARCH || bSID == BS_WANDER ) { //FIXME: Reimplement? if( self->waypoint != WAYPOINT_NONE ) { NPC_BSSearchStart( self->waypoint, bSID ); } else { self->waypoint = NAV::GetNearestNode(self); if( self->waypoint != WAYPOINT_NONE ) { NPC_BSSearchStart( self->waypoint, bSID ); } } } } else { Quake3Game()->DebugPrint( IGameInterface::WL_VERBOSE, "%s attempting to run bSet %s (%s)\n", self->targetname, GetStringForID( BSETTable, bset ), bs_name ); Quake3Game()->RunScript( self, bs_name ); } return qtrue; } /* ============================================================================= Extended Functions ============================================================================= */ /* ------------------------- NPC_ValidEnemy ------------------------- */ qboolean G_ValidEnemy( gentity_t *self, gentity_t *enemy ) { //Must be a valid pointer if ( enemy == NULL ) return qfalse; //Must not be me if ( enemy == self ) return qfalse; //Must not be deleted if ( enemy->inuse == qfalse ) return qfalse; //Must be alive if ( enemy->health <= 0 ) return qfalse; //In case they're in notarget mode if ( enemy->flags & FL_NOTARGET ) return qfalse; //Must be an NPC if ( enemy->client == NULL ) { if ( enemy->svFlags&SVF_NONNPC_ENEMY ) {//still potentially valid if (self->client) { if ( enemy->noDamageTeam == self->client->playerTeam ) { return qfalse; } else { return qtrue; } } else { if ( enemy->noDamageTeam == self->noDamageTeam ) { return qfalse; } else { return qtrue; } } } else { return qfalse; } } if ( enemy->client->playerTeam == TEAM_FREE && enemy->s.number < MAX_CLIENTS ) {//An evil player, everyone attacks him return qtrue; } //Can't be on the same team if ( enemy->client->playerTeam == self->client->playerTeam ) { return qfalse; } //if haven't seen him in a while, give up //if ( NPCInfo->enemyLastSeenTime != 0 && level.time - NPCInfo->enemyLastSeenTime > 7000 )//FIXME: make a stat? // return qfalse; if ( enemy->client->playerTeam == self->client->enemyTeam //simplest case: they're on my enemy team || (self->client->enemyTeam == TEAM_FREE && enemy->client->NPC_class != self->client->NPC_class )//I get mad at anyone and this guy isn't the same class as me || (enemy->client->NPC_class == CLASS_WAMPA && enemy->enemy )//a rampaging wampa || (enemy->client->NPC_class == CLASS_RANCOR && enemy->enemy )//a rampaging rancor || (enemy->client->playerTeam == TEAM_FREE && enemy->client->enemyTeam == TEAM_FREE && enemy->enemy && enemy->enemy->client && (enemy->enemy->client->playerTeam == self->client->playerTeam||(enemy->enemy->client->playerTeam != TEAM_ENEMY&&self->client->playerTeam==TEAM_PLAYER))) //enemy is a rampaging non-aligned creature who is attacking someone on our team or a non-enemy (this last condition is used only if we're a good guy - in effect, we protect the innocent) ) { return qtrue; } //all other cases = false? return qfalse; } qboolean NPC_ValidEnemy( gentity_t *ent ) { return G_ValidEnemy( NPC, ent ); } /* ------------------------- NPC_TargetVisible ------------------------- */ qboolean NPC_TargetVisible( gentity_t *ent ) { //Make sure we're in a valid range if ( DistanceSquared( ent->currentOrigin, NPC->currentOrigin ) > ( NPCInfo->stats.visrange * NPCInfo->stats.visrange ) ) return qfalse; //Check our FOV if ( InFOV( ent, NPC, NPCInfo->stats.hfov, NPCInfo->stats.vfov ) == qfalse ) return qfalse; //Check for sight if ( NPC_ClearLOS( ent ) == qfalse ) return qfalse; return qtrue; } /* ------------------------- NPC_GetCheckDelta ------------------------- */ /* #define CHECK_TIME_BASE 250 #define CHECK_TIME_BASE_SQUARED ( CHECK_TIME_BASE * CHECK_TIME_BASE ) static int NPC_GetCheckDelta( void ) { if ( NPC_ValidEnemy( NPC->enemy ) == qfalse ) { int distance = DistanceSquared( NPC->currentOrigin, g_entities[0].currentOrigin ); distance /= CHECK_TIME_BASE_SQUARED; return ( CHECK_TIME_BASE * distance ); } return 0; } */ /* ------------------------- NPC_FindNearestEnemy ------------------------- */ #define MAX_RADIUS_ENTS 256 //NOTE: This can cause entities to be lost #define NEAR_DEFAULT_RADIUS 256 extern gentity_t *G_CheckControlledTurretEnemy(gentity_t *self, gentity_t *enemy, qboolean validate ); int NPC_FindNearestEnemy( gentity_t *ent ) { gentity_t *radiusEnts[ MAX_RADIUS_ENTS ]; gentity_t *nearest; vec3_t mins, maxs; int nearestEntID = -1; float nearestDist = (float)WORLD_SIZE*(float)WORLD_SIZE; float distance; int numEnts, numChecks = 0; int i; //Setup the bbox to search in for ( i = 0; i < 3; i++ ) { mins[i] = ent->currentOrigin[i] - NPCInfo->stats.visrange; maxs[i] = ent->currentOrigin[i] + NPCInfo->stats.visrange; } //Get a number of entities in a given space numEnts = gi.EntitiesInBox( mins, maxs, radiusEnts, MAX_RADIUS_ENTS ); for ( i = 0; i < numEnts; i++ ) { nearest = G_CheckControlledTurretEnemy(ent, radiusEnts[i], qtrue); //Don't consider self if ( nearest == ent ) continue; //Must be valid if ( NPC_ValidEnemy( nearest ) == qfalse ) continue; numChecks++; //Must be visible if ( NPC_TargetVisible( nearest ) == qfalse ) continue; distance = DistanceSquared( ent->currentOrigin, nearest->currentOrigin ); //Found one closer to us if ( distance < nearestDist ) { nearestEntID = nearest->s.number; nearestDist = distance; } } return nearestEntID; } /* ------------------------- NPC_PickEnemyExt ------------------------- */ gentity_t *NPC_PickEnemyExt( qboolean checkAlerts = qfalse ) { //If we've asked for the closest enemy int entID = NPC_FindNearestEnemy( NPC ); //If we have a valid enemy, use it if ( entID >= 0 ) return &g_entities[entID]; if ( checkAlerts ) { int alertEvent = NPC_CheckAlertEvents( qtrue, qtrue, -1, qtrue, AEL_DISCOVERED ); //There is an event to look at if ( alertEvent >= 0 ) { alertEvent_t *event = &level.alertEvents[alertEvent]; //Don't pay attention to our own alerts if ( event->owner == NPC ) return NULL; if ( event->level >= AEL_DISCOVERED ) { //If it's the player, attack him if ( event->owner == &g_entities[0] ) return event->owner; //If it's on our team, then take its enemy as well if ( ( event->owner->client ) && ( event->owner->client->playerTeam == NPC->client->playerTeam ) ) return event->owner->enemy; } } } return NULL; } /* ------------------------- NPC_FindPlayer ------------------------- */ qboolean NPC_FindPlayer( void ) { return NPC_TargetVisible( &g_entities[0] ); } /* ------------------------- NPC_CheckPlayerDistance ------------------------- */ static qboolean NPC_CheckPlayerDistance( void ) { //Make sure we have an enemy if ( NPC->enemy == NULL ) return qfalse; //Only do this for non-players if ( NPC->enemy->s.number == 0 ) return qfalse; //must be set up to get mad at player if ( !NPC->client || NPC->client->enemyTeam != TEAM_PLAYER ) return qfalse; //Must be within our FOV if ( InFOV( &g_entities[0], NPC, NPCInfo->stats.hfov, NPCInfo->stats.vfov ) == qfalse ) return qfalse; float distance = DistanceSquared( NPC->currentOrigin, NPC->enemy->currentOrigin ); if ( distance > DistanceSquared( NPC->currentOrigin, g_entities[0].currentOrigin ) ) { G_SetEnemy( NPC, &g_entities[0] ); return qtrue; } return qfalse; } /* ------------------------- NPC_FindEnemy ------------------------- */ qboolean NPC_FindEnemy( qboolean checkAlerts = qfalse ) { //We're ignoring all enemies for now if( NPC->svFlags & SVF_IGNORE_ENEMIES ) { G_ClearEnemy( NPC ); return qfalse; } //we can't pick up any enemies for now if( NPCInfo->confusionTime > level.time ) { G_ClearEnemy( NPC ); return qfalse; } //Don't want a new enemy if ( ( NPC_ValidEnemy( NPC->enemy ) ) && ( NPC->svFlags & SVF_LOCKEDENEMY ) ) return qtrue; //See if the player is closer than our current enemy if ( NPC->client->NPC_class != CLASS_RANCOR && NPC->client->NPC_class != CLASS_WAMPA && NPC->client->NPC_class != CLASS_SAND_CREATURE && NPC_CheckPlayerDistance() ) {//rancors, wampas & sand creatures don't care if player is closer, they always go with closest return qtrue; } //Otherwise, turn off the flag NPC->svFlags &= ~SVF_LOCKEDENEMY; //If we've gotten here alright, then our target it still valid if ( NPC_ValidEnemy( NPC->enemy ) ) return qtrue; gentity_t *newenemy = NPC_PickEnemyExt( checkAlerts ); //if we found one, take it as the enemy if( NPC_ValidEnemy( newenemy ) ) { G_SetEnemy( NPC, newenemy ); return qtrue; } G_ClearEnemy( NPC ); return qfalse; } /* ------------------------- NPC_CheckEnemyExt ------------------------- */ qboolean NPC_CheckEnemyExt( qboolean checkAlerts ) { //Make sure we're ready to think again /* if ( NPCInfo->enemyCheckDebounceTime > level.time ) return qfalse; //Get our next think time NPCInfo->enemyCheckDebounceTime = level.time + NPC_GetCheckDelta(); //Attempt to find an enemy return NPC_FindEnemy(); */ return NPC_FindEnemy( checkAlerts ); } /* ------------------------- NPC_FacePosition ------------------------- */ qboolean NPC_FacePosition( vec3_t position, qboolean doPitch ) { vec3_t muzzle; qboolean facing = qtrue; //Get the positions if ( NPC->client && (NPC->client->NPC_class == CLASS_RANCOR || NPC->client->NPC_class == CLASS_WAMPA || NPC->client->NPC_class == CLASS_SAND_CREATURE) ) { CalcEntitySpot( NPC, SPOT_ORIGIN, muzzle ); muzzle[2] += NPC->maxs[2] * 0.75f; } else if ( NPC->client && NPC->client->NPC_class == CLASS_GALAKMECH ) { CalcEntitySpot( NPC, SPOT_WEAPON, muzzle ); } else { CalcEntitySpot( NPC, SPOT_HEAD_LEAN, muzzle );//SPOT_HEAD if ( NPC->client->NPC_class == CLASS_ROCKETTROOPER ) {//*sigh*, look down more position[2] -= 32; } } //Find the desired angles vec3_t angles; GetAnglesForDirection( muzzle, position, angles ); NPCInfo->desiredYaw = AngleNormalize360( angles[YAW] ); NPCInfo->desiredPitch = AngleNormalize360( angles[PITCH] ); if ( NPC->enemy && NPC->enemy->client && NPC->enemy->client->NPC_class == CLASS_ATST ) { // FIXME: this is kind of dumb, but it was the easiest way to get it to look sort of ok NPCInfo->desiredYaw += Q_flrand( -5, 5 ) + sin( level.time * 0.004f ) * 7; NPCInfo->desiredPitch += Q_flrand( -2, 2 ); } //Face that yaw NPC_UpdateAngles( qtrue, qtrue ); //Find the delta between our goal and our current facing float yawDelta = AngleNormalize360( NPCInfo->desiredYaw - ( SHORT2ANGLE( ucmd.angles[YAW] + client->ps.delta_angles[YAW] ) ) ); //See if we are facing properly if ( fabs( yawDelta ) > VALID_ATTACK_CONE ) facing = qfalse; if ( doPitch ) { //Find the delta between our goal and our current facing float currentAngles = ( SHORT2ANGLE( ucmd.angles[PITCH] + client->ps.delta_angles[PITCH] ) ); float pitchDelta = NPCInfo->desiredPitch - currentAngles; //See if we are facing properly if ( fabs( pitchDelta ) > VALID_ATTACK_CONE ) facing = qfalse; } return facing; } /* ------------------------- NPC_FaceEntity ------------------------- */ qboolean NPC_FaceEntity( gentity_t *ent, qboolean doPitch ) { vec3_t entPos; //Get the positions CalcEntitySpot( ent, SPOT_HEAD_LEAN, entPos ); return NPC_FacePosition( entPos, doPitch ); } /* ------------------------- NPC_FaceEnemy ------------------------- */ qboolean NPC_FaceEnemy( qboolean doPitch ) { if ( NPC == NULL ) return qfalse; if ( NPC->enemy == NULL ) return qfalse; return NPC_FaceEntity( NPC->enemy, doPitch ); } /* ------------------------- NPC_CheckCanAttackExt ------------------------- */ qboolean NPC_CheckCanAttackExt( void ) { //We don't want them to shoot if( NPCInfo->scriptFlags & SCF_DONT_FIRE ) return qfalse; //Turn to face if ( NPC_FaceEnemy( qtrue ) == qfalse ) return qfalse; //Must have a clear line of sight to the target if ( NPC_ClearShot( NPC->enemy ) == qfalse ) return qfalse; return qtrue; } /* ------------------------- NPC_ClearLookTarget ------------------------- */ void NPC_ClearLookTarget( gentity_t *self ) { if ( !self->client ) { return; } self->client->renderInfo.lookTarget = ENTITYNUM_NONE;//ENTITYNUM_WORLD; self->client->renderInfo.lookTargetClearTime = 0; } /* ------------------------- NPC_SetLookTarget ------------------------- */ void NPC_SetLookTarget( gentity_t *self, int entNum, int clearTime ) { if ( !self->client ) { return; } self->client->renderInfo.lookTarget = entNum; self->client->renderInfo.lookTargetClearTime = clearTime; } /* ------------------------- NPC_CheckLookTarget ------------------------- */ qboolean NPC_CheckLookTarget( gentity_t *self ) { if ( self->client ) { if ( self->client->renderInfo.lookTarget >= 0 && self->client->renderInfo.lookTarget < ENTITYNUM_WORLD ) {//within valid range if ( (&g_entities[self->client->renderInfo.lookTarget] == NULL) || !g_entities[self->client->renderInfo.lookTarget].inuse ) {//lookTarget not inuse or not valid anymore NPC_ClearLookTarget( self ); } else if ( self->client->renderInfo.lookTargetClearTime && self->client->renderInfo.lookTargetClearTime < level.time ) {//Time to clear lookTarget NPC_ClearLookTarget( self ); } else if ( g_entities[self->client->renderInfo.lookTarget].client && self->enemy && (&g_entities[self->client->renderInfo.lookTarget] != self->enemy) ) {//should always look at current enemy if engaged in battle... FIXME: this could override certain scripted lookTargets...??? NPC_ClearLookTarget( self ); } else { return qtrue; } } } return qfalse; } /* ------------------------- NPC_CheckCharmed ------------------------- */ extern void G_AddVoiceEvent( gentity_t *self, int event, int speakDebounceTime ); void G_CheckCharmed( gentity_t *self ) { if ( self && self->client && self->client->playerTeam == TEAM_PLAYER && self->NPC && self->NPC->charmedTime && (self->NPC->charmedTime < level.time ||self->health <= 0) ) {//we were charmed, set us back! //NOTE: presumptions here... team_t savTeam = self->client->enemyTeam; self->client->enemyTeam = self->client->playerTeam; self->client->playerTeam = savTeam; self->client->leader = NULL; self->NPC->charmedTime = 0; if ( self->health > 0 ) { if ( self->NPC->tempBehavior == BS_FOLLOW_LEADER ) { self->NPC->tempBehavior = BS_DEFAULT; } G_ClearEnemy( self ); //say something to let player know you've snapped out of it G_AddVoiceEvent( self, Q_irand(EV_CONFUSE1, EV_CONFUSE3), 2000 ); } } } void G_GetBoltPosition( gentity_t *self, int boltIndex, vec3_t pos, int modelIndex = 0 ) { if ( !self || !self->ghoul2.size() ) { return; } mdxaBone_t boltMatrix; vec3_t result, angles={0,self->currentAngles[YAW],0}; gi.G2API_GetBoltMatrix( self->ghoul2, modelIndex, boltIndex, &boltMatrix, angles, self->currentOrigin, (cg.time?cg.time:level.time), NULL, self->s.modelScale ); if ( pos ) { gi.G2API_GiveMeVectorFromMatrix( boltMatrix, ORIGIN, result ); VectorCopy( result, pos ); } } float NPC_EntRangeFromBolt( gentity_t *targEnt, int boltIndex ) { vec3_t org = { 0.0f }; if ( !targEnt ) { return Q3_INFINITE; } G_GetBoltPosition( NPC, boltIndex, org ); return (Distance( targEnt->currentOrigin, org )); } float NPC_EnemyRangeFromBolt( int boltIndex ) { return (NPC_EntRangeFromBolt( NPC->enemy, boltIndex )); } int G_GetEntsNearBolt( gentity_t *self, gentity_t **radiusEnts, float radius, int boltIndex, vec3_t boltOrg ) { vec3_t mins, maxs; int i; //get my handRBolt's position vec3_t org = { 0.0f }; G_GetBoltPosition( self, boltIndex, org ); VectorCopy( org, boltOrg ); //Setup the bbox to search in for ( i = 0; i < 3; i++ ) { mins[i] = boltOrg[i] - radius; maxs[i] = boltOrg[i] + radius; } //Get the number of entities in a given space return (gi.EntitiesInBox( mins, maxs, radiusEnts, 128 )); } int NPC_GetEntsNearBolt( gentity_t **radiusEnts, float radius, int boltIndex, vec3_t boltOrg ) { return (G_GetEntsNearBolt( NPC, radiusEnts, radius, boltIndex, boltOrg )); } extern qboolean RT_Flying( gentity_t *self ); extern void RT_FlyStart( gentity_t *self ); extern void RT_FlyStop( gentity_t *self ); extern qboolean Boba_Flying( gentity_t *self ); extern void Boba_FlyStart( gentity_t *self ); extern void Boba_FlyStop( gentity_t *self ); qboolean JET_Flying( gentity_t *self ) { if ( !self || !self->client ) { return qfalse; } if ( self->client->NPC_class == CLASS_BOBAFETT ) { return (Boba_Flying(self)); } else if ( self->client->NPC_class == CLASS_ROCKETTROOPER ) { return (RT_Flying(self)); } else { return qfalse; } } void JET_FlyStart( gentity_t *self ) { if ( !self || !self->client ) { return; } self->lastInAirTime = level.time; if ( self->client->NPC_class == CLASS_BOBAFETT ) { Boba_FlyStart( self ); } else if ( self->client->NPC_class == CLASS_ROCKETTROOPER ) { RT_FlyStart( self ); } } void JET_FlyStop( gentity_t *self ) { if ( !self || !self->client ) { return; } if ( self->client->NPC_class == CLASS_BOBAFETT ) { Boba_FlyStop( self ); } else if ( self->client->NPC_class == CLASS_ROCKETTROOPER ) { RT_FlyStop( self ); } }