/*
===========================================================================
Copyright (C) 2000 - 2013, Raven Software, Inc.
Copyright (C) 2001 - 2013, Activision, Inc.
Copyright (C) 2013 - 2015, OpenJK contributors

This file is part of the OpenJK source code.

OpenJK is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License version 2 as
published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, see <http://www.gnu.org/licenses/>.
===========================================================================
*/

#include "g_local.h"
#include "g_functions.h"
#include "b_local.h"

extern int	G_FindConfigstringIndex( const char *name, int start, int max, qboolean create );

#define FX_ENT_RADIUS 32

extern int	BMS_START;
extern int	BMS_MID;
extern int	BMS_END;

//----------------------------------------------------------

/*QUAKED fx_runner (0 0 1) (-8 -8 -8) (8 8 8) STARTOFF ONESHOT DAMAGE
Runs the specified effect, can also be targeted at an info_notnull to orient the effect

	STARTOFF - effect starts off, toggles on/off when used
	ONESHOT - effect fires only when used
	DAMAGE - does radius damage around effect every "delay" milliseonds

	"fxFile" - name of the effect file to play
	"target" - direction to aim the effect in, otherwise defaults to up
	"target2" - uses its target2 when the fx gets triggered
	"delay"  - how often to call the effect, don't over-do this ( default 200 )
	"random" - random amount of time to add to delay, ( default 0, 200 = 0ms to 200ms )
	"splashRadius" - only works when damage is checked ( default 16 )
	"splashDamage" - only works when damage is checked ( default 5 )
	"soundset"	- bmodel set to use, plays start sound when toggled on, loop sound while on ( doesn't play on a oneshot), and a stop sound when turned off
*/
#define FX_RUNNER_RESERVED 0x800000

//----------------------------------------------------------
void fx_runner_think( gentity_t *ent )
{
	vec3_t temp;

	EvaluateTrajectory( &ent->s.pos, level.time, ent->currentOrigin );
	EvaluateTrajectory( &ent->s.apos, level.time, ent->currentAngles );

	// call the effect with the desired position and orientation
	G_AddEvent( ent, EV_PLAY_EFFECT, ent->fxID );

	// Assume angles, we'll do a cross product on the other end to finish up
	AngleVectors( ent->currentAngles, ent->pos3, NULL, NULL );
	MakeNormalVectors( ent->pos3, ent->pos4, temp ); // there IS a reason this is done...it's so that it doesn't break every effect in the game...

	ent->nextthink = level.time + ent->delay + Q_flrand(0.0f, 1.0f) * ent->random;

	if ( ent->spawnflags & 4 ) // damage
	{
		G_RadiusDamage( ent->currentOrigin, ent, ent->splashDamage, ent->splashRadius, ent, MOD_UNKNOWN );
	}

	if ( ent->target2 )
	{
		// let our target know that we have spawned an effect
		G_UseTargets2( ent, ent, ent->target2 );
	}

	if ( !(ent->spawnflags & 2 ) && !ent->s.loopSound ) // NOT ONESHOT...this is an assy thing to do
	{
		if ( VALIDSTRING( ent->soundSet ) == true )
		{
			ent->s.loopSound = CAS_GetBModelSound( ent->soundSet, BMS_MID );

			if ( ent->s.loopSound < 0 )
			{
				ent->s.loopSound = 0;
			}
		}
	}

}

//----------------------------------------------------------
void fx_runner_use( gentity_t *self, gentity_t *other, gentity_t *activator )
{
	if (self->s.isPortalEnt)
	{ //rww - mark it as broadcast upon first use if it's within the area of a skyportal
		self->svFlags |= SVF_BROADCAST;
	}

	if ( self->spawnflags & 2 ) // ONESHOT
	{
		// call the effect with the desired position and orientation, as a safety thing,
		//	make sure we aren't thinking at all.
		fx_runner_think( self );
		self->nextthink = -1;

		if ( self->target2 )
		{
			// let our target know that we have spawned an effect
			G_UseTargets2( self, self, self->target2 );
		}

		if ( VALIDSTRING( self->soundSet ) == true )
		{
			G_AddEvent( self, EV_BMODEL_SOUND, CAS_GetBModelSound( self->soundSet, BMS_START ));
		}
	}
	else
	{
		// ensure we are working with the right think function
		self->e_ThinkFunc = thinkF_fx_runner_think;

		// toggle our state
		if ( self->nextthink == -1 )
		{
			// NOTE: we fire the effect immediately on use, the fx_runner_think func will set
			//	up the nextthink time.
			fx_runner_think( self );

			if ( VALIDSTRING( self->soundSet ) == true )
			{
				G_AddEvent( self, EV_BMODEL_SOUND, CAS_GetBModelSound( self->soundSet, BMS_START ));
				self->s.loopSound = CAS_GetBModelSound( self->soundSet, BMS_MID );

				if ( self->s.loopSound < 0 )
				{
					self->s.loopSound = 0;
				}
			}
		}
		else
		{
			// turn off for now
			self->nextthink = -1;

			if ( VALIDSTRING( self->soundSet ) == true )
			{
				G_AddEvent( self, EV_BMODEL_SOUND, CAS_GetBModelSound( self->soundSet, BMS_END ));
				self->s.loopSound = 0;
			}
		}
	}
}

