// Copyright (C) 2001-2002 Raven Software
//
// cg_event.c -- handle entity events at snapshot or playerstate transitions

#include "cg_local.h"
#include "..\ghoul2\g2.h"
#include "../../ui/menudef.h"

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

/*
===================
CG_PlaceString

Also called by scoreboard drawing
===================
*/
const char	*CG_PlaceString( int rank ) {
	static char	str[64];
	char	*s, *t;

	if ( rank & RANK_TIED_FLAG ) {
		rank &= ~RANK_TIED_FLAG;
		t = "Tied for ";
	} else {
		t = "";
	}

	if ( rank == 1 ) {
		s = S_COLOR_BLUE "1st" S_COLOR_WHITE;		// draw in blue
	} else if ( rank == 2 ) {
		s = S_COLOR_RED "2nd" S_COLOR_WHITE;		// draw in red
	} else if ( rank == 3 ) {
		s = S_COLOR_YELLOW "3rd" S_COLOR_WHITE;		// draw in yellow
	} else if ( rank == 11 ) {
		s = "11th";
	} else if ( rank == 12 ) {
		s = "12th";
	} else if ( rank == 13 ) {
		s = "13th";
	} else if ( rank % 10 == 1 ) {
		s = va("%ist", rank);
	} else if ( rank % 10 == 2 ) {
		s = va("%ind", rank);
	} else if ( rank % 10 == 3 ) {
		s = va("%ird", rank);
	} else {
		s = va("%ith", rank);
	}

	Com_sprintf( str, sizeof( str ), "%s%s", t, s );
	return str;
}

/*
=============
CG_GameOver
=============
*/
static void CG_GameOver ( entityState_t *ent )
{
	switch ( ent->eventParm )
	{
		case GAME_OVER_TIMELIMIT:
			Com_sprintf ( cgs.gameover, MAX_QPATH, "Timelimit Hit" );
			break;

		case GAME_OVER_SCORELIMIT:
			if ( cgs.gametypeData->teams )
			{
				switch ( ent->otherEntityNum )
				{
					case TEAM_RED:					
						Com_sprintf ( cgs.gameover, MAX_QPATH, "%s%s^7 Team hit the score limit", cgs.teamData.teamcolor[1], cgs.teamData.redName );
						break;

					case TEAM_BLUE:
						Com_sprintf ( cgs.gameover, MAX_QPATH, "%s%s^7 Team hit the score limit", cgs.teamData.teamcolor[2], cgs.teamData.blueName );
						break;
				}
			}
			else
			{
				Com_sprintf ( cgs.gameover, MAX_QPATH, "%s" S_COLOR_WHITE " hit the score limit", cgs.clientinfo[ent->otherEntityNum].name );
			}
			break;

		default:
			return;
	} 	

	CG_CenterPrint ( cgs.gameover, 0.43f );

	Com_Printf ( "@%s\n", cgs.gameover );
}

