/*
========================
CG_InterpolatePlayerState

Generates cg.cur_lc->predictedPlayerState by interpolating between
cg.snap->player_state and cg.nextFrame->player_state
========================
*/
static void CG_InterpolatePlayerState( qboolean grabAngles ) {
	float			f;
	int				i;
	playerState_t	*out;
	snapshot_t		*prev, *next;
	playerState_t	*prevPS, *nextPS;

	out = &cg.cur_lc->predictedPlayerState;
	prev = cg.snap;
	next = cg.nextSnap;

	*out = *cg.cur_ps;

	// if we are still allowing local input, short circuit the view angles
	if ( grabAngles ) {
		usercmd_t	cmd;
		int			cmdNum;

		cmdNum = trap_GetCurrentCmdNumber();
		trap_GetUserCmd( cmdNum, &cmd, cg.cur_localPlayerNum );

		PM_UpdateViewAngles( out, &cmd );
	}

	// if the next frame is a teleport, we can't lerp to it
	if ( cg.nextFrameTeleport ) {
		return;
	}

	if ( !next || next->serverTime <= prev->serverTime ) {
		return;
	}

	if (prev->playerNums[cg.cur_localPlayerNum] == -1 ||
		next->playerNums[cg.cur_localPlayerNum] == -1) {
		return;
	}

	prevPS = &prev->pss[cg.cur_localPlayerNum];
	nextPS = &next->pss[cg.cur_localPlayerNum];

	f = (float)( cg.time - prev->serverTime ) / ( next->serverTime - prev->serverTime );

	i = nextPS->bobCycle;
	if ( i < prevPS->bobCycle ) {
		i += 256;		// handle wraparound
	}
	out->bobCycle = prevPS->bobCycle + f * ( i - prevPS->bobCycle );

	for ( i = 0 ; i < 3 ; i++ ) {
		out->origin[i] = prevPS->origin[i] + f * (nextPS->origin[i] - prevPS->origin[i] );
		if ( !grabAngles ) {
			out->viewangles[i] = LerpAngle( 
				prevPS->viewangles[i], nextPS->viewangles[i], f );
		}
		out->velocity[i] = prevPS->velocity[i] + 
			f * (nextPS->velocity[i] - prevPS->velocity[i] );
	}

}
Exemple #2
0
static void CG_DemosUpdatePlayer( void ) {
	demo.oldcmd = demo.cmd;
	trap_GetUserCmd( trap_GetCurrentCmdNumber(), &demo.cmd );
	demoMoveUpdateAngles();
	demoMoveDeltaCmd();

	if ( demo.seekEnabled ) {
		int delta;
		demo.play.fraction -= demo.serverDeltaTime * 1000 * demo.cmdDeltaAngles[YAW];
		delta = (int)demo.play.fraction;
		demo.play.time += delta;
		demo.play.fraction -= delta;
	
		if (demo.deltaRight) {
			int interval = mov_seekInterval.value * 1000;
			int rem = demo.play.time % interval;
			if (demo.deltaRight > 0) {
                demo.play.time += interval - rem;
			} else {
                demo.play.time -= interval + rem;
			}
			demo.play.fraction = 0;
		}
		if (demo.play.fraction < 0) {
			demo.play.fraction += 1;
			demo.play.time -= 1;
		}
		if (demo.play.time < 0) {
			demo.play.time = demo.play.fraction = 0;
		}
		return;
	}
	switch ( demo.editType ) {
	case editCamera:
		cameraMove();
		break;
	case editChase:
		demoMoveChase();
		break;
	case editDof:
		dofMove();
		break;
	case editEffect:
		demoEffectMove();
		break;
	case editLine:
		demoMoveLine();
		break;
	}
}
Exemple #3
0
static int CG_CalcFov( void )
{
  float     y;
  float     phase;
  float     v;
  int       contents;
  float     fov_x, fov_y;
  float     zoomFov;
  float     f;
  int       inwater;
  int       attribFov;
  usercmd_t cmd;
  usercmd_t oldcmd;
  int       cmdNum;

  cmdNum = trap_GetCurrentCmdNumber( );
  trap_GetUserCmd( cmdNum, &cmd );
  trap_GetUserCmd( cmdNum - 1, &oldcmd );

  // switch follow modes if necessary: cycle between free -> follow -> third-person follow
  if( cmd.buttons & BUTTON_USE_HOLDABLE && !( oldcmd.buttons & BUTTON_USE_HOLDABLE ) )
  {
    if ( cg.snap->ps.pm_flags & PMF_FOLLOW ) 
    {
      if( !cg.chaseFollow )
        cg.chaseFollow = qtrue;
      else
      {
        cg.chaseFollow = qfalse;
        trap_SendClientCommand( "follow\n" );
      }
    }
    else if ( cg.snap->ps.persistant[ PERS_SPECSTATE ] != SPECTATOR_NOT )
      trap_SendClientCommand( "follow\n" );
  }

  if( cg.predictedPlayerState.pm_type == PM_INTERMISSION ||
      ( cg.snap->ps.persistant[ PERS_SPECSTATE ] != SPECTATOR_NOT ) || 
      ( cg.renderingThirdPerson ) )
  {
    // if in intermission or third person, use a fixed value
    fov_y = BASE_FOV_Y;
  }
  else
  {
    // don't lock the fov globally - we need to be able to change it
    attribFov = BG_Class( cg.predictedPlayerState.stats[ STAT_CLASS ] )->fov * 0.75f;
    fov_y = attribFov;

    if ( fov_y < 1.0f )
      fov_y = 1.0f;
    else if ( fov_y > MAX_FOV_Y )
      fov_y = MAX_FOV_Y;

    if( cg.spawnTime > ( cg.time - FOVWARPTIME ) &&
        BG_ClassHasAbility( cg.predictedPlayerState.stats[ STAT_CLASS ], SCA_FOVWARPS ) )
    {
      float fraction = (float)( cg.time - cg.spawnTime ) / FOVWARPTIME;

      fov_y = MAX_FOV_WARP_Y - ( ( MAX_FOV_WARP_Y - fov_y ) * fraction );
    }

    // account for zooms
    zoomFov = BG_Weapon( cg.predictedPlayerState.weapon )->zoomFov * 0.75f;
    if ( zoomFov < 1.0f )
      zoomFov = 1.0f;
    else if ( zoomFov > attribFov )
      zoomFov = attribFov;

    // only do all the zoom stuff if the client CAN zoom
    // FIXME: zoom control is currently hard coded to BUTTON_ATTACK2
    if( BG_Weapon( cg.predictedPlayerState.weapon )->canZoom )
    {
      if ( cg.zoomed )
      {
        f = ( cg.time - cg.zoomTime ) / (float)ZOOM_TIME;

        if ( f > 1.0f )
          fov_y = zoomFov;
        else
          fov_y = fov_y + f * ( zoomFov - fov_y );

        // BUTTON_ATTACK2 isn't held so unzoom next time
        if( !( cmd.buttons & BUTTON_ATTACK2 ) )
        {
          cg.zoomed   = qfalse;
          cg.zoomTime = MIN( cg.time, 
              cg.time + cg.time - cg.zoomTime - ZOOM_TIME );
        }
      }
      else
      {
        f = ( cg.time - cg.zoomTime ) / (float)ZOOM_TIME;

        if ( f <= 1.0f )
          fov_y = zoomFov + f * ( fov_y - zoomFov );

        // BUTTON_ATTACK2 is held so zoom next time
        if( cmd.buttons & BUTTON_ATTACK2 )
        {
          cg.zoomed   = qtrue;
          cg.zoomTime = MIN( cg.time, 
              cg.time + cg.time - cg.zoomTime - ZOOM_TIME );
        }
      }
    }
  }

  y = cg.refdef.height / tan( 0.5f * DEG2RAD( fov_y ) );
  fov_x = atan2( cg.refdef.width, y );
  fov_x = 2.0f * RAD2DEG( fov_x );

  // warp if underwater
  contents = CG_PointContents( cg.refdef.vieworg, -1 );

  if( contents & ( CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA ) )
  {
    phase = cg.time / 1000.0f * WAVE_FREQUENCY * M_PI * 2.0f;
    v = WAVE_AMPLITUDE * sin( phase );
    fov_x += v;
    fov_y -= v;
    inwater = qtrue;
  }
  else
    inwater = qfalse;

  if( ( cg.predictedPlayerEntity.currentState.eFlags & EF_POISONCLOUDED ) &&
      ( cg.time - cg.poisonedTime < PCLOUD_DISORIENT_DURATION) &&
      cg.predictedPlayerState.stats[ STAT_HEALTH ] > 0 &&
      !( cg.snap->ps.pm_flags & PMF_FOLLOW ) )
  {
    float scale = 1.0f - (float)( cg.time - cg.poisonedTime ) /
                  BG_PlayerPoisonCloudTime( &cg.predictedPlayerState );
      
    phase = ( cg.time - cg.poisonedTime ) / 1000.0f * PCLOUD_ZOOM_FREQUENCY * M_PI * 2.0f;
    v = PCLOUD_ZOOM_AMPLITUDE * sin( phase ) * scale;
    fov_x += v;
    fov_y += v;
  }


  // set it
  cg.refdef.fov_x = fov_x;
  cg.refdef.fov_y = fov_y;

  if( !cg.zoomed )
    cg.zoomSensitivity = 1.0f;
  else
    cg.zoomSensitivity = cg.refdef.fov_y / 75.0f;

  return inwater;
}
Exemple #4
0
/*
===============
CG_OffsetFirstPersonView

===============
*/
void CG_OffsetFirstPersonView( void )
{
  float         *origin;
  float         *angles;
  float         bob;
  float         ratio;
  float         delta;
  float         speed;
  float         f;
  vec3_t        predictedVelocity;
  int           timeDelta;
  float         bob2;
  vec3_t        normal, baseOrigin;
  playerState_t *ps = &cg.predictedPlayerState;

  BG_GetClientNormal( ps, normal );

  if( cg.snap->ps.pm_type == PM_INTERMISSION )
    return;

  origin = cg.refdef.vieworg;
  angles = cg.refdefViewAngles;

  VectorCopy( origin, baseOrigin );

  // if dead, fix the angle and don't add any kick
  if( cg.snap->ps.stats[ STAT_HEALTH ] <= 0 )
  {
    angles[ ROLL ] = 40;
    angles[ PITCH ] = -15;
    angles[ YAW ] = cg.snap->ps.stats[ STAT_VIEWLOCK ];
    origin[ 2 ] += cg.predictedPlayerState.viewheight;
    return;
  }
  // camera shake effect
  else if( cg.snap->ps.stats[ STAT_SHAKE ] > 0 )
  {
    float fac;

    fac = (float) cg.snap->ps.stats[ STAT_SHAKE ] *
          cg_cameraShakeMagnitude.value * 0.15f;

    angles[ 0 ] += crandom() * fac;
    angles[ 1 ] += crandom() * fac;
    angles[ 2 ] += crandom() * fac;
  }

  // add angles based on damage kick
  if( cg.damageTime )
  {
    ratio = cg.time - cg.damageTime;
    if( ratio < DAMAGE_DEFLECT_TIME )
    {
      ratio /= DAMAGE_DEFLECT_TIME;
      angles[ PITCH ] += ratio * cg.v_dmg_pitch;
      angles[ ROLL ] += ratio * cg.v_dmg_roll;
    }
    else
    {
      ratio = 1.0 - ( ratio - DAMAGE_DEFLECT_TIME ) / DAMAGE_RETURN_TIME;
      if( ratio > 0 )
      {
        angles[ PITCH ] += ratio * cg.v_dmg_pitch;
        angles[ ROLL ] += ratio * cg.v_dmg_roll;
      }
    }
  }

  // add pitch based on fall kick
#if 0
  ratio = ( cg.time - cg.landTime) / FALL_TIME;
  if (ratio < 0)
    ratio = 0;
  angles[PITCH] += ratio * cg.fall_value;
#endif

  // add angles based on velocity
  VectorCopy( cg.predictedPlayerState.velocity, predictedVelocity );

  delta = DotProduct( predictedVelocity, cg.refdef.viewaxis[ 0 ] );
  angles[ PITCH ] += delta * cg_runpitch.value;

  delta = DotProduct( predictedVelocity, cg.refdef.viewaxis[ 1 ] );
  angles[ ROLL ] -= delta * cg_runroll.value;

  // add angles based on bob
  // bob amount is class dependant

  if( cg.snap->ps.persistant[ PERS_SPECSTATE ] != SPECTATOR_NOT )
    bob2 = 0.0f;
  else
    bob2 = BG_Class( cg.predictedPlayerState.stats[ STAT_CLASS ] )->bob;


#define LEVEL4_FEEDBACK  10.0f

  //give a charging player some feedback
  if( ps->weapon == WP_ALEVEL4 )
  {
    if( ps->stats[ STAT_MISC ] > 0 )
    {
      float fraction = (float)ps->stats[ STAT_MISC ] /
                       LEVEL4_TRAMPLE_CHARGE_MAX;

      if( fraction > 1.0f )
        fraction = 1.0f;

      bob2 *= ( 1.0f + fraction * LEVEL4_FEEDBACK );
    }
  }

  if( bob2 != 0.0f )
  {
    // make sure the bob is visible even at low speeds
    speed = cg.xyspeed > 200 ? cg.xyspeed : 200;

    delta = cg.bobfracsin * ( bob2 ) * speed;
    if( cg.predictedPlayerState.pm_flags & PMF_DUCKED )
      delta *= 3;   // crouching

    angles[ PITCH ] += delta;
    delta = cg.bobfracsin * ( bob2 ) * speed;
    if( cg.predictedPlayerState.pm_flags & PMF_DUCKED )
      delta *= 3;   // crouching accentuates roll

    if( cg.bobcycle & 1 )
      delta = -delta;

    angles[ ROLL ] += delta;
  }

#define LEVEL3_FEEDBACK  20.0f

  //provide some feedback for pouncing
  if( ( cg.predictedPlayerState.weapon == WP_ALEVEL3 ||
        cg.predictedPlayerState.weapon == WP_ALEVEL3_UPG ) &&
      cg.predictedPlayerState.stats[ STAT_MISC ] > 0 )
  {
    float fraction1, fraction2;
    vec3_t forward;

    AngleVectors( angles, forward, NULL, NULL );
    VectorNormalize( forward );

    fraction1 = (float)cg.predictedPlayerState.stats[ STAT_MISC ] /
                LEVEL3_POUNCE_TIME_UPG;
    if( fraction1 > 1.0f )
      fraction1 = 1.0f;

    fraction2 = -sin( fraction1 * M_PI / 2 );

    VectorMA( origin, LEVEL3_FEEDBACK * fraction2, forward, origin );
  }

#define STRUGGLE_DIST 5.0f
#define STRUGGLE_TIME 250

  //allow the player to struggle a little whilst grabbed
  if( cg.predictedPlayerState.pm_type == PM_GRABBED )
  {
    vec3_t    forward, right, up;
    usercmd_t cmd;
    int       cmdNum;
    float     fFraction, rFraction, uFraction;
    float     fFraction2, rFraction2, uFraction2;

    cmdNum = trap_GetCurrentCmdNumber();
    trap_GetUserCmd( cmdNum, &cmd );

    AngleVectors( angles, forward, right, up );

    fFraction = (float)( cg.time - cg.forwardMoveTime ) / STRUGGLE_TIME;
    rFraction = (float)( cg.time - cg.rightMoveTime ) / STRUGGLE_TIME;
    uFraction = (float)( cg.time - cg.upMoveTime ) / STRUGGLE_TIME;

    if( fFraction > 1.0f )
      fFraction = 1.0f;
    if( rFraction > 1.0f )
      rFraction = 1.0f;
    if( uFraction > 1.0f )
      uFraction = 1.0f;

    fFraction2 = -sin( fFraction * M_PI / 2 );
    rFraction2 = -sin( rFraction * M_PI / 2 );
    uFraction2 = -sin( uFraction * M_PI / 2 );

    if( cmd.forwardmove > 0 )
      VectorMA( origin, STRUGGLE_DIST * fFraction, forward, origin );
    else if( cmd.forwardmove < 0 )
      VectorMA( origin, -STRUGGLE_DIST * fFraction, forward, origin );
    else
      cg.forwardMoveTime = cg.time;

    if( cmd.rightmove > 0 )
      VectorMA( origin, STRUGGLE_DIST * rFraction, right, origin );
    else if( cmd.rightmove < 0 )
      VectorMA( origin, -STRUGGLE_DIST * rFraction, right, origin );
    else
      cg.rightMoveTime = cg.time;

    if( cmd.upmove > 0 )
      VectorMA( origin, STRUGGLE_DIST * uFraction, up, origin );
    else if( cmd.upmove < 0 )
      VectorMA( origin, -STRUGGLE_DIST * uFraction, up, origin );
    else
      cg.upMoveTime = cg.time;
  }

  if( ( cg.predictedPlayerEntity.currentState.eFlags & EF_POISONCLOUDED ) &&
      ( cg.time - cg.poisonedTime < PCLOUD_DISORIENT_DURATION) &&
      !( cg.snap->ps.pm_flags & PMF_FOLLOW ) )
  {
    float scale, fraction, pitchFraction;
    
    scale = 1.0f - (float)( cg.time - cg.poisonedTime ) /
            BG_PlayerPoisonCloudTime( &cg.predictedPlayerState );
    if( scale < 0.0f )
      scale = 0.0f;

    fraction = sin( ( cg.time - cg.poisonedTime ) / 500.0f * M_PI * PCLOUD_ROLL_FREQUENCY ) *
               scale;
    pitchFraction = sin( ( cg.time - cg.poisonedTime ) / 200.0f * M_PI * PCLOUD_ROLL_FREQUENCY ) *
                    scale;

    angles[ ROLL ] += fraction * PCLOUD_ROLL_AMPLITUDE;
    angles[ YAW ] += fraction * PCLOUD_ROLL_AMPLITUDE;
    angles[ PITCH ] += pitchFraction * PCLOUD_ROLL_AMPLITUDE / 2.0f;
  }

  // this *feels* more realisitic for humans
  if( cg.predictedPlayerState.stats[ STAT_TEAM ] == TEAM_HUMANS &&
      ( cg.predictedPlayerState.pm_type == PM_NORMAL ||
        cg.predictedPlayerState.pm_type == PM_JETPACK ) )
  {
    angles[PITCH] += cg.bobfracsin * bob2 * 0.5;

    // heavy breathing effects //FIXME: sound
    if( cg.predictedPlayerState.stats[ STAT_STAMINA ] < STAMINA_BREATHING_LEVEL )
    {
      float deltaBreath = ( cg.predictedPlayerState.stats[ STAT_STAMINA ] -
                            STAMINA_BREATHING_LEVEL ) / -250.0;
      float deltaAngle = cos( (float)cg.time/150.0 ) * deltaBreath;

      deltaAngle += ( deltaAngle < 0 ? -deltaAngle : deltaAngle ) * 0.5;

      angles[ PITCH ] -= deltaAngle;
    }
  }

//===================================

  // add view height
  VectorMA( origin, ps->viewheight, normal, origin );

  // smooth out duck height changes
  timeDelta = cg.time - cg.duckTime;
  if( timeDelta < DUCK_TIME)
  {
    cg.refdef.vieworg[ 2 ] -= cg.duckChange
      * ( DUCK_TIME - timeDelta ) / DUCK_TIME;
  }

  // add bob height
  bob = cg.bobfracsin * cg.xyspeed * bob2;

  if( bob > 6 )
    bob = 6;

  VectorMA( origin, bob, normal, origin );

  // add fall height
  delta = cg.time - cg.landTime;

  if( delta < LAND_DEFLECT_TIME )
  {
    f = delta / LAND_DEFLECT_TIME;
    cg.refdef.vieworg[ 2 ] += cg.landChange * f;
  }
  else if( delta < LAND_DEFLECT_TIME + LAND_RETURN_TIME )
  {
    delta -= LAND_DEFLECT_TIME;
    f = 1.0 - ( delta / LAND_RETURN_TIME );
    cg.refdef.vieworg[ 2 ] += cg.landChange * f;
  }

  // add step offset
  CG_StepOffset( );
}
Exemple #5
0
/*
===============
CG_OffsetShoulderView

===============
*/
void CG_OffsetShoulderView( void )
{
  int            i;
  int            cmdNum;
  usercmd_t      cmd, oldCmd;
  vec3_t         rotationAngles;
  vec3_t         axis[ 3 ], rotaxis[ 3 ];
  float          deltaMousePitch;
  static float   mousePitch;
  vec3_t         forward, right, up;
  classConfig_t* classConfig;

  // Ignore following pitch; it's too jerky otherwise.
  if( !cg_thirdPersonPitchFollow.integer ) 
    cg.refdefViewAngles[ PITCH ] = 0.0f;
    
  AngleVectors( cg.refdefViewAngles, forward, right, up );

  classConfig = BG_ClassConfig( cg.snap->ps.stats[ STAT_CLASS ] );
  VectorMA( cg.refdef.vieworg, classConfig->shoulderOffsets[ 0 ], forward, cg.refdef.vieworg );
  VectorMA( cg.refdef.vieworg, classConfig->shoulderOffsets[ 1 ], right, cg.refdef.vieworg );
  VectorMA( cg.refdef.vieworg, classConfig->shoulderOffsets[ 2 ], up, cg.refdef.vieworg );

  // If someone is playing like this, the rest is already taken care of
  // so just get the firstperson effects and leave.
  if( !cg.demoPlayback && !( cg.snap->ps.pm_flags & PMF_FOLLOW ) )
  {
    CG_OffsetFirstPersonView();
    return;
  }

  // Get mouse input for camera rotation. 
  cmdNum = trap_GetCurrentCmdNumber();
  trap_GetUserCmd( cmdNum, &cmd );
  trap_GetUserCmd( cmdNum - 1, &oldCmd );

  // Prevent pitch from wrapping and clamp it within a [30, -50] range.
  // Cgame has no access to ps.delta_angles[] here, so we need to reproduce
  // it ourselves here.
  deltaMousePitch = SHORT2ANGLE( cmd.angles[ PITCH ] - oldCmd.angles[ PITCH ] );
  if( fabs(deltaMousePitch) < 200.0f )
    mousePitch += deltaMousePitch;

  // Handle pitch.
  rotationAngles[ PITCH ] = mousePitch;

  rotationAngles[ PITCH ] = AngleNormalize180( rotationAngles[ PITCH ] + AngleNormalize180( cg.refdefViewAngles[ PITCH ] ) );
  if( rotationAngles [ PITCH ] < -90.0f ) rotationAngles [ PITCH ] = -90.0f;
  if( rotationAngles [ PITCH ] > 90.0f ) rotationAngles [ PITCH ] = 90.0f;

  // Yaw and Roll are much easier.
  rotationAngles[ YAW ] = SHORT2ANGLE( cmd.angles[ YAW ] ) + cg.refdefViewAngles[ YAW ];
  rotationAngles[ ROLL ] = 0.0f;

  // Perform the rotations.
  AnglesToAxis( rotationAngles, axis );
  if( !( cg.snap->ps.stats[ STAT_STATE ] & SS_WALLCLIMBING ) ||
      !BG_RotateAxis( cg.snap->ps.grapplePoint, axis, rotaxis, qfalse,
                      cg.snap->ps.eFlags & EF_WALLCLIMBCEILING ) )
    AxisCopy( axis, rotaxis );

  AxisToAngles( rotaxis, rotationAngles );

  // Actually set the viewangles.
  for( i = 0; i < 3; i++ )
    cg.refdefViewAngles[ i ] = rotationAngles[ i ];

  // Now run the first person stuff so we get various effects added.
  CG_OffsetFirstPersonView( );
}
Exemple #6
0
/*
===============
CG_OffsetThirdPersonView

===============
*/
void CG_OffsetThirdPersonView( void )
{
  int i;
  vec3_t        forward, right, up;
  vec3_t        view;
  trace_t       trace;
  static vec3_t mins = { -8, -8, -8 };
  static vec3_t maxs = { 8, 8, 8 };
  vec3_t        focusPoint;
  vec3_t        surfNormal;
  int           cmdNum;
  usercmd_t     cmd, oldCmd;
  float         range;
  vec3_t        mouseInputAngles;
  vec3_t        rotationAngles;
  vec3_t        axis[ 3 ], rotaxis[ 3 ];
  float         deltaPitch;
  static float  pitch;
  static vec3_t killerPos = { 0, 0, 0 };

  // If cg_thirdpersonShoulderViewMode == 2, do shoulder view instead
  // If cg_thirdpersonShoulderViewMode == 1, do shoulder view when chasing
  //   a wallwalker because it's really erratic to watch
  if( ( cg_thirdPersonShoulderViewMode.integer == 2 ) ||
      ( ( cg_thirdPersonShoulderViewMode.integer == 1 ) &&
        ( cg.snap->ps.stats[ STAT_STATE ] & SS_WALLCLIMBING ) &&
        ( cg.snap->ps.stats[ STAT_HEALTH ] > 0 ) ) )
  {
    CG_OffsetShoulderView( );
    return;
  }

  BG_GetClientNormal( &cg.predictedPlayerState, surfNormal );
  // Set the view origin to the class's view height
  VectorMA( cg.refdef.vieworg, cg.predictedPlayerState.viewheight, surfNormal, cg.refdef.vieworg );

  // Set the focus point where the camera will look (at the player's vieworg)
  VectorCopy( cg.refdef.vieworg, focusPoint );

  // If player is dead, we want the player to be between us and the killer
  // so pretend that the player was looking at the killer, then place cam behind them.
  if( cg.predictedPlayerState.stats[ STAT_HEALTH ] <= 0 )
  {
    int killerEntNum = cg.predictedPlayerState.stats[ STAT_VIEWLOCK ];

    // already looking at ourself
    if( killerEntNum != cg.snap->ps.clientNum )
    {
      vec3_t lookDirection;
      if( cg.wasDeadLastFrame == qfalse || !cg_staticDeathCam.integer )
      {
        VectorCopy( cg_entities[ killerEntNum ].lerpOrigin, killerPos );
        cg.wasDeadLastFrame = qtrue;
      }
      VectorSubtract( killerPos, cg.refdef.vieworg, lookDirection );
      vectoangles( lookDirection, cg.refdefViewAngles );
    }
  }

  // get and rangecheck cg_thirdPersonRange
  range = cg_thirdPersonRange.value;
  if( range > 150.0f ) range = 150.0f;
  if( range < 30.0f ) range = 30.0f;

  // Calculate the angle of the camera's position around the player.
  // Unless in demo, PLAYING in third person, or in dead-third-person cam, allow the player 
  // to control camera position offsets using the mouse position.
  if( cg.demoPlayback || 
    ( ( cg.snap->ps.pm_flags & PMF_FOLLOW ) && 
      ( cg.predictedPlayerState.stats[ STAT_HEALTH ] > 0 ) ) )
  {
    // Collect our input values from the mouse.
    cmdNum = trap_GetCurrentCmdNumber();
    trap_GetUserCmd( cmdNum, &cmd );
    trap_GetUserCmd( cmdNum - 1, &oldCmd );

    // Prevent pitch from wrapping and clamp it within a [-75, 90] range.
    // Cgame has no access to ps.delta_angles[] here, so we need to reproduce
    // it ourselves.
    deltaPitch = SHORT2ANGLE( cmd.angles[ PITCH ] - oldCmd.angles[ PITCH ] );
    if( fabs(deltaPitch) < 200.0f )
    {
      pitch += deltaPitch;
    }

    mouseInputAngles[ PITCH ] = pitch;
    mouseInputAngles[ YAW ] = -1.0f * SHORT2ANGLE( cmd.angles[ YAW ] ); // yaw is inverted
    mouseInputAngles[ ROLL ] = 0.0f;

    for( i = 0; i < 3; i++ )
      mouseInputAngles[ i ] = AngleNormalize180( mouseInputAngles[ i ] );

    // Set the rotation angles to be the view angles offset by the mouse input
    // Ignore the original pitch though; it's too jerky otherwise
    if( !cg_thirdPersonPitchFollow.integer ) 
      cg.refdefViewAngles[ PITCH ] = 0.0f;

    for( i = 0; i < 3; i++ )
    {
      rotationAngles[ i ] = AngleNormalize180(cg.refdefViewAngles[ i ]) + mouseInputAngles[ i ];
      AngleNormalize180( rotationAngles[ i ] );
    }

    // Don't let pitch go too high/too low or the camera flips around and
    // that's really annoying.
    // However, when we're not on the floor or ceiling (wallwalk) pitch 
    // may not be pitch, so just let it go.
    if( surfNormal[ 2 ] > 0.5f || surfNormal[ 2 ] < -0.5f ) 
    {
      if( rotationAngles[ PITCH ] > 85.0f )
        rotationAngles[ PITCH ] = 85.0f;
      else if( rotationAngles[ PITCH ] < -85.0f )
        rotationAngles[ PITCH ] = -85.0f;
    }

    // Perform the rotations specified by rotationAngles.
    AnglesToAxis( rotationAngles, axis );
    if( !( cg.snap->ps.stats[ STAT_STATE ] & SS_WALLCLIMBING ) ||
        !BG_RotateAxis( cg.snap->ps.grapplePoint, axis, rotaxis, qfalse,
                        cg.snap->ps.eFlags & EF_WALLCLIMBCEILING ) )
      AxisCopy( axis, rotaxis );

    // Convert the new axis back to angles.
    AxisToAngles( rotaxis, rotationAngles );
  }
  else 
  {
    if( cg.predictedPlayerState.stats[ STAT_HEALTH ] > 0 )
    {
      // If we're playing the game in third person, the viewangles already
      // take care of our mouselook, so just use them.
      for( i = 0; i < 3; i++ )
        rotationAngles[ i ] = cg.refdefViewAngles[ i ];
    }
    else // dead
    {
      rotationAngles[ PITCH ] = 20.0f;
      rotationAngles[ YAW ] = cg.refdefViewAngles[ YAW ];
    }
  }

  rotationAngles[ YAW ] -= cg_thirdPersonAngle.value;

  // Move the camera range distance back.
  AngleVectors( rotationAngles, forward, right, up );
  VectorCopy( cg.refdef.vieworg, view );
  VectorMA( view, -range, forward, view );

  // Ensure that the current camera position isn't out of bounds and that there
  // is nothing between the camera and the player.
  if( !cg_cameraMode.integer )
  {
    // Trace a ray from the origin to the viewpoint to make sure the view isn't
    // in a solid block.  Use an 8 by 8 block to prevent the view from near clipping anything
    CG_Trace( &trace, cg.refdef.vieworg, mins, maxs, view, cg.predictedPlayerState.clientNum, MASK_SOLID );

    if( trace.fraction != 1.0f )
    {
      VectorCopy( trace.endpos, view );
      view[ 2 ] += ( 1.0f - trace.fraction ) * 32;
      // Try another trace to this position, because a tunnel may have the ceiling
      // close enogh that this is poking out.

      CG_Trace( &trace, cg.refdef.vieworg, mins, maxs, view, cg.predictedPlayerState.clientNum, MASK_SOLID );
      VectorCopy( trace.endpos, view );
    }
  }

  // Set the camera position to what we calculated.
  VectorCopy( view, cg.refdef.vieworg );

  // The above checks may have moved the camera such that the existing viewangles
  // may not still face the player. Recalculate them to do so.
  // but if we're dead, don't bother because we'd rather see what killed us
  if( cg.predictedPlayerState.stats[ STAT_HEALTH ] > 0 )
  {
    VectorSubtract( focusPoint, cg.refdef.vieworg, focusPoint );
    vectoangles( focusPoint, cg.refdefViewAngles );
  }
}
Exemple #7
0
// Mouse overlay for controlling multiview windows
void CG_cursorUpdate(void)
{
	int i, j, x;
	float nx, ny;
	int nSelectedWindow = -1;
	cg_window_t *w;
	cg_windowHandler_t *wh = &cg.winHandler;
	qboolean fFound = qfalse, fUpdateOverlay = qfalse;
	qboolean fSelect, fResize;


	// Get cursor current position (when connected to a server)
	if(!cg.demoPlayback) {

		// Allow for limbo'd updates as well
		trap_GetUserCmd(trap_GetCurrentCmdNumber(), &cg_pmove.cmd);

		nx = 640.0 * (65536.0 - cg_pmove.cmd.angles[1]) / 65536.0;
		ny = 480.0 / 65536.0 * ((int_m_pitch.value < 0.0) ? (65536.0 - cg_pmove.cmd.angles[0]) : cg_pmove.cmd.angles[0]);

		fSelect = ((cg_pmove.cmd.buttons & BUTTON_ATTACK) != 0);

		if(cgs.cursorX == (int)nx  && cgs.cursorY == (int)ny && !fSelect) {
			return;
		}

		fResize = ((cg_pmove.cmd.buttons & BUTTON_SPRINT) != 0);

		cgs.cursorUpdate = cg.time + 5000;
		cgs.cursorX = nx;
		cgs.cursorY = ny;
	} else {
		// Already updated in the keycatcher
		nx = cgs.cursorX;
		ny = cgs.cursorY;
		fSelect = cgs.fSelect;
		fResize = cgs.fResize;
	}

	// For mm4
	cg.mvCurrentActive = cg.mvCurrentMainview;

	// For overlay highlights
	for(i=0; i<cg.mvTotalClients; i++) {
		cg.mvOverlay[i].fActive = qfalse;
	}

	for(i=wh->numActiveWindows-1; i>=0; i--) {
		w = &wh->window[wh->activeWindows[i]];
		if((w->effects & WFX_MULTIVIEW) && w != cg.mvCurrentMainview) {
			// Mouse/window detection
			// If the current window is selected, and the button is down, then allow the update
			// to occur, as quick mouse movements can move it past the window borders
			if(!fFound &&
			  (
				((w->mvInfo & MV_SELECTED) && fSelect) ||
				(!fSelect && nx >= w->x && nx < w->x + w->w && ny >= w->y && ny < w->y + w->h)
			  )) {
				if(!(w->mvInfo & MV_SELECTED)) {
					w->mvInfo |= MV_SELECTED;
					nSelectedWindow = i;
				}

				// If not dragging/resizing, prime for later update
				if(!fSelect) {
					w->m_x = -1.0f;
					w->m_y = -1.0f;
				} else {
					if(w->m_x > 0 && w->m_y > 0) {
						if(fResize) {
							w->w += nx - w->m_x;
							if(w->x + w->w > 640-2) w->w = 640 - 2 - w->x;
							if(w->w < 64) w->w = 64;

							w->h += ny - w->m_y;
							if(w->y + w->h > 480-2) w->h = 480 - 2 - w->y;
							if(w->h < 48) w->h = 48;
						} else {
							w->x += nx - w->m_x;
							if(w->x + w->w > 640-2) w->x = 640 - 2 - w->w;
							if(w->x < 2) w->x = 2;

							w->y += ny - w->m_y;
							if(w->y + w->h > 480-2) w->y = 480 - 2 - w->h;
							if(w->y < 2) w->y = 2;
						}
					}

					w->m_x = nx;
					w->m_y = ny;
				}

				fFound = qtrue;
				cg.mvCurrentActive = w;

			// Reset mouse info for window if it loses focuse
			} else if(w->mvInfo & MV_SELECTED) {
				fUpdateOverlay = qtrue;
				w->m_x = -1.0f;
				w->m_y = -1.0f;
				w->mvInfo &= ~MV_SELECTED;

				if(fFound) break;		// Small optimization: we've found a new window, and cleared the old focus
			}
		}
	}

	nx = (float)(MVINFO_RIGHT - (MVINFO_TEXTSIZE * 3));
	ny = (float)(MVINFO_TOP + (MVINFO_TEXTSIZE + 1));

	// Highlight corresponding active window's overlay element
	if(fFound) {
		for(i=0; i<cg.mvTotalClients; i++) {
			if(cg.mvOverlay[i].pID == (cg.mvCurrentActive->mvInfo & MV_PID)) {
				cg.mvOverlay[i].fActive = qtrue;
				break;
			}
		}
	}

	// Check MV overlay detection here for better perf with more text elements
	// General boundary check
	else {
		// Ugh, have to loop through BOTH team lists
		int vOffset = 0;

		for(i=TEAM_AXIS; i<=TEAM_ALLIES; i++) {
			if(cg.mvTotalTeam[i] == 0) continue;
			if(cgs.cursorX >= nx && cgs.cursorY >= ny && cgs.cursorX < MVINFO_RIGHT &&
			  cgs.cursorY < ny + (cg.mvTotalTeam[i] * (MVINFO_TEXTSIZE + 1))) {
				int pos = (int)(cgs.cursorY - ny) / (MVINFO_TEXTSIZE + 1);

				if(pos < cg.mvTotalTeam[i]) {
					int x = MVINFO_RIGHT - cg.mvOverlay[(cg.mvTeamList[i][pos])].width;
					int y = MVINFO_TOP + vOffset + ((pos + 1) * (MVINFO_TEXTSIZE + 1));

					// See if we're really over something
					if(cgs.cursorX >= x && cgs.cursorY >= y &&
					   cgs.cursorX <= MVINFO_RIGHT &&
					   cgs.cursorY <= y + MVINFO_TEXTSIZE) {
						// Perform any other window handling here for MV
						// views based on element selection
						cg.mvOverlay[(cg.mvTeamList[i][pos])].fActive =	qtrue;
						
						w = CG_mvClientLocate(cg.mvOverlay[(cg.mvTeamList[i][pos])].pID);
						if(w != NULL) {
							cg.mvCurrentActive = w;
						}

						if(fSelect) {
							if(w != NULL) {
								// Swap window-view with mainview
								if(w != cg.mvCurrentMainview) {
									CG_mvMainviewSwap(w);
								}
							} else {
								// Swap non-view with mainview
								cg.mvCurrentMainview->mvInfo = (cg.mvCurrentMainview->mvInfo & ~MV_PID) |
															   (cg.mvOverlay[cg.mvTeamList[i][pos]].pID & MV_PID);
								fUpdateOverlay = qtrue;
							}
						}
					}
				}
			}
			vOffset += (cg.mvTotalTeam[i] + 2) * (MVINFO_TEXTSIZE + 1);
			ny += vOffset;
		}
	}

	// If we have a new highlight, reorder so our highlight is always
	// drawn last (on top of all other windows)
	if(nSelectedWindow >= 0) {
		fUpdateOverlay = qtrue;
		x = wh->activeWindows[nSelectedWindow];
		
		for(j=nSelectedWindow; j<wh->numActiveWindows-1; j++) {
			wh->activeWindows[j] = wh->activeWindows[j+1];
		}
		
		wh->activeWindows[wh->numActiveWindows-1] = x;
	}

	// Finally, sync the overlay, if needed
	if(fUpdateOverlay) {
		CG_mvOverlayUpdate();
	}
}
static int CG_CalcFov( void )
{
	float     y;
	float     phase;
	float     v;
	int       contents;
	float     fov_x, fov_y;
	float     zoomFov;
	float     f;
	int       inwater;
	int       attribFov;
	usercmd_t cmd;
	usercmd_t oldcmd;
	int       cmdNum;

	cmdNum = trap_GetCurrentCmdNumber();
	trap_GetUserCmd( cmdNum, &cmd );
	trap_GetUserCmd( cmdNum - 1, &oldcmd );

	// switch follow modes if necessary: cycle between free -> follow -> third-person follow
	if ( usercmdButtonPressed( cmd.buttons, BUTTON_USE_HOLDABLE ) && !usercmdButtonPressed( oldcmd.buttons, BUTTON_USE_HOLDABLE ) )
	{
		if ( cg.snap->ps.pm_flags & PMF_FOLLOW )
		{
			if ( !cg.chaseFollow )
			{
				cg.chaseFollow = qtrue;
			}
			else
			{
				cg.chaseFollow = qfalse;
				trap_SendClientCommand( "follow\n" );
			}
		}
		else if ( cg.snap->ps.persistant[ PERS_SPECSTATE ] != SPECTATOR_NOT )
		{
			trap_SendClientCommand( "follow\n" );
		}
	}

	if ( cg.predictedPlayerState.pm_type == PM_INTERMISSION ||
	     ( cg.snap->ps.persistant[ PERS_SPECSTATE ] != SPECTATOR_NOT ) ||
	     ( cg.renderingThirdPerson ) )
	{
		// if in intermission or third person, use a fixed value
		fov_y = BASE_FOV_Y;
	}
	else
	{
		// don't lock the fov globally - we need to be able to change it
		if ( ( attribFov = trap_Cvar_VariableIntegerValue( BG_Class( cg.predictedPlayerState.stats[ STAT_CLASS ] )->fovCvar ) ) )
		{
			if ( attribFov < 80 )
			{
				attribFov = 80;
			}
			else if ( attribFov >= 140 )
			{
				attribFov = 140;
			}
		}
		else
		{
			attribFov = BG_Class( cg.predictedPlayerState.stats[ STAT_CLASS ] )->fov;
		}
		attribFov *= 0.75;
		fov_y = attribFov;

		if ( fov_y < 1.0f )
		{
			fov_y = 1.0f;
		}
		else if ( fov_y > MAX_FOV_Y )
		{
			fov_y = MAX_FOV_Y;
		}

		if ( cg.spawnTime > ( cg.time - FOVWARPTIME ) &&
		     BG_ClassHasAbility( cg.predictedPlayerState.stats[ STAT_CLASS ], SCA_FOVWARPS ) )
		{
			float fraction = ( float )( cg.time - cg.spawnTime ) / FOVWARPTIME;

			fov_y = MAX_FOV_WARP_Y - ( ( MAX_FOV_WARP_Y - fov_y ) * fraction );
		}

		// account for zooms
		zoomFov = BG_Weapon( cg.predictedPlayerState.weapon )->zoomFov * 0.75f;

		if ( zoomFov < 1.0f )
		{
			zoomFov = 1.0f;
		}
		else if ( zoomFov > attribFov )
		{
			zoomFov = attribFov;
		}

		// only do all the zoom stuff if the client CAN zoom
		// FIXME: zoom control is currently hard coded to WBUTTON_ATTACK2
		if ( BG_Weapon( cg.predictedPlayerState.weapon )->canZoom )
		{
			if ( cg.zoomed )
			{
				f = ( cg.time - cg.zoomTime ) / ( float ) ZOOM_TIME;

				if ( f > 1.0f )
				{
					fov_y = zoomFov;
				}
				else
				{
					fov_y = fov_y + f * ( zoomFov - fov_y );
				}

				// WBUTTON_ATTACK2 isn't held so unzoom next time
				if ( !usercmdButtonPressed( cmd.buttons, BUTTON_ATTACK2 ) || cg.snap->ps.weaponstate == WEAPON_RELOADING )
				{
					cg.zoomed = qfalse;
					cg.zoomTime = MIN( cg.time,
					                   cg.time + cg.time - cg.zoomTime - ZOOM_TIME );
				}
			}
			else
			{
				f = ( cg.time - cg.zoomTime ) / ( float ) ZOOM_TIME;

				if ( f < 1.0f )
				{
					fov_y = zoomFov + f * ( fov_y - zoomFov );
				}

				// WBUTTON_ATTACK2 is held so zoom next time
				if ( usercmdButtonPressed( cmd.buttons, BUTTON_ATTACK2 ) && cg.snap->ps.weaponstate != WEAPON_RELOADING )
				{
					cg.zoomed = qtrue;
					cg.zoomTime = MIN( cg.time,
					                   cg.time + cg.time - cg.zoomTime - ZOOM_TIME );
				}
			}
		}
		else if ( cg.zoomed )
		{
			cg.zoomed = qfalse;
		}
	}

	y = cg.refdef.height / tan( 0.5f * DEG2RAD( fov_y ) );
	fov_x = atan2( cg.refdef.width, y );
	fov_x = 2.0f * RAD2DEG( fov_x );

	// warp if underwater
	contents = CG_PointContents( cg.refdef.vieworg, -1 );

	if ( contents & ( CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA ) )
	{
		phase = cg.time / 1000.0f * WAVE_FREQUENCY * M_PI * 2.0f;
		v = WAVE_AMPLITUDE * sin( phase );
		fov_x += v;
		fov_y -= v;
		inwater = qtrue;
	}
	else
	{
		inwater = qfalse;
	}

	// set it
	cg.refdef.fov_x = fov_x;
	cg.refdef.fov_y = fov_y;

	if ( !cg.zoomed )
	{
		cg.zoomSensitivity = 1.0f;
	}
	else
	{
		cg.zoomSensitivity = cg.refdef.fov_y / 75.0f;
	}

	return inwater;
}
/*
===============
CG_OffsetFirstPersonView

===============
*/
void CG_OffsetFirstPersonView( void )
{
	float         *origin;
	float         *angles;
	float         bob;
	float         ratio;
	float         delta;
	float         speed;
	float         f;
	vec3_t        predictedVelocity;
	int           timeDelta;
	float         bob2;
	vec3_t        normal, baseOrigin;
	playerState_t *ps = &cg.predictedPlayerState;

	BG_GetClientNormal( ps, normal );

	if ( cg.snap->ps.pm_type == PM_INTERMISSION )
	{
		return;
	}

	origin = cg.refdef.vieworg;
	angles = cg.refdefViewAngles;

	VectorCopy( origin, baseOrigin );

	// if dead, fix the angle and don't add any kick
	if ( cg.snap->ps.stats[ STAT_HEALTH ] <= 0 )
	{
		angles[ ROLL ] = 40;
		angles[ PITCH ] = -15;
		angles[ YAW ] = cg.snap->ps.stats[ STAT_VIEWLOCK ];
		origin[ 2 ] += cg.predictedPlayerState.viewheight;
		return;
	}

	// add angles based on damage kick
	if ( cg.damageTime )
	{
		ratio = cg.time - cg.damageTime;

		if ( ratio < DAMAGE_DEFLECT_TIME )
		{
			ratio /= DAMAGE_DEFLECT_TIME;
			angles[ PITCH ] += ratio * cg.v_dmg_pitch;
			angles[ ROLL ] += ratio * cg.v_dmg_roll;
		}
		else
		{
			ratio = 1.0 - ( ratio - DAMAGE_DEFLECT_TIME ) / DAMAGE_RETURN_TIME;

			if ( ratio > 0 )
			{
				angles[ PITCH ] += ratio * cg.v_dmg_pitch;
				angles[ ROLL ] += ratio * cg.v_dmg_roll;
			}
		}
	}

	// add pitch based on fall kick
#if 0
	ratio = ( cg.time - cg.landTime ) / FALL_TIME;

	if ( ratio < 0 )
	{
		ratio = 0;
	}

	angles[ PITCH ] += ratio * cg.fall_value;
#endif

	// add angles based on velocity
	VectorCopy( cg.predictedPlayerState.velocity, predictedVelocity );

	delta = DotProduct( predictedVelocity, cg.refdef.viewaxis[ 0 ] );
	angles[ PITCH ] += delta * cg_runpitch.value;

	delta = DotProduct( predictedVelocity, cg.refdef.viewaxis[ 1 ] );
	angles[ ROLL ] -= delta * cg_runroll.value;

	// add angles based on bob
	// bob amount is class-dependent

	if ( cg.snap->ps.persistant[ PERS_SPECSTATE ] != SPECTATOR_NOT )
	{
		bob2 = 0.0f;
	}
	else
	{
		bob2 = BG_Class( cg.predictedPlayerState.stats[ STAT_CLASS ] )->bob;
	}

#define LEVEL4_FEEDBACK 10.0f

	//give a charging player some feedback
	if ( ps->weapon == WP_ALEVEL4 )
	{
		if ( ps->stats[ STAT_MISC ] > 0 )
		{
			float fraction = ( float ) ps->stats[ STAT_MISC ] /
			                 LEVEL4_TRAMPLE_CHARGE_MAX;

			if ( fraction > 1.0f )
			{
				fraction = 1.0f;
			}

			bob2 *= ( 1.0f + fraction * LEVEL4_FEEDBACK );
		}
	}

	if ( bob2 != 0.0f )
	{
		// make sure the bob is visible even at low speeds
		speed = cg.xyspeed > 200 ? cg.xyspeed : 200;

		delta = cg.bobfracsin * ( bob2 ) * speed;

		if ( cg.predictedPlayerState.pm_flags & PMF_DUCKED )
		{
			delta *= 3; // crouching
		}

		angles[ PITCH ] += delta;
		delta = cg.bobfracsin * ( bob2 ) * speed;

		if ( cg.predictedPlayerState.pm_flags & PMF_DUCKED )
		{
			delta *= 3; // crouching accentuates roll
		}

		if ( cg.bobcycle & 1 )
		{
			delta = -delta;
		}

		angles[ ROLL ] += delta;
	}

#define LEVEL3_FEEDBACK 20.0f

	//provide some feedback for pouncing
	if ( ( cg.predictedPlayerState.weapon == WP_ALEVEL3 ||
	       cg.predictedPlayerState.weapon == WP_ALEVEL3_UPG ) &&
	     cg.predictedPlayerState.stats[ STAT_MISC ] > 0 )
	{
		float  fraction1, fraction2;
		vec3_t forward;

		AngleVectors( angles, forward, NULL, NULL );
		VectorNormalize( forward );

		fraction1 = ( float ) cg.predictedPlayerState.stats[ STAT_MISC ] /
		            LEVEL3_POUNCE_TIME_UPG;

		if ( fraction1 > 1.0f )
		{
			fraction1 = 1.0f;
		}

		fraction2 = -sin( fraction1 * M_PI / 2 );

		VectorMA( origin, LEVEL3_FEEDBACK * fraction2, forward, origin );
	}

#define STRUGGLE_DIST 5.0f
#define STRUGGLE_TIME 250

	//allow the player to struggle a little whilst grabbed
	if ( cg.predictedPlayerState.pm_type == PM_GRABBED )
	{
		vec3_t    forward, right, up;
		usercmd_t cmd;
		int       cmdNum;
		float     fFraction, rFraction, uFraction;

		cmdNum = trap_GetCurrentCmdNumber();
		trap_GetUserCmd( cmdNum, &cmd );

		AngleVectors( angles, forward, right, up );

		fFraction = ( float )( cg.time - cg.forwardMoveTime ) / STRUGGLE_TIME;
		rFraction = ( float )( cg.time - cg.rightMoveTime ) / STRUGGLE_TIME;
		uFraction = ( float )( cg.time - cg.upMoveTime ) / STRUGGLE_TIME;

		if ( fFraction > 1.0f )
		{
			fFraction = 1.0f;
		}

		if ( rFraction > 1.0f )
		{
			rFraction = 1.0f;
		}

		if ( uFraction > 1.0f )
		{
			uFraction = 1.0f;
		}

		if ( cmd.forwardmove > 0 )
		{
			VectorMA( origin, STRUGGLE_DIST * fFraction, forward, origin );
		}
		else if ( cmd.forwardmove < 0 )
		{
			VectorMA( origin, -STRUGGLE_DIST * fFraction, forward, origin );
		}
		else
		{
			cg.forwardMoveTime = cg.time;
		}

		if ( cmd.rightmove > 0 )
		{
			VectorMA( origin, STRUGGLE_DIST * rFraction, right, origin );
		}
		else if ( cmd.rightmove < 0 )
		{
			VectorMA( origin, -STRUGGLE_DIST * rFraction, right, origin );
		}
		else
		{
			cg.rightMoveTime = cg.time;
		}

		if ( cmd.upmove > 0 )
		{
			VectorMA( origin, STRUGGLE_DIST * uFraction, up, origin );
		}
		else if ( cmd.upmove < 0 )
		{
			VectorMA( origin, -STRUGGLE_DIST * uFraction, up, origin );
		}
		else
		{
			cg.upMoveTime = cg.time;
		}
	}

	// this *feels* more realisitic for humans <- this comment feels very descriptive
	if ( cg.predictedPlayerState.persistant[ PERS_TEAM ] == TEAM_HUMANS &&
	     cg.predictedPlayerState.pm_type == PM_NORMAL )
	{
		angles[ PITCH ] += cg.bobfracsin * bob2 * 0.5;
	}

	// add view height
	VectorMA( origin, ps->viewheight, normal, origin );

	// smooth out duck height changes
	timeDelta = cg.time - cg.duckTime;

	if ( timeDelta < DUCK_TIME )
	{
		cg.refdef.vieworg[ 2 ] -= cg.duckChange
		                          * ( DUCK_TIME - timeDelta ) / DUCK_TIME;
	}

	// add bob height
	bob = cg.bobfracsin * cg.xyspeed * bob2;

	if ( bob > 6 )
	{
		bob = 6;
	}

	VectorMA( origin, bob, normal, origin );

	// add fall height
	delta = cg.time - cg.landTime;

	if ( delta < LAND_DEFLECT_TIME )
	{
		f = delta / LAND_DEFLECT_TIME;
		cg.refdef.vieworg[ 2 ] += cg.landChange * f;
	}
	else if ( delta < LAND_DEFLECT_TIME + LAND_RETURN_TIME )
	{
		delta -= LAND_DEFLECT_TIME;
		f = 1.0 - ( delta / LAND_RETURN_TIME );
		cg.refdef.vieworg[ 2 ] += cg.landChange * f;
	}

	// add step offset
	CG_StepOffset();
}
/*
=================
CG_PredictPlayerState

Generates cg.cur_lc->predictedPlayerState for the current cg.time
cg.cur_lc->predictedPlayerState is guaranteed to be valid after exiting.

For demo playback, this will be an interpolation between two valid
playerState_t.

For normal gameplay, it will be the result of predicted usercmd_t on
top of the most recent playerState_t received from the server.

Each new snapshot will usually have one or more new usercmd over the last,
but we simulate all unacknowledged commands each time, not just the new ones.
This means that on an internet connection, quite a few pmoves may be issued
each frame.

OPTIMIZE: don't re-simulate unless the newly arrived snapshot playerState_t
differs from the predicted one.  Would require saving all intermediate
playerState_t during prediction.

We detect prediction errors and allow them to be decayed off over several frames
to ease the jerk.
=================
*/
void CG_PredictPlayerState( void ) {
	int			cmdNum, current;
	playerState_t	oldPlayerState;
	qboolean	moved;
	usercmd_t	oldestCmd;
	usercmd_t	latestCmd;

	cg.cur_lc->hyperspace = qfalse;	// will be set if touching a trigger_teleport

	// if this is the first frame we must guarantee
	// predictedPlayerState is valid even if there is some
	// other error condition
	if ( !cg.cur_lc->validPPS ) {
		cg.cur_lc->validPPS = qtrue;
		cg.cur_lc->predictedPlayerState = *cg.cur_ps;
	}


	// demo playback just copies the moves
	if ( cg.demoPlayback || (cg.cur_ps->pm_flags & PMF_FOLLOW) ) {
		CG_InterpolatePlayerState( qfalse );
		return;
	}

	// non-predicting local movement will grab the latest angles
	if ( cg_nopredict.integer || cg_synchronousClients.integer ) {
		CG_InterpolatePlayerState( qtrue );
		return;
	}

	// prepare for pmove
	cg_pmove.ps = &cg.cur_lc->predictedPlayerState;
	if (cg.cur_lc->predictedPlayerState.collisionType == CT_CAPSULE) {
		cg_pmove.trace = CG_TraceCapsule;
	} else {
		cg_pmove.trace = CG_Trace;
	}
	cg_pmove.pointcontents = CG_PointContents;
	if ( cg_pmove.ps->pm_type == PM_DEAD ) {
		cg_pmove.tracemask = MASK_PLAYERSOLID & ~CONTENTS_BODY;
	}
	else {
		cg_pmove.tracemask = MASK_PLAYERSOLID;
	}
	if ( cg.cur_ps->persistant[PERS_TEAM] == TEAM_SPECTATOR ) {
		cg_pmove.tracemask &= ~CONTENTS_BODY;	// spectators can fly through bodies
	}
	cg_pmove.noFootsteps = ( cgs.dmflags & DF_NO_FOOTSTEPS ) > 0;

	// save the state before the pmove so we can detect transitions
	oldPlayerState = cg.cur_lc->predictedPlayerState;

	current = trap_GetCurrentCmdNumber();

	// if we don't have the commands right after the snapshot, we
	// can't accurately predict a current position, so just freeze at
	// the last good position we had
	cmdNum = current - CMD_BACKUP + 1;
	trap_GetUserCmd( cmdNum, &oldestCmd, cg.cur_localPlayerNum );
	if ( oldestCmd.serverTime > cg.cur_ps->commandTime 
		&& oldestCmd.serverTime < cg.time ) {	// special check for map_restart
		if ( cg_showmiss.integer ) {
			CG_Printf ("exceeded PACKET_BACKUP on commands\n");
		}
		return;
	}

	// get the latest command so we can know which commands are from previous map_restarts
	trap_GetUserCmd( current, &latestCmd, cg.cur_localPlayerNum );

	// get the most recent information we have, even if
	// the server time is beyond our current cg.time,
	// because predicted player positions are going to 
	// be ahead of everything else anyway
	if ( cg.nextSnap && !cg.nextFrameTeleport && !cg.thisFrameTeleport
		&& cg.nextSnap->playerNums[cg.cur_localPlayerNum] != -1) {
		cg.cur_lc->predictedPlayerState = cg.nextSnap->pss[cg.cur_localPlayerNum];
		cg.physicsTime = cg.nextSnap->serverTime;
	} else {
		cg.cur_lc->predictedPlayerState = *cg.cur_ps;
		cg.physicsTime = cg.snap->serverTime;
	}

	if ( pmove_msec.integer < 8 ) {
		trap_Cvar_SetValue( "pmove_msec", 8 );
	}
	else if ( pmove_msec.integer > 33 ) {
		trap_Cvar_SetValue( "pmove_msec", 33 );
	}

	cg_pmove.pmove_fixed = pmove_fixed.integer;// | cg_pmove_fixed.integer;
	cg_pmove.pmove_msec = pmove_msec.integer;

	cg_pmove.pmove_overbounce = pmove_overbounce.integer;

	// run cmds
	moved = qfalse;
	for ( cmdNum = current - CMD_BACKUP + 1 ; cmdNum <= current ; cmdNum++ ) {
		// get the command
		trap_GetUserCmd( cmdNum, &cg_pmove.cmd, cg.cur_localPlayerNum );

		if ( cg_pmove.pmove_fixed ) {
			PM_UpdateViewAngles( cg_pmove.ps, &cg_pmove.cmd );
		}

		// don't do anything if the time is before the snapshot player time
		if ( cg_pmove.cmd.serverTime <= cg.cur_lc->predictedPlayerState.commandTime ) {
			continue;
		}

		// don't do anything if the command was from a previous map_restart
		if ( cg_pmove.cmd.serverTime > latestCmd.serverTime ) {
			continue;
		}

		// check for a prediction error from last frame
		// on a lan, this will often be the exact value
		// from the snapshot, but on a wan we will have
		// to predict several commands to get to the point
		// we want to compare
		if ( cg.cur_lc->predictedPlayerState.commandTime == oldPlayerState.commandTime ) {
			vec3_t	delta;
			float	len;

			if ( cg.thisFrameTeleport ) {
				// a teleport will not cause an error decay
				VectorClear( cg.cur_lc->predictedError );
				if ( cg_showmiss.integer ) {
					CG_Printf( "PredictionTeleport\n" );
				}
				cg.thisFrameTeleport = qfalse;
			} else {
				vec3_t adjusted, new_angles;
				CG_AdjustPositionForMover( cg.cur_lc->predictedPlayerState.origin, 
				cg.cur_lc->predictedPlayerState.groundEntityNum, cg.physicsTime, cg.oldTime, adjusted, cg.cur_lc->predictedPlayerState.viewangles, new_angles);

				if ( cg_showmiss.integer ) {
					if (!VectorCompare( oldPlayerState.origin, adjusted )) {
						CG_Printf("prediction error\n");
					}
				}
				VectorSubtract( oldPlayerState.origin, adjusted, delta );
				len = VectorLength( delta );
				if ( len > 0.1 ) {
					if ( cg_showmiss.integer ) {
						CG_Printf("Prediction miss: %f\n", len);
					}
					if ( cg_errorDecay.integer ) {
						int		t;
						float	f;

						t = cg.time - cg.cur_lc->predictedErrorTime;
						f = ( cg_errorDecay.value - t ) / cg_errorDecay.value;
						if ( f < 0 ) {
							f = 0;
						}
						if ( f > 0 && cg_showmiss.integer ) {
							CG_Printf("Double prediction decay: %f\n", f);
						}
						VectorScale( cg.cur_lc->predictedError, f, cg.cur_lc->predictedError );
					} else {
						VectorClear( cg.cur_lc->predictedError );
					}
					VectorAdd( delta, cg.cur_lc->predictedError, cg.cur_lc->predictedError );
					cg.cur_lc->predictedErrorTime = cg.oldTime;
				}
			}
		}

		// don't predict gauntlet firing, which is only supposed to happen
		// when it actually inflicts damage
		cg_pmove.gauntletHit = qfalse;

		if ( cg_pmove.pmove_fixed ) {
			cg_pmove.cmd.serverTime = ((cg_pmove.cmd.serverTime + pmove_msec.integer-1) / pmove_msec.integer) * pmove_msec.integer;
		}

		Pmove (&cg_pmove);

		moved = qtrue;

		// add push trigger movement effects
		CG_TouchTriggerPrediction();

		// check for predictable events that changed from previous predictions
		//CG_CheckChangedPredictableEvents(&cg.cur_lc->predictedPlayerState);
	}

	if ( cg_showmiss.integer > 1 ) {
		CG_Printf( "[%i : %i] ", cg_pmove.cmd.serverTime, cg.time );
	}

	if ( !moved ) {
		if ( cg_showmiss.integer ) {
			CG_Printf( "not moved\n" );
		}
		return;
	}

	// adjust for the movement of the groundentity
	CG_AdjustPositionForMover( cg.cur_lc->predictedPlayerState.origin, 
		cg.cur_lc->predictedPlayerState.groundEntityNum, 
		cg.physicsTime, cg.time, cg.cur_lc->predictedPlayerState.origin, cg.cur_lc->predictedPlayerState.viewangles,cg.cur_lc->predictedPlayerState.viewangles);

	if ( cg_showmiss.integer ) {
		if (cg.cur_lc->predictedPlayerState.eventSequence > oldPlayerState.eventSequence + MAX_PS_EVENTS) {
			CG_Printf("WARNING: dropped event\n");
		}
	}

	// fire events and other transition triggered things
	CG_TransitionPlayerState( &cg.cur_lc->predictedPlayerState, &oldPlayerState );

	if ( cg_showmiss.integer ) {
		if (cg.cur_lc->eventSequence > cg.cur_lc->predictedPlayerState.eventSequence) {
			CG_Printf("WARNING: double event\n");
			cg.cur_lc->eventSequence = cg.cur_lc->predictedPlayerState.eventSequence;
		}
	}
}
Exemple #11
0
/*
===============
CG_OffsetFirstPersonView

===============
*/
static void CG_OffsetFirstPersonView( void )
{
  float         *origin;
  float         *angles;
  float         bob;
  float         ratio;
  float         delta;
  float         speed;
  float         f;
  vec3_t        predictedVelocity;
  char temp[1024];
    vec3_t org;
    vec3_t axis[3];
  vec3_t	ang;
  int           timeDelta;
  float         bob2;
  vec3_t        normal, baseOrigin;

  playerState_t *ps = &cg.predictedPlayerState;

  if( ps->stats[ STAT_STATE ] & SS_WALLCLIMBING )
  {
    if( ps->stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING )
      VectorSet( normal, 0.0f, 0.0f, -1.0f );
    else
      VectorCopy( ps->grapplePoint, normal );
  }
  else
    VectorSet( normal, 0.0f, 0.0f, 1.0f );


  if( cg.snap->ps.pm_type == PM_INTERMISSION )
    return;

  origin = cg.refdef.vieworg;
  angles = cg.refdefViewAngles;

  VectorCopy( origin, baseOrigin );

 
  // if dead, fix the angle and don't add any kick
  if( cg.snap->ps.stats[ STAT_HEALTH ] <= 0 )
  {
    angles[ ROLL ] = 40;
    angles[ PITCH ] = -15;
    angles[ YAW ] = cg.snap->ps.stats[ STAT_VIEWLOCK ];
    origin[ 2 ] += cg.predictedPlayerState.viewheight;
    return;
  }

    // add angles based on weapon kick
    VectorAdd( angles, cg.kick_angles, angles );
  

  // add angles based on damage kick
  if( cg.damageTime )
  {
    ratio = cg.time - cg.damageTime;
    if( ratio < DAMAGE_DEFLECT_TIME )
    {
      ratio /= DAMAGE_DEFLECT_TIME;
      angles[ PITCH ] += ratio * cg.v_dmg_pitch;
      angles[ ROLL ] += ratio * cg.v_dmg_roll;
    }
    else
    {
      ratio = 1.0 - ( ratio - DAMAGE_DEFLECT_TIME ) / DAMAGE_RETURN_TIME;
      if( ratio > 0 )
      {
        angles[ PITCH ] += ratio * cg.v_dmg_pitch;
        angles[ ROLL ] += ratio * cg.v_dmg_roll;
      }
    }
  }

  // add pitch based on fall kick
#if 0
  ratio = ( cg.time - cg.landTime) / FALL_TIME;
  if (ratio < 0)
    ratio = 0;
  angles[PITCH] += ratio * cg.fall_value;
#endif

  // add angles based on velocity
  VectorCopy( cg.predictedPlayerState.velocity, predictedVelocity );

  delta = DotProduct( predictedVelocity, cg.refdef.viewaxis[ 0 ] );
  angles[ PITCH ] += delta * cg_runpitch.value;

  delta = DotProduct( predictedVelocity, cg.refdef.viewaxis[ 1 ] );
  angles[ ROLL ] -= delta * cg_runroll.value;

  // add angles based on bob
  //TA: bob amount is class dependant
  if( cg.snap->ps.persistant[ PERS_TEAM ] == TEAM_SPECTATOR )
    bob2 = 0.0f;
  else
    bob2 = BG_FindBobForClass( cg.predictedPlayerState.stats[ STAT_PCLASS ] );

#define LEVEL4_FEEDBACK  10.0f

  //give a charging player some feedback
  if( ps->weapon == WP_ALEVEL4 )
  {
    if( ps->stats[ STAT_MISC ] > 0 )
    {
      float fraction = (float)ps->stats[ STAT_MISC ] / (float)LEVEL4_CHARGE_TIME;

      if( fraction > 1.0f )
        fraction = 1.0f;

      bob2 *= ( 1.0f + fraction * LEVEL4_FEEDBACK );
    }
  }

  if( bob2 != 0.0f )
  {
    // make sure the bob is visible even at low speeds
    speed = cg.xyspeed > 200 ? cg.xyspeed : 200;

    delta = cg.bobfracsin * ( bob2 ) * speed;
    if( cg.predictedPlayerState.pm_flags & PMF_DUCKED )
      delta *= 3;   // crouching

    angles[ PITCH ] += delta;
    delta = cg.bobfracsin * ( bob2 ) * speed;
    if( cg.predictedPlayerState.pm_flags & PMF_DUCKED )
      delta *= 3;   // crouching accentuates roll

    if( cg.bobcycle & 1 )
      delta = -delta;

    angles[ ROLL ] += delta;
  }

#define LEVEL3_FEEDBACK  20.0f

  //provide some feedback for pouncing
  if( cg.predictedPlayerState.weapon == WP_ALEVEL3 ||
      cg.predictedPlayerState.weapon == WP_ALEVEL3_UPG )
  {
    if( cg.predictedPlayerState.stats[ STAT_MISC ] > 0 )
    {
      float   fraction1, fraction2;
      vec3_t  forward;

      AngleVectors( angles, forward, NULL, NULL );
      VectorNormalize( forward );

      fraction1 = (float)( cg.time - cg.weapon2Time ) / (float)LEVEL3_POUNCE_CHARGE_TIME;

      if( fraction1 > 1.0f )
        fraction1 = 1.0f;

      fraction2 = -sin( fraction1 * M_PI / 2 );

      VectorMA( origin, LEVEL3_FEEDBACK * fraction2, forward, origin );
    }
  }

#define STRUGGLE_DIST 5.0f
#define STRUGGLE_TIME 250

  //allow the player to struggle a little whilst grabbed
  if( cg.predictedPlayerState.pm_type == PM_GRABBED )
  {
    vec3_t    forward, right, up;
    usercmd_t cmd;
    int       cmdNum;
    float     fFraction, rFraction, uFraction;
    float     fFraction2, rFraction2, uFraction2;

    cmdNum = trap_GetCurrentCmdNumber();
    trap_GetUserCmd( cmdNum, &cmd );

    AngleVectors( angles, forward, right, up );

    fFraction = (float)( cg.time - cg.forwardMoveTime ) / STRUGGLE_TIME;
    rFraction = (float)( cg.time - cg.rightMoveTime ) / STRUGGLE_TIME;
    uFraction = (float)( cg.time - cg.upMoveTime ) / STRUGGLE_TIME;

    if( fFraction > 1.0f )
      fFraction = 1.0f;
    if( rFraction > 1.0f )
      rFraction = 1.0f;
    if( uFraction > 1.0f )
      uFraction = 1.0f;

    fFraction2 = -sin( fFraction * M_PI / 2 );
    rFraction2 = -sin( rFraction * M_PI / 2 );
    uFraction2 = -sin( uFraction * M_PI / 2 );

    if( cmd.forwardmove > 0 )
      VectorMA( origin, STRUGGLE_DIST * fFraction, forward, origin );
    else if( cmd.forwardmove < 0 )
      VectorMA( origin, -STRUGGLE_DIST * fFraction, forward, origin );
    else
      cg.forwardMoveTime = cg.time;

    if( cmd.rightmove > 0 )
      VectorMA( origin, STRUGGLE_DIST * rFraction, right, origin );
    else if( cmd.rightmove < 0 )
      VectorMA( origin, -STRUGGLE_DIST * rFraction, right, origin );
    else
      cg.rightMoveTime = cg.time;

    if( cmd.upmove > 0 )
      VectorMA( origin, STRUGGLE_DIST * uFraction, up, origin );
    else if( cmd.upmove < 0 )
      VectorMA( origin, -STRUGGLE_DIST * uFraction, up, origin );
    else
      cg.upMoveTime = cg.time;
  }

  if( cg.predictedPlayerState.stats[ STAT_STATE ] & SS_POISONCLOUDED &&
      !( cg.snap->ps.pm_flags & PMF_FOLLOW ) )
  {
    float fraction = sin( ( (float)cg.time / 1000.0f ) * M_PI * 2 * PCLOUD_ROLL_FREQUENCY );
    float pitchFraction = sin( ( (float)cg.time / 1000.0f ) * M_PI * 5 * PCLOUD_ROLL_FREQUENCY );

    fraction *= 1.0f - ( ( cg.time - cg.poisonedTime ) / (float)LEVEL1_PCLOUD_TIME );
    pitchFraction *= 1.0f - ( ( cg.time - cg.poisonedTime ) / (float)LEVEL1_PCLOUD_TIME );

    angles[ ROLL ] += fraction * PCLOUD_ROLL_AMPLITUDE;
    angles[ YAW ] += fraction * PCLOUD_ROLL_AMPLITUDE;
    angles[ PITCH ] += pitchFraction * PCLOUD_ROLL_AMPLITUDE / 2.0f;
  }

  //TA: this *feels* more realisitic for humans
  if( cg.predictedPlayerState.stats[ STAT_PTEAM ] == PTE_HUMANS )
  {
    angles[PITCH] += cg.bobfracsin * bob2 * 0.5;

    //TA: heavy breathing effects //FIXME: sound
    if( cg.predictedPlayerState.stats[ STAT_STAMINA ] < 0 )
    {
      float deltaBreath = (float)(
        cg.predictedPlayerState.stats[ STAT_STAMINA ] < 0 ?
        -cg.predictedPlayerState.stats[ STAT_STAMINA ] :
        cg.predictedPlayerState.stats[ STAT_STAMINA ] ) / 200.0;
      float deltaAngle = cos( (float)cg.time/150.0 ) * deltaBreath;

      deltaAngle += ( deltaAngle < 0 ? -deltaAngle : deltaAngle ) * 0.5;

      angles[ PITCH ] -= deltaAngle;
    }
  }

//===================================

  // add view height
  //TA: when wall climbing the viewheight is not straight up
  if( cg.predictedPlayerState.stats[ STAT_STATE ] & SS_WALLCLIMBING )
    VectorMA( origin, ps->viewheight, normal, origin );
  else
    origin[ 2 ] += cg.predictedPlayerState.viewheight;

  // smooth out duck height changes
  timeDelta = cg.time - cg.duckTime;
  if( timeDelta < DUCK_TIME)
  {
    cg.refdef.vieworg[ 2 ] -= cg.duckChange
      * ( DUCK_TIME - timeDelta ) / DUCK_TIME;
  }

  // add bob height
  bob = cg.bobfracsin * cg.xyspeed * bob2;

  if( bob > 6 )
    bob = 6;

  //TA: likewise for bob
  if( cg.predictedPlayerState.stats[ STAT_STATE ] & SS_WALLCLIMBING )
    VectorMA( origin, bob, normal, origin );
  else
    origin[ 2 ] += bob;


  // add fall height
  delta = cg.time - cg.landTime;

  if( delta < LAND_DEFLECT_TIME )
  {
    f = delta / LAND_DEFLECT_TIME;
    cg.refdef.vieworg[ 2 ] += cg.landChange * f;
  }
  else if( delta < LAND_DEFLECT_TIME + LAND_RETURN_TIME )
  {
    delta -= LAND_DEFLECT_TIME;
    f = 1.0 - ( delta / LAND_RETURN_TIME );
    cg.refdef.vieworg[ 2 ] += cg.landChange * f;
  }

  // add step offset
  CG_StepOffset( );

  // add kick offset

  VectorAdd (origin, cg.kick_origin, origin);
}
Exemple #12
0
//@Jkent: Is there a way to get access to R_customheight/width and/or r_height/width? (Get the newer height-dependant FOV rather than width-dependant as 1.1 widescreens actually have smaller FOVs)
static int CG_CalcFov( void )
{
  float     x;
  float     phase;
  float     v;
  int       contents;
  float     fov_x, fov_y;
  float     zoomFov;
  float     f;
  int       inwater;
  int       attribFov;
  usercmd_t cmd;
  int       cmdNum;

  cmdNum = trap_GetCurrentCmdNumber( );
  trap_GetUserCmd( cmdNum, &cmd );

  if( cg.predictedPlayerState.pm_type == PM_INTERMISSION ||
      ( cg.snap->ps.persistant[ PERS_TEAM ] == TEAM_SPECTATOR ) )
  {
    // if in intermission, use a fixed value
    fov_x = 90;
  }
  else
  {
    // don't lock the fov globally - we need to be able to change it
    attribFov = BG_FindFovForClass( cg.predictedPlayerState.stats[ STAT_PCLASS ] );
    fov_x = attribFov;

    if ( fov_x < 1 )
      fov_x = 1;
    else if ( fov_x > 160 )
      fov_x = 160;

    if( cg.spawnTime > ( cg.time - FOVWARPTIME ) &&
        BG_ClassHasAbility( cg.predictedPlayerState.stats[ STAT_PCLASS ], SCA_FOVWARPS ) )
    {
      float temp, temp2;

      temp = (float)( cg.time - cg.spawnTime ) / FOVWARPTIME;
      temp2 = ( 170 - fov_x ) * temp;

      //Com_Printf( "%f %f\n", temp*100, temp2*100 );

      fov_x = 170 - temp2;
    }

    // account for zooms
    zoomFov = BG_FindZoomFovForWeapon( cg.predictedPlayerState.weapon );
    if ( zoomFov < 1 )
      zoomFov = 1;
    else if ( zoomFov > attribFov )
      zoomFov = attribFov;

    // only do all the zoom stuff if the client CAN zoom
    // FIXME: zoom control is currently hard coded to BUTTON_ATTACK2
    if( BG_WeaponCanZoom( cg.predictedPlayerState.weapon ) )
    {
      if ( cg.zoomed )
      {
        f = ( cg.time - cg.zoomTime ) / (float)ZOOM_TIME;

        if ( f > 1.0 )
          fov_x = zoomFov;
        else
          fov_x = fov_x + f * ( zoomFov - fov_x );

        // BUTTON_ATTACK2 isn't held so unzoom next time
        if( !( cmd.buttons & BUTTON_ATTACK2 ) )
        {
          cg.zoomed   = qfalse;
          cg.zoomTime = cg.time;
        }
      }
      else
      {
        f = ( cg.time - cg.zoomTime ) / (float)ZOOM_TIME;

        if ( f > 1.0 )
          fov_x = fov_x;
        else
          fov_x = zoomFov + f * ( fov_x - zoomFov );

        // BUTTON_ATTACK2 is held so zoom next time
        if( cmd.buttons & BUTTON_ATTACK2 )
        {
          cg.zoomed   = qtrue;
          cg.zoomTime = cg.time;
        }
      }
    }
  }

  x = cg.refdef.width / tan( fov_x / 360 * M_PI );
  fov_y = atan2( cg.refdef.height, x );
  fov_y = fov_y * 360 / M_PI;

  // warp if underwater
  contents = CG_PointContents( cg.refdef.vieworg, -1 );

  if( contents & ( CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA ) )
  {
    phase = cg.time / 1000.0 * WAVE_FREQUENCY * M_PI * 2;
    v = WAVE_AMPLITUDE * sin( phase );
    fov_x += v;
    fov_y -= v;
    inwater = qtrue;
  }
  else
    inwater = qfalse;

  if( cg.predictedPlayerState.stats[ STAT_STATE ] & SS_POISONCLOUDED &&
      cg.predictedPlayerState.stats[ STAT_HEALTH ] > 0 &&
      !( cg.snap->ps.pm_flags & PMF_FOLLOW ) )
  {
    phase = cg.time / 1000.0 * PCLOUD_ZOOM_FREQUENCY * M_PI * 2;
    v = PCLOUD_ZOOM_AMPLITUDE * sin( phase );
    v *= 1.0f - ( ( cg.time - cg.poisonedTime ) / (float)LEVEL1_PCLOUD_TIME );
    fov_x += v;
    fov_y += v;
  }


  // set it
  cg.refdef.fov_x = fov_x;
  cg.refdef.fov_y = fov_y;

  if( !cg.zoomed )
    cg.zoomSensitivity = (7 + (90/cg.refdef.fov_y))/8;//1;
  else
    cg.zoomSensitivity = cg.refdef.fov_y / 75.0;

  return inwater;
}