
// cg_flamethrower.c - special code for the flamethrower effects
//
//	the flameChunks behave similarly to the trailJunc's, except they are rendered differently, and
//	also interact with the environment
//
// NOTE: some AI's are treated different, mostly for aesthetical reasons.

#include "cg_local.h"

// a flameChunk is a ball or section of fuel which goes from fuel->blue ignition->flame ball
// optimization is necessary, since lots of these will be spawned, but as they grow, they can be
// merged so that less overdraw occurs
typedef struct flameChunk_s
{
	struct flameChunk_s *nextGlobal, *prevGlobal;	// next junction in the global list it is in (free or used)
	struct flameChunk_s *nextFlameChunk;			// next junction in the trail
	struct flameChunk_s *nextHead, *prevHead;		// next head junc in the world

	qboolean		inuse;
	qboolean		dead;	// set when a chunk is effectively inactive, but waiting to be freed
	int				ownerCent;	// cent that spawned us

	int				timeStart, timeEnd;
	float			sizeMax;		// start small, increase if we slow down
	float			sizeRand;
	float			sizeRate;		// rate per ms, variable according to speed (larger if moving slower)
	vec3_t			baseOrg;
	int				baseOrgTime;
	vec3_t			velDir;
	float			velSpeed;		// flame chunks should start with a fast velocity, then slow down if there is nothing behind them pushing them along
	float			rollAngle;
	qboolean		ignitionOnly;
	int				blueLife;
	float			gravity;
	vec3_t			startVelDir;
	float			speedScale;

	// current variables
	vec3_t			org;
	float			size;
	float			lifeFrac;		// 0.0 (baby) -> 1.0 (aged)

	int				lastFriction, lastFrictionTake;
	vec3_t			parentFwd;
} flameChunk_t;

// DHM - Nerve :: lowered this from 2048.  Still allows 6-9 people flaming.
#define	MAX_FLAME_CHUNKS	1024
static flameChunk_t	flameChunks[MAX_FLAME_CHUNKS];
static flameChunk_t	*freeFlameChunks, *activeFlameChunks, *headFlameChunks;

static qboolean initFlameChunks = qfalse;

static int numFlameChunksInuse;

// this structure stores information relevant to each cent in the game, this way we keep
// the flamethrower data seperate to the rest of the code, which helps if we decide against
// using this weapon in the game
typedef struct centFlameInfo_s
{
	int		lastClientFrame;		// client frame that we last fired the flamethrower
	vec3_t	lastAngles;				// angles at last firing
	vec3_t	lastOrigin;				// origin at last firing
	flameChunk_t
			*lastFlameChunk;		// flame chunk we last spawned
	int		lastSoundUpdate;

	qboolean	lastFiring;

	int		lastDmgUpdate;			// time we last told server about this ent's flame damage
	int		lastDmgCheck;			// only check once per 100ms
	int		lastDmgEnemy;			// entity that inflicted the damage
} centFlameInfo_t;

static centFlameInfo_t centFlameInfo[MAX_GENTITIES];

typedef struct
{
//	float fireVolume;	// not needed, since we add individual loop sources for the flame, so it gets spacialized
	float blowVolume;
	float streamVolume;
} flameSoundStatus_t;

static flameSoundStatus_t centFlameStatus[MAX_GENTITIES];

// procedure defs
flameChunk_t *CG_SpawnFlameChunk( flameChunk_t *headFlameChunk );
void CG_FlameCalcOrg( flameChunk_t *f, int time, vec3_t outOrg );
void CG_FlameGetMuzzlePoint( vec3_t org, vec3_t fwd, vec3_t right, vec3_t up, vec3_t outPos );

// these must be globals, since they cannot expand or contract, since that might result in them getting
//	stuck in geometry. therefore when a chunk hits a surface, we should deflect it away from the surface
//	slightly, rather than running along it, so that as the chink grows, the sprites don't sink into the
//	wall too much.
static vec3_t	flameChunkMins = {-4, -4, -4};
static vec3_t	flameChunkMaxs = { 4,  4,  4};

// these define how the flame looks
#define	FLAME_START_SIZE		1.0
#define	FLAME_START_MAX_SIZE	140.0	// when the flame is spawned, it should endevour to reach this size
#define	FLAME_START_MAX_SIZE_RAND	60.0
#define	FLAME_MAX_SIZE			200.0	// flame sprites cannot be larger than this
#define	FLAME_MIN_MAXSIZE		40.0	// don't ever let the sizeMax go less than this
#define	FLAME_START_SPEED		1200.0//1200.0	// speed of flame as it leaves the nozzle
#define	FLAME_MIN_SPEED			60.0//200.0
#define	FLAME_CHUNK_DIST		4.0		// space in between chunks when fired

#define	FLAME_BLUE_LENGTH		130.0
#define	FLAME_BLUE_MAX_ALPHA	1.0

#define	FLAME_FUEL_LENGTH		48.0
#define	FLAME_FUEL_MAX_ALPHA	0.35
#define	FLAME_FUEL_MIN_WIDTH	1.0

// these are calculated (don't change)
#define	FLAME_LENGTH			(FLAMETHROWER_RANGE + 50.0)	// NOTE: only modify the range, since this should always reflect that range

#define	FLAME_LIFETIME			(int)((FLAME_LENGTH/FLAME_START_SPEED)*1000)	// life duration in milliseconds
#define	FLAME_FRICTION_PER_SEC	(2.0*FLAME_START_SPEED)
#define	FLAME_BLUE_LIFE			(int)((FLAME_BLUE_LENGTH/FLAME_START_SPEED)*1000)
#define	FLAME_FUEL_LIFE			(int)((FLAME_FUEL_LENGTH/FLAME_START_SPEED)*1000)
#define	FLAME_FUEL_FADEIN_TIME	(0.2*FLAME_FUEL_LIFE)

#define	FLAME_BLUE_FADEIN_TIME(x)		(0.2*x)
#define	FLAME_BLUE_FADEOUT_TIME(x)		(0.05*x)
#define	GET_FLAME_BLUE_SIZE_SPEED(x)	(((float)x / FLAME_LIFETIME) / 1.0)	// x is the current sizeMax
#define	GET_FLAME_SIZE_SPEED(x)			(((float)x / FLAME_LIFETIME) / 0.3)	// x is the current sizeMax

//#define	FLAME_MIN_DRAWSIZE		20

// enable this for the fuel stream
//#define FLAME_ENABLE_FUEL_STREAM

// enable this for dynamic lighting around flames
//#define FLAMETHROW_LIGHTS

// disable this to stop rotating flames (this is variable so we can change it at run-time)
int rotatingFlames = qtrue;