//----------------------------------------------------------
void fx_runner_link( gentity_t *ent )
{
	vec3_t	dir;

	if ( ent->target )
	{
		// try to use the target to override the orientation
		gentity_t	*target = NULL;

		target = G_Find( target, FOFS(targetname), ent->target );

		if ( !target )
		{
			// Bah, no good, dump a warning, but continue on and use the UP vector
			Com_Printf( "fx_runner_link: target specified but not found: %s\n", ent->target );
			Com_Printf( "  -assuming UP orientation.\n" );
		}
		else
		{
			// Our target is valid so let's override the default UP vector
			VectorSubtract( target->s.origin, ent->s.origin, dir );
			VectorNormalize( dir );
			vectoangles( dir, ent->s.angles );
		}
	}

	// don't really do anything with this right now other than do a check to warn the designers if the target2 is bogus
	if ( ent->target2 )
	{
		gentity_t	*target = NULL;

		target = G_Find( target, FOFS(targetname), ent->target2 );

		if ( !target )
		{
			// Target2 is bogus, but we can still continue
			Com_Printf( "fx_runner_link: target2 was specified but is not valid: %s\n", ent->target2 );
		}
	}

	G_SetAngles( ent, ent->s.angles );

	if ( ent->spawnflags & 1 || ent->spawnflags & 2 ) // STARTOFF || ONESHOT
	{
		// We won't even consider thinking until we are used
		ent->nextthink = -1;
	}
	else
	{
		if ( VALIDSTRING( ent->soundSet ) == true )
		{
			ent->s.loopSound = CAS_GetBModelSound( ent->soundSet, BMS_MID );

			if ( ent->s.loopSound < 0 )
			{
				ent->s.loopSound = 0;
			}
		}

		// Let's get to work right now!
		ent->e_ThinkFunc = thinkF_fx_runner_think;
		ent->nextthink = level.time + 200; // wait a small bit, then start working
	}

	// make us useable if we can be targeted
	if ( ent->targetname )
	{
		ent->e_UseFunc = useF_fx_runner_use;
	}
}

//----------------------------------------------------------
void SP_fx_runner( gentity_t *ent )
{
	// Get our defaults
	G_SpawnInt( "delay", "200", &ent->delay );
	G_SpawnFloat( "random", "0", &ent->random );
	G_SpawnInt( "splashRadius", "16", &ent->splashRadius );
	G_SpawnInt( "splashDamage", "5", &ent->splashDamage );

	if ( !G_SpawnAngleHack( "angle", "0", ent->s.angles ))
	{
		// didn't have angles, so give us the default of up
		VectorSet( ent->s.angles, -90, 0, 0 );
	}

	if ( !ent->fxFile )
	{
		gi.Printf( S_COLOR_RED"ERROR: fx_runner %s at %s has no fxFile specified\n", ent->targetname, vtos(ent->s.origin) );
		G_FreeEntity( ent );
		return;
	}

	// Try and associate an effect file, unfortunately we won't know if this worked or not
	//	until the CGAME trys to register it...
	ent->fxID = G_EffectIndex( ent->fxFile );

	ent->s.eType = ET_MOVER;

	// Give us a bit of time to spawn in the other entities, since we may have to target one of 'em
	ent->e_ThinkFunc = thinkF_fx_runner_link;
	ent->nextthink = level.time + 400;

	// Save our position and link us up!
	G_SetOrigin( ent, ent->s.origin );

	VectorSet( ent->maxs, FX_ENT_RADIUS, FX_ENT_RADIUS, FX_ENT_RADIUS );
	VectorScale( ent->maxs, -1, ent->mins );

	gi.linkentity( ent );
}


/*QUAKED fx_snow (1 0 0) (-16 -16 -16) (16 16 16) LIGHT MEDIUM HEAVY MISTY_FOG
This world effect will spawn snow globally into the level.
*/
void SP_CreateSnow( gentity_t *ent )
{
	cvar_t *r_weatherScale = gi.cvar( "r_weatherScale", "1", CVAR_ARCHIVE );
	if ( r_weatherScale->value == 0.0f )
	{
		return;
	}


	// Different Types Of Rain
	//-------------------------
	if (ent->spawnflags & 1)
	{
		G_FindConfigstringIndex("lightsnow", CS_WORLD_FX, MAX_WORLD_FX, qtrue);
	}
	else if (ent->spawnflags & 2)
	{
		G_FindConfigstringIndex("snow", CS_WORLD_FX, MAX_WORLD_FX, qtrue);
	}
	else if (ent->spawnflags & 4)
	{
		G_FindConfigstringIndex("heavysnow", CS_WORLD_FX, MAX_WORLD_FX, qtrue);
	}
	else
	{
		G_FindConfigstringIndex("snow", CS_WORLD_FX, MAX_WORLD_FX, qtrue);
		G_FindConfigstringIndex("fog", CS_WORLD_FX, MAX_WORLD_FX, qtrue);
	}


	// MISTY FOG
	//===========
	if (ent->spawnflags & 8)
	{
		G_FindConfigstringIndex("fog", CS_WORLD_FX, MAX_WORLD_FX, qtrue);
	}
}

