13. Weapons¶
Half-Life is a shining example of having an already stellar gameplay elevated by well designed weapons, not just from a casual gamer’s point of view, but also that of a speedrunner. In this article, we will attempt to describe the physics of all the weapons in Half-Life that may be relevant to speedrunning. The physics of the various weapons are varied and interesting, with complexities ranging from those requiring only the simplest of descriptions to those demanding a level of research rivalling the player movement physics.
13.1. Bullet spread¶
The bullets of some weapons in Half-Life have a spread that cause them to deviate from the target slightly. The amount of deviation depends on the weapon. The deviation appears to be random and unpredictable to a casual player. However, bullets fired by the player rely only on the shared RNG, which we have concluded in Shared RNG that it is wholly predictable and non-random. In particular, if in a TAS the player loads from a savestate and fires a weapon with bullet spread in some fixed -th frame, then the spread of the bullets will not change if the savestate is loaded again and the same actions are repeated.
The spread of the bullets when fired by the player is computed in CBaseEntity::FireBulletsPlayer
. One of the arguments to this function is the number of bullets . For example, the shotgun (see Shotgun) in primary mode would pass 6 for this argument. In all other weapons that have bullet spreads, 1 is always passed in, because those weapons fire one bullet at a given time. This function also accepts several other obvious arguments. Of concern to us are the spread vector , the source of line trace , the aiming direction , the range of the weapon, the bullet type, and the shared RNG “seed” .
This function uses the multidamage mechanism described in Damage system. At a higher level, the function begins by doing a clear on the currently accumulated damage and sets the accumulated damage type to DMG_BULLET | DMG_NEVERGIB
. It then enters a loop that spreads and adds or accumulates the damage that will be inflicted due to each of the bullets. Once it exits the loop, it does an apply operation. The effect of these is that the damage will usually be accumulated and then actually inflicted, rather than inflicted for each bullet.
The loop is the meat of the bullet spread physics. Let be the loop counter such that in the first iteration, in the second iteration, and so on. At the start of an iteration, it computes the deviation multipliers and in the horizontal and vertical directions respectively. Mathematically, these are computed as such:
Then, if and are the player’s unit right and up vectors (see View vectors), then it traces a line ignoring the player entity from the source to the point given by
If this line trace hits nothing, then nothing important is done. If this line hits an entity, then the TraceAttack
of the hit entity will be called with an amount of damage depending on the bullet type argument passed to this function. The TraceAttack
typically performs the add multidamage operation. The reader is encouraged to read the SDK code for more details.
13.1.1. Actual bullet range¶
In (13.2), we observe that distance from the source to the end point is not , the intended bullet range. We can see this because the direction vector is not normalised before being scaled up by . If we assume the player roll angle is 0 and , both of which are true for the vast majority of the time, then it can be shown that the range of a bullet is actually
rather than just . Though this error is very small and unnoticeable in practice. In the subsequent descriptions of the various weapons in this chapter, when we claim that a weapon has a bullet spread and a range of some value of , keep in mind that the actual range of the bullet is slightly longer depending on the spread. Weapons that do not have a bullet spread are not affected by the error described in this section.
13.1.2. Distribution¶
In (13.1), if we ignored the deeply flawed randomness of , then we can immediately see that and . In addition, both and are drawn from a triangular distribution (rather than a gaussian distribution claimed by the comments in the code), the PDF of which may be given by
However, due to the non-randomness of , and the fact that the values of the first argument provided to are deterministic and predictable, there are at most only 256 possible combinations of . This further implies that there are only at most 256 possible bullet spread patterns.
We also observe that the spread of the bullets is square rather than circular. In other words, if is truly random and enough bullets have been fired at a wall, then the bullet markings on the wall would form a square rather than a circle. This is illustrated in Fig. 13.1.. Notice that two of the pellets lie outside the circle, proving that bullet spreads are not confined within it. The deviation of bullets in each of the horizontal and vertical directions is independent. We can see this easily because is false.
13.1.3. Meaning of ¶
The vector is referred to as the spread vector above. The way this vector is named and defined in the SDK implies that each element in the vector is where is the intended maximum side-to-side angle of deviation. In Fig. 13.2., the angle and therefore the length of . The SDK defines a few constants for that are used by the various weapons. For example, the MP5 (see MP5) uses the following as its :
#define VECTOR_CONE_6DEGREES Vector( 0.05234, 0.05234, 0.05234 )
Indeed, .
However, if we look more closely at (13.2), we see that the actual maximum angle of deviation is not exactly 6 degrees, for two reasons. Firstly, as explained in Distribution, the bullets spread in a square rather than a circle, so the angle of deviation from the centre is not constant. Even if we consider just the horizontal and the vertical angles of deviation, the actual angle differs from the intended angle because the method of obtaining the values defined in the SDK is incorrect given how those values are then used in (13.2). Specifically, (13.2) makes the maximum deviation to be the the line with angle . Using the MP5 as an example, the actual angle of deviation is which is smaller than intended.
In general, the actual angle of deviation is always slightly smaller than the intended angle . We can see this easily by expanding and to two terms in their respective Maclaurin series:
Admittedly, the difference is very small thanks to the small angle approximations in radians.
13.2. Quick weapon switching¶
When the player switches to any weapon in Half-Life, the weapon imposes a delay before any attack or reload is permitted. This delay may be called the switching delay, and is independent of the attack cycle time or delay between shots, except for the unique case of Gauss described in Gauss rapid fire. When the player switches to a weapon, CBasePlayer::SelectItem
or CBasePlayerWeapon::SelectLastItem
will be called. Both of these functions call the Deploy
virtual method of the selected item. In all weapons of Half-Life, the CBasePlayerWeapon::DefaultDeploy
is eventually called, whether or not the Deploy
function is overridden by the weapon derived class. In CBasePlayerWeapon::DefaultDeploy
, the switching delay is set to be half a second:
m_pPlayer->m_flNextAttack = UTIL_WeaponTimeBase() + 0.5;
This delay is enforced in CBasePlayer::ItemPostFrame
:
#if defined( CLIENT_WEAPONS )
if ( m_flNextAttack > 0 )
#else
if ( gpGlobals->time < m_flNextAttack )
#endif
{
return;
}
ImpulseCommands();
if (!m_pActiveItem)
return;
m_pActiveItem->ItemPostFrame( );
The CLIENT_WEAPONS
is always defined in the compiler flags. Therefore, as long as the countdown is not up, m_pActiveItem->ItemPostFrame
will not be called, which in turn prevents the reload and attack methods of the weapon being called.
The technique of quick weapon switching eliminates the switching delay completely. This technique is also known as “fastfire” in the community, which frequently creates confusion with gauss rapid fire (Gauss rapid fire) and thus must be avoided. To eliminate the switching delay, we simply perform a saveload immediately upon switching to a weapon. Examining CBaseMonster::m_SaveData
in monsters.cpp
, we do see CBaseMonster::m_flNextAttack
being saved, which is inherited by the CBasePlayer
derived class:
DEFINE_FIELD( CBaseMonster, m_flNextAttack, FIELD_TIME ),
When a game is loaded, the field is restored properly. However, in the overridden CBasePlayer::Restore
, we see the following at the end of the method:
#if defined( CLIENT_WEAPONS )
// HACK: This variable is saved/restored in CBaseMonster as a time variable, but we're using it
// as just a counter. Ideally, this needs its own variable that's saved as a plain float.
// Barring that, we clear it out here instead of using the incorrect restored time value.
m_flNextAttack = UTIL_WeaponTimeBase();
#endif
Since CLIENT_WEAPONS
is defined, UTIL_WeaponTimeBase
simply returns 0. This effectively resets CBasePlayer::m_flNextAttack
, bypassing the check in CBasePlayer::ItemPostFrame
described above.
Note that picking up a weapon also incurs the same delay, though the mechanism by which this happens is slightly different: CBasePlayer::SwitchWeapon
is called instead, which in turn also calls the Deploy
virtual method of the new weapon. The subsequent logic follows the descriptions above.
A typical attempt at explaining the quick weapon switch, commonly seen even as of 2020, is to claim that the saveload eliminates the weapon switching animation. This explanation is incorrect, because the delay before being allowed to fire, or indeed any other weapon-related delays including attacks and reloads, are independent of the viewmodel animation. The delays are “invisible”.
13.3. Gauss¶
The gauss is one of the more interesting weapons in Half-Life. One of the earliest speedrunning tricks, the simple gauss boosting, and one of the most recently discovered tricks, quickgaussing, are both gauss-related. The gauss is arguably the most complex weapon in Half-Life, and it is worth devoting some paragraphs exploring its behaviour.
The gauss weapon has two modes, as any entry level Half-Life player would know. The primary mode of the gauss fires orange beams consuming 2 cells and providing 20 damage. The delay between shots in primary mode is 0.2s. The secondary mode is more interesting. In single-player mode, the weapon consumes a cell every 0.3s while charging, starting from an initial consumption of one cell. The minimum charge time is 0.5s. If the weapon charges for more than 10 seconds, the weapon will discharge and inflict 50 damage onto the player. The damage of the beam in secondary mode otherwise scales linearly with charging time such that
Observe that the damage caps at 200 after charging for 4 seconds. The secondary fire produces a physical recoil, which is manifested by a change in the player velocity. If is the current player velocity, and is the unit forward vector, then the new velocity is given by
where refers to the diagonal matrix with 1, 1, and 0 as the entries on the diagonal. Observe that the vertical velocity never changes. To maximise horizontal speed change, the player must fire from zero pitch, which gives the maximum speed change of 1000 ups, one of the greatest speed boosts available in Half-Life. The gauss cannot be fired when the waterlevel is 3, and the gunfire volume is always 450 regardless of the mode.
The behaviour of gauss beams is relatively complex compared to other Half-Life projectiles. Intuitively, players recognise that gauss beams have a tendency to reflect off surfaces. Gauss beams can also “punch” through walls and damage entities at the opposite side, through a mechanism that is not obvious at first sight. Gauss beams can even inflict damage onto oneself under the right circumstances. What is more, the damage from gauss beams can be seemingly amplified when fired through particular geometries.
A detailed and complete understanding of the gauss behaviour can be achieved by reading the code in dlls/gauss.cpp
in the Half-Life SDK. Rather than listing the code and tracing through line by line here, we shall explore the weapon by examples of its exploits.
13.3.1. Beam iterations¶
A gauss beam starts off with an initial damage and hit counter nMaxHits
with an initial value of 10. The game then perform at most 10 iterations (tracked by nMaxHits
which is decremented each iteration) to to calculate the dynamic behaviour of the gauss beam. In each iteration, the game may reduce the damage, inflict damage to some entity, or calculate beam reflection. If or nMaxHits
reaches zero at some iteration , then the beam will stop iterating. This implies that the damage is akin to the “energy” of the beam.
Crucially, the game also keeps track of the ignored entity, or pentIgnore
in the code. This ignored entity is initialised to be the player entity before the iterations begin. The ignored entity is the entity that will be ignored by any line tracing, and can be changed from time to time as we will see shortly. What entity is or is not being ignored plays a huge role in some of the common exploits of this weapon.
In performing line traces, the game also keeps track of , represented by vecDir
in the code. You can think of this vector as representing the direction of a the gauss beam in any iteration. This direction vector is initialised to be the player’s unit forward vector . It can be modified if the beam reflects, as we will see below.
Roughly speaking, in any iteration , the game begins by tracing a line from the player gun position to 8192 units away in the direction of , namely with the end point of . This line may or may not hit some entity. If no entity is hit, the iterations simply stop. Otherwise, the behaviour of the beam will depend on whether this entity is gauss reflectable (GR) or non-gauss reflectable (non-GR). For our purposes, we can say that an entity is gauss reflectable if it cannot receive damage, and vice versa.
Note
The game graphics will sometimes show a gauss beam seemingly reflecting off a non-gauss reflectable entity. This does not contradict what we described here: what you see is purely a client side visual artefact, and no reflection actually occurred on the server side. You can check this by placing an NPC in the path of the apparent reflected beam, and observe that the reflected beam does not damage the NPC.
Denote the point of intersection of the tracing line and the hit entity. If the hit entity is non-GR, then the game will simply set the trace origin of the next iteration . This implies that the next trace origin is not exactly at the point of intersection, but displaced 1 unit away. Also, the game will set the pentIgnore
to be the hit entity. This causes the line trace in the next iteration to ignore this entity. The game now proceeds to the next iteration.
On the other hand, if the hit entity is GR, then the beam behaviour is more complex. The game first sets the ignored entity pentIgnore
to null, which causes the subsequent line traces in this iteration to not ignore anything. Then, the game decides whether to reflect the beam. If is the plane normal of the hit surface and is the current beam direction vector, then the game calculates the component of the beam in the direction of the plane normal, that is . This quantity can also be thought of this way: if is the angle of incidence (the shortest angle between the beam and the plane normal), then this quantity equals .
A gauss beam can reflect only if , which implies an angle of incidence larger than 60 degrees. If the beam reflects, the game will set the beam direction to be . This new direction represents the direction of the reflected beam. Observe that the angle of reflection is the same as the angle of incidence, because
Then the game will set for the next iteration. Notice that the line trace source for the next iteration is displaced 8 units away from the point of reflection. Following this, the game will create an explosion with the origin at and a source damage of (see Explosions). This implies that the larger the angle of incidence, the lower the blast damage. Finally, the game calculates the damage for the next iteration. The game then proceeds to the next iteration.
On the other hand, if the beam cannot reflect, which implies an angle of incidence of less than 60 degrees, then the subsequent beam behaviour is the most complex. The game first checks if the beam has reached this point in the previous iterations. If so, the iterations will simply stop. Otherwise, now that the beam has just reached this point in the code, that check will fail for subsequent iterations. In addition, if this gauss beam is fired from the primary mode, then the game will also stop the iterations.
The game traces a line from to . Note that always lies on the line from to . If the trace result is all solid, the game moves on to the next iteration. Otherwise, set the tracing point of intersection be . Now, the game attempts to trace backwards by tracing from to . Set the tracing point of intersection be . Now, denote
If , the game moves on to the next iteration. Otherwise, if set . The game now calculates . With this new damage, the game then creates an explosion with the origin at and source damage . Finally, the game sets .
13.3.2. Simple gauss boost¶
One of the simplest tricks for the gauss weapon is simple gauss boosting. Namely, speed boosting from the recoil of firing the secondary attack. The simple gauss boost remains one of the most versatile boosting methods in Half-Life, and is used to great effects in a typical Half-Life speedrun. Unfortunately, the gauss being an endgame weapon is manifested by the fact that its ammo supply is relatively constrained. Good ammo planning must be done to maximise the effectiveness of this weapon.
A common task in speedrunning is to get from one point to another in the shortest time possible using simple gauss boosting. From (13.3) we know that, assuming zero pitch, the horizontal speed boost is proportional to the charging time. On the other hand, the minimum charging time is 0.5 seconds. What is the optimal way to perform gauss boosts? Or, what is the boosting scheme for maximising the average speed (total distance travelled in a given time)? Should one repeatedly charge the gauss for 1s and firing, or repeatedly charge the gauss for 0.5s and firing? There is a simple way to answer this.
Suppose the player is stationary. At , the player begins charging and after an interval , the player releases the charge. Immediately after the release, the player begins charging again for , and so on. From Fig. 13.5. we observe that the smaller the boosting interval, the closer the graph approximates the continuous boosting line, which is a theoretical case of . More importantly, observe that the area under the discrete graph also increases with decreasing , up to a maximum corresponding to the continuous case. Given that the minimum is half a second, we conclude that the optimal boosting scheme is to charge for half a second a firing, repeatedly.
If it is possible to pre-charge the gauss weapon before timing a particular run, then it is still beneficial to charge the weapon as much as possible and then release the charge at . This gives a higher initial speed in Fig. 13.5. instead of 0 as shown, which effectively shifts the graphs upwards and increasing the area under the graphs.
13.3.3. Quickgauss¶
Quickgauss is a special kind of speed boosting similar to the simple gauss boost, except a save/load is done while the gauss is charging. When the charge is released after a game load, the result is similar to releasing a secondary charge. In particular, the resulting speed boost and damage are the maximum attainable from the weapon, that is 1000 ups and 200 damage, while consuming only one cell and a charging time of half a second. This provides one of the highest accelerations from a single weapon achievable in game (2000 ups/s), and also one of the highest damage rates (400 dps).
Unfortunately, quickgauss only works in versions of Half-Life with the bunnyhop cap. This is because in these versions of Half-Life, in dlls/weapons.cpp
the following lines define the states of the gauss weapon to be stored in the savestate:
TYPEDESCRIPTION CGauss::m_SaveData[] =
{
DEFINE_FIELD( CGauss, m_fInAttack, FIELD_INTEGER ),
// DEFINE_FIELD( CGauss, m_flStartCharge, FIELD_TIME ),
// DEFINE_FIELD( CGauss, m_flPlayAftershock, FIELD_TIME ),
// DEFINE_FIELD( CGauss, m_flNextAmmoBurn, FIELD_TIME ),
DEFINE_FIELD( CGauss, m_fPrimaryFire, FIELD_BOOLEAN ),
};
IMPLEMENT_SAVERESTORE( CGauss, CBasePlayerWeapon );
Notice how the highlighted line is inexplicably commented out, so that m_flStartCharge
does not get saved to the savestate. When the game is loaded from a save, this field will be zero. The calculation of current charging time is done by gpGlobals->time - m_pPlayer->m_flStartCharge
throughout dlls/gauss.cpp
, with gpGlobals->time
being much larger than 4 in most cases. If m_flStartCharge
is zero, the calculated charging time will be much larger than 4, and thus clamped to 4 and giving the maximum damage and recoil. On the other hand, the consumption of cells while charging is done periodically over a real passage of time, rather than calculated from the charging time. Since the real charging time is only half a second, only one cell is consumed in total.
It should be immediately apparent that quickgaussing is very powerful. If quickgauss is available in a particular version of Half-Life, the runner will almost never need to use the simple gauss boosting, as quickgauss is so ammo efficient. In the rare cases where it is desirable to perform a boost less than the maximum attainable, the runner can pitch up or down accordingly when firing to reduce the boost amount, according to (13.3).
13.3.4. Entity piercing¶
When a beam hits a non-gauss-reflectable entity, which implies it is damageable, the beam will first inflict damage onto the entity. Then, in the next iteration, the beam will ignore that entity while continue tracing forward. This results in the beam passing right through the entity untouched, and hitting some other entity instead. It is also important to note the beam does not lose energy when it passes through non-GR entities. Understanding entity piercing allows the runner to save time and ammo by reducing the number of shots required to damage multiple targets.
13.3.5. Doublegauss¶
Doublegauss refers to the technique in which a gauss beam hits a non-gauss-reflectable target entity, reflects off a gauss-reflectable entity beyond the first entity, and hits the target entity again with the reflected beam. As described in Beam iterations, a beam reflection is always accompanied by a radius damage created at the point of reflection. Using this technique, this radius damage is normally also inflicted onto the target entity. Typically, the target entity receives three damage inflictions overall. Though inconspicuous and seemingly unremarkable when executed in a speedrun, doublegauss is the secret to getting nearly double (with some caveats explained below) the damage out of a single shot for free, whether in primary or secondary mode.
Let be the initial damage. In Entity piercing, we explained that when a gauss beam hits a non-gauss-reflectable entity, it will inflict onto the entity. In Fig. 13.7., this initial beam is represented by the , and therefore the damage infliction point is . In the next beam iteration, the beam will ignore the target entity and pass right through it as line . 1 Suppose the beam subsequently hits a gauss-reflectable entity at , such as the ground or a wall. If the angle of incidence is greater than 60 degrees, which is a necessary condition for the beam to reflect, the beam will reflect off the GR entity, as explained in Beam iterations.
Recall that if the angle of incidence is , then the radius damage created from the reflection is . This radius damage will be inflicted onto the target entity with the explosion origin at . The actual damage inflicted depends on how far away the target entity is from as described in General physics. In most cases, the target entity is in contact with the GR entity. For instance, the target entity could be a damageable crate (func_breakable) which is almost always resting and in contact with the ground in Half-Life maps. In such cases, the distance of the target entity from will be zero, causing the full radius damage to be inflicted onto it.
At the end of the second iteration, the gauss beam will no longer ignore the target entity stuck in the first iteration. The reflected beam will hit the target entity again at point (point blank) in the third iteration, though with a reduced damage of . Observe that because is 8 units away from , it is possible for to be positioned beyond the target entity and missing it, resulting in a bypass (Reflection bypass). Assuming is inside the target entity, then the damage inflictions onto the target are shown in the table below.
Iteration |
Damage |
---|---|
First |
|
Second |
|
Third |
The total damage inflicted onto the target non-GR entity is simply the sum of all damages, which has a maximum of . Observe that the maximum possible damage is independent of the angle of incidence .
In the above analysis, we ignored what could happen next for the beam . In reality, this beam could carry on to hit other entities, and even triggering subsequent doublegausses. Let be initial damage and be the angle of incidence of the first doublegauss. In the first doublegauss, the maximum potential damage inflicted is and the remaining beam damage is . In the second doublegauss, the maximum potential damage inflicted is therefore and the remaining beam damage is . In general, the maximum potential damage inflicted by the -th doublegauss is simply and the remaining damage is
Therefore, the total maximum potential damage inflicted by all of the doublegauss executions is
Of academic note, as each of tends towards , the total damage tends towards . Therefore, at least purely mathematically, we could have infinite total damage inflicted by a single shot. Examining Fig. 13.4. more closely, however, reveals that the maximum number of beam iterations is . A quickgauss (Quickgauss) gives , which translates to the maximum total damage by a single shot of gauss as 4000, when combined with the doublegauss technique and the precise arrangement of entities in a map.
13.3.6. Entity punch¶
As explained in Beam iterations, a secondary gauss beam can punch through a GR entity if it does not meet the criterion for reflection. The damage produced by an entity punch is dependent on . This quantity is typically the thickness of the wall, but this is not always the case. In particular, the position is found by tracing a line from the inside an entity. It is not always obvious what the line can “see” and end up under these circumstances. Sometimes, the trace line can ignore (unrelated to pentIgnore
) and not able to detect certain entities, and therefore the line tracing may end at a further point, skipping these undetected entities along the way. And sometimes, if the thickness of an entity is too high, the game may wrongly determine that the trace is all solid.
If the line tracings went well, the game will create an explosion 8 units away from the exit point. The thinner the walls or entities (barring the caveats above), the higher the explosive damage. Since the explosion origin is displaced from the exit surface, it is possible for the origin to be located inside some other entity, thus causes nuking (see Nuking and headshots). In general, entity punching can be very powerful. With a full gauss charge, the player can instantly create explosions of a maximum of 200 source damage, outclassing most explosive weapons.
13.3.7. Reflection boost¶
Reflection boost refers to boosting which involves a reflection of the gauss beam. There are two variants of gauss reflect boost: ducking and standing. Reflection boosts can be used to provide vertical boost, which is not possible with a normal gauss boost in single player. The vertical boost is provided by means of self-inflicted damage, which can be costly to player health.
The ducking reflect boost sequence is as follows.
Start charging for quickgauss
Duck on the ground
Pitch to 30 degrees downward
Jump just before firing
Save/load for quickgauss
The beam should be reflected off the ground, at a 60 degrees angle of incidence. This provides the player a 866 ups horizontal boost and a respectable vertical boost. The sequence demands high precision to produce the desired effects.
The standing reflect boost uses explosive damage from beam reflection as well. However, the standing reflect boost sequence requires even higher precision to execute.
Start charging for quickgauss
Stand touching a wall
Pitch to 60 degrees downward
Yaw to look perpendicularly into the wall
Offset the yaw slightly to the left or right by about 1 to 2 degrees
Duck and jump simultaneously just before firing
Save/load for quickgauss
The result, however, is respectable.
13.3.8. Selfgauss¶
Selfgauss is a very well known trick, but probably one of the least understood among speedrunners. Selfgaussing is the result of the beam hitting the player as it is being fired out of the imaginary gun barrel, or specifically the player’s gun position. This is due to the origin of the line tracing being inside the player model. An analogy from the real world would be firing a pistol from inside one’s body, so that the bullet hits the player’s internals point blank. The outcome is a perfectly vertical speed boost, as the inflictor origin and the player origin coincides, thus giving a perfectly upward vector (see Damage boosting).
Caution
It is a common misconception that selfgauss occurs because the beam somehow “reflects” backwards onto the player after hitting a wall. It is easy to see that this is a wrong explanation, because the beam cannot reflect when the angle of incidence is less than 60 degrees, and the gauss beam cannot reverse its damage inflicting direction.
In the first iteration, the gauss beam will ignore the player, because pentIgnore
is set the be the player entity, as explained in Beam iterations. Selfgauss will only work in the next iteration if pentIgnore
is set to null, and . Therefore, selfgauss cannot happen if the beam strikes a non-gauss reflectable entity, for it modifies in the next iteration. Selfgauss cannot happen if the beam reflects, as reflections change as well.
Suppose when the player fires the gauss in secondary mode, the beam first strikes some entity at a sufficiently small angle of incidence so that the beam does not reflect. Assuming this entity is gauss reflectable, the game will perform two traces to determine the distance between the “exit point” and the entry point. This distance is denoted as . Selfgauss will only work if is less than the numerical damage of the beam. If the opposite is true, then will be modified, preventing selfgauss. This implies that higher is more desirable as it allows for selfgaussing with a greater damage, and thus producing greater boosts. The same caveat with regards to the meaning of should be applied, as explained in Entity punch. Namely, while it commonly is found to be the thickness of the entity the beam is hitting, this is not always the case. It is not always easy to tell at first sight what might be for a given geometry and terrain.
To perform selfgauss in practice, there are a few notes to keep in mind. Recall from Hitgroup that attacks that trace to the player’s head will deal three times the original damage. To save ammo, it is desirable to headshot the player while selfgaussing, giving a higher speed boost to ammo ratio. In addition, it is desirable to jump immediately before selfgaussing, as jumping provides an important initial vertical speed that can save health and ammo. However, note that a simple jump may not work. Recall from Duckjump that when the player jumps, the player model plays the jumping animation, which changes the model geometry (and thus the hitboxes’ positions) considerably. This can prevent headshotting even when the beam is fired from the same viewangles without jumping. The solution is to duck and jump, which prevents the jumping animation from playing.
13.3.9. Entity selfgauss¶
Entity selfgaussing is a way of doubling the damage of a secondary gauss attack using the same number of cells and charge time. Entity selfgaussing works very similarly to selfgauss (Selfgauss). The only difference is that, in the first beam iteration, the beam should hit the target entity which must be non-GR. As a result, the first damage will be inflicted and will be calculated to be inside the target entity. The rest of the mechanism work exactly the same as that of selfgauss, except the trace origins are inside the target entity rather than the inside the player entity. Specifically, the beam will ignore the target entity in the second iteration and inflict a second unattenuated damage onto the entity in the third iteration. This implies that the conditions for triggering entity selfgauss are the same as selfgauss as though the target entity were not there.
13.3.10. Gauss rapid fire¶
When firing the gauss in the primary mode, there is a delay 0.2s between shots, similar to how other weapons behave. However, unlike other weapons in Half-Life, if a saveload is performed immediately after a primary fire, this firing delay will be eliminated entirely. Consequently, it is possible to fire the gauss at a dramatic rate, dishing out an extreme damage rate. For instance, each primary fire deals 20 damage. At 1000 fps, it is possible to fire the weapon at a rate of 1000 times per second, for a total of 50 shots (recall that each primary fire consumes 2 out of 100 cells). This results in an impressive 1000 damage in just 0.05 seconds. The downside, of course, is the dramatic ammo consumption. This technique is sometimes confusingly referred to as “fastfire” in the community, which must be avoided to prevent confusion with quick weapon switching (see Quick weapon switching).
The delay between primary fire in gauss is implemented by setting CBasePlayer::m_flNextAttack
, which is unlike other weapons where CBasePlayerWeapon:::m_flNextPrimaryAttack
is set instead. As described in Quick weapon switching, the value of CBasePlayer::m_flNextAttack
is always reset after a restore, which eliminates the switching delay of all weapons and the cycle delay in gauss.
Gauss rapid fire is useful in situations where gibbing damageable entities as quick as possible is of utmost consideration, thanks to the primary fire’s ability to gib corpses. For example, clearing monsters in a narrow pathway which obstruct the runner’s path. The runner should always headshot monsters if possible to obtain a threefold damage increase. The weapon should be continuously fired even as the corpse is slowly falling after the monster is dead.
13.3.11. Reflection bypass¶
The reflection bypass refers to a method of causing the gauss beam to reflect and bypass a solid obstruction. Unlike the traditional way of punching through a wall using the secondary attack, this method relies on shooting very close to an edge so that the origin of the reflected beam at some iteration is outside the obstructing entity. This works because the origin of the reflected beam is 8 units away from in the direction of the reflected vector . This 8 units skip in space allows bypassing any entity of any type of sufficient thinness. This trick works on both GR and non-GR entities, and for both primary and secondary shots.
This trick is useful for getting the beam to hit some entity on the other side of some thin obstruction with less than 8 units thickness. Although 8 units thin entities are relatively rare in Half-Life, it is not unusual to find them from time to time. The downside of this trick is that the beam loses some damage after reflection.
13.4. Hornet gun¶
The hornets created in primary fire has initial velocity
where is the player’s unit forward vector. Hornets created in second fire has initial velocity
In both cases, the initial velocity is independent of the player velocity.
TODO
TODO
13.5. Gluon gun¶
The gluon gun, also known as the egon gun, is a powerful weapon when measured by the damage rate, especially in casual gameplay. Its damage rate is so high that it can obliterate almost every damageable entity in mere moments. It also does not require a particularly good aim or set up to use effectively. In speedrunning, however, weapon switching, the gauss (Gauss), and precise explosive placements almost always deliver better damage rate in short bursts.
The gluon gun only fires in the primary mode. It cannot be fired when the waterlevel is 3. Like the gauss, when fired it produces a sound of volume 450. When +attack
is issued, the gluon gun initialises a damage countdown of 0.1s. If +attack
is still active after 0.1s, the first damage will be inflicted onto whatever damageable entity is in aim. A line is traced from the player’s gun position to 2048 units in the direction of until an entity is hit. This implies the gluon gun only has a range of 2048 units. In addition, an ammo cell is consumed simultaneously with the damage. After the first damage infliction and cell consumption, a countdown to the next damage of 0.1s is restarted. In the default game settings, 14 damage of type DMG_ENERGYBEAM | DMG_ALWAYSGIB
is inflicted in the first and subsequent damages. This cycle continues until the attack is interrupted. If is the Dirac delta function, then the damage inflicted at time may be written as
Overall, the damage rate is 140 per second and the ammo consumption rate is 10 per second. When the attack stops, it takes 0.5s to restart it.
13.6. Tripmine¶
The tripmine is a high explosive that proved to be useful in speedrunning for damage boosting. As a weapon, it only admits the primary attack. The weapon first traces a line from the player’s gun position to 128 units ahead in the direction of the player’s unit forward vector . If the line hits an entity, and if the entity is not a conveyor (i.e. does not have the FL_CONVEYOR
flag set), then the weapon will create a monster_tripmine
entity on the target surface. Specifically, if is the point hit by the trace line and is the plane normal, then the origin of the tripmine entity is set to be . Regardless of whether the trace line hits an entity though, the next primary attack will only be allowed 0.3s later.
The tripmine entity has very low health of 1, a unit forward vector , and a movetype of MOVETYPE_FLY
, which ignores gravity. Initially, it does not collide with any entity due to being a SOLID_NOT
. This makes the tripmine impossible to be hit by any line trace and therefore any weapon, except for explosions. On spawn, it waits for 0.2s, then initiates the power up process. At a high level, this process involves the tripmine entity trying to find the entity it is attached to (hereinafter known as the host entity, the tripmine being the “parasite”) and making sure that entity does not change its origin position and the angles. In the beginning, it does not know the host entity. It tries to find it by tracing a line from to , where is the tripmine origin and is the tripmine’s unit forward vector. Then there are three cases that follows:
- hit its owner (usually the player) or started in solid
extends the process by another 0.1s
- hit an entity other than its owner
sets this entity as the host entity and remembers its origin and angles
- did not hit anything
remove the tripmine from the world after 0.1s
Assuming the tripmine found the host entity. In subsequent iterations, it tries to make sure the host entity has not moved or rotated since. If the host entity does move or rotate, then the tripmine will simulate “dropping” itself by creating a weapon_tripmine
entity with the origin of and set to remove itself after 0.1s. As long as the host entity stays put the whole time, the powering up process will eventually complete. The time it takes to complete the powering up process depends on the spawn flags of the tripmine. If the spawn flag has bit 1 set, then the powering up process is set to complete in one second (unless extended due to the first case above when finding the host entity). On the other hand, if bit 1 is not set, the process will complete in 2.5 seconds. Once the power up process completes, the tripmine entity will become a solid SOLID_BBOX
and the characteristic laser beam will be activated.
To activate the beam, the tripmine first traces a line from its origin to . It then remembers the unobstructed trace fraction of this line. After 0.1s, the beam will be activated. While it is active, it checks for any entity crossing its path once every 0.1s. The low frequency allows a fast moving entity to move past the beam without detonating the tripmine, a fact exploited in many speedruns. It does the check by tracing a line from its origin at to , similar to trace involved in the beam activation. If the unobstructed trace fraction differs from that obtained from the beam activation by more than 0.001, then the tripmine will kill itself, which causes it to explode after a brief time (explained below). Even if the trace fraction remains the same, if the host entity has somehow become null, or if the host entity moved or rotated, the tripmine will also kill itself. And of course, since the tripmine entity is damageable, it can be killed by conventional means.
When the tripmine entity is killed, it initiates a delay before it actually explodes and inflicts a radius damage to the surrounding entities. This delay is randomised using the non-shared RNG (see Non-shared RNG) by picking a random number with for some state . When it is finally time to explode, the physics governing the rest follows the description in Tripmines. The source damage of the tripmine in the default game settings is 150, similar to the satchel charge (Satchel charge).
13.6.1. Skipping beams¶
As mentioned above, it is possible to skip the tripmine beam if the body moving across it has a sufficiently high speed. This is thanks to the beam meaning to check for entities obstructing its path only once every 0.1s. One way to decrease the actual checking frequency is by changing the frame rate to a very low value. If this is undesirable, then 0.1s is still a sufficiently large window to allow skips. Denote the cross sectional length of the body cut by the beam. Then, we can see that a minimum speed of
is needed to skip a beam. For example, the player’s cross sectional width is approximately 20. Then a minimum speed of is needed.
In general, it may be tedious or impossible to predict the actual instances of time where the beam checks for entities. In particular, the -th checking is done at time for some unknown . Nonetheless, we can compute the probability of an object of cross sectional length with cross sectional speed getting checked by the beam, assuming that is uniformly distributed. This is simply
13.7. Hand grenade¶
The handgrenade is one of the most useful weapons for damage boosting in Half-Life. It is versatile and can be used in many situations. However, making a handgrenade land and explode at the right location can be tricky due to its bouncy nature and the delayed detonation.
The handgrenade experiences entity gravity and entity friction
, and moves with type MOVETYPE_BOUNCE
. As a result, the
handgrenade experiences only half of the gravity experienced by the player. In
addition, recall from Collision that, if the entity friction is not 1,
then a MOVETYPE_BOUNCE
entity has bounce coefficient ,
which, in the case of the handgrenade, is numerically . This is
why a handgrenade bounces off surfaces unlike other entities.
Interestingly, the initial speed and direction of the grenade when it is tossed depend on the player pitch in a subtle way. For example, when (i.e. the player is facing straight down) the initial speed and direction are and respectively. However, when the initial speed and direction now become and respectively. Another notable aspect of handgrenades is that its initial velocity depends on the player velocity at the instant of throwing. This is unlike MP5 grenades.
In general, we can describe the initial velocity and direction of handgrenades in the following way. Assuming all angles are in degrees. First of all, the player pitch will be clamped within . Let be the handgrenade’s initial pitch, then we have
And if is the current player velocity, is the grenade’s initial velocity, and is the unit forward vector computed using and player’s , then
To visualise this equation, we plotted Fig. 13.12. which depicts how the handgrenade’s relative horizontal speed and vertical velocities vary with player pitch.
From Fig. 13.12., we can make a few observations to understand the handgrenade throwing angles better. Firstly, player pitch within , the curve is a circular arc. This is because the relative speed of the full 3D relative velocity vector is exactly , since in this range . Player pitch beyond the non-smooth point at corresponds to a less trivial curve, however, as the relative speed itself varies with the pitch. A second observation we can make is that, for the vast majority of player pitch, the relative vertical velocity is positive or pointing upward. There exist some pitch angles that result in downward vertical velocity, and these angles may be useful under certain circumstances. A third observation is that, there is a difference between throwing backwards by rotating by 180 degrees and keeping the same, versus keeping the same and setting . For example, although the player’s unit forward vector is exactly the same when and , and when and , observe that the throw velocity is quite different. Indeed, by having we obtain the maximum possible horizontal throwing velocity not attainable with the “normal” player pitch range in . A fourth observation is that, assuming the player pitch lies within , the relative horizontal velocity is invariant under the transformation . For example, the relative horizontal velocity at and is equal.
13.8. Glock¶
The glock 2, also known as the 9mm handgun, is the first hitscan ranged weapon acquired by the player in Half-Life. It does not see much use in speedruns once more powerful alternatives are acquired, owing to its relatively slow firing rate and low damage. Nevertheless, it can be used in a combination of weapons for quick weapon switching, offering 8 damage of type DMG_BULLET
. The volume of the gunfire is 600. Like most hitscan weapons in Half-Life, glock’s range is 8192 units from the player’s gun position. Reloading the glock takes 1.5s. Unlike the revolver (.357 Magnum revolver), the glock can be fired under water. It can also be fired in both primary and secondary mode. The main differences between them are the firing rate and the bullet spread.
Mode |
Cycle time |
Bullet spread |
---|---|---|
Primary |
0.3s |
|
Secondary |
0.2s |
In primary mode, glock’s precision is only slightly worse than the revolver. In practice, since the damage of each shot in either mode is the same, the speedrunner should almost always fire in the secondary mode when a sustained fire is called for. The lack of precision can be compensated easily by knowing where the next shot would land and adjusting the player yaw and pitch.
13.9. MP5¶
The MP5 submachine gun is a fairly versatile weapon thanks to its secondary mode of firing contact grenades. The primary mode is also always fairly strong in the early game. Although it shares the ammo capacity with the glock (Glock), the damage of each bullet is 5 in the default game settings, lower than the glock’s damage. Nonetheless, the MP5 primary mode fires a shot every 0.1s, yielding a respectable damage rate of 50 per second, which is higher than glock’s 40 per second in the secondary mode. Unlike the glock’s secondary mode, the MP5’s primary mode fires at a higher precision, with a bullet spread of approximately in single-player. The MP5 can fire in neither the primary nor the secondary mode when the waterlevel is 3. Like the glock, the primary fire has a range of 8192 units from the player’s gun position, reloading takes 1.5s, and the volume of gunfire is 600.
An MP5 grenade can be fired at a sound volume of 600. When touched, it explodes with a source damage of 100 in the default game settings. See Contact grenades for a description of its explosive physics. An MP5 grenade has an entity gravity multiplier of , causing it to experience a gravity of half the strength as experienced by the player. It is fired from the starting position of , at a rate of one grenade per second. Interestingly, the grenade is unique in how its initial velocity is independent of the current player velocity. This contradicts real life physics. In particular, the initial velocity of the grenade is always
where is the player’s unit forward vector. This idiosyncratic behaviour can be advantageous in certain situations. For instance, the speedrunner could “outrun” the grenade with the intention of making it explode adjacent or behind the player at some point later.
It is possible to have two MP5 grenades touch each other and explode together.
13.10. .357 Magnum revolver¶
The .357 Magnum revolver or Colt Python is a very powerful hitscan weapon that fires high damaging rounds. With the default game settings, a single shot of the revolver deals 40 damage of type DMG_BULLET
, which is greater than that of gauss in primary mode. The bullet range is 8192 units from the player’s gun position. Each shot creates a sound with volume 1000. The behaviour of the revolver is simple. In single-player mode, which is game mode we are most interested in, it only fires in primary mode. It cannot be fired when the waterlevel is 3. When the waterlevel is less than 3 and the clip is not empty, it fires once every 0.75 seconds. A reload takes 2 seconds. Contrary to what some believed, the revolver has a bullet spread of approximately in the horizontal and vertical directions.
13.11. Crossbow¶
The crossbow is very powerful and important in speedrunning, thanks to its high damage. The crossbow has many downsides, however. It is not a hitscan weapon in single-player. When fired, a bolt entity is created with a low sound volume of 200, from a starting position of . The bolt is set to have a movetype of MOVETYPE_FLY
, which makes it ignore gravity. The initial velocity of the crossbow bolt depends on the waterlevel at the time it is fired:
When the bolt touches a damageable entity, it applies a damage of type DMG_BULLET
and removes itself from the world. If the entity it touches is not damageable and is the worldspawn
entity, it will embed itself at where the entity is struck for 10s as a SOLID_NOT
.
Tip
This is a quick-and-dirty way of testing if an entity is the worldspawn
: simply fire the crossbow at the entity and check if the bolt embeds itself on it.
Similar to the 357 revolver (.357 Magnum revolver), the crossbow fires at a rate of one bolt per 0.75 seconds. Still, it reloads at a glacial speed, taking 4.5 seconds to complete. Despite these downsides, the crossbow bolt does not have a spread and no known mechanism can cause its path to deviate.
13.12. Crowbar¶
The crowbar is
13.13. Shotgun¶
The shotgun is a very powerful hitscan weapon in Half-Life, and has seen much use in the early and middle game, and as a quick weapon switching combination in the late game. It can be fired in primary or secondary mode. Regardless of the mode, the shotgun fires a certain number of pellets in each shot. Each pellet deals a damage of 5 in the default game settings. The shotgun has a bullet spread of approximately in both modes, and creates a volume of 1000. Unlike other hitscan weapons, the shotgun has a range of only 2048 units from the player’s gun position. Here are the differences between the primary and the secondary modes:
Mode |
Pellets |
Shells |
Cycle time |
Total damage |
---|---|---|---|---|
Primary |
6 |
1 |
0.75s |
30 |
Secondary |
12 |
2 |
1.5s |
60 |
The shotgun has a more complex reloading behaviour. 3 Unlike other weapons, there is a delay after firing before the shotgun can be reloaded, and this delay is the cycle time corresponding to the last firing mode. For example, if the player fires the shotgun in secondary mode at time and tries to reload at , the weapon will not respond.
Suppose the player initiates a reload after more than the cycle time since the last shot, either by issuing the +reload
command or by emptying the clip. The CShotGun::Reload
function will get called by CBasePlayerWeapon::ItemPostFrame
, which kickstarts the reload process and starting a firing delay timer of one second. This causes the player to not be able to fire and cancel the reload until one second after the reload has begun. When this timer expires, the shotgun will initiate the actual loading of shells into the clip. This new state begins by starting another timer of half a second. Once this other timer expires, a shell will be added to the clip, and then the half-a-second timer restarts, and so on. This cycle is repeated at a rate of one shell every 0.5s, until either the clip is full or interrupted by firing the weapon.
13.13.1. Fast shotgun reload¶
The fast shotgun reload technique decreases the total shotgun reloading time by half a second. Observe that while the half-second timer is active and pending the loading of the next shell, the player can forcibly issue the +reload
command again. Normally, issuing +reload
while reloading other weapons has no effect. Those weapons call CBasePlayerWeapon::DefaultReload
to reload, which sets the CBasePlayerWeapon::m_fInReload
to true to indicate that reloading is in progress. If the player then issues a +reload
command, a check in CBasePlayerWeapon::ItemPostFrame
will prevent calling Reload
again:
else if ( m_pPlayer->pev->button & IN_RELOAD && iMaxClip() != WEAPON_NOCLIP && !m_fInReload )
{
// reload when reload is pressed, or if no buttons are down and weapon is empty.
Reload();
}
The shotgun, however, does not call the CBasePlayerWeapon::DefaultReload
and so CBasePlayerWeapon::m_fInReload
remains false at all time. Every time the player issues +reload
, CShotGun::Reload
will be called. As illustrated in Fig. 13.14., if CShotGun::Reload
is called while the half-second timer is still active, the shotgun will load the next shell prematurely, before the timer expires. In other words, in normal reloading (represented by the second graph) shells are loaded at the trailing edges of the half-second timers, but manually issuing +reload
moves each loading of shell to the leading edge.
By issuing the +reload
command at the points in the top graph, the shell count can be incremented early. The result is that the clip is filled with 8 shells at , compared to in the normal reloading process (second graph). In general, it takes half a second shorter to fill the clip with the desired shell count. Note that once a shell has been loaded within a half-second window, issuing +reload
again has no effect until the current timer expires.
13.14. Satchel charge¶
The satchel charge provides one of the highest explosive damages at the player’s disposal. As a weapon, it has both primary and secondary mode. The behaviour in each mode depends on the weapon’s state. If there is no thrown charges, issuing either +attack
or +attack2
will throw a charge. Then, issuing +attack
again will detonate the charge, while issuing +attack2
will throw another charge.
Throwing a satchel charge creates the entity at the player origin, rather than the gun position. The initial velocity of the satchel charge is set to be
where is the current player velocity and is the player’s unit forward vector. After a satchel charge is thrown, it takes half a second before another one can be thrown. The only way to detonate the thrown charges is to issue the primary attack at least one second since the last one was thrown. When issued, the weapon searches for all the thrown satchel charges within 4096 units from the player’s origin. All charges with the owner set to the player will be detonated. The weapon then starts a timer of one second before any more charges can be thrown again.
A satchel charge has a source damage of 150 in the default game settings, which is higher than average. Refer to Detonating grenades for the explosive physics of the satchel charge. With regards to its movement physics, it is similar to the hand grenade (Hand grenade) in that it has an entity gravity of , an entity friction of , and is set to have a movetype of MOVETYPE_BOUNCE
. This allows the satchel charge to experience half the gravity as experienced by the player, in addition to bounciness when colliding with other entities. One difference from the hand grenade is that the entity gravity is reset to 1 in the CSatchelCharge::SatchelSlide
touch function upon touching another entity other than its owner. This makes a thrown satchel charge twice as heavy as soon as touching some entity such as a wall.
The satchel charge has a unique water physics. The satchel charge entity checks its waterlevel once every 0.1s in CSatchelCharge::SatchelThink
. If the waterlevel is 3, the entity’s movetype is set to be MOVETYPE_FLY
, making it ignore gravity. In addition, the new velocity is set to be
where . In this movetype, the satchel charge will not see its velocity modified by any means except the above. Therefore, we can easily determine the steady state velocity to be , which can be found by equating and solving. If the waterlevel is 0, the movetype will be reset back to MOVETYPE_BOUNCE
. In any waterlevel other than 0 or 3, the velocity is modified as such:
The satchel charge also applies an additional geometric friction when it is close to the ground. Specifically, every 0.1s, it traces a line from the charge origin to 10 units below at . If this line hits an entity other than monsters, the satchel charge will modify its velocity to
The satchel charge has an owner property that remembers who created it. The satchel charge will pass through the owner, but collide with any other entity. The owner property is saved across save loads. It is also saved across map changes, provided the satchel charge does not disappear across transition. If it does disappear, the charge will lose the information about who the owner is, and so it cannot be detonated on primary attack and it will collide with the original owner.
13.15. Snarks¶
The snarks is a weapon which, when fired in primary attack, tosses snarks as monsters. The snarks is more useful in speedruns of the past, and has gradually been supplanted by more modern boosting and movement techniques aided by other weapons. Nevertheless, it still sees uses in certain situations as yet another tool available to the speedrunner for manipulating the difficult-to-control vertical movement. In primary attack, the weapon first traces a line from to . We have
where is the player position, is the player’s unit forward vector, and . If this line trace is not allsolid, and not startsolid, and has a trace fraction above 0.25, then a snark monster will be created at the trace end position with an initial velocity of
where is the player velocity. The volume of the tossing sound is a quiet 200. After successfully tossing a snark, the player cannot toss another one until 0.3s later. If the tossing is unsuccessful, namely if the line trace above does not meet the requirements, there is no delay to the next primary attack.
The behaviour of the squeak grenade after release is described in Snarks.
Footnotes
- 1
Representing the second iteration beam as is technically not correct, because the start of the beam is not exactly , but rather, offset by 1 unit in the direction of .
- 2
A note on glock’s implementation in the Half-Life SDK: the
dlls/glock.cpp
is not the file to look for. The code actually resides indlls/wpn_shared/hl_wpn_glock.cpp
.- 3
This description of the shotgun reloading process is an abstraction over the implementation details in the SDK code, at a level appropriate for understanding the mechanisms relevant to exploitation and speedrunning. If you wish to understand the logic at a lower level, there is no substitute to reading the actual code in
dlls/shotgun.cpp
.