// Copyright (C) 1999-2000 Id Software, Inc.
//
// cg_weapons.c -- events and effects dealing with weapons

/*
 *  Portions Copyright (C) 2000-2001 Tim Angus
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation; either version 2.1, or (at your option)
 *  any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

/*  To assertain which portions are licensed under the LGPL and which are
 *  licensed by Id Software, Inc. please run a diff between the equivalent
 *  versions of the "Tremulous" modification and the unmodified "Quake3"
 *  game source code.
 */
                    
#include "cg_local.h"

/*
==========================
CG_MachineGunEjectBrass
==========================
*/
static void CG_MachineGunEjectBrass( centity_t *cent ) {
  localEntity_t *le;
  refEntity_t   *re;
  vec3_t      velocity, xvelocity;
  vec3_t      offset, xoffset;
  float     waterScale = 1.0f;
  vec3_t      v[3];

  if ( cg_brassTime.integer <= 0 ) {
    return;
  }

  le = CG_AllocLocalEntity();
  re = &le->refEntity;

  velocity[0] = 0;
  velocity[1] = -50 + 40 * crandom();
  velocity[2] = 100 + 50 * crandom();

  le->leType = LE_FRAGMENT;
  le->startTime = cg.time;
  le->endTime = le->startTime + cg_brassTime.integer + ( cg_brassTime.integer / 4 ) * random();

  le->pos.trType = TR_GRAVITY;
  le->pos.trTime = cg.time - (rand()&15);

  AnglesToAxis( cent->lerpAngles, v );

  offset[0] = 8;
  offset[1] = -4;
  offset[2] = 24;

  xoffset[0] = offset[0] * v[0][0] + offset[1] * v[1][0] + offset[2] * v[2][0];
  xoffset[1] = offset[0] * v[0][1] + offset[1] * v[1][1] + offset[2] * v[2][1];
  xoffset[2] = offset[0] * v[0][2] + offset[1] * v[1][2] + offset[2] * v[2][2];
  VectorAdd( cent->lerpOrigin, xoffset, re->origin );

  VectorCopy( re->origin, le->pos.trBase );

  if ( CG_PointContents( re->origin, -1 ) & CONTENTS_WATER ) {
    waterScale = 0.10f;
  }

  xvelocity[0] = velocity[0] * v[0][0] + velocity[1] * v[1][0] + velocity[2] * v[2][0];
  xvelocity[1] = velocity[0] * v[0][1] + velocity[1] * v[1][1] + velocity[2] * v[2][1];
  xvelocity[2] = velocity[0] * v[0][2] + velocity[1] * v[1][2] + velocity[2] * v[2][2];
  VectorScale( xvelocity, waterScale, le->pos.trDelta );

  AxisCopy( axisDefault, re->axis );
  re->hModel = cgs.media.machinegunBrassModel;

  le->bounceFactor = 0.4 * waterScale;

  le->angles.trType = TR_LINEAR;
  le->angles.trTime = cg.time;
  le->angles.trBase[0] = rand()&31;
  le->angles.trBase[1] = rand()&31;
  le->angles.trBase[2] = rand()&31;
  le->angles.trDelta[0] = 2;
  le->angles.trDelta[1] = 1;
  le->angles.trDelta[2] = 0;

  le->leFlags = LEF_TUMBLE;
  le->leBounceSoundType = LEBS_BRASS;
  le->leMarkType = LEMT_NONE;
}

/*
==========================
CG_ShotgunEjectBrass
==========================
*/
static void CG_ShotgunEjectBrass( centity_t *cent ) {
  localEntity_t *le;
  refEntity_t   *re;
  vec3_t      velocity, xvelocity;
  vec3_t      offset, xoffset;
  vec3_t      v[3];
  int       i;

  if ( cg_brassTime.integer <= 0 ) {
    return;
  }

  for ( i = 0; i < 2; i++ ) {
    float waterScale = 1.0f;

    le = CG_AllocLocalEntity();
    re = &le->refEntity;

    velocity[0] = 60 + 60 * crandom();
    if ( i == 0 ) {
      velocity[1] = 40 + 10 * crandom();
    } else {
      velocity[1] = -40 + 10 * crandom();
    }
    velocity[2] = 100 + 50 * crandom();

    le->leType = LE_FRAGMENT;
    le->startTime = cg.time;
    le->endTime = le->startTime + cg_brassTime.integer*3 + cg_brassTime.integer * random();

    le->pos.trType = TR_GRAVITY;
    le->pos.trTime = cg.time;

    AnglesToAxis( cent->lerpAngles, v );

    offset[0] = 8;
    offset[1] = 0;
    offset[2] = 24;

    xoffset[0] = offset[0] * v[0][0] + offset[1] * v[1][0] + offset[2] * v[2][0];
    xoffset[1] = offset[0] * v[0][1] + offset[1] * v[1][1] + offset[2] * v[2][1];
    xoffset[2] = offset[0] * v[0][2] + offset[1] * v[1][2] + offset[2] * v[2][2];
    VectorAdd( cent->lerpOrigin, xoffset, re->origin );
    VectorCopy( re->origin, le->pos.trBase );
    if ( CG_PointContents( re->origin, -1 ) & CONTENTS_WATER ) {
      waterScale = 0.10f;
    }

    xvelocity[0] = velocity[0] * v[0][0] + velocity[1] * v[1][0] + velocity[2] * v[2][0];
    xvelocity[1] = velocity[0] * v[0][1] + velocity[1] * v[1][1] + velocity[2] * v[2][1];
    xvelocity[2] = velocity[0] * v[0][2] + velocity[1] * v[1][2] + velocity[2] * v[2][2];
    VectorScale( xvelocity, waterScale, le->pos.trDelta );

    AxisCopy( axisDefault, re->axis );
    re->hModel = cgs.media.shotgunBrassModel;
    le->bounceFactor = 0.3f;

    le->angles.trType = TR_LINEAR;
    le->angles.trTime = cg.time;
    le->angles.trBase[0] = rand()&31;
    le->angles.trBase[1] = rand()&31;
    le->angles.trBase[2] = rand()&31;
    le->angles.trDelta[0] = 1;
    le->angles.trDelta[1] = 0.5;
    le->angles.trDelta[2] = 0;

    le->leFlags = LEF_TUMBLE;
    le->leBounceSoundType = LEBS_BRASS;
    le->leMarkType = LEMT_NONE;
  }
}


/*
==========================
CG_RailTrail
==========================
*/
void CG_RailTrail( vec3_t start, vec3_t end )
{
  localEntity_t *le;
  refEntity_t   *re;

  //
  // rings
  //
  le = CG_AllocLocalEntity();
  re = &le->refEntity;

  le->leType = LE_FADE_RGB;
  le->startTime = cg.time;
  le->endTime = cg.time + cg_railTrailTime.value;
  le->lifeRate = 1.0 / ( le->endTime - le->startTime );

  re->shaderTime = cg.time / 1000.0f;
  re->reType = RT_RAIL_RINGS;
  re->customShader = cgs.media.railRingsShader;

  VectorCopy( start, re->origin );
  VectorCopy( end, re->oldorigin );

  // nudge down a bit so it isn't exactly in center
  re->origin[2] -= 8;
  re->oldorigin[2] -= 8;

  le->color[0] = 0.75f;
  le->color[1] = 0.75f;
  le->color[2] = 0.75f;
  le->color[3] = 1.0f;

  AxisClear( re->axis );

  //
  // core
  //
  le = CG_AllocLocalEntity();
  re = &le->refEntity;

  le->leType = LE_FADE_RGB;
  le->startTime = cg.time;
  le->endTime = cg.time + cg_railTrailTime.value;
  le->lifeRate = 1.0 / ( le->endTime - le->startTime );

  re->shaderTime = cg.time / 1000.0f;
  re->reType = RT_RAIL_CORE;
  re->customShader = cgs.media.railCoreShader;

  VectorCopy( start, re->origin );
  VectorCopy( end, re->oldorigin );

  // nudge down a bit so it isn't exactly in center
  re->origin[2] -= 8;
  re->oldorigin[2] -= 8;

  le->color[0] = 0.75f;
  le->color[1] = 0.75f;
  le->color[2] = 0.75f;
  le->color[3] = 1.0f;

  AxisClear( re->axis );
}

