From e5ff1d3d4bc01d98b12d05f9cb85457c7f15c424 Mon Sep 17 00:00:00 2001 From: IronClawTrem Date: Tue, 6 Aug 2019 02:17:23 +0100 Subject: first commit --- src/cgame/cg_players.c | 2565 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2565 insertions(+) create mode 100644 src/cgame/cg_players.c (limited to 'src/cgame/cg_players.c') diff --git a/src/cgame/cg_players.c b/src/cgame/cg_players.c new file mode 100644 index 0000000..3bd0e65 --- /dev/null +++ b/src/cgame/cg_players.c @@ -0,0 +1,2565 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous 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 Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +// cg_players.c -- handle the media and animation for player entities + + +#include "cg_local.h" + +char *cg_customSoundNames[ MAX_CUSTOM_SOUNDS ] = +{ + "*death1.wav", + "*death2.wav", + "*death3.wav", + "*jump1.wav", + "*pain25_1.wav", + "*pain50_1.wav", + "*pain75_1.wav", + "*pain100_1.wav", + "*falling1.wav", + "*gasp.wav", + "*drown.wav", + "*fall1.wav", + "*taunt.wav" +}; + + +/* +================ +CG_CustomSound + +================ +*/ +sfxHandle_t CG_CustomSound( int clientNum, const char *soundName ) +{ + clientInfo_t *ci; + int i; + + if( soundName[ 0 ] != '*' ) + return trap_S_RegisterSound( soundName, qfalse ); + + if( clientNum < 0 || clientNum >= MAX_CLIENTS ) + clientNum = 0; + + ci = &cgs.clientinfo[ clientNum ]; + + for( i = 0; i < MAX_CUSTOM_SOUNDS && cg_customSoundNames[ i ]; i++ ) + { + if( !strcmp( soundName, cg_customSoundNames[ i ] ) ) + return ci->sounds[ i ]; + } + + CG_Error( "Unknown custom sound: %s", soundName ); + return 0; +} + + + +/* +============================================================================= + +CLIENT INFO + +============================================================================= +*/ + +/* +====================== +CG_ParseAnimationFile + +Read a configuration file containing animation coutns and rates +models/players/visor/animation.cfg, etc +====================== +*/ +static qboolean CG_ParseAnimationFile( const char *filename, clientInfo_t *ci ) +{ + char *text_p, *prev; + int len; + int i; + char *token; + float fps; + int skip; + char text[ 20000 ]; + fileHandle_t f; + animation_t *animations; + + animations = ci->animations; + + // load the file + len = trap_FS_FOpenFile( filename, &f, FS_READ ); + if( len <= 0 ) + return qfalse; + + if( len >= sizeof( text ) - 1 ) + { + CG_Printf( "File %s too long\n", filename ); + trap_FS_FCloseFile( f ); + return qfalse; + } + + trap_FS_Read( text, len, f ); + text[ len ] = 0; + trap_FS_FCloseFile( f ); + + // parse the text + text_p = text; + skip = 0; // quite the compiler warning + + ci->footsteps = FOOTSTEP_NORMAL; + VectorClear( ci->headOffset ); + ci->gender = GENDER_MALE; + ci->fixedlegs = qfalse; + ci->fixedtorso = qfalse; + ci->nonsegmented = qfalse; + + // read optional parameters + while( 1 ) + { + prev = text_p; // so we can unget + token = COM_Parse( &text_p ); + + if( !token ) + break; + + if( !Q_stricmp( token, "footsteps" ) ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + if( !Q_stricmp( token, "default" ) || !Q_stricmp( token, "normal" ) ) + ci->footsteps = FOOTSTEP_NORMAL; + else if( !Q_stricmp( token, "flesh" ) ) + ci->footsteps = FOOTSTEP_FLESH; + else if( !Q_stricmp( token, "none" ) ) + ci->footsteps = FOOTSTEP_NONE; + else if( !Q_stricmp( token, "custom" ) ) + ci->footsteps = FOOTSTEP_CUSTOM; + else + CG_Printf( "Bad footsteps parm in %s: %s\n", filename, token ); + + continue; + } + else if( !Q_stricmp( token, "headoffset" ) ) + { + for( i = 0 ; i < 3 ; i++ ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + ci->headOffset[ i ] = atof( token ); + } + + continue; + } + else if( !Q_stricmp( token, "sex" ) ) + { + token = COM_Parse( &text_p ); + + if( !token ) + break; + + if( token[ 0 ] == 'f' || token[ 0 ] == 'F' ) + ci->gender = GENDER_FEMALE; + else if( token[ 0 ] == 'n' || token[ 0 ] == 'N' ) + ci->gender = GENDER_NEUTER; + else + ci->gender = GENDER_MALE; + + continue; + } + else if( !Q_stricmp( token, "fixedlegs" ) ) + { + ci->fixedlegs = qtrue; + continue; + } + else if( !Q_stricmp( token, "fixedtorso" ) ) + { + ci->fixedtorso = qtrue; + continue; + } + else if( !Q_stricmp( token, "nonsegmented" ) ) + { + ci->nonsegmented = qtrue; + continue; + } + + // if it is a number, start parsing animations + if( token[ 0 ] >= '0' && token[ 0 ] <= '9' ) + { + text_p = prev; // unget the token + break; + } + + Com_Printf( "unknown token '%s' is %s\n", token, filename ); + } + + if( !ci->nonsegmented ) + { + // read information for each frame + for( i = 0; i < MAX_PLAYER_ANIMATIONS; i++ ) + { + token = COM_Parse( &text_p ); + + if( !*token ) + { + if( i >= TORSO_GETFLAG && i <= TORSO_NEGATIVE ) + { + animations[ i ].firstFrame = animations[ TORSO_GESTURE ].firstFrame; + animations[ i ].frameLerp = animations[ TORSO_GESTURE ].frameLerp; + animations[ i ].initialLerp = animations[ TORSO_GESTURE ].initialLerp; + animations[ i ].loopFrames = animations[ TORSO_GESTURE ].loopFrames; + animations[ i ].numFrames = animations[ TORSO_GESTURE ].numFrames; + animations[ i ].reversed = qfalse; + animations[ i ].flipflop = qfalse; + continue; + } + + break; + } + + animations[ i ].firstFrame = atoi( token ); + + // leg only frames are adjusted to not count the upper body only frames + if( i == LEGS_WALKCR ) + skip = animations[ LEGS_WALKCR ].firstFrame - animations[ TORSO_GESTURE ].firstFrame; + + if( i >= LEGS_WALKCR && inonsegmented ) + { + Com_sprintf( filename, sizeof( filename ), "models/players/%s/lower_%s.skin", modelName, skinName ); + ci->legsSkin = trap_R_RegisterSkin( filename ); + if( !ci->legsSkin ) + Com_Printf( "Leg skin load failure: %s\n", filename ); + + Com_sprintf( filename, sizeof( filename ), "models/players/%s/upper_%s.skin", modelName, skinName ); + ci->torsoSkin = trap_R_RegisterSkin( filename ); + if( !ci->torsoSkin ) + Com_Printf( "Torso skin load failure: %s\n", filename ); + + Com_sprintf( filename, sizeof( filename ), "models/players/%s/head_%s.skin", modelName, skinName ); + ci->headSkin = trap_R_RegisterSkin( filename ); + if( !ci->headSkin ) + Com_Printf( "Head skin load failure: %s\n", filename ); + + if( !ci->legsSkin || !ci->torsoSkin || !ci->headSkin ) + return qfalse; + } + else + { + Com_sprintf( filename, sizeof( filename ), "models/players/%s/nonseg_%s.skin", modelName, skinName ); + ci->nonSegSkin = trap_R_RegisterSkin( filename ); + if( !ci->nonSegSkin ) + Com_Printf( "Non-segmented skin load failure: %s\n", filename ); + + if( !ci->nonSegSkin ) + return qfalse; + } + + return qtrue; +} + +/* +========================== +CG_RegisterClientModelname +========================== +*/ +static qboolean CG_RegisterClientModelname( clientInfo_t *ci, const char *modelName, const char *skinName ) +{ + char filename[ MAX_QPATH * 2 ]; + + // do this first so the nonsegmented property is set + // load the animations + Com_sprintf( filename, sizeof( filename ), "models/players/%s/animation.cfg", modelName ); + if( !CG_ParseAnimationFile( filename, ci ) ) + { + Com_Printf( "Failed to load animation file %s\n", filename ); + return qfalse; + } + + // load cmodels before models so filecache works + + if( !ci->nonsegmented ) + { + Com_sprintf( filename, sizeof( filename ), "models/players/%s/lower.md3", modelName ); + ci->legsModel = trap_R_RegisterModel( filename ); + if( !ci->legsModel ) + { + Com_Printf( "Failed to load model file %s\n", filename ); + return qfalse; + } + + Com_sprintf( filename, sizeof( filename ), "models/players/%s/upper.md3", modelName ); + ci->torsoModel = trap_R_RegisterModel( filename ); + if( !ci->torsoModel ) + { + Com_Printf( "Failed to load model file %s\n", filename ); + return qfalse; + } + + Com_sprintf( filename, sizeof( filename ), "models/players/%s/head.md3", modelName ); + ci->headModel = trap_R_RegisterModel( filename ); + if( !ci->headModel ) + { + Com_Printf( "Failed to load model file %s\n", filename ); + return qfalse; + } + } + else + { + Com_sprintf( filename, sizeof( filename ), "models/players/%s/nonseg.md3", modelName ); + ci->nonSegModel = trap_R_RegisterModel( filename ); + if( !ci->nonSegModel ) + { + Com_Printf( "Failed to load model file %s\n", filename ); + return qfalse; + } + } + + // if any skins failed to load, return failure + if( !CG_RegisterClientSkin( ci, modelName, skinName ) ) + { + Com_Printf( "Failed to load skin file: %s : %s\n", modelName, skinName ); + return qfalse; + } + + //FIXME: skins do not load without icon present. do we want icons anyway? +/* Com_sprintf( filename, sizeof( filename ), "models/players/%s/icon_%s.tga", modelName, skinName ); + ci->modelIcon = trap_R_RegisterShaderNoMip( filename ); + if( !ci->modelIcon ) + { + Com_Printf( "Failed to load icon file: %s\n", filename ); + return qfalse; + }*/ + + return qtrue; +} + +/* +==================== +CG_ColorFromString +==================== +*/ +static void CG_ColorFromString( const char *v, vec3_t color ) +{ + int val; + + VectorClear( color ); + + val = atoi( v ); + + if( val < 1 || val > 7 ) + { + VectorSet( color, 1, 1, 1 ); + return; + } + + if( val & 1 ) + color[ 2 ] = 1.0f; + + if( val & 2 ) + color[ 1 ] = 1.0f; + + if( val & 4 ) + color[ 0 ] = 1.0f; +} + + +/* +=================== +CG_LoadClientInfo + +Load it now, taking the disk hits +=================== +*/ +static void CG_LoadClientInfo( clientInfo_t *ci ) +{ + const char *dir, *fallback; + int i; + const char *s; + int clientNum; + + if( !CG_RegisterClientModelname( ci, ci->modelName, ci->skinName ) ) + { + if( cg_buildScript.integer ) + CG_Error( "CG_RegisterClientModelname( %s, %s ) failed", ci->modelName, ci->skinName ); + + // fall back + if( !CG_RegisterClientModelname( ci, DEFAULT_MODEL, "default" ) ) + CG_Error( "DEFAULT_MODEL (%s) failed to register", DEFAULT_MODEL ); + } + + // sounds + dir = ci->modelName; + fallback = DEFAULT_MODEL; + + for( i = 0; i < MAX_CUSTOM_SOUNDS; i++ ) + { + s = cg_customSoundNames[ i ]; + + if( !s ) + break; + + // fanny about a bit with sounds that are missing + if( !CG_FileExists( va( "sound/player/%s/%s", dir, s + 1 ) ) ) + { + //file doesn't exist + + if( i == 11 || i == 8 ) //fall or falling + { + ci->sounds[ i ] = trap_S_RegisterSound( "sound/null.wav", qfalse ); + } + else + { + if( i == 9 ) //gasp + s = cg_customSoundNames[ 7 ]; //pain100_1 + else if( i == 10 ) //drown + s = cg_customSoundNames[ 0 ]; //death1 + + ci->sounds[ i ] = trap_S_RegisterSound( va( "sound/player/%s/%s", dir, s + 1 ), qfalse ); + if( !ci->sounds[ i ] ) + ci->sounds[ i ] = trap_S_RegisterSound( va( "sound/player/%s/%s", fallback, s + 1 ), qfalse ); + } + } + else + { + ci->sounds[ i ] = trap_S_RegisterSound( va( "sound/player/%s/%s", dir, s + 1 ), qfalse ); + if( !ci->sounds[ i ] ) + ci->sounds[ i ] = trap_S_RegisterSound( va( "sound/player/%s/%s", fallback, s + 1 ), qfalse ); + } + } + + if( ci->footsteps == FOOTSTEP_CUSTOM ) + { + for( i = 0; i < 4; i++ ) + { + ci->customFootsteps[ i ] = trap_S_RegisterSound( va( "sound/player/%s/step%d.wav", dir, i + 1 ), qfalse ); + if( !ci->customFootsteps[ i ] ) + ci->customFootsteps[ i ] = trap_S_RegisterSound( va( "sound/player/footsteps/step%d.wav", i + 1 ), qfalse ); + + ci->customMetalFootsteps[ i ] = trap_S_RegisterSound( va( "sound/player/%s/clank%d.wav", dir, i + 1 ), qfalse ); + if( !ci->customMetalFootsteps[ i ] ) + ci->customMetalFootsteps[ i ] = trap_S_RegisterSound( va( "sound/player/footsteps/clank%d.wav", i + 1 ), qfalse ); + } + } + + // reset any existing players and bodies, because they might be in bad + // frames for this new model + clientNum = ci - cgs.clientinfo; + for( i = 0; i < MAX_GENTITIES; i++ ) + { + if( cg_entities[ i ].currentState.clientNum == clientNum && + cg_entities[ i ].currentState.eType == ET_PLAYER ) + CG_ResetPlayerEntity( &cg_entities[ i ] ); + } +} + +/* +====================== +CG_CopyClientInfoModel +====================== +*/ +static void CG_CopyClientInfoModel( clientInfo_t *from, clientInfo_t *to ) +{ + VectorCopy( from->headOffset, to->headOffset ); + to->footsteps = from->footsteps; + to->gender = from->gender; + + to->legsModel = from->legsModel; + to->legsSkin = from->legsSkin; + to->torsoModel = from->torsoModel; + to->torsoSkin = from->torsoSkin; + to->headModel = from->headModel; + to->headSkin = from->headSkin; + to->nonSegModel = from->nonSegModel; + to->nonSegSkin = from->nonSegSkin; + to->nonsegmented = from->nonsegmented; + to->modelIcon = from->modelIcon; + + memcpy( to->animations, from->animations, sizeof( to->animations ) ); + memcpy( to->sounds, from->sounds, sizeof( to->sounds ) ); + memcpy( to->customFootsteps, from->customFootsteps, sizeof( to->customFootsteps ) ); + memcpy( to->customMetalFootsteps, from->customMetalFootsteps, sizeof( to->customMetalFootsteps ) ); +} + + +/* +====================== +CG_GetCorpseNum +====================== +*/ +static int CG_GetCorpseNum( pClass_t class ) +{ + int i; + clientInfo_t *match; + char *modelName; + char *skinName; + + modelName = BG_FindModelNameForClass( class ); + skinName = BG_FindSkinNameForClass( class ); + + for( i = PCL_NONE + 1; i < PCL_NUM_CLASSES; i++ ) + { + match = &cgs.corpseinfo[ i ]; + + if( !match->infoValid ) + continue; + + if( !Q_stricmp( modelName, match->modelName ) + && !Q_stricmp( skinName, match->skinName ) ) + { + // this clientinfo is identical, so use it's handles + return i; + } + } + + //something has gone horribly wrong + return -1; +} + + +/* +====================== +CG_ScanForExistingClientInfo +====================== +*/ +static qboolean CG_ScanForExistingClientInfo( clientInfo_t *ci ) +{ + int i; + clientInfo_t *match; + + for( i = PCL_NONE + 1; i < PCL_NUM_CLASSES; i++ ) + { + match = &cgs.corpseinfo[ i ]; + + if( !match->infoValid ) + continue; + + if( !Q_stricmp( ci->modelName, match->modelName ) && + !Q_stricmp( ci->skinName, match->skinName ) ) + { + // this clientinfo is identical, so use it's handles + CG_CopyClientInfoModel( match, ci ); + + return qtrue; + } + } + + // shouldn't happen + return qfalse; +} + + +/* +====================== +CG_PrecacheClientInfo +====================== +*/ +void CG_PrecacheClientInfo( pClass_t class, char *model, char *skin ) +{ + clientInfo_t *ci; + clientInfo_t newInfo; + + ci = &cgs.corpseinfo[ class ]; + + // the old value + memset( &newInfo, 0, sizeof( newInfo ) ); + + // model + Q_strncpyz( newInfo.modelName, model, sizeof( newInfo.modelName ) ); + Q_strncpyz( newInfo.headModelName, model, sizeof( newInfo.headModelName ) ); + + // modelName didn not include a skin name + if( !skin ) + { + Q_strncpyz( newInfo.skinName, "default", sizeof( newInfo.skinName ) ); + Q_strncpyz( newInfo.headSkinName, "default", sizeof( newInfo.headSkinName ) ); + } + else + { + Q_strncpyz( newInfo.skinName, skin, sizeof( newInfo.skinName ) ); + Q_strncpyz( newInfo.headSkinName, skin, sizeof( newInfo.headSkinName ) ); + } + + newInfo.infoValid = qtrue; + + // actually register the models + *ci = newInfo; + CG_LoadClientInfo( ci ); +} + + +/* +====================== +CG_NewClientInfo +====================== +*/ +void CG_NewClientInfo( int clientNum ) +{ + clientInfo_t *ci; + clientInfo_t newInfo; + const char *configstring; + const char *v; + char *slash; + + ci = &cgs.clientinfo[ clientNum ]; + + configstring = CG_ConfigString( clientNum + CS_PLAYERS ); + if( !configstring[ 0 ] ) + { + memset( ci, 0, sizeof( *ci ) ); + return; // player just left + } + + // the old value + memset( &newInfo, 0, sizeof( newInfo ) ); + + // isolate the player's name + v = Info_ValueForKey( configstring, "n" ); + Q_strncpyz( newInfo.name, v, sizeof( newInfo.name ) ); + + // colors + v = Info_ValueForKey( configstring, "c1" ); + CG_ColorFromString( v, newInfo.color1 ); + + v = Info_ValueForKey( configstring, "c2" ); + CG_ColorFromString( v, newInfo.color2 ); + + // bot skill + v = Info_ValueForKey( configstring, "skill" ); + newInfo.botSkill = atoi( v ); + + // handicap + v = Info_ValueForKey( configstring, "hc" ); + newInfo.handicap = atoi( v ); + + // wins + v = Info_ValueForKey( configstring, "w" ); + newInfo.wins = atoi( v ); + + // losses + v = Info_ValueForKey( configstring, "l" ); + newInfo.losses = atoi( v ); + + // team + v = Info_ValueForKey( configstring, "t" ); + newInfo.team = atoi( v ); + + // team task + v = Info_ValueForKey( configstring, "tt" ); + newInfo.teamTask = atoi( v ); + + // team leader + v = Info_ValueForKey( configstring, "tl" ); + newInfo.teamLeader = atoi( v ); + + // model + v = Info_ValueForKey( configstring, "model" ); + Q_strncpyz( newInfo.modelName, v, sizeof( newInfo.modelName ) ); + + slash = strchr( newInfo.modelName, '/' ); + + if( !slash ) + { + // modelName didn not include a skin name + Q_strncpyz( newInfo.skinName, "default", sizeof( newInfo.skinName ) ); + } + else + { + Q_strncpyz( newInfo.skinName, slash + 1, sizeof( newInfo.skinName ) ); + // truncate modelName + *slash = 0; + } + + //CG_Printf( "NCI: %s\n", v ); + + // head model + v = Info_ValueForKey( configstring, "hmodel" ); + Q_strncpyz( newInfo.headModelName, v, sizeof( newInfo.headModelName ) ); + + slash = strchr( newInfo.headModelName, '/' ); + + if( !slash ) + { + // modelName didn not include a skin name + Q_strncpyz( newInfo.headSkinName, "default", sizeof( newInfo.headSkinName ) ); + } + else + { + Q_strncpyz( newInfo.headSkinName, slash + 1, sizeof( newInfo.headSkinName ) ); + // truncate modelName + *slash = 0; + } + + // replace whatever was there with the new one + newInfo.infoValid = qtrue; + *ci = newInfo; + + // scan for an existing clientinfo that matches this modelname + // so we can avoid loading checks if possible + if( !CG_ScanForExistingClientInfo( ci ) ) + CG_LoadClientInfo( ci ); +} + + + +/* +============================================================================= + +PLAYER ANIMATION + +============================================================================= +*/ + + +/* +=============== +CG_SetLerpFrameAnimation + +may include ANIM_TOGGLEBIT +=============== +*/ +static void CG_SetLerpFrameAnimation( clientInfo_t *ci, lerpFrame_t *lf, int newAnimation ) +{ + animation_t *anim; + + lf->animationNumber = newAnimation; + newAnimation &= ~ANIM_TOGGLEBIT; + + if( newAnimation < 0 || newAnimation >= MAX_PLAYER_TOTALANIMATIONS ) + CG_Error( "Bad animation number: %i", newAnimation ); + + anim = &ci->animations[ newAnimation ]; + + lf->animation = anim; + lf->animationTime = lf->frameTime + anim->initialLerp; + + if( cg_debugAnim.integer ) + CG_Printf( "Anim: %i\n", newAnimation ); +} + +/* +=============== +CG_RunPlayerLerpFrame + +Sets cg.snap, cg.oldFrame, and cg.backlerp +cg.time should be between oldFrameTime and frameTime after exit +=============== +*/ +static void CG_RunPlayerLerpFrame( clientInfo_t *ci, lerpFrame_t *lf, int newAnimation, float speedScale ) +{ + int f, numFrames; + animation_t *anim; + + // debugging tool to get no animations + if( cg_animSpeed.integer == 0 ) + { + lf->oldFrame = lf->frame = lf->backlerp = 0; + return; + } + + // see if the animation sequence is switching + if( newAnimation != lf->animationNumber || !lf->animation ) + { + CG_SetLerpFrameAnimation( ci, lf, newAnimation ); + } + + // if we have passed the current frame, move it to + // oldFrame and calculate a new frame + if( cg.time >= lf->frameTime ) + { + lf->oldFrame = lf->frame; + lf->oldFrameTime = lf->frameTime; + + // get the next frame based on the animation + anim = lf->animation; + if( !anim->frameLerp ) + return; // shouldn't happen + + if( cg.time < lf->animationTime ) + lf->frameTime = lf->animationTime; // initial lerp + else + lf->frameTime = lf->oldFrameTime + anim->frameLerp; + + f = ( lf->frameTime - lf->animationTime ) / anim->frameLerp; + f *= speedScale; // adjust for haste, etc + numFrames = anim->numFrames; + + if( anim->flipflop ) + numFrames *= 2; + + if( f >= numFrames ) + { + f -= numFrames; + if( anim->loopFrames ) + { + f %= anim->loopFrames; + f += anim->numFrames - anim->loopFrames; + } + else + { + f = numFrames - 1; + // the animation is stuck at the end, so it + // can immediately transition to another sequence + lf->frameTime = cg.time; + } + } + + if( anim->reversed ) + lf->frame = anim->firstFrame + anim->numFrames - 1 - f; + else if( anim->flipflop && f>=anim->numFrames ) + lf->frame = anim->firstFrame + anim->numFrames - 1 - ( f % anim->numFrames ); + else + lf->frame = anim->firstFrame + f; + + if( cg.time > lf->frameTime ) + { + lf->frameTime = cg.time; + + if( cg_debugAnim.integer ) + CG_Printf( "Clamp lf->frameTime\n" ); + } + } + + if( lf->frameTime > cg.time + 200 ) + lf->frameTime = cg.time; + + if( lf->oldFrameTime > cg.time ) + lf->oldFrameTime = cg.time; + + // calculate current lerp value + if( lf->frameTime == lf->oldFrameTime ) + lf->backlerp = 0; + else + lf->backlerp = 1.0 - (float)( cg.time - lf->oldFrameTime ) / ( lf->frameTime - lf->oldFrameTime ); +} + + +/* +=============== +CG_ClearLerpFrame +=============== +*/ +static void CG_ClearLerpFrame( clientInfo_t *ci, lerpFrame_t *lf, int animationNumber ) +{ + lf->frameTime = lf->oldFrameTime = cg.time; + CG_SetLerpFrameAnimation( ci, lf, animationNumber ); + lf->oldFrame = lf->frame = lf->animation->firstFrame; +} + + +/* +=============== +CG_PlayerAnimation +=============== +*/ +static void CG_PlayerAnimation( centity_t *cent, int *legsOld, int *legs, float *legsBackLerp, + int *torsoOld, int *torso, float *torsoBackLerp ) +{ + clientInfo_t *ci; + int clientNum; + float speedScale = 1.0f; + + clientNum = cent->currentState.clientNum; + + if( cg_noPlayerAnims.integer ) + { + *legsOld = *legs = *torsoOld = *torso = 0; + return; + } + + ci = &cgs.clientinfo[ clientNum ]; + + // do the shuffle turn frames locally + if( cent->pe.legs.yawing && ( cent->currentState.legsAnim & ~ANIM_TOGGLEBIT ) == LEGS_IDLE ) + CG_RunPlayerLerpFrame( ci, ¢->pe.legs, LEGS_TURN, speedScale ); + else + CG_RunPlayerLerpFrame( ci, ¢->pe.legs, cent->currentState.legsAnim, speedScale ); + + *legsOld = cent->pe.legs.oldFrame; + *legs = cent->pe.legs.frame; + *legsBackLerp = cent->pe.legs.backlerp; + + CG_RunPlayerLerpFrame( ci, ¢->pe.torso, cent->currentState.torsoAnim, speedScale ); + + *torsoOld = cent->pe.torso.oldFrame; + *torso = cent->pe.torso.frame; + *torsoBackLerp = cent->pe.torso.backlerp; +} + + +/* +=============== +CG_PlayerNonSegAnimation +=============== +*/ +static void CG_PlayerNonSegAnimation( centity_t *cent, int *nonSegOld, + int *nonSeg, float *nonSegBackLerp ) +{ + clientInfo_t *ci; + int clientNum; + float speedScale = 1.0f; + + clientNum = cent->currentState.clientNum; + + if( cg_noPlayerAnims.integer ) + { + *nonSegOld = *nonSeg = 0; + return; + } + + ci = &cgs.clientinfo[ clientNum ]; + + // do the shuffle turn frames locally + if( cent->pe.nonseg.yawing && ( cent->currentState.legsAnim & ~ANIM_TOGGLEBIT ) == NSPA_STAND ) + CG_RunPlayerLerpFrame( ci, ¢->pe.nonseg, NSPA_TURN, speedScale ); + else + CG_RunPlayerLerpFrame( ci, ¢->pe.nonseg, cent->currentState.legsAnim, speedScale ); + + *nonSegOld = cent->pe.nonseg.oldFrame; + *nonSeg = cent->pe.nonseg.frame; + *nonSegBackLerp = cent->pe.nonseg.backlerp; +} + +/* +============================================================================= + +PLAYER ANGLES + +============================================================================= +*/ + +/* +================== +CG_SwingAngles +================== +*/ +static void CG_SwingAngles( float destination, float swingTolerance, float clampTolerance, + float speed, float *angle, qboolean *swinging ) +{ + float swing; + float move; + float scale; + + if( !*swinging ) + { + // see if a swing should be started + swing = AngleSubtract( *angle, destination ); + + if( swing > swingTolerance || swing < -swingTolerance ) + *swinging = qtrue; + } + + if( !*swinging ) + return; + + // modify the speed depending on the delta + // so it doesn't seem so linear + swing = AngleSubtract( destination, *angle ); + scale = fabs( swing ); + + if( scale < swingTolerance * 0.5 ) + scale = 0.5; + else if( scale < swingTolerance ) + scale = 1.0; + else + scale = 2.0; + + // swing towards the destination angle + if( swing >= 0 ) + { + move = cg.frametime * scale * speed; + + if( move >= swing ) + { + move = swing; + *swinging = qfalse; + } + *angle = AngleMod( *angle + move ); + } + else if( swing < 0 ) + { + move = cg.frametime * scale * -speed; + + if( move <= swing ) + { + move = swing; + *swinging = qfalse; + } + *angle = AngleMod( *angle + move ); + } + + // clamp to no more than tolerance + swing = AngleSubtract( destination, *angle ); + if( swing > clampTolerance ) + *angle = AngleMod( destination - ( clampTolerance - 1 ) ); + else if( swing < -clampTolerance ) + *angle = AngleMod( destination + ( clampTolerance - 1 ) ); +} + +/* +================= +CG_AddPainTwitch +================= +*/ +static void CG_AddPainTwitch( centity_t *cent, vec3_t torsoAngles ) +{ + int t; + float f; + + t = cg.time - cent->pe.painTime; + + if( t >= PAIN_TWITCH_TIME ) + return; + + f = 1.0 - (float)t / PAIN_TWITCH_TIME; + + if( cent->pe.painDirection ) + torsoAngles[ ROLL ] += 20 * f; + else + torsoAngles[ ROLL ] -= 20 * f; +} + + +/* +=============== +CG_PlayerAngles + +Handles seperate torso motion + + legs pivot based on direction of movement + + head always looks exactly at cent->lerpAngles + + if motion < 20 degrees, show in head only + if < 45 degrees, also show in torso +=============== +*/ +static void CG_PlayerAngles( centity_t *cent, vec3_t srcAngles, + vec3_t legs[ 3 ], vec3_t torso[ 3 ], vec3_t head[ 3 ] ) +{ + vec3_t legsAngles, torsoAngles, headAngles; + float dest; + static int movementOffsets[ 8 ] = { 0, 22, 45, -22, 0, 22, -45, -22 }; + vec3_t velocity; + float speed; + int dir, clientNum; + clientInfo_t *ci; + + VectorCopy( srcAngles, headAngles ); + headAngles[ YAW ] = AngleMod( headAngles[ YAW ] ); + VectorClear( legsAngles ); + VectorClear( torsoAngles ); + + // --------- yaw ------------- + + // allow yaw to drift a bit + if( ( cent->currentState.legsAnim & ~ANIM_TOGGLEBIT ) != LEGS_IDLE || + ( cent->currentState.torsoAnim & ~ANIM_TOGGLEBIT ) != TORSO_STAND ) + { + // if not standing still, always point all in the same direction + cent->pe.torso.yawing = qtrue; // always center + cent->pe.torso.pitching = qtrue; // always center + cent->pe.legs.yawing = qtrue; // always center + } + + // adjust legs for movement dir + if( cent->currentState.eFlags & EF_DEAD ) + { + // don't let dead bodies twitch + dir = 0; + } + else + { + // did use angles2.. now uses time2.. looks a bit funny but time2 isn't used othwise + dir = cent->currentState.time2; + if( dir < 0 || dir > 7 ) + CG_Error( "Bad player movement angle" ); + } + + legsAngles[ YAW ] = headAngles[ YAW ] + movementOffsets[ dir ]; + torsoAngles[ YAW ] = headAngles[ YAW ] + 0.25 * movementOffsets[ dir ]; + + // torso + if( cent->currentState.eFlags & EF_DEAD ) + { + CG_SwingAngles( torsoAngles[ YAW ], 0, 0, cg_swingSpeed.value, + ¢->pe.torso.yawAngle, ¢->pe.torso.yawing ); + CG_SwingAngles( legsAngles[ YAW ], 0, 0, cg_swingSpeed.value, + ¢->pe.legs.yawAngle, ¢->pe.legs.yawing ); + } + else + { + CG_SwingAngles( torsoAngles[ YAW ], 25, 90, cg_swingSpeed.value, + ¢->pe.torso.yawAngle, ¢->pe.torso.yawing ); + CG_SwingAngles( legsAngles[ YAW ], 40, 90, cg_swingSpeed.value, + ¢->pe.legs.yawAngle, ¢->pe.legs.yawing ); + } + + torsoAngles[ YAW ] = cent->pe.torso.yawAngle; + legsAngles[ YAW ] = cent->pe.legs.yawAngle; + + // --------- pitch ------------- + + // only show a fraction of the pitch angle in the torso + if( headAngles[ PITCH ] > 180 ) + dest = ( -360 + headAngles[ PITCH ] ) * 0.75f; + else + dest = headAngles[ PITCH ] * 0.75f; + + CG_SwingAngles( dest, 15, 30, 0.1f, ¢->pe.torso.pitchAngle, ¢->pe.torso.pitching ); + torsoAngles[ PITCH ] = cent->pe.torso.pitchAngle; + + // + clientNum = cent->currentState.clientNum; + + if( clientNum >= 0 && clientNum < MAX_CLIENTS ) + { + ci = &cgs.clientinfo[ clientNum ]; + if( ci->fixedtorso ) + torsoAngles[ PITCH ] = 0.0f; + } + + // --------- roll ------------- + + + // lean towards the direction of travel + VectorCopy( cent->currentState.pos.trDelta, velocity ); + speed = VectorNormalize( velocity ); + + if( speed ) + { + vec3_t axis[ 3 ]; + float side; + + speed *= 0.05f; + + AnglesToAxis( legsAngles, axis ); + side = speed * DotProduct( velocity, axis[ 1 ] ); + legsAngles[ ROLL ] -= side; + + side = speed * DotProduct( velocity, axis[ 0 ] ); + legsAngles[ PITCH ] += side; + } + + // + clientNum = cent->currentState.clientNum; + + if( clientNum >= 0 && clientNum < MAX_CLIENTS ) + { + ci = &cgs.clientinfo[ clientNum ]; + + if( ci->fixedlegs ) + { + legsAngles[ YAW ] = torsoAngles[ YAW ]; + legsAngles[ PITCH ] = 0.0f; + legsAngles[ ROLL ] = 0.0f; + } + } + + // pain twitch + CG_AddPainTwitch( cent, torsoAngles ); + + // pull the angles back out of the hierarchial chain + AnglesSubtract( headAngles, torsoAngles, headAngles ); + AnglesSubtract( torsoAngles, legsAngles, torsoAngles ); + AnglesToAxis( legsAngles, legs ); + AnglesToAxis( torsoAngles, torso ); + AnglesToAxis( headAngles, head ); +} + +#define MODEL_WWSMOOTHTIME 200 + +/* +=============== +CG_PlayerWWSmoothing + +Smooth the angles of transitioning wall walkers +=============== +*/ +static void CG_PlayerWWSmoothing( centity_t *cent, vec3_t in[ 3 ], vec3_t out[ 3 ] ) +{ + entityState_t *es = ¢->currentState; + int i; + vec3_t surfNormal, rotAxis, temp; + vec3_t refNormal = { 0.0f, 0.0f, 1.0f }; + vec3_t ceilingNormal = { 0.0f, 0.0f, -1.0f }; + float stLocal, sFraction, rotAngle; + vec3_t inAxis[ 3 ], lastAxis[ 3 ], outAxis[ 3 ]; + + //set surfNormal + if( !(es->eFlags & EF_WALLCLIMB ) ) + VectorCopy( refNormal, surfNormal ); + else if( !( es->eFlags & EF_WALLCLIMBCEILING ) ) + VectorCopy( es->angles2, surfNormal ); + else + VectorCopy( ceilingNormal, surfNormal ); + + AxisCopy( in, inAxis ); + + if( !VectorCompare( surfNormal, cent->pe.lastNormal ) ) + { + //if we moving from the ceiling to the floor special case + //( x product of colinear vectors is undefined) + if( VectorCompare( ceilingNormal, cent->pe.lastNormal ) && + VectorCompare( refNormal, surfNormal ) ) + { + VectorCopy( in[ 1 ], rotAxis ); + rotAngle = 180.0f; + } + else + { + AxisCopy( cent->pe.lastAxis, lastAxis ); + rotAngle = DotProduct( inAxis[ 0 ], lastAxis[ 0 ] ) + + DotProduct( inAxis[ 1 ], lastAxis[ 1 ] ) + + DotProduct( inAxis[ 2 ], lastAxis[ 2 ] ); + + rotAngle = RAD2DEG( acos( ( rotAngle - 1.0f ) / 2.0f ) ); + + CrossProduct( lastAxis[ 0 ], inAxis[ 0 ], temp ); + VectorCopy( temp, rotAxis ); + CrossProduct( lastAxis[ 1 ], inAxis[ 1 ], temp ); + VectorAdd( rotAxis, temp, rotAxis ); + CrossProduct( lastAxis[ 2 ], inAxis[ 2 ], temp ); + VectorAdd( rotAxis, temp, rotAxis ); + + VectorNormalize( rotAxis ); + } + + //iterate through smooth array + for( i = 0; i < MAXSMOOTHS; i++ ) + { + //found an unused index in the smooth array + if( cent->pe.sList[ i ].time + MODEL_WWSMOOTHTIME < cg.time ) + { + //copy to array and stop + VectorCopy( rotAxis, cent->pe.sList[ i ].rotAxis ); + cent->pe.sList[ i ].rotAngle = rotAngle; + cent->pe.sList[ i ].time = cg.time; + break; + } + } + } + + //iterate through ops + for( i = MAXSMOOTHS - 1; i >= 0; i-- ) + { + //if this op has time remaining, perform it + if( cg.time < cent->pe.sList[ i ].time + MODEL_WWSMOOTHTIME ) + { + stLocal = 1.0f - ( ( ( cent->pe.sList[ i ].time + MODEL_WWSMOOTHTIME ) - cg.time ) / MODEL_WWSMOOTHTIME ); + sFraction = -( cos( stLocal * M_PI ) + 1.0f ) / 2.0f; + + RotatePointAroundVector( outAxis[ 0 ], cent->pe.sList[ i ].rotAxis, + inAxis[ 0 ], sFraction * cent->pe.sList[ i ].rotAngle ); + RotatePointAroundVector( outAxis[ 1 ], cent->pe.sList[ i ].rotAxis, + inAxis[ 1 ], sFraction * cent->pe.sList[ i ].rotAngle ); + RotatePointAroundVector( outAxis[ 2 ], cent->pe.sList[ i ].rotAxis, + inAxis[ 2 ], sFraction * cent->pe.sList[ i ].rotAngle ); + + AxisCopy( outAxis, inAxis ); + } + } + + //outAxis has been copied to inAxis + AxisCopy( inAxis, out ); +} + +/* +=============== +CG_PlayerNonSegAngles + +Resolve angles for non-segmented models +=============== +*/ +static void CG_PlayerNonSegAngles( centity_t *cent, vec3_t srcAngles, vec3_t nonSegAxis[ 3 ] ) +{ + vec3_t localAngles; + vec3_t velocity; + float speed; + int dir; + entityState_t *es = ¢->currentState; + vec3_t surfNormal; + vec3_t ceilingNormal = { 0.0f, 0.0f, -1.0f }; + + VectorCopy( srcAngles, localAngles ); + localAngles[ YAW ] = AngleMod( localAngles[ YAW ] ); + localAngles[ PITCH ] = 0.0f; + localAngles[ ROLL ] = 0.0f; + + //set surfNormal + if( !( es->eFlags & EF_WALLCLIMBCEILING ) ) + VectorCopy( es->angles2, surfNormal ); + else + VectorCopy( ceilingNormal, surfNormal ); + + //make sure that WW transitions don't cause the swing stuff to go nuts + if( !VectorCompare( surfNormal, cent->pe.lastNormal ) ) + { + //stop CG_SwingAngles having an eppy + cent->pe.nonseg.yawAngle = localAngles[ YAW ]; + cent->pe.nonseg.yawing = qfalse; + } + + // --------- yaw ------------- + + // allow yaw to drift a bit + if( ( cent->currentState.legsAnim & ~ANIM_TOGGLEBIT ) != NSPA_STAND ) + { + // if not standing still, always point all in the same direction + cent->pe.nonseg.yawing = qtrue; // always center + } + + // adjust legs for movement dir + if( cent->currentState.eFlags & EF_DEAD ) + { + // don't let dead bodies twitch + dir = 0; + } + else + { + // did use angles2.. now uses time2.. looks a bit funny but time2 isn't used othwise + dir = cent->currentState.time2; + if( dir < 0 || dir > 7 ) + CG_Error( "Bad player movement angle" ); + } + + // torso + if( cent->currentState.eFlags & EF_DEAD ) + { + CG_SwingAngles( localAngles[ YAW ], 0, 0, cg_swingSpeed.value, + ¢->pe.nonseg.yawAngle, ¢->pe.nonseg.yawing ); + } + else + { + CG_SwingAngles( localAngles[ YAW ], 40, 90, cg_swingSpeed.value, + ¢->pe.nonseg.yawAngle, ¢->pe.nonseg.yawing ); + } + + localAngles[ YAW ] = cent->pe.nonseg.yawAngle; + + // --------- pitch ------------- + + //NO PITCH! + + + // --------- roll ------------- + + + // lean towards the direction of travel + VectorCopy( cent->currentState.pos.trDelta, velocity ); + speed = VectorNormalize( velocity ); + + if( speed ) + { + vec3_t axis[ 3 ]; + float side; + + //much less than with the regular model system + speed *= 0.01f; + + AnglesToAxis( localAngles, axis ); + side = speed * DotProduct( velocity, axis[ 1 ] ); + localAngles[ ROLL ] -= side; + + side = speed * DotProduct( velocity, axis[ 0 ] ); + localAngles[ PITCH ] += side; + } + + //FIXME: PAIN[123] animations? + // pain twitch + //CG_AddPainTwitch( cent, torsoAngles ); + + AnglesToAxis( localAngles, nonSegAxis ); +} + + +//========================================================================== + +/* +=============== +CG_PlayerUpgrade +=============== +*/ +static void CG_PlayerUpgrades( centity_t *cent, refEntity_t *torso ) +{ + int held, active; + refEntity_t jetpack; + refEntity_t battpack; + refEntity_t flash; + entityState_t *es = ¢->currentState; + + held = es->modelindex; + active = es->modelindex2; + + if( held & ( 1 << UP_JETPACK ) ) + { + memset( &jetpack, 0, sizeof( jetpack ) ); + VectorCopy( torso->lightingOrigin, jetpack.lightingOrigin ); + jetpack.shadowPlane = torso->shadowPlane; + jetpack.renderfx = torso->renderfx; + + jetpack.hModel = cgs.media.jetpackModel; + + //identity matrix + AxisCopy( axisDefault, jetpack.axis ); + + //FIXME: change to tag_back when it exists + CG_PositionRotatedEntityOnTag( &jetpack, torso, torso->hModel, "tag_head" ); + + trap_R_AddRefEntityToScene( &jetpack ); + + if( active & ( 1 << UP_JETPACK ) ) + { + if( es->pos.trDelta[ 2 ] > 10.0f ) + { + if( cent->jetPackState != JPS_ASCENDING ) + { + if( CG_IsParticleSystemValid( ¢->jetPackPS ) ) + CG_DestroyParticleSystem( ¢->jetPackPS ); + + cent->jetPackPS = CG_SpawnNewParticleSystem( cgs.media.jetPackAscendPS ); + cent->jetPackState = JPS_ASCENDING; + } + + trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, + vec3_origin, cgs.media.jetpackAscendSound ); + } + else if( es->pos.trDelta[ 2 ] < -10.0f ) + { + if( cent->jetPackState != JPS_DESCENDING ) + { + if( CG_IsParticleSystemValid( ¢->jetPackPS ) ) + CG_DestroyParticleSystem( ¢->jetPackPS ); + + cent->jetPackPS = CG_SpawnNewParticleSystem( cgs.media.jetPackDescendPS ); + cent->jetPackState = JPS_DESCENDING; + } + + trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, + vec3_origin, cgs.media.jetpackDescendSound ); + } + else + { + if( cent->jetPackState != JPS_HOVERING ) + { + if( CG_IsParticleSystemValid( ¢->jetPackPS ) ) + CG_DestroyParticleSystem( ¢->jetPackPS ); + + cent->jetPackPS = CG_SpawnNewParticleSystem( cgs.media.jetPackHoverPS ); + cent->jetPackState = JPS_HOVERING; + } + + trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, + vec3_origin, cgs.media.jetpackIdleSound ); + } + + memset( &flash, 0, sizeof( flash ) ); + VectorCopy( torso->lightingOrigin, flash.lightingOrigin ); + flash.shadowPlane = torso->shadowPlane; + flash.renderfx = torso->renderfx; + + flash.hModel = cgs.media.jetpackFlashModel; + if( !flash.hModel ) + return; + + AxisCopy( axisDefault, flash.axis ); + + CG_PositionRotatedEntityOnTag( &flash, &jetpack, jetpack.hModel, "tag_flash" ); + trap_R_AddRefEntityToScene( &flash ); + + if( CG_IsParticleSystemValid( ¢->jetPackPS ) ) + { + CG_SetAttachmentTag( ¢->jetPackPS->attachment, + jetpack, jetpack.hModel, "tag_flash" ); + CG_SetAttachmentCent( ¢->jetPackPS->attachment, cent ); + CG_AttachToTag( ¢->jetPackPS->attachment ); + } + } + else if( CG_IsParticleSystemValid( ¢->jetPackPS ) ) + { + CG_DestroyParticleSystem( ¢->jetPackPS ); + cent->jetPackState = JPS_OFF; + } + } + else if( CG_IsParticleSystemValid( ¢->jetPackPS ) ) + { + CG_DestroyParticleSystem( ¢->jetPackPS ); + cent->jetPackState = JPS_OFF; + } + + if( held & ( 1 << UP_BATTPACK ) ) + { + memset( &battpack, 0, sizeof( battpack ) ); + VectorCopy( torso->lightingOrigin, battpack.lightingOrigin ); + battpack.shadowPlane = torso->shadowPlane; + battpack.renderfx = torso->renderfx; + + battpack.hModel = cgs.media.battpackModel; + + //identity matrix + AxisCopy( axisDefault, battpack.axis ); + + //FIXME: change to tag_back when it exists + CG_PositionRotatedEntityOnTag( &battpack, torso, torso->hModel, "tag_head" ); + + trap_R_AddRefEntityToScene( &battpack ); + } + + if( es->eFlags & EF_BLOBLOCKED ) + { + vec3_t temp, origin, up = { 0.0f, 0.0f, 1.0f }; + trace_t tr; + float size; + + VectorCopy( es->pos.trBase, temp ); + temp[ 2 ] -= 4096.0f; + + CG_Trace( &tr, es->pos.trBase, NULL, NULL, temp, es->number, MASK_SOLID ); + VectorCopy( tr.endpos, origin ); + + size = 32.0f; + + if( size > 0.0f ) + CG_ImpactMark( cgs.media.creepShader, origin, up, + 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, qfalse, size, qtrue ); + } +} + + +/* +=============== +CG_PlayerFloatSprite + +Float a sprite over the player's head +=============== +*/ +static void CG_PlayerFloatSprite( centity_t *cent, qhandle_t shader ) +{ + int rf; + refEntity_t ent; + + if( cent->currentState.number == cg.snap->ps.clientNum && !cg.renderingThirdPerson ) + rf = RF_THIRD_PERSON; // only show in mirrors + else + rf = 0; + + memset( &ent, 0, sizeof( ent ) ); + VectorCopy( cent->lerpOrigin, ent.origin ); + ent.origin[ 2 ] += 48; + ent.reType = RT_SPRITE; + ent.customShader = shader; + ent.radius = 10; + ent.renderfx = rf; + ent.shaderRGBA[ 0 ] = 255; + ent.shaderRGBA[ 1 ] = 255; + ent.shaderRGBA[ 2 ] = 255; + ent.shaderRGBA[ 3 ] = 255; + trap_R_AddRefEntityToScene( &ent ); +} + + + +/* +=============== +CG_PlayerSprites + +Float sprites over the player's head +=============== +*/ +static void CG_PlayerSprites( centity_t *cent ) +{ + if( cent->currentState.eFlags & EF_CONNECTION ) + { + CG_PlayerFloatSprite( cent, cgs.media.connectionShader ); + return; + } + + if( cent->currentState.eFlags & EF_TALK ) + { + // the masses have decreed this to be wrong +/* CG_PlayerFloatSprite( cent, cgs.media.balloonShader ); + return;*/ + } +} + +/* +=============== +CG_PlayerShadow + +Returns the Z component of the surface being shadowed + + should it return a full plane instead of a Z? +=============== +*/ +#define SHADOW_DISTANCE 128 +static qboolean CG_PlayerShadow( centity_t *cent, float *shadowPlane, pClass_t class ) +{ + vec3_t end, mins, maxs; + trace_t trace; + float alpha; + entityState_t *es = ¢->currentState; + vec3_t surfNormal = { 0.0f, 0.0f, 1.0f }; + + BG_FindBBoxForClass( class, mins, maxs, NULL, NULL, NULL ); + mins[ 2 ] = 0.0f; + maxs[ 2 ] = 2.0f; + + if( es->eFlags & EF_WALLCLIMB ) + { + if( es->eFlags & EF_WALLCLIMBCEILING ) + VectorSet( surfNormal, 0.0f, 0.0f, -1.0f ); + else + VectorCopy( es->angles2, surfNormal ); + } + + *shadowPlane = 0; + + if( cg_shadows.integer == 0 ) + return qfalse; + + // send a trace down from the player to the ground + VectorCopy( cent->lerpOrigin, end ); + VectorMA( cent->lerpOrigin, -SHADOW_DISTANCE, surfNormal, end ); + + trap_CM_BoxTrace( &trace, cent->lerpOrigin, end, mins, maxs, 0, MASK_PLAYERSOLID ); + + // no shadow if too high + if( trace.fraction == 1.0 || trace.startsolid || trace.allsolid ) + return qfalse; + + // FIXME: stencil shadows will be broken for walls. + // Unfortunately there isn't much that can be + // done since Q3 references only the Z coord + // of the shadowPlane + if( surfNormal[ 2 ] < 0.0f ) + *shadowPlane = trace.endpos[ 2 ] - 1.0f; + else + *shadowPlane = trace.endpos[ 2 ] + 1.0f; + + if( cg_shadows.integer != 1 ) // no mark for stencil or projection shadows + return qtrue; + + // fade the shadow out with height + alpha = 1.0 - trace.fraction; + + // add the mark as a temporary, so it goes directly to the renderer + // without taking a spot in the cg_marks array + CG_ImpactMark( cgs.media.shadowMarkShader, trace.endpos, trace.plane.normal, + cent->pe.legs.yawAngle, 0.0f, 0.0f, 0.0f, alpha, qfalse, + 24.0f * BG_FindShadowScaleForClass( class ), qtrue ); + + return qtrue; +} + + +/* +=============== +CG_PlayerSplash + +Draw a mark at the water surface +=============== +*/ +static void CG_PlayerSplash( centity_t *cent, pClass_t class ) +{ + vec3_t start, end; + vec3_t mins, maxs; + trace_t trace; + int contents; + + if( !cg_shadows.integer ) + return; + + BG_FindBBoxForClass( class, mins, maxs, NULL, NULL, NULL ); + + VectorCopy( cent->lerpOrigin, end ); + end[ 2 ] += mins[ 2 ]; + + // if the feet aren't in liquid, don't make a mark + // this won't handle moving water brushes, but they wouldn't draw right anyway... + contents = trap_CM_PointContents( end, 0 ); + + if( !( contents & ( CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA ) ) ) + return; + + VectorCopy( cent->lerpOrigin, start ); + start[ 2 ] += 32; + + // if the head isn't out of liquid, don't make a mark + contents = trap_CM_PointContents( start, 0 ); + + if( contents & ( CONTENTS_SOLID | CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA ) ) + return; + + // trace down to find the surface + trap_CM_BoxTrace( &trace, start, end, NULL, NULL, 0, + ( CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA ) ); + + if( trace.fraction == 1.0f ) + return; + + CG_ImpactMark( cgs.media.wakeMarkShader, trace.endpos, trace.plane.normal, + cent->pe.legs.yawAngle, 1.0f, 1.0f, 1.0f, 1.0f, qfalse, + 32.0f * BG_FindShadowScaleForClass( class ), qtrue ); +} + + +/* +================= +CG_LightVerts +================= +*/ +int CG_LightVerts( vec3_t normal, int numVerts, polyVert_t *verts ) +{ + int i, j; + float incoming; + vec3_t ambientLight; + vec3_t lightDir; + vec3_t directedLight; + + trap_R_LightForPoint( verts[ 0 ].xyz, ambientLight, directedLight, lightDir ); + + for( i = 0; i < numVerts; i++ ) + { + incoming = DotProduct( normal, lightDir ); + + if( incoming <= 0 ) + { + verts[ i ].modulate[ 0 ] = ambientLight[ 0 ]; + verts[ i ].modulate[ 1 ] = ambientLight[ 1 ]; + verts[ i ].modulate[ 2 ] = ambientLight[ 2 ]; + verts[ i ].modulate[ 3 ] = 255; + continue; + } + + j = ( ambientLight[ 0 ] + incoming * directedLight[ 0 ] ); + + if( j > 255 ) + j = 255; + + verts[ i ].modulate[ 0 ] = j; + + j = ( ambientLight[ 1 ] + incoming * directedLight[ 1 ] ); + + if( j > 255 ) + j = 255; + + verts[ i ].modulate[ 1 ] = j; + + j = ( ambientLight[ 2 ] + incoming * directedLight[ 2 ] ); + + if( j > 255 ) + j = 255; + + verts[ i ].modulate[ 2 ] = j; + + verts[ i ].modulate[ 3 ] = 255; + } + return qtrue; +} + + +/* +================= +CG_LightFromDirection +================= +*/ +int CG_LightFromDirection( vec3_t point, vec3_t direction ) +{ + int j; + float incoming; + vec3_t ambientLight; + vec3_t lightDir; + vec3_t directedLight; + vec3_t result; + + trap_R_LightForPoint( point, ambientLight, directedLight, lightDir ); + + incoming = DotProduct( direction, lightDir ); + + if( incoming <= 0 ) + { + result[ 0 ] = ambientLight[ 0 ]; + result[ 1 ] = ambientLight[ 1 ]; + result[ 2 ] = ambientLight[ 2 ]; + return (int)( (float)( result[ 0 ] + result[ 1 ] + result[ 2 ] ) / 3.0f ); + } + + j = ( ambientLight[ 0 ] + incoming * directedLight[ 0 ] ); + + if( j > 255 ) + j = 255; + + result[ 0 ] = j; + + j = ( ambientLight[ 1 ] + incoming * directedLight[ 1 ] ); + + if( j > 255 ) + j = 255; + + result[ 1 ] = j; + + j = ( ambientLight[ 2 ] + incoming * directedLight[ 2 ] ); + + if( j > 255 ) + j = 255; + + result[ 2 ] = j; + + return (int)((float)( result[ 0 ] + result[ 1 ] + result[ 2 ] ) / 3.0f ); +} + + +/* +================= +CG_AmbientLight +================= +*/ +int CG_AmbientLight( vec3_t point ) +{ + vec3_t ambientLight; + vec3_t lightDir; + vec3_t directedLight; + vec3_t result; + + trap_R_LightForPoint( point, ambientLight, directedLight, lightDir ); + + result[ 0 ] = ambientLight[ 0 ]; + result[ 1 ] = ambientLight[ 1 ]; + result[ 2 ] = ambientLight[ 2 ]; + return (int)((float)( result[ 0 ] + result[ 1 ] + result[ 2 ] ) / 3.0f ); +} + +#define TRACE_DEPTH 32.0f + +/* +=============== +CG_Player +=============== +*/ +void CG_Player( centity_t *cent ) +{ + clientInfo_t *ci; + + // NOTE: legs is used for nonsegmented models + // this helps reduce code to be changed + refEntity_t legs; + refEntity_t torso; + refEntity_t head; + int clientNum; + int renderfx; + qboolean shadow = qfalse; + float shadowPlane = 0.0f; + entityState_t *es = ¢->currentState; + pClass_t class = ( es->misc >> 8 ) & 0xFF; + float scale; + vec3_t tempAxis[ 3 ], tempAxis2[ 3 ]; + vec3_t angles; + int held = es->modelindex; + vec3_t surfNormal = { 0.0f, 0.0f, 1.0f }; + + // the client number is stored in clientNum. It can't be derived + // from the entity number, because a single client may have + // multiple corpses on the level using the same clientinfo + clientNum = es->clientNum; + if( clientNum < 0 || clientNum >= MAX_CLIENTS ) + CG_Error( "Bad clientNum on player entity" ); + + ci = &cgs.clientinfo[ clientNum ]; + + // it is possible to see corpses from disconnected players that may + // not have valid clientinfo + if( !ci->infoValid ) + return; + + //don't draw + if( es->eFlags & EF_NODRAW ) + return; + + // get the player model information + renderfx = 0; + if( es->number == cg.snap->ps.clientNum ) + { + if( !cg.renderingThirdPerson ) + renderfx = RF_THIRD_PERSON; // only draw in mirrors + else if( cg_cameraMode.integer ) + return; + } + + if( cg_drawBBOX.integer ) + { + vec3_t mins, maxs; + + BG_FindBBoxForClass( class, mins, maxs, NULL, NULL, NULL ); + CG_DrawBoundingBox( cent->lerpOrigin, mins, maxs ); + } + + memset( &legs, 0, sizeof( legs ) ); + memset( &torso, 0, sizeof( torso ) ); + memset( &head, 0, sizeof( head ) ); + + VectorCopy( cent->lerpAngles, angles ); + AnglesToAxis( cent->lerpAngles, tempAxis ); + + //rotate lerpAngles to floor + if( es->eFlags & EF_WALLCLIMB && + BG_RotateAxis( es->angles2, tempAxis, tempAxis2, qtrue, es->eFlags & EF_WALLCLIMBCEILING ) ) + AxisToAngles( tempAxis2, angles ); + else + VectorCopy( cent->lerpAngles, angles ); + + //normalise the pitch + if( angles[ PITCH ] < -180.0f ) + angles[ PITCH ] += 360.0f; + + // get the rotation information + if( !ci->nonsegmented ) + CG_PlayerAngles( cent, angles, legs.axis, torso.axis, head.axis ); + else + CG_PlayerNonSegAngles( cent, angles, legs.axis ); + + AxisCopy( legs.axis, tempAxis ); + + //rotate the legs axis to back to the wall + if( es->eFlags & EF_WALLCLIMB && + BG_RotateAxis( es->angles2, legs.axis, tempAxis, qfalse, es->eFlags & EF_WALLCLIMBCEILING ) ) + AxisCopy( tempAxis, legs.axis ); + + //smooth out WW transitions so the model doesn't hop around + CG_PlayerWWSmoothing( cent, legs.axis, legs.axis ); + + AxisCopy( tempAxis, cent->pe.lastAxis ); + + // get the animation state (after rotation, to allow feet shuffle) + if( !ci->nonsegmented ) + CG_PlayerAnimation( cent, &legs.oldframe, &legs.frame, &legs.backlerp, + &torso.oldframe, &torso.frame, &torso.backlerp ); + else + CG_PlayerNonSegAnimation( cent, &legs.oldframe, &legs.frame, &legs.backlerp ); + + // add the talk baloon or disconnect icon + CG_PlayerSprites( cent ); + + // add the shadow + if( ( es->number == cg.snap->ps.clientNum && cg.renderingThirdPerson ) || + es->number != cg.snap->ps.clientNum ) + shadow = CG_PlayerShadow( cent, &shadowPlane, class ); + + // add a water splash if partially in and out of water + CG_PlayerSplash( cent, class ); + + if( cg_shadows.integer == 3 && shadow ) + renderfx |= RF_SHADOW_PLANE; + + renderfx |= RF_LIGHTING_ORIGIN; // use the same origin for all + + // + // add the legs + // + if( !ci->nonsegmented ) + { + legs.hModel = ci->legsModel; + + if( held & ( 1 << UP_LIGHTARMOUR ) ) + legs.customSkin = cgs.media.larmourLegsSkin; + else + legs.customSkin = ci->legsSkin; + } + else + { + legs.hModel = ci->nonSegModel; + legs.customSkin = ci->nonSegSkin; + } + + VectorCopy( cent->lerpOrigin, legs.origin ); + + VectorCopy( cent->lerpOrigin, legs.lightingOrigin ); + legs.shadowPlane = shadowPlane; + legs.renderfx = renderfx; + VectorCopy( legs.origin, legs.oldorigin ); // don't positionally lerp at all + + //move the origin closer into the wall with a CapTrace + if( es->eFlags & EF_WALLCLIMB && !( es->eFlags & EF_DEAD ) && !( cg.intermissionStarted ) ) + { + vec3_t start, end, mins, maxs; + trace_t tr; + + if( es->eFlags & EF_WALLCLIMBCEILING ) + VectorSet( surfNormal, 0.0f, 0.0f, -1.0f ); + else + VectorCopy( es->angles2, surfNormal ); + + BG_FindBBoxForClass( class, mins, maxs, NULL, NULL, NULL ); + + VectorMA( legs.origin, -TRACE_DEPTH, surfNormal, end ); + VectorMA( legs.origin, 1.0f, surfNormal, start ); + CG_CapTrace( &tr, start, mins, maxs, end, es->number, MASK_PLAYERSOLID ); + + //if the trace misses completely then just use legs.origin + //apparently capsule traces are "smaller" than box traces + if( tr.fraction != 1.0f ) + VectorMA( legs.origin, tr.fraction * -TRACE_DEPTH, surfNormal, legs.origin ); + + VectorCopy( legs.origin, legs.lightingOrigin ); + VectorCopy( legs.origin, legs.oldorigin ); // don't positionally lerp at all + } + + //rescale the model + scale = BG_FindModelScaleForClass( class ); + + if( scale != 1.0f ) + { + VectorScale( legs.axis[ 0 ], scale, legs.axis[ 0 ] ); + VectorScale( legs.axis[ 1 ], scale, legs.axis[ 1 ] ); + VectorScale( legs.axis[ 2 ], scale, legs.axis[ 2 ] ); + + legs.nonNormalizedAxes = qtrue; + } + + //offset on the Z axis if required + VectorMA( legs.origin, BG_FindZOffsetForClass( class ), surfNormal, legs.origin ); + VectorCopy( legs.origin, legs.lightingOrigin ); + VectorCopy( legs.origin, legs.oldorigin ); // don't positionally lerp at all + + trap_R_AddRefEntityToScene( &legs ); + + // if the model failed, allow the default nullmodel to be displayed + if( !legs.hModel ) + return; + + if( !ci->nonsegmented ) + { + // + // add the torso + // + torso.hModel = ci->torsoModel; + + if( held & ( 1 << UP_LIGHTARMOUR ) ) + torso.customSkin = cgs.media.larmourTorsoSkin; + else + torso.customSkin = ci->torsoSkin; + + if( !torso.hModel ) + return; + + VectorCopy( cent->lerpOrigin, torso.lightingOrigin ); + + CG_PositionRotatedEntityOnTag( &torso, &legs, ci->legsModel, "tag_torso" ); + + torso.shadowPlane = shadowPlane; + torso.renderfx = renderfx; + + trap_R_AddRefEntityToScene( &torso ); + + // + // add the head + // + head.hModel = ci->headModel; + + if( held & ( 1 << UP_HELMET ) ) + head.customSkin = cgs.media.larmourHeadSkin; + else + head.customSkin = ci->headSkin; + + if( !head.hModel ) + return; + + VectorCopy( cent->lerpOrigin, head.lightingOrigin ); + + CG_PositionRotatedEntityOnTag( &head, &torso, ci->torsoModel, "tag_head" ); + + head.shadowPlane = shadowPlane; + head.renderfx = renderfx; + + trap_R_AddRefEntityToScene( &head ); + } + + // + // add the gun / barrel / flash + // + if( es->weapon != WP_NONE ) + { + if( !ci->nonsegmented ) + CG_AddPlayerWeapon( &torso, NULL, cent ); + else + CG_AddPlayerWeapon( &legs, NULL, cent ); + } + + CG_PlayerUpgrades( cent, &torso ); + + //sanity check that particle systems are stopped when dead + if( es->eFlags & EF_DEAD ) + { + if( CG_IsParticleSystemValid( ¢->muzzlePS ) ) + CG_DestroyParticleSystem( ¢->muzzlePS ); + + if( CG_IsParticleSystemValid( ¢->jetPackPS ) ) + CG_DestroyParticleSystem( ¢->jetPackPS ); + } + + VectorCopy( surfNormal, cent->pe.lastNormal ); +} + +/* +=============== +CG_Corpse +=============== +*/ +void CG_Corpse( centity_t *cent ) +{ + clientInfo_t *ci; + refEntity_t legs; + refEntity_t torso; + refEntity_t head; + entityState_t *es = ¢->currentState; + int corpseNum; + int renderfx; + qboolean shadow = qfalse; + float shadowPlane; + vec3_t origin, liveZ, deadZ; + float scale; + + corpseNum = CG_GetCorpseNum( es->clientNum ); + + if( corpseNum < 0 || corpseNum >= MAX_CLIENTS ) + CG_Error( "Bad corpseNum on corpse entity: %d", corpseNum ); + + ci = &cgs.corpseinfo[ corpseNum ]; + + // it is possible to see corpses from disconnected players that may + // not have valid clientinfo + if( !ci->infoValid ) + return; + + memset( &legs, 0, sizeof( legs ) ); + memset( &torso, 0, sizeof( torso ) ); + memset( &head, 0, sizeof( head ) ); + + VectorCopy( cent->lerpOrigin, origin ); + BG_FindBBoxForClass( es->clientNum, liveZ, NULL, NULL, deadZ, NULL ); + origin[ 2 ] -= ( liveZ[ 2 ] - deadZ[ 2 ] ); + + VectorCopy( es->angles, cent->lerpAngles ); + + // get the rotation information + if( !ci->nonsegmented ) + CG_PlayerAngles( cent, cent->lerpAngles, legs.axis, torso.axis, head.axis ); + else + CG_PlayerNonSegAngles( cent, cent->lerpAngles, legs.axis ); + + //set the correct frame (should always be dead) + if( cg_noPlayerAnims.integer ) + legs.oldframe = legs.frame = torso.oldframe = torso.frame = 0; + else if( !ci->nonsegmented ) + { + memset( ¢->pe.legs, 0, sizeof( lerpFrame_t ) ); + CG_RunPlayerLerpFrame( ci, ¢->pe.legs, es->legsAnim, 1 ); + legs.oldframe = cent->pe.legs.oldFrame; + legs.frame = cent->pe.legs.frame; + legs.backlerp = cent->pe.legs.backlerp; + + memset( ¢->pe.torso, 0, sizeof( lerpFrame_t ) ); + CG_RunPlayerLerpFrame( ci, ¢->pe.torso, es->torsoAnim, 1 ); + torso.oldframe = cent->pe.torso.oldFrame; + torso.frame = cent->pe.torso.frame; + torso.backlerp = cent->pe.torso.backlerp; + } + else + { + memset( ¢->pe.nonseg, 0, sizeof( lerpFrame_t ) ); + CG_RunPlayerLerpFrame( ci, ¢->pe.nonseg, es->legsAnim, 1 ); + legs.oldframe = cent->pe.nonseg.oldFrame; + legs.frame = cent->pe.nonseg.frame; + legs.backlerp = cent->pe.nonseg.backlerp; + } + + // add the shadow + shadow = CG_PlayerShadow( cent, &shadowPlane, es->clientNum ); + + // get the player model information + renderfx = 0; + + if( cg_shadows.integer == 3 && shadow ) + renderfx |= RF_SHADOW_PLANE; + + renderfx |= RF_LIGHTING_ORIGIN; // use the same origin for all + + // + // add the legs + // + if( !ci->nonsegmented ) + { + legs.hModel = ci->legsModel; + legs.customSkin = ci->legsSkin; + } + else + { + legs.hModel = ci->nonSegModel; + legs.customSkin = ci->nonSegSkin; + } + + VectorCopy( origin, legs.origin ); + + VectorCopy( origin, legs.lightingOrigin ); + legs.shadowPlane = shadowPlane; + legs.renderfx = renderfx; + legs.origin[ 2 ] += BG_FindZOffsetForClass( es->clientNum ); + VectorCopy( legs.origin, legs.oldorigin ); // don't positionally lerp at all + + //rescale the model + scale = BG_FindModelScaleForClass( es->clientNum ); + + if( scale != 1.0f ) + { + VectorScale( legs.axis[ 0 ], scale, legs.axis[ 0 ] ); + VectorScale( legs.axis[ 1 ], scale, legs.axis[ 1 ] ); + VectorScale( legs.axis[ 2 ], scale, legs.axis[ 2 ] ); + + legs.nonNormalizedAxes = qtrue; + } + + //CG_AddRefEntityWithPowerups( &legs, es->misc, ci->team ); + trap_R_AddRefEntityToScene( &legs ); + + // if the model failed, allow the default nullmodel to be displayed + if( !legs.hModel ) + return; + + if( !ci->nonsegmented ) + { + // + // add the torso + // + torso.hModel = ci->torsoModel; + if( !torso.hModel ) + return; + + torso.customSkin = ci->torsoSkin; + + VectorCopy( origin, torso.lightingOrigin ); + + CG_PositionRotatedEntityOnTag( &torso, &legs, ci->legsModel, "tag_torso" ); + + torso.shadowPlane = shadowPlane; + torso.renderfx = renderfx; + + //CG_AddRefEntityWithPowerups( &torso, es->misc, ci->team ); + trap_R_AddRefEntityToScene( &torso ); + + // + // add the head + // + head.hModel = ci->headModel; + if( !head.hModel ) + return; + + head.customSkin = ci->headSkin; + + VectorCopy( origin, head.lightingOrigin ); + + CG_PositionRotatedEntityOnTag( &head, &torso, ci->torsoModel, "tag_head"); + + head.shadowPlane = shadowPlane; + head.renderfx = renderfx; + + //CG_AddRefEntityWithPowerups( &head, es->misc, ci->team ); + trap_R_AddRefEntityToScene( &head ); + } +} + + +//===================================================================== + +/* +=============== +CG_ResetPlayerEntity + +A player just came into view or teleported, so reset all animation info +=============== +*/ +void CG_ResetPlayerEntity( centity_t *cent ) +{ + cent->errorTime = -99999; // guarantee no error decay added + cent->extrapolated = qfalse; + + CG_ClearLerpFrame( &cgs.clientinfo[ cent->currentState.clientNum ], + ¢->pe.legs, cent->currentState.legsAnim ); + CG_ClearLerpFrame( &cgs.clientinfo[ cent->currentState.clientNum ], + ¢->pe.torso, cent->currentState.torsoAnim ); + CG_ClearLerpFrame( &cgs.clientinfo[ cent->currentState.clientNum ], + ¢->pe.nonseg, cent->currentState.legsAnim ); + + BG_EvaluateTrajectory( ¢->currentState.pos, cg.time, cent->lerpOrigin ); + BG_EvaluateTrajectory( ¢->currentState.apos, cg.time, cent->lerpAngles ); + + VectorCopy( cent->lerpOrigin, cent->rawOrigin ); + VectorCopy( cent->lerpAngles, cent->rawAngles ); + + memset( ¢->pe.legs, 0, sizeof( cent->pe.legs ) ); + cent->pe.legs.yawAngle = cent->rawAngles[ YAW ]; + cent->pe.legs.yawing = qfalse; + cent->pe.legs.pitchAngle = 0; + cent->pe.legs.pitching = qfalse; + + memset( ¢->pe.torso, 0, sizeof( cent->pe.legs ) ); + cent->pe.torso.yawAngle = cent->rawAngles[ YAW ]; + cent->pe.torso.yawing = qfalse; + cent->pe.torso.pitchAngle = cent->rawAngles[ PITCH ]; + cent->pe.torso.pitching = qfalse; + + memset( ¢->pe.nonseg, 0, sizeof( cent->pe.nonseg ) ); + cent->pe.nonseg.yawAngle = cent->rawAngles[ YAW ]; + cent->pe.nonseg.yawing = qfalse; + cent->pe.nonseg.pitchAngle = cent->rawAngles[ PITCH ]; + cent->pe.nonseg.pitching = qfalse; + + if( cg_debugPosition.integer ) + CG_Printf( "%i ResetPlayerEntity yaw=%i\n", cent->currentState.number, cent->pe.torso.yawAngle ); +} + +/* +================== +CG_PlayerDisconnect + +Player disconnecting +================== +*/ +void CG_PlayerDisconnect( vec3_t org ) +{ + particleSystem_t *ps; + + trap_S_StartSound( org, ENTITYNUM_WORLD, CHAN_AUTO, cgs.media.disconnectSound ); + + ps = CG_SpawnNewParticleSystem( cgs.media.disconnectPS ); + + if( CG_IsParticleSystemValid( &ps ) ) + { + CG_SetAttachmentPoint( &ps->attachment, org ); + CG_AttachToPoint( &ps->attachment ); + } +} + +/* +================= +CG_Bleed + +This is the spurt of blood when a character gets hit +================= +*/ +void CG_Bleed( vec3_t origin, vec3_t normal, int entityNum ) +{ + pTeam_t team = cgs.clientinfo[ entityNum ].team; + qhandle_t bleedPS; + particleSystem_t *ps; + + if( !cg_blood.integer ) + return; + + if( team == PTE_ALIENS ) + bleedPS = cgs.media.alienBleedPS; + else if( team == PTE_HUMANS ) + bleedPS = cgs.media.humanBleedPS; + else + return; + + ps = CG_SpawnNewParticleSystem( bleedPS ); + + if( CG_IsParticleSystemValid( &ps ) ) + { + CG_SetAttachmentPoint( &ps->attachment, origin ); + CG_SetAttachmentCent( &ps->attachment, &cg_entities[ entityNum ] ); + CG_AttachToPoint( &ps->attachment ); + + CG_SetParticleSystemNormal( ps, normal ); + } +} + +/* +=============== +CG_AtHighestClass + +Is the local client at the highest class possible? +=============== +*/ +qboolean CG_AtHighestClass( void ) +{ + int i; + qboolean superiorClasses = qfalse; + + for( i = PCL_NONE + 1; i < PCL_NUM_CLASSES; i++ ) + { + if( BG_ClassCanEvolveFromTo( + cg.predictedPlayerState.stats[ STAT_PCLASS ], i, + ALIEN_MAX_KILLS, 0 ) >= 0 && + BG_FindStagesForClass( i, cgs.alienStage ) && + BG_ClassIsAllowed( i ) ) + { + superiorClasses = qtrue; + break; + } + } + + return !superiorClasses; +} + -- cgit