Compare commits

..

6 commits

Author SHA1 Message Date
ba1c1b5a60 QL step jump with master gate and projected validation
Add full QL-style PM_StepSlideMove with:
- Master gate: trace from start to final position filters micro-bumps
- Projected position validation: trace down at where player would be
  without collision, rejects walls (startsolid) and confirms stairs
- Air-step friction: 3% horizontal penalty on airborne step-ups
- PM_Jump() call when all gates pass on valid stair geometry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 20:58:11 +08:00
9b0bd3c03f Input sticky buttons fixed 2026-03-21 20:51:28 +08:00
043e3a2def QL stair traversal: conditional velocity clip + air steps
Two changes to PM_StepSlideMove that transform stair traversal:

1. Remove Q3 velocity[2]>0 gate (QL pm_airSteps): allow step-ups
   while airborne, enabling bunny-hop stair traversal.

2. Conditional velocity clip: only clip velocity to step-down surface
   when moving INTO it (dot product < 0). Skip clip when velocity is
   moving away (dot >= 0), preserving upward momentum through steps.
   This is THE key mechanic for smooth QL-style stair hopping.

Also adds 100ms jump cooldown (lastJumpTime) to prevent same-frame
double-fires during rapid step-ups.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:08:16 +08:00
3327e9680c QL movement foundation: auto-hop, PM_Jump/PM_CanJump extraction, lastJumpTime
- Extract PM_Jump from PM_CheckJump for reuse by future step jump code
- Add PM_CanJump gate function (checks respawned, pm_type, upmove)
- Remove PMF_JUMP_HELD gate for QL-style auto-hop (hold jump to bunny hop)
- Add lastJumpTime to playerState_t (networked via msg.c) for jump cooldown
- bg_slidemove.c unchanged (no step jump yet -- needs proper QL analysis)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 05:45:18 +08:00
009dc313d4 Implement QL-style auto-hop (always enabled)
Remove the PMF_JUMP_HELD gate from PM_CheckJump so players can
hold jump to bunny hop continuously without releasing between
hops. The existing Pmove() outer loop already forces upmove=20
when PMF_JUMP_HELD is set, making this the only change needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:33:41 +08:00
0602b6ad4b Extract PM_Jump from PM_CheckJump (QL-style refactor)
Separate the jump execution (velocity, event, animation) from the
gate logic (respawn check, upmove threshold, jump-held check).
This prepares the code for QL features that need to trigger jumps
from contexts other than the normal ground jump path (step jump,
double jump, etc). No behavior change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:31:49 +08:00
6 changed files with 135 additions and 82 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

