foxygit / GZDoom Log in
commits tags

/src/hu_scores.cpp · 17.89 KB

raw
/*
** hu_scores.cpp
** Routines for drawing the scoreboards.
**
**---------------------------------------------------------------------------
** Copyright 1998-2008 Randy Heit
** Copyright 2007-2008 Christopher Westley
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
**    notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
**    notice, this list of conditions and the following disclaimer in the
**    documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
**    derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/

// HEADER FILES ------------------------------------------------------------

#include "c_console.h"
#include "teaminfo.h"

#include "v_video.h"
#include "doomstat.h"
#include "g_level.h"
#include "d_netinf.h"
#include "v_font.h"
#include "d_player.h"
#include "hu_stuff.h"
#include "gstrings.h"
#include "d_net.h"
#include "c_dispatch.h"
#include "g_levellocals.h"
#include "g_game.h"
#include "sbar.h"
#include "texturemanager.h"
#include "v_draw.h"

// MACROS ------------------------------------------------------------------

// TYPES -------------------------------------------------------------------

// EXTERNAL FUNCTION PROTOTYPES --------------------------------------------

// PUBLIC FUNCTION PROTOTYPES ----------------------------------------------

// PRIVATE FUNCTION PROTOTYPES ---------------------------------------------

static void HU_DoDrawScores (player_t *, player_t *[MAXPLAYERS]);
static void HU_DrawTimeRemaining (int y);
static void HU_DrawPlayer(player_t *, bool, int, int, int, int, int, int, int, int, int);

// EXTERNAL DATA DECLARATIONS ----------------------------------------------

EXTERN_CVAR (Float, timelimit)
EXTERN_CVAR (Bool, ui_classic)

// PUBLIC DATA DEFINITIONS -------------------------------------------------

CVAR (Bool,	sb_cooperative_enable,				true,		CVAR_ARCHIVE)
CVAR (Int,	sb_cooperative_headingcolor,		CR_RED,		CVAR_ARCHIVE)
CVAR (Int,	sb_cooperative_yourplayercolor,		CR_GREEN,	CVAR_ARCHIVE)
CVAR (Int,	sb_cooperative_otherplayercolor,	CR_GREY,	CVAR_ARCHIVE)

CVAR (Bool,	sb_deathmatch_enable,				true,		CVAR_ARCHIVE)
CVAR (Int,	sb_deathmatch_headingcolor,			CR_RED,		CVAR_ARCHIVE)
CVAR (Int,	sb_deathmatch_yourplayercolor,		CR_GREEN,	CVAR_ARCHIVE)
CVAR (Int,	sb_deathmatch_otherplayercolor,		CR_GREY,	CVAR_ARCHIVE)

CVAR (Bool,	sb_teamdeathmatch_enable,			true,		CVAR_ARCHIVE)
CVAR (Int,	sb_teamdeathmatch_headingcolor,		CR_RED,		CVAR_ARCHIVE)

// Splitscreen convenience: briefly force the scoreboard on its own, without
// anyone holding +showscores, so a couch full of players can see standings
// without needing a keyboard. Deathmatch only -- coop has no rivalry to
// check in on. 0 disables either trigger independently.
CVAR (Float, sb_autoshow_interval,					0.f,		CVAR_ARCHIVE)	// minutes; 0 = off
CVAR (Int,	 sb_autoshow_deaths,					0,			CVAR_ARCHIVE)	// deaths since last auto-show; 0 = off
CVAR (Float, sb_autoshow_duration,					4.f,		CVAR_ARCHIVE)	// seconds the auto-show stays up

int SB_AutoActiveUntilTic = 0;

void HU_AutoShowTicker()
{
	if (!deathmatch)
		return;

	static int lastIntervalMapTime = 0;
	static int deathsSinceShow = 0;
	static uint8_t prevState[MAXPLAYERS];

	// level.maptime resets to 0 for exactly one tic at the start of every
	// map -- use that instead of a level-change callback to reset our
	// per-match bookkeeping, since this ticker already runs unconditionally
	// every tic and level transitions are otherwise invisible from here.
	if (primaryLevel->maptime == 0)
	{
		lastIntervalMapTime = 0;
		deathsSinceShow = 0;
		for (int i = 0; i < MAXPLAYERS; i++) prevState[i] = PST_LIVE;
	}

	for (int i = 0; i < MAXPLAYERS; i++)
	{
		if (!playeringame[i]) continue;
		uint8_t st = (uint8_t)players[i].playerstate;
		if (st == PST_DEAD && prevState[i] != PST_DEAD) deathsSinceShow++;
		prevState[i] = st;
	}

	bool trigger = false;
	if (sb_autoshow_interval > 0.f && (primaryLevel->maptime - lastIntervalMapTime) >= (int)(sb_autoshow_interval * 60.f * TICRATE))
	{
		trigger = true;
		lastIntervalMapTime = primaryLevel->maptime;
	}
	if (sb_autoshow_deaths > 0 && deathsSinceShow >= sb_autoshow_deaths)
	{
		trigger = true;
		deathsSinceShow = 0;
	}
	if (trigger)
		SB_AutoActiveUntilTic = gametic + (int)(sb_autoshow_duration * TICRATE);
}

int comparepoints (const void *arg1, const void *arg2)
{
	// Compare first be frags/kills, then by name.
	player_t *p1 = *(player_t **)arg1;
	player_t *p2 = *(player_t **)arg2;
	int diff;

	diff = deathmatch ? p2->fragcount - p1->fragcount : p2->killcount - p1->killcount;
	if (diff == 0)
	{
		diff = stricmp(p1->userinfo.GetName(), p2->userinfo.GetName());
	}
	return diff;
}

int compareteams (const void *arg1, const void *arg2)
{
	// Compare first by teams, then by frags, then by name.
	player_t *p1 = *(player_t **)arg1;
	player_t *p2 = *(player_t **)arg2;
	int diff;

	diff = p1->userinfo.GetTeam() - p2->userinfo.GetTeam();
	if (diff == 0)
	{
		diff = p2->fragcount - p1->fragcount;
		if (diff == 0)
		{
			diff = stricmp (p1->userinfo.GetName(), p2->userinfo.GetName());
		}
	}
	return diff;
}

/*
void HU_SortPlayers
{
	if (teamplay)
	qsort(sortedplayers, MAXPLAYERS, sizeof(player_t *), compareteams);
	else
		qsort(sortedplayers, MAXPLAYERS, sizeof(player_t *), comparepoints);
}
*/

