/*
   Copyright (C) 1997-2001 Id Software, Inc.

   This program 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.

   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, write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

 */
#include "cg_local.h"

/*
   ==============
   CG_TouchJumpPad
   ==============
 */
static void CG_TouchJumpPad( int entNum )
{
	CG_SexedSound( entNum, CHAN_VOICE, va( S_PLAYER_JUMP_1_to_2, ( rand()&1 )+1 ), cg_volume_players->value );
	CG_AddPModelAnimation( entNum, LEGS_JUMP3, 0, 0, EVENT_CHANNEL );
}

//==============
//CG_StartVoiceTokenEffect
//==============
static void CG_StartVoiceTokenEffect( int entNum, int type, int vsay )
{
	centity_t *cent;
	cgs_media_handle_t *sound = NULL;

	if( !cg_voiceChats->integer || cg_volume_voicechats->value <= 0.0f )
		return;

	cent = &cg_entities[entNum];

	// set the icon effect
	cent->localEffects[LOCALEFFECT_VSAY_HEADICON] = vsay;
	cent->localEffects[LOCALEFFECT_VSAY_HEADICON_TIMEOUT] = cg.time + HEADICON_TIMEOUT;

	// play the sound
	sound = cgs.media.sfxVSaySounds[vsay];

	// played as it was made by the 1st person player
	trap_S_StartGlobalSound( CG_MediaSfx( sound ), CHAN_AUTO, cg_volume_voicechats->value );
}

/*
   ==============
   CG_PlayerMuzzleFlash
   ==============
 */
void CG_PlayerMuzzleFlash( int entNum, int parms )
{
	int i;
	centity_t *pl;
	float radius, volume, attenuation;
	vec3_t lightcolor;
	vec3_t muzzle;
	int weapon;
	int strongmode = parms;
	orientation_t projection_source;
	cgs_media_handle_t *sound = NULL;

	pl = &cg_entities[entNum];
	if( pl->current.type != ET_PLAYER )
		return;

	volume = 1.0f;

	if( strongmode )
		radius = 200 + ( rand()&31 );
	else
		radius = 100 + ( rand()&31 );

	if( pl->current.renderfx & RF_FRAMELERP )
	{
		VectorCopy( pl->current.origin, muzzle );
	}
	else if( CG_PModel_GetProjectionSource( entNum, &projection_source ) )
	{
		VectorCopy( projection_source.origin, muzzle );
	}
	else
	{
		for( i = 0; i < 3; i++ )
			muzzle[i] = pl->prev.origin[i] + cg.lerpfrac * ( pl->current.origin[i] - pl->prev.origin[i] );
	}

	weapon = pl->current.weapon;

	attenuation = ATTN_NORM;

	switch( weapon )
	{
	case WEAP_GUNBLADE:
		if( strongmode )
		{
			VectorSet( lightcolor, 1.0f, 0.0f, 0.2f );
			sound = cgs.media.sfxGunbladeStrongShot;
		}
		else
		{
			radius = 0;
			sound = cgs.media.sfxGunbladeWeakShot[(int)( random()*3 )];
		}
		break;
	case WEAP_RIOTGUN:
		VectorSet( lightcolor, 1.0f, 0.0f, 0.2f );
		sound = strongmode ? cgs.media.sfxRiotgunStrongShot : cgs.media.sfxRiotgunWeakShot;
		break;
	case WEAP_GRENADELAUNCHER:
		VectorSet( lightcolor, 1.0f, 0.0f, 0.2f );
		sound = strongmode ? cgs.media.sfxGrenadeLauncherStrongShot : cgs.media.sfxGrenadeLauncherWeakShot;
		break;
	case WEAP_ROCKETLAUNCHER:
		VectorSet( lightcolor, 1.0f, 0.0f, 0.2f );
		sound = strongmode ? cgs.media.sfxRocketLauncherStrongShot : cgs.media.sfxRocketLauncherWeakShot;
		break;
	case WEAP_PLASMAGUN:
		VectorSet( lightcolor, 0.0f, 1.0f, 0.0f );
		sound = strongmode ? cgs.media.sfxPlasmagunStrongShot[rand()%3] : cgs.media.sfxPlasmagunWeakShot;
		attenuation = ATTN_IDLE;
		break;
	case WEAP_ELECTROBOLT:
		VectorSet( lightcolor, 0.9f, 0.9f, 1.0f );
		sound = strongmode ? cgs.media.sfxElectroboltStrongShot : cgs.media.sfxElectroboltWeakShot;
		break;
	case WEAP_INSTAGUN:
		VectorSet( lightcolor, 1.0f, 0.2f, 0.0f );
		sound = strongmode ? cgs.media.sfxInstagunStrongShot : cgs.media.sfxInstagunWeakShot;
		break;
	case WEAP_LASERGUN:
		sound = strongmode ? cgs.media.sfxLasergunStrongShot : cgs.media.sfxLasergunWeakShot;
		radius = 0;
		VectorClear( lightcolor );
		break;

		// default to no light
	default:
		radius = 0;
		VectorClear( lightcolor );
		break;
	}

	// spawn light if not cleared
	if( radius )
		CG_AddLightToScene( muzzle, radius, lightcolor[0], lightcolor[1], lightcolor[2], NULL );

	if( sound )
	{
		if( ISVIEWERENTITY( entNum ) )
		{
			trap_S_StartGlobalSound( CG_MediaSfx( sound ), CHAN_MUZZLEFLASH, cg_volume_effects->value );
		}
		else
		{
			// fixed position is better for location, but the channels are used from worldspawn
			// and openal runs out of channels quick on cheap cards. Relative sound uses per-entity channels.
			trap_S_StartRelativeSound( CG_MediaSfx( sound ), entNum, CHAN_MUZZLEFLASH, cg_volume_effects->value, attenuation );
		}
	}

	//add the animation and weapon flash
	CG_PModel_StartShootEffect( entNum );
	if( ISVIEWERENTITY( entNum ) && !cg.view.thirdperson )  // in first person view start view weapon fx too
		CG_ViewWeapon_StartShootEffect( strongmode );
}

