/* =========================================================================== 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_playerstate.c -- this file acts on changes in a new playerState_t // With normal play, this will be done after local prediction, but when // following another player or playing back a demo, it will be checked // when the snapshot transitions like all the other entities #include "cg_local.h" /* ============== CG_CheckAmmo If the ammo has gone low enough to generate the warning, play a sound ============== */ void CG_CheckAmmo(void) { int i; int total; int previous; int weapons; // see about how many seconds of ammo we have remaining weapons = cg.snap->ps.stats[STAT_WEAPONS]; total = 0; for(i = WP_MACHINEGUN; i < WP_NUM_WEAPONS; i++) { if(!(weapons & (1 << i))) { continue; } switch (i) { case WP_ROCKET_LAUNCHER: case WP_GRENADE_LAUNCHER: case WP_RAILGUN: case WP_SHOTGUN: #ifdef MISSIONPACK case WP_PROX_LAUNCHER: #endif total += cg.snap->ps.ammo[i] * 1000; break; default: total += cg.snap->ps.ammo[i] * 200; break; } if(total >= 5000) { cg.lowAmmoWarning = 0; return; } } previous = cg.lowAmmoWarning; if(total == 0) { cg.lowAmmoWarning = 2; } else { cg.lowAmmoWarning = 1; } // play a sound on transitions if(cg.lowAmmoWarning != previous) { trap_S_StartLocalSound(cgs.media.noAmmoSound, CHAN_LOCAL_SOUND); } } /* ============== CG_DamageFeedback ============== */ void CG_DamageFeedback(int yawByte, int pitchByte, int damage) { float left, front, up; float kick; int health; float scale; vec3_t dir; vec3_t angles; float dist; float yaw, pitch; // show the attacking player's head and name in corner cg.attackerTime = cg.time; // the lower on health you are, the greater the view kick will be health = cg.snap->ps.stats[STAT_HEALTH]; if(health < 40) { scale = 1; } else { scale = 40.0 / health; } kick = damage * scale; if(kick < 5) kick = 5; if(kick > 10) kick = 10; // if yaw and pitch are both 255, make the damage always centered (falling, etc) if(yawByte == 255 && pitchByte == 255) { cg.damageX = 0; cg.damageY = 0; cg.v_dmg_roll = 0; cg.v_dmg_pitch = -kick; } else { // positional pitch = pitchByte / 255.0 * 360; yaw = yawByte / 255.0 * 360; angles[PITCH] = pitch; angles[YAW] = yaw; angles[ROLL] = 0; AngleVectors(angles, dir, NULL, NULL); VectorSubtract(vec3_origin, dir, dir); front = DotProduct(dir, cg.refdef.viewaxis[0]); left = DotProduct(dir, cg.refdef.viewaxis[1]); up = DotProduct(dir, cg.refdef.viewaxis[2]); dir[0] = front; dir[1] = left; dir[2] = 0; dist = VectorLength(dir); if(dist < 0.1) { dist = 0.1f; } cg.v_dmg_roll = kick * left; cg.v_dmg_pitch = -kick * front; if(front <= 0.1) { front = 0.1f; } cg.damageX = -left / front; cg.damageY = up / dist; } // clamp the position if(cg.damageX > 1.0) { cg.damageX = 1.0; } if(cg.damageX < -1.0) { cg.damageX = -1.0; } if(cg.damageY > 1.0) { cg.damageY = 1.0; } if(cg.damageY < -1.0) { cg.damageY = -1.0; } // don't let the screen flashes vary as much if(kick > 10) { kick = 10; } cg.damageValue = kick; cg.v_dmg_time = cg.time + DAMAGE_TIME; cg.damageTime = cg.snap->serverTime; } /* ================ CG_Respawn A respawn happened this snapshot ================ */ void CG_Respawn(void) { // no error decay on player movement cg.thisFrameTeleport = qtrue; // display weapons available cg.weaponSelectTime = cg.time; // select the weapon the server says we are using cg.weaponSelect = cg.snap->ps.weapon; } extern char *eventnames[]; /* ============== CG_CheckPlayerstateEvents ============== */ void CG_CheckPlayerstateEvents(playerState_t * ps, playerState_t * ops) { int i; int event; centity_t *cent; if(ps->externalEvent && ps->externalEvent != ops->externalEvent) { cent = &cg_entities[ps->clientNum]; cent->currentState.event = ps->externalEvent; cent->currentState.eventParm = ps->externalEventParm; CG_EntityEvent(cent, cent->lerpOrigin); } cent = &cg.predictedPlayerEntity; // cg_entities[ ps->clientNum ]; // go through the predictable events buffer for(i = ps->eventSequence - MAX_PS_EVENTS; i < ps->eventSequence; i++) { // if we have a new predictable event if(i >= ops->eventSequence // or the server told us to play another event instead of a predicted event we already issued // or something the server told us changed our prediction causing a different event || (i > ops->eventSequence - MAX_PS_EVENTS && ps->events[i & (MAX_PS_EVENTS - 1)] != ops->events[i & (MAX_PS_EVENTS - 1)])) { event = ps->events[i & (MAX_PS_EVENTS - 1)]; cent->currentState.event = event; cent->currentState.eventParm = ps->eventParms[i & (MAX_PS_EVENTS - 1)]; CG_EntityEvent(cent, cent->lerpOrigin); cg.predictableEvents[i & (MAX_PREDICTED_EVENTS - 1)] = event; cg.eventSequence++; } } } /* ================== CG_CheckChangedPredictableEvents ================== */ void CG_CheckChangedPredictableEvents(playerState_t * ps) { int i; int event; centity_t *cent; cent = &cg.predictedPlayerEntity; for(i = ps->eventSequence - MAX_PS_EVENTS; i < ps->eventSequence; i++) { // if(i >= cg.eventSequence) { continue; } // if this event is not further back in than the maximum predictable events we remember if(i > cg.eventSequence - MAX_PREDICTED_EVENTS) { // if the new playerstate event is different from a previously predicted one if(ps->events[i & (MAX_PS_EVENTS - 1)] != cg.predictableEvents[i & (MAX_PREDICTED_EVENTS - 1)]) { event = ps->events[i & (MAX_PS_EVENTS - 1)]; cent->currentState.event = event; cent->currentState.eventParm = ps->eventParms[i & (MAX_PS_EVENTS - 1)]; CG_EntityEvent(cent, cent->lerpOrigin); cg.predictableEvents[i & (MAX_PREDICTED_EVENTS - 1)] = event; if(cg_showmiss.integer) { CG_Printf("WARNING: changed predicted event\n"); } } } } } /* ================== pushReward ================== */ static void pushReward(sfxHandle_t sfx, qhandle_t shader, int rewardCount) { if(cg.rewardStack < (MAX_REWARDSTACK - 1)) { cg.rewardStack++; cg.rewardSound[cg.rewardStack] = sfx; cg.rewardShader[cg.rewardStack] = shader; cg.rewardCount[cg.rewardStack] = rewardCount; } } /* ================== CG_CheckLocalSounds ================== */ void CG_CheckLocalSounds(playerState_t * ps, playerState_t * ops) { int highScore, health, armor, reward; sfxHandle_t sfx; // don't play the sounds if the player just changed teams if(ps->persistant[PERS_TEAM] != ops->persistant[PERS_TEAM]) { return; } // hit changes if(ps->persistant[PERS_HITS] > ops->persistant[PERS_HITS]) { armor = ps->persistant[PERS_ATTACKEE_ARMOR] & 0xff; health = ps->persistant[PERS_ATTACKEE_ARMOR] >> 8; #ifdef MISSIONPACK if(armor > 50) { trap_S_StartLocalSound(cgs.media.hitSoundHighArmor, CHAN_LOCAL_SOUND); } else if(armor || health > 100) { trap_S_StartLocalSound(cgs.media.hitSoundLowArmor, CHAN_LOCAL_SOUND); } else { trap_S_StartLocalSound(cgs.media.hitSound, CHAN_LOCAL_SOUND); } #else trap_S_StartLocalSound(cgs.media.hitSound, CHAN_LOCAL_SOUND); #endif } else if(ps->persistant[PERS_HITS] < ops->persistant[PERS_HITS]) { trap_S_StartLocalSound(cgs.media.hitTeamSound, CHAN_LOCAL_SOUND); } // health changes of more than -1 should make pain sounds if(ps->stats[STAT_HEALTH] < ops->stats[STAT_HEALTH] - 1) { if(ps->stats[STAT_HEALTH] > 0) { CG_PainEvent(&cg.predictedPlayerEntity, ps->stats[STAT_HEALTH]); } } // if we are going into the intermission, don't start any voices if(cg.intermissionStarted) { return; } // reward sounds reward = qfalse; if(ps->persistant[PERS_CAPTURES] != ops->persistant[PERS_CAPTURES]) { pushReward(cgs.media.captureAwardSound, cgs.media.medalCapture, ps->persistant[PERS_CAPTURES]); reward = qtrue; //Com_Printf("capture\n"); } if(ps->persistant[PERS_IMPRESSIVE_COUNT] != ops->persistant[PERS_IMPRESSIVE_COUNT]) { #ifdef MISSIONPACK if(ps->persistant[PERS_IMPRESSIVE_COUNT] == 1) { sfx = cgs.media.firstImpressiveSound; } else { sfx = cgs.media.impressiveSound; } #else sfx = cgs.media.impressiveSound; #endif pushReward(sfx, cgs.media.medalImpressive, ps->persistant[PERS_IMPRESSIVE_COUNT]); reward = qtrue; //Com_Printf("impressive\n"); } if(ps->persistant[PERS_EXCELLENT_COUNT] != ops->persistant[PERS_EXCELLENT_COUNT]) { #ifdef MISSIONPACK if(ps->persistant[PERS_EXCELLENT_COUNT] == 1) { sfx = cgs.media.firstExcellentSound; } else { sfx = cgs.media.excellentSound; } #else sfx = cgs.media.excellentSound; #endif pushReward(sfx, cgs.media.medalExcellent, ps->persistant[PERS_EXCELLENT_COUNT]); reward = qtrue; //Com_Printf("excellent\n"); } if(ps->persistant[PERS_GAUNTLET_FRAG_COUNT] != ops->persistant[PERS_GAUNTLET_FRAG_COUNT]) { #ifdef MISSIONPACK if(ops->persistant[PERS_GAUNTLET_FRAG_COUNT] == 1) { sfx = cgs.media.firstHumiliationSound; } else { sfx = cgs.media.humiliationSound; } #else sfx = cgs.media.humiliationSound; #endif pushReward(sfx, cgs.media.medalGauntlet, ps->persistant[PERS_GAUNTLET_FRAG_COUNT]); reward = qtrue; //Com_Printf("guantlet frag\n"); } if(ps->persistant[PERS_DEFEND_COUNT] != ops->persistant[PERS_DEFEND_COUNT]) { pushReward(cgs.media.defendSound, cgs.media.medalDefend, ps->persistant[PERS_DEFEND_COUNT]); reward = qtrue; //Com_Printf("defend\n"); } if(ps->persistant[PERS_ASSIST_COUNT] != ops->persistant[PERS_ASSIST_COUNT]) { pushReward(cgs.media.assistSound, cgs.media.medalAssist, ps->persistant[PERS_ASSIST_COUNT]); reward = qtrue; //Com_Printf("assist\n"); } // if any of the player event bits changed if(ps->persistant[PERS_PLAYEREVENTS] != ops->persistant[PERS_PLAYEREVENTS]) { if((ps->persistant[PERS_PLAYEREVENTS] & PLAYEREVENT_DENIEDREWARD) != (ops->persistant[PERS_PLAYEREVENTS] & PLAYEREVENT_DENIEDREWARD)) { trap_S_StartLocalSound(cgs.media.deniedSound, CHAN_ANNOUNCER); } else if((ps->persistant[PERS_PLAYEREVENTS] & PLAYEREVENT_GAUNTLETREWARD) != (ops->persistant[PERS_PLAYEREVENTS] & PLAYEREVENT_GAUNTLETREWARD)) { trap_S_StartLocalSound(cgs.media.humiliationSound, CHAN_ANNOUNCER); } else if((ps->persistant[PERS_PLAYEREVENTS] & PLAYEREVENT_HOLYSHIT) != (ops->persistant[PERS_PLAYEREVENTS] & PLAYEREVENT_HOLYSHIT)) { trap_S_StartLocalSound(cgs.media.holyShitSound, CHAN_ANNOUNCER); } reward = qtrue; } // check for flag pickup if(cgs.gametype >= GT_TEAM) { if((ps->powerups[PW_REDFLAG] != ops->powerups[PW_REDFLAG] && ps->powerups[PW_REDFLAG]) || (ps->powerups[PW_BLUEFLAG] != ops->powerups[PW_BLUEFLAG] && ps->powerups[PW_BLUEFLAG]) || (ps->powerups[PW_NEUTRALFLAG] != ops->powerups[PW_NEUTRALFLAG] && ps->powerups[PW_NEUTRALFLAG])) { trap_S_StartLocalSound(cgs.media.youHaveFlagSound, CHAN_ANNOUNCER); } } // lead changes if(!reward) { // if(!cg.warmup) { // never play lead changes during warmup if(ps->persistant[PERS_RANK] != ops->persistant[PERS_RANK]) { if(cgs.gametype < GT_TEAM) { if(ps->persistant[PERS_RANK] == 0) { CG_AddBufferedSound(cgs.media.takenLeadSound); } else if(ps->persistant[PERS_RANK] == RANK_TIED_FLAG) { CG_AddBufferedSound(cgs.media.tiedLeadSound); } else if((ops->persistant[PERS_RANK] & ~RANK_TIED_FLAG) == 0) { CG_AddBufferedSound(cgs.media.lostLeadSound); } } } } } // timelimit warnings if(cgs.timelimit > 0) { int msec; msec = cg.time - cgs.levelStartTime; if(!(cg.timelimitWarnings & 4) && msec > (cgs.timelimit * 60 + 2) * 1000) { cg.timelimitWarnings |= 1 | 2 | 4; trap_S_StartLocalSound(cgs.media.suddenDeathSound, CHAN_ANNOUNCER); } else if(!(cg.timelimitWarnings & 2) && msec > (cgs.timelimit - 1) * 60 * 1000) { cg.timelimitWarnings |= 1 | 2; trap_S_StartLocalSound(cgs.media.oneMinuteSound, CHAN_ANNOUNCER); } else if(cgs.timelimit > 5 && !(cg.timelimitWarnings & 1) && msec > (cgs.timelimit - 5) * 60 * 1000) { cg.timelimitWarnings |= 1; trap_S_StartLocalSound(cgs.media.fiveMinuteSound, CHAN_ANNOUNCER); } } // fraglimit warnings if(cgs.fraglimit > 0 && cgs.gametype < GT_CTF) { highScore = cgs.scores1; if(!(cg.fraglimitWarnings & 4) && highScore == (cgs.fraglimit - 1)) { cg.fraglimitWarnings |= 1 | 2 | 4; CG_AddBufferedSound(cgs.media.oneFragSound); } else if(cgs.fraglimit > 2 && !(cg.fraglimitWarnings & 2) && highScore == (cgs.fraglimit - 2)) { cg.fraglimitWarnings |= 1 | 2; CG_AddBufferedSound(cgs.media.twoFragSound); } else if(cgs.fraglimit > 3 && !(cg.fraglimitWarnings & 1) && highScore == (cgs.fraglimit - 3)) { cg.fraglimitWarnings |= 1; CG_AddBufferedSound(cgs.media.threeFragSound); } } } /* =============== CG_TransitionPlayerState =============== */ void CG_TransitionPlayerState(playerState_t * ps, playerState_t * ops) { // check for changing follow mode if(ps->clientNum != ops->clientNum) { cg.thisFrameTeleport = qtrue; // make sure we don't get any unwanted transition effects *ops = *ps; } // damage events (player is getting wounded) if(ps->damageEvent != ops->damageEvent && ps->damageCount) { CG_DamageFeedback(ps->damageYaw, ps->damagePitch, ps->damageCount); } // respawning if(ps->persistant[PERS_SPAWN_COUNT] != ops->persistant[PERS_SPAWN_COUNT]) { CG_Respawn(); } if(cg.mapRestart) { CG_Respawn(); cg.mapRestart = qfalse; } if(cg.snap->ps.pm_type != PM_INTERMISSION && ps->persistant[PERS_TEAM] != TEAM_SPECTATOR) { CG_CheckLocalSounds(ps, ops); } // check for going low on ammo CG_CheckAmmo(); // run events CG_CheckPlayerstateEvents(ps, ops); // smooth the ducking viewheight change if(ps->viewheight != ops->viewheight) { cg.duckChange = ps->viewheight - ops->viewheight; cg.duckTime = cg.time; } }