/*
=============
CG_Obituary
=============
*/
static void CG_Obituary( entityState_t *ent ) 
{
	int				mod;
	int				target, attacker;
	char			*message;
	char			*message2;
	const char		*targetInfo;
	const char		*attackerInfo;
	char			targetName[32];
	char			attackerName[32];
	const char		*targetColor;
	const char		*attackerColor;
	attackType_t	attack;
	gender_t		gender;
	clientInfo_t	*ci;
	int				shotlocation;
	char			*shotstr;
	char			*genderstr;
	char			*bodypartstr;

	target		= ent->otherEntityNum;
	attacker	= ent->otherEntityNum2;
	mod			= ent->eventParm & 0xFF;
	attack		= (ent->eventParm >> 8) & 0xFF;
	attackerColor = S_COLOR_WHITE;
	targetColor   = S_COLOR_WHITE;

	shotlocation = ent->time & (~HL_DISMEMBERBIT);
	if ( cgs.gametypeData->teams && cgs.clientinfo[target].team == cgs.clientinfo[attacker].team )
	{
		shotstr = "^1[TEAMKILL]";
	}
	else if ( shotlocation == HL_HEAD && mod != MOD_ADMIN_STRIKE && mod != MOD_ADMIN_FRY && mod != MOD_ADMIN_EXPLODE && mod != MOD_ADMIN_TELEFRAG && mod != MOD_ADMIN_SLAP && mod != MOD_TELEFRAG && mod != MOD_SMOHG92_GRENADE && mod != (MOD_M4_ASSAULT_RIFLE|(ATTACK_ALTERNATE << 8)) )
	{
		shotstr = "^3[HEADSHOT]";
	}
	else
	{
		shotstr = "";
	}

	if ( target < 0 || target >= MAX_CLIENTS ) 
	{
		Com_Error( ERR_FATAL, "CG_Obituary: target out of range" );
	}

	// Play the death sound, water if they drowned
	if ( mod == MOD_WATER )
	{
		trap_S_StartSound ( NULL, target, CHAN_AUTO, cgs.media.drownDeathSound, -1, -1 );
	}
	else
	{
		trap_S_StartSound( NULL, target, CHAN_VOICE, CG_CustomPlayerSound(target, SOUND_DIE_1 + (cg.time%3)), -1, -1);
	}

	// Play the frag sound, and make sure its not played more than every 250ms
	if ( cg.time - cg.lastKillTime > 250 && attacker == cg.snap->ps.clientNum )
	{
		if ( cg_soundFrag.integer )
		{
			// If the attacker killed themselves play the selffrag sound
			if ( attacker == target )
			{
				trap_S_StartLocalSound ( cgs.media.fragSelfSound, CHAN_AUTO );
			}
			else
			{
				// In a team game a kill of a teammate will play the self frag sound rather 
				// than the frag sound
				if ( cgs.gametypeData->teams )
				{
					if ( cgs.clientinfo[target].team == cgs.clientinfo[attacker].team )
					{
						trap_S_StartLocalSound ( cgs.media.fragSelfSound, CHAN_AUTO );
					}
					else
					{
						trap_S_StartLocalSound ( cgs.media.fragSound, CHAN_AUTO );
					}
				}
				else
				{
					trap_S_StartLocalSound ( cgs.media.fragSound, CHAN_AUTO );
				}
			}
		}

		cg.lastKillTime = cg.time;
	}			

	ci = &cgs.clientinfo[target];

	if ( attacker < 0 || attacker >= MAX_CLIENTS ) 
	{
		attacker = ENTITYNUM_WORLD;
		attackerInfo = NULL;
	} 
	else 
	{
		attackerInfo = CG_ConfigString( CS_PLAYERS + attacker );
	}

	targetInfo = CG_ConfigString( CS_PLAYERS + target );
	if ( !targetInfo ) {
		return;
	}
	Q_strncpyz( targetName, Info_ValueForKey( targetInfo, "n" ), sizeof(targetName) - 2);
	strcat( targetName, S_COLOR_WHITE );

	switch ( cgs.clientinfo[target].team )
	{
		case TEAM_RED:
			targetColor = cgs.teamData.teamcolor[1];
			break;

		case TEAM_BLUE:
			targetColor = cgs.teamData.teamcolor[2];
			break;
	}

	message2 = "";

	// check for single client messages

	gender = ci->gender;

	switch( mod ) 
	{
		case MOD_SUICIDE:
			message = "suicides";
			break;
		case MOD_FALLING:
			if ( gender == GENDER_FEMALE )
				message = "fell to her death";
			else
				message = "fell to his death";
			break;
		case MOD_CRUSH:
			message = "was squished";
			break;
		case MOD_WATER:
			message = "sank like a rock";
			break;
		case MOD_TARGET_LASER:
			message = "saw the light";
			break;
		case MOD_TRIGGER_HURT:
		case MOD_TRIGGER_HURT_NOSUICIDE:
			message = "was in the wrong place";
			break;
		case MOD_TEAMCHANGE:
		case MOD_ADMIN_STRIKE:
		case MOD_ADMIN_SLAP:
		case MOD_ADMIN_FRY:
		case MOD_ADMIN_EXPLODE:
		case MOD_ADMIN_TELEFRAG:
			return;

		default:
			message = NULL;
			break;
	}

	// Attacker killed themselves.  Ridicule them for it.
	if (attacker == target) 
	{
		switch (mod) 
		{
			case MOD_MM1_GRENADE_LAUNCHER:    
			case MOD_RPG7_LAUNCHER:           
			case MOD_M84_GRENADE:
			case MOD_SMOHG92_GRENADE:
			case MOD_ANM14_GRENADE:
			case MOD_M15_GRENADE:
				if ( gender == GENDER_FEMALE )
					message = "blew herself up";
				else if ( gender == GENDER_NEUTER )
					message = "blew itself up";
				else
					message = "blew himself up";
				break;

			default:
				if ( gender == GENDER_FEMALE )
					message = "killed herself";
				else if ( gender == GENDER_NEUTER )
					message = "killed itself";
				else
					message = "killed himself";
				break;
		}
	}

	if (message) 
	{
		Com_Printf( "%s%s %s.\n", targetColor, targetName, message);
		return;
	}

	// check for kill messages from the current clientNum when
	// not in a team game.
	if ( cgs.gametypeData->showKills )
	{
		if ( attacker == cg.snap->ps.clientNum ) 
		{
			char	*s;

			if ( !cgs.gametypeData->teams ) 
			{
				s = va("You killed %s%s\n%s place with %i\n%s", targetColor, targetName, 
					CG_PlaceString( cg.snap->ps.persistant[PERS_RANK] + 1 ),
					cg.snap->ps.persistant[PERS_SCORE],
					cg.centerPrintPriority?cg.centerPrint:"" );
			} 
			else 
			{
				s = va("You killed %s%s\n%s", targetColor, targetName,
					cg.centerPrintPriority?cg.centerPrint:"" );
			}

			CG_CenterPrint( s, 0.43f );
			cg.centerPrintPriority = qfalse;
		}
	}


	// check for double client messages
	if ( !attackerInfo ) 
	{
		attacker = ENTITYNUM_WORLD;
		strcpy( attackerName, "noname" );
	} 
	else 
	{
		Q_strncpyz( attackerName, Info_ValueForKey( attackerInfo, "n" ), sizeof(attackerName) - 2);
		strcat( attackerName, S_COLOR_WHITE );
		// check for kill messages about the current clientNum
		if ( target == cg.snap->ps.clientNum ) 
		{
			Q_strncpyz( cg.killerName, attackerName, sizeof( cg.killerName ) );
		}

		switch ( cgs.clientinfo[attacker].team )
		{
			case TEAM_RED:
				attackerColor = cgs.teamData.teamcolor[1];
				break;

			case TEAM_BLUE:
				attackerColor = cgs.teamData.teamcolor[2];
				break;
		}
	}

			
	if ( attacker != ENTITYNUM_WORLD ) 
	{
		bodypartstr = "";
		if ( cg_drawDetailedObituary.integer == 1 )
		{
			switch ( shotlocation )
			{
				case HL_FOOT_RT:
				case HL_FOOT_LT:
					bodypartstr = "in the foot ";
					break;
				case HL_LEG_UPPER_RT:
				case HL_LEG_UPPER_LT:
				case HL_LEG_LOWER_RT:
				case HL_LEG_LOWER_LT:
					bodypartstr = "in the leg ";
					break;
				case HL_HAND_RT:
				case HL_HAND_LT:
					bodypartstr = "in the hand ";
					break;
				case HL_ARM_RT:
				case HL_ARM_LT:
					bodypartstr = "in the arm ";
					break;
				case HL_HEAD:
					bodypartstr = "in the head ";
					break;
				case HL_WAIST:
					bodypartstr = "in the waist ";
					break;
				case HL_BACK_RT:
				case HL_BACK_LT:
				case HL_BACK:
					bodypartstr = "in the back ";
					break;
				case HL_CHEST_RT:
				case HL_CHEST_LT:
				case HL_CHEST:
					bodypartstr = "in the chest ";
					break;
				case HL_NECK:
					bodypartstr = "in the neck ";
					break;
				default:
					break;
			}
		}
		else if ( cg_drawDetailedObituary.integer == 2 )
		{
			switch ( shotlocation )
			{
				case HL_FOOT_RT:
					bodypartstr = "in the right foot ";
					break;
				case HL_FOOT_LT:
					bodypartstr = "in the left foot ";
					break;
				case HL_LEG_UPPER_RT:
					bodypartstr = "in the right thigh ";
					break;
				case HL_LEG_UPPER_LT:
					bodypartstr = "in the left thigh ";
					break;
				case HL_LEG_LOWER_RT:
					bodypartstr = "in the right calf ";
					break;
				case HL_LEG_LOWER_LT:
					bodypartstr = "in the left calf ";
					break;
				case HL_HAND_RT:
					bodypartstr = "in the right hand ";
					break;
				case HL_HAND_LT:
					bodypartstr = "in the left hand ";
					break;
				case HL_ARM_RT:
					bodypartstr = "in the right arm ";
					break;
				case HL_ARM_LT:
					bodypartstr = "in the left arm ";
					break;
				case HL_HEAD:
					bodypartstr = "in the head ";
					break;
				case HL_WAIST:
					bodypartstr = "in the waist ";
					break;
				case HL_BACK_RT:
					bodypartstr = "in the right back ";
					break;
				case HL_BACK_LT:
					bodypartstr = "in the left back ";
					break;
				case HL_BACK:
					bodypartstr = "in the back ";
					break;
				case HL_CHEST_RT:
					bodypartstr = "in the right chest ";
					break;
				case HL_CHEST_LT:
					bodypartstr = "in the left chest ";
					break;
				case HL_CHEST:
					bodypartstr = "in the chest ";
					break;
				case HL_NECK:
					bodypartstr = "in the neck ";
					break;
				default:
					break;
			}
		}

		switch (mod) 
		{
			case MOD_KNIFE:
				message = va("was sliced %sby", bodypartstr);
				break;

			case MOD_USAS_12_SHOTGUN:
			case MOD_M590_SHOTGUN:
				if ( attack == ATTACK_ALTERNATE )
				{
					message = va("was bludgeoned %sby", bodypartstr);
					message2 = va("'s %s", weaponParseInfo[mod].mName );
				}
				else
				{
					message = va("was pumped full of lead %sby", bodypartstr);
					message2 = va("'s %s", weaponParseInfo[mod].mName );
				}
				break;

			case MOD_M1911A1_PISTOL:
			case MOD_USSOCOM_PISTOL: 
			case MOD_SILVER_TALON:
				if ( attack == ATTACK_ALTERNATE )
				{
					message = va("was pistol whipped %sby", bodypartstr);
					message2 = va("'s %s", weaponParseInfo[mod].mName );
				}
				else
				{
					message = va("was shot %sby", bodypartstr);
					message2 = va("'s %s", weaponParseInfo[mod].mName );
				}
				break;

			case MOD_AK74_ASSAULT_RIFLE:
				if ( attack == ATTACK_ALTERNATE )
				{
					message = va("was stabbed %sby", bodypartstr);
				}
				else
				{
					message = va("was shot %sby", bodypartstr);
					message2 = va("'s %s", weaponParseInfo[mod].mName );
				}
				break;

			case MOD_M4_ASSAULT_RIFLE:
				if ( attack == ATTACK_ALTERNATE )
				{
					message = "was detonated by";
					message2 = va("'s %s", "M203" );
				}
				else
				{
					message = va("was shot %sby", bodypartstr);
					message2 = va("'s %s", weaponParseInfo[mod].mName );
				}
				break;

			case MOD_M60_MACHINEGUN:
			case MOD_MICRO_UZI_SUBMACHINEGUN:
			case MOD_MP5:
			case MOD_M3A1_SUBMACHINEGUN:
			case MOD_SIG551:
#ifdef SMKWEAPONS
			case MOD_OICW:
#endif
				message = va("was shot %sby", bodypartstr);
				message2 = va("'s %s", weaponParseInfo[mod].mName );
				break;

			case MOD_MSG90A1_SNIPER_RIFLE:    
				message = va("was sniped %sby", bodypartstr);
				message2 = va("'s %s", weaponParseInfo[mod].mName );
				break;

			case MOD_MM1_GRENADE_LAUNCHER:    
			case MOD_RPG7_LAUNCHER:           
			case MOD_M84_GRENADE:
			case MOD_SMOHG92_GRENADE:
			case MOD_ANM14_GRENADE:
			case MOD_M15_GRENADE:
				message = "was detonated by";
				message2 = va("'s %s", weaponParseInfo[mod].mName );
				break;

			case MOD_TELEFRAG:
				message = "tried to invade";
				message2 = "'s personal space";
				break;

			default:
				message = "was killed by";
				break;
		}

		if (message) {
			Com_Printf( "%s%s %s %s%s%s %s\n", targetColor, targetName, message, attackerColor, attackerName, message2, shotstr);
			return;
		}
	}

	// we don't know what it was
	Com_Printf( "%s%s died.\n", targetColor, targetName );
}