/*
   ==============
   CG_FireLead

   Clientside prediction of gunshots.
   Must match fire_lead in g_weapon.c
   ==============
 */
static void CG_FireLead( int self, vec3_t start, vec3_t axis[3], int hspread, int vspread, int *seed, trace_t *trace )
{
	trace_t	tr;
	vec3_t dir;
	vec3_t end;
	float r;
	float u;
	vec3_t water_start;
	qboolean water = qfalse;
	int content_mask = MASK_SHOT | MASK_WATER;

#ifdef SPIRALRIOT
	// Uz: fixed pattern: spiral
	r = cos( ( *seed ) )*hspread * ( *seed );
	u = sin( ( *seed ) )*vspread * ( *seed );
#else
#if 1
	// circle
	double alpha = M_PI *Q_crandom( seed ); // [-PI ..+PI]
	double s = fabs( Q_crandom( seed ) ); // [0..1]
	r = s *cos( alpha )*hspread;
	u = s *sin( alpha )*vspread;
#else
	// square
	r = Q_crandom( seed ) * hspread;
	u = Q_crandom( seed ) * vspread;
#endif
#endif

	VectorMA( start, 8192, axis[0], end );
	VectorMA( end, r, axis[1], end );
	VectorMA( end, u, axis[2], end );

	if( CG_PointContents( start ) & MASK_WATER )
	{
		water = qtrue;
		VectorCopy( start, water_start );
		content_mask &= ~MASK_WATER;
	}

	CG_Trace( &tr, start, vec3_origin, vec3_origin, end, self, content_mask );

	// see if we hit water
	if( tr.contents & MASK_WATER )
	{
		water = qtrue;
		VectorCopy( tr.endpos, water_start );

		if( !VectorCompare( start, tr.endpos ) )
		{
			vec3_t forward, right, up;

			if( tr.contents & CONTENTS_WATER )
				CG_ParticleEffect( tr.endpos, tr.plane.normal, 0.47f, 0.48f, 0.8f, 8 );
			else if( tr.contents & CONTENTS_SLIME )
				CG_ParticleEffect( tr.endpos, tr.plane.normal, 0.0f, 1.0f, 0.0f, 8 );
			else if( tr.contents & CONTENTS_LAVA )
				CG_ParticleEffect( tr.endpos, tr.plane.normal, 1.0f, 0.67f, 0.0f, 8 );

			// change bullet's course when it enters water
			VectorSubtract( end, start, dir );
			VecToAngles( dir, dir );
			AngleVectors( dir, forward, right, up );
#ifdef SPIRALRIOT
			// Uz: fixed pattern: spiral
			r = cos( ( *seed ) )*hspread * 1.5 * ( *seed );
			u = sin( ( *seed ) )*vspread * 1.5 * ( *seed );
#else
#if 1
			// circle
			alpha = M_PI *Q_crandom( seed ); // [-PI ..+PI]
			s = fabs( Q_crandom( seed ) ); // [0..1]
			r = s *cos( alpha )*hspread*1.5;
			u = s *sin( alpha )*vspread*1.5;
#else
			r = Q_crandom( seed ) * hspread * 2;
			u = Q_crandom( seed ) * vspread * 2;
#endif
#endif
			VectorMA( water_start, 8192, forward, end );
			VectorMA( end, r, right, end );
			VectorMA( end, u, up, end );
		}

		// re-trace ignoring water this time
		CG_Trace( &tr, water_start, vec3_origin, vec3_origin, end, self, MASK_SHOT );
	}

	// save the final trace
	*trace = tr;

	// if went through water, determine where the end and make a bubble trail
	if( water )
	{
		vec3_t pos;

		VectorSubtract( tr.endpos, water_start, dir );
		VectorNormalize( dir );
		VectorMA( tr.endpos, -2, dir, pos );

		if( CG_PointContents( pos ) & MASK_WATER )
			VectorCopy( pos, tr.endpos );
		else
			CG_Trace( &tr, pos, vec3_origin, vec3_origin, water_start, tr.ent ? cg_entities[tr.ent].current.number : 0, MASK_WATER );

		VectorAdd( water_start, tr.endpos, pos );
		VectorScale( pos, 0.5, pos );

		CG_BubbleTrail( water_start, tr.endpos, 32 );
	}
}

//==============
//CG_FireBullet
//==============
static void CG_FireBullet( int self, vec3_t start, vec3_t forward, int count, int spread, int seed, void ( *impact )(trace_t *tr /*, int impactnum*/) )
{
	int i;
	trace_t tr;
	vec3_t dir, axis[3];
	qboolean takedamage;

	// calculate normal vectors
	VecToAngles( forward, dir );
	AngleVectors( dir, axis[0], axis[1], axis[2] );

	for( i = 0; i < count; i++ )
	{
#ifdef SPIRALRIOT
		CG_FireLead( self, start, axis, spread, spread, &i, &tr );
#else
		CG_FireLead( self, start, axis, spread, spread, &seed, &tr );
#endif
		takedamage = tr.ent && ( cg_entities[tr.ent].current.effects & EF_TAKEDAMAGE );

		if( tr.fraction < 1.0f && !takedamage && !( tr.surfFlags & SURF_NOIMPACT ) )
			impact( &tr );
	}
}

/*
   ==============
   CG_BulletImpact
   ==============
 */