/*
==========================
CG_RocketTrail
==========================
*/
static void CG_RocketTrail( centity_t *ent, const weaponInfo_t *wi ) {
  int   step;
  vec3_t  origin, lastPos;
  int   t;
  int   startTime, contents;
  int   lastContents;
  entityState_t *es;
  vec3_t  up;
  localEntity_t *smoke;

  up[0] = 0;
  up[1] = 0;
  up[2] = 0;

  step = 50;

  es = &ent->currentState;
  startTime = ent->trailTime;
  t = step * ( (startTime + step) / step );

  BG_EvaluateTrajectory( &es->pos, cg.time, origin );
  contents = CG_PointContents( origin, -1 );

  // if object (e.g. grenade) is stationary, don't toss up smoke
  if ( es->pos.trType == TR_STATIONARY ) {
    ent->trailTime = cg.time;
    return;
  }

  BG_EvaluateTrajectory( &es->pos, ent->trailTime, lastPos );
  lastContents = CG_PointContents( lastPos, -1 );

  ent->trailTime = cg.time;

  if ( contents & ( CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA ) ) {
    if ( contents & lastContents & CONTENTS_WATER ) {
      CG_BubbleTrail( lastPos, origin, 8 );
    }
    return;
  }

  for ( ; t <= ent->trailTime ; t += step ) {
    BG_EvaluateTrajectory( &es->pos, t, lastPos );

    smoke = CG_SmokePuff( lastPos, up,
            wi->trailRadius,
            1, 1, 1, 0.33f,
            wi->wiTrailTime,
            t,
            0,
            0,
            cgs.media.smokePuffShader );
    // use the optimized local entity add
    smoke->leType = LE_SCALE_FADE;
  }

}

/*
==========================
CG_GrappleTrail
==========================
*/
void CG_GrappleTrail( centity_t *ent, const weaponInfo_t *wi ) {
  vec3_t  origin;
  entityState_t *es;
  vec3_t      forward, up;
  refEntity_t   beam;

  es = &ent->currentState;

  BG_EvaluateTrajectory( &es->pos, cg.time, origin );
  ent->trailTime = cg.time;

  memset( &beam, 0, sizeof( beam ) );
  //FIXME adjust for muzzle position
  VectorCopy ( cg_entities[ ent->currentState.otherEntityNum ].lerpOrigin, beam.origin );
  beam.origin[2] += 26;
  AngleVectors( cg_entities[ ent->currentState.otherEntityNum ].lerpAngles, forward, NULL, up );
  VectorMA( beam.origin, -6, up, beam.origin );
  VectorCopy( origin, beam.oldorigin );

  if (Distance( beam.origin, beam.oldorigin ) < 64 )
    return; // Don't draw if close

  beam.reType = RT_LIGHTNING;
  beam.customShader = cgs.media.lightningShader;

  AxisClear( beam.axis );
  beam.shaderRGBA[0] = 0xff;
  beam.shaderRGBA[1] = 0xff;
  beam.shaderRGBA[2] = 0xff;
  beam.shaderRGBA[3] = 0xff;
  trap_R_AddRefEntityToScene( &beam );
}

/*
==========================
CG_GrenadeTrail
==========================
*/
static void CG_GrenadeTrail( centity_t *ent, const weaponInfo_t *wi ) {
  CG_RocketTrail( ent, wi );
}

/*
=================
CG_RegisterUpgrade

The server says this item is used on this level
=================
*/
void CG_RegisterUpgrade( int upgradeNum ) {
  upgradeInfo_t  *upgradeInfo;
  gitem_t     *item;
  char      path[MAX_QPATH];
  int       i;

  upgradeInfo = &cg_upgrades[ upgradeNum ];

  if ( upgradeNum == 0 ) {
    return;
  }

  if ( upgradeInfo->registered ) {
    return;
  }

  memset( upgradeInfo, 0, sizeof( *upgradeInfo ) );
  upgradeInfo->registered = qtrue;

  for ( item = bg_itemlist + 1 ; item->classname ; item++ ) {
    if ( item->giType == IT_UPGRADE && item->giTag == upgradeNum ) {
      upgradeInfo->item = item;
      break;
    }
  }
  if ( !item->classname ) {
    CG_Error( "Couldn't find upgrade %i", upgradeNum );
  }
  CG_RegisterItemVisuals( item - bg_itemlist );

  upgradeInfo->upgradeIcon = trap_R_RegisterShader( item->icon );
}