/*
================
CG_ItemPickup

A new item was picked up this frame
================
*/
static void CG_ItemPickup( int itemNum, qboolean autoswitch ) 
{
	cg.itemPickup = itemNum;

	// see if it should be the grabbed weapon
	if ( cg_autoswitch.integer && bg_itemlist[itemNum].giType == IT_WEAPON && autoswitch ) 
	{
		if ( cg_autoswitch.integer >= 2 )
		{
			if ( weaponData[bg_itemlist[itemNum].giTag].safe )
			{
				cg.weaponSelectTime = cg.time;
				cg.weaponSelect = bg_itemlist[itemNum].giTag;
			}
		}
		else
		{
			cg.weaponSelectTime = cg.time;
			cg.weaponSelect = bg_itemlist[itemNum].giTag;
		}
	}

	Com_Printf ( "You picked up %s %s!\n", bg_itemlist[itemNum].pickup_prefix, bg_itemlist[itemNum].pickup_name );
}


/*
================
CG_PainEvent

Also called by playerstate transition
================
*/
void CG_PainEvent( centity_t *cent, int health ) 
{
	ECustomSounds	sound;

	// don't do more than two pain sounds a second
	if ( cg.time - cent->pe.painTime < 500 ) {
		return;
	}

	if (health <= 0 )
	{
		return;
	}
	else if ( health < 25 ) 
	{
		sound = SOUND_PAIN_1;
	} 
	else if ( health < 50 ) 
	{
		sound = SOUND_PAIN_2;
	} 
	else if ( health < 75 ) 
	{
		sound = SOUND_PAIN_3;
	} 
	else 
	{
		sound = SOUND_PAIN_3;
	}
	trap_S_StartSound( NULL, cent->currentState.number, CHAN_VOICE, 
		CG_CustomPlayerSound( cent->currentState.number, sound ), -1, -1 );

	// save pain time for programitic twitch animation
	cent->pe.painTime = cg.time;
	cent->pe.painDirection	^= 1;
}