static void CG_BulletImpact( trace_t *tr )
{
	// bullet impact
	CG_BulletExplosion( tr->endpos, tr->plane.normal );

	// spawn decal
	CG_SpawnDecal( tr->endpos, tr->plane.normal, random()*360, 8, 1, 1, 1, 1, 8, 1, qfalse, CG_MediaShader( cgs.media.shaderBulletMark ) );

	// throw particles on dust
	if( tr->surfFlags & SURF_DUST )
		CG_ParticleEffect( tr->endpos, tr->plane.normal, 0.30f, 0.30f, 0.25f, 20 );

	// impact sound
	trap_S_StartFixedSound( CG_MediaSfx( cgs.media.sfxRic[rand()&2] ), tr->endpos, CHAN_AUTO, cg_volume_effects->value,
	                        ATTN_NORM );
}

/*
   ==============
   CG_RiotGunImpactSound
   ==============
 */
static void CG_RiotGunImpactSound( int self, vec3_t start, vec3_t dir, int count )
{
	trace_t	trace;
	vec3_t end;

	VectorMA( start, 8192, dir, end );
	CG_Trace( &trace, start, vec3_origin, vec3_origin, end, self, MASK_SHOT );
	if( trace.fraction < 1.0f && !( trace.surfFlags & SURF_NOIMPACT ) )
	{
		if( count > 20 )
		{
			trap_S_StartFixedSound( CG_MediaSfx( cgs.media.sfxRiotgunStrongHit ), trace.endpos, CHAN_AUTO,
			                        cg_volume_effects->value, ATTN_NORM );
		}
		else
		{
			trap_S_StartFixedSound( CG_MediaSfx( cgs.media.sfxRiotgunWeakHit ), trace.endpos, CHAN_AUTO,
			                        cg_volume_effects->value, ATTN_NORM );
		}
	}
}

/*
   ==============
   CG_RiotgunStrongImpact
   ==============
 */
static void CG_RiotgunStrongImpact( trace_t *tr ) //, int impactnum )
{
	// bullet impact
	CG_BulletExplosion( tr->endpos, tr->plane.normal );

	// throw particles on dust
	if( tr->surfFlags & SURF_DUST )
	{
		CG_ParticleEffect( tr->endpos, tr->plane.normal, 0.30f, 0.30f, 0.25f, 20 );
	}

	// spawn decal
	CG_SpawnDecal( tr->endpos, tr->plane.normal, random()*360, 8, 1, 1, 1, 1, 8, 1, qfalse, CG_MediaShader( cgs.media.shaderBulletMark ) );

	//if ( !impactnum )
	//	trap_S_StartSound ( tr->endpos, 0, 0, CG_MediaSfx (cgs.media.sfxRiotgunStrongHit), cg_volume_effects->value, ATTN_NORM, 0 );
}




//==================================================================

//=========================================================
#define CG_MAX_ANNOUNCER_EVENTS	32
#define CG_MAX_ANNOUNCER_EVENTS_MASK ( CG_MAX_ANNOUNCER_EVENTS - 1 )
#define CG_ANNOUNCER_EVENTS_FRAMETIME 1.5f // the announcer will speak each 1.5 seconds
typedef struct cg_announcerevent_s
{
	int soundindex;
} cg_announcerevent_t;
cg_announcerevent_t cg_announcerEvents[CG_MAX_ANNOUNCER_EVENTS];
static int cg_announcerEventsCurrent = 0;
static int cg_announcerEventsHead = 0;
static float cg_announcerEventsDelay = 0.0f;

//=================
//G_ClearAnnouncerEvents
//=================
void CG_ClearAnnouncerEvents( void )
{
	cg_announcerEventsCurrent = cg_announcerEventsHead = 0;
}

//=================
//G_AddAnnouncerEvent
//=================
static void CG_AddAnnouncerEvent( int soundindex )
{
	if( cg_announcerEventsCurrent + CG_MAX_ANNOUNCER_EVENTS >= cg_announcerEventsHead )
	{
		// full buffer (we do nothing, just let it overwrite the oldest
	}

	// add it
	cg_announcerEvents[cg_announcerEventsHead&CG_MAX_ANNOUNCER_EVENTS_MASK].soundindex = soundindex;
	cg_announcerEventsHead++;
}

//=================
//G_ReleaseAnnouncerEvents
//=================
void CG_ReleaseAnnouncerEvents( void )
{
	// see if enough time has passed
	cg_announcerEventsDelay -= cg.realFrameTime;
	if( cg_announcerEventsDelay > 0.0f )
		return;

	if( cg_announcerEventsCurrent < cg_announcerEventsHead )
	{
		// play the event
		int soundindex = cg_announcerEvents[cg_announcerEventsCurrent&CG_MAX_ANNOUNCER_EVENTS_MASK].soundindex;
		trap_S_StartGlobalSound( cgs.soundPrecache[soundindex], CHAN_AUTO, cg_volume_announcer->value );
		cg_announcerEventsCurrent++;
		cg_announcerEventsDelay = CG_ANNOUNCER_EVENTS_FRAMETIME; // wait
	}
	else
	{
		cg_announcerEventsDelay = 0; // no wait
	}
}

//==================================================================

//==================================================================

//=================
//CG_Event_Fall
//=================
void CG_Event_Fall( entity_state_t *state, int parm )
{
	if( !cgs.serverRules.falldamage )
		parm = 0;

	if( ISVIEWERENTITY( state->number ) )
	{
		CG_ViewWeapon_StartFallKickEff( parm );
		if( cg.frame.playerState.pmove.pm_type == PM_GIB || cg.frame.playerState.pmove.pm_type == PM_DEAD )
		{
			CG_SexedSound( state->number, CHAN_BODY, "*fall_0", cg_volume_players->value );
			return;
		}
	}

	CG_FallEffect( state->origin, state->ownerNum );

	if( parm > 10 )
	{
		CG_SexedSound( state->number, CHAN_BODY, "*fall_2", cg_volume_players->value );
		switch( (int)brandom( 0, 3 ) )
		{
		case 0:
			CG_AddPModelAnimation( state->number, 0, TORSO_PAIN1, 0, EVENT_CHANNEL );
			break;
		case 1:
			CG_AddPModelAnimation( state->number, 0, TORSO_PAIN2, 0, EVENT_CHANNEL );
			break;
		case 2:
		default:
			CG_AddPModelAnimation( state->number, 0, TORSO_PAIN3, 0, EVENT_CHANNEL );
			break;
		}
	}
	else if( parm > 0 )
	{
		CG_SexedSound( state->number, CHAN_BODY, "*fall_1", cg_volume_players->value );
	}
	else
		CG_SexedSound( state->number, CHAN_BODY, "*fall_0", cg_volume_players->value );
}

