Compare commits

..

1 commit

Author SHA1 Message Date
8f778b2f64 QL-style auto-hop with smooth stair traversal
Backports key Quake Live movement mechanics to Q3:

- Auto-hop: remove PMF_JUMP_HELD gate so holding jump bunny-hops
  continuously. The Pmove() outer loop already forces upmove=20.

- Air steps: remove Q3's velocity[2]>0 gate in PM_StepSlideMove,
  allowing step-ups while airborne for bunny-hop stair traversal.

- Conditional velocity clip: only clip velocity to step-down surface
  when moving INTO it. Preserve upward momentum when velocity is
  already moving away from the surface. This is the key mechanic
  for smooth stair hopping.

- 100ms jump cooldown via lastJumpTime (networked in playerState_t)
  to prevent same-frame double-fires.

- PM_Jump/PM_CanJump extracted for reuse by future features
  (step jump, double jump, etc).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:10:22 +08:00
5 changed files with 66 additions and 20 deletions

View file

@ -26,7 +26,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#define STEPSIZE 18
#define JUMP_VELOCITY 270
#define JUMP_VELOCITY 275
#define TIMER_LAND 130
#define TIMER_GESTURE (34*66+50)
@ -77,6 +77,8 @@ void PM_ClipVelocity( vec3_t in, vec3_t normal, vec3_t out, float overbounce );
void PM_AddTouchEnt( int entityNum );
void PM_AddEvent( int newEvent );
qboolean PM_CanJump( void );
void PM_Jump( void );
qboolean PM_SlideMove( qboolean gravity );
void PM_StepSlideMove( qboolean gravity );

View file

@ -353,32 +353,55 @@ static void PM_SetMovementDir( void ) {
/*
=============
PM_CheckJump
PM_Jump
Applies jump velocity, event, and animation.
Extracted from PM_CheckJump so it can be called
from other contexts (step jump, double jump, etc).
=============
*/
static qboolean PM_CheckJump( void ) {
/*
=============
PM_CanJump
Returns qtrue if a jump would succeed right now.
Checks both player state AND input (upmove >= 10).
Used by PM_StepSlideMove to decide whether stepping
up stairs should trigger a jump.
=============
*/
qboolean PM_CanJump( void ) {
if ( pm->ps->pm_flags & PMF_RESPAWNED ) {
return qfalse; // don't allow jump until all buttons are up
return qfalse;
}
if ( pm->ps->pm_type != PM_NORMAL ) {
return qfalse;
}
if ( pm->cmd.upmove < 10 ) {
// not holding jump
return qfalse;
}
// must wait for jump to be released
if ( pm->ps->pm_flags & PMF_JUMP_HELD ) {
// clear upmove so cmdscale doesn't lower running speed
pm->cmd.upmove = 0;
// QL: 100ms minimum delay between jumps.
// Prevents same-frame double-fires. Step jumps launch the player
// high enough (~400ms airtime) that the next stair collision
// naturally exceeds this threshold.
if ( pm->cmd.serverTime - pm->ps->lastJumpTime < 100 ) {
return qfalse;
}
return qtrue;
}
void PM_Jump( void ) {
pml.groundPlane = qfalse; // jumping away
pml.walking = qfalse;
pm->ps->pm_flags |= PMF_JUMP_HELD;
pm->ps->groundEntityNum = ENTITYNUM_NONE;
pm->ps->velocity[2] = JUMP_VELOCITY;
pm->ps->lastJumpTime = pm->cmd.serverTime;
PM_AddEvent( EV_JUMP );
if ( pm->cmd.forwardmove >= 0 ) {
@ -388,6 +411,23 @@ static qboolean PM_CheckJump( void ) {
PM_ForceLegsAnim( LEGS_JUMPB );
pm->ps->pm_flags |= PMF_BACKWARDS_JUMP;
}
}
/*
=============
PM_CheckJump
=============
*/
static qboolean PM_CheckJump( void ) {
if ( !PM_CanJump() ) {
return qfalse;
}
// QL autohop: no PMF_JUMP_HELD gate here.
// The Pmove() outer loop forces upmove=20 when
// PMF_JUMP_HELD is set, allowing continuous bunny hopping.
PM_Jump();
return qtrue;
}

View file

@ -246,15 +246,9 @@ void PM_StepSlideMove( qboolean gravity ) {
return; // we got exactly where we wanted to go first try
}
VectorCopy(start_o, down);
down[2] -= STEPSIZE;
pm->trace (&trace, start_o, pm->mins, pm->maxs, down, pm->ps->clientNum, pm->tracemask);
VectorSet(up, 0, 0, 1);
// never step up when you still have up velocity
if ( pm->ps->velocity[2] > 0 && (trace.fraction == 1.0 ||
DotProduct(trace.plane.normal, up) < 0.7)) {
return;
}
// QL pm_airSteps: allow step-ups with upward velocity.
// Q3 blocked step-ups during jumps unless ground was directly below.
// This prevented smooth stair traversal while bunny-hopping.
VectorCopy (pm->ps->origin, down_o);
VectorCopy (pm->ps->velocity, down_v);
@ -285,9 +279,15 @@ void PM_StepSlideMove( qboolean gravity ) {
if ( !trace.allsolid ) {
VectorCopy (trace.endpos, pm->ps->origin);
}
// QL: only clip velocity to step surface when moving INTO it.
// Skip clip when velocity is already moving away (preserves
// upward momentum through stair steps during bunny-hopping).
if ( trace.fraction < 1.0 ) {
float vdotn = DotProduct( pm->ps->velocity, trace.plane.normal );
if ( vdotn < 0 || fabs( vdotn ) < 0.001f ) {
PM_ClipVelocity( pm->ps->velocity, trace.plane.normal, pm->ps->velocity, OVERCLIP );
}
}
#if 0
// if the down trace can trace back to the original position directly, don't step

View file

@ -1212,6 +1212,9 @@ typedef struct playerState_s {
int pmove_framecount; // FIXME: don't transmit over the network
int jumppad_frame;
int entityEventSequence;
// QL additions
int lastJumpTime; // serverTime of last jump, for 100ms cooldown
} playerState_t;

View file

@ -1147,7 +1147,8 @@ netField_t playerStateFields[] =
{ PSF(grapplePoint[1]), 0 },
{ PSF(grapplePoint[2]), 0 },
{ PSF(jumppad_ent), 10 },
{ PSF(loopSound), 16 }
{ PSF(loopSound), 16 },
{ PSF(lastJumpTime), 32 }
};
/*