/*
===============
CG_FlameLerpVec
===============
*/
void CG_FlameLerpVec( const vec3_t oldV, const vec3_t newV, float backLerp, vec3_t outV )
{
	VectorScale( newV, (1.0 - backLerp), outV );
	VectorMA( outV, backLerp, oldV, outV );
}

/*
===============
CG_FlameAdjustSpeed
===============
*/
void CG_FlameAdjustSpeed( flameChunk_t *f, float change )
{
	if (!f->velSpeed && !change) {
		return;
	}

	f->velSpeed += change;
	if (f->velSpeed < FLAME_MIN_SPEED)
		f->velSpeed = FLAME_MIN_SPEED;
}

/*
===============
CG_FireFlameChunks

  The given entity is firing a flamethrower
===============
*/
void CG_FireFlameChunks( centity_t *cent, vec3_t origin, vec3_t angles, float speedScale, qboolean firing )
{
	centFlameInfo_t	*centInfo;
	flameChunk_t *f, *of;
	vec3_t	lastFwd, thisFwd, fwd;
	vec3_t	lastUp, thisUp, up;
	vec3_t	lastRight, thisRight, right;
	vec3_t	thisOrg, lastOrg, org;
	double	timeInc, backLerp, fracInc;
	int		t, numFrameChunks;
	double	ft;
	trace_t trace;
	vec3_t	parentFwd;

	centInfo = &centFlameInfo[cent->currentState.number];

	// for any other character or in 3rd person view, use entity angles for friction
	if (cent->currentState.number != cg.snap->ps.clientNum || cg_thirdPerson.integer) {
		AngleVectors( cent->currentState.angles, parentFwd, NULL, NULL );
	} else {
		AngleVectors( angles, parentFwd, NULL, NULL );
	}

	AngleVectors( angles, thisFwd, thisRight, thisUp );
	VectorCopy( origin, thisOrg );

	// if this entity was firing last frame, interpolate the angles as we spawn the chunks that
	// fired over the last frame
	if (	(centInfo->lastClientFrame == cent->currentState.frame) &&
			(centInfo->lastFlameChunk && centInfo->lastFiring == firing) ) {
		AngleVectors( centInfo->lastAngles, lastFwd, lastRight, lastUp );
		VectorCopy( centInfo->lastOrigin, lastOrg );
		centInfo->lastFiring = firing;

		of = centInfo->lastFlameChunk;
		timeInc = 1000.0 * (firing ? 1.0 : 0.5) * (FLAME_CHUNK_DIST / (FLAME_START_SPEED * speedScale));
		ft = ((double)of->timeStart + timeInc);
		t = (int)ft;
		fracInc = timeInc / (double)(cg.time - of->timeStart);
		backLerp = 1.0 - fracInc;

		numFrameChunks = 0;			// CHANGE: id

		while (t <= cg.time) {
			// spawn a new chunk
			CG_FlameLerpVec( lastOrg, thisOrg, backLerp, org );

			CG_Trace( &trace, org, flameChunkMins, flameChunkMaxs, org, cent->currentState.number, MASK_SHOT | MASK_WATER ); // JPW NERVE water fixes
			
			if (trace.startsolid)
				return;		// don't spawn inside a wall

			f = CG_SpawnFlameChunk( of );

			if (!f) {
				//CG_Printf( "Out of flame chunks\n" );
				// CHANGE: id
				// to make sure we do not keep trying to add more and more chunks
				centInfo->lastFlameChunk->timeStart = cg.time;
				// end CHANGE: id
				return;
			}

			CG_FlameLerpVec( lastFwd, thisFwd, backLerp, fwd );
			VectorNormalize( fwd );
			CG_FlameLerpVec( lastRight, thisRight, backLerp, right );
			VectorNormalize( right );
			CG_FlameLerpVec( lastUp, thisUp, backLerp, up );
			VectorNormalize( up );

			f->timeStart = t;
			f->timeEnd = t + FLAME_LIFETIME * (1.0/(0.5 + 0.5*speedScale));
			f->size = FLAME_START_SIZE * speedScale;
			f->sizeMax = speedScale * (FLAME_START_MAX_SIZE + f->sizeRand * (firing ? 1.0 : 0.0));
			f->sizeRand = 0;

			if (f->sizeMax > FLAME_MAX_SIZE)
				f->sizeMax = FLAME_MAX_SIZE;

			f->sizeRate = GET_FLAME_BLUE_SIZE_SPEED( f->sizeMax * speedScale * (1.0+(0.5*(float)!firing)) );
			VectorCopy( org, f->baseOrg );
			f->baseOrgTime = t;
			VectorCopy( fwd, f->velDir );
			VectorCopy( fwd, f->startVelDir );
			f->speedScale = speedScale;

			VectorNormalize( f->velDir );
			f->velSpeed = FLAME_START_SPEED * (0.5+0.5*speedScale) * (firing ? 1.0 : 4.5);
			f->ownerCent = cent->currentState.number;
			f->rollAngle = crandom()*179;
			f->ignitionOnly = !firing;

			if (!firing) {
				f->gravity = -150;
				f->blueLife = FLAME_BLUE_LIFE * 0.1;
			} else {
				f->gravity = 0;
				f->blueLife = FLAME_BLUE_LIFE;
			}
			f->lastFriction = cg.time;
			f->lastFrictionTake = cg.time;
			VectorCopy( parentFwd, f->parentFwd );

			ft += timeInc;
			// always spawn a chunk right on the current time
			if ((int)ft > cg.time && t < cg.time) {
				ft = (double)cg.time;
				backLerp = fracInc;	// so it'll get set to zero a few lines down
			}
			t = (int)ft;
			backLerp -= fracInc;
			centInfo->lastFlameChunk = of = f;
			// CHANGE: id
			// don't spawn too many chunks each frame
			if ( ++numFrameChunks > 50 ) {
				// to make sure we do not keep trying to add more and more chunks
				centInfo->lastFlameChunk->timeStart = cg.time;
				break;
			}
			// end CHANGE: id
		}
	} else {
		centInfo->lastFiring = firing;

		// just fire a single chunk to get us started
		f = CG_SpawnFlameChunk( NULL );

		if (!f) {
			//CG_Printf( "Out of flame chunks\n" );
			return;
		}
		
		VectorCopy( thisOrg, org );
		VectorCopy( thisFwd, fwd );
		VectorCopy( thisUp, up );
		VectorCopy( thisRight, right );

		f->timeStart = cg.time;
		f->timeEnd = cg.time + FLAME_LIFETIME * (1.0/(0.5 + 0.5*speedScale));
		f->size = FLAME_START_SIZE * speedScale;
		f->sizeMax = FLAME_START_MAX_SIZE * speedScale;
		if (f->sizeMax > FLAME_MAX_SIZE)
			f->sizeMax = FLAME_MAX_SIZE;

		f->sizeRand = 0;
		f->sizeRate = GET_FLAME_BLUE_SIZE_SPEED( f->sizeMax * speedScale );
		VectorCopy( org, f->baseOrg );
		f->baseOrgTime = cg.time;
		VectorCopy( fwd, f->velDir );
		VectorCopy( fwd, f->startVelDir );
		f->velSpeed = FLAME_START_SPEED * (0.5+0.5*speedScale);
		f->ownerCent = cent->currentState.number;
		f->rollAngle = crandom()*179;
		f->ignitionOnly = !firing;
		f->speedScale = speedScale;
		if (!firing) {
			f->gravity = -100;
			f->blueLife = (int)(0.3*(1.0/speedScale)*(float)FLAME_BLUE_LIFE);
		} else {
			f->gravity = 0;
			f->blueLife = FLAME_BLUE_LIFE;
		}
		f->lastFriction = cg.time;
		f->lastFrictionTake = cg.time;
		VectorCopy( parentFwd, f->parentFwd );

		centInfo->lastFlameChunk = f;
	}

	// push them along
	/*
	f = centInfo->lastFlameChunk;
	while (f) {

		if (f->lastFriction < cg.time - 50) {
			frametime = (float)(cg.time - f->lastFriction) / 1000.0;
			f->lastFriction = cg.time;
			dot = DotProduct(parentFwd, f->parentFwd);
			if (dot >= 0.99) {
				dot -= 0.99;
				dot *= (1.0/(1.0-0.99));
				CG_FlameAdjustSpeed( f, 0.5 * frametime * FLAME_FRICTION_PER_SEC * pow(dot,4) );
			}
		}

		f = f->nextFlameChunk;
	}
	*/

	VectorCopy( angles, centInfo->lastAngles );
	VectorCopy( origin, centInfo->lastOrigin );
	centInfo->lastClientFrame = cent->currentState.frame;
}

