/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.
Copyright (C) 2000-2009 Darklegion Development

This file is part of Tremulous.

Tremulous is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.

Tremulous 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with Tremulous; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/


#include "cg_local.h"

char *cg_buildableSoundNames[ MAX_BUILDABLE_ANIMATIONS ] =
{
  "construct1.wav",
  "construct2.wav",
  "idle1.wav",
  "idle2.wav",
  "idle3.wav",
  "attack1.wav",
  "attack2.wav",
  "spawn1.wav",
  "spawn2.wav",
  "pain1.wav",
  "pain2.wav",
  "destroy1.wav",
  "destroy2.wav",
  "destroyed.wav"
};

static sfxHandle_t defaultAlienSounds[ MAX_BUILDABLE_ANIMATIONS ];
static sfxHandle_t defaultHumanSounds[ MAX_BUILDABLE_ANIMATIONS ];

/*
======================
CG_RenderCuboid

Render a cuboid with proper lighting and UV maps
======================
*/
static void CG_RenderCuboid_Face( vec3_t a, vec3_t b, vec3_t c, vec3_t d, 
                                  int da, int db,
                                  vec4_t color,
                                  float texscale, qhandle_t shader )
{
  polyVert_t verts[ 4 ];
  
  VectorCopy( d, verts[ 0 ].xyz );
  verts[ 0 ].st[ 0 ] = d[ da ] * texscale;
  verts[ 0 ].st[ 1 ] = d[ db ] * texscale;
  Vector4Copy( color, verts[ 0 ].modulate );
  VectorCopy( c, verts[ 1 ].xyz );
  verts[ 1 ].st[ 0 ] = c[ da ] * texscale;
  verts[ 1 ].st[ 1 ] = c[ db ] * texscale;
  Vector4Copy( color, verts[ 1 ].modulate );
  VectorCopy( b, verts[ 2 ].xyz );
  verts[ 2 ].st[ 0 ] = b[ da ] * texscale;
  verts[ 2 ].st[ 1 ] = b[ db ] * texscale;
  Vector4Copy( color, verts[ 2 ].modulate );
  VectorCopy( a, verts[ 3 ].xyz );
  verts[ 3 ].st[ 0 ] = a[ da ] * texscale;
  verts[ 3 ].st[ 1 ] = a[ db ] * texscale;
  Vector4Copy( color, verts[ 3 ].modulate );

  trap_R_AddPolyToScene( shader, 4, verts );
}

static void CG_RenderCuboid( vec3_t mins, vec3_t maxs, float texscale, qhandle_t shader )
{
  int i;
  vec3_t midpoint, ambient, directed, idc;
  vec4_t color = { 255.0f, 255.0f, 255.0f, 255.0f };
  vec3_t ppp, ppn, pnp, pnn, npp, npn, nnp, nnn;
 
  //lighting
  VectorAdd( mins, maxs, midpoint );
  VectorScale( midpoint, 0.5f, midpoint ); 
  trap_R_LightForPoint( midpoint, ambient, directed, idc );
  VectorAdd( ambient, directed, color );
  for( i = 0; i < 3; i++ )
    if( color[ i ] > 255.0f )
      color[ i ] = 255.0f;
  
  //vertices 
  VectorSet( ppp, maxs[ 0 ], maxs[ 1 ], maxs[ 2 ] );
  VectorSet( ppn, maxs[ 0 ], maxs[ 1 ], mins[ 2 ] );
  VectorSet( pnp, maxs[ 0 ], mins[ 1 ], maxs[ 2 ] );
  VectorSet( pnn, maxs[ 0 ], mins[ 1 ], mins[ 2 ] );
  VectorSet( npp, mins[ 0 ], maxs[ 1 ], maxs[ 2 ] );
  VectorSet( npn, mins[ 0 ], maxs[ 1 ], mins[ 2 ] );
  VectorSet( nnp, mins[ 0 ], mins[ 1 ], maxs[ 2 ] );
  VectorSet( nnn, mins[ 0 ], mins[ 1 ], mins[ 2 ] );

  //faces
  //+-x
  CG_RenderCuboid_Face( ppn, ppp, pnp, pnn, 1, 2, color, texscale, shader );
  CG_RenderCuboid_Face( nnn, nnp, npp, npn, 1, 2, color, texscale, shader );
  //+-y
  CG_RenderCuboid_Face( ppp, ppn, npn, npp, 0, 2, color, texscale, shader );
  CG_RenderCuboid_Face( nnp, nnn, pnn, pnp, 0, 2, color, texscale, shader );
  //+-z
  CG_RenderCuboid_Face( npp, nnp, pnp, ppp, 0, 1, color, texscale, shader );
  CG_RenderCuboid_Face( ppn, pnn, nnn, npn, 0, 1, color, texscale, shader );
}

/*
======================
CG_DrawCuboid

cg_drawbbox for cuboids
======================
*/
static void CG_DrawCuboidFace( vec3_t a, vec3_t b, vec3_t c, vec3_t d, qhandle_t shader )
{
  polyVert_t  verts[ 4 ];
  vec4_t      color = { 255.0f, 255.0f, 255.0f, 255.0f };
  VectorCopy( d, verts[ 0 ].xyz );
  verts[ 0 ].st[ 0 ] = 1;
  verts[ 0 ].st[ 1 ] = 1;
  Vector4Copy( color, verts[ 0 ].modulate );
  VectorCopy( c, verts[ 1 ].xyz );
  verts[ 1 ].st[ 0 ] = 1;
  verts[ 1 ].st[ 1 ] = 0;
  Vector4Copy( color, verts[ 1 ].modulate );
  VectorCopy( b, verts[ 2 ].xyz );
  verts[ 2 ].st[ 0 ] = 0;
  verts[ 2 ].st[ 1 ] = 0;
  Vector4Copy( color, verts[ 2 ].modulate );
  VectorCopy( a, verts[ 3 ].xyz );
  verts[ 3 ].st[ 0 ] = 0;
  verts[ 3 ].st[ 1 ] = 1;
  Vector4Copy( color, verts[ 3 ].modulate );
  trap_R_AddPolyToScene( (shader ? shader : cgs.media.outlineShader ), 4, verts );
}

void CG_DrawCuboid( vec3_t origin, vec3_t dims, qhandle_t shader, int margin )
{
  vec3_t  mins, maxs;
  vec3_t  ppp, mpp, mmp, pmp;
  vec3_t  mmm, pmm, ppm, mpm;
  int i;
  BG_CuboidBBox(dims,mins,maxs);
  for(i=0;i<3&&margin;i++)
  {
   mins[i]-=margin;
   maxs[i]+=margin;
  }
  ppp[ 0 ] = origin[ 0 ] + maxs[ 0 ];
  ppp[ 1 ] = origin[ 1 ] + maxs[ 1 ];
  ppp[ 2 ] = origin[ 2 ] + maxs[ 2 ];
  mpp[ 0 ] = origin[ 0 ] + mins[ 0 ];
  mpp[ 1 ] = origin[ 1 ] + maxs[ 1 ];
  mpp[ 2 ] = origin[ 2 ] + maxs[ 2 ];
  mmp[ 0 ] = origin[ 0 ] + mins[ 0 ];
  mmp[ 1 ] = origin[ 1 ] + mins[ 1 ];
  mmp[ 2 ] = origin[ 2 ] + maxs[ 2 ];
  pmp[ 0 ] = origin[ 0 ] + maxs[ 0 ];
  pmp[ 1 ] = origin[ 1 ] + mins[ 1 ];
  pmp[ 2 ] = origin[ 2 ] + maxs[ 2 ];
  ppm[ 0 ] = origin[ 0 ] + maxs[ 0 ];
  ppm[ 1 ] = origin[ 1 ] + maxs[ 1 ];
  ppm[ 2 ] = origin[ 2 ] + mins[ 2 ];
  mpm[ 0 ] = origin[ 0 ] + mins[ 0 ];
  mpm[ 1 ] = origin[ 1 ] + maxs[ 1 ];
  mpm[ 2 ] = origin[ 2 ] + mins[ 2 ];
  mmm[ 0 ] = origin[ 0 ] + mins[ 0 ];
  mmm[ 1 ] = origin[ 1 ] + mins[ 1 ];
  mmm[ 2 ] = origin[ 2 ] + mins[ 2 ];
  pmm[ 0 ] = origin[ 0 ] + maxs[ 0 ];
  pmm[ 1 ] = origin[ 1 ] + mins[ 1 ];
  pmm[ 2 ] = origin[ 2 ] + mins[ 2 ];
  CG_DrawCuboidFace( ppp, mpp, mmp, pmp, shader );
  CG_DrawCuboidFace( ppp, pmp, pmm, ppm, shader );
  CG_DrawCuboidFace( mpp, ppp, ppm, mpm, shader );
  CG_DrawCuboidFace( mmp, mpp, mpm, mmm, shader );
  CG_DrawCuboidFace( pmp, mmp, mmm, pmm, shader );
  CG_DrawCuboidFace( mmm, mpm, ppm, pmm, shader );
}

void CG_DrawCuboidAxis(vec3_t cuboidOrigin, vec3_t size, int axis, qhandle_t shader)
{
 vec3_t origin, localZ, localX, localY, start, end, quad[2][4];
 float len;
 
 VectorCopy(cuboidOrigin,origin);
 origin[2]+=size[2]/2.0f;
 VectorClear(localZ);
 VectorClear(localX);
 switch(axis)
 {
  case 0:
   localZ[0]=1.0f;
   localX[1]=1.0f;
   len=size[0];
   break;
  case 1:
   localZ[1]=1.0f;
   localX[2]=1.0f;
   len=size[1];
   break;
  default:
   localZ[2]=1.0f;
   localX[0]=1.0f;
   len=size[2];
   break;
 }
 CrossProduct(localZ,localX,localY);
 VectorMA(origin,-MAX(len/2.0f,32),localZ,start);
 VectorMA(origin,MAX(len/2.0f,32),localZ,end);
 VectorMA(start,4.0f,localX,quad[0][0]);
 VectorMA(start,-4.0f,localX,quad[0][1]);
 VectorMA(end,-4.0f,localX,quad[0][2]);
 VectorMA(end,4.0f,localX,quad[0][3]);
 VectorMA(start,4.0f,localY,quad[1][0]);
 VectorMA(start,-4.0f,localY,quad[1][1]);
 VectorMA(end,-4.0f,localY,quad[1][2]);
 VectorMA(end,4.0f,localY,quad[1][3]); 
 CG_DrawCuboidFace(quad[0][0],quad[0][1],quad[0][2],quad[0][3],shader);
 CG_DrawCuboidFace(quad[1][0],quad[1][1],quad[1][2],quad[1][3],shader);
}

/*
===================
CG_InitCuboidExplosions
CG_CuboidExplosion
CG_DrawCuboidParticles

Simple particle system based on cg_particles.c
===================
*/

#define MAX_CPARTICLES 4096
#define MAX_CPARTICLES_PER_EXPLOSION 128

