/* =========================================================================== Copyright (C) 1999-2005 Id Software, Inc. This file is part of Quake III Arena source code. Quake III Arena source code 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. Quake III Arena source code 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 Foobar; 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); 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; // 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, "boot")) { ci->footsteps = FOOTSTEP_BOOT; } else if(!Q_stricmp(token, "flesh")) { ci->footsteps = FOOTSTEP_FLESH; } else if(!Q_stricmp(token, "mech")) { ci->footsteps = FOOTSTEP_MECH; } else if(!Q_stricmp(token, "energy")) { ci->footsteps = FOOTSTEP_ENERGY; } 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; } // 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) { 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 && i < TORSO_GETFLAG) { animations[i].firstFrame -= skip; } token = COM_Parse(&text_p); if(!*token) { break; } animations[i].numFrames = atoi(token); animations[i].reversed = qfalse; animations[i].flipflop = qfalse; // if numFrames is negative the animation is reversed if(animations[i].numFrames < 0) { animations[i].numFrames = -animations[i].numFrames; animations[i].reversed = qtrue; } 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; } // crouch backward animation memcpy(&animations[LEGS_BACKCR], &animations[LEGS_WALKCR], sizeof(animation_t)); animations[LEGS_BACKCR].reversed = qtrue; // walk backward animation memcpy(&animations[LEGS_BACKWALK], &animations[LEGS_WALK], sizeof(animation_t)); animations[LEGS_BACKWALK].reversed = qtrue; // flag moving fast animations[FLAG_RUN].firstFrame = 0; animations[FLAG_RUN].numFrames = 16; animations[FLAG_RUN].loopFrames = 16; animations[FLAG_RUN].frameLerp = 1000 / 15; animations[FLAG_RUN].initialLerp = 1000 / 15; animations[FLAG_RUN].reversed = qfalse; // flag not moving or moving slowly animations[FLAG_STAND].firstFrame = 16; animations[FLAG_STAND].numFrames = 5; animations[FLAG_STAND].loopFrames = 0; animations[FLAG_STAND].frameLerp = 1000 / 20; animations[FLAG_STAND].initialLerp = 1000 / 20; animations[FLAG_STAND].reversed = qfalse; // flag speeding up animations[FLAG_STAND2RUN].firstFrame = 16; animations[FLAG_STAND2RUN].numFrames = 5; animations[FLAG_STAND2RUN].loopFrames = 1; animations[FLAG_STAND2RUN].frameLerp = 1000 / 15; animations[FLAG_STAND2RUN].initialLerp = 1000 / 15; animations[FLAG_STAND2RUN].reversed = qtrue; // // new anims changes // // animations[TORSO_GETFLAG].flipflop = qtrue; // animations[TORSO_GUARDBASE].flipflop = qtrue; // animations[TORSO_PATROL].flipflop = qtrue; // animations[TORSO_AFFIRMATIVE].flipflop = qtrue; // animations[TORSO_NEGATIVE].flipflop = qtrue; // return qtrue; } /* ========================== CG_FileExists ========================== */ static qboolean CG_FileExists(const char *filename) { int len; len = trap_FS_FOpenFile(filename, 0, FS_READ); if(len > 0) { return qtrue; } return qfalse; } /* ========================== CG_FindClientModelFile ========================== */ static qboolean CG_FindClientModelFile(char *filename, int length, clientInfo_t * ci, const char *teamName, const char *modelName, const char *skinName, const char *base, const char *ext) { char *team, *charactersFolder; int i; if(cgs.gametype >= GT_TEAM) { switch (ci->team) { case TEAM_BLUE: { team = "blue"; break; } default: { team = "red"; break; } } } else { team = "default"; } charactersFolder = ""; while(1) { for(i = 0; i < 2; i++) { if(i == 0 && teamName && *teamName) { // "models/players/characters/james/stroggs/lower_lily_red.skin" Com_sprintf(filename, length, "models/players/%s%s/%s%s_%s_%s.%s", charactersFolder, modelName, teamName, base, skinName, team, ext); } else { // "models/players/characters/james/lower_lily_red.skin" Com_sprintf(filename, length, "models/players/%s%s/%s_%s_%s.%s", charactersFolder, modelName, base, skinName, team, ext); } if(CG_FileExists(filename)) { return qtrue; } if(cgs.gametype >= GT_TEAM) { if(i == 0 && teamName && *teamName) { // "models/players/characters/james/stroggs/lower_red.skin" Com_sprintf(filename, length, "models/players/%s%s/%s%s_%s.%s", charactersFolder, modelName, teamName, base, team, ext); } else { // "models/players/characters/james/lower_red.skin" Com_sprintf(filename, length, "models/players/%s%s/%s_%s.%s", charactersFolder, modelName, base, team, ext); } } else { if(i == 0 && teamName && *teamName) { // "models/players/characters/james/stroggs/lower_lily.skin" Com_sprintf(filename, length, "models/players/%s%s/%s%s_%s.%s", charactersFolder, modelName, teamName, base, skinName, ext); } else { // "models/players/characters/james/lower_lily.skin" Com_sprintf(filename, length, "models/players/%s%s/%s_%s.%s", charactersFolder, modelName, base, skinName, ext); } } if(CG_FileExists(filename)) { return qtrue; } if(!teamName || !*teamName) { break; } } // if tried the heads folder first if(charactersFolder[0]) { break; } charactersFolder = "characters/"; } return qfalse; } /* ========================== CG_FindClientHeadFile ========================== */ static qboolean CG_FindClientHeadFile(char *filename, int length, clientInfo_t * ci, const char *teamName, const char *headModelName, const char *headSkinName, const char *base, const char *ext) { char *team, *headsFolder; int i; if(cgs.gametype >= GT_TEAM) { switch (ci->team) { case TEAM_BLUE: { team = "blue"; break; } default: { team = "red"; break; } } } else { team = "default"; } if(headModelName[0] == '*') { headsFolder = "heads/"; headModelName++; } else { headsFolder = ""; } while(1) { for(i = 0; i < 2; i++) { if(i == 0 && teamName && *teamName) { Com_sprintf(filename, length, "models/players/%s%s/%s/%s%s_%s.%s", headsFolder, headModelName, headSkinName, teamName, base, team, ext); } else { Com_sprintf(filename, length, "models/players/%s%s/%s/%s_%s.%s", headsFolder, headModelName, headSkinName, base, team, ext); } if(CG_FileExists(filename)) { return qtrue; } if(cgs.gametype >= GT_TEAM) { if(i == 0 && teamName && *teamName) { Com_sprintf(filename, length, "models/players/%s%s/%s%s_%s.%s", headsFolder, headModelName, teamName, base, team, ext); } else { Com_sprintf(filename, length, "models/players/%s%s/%s_%s.%s", headsFolder, headModelName, base, team, ext); } } else { if(i == 0 && teamName && *teamName) { Com_sprintf(filename, length, "models/players/%s%s/%s%s_%s.%s", headsFolder, headModelName, teamName, base, headSkinName, ext); } else { Com_sprintf(filename, length, "models/players/%s%s/%s_%s.%s", headsFolder, headModelName, base, headSkinName, ext); } } if(CG_FileExists(filename)) { return qtrue; } if(!teamName || !*teamName) { break; } } // if tried the heads folder first if(headsFolder[0]) { break; } headsFolder = "heads/"; } return qfalse; } /* ========================== CG_RegisterClientSkin ========================== */ static qboolean CG_RegisterClientSkin(clientInfo_t * ci, const char *teamName, const char *modelName, const char *skinName, const char *headModelName, const char *headSkinName) { char filename[MAX_QPATH]; /* Com_sprintf( filename, sizeof( filename ), "models/players/%s/%slower_%s.skin", modelName, teamName, skinName ); ci->legsSkin = trap_R_RegisterSkin( filename ); if (!ci->legsSkin) { Com_sprintf( filename, sizeof( filename ), "models/players/characters/%s/%slower_%s.skin", modelName, teamName, 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/%supper_%s.skin", modelName, teamName, skinName ); ci->torsoSkin = trap_R_RegisterSkin( filename ); if (!ci->torsoSkin) { Com_sprintf( filename, sizeof( filename ), "models/players/characters/%s/%supper_%s.skin", modelName, teamName, skinName ); ci->torsoSkin = trap_R_RegisterSkin( filename ); if (!ci->torsoSkin) { Com_Printf( "Torso skin load failure: %s\n", filename ); } } */ if(CG_FindClientModelFile(filename, sizeof(filename), ci, teamName, modelName, skinName, "lower", "skin")) { ci->legsSkin = trap_R_RegisterSkin(filename); } if(!ci->legsSkin) { Com_Printf("Leg skin load failure: %s\n", filename); } if(CG_FindClientModelFile(filename, sizeof(filename), ci, teamName, modelName, skinName, "upper", "skin")) { ci->torsoSkin = trap_R_RegisterSkin(filename); } if(!ci->torsoSkin) { Com_Printf("Torso skin load failure: %s\n", filename); } if(CG_FindClientHeadFile(filename, sizeof(filename), ci, teamName, headModelName, headSkinName, "head", "skin")) { ci->headSkin = trap_R_RegisterSkin(filename); } if(!ci->headSkin) { Com_Printf("Head skin load failure: %s\n", filename); } // if any skins failed to load 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, const char *headModelName, const char *headSkinName, const char *teamName) { char filename[MAX_QPATH * 2]; const char *headName; char newTeamName[MAX_QPATH * 2]; if(headModelName[0] == '\0') { headName = modelName; } else { headName = headModelName; } Com_sprintf(filename, sizeof(filename), "models/players/%s/lower.md3", modelName); ci->legsModel = trap_R_RegisterModel(filename); if(!ci->legsModel) { Com_sprintf(filename, sizeof(filename), "models/players/characters/%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_sprintf(filename, sizeof(filename), "models/players/characters/%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; } } if(headName[0] == '*') { Com_sprintf(filename, sizeof(filename), "models/players/heads/%s/%s.md3", &headModelName[1], &headModelName[1]); } else { Com_sprintf(filename, sizeof(filename), "models/players/%s/head.md3", headName); } ci->headModel = trap_R_RegisterModel(filename); // if the head model could not be found and we didn't load from the heads folder try to load from there if(!ci->headModel && headName[0] != '*') { Com_sprintf(filename, sizeof(filename), "models/players/heads/%s/%s.md3", headModelName, headModelName); 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, teamName, modelName, skinName, headName, headSkinName)) { if(teamName && *teamName) { Com_Printf("Failed to load skin file: %s : %s : %s, %s : %s\n", teamName, modelName, skinName, headName, headSkinName); if(ci->team == TEAM_BLUE) { Com_sprintf(newTeamName, sizeof(newTeamName), "%s/", DEFAULT_BLUETEAM_NAME); } else { Com_sprintf(newTeamName, sizeof(newTeamName), "%s/", DEFAULT_REDTEAM_NAME); } if(!CG_RegisterClientSkin(ci, newTeamName, modelName, skinName, headName, headSkinName)) { Com_Printf("Failed to load skin file: %s : %s : %s, %s : %s\n", newTeamName, modelName, skinName, headName, headSkinName); return qfalse; } } else { Com_Printf("Failed to load skin file: %s : %s, %s : %s\n", modelName, skinName, headName, headSkinName); return qfalse; } } // load the animations Com_sprintf(filename, sizeof(filename), "models/players/%s/animation.cfg", modelName); if(!CG_ParseAnimationFile(filename, ci)) { Com_sprintf(filename, sizeof(filename), "models/players/characters/%s/animation.cfg", modelName); if(!CG_ParseAnimationFile(filename, ci)) { Com_Printf("Failed to load animation file %s\n", filename); return qfalse; } } if(CG_FindClientHeadFile(filename, sizeof(filename), ci, teamName, headName, headSkinName, "icon", "skin")) { ci->modelIcon = trap_R_RegisterShaderNoMip(filename); } else if(CG_FindClientHeadFile(filename, sizeof(filename), ci, teamName, headName, headSkinName, "icon", "tga")) { ci->modelIcon = trap_R_RegisterShaderNoMip(filename); } if(!ci->modelIcon) { 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) { const char *dir, *fallback; int i, modelloaded; const char *s; int clientNum; char teamname[MAX_QPATH]; teamname[0] = 0; #ifdef MISSIONPACK if(cgs.gametype >= GT_TEAM) { if(ci->team == TEAM_BLUE) { Q_strncpyz(teamname, cg_blueTeamName.string, sizeof(teamname)); } else { Q_strncpyz(teamname, cg_redTeamName.string, sizeof(teamname)); } } if(teamname[0]) { strcat(teamname, "/"); } #endif modelloaded = qtrue; if(!CG_RegisterClientModelname(ci, ci->modelName, ci->skinName, ci->headModelName, ci->headSkinName, teamname)) { if(cg_buildScript.integer) { CG_Error("CG_RegisterClientModelname( %s, %s, %s, %s %s ) failed", ci->modelName, ci->skinName, ci->headModelName, ci->headSkinName, teamname); } // fall back to default team name if(cgs.gametype >= GT_TEAM) { // keep skin name if(ci->team == TEAM_BLUE) { Q_strncpyz(teamname, DEFAULT_BLUETEAM_NAME, sizeof(teamname)); } else { Q_strncpyz(teamname, DEFAULT_REDTEAM_NAME, sizeof(teamname)); } if(!CG_RegisterClientModelname(ci, DEFAULT_TEAM_MODEL, ci->skinName, DEFAULT_TEAM_HEAD, ci->skinName, teamname)) { CG_Error("DEFAULT_TEAM_MODEL / skin (%s/%s) failed to register", DEFAULT_TEAM_MODEL, ci->skinName); } } else { if(!CG_RegisterClientModelname(ci, DEFAULT_MODEL, "default", DEFAULT_MODEL, "default", teamname)) { CG_Error("DEFAULT_MODEL (%s) failed to register", DEFAULT_MODEL); } } modelloaded = qfalse; } ci->newAnims = qfalse; if(ci->torsoModel) { orientation_t tag; // if the torso model has the "tag_flag" if(trap_R_LerpTag(&tag, ci->torsoModel, 0, 0, 1, "tag_flag")) { ci->newAnims = qtrue; } } // sounds dir = ci->modelName; fallback = (cgs.gametype >= GT_TEAM) ? DEFAULT_TEAM_MODEL : DEFAULT_MODEL; for(i = 0; i < MAX_CUSTOM_SOUNDS; i++) { s = cg_customSoundNames[i]; if(!s) { break; } ci->sounds[i] = 0; // if the model didn't load use the sounds of the default model if(modelloaded) { 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); } } ci->deferred = 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->modelIcon = from->modelIcon; to->newAnims = from->newAnims; 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) && !Q_stricmp(ci->headModelName, match->headModelName) && !Q_stricmp(ci->headSkinName, match->headSkinName) && !Q_stricmp(ci->blueTeam, match->blueTeam) && !Q_stricmp(ci->redTeam, match->redTeam) && (cgs.gametype < GT_TEAM || ci->team == match->team)) { // 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 i; clientInfo_t *match; // if someone else is already the same models and skins we // can just load the client info for(i = 0; i < cgs.maxclients; i++) { match = &cgs.clientinfo[i]; if(!match->infoValid || match->deferred) { continue; } if(Q_stricmp(ci->skinName, match->skinName) || Q_stricmp(ci->modelName, match->modelName) || // Q_stricmp( ci->headModelName, match->headModelName ) || // Q_stricmp( ci->headSkinName, match->headSkinName ) || (cgs.gametype >= GT_TEAM && ci->team != match->team)) { continue; } // just load the real info cause it uses the same models and skins CG_LoadClientInfo(ci); return; } // if we are in teamplay, only grab a model if the skin is correct if(cgs.gametype >= GT_TEAM) { for(i = 0; i < cgs.maxclients; i++) { match = &cgs.clientinfo[i]; if(!match->infoValid || match->deferred) { continue; } if(Q_stricmp(ci->skinName, match->skinName) || (cgs.gametype >= GT_TEAM && ci->team != match->team)) { 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); 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); } /* ====================== 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.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); v = Info_ValueForKey(configstring, "g_redteam"); Q_strncpyz(newInfo.redTeam, v, MAX_TEAMNAME); v = Info_ValueForKey(configstring, "g_blueteam"); Q_strncpyz(newInfo.blueTeam, v, MAX_TEAMNAME); // 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; if(cgs.gametype >= GT_TEAM) { Q_strncpyz(newInfo.modelName, DEFAULT_TEAM_MODEL, sizeof(newInfo.modelName)); Q_strncpyz(newInfo.skinName, "default", sizeof(newInfo.skinName)); } else { 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)); } 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; } } // head model v = Info_ValueForKey(configstring, "hmodel"); if(cg_forceModel.integer) { // forcemodel makes everyone use a single model // to prevent load hitches char modelStr[MAX_QPATH]; char *skin; if(cgs.gametype >= GT_TEAM) { Q_strncpyz(newInfo.headModelName, DEFAULT_TEAM_MODEL, sizeof(newInfo.headModelName)); Q_strncpyz(newInfo.headSkinName, "default", sizeof(newInfo.headSkinName)); } else { trap_Cvar_VariableStringBuffer("headmodel", modelStr, sizeof(modelStr)); if((skin = strchr(modelStr, '/')) == NULL) { skin = "default"; } else { *skin++ = 0; } Q_strncpyz(newInfo.headSkinName, skin, sizeof(newInfo.headSkinName)); Q_strncpyz(newInfo.headModelName, modelStr, sizeof(newInfo.headModelName)); } if(cgs.gametype >= GT_TEAM) { // keep skin name slash = strchr(v, '/'); if(slash) { Q_strncpyz(newInfo.headSkinName, slash + 1, sizeof(newInfo.headSkinName)); } } } else { 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; } } // 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() < 4000000; // if we are defering loads, just have it pick the first valid if(forceDefer || (cg_deferPlayers.integer && !cg_buildScript.integer && !cg.loading)) { // keep whatever they had if it won't violate team skins CG_SetDeferredClientInfo(&newInfo); // 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); } } // 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); // 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_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_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, 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; 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, clientNum; clientInfo_t *ci; 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.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); } //========================================================================== /* =============== CG_HasteTrail =============== */ static void CG_HasteTrail(centity_t * cent) { localEntity_t *smoke; vec3_t origin; 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[2] -= 16; smoke = CG_SmokePuff(origin, vec3_origin, 8, 1, 1, 1, 1, 500, cg.time, 0, 0, cgs.media.hastePuffShader); // use the optimized local entity add smoke->leType = LE_SCALE_FADE; } #ifdef MISSIONPACK /* =============== CG_BreathPuffs =============== */ static void CG_BreathPuffs(centity_t * cent, refEntity_t * head) { clientInfo_t *ci; vec3_t up, origin; int contents; ci = &cgs.clientinfo[cent->currentState.number]; if(!cg_enableBreath.integer) { return; } if(cent->currentState.number == cg.snap->ps.clientNum && !cg.renderingThirdPerson) { return; } if(cent->currentState.eFlags & EF_DEAD) { return; } contents = trap_CM_PointContents(head->origin, 0); if(contents & (CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA)) { return; } if(ci->breathPuffTime > cg.time) { return; } VectorSet(up, 0, 0, 8); VectorMA(head->origin, 8, head->axis[0], origin); VectorMA(origin, -4, head->axis[2], origin); CG_SmokePuff(origin, up, 16, 1, 1, 1, 0.66f, 1500, cg.time, cg.time + 400, LEF_PUFF_DONT_SCALE, cgs.media.shotgunSmokePuffShader); ci->breathPuffTime = cg.time + 2000; } /* =============== CG_DustTrail =============== */ static void CG_DustTrail(centity_t * cent) { int anim; localEntity_t *dust; vec3_t end, vel; trace_t tr; if(!cg_enableDust.integer) return; if(cent->dustTrailTime > cg.time) { return; } anim = cent->pe.legs.animationNumber & ~ANIM_TOGGLEBIT; if(anim != LEGS_LANDB && anim != LEGS_LAND) { return; } cent->dustTrailTime += 40; if(cent->dustTrailTime < cg.time) { cent->dustTrailTime = cg.time; } VectorCopy(cent->currentState.pos.trBase, end); end[2] -= 64; CG_Trace(&tr, cent->currentState.pos.trBase, NULL, NULL, end, cent->currentState.number, MASK_PLAYERSOLID); if(!(tr.surfaceFlags & SURF_DUST)) return; VectorCopy(cent->currentState.pos.trBase, end); end[2] -= 16; VectorSet(vel, 0, 0, -30); dust = CG_SmokePuff(end, vel, 24, .8f, .8f, 0.7f, 0.33f, 500, cg.time, 0, 0, cgs.media.dustPuffShader); } #endif /* =============== CG_TrailItem =============== */ static void CG_TrailItem(centity_t * cent, qhandle_t hModel) { refEntity_t ent; vec3_t angles; vec3_t axis[3]; VectorCopy(cent->lerpAngles, angles); angles[PITCH] = 0; angles[ROLL] = 0; AnglesToAxis(angles, axis); memset(&ent, 0, sizeof(ent)); VectorMA(cent->lerpOrigin, -16, axis[0], ent.origin); ent.origin[2] += 16; angles[YAW] += 90; AnglesToAxis(angles, ent.axis); ent.hModel = hModel; trap_R_AddRefEntityToScene(&ent); } /* =============== CG_PlayerFlag =============== */ static void CG_PlayerFlag(centity_t * cent, qhandle_t hSkin, refEntity_t * torso) { clientInfo_t *ci; refEntity_t pole; refEntity_t flag; vec3_t angles, dir; int legsAnim, flagAnim, updateangles; float angle, d; // show the flag pole model memset(&pole, 0, sizeof(pole)); pole.hModel = cgs.media.flagPoleModel; VectorCopy(torso->lightingOrigin, pole.lightingOrigin); pole.shadowPlane = torso->shadowPlane; pole.renderfx = torso->renderfx; CG_PositionEntityOnTag(&pole, torso, torso->hModel, "tag_flag"); trap_R_AddRefEntityToScene(&pole); // show the flag model memset(&flag, 0, sizeof(flag)); flag.hModel = cgs.media.flagFlapModel; flag.customSkin = hSkin; VectorCopy(torso->lightingOrigin, flag.lightingOrigin); flag.shadowPlane = torso->shadowPlane; flag.renderfx = torso->renderfx; VectorClear(angles); updateangles = qfalse; legsAnim = cent->currentState.legsAnim & ~ANIM_TOGGLEBIT; if(legsAnim == LEGS_IDLE || legsAnim == LEGS_IDLECR) { flagAnim = FLAG_STAND; } else if(legsAnim == LEGS_WALK || legsAnim == LEGS_WALKCR) { flagAnim = FLAG_STAND; updateangles = qtrue; } else { flagAnim = FLAG_RUN; updateangles = qtrue; } if(updateangles) { VectorCopy(cent->currentState.pos.trDelta, dir); // add gravity dir[2] += 100; VectorNormalize(dir); d = DotProduct(pole.axis[2], dir); // if there is anough movement orthogonal to the flag pole if(fabs(d) < 0.9) { // d = DotProduct(pole.axis[0], dir); if(d > 1.0f) { d = 1.0f; } else if(d < -1.0f) { d = -1.0f; } angle = acos(d); d = DotProduct(pole.axis[1], dir); if(d < 0) { angles[YAW] = 360 - angle * 180 / M_PI; } else { angles[YAW] = angle * 180 / M_PI; } if(angles[YAW] < 0) angles[YAW] += 360; if(angles[YAW] > 360) angles[YAW] -= 360; //vectoangles( cent->currentState.pos.trDelta, tmpangles ); //angles[YAW] = tmpangles[YAW] + 45 - cent->pe.torso.yawAngle; // change the yaw angle CG_SwingAngles(angles[YAW], 25, 90, 0.15f, ¢->pe.flag.yawAngle, ¢->pe.flag.yawing); } /* d = DotProduct(pole.axis[2], dir); angle = Q_acos(d); d = DotProduct(pole.axis[1], dir); if (d < 0) { angle = 360 - angle * 180 / M_PI; } else { angle = angle * 180 / M_PI; } if (angle > 340 && angle < 20) { flagAnim = FLAG_RUNUP; } if (angle > 160 && angle < 200) { flagAnim = FLAG_RUNDOWN; } */ } // set the yaw angle angles[YAW] = cent->pe.flag.yawAngle; // lerp the flag animation frames ci = &cgs.clientinfo[cent->currentState.clientNum]; CG_RunLerpFrame(ci, ¢->pe.flag, flagAnim, 1); flag.oldframe = cent->pe.flag.oldFrame; flag.frame = cent->pe.flag.frame; flag.backlerp = cent->pe.flag.backlerp; AnglesToAxis(angles, flag.axis); CG_PositionRotatedEntityOnTag(&flag, &pole, pole.hModel, "tag_flag"); trap_R_AddRefEntityToScene(&flag); } #ifdef MISSIONPACK // bk001204 /* =============== CG_PlayerTokens =============== */ static void CG_PlayerTokens(centity_t * cent, int renderfx) { int tokens, i, j; float angle; refEntity_t ent; vec3_t dir, origin; skulltrail_t *trail; trail = &cg.skulltrails[cent->currentState.number]; tokens = cent->currentState.generic1; if(!tokens) { trail->numpositions = 0; return; } if(tokens > MAX_SKULLTRAIL) { tokens = MAX_SKULLTRAIL; } // add skulls if there are more than last time for(i = 0; i < tokens - trail->numpositions; i++) { for(j = trail->numpositions; j > 0; j--) { VectorCopy(trail->positions[j - 1], trail->positions[j]); } VectorCopy(cent->lerpOrigin, trail->positions[0]); } trail->numpositions = tokens; // move all the skulls along the trail VectorCopy(cent->lerpOrigin, origin); for(i = 0; i < trail->numpositions; i++) { VectorSubtract(trail->positions[i], origin, dir); if(VectorNormalize(dir) > 30) { VectorMA(origin, 30, dir, trail->positions[i]); } VectorCopy(trail->positions[i], origin); } memset(&ent, 0, sizeof(ent)); if(cgs.clientinfo[cent->currentState.clientNum].team == TEAM_BLUE) { ent.hModel = cgs.media.redCubeModel; } else { ent.hModel = cgs.media.blueCubeModel; } ent.renderfx = renderfx; VectorCopy(cent->lerpOrigin, origin); for(i = 0; i < trail->numpositions; i++) { VectorSubtract(origin, trail->positions[i], ent.axis[0]); ent.axis[0][2] = 0; VectorNormalize(ent.axis[0]); VectorSet(ent.axis[2], 0, 0, 1); CrossProduct(ent.axis[0], ent.axis[2], ent.axis[1]); VectorCopy(trail->positions[i], ent.origin); angle = (((cg.time + 500 * MAX_SKULLTRAIL - 500 * i) / 16) & 255) * (M_PI * 2) / 255; ent.origin[2] += sin(angle) * 10; trap_R_AddRefEntityToScene(&ent); VectorCopy(trail->positions[i], origin); } } #endif /* =============== CG_PlayerPowerups =============== */ static void CG_PlayerPowerups(centity_t * cent, refEntity_t * torso) { int powerups; clientInfo_t *ci; 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.2f, 0.2f, 1); } // flight plays a looped sound if(powerups & (1 << PW_FLIGHT)) { trap_S_AddLoopingSound(cent->currentState.number, cent->lerpOrigin, vec3_origin, cgs.media.flightSound); } ci = &cgs.clientinfo[cent->currentState.clientNum]; // redflag if(powerups & (1 << PW_REDFLAG)) { if(ci->newAnims) { CG_PlayerFlag(cent, cgs.media.redFlagFlapSkin, torso); } else { CG_TrailItem(cent, cgs.media.redFlagModel); } trap_R_AddLightToScene(cent->lerpOrigin, 200 + (rand() & 31), 1.0, 0.2f, 0.2f); } // blueflag if(powerups & (1 << PW_BLUEFLAG)) { if(ci->newAnims) { CG_PlayerFlag(cent, cgs.media.blueFlagFlapSkin, torso); } else { CG_TrailItem(cent, cgs.media.blueFlagModel); } trap_R_AddLightToScene(cent->lerpOrigin, 200 + (rand() & 31), 0.2f, 0.2f, 1.0); } // neutralflag if(powerups & (1 << PW_NEUTRALFLAG)) { if(ci->newAnims) { CG_PlayerFlag(cent, cgs.media.neutralFlagFlapSkin, torso); } else { CG_TrailItem(cent, cgs.media.neutralFlagModel); } trap_R_AddLightToScene(cent->lerpOrigin, 200 + (rand() & 31), 1.0, 1.0, 1.0); } // haste leaves smoke trails if(powerups & (1 << PW_HASTE)) { CG_HasteTrail(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; 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) { 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.balloonShader); 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_GAUNTLET) { CG_PlayerFloatSprite(cent, cgs.media.medalGauntlet); return; } if(cent->currentState.eFlags & EF_AWARD_DEFEND) { CG_PlayerFloatSprite(cent, cgs.media.medalDefend); return; } if(cent->currentState.eFlags & EF_AWARD_ASSIST) { CG_PlayerFloatSprite(cent, cgs.media.medalAssist); return; } if(cent->currentState.eFlags & EF_AWARD_CAP) { CG_PlayerFloatSprite(cent, cgs.media.medalCapture); 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) { if(cg_drawFriend.integer) { CG_PlayerFloatSprite(cent, cgs.media.friendShader); } 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 = { -15, -15, 0 }, maxs = { 15, 15, 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 || trace.startsolid || trace.allsolid) { 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; // bk0101022 - hack / FPE - bogus planes? //assert( DotProduct( trace.plane.normal, trace.plane.normal ) != 0.0f ) // 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, alpha, alpha, alpha, 1, qfalse, 24, 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); } /* =============== 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, entityState_t * state, int team) { if(state->powerups & (1 << PW_INVIS)) { ent->customShader = cgs.media.invisShader; trap_R_AddRefEntityToScene(ent); } else { /* if ( state->eFlags & EF_KAMIKAZE ) { if (team == TEAM_BLUE) ent->customShader = cgs.media.blueKamikazeShader; else ent->customShader = cgs.media.redKamikazeShader; trap_R_AddRefEntityToScene( ent ); } else { */ trap_R_AddRefEntityToScene(ent); //} if(state->powerups & (1 << PW_QUAD)) { if(team == TEAM_RED) ent->customShader = cgs.media.redQuadShader; else ent->customShader = cgs.media.quadShader; trap_R_AddRefEntityToScene(ent); } if(state->powerups & (1 << PW_REGEN)) { if(((cg.time / 100) % 10) == 1) { ent->customShader = cgs.media.regenShader; trap_R_AddRefEntityToScene(ent); } } if(state->powerups & (1 << PW_BATTLESUIT)) { ent->customShader = cgs.media.battleSuitShader; trap_R_AddRefEntityToScene(ent); } } } /* ================= 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_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; #ifdef MISSIONPACK refEntity_t skull; refEntity_t powerup; int t; float c; float angle; vec3_t dir, angles; #endif // 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; } // get the player model information renderfx = 0; if(cent->currentState.number == cg.snap->ps.clientNum) { if(!cg.renderingThirdPerson) { renderfx = RF_THIRD_PERSON; // only draw in mirrors } else { if(cg_cameraMode.integer) { 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 the talk baloon or disconnect icon CG_PlayerSprites(cent); // add the shadow shadow = CG_PlayerShadow(cent, &shadowPlane); // add a water splash if partially in and out of water CG_PlayerSplash(cent); if(cg_shadows.integer == 2 && shadow) { renderfx |= RF_SHADOW_PLANE; } renderfx |= RF_LIGHTING_ORIGIN; // use the same origin for all #ifdef MISSIONPACK if(cgs.gametype == GT_HARVESTER) { CG_PlayerTokens(cent, renderfx); } #endif // // 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 CG_AddRefEntityWithPowerups(&legs, ¢->currentState, ci->team); // 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, ¢->currentState, ci->team); #ifdef MISSIONPACK if(cent->currentState.eFlags & EF_KAMIKAZE) { memset(&skull, 0, sizeof(skull)); VectorCopy(cent->lerpOrigin, skull.lightingOrigin); skull.shadowPlane = shadowPlane; skull.renderfx = renderfx; if(cent->currentState.eFlags & EF_DEAD) { // one skull bobbing above the dead body angle = ((cg.time / 7) & 255) * (M_PI * 2) / 255; if(angle > M_PI * 2) angle -= (float)M_PI *2; dir[0] = sin(angle) * 20; dir[1] = cos(angle) * 20; angle = ((cg.time / 4) & 255) * (M_PI * 2) / 255; dir[2] = 15 + sin(angle) * 8; VectorAdd(torso.origin, dir, skull.origin); dir[2] = 0; VectorCopy(dir, skull.axis[1]); VectorNormalize(skull.axis[1]); VectorSet(skull.axis[2], 0, 0, 1); CrossProduct(skull.axis[1], skull.axis[2], skull.axis[0]); skull.hModel = cgs.media.kamikazeHeadModel; trap_R_AddRefEntityToScene(&skull); skull.hModel = cgs.media.kamikazeHeadTrail; trap_R_AddRefEntityToScene(&skull); } else { // three skulls spinning around the player angle = ((cg.time / 4) & 255) * (M_PI * 2) / 255; dir[0] = cos(angle) * 20; dir[1] = sin(angle) * 20; dir[2] = cos(angle) * 20; VectorAdd(torso.origin, dir, skull.origin); angles[0] = sin(angle) * 30; angles[1] = (angle * 180 / M_PI) + 90; if(angles[1] > 360) angles[1] -= 360; angles[2] = 0; AnglesToAxis(angles, skull.axis); /* dir[2] = 0; VectorInverse(dir); VectorCopy(dir, skull.axis[1]); VectorNormalize(skull.axis[1]); VectorSet(skull.axis[2], 0, 0, 1); CrossProduct(skull.axis[1], skull.axis[2], skull.axis[0]); */ skull.hModel = cgs.media.kamikazeHeadModel; trap_R_AddRefEntityToScene(&skull); // flip the trail because this skull is spinning in the other direction VectorInverse(skull.axis[1]); skull.hModel = cgs.media.kamikazeHeadTrail; trap_R_AddRefEntityToScene(&skull); angle = ((cg.time / 4) & 255) * (M_PI * 2) / 255 + M_PI; if(angle > M_PI * 2) angle -= (float)M_PI *2; dir[0] = sin(angle) * 20; dir[1] = cos(angle) * 20; dir[2] = cos(angle) * 20; VectorAdd(torso.origin, dir, skull.origin); angles[0] = cos(angle - 0.5 * M_PI) * 30; angles[1] = 360 - (angle * 180 / M_PI); if(angles[1] > 360) angles[1] -= 360; angles[2] = 0; AnglesToAxis(angles, skull.axis); /* dir[2] = 0; VectorCopy(dir, skull.axis[1]); VectorNormalize(skull.axis[1]); VectorSet(skull.axis[2], 0, 0, 1); CrossProduct(skull.axis[1], skull.axis[2], skull.axis[0]); */ skull.hModel = cgs.media.kamikazeHeadModel; trap_R_AddRefEntityToScene(&skull); skull.hModel = cgs.media.kamikazeHeadTrail; trap_R_AddRefEntityToScene(&skull); angle = ((cg.time / 3) & 255) * (M_PI * 2) / 255 + 0.5 * M_PI; if(angle > M_PI * 2) angle -= (float)M_PI *2; dir[0] = sin(angle) * 20; dir[1] = cos(angle) * 20; dir[2] = 0; VectorAdd(torso.origin, dir, skull.origin); VectorCopy(dir, skull.axis[1]); VectorNormalize(skull.axis[1]); VectorSet(skull.axis[2], 0, 0, 1); CrossProduct(skull.axis[1], skull.axis[2], skull.axis[0]); skull.hModel = cgs.media.kamikazeHeadModel; trap_R_AddRefEntityToScene(&skull); skull.hModel = cgs.media.kamikazeHeadTrail; trap_R_AddRefEntityToScene(&skull); } } if(cent->currentState.powerups & (1 << PW_GUARD)) { memcpy(&powerup, &torso, sizeof(torso)); powerup.hModel = cgs.media.guardPowerupModel; powerup.frame = 0; powerup.oldframe = 0; powerup.customSkin = 0; trap_R_AddRefEntityToScene(&powerup); } if(cent->currentState.powerups & (1 << PW_SCOUT)) { memcpy(&powerup, &torso, sizeof(torso)); powerup.hModel = cgs.media.scoutPowerupModel; powerup.frame = 0; powerup.oldframe = 0; powerup.customSkin = 0; trap_R_AddRefEntityToScene(&powerup); } if(cent->currentState.powerups & (1 << PW_DOUBLER)) { memcpy(&powerup, &torso, sizeof(torso)); powerup.hModel = cgs.media.doublerPowerupModel; powerup.frame = 0; powerup.oldframe = 0; powerup.customSkin = 0; trap_R_AddRefEntityToScene(&powerup); } if(cent->currentState.powerups & (1 << PW_AMMOREGEN)) { memcpy(&powerup, &torso, sizeof(torso)); powerup.hModel = cgs.media.ammoRegenPowerupModel; powerup.frame = 0; powerup.oldframe = 0; powerup.customSkin = 0; trap_R_AddRefEntityToScene(&powerup); } if(cent->currentState.powerups & (1 << PW_INVULNERABILITY)) { if(!ci->invulnerabilityStartTime) { ci->invulnerabilityStartTime = cg.time; } ci->invulnerabilityStopTime = cg.time; } else { ci->invulnerabilityStartTime = 0; } if((cent->currentState.powerups & (1 << PW_INVULNERABILITY)) || cg.time - ci->invulnerabilityStopTime < 250) { memcpy(&powerup, &torso, sizeof(torso)); powerup.hModel = cgs.media.invulnerabilityPowerupModel; powerup.customSkin = 0; // always draw powerup.renderfx &= ~RF_THIRD_PERSON; VectorCopy(cent->lerpOrigin, powerup.origin); if(cg.time - ci->invulnerabilityStartTime < 250) { c = (float)(cg.time - ci->invulnerabilityStartTime) / 250; } else if(cg.time - ci->invulnerabilityStopTime < 250) { c = (float)(250 - (cg.time - ci->invulnerabilityStopTime)) / 250; } else { c = 1; } VectorSet(powerup.axis[0], c, 0, 0); VectorSet(powerup.axis[1], 0, c, 0); VectorSet(powerup.axis[2], 0, 0, c); trap_R_AddRefEntityToScene(&powerup); } t = cg.time - ci->medkitUsageTime; if(ci->medkitUsageTime && t < 500) { memcpy(&powerup, &torso, sizeof(torso)); powerup.hModel = cgs.media.medkitUsageModel; powerup.customSkin = 0; // always draw powerup.renderfx &= ~RF_THIRD_PERSON; VectorClear(angles); AnglesToAxis(angles, powerup.axis); VectorCopy(cent->lerpOrigin, powerup.origin); powerup.origin[2] += -24 + (float)t *80 / 500; if(t > 400) { c = (float)(t - 1000) * 0xff / 100; powerup.shaderRGBA[0] = 0xff - c; powerup.shaderRGBA[1] = 0xff - c; powerup.shaderRGBA[2] = 0xff - c; powerup.shaderRGBA[3] = 0xff - c; } else { powerup.shaderRGBA[0] = 0xff; powerup.shaderRGBA[1] = 0xff; powerup.shaderRGBA[2] = 0xff; powerup.shaderRGBA[3] = 0xff; } trap_R_AddRefEntityToScene(&powerup); } #endif // MISSIONPACK // // 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, ¢->currentState, ci->team); #ifdef MISSIONPACK CG_BreathPuffs(cent, &head); CG_DustTrail(cent); #endif // // add the gun / barrel / flash // CG_AddPlayerWeapon(&torso, NULL, cent, ci->team); // add powerups floating behind the player CG_PlayerPowerups(cent, &torso); } //===================================================================== /* =============== 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); } }