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