/* ======================== 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] ); } }
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; } }
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; }
/* =============== 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( ); }
/* =============== 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( ); }
/* =============== 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 ); } }
// 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; } } }
/* =============== 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); }
//@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; }