bool SB_ForceActive = false;
static FFont *displayFont;
static int FontScale;


// PRIVATE DATA DEFINITIONS ------------------------------------------------

// CODE --------------------------------------------------------------------

//==========================================================================
//
// HU_DrawScores
//
//==========================================================================

void HU_DrawScores (player_t *player)
{
	displayFont = ui_classic? SmallFont : NewSmallFont;
	FontScale = max(screen->GetHeight() / (ui_classic? 200 : 400), 1);

	if (deathmatch)
	{
		if (teamplay)
		{
			if (!sb_teamdeathmatch_enable)
				return;
		}
		else
		{
			if (!sb_deathmatch_enable)
				return;
		}
	}
	else
	{
		if (!sb_cooperative_enable || !multiplayer)
			return;
	}

	int i, j;
	player_t *sortedplayers[MAXPLAYERS];

	if (player->camera && player->camera->player)
		player = player->camera->player;

	sortedplayers[MAXPLAYERS-1] = player;
	for (i = 0, j = 0; j < MAXPLAYERS - 1; i++, j++)
	{
		if (&players[i] == player)
			i++;
		sortedplayers[j] = &players[i];
	}

	if (teamplay && deathmatch)
		qsort (sortedplayers, MAXPLAYERS, sizeof(player_t *), compareteams);
	else
		qsort (sortedplayers, MAXPLAYERS, sizeof(player_t *), comparepoints);

	HU_DoDrawScores (player, sortedplayers);

}

//==========================================================================
//
// HU_GetPlayerWidths
//
// Returns the widest player name and class icon.
//
//==========================================================================