typedef struct
{
 qboolean use;
 vec3_t origin;
 vec3_t velocity;
 float rot,rotspeed;
 float  size;
 qhandle_t shader;
 int lastphysics;
 int spawntime;
 qboolean solid;
} cuboidParticle_t;

cuboidParticle_t cuboidParticles[MAX_CPARTICLES];
int              cuboidParticlesCount=0;
qboolean         cuboidParticles_init=qfalse;

void CG_InitCuboidExplosions(void)
{
 memset(cuboidParticles,0,sizeof(cuboidParticles));
 cuboidParticles_init=qtrue;
 return;
}

void CG_CuboidExplosion(buildable_t buildable, vec3_t origin, vec3_t dims)
{
 int i,p,q;
 float v,s;
 cuboidParticle_t *pc;
 vec3_t sdims;
 const cuboidInfo_t *info;
 
 info=&cg_cuboids[buildable-CUBOID_FIRST];
 v=dims[0]*dims[1]*dims[2];
 for(i=0;i<info->destroySoundCount;i++)
  if(v*5.5306341e-5>=info->destroySoundThresholds[i])
  {
   trap_S_StartSound(origin,ENTITYNUM_WORLD,CHAN_AUTO,info->destroySounds[i]);
   break;
  }
  
  if(!info->fragmentCount)
   return;
  
 BG_CuboidSortSize(dims,sdims);
 
 q=cg_cuboidPSQuality.integer;
 
 if(q<=0)
  p=2;
 else if(q==1)
  p=ceil(sqrt(v/pow(sdims[2],3)))*2;
 else if(q==2)
  p=ceil(pow(v/pow(sdims[2],3),0.75f))*3;
 else
  p=ceil(pow(v/pow(sdims[2],3),0.85f))*6;
 p=MIN(p,MAX_CPARTICLES_PER_EXPLOSION);
 
 s=ceil(pow(v/p,1.0f/3.0f))*1.2f;
 
 for(i=0;i<MAX_CPARTICLES&&p;i++)
 {
  pc=&cuboidParticles[i];
  if(pc->use)
   continue;
  pc->solid=qfalse;
  pc->origin[0]=origin[0]+crandom()*dims[0]*0.5f;
  pc->origin[1]=origin[1]+crandom()*dims[1]*0.5f;
  pc->origin[2]=origin[2]+dims[2]/2.0f+crandom()*dims[2]*0.5f;
  VectorSubtract(pc->origin,origin,pc->velocity);
  pc->size=s;
  pc->rot=crandom()*360.0f;
  pc->rotspeed=crandom()*450.0f;
  pc->shader=info->fragments[(int)(rand()%info->fragmentCount)];
  pc->lastphysics=pc->spawntime=cg.time;
  pc->use=qtrue;
  cuboidParticlesCount++;
  p--;
 }
}

void CG_CuboidParticlePhysics(cuboidParticle_t *pc)
{
 float t;
 int c;
 vec3_t newOrigin;
 t=(cg.time-pc->lastphysics)/1e3;
 pc->lastphysics=cg.time;
 if(pc->spawntime+3000<cg.time)
 {
  pc->use=qfalse;
  cuboidParticlesCount--;
  return;
 }
 if(pc->solid)
  return;
 pc->rot+=pc->rotspeed*t;
 pc->velocity[2]-=t*DEFAULT_GRAVITY;
 VectorMA(pc->origin,t,pc->velocity,newOrigin);
 c=trap_CM_PointContents(newOrigin,0);
 if(c&CONTENTS_NODROP)
 {
  pc->use=qfalse;
  cuboidParticlesCount--;
  return;
 }
 if(c&(CONTENTS_SOLID|CONTENTS_BODY))
 {
  pc->solid=qtrue;
  return;
 }
 VectorCopy(newOrigin,pc->origin);
}

void CG_DrawCuboidParticles(void)
{
 int i,j,drawn=0;
 //vec3_t alight, dlight, lightdir;
 cuboidParticle_t *pc;
 refEntity_t re;
  
 if(!cuboidParticles_init)
  CG_InitCuboidExplosions();
  
 for(i=0;i<MAX_CPARTICLES;i++)
 {
  pc=&cuboidParticles[i];
  
  if(!pc->use)
   continue;
  CG_CuboidParticlePhysics(pc);
  if(!pc->use) //particle physics can delete a particle
   continue;
  memset(&re,0,sizeof(refEntity_t));
  re.reType=RT_SPRITE;
  VectorCopy(pc->origin,re.origin);
  //trap_R_LightForPoint(pc->origin,alight,dlight,lightdir);
  //for(j=0;j<3;j++)
  // re.shaderRGBA[j]=(byte)alight[j];
  for(j=0;j<3;j++)
   re.shaderRGBA[j]=255;
  re.customShader=pc->shader;
  re.radius=pc->size;
  re.rotation=pc->rot;
  trap_R_AddRefEntityToScene(&re);
  drawn++;
 }
}


/*
===================
CG_AlienBuildableExplosion

Generated a bunch of gibs launching out from a location
===================
*/
void CG_AlienBuildableExplosion( vec3_t origin, vec3_t dir )
{
  particleSystem_t  *ps;

  trap_S_StartSound( origin, ENTITYNUM_WORLD, CHAN_AUTO, cgs.media.alienBuildableExplosion );

  //particle system
  ps = CG_SpawnNewParticleSystem( cgs.media.alienBuildableDestroyedPS );

  if( CG_IsParticleSystemValid( &ps ) )
  {
    CG_SetAttachmentPoint( &ps->attachment, origin );
    CG_SetParticleSystemNormal( ps, dir );
    CG_AttachToPoint( &ps->attachment );
  }
}

/*
=================
CG_HumanBuildableExplosion

Called for human buildables as they are destroyed
=================
*/
void CG_HumanBuildableExplosion( vec3_t origin, vec3_t dir )
{
  particleSystem_t  *ps;

  trap_S_StartSound( origin, ENTITYNUM_WORLD, CHAN_AUTO, cgs.media.humanBuildableExplosion );

  //particle system
  ps = CG_SpawnNewParticleSystem( cgs.media.humanBuildableDestroyedPS );

  if( CG_IsParticleSystemValid( &ps ) )
  {
    CG_SetAttachmentPoint( &ps->attachment, origin );
    CG_SetParticleSystemNormal( ps, dir );
    CG_AttachToPoint( &ps->attachment );
  }
}


#define CREEP_SIZE            64.0f
#define CREEP_DISTANCE        64.0f

/*
==================
CG_Creep
==================
*/
static void CG_Creep( centity_t *cent )
{
  int           msec;
  float         size, frac;
  trace_t       tr;
  vec3_t        temp, origin;
  int           scaleUpTime = BG_Buildable( cent->currentState.modelindex, cent->currentState.angles )->buildTime;
  int           time;

  if(BG_Buildable(cent->currentState.modelindex,NULL)->cuboid)
   return;
  
  time = cent->currentState.time;

  //should the creep be growing or receding?
  if( time >= 0 )
  {
    msec = cg.time - time;
    if( msec >= 0 && msec < scaleUpTime )
      frac = (float)msec / scaleUpTime;
    else
      frac = 1.0f;
  }
  else if( time < 0 )
  {
    msec = cg.time + time;
    if( msec >= 0 && msec < CREEP_SCALEDOWN_TIME )
      frac = 1.0f - ( (float)msec / CREEP_SCALEDOWN_TIME );
    else
      frac = 0.0f;
  }

  VectorCopy( cent->currentState.origin2, temp );
  VectorScale( temp, -CREEP_DISTANCE, temp );
  VectorAdd( temp, cent->lerpOrigin, temp );

  CG_Trace( &tr, cent->lerpOrigin, NULL, NULL, temp, cent->currentState.number, MASK_PLAYERSOLID );

  if(tr.entityNum!=ENTITYNUM_WORLD)
   return;
  
  VectorCopy( tr.endpos, origin );

  size = CREEP_SIZE * frac;

  if( size > 0.0f && tr.fraction < 1.0f )
    CG_ImpactMark( cgs.media.creepShader, origin, cent->currentState.origin2,
                   0.0f, 1.0f, 1.0f, 1.0f, 1.0f, qfalse, size, qtrue );
}

/*
======================
CG_ParseBuildableAnimationFile

Read a configuration file containing animation counts and rates
models/buildables/hivemind/animation.cfg, etc
======================
*/
static qboolean CG_ParseBuildableAnimationFile( const char *filename, buildable_t buildable )
{
  char          *text_p;
  int           len;
  int           i;
  char          *token;
  float         fps;
  char          text[ 20000 ];
  fileHandle_t  f;
  animation_t   *animations;
  
  if(BG_Buildable(buildable,NULL)->cuboid)
   return qtrue;

  animations = cg_buildables[ buildable ].animations;

  // load the file
  len = trap_FS_FOpenFile( filename, &f, FS_READ );
  if( len < 0 )
    return qfalse;

  if( len == 0 || len >= sizeof( text ) - 1 )
  {
    trap_FS_FCloseFile( f );
    CG_Printf( "File %s is %s\n", filename, len == 0 ? "empty" : "too long" );
    return qfalse;
  }

  trap_FS_Read( text, len, f );
  text[ len ] = 0;
  trap_FS_FCloseFile( f );

  // parse the text
  text_p = text;

  // read information for each frame
  for( i = BANIM_NONE + 1; i < MAX_BUILDABLE_ANIMATIONS; i++ )
  {

    token = COM_Parse( &text_p );
    if( !*token )
      break;

    animations[ i ].firstFrame = atoi( token );

    token = COM_Parse( &text_p );
    if( !*token )
      break;

    animations[ i ].numFrames = atoi( token );
    animations[ i ].reversed = qfalse;
    animations[ i ].flipflop = qfalse;

    // if numFrames is negative the animation is reversed
    if( animations[ i ].numFrames < 0 )
    {
      animations[ i ].numFrames = -animations[ i ].numFrames;
      animations[ i ].reversed = qtrue;
    }

    token = COM_Parse( &text_p );
    if ( !*token )
      break;

    animations[i].loopFrames = atoi( token );

    token = COM_Parse( &text_p );
    if( !*token )
      break;

    fps = atof( token );
    if( fps == 0 )
      fps = 1;

    animations[ i ].frameLerp = 1000 / fps;
    animations[ i ].initialLerp = 1000 / fps;
  }

  if( i != MAX_BUILDABLE_ANIMATIONS )
  {
    CG_Printf( "Error parsing animation file: %s\n", filename );
    return qfalse;
  }

  return qtrue;
}