/*
=================
CG_RegisterWeapon

The server says this item is used on this level
=================
*/
void CG_RegisterWeapon( int weaponNum ) {
  weaponInfo_t  *weaponInfo;
  gitem_t     *item, *ammo;
  char      path[MAX_QPATH];
  vec3_t      mins, maxs;
  int       i;

  weaponInfo = &cg_weapons[weaponNum];

  if ( weaponNum == 0 ) {
    return;
  }

  if ( weaponInfo->registered ) {
    return;
  }

  memset( weaponInfo, 0, sizeof( *weaponInfo ) );
  weaponInfo->registered = qtrue;

  for ( item = bg_itemlist + 1 ; item->classname ; item++ ) {
    if ( item->giType == IT_WEAPON && item->giTag == weaponNum ) {
      weaponInfo->item = item;
      break;
    }
  }
  if ( !item->classname ) {
    CG_Error( "Couldn't find weapon %i", weaponNum );
  }
  CG_RegisterItemVisuals( item - bg_itemlist );

  // load cmodel before model so filecache works
  weaponInfo->weaponModel = trap_R_RegisterModel( item->world_model[0] );

  // calc midpoint for rotation
  trap_R_ModelBounds( weaponInfo->weaponModel, mins, maxs );
  for ( i = 0 ; i < 3 ; i++ ) {
    weaponInfo->weaponMidpoint[i] = mins[i] + 0.5 * ( maxs[i] - mins[i] );
  }

  weaponInfo->weaponIcon = trap_R_RegisterShader( item->icon );
  weaponInfo->ammoIcon = trap_R_RegisterShader( item->icon );

  for ( ammo = bg_itemlist + 1 ; ammo->classname ; ammo++ ) {
    if ( ammo->giType == IT_AMMO && ammo->giTag == weaponNum ) {
      break;
    }
  }
  if ( ammo->classname && ammo->world_model[0] ) {
    weaponInfo->ammoModel = trap_R_RegisterModel( ammo->world_model[0] );
  }

  strcpy( path, item->world_model[0] );
  COM_StripExtension( path, path );
  strcat( path, "_flash.md3" );
  weaponInfo->flashModel = trap_R_RegisterModel( path );

  strcpy( path, item->world_model[0] );
  COM_StripExtension( path, path );
  strcat( path, "_barrel.md3" );
  weaponInfo->barrelModel = trap_R_RegisterModel( path );

  strcpy( path, item->world_model[0] );
  COM_StripExtension( path, path );
  strcat( path, "_hand.md3" );
  weaponInfo->handsModel = trap_R_RegisterModel( path );

  if ( !weaponInfo->handsModel ) {
    weaponInfo->handsModel = trap_R_RegisterModel( "models/weapons2/shotgun/shotgun_hand.md3" );
  }

  weaponInfo->loopFireSound = qfalse;

  switch ( weaponNum ) {
  case WP_GAUNTLET:
    MAKERGB( weaponInfo->flashDlightColor, 0.6f, 0.6f, 1.0f );
    weaponInfo->firingSound = trap_S_RegisterSound( "sound/weapons/melee/fstrun.wav", qfalse );
    weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/melee/fstatck.wav", qfalse );
    break;

  case WP_LIGHTNING:
    MAKERGB( weaponInfo->flashDlightColor, 0.6f, 0.6f, 1.0f );
    weaponInfo->readySound = trap_S_RegisterSound( "sound/weapons/melee/fsthum.wav", qfalse );
    weaponInfo->firingSound = trap_S_RegisterSound( "sound/weapons/lightning/lg_hum.wav", qfalse );

    weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/lightning/lg_fire.wav", qfalse );
    cgs.media.lightningShader = trap_R_RegisterShader( "lightningBolt");
    cgs.media.lightningExplosionModel = trap_R_RegisterModel( "models/weaphits/crackle.md3" );
    cgs.media.sfx_lghit1 = trap_S_RegisterSound( "sound/weapons/lightning/lg_hit.wav", qfalse );
    cgs.media.sfx_lghit2 = trap_S_RegisterSound( "sound/weapons/lightning/lg_hit2.wav", qfalse );
    cgs.media.sfx_lghit3 = trap_S_RegisterSound( "sound/weapons/lightning/lg_hit3.wav", qfalse );

    break;

  case WP_GRAPPLING_HOOK:
    MAKERGB( weaponInfo->flashDlightColor, 0.6f, 0.6f, 1.0f );
    weaponInfo->missileModel = trap_R_RegisterModel( "models/ammo/rocket/rocket.md3" );
    weaponInfo->missileTrailFunc = CG_GrappleTrail;
    weaponInfo->missileDlight = 200;
    weaponInfo->wiTrailTime = 2000;
    weaponInfo->trailRadius = 64;
    MAKERGB( weaponInfo->missileDlightColor, 1, 0.75f, 0 );
    weaponInfo->readySound = trap_S_RegisterSound( "sound/weapons/melee/fsthum.wav", qfalse );
    weaponInfo->firingSound = trap_S_RegisterSound( "sound/weapons/melee/fstrun.wav", qfalse );
    break;

  case WP_MACHINEGUN:
    MAKERGB( weaponInfo->flashDlightColor, 1, 1, 0 );
    weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/machinegun/machgf1b.wav", qfalse );
    weaponInfo->flashSound[1] = trap_S_RegisterSound( "sound/weapons/machinegun/machgf2b.wav", qfalse );
    weaponInfo->flashSound[2] = trap_S_RegisterSound( "sound/weapons/machinegun/machgf3b.wav", qfalse );
    weaponInfo->flashSound[3] = trap_S_RegisterSound( "sound/weapons/machinegun/machgf4b.wav", qfalse );
    weaponInfo->ejectBrassFunc = CG_MachineGunEjectBrass;
    cgs.media.bulletExplosionShader = trap_R_RegisterShader( "bulletExplosion" );
    break;

  case WP_CHAINGUN:
    MAKERGB( weaponInfo->flashDlightColor, 1, 1, 0 );
    weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/machinegun/machgf1b.wav", qfalse );
    weaponInfo->flashSound[1] = trap_S_RegisterSound( "sound/weapons/machinegun/machgf2b.wav", qfalse );
    weaponInfo->flashSound[2] = trap_S_RegisterSound( "sound/weapons/machinegun/machgf3b.wav", qfalse );
    weaponInfo->flashSound[3] = trap_S_RegisterSound( "sound/weapons/machinegun/machgf4b.wav", qfalse );
    weaponInfo->ejectBrassFunc = CG_MachineGunEjectBrass;
    cgs.media.bulletExplosionShader = trap_R_RegisterShader( "bulletExplosion" );
    break;
    
  case WP_SHOTGUN:
    MAKERGB( weaponInfo->flashDlightColor, 1, 1, 0 );
    weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/shotgun/sshotf1b.wav", qfalse );
    weaponInfo->ejectBrassFunc = CG_ShotgunEjectBrass;
    break;

  case WP_ROCKET_LAUNCHER:
    weaponInfo->missileModel = trap_R_RegisterModel( "models/ammo/rocket/rocket.md3" );
    weaponInfo->missileSound = trap_S_RegisterSound( "sound/weapons/rocket/rockfly.wav", qfalse );
    weaponInfo->missileTrailFunc = CG_RocketTrail;
    weaponInfo->missileDlight = 200;
    weaponInfo->wiTrailTime = 2000;
    weaponInfo->trailRadius = 64;
    MAKERGB( weaponInfo->missileDlightColor, 1, 0.75f, 0 );
    MAKERGB( weaponInfo->flashDlightColor, 1, 0.75f, 0 );
    weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/rocket/rocklf1a.wav", qfalse );
    cgs.media.rocketExplosionShader = trap_R_RegisterShader( "rocketExplosion" );
    break;

  case WP_SAWBLADE_LAUNCHER:
    weaponInfo->missileModel = trap_R_RegisterModel( "models/ammo/sawblade/sawblade.md3" );
    weaponInfo->missileSound = trap_S_RegisterSound( "sound/weapons/rocket/rockfly.wav", qfalse );
/*    weaponInfo->missileTrailFunc = CG_RocketTrail;
    weaponInfo->missileDlight = 200;
    weaponInfo->wiTrailTime = 2000;
    weaponInfo->trailRadius = 64;
    MAKERGB( weaponInfo->missileDlightColor, 1, 0.75f, 0 );
    MAKERGB( weaponInfo->flashDlightColor, 1, 0.75f, 0 );*/
    weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/rocket/rocklf1a.wav", qfalse );
    /*cgs.media.rocketExplosionShader = trap_R_RegisterShader( "rocketExplosion" );*/
    break;

  case WP_GRENADE_LAUNCHER:
    weaponInfo->missileModel = trap_R_RegisterModel( "models/ammo/grenade1.md3" );
    weaponInfo->missileTrailFunc = CG_GrenadeTrail;
    weaponInfo->wiTrailTime = 700;
    weaponInfo->trailRadius = 32;
    MAKERGB( weaponInfo->flashDlightColor, 1, 0.70f, 0 );
    weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/grenade/grenlf1a.wav", qfalse );
    cgs.media.grenadeExplosionShader = trap_R_RegisterShader( "grenadeExplosion" );
    break;

  case WP_FLAMER:
    weaponInfo->missileSound = trap_S_RegisterSound( "sound/weapons/plasma/lasfly.wav", qfalse );
    MAKERGB( weaponInfo->flashDlightColor, 0.25, 0.1, 0 );
    //weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/railgun/railgf1a.wav", qfalse );
    cgs.media.flameExplShader = trap_R_RegisterShader( "rocketExplosion" );
    break;
    
  case WP_PLASMAGUN:
    weaponInfo->missileSound = trap_S_RegisterSound( "sound/weapons/plasma/lasfly.wav", qfalse );
    MAKERGB( weaponInfo->flashDlightColor, 0.6f, 0.6f, 1.0f );
    weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/plasma/hyprbf1a.wav", qfalse );
    cgs.media.plasmaExplosionShader = trap_R_RegisterShader( "plasmaExplosion" );
    break;

  case WP_RAILGUN:
    weaponInfo->readySound = trap_S_RegisterSound( "sound/weapons/railgun/rg_hum.wav", qfalse );
    MAKERGB( weaponInfo->flashDlightColor, 1, 0.5f, 0 );
    weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/railgun/railgf1a.wav", qfalse );
    cgs.media.railExplosionShader = trap_R_RegisterShader( "railExplosion" );
    cgs.media.railRingsShader = trap_R_RegisterShader( "railDisc" );
    cgs.media.railCoreShader = trap_R_RegisterShader( "railCore" );
    break;

  case WP_BFG:
    weaponInfo->readySound = trap_S_RegisterSound( "sound/weapons/bfg/bfg_hum.wav", qfalse );
    MAKERGB( weaponInfo->flashDlightColor, 1, 0.7f, 1 );
    weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/bfg/bfg_fire.wav", qfalse );
    cgs.media.bfgExplosionShader = trap_R_RegisterShader( "bfgExplosion" );
    weaponInfo->missileModel = trap_R_RegisterModel( "models/weaphits/bfg.md3" );
    weaponInfo->missileSound = trap_S_RegisterSound( "sound/weapons/rocket/rockfly.wav", qfalse );
    break;
 
  case WP_VENOM:
    MAKERGB( weaponInfo->flashDlightColor, 0, 0, 0 );
    weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/melee/fstatck.wav", qfalse );
    break;

  case WP_GRABANDCSAW:
    MAKERGB( weaponInfo->flashDlightColor, 0, 0, 0 );
    weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/melee/fstatck.wav", qfalse );
    break;

  case WP_POUNCE:
    MAKERGB( weaponInfo->flashDlightColor, 0, 0, 0 );
    weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/melee/fstatck.wav", qfalse );
    break;

  case WP_DBUILD:
  case WP_HBUILD:
  case WP_SCANNER:
    //nowt
    break;

   default:
    MAKERGB( weaponInfo->flashDlightColor, 1, 1, 1 );
    weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/rocket/rocklf1a.wav", qfalse );
    break;
  }
}

/*
=================
CG_RegisterItemVisuals

The server says this item is used on this level
=================
*/
void CG_RegisterItemVisuals( int itemNum ) {
  itemInfo_t    *itemInfo;
  gitem_t       *item;
  int           i;

  itemInfo = &cg_items[ itemNum ];
  if ( itemInfo->registered ) {
    return;
  }

  item = &bg_itemlist[ itemNum ];

  memset( itemInfo, 0, sizeof( &itemInfo ) );
  itemInfo->registered = qtrue;

  itemInfo->icon = trap_R_RegisterShader( item->icon );

  if ( item->giType == IT_WEAPON ) {
    CG_RegisterWeapon( item->giTag );
  }

  for( i = 0; i < 4; i++ )
  {
    if( item->world_model[ i ] )
      itemInfo->models[i] = trap_R_RegisterModel( item->world_model[i] );
  }

}


/*
========================================================================================

VIEW WEAPON

========================================================================================
*/

