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