/*
======================
CG_ParseBuildableSoundFile

Read a configuration file containing sound properties
sound/buildables/hivemind/sound.cfg, etc
======================
*/
static qboolean CG_ParseBuildableSoundFile( const char *filename, buildable_t buildable )
{
  char          *text_p;
  int           len;
  int           i;
  char          *token;
  char          text[ 20000 ];
  fileHandle_t  f;
  sound_t       *sounds;

  if(BG_Buildable(buildable,NULL)->cuboid)
   return qtrue;
  
  sounds = cg_buildables[ buildable ].sounds;

  // load the file
  len = trap_FS_FOpenFile( filename, &f, FS_READ );
  if ( len < 0 )
    return qfalse;

  if ( len == 0 || len >= sizeof( text ) - 1 )
  {
    trap_FS_FCloseFile( f );
    CG_Printf( "File %s is %s\n", filename, len == 0 ? "empty" : "too long" );
    return qfalse;
  }

  trap_FS_Read( text, len, f );
  text[len] = 0;
  trap_FS_FCloseFile( f );

  // parse the text
  text_p = text;

  // read information for each frame
  for( i = BANIM_NONE + 1; i < MAX_BUILDABLE_ANIMATIONS; i++ )
  {

    token = COM_Parse( &text_p );
    if ( !*token )
      break;

    sounds[ i ].enabled = atoi( token );

    token = COM_Parse( &text_p );
    if ( !*token )
      break;

    sounds[ i ].looped = atoi( token );

  }

  if( i != MAX_BUILDABLE_ANIMATIONS )
  {
    CG_Printf( "Error parsing sound file: %s\n", filename );
    return qfalse;
  }

  return qtrue;
}
/*
===============
CG_InitBuildables

Initialises the animation db
===============
*/
void CG_InitBuildables( void )
{
  char          filename[ MAX_QPATH ];
  char          soundfile[ MAX_QPATH ];
  char          *buildableName;
  const char    *cuboidName;
  char          *modelFile;
  int           i;
  int           j;
  fileHandle_t  f;
  pc_token_t    token;
  cuboidInfo_t  *cuboid;
 
  memset( cg_buildables, 0, sizeof( cg_buildables ) );

  //default sounds
  for( j = BANIM_NONE + 1; j < MAX_BUILDABLE_ANIMATIONS; j++ )
  {
    strcpy( soundfile, cg_buildableSoundNames[ j - 1 ] );

    Com_sprintf( filename, sizeof( filename ), "sound/buildables/alien/%s", soundfile );
    defaultAlienSounds[ j ] = trap_S_RegisterSound( filename, qfalse );

    Com_sprintf( filename, sizeof( filename ), "sound/buildables/human/%s", soundfile );
    defaultHumanSounds[ j ] = trap_S_RegisterSound( filename, qfalse );
  }

  cg.buildablesFraction = 0.0f;

  for( i = BA_NONE + 1; i < CUBOID_FIRST; i++ )
  {
    buildableName = BG_Buildable( i, NULL )->name;

    //animation.cfg
    Com_sprintf( filename, sizeof( filename ), "models/buildables/%s/animation.cfg", buildableName );
    if ( !CG_ParseBuildableAnimationFile( filename, i ) )
      Com_Printf( S_COLOR_YELLOW "WARNING: failed to load animation file %s\n", filename );

    //sound.cfg
    Com_sprintf( filename, sizeof( filename ), "sound/buildables/%s/sound.cfg", buildableName );
    if ( !CG_ParseBuildableSoundFile( filename, i ) )
      Com_Printf( S_COLOR_YELLOW "WARNING: failed to load sound file %s\n", filename );

    //models
    for( j = 0; j <= 3; j++ )
    {
      modelFile = BG_BuildableConfig( i )->models[ j ];
      if( strlen( modelFile ) > 0 )
        cg_buildables[ i ].models[ j ] = trap_R_RegisterModel( modelFile );
    }

    //sounds
    for( j = BANIM_NONE + 1; j < MAX_BUILDABLE_ANIMATIONS; j++ )
    {
      strcpy( soundfile, cg_buildableSoundNames[ j - 1 ] );
      Com_sprintf( filename, sizeof( filename ), "sound/buildables/%s/%s", buildableName, soundfile );

      if( cg_buildables[ i ].sounds[ j ].enabled )
      {
        if( trap_FS_FOpenFile( filename, &f, FS_READ ) > 0 )
        {
          //file exists so close it
          trap_FS_FCloseFile( f );

          cg_buildables[ i ].sounds[ j ].sound = trap_S_RegisterSound( filename, qfalse );
        }
        else
        {
          //file doesn't exist - use default
          if( BG_Buildable( i, NULL )->team == TEAM_ALIENS )
            cg_buildables[ i ].sounds[ j ].sound = defaultAlienSounds[ j ];
          else
            cg_buildables[ i ].sounds[ j ].sound = defaultHumanSounds[ j ];
        }
      }
    }

    cg.buildablesFraction = (float)i / (float)( CUBOID_FIRST - 1 );
    trap_UpdateScreen( );
  }

  cgs.media.teslaZapTS = CG_RegisterTrailSystem( "models/buildables/tesla/zap" );
  
  memset( cg_cuboids, 0, sizeof( cg_cuboids ) );
  
  for( i = CUBOID_FIRST; i <= CUBOID_LAST; i++ )
  {
    cuboid = &cg_cuboids[ i - CUBOID_FIRST ];
    cuboidName = BG_CuboidAttributes( i )->file;
    
    Com_sprintf( filename, sizeof( filename ), "configs/cuboids/%s.cfg", cuboidName ); 
    if( !( f = trap_Parse_LoadSource( filename ) ) )
    {
      Com_Printf( S_COLOR_YELLOW "WARNING: failed to load cuboid config %s\n", filename );
      continue;
    }
    while( 1 )
    {
      if( !trap_Parse_ReadToken( f, &token ) )
        break;
      else if( !Q_stricmp( token.string, "texture" ) )
      {
        if( cuboid->textureCount == MAX_CUBOID_TEXTURES )
        {
          Com_Printf( S_COLOR_RED "ERROR: too many cuboid textures in %s\n", filename );
          break;
        }
        if( trap_Parse_ReadToken( f, &token ) )
        {
          cuboid->textureThresholds[ cuboid->textureCount++ ] = atof( token.string );
          if( trap_Parse_ReadToken( f, &token ) )
            cuboid->textures[ cuboid->textureCount - 1 ] = trap_R_RegisterShader( token.string );
          else
          {
            Com_Printf( S_COLOR_YELLOW "WARNING: incomplete cuboid texture definition in %s\n", filename );
            cuboid->textureCount--;
          }
        }
      }
      else if( !Q_stricmp( token.string, "useCracks" ) )
        cuboid->useCracks=qtrue;
      else if( !Q_stricmp( token.string, "fragment")  )
      {
        if( cuboid->fragmentCount == MAX_CUBOID_FRAGMENTS )
        {
          Com_Printf( S_COLOR_RED "ERROR: too many cuboid fragments in %s\n", filename );
          break;
        }
        if( trap_Parse_ReadToken( f, &token ) )
          cuboid->fragments[ cuboid->fragmentCount++ ] = trap_R_RegisterShader( token.string );
      }
      else if( !Q_stricmp( token.string, "painSound" ) )
      {
        if( cuboid->painSoundCount == MAX_CUBOID_SOUNDS )
        {
          Com_Printf( S_COLOR_RED "ERROR: too many cuboid pain sounds in %s\n", filename );
          break;
        }
        if( trap_Parse_ReadToken( f, &token ) )
          cuboid->painSounds[ cuboid->painSoundCount++ ] = trap_S_RegisterSound( token.string, qfalse );
      }
      else if( !Q_stricmp( token.string, "destroySound" ) )
      {
        if( cuboid->destroySoundCount == MAX_CUBOID_SOUNDS )
        {
          Com_Printf( S_COLOR_RED "ERROR: too many cuboid destroy sounds in %s\n", filename );
          break;
        }
        if( trap_Parse_ReadToken( f, &token ) )
        {
          cuboid->destroySoundThresholds[ cuboid->destroySoundCount++ ] = atof( token.string );
          if( trap_Parse_ReadToken( f, &token ) )
            cuboid->destroySounds[ cuboid->destroySoundCount - 1 ] = trap_S_RegisterSound( token.string, qfalse );
          else
          {
            Com_Printf( S_COLOR_YELLOW "WARNING: incomplete cuboid destroy sound definition in %s\n", filename );
            cuboid->destroySoundCount--;
          }
        }
      }
      else
        Com_Printf( S_COLOR_YELLOW "WARNING: invalid token %s in %s\n", token.string, filename );
    }
    trap_Parse_FreeSource( f );
  }
}

/*
===============
CG_SetBuildableLerpFrameAnimation

may include ANIM_TOGGLEBIT
===============
*/
static void CG_SetBuildableLerpFrameAnimation( buildable_t buildable, lerpFrame_t *lf, int newAnimation )
{
  animation_t *anim;

  lf->animationNumber = newAnimation;

  if( newAnimation < 0 || newAnimation >= MAX_BUILDABLE_ANIMATIONS )
    CG_Error( "Bad animation number: %i", newAnimation );

  anim = &cg_buildables[ buildable ].animations[ newAnimation ];

  //this item has just spawned so lf->frameTime will be zero
  if( !lf->animation )
    lf->frameTime = cg.time + 1000; //1 sec delay before starting the spawn anim

  lf->animation = anim;
  lf->animationTime = lf->frameTime + anim->initialLerp;

  if( cg_debugAnim.integer )
    CG_Printf( "Anim: %i\n", newAnimation );
}

/*
===============
CG_RunBuildableLerpFrame

Sets cg.snap, cg.oldFrame, and cg.backlerp
cg.time should be between oldFrameTime and frameTime after exit
===============
*/
static void CG_RunBuildableLerpFrame( centity_t *cent )
{
  buildable_t           buildable = cent->currentState.modelindex;
  lerpFrame_t           *lf = &cent->lerpFrame;
  buildableAnimNumber_t newAnimation = cent->buildableAnim & ~( ANIM_TOGGLEBIT|ANIM_FORCEBIT );

  // see if the animation sequence is switching
  if( newAnimation != lf->animationNumber || !lf->animation )
  {
    if( cg_debugRandom.integer )
      CG_Printf( "newAnimation: %d lf->animationNumber: %d lf->animation: %d\n",
                 newAnimation, lf->animationNumber, lf->animation );

    CG_SetBuildableLerpFrameAnimation( buildable, lf, newAnimation );

    if( !cg_buildables[ buildable ].sounds[ newAnimation ].looped &&
        cg_buildables[ buildable ].sounds[ newAnimation ].enabled )
    {
      if( cg_debugRandom.integer )
        CG_Printf( "Sound for animation %d for a %s\n",
            newAnimation, BG_Buildable( buildable, NULL )->humanName );

      trap_S_StartSound( cent->lerpOrigin, cent->currentState.number, CHAN_AUTO,
        cg_buildables[ buildable ].sounds[ newAnimation ].sound );
    }
  }

  if( cg_buildables[ buildable ].sounds[ lf->animationNumber ].looped &&
      cg_buildables[ buildable ].sounds[ lf->animationNumber ].enabled )
    trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, vec3_origin,
      cg_buildables[ buildable ].sounds[ lf->animationNumber ].sound );

  CG_RunLerpFrame( lf, 1.0f );

  // animation ended
  if( lf->frameTime == cg.time )
  {
    cent->buildableAnim = cent->currentState.torsoAnim;
    cent->buildableIdleAnim = qtrue;
  }
}