/*
=================
CG_MapTorsoToWeaponFrame

=================
*/
static int CG_MapTorsoToWeaponFrame( clientInfo_t *ci, int frame ) {

  // change weapon
  if ( frame >= ci->animations[TORSO_DROP].firstFrame
    && frame < ci->animations[TORSO_DROP].firstFrame + 9 ) {
    return frame - ci->animations[TORSO_DROP].firstFrame + 6;
  }

  // stand attack
  if ( frame >= ci->animations[TORSO_ATTACK].firstFrame
    && frame < ci->animations[TORSO_ATTACK].firstFrame + 6 ) {
    return 1 + frame - ci->animations[TORSO_ATTACK].firstFrame;
  }

  // stand attack 2
  if ( frame >= ci->animations[TORSO_ATTACK2].firstFrame
    && frame < ci->animations[TORSO_ATTACK2].firstFrame + 6 ) {
    return 1 + frame - ci->animations[TORSO_ATTACK].firstFrame;
  }

  return 0;
}


/*
==============
CG_CalculateWeaponPosition
==============
*/
static void CG_CalculateWeaponPosition( vec3_t origin, vec3_t angles ) {
  float scale;
  int   delta;
  float fracsin;
  float bob;

  VectorCopy( cg.refdef.vieworg, origin );
  VectorCopy( cg.refdefViewAngles, angles );

  // on odd legs, invert some angles
  if ( cg.bobcycle & 1 ) {
    scale = -cg.xyspeed;
  } else {
    scale = cg.xyspeed;
  }

  // gun angles from bobbing
  //TA: bob amount is class dependant
  bob = BG_FindBobForClass( cg.predictedPlayerState.stats[ STAT_PCLASS ] );
  if( bob != 0 )
  {
    angles[ROLL] += scale * cg.bobfracsin * 0.005;
    angles[YAW] += scale * cg.bobfracsin * 0.01;
    angles[PITCH] += cg.xyspeed * cg.bobfracsin * 0.005;
  }

  // drop the weapon when landing
  if( !BG_ClassHasAbility( cg.predictedPlayerState.stats[ STAT_PCLASS ], SCA_NOWEAPONDRIFT ) )
  {
    delta = cg.time - cg.landTime;
    if ( delta < LAND_DEFLECT_TIME )
    {
      origin[2] += cg.landChange*0.25 * delta / LAND_DEFLECT_TIME;
    }
    else if ( delta < LAND_DEFLECT_TIME + LAND_RETURN_TIME )
    {
      origin[2] += cg.landChange*0.25 *
        (LAND_DEFLECT_TIME + LAND_RETURN_TIME - delta) / LAND_RETURN_TIME;
    }

  //TA: huh? why drop the weapon when stair climbing... just as well this isn't actually used :)
#if 0
  // drop the weapon when stair climbing
  delta = cg.time - cg.stepTime;
  if ( delta < STEP_TIME/2 ) {
    origin[2] -= cg.stepChange*0.25 * delta / (STEP_TIME/2);
  } else if ( delta < STEP_TIME ) {
    origin[2] -= cg.stepChange*0.25 * (STEP_TIME - delta) / (STEP_TIME/2);
  }
#endif

  // idle drift
    scale = cg.xyspeed + 40;
    fracsin = sin( cg.time * 0.001 );
    angles[ROLL] += scale * fracsin * 0.01;
    angles[YAW] += scale * fracsin * 0.01;
    angles[PITCH] += scale * fracsin * 0.01;
  }
}


/*
===============
CG_LightningBolt

Origin will be the exact tag point, which is slightly
different than the muzzle point used for determining hits.
The cent should be the non-predicted cent if it is from the player,
so the endpoint will reflect the simulated strike (lagging the predicted
angle)
===============
*/
static void CG_LightningBolt( centity_t *cent, vec3_t origin ) {
  trace_t   trace;
  refEntity_t   beam;
  vec3_t      forward;
  vec3_t      muzzlePoint, endPoint;

  if ( cent->currentState.weapon != WP_LIGHTNING ) {
    return;
  }

  memset( &beam, 0, sizeof( beam ) );

  // find muzzle point for this frame
  VectorCopy( cent->lerpOrigin, muzzlePoint );
  AngleVectors( cent->lerpAngles, forward, NULL, NULL );

  // FIXME: crouch
  muzzlePoint[2] += DEFAULT_VIEWHEIGHT;

  VectorMA( muzzlePoint, 14, forward, muzzlePoint );

  // project forward by the lightning range
  VectorMA( muzzlePoint, LIGHTNING_RANGE, forward, endPoint );

  // see if it hit a wall
  CG_Trace( &trace, muzzlePoint, vec3_origin, vec3_origin, endPoint,
    cent->currentState.number, MASK_SHOT );

  // this is the endpoint
  VectorCopy( trace.endpos, beam.oldorigin );

  // use the provided origin, even though it may be slightly
  // different than the muzzle origin
  VectorCopy( origin, beam.origin );

  beam.reType = RT_LIGHTNING;
  beam.customShader = cgs.media.lightningShader;
  trap_R_AddRefEntityToScene( &beam );

  // add the impact flare if it hit something
  if ( trace.fraction < 1.0 ) {
    vec3_t  angles;
    vec3_t  dir;

    VectorSubtract( beam.oldorigin, beam.origin, dir );
    VectorNormalize( dir );

    memset( &beam, 0, sizeof( beam ) );
    beam.hModel = cgs.media.lightningExplosionModel;

    VectorMA( trace.endpos, -16, dir, beam.origin );

    // make a random orientation
    angles[0] = rand() % 360;
    angles[1] = rand() % 360;
    angles[2] = rand() % 360;
    AnglesToAxis( angles, beam.axis );
    trap_R_AddRefEntityToScene( &beam );
  }
}


/*
===============
CG_SpawnRailTrail

Origin will be the exact tag point, which is slightly
different than the muzzle point used for determining hits.
===============
*/
static void CG_SpawnRailTrail( centity_t *cent, vec3_t origin )
{
  if ( cent->currentState.weapon != WP_RAILGUN )
    return;

  if ( !cent->pe.railgunFlash )
    return;
 
  cent->pe.railgunFlash = qtrue;
  CG_RailTrail( origin, cent->pe.railgunImpact );
}


/*
======================
CG_MachinegunSpinAngle
======================
*/
#define   SPIN_SPEED  0.9
#define   COAST_TIME  1000
static float  CG_MachinegunSpinAngle( centity_t *cent ) {
  int   delta;
  float angle;
  float speed;

  delta = cg.time - cent->pe.barrelTime;
  if ( cent->pe.barrelSpinning ) {
    angle = cent->pe.barrelAngle + delta * SPIN_SPEED;
  } else {
    if ( delta > COAST_TIME ) {
      delta = COAST_TIME;
    }

    speed = 0.5 * ( SPIN_SPEED + (float)( COAST_TIME - delta ) / COAST_TIME );
    angle = cent->pe.barrelAngle + delta * speed;
  }

  if ( cent->pe.barrelSpinning == !(cent->currentState.eFlags & EF_FIRING) ) {
    cent->pe.barrelTime = cg.time;
    cent->pe.barrelAngle = AngleMod( angle );
    cent->pe.barrelSpinning = !!(cent->currentState.eFlags & EF_FIRING);
  }

  return angle;
}


/*
========================
CG_AddWeaponWithPowerups
========================
*/
static void CG_AddWeaponWithPowerups( refEntity_t *gun, int powerups ) {
  // add powerup effects
  /*if ( powerups & ( 1 << PW_INVIS ) ) {
    gun->customShader = cgs.media.invisShader;
    trap_R_AddRefEntityToScene( gun );
  } else*/ {
    trap_R_AddRefEntityToScene( gun );

    /*if ( powerups & ( 1 << PW_BATTLESUIT ) ) {
      gun->customShader = cgs.media.battleWeaponShader;
      trap_R_AddRefEntityToScene( gun );
    }
    if ( powerups & ( 1 << PW_QUAD ) ) {
      gun->customShader = cgs.media.quadWeaponShader;
      trap_R_AddRefEntityToScene( gun );
    }*/
  }
}


