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_<weapon name>_Fire
             fire_<projectile> (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.