/*
===============
CG_ClearFlameChunks
===============
*/
void CG_ClearFlameChunks (void)
{
	int		i;

	memset( flameChunks, 0, sizeof(flameChunks) );
	memset( centFlameInfo, 0, sizeof(centFlameInfo) );

	freeFlameChunks = flameChunks;
	activeFlameChunks = NULL;
	headFlameChunks = NULL;

	for (i=0 ;i<MAX_FLAME_CHUNKS ; i++)
	{
		flameChunks[i].nextGlobal = &flameChunks[i+1];

		if (i>0)
			flameChunks[i].prevGlobal = &flameChunks[i-1];
		else
			flameChunks[i].prevGlobal = NULL;

		flameChunks[i].inuse = qfalse;
	}
	flameChunks[MAX_FLAME_CHUNKS-1].nextGlobal = NULL;

	initFlameChunks = qtrue;
	numFlameChunksInuse = 0;
}

/*
===============
CG_SpawnFlameChunk
===============
*/
flameChunk_t *CG_SpawnFlameChunk( flameChunk_t *headFlameChunk )
{
	flameChunk_t	*f;

	if (!freeFlameChunks)
		return NULL;

	if (headFlameChunks && headFlameChunks->dead)
		headFlameChunks = NULL;

	// select the first free trail, and remove it from the list
	f = freeFlameChunks;
	freeFlameChunks = f->nextGlobal;
	if (freeFlameChunks)
		freeFlameChunks->prevGlobal = NULL;

	f->nextGlobal = activeFlameChunks;
	if (activeFlameChunks)
		activeFlameChunks->prevGlobal = f;
	activeFlameChunks = f;
	f->prevGlobal = NULL;
	f->inuse = qtrue;
	f->dead = qfalse;

	// if this owner has a headJunc, add us to the start
	if (headFlameChunk) {
		// remove the headJunc from the list of heads
		if (headFlameChunk == headFlameChunks) {
			headFlameChunks = headFlameChunks->nextHead;
			if (headFlameChunks)
				headFlameChunks->prevHead = NULL;
		} else {
			if (headFlameChunk->nextHead)
				headFlameChunk->nextHead->prevHead = headFlameChunk->prevHead;
			if (headFlameChunk->prevHead)
				headFlameChunk->prevHead->nextHead = headFlameChunk->nextHead;
		}
		headFlameChunk->prevHead = NULL;
		headFlameChunk->nextHead = NULL;
	}
	// make us the headTrail
	if (headFlameChunks)
		headFlameChunks->prevHead = f;
	f->nextHead = headFlameChunks;
	f->prevHead = NULL;
	headFlameChunks = f;

	f->nextFlameChunk = headFlameChunk;	// if headJunc is NULL, then we'll just be the end of the list

	numFlameChunksInuse++;

	// debugging
//	if ( cg_drawRewards.integer > 1 )
//		CG_Printf( "NumFlameChunks: %i\n", numFlameChunksInuse );

	return f;
}

/*
===========
CG_FreeFlameChunk
===========
*/
void CG_FreeFlameChunk( flameChunk_t *f )
{
	// kill any juncs after us, so they aren't left hanging
	if (f->nextFlameChunk) {
		CG_FreeFlameChunk( f->nextFlameChunk );
		f->nextFlameChunk = NULL;
	}

	// make it non-active
	f->inuse = qfalse;
	f->dead = qfalse;
	if (f->nextGlobal)
		f->nextGlobal->prevGlobal = f->prevGlobal;
	if (f->prevGlobal)
		f->prevGlobal->nextGlobal = f->nextGlobal;
	if (f == activeFlameChunks)
		activeFlameChunks = f->nextGlobal;

	// if it's a head, remove it
	if (f == headFlameChunks)
		headFlameChunks = f->nextHead;
	if (f->nextHead)
		f->nextHead->prevHead = f->prevHead;
	if (f->prevHead)
		f->prevHead->nextHead = f->nextHead;
	f->nextHead = NULL;
	f->prevHead = NULL;

	// stick it in the free list
	f->prevGlobal = NULL;
	f->nextGlobal = freeFlameChunks;
	if (freeFlameChunks)
		freeFlameChunks->prevGlobal = f;
	freeFlameChunks = f;

	numFlameChunksInuse--;
}

/*
===============
CG_MergeFlameChunks

  Assumes f1 comes before f2
===============
*/
void CG_MergeFlameChunks( flameChunk_t *f1, flameChunk_t *f2 )
{
	if (f1->nextFlameChunk != f2) {
		CG_Error( "CG_MergeFlameChunks: f2 doesn't follow f1, cannot merge\n" );
	}

	f1->nextFlameChunk = f2->nextFlameChunk;
	f2->nextFlameChunk = NULL;

	VectorCopy( f2->velDir, f1->velDir );

	VectorCopy( f2->baseOrg, f1->baseOrg );
	f1->baseOrgTime = f2->baseOrgTime;

	f1->velSpeed = f2->velSpeed;
	f1->sizeMax = f2->sizeMax;
	f1->size = f2->size;
	f1->timeStart = f2->timeStart;
	f1->timeEnd = f2->timeEnd;

	CG_FreeFlameChunk( f2 );
}