static void CG_BodyQueueCopy(centity_t *cent, int clientNum, int hitLocation, vec3_t direction )
{
	centity_t		*source;
	animation_t		*anim;
	float			animSpeed;
	int				flags=BONE_ANIM_OVERRIDE_FREEZE;
	clientInfo_t	*ci;
	int				i;

	if (cent->ghoul2)
	{
		trap_G2API_CleanGhoul2Models(&cent->ghoul2);
		cent->ghoul2 = 0;
	}

	if (clientNum < 0 || clientNum >= MAX_CLIENTS)
	{
		return;
	}

	source = CG_GetEntity ( clientNum );

	ci = &cgs.clientinfo[ clientNum ];

	cent->radius = 100;

	if (!source)
	{
		return;
	}

	// Make sure the player model is updated before copying it to the body queue
	CG_UpdatePlayerModel ( source );

	if (!source->ghoul2)
	{	
		// some how we don't have a g2 model, so don't do anything
		return;
	}

	// Remove the weapon bolt for the death
	if ( source->pe.weaponModelSpot )
	{
		trap_G2API_DetachG2Model ( source->ghoul2, source->pe.weaponModelSpot );
		trap_G2API_RemoveGhoul2Model ( &source->ghoul2, source->pe.weaponModelSpot );
		source->pe.weaponModelSpot = 0;

		source->flashBoltInterface.isValid = qfalse;
		source->ejectBoltInterface.isValid = qfalse;
	}

	trap_G2API_DuplicateGhoul2Instance(source->ghoul2, &cent->ghoul2);
	
	if ( !cent->ghoul2 )
	{
		return;
	}

	// Reset all mision bolt positions
	for ( i = 0; i < MAX_GAMETYPE_ITEMS; i ++ )
	{
		ci->boltGametypeItems[i] = -1;
	}

	ci->boltNightvision = -1;

	// Clear the source's ghoul2 model to force it to be re-duplicatd.  This will
	// then cause all the gore attached to it to be cleared
	trap_G2API_CleanGhoul2Models ( &source->ghoul2 );
	source->ghoul2 = NULL;

	if (cg_lockDeaths.integer)
	{
		anim = &ci->animations[ BOTH_DEATH_CHEST_1 ];
	}
	else
	{
		anim = &ci->animations[ cent->currentState.torsoAnim & ~(ANIM_TOGGLEBIT) ];
	}
	animSpeed = 50.0f / anim->frameLerp;

	// Clear any bone angles

	trap_G2API_SetBoneAngles(cent->ghoul2, 0, "lower_lumbar", vec3_origin, BONE_ANGLES_POSTMULT, POSITIVE_X, NEGATIVE_Y, NEGATIVE_Z, cgs.gameModels, 0, cg.time ); 
	trap_G2API_SetBoneAngles(cent->ghoul2, 0, "upper_lumbar", vec3_origin, BONE_ANGLES_POSTMULT, POSITIVE_X, NEGATIVE_Y, NEGATIVE_Z, cgs.gameModels, 0, cg.time ); 
	trap_G2API_SetBoneAngles(cent->ghoul2, 0, "cranium", vec3_origin, BONE_ANGLES_POSTMULT, POSITIVE_Z, NEGATIVE_Y, POSITIVE_X, cgs.gameModels,0, cg.time ); 

	// Set the death animation
	trap_G2API_SetBoneAnim(cent->ghoul2, 0, "model_root", anim->firstFrame, anim->firstFrame + anim->numFrames, flags, animSpeed, cg.time, -1, 150);
	trap_G2API_SetBoneAnim(cent->ghoul2, 0, "lower_lumbar", anim->firstFrame, anim->firstFrame + anim->numFrames, flags, animSpeed, cg.time, -1, 150);

	// hit location is a bit field and we need to iterate through the bits
	for ( i = 0; (1<<i) < HL_MAX; i++ )
	{
		if (hitLocation & (1<<i))
		{
			CG_ApplyGore(clientNum, cent, i+1, direction);
		}
	}
}

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