/*
===============
CG_BuildableAnimation
===============
*/
static void CG_BuildableAnimation( centity_t *cent, int *old, int *now, float *backLerp )
{
  entityState_t *es = &cent->currentState;

  //if no animation is set default to idle anim
  if( cent->buildableAnim == BANIM_NONE )
  {
    cent->buildableAnim = es->torsoAnim;
    cent->buildableIdleAnim = qtrue;
  }

  //display the first frame of the construction anim if not yet spawned
  if( !( es->eFlags & EF_B_SPAWNED ) )
  {
    animation_t *anim = &cg_buildables[ es->modelindex ].animations[ BANIM_CONSTRUCT1 ];

    //so that when animation starts for real it has sensible numbers
    cent->lerpFrame.oldFrameTime =
      cent->lerpFrame.frameTime =
      cent->lerpFrame.animationTime =
      cg.time;

    *old      = cent->lerpFrame.oldFrame = anim->firstFrame;
    *now      = cent->lerpFrame.frame    = anim->firstFrame;
    *backLerp = cent->lerpFrame.backlerp = 0.0f;

    //ensure that an animation is triggered once the buildable has spawned
    cent->oldBuildableAnim = BANIM_NONE;
  }
  else
  {
    if( ( cent->oldBuildableAnim ^ es->legsAnim ) & ANIM_TOGGLEBIT )
    {
      if( cg_debugAnim.integer )
        CG_Printf( "%d->%d l:%d t:%d %s(%d)\n",
                   cent->oldBuildableAnim, cent->buildableAnim,
                   es->legsAnim, es->torsoAnim,
                   BG_Buildable( es->modelindex, NULL )->humanName, es->number );

      if( cent->buildableAnim == es->torsoAnim || es->legsAnim & ANIM_FORCEBIT )
      {
        cent->buildableAnim = cent->oldBuildableAnim = es->legsAnim;
        cent->buildableIdleAnim = qfalse;
      }
      else
      {
        cent->buildableAnim = cent->oldBuildableAnim = es->torsoAnim;
        cent->buildableIdleAnim = qtrue;
      }
    }
    else if( cent->buildableIdleAnim == qtrue &&
             cent->buildableAnim != es->torsoAnim )
    {
      cent->buildableAnim = es->torsoAnim;
    }

    CG_RunBuildableLerpFrame( cent );

    *old      = cent->lerpFrame.oldFrame;
    *now      = cent->lerpFrame.frame;
    *backLerp = cent->lerpFrame.backlerp;
  }
}

#define TRACE_DEPTH 64.0f

/*
===============
CG_PositionAndOrientateBuildable
===============
*/
static void CG_PositionAndOrientateBuildable( const vec3_t angles, const vec3_t inOrigin,
                                              const vec3_t normal, const int skipNumber,
                                              const vec3_t mins, const vec3_t maxs,
                                              vec3_t outAxis[ 3 ], vec3_t outOrigin,
					      qboolean cuboid )
{
  vec3_t  forward, start, end;
  trace_t tr, box_tr;
  float mag, fraction;

  AngleVectors( angles, forward, NULL, NULL );
  VectorCopy( normal, outAxis[ 2 ] );
  ProjectPointOnPlane( outAxis[ 0 ], forward, outAxis[ 2 ] );

  if( !VectorNormalize( outAxis[ 0 ] ) )
  {
    if(cuboid)
      forward[0]=forward[1]=0.0f,forward[2]=1.0f;
    else
      AngleVectors( angles, NULL, NULL, forward );
    ProjectPointOnPlane( outAxis[ 0 ], forward, outAxis[ 2 ] );
    VectorNormalize( outAxis[ 0 ] );
  }

  CrossProduct( outAxis[ 0 ], outAxis[ 2 ], outAxis[ 1 ] );
  outAxis[ 1 ][ 0 ] = -outAxis[ 1 ][ 0 ];
  outAxis[ 1 ][ 1 ] = -outAxis[ 1 ][ 1 ];
  outAxis[ 1 ][ 2 ] = -outAxis[ 1 ][ 2 ];

  if(cuboid)
  {
    VectorCopy(inOrigin,outOrigin);
    return;
  }
  VectorMA( inOrigin, -TRACE_DEPTH, normal, end );
  VectorMA( inOrigin, 1.0f, normal, start );

  // Take both capsule and box traces. If the capsule trace does not differ
  //  significantly from the box trace use it. This may cause buildables to be
  //  positioned *inside* the surface on which it is placed. This is intentional

  CG_CapTrace( &tr, start, mins, maxs, end, skipNumber,
               MASK_PLAYERSOLID );

  if(tr.contents&(~MASK_SOLID)) //if we hit a player or another buildable then display buildable exactly at its origin
   VectorCopy(inOrigin,outOrigin);
  else
  {
   CG_Trace( &box_tr, start, mins, maxs, end, skipNumber,
             MASK_PLAYERSOLID );

   mag = Distance( tr.endpos, box_tr.endpos );

   fraction = tr.fraction;

   // this is either too far off of the bbox to be useful for gameplay purposes
   //  or the model is positioned in thin air anyways.
   if( mag > 15.0f || tr.fraction == 1.0f )
     fraction = box_tr.fraction; 

   VectorMA( inOrigin, fraction * -TRACE_DEPTH, normal, outOrigin );
  }
}

/*
==================
CG_GhostBuildable
==================
*/
void CG_Cuboid_Info(void);
void CG_GhostBuildable( buildable_t buildable, vec3_t dims )
{
  refEntity_t     ent;
  playerState_t   *ps;
  vec3_t          angles, entity_origin;
  vec3_t          mins, maxs;
  trace_t         tr;
  float           scale;
  vec3_t          viewangles;

  ps = &cg.predictedPlayerState;

  memset( &ent, 0, sizeof( ent ) );
  
  if(BG_Buildable(buildable,NULL)->cuboid)
  {
   BG_CuboidBBox(dims,mins,maxs);
   CG_Cuboid_Info();
  }
  else BG_BuildableBoundingBox( buildable, mins, maxs );

  if(!BG_PositionBuildableRelativeToPlayer( ps, BG_Buildable(buildable,NULL)->cuboid, mins, maxs, CG_Trace, entity_origin, angles, &tr ))
    return;

  if(BG_Buildable(buildable, NULL)->cuboid)
  { 
    qhandle_t shader;

    CG_Cuboid_Send( ); //NOTE: CG_Cuboid_Send has its own timer so we don't spam server with commands every frame

    if( cg.waitForCB )
      shader = cgs.media.cuboidYellowBuildShader;
    else if( cg.cuboidValid && ps->stats[ STAT_BUILDABLE ] & SB_VALID_TOGGLEBIT )
      shader = cgs.media.cuboidGreenBuildShader;
    else
      shader = cgs.media.cuboidRedBuildShader;

    CG_DrawCuboid( entity_origin, dims, shader, 0 );
    CG_DrawCuboidAxis( entity_origin, dims, cg_cuboidResizeAxis.integer, cgs.media.cuboidAxis );
    return;
  }

  VectorCopy(ps->viewangles,viewangles);
  
  CG_PositionAndOrientateBuildable( viewangles, entity_origin, tr.plane.normal, ps->clientNum,
                                    mins, maxs, ent.axis, ent.origin, qfalse );

  //offset on the Z axis if required

  VectorMA( ent.origin, BG_BuildableConfig( buildable )->zOffset, tr.plane.normal, ent.origin );

  VectorCopy( ent.origin, ent.lightingOrigin );
  VectorCopy( ent.origin, ent.oldorigin ); // don't positionally lerp at all

  ent.hModel = cg_buildables[ buildable ].models[ 0 ];

  if( ps->stats[ STAT_BUILDABLE ] & SB_VALID_TOGGLEBIT )
    ent.customShader = cgs.media.greenBuildShader;
  else
    ent.customShader = cgs.media.redBuildShader;

  ent.nonNormalizedAxes = qfalse;

  //rescale the model
  scale = BG_BuildableConfig( buildable )->modelScale;

  if( scale != 1.0f )
  {
    VectorScale( ent.axis[ 0 ], scale, ent.axis[ 0 ] );
    VectorScale( ent.axis[ 1 ], scale, ent.axis[ 1 ] );
    VectorScale( ent.axis[ 2 ], scale, ent.axis[ 2 ] );

    ent.nonNormalizedAxes = qtrue;
  }

  // add to refresh list
  trap_R_AddRefEntityToScene( &ent );
}

/*
==================
CG_BuildableParticleEffects
==================
*/
static void CG_BuildableParticleEffects( centity_t *cent )
{
  entityState_t   *es = &cent->currentState;
  team_t          team = BG_Buildable( es->modelindex, NULL )->team;
  int             health = es->generic1;
  float           healthFrac = (float)health / BG_Buildable( es->modelindex, es->angles )->health;

  if( !( es->eFlags & EF_B_SPAWNED ) )
    return;

  if( team == TEAM_HUMANS )
  {
    if( healthFrac < 0.33f && !CG_IsParticleSystemValid( &cent->buildablePS ) )
    {
      cent->buildablePS = CG_SpawnNewParticleSystem( cgs.media.humanBuildableDamagedPS );

      if( CG_IsParticleSystemValid( &cent->buildablePS ) )
      {
        CG_SetAttachmentCent( &cent->buildablePS->attachment, cent );
        CG_AttachToCent( &cent->buildablePS->attachment );
      }
    }
    else if( healthFrac >= 0.33f && CG_IsParticleSystemValid( &cent->buildablePS ) )
      CG_DestroyParticleSystem( &cent->buildablePS );
  }
  else if( team == TEAM_ALIENS )
  {
    if( healthFrac < 0.33f && !CG_IsParticleSystemValid( &cent->buildablePS ) )
    {
      cent->buildablePS = CG_SpawnNewParticleSystem( cgs.media.alienBuildableDamagedPS );

      if( CG_IsParticleSystemValid( &cent->buildablePS ) )
      {
        CG_SetAttachmentCent( &cent->buildablePS->attachment, cent );
        CG_SetParticleSystemNormal( cent->buildablePS, es->origin2 );
        CG_AttachToCent( &cent->buildablePS->attachment );
      }
    }
    else if( healthFrac >= 0.33f && CG_IsParticleSystemValid( &cent->buildablePS ) )
      CG_DestroyParticleSystem( &cent->buildablePS );
  }
}