/*
===============
CG_FlameCalcOrg
===============
*/
void CG_FlameCalcOrg( flameChunk_t *f, int time, vec3_t outOrg )
{
	VectorMA( f->baseOrg, f->velSpeed * ((float)(time - f->baseOrgTime) / 1000), f->velDir, outOrg );
	//outOrg[2] -= f->gravity * ((float)(time - f->timeStart)/1000.0) * ((float)(time - f->timeStart)/1000.0);
}

/*
===============
CG_MoveFlameChunk
===============
*/
void CG_MoveFlameChunk( flameChunk_t *f )
{
	vec3_t	newOrigin, sOrg;
	trace_t	trace;
	int		jiggleCount;
	float	dot;
  // TTimo: unused
	//static vec3_t	umins = {-1,-1,-1};
	//static vec3_t	umaxs = { 1, 1, 1};

	// subtract friction from speed
	if (f->velSpeed > 1 && f->lastFrictionTake < cg.time - 50) {
		CG_FlameAdjustSpeed( f, -((float)(cg.time - f->lastFrictionTake)/1000.0) * FLAME_FRICTION_PER_SEC );
		f->lastFrictionTake = cg.time;
	}

	// adjust size
	if (f->size < f->sizeMax) {
		if ((cg.time - f->timeStart) < f->blueLife)
			f->sizeRate = GET_FLAME_BLUE_SIZE_SPEED(FLAME_START_MAX_SIZE);	// use a constant so the blue flame doesn't distort
		else
			f->sizeRate = GET_FLAME_SIZE_SPEED(f->sizeMax);

		f->size += f->sizeRate * (float)(cg.time - f->baseOrgTime);
		if (f->size > f->sizeMax) {
			f->size = f->sizeMax;
		}
	}

	jiggleCount = 0;
	VectorCopy( f->baseOrg, sOrg );
	while ( f->velSpeed > 1 ) {
		CG_FlameCalcOrg( f, cg.time, newOrigin );

		// trace a line from previous position to new position
		CG_Trace( &trace, sOrg, flameChunkMins, flameChunkMaxs, newOrigin, f->ownerCent, MASK_SHOT | MASK_WATER ); // JPW NERVE water fixes

		if (trace.startsolid) {
			f->velSpeed = 0;
			f->dead = 1; // JPW NERVE water fixes
			break;
		}

		if ( trace.surfaceFlags & SURF_NOIMPACT ) {
			break;
		}

		// moved some distance
		VectorCopy( trace.endpos, f->baseOrg );
		f->baseOrgTime += (int)((float)(cg.time - f->baseOrgTime) * trace.fraction);

		if (trace.fraction == 1.0) {
			// check for hitting client
			if ((f->ownerCent != cg.snap->ps.clientNum) && !(cg.snap->ps.eFlags & EF_DEAD) && VectorDistance( newOrigin, cg.snap->ps.origin ) < 32) {
				VectorNegate( f->velDir, trace.plane.normal );
			} else {
				break;
			}
		}

		// reflect off surface
		dot = DotProduct( f->velDir, trace.plane.normal );
		VectorMA( f->velDir, -2*dot, trace.plane.normal, f->velDir );
		VectorNormalize( f->velDir );
		// subtract some speed
		f->velSpeed *= 0.5 * (0.25 + 0.75*((dot+1.0)*0.5));
		VectorCopy( f->velDir, f->parentFwd );

		VectorCopy( f->baseOrg, sOrg );
	}

	CG_FlameCalcOrg( f, cg.time, f->org );
	f->baseOrgTime = cg.time;	// incase we skipped the movement
}

/*
===============
CG_AddFlameSpriteToScene
===============
*/
static vec3_t	vright, vup;
static vec3_t	rright, rup;

#ifdef _DEBUG	// just in case we forget about it, but it should be disabled at all times (only enabled to generate updated shaders)
#ifdef ALLOW_GEN_SHADERS	// secondary security measure

//#define	GEN_FLAME_SHADER

#endif	// ALLOW_GEN_SHADERS
#endif	// _DEBUG

#define	FLAME_BLEND_SRC		"GL_ONE"
#define	FLAME_BLEND_DST		"GL_ONE_MINUS_SRC_COLOR"

#define	NUM_FLAME_SPRITES		45
#define	FLAME_SPRITE_DIR		"twiltb2"

#define	NUM_NOZZLE_SPRITES	8

static qhandle_t flameShaders[NUM_FLAME_SPRITES];
static qhandle_t nozzleShaders[NUM_NOZZLE_SPRITES];
static qboolean initFlameShaders = qtrue;

#define	MAX_CLIPPED_FLAMES	8		// dont draw more than this many per frame
static int numClippedFlames;