@ -234,27 +234,23 @@ void PM_StepSlideMove( qboolean gravity ) {
vec3_t start_o, start_v;
vec3_t down_o, down_v;
trace_t trace;
// float down_dist, up_dist;
// vec3_t delta, delta2;
vec3_t up, down;
vec3_t projected;
float stepSize;
VectorCopy (pm->ps->origin, start_o);
VectorCopy (pm->ps->velocity, start_v);
if ( PM_SlideMove( gravity ) == 0 ) {
return; // we got exactly where we wanted to go first try
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: compute projected position (where player would be without collision)
VectorMA( start_o, pml.frametime, start_v, projected );
// 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,27 +281,29 @@ 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 ) {
PM_ClipVelocity( pm->ps->velocity, trace.plane.normal, pm->ps->velocity, OVERCLIP );
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
pm->trace( &trace, pm->ps->origin, pm->mins, pm->maxs, start_o, pm->ps->clientNum, pm->tracemask);
if ( trace.fraction == 1.0 ) {
// use the original move
VectorCopy (down_o, pm->ps->origin);
VectorCopy (down_v, pm->ps->velocity);
if ( pm->debugLevel ) {
Com_Printf("%i:bend\n", c_pmove);
}
} else
#endif
{
// use the step move
// QL master gate: trace from original position to final position.
// If the trace hits geometry (fraction < 1.0), a real step was crossed.
// On flat ground with micro-bumps, the trace is clear and this entire
// block is skipped — filtering out false positives.
pm->trace( &trace, start_o, pm->mins, pm->maxs, pm->ps->origin,
pm->ps->clientNum, pm->tracemask );
if ( trace.fraction < 1.0 ) {
float delta;
delta = pm->ps->origin[2] - start_o[2];
// step sound events
if ( delta > 2 ) {
if ( delta < 7 ) {
PM_AddEvent( EV_STEP_4 );
@ -317,6 +315,35 @@ void PM_StepSlideMove( qboolean gravity ) {
PM_AddEvent( EV_STEP_16 );
}
}
// QL air-step friction: 3% horizontal speed penalty per airborne step-up
if ( !pml.groundPlane && delta > 0 && start_v[2] > 0 ) {
pm->ps->velocity[0] *= 0.97f;
pm->ps->velocity[1] *= 0.97f;
}
// QL step jump gate: validate stair geometry at projected position.
// Traces DOWN at where the player would be without collision.
// Walls fail (startsolid), flat ground fails (delta<=0), stairs pass.
if ( delta > 0 && pm->ps->pm_type == PM_NORMAL
&& pm->waterlevel < 2 && PM_CanJump() ) {
vec3_t stepStart, stepEnd;
trace_t stepTrace;
VectorCopy( projected, stepStart );
VectorCopy( projected, stepEnd );
stepStart[2] += STEPSIZE;
stepEnd[2] -= STEPSIZE;
pm->trace( &stepTrace, stepStart, pm->mins, pm->maxs,
stepEnd, pm->ps->clientNum, pm->tracemask );
if ( !stepTrace.startsolid && !stepTrace.allsolid
&& stepTrace.plane.normal[2] >= MIN_WALK_NORMAL ) {
PM_Jump();
}
}
if ( pm->debugLevel ) {
Com_Printf("%i:stepped\n", c_pmove);
}

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 }
};
/*

View file

@ -343,13 +343,20 @@ void IN_DIMouse( int *mx, int *my ) {
DIMOUSESTATE2 state;
DWORD dwElements;
HRESULT hr;
int value;
int i, value;
static BYTE oldButtons[8];
static const int buttonKeys[5] = {
K_MOUSE1, K_MOUSE2, K_MOUSE3, K_MOUSE4, K_MOUSE5
};
if ( !g_pMouse ) {
return;
}
// fetch new events
// flush the buffered data — we only use it for wheel events.
// buttons are handled via immediate state below to avoid
// stuck keys from buffer overflow.
for (;;)
{
dwElements = 1;
@ -361,51 +368,11 @@ void IN_DIMouse( int *mx, int *my ) {
return;
}
/* Unable to read data or no data available */
if ( FAILED(hr) ) {
break;
}
if ( dwElements == 0 ) {
if ( FAILED(hr) || dwElements == 0 ) {
break;
}
switch (od.dwOfs) {
case DIMOFS_BUTTON0:
if (od.dwData & 0x80)
Sys_QueEvent( od.dwTimeStamp, SE_KEY, K_MOUSE1, qtrue, 0, NULL );
else
Sys_QueEvent( od.dwTimeStamp, SE_KEY, K_MOUSE1, qfalse, 0, NULL );
break;
case DIMOFS_BUTTON1:
if (od.dwData & 0x80)
Sys_QueEvent( od.dwTimeStamp, SE_KEY, K_MOUSE2, qtrue, 0, NULL );
else
Sys_QueEvent( od.dwTimeStamp, SE_KEY, K_MOUSE2, qfalse, 0, NULL );
break;
case DIMOFS_BUTTON2:
if (od.dwData & 0x80)
Sys_QueEvent( od.dwTimeStamp, SE_KEY, K_MOUSE3, qtrue, 0, NULL );
else
Sys_QueEvent( od.dwTimeStamp, SE_KEY, K_MOUSE3, qfalse, 0, NULL );
break;
case DIMOFS_BUTTON3:
if (od.dwData & 0x80)
Sys_QueEvent( od.dwTimeStamp, SE_KEY, K_MOUSE4, qtrue, 0, NULL );
else
Sys_QueEvent( od.dwTimeStamp, SE_KEY, K_MOUSE4, qfalse, 0, NULL );
break;
case DIMOFS_BUTTON4:
if (od.dwData & 0x80)
Sys_QueEvent( od.dwTimeStamp, SE_KEY, K_MOUSE5, qtrue, 0, NULL );
else
Sys_QueEvent( od.dwTimeStamp, SE_KEY, K_MOUSE5, qfalse, 0, NULL );
break;
case DIMOFS_Z:
value = od.dwData;
if (value < 0) {
@ -419,16 +386,29 @@ void IN_DIMouse( int *mx, int *my ) {
}
}
// read the raw delta counter and ignore
// the individual sample time / values
// read immediate device state for axes and buttons
hr = IDirectInputDevice8_GetDeviceState(g_pMouse,
sizeof(DIMOUSESTATE2), &state);
if ( FAILED(hr) ) {
*mx = *my = 0;
return;
}
*mx = state.lX;
*my = state.lY;
// generate button press/release events from immediate state.
// comparing against previous state avoids stuck buttons entirely —
// if a press or release was missed in the buffer, the immediate
// state catches it next frame.
for ( i = 0; i < 5; i++ ) {
BYTE down = state.rgbButtons[i] & 0x80;
if ( down != oldButtons[i] ) {
Sys_QueEvent( g_wv.sysMsgTime, SE_KEY, buttonKeys[i],
down ? qtrue : qfalse, 0, NULL );
oldButtons[i] = down;
}
}
}
/*