diff --git a/code/game/g_combat.c b/code/game/g_combat.c index 6ea2d866..d615ba2c 100644 --- a/code/game/g_combat.c +++ b/code/game/g_combat.c @@ -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 ); } @@ -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; @@ -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. diff --git a/code/game/g_cvar.h b/code/game/g_cvar.h index c9a8ad9b..df88742d 100644 --- a/code/game/g_cvar.h +++ b/code/game/g_cvar.h @@ -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 ) diff --git a/code/game/g_local.h b/code/game/g_local.h index 38397e67..053f9ece 100644 --- a/code/game/g_local.h +++ b/code/game/g_local.h @@ -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; @@ -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 @@ -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; diff --git a/code/game/g_main.c b/code/game/g_main.c index 6b1d1f85..c3b3e9ec 100644 --- a/code/game/g_main.c +++ b/code/game/g_main.c @@ -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 @@ -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 ) { @@ -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 ) { diff --git a/code/game/g_team.c b/code/game/g_team.c index 62362831..d5cc02cb 100644 --- a/code/game/g_team.c +++ b/code/game/g_team.c @@ -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 );