//=================
//CG_Event_Pain
//=================
void CG_Event_Pain( entity_state_t *state, int parm )
{
	if( parm == PAIN_WARSHELL )
	{
		if( ISVIEWERENTITY( state->number ) )
			trap_S_StartGlobalSound( CG_MediaSfx( cgs.media.sfxShellHit ), CHAN_PAIN, cg_volume_players->value );
		else
			trap_S_StartRelativeSound( CG_MediaSfx( cgs.media.sfxShellHit ), state->number, CHAN_PAIN, cg_volume_players->value, ATTN_NORM );
	}
	else
	{
		CG_SexedSound( state->number, CHAN_PAIN, va( S_PLAYER_PAINS, 25*( parm+1 ) ), cg_volume_players->value );
	}

	switch( (int)brandom( 0, 3 ) )
	{
	case 0:
		CG_AddPModelAnimation( state->number, 0, TORSO_PAIN1, 0, EVENT_CHANNEL );
		break;
	case 1:
		CG_AddPModelAnimation( state->number, 0, TORSO_PAIN2, 0, EVENT_CHANNEL );
		break;
	case 2:
	default:
		CG_AddPModelAnimation( state->number, 0, TORSO_PAIN3, 0, EVENT_CHANNEL );
		break;
	}
}

//=================
//CG_Event_Die
//=================
void CG_Event_Die( entity_state_t *state, int parm )
{
	CG_SexedSound( state->number, CHAN_PAIN, S_PLAYER_DEATH, cg_volume_players->value );
	switch( parm )
	{
	case 0:
	default:
		CG_AddPModelAnimation( state->number, BOTH_DEATH1, BOTH_DEATH1, ANIM_NONE, EVENT_CHANNEL );
		break;
	case 1:
		CG_AddPModelAnimation( state->number, BOTH_DEATH2, BOTH_DEATH2, ANIM_NONE, EVENT_CHANNEL );
		break;
	case 2:
		CG_AddPModelAnimation( state->number, BOTH_DEATH3, BOTH_DEATH3, ANIM_NONE, EVENT_CHANNEL );
		break;
	}
}

//=================
//CG_Event_WeaponUp
//=================
void CG_Event_WeaponUp( entity_state_t *state, int parm )
{
	cgs_media_handle_t *sound = NULL;

	CG_AddPModelAnimation( state->number, 0, TORSO_WEAP_UP, 0, EVENT_CHANNEL );

	if( ISVIEWERENTITY( state->number ) )
	{
		vweap.newAnim = VWEAP_WEAPONUP;
	}

	if( parm == 1 )
		sound = cgs.media.sfxWeaponUp;
	else if( parm == 2 )
		sound = cgs.media.sfxWeaponUpNoAmmo;

	if( sound )
	{
		if( ISVIEWERENTITY( state->number ) )
		{
			trap_S_StartGlobalSound( CG_MediaSfx( sound ), CHAN_AUTO, cg_volume_effects->value );
		}
		else
		{
			trap_S_StartFixedSound( CG_MediaSfx( sound ), state->origin, CHAN_AUTO, cg_volume_effects->value, ATTN_NORM );
		}
	}
}

void CG_Event_WeaponDown( entity_state_t *state, int parm )
{
	if( ISVIEWERENTITY( state->number ) )
	{
		if( vweap.currentAnim < VWEAP_WEAPDOWN && vweap.newAnim < VWEAP_WEAPDOWN )
		{
			vweap.newAnim = VWEAP_WEAPDOWN;
		}
	}
	if( cg_entPModels[state->number].anim.current[UPPER] != TORSO_WEAP_UP )
		CG_AddPModelAnimation( state->number, 0, TORSO_WEAP_DOWN, 0, EVENT_CHANNEL );
}

//=================
//CG_Event_Jump
//=================
void CG_Event_Dash( entity_state_t *state, int parm )
{
	switch( parm )
	{
	default:
		break;
	case 0: // dash front
		CG_AddPModelAnimation( state->number, LEGS_DASH, 0, 0, EVENT_CHANNEL );
		CG_SexedSound( state->number, CHAN_VOICE, va( S_PLAYER_DASH_1_to_2, ( rand()&1 )+1 ), cg_volume_players->value );
		break;
	case 1: // dash left
		CG_AddPModelAnimation( state->number, LEGS_DASH_LEFT, 0, 0, EVENT_CHANNEL );
		CG_SexedSound( state->number, CHAN_VOICE, va( S_PLAYER_DASH_1_to_2, ( rand()&1 )+1 ), cg_volume_players->value );
		break;
	case 2: // dash right
		CG_AddPModelAnimation( state->number, LEGS_DASH_RIGHT, 0, 0, EVENT_CHANNEL );
		CG_SexedSound( state->number, CHAN_VOICE, va( S_PLAYER_DASH_1_to_2, ( rand()&1 )+1 ), cg_volume_players->value );
		break;
	case 3: // dash back
		CG_AddPModelAnimation( state->number, LEGS_DASH_BACK, 0, 0, EVENT_CHANNEL );
		CG_SexedSound( state->number, CHAN_VOICE, va( S_PLAYER_DASH_1_to_2, ( rand()&1 )+1 ), cg_volume_players->value );
		break;
	}
	CG_Dash( state ); // Dash smoke effect
	// since most dash animations jump with right leg, reset the jump to start with left leg after a dash
	cg_entities[state->number].jumpedLeft = qtrue;
}