void CG_AddFlameSpriteToScene( flameChunk_t *f, float lifeFrac, float alpha )
{
	vec3_t		point, p2, projVec, sProj;
	polyVert_t	verts[4];
	float		radius, sdist, x;
	int			frameNum;
	vec3_t		vec, rotate_ang;
	unsigned char alphaChar;
	vec2_t		fovRadius;
	vec2_t		rdist, rST;
	int rollAngleClamped;
	static vec3_t	lastPos;

	// DHM - Nerve :: No client side damage
	//CG_FlameDamage( f->ownerCent, f->org, f->size );

	if (alpha < 0)
		return;	// we dont want to see this

	radius = (f->size/2.0);
	if (radius < 6) radius = 6;
	rST[0] = radius * 1.0;
	rST[1] = radius * 1.0/1.481;
	alphaChar = (unsigned char)(255.0 * alpha);

	verts[0].modulate[0] = alphaChar;	
	verts[0].modulate[1] = alphaChar;	
	verts[0].modulate[2] = alphaChar;	
	verts[0].modulate[3] = alphaChar;	
	verts[1] = verts[0];
	verts[2] = verts[0];
	verts[3] = verts[0];

	// find the projected distance from the eye to the projection of the flame origin
	// onto the view direction vector
	VectorMA( cg.refdef.vieworg, 1024, cg.refdef.viewaxis[0], p2 );
	ProjectPointOntoVector( f->org, cg.refdef.vieworg, p2, sProj );

	// make sure its infront of us
	VectorSubtract( sProj, cg.refdef.vieworg, vec );
	sdist = VectorNormalize( vec );
	if (!sdist || DotProduct( vec, cg.refdef.viewaxis[0] ) < 0)
		return;

	// if we are "inside" this sprite, clip it to our view frustum
	if (sdist < f->size*0.6) {
		// clip the sprite to the viewport, avoiding rendering off-screen pixels which
		// is the main cause of slow-downs

		if ((sdist < f->size*0.6) && (numClippedFlames++ > MAX_CLIPPED_FLAMES))
			return;

		// OPTIMIZATION: clamp to 90 degree rotations
		rollAngleClamped = 0;
		if ( (rotatingFlames) && (!(cg_fxflags & 1)) ) { // JPW NERVE no rotate for alt flame shaders
			vectoangles( cg.refdef.viewaxis[0], rotate_ang );
			rotate_ang[ROLL] += (rollAngleClamped = ((int)(f->rollAngle)/90) * 90);
			AngleVectors ( rotate_ang, NULL, rright, rup);
		} else {
			VectorCopy( vright, rright );
			VectorCopy( vup, rup );
		}

		VectorSubtract( sProj, f->org, projVec );

		// find the distances from the sprite origin to the projection along the refdef axis
		rdist[0] = -1.0*DotProduct( rright, projVec );
		rdist[1] = -1.0*DotProduct( rup, projVec );

		if (fabs(rdist[0]) > radius || fabs(rdist[1]) > radius)
			return;	// completely off-screen

		// now set the bounds for clipping
		fovRadius[0] = tan(DEG2RAD((rollAngleClamped%2==0 ? cg.refdef.fov_x : cg.refdef.fov_x)*0.52)) * sdist;
		fovRadius[1] = tan(DEG2RAD((rollAngleClamped%2==0 ? cg.refdef.fov_x : cg.refdef.fov_x)*0.52)) * sdist;

		// BOTTOM LEFT
		x = (-radius + rdist[0]);
		if (x < -fovRadius[0]) {	// clip
			VectorMA (f->org, -radius + (-fovRadius[0] - x), rright, point);	
			verts[0].st[0] = 0.0 + (-fovRadius[0] - x) / (radius*2);
		} else {
			VectorMA (f->org, -radius, rright, point);	
			verts[0].st[0] = 0.0;
		}
		x = (-radius + rdist[1]);
		if (x < -fovRadius[1]) {
			VectorMA (point, -radius + (-fovRadius[1] - x), rup, point);	
			verts[0].st[1] = 0.0 + (-fovRadius[1] - x) / (radius*2);
		} else {
			VectorMA (point, -radius, rup, point);	
			verts[0].st[1] = 0.0;
		}
		VectorCopy( point, verts[0].xyz );

		// TOP LEFT
		x = (-radius + rdist[0]);
		if (x < -fovRadius[0]) {	// clip
			VectorMA (f->org, -radius + (-fovRadius[0] - x), rright, point);	
			verts[1].st[0] = 0.0 + (-fovRadius[0] - x) / (radius*2);
		} else {
			VectorMA (f->org, -radius, rright, point);	
			verts[1].st[0] = 0.0;
		}
		x = (radius + rdist[1]);
		if (x > fovRadius[1]) {
			VectorMA (point, radius + (fovRadius[1] - x), rup, point);	
			verts[1].st[1] = 1.0 + (fovRadius[1] - x) / (radius*2);
		} else {
			VectorMA (point, radius, rup, point);	
			verts[1].st[1] = 1.0;
		}
		VectorCopy( point, verts[1].xyz );
			
		// TOP RIGHT
		x = (radius + rdist[0]);
		if (x > fovRadius[0]) {
			VectorMA (f->org, radius + (fovRadius[0] - x), rright, point);	
			verts[2].st[0] = 1.0 + (fovRadius[0] - x) / (radius*2);
		} else {
			VectorMA (f->org, radius, rright, point);	
			verts[2].st[0] = 1.0;
		}
		x = (radius + rdist[1]);
		if (x > fovRadius[1]) {
			VectorMA (point, radius + (fovRadius[1] - x), rup, point);	
			verts[2].st[1] = 1.0 + (fovRadius[1] - x) / (radius*2);
		} else {
			VectorMA (point, radius, rup, point);	
			verts[2].st[1] = 1.0;
		}
		VectorCopy( point, verts[2].xyz );
			
		// BOTTOM RIGHT
		x = (radius + rdist[0]);
		if (x > fovRadius[0]) {
			VectorMA (f->org, radius + (fovRadius[0] - x), rright, point);	
			verts[3].st[0] = 1.0 + (fovRadius[0] - x) / (radius*2);
		} else {
			VectorMA (f->org, radius, rright, point);	
			verts[3].st[0] = 1.0;
		}
		x = (-radius + rdist[1]);
		if (x < -fovRadius[1]) {
			VectorMA (point, -radius + (-fovRadius[1] - x), rup, point);	
			verts[3].st[1] = 0.0 + (-fovRadius[1] - x) / (radius*2);
		} else {
			VectorMA (point, -radius, rup, point);	
			verts[3].st[1] = 0.0;
		}
		VectorCopy( point, verts[3].xyz );

	} else {	// set default values for no clipping

		if ( (rotatingFlames) && (!(cg_fxflags & 1)) ) { // JPW NERVE no rotate for alt flame shaders {
			vectoangles( cg.refdef.viewaxis[0], rotate_ang );
			rotate_ang[ROLL] += f->rollAngle;
			AngleVectors ( rotate_ang, NULL, rright, rup);
		} else {
			VectorCopy( vright, rright );
			VectorCopy( vup, rup );
		}

		VectorMA (f->org, -rST[1], rup, point);	
		VectorMA (point, -rST[0], rright, point);	
		VectorCopy (point, verts[0].xyz);	
		verts[0].st[0] = 0;	
		verts[0].st[1] = 0;	

		VectorMA (point, rST[1]*2, rup, point);	
		VectorCopy (point, verts[1].xyz);	
		verts[1].st[0] = 0;	
		verts[1].st[1] = 1;	

		VectorMA (point, rST[0]*2, rright, point);	
		VectorCopy (point, verts[2].xyz);	
		verts[2].st[0] = 1;	
		verts[2].st[1] = 1;	

		VectorMA (point, -rST[1]*2, rup, point);	
		VectorCopy (point, verts[3].xyz);	
		verts[3].st[0] = 1;	
		verts[3].st[1] = 0;	
	}

	frameNum = (int)floor(lifeFrac*NUM_FLAME_SPRITES);
	if (frameNum < 0) frameNum = 0;
	else if (frameNum > NUM_FLAME_SPRITES-1) frameNum = NUM_FLAME_SPRITES-1;


// JPW NERVE alternate FT shaders
	if (cg_fxflags & 1) {
/*
		p->roll = 0;
		p->pshader = getTestShader();
		rotate_ang[ROLL]=90;
*/
		trap_R_AddPolyToScene(getTestShader(),4,verts);
	}
	else
		trap_R_AddPolyToScene( flameShaders[frameNum], 4, verts );
// jpw
	VectorCopy( f->org, lastPos );
}