/*QUAKED fx_wind (0 .5 .8) (-16 -16 -16) (16 16 16) NORMAL CONSTANT GUSTING SWIRLING x  FOG LIGHT_FOG
Generates global wind forces

NORMAL    creates a random light global wind
CONSTANT  forces all wind to go in a specified direction
GUSTING   causes random gusts of wind
SWIRLING  causes random swirls of wind

"angles" the direction for constant wind
"speed"  the speed for constant wind
*/
void SP_CreateWind( gentity_t *ent )
{
	cvar_t *r_weatherScale = gi.cvar( "r_weatherScale", "1", CVAR_ARCHIVE );
	if ( r_weatherScale->value <= 0.0f )
	{
		return;
	}

	char	temp[256];

	// Normal Wind
	//-------------
	if (ent->spawnflags & 1)
	{
		G_FindConfigstringIndex("wind", CS_WORLD_FX, MAX_WORLD_FX, qtrue);
	}

	// Constant Wind
	//---------------
	if (ent->spawnflags & 2)
	{
		vec3_t	windDir;
		AngleVectors(ent->s.angles, windDir, 0, 0);
		G_SpawnFloat( "speed", "500", &ent->speed );
		VectorScale(windDir, ent->speed, windDir);

		sprintf( temp, "constantwind ( %f %f %f )", windDir[0], windDir[1], windDir[2] );
		G_FindConfigstringIndex(temp, CS_WORLD_FX, MAX_WORLD_FX, qtrue);
	}

	// Gusting Wind
	//--------------
	if (ent->spawnflags & 4)
	{
		G_FindConfigstringIndex("gustingwind", CS_WORLD_FX, MAX_WORLD_FX, qtrue);
	}

	// Swirling Wind
	//---------------
	if (ent->spawnflags & 8)
	{
		G_FindConfigstringIndex("swirlingwind", CS_WORLD_FX, MAX_WORLD_FX, qtrue);
	}


	// MISTY FOG
	//===========
	if (ent->spawnflags & 32)
	{
		G_FindConfigstringIndex("fog", CS_WORLD_FX, MAX_WORLD_FX, qtrue);
	}

	// MISTY FOG
	//===========
	if (ent->spawnflags & 64)
	{
		G_FindConfigstringIndex("light_fog", CS_WORLD_FX, MAX_WORLD_FX, qtrue);
	}
}

/*QUAKED fx_wind_zone (0 .5 .8) ?  Creates a constant wind in a local area
Generates local wind forces

"angles" the direction for constant wind
"speed"  the speed for constant wind
*/
void SP_CreateWindZone( gentity_t *ent )
{
	cvar_t *r_weatherScale = gi.cvar( "r_weatherScale", "1", CVAR_ARCHIVE );
	if ( r_weatherScale->value <= 0.0f )
	{
		return;
	}

	gi.SetBrushModel(ent, ent->model);

	vec3_t	windDir;
 	AngleVectors(ent->s.angles, windDir, 0, 0);
	G_SpawnFloat( "speed", "500", &ent->speed );
	VectorScale(windDir, ent->speed, windDir);

	char	temp[256];
	sprintf( temp, "windzone ( %f %f %f ) ( %f %f %f ) ( %f %f %f )",
		ent->mins[0], ent->mins[1], ent->mins[2],
		ent->maxs[0], ent->maxs[1], ent->maxs[2],
		windDir[0],	  windDir[1],   windDir[2]
		);
	G_FindConfigstringIndex(temp, CS_WORLD_FX, MAX_WORLD_FX, qtrue);
}



/*QUAKED fx_rain (1 0 0) (-16 -16 -16) (16 16 16) LIGHT MEDIUM HEAVY ACID OUTSIDE_SHAKE MISTY_FOG
This world effect will spawn rain globally into the level.

LIGHT   create light drizzle
MEDIUM  create average medium rain
HEAVY   create heavy downpour (with fog and lightning automatically)
ACID    create acid rain

OUTSIDE_SHAKE  will cause the camera to shake slightly whenever outside
MISTY_FOG      causes clouds of misty fog to float through the level
LIGHTNING      causes random bursts of lightning and thunder in the level

The following fields are for lightning:
"flashcolor"    "200 200 200" (r g b) (values 0.0-255.0)
"flashdelay"    "12000" maximum time delay between lightning strikes
"chanceflicker" "2"  1 in 2 chance of flickering fog
"chancesound"   "3"  1 in 3 chance of playing a sound
"chanceeffect"  "4"  1 in 4 chance of playing the effect
*/
//----------------------------------------------------------

//----------------------------------------------------------
extern void G_SoundAtSpot( vec3_t org, int soundIndex, qboolean broadcast );

