// Copyright (C) 2001-2002 Raven Software // // bg_pmove.c -- both games player movement code // takes a playerstate and a usercmd as input and returns a modifed playerstate #include "q_shared.h" #include "bg_public.h" #include "bg_local.h" pmove_t *pm; pml_t pml; // Speed scales float pm_stopspeed = 100.0f; float pm_duckScale = 0.25f; float pm_swimScale = 0.50f; float pm_wadeScale = 0.70f; const float pm_ladderScale = 0.5f; // Accelerations float pm_accelerate = 6.0f; float pm_airaccelerate = 1.0f; float pm_wateraccelerate = 4.0f; float pm_flyaccelerate = 8.0f; // Frictions float pm_headfriction = 0.0f; // Friction when on someones head float pm_friction = 6.0f; // Friction when on the ground float pm_waterfriction = 3.0f; // Friction when in water float pm_ladderfriction = 6.0f; // Friction when on a ladder float pm_spectatorfriction = 5.0f; // Friction when flying aroudn as a spectator int c_pmove = 0; ladder_t pm_ladders[MAX_LADDERS]; int pm_laddercount = 0; static void PM_Weapon_AddInaccuracy ( attackType_t attack ); static void PM_Weapon_AddKickAngles ( vec3_t kickAngles ); static void PM_BeginZoomOut ( void ); /* =============== PM_AddEvent =============== */ void PM_AddEvent( int newEvent ) { BG_AddPredictableEventToPlayerstate( newEvent, 0, pm->ps ); } /* =============== PM_AddEventWithParm =============== */ void PM_AddEventWithParm( int newEvent, int parm ) { BG_AddPredictableEventToPlayerstate( newEvent, parm, pm->ps ); } /* =============== PM_AddTouchEnt =============== */ void PM_AddTouchEnt ( int entityNum ) { int i; // Cant add the world as a touch entity if ( entityNum == ENTITYNUM_WORLD ) { return; } // Ensure the max touch limit has not been reached if ( pm->numtouch == MAXTOUCH ) { return; } // see if it is already added for ( i = 0 ; i < pm->numtouch ; i++ ) { if ( pm->touchents[ i ] == entityNum ) { return; } } // add it pm->touchents[pm->numtouch] = entityNum; pm->numtouch++; } /* ================== PM_ClipVelocity Slide off of the impacting surface ================== */ void PM_ClipVelocity( vec3_t in, vec3_t normal, vec3_t out, float overbounce ) { float backoff; float change; int i; backoff = DotProduct (in, normal); if ( backoff < 0 ) { backoff *= overbounce; } else { backoff /= overbounce; } for ( i=0 ; i<3 ; i++ ) { change = normal[i]*backoff; out[i] = in[i] - change; } } /* ================== PM_Friction Handles both ground friction and water friction ================== */ static void PM_Friction( void ) { vec3_t vec; float *vel; float speed, newspeed, control; float drop; vel = pm->ps->velocity; VectorCopy( vel, vec ); if ( pml.walking ) { vec[2] = 0; // ignore slope movement } speed = VectorLength(vec); if (speed < 1) { vel[0] = 0; vel[1] = 0; // allow sinking underwater // FIXME: still have z friction underwater? return; } drop = 0; // apply ground friction if ( pm->waterlevel <= 1 ) { if ( pml.walking && !(pml.groundTrace.surfaceFlags & SURF_SLICK) ) { // if getting knocked back, no friction if ( ! (pm->ps->pm_flags & PMF_TIME_KNOCKBACK) ) { control = speed < pm_stopspeed ? pm_stopspeed : speed; drop += control*pm_friction*pml.frametime; } } } // apply water friction even if just wading if ( pm->ps->pm_flags & PMF_LADDER ) { if ( !pml.groundPlane ) { control = speed < pm_stopspeed ? pm_stopspeed : speed; drop += control*pm_ladderfriction*pml.frametime; } } else if ( pm->waterlevel > 1 ) { drop += speed*pm_waterfriction*pm->waterlevel*pml.frametime; } // If on someones head then use special friction else if ( pm->ps->groundEntityNum < MAX_CLIENTS ) { drop = speed*pm_headfriction*pml.frametime; } if ( pm->ps->pm_type == PM_SPECTATOR) { drop += speed*pm_spectatorfriction*pml.frametime; } // scale the velocity newspeed = speed - drop; if (newspeed < 0) { newspeed = 0; } newspeed /= speed; vel[0] = vel[0] * newspeed; vel[1] = vel[1] * newspeed; vel[2] = vel[2] * newspeed; } /* ============== PM_Accelerate Handles user intended acceleration ============== */ static void PM_Accelerate( vec3_t wishdir, float wishspeed, float accel ) { #if 1 // q2 style int i; float addspeed, accelspeed, currentspeed; currentspeed = DotProduct (pm->ps->velocity, wishdir); addspeed = wishspeed - currentspeed; if (addspeed <= 0) { return; } accelspeed = accel*pml.frametime*wishspeed; if (accelspeed > addspeed) { accelspeed = addspeed; } for (i=0 ; i<3 ; i++) { pm->ps->velocity[i] += accelspeed*wishdir[i]; } #else // proper way (avoids strafe jump maxspeed bug), but feels bad vec3_t wishVelocity; vec3_t pushDir; float pushLen; float canPush; VectorScale( wishdir, wishspeed, wishVelocity ); VectorSubtract( wishVelocity, pm->ps->velocity, pushDir ); pushLen = VectorNormalize( pushDir ); canPush = accel*pml.frametime*wishspeed; if (canPush > pushLen) { canPush = pushLen; } VectorMA( pm->ps->velocity, canPush, pushDir, pm->ps->velocity ); #endif } /* ============ PM_CmdScale Returns the scale factor to apply to cmd movements This allows the clients to use axial -127 to 127 values for all directions without getting a sqrt(2) distortion in speed. ============ */ static float PM_CmdScale( usercmd_t *cmd ) { int max; float total; float scale; max = abs( cmd->forwardmove ); if ( abs( cmd->rightmove ) > max ) { max = abs( cmd->rightmove ); } if ( abs( cmd->upmove ) > max ) { max = abs( cmd->upmove ); } if ( !max ) { return 0; } total = sqrt( cmd->forwardmove * cmd->forwardmove + cmd->rightmove * cmd->rightmove + cmd->upmove * cmd->upmove ); scale = (float)pm->ps->speed * max / ( 127.0 * total ); return scale; } /* ================ PM_SetMovementDir Determine the rotation of the legs reletive to the facing dir ================ */ static void PM_SetMovementDir( void ) { if ( pm->cmd.forwardmove || pm->cmd.rightmove ) { if ( pm->cmd.rightmove == 0 && pm->cmd.forwardmove > 0 ) { pm->ps->movementDir = 0; } else if ( pm->cmd.rightmove < 0 && pm->cmd.forwardmove > 0 ) { pm->ps->movementDir = 1; } else if ( pm->cmd.rightmove < 0 && pm->cmd.forwardmove == 0 ) { pm->ps->movementDir = 2; } else if ( pm->cmd.rightmove < 0 && pm->cmd.forwardmove < 0 ) { pm->ps->movementDir = 3; } else if ( pm->cmd.rightmove == 0 && pm->cmd.forwardmove < 0 ) { pm->ps->movementDir = 4; } else if ( pm->cmd.rightmove > 0 && pm->cmd.forwardmove < 0 ) { pm->ps->movementDir = 5; } else if ( pm->cmd.rightmove > 0 && pm->cmd.forwardmove == 0 ) { pm->ps->movementDir = 6; } else if ( pm->cmd.rightmove > 0 && pm->cmd.forwardmove > 0 ) { pm->ps->movementDir = 7; } } else { // if they aren't actively going directly sideways, // change the animation to the diagonal so they // don't stop too crooked if ( pm->ps->movementDir == 2 ) { pm->ps->movementDir = 1; } else if ( pm->ps->movementDir == 6 ) { pm->ps->movementDir = 7; } } } /* ============= PM_CheckJump ============= */ static qboolean PM_CheckJump( void ) { if ( pm->ps->pm_time ) { return qfalse; } // Cant jump when ducked if ( pm->ps->pm_flags & PMF_DUCKED ) { return qfalse; } // don't allow jump until all buttons are up if ( pm->ps->pm_flags & PMF_RESPAWNED ) { return qfalse; } // not holding jump if ( pm->cmd.upmove < 10 ) { return qfalse; } // must wait for jump to be released if ( pm->ps->pm_debounce & PMD_JUMP ) { // clear upmove so cmdscale doesn't lower running speed pm->cmd.upmove = 0; return qfalse; } pml.groundPlane = qfalse; // jumping away pml.walking = qfalse; pm->ps->pm_debounce |= PMD_JUMP; pm->ps->pm_flags |= PMF_JUMPING; pm->ps->groundEntityNum = ENTITYNUM_NONE; if ( pm->cmd.forwardmove >= 0 ) { PM_ForceLegsAnim( pm->ps, LEGS_JUMP ); } else { PM_ForceLegsAnim( pm->ps, LEGS_JUMP_BACK ); pm->ps->pm_flags |= PMF_BACKWARDS_JUMP; } // Special case for ladders if ( pm->ps->ladder != -1 ) { vec3_t forward; VectorCopy ( pm_ladders[pm->ps->ladder].fwd, forward ); forward[2] = 0; VectorNormalize ( forward ); VectorMA ( pm->ps->velocity, -50, forward, pm->ps->velocity ); pm->ps->pm_flags |= PMF_LADDER_JUMP; return qtrue; } pm->ps->velocity[2] = JUMP_VELOCITY; return qtrue; } /* ============= PM_CheckWaterJump ============= */ static qboolean PM_CheckWaterJump( void ) { vec3_t spot; int cont; vec3_t flatforward; if (pm->ps->pm_time) { return qfalse; } // check for water jump if ( pm->waterlevel != 2 ) { return qfalse; } flatforward[0] = pml.forward[0]; flatforward[1] = pml.forward[1]; flatforward[2] = 0; VectorNormalize (flatforward); VectorMA (pm->ps->origin, 30, flatforward, spot); spot[2] += 4; cont = pm->pointcontents (spot, pm->ps->clientNum ); if ( !(cont & CONTENTS_SOLID) ) { return qfalse; } spot[2] += 16; cont = pm->pointcontents (spot, pm->ps->clientNum ); if ( cont ) { return qfalse; } // jump out of water VectorScale (pml.forward, 200, pm->ps->velocity); pm->ps->velocity[2] = 350; pm->ps->pm_flags |= PMF_TIME_WATERJUMP; pm->ps->pm_time = 2000; return qtrue; } /* =================== PM_WaterJumpMove Flying out of the water =================== */ static void PM_WaterJumpMove( void ) { // waterjump has no control, but falls PM_StepSlideMove( qtrue ); pm->ps->velocity[2] -= pm->ps->gravity * pml.frametime; if (pm->ps->velocity[2] < 0) { // cancel as soon as we are falling down again pm->ps->pm_flags &= ~PMF_ALL_TIMES; pm->ps->pm_time = 0; } } /* =================== PM_WaterMove =================== */ static void PM_WaterMove( void ) { int i; vec3_t wishvel; float wishspeed; vec3_t wishdir; float scale; float vel; if ( PM_CheckWaterJump() ) { PM_WaterJumpMove(); return; } #if 0 // jump = head for surface if ( pm->cmd.upmove >= 10 ) { if (pm->ps->velocity[2] > -300) { if ( pm->watertype == CONTENTS_WATER ) { pm->ps->velocity[2] = 100; } else { pm->ps->velocity[2] = 50; } } } #endif PM_Friction (); scale = PM_CmdScale( &pm->cmd ); // // user intentions // if ( !scale ) { wishvel[0] = 0; wishvel[1] = 0; wishvel[2] = -60; // sink towards bottom } else { for (i=0 ; i<3 ; i++) { wishvel[i] = scale * pml.forward[i]*pm->cmd.forwardmove + scale * pml.right[i]*pm->cmd.rightmove; } wishvel[2] += scale * pm->cmd.upmove; } VectorCopy (wishvel, wishdir); wishspeed = VectorNormalize(wishdir); if ( wishspeed > pm->ps->speed * pm_swimScale ) { wishspeed = pm->ps->speed * pm_swimScale; } PM_Accelerate (wishdir, wishspeed, pm_wateraccelerate); // make sure we can go up slopes easily under water if ( pml.groundPlane && DotProduct( pm->ps->velocity, pml.groundTrace.plane.normal ) < 0 ) { vel = VectorLength(pm->ps->velocity); // slide along the ground plane PM_ClipVelocity (pm->ps->velocity, pml.groundTrace.plane.normal, pm->ps->velocity, OVERCLIP ); VectorNormalize(pm->ps->velocity); VectorScale(pm->ps->velocity, vel, pm->ps->velocity); } PM_SlideMove( qfalse ); } /* =================== PM_FlyMove =================== */ static void PM_FlyMove( void ) { int i; vec3_t wishvel; float wishspeed; vec3_t wishdir; float scale; // normal slowdown PM_Friction (); scale = PM_CmdScale( &pm->cmd ); // // user intentions // if ( !scale ) { wishvel[0] = 0; wishvel[1] = 0; wishvel[2] = 0; } else { for (i=0 ; i<3 ; i++) { wishvel[i] = scale * pml.forward[i]*pm->cmd.forwardmove + scale * pml.right[i]*pm->cmd.rightmove; } wishvel[2] += scale * pm->cmd.upmove; } VectorCopy (wishvel, wishdir); wishspeed = VectorNormalize(wishdir); PM_Accelerate (wishdir, wishspeed, pm_flyaccelerate); PM_StepSlideMove( qfalse ); } /* =================== PM_AirMove =================== */ static void PM_AirMove( void ) { int i; vec3_t wishvel; float fmove, smove; vec3_t wishdir; float wishspeed; float scale; usercmd_t cmd; PM_Friction(); fmove = pm->cmd.forwardmove; smove = pm->cmd.rightmove; cmd = pm->cmd; scale = PM_CmdScale( &cmd ); // set the movementDir so clients can rotate the legs for strafing PM_SetMovementDir(); // project moves down to flat plane pml.forward[2] = 0; pml.right[2] = 0; VectorNormalize (pml.forward); VectorNormalize (pml.right); for ( i = 0 ; i < 2 ; i++ ) { wishvel[i] = pml.forward[i]*fmove + pml.right[i]*smove; } wishvel[2] = 0; VectorCopy (wishvel, wishdir); wishspeed = VectorNormalize(wishdir); wishspeed *= scale; // not on ground, so little effect on velocity PM_Accelerate (wishdir, wishspeed, pm_airaccelerate); // we may have a ground plane that is very steep, even // though we don't have a groundentity // slide along the steep plane if ( pml.groundPlane ) { PM_ClipVelocity (pm->ps->velocity, pml.groundTrace.plane.normal, pm->ps->velocity, OVERCLIP ); } PM_StepSlideMove ( qtrue ); } /* =================== PM_WalkMove =================== */ static void PM_WalkMove( void ) { int i; vec3_t wishvel; float fmove, smove; vec3_t wishdir; float wishspeed; float scale; usercmd_t cmd; float accelerate; float vel; if ( pm->waterlevel > 2 && DotProduct( pml.forward, pml.groundTrace.plane.normal ) > 0 ) { // begin swimming PM_WaterMove(); return; } if ( PM_CheckJump () ) { PM_BeginZoomOut ( ); // jumped away if ( pm->waterlevel > 1 ) { PM_WaterMove(); } else { PM_AirMove(); } return; } PM_Friction (); fmove = pm->cmd.forwardmove; smove = pm->cmd.rightmove; cmd = pm->cmd; scale = PM_CmdScale( &cmd ); // set the movementDir so clients can rotate the legs for strafing PM_SetMovementDir(); // project moves down to flat plane pml.forward[2] = 0; pml.right[2] = 0; // project the forward and right directions onto the ground plane PM_ClipVelocity (pml.forward, pml.groundTrace.plane.normal, pml.forward, OVERCLIP ); PM_ClipVelocity (pml.right, pml.groundTrace.plane.normal, pml.right, OVERCLIP ); // VectorNormalize (pml.forward); VectorNormalize (pml.right); for ( i = 0 ; i < 3 ; i++ ) { wishvel[i] = pml.forward[i]*fmove + pml.right[i]*smove; } // when going up or down slopes the wish velocity should Not be zero // wishvel[2] = 0; VectorCopy (wishvel, wishdir); wishspeed = VectorNormalize(wishdir); wishspeed *= scale; // clamp the speed lower if ducking if ( pm->ps->pm_flags & PMF_DUCKED ) { if ( wishspeed > pm->ps->speed * pm_duckScale ) { wishspeed = pm->ps->speed * pm_duckScale; } } // clamp the speed lower if wading or walking on the bottom if ( pm->waterlevel && !(pm->watertype & CONTENTS_LADDER) ) { float waterScale; waterScale = pm->waterlevel / 3.0; waterScale = 1.0 - ( 1.0 - pm_swimScale ) * waterScale; if ( wishspeed > pm->ps->speed * waterScale ) { wishspeed = pm->ps->speed * waterScale; } } // when a player gets hit, they temporarily lose // full control, which allows them to be moved a bit if ( ( pml.groundTrace.surfaceFlags & SURF_SLICK ) || pm->ps->pm_flags & PMF_TIME_KNOCKBACK ) { accelerate = pm_airaccelerate; } else { accelerate = pm_accelerate; // Accelerate faster when ducked if ( pm->ps->pm_flags & PMF_DUCKED ) { accelerate *= 2; } } PM_Accelerate (wishdir, wishspeed, accelerate); if ( ( pml.groundTrace.surfaceFlags & SURF_SLICK ) || pm->ps->pm_flags & PMF_TIME_KNOCKBACK ) { pm->ps->velocity[2] -= pm->ps->gravity * pml.frametime; } vel = VectorLength(pm->ps->velocity); // slide along the ground plane PM_ClipVelocity (pm->ps->velocity, pml.groundTrace.plane.normal, pm->ps->velocity, OVERCLIP ); // don't decrease velocity when going up or down a slope VectorNormalize(pm->ps->velocity); VectorScale(pm->ps->velocity, vel, pm->ps->velocity); // don't do anything if standing still if (!pm->ps->velocity[0] && !pm->ps->velocity[1]) { return; } PM_StepSlideMove( qfalse ); } /* =================== PM_LadderMove =================== */ static void PM_LadderMove ( void ) { float wishspeed; float scale; vec3_t wishdir; vec3_t wishvel; float accelerate; if ( PM_CheckJump () ) { return; } PM_Friction (); scale = PM_CmdScale( &pm->cmd ); accelerate = pm_accelerate; // // user intentions // if ( !scale ) { wishvel[0] = 0; wishvel[1] = 0; wishvel[2] = 0; } else { int i; VectorNormalize ( pm_ladders[pm->ps->ladder].fwd ); VectorNormalize ( pml.forward ); if ( !pml.groundPlane ) { vec3_t mins; vec3_t maxs; vec3_t offset = {1, 1, 1}; trace_t tr; VectorCopy ( pm->mins, mins ); VectorSubtract ( mins, offset, mins ); VectorCopy ( pm->maxs, maxs ); VectorAdd ( maxs, offset, maxs ); pm->trace ( &tr, pm->ps->origin, mins, maxs, pm->ps->origin, pm->ps->clientNum, pm->tracemask ); if ( tr.fraction == 1.0f || !tr.startsolid ) { if ( pm->cmd.forwardmove >= 0 ) { VectorAdd ( pml.forward, pm_ladders[pm->ps->ladder].fwd, pml.forward ); } else { VectorSubtract ( pml.forward, pm_ladders[pm->ps->ladder].fwd, pml.forward ); } } } for ( i=0 ; i<3 ; i++ ) { wishvel[i] = scale * pml.forward[i]*pm->cmd.forwardmove + scale * pml.right[i]*pm->cmd.rightmove * pm_ladderScale; } // Duck down ladders if ( pm->cmd.upmove < 0 ) { wishvel[2] += scale * pm->cmd.upmove; } } VectorCopy (wishvel, wishdir); wishspeed = VectorNormalize(wishdir); PM_Accelerate( wishdir, wishspeed, accelerate ); PM_StepSlideMove( qfalse ); } /* ============== PM_DeadMove ============== */ static void PM_DeadMove( void ) { float forward; if ( !pml.walking ) { return; } // extra friction forward = VectorLength (pm->ps->velocity); forward -= 20; if ( forward <= 0 ) { VectorClear (pm->ps->velocity); } else { VectorNormalize (pm->ps->velocity); VectorScale (pm->ps->velocity, forward, pm->ps->velocity); } } /* =============== PM_NoclipMove =============== */ static void PM_NoclipMove( void ) { float speed, drop, friction, control, newspeed; int i; vec3_t wishvel; float fmove, smove; vec3_t wishdir; float wishspeed; float scale; pm->ps->viewheight = DEFAULT_VIEWHEIGHT; // friction speed = VectorLength (pm->ps->velocity); if (speed < 1) { VectorCopy (vec3_origin, pm->ps->velocity); } else { drop = 0; friction = pm_friction*1.5; // extra friction control = speed < pm_stopspeed ? pm_stopspeed : speed; drop += control*friction*pml.frametime; // scale the velocity newspeed = speed - drop; if (newspeed < 0) newspeed = 0; newspeed /= speed; VectorScale (pm->ps->velocity, newspeed, pm->ps->velocity); } // accelerate scale = PM_CmdScale( &pm->cmd ); scale *= 1.5f; fmove = pm->cmd.forwardmove; smove = pm->cmd.rightmove; for (i=0 ; i<3 ; i++) wishvel[i] = pml.forward[i]*fmove + pml.right[i]*smove; wishvel[2] += pm->cmd.upmove; VectorCopy (wishvel, wishdir); wishspeed = VectorNormalize(wishdir); wishspeed *= scale; PM_Accelerate( wishdir, wishspeed, pm_flyaccelerate ); // move VectorMA (pm->ps->origin, pml.frametime, pm->ps->velocity, pm->ps->origin); } //============================================================================ /* ============== PM_Use Generates a use event ============== */ #define USE_DELAY 2000 void PM_Use( void ) { int useTime = 0; // don't allow attack until all buttons are up if ( pm->ps->pm_flags & PMF_RESPAWNED ) { return; } // ignore if not a normal player if ( pm->ps->pm_type != PM_NORMAL ) { return; } // check for dead player if ( pm->ps->stats[STAT_HEALTH] <= 0 ) { pm->ps->weapon = WP_NONE; return; } // Cant use so dont bother letting them try if ( !(pm->ps->pm_flags & PMF_CAN_USE ) || !(pm->cmd.buttons & BUTTON_USE ) ) { if ( pm->ps->stats[STAT_USEWEAPONDROP] ) { pm->ps->stats[STAT_USEWEAPONDROP] -= pml.msec; if ( pm->ps->stats[STAT_USEWEAPONDROP] < 0 ) { pm->ps->stats[STAT_USEWEAPONDROP] = 0; } } if ( pm->ps->pm_debounce & PMD_USE ) { pm->ps->pm_debounce &= ~PMD_USE; pm->ps->stats[STAT_USETIME] = 0; } return; } pm->ps->pm_debounce |= PMD_USE; useTime = pm->ps->stats[STAT_USETIME_MAX]; if ( useTime ) { int elapsedTime = pm->ps->stats[STAT_USETIME]; if ( elapsedTime < useTime ) { elapsedTime += pml.msec; } pm->ps->stats[STAT_USEWEAPONDROP] += pml.msec; if ( pm->ps->stats[STAT_USEWEAPONDROP] > 300 ) { pm->ps->stats[STAT_USEWEAPONDROP] = 300; } if ( elapsedTime >= useTime ) { pm->ps->stats[STAT_USETIME] = 0; PM_AddEvent ( EV_USE ); } else { pm->ps->stats[STAT_USETIME] = elapsedTime; } return; } if ( !(pm->ps->pm_debounce & PMD_USE) ) { PM_AddEvent ( EV_USE ); } } /* ================ PM_FootstepForSurface Returns an event number apropriate for the groundsurface ================ */ static void PM_FootstepForSurface( void ) { if ( pml.groundTrace.surfaceFlags & SURF_NOSTEPS ) { return; } PM_AddEventWithParm(EV_FOOTSTEP, pml.groundTrace.surfaceFlags & MATERIAL_MASK); } /* ================= PM_CrashLand Check for hard landings that generate sound events ================= */ int minDeltaForDmg = 97; int minDeltaForSmallPainSound = 30; int minDeltaForBigPainSound = 97; int minDeltaForSlowDown = 17; static void PM_CrashLand( int impactMaterial, vec3_t impactNormal ) { float delta; float dist; float vel, acc; float t; float a, b, c, den; float f; int scaleDelta; qboolean jumped; static vec3_t up = {0,0,1}; // were they juping? jumped = (pm->ps->pm_flags&PMF_JUMPING) ? qtrue : qfalse; pm->ps->pm_flags &= (~PMF_LADDER_JUMP); pm->ps->pm_flags &= (~PMF_JUMPING); // calculate the exact velocity on landing dist = pm->ps->origin[2] - pml.previous_origin[2]; vel = pml.previous_velocity[2]; acc = -pm->ps->gravity; a = acc / 2; b = vel; c = -dist; den = b * b - 4 * a * c; if ( den < 0 ) { return; } t = (-b - sqrt( den ) ) / ( 2 * a ); delta = vel + t * acc; delta = delta * delta * 0.000275f; switch ( pm->waterlevel ) { case 3: // never take falling damage if completely underwater return; // reduce falling damage if there is standing water case 2: delta *= 0.25; break; // reduce falling damage if there is standing water case 1: delta *= 0.5; break; } // Scale the delta based on the normal of the plane we hit f = DotProduct ( up, impactNormal ); if ( f < .25 ) { delta *= f; } // Just hit the ground, no more z velocity or we could bounce pm->ps->velocity[2] = 0; // start footstep cycle over if it wasnt a little jump if ( delta > minDeltaForSlowDown ) { pm->ps->bobCycle = 0; } if ( delta < 1 ) { return; } else if ( jumped && delta >= minDeltaForSlowDown ) { // Cut their forward velocity, this pretty much eliminates strafe jumping pm->ps->velocity[0] *= 0.25f; pm->ps->velocity[1] *= 0.25f; pm->ps->pm_time = 750; } // create a local entity event to play the sound // SURF_NODAMAGE is used for bounce pads where you don't ever // want to take damage or play a crunch sound scaleDelta = (int)delta; if (scaleDelta > 100 + minDeltaForDmg) { scaleDelta = 100 + minDeltaForDmg; } scaleDelta -= minDeltaForDmg; if ( !(pml.groundTrace.surfaceFlags & SURF_NODAMAGE) ) { if ( delta > minDeltaForBigPainSound ) { PM_AddEventWithParm(EV_FALL_FAR, scaleDelta | ((impactMaterial & MATERIAL_MASK)<<8)); } else if ( delta > minDeltaForDmg ) { // this is a pain grunt, so don't play it if dead if ( pm->ps->stats[STAT_HEALTH] > 0 ) { PM_AddEventWithParm(EV_FALL_MEDIUM, scaleDelta | ((impactMaterial & MATERIAL_MASK)<<8)); } } else if ( delta > minDeltaForSlowDown ) { PM_AddEventWithParm(EV_FALL_SHORT, impactMaterial & MATERIAL_MASK ); } else if ( delta > 10 && (pm->cmd.buttons & BUTTON_WALKING) ) { PM_FootstepForSurface(); } } } /* ============= PM_CorrectAllSolid ============= */ static int PM_CorrectAllSolid( trace_t *trace ) { int i, j, k; vec3_t point; if ( pm->debugLevel ) { Com_Printf("%i:allsolid\n", c_pmove); } // jitter around for (i = -1; i <= 1; i++) { for (j = -1; j <= 1; j++) { for (k = -1; k <= 1; k++) { VectorCopy(pm->ps->origin, point); point[0] += (float) i; point[1] += (float) j; point[2] += (float) k; pm->trace (trace, point, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask); if ( !trace->allsolid ) { point[0] = pm->ps->origin[0]; point[1] = pm->ps->origin[1]; point[2] = pm->ps->origin[2] - 0.25; pm->trace (trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask); pml.groundTrace = *trace; return qtrue; } } } } pm->ps->groundEntityNum = ENTITYNUM_NONE; pml.groundPlane = qfalse; pml.walking = qfalse; return qfalse; } /* ============= PM_GroundTraceMissed The ground trace didn't hit a surface, so we are in freefall ============= */ static void PM_GroundTraceMissed( void ) { trace_t trace; vec3_t point; if ( pm->ps->groundEntityNum != ENTITYNUM_NONE ) { // we just transitioned into freefall if ( pm->debugLevel ) { Com_Printf("%i:lift\n", c_pmove); } // if they aren't in a jumping animation and the ground is a ways away, force into it // if we didn't do the trace, the player would be backflipping down staircases VectorCopy( pm->ps->origin, point ); point[2] -= 64; pm->trace (&trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask); if ( trace.fraction == 1.0 ) { if ( pm->cmd.forwardmove >= 0 ) { pm->ps->pm_flags &= ~PMF_BACKWARDS_JUMP; } else { pm->ps->pm_flags |= PMF_BACKWARDS_JUMP; } } } pm->ps->groundEntityNum = ENTITYNUM_NONE; pml.groundPlane = qfalse; pml.walking = qfalse; } /* ============= PM_GroundTrace ============= */ static void PM_GroundTrace( void ) { vec3_t point; trace_t trace; float minWalkNormal; point[0] = pm->ps->origin[0]; point[1] = pm->ps->origin[1]; point[2] = pm->ps->origin[2] - 0.25; pm->trace (&trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask); pml.groundTrace = trace; // When stuck to antoher player set a flag to let the trigger code know so it can unstick the player if ( (trace.allsolid || trace.startsolid) && trace.entityNum < MAX_CLIENTS ) { pm->ps->pm_flags |= PMF_SIAMESETWINS; } // if the trace didn't hit anything, we are in free fall if ( trace.fraction == 1.0 ) { PM_GroundTraceMissed(); pml.groundPlane = qfalse; pml.walking = qfalse; return; } // check if getting thrown off the ground if ( pm->ps->velocity[2] > 0 && DotProduct( pm->ps->velocity, trace.plane.normal ) > 10 ) { if ( pm->debugLevel ) { Com_Printf("%i:kickoff\n", c_pmove); } // go into jump animation if ( pm->cmd.forwardmove >= 0 ) { PM_ForceLegsAnim( pm->ps, LEGS_JUMP ); pm->ps->pm_flags &= ~PMF_BACKWARDS_JUMP; } else { PM_ForceLegsAnim( pm->ps, LEGS_JUMP_BACK ); pm->ps->pm_flags |= PMF_BACKWARDS_JUMP; } pm->ps->groundEntityNum = ENTITYNUM_NONE; pml.groundPlane = qfalse; pml.walking = qfalse; return; } // slopes that are too steep will not be considered onground if ( trace.contents & CONTENTS_TERRAIN ) { minWalkNormal = MIN_WALK_NORMAL_TERRAIN; } else { minWalkNormal = MIN_WALK_NORMAL; } if ( trace.plane.normal[2] < minWalkNormal ) { if ( pm->debugLevel ) { Com_Printf("%i:steep\n", c_pmove); } // FIXME: if they can't slide down the slope, let them // walk (sharp crevices) pm->ps->groundEntityNum = ENTITYNUM_NONE; pml.groundPlane = qtrue; pml.walking = qfalse; return; } pml.groundPlane = qtrue; pml.walking = qtrue; // hitting solid ground will end a waterjump if (pm->ps->pm_flags & PMF_TIME_WATERJUMP) { pm->ps->pm_flags &= ~(PMF_TIME_WATERJUMP | PMF_TIME_LAND); pm->ps->pm_time = 0; } if ( pm->ps->groundEntityNum == ENTITYNUM_NONE) { // just hit the ground // if ((pml.groundTrace.contents & CONTENTS_TERRAIN) && pml.previous_velocity[2] > -200) // { // } // else { if ( pm->debugLevel ) { Com_Printf("%i:Land\n", c_pmove); } PM_CrashLand(trace.surfaceFlags & MATERIAL_MASK, trace.plane.normal ); // don't do landing time if we were just going down a slope if ( pml.previous_velocity[2] < -200 ) { // don't allow another jump for a little while pm->ps->pm_flags |= PMF_TIME_LAND; pm->ps->pm_time = 250; } } } pm->ps->groundEntityNum = trace.entityNum; // don't reset the z velocity for slopes // pm->ps->velocity[2] = 0; PM_AddTouchEnt( trace.entityNum ); } /* ============= PM_SetWaterLevel FIXME: avoid this twice? certainly if not moving ============= */ static void PM_SetWaterLevel( void ) { vec3_t point; int cont; int sample1; int sample2; // // get waterlevel, accounting for ducking // pm->waterlevel = 0; pm->watertype = 0; point[0] = pm->ps->origin[0]; point[1] = pm->ps->origin[1]; point[2] = pm->ps->origin[2] + pm->mins[2]; cont = pm->pointcontents( point, pm->ps->clientNum ); // See if we are on a ladder too if ( !(pm->ps->pm_flags&PMF_LADDER_JUMP) && (cont & CONTENTS_LADDER) ) { if ( pm->ps->ladder == -1 ) { pm->ps->ladder = BG_FindLadder ( pm->ps->origin ); } pm->ps->pm_flags |= PMF_LADDER; } else { pm->ps->ladder = -1; pm->ps->pm_flags &= ~PMF_LADDER; } if ( cont & MASK_WATER ) { sample2 = pm->ps->viewheight - MINS_Z; sample1 = sample2 / 2; pm->watertype = cont; pm->waterlevel = 1; point[2] = pm->ps->origin[2] + MINS_Z + sample1; cont = pm->pointcontents (point, pm->ps->clientNum ); if ( cont & MASK_WATER ) { pm->waterlevel = 2; point[2] = pm->ps->origin[2] + MINS_Z + sample2; cont = pm->pointcontents (point, pm->ps->clientNum ); if ( cont & MASK_WATER ) { pm->waterlevel = 3; } } } } /* ============== PM_CheckCrouchJump Handles crouch jumping ============== */ static void PM_CheckCrouchJump ( void ) { // Already crouch jumping so check to see if its over if ( pm->ps->pm_flags & PMF_CROUCH_JUMP ) { // If they are on the ground the crouch jump is over if ( pml.groundPlane ) { pm->ps->pm_flags &= ~PMF_CROUCH_JUMP; } } else { // If not on the ground and still heading up then crouch jump is possible. if ( !pml.groundPlane && (pm->ps->pm_flags & PMF_JUMPING) && pm->cmd.upmove < 0 ) { pm->ps->pm_flags |= PMF_CROUCH_JUMP; } } // Check again if still crouch jumping and if so alter the view height // so the client doesnt look like they are ducking in mid air if ( (pm->ps->pm_flags & PMF_CROUCH_JUMP) && (pm->ps->pm_flags & PMF_JUMPING) ) { // If still ducked look for windows if ( pm->ps->pm_flags & PMF_DUCKED ) { trace_t trace; vec3_t maxs; VectorCopy ( pm->maxs, maxs ); maxs[2] = DEFAULT_PLAYER_Z_MAX; pm->trace (&trace, pm->ps->origin, pm->mins, maxs, pm->ps->origin, pm->ps->clientNum, pm->tracemask ); if ( !(trace.allsolid || trace.startsolid) ) { pm->ps->viewheight = DEFAULT_VIEWHEIGHT; } } } } /* ============== PM_CheckDuck Sets mins, maxs, and pm->ps->viewheight ============== */ static void PM_CheckDuck (void) { trace_t trace; if ( (pm == 0) || (pm->ps == 0) ) { return; } pm->mins[0] = -15; pm->mins[1] = -15; pm->maxs[0] = 15; pm->maxs[1] = 15; pm->mins[2] = MINS_Z; if (pm->ps->pm_type == PM_DEAD) { pm->maxs[2] = DEAD_PLAYER_Z_MAX; pm->ps->viewheight = DEAD_VIEWHEIGHT; return; } // duck or prone if (pm->cmd.upmove < 0) { // assume ducked at first pm->ps->pm_flags |= PMF_DUCKED; } else { // stand up if possible if ( pm->ps->pm_flags & PMF_DUCKED ) { pm->maxs[2] = DEFAULT_PLAYER_Z_MAX; pm->trace (&trace, pm->ps->origin, pm->mins, pm->maxs, pm->ps->origin, pm->ps->clientNum, pm->tracemask ); if (!trace.allsolid) { pm->ps->pm_flags &= ~PMF_DUCKED; } } } if ( (pm->ps->pm_flags & PMF_DUCKED) ) { pm->maxs[2] = CROUCH_PLAYER_Z_MAX; pm->ps->viewheight = CROUCH_VIEWHEIGHT; } else { pm->maxs[2] = DEFAULT_PLAYER_Z_MAX; pm->ps->viewheight = DEFAULT_VIEWHEIGHT; } } /* =============== PM_Footsteps =============== */ static void PM_Footsteps( void ) { float bobmove; int old; qboolean footstep; // // calculate speed and cycle to be used for // all cyclic walking effects // pm->xyspeed = sqrt( pm->ps->velocity[0] * pm->ps->velocity[0] + pm->ps->velocity[1] * pm->ps->velocity[1] ); if ( pm->ps->groundEntityNum == ENTITYNUM_NONE ) { if ( pm->ps->pm_flags & PMF_LADDER ) { } else { // airborne leaves position in cycle intact, but doesn't advance if ( pm->waterlevel > 1 ) { PM_ContinueLegsAnim( pm->ps, LEGS_SWIM ); } return; } } // if not trying to move if ( !pm->cmd.forwardmove && !pm->cmd.rightmove ) { if ( pm->xyspeed < 5 ) { pm->ps->bobCycle = 0; // start at beginning of cycle again if ( pm->ps->pm_flags & PMF_DUCKED ) { if ( pm->ps->leanTime - LEAN_TIME < 0 ) { PM_ContinueLegsAnim( pm->ps, LEGS_LEAN_CROUCH_LEFT ); } else if ( pm->ps->leanTime - LEAN_TIME > 0 ) { PM_ContinueLegsAnim( pm->ps, LEGS_LEAN_CROUCH_RIGHT ); } else { PM_ContinueLegsAnim( pm->ps, LEGS_IDLE_CROUCH ); } } else { if ( pm->ps->leanTime - LEAN_TIME < 0 ) { PM_ContinueLegsAnim( pm->ps, LEGS_LEAN_LEFT ); } else if ( pm->ps->leanTime - LEAN_TIME > 0 ) { PM_ContinueLegsAnim( pm->ps, LEGS_LEAN_RIGHT ); } else { PM_ContinueLegsAnim( pm->ps, TORSO_IDLE_PISTOL ); } } } return; } footstep = qfalse; if ( (pm->ps->pm_flags & PMF_DUCKED) && (pm->ps->groundEntityNum != ENTITYNUM_NONE ) ) { bobmove = 0.25; // ducked characters bob much faster if ( pm->ps->pm_flags & PMF_BACKWARDS_RUN ) { PM_ContinueLegsAnim( pm->ps, LEGS_WALK_CROUCH_BACK ); } else { if ( pm->ps->leanTime - LEAN_TIME < 0 ) { if ( pm->cmd.rightmove > 0 ) { PM_ContinueLegsAnim( pm->ps, LEGS_LEANLEFT_CROUCH_WALKRIGHT ); } else { PM_ContinueLegsAnim( pm->ps, LEGS_LEANLEFT_CROUCH_WALKLEFT ); } } else if ( pm->ps->leanTime - LEAN_TIME > 0 ) { if ( pm->cmd.rightmove > 0 ) { PM_ContinueLegsAnim( pm->ps, LEGS_LEANRIGHT_CROUCH_WALKRIGHT ); } else { PM_ContinueLegsAnim( pm->ps, LEGS_LEANRIGHT_CROUCH_WALKLEFT ); } } else { PM_ContinueLegsAnim( pm->ps, LEGS_WALK_CROUCH ); } } } else { if ( !( pm->cmd.buttons & BUTTON_WALKING ) ) { PM_BeginZoomOut ( ); bobmove = 0.4f; // faster speeds bob faster if ( pm->ps->pm_flags & PMF_BACKWARDS_RUN ) { PM_ContinueLegsAnim( pm->ps, LEGS_RUN_BACK ); } else { PM_ContinueLegsAnim( pm->ps, LEGS_RUN ); } footstep = qtrue; } else { bobmove = 0.3f; // walking bobs slow if ( pm->ps->pm_flags & PMF_BACKWARDS_RUN ) { PM_ContinueLegsAnim( pm->ps, LEGS_WALK_BACK ); } else { if ( pm->ps->leanTime - LEAN_TIME < 0 ) { if ( pm->cmd.rightmove > 0 ) { PM_ContinueLegsAnim( pm->ps, LEGS_LEANLEFT_WALKRIGHT ); } else { PM_ContinueLegsAnim( pm->ps, LEGS_LEANLEFT_WALKLEFT ); } } else if ( pm->ps->leanTime - LEAN_TIME > 0 ) { if ( pm->cmd.rightmove > 0 ) { PM_ContinueLegsAnim( pm->ps, LEGS_LEANRIGHT_WALKRIGHT ); } else { PM_ContinueLegsAnim( pm->ps, LEGS_LEANRIGHT_WALKLEFT ); } } else { PM_ContinueLegsAnim( pm->ps, LEGS_WALK ); } } } } // check for footstep / splash sounds old = pm->ps->bobCycle; pm->ps->bobCycle = (int)( old + bobmove * pml.msec ) & 255; // if we just crossed a cycle boundary, play an apropriate footstep event if ( ( ( old + 64 ) ^ ( pm->ps->bobCycle + 64 ) ) & 128 ) { if ( pm->waterlevel == 0 ) { // on ground will only play sounds if running if ( footstep && !pm->noFootsteps ) { PM_FootstepForSurface(); } } else if ( pm->waterlevel == 1 ) { // splashing PM_AddEvent( EV_WATER_FOOTSTEP ); } else if ( pm->waterlevel == 2 ) { // wading / swimming at surface PM_AddEvent( EV_SWIM ); } else if ( pm->waterlevel == 3 ) { // no sound when completely underwater } } } /* ============== PM_WaterEvents Generate sound events for entering and leaving water ============== */ static void PM_WaterEvents( void ) { if ( !pml.previous_waterlevel && pm->waterlevel == 1 ) { PM_AddEvent( EV_WATER_TOUCH ); } else if ( pml.previous_waterlevel <= 1 && pm->waterlevel > 1 ) { if ( pm->ps->velocity[2] < -100 ) { PM_AddEvent( EV_WATER_LAND ); } } // // check for head just coming out of water // if (pml.previous_waterlevel == 3 && pm->waterlevel != 3) { PM_AddEvent( EV_WATER_CLEAR ); } } /* =============== PM_SetWeaponTime =============== */ static void PM_SetWeaponTime ( TAnimWeapon *aW ) { TAnimInfoWeapon *aIW; if(!aW) { assert(0); } // Weapon model info tells us how long the anim is aIW = aW->mWeaponModelInfo; if ( !aIW ) { return; } pm->ps->weaponTime = 1000.0f / aIW->mFPS[0] * aIW->mNumFrames[0] / aIW->mSpeed; pm->ps->weaponAnimTime = pm->ps->weaponTime; } /* =============== BG_GetWeaponNote =============== */ TNoteTrack *BG_GetWeaponNote( playerState_t* ps, int weapon, int anim, int animChoice, int callbackStep ) { TAnimWeapon *aW; TAnimInfoWeapon *aIW; TNoteTrack *note; int n=0; note = NULL; aW=BG_GetInviewAnimFromIndex( weapon, anim&~ANIM_TOGGLEBIT); if (!aW) { return 0; } aIW = aW->mWeaponModelInfo; if ( !aIW ) { return 0; } // Find the callback for the given step for ( note = aIW->mNoteTracks[ps->weaponAnimIdChoice], n=0; note && n < callbackStep; note = note->mNext, n++ ) { // Do nothing, loop does it all } return(note); } /* ============== PM_CheckWeaponNotes ============== */ void PM_CheckWeaponNotes ( void ) { playerState_t *ps; TAnimWeapon *aW; TAnimInfoWeapon *aIW; TNoteTrack *note; int step; int stepTime; pm->ps->weaponCallbackTime += pml.msec; // 1st note step. step = 0; ps = pm->ps; aW = BG_GetInviewAnimFromIndex ( ps->weapon, (ps->weaponAnimId&~ANIM_TOGGLEBIT) ); assert ( aW ); if ( !aW ) { return; } // Get the cached weapon model info aIW = aW->mWeaponModelInfo; if ( !aIW ) { return; } stepTime = 1000.0f / aIW->mFPS[pm->ps->weaponAnimIdChoice] / aIW->mSpeed;; note = aIW->mNoteTracks[pm->ps->weaponAnimIdChoice]; while(note) { if( pm->ps->weaponCallbackTime >= note->mFrame * stepTime ) { if(step > pm->ps->weaponCallbackStep) { if( !Q_stricmp("fire",note->mNote) || !Q_stricmp("altfire",note->mNote) ) { if(pm->ps->weaponstate==WEAPON_FIRING) { int seed; // Update the seed seed = pm->ps->stats[STAT_SEED]; Q_rand ( &seed ); seed = seed & 0xFFFF; pm->ps->stats[STAT_SEED] = seed; PM_AddEvent(EV_FIRE_WEAPON); PM_Weapon_AddInaccuracy(ATTACK_NORMAL); PM_Weapon_AddKickAngles(weaponData[pm->ps->weapon].attack[ATTACK_NORMAL].maxKickAngles); } else if(pm->ps->weaponstate==WEAPON_FIRING_ALT) { PM_AddEvent(EV_ALT_FIRE); PM_Weapon_AddKickAngles(weaponData[pm->ps->weapon].attack[ATTACK_ALTERNATE].maxKickAngles); } } PM_AddEventWithParm ( EV_WEAPON_CALLBACK, ((step&0xFF) << 24) + ((pm->ps->weaponAnimIdChoice&0xFF)<<16) + (((ps->weaponAnimId&~ANIM_TOGGLEBIT)&0xFF)<<8) + pm->ps->weapon); pm->ps->weaponCallbackStep=step; } } step++; note=note->mNext; } } /* =============== PM_SetWeaponAnimChoice =============== */ void PM_SetWeaponAnimChoice(TAnimWeapon *aW) { TAnimInfoWeapon *aIW; if(!aW) { assert(0); return; } // Get the cached weapon model info aIW = aW->mWeaponModelInfo; if ( !aIW ) { return; } pm->ps->weaponAnimIdChoice = rand()%aIW->mNumChoices; } /* =============== PM_GetAnimFromName =============== */ TAnimWeapon* PM_GetAnimFromName ( char *animName, playerState_t *ps, int *animIndex ) { TAnimWeapon *aW=0; TAnimInfoWeapon *aIW=0; char tempname[MAX_QPATH]; switch(ps->weapon) { case WP_KNIFE: if(!strcmp(animName,"charge")) { // Get 'prefire' anim. aW=BG_GetInviewAnim(pm->ps->weapon,"prefire",animIndex); PM_SetWeaponAnimChoice(aW); } else if(!strcmp(animName,"fire")) { aW=BG_GetInviewAnimFromIndex(ps->weapon,ps->weaponAnimId&~ANIM_TOGGLEBIT); if((!strcmp(aW->mName,"prefire"))||strstr(aW->mName,"firetrans")) { // Get 'fire' anim. aW=BG_GetInviewAnim(ps->weapon,"fire",animIndex); PM_SetWeaponAnimChoice(aW); } else if(!strcmp(aW->mName,"fire")) { // Get 'firetrans' anim. We don't call PM_SetWeaponAnimChoice() // because the firetrans anims are matched to the fire anims. aIW=BG_GetInviewModelAnim(ps->weapon,"weaponmodel","fire"); strcpy(tempname,aIW->mTransition[ps->weaponAnimIdChoice]); aW=BG_GetInviewAnim(ps->weapon,tempname,animIndex); } else { // Get 'prefire' anim. aW=BG_GetInviewAnim(pm->ps->weapon,"fire",animIndex); PM_SetWeaponAnimChoice(aW); } } else if(!strcmp(animName,"fireend")) { aW=BG_GetInviewAnim(ps->weapon,"fireend1",animIndex); PM_SetWeaponAnimChoice(aW); } else { // Nothing clever about the other sequences. aW=BG_GetInviewAnim(pm->ps->weapon,animName,animIndex); PM_SetWeaponAnimChoice(aW); } break; case WP_MM1_GRENADE_LAUNCHER: case WP_M590_SHOTGUN: if(!strcmp(animName,"reload")) { aW=BG_GetInviewAnimFromIndex(ps->weapon,ps->weaponAnimId&~ANIM_TOGGLEBIT); if(!strcmp(aW->mName,"reloadbegin")||!strcmp(aW->mName,"reloadshell")) { // Get 'reloadshell' anim. aW=BG_GetInviewAnim(ps->weapon,"reloadshell",animIndex); } else { // Get 'reloadbegin' anim. aW=BG_GetInviewAnim(pm->ps->weapon,"reloadbegin",animIndex); } } else if(!strcmp(animName,"reloadend")) { // Get 'reloadend' anim. aW=BG_GetInviewAnim(ps->weapon,"reloadend",animIndex); } else { // Nothing clever about the other sequences. aW=BG_GetInviewAnim(pm->ps->weapon,animName,animIndex); } PM_SetWeaponAnimChoice(aW); break; case WP_M84_GRENADE: case WP_SMOHG92_GRENADE: case WP_ANM14_GRENADE: case WP_M15_GRENADE: if(!strcmp(animName,"charge")) { // Get 'throwbegin' anim. aW=BG_GetInviewAnim(ps->weapon,"throwbegin",animIndex); } else if(!strcmp(animName,"altcharge")) { // Get 'throwbegin' anim. aW=BG_GetInviewAnim(ps->weapon,"altthrowbegin",animIndex); } else if(!strcmp(animName,"fire")) { // Get 'throwend' anim. aW=BG_GetInviewAnim(ps->weapon,"throwend",animIndex); } else if(!strcmp(animName,"altfire")) { // Get 'throwend' anim. aW=BG_GetInviewAnim(ps->weapon,"altthrowend",animIndex); } else { // Nothing clever about the other sequences. aW=BG_GetInviewAnim(pm->ps->weapon,animName,animIndex); } PM_SetWeaponAnimChoice(aW); break; default: // Other weapons don't do anything fancy. aW=BG_GetInviewAnim(ps->weapon,animName,animIndex); PM_SetWeaponAnimChoice(aW); break; } return(aW); } /* =============== BG_GetWeaponAnim =============== */ TAnimWeapon *BG_GetWeaponAnim(int weaponAction,playerState_t *ps,int *animIndex) { TAnimWeapon *aW=0; switch(weaponAction) { case WACT_READY: aW=PM_GetAnimFromName("ready",ps,animIndex); break; case WACT_IDLE: aW=PM_GetAnimFromName("idle",ps,animIndex); break; case WACT_FIRE: aW=PM_GetAnimFromName("fire",ps,animIndex); break; case WACT_FIRE_END: aW=PM_GetAnimFromName("fireend",ps,animIndex); break; case WACT_ALTFIRE: aW=PM_GetAnimFromName("altfire",ps,animIndex); break; case WACT_ALTFIRE_END: aW=PM_GetAnimFromName("altfireend",ps,animIndex); break; case WACT_RELOAD: aW=PM_GetAnimFromName("reload",ps,animIndex); break; case WACT_ALTRELOAD: aW=PM_GetAnimFromName("altreload",ps,animIndex); break; case WACT_RELOAD_END: aW=PM_GetAnimFromName("reloadend",ps,animIndex); break; case WACT_PUTAWAY: aW=PM_GetAnimFromName("done",ps,animIndex); break; case WACT_ZOOMIN: aW=PM_GetAnimFromName("zoomin",ps,animIndex); break; case WACT_ZOOMOUT: aW=PM_GetAnimFromName("zoomout",ps,animIndex); break; case WACT_CHARGE: aW=PM_GetAnimFromName("charge",ps,animIndex); break; case WACT_ALTCHARGE: aW=PM_GetAnimFromName("altcharge",ps,animIndex); break; default: Com_Printf("Anim unknown: %i\n",weaponAction); break; } return(aW); } /* =============== PM_HandleWeaponAction =============== */ static void PM_HandleWeaponAction(int weaponAction) { TAnimWeapon *aW; int animIndex; aW = BG_GetWeaponAnim ( weaponAction, pm->ps, &animIndex ); if(!aW) { return; } // Reset callback timer. We have to account for any remaining weapontime // or we could miss off callbacks during sequences that are short in duration. pm->ps->weaponCallbackTime = -pm->ps->weaponTime; pm->ps->weaponCallbackStep = -1; PM_SetWeaponTime(aW); if((pm->ps->weaponAnimId&~ANIM_TOGGLEBIT)==animIndex) { animIndex=pm->ps->weaponAnimId^ANIM_TOGGLEBIT; } pm->ps->weaponAnimId = animIndex; } /* =============== PM_BeginZoomIn =============== */ static void PM_BeginZoomIn(void) { // Reset the zom fov if not rezooming if ( !(pm->ps->pm_flags & PMF_ZOOM_REZOOM) ) { pm->ps->zoomFov = 0; } pm->ps->weaponstate=WEAPON_ZOOMIN; PM_HandleWeaponAction(WACT_ZOOMIN); } /* =============== PM_BeginZoomOut =============== */ static void PM_BeginZoomOut(void) { if ( !(pm->ps->pm_flags & PMF_ZOOMED) ) { return; } if ( !(pm->ps->pm_flags & PMF_ZOOM_DEFER_RELOAD ) ) { pm->ps->zoomFov = 0; } pm->ps->weaponstate=WEAPON_ZOOMOUT; PM_HandleWeaponAction(WACT_ZOOMOUT); pm->ps->zoomTime=pm->ps->commandTime; pm->ps->pm_flags &= ~(PMF_ZOOM_LOCKED|PMF_ZOOM_REZOOM|PMF_ZOOMED); } /* =============== PM_BeginWeaponChange =============== */ static void PM_BeginWeaponChange(int weapon) { if ( weapon <= WP_NONE || weapon >= WP_NUM_WEAPONS ) { return; } if ( !( pm->ps->stats[STAT_WEAPONS] & ( 1 << weapon ) ) ) { return; } if ( pm->ps->weaponstate == WEAPON_DROPPING ) { return; } // Dont allow switching to the weapon the client is already using if ( pm->ps->weapon == weapon ) { PM_AddEvent(EV_CHANGE_WEAPON_CANCELLED ); if ( pm->ps->weaponTime <= 0 ) { // Add a little delay so it doenst fire because this was caused by // the menu selection pm->ps->weaponTime = 150; } return; } // turn off any kind of zooming when weapon switching. pm->ps->zoomFov = 0; pm->ps->zoomTime = 0; pm->ps->pm_flags &= ~(PMF_ZOOM_FLAGS); // Clear the weapon time pm->ps->weaponTime = 0; pm->ps->weaponFireBurstCount = 0; pm->ps->weaponAnimTime = 0; PM_AddEvent(EV_CHANGE_WEAPON); pm->ps->weaponstate = WEAPON_DROPPING; if( pm->ps->weapon >= WP_M84_GRENADE && pm->ps->weapon <= WP_M15_GRENADE && pm->ps->clip[ATTACK_NORMAL][pm->ps->weapon] <= 0 ) { // We don't want to play the 'putaway' anim for the grenades if we are out of grenades! return; } PM_HandleWeaponAction(WACT_PUTAWAY); PM_StartTorsoAnim( pm->ps, weaponData[pm->ps->weapon].animDrop, pm->ps->weaponAnimTime ); } /* =============== PM_FinishWeaponChange =============== */ static void PM_FinishWeaponChange( void ) { int weapon; weapon = pm->cmd.weapon & ~WP_DELAYED_CHANGE_BIT; if( weapon < WP_NONE || weapon >= WP_NUM_WEAPONS ) { weapon = WP_KNIFE; } if(!( pm->ps->stats[STAT_WEAPONS] & ( 1 << weapon ) ) ) { weapon = WP_KNIFE; } PM_AddEvent(EV_READY_WEAPON); pm->ps->weapon = weapon; pm->ps->weaponstate = WEAPON_RAISING; pm->ps->weaponTime = 0; pm->ps->weaponAnimTime = 0; // Default to auto (or next available fire mode). if ( pm->ps->firemode[pm->ps->weapon] == WP_FIREMODE_NONE ) { pm->ps->firemode[pm->ps->weapon] = BG_FindFireMode ( pm->ps->weapon, ATTACK_NORMAL, WP_FIREMODE_AUTO ); } // We don't want to play the 'takeout' anim for the grenades if we are about to reload anyway if( pm->ps->weapon >= WP_M84_GRENADE && pm->ps->weapon <= WP_M15_GRENADE && pm->ps->clip[ATTACK_NORMAL][pm->ps->weapon] <=0 ) { return; } PM_HandleWeaponAction(WACT_READY); pm->ps->weaponTime = min(150,pm->ps->weaponTime); PM_StartTorsoAnim( pm->ps, weaponData[pm->ps->weapon].animRaise, pm->ps->weaponAnimTime ); } /* ============== PM_LoadShell Only used for the M590 shotgun. ============== */ void PM_LoadShell(void) { pm->ps->clip[ATTACK_NORMAL][pm->ps->weapon]++; pm->ps->ammo[weaponData[pm->ps->weapon].attack[ATTACK_NORMAL].ammoIndex]--; } /* ============== PM_StartRefillClip ============== */ void PM_StartRefillClip ( attackType_t attack ) { int extra; assert ( attack >= ATTACK_NORMAL && attack < ATTACK_MAX ); // Sniper rifle should unzoom first before reloading. if( pm->ps->pm_flags & PMF_ZOOMED ) { pm->ps->pm_flags |= PMF_ZOOM_DEFER_RELOAD; PM_BeginZoomOut(); return; } pm->ps->weaponstate=(attack==ATTACK_ALTERNATE)?WEAPON_RELOADING_ALT:WEAPON_RELOADING; pm->ps->weaponFireBurstCount=0; if(pm->ps->weapon!=WP_KNIFE) { // We don't want to play the reload anim for the knife, as it is part of // the throw anim anyway. if(attack==ATTACK_ALTERNATE) { PM_HandleWeaponAction(WACT_ALTRELOAD); } else { PM_HandleWeaponAction(WACT_RELOAD); } } if( pm->ps->weapon==WP_M590_SHOTGUN || pm->ps->weapon == WP_MM1_GRENADE_LAUNCHER ) { PM_StartTorsoAnim ( pm->ps, weaponData[pm->ps->weapon].animReloadStart, pm->ps->weaponTime ); return; } extra = weaponData[pm->ps->weapon].attack[attack].clipSize; extra -= pm->ps->clip[attack][pm->ps->weapon]; if(pm->ps->ammo[weaponData[pm->ps->weapon].attack[attack].ammoIndex]ps->ammo[weaponData[pm->ps->weapon].attack[attack].ammoIndex]; } pm->ps->clip[attack][pm->ps->weapon]+=extra; pm->ps->ammo[weaponData[pm->ps->weapon].attack[attack].ammoIndex]-=extra; // Rezoom the sniper rifle, if it was zoomed before we started reloading. if(pm->ps->pm_flags & PMF_ZOOM_DEFER_RELOAD ) { pm->ps->pm_flags |= PMF_ZOOM_REZOOM; pm->ps->pm_flags &= ~PMF_ZOOM_DEFER_RELOAD; } PM_StartTorsoAnim ( pm->ps, weaponData[pm->ps->weapon].animReload, pm->ps->weaponTime ); } /* ============== PM_EndRefillClip ============== */ void PM_EndRefillClip(void) { pm->ps->weaponstate=WEAPON_READY; pm->ps->weaponFireBurstCount = 0; } /* =============== PM_GetAttackButtons =============== */ int PM_GetAttackButtons(void) { int buttons=pm->cmd.buttons; // Debounce firemode select button. if ( buttons & BUTTON_FIREMODE ) { if(!(pm->ps->pm_debounce & PMD_FIREMODE)) { pm->ps->pm_debounce |= PMD_FIREMODE; } else { buttons &= ~BUTTON_FIREMODE; } } else { pm->ps->pm_debounce &= ~PMD_FIREMODE; } // As soon as the button is released you are ok to press attack again if ( pm->ps->pm_debounce & PMD_ATTACK ) { if ( !(buttons & BUTTON_ATTACK) ) { pm->ps->pm_debounce &= ~(PMD_ATTACK); } else if ( pm->ps->firemode[pm->ps->weapon] != WP_FIREMODE_AUTO ) { buttons &= ~BUTTON_ATTACK; } } if ( pm->ps->stats[STAT_FROZEN] ) { buttons &= ~BUTTON_ATTACK; } // Handle firebutton in varous firemodes. switch( pm->ps->firemode[pm->ps->weapon] ) { case WP_FIREMODE_AUTO: break; case WP_FIREMODE_BURST: // Debounce attack button and disable other buttons during burst fire. if(buttons&BUTTON_ATTACK) { if(!pm->ps->weaponFireBurstCount) { pm->ps->weaponFireBurstCount=3; } } if(pm->ps->weaponFireBurstCount) { buttons|=BUTTON_ATTACK; buttons&=~BUTTON_ALT_ATTACK; buttons&=~BUTTON_RELOAD; buttons&=~BUTTON_ZOOMIN; buttons&=~BUTTON_ZOOMOUT; buttons&=~BUTTON_FIREMODE; } break; case WP_FIREMODE_SINGLE: break; } // Handle single fire alt fire attacks or the sniper zoom if ( pm->ps->weapon == WP_MSG90A1 || (weaponData[pm->ps->weapon].attack[ATTACK_ALTERNATE].weaponFlags & (1<ps->pm_debounce & PMD_ALTATTACK ) ) { pm->ps->pm_debounce |= PMD_ALTATTACK; } else { buttons &= ~BUTTON_ALT_ATTACK; } } else { pm->ps->pm_debounce &= ~PMD_ALTATTACK; } } return buttons; } /* ============== PM_Weapon_AddInaccuracy ============== */ #define ACCURACY_FADERATE 0.2 #define RECOVER_TIME 800 #define RECOVER_TIME_SQ 200000.0 static void PM_Weapon_AddInaccuracy( attackType_t attack ) { assert ( attack >= ATTACK_NORMAL && attack < ATTACK_MAX ); // Zoomed sniper weapons don't add innacuracy if ont hte ground if( (pm->ps->pm_flags & PMF_ZOOMED) && pml.groundPlane ) { pm->ps->inaccuracy += weaponData[pm->ps->weapon].attack[attack].zoomInaccuracy; } else { pm->ps->inaccuracy += weaponData[pm->ps->weapon].attack[attack].inaccuracy; } pm->ps->inaccuracyTime = RECOVER_TIME; if ( pm->ps->inaccuracy > weaponData[pm->ps->weapon].attack[attack].maxInaccuracy ) { pm->ps->inaccuracy = weaponData[pm->ps->weapon].attack[attack].maxInaccuracy; } } /* ============== PM_Weapon_UpdateInaccuracy ============== */ static void PM_Weapon_UpdateInaccuracy(void) { if( pm->ps->inaccuracy <= 0 ) { pm->ps->inaccuracy = 0; return; } if ( pm->ps->weaponstate == WEAPON_FIRING || pm->ps->weaponstate == WEAPON_FIRING_ALT) { pm->ps->inaccuracyTime -= (pml.msec * 3 / 4); } else { pm->ps->inaccuracyTime -= pml.msec; } if ( pm->ps->inaccuracyTime <= 0 ) { pm->ps->inaccuracy = 0; } else { // decrement inaccuracy quadraticly to simulate the player recovering slowly at first, then rapidly int diff = RECOVER_TIME - pm->ps->inaccuracyTime; pm->ps->inaccuracy *= (1 - (diff*diff)/RECOVER_TIME_SQ); } } /* ============== PM_Weapon_AddKickAngles ============== */ static void PM_Weapon_AddKickAngles(vec3_t kickAngles) { // Throw the new kick angles into the integer versions pm->ps->kickPitch += (int)(kickAngles[PITCH] * 500.0f); if ( pm->ps->kickPitch > 180000 ) pm->ps->kickPitch = 180000; } /* ============== PM_Weapon_UpdateKickAngles ============== */ static void PM_Weapon_UpdateKickAngles(void) { // bring our kickAngles back down to zero over time int i; float degreesCorrectedPerMSecond = 0.01f; qboolean firing; float degreesToCorrect = degreesCorrectedPerMSecond*pml.msec; vec3_t kickAngles; // Extract the kick angles from their integer versions kickAngles[YAW] = kickAngles[ROLL] = 0; kickAngles[PITCH] = (float)pm->ps->kickPitch / 1000.0f; firing = qfalse; // Determine if firing or not if ( pm->ps->weaponstate == WEAPON_FIRING || pm->ps->weaponstate == WEAPON_FIRING_ALT) { firing = qtrue; } // If not firing then bring it down alot faster. if (!firing) { // return a whole lot faster if not firing VectorScale( kickAngles, 1.0f - (0.3f*((float)pml.msec/50.0f)), kickAngles ); for (i = 0; i < 3; i++) { if (kickAngles[i] >= 0 && kickAngles[i] < 0.05f) { kickAngles[i] = 0.0f; } else if (kickAngles[i] <= 0 && kickAngles[i] > -0.05f) { kickAngles[i] = 0.0f; } } } else { for (i = 0; i < 3; i++) { if (kickAngles[i] > 0) { if (kickAngles[i] < degreesToCorrect) { kickAngles[i] = 0; } else { kickAngles[i] -= degreesToCorrect; } } else if (kickAngles[i] < 0) { if (kickAngles[i] > -degreesToCorrect) { kickAngles[i] = 0; } else { kickAngles[i] += degreesToCorrect; } } } } // Throw the new kick angles into the integer versions pm->ps->kickPitch = (int)(kickAngles[PITCH] * 1000.0f); } /* ============== PM_Goggles Handles turning goggles on and off ============== */ static void PM_Goggles ( void ) { // ignore if not a normal player or dead or a ghost if ( pm->ps->pm_type != PM_NORMAL || pm->ps->stats[STAT_HEALTH] <= 0 || (pm->ps->pm_flags & PMF_GHOST) ) { return; } // See if they even have goggles if ( pm->ps->stats[STAT_GOGGLES] == GOGGLES_NONE ) { return; } // If the thermal goggles are on and the user has zoomed then turn them off if ( pm->ps->stats[STAT_GOGGLES] == GOGGLES_INFRARED ) { // If the player is zoomed then no goggles if ( pm->ps->pm_flags & PMF_ZOOMED ) { pm->ps->pm_flags &= ~PMF_GOGGLES_ON; return; } } // When goggles button isnt down there is nothing to do if ( !(pm->cmd.buttons & BUTTON_GOGGLES ) ) { pm->ps->pm_debounce &= ~PMD_GOGGLES; return; } // Dont do anything if the goggles button is being held down if ( pm->ps->pm_debounce & PMD_GOGGLES ) { return; } // toggle the goggles pm->ps->pm_debounce |= PMD_GOGGLES; pm->ps->pm_flags ^= PMF_GOGGLES_ON; // Play some noise PM_AddEventWithParm(EV_GOGGLES, (pm->ps->pm_flags&PMF_GOGGLES_ON) ? qtrue : qfalse ); } /* ============== PM_WeaponIdle Handles the progression of the looping idle animation ============== */ static void PM_WeaponIdle ( void ) { pm->ps->weaponAnimTime -= pml.msec; if ( pm->ps->weaponAnimTime <= 0 ) { pm->ps->weaponAnimTime = 0; if ( pm->ps->weaponTime <= 0 ) { PM_HandleWeaponAction(WACT_IDLE); pm->ps->weaponTime = 0; } } } /* ============== PM_Weapon Generates weapon events and modifes the weapon counter ============== */ static void PM_Weapon( void ) { int *ammoSource; int attackButtons; attackData_t *attackData; qboolean altFire; // Get modifed attack buttons. attackButtons = PM_GetAttackButtons(); // Gun goes away when using something if ( pm->ps->stats[STAT_USEWEAPONDROP] ) { return; } // don't allow attack until all buttons are up if ( pm->ps->pm_flags & PMF_RESPAWNED ) { return; } // ignore if not a normal player if ( pm->ps->pm_type != PM_NORMAL ) { return; } // check for dead player if ( pm->ps->stats[STAT_HEALTH] <= 0 ) { pm->ps->weapon = WP_NONE; return; } // Update the weapon inaccuracies and recoil PM_Weapon_UpdateInaccuracy(); PM_Weapon_UpdateKickAngles(); if( pm->ps->weaponTime > 0 ) { pm->ps->weaponTime-=pml.msec; } // See if we've hit a note? PM_CheckWeaponNotes(); // Check for weapon change. if( pm->ps->weaponstate == WEAPON_CHARGING || pm->ps->weaponstate == WEAPON_CHARGING_ALT ) { // Can't change if weapon is charging. // Update the grenade timer if ( pm->ps->grenadeTimer > 0 ) { pm->ps->grenadeTimer -= pml.msec; // Force it to go off if the timer has run out if ( pm->ps->grenadeTimer <= 0 ) { pm->ps->grenadeTimer = 1; pm->ps->weaponTime = 0; attackButtons &= ~(BUTTON_ATTACK|BUTTON_ALT_ATTACK); } } } else if ( pm->ps->weaponstate == WEAPON_SPAWNING ) { if ( pm->cmd.weapon != pm->ps->weapon ) { return; } pm->ps->weaponstate = WEAPON_READY; pm->ps->weaponTime = 0; } else if( pm->ps->weaponTime <= 0 || pm->ps->weaponstate < WEAPON_RELOADING ) { // Dont change weapons if this is a weapon selection if ( (pm->cmd.weapon & WP_DELAYED_CHANGE_BIT) && ((pm->cmd.buttons&BUTTON_ATTACK)||(pm->cmd.buttons&BUTTON_ALT_ATTACK)) ) { PM_BeginWeaponChange( pm->cmd.weapon & ~WP_DELAYED_CHANGE_BIT ); } else if ( !(pm->cmd.weapon & WP_DELAYED_CHANGE_BIT) && pm->ps->weapon != pm->cmd.weapon ) { PM_BeginWeaponChange( pm->cmd.weapon ); } } if ( pm->ps->weaponTime > 0 ) { // Handle the weapons idle animation PM_WeaponIdle ( ); return; } // Reload the alt clip immediately if( !pm->ps->clip[ATTACK_ALTERNATE][pm->ps->weapon] && pm->ps->ammo[weaponData[pm->ps->weapon].attack[ATTACK_ALTERNATE].ammoIndex] > 0) { switch(weaponData[pm->ps->weapon].attack[ATTACK_ALTERNATE].fireFromClip) { case 2: // Reload altclip. PM_StartRefillClip( ATTACK_ALTERNATE ); return; } } // Select firemode. if( attackButtons & BUTTON_FIREMODE ) { pm->ps->firemode[pm->ps->weapon] = BG_FindFireMode( pm->ps->weapon, ATTACK_NORMAL, pm->ps->firemode[pm->ps->weapon] + 1 ); } // Decrement burst fire counter if running. if(pm->ps->weaponFireBurstCount) { pm->ps->weaponFireBurstCount--; } // change weapon if time if ( pm->ps->weaponstate == WEAPON_DROPPING ) { PM_FinishWeaponChange(); return; } // Zoom in animation complete... now set zoom parms. if( pm->ps->weaponstate == WEAPON_ZOOMIN ) { // The zoomfov may still be remembered from a reload while zooming pm->ps->pm_flags |= PMF_ZOOMED; pm->ps->pm_flags |= PMF_ZOOM_LOCKED; pm->ps->pm_flags &= ~PMF_ZOOM_REZOOM; pm->ps->weaponstate=WEAPON_READY; return; } if( pm->ps->weaponstate==WEAPON_CHARGING || pm->ps->weaponstate==WEAPON_CHARGING_ALT ) { switch(pm->ps->weapon) { case WP_M84_GRENADE: case WP_SMOHG92_GRENADE: case WP_ANM14_GRENADE: case WP_M15_GRENADE: if(!(attackButtons&(BUTTON_ATTACK|BUTTON_ALT_ATTACK)) ) { if(pm->ps->weaponstate==WEAPON_CHARGING) { if ( pm->ps->grenadeTimer <= 1 ) { PM_AddEvent(EV_FIRE_WEAPON); pm->ps->weaponstate=WEAPON_FIRING; pm->ps->weaponTime = 250; } else { pm->ps->weaponstate=WEAPON_FIRING; PM_HandleWeaponAction(WACT_FIRE); PM_StartTorsoAnim( pm->ps, TORSO_ATTACK_GRENADE_END, pm->ps->weaponTime); } } else { if ( pm->ps->grenadeTimer <= 1 ) { PM_AddEvent(EV_ALT_FIRE); pm->ps->weaponstate=WEAPON_FIRING; pm->ps->weaponTime = 250; } else { pm->ps->weaponstate=WEAPON_FIRING_ALT; PM_HandleWeaponAction(WACT_ALTFIRE); PM_StartTorsoAnim( pm->ps, TORSO_ATTACK_GRENADE_END, pm->ps->weaponTime); } } return; } else { return; } break; default: break; } } // Special end of fire animation for some weapons. Knife currently the only one. if( pm->ps->weaponstate == WEAPON_FIRING ) { switch(pm->ps->weapon) { case WP_KNIFE: if(!(pm->cmd.buttons&BUTTON_ATTACK)) { PM_HandleWeaponAction(WACT_FIRE_END); pm->ps->weaponTime=0; pm->ps->weaponstate=WEAPON_READY; return; } break; default: break; } } // ************************************************************************ // Special reload behavior for warious weapons. // ************************************************************************ if((pm->ps->weaponstate==WEAPON_RELOADING)||(pm->ps->weaponstate==WEAPON_RELOADING_ALT)) { switch(pm->ps->weapon) { case WP_MM1_GRENADE_LAUNCHER: case WP_M590_SHOTGUN: // The M590 shotgun has a unique behavior in that when it is reloading, // the reload can be interrupted by firing. This cancels the reload of // course. if(attackButtons&(BUTTON_ATTACK|BUTTON_ALT_ATTACK)&&(pm->ps->clip[ATTACK_NORMAL][pm->ps->weapon]>0)) { // Allow normal fire operation and cancel reload. Note: in // the interests of gameplay, I allow us to go right to the // fire anim, although it doesn't look as smooth as going to // reload end and then fire. PM_HandleWeaponAction(WACT_RELOAD_END); PM_EndRefillClip(); PM_StartTorsoAnim ( pm->ps, weaponData[pm->ps->weapon].animReloadEnd, pm->ps->weaponTime ); return; } else if( ( pm->ps->clip[ATTACK_NORMAL][pm->ps->weapon] < weaponData[pm->ps->weapon].attack[ATTACK_NORMAL].clipSize) && ( pm->ps->ammo[weaponData[pm->ps->weapon].attack[ATTACK_NORMAL].ammoIndex]>0)) { // Load 1 more shell. pm->ps->clip[ATTACK_NORMAL][pm->ps->weapon]++; pm->ps->ammo[weaponData[pm->ps->weapon].attack[ATTACK_NORMAL].ammoIndex]--; PM_HandleWeaponAction(WACT_RELOAD); PM_StartTorsoAnim ( pm->ps, weaponData[pm->ps->weapon].animReload, pm->ps->weaponTime ); return; } else { // Weapon fully loaded so play end reload sequence. PM_HandleWeaponAction(WACT_RELOAD_END); PM_EndRefillClip(); PM_StartTorsoAnim ( pm->ps, weaponData[pm->ps->weapon].animReloadEnd, pm->ps->weaponTime ); return; } break; default: PM_EndRefillClip(); return; } } else if(pm->ps->pm_flags & PMF_ZOOM_DEFER_RELOAD ) { PM_StartRefillClip( ATTACK_NORMAL ); return; } else if( pm->ps->weapon==WP_KNIFE || (pm->ps->weapon>=WP_RPG7_LAUNCHER && pm->ps->weapon<=WP_M15_GRENADE) ) { if(pm->ps->clip[ATTACK_NORMAL][pm->ps->weapon]<1) { // Clip is now empty so see if we have enough ammo to reload this weapon? if (pm->ps->ammo[weaponData[pm->ps->weapon].attack[ATTACK_NORMAL].ammoIndex] > 0) { // Yes, so reload it. PM_StartRefillClip( ATTACK_NORMAL ); return; } else { if(pm->ps->weapon!=WP_RPG7_LAUNCHER) { // Clear grenade type from inventory. pm->ps->stats[STAT_WEAPONS]&=~(1<ps->weapon); // Out of ammo so switch weapons. PM_AddEventWithParm(EV_NOAMMO, pm->ps->weapon); return; } } } } // Handle zooming in/out for sniper rifle. if( weaponData[pm->ps->weapon].zoom[0].fov ) { if( (attackButtons&BUTTON_ALT_ATTACK) || (pm->ps->pm_flags & PMF_ZOOM_REZOOM) ) { if( pm->ps->pm_flags & PMF_ZOOMED ) { PM_BeginZoomOut(); } else { PM_BeginZoomIn(); } return; } else if( pm->ps->pm_flags & PMF_ZOOMED ) { if(pm->cmd.buttons&BUTTON_ZOOMIN) { if ( pm->ps->zoomFov + 1 < ZOOMLEVEL_MAX && weaponData[pm->ps->weapon].zoom[pm->ps->zoomFov+1].fov ) { pm->ps->zoomFov++; pm->ps->weaponTime=175; } return; } else if(pm->cmd.buttons&BUTTON_ZOOMOUT) { if ( pm->ps->zoomFov > 0 ) { pm->ps->zoomFov--; pm->ps->weaponTime=175; } return; } } } // Reload weapon? if ( attackButtons & BUTTON_RELOAD ) { if(pm->ps->clip[ATTACK_NORMAL][pm->ps->weapon] < weaponData[pm->ps->weapon].attack[ATTACK_NORMAL].clipSize) { // No, so see if we have enough ammo to reload this weapon? if (pm->ps->ammo[weaponData[pm->ps->weapon].attack[ATTACK_NORMAL].ammoIndex] > 0) { // Yes, so reload it. PM_StartRefillClip( ATTACK_NORMAL ); return; } } } // Start weapon when either frozen or not shooting if( pm->ps->stats[STAT_FROZEN] || !(attackButtons&(BUTTON_ATTACK|BUTTON_ALT_ATTACK)) ) { // Handle the weapons idle animation PM_WeaponIdle ( ); pm->ps->weaponstate = WEAPON_READY; return; } // Determine whether to use the alternate or normal attack info if ( attackButtons & BUTTON_ATTACK ) { altFire = qfalse; attackData = &weaponData[pm->ps->weapon].attack[ATTACK_NORMAL]; } else { altFire = qtrue; attackData = &weaponData[pm->ps->weapon].attack[ATTACK_ALTERNATE]; // Cant throw last knife if( pm->ps->weapon==WP_KNIFE && pm->ps->ammo[attackData->ammoIndex] < 1 ) { return; } } // Ammo taken from pool, clip or altclip? switch(attackData->fireFromClip) { case 0: ammoSource = &pm->ps->ammo[attackData->ammoIndex]; break; default: case 1: ammoSource = &pm->ps->clip[ATTACK_NORMAL][pm->ps->weapon]; break; case 2: ammoSource = &pm->ps->clip[ATTACK_ALTERNATE][pm->ps->weapon]; break; } // Is there enough ammo to fire? if ( (*ammoSource) - attackData->fireAmount < 0 ) { // No, so reload if there is more ammo if( pm->ps->ammo[ attackData->ammoIndex ] > 0 ) { // If auto reloading is enabled then reload the gun if ( pm->ps->pm_flags & PMF_AUTORELOAD ) { switch ( attackData->fireFromClip) { case 1: // Reload clip. PM_StartRefillClip( ATTACK_NORMAL ); return; case 2: // Reload altclip. PM_StartRefillClip( ATTACK_ALTERNATE ); return; } } } // Out of ammo, switch weapons if not an alt-attack else if ( !altFire ) { PM_AddEventWithParm (EV_NOAMMO, pm->ps->weapon); } // Handle the weapons idle animation PM_WeaponIdle ( ); pm->ps->weaponstate = WEAPON_READY; return; } // This attack doesnt exist if ( !attackData->damage ) { // Handle the weapons idle animation PM_WeaponIdle ( ); pm->ps->weaponstate = WEAPON_READY; return; } pm->ps->pm_debounce |= PMD_ATTACK; // Decrease the ammo (*ammoSource) -= attackData->fireAmount; // Handle charging cases switch(pm->ps->weapon) { case WP_KNIFE: if ( altFire ) { if( pm->ps->weaponstate != WEAPON_FIRING && pm->ps->weaponstate != WEAPON_FIRING_ALT ) { PM_HandleWeaponAction(WACT_ALTFIRE); pm->ps->weaponstate=WEAPON_FIRING_ALT; } } else { PM_HandleWeaponAction ( WACT_FIRE ); pm->ps->weaponstate = WEAPON_FIRING; } // Play the torso animation associated with the attack PM_StartTorsoAnim ( pm->ps, attackData->animFire, pm->ps->weaponAnimTime ); break; case WP_M84_GRENADE: case WP_SMOHG92_GRENADE: case WP_ANM14_GRENADE: case WP_M15_GRENADE: // Start the detonation timer on the grenade going. pm->ps->grenadeTimer = attackData->projectileLifetime; if ( altFire ) { PM_HandleWeaponAction ( WACT_ALTCHARGE ); pm->ps->weaponstate = WEAPON_CHARGING_ALT; } else { PM_HandleWeaponAction ( WACT_CHARGE ); pm->ps->weaponstate = WEAPON_CHARGING; } PM_StartTorsoAnim( pm->ps, TORSO_ATTACK_GRENADE_START, pm->ps->weaponTime); break; default: if ( altFire ) { PM_HandleWeaponAction(WACT_ALTFIRE); pm->ps->weaponstate=WEAPON_FIRING_ALT; } else { PM_HandleWeaponAction(WACT_FIRE); pm->ps->weaponstate=WEAPON_FIRING; } pm->ps->weaponTime += attackData->fireDelay; // Play the torso animation associated with the attack if ( pm->ps->pm_flags & PMF_ZOOMED ) { PM_StartTorsoAnim ( pm->ps, attackData->animFireZoomed, pm->ps->weaponTime ); } else { PM_StartTorsoAnim ( pm->ps, attackData->animFire, pm->ps->weaponTime ); } break; } } /* ================ PM_DropTimers ================ */ static void PM_DropTimers( void ) { // drop misc timing counter if ( pm->ps->pm_time ) { if ( pml.msec >= pm->ps->pm_time ) { pm->ps->pm_flags &= ~PMF_ALL_TIMES; pm->ps->pm_time = 0; } else { pm->ps->pm_time -= pml.msec; } } } /* ============== PM_CheckLean ============== */ static void PM_CheckLean( void ) { trace_t trace; qboolean canlean; float leanTime; if ( !pm || !pm->ps ) { return; } // No leaning as a spectator or a ghost if ( (pm->ps->pm_flags & PMF_GHOST) || pm->ps->pm_type == PM_SPECTATOR ) { pm->ps->leanTime = LEAN_TIME; pm->ps->pm_flags &= ~PMF_LEANING; return; } leanTime = (float)pm->ps->leanTime - LEAN_TIME; canlean = qfalse; // If their lean button is being pressed and they are on the ground then perform the lean if( (pm->cmd.buttons & (BUTTON_LEAN_RIGHT|BUTTON_LEAN_LEFT)) && (pm->ps->groundEntityNum != ENTITYNUM_NONE) ) { vec3_t start, end, right, mins, maxs; int leanDir; if( pm->cmd.buttons & BUTTON_LEAN_RIGHT ) { leanDir = 1; } else { leanDir = -1; } // check for collision VectorCopy( pm->ps->origin, start ); AngleVectors( pm->ps->viewangles, NULL, right, NULL ); VectorSet( mins, -6, -6, -20 ); VectorSet( maxs, 6, 6, 20 ); // since we're moving the camera over // check that move VectorMA( start, leanDir * LEAN_OFFSET * 1.25f, right, end ); pm->trace(&trace, start, mins, maxs, end, pm->ps->clientNum, pm->tracemask ); if ( trace.fraction < 0 || trace.fraction >= 1.0f ) { leanTime += (leanDir * pml.msec); if( leanTime > LEAN_TIME ) { leanTime = LEAN_TIME; } else if( leanTime < -LEAN_TIME ) { leanTime = -LEAN_TIME; } canlean = qtrue; } else if ( (pm->ps->pm_flags&PMF_LEANING) && trace.fraction < 1.0f ) { int templeanTime = (float)leanDir * (float)LEAN_TIME * trace.fraction; if ( fabs(templeanTime) < fabs(leanTime) ) { leanTime = templeanTime; } } } if ( !canlean ) { if( leanTime > 0 ) { leanTime -= pml.msec; if( leanTime < 0 ) { leanTime = 0; } } else if ( leanTime < 0 ) { leanTime += pml.msec; if( leanTime > 0 ) { leanTime = 0; } } } // Set a pm flag for leaning for convienience if ( leanTime != 0 ) { pm->ps->pm_flags |= PMF_LEANING; } else { pm->ps->pm_flags &= ~PMF_LEANING; } // The lean time is kept positive by adding in the base lean time pm->ps->leanTime = (int) (leanTime + LEAN_TIME); } /* ================ PM_UpdateViewAngles This can be used as another entry point when only the viewangles are being updated isntead of a full move ================ */ void PM_UpdateViewAngles( playerState_t *ps, const usercmd_t *cmd ) { short temp; int i; vec3_t kickAngles; if ( ps->pm_type == PM_INTERMISSION) { return; // no view changes at all } if ( ps->pm_type != PM_SPECTATOR && ps->stats[STAT_HEALTH] <= 0 ) { return; // no view changes at all } // Extract the kcik angles kickAngles[PITCH] = ((float)ps->kickPitch / 1000.0f); kickAngles[YAW] = kickAngles[ROLL] = 0; // circularly clamp the angles with deltas for (i=0 ; i<3 ; i++) { temp = cmd->angles[i] + ps->delta_angles[i] - ANGLE2SHORT(kickAngles[i]); if ( i == PITCH ) { // don't let the player look up or down more than 90 degrees if ( temp > 16000 ) { ps->delta_angles[i] = 16000 - (cmd->angles[i]- ANGLE2SHORT(kickAngles[i])); temp = 16000; } else if ( temp < -16000 ) { ps->delta_angles[i] = -16000 - (cmd->angles[i] - ANGLE2SHORT(kickAngles[i])); temp = -16000; } } ps->viewangles[i] = SHORT2ANGLE(temp); } PM_CheckLean ( ); } /* ================ PM_AdjustAttackStates ================ */ void PM_AdjustAttackStates( pmove_t *pm ) { int ammoOk; // Check ammo after usage... if(pm->cmd.buttons & BUTTON_ALT_ATTACK) { ammoOk = pm->ps->ammo[weaponData[pm->ps->weapon].attack[ATTACK_ALTERNATE].ammoIndex] - weaponData[pm->ps->weapon].attack[ATTACK_ALTERNATE].fireAmount; } else { ammoOk = pm->ps->clip[ATTACK_NORMAL][pm->ps->weapon ] - weaponData[pm->ps->weapon].attack[ATTACK_NORMAL].fireAmount; } // Set the firing flag. if(!(pm->ps->pm_flags & PMF_RESPAWNED) && (pm->ps->pm_type!=PM_INTERMISSION) && (ammoOk>=0)) { if((pm->cmd.buttons & BUTTON_ALT_ATTACK)||(pm->ps->weaponstate==WEAPON_FIRING_ALT)) { pm->ps->eFlags |= EF_ALT_FIRING; } else if((pm->cmd.buttons & BUTTON_ATTACK)||(pm->ps->weaponstate==WEAPON_FIRING)) { pm->ps->eFlags &= ~EF_ALT_FIRING; } // This flag should always get set, even when alt-firing pm->ps->eFlags |= EF_FIRING; } else { // Clear 'em out pm->ps->eFlags &= ~(EF_FIRING|EF_ALT_FIRING); } } /* ================ PmoveSingle ================ */ void trap_SnapVector( float *v ); void PmoveSingle (pmove_t *pmove) { pm = pmove; // this counter lets us debug movement problems with a journal // by setting a conditional breakpoint fot the previous frame c_pmove++; // clear results pm->numtouch = 0; pm->watertype = 0; pm->waterlevel = 0; if ( pm->ps->stats[STAT_HEALTH] <= 0 ) { pm->tracemask &= ~CONTENTS_BODY; // corpses can fly through bodies } // make sure walking button is clear if they are running, to avoid // proxy no-footsteps cheats if ( abs( pm->cmd.forwardmove ) > 64 || abs( pm->cmd.rightmove ) > 64 ) { pm->cmd.buttons &= ~BUTTON_WALKING; } // When the lean modifier button is held the strafe left and right keys // will act as lean left and right if ( pm->cmd.buttons & BUTTON_LEAN ) { pm->cmd.buttons &= ~(BUTTON_LEAN_LEFT|BUTTON_LEAN_RIGHT); // Strafe left = lean left if ( pm->cmd.rightmove < 0 ) { pm->cmd.buttons |= BUTTON_LEAN_LEFT; } // Strafe right = lean right else if ( pm->cmd.rightmove > 0 ) { pm->cmd.buttons |= BUTTON_LEAN_RIGHT; } // NO strafing with lean button down pm->cmd.rightmove = 0; } // Cant move when leaning if ( (pm->cmd.buttons & (BUTTON_LEAN_LEFT|BUTTON_LEAN_RIGHT))) { // pm->cmd.rightmove = 0; pm->cmd.forwardmove = 0; // Cant jump when leaning if ( pm->cmd.upmove > 0 ) { pm->cmd.upmove = 0; } } // Cant run when zoomed, leaning, or using something that takes time if ( (pm->ps->pm_flags&PMF_ZOOMED) || (pm->ps->weaponstate == WEAPON_ZOOMIN) || (pm->cmd.buttons & (BUTTON_LEAN_LEFT|BUTTON_LEAN_RIGHT)) || (pm->ps->stats[STAT_USEWEAPONDROP]) ) { if ( pm->cmd.forwardmove > 64 ) { pm->cmd.forwardmove = 64; } else if ( pm->cmd.forwardmove < -64 ) { pm->cmd.forwardmove = -64; } if ( pm->cmd.rightmove > 64 ) { pm->cmd.rightmove = 64; } else if ( pm->cmd.rightmove < -64 ) { pm->cmd.rightmove = -64; } pm->cmd.buttons |= BUTTON_WALKING; } // set the talk balloon flag if ( pm->cmd.buttons & BUTTON_TALK ) { pm->ps->eFlags |= EF_TALK; } else { pm->ps->eFlags &= ~EF_TALK; } // In certain situations, we may want to control which attack buttons are pressed and what kind of functionality // is attached to them PM_AdjustAttackStates( pm ); // clear the respawned flag if attack and use are cleared if ( pm->ps->stats[STAT_HEALTH] > 0 && !( pm->cmd.buttons & BUTTON_ATTACK) ) { pm->ps->pm_flags &= ~PMF_RESPAWNED; } // if talk button is down, dissallow all other input // this is to prevent any possible intercept proxy from // adding fake talk balloons if ( pmove->cmd.buttons & BUTTON_TALK ) { // keep the talk button set tho for when the cmd.serverTime > 66 msec // and the same cmd is used multiple times in Pmove pmove->cmd.buttons = BUTTON_TALK; pmove->cmd.forwardmove = 0; pmove->cmd.rightmove = 0; pmove->cmd.upmove = 0; } // clear all pmove local vars memset (&pml, 0, sizeof(pml)); // determine the time pml.msec = pmove->cmd.serverTime - pm->ps->commandTime; if ( pml.msec < 1 ) { pml.msec = 1; } else if ( pml.msec > 200 ) { pml.msec = 200; } pm->ps->commandTime = pmove->cmd.serverTime; // Frozen? if ( pm->ps->stats[STAT_FROZEN] ) { pm->ps->stats[STAT_FROZEN] -= pml.msec; if ( pm->ps->stats[STAT_FROZEN] < 0 ) { pm->ps->stats[STAT_FROZEN] = 0; } else { // pm->cmd.buttons = pm->cmd.buttons & (BUTTON_RELOAD|BUTTON_ATTACK|BUTTON_); pm->cmd.forwardmove = 0; pm->cmd.rightmove = 0; pm->cmd.upmove = 0; } } // save old org in case we get stuck VectorCopy (pm->ps->origin, pml.previous_origin); // save old velocity for crashlanding VectorCopy (pm->ps->velocity, pml.previous_velocity); pml.frametime = pml.msec * 0.001; // update the viewangles PM_UpdateViewAngles( pm->ps, &pm->cmd ); AngleVectors (pm->ps->viewangles, pml.forward, pml.right, pml.up); if ( pm->cmd.upmove < 10 ) { // not holding jump pm->ps->pm_debounce &= ~PMD_JUMP; } // decide if backpedaling animations should be used if ( pm->cmd.forwardmove < 0 ) { pm->ps->pm_flags |= PMF_BACKWARDS_RUN; } else if ( pm->cmd.forwardmove > 0 || ( pm->cmd.forwardmove == 0 && pm->cmd.rightmove ) ) { pm->ps->pm_flags &= ~PMF_BACKWARDS_RUN; } if ( pm->ps->pm_type >= PM_DEAD ) { pm->cmd.forwardmove = 0; pm->cmd.rightmove = 0; pm->cmd.upmove = 0; } if ( pm->ps->pm_type == PM_SPECTATOR ) { pm->mins[0] = -15; pm->mins[1] = -15; pm->maxs[0] = 15; pm->maxs[1] = 15; pm->mins[2] = MINS_Z; pm->maxs[2] = DEFAULT_PLAYER_Z_MAX; pm->ps->viewheight = DEFAULT_VIEWHEIGHT; // PM_FlyMove (); PM_NoclipMove ( ); PM_DropTimers (); return; } if ( pm->ps->pm_type == PM_NOCLIP ) { PM_NoclipMove (); PM_DropTimers (); return; } if (pm->ps->pm_type == PM_FREEZE) { return; // no movement at all } if ( pm->ps->pm_type == PM_INTERMISSION ) { return; // no movement at all } // set mins, maxs, and viewheight PM_CheckDuck (); // set groundentity PM_GroundTrace(); // set watertype, and waterlevel PM_SetWaterLevel(); pml.previous_waterlevel = pmove->waterlevel; PM_CheckCrouchJump ( ); if ( pm->ps->pm_type == PM_DEAD ) { PM_DeadMove (); } PM_DropTimers(); if ( pm->ps->pm_flags & PMF_LADDER ) { PM_LadderMove ( ); } else if (pm->ps->pm_flags & PMF_TIME_WATERJUMP) { PM_WaterJumpMove(); } else if ( pm->waterlevel > 1 ) { // swimming PM_WaterMove(); } else if ( pml.walking ) { // walking on ground PM_WalkMove(); } else { // airborne PM_AirMove(); } // set groundentity, watertype, and waterlevel if(!(VectorCompare(pm->ps->origin,pml.previous_origin))) { // If we didn't move at all, then why bother doing this again -MW. PM_GroundTrace(); } PM_SetWaterLevel(); // turn goggles on/off PM_Goggles ( ); // weapons PM_Weapon(); // Use PM_Use ( ); // torso animation PM_TorsoAnimation( pm->ps ); // footstep events / legs animations PM_Footsteps(); // entering / leaving water splashes PM_WaterEvents(); // snap some parts of playerstate to save network bandwidth trap_SnapVector( pm->ps->velocity ); } /* ================ PM_UpdatePVSOrigin The pvs of the client is calculated using its own origin and this function ensures that the origin is set correctly. The main reason for having a PVS origin is that when leaning your can poke around corners which will in turn change what you can see. ================ */ void PM_UpdatePVSOrigin ( pmove_t *pmove ) { pm = pmove; // Set a pm flag for leaning and calculate the view origin for the lean if ( pm->ps->leanTime - LEAN_TIME != 0 ) { VectorCopy ( pm->ps->origin, pm->ps->pvsOrigin ); BG_ApplyLeanOffset ( pm->ps, pm->ps->pvsOrigin ); } else { VectorCopy ( pm->ps->origin, pm->ps->pvsOrigin ); } } /* ================ Pmove Can be called by either the server or the client ================ */ void Pmove (pmove_t *pmove) { int finalTime; finalTime = pmove->cmd.serverTime; if ( finalTime < pmove->ps->commandTime ) { return; // should not happen } if ( finalTime > pmove->ps->commandTime + 1000 ) { pmove->ps->commandTime = finalTime - 1000; } pmove->ps->pmove_framecount = (pmove->ps->pmove_framecount+1) & ((1<ps->commandTime != finalTime ) { int msec; msec = finalTime - pmove->ps->commandTime; if ( pmove->pmove_fixed ) { if ( msec > pmove->pmove_msec ) { msec = pmove->pmove_msec; } } else { if ( msec > 66 ) { msec = 66; } } pmove->cmd.serverTime = pmove->ps->commandTime + msec; PmoveSingle( pmove ); PM_UpdatePVSOrigin ( pmove ); if ( pmove->ps->pm_debounce & PMD_JUMP ) { pmove->cmd.upmove = 20; } } } /* ================ BG_AddLadder Adds a ladder to the ladder list ================ */ void BG_AddLadder ( vec3_t absmin, vec3_t absmax, vec3_t fwd ) { pm_ladders[pm_laddercount].origin[0] = (absmax[0] + absmin[0]) / 2; pm_ladders[pm_laddercount].origin[1] = (absmax[1] + absmin[1]) / 2; pm_ladders[pm_laddercount].origin[2] = (absmax[2] + absmin[2]) / 2; VectorCopy ( fwd, pm_ladders[pm_laddercount].fwd ); pm_laddercount++; } /* ================ BG_FindLadder Searches through the ladder list and finds the closes to the given origin ================ */ int BG_FindLadder ( vec3_t pos ) { int ladder; int result; float dist; dist = 999999.0f; result = -1; for ( ladder = 0; ladder < pm_laddercount; ladder ++ ) { float dist2 = DistanceSquared( pos, pm_ladders[ladder].origin ); if ( dist2 < dist ) { vec3_t diff; VectorSubtract ( pm_ladders[ladder].origin, pos, diff ); diff[2] = 0; if ( VectorLengthSquared ( diff ) < 500 * 500 ) { dist = dist2; result = ladder; } } } return result; }