static int	nextFlameLight = 0;
static int	lastFlameOwner = -1;
/*
===============
CG_AddFlameToScene
===============
*/
void CG_AddFlameToScene( flameChunk_t *fHead )
{
	flameChunk_t *f, *fNext;
	int		blueTrailHead=0, fuelTrailHead=0;
	static	vec3_t whiteColor = {1,1,1};
	vec3_t	c;
	float	alpha;
	float	lived;
	int		headTimeStart;
	// qboolean	firstFuel = qtrue; // TTimo: unused
	float	vdist, bdot;
#define	FLAME_SOUND_RANGE	1024.0
	//flameChunk_t *lastSoundFlameChunk=NULL; // TTimo: unused
  flameChunk_t *lastBlowChunk=NULL;
	qboolean isClientFlame, firing;
	int shader;
	flameChunk_t *lastBlueChunk=NULL;
	qboolean skip=qfalse, droppedTrail;
	vec3_t	v;
	vec3_t	lightOrg;				// origin to place light at
	float	lightSize;
	float	lightFlameCount;
	float	lastFuelAlpha;

	isClientFlame = (fHead == centFlameInfo[fHead->ownerCent].lastFlameChunk);

	if ((cg_entities[fHead->ownerCent].currentState.eFlags & EF_FIRING) && (centFlameInfo[fHead->ownerCent].lastFlameChunk == fHead)) {
		headTimeStart = fHead->timeStart;
		firing = qtrue;
	} else {
		headTimeStart = cg.time;
		firing = qfalse;
	}

	VectorClear( lightOrg );
	lightSize = 0;
	lightFlameCount = 0;

	lastFuelAlpha = 1.0;

	f = fHead;
	while (f) {

		if (f->nextFlameChunk && f->nextFlameChunk->dead) {
			// kill it
			CG_FreeFlameChunk(f->nextFlameChunk);
			f->nextFlameChunk = NULL;
		}

		// draw this chunk

		fNext = f->nextFlameChunk;
		lived = (float)(headTimeStart - f->timeStart);

		// update the "blow" sound volume (louder as we sway it)
		vdist = Distance( cg.refdef.vieworg, f->org );	// NOTE: this needs to be here or the flameSound code further below won't work
		if (	lastBlowChunk && (centFlameStatus[f->ownerCent].blowVolume < 1.0) &&
				((bdot = DotProduct(lastBlowChunk->startVelDir, f->startVelDir)) < 1.0)) {
			if (vdist < FLAME_SOUND_RANGE) {
				centFlameStatus[f->ownerCent].blowVolume += 500.0 * (1.0 - bdot) * (1.0 - (vdist / FLAME_SOUND_RANGE));
				if (centFlameStatus[f->ownerCent].blowVolume > 1.0)
					centFlameStatus[f->ownerCent].blowVolume = 1.0;
			}
		}
		lastBlowChunk = f;

		VectorMA( lightOrg, f->size/20.0, f->org, lightOrg );
		lightSize += f->size;
		lightFlameCount += f->size/20.0;

		droppedTrail = qfalse;

		// is it a stream chunk? (no special handling)
		if (!f->ignitionOnly && f->velSpeed < 1) {
			CG_AddFlameSpriteToScene (f, f->lifeFrac, 1.0);

		// is it in the blue ignition section of the flame?
		} else if (isClientFlame && f->blueLife > (lived/2.0)) {

			skip = qfalse;

			// if this is backwards from the last chunk, then skip it
			if ( fNext && f != fHead && lastBlueChunk) {
				VectorSubtract( f->org, lastBlueChunk->org, v );
				if (VectorNormalize( v ) < f->size/2 )
					skip = qtrue;
				else if (DotProduct( v, f->velDir ) < 0)
					skip = qtrue;
			}

			// stream sound
			if (!f->ignitionOnly) {
				centFlameStatus[f->ownerCent].streamVolume += 0.05;
				if (centFlameStatus[f->ownerCent].streamVolume > 1.0)
					centFlameStatus[f->ownerCent].streamVolume = 1.0;
			}


			if (!skip) {

				// just call this for damage checking
				//if (!f->ignitionOnly)
					//CG_AddFlameSpriteToScene( f, f->lifeFrac, -1 );

				lastBlueChunk = f;

				alpha = 1.0;	// new nozzle sprite
				VectorScale( whiteColor, alpha, c );

				if (f->blueLife > lived*(f->ignitionOnly ? 3.0 : 3.0)) {

					shader = nozzleShaders[(cg.time/50 + (cg.time/50 >> 1))%NUM_NOZZLE_SPRITES];

					blueTrailHead = CG_AddTrailJunc(	blueTrailHead,
														shader,
														cg.time,
														STYPE_STRETCH,
														f->org,
														1,
														alpha, alpha,
														f->size * (f->ignitionOnly /*&& (cg.snap->ps.clientNum != f->ownerCent || cg_thirdPerson.integer)*/ ? 2.0 : 1.0),
														FLAME_MAX_SIZE,
														TJFL_NOCULL|TJFL_FIXDISTORT,
														c, c, 1.0, 5.0 );
				}

				// fire stream
				if (!f->ignitionOnly) {
					float bscale;
					qboolean fskip=qfalse;

					bscale = 1.0;

					if (!f->nextFlameChunk) {
						alpha = 0;
					} else if (lived/1.3 < bscale*FLAME_BLUE_FADEIN_TIME(f->blueLife)) {
						alpha = FLAME_BLUE_MAX_ALPHA * ((lived/1.3) / (bscale*FLAME_BLUE_FADEIN_TIME(f->blueLife)));
					} else if (lived/1.3 < (f->blueLife - FLAME_BLUE_FADEOUT_TIME(f->blueLife))) {
						alpha = FLAME_BLUE_MAX_ALPHA;
					} else {
						alpha = FLAME_BLUE_MAX_ALPHA * (1.0 - ((lived/1.3 - (f->blueLife - FLAME_BLUE_FADEOUT_TIME(f->blueLife))) / (FLAME_BLUE_FADEOUT_TIME(f->blueLife))));
					}
					if (alpha <= 0.0) {
						alpha = 0.0;
						if (lastFuelAlpha <= 0.0)
							fskip = qtrue;
					}

					if (!fskip) {
						lastFuelAlpha = alpha;

						VectorScale( whiteColor, alpha, c );

						droppedTrail = qtrue;

						fuelTrailHead = CG_AddTrailJunc(	fuelTrailHead,
															cgs.media.flamethrowerFireStream,
															cg.time,
															(f->ignitionOnly ? STYPE_STRETCH : STYPE_REPEAT),
															f->org,
															1,
															alpha, alpha,
															(f->size/2 < f->sizeMax/4 ? f->size/2 : f->sizeMax/4),
															FLAME_MAX_SIZE,
															TJFL_NOCULL|TJFL_FIXDISTORT|TJFL_CROSSOVER,
															c, c, 0.5, 1.5 );
					}
				}
			}
		}

#define	FLAME_SPRITE_START_BLUE_SCALE	0.2

		if (!f->ignitionOnly &&
			((float)(FLAME_SPRITE_START_BLUE_SCALE*f->blueLife) < (float)lived)) {

			float alpha, lifeFrac;
			qboolean skip=qfalse;

			// should we merge it with the next sprite?
			while (fNext && !droppedTrail) {
				if ( (Distance( f->org, fNext->org ) < ( (0.1 + 0.9*f->lifeFrac) * f->size*0.35 ) )
					&&	(fabs(f->size - fNext->size) < (40.0))
					&&	(fabs(f->timeStart - fNext->timeStart) < 100)
					&&	(DotProduct( f->velDir, fNext->velDir ) > 0.99)
					) {
					if (!droppedTrail) {
						CG_MergeFlameChunks( f, fNext );
						fNext = f->nextFlameChunk;		// it may have changed
					} else {
						skip = qtrue;
						break;
					}
				} else {
					break;
				}
			}

			lifeFrac = (lived - FLAME_SPRITE_START_BLUE_SCALE*f->blueLife) / (FLAME_LIFETIME - FLAME_SPRITE_START_BLUE_SCALE*f->blueLife);

			alpha = (1.0 - lifeFrac)*1.4;
			if (alpha > 1.0)
				alpha = 1.0;

			if (!skip) {
				// draw the sprite
				CG_AddFlameSpriteToScene (f, lifeFrac, alpha );
			}
			// update the sizeRate
			f->sizeRate = GET_FLAME_SIZE_SPEED( f->sizeMax );
		}

		f = fNext;
	}

	if ( lastFlameOwner == fHead->ownerCent && nextFlameLight == cg.clientFrame )
		return;

	if ( !fHead->ignitionOnly ) {
		nextFlameLight = cg.clientFrame;
		lastFlameOwner = fHead->ownerCent;
	}

	if (lightSize < 80) {
		lightSize = 80;
	}

	if (lightSize > 500) lightSize = 500;
	lightSize *= 1.0 + 0.2*(sin(1.0*cg.time/50.0) * cos(1.0*cg.time/43.0));
	// set the alpha
	alpha = lightSize / 500.0;
	if (alpha > 1.0) {
		alpha = 1.0;
	}
	VectorScale( lightOrg, 1.0/lightFlameCount, lightOrg );
	// if it's only a nozzle, make it blue
	if (fHead->ignitionOnly) {
		if (lightSize > 80) lightSize = 80;
		trap_R_AddLightToScene( lightOrg, 90 + lightSize, 0, 0, alpha, 0 + isClientFlame * (fHead->ownerCent == cg.snap->ps.clientNum) );
	} else if (isClientFlame || (fHead->ownerCent == cg.snap->ps.clientNum)) {
		trap_R_AddLightToScene( lightOrg, 90 + lightSize, 1.000000*alpha, 0.603922*alpha, 0.207843*alpha, 0 );
	}
}

