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