ddnet/src/game/client/components/hud.cpp
bors[bot] 74789f86dc
Merge #6818
6818: Fix HUD PlayerState weapons rendering r=def- a=Kaffeine

<!-- What is the motivation for the changes of this pull request? -->
The proper offset for the first available weapon is provided only for Hammer but sometimes (in some mods) the character has no hammer and the icons got a wrong left margin.

### Current code

83a2ad0e24/src/game/client/components/hud.cpp (L882-L895)

Notice the `x -= 3;` in the hammer rendering branch. It fixes the `x` offset for this and further weapons but it works only if the character has a hammer (other weapons has no such line).
`-3` is specific for the hammer, we need different "first item" offsets for other weapons.

## Before

![image](https://github.com/ddnet/ddnet/assets/374839/419f97be-d1e3-4eff-94a6-387e89d6ef64)
(the hammer offset is _correct_)

![image](https://github.com/ddnet/ddnet/assets/374839/400ab01f-02a2-4d2a-bd6f-97806e12cbef)
but if the character has no hammer then the things go _wrong_.

![image](https://github.com/ddnet/ddnet/assets/374839/262e45a5-cee8-45b9-bd8b-a982761a5dc5)
Shotgun/Grenade Launcher/Laser has just a small offset but the _wrong_ placement of katana indicator if very noticeable.

<!-- Note that builds and other checks will be run for your change. Don't feel intimidated by failures in some of the checks. If you can't resolve them yourself, experienced devs can also resolve them before merging your pull request. -->

## After
![image](https://github.com/ddnet/ddnet/assets/374839/af24c361-228e-4461-b7ea-0b4531ffedf0)
(no difference in the hammer icon rendering)

![image](https://github.com/ddnet/ddnet/assets/374839/04b30b2e-5560-431d-a949-b37b63e27b9e)
(fixed gun offset)

![image](https://github.com/ddnet/ddnet/assets/374839/5abb5be3-212c-48b1-9c78-169441d6442e)
(fixed shotgun offset)

![image](https://github.com/ddnet/ddnet/assets/374839/918ef960-162e-4942-916f-b8eb9802c2e4)
(fixed grenade launcher offset)

![image](https://github.com/ddnet/ddnet/assets/374839/7a8e1257-590b-4589-877a-51f90298d6f7)
(fixed laser offset)

![image](https://github.com/ddnet/ddnet/assets/374839/157258b6-f7de-4411-8b63-140083b883c3)
(fixed katana offset)

## Checklist

- [x] Tested the change ingame
- [x] Provided screenshots if it is a visual change
- [ ] Tested in combination with possibly related configuration options
- [ ] Written a unit test (especially base/) or added coverage to integration test
- [ ] Considered possible null pointers and out of bounds array indexing
- [ ] Changed no physics that affect existing maps
- [ ] Tested the change with [ASan+UBSan or valgrind's memcheck](https://github.com/ddnet/ddnet/#using-addresssanitizer--undefinedbehavioursanitizer-or-valgrinds-memcheck) (optional)


Co-authored-by: Alexander Akulich <akulichalexander@gmail.com>
2023-07-07 18:45:19 +00:00

1672 lines
64 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
#include <engine/graphics.h>
#include <engine/shared/config.h>
#include <engine/textrender.h>
#include <game/client/animstate.h>
#include <game/client/components/scoreboard.h>
#include <game/client/gameclient.h>
#include <game/client/render.h>
#include <game/generated/client_data.h>
#include <game/generated/protocol.h>
#include <game/layers.h>
#include <game/localization.h>
#include <cmath>
#include "binds.h"
#include "camera.h"
#include "controls.h"
#include "hud.h"
#include "voting.h"
CHud::CHud()
{
// won't work if zero
m_FrameTimeAvg = 0.0f;
m_FPSTextContainerIndex.Reset();
m_DDRaceEffectsTextContainerIndex.Reset();
}
void CHud::ResetHudContainers()
{
for(auto &ScoreInfo : m_aScoreInfo)
{
TextRender()->DeleteTextContainer(ScoreInfo.m_OptionalNameTextContainerIndex);
TextRender()->DeleteTextContainer(ScoreInfo.m_TextRankContainerIndex);
TextRender()->DeleteTextContainer(ScoreInfo.m_TextScoreContainerIndex);
Graphics()->DeleteQuadContainer(ScoreInfo.m_RoundRectQuadContainerIndex);
ScoreInfo.Reset();
}
TextRender()->DeleteTextContainer(m_FPSTextContainerIndex);
TextRender()->DeleteTextContainer(m_DDRaceEffectsTextContainerIndex);
}
void CHud::OnWindowResize()
{
ResetHudContainers();
}
void CHud::OnReset()
{
m_TimeCpDiff = 0.0f;
m_DDRaceTime = 0;
m_FinishTimeLastReceivedTick = 0;
m_TimeCpLastReceivedTick = 0;
m_ShowFinishTime = false;
m_ServerRecord = -1.0f;
m_aPlayerRecord[0] = -1.0f;
m_aPlayerRecord[1] = -1.0f;
ResetHudContainers();
}
void CHud::OnInit()
{
OnReset();
m_HudQuadContainerIndex = Graphics()->CreateQuadContainer(false);
Graphics()->QuadsSetSubset(0, 0, 1, 1);
PrepareAmmoHealthAndArmorQuads();
// all cursors for the different weapons
for(int i = 0; i < NUM_WEAPONS; ++i)
{
float ScaleX, ScaleY;
RenderTools()->GetSpriteScale(g_pData->m_Weapons.m_aId[i].m_pSpriteCursor, ScaleX, ScaleY);
m_aCursorOffset[i] = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 64.f * ScaleX, 64.f * ScaleY);
}
// the flags
m_FlagOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 8.f, 16.f);
PreparePlayerStateQuads();
Graphics()->QuadContainerUpload(m_HudQuadContainerIndex);
}
void CHud::RenderGameTimer()
{
float Half = m_Width / 2.0f;
if(!(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_SUDDENDEATH))
{
char aBuf[32];
int Time = 0;
if(m_pClient->m_Snap.m_pGameInfoObj->m_TimeLimit && (m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer <= 0))
{
Time = m_pClient->m_Snap.m_pGameInfoObj->m_TimeLimit * 60 - ((Client()->GameTick(g_Config.m_ClDummy) - m_pClient->m_Snap.m_pGameInfoObj->m_RoundStartTick) / Client()->GameTickSpeed());
if(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER)
Time = 0;
}
else if(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME)
{
// The Warmup timer is negative in this case to make sure that incompatible clients will not see a warmup timer
Time = (Client()->GameTick(g_Config.m_ClDummy) + m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer) / Client()->GameTickSpeed();
}
else
Time = (Client()->GameTick(g_Config.m_ClDummy) - m_pClient->m_Snap.m_pGameInfoObj->m_RoundStartTick) / Client()->GameTickSpeed();
str_time((int64_t)Time * 100, TIME_DAYS, aBuf, sizeof(aBuf));
float FontSize = 10.0f;
static float s_TextWidthM = TextRender()->TextWidth(FontSize, "00:00", -1, -1.0f);
static float s_TextWidthH = TextRender()->TextWidth(FontSize, "00:00:00", -1, -1.0f);
static float s_TextWidth0D = TextRender()->TextWidth(FontSize, "0d 00:00:00", -1, -1.0f);
static float s_TextWidth00D = TextRender()->TextWidth(FontSize, "00d 00:00:00", -1, -1.0f);
static float s_TextWidth000D = TextRender()->TextWidth(FontSize, "000d 00:00:00", -1, -1.0f);
float w = Time >= 3600 * 24 * 100 ? s_TextWidth000D : Time >= 3600 * 24 * 10 ? s_TextWidth00D : Time >= 3600 * 24 ? s_TextWidth0D : Time >= 3600 ? s_TextWidthH : s_TextWidthM;
// last 60 sec red, last 10 sec blink
if(m_pClient->m_Snap.m_pGameInfoObj->m_TimeLimit && Time <= 60 && (m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer <= 0))
{
float Alpha = Time <= 10 && (2 * time() / time_freq()) % 2 ? 0.5f : 1.0f;
TextRender()->TextColor(1.0f, 0.25f, 0.25f, Alpha);
}
TextRender()->Text(Half - w / 2, 2, FontSize, aBuf, -1.0f);
TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f);
}
}
void CHud::RenderPauseNotification()
{
if(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED &&
!(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
{
const char *pText = Localize("Game paused");
float FontSize = 20.0f;
float w = TextRender()->TextWidth(FontSize, pText, -1, -1.0f);
TextRender()->Text(150.0f * Graphics()->ScreenAspect() + -w / 2.0f, 50.0f, FontSize, pText, -1.0f);
}
}
void CHud::RenderSuddenDeath()
{
if(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_SUDDENDEATH)
{
float Half = m_Width / 2.0f;
const char *pText = Localize("Sudden Death");
float FontSize = 12.0f;
float w = TextRender()->TextWidth(FontSize, pText, -1, -1.0f);
TextRender()->Text(Half - w / 2, 2, FontSize, pText, -1.0f);
}
}
void CHud::RenderScoreHud()
{
// render small score hud
if(!(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
{
int GameFlags = m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags;
float StartY = 229.0f; // the height of this display is 56, so EndY is 285
const float ScoreSingleBoxHeight = 18.0f;
bool ForceScoreInfoInit = !m_aScoreInfo[0].m_Initialized || !m_aScoreInfo[1].m_Initialized;
m_aScoreInfo[0].m_Initialized = m_aScoreInfo[1].m_Initialized = true;
if(GameFlags & GAMEFLAG_TEAMS && m_pClient->m_Snap.m_pGameDataObj)
{
char aScoreTeam[2][16];
str_format(aScoreTeam[TEAM_RED], sizeof(aScoreTeam), "%d", m_pClient->m_Snap.m_pGameDataObj->m_TeamscoreRed);
str_format(aScoreTeam[TEAM_BLUE], sizeof(aScoreTeam), "%d", m_pClient->m_Snap.m_pGameDataObj->m_TeamscoreBlue);
bool aRecreateTeamScore[2] = {str_comp(aScoreTeam[0], m_aScoreInfo[0].m_aScoreText) != 0, str_comp(aScoreTeam[1], m_aScoreInfo[1].m_aScoreText) != 0};
const int aFlagCarrier[2] = {
m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierRed,
m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierBlue};
bool RecreateRect = ForceScoreInfoInit;
for(int t = 0; t < 2; t++)
{
if(aRecreateTeamScore[t])
{
m_aScoreInfo[t].m_ScoreTextWidth = TextRender()->TextWidth(14.0f, aScoreTeam[t == 0 ? TEAM_RED : TEAM_BLUE], -1, -1.0f);
mem_copy(m_aScoreInfo[t].m_aScoreText, aScoreTeam[t == 0 ? TEAM_RED : TEAM_BLUE], sizeof(m_aScoreInfo[t].m_aScoreText));
RecreateRect = true;
}
}
static float s_TextWidth100 = TextRender()->TextWidth(14.0f, "100", -1, -1.0f);
float ScoreWidthMax = maximum(maximum(m_aScoreInfo[0].m_ScoreTextWidth, m_aScoreInfo[1].m_ScoreTextWidth), s_TextWidth100);
float Split = 3.0f;
float ImageSize = (GameFlags & GAMEFLAG_FLAGS) ? 16.0f : Split;
for(int t = 0; t < 2; t++)
{
// draw box
if(RecreateRect)
{
Graphics()->DeleteQuadContainer(m_aScoreInfo[t].m_RoundRectQuadContainerIndex);
if(t == 0)
Graphics()->SetColor(1.0f, 0.0f, 0.0f, 0.25f);
else
Graphics()->SetColor(0.0f, 0.0f, 1.0f, 0.25f);
m_aScoreInfo[t].m_RoundRectQuadContainerIndex = Graphics()->CreateRectQuadContainer(m_Width - ScoreWidthMax - ImageSize - 2 * Split, StartY + t * 20, ScoreWidthMax + ImageSize + 2 * Split, ScoreSingleBoxHeight, 5.0f, IGraphics::CORNER_L);
}
Graphics()->TextureClear();
Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
if(m_aScoreInfo[t].m_RoundRectQuadContainerIndex != -1)
Graphics()->RenderQuadContainer(m_aScoreInfo[t].m_RoundRectQuadContainerIndex, -1);
// draw score
if(aRecreateTeamScore[t])
{
CTextCursor Cursor;
TextRender()->SetCursor(&Cursor, m_Width - ScoreWidthMax + (ScoreWidthMax - m_aScoreInfo[t].m_ScoreTextWidth) / 2 - Split, StartY + t * 20 + (18.f - 14.f) / 2.f, 14.0f, TEXTFLAG_RENDER);
Cursor.m_LineWidth = -1;
TextRender()->RecreateTextContainer(m_aScoreInfo[t].m_TextScoreContainerIndex, &Cursor, aScoreTeam[t]);
}
if(m_aScoreInfo[t].m_TextScoreContainerIndex.Valid())
{
ColorRGBA TColor(1.f, 1.f, 1.f, 1.f);
ColorRGBA TOutlineColor(0.f, 0.f, 0.f, 0.3f);
TextRender()->RenderTextContainer(m_aScoreInfo[t].m_TextScoreContainerIndex, TColor, TOutlineColor);
}
if(GameFlags & GAMEFLAG_FLAGS)
{
int BlinkTimer = (m_pClient->m_aFlagDropTick[t] != 0 &&
(Client()->GameTick(g_Config.m_ClDummy) - m_pClient->m_aFlagDropTick[t]) / Client()->GameTickSpeed() >= 25) ?
10 :
20;
if(aFlagCarrier[t] == FLAG_ATSTAND || (aFlagCarrier[t] == FLAG_TAKEN && ((Client()->GameTick(g_Config.m_ClDummy) / BlinkTimer) & 1)))
{
// draw flag
Graphics()->TextureSet(t == 0 ? m_pClient->m_GameSkin.m_SpriteFlagRed : m_pClient->m_GameSkin.m_SpriteFlagBlue);
Graphics()->SetColor(1.f, 1.f, 1.f, 1.f);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_FlagOffset, m_Width - ScoreWidthMax - ImageSize, StartY + 1.0f + t * 20);
}
else if(aFlagCarrier[t] >= 0)
{
// draw name of the flag holder
int ID = aFlagCarrier[t] % MAX_CLIENTS;
const char *pName = m_pClient->m_aClients[ID].m_aName;
if(str_comp(pName, m_aScoreInfo[t].m_aPlayerNameText) != 0 || RecreateRect)
{
mem_copy(m_aScoreInfo[t].m_aPlayerNameText, pName, sizeof(m_aScoreInfo[t].m_aPlayerNameText));
float w = TextRender()->TextWidth(8.0f, pName, -1, -1.0f);
CTextCursor Cursor;
TextRender()->SetCursor(&Cursor, minimum(m_Width - w - 1.0f, m_Width - ScoreWidthMax - ImageSize - 2 * Split), StartY + (t + 1) * 20.0f - 2.0f, 8.0f, TEXTFLAG_RENDER);
Cursor.m_LineWidth = -1;
TextRender()->RecreateTextContainer(m_aScoreInfo[t].m_OptionalNameTextContainerIndex, &Cursor, pName);
}
if(m_aScoreInfo[t].m_OptionalNameTextContainerIndex.Valid())
{
ColorRGBA TColor(1.f, 1.f, 1.f, 1.f);
ColorRGBA TOutlineColor(0.f, 0.f, 0.f, 0.3f);
TextRender()->RenderTextContainer(m_aScoreInfo[t].m_OptionalNameTextContainerIndex, TColor, TOutlineColor);
}
// draw tee of the flag holder
CTeeRenderInfo TeeInfo = m_pClient->m_aClients[ID].m_RenderInfo;
TeeInfo.m_Size = ScoreSingleBoxHeight;
const CAnimState *pIdleState = CAnimState::GetIdle();
vec2 OffsetToMid;
RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid);
vec2 TeeRenderPos(m_Width - ScoreWidthMax - TeeInfo.m_Size / 2 - Split, StartY + (t * 20) + ScoreSingleBoxHeight / 2.0f + OffsetToMid.y);
RenderTools()->RenderTee(pIdleState, &TeeInfo, EMOTE_NORMAL, vec2(1.0f, 0.0f), TeeRenderPos);
}
}
StartY += 8.0f;
}
}
else
{
int Local = -1;
int aPos[2] = {1, 2};
const CNetObj_PlayerInfo *apPlayerInfo[2] = {0, 0};
int i = 0;
for(int t = 0; t < 2 && i < MAX_CLIENTS && m_pClient->m_Snap.m_apInfoByScore[i]; ++i)
{
if(m_pClient->m_Snap.m_apInfoByScore[i]->m_Team != TEAM_SPECTATORS)
{
apPlayerInfo[t] = m_pClient->m_Snap.m_apInfoByScore[i];
if(apPlayerInfo[t]->m_ClientID == m_pClient->m_Snap.m_LocalClientID)
Local = t;
++t;
}
}
// search local player info if not a spectator, nor within top2 scores
if(Local == -1 && m_pClient->m_Snap.m_pLocalInfo && m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_SPECTATORS)
{
for(; i < MAX_CLIENTS && m_pClient->m_Snap.m_apInfoByScore[i]; ++i)
{
if(m_pClient->m_Snap.m_apInfoByScore[i]->m_Team != TEAM_SPECTATORS)
++aPos[1];
if(m_pClient->m_Snap.m_apInfoByScore[i]->m_ClientID == m_pClient->m_Snap.m_LocalClientID)
{
apPlayerInfo[1] = m_pClient->m_Snap.m_apInfoByScore[i];
Local = 1;
break;
}
}
}
char aScore[2][16];
for(int t = 0; t < 2; ++t)
{
if(apPlayerInfo[t])
{
if(m_pClient->m_GameInfo.m_TimeScore && g_Config.m_ClDDRaceScoreBoard)
{
if(apPlayerInfo[t]->m_Score != -9999)
str_time((int64_t)absolute(apPlayerInfo[t]->m_Score) * 100, TIME_HOURS, aScore[t], sizeof(aScore[t]));
else
aScore[t][0] = 0;
}
else
str_format(aScore[t], sizeof(aScore) / 2, "%d", apPlayerInfo[t]->m_Score);
}
else
aScore[t][0] = 0;
}
static int LocalClientID = -1;
bool RecreateScores = str_comp(aScore[0], m_aScoreInfo[0].m_aScoreText) != 0 || str_comp(aScore[1], m_aScoreInfo[1].m_aScoreText) != 0 || LocalClientID != m_pClient->m_Snap.m_LocalClientID;
LocalClientID = m_pClient->m_Snap.m_LocalClientID;
bool RecreateRect = ForceScoreInfoInit;
for(int t = 0; t < 2; t++)
{
if(RecreateScores)
{
m_aScoreInfo[t].m_ScoreTextWidth = TextRender()->TextWidth(14.0f, aScore[t], -1, -1.0f);
mem_copy(m_aScoreInfo[t].m_aScoreText, aScore[t], sizeof(m_aScoreInfo[t].m_aScoreText));
RecreateRect = true;
}
if(apPlayerInfo[t])
{
int ID = apPlayerInfo[t]->m_ClientID;
if(ID >= 0 && ID < MAX_CLIENTS)
{
const char *pName = m_pClient->m_aClients[ID].m_aName;
if(str_comp(pName, m_aScoreInfo[t].m_aPlayerNameText) != 0)
RecreateRect = true;
}
}
else
{
if(m_aScoreInfo[t].m_aPlayerNameText[0] != 0)
RecreateRect = true;
}
char aBuf[16];
str_format(aBuf, sizeof(aBuf), "%d.", aPos[t]);
if(str_comp(aBuf, m_aScoreInfo[t].m_aRankText) != 0)
RecreateRect = true;
}
static float s_TextWidth10 = TextRender()->TextWidth(14.0f, "10", -1, -1.0f);
float ScoreWidthMax = maximum(maximum(m_aScoreInfo[0].m_ScoreTextWidth, m_aScoreInfo[1].m_ScoreTextWidth), s_TextWidth10);
float Split = 3.0f, ImageSize = 16.0f, PosSize = 16.0f;
for(int t = 0; t < 2; t++)
{
// draw box
if(RecreateRect)
{
Graphics()->DeleteQuadContainer(m_aScoreInfo[t].m_RoundRectQuadContainerIndex);
if(t == Local)
Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.25f);
else
Graphics()->SetColor(0.0f, 0.0f, 0.0f, 0.25f);
m_aScoreInfo[t].m_RoundRectQuadContainerIndex = Graphics()->CreateRectQuadContainer(m_Width - ScoreWidthMax - ImageSize - 2 * Split - PosSize, StartY + t * 20, ScoreWidthMax + ImageSize + 2 * Split + PosSize, ScoreSingleBoxHeight, 5.0f, IGraphics::CORNER_L);
}
Graphics()->TextureClear();
Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
if(m_aScoreInfo[t].m_RoundRectQuadContainerIndex != -1)
Graphics()->RenderQuadContainer(m_aScoreInfo[t].m_RoundRectQuadContainerIndex, -1);
if(RecreateScores)
{
CTextCursor Cursor;
TextRender()->SetCursor(&Cursor, m_Width - ScoreWidthMax + (ScoreWidthMax - m_aScoreInfo[t].m_ScoreTextWidth) - Split, StartY + t * 20 + (18.f - 14.f) / 2.f, 14.0f, TEXTFLAG_RENDER);
Cursor.m_LineWidth = -1;
TextRender()->RecreateTextContainer(m_aScoreInfo[t].m_TextScoreContainerIndex, &Cursor, aScore[t]);
}
// draw score
if(m_aScoreInfo[t].m_TextScoreContainerIndex.Valid())
{
ColorRGBA TColor(1.f, 1.f, 1.f, 1.f);
ColorRGBA TOutlineColor(0.f, 0.f, 0.f, 0.3f);
TextRender()->RenderTextContainer(m_aScoreInfo[t].m_TextScoreContainerIndex, TColor, TOutlineColor);
}
if(apPlayerInfo[t])
{
// draw name
int ID = apPlayerInfo[t]->m_ClientID;
if(ID >= 0 && ID < MAX_CLIENTS)
{
const char *pName = m_pClient->m_aClients[ID].m_aName;
if(RecreateRect)
{
mem_copy(m_aScoreInfo[t].m_aPlayerNameText, pName, sizeof(m_aScoreInfo[t].m_aPlayerNameText));
CTextCursor Cursor;
float w = TextRender()->TextWidth(8.0f, pName, -1, -1.0f);
TextRender()->SetCursor(&Cursor, minimum(m_Width - w - 1.0f, m_Width - ScoreWidthMax - ImageSize - 2 * Split - PosSize), StartY + (t + 1) * 20.0f - 2.0f, 8.0f, TEXTFLAG_RENDER);
Cursor.m_LineWidth = -1;
TextRender()->RecreateTextContainer(m_aScoreInfo[t].m_OptionalNameTextContainerIndex, &Cursor, pName);
}
if(m_aScoreInfo[t].m_OptionalNameTextContainerIndex.Valid())
{
ColorRGBA TColor(1.f, 1.f, 1.f, 1.f);
ColorRGBA TOutlineColor(0.f, 0.f, 0.f, 0.3f);
TextRender()->RenderTextContainer(m_aScoreInfo[t].m_OptionalNameTextContainerIndex, TColor, TOutlineColor);
}
// draw tee
CTeeRenderInfo TeeInfo = m_pClient->m_aClients[ID].m_RenderInfo;
TeeInfo.m_Size = ScoreSingleBoxHeight;
const CAnimState *pIdleState = CAnimState::GetIdle();
vec2 OffsetToMid;
RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid);
vec2 TeeRenderPos(m_Width - ScoreWidthMax - TeeInfo.m_Size / 2 - Split, StartY + (t * 20) + ScoreSingleBoxHeight / 2.0f + OffsetToMid.y);
RenderTools()->RenderTee(pIdleState, &TeeInfo, EMOTE_NORMAL, vec2(1.0f, 0.0f), TeeRenderPos);
}
}
else
{
m_aScoreInfo[t].m_aPlayerNameText[0] = 0;
}
// draw position
char aBuf[16];
str_format(aBuf, sizeof(aBuf), "%d.", aPos[t]);
if(RecreateRect)
{
mem_copy(m_aScoreInfo[t].m_aRankText, aBuf, sizeof(m_aScoreInfo[t].m_aRankText));
CTextCursor Cursor;
TextRender()->SetCursor(&Cursor, m_Width - ScoreWidthMax - ImageSize - Split - PosSize, StartY + t * 20 + (18.f - 10.f) / 2.f, 10.0f, TEXTFLAG_RENDER);
Cursor.m_LineWidth = -1;
TextRender()->RecreateTextContainer(m_aScoreInfo[t].m_TextRankContainerIndex, &Cursor, aBuf);
}
if(m_aScoreInfo[t].m_TextRankContainerIndex.Valid())
{
ColorRGBA TColor(1.f, 1.f, 1.f, 1.f);
ColorRGBA TOutlineColor(0.f, 0.f, 0.f, 0.3f);
TextRender()->RenderTextContainer(m_aScoreInfo[t].m_TextRankContainerIndex, TColor, TOutlineColor);
}
StartY += 8.0f;
}
}
}
}
void CHud::RenderWarmupTimer()
{
// render warmup timer
if(m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer > 0 && !(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME))
{
char aBuf[256];
float FontSize = 20.0f;
float w = TextRender()->TextWidth(FontSize, Localize("Warmup"), -1, -1.0f);
TextRender()->Text(150 * Graphics()->ScreenAspect() + -w / 2, 50, FontSize, Localize("Warmup"), -1.0f);
int Seconds = m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer / SERVER_TICK_SPEED;
if(Seconds < 5)
str_format(aBuf, sizeof(aBuf), "%d.%d", Seconds, (m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer * 10 / SERVER_TICK_SPEED) % 10);
else
str_format(aBuf, sizeof(aBuf), "%d", Seconds);
w = TextRender()->TextWidth(FontSize, aBuf, -1, -1.0f);
TextRender()->Text(150 * Graphics()->ScreenAspect() + -w / 2, 75, FontSize, aBuf, -1.0f);
}
}
void CHud::RenderTextInfo()
{
if(g_Config.m_ClShowfps)
{
// calculate avg. fps
m_FrameTimeAvg = m_FrameTimeAvg * 0.9f + Client()->RenderFrameTime() * 0.1f;
char aBuf[64];
int FrameTime = (int)(1.0f / m_FrameTimeAvg + 0.5f);
str_format(aBuf, sizeof(aBuf), "%d", FrameTime);
static float s_TextWidth0 = TextRender()->TextWidth(12.f, "0", -1, -1.0f);
static float s_TextWidth00 = TextRender()->TextWidth(12.f, "00", -1, -1.0f);
static float s_TextWidth000 = TextRender()->TextWidth(12.f, "000", -1, -1.0f);
static float s_TextWidth0000 = TextRender()->TextWidth(12.f, "0000", -1, -1.0f);
static float s_TextWidth00000 = TextRender()->TextWidth(12.f, "00000", -1, -1.0f);
static const float s_aTextWidth[5] = {s_TextWidth0, s_TextWidth00, s_TextWidth000, s_TextWidth0000, s_TextWidth00000};
int DigitIndex = GetDigitsIndex(FrameTime, 4);
CTextCursor Cursor;
TextRender()->SetCursor(&Cursor, m_Width - 10 - s_aTextWidth[DigitIndex], 5, 12, TEXTFLAG_RENDER);
Cursor.m_LineWidth = -1;
auto OldFlags = TextRender()->GetRenderFlags();
TextRender()->SetRenderFlags(OldFlags | TEXT_RENDER_FLAG_ONE_TIME_USE);
if(m_FPSTextContainerIndex.Valid())
TextRender()->RecreateTextContainerSoft(m_FPSTextContainerIndex, &Cursor, aBuf);
else
TextRender()->CreateTextContainer(m_FPSTextContainerIndex, &Cursor, "0");
TextRender()->SetRenderFlags(OldFlags);
if(m_FPSTextContainerIndex.Valid())
{
TextRender()->RenderTextContainer(m_FPSTextContainerIndex, TextRender()->DefaultTextColor(), TextRender()->DefaultTextOutlineColor());
}
}
if(g_Config.m_ClShowpred)
{
char aBuf[64];
str_format(aBuf, sizeof(aBuf), "%d", Client()->GetPredictionTime());
TextRender()->Text(m_Width - 10 - TextRender()->TextWidth(12, aBuf, -1, -1.0f), g_Config.m_ClShowfps ? 20 : 5, 12, aBuf, -1.0f);
}
}
void CHud::RenderConnectionWarning()
{
if(Client()->ConnectionProblems())
{
const char *pText = Localize("Connection Problems...");
float w = TextRender()->TextWidth(24, pText, -1, -1.0f);
TextRender()->Text(150 * Graphics()->ScreenAspect() - w / 2, 50, 24, pText, -1.0f);
}
}
void CHud::RenderTeambalanceWarning()
{
// render prompt about team-balance
bool Flash = time() / (time_freq() / 2) % 2 == 0;
if(m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS)
{
int TeamDiff = m_pClient->m_Snap.m_aTeamSize[TEAM_RED] - m_pClient->m_Snap.m_aTeamSize[TEAM_BLUE];
if(g_Config.m_ClWarningTeambalance && (TeamDiff >= 2 || TeamDiff <= -2))
{
const char *pText = Localize("Please balance teams!");
if(Flash)
TextRender()->TextColor(1, 1, 0.5f, 1);
else
TextRender()->TextColor(0.7f, 0.7f, 0.2f, 1.0f);
TextRender()->Text(5, 50, 6, pText, -1.0f);
TextRender()->TextColor(TextRender()->DefaultTextColor());
}
}
}
void CHud::RenderVoting()
{
if((!g_Config.m_ClShowVotesAfterVoting && !m_pClient->m_Scoreboard.Active() && m_pClient->m_Voting.TakenChoice()) || !m_pClient->m_Voting.IsVoting() || Client()->State() == IClient::STATE_DEMOPLAYBACK)
return;
Graphics()->DrawRect(-10, 60 - 2, 100 + 10 + 4 + 5, 46, ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), IGraphics::CORNER_ALL, 5.0f);
TextRender()->TextColor(TextRender()->DefaultTextColor());
CTextCursor Cursor;
char aBuf[512];
str_format(aBuf, sizeof(aBuf), Localize("%ds left"), m_pClient->m_Voting.SecondsLeft());
float tw = TextRender()->TextWidth(6, aBuf, -1, -1.0f);
TextRender()->SetCursor(&Cursor, 5.0f + 100.0f - tw, 60.0f, 6.0f, TEXTFLAG_RENDER);
TextRender()->TextEx(&Cursor, aBuf, -1);
TextRender()->SetCursor(&Cursor, 5.0f, 60.0f, 6.0f, TEXTFLAG_RENDER);
Cursor.m_LineWidth = 100.0f - tw;
Cursor.m_MaxLines = 3;
TextRender()->TextEx(&Cursor, m_pClient->m_Voting.VoteDescription(), -1);
// reason
str_format(aBuf, sizeof(aBuf), "%s %s", Localize("Reason:"), m_pClient->m_Voting.VoteReason());
TextRender()->SetCursor(&Cursor, 5.0f, 79.0f, 6.0f, TEXTFLAG_RENDER | TEXTFLAG_STOP_AT_END);
Cursor.m_LineWidth = 100.0f;
TextRender()->TextEx(&Cursor, aBuf, -1);
CUIRect Base = {5, 88, 100, 4};
m_pClient->m_Voting.RenderBars(Base, false);
char aKey[64];
m_pClient->m_Binds.GetKey("vote yes", aKey, sizeof(aKey));
str_format(aBuf, sizeof(aBuf), "%s - %s", aKey, Localize("Vote yes"));
Base.y += Base.h;
Base.h = 12.0f;
UI()->DoLabel(&Base, aBuf, 6.0f, TEXTALIGN_ML);
m_pClient->m_Binds.GetKey("vote no", aKey, sizeof(aKey));
str_format(aBuf, sizeof(aBuf), "%s - %s", Localize("Vote no"), aKey);
UI()->DoLabel(&Base, aBuf, 6.0f, TEXTALIGN_MR);
}
void CHud::RenderCursor()
{
if(!m_pClient->m_Snap.m_pLocalCharacter || Client()->State() == IClient::STATE_DEMOPLAYBACK)
return;
RenderTools()->MapScreenToInterface(m_pClient->m_Camera.m_Center.x, m_pClient->m_Camera.m_Center.y);
// render cursor
int CurWeapon = m_pClient->m_Snap.m_pLocalCharacter->m_Weapon % NUM_WEAPONS;
Graphics()->SetColor(1.f, 1.f, 1.f, 1.f);
Graphics()->TextureSet(m_pClient->m_GameSkin.m_aSpriteWeaponCursors[CurWeapon]);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_aCursorOffset[CurWeapon], m_pClient->m_Controls.m_aTargetPos[g_Config.m_ClDummy].x, m_pClient->m_Controls.m_aTargetPos[g_Config.m_ClDummy].y);
}
void CHud::PrepareAmmoHealthAndArmorQuads()
{
float x = 5;
float y = 5;
IGraphics::CQuadItem Array[10];
// ammo of the different weapons
for(int i = 0; i < NUM_WEAPONS; ++i)
{
// 0.6
for(int n = 0; n < 10; n++)
Array[n] = IGraphics::CQuadItem(x + n * 12, y, 10, 10);
m_aAmmoOffset[i] = Graphics()->QuadContainerAddQuads(m_HudQuadContainerIndex, Array, 10);
// 0.7
if(i == WEAPON_GRENADE)
{
// special case for 0.7 grenade
for(int n = 0; n < 10; n++)
Array[n] = IGraphics::CQuadItem(1 + x + n * 12, y, 10, 10);
}
else
{
for(int n = 0; n < 10; n++)
Array[n] = IGraphics::CQuadItem(x + n * 12, y, 12, 12);
}
Graphics()->QuadContainerAddQuads(m_HudQuadContainerIndex, Array, 10);
}
// health
for(int i = 0; i < 10; ++i)
Array[i] = IGraphics::CQuadItem(x + i * 12, y, 10, 10);
m_HealthOffset = Graphics()->QuadContainerAddQuads(m_HudQuadContainerIndex, Array, 10);
// 0.7
for(int i = 0; i < 10; ++i)
Array[i] = IGraphics::CQuadItem(x + i * 12, y, 12, 12);
Graphics()->QuadContainerAddQuads(m_HudQuadContainerIndex, Array, 10);
// empty health
for(int i = 0; i < 10; ++i)
Array[i] = IGraphics::CQuadItem(x + i * 12, y, 10, 10);
m_EmptyHealthOffset = Graphics()->QuadContainerAddQuads(m_HudQuadContainerIndex, Array, 10);
// 0.7
for(int i = 0; i < 10; ++i)
Array[i] = IGraphics::CQuadItem(x + i * 12, y, 12, 12);
Graphics()->QuadContainerAddQuads(m_HudQuadContainerIndex, Array, 10);
// armor meter
for(int i = 0; i < 10; ++i)
Array[i] = IGraphics::CQuadItem(x + i * 12, y + 12, 10, 10);
m_ArmorOffset = Graphics()->QuadContainerAddQuads(m_HudQuadContainerIndex, Array, 10);
// 0.7
for(int i = 0; i < 10; ++i)
Array[i] = IGraphics::CQuadItem(x + i * 12, y + 12, 12, 12);
Graphics()->QuadContainerAddQuads(m_HudQuadContainerIndex, Array, 10);
// empty armor meter
for(int i = 0; i < 10; ++i)
Array[i] = IGraphics::CQuadItem(x + i * 12, y + 12, 10, 10);
m_EmptyArmorOffset = Graphics()->QuadContainerAddQuads(m_HudQuadContainerIndex, Array, 10);
// 0.7
for(int i = 0; i < 10; ++i)
Array[i] = IGraphics::CQuadItem(x + i * 12, y + 12, 12, 12);
Graphics()->QuadContainerAddQuads(m_HudQuadContainerIndex, Array, 10);
}
void CHud::RenderAmmoHealthAndArmor(const CNetObj_Character *pCharacter)
{
if(!pCharacter)
return;
bool IsSixupGameSkin = m_pClient->m_GameSkin.IsSixup();
int QuadOffsetSixup = (IsSixupGameSkin ? 10 : 0);
if(GameClient()->m_GameInfo.m_HudAmmo)
{
// ammo display
float AmmoOffsetY = GameClient()->m_GameInfo.m_HudHealthArmor ? 24 : 0;
int CurWeapon = pCharacter->m_Weapon % NUM_WEAPONS;
if(m_pClient->m_GameSkin.m_aSpriteWeaponProjectiles[CurWeapon].IsValid())
{
Graphics()->TextureSet(m_pClient->m_GameSkin.m_aSpriteWeaponProjectiles[CurWeapon]);
if(AmmoOffsetY > 0)
{
Graphics()->RenderQuadContainerEx(m_HudQuadContainerIndex, m_aAmmoOffset[CurWeapon] + QuadOffsetSixup, minimum(pCharacter->m_AmmoCount, 10), 0, AmmoOffsetY);
}
else
{
Graphics()->RenderQuadContainer(m_HudQuadContainerIndex, m_aAmmoOffset[CurWeapon] + QuadOffsetSixup, minimum(pCharacter->m_AmmoCount, 10));
}
}
}
if(GameClient()->m_GameInfo.m_HudHealthArmor)
{
// health display
Graphics()->TextureSet(m_pClient->m_GameSkin.m_SpriteHealthFull);
Graphics()->RenderQuadContainer(m_HudQuadContainerIndex, m_HealthOffset + QuadOffsetSixup, minimum(pCharacter->m_Health, 10));
Graphics()->TextureSet(m_pClient->m_GameSkin.m_SpriteHealthEmpty);
Graphics()->RenderQuadContainer(m_HudQuadContainerIndex, m_EmptyHealthOffset + QuadOffsetSixup + minimum(pCharacter->m_Health, 10), 10 - minimum(pCharacter->m_Health, 10));
// armor display
Graphics()->TextureSet(m_pClient->m_GameSkin.m_SpriteArmorFull);
Graphics()->RenderQuadContainer(m_HudQuadContainerIndex, m_ArmorOffset + QuadOffsetSixup, minimum(pCharacter->m_Armor, 10));
Graphics()->TextureSet(m_pClient->m_GameSkin.m_SpriteArmorEmpty);
Graphics()->RenderQuadContainer(m_HudQuadContainerIndex, m_ArmorOffset + QuadOffsetSixup + minimum(pCharacter->m_Armor, 10), 10 - minimum(pCharacter->m_Armor, 10));
}
}
void CHud::PreparePlayerStateQuads()
{
float x = 5;
float y = 5 + 24;
IGraphics::CQuadItem Array[10];
// Quads for displaying the available and used jumps
for(int i = 0; i < 10; ++i)
Array[i] = IGraphics::CQuadItem(x + i * 12, y, 12, 12);
m_AirjumpOffset = Graphics()->QuadContainerAddQuads(m_HudQuadContainerIndex, Array, 10);
for(int i = 0; i < 10; ++i)
Array[i] = IGraphics::CQuadItem(x + i * 12, y, 12, 12);
m_AirjumpEmptyOffset = Graphics()->QuadContainerAddQuads(m_HudQuadContainerIndex, Array, 10);
// Quads for displaying weapons
for(int Weapon = 0; Weapon < NUM_WEAPONS; ++Weapon)
{
const CDataWeaponspec &WeaponSpec = g_pData->m_Weapons.m_aId[Weapon];
float ScaleX, ScaleY;
RenderTools()->GetSpriteScale(WeaponSpec.m_pSpriteBody, ScaleX, ScaleY);
constexpr float HudWeaponScale = 0.25f;
float Width = WeaponSpec.m_VisualSize * ScaleX * HudWeaponScale;
float Height = WeaponSpec.m_VisualSize * ScaleY * HudWeaponScale;
m_aWeaponOffset[Weapon] = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, Width, Height);
}
// Quads for displaying capabilities
m_EndlessJumpOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
m_EndlessHookOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
m_JetpackOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
m_TeleportGrenadeOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
m_TeleportGunOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
m_TeleportLaserOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
// Quads for displaying prohibited capabilities
m_SoloOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
m_CollisionDisabledOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
m_HookHitDisabledOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
m_HammerHitDisabledOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
m_GunHitDisabledOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
m_ShotgunHitDisabledOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
m_GrenadeHitDisabledOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
m_LaserHitDisabledOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
// Quads for displaying freeze status
m_DeepFrozenOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
m_LiveFrozenOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
// Quads for displaying dummy actions
m_DummyHammerOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
m_DummyCopyOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
// Quad for displaying practice mode
m_PracticeModeOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f);
}
void CHud::RenderPlayerState(const int ClientID)
{
Graphics()->SetColor(1.f, 1.f, 1.f, 1.f);
// pCharacter contains the predicted character for local players or the last snap for players who are spectated
CCharacterCore *pCharacter = &m_pClient->m_aClients[ClientID].m_Predicted;
CNetObj_Character *pPlayer = &m_pClient->m_aClients[ClientID].m_RenderCur;
int TotalJumpsToDisplay = 0;
if(g_Config.m_ClShowhudJumpsIndicator)
{
int AvailableJumpsToDisplay;
if(m_pClient->m_Snap.m_aCharacters[ClientID].m_HasExtendedDisplayInfo)
{
bool Grounded = false;
if(Collision()->CheckPoint(pPlayer->m_X + CCharacterCore::PhysicalSize() / 2,
pPlayer->m_Y + CCharacterCore::PhysicalSize() / 2 + 5))
{
Grounded = true;
}
if(Collision()->CheckPoint(pPlayer->m_X - CCharacterCore::PhysicalSize() / 2,
pPlayer->m_Y + CCharacterCore::PhysicalSize() / 2 + 5))
{
Grounded = true;
}
int UsedJumps = pCharacter->m_JumpedTotal;
if(pCharacter->m_Jumps > 1)
{
UsedJumps += !Grounded;
}
else if(pCharacter->m_Jumps == 1)
{
// If the player has only one jump, each jump is the last one
UsedJumps = pPlayer->m_Jumped & 2;
}
else if(pCharacter->m_Jumps == -1)
{
// The player has only one ground jump
UsedJumps = !Grounded;
}
if(pCharacter->m_EndlessJump && UsedJumps >= absolute(pCharacter->m_Jumps))
{
UsedJumps = absolute(pCharacter->m_Jumps) - 1;
}
int UnusedJumps = absolute(pCharacter->m_Jumps) - UsedJumps;
if(!(pPlayer->m_Jumped & 2) && UnusedJumps <= 0)
{
// In some edge cases when the player just got another number of jumps, UnusedJumps is not correct
UnusedJumps = 1;
}
TotalJumpsToDisplay = maximum(minimum(absolute(pCharacter->m_Jumps), 10), 0);
AvailableJumpsToDisplay = maximum(minimum(UnusedJumps, TotalJumpsToDisplay), 0);
}
else
{
TotalJumpsToDisplay = AvailableJumpsToDisplay = absolute(m_pClient->m_Snap.m_aCharacters[ClientID].m_ExtendedData.m_Jumps);
}
// render available and used jumps
int JumpsOffsetY = ((GameClient()->m_GameInfo.m_HudHealthArmor && g_Config.m_ClShowhudHealthAmmo ? 24 : 0) +
(GameClient()->m_GameInfo.m_HudAmmo && g_Config.m_ClShowhudHealthAmmo ? 12 : 0));
if(JumpsOffsetY > 0)
{
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudAirjump);
Graphics()->RenderQuadContainerEx(m_HudQuadContainerIndex, m_AirjumpOffset, AvailableJumpsToDisplay, 0, JumpsOffsetY);
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudAirjumpEmpty);
Graphics()->RenderQuadContainerEx(m_HudQuadContainerIndex, m_AirjumpEmptyOffset + AvailableJumpsToDisplay, TotalJumpsToDisplay - AvailableJumpsToDisplay, 0, JumpsOffsetY);
}
else
{
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudAirjump);
Graphics()->RenderQuadContainer(m_HudQuadContainerIndex, m_AirjumpOffset, AvailableJumpsToDisplay);
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudAirjumpEmpty);
Graphics()->RenderQuadContainer(m_HudQuadContainerIndex, m_AirjumpEmptyOffset + AvailableJumpsToDisplay, TotalJumpsToDisplay - AvailableJumpsToDisplay);
}
}
float x = 5 + 12;
float y = (5 + 12 + (GameClient()->m_GameInfo.m_HudHealthArmor && g_Config.m_ClShowhudHealthAmmo ? 24 : 0) +
(GameClient()->m_GameInfo.m_HudAmmo && g_Config.m_ClShowhudHealthAmmo ? 12 : 0));
// render weapons
{
constexpr float aWeaponWidth[NUM_WEAPONS] = {16, 12, 12, 12, 12, 12};
constexpr float aWeaponInitialOffset[NUM_WEAPONS] = {-3, -4, -1, -1, -2, -4};
bool InitialOffsetAdded = false;
for(int Weapon = 0; Weapon < NUM_WEAPONS; ++Weapon)
{
if(!pCharacter->m_aWeapons[Weapon].m_Got)
continue;
if(!InitialOffsetAdded)
{
x += aWeaponInitialOffset[Weapon];
InitialOffsetAdded = true;
}
if(pPlayer->m_Weapon != Weapon)
Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.4f);
Graphics()->QuadsSetRotation(pi * 7 / 4);
Graphics()->TextureSet(m_pClient->m_GameSkin.m_aSpritePickupWeapons[Weapon]);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_aWeaponOffset[Weapon], x, y);
Graphics()->QuadsSetRotation(0);
Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
x += aWeaponWidth[Weapon];
}
if(pCharacter->m_aWeapons[WEAPON_NINJA].m_Got)
{
const int Max = g_pData->m_Weapons.m_Ninja.m_Duration * Client()->GameTickSpeed() / 1000;
float NinjaProgress = clamp(pCharacter->m_Ninja.m_ActivationTick + g_pData->m_Weapons.m_Ninja.m_Duration * Client()->GameTickSpeed() / 1000 - Client()->GameTick(g_Config.m_ClDummy), 0, Max) / (float)Max;
if(NinjaProgress > 0.0f && m_pClient->m_Snap.m_aCharacters[ClientID].m_HasExtendedDisplayInfo)
{
RenderNinjaBarPos(x, y - 12, 6.f, 24.f, NinjaProgress);
}
}
}
// render capabilities
x = 5;
y += 12;
if(TotalJumpsToDisplay > 0)
{
y += 12;
}
bool HasCapabilities = false;
if(pCharacter->m_EndlessJump)
{
HasCapabilities = true;
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudEndlessJump);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_EndlessJumpOffset, x, y);
x += 12;
}
if(pCharacter->m_EndlessHook)
{
HasCapabilities = true;
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudEndlessHook);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_EndlessHookOffset, x, y);
x += 12;
}
if(pCharacter->m_Jetpack)
{
HasCapabilities = true;
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudJetpack);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_JetpackOffset, x, y);
x += 12;
}
if(pCharacter->m_HasTelegunGun && pCharacter->m_aWeapons[WEAPON_GUN].m_Got)
{
HasCapabilities = true;
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudTeleportGun);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_TeleportGunOffset, x, y);
x += 12;
}
if(pCharacter->m_HasTelegunGrenade && pCharacter->m_aWeapons[WEAPON_GRENADE].m_Got)
{
HasCapabilities = true;
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudTeleportGrenade);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_TeleportGrenadeOffset, x, y);
x += 12;
}
if(pCharacter->m_HasTelegunLaser && pCharacter->m_aWeapons[WEAPON_LASER].m_Got)
{
HasCapabilities = true;
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudTeleportLaser);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_TeleportLaserOffset, x, y);
}
// render prohibited capabilities
x = 5;
if(HasCapabilities)
{
y += 12;
}
bool HasProhibitedCapabilities = false;
if(pCharacter->m_Solo)
{
HasProhibitedCapabilities = true;
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudSolo);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_SoloOffset, x, y);
x += 12;
}
if(pCharacter->m_CollisionDisabled)
{
HasProhibitedCapabilities = true;
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudCollisionDisabled);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_CollisionDisabledOffset, x, y);
x += 12;
}
if(pCharacter->m_HookHitDisabled)
{
HasProhibitedCapabilities = true;
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudHookHitDisabled);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_HookHitDisabledOffset, x, y);
x += 12;
}
if(pCharacter->m_HammerHitDisabled)
{
HasProhibitedCapabilities = true;
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudHammerHitDisabled);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_HammerHitDisabledOffset, x, y);
x += 12;
}
if((pCharacter->m_GrenadeHitDisabled && pCharacter->m_HasTelegunGun && pCharacter->m_aWeapons[WEAPON_GUN].m_Got))
{
HasProhibitedCapabilities = true;
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudGunHitDisabled);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_LaserHitDisabledOffset, x, y);
x += 12;
}
if((pCharacter->m_ShotgunHitDisabled && pCharacter->m_aWeapons[WEAPON_SHOTGUN].m_Got))
{
HasProhibitedCapabilities = true;
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudShotgunHitDisabled);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_ShotgunHitDisabledOffset, x, y);
x += 12;
}
if((pCharacter->m_GrenadeHitDisabled && pCharacter->m_aWeapons[WEAPON_GRENADE].m_Got))
{
HasProhibitedCapabilities = true;
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudGrenadeHitDisabled);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_GrenadeHitDisabledOffset, x, y);
x += 12;
}
if((pCharacter->m_LaserHitDisabled && pCharacter->m_aWeapons[WEAPON_LASER].m_Got))
{
HasProhibitedCapabilities = true;
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudLaserHitDisabled);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_LaserHitDisabledOffset, x, y);
}
// render dummy actions and freeze state
x = 5;
if(HasProhibitedCapabilities)
{
y += 12;
}
if(m_pClient->m_Snap.m_aCharacters[ClientID].m_HasExtendedDisplayInfo && m_pClient->m_Snap.m_aCharacters[ClientID].m_ExtendedData.m_Flags & CHARACTERFLAG_PRACTICE_MODE)
{
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudPracticeMode);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_PracticeModeOffset, x, y);
x += 12;
}
if(pCharacter->m_DeepFrozen)
{
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudDeepFrozen);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_DeepFrozenOffset, x, y);
x += 12;
}
if(pCharacter->m_LiveFrozen)
{
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudLiveFrozen);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_LiveFrozenOffset, x, y);
}
}
void CHud::RenderNinjaBarPos(const float x, float y, const float width, const float height, float Progress, const float Alpha)
{
Progress = clamp(Progress, 0.0f, 1.0f);
// what percentage of the end pieces is used for the progress indicator and how much is the rest
// half of the ends are used for the progress display
const float RestPct = 0.5f;
const float ProgPct = 0.5f;
const float EndHeight = width; // to keep the correct scale - the width of the sprite is as long as the height
const float BarWidth = width;
const float WholeBarHeight = height;
const float MiddleBarHeight = WholeBarHeight - (EndHeight * 2.0f);
const float EndProgressHeight = EndHeight * ProgPct;
const float EndRestHeight = EndHeight * RestPct;
const float ProgressBarHeight = WholeBarHeight - (EndProgressHeight * 2.0f);
const float EndProgressProportion = EndProgressHeight / ProgressBarHeight;
const float MiddleProgressProportion = MiddleBarHeight / ProgressBarHeight;
// beginning piece
float BeginningPieceProgress = 1;
if(Progress <= 1)
{
if(Progress <= (EndProgressProportion + MiddleProgressProportion))
{
BeginningPieceProgress = 0;
}
else
{
BeginningPieceProgress = (Progress - EndProgressProportion - MiddleProgressProportion) / EndProgressProportion;
}
}
// empty
Graphics()->WrapClamp();
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudNinjaBarEmptyRight);
Graphics()->QuadsBegin();
Graphics()->SetColor(1.f, 1.f, 1.f, Alpha);
// Subset: btm_r, top_r, top_m, btm_m | it is mirrored on the horizontal axe and rotated 90 degrees counterclockwise
Graphics()->QuadsSetSubsetFree(1, 1, 1, 0, ProgPct - ProgPct * (1.0f - BeginningPieceProgress), 0, ProgPct - ProgPct * (1.0f - BeginningPieceProgress), 1);
IGraphics::CQuadItem QuadEmptyBeginning(x, y, BarWidth, EndRestHeight + EndProgressHeight * (1.0f - BeginningPieceProgress));
Graphics()->QuadsDrawTL(&QuadEmptyBeginning, 1);
Graphics()->QuadsEnd();
// full
if(BeginningPieceProgress > 0.0f)
{
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudNinjaBarFullLeft);
Graphics()->QuadsBegin();
Graphics()->SetColor(1.f, 1.f, 1.f, Alpha);
// Subset: btm_m, top_m, top_r, btm_r | it is rotated 90 degrees clockwise
Graphics()->QuadsSetSubsetFree(RestPct + ProgPct * (1.0f - BeginningPieceProgress), 1, RestPct + ProgPct * (1.0f - BeginningPieceProgress), 0, 1, 0, 1, 1);
IGraphics::CQuadItem QuadFullBeginning(x, y + (EndRestHeight + EndProgressHeight * (1.0f - BeginningPieceProgress)), BarWidth, EndProgressHeight * BeginningPieceProgress);
Graphics()->QuadsDrawTL(&QuadFullBeginning, 1);
Graphics()->QuadsEnd();
}
// middle piece
y += EndHeight;
float MiddlePieceProgress = 1;
if(Progress <= EndProgressProportion + MiddleProgressProportion)
{
if(Progress <= EndProgressProportion)
{
MiddlePieceProgress = 0;
}
else
{
MiddlePieceProgress = (Progress - EndProgressProportion) / MiddleProgressProportion;
}
}
const float FullMiddleBarHeight = MiddleBarHeight * MiddlePieceProgress;
const float EmptyMiddleBarHeight = MiddleBarHeight - FullMiddleBarHeight;
// empty ninja bar
if(EmptyMiddleBarHeight > 0.0f)
{
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudNinjaBarEmpty);
Graphics()->QuadsBegin();
Graphics()->SetColor(1.f, 1.f, 1.f, Alpha);
// select the middle portion of the sprite so we don't get edge bleeding
if(EmptyMiddleBarHeight <= EndHeight)
{
// prevent pixel puree, select only a small slice
// Subset: btm_r, top_r, top_m, btm_m | it is mirrored on the horizontal axe and rotated 90 degrees counterclockwise
Graphics()->QuadsSetSubsetFree(1, 1, 1, 0, 1.0f - (EmptyMiddleBarHeight / EndHeight), 0, 1.0f - (EmptyMiddleBarHeight / EndHeight), 1);
}
else
{
// Subset: btm_r, top_r, top_l, btm_l | it is mirrored on the horizontal axe and rotated 90 degrees counterclockwise
Graphics()->QuadsSetSubsetFree(1, 1, 1, 0, 0, 0, 0, 1);
}
IGraphics::CQuadItem QuadEmpty(x, y, BarWidth, EmptyMiddleBarHeight);
Graphics()->QuadsDrawTL(&QuadEmpty, 1);
Graphics()->QuadsEnd();
}
// full ninja bar
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudNinjaBarFull);
Graphics()->QuadsBegin();
Graphics()->SetColor(1.f, 1.f, 1.f, Alpha);
// select the middle portion of the sprite so we don't get edge bleeding
if(FullMiddleBarHeight <= EndHeight)
{
// prevent pixel puree, select only a small slice
// Subset: btm_m, top_m, top_r, btm_r | it is rotated 90 degrees clockwise
Graphics()->QuadsSetSubsetFree(1.0f - (FullMiddleBarHeight / EndHeight), 1, 1.0f - (FullMiddleBarHeight / EndHeight), 0, 1, 0, 1, 1);
}
else
{
// Subset: btm_l, top_l, top_r, btm_r | it is rotated 90 degrees clockwise
Graphics()->QuadsSetSubsetFree(0, 1, 0, 0, 1, 0, 1, 1);
}
IGraphics::CQuadItem QuadFull(x, y + EmptyMiddleBarHeight, BarWidth, FullMiddleBarHeight);
Graphics()->QuadsDrawTL(&QuadFull, 1);
Graphics()->QuadsEnd();
// ending piece
y += MiddleBarHeight;
float EndingPieceProgress = 1;
if(Progress <= EndProgressProportion)
{
EndingPieceProgress = Progress / EndProgressProportion;
}
// empty
if(EndingPieceProgress < 1.0f)
{
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudNinjaBarEmptyRight);
Graphics()->QuadsBegin();
Graphics()->SetColor(1.f, 1.f, 1.f, Alpha);
// Subset: btm_l, top_l, top_m, btm_m | it is rotated 90 degrees clockwise
Graphics()->QuadsSetSubsetFree(0, 1, 0, 0, ProgPct - ProgPct * EndingPieceProgress, 0, ProgPct - ProgPct * EndingPieceProgress, 1);
IGraphics::CQuadItem QuadEmptyEnding(x, y, BarWidth, EndProgressHeight * (1.0f - EndingPieceProgress));
Graphics()->QuadsDrawTL(&QuadEmptyEnding, 1);
Graphics()->QuadsEnd();
}
// full
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudNinjaBarFullLeft);
Graphics()->QuadsBegin();
Graphics()->SetColor(1.f, 1.f, 1.f, Alpha);
// Subset: btm_m, top_m, top_l, btm_l | it is mirrored on the horizontal axe and rotated 90 degrees counterclockwise
Graphics()->QuadsSetSubsetFree(RestPct + ProgPct * EndingPieceProgress, 1, RestPct + ProgPct * EndingPieceProgress, 0, 0, 0, 0, 1);
IGraphics::CQuadItem QuadFullEnding(x, y + (EndProgressHeight * (1.0f - EndingPieceProgress)), BarWidth, EndRestHeight + EndProgressHeight * EndingPieceProgress);
Graphics()->QuadsDrawTL(&QuadFullEnding, 1);
Graphics()->QuadsEnd();
Graphics()->QuadsSetSubset(0, 0, 1, 1);
Graphics()->SetColor(1.f, 1.f, 1.f, 1.f);
Graphics()->WrapNormal();
}
void CHud::RenderDummyActions()
{
if(!g_Config.m_ClShowhudDummyActions || (m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER) || !Client()->DummyConnected())
{
return;
}
// render small dummy actions hud
const float BoxHeight = 29.0f;
const float BoxWidth = 16.0f;
float StartX = m_Width - BoxWidth;
float StartY = 285.0f - BoxHeight - 4; // 4 units distance to the next display;
if(g_Config.m_ClShowhudPlayerPosition || g_Config.m_ClShowhudPlayerSpeed || g_Config.m_ClShowhudPlayerAngle)
{
StartY -= 4;
}
StartY -= GetMovementInformationBoxHeight();
if(g_Config.m_ClShowhudScore)
{
StartY -= 56;
}
Graphics()->DrawRect(StartX, StartY, BoxWidth, BoxHeight, ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), IGraphics::CORNER_L, 5.0f);
float y = StartY + 2;
float x = StartX + 2;
Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.4f);
if(g_Config.m_ClDummyHammer)
{
Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
}
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudDummyHammer);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_DummyHammerOffset, x, y);
y += 13;
Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.4f);
if(g_Config.m_ClDummyCopyMoves)
{
Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
}
Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudDummyCopy);
Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_DummyCopyOffset, x, y);
}
inline int CHud::GetDigitsIndex(int Value, int Max)
{
if(Value < 0)
{
Value *= -1;
}
int DigitsIndex = std::log10((Value ? Value : 1));
if(DigitsIndex > Max)
{
DigitsIndex = Max;
}
if(DigitsIndex < 0)
{
DigitsIndex = 0;
}
return DigitsIndex;
}
inline float CHud::GetMovementInformationBoxHeight()
{
float BoxHeight = 3 * MOVEMENT_INFORMATION_LINE_HEIGHT * (g_Config.m_ClShowhudPlayerPosition + g_Config.m_ClShowhudPlayerSpeed) + 2 * MOVEMENT_INFORMATION_LINE_HEIGHT * g_Config.m_ClShowhudPlayerAngle;
if(g_Config.m_ClShowhudPlayerPosition || g_Config.m_ClShowhudPlayerSpeed || g_Config.m_ClShowhudPlayerAngle)
{
BoxHeight += 2;
}
return BoxHeight;
}
void CHud::RenderMovementInformation(const int ClientID)
{
// Draw the infomations depending on settings: Position, speed and target angle
// This display is only to present the available information from the last snapshot, not to interpolate or predict
if(!g_Config.m_ClShowhudPlayerPosition && !g_Config.m_ClShowhudPlayerSpeed && !g_Config.m_ClShowhudPlayerAngle)
{
return;
}
const float LineSpacer = 1.0f; // above and below each entry
const float Fontsize = 6.0f;
float BoxHeight = GetMovementInformationBoxHeight();
const float BoxWidth = 62.0f;
float StartX = m_Width - BoxWidth;
float StartY = 285.0f - BoxHeight - 4; // 4 units distance to the next display;
if(g_Config.m_ClShowhudScore)
{
StartY -= 56;
}
Graphics()->DrawRect(StartX, StartY, BoxWidth, BoxHeight, ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), IGraphics::CORNER_L, 5.0f);
CNetObj_Character *pCharacter = &m_pClient->m_Snap.m_aCharacters[ClientID].m_Cur;
const float TicksPerSecond = 50.0f;
// To make the player position relative to blocks we need to divide by the block size
float PosX = pCharacter->m_X / 32.0f;
float PosY = pCharacter->m_Y / 32.0f;
float VelspeedX = pCharacter->m_VelX / 256.0f * TicksPerSecond;
if(pCharacter->m_VelX >= -1 && pCharacter->m_VelX <= 1)
{
VelspeedX = 0;
}
float VelspeedY = pCharacter->m_VelY / 256.0f * TicksPerSecond;
if(pCharacter->m_VelY >= -128 && pCharacter->m_VelY <= 128)
{
VelspeedY = 0;
}
// We show the speed in Blocks per Second (Bps) and therefore have to divide by the block size
float DisplaySpeedX = VelspeedX / 32;
float VelspeedLength = length(vec2(pCharacter->m_VelX / 256.0f, pCharacter->m_VelY / 256.0f)) * TicksPerSecond;
// Todo: Use Velramp tuning of each individual player
// Since these tuning parameters are almost never changed, the default values are sufficient in most cases
float Ramp = VelocityRamp(VelspeedLength, m_pClient->m_aTuning[g_Config.m_ClDummy].m_VelrampStart, m_pClient->m_aTuning[g_Config.m_ClDummy].m_VelrampRange, m_pClient->m_aTuning[g_Config.m_ClDummy].m_VelrampCurvature);
DisplaySpeedX *= Ramp;
float DisplaySpeedY = VelspeedY / 32;
float Angle = 0.0f;
if(m_pClient->m_Snap.m_aCharacters[ClientID].m_HasExtendedDisplayInfo)
{
// On DDNet servers the more accurate angle is displayed, calculated from the target coordinates
CNetObj_DDNetCharacter *pExtendedData = &m_pClient->m_Snap.m_aCharacters[ClientID].m_ExtendedData;
Angle = std::atan2(pExtendedData->m_TargetY, pExtendedData->m_TargetX);
}
else
{
Angle = pCharacter->m_Angle / 256.0f;
}
if(Angle < 0)
{
Angle += 2.0f * pi;
}
float DisplayAngle = Angle * 180.0f / pi;
char aBuf[128];
float w;
float y = StartY + LineSpacer * 2;
float xl = StartX + 2;
float xr = m_Width - 2;
int DigitsIndex;
static float s_TextWidth0 = TextRender()->TextWidth(Fontsize, "0.00", -1, -1.0f);
static float s_TextWidth00 = TextRender()->TextWidth(Fontsize, "00.00", -1, -1.0f);
static float s_TextWidth000 = TextRender()->TextWidth(Fontsize, "000.00", -1, -1.0f);
static float s_TextWidth0000 = TextRender()->TextWidth(Fontsize, "0000.00", -1, -1.0f);
static float s_TextWidth00000 = TextRender()->TextWidth(Fontsize, "00000.00", -1, -1.0f);
static float s_TextWidth000000 = TextRender()->TextWidth(Fontsize, "000000.00", -1, -1.0f);
static float s_aTextWidth[6] = {s_TextWidth0, s_TextWidth00, s_TextWidth000, s_TextWidth0000, s_TextWidth00000, s_TextWidth000000};
static float s_TextWidthMinus0 = TextRender()->TextWidth(Fontsize, "-0.00", -1, -1.0f);
static float s_TextWidthMinus00 = TextRender()->TextWidth(Fontsize, "-00.00", -1, -1.0f);
static float s_TextWidthMinus000 = TextRender()->TextWidth(Fontsize, "-000.00", -1, -1.0f);
static float s_TextWidthMinus0000 = TextRender()->TextWidth(Fontsize, "-0000.00", -1, -1.0f);
static float s_TextWidthMinus00000 = TextRender()->TextWidth(Fontsize, "-00000.00", -1, -1.0f);
static float s_TextWidthMinus000000 = TextRender()->TextWidth(Fontsize, "-000000.00", -1, -1.0f);
static float s_aTextWidthMinus[6] = {s_TextWidthMinus0, s_TextWidthMinus00, s_TextWidthMinus000, s_TextWidthMinus0000, s_TextWidthMinus00000, s_TextWidthMinus000000};
if(g_Config.m_ClShowhudPlayerPosition)
{
TextRender()->Text(xl, y, Fontsize, Localize("Position:"), -1.0f);
y += MOVEMENT_INFORMATION_LINE_HEIGHT;
TextRender()->Text(xl, y, Fontsize, "X:", -1.0f);
str_format(aBuf, sizeof(aBuf), "%.2f", PosX);
DigitsIndex = GetDigitsIndex(PosX, 5);
w = (PosX < 0) ? s_aTextWidthMinus[DigitsIndex] : s_aTextWidth[DigitsIndex];
TextRender()->Text(xr - w, y, Fontsize, aBuf, -1.0f);
y += MOVEMENT_INFORMATION_LINE_HEIGHT;
TextRender()->Text(xl, y, Fontsize, "Y:", -1.0f);
str_format(aBuf, sizeof(aBuf), "%.2f", PosY);
DigitsIndex = GetDigitsIndex(PosY, 5);
w = (PosY < 0) ? s_aTextWidthMinus[DigitsIndex] : s_aTextWidth[DigitsIndex];
TextRender()->Text(xr - w, y, Fontsize, aBuf, -1.0f);
y += MOVEMENT_INFORMATION_LINE_HEIGHT;
}
if(g_Config.m_ClShowhudPlayerSpeed)
{
TextRender()->Text(xl, y, Fontsize, Localize("Speed:"), -1.0f);
y += MOVEMENT_INFORMATION_LINE_HEIGHT;
TextRender()->Text(xl, y, Fontsize, "X:", -1.0f);
str_format(aBuf, sizeof(aBuf), "%.2f", DisplaySpeedX);
DigitsIndex = GetDigitsIndex(DisplaySpeedX, 5);
w = (DisplaySpeedX < 0) ? s_aTextWidthMinus[DigitsIndex] : s_aTextWidth[DigitsIndex];
TextRender()->Text(xr - w, y, Fontsize, aBuf, -1.0f);
y += MOVEMENT_INFORMATION_LINE_HEIGHT;
TextRender()->Text(xl, y, Fontsize, "Y:", -1.0f);
str_format(aBuf, sizeof(aBuf), "%.2f", DisplaySpeedY);
DigitsIndex = GetDigitsIndex(DisplaySpeedY, 5);
w = (DisplaySpeedY < 0) ? s_aTextWidthMinus[DigitsIndex] : s_aTextWidth[DigitsIndex];
TextRender()->Text(xr - w, y, Fontsize, aBuf, -1.0f);
y += MOVEMENT_INFORMATION_LINE_HEIGHT;
}
if(g_Config.m_ClShowhudPlayerAngle)
{
TextRender()->Text(xl, y, Fontsize, Localize("Angle:"), -1.0f);
y += MOVEMENT_INFORMATION_LINE_HEIGHT;
str_format(aBuf, sizeof(aBuf), "%.2f", DisplayAngle);
DigitsIndex = GetDigitsIndex(DisplayAngle, 5);
w = (DisplayAngle < 0) ? s_aTextWidthMinus[DigitsIndex] : s_aTextWidth[DigitsIndex];
TextRender()->Text(xr - w, y, Fontsize, aBuf, -1.0f);
}
}
void CHud::RenderSpectatorHud()
{
// draw the box
Graphics()->DrawRect(m_Width - 180.0f, m_Height - 15.0f, 180.0f, 15.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), IGraphics::CORNER_TL, 5.0f);
// draw the text
char aBuf[128];
str_format(aBuf, sizeof(aBuf), "%s: %s", Localize("Spectate"), GameClient()->m_MultiViewActivated ? Localize("Multi-View") : m_pClient->m_Snap.m_SpecInfo.m_SpectatorID != SPEC_FREEVIEW ? m_pClient->m_aClients[m_pClient->m_Snap.m_SpecInfo.m_SpectatorID].m_aName : Localize("Free-View"));
TextRender()->Text(m_Width - 174.0f, m_Height - 15.0f + (15.f - 8.f) / 2.f, 8.0f, aBuf, -1.0f);
}
void CHud::RenderLocalTime(float x)
{
if(!g_Config.m_ClShowLocalTimeAlways && !m_pClient->m_Scoreboard.Active())
return;
// draw the box
Graphics()->DrawRect(x - 30.0f, 0.0f, 25.0f, 12.5f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), IGraphics::CORNER_B, 3.75f);
// draw the text
char aTimeStr[6];
str_timestamp_format(aTimeStr, sizeof(aTimeStr), "%H:%M");
TextRender()->Text(x - 25.0f, (12.5f - 5.f) / 2.f, 5.0f, aTimeStr, -1.0f);
}
void CHud::OnRender()
{
if(!m_pClient->m_Snap.m_pGameInfoObj)
return;
m_Width = 300.0f * Graphics()->ScreenAspect();
m_Height = 300.0f;
Graphics()->MapScreen(0.0f, 0.0f, m_Width, m_Height);
#if defined(CONF_VIDEORECORDER)
if((IVideo::Current() && g_Config.m_ClVideoShowhud) || (!IVideo::Current() && g_Config.m_ClShowhud))
#else
if(g_Config.m_ClShowhud)
#endif
{
if(m_pClient->m_Snap.m_pLocalCharacter && !m_pClient->m_Snap.m_SpecInfo.m_Active && !(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
{
if(g_Config.m_ClShowhudHealthAmmo)
{
RenderAmmoHealthAndArmor(m_pClient->m_Snap.m_pLocalCharacter);
}
if(m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientID].m_HasExtendedData && g_Config.m_ClShowhudDDRace && GameClient()->m_GameInfo.m_HudDDRace)
{
RenderPlayerState(m_pClient->m_Snap.m_LocalClientID);
}
RenderMovementInformation(m_pClient->m_Snap.m_LocalClientID);
RenderDDRaceEffects();
}
else if(m_pClient->m_Snap.m_SpecInfo.m_Active)
{
int SpectatorID = m_pClient->m_Snap.m_SpecInfo.m_SpectatorID;
if(SpectatorID != SPEC_FREEVIEW && g_Config.m_ClShowhudHealthAmmo)
{
RenderAmmoHealthAndArmor(&m_pClient->m_Snap.m_aCharacters[SpectatorID].m_Cur);
}
if(SpectatorID != SPEC_FREEVIEW &&
m_pClient->m_Snap.m_aCharacters[SpectatorID].m_HasExtendedData &&
g_Config.m_ClShowhudDDRace &&
(!GameClient()->m_MultiViewActivated || GameClient()->m_MultiViewShowHud) &&
GameClient()->m_GameInfo.m_HudDDRace)
{
RenderPlayerState(SpectatorID);
}
if(SpectatorID != SPEC_FREEVIEW)
{
RenderMovementInformation(SpectatorID);
}
RenderSpectatorHud();
}
if(g_Config.m_ClShowhudTimer)
RenderGameTimer();
RenderPauseNotification();
RenderSuddenDeath();
if(g_Config.m_ClShowhudScore)
RenderScoreHud();
RenderDummyActions();
RenderWarmupTimer();
RenderTextInfo();
RenderLocalTime((m_Width / 7) * 3);
if(Client()->State() != IClient::STATE_DEMOPLAYBACK)
RenderConnectionWarning();
RenderTeambalanceWarning();
RenderVoting();
if(g_Config.m_ClShowRecord)
RenderRecord();
}
RenderCursor();
}
void CHud::OnMessage(int MsgType, void *pRawMsg)
{
if(MsgType == NETMSGTYPE_SV_DDRACETIME || MsgType == NETMSGTYPE_SV_DDRACETIMELEGACY)
{
CNetMsg_Sv_DDRaceTime *pMsg = (CNetMsg_Sv_DDRaceTime *)pRawMsg;
m_DDRaceTime = pMsg->m_Time;
m_ShowFinishTime = pMsg->m_Finish != 0;
if(!m_ShowFinishTime)
{
m_TimeCpDiff = (float)pMsg->m_Check / 100;
m_TimeCpLastReceivedTick = Client()->GameTick(g_Config.m_ClDummy);
}
else
{
m_FinishTimeDiff = (float)pMsg->m_Check / 100;
m_FinishTimeLastReceivedTick = Client()->GameTick(g_Config.m_ClDummy);
}
}
else if(MsgType == NETMSGTYPE_SV_RECORD || MsgType == NETMSGTYPE_SV_RECORDLEGACY)
{
CNetMsg_Sv_Record *pMsg = (CNetMsg_Sv_Record *)pRawMsg;
// NETMSGTYPE_SV_RACETIME on old race servers
if(MsgType == NETMSGTYPE_SV_RECORDLEGACY && m_pClient->m_GameInfo.m_DDRaceRecordMessage)
{
m_DDRaceTime = pMsg->m_ServerTimeBest; // First value: m_Time
m_FinishTimeLastReceivedTick = Client()->GameTick(g_Config.m_ClDummy);
if(pMsg->m_PlayerTimeBest) // Second value: m_Check
{
m_TimeCpDiff = (float)pMsg->m_PlayerTimeBest / 100;
m_TimeCpLastReceivedTick = Client()->GameTick(g_Config.m_ClDummy);
}
}
else if(MsgType == NETMSGTYPE_SV_RECORD || m_pClient->m_GameInfo.m_RaceRecordMessage)
{
m_ServerRecord = (float)pMsg->m_ServerTimeBest / 100;
m_aPlayerRecord[g_Config.m_ClDummy] = (float)pMsg->m_PlayerTimeBest / 100;
}
}
}
void CHud::RenderDDRaceEffects()
{
if(m_DDRaceTime)
{
char aBuf[64];
char aTime[32];
if(m_ShowFinishTime && m_FinishTimeLastReceivedTick + Client()->GameTickSpeed() * 6 > Client()->GameTick(g_Config.m_ClDummy))
{
str_time(m_DDRaceTime, TIME_HOURS_CENTISECS, aTime, sizeof(aTime));
str_format(aBuf, sizeof(aBuf), "Finish time: %s", aTime);
// calculate alpha (4 sec 1 than get lower the next 2 sec)
float Alpha = 1.0f;
if(m_FinishTimeLastReceivedTick + Client()->GameTickSpeed() * 4 < Client()->GameTick(g_Config.m_ClDummy) && m_FinishTimeLastReceivedTick + Client()->GameTickSpeed() * 6 > Client()->GameTick(g_Config.m_ClDummy))
{
// lower the alpha slowly to blend text out
Alpha = ((float)(m_FinishTimeLastReceivedTick + Client()->GameTickSpeed() * 6) - (float)Client()->GameTick(g_Config.m_ClDummy)) / (float)(Client()->GameTickSpeed() * 2);
}
TextRender()->TextColor(1, 1, 1, Alpha);
CTextCursor Cursor;
TextRender()->SetCursor(&Cursor, 150 * Graphics()->ScreenAspect() - TextRender()->TextWidth(12, aBuf, -1, -1.0f) / 2, 20, 12, TEXTFLAG_RENDER);
Cursor.m_LineWidth = -1.0f;
TextRender()->RecreateTextContainer(m_DDRaceEffectsTextContainerIndex, &Cursor, aBuf);
if(m_FinishTimeDiff != 0.0f && m_DDRaceEffectsTextContainerIndex.Valid())
{
if(m_FinishTimeDiff < 0)
{
str_time_float(-m_FinishTimeDiff, TIME_HOURS_CENTISECS, aTime, sizeof(aTime));
str_format(aBuf, sizeof(aBuf), "-%s", aTime);
TextRender()->TextColor(0.5f, 1.0f, 0.5f, Alpha); // green
}
else
{
str_time_float(m_FinishTimeDiff, TIME_HOURS_CENTISECS, aTime, sizeof(aTime));
str_format(aBuf, sizeof(aBuf), "+%s", aTime);
TextRender()->TextColor(1.0f, 0.5f, 0.5f, Alpha); // red
}
TextRender()->SetCursor(&Cursor, 150 * Graphics()->ScreenAspect() - TextRender()->TextWidth(10, aBuf, -1, -1.0f) / 2, 34, 10, TEXTFLAG_RENDER);
Cursor.m_LineWidth = -1.0f;
TextRender()->AppendTextContainer(m_DDRaceEffectsTextContainerIndex, &Cursor, aBuf);
}
if(m_DDRaceEffectsTextContainerIndex.Valid())
{
auto OutlineColor = TextRender()->DefaultTextOutlineColor();
OutlineColor.a *= Alpha;
TextRender()->RenderTextContainer(m_DDRaceEffectsTextContainerIndex, TextRender()->DefaultTextColor(), OutlineColor);
}
TextRender()->TextColor(TextRender()->DefaultTextColor());
}
else if(!m_ShowFinishTime && m_TimeCpLastReceivedTick + Client()->GameTickSpeed() * 6 > Client()->GameTick(g_Config.m_ClDummy))
{
if(m_TimeCpDiff < 0)
{
str_time_float(-m_TimeCpDiff, TIME_HOURS_CENTISECS, aTime, sizeof(aTime));
str_format(aBuf, sizeof(aBuf), "-%s", aTime);
}
else
{
str_time_float(m_TimeCpDiff, TIME_HOURS_CENTISECS, aTime, sizeof(aTime));
str_format(aBuf, sizeof(aBuf), "+%s", aTime);
}
// calculate alpha (4 sec 1 than get lower the next 2 sec)
float Alpha = 1.0f;
if(m_TimeCpLastReceivedTick + Client()->GameTickSpeed() * 4 < Client()->GameTick(g_Config.m_ClDummy) && m_TimeCpLastReceivedTick + Client()->GameTickSpeed() * 6 > Client()->GameTick(g_Config.m_ClDummy))
{
// lower the alpha slowly to blend text out
Alpha = ((float)(m_TimeCpLastReceivedTick + Client()->GameTickSpeed() * 6) - (float)Client()->GameTick(g_Config.m_ClDummy)) / (float)(Client()->GameTickSpeed() * 2);
}
if(m_TimeCpDiff > 0)
TextRender()->TextColor(1.0f, 0.5f, 0.5f, Alpha); // red
else if(m_TimeCpDiff < 0)
TextRender()->TextColor(0.5f, 1.0f, 0.5f, Alpha); // green
else if(!m_TimeCpDiff)
TextRender()->TextColor(1, 1, 1, Alpha); // white
CTextCursor Cursor;
TextRender()->SetCursor(&Cursor, 150 * Graphics()->ScreenAspect() - TextRender()->TextWidth(10, aBuf, -1, -1.0f) / 2, 20, 10, TEXTFLAG_RENDER);
Cursor.m_LineWidth = -1.0f;
TextRender()->RecreateTextContainer(m_DDRaceEffectsTextContainerIndex, &Cursor, aBuf);
if(m_DDRaceEffectsTextContainerIndex.Valid())
{
auto OutlineColor = TextRender()->DefaultTextOutlineColor();
OutlineColor.a *= Alpha;
TextRender()->RenderTextContainer(m_DDRaceEffectsTextContainerIndex, TextRender()->DefaultTextColor(), OutlineColor);
}
TextRender()->TextColor(TextRender()->DefaultTextColor());
}
}
}
void CHud::RenderRecord()
{
if(m_ServerRecord > 0.0f)
{
char aBuf[64];
str_format(aBuf, sizeof(aBuf), Localize("Server best:"));
TextRender()->Text(5, 75, 6, aBuf, -1.0f);
char aTime[32];
str_time_float(m_ServerRecord, TIME_HOURS_CENTISECS, aTime, sizeof(aTime));
str_format(aBuf, sizeof(aBuf), "%s%s", m_ServerRecord > 3600 ? "" : " ", aTime);
TextRender()->Text(53, 75, 6, aBuf, -1.0f);
}
const float PlayerRecord = m_aPlayerRecord[g_Config.m_ClDummy];
if(PlayerRecord > 0.0f)
{
char aBuf[64];
str_format(aBuf, sizeof(aBuf), Localize("Personal best:"));
TextRender()->Text(5, 82, 6, aBuf, -1.0f);
char aTime[32];
str_time_float(PlayerRecord, TIME_HOURS_CENTISECS, aTime, sizeof(aTime));
str_format(aBuf, sizeof(aBuf), "%s%s", PlayerRecord > 3600 ? "" : " ", aTime);
TextRender()->Text(53, 82, 6, aBuf, -1.0f);
}
}