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 float aPoints[4];
631 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);
632 Graphics()->MapScreen(TopLeftX: aPoints[0], TopLeftY: aPoints[1], BottomRightX: aPoints[2], BottomRightY: aPoints[3]);
633
634 if(Client()->State() != IClient::STATE_DEMOPLAYBACK && GameClient()->m_Snap.m_pLocalCharacter)
635 {
636 // Render local cursor
637 CurWeapon = std::max(a: 0, b: GameClient()->m_aClients[GameClient()->m_Snap.m_LocalClientId].m_Predicted.m_ActiveWeapon);
638 TargetPos = GameClient()->m_Controls.m_aTargetPos[g_Config.m_ClDummy];
639 }
640 else
641 {
642 // Render spec cursor
643 if(!g_Config.m_ClSpecCursor || !GameClient()->m_CursorInfo.IsAvailable())
644 return;
645
646 bool RenderSpecCursor = (GameClient()->m_Snap.m_SpecInfo.m_Active && GameClient()->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW) || Client()->State() == IClient::STATE_DEMOPLAYBACK;
647
648 if(!RenderSpecCursor)
649 return;
650
651 // Calculate factor to keep cursor on screen
652 const vec2 HalfSize = vec2(Center.x - aPoints[0], Center.y - aPoints[1]);
653 const vec2 ScreenPos = (GameClient()->m_CursorInfo.WorldTarget() - Center) / GameClient()->m_Camera.m_Zoom;
654 const float ClampFactor = std::max(l: {
655 1.0f,
656 absolute(a: ScreenPos.x / HalfSize.x),
657 absolute(a: ScreenPos.y / HalfSize.y),
658 });
659
660 CurWeapon = std::max(a: 0, b: GameClient()->m_CursorInfo.Weapon() % NUM_WEAPONS);
661 TargetPos = ScreenPos / ClampFactor + Center;
662 if(ClampFactor != 1.0f)
663 Alpha /= 2.0f;
664 }
665
666 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: Alpha);
667 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_aSpriteWeaponCursors[CurWeapon]);
668 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_aCursorOffset[CurWeapon], X: TargetPos.x, Y: TargetPos.y);
669}
670
671void CHud::PrepareAmmoHealthAndArmorQuads()
672{
673 float x = 5;
674 float y = 5;
675 IGraphics::CQuadItem Array[10];
676
677 // ammo of the different weapons
678 for(int i = 0; i < NUM_WEAPONS; ++i)
679 {
680 // 0.6
681 for(int n = 0; n < 10; n++)
682 Array[n] = IGraphics::CQuadItem(x + n * 12, y, 10, 10);
683
684 m_aAmmoOffset[i] = Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
685
686 // 0.7
687 if(i == WEAPON_GRENADE)
688 {
689 // special case for 0.7 grenade
690 for(int n = 0; n < 10; n++)
691 Array[n] = IGraphics::CQuadItem(1 + x + n * 12, y, 10, 10);
692 }
693 else
694 {
695 for(int n = 0; n < 10; n++)
696 Array[n] = IGraphics::CQuadItem(x + n * 12, y, 12, 12);
697 }
698
699 Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
700 }
701
702 // health
703 for(int i = 0; i < 10; ++i)
704 Array[i] = IGraphics::CQuadItem(x + i * 12, y, 10, 10);
705 m_HealthOffset = 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);
710 Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
711
712 // empty health
713 for(int i = 0; i < 10; ++i)
714 Array[i] = IGraphics::CQuadItem(x + i * 12, y, 10, 10);
715 m_EmptyHealthOffset = Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
716
717 // 0.7
718 for(int i = 0; i < 10; ++i)
719 Array[i] = IGraphics::CQuadItem(x + i * 12, y, 12, 12);
720 Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
721
722 // armor meter
723 for(int i = 0; i < 10; ++i)
724 Array[i] = IGraphics::CQuadItem(x + i * 12, y + 12, 10, 10);
725 m_ArmorOffset = Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
726
727 // 0.7
728 for(int i = 0; i < 10; ++i)
729 Array[i] = IGraphics::CQuadItem(x + i * 12, y + 12, 12, 12);
730 Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
731
732 // empty armor meter
733 for(int i = 0; i < 10; ++i)
734 Array[i] = IGraphics::CQuadItem(x + i * 12, y + 12, 10, 10);
735 m_EmptyArmorOffset = Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
736
737 // 0.7
738 for(int i = 0; i < 10; ++i)
739 Array[i] = IGraphics::CQuadItem(x + i * 12, y + 12, 12, 12);
740 Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
741}
742
743void CHud::RenderAmmoHealthAndArmor(const CNetObj_Character *pCharacter)
744{
745 if(!pCharacter)
746 return;
747
748 bool IsSixupGameSkin = GameClient()->m_GameSkin.IsSixup();
749 int QuadOffsetSixup = (IsSixupGameSkin ? 10 : 0);
750
751 if(GameClient()->m_GameInfo.m_HudAmmo)
752 {
753 // ammo display
754 float AmmoOffsetY = GameClient()->m_GameInfo.m_HudHealthArmor ? 24 : 0;
755 int CurWeapon = pCharacter->m_Weapon % NUM_WEAPONS;
756 // 0.7 only
757 if(CurWeapon == WEAPON_NINJA)
758 {
759 if(!GameClient()->m_GameInfo.m_HudDDRace && Client()->IsSixup())
760 {
761 const int Max = g_pData->m_Weapons.m_Ninja.m_Duration * Client()->GameTickSpeed() / 1000;
762 float NinjaProgress = std::clamp(val: pCharacter->m_AmmoCount - Client()->GameTick(Conn: g_Config.m_ClDummy), lo: 0, hi: Max) / (float)Max;
763 RenderNinjaBarPos(x: 5 + 10 * 12, y: 5, Width: 6.f, Height: 24.f, Progress: NinjaProgress);
764 }
765 }
766 else if(CurWeapon >= 0 && GameClient()->m_GameSkin.m_aSpriteWeaponProjectiles[CurWeapon].IsValid())
767 {
768 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_aSpriteWeaponProjectiles[CurWeapon]);
769 if(AmmoOffsetY > 0)
770 {
771 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);
772 }
773 else
774 {
775 Graphics()->RenderQuadContainer(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_aAmmoOffset[CurWeapon] + QuadOffsetSixup, QuadDrawNum: std::clamp(val: pCharacter->m_AmmoCount, lo: 0, hi: 10));
776 }
777 }
778 }
779
780 if(GameClient()->m_GameInfo.m_HudHealthArmor)
781 {
782 // health display
783 const int DisplayHealth = std::min(a: pCharacter->m_Health, b: 10);
784 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpriteHealthFull);
785 Graphics()->RenderQuadContainer(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_HealthOffset + QuadOffsetSixup, QuadDrawNum: DisplayHealth);
786 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpriteHealthEmpty);
787 Graphics()->RenderQuadContainer(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_EmptyHealthOffset + QuadOffsetSixup + DisplayHealth, QuadDrawNum: 10 - DisplayHealth);
788
789 // armor display
790 const int DisplayArmor = std::min(a: pCharacter->m_Armor, b: 10);
791 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpriteArmorFull);
792 Graphics()->RenderQuadContainer(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_ArmorOffset + QuadOffsetSixup, QuadDrawNum: DisplayArmor);
793 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpriteArmorEmpty);
794 Graphics()->RenderQuadContainer(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_ArmorOffset + QuadOffsetSixup + DisplayArmor, QuadDrawNum: 10 - DisplayArmor);
795 }
796}
797
798void CHud::PreparePlayerStateQuads()
799{
800 float x = 5;
801 float y = 5 + 24;
802 IGraphics::CQuadItem Array[10];
803
804 // Quads for displaying the available and used jumps
805 for(int i = 0; i < 10; ++i)
806 Array[i] = IGraphics::CQuadItem(x + i * 12, y, 12, 12);
807 m_AirjumpOffset = Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
808
809 for(int i = 0; i < 10; ++i)
810 Array[i] = IGraphics::CQuadItem(x + i * 12, y, 12, 12);
811 m_AirjumpEmptyOffset = Graphics()->QuadContainerAddQuads(ContainerIndex: m_HudQuadContainerIndex, pArray: Array, Num: 10);
812
813 // Quads for displaying weapons
814 for(int Weapon = 0; Weapon < NUM_WEAPONS; ++Weapon)
815 {
816 const CDataWeaponspec &WeaponSpec = g_pData->m_Weapons.m_aId[Weapon];
817 float ScaleX, ScaleY;
818 Graphics()->GetSpriteScale(pSprite: WeaponSpec.m_pSpriteBody, ScaleX, ScaleY);
819 constexpr float HudWeaponScale = 0.25f;
820 float Width = WeaponSpec.m_VisualSize * ScaleX * HudWeaponScale;
821 float Height = WeaponSpec.m_VisualSize * ScaleY * HudWeaponScale;
822 m_aWeaponOffset[Weapon] = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, Width, Height);
823 }
824
825 // Quads for displaying capabilities
826 m_EndlessJumpOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
827 m_EndlessHookOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
828 m_JetpackOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
829 m_TeleportGrenadeOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
830 m_TeleportGunOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
831 m_TeleportLaserOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
832
833 // Quads for displaying prohibited capabilities
834 m_SoloOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
835 m_CollisionDisabledOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
836 m_HookHitDisabledOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
837 m_HammerHitDisabledOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
838 m_GunHitDisabledOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
839 m_ShotgunHitDisabledOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
840 m_GrenadeHitDisabledOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
841 m_LaserHitDisabledOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
842
843 // Quads for displaying freeze status
844 m_DeepFrozenOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
845 m_LiveFrozenOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
846
847 // Quads for displaying dummy actions
848 m_DummyHammerOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
849 m_DummyCopyOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
850
851 // Quads for displaying team modes
852 m_PracticeModeOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
853 m_LockModeOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
854 m_Team0ModeOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_HudQuadContainerIndex, X: 0.f, Y: 0.f, Width: 12.f, Height: 12.f);
855}
856
857void CHud::RenderPlayerState(const int ClientId)
858{
859 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: 1.f);
860
861 // pCharacter contains the predicted character for local players or the last snap for players who are spectated
862 CCharacterCore *pCharacter = &GameClient()->m_aClients[ClientId].m_Predicted;
863 CNetObj_Character *pPlayer = &GameClient()->m_aClients[ClientId].m_RenderCur;
864 int TotalJumpsToDisplay = 0;
865 if(g_Config.m_ClShowhudJumpsIndicator)
866 {
867 int AvailableJumpsToDisplay;
868 if(GameClient()->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo)
869 {
870 const bool Grounded = Collision()->IsOnGround(Pos: vec2(pPlayer->m_X, pPlayer->m_Y), Size: CCharacterCore::PhysicalSize());
871 int UsedJumps = pCharacter->m_JumpedTotal;
872 if(pCharacter->m_Jumps > 1)
873 {
874 UsedJumps += !Grounded;
875 }
876 else if(pCharacter->m_Jumps == 1)
877 {
878 // If the player has only one jump, each jump is the last one
879 UsedJumps = pPlayer->m_Jumped & 2;
880 }
881 else if(pCharacter->m_Jumps == -1)
882 {
883 // The player has only one ground jump
884 UsedJumps = !Grounded;
885 }
886
887 if(pCharacter->m_EndlessJump && UsedJumps >= absolute(a: pCharacter->m_Jumps))
888 {
889 UsedJumps = absolute(a: pCharacter->m_Jumps) - 1;
890 }
891
892 int UnusedJumps = absolute(a: pCharacter->m_Jumps) - UsedJumps;
893 if(!(pPlayer->m_Jumped & 2) && UnusedJumps <= 0)
894 {
895 // In some edge cases when the player just got another number of jumps, UnusedJumps is not correct
896 UnusedJumps = 1;
897 }
898 TotalJumpsToDisplay = std::clamp(val: absolute(a: pCharacter->m_Jumps), lo: 0, hi: 10);
899 AvailableJumpsToDisplay = std::clamp(val: UnusedJumps, lo: 0, hi: TotalJumpsToDisplay);
900 }
901 else
902 {
903 TotalJumpsToDisplay = AvailableJumpsToDisplay = absolute(a: GameClient()->m_Snap.m_aCharacters[ClientId].m_ExtendedData.m_Jumps);
904 }
905
906 // render available and used jumps
907 int JumpsOffsetY = ((GameClient()->m_GameInfo.m_HudHealthArmor && g_Config.m_ClShowhudHealthAmmo ? 24 : 0) +
908 (GameClient()->m_GameInfo.m_HudAmmo && g_Config.m_ClShowhudHealthAmmo ? 12 : 0));
909 if(JumpsOffsetY > 0)
910 {
911 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudAirjump);
912 Graphics()->RenderQuadContainerEx(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_AirjumpOffset, QuadDrawNum: AvailableJumpsToDisplay, X: 0, Y: JumpsOffsetY);
913 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudAirjumpEmpty);
914 Graphics()->RenderQuadContainerEx(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_AirjumpEmptyOffset + AvailableJumpsToDisplay, QuadDrawNum: TotalJumpsToDisplay - AvailableJumpsToDisplay, X: 0, Y: JumpsOffsetY);
915 }
916 else
917 {
918 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudAirjump);
919 Graphics()->RenderQuadContainer(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_AirjumpOffset, QuadDrawNum: AvailableJumpsToDisplay);
920 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudAirjumpEmpty);
921 Graphics()->RenderQuadContainer(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_AirjumpEmptyOffset + AvailableJumpsToDisplay, QuadDrawNum: TotalJumpsToDisplay - AvailableJumpsToDisplay);
922 }
923 }
924
925 float x = 5 + 12;
926 float y = (5 + 12 + (GameClient()->m_GameInfo.m_HudHealthArmor && g_Config.m_ClShowhudHealthAmmo ? 24 : 0) +
927 (GameClient()->m_GameInfo.m_HudAmmo && g_Config.m_ClShowhudHealthAmmo ? 12 : 0));
928
929 // render weapons
930 {
931 constexpr float aWeaponWidth[NUM_WEAPONS] = {16, 12, 12, 12, 12, 12};
932 constexpr float aWeaponInitialOffset[NUM_WEAPONS] = {-3, -4, -1, -1, -2, -4};
933 bool InitialOffsetAdded = false;
934 for(int Weapon = 0; Weapon < NUM_WEAPONS; ++Weapon)
935 {
936 if(!pCharacter->m_aWeapons[Weapon].m_Got)
937 continue;
938 if(!InitialOffsetAdded)
939 {
940 x += aWeaponInitialOffset[Weapon];
941 InitialOffsetAdded = true;
942 }
943 if(pPlayer->m_Weapon != Weapon)
944 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
945 Graphics()->QuadsSetRotation(Angle: pi * 7 / 4);
946 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_aSpritePickupWeapons[Weapon]);
947 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_aWeaponOffset[Weapon], X: x, Y: y);
948 Graphics()->QuadsSetRotation(Angle: 0);
949 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
950 x += aWeaponWidth[Weapon];
951 }
952 if(pCharacter->m_aWeapons[WEAPON_NINJA].m_Got)
953 {
954 const int Max = g_pData->m_Weapons.m_Ninja.m_Duration * Client()->GameTickSpeed() / 1000;
955 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;
956 if(NinjaProgress > 0.0f && GameClient()->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo)
957 {
958 RenderNinjaBarPos(x, y: y - 12, Width: 6.f, Height: 24.f, Progress: NinjaProgress);
959 }
960 }
961 }
962
963 // render capabilities
964 x = 5;
965 y += 12;
966 if(TotalJumpsToDisplay > 0)
967 {
968 y += 12;
969 }
970 bool HasCapabilities = false;
971 if(pCharacter->m_EndlessJump)
972 {
973 HasCapabilities = true;
974 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudEndlessJump);
975 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_EndlessJumpOffset, X: x, Y: y);
976 x += 12;
977 }
978 if(pCharacter->m_EndlessHook)
979 {
980 HasCapabilities = true;
981 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudEndlessHook);
982 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_EndlessHookOffset, X: x, Y: y);
983 x += 12;
984 }
985 if(pCharacter->m_Jetpack)
986 {
987 HasCapabilities = true;
988 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudJetpack);
989 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_JetpackOffset, X: x, Y: y);
990 x += 12;
991 }
992 if(pCharacter->m_HasTelegunGun && pCharacter->m_aWeapons[WEAPON_GUN].m_Got)
993 {
994 HasCapabilities = true;
995 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudTeleportGun);
996 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_TeleportGunOffset, X: x, Y: y);
997 x += 12;
998 }
999 if(pCharacter->m_HasTelegunGrenade && pCharacter->m_aWeapons[WEAPON_GRENADE].m_Got)
1000 {
1001 HasCapabilities = true;
1002 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudTeleportGrenade);
1003 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_TeleportGrenadeOffset, X: x, Y: y);
1004 x += 12;
1005 }
1006 if(pCharacter->m_HasTelegunLaser && pCharacter->m_aWeapons[WEAPON_LASER].m_Got)
1007 {
1008 HasCapabilities = true;
1009 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudTeleportLaser);
1010 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_TeleportLaserOffset, X: x, Y: y);
1011 }
1012
1013 // render prohibited capabilities
1014 x = 5;
1015 if(HasCapabilities)
1016 {
1017 y += 12;
1018 }
1019 bool HasProhibitedCapabilities = false;
1020 if(pCharacter->m_Solo)
1021 {
1022 HasProhibitedCapabilities = true;
1023 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudSolo);
1024 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_SoloOffset, X: x, Y: y);
1025 x += 12;
1026 }
1027 if(pCharacter->m_CollisionDisabled)
1028 {
1029 HasProhibitedCapabilities = true;
1030 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudCollisionDisabled);
1031 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_CollisionDisabledOffset, X: x, Y: y);
1032 x += 12;
1033 }
1034 if(pCharacter->m_HookHitDisabled)
1035 {
1036 HasProhibitedCapabilities = true;
1037 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudHookHitDisabled);
1038 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_HookHitDisabledOffset, X: x, Y: y);
1039 x += 12;
1040 }
1041 if(pCharacter->m_HammerHitDisabled)
1042 {
1043 HasProhibitedCapabilities = true;
1044 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudHammerHitDisabled);
1045 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_HammerHitDisabledOffset, X: x, Y: y);
1046 x += 12;
1047 }
1048 if((pCharacter->m_GrenadeHitDisabled && pCharacter->m_HasTelegunGun && pCharacter->m_aWeapons[WEAPON_GUN].m_Got))
1049 {
1050 HasProhibitedCapabilities = true;
1051 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudGunHitDisabled);
1052 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_LaserHitDisabledOffset, X: x, Y: y);
1053 x += 12;
1054 }
1055 if((pCharacter->m_ShotgunHitDisabled && pCharacter->m_aWeapons[WEAPON_SHOTGUN].m_Got))
1056 {
1057 HasProhibitedCapabilities = true;
1058 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudShotgunHitDisabled);
1059 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_ShotgunHitDisabledOffset, X: x, Y: y);
1060 x += 12;
1061 }
1062 if((pCharacter->m_GrenadeHitDisabled && pCharacter->m_aWeapons[WEAPON_GRENADE].m_Got))
1063 {
1064 HasProhibitedCapabilities = true;
1065 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudGrenadeHitDisabled);
1066 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_GrenadeHitDisabledOffset, X: x, Y: y);
1067 x += 12;
1068 }
1069 if((pCharacter->m_LaserHitDisabled && pCharacter->m_aWeapons[WEAPON_LASER].m_Got))
1070 {
1071 HasProhibitedCapabilities = true;
1072 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudLaserHitDisabled);
1073 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_LaserHitDisabledOffset, X: x, Y: y);
1074 }
1075
1076 // render dummy actions and freeze state
1077 x = 5;
1078 if(HasProhibitedCapabilities)
1079 {
1080 y += 12;
1081 }
1082 if(GameClient()->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo && GameClient()->m_Snap.m_aCharacters[ClientId].m_ExtendedData.m_Flags & CHARACTERFLAG_LOCK_MODE)
1083 {
1084 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudLockMode);
1085 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_LockModeOffset, X: x, Y: y);
1086 x += 12;
1087 }
1088 if(GameClient()->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo && GameClient()->m_Snap.m_aCharacters[ClientId].m_ExtendedData.m_Flags & CHARACTERFLAG_PRACTICE_MODE)
1089 {
1090 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudPracticeMode);
1091 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_PracticeModeOffset, X: x, Y: y);
1092 x += 12;
1093 }
1094 if(GameClient()->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo && GameClient()->m_Snap.m_aCharacters[ClientId].m_ExtendedData.m_Flags & CHARACTERFLAG_TEAM0_MODE)
1095 {
1096 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudTeam0Mode);
1097 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_Team0ModeOffset, X: x, Y: y);
1098 x += 12;
1099 }
1100 if(pCharacter->m_DeepFrozen)
1101 {
1102 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudDeepFrozen);
1103 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_DeepFrozenOffset, X: x, Y: y);
1104 x += 12;
1105 }
1106 if(pCharacter->m_LiveFrozen)
1107 {
1108 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudLiveFrozen);
1109 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_LiveFrozenOffset, X: x, Y: y);
1110 }
1111}
1112
1113void CHud::RenderNinjaBarPos(const float x, float y, const float Width, const float Height, float Progress, const float Alpha)
1114{
1115 Progress = std::clamp(val: Progress, lo: 0.0f, hi: 1.0f);
1116
1117 // what percentage of the end pieces is used for the progress indicator and how much is the rest
1118 // half of the ends are used for the progress display
1119 const float RestPct = 0.5f;
1120 const float ProgPct = 0.5f;
1121
1122 const float EndHeight = Width; // to keep the correct scale - the width of the sprite is as long as the height
1123 const float BarWidth = Width;
1124 const float WholeBarHeight = Height;
1125 const float MiddleBarHeight = WholeBarHeight - (EndHeight * 2.0f);
1126 const float EndProgressHeight = EndHeight * ProgPct;
1127 const float EndRestHeight = EndHeight * RestPct;
1128 const float ProgressBarHeight = WholeBarHeight - (EndProgressHeight * 2.0f);
1129 const float EndProgressProportion = EndProgressHeight / ProgressBarHeight;
1130 const float MiddleProgressProportion = MiddleBarHeight / ProgressBarHeight;
1131
1132 // beginning piece
1133 float BeginningPieceProgress = 1;
1134 if(Progress <= 1)
1135 {
1136 if(Progress <= (EndProgressProportion + MiddleProgressProportion))
1137 {
1138 BeginningPieceProgress = 0;
1139 }
1140 else
1141 {
1142 BeginningPieceProgress = (Progress - EndProgressProportion - MiddleProgressProportion) / EndProgressProportion;
1143 }
1144 }
1145 // empty
1146 Graphics()->WrapClamp();
1147 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudNinjaBarEmptyRight);
1148 Graphics()->QuadsBegin();
1149 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: Alpha);
1150 // Subset: btm_r, top_r, top_m, btm_m | it is mirrored on the horizontal axe and rotated 90 degrees counterclockwise
1151 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);
1152 IGraphics::CQuadItem QuadEmptyBeginning(x, y, BarWidth, EndRestHeight + EndProgressHeight * (1.0f - BeginningPieceProgress));
1153 Graphics()->QuadsDrawTL(pArray: &QuadEmptyBeginning, Num: 1);
1154 Graphics()->QuadsEnd();
1155 // full
1156 if(BeginningPieceProgress > 0.0f)
1157 {
1158 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudNinjaBarFullLeft);
1159 Graphics()->QuadsBegin();
1160 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: Alpha);
1161 // Subset: btm_m, top_m, top_r, btm_r | it is rotated 90 degrees clockwise
1162 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);
1163 IGraphics::CQuadItem QuadFullBeginning(x, y + (EndRestHeight + EndProgressHeight * (1.0f - BeginningPieceProgress)), BarWidth, EndProgressHeight * BeginningPieceProgress);
1164 Graphics()->QuadsDrawTL(pArray: &QuadFullBeginning, Num: 1);
1165 Graphics()->QuadsEnd();
1166 }
1167
1168 // middle piece
1169 y += EndHeight;
1170
1171 float MiddlePieceProgress = 1;
1172 if(Progress <= EndProgressProportion + MiddleProgressProportion)
1173 {
1174 if(Progress <= EndProgressProportion)
1175 {
1176 MiddlePieceProgress = 0;
1177 }
1178 else
1179 {
1180 MiddlePieceProgress = (Progress - EndProgressProportion) / MiddleProgressProportion;
1181 }
1182 }
1183
1184 const float FullMiddleBarHeight = MiddleBarHeight * MiddlePieceProgress;
1185 const float EmptyMiddleBarHeight = MiddleBarHeight - FullMiddleBarHeight;
1186
1187 // empty ninja bar
1188 if(EmptyMiddleBarHeight > 0.0f)
1189 {
1190 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudNinjaBarEmpty);
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(EmptyMiddleBarHeight <= EndHeight)
1195 {
1196 // prevent pixel puree, select only a small slice
1197 // Subset: btm_r, top_r, top_m, btm_m | it is mirrored on the horizontal axe and rotated 90 degrees counterclockwise
1198 Graphics()->QuadsSetSubsetFree(x0: 1, y0: 1, x1: 1, y1: 0, x2: 1.0f - (EmptyMiddleBarHeight / EndHeight), y2: 0, x3: 1.0f - (EmptyMiddleBarHeight / EndHeight), y3: 1);
1199 }
1200 else
1201 {
1202 // Subset: btm_r, top_r, top_l, btm_l | it is mirrored on the horizontal axe and rotated 90 degrees counterclockwise
1203 Graphics()->QuadsSetSubsetFree(x0: 1, y0: 1, x1: 1, y1: 0, x2: 0, y2: 0, x3: 0, y3: 1);
1204 }
1205 IGraphics::CQuadItem QuadEmpty(x, y, BarWidth, EmptyMiddleBarHeight);
1206 Graphics()->QuadsDrawTL(pArray: &QuadEmpty, Num: 1);
1207 Graphics()->QuadsEnd();
1208 }
1209
1210 // full ninja bar
1211 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudNinjaBarFull);
1212 Graphics()->QuadsBegin();
1213 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: Alpha);
1214 // select the middle portion of the sprite so we don't get edge bleeding
1215 if(FullMiddleBarHeight <= EndHeight)
1216 {
1217 // prevent pixel puree, select only a small slice
1218 // Subset: btm_m, top_m, top_r, btm_r | it is rotated 90 degrees clockwise
1219 Graphics()->QuadsSetSubsetFree(x0: 1.0f - (FullMiddleBarHeight / EndHeight), y0: 1, x1: 1.0f - (FullMiddleBarHeight / EndHeight), y1: 0, x2: 1, y2: 0, x3: 1, y3: 1);
1220 }
1221 else
1222 {
1223 // Subset: btm_l, top_l, top_r, btm_r | it is rotated 90 degrees clockwise
1224 Graphics()->QuadsSetSubsetFree(x0: 0, y0: 1, x1: 0, y1: 0, x2: 1, y2: 0, x3: 1, y3: 1);
1225 }
1226 IGraphics::CQuadItem QuadFull(x, y + EmptyMiddleBarHeight, BarWidth, FullMiddleBarHeight);
1227 Graphics()->QuadsDrawTL(pArray: &QuadFull, Num: 1);
1228 Graphics()->QuadsEnd();
1229
1230 // ending piece
1231 y += MiddleBarHeight;
1232 float EndingPieceProgress = 1;
1233 if(Progress <= EndProgressProportion)
1234 {
1235 EndingPieceProgress = Progress / EndProgressProportion;
1236 }
1237 // empty
1238 if(EndingPieceProgress < 1.0f)
1239 {
1240 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudNinjaBarEmptyRight);
1241 Graphics()->QuadsBegin();
1242 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: Alpha);
1243 // Subset: btm_l, top_l, top_m, btm_m | it is rotated 90 degrees clockwise
1244 Graphics()->QuadsSetSubsetFree(x0: 0, y0: 1, x1: 0, y1: 0, x2: ProgPct - ProgPct * EndingPieceProgress, y2: 0, x3: ProgPct - ProgPct * EndingPieceProgress, y3: 1);
1245 IGraphics::CQuadItem QuadEmptyEnding(x, y, BarWidth, EndProgressHeight * (1.0f - EndingPieceProgress));
1246 Graphics()->QuadsDrawTL(pArray: &QuadEmptyEnding, Num: 1);
1247 Graphics()->QuadsEnd();
1248 }
1249 // full
1250 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudNinjaBarFullLeft);
1251 Graphics()->QuadsBegin();
1252 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: Alpha);
1253 // Subset: btm_m, top_m, top_l, btm_l | it is mirrored on the horizontal axe and rotated 90 degrees counterclockwise
1254 Graphics()->QuadsSetSubsetFree(x0: RestPct + ProgPct * EndingPieceProgress, y0: 1, x1: RestPct + ProgPct * EndingPieceProgress, y1: 0, x2: 0, y2: 0, x3: 0, y3: 1);
1255 IGraphics::CQuadItem QuadFullEnding(x, y + (EndProgressHeight * (1.0f - EndingPieceProgress)), BarWidth, EndRestHeight + EndProgressHeight * EndingPieceProgress);
1256 Graphics()->QuadsDrawTL(pArray: &QuadFullEnding, Num: 1);
1257 Graphics()->QuadsEnd();
1258
1259 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
1260 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: 1.f);
1261 Graphics()->WrapNormal();
1262}
1263
1264void CHud::RenderSpectatorCount()
1265{
1266 if(!g_Config.m_ClShowhudSpectatorCount)
1267 {
1268 return;
1269 }
1270
1271 int Count = 0;
1272 if(Client()->IsSixup())
1273 {
1274 for(int i = 0; i < MAX_CLIENTS; i++)
1275 {
1276 if(i == GameClient()->m_aLocalIds[0] || (GameClient()->Client()->DummyConnected() && i == GameClient()->m_aLocalIds[1]))
1277 continue;
1278
1279 if(Client()->m_TranslationContext.m_aClients[i].m_PlayerFlags7 & protocol7::PLAYERFLAG_WATCHING)
1280 {
1281 Count++;
1282 }
1283 }
1284 }
1285 else
1286 {
1287 const CNetObj_SpectatorCount *pSpectatorCount = GameClient()->m_Snap.m_pSpectatorCount;
1288 if(!pSpectatorCount)
1289 {
1290 m_LastSpectatorCountTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
1291 return;
1292 }
1293 Count = pSpectatorCount->m_NumSpectators;
1294 }
1295
1296 if(Count == 0)
1297 {
1298 m_LastSpectatorCountTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
1299 return;
1300 }
1301
1302 // 1 second delay
1303 if(Client()->GameTick(Conn: g_Config.m_ClDummy) < m_LastSpectatorCountTick + Client()->GameTickSpeed())
1304 return;
1305
1306 char aBuf[16];
1307 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", Count);
1308
1309 const float Fontsize = 6.0f;
1310 const float BoxHeight = 14.f;
1311 const float BoxWidth = 13.f + TextRender()->TextWidth(Size: Fontsize, pText: aBuf);
1312
1313 float StartX = m_Width - BoxWidth;
1314 float StartY = 285.0f - BoxHeight - 4; // 4 units distance to the next display;
1315 if(g_Config.m_ClShowhudPlayerPosition || g_Config.m_ClShowhudPlayerSpeed || g_Config.m_ClShowhudPlayerAngle)
1316 {
1317 StartY -= 4;
1318 }
1319 StartY -= GetMovementInformationBoxHeight();
1320
1321 if(g_Config.m_ClShowhudScore)
1322 {
1323 StartY -= 56;
1324 }
1325
1326 if(g_Config.m_ClShowhudDummyActions && !(GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER) && Client()->DummyConnected())
1327 {
1328 StartY = StartY - 29.0f - 4; // dummy actions height and padding
1329 }
1330
1331 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);
1332
1333 float y = StartY + BoxHeight / 3;
1334 float x = StartX + 2;
1335
1336 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1337 TextRender()->Text(x, y, Size: Fontsize, pText: FontIcon::EYE, LineWidth: -1.0f);
1338 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1339 TextRender()->Text(x: x + Fontsize + 3.f, y, Size: Fontsize, pText: aBuf, LineWidth: -1.0f);
1340}
1341
1342void CHud::RenderDummyActions()
1343{
1344 if(!g_Config.m_ClShowhudDummyActions || (GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER) || !Client()->DummyConnected())
1345 {
1346 return;
1347 }
1348 // render small dummy actions hud
1349 const float BoxHeight = 29.0f;
1350 const float BoxWidth = 16.0f;
1351
1352 float StartX = m_Width - BoxWidth;
1353 float StartY = 285.0f - BoxHeight - 4; // 4 units distance to the next display;
1354 if(g_Config.m_ClShowhudPlayerPosition || g_Config.m_ClShowhudPlayerSpeed || g_Config.m_ClShowhudPlayerAngle)
1355 {
1356 StartY -= 4;
1357 }
1358 StartY -= GetMovementInformationBoxHeight();
1359
1360 if(g_Config.m_ClShowhudScore)
1361 {
1362 StartY -= 56;
1363 }
1364
1365 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);
1366
1367 float y = StartY + 2;
1368 float x = StartX + 2;
1369 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
1370 if(g_Config.m_ClDummyHammer)
1371 {
1372 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
1373 }
1374 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudDummyHammer);
1375 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_DummyHammerOffset, X: x, Y: y);
1376 y += 13;
1377 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
1378 if(g_Config.m_ClDummyCopyMoves)
1379 {
1380 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
1381 }
1382 Graphics()->TextureSet(Texture: GameClient()->m_HudSkin.m_SpriteHudDummyCopy);
1383 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_HudQuadContainerIndex, QuadOffset: m_DummyCopyOffset, X: x, Y: y);
1384}
1385
1386inline int CHud::GetDigitsIndex(int Value, int Max)
1387{
1388 if(Value < 0)
1389 {
1390 Value *= -1;
1391 }
1392 int DigitsIndex = std::log10(x: (Value ? Value : 1));
1393 if(DigitsIndex > Max)
1394 {
1395 DigitsIndex = Max;
1396 }
1397 if(DigitsIndex < 0)
1398 {
1399 DigitsIndex = 0;
1400 }
1401 return DigitsIndex;
1402}
1403
1404inline float CHud::GetMovementInformationBoxHeight()
1405{
1406 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))
1407 return g_Config.m_ClShowhudPlayerPosition ? 3.0f * MOVEMENT_INFORMATION_LINE_HEIGHT + 2.0f : 0.0f;
1408 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;
1409 if(g_Config.m_ClShowhudPlayerPosition || g_Config.m_ClShowhudPlayerSpeed || g_Config.m_ClShowhudPlayerAngle)
1410 {
1411 BoxHeight += 2.0f;
1412 }
1413 return BoxHeight;
1414}
1415
1416void CHud::UpdateMovementInformationTextContainer(STextContainerIndex &TextContainer, float FontSize, float Value, float &PrevValue)
1417{
1418 Value = std::round(x: Value * 100.0f) / 100.0f; // Round to 2dp
1419 if(TextContainer.Valid() && PrevValue == Value)
1420 return;
1421 PrevValue = Value;
1422
1423 char aBuf[128];
1424 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%.2f", Value);
1425
1426 CTextCursor Cursor;
1427 Cursor.m_FontSize = FontSize;
1428 TextRender()->RecreateTextContainer(TextContainerIndex&: TextContainer, pCursor: &Cursor, pText: aBuf);
1429}
1430
1431void CHud::RenderMovementInformationTextContainer(STextContainerIndex &TextContainer, const ColorRGBA &Color, float X, float Y)
1432{
1433 if(TextContainer.Valid())
1434 {
1435 TextRender()->RenderTextContainer(TextContainerIndex: TextContainer, TextColor: Color, TextOutlineColor: TextRender()->DefaultTextOutlineColor(), X: X - TextRender()->GetBoundingBoxTextContainer(TextContainerIndex: TextContainer).m_W, Y);
1436 }
1437}
1438
1439CHud::CMovementInformation CHud::GetMovementInformation(int ClientId, int Conn) const
1440{
1441 CMovementInformation Out;
1442 if(ClientId == SPEC_FREEVIEW)
1443 {
1444 Out.m_Pos = GameClient()->m_Camera.m_Center / 32.0f;
1445 }
1446 else if(GameClient()->m_aClients[ClientId].m_SpecCharPresent)
1447 {
1448 Out.m_Pos = GameClient()->m_aClients[ClientId].m_SpecChar / 32.0f;
1449 }
1450 else
1451 {
1452 const CNetObj_Character *pPrevChar = &GameClient()->m_Snap.m_aCharacters[ClientId].m_Prev;
1453 const CNetObj_Character *pCurChar = &GameClient()->m_Snap.m_aCharacters[ClientId].m_Cur;
1454 const float IntraTick = Client()->IntraGameTick(Conn);
1455
1456 // To make the player position relative to blocks we need to divide by the block size
1457 Out.m_Pos = mix(a: vec2(pPrevChar->m_X, pPrevChar->m_Y), b: vec2(pCurChar->m_X, pCurChar->m_Y), amount: IntraTick) / 32.0f;
1458
1459 const vec2 Vel = mix(a: vec2(pPrevChar->m_VelX, pPrevChar->m_VelY), b: vec2(pCurChar->m_VelX, pCurChar->m_VelY), amount: IntraTick);
1460
1461 float VelspeedX = Vel.x / 256.0f * Client()->GameTickSpeed();
1462 if(Vel.x >= -1.0f && Vel.x <= 1.0f)
1463 {
1464 VelspeedX = 0.0f;
1465 }
1466 float VelspeedY = Vel.y / 256.0f * Client()->GameTickSpeed();
1467 if(Vel.y >= -128.0f && Vel.y <= 128.0f)
1468 {
1469 VelspeedY = 0.0f;
1470 }
1471 // We show the speed in Blocks per Second (Bps) and therefore have to divide by the block size
1472 Out.m_Speed.x = VelspeedX / 32.0f;
1473 float VelspeedLength = length(a: vec2(Vel.x, Vel.y) / 256.0f) * Client()->GameTickSpeed();
1474 // Todo: Use Velramp tuning of each individual player
1475 // Since these tuning parameters are almost never changed, the default values are sufficient in most cases
1476 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);
1477 Out.m_Speed.x *= Ramp;
1478 Out.m_Speed.y = VelspeedY / 32.0f;
1479
1480 float Angle = GameClient()->m_Players.GetPlayerTargetAngle(pPrevChar, pPlayerChar: pCurChar, ClientId, Intra: IntraTick);
1481 if(Angle < 0.0f)
1482 {
1483 Angle += 2.0f * pi;
1484 }
1485 Out.m_Angle = Angle * 180.0f / pi;
1486 }
1487 return Out;
1488}
1489
1490void CHud::RenderMovementInformation()
1491{
1492 const int ClientId = GameClient()->m_Snap.m_SpecInfo.m_Active ? GameClient()->m_Snap.m_SpecInfo.m_SpectatorId : GameClient()->m_Snap.m_LocalClientId;
1493 const bool PosOnly = ClientId == SPEC_FREEVIEW || (GameClient()->m_aClients[ClientId].m_SpecCharPresent);
1494 // Draw the information depending on settings: Position, speed and target angle
1495 // This display is only to present the available information from the last snapshot, not to interpolate or predict
1496 if(!g_Config.m_ClShowhudPlayerPosition && (PosOnly || (!g_Config.m_ClShowhudPlayerSpeed && !g_Config.m_ClShowhudPlayerAngle)))
1497 {
1498 return;
1499 }
1500 const float LineSpacer = 1.0f; // above and below each entry
1501 const float Fontsize = 6.0f;
1502
1503 float BoxHeight = GetMovementInformationBoxHeight();
1504 const float BoxWidth = 62.0f;
1505
1506 float StartX = m_Width - BoxWidth;
1507 float StartY = 285.0f - BoxHeight - 4.0f; // 4 units distance to the next display;
1508 if(g_Config.m_ClShowhudScore)
1509 {
1510 StartY -= 56.0f;
1511 }
1512
1513 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);
1514
1515 const CMovementInformation Info = GetMovementInformation(ClientId, Conn: g_Config.m_ClDummy);
1516
1517 float y = StartY + LineSpacer * 2.0f;
1518 const float LeftX = StartX + 2.0f;
1519 const float RightX = m_Width - 2.0f;
1520
1521 if(g_Config.m_ClShowhudPlayerPosition)
1522 {
1523 TextRender()->Text(x: LeftX, y, Size: Fontsize, pText: Localize(pStr: "Position:"), LineWidth: -1.0f);
1524 y += MOVEMENT_INFORMATION_LINE_HEIGHT;
1525
1526 TextRender()->Text(x: LeftX, y, Size: Fontsize, pText: "X:", LineWidth: -1.0f);
1527 UpdateMovementInformationTextContainer(TextContainer&: m_aPlayerPositionContainers[0], FontSize: Fontsize, Value: Info.m_Pos.x, PrevValue&: m_aPlayerPrevPosition[0]);
1528 RenderMovementInformationTextContainer(TextContainer&: m_aPlayerPositionContainers[0], Color: TextRender()->DefaultTextColor(), X: RightX, Y: y);
1529 y += MOVEMENT_INFORMATION_LINE_HEIGHT;
1530
1531 TextRender()->Text(x: LeftX, y, Size: Fontsize, pText: "Y:", LineWidth: -1.0f);
1532 UpdateMovementInformationTextContainer(TextContainer&: m_aPlayerPositionContainers[1], FontSize: Fontsize, Value: Info.m_Pos.y, PrevValue&: m_aPlayerPrevPosition[1]);
1533 RenderMovementInformationTextContainer(TextContainer&: m_aPlayerPositionContainers[1], Color: TextRender()->DefaultTextColor(), X: RightX, Y: y);
1534 y += MOVEMENT_INFORMATION_LINE_HEIGHT;
1535 }
1536
1537 if(PosOnly)
1538 return;
1539
1540 if(g_Config.m_ClShowhudPlayerSpeed)
1541 {
1542 TextRender()->Text(x: LeftX, y, Size: Fontsize, pText: Localize(pStr: "Speed:"), LineWidth: -1.0f);
1543 y += MOVEMENT_INFORMATION_LINE_HEIGHT;
1544
1545 const char aaCoordinates[][4] = {"X:", "Y:"};
1546 for(int i = 0; i < 2; i++)
1547 {
1548 ColorRGBA Color(1.0f, 1.0f, 1.0f, 1.0f);
1549 if(m_aLastPlayerSpeedChange[i] == ESpeedChange::INCREASE)
1550 Color = ColorRGBA(0.0f, 1.0f, 0.0f, 1.0f);
1551 if(m_aLastPlayerSpeedChange[i] == ESpeedChange::DECREASE)
1552 Color = ColorRGBA(1.0f, 0.5f, 0.5f, 1.0f);
1553 TextRender()->Text(x: LeftX, y, Size: Fontsize, pText: aaCoordinates[i], LineWidth: -1.0f);
1554 UpdateMovementInformationTextContainer(TextContainer&: m_aPlayerSpeedTextContainers[i], FontSize: Fontsize, Value: i == 0 ? Info.m_Speed.x : Info.m_Speed.y, PrevValue&: m_aPlayerPrevSpeed[i]);
1555 RenderMovementInformationTextContainer(TextContainer&: m_aPlayerSpeedTextContainers[i], Color, X: RightX, Y: y);
1556 y += MOVEMENT_INFORMATION_LINE_HEIGHT;
1557 }
1558
1559 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
1560 }
1561
1562 if(g_Config.m_ClShowhudPlayerAngle)
1563 {
1564 TextRender()->Text(x: LeftX, y, Size: Fontsize, pText: Localize(pStr: "Angle:"), LineWidth: -1.0f);
1565 y += MOVEMENT_INFORMATION_LINE_HEIGHT;
1566
1567 UpdateMovementInformationTextContainer(TextContainer&: m_PlayerAngleTextContainerIndex, FontSize: Fontsize, Value: Info.m_Angle, PrevValue&: m_PlayerPrevAngle);
1568 RenderMovementInformationTextContainer(TextContainer&: m_PlayerAngleTextContainerIndex, Color: TextRender()->DefaultTextColor(), X: RightX, Y: y);
1569 }
1570}
1571
1572void CHud::RenderSpectatorHud()
1573{
1574 if(!g_Config.m_ClShowhudSpectator)
1575 return;
1576
1577 // draw the box
1578 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);
1579
1580 // draw the text
1581 char aBuf[128];
1582 if(GameClient()->m_MultiViewActivated)
1583 {
1584 str_copy(dst&: aBuf, src: Localize(pStr: "Multi-View"));
1585 }
1586 else if(GameClient()->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW)
1587 {
1588 const auto &Player = GameClient()->m_aClients[GameClient()->m_Snap.m_SpecInfo.m_SpectatorId];
1589 if(g_Config.m_ClShowIds)
1590 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Following %d: %s", pContext: "Spectating"), Player.ClientId(), Player.m_aName);
1591 else
1592 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Following %s", pContext: "Spectating"), Player.m_aName);
1593 }
1594 else
1595 {
1596 str_copy(dst&: aBuf, src: Localize(pStr: "Free-View"));
1597 }
1598 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);
1599
1600 // draw the camera info
1601 if(Client()->State() != IClient::STATE_DEMOPLAYBACK && GameClient()->m_Camera.SpectatingPlayer() && GameClient()->m_Camera.CanUseAutoSpecCamera() && g_Config.m_ClSpecAutoSync)
1602 {
1603 bool AutoSpecCameraEnabled = GameClient()->m_Camera.m_AutoSpecCamera;
1604 const char *pLabelText = Localize(pStr: "AUTO", pContext: "Spectating Camera Mode Icon");
1605 const float TextWidth = TextRender()->TextWidth(Size: 6.0f, pText: pLabelText);
1606
1607 constexpr float RightMargin = 4.0f;
1608 constexpr float IconWidth = 6.0f;
1609 constexpr float Padding = 3.0f;
1610 const float TagWidth = IconWidth + TextWidth + Padding * 3.0f;
1611 const float TagX = m_Width - RightMargin - TagWidth;
1612 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);
1613 TextRender()->TextColor(r: 1, g: 1, b: 1, a: AutoSpecCameraEnabled ? 1.0f : 0.65f);
1614 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1615 TextRender()->Text(x: TagX + Padding, y: m_Height - 10.0f, Size: 6.0f, pText: FontIcon::CAMERA, LineWidth: -1.0f);
1616 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1617 TextRender()->Text(x: TagX + Padding + IconWidth + Padding, y: m_Height - 10.0f, Size: 6.0f, pText: pLabelText, LineWidth: -1.0f);
1618 TextRender()->TextColor(r: 1, g: 1, b: 1, a: 1);
1619 }
1620}
1621
1622void CHud::RenderLocalTime(float x)
1623{
1624 if(!g_Config.m_ClShowLocalTimeAlways && !GameClient()->m_Scoreboard.IsActive())
1625 return;
1626
1627 // draw the box
1628 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);
1629
1630 // draw the text
1631 char aTimeStr[6];
1632 str_timestamp_format(buffer: aTimeStr, buffer_size: sizeof(aTimeStr), format: "%H:%M");
1633 TextRender()->Text(x: x - 25.0f, y: (12.5f - 5.f) / 2.f, Size: 5.0f, pText: aTimeStr, LineWidth: -1.0f);
1634}
1635
1636void CHud::OnNewSnapshot()
1637{
1638 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
1639 return;
1640 if(!GameClient()->m_Snap.m_pGameInfoObj)
1641 return;
1642
1643 int ClientId = -1;
1644 if(GameClient()->m_Snap.m_pLocalCharacter && !GameClient()->m_Snap.m_SpecInfo.m_Active && !(GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
1645 ClientId = GameClient()->m_Snap.m_LocalClientId;
1646 else if(GameClient()->m_Snap.m_SpecInfo.m_Active)
1647 ClientId = GameClient()->m_Snap.m_SpecInfo.m_SpectatorId;
1648
1649 if(ClientId == -1)
1650 return;
1651
1652 const CNetObj_Character *pPrevChar = &GameClient()->m_Snap.m_aCharacters[ClientId].m_Prev;
1653 const CNetObj_Character *pCurChar = &GameClient()->m_Snap.m_aCharacters[ClientId].m_Cur;
1654 const float IntraTick = Client()->IntraGameTick(Conn: g_Config.m_ClDummy);
1655 ivec2 Vel = mix(a: ivec2(pPrevChar->m_VelX, pPrevChar->m_VelY), b: ivec2(pCurChar->m_VelX, pCurChar->m_VelY), amount: IntraTick);
1656
1657 CCharacter *pChar = GameClient()->m_PredictedWorld.GetCharacterById(Id: ClientId);
1658 if(pChar && pChar->IsGrounded())
1659 Vel.y = 0;
1660
1661 int aVels[2] = {Vel.x, Vel.y};
1662
1663 for(int i = 0; i < 2; i++)
1664 {
1665 int AbsVel = abs(x: aVels[i]);
1666 if(AbsVel > m_aPlayerSpeed[i])
1667 {
1668 m_aLastPlayerSpeedChange[i] = ESpeedChange::INCREASE;
1669 }
1670 if(AbsVel < m_aPlayerSpeed[i])
1671 {
1672 m_aLastPlayerSpeedChange[i] = ESpeedChange::DECREASE;
1673 }
1674 if(AbsVel < 2)
1675 {
1676 m_aLastPlayerSpeedChange[i] = ESpeedChange::NONE;
1677 }
1678 m_aPlayerSpeed[i] = AbsVel;
1679 }
1680}
1681
1682void CHud::OnRender()
1683{
1684 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
1685 return;
1686
1687 if(!GameClient()->m_Snap.m_pGameInfoObj)
1688 return;
1689
1690 m_Width = 300.0f * Graphics()->ScreenAspect();
1691 m_Height = 300.0f;
1692 Graphics()->MapScreen(TopLeftX: 0.0f, TopLeftY: 0.0f, BottomRightX: m_Width, BottomRightY: m_Height);
1693
1694#if defined(CONF_VIDEORECORDER)
1695 if((IVideo::Current() && g_Config.m_ClVideoShowhud) || (!IVideo::Current() && g_Config.m_ClShowhud))
1696#else
1697 if(g_Config.m_ClShowhud)
1698#endif
1699 {
1700 if(GameClient()->m_Snap.m_pLocalCharacter && !GameClient()->m_Snap.m_SpecInfo.m_Active && !(GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
1701 {
1702 if(g_Config.m_ClShowhudHealthAmmo)
1703 {
1704 RenderAmmoHealthAndArmor(pCharacter: GameClient()->m_Snap.m_pLocalCharacter);
1705 }
1706 if(GameClient()->m_Snap.m_aCharacters[GameClient()->m_Snap.m_LocalClientId].m_HasExtendedData && g_Config.m_ClShowhudDDRace && GameClient()->m_GameInfo.m_HudDDRace)
1707 {
1708 RenderPlayerState(ClientId: GameClient()->m_Snap.m_LocalClientId);
1709 }
1710 RenderSpectatorCount();
1711 RenderMovementInformation();
1712 RenderDDRaceEffects();
1713 }
1714 else if(GameClient()->m_Snap.m_SpecInfo.m_Active)
1715 {
1716 int SpectatorId = GameClient()->m_Snap.m_SpecInfo.m_SpectatorId;
1717 if(SpectatorId != SPEC_FREEVIEW && g_Config.m_ClShowhudHealthAmmo)
1718 {
1719 RenderAmmoHealthAndArmor(pCharacter: &GameClient()->m_Snap.m_aCharacters[SpectatorId].m_Cur);
1720 }
1721 if(SpectatorId != SPEC_FREEVIEW &&
1722 GameClient()->m_Snap.m_aCharacters[SpectatorId].m_HasExtendedData &&
1723 g_Config.m_ClShowhudDDRace &&
1724 (!GameClient()->m_MultiViewActivated || GameClient()->m_MultiViewShowHud) &&
1725 GameClient()->m_GameInfo.m_HudDDRace)
1726 {
1727 RenderPlayerState(ClientId: SpectatorId);
1728 }
1729 RenderMovementInformation();
1730 RenderSpectatorHud();
1731 }
1732
1733 if(g_Config.m_ClShowhudTimer)
1734 RenderGameTimer();
1735 RenderPauseNotification();
1736 RenderSuddenDeath();
1737 if(g_Config.m_ClShowhudScore)
1738 RenderScoreHud();
1739 RenderDummyActions();
1740 RenderWarmupTimer();
1741 RenderTextInfo();
1742 RenderLocalTime(x: (m_Width / 7) * 3);
1743 if(Client()->State() != IClient::STATE_DEMOPLAYBACK)
1744 RenderConnectionWarning();
1745 RenderTeambalanceWarning();
1746 GameClient()->m_Voting.Render();
1747 if(g_Config.m_ClShowRecord)
1748 RenderRecord();
1749 }
1750 RenderCursor();
1751}
1752
1753void CHud::OnMessage(int MsgType, void *pRawMsg)
1754{
1755 if(MsgType == NETMSGTYPE_SV_DDRACETIME || MsgType == NETMSGTYPE_SV_DDRACETIMELEGACY)
1756 {
1757 CNetMsg_Sv_DDRaceTime *pMsg = (CNetMsg_Sv_DDRaceTime *)pRawMsg;
1758
1759 m_DDRaceTime = pMsg->m_Time;
1760
1761 m_ShowFinishTime = pMsg->m_Finish != 0;
1762
1763 if(!m_ShowFinishTime)
1764 {
1765 m_TimeCpDiff = (float)pMsg->m_Check / 100;
1766 m_TimeCpLastReceivedTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
1767 }
1768 else
1769 {
1770 m_FinishTimeDiff = (float)pMsg->m_Check / 100;
1771 m_FinishTimeLastReceivedTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
1772 }
1773 }
1774 else if(MsgType == NETMSGTYPE_SV_RECORD || MsgType == NETMSGTYPE_SV_RECORDLEGACY)
1775 {
1776 CNetMsg_Sv_Record *pMsg = (CNetMsg_Sv_Record *)pRawMsg;
1777
1778 // NETMSGTYPE_SV_RACETIME on old race servers
1779 if(MsgType == NETMSGTYPE_SV_RECORDLEGACY && GameClient()->m_GameInfo.m_DDRaceRecordMessage)
1780 {
1781 m_DDRaceTime = pMsg->m_ServerTimeBest; // First value: m_Time
1782
1783 m_FinishTimeLastReceivedTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
1784
1785 if(pMsg->m_PlayerTimeBest) // Second value: m_Check
1786 {
1787 m_TimeCpDiff = (float)pMsg->m_PlayerTimeBest / 100;
1788 m_TimeCpLastReceivedTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
1789 }
1790 }
1791 else if(MsgType == NETMSGTYPE_SV_RECORD || GameClient()->m_GameInfo.m_RaceRecordMessage)
1792 {
1793 // ignore m_ServerTimeBest, it's handled by the game client
1794 m_aPlayerRecord[g_Config.m_ClDummy] = (float)pMsg->m_PlayerTimeBest / 100;
1795 }
1796 }
1797}
1798
1799void CHud::RenderDDRaceEffects()
1800{
1801 if(m_DDRaceTime)
1802 {
1803 char aBuf[64];
1804 char aTime[32];
1805 if(m_ShowFinishTime && m_FinishTimeLastReceivedTick + Client()->GameTickSpeed() * 6 > Client()->GameTick(Conn: g_Config.m_ClDummy))
1806 {
1807 str_time(centisecs: m_DDRaceTime, format: ETimeFormat::HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1808 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Finish time: %s", aTime);
1809
1810 // calculate alpha (4 sec 1 than get lower the next 2 sec)
1811 float Alpha = 1.0f;
1812 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))
1813 {
1814 // lower the alpha slowly to blend text out
1815 Alpha = ((float)(m_FinishTimeLastReceivedTick + Client()->GameTickSpeed() * 6) - (float)Client()->GameTick(Conn: g_Config.m_ClDummy)) / (float)(Client()->GameTickSpeed() * 2);
1816 }
1817
1818 TextRender()->TextColor(r: 1, g: 1, b: 1, a: Alpha);
1819 CTextCursor Cursor;
1820 Cursor.SetPosition(vec2(150 * Graphics()->ScreenAspect() - TextRender()->TextWidth(Size: 12, pText: aBuf) / 2, 20));
1821 Cursor.m_FontSize = 12.0f;
1822 TextRender()->RecreateTextContainer(TextContainerIndex&: m_DDRaceEffectsTextContainerIndex, pCursor: &Cursor, pText: aBuf);
1823 if(m_FinishTimeDiff != 0.0f && m_DDRaceEffectsTextContainerIndex.Valid())
1824 {
1825 if(m_FinishTimeDiff < 0)
1826 {
1827 str_time_float(secs: -m_FinishTimeDiff, format: ETimeFormat::HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1828 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "-%s", aTime);
1829 TextRender()->TextColor(r: 0.5f, g: 1.0f, b: 0.5f, a: Alpha); // green
1830 }
1831 else
1832 {
1833 str_time_float(secs: m_FinishTimeDiff, format: ETimeFormat::HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1834 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "+%s", aTime);
1835 TextRender()->TextColor(r: 1.0f, g: 0.5f, b: 0.5f, a: Alpha); // red
1836 }
1837 CTextCursor DiffCursor;
1838 DiffCursor.SetPosition(vec2(150 * Graphics()->ScreenAspect() - TextRender()->TextWidth(Size: 10, pText: aBuf) / 2, 34));
1839 DiffCursor.m_FontSize = 10.0f;
1840 TextRender()->AppendTextContainer(TextContainerIndex: m_DDRaceEffectsTextContainerIndex, pCursor: &DiffCursor, pText: aBuf);
1841 }
1842 if(m_DDRaceEffectsTextContainerIndex.Valid())
1843 {
1844 auto OutlineColor = TextRender()->DefaultTextOutlineColor();
1845 OutlineColor.a *= Alpha;
1846 TextRender()->RenderTextContainer(TextContainerIndex: m_DDRaceEffectsTextContainerIndex, TextColor: TextRender()->DefaultTextColor(), TextOutlineColor: OutlineColor);
1847 }
1848 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1849 }
1850 else if(g_Config.m_ClShowhudTimeCpDiff && !m_ShowFinishTime && m_TimeCpLastReceivedTick + Client()->GameTickSpeed() * 6 > Client()->GameTick(Conn: g_Config.m_ClDummy))
1851 {
1852 if(m_TimeCpDiff < 0)
1853 {
1854 str_time_float(secs: -m_TimeCpDiff, format: ETimeFormat::HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1855 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "-%s", aTime);
1856 }
1857 else
1858 {
1859 str_time_float(secs: m_TimeCpDiff, format: ETimeFormat::HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1860 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "+%s", aTime);
1861 }
1862
1863 // calculate alpha (4 sec 1 than get lower the next 2 sec)
1864 float Alpha = 1.0f;
1865 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))
1866 {
1867 // lower the alpha slowly to blend text out
1868 Alpha = ((float)(m_TimeCpLastReceivedTick + Client()->GameTickSpeed() * 6) - (float)Client()->GameTick(Conn: g_Config.m_ClDummy)) / (float)(Client()->GameTickSpeed() * 2);
1869 }
1870
1871 if(m_TimeCpDiff > 0)
1872 TextRender()->TextColor(r: 1.0f, g: 0.5f, b: 0.5f, a: Alpha); // red
1873 else if(m_TimeCpDiff < 0)
1874 TextRender()->TextColor(r: 0.5f, g: 1.0f, b: 0.5f, a: Alpha); // green
1875 else if(!m_TimeCpDiff)
1876 TextRender()->TextColor(r: 1, g: 1, b: 1, a: Alpha); // white
1877
1878 CTextCursor Cursor;
1879 Cursor.SetPosition(vec2(150 * Graphics()->ScreenAspect() - TextRender()->TextWidth(Size: 10, pText: aBuf) / 2, 20));
1880 Cursor.m_FontSize = 10.0f;
1881 TextRender()->RecreateTextContainer(TextContainerIndex&: m_DDRaceEffectsTextContainerIndex, pCursor: &Cursor, pText: aBuf);
1882
1883 if(m_DDRaceEffectsTextContainerIndex.Valid())
1884 {
1885 auto OutlineColor = TextRender()->DefaultTextOutlineColor();
1886 OutlineColor.a *= Alpha;
1887 TextRender()->RenderTextContainer(TextContainerIndex: m_DDRaceEffectsTextContainerIndex, TextColor: TextRender()->DefaultTextColor(), TextOutlineColor: OutlineColor);
1888 }
1889 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1890 }
1891 }
1892}
1893
1894void CHud::RenderRecord()
1895{
1896 if(GameClient()->m_MapBestTimeSeconds != FinishTime::UNSET && GameClient()->m_MapBestTimeSeconds != FinishTime::NOT_FINISHED_MILLIS)
1897 {
1898 char aBuf[64];
1899 TextRender()->Text(x: 5, y: 75, Size: 6, pText: Localize(pStr: "Server best:"), LineWidth: -1.0f);
1900 char aTime[32];
1901 int64_t TimeCentiseconds = static_cast<int64_t>(GameClient()->m_MapBestTimeSeconds) * 100 + static_cast<int64_t>(GameClient()->m_MapBestTimeMillis) / 10;
1902 str_time(centisecs: TimeCentiseconds, format: ETimeFormat::HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1903 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s%s", GameClient()->m_MapBestTimeSeconds > 3600 ? "" : "   ", aTime);
1904 TextRender()->Text(x: 53, y: 75, Size: 6, pText: aBuf, LineWidth: -1.0f);
1905 }
1906
1907 if(GameClient()->m_ReceivedDDNetPlayerFinishTimes)
1908 {
1909 const int PlayerTimeSeconds = GameClient()->m_aClients[GameClient()->m_aLocalIds[g_Config.m_ClDummy]].m_FinishTimeSeconds;
1910 if(PlayerTimeSeconds != FinishTime::NOT_FINISHED_MILLIS)
1911 {
1912 char aBuf[64];
1913 TextRender()->Text(x: 5, y: 82, Size: 6, pText: Localize(pStr: "Personal best:"), LineWidth: -1.0f);
1914 char aTime[32];
1915 const int PlayerTimeMillis = GameClient()->m_aClients[GameClient()->m_aLocalIds[g_Config.m_ClDummy]].m_FinishTimeMillis;
1916 int64_t TimeCentiseconds = static_cast<int64_t>(PlayerTimeSeconds) * 100 + static_cast<int64_t>(PlayerTimeMillis) / 10;
1917 str_time(centisecs: TimeCentiseconds, format: ETimeFormat::HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1918 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s%s", PlayerTimeSeconds > 3600 ? "" : "   ", aTime);
1919 TextRender()->Text(x: 53, y: 82, Size: 6, pText: aBuf, LineWidth: -1.0f);
1920 }
1921 }
1922 else
1923 {
1924 const float PlayerRecord = m_aPlayerRecord[g_Config.m_ClDummy];
1925 if(PlayerRecord > 0.0f)
1926 {
1927 char aBuf[64];
1928 TextRender()->Text(x: 5, y: 82, Size: 6, pText: Localize(pStr: "Personal best:"), LineWidth: -1.0f);
1929 char aTime[32];
1930 str_time_float(secs: PlayerRecord, format: ETimeFormat::HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1931 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s%s", PlayerRecord > 3600 ? "" : "   ", aTime);
1932 TextRender()->Text(x: 53, y: 82, Size: 6, pText: aBuf, LineWidth: -1.0f);
1933 }
1934 }
1935}
1936