/* =========================================================================== Copyright (C) 1999 - 2005, Id Software, Inc. 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 . =========================================================================== */ #include "g_headers.h" #include "g_local.h" #include "g_functions.h" #include "../cgame/cg_local.h" #include "Q3_Interface.h" #include "wp_saber.h" #include "g_icarus.h" #include #ifdef _DEBUG #include #endif //_DEBUG #define SLOWDOWN_DIST 128.0f #define MIN_NPC_SPEED 16.0f extern qboolean Q3_TaskIDPending( gentity_t *ent, taskID_t taskType ); extern void G_MaintainFormations(gentity_t *self); extern void BG_CalculateOffsetAngles( gentity_t *ent, usercmd_t *ucmd );//in bg_pangles.cpp extern void BG_CalculateVRWeaponPosition( vec3_t origin, vec3_t angles );//in bg_pmisc.cpp extern void BG_CalculateVROffHandPosition( vec3_t origin, vec3_t angles );//in bg_pmisc.cpp extern void TryUse( gentity_t *ent ); extern void TryAltUse( gentity_t *ent ); extern void ChangeWeapon( gentity_t *ent, int newWeapon ); extern void ScoreBoardReset(void); extern void WP_SaberReflectCheck( gentity_t *self, usercmd_t *ucmd ); extern void WP_SaberUpdate( gentity_t *self, usercmd_t *ucmd ); extern void WP_SaberStartMissileBlockCheck( gentity_t *self, usercmd_t *ucmd ); extern void WP_ForcePowersUpdate( gentity_t *self, usercmd_t *ucmd ); extern void WP_SaberInitBladeData( gentity_t *ent ); extern gentity_t *SeekerAcquiresTarget ( gentity_t *ent, vec3_t pos ); extern void FireSeeker( gentity_t *owner, gentity_t *target, vec3_t origin, vec3_t dir ); extern qboolean InFront( vec3_t spot, vec3_t from, vec3_t fromAngles, float threshHold = 0.0f ); extern void NPC_SetLookTarget( gentity_t *self, int entNum, int clearTime ); extern qboolean PM_AdjustAnglesToGripper( gentity_t *gent, usercmd_t *cmd ); extern qboolean PM_AdjustAngleForWallRun( gentity_t *ent, usercmd_t *ucmd, qboolean doMove ); extern qboolean PM_AdjustAnglesForSpinningFlip( gentity_t *ent, usercmd_t *ucmd, qboolean anglesOnly ); extern qboolean PM_AdjustAnglesForBackAttack( gentity_t *ent, usercmd_t *ucmd ); extern qboolean PM_AdjustAnglesForSaberLock( gentity_t *ent, usercmd_t *ucmd ); extern qboolean PM_AdjustAnglesForKnockdown( gentity_t *ent, usercmd_t *ucmd, qboolean angleClampOnly ); extern qboolean PM_HasAnimation( gentity_t *ent, int animation ); extern qboolean PM_SpinningSaberAnim( int anim ); extern qboolean PM_SaberInAttack( int move ); extern int PM_AnimLength( int index, animNumber_t anim ); extern qboolean PM_InKnockDown( playerState_t *ps ); extern qboolean PM_InRoll( playerState_t *ps ); extern void PM_CmdForRoll( int anim, usercmd_t *pCmd ); extern qboolean PM_CrouchAnim( int anim ); extern qboolean PM_FlippingAnim( int anim ); extern qboolean PM_InCartwheel( int anim ); extern qboolean PM_StandingAnim( int anim ); extern qboolean PM_InForceGetUp( playerState_t *ps ); extern void G_CreateG2AttachedWeaponModel( gentity_t *ent, const char *weaponModel ); extern qboolean FlyingCreature( gentity_t *ent ); extern bool in_camera; extern qboolean player_locked; extern qboolean stop_icarus; extern cvar_t *g_spskill; extern cvar_t *g_timescale; extern cvar_t *g_saberMoveSpeed; extern cvar_t *g_saberAutoBlocking; extern vmCvar_t cg_thirdPersonAlpha; extern vmCvar_t cg_thirdPersonAutoAlpha; void ClientEndPowerUps( gentity_t *ent ); int G_FindLookItem( gentity_t *self ) { //FIXME: should be a more intelligent way of doing this, like auto aim? //closest, most in front... did damage to... took damage from? How do we know who the player is focusing on? gentity_t *ent; int bestEntNum = ENTITYNUM_NONE; gentity_t *entityList[MAX_GENTITIES]; int numListedEntities; vec3_t center, mins, maxs, fwdangles, forward, dir; int i, e; float radius = 256; float rating, bestRating = 0.0f; //FIXME: no need to do this in 1st person? fwdangles[1] = self->client->ps.viewangles[1]; AngleVectors( fwdangles, forward, NULL, NULL ); VectorCopy( self->currentOrigin, center ); for ( i = 0 ; i < 3 ; i++ ) { mins[i] = center[i] - radius; maxs[i] = center[i] + radius; } numListedEntities = gi.EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); if ( !numListedEntities ) { return ENTITYNUM_NONE; } for ( e = 0 ; e < numListedEntities ; e++ ) { ent = entityList[ e ]; if ( !ent->item ) { continue; } if ( ent->s.eFlags&EF_NODRAW ) { continue; } if ( (ent->spawnflags&4/*ITMSF_MONSTER*/) ) {//NPCs only continue; } if ( !BG_CanItemBeGrabbed( &ent->s, &self->client->ps ) ) {//don't need it continue; } if ( !gi.inPVS( self->currentOrigin, ent->currentOrigin ) ) {//not even potentially visible continue; } if ( !G_ClearLOS( self, self->client->renderInfo.eyePoint, ent ) ) {//can't see him continue; } //rate him based on how close & how in front he is VectorSubtract( ent->currentOrigin, center, dir ); rating = (1.0f-(VectorNormalize( dir )/radius)); rating *= DotProduct( forward, dir ); if ( ent->item->giType == IT_HOLDABLE && ent->item->giTag == INV_SECURITY_KEY ) {//security keys are of the highest importance rating *= 2.0f; } if ( rating > bestRating ) { bestEntNum = ent->s.number; bestRating = rating; } } return bestEntNum; } extern void CG_SetClientViewAngles( vec3_t angles, qboolean overrideViewEnt ); qboolean G_ClearViewEntity( gentity_t *ent ) { if ( !ent->client->ps.viewEntity ) return qfalse; if ( ent->client->ps.viewEntity > 0 && ent->client->ps.viewEntity < ENTITYNUM_NONE ) { if ( &g_entities[ent->client->ps.viewEntity] ) { g_entities[ent->client->ps.viewEntity].svFlags &= ~SVF_BROADCAST; if ( g_entities[ent->client->ps.viewEntity].NPC ) { g_entities[ent->client->ps.viewEntity].NPC->controlledTime = 0; SetClientViewAngle( &g_entities[ent->client->ps.viewEntity], g_entities[ent->client->ps.viewEntity].currentAngles ); G_SetAngles( &g_entities[ent->client->ps.viewEntity], g_entities[ent->client->ps.viewEntity].currentAngles ); VectorCopy( g_entities[ent->client->ps.viewEntity].currentAngles, g_entities[ent->client->ps.viewEntity].NPC->lastPathAngles ); g_entities[ent->client->ps.viewEntity].NPC->desiredYaw = g_entities[ent->client->ps.viewEntity].currentAngles[YAW]; } } CG_SetClientViewAngles( ent->pos4, qtrue ); SetClientViewAngle( ent, ent->pos4 ); } ent->client->ps.viewEntity = 0; return qtrue; } void G_SetViewEntity( gentity_t *self, gentity_t *viewEntity ) { if ( !self || !self->client || !viewEntity ) { return; } if ( self->s.number == 0 && cg.zoomMode ) { // yeah, it should really toggle them so it plays the end sound.... cg.zoomMode = 0; } if ( viewEntity->s.number == self->client->ps.viewEntity ) { return; } //clear old one first G_ClearViewEntity( self ); //set new one self->client->ps.viewEntity = viewEntity->s.number; viewEntity->svFlags |= SVF_BROADCAST; //remember current angles VectorCopy( self->client->ps.viewangles, self->pos4 ); if ( viewEntity->client ) { //vec3_t clear = {0,0,0}; CG_SetClientViewAngles( viewEntity->client->ps.viewangles, qtrue ); VectorCopy(viewEntity->client->ps.viewangles, vr->remote_angles); vr->remote_snapTurn = vr->snapTurn; vr->take_snap = true; vr->remote_cooldown = cg.time + 250; //SetClientViewAngle( self, viewEntity->client->ps.viewangles ); //SetClientViewAngle( viewEntity, clear ); /* VectorCopy( viewEntity->client->ps.viewangles, self->client->ps.viewangles ); for ( int i = 0; i < 3; i++ ) { self->client->ps.delta_angles[i] = viewEntity->client->ps.delta_angles[i]; } */ } if ( !self->s.number ) { if (viewEntity->client && viewEntity->client->NPC_class == CLASS_MOUSE) { CG_CenterPrint( "@VR_INGAME_EXIT_DROID_VIEW", SCREEN_HEIGHT * 0.95 ); } else if (Q_stricmp( "misc_panel_turret", viewEntity->classname ) == 0) { CG_CenterPrint( "@VR_INGAME_EXIT_TURRET_VIEW", SCREEN_HEIGHT * 0.95 ); } else { CG_CenterPrint( "@VR_INGAME_EXIT_CAMERA_VIEW", SCREEN_HEIGHT * 0.95 ); } } } qboolean G_ControlledByPlayer( gentity_t *self ) { if ( self && self->NPC && self->NPC->controlledTime > level.time ) {//being controlled gentity_t *controller = &g_entities[0]; if ( controller->client && controller->client->ps.viewEntity == self->s.number ) {//we're the player's viewEntity return qtrue; } } return qfalse; } qboolean G_ValidateLookEnemy( gentity_t *self, gentity_t *enemy ) { if ( !enemy ) { return qfalse; } if ( enemy->flags&FL_NOTARGET ) { return qfalse; } if ( !enemy || enemy == self || !enemy->inuse ) { return qfalse; } if ( !enemy->client || !enemy->NPC ) {//not valid if ( (enemy->svFlags&SVF_NONNPC_ENEMY) && enemy->s.weapon == WP_TURRET && enemy->noDamageTeam != self->client->playerTeam && enemy->health > 0 ) {//a turret //return qtrue; } else { return qfalse; } } else { if ( enemy->client->playerTeam == self->client->playerTeam ) {//on same team return qfalse; } if ( enemy->health <= 0 && ((level.time-enemy->s.time) > 3000||!InFront(enemy->currentOrigin,self->currentOrigin,self->client->ps.viewangles,0.2f)||DistanceHorizontal(enemy->currentOrigin,self->currentOrigin)>16384))//>128 {//corpse, been dead too long or too out of sight to be interesting if ( !enemy->message ) { return qfalse; } } } if ( (!InFront( enemy->currentOrigin, self->currentOrigin, self->client->ps.viewangles, 0.0f) || !G_ClearLOS( self, self->client->renderInfo.eyePoint, enemy ) ) && ( DistanceHorizontalSquared( enemy->currentOrigin, self->currentOrigin ) > 65536 || fabs(enemy->currentOrigin[2]-self->currentOrigin[2]) > 384 ) ) {//(not in front or not clear LOS) & greater than 256 away return qfalse; } //LOS? return qtrue; } void G_ChooseLookEnemy( gentity_t *self, usercmd_t *ucmd ) { //FIXME: should be a more intelligent way of doing this, like auto aim? //closest, most in front... did damage to... took damage from? How do we know who the player is focusing on? gentity_t *ent, *bestEnt = NULL; gentity_t *entityList[MAX_GENTITIES]; int numListedEntities; vec3_t center, mins, maxs, fwdangles, forward, dir; int i, e; float radius = 256; float rating, bestRating = 0.0f; //FIXME: no need to do this in 1st person? fwdangles[0] = 0; //Must initialize data! fwdangles[1] = self->client->ps.viewangles[1]; fwdangles[2] = 0; AngleVectors( fwdangles, forward, NULL, NULL ); VectorCopy( self->currentOrigin, center ); for ( i = 0 ; i < 3 ; i++ ) { mins[i] = center[i] - radius; maxs[i] = center[i] + radius; } numListedEntities = gi.EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); if ( !numListedEntities ) {//should we clear the enemy? return; } for ( e = 0 ; e < numListedEntities ; e++ ) { ent = entityList[ e ]; if ( !gi.inPVS( self->currentOrigin, ent->currentOrigin ) ) {//not even potentially visible continue; } if ( !G_ValidateLookEnemy( self, ent ) ) {//doesn't meet criteria of valid look enemy (don't check current since we would have done that before this func's call continue; } if ( !G_ClearLOS( self, self->client->renderInfo.eyePoint, ent ) ) {//can't see him continue; } //rate him based on how close & how in front he is VectorSubtract( ent->currentOrigin, center, dir ); rating = (1.0f-(VectorNormalize( dir )/radius)); rating *= DotProduct( forward, dir )+1.0f; if ( ent->health <= 0 ) { if ( (ucmd->buttons&BUTTON_ATTACK) || (ucmd->buttons&BUTTON_ALT_ATTACK) ) {//if attacking, don't consider dead enemies continue; } if ( ent->message ) {//keyholder rating *= 0.5f; } else { rating *= 0.1f; } } if ( ent->s.weapon == WP_SABER ) { rating *= 2.0f; } if ( ent->enemy == self ) {//he's mad at me, he's more important rating *= 2.0f; } else if ( ent->NPC && ent->NPC->blockedSpeechDebounceTime > level.time - 6000 ) {//he's detected me, he's more important if ( ent->NPC->blockedSpeechDebounceTime > level.time + 4000 ) { rating *= 1.5f; } else {//from 1.0f to 1.5f rating += rating * ((float)(ent->NPC->blockedSpeechDebounceTime-level.time) + 6000.0f)/20000.0f; } } /* if ( g_crosshairEntNum == ent && !self->enemy ) {//we don't have an enemy and we are aiming at this guy rading *= 2.0f; } */ if ( rating > bestRating ) { bestEnt = ent; bestRating = rating; } } if ( bestEnt ) { self->enemy = bestEnt; } } /* =============== G_DamageFeedback Called just before a snapshot is sent to the given player. Totals up all damage and generates both the player_state_t damage values to that client for pain blends and kicks, and global pain sound events for all clients. =============== */ void P_DamageFeedback( gentity_t *player ) { gclient_t *client; float count; vec3_t angles; client = player->client; if ( client->ps.pm_type == PM_DEAD ) { return; } // total points of damage shot at the player this frame count = client->damage_blood + client->damage_armor; if ( count == 0 ) { return; // didn't take any damage } if ( count > 255 ) { count = 255; } // send the information to the client // world damage (falling, slime, etc) uses a special code // to make the blend blob centered instead of positional if ( client->damage_fromWorld ) { client->ps.damagePitch = 255; client->ps.damageYaw = 255; client->damage_fromWorld = qfalse; } else { vectoangles( client->damage_from, angles ); client->ps.damagePitch = angles[PITCH]/360.0 * 256; client->ps.damageYaw = angles[YAW]/360.0 * 256; } client->ps.damageCount = count; // // clear totals // client->damage_blood = 0; client->damage_armor = 0; client->damage_knockback = 0; } /* ============= P_WorldEffects Check for lava / slime contents and drowning ============= */ void P_WorldEffects( gentity_t *ent ) { int mouthContents = 0; if ( ent->client->noclip ) { ent->client->airOutTime = level.time + 12000; // don't need air return; } if ( !in_camera ) { mouthContents = gi.pointcontents( ent->client->renderInfo.eyePoint, ent->s.number ); } // // check for drowning // if ( (mouthContents&(CONTENTS_WATER|CONTENTS_SLIME)) ) { if ( ent->client->NPC_class == CLASS_SWAMPTROOPER ) {//they have air tanks ent->client->airOutTime = level.time + 12000; // don't need air ent->damage = 2; } else if ( ent->client->airOutTime < level.time) {// if out of air, start drowning // drown! ent->client->airOutTime += 1000; if ( ent->health > 0 ) { // take more damage the longer underwater ent->damage += 2; if (ent->damage > 15) ent->damage = 15; // play a gurp sound instead of a normal pain sound if (ent->health <= ent->damage) { G_AddEvent( ent, EV_WATER_DROWN, 0 ); } else { G_AddEvent( ent, Q_irand(EV_WATER_GURP1, EV_WATER_GURP2), 0 ); } // don't play a normal pain sound ent->painDebounceTime = level.time + 200; G_Damage (ent, NULL, NULL, NULL, NULL, ent->damage, DAMAGE_NO_ARMOR, MOD_WATER); } } } else { ent->client->airOutTime = level.time + 12000; ent->damage = 2; } // // check for sizzle damage (move to pmove?) // if (ent->waterlevel && (ent->watertype&(CONTENTS_LAVA|CONTENTS_SLIME)) ) { if (ent->health > 0 && ent->painDebounceTime < level.time ) { if (ent->watertype & CONTENTS_LAVA) { G_Damage (ent, NULL, NULL, NULL, NULL, 15*ent->waterlevel, 0, MOD_LAVA); } if (ent->watertype & CONTENTS_SLIME) { G_Damage (ent, NULL, NULL, NULL, NULL, 1, 0, MOD_SLIME); } } } // Poisoned? if ((ent->client->poisonDamage) && (ent->client->poisonTime < level.time)) { ent->client->poisonDamage -= 2; ent->client->poisonTime = level.time + 1000; G_Damage( ent, NULL, NULL, 0, 0, 2, DAMAGE_NO_KNOCKBACK|DAMAGE_NO_ARMOR, MOD_UNKNOWN );//FIXME: MOD_POISON? if (ent->client->poisonDamage<0) { ent->client->poisonDamage = 0; } } } /* =============== G_SetClientSound =============== */ void G_SetClientSound( gentity_t *ent ) { // if (ent->waterlevel && (ent->watertype&(CONTENTS_LAVA|CONTENTS_SLIME)) ) // ent->s.loopSound = G_SoundIndex("sound/weapons/stasis/electricloop.wav"); // else // ent->s.loopSound = 0; } //============================================================== void DoImpact( gentity_t *self, gentity_t *other, qboolean damageSelf ) { float magnitude, my_mass; vec3_t velocity; if( self->client ) { VectorCopy( self->client->ps.velocity, velocity ); my_mass = self->mass; } else { VectorCopy( self->s.pos.trDelta, velocity ); if ( self->s.pos.trType == TR_GRAVITY ) { velocity[2] -= 0.25f * g_gravity->value; } if( !self->mass ) { my_mass = 1; } else if ( self->mass <= 10 ) { my_mass = 10; } else { my_mass = self->mass;///10; } } magnitude = VectorLength( velocity ) * my_mass / 50; if ( !self->client || self->client->ps.lastOnGround+300client->ps.lastOnGround+100 < level.time ) ) { vec3_t dir1, dir2; float force = 0, dot; if ( other->material == MAT_GLASS || other->material == MAT_GLASS_METAL || other->material == MAT_GRATE1 || ((other->svFlags&SVF_BBRUSH)&&(other->spawnflags&4/*THIN*/)) )//(other->absmax[0]-other->absmin[0]<=32||other->absmax[1]-other->absmin[1]<=32||other->absmax[2]-other->absmin[2]<=32)) ) {//glass and thin breakable brushes (axially aligned only, unfortunately) take more impact damage magnitude *= 2; } //damage them if ( magnitude >= 100 && other->s.number < ENTITYNUM_WORLD ) { VectorCopy( velocity, dir1 ); VectorNormalize( dir1 ); if( VectorCompare( other->currentOrigin, vec3_origin ) ) {//a brush with no origin VectorCopy ( dir1, dir2 ); } else { VectorSubtract( other->currentOrigin, self->currentOrigin, dir2 ); VectorNormalize( dir2 ); } dot = DotProduct( dir1, dir2 ); if ( dot >= 0.2 ) { force = dot; } else { force = 0; } force *= (magnitude/50); int cont = gi.pointcontents( other->absmax, other->s.number ); if( (cont&CONTENTS_WATER) )//|| (self.classname=="barrel"&&self.aflag))//FIXME: or other watertypes { force /= 3; //water absorbs 2/3 velocity } if ( self->NPC && other->s.number == ENTITYNUM_WORLD ) {//NPCs take less damage force /= 2; } /* if(self.frozen>0&&force>10) force=10; */ if( ( force >= 1 && other->s.number != 0 ) || force >= 10) { /* dprint("Damage other ("); dprint(loser.classname); dprint("): "); dprint(ftos(force)); dprint("\n"); */ if ( other->svFlags & SVF_GLASS_BRUSH ) { other->splashRadius = (float)(self->maxs[0] - self->mins[0])/4.0f; } if ( self->forcePushTime > level.time - 1000//was force pushed/pulled in the last 1600 milliseconds && self->forcePuller == other->s.number )//hit the person who pushed/pulled me {//ignore the impact } else if ( other->takedamage ) { if ( !self->client || !other->s.number || !other->client ) {//aw, fuck it, clients no longer take impact damage from other clients, unless you're the player G_Damage( other, self, self, velocity, self->currentOrigin, force, DAMAGE_NO_ARMOR, MOD_IMPACT ); } else { GEntity_PainFunc( other, self, self, self->currentOrigin, force, MOD_IMPACT ); //Hmm, maybe knockdown? G_Throw( other, dir2, force ); } } else { //Hmm, maybe knockdown? G_Throw( other, dir2, force ); } } } if ( damageSelf && self->takedamage && !(self->flags&FL_NO_IMPACT_DMG)) { //Now damage me //FIXME: more lenient falling damage, especially for when driving a vehicle if ( self->client && self->client->ps.forceJumpZStart ) {//we were force-jumping if ( self->currentOrigin[2] >= self->client->ps.forceJumpZStart ) {//we landed at same height or higher than we landed magnitude = 0; } else {//FIXME: take off some of it, at least? magnitude = (self->client->ps.forceJumpZStart-self->currentOrigin[2])/3; } } //if(self.classname!="monster_mezzoman"&&self.netname!="spider")//Cats always land on their feet if( ( magnitude >= 100 + self->health && self->s.number != 0 && self->s.weapon != WP_SABER ) || ( magnitude >= 700 ) )//&& self.safe_time < level.time ))//health here is used to simulate structural integrity { if ( (self->s.weapon == WP_SABER || self->s.number == 0) && self->client && self->client->ps.groundEntityNum < ENTITYNUM_NONE && magnitude < 1000 ) {//players and jedi take less impact damage //allow for some lenience on high falls magnitude /= 2; /* if ( self.absorb_time >= time )//crouching on impact absorbs 1/2 the damage { magnitude/=2; } */ } magnitude /= 40; magnitude = magnitude - force/2;//If damage other, subtract half of that damage off of own injury if ( magnitude >= 1 ) { //FIXME: Put in a thingtype impact sound function /* dprint("Damage self ("); dprint(self.classname); dprint("): "); dprint(ftos(magnitude)); dprint("\n"); */ /* if ( self.classname=="player_sheep "&& self.flags&FL_ONGROUND && self.velocity_z > -50 ) return; */ if ( self->NPC && self->s.weapon == WP_SABER ) {//FIXME: for now Jedi take no falling damage, but really they should if pushed off? magnitude = 0; } G_Damage( self, NULL, NULL, NULL, self->currentOrigin, magnitude/2, DAMAGE_NO_ARMOR, MOD_FALLING );//FIXME: MOD_IMPACT } } } //FIXME: slow my velocity some? self->lastImpact = level.time; /* if(self.flags&FL_ONGROUND) self.last_onground=time; */ } } /* ============== ClientImpacts ============== */ void ClientImpacts( gentity_t *ent, pmove_t *pm ) { int i, j; trace_t trace; gentity_t *other; memset( &trace, 0, sizeof( trace ) ); for (i=0 ; inumtouch ; i++) { for (j=0 ; jtouchents[j] == pm->touchents[i] ) { break; } } if (j != i) { continue; // duplicated } other = &g_entities[ pm->touchents[i] ]; if ( ( ent->NPC != NULL ) && ( ent->e_TouchFunc != touchF_NULL ) ) { // last check unneccessary GEntity_TouchFunc( ent, other, &trace ); } if ( other->e_TouchFunc == touchF_NULL ) { // not needed, but I'll leave it I guess (cache-hit issues) continue; } GEntity_TouchFunc( other, ent, &trace ); } } // Use triggers seems to have bigger "front" boundaries allowing // user to trigger them from distance while it is possible to reach // through them when standing near. To ballance this, move hand // origin a bit back const float TOUCH_OFFSET = -10.0f; const vec3_t TOUCH_RANGE = { 4, 4, 4 }; void G_TouchTriggersWithHand( bool offHand, gentity_t *ent, vec3_t src, vec3_t vf ) { vec3_t dest, mins, maxs; gentity_t *touch[MAX_GENTITIES], *hit; qboolean touched[MAX_GENTITIES]; int i, num; trace_t trace; memset (touched, qfalse, sizeof(touched) ); VectorMA( src, TOUCH_OFFSET, vf, dest ); VectorSubtract( dest, TOUCH_RANGE, mins ); VectorAdd( dest, TOUCH_RANGE, maxs ); num = gi.EntitiesInBox( mins, maxs, touch, MAX_GENTITIES ); for ( i=0 ; icontents & CONTENTS_TRIGGER ) ) { // Entity is not trigger continue; } if ( touched[i] == qtrue ) { // Already touched this move continue; } if ( !( hit->spawnflags & 4 ) ) { // Non-BUTTON entities were already processed continue; } // In case touching entity requiring to be activated facing to it via use gesture, check // activation angle here. Existing check in g_trigger.cpp Touch_Multi is problematic because // we do not have there information by which hand was entity triggered. if ( (hit->spawnflags & 2) && DotProduct( hit->movedir, vf ) < 0.5 ) { //Not Within 45 degrees continue; } touched[i] = qtrue; memset( &trace, 0, sizeof(trace) ); if ( hit->e_TouchFunc != touchF_NULL ) { int channel = vr->right_handed != offHand ? 1 : 2; if (level.time > vr->useHapticFeedbackTime[channel - 1]) { cgi_HapticEvent("use_button", 0, channel, 60, 0, 0); vr->useHapticFeedbackTime[channel - 1] = level.time + USE_HAPTIC_FEEDBACK_DELAY; } GEntity_TouchFunc(hit, ent, &trace); } } } /* ============ G_TouchTriggersLerped Find all trigger entities that ent's current position touches. Spectators will only interact with teleporters. This version checks at 6 unit steps between last and current origins ============ */ void G_TouchTriggersLerped( gentity_t *ent ) { int i, num; float dist, curDist = 0; gentity_t *touch[MAX_GENTITIES], *hit; trace_t trace; vec3_t end, mins, maxs, diff; const vec3_t range = { 40, 40, 52 }; qboolean touched[MAX_GENTITIES]; qboolean done = qfalse; if ( !ent->client ) { return; } // dead NPCs don't activate triggers! if ( ent->client->ps.stats[STAT_HEALTH] <= 0 ) { if ( ent->s.number ) { return; } } #ifdef _DEBUG for ( int j = 0; j < 3; j++ ) { assert( !Q_isnan(ent->currentOrigin[j])); assert( !Q_isnan(ent->lastOrigin[j])); } #endif// _DEBUG VectorSubtract( ent->currentOrigin, ent->lastOrigin, diff ); dist = VectorNormalize( diff ); memset (touched, qfalse, sizeof(touched) ); bool thirdPersonActive = gi.cvar("cg_thirdPerson", "0", CVAR_TEMP)->integer; bool useGestureEnabled = gi.cvar("vr_gesture_triggered_use", "1", CVAR_ARCHIVE)->integer; // defined in VrCvars.h bool useGestureAllowed = useGestureEnabled && !thirdPersonActive && !(vr && vr->remote_droid); for ( curDist = 0; !done && ent->maxs[1]>0; curDist += (float)ent->maxs[1]/2.0f ) { if ( curDist >= dist ) { VectorCopy( ent->currentOrigin, end ); done = qtrue; } else { VectorMA( ent->lastOrigin, curDist, diff, end ); } VectorSubtract( end, range, mins ); VectorAdd( end, range, maxs ); num = gi.EntitiesInBox( mins, maxs, touch, MAX_GENTITIES ); // can't use ent->absmin, because that has a one unit pad VectorAdd( end, ent->mins, mins ); VectorAdd( end, ent->maxs, maxs ); for ( i=0 ; ie_TouchFunc == touchF_NULL) && (ent->e_TouchFunc == touchF_NULL) ) { continue; } if ( !( hit->contents & CONTENTS_TRIGGER ) ) { continue; } if ( touched[i] == qtrue ) { continue;//already touched this move } if ( ent->client->ps.stats[STAT_HEALTH] <= 0 ) { if ( Q_stricmp( "trigger_teleport", hit->classname ) || !(hit->spawnflags&16/*TTSF_DEAD_OK*/) ) {//dead clients can only touch tiogger_teleports that are marked as touchable continue; } } // use seperate code for determining if an item is picked up // so you don't have to actually contact its bounding box /* if ( hit->s.eType == ET_ITEM ) { if ( !BG_PlayerTouchesItem( &ent->client->ps, &hit->s, level.time ) ) { continue; } } else */ { if ( !gi.EntityContact( mins, maxs, hit ) ) { continue; } } if (ent->client && ent->client->ps.clientNum == 0 && hit->spawnflags & 4 && useGestureAllowed) { // Entity is BUTTON touched by player with enabled use gestures. Skip it as we want to touch // buttons by hands and not by body in this case continue; } touched[i] = qtrue; memset( &trace, 0, sizeof(trace) ); if ( hit->e_TouchFunc != touchF_NULL ) { GEntity_TouchFunc(hit, ent, &trace); } //WTF? Why would a trigger ever fire off the NPC's touch func??!!! /* if ( ( ent->NPC != NULL ) && ( ent->e_TouchFunc != touchF_NULL ) ) { GEntity_TouchFunc( ent, hit, &trace ); } */ } } // In case of player entity with use gesture enabled, trace additional entities for each players hand based on active use action. if ( ent->client && ent->client->ps.clientNum == 0 && ent->client->ps.stats[STAT_HEALTH] > 0 && useGestureAllowed) { if( ent->client->usercmd.buttons & BUTTON_USE ) { vec3_t src, angles, vf; BG_CalculateVRWeaponPosition(src, angles); AngleVectors( angles, vf, NULL, NULL ); G_TouchTriggersWithHand(false, ent, src, vf ); } if( ent->client->usercmd.buttons & BUTTON_ALT_USE ) { vec3_t src, angles, vf; BG_CalculateVROffHandPosition(src, angles); AngleVectors( angles, vf, NULL, NULL ); G_TouchTriggersWithHand(true, ent, src, vf ); } } } /* ============ G_TouchTriggers Find all trigger entities that ent's current position touches. Spectators will only interact with teleporters. ============ */ void G_TouchTriggers( gentity_t *ent ) { int i, num; gentity_t *touch[MAX_GENTITIES], *hit; trace_t trace; vec3_t mins, maxs; const vec3_t range = { 40, 40, 52 }; if ( !ent->client ) { return; } // dead clients don't activate triggers! if ( ent->client->ps.stats[STAT_HEALTH] <= 0 ) { return; } VectorSubtract( ent->client->ps.origin, range, mins ); VectorAdd( ent->client->ps.origin, range, maxs ); num = gi.EntitiesInBox( mins, maxs, touch, MAX_GENTITIES ); // can't use ent->absmin, because that has a one unit pad VectorAdd( ent->client->ps.origin, ent->mins, mins ); VectorAdd( ent->client->ps.origin, ent->maxs, maxs ); for ( i=0 ; ie_TouchFunc == touchF_NULL) && (ent->e_TouchFunc == touchF_NULL) ) { continue; } if ( !( hit->contents & CONTENTS_TRIGGER ) ) { continue; } // use seperate code for determining if an item is picked up // so you don't have to actually contact its bounding box /* if ( hit->s.eType == ET_ITEM ) { if ( !BG_PlayerTouchesItem( &ent->client->ps, &hit->s, level.time ) ) { continue; } } else */ { if ( !gi.EntityContact( mins, maxs, hit ) ) { continue; } } memset( &trace, 0, sizeof(trace) ); if ( hit->e_TouchFunc != touchF_NULL ) { GEntity_TouchFunc(hit, ent, &trace); } if ( ( ent->NPC != NULL ) && ( ent->e_TouchFunc != touchF_NULL ) ) { GEntity_TouchFunc( ent, hit, &trace ); } } } /* ============ G_MoverTouchTriggers Find all trigger entities that ent's current position touches. Spectators will only interact with teleporters. ============ */ void G_MoverTouchPushTriggers( gentity_t *ent, vec3_t oldOrg ) { int i, num; float step, stepSize, dist; gentity_t *touch[MAX_GENTITIES], *hit; trace_t trace; vec3_t mins, maxs, dir, size, checkSpot; const vec3_t range = { 40, 40, 52 }; // non-moving movers don't hit triggers! if ( !VectorLengthSquared( ent->s.pos.trDelta ) ) { return; } VectorSubtract( ent->mins, ent->maxs, size ); stepSize = VectorLength( size ); if ( stepSize < 1 ) { stepSize = 1; } VectorSubtract( ent->currentOrigin, oldOrg, dir ); dist = VectorNormalize( dir ); for ( step = 0; step <= dist; step += stepSize ) { VectorMA( ent->currentOrigin, step, dir, checkSpot ); VectorSubtract( checkSpot, range, mins ); VectorAdd( checkSpot, range, maxs ); num = gi.EntitiesInBox( mins, maxs, touch, MAX_GENTITIES ); // can't use ent->absmin, because that has a one unit pad VectorAdd( checkSpot, ent->mins, mins ); VectorAdd( checkSpot, ent->maxs, maxs ); for ( i=0 ; is.eType != ET_PUSH_TRIGGER ) { continue; } if ( hit->e_TouchFunc == touchF_NULL ) { continue; } if ( !( hit->contents & CONTENTS_TRIGGER ) ) { continue; } if ( !gi.EntityContact( mins, maxs, hit ) ) { continue; } memset( &trace, 0, sizeof(trace) ); if ( hit->e_TouchFunc != touchF_NULL ) { GEntity_TouchFunc(hit, ent, &trace); } } } } void G_MatchPlayerWeapon( gentity_t *ent ) { if ( g_entities[0].inuse && g_entities[0].client ) {//player is around int newWeap; if ( g_entities[0].client->ps.weapon > WP_DET_PACK ) { newWeap = WP_BRYAR_PISTOL; } else { newWeap = g_entities[0].client->ps.weapon; } if ( newWeap != WP_NONE && ent->client->ps.weapon != newWeap ) { if ( ent->weaponModel >= 0 ) { gi.G2API_RemoveGhoul2Model(ent->ghoul2, ent->weaponModel); } ent->client->ps.stats[STAT_WEAPONS] = ( 1 << newWeap ); ent->client->ps.ammo[weaponData[newWeap].ammoIndex] = 999; ChangeWeapon( ent, newWeap ); ent->client->ps.weapon = newWeap; ent->client->ps.weaponstate = WEAPON_READY; if ( newWeap == WP_SABER ) { //FIXME: AddSound/Sight Event WP_SaberInitBladeData( ent ); G_CreateG2AttachedWeaponModel( ent, ent->client->ps.saberModel ); ent->client->ps.saberActive = g_entities[0].client->ps.saberActive; ent->client->ps.saberLength = g_entities[0].client->ps.saberLength; ent->client->ps.saberAnimLevel = g_entities[0].client->ps.saberAnimLevel; } else { G_CreateG2AttachedWeaponModel( ent, weaponData[newWeap].weaponMdl ); } } } } void G_NPCMunroMatchPlayerWeapon( gentity_t *ent ) { //special uber hack for cinematic Munro's to match player's weapon if ( !in_camera ) { if ( ent && ent->client && ent->NPC && (ent->NPC->aiFlags&NPCAI_MATCHPLAYERWEAPON) ) {//we're a Kyle NPC G_MatchPlayerWeapon( ent ); } } } /* ================= ClientInactivityTimer Returns qfalse if the client is dropped ================= */ qboolean ClientInactivityTimer( gclient_t *client ) { if ( ! g_inactivity->integer ) { // give everyone some time, so if the operator sets g_inactivity during // gameplay, everyone isn't kicked client->inactivityTime = level.time + 60 * 1000; client->inactivityWarning = qfalse; } else if ( client->usercmd.forwardmove || client->usercmd.rightmove || client->usercmd.upmove || (client->usercmd.buttons & BUTTON_ATTACK) || (client->usercmd.buttons & BUTTON_ALT_ATTACK) ) { client->inactivityTime = level.time + g_inactivity->integer * 1000; client->inactivityWarning = qfalse; } else if ( !client->pers.localClient ) { if ( level.time > client->inactivityTime ) { gi.DropClient( client - level.clients, "Dropped due to inactivity" ); return qfalse; } if ( level.time > client->inactivityTime - 10000 && !client->inactivityWarning ) { client->inactivityWarning = qtrue; gi.SendServerCommand( client - level.clients, "cp \"Ten seconds until inactivity drop!\n\"" ); } } else {//FIXME: here is where we can decide to play an idle animation } return qtrue; } /* ================== ClientTimerActions Actions that happen once a second ================== */ void ClientTimerActions( gentity_t *ent, int msec ) { gclient_t *client; client = ent->client; client->timeResidual += msec; while ( client->timeResidual >= 1000 ) { client->timeResidual -= 1000; if ( ent->s.weapon != WP_NONE ) { ent->client->sess.missionStats.weaponUsed[ent->s.weapon]++; } // if we've got the seeker powerup, see if we can shoot it at someone /* if ( ent->client->ps.powerups[PW_SEEKER] > level.time ) { vec3_t seekerPos, dir; gentity_t *enemy = SeekerAcquiresTarget( ent, seekerPos ); if ( enemy != NULL ) // set the client's enemy to a valid target { FireSeeker( ent, enemy, seekerPos, dir ); gentity_t *tent; tent = G_TempEntity( seekerPos, EV_POWERUP_SEEKER_FIRE ); VectorCopy( dir, tent->pos1 ); tent->s.eventParm = ent->s.number; } }*/ } } /* ==================== ClientIntermissionThink ==================== */ static qboolean ClientCinematicThink( gclient_t *client ) { client->ps.eFlags &= ~EF_FIRING; // swap button actions client->oldbuttons = client->buttons; client->buttons = client->usercmd.buttons; if ( client->buttons & ( BUTTON_USE ) & ( client->oldbuttons ^ client->buttons ) ) { return( qtrue ); } return( qfalse ); } /* ================ ClientEvents Events will be passed on to the clients for presentation, but any server game effects are handled here ================ */ extern void WP_SaberDamageTrace( gentity_t *ent ); extern void WP_SaberUpdateOldBladeData( gentity_t *ent ); void ClientEvents( gentity_t *ent, int oldEventSequence ) { int i; int event; gclient_t *client; //int damage; #ifndef FINAL_BUILD qboolean fired = qfalse; #endif client = ent->client; for ( i = oldEventSequence ; i < client->ps.eventSequence ; i++ ) { event = client->ps.events[ i & (MAX_PS_EVENTS-1) ]; switch ( event ) { case EV_FALL_MEDIUM: case EV_FALL_FAR://these come from bg_pmove, PM_CrashLand if ( ent->s.eType != ET_PLAYER ) { break; // not in the player model } /* //FIXME: isn't there a more accurate way to calculate damage from falls? if ( event == EV_FALL_FAR ) { damage = 50; } else { damage = 25; } ent->painDebounceTime = level.time + 200; // no normal pain sound G_Damage (ent, NULL, NULL, NULL, NULL, damage, 0, MOD_FALLING); */ break; case EV_FIRE_WEAPON: #ifndef FINAL_BUILD if ( fired ) { gi.Printf( "DOUBLE EV_FIRE_WEAPON AND-OR EV_ALT_FIRE!!\n" ); } fired = qtrue; #endif FireWeapon( ent, qfalse ); break; case EV_ALT_FIRE: #ifndef FINAL_BUILD if ( fired ) { gi.Printf( "DOUBLE EV_FIRE_WEAPON AND-OR EV_ALT_FIRE!!\n" ); } fired = qtrue; #endif FireWeapon( ent, qtrue ); break; default: break; } } //by the way, if you have your saber in hand and it's on, do the damage trace if ( client->ps.weapon == WP_SABER ) { if ( g_timescale->value >= 1.0f || !(client->ps.forcePowersActive&(1<ps.saberDamageDebounceTime - level.time > wait ) {//when you unpause the game with force speed on, the time gets *really* wiggy... client->ps.saberDamageDebounceTime = level.time + wait; } if ( client->ps.saberDamageDebounceTime <= level.time ) { WP_SaberDamageTrace( ent ); WP_SaberUpdateOldBladeData( ent ); /* if ( g_timescale->value&&client->ps.clientNum==0&&!player_locked&&!MatrixMode&&client->ps.forcePowersActive&(1<value ); } */ client->ps.saberDamageDebounceTime = level.time + wait; } } } } qboolean G_CheckClampUcmd( gentity_t *ent, usercmd_t *ucmd ) { qboolean overridAngles = qfalse; if ( (!ent->s.number&&ent->aimDebounceTime>level.time) || (ent->client->ps.pm_time && (ent->client->ps.pm_flags&PMF_TIME_KNOCKBACK)) || ent->forcePushTime > level.time ) {//being knocked back, can't do anything! ucmd->buttons = 0; ucmd->forwardmove = 0; ucmd->rightmove = 0; ucmd->upmove = 0; if ( ent->NPC ) { VectorClear( ent->client->ps.moveDir ); } } overridAngles = (PM_AdjustAnglesForKnockdown( ent, ucmd, qfalse )?qtrue:overridAngles); if ( ent->client->ps.saberLockTime > level.time ) { ucmd->forwardmove = ucmd->rightmove = ucmd->upmove = 0; if ( ent->client->ps.saberLockTime - level.time > SABER_LOCK_DELAYED_TIME ) {//2 second delay before either can push //FIXME: base on difficulty ucmd->buttons = 0; } else { ucmd->buttons &= ~(ucmd->buttons&~BUTTON_ATTACK); } overridAngles = (PM_AdjustAnglesForSaberLock( ent, ucmd )?qtrue:overridAngles); if ( ent->NPC ) { VectorClear( ent->client->ps.moveDir ); } } if ( ent->client->ps.saberMove == LS_A_LUNGE ) {//can't move during lunge ucmd->rightmove = ucmd->upmove = 0; if ( ent->client->ps.legsAnimTimer > 500 && (ent->s.number || !player_locked) ) { ucmd->forwardmove = 127; } else { ucmd->forwardmove = 0; } if ( ent->NPC ) {//invalid now VectorClear( ent->client->ps.moveDir ); } } if ( ent->client->ps.saberMove == LS_A_JUMP_T__B_ ) {//can't move during leap if ( ent->client->ps.groundEntityNum != ENTITYNUM_NONE || (!ent->s.number && player_locked) ) {//hit the ground ucmd->forwardmove = 0; } ucmd->rightmove = ucmd->upmove = 0; if ( ent->NPC ) {//invalid now VectorClear( ent->client->ps.moveDir ); } } if ( ent->client->ps.saberMove == LS_A_BACK || ent->client->ps.saberMove == LS_A_BACK_CR || ent->client->ps.saberMove == LS_A_BACKSTAB ) {//can't move or turn during back attacks ucmd->forwardmove = ucmd->rightmove = 0; if ( ent->NPC ) { VectorClear( ent->client->ps.moveDir ); } if ( (overridAngles = (PM_AdjustAnglesForBackAttack( ent, ucmd )?qtrue:overridAngles)) == qtrue ) { //pull back the view if ( !ent->s.number ) { float animLength = PM_AnimLength( ent->client->clientInfo.animFileIndex, (animNumber_t)ent->client->ps.torsoAnim ); float elapsedTime = (float)(animLength-ent->client->ps.legsAnimTimer); float backDist = 0; if ( elapsedTime < animLength/2.0f ) {//starting anim backDist = (elapsedTime/animLength)*120.0f; } else {//ending anim backDist = ((animLength-elapsedTime)/animLength)*120.0f; } cg.overrides.active |= CG_OVERRIDE_3RD_PERSON_RNG; cg.overrides.thirdPersonRange = cg_thirdPersonRange.value+backDist; } } } else if ( ent->client->ps.torsoAnim == BOTH_WALL_FLIP_BACK1 || ent->client->ps.torsoAnim == BOTH_WALL_FLIP_BACK2 ) { //pull back the view if ( !ent->s.number ) { float animLength = PM_AnimLength( ent->client->clientInfo.animFileIndex, (animNumber_t)ent->client->ps.torsoAnim ); float elapsedTime = (float)(animLength-ent->client->ps.legsAnimTimer); float backDist = 0; if ( elapsedTime < animLength/2.0f ) {//starting anim backDist = (elapsedTime/animLength)*120.0f; } else {//ending anim backDist = ((animLength-elapsedTime)/animLength)*120.0f; } cg.overrides.active |= CG_OVERRIDE_3RD_PERSON_RNG; cg.overrides.thirdPersonRange = cg_thirdPersonRange.value+backDist; } } else if ( !ent->s.number ) { if ( ent->client->NPC_class != CLASS_ATST ) { cg.overrides.active &= ~CG_OVERRIDE_3RD_PERSON_RNG; cg.overrides.thirdPersonRange = 0; } } if ( PM_InRoll( &ent->client->ps ) ) { if ( ent->s.number || !player_locked ) { PM_CmdForRoll( ent->client->ps.legsAnim, ucmd ); } if ( ent->NPC ) {//invalid now VectorClear( ent->client->ps.moveDir ); } ent->client->ps.speed = 400; } if ( PM_InCartwheel( ent->client->ps.legsAnim ) ) {//can't keep moving in cartwheel ucmd->forwardmove = ucmd->rightmove = ucmd->upmove = 0; if ( ent->NPC ) {//invalid now VectorClear( ent->client->ps.moveDir ); } if ( ent->s.number || !player_locked ) { switch ( ent->client->ps.legsAnim ) { case BOTH_ARIAL_LEFT: case BOTH_CARTWHEEL_LEFT: ucmd->rightmove = -127; break; case BOTH_ARIAL_RIGHT: case BOTH_CARTWHEEL_RIGHT: ucmd->rightmove = 127; break; case BOTH_ARIAL_F1: ucmd->forwardmove = 127; break; default: break; } } } overridAngles = (PM_AdjustAngleForWallRun( ent, ucmd, qtrue )?qtrue:overridAngles); return overridAngles; } void BG_AddPushVecToUcmd( gentity_t *self, usercmd_t *ucmd ) { vec3_t forward, right, moveDir; float pushSpeed, fMove, rMove; if ( !self->client ) { return; } pushSpeed = VectorLengthSquared(self->client->pushVec); if(!pushSpeed) {//not being pushed return; } AngleVectors(self->client->ps.viewangles, forward, right, NULL); VectorScale(forward, ucmd->forwardmove/127.0f * self->client->ps.speed, moveDir); VectorMA(moveDir, ucmd->rightmove/127.0f * self->client->ps.speed, right, moveDir); //moveDir is now our intended move velocity VectorAdd(moveDir, self->client->pushVec, moveDir); self->client->ps.speed = VectorNormalize(moveDir); //moveDir is now our intended move velocity plus our push Vector fMove = 127.0 * DotProduct(forward, moveDir); rMove = 127.0 * DotProduct(right, moveDir); ucmd->forwardmove = floor(fMove);//If in the same dir , will be positive ucmd->rightmove = floor(rMove);//If in the same dir , will be positive if ( self->client->pushVecTime < level.time ) { VectorClear( self->client->pushVec ); } } void NPC_Accelerate( gentity_t *ent, qboolean fullWalkAcc, qboolean fullRunAcc ) { if ( !ent->client || !ent->NPC ) { return; } if ( !ent->NPC->stats.acceleration ) {//No acceleration means just start and stop ent->NPC->currentSpeed = ent->NPC->desiredSpeed; } //FIXME: in cinematics always accel/decel? else if ( ent->NPC->desiredSpeed <= ent->NPC->stats.walkSpeed ) {//Only accelerate if at walkSpeeds if ( ent->NPC->desiredSpeed > ent->NPC->currentSpeed + ent->NPC->stats.acceleration ) { //ent->client->ps.friction = 0; ent->NPC->currentSpeed += ent->NPC->stats.acceleration; } else if ( ent->NPC->desiredSpeed > ent->NPC->currentSpeed ) { //ent->client->ps.friction = 0; ent->NPC->currentSpeed = ent->NPC->desiredSpeed; } else if ( fullWalkAcc && ent->NPC->desiredSpeed < ent->NPC->currentSpeed - ent->NPC->stats.acceleration ) {//decelerate even when walking ent->NPC->currentSpeed -= ent->NPC->stats.acceleration; } else if ( ent->NPC->desiredSpeed < ent->NPC->currentSpeed ) {//stop on a dime ent->NPC->currentSpeed = ent->NPC->desiredSpeed; } } else// if ( ent->NPC->desiredSpeed > ent->NPC->stats.walkSpeed ) {//Only decelerate if at runSpeeds if ( fullRunAcc && ent->NPC->desiredSpeed > ent->NPC->currentSpeed + ent->NPC->stats.acceleration ) {//Accelerate to runspeed //ent->client->ps.friction = 0; ent->NPC->currentSpeed += ent->NPC->stats.acceleration; } else if ( ent->NPC->desiredSpeed > ent->NPC->currentSpeed ) {//accelerate instantly //ent->client->ps.friction = 0; ent->NPC->currentSpeed = ent->NPC->desiredSpeed; } else if ( fullRunAcc && ent->NPC->desiredSpeed < ent->NPC->currentSpeed - ent->NPC->stats.acceleration ) { ent->NPC->currentSpeed -= ent->NPC->stats.acceleration; } else if ( ent->NPC->desiredSpeed < ent->NPC->currentSpeed ) { ent->NPC->currentSpeed = ent->NPC->desiredSpeed; } } } /* ------------------------- NPC_GetWalkSpeed ------------------------- */ static int NPC_GetWalkSpeed( gentity_t *ent ) { int walkSpeed = 0; if ( ( ent->client == NULL ) || ( ent->NPC == NULL ) ) return 0; switch ( ent->client->playerTeam ) { case TEAM_PLAYER: //To shutup compiler, will add entries later (this is stub code) default: walkSpeed = ent->NPC->stats.walkSpeed; break; } return walkSpeed; } /* ------------------------- NPC_GetRunSpeed ------------------------- */ #define BORG_RUN_INCR 25 #define SPECIES_RUN_INCR 25 #define STASIS_RUN_INCR 20 #define WARBOT_RUN_INCR 20 static int NPC_GetRunSpeed( gentity_t *ent ) { int runSpeed = 0; if ( ( ent->client == NULL ) || ( ent->NPC == NULL ) ) return 0; // team no longer indicates species/race. Use NPC_class to adjust speed for specific npc types switch( ent->client->NPC_class) { case CLASS_PROBE: // droid cases here to shut-up compiler case CLASS_GONK: case CLASS_R2D2: case CLASS_R5D2: case CLASS_MARK1: case CLASS_MARK2: case CLASS_PROTOCOL: case CLASS_ATST: // hmm, not really your average droid case CLASS_MOUSE: case CLASS_SEEKER: case CLASS_REMOTE: runSpeed = ent->NPC->stats.runSpeed; break; default: runSpeed = ent->NPC->stats.runSpeed; break; } return runSpeed; } void G_UpdateEmplacedWeaponData( gentity_t *ent ) { if ( ent && ent->owner && ent->health > 0 ) { gentity_t *chair = ent->owner; //take the emplaced gun's waypoint as your own ent->waypoint = chair->waypoint; //update the actual origin of the sitter mdxaBone_t boltMatrix; vec3_t chairAng = {0, ent->client->ps.viewangles[YAW], 0}; // Getting the seat bolt here gi.G2API_GetBoltMatrix( chair->ghoul2, chair->playerModel, chair->headBolt, &boltMatrix, chairAng, chair->currentOrigin, (cg.time?cg.time:level.time), NULL, chair->s.modelScale ); // Storing ent position, bolt position, and bolt axis gi.G2API_GiveMeVectorFromMatrix( boltMatrix, ORIGIN, ent->client->ps.origin ); gi.linkentity( ent ); } } void ExitEmplacedWeapon( gentity_t *ent ) { // requesting to unlock from the weapon int oldWeapon; // Remove this gun from our inventory ent->client->ps.stats[STAT_WEAPONS] &= ~( 1 << ent->client->ps.weapon ); // when we lock or unlock from the the gun, we just swap weapons with it oldWeapon = ent->client->ps.weapon; ent->client->ps.weapon = ent->owner->s.weapon; ent->owner->s.weapon = oldWeapon; extern void ChangeWeapon( gentity_t *ent, int newWeapon ); if ( ent->NPC ) { ChangeWeapon( ent, ent->client->ps.weapon ); } else { extern void CG_ChangeWeapon( int num ); CG_ChangeWeapon( ent->client->ps.weapon ); if (weaponData[ent->client->ps.weapon].weaponMdl[0]) { //might be NONE, so check if it has a model G_CreateG2AttachedWeaponModel( ent, weaponData[ent->client->ps.weapon].weaponMdl ); if ( ent->client->ps.weapon == WP_SABER && cg_saberAutoThird.value ) { gi.cvar_set( "cg_thirdperson", "1" ); } else if ( ent->client->ps.weapon != WP_SABER && cg_gunAutoFirst.value ) { gi.cvar_set( "cg_thirdperson", "0" ); } } } if ( ent->client->ps.weapon == WP_SABER ) { ent->client->ps.saberActive = ent->owner->alt_fire; } // We'll leave the gun pointed in the direction it was last facing, though we'll cut out the pitch if ( ent->client ) { VectorCopy( ent->client->ps.viewangles, ent->owner->s.angles ); ent->owner->s.angles[PITCH] = 0; G_SetAngles( ent->owner, ent->owner->s.angles ); VectorCopy( ent->owner->s.angles, ent->owner->pos1 ); // if we are the player we will have put down a brush that blocks NPCs so that we have a clear spot to get back out. //gentity_t *place = G_Find( NULL, FOFS(classname), "emp_placeholder" ); if ( ent->health > 0 && ent->owner->nextTrain ) {//he's still alive, and we have a placeholder, so put him back // reset the players position VectorCopy( ent->owner->nextTrain->currentOrigin, ent->client->ps.origin ); //reset ent's size to normal VectorCopy( ent->owner->nextTrain->mins, ent->mins ); VectorCopy( ent->owner->nextTrain->maxs, ent->maxs ); //free the placeholder G_FreeEntity( ent->owner->nextTrain ); //re-link the ent gi.linkentity( ent ); } else if ( ent->health <= 0 ) { // dead, so give 'em a push out of the chair vec3_t dir; AngleVectors( ent->owner->s.angles, NULL, dir, NULL ); if ( rand() & 1 ) { VectorScale( dir, -1, dir ); } VectorMA( ent->client->ps.velocity, 75, dir, ent->client->ps.velocity ); } } // gi.G2API_DetachG2Model( &ent->ghoul2[ent->playerModel] ); ent->s.eFlags &= ~EF_LOCKED_TO_WEAPON; ent->client->ps.eFlags &= ~EF_LOCKED_TO_WEAPON; ent->owner->noDamageTeam = TEAM_FREE; ent->owner->svFlags &= ~SVF_NONNPC_ENEMY; ent->owner->delay = level.time; ent->owner->activator = NULL; if ( !ent->NPC ) { // by keeping the owner, a dead npc can be pushed out of the chair without colliding with it ent->owner = NULL; } } void RunEmplacedWeapon( gentity_t *ent, usercmd_t **ucmd ) { if (( (*ucmd)->buttons & BUTTON_USE || /*(*ucmd)->forwardmove < 0 ||*/ (*ucmd)->upmove > 0 ) && ent->owner && ent->owner->delay + 500 < level.time ) { ent->owner->s.loopSound = 0; ExitEmplacedWeapon( ent ); (*ucmd)->buttons &= ~BUTTON_USE; G_Sound( ent, G_SoundIndex( "sound/weapons/emplaced/emplaced_dismount.mp3" )); } else { // this is a crappy way to put sounds on a moving emplaced gun.... /* if ( ent->owner ) { if ( !VectorCompare( ent->owner->pos3, ent->owner->movedir )) { ent->owner->s.loopSound = G_SoundIndex( "sound/weapons/emplaced/emplaced_move_lp.wav" ); ent->owner->fly_sound_debounce_time = level.time; } else { if ( ent->owner->fly_sound_debounce_time + 100 <= level.time ) { ent->owner->s.loopSound = 0; } } VectorCopy( ent->owner->pos3, ent->owner->movedir ); } */ // don't allow movement, weapon switching, and most kinds of button presses (*ucmd)->forwardmove = 0; (*ucmd)->rightmove = 0; (*ucmd)->upmove = 0; (*ucmd)->buttons &= (BUTTON_ATTACK|BUTTON_ALT_ATTACK); (*ucmd)->weapon = ent->client->ps.weapon; //WP_EMPLACED_GUN; if ( ent->health <= 0 ) { ExitEmplacedWeapon( ent ); } } } // yes... so stop skipping... void G_StopCinematicSkip( void ) { gi.cvar_set("skippingCinematic", "0"); gi.cvar_set("timescale", "1"); } void G_StartCinematicSkip( void ) { if (cinematicSkipScript[0]) { ICARUS_RunScript( &g_entities[0], va( "%s/%s", Q3_SCRIPT_DIR, cinematicSkipScript ) ); memset( cinematicSkipScript, 0, sizeof( cinematicSkipScript ) ); gi.cvar_set("skippingCinematic", "1"); gi.cvar_set("timescale", "100"); } else { // no... so start skipping... gi.cvar_set("skippingCinematic", "1"); gi.cvar_set("timescale", "100"); } } void G_CheckClientIdle( gentity_t *ent, usercmd_t *ucmd ) { if ( !ent || !ent->client || ent->health <= 0 ) { return; } if ( !ent->s.number && ( !cg.renderingThirdPerson || cg.zoomMode ) ) { if ( ent->client->idleTime < level.time ) { ent->client->idleTime = level.time; } return; } if ( !VectorCompare( vec3_origin, ent->client->ps.velocity ) || ucmd->buttons || ucmd->forwardmove || ucmd->rightmove || ucmd->upmove || !PM_StandingAnim( ent->client->ps.legsAnim ) || ent->enemy || ent->client->ps.legsAnimTimer || ent->client->ps.torsoAnimTimer ) {//FIXME: also check for turning? if ( !VectorCompare( vec3_origin, ent->client->ps.velocity ) || ucmd->buttons || ucmd->forwardmove || ucmd->rightmove || ucmd->upmove || ent->enemy ) { //if in an idle, break out switch ( ent->client->ps.legsAnim ) { case BOTH_STAND1IDLE1: case BOTH_STAND2IDLE1: case BOTH_STAND2IDLE2: case BOTH_STAND3IDLE1: case BOTH_STAND4IDLE1: ent->client->ps.legsAnimTimer = 0; break; } switch ( ent->client->ps.torsoAnim ) { case BOTH_STAND1IDLE1: case BOTH_STAND2IDLE1: case BOTH_STAND2IDLE2: case BOTH_STAND3IDLE1: case BOTH_STAND4IDLE1: ent->client->ps.torsoAnimTimer = 0; break; } } // if ( ent->client->idleTime < level.time ) { ent->client->idleTime = level.time; } } else if ( level.time - ent->client->idleTime > 5000 ) {//been idle for 5 seconds int idleAnim = -1; switch ( ent->client->ps.legsAnim ) { case BOTH_STAND1: idleAnim = BOTH_STAND1IDLE1; break; case BOTH_STAND2: idleAnim = Q_irand(BOTH_STAND2IDLE1,BOTH_STAND2IDLE2); break; case BOTH_STAND3: idleAnim = BOTH_STAND3IDLE1; break; case BOTH_STAND4: idleAnim = BOTH_STAND4IDLE1; break; } if ( idleAnim != -1 && PM_HasAnimation( ent, idleAnim ) ) { NPC_SetAnim( ent, SETANIM_BOTH, idleAnim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD ); //don't idle again after this anim for a while ent->client->idleTime = level.time + PM_AnimLength( ent->client->clientInfo.animFileIndex, (animNumber_t)idleAnim ) + Q_irand( 0, 2000 ); } } } void G_CheckMovingLoopingSounds( gentity_t *ent, usercmd_t *ucmd ) { if ( ent->client ) { if ( (ent->NPC&&!VectorCompare( vec3_origin, ent->client->ps.moveDir ))//moving using moveDir || ucmd->forwardmove || ucmd->rightmove//moving using ucmds || (ucmd->upmove&&FlyingCreature( ent ))//flier using ucmds to move || (FlyingCreature( ent )&&!VectorCompare( vec3_origin, ent->client->ps.velocity )&&ent->health>0))//flier using velocity to move { switch( ent->client->NPC_class ) { case CLASS_R2D2: ent->s.loopSound = G_SoundIndex( "sound/chars/r2d2/misc/r2_move_lp.wav" ); break; case CLASS_R5D2: ent->s.loopSound = G_SoundIndex( "sound/chars/r2d2/misc/r2_move_lp2.wav" ); break; case CLASS_MARK2: ent->s.loopSound = G_SoundIndex( "sound/chars/mark2/misc/mark2_move_lp" ); break; case CLASS_MOUSE: ent->s.loopSound = G_SoundIndex( "sound/chars/mouse/misc/mouse_lp" ); break; case CLASS_PROBE: ent->s.loopSound = G_SoundIndex( "sound/chars/probe/misc/probedroidloop" ); default: break; } } else {//not moving under your own control, stop loopSound if ( ent->client->NPC_class == CLASS_R2D2 || ent->client->NPC_class == CLASS_R5D2 || ent->client->NPC_class == CLASS_MARK2 || ent->client->NPC_class == CLASS_MOUSE || ent->client->NPC_class == CLASS_PROBE ) { ent->s.loopSound = 0; } } } } /* ============== ClientThink This will be called once for each client frame, which will usually be a couple times for each server frame on fast clients. ============== */ extern int G_FindLocalInterestPoint( gentity_t *self ); extern void ForceGrip(gentity_t *ent); extern void ForceLightning(gentity_t *ent); extern void ForceTelepathy(gentity_t *ent); extern void ForceHeal(gentity_t *ent); extern void ForceThrowEx( gentity_t *self, qboolean pull, qboolean aimByViewAngles ); static void ProcessGenericCmd(gentity_t *ent, byte cmd) { switch(cmd) { default: break; case GENCMD_FORCE_HEAL: ForceHeal( ent ); break; case GENCMD_FORCE_SPEED: ForceSpeed( ent ); break; case GENCMD_FORCE_THROW: ForceThrowEx(ent, qfalse, qtrue); break; case GENCMD_FORCE_PULL: ForceThrowEx(ent, qtrue, qtrue); break; case GENCMD_FORCE_DISTRACT: ForceTelepathy(ent); break; case GENCMD_FORCE_GRIP: ForceGrip(ent); break; case GENCMD_FORCE_LIGHTNING: ForceLightning(ent); break; } } void ClientThink_real( gentity_t *ent, usercmd_t *ucmd ) { gclient_t *client; pmove_t pm; vec3_t oldOrigin; int oldEventSequence; int msec; qboolean inSpinFlipAttack = PM_AdjustAnglesForSpinningFlip( ent, ucmd, qfalse ); qboolean controlledByPlayer = qfalse; //Don't let the player do anything if in a camera if ( ent->s.number == 0 ) { extern cvar_t *g_skippingcin; if ( ent->s.eFlags & EF_LOCKED_TO_WEAPON ) { G_UpdateEmplacedWeaponData( ent ); RunEmplacedWeapon( ent, &ucmd ); } if ( ent->client->ps.saberLockTime > level.time && ent->client->ps.saberLockEnemy != ENTITYNUM_NONE ) { NPC_SetLookTarget( ent, ent->client->ps.saberLockEnemy, level.time+1000 ); } if ( ent->client->renderInfo.lookTargetClearTime < level.time //NOTE: here this is used as a debounce, not an actual timer && ent->health > 0 //must be alive && (!ent->enemy || ent->client->ps.saberMove != LS_A_BACKSTAB) )//don't update if in backstab unless don't currently have an enemy {//NOTE: doesn't keep updating to nearest enemy once you're dead int newLookTarget; if ( !G_ValidateLookEnemy( ent, ent->enemy ) ) { ent->enemy = NULL; } //FIXME: make this a little prescient? G_ChooseLookEnemy( ent, ucmd ); if ( ent->enemy ) {//target newLookTarget = ent->enemy->s.number; //so we don't change our minds in the next 1 second ent->client->renderInfo.lookTargetClearTime = level.time+1000; ent->client->renderInfo.lookMode = LM_ENT; } else {//no target //FIXME: what about sightalerts and missiles? newLookTarget = ENTITYNUM_NONE; newLookTarget = G_FindLocalInterestPoint( ent ); if ( newLookTarget != ENTITYNUM_NONE ) {//found something of interest ent->client->renderInfo.lookMode = LM_INTEREST; } else {//okay, no interesting things and no enemies, so look for items newLookTarget = G_FindLookItem( ent ); ent->client->renderInfo.lookMode = LM_ENT; } } if ( ent->client->renderInfo.lookTarget != newLookTarget ) {//transitioning NPC_SetLookTarget( ent, newLookTarget, level.time+1000 ); } } if ( in_camera ) { // watch the code here, you MUST "return" within this IF(), *unless* you're stopping the cinematic skip. // if ( ClientCinematicThink(ent->client) ) { if (g_skippingcin->integer) // already doing cinematic skip? {// yes... so stop skipping... G_StopCinematicSkip(); } else {// no... so start skipping... G_StartCinematicSkip(); return; } } else { return; } } else if ( ent->client->ps.vehicleModel != 0 && ent->health > 0 ) { float speed; speed = VectorLength( ent->client->ps.velocity ); if ( speed && ent->client->ps.groundEntityNum == ENTITYNUM_NONE ) { int diff = AngleNormalize180(SHORT2ANGLE(ucmd->angles[YAW]+ent->client->ps.delta_angles[YAW]) - floor(ent->client->ps.viewangles[YAW])); int slide = floor(((float)(diff))/120.0f*-127.0f); if ( (slide > 0 && ucmd->rightmove >= 0) || ((slide < 0 && ucmd->rightmove <= 0)) ) {//note: don't want these to conflict right now because that seems to feel really weird //gi.Printf( "slide %i, diff %i, yaw %i\n", slide, diff, ucmd->angles[YAW] ); ucmd->rightmove += slide; } if ( ucmd->rightmove > 64 ) { ucmd->rightmove = 64; } else if ( ucmd->rightmove < -64 ) { ucmd->rightmove = -64; } } else { ucmd->rightmove = 0; ucmd->angles[PITCH] = 0; ucmd->angles[YAW] = ANGLE2SHORT( ent->client->ps.viewangles[YAW] ) - ent->client->ps.delta_angles[YAW]; } } else { if ( g_skippingcin->integer ) {//We're skipping the cinematic and it's over now gi.cvar_set("timescale", "1"); gi.cvar_set("skippingCinematic", "0"); } if ( ent->client->ps.pm_type == PM_DEAD && cg.missionStatusDeadTime < level.time ) {//mission status screen is up because player is dead, stop all scripts stop_icarus = qtrue; } } // // Don't allow the player to adjust the pitch when they are in third person overhead cam. //extern vmCvar_t cg_thirdPerson; // if ( cg_thirdPerson.integer == 2 ) // { // ucmd->angles[PITCH] = 0; // } if ( cg.zoomMode == 2 ) { // Any kind of movement when the player is NOT ducked when the disruptor gun is zoomed will cause us to auto-magically un-zoom if ( ( (ucmd->forwardmove||ucmd->rightmove) && ucmd->upmove >= 0 //crouching-moving is ok && !(ucmd->buttons&BUTTON_USE)/*leaning is ok*/ ) || ucmd->upmove > 0 //jumping not allowed ) { // already zooming, so must be wanting to turn it off G_Sound( ent, G_SoundIndex( "sound/weapons/disruptor/zoomend.wav" )); cg.zoomMode = 0; cg.zoomTime = cg.time; cg.zoomLocked = qfalse; } } if ( (player_locked || (ent->client->ps.eFlags&EF_FORCE_GRIPPED)) && ent->client->ps.pm_type < PM_DEAD ) // unless dead {//lock out player control if ( !player_locked ) { VectorClearM( ucmd->angles ); } ucmd->forwardmove = 0; ucmd->rightmove = 0; ucmd->buttons = 0; ucmd->upmove = 0; PM_AdjustAnglesToGripper( ent, ucmd ); } if ( ent->client->ps.leanofs ) {//no shooting while leaning ucmd->buttons &= ~BUTTON_ATTACK; if ( ent->client->ps.weapon != WP_DISRUPTOR ) {//can still zoom around corners ucmd->buttons &= ~BUTTON_ALT_ATTACK; } } } else { if ( ent->s.eFlags & EF_LOCKED_TO_WEAPON ) { G_UpdateEmplacedWeaponData( ent ); } if ( player && player->client && player->client->ps.viewEntity == ent->s.number ) { controlledByPlayer = qtrue; int sav_weapon = ucmd->weapon; memcpy( ucmd, &player->client->usercmd, sizeof( usercmd_t ) ); ucmd->weapon = sav_weapon; ent->client->usercmd = *ucmd; } G_NPCMunroMatchPlayerWeapon( ent ); } if ( ent->client ) { if ( ent->client->NPC_class == CLASS_GONK || ent->client->NPC_class == CLASS_MOUSE || ent->client->NPC_class == CLASS_R2D2 || ent->client->NPC_class == CLASS_R5D2 ) {//no jumping or strafing in these guys ucmd->upmove = ucmd->rightmove = 0; } else if ( ent->client->NPC_class == CLASS_ATST ) {//no jumping in atst if (ent->client->ps.pm_type != PM_NOCLIP) { ucmd->upmove = 0; } if ( ent->client->ps.groundEntityNum != ENTITYNUM_NONE ) {//ATST crushed anything underneath it gentity_t *under = &g_entities[ent->client->ps.groundEntityNum]; if ( under && under->health && under->takedamage ) { vec3_t down = {0,0,-1}; //FIXME: we'll be doing traces down from each foot, so we'll have a real impact origin G_Damage( under, ent, ent, down, under->currentOrigin, 100, 0, MOD_CRUSH ); } //so they know to run like hell when I get close //FIXME: project this in the direction I'm moving? if ( !Q_irand( 0, 10 ) ) {//not so often... AddSoundEvent( ent, ent->currentOrigin, ent->maxs[1]*5, AEL_DANGER ); AddSightEvent( ent, ent->currentOrigin, ent->maxs[1]*5, AEL_DANGER, 100 ); } } } else if ( ent->client->ps.groundEntityNum < ENTITYNUM_WORLD && !ent->client->ps.forceJumpCharge ) {//standing on an entity and not currently force jumping gentity_t *groundEnt = &g_entities[ent->client->ps.groundEntityNum]; if ( groundEnt && groundEnt->client && groundEnt->client->ps.groundEntityNum != ENTITYNUM_NONE && groundEnt->health > 0 && !PM_InRoll( &groundEnt->client->ps ) && !(groundEnt->client->ps.eFlags&EF_LOCKED_TO_WEAPON) && !inSpinFlipAttack ) {//landed on a live client who is on the ground, jump off them and knock them down if ( ent->health > 0 ) { if ( !PM_InRoll( &ent->client->ps ) && !PM_FlippingAnim( ent->client->ps.legsAnim ) ) { if ( ent->s.number && ent->s.weapon == WP_SABER ) { ent->client->ps.forceJumpCharge = 320;//FIXME: calc this intelligently? } else if ( !ucmd->upmove ) {//if not ducking (which should cause a roll), then jump ucmd->upmove = 127; } if ( !ucmd->forwardmove && !ucmd->rightmove ) {// If not moving, don't want to jump straight up //FIXME: trace for clear di? if ( !Q_irand( 0, 3 ) ) { ucmd->forwardmove = 127; } else if ( !Q_irand( 0, 3 ) ) { ucmd->forwardmove = -127; } else if ( !Q_irand( 0, 1 ) ) { ucmd->rightmove = 127; } else { ucmd->rightmove = -127; } } if ( !ent->s.number && ucmd->upmove < 0 ) {//player who should roll- force it int rollAnim = BOTH_ROLL_F; if ( ucmd->forwardmove >= 0 ) { rollAnim = BOTH_ROLL_F; } else if ( ucmd->forwardmove < 0 ) { rollAnim = BOTH_ROLL_B; } else if ( ucmd->rightmove > 0 ) { rollAnim = BOTH_ROLL_R; } else if ( ucmd->rightmove < 0 ) { rollAnim = BOTH_ROLL_L; } NPC_SetAnim(ent,SETANIM_BOTH,rollAnim,SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD); G_AddEvent( ent, EV_ROLL, 0 ); ent->client->ps.saberMove = LS_NONE; } } } else {//a corpse? Shit //Hmm, corpses should probably *always* knockdown... ent->clipmask &= ~CONTENTS_BODY; } //FIXME: need impact sound event GEntity_PainFunc( groundEnt, ent, ent, groundEnt->currentOrigin, 0, MOD_CRUSH ); if ( groundEnt->client->NPC_class == CLASS_DESANN && ent->client->NPC_class != CLASS_LUKE ) {//can't knock down desann unless you're luke //FIXME: should he smack you away like Galak Mech? } else if ( ( ( (groundEnt->s.number&&(groundEnt->s.weapon!=WP_SABER||!groundEnt->NPC||groundEnt->NPC->ranks.number||G_ControlledByPlayer(groundEnt)) && !Q_irand( 0, 3 )&&cg.renderingThirdPerson&&!cg.zoomMode) )//or a player in third person, 25% of the time && groundEnt->client->playerTeam != ent->client->playerTeam ) //and not on the same team || ent->client->NPC_class == CLASS_DESANN )//desann always knocks people down { int knockAnim = BOTH_KNOCKDOWN1; if ( PM_CrouchAnim( groundEnt->client->ps.legsAnim ) ) {//knockdown from crouch knockAnim = BOTH_KNOCKDOWN4; } else { vec3_t gEFwd, gEAngles = {0,groundEnt->client->ps.viewangles[YAW],0}; AngleVectors( gEAngles, gEFwd, NULL, NULL ); if ( DotProduct( ent->client->ps.velocity, gEFwd ) > 50 ) {//pushing him forward knockAnim = BOTH_KNOCKDOWN3; } } NPC_SetAnim( groundEnt, SETANIM_BOTH, knockAnim, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD ); } } } } client = ent->client; // mark the time, so the connection sprite can be removed client->lastCmdTime = level.time; client->pers.lastCommand = *ucmd; // sanity check the command time to prevent speedup cheating if ( ucmd->serverTime > level.time + 200 ) { ucmd->serverTime = level.time + 200; } if ( ucmd->serverTime < level.time - 1000 ) { ucmd->serverTime = level.time - 1000; } msec = ucmd->serverTime - client->ps.commandTime; if ( msec < 1 ) { msec = 1; } if ( msec > 200 ) { msec = 200; } // check for inactivity timer, but never drop the local client of a non-dedicated server if ( !ClientInactivityTimer( client ) ) return; if ( client->noclip ) { client->ps.pm_type = PM_NOCLIP; } else if ( client->ps.stats[STAT_HEALTH] <= 0 ) { client->ps.pm_type = PM_DEAD; } else { client->ps.pm_type = PM_NORMAL; } //FIXME: if global gravity changes this should update everyone's personal gravity... if ( !(ent->svFlags & SVF_CUSTOM_GRAVITY) ) { client->ps.gravity = g_gravity->value; } // set speed if ( ent->NPC != NULL ) {//we don't actually scale the ucmd, we use actual speeds if ( ent->NPC->combatMove == qfalse ) { if ( !(ucmd->buttons & BUTTON_USE) ) {//Not leaning qboolean Flying = (qboolean)(ucmd->upmove && ent->NPC->stats.moveType == MT_FLYSWIM); qboolean Climbing = (qboolean)(ucmd->upmove && (ent->watertype & CONTENTS_LADDER)); client->ps.friction = 6; if ( ucmd->forwardmove || ucmd->rightmove || Flying ) { //if ( ent->NPC->behaviorState != BS_FORMATION ) {//In - Formation NPCs set thier desiredSpeed themselves if ( ucmd->buttons & BUTTON_WALKING ) { ent->NPC->desiredSpeed = NPC_GetWalkSpeed( ent );//ent->NPC->stats.walkSpeed; } else//running { ent->NPC->desiredSpeed = NPC_GetRunSpeed( ent );//ent->NPC->stats.runSpeed; } if ( ent->NPC->currentSpeed >= 80 && !controlledByPlayer ) {//At higher speeds, need to slow down close to stuff //Slow down as you approach your goal if ( ent->NPC->distToGoal < SLOWDOWN_DIST && !(ent->NPC->aiFlags&NPCAI_NO_SLOWDOWN) )//128 { if ( ent->NPC->desiredSpeed > MIN_NPC_SPEED ) { float slowdownSpeed = ((float)ent->NPC->desiredSpeed) * ent->NPC->distToGoal / SLOWDOWN_DIST; ent->NPC->desiredSpeed = ceil(slowdownSpeed); if ( ent->NPC->desiredSpeed < MIN_NPC_SPEED ) {//don't slow down too much ent->NPC->desiredSpeed = MIN_NPC_SPEED; } } } } } } else if ( Climbing ) { ent->NPC->desiredSpeed = ent->NPC->stats.walkSpeed; } else {//We want to stop ent->NPC->desiredSpeed = 0; } NPC_Accelerate( ent, qfalse, qfalse ); if ( ent->NPC->currentSpeed <= 24 && ent->NPC->desiredSpeed < ent->NPC->currentSpeed ) {//No-one walks this slow client->ps.speed = ent->NPC->currentSpeed = 0;//Full stop ucmd->forwardmove = 0; ucmd->rightmove = 0; } else { if ( ent->NPC->currentSpeed <= ent->NPC->stats.walkSpeed ) {//Play the walkanim ucmd->buttons |= BUTTON_WALKING; } else { ucmd->buttons &= ~BUTTON_WALKING; } if ( ent->NPC->currentSpeed > 0 ) {//We should be moving if ( Climbing || Flying ) { if ( !ucmd->upmove ) {//We need to force them to take a couple more steps until stopped ucmd->upmove = ent->NPC->last_ucmd.upmove;//was last_upmove; } } else if ( !ucmd->forwardmove && !ucmd->rightmove ) {//We need to force them to take a couple more steps until stopped ucmd->forwardmove = ent->NPC->last_ucmd.forwardmove;//was last_forwardmove; ucmd->rightmove = ent->NPC->last_ucmd.rightmove;//was last_rightmove; } } client->ps.speed = ent->NPC->currentSpeed; if ( player && player->client && player->client->ps.viewEntity == ent->s.number ) { } else { //Slow down on turns - don't orbit!!! float turndelta = 0; // if the NPC is locked into a Yaw, we want to check the lockedDesiredYaw...otherwise the NPC can't walk backwards, because it always thinks it trying to turn according to desiredYaw if( client->renderInfo.renderFlags & RF_LOCKEDANGLE ) // yeah I know the RF_ flag is a pretty ugly hack... { turndelta = (180 - fabs( AngleDelta( ent->currentAngles[YAW], ent->NPC->lockedDesiredYaw ) ))/180; } else { turndelta = (180 - fabs( AngleDelta( ent->currentAngles[YAW], ent->NPC->desiredYaw ) ))/180; } if ( turndelta < 0.75f ) { client->ps.speed = 0; } else if ( ent->NPC->distToGoal < 100 && turndelta < 1.0 ) {//Turn is greater than 45 degrees or closer than 100 to goal client->ps.speed = floor(((float)(client->ps.speed))*turndelta); } } } } } else { ent->NPC->desiredSpeed = ( ucmd->buttons & BUTTON_WALKING ) ? NPC_GetWalkSpeed( ent ) : NPC_GetRunSpeed( ent ); client->ps.speed = ent->NPC->desiredSpeed; } } else {//Client sets ucmds and such for speed alterations if ( ent->client->ps.vehicleModel != 0 && ent->health > 0 ) { if ( client->ps.speed || client->ps.groundEntityNum == ENTITYNUM_NONE || ucmd->forwardmove > 0 || ucmd->upmove > 0) { if ( ucmd->forwardmove > 0 ) { client->ps.speed += 20; } else if ( ucmd->forwardmove < 0 ) { if ( client->ps.speed > 800 ) { client->ps.speed -= 20; } else { client->ps.speed -= 5; } } else {//accelerate to cruising speed only, otherwise, just coast if ( client->ps.speed < 800 ) { client->ps.speed += 10; if ( client->ps.speed > 800 ) { client->ps.speed = 800; } } } ucmd->forwardmove = 127; } else { if ( ucmd->forwardmove < 0 ) { ucmd->forwardmove = 0; } if ( ucmd->upmove < 0 ) { ucmd->upmove = 0; } ucmd->rightmove = 0; } if ( client->ps.speed > 3000 ) { client->ps.speed = 3000; } else if ( client->ps.speed < 0 ) { client->ps.speed = 0; } if ( client->ps.speed < 800 ) { client->ps.gravity = (800 - client->ps.speed)/4; } else { client->ps.gravity = 0; } } else { client->ps.speed = g_speed->value;//default is 320 /*if ( !ent->s.number && ent->painDebounceTime>level.time ) { client->ps.speed *= 0.25f; } else */if ( PM_SaberInAttack( ent->client->ps.saberMove ) && ucmd->forwardmove < 0 ) {//if running backwards while attacking, don't run as fast. switch( client->ps.saberAnimLevel ) { case FORCE_LEVEL_1: client->ps.speed *= 0.75f; break; case FORCE_LEVEL_2: client->ps.speed *= 0.60f; break; case FORCE_LEVEL_3: client->ps.speed *= 0.45f; break; } if ( g_saberMoveSpeed->value != 1.0f ) { client->ps.speed *= g_saberMoveSpeed->value; } } else if ( PM_SpinningSaberAnim( client->ps.legsAnim ) ) { client->ps.speed *= 0.5f; if ( g_saberMoveSpeed->value != 1.0f ) { client->ps.speed *= g_saberMoveSpeed->value; } } else if ( client->ps.weapon == WP_SABER && ( ucmd->buttons & BUTTON_ATTACK ) ) {//if attacking with saber while running, drop your speed //FIXME: should be weaponTime? Or in certain anims? switch( client->ps.saberAnimLevel ) { case FORCE_LEVEL_2: client->ps.speed *= 0.85f; break; case FORCE_LEVEL_3: client->ps.speed *= 0.70f; break; } if ( g_saberMoveSpeed->value != 1.0f ) { client->ps.speed *= g_saberMoveSpeed->value; } } } } if ( client->NPC_class == CLASS_ATST && client->ps.legsAnim == BOTH_RUN1START ) {//HACK: when starting to move as atst, ramp up speed //float animLength = PM_AnimLength( client->clientInfo.animFileIndex, (animNumber_t)client->ps.legsAnim); //client->ps.speed *= ( animLength - client->ps.legsAnimTimer)/animLength; if ( client->ps.legsAnimTimer > 100 ) { client->ps.speed = 0; } } //Apply forced movement if ( client->forced_forwardmove ) { ucmd->forwardmove = client->forced_forwardmove; if ( !client->ps.speed ) { if ( ent->NPC != NULL ) { client->ps.speed = ent->NPC->stats.runSpeed; } else { client->ps.speed = g_speed->value;//default is 320 } } } if ( client->forced_rightmove ) { ucmd->rightmove = client->forced_rightmove; if ( !client->ps.speed ) { if ( ent->NPC != NULL ) { client->ps.speed = ent->NPC->stats.runSpeed; } else { client->ps.speed = g_speed->value;//default is 320 } } } if ( ucmd->forwardmove < 0 && !(ucmd->buttons&BUTTON_WALKING) && client->ps.groundEntityNum != ENTITYNUM_NONE ) {//running backwards is slower than running forwards client->ps.speed *= 0.75; } //FIXME: need to do this before check to avoid walls and cliffs (or just cliffs?) BG_AddPushVecToUcmd( ent, ucmd ); G_CheckClampUcmd( ent, ucmd ); if ( (ucmd->buttons&BUTTON_BLOCKING) && !g_saberAutoBlocking->integer ) {//blocking with saber ent->client->ps.saberBlockingTime = level.time + FRAMETIME; } WP_ForcePowersUpdate( ent, ucmd ); //if we have the saber in hand, check for starting a block to reflect shots WP_SaberStartMissileBlockCheck( ent, ucmd ); // Update the position of the saber, and check to see if we're throwing it if ( client->ps.saberEntityNum != ENTITYNUM_NONE ) { int updates = 1; if ( ent->NPC ) { updates = 3;//simulate player update rate? } for ( int update = 0; update < updates; update++ ) { WP_SaberUpdate( ent, ucmd ); } } //NEED to do this every frame, since these overrides do not go into the save/load data if ( ent->client->ps.vehicleModel != 0 ) { cg.overrides.active |= (CG_OVERRIDE_3RD_PERSON_RNG|CG_OVERRIDE_FOV); cg.overrides.thirdPersonRange = 240; cg.overrides.fov = 100; } else if ( client->ps.eFlags&EF_IN_ATST ) { cg.overrides.active |= (CG_OVERRIDE_3RD_PERSON_RNG|CG_OVERRIDE_3RD_PERSON_POF|CG_OVERRIDE_3RD_PERSON_VOF); cg.overrides.thirdPersonRange = 240; if ( cg_thirdPersonAutoAlpha.integer ) { if ( ent->health > 0 && ent->client->ps.viewangles[PITCH] < 15 && ent->client->ps.viewangles[PITCH] > 0 ) { cg.overrides.active |= CG_OVERRIDE_3RD_PERSON_APH; if ( cg.overrides.thirdPersonAlpha > 0.525f ) { cg.overrides.thirdPersonAlpha -= 0.025f; } else if ( cg.overrides.thirdPersonAlpha > 0.5f ) { cg.overrides.thirdPersonAlpha = 0.5f; } } else if ( cg.overrides.active&CG_OVERRIDE_3RD_PERSON_APH ) { if ( cg.overrides.thirdPersonAlpha > cg_thirdPersonAlpha.value ) { cg.overrides.active &= ~CG_OVERRIDE_3RD_PERSON_APH; } else if ( cg.overrides.thirdPersonAlpha < cg_thirdPersonAlpha.value-0.1f ) { cg.overrides.thirdPersonAlpha += 0.1f; } else if ( cg.overrides.thirdPersonAlpha < cg_thirdPersonAlpha.value ) { cg.overrides.thirdPersonAlpha = cg_thirdPersonAlpha.value; cg.overrides.active &= ~CG_OVERRIDE_3RD_PERSON_APH; } } } if ( ent->client->ps.viewangles[PITCH] > 0 ) { cg.overrides.thirdPersonPitchOffset = ent->client->ps.viewangles[PITCH]*-0.75; cg.overrides.thirdPersonVertOffset = 300+ent->client->ps.viewangles[PITCH]*-10; if ( cg.overrides.thirdPersonVertOffset < 0 ) { cg.overrides.thirdPersonVertOffset = 0; } } else if ( ent->client->ps.viewangles[PITCH] < 0 ) { cg.overrides.thirdPersonPitchOffset = ent->client->ps.viewangles[PITCH]*-0.75; cg.overrides.thirdPersonVertOffset = 300+ent->client->ps.viewangles[PITCH]*-5; if ( cg.overrides.thirdPersonVertOffset > 300 ) { cg.overrides.thirdPersonVertOffset = 300; } } else { cg.overrides.thirdPersonPitchOffset = 0; cg.overrides.thirdPersonVertOffset = 200; } } //play/stop any looping sounds tied to controlled movement G_CheckMovingLoopingSounds( ent, ucmd ); //remember your last angles VectorCopy ( ent->client->ps.viewangles, ent->lastAngles ); // set up for pmove oldEventSequence = client->ps.eventSequence; memset( &pm, 0, sizeof(pm) ); pm.gent = ent; pm.ps = &client->ps; pm.cmd = *ucmd; // pm.tracemask = MASK_PLAYERSOLID; // used differently for navgen pm.tracemask = ent->clipmask; pm.trace = gi.trace; pm.pointcontents = gi.pointcontents; pm.debugLevel = g_debugMove->integer; pm.noFootsteps = qfalse;//( g_dmflags->integer & DF_NO_FOOTSTEPS ) > 0; VectorCopy( client->ps.origin, oldOrigin ); // perform a pmove Pmove( &pm ); ProcessGenericCmd(ent, pm.cmd.generic_cmd); // save results of pmove if ( ent->client->ps.eventSequence != oldEventSequence ) { ent->eventTime = level.time; { int seq; seq = (ent->client->ps.eventSequence-1) & (MAX_PS_EVENTS-1); ent->s.event = ent->client->ps.events[ seq ] | ( ( ent->client->ps.eventSequence & 3 ) << 8 ); ent->s.eventParm = ent->client->ps.eventParms[ seq ]; } } PlayerStateToEntityState( &ent->client->ps, &ent->s ); VectorCopy ( ent->currentOrigin, ent->lastOrigin ); #if 1 // use the precise origin for linking VectorCopy( ent->client->ps.origin, ent->currentOrigin ); #else //We don't use prediction anymore, so screw this // use the snapped origin for linking so it matches client predicted versions VectorCopy( ent->s.pos.trBase, ent->currentOrigin ); #endif //Had to leave this in, some legacy code must still be using s.angles //Shouldn't interfere with interpolation of angles, should it? VectorCopy( ent->client->ps.viewangles, ent->currentAngles ); VectorCopy( pm.mins, ent->mins ); VectorCopy( pm.maxs, ent->maxs ); ent->waterlevel = pm.waterlevel; ent->watertype = pm.watertype; VectorCopyM( ucmd->angles, client->pers.cmd_angles ); // execute client events ClientEvents( ent, oldEventSequence ); //Stun Baton is _always_ firing if (ent->s.weapon == WP_STUN_BATON) { //Use alt-fire to indicate not to make a noise, but do inflict damage FireWeapon(ent, qtrue); } if ( pm.useEvent ) { //TODO: Use TryUse( ent ); } if ( pm.altUseEvent ) { TryAltUse( ent ); } // link entity now, after any personal teleporters have been used gi.linkentity( ent ); ent->client->hiddenDist = 0; if ( !ent->client->noclip ) { G_TouchTriggersLerped( ent ); } // touch other objects ClientImpacts( ent, &pm ); // swap and latch button actions client->oldbuttons = client->buttons; client->buttons = ucmd->buttons; client->latched_buttons |= client->buttons & ~client->oldbuttons; // check for respawning if ( client->ps.stats[STAT_HEALTH] <= 0 ) { // wait for the attack button to be pressed if ( ent->NPC == NULL && level.time > client->respawnTime ) { // don't allow respawn if they are still flying through the // air, unless 10 extra seconds have passed, meaning something // strange is going on, like the corpse is caught in a wind tunnel /* if ( level.time < client->respawnTime + 10000 ) { if ( client->ps.groundEntityNum == ENTITYNUM_NONE ) { return; } } */ // pressing attack or use is the normal respawn method if ( ucmd->buttons & ( BUTTON_ATTACK ) ) { respawn( ent ); // gi.SendConsoleCommand( va("disconnect;wait;wait;wait;wait;wait;wait;devmap %s\n",level.mapname) ); } } if ( ent && !ent->s.number && ent->enemy && ent->enemy != ent && ent->enemy->s.number < ENTITYNUM_WORLD && ent->enemy->inuse && !(cg.overrides.active&CG_OVERRIDE_3RD_PERSON_ANG) ) {//keep facing enemy vec3_t deadDir; float deadYaw; VectorSubtract( ent->enemy->currentOrigin, ent->currentOrigin, deadDir ); deadYaw = AngleNormalize180( vectoyaw ( deadDir ) ); if ( deadYaw > ent->client->ps.stats[STAT_DEAD_YAW] + 1 ) { ent->client->ps.stats[STAT_DEAD_YAW]++; } else if ( deadYaw < ent->client->ps.stats[STAT_DEAD_YAW] - 1 ) { ent->client->ps.stats[STAT_DEAD_YAW]--; } else { ent->client->ps.stats[STAT_DEAD_YAW] = deadYaw; } } return; } // perform once-a-second actions ClientTimerActions( ent, msec ); ClientEndPowerUps( ent ); //DEBUG INFO /* if ( client->ps.clientNum < 1 ) {//Only a player if ( ucmd->buttons & BUTTON_USE ) { NAV_PrintLocalWpDebugInfo( ent ); } } */ //try some idle anims on ent if getting no input and not moving for some time G_CheckClientIdle( ent, ucmd ); } /* ================== ClientThink A new command has arrived from the client ================== */ extern void PM_CheckForceUseButton( gentity_t *ent, usercmd_t *ucmd ); extern qboolean PM_GentCantJump( gentity_t *gent ); void ClientThink( int clientNum, usercmd_t *ucmd ) { gentity_t *ent; qboolean restore_ucmd = qfalse; usercmd_t sav_ucmd = {0}; ent = g_entities + clientNum; if ( !ent->s.number ) { if ( ent->client->ps.viewEntity > 0 && ent->client->ps.viewEntity < ENTITYNUM_WORLD ) {//you're controlling another NPC gentity_t *controlled = &g_entities[ent->client->ps.viewEntity]; qboolean freed = qfalse; if ( controlled->NPC && controlled->NPC->controlledTime && ent->client->ps.forcePowerLevel[FP_TELEPATHY] > FORCE_LEVEL_3 ) {//An NPC I'm controlling with mind trick if ( controlled->NPC->controlledTime < level.time ) {//time's up! G_ClearViewEntity( ent ); freed = qtrue; } else if ( ucmd->upmove > 0 ) {//jumping gets you out of it FIXME: check some other button instead... like ESCAPE... so you could even have total control over an NPC? G_ClearViewEntity( ent ); ucmd->upmove = 0;//ucmd->buttons = 0; //stop player from doing anything for a half second after ent->aimDebounceTime = level.time + 500; freed = qtrue; } } else if ( controlled->NPC //an NPC && PM_GentCantJump( controlled ) //that cannot jump && controlled->NPC->stats.moveType != MT_FLYSWIM ) //and does not use upmove to fly {//these types use jump to get out if ( ucmd->upmove > 0 ) {//jumping gets you out of it FIXME: check some other button instead... like ESCAPE... so you could even have total control over an NPC? G_ClearViewEntity( ent ); ucmd->upmove = 0;//ucmd->buttons = 0; //stop player from doing anything for a half second after ent->aimDebounceTime = level.time + 500; freed = qtrue; } } else {//others use the blocking key, button3 if ( (ucmd->buttons&BUTTON_BLOCKING) ) {//jumping gets you out of it FIXME: check some other button instead... like ESCAPE... so you could even have total control over an NPC? G_ClearViewEntity( ent ); ucmd->buttons = 0; freed = qtrue; } } if ( !freed ) {//still controlling, save off my ucmd and clear it for my actual run through pmove restore_ucmd = qtrue; memcpy( &sav_ucmd, ucmd, sizeof( usercmd_t ) ); memset( ucmd, 0, sizeof( usercmd_t ) ); //to keep pointing in same dir, need to set ucmd->angles ucmd->angles[PITCH] = ANGLE2SHORT( ent->client->ps.viewangles[PITCH] ) - ent->client->ps.delta_angles[PITCH]; ucmd->angles[YAW] = ANGLE2SHORT( ent->client->ps.viewangles[YAW] ) - ent->client->ps.delta_angles[YAW]; ucmd->angles[ROLL] = 0; } else { ucmd->angles[PITCH] = ANGLE2SHORT( ent->client->ps.viewangles[PITCH] ) - ent->client->ps.delta_angles[PITCH]; ucmd->angles[YAW] = ANGLE2SHORT( ent->client->ps.viewangles[YAW] ) - ent->client->ps.delta_angles[YAW]; ucmd->angles[ROLL] = 0; } } else if ( ent->client->NPC_class == CLASS_ATST ) { if ( ucmd->upmove > 0 ) {//get out of ATST GEntity_UseFunc( ent->activator, ent, ent ); ucmd->upmove = 0;//ucmd->buttons = 0; } } if ( (ucmd->buttons&BUTTON_BLOCKING) && !g_saberAutoBlocking->integer ) { ucmd->buttons &= ~(BUTTON_ATTACK|BUTTON_ALT_ATTACK); } PM_CheckForceUseButton( ent, ucmd ); } ent->client->usercmd = *ucmd; // if ( !g_syncronousClients->integer ) { ClientThink_real( ent, ucmd ); } // ClientThink_real can end up freeing this ent, need to check if ( restore_ucmd && ent->client ) {//restore ucmd for later so NPC you're controlling can refer to them memcpy( &ent->client->usercmd, &sav_ucmd, sizeof( usercmd_t ) ); } if ( ent->s.number ) {//NPCs drown, burn from lava, etc, also P_WorldEffects( ent ); } } void ClientEndPowerUps( gentity_t *ent ) { int i; if ( ent == NULL || ent->client == NULL ) { return; } // turn off any expired powerups for ( i = 0 ; i < MAX_POWERUPS ; i++ ) { if ( ent->client->ps.powerups[ i ] < level.time ) { ent->client->ps.powerups[ i ] = 0; } } } /* ============== ClientEndFrame Called at the end of each server frame for each connected client A fast client will have multiple ClientThink for each ClientEdFrame, while a slow client may have multiple ClientEndFrame between ClientThink. ============== */ void ClientEndFrame( gentity_t *ent ) { // // If the end of unit layout is displayed, don't give // the player any normal movement attributes // // burn from lava, etc P_WorldEffects (ent); // apply all the damage taken this frame P_DamageFeedback (ent); // add the EF_CONNECTION flag if we haven't gotten commands recently if ( level.time - ent->client->lastCmdTime > 1000 ) { ent->s.eFlags |= EF_CONNECTION; } else { ent->s.eFlags &= ~EF_CONNECTION; } ent->client->ps.stats[STAT_HEALTH] = ent->health; // FIXME: get rid of ent->health... // G_SetClientSound (ent); }