An entity has an event value
also called by CG_CheckPlayerstateEvents
==============
*/
#define	DEBUGNAME(x) if(cg_debugEvents.integer){Com_Printf(x"\n");}
void CG_EntityEvent( centity_t *cent, vec3_t position ) 
{
	entityState_t	*es;
	int				event;
	vec3_t			dir;
	const char		*s;
	int				clientNum;
	clientInfo_t	*ci;

	es = &cent->currentState;
	event = es->event & ~EV_EVENT_BITS;

	if ( cg_debugEvents.integer ) 
	{
		Com_Printf( "ent:%3i  event:%3i ", es->number, event );
	}

	// Ignore all events until map is done changing
	if ( cg.mMapChange )
	{
		return;
	}

	if ( !event ) 
	{
		DEBUGNAME("ZEROEVENT");
		return;
	}

	clientNum = es->clientNum;
	if ( clientNum < 0 || clientNum >= MAX_CLIENTS ) 
	{
		clientNum = 0;
	}

	ci = &cgs.clientinfo[ clientNum ];

	switch ( event ) 
	{
		//
		// movement generated events
		//
		case EV_FOOTSTEP:
			DEBUGNAME("EV_FOOTSTEP");
			if (cg_footsteps.integer) 
			{
				trap_S_StartSound (NULL, es->number, CHAN_BODY, trap_MAT_GetSound(MAT_FOOTSTEP_NORMAL, (es->eventParm&MATERIAL_MASK)), 180, 1000 );
			}
			break;

		case EV_FOOTWADE:
			DEBUGNAME("EV_FOOTWADE");
			break;

		case EV_FALL_SHORT:
			DEBUGNAME("EV_FALL_SHORT");
			trap_S_StartSound (NULL, es->number, CHAN_AUTO, trap_MAT_GetSound(MAT_LAND_NORMAL, es->eventParm&MATERIAL_MASK), 150, 900 );
			if ( clientNum == cg.predictedPlayerState.clientNum ) 
			{
				// smooth landing z changes
				cg.landChange = -8;
				cg.landTime = cg.time;
			}
			break;
	
		case EV_FALL_MEDIUM:
			DEBUGNAME("EV_FALL_MEDIUM");
			// use normal pain sound
			trap_S_StartSound (NULL, es->number, CHAN_AUTO, trap_MAT_GetSound(MAT_LAND_PAIN, (es->eventParm>>8)&MATERIAL_MASK), 150, 900 );
			trap_S_StartSound( NULL, es->number, CHAN_VOICE, CG_CustomPlayerSound( es->number, SOUND_PAIN_3 ), -1, -1 );
			if ( clientNum == cg.predictedPlayerState.clientNum ) 
			{
				// smooth landing z changes
				cg.landChange = -16;
				cg.landTime = cg.time;
			}
			break;
	
		case EV_FALL_FAR:
			DEBUGNAME("EV_FALL_FAR");
	
			trap_S_StartSound (NULL, es->number, CHAN_AUTO, trap_MAT_GetSound(MAT_LAND_DEATH, (es->eventParm>>8)&MATERIAL_MASK), -1, -1 );
			trap_S_StartSound (NULL, es->number, CHAN_AUTO, CG_CustomPlayerSound( cent->currentState.number, SOUND_PAIN_2 ), 150, 900 );
		
			// don't play a pain sound right after this
			cent->pe.painTime = cg.time;	
			
			if ( clientNum == cg.predictedPlayerState.clientNum ) 
			{
				// smooth landing z changes
				cg.landChange = -24;
				cg.landTime = cg.time;
			}
			break;

		case EV_STEP_4:
		case EV_STEP_8:
		case EV_STEP_12:
		case EV_STEP_16:		// smooth out step up transitions
		{
			float	oldStep;
			int		delta;
			int		step;

			DEBUGNAME("EV_STEP");

			if ( clientNum != cg.predictedPlayerState.clientNum ) {
				break;
			}
			// if we are interpolating, we don't need to smooth steps
			if ( cg.demoPlayback || (cg.snap->ps.pm_flags & PMF_FOLLOW) ||
				cg_nopredict.integer || cg_synchronousClients.integer ) {
				break;
			}
			// check for stepping up before a previous step is completed
			delta = cg.time - cg.stepTime;
			
			if (delta < STEP_TIME) 
			{
				oldStep = cg.stepChange * (STEP_TIME - delta) / STEP_TIME;
			} 
			else 
			{
				oldStep = 0;
			}

			// add this amount
			step = 4 * (event - EV_STEP_4 + 1 );
			cg.stepChange = oldStep + step;
			
			if ( cg.stepChange > MAX_STEP_CHANGE ) 
			{
				cg.stepChange = MAX_STEP_CHANGE;
			}
			cg.stepTime = cg.time;

			break;
		}

		case EV_JUMP:
			DEBUGNAME("EV_JUMP");
			trap_S_StartSound (NULL, es->number, CHAN_VOICE, CG_CustomSound( es->number, "*jump1.wav" ), -1, -1 );
			break;

		case EV_WATER_FOOTSTEP:
			DEBUGNAME("EV_WATER_FOOTSTEP");
			trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.waterFootstep[rand()%2], -1, -1);
			break;

		case EV_WATER_TOUCH:
			DEBUGNAME("EV_WATER_TOUCH");
			trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.waterFootstep[rand()%2], -1, -1);
			break;

		case EV_WATER_LAND:
			DEBUGNAME("EV_WATER_LAND");
			trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.waterJumpIn, -1, -1 );
			break;

		case EV_WATER_CLEAR:
			DEBUGNAME("EV_WATER_CLEAR");
			trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.waterLeave, -1, -1 );
			break;

		case EV_SWIM:
			DEBUGNAME("EV_SWIM");
			trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.waterWade[rand()%2], -1, -1 );
			break;

		case EV_ITEM_PICKUP_QUIET:
		case EV_ITEM_PICKUP:
		{
			gitem_t		*item;
			int			index;
			qboolean	autoswitch = qfalse;

			DEBUGNAME("EV_ITEM_PICKUP");

			// Dtermine if this item should autoswitch
			autoswitch = (es->eventParm & ITEM_AUTOSWITCHBIT)?qtrue:qfalse;

			// player predicted index
			index = es->eventParm & ~(ITEM_AUTOSWITCHBIT|ITEM_QUIETPICKUP);

			if ( index < 1 || index >= bg_numItems ) 
			{
				break;
			}

			item = &bg_itemlist[ index ];

			if ( event != EV_ITEM_PICKUP_QUIET && item->pickup_sound )
			{
				trap_S_StartSound (NULL, es->number, CHAN_AUTO,	trap_S_RegisterSound( item->pickup_sound ), -1, -1 );
			}

			// show icon and name on status bar
			if ( es->number == cg.snap->ps.clientNum ) 
			{
				if ( cg.predictedPlayerState.pm_type == PM_NORMAL )
				{
					CG_ItemPickup( index, autoswitch );
				}
			}

			break;
		}

		//=================================================================
		//
		// weapon events
		//
		//=================================================================
		case EV_CHANGE_WEAPON_CANCELLED:
		case EV_CHANGE_WEAPON:

			DEBUGNAME("EV_CHANGE_WEAPON");
			
			// Determine whether or not the alt fire popup should show up
			if(es->number==cg.snap->ps.clientNum && cg.weaponMenuUp )
			{
				// done with weapon menu
				cg.weaponMenuUp = qfalse;

				cg.weaponSelect = cg.weaponMenuSelect;
			}

			break;

		case EV_READY_WEAPON:
			DEBUGNAME("EV_READY_WEAPON");
			break;	

		case EV_FIRE_WEAPON:
			DEBUGNAME("EV_FIRE_WEAPON");
			CG_FireWeapon( cent, ATTACK_NORMAL );
			break;

		case EV_ALT_FIRE:
			DEBUGNAME("EV_ALT_FIRE");
			CG_FireWeapon( cent, ATTACK_ALTERNATE );
			break;

		case EV_NOAMMO:
			DEBUGNAME("EV_NOAMMO");

			if(es->number==cg.snap->ps.clientNum)
			{
				CG_OutOfAmmoChange( es->eventParm );
			}
			break;

		case EV_ITEM_POP:
			DEBUGNAME("EV_ITEM_POP");
			break;

		case EV_ITEM_RESPAWN:
			DEBUGNAME("EV_ITEM_RESPAWN");
			trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.itemRespawnSound, -1, -1 );
			cent->miscTime = cg.time;
			break;

		//=================================================================
		//
		// other events
		//
		//=================================================================

		case EV_PLAYER_TELEPORT_IN:
			DEBUGNAME("EV_PLAYER_TELEPORT_IN");
			if ( cgs.gametypeData->respawnType != RT_NONE )
			{
				trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.respawnSound, -1, -1 );
			}
			break;

		case EV_PLAYER_TELEPORT_OUT:
			DEBUGNAME("EV_PLAYER_TELEPORT_OUT");
			break;

		case EV_GRENADE_BOUNCE:
			DEBUGNAME("EV_GRENADE_BOUNCE");
			if ( rand() & 1 ) 
			{
				trap_S_StartSound (NULL, es->number, CHAN_AUTO, trap_MAT_GetSound(MAT_BOUNCEMETAL_1, (es->eventParm&MATERIAL_MASK)), -1, -1 );
			} 
			else 
			{
				trap_S_StartSound (NULL, es->number, CHAN_AUTO, trap_MAT_GetSound(MAT_BOUNCEMETAL_2, (es->eventParm&MATERIAL_MASK)), -1, -1 );
			}
			break;

		case EV_DESTROY_GHOUL2_INSTANCE:
		{
			centity_t* cent2 = CG_GetEntity (es->eventParm);
			DEBUGNAME("EV_DESTROY_GHOUL2_INSTANCE");
			if ( cent2->ghoul2 && trap_G2_HaveWeGhoul2Models( cent2->ghoul2))
			{
				trap_G2API_CleanGhoul2Models(&(cent2->ghoul2));
			}
			break;
		}
		
	//=================================================================

	//
	// missile impacts
	//

	case EV_MISSILE_HIT:
		DEBUGNAME("EV_MISSILE_HIT");
		ByteToDir( (es->eventParm >> MATERIAL_BITS), dir );
		CG_MissileHitPlayer( es->weapon, position, dir, es->otherEntityNum,
							(cent->currentState.eFlags & EF_ALT_FIRING)?ATTACK_ALTERNATE:ATTACK_NORMAL );
		if ( es->otherEntityNum != cg.snap->ps.clientNum ) 
		{
			// Some missiles - e.g. thrown knives stick in players (for visual effect only).
			CG_HandleStickyMissile(cent,es,dir,es->otherEntityNum);
		}
		break;

	case EV_MISSILE_MISS:
		DEBUGNAME("EV_MISSILE_MISS");
		ByteToDir( (es->eventParm >> MATERIAL_BITS), dir );
		CG_MissileHitWall(es->weapon, position, dir, 
						(es->eventParm & MATERIAL_MASK), (cent->currentState.eFlags & EF_ALT_FIRING)?ATTACK_ALTERNATE:ATTACK_NORMAL );
		break;

	case EV_BULLET_HIT_WALL:
		DEBUGNAME("EV_BULLET_HIT_WALL");
		
		if ( !(cg_antiLag.integer && cg_impactPrediction.integer && !cg_synchronousClients.integer && es->otherEntityNum == cg.predictedPlayerState.clientNum ) )
		{
			// eventParm contains the direction byte and the material id
			ByteToDir( (es->eventParm >> MATERIAL_BITS), dir );
			
			// time contains the weapon and attack of the shot
			CG_Bullet( es->pos.trBase, es->otherEntityNum, (es->time&0xFF), 
					   dir, ENTITYNUM_WORLD, (es->eventParm & MATERIAL_MASK),
					   ((es->time>>8)&0xFF) );
		}

		break;

	case EV_BULLET_HIT_FLESH:

		DEBUGNAME("EV_BULLET_HIT_FLESH");

		// Play hit sounds for local player
		if ( es->otherEntityNum2 == cg.snap->ps.clientNum )
		{
			if ( cg.snap->ps.stats[STAT_ARMOR] )
			{
				trap_S_StartLocalSound ( cgs.media.armorHitSound[rand()%2], CHAN_AUTO  );
			}
			else
			{
				trap_S_StartLocalSound ( cgs.media.fleshHitSound[rand()%2], CHAN_AUTO  );
			}
		}