void fx_rain_think( gentity_t *ent )
{
	if (player)
	{
 		if (ent->count!=0)
		{
			ent->count--;
			if (ent->count==0 || (ent->count%2)==0)
			{
				gi.WE_SetTempGlobalFogColor(ent->pos2);		// Turn Off
				if (ent->count==0)
				{
					ent->nextthink = level.time + Q_irand(1000, 12000);
				}
				else if (ent->count==2)
				{
					ent->nextthink = level.time + Q_irand(150, 450);
				}
				else
				{
					ent->nextthink = level.time + Q_irand(50, 150);
				}
			}
			else
			{
				gi.WE_SetTempGlobalFogColor(ent->pos3);		// Turn On
				ent->nextthink = level.time + 50;
			}
		}
		else if (gi.WE_IsOutside(player->currentOrigin))
		{
			vec3_t	effectPos;
			vec3_t	effectDir;
			VectorClear(effectDir);
		 	effectDir[0] += Q_flrand(-1.0f, 1.0f);
			effectDir[1] += Q_flrand(-1.0f, 1.0f);

			bool	PlayEffect	= Q_irand(1,ent->aimDebounceTime)==1;
			bool	PlayFlicker = Q_irand(1,ent->attackDebounceTime)==1;
			bool	PlaySound	= (PlayEffect || PlayFlicker || Q_irand(1,ent->pushDebounceTime)==1);

			// Play The Sound
			//----------------
			if (PlaySound && !PlayEffect)
			{
				VectorMA(player->currentOrigin, 250.0f, effectDir, effectPos);
				G_SoundAtSpot(effectPos, G_SoundIndex(va("sound/ambience/thunder%d", Q_irand(1,4))), qtrue);
			}

			// Play The Effect
			//-----------------
 			if (PlayEffect)
			{
  			 	VectorMA(player->currentOrigin, 400.0f, effectDir, effectPos);
				if (PlaySound)
				{
					G_Sound(player, G_SoundIndex(va("sound/ambience/thunder_close%d", Q_irand(1,2))));
				}

				// Raise It Up Into The Sky
				//--------------------------
				effectPos[2] += Q_flrand(600.0f, 1000.0f);

				VectorClear(effectDir);
				effectDir[2]  = -1.0f;

				G_PlayEffect("env/huge_lightning", effectPos, effectDir);
				ent->nextthink = level.time + Q_irand(100, 200);
			}

			// Change The Fog Color
			//----------------------
			if (PlayFlicker)
			{
				ent->count = (Q_irand(1,4) * 2);
				ent->nextthink = level.time + 50;
				gi.WE_SetTempGlobalFogColor(ent->pos3);
			}
			else
			{
				ent->nextthink = level.time + Q_irand(1000, ent->delay);
			}
		}
		else
		{
			ent->nextthink = level.time + Q_irand(1000, ent->delay);
		}
	}
	else
	{
		ent->nextthink = level.time + Q_irand(1000, ent->delay);
	}
}

void SP_CreateRain( gentity_t *ent )
{
	// Different Types Of Rain
	//-------------------------
	if (ent->spawnflags & 1)
	{
		G_FindConfigstringIndex("lightrain", CS_WORLD_FX, MAX_WORLD_FX, qtrue);
	}
	else if (ent->spawnflags & 2)
	{
		G_FindConfigstringIndex("rain", CS_WORLD_FX, MAX_WORLD_FX, qtrue);
	}
	else if (ent->spawnflags & 4)
	{
		G_FindConfigstringIndex("heavyrain", CS_WORLD_FX, MAX_WORLD_FX, qtrue);

		// Automatically Get Heavy Fog
		//-----------------------------
		G_FindConfigstringIndex("heavyrainfog", CS_WORLD_FX, MAX_WORLD_FX, qtrue );

		// Automatically Get Lightning & Thunder
		//---------------------------------------
		ent->spawnflags |= 64;
	}
	else if (ent->spawnflags & 8)
	{
		G_EffectIndex( "world/acid_fizz" );
		G_FindConfigstringIndex("acidrain", CS_WORLD_FX, MAX_WORLD_FX, qtrue);
	}
	else
	{
		G_FindConfigstringIndex("lightrain", CS_WORLD_FX, MAX_WORLD_FX, qtrue);
		G_FindConfigstringIndex("wind", CS_WORLD_FX, MAX_WORLD_FX, qtrue);
	}


	// OUTSIDE SHAKE
	//===============
	if (ent->spawnflags & 16)
	{
		G_FindConfigstringIndex("outsideShake", CS_WORLD_FX, MAX_WORLD_FX, qtrue);
	}

	// MISTY FOG
	//===========
	if (ent->spawnflags & 32)
	{
		G_FindConfigstringIndex("fog", CS_WORLD_FX, MAX_WORLD_FX, qtrue );
	}

	// LIGHTNING
	//===========
	if (ent->spawnflags & 64)
	{
		G_SoundIndex("sound/ambience/thunder1");
		G_SoundIndex("sound/ambience/thunder2");
		G_SoundIndex("sound/ambience/thunder3");
		G_SoundIndex("sound/ambience/thunder4");
		G_SoundIndex("sound/ambience/thunder_close1");
		G_SoundIndex("sound/ambience/thunder_close2");
		G_EffectIndex( "env/huge_lightning" );
		ent->e_ThinkFunc = thinkF_fx_rain_think;
		ent->nextthink = level.time + Q_irand(4000, 8000);

		if (!G_SpawnVector( "flashcolor", "200 200 200", ent->pos3))
		{
			VectorSet(ent->pos3, 200, 200, 200);
		}
		VectorClear(ent->pos2);		// the "off" color

		G_SpawnInt("flashdelay",    "12000",	&ent->delay);
		G_SpawnInt("chanceflicker", "2",		&ent->attackDebounceTime);
		G_SpawnInt("chancesound",   "3",		&ent->pushDebounceTime);
		G_SpawnInt("chanceeffect",  "4",		&ent->aimDebounceTime);
	}
}