//=================
//CG_Event_Jump
//=================
void CG_Event_WallJump( entity_state_t *state, int parm )
{
	vec3_t dir;
	// Extract the orientation of the wall surface jumped from
	ByteToDir( ( parm & 0xFF ), dir );
	// Restore the parameter to an expected value :)
	parm = ( parm >> 8 ) & 0xFF;
	switch( parm )
	{
	default:
		break;
	case 0: // wall-jump front
		CG_AddPModelAnimation( state->number, LEGS_WALLJUMP, 0, 0, EVENT_CHANNEL );
		CG_SexedSound( state->number, CHAN_VOICE, va( S_PLAYER_WALLJUMP_1_to_2, ( rand()&1 )+1 ), cg_volume_players->value );
		break;
	case 1: // wall-jump left
		CG_AddPModelAnimation( state->number, LEGS_WALLJUMP_LEFT, 0, 0, EVENT_CHANNEL );
		CG_SexedSound( state->number, CHAN_VOICE, va( S_PLAYER_WALLJUMP_1_to_2, ( rand()&1 )+1 ), cg_volume_players->value );
		break;
	case 2: // wall-jump right
		CG_AddPModelAnimation( state->number, LEGS_WALLJUMP_RIGHT, 0, 0, EVENT_CHANNEL );
		CG_SexedSound( state->number, CHAN_VOICE, va( S_PLAYER_WALLJUMP_1_to_2, ( rand()&1 )+1 ), cg_volume_players->value );
		break;
	case 3: // wall-jump back
		CG_AddPModelAnimation( state->number, LEGS_WALLJUMP_BACK, 0, 0, EVENT_CHANNEL );
		CG_SexedSound( state->number, CHAN_VOICE, va( S_PLAYER_WALLJUMP_1_to_2, ( rand()&1 )+1 ), cg_volume_players->value );
		break;
	}
	CG_WalljumpEffect( state, dir ); // Walljump smoke effect
}

//=================
//CG_Event_DoubleJump - just adds the sound and doesn't modify the animation
//=================
void CG_Event_DoubleJump( entity_state_t *state, int parm )
{
	CG_SexedSound( state->number, CHAN_VOICE, va( S_PLAYER_JUMP_1_to_2, ( rand()&1 )+1 ), cg_volume_players->value );
}

//=================
//CG_Event_Jump
//=================
void CG_Event_Jump( entity_state_t *state, int parm )
{
#define MOVEDIREPSILON 0.25f
	centity_t *cent;
	int xyspeedcheck;

	cent = &cg_entities[state->number];
	xyspeedcheck = SQRTFAST( cent->animVelocity[0]*cent->animVelocity[0] + cent->animVelocity[1]*cent->animVelocity[1] );
	if( xyspeedcheck < 100 )
	{                      // the player is jumping on the same place, not running
		CG_AddPModelAnimation( state->number, LEGS_JUMP3, 0, 0, EVENT_CHANNEL );
		CG_SexedSound( state->number, CHAN_VOICE, va( S_PLAYER_JUMP_1_to_2, ( rand()&1 )+1 ), cg_volume_players->value );
	}
	else
	{
		vec3_t movedir, viewaxis[3];
		movedir[0] = cent->animVelocity[0];
		movedir[1] = cent->animVelocity[1];
		movedir[2] = 0;
		VectorNormalizeFast( movedir );
		AngleVectors( tv( 0, cent->current.angles[YAW], 0 ), viewaxis[FORWARD], viewaxis[RIGHT], viewaxis[UP] );
		// see what's his relative movement direction
		if( DotProduct( movedir, viewaxis[FORWARD] ) > MOVEDIREPSILON )
		{
			cent->jumpedLeft = !cent->jumpedLeft;
			if( !cent->jumpedLeft )
			{
				CG_AddPModelAnimation( state->number, LEGS_JUMP2, 0, 0, EVENT_CHANNEL );
				CG_SexedSound( state->number, CHAN_VOICE, va( S_PLAYER_JUMP_1_to_2, ( rand()&1 )+1 ), cg_volume_players->value );
			}
			else
			{
				CG_AddPModelAnimation( state->number, LEGS_JUMP1, 0, 0, EVENT_CHANNEL );
				CG_SexedSound( state->number, CHAN_VOICE, va( S_PLAYER_JUMP_1_to_2, ( rand()&1 )+1 ), cg_volume_players->value );
			}
		}
		else
		{
			CG_AddPModelAnimation( state->number, LEGS_JUMP3, 0, 0, EVENT_CHANNEL );
			CG_SexedSound( state->number, CHAN_VOICE, va( S_PLAYER_JUMP_1_to_2, ( rand()&1 )+1 ), cg_volume_players->value );
		}
	}
#undef MOVEDIREPSILON
}

/*
   ==============
   CG_EntityEvent

   An entity has just been parsed that has an event value
   ==============
 */
