/* =========================================================================== Copyright (C) 1999-2005 Id Software, Inc. This file is part of Quake III Arena source code. Quake III Arena source code is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Quake III Arena source code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Foobar; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =========================================================================== */ // // bg_pmove.c -- both games player movement code // takes a playerstate and a usercmd as input and returns a modifed playerstate // // Reconstructed from Quake Live binary — replaces the Q3TA pmove. #include "q_shared.h" #include "bg_public.h" #include "bg_local.h" #include pmove_t *pm; pml_t pml; // movement parameters float pm_stopspeed = 100.0f; float pm_duckScale = 0.25f; float pm_swimScale = 0.50f; float pm_wadeScale = 0.70f; float pm_accelerate = 10.0f; float pm_airaccelerate = 1.0f; float pm_wateraccelerate = 4.0f; float pm_flyaccelerate = 8.0f; float pm_friction = 6.0f; float pm_waterfriction = 1.0f; float pm_flightfriction = 3.0f; float pm_spectatorfriction = 5.0f; int c_pmove = 0; // QL cached cvar globals float pm_jumpVelocity = 270.0f; float pm_jumpVelocityMax = 400.0f; float pm_jumpVelocityScaleAdd = 0.5f; float pm_jumpVelocityTimeThreshold = 400.0f; float pm_jumpVelocityTimeThresholdOffset = 0.75f; float pm_jumpTimeDeltaMin = 0.0f; int pm_chainJump = 1; float pm_chainJumpVelocity = 50.0f; float pm_stepJumpVelocity = 50.0f; float pm_rampJumpScale = 0.5f; float pm_stepHeight = 18.0f; float pm_walkAccel = 10.0f; float pm_walkFriction = 6.0f; float pm_strafeAccel = 1.0f; float pm_circleStrafeFriction = 6.0f; float pm_crouchSlideFriction = 1.0f; int pm_crouchSlideTime = 1000; float pm_waterSwimScale = 0.50f; float pm_waterWadeScale = 0.70f; int pm_weaponDropTime = 200; int pm_weaponRaiseTime = 250; float pm_wishSpeed = 400.0f; int pm_airSteps = 0; float pm_airStepFriction = 1.0f; float pm_airAccel = 1.0f; float pm_airStopAccel = 1.0f; float pm_airControl = 0.0f; int pm_autoHopEnabled = 0; int pm_bunnyHop = 0; /* DAT_001ab5c4 related */ int pm_stepJumpEnabled = 0; int pm_crouchStepJump = 0; int pm_rampJumpEnabled = 0; int pm_noPlayerClip = 0; float pm_velocityGh = 0.0f; // QL promode locals static float pm_airstopaccelerate = 1.0f; static float pmove_crouchSlideVal = 0.0f; static int pm_bunnyHopEnabled = 0; // QL extern flags int pmove_globalFlag = 0; int pmove_rampJumpFlag = 0; int pmove_stepJumpFlag = 0; float _DAT_003cfe90 = 0.0f; // step-up velocity damping factor int DAT_003cfe30 = 0; // edge grab enabled // Forward declarations for functions in this file static void PM_StartTorsoAnim( int anim ); static void PM_StartLegsAnim( int anim ); /* =============== PM_AddEvent =============== */ void PM_AddEvent( int newEvent ) { BG_AddPredictableEventToPlayerstate( newEvent, 0, pm->ps ); } /* =============== PM_AddTouchEnt =============== */ void PM_AddTouchEnt( int entityNum ) { int i; if ( entityNum == ENTITYNUM_WORLD ) { return; } 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_StartTorsoAnim =================== */ static void PM_StartTorsoAnim( int anim ) { if ( pm->ps->pm_type >= PM_DEAD ) { return; } pm->ps->torsoAnim = ( ( pm->ps->torsoAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) | anim; } /* =================== PM_StartLegsAnim =================== */ static void PM_StartLegsAnim( int anim ) { if ( pm->ps->pm_type >= PM_DEAD ) { return; } if ( pm->ps->legsTimer > 0 ) { return; // a high priority animation is running } pm->ps->legsAnim = ( ( pm->ps->legsAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) | anim; } /* =================== PM_ContinueLegsAnim =================== */ void PM_ContinueLegsAnim( int anim ) { playerState_t *ps; ps = pm->ps; if ( ( ps->legsAnim & ~ANIM_TOGGLEBIT ) != anim && ps->legsTimer < 1 && ps->pm_type < PM_DEAD ) { ps->legsAnim = ( ~ps->legsAnim & ANIM_TOGGLEBIT ) | anim; } } static void PM_ContinueTorsoAnim( int anim ) { if ( ( pm->ps->torsoAnim & ~ANIM_TOGGLEBIT ) == anim ) { return; } if ( pm->ps->torsoTimer > 0 ) { return; } PM_StartTorsoAnim( anim ); } /* =================== PM_ForceLegsAnim =================== */ void PM_ForceLegsAnim( int anim ) { playerState_t *ps; pm->ps->legsTimer = 0; ps = pm->ps; if ( ps->pm_type < PM_DEAD && ps->legsTimer < 1 ) { ps->legsAnim = ( ~ps->legsAnim & ANIM_TOGGLEBIT ) | anim; } } /* ================== PM_ClipVelocity Slide off of the impacting surface QL version: no overbounce parameter, uses OVERCLIP constant internally ================== */ void PM_ClipVelocity( vec3_t in, vec3_t normal, vec3_t out, float overbounce ) { float backoff; backoff = DotProduct( in, normal ); if ( backoff < 0.0f ) { backoff *= overbounce; } else { backoff /= overbounce; } // negate backoff (Ghidra: xor with sign bit) backoff = -backoff; out[0] = normal[0] * backoff + in[0]; out[1] = normal[1] * backoff + in[1]; out[2] = backoff * normal[2] + in[2]; } /* =============== PM_SetupPhysicsOverrides Sets up physics parameters based on PMF_PROMODE flag =============== */ void PM_SetupPhysicsOverrides( void ) { if ( ( pm->ps->pm_flags & PMF_PROMODE ) == 0 ) { pmove_crouchSlideVal = 0.0f; pm_airaccelerate = 1.0f; pm_airstopaccelerate = 1.0f; pm_chainJump = 1; pm_circleStrafeFriction = 6.0f; pm_strafeAccel = 1.0f; pm_walkAccel = 10.0f; pm_wishSpeed = 400.0f; return; } pmove_crouchSlideVal = 150.0f; pm_airaccelerate = 1.1f; pm_airstopaccelerate = 2.5f; pm_chainJump = 3; pm_circleStrafeFriction = 5.5f; pm_strafeAccel = 70.0f; pm_walkAccel = 15.0f; pm_wishSpeed = 35.0f; } /* ============== PM_CheckDuck Sets mins, maxs, and pm->ps->viewheight QL version: adds crouch slide support ============== */ void PM_CheckDuck( void ) { trace_t tr; playerState_t *ps; int pm_flags; ps = pm->ps; if ( ps->powerups[PW_INVULNERABILITY] != 0 ) { if ( ( ps->pm_flags & PMF_INVUL_EXPAND ) == 0 ) { pm->mins[0] = -15.0f; pm->mins[1] = -15.0f; pm->mins[2] = MINS_Z; pm->maxs[0] = 15.0f; pm->maxs[1] = 15.0f; pm->maxs[2] = 16.0f; } else { pm->mins[0] = -42.0f; pm->mins[1] = -42.0f; pm->mins[2] = -42.0f; pm->maxs[0] = 42.0f; pm->maxs[1] = 42.0f; pm->maxs[2] = 42.0f; } ps->pm_flags |= PMF_DUCKED; pm->ps->viewheight = CROUCH_VIEWHEIGHT; return; } ps->pm_flags &= ~PMF_INVUL_EXPAND; pm->mins[0] = -15.0f; pm->mins[1] = -15.0f; pm->mins[2] = MINS_Z; pm->maxs[0] = 15.0f; pm->maxs[1] = 15.0f; pm->maxs[2] = 32.0f; ps = pm->ps; if ( ps->pm_type == PM_DEAD ) { pm->maxs[2] = -8.0f; ps->viewheight = DEAD_VIEWHEIGHT; return; } if ( pm->cmd.upmove < 0 ) { ps->pm_flags |= PMF_DUCKED; ps = pm->ps; if ( ps->field_1d4 == 0 ) { ps->field_1d4 = pm->cmd.serverTime; ps = pm->ps; } pm_flags = ps->pm_flags; } else { if ( ( ps->pm_flags & PMF_DUCKED ) == 0 ) { goto skip; } pm->trace( &tr, ps->origin, pm->mins, pm->maxs, ps->origin, ps->clientNum, pm->tracemask ); if ( tr.allsolid == 0 ) { pm->ps->pm_flags &= ~PMF_DUCKED; } ps = pm->ps; pm_flags = ps->pm_flags; } if ( ( pm_flags & PMF_DUCKED ) != 0 ) { if ( ps->crouchSlideTimer == 0 && ( pm_flags & PMF_CROUCH_SLIDE ) != 0 && (float)ps->speed < 400.0f ) { /* DAT_001a8988 = 400.0f */ ps->speed = 400; ps = pm->ps; } pm->maxs[2] = 16.0f; ps->viewheight = CROUCH_VIEWHEIGHT; return; } skip: ps->viewheight = DEFAULT_VIEWHEIGHT; ps = pm->ps; if ( ( ps->pm_flags & PMF_CROUCH_SLIDE ) != 0 && ps->crouchSlideTimer != 0 && pml.groundPlane != 0 ) { ps->crouchSlideTimer = 0; } } /* ================ PM_DropTimers ================ */ void PM_DropTimers( void ) { playerState_t *ps; int pm_time; ps = pm->ps; pm_time = ps->pm_time; if ( pm_time != 0 ) { if ( pml.msec < pm_time ) { ps->pm_time = pm_time - pml.msec; } else { ps->pm_flags &= ~PMF_ALL_TIMES; pm->ps->pm_time = 0; } ps = pm->ps; } if ( ps->legsTimer > 0 ) { ps->legsTimer -= pml.msec; ps = pm->ps; if ( ps->legsTimer < 0 ) { ps->legsTimer = 0; ps = pm->ps; } } if ( ps->torsoTimer > 0 ) { ps->torsoTimer -= pml.msec; ps = pm->ps; if ( ps->torsoTimer < 0 ) { ps->torsoTimer = 0; ps = pm->ps; } } // crouch slide timer if ( ( ps->pm_flags & PMF_CROUCH_SLIDE ) != 0 && pml.groundPlane != 0 && ps->crouchSlideTimer != 0 ) { if ( ps->crouchSlideTimer <= pml.msec ) { ps->crouchSlideTimer = 0; } else { ps->crouchSlideTimer = ps->crouchSlideTimer - pml.msec; } } } /* ============== PM_Accelerate QL version: includes bunny hop forward boost logic ============== */ void PM_Accelerate( float wishspeed, float accel, vec3_t wishdir ) { float addspeed, accelspeed, currentspeed; float airControlScale; float speed, doubleSpeed; playerState_t *ps; ps = pm->ps; currentspeed = DotProduct( ps->velocity, wishdir ); addspeed = wishspeed - currentspeed; speed = sqrt( ps->velocity[0] * ps->velocity[0] + ps->velocity[1] * ps->velocity[1] ); if ( addspeed <= 0.0f ) { // bunny hop: forward boost when moving forward (movementDir == 0) if ( pm_bunnyHopEnabled != 0 && ps->movementDir == 0 ) { float playerSpeed = (float)ps->speed; doubleSpeed = playerSpeed + playerSpeed; if ( speed <= doubleSpeed ) { airControlScale = pm_velocityGh; /* DAT_001ab5c4 */ if ( playerSpeed < speed ) { if ( doubleSpeed <= speed ) { speed = doubleSpeed; } airControlScale = ( ( doubleSpeed - speed ) / playerSpeed ) * pm_velocityGh; } ps->velocity[0] += wishdir[0] * airControlScale; pm->ps->velocity[1] += wishdir[1] * airControlScale; pm->ps->velocity[2] += wishdir[2] * airControlScale; } } return; } accelspeed = wishspeed * pml.frametime * accel; if ( addspeed <= accelspeed ) { accelspeed = addspeed; } ps->velocity[0] += wishdir[0] * accelspeed; pm->ps->velocity[1] += wishdir[1] * accelspeed; pm->ps->velocity[2] += wishdir[2] * accelspeed; } /* ============ PM_CmdScale Returns the scale factor to apply to cmd movements QL version: takes forwardmove, rightmove, upmove as separate params ============ */ void PM_CmdScale( usercmd_t *cmd, float *scale ) { int max; float total; int fm, rm, um; fm = abs( cmd->forwardmove ); rm = abs( cmd->rightmove ); um = abs( cmd->upmove ); max = fm; if ( rm > max ) { max = rm; } if ( um > max ) { max = um; } if ( max == 0 ) { *scale = 0.0f; return; } total = sqrt( (float)( fm * fm + rm * rm + um * um ) ); *scale = (float)pm->ps->speed * (float)max / ( 127.0f * total ); } // Internal helper: returns scale as float (matches Ghidra's undefined8 return) static float PM_CmdScaleValue( signed char forwardmove, signed char rightmove, signed char upmove ) { int max; float total; int fm, rm, um; fm = abs( forwardmove ); rm = abs( rightmove ); um = abs( upmove ); max = fm; if ( rm > max ) { max = rm; } if ( um > max ) { max = um; } if ( max == 0 ) { return 0.0f; } total = sqrt( (float)( fm * fm + rm * rm + um * um ) ); return (float)pm->ps->speed * (float)max / ( 127.0f * total ); } /* ============== PM_BuildWishVelocity Builds the wish velocity vector from the command Returns 1 if the player is moving, 0 if not ============== */ int PM_BuildWishVelocity( usercmd_t *cmd, float *wishspeed, vec3_t wishdir ) { int i; float scale; float wishspeedNorm; signed char fmove, smove; scale = PM_CmdScaleValue( cmd->forwardmove, cmd->rightmove, cmd->upmove ); fmove = pm->cmd.forwardmove; smove = pm->cmd.rightmove; if ( scale != 0.0f ) { for ( i = 0; i < 3; i++ ) { wishdir[i] = pml.forward[i] * (float)(int)fmove + pml.right[i] * (float)(int)smove; } wishdir[2] += (float)(int)pm->cmd.upmove; wishspeedNorm = VectorNormalize( wishdir ); *wishspeed = wishspeedNorm * scale; return 1; } wishdir[0] = 0.0f; wishdir[1] = 0.0f; wishdir[2] = 0.0f; *wishspeed = 0.0f; return 0; } /* ================ PM_SetMovementDir Determine the rotation of the legs relative to the facing dir ================ */ void PM_SetMovementDir( void ) { signed char fmove, rmove; fmove = pm->cmd.forwardmove; rmove = pm->cmd.rightmove; if ( fmove == 0 && rmove == 0 ) { if ( pm->ps->movementDir == 2 ) { pm->ps->movementDir = 1; } else if ( pm->ps->movementDir == 6 ) { pm->ps->movementDir = 7; } return; } if ( fmove == 0 ) { if ( rmove > 0 ) { pm->ps->movementDir = 6; } else { pm->ps->movementDir = 2; } return; } if ( rmove == 0 ) { if ( fmove > 0 ) { pm->ps->movementDir = 0; } else { pm->ps->movementDir = 4; } return; } if ( rmove > 0 ) { if ( fmove < 0 ) { pm->ps->movementDir = 5; } else if ( fmove > 0 ) { pm->ps->movementDir = 7; } else { pm->ps->movementDir = 6; } } else { if ( fmove > 0 ) { pm->ps->movementDir = 1; } else if ( fmove < 0 ) { pm->ps->movementDir = 3; } else { pm->ps->movementDir = 2; } } } /* ================== PM_Friction Handles ground, water, air, and wall contact friction QL version: adds promode circle strafe friction, crouch slide friction, wall contact friction, and flight/freeze friction ================== */ void PM_Friction( void ) { float speed, newspeed, drop, control; float vel_z_sq; playerState_t *ps; int waterlvl; int pm_flags; ps = pm->ps; vel_z_sq = 0.0f; if ( pml.walking == 0 ) { vel_z_sq = ps->velocity[2] * ps->velocity[2]; } speed = sqrt( ps->velocity[0] * ps->velocity[0] + ps->velocity[1] * ps->velocity[1] + vel_z_sq ); if ( speed < 1.0f ) { ps->velocity[0] = 0.0f; pm->ps->velocity[1] = 0.0f; return; } waterlvl = pm->waterlevel; drop = 0.0f; if ( waterlvl < 2 ) { if ( pml.walking == 0 || ( pml.groundTrace.surfaceFlags & SURF_SLICK ) || ( ps->pm_flags & PMF_TIME_WATERJUMP ) ) { drop = 0.0f; } else { control = pm_stopspeed; if ( pm_stopspeed <= speed ) { control = speed; } pm_flags = ps->pm_flags; if ( ( pm_flags & PMF_CROUCH_SLIDE ) != 0 && pm->cmd.upmove < 0 && ps->crouchSlideTimer > 0 ) { // crouch slide friction drop = control * pml.frametime * pm_crouchSlideFriction; } else if ( ( pm_flags & PMF_PROMODE ) != 0 && pm->cmd.forwardmove != 0 && pm->cmd.rightmove != 0 && ( ps->movementDir == 1 || ps->movementDir == 5 ) ) { // promode circle strafe friction drop = control * pml.frametime * pm_circleStrafeFriction; } else { // normal walk friction drop = control * pml.frametime * pm_walkFriction; } } // water friction when waterlevel == 1 if ( waterlvl != 0 ) { if ( pml.wallContact == 0 ) { // apply water friction based on waterlevel drop += speed * (float)waterlvl * pml.frametime; } } } else { // deep water if ( pml.wallContact == 0 ) { drop += speed * (float)waterlvl * pml.frametime; } } // spectator friction if ( ps->pm_type == PM_SPECTATOR ) { drop += speed * pm_spectatorfriction * pml.frametime; } // flight powerup friction if ( ps->powerups[PW_FLIGHT] ) { drop += speed * pm_flightfriction * pml.frametime; } // PM_FREEZE doubles friction if ( ps->pm_type == PM_FREEZE ) { drop = drop * 2.0f; } // wall contact friction if ( pml.wallContact != 0 ) { drop += pm_airStepFriction * speed * pml.frametime; /* DAT_001a8968 */ } newspeed = speed - drop; if ( newspeed <= 0.0f ) { newspeed = 0.0f; } newspeed /= speed; ps->velocity[0] *= newspeed; pm->ps->velocity[1] *= newspeed; pm->ps->velocity[2] *= newspeed; } /* ============= PM_Jump Performs the actual jump: sets velocity, events, animations, lastJumpTime QL version: chain jump, step jump, ramp jump logic ============= */ #define JUMPMODE_CHAIN 1 #define JUMPMODE_RAMP 2 #define JUMPMODE_SCALED 3 void PM_Jump( void ) { playerState_t *ps; float jumpvel; float duration; jumpvel = pm_jumpVelocity; pml.groundPlane = qfalse; pml.walking = qfalse; duration = (float)( pm->cmd.serverTime - pm->ps->lastJumpTime ); pm->ps->pm_flags |= PMF_JUMP_HELD; if ( duration > 0.0f && duration < pm_jumpVelocityTimeThreshold ) { if ( pm_chainJump == JUMPMODE_CHAIN ) { if ( pml.isStepJump == 1 ) { jumpvel += pm_stepJumpVelocity; } else { jumpvel += pm_chainJumpVelocity; } } else if ( pm_chainJump == JUMPMODE_RAMP ) { float threshold = pm_jumpVelocityTimeThreshold * pm_jumpVelocityTimeThresholdOffset; if ( duration < threshold ) { // below threshold: full chain jump if ( pml.isStepJump == 1 ) { jumpvel += pm_stepJumpVelocity; } else { jumpvel += pm_chainJumpVelocity; } } else { // ramp down jumpvel += ( ( threshold - duration ) / threshold ) * pm_chainJumpVelocity + pm_chainJumpVelocity; } } else if ( pm_chainJump == JUMPMODE_SCALED ) { jumpvel *= ( ( ( pm_jumpVelocityTimeThreshold - duration ) / pm_jumpVelocityTimeThreshold ) * pm_jumpVelocityScaleAdd + 1.0f ); } } pm->ps->groundEntityNum = ENTITYNUM_NONE; ps = pm->ps; if ( pmove_rampJumpFlag != 0 && pml.isJumppad != 1 ) { ps->velocity[2] = ps->velocity[2] * pm_rampJumpScale; pm->ps->velocity[2] += jumpvel; ps = pm->ps; if ( ps->velocity[2] < jumpvel ) { ps->velocity[2] = jumpvel; ps = pm->ps; } if ( ps->velocity[2] <= pm_jumpVelocityMax ) { goto do_event; } } ps->velocity[2] = jumpvel; ps = pm->ps; do_event: BG_AddPredictableEventToPlayerstate( EV_JUMP, 0, ps ); if ( pm->cmd.forwardmove >= 0 ) { PM_ForceLegsAnim( LEGS_JUMP ); pm->ps->pm_flags &= ~PMF_BACKWARDS_JUMP; } else { PM_ForceLegsAnim( LEGS_JUMPB ); pm->ps->pm_flags |= PMF_BACKWARDS_JUMP; } pm->ps->lastJumpTime = pm->cmd.serverTime; } /* ============= PM_CheckJump Returns 1 if the player jumped, 0 otherwise QL version: adds autohop, jump time delta, double jump support ============= */ int PM_CheckJump( int checkDoubleJump ) { playerState_t *ps; int pm_flags; ps = pm->ps; pm_flags = ps->pm_flags; // autohop + respawned check if ( ( pm_autoHopEnabled == 0 || ( pm_flags & PMF_AUTOHOP_HELD ) != 0 ) && ( pm_flags & PMF_RESPAWNED ) != 0 ) { return 0; } if ( pm->cmd.upmove < 10 ) { return 0; } // jump time delta minimum if ( (float)( pm->cmd.serverTime - ps->lastJumpTime ) < pm_jumpTimeDeltaMin ) { return 0; } // standard jump held check if ( pm_autoHopEnabled == 0 && ( pm_flags & PMF_JUMP_HELD ) != 0 ) { pm->cmd.upmove = 0; return 0; } pm_flags = ps->pm_flags; // autohop + jump held: no jump if ( ( pm_flags & ( PMF_AUTOHOP_HELD | PMF_JUMP_HELD ) ) == ( PMF_AUTOHOP_HELD | PMF_JUMP_HELD ) ) { pm->cmd.upmove = 0; return 0; } // double jump checks if ( checkDoubleJump != 0 ) { if ( ( pm_flags & ( PMF_DOUBLE_JUMP | PMF_JUMP_HELD ) ) == ( PMF_DOUBLE_JUMP | PMF_JUMP_HELD ) ) { pm->cmd.upmove = 0; return 0; } if ( ( pm_flags & PMF_DOUBLE_JUMP ) != 0 && ps->jumped != 0 ) { return 0; } } // clear crouch slide timer if ( ( pm_flags & PMF_CROUCH_SLIDE ) != 0 ) { ps->crouchSlideTimer = 0; } PM_Jump(); if ( checkDoubleJump != 0 && ( pm->ps->pm_flags & PMF_DOUBLE_JUMP ) != 0 ) { pm->ps->jumped = 1; } return 1; } /* ============= PM_CanJump Returns qtrue if the player is able to jump Used by step slide move for step-jump logic ============= */ int PM_CanJump( void ) { playerState_t *ps; int pm_flags; ps = pm->ps; pm_flags = ps->pm_flags; if ( ( pm_autoHopEnabled != 0 && ( pm_flags & PMF_AUTOHOP_HELD ) == 0 ) || ( pm_flags & PMF_RESPAWNED ) == 0 ) { if ( pm->cmd.upmove > 9 && pm_jumpTimeDeltaMin <= (float)( pm->cmd.serverTime - ps->lastJumpTime ) ) { if ( pm_autoHopEnabled == 0 && ( pm_flags & PMF_JUMP_HELD ) != 0 ) { pm->cmd.upmove = 0; return qfalse; } if ( ( ps->pm_flags & ( PMF_AUTOHOP_HELD | PMF_JUMP_HELD ) ) == ( PMF_AUTOHOP_HELD | PMF_JUMP_HELD ) ) { pm->cmd.upmove = 0; return qfalse; } return qtrue; } } return qfalse; } /* ============= PM_CanEdgeGrab (actually PM_CanAirStep in QL binary) Returns qtrue if the player can air-step (crouch + airborne + not falling + past jump threshold) ============= */ int PM_CanEdgeGrab( void ) { playerState_t *ps; ps = pm->ps; if ( ( ps->pm_flags & PMF_DUCKED ) != 0 && pml.groundPlane == 0 && ps->velocity[2] >= 0.0f ) { return pm_jumpVelocityTimeThreshold <= (float)( pm->cmd.serverTime - ps->lastJumpTime ); } return qfalse; } /* ============= PM_CheckGrapple Returns 1 if the grapple is active and pulling ============= */ int PM_CheckGrapple( void ) { playerState_t *ps; ps = pm->ps; if ( bg_itemlist[ ps->stats[STAT_HOLDABLE_ITEM] ].giType == IT_HOLDABLE /* entity type check: giTag == 6 */ && ( ps->pm_flags & PMF_GRAPPLE_PULL ) != 0 && ps->pm_type != PM_DEAD && ( ps->stats[12] == 0 || ps->stats[11] != 0 ) ) { return 1; } return 0; } /* =================== PM_GrappleMove QL version: uses stats array for grapple parameters =================== */ void PM_GrappleMove( void ) { int grappleActive; playerState_t *ps; grappleActive = PM_CheckGrapple(); if ( grappleActive == 0 ) { pm->ps->powerups[PW_NEUTRALFLAG] = 0; /* powerups[9] */ return; } pm->ps->powerups[PW_NEUTRALFLAG] = 0x7fffffff; ps = pm->ps; ps->velocity[2] += (float)(int)( (float)ps->stats[9] * pml.frametime ); ps = pm->ps; if ( ps->stats[11] < 0x7d01 ) { ps->stats[11] -= pml.msec; ps = pm->ps; if ( ps->stats[11] < 1 ) { ps->stats[11] = 0; ps = pm->ps; if ( ps->stats[12] == 0 ) { ps->stats[STAT_HOLDABLE_ITEM] = 0; ps = pm->ps; } } } if ( ( ps->pm_flags & PMF_DUCKED ) != 0 ) { PM_ForceLegsAnim( LEGS_IDLECR ); } else if ( ( ps->legsAnim & ~ANIM_TOGGLEBIT ) != LEGS_JUMP ) { PM_ForceLegsAnim( LEGS_JUMP ); } } /* =================== PM_WaterJumpMove Flying out of the water =================== */ void PM_WaterJumpMove( void ) { playerState_t *ps; PM_StepSlideMove( qtrue ); ps = pm->ps; ps->velocity[2] -= (float)ps->gravity * pml.frametime; ps = pm->ps; if ( ps->velocity[2] < 0.0f ) { ps->pm_flags &= ~PMF_ALL_TIMES; pm->ps->pm_time = 0; } } /* =================== PM_WaterMove QL version: uses PM_BuildWishVelocity, water swim/wade scale cvars, waterjump uses PMF_TIME_WATERJUMP_INIT =================== */ void PM_WaterMove( void ) { float wishspeed; vec3_t wishdir; int result; float swimScale; int contents; vec3_t spot; vec3_t flatforward; playerState_t *ps; // Check for water jump if ( pm->ps->pm_time == 0 && pm->waterlevel == 2 ) { flatforward[0] = pml.forward[0]; flatforward[1] = pml.forward[1]; flatforward[2] = 0.0f; VectorNormalize( flatforward ); spot[0] = flatforward[0] * 30.0f + pm->ps->origin[0]; /* DAT_001a89d4 = 30.0f */ spot[1] = flatforward[1] * 30.0f + pm->ps->origin[1]; spot[2] = 30.0f * flatforward[2] + pm->ps->origin[2] + 4.0f; /* DAT_001a8984 = 4.0f */ contents = pm->pointcontents( spot, pm->ps->clientNum ); if ( contents & CONTENTS_SOLID ) { spot[2] += 16.0f; /* DAT_001a89b0 = 16.0f */ result = pm->pointcontents( spot, pm->ps->clientNum ); if ( result == 0 ) { pm->ps->velocity[0] = pml.forward[0] * 200.0f; /* DAT_001a91bc = 200.0f */ pm->ps->velocity[1] = pml.forward[1] * 200.0f; pm->ps->velocity[2] = 350.0f; pm->ps->pm_flags |= PMF_TIME_WATERJUMP_INIT; pm->ps->pm_time = 2000; PM_WaterJumpMove(); return; } } } PM_Friction(); result = PM_BuildWishVelocity( &pm->cmd, &wishspeed, wishdir ); if ( result == 0 ) { wishdir[0] = 0.0f; wishdir[1] = 0.0f; wishdir[2] = -60.0f; wishspeed = VectorNormalize( wishdir ); } swimScale = pm_waterWadeScale; if ( pm->waterlevel != 1 ) { swimScale = pm_waterSwimScale; } swimScale *= (float)pm->ps->speed; if ( swimScale < wishspeed ) { wishspeed = swimScale; } PM_Accelerate( wishspeed, 4.0f, wishdir ); /* DAT_001a8984 = 4.0f for water accel */ if ( pml.groundPlane != 0 ) { ps = pm->ps; if ( DotProduct( ps->velocity, pml.groundTrace.plane.normal ) < 0.0f ) { PM_ClipVelocity( ps->velocity, pml.groundTrace.plane.normal, ps->velocity, OVERCLIP ); } } PM_StepSlideMove( qfalse ); } /* =================== PM_AirMove QL version: adds promode air strafe, air stop, and air control =================== */ void PM_AirMove( void ) { vec3_t wishvel; float fmove, smove; float wishspeed; float scale; float accel; playerState_t *ps; unsigned int movedir; // crouch slide charge in air if ( ( pm->ps->pm_flags & PMF_CROUCH_SLIDE ) != 0 && pm->cmd.upmove >= 0 ) { pm->ps->crouchSlideTimer += pml.msec * 5; if ( pm_crouchSlideTime < pm->ps->crouchSlideTimer ) { pm->ps->crouchSlideTimer = pm_crouchSlideTime; } } PM_Friction(); PM_SetMovementDir(); pml.forward[2] = 0.0f; pml.right[2] = 0.0f; VectorNormalize( pml.forward ); VectorNormalize( pml.right ); wishvel[2] = 0.0f; fmove = (float)(int)pm->cmd.forwardmove; smove = (float)(int)pm->cmd.rightmove; wishvel[0] = pml.forward[0] * fmove + pml.right[0] * smove; wishvel[1] = fmove * pml.forward[1] + smove * pml.right[1]; wishspeed = VectorNormalize( wishvel ); scale = PM_CmdScaleValue( pm->cmd.forwardmove, pm->cmd.rightmove, pm->cmd.upmove ); ps = pm->ps; wishspeed *= scale; accel = pm_airaccelerate; // promode air control logic if ( ( ps->pm_flags & PMF_PROMODE ) != 0 ) { if ( DotProduct( ps->velocity, wishvel ) < 0.0f ) { accel = pm_airstopaccelerate; movedir = ps->movementDir; } else { movedir = ps->movementDir; } // strafing (movementDir 2 or 6) if ( ( movedir & ~4 ) == 2 ) { accel = pm_strafeAccel; if ( pm_wishSpeed <= wishspeed ) { scale = pm_wishSpeed; // cap strafe wish speed } else { scale = wishspeed; } } else { scale = wishspeed; } } else { scale = wishspeed; } PM_Accelerate( scale, accel, wishvel ); ps = pm->ps; // promode air control (forward movement) if ( ( ps->pm_flags & PMF_PROMODE ) != 0 && ( ps->movementDir & ~4 ) == 0 && wishspeed != 0.0f ) { float savedZ, speed, dot; savedZ = ps->velocity[2]; ps->velocity[2] = 0.0f; speed = VectorNormalize( pm->ps->velocity ); ps = pm->ps; dot = DotProduct( ps->velocity, wishvel ); if ( dot > 0.0f ) { float k = pmove_crouchSlideVal * dot * dot * pml.frametime * pm_airControl; /* DAT_001a7200 */ ps->velocity[0] = ps->velocity[0] * speed + wishvel[0] * k; pm->ps->velocity[1] = speed * pm->ps->velocity[1] + k * wishvel[1]; VectorNormalize( pm->ps->velocity ); ps = pm->ps; } ps->velocity[0] *= speed; pm->ps->velocity[1] *= speed; pm->ps->velocity[2] = savedZ; } // clip to ground plane if ( pml.groundPlane != 0 ) { PM_ClipVelocity( pm->ps->velocity, pml.groundTrace.plane.normal, pm->ps->velocity, OVERCLIP ); } PM_StepSlideMove( qtrue ); // ducking animation if ( ( pm->ps->pm_flags & PMF_DUCKED ) == 0 ) { if ( ( pm->ps->legsAnim & ~ANIM_TOGGLEBIT ) == LEGS_IDLECR ) { PM_ForceLegsAnim( LEGS_IDLE ); } } else { PM_ForceLegsAnim( LEGS_IDLECR ); } PM_GrappleMove(); // double jump in air if ( ( pm->ps->pm_flags & PMF_DOUBLE_JUMP ) != 0 ) { PM_CheckJump( 1 ); } } /* =================== PM_FlyMove (used for spectator) QL version in PmoveSingle handles spectator inline =================== */ void PM_FlyMove( void ) { float wishspeed; vec3_t wishdir; PM_Friction(); PM_BuildWishVelocity( &pm->cmd, &wishspeed, wishdir ); PM_Accelerate( wishspeed, pm_flyaccelerate, wishdir ); PM_StepSlideMove( qfalse ); } /* ================ PM_FootstepForSurface Returns an event number appropriate for the groundsurface QL version: adds extra surface flag checks (0x80000, 0x100000) ================ */ int PM_FootstepForSurface( void ) { if ( pml.groundTrace.surfaceFlags & SURF_NOSTEPS ) { return 0; } if ( pml.groundTrace.surfaceFlags & SURF_METALSTEPS ) { return EV_FOOTSTEP_METAL; } /* QL adds extra surface type checks */ if ( pml.groundTrace.surfaceFlags & 0x80000 ) { return 0x51; /* custom footstep sound */ } if ( pml.groundTrace.surfaceFlags & 0x100000 ) { return 0x52; /* custom footstep sound 2 */ } return EV_FOOTSTEP; } /* =================== PM_CheckWallContact QL: Checks if the player is touching a wall ahead of them =================== */ void PM_CheckWallContact( void ) { vec3_t dir; vec3_t end; trace_t tr; pml.wallContact = 0; dir[0] = pml.forward[0]; dir[1] = pml.forward[1]; dir[2] = 0.0f; VectorNormalize( dir ); end[0] = pm->ps->origin[0] + dir[0]; end[1] = pm->ps->origin[1] + dir[1]; end[2] = pm->ps->origin[2] + dir[2]; pm->trace( &tr, pm->ps->origin, pm->mins, pm->maxs, end, pm->ps->clientNum, CONTENTS_SOLID | CONTENTS_BODY | CONTENTS_PLAYERCLIP ); if ( tr.fraction < 1.0f && ( tr.surfaceFlags & SURF_NOIMPACT ) ) { pml.wallContact = 1; } } /* ============= PM_SetWaterLevel QL version: simplified water level test ============= */ void PM_SetWaterLevel( void ) { vec3_t point; int cont; pm->waterlevel = 0; point[0] = pm->ps->origin[0]; point[1] = pm->ps->origin[1]; point[2] = pm->ps->origin[2] + MINS_Z + 1.0f; cont = pm->pointcontents( point, pm->ps->clientNum ); pm->watertype = cont; if ( ( cont & MASK_WATER ) != 0 ) { pm->waterlevel = 1; point[2] = pm->ps->origin[2]; cont = pm->pointcontents( point, pm->ps->clientNum ); if ( ( cont & MASK_WATER ) != 0 ) { pm->waterlevel = 2; point[2] = (float)pm->ps->viewheight + pm->ps->origin[2]; cont = pm->pointcontents( point, pm->ps->clientNum ); if ( ( cont & MASK_WATER ) != 0 ) { pm->waterlevel = 3; } } } } /* ============= PM_GroundTrace QL version: inlined PM_CorrectAllSolid, PM_GroundTraceMissed, PM_CrashLand; handles double jump reset ============= */ void PM_GroundTrace( void ) { vec3_t point; vec3_t point2; // used in PM_CorrectAllSolid jitter loop vec3_t traceEnd; trace_t tr; trace_t tr2; // second trace for GroundTraceMissed staircase check float delta; float dist, vel, acc, a, b, c, den, t; // crashland quadratic playerState_t *ps; int jitter_x, jitter_y, jitter_z; int event; point[0] = pm->ps->origin[0]; point[1] = pm->ps->origin[1]; point[2] = pm->ps->origin[2] - 2.0f; /* DAT_001a7214 = 2.0f */ pm->trace( &tr, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask ); pml.groundTrace = tr; // allsolid correction (inlined PM_CorrectAllSolid) if ( tr.allsolid != 0 ) { if ( pm->debugLevel ) { Com_Printf( "%i:allsolid\n", c_pmove ); } for ( jitter_x = -1; jitter_x <= 1; jitter_x++ ) { for ( jitter_y = -1; jitter_y <= 1; jitter_y++ ) { for ( jitter_z = -1; jitter_z <= 1; jitter_z++ ) { point2[0] = (float)jitter_x + pm->ps->origin[0]; point2[1] = (float)jitter_y + pm->ps->origin[1]; point2[2] = (float)jitter_z + pm->ps->origin[2]; pm->trace( &tr, point2, pm->mins, pm->maxs, point2, pm->ps->clientNum, pm->tracemask ); if ( tr.allsolid == 0 ) { point2[0] = pm->ps->origin[0]; point2[1] = pm->ps->origin[1]; point2[2] = pm->ps->origin[2] - 2.0f; pm->trace( &tr, pm->ps->origin, pm->mins, pm->maxs, point2, pm->ps->clientNum, pm->tracemask ); pml.groundTrace = tr; goto corrected; } } } } pm->ps->groundEntityNum = ENTITYNUM_NONE; pml.groundPlane = qfalse; pml.walking = qfalse; return; } corrected: // free fall check if ( tr.fraction == 1.0f ) { ps = pm->ps; if ( ps->groundEntityNum != ENTITYNUM_NONE ) { if ( pm->debugLevel ) { Com_Printf( "%i:lift\n", c_pmove ); ps = pm->ps; } traceEnd[0] = ps->origin[0]; traceEnd[1] = pm->ps->origin[1]; traceEnd[2] = pm->ps->origin[2] - 64.0f; /* DAT_001a91f8 = 64.0f */ pm->trace( &tr2, pm->ps->origin, pm->mins, pm->maxs, traceEnd, pm->ps->clientNum, pm->tracemask ); if ( tr2.fraction == 1.0f ) { if ( pm->cmd.forwardmove < 0 ) { PM_ForceLegsAnim( LEGS_JUMPB ); pm->ps->pm_flags |= PMF_BACKWARDS_JUMP; } else { PM_ForceLegsAnim( LEGS_JUMP ); pm->ps->pm_flags &= ~PMF_BACKWARDS_JUMP; } } } pm->ps->groundEntityNum = ENTITYNUM_NONE; pml.groundPlane = qfalse; pml.walking = qfalse; return; } // check if getting thrown off the ground ps = pm->ps; if ( ps->velocity[2] > 0.0f && DotProduct( ps->velocity, tr.plane.normal ) > 10.0f ) { if ( pm->debugLevel ) { Com_Printf( "%i:kickoff\n", c_pmove ); } if ( pm->cmd.forwardmove < 0 ) { PM_ForceLegsAnim( LEGS_JUMPB ); pm->ps->pm_flags |= PMF_BACKWARDS_JUMP; } else { PM_ForceLegsAnim( LEGS_JUMP ); pm->ps->pm_flags &= ~PMF_BACKWARDS_JUMP; } pm->ps->groundEntityNum = ENTITYNUM_NONE; pml.groundPlane = qfalse; pml.walking = qfalse; return; } // steep slope if ( tr.plane.normal[2] < MIN_WALK_NORMAL ) { if ( pm->debugLevel ) { Com_Printf( "%i:steep\n", c_pmove ); ps = pm->ps; } ps->groundEntityNum = ENTITYNUM_NONE; pml.groundPlane = qtrue; pml.walking = qfalse; return; } pml.groundPlane = qtrue; pml.walking = qtrue; ps = pm->ps; // hitting solid ground will end a waterjump if ( ( ps->pm_flags & PMF_TIME_WATERJUMP_INIT ) != 0 ) { ps->pm_flags &= ~( PMF_TIME_WATERJUMP_INIT | PMF_TIME_KNOCKBACK ); pm->ps->pm_time = 0; ps = pm->ps; } if ( ps->groundEntityNum == ENTITYNUM_NONE ) { // just hit the ground — inlined PM_CrashLand if ( pm->debugLevel ) { Com_Printf( "%i:Land\n", c_pmove ); ps = pm->ps; } // landing animation if ( ( ps->pm_flags & PMF_BACKWARDS_JUMP ) == 0 ) { PM_ForceLegsAnim( LEGS_LAND ); } else { PM_ForceLegsAnim( LEGS_LANDB ); } pm->ps->legsTimer = TIMER_LAND; ps = pm->ps; // Q3 PM_CrashLand quadratic: solve for exact landing velocity dist = ps->origin[2] - pml.previous_origin[2]; vel = pml.previous_velocity[2]; acc = -(float)ps->gravity; a = acc / 2.0f; b = vel; c = -dist; den = b * b - 4.0f * a * c; if ( den >= 0.0f && pm->waterlevel != 3 ) { t = ( -b - (float)sqrt( den ) ) / ( 2.0f * a ); delta = vel + t * acc; delta = delta * delta * 0.0001f; // reduce falling damage if there is standing water if ( pm->waterlevel == 2 ) { delta *= 0.25f; } else if ( pm->waterlevel == 1 ) { delta *= 0.5f; } if ( delta >= 1.0f ) { // landing events if ( !( pml.groundTrace.surfaceFlags & SURF_NODAMAGE ) && ps->stats[STAT_HEALTH] > -40 ) { if ( delta > 60.0f ) { BG_AddPredictableEventToPlayerstate( EV_FALL_FAR, 0, ps ); ps = pm->ps; } else if ( delta > 40.0f ) { if ( ps->stats[STAT_HEALTH] > 0 ) { BG_AddPredictableEventToPlayerstate( EV_FALL_MEDIUM, 0, ps ); ps = pm->ps; } } else if ( delta > 7.0f ) { BG_AddPredictableEventToPlayerstate( EV_FALL_SHORT, 0, ps ); ps = pm->ps; } else { event = 0; if ( pm->noFootsteps == 0 ) { event = PM_FootstepForSurface(); } BG_AddPredictableEventToPlayerstate( event, 0, ps ); ps = pm->ps; } } ps->bobCycle = 0; ps = pm->ps; // reset double jump if ( ( ps->pm_flags & PMF_DOUBLE_JUMP ) != 0 ) { ps->jumped = 0; ps = pm->ps; } } } if ( pml.previous_velocity[2] < -200.0f ) { ps->pm_flags |= PMF_TIME_KNOCKBACK; pm->ps->pm_time = 250; ps = pm->ps; } } ps->groundEntityNum = tr.entityNum; PM_AddTouchEnt( tr.entityNum ); } /* ================ PM_UpdateViewAngles This can be used as another entry point when only the viewangles are being updated instead of a full move ================ */ void PM_UpdateViewAngles( playerState_t *ps, const usercmd_t *cmd ) { short temp; int i; if ( ps->pm_type == PM_INTERMISSION || ps->pm_type == PM_SPINTERMISSION ) { return; // no view changes at all } if ( ps->pm_type != PM_SPECTATOR && ps->pm_type != PM_FREEZE && ps->stats[STAT_HEALTH] <= 0 ) { return; // no view changes at all } // circularly clamp the angles with deltas for ( i = 0; i < 3; i++ ) { temp = cmd->angles[i] + ps->delta_angles[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]; temp = 16000; } else if ( temp < -16000 ) { ps->delta_angles[i] = -16000 - cmd->angles[i]; temp = -16000; } } ps->viewangles[i] = SHORT2ANGLE( temp ); } } /* ================ PmoveSingle QL version: massive inlined function that contains movement dispatch, weapon handling, animation, footsteps, and water events all inline. ================ */ void trap_SnapVector( float *v ); void PmoveSingle( pmove_t *pmove ) { playerState_t *ps; int pm_flags; int oldTime; int msec; int buttons; signed char fmove, rmove; float speed, accel, wishspd, cmdScale; float wishspeed; vec3_t wishvel, wishdir; int waterlvl; qboolean playFootstep; int i; pm = pmove; ps = pm->ps; // this counter lets us debug movement problems with a journal c_pmove++; // set up physics overrides based on promode flag PM_SetupPhysicsOverrides(); ps = pm->ps; // clear results pm->numtouch = 0; pm->watertype = 0; pm->waterlevel = 0; if ( ps->stats[STAT_HEALTH] < 1 && ps->powerups[PW_NUM_POWERUPS - 1] == 0 ) { pm->tracemask &= ~CONTENTS_BODY; // corpses can fly through bodies } if ( pmove_globalFlag == 1 ) { pm->tracemask &= ~CONTENTS_BODY; } // make sure walking button is clear if they are running if ( abs( pm->cmd.forwardmove ) > 64 || abs( pm->cmd.rightmove ) > 64 ) { pm->cmd.buttons &= ~BUTTON_WALKING; } // set the talk balloon flag if ( pm->cmd.buttons & BUTTON_TALK ) { ps->eFlags |= EF_TALK; } else { ps->eFlags &= ~EF_TALK; } // clear firing flag ps->eFlags &= ~EF_FIRING; // clear the respawned flag if attack and use are cleared buttons = pm->cmd.buttons; if ( pm->ps->stats[STAT_HEALTH] > 0 && ( buttons & ( BUTTON_ATTACK | BUTTON_USE_HOLDABLE ) ) == 0 ) { pm->ps->pm_flags &= ~PMF_RESPAWNED; buttons = pm->cmd.buttons; } // if talk button is down, disallow all other input if ( buttons & BUTTON_TALK ) { pm->cmd.buttons = BUTTON_TALK; pm->cmd.forwardmove = 0; pm->cmd.rightmove = 0; pm->cmd.upmove = 0; } // clear all pmove local vars memset( &pml, 0, sizeof( pml ) ); // determine the time pml.msec = pm->cmd.serverTime - pm->ps->commandTime; if ( pml.msec < 1 ) { pml.msec = 1; } else if ( pml.msec > 200 ) { pml.msec = 200; } pm->ps->commandTime = pm->cmd.serverTime; ps = pm->ps; // PMF_USE_ITEM_HELD freezes player (intermission) if ( ( ps->pm_flags & PMF_USE_ITEM_HELD ) != 0 ) { return; } // save old org in case we get stuck VectorCopy( ps->origin, pml.previous_origin ); // save old velocity for crashlanding VectorCopy( pm->ps->velocity, pml.previous_velocity ); pml.frametime = (float)( (double)pml.msec * 0.001 ); // update the viewangles PM_UpdateViewAngles( pm->ps, &pm->cmd ); // sync cmd fields to playerstate ps = pm->ps; if ( pm->cmd.forwardmove != ps->forwardmove ) { ps->forwardmove = pm->cmd.forwardmove; } if ( pm->cmd.rightmove != ps->rightmove ) { ps->rightmove = pm->cmd.rightmove; } if ( pm->cmd.upmove != ps->upmove ) { ps->upmove = pm->cmd.upmove; } AngleVectors( pm->ps->viewangles, pml.forward, pml.right, pml.up ); if ( pm->cmd.upmove < 10 ) { // not holding jump pm->ps->pm_flags &= ~PMF_JUMP_HELD; } // decide if backpedaling animations should be used fmove = pm->cmd.forwardmove; if ( fmove < 0 ) { pm->ps->pm_flags |= PMF_BACKWARDS_RUN; } else if ( fmove != 0 || pm->cmd.rightmove != 0 ) { pm->ps->pm_flags &= ~PMF_BACKWARDS_RUN; } ps = pm->ps; oldTime = ps->pm_type; if ( oldTime > PM_SPECTATOR ) { pm->cmd.forwardmove = 0; pm->cmd.rightmove = 0; pm->cmd.upmove = 0; oldTime = ps->pm_type; } // // Movement type dispatch // if ( oldTime == PM_SPECTATOR ) { PM_CheckDuck(); PM_Friction(); PM_BuildWishVelocity( &pm->cmd, wishvel, wishdir ); PM_Accelerate( wishvel[0], pm_flyaccelerate, wishdir ); PM_StepSlideMove( qfalse ); PM_DropTimers(); goto done_movement; } if ( oldTime == PM_NOCLIP ) { // noclip move ps->viewheight = DEFAULT_VIEWHEIGHT; speed = sqrt( ps->velocity[0] * ps->velocity[0] + ps->velocity[1] * ps->velocity[1] + ps->velocity[2] * ps->velocity[2] ); if ( speed < 1.0f ) { VectorClear( ps->velocity ); } else { wishspd = pm_stopspeed; if ( pm_stopspeed <= speed ) { wishspd = speed; } wishspd = speed - 10.0f * pml.frametime * wishspd; /* DAT_001a7208 = 10.0 */ if ( wishspd <= 0.0f ) { wishspd = 0.0f; } wishspd /= speed; ps->velocity[0] *= wishspd; pm->ps->velocity[1] *= wishspd; pm->ps->velocity[2] *= wishspd; } PM_BuildWishVelocity( &pm->cmd, wishvel, wishdir ); PM_Accelerate( wishvel[0], pm_walkAccel, wishdir ); ps = pm->ps; ps->origin[0] += ps->velocity[0] * pml.frametime; ps = pm->ps; ps->origin[1] += ps->velocity[1] * pml.frametime; ps = pm->ps; ps->origin[2] += ps->velocity[2] * pml.frametime; PM_DropTimers(); goto done_movement; } if ( oldTime == PM_FREEZE ) { pm->cmd.forwardmove = 0; pm->cmd.rightmove = 0; pm->cmd.upmove = 0; oldTime = ps->pm_type; } if ( oldTime == PM_INTERMISSION || oldTime == PM_SPINTERMISSION ) { return; // no movement at all } // set watertype, and waterlevel PM_SetWaterLevel(); pml.previous_waterlevel = pm->waterlevel; // set mins, maxs, and viewheight PM_CheckDuck(); // set groundentity PM_GroundTrace(); // dead move ps = pm->ps; if ( ps->pm_type == PM_DEAD && pml.walking != 0 ) { speed = sqrt( ps->velocity[0] * ps->velocity[0] + ps->velocity[1] * ps->velocity[1] + ps->velocity[2] * ps->velocity[2] ) - 20.0f; /* DAT_001a8970 = 20.0f */ if ( speed <= 0.0f ) { ps->velocity[0] = 0.0f; ps->velocity[1] = 0.0f; ps->velocity[2] = 0.0f; } else { VectorNormalize( ps->velocity ); pm->ps->velocity[0] *= speed; pm->ps->velocity[1] *= speed; pm->ps->velocity[2] *= speed; } } PM_DropTimers(); PM_CheckWallContact(); // clear grapple powerup counter pm->ps->powerups[PW_NEUTRALFLAG] = 0; /* powerups[9] */ ps = pm->ps; // invulnerability sphere if ( ps->powerups[PW_INVULNERABILITY] != 0 ) { pm->cmd.forwardmove = 0; pm->cmd.rightmove = 0; pm->cmd.upmove = 0; ps->velocity[0] = 0.0f; ps->velocity[1] = 0.0f; ps->velocity[2] = 0.0f; goto after_move; } // grapple active (non-pulling) if ( ( ps->pm_flags & PMF_GRAPPLE_ACTIVE ) != 0 ) { /* QL grapple move uses DAT_003cfe24 and grapple point */ /* This is a simplified reconstruction */ float grappleSpeed; /* DAT_003cfe24 */ grappleSpeed = pm_velocityGh; /* placeholder — actual cvar unknown */ if ( grappleSpeed != 0.0f ) { vec3_t gdir; float dist; VectorScale( pml.forward, -16.0f, gdir ); /* DAT_001a89cc = -16.0f */ gdir[0] = gdir[0] + ps->grapplePoint[0] - ps->origin[0]; gdir[1] = gdir[1] + ps->grapplePoint[1] - ps->origin[1]; gdir[2] = gdir[2] + ps->grapplePoint[2] - ps->origin[2]; dist = VectorLength( gdir ); VectorNormalize( gdir ); gdir[0] *= grappleSpeed; gdir[1] *= grappleSpeed; gdir[2] *= grappleSpeed; if ( (double)dist <= (double)grappleSpeed * 0.1 ) { /* _DAT_001a7230 = 0.1 */ float s = ( dist * 10.0f ) / grappleSpeed; gdir[0] *= s; gdir[1] *= s; gdir[2] *= s; } if ( pm->field_0f8 == 0 ) { pml.groundPlane = qfalse; } else { gdir[0] *= 2.0f; gdir[1] *= 2.0f; gdir[2] *= 2.0f; } if ( (int)pm->waterlevel > 1 ) { float wscale = 0.75f; /* DAT_001a91b8 = 0.75f */ gdir[0] *= wscale; gdir[1] *= wscale; gdir[2] *= wscale; } pm->ps->velocity[0] = gdir[0]; pm->ps->velocity[1] = gdir[1]; pm->ps->velocity[2] = gdir[2]; } PM_AirMove(); goto after_move; } // waterjump if ( ( ps->pm_flags & PMF_TIME_WATERJUMP_INIT ) != 0 ) { PM_WaterJumpMove(); goto after_move; } // swimming if ( (int)pm->waterlevel > 1 ) { if ( pml.wallContact == 0 ) { PM_WaterMove(); goto after_move; } /* wall contact in water: fall through to walking/air */ } // walking on ground if ( pml.walking != 0 ) { int grapple = PM_CheckGrapple(); if ( grapple != 0 ) { PM_StepSlideMove( qfalse ); PM_GrappleMove(); goto after_move; } oldTime = PM_CheckJump( 0 ); if ( oldTime != 0 ) { if ( (int)pm->waterlevel > 1 ) { PM_WaterMove(); } else { PM_AirMove(); } goto after_move; } // ground walk PM_Friction(); PM_SetMovementDir(); pml.forward[2] = 0.0f; pml.right[2] = 0.0f; 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 ); fmove = pm->cmd.forwardmove; rmove = pm->cmd.rightmove; for ( i = 0; i < 3; i++ ) { wishdir[i] = pml.forward[i] * (float)(int)fmove + pml.right[i] * (float)(int)rmove; } accel = VectorNormalize( wishdir ); wishspd = PM_CmdScaleValue( pm->cmd.forwardmove, pm->cmd.rightmove, pm->cmd.upmove ); wishspd *= accel; pm_flags = pm->ps->pm_flags; cmdScale = (float)pm->ps->speed; // crouch slide speed scale accel = 2.0f; /* DAT_001a7214 = 2.0f — but for duck scale it's probably 0.25f */ if ( ( pm_flags & PMF_CROUCH_SLIDE ) != 0 ) { accel = 0.75f; /* DAT_001a91b8 = 0.75f */ } accel *= cmdScale; if ( accel < wishspd && ( pm_flags & PMF_DUCKED ) != 0 ) { wishspd = accel; } // water scaling waterlvl = pm->waterlevel; if ( waterlvl != 0 ) { float wscale; wscale = pm_waterSwimScale; if ( waterlvl == 1 ) { wscale = pm_waterWadeScale; } cmdScale = (float)( 1.0 - ( 1.0 - (double)wscale ) * ( (double)waterlvl / 3.0 ) ) * cmdScale; if ( cmdScale <= wishspd ) { wishspd = cmdScale; } } // acceleration speed = pm_walkAccel; if ( pml.groundTrace.surfaceFlags & SURF_SLICK ) { speed = pm_airaccelerate; /* DAT_001ab5d8 for slick+ducked */ if ( ( pm_flags & PMF_DUCKED ) == 0 ) { speed = 1.0f; } } else if ( ( pm_flags & PMF_TIME_WATERJUMP ) != 0 ) { speed = 1.0f; } PM_Accelerate( wishspd, speed, wishdir ); ps = pm->ps; if ( ( pml.groundTrace.surfaceFlags & SURF_SLICK ) || ( ps->pm_flags & PMF_TIME_WATERJUMP ) ) { ps->velocity[2] -= (float)ps->gravity * pml.frametime; ps = pm->ps; } PM_ClipVelocity( ps->velocity, pml.groundTrace.plane.normal, ps->velocity, OVERCLIP ); if ( pm->ps->velocity[0] != 0.0f || pm->ps->velocity[1] != 0.0f ) { PM_StepSlideMove( qfalse ); } goto after_move; } // airborne PM_AirMove(); after_move: // grapple stat tracking ps = pm->ps; if ( ps->stats[12] != 0 && ( ps->pm_flags & PMF_GRAPPLE_PULL ) == 0 ) { int val = (int)( (float)ps->stats[12] * pml.frametime ) + ps->stats[11]; if ( ps->stats[10] < val ) { val = ps->stats[10]; } ps->stats[11] = val; } // // Animate (gesture buttons) // buttons = pm->cmd.buttons; ps = pm->ps; if ( buttons & BUTTON_GESTURE ) { if ( ps->torsoTimer == 0 ) { PM_StartTorsoAnim( TORSO_GESTURE ); ps->torsoTimer = TIMER_GESTURE; BG_AddPredictableEventToPlayerstate( EV_TAUNT, 0, pm->ps ); } } else if ( buttons & 0x80 ) { if ( ps->torsoTimer == 0 ) { PM_StartTorsoAnim( TORSO_GUARDBASE ); ps->torsoTimer = 600; } } else if ( buttons & 0x100 ) { if ( ps->torsoTimer == 0 ) { PM_StartTorsoAnim( TORSO_PATROL ); ps->torsoTimer = 600; } } else if ( buttons & 0x200 ) { if ( ps->torsoTimer == 0 ) { PM_StartTorsoAnim( TORSO_FOLLOWME ); ps->torsoTimer = 600; } } else if ( buttons & 0x400 ) { if ( ps->torsoTimer == 0 ) { PM_StartTorsoAnim( TORSO_AFFIRMATIVE ); ps->torsoTimer = 600; } } else if ( buttons & 0x20 ) { if ( ps->torsoTimer == 0 ) { PM_StartTorsoAnim( TORSO_NEGATIVE ); ps->torsoTimer = 600; } } else if ( buttons & 0x40 ) { if ( ps->torsoTimer == 0 ) { PM_StartTorsoAnim( TORSO_GETFLAG ); ps->torsoTimer = 600; } } // // Second ground trace + water level // PM_GroundTrace(); PM_SetWaterLevel(); // // Weapon handling // ps = pm->ps; pm_flags = ps->pm_flags; if ( pm_flags & PMF_RESPAWNED ) { goto after_weapon; } if ( ps->stats[STAT_HEALTH] < 1 ) { ps->weapon = WP_NONE; goto torso_anim; } // set firing flag buttons = pm->cmd.buttons; if ( ( buttons & BUTTON_ATTACK ) != 0 && ( pm_flags & PMF_ATTACK_HELD ) == 0 && ( ps->ammo[ ps->weapon ] != 0 || ps->weapon == WP_GRAPPLING_HOOK ) ) { ps->eFlags |= EF_FIRING; ps = pm->ps; buttons = pm->cmd.buttons; pm_flags = ps->pm_flags; } // holdable item use if ( ( buttons & BUTTON_USE_HOLDABLE ) == 0 ) { ps->pm_flags &= ~PMF_GRAPPLE_PULL; ps = pm->ps; } else if ( ( pm_flags & PMF_GRAPPLE_PULL ) == 0 ) { /* QL holdable item use logic */ ps->pm_flags |= PMF_GRAPPLE_PULL; /* simplified: consume item */ BG_AddPredictableEventToPlayerstate( EV_USE_ITEM0 + bg_itemlist[ ps->stats[STAT_HOLDABLE_ITEM] ].giTag, 0, pm->ps ); pm->ps->stats[STAT_HOLDABLE_ITEM] = 0; ps = pm->ps; goto torso_anim; } // weapon time countdown if ( ps->weaponTime > 0 ) { ps->weaponTime -= pml.msec; ps = pm->ps; if ( ps->weaponTime > 0 && ps->weaponstate == WEAPON_FIRING ) { goto weapon_change_check; } } weapon_change_check: // check for weapon change { int cmdWeapon = pm->cmd.weapon; if ( ps->weapon != cmdWeapon && cmdWeapon >= 1 && cmdWeapon <= WP_NUM_WEAPONS && ( ps->stats[STAT_WEAPONS] & ( 1 << cmdWeapon ) ) ) { if ( ps->weaponstate == WEAPON_DROPPING ) { // already dropping } else { BG_AddPredictableEventToPlayerstate( EV_CHANGE_WEAPON, 0, ps ); pm->ps->weaponstate = WEAPON_DROPPING; pm->ps->weaponTime += pm_weaponDropTime; ps = pm->ps; PM_StartTorsoAnim( TORSO_DROP ); ps = pm->ps; } } } if ( ps->weaponTime > 0 ) { goto torso_anim; } // finish weapon change if ( ps->weaponstate == WEAPON_DROPPING ) { int weapon = pm->cmd.weapon; if ( weapon < WP_NONE || weapon >= WP_NUM_WEAPONS ) { weapon = WP_NONE; } if ( !( ps->stats[STAT_WEAPONS] & ( 1 << weapon ) ) ) { weapon = WP_NONE; } ps->weapon = weapon; pm->ps->weaponstate = WEAPON_RAISING; pm->ps->weaponTime += pm_weaponRaiseTime; pm->ps->stats[8] = 0; ps = pm->ps; PM_StartTorsoAnim( TORSO_RAISE ); ps = pm->ps; goto torso_anim; } if ( ps->weaponstate == WEAPON_RAISING ) { ps->weaponstate = WEAPON_READY; ps = pm->ps; goto torso_anim; } // check for fire if ( ps->weapon == WP_GRAPPLING_HOOK ) { /* grapple weapon has special handling */ goto torso_anim; } if ( !( pm->cmd.buttons & BUTTON_ATTACK ) || ( ps->pm_flags & PMF_ATTACK_HELD ) ) { ps->weaponTime = 0; pm->ps->weaponstate = WEAPON_READY; ps = pm->ps; goto torso_anim; } // grapple released blocks fire if ( ( ps->pm_flags & PMF_GRAPPLE_RELEASED ) != 0 ) { ps->weaponTime = 0; pm->ps->weaponstate = WEAPON_READY; pm->ps->eFlags &= ~EF_FIRING; pm->ps->pm_flags &= ~PMF_GRAPPLE_RELEASED; ps = pm->ps; goto torso_anim; } // gauntlet special if ( ps->weapon == WP_GAUNTLET ) { if ( pm->gauntletHit == 0 ) { ps->weaponTime = 0; pm->ps->weaponstate = WEAPON_READY; ps = pm->ps; goto torso_anim; } PM_StartTorsoAnim( TORSO_ATTACK2 ); ps = pm->ps; } else { PM_StartTorsoAnim( TORSO_ATTACK ); ps = pm->ps; } ps->weaponstate = WEAPON_FIRING; ps = pm->ps; // check for out of ammo if ( ps->ammo[ ps->weapon ] == 0 ) { BG_AddPredictableEventToPlayerstate( EV_NOAMMO, 0, ps ); pm->ps->weaponTime += 500; ps = pm->ps; goto torso_anim; } // take an ammo away if not infinite if ( ps->ammo[ ps->weapon ] != -1 ) { ps->ammo[ ps->weapon ]--; ps = pm->ps; } // fire weapon BG_AddPredictableEventToPlayerstate( EV_FIRE_WEAPON, 0, ps ); ps = pm->ps; { int addTime; /* QL uses a table lookup for weapon fire times */ /* Simplified inline version */ switch ( ps->weapon ) { default: case WP_GAUNTLET: addTime = 400; break; case WP_LIGHTNING: addTime = 50; break; case WP_SHOTGUN: addTime = 1000; break; case WP_MACHINEGUN: addTime = 100; break; case WP_GRENADE_LAUNCHER: addTime = 800; break; case WP_ROCKET_LAUNCHER: addTime = 800; break; case WP_PLASMAGUN: addTime = 100; break; case WP_RAILGUN: addTime = 1500; break; case WP_BFG: addTime = 200; break; case WP_GRAPPLING_HOOK: addTime = 400; break; } // haste powerup if ( ps->powerups[PW_HASTE] ) { addTime = (int)( (double)addTime * 0.75 ); } ps->weaponTime += addTime; } ps = pm->ps; torso_anim: // torso animation if ( ps->weaponstate == WEAPON_READY ) { int anim; if ( ps->weapon == WP_GRAPPLING_HOOK || ps->weapon == WP_GAUNTLET ) { anim = TORSO_STAND2; } else { anim = TORSO_STAND; } PM_ContinueTorsoAnim( anim ); } after_weapon: // // Footsteps / legs animations // ps = pm->ps; pm->xyspeed = sqrt( ps->velocity[0] * ps->velocity[0] + ps->velocity[1] * ps->velocity[1] ); speed = pm->xyspeed; if ( ps->groundEntityNum == ENTITYNUM_NONE ) { if ( ps->powerups[PW_INVULNERABILITY] != 0 ) { PM_ContinueLegsAnim( LEGS_IDLECR ); } waterlvl = pm->waterlevel; if ( waterlvl > 1 ) { PM_ContinueLegsAnim( LEGS_SWIM ); waterlvl = pm->waterlevel; } ps = pm->ps; goto water_events; } // on ground if ( pm->cmd.forwardmove == 0 ) { pm_flags = ps->pm_flags; if ( pm->cmd.rightmove != 0 || ( ( pm_flags & PMF_CROUCH_SLIDE ) != 0 && pm->cmd.upmove < 0 && ps->crouchSlideTimer > 0 ) ) { // moving sideways or crouch sliding if ( pm_flags & PMF_DUCKED ) { goto ducked_move; } goto running_move; } // not moving if ( speed < 5.0f ) { ps->bobCycle = 0; if ( pm->ps->pm_flags & PMF_DUCKED ) { PM_ContinueLegsAnim( LEGS_IDLECR ); } else { PM_ContinueLegsAnim( LEGS_IDLE ); } waterlvl = pm->waterlevel; ps = pm->ps; goto water_events; } waterlvl = pm->waterlevel; goto water_events; } pm_flags = ps->pm_flags; if ( pm_flags & PMF_DUCKED ) { ducked_move: playFootstep = qfalse; if ( pm_flags & PMF_BACKWARDS_RUN ) { PM_ContinueLegsAnim( LEGS_BACKCR ); } else { PM_ContinueLegsAnim( LEGS_WALKCR ); } speed = 0.5f; goto bob_cycle; } running_move: if ( !( pm->cmd.buttons & BUTTON_WALKING ) ) { if ( ps->velocity[0] >= pm_airControl || ps->velocity[0] <= -pm_airControl || ps->velocity[1] >= pm_airControl || ps->velocity[1] <= -pm_airControl ) { playFootstep = ( ps->stats[STAT_WEAPONS] != 1 ); if ( pm_flags & PMF_BACKWARDS_RUN ) { PM_ContinueLegsAnim( LEGS_BACK ); } else { PM_ContinueLegsAnim( LEGS_RUN ); } speed = 0.4f; } else { playFootstep = qfalse; if ( pm_flags & PMF_BACKWARDS_RUN ) { PM_ContinueLegsAnim( LEGS_BACKWALK ); } else { PM_ContinueLegsAnim( LEGS_WALK ); } speed = 0.3f; } } else { playFootstep = qfalse; if ( pm_flags & PMF_BACKWARDS_RUN ) { PM_ContinueLegsAnim( LEGS_BACKWALK ); } else { PM_ContinueLegsAnim( LEGS_WALK ); } speed = 0.3f; } bob_cycle: { int old = pm->ps->bobCycle; pm->ps->bobCycle = (int)( (float)old + (float)pml.msec * speed ) & 255; ps = pm->ps; if ( ( ( ps->bobCycle + 64 ) ^ ( old + 64 ) ) & 128 ) { waterlvl = pm->waterlevel; if ( waterlvl == 0 ) { if ( playFootstep ) { int evt = 0; if ( pm->noFootsteps == 0 ) { evt = PM_FootstepForSurface(); } BG_AddPredictableEventToPlayerstate( evt, 0, ps ); waterlvl = pm->waterlevel; ps = pm->ps; } goto water_events; } else if ( waterlvl == 1 ) { BG_AddPredictableEventToPlayerstate( EV_FOOTSPLASH, 0, ps ); waterlvl = pm->waterlevel; ps = pm->ps; goto water_events; } else if ( waterlvl == 2 ) { BG_AddPredictableEventToPlayerstate( EV_SWIM, 0, ps ); waterlvl = pm->waterlevel; ps = pm->ps; goto water_events; } // waterlevel 3: no sound waterlvl = pm->waterlevel; ps = pm->ps; } else { waterlvl = pm->waterlevel; } } water_events: // // Water events // if ( pml.previous_waterlevel == 0 && waterlvl != 0 ) { BG_AddPredictableEventToPlayerstate( EV_WATER_TOUCH, 0, ps ); ps = pm->ps; waterlvl = pm->waterlevel; } if ( pml.previous_waterlevel != 0 && waterlvl == 0 ) { BG_AddPredictableEventToPlayerstate( EV_WATER_LEAVE, 0, ps ); ps = pm->ps; } // head underwater events if ( ps->pm_type != PM_DEAD ) { if ( pml.previous_waterlevel != 3 && pm->waterlevel == 3 ) { BG_AddPredictableEventToPlayerstate( EV_WATER_UNDER, 0, ps ); ps = pm->ps; } if ( ps->powerups[PW_FLIGHT] == 0 && pml.previous_waterlevel == 3 && pm->waterlevel != 3 ) { BG_AddPredictableEventToPlayerstate( EV_WATER_CLEAR, 0, ps ); } } done_movement: // snap velocity trap_SnapVector( pm->ps->velocity ); } /* ================ 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_PMOVEFRAMECOUNTBITS ) - 1 ); // chop the move up if it is too long, to prevent framerate // dependent behavior while ( pmove->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 ); if ( pmove->ps->pm_flags & PMF_JUMP_HELD ) { pmove->cmd.upmove = 20; } } }