// Added by Aurelio Reis on 10/20/02.
/*QUAKED fx_puff (1 0 0) (-16 -16 -16) (16 16 16)
This world effect will spawn a puff system globally into the level.
Enter any valid puff command as a key and value to setup the puff
system properties.

"count"			The number of puffs/particles (default of 1000).

"whichsystem"	Which puff system to use (currently 0 and 1. Default 0).

// Apply a default puff system.
default  <value>
Current defaults are "snowstorm", "sandstorm", "foggy", and "smokey"

// Set the color of the particles (0-1.0).
color  ( <red>, <green>, <blue> )
default ( 0.5, 0.5, 0.5 )

// Set the alpha (transparency) value for the particles (0-1.0).
alpha  <value>
default 0.5

// Set which texture to use for the particles (make sure to include full path).
texture  <textures/texture.tga>
default gfx/effects/alpha_smoke2b.tga

// Set the size of particles (from center, like a radius) (MIN 4, MAX 2048).
size  <value>
default 100

// Whether the saber should flicker and spark or not (0 false, 1 true).
sabersparks   <value>
default 0

// Set texture filtering mode (0 = Bilinear(default), 1 = Nearest(less quality).
filtermode  <value>
default 0

// Set the alpha blending mode (0 = src, src-1, 1 = one, one (additive)).
blendmode  <value>
default 0

// How much to rotate particles per second (in degree's).
rotate ( <min>, <max> )
default ( 0, 0 )

// Set the area around the player the puffs cover:
spread ( minX minY minZ ) ( maxX maxY maxZ )
default: ( -600 -600 -500 ) ( 600 600 550 )

// Set the random range that sets the speed the puffs fall:
velocity ( minX minY minZ ) ( maxX maxY maxZ )
default: ( -15 -15 -20 ) ( 15 15 -70 )

// Set an area of puff blowing:
wind ( windOriginX windOriginY windOriginZ ) ( windVelocityX windVelocityY windVelocityZ ) ( sizeX sizeY sizeZ  )

// Set puff blowing data:
blowing duration <int>
blowing low <int>
		default: 3
blowing velocity ( min max )
		default: ( 30 70 )
blowing size ( minX minY minZ )
		default: ( 1000 300 300 )
*/
//----------------------------------------------------------
void SP_CreatePuffSystem( gentity_t *ent )
{
	char	temp[128];

	// Initialize the puff system to either 1000 particles or whatever they choose.
	G_SpawnInt( "count", "1000", &ent->count );
	cvar_t *r_weatherScale = gi.cvar( "r_weatherScale", "1", CVAR_ARCHIVE );

	// See which puff system to use.
	int iPuffSystem = 0;
	int iVal = 0;
	if ( G_SpawnInt( "whichsystem", "0", &iVal ) )
	{
		iPuffSystem = iVal;
		if ( iPuffSystem < 0 || iPuffSystem > 1 )
		{
			iPuffSystem = 0;
			//ri.Error( ERR_DROP, "Weather Effect: Invalid value for whichsystem key" );
			Com_Printf( "Weather Effect: Invalid value for whichsystem key\n" );
		}
	}

	if ( r_weatherScale->value > 0.0f )
	{
		sprintf( temp, "puff%i init %i", iPuffSystem, (int)( ent->count * r_weatherScale->value ));

		G_FindConfigstringIndex( temp, CS_WORLD_FX, MAX_WORLD_FX, qtrue );

		// Should return here??? It didn't originally...
	}

	// See whether we should have the saber spark from the puff system.
	iVal = 0;
	G_SpawnInt( "sabersparks", "0", &iVal );
	if ( iVal == 1 )
		level.worldFlags |= WF_PUFFING;
	else
		level.worldFlags &= ~WF_PUFFING;

	// Go through all the fields and assign the values to the created puff system now.
	for ( int i = 0; i < 20; i++ )
	{
		char *key = NULL;
		char *value = NULL;
		// Fetch a field.
		if ( !G_SpawnField( i, &key, &value ) )
			continue;

		// Make sure we don't get key's that are worthless.
		if ( Q_stricmp( key, "origin" ) == 0 || Q_stricmp( key, "classname" ) == 0 ||
			 Q_stricmp( key, "count" ) == 0 || Q_stricmp( key, "targetname" ) == 0  ||
			 Q_stricmp( key, "sabersparks" ) == 0 || Q_stricmp( key, "whichsystem" ) == 0 )
			continue;

		// Send the command.
		Com_sprintf( temp, 128, "puff%i %s %s", iPuffSystem, key, value );
		G_FindConfigstringIndex( temp, CS_WORLD_FX, MAX_WORLD_FX, qtrue );
 	}
}

// Don't use this! Too powerful! - Aurelio
/*NOTEINUSE! fx_command (1 0 0) (-16 -16 -16) (16 16 16)
//This effect allows you to issue console commands from within the world editor.
//Use the variables c00 to c99 to issue a maximum of 100 console commands.

//example: c00 r_showtris 1

*/
//----------------------------------------------------------
/*void SP_Command( gentity_t *ent )
{
	char *strCommand;

	// Go through all the commands.
	for ( int i = 0; i < 100; i++ )
	{
		strCommand = NULL;

		// Fetch a command.
		G_SpawnString( va("c%02d", i), NULL, &strCommand );

		// If it's valid, issue it.
		if ( strCommand && strCommand[0] )
		{
			gi.SendConsoleCommand( strCommand );
		}
	}
}*/

//-----------------
// Explosion Trail
//-----------------

