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