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