//----------------------------------------------------------
void fx_explosion_trail_think( gentity_t *ent )
{
	vec3_t	origin;
	trace_t	tr;

	if ( ent->spawnflags & 1 ) // gravity
	{
		ent->s.pos.trType = TR_GRAVITY;
	}
	else
	{
		ent->s.pos.trType = TR_LINEAR;
	}

	EvaluateTrajectory( &ent->s.pos, level.time, origin );

	gi.trace( &tr, ent->currentOrigin, vec3_origin, vec3_origin, origin,
				ent->owner ? ent->owner->s.number : ENTITYNUM_NONE, ent->clipmask, G2_RETURNONHIT, 10 );

	if ( tr.fraction < 1.0f )
	{
		// never explode or bounce on sky
		if ( !( tr.surfaceFlags & SURF_NOIMPACT ))
		{
			if ( ent->splashDamage && ent->splashRadius )
			{
				G_RadiusDamage( tr.endpos, ent, ent->splashDamage, ent->splashRadius, ent, MOD_EXPLOSIVE_SPLASH );
			}
		}

		if ( ent->cameraGroup )
		{
			// fxFile2....in other words, impact fx
			G_PlayEffect( ent->cameraGroup, tr.endpos, tr.plane.normal );
		}

		if ( VALIDSTRING( ent->soundSet ) == true )
		{
			G_AddEvent( ent, EV_BMODEL_SOUND, CAS_GetBModelSound( ent->soundSet, BMS_END ));
		}

		G_FreeEntity( ent );
		return;
	}

	G_RadiusDamage( origin, ent, ent->damage, ent->radius, ent, MOD_EXPLOSIVE_SPLASH );

	// call the effect with the desired position and orientation
	G_PlayEffect( ent->fxID, origin, ent->currentAngles );

	ent->nextthink = level.time + 50;
	gi.linkentity( ent );
}

//----------------------------------------------------------
void fx_explosion_trail_use( gentity_t *self, gentity_t *other, gentity_t *activator )
{
	gentity_t *missile = G_Spawn();

	// We aren't a missile in the truest sense, rather we just move through the world and spawn effects
	if ( missile )
	{
		missile->classname = "fx_exp_trail";

		missile->nextthink = level.time + 50;
		missile->e_ThinkFunc = thinkF_fx_explosion_trail_think;

		missile->s.eType = ET_MOVER;

		missile->owner = self;

		missile->s.modelindex = self->s.modelindex2;

		missile->s.pos.trTime = level.time;
		G_SetOrigin( missile, self->currentOrigin );
		if ( self->spawnflags & 1 ) // gravity
		{
			missile->s.pos.trType = TR_GRAVITY;
		}
		else
		{
			missile->s.pos.trType = TR_LINEAR;
		}

		missile->spawnflags = self->spawnflags;

		G_SetAngles( missile, self->currentAngles );
		VectorScale( self->currentAngles, self->speed, missile->s.pos.trDelta );
		missile->s.pos.trTime = level.time;
		missile->radius = self->radius;
		missile->damage = self->damage;
		missile->splashDamage = self->splashDamage;
		missile->splashRadius = self->splashRadius;
		missile->fxID = self->fxID;
		missile->cameraGroup = self->cameraGroup;	//fxfile2

		missile->clipmask = MASK_SHOT;

		gi.linkentity( missile );

		if ( VALIDSTRING( self->soundSet ) == true )
		{
			G_AddEvent( self, EV_BMODEL_SOUND, CAS_GetBModelSound( self->soundSet, BMS_START ));
			missile->s.loopSound = CAS_GetBModelSound( self->soundSet, BMS_MID );
			missile->soundSet = G_NewString(self->soundSet);//get my own copy so i can free it when i die

			if ( missile->s.loopSound < 0 )
			{
				missile->s.loopSound = 0;
			}
		}
	}
}

//----------------------------------------------------------
void fx_explosion_trail_link( gentity_t *ent )
{
	vec3_t		dir;
	gentity_t	*target = NULL;

	// we ony activate when used
	ent->e_UseFunc = useF_fx_explosion_trail_use;

	if ( ent->target )
	{
		// try to use the target to override the orientation
		target = G_Find( target, FOFS(targetname), ent->target );

		if ( !target )
		{
			gi.Printf( S_COLOR_RED"ERROR: fx_explosion_trail %s could not find target %s\n", ent->targetname, ent->target );
			G_FreeEntity( ent );
			return;
		}

		// Our target is valid so lets use that
		VectorSubtract( target->s.origin, ent->s.origin, dir );
		VectorNormalize( dir );
	}
	else
	{
		// we are assuming that we have angles, but there are no checks to verify this
		AngleVectors( ent->s.angles, dir, NULL, NULL );
	}

	// NOTE: this really isn't an angle, but rather an orientation vector
	G_SetAngles( ent, dir );
}