/*
==================
CG_BuildableStatusParse
==================
*/
void CG_BuildableStatusParse( const char *filename, buildStat_t *bs )
{
  pc_token_t token;
  int        handle;
  const char *s;
  int        i;
  float      f;
  vec4_t     c;

  handle = trap_Parse_LoadSource( filename );
  if( !handle )
    return;
  while( 1 )
  {
    if( !trap_Parse_ReadToken( handle, &token ) )
      break;
    if( !Q_stricmp( token.string, "frameShader" ) )
    {
      if( PC_String_Parse( handle, &s ) )
        bs->frameShader = trap_R_RegisterShader( s );
      continue;
    }
    else if( !Q_stricmp( token.string, "overlayShader" ) )
    {
      if( PC_String_Parse( handle, &s ) )
        bs->overlayShader = trap_R_RegisterShader( s );
      continue;
    }
    else if( !Q_stricmp( token.string, "noPowerShader" ) )
    {
      if( PC_String_Parse( handle, &s ) )
        bs->noPowerShader = trap_R_RegisterShader( s );
      continue;
    }
    else if( !Q_stricmp( token.string, "markedShader" ) )
    {
      if( PC_String_Parse( handle, &s ) )
        bs->markedShader = trap_R_RegisterShader( s );
      continue;
    }
    else if( !Q_stricmp( token.string, "healthSevereColor" ) )
    {
      if( PC_Color_Parse( handle, &c ) )
        Vector4Copy( c, bs->healthSevereColor );
      continue;
    }
    else if( !Q_stricmp( token.string, "healthHighColor" ) )
    {
      if( PC_Color_Parse( handle, &c ) )
        Vector4Copy( c, bs->healthHighColor );
      continue;
    }
    else if( !Q_stricmp( token.string, "healthElevatedColor" ) )
    {
      if( PC_Color_Parse( handle, &c ) )
        Vector4Copy( c, bs->healthElevatedColor );
      continue;
    }
    else if( !Q_stricmp( token.string, "healthGuardedColor" ) )
    {
      if( PC_Color_Parse( handle, &c ) )
        Vector4Copy( c, bs->healthGuardedColor );
      continue;
    }
    else if( !Q_stricmp( token.string, "healthLowColor" ) )
    {
      if( PC_Color_Parse( handle, &c ) )
        Vector4Copy( c, bs->healthLowColor );
      continue;
    }
    else if( !Q_stricmp( token.string, "foreColor" ) )
    {
      if( PC_Color_Parse( handle, &c ) )
        Vector4Copy( c, bs->foreColor );
      continue;
    }
    else if( !Q_stricmp( token.string, "backColor" ) )
    {
      if( PC_Color_Parse( handle, &c ) )
        Vector4Copy( c, bs->backColor );
      continue;
    }
    else if( !Q_stricmp( token.string, "frameHeight" ) )
    {
      if( PC_Int_Parse( handle, &i ) )
        bs->frameHeight = i;
      continue;
    }
    else if( !Q_stricmp( token.string, "frameWidth" ) )
    {
      if( PC_Int_Parse( handle, &i ) )
        bs->frameWidth = i;
      continue;
    }
    else if( !Q_stricmp( token.string, "healthPadding" ) )
    {
      if( PC_Int_Parse( handle, &i ) )
        bs->healthPadding = i;
      continue;
    }
    else if( !Q_stricmp( token.string, "overlayHeight" ) )
    {
      if( PC_Int_Parse( handle, &i ) )
        bs->overlayHeight = i;
      continue;
    }
    else if( !Q_stricmp( token.string, "overlayWidth" ) )
    {
      if( PC_Int_Parse( handle, &i ) )
        bs->overlayWidth = i;
      continue;
    }
    else if( !Q_stricmp( token.string, "verticalMargin" ) )
    {
      if( PC_Float_Parse( handle, &f ) )
        bs->verticalMargin = f;
      continue;
    }
    else if( !Q_stricmp( token.string, "horizontalMargin" ) )
    {
      if( PC_Float_Parse( handle, &f ) )
        bs->horizontalMargin = f;
      continue;
    }
    else
    {
      Com_Printf("CG_BuildableStatusParse: unknown token %s in %s\n",
        token.string, filename );
      bs->loaded = qfalse;
      trap_Parse_FreeSource( handle );
      return;
    }
  }
  bs->loaded = qtrue;
  trap_Parse_FreeSource( handle );
}

#define STATUS_FADE_TIME      200
#define STATUS_MAX_VIEW_DIST  900.0f
#define STATUS_PEEK_DIST      20
/*
==================
CG_BuildableStatusDisplay
==================
*/
static void CG_BuildableStatusDisplay( centity_t *cent, qboolean cuboid, vec3_t trac )
{
  entityState_t   *es = &cent->currentState;
  vec3_t          origin;
  float           healthScale;
  int             health;
  float           x, y;
  vec4_t          color;
  trace_t         tr;
  float           d;
  buildStat_t     *bs;
  int             i, j;
  int             entNum;
  vec3_t          trOrigin;
  vec3_t          right;
  qboolean        visible = qfalse;
  vec3_t          mins, maxs;
  entityState_t   *hit;
  int             anim;
  char            buf[12];
  int             bufl;

  if( BG_Buildable( es->modelindex, NULL )->team == TEAM_ALIENS )
    bs = &cgs.alienBuildStat;
  else
    bs = &cgs.humanBuildStat;

  if( !bs->loaded )
    return;

  d = Distance( cent->lerpOrigin, cg.refdef.vieworg );
  if( d > STATUS_MAX_VIEW_DIST )
    return;

  Vector4Copy( bs->foreColor, color );

  // trace for center point
  if(BG_Buildable(es->modelindex, NULL)->cuboid)
    BG_CuboidBBox( es->angles, mins, maxs );
  else
    BG_BuildableBoundingBox( es->modelindex, mins, maxs );

  // hack for shrunken barricades
  anim = es->torsoAnim & ~( ANIM_FORCEBIT | ANIM_TOGGLEBIT );
  if( es->modelindex == BA_A_BARRICADE &&
      ( anim == BANIM_DESTROYED || !( es->eFlags & EF_B_SPAWNED ) ) )
    maxs[ 2 ] = (int)( maxs[ 2 ] * BARRICADE_SHRINKPROP );

  VectorCopy( cent->lerpOrigin, origin );

  // center point
  origin[ 2 ] += mins[ 2 ];
  origin[ 2 ] += ( abs( mins[ 2 ] ) + abs( maxs[ 2 ] ) ) / 2;

  entNum = cg.predictedPlayerState.clientNum;

  // if first try fails, step left, step right
  for( j = 0; j < 3 && !cuboid; j++ )
  {
    VectorCopy( cg.refdef.vieworg, trOrigin );
    switch( j )
    {
      case 1:
        // step right
        AngleVectors( cg.refdefViewAngles, NULL, right, NULL );
        VectorMA( trOrigin, STATUS_PEEK_DIST, right, trOrigin );
        break;
      case 2:
        // step left
        AngleVectors( cg.refdefViewAngles, NULL, right, NULL );
        VectorMA( trOrigin, -STATUS_PEEK_DIST, right, trOrigin );
        break;
      default:
        break;
    }
    // look through up to 3 players and/or transparent buildables
    for( i = 0; i < 3; i++ )
    {
      CG_Trace( &tr, trOrigin, NULL, NULL, origin, entNum, MASK_SHOT );
      if( tr.entityNum == cent->currentState.number )
      {
        visible = qtrue;
        break;
      }

      if( tr.entityNum == ENTITYNUM_WORLD )
        break;

      hit  = &cg_entities[ tr.entityNum ].currentState;

      if( tr.entityNum < MAX_CLIENTS || ( hit->eType == ET_BUILDABLE &&
          ( !( es->eFlags & EF_B_SPAWNED ) ||
            BG_Buildable( hit->modelindex, NULL )->transparentTest ) ) )
      {
        entNum = tr.entityNum;
        VectorCopy( tr.endpos, trOrigin );
      }
      else
        break;
    }
  }
  
  if( cuboid )
    visible = qtrue;
  
  // hack to make the kit obscure view
  if( cg_drawGun.integer && visible &&
      cg.predictedPlayerState.stats[ STAT_TEAM ] == TEAM_HUMANS &&
      CG_WorldToScreen( origin, &x, &y ) )
  {
    if( x > 450 && y > 290 )
      visible = qfalse;
  }

  if( !visible && cent->buildableStatus.visible )
  {
    cent->buildableStatus.visible   = qfalse;
    cent->buildableStatus.lastTime  = cg.time;
  }
  else if( visible && !cent->buildableStatus.visible )
  {
    cent->buildableStatus.visible   = qtrue;
    cent->buildableStatus.lastTime  = cg.time;
  }

  // Fade up
  if( cent->buildableStatus.visible )
  {
    if( cent->buildableStatus.lastTime + STATUS_FADE_TIME > cg.time )
      color[ 3 ] = (float)( cg.time - cent->buildableStatus.lastTime ) / STATUS_FADE_TIME;
  }

  // Fade down
  if( !cent->buildableStatus.visible )
  {
    if( cent->buildableStatus.lastTime + STATUS_FADE_TIME > cg.time )
      color[ 3 ] = 1.0f - (float)( cg.time - cent->buildableStatus.lastTime ) / STATUS_FADE_TIME;
    else
      return;
  }

  health = es->generic1;
  if(cuboid)
    health = BG_CuboidUnpackHealth(es);
  healthScale = (float)health / BG_Buildable( es->modelindex, es->angles )->health;

  if( health > 0 && healthScale < 0.01f )
    healthScale = 0.01f;
  else if( healthScale < 0.0f )
    healthScale = 0.0f;
  else if( healthScale > 1.0f )
    healthScale = 1.0f;

  if( cuboid )
  {
    x = 320;
    y = 240;
    d = Distance( cg.refdef.vieworg, trac );
    if( d < 64.0f )
      d = 64.0f ;
  }
  else
    if( !CG_WorldToScreen( origin, &x, &y ) )
      return;
 
  {
    float  picH = bs->frameHeight;
    float  picW = bs->frameWidth;
    float  picX = x;
    float  picY = y;
    float  scale;
    float  subH, subY;
    float  clipX, clipY, clipW, clipH;
    vec4_t frameColor;

    // this is fudged to get the width/height in the cfg to be more realistic
    scale = ( picH / d ) * 3;

    picH *= scale;
    picW *= scale;
    picX -= ( picW * 0.5f );
    picY -= ( picH * 0.5f );

    // sub-elements such as icons and number
    subH = picH - ( picH * bs->verticalMargin );
    subY = picY + ( picH * 0.5f ) - ( subH * 0.5f );

    clipW = ( 640.0f * cg_viewsize.integer ) / 100.0f;
    clipH = ( 480.0f * cg_viewsize.integer ) / 100.0f;
    clipX = 320.0f - ( clipW * 0.5f );
    clipY = 240.0f - ( clipH * 0.5f );
    CG_SetClipRegion( clipX, clipY, clipW, clipH );

    if( bs->frameShader )
    {
      Vector4Copy( bs->backColor, frameColor );
      frameColor[ 3 ] = color[ 3 ];
      trap_R_SetColor( frameColor );
      CG_DrawPic( picX, picY, picW, picH, bs->frameShader );
      trap_R_SetColor( NULL );
    }

    if( health > 0 )
    {
      float hX, hY, hW, hH;
      vec4_t healthColor;

      hX = picX + ( bs->healthPadding * scale );
      hY = picY + ( bs->healthPadding * scale );
      hH = picH - ( bs->healthPadding * 2.0f * scale );
      hW = picW * healthScale - ( bs->healthPadding * 2.0f * scale );

      if( healthScale == 1.0f )
        Vector4Copy( bs->healthLowColor, healthColor );
      else if( healthScale >= 0.75f )
        Vector4Copy( bs->healthGuardedColor, healthColor );
      else if( healthScale >= 0.50f )
        Vector4Copy( bs->healthElevatedColor, healthColor );
      else if( healthScale >= 0.25f )
        Vector4Copy( bs->healthHighColor, healthColor );
      else
        Vector4Copy( bs->healthSevereColor, healthColor );

      healthColor[ 3 ] = color[ 3 ];
      trap_R_SetColor( healthColor );

      CG_DrawPic( hX, hY, hW, hH, cgs.media.whiteShader );
      trap_R_SetColor( NULL );
    }

    if( bs->overlayShader )
    {
      float oW = bs->overlayWidth;
      float oH = bs->overlayHeight;
      float oX = x;
      float oY = y;

      oH *= scale;
      oW *= scale;
      oX -= ( oW * 0.5f );
      oY -= ( oH * 0.5f );

      trap_R_SetColor( frameColor );
      CG_DrawPic( oX, oY, oW, oH, bs->overlayShader );
      trap_R_SetColor( NULL );
    }

    trap_R_SetColor( color );
    
    {
      float pX;
      
      pX = picX + ( subH * bs->horizontalMargin );
    
      if( BG_Buildable( es->modelindex, NULL )->team == TEAM_HUMANS )
      {
        float offs = 2000.0f / d;
        CG_DrawPic( pX - offs, subY - offs,
                    subH + 2.0f * offs, subH + 2.0f * offs, 
                    CG_BuildablePowerStatusIcon( es ) );
      }
      else if( !( es->eFlags & EF_B_POWERED ) )
        CG_DrawPic( pX, subY, subH, subH, bs->noPowerShader );
    }

    //NOTE: dont use CG_DrawField, too few digits
    {
      float nX,cW,cH;
      int healthMax;
      int healthPoints;
      int frame;
      
      healthMax = BG_Buildable( es->modelindex, es->angles )->health;
      healthPoints = (int)( healthScale * healthMax );
      if( health > 0 && healthPoints < 1 )
        healthPoints = 1;
      
      Com_sprintf( buf, sizeof( buf ), "%i", healthPoints );
      bufl = strlen(buf);
      cW = subH*cgDC.aspectScale;
      cH = subH;
      nX = picX + picW * 0.5f - cW * bufl * 0.5f;
      
      for( i = 0; i < bufl; i++ )
      {
        if( buf[ i ] == '-' )
          frame = STAT_MINUS;
        else
          frame = buf[ i ] - '0';
          CG_DrawPic( nX + i * cW,
                      y + bs->verticalMargin - subH * 0.5f,
                      cW, cH,
                      cgs.media.numberShaders[ frame ] );
      }
    }

    trap_R_SetColor( NULL );
    CG_ClearClipRegion( );
  }
}

