mirror of
https://github.com/ioquake/ioq3.git
synced 2025-02-25 04:51:12 +00:00
Gauntlet and Grappling Hook use -1 ammo. Gauntlet is excluded from the check ammo loop but Grappling Hook causes ammo time remaining to decrease 200 milliseconds. The out of ammo check tests time remaining is equal to zero. This means carrying Grappling Hook and out of ammo will have negative time remaining which results in the low ammo message being displayed instead of out of ammo.
537 lines
15 KiB
C
537 lines
15 KiB
C
/*
|
|
===========================================================================
|
|
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 Quake III Arena source code; 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;
|
|
}
|
|
if ( cg.cur_ps->ammo[i] < 0 ) {
|
|
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, reward;
|
|
#ifdef MISSIONPACK
|
|
int health, armor;
|
|
#endif
|
|
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] ) {
|
|
#ifdef MISSIONPACK
|
|
armor = ps->persistant[PERS_ATTACKEE_ARMOR] & 0xff;
|
|
health = ps->persistant[PERS_ATTACKEE_ARMOR] >> 8;
|
|
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 (ps->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("gauntlet 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 (cgs.gametype == GT_TEAM && cgs.scores2 > highScore) {
|
|
highScore = cgs.scores2;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|