void HU_GetPlayerWidths(int &maxnamewidth, int &maxscorewidth, int &maxiconheight)
{
	displayFont = ui_classic? SmallFont : NewSmallFont;
	maxnamewidth = displayFont->StringWidth("Name");
	maxscorewidth = 0;
	maxiconheight = 0;

	for (int i = 0; i < MAXPLAYERS; i++)
	{
		if (playeringame[i])
		{
			int width = displayFont->StringWidth(players[i].userinfo.GetName());
			if (width > maxnamewidth)
			{
				maxnamewidth = width;
			}
			auto icon = FSetTextureID(players[i].mo->IntVar(NAME_ScoreIcon));
			if (icon.isValid())
			{
				auto pic = TexMan.GetGameTexture(icon);
				width = int(pic->GetDisplayWidth() - pic->GetDisplayLeftOffset() + 2.5);
				if (width > maxscorewidth)
				{
					maxscorewidth = width;
				}
				// The icon's top offset does not count toward its height, because
				// zdoom.pk3's standard Hexen class icons are designed that way.
				int height = int(pic->GetDisplayHeight() - pic->GetDisplayTopOffset() + 0.5);
				if (height > maxiconheight)
				{
					maxiconheight = height;
				}
			}
		}
	}
}

//==========================================================================
//
// HU_DoDrawScores
//
//==========================================================================

static void HU_DrawFontScaled(double x, double y, int color, const char *text)
{
	DrawText(twod, displayFont, color, x / FontScale, y / FontScale, text, DTA_VirtualWidth, twod->GetWidth() / FontScale, DTA_VirtualHeight, twod->GetHeight() / FontScale, TAG_END);
}