/*
=============
CG_AddPlayerWeapon

Used for both the view weapon (ps is valid) and the world modelother character models (ps is NULL)
The main player will have this called for BOTH cases, so effects like light and
sound should only be done on the world model case.
=============
*/
void CG_AddPlayerWeapon( refEntity_t *parent, playerState_t *ps, centity_t *cent ) {
  refEntity_t gun;
  refEntity_t barrel;
  refEntity_t flash;
  vec3_t    angles;
  weapon_t  weaponNum;
  weaponInfo_t  *weapon;
  centity_t *nonPredictedCent;

  weaponNum = cent->currentState.weapon;

  CG_RegisterWeapon( weaponNum );
  weapon = &cg_weapons[weaponNum];

  // add the weapon
  memset( &gun, 0, sizeof( gun ) );
  VectorCopy( parent->lightingOrigin, gun.lightingOrigin );
  gun.shadowPlane = parent->shadowPlane;
  gun.renderfx = parent->renderfx;

  // set custom shading for railgun refire rate
  if ( ps )
  {
    if ( cg.predictedPlayerState.weapon == WP_RAILGUN
      && cg.predictedPlayerState.weaponstate == WEAPON_FIRING )
    {
      float f;

      f = (float)cg.predictedPlayerState.weaponTime / 1500;
      gun.shaderRGBA[1] = 0;
      gun.shaderRGBA[0] =
      gun.shaderRGBA[2] = 255 * ( 1.0 - f );
    }
    else
    {
      gun.shaderRGBA[0] = 255;
      gun.shaderRGBA[1] = 255;
      gun.shaderRGBA[2] = 255;
      gun.shaderRGBA[3] = 255;
    }

    //set weapon[1/2]Time when respective buttons change state
    if( cg.weapon1Firing != ( cg.predictedPlayerState.eFlags & EF_FIRING ) )
    {
      cg.weapon1Time = cg.time;
      cg.weapon1Firing = ( cg.predictedPlayerState.eFlags & EF_FIRING );
    }
    
    if( cg.weapon2Firing != ( cg.predictedPlayerState.eFlags & EF_FIRING2 ) )
    {
      cg.weapon2Time = cg.time;
      cg.weapon2Firing = ( cg.predictedPlayerState.eFlags & EF_FIRING2 );
    }
  }

  gun.hModel = weapon->weaponModel;
  if (!gun.hModel) {
    return;
  }

  if ( !ps ) {
    // add weapon ready sound
    cent->pe.lightningFiring = qfalse;
    if ( ( cent->currentState.eFlags & EF_FIRING ) && weapon->firingSound ) {
      // lightning gun and guantlet make a different sound when fire is held down
      trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, vec3_origin, weapon->firingSound );
      cent->pe.lightningFiring = qtrue;
    } else if ( weapon->readySound ) {
      trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, vec3_origin, weapon->readySound );
    }
  }

  CG_PositionEntityOnTag( &gun, parent, parent->hModel, "tag_weapon");

  CG_AddWeaponWithPowerups( &gun, cent->currentState.powerups );

  // add the spinning barrel
  if( weapon->barrelModel ) {
    memset( &barrel, 0, sizeof( barrel ) );
    VectorCopy( parent->lightingOrigin, barrel.lightingOrigin );
    barrel.shadowPlane = parent->shadowPlane;
    barrel.renderfx = parent->renderfx;

    barrel.hModel = weapon->barrelModel;
    angles[YAW] = 0;
    angles[PITCH] = 0;
    angles[ROLL] = CG_MachinegunSpinAngle( cent );
    AnglesToAxis( angles, barrel.axis );

    CG_PositionRotatedEntityOnTag( &barrel, &gun, weapon->weaponModel, "tag_barrel" );

    CG_AddWeaponWithPowerups( &barrel, cent->currentState.powerups );
  }

  // make sure we aren't looking at cg.predictedPlayerEntity for LG
  nonPredictedCent = &cg_entities[cent->currentState.clientNum];

  // if the index of the nonPredictedCent is not the same as the clientNum
  // then this is a fake player (like on teh single player podiums), so
  // go ahead and use the cent
  if( ( nonPredictedCent - cg_entities ) != cent->currentState.clientNum ) {
    nonPredictedCent = cent;
  }

  // add the flash
  if ( ( weaponNum == WP_LIGHTNING || weaponNum == WP_GAUNTLET || weaponNum == WP_GRAPPLING_HOOK )
    && ( nonPredictedCent->currentState.eFlags & EF_FIRING ) )
  {
    // continuous flash
  } else {
    // impulse flash
    if ( cg.time - cent->muzzleFlashTime > MUZZLE_FLASH_TIME && !cent->pe.railgunFlash ) {
      return;
    }
  }

  memset( &flash, 0, sizeof( flash ) );
  VectorCopy( parent->lightingOrigin, flash.lightingOrigin );
  flash.shadowPlane = parent->shadowPlane;
  flash.renderfx = parent->renderfx;

  flash.hModel = weapon->flashModel;
  if (!flash.hModel) {
    return;
  }
  angles[YAW] = 0;
  angles[PITCH] = 0;
  angles[ROLL] = crandom() * 10;
  AnglesToAxis( angles, flash.axis );

  // colorize the railgun blast
  if ( weaponNum == WP_RAILGUN ) {
    clientInfo_t  *ci;

    ci = &cgs.clientinfo[ cent->currentState.clientNum ];
    flash.shaderRGBA[0] = 255 * ci->color[0];
    flash.shaderRGBA[1] = 255 * ci->color[1];
    flash.shaderRGBA[2] = 255 * ci->color[2];
  }

  CG_PositionRotatedEntityOnTag( &flash, &gun, weapon->weaponModel, "tag_flash");
  trap_R_AddRefEntityToScene( &flash );

  if ( ps || cg.renderingThirdPerson ||
    cent->currentState.number != cg.predictedPlayerState.clientNum ) {
    // add lightning bolt
    CG_LightningBolt( nonPredictedCent, flash.origin );

    // add rail trail
    CG_SpawnRailTrail( cent, flash.origin );

    // make a dlight for the flash
    if ( weapon->flashDlightColor[0] || weapon->flashDlightColor[1] || weapon->flashDlightColor[2] ) {
      trap_R_AddLightToScene( flash.origin, 300 + (rand()&31), weapon->flashDlightColor[0],
        weapon->flashDlightColor[1], weapon->flashDlightColor[2] );
    }
  }
}

/*
==============
CG_AddViewWeapon

Add the weapon, and flash for the player's view
==============
*/
void CG_AddViewWeapon( playerState_t *ps ) {
  refEntity_t hand;
  centity_t *cent;
  clientInfo_t  *ci;
  float   fovOffset;
  vec3_t    angles;
  weaponInfo_t  *weapon;

  if( ( ps->persistant[PERS_TEAM] == TEAM_SPECTATOR ) ||
      ( ps->stats[ STAT_STATE ] & SS_INFESTING ) ) {
    return;
  }

  //TA: no weapon carried - can't draw it
  if( ps->weapon == WP_NONE )
    return;
      
  if ( ps->pm_type == PM_INTERMISSION ) {
    return;
  }

  // no gun if in third person view
  if ( cg.renderingThirdPerson ) {
    return;
  }

  // allow the gun to be completely removed
  if ( !cg_drawGun.integer ) {
    vec3_t    origin;

    if ( cg.predictedPlayerState.eFlags & EF_FIRING ) {
      // special hack for lightning gun...
      VectorCopy( cg.refdef.vieworg, origin );
      VectorMA( origin, -8, cg.refdef.viewaxis[2], origin );
      CG_LightningBolt( &cg_entities[ps->clientNum], origin );
    }
    return;
  }

  // don't draw if testing a gun model
  if ( cg.testGun ) {
    return;
  }

  // drop gun lower at higher fov
  //if ( cg_fov.integer > 90 ) {
  //TA: the client side variable isn't used ( shouldn't iD have done this anyway? )
  if( cg.refdef.fov_y > 90 )
    fovOffset = -0.4 * ( cg.refdef.fov_y - 90 );
  else
    fovOffset = 0;

  cent = &cg.predictedPlayerEntity; // &cg_entities[cg.snap->ps.clientNum];
  CG_RegisterWeapon( ps->weapon );
  weapon = &cg_weapons[ ps->weapon ];

  memset (&hand, 0, sizeof(hand));

  // set up gun position
  CG_CalculateWeaponPosition( hand.origin, angles );

  VectorMA( hand.origin, cg_gun_x.value, cg.refdef.viewaxis[0], hand.origin );
  VectorMA( hand.origin, cg_gun_y.value, cg.refdef.viewaxis[1], hand.origin );
  VectorMA( hand.origin, (cg_gun_z.value+fovOffset), cg.refdef.viewaxis[2], hand.origin );

  AnglesToAxis( angles, hand.axis );

  // map torso animations to weapon animations
  if ( cg_gun_frame.integer ) {
    // development tool
    hand.frame = hand.oldframe = cg_gun_frame.integer;
    hand.backlerp = 0;
  } else {
    // get clientinfo for animation map
    ci = &cgs.clientinfo[ cent->currentState.clientNum ];
    hand.frame = CG_MapTorsoToWeaponFrame( ci, cent->pe.torso.frame );
    hand.oldframe = CG_MapTorsoToWeaponFrame( ci, cent->pe.torso.oldFrame );
    hand.backlerp = cent->pe.torso.backlerp;
  }

  hand.hModel = weapon->handsModel;
  hand.renderfx = RF_DEPTHHACK | RF_FIRST_PERSON | RF_MINLIGHT;

  // add everything onto the hand
  CG_AddPlayerWeapon( &hand, ps, &cg.predictedPlayerEntity );
}