void CG_EntityEvent( entity_state_t *ent, int ev, int parm, qboolean predicted )
{
	static orientation_t projection;
	vec3_t dir;
	qboolean viewer = ISVIEWERENTITY( ent->number );
	vec4_t color;

	if( viewer && ( ev >= PREDICTABLE_EVENTS_START ) && ( predicted != cg.view.playerPrediction ) )
		return;

	switch( ev )
	{
	case EV_NONE:
	default:
		break;

	case EV_SEXEDSOUND:
		if( parm == 2 )
			CG_SexedSound( ent->number, CHAN_PAIN, S_PLAYER_GASP, cg_volume_players->value );
		else if( parm == 1 )
			CG_SexedSound( ent->number, CHAN_PAIN, S_PLAYER_DROWN, cg_volume_players->value );
		break;

	case EV_PAIN:
		CG_Event_Pain( ent, parm );
		break;

	case EV_DIE:
		CG_Event_Die( ent, parm );
		break;

	case EV_GIB:
		break;

	case EV_EXPLOSION1:
		CG_Explosion1( ent->origin );
		break;

	case EV_EXPLOSION2:
		CG_Explosion2( ent->origin );
		break;

	case EV_GREEN_LASER:
		CG_GreenLaser( ent->origin, ent->origin2 );
		break;

	case EV_PNODE:
		color[0] = COLOR_R( ent->colorRGBA ) * ( 1.0 / 255.0 );
		color[1] = COLOR_G( ent->colorRGBA ) * ( 1.0 / 255.0 );
		color[2] = COLOR_B( ent->colorRGBA ) * ( 1.0 / 255.0 );
		color[3] = COLOR_A( ent->colorRGBA ) * ( 1.0 / 255.0 );
		CG_PLink( ent->origin, ent->origin2, color, parm );
		break;

	case EV_BLOOD:
		ByteToDir( parm, dir );
		CG_ParticleEffect( ent->origin, dir, 0.61f, 0.1f, 0.0f, 60 );
		break;

	case EV_SPARKS:
		ByteToDir( parm, dir );
		CG_ParticleEffect( ent->origin, dir, 1.0f, 0.67f, 0.0f, 6 );
		break;

	case EV_BULLET_SPARKS:
		ByteToDir( parm, dir );
		CG_BulletExplosion( ent->origin, dir );
		CG_ParticleEffect( ent->origin, dir, 1.0f, 0.67f, 0.0f, 6 );
		trap_S_StartFixedSound( CG_MediaSfx( cgs.media.sfxRic[rand()&2] ), ent->origin, CHAN_AUTO,
		                        cg_volume_effects->value, ATTN_IDLE );
		break;

	case EV_LASER_SPARKS:
		ByteToDir( parm, dir );
		CG_ParticleEffect2( ent->origin, dir,
		                    COLOR_R( ent->colorRGBA ) * ( 1.0 / 255.0 ),
		                    COLOR_G( ent->colorRGBA ) * ( 1.0 / 255.0 ),
		                    COLOR_B( ent->colorRGBA ) * ( 1.0 / 255.0 ),
		                    ent->eventCount );
		break;

	case EV_GESTURE:
		CG_SexedSound( ent->number, CHAN_VOICE, "*taunt", cg_volume_players->value );
		CG_AddPModelAnimation( ent->number, 0, TORSO_TAUNT, 0, EVENT_CHANNEL );
		break;

	case EV_DROP:
		CG_AddPModelAnimation( ent->number, 0, TORSO_DROP, 0, EVENT_CHANNEL );
		break;

	case EV_WEAPONDOWN:
		CG_Event_WeaponDown( ent, parm );
		break;

	case EV_WEAPONUP:
		CG_Event_WeaponUp( ent, parm );
		break;

	case EV_SPOG:
		CG_SmallPileOfGibs( ent->origin, parm, ent->origin2 );
		break;

	case EV_ITEM_RESPAWN:
		cg_entities[ent->number].respawnTime = cg.time;
		cg_entities[ent->number].respawnIndicator = 0;
		if( parm == POWERUP_QUAD )
		{
			trap_S_StartRelativeSound( CG_MediaSfx( cgs.media.sfxItemQuadRespawn ), ent->number, CHAN_AUTO,
			                           cg_volume_effects->value, ATTN_IDLE );
		}
		else if( parm == POWERUP_SHELL )
		{
			trap_S_StartRelativeSound( CG_MediaSfx( cgs.media.sfxItemWarShellRespawn ), ent->number, CHAN_AUTO,
			                           cg_volume_effects->value, ATTN_IDLE );
		}
		else
		{
			trap_S_StartRelativeSound( CG_MediaSfx( cgs.media.sfxItemRespawn ), ent->number, CHAN_AUTO,
			                           cg_volume_effects->value, ATTN_IDLE );
		}
		break;


	case EV_PLAYER_TELEPORT_IN:
		if( ISVIEWERENTITY( ent->ownerNum ) )
		{
			trap_S_StartGlobalSound( CG_MediaSfx( cgs.media.sfxTeleportIn ), CHAN_AUTO,
			                         cg_volume_effects->value );
		}
		else
		{
			trap_S_StartFixedSound( CG_MediaSfx( cgs.media.sfxTeleportIn ), ent->origin, CHAN_AUTO,
			                        cg_volume_effects->value, ATTN_NORM );
		}
		if( ent->ownerNum && ent->ownerNum < MAX_CLIENTS )
		{
			cg_entities[ent->ownerNum].localEffects[LOCALEFFECT_EV_PLAYER_TELEPORT_IN] = cg.time;
		}
		break;

	case EV_PLAYER_TELEPORT_OUT:
		if( ISVIEWERENTITY( ent->ownerNum ) )
		{
			trap_S_StartGlobalSound( CG_MediaSfx( cgs.media.sfxTeleportOut ), CHAN_AUTO,
			                         cg_volume_effects->value );
		}
		else
		{
			trap_S_StartFixedSound( CG_MediaSfx( cgs.media.sfxTeleportOut ), ent->origin, CHAN_AUTO,
			                        cg_volume_effects->value, ATTN_NORM );
		}
		if( ent->ownerNum && ent->ownerNum < MAX_CLIENTS )
		{
			cg_entities[ent->ownerNum].localEffects[LOCALEFFECT_EV_PLAYER_TELEPORT_OUT] = cg.time;
			VectorCopy( ent->origin, cg_entities[ent->ownerNum].teleportedFrom );
		}
		break;

	case EV_MUZZLEFLASH: //parm is fire mode
		CG_PlayerMuzzleFlash( ent->number, parm );
		break;

	case EV_ELECTROTRAIL:
		if( !CG_TeamBeamColor( cg_entities[ent->ownerNum].current.team, ent->ownerNum, color ) )
		{
			color[0] = 1.0f;
			color[1] = 1.0f;
			color[2] = 1.0f;
			color[3] = 0.8f;
		}
		if( CG_PModel_GetProjectionSource( ent->ownerNum, &projection ) )
			CG_ElectroTrail2( projection.origin, ent->origin2, parm, color );
		else
			CG_ElectroTrail2( ent->origin, ent->origin2, parm, color );
		break;

	case EV_INSTATRAIL:
		if( !CG_TeamBeamColor( cg_entities[ent->ownerNum].current.team, ent->ownerNum, color ) )
		{
			color[0] = 1.0f;
			color[1] = 0.0f;
			color[2] = 0.0f;
			color[3] = 0.8f;
		}

		if( CG_PModel_GetProjectionSource( ent->ownerNum, &projection ) )
			CG_InstaPolyBeam( projection.origin, ent->origin2, parm, color );
		else
			CG_InstaPolyBeam( ent->origin, ent->origin2, parm, color );
		break;

	case EV_PLASMA_EXPLOSION:
		ByteToDir( parm, dir );
		CG_PlasmaExplosion( ent->origin, dir, ent->firemode, (float)ent->weapon * 8.0f );
		if( ent->firemode == FIRE_MODE_STRONG )
		{
			trap_S_StartFixedSound( CG_MediaSfx( cgs.media.sfxPlasmaStrongHit ), ent->origin, CHAN_AUTO, cg_volume_effects->value, ATTN_IDLE );
			CG_StartKickAnglesEffect( ent->origin, 50, ent->weapon * 8, 100 );
		}
		else
		{
			trap_S_StartFixedSound( CG_MediaSfx( cgs.media.sfxPlasmaWeakHit ), ent->origin, CHAN_AUTO, cg_volume_effects->value, ATTN_IDLE );
			CG_StartKickAnglesEffect( ent->origin, 30, ent->weapon * 8, 75 );
		}
		break;

	case EV_BOLT_EXPLOSION:
		ByteToDir( parm, dir );
		CG_BoltExplosionMode( ent->origin, dir, ent->firemode );
		break;

	case EV_INSTA_EXPLOSION:
		ByteToDir( parm, dir );
		CG_InstaExplosionMode( ent->origin, dir, ent->firemode );
		break;

	case EV_GRENADE_EXPLOSION:
		if( parm )
		{    // we have a direction
			ByteToDir( parm, dir );
			CG_GrenadeExplosionMode( ent->origin, dir, ent->firemode, (float)ent->weapon*8.0f );
		}
		else
		{ // no direction
			CG_GrenadeExplosionMode( ent->origin, vec3_origin, ent->firemode, (float)ent->weapon*8.0f );
		}

		if( ent->firemode == FIRE_MODE_STRONG )
		{
			trap_S_StartFixedSound( CG_MediaSfx( cgs.media.sfxGrenadeStrongExplosion ), ent->origin, CHAN_AUTO, cg_volume_effects->value, ATTN_DISTANT );
			CG_StartKickAnglesEffect( ent->origin, 135, ent->weapon*8, 325 );
		}
		else
		{
			trap_S_StartFixedSound( CG_MediaSfx( cgs.media.sfxGrenadeWeakExplosion ), ent->origin, CHAN_AUTO, cg_volume_effects->value, ATTN_DISTANT );
			CG_StartKickAnglesEffect( ent->origin, 125, ent->weapon*8, 300 );
		}
		break;

	case EV_ROCKET_EXPLOSION:
		ByteToDir( parm, dir );
		CG_RocketExplosionMode( ent->origin, dir, ent->firemode, (float)ent->weapon * 8.0f );
		if( ent->firemode == FIRE_MODE_STRONG )
		{
			CG_StartKickAnglesEffect( ent->origin, 135, ent->weapon * 8, 300 );
			trap_S_StartFixedSound( CG_MediaSfx( cgs.media.sfxRocketLauncherStrongHit ), ent->origin, CHAN_AUTO, cg_volume_effects->value, ATTN_DISTANT );
		}
		else
		{
			CG_StartKickAnglesEffect( ent->origin, 125, ent->weapon * 8, 275 );
			trap_S_StartFixedSound( CG_MediaSfx( cgs.media.sfxRocketLauncherWeakHit ), ent->origin, CHAN_AUTO, cg_volume_effects->value, ATTN_DISTANT );
		}
		break;

	case EV_FIRE_RIOTGUN:
		//spreads come inside the free shorts I found: //skinnum = spread
		CG_FireBullet( ent->ownerNum, ent->origin, ent->origin2, ent->eventCount, ent->skinnum, parm, CG_RiotgunStrongImpact );
		//spawn a single sound in the impact
		CG_RiotGunImpactSound( ent->ownerNum, ent->origin, ent->origin2, ent->eventCount );
		break;

	case EV_FIRE_BULLET:
		CG_FireBullet( ent->ownerNum, ent->origin, ent->origin2, 1, DEFAULT_BULLET_SPREAD, parm, CG_BulletImpact );
		break;

	case EV_GRENADE_BOUNCE:
		if( parm == FIRE_MODE_STRONG )
		{
			trap_S_StartRelativeSound( CG_MediaSfx( cgs.media.sfxGrenadeStrongBounce[rand()&1] ), ent->number, CHAN_AUTO, cg_volume_effects->value, ATTN_IDLE );
		}
		else
		{
			trap_S_StartRelativeSound( CG_MediaSfx( cgs.media.sfxGrenadeWeakBounce[rand()&1] ), ent->number, CHAN_AUTO, cg_volume_effects->value, ATTN_IDLE );
		}
		break;

	case EV_BLADE_IMPACT:
		CG_BladeImpact( ent->origin, ent->origin2 );
		break;

	case EV_GUNBLADEBLAST_IMPACT:
		ByteToDir( parm, dir );
		CG_GunBladeBlastImpact( ent->origin, dir, (float)ent->weapon*8 );
		if( ent->skinnum > 64 )
		{
			trap_S_StartFixedSound( CG_MediaSfx( cgs.media.sfxGunbladeStrongHit[2] ), ent->origin, CHAN_AUTO,
			                        cg_volume_effects->value, ATTN_DISTANT );
		}
		else if( ent->skinnum > 34 )
		{
			trap_S_StartFixedSound( CG_MediaSfx( cgs.media.sfxGunbladeStrongHit[1] ), ent->origin, CHAN_AUTO,
			                        cg_volume_effects->value, ATTN_NORM );
		}
		else
		{
			trap_S_StartFixedSound( CG_MediaSfx( cgs.media.sfxGunbladeStrongHit[0] ), ent->origin, CHAN_AUTO,
			                        cg_volume_effects->value, ATTN_IDLE );
		}

		//ent->skinnum is knockback value
		CG_StartKickAnglesEffect( ent->origin, ent->skinnum*8, ent->weapon*8, 200 );
		break;

	case EV_BLOOD2:
		CG_UpdateDamageIndicator( ent );
		if( cg_showBloodTrail->integer == 2 && ISVIEWERENTITY( ent->ownerNum ) )
			break;
		ByteToDir( parm, dir );
		CG_BloodDamageEffect( ent->origin, dir, ent->damage );
		break;

	case EV_BLOOD_SAVED:
		break;

		// func movers
	case EV_PLAT_HIT_TOP:
	case EV_PLAT_HIT_BOTTOM:
	case EV_PLAT_START_MOVING:
	case EV_DOOR_HIT_TOP:
	case EV_DOOR_HIT_BOTTOM:
	case EV_DOOR_START_MOVING:
	case EV_BUTTON_FIRE:
	case EV_TRAIN_STOP:
	case EV_TRAIN_START:
	{
		vec3_t so;
		CG_GetEntitySpatilization( ent->number, so, NULL );
		trap_S_StartFixedSound( cgs.soundPrecache[parm], so, CHAN_AUTO, cg_volume_effects->value, ATTN_STATIC );
	}
		break;

	case EV_VSAY:
		CG_StartVoiceTokenEffect( ent->ownerNum, EV_VSAY, parm );
		break;

		//  PREDICTABLE EVENTS

	case EV_DASH:
		CG_Event_Dash( ent, parm );
		break;

	case EV_WALLJUMP:
		CG_Event_WallJump( ent, parm );
		break;

	case EV_DOUBLEJUMP:
		CG_Event_DoubleJump( ent, parm );
		break;

	case EV_JUMP:
		CG_Event_Jump( ent, parm );
		break;

	case EV_JUMP_PAD:
		CG_TouchJumpPad( ent->number );
		break;

	case EV_FALL:
		CG_Event_Fall( ent, parm );
		break;
	}
}

