void RT_FireDecide( void ) { qboolean enemyLOS = qfalse; qboolean enemyCS = qfalse; qboolean enemyInFOV = qfalse; //qboolean move = qtrue; qboolean faceEnemy = qfalse; qboolean shoot = qfalse; qboolean hitAlly = qfalse; vec3_t impactPos; float enemyDist; if ( NPC->client->ps.groundEntityNum == ENTITYNUM_NONE && NPC->client->ps.forceJumpZStart && !PM_FlippingAnim( NPC->client->ps.legsAnim ) && !Q_irand( 0, 10 ) ) {//take off RT_FlyStart( NPC ); } if ( !NPC->enemy ) { return; } VectorClear( impactPos ); enemyDist = DistanceSquared( NPC->currentOrigin, NPC->enemy->currentOrigin ); vec3_t enemyDir, shootDir; VectorSubtract( NPC->enemy->currentOrigin, NPC->currentOrigin, enemyDir ); VectorNormalize( enemyDir ); AngleVectors( NPC->client->ps.viewangles, shootDir, NULL, NULL ); float dot = DotProduct( enemyDir, shootDir ); if ( dot > 0.5f ||( enemyDist * (1.0f-dot)) < 10000 ) {//enemy is in front of me or they're very close and not behind me enemyInFOV = qtrue; } if ( enemyDist < MIN_ROCKET_DIST_SQUARED )//128 {//enemy within 128 if ( (NPC->client->ps.weapon == WP_FLECHETTE || NPC->client->ps.weapon == WP_REPEATER) && (NPCInfo->scriptFlags & SCF_ALT_FIRE) ) {//shooting an explosive, but enemy too close, switch to primary fire NPCInfo->scriptFlags &= ~SCF_ALT_FIRE; //FIXME: we can never go back to alt-fire this way since, after this, we don't know if we were initially supposed to use alt-fire or not... } } //can we see our target? if ( TIMER_Done( NPC, "nextAttackDelay" ) && TIMER_Done( NPC, "flameTime" ) ) { if ( NPC_ClearLOS( NPC->enemy ) ) { NPCInfo->enemyLastSeenTime = level.time; enemyLOS = qtrue; if ( NPC->client->ps.weapon == WP_NONE ) { enemyCS = qfalse;//not true, but should stop us from firing } else {//can we shoot our target? if ( (NPC->client->ps.weapon == WP_ROCKET_LAUNCHER || (NPC->client->ps.weapon == WP_CONCUSSION && !(NPCInfo->scriptFlags&SCF_ALT_FIRE)) || (NPC->client->ps.weapon == WP_FLECHETTE && (NPCInfo->scriptFlags&SCF_ALT_FIRE))) && enemyDist < MIN_ROCKET_DIST_SQUARED )//128*128 { enemyCS = qfalse;//not true, but should stop us from firing hitAlly = qtrue;//us! //FIXME: if too close, run away! } else if ( enemyInFOV ) {//if enemy is FOV, go ahead and check for shooting int hit = NPC_ShotEntity( NPC->enemy, impactPos ); gentity_t *hitEnt = &g_entities[hit]; if ( hit == NPC->enemy->s.number || ( hitEnt && hitEnt->client && hitEnt->client->playerTeam == NPC->client->enemyTeam ) || ( hitEnt && hitEnt->takedamage && ((hitEnt->svFlags&SVF_GLASS_BRUSH)||hitEnt->health < 40||NPC->s.weapon == WP_EMPLACED_GUN) ) ) {//can hit enemy or enemy ally or will hit glass or other minor breakable (or in emplaced gun), so shoot anyway enemyCS = qtrue; //NPC_AimAdjust( 2 );//adjust aim better longer we have clear shot at enemy VectorCopy( NPC->enemy->currentOrigin, NPCInfo->enemyLastSeenLocation ); } else {//Hmm, have to get around this bastard //NPC_AimAdjust( 1 );//adjust aim better longer we can see enemy if ( hitEnt && hitEnt->client && hitEnt->client->playerTeam == NPC->client->playerTeam ) {//would hit an ally, don't fire!!! hitAlly = qtrue; } else {//Check and see where our shot *would* hit... if it's not close to the enemy (within 256?), then don't fire } } } else { enemyCS = qfalse;//not true, but should stop us from firing } } } else if ( gi.inPVS( NPC->enemy->currentOrigin, NPC->currentOrigin ) ) { NPCInfo->enemyLastSeenTime = level.time; faceEnemy = qtrue; //NPC_AimAdjust( -1 );//adjust aim worse longer we cannot see enemy } if ( NPC->client->ps.weapon == WP_NONE ) { faceEnemy = qfalse; shoot = qfalse; } else { if ( enemyLOS ) {//FIXME: no need to face enemy if we're moving to some other goal and he's too far away to shoot? faceEnemy = qtrue; } if ( enemyCS ) { shoot = qtrue; } } if ( !enemyCS ) {//if have a clear shot, always try //See if we should continue to fire on their last position //!TIMER_Done( NPC, "stick" ) || if ( !hitAlly //we're not going to hit an ally && enemyInFOV //enemy is in our FOV //FIXME: or we don't have a clear LOS? && NPCInfo->enemyLastSeenTime > 0 )//we've seen the enemy { if ( level.time - NPCInfo->enemyLastSeenTime < 10000 )//we have seem the enemy in the last 10 seconds { if ( !Q_irand( 0, 10 ) ) { //Fire on the last known position vec3_t muzzle, dir, angles; qboolean tooClose = qfalse; qboolean tooFar = qfalse; CalcEntitySpot( NPC, SPOT_HEAD, muzzle ); if ( VectorCompare( impactPos, vec3_origin ) ) {//never checked ShotEntity this frame, so must do a trace... trace_t tr; //vec3_t mins = {-2,-2,-2}, maxs = {2,2,2}; vec3_t forward, end; AngleVectors( NPC->client->ps.viewangles, forward, NULL, NULL ); VectorMA( muzzle, 8192, forward, end ); gi.trace( &tr, muzzle, vec3_origin, vec3_origin, end, NPC->s.number, MASK_SHOT ); VectorCopy( tr.endpos, impactPos ); } //see if impact would be too close to me float distThreshold = 16384/*128*128*/;//default switch ( NPC->s.weapon ) { case WP_ROCKET_LAUNCHER: case WP_FLECHETTE: case WP_THERMAL: case WP_TRIP_MINE: case WP_DET_PACK: distThreshold = 65536/*256*256*/; break; case WP_REPEATER: if ( NPCInfo->scriptFlags&SCF_ALT_FIRE ) { distThreshold = 65536/*256*256*/; } break; case WP_CONCUSSION: if ( !(NPCInfo->scriptFlags&SCF_ALT_FIRE) ) { distThreshold = 65536/*256*256*/; } break; default: break; } float dist = DistanceSquared( impactPos, muzzle ); if ( dist < distThreshold ) {//impact would be too close to me tooClose = qtrue; } else if ( level.time - NPCInfo->enemyLastSeenTime > 5000 || (NPCInfo->group && level.time - NPCInfo->group->lastSeenEnemyTime > 5000 )) {//we've haven't seen them in the last 5 seconds //see if it's too far from where he is distThreshold = 65536/*256*256*/;//default switch ( NPC->s.weapon ) { case WP_ROCKET_LAUNCHER: case WP_FLECHETTE: case WP_THERMAL: case WP_TRIP_MINE: case WP_DET_PACK: distThreshold = 262144/*512*512*/; break; case WP_REPEATER: if ( NPCInfo->scriptFlags&SCF_ALT_FIRE ) { distThreshold = 262144/*512*512*/; } break; case WP_CONCUSSION: if ( !(NPCInfo->scriptFlags&SCF_ALT_FIRE) ) { distThreshold = 262144/*512*512*/; } break; default: break; } dist = DistanceSquared( impactPos, NPCInfo->enemyLastSeenLocation ); if ( dist > distThreshold ) {//impact would be too far from enemy tooFar = qtrue; } } if ( !tooClose && !tooFar ) {//okay too shoot at last pos VectorSubtract( NPCInfo->enemyLastSeenLocation, muzzle, dir ); VectorNormalize( dir ); vectoangles( dir, angles ); NPCInfo->desiredYaw = angles[YAW]; NPCInfo->desiredPitch = angles[PITCH]; shoot = qtrue; faceEnemy = qfalse; } } } } } //FIXME: don't shoot right away! if ( NPC->client->fireDelay ) { if ( NPC->s.weapon == WP_ROCKET_LAUNCHER || (NPC->s.weapon == WP_CONCUSSION&&!(NPCInfo->scriptFlags&SCF_ALT_FIRE)) ) { if ( !enemyLOS || !enemyCS ) {//cancel it NPC->client->fireDelay = 0; } else {//delay our next attempt TIMER_Set( NPC, "nextAttackDelay", Q_irand( 1000, 3000 ) );//FIXME: base on g_spskill } } } else if ( shoot ) {//try to shoot if it's time if ( TIMER_Done( NPC, "nextAttackDelay" ) ) { if( !(NPCInfo->scriptFlags & SCF_FIRE_WEAPON) ) // we've already fired, no need to do it again here { WeaponThink( qtrue ); } //NASTY int altChance = 6;//FIXME: base on g_spskill if ( NPC->s.weapon == WP_ROCKET_LAUNCHER ) { if ( (ucmd.buttons&BUTTON_ATTACK) && !Q_irand( 0, altChance ) ) {//every now and then, shoot a homing rocket ucmd.buttons &= ~BUTTON_ATTACK; ucmd.buttons |= BUTTON_ALT_ATTACK; NPC->client->fireDelay = Q_irand( 1000, 3000 );//FIXME: base on g_spskill } } else if ( NPC->s.weapon == WP_CONCUSSION ) { if ( (ucmd.buttons&BUTTON_ATTACK) && Q_irand( 0, altChance*5 ) ) {//fire the beam shot ucmd.buttons &= ~BUTTON_ATTACK; ucmd.buttons |= BUTTON_ALT_ATTACK; TIMER_Set( NPC, "nextAttackDelay", Q_irand( 1500, 2500 ) );//FIXME: base on g_spskill } else {//fire the rocket-like shot TIMER_Set( NPC, "nextAttackDelay", Q_irand( 3000, 5000 ) );//FIXME: base on g_spskill } } } } } }
void NPC_ChoosePainAnimation( gentity_t *self, gentity_t *other, const vec3_t point, int damage, int mod, int hitLoc, int voiceEvent = -1 ) { //If we've already taken pain, then don't take it again if ( level.time < self->painDebounceTime && mod != MOD_ELECTROCUTE && mod != MOD_MELEE ) {//FIXME: if hit while recoving from losing a saber lock, we should still play a pain anim? return; } int pain_anim = -1; float pain_chance; if ( self->s.weapon == WP_THERMAL && self->client->fireDelay > 0 ) {//don't interrupt thermal throwing anim return; } else if (self->client->ps.powerups[PW_GALAK_SHIELD]) { return; } else if ( self->client->NPC_class == CLASS_GALAKMECH ) { if ( hitLoc == HL_GENERIC1 ) {//hit the antenna! pain_chance = 1.0f; self->s.powerups |= ( 1 << PW_SHOCKED ); self->client->ps.powerups[PW_SHOCKED] = level.time + Q_irand( 500, 2500 ); } else if ( self->client->ps.powerups[PW_GALAK_SHIELD] ) {//shield up return; } else if ( self->health > 200 && damage < 100 ) {//have a *lot* of health pain_chance = 0.05f; } else {//the lower my health and greater the damage, the more likely I am to play a pain anim pain_chance = (200.0f-self->health)/100.0f + damage/50.0f; } } else if ( self->client && self->client->playerTeam == TEAM_PLAYER && other && !other->s.number ) {//ally shot by player always complains pain_chance = 1.1f; } else { if ( other && (other->s.weapon == WP_SABER || mod == MOD_ELECTROCUTE || mod == MOD_CRUSH/*FIXME:MOD_FORCE_GRIP*/) ) { if ( self->client->ps.weapon == WP_SABER && other->s.number < MAX_CLIENTS ) {//hmm, shouldn't *always* react to damage from player if I have a saber pain_chance = 1.05f - ((self->NPC->rank)/(float)RANK_CAPTAIN); } else { pain_chance = 1.0f;//always take pain from saber } } else if ( mod == MOD_GAS ) { pain_chance = 1.0f; } else if ( mod == MOD_MELEE ) {//higher in rank (skill) we are, less likely we are to be fazed by a punch pain_chance = 1.0f - ((RANK_CAPTAIN-self->NPC->rank)/(float)RANK_CAPTAIN); } else if ( self->client->NPC_class == CLASS_PROTOCOL ) { pain_chance = 1.0f; } else { pain_chance = NPC_GetPainChance( self, damage ); } if ( self->client->NPC_class == CLASS_DESANN ) { pain_chance *= 0.5f; } } //See if we're going to flinch if ( Q_flrand(0.0f, 1.0f) < pain_chance ) { //Pick and play our animation if ( (self->client->ps.eFlags&EF_FORCE_GRIPPED) ) { G_AddVoiceEvent( self, Q_irand(EV_CHOKE1, EV_CHOKE3), 0 ); } else if ( mod == MOD_GAS ) { //SIGH... because our choke sounds are inappropriately long, I have to debounce them in code! if ( TIMER_Done( self, "gasChokeSound" ) ) { TIMER_Set( self, "gasChokeSound", Q_irand( 1000, 2000 ) ); G_AddVoiceEvent( self, Q_irand(EV_CHOKE1, EV_CHOKE3), 0 ); } } else if ( (self->client->ps.eFlags&EF_FORCE_DRAINED) ) { NPC_SetPainEvent( self ); } else {//not being force-gripped or force-drained if ( G_CheckForStrongAttackMomentum( self ) || PM_SpinningAnim( self->client->ps.legsAnim ) || PM_SaberInSpecialAttack( self->client->ps.torsoAnim ) || PM_InKnockDown( &self->client->ps ) || PM_RollingAnim( self->client->ps.legsAnim ) || (PM_FlippingAnim( self->client->ps.legsAnim )&&!PM_InCartwheel( self->client->ps.legsAnim )) ) {//strong attacks, rolls, knockdowns, flips and spins cannot be interrupted by pain return; } else {//play an anim if ( self->client->NPC_class == CLASS_GALAKMECH ) {//only has 1 for now //FIXME: never plays this, it seems... pain_anim = BOTH_PAIN1; } else if ( mod == MOD_MELEE ) { pain_anim = PM_PickAnim( self, BOTH_PAIN2, BOTH_PAIN3 ); } else if ( self->s.weapon == WP_SABER ) {//temp HACK: these are the only 2 pain anims that look good when holding a saber pain_anim = PM_PickAnim( self, BOTH_PAIN2, BOTH_PAIN3 ); } else if ( mod != MOD_ELECTROCUTE ) { pain_anim = G_PickPainAnim( self, point, damage, hitLoc ); } if ( pain_anim == -1 ) { pain_anim = PM_PickAnim( self, BOTH_PAIN1, BOTH_PAIN18 ); } self->client->ps.saberAnimLevel = SS_FAST;//next attack must be a quick attack self->client->ps.saberMove = LS_READY;//don't finish whatever saber move you may have been in int parts = SETANIM_BOTH; if ( PM_CrouchAnim( self->client->ps.legsAnim ) || PM_InCartwheel( self->client->ps.legsAnim ) ) { parts = SETANIM_LEGS; } self->NPC->aiFlags &= ~NPCAI_KNEEL; NPC_SetAnim( self, parts, pain_anim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD ); } if ( voiceEvent != -1 ) { G_AddVoiceEvent( self, voiceEvent, Q_irand( 2000, 4000 ) ); } else { NPC_SetPainEvent( self ); } } //Setup the timing for it if ( mod == MOD_ELECTROCUTE ) { self->painDebounceTime = level.time + 4000; } self->painDebounceTime = level.time + PM_AnimLength( self->client->clientInfo.animFileIndex, (animNumber_t) pain_anim ); self->client->fireDelay = 0; } }