Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions code/game/g_combat.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,27 @@ void AddScore( gentity_t *ent, vec3_t origin, int score ) {
if ( level.warmupTime ) {
return;
}
// show score plum
ScorePlum(ent, origin, score);

// Ensure that the score doesn't change after match end
// (when `level.intermissionQueued`).
// This check is not present in the original Quake III Arena.
// It has been added mainly for `g_canDamageAfterMatchEnd`.
// This also fixes a perhaps funny bug where you could `\kill`
// right after winning and it would decrease your score.
//
ent->client->ps.persistant[PERS_SCORE] += score;
// Note that we do not early-return from this function,
// because we have to run `AddTeamScore()` in both cases.
// We'll do the same kind of check in `AddTeamScore()`.
if ( !level.intermissionQueued ) {
// show score plum
ScorePlum(ent, origin, score);
//
ent->client->ps.persistant[PERS_SCORE] += score;
}
#ifndef NO_HOLYSHIT_MOD
ent->client->pers.imaginaryScore += score;
#endif

if ( g_gametype.integer == GT_TEAM ) {
AddTeamScore( origin, ent->client->ps.persistant[PERS_TEAM], score );
}
Expand Down Expand Up @@ -465,7 +482,11 @@ void player_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int

self->enemy = attacker;

self->client->ps.persistant[PERS_KILLED]++;
// Check `level.intermissionQueued` so as to not ruin the "Perfect" award
// if the player got killed after the match has already ended.
if ( !level.intermissionQueued ) {
self->client->ps.persistant[PERS_KILLED]++;
}

if (attacker && attacker->client) {
attacker->client->lastkilled_client = self->s.number;
Expand Down Expand Up @@ -815,7 +836,17 @@ void G_Damage( gentity_t *targ, gentity_t *inflictor, gentity_t *attacker,

// the intermission has allready been qualified for, so don't
// allow any extra scoring
if ( level.intermissionQueued ) {
//
// When `g_canDamageAfterMatchEnd 1`,
// we'll do the `level.intermissionQueued` check
// in `AddScore` and `AddTeamScore` instead of doing it here.
//
// Note that when `g_canDamageAfterMatchEnd 1`
// it would make sense to check `level.intermissiontime` instead,
// but that would be further from the original game,
// because `level.intermissionQueued` gets reset to 0
// when intermission starts.
if ( level.intermissionQueued && !g_canDamageAfterMatchEnd.integer ) {

// With a special exception for gibbing bodies.
// This was introduced in https://github.com/ec-/baseq3a/pull/50.
Expand Down
2 changes: 2 additions & 0 deletions code/game/g_cvar.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ G_CVAR( g_synchronousClients, "g_synchronousClients", "0", CVAR_SYSTEMINFO, 0, q

G_CVAR( g_friendlyFire, "g_friendlyFire", "0", CVAR_ARCHIVE, 0, qtrue, qfalse )

G_CVAR( g_canDamageAfterMatchEnd, "g_canDamageAfterMatchEnd", "1", CVAR_ARCHIVE, 0, qtrue, qfalse )

G_CVAR( g_autoJoin, "g_autoJoin", "1", CVAR_ARCHIVE, 0, qfalse, qfalse )
G_CVAR( g_teamForceBalance, "g_teamForceBalance", "0", CVAR_ARCHIVE, 0, qfalse, qfalse )

Expand Down
28 changes: 27 additions & 1 deletion code/game/g_local.h
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,20 @@ typedef struct {
int voted;
int teamVoted;

#ifndef NO_HOLYSHIT_MOD
// These are only valid after match end, when `intermissionQueued`.

// Like `ps.persistant[PERS_SCORE]`, but this can also change
// after match end, i.e. when `level.intermissionQueued`.
int imaginaryScore;
// Whether to play the "Holy shit!" sound if this player's `imaginaryScore`
// reaches `level.winnerScore`.
// This will be `qfalse` for the winner, disconnected and spectator players,
// in team games, and for the players
// for whom we've already played the sound.
qboolean needsHolyshitCheck;
#endif

qboolean inGame;
} clientPersistant_t;

Expand Down Expand Up @@ -368,6 +382,18 @@ typedef struct {
int msec; // current frame duration

int teamScores[TEAM_NUM_TEAMS];
#ifndef NO_HOLYSHIT_MOD
// These are only valid after match end, when `intermissionQueued`.

// The score of the winning player or team when the match ended
// (when `intermissionQueued` became non-zero).
int winnerScore;
// See `clientPersistant_t.imaginaryScore`.
int teamImaginaryScores[TEAM_NUM_TEAMS];
// See `clientPersistant_t.needsHolyshitCheck`.
qboolean teamNeedsHolyshitCheck[TEAM_NUM_TEAMS];
#endif

int lastTeamLocationTime; // last time of client team location update

qboolean newSession; // don't use any old session data, because
Expand Down Expand Up @@ -413,7 +439,7 @@ typedef struct {
// wait INTERMISSION_DELAY_TIME before
// actually going there so the last
// frag can be watched. Disable future
// kills during this delay
// scores during this delay
int intermissiontime; // time the intermission was started
qboolean readyToExit; // at least one client wants to exit
int exitTime;
Expand Down
157 changes: 157 additions & 0 deletions code/game/g_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,150 @@ void QDECL G_LogPrintf( const char *fmt, ... ) {
}


#ifndef NO_HOLYSHIT_MOD
static void PlayGlobalHolyshitSound( void ) {
// Note that there is another way to play this sound,
// i.e. with `PLAYEREVENT_HOLYSHIT`,
// but that is per-player and not global.
gentity_t *te = G_TempEntity( vec3_origin, EV_GLOBAL_SOUND );
te->s.eventParm = G_SoundIndex( "sound/feedback/voc_holyshit.wav" );
te->r.svFlags |= SVF_BROADCAST;
}
static char* FormatHolyshitMsg( const char* name ) {
const int diffMs = level.time - level.intermissionQueued;
return va( "print \"%s" S_COLOR_WHITE " was %s away from not losing\n\"",
name,
diffMs == 0
? "one moment"
: diffMs < 500
// Don't convert to float to avoid precision loss.
? va( "%i.%03is", diffMs / 1000, diffMs % 1000 )
: va( "%i.%01is", diffMs / 1000, (diffMs % 1000)/100 )
);
}

/*
=============
ClearHolyshit

Sync imaginary scores to real scores
and clear all other holy-shit-related values.

This should be called at match end, to ensure that the relevant fields
don't keep values from the previous match or something,
such as after `ExitLevel()`.
=============
*/
static void ClearHolyshit( void ) {
int i;

// assert( sizeof( level.teamImaginaryScores ) == sizeof( level.teamScores ) );
memcpy( level.teamImaginaryScores, level.teamScores, sizeof( level.teamImaginaryScores ) );
memset( level.teamNeedsHolyshitCheck, 0, sizeof( level.teamNeedsHolyshitCheck ) );
// Setting to `MAX_QINT` basically ensures that nobody can reach this score.
// However we expect to never read `winnerScore` when it has this value.
// This is just to be defensive.
level.winnerScore = MAX_QINT;

for ( i = 0 ; i < level.maxclients ; i++ ) {
gclient_t *cl = level.clients + i;

// Not checking `connected` and `sessionTeam` as in `CheckExitRules`,
// just copy scores for all players as is.

cl->pers.imaginaryScore = cl->ps.persistant[PERS_SCORE];
cl->pers.needsHolyshitCheck = qfalse;
}
}
static void InitHolyshit( void ) {
ClearHolyshit();

if ( g_gametype.integer >= GT_TEAM ) {
level.winnerScore = level.teamScores[TEAM_RED] > level.teamScores[TEAM_BLUE]
? level.teamScores[TEAM_RED]
: level.teamScores[TEAM_BLUE];
level.teamNeedsHolyshitCheck[TEAM_RED] =
level.teamScores[TEAM_RED] < level.winnerScore;
level.teamNeedsHolyshitCheck[TEAM_BLUE] =
level.teamScores[TEAM_BLUE] < level.winnerScore;
} else {
int i;
const gclient_t *winner;

if ( level.numPlayingClients < 1 ) {
// This shouldn't happen, but let's still gracefully handle.
// In this case nobody will have `needsHolyshitCheck == qtrue`.
return;
}
// Assuming `CalculateRanks()` has already been called.
winner = &level.clients[ level.sortedClients[0] ];

level.winnerScore = winner->ps.persistant[PERS_SCORE];

for ( i = 0; i < level.numPlayingClients; i++ ) {
gclient_t *cl = &level.clients[ level.sortedClients[i] ];
if ( cl->ps.persistant[PERS_SCORE] < level.winnerScore ) {
cl->pers.needsHolyshitCheck = qtrue;
}
}
}
}
/*
=============
CheckHolyshit

After match end but before intermission (when `level.intermissionQueued`),
check if someone else would have reached the winning score
if the match hadn't ended, and say the line if so.
This includes:
- Hitting the fraglimit after someone else has already hit it.
(`g_canDamageAfterMatchEnd 1` is required for this).
- Tying score after the timelimit has been hit.
- Tying score after sudden death.
- Bringing the cubes after the opponent has brought them.
=============
*/
static void CheckHolyshit( void ) {
int i;

if ( !level.intermissionQueued ) {
// This function should not be called when `!level.intermissionQueued`.
return;
}

if ( level.teamNeedsHolyshitCheck[TEAM_RED] &&
level.teamImaginaryScores[TEAM_RED] >= level.winnerScore )
{
PlayGlobalHolyshitSound();
G_BroadcastServerCommand( -1, FormatHolyshitMsg( "Red" ) );
level.teamNeedsHolyshitCheck[TEAM_RED] = qfalse;
}
if ( level.teamNeedsHolyshitCheck[TEAM_BLUE] &&
level.teamImaginaryScores[TEAM_BLUE] >= level.winnerScore )
{
PlayGlobalHolyshitSound();
G_BroadcastServerCommand( -1, FormatHolyshitMsg( "Blue" ) );
level.teamNeedsHolyshitCheck[TEAM_BLUE] = qfalse;
}

for ( i = 0 ; i < level.maxclients ; i++ ) {
gclient_t *cl = level.clients + i;
if ( cl->pers.connected != CON_CONNECTED ) {
continue;
}

if ( cl->pers.needsHolyshitCheck &&
cl->pers.imaginaryScore >= level.winnerScore )
{
PlayGlobalHolyshitSound();
G_BroadcastServerCommand( -1, FormatHolyshitMsg( cl->pers.netname ) );
cl->pers.needsHolyshitCheck = qfalse;
}
}
}
#endif


/*
================
LogExit
Expand All @@ -1146,6 +1290,15 @@ void LogExit( const char *string ) {
// that will get cut off when the queued intermission starts
trap_SetConfigstring( CS_INTERMISSION, "1" );

#ifndef NO_HOLYSHIT_MOD
InitHolyshit();
// We don't expect a player (or team) to win and another player
// to almost win at the very same instant,
// so we don't need to also `CheckHolyshit()` here.
// Even if that were to happen, let's wait until another `CheckExitRules()`,
// hoping that this situation resolves by then.
#endif

// don't send more than 32 scores (FIXME?)
numSorted = level.numConnectedClients;
if ( numSorted > 32 ) {
Expand Down Expand Up @@ -1331,6 +1484,10 @@ static void CheckExitRules( void ) {
}

if ( level.intermissionQueued ) {
#ifndef NO_HOLYSHIT_MOD
CheckHolyshit();
#endif

#ifdef MISSIONPACK
int time = (g_singlePlayer.integer) ? SP_INTERMISSION_DELAY_TIME : INTERMISSION_DELAY_TIME;
if ( level.time - level.intermissionQueued >= time ) {
Expand Down
12 changes: 12 additions & 0 deletions code/game/g_team.c
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ void AddTeamScore( vec3_t origin, team_t team, int score ) {
return;
}

#ifndef NO_HOLYSHIT_MOD
level.teamImaginaryScores[ team ] += score;
#endif
// See the same check in `AddScore()`.
// This has effect in flag-like game modes such as Harvest
// where it's possible for both teams to capture at the same time.
// And also with `g_canDamageAfterMatchEnd 1` with `GT_TEAM` game type,
// and maybe more.
if ( level.intermissionQueued ) {
return;
}

eventParm = -1;
otherTeam = OtherTeam( team );

Expand Down