/*QUAKED fx_explosion_trail (0 0 1) (-8 -8 -8) (8 8 8) GRAVITY
Creates an explosion type trail using the specified effect file, damaging things as it moves through the environment
Can also be used for something like a meteor, just add an impact effect ( fxFile2 ) and a splashDamage and splashRadius

  GRAVITY - object uses gravity instead of linear motion

  "fxFile" - name of the effect to play for the trail ( default "env/exp_trail_comp" )
  "fxFile2" - effect file to play on impact

  "model" - model to attach to the trail

  "target" - direction to aim the trail in, required unless you specify angles
  "targetname" - (required) trail effect spawns only when used.
  "speed" - velocity through the world, ( default 350 )

  "radius" - damage radius around trail as it travels through the world ( default 128 )
  "damage" - radius damage ( default 128 )
  "splashDamage" - damage when thing impacts ( default 0 )
  "splashRadius" - damage radius on impact ( default 0 )
  "soundset" - soundset to use, start sound plays when explosion trail starts, loop sound plays on explosion trail, end sound plays when it impacts

*/
//----------------------------------------------------------
void SP_fx_explosion_trail( gentity_t *ent )
{
	// We have to be useable, otherwise we won't spawn in
	if ( !ent->targetname )
	{
		gi.Printf( S_COLOR_RED"ERROR: fx_explosion_trail at %s has no targetname specified\n", vtos( ent->s.origin ));
		G_FreeEntity( ent );
		return;
	}

	// Get our defaults
	G_SpawnString( "fxFile", "env/exp_trail_comp", &ent->fxFile );
	G_SpawnInt( "damage", "128", &ent->damage );
	G_SpawnFloat( "radius", "128", &ent->radius );
	G_SpawnFloat( "speed", "350", &ent->speed );

	// Try to associate an effect file, unfortunately we won't know if this worked or not until the CGAME trys to register it...
	ent->fxID = G_EffectIndex( ent->fxFile );

	if ( ent->cameraGroup )
	{
		G_EffectIndex( ent->cameraGroup );
	}

	if ( ent->model )
	{
		ent->s.modelindex2 = G_ModelIndex( ent->model );
	}

	// Give us a bit of time to spawn in the other entities, since we may have to target one of 'em
	ent->e_ThinkFunc = thinkF_fx_explosion_trail_link;
	ent->nextthink = level.time + 500;

	// Save our position and link us up!
	G_SetOrigin( ent, ent->s.origin );

	VectorSet( ent->maxs, FX_ENT_RADIUS, FX_ENT_RADIUS, FX_ENT_RADIUS );
	VectorScale( ent->maxs, -1, ent->mins );

	gi.linkentity( ent );
}


//
//

//------------------------------------------
void fx_target_beam_set_debounce( gentity_t *self )
{
	if ( self->wait >= FRAMETIME )
	{
		self->attackDebounceTime = level.time + self->wait + Q_irand( -self->random, self->random );
	}
	else if ( self->wait < 0 )
	{
		self->e_UseFunc = useF_NULL;
	}
	else
	{
		self->attackDebounceTime = level.time + FRAMETIME + Q_irand( -self->random, self->random );
	}
}

//------------------------------------------
void fx_target_beam_fire( gentity_t *ent )
{
	trace_t		trace;
	vec3_t		dir, org, end;
	qboolean	open;

	if ( !ent->enemy || !ent->enemy->inuse )
	{//info_null most likely
		//ignore = ent->s.number;
		ent->enemy = NULL;
		VectorCopy( ent->s.origin2, org );
	}
	else
	{
		//ignore = ent->enemy->s.number;
		VectorCopy( ent->enemy->currentOrigin, org );
	}

	VectorCopy( org, ent->s.origin2 );
	VectorSubtract( org, ent->s.origin, dir );
	VectorNormalize( dir );

	gi.trace( &trace, ent->s.origin, NULL, NULL, org, ENTITYNUM_NONE, MASK_SHOT, (EG2_Collision)0, 0 );//ignore
	if ( ent->spawnflags & 2 )
	{
		open = qtrue;
		VectorCopy( org, end );
	}
	else
	{
		open = qfalse;
		VectorCopy( trace.endpos, end );
	}

	if ( trace.fraction < 1.0 )
	{
		if ( trace.entityNum < ENTITYNUM_WORLD )
		{
			gentity_t *victim = &g_entities[trace.entityNum];
			if ( victim && victim->takedamage )
			{
				if ( ent->spawnflags & 4 ) // NO_KNOCKBACK
				{
					G_Damage( victim, ent, ent->activator, dir, trace.endpos, ent->damage, DAMAGE_NO_KNOCKBACK, MOD_UNKNOWN );
				}
				else
				{
					G_Damage( victim, ent, ent->activator, dir, trace.endpos, ent->damage, 0, MOD_UNKNOWN );
				}
			}
		}
	}

	G_AddEvent( ent, EV_TARGET_BEAM_DRAW, ent->fxID );
	VectorCopy( end, ent->s.origin2 );

	if ( open )
	{
		VectorScale( dir, -1, ent->pos1 );
	}
	else
	{
		VectorCopy( trace.plane.normal, ent->pos1 );
	}

	ent->e_ThinkFunc = thinkF_fx_target_beam_think;
	ent->nextthink = level.time + FRAMETIME;
}

//------------------------------------------
void fx_target_beam_fire_start( gentity_t *self )
{
	fx_target_beam_set_debounce( self );
	self->e_ThinkFunc		= thinkF_fx_target_beam_think;
	self->nextthink			= level.time + FRAMETIME;
	self->painDebounceTime	= level.time + self->speed + Q_irand( -500, 500 );
	fx_target_beam_fire( self );
}

//------------------------------------------
void fx_target_beam_use( gentity_t *self, gentity_t *other, gentity_t *activator )
{
	if ( self->spawnflags & 8 ) // one shot
	{
		fx_target_beam_fire( self );
		self->e_ThinkFunc	= thinkF_NULL;
	}
	else if ( self->e_ThinkFunc == thinkF_NULL )
	{
		self->e_ThinkFunc	= thinkF_fx_target_beam_think;
		self->nextthink		= level.time + 50;
	}
	else
	{
		self->e_ThinkFunc	= thinkF_NULL;
	}

	self->activator = activator;
}

//------------------------------------------
void fx_target_beam_think( gentity_t *ent )
{
	if ( ent->attackDebounceTime > level.time )
	{
		ent->nextthink = level.time + FRAMETIME;
		return;
	}

	fx_target_beam_fire_start( ent );
}