/*
=============
CG_GenerateShaders

  A util to create a bunch of shaders in a unique shader file, which represent an animation
=============
*/
void CG_GenerateShaders( char *filename, char *shaderName, char *dir, int numFrames, char *srcBlend, char *dstBlend, char *extras, qboolean compressedVersionAvailable, qboolean nomipmap )
{
	fileHandle_t f;
	int b, c, d, lastNumber;
	char	str[512];
	int i;

	trap_FS_FOpenFile( filename, &f, FS_WRITE );
	for (i=0; i<numFrames; i++) {
		lastNumber = i;
		b = lastNumber / 100;
		lastNumber -= b*100;
		c = lastNumber / 10;
		lastNumber -= c*10;
		d = lastNumber;

		if (compressedVersionAvailable) {
			Com_sprintf( str, sizeof(str), "%s%i\n{\n\tnofog%s\n\tallowCompress\n\tcull none\n\t{\n\t\tmapcomp sprites/%s_lg/spr%i%i%i.tga\n\t\tmapnocomp sprites/%s/spr%i%i%i.tga\n\t\tblendFunc %s %s\n%s\t}\n}\n", shaderName, i+1, nomipmap ? "\n\tnomipmaps" : "", dir, b, c, d, dir, b, c, d, srcBlend, dstBlend, extras );
		} else {
			Com_sprintf( str, sizeof(str), "%s%i\n{\n\tnofog%s\n\tallowCompress\n\tcull none\n\t{\n\t\tmap sprites/%s/spr%i%i%i.tga\n\t\tblendFunc %s %s\n%s\t}\n}\n", shaderName, i+1, nomipmap ? "\n\tnomipmap" : "", dir, b, c, d, srcBlend, dstBlend, extras );
		}
		trap_FS_Write( str, strlen(str), f );
	}
	trap_FS_FCloseFile( f );
}	

