Updated March 6, 2024 Not all of this is guaranteed to be accurate. When in doubt, check the source code! I've had to revise this many times. +----------+ | | | CONTENTS | | | +----------+ 1. Projectile Weapons 2. Radius Damage 3. Ceiling Bugs 4. Ground Boosts 5. Weapon Switching 6. Quad Damage 7. Haste 8. CPM Rocket Launcher 9. Frame Order +--------------------+ | | | PROJECTILE WEAPONS | | | +--------------------+ All projectile weapons fire projectiles in the same way: 1. Calculate the muzzle point The muzzle point is where the projectile is spawned. It is calculated by going to the player's eye position, moving forward 14 units, and then snapping that position to integer coordinates. This snapping is done by simply truncating the fractional part of the coordinates, not by rounding. muzzle = vector_snap(eyes + (forward * 14)) The player bbox size is 30x30x56 when standing, and 30x30x40 while ducking. When standing, the eyes are 6 units below the top of the bbox, and when ducking, they are 4 units below the top of the bbox. Because the bbox extends at least 15 units away from the eyes horizontally, when firing horizontally, the projectile will always spawn inside the player's bbox. however, when looking up, the bbox only extends 6 units away from the eyes, so the projectile can spawn outside the player's bbox. The muzzle point is calculated as a simple addition and multiplication, as given above, and not a trace. So if this point ends up inside a wall, that's where the projectile will spawn. 2. The muzzle point is copied into the projectile's "trBase" variable. I don't know exactly what this is used for, but it seems like the game keeps track of a "base position" and then moves the projectile by offsetting it from that base position. The fact that this is saved is important later. 3. On the next tick the projectile is moved forward an amount equal to speed * (frametime + 50 ms). This looks like some kind of extra trace that's done by setting the missile's "trTime" variable to the current time minus MISSILE_PRESTEP_TIME, which is 50. On subsequent ticks it only moves forward by speed * frametime. 4. On the tick that the projectile hits something, snap the trace's coordinates towards the original muzzle point of the projectile, set the origin to that snapped position, and use that as the origin for radius damage. +---------------+ | | | RADIUS DAMAGE | | | +---------------+ 1. G_RadiusDamage takes an origin, radius, and damage amount. 2. First, find all entities in a square box centered at the given origin. The box has a width of 2x radius. 3. For each of these entities, calculate the distance from the given origin to the nearest part of the entity. This is done by comparing each component of the origin to the absmin and absmax. The algorithm works like this: let vector v = <0, 0, 0> for each component C in XYZ: if origin.C < absmin.C: v.C = absmin.C - origin.C if origin.C > absmax.C" v.C = origin.C - absmax.C else: v.C = 0 To see how this works visually, it may be useful to work out this algorithm on paper in 2 dimensions. After v is determined, the distance is taken by calculating the length of v. If this distance is greater than the radius, then the entity is ignored and the next entity in the box is checked. One oddity about this algorithm is how the absmin and absmax are calculated. They are not just the players origin + the mins or maxs. The absmin and absmax are calculated by these steps: 1. First, snap the player's position to integer coordinates by truncation. (ClientThink_real in g_active.c) 2. Add the player's origin to their mins or maxs to get the absmin or absmax. (SV_LinkEntity in sv_world.c) 3. Add 1 to each component of the absmax, and -1 to each component of the absmin. (SV_LinkEntity in sv_world.c) Because of this, the player is treated as if they're two units wider in all directions than they actually are, an equivalent bounding box size of 32x32x58. 4. If the distance is less than or equal to the radius, an amount of damage points is calculated based on the ratio of distance to the radius. Specifically, it is: points = damage * (1.0 - distance / radius) 5. Before the calculated points are actually applied, whether or not the projectile can damage the target is determined. This is done by calculating the center point of the target, and then tracing from the origin of the projectile to that center point, and every corner of a 30x30x30 box around the center of the target. If any of the traces succeeds, then the target can be damaged, and the calculation continues. 6. A knockback direction is calculated. The direction is the vector from the origin of the inflicting entity (the projectile), to the origin of the target, with an extra 24 units of vertical distance added to make players get knocked into the air more. 7. Control goes from G_RadiusDamage to G_Damage, which is passed in the new points value for damage and the knockback direction. 8. If the target is some kind of button or door, then it is pressed or opened and the function returns. 9. Knockback is set equal to max(damage, 200) 10. The target's velocity has the knockback velocity added to it, only if there is no current timer flag. To calculate the knockback velocity, the knockback direction is normalized, then scaled by a value calculated from the knockback. The actual calculation is this: vel = knockback_dir * (g_knockback * knockback / 200) This knockback velocity is added to the target's existing velocity, even if they don't take any damage. 11. A knockback timer is set on the target if they don't already have a timer running. The knockback timer is set equal to clamp(50, knockback * 2, 200) in milliseconds. +--------------+ | | | CEILING BUGS | | | +--------------+ When you are standing, your bounding box is 30x30x56, and specifically your mins are (-15, -15, -24) and your maxs are (15, 15, 32). When ducking, your maxs become (15, 15, 16), for a bounding box size of 30x30x40. This means your origin is only 16 units above the top of your bounding box when crouched. During radius damage calculation, the knockback direction has an extra 24 units added to its upwards component. If a projectile hits the ceiling, it will only be 16 units above the player, and those 16 units will be canceled out by the added 24 units, resulting in a knockback direction that's actually going up. To exploit this, you have to fire the projectile at an angle, not straight up, otherwise it will actually spawn inside the ceiling and it will be too high above you for the knockback direction to point upwards. +--------------+ | | | GROUNDBOOSTS | | | +--------------+ As explained in the radius damage section, whenever the player takes knockback, they get a timer set on them that is proportional to the amount of knockback they took. This timer is a single value, counted in milliseconds, that applies to each player separately. The timer can mean a lot of different things, so special flags are also kept track of that indicate what the timer is actually counting. When the timer is running, and you have the knockback flag set, you get slick movement. Groundboosts work when you combine the knockback flag from knockback, with the relatively long 250 ms timer that you get from landing on the ground. Normally, you can only have one flag set at a time, so that the timer can only count one thing at a time. When the timer runs out, all the flags are cleared, and you can then get a new timer with a new flag. However, whenever the player lands with more than 200 ups of downwards speed, their timer is set to 250 ms without clearing the old flags. This means that if you already had a flag like knockback, that flag is preserved *and* the timer is extended from its old value up to 250 ms. Normal scenario: level time: 0 50 100 (knockback flag) . . . . . . (landing flag) . . . . . . . . . . . . timer: P---------X L---------------------> In this diagram, P marks when the player takes knockback from a plasma bolt, which does very little damage, so they get the minimum knockback time of 50 ms. X marks when the timer runs out, and the flags are cleared. L marks when the player lands, and the new flag is set with the new timer. Groundboost scenario: level time: 0 50 100 (knockback flag) . . . . . . . . . . . . . . . . . . . . . . (landing flag) . . . . . . . . . . . . . . . . . . . timer: P-----L-----------------------------------> Here, the player lands *before* their knockback timer runs out at 50 ms, so the X point when the timer runs out is never reached. They are able to keep the knockback timer for much longer, which gives them a full quarter second of slick on the ground. "Pseudo-groundboosts" can be done with more powerful weapons like the rocket launcher, since they can give you the full 200 ms of knockback time. However, 250 ms can only be had by landing on the ground after getting knockback. It may be possible to get different times by abusing other timer changes, such as jumping out of water. +------------------+ | | | WEAPON SWITCHING | | | +------------------+ Unlike in Quake 1 and Half-Life based games, Quake 3 restricts when you can switch off your weapons. After a weapon is fired, it enters a cooldown time during which it cannot be fired or switched away from. If the weapon has ammo, it will have the cooldown time listed for that weapon below. If it does not have ammo, it will enter a 500 ms cooldown time. The gauntlet is a special exception to these rules. Although its animation will play, it will only perform an attack and enter cooldown if there is something in front of it to hit. During the cooldown time, you cannot switch off the weapon. However, any attempt to switch off the weapon will be remembered as your weapon cmd, and as soon as your weapon has finished firing the game will begin switching to the most recent desired weapon, as long as it isn't the weapon you are currently holding. If you try switching to the weapon you are currently holding, nothing happens. Switching always takes priority over firing. That is, if you try to continue firing when switching weapons, it will interrupt your firing, switch to the new weapon, then allow you to continue firing. The following conditions have to occur to switch weapons: - You are not in weapon cooldown OR your weapon is not currently firing - Your weapon cmd is not your currently equipped weapon Sometimes, the game sets your weapon cmd automatically. This happens in two cases: - Autoswitch when picking up weapons - Firing a weapon that's out of ammo cg_autoswitch controls the exact behavior of the first case. When it is set to 0, no switching occurs. When it is set to 1, any time you pick up a new weapon, that weapon is set to your weapon cmd. When it is set to 2, your weapon cmd is only set if you didn't already have the weapon you picked up. When you attempt to fire a weapon that is out of ammo, you will get the above mentioned 500 ms cooldown, and then you will switch to the highest weapon that you are holding that has ammo. For example, if you run out of ammo on the Machine Gun, and you are holding the Rocket Launcher and the Lightning Gun, your weapon cmd will be set to the Lightning Gun. However, if the Lightning Gun is out of ammo as well, then it will be set to the Rocket Launcher. In both cases, the autoswitching is treated just like a player input. If you autoswitch during a cooldown time, you can enter another weapon switch input to override the autoswitch. Autoswitch can be cancelled entirely by overriding it with the weapon you are currently holding. For example, because defrag lets you switch to weapons that are out of ammo, you can simply press the key for the weapon you are out of ammo on after firing it, to prevent the game from switching you to something else. Cooldown times for all weapons in ms: 1. Gauntlet: 400 2. Machine Gun: 100 3. Shotgun: 1000 4. Grenade Launcher: 800 5. Rocket Launcher: 800 6. Lightning Gun: 50 7. Rail Gun: 1500 8. Plasma Gun: 100 9. BFG: 200 Grappling Hook: 400 Any weapon without ammo: 500 All found in PM_Weapon (bg_pmove.c) Other engines like Quakeworld and Source keep track of time mainly using floats, including for their weapons. However, Quake 3 uses integer millisecond values for timing. Thus, these times are very accurate. For example, the plasma gun has a 100 ms cooldown. At 8 ms frametime, this corresponds to 12.5 ticks, and indeed, the plasma gun fires exactly every 12 or 13 ticks, alternating each shot. This accuracy needs to be taken into account if implementing the plasma gun in another engine. Slight errors can cause it to shoot slower or faster on average, which quickly builds up into noticable error and makes the weapon over- or underpowered. In VQ3, all weapons have a 450 ms deploy time, during which they cannot be fired. However, weapons can be switched during this time. +-------------+ | | | QUAD DAMAGE | | | +-------------+ First, quad damage is actually only triple damage, since the default value of g_quadfactor is set to 3. When you have quad damage, the damage and splash damage is simply multiplied by this factor. The splash radius remains the same. Although the damage is increased, the maximum speed you can get from knockback is limited in G_Damage to 1000 ups. This means that higher values of g_quadfactor don't translate to more knockback from the rocket launcher, BFG, or grenade launcher, since those already do maximum knockback with a quad factor of 3. +-------+ | | | HASTE | | | +-------+ When your weapon is fired, your cooldown time is divided by 1.3. This only applied when you successfully shoot your weapon, if cooldown is applied because you ran out of ammo, your cooldown time will still be 500 ms. Haste also does not affect weapon deploy time in VQ3, it's still 200 ms. +---------------------+ | | | CPM ROCKET LAUNCHER | | | +---------------------+ The rocket launcher has an additional mechanic in CPM, where it does 1.2x horizontal knockback. This is calculated after knockback is clamped to 200 max, which is between steps 9 and 10 in the radius knockback section above. It's unknown what calculation is used to only apply 1.2x knockback to more or less horizontal rockets, but it seems to be fairly lenient. It might be something like "if the vector from the rocket to the player is less than 45 degrees from the horizontal, apply 1.2x knockback." Because of this, only the rocket launcher can give more than 1000 ups of knockback under the effects of quad damage. Specifically, it can give 1200 ups of knockback. +-------------+ | | | FRAME ORDER | | | +-------------+ At the end of pmove, weapons are fired. If it's a projectile weapon, the projectile will not move or explode until the next server tick. Quake 3 has tick-based server physics, and in Defrag these run at the same rate as the pseudo-tick based pmove system, which is 125 ticks per second (8 ms tick duration.) The order is like this: tick 1: player moves tick 1: player spawns projectile tick 1: all other movement finishes (8 ms later) tick 2: player moves again tick 2: projectile is updated for the first time, and can now explode if it hits something The order of events can be seen starting in g_main.c: vmMain G_RunFrame G_RunClient ClientThink_real (g_active.c) Pmove (bg_pmove.c) PmoveSingle PM_Weapon (here we fire an event indicating that the weapon was fired.) ClientEvents (g_active.c) FireWeapon (g_weapon.c) CalcMuzzlePointOrigin Weapon__Fire fire_ (g_missile.c) G_RunMissile (g_missile.c) G_MissileImpact (if it hit something) SnapVectorTowards (g_weapon.c) (towards the trBase, which is the muzzle point. Explained below.) G_RadiusDamage (g_combat.c) G_Damage Despite the projectile being spawned before missiles are updated, it still doesn't move until the next tick.