static void HU_DoDrawScores (player_t *player, player_t *sortedplayers[MAXPLAYERS])
{
	int color;
	int height, lineheight;
	unsigned int i;
	int maxnamewidth, maxscorewidth, maxiconheight;
	int numTeams = 0;
	int x, y, ypadding, bottom;
	int col2, col3, col4, col5;
	
	if (deathmatch)
	{
		if (teamplay)
			color = sb_teamdeathmatch_headingcolor;
		else
			color = sb_deathmatch_headingcolor;
	}
	else
	{
		color = sb_cooperative_headingcolor;
	}

	HU_GetPlayerWidths(maxnamewidth, maxscorewidth, maxiconheight);
	height = displayFont->GetHeight() * FontScale;
	lineheight = max(height, maxiconheight * CleanYfac);
	ypadding = (lineheight - height + 1) / 2;

	bottom = StatusBar->GetTopOfStatusbar();
	// Pin the list near the top instead of centering for a hypothetical full
	// MAXPLAYERS roster: with up to 25 slots that formula always bottoms out
	// at the clamp anyway, and centering on capacity rather than the actual
	// player count never made much sense.
	y = 48*CleanYfac;

	HU_DrawTimeRemaining (bottom - height);

	if (teamplay && deathmatch)
	{
		y -= (BigFont->GetHeight() + 8) * CleanYfac;

		for (i = 0; i < Teams.Size (); i++)
		{
			Teams[i].m_iPlayerCount = 0;
			Teams[i].m_iScore = 0;
		}

		for (i = 0; i < MAXPLAYERS; ++i)
		{
			if (playeringame[sortedplayers[i]-players] && FTeam::IsValid (sortedplayers[i]->userinfo.GetTeam()))
			{
				if (Teams[sortedplayers[i]->userinfo.GetTeam()].m_iPlayerCount++ == 0)
				{
					numTeams++;
				}

				Teams[sortedplayers[i]->userinfo.GetTeam()].m_iScore += sortedplayers[i]->fragcount;
			}
		}

		int scorexwidth = twod->GetWidth() / max(8, numTeams);
		int numscores = 0;
		int scorex;

		for (i = 0; i < Teams.Size(); ++i)
		{
			if (Teams[i].m_iPlayerCount)
			{
				numscores++;
			}
		}

		scorex = (twod->GetWidth() - scorexwidth * (numscores - 1)) / 2;

		for (i = 0; i < Teams.Size(); ++i)
		{
			if (Teams[i].m_iPlayerCount)
			{
				char score[80];
				mysnprintf (score, countof(score), "%d", Teams[i].m_iScore);

				DrawText(twod, BigFont, Teams[i].GetTextColor(),
					scorex - BigFont->StringWidth(score)*CleanXfac/2, y, score,
					DTA_CleanNoMove, true, TAG_DONE);

				scorex += scorexwidth;
			}
		}

		y += (BigFont->GetHeight() + 8) * CleanYfac;
	}

	const char *text_color = GStrings.GetString("SCORE_COLOR"),
		*text_frags = GStrings.GetString(deathmatch ? "SCORE_FRAGS" : "SCORE_KILLS"),
		*text_name = GStrings.GetString("SCORE_NAME"),
		*text_delay = GStrings.GetString("SCORE_DELAY");

	col2 = (displayFont->StringWidth(text_color) + (ui_classic? 8 : 16)) * FontScale;
	col3 = col2 + (displayFont->StringWidth(text_frags) + (ui_classic? 8 : 16)) * FontScale;
	col4 = col3 + maxscorewidth * FontScale;
	col5 = col4 + (maxnamewidth + (ui_classic? 8 : 16)) * FontScale;
	int colgap = (displayFont->StringWidth(text_delay) + (ui_classic? 24 : 32)) * FontScale;
	int colwidth = col5 + colgap;

	// Compact the sorted list down to only the players actually in the game,
	// then lay them out in as many side-by-side columns as are needed to fit
	// everyone in the vertical space below the heading -- a plain single
	// column stops making sense once the roster no longer fits on screen.
	player_t *inGame[MAXPLAYERS];
	unsigned int numplayers = 0;
	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (playeringame[sortedplayers[i] - players])
		{
			inGame[numplayers++] = sortedplayers[i];
		}
	}

	bottom -= height;
	int rowsavail = max(1, int((bottom - (y + height + 6 * CleanYfac)) / (lineheight + CleanYfac)) + 1);
	int numcols = numplayers > 0 ? max(1, int((numplayers + rowsavail - 1) / rowsavail)) : 1;
	int rowspercol = numplayers > 0 ? int((numplayers + numcols - 1) / numcols) : 1;

	// Don't let columns run off the sides of the screen -- shrink the column
	// count (accepting some vertical overflow) rather than draw off-screen.
	while (numcols > 1 && colwidth * numcols > twod->GetWidth())
	{
		numcols--;
		rowspercol = int((numplayers + numcols - 1) / numcols);
	}

	int xstart = (twod->GetWidth() - colwidth * numcols) / 2;

	for (int col = 0; col < numcols; col++)
	{
		x = xstart + col * colwidth;
		HU_DrawFontScaled(x + col2, y, color, text_frags);
		HU_DrawFontScaled(x + col4, y, color, text_name);
		HU_DrawFontScaled(x + col5, y, color, text_delay);
	}

	int headery = y;
	for (i = 0; i < numplayers; i++)
	{
		int col = i / rowspercol;
		int row = i % rowspercol;
		int rowy = headery + height + 6 * CleanYfac + row * (lineheight + CleanYfac);
		x = xstart + col * colwidth;

		HU_DrawPlayer(inGame[i], player == inGame[i], x, col2, col3, col4, col5, maxnamewidth, rowy, ypadding, lineheight);
	}
}

//==========================================================================
//
// HU_DrawTimeRemaining
//
//==========================================================================

static void HU_DrawTimeRemaining (int y)
{
	if (deathmatch && timelimit && gamestate == GS_LEVEL)
	{
		char str[80];
		int timeleft = (int)(timelimit * TICRATE * 60) - primaryLevel->maptime;
		int hours, minutes, seconds;

		if (timeleft < 0)
			timeleft = 0;

		hours = timeleft / (TICRATE * 3600);
		timeleft -= hours * TICRATE * 3600;
		minutes = timeleft / (TICRATE * 60);
		timeleft -= minutes * TICRATE * 60;
		seconds = timeleft / TICRATE;

		if (hours)
			mysnprintf (str, countof(str), "Level ends in %d:%02d:%02d", hours, minutes, seconds);
		else
			mysnprintf (str, countof(str), "Level ends in %d:%02d", minutes, seconds);

		HU_DrawFontScaled(twod->GetWidth() / 2 - displayFont->StringWidth(str) / 2 * FontScale, y, CR_GRAY, str);
	}
}

