1217 lines
30 KiB
C
1217 lines
30 KiB
C
// 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, "Red Team hit the score limit" );
|
|
break;
|
|
|
|
case TEAM_BLUE:
|
|
Com_sprintf ( cgs.gameover, MAX_QPATH, "Blue Team hit the score limit" );
|
|
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;
|
|
|
|
target = ent->otherEntityNum;
|
|
attacker = ent->otherEntityNum2;
|
|
mod = ent->eventParm & 0xFF;
|
|
attack = (ent->eventParm >> 8) & 0xFF;
|
|
attackerColor = S_COLOR_WHITE;
|
|
targetColor = S_COLOR_WHITE;
|
|
|
|
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 = S_COLOR_RED;
|
|
break;
|
|
|
|
case TEAM_BLUE:
|
|
targetColor = S_COLOR_BLUE;
|
|
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:
|
|
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", targetColor, targetName,
|
|
CG_PlaceString( cg.snap->ps.persistant[PERS_RANK] + 1 ),
|
|
cg.snap->ps.persistant[PERS_SCORE] );
|
|
}
|
|
else
|
|
{
|
|
s = va("You killed %s%s", targetColor, targetName );
|
|
}
|
|
|
|
CG_CenterPrint( s, 0.43f );
|
|
}
|
|
}
|
|
|
|
|
|
// 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 = S_COLOR_RED;
|
|
break;
|
|
|
|
case TEAM_BLUE:
|
|
attackerColor = S_COLOR_BLUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
if ( attacker != ENTITYNUM_WORLD )
|
|
{
|
|
switch (mod)
|
|
{
|
|
case MOD_KNIFE:
|
|
message = "was sliced by";
|
|
break;
|
|
|
|
case MOD_USAS_12_SHOTGUN:
|
|
case MOD_M590_SHOTGUN:
|
|
if ( attack == ATTACK_ALTERNATE )
|
|
{
|
|
message = "was bludgeoned by";
|
|
message2 = va("'s %s", weaponParseInfo[mod].mName );
|
|
}
|
|
else
|
|
{
|
|
message = "was pumped full of lead by";
|
|
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 = "was pistol whipped by";
|
|
message2 = va("'s %s", weaponParseInfo[mod].mName );
|
|
}
|
|
else
|
|
{
|
|
message = "was shot by";
|
|
message2 = va("'s %s", weaponParseInfo[mod].mName );
|
|
}
|
|
break;
|
|
|
|
case MOD_AK74_ASSAULT_RIFLE:
|
|
if ( attack == ATTACK_ALTERNATE )
|
|
{
|
|
message = "was stabbed by";
|
|
}
|
|
else
|
|
{
|
|
message = "was shot by";
|
|
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 = "was shot by";
|
|
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:
|
|
message = "was shot by";
|
|
message2 = va("'s %s", weaponParseInfo[mod].mName );
|
|
break;
|
|
|
|
case MOD_MSG90A1_SNIPER_RIFLE:
|
|
message = "was sniped by";
|
|
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\n", targetColor, targetName, message, attackerColor, attackerName, message2);
|
|
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(¢->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, ¢->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 = ¢->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_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( ¢->currentState.pos, cg.snap->serverTime, cent->lerpOrigin );
|
|
|
|
CG_SetEntitySoundPosition( cent );
|
|
|
|
CG_EntityEvent( cent, cent->lerpOrigin );
|
|
}
|
|
|