Stamina in Counter-Strike 1.6

Player movement in Counter-Strike 1.6 is hampered with an addition of a “stamina”. The stamina is stored in pmove->fuser2. Let \(\sigma\) be the stamina. The mechanism around stamina can be described rather simply.

The stamina starts off with a value of 0, and is decreased in every player movement frame by PM_ReduceTimers, which is called by PM_PlayerMove just like in Half-Life. If \(\tau\) is the frame time, which is the inverse of frame rate, then in PM_ReduceTimers we have

$$\sigma \gets \max(0, \sigma - \tau)$$

Upon a successful jump as determined by PM_Jump, the player vertical velocity will be set to exactly 268.3282 in single precision, which is roughly \(\sqrt(2 \cdot 800 \cdot 45) = 120 \sqrt{5}\). This is no different from Half-Life. What follows immediately is the code below decompiled using Ghidra:

if (0.0 < ppVar2->fuser2) {
    ppVar2->velocity[2] = ((100.0 - (ppVar2->fuser2 / 1000.0) * 19.0) / 100.0) * 268.3282;
ppVar2->fuser2 = 1315.789;

Here, ppVar2 is simply pmove. Simplifying, we have

$$ \begin{aligned} v_z &= \left( 1 - 0.00019 \sigma \right) 120 \sqrt{5} \quad (\sigma \ge 0) \\ \sigma &= 1315.789 \end{aligned} $$

It takes some amount of time to chip away at the stamina towards zero. If another jump is made in the meantime, the jump height will be lower than normal.

The stamina has an effect not just on subsequent vertical jumping velocities, but also on ground movement. Although, it notably has no effect on air movement at all. In Ghidra’s decompiled code of PM_WalkMove at the beginning of the function, we see a computation that bears a striking resemblence:

ppVar12 = pmove;
if (0.0 < pmove->fuser2) {
    local_90 = (100.0 - (pmove->fuser2 / 1000.0) * 19.0) / 100.0;
    pmove->velocity[0] = pmove->velocity[0] * local_90;
    ppVar12->velocity[1] = local_90 * ppVar12->velocity[1];

Assuming, \(\sigma \ge 0\), this again simplifies to

$$ \begin{aligned} v_x &\gets \left( 1 - 0.00019 \sigma \right) v_x \\ v_y &\gets \left( 1 - 0.00019 \sigma \right) v_y \end{aligned} $$

It is clear that the effect of the stamina on ground movement can be summarised as the infliction of an additional geometric friction.

The Kreedz tutorial is an excellent introductory guide to advanced movement in CS 1.6. It is partly what propelled my own journey to becoming a Half-Life physics investigator years ago. However, great care must be taken in applying the explanations given in the tutorial to Half-Life physics. I have seen newcomers to Half-Life speedrunning who believed that the “standup bhop” technique has the same effect in Half-Life: by making the player jump higher. This is not the case. That a player could jump higher in CS 1.6 with this technique is thanks to the existence of the stamina mechanism.

The standup bhop technique is executed by making a jump, and ducking before reaching the ground. As a result, the player spends more in the air, buying more time for the stamina \(\sigma\) to be decremented by PM_ReduceTimers. When the next jump is made as the player reaches the ground, the \(\sigma\) value will be lower than what it would have been, had the player made a normal bunnyhop. A lower \(\sigma\) results in a higher vertical speed \(v_z\) after jumping, which translates to increased jump height.

The Kreedz tutorial is correct in saying that the standup bhop is “slower”:

Tip by chrizZo: Notice that you are usually slower with performing standup bhops only! Often a mixture between standup and normal bhops is the fastest way you can move forward by bhopping!

This is also correct in Half-Life. The reason is simple: while you have bought more time in the air, a portion of that time is spent airstrafing in the ducked state. As I have explained in the Half-Life Physics Reference, the FSU values are scaled by a factor of 0.333 in the ducked state, giving rise to significantly lower accelerations.