/*
==============================================================================

WEAPON SELECTION

==============================================================================
*/

/*
===================
CG_DrawWeaponSelect

===================
*/
void CG_DrawWeaponSelect( void ) {
  int   i;
  //int   count;
  int   x, y, w;
  char  *name;
  float *color;
  int   ammo, clips, maxclips;

  // don't display if dead
  if ( cg.predictedPlayerState.stats[STAT_HEALTH] <= 0 ) {
    return;
  }

  color = CG_FadeColor( cg.weaponSelectTime, WEAPON_SELECT_TIME );
  if ( !color ) {
    return;
  }
  trap_R_SetColor( color );

  // showing weapon select clears pickup item display, but not the blend blob
  cg.itemPickupTime = 0;

  /*// count the number of weapons owned
  count = 0;
  for ( i = WP_GAUNTLET; i < WP_NUM_WEAPONS; i++ ) {
    if ( BG_gotWeapon( i, cg.snap->ps.stats ) ) {
      count++;
    }
  }*/
  
  //x = 320 - count * 20;
  //y = 380;

  x = 10;
  y = 10;

  for ( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ ) {
    if( !BG_gotWeapon( i, cg.snap->ps.stats ) )
      continue;

    CG_RegisterWeapon( i );

    // draw weapon icon
    CG_DrawPic( x, y, 16, 16, cg_weapons[i].weaponIcon );

    // draw selection marker
    if ( i == cg.weaponSelect ) {
      CG_DrawPic( x-2, y-2, 20, 20, cgs.media.selectShader );
    }

    BG_unpackAmmoArray( i, cg.snap->ps.ammo, cg.snap->ps.powerups, &ammo, &clips, &maxclips );

    // no ammo cross on top
    if ( !ammo && !clips && !BG_infiniteAmmo( i ) ) {
      CG_DrawPic( x, y, 16, 16, cgs.media.noammoShader );
    }

    y += 20;
  }
  
  for ( i = UP_TORCH; i < UP_NUM_UPGRADES; i++ ) {
    if( !BG_gotItem( i, cg.snap->ps.stats ) )
      continue;

    CG_RegisterUpgrade( i );

    // draw weapon icon
    CG_DrawPic( x, y, 16, 16, cg_upgrades[i].upgradeIcon );

    // draw selection marker
    if ( i == ( cg.weaponSelect - 32 ) ) {
      CG_DrawPic( x-2, y-2, 20, 20, cgs.media.selectShader );
    }

    y += 20;
  }
  
  //TA: yuck! :)
  if( y == 10 )
  {
    trap_R_SetColor( NULL );
    return;
  }

  // draw the selected name
  if( cg.weaponSelect <= 32 )
  {
    if ( cg_weapons[ cg.weaponSelect ].item )
    {
      name = cg_weapons[ cg.weaponSelect ].item->pickup_name;
      if ( name )
      {
        w = CG_DrawStrlen( name ) * BIGCHAR_WIDTH;
        x = ( SCREEN_WIDTH - w ) / 2;
        CG_DrawBigStringColor(x, y - 22, name, color);
      }
    }
  }
  else if( cg.weaponSelect > 32 )
  {
    if ( cg_upgrades[ cg.weaponSelect - 32 ].item )
    {
      name = cg_upgrades[ cg.weaponSelect - 32 ].item->pickup_name;
      if ( name )
      {
        w = CG_DrawStrlen( name ) * BIGCHAR_WIDTH;
        x = ( SCREEN_WIDTH - w ) / 2;
        CG_DrawBigStringColor(x, y - 22, name, color);
      }
    }
  }

  trap_R_SetColor( NULL );
}


/*
===============
CG_WeaponSelectable
===============
*/
static qboolean CG_WeaponSelectable( int i )
{
  int ammo, clips, maxclips;

  BG_unpackAmmoArray( i, cg.snap->ps.ammo, cg.snap->ps.powerups, &ammo, &clips, &maxclips );
  
  if ( !ammo && !clips && !BG_infiniteAmmo( i ) ) {
    return qfalse;
  }
  if ( !BG_gotWeapon( i, cg.snap->ps.stats ) ) {
    return qfalse;
  }

  return qtrue;
}


/*
===============
CG_ItemSelectable
===============
*/
static qboolean CG_ItemSelectable( int i )
{
  if( !BG_gotItem( i, cg.snap->ps.stats ) ) {
    return qfalse;
  }

  return qtrue;
}


/*
===============
CG_NextWeapon_f
===============
*/
void CG_NextWeapon_f( void ) {
  int   i;
  int   original;

  if ( !cg.snap ) {
    return;
  }
  if ( cg.snap->ps.pm_flags & PMF_FOLLOW ) {
    return;
  }

  cg.weaponSelectTime = cg.time;
  original = cg.weaponSelect;

  for ( i = 0 ; i < 64 ; i++ ) {
    cg.weaponSelect++;
    if ( cg.weaponSelect == 64 ) {
      cg.weaponSelect = 0;
    }
    /*if ( cg.weaponSelect == WP_GAUNTLET ) {
      continue;   // never cycle to gauntlet
    }*/

    if( cg.weaponSelect <= 32 )
    {
      if ( CG_WeaponSelectable( cg.weaponSelect ) ) {
        break;
      }
    }
    else if( cg.weaponSelect > 32 )
    {
      if ( CG_ItemSelectable( cg.weaponSelect - 32 ) ) {
        break;
      }
    }
  }
  if ( i == 64 ) {
    cg.weaponSelect = original;
  }
}

/*
===============
CG_PrevWeapon_f
===============
*/
void CG_PrevWeapon_f( void ) {
  int   i;
  int   original;

  if ( !cg.snap ) {
    return;
  }
  if ( cg.snap->ps.pm_flags & PMF_FOLLOW ) {
    return;
  }

  cg.weaponSelectTime = cg.time;
  original = cg.weaponSelect;

  for ( i = 0 ; i < 64 ; i++ ) {
    cg.weaponSelect--;
    if ( cg.weaponSelect == -1 ) {
      cg.weaponSelect = 63;
    }
    /*if ( cg.weaponSelect == WP_GAUNTLET ) {
      continue;   // never cycle to gauntlet
    }*/
    if( cg.weaponSelect <= 32 )
    {
      if ( CG_WeaponSelectable( cg.weaponSelect ) ) {
        break;
      }
    }
    else if( cg.weaponSelect > 32 )
    {
      if ( CG_ItemSelectable( cg.weaponSelect - 32 ) ) {
        break;
      }
    }
  }
  if ( i == 64 ) {
    cg.weaponSelect = original;
  }
}

/*
===============
CG_Weapon_f
===============
*/
void CG_Weapon_f( void ) {
  int   num;

  if ( !cg.snap ) {
    return;
  }
  if ( cg.snap->ps.pm_flags & PMF_FOLLOW ) {
    return;
  }

  num = atoi( CG_Argv( 1 ) );

  if ( num < 1 || num > 31 ) {
    return;
  }

  cg.weaponSelectTime = cg.time;

  if ( !BG_gotWeapon( num, cg.snap->ps.stats ) ) {
    return;   // don't have the weapon
  }

  cg.weaponSelect = num;
}

/*
===================
CG_OutOfAmmoChange

The current weapon has just run out of ammo
===================
*/
void CG_OutOfAmmoChange( void ) {
  int   i;

  //TA: mwhaha, must manually change weapons
  /*cg.weaponSelectTime = cg.time;

  for ( i = 31 ; i > 0 ; i-- ) {
    if ( CG_WeaponSelectable( i ) ) {
      cg.weaponSelect = i;
      break;
    }
  }*/
}



/*
===================================================================================================

WEAPON EVENTS

===================================================================================================
*/