/*
==================
CG_SortDistance
==================
*/
static int CG_SortDistance( const void *a, const void *b )
{
  centity_t    *aent, *bent;
  float        adist, bdist;

  aent = &cg_entities[ *(int *)a ];
  bent = &cg_entities[ *(int *)b ];
  adist = Distance( cg.refdef.vieworg, aent->lerpOrigin );
  bdist = Distance( cg.refdef.vieworg, bent->lerpOrigin );
  if( adist > bdist )
    return -1;
  else if( adist < bdist )
    return 1;
  else
    return 0;
}

/*
==================
CG_PlayerIsBuilder
==================
*/
static qboolean CG_PlayerIsBuilder( buildable_t buildable )
{
  switch( cg.predictedPlayerState.weapon )
  {
    case WP_ABUILD:
    case WP_ABUILD2:
    case WP_HBUILD:
      return BG_Buildable( buildable, NULL )->team ==
             BG_Weapon( cg.predictedPlayerState.weapon )->team;

    default:
      return qfalse;
  }
}

/*
==================
CG_DrawBuildableStatus
==================
*/
void CG_DrawBuildableStatus( void )
{
  int             i;
  centity_t       *cent;
  entityState_t   *es;
  int             buildableList[ MAX_ENTITIES_IN_SNAPSHOT ];
  int             buildables = 0;
  vec3_t          end;
  trace_t         tr;
  qboolean        cuboid;
  
  if( ( cg.predictedPlayerState.stats[STAT_BUILDABLE] & ~SB_VALID_TOGGLEBIT) > BA_NONE )
   return; //hide buildstats if we're placing a buildable
  
  for( i = 0; i < cg.snap->numEntities; i++ )
  {
    cent  = &cg_entities[ cg.snap->entities[ i ].number ];
    es    = &cent->currentState;

    if( es->eType == ET_BUILDABLE && CG_PlayerIsBuilder( es->modelindex ) )
      buildableList[ buildables++ ] = cg.snap->entities[ i ].number;
  }

  VectorMA(cg.refdef.vieworg,STATUS_MAX_VIEW_DIST,cg.refdef.viewaxis[0],end);
  CG_Trace(&tr,cg.refdef.vieworg,NULL,NULL,end,cg.predictedPlayerState.clientNum,MASK_SHOT);
  
  qsort( buildableList, buildables, sizeof( int ), CG_SortDistance );
  for( i = 0; i < buildables; i++ )
  {
    cuboid = BG_IsCuboid( cg_entities[ buildableList[ i ] ].currentState.modelindex );
    if(cuboid && tr.entityNum != buildableList[ i ] )
      continue;
    CG_BuildableStatusDisplay( &cg_entities[ buildableList[ i ] ], cuboid, tr.endpos );
  }
}

#define BUILDABLE_SOUND_PERIOD  500