#ifdef _SOF2_FLESHIMPACTPREDICTION
		if ( !(cg_antiLag.integer && cg_impactPrediction.integer >= 2 && !!cg_synchronousClients.integer && es->otherEntityNum == cg.predictedPlayerState.clientNum ) )
#endif
		{
			int	fxtype = MATERIAL_FLESH;

			// eventParm contains the direction byte
			ByteToDir( es->eventParm, dir );

			if (cg_lockBlood.integer)
			{
				fxtype = MATERIAL_NONE;
			}

			CG_Bullet( es->pos.trBase, es->otherEntityNum, (es->time&0xFF), dir,  
					   es->otherEntityNum2, fxtype,
					   ((es->time>>8)&0xFF) );

			CG_AddProcGore ( cent );
		}

		break;

	case EV_EXPLOSION_HIT_FLESH:

		DEBUGNAME("EV_EXPLOSION_HIT_FLESH");

		CG_AddProcGore ( cent );
		break;

	case EV_PLAY_EFFECT:
		DEBUGNAME("EV_PLAY_EFFECT");
		if (es->eventParm != -1)
		{
			trap_FX_PlayEffectID(es->eventParm, es->origin, es->angles, -1, -1 );
		}
		break;

	case EV_GENERAL_SOUND:
		DEBUGNAME("EV_GENERAL_SOUND");
		if ( cgs.gameSounds[ es->eventParm ] ) {
			trap_S_StartSound (NULL, es->number, CHAN_VOICE, cgs.gameSounds[ es->eventParm ], -1, -1 );
		} else {
			s = CG_ConfigString( CS_SOUNDS + es->eventParm );
			trap_S_StartSound (NULL, es->number, CHAN_VOICE, CG_CustomSound( es->number, s ), -1, -1 );
		}
		break;

	case EV_GLOBAL_SOUND:
		if ( cg_soundGlobal.integer )
		{
			DEBUGNAME("EV_GLOBAL_SOUND");
			if ( cgs.gameSounds[ es->eventParm ] ) 
			{
				trap_S_StartLocalSound ( cgs.gameSounds[ es->eventParm ], CHAN_AUTO );
			} 
			else 
			{
				s = CG_ConfigString( CS_SOUNDS + es->eventParm );
				trap_S_StartLocalSound ( CG_CustomSound( es->number, s ), CHAN_AUTO );
			}
		}
		break;

	case EV_ENTITY_SOUND:
		DEBUGNAME("EV_ENTITY_SOUND");
		//somewhat of a hack - weapon is the caller entity's index, trickedentindex is the proper sound channel
		if ( cgs.gameSounds[ es->eventParm ] ) {
			trap_S_StartSound (NULL, es->weapon, 0, cgs.gameSounds[ es->eventParm ], -1, -1 );
		} else {
			s = CG_ConfigString( CS_SOUNDS + es->eventParm );
			trap_S_StartSound (NULL, es->weapon, 0, CG_CustomSound( es->weapon, s ), -1, -1 );
		}
		break;

	case EV_GLASS_SHATTER:
		DEBUGNAME("EV_GLASS_SHATTER");
		CG_GlassShatter(es->number, es->pos.trBase, es->angles, es->origin);
		break;

	case EV_PAIN:
		// local player sounds are triggered in CG_CheckLocalSounds,
		// so ignore events on the player
		DEBUGNAME("EV_PAIN");
		if ( cent->currentState.number != cg.snap->ps.clientNum ) 
		{
			CG_PainEvent( cent, es->eventParm );
		}
		break;

	case EV_PAIN_WATER:
	{
		static drownIndex = 0;
		DEBUGNAME("EV_PAIN_WATER");
		trap_S_StartSound ( NULL, es->number, CHAN_VOICE, cgs.media.drownPainSound[(drownIndex++)%2], -1, -1 );
		break;
	}

	case EV_GAME_OVER:
		DEBUGNAME("EV_OBITUARY");
		CG_GameOver ( es );
		break;

	case EV_GOGGLES:
		DEBUGNAME("EV_GOGGLES");

		// Sound is handled elsewhere for local client
		trap_S_StartSound ( NULL, es->number, CHAN_AUTO, es->eventParm?cgs.media.gogglesOnSound:cgs.media.gogglesOffSound, 120, -1 );
		break;

	case EV_OBITUARY:
		DEBUGNAME("EV_OBITUARY");
		CG_Obituary( es );
		break;

	case EV_STOPLOOPINGSOUND:
		DEBUGNAME("EV_STOPLOOPINGSOUND");
		trap_S_StopLoopingSound( es->number );
		es->loopSound = 0;
		break;

	case EV_DEBUG_LINE:
		DEBUGNAME("EV_DEBUG_LINE");
		CG_Beam( cent );
		break;

	case EV_TESTLINE:
		DEBUGNAME("EV_TESTLINE");
		CG_TestLine(es->origin, es->origin2, 0, es->weapon, 1);
		break;

	case EV_BODY_QUEUE_COPY:
		DEBUGNAME("EV_BODY_QUEUE_COPY");

		// First byte of eventParm is the client Number
		// Second byte of eventParm is the direction of the incoming shot
		// Third and fourth byte of eventParm is the hit location
		ByteToDir( (es->eventParm & 0xFF), dir );

		CG_BodyQueueCopy(cent, es->otherEntityNum, (es->eventParm>>8), dir);
		break;

	case EV_PROC_GORE:
		DEBUGNAME("EV_PROC_GORE");
		CG_AddProcGore(cent);
		break;

	case EV_BOTWAYPOINT:
		DEBUGNAME("EV_BOTWAYPOINT");
		CG_TestLine(cent->lerpOrigin, es->angles, 30000, 0x0000ff, 3);
		//Just render for 30 seconds because this waypoint might not be rendered again for quite some time.
		break;

	case EV_BOTSUBWAYPOINT:
		DEBUGNAME("EV_BOTSUBWAYPOINT");
		CG_TestLine(cent->lerpOrigin, es->angles, 30000, 0xff00ff, 3);
		//Just render for 30 seconds because this waypoint might not be rendered again for quite some time.
		break;

	case EV_GAMETYPE_RESTART:
		CG_MapRestart ( qtrue );
		break;

	case EV_USE:
		break;

	case EV_WEAPON_CALLBACK:
		DEBUGNAME("EV_WEAPON_CALLBACK");
		if ( cent->currentState.number == cg.snap->ps.clientNum ) 
		{
			CG_WeaponCallback ( &cg.predictedPlayerState,
								&cg_entities[cg.predictedPlayerState.clientNum], 
								(es->eventParm&0xFF),			// Weapon id
								((es->eventParm>>8)&0xFF),		// Anim id
								((es->eventParm>>16)&0xFF),		// Anim choice
								((es->eventParm>>24)&0xFF) );	// Callback step
		}
		break;

	default:
		DEBUGNAME("UNKNOWN");
		Com_Error( ERR_FATAL, "Unknown event: %i", event );
		break;
	}

}

/*
==============
CG_CheckEvents
==============
*/
void CG_CheckEvents( centity_t *cent ) 
{
	// check for event-only entities
	if ( cent->currentState.eType > ET_EVENTS ) 
	{
		// already fired
		if ( cent->previousEvent ) 
		{			
			return;	
		}
		
		// if this is a player event set the entity number of the client entity number
		if ( cent->currentState.eFlags & EF_PLAYER_EVENT )
		{
			cent->currentState.number = cent->currentState.otherEntityNum;
		}

		cent->previousEvent = 1;

		cent->currentState.event = cent->currentState.eType - ET_EVENTS;
	} 
	else 
	{
		// check for events riding with another entity
		if ( cent->currentState.event == cent->previousEvent ) 
		{
			return;
		}

		cent->previousEvent = cent->currentState.event;

		if ( ( cent->currentState.event & ~EV_EVENT_BITS ) == 0 ) 
		{
			return;
		}
	}

	// calculate the position at exactly the frame time
	BG_EvaluateTrajectory( &cent->currentState.pos, cg.snap->serverTime, cent->lerpOrigin );

	CG_SetEntitySoundPosition( cent );

	CG_EntityEvent( cent, cent->lerpOrigin );
}

