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