/*
================
CG_FireWeapon

Caused by an EV_FIRE_WEAPON event
================
*/
void CG_FireWeapon( centity_t *cent ) {
  entityState_t *ent;
  int       c;
  weaponInfo_t  *weap;

  ent = &cent->currentState;
  if ( ent->weapon == WP_NONE ) {
    return;
  }
  if ( ent->weapon >= WP_NUM_WEAPONS ) {
    CG_Error( "CG_FireWeapon: ent->weapon >= WP_NUM_WEAPONS" );
    return;
  }
  weap = &cg_weapons[ ent->weapon ];

  // mark the entity as muzzle flashing, so when it is added it will
  // append the flash to the weapon model
  cent->muzzleFlashTime = cg.time;

  // lightning gun only does this this on initial press
  if ( ent->weapon == WP_LIGHTNING ) {
    if ( cent->pe.lightningFiring ) {
      return;
    }
  }

  // play quad sound if needed
  /*if ( cent->currentState.powerups & ( 1 << PW_QUAD ) ) {
    trap_S_StartSound (NULL, cent->currentState.number, CHAN_ITEM, cgs.media.quadSound );
  }*/

  // play a sound
  for ( c = 0 ; c < 4 ; c++ ) {
    if ( !weap->flashSound[c] ) {
      break;
    }
  }
  if ( c > 0 ) {
    c = rand() % c;
    if ( weap->flashSound[c] )
    {
      trap_S_StartSound( NULL, ent->number, CHAN_WEAPON, weap->flashSound[c] );
    }
  }

  // do brass ejection
  if ( weap->ejectBrassFunc && cg_brassTime.integer > 0 ) {
    weap->ejectBrassFunc( cent );
  }
}


/*
=================
CG_MissileHitWall

Caused by an EV_MISSILE_MISS event, or directly by local bullet tracing
=================
*/
void CG_MissileHitWall( int weapon, int clientNum, vec3_t origin, vec3_t dir, impactSound_t soundType ) {
  qhandle_t   mod;
  qhandle_t   mark;
  qhandle_t   shader;
  sfxHandle_t   sfx;
  float     radius;
  float     light;
  vec3_t      lightColor;
  localEntity_t *le;
  int       r;
  qboolean    alphaFade;
  qboolean    isSprite;
  int       duration;

  mark = 0;
  radius = 32;
  sfx = 0;
  mod = 0;
  shader = 0;
  light = 0;
  lightColor[0] = 1;
  lightColor[1] = 1;
  lightColor[2] = 0;

  // set defaults
  isSprite = qfalse;
  duration = 600;

  switch ( weapon ) {
  default:
  case WP_LIGHTNING:
    // no explosion at LG impact, it is added with the beam
    r = rand() & 3;
    if ( r < 2 ) {
      sfx = cgs.media.sfx_lghit2;
    } else if ( r == 2 ) {
      sfx = cgs.media.sfx_lghit1;
    } else {
      sfx = cgs.media.sfx_lghit3;
    }
    mark = cgs.media.holeMarkShader;
    radius = 12;
    break;
  case WP_GRENADE_LAUNCHER:
    mod = cgs.media.dishFlashModel;
    shader = cgs.media.grenadeExplosionShader;
    sfx = cgs.media.sfx_rockexp;
    mark = cgs.media.burnMarkShader;
    radius = 64;
    light = 300;
    isSprite = qtrue;
    break;
  case WP_ROCKET_LAUNCHER:
    mod = cgs.media.dishFlashModel;
    shader = cgs.media.rocketExplosionShader;
    sfx = cgs.media.sfx_rockexp;
    mark = cgs.media.burnMarkShader;
    radius = 64;
    light = 300;
    isSprite = qtrue;
    duration = 1000;
    lightColor[0] = 1;
    lightColor[1] = 0.75;
    lightColor[2] = 0.0;
    break;
  case WP_SAWBLADE_LAUNCHER:
    mod = cgs.media.dishFlashModel;
    shader = cgs.media.rocketExplosionShader;
    sfx = cgs.media.sfx_rockexp;
    mark = cgs.media.burnMarkShader;
    radius = 64;
    isSprite = qtrue;
    break;
  case WP_RAILGUN:
    mod = cgs.media.ringFlashModel;
    shader = cgs.media.railExplosionShader;
    sfx = cgs.media.sfx_plasmaexp;
    mark = cgs.media.energyMarkShader;
    radius = 24;
    break;
  case WP_FLAMER:
    mod = cgs.media.dishFlashModel;
    shader = cgs.media.flameExplShader;
    sfx = cgs.media.sfx_lghit2;
    mark = cgs.media.burnMarkShader;
    radius = 32;
    isSprite = qtrue;
    break;
  case WP_PLASMAGUN:
    mod = cgs.media.ringFlashModel;
    shader = cgs.media.plasmaExplosionShader;
    sfx = cgs.media.sfx_plasmaexp;
    mark = cgs.media.energyMarkShader;
    radius = 16;
    break;
  case WP_BFG:
    mod = cgs.media.dishFlashModel;
    shader = cgs.media.bfgExplosionShader;
    sfx = cgs.media.sfx_rockexp;
    mark = cgs.media.burnMarkShader;
    radius = 32;
    isSprite = qtrue;
    break;
  case WP_SHOTGUN:
    mod = cgs.media.bulletFlashModel;
    shader = cgs.media.bulletExplosionShader;
    mark = cgs.media.bulletMarkShader;
    sfx = 0;
    radius = 4;
    break;
  case WP_MACHINEGUN:
    mod = cgs.media.bulletFlashModel;
    shader = cgs.media.bulletExplosionShader;
    mark = cgs.media.bulletMarkShader;
  case WP_CHAINGUN:
    mod = cgs.media.bulletFlashModel;
    shader = cgs.media.bulletExplosionShader;
    mark = cgs.media.bulletMarkShader;
    
    r = rand() & 3;
    if ( r < 2 ) {
      sfx = cgs.media.sfx_ric1;
    } else if ( r == 2 ) {
      sfx = cgs.media.sfx_ric2;
    } else {
      sfx = cgs.media.sfx_ric3;
    }

    radius = 8;
    break;
  }

  if ( sfx ) {
    trap_S_StartSound( origin, ENTITYNUM_WORLD, CHAN_AUTO, sfx );
  }

  //
  // create the explosion
  //
  if ( mod ) {
    le = CG_MakeExplosion( origin, dir,
                 mod, shader,
                 duration, isSprite );
    le->light = light;
    VectorCopy( lightColor, le->lightColor );
    if ( weapon == WP_RAILGUN ) {
      // colorize with client color
      VectorCopy( cgs.clientinfo[clientNum].color, le->color );
    }
  }

  //
  // impact mark
  //
  alphaFade = (mark == cgs.media.energyMarkShader); // plasma fades alpha, all others fade color
  if ( weapon == WP_RAILGUN ) {
    float *color;

    // colorize with client color
    color = cgs.clientinfo[clientNum].color;
    CG_ImpactMark( mark, origin, dir, random()*360, color[0],color[1], color[2],1, alphaFade, radius, qfalse );
  } else {
    CG_ImpactMark( mark, origin, dir, random()*360, 1,1,1,1, alphaFade, radius, qfalse );
  }
}


/*
=================
CG_MissileHitPlayer
=================
*/
void CG_MissileHitPlayer( int weapon, vec3_t origin, vec3_t dir, int entityNum ) {
  CG_Bleed( origin, entityNum );

  // some weapons will make an explosion with the blood, while
  // others will just make the blood
  switch ( weapon ) {
  case WP_GRENADE_LAUNCHER:
  case WP_ROCKET_LAUNCHER:
    CG_MissileHitWall( weapon, 0, origin, dir, IMPACTSOUND_FLESH );
    break;
  default:
    break;
  }
}



/*
============================================================================

SHOTGUN TRACING

============================================================================
*/

/*
================
CG_ShotgunPellet
================
*/
static void CG_ShotgunPellet( vec3_t start, vec3_t end, int skipNum ) {
  trace_t   tr;
  int sourceContentType, destContentType;

  CG_Trace( &tr, start, NULL, NULL, end, skipNum, MASK_SHOT );

  sourceContentType = trap_CM_PointContents( start, 0 );
  destContentType = trap_CM_PointContents( tr.endpos, 0 );

  // FIXME: should probably move this cruft into CG_BubbleTrail
  if ( sourceContentType == destContentType ) {
    if ( sourceContentType & CONTENTS_WATER ) {
      CG_BubbleTrail( start, tr.endpos, 32 );
    }
  } else if ( sourceContentType & CONTENTS_WATER ) {
    trace_t trace;

    trap_CM_BoxTrace( &trace, end, start, NULL, NULL, 0, CONTENTS_WATER );
    CG_BubbleTrail( start, trace.endpos, 32 );
  } else if ( destContentType & CONTENTS_WATER ) {
    trace_t trace;

    trap_CM_BoxTrace( &trace, start, end, NULL, NULL, 0, CONTENTS_WATER );
    CG_BubbleTrail( tr.endpos, trace.endpos, 32 );
  }

  if (  tr.surfaceFlags & SURF_NOIMPACT ) {
    return;
  }

  if ( cg_entities[tr.entityNum].currentState.eType == ET_PLAYER ) {
    CG_MissileHitPlayer( WP_SHOTGUN, tr.endpos, tr.plane.normal, tr.entityNum );
  } else {
    if ( tr.surfaceFlags & SURF_NOIMPACT ) {
      // SURF_NOIMPACT will not make a flame puff or a mark
      return;
    }
    if ( tr.surfaceFlags & SURF_METALSTEPS ) {
      CG_MissileHitWall( WP_SHOTGUN, 0, tr.endpos, tr.plane.normal, IMPACTSOUND_METAL );
    } else {
      CG_MissileHitWall( WP_SHOTGUN, 0, tr.endpos, tr.plane.normal, IMPACTSOUND_DEFAULT );
    }
  }
}