/*
===============
CG_InitFlameChunks
===============
*/
void CG_InitFlameChunks(void)
{
	int i;
	char filename[MAX_QPATH];

	CG_ClearFlameChunks();

#ifdef GEN_FLAME_SHADER
	CG_GenerateShaders( "scripts/flamethrower.shader",
						"flamethrowerFire",
						FLAME_SPRITE_DIR,
						NUM_FLAME_SPRITES,
						FLAME_BLEND_SRC,
						FLAME_BLEND_DST,
						"",
						qtrue, qtrue );

	CG_GenerateShaders( "scripts/blacksmokeanim.shader",
						"blacksmokeanim",
						"explode1",
						23,
						"GL_ZERO",
						"GL_ONE_MINUS_SRC_ALPHA",
						"\t\talphaGen const 0.2\n",
						qfalse, qfalse );

	CG_GenerateShaders( "scripts/viewflames.shader",
						"viewFlashFire",
						"clnfire",
						16,
						"GL_ONE",
						"GL_ONE",
						"\t\talphaGen vertex\n\t\trgbGen vertex\n",
						qtrue, qtrue );

	CG_GenerateShaders( "scripts/twiltb.shader",
						"twiltb",
						"twiltb",
						42,
						"GL_SRC_ALPHA",
						"GL_ONE_MINUS_SRC_COLOR",
						"",
						qtrue, qfalse );

	CG_GenerateShaders( "scripts/twiltb2.shader",
						"twiltb2",
						"twiltb2",
						45,
						"GL_ONE",
						"GL_ONE_MINUS_SRC_COLOR",
						"",
						qtrue, qfalse );
/*
	CG_GenerateShaders( "scripts/expblue.shader",
						"expblue",
						"expblue",
						25,
						"GL_ONE",
						"GL_ONE_MINUS_SRC_COLOR",
						"",
						qfalse, qfalse );
*/
	CG_GenerateShaders( "scripts/firest.shader",
						"firest",
						"firest",
						36,
						"GL_ONE",
						"GL_ONE_MINUS_SRC_COLOR",
						"",
						qtrue, qfalse );

	CG_GenerateShaders( "scripts/explode1.shader",
						"explode1",
						"explode1",
						23,
						"GL_ONE",
						"GL_ONE_MINUS_SRC_COLOR",
						"",
						qtrue, qfalse );

	CG_GenerateShaders( "scripts/funnel.shader",
						"funnel",
						"funnel",
						21,
						"GL_ONE",
						"GL_ONE_MINUS_SRC_COLOR",
						"",
						qfalse, qfalse );
#endif

	for (i=0; i<NUM_FLAME_SPRITES; i++) {
		Com_sprintf( filename, MAX_QPATH, "flamethrowerFire%i", i+1 );
		flameShaders[i] = trap_R_RegisterShader( filename );
	}
	for (i=0; i<NUM_NOZZLE_SPRITES; i++) {
		Com_sprintf( filename, MAX_QPATH, "nozzleFlame%i", i+1 );
		nozzleShaders[i] = trap_R_RegisterShader( filename );
	}
	initFlameShaders = qfalse;
}

/*
===============
CG_AddFlameChunks
===============
*/
void CG_AddFlameChunks(void)
{
	flameChunk_t *f, *fNext;

	//AngleVectors( cg.refdef.viewangles, NULL, vright, vup );
	VectorCopy( cg.refdef.viewaxis[1], vright );
	VectorCopy( cg.refdef.viewaxis[2], vup );

	// clear out the volumes so we can rebuild them
	memset( centFlameStatus, 0, sizeof(centFlameStatus) );

	numClippedFlames = 0;

	// age them
	f = activeFlameChunks;
	while (f) {
		if (!f->dead) {
			if (cg.time > f->timeEnd) {
				f->dead = qtrue;
			} else if (f->ignitionOnly && (f->blueLife < (cg.time - f->timeStart))) {
				f->dead = qtrue;
			} else {
				CG_MoveFlameChunk( f );
				f->lifeFrac = (float)(cg.time - f->timeStart)/(float)(f->timeEnd - f->timeStart);
			}
		}
		f = f->nextGlobal;
	}

	// draw each of the headFlameChunk's
	f = headFlameChunks;
	while (f) {
		fNext = f->nextHead;		// in case it gets removed
		if (f->dead) {
			if (centFlameInfo[f->ownerCent].lastFlameChunk == f) {
				centFlameInfo[f->ownerCent].lastFlameChunk = NULL;
				centFlameInfo[f->ownerCent].lastClientFrame = 0;
			}
			CG_FreeFlameChunk(f);
		} else if (!f->ignitionOnly || (centFlameInfo[f->ownerCent].lastFlameChunk == f)) {	// don't draw the ignition flame after we start firing
			CG_AddFlameToScene( f );
		}
		f = fNext;
	}
}

/*
===============
CG_UpdateFlamethrowerSounds
===============
*/
void CG_UpdateFlamethrowerSounds(void) {
	flameChunk_t *f, *trav; // , *lastSoundFlameChunk=NULL; // TTimo: unused
	#define	MIN_BLOW_VOLUME		30

	// draw each of the headFlameChunk's
	f = headFlameChunks;
	while (f) {
		// update this entity?
		if (centFlameInfo[f->ownerCent].lastSoundUpdate != cg.time) {
			// blow/ignition sound
			if (centFlameStatus[f->ownerCent].blowVolume*255.0 > MIN_BLOW_VOLUME) {
				trap_S_AddLoopingSound( f->ownerCent, f->org, vec3_origin, cgs.media.flameBlowSound, (int)(255.0*centFlameStatus[f->ownerCent].blowVolume) ); // JPW NERVE
			} else {
				trap_S_AddLoopingSound( f->ownerCent, f->org, vec3_origin, cgs.media.flameBlowSound, MIN_BLOW_VOLUME ); // JPW NERVE
			}

			if (centFlameStatus[f->ownerCent].streamVolume) {
				if (cg_entities[f->ownerCent].currentState.aiChar != AICHAR_ZOMBIE) {
					trap_S_AddLoopingSound( f->ownerCent, f->org, vec3_origin, cgs.media.flameStreamSound, (int)(255.0*centFlameStatus[f->ownerCent].streamVolume) ); // JPW NERVE
				} else {
					trap_S_AddLoopingSound( f->ownerCent, f->org, vec3_origin, cgs.media.flameCrackSound, (int)(255.0*centFlameStatus[f->ownerCent].streamVolume) ); // JPW NERVE
				}
			}

			centFlameInfo[f->ownerCent].lastSoundUpdate = cg.time;
		}

		// traverse the chunks, spawning flame sound sources as we go
		for (trav=f; trav; trav=trav->nextFlameChunk) {
			// update the sound volume
			if (trav->blueLife+100 < (cg.time - trav->timeStart)) {
				trap_S_AddLoopingSound( trav->ownerCent, trav->org, vec3_origin, cgs.media.flameSound, (int)(255.0*(0.2 * (trav->size / FLAME_MAX_SIZE))) );
			}
		}

		f = f->nextHead;
	}

	// DHM - Nerve :: No more client side damage
	// send client damage updates if required
	/*
	for (cent=cg_entities, i=0; i<cgs.maxclients; cent++, i++) {
		if (centFlameInfo[i].lastDmgCheck > centFlameInfo[i].lastDmgUpdate &&
			centFlameInfo[i].lastDmgUpdate < cg.time - 50 ) // JPW NERVE (cgs.gametype == GT_SINGLE_PLAYER ? 50 : 50)) -- sean changed clientdamage so this isn't a saturation issue any longer
		{
			if ((cg.snap->ps.pm_flags & PMF_LIMBO) || ( cg.snap->ps.persistant[PERS_TEAM] == TEAM_SPECTATOR )) // JPW NERVE
				return; // JPW NERVE don't do flame damage to guys in limbo or spectator, they drop out of the game
			CG_ClientDamage( i, centFlameInfo[i].lastDmgEnemy, CLDMG_FLAMETHROWER );
			centFlameInfo[i].lastDmgUpdate = cg.time;
		}
	}
	*/
}
