// // cg_players.c -- handle the media and animation for player entities #include "cg_local.h" #include "cg_screenfx.h" #include "fx_local.h" const char *cg_customSoundNames[MAX_CUSTOM_SOUNDS] = { "*death1.wav", "*death2.wav", "*death3.wav", "*jump1.wav", "*pain25.wav", "*pain50.wav", "*pain75.wav", "*pain100.wav", "*falling1.wav", "*gasp.wav", "*drown.wav", "*fall1.wav", "*taunt1.wav" }; int timeParam; int entNum; extern char* BG_RegisterRace( const char *name ); /* ================ CG_CustomSound ================ */ sfxHandle_t CG_CustomSound( int clientNum, const char *soundName ) { clientInfo_t *ci; int i; if ( soundName[0] != '*' ) { return trap_S_RegisterSound( soundName ); } 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/players2/munro/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 ); 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; Q_strncpyz(ci->soundPath, ci->modelName, sizeof(ci->soundPath)); // 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, "borg" ) ) { ci->footsteps = FOOTSTEP_BORG; } else if ( !Q_stricmp( token, "reaver" ) ) { ci->footsteps = FOOTSTEP_REAVER; } else if ( !Q_stricmp( token, "species" ) ) { ci->footsteps = FOOTSTEP_SPECIES; } else if ( !Q_stricmp( token, "warbot" ) ) { ci->footsteps = FOOTSTEP_WARBOT; } else if ( !Q_stricmp( token, "boot" ) ) { ci->footsteps = FOOTSTEP_BOOT; } else if ( !Q_stricmp( token, "flesh" ) ) { // Old Q3 defaults, for compatibility. -PJL ci->footsteps = FOOTSTEP_SPECIES; } else if ( !Q_stricmp( token, "mech" ) ) { // Ditto ci->footsteps = FOOTSTEP_BORG; } else if ( !Q_stricmp( token, "energy" ) ) { // Ditto ci->footsteps = FOOTSTEP_BORG; } 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, "soundpath" ) ) { token = COM_Parse( &text_p ); if ( !token ) { break; } Q_strncpyz(ci->soundPath,token,sizeof (ci->soundPath) ); 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 ); } // read information for each frame for ( i = 0 ; i < MAX_ANIMATIONS ; i++ ) { token = COM_Parse( &text_p ); if ( !token ) { 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 ) { animations[i].firstFrame -= skip; } token = COM_Parse( &text_p ); if ( !token ) { break; } animations[i].numFrames = atoi( token ); token = COM_Parse( &text_p ); if ( !token ) { break; } animations[i].loopFrames = atoi( token ); token = COM_Parse( &text_p ); if ( !token ) { break; } fps = atof( token ); if ( fps == 0 ) { fps = 1; } animations[i].frameLerp = 1000 / fps; animations[i].initialLerp = 1000 / fps; } if ( i != MAX_ANIMATIONS ) { CG_Printf( "Error parsing animation file: %s", filename ); return qfalse; } return qtrue; } /* ========================== CG_RegisterClientSkin ========================== */ static qboolean CG_RegisterClientSkin( clientInfo_t *ci, const char *modelName, const char *skinName ) { char filename[MAX_QPATH]; Com_sprintf( filename, sizeof( filename ), "models/players2/%s/lower_%s.skin", modelName, skinName ); ci->legsSkin = trap_R_RegisterSkin( filename ); Com_sprintf( filename, sizeof( filename ), "models/players2/%s/upper_%s.skin", modelName, skinName ); ci->torsoSkin = trap_R_RegisterSkin( filename ); Com_sprintf( filename, sizeof( filename ), "models/players2/%s/head_%s.skin", modelName, skinName ); ci->headSkin = trap_R_RegisterSkin( filename ); Com_sprintf( filename, sizeof( filename ), "models/players2/%s/groups.cfg", modelName); strcpy(ci->race, BG_RegisterRace( filename )); if ( !ci->legsSkin || !ci->torsoSkin || !ci->headSkin ) { return qfalse; } return qtrue; } /* ========================== CG_RegisterClientModelname ========================== */ static qboolean CG_RegisterClientModelname( clientInfo_t *ci, const char *modelName, const char *skinName ) { char filename[MAX_QPATH]; // load cmodels before models so filecache works Com_sprintf( filename, sizeof( filename ), "models/players2/%s/lower.mdr", modelName ); ci->legsModel = trap_R_RegisterModel( filename ); if ( !ci->legsModel ) { Com_sprintf( filename, sizeof( filename ), "models/players2/%s/lower.md3", modelName ); ci->legsModel = trap_R_RegisterModel( filename ); if ( !ci->legsModel ) { Com_Printf( S_COLOR_RED"Failed to load model file %s\n", filename ); return qfalse; } } Com_sprintf( filename, sizeof( filename ), "models/players2/%s/upper.mdr", modelName ); ci->torsoModel = trap_R_RegisterModel( filename ); if ( !ci->torsoModel ) { Com_sprintf( filename, sizeof( filename ), "models/players2/%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/players2/%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; } // 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; } // load the animations Com_sprintf( filename, sizeof( filename ), "models/players2/%s/animation.cfg", modelName ); if ( !CG_ParseAnimationFile( filename, ci ) ) { Com_Printf( "Failed to load animation file %s\n", filename ); return qfalse; } Com_sprintf( filename, sizeof( filename ), "models/players2/%s/icon_%s.jpg", 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. This will usually be deferred to a safe time =================== */ static void CG_LoadClientInfo( clientInfo_t *ci , int clientNum) { const char *dir, *fallback; int i; const char *s; char temp_string[200]; 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 ( cgs.gametype >= GT_TEAM ) { // keep skin name if ( !CG_RegisterClientModelname( ci, DEFAULT_MODEL, ci->skinName ) ) { CG_Error( "DEFAULT_MODEL / skin (%s/%s) failed to register", DEFAULT_MODEL, ci->skinName ); } } else { if ( !CG_RegisterClientModelname( ci, DEFAULT_MODEL, "default" ) ) { CG_Error( "DEFAULT_MODEL (%s) failed to register", DEFAULT_MODEL ); } } } // sounds dir = ci->soundPath; fallback = (ci->gender==GENDER_FEMALE)?"hm_female":"hm_male"; for ( i = 0 ; i < MAX_CUSTOM_SOUNDS ; i++ ) { s = cg_customSoundNames[i]; if ( !s ) { break; } ci->sounds[i] = trap_S_RegisterSound( va("sound/voice/%s/misc/%s", dir, s + 1) ); if ( !ci->sounds[i] ) { ci->sounds[i] = trap_S_RegisterSound( va("sound/player/%s/%s", fallback, s + 1) ); } } ci->deferred = qfalse; Com_sprintf(temp_string, sizeof(temp_string), "%s/%s", ci->modelName, ci->skinName); updateSkin(clientNum, temp_string); // reset any existing players and bodies, because they might be in bad // frames for this new model 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] ); } } } // we need to check here to see if the clientinfo model variable is the same as the one that is in the // clientinfo block. This is because it is possible for the server to change skins on us when we hit a CTF // teamplay game where groups are defined. // most of the time this will not hit void updateSkin(int clientNum, char *new_model) { char model_string[200]; // create string to be checked against trap_Cvar_VariableStringBuffer("model", model_string, sizeof(model_string) ); if (Q_stricmp(new_model, model_string) && cg.validPPS && (clientNum == cg.predictedPlayerState.clientNum)) { trap_Cvar_Set_No_Modify ("model",new_model); } } /* ====================== 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->modelIcon = from->modelIcon; Q_strncpyz( to->soundPath, from->soundPath, sizeof (to->soundPath) ); memcpy( to->animations, from->animations, sizeof( to->animations ) ); memcpy( to->sounds, from->sounds, sizeof( to->sounds ) ); } /* ====================== CG_ScanForExistingClientInfo ====================== */ static qboolean CG_ScanForExistingClientInfo( clientInfo_t *ci ) { int i; clientInfo_t *match; for ( i = 0 ; i < cgs.maxclients ; i++ ) { match = &cgs.clientinfo[ i ]; if ( !match->infoValid ) { continue; } if ( match->deferred ) { continue; } if ( !Q_stricmp( ci->modelName, match->modelName ) && !Q_stricmp( ci->skinName, match->skinName ) ) { // this clientinfo is identical, so use it's handles ci->deferred = qfalse; CG_CopyClientInfoModel( match, ci ); return qtrue; } } // nothing matches, so defer the load return qfalse; } /* ====================== CG_SetDeferredClientInfo We aren't going to load it now, so grab some other client's info to use until we have some spare time. ====================== */ static void CG_SetDeferredClientInfo( clientInfo_t *ci, int clientNum ) { int i; clientInfo_t *match; // if we are in teamplay, only grab a model if the skin is correct if ( cgs.gametype >= GT_TEAM ) { // this is ONLY for optimization - it's exactly the same effect as CG_LoadClientInfo for ( i = 0 ; i < cgs.maxclients ; i++ ) { match = &cgs.clientinfo[ i ]; if ( !match->infoValid ) { continue; } if ( Q_stricmp( ci->skinName, match->skinName ) ) { continue; } ci->deferred = qtrue; CG_CopyClientInfoModel( match, ci ); return; } // load the full model, because we don't ever want to show // an improper team skin. This will cause a hitch for the first // player, when the second enters. Combat shouldn't be going on // yet, so it shouldn't matter CG_LoadClientInfo( ci, clientNum ); return; } // find the first valid clientinfo and grab its stuff for ( i = 0 ; i < cgs.maxclients ; i++ ) { match = &cgs.clientinfo[ i ]; if ( !match->infoValid ) { continue; } ci->deferred = qtrue; CG_CopyClientInfoModel( match, ci ); return; } // we should never get here... CG_Printf( "CG_SetDeferredClientInfo: no valid clients!\n" ); CG_LoadClientInfo( ci ,clientNum); } /* ====================== 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 } // build into a temp buffer so the defer checks can use // 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.color ); // 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 ); // model v = Info_ValueForKey( configstring, "model" ); if ( cg_forceModel.integer ) { // forcemodel makes everyone use a single model // to prevent load hitches char modelStr[MAX_QPATH]; char *skin; trap_Cvar_VariableStringBuffer( "model", modelStr, sizeof( modelStr ) ); if ( ( skin = strchr( modelStr, '/' ) ) == NULL) { skin = "default"; } else { *skin++ = 0; } Q_strncpyz( newInfo.skinName, skin, sizeof( newInfo.skinName ) ); Q_strncpyz( newInfo.modelName, modelStr, sizeof( newInfo.modelName ) ); // Q_strncpyz( newInfo.modelName, DEFAULT_MODEL, sizeof( newInfo.modelName ) ); // Q_strncpyz( newInfo.skinName, "default", sizeof( newInfo.skinName ) ); if ( cgs.gametype >= GT_TEAM ) { // keep skin name slash = strchr( v, '/' ); if ( slash ) { Q_strncpyz( newInfo.skinName, slash + 1, sizeof( newInfo.skinName ) ); } } } else { 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; } } // scan for an existing clientinfo that matches this modelname // so we can avoid loading checks if possible if ( !CG_ScanForExistingClientInfo( &newInfo ) ) { qboolean forceDefer; forceDefer = trap_MemoryRemaining() < 2000000; // if we are defering loads, just have it pick the first valid if ( forceDefer || ( cg_deferPlayers.integer && !cg_buildScript.integer && !cg.loading && ((clientNum != cg.predictedPlayerState.clientNum) && cg.validPPS) ) ) { // keep whatever they had if it won't violate team skins if ( ci->infoValid && ( cgs.gametype < GT_TEAM || !Q_stricmp( newInfo.skinName, ci->skinName ) ) ) { CG_CopyClientInfoModel( ci, &newInfo ); newInfo.deferred = qtrue; } else { // use whatever is available CG_SetDeferredClientInfo( &newInfo, clientNum ); } // if we are low on memory, leave them with this model if ( forceDefer ) { CG_Printf( "Memory is low. Using deferred model.\n" ); newInfo.deferred = qfalse; } } else { CG_LoadClientInfo( &newInfo, clientNum ); } } // replace whatever was there with the new one newInfo.infoValid = qtrue; *ci = newInfo; } /* ====================== CG_LoadDeferredPlayers Called each frame when a player is dead and the scoreboard is up so deferred players can be loaded ====================== */ void CG_LoadDeferredPlayers( void ) { int i; clientInfo_t *ci; // scan for a deferred player to load for ( i = 0, ci = cgs.clientinfo ; i < cgs.maxclients ; i++, ci++ ) { if ( ci->infoValid && ci->deferred ) { // if we are low on memory, leave it deferred if ( trap_MemoryRemaining() < 4000000 ) { CG_Printf( "Memory is low. Using deferred model.\n" ); ci->deferred = qfalse; continue; } CG_LoadClientInfo( ci, i ); // break; } } } /* ============================================================================= 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_ANIMATIONS ) { 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_RunLerpFrame Sets cg.snap, cg.oldFrame, and cg.backlerp cg.time should be between oldFrameTime and frameTime after exit =============== */ static void CG_RunLerpFrame( clientInfo_t *ci, lerpFrame_t *lf, int newAnimation, float speedScale ) { int f; 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 if ( f >= anim->numFrames ) { f -= anim->numFrames; if ( anim->loopFrames ) { f %= anim->loopFrames; f += anim->numFrames - anim->loopFrames; } else { f = anim->numFrames - 1; // the animation is stuck at the end, so it // can immediately transition to another sequence lf->frameTime = cg.time; } } 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; clientNum = cent->currentState.clientNum; if ( cg_noPlayerAnims.integer ) { *legsOld = *legs = *torsoOld = *torso = 0; return; } if ( cent->currentState.powerups & ( 1 << PW_HASTE ) ) { speedScale = 1.5; } else { speedScale = 1; } ci = &cgs.clientinfo[ clientNum ]; // do the shuffle turn frames locally if ( cent->pe.legs.yawing && ( cent->currentState.legsAnim & ~ANIM_TOGGLEBIT ) == LEGS_IDLE ) { CG_RunLerpFrame( ci, ¢->pe.legs, LEGS_TURN, speedScale ); } else { CG_RunLerpFrame( ci, ¢->pe.legs, cent->currentState.legsAnim, speedScale ); } *legsOld = cent->pe.legs.oldFrame; *legs = cent->pe.legs.frame; *legsBackLerp = cent->pe.legs.backlerp; CG_RunLerpFrame( ci, ¢->pe.torso, cent->currentState.torsoAnim, speedScale ); *torsoOld = cent->pe.torso.oldFrame; *torso = cent->pe.torso.frame; *torsoBackLerp = cent->pe.torso.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 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; VectorCopy( cent->lerpAngles, 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 { dir = cent->currentState.angles2[YAW]; 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 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.75; } else { dest = headAngles[PITCH] * 0.75; } CG_SwingAngles( dest, 15, 30, 0.1, ¢->pe.torso.pitchAngle, ¢->pe.torso.pitching ); torsoAngles[PITCH] = cent->pe.torso.pitchAngle; // --------- 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.05; AnglesToAxis( legsAngles, axis ); side = speed * DotProduct( velocity, axis[1] ); legsAngles[ROLL] -= side; side = speed * DotProduct( velocity, axis[0] ); legsAngles[PITCH] += side; } // 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 ); } //========================================================================== /* =============== CG_HasteTrail =============== */ static void CG_HasteTrail( centity_t *cent ) { localEntity_t *smoke; vec3_t origin, pos2; int anim; if ( cent->trailTime > cg.time ) { return; } anim = cent->pe.legs.animationNumber & ~ANIM_TOGGLEBIT; if ( anim != LEGS_RUN && anim != LEGS_BACK ) { return; } cent->trailTime += 100; if ( cent->trailTime < cg.time ) { cent->trailTime = cg.time; } VectorCopy( cent->lerpOrigin, origin ); origin[0] += flrandom(-5,5); origin[1] += flrandom(-5,5); origin[2] -= 15; AngleVectors(cent->lerpAngles, pos2, NULL, NULL); pos2[2]=0; VectorMA(origin, -22.0, pos2, pos2); smoke = FX_AddLine(origin, pos2, 1.0, 20.0, -12.0, 0.7, 0.0, 500, cgs.media.hastePuffShader); } /* =============== CG_FlightTrail =============== */ static void CG_FlightTrail( centity_t *cent ) { localEntity_t *smoke; vec3_t origin; vec3_t vel; vec3_t startrgb={0.5, 0.5, 0.5}; vec3_t endrgb={0.0,0.0,0.0}; /* if ( cent->trailTime > cg.time ) { return; } cent->trailTime += 100; // only draw a sprite every 0.01 s if ( cent->trailTime < cg.time ) { cent->trailTime = cg.time; } */ VectorCopy( cent->lerpOrigin, origin ); origin[2] -= flrandom(10,18); VectorSet(vel, flrandom(-10,10), flrandom(-10, 10), flrandom(-30,-50)); smoke = FX_AddSprite2(origin, vel, qfalse, 4.0, 4.0, 0.5, 0.0, startrgb, endrgb, flrandom(0,360), 0, 500, cgs.media.flightPuffShader); } /* =============== CG_TrailItem =============== */ static void CG_TrailItem( centity_t *cent, qhandle_t hModel ) { refEntity_t ent; vec3_t angles; float frame; if ( cent->currentState.number == cg.snap->ps.clientNum && !cg.renderingThirdPerson ) { return; } memset( &ent, 0, sizeof( ent ) ); VectorCopy( cent->lerpAngles, angles ); angles[PITCH] = 0; angles[ROLL] = 0; angles[YAW] += 180.0; // It's facing the wrong way. AnglesToAxis( angles, ent.axis ); VectorMA( cent->lerpOrigin, 12, ent.axis[0], ent.origin ); ent.origin[2] += 4; // Make it animate. frame = (cg.time / 100.0); ent.renderfx|=RF_WRAP_FRAMES; ent.oldframe = (int)frame; ent.frame = (int)frame+1; ent.backlerp = (float)(ent.frame) - frame; // if the player is looking at himself in 3rd person, don't show the flag solid, 'cause he can't see!!! if (cent->currentState.number == cg.snap->ps.clientNum) { ent.shaderRGBA[3] = 128; ent.renderfx |= RF_FORCE_ENT_ALPHA; } VectorScale(ent.axis[0], 0.75, ent.axis[0]); VectorScale(ent.axis[1], 0.9, ent.axis[1]); VectorScale(ent.axis[2], 0.9, ent.axis[2]); ent.nonNormalizedAxes = qtrue; #if 0 // This approach is used if you want the item to autorotate. Since this is the flag, we don't. VectorScale( cg.autoAxis[0], 0.75, ent.axis[0] ); VectorScale( cg.autoAxis[1], 0.75, ent.axis[1] ); VectorScale( cg.autoAxis[2], 0.75, ent.axis[2] ); #endif ent.hModel = hModel; trap_R_AddRefEntityToScene( &ent ); } /* =============== CG_PlayerPowerups =============== */ static void CG_PlayerPowerups( centity_t *cent ) { int powerups; powerups = cent->currentState.powerups; if ( !powerups ) { return; } // quad gives a dlight if ( powerups & ( 1 << PW_QUAD ) ) { trap_R_AddLightToScene( cent->lerpOrigin, 200 + (rand()&31), 0.2, 0.2, 1.0 ); } // invul gives a dlight if ( powerups & ( 1 << PW_BATTLESUIT ) ) { trap_R_AddLightToScene( cent->lerpOrigin, 200 + (rand()&31), 0.8, 0.8, 0.2 ); } // flight plays a looped sound if ( powerups & ( 1 << PW_FLIGHT ) ) { trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, vec3_origin, cgs.media.flightSound ); } // redflag if ( powerups & ( 1 << PW_REDFLAG ) ) { CG_TrailItem( cent, cgs.media.redFlagModel ); trap_R_AddLightToScene( cent->lerpOrigin, 200 + (rand()&31), 1, 0.2, 0.2 ); } // blueflag if ( powerups & ( 1 << PW_BLUEFLAG ) ) { CG_TrailItem( cent, cgs.media.blueFlagModel ); trap_R_AddLightToScene( cent->lerpOrigin, 200 + (rand()&31), 0.2, 0.2, 1 ); } // haste leaves smoke trails if ( powerups & ( 1 << PW_HASTE ) && (cent->currentState.groundEntityNum==ENTITYNUM_WORLD)) { CG_HasteTrail( cent ); } // haste leaves smoke trails if ( powerups & ( 1 << PW_FLIGHT ) && (cent->currentState.groundEntityNum!=ENTITYNUM_WORLD)) { CG_FlightTrail( cent ); } // seeker coolness if ( powerups & ( 1 << PW_SEEKER ) ) { CG_Seeker(cent); } } /* =============== 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; int team; if ( cent->currentState.number == cg.snap->ps.clientNum && !cg.renderingThirdPerson ) { rf = RF_THIRD_PERSON; // only show in mirrors } else { rf = 0; } team = cgs.clientinfo[ cent->currentState.clientNum ].team; memset( &ent, 0, sizeof( ent ) ); VectorCopy( cent->lerpOrigin, ent.origin ); ent.origin[2] += 48; ent.reType = RT_SPRITE; ent.customShader = shader; ent.data.sprite.radius = 10; ent.renderfx = rf; if (team==TEAM_RED) { ent.shaderRGBA[0] = 255; ent.shaderRGBA[1] = 64; ent.shaderRGBA[2] = 64; } else if (team==TEAM_BLUE) { ent.shaderRGBA[0] = 64; ent.shaderRGBA[1] = 64; ent.shaderRGBA[2] = 255; } else { 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 ) { int team; if ( cent->currentState.eFlags & EF_CONNECTION ) { CG_PlayerFloatSprite( cent, cgs.media.connectionShader ); return; } if ( cent->currentState.eFlags & EF_TALK ) { CG_PlayerFloatSprite( cent, cgs.media.chatShader ); return; } if ( cent->currentState.eFlags & EF_AWARD_FIRSTSTRIKE ) { CG_PlayerFloatSprite( cent, cgs.media.medalFirstStrike ); return; } if ( cent->currentState.eFlags & EF_AWARD_IMPRESSIVE ) { CG_PlayerFloatSprite( cent, cgs.media.medalImpressive ); return; } if ( cent->currentState.eFlags & EF_AWARD_EXCELLENT ) { CG_PlayerFloatSprite( cent, cgs.media.medalExcellent ); return; } if ( cent->currentState.eFlags & EF_AWARD_ACE ) { CG_PlayerFloatSprite( cent, cgs.media.medalAce ); return; } if ( cent->currentState.eFlags & EF_AWARD_EXPERT ) { CG_PlayerFloatSprite( cent, cgs.media.medalExpert ); return; } if ( cent->currentState.eFlags & EF_AWARD_MASTER ) { CG_PlayerFloatSprite( cent, cgs.media.medalMaster ); return; } if ( cent->currentState.eFlags & EF_AWARD_CHAMPION ) { CG_PlayerFloatSprite( cent, cgs.media.medalChampion ); return; } team = cgs.clientinfo[ cent->currentState.clientNum ].team; if ( !(cent->currentState.eFlags & EF_DEAD) && cg.snap->ps.persistant[PERS_TEAM] == team && cgs.gametype >= GT_TEAM && cent->currentState.number != cg.snap->ps.clientNum ) // Don't show a sprite above a player's own head in 3rd person. { if (team==TEAM_RED) { CG_PlayerFloatSprite( cent, cgs.media.teamRedShader ); } else if (team==TEAM_BLUE) { CG_PlayerFloatSprite( cent, cgs.media.teamBlueShader ); } // else don't show an icon. There currently are no other team types. 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 ) { vec3_t end, mins = {-7, -7, 0}, maxs = {7, 7, 2}; trace_t trace; float alpha; *shadowPlane = 0; if ( cg_shadows.integer == 0 ) { return qfalse; } // no shadows when invisible if ( cent->currentState.powerups & ( 1 << PW_INVIS ) ) { return qfalse; } // send a trace down from the player to the ground VectorCopy( cent->lerpOrigin, end ); end[2] -= SHADOW_DISTANCE; trap_CM_BoxTrace( &trace, cent->lerpOrigin, end, mins, maxs, 0, MASK_PLAYERSOLID ); // no shadow if too high if ( trace.fraction == 1.0 ) { return qfalse; } *shadowPlane = trace.endpos[2] + 1; 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, 1,1,1,alpha, qfalse, 16, qtrue ); return qtrue; } /* =============== CG_PlayerSplash Draw a mark at the water surface =============== */ static void CG_PlayerSplash( centity_t *cent ) { vec3_t start, end; trace_t trace; int contents; polyVert_t verts[4]; if ( !cg_shadows.integer ) { return; } VectorCopy( cent->lerpOrigin, end ); end[2] -= 24; // 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.0 ) { return; } // create a mark polygon VectorCopy( trace.endpos, verts[0].xyz ); verts[0].xyz[0] -= 32; verts[0].xyz[1] -= 32; verts[0].st[0] = 0; verts[0].st[1] = 0; verts[0].modulate[0] = 255; verts[0].modulate[1] = 255; verts[0].modulate[2] = 255; verts[0].modulate[3] = 255; VectorCopy( trace.endpos, verts[1].xyz ); verts[1].xyz[0] -= 32; verts[1].xyz[1] += 32; verts[1].st[0] = 0; verts[1].st[1] = 1; verts[1].modulate[0] = 255; verts[1].modulate[1] = 255; verts[1].modulate[2] = 255; verts[1].modulate[3] = 255; VectorCopy( trace.endpos, verts[2].xyz ); verts[2].xyz[0] += 32; verts[2].xyz[1] += 32; verts[2].st[0] = 1; verts[2].st[1] = 1; verts[2].modulate[0] = 255; verts[2].modulate[1] = 255; verts[2].modulate[2] = 255; verts[2].modulate[3] = 255; VectorCopy( trace.endpos, verts[3].xyz ); verts[3].xyz[0] += 32; verts[3].xyz[1] -= 32; verts[3].st[0] = 1; verts[3].st[1] = 0; verts[3].modulate[0] = 255; verts[3].modulate[1] = 255; verts[3].modulate[2] = 255; verts[3].modulate[3] = 255; trap_R_AddPolyToScene( cgs.media.wakeMarkShader, 4, verts ); } static int timestamp; /* =============== CG_AddRefEntityWithPowerups Adds a piece with modifications or duplications for powerups Also called by CG_Missile for quad rockets, but nobody can tell... =============== */ void CG_AddRefEntityWithPowerups( refEntity_t *ent, int powerups, int eFlags) { if ( eFlags & EF_ITEMPLACEHOLDER ) // Hologram Decoy { float f1, f2; // We used EF_ITEMPLACEHOLDER flag to indicate that this 'player' model // is actually a holographic decoy. Now there is a chance that the // decoy will flicker a bit because of ordering with alpha shaders... // The lowest the alpha goes is 4.0-2.5-1.0=0.5. f1 = 4.0 + 2.5*sin(52.423 + cg.time/205.243); f2 = sin(14.232 + cg.time/63.572); f1 = f1+f2; if (f1 > 1.0) { // Just draw him solid. trap_R_AddRefEntityToScene( ent ); } else { // Draw him faded. if (f1 > 0.8) { // Don't have alphas over 0.8, it just looks bad. f1=0.8; } else if (f1 < 0.1) { f1=0.1; } ent->renderfx |= RF_FORCE_ENT_ALPHA; // Override the skin shader info and use this alpha value. ent->shaderRGBA[3] = 255.0*f1; trap_R_AddRefEntityToScene( ent ); ent->renderfx &= ~RF_FORCE_ENT_ALPHA; ent->shaderRGBA[3] = 255; // ...with a static shader. ent->customShader = cgs.media.holoDecoyShader; ent->shaderRGBA[0] = ent->shaderRGBA[1] = ent->shaderRGBA[2] = 255.0*(1.0-f1); // More solid as the player fades out... trap_R_AddRefEntityToScene(ent); } return; } if ((eFlags & EF_DEAD) && (timestamp > cg.time)) { // Dead. timestamp holds the time of death. float alpha; int a; // First draw the entity itself. alpha = (timestamp - cg.time)/2500.0; ent->renderfx |= RF_FORCE_ENT_ALPHA; a = alpha * 255.0; if (a <= 0) a=1; ent->shaderRGBA[3] = a; trap_R_AddRefEntityToScene( ent ); ent->renderfx &= ~RF_FORCE_ENT_ALPHA; ent->shaderRGBA[3] = 255; // Now draw the static shader over it. // Alpha in over half the time, out over half. alpha = sin(M_PI*alpha); a = alpha * 255.0; if (a <= 0) a=1; ent->customShader = cgs.media.rezOutShader; ent->shaderRGBA[0] = ent->shaderRGBA[1] = ent->shaderRGBA[2] = a; trap_R_AddRefEntityToScene( ent ); ent->shaderRGBA[0] = ent->shaderRGBA[1] = ent->shaderRGBA[2] = 255; } else if ( powerups & ( 1 << PW_INVIS ) ) { ent->customShader = cgs.media.invisShader; trap_R_AddRefEntityToScene( ent ); } else if (powerups & (1<renderfx |= RF_FORCE_ENT_ALPHA; ent->shaderRGBA[3] = 255 - (dtime)*0.25; trap_R_AddRefEntityToScene( ent ); ent->renderfx &= ~RF_FORCE_ENT_ALPHA; ent->shaderRGBA[3] = 255; } if (dtime < 2000) { ent->customShader = cgs.media.disruptorShader; ent->shaderTime = timeParam / 1000.0f; trap_R_AddRefEntityToScene( ent ); } } else if (powerups & (1<renderfx |= RF_FORCE_ENT_ALPHA; ent->shaderRGBA[3] = (int)(255.0 - (dtime / 300.0) * 254.0); trap_R_AddRefEntityToScene( ent ); ent->renderfx &= ~RF_FORCE_ENT_ALPHA; ent->shaderRGBA[3] = 255; } if (dtime < 500) { ent->customShader = cgs.media.explodeShellShader; ent->renderfx |= RF_CAP_FRAMES; ent->shaderTime = timeParam / 1000.0f; trap_R_AddRefEntityToScene( ent ); ent->renderfx &= ~RF_CAP_FRAMES; } } else if (powerups & (1<renderfx |= RF_FORCE_ENT_ALPHA; ent->shaderRGBA[3] = 100 + 50*sin(cg.time/200.0); trap_R_AddRefEntityToScene( ent ); ent->renderfx &= ~RF_FORCE_ENT_ALPHA; ent->shaderRGBA[3] = 255; } else { trap_R_AddRefEntityToScene( ent ); // Quad should JUST be on the weapon now, sparky. /* if ( powerups & ( 1 << PW_QUAD ) ) { if (team == TEAM_RED) ent->customShader = cgs.media.redQuadShader; else ent->customShader = cgs.media.quadShader; trap_R_AddRefEntityToScene( ent ); } */ if ( powerups & ( 1 << PW_REGEN ) ) { if ( ( ( cg.time / 100 ) % 10 ) == 1 ) { ent->customShader = cgs.media.regenShader; trap_R_AddRefEntityToScene( ent ); } } if ( powerups & ( 1 << PW_BATTLESUIT ) ) { ent->customShader = cgs.media.battleSuitShader; trap_R_AddRefEntityToScene( ent ); return; } if (powerups & (1 << PW_OUCH)) { ent->customShader = cgs.media.holoOuchShader; // set rgb to 1 of 16 values from 0 to 255. don't use random so that the three //parts of the player model as well as the gun will all look the same ent->shaderRGBA[0] = ent->shaderRGBA[1] = ent->shaderRGBA[2] = ((cg.time % 17)*0.0625)*255.0;//irandom(0,255); trap_R_AddRefEntityToScene(ent); } if (powerups & (1<customShader = cgs.media.electricBodyShader; // ent->shaderTime = timeParam / 1000.0f; ent->shaderRGBA[0] = ent->shaderRGBA[1] = ent->shaderRGBA[2] = (int)(1.0 + ((4000.0 - dtime) / 4000.0f * 254.0f )); ent->shaderRGBA[3] = 255; trap_R_AddRefEntityToScene( ent ); if ( random() > 0.95f ) { // Play a zap sound to go it. trap_S_StartSound (ent->origin, entNum, CHAN_AUTO, cg_weapons[WP_DREADNOUGHT].altHitSound); } } } } } #define MAX_SHIELD_TIME 2500.0 #define MIN_SHIELD_TIME 1750.0 void CG_PlayerShieldHit(int entitynum, vec3_t dir, int amount) { centity_t *cent; int time; if (entitynum<0 || entitynum >= MAX_CLIENTS) { return; } cent = &cg_entities[entitynum]; if (amount > 100) { time = cg.time + MAX_SHIELD_TIME; // 2 sec. } else { time = cg.time + 500 + amount*20; } if (time > cent->damageTime) { cent->damageTime = time; VectorScale(dir, -1, dir); vectoangles(dir, cent->damageAngles); } } void CG_DrawPlayerShield(centity_t *cent, vec3_t origin) { refEntity_t ent; int alpha; float scale; // Don't draw the shield when the player is dead. if (cent->currentState.eFlags & EF_DEAD) { return; } memset( &ent, 0, sizeof( ent ) ); VectorCopy( origin, ent.origin ); ent.origin[2] += 10.0; AnglesToAxis( cent->damageAngles, ent.axis ); alpha = 255.0 * ((cent->damageTime - cg.time) / MIN_SHIELD_TIME) + irandom(0, 16); if (alpha>255) alpha=255; // Make it bigger, but tighter if more solid scale = 1.8 - ((float)alpha*(0.4/255.0)); // Range from 1.4 to 1.8 VectorScale( ent.axis[0], scale, ent.axis[0] ); VectorScale( ent.axis[1], scale, ent.axis[1] ); VectorScale( ent.axis[2], scale, ent.axis[2] ); ent.hModel = cgs.media.explosionModel; ent.customShader = cgs.media.halfShieldShader; ent.shaderRGBA[0] = alpha; ent.shaderRGBA[1] = alpha; ent.shaderRGBA[2] = alpha; ent.shaderRGBA[3] = 255; trap_R_AddRefEntityToScene( &ent ); } void CG_PlayerHitFX(centity_t *cent) { centity_t *curent; // only do the below fx if the cent in question is...uh...me, and it's first person. if (cent->currentState.clientNum != cg.predictedPlayerState.clientNum || cg.renderingThirdPerson) { // Get the NON-PREDICTED player entity, because the predicted one doesn't have the damage info on it. curent = &cg_entities[cent->currentState.number]; if (curent->damageTime > cg.time) { CG_DrawPlayerShield(curent, cent->lerpOrigin); } return; } } /* =============== CG_Player =============== */ void CG_Player( centity_t *cent ) { clientInfo_t *ci; refEntity_t legs; refEntity_t torso; refEntity_t head; int clientNum; int renderfx; qboolean shadow; float shadowPlane; // 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 = cent->currentState.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; } if (cent->currentState.eFlags & EF_NODRAW) { // Don't draw anymore... return; } memset( &legs, 0, sizeof(legs) ); memset( &torso, 0, sizeof(torso) ); memset( &head, 0, sizeof(head) ); // get the rotation information CG_PlayerAngles( cent, legs.axis, torso.axis, head.axis ); // get the animation state (after rotation, to allow feet shuffle) CG_PlayerAnimation( cent, &legs.oldframe, &legs.frame, &legs.backlerp, &torso.oldframe, &torso.frame, &torso.backlerp ); // add powerups floating behind the player CG_PlayerPowerups( cent ); // add the talk baloon or disconnect icon (not in intermission) if ( !cg.intermissionStarted ) { CG_PlayerSprites( cent ); } // add the shadow if ( !(cent->currentState.eFlags & EF_ITEMPLACEHOLDER) ) { shadow = CG_PlayerShadow( cent, &shadowPlane ); } else { // - unless we are a hologram... shadow=qfalse; shadowPlane=0; } // add a water splash if partially in and out of water CG_PlayerSplash( cent ); // get the player model information renderfx = 0; if ( cent->currentState.number == cg.snap->ps.clientNum && !cg.renderingThirdPerson ) { renderfx = RF_THIRD_PERSON; // only draw in mirrors } if ( cg_shadows.integer == 3 && shadow ) { renderfx |= RF_SHADOW_PLANE; } renderfx |= RF_LIGHTING_ORIGIN; // use the same origin for all // if we've been hit, display proper fullscreen fx CG_PlayerHitFX(cent); // If we are dead, set up the correct fx. if (cent->currentState.eFlags & EF_DEAD) { if (cent->deathTime==0) cent->deathTime = cg.time; // Set Death Time so you can see yourself if (cent->currentState.time > cg.time) { // Fading out. timestamp=cent->currentState.time; shadow = qfalse; shadowPlane = 0; } else { timestamp = 0; } } else cent->deathTime = 0; // // add the legs // legs.hModel = ci->legsModel; legs.customSkin = ci->legsSkin; 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 // Setup the param, in case it is needed. timeParam = cent->deathTime; entNum = cent->currentState.number; CG_AddRefEntityWithPowerups( &legs, cent->currentState.powerups, cent->currentState.eFlags); // if the model failed, allow the default nullmodel to be displayed if (!legs.hModel) { return; } // // add the torso // torso.hModel = ci->torsoModel; if (!torso.hModel) { return; } torso.customSkin = ci->torsoSkin; VectorCopy( cent->lerpOrigin, torso.lightingOrigin ); CG_PositionRotatedEntityOnTag( &torso, &legs, ci->legsModel, "tag_torso"); torso.shadowPlane = shadowPlane; torso.renderfx = renderfx; CG_AddRefEntityWithPowerups( &torso, cent->currentState.powerups, cent->currentState.eFlags ); // // add the head // head.hModel = ci->headModel; if (!head.hModel) { return; } head.customSkin = ci->headSkin; VectorCopy( cent->lerpOrigin, head.lightingOrigin ); CG_PositionRotatedEntityOnTag( &head, &torso, ci->torsoModel, "tag_head"); head.shadowPlane = shadowPlane; head.renderfx = renderfx; CG_AddRefEntityWithPowerups( &head, cent->currentState.powerups, cent->currentState.eFlags ); // // add the gun / barrel / flash // CG_AddPlayerWeapon( &torso, NULL, cent ); } //===================================================================== /* =============== 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 ); 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; if ( cg_debugPosition.integer ) { CG_Printf("%i ResetPlayerEntity yaw=%i\n", cent->currentState.number, cent->pe.torso.yawAngle ); } }