//------------------------------------------
void fx_target_beam_link( gentity_t *ent )
{
	gentity_t	*target = NULL;
	vec3_t		dir;

	target = G_Find( target, FOFS(targetname), ent->target );

	if ( !target )
	{
		Com_Printf( "bolt_link: unable to find target %s\n", ent->target );
		G_FreeEntity( ent );
		return;
	}

	ent->attackDebounceTime = level.time;

	if ( !target->classname || Q_stricmp( "info_null", target->classname ) )
	{//don't want to set enemy to something that's going to free itself... actually, this could be bad in other ways, too... ent pointer could be freed up and re-used by the time we check it next
		G_SetEnemy( ent, target );
	}
	VectorSubtract( target->s.origin, ent->s.origin, dir );//er, does it ever use dir?
	/*len = */VectorNormalize( dir );//er, does it use len or dir?
	vectoangles( dir, ent->s.angles );//er, does it use s.angles?

	VectorCopy( target->s.origin, ent->s.origin2 );

	if ( ent->spawnflags & 1 )
	{
		// Do nothing
		ent->e_ThinkFunc	= thinkF_NULL;
	}
	else
	{
		if ( !(ent->spawnflags & 8 )) // one_shot, only calls when used
		{
			// switch think functions to avoid doing the bolt_link every time
			ent->e_ThinkFunc = thinkF_fx_target_beam_think;
			ent->nextthink	= level.time + FRAMETIME;
		}
	}

	ent->e_UseFunc = useF_fx_target_beam_use;
	gi.linkentity( ent );
}

/*QUAKED fx_target_beam (1 0.5 0.5) (-8 -8 -8) (8 8 8) STARTOFF OPEN NO_KNOCKBACK ONE_SHOT NO_IMPACT
 Emits specified effect file, doing damage if required

STARTOFF - must be used before it's on
OPEN - will draw all the way to the target, regardless of where the trace hits
NO_KNOCKBACK - beam damage does no knockback

 "fxFile" - Effect used to draw the beam, ( default "env/targ_beam" )
 "fxFile2" - Effect used for the beam impact effect, ( default "env/targ_beam_impact" )
 "targetname" - Fires only when used
 "duration" - How many seconds each burst lasts, -1 will make it stay on forever
 "wait" - If always on, how long to wait between blasts, in MILLISECONDS - default/min is 100 (1 frame at 10 fps), -1 means it will never fire again
 "random" - random amount of seconds added to/subtracted from "wait" each firing
 "damage" - How much damage to inflict PER FRAME (so probably want it kind of low), default is none
 "target" - ent to point at- you MUST have this.  This can be anything you want, including a moving ent - for static beams, just use info_null
*/
//------------------------------------------
void SP_fx_target_beam( gentity_t *ent )
{
	G_SetOrigin( ent, ent->s.origin );

	ent->speed	*= 1000;
	ent->wait	*= 1000;
	ent->random *= 1000;

	if ( ent->speed < FRAMETIME )
	{
		ent->speed = FRAMETIME;
	}

	G_SpawnInt( "damage", "0", &ent->damage );
	G_SpawnString( "fxFile", "env/targ_beam", &ent->fxFile );

	if ( ent->spawnflags & 16 ) // NO_IMPACT FX
	{
		ent->delay = 0;
	}
	else
	{
		G_SpawnString( "fxFile2", "env/targ_beam_impact", &ent->cameraGroup );
		ent->delay = G_EffectIndex( ent->cameraGroup );
	}

	ent->fxID = G_EffectIndex( ent->fxFile );

	ent->activator = ent;
	ent->owner	= NULL;

	ent->e_ThinkFunc = thinkF_fx_target_beam_link;
	ent->nextthink	= level.time + START_TIME_LINK_ENTS;

	VectorSet( ent->maxs, FX_ENT_RADIUS, FX_ENT_RADIUS, FX_ENT_RADIUS );
	VectorScale( ent->maxs, -1, ent->mins );

	gi.linkentity( ent );
}


/*QUAKED fx_cloudlayer (1 0.3 0.5) (-8 -8 -8) (8 8 8) TUBE ALT

  Creates a scalable scrolling cloud layer, mostly for bespin undercity but could be used other places

  TUBE - creates cloud layer with tube opening in the middle, must an INNER radius also
  ALT - uses slightly different shader, good if using two layers sort of close together

"radius" - outer radius of cloud layer, (default 2048)
"random" - inner radius of cloud layer, (default 128) only works for TUBE type
"wait" - adds curvature as it moves out to the edge of the layer.  ( default 0 ), 1 = small up, 3 = up more, -1 = small down, -3 = down more, etc.

*/

void SP_fx_cloudlayer( gentity_t *ent )
{
	// HACK: this effect is never played, rather it just caches the shaders I need cgame side
	G_EffectIndex( "world/haze_cache" );

	G_SpawnFloat( "radius", "2048", &ent->radius );
	G_SpawnFloat( "random", "128", &ent->random );
	G_SpawnFloat( "wait", "0", &ent->wait );

	ent->s.eType = ET_CLOUD; // dumb

	G_SetOrigin( ent, ent->s.origin );

	ent->contents = 0;
	VectorSet( ent->maxs, 200, 200, 200 );
	VectorScale( ent->maxs, -1, ent->mins );

	gi.linkentity( ent );
}