//==================
//CG_FirePlayerStateEvents
//This events are only received by this client, and only affect it.
//==================
void CG_FirePlayerStateEvents( void )
{
	unsigned int event, parm;

	if( !cg.frame.playerState.event )
		return;

	if( cg.view.POVent != (int)cg.frame.playerState.POVnum )
		return;

	cg.damage_given = cg.damage_received = 0;

	// first byte is event number, second is parm
	event = cg.frame.playerState.event & 0xFF;
	parm = ( cg.frame.playerState.event>>8 ) & 0xFF;

	switch( event )
	{
	case PSEV_HIT:
		if( parm < 0 || parm > 6 )
			break;
		if( parm < 4 )
		{        // hit of some caliber
			trap_S_StartGlobalSound( CG_MediaSfx( cgs.media.sfxWeaponHit[parm] ), CHAN_AUTO, cg_volume_hitsound->value );
			cg.damage_given += ( 85 - ( parm*25 ) );
		}
		else if( parm == 4 )
		{                // killed an enemy
			trap_S_StartGlobalSound( CG_MediaSfx( cgs.media.sfxWeaponKill ), CHAN_AUTO, cg_volume_hitsound->value );
		}
		else
		{ // hit a teammate
			trap_S_StartGlobalSound( CG_MediaSfx( cgs.media.sfxWeaponHitTeam ), CHAN_AUTO, cg_volume_hitsound->value );
			if( cg_showhelp->integer )
			{
				if( random() <= 0.5f )
					CG_CenterPrint( "Don't shoot at members of your team!" );
				else
					CG_CenterPrint( "You are shooting at your team-mates!" );
			}
		}
		break;

	case PSEV_PICKUP:
		if( parm > WEAP_NONE && parm <= WEAP_TOTAL && !cgs.demoPlaying &&
		    cg.frame.playerState.pmove.pm_type == PM_NORMAL && cg.oldFrame.playerState.pmove.pm_type == PM_NORMAL )
		{
			CG_WeaponAutoswitch( parm );
		}
		if( cg_pickup_flash->integer && !cg.view.thirdperson )
			CG_StartColorBlendEffect( 1.0f, 1.0f, 1.0f, 0.25f, 150 );
		break;

	case PSEV_DAMAGE_BLEND:
		if( cg_damage_blend->integer && !cg.view.thirdperson )
			CG_StartColorBlendEffect( 1.0f, 0.0f, 0.0f, 0.4f, parm * 10 );
		cg.damage_received += parm;
		break;

	case PSEV_INDEXEDSOUND:
		trap_S_StartGlobalSound( cgs.soundPrecache[parm], CHAN_AUTO, cg_volume_effects->value );
		break;

	case PSEV_NOAMMO:
		if( (int)parm == cg.frame.playerState.stats[STAT_WEAPON_ITEM] )
			CG_NoAmmoWeaponChange();
		break;

	case PSEV_ANNOUNCER:
		trap_S_StartGlobalSound( cgs.soundPrecache[parm], CHAN_AUTO, cg_volume_announcer->value );
		break;

	case PSEV_ANNOUNCER_QUEUED:
		CG_AddAnnouncerEvent( parm );
		break;

	default:
		break;
	}
}
