1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "hud.h"
4
5#include "binds.h"
6#include "camera.h"
7#include "controls.h"
8#include "voting.h"
9
10#include <base/color.h>
11
12#include <engine/graphics.h>
13#include <engine/shared/config.h>
14#include <engine/textrender.h>
15
16#include <generated/client_data.h>
17#include <generated/protocol.h>
18
19#include <game/client/animstate.h>
20#include <game/client/components/scoreboard.h>
21#include <game/client/gameclient.h>
22#include <game/client/prediction/entities/character.h>
23#include <game/layers.h>
24#include <game/localization.h>
25
26#include <cmath>
27
28CHud::CHud()
29{
30 m_FPSTextContainerIndex.Reset();
31 m_DDRaceEffectsTextContainerIndex.Reset();
32 m_PlayerAngleTextContainerIndex.Reset();
33 m_PlayerPrevAngle = -INFINITY;
34
35 for(int i = 0; i < 2; i++)
36 {
37 m_aPlayerSpeedTextContainers[i].Reset();
38 m_aPlayerPrevSpeed[i] = -INFINITY;
39 m_aPlayerPositionContainers[i].Reset();
40 m_aPlayerPrevPosition[i] = -INFINITY;
41 }
42}
43
44void CHud::ResetHudContainers()
45{
46 for(auto &ScoreInfo : m_aScoreInfo)
47 {
48 TextRender()->DeleteTextContainer(TextContainerIndex&: ScoreInfo.m_OptionalNameTextContainerIndex);
49 TextRender()->DeleteTextContainer(TextContainerIndex&: ScoreInfo.m_TextRankContainerIndex);
50 TextRender()->DeleteTextContainer(TextContainerIndex&: ScoreInfo.m_TextScoreContainerIndex);
51 Graphics()->DeleteQuadContainer(ContainerIndex&: ScoreInfo.m_RoundRectQuadContainerIndex);
52
53 ScoreInfo.Reset();
54 }
55
56 TextRender()->DeleteTextContainer(TextContainerIndex&: m_FPSTextContainerIndex);
57 TextRender()->DeleteTextContainer(TextContainerIndex&: m_DDRaceEffectsTextContainerIndex);
58 TextRender()->DeleteTextContainer(TextContainerIndex&: m_PlayerAngleTextContainerIndex);
59 m_PlayerPrevAngle = -INFINITY;
60 for(int i = 0; i < 2; i++)
61 {
62 TextRender()->DeleteTextContainer(TextContainerIndex&: m_aPlayerSpeedTextContainers[i]);
63 m_aPlayerPrevSpeed[i] = -INFINITY;
64 TextRender()->DeleteTextContainer(TextContainerIndex&: m_aPlayerPositionContainers[i]);
65 m_aPlayerPrevPosition[i] = -INFINITY;
66 }
67}
68
69void CHud::OnWindowResize()
70{
71 ResetHudContainers();
72}
73
74void CHud::OnReset()
75{
76 m_TimeCpDiff = 0.0f;
77 m_DDRaceTime = 0;
78 m_FinishTimeLastReceivedTick = 0;
79 m_TimeCpLastReceivedTick = 0;
80 m_ShowFinishTime = false;
81 m_aPlayerRecord[0] = -1.0f;
82 m_aPlayerRecord[1] = -1.0f;
83 m_aPlayerSpeed[0] = 0;
84 m_aPlayerSpeed[1] = 0;
85 m_aLastPlayerSpeedChange[0] = ESpeedChange::NONE;
86 m_aLastPlayerSpeedChange[1] = ESpeedChange::NONE;
87 m_LastSpectatorCountTick = 0;
88
89 ResetHudContainers();
90}
91
92void CHud::OnInit()
93{
94 OnReset();
95
96 Graphics()->SetColor(r: 1.0, g: 1.0, b: 1.0, a: 1.0);
97
98 m_HudQuadContainerIndex = Graphics()->CreateQuadContainer(AutomaticUpload: false);
99 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
100 PrepareAmmoHealthAndArmorQuads();
101
102 // all cursors for the different weapons
103 for(int i = 0; i < NUM_WEAPONS; ++i)
104 {
105 float ScaleX, ScaleY;
106 Graphics()->GetSpriteScale(pSprite: g_pData->m_Weapons.m_aId[i].m_pSpriteCursor, ScaleX, ScaleY);
107 m_aCursorOffset[i] = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, Width: 64.f * ScaleX, Height: 64.f * ScaleY);
108 }
109
110 // the flags
111 m_FlagOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 8.f, Height: 16.f);
112
113 PreparePlayerStateQuads();
114
115 Graphics()->QuadContainerUpload(ContainerIndex: m_HudQuadContainerIndex);
116}
117
118void CHud::RenderGameTimer()
119{
120 float Half = m_Width / 2.0f;
121
122 if(!(GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_SUDDENDEATH))
123 {
124 char aBuf[32];
125 int Time = 0;
126 if(GameClient()->m_Snap.m_pGameInfoObj->m_TimeLimit && (GameClient()->m_Snap.m_pGameInfoObj->m_WarmupTimer <= 0))
127 {
128 Time = GameClient()->m_Snap.m_pGameInfoObj->m_TimeLimit * 60 - ((Client()->GameTick(Conn: g_Config.m_ClDummy) - GameClient()->m_Snap.m_pGameInfoObj->m_RoundStartTick) / Client()->GameTickSpeed());
129
130 if(GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER)
131 Time = 0;
132 }
133 else if(GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME)
134 {
135 // The Warmup timer is negative in this case to make sure that incompatible clients will not see a warmup timer
136 Time = (Client()->GameTick(Conn: g_Config.m_ClDummy) + GameClient()->m_Snap.m_pGameInfoObj->m_WarmupTimer) / Client()->GameTickSpeed();
137 }
138 else
139 Time = (Client()->GameTick(Conn: g_Config.m_ClDummy) - GameClient()->m_Snap.m_pGameInfoObj->m_RoundStartTick) / Client()->GameTickSpeed();
140
141 str_time(centisecs: (int64_t)Time * 100, format: TIME_DAYS, buffer: aBuf, buffer_size: sizeof(aBuf));
142 float FontSize = 10.0f;
143 static float s_TextWidthM = TextRender()->TextWidth(Size: FontSize, pText: "00:00", StrLength: -1, LineWidth: -1.0f);
144 static float s_TextWidthH = TextRender()->TextWidth(Size: FontSize, pText: "00:00:00", StrLength: -1, LineWidth: -1.0f);
145 static float s_TextWidth0D = TextRender()->TextWidth(Size: FontSize, pText: "0d 00:00:00", StrLength: -1, LineWidth: -1.0f);
146 static float s_TextWidth00D = TextRender()->TextWidth(Size: FontSize, pText: "00d 00:00:00", StrLength: -1, LineWidth: -1.0f);
147 static float s_TextWidth000D = TextRender()->TextWidth(Size: FontSize, pText: "000d 00:00:00", StrLength: -1, LineWidth: -1.0f);
148 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)));
149 // last 60 sec red, last 10 sec blink
150 if(GameClient()->m_Snap.m_pGameInfoObj->m_TimeLimit && Time <= 60 && (GameClient()->m_Snap.m_pGameInfoObj->m_WarmupTimer <= 0))
151 {
152 float Alpha = Time <= 10 && (2 * time() / time_freq()) % 2 ? 0.5f : 1.0f;
153 TextRender()->TextColor(r: 1.0f, g: 0.25f, b: 0.25f, a: Alpha);
154 }
155 TextRender()->Text(x: Half - w / 2, y: 2, Size: FontSize, pText: aBuf, LineWidth: -1.0f);
156 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
157 }
158}
159
160void CHud::RenderPauseNotification()
161{
162 if(GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED &&
163 !(GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
164 {
165 const char *pText = Localize(pStr: "Game paused");
166 float FontSize = 20.0f;
167 float w = TextRender()->TextWidth(Size: FontSize, pText, StrLength: -1, LineWidth: -1.0f);
168 TextRender()->Text(x: 150.0f * Graphics()->ScreenAspect() + -w / 2.0f, y: 50.0f, Size: FontSize, pText, LineWidth: -1.0f);
169 }
170}
171
172void CHud::RenderSuddenDeath()
173{
174 if(GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_SUDDENDEATH)
175 {
176 float Half = m_Width / 2.0f;
177 const char *pText = Localize(pStr: "Sudden Death");
178 float FontSize = 12.0f;
179 float w = TextRender()->TextWidth(Size: FontSize, pText, StrLength: -1, LineWidth: -1.0f);
180 TextRender()->Text(x: Half - w / 2, y: 2, Size: FontSize, pText, LineWidth: -1.0f);
181 }
182}
183
184void CHud::RenderScoreHud()
185{
186 // render small score hud
187 if(!(GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
188 {
189 float StartY = 229.0f; // the height of this display is 56, so EndY is 285
190
191 const float ScoreSingleBoxHeight = 18.0f;
192
193 bool ForceScoreInfoInit = !m_aScoreInfo[0].m_Initialized || !m_aScoreInfo[1].m_Initialized;
194 m_aScoreInfo[0].m_Initialized = m_aScoreInfo[1].m_Initialized = true;
195
196 if(GameClient()->IsTeamPlay() && GameClient()->m_Snap.m_pGameDataObj)
197 {
198 char aScoreTeam[2][16];
199 str_format(buffer: aScoreTeam[TEAM_RED], buffer_size: sizeof(aScoreTeam[TEAM_RED]), format: "%d", GameClient()->m_Snap.m_pGameDataObj->m_TeamscoreRed);
200 str_format(buffer: aScoreTeam[TEAM_BLUE], buffer_size: sizeof(aScoreTeam[TEAM_BLUE]), format: "%d", GameClient()->m_Snap.m_pGameDataObj->m_TeamscoreBlue);
201
202 bool aRecreateTeamScore[2] = {str_comp(a: aScoreTeam[0], b: m_aScoreInfo[0].m_aScoreText) != 0, str_comp(a: aScoreTeam[1], b: m_aScoreInfo[1].m_aScoreText) != 0};
203
204 const int aFlagCarrier[2] = {
205 GameClient()->m_Snap.m_pGameDataObj->m_FlagCarrierRed,
206 GameClient()->m_Snap.m_pGameDataObj->m_FlagCarrierBlue};
207
208 bool RecreateRect = ForceScoreInfoInit;
209 for(int t = 0; t < 2; t++)
210 {
211 if(aRecreateTeamScore[t])
212 {
213 m_aScoreInfo[t].m_ScoreTextWidth = TextRender()->TextWidth(Size: 14.0f, pText: aScoreTeam[t == 0 ? TEAM_RED : TEAM_BLUE], StrLength: -1, LineWidth: -1.0f);
214 str_copy(dst&: m_aScoreInfo[t].m_aScoreText, src: aScoreTeam[t == 0 ? TEAM_RED : TEAM_BLUE]);
215 RecreateRect = true;
216 }
217 }
218
219 static float s_TextWidth100 = TextRender()->TextWidth(Size: 14.0f, pText: "100", StrLength: -1, LineWidth: -1.0f);
220 float ScoreWidthMax = maximum(a: maximum(a: m_aScoreInfo[0].m_ScoreTextWidth, b: m_aScoreInfo[1].m_ScoreTextWidth), b: s_TextWidth100);
221 float Split = 3.0f;
222 float ImageSize = (GameClient()->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS) ? 16.0f : Split;
223 for(int t = 0; t < 2; t++)
224 {
225 // draw box
226 if(RecreateRect)
227 {
228 Graphics()->DeleteQuadContainer(ContainerIndex&: m_aScoreInfo[t].m_RoundRectQuadContainerIndex);
229
230 if(t == 0)
231 Graphics()->SetColor(r: 0.975f, g: 0.17f, b: 0.17f, a: 0.3f);
232 else
233 Graphics()->SetColor(r: 0.17f, g: 0.46f, b: 0.975f, a: 0.3f);
234 m_aScoreInfo[t].m_RoundRectQuadContainerIndex = Graphics()->CreateRectQuadContainer(x: m_Width - ScoreWidthMax - ImageSize - 2 * Split, y: StartY + t * 20, w: ScoreWidthMax + ImageSize + 2 * Split, h: ScoreSingleBoxHeight, r: 5.0f, Corners: IGraphics::CORNER_L);
235 }
236 Graphics()->TextureClear();
237 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
238 if(m_aScoreInfo[t].m_RoundRectQuadContainerIndex != -1)
239 Graphics()->RenderQuadContainer(ContainerIndex: m_aScoreInfo[t].m_RoundRectQuadContainerIndex, QuadDrawNum: -1);
240
241 // draw score
242 if(aRecreateTeamScore[t])
243 {
244 CTextCursor Cursor;
245 Cursor.SetPosition(vec2(m_Width - ScoreWidthMax + (ScoreWidthMax - m_aScoreInfo[t].m_ScoreTextWidth) / 2 - Split, StartY + t * 20 + (18.f - 14.f) / 2.f));
246 Cursor.m_FontSize = 14.0f;
247 TextRender()->RecreateTextContainer(TextContainerIndex&: m_aScoreInfo[t].m_TextScoreContainerIndex, pCursor: &Cursor, pText: aScoreTeam[t]);
248 }
249 if(m_aScoreInfo[t].m_TextScoreContainerIndex.Valid())
250 {
251 ColorRGBA TColor(1.f, 1.f, 1.f, 1.f);
252 ColorRGBA TOutlineColor(0.f, 0.f, 0.f, 0.3f);
253 TextRender()->RenderTextContainer(TextContainerIndex: m_aScoreInfo[t].m_TextScoreContainerIndex, TextColor: TColor, TextOutlineColor: TOutlineColor);
254 }
255
256 if(GameClient()->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS)
257 {
258 int BlinkTimer = (GameClient()->m_aFlagDropTick[t] != 0 &&
259 (Client()->GameTick(Conn: g_Config.m_ClDummy) - GameClient()->m_aFlagDropTick[t]) / Client()->GameTickSpeed() >= 25) ?
260 10 :
261 20;
262 if(aFlagCarrier[t] == FLAG_ATSTAND || (aFlagCarrier[t] == FLAG_TAKEN && ((Client()->GameTick(Conn: g_Config.m_ClDummy) / BlinkTimer) & 1)))
263 {
264 // draw flag
265 Graphics()->TextureSet(Texture: t == 0 ? GameClient()->m_GameSkin.m_SpriteFlagRed : GameClient()->m_GameSkin.m_SpriteFlagBlue);
266 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: 1.f);
267 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_FlagOffset, X: m_Width - ScoreWidthMax - ImageSize, Y: StartY + 1.0f + t * 20);
268 }
269 else if(aFlagCarrier[t] >= 0)
270 {
271 // draw name of the flag holder
272 int Id = aFlagCarrier[t] % MAX_CLIENTS;
273 const char *pName = GameClient()->m_aClients[Id].m_aName;
274 if(str_comp(a: pName, b: m_aScoreInfo[t].m_aPlayerNameText) != 0 || RecreateRect)
275 {
276 str_copy(dst&: m_aScoreInfo[t].m_aPlayerNameText, src: pName);
277
278 float w = TextRender()->TextWidth(Size: 8.0f, pText: pName, StrLength: -1, LineWidth: -1.0f);
279
280 CTextCursor Cursor;
281 Cursor.SetPosition(vec2(minimum(a: m_Width - w - 1.0f, b: m_Width - ScoreWidthMax - ImageSize - 2 * Split), StartY + (t + 1) * 20.0f - 2.0f));
282 Cursor.m_FontSize = 8.0f;
283 TextRender()->RecreateTextContainer(TextContainerIndex&: m_aScoreInfo[t].m_OptionalNameTextContainerIndex, pCursor: &Cursor, pText: pName);
284 }
285
286 if(m_aScoreInfo[t].m_OptionalNameTextContainerIndex.Valid())
287 {
288 ColorRGBA TColor(1.f, 1.f, 1.f, 1.f);
289 ColorRGBA TOutlineColor(0.f, 0.f, 0.f, 0.3f);
290 TextRender()->RenderTextContainer(TextContainerIndex: m_aScoreInfo[t].m_OptionalNameTextContainerIndex, TextColor: TColor, TextOutlineColor: TOutlineColor);
291 }
292
293 // draw tee of the flag holder
294 CTeeRenderInfo TeeInfo = GameClient()->m_aClients[Id].m_RenderInfo;
295 TeeInfo.m_Size = ScoreSingleBoxHeight;
296
297 const CAnimState *pIdleState = CAnimState::GetIdle();
298 vec2 OffsetToMid;
299 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
300 vec2 TeeRenderPos(m_Width - ScoreWidthMax - TeeInfo.m_Size / 2 - Split, StartY + (t * 20) + ScoreSingleBoxHeight / 2.0f + OffsetToMid.y);
301
302 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &TeeInfo, Emote: EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
303 }
304 }
305 StartY += 8.0f;
306 }
307 }
308 else
309 {
310 int Local = -1;
311 int aPos[2] = {1, 2};
312 const CNetObj_PlayerInfo *apPlayerInfo[2] = {nullptr, nullptr};
313 int i = 0;
314 for(int t = 0; t < 2 && i < MAX_CLIENTS && GameClient()->m_Snap.m_apInfoByScore[i]; ++i)
315 {
316 if(GameClient()->m_Snap.m_apInfoByScore[i]->m_Team != TEAM_SPECTATORS)
317 {
318 apPlayerInfo[t] = GameClient()->m_Snap.m_apInfoByScore[i];
319 if(apPlayerInfo[t]->m_ClientId == GameClient()->m_Snap.m_LocalClientId)
320 Local = t;
321 ++t;
322 }
323 }
324 // search local player info if not a spectator, nor within top2 scores
325 if(Local == -1 && GameClient()->m_Snap.m_pLocalInfo && GameClient()->m_Snap.m_pLocalInfo->m_Team != TEAM_SPECTATORS)
326 {
327 for(; i < MAX_CLIENTS && GameClient()->m_Snap.m_apInfoByScore[i]; ++i)
328 {
329 if(GameClient()->m_Snap.m_apInfoByScore[i]->m_Team != TEAM_SPECTATORS)
330 ++aPos[1];
331 if(GameClient()->m_Snap.m_apInfoByScore[i]->m_ClientId == GameClient()->m_Snap.m_LocalClientId)
332 {
333 apPlayerInfo[1] = GameClient()->m_Snap.m_apInfoByScore[i];
334 Local = 1;
335 break;
336 }
337 }
338 }
339 char aScore[2][16];
340 for(int t = 0; t < 2; ++t)
341 {
342 if(apPlayerInfo[t])
343 {
344 if(Client()->IsSixup() && GameClient()->m_Snap.m_pGameInfoObj->m_GameFlags & protocol7::GAMEFLAG_RACE)
345 str_time(centisecs: (int64_t)absolute(a: apPlayerInfo[t]->m_Score) / 10, format: TIME_MINS_CENTISECS, buffer: aScore[t], buffer_size: sizeof(aScore[t]));
346 else if(GameClient()->m_GameInfo.m_TimeScore)
347 {
348 CGameClient::CClientData &ClientData = GameClient()->m_aClients[apPlayerInfo[t]->m_ClientId];
349 if(GameClient()->m_ReceivedDDNetPlayerFinishTimes && ClientData.m_FinishTimeSeconds != FinishTime::NOT_FINISHED_MILLIS)
350 {
351 int64_t TimeSeconds = static_cast<int64_t>(absolute(a: ClientData.m_FinishTimeSeconds));
352 int64_t TimeMillis = TimeSeconds * 1000 + (absolute(a: ClientData.m_FinishTimeMillis) % 1000);
353
354 // show centiseconds if we are under an hour
355 str_time(centisecs: TimeMillis / 10, format: TimeSeconds < 60 * 60 ? TIME_MINS_CENTISECS : TIME_HOURS, buffer: aScore[t], buffer_size: sizeof(aScore[t]));
356 }
357 else if(apPlayerInfo[t]->m_Score != FinishTime::NOT_FINISHED_TIMESCORE)
358 {
359 str_time(centisecs: (int64_t)absolute(a: apPlayerInfo[t]->m_Score) * 100, format: TIME_HOURS, buffer: aScore[t], buffer_size: sizeof(aScore[t]));
360 }
361 else
362 aScore[t][0] = 0;
363 }
364 else
365 str_format(buffer: aScore[t], buffer_size: sizeof(aScore[t]), format: "%d", apPlayerInfo[t]->m_Score);
366 }
367 else
368 aScore[t][0] = 0;
369 }
370
371 bool RecreateScores = str_comp(a: aScore[0], b: m_aScoreInfo[0].m_aScoreText) != 0 || str_comp(a: aScore[1], b: m_aScoreInfo[1].m_aScoreText) != 0 || m_LastLocalClientId != GameClient()->m_Snap.m_LocalClientId;
372 m_LastLocalClientId = GameClient()->m_Snap.m_LocalClientId;
373
374 bool RecreateRect = ForceScoreInfoInit;
375 for(int t = 0; t < 2; t++)
376 {
377 if(RecreateScores)
378 {
379 m_aScoreInfo[t].m_ScoreTextWidth = TextRender()->TextWidth(Size: 14.0f, pText: aScore[t], StrLength: -1, LineWidth: -1.0f);
380 str_copy(dst&: m_aScoreInfo[t].m_aScoreText, src: aScore[t]);
381 RecreateRect = true;
382 }
383
384 if(apPlayerInfo[t])
385 {
386 int Id = apPlayerInfo[t]->m_ClientId;
387 if(Id >= 0 && Id < MAX_CLIENTS)
388 {
389 const char *pName = GameClient()->m_aClients[Id].m_aName;
390 if(str_comp(a: pName, b: m_aScoreInfo[t].m_aPlayerNameText) != 0)
391 RecreateRect = true;
392 }
393 }
394 else
395 {
396 if(m_aScoreInfo[t].m_aPlayerNameText[0] != 0)
397 RecreateRect = true;
398 }
399
400 char aBuf[16];
401 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d.", aPos[t]);
402 if(str_comp(a: aBuf, b: m_aScoreInfo[t].m_aRankText) != 0)
403 RecreateRect = true;
404 }
405
406 static float s_TextWidth10 = TextRender()->TextWidth(Size: 14.0f, pText: "10", StrLength: -1, LineWidth: -1.0f);
407 float ScoreWidthMax = maximum(a: maximum(a: m_aScoreInfo[0].m_ScoreTextWidth, b: m_aScoreInfo[1].m_ScoreTextWidth), b: s_TextWidth10);
408 float Split = 3.0f, ImageSize = 16.0f, PosSize = 16.0f;
409
410 for(int t = 0; t < 2; t++)
411 {
412 // draw box
413 if(RecreateRect)
414 {
415 Graphics()->DeleteQuadContainer(ContainerIndex&: m_aScoreInfo[t].m_RoundRectQuadContainerIndex);
416
417 if(t == Local)
418 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.25f);
419 else
420 Graphics()->SetColor(r: 0.0f, g: 0.0f, b: 0.0f, a: 0.25f);
421 m_aScoreInfo[t].m_RoundRectQuadContainerIndex = Graphics()->CreateRectQuadContainer(x: m_Width - ScoreWidthMax - ImageSize - 2 * Split - PosSize, y: StartY + t * 20, w: ScoreWidthMax + ImageSize + 2 * Split + PosSize, h: ScoreSingleBoxHeight, r: 5.0f, Corners: IGraphics::CORNER_L);
422 }
423 Graphics()->TextureClear();
424 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
425 if(m_aScoreInfo[t].m_RoundRectQuadContainerIndex != -1)
426 Graphics()->RenderQuadContainer(ContainerIndex: m_aScoreInfo[t].m_RoundRectQuadContainerIndex, QuadDrawNum: -1);
427
428 if(RecreateScores)
429 {
430 CTextCursor Cursor;
431 Cursor.SetPosition(vec2(m_Width - ScoreWidthMax + (ScoreWidthMax - m_aScoreInfo[t].m_ScoreTextWidth) - Split, StartY + t * 20 + (18.f - 14.f) / 2.f));
432 Cursor.m_FontSize = 14.0f;
433 TextRender()->RecreateTextContainer(TextContainerIndex&: m_aScoreInfo[t].m_TextScoreContainerIndex, pCursor: &Cursor, pText: aScore[t]);
434 }
435 // draw score
436 if(m_aScoreInfo[t].m_TextScoreContainerIndex.Valid())
437 {
438 ColorRGBA TColor(1.f, 1.f, 1.f, 1.f);
439 ColorRGBA TOutlineColor(0.f, 0.f, 0.f, 0.3f);
440 TextRender()->RenderTextContainer(TextContainerIndex: m_aScoreInfo[t].m_TextScoreContainerIndex, TextColor: TColor, TextOutlineColor: TOutlineColor);
441 }
442
443 if(apPlayerInfo[t])
444 {
445 // draw name
446 int Id = apPlayerInfo[t]->m_ClientId;
447 if(Id >= 0 && Id < MAX_CLIENTS)
448 {
449 const char *pName = GameClient()->m_aClients[Id].m_aName;
450 if(RecreateRect)
451 {
452 str_copy(dst&: m_aScoreInfo[t].m_aPlayerNameText, src: pName);
453
454 CTextCursor Cursor;
455 Cursor.SetPosition(vec2(minimum(a: m_Width - TextRender()->TextWidth(Size: 8.0f, pText: pName) - 1.0f, b: m_Width - ScoreWidthMax - ImageSize - 2 * Split - PosSize), StartY + (t + 1) * 20.0f - 2.0f));
456 Cursor.m_FontSize = 8.0f;
457 TextRender()->RecreateTextContainer(TextContainerIndex&: m_aScoreInfo[t].m_OptionalNameTextContainerIndex, pCursor: &Cursor, pText: pName);
458 }
459
460 if(m_aScoreInfo[t].m_OptionalNameTextContainerIndex.Valid())
461 {
462 ColorRGBA TColor(1.f, 1.f, 1.f, 1.f);
463 ColorRGBA TOutlineColor(0.f, 0.f, 0.f, 0.3f);
464 TextRender()->RenderTextContainer(TextContainerIndex: m_aScoreInfo[t].m_OptionalNameTextContainerIndex, TextColor: TColor, TextOutlineColor: TOutlineColor);
465 }
466
467 // draw tee
468 CTeeRenderInfo TeeInfo = GameClient()->m_aClients[Id].m_RenderInfo;
469 TeeInfo.m_Size = ScoreSingleBoxHeight;
470
471 const CAnimState *pIdleState = CAnimState::GetIdle();
472 vec2 OffsetToMid;
473 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
474 vec2 TeeRenderPos(m_Width - ScoreWidthMax - TeeInfo.m_Size / 2 - Split, StartY + (t * 20) + ScoreSingleBoxHeight / 2.0f + OffsetToMid.y);
475
476 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &TeeInfo, Emote: EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
477 }
478 }
479 else
480 {
481 m_aScoreInfo[t].m_aPlayerNameText[0] = 0;
482 }
483
484 // draw position
485 char aBuf[16];
486 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d.", aPos[t]);
487 if(RecreateRect)
488 {
489 str_copy(dst&: m_aScoreInfo[t].m_aRankText, src: aBuf);
490
491 CTextCursor Cursor;
492 Cursor.SetPosition(vec2(m_Width - ScoreWidthMax - ImageSize - Split - PosSize, StartY + t * 20 + (18.f - 10.f) / 2.f));
493 Cursor.m_FontSize = 10.0f;
494 TextRender()->RecreateTextContainer(TextContainerIndex&: m_aScoreInfo[t].m_TextRankContainerIndex, pCursor: &Cursor, pText: aBuf);
495 }
496 if(m_aScoreInfo[t].m_TextRankContainerIndex.Valid())
497 {
498 ColorRGBA TColor(1.f, 1.f, 1.f, 1.f);
499 ColorRGBA TOutlineColor(0.f, 0.f, 0.f, 0.3f);
500 TextRender()->RenderTextContainer(TextContainerIndex: m_aScoreInfo[t].m_TextRankContainerIndex, TextColor: TColor, TextOutlineColor: TOutlineColor);
501 }
502
503 StartY += 8.0f;
504 }
505 }
506 }
507}
508
509void CHud::RenderWarmupTimer()
510{
511 // render warmup timer
512 if(GameClient()->m_Snap.m_pGameInfoObj->m_WarmupTimer > 0 && !(GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME))
513 {
514 char aBuf[256];
515 float FontSize = 20.0f;
516 float w = TextRender()->TextWidth(Size: FontSize, pText: Localize(pStr: "Warmup"), StrLength: -1, LineWidth: -1.0f);
517 TextRender()->Text(x: 150 * Graphics()->ScreenAspect() + -w / 2, y: 50, Size: FontSize, pText: Localize(pStr: "Warmup"), LineWidth: -1.0f);
518
519 int Seconds = GameClient()->m_Snap.m_pGameInfoObj->m_WarmupTimer / Client()->GameTickSpeed();
520 if(Seconds < 5)
521 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d.%d", Seconds, (GameClient()->m_Snap.m_pGameInfoObj->m_WarmupTimer * 10 / Client()->GameTickSpeed()) % 10);
522 else
523 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", Seconds);
524 w = TextRender()->TextWidth(Size: FontSize, pText: aBuf, StrLength: -1, LineWidth: -1.0f);
525 TextRender()->Text(x: 150 * Graphics()->ScreenAspect() + -w / 2, y: 75, Size: FontSize, pText: aBuf, LineWidth: -1.0f);
526 }
527}
528
529void CHud::RenderTextInfo()
530{
531 int Showfps = g_Config.m_ClShowfps;
532#if defined(CONF_VIDEORECORDER)
533 if(IVideo::Current())
534 Showfps = 0;
535#endif
536 if(Showfps)
537 {
538 char aBuf[16];
539 const int FramesPerSecond = round_to_int(f: 1.0f / Client()->FrameTimeAverage());
540 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", FramesPerSecond);
541
542 static float s_TextWidth0 = TextRender()->TextWidth(Size: 12.f, pText: "0", StrLength: -1, LineWidth: -1.0f);
543 static float s_TextWidth00 = TextRender()->TextWidth(Size: 12.f, pText: "00", StrLength: -1, LineWidth: -1.0f);
544 static float s_TextWidth000 = TextRender()->TextWidth(Size: 12.f, pText: "000", StrLength: -1, LineWidth: -1.0f);
545 static float s_TextWidth0000 = TextRender()->TextWidth(Size: 12.f, pText: "0000", StrLength: -1, LineWidth: -1.0f);
546 static float s_TextWidth00000 = TextRender()->TextWidth(Size: 12.f, pText: "00000", StrLength: -1, LineWidth: -1.0f);
547 static const float s_aTextWidth[5] = {s_TextWidth0, s_TextWidth00, s_TextWidth000, s_TextWidth0000, s_TextWidth00000};
548
549 int DigitIndex = GetDigitsIndex(Value: FramesPerSecond, Max: 4);
550
551 CTextCursor Cursor;
552 Cursor.SetPosition(vec2(m_Width - 10 - s_aTextWidth[DigitIndex], 5));
553 Cursor.m_FontSize = 12.0f;
554 auto OldFlags = TextRender()->GetRenderFlags();
555 TextRender()->SetRenderFlags(OldFlags | TEXT_RENDER_FLAG_ONE_TIME_USE);
556 if(m_FPSTextContainerIndex.Valid())
557 TextRender()->RecreateTextContainerSoft(TextContainerIndex&: m_FPSTextContainerIndex, pCursor: &Cursor, pText: aBuf);
558 else
559 TextRender()->CreateTextContainer(TextContainerIndex&: m_FPSTextContainerIndex, pCursor: &Cursor, pText: "0");
560 TextRender()->SetRenderFlags(OldFlags);
561 if(m_FPSTextContainerIndex.Valid())
562 {
563 TextRender()->RenderTextContainer(TextContainerIndex: m_FPSTextContainerIndex, TextColor: TextRender()->DefaultTextColor(), TextOutlineColor: TextRender()->DefaultTextOutlineColor());
564 }
565 }
566 if(g_Config.m_ClShowpred && Client()->State() != IClient::STATE_DEMOPLAYBACK)
567 {
568 char aBuf[64];
569 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", Client()->GetPredictionTime());
570 TextRender()->Text(x: m_Width - 10 - TextRender()->TextWidth(Size: 12, pText: aBuf, StrLength: -1, LineWidth: -1.0f), y: Showfps ? 20 : 5, Size: 12, pText: aBuf, LineWidth: -1.0f);
571 }
572}
573
574void CHud::RenderConnectionWarning()
575{
576 if(Client()->ConnectionProblems())
577 {
578 const char *pText = Localize(pStr: "Connection Problems…");
579 float w = TextRender()->TextWidth(Size: 24, pText, StrLength: -1, LineWidth: -1.0f);
580 TextRender()->Text(x: 150 * Graphics()->ScreenAspect() - w / 2, y: 50, Size: 24, pText, LineWidth: -1.0f);
581 }
582}
583
584void CHud::RenderTeambalanceWarning()
585{
586 // render prompt about team-balance
587 bool Flash = time() / (time_freq() / 2) % 2 == 0;
588 if(GameClient()->IsTeamPlay())
589 {
590 int TeamDiff = GameClient()->m_Snap.m_aTeamSize[TEAM_RED] - GameClient()->m_Snap.m_aTeamSize[TEAM_BLUE];
591 if(g_Config.m_ClWarningTeambalance && (TeamDiff >= 2 || TeamDiff <= -2))
592 {
593 const char *pText = Localize(pStr: "Please balance teams!");
594 if(Flash)
595 TextRender()->TextColor(r: 1, g: 1, b: 0.5f, a: 1);
596 else
597 TextRender()->TextColor(r: 0.7f, g: 0.7f, b: 0.2f, a: 1.0f);
598 TextRender()->Text(x: 5, y: 50, Size: 6, pText, LineWidth: -1.0f);
599 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
600 }
601 }
602}
603
604void CHud::RenderCursor()
605{
606 int CurWeapon = 0;
607 vec2 TargetPos;
608 float Alpha = 1.0f;
609
610 const vec2 Center = GameClient()->m_Camera.m_Center;
611 float aPoints[4];
612 Graphics()->MapScreenToWorld(CenterX: Center.x, CenterY: Center.y, ParallaxX: 100.0f, ParallaxY: 100.0f, ParallaxZoom: 100.0f, OffsetX: 0, OffsetY: 0, Aspect: Graphics()->ScreenAspect(), Zoom: 1.0f, pPoints: aPoints);
613 Graphics()->MapScreen(TopLeftX: aPoints[0], TopLeftY: aPoints[1], BottomRightX: aPoints[2], BottomRightY: aPoints[3]);
614
615 if(Client()->State() != IClient::STATE_DEMOPLAYBACK && GameClient()->m_Snap.m_pLocalCharacter)
616 {
617 // Render local cursor
618 CurWeapon = maximum(a: 0, b: GameClient()->m_Snap.m_pLocalCharacter->m_Weapon % NUM_WEAPONS);
619 TargetPos = GameClient()->m_Controls.m_aTargetPos[g_Config.m_ClDummy];
620 }
621 else
622 {
623 // Render spec cursor
624 if(!g_Config.m_ClSpecCursor || !GameClient()->m_CursorInfo.IsAvailable())
625 return;
626
627 bool RenderSpecCursor = (GameClient()->m_Snap.m_SpecInfo.m_Active && GameClient()->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW) || Client()->State() == IClient::STATE_DEMOPLAYBACK;
628
629 if(!RenderSpecCursor)
630 return;
631
632 // Calculate factor to keep cursor on screen
633 const vec2 HalfSize = vec2(Center.x - aPoints[0], Center.y - aPoints[1]);
634 const vec2 ScreenPos = (GameClient()->m_CursorInfo.WorldTarget() - Center) / GameClient()->m_Camera.m_Zoom;
635 const float ClampFactor = maximum(
636 a: 1.0f,
637 b: absolute(a: ScreenPos.x / HalfSize.x),
638 c: absolute(a: ScreenPos.y / HalfSize.y));
639
640 CurWeapon = maximum(a: 0, b: GameClient()->m_CursorInfo.Weapon() % NUM_WEAPONS);
641 TargetPos = ScreenPos / ClampFactor + Center;
642 if(ClampFactor != 1.0f)
643 Alpha /= 2.0f;
644 }
645
646 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: Alpha);
647 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_aSpriteWeaponCursors[CurWeapon]);
648 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_aCursorOffset[CurWeapon], X: TargetPos.x, Y: TargetPos.y);
649}
650
651void CHud::PrepareAmmoHealthAndArmorQuads()
652{
653 float x = 5;
654 float y = 5;
655 IGraphics::CQuadItem Array[10];
656
657 // ammo of the different weapons
658 for(int i = 0; i < NUM_WEAPONS; ++i)
659 {
660 // 0.6
661 for(int n = 0; n < 10; n++)
662 Array[n] = IGraphics::CQuadItem(x + n * 12, y, 10, 10);
663
664 m_aAmmoOffset[i] = Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
665
666 // 0.7
667 if(i == WEAPON_GRENADE)
668 {
669 // special case for 0.7 grenade
670 for(int n = 0; n < 10; n++)
671 Array[n] = IGraphics::CQuadItem(1 + x + n * 12, y, 10, 10);
672 }
673 else
674 {
675 for(int n = 0; n < 10; n++)
676 Array[n] = IGraphics::CQuadItem(x + n * 12, y, 12, 12);
677 }
678
679 Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
680 }
681
682 // health
683 for(int i = 0; i < 10; ++i)
684 Array[i] = IGraphics::CQuadItem(x + i * 12, y, 10, 10);
685 m_HealthOffset = Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
686
687 // 0.7
688 for(int i = 0; i < 10; ++i)
689 Array[i] = IGraphics::CQuadItem(x + i * 12, y, 12, 12);
690 Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
691
692 // empty health
693 for(int i = 0; i < 10; ++i)
694 Array[i] = IGraphics::CQuadItem(x + i * 12, y, 10, 10);
695 m_EmptyHealthOffset = Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
696
697 // 0.7
698 for(int i = 0; i < 10; ++i)
699 Array[i] = IGraphics::CQuadItem(x + i * 12, y, 12, 12);
700 Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
701
702 // armor meter
703 for(int i = 0; i < 10; ++i)
704 Array[i] = IGraphics::CQuadItem(x + i * 12, y + 12, 10, 10);
705 m_ArmorOffset = Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
706
707 // 0.7
708 for(int i = 0; i < 10; ++i)
709 Array[i] = IGraphics::CQuadItem(x + i * 12, y + 12, 12, 12);
710 Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
711
712 // empty armor meter
713 for(int i = 0; i < 10; ++i)
714 Array[i] = IGraphics::CQuadItem(x + i * 12, y + 12, 10, 10);
715 m_EmptyArmorOffset = Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
716
717 // 0.7
718 for(int i = 0; i < 10; ++i)
719 Array[i] = IGraphics::CQuadItem(x + i * 12, y + 12, 12, 12);
720 Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
721}
722
723void CHud::RenderAmmoHealthAndArmor(const CNetObj_Character *pCharacter)
724{
725 if(!pCharacter)
726 return;
727
728 bool IsSixupGameSkin = GameClient()->m_GameSkin.IsSixup();
729 int QuadOffsetSixup = (IsSixupGameSkin ? 10 : 0);
730
731 if(GameClient()->m_GameInfo.m_HudAmmo)
732 {
733 // ammo display
734 float AmmoOffsetY = GameClient()->m_GameInfo.m_HudHealthArmor ? 24 : 0;
735 int CurWeapon = pCharacter->m_Weapon % NUM_WEAPONS;
736 // 0.7 only
737 if(CurWeapon == WEAPON_NINJA)
738 {
739 if(!GameClient()->m_GameInfo.m_HudDDRace && Client()->IsSixup())
740 {
741 const int Max = g_pData->m_Weapons.m_Ninja.m_Duration * Client()->GameTickSpeed() / 1000;
742 float NinjaProgress = std::clamp(val: pCharacter->m_AmmoCount - Client()->GameTick(Conn: g_Config.m_ClDummy), lo: 0, hi: Max) / (float)Max;
743 RenderNinjaBarPos(x: 5 + 10 * 12, y: 5, Width: 6.f, Height: 24.f, Progress: NinjaProgress);
744 }
745 }
746 else if(CurWeapon >= 0 && GameClient()->m_GameSkin.m_aSpriteWeaponProjectiles[CurWeapon].IsValid())
747 {
748 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_aSpriteWeaponProjectiles[CurWeapon]);
749 if(AmmoOffsetY > 0)
750 {
751 Graphics()->RenderQuadContainerEx(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_aAmmoOffset[CurWeapon] + QuadOffsetSixup, QuadDrawNum: std::clamp(val: pCharacter->m_AmmoCount, lo: 0, hi: 10), X: 0, Y: AmmoOffsetY);
752 }
753 else
754 {
755 Graphics()->RenderQuadContainer(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_aAmmoOffset[CurWeapon] + QuadOffsetSixup, QuadDrawNum: std::clamp(val: pCharacter->m_AmmoCount, lo: 0, hi: 10));
756 }
757 }
758 }
759
760 if(GameClient()->m_GameInfo.m_HudHealthArmor)
761 {
762 // health display
763 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpriteHealthFull);
764 Graphics()->RenderQuadContainer(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_HealthOffset + QuadOffsetSixup, QuadDrawNum: minimum(a: pCharacter->m_Health, b: 10));
765 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpriteHealthEmpty);
766 Graphics()->RenderQuadContainer(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_EmptyHealthOffset + QuadOffsetSixup + minimum(a: pCharacter->m_Health, b: 10), QuadDrawNum: 10 - minimum(a: pCharacter->m_Health, b: 10));
767
768 // armor display
769 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpriteArmorFull);
770 Graphics()->RenderQuadContainer(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_ArmorOffset + QuadOffsetSixup, QuadDrawNum: minimum(a: pCharacter->m_Armor, b: 10));
771 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpriteArmorEmpty);
772 Graphics()->RenderQuadContainer(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_ArmorOffset + QuadOffsetSixup + minimum(a: pCharacter->m_Armor, b: 10), QuadDrawNum: 10 - minimum(a: pCharacter->m_Armor, b: 10));
773 }
774}
775
776void CHud::PreparePlayerStateQuads()
777{
778 float x = 5;
779 float y = 5 + 24;
780 IGraphics::CQuadItem Array[10];
781
782 // Quads for displaying the available and used jumps
783 for(int i = 0; i < 10; ++i)
784 Array[i] = IGraphics::CQuadItem(x + i * 12, y, 12, 12);
785 m_AirjumpOffset = Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
786
787 for(int i = 0; i < 10; ++i)
788 Array[i] = IGraphics::CQuadItem(x + i * 12, y, 12, 12);
789 m_AirjumpEmptyOffset = Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
790
791 // Quads for displaying weapons
792 for(int Weapon = 0; Weapon < NUM_WEAPONS; ++Weapon)
793 {
794 const CDataWeaponspec &WeaponSpec = g_pData->m_Weapons.m_aId[Weapon];
795 float ScaleX, ScaleY;
796 Graphics()->GetSpriteScale(pSprite: WeaponSpec.m_pSpriteBody, ScaleX, ScaleY);
797 constexpr float HudWeaponScale = 0.25f;
798 float Width = WeaponSpec.m_VisualSize * ScaleX * HudWeaponScale;
799 float Height = WeaponSpec.m_VisualSize * ScaleY * HudWeaponScale;
800 m_aWeaponOffset[Weapon] = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, Width, Height);
801 }
802
803 // Quads for displaying capabilities
804 m_EndlessJumpOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
805 m_EndlessHookOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
806 m_JetpackOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
807 m_TeleportGrenadeOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
808 m_TeleportGunOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
809 m_TeleportLaserOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
810
811 // Quads for displaying prohibited capabilities
812 m_SoloOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
813 m_CollisionDisabledOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
814 m_HookHitDisabledOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
815 m_HammerHitDisabledOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
816 m_GunHitDisabledOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
817 m_ShotgunHitDisabledOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
818 m_GrenadeHitDisabledOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
819 m_LaserHitDisabledOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
820
821 // Quads for displaying freeze status
822 m_DeepFrozenOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
823 m_LiveFrozenOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
824
825 // Quads for displaying dummy actions
826 m_DummyHammerOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
827 m_DummyCopyOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
828
829 // Quads for displaying team modes
830 m_PracticeModeOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
831 m_LockModeOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
832 m_Team0ModeOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
833}
834
835void CHud::RenderPlayerState(const int ClientId)
836{
837 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: 1.f);
838
839 // pCharacter contains the predicted character for local players or the last snap for players who are spectated
840 CCharacterCore *pCharacter = &GameClient()->m_aClients[ClientId].m_Predicted;
841 CNetObj_Character *pPlayer = &GameClient()->m_aClients[ClientId].m_RenderCur;
842 int TotalJumpsToDisplay = 0;
843 if(g_Config.m_ClShowhudJumpsIndicator)
844 {
845 int AvailableJumpsToDisplay;
846 if(GameClient()->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo)
847 {
848 bool Grounded = false;
849 if(Collision()->CheckPoint(x: pPlayer->m_X + CCharacterCore::PhysicalSize() / 2,
850 y: pPlayer->m_Y + CCharacterCore::PhysicalSize() / 2 + 5))
851 {
852 Grounded = true;
853 }
854 if(Collision()->CheckPoint(x: pPlayer->m_X - CCharacterCore::PhysicalSize() / 2,
855 y: pPlayer->m_Y + CCharacterCore::PhysicalSize() / 2 + 5))
856 {
857 Grounded = true;
858 }
859
860 int UsedJumps = pCharacter->m_JumpedTotal;
861 if(pCharacter->m_Jumps > 1)
862 {
863 UsedJumps += !Grounded;
864 }
865 else if(pCharacter->m_Jumps == 1)
866 {
867 // If the player has only one jump, each jump is the last one
868 UsedJumps = pPlayer->m_Jumped & 2;
869 }
870 else if(pCharacter->m_Jumps == -1)
871 {
872 // The player has only one ground jump
873 UsedJumps = !Grounded;
874 }
875
876 if(pCharacter->m_EndlessJump && UsedJumps >= absolute(a: pCharacter->m_Jumps))
877 {
878 UsedJumps = absolute(a: pCharacter->m_Jumps) - 1;
879 }
880
881 int UnusedJumps = absolute(a: pCharacter->m_Jumps) - UsedJumps;
882 if(!(pPlayer->m_Jumped & 2) && UnusedJumps <= 0)
883 {
884 // In some edge cases when the player just got another number of jumps, UnusedJumps is not correct
885 UnusedJumps = 1;
886 }
887 TotalJumpsToDisplay = maximum(a: minimum(a: absolute(a: pCharacter->m_Jumps), b: 10), b: 0);
888 AvailableJumpsToDisplay = maximum(a: minimum(a: UnusedJumps, b: TotalJumpsToDisplay), b: 0);
889 }
890 else
891 {
892 TotalJumpsToDisplay = AvailableJumpsToDisplay = absolute(a: GameClient()->m_Snap.m_aCharacters[ClientId].m_ExtendedData.m_Jumps);
893 }
894
895 // render available and used jumps
896 int JumpsOffsetY = ((GameClient()->m_GameInfo.m_HudHealthArmor && g_Config.m_ClShowhudHealthAmmo ? 24 : 0) +
897 (GameClient()->m_GameInfo.m_HudAmmo && g_Config.m_ClShowhudHealthAmmo ? 12 : 0));
898 if(JumpsOffsetY > 0)
899 {
900 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudAirjump);
901 Graphics()->RenderQuadContainerEx(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_AirjumpOffset, QuadDrawNum: AvailableJumpsToDisplay, X: 0, Y: JumpsOffsetY);
902 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudAirjumpEmpty);
903 Graphics()->RenderQuadContainerEx(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_AirjumpEmptyOffset + AvailableJumpsToDisplay, QuadDrawNum: TotalJumpsToDisplay - AvailableJumpsToDisplay, X: 0, Y: JumpsOffsetY);
904 }
905 else
906 {
907 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudAirjump);
908 Graphics()->RenderQuadContainer(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_AirjumpOffset, QuadDrawNum: AvailableJumpsToDisplay);
909 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudAirjumpEmpty);
910 Graphics()->RenderQuadContainer(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_AirjumpEmptyOffset + AvailableJumpsToDisplay, QuadDrawNum: TotalJumpsToDisplay - AvailableJumpsToDisplay);
911 }
912 }
913
914 float x = 5 + 12;
915 float y = (5 + 12 + (GameClient()->m_GameInfo.m_HudHealthArmor && g_Config.m_ClShowhudHealthAmmo ? 24 : 0) +
916 (GameClient()->m_GameInfo.m_HudAmmo && g_Config.m_ClShowhudHealthAmmo ? 12 : 0));
917
918 // render weapons
919 {
920 constexpr float aWeaponWidth[NUM_WEAPONS] = {16, 12, 12, 12, 12, 12};
921 constexpr float aWeaponInitialOffset[NUM_WEAPONS] = {-3, -4, -1, -1, -2, -4};
922 bool InitialOffsetAdded = false;
923 for(int Weapon = 0; Weapon < NUM_WEAPONS; ++Weapon)
924 {
925 if(!pCharacter->m_aWeapons[Weapon].m_Got)
926 continue;
927 if(!InitialOffsetAdded)
928 {
929 x += aWeaponInitialOffset[Weapon];
930 InitialOffsetAdded = true;
931 }
932 if(pPlayer->m_Weapon != Weapon)
933 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
934 Graphics()->QuadsSetRotation(Angle: pi * 7 / 4);
935 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_aSpritePickupWeapons[Weapon]);
936 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_aWeaponOffset[Weapon], X: x, Y: y);
937 Graphics()->QuadsSetRotation(Angle: 0);
938 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
939 x += aWeaponWidth[Weapon];
940 }
941 if(pCharacter->m_aWeapons[WEAPON_NINJA].m_Got)
942 {
943 const int Max = g_pData->m_Weapons.m_Ninja.m_Duration * Client()->GameTickSpeed() / 1000;
944 float NinjaProgress = std::clamp(val: pCharacter->m_Ninja.m_ActivationTick + g_pData->m_Weapons.m_Ninja.m_Duration * Client()->GameTickSpeed() / 1000 - Client()->GameTick(Conn: g_Config.m_ClDummy), lo: 0, hi: Max) / (float)Max;
945 if(NinjaProgress > 0.0f && GameClient()->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo)
946 {
947 RenderNinjaBarPos(x, y: y - 12, Width: 6.f, Height: 24.f, Progress: NinjaProgress);
948 }
949 }
950 }
951
952 // render capabilities
953 x = 5;
954 y += 12;
955 if(TotalJumpsToDisplay > 0)
956 {
957 y += 12;
958 }
959 bool HasCapabilities = false;
960 if(pCharacter->m_EndlessJump)
961 {
962 HasCapabilities = true;
963 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudEndlessJump);
964 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_EndlessJumpOffset, X: x, Y: y);
965 x += 12;
966 }
967 if(pCharacter->m_EndlessHook)
968 {
969 HasCapabilities = true;
970 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudEndlessHook);
971 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_EndlessHookOffset, X: x, Y: y);
972 x += 12;
973 }
974 if(pCharacter->m_Jetpack)
975 {
976 HasCapabilities = true;
977 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudJetpack);
978 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_JetpackOffset, X: x, Y: y);
979 x += 12;
980 }
981 if(pCharacter->m_HasTelegunGun && pCharacter->m_aWeapons[WEAPON_GUN].m_Got)
982 {
983 HasCapabilities = true;
984 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudTeleportGun);
985 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_TeleportGunOffset, X: x, Y: y);
986 x += 12;
987 }
988 if(pCharacter->m_HasTelegunGrenade && pCharacter->m_aWeapons[WEAPON_GRENADE].m_Got)
989 {
990 HasCapabilities = true;
991 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudTeleportGrenade);
992 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_TeleportGrenadeOffset, X: x, Y: y);
993 x += 12;
994 }
995 if(pCharacter->m_HasTelegunLaser && pCharacter->m_aWeapons[WEAPON_LASER].m_Got)
996 {
997 HasCapabilities = true;
998 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudTeleportLaser);
999 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_TeleportLaserOffset, X: x, Y: y);
1000 }
1001
1002 // render prohibited capabilities
1003 x = 5;
1004 if(HasCapabilities)
1005 {
1006 y += 12;
1007 }
1008 bool HasProhibitedCapabilities = false;
1009 if(pCharacter->m_Solo)
1010 {
1011 HasProhibitedCapabilities = true;
1012 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudSolo);
1013 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_SoloOffset, X: x, Y: y);
1014 x += 12;
1015 }
1016 if(pCharacter->m_CollisionDisabled)
1017 {
1018 HasProhibitedCapabilities = true;
1019 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudCollisionDisabled);
1020 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_CollisionDisabledOffset, X: x, Y: y);
1021 x += 12;
1022 }
1023 if(pCharacter->m_HookHitDisabled)
1024 {
1025 HasProhibitedCapabilities = true;
1026 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudHookHitDisabled);
1027 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_HookHitDisabledOffset, X: x, Y: y);
1028 x += 12;
1029 }
1030 if(pCharacter->m_HammerHitDisabled)
1031 {
1032 HasProhibitedCapabilities = true;
1033 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudHammerHitDisabled);
1034 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_HammerHitDisabledOffset, X: x, Y: y);
1035 x += 12;
1036 }
1037 if((pCharacter->m_GrenadeHitDisabled && pCharacter->m_HasTelegunGun && pCharacter->m_aWeapons[WEAPON_GUN].m_Got))
1038 {
1039 HasProhibitedCapabilities = true;
1040 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudGunHitDisabled);
1041 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_LaserHitDisabledOffset, X: x, Y: y);
1042 x += 12;
1043 }
1044 if((pCharacter->m_ShotgunHitDisabled && pCharacter->m_aWeapons[WEAPON_SHOTGUN].m_Got))
1045 {
1046 HasProhibitedCapabilities = true;
1047 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudShotgunHitDisabled);
1048 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_ShotgunHitDisabledOffset, X: x, Y: y);
1049 x += 12;
1050 }
1051 if((pCharacter->m_GrenadeHitDisabled && pCharacter->m_aWeapons[WEAPON_GRENADE].m_Got))
1052 {
1053 HasProhibitedCapabilities = true;
1054 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudGrenadeHitDisabled);
1055 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_GrenadeHitDisabledOffset, X: x, Y: y);
1056 x += 12;
1057 }
1058 if((pCharacter->m_LaserHitDisabled && pCharacter->m_aWeapons[WEAPON_LASER].m_Got))
1059 {
1060 HasProhibitedCapabilities = true;
1061 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudLaserHitDisabled);
1062 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_LaserHitDisabledOffset, X: x, Y: y);
1063 }
1064
1065 // render dummy actions and freeze state
1066 x = 5;
1067 if(HasProhibitedCapabilities)
1068 {
1069 y += 12;
1070 }
1071 if(GameClient()->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo && GameClient()->m_Snap.m_aCharacters[ClientId].m_ExtendedData.m_Flags & CHARACTERFLAG_LOCK_MODE)
1072 {
1073 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudLockMode);
1074 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_LockModeOffset, X: x, Y: y);
1075 x += 12;
1076 }
1077 if(GameClient()->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo && GameClient()->m_Snap.m_aCharacters[ClientId].m_ExtendedData.m_Flags & CHARACTERFLAG_PRACTICE_MODE)
1078 {
1079 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudPracticeMode);
1080 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_PracticeModeOffset, X: x, Y: y);
1081 x += 12;
1082 }
1083 if(GameClient()->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo && GameClient()->m_Snap.m_aCharacters[ClientId].m_ExtendedData.m_Flags & CHARACTERFLAG_TEAM0_MODE)
1084 {
1085 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudTeam0Mode);
1086 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_Team0ModeOffset, X: x, Y: y);
1087 x += 12;
1088 }
1089 if(pCharacter->m_DeepFrozen)
1090 {
1091 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudDeepFrozen);
1092 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_DeepFrozenOffset, X: x, Y: y);
1093 x += 12;
1094 }
1095 if(pCharacter->m_LiveFrozen)
1096 {
1097 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudLiveFrozen);
1098 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_LiveFrozenOffset, X: x, Y: y);
1099 }
1100}
1101
1102void CHud::RenderNinjaBarPos(const float x, float y, const float Width, const float Height, float Progress, const float Alpha)
1103{
1104 Progress = std::clamp(val: Progress, lo: 0.0f, hi: 1.0f);
1105
1106 // what percentage of the end pieces is used for the progress indicator and how much is the rest
1107 // half of the ends are used for the progress display
1108 const float RestPct = 0.5f;
1109 const float ProgPct = 0.5f;
1110
1111 const float EndHeight = Width; // to keep the correct scale - the width of the sprite is as long as the height
1112 const float BarWidth = Width;
1113 const float WholeBarHeight = Height;
1114 const float MiddleBarHeight = WholeBarHeight - (EndHeight * 2.0f);
1115 const float EndProgressHeight = EndHeight * ProgPct;
1116 const float EndRestHeight = EndHeight * RestPct;
1117 const float ProgressBarHeight = WholeBarHeight - (EndProgressHeight * 2.0f);
1118 const float EndProgressProportion = EndProgressHeight / ProgressBarHeight;
1119 const float MiddleProgressProportion = MiddleBarHeight / ProgressBarHeight;
1120
1121 // beginning piece
1122 float BeginningPieceProgress = 1;
1123 if(Progress <= 1)
1124 {
1125 if(Progress <= (EndProgressProportion + MiddleProgressProportion))
1126 {
1127 BeginningPieceProgress = 0;
1128 }
1129 else
1130 {
1131 BeginningPieceProgress = (Progress - EndProgressProportion - MiddleProgressProportion) / EndProgressProportion;
1132 }
1133 }
1134 // empty
1135 Graphics()->WrapClamp();
1136 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudNinjaBarEmptyRight);
1137 Graphics()->QuadsBegin();
1138 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: Alpha);
1139 // Subset: btm_r, top_r, top_m, btm_m | it is mirrored on the horizontal axe and rotated 90 degrees counterclockwise
1140 Graphics()->QuadsSetSubsetFree(x0: 1, y0: 1, x1: 1, y1: 0, x2: ProgPct - ProgPct * (1.0f - BeginningPieceProgress), y2: 0, x3: ProgPct - ProgPct * (1.0f - BeginningPieceProgress), y3: 1);
1141 IGraphics::CQuadItem QuadEmptyBeginning(x, y, BarWidth, EndRestHeight + EndProgressHeight * (1.0f - BeginningPieceProgress));
1142 Graphics()->QuadsDrawTL(pArray: &QuadEmptyBeginning, Num: 1);
1143 Graphics()->QuadsEnd();
1144 // full
1145 if(BeginningPieceProgress > 0.0f)
1146 {
1147 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudNinjaBarFullLeft);
1148 Graphics()->QuadsBegin();
1149 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: Alpha);
1150 // Subset: btm_m, top_m, top_r, btm_r | it is rotated 90 degrees clockwise
1151 Graphics()->QuadsSetSubsetFree(x0: RestPct + ProgPct * (1.0f - BeginningPieceProgress), y0: 1, x1: RestPct + ProgPct * (1.0f - BeginningPieceProgress), y1: 0, x2: 1, y2: 0, x3: 1, y3: 1);
1152 IGraphics::CQuadItem QuadFullBeginning(x, y + (EndRestHeight + EndProgressHeight * (1.0f - BeginningPieceProgress)), BarWidth, EndProgressHeight * BeginningPieceProgress);
1153 Graphics()->QuadsDrawTL(pArray: &QuadFullBeginning, Num: 1);
1154 Graphics()->QuadsEnd();
1155 }
1156
1157 // middle piece
1158 y += EndHeight;
1159
1160 float MiddlePieceProgress = 1;
1161 if(Progress <= EndProgressProportion + MiddleProgressProportion)
1162 {
1163 if(Progress <= EndProgressProportion)
1164 {
1165 MiddlePieceProgress = 0;
1166 }
1167 else
1168 {
1169 MiddlePieceProgress = (Progress - EndProgressProportion) / MiddleProgressProportion;
1170 }
1171 }
1172
1173 const float FullMiddleBarHeight = MiddleBarHeight * MiddlePieceProgress;
1174 const float EmptyMiddleBarHeight = MiddleBarHeight - FullMiddleBarHeight;
1175
1176 // empty ninja bar
1177 if(EmptyMiddleBarHeight > 0.0f)
1178 {
1179 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudNinjaBarEmpty);
1180 Graphics()->QuadsBegin();
1181 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: Alpha);
1182 // select the middle portion of the sprite so we don't get edge bleeding
1183 if(EmptyMiddleBarHeight <= EndHeight)
1184 {
1185 // prevent pixel puree, select only a small slice
1186 // Subset: btm_r, top_r, top_m, btm_m | it is mirrored on the horizontal axe and rotated 90 degrees counterclockwise
1187 Graphics()->QuadsSetSubsetFree(x0: 1, y0: 1, x1: 1, y1: 0, x2: 1.0f - (EmptyMiddleBarHeight / EndHeight), y2: 0, x3: 1.0f - (EmptyMiddleBarHeight / EndHeight), y3: 1);
1188 }
1189 else
1190 {
1191 // Subset: btm_r, top_r, top_l, btm_l | it is mirrored on the horizontal axe and rotated 90 degrees counterclockwise
1192 Graphics()->QuadsSetSubsetFree(x0: 1, y0: 1, x1: 1, y1: 0, x2: 0, y2: 0, x3: 0, y3: 1);
1193 }
1194 IGraphics::CQuadItem QuadEmpty(x, y, BarWidth, EmptyMiddleBarHeight);
1195 Graphics()->QuadsDrawTL(pArray: &QuadEmpty, Num: 1);
1196 Graphics()->QuadsEnd();
1197 }
1198
1199 // full ninja bar
1200 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudNinjaBarFull);
1201 Graphics()->QuadsBegin();
1202 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: Alpha);
1203 // select the middle portion of the sprite so we don't get edge bleeding
1204 if(FullMiddleBarHeight <= EndHeight)
1205 {
1206 // prevent pixel puree, select only a small slice
1207 // Subset: btm_m, top_m, top_r, btm_r | it is rotated 90 degrees clockwise
1208 Graphics()->QuadsSetSubsetFree(x0: 1.0f - (FullMiddleBarHeight / EndHeight), y0: 1, x1: 1.0f - (FullMiddleBarHeight / EndHeight), y1: 0, x2: 1, y2: 0, x3: 1, y3: 1);
1209 }
1210 else
1211 {
1212 // Subset: btm_l, top_l, top_r, btm_r | it is rotated 90 degrees clockwise
1213 Graphics()->QuadsSetSubsetFree(x0: 0, y0: 1, x1: 0, y1: 0, x2: 1, y2: 0, x3: 1, y3: 1);
1214 }
1215 IGraphics::CQuadItem QuadFull(x, y + EmptyMiddleBarHeight, BarWidth, FullMiddleBarHeight);
1216 Graphics()->QuadsDrawTL(pArray: &QuadFull, Num: 1);
1217 Graphics()->QuadsEnd();
1218
1219 // ending piece
1220 y += MiddleBarHeight;
1221 float EndingPieceProgress = 1;
1222 if(Progress <= EndProgressProportion)
1223 {
1224 EndingPieceProgress = Progress / EndProgressProportion;
1225 }
1226 // empty
1227 if(EndingPieceProgress < 1.0f)
1228 {
1229 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudNinjaBarEmptyRight);
1230 Graphics()->QuadsBegin();
1231 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: Alpha);
1232 // Subset: btm_l, top_l, top_m, btm_m | it is rotated 90 degrees clockwise
1233 Graphics()->QuadsSetSubsetFree(x0: 0, y0: 1, x1: 0, y1: 0, x2: ProgPct - ProgPct * EndingPieceProgress, y2: 0, x3: ProgPct - ProgPct * EndingPieceProgress, y3: 1);
1234 IGraphics::CQuadItem QuadEmptyEnding(x, y, BarWidth, EndProgressHeight * (1.0f - EndingPieceProgress));
1235 Graphics()->QuadsDrawTL(pArray: &QuadEmptyEnding, Num: 1);
1236 Graphics()->QuadsEnd();
1237 }
1238 // full
1239 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudNinjaBarFullLeft);
1240 Graphics()->QuadsBegin();
1241 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: Alpha);
1242 // Subset: btm_m, top_m, top_l, btm_l | it is mirrored on the horizontal axe and rotated 90 degrees counterclockwise
1243 Graphics()->QuadsSetSubsetFree(x0: RestPct + ProgPct * EndingPieceProgress, y0: 1, x1: RestPct + ProgPct * EndingPieceProgress, y1: 0, x2: 0, y2: 0, x3: 0, y3: 1);
1244 IGraphics::CQuadItem QuadFullEnding(x, y + (EndProgressHeight * (1.0f - EndingPieceProgress)), BarWidth, EndRestHeight + EndProgressHeight * EndingPieceProgress);
1245 Graphics()->QuadsDrawTL(pArray: &QuadFullEnding, Num: 1);
1246 Graphics()->QuadsEnd();
1247
1248 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
1249 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: 1.f);
1250 Graphics()->WrapNormal();
1251}
1252
1253void CHud::RenderSpectatorCount()
1254{
1255 if(!g_Config.m_ClShowhudSpectatorCount)
1256 {
1257 return;
1258 }
1259
1260 int Count = 0;
1261 if(Client()->IsSixup())
1262 {
1263 for(int i = 0; i < MAX_CLIENTS; i++)
1264 {
1265 if(i == GameClient()->m_aLocalIds[0] || (GameClient()->Client()->DummyConnected() && i == GameClient()->m_aLocalIds[1]))
1266 continue;
1267
1268 if(Client()->m_TranslationContext.m_aClients[i].m_PlayerFlags7 & protocol7::PLAYERFLAG_WATCHING)
1269 {
1270 Count++;
1271 }
1272 }
1273 }
1274 else
1275 {
1276 const CNetObj_SpectatorCount *pSpectatorCount = GameClient()->m_Snap.m_pSpectatorCount;
1277 if(!pSpectatorCount)
1278 {
1279 m_LastSpectatorCountTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
1280 return;
1281 }
1282 Count = pSpectatorCount->m_NumSpectators;
1283 }
1284
1285 if(Count == 0)
1286 {
1287 m_LastSpectatorCountTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
1288 return;
1289 }
1290
1291 // 1 second delay
1292 if(Client()->GameTick(Conn: g_Config.m_ClDummy) < m_LastSpectatorCountTick + Client()->GameTickSpeed())
1293 return;
1294
1295 char aBuf[16];
1296 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", Count);
1297
1298 const float Fontsize = 6.0f;
1299 const float BoxHeight = 14.f;
1300 const float BoxWidth = 13.f + TextRender()->TextWidth(Size: Fontsize, pText: aBuf);
1301
1302 float StartX = m_Width - BoxWidth;
1303 float StartY = 285.0f - BoxHeight - 4; // 4 units distance to the next display;
1304 if(g_Config.m_ClShowhudPlayerPosition || g_Config.m_ClShowhudPlayerSpeed || g_Config.m_ClShowhudPlayerAngle)
1305 {
1306 StartY -= 4;
1307 }
1308 StartY -= GetMovementInformationBoxHeight();
1309
1310 if(g_Config.m_ClShowhudScore)
1311 {
1312 StartY -= 56;
1313 }
1314
1315 if(g_Config.m_ClShowhudDummyActions && !(GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER) && Client()->DummyConnected())
1316 {
1317 StartY = StartY - 29.0f - 4; // dummy actions height and padding
1318 }
1319
1320 Graphics()->DrawRect(x: StartX, y: StartY, w: BoxWidth, h: BoxHeight, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), Corners: IGraphics::CORNER_L, Rounding: 5.0f);
1321
1322 float y = StartY + BoxHeight / 3;
1323 float x = StartX + 2;
1324
1325 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1326 TextRender()->Text(x, y, Size: Fontsize, pText: FontIcons::FONT_ICON_EYE, LineWidth: -1.0f);
1327 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1328 TextRender()->Text(x: x + Fontsize + 3.f, y, Size: Fontsize, pText: aBuf, LineWidth: -1.0f);
1329}
1330
1331void CHud::RenderDummyActions()
1332{
1333 if(!g_Config.m_ClShowhudDummyActions || (GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER) || !Client()->DummyConnected())
1334 {
1335 return;
1336 }
1337 // render small dummy actions hud
1338 const float BoxHeight = 29.0f;
1339 const float BoxWidth = 16.0f;
1340
1341 float StartX = m_Width - BoxWidth;
1342 float StartY = 285.0f - BoxHeight - 4; // 4 units distance to the next display;
1343 if(g_Config.m_ClShowhudPlayerPosition || g_Config.m_ClShowhudPlayerSpeed || g_Config.m_ClShowhudPlayerAngle)
1344 {
1345 StartY -= 4;
1346 }
1347 StartY -= GetMovementInformationBoxHeight();
1348
1349 if(g_Config.m_ClShowhudScore)
1350 {
1351 StartY -= 56;
1352 }
1353
1354 Graphics()->DrawRect(x: StartX, y: StartY, w: BoxWidth, h: BoxHeight, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), Corners: IGraphics::CORNER_L, Rounding: 5.0f);
1355
1356 float y = StartY + 2;
1357 float x = StartX + 2;
1358 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
1359 if(g_Config.m_ClDummyHammer)
1360 {
1361 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
1362 }
1363 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudDummyHammer);
1364 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_DummyHammerOffset, X: x, Y: y);
1365 y += 13;
1366 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
1367 if(g_Config.m_ClDummyCopyMoves)
1368 {
1369 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
1370 }
1371 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudDummyCopy);
1372 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_DummyCopyOffset, X: x, Y: y);
1373}
1374
1375inline int CHud::GetDigitsIndex(int Value, int Max)
1376{
1377 if(Value < 0)
1378 {
1379 Value *= -1;
1380 }
1381 int DigitsIndex = std::log10(x: (Value ? Value : 1));
1382 if(DigitsIndex > Max)
1383 {
1384 DigitsIndex = Max;
1385 }
1386 if(DigitsIndex < 0)
1387 {
1388 DigitsIndex = 0;
1389 }
1390 return DigitsIndex;
1391}
1392
1393inline float CHud::GetMovementInformationBoxHeight()
1394{
1395 if(GameClient()->m_Snap.m_SpecInfo.m_Active && (GameClient()->m_Snap.m_SpecInfo.m_SpectatorId == SPEC_FREEVIEW || GameClient()->m_aClients[GameClient()->m_Snap.m_SpecInfo.m_SpectatorId].m_SpecCharPresent))
1396 return g_Config.m_ClShowhudPlayerPosition ? 3.0f * MOVEMENT_INFORMATION_LINE_HEIGHT + 2.0f : 0.0f;
1397 float BoxHeight = 3.0f * MOVEMENT_INFORMATION_LINE_HEIGHT * (g_Config.m_ClShowhudPlayerPosition + g_Config.m_ClShowhudPlayerSpeed) + 2.0f * MOVEMENT_INFORMATION_LINE_HEIGHT * g_Config.m_ClShowhudPlayerAngle;
1398 if(g_Config.m_ClShowhudPlayerPosition || g_Config.m_ClShowhudPlayerSpeed || g_Config.m_ClShowhudPlayerAngle)
1399 {
1400 BoxHeight += 2.0f;
1401 }
1402 return BoxHeight;
1403}
1404
1405void CHud::UpdateMovementInformationTextContainer(STextContainerIndex &TextContainer, float FontSize, float Value, float &PrevValue)
1406{
1407 Value = std::round(x: Value * 100.0f) / 100.0f; // Round to 2dp
1408 if(TextContainer.Valid() && PrevValue == Value)
1409 return;
1410 PrevValue = Value;
1411
1412 char aBuf[128];
1413 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%.2f", Value);
1414
1415 CTextCursor Cursor;
1416 Cursor.m_FontSize = FontSize;
1417 TextRender()->RecreateTextContainer(TextContainerIndex&: TextContainer, pCursor: &Cursor, pText: aBuf);
1418}
1419
1420void CHud::RenderMovementInformationTextContainer(STextContainerIndex &TextContainer, const ColorRGBA &Color, float X, float Y)
1421{
1422 if(TextContainer.Valid())
1423 {
1424 TextRender()->RenderTextContainer(TextContainerIndex: TextContainer, TextColor: Color, TextOutlineColor: TextRender()->DefaultTextOutlineColor(), X: X - TextRender()->GetBoundingBoxTextContainer(TextContainerIndex: TextContainer).m_W, Y);
1425 }
1426}
1427
1428CHud::CMovementInformation CHud::GetMovementInformation(int ClientId, int Conn) const
1429{
1430 CMovementInformation Out;
1431 if(ClientId == SPEC_FREEVIEW)
1432 {
1433 Out.m_Pos = GameClient()->m_Camera.m_Center / 32.0f;
1434 }
1435 else if(GameClient()->m_aClients[ClientId].m_SpecCharPresent)
1436 {
1437 Out.m_Pos = GameClient()->m_aClients[ClientId].m_SpecChar / 32.0f;
1438 }
1439 else
1440 {
1441 const CNetObj_Character *pPrevChar = &GameClient()->m_Snap.m_aCharacters[ClientId].m_Prev;
1442 const CNetObj_Character *pCurChar = &GameClient()->m_Snap.m_aCharacters[ClientId].m_Cur;
1443 const float IntraTick = Client()->IntraGameTick(Conn);
1444
1445 // To make the player position relative to blocks we need to divide by the block size
1446 Out.m_Pos = mix(a: vec2(pPrevChar->m_X, pPrevChar->m_Y), b: vec2(pCurChar->m_X, pCurChar->m_Y), amount: IntraTick) / 32.0f;
1447
1448 const vec2 Vel = mix(a: vec2(pPrevChar->m_VelX, pPrevChar->m_VelY), b: vec2(pCurChar->m_VelX, pCurChar->m_VelY), amount: IntraTick);
1449
1450 float VelspeedX = Vel.x / 256.0f * Client()->GameTickSpeed();
1451 if(Vel.x >= -1.0f && Vel.x <= 1.0f)
1452 {
1453 VelspeedX = 0.0f;
1454 }
1455 float VelspeedY = Vel.y / 256.0f * Client()->GameTickSpeed();
1456 if(Vel.y >= -128.0f && Vel.y <= 128.0f)
1457 {
1458 VelspeedY = 0.0f;
1459 }
1460 // We show the speed in Blocks per Second (Bps) and therefore have to divide by the block size
1461 Out.m_Speed.x = VelspeedX / 32.0f;
1462 float VelspeedLength = length(a: vec2(Vel.x, Vel.y) / 256.0f) * Client()->GameTickSpeed();
1463 // Todo: Use Velramp tuning of each individual player
1464 // Since these tuning parameters are almost never changed, the default values are sufficient in most cases
1465 float Ramp = VelocityRamp(Value: VelspeedLength, Start: GameClient()->m_aTuning[Conn].m_VelrampStart, Range: GameClient()->m_aTuning[Conn].m_VelrampRange, Curvature: GameClient()->m_aTuning[Conn].m_VelrampCurvature);
1466 Out.m_Speed.x *= Ramp;
1467 Out.m_Speed.y = VelspeedY / 32.0f;
1468
1469 float Angle = GameClient()->m_Players.GetPlayerTargetAngle(pPrevChar, pPlayerChar: pCurChar, ClientId, Intra: IntraTick);
1470 if(Angle < 0.0f)
1471 {
1472 Angle += 2.0f * pi;
1473 }
1474 Out.m_Angle = Angle * 180.0f / pi;
1475 }
1476 return Out;
1477}
1478
1479void CHud::RenderMovementInformation()
1480{
1481 const int ClientId = GameClient()->m_Snap.m_SpecInfo.m_Active ? GameClient()->m_Snap.m_SpecInfo.m_SpectatorId : GameClient()->m_Snap.m_LocalClientId;
1482 const bool PosOnly = ClientId == SPEC_FREEVIEW || (GameClient()->m_aClients[ClientId].m_SpecCharPresent);
1483 // Draw the information depending on settings: Position, speed and target angle
1484 // This display is only to present the available information from the last snapshot, not to interpolate or predict
1485 if(!g_Config.m_ClShowhudPlayerPosition && (PosOnly || (!g_Config.m_ClShowhudPlayerSpeed && !g_Config.m_ClShowhudPlayerAngle)))
1486 {
1487 return;
1488 }
1489 const float LineSpacer = 1.0f; // above and below each entry
1490 const float Fontsize = 6.0f;
1491
1492 float BoxHeight = GetMovementInformationBoxHeight();
1493 const float BoxWidth = 62.0f;
1494
1495 float StartX = m_Width - BoxWidth;
1496 float StartY = 285.0f - BoxHeight - 4.0f; // 4 units distance to the next display;
1497 if(g_Config.m_ClShowhudScore)
1498 {
1499 StartY -= 56.0f;
1500 }
1501
1502 Graphics()->DrawRect(x: StartX, y: StartY, w: BoxWidth, h: BoxHeight, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), Corners: IGraphics::CORNER_L, Rounding: 5.0f);
1503
1504 const CMovementInformation Info = GetMovementInformation(ClientId, Conn: g_Config.m_ClDummy);
1505
1506 float y = StartY + LineSpacer * 2.0f;
1507 const float LeftX = StartX + 2.0f;
1508 const float RightX = m_Width - 2.0f;
1509
1510 if(g_Config.m_ClShowhudPlayerPosition)
1511 {
1512 TextRender()->Text(x: LeftX, y, Size: Fontsize, pText: Localize(pStr: "Position:"), LineWidth: -1.0f);
1513 y += MOVEMENT_INFORMATION_LINE_HEIGHT;
1514
1515 TextRender()->Text(x: LeftX, y, Size: Fontsize, pText: "X:", LineWidth: -1.0f);
1516 UpdateMovementInformationTextContainer(TextContainer&: m_aPlayerPositionContainers[0], FontSize: Fontsize, Value: Info.m_Pos.x, PrevValue&: m_aPlayerPrevPosition[0]);
1517 RenderMovementInformationTextContainer(TextContainer&: m_aPlayerPositionContainers[0], Color: TextRender()->DefaultTextColor(), X: RightX, Y: y);
1518 y += MOVEMENT_INFORMATION_LINE_HEIGHT;
1519
1520 TextRender()->Text(x: LeftX, y, Size: Fontsize, pText: "Y:", LineWidth: -1.0f);
1521 UpdateMovementInformationTextContainer(TextContainer&: m_aPlayerPositionContainers[1], FontSize: Fontsize, Value: Info.m_Pos.y, PrevValue&: m_aPlayerPrevPosition[1]);
1522 RenderMovementInformationTextContainer(TextContainer&: m_aPlayerPositionContainers[1], Color: TextRender()->DefaultTextColor(), X: RightX, Y: y);
1523 y += MOVEMENT_INFORMATION_LINE_HEIGHT;
1524 }
1525
1526 if(PosOnly)
1527 return;
1528
1529 if(g_Config.m_ClShowhudPlayerSpeed)
1530 {
1531 TextRender()->Text(x: LeftX, y, Size: Fontsize, pText: Localize(pStr: "Speed:"), LineWidth: -1.0f);
1532 y += MOVEMENT_INFORMATION_LINE_HEIGHT;
1533
1534 const char aaCoordinates[][4] = {"X:", "Y:"};
1535 for(int i = 0; i < 2; i++)
1536 {
1537 ColorRGBA Color(1.0f, 1.0f, 1.0f, 1.0f);
1538 if(m_aLastPlayerSpeedChange[i] == ESpeedChange::INCREASE)
1539 Color = ColorRGBA(0.0f, 1.0f, 0.0f, 1.0f);
1540 if(m_aLastPlayerSpeedChange[i] == ESpeedChange::DECREASE)
1541 Color = ColorRGBA(1.0f, 0.5f, 0.5f, 1.0f);
1542 TextRender()->Text(x: LeftX, y, Size: Fontsize, pText: aaCoordinates[i], LineWidth: -1.0f);
1543 UpdateMovementInformationTextContainer(TextContainer&: m_aPlayerSpeedTextContainers[i], FontSize: Fontsize, Value: i == 0 ? Info.m_Speed.x : Info.m_Speed.y, PrevValue&: m_aPlayerPrevSpeed[i]);
1544 RenderMovementInformationTextContainer(TextContainer&: m_aPlayerSpeedTextContainers[i], Color, X: RightX, Y: y);
1545 y += MOVEMENT_INFORMATION_LINE_HEIGHT;
1546 }
1547
1548 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
1549 }
1550
1551 if(g_Config.m_ClShowhudPlayerAngle)
1552 {
1553 TextRender()->Text(x: LeftX, y, Size: Fontsize, pText: Localize(pStr: "Angle:"), LineWidth: -1.0f);
1554 y += MOVEMENT_INFORMATION_LINE_HEIGHT;
1555
1556 UpdateMovementInformationTextContainer(TextContainer&: m_PlayerAngleTextContainerIndex, FontSize: Fontsize, Value: Info.m_Angle, PrevValue&: m_PlayerPrevAngle);
1557 RenderMovementInformationTextContainer(TextContainer&: m_PlayerAngleTextContainerIndex, Color: TextRender()->DefaultTextColor(), X: RightX, Y: y);
1558 }
1559}
1560
1561void CHud::RenderSpectatorHud()
1562{
1563 if(!g_Config.m_ClShowhudSpectator)
1564 return;
1565
1566 // draw the box
1567 Graphics()->DrawRect(x: m_Width - 180.0f, y: m_Height - 15.0f, w: 180.0f, h: 15.0f, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), Corners: IGraphics::CORNER_TL, Rounding: 5.0f);
1568
1569 // draw the text
1570 char aBuf[128];
1571 if(GameClient()->m_MultiViewActivated)
1572 {
1573 str_copy(dst&: aBuf, src: Localize(pStr: "Multi-View"));
1574 }
1575 else if(GameClient()->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW)
1576 {
1577 const auto &Player = GameClient()->m_aClients[GameClient()->m_Snap.m_SpecInfo.m_SpectatorId];
1578 if(g_Config.m_ClShowIds)
1579 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Following %d: %s", pContext: "Spectating"), Player.ClientId(), Player.m_aName);
1580 else
1581 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Following %s", pContext: "Spectating"), Player.m_aName);
1582 }
1583 else
1584 {
1585 str_copy(dst&: aBuf, src: Localize(pStr: "Free-View"));
1586 }
1587 TextRender()->Text(x: m_Width - 174.0f, y: m_Height - 15.0f + (15.f - 8.f) / 2.f, Size: 8.0f, pText: aBuf, LineWidth: -1.0f);
1588
1589 // draw the camera info
1590 if(Client()->State() != IClient::STATE_DEMOPLAYBACK && GameClient()->m_Camera.SpectatingPlayer() && GameClient()->m_Camera.CanUseAutoSpecCamera() && g_Config.m_ClSpecAutoSync)
1591 {
1592 bool AutoSpecCameraEnabled = GameClient()->m_Camera.m_AutoSpecCamera;
1593 const char *pLabelText = Localize(pStr: "AUTO", pContext: "Spectating Camera Mode Icon");
1594 const float TextWidth = TextRender()->TextWidth(Size: 6.0f, pText: pLabelText);
1595
1596 constexpr float RightMargin = 4.0f;
1597 constexpr float IconWidth = 6.0f;
1598 constexpr float Padding = 3.0f;
1599 const float TagWidth = IconWidth + TextWidth + Padding * 3.0f;
1600 const float TagX = m_Width - RightMargin - TagWidth;
1601 Graphics()->DrawRect(x: TagX, y: m_Height - 12.0f, w: TagWidth, h: 10.0f, Color: ColorRGBA(1.0f, 1.0f, 1.0f, AutoSpecCameraEnabled ? 0.50f : 0.10f), Corners: IGraphics::CORNER_ALL, Rounding: 2.5f);
1602 TextRender()->TextColor(r: 1, g: 1, b: 1, a: AutoSpecCameraEnabled ? 1.0f : 0.65f);
1603 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1604 TextRender()->Text(x: TagX + Padding, y: m_Height - 10.0f, Size: 6.0f, pText: FontIcons::FONT_ICON_CAMERA, LineWidth: -1.0f);
1605 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1606 TextRender()->Text(x: TagX + Padding + IconWidth + Padding, y: m_Height - 10.0f, Size: 6.0f, pText: pLabelText, LineWidth: -1.0f);
1607 TextRender()->TextColor(r: 1, g: 1, b: 1, a: 1);
1608 }
1609}
1610
1611void CHud::RenderLocalTime(float x)
1612{
1613 if(!g_Config.m_ClShowLocalTimeAlways && !GameClient()->m_Scoreboard.IsActive())
1614 return;
1615
1616 // draw the box
1617 Graphics()->DrawRect(x: x - 30.0f, y: 0.0f, w: 25.0f, h: 12.5f, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), Corners: IGraphics::CORNER_B, Rounding: 3.75f);
1618
1619 // draw the text
1620 char aTimeStr[6];
1621 str_timestamp_format(buffer: aTimeStr, buffer_size: sizeof(aTimeStr), format: "%H:%M");
1622 TextRender()->Text(x: x - 25.0f, y: (12.5f - 5.f) / 2.f, Size: 5.0f, pText: aTimeStr, LineWidth: -1.0f);
1623}
1624
1625void CHud::OnNewSnapshot()
1626{
1627 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
1628 return;
1629 if(!GameClient()->m_Snap.m_pGameInfoObj)
1630 return;
1631
1632 int ClientId = -1;
1633 if(GameClient()->m_Snap.m_pLocalCharacter && !GameClient()->m_Snap.m_SpecInfo.m_Active && !(GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
1634 ClientId = GameClient()->m_Snap.m_LocalClientId;
1635 else if(GameClient()->m_Snap.m_SpecInfo.m_Active)
1636 ClientId = GameClient()->m_Snap.m_SpecInfo.m_SpectatorId;
1637
1638 if(ClientId == -1)
1639 return;
1640
1641 const CNetObj_Character *pPrevChar = &GameClient()->m_Snap.m_aCharacters[ClientId].m_Prev;
1642 const CNetObj_Character *pCurChar = &GameClient()->m_Snap.m_aCharacters[ClientId].m_Cur;
1643 const float IntraTick = Client()->IntraGameTick(Conn: g_Config.m_ClDummy);
1644 ivec2 Vel = mix(a: ivec2(pPrevChar->m_VelX, pPrevChar->m_VelY), b: ivec2(pCurChar->m_VelX, pCurChar->m_VelY), amount: IntraTick);
1645
1646 CCharacter *pChar = GameClient()->m_PredictedWorld.GetCharacterById(Id: ClientId);
1647 if(pChar && pChar->IsGrounded())
1648 Vel.y = 0;
1649
1650 int aVels[2] = {Vel.x, Vel.y};
1651
1652 for(int i = 0; i < 2; i++)
1653 {
1654 int AbsVel = abs(x: aVels[i]);
1655 if(AbsVel > m_aPlayerSpeed[i])
1656 {
1657 m_aLastPlayerSpeedChange[i] = ESpeedChange::INCREASE;
1658 }
1659 if(AbsVel < m_aPlayerSpeed[i])
1660 {
1661 m_aLastPlayerSpeedChange[i] = ESpeedChange::DECREASE;
1662 }
1663 if(AbsVel < 2)
1664 {
1665 m_aLastPlayerSpeedChange[i] = ESpeedChange::NONE;
1666 }
1667 m_aPlayerSpeed[i] = AbsVel;
1668 }
1669}
1670
1671void CHud::OnRender()
1672{
1673 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
1674 return;
1675
1676 if(!GameClient()->m_Snap.m_pGameInfoObj)
1677 return;
1678
1679 m_Width = 300.0f * Graphics()->ScreenAspect();
1680 m_Height = 300.0f;
1681 Graphics()->MapScreen(TopLeftX: 0.0f, TopLeftY: 0.0f, BottomRightX: m_Width, BottomRightY: m_Height);
1682
1683#if defined(CONF_VIDEORECORDER)
1684 if((IVideo::Current() && g_Config.m_ClVideoShowhud) || (!IVideo::Current() && g_Config.m_ClShowhud))
1685#else
1686 if(g_Config.m_ClShowhud)
1687#endif
1688 {
1689 if(GameClient()->m_Snap.m_pLocalCharacter && !GameClient()->m_Snap.m_SpecInfo.m_Active && !(GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
1690 {
1691 if(g_Config.m_ClShowhudHealthAmmo)
1692 {
1693 RenderAmmoHealthAndArmor(pCharacter: GameClient()->m_Snap.m_pLocalCharacter);
1694 }
1695 if(GameClient()->m_Snap.m_aCharacters[GameClient()->m_Snap.m_LocalClientId].m_HasExtendedData && g_Config.m_ClShowhudDDRace && GameClient()->m_GameInfo.m_HudDDRace)
1696 {
1697 RenderPlayerState(ClientId: GameClient()->m_Snap.m_LocalClientId);
1698 }
1699 RenderSpectatorCount();
1700 RenderMovementInformation();
1701 RenderDDRaceEffects();
1702 }
1703 else if(GameClient()->m_Snap.m_SpecInfo.m_Active)
1704 {
1705 int SpectatorId = GameClient()->m_Snap.m_SpecInfo.m_SpectatorId;
1706 if(SpectatorId != SPEC_FREEVIEW && g_Config.m_ClShowhudHealthAmmo)
1707 {
1708 RenderAmmoHealthAndArmor(pCharacter: &GameClient()->m_Snap.m_aCharacters[SpectatorId].m_Cur);
1709 }
1710 if(SpectatorId != SPEC_FREEVIEW &&
1711 GameClient()->m_Snap.m_aCharacters[SpectatorId].m_HasExtendedData &&
1712 g_Config.m_ClShowhudDDRace &&
1713 (!GameClient()->m_MultiViewActivated || GameClient()->m_MultiViewShowHud) &&
1714 GameClient()->m_GameInfo.m_HudDDRace)
1715 {
1716 RenderPlayerState(ClientId: SpectatorId);
1717 }
1718 RenderMovementInformation();
1719 RenderSpectatorHud();
1720 }
1721
1722 if(g_Config.m_ClShowhudTimer)
1723 RenderGameTimer();
1724 RenderPauseNotification();
1725 RenderSuddenDeath();
1726 if(g_Config.m_ClShowhudScore)
1727 RenderScoreHud();
1728 RenderDummyActions();
1729 RenderWarmupTimer();
1730 RenderTextInfo();
1731 RenderLocalTime(x: (m_Width / 7) * 3);
1732 if(Client()->State() != IClient::STATE_DEMOPLAYBACK)
1733 RenderConnectionWarning();
1734 RenderTeambalanceWarning();
1735 GameClient()->m_Voting.Render();
1736 if(g_Config.m_ClShowRecord)
1737 RenderRecord();
1738 }
1739 RenderCursor();
1740}
1741
1742void CHud::OnMessage(int MsgType, void *pRawMsg)
1743{
1744 if(MsgType == NETMSGTYPE_SV_DDRACETIME || MsgType == NETMSGTYPE_SV_DDRACETIMELEGACY)
1745 {
1746 CNetMsg_Sv_DDRaceTime *pMsg = (CNetMsg_Sv_DDRaceTime *)pRawMsg;
1747
1748 m_DDRaceTime = pMsg->m_Time;
1749
1750 m_ShowFinishTime = pMsg->m_Finish != 0;
1751
1752 if(!m_ShowFinishTime)
1753 {
1754 m_TimeCpDiff = (float)pMsg->m_Check / 100;
1755 m_TimeCpLastReceivedTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
1756 }
1757 else
1758 {
1759 m_FinishTimeDiff = (float)pMsg->m_Check / 100;
1760 m_FinishTimeLastReceivedTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
1761 }
1762 }
1763 else if(MsgType == NETMSGTYPE_SV_RECORD || MsgType == NETMSGTYPE_SV_RECORDLEGACY)
1764 {
1765 CNetMsg_Sv_Record *pMsg = (CNetMsg_Sv_Record *)pRawMsg;
1766
1767 // NETMSGTYPE_SV_RACETIME on old race servers
1768 if(MsgType == NETMSGTYPE_SV_RECORDLEGACY && GameClient()->m_GameInfo.m_DDRaceRecordMessage)
1769 {
1770 m_DDRaceTime = pMsg->m_ServerTimeBest; // First value: m_Time
1771
1772 m_FinishTimeLastReceivedTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
1773
1774 if(pMsg->m_PlayerTimeBest) // Second value: m_Check
1775 {
1776 m_TimeCpDiff = (float)pMsg->m_PlayerTimeBest / 100;
1777 m_TimeCpLastReceivedTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
1778 }
1779 }
1780 else if(MsgType == NETMSGTYPE_SV_RECORD || GameClient()->m_GameInfo.m_RaceRecordMessage)
1781 {
1782 // ignore m_ServerTimeBest, it's handled by the game client
1783 m_aPlayerRecord[g_Config.m_ClDummy] = (float)pMsg->m_PlayerTimeBest / 100;
1784 }
1785 }
1786}
1787
1788void CHud::RenderDDRaceEffects()
1789{
1790 if(m_DDRaceTime)
1791 {
1792 char aBuf[64];
1793 char aTime[32];
1794 if(m_ShowFinishTime && m_FinishTimeLastReceivedTick + Client()->GameTickSpeed() * 6 > Client()->GameTick(Conn: g_Config.m_ClDummy))
1795 {
1796 str_time(centisecs: m_DDRaceTime, format: TIME_HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1797 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Finish time: %s", aTime);
1798
1799 // calculate alpha (4 sec 1 than get lower the next 2 sec)
1800 float Alpha = 1.0f;
1801 if(m_FinishTimeLastReceivedTick + Client()->GameTickSpeed() * 4 < Client()->GameTick(Conn: g_Config.m_ClDummy) && m_FinishTimeLastReceivedTick + Client()->GameTickSpeed() * 6 > Client()->GameTick(Conn: g_Config.m_ClDummy))
1802 {
1803 // lower the alpha slowly to blend text out
1804 Alpha = ((float)(m_FinishTimeLastReceivedTick + Client()->GameTickSpeed() * 6) - (float)Client()->GameTick(Conn: g_Config.m_ClDummy)) / (float)(Client()->GameTickSpeed() * 2);
1805 }
1806
1807 TextRender()->TextColor(r: 1, g: 1, b: 1, a: Alpha);
1808 CTextCursor Cursor;
1809 Cursor.SetPosition(vec2(150 * Graphics()->ScreenAspect() - TextRender()->TextWidth(Size: 12, pText: aBuf) / 2, 20));
1810 Cursor.m_FontSize = 12.0f;
1811 TextRender()->RecreateTextContainer(TextContainerIndex&: m_DDRaceEffectsTextContainerIndex, pCursor: &Cursor, pText: aBuf);
1812 if(m_FinishTimeDiff != 0.0f && m_DDRaceEffectsTextContainerIndex.Valid())
1813 {
1814 if(m_FinishTimeDiff < 0)
1815 {
1816 str_time_float(secs: -m_FinishTimeDiff, format: TIME_HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1817 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "-%s", aTime);
1818 TextRender()->TextColor(r: 0.5f, g: 1.0f, b: 0.5f, a: Alpha); // green
1819 }
1820 else
1821 {
1822 str_time_float(secs: m_FinishTimeDiff, format: TIME_HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1823 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "+%s", aTime);
1824 TextRender()->TextColor(r: 1.0f, g: 0.5f, b: 0.5f, a: Alpha); // red
1825 }
1826 CTextCursor DiffCursor;
1827 DiffCursor.SetPosition(vec2(150 * Graphics()->ScreenAspect() - TextRender()->TextWidth(Size: 10, pText: aBuf) / 2, 34));
1828 DiffCursor.m_FontSize = 10.0f;
1829 TextRender()->AppendTextContainer(TextContainerIndex: m_DDRaceEffectsTextContainerIndex, pCursor: &DiffCursor, pText: aBuf);
1830 }
1831 if(m_DDRaceEffectsTextContainerIndex.Valid())
1832 {
1833 auto OutlineColor = TextRender()->DefaultTextOutlineColor();
1834 OutlineColor.a *= Alpha;
1835 TextRender()->RenderTextContainer(TextContainerIndex: m_DDRaceEffectsTextContainerIndex, TextColor: TextRender()->DefaultTextColor(), TextOutlineColor: OutlineColor);
1836 }
1837 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1838 }
1839 else if(g_Config.m_ClShowhudTimeCpDiff && !m_ShowFinishTime && m_TimeCpLastReceivedTick + Client()->GameTickSpeed() * 6 > Client()->GameTick(Conn: g_Config.m_ClDummy))
1840 {
1841 if(m_TimeCpDiff < 0)
1842 {
1843 str_time_float(secs: -m_TimeCpDiff, format: TIME_HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1844 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "-%s", aTime);
1845 }
1846 else
1847 {
1848 str_time_float(secs: m_TimeCpDiff, format: TIME_HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1849 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "+%s", aTime);
1850 }
1851
1852 // calculate alpha (4 sec 1 than get lower the next 2 sec)
1853 float Alpha = 1.0f;
1854 if(m_TimeCpLastReceivedTick + Client()->GameTickSpeed() * 4 < Client()->GameTick(Conn: g_Config.m_ClDummy) && m_TimeCpLastReceivedTick + Client()->GameTickSpeed() * 6 > Client()->GameTick(Conn: g_Config.m_ClDummy))
1855 {
1856 // lower the alpha slowly to blend text out
1857 Alpha = ((float)(m_TimeCpLastReceivedTick + Client()->GameTickSpeed() * 6) - (float)Client()->GameTick(Conn: g_Config.m_ClDummy)) / (float)(Client()->GameTickSpeed() * 2);
1858 }
1859
1860 if(m_TimeCpDiff > 0)
1861 TextRender()->TextColor(r: 1.0f, g: 0.5f, b: 0.5f, a: Alpha); // red
1862 else if(m_TimeCpDiff < 0)
1863 TextRender()->TextColor(r: 0.5f, g: 1.0f, b: 0.5f, a: Alpha); // green
1864 else if(!m_TimeCpDiff)
1865 TextRender()->TextColor(r: 1, g: 1, b: 1, a: Alpha); // white
1866
1867 CTextCursor Cursor;
1868 Cursor.SetPosition(vec2(150 * Graphics()->ScreenAspect() - TextRender()->TextWidth(Size: 10, pText: aBuf) / 2, 20));
1869 Cursor.m_FontSize = 10.0f;
1870 TextRender()->RecreateTextContainer(TextContainerIndex&: m_DDRaceEffectsTextContainerIndex, pCursor: &Cursor, pText: aBuf);
1871
1872 if(m_DDRaceEffectsTextContainerIndex.Valid())
1873 {
1874 auto OutlineColor = TextRender()->DefaultTextOutlineColor();
1875 OutlineColor.a *= Alpha;
1876 TextRender()->RenderTextContainer(TextContainerIndex: m_DDRaceEffectsTextContainerIndex, TextColor: TextRender()->DefaultTextColor(), TextOutlineColor: OutlineColor);
1877 }
1878 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1879 }
1880 }
1881}
1882
1883void CHud::RenderRecord()
1884{
1885 if(GameClient()->m_MapBestTimeSeconds != FinishTime::UNSET && GameClient()->m_MapBestTimeSeconds != FinishTime::NOT_FINISHED_MILLIS)
1886 {
1887 char aBuf[64];
1888 TextRender()->Text(x: 5, y: 75, Size: 6, pText: Localize(pStr: "Server best:"), LineWidth: -1.0f);
1889 char aTime[32];
1890 int64_t TimeCentiseconds = static_cast<int64_t>(GameClient()->m_MapBestTimeSeconds) * 100 + static_cast<int64_t>(GameClient()->m_MapBestTimeMillis) / 10;
1891 str_time(centisecs: TimeCentiseconds, format: TIME_HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1892 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s%s", GameClient()->m_MapBestTimeSeconds > 3600 ? "" : "   ", aTime);
1893 TextRender()->Text(x: 53, y: 75, Size: 6, pText: aBuf, LineWidth: -1.0f);
1894 }
1895
1896 if(GameClient()->m_ReceivedDDNetPlayerFinishTimes)
1897 {
1898 const int PlayerTimeSeconds = GameClient()->m_aClients[GameClient()->m_aLocalIds[g_Config.m_ClDummy]].m_FinishTimeSeconds;
1899 if(PlayerTimeSeconds != FinishTime::NOT_FINISHED_MILLIS)
1900 {
1901 char aBuf[64];
1902 TextRender()->Text(x: 5, y: 82, Size: 6, pText: Localize(pStr: "Personal best:"), LineWidth: -1.0f);
1903 char aTime[32];
1904 const int PlayerTimeMillis = GameClient()->m_aClients[GameClient()->m_aLocalIds[g_Config.m_ClDummy]].m_FinishTimeMillis;
1905 int64_t TimeCentiseconds = static_cast<int64_t>(PlayerTimeSeconds) * 100 + static_cast<int64_t>(PlayerTimeMillis) / 10;
1906 str_time(centisecs: TimeCentiseconds, format: TIME_HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1907 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s%s", PlayerTimeSeconds > 3600 ? "" : "   ", aTime);
1908 TextRender()->Text(x: 53, y: 82, Size: 6, pText: aBuf, LineWidth: -1.0f);
1909 }
1910 }
1911 else
1912 {
1913 const float PlayerRecord = m_aPlayerRecord[g_Config.m_ClDummy];
1914 if(PlayerRecord > 0.0f)
1915 {
1916 char aBuf[64];
1917 TextRender()->Text(x: 5, y: 82, Size: 6, pText: Localize(pStr: "Personal best:"), LineWidth: -1.0f);
1918 char aTime[32];
1919 str_time_float(secs: PlayerRecord, format: TIME_HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1920 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s%s", PlayerRecord > 3600 ? "" : "   ", aTime);
1921 TextRender()->Text(x: 53, y: 82, Size: 6, pText: aBuf, LineWidth: -1.0f);
1922 }
1923 }
1924}
1925