/*
================
CG_ShotgunPattern

Perform the same traces the server did to locate the
hit splashes (FIXME: ranom seed isn't synce anymore)
================
*/
static void CG_ShotgunPattern( vec3_t origin, vec3_t origin2, int otherEntNum ) {
  int     i;
  float   r, u;
  vec3_t    end;
  vec3_t    forward, right, up;

  // derive the right and up vectors from the forward vector, because
  // the client won't have any other information
  VectorNormalize2( origin2, forward );
  PerpendicularVector( right, forward );
  CrossProduct( forward, right, up );

  // generate the "random" spread pattern
  for ( i = 0 ; i < DEFAULT_SHOTGUN_COUNT ; i++ ) {
    r = crandom() * DEFAULT_SHOTGUN_SPREAD * 16;
    u = crandom() * DEFAULT_SHOTGUN_SPREAD * 16;
    VectorMA( origin, 8192 * 16, forward, end);
    VectorMA (end, r, right, end);
    VectorMA (end, u, up, end);

    CG_ShotgunPellet( origin, end, otherEntNum );
  }
}

/*
==============
CG_ShotgunFire
==============
*/
void CG_ShotgunFire( entityState_t *es ) {
  vec3_t  v;
  int   contents;

  VectorSubtract( es->origin2, es->pos.trBase, v );
  VectorNormalize( v );
  VectorScale( v, 32, v );
  VectorAdd( es->pos.trBase, v, v );
  if ( cgs.glconfig.hardwareType != GLHW_RAGEPRO ) {
    // ragepro can't alpha fade, so don't even bother with smoke
    vec3_t      up;

    contents = trap_CM_PointContents( es->pos.trBase, 0 );
    if ( !( contents & CONTENTS_WATER ) ) {
      VectorSet( up, 0, 0, 8 );
      CG_SmokePuff( v, up, 32, 1, 1, 1, 0.33f, 900, cg.time, 0, LEF_PUFF_DONT_SCALE, cgs.media.shotgunSmokePuffShader );
    }
  }
  CG_ShotgunPattern( es->pos.trBase, es->origin2, es->otherEntityNum );
}

/*
============================================================================

BULLETS

============================================================================
*/


/*
===============
CG_Tracer
===============
*/
void CG_Tracer( vec3_t source, vec3_t dest ) {
  vec3_t    forward, right;
  polyVert_t  verts[4];
  vec3_t    line;
  float   len, begin, end;
  vec3_t    start, finish;
  vec3_t    midpoint;

  // tracer
  VectorSubtract( dest, source, forward );
  len = VectorNormalize( forward );

  // start at least a little ways from the muzzle
  if ( len < 100 ) {
    return;
  }
  begin = 50 + random() * (len - 60);
  end = begin + cg_tracerLength.value;
  if ( end > len ) {
    end = len;
  }
  VectorMA( source, begin, forward, start );
  VectorMA( source, end, forward, finish );

  line[0] = DotProduct( forward, cg.refdef.viewaxis[1] );
  line[1] = DotProduct( forward, cg.refdef.viewaxis[2] );

  VectorScale( cg.refdef.viewaxis[1], line[1], right );
  VectorMA( right, -line[0], cg.refdef.viewaxis[2], right );
  VectorNormalize( right );

  VectorMA( finish, cg_tracerWidth.value, right, verts[0].xyz );
  verts[0].st[0] = 0;
  verts[0].st[1] = 1;
  verts[0].modulate[0] = 255;
  verts[0].modulate[1] = 255;
  verts[0].modulate[2] = 255;
  verts[0].modulate[3] = 255;

  VectorMA( finish, -cg_tracerWidth.value, right, verts[1].xyz );
  verts[1].st[0] = 1;
  verts[1].st[1] = 0;
  verts[1].modulate[0] = 255;
  verts[1].modulate[1] = 255;
  verts[1].modulate[2] = 255;
  verts[1].modulate[3] = 255;

  VectorMA( start, -cg_tracerWidth.value, right, verts[2].xyz );
  verts[2].st[0] = 1;
  verts[2].st[1] = 1;
  verts[2].modulate[0] = 255;
  verts[2].modulate[1] = 255;
  verts[2].modulate[2] = 255;
  verts[2].modulate[3] = 255;

  VectorMA( start, cg_tracerWidth.value, right, verts[3].xyz );
  verts[3].st[0] = 0;
  verts[3].st[1] = 0;
  verts[3].modulate[0] = 255;
  verts[3].modulate[1] = 255;
  verts[3].modulate[2] = 255;
  verts[3].modulate[3] = 255;

  trap_R_AddPolyToScene( cgs.media.tracerShader, 4, verts );

  midpoint[0] = ( start[0] + finish[0] ) * 0.5;
  midpoint[1] = ( start[1] + finish[1] ) * 0.5;
  midpoint[2] = ( start[2] + finish[2] ) * 0.5;

  // add the tracer sound
  trap_S_StartSound( midpoint, ENTITYNUM_WORLD, CHAN_AUTO, cgs.media.tracerSound );

}


/*
======================
CG_CalcMuzzlePoint
======================
*/
static qboolean CG_CalcMuzzlePoint( int entityNum, vec3_t muzzle ) {
  vec3_t    forward;
  centity_t *cent;
  int     anim;

  if ( entityNum == cg.snap->ps.clientNum ) {
    VectorCopy( cg.snap->ps.origin, muzzle );
    muzzle[2] += cg.snap->ps.viewheight;
    AngleVectors( cg.snap->ps.viewangles, forward, NULL, NULL );
    VectorMA( muzzle, 14, forward, muzzle );
    return qtrue;
  }

  cent = &cg_entities[entityNum];
  if ( !cent->currentValid ) {
    return qfalse;
  }

  VectorCopy( cent->currentState.pos.trBase, muzzle );

  AngleVectors( cent->currentState.apos.trBase, forward, NULL, NULL );
  anim = cent->currentState.legsAnim & ~ANIM_TOGGLEBIT;
  if ( anim == LEGS_WALKCR || anim == LEGS_IDLECR ) {
    muzzle[2] += CROUCH_VIEWHEIGHT;
  } else {
    muzzle[2] += DEFAULT_VIEWHEIGHT;
  }

  VectorMA( muzzle, 14, forward, muzzle );

  return qtrue;

}

/*
======================
CG_Bullet

Renders bullet effects.
======================
*/
void CG_Bullet( vec3_t end, int sourceEntityNum, vec3_t normal, qboolean flesh, int fleshEntityNum ) {
  trace_t trace;
  int sourceContentType, destContentType;
  vec3_t    start;

  // if the shooter is currently valid, calc a source point and possibly
  // do trail effects
  if ( sourceEntityNum >= 0 && cg_tracerChance.value > 0 ) {
    if ( CG_CalcMuzzlePoint( sourceEntityNum, start ) ) {
      sourceContentType = trap_CM_PointContents( start, 0 );
      destContentType = trap_CM_PointContents( end, 0 );

      // do a complete bubble trail if necessary
      if ( ( sourceContentType == destContentType ) && ( sourceContentType & CONTENTS_WATER ) ) {
        CG_BubbleTrail( start, end, 32 );
      }
      // bubble trail from water into air
      else if ( ( sourceContentType & CONTENTS_WATER ) ) {
        trap_CM_BoxTrace( &trace, end, start, NULL, NULL, 0, CONTENTS_WATER );
        CG_BubbleTrail( start, trace.endpos, 32 );
      }
      // bubble trail from air into water
      else if ( ( destContentType & CONTENTS_WATER ) ) {
        trap_CM_BoxTrace( &trace, start, end, NULL, NULL, 0, CONTENTS_WATER );
        CG_BubbleTrail( trace.endpos, end, 32 );
      }

      // draw a tracer
      if ( random() < cg_tracerChance.value ) {
        CG_Tracer( start, end );
      }
    }
  }

  // impact splash and mark
  if ( flesh ) {
    CG_Bleed( end, fleshEntityNum );
  } else {
    CG_MissileHitWall( WP_MACHINEGUN, 0, end, normal, IMPACTSOUND_DEFAULT );
  }

}