/*
==================
CG_Buildable
==================
*/
void CG_Buildable( centity_t *cent )
{
  refEntity_t     ent;
  entityState_t   *es = &cent->currentState;
  vec3_t          angles;
  vec3_t          surfNormal, xNormal, mins, maxs;
  vec3_t          refNormal = { 0.0f, 0.0f, 1.0f };
  float           rotAngle;
  team_t          team = BG_Buildable( es->modelindex, NULL )->team;
  float           scale;
  int             health;

  //must be before EF_NODRAW check
  if( team == TEAM_ALIENS )
    CG_Creep( cent );

  // if set to invisible, skip
  if( es->eFlags & EF_NODRAW )
  {
    if( CG_IsParticleSystemValid( &cent->buildablePS ) )
      CG_DestroyParticleSystem( &cent->buildablePS );

    return;
  }
  
  // cuboids use a bit different rendering code
  if( BG_IsCuboid( es->modelindex ) )
  {
    qhandle_t texture = 0,cracks = 0;
    vec3_t    dims, mins, maxs;
    const cuboidAttributes_t *cuboidAttr;
    const cuboidInfo_t *cuboidInfo;
    int       i, health, sound;
    float     healthPct;
   
    cuboidAttr = BG_CuboidAttributes( es->modelindex );
    cuboidInfo = &cg_cuboids[ es->modelindex - CUBOID_FIRST ];
    VectorCopy( es->angles, dims );
    healthPct = (float)( health = BG_CuboidUnpackHealth(es) ) / (float)BG_Buildable(es->modelindex,dims)->health;

    if( healthPct > 1.0f )
      healthPct = 1.0f;
    else if ( healthPct < 0.0f )
      healthPct = 0.0f;
    
    if( ( es->eFlags & EF_B_SPAWNED ) && cuboidInfo->useCracks && healthPct < 0.95f )
    {
      float progress;
      const float o = 1.02f; //md3 rendering is not exact, so render it bigger to compensate
      
      if( cuboidInfo->textureCount )
        texture = cuboidInfo->textures[ 0 ];
      
      memset( &ent, 0, sizeof( ent ) );
      ent.reType = RT_MODEL;
      VectorCopy( cent->lerpOrigin, ent.origin );

      ent.axis[0][0]=-dims[0]/2.0f*o;ent.axis[0][1]=0.0f;           ent.axis[0][2]=0.0f;
      ent.axis[1][0]=0.0f;           ent.axis[1][1]=-dims[1]/2.0f*o;ent.axis[1][2]=0.0f;
      ent.axis[2][0]=0.0f;           ent.axis[2][1]=0.0f;           ent.axis[2][2]=dims[2]/2.0f*o;
      ent.nonNormalizedAxes = qtrue;
      
      progress = healthPct;
  
      ent.customShader = cgs.media.cuboidCracks;
      ent.shaderTime = 0.001f * cg.time + progress;
      
      ent.hModel = cgs.media.cuboidModel;
      trap_R_AddRefEntityToScene( &ent );
    }
    else
      for( i = 0; i < cuboidInfo->textureCount; i++ )
        if( healthPct >= cuboidInfo->textureThresholds[ i ] )
        {
          texture = cuboidInfo->textures[ i ];
          break;
        }
        
    if( !( es->eFlags & EF_B_SPAWNED ) )
    {
      sfxHandle_t prebuildSound;

      if( team == TEAM_HUMANS )
      {
        if( es->eFlags & EF_B_POWERED )
        {
          texture = cgs.media.humanSpawningShader;
          prebuildSound = cgs.media.humanBuildablePrebuild;
        }
        else
        {
          texture = cgs.media.humanUnpoweredSpawningShader;
          prebuildSound = cgs.media.unpoweredSurgeLoop;
        }
      }
      else
      {
        texture = cgs.media.cuboidAlienPrebuild;
        prebuildSound = cgs.media.alienBuildablePrebuild;
      }
      cracks = 0;
      trap_S_AddLoopingSound( es->number, cent->lerpOrigin, vec3_origin, prebuildSound );
    }
      
    BG_CuboidBBox( dims, mins, maxs );
    VectorAdd( mins, cent->lerpOrigin, mins );
    VectorAdd( maxs, cent->lerpOrigin, maxs );
    CG_RenderCuboid( mins, maxs, 0.01f, texture );
   
    if( health < cent->lastBuildableHealth && ( es->eFlags & EF_B_SPAWNED ) )
    {
      if( cent->lastBuildableDamageSoundTime + BUILDABLE_SOUND_PERIOD < cg.time )
      {
        sound = (int)( cuboidInfo->painSoundCount * random() );
        trap_S_StartSound( NULL, es->number, CHAN_BODY, cuboidInfo->painSounds[sound] );
        cent->lastBuildableDamageSoundTime = cg.time;
      }
      cent->lastBuildableHealth = health;
    }
  }
  else
    {
    memset ( &ent, 0, sizeof( ent ) );

    VectorCopy( cent->lerpOrigin, ent.origin );
    VectorCopy( cent->lerpOrigin, ent.oldorigin );
    VectorCopy( cent->lerpOrigin, ent.lightingOrigin );

    VectorCopy( es->origin2, surfNormal );

    VectorCopy( es->angles, angles );
    BG_BuildableBoundingBox( es->modelindex, mins, maxs );

    if( es->pos.trType == TR_STATIONARY )
    {
      // Positioning a buildable involves potentially up to two traces, and
      // seeing as buildables rarely move, we cache the results and recalculate
      // only if the buildable moves or changes orientation
      if( VectorCompare( cent->buildableCache.cachedOrigin, cent->lerpOrigin ) &&
          VectorCompare( cent->buildableCache.cachedNormal, surfNormal ) )
      {
        VectorCopy( cent->buildableCache.axis[ 0 ], ent.axis[ 0 ] );
        VectorCopy( cent->buildableCache.axis[ 1 ], ent.axis[ 1 ] );
        VectorCopy( cent->buildableCache.axis[ 2 ], ent.axis[ 2 ] );
        VectorCopy( cent->buildableCache.origin, ent.origin );
      }
      else
      {
        CG_PositionAndOrientateBuildable( angles, ent.origin, surfNormal,
                                          es->number, mins, maxs, ent.axis,
                                          ent.origin, qfalse );
        VectorCopy( ent.axis[ 0 ], cent->buildableCache.axis[ 0 ] );
        VectorCopy( ent.axis[ 1 ], cent->buildableCache.axis[ 1 ] );
        VectorCopy( ent.axis[ 2 ], cent->buildableCache.axis[ 2 ] );
        VectorCopy( ent.origin, cent->buildableCache.origin );
        VectorCopy( cent->lerpOrigin, cent->buildableCache.cachedOrigin );
        VectorCopy( surfNormal, cent->buildableCache.cachedNormal );
      }
    }

    VectorMA( ent.origin, BG_BuildableConfig( es->modelindex )->zOffset, surfNormal, ent.origin );
    

    VectorCopy( ent.origin, ent.oldorigin ); // don't positionally lerp at all
    VectorCopy( ent.origin, ent.lightingOrigin );

    ent.hModel = cg_buildables[ es->modelindex ].models[ 0 ];

    if( !( es->eFlags & EF_B_SPAWNED ) )
    {
      sfxHandle_t prebuildSound = cgs.media.humanBuildablePrebuild;

      if( team == TEAM_HUMANS )
      {
        if( es->eFlags & EF_B_POWERED )
        {
          ent.customShader = cgs.media.humanSpawningShader;
          prebuildSound = cgs.media.humanBuildablePrebuild;
        }
        else
        {
          ent.customShader = cgs.media.humanUnpoweredSpawningShader;
          prebuildSound = cgs.media.unpoweredSurgeLoop;
        }
      }
      else if( team == TEAM_ALIENS )
        prebuildSound = cgs.media.alienBuildablePrebuild;

      trap_S_AddLoopingSound( es->number, cent->lerpOrigin, vec3_origin, prebuildSound );
    }

    CG_BuildableAnimation( cent, &ent.oldframe, &ent.frame, &ent.backlerp );

    //rescale the model
    scale = BG_BuildableConfig( es->modelindex )->modelScale;

    if( scale != 1.0f )
    {
      VectorScale( ent.axis[ 0 ], scale, ent.axis[ 0 ] );
      VectorScale( ent.axis[ 1 ], scale, ent.axis[ 1 ] );
      VectorScale( ent.axis[ 2 ], scale, ent.axis[ 2 ] );

      ent.nonNormalizedAxes = qtrue;
    }
    else
     ent.nonNormalizedAxes = qfalse;

    //add to refresh list
    trap_R_AddRefEntityToScene( &ent );

    CrossProduct( surfNormal, refNormal, xNormal );
    VectorNormalize( xNormal );
    rotAngle = RAD2DEG( acos( DotProduct( surfNormal, refNormal ) ) );

    //turret barrel bit
    if( cg_buildables[ es->modelindex ].models[ 1 ] )
    {
      refEntity_t turretBarrel;
      vec3_t      flatAxis[ 3 ];

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

      turretBarrel.hModel = cg_buildables[ es->modelindex ].models[ 1 ];

      CG_PositionEntityOnTag( &turretBarrel, &ent, ent.hModel, "tag_turret" );
      VectorCopy( cent->lerpOrigin, turretBarrel.lightingOrigin );

      {
        vec3_t interpolated;
        int i;

        for( i = 0; i < 3 ; i++ )
          interpolated[ i ] = LerpAngle( es->angles2[ i ], cent->nextState.angles2[ i ], cg.frameInterpolation );

        AnglesToAxis( interpolated, flatAxis );
      }

      RotatePointAroundVector( turretBarrel.axis[ 0 ], xNormal, flatAxis[ 0 ], -rotAngle );
      RotatePointAroundVector( turretBarrel.axis[ 1 ], xNormal, flatAxis[ 1 ], -rotAngle );
      RotatePointAroundVector( turretBarrel.axis[ 2 ], xNormal, flatAxis[ 2 ], -rotAngle );

      turretBarrel.oldframe = ent.oldframe;
      turretBarrel.frame    = ent.frame;
      turretBarrel.backlerp = ent.backlerp;

      turretBarrel.customShader = ent.customShader;

      if( scale != 1.0f )
      {
        VectorScale( turretBarrel.axis[ 0 ], scale, turretBarrel.axis[ 0 ] );
        VectorScale( turretBarrel.axis[ 1 ], scale, turretBarrel.axis[ 1 ] );
        VectorScale( turretBarrel.axis[ 2 ], scale, turretBarrel.axis[ 2 ] );

        turretBarrel.nonNormalizedAxes = qtrue;
      }
      else
        turretBarrel.nonNormalizedAxes = qfalse;

      trap_R_AddRefEntityToScene( &turretBarrel );
    }

    //turret barrel bit
    if( cg_buildables[ es->modelindex ].models[ 2 ] )
    {
      refEntity_t turretTop;
      vec3_t      flatAxis[ 3 ];
      vec3_t      swivelAngles;

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

      VectorCopy( es->angles2, swivelAngles );
      swivelAngles[ PITCH ] = 0.0f;

      turretTop.hModel = cg_buildables[ es->modelindex ].models[ 2 ];

      CG_PositionRotatedEntityOnTag( &turretTop, &ent, ent.hModel, "tag_turret" );
      VectorCopy( cent->lerpOrigin, turretTop.lightingOrigin );
      AnglesToAxis( swivelAngles, flatAxis );

      RotatePointAroundVector( turretTop.axis[ 0 ], xNormal, flatAxis[ 0 ], -rotAngle );
      RotatePointAroundVector( turretTop.axis[ 1 ], xNormal, flatAxis[ 1 ], -rotAngle );
      RotatePointAroundVector( turretTop.axis[ 2 ], xNormal, flatAxis[ 2 ], -rotAngle );

      turretTop.oldframe = ent.oldframe;
      turretTop.frame    = ent.frame;
      turretTop.backlerp = ent.backlerp;

      turretTop.customShader = ent.customShader;

      if( scale != 1.0f )
      {
        VectorScale( turretTop.axis[ 0 ], scale, turretTop.axis[ 0 ] );
        VectorScale( turretTop.axis[ 1 ], scale, turretTop.axis[ 1 ] );
        VectorScale( turretTop.axis[ 2 ], scale, turretTop.axis[ 2 ] );

        turretTop.nonNormalizedAxes = qtrue;
      }
      else
        turretTop.nonNormalizedAxes = qfalse;

      trap_R_AddRefEntityToScene( &turretTop );
    }

    //weapon effects for turrets
    if( es->eFlags & EF_FIRING )
    {
      weaponInfo_t  *weapon = &cg_weapons[ es->weapon ];

      if( cg.time - cent->muzzleFlashTime > MUZZLE_FLASH_TIME ||
          BG_Buildable( es->modelindex, NULL )->turretProjType == WP_TESLAGEN )
      {
        if( weapon->wim[ WPM_PRIMARY ].flashDlightColor[ 0 ] ||
            weapon->wim[ WPM_PRIMARY ].flashDlightColor[ 1 ] ||
            weapon->wim[ WPM_PRIMARY ].flashDlightColor[ 2 ] )
        {
          trap_R_AddLightToScene( cent->lerpOrigin, 300 + ( rand( ) & 31 ),
              weapon->wim[ WPM_PRIMARY ].flashDlightColor[ 0 ],
              weapon->wim[ WPM_PRIMARY ].flashDlightColor[ 1 ],
              weapon->wim[ WPM_PRIMARY ].flashDlightColor[ 2 ] );
        }
      }

      if( weapon->wim[ WPM_PRIMARY ].firingSound )
      {
        trap_S_AddLoopingSound( es->number, cent->lerpOrigin, vec3_origin,
            weapon->wim[ WPM_PRIMARY ].firingSound );
      }
      else if( weapon->readySound )
        trap_S_AddLoopingSound( es->number, cent->lerpOrigin, vec3_origin, weapon->readySound );
    }
    
    //smoke etc for damaged buildables
    CG_BuildableParticleEffects( cent );
    
    

    
    health = es->generic1;

    if( health < cent->lastBuildableHealth &&
        ( es->eFlags & EF_B_SPAWNED ) )
    {
      if( cent->lastBuildableDamageSoundTime + BUILDABLE_SOUND_PERIOD < cg.time )
      {
        if( team == TEAM_HUMANS )
        {
          int i = rand( ) % 4;
          trap_S_StartSound( NULL, es->number, CHAN_BODY, cgs.media.humanBuildableDamage[ i ] );
        }
        else if( team == TEAM_ALIENS )
          trap_S_StartSound( NULL, es->number, CHAN_BODY, cgs.media.alienBuildableDamage );

        cent->lastBuildableDamageSoundTime = cg.time;
      }
    }

    cent->lastBuildableHealth = health;
  } //if (is a cuboid)
  
    
  // play a loop if there's not enough power for it to build / activate
  if( ( !( es->eFlags & EF_B_SPAWNED ) &&
        !( es->eFlags & EF_B_POWERED ) ) ||
      ( ( BG_Buildable( es->modelindex, NULL )->requiresPower ||
      es->modelindex != BA_H_REPEATER ) &&
      !( es->eFlags & EF_B_POWERED ) &&
      ( es->eFlags & EF_B_SURGE ) ) )
    trap_S_AddLoopingSound( es->number, cent->lerpOrigin, vec3_origin, cgs.media.unpoweredSurgeLoop );
}

char cuboidInfo[128];