//==========================================================================
//
// HU_DrawPlayer
//
//==========================================================================

static void HU_DrawPlayer (player_t *player, bool highlight, int col1, int col2, int col3, int col4, int col5, int maxnamewidth, int y, int ypadding, int height)
{
	int color;
	char str[80];

	if (highlight)
	{
		// The teamplay mode uses colors to show teams, so we need some
		// other way to do highlighting. And it may as well be used for
		// all modes for the sake of consistancy.
		Dim(twod, MAKERGB(200,245,255), 0.125f, col1 - 12*FontScale, y - 1, col5 + (maxnamewidth + 24)*FontScale, height + 2);
	}

	col2 += col1;
	col3 += col1;
	col4 += col1;
	col5 += col1;

	color = HU_GetRowColor(player, highlight);
	HU_DrawColorBar(col1, y, height, (int)(player - players));
	mysnprintf (str, countof(str), "%d", deathmatch ? player->fragcount : player->killcount);

	HU_DrawFontScaled(col2, y + ypadding, color, player->playerstate == PST_DEAD && !deathmatch ? "DEAD" : str);

	auto icon = FSetTextureID(player->mo->IntVar(NAME_ScoreIcon));
	if (icon.isValid())
	{
		DrawTexture(twod, icon, false, col3, y,
			DTA_CleanNoMove, true,
			TAG_DONE);
	}

	HU_DrawFontScaled(col4, y + ypadding, color, player->userinfo.GetName());

	int avgdelay = 0;
	for (int i = 0; i < BACKUPTICS; i++)
	{
		avgdelay += netdelay[nodeforplayer[(int)(player - players)]][i];
	}
	avgdelay /= BACKUPTICS;

	mysnprintf(str, countof(str), "%d", (avgdelay * ticdup) * (1000 / TICRATE));

	HU_DrawFontScaled(col5, y + ypadding, color, str);

	const int team = player->userinfo.GetTeam();
	if (team != TEAM_NONE && teamplay && Teams[team].GetLogo().IsNotEmpty ())
	{
		auto pic = TexMan.GetGameTextureByName(Teams[player->userinfo.GetTeam()].GetLogo().GetChars ());
		DrawTexture(twod, pic, col1 - (pic->GetDisplayWidth() + 2) * CleanXfac, y,
			DTA_CleanNoMove, true, TAG_DONE);
	}
}

//==========================================================================
//
// HU_DrawColorBar
//
//==========================================================================

void HU_DrawColorBar(int x, int y, int height, int playernum)
{
	float h, s, v, r, g, b;

	D_GetPlayerColor (playernum, &h, &s, &v, NULL);
	HSVtoRGB (&r, &g, &b, h, s, v);

	ClearRect(twod, x, y, x + 24*FontScale, y + height, -1,
		MAKEARGB(255,clamp(int(r*255.f),0,255),
					 clamp(int(g*255.f),0,255),
					 clamp(int(b*255.f),0,255)));
}

//==========================================================================
//
// HU_GetRowColor
//
//==========================================================================

int HU_GetRowColor(player_t *player, bool highlight)
{
	if (teamplay && deathmatch)
	{
		if (FTeam::IsValid (player->userinfo.GetTeam()))
			return Teams[player->userinfo.GetTeam()].GetTextColor();
		else
			return CR_GREY;
	}
	else
	{
		if (!highlight)
		{
			if (demoplayback && player == &players[consoleplayer])
			{
				return CR_GOLD;
			}
			else
			{
				return deathmatch ? sb_deathmatch_otherplayercolor : sb_cooperative_otherplayercolor;
			}
		}
		else
		{
			return deathmatch ? sb_deathmatch_yourplayercolor : sb_cooperative_yourplayercolor;
		}
	}
}

CCMD (togglescoreboard)
{
	SB_ForceActive = !SB_ForceActive;
}