/*
======================
CG_Cuboid_DrawInfo

Draw the cuboid info string generated by CG_Cuboid_Info.
======================
*/
void CG_Cuboid_DrawInfo(void)
{
  float x,y,w,h,s=0.5f;
  
  // disabled by default (replaced by ckit's display)
  if( !cg_drawCuboidInfo.integer )
    return;
 
  if( !BG_Buildable( cg.predictedPlayerState.stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT, NULL )->cuboid )
    return;
 
  w= UI_Text_Width( cuboidInfo, s );
  h= UI_Text_Height( cuboidInfo,  s );
  x= 320.0f - w / 2.0f + cg_cuboidInfoX.value;
  y= 240.0f - h / 2.0f + cg_cuboidInfoY.value;
 
  UI_Text_Paint( x, y, s, colorWhite, cuboidInfo, 0, 0, ITEM_TEXTSTYLE_SHADOWEDMORE );
}

/*
======================
CG_Cuboid_Info

Update the cuboid info string with current cuboid's dimensions,
name, health, build time and build points (pretty much everything
one needs to know about his cuboid).
======================
*/
void CG_Cuboid_Info(void)
{
 const buildableAttributes_t *attr;
 int axis=cg_cuboidResizeAxis.integer;
 
 if( !cg_drawCuboidInfo.integer )
  return;
 
 attr=BG_Buildable(cg.predictedPlayerState.stats[STAT_BUILDABLE]&~SB_VALID_TOGGLEBIT,cg.cuboidSelection);
 Com_sprintf(cuboidInfo,sizeof(cuboidInfo),
             "^7[^3%c^7] | ^%c%.1f^7x^%c%.1f^7x^%c%.1f ^7| ^3%i^7HP | ^3%i^7ms | ^3%i^7BP",
             (axis==0?'X':(axis==1?'Y':'Z')),
             (axis==0?'3':'5'),
             cg.cuboidSelection[0],
             (axis==1?'3':'5'),
             cg.cuboidSelection[1],
             (axis==2?'3':'5'),
             cg.cuboidSelection[2],
             attr->health,
             attr->buildTime,
             attr->buildPoints);
}

/*
======================
CG_Cuboid_Send

Send the cuboid selection via commands.
======================
*/
void CG_Cuboid_Send(void)
{
 static qboolean init = qfalse;
 static int lastupdate;
 static vec3_t lastcuboid;
 
 if( !BG_Buildable( cg.predictedPlayerState.stats[ STAT_BUILDABLE ] &~ SB_VALID_TOGGLEBIT, NULL)->cuboid)
   return;
 
 if( !init )
 {
   lastupdate = cg.time;
   VectorCopy( cg.cuboidSelection, lastcuboid );
   init = qtrue;
 }

 if( lastupdate + 100 > cg.time )
   return;
 
 if( !VectorCompareEpsilon( lastcuboid, cg.cuboidSelection, 1e-3 ) )
 {
   cg.latestCBNumber++;
   cg.latestCBNumber %= 100;
   trap_SendClientCommand( va("cb %i %f %f %f\n",
                              cg.latestCBNumber,
                              cg.cuboidSelection[ 0 ],
                              cg.cuboidSelection[ 1 ],
                              cg.cuboidSelection[ 2 ] ) );
   cg.waitForCB = qtrue; //wait for response
 }
 lastupdate = cg.time;
 VectorCopy( cg.cuboidSelection, lastcuboid );
}

/*
======================
CG_Cuboid_Response

Server responded to our cb with either cb2 or cb3.
======================
*/
void CG_Cuboid_Response(void)
{
  // cb2 <a> <b> <c> : server sets client-side cuboid
  // cb3 <echo>      : server agrees on player's cuboid
  // cb4 <echo>      : server disagrees on player's cuboid
  int offs = 0;
  
  if( !BG_Buildable( cg.predictedPlayerState.stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT, NULL )->cuboid )
    return;
 
  if( !Q_stricmp( CG_Argv( 0 ), "cb2" ) && trap_Argc( ) == 4 )
  {
    if( trap_Argc( ) == 4 ||
        ( offs = 1, trap_Argc() == 5 && atoi( CG_Argv( 1 ) ) == cg.latestCBNumber ) )
    {
      cg.cuboidValid = qtrue;
      cg.cuboidSelection[ 0 ] = atof( CG_Argv( 1 + offs ) );
      cg.cuboidSelection[ 1 ] = atof( CG_Argv( 2 + offs ) );
      cg.cuboidSelection[ 2 ] = atof( CG_Argv( 3 + offs ) );
      return;
    }
  }
  else if( !Q_stricmp( CG_Argv( 0 ), "cb3" ) )
  {
    if( trap_Argc( ) == 2 )
    {
      if( atoi( CG_Argv( 1 ) ) == cg.latestCBNumber )
        cg.waitForCB = qfalse;
        cg.cuboidValid = qtrue;
      return;
    }
  }
  else if( !Q_stricmp( CG_Argv( 0 ), "cb4" ) )
  {
    if( trap_Argc() == 2 )
    {
      if( atoi( CG_Argv( 1 ) ) == cg.latestCBNumber )
      {
        cg.waitForCB = qfalse;
        cg.cuboidValid = qfalse;

        if( cg.lastCuboidError + 250 < cg.time )
        {
          trap_S_StartLocalSound( cgs.media.cuboidErrorSound, CHAN_LOCAL_SOUND );
          cg.lastCuboidError = cg.time;
        }
      }
      return;
   }
 }
 Com_Printf( "^3warning: wrong cb from server\n" );
}

/*
======================
CG_CuboidResize_f
======================
*/
void CG_CuboidResize(qboolean enlarge)
{
  vec3_t dims;
  float rate = ( enlarge ? 1.0f : -1.0f ) * cg_cuboidResizeRate.value;
  
  if( !BG_Buildable( cg.predictedPlayerState.stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT, NULL)->cuboid )
    return;

  VectorCopy( cg.cuboidSelection, dims );
  switch( cg_cuboidResizeAxis.integer )
  {
    case 0:
      dims[ 0 ] += rate;
      break;
    case 1:
      dims[ 1 ] += rate;
      break;
    default:
      dims[ 2 ] += rate;
      break;
  }
  
  if( dims[ 0 ] * dims[ 1 ] * dims[ 2 ] < CUBOID_MINVOLUME )
    return;
  
  if( enlarge )
    trap_S_StartLocalSound( cgs.media.cuboidResizeSoundA, CHAN_LOCAL_SOUND );
  else
    trap_S_StartLocalSound( cgs.media.cuboidResizeSoundB, CHAN_LOCAL_SOUND );
 
  VectorCopy( dims, cg.cuboidSelection );
}

/*
======================
CG_CuboidRotate_f

Rotate the cuboid selection 90 degrees around the current axis.
Syntax:
 cuboidRotate
======================
*/
#define SWAPFLOATS(a,b) {float __t;__t=a,a=b,b=__t;}
void CG_CuboidRotate_f(void)
{
  if( !BG_Buildable( cg.predictedPlayerState.stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT, NULL )->cuboid )
    return;
  switch( cg_cuboidResizeAxis.integer )
  {
    case 0:
      SWAPFLOATS( cg.cuboidSelection[ 1 ], cg.cuboidSelection[ 2 ] );
      break;
    case 1:
      SWAPFLOATS( cg.cuboidSelection[ 2 ], cg.cuboidSelection[ 0 ] );
     break;
    case 2:
      SWAPFLOATS( cg.cuboidSelection[ 0 ], cg.cuboidSelection[ 1 ] );
      break;
  }
  trap_S_StartLocalSound( cgs.media.cuboidRotateSound, CHAN_LOCAL_SOUND );
}

/*
======================
CG_CuboidAxis_f

Control the axis along which cuboids are resized and around which they are rotated.
Syntax:
 cuboidAxis next - next axis (order is X->Y->Z)
 cuboidAxis 0|1|2 - switch to a specific axis (respectively: X, Y and Z)
======================
*/
void CG_CuboidAxis_f(void)
{
  int axis;
 
  axis = cg_cuboidResizeAxis.integer;
  if( !BG_Buildable( cg.predictedPlayerState.stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT, NULL )->cuboid)
    return;
  if( trap_Argc( ) < 2 )
  {
    Com_Printf( "cuboidAxis next|0|1|2 : set axis on which you want to resize your cuboid selection (0 - X, 1 - Y, 2 - Z)\n" );
    return;
  }
  if( !Q_stricmp( CG_Argv( 1 ), "next" ) )
    axis++;
  else
    axis = atoi( CG_Argv( 1 ) );
  trap_Cvar_Set( "cg_cuboidResizeAxis", va( "%i" , ( axis + 3 ) % 3 ) );
  trap_S_StartLocalSound( cgs.media.cuboidAxisChangeSound, CHAN_LOCAL_SOUND );
}

/*
======================
CG_CuboidSize_f

Set the cuboid selection
Syntax:
 cuboidAxis x y z
======================
*/
void CG_CuboidSize_f(void)
{
  if( trap_Argc( ) < 4 )
  {
    Com_Printf( "cuboidAxis x y z : set the cuboid selection\n" );
    return;
  }

  VectorSet( cg.cuboidSelection,
             atof( CG_Argv( 1 ) ),
             atof( CG_Argv( 2 ) ),
             atof( CG_Argv( 3 ) ) );

  trap_S_StartLocalSound( cgs.media.cuboidResizeSoundA, CHAN_LOCAL_SOUND );
}

/*
======================
CG_CuboidAttack_f

Replaces +attack/-attack.
If building a cuboid and the selection is somehow invalid, play an error sound.
Otherwise send the normal +attack / -attack.
======================
*/
void CG_CuboidAttack_f(void)
{
  if( BG_Buildable( cg.predictedPlayerState.stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT, NULL )->cuboid )
  {
    if( cg.waitForCB )
      return;
    else if( !cg.cuboidValid )
    {
      trap_S_StartLocalSound( cgs.media.cuboidErrorSound, CHAN_LOCAL_SOUND );
      return;
    }
  }
  trap_SendClientCommand( va( "%s", CG_Argv(0) ) );
}

/*
======================
CG_BuildablePowerStatusIcon

Figures out the power status icon for a buildable
======================
*/
qhandle_t CG_BuildablePowerStatusIcon( entityState_t *es )
{
  qboolean powered = ( es->eFlags & EF_B_POWERED );
  
  if( !( es->eFlags & EF_B_SPAWNED ) )
  {
    if( powered )
      return cgs.media.ckit_icon_surge;
    else
      return cgs.media.ckit_icon_nosurge;
  }
  
  if( BG_Buildable( es->modelindex, NULL )->isPowerSource ||
      es->modelindex == BA_H_REPEATER )
  {
    qboolean active = ( es->eFlags & EF_B_SURGE );
    
    if( !active )
      return cgs.media.ckit_icon_off;
    else if( powered )
      return cgs.media.ckit_icon_power;
    else
      return cgs.media.ckit_icon_nopower;
  }
  else
  {
    if( !( es->eFlags & EF_B_SURGE ) )
    {
      if( powered )
        return cgs.media.ckit_icon_power;
      else
        return cgs.media.ckit_icon_nopower;
    }
    else
    {
      if( powered )
        return cgs.media.ckit_icon_surge;
      else
        return cgs.media.ckit_icon_nosurge;
    }
  }
  return 0;
}
