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 "scoreboard.h" |
4 | |
5 | #include <engine/demo.h> |
6 | #include <engine/graphics.h> |
7 | #include <engine/shared/config.h> |
8 | #include <engine/textrender.h> |
9 | |
10 | #include <game/generated/protocol.h> |
11 | |
12 | #include <game/client/animstate.h> |
13 | #include <game/client/components/countryflags.h> |
14 | #include <game/client/components/motd.h> |
15 | #include <game/client/components/statboard.h> |
16 | #include <game/client/gameclient.h> |
17 | #include <game/client/render.h> |
18 | #include <game/client/ui.h> |
19 | #include <game/localization.h> |
20 | |
21 | CScoreboard::CScoreboard() |
22 | { |
23 | OnReset(); |
24 | } |
25 | |
26 | void CScoreboard::ConKeyScoreboard(IConsole::IResult *pResult, void *pUserData) |
27 | { |
28 | CScoreboard *pSelf = static_cast<CScoreboard *>(pUserData); |
29 | pSelf->m_Active = pResult->GetInteger(Index: 0) != 0; |
30 | } |
31 | |
32 | void CScoreboard::OnConsoleInit() |
33 | { |
34 | Console()->Register(pName: "+scoreboard" , pParams: "" , Flags: CFGFLAG_CLIENT, pfnFunc: ConKeyScoreboard, pUser: this, pHelp: "Show scoreboard" ); |
35 | } |
36 | |
37 | void CScoreboard::OnReset() |
38 | { |
39 | m_Active = false; |
40 | m_ServerRecord = -1.0f; |
41 | } |
42 | |
43 | void CScoreboard::OnRelease() |
44 | { |
45 | m_Active = false; |
46 | } |
47 | |
48 | void CScoreboard::OnMessage(int MsgType, void *pRawMsg) |
49 | { |
50 | if(MsgType == NETMSGTYPE_SV_RECORD) |
51 | { |
52 | CNetMsg_Sv_Record *pMsg = static_cast<CNetMsg_Sv_Record *>(pRawMsg); |
53 | m_ServerRecord = pMsg->m_ServerTimeBest / 100.0f; |
54 | } |
55 | else if(MsgType == NETMSGTYPE_SV_RECORDLEGACY) |
56 | { |
57 | CNetMsg_Sv_RecordLegacy *pMsg = static_cast<CNetMsg_Sv_RecordLegacy *>(pRawMsg); |
58 | m_ServerRecord = pMsg->m_ServerTimeBest / 100.0f; |
59 | } |
60 | } |
61 | |
62 | void CScoreboard::RenderTitle(CUIRect TitleBar, int Team, const char *pTitle) |
63 | { |
64 | dbg_assert(Team == TEAM_RED || Team == TEAM_BLUE, "Team invalid" ); |
65 | |
66 | const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj; |
67 | |
68 | char aScore[128] = "" ; |
69 | if(GameClient()->m_GameInfo.m_TimeScore) |
70 | { |
71 | if(m_ServerRecord > 0) |
72 | { |
73 | str_time_float(secs: m_ServerRecord, format: TIME_HOURS, buffer: aScore, buffer_size: sizeof(aScore)); |
74 | } |
75 | } |
76 | else if(pGameInfoObj && (pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS)) |
77 | { |
78 | const CNetObj_GameData *pGameDataObj = GameClient()->m_Snap.m_pGameDataObj; |
79 | if(pGameDataObj) |
80 | { |
81 | str_format(buffer: aScore, buffer_size: sizeof(aScore), format: "%d" , Team == TEAM_RED ? pGameDataObj->m_TeamscoreRed : pGameDataObj->m_TeamscoreBlue); |
82 | } |
83 | } |
84 | else |
85 | { |
86 | if(GameClient()->m_Snap.m_SpecInfo.m_Active && |
87 | GameClient()->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW && |
88 | GameClient()->m_Snap.m_apPlayerInfos[GameClient()->m_Snap.m_SpecInfo.m_SpectatorId]) |
89 | { |
90 | str_format(buffer: aScore, buffer_size: sizeof(aScore), format: "%d" , GameClient()->m_Snap.m_apPlayerInfos[GameClient()->m_Snap.m_SpecInfo.m_SpectatorId]->m_Score); |
91 | } |
92 | else if(GameClient()->m_Snap.m_pLocalInfo) |
93 | { |
94 | str_format(buffer: aScore, buffer_size: sizeof(aScore), format: "%d" , GameClient()->m_Snap.m_pLocalInfo->m_Score); |
95 | } |
96 | } |
97 | |
98 | const float TitleFontSize = 40.0f; |
99 | const float ScoreTextWidth = TextRender()->TextWidth(Size: TitleFontSize, pText: aScore); |
100 | |
101 | TitleBar.VMargin(Cut: 20.0f, pOtherRect: &TitleBar); |
102 | CUIRect TitleLabel, ScoreLabel; |
103 | if(Team == TEAM_RED) |
104 | { |
105 | TitleBar.VSplitRight(Cut: ScoreTextWidth, pLeft: &TitleLabel, pRight: &ScoreLabel); |
106 | TitleLabel.VSplitRight(Cut: 10.0f, pLeft: &TitleLabel, pRight: nullptr); |
107 | } |
108 | else |
109 | { |
110 | TitleBar.VSplitLeft(Cut: ScoreTextWidth, pLeft: &ScoreLabel, pRight: &TitleLabel); |
111 | TitleLabel.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &TitleLabel); |
112 | } |
113 | |
114 | { |
115 | SLabelProperties Props; |
116 | Props.m_MaxWidth = TitleLabel.w; |
117 | Props.m_EllipsisAtEnd = true; |
118 | Ui()->DoLabel(pRect: &TitleLabel, pText: pTitle, Size: TitleFontSize, Align: Team == TEAM_RED ? TEXTALIGN_ML : TEXTALIGN_MR, LabelProps: Props); |
119 | } |
120 | |
121 | if(aScore[0] != '\0') |
122 | { |
123 | Ui()->DoLabel(pRect: &ScoreLabel, pText: aScore, Size: TitleFontSize, Align: Team == TEAM_RED ? TEXTALIGN_MR : TEXTALIGN_ML); |
124 | } |
125 | } |
126 | |
127 | void CScoreboard::RenderGoals(CUIRect Goals) |
128 | { |
129 | Goals.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 15.0f); |
130 | Goals.VMargin(Cut: 10.0f, pOtherRect: &Goals); |
131 | |
132 | const float FontSize = 20.0f; |
133 | const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj; |
134 | char aBuf[64]; |
135 | |
136 | if(pGameInfoObj->m_ScoreLimit) |
137 | { |
138 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %d" , Localize(pStr: "Score limit" ), pGameInfoObj->m_ScoreLimit); |
139 | Ui()->DoLabel(pRect: &Goals, pText: aBuf, Size: FontSize, Align: TEXTALIGN_ML); |
140 | } |
141 | |
142 | if(pGameInfoObj->m_TimeLimit) |
143 | { |
144 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Time limit: %d min" ), pGameInfoObj->m_TimeLimit); |
145 | Ui()->DoLabel(pRect: &Goals, pText: aBuf, Size: FontSize, Align: TEXTALIGN_MC); |
146 | } |
147 | |
148 | if(pGameInfoObj->m_RoundNum && pGameInfoObj->m_RoundCurrent) |
149 | { |
150 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Round %d/%d" ), pGameInfoObj->m_RoundCurrent, pGameInfoObj->m_RoundNum); |
151 | Ui()->DoLabel(pRect: &Goals, pText: aBuf, Size: FontSize, Align: TEXTALIGN_MR); |
152 | } |
153 | } |
154 | |
155 | void CScoreboard::RenderSpectators(CUIRect Spectators) |
156 | { |
157 | Spectators.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 15.0f); |
158 | Spectators.Margin(Cut: 10.0f, pOtherRect: &Spectators); |
159 | |
160 | CTextCursor Cursor; |
161 | TextRender()->SetCursor(pCursor: &Cursor, x: Spectators.x, y: Spectators.y, FontSize: 22.0f, Flags: TEXTFLAG_RENDER); |
162 | Cursor.m_LineWidth = Spectators.w; |
163 | Cursor.m_MaxLines = round_truncate(f: Spectators.h / Cursor.m_FontSize); |
164 | |
165 | int RemainingSpectators = 0; |
166 | for(const CNetObj_PlayerInfo *pInfo : GameClient()->m_Snap.m_apInfoByName) |
167 | { |
168 | if(!pInfo || pInfo->m_Team != TEAM_SPECTATORS) |
169 | continue; |
170 | ++RemainingSpectators; |
171 | } |
172 | |
173 | TextRender()->TextEx(pCursor: &Cursor, pText: Localize(pStr: "Spectators" )); |
174 | |
175 | if(RemainingSpectators > 0) |
176 | { |
177 | TextRender()->TextEx(pCursor: &Cursor, pText: ": " ); |
178 | } |
179 | |
180 | bool CommaNeeded = false; |
181 | for(const CNetObj_PlayerInfo *pInfo : GameClient()->m_Snap.m_apInfoByName) |
182 | { |
183 | if(!pInfo || pInfo->m_Team != TEAM_SPECTATORS) |
184 | continue; |
185 | |
186 | if(CommaNeeded) |
187 | { |
188 | TextRender()->TextEx(pCursor: &Cursor, pText: ", " ); |
189 | } |
190 | |
191 | if(Cursor.m_LineCount == Cursor.m_MaxLines && RemainingSpectators >= 2) |
192 | { |
193 | // This is less expensive than checking with a separate invisible |
194 | // text cursor though we waste some space at the end of the line. |
195 | char aRemaining[64]; |
196 | str_format(buffer: aRemaining, buffer_size: sizeof(aRemaining), format: Localize(pStr: "%d others…" , pContext: "Spectators" ), RemainingSpectators); |
197 | TextRender()->TextEx(pCursor: &Cursor, pText: aRemaining); |
198 | break; |
199 | } |
200 | |
201 | if(GameClient()->m_aClients[pInfo->m_ClientId].m_AuthLevel) |
202 | { |
203 | TextRender()->TextColor(rgb: color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClAuthedPlayerColor))); |
204 | } |
205 | |
206 | if(g_Config.m_ClShowIds) |
207 | { |
208 | char aClientId[5]; |
209 | str_format(buffer: aClientId, buffer_size: sizeof(aClientId), format: "%d: " , pInfo->m_ClientId); |
210 | TextRender()->TextEx(pCursor: &Cursor, pText: aClientId); |
211 | } |
212 | TextRender()->TextEx(pCursor: &Cursor, pText: GameClient()->m_aClients[pInfo->m_ClientId].m_aName); |
213 | TextRender()->TextColor(rgb: TextRender()->DefaultTextColor()); |
214 | |
215 | CommaNeeded = true; |
216 | --RemainingSpectators; |
217 | } |
218 | } |
219 | |
220 | void CScoreboard::RenderScoreboard(CUIRect Scoreboard, int Team, int CountStart, int CountEnd) |
221 | { |
222 | dbg_assert(Team == TEAM_RED || Team == TEAM_BLUE, "Team invalid" ); |
223 | |
224 | const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj; |
225 | const CNetObj_GameData *pGameDataObj = GameClient()->m_Snap.m_pGameDataObj; |
226 | const bool TimeScore = GameClient()->m_GameInfo.m_TimeScore; |
227 | const int NumPlayers = CountEnd - CountStart; |
228 | |
229 | // calculate measurements |
230 | float LineHeight; |
231 | float TeeSizeMod; |
232 | float Spacing; |
233 | float RoundRadius; |
234 | float FontSize; |
235 | if(NumPlayers <= 8) |
236 | { |
237 | LineHeight = 60.0f; |
238 | TeeSizeMod = 1.0f; |
239 | Spacing = 16.0f; |
240 | RoundRadius = 10.0f; |
241 | FontSize = 24.0f; |
242 | } |
243 | else if(NumPlayers <= 12) |
244 | { |
245 | LineHeight = 50.0f; |
246 | TeeSizeMod = 0.9f; |
247 | Spacing = 5.0f; |
248 | RoundRadius = 10.0f; |
249 | FontSize = 24.0f; |
250 | } |
251 | else if(NumPlayers <= 16) |
252 | { |
253 | LineHeight = 40.0f; |
254 | TeeSizeMod = 0.8f; |
255 | Spacing = 0.0f; |
256 | RoundRadius = 5.0f; |
257 | FontSize = 24.0f; |
258 | } |
259 | else if(NumPlayers <= 24) |
260 | { |
261 | LineHeight = 27.0f; |
262 | TeeSizeMod = 0.6f; |
263 | Spacing = 0.0f; |
264 | RoundRadius = 5.0f; |
265 | FontSize = 20.0f; |
266 | } |
267 | else |
268 | { |
269 | LineHeight = 20.0f; |
270 | TeeSizeMod = 0.4f; |
271 | Spacing = 0.0f; |
272 | RoundRadius = 5.0f; |
273 | FontSize = 16.0f; |
274 | } |
275 | |
276 | const float ScoreOffset = Scoreboard.x + 10.0f + 10.0f; |
277 | const float ScoreLength = TextRender()->TextWidth(Size: FontSize, pText: TimeScore ? "00:00:00" : "99999" ); |
278 | const float TeeOffset = ScoreOffset + ScoreLength + 15.0f; |
279 | const float TeeLength = 60.0f * TeeSizeMod; |
280 | const float NameOffset = TeeOffset + TeeLength; |
281 | const float NameLength = 300.0f - TeeLength; |
282 | const float CountryLength = (LineHeight - Spacing - TeeSizeMod * 5.0f) * 2.0f; |
283 | const float PingLength = 65.0f; |
284 | const float PingOffset = Scoreboard.x + Scoreboard.w - PingLength - 10.0f - 10.0f; |
285 | const float CountryOffset = PingOffset - CountryLength; |
286 | const float ClanLength = Scoreboard.w - ((NameOffset - Scoreboard.x) + NameLength) - (Scoreboard.w - (CountryOffset - Scoreboard.x)); |
287 | const float ClanOffset = CountryOffset - ClanLength; |
288 | |
289 | // render headlines |
290 | const float HeadlineFontsize = 22.0f; |
291 | CUIRect Headline; |
292 | Scoreboard.HSplitTop(Cut: HeadlineFontsize * 2.0f, pTop: &Headline, pBottom: &Scoreboard); |
293 | const float HeadlineY = Headline.y + Headline.h / 2.0f - HeadlineFontsize / 2.0f; |
294 | const char *pScore = TimeScore ? Localize(pStr: "Time" ) : Localize(pStr: "Score" ); |
295 | TextRender()->Text(x: ScoreOffset + ScoreLength - TextRender()->TextWidth(Size: HeadlineFontsize, pText: pScore), y: HeadlineY, Size: HeadlineFontsize, pText: pScore); |
296 | TextRender()->Text(x: NameOffset, y: HeadlineY, Size: HeadlineFontsize, pText: Localize(pStr: "Name" )); |
297 | const char *pClanLabel = Localize(pStr: "Clan" ); |
298 | TextRender()->Text(x: ClanOffset + (ClanLength - TextRender()->TextWidth(Size: HeadlineFontsize, pText: pClanLabel)) / 2.0f, y: HeadlineY, Size: HeadlineFontsize, pText: pClanLabel); |
299 | const char *pPingLabel = Localize(pStr: "Ping" ); |
300 | TextRender()->Text(x: PingOffset + PingLength - TextRender()->TextWidth(Size: HeadlineFontsize, pText: pPingLabel), y: HeadlineY, Size: HeadlineFontsize, pText: pPingLabel); |
301 | |
302 | // render player entries |
303 | int CountRendered = 0; |
304 | int PrevDDTeam = -1; |
305 | |
306 | char aBuf[64]; |
307 | for(int i = 0; i < MAX_CLIENTS; i++) |
308 | { |
309 | // make sure that we render the correct team |
310 | const CNetObj_PlayerInfo *pInfo = GameClient()->m_Snap.m_apInfoByDDTeamScore[i]; |
311 | if(!pInfo || pInfo->m_Team != Team) |
312 | continue; |
313 | |
314 | if(CountRendered++ < CountStart) |
315 | continue; |
316 | |
317 | int DDTeam = GameClient()->m_Teams.Team(ClientId: pInfo->m_ClientId); |
318 | int NextDDTeam = 0; |
319 | |
320 | for(int j = i + 1; j < MAX_CLIENTS; j++) |
321 | { |
322 | const CNetObj_PlayerInfo *pInfoNext = GameClient()->m_Snap.m_apInfoByDDTeamScore[j]; |
323 | if(!pInfoNext || pInfoNext->m_Team != Team) |
324 | continue; |
325 | |
326 | NextDDTeam = GameClient()->m_Teams.Team(ClientId: pInfoNext->m_ClientId); |
327 | break; |
328 | } |
329 | |
330 | if(PrevDDTeam == -1) |
331 | { |
332 | for(int j = i - 1; j >= 0; j--) |
333 | { |
334 | const CNetObj_PlayerInfo *pInfoPrev = GameClient()->m_Snap.m_apInfoByDDTeamScore[j]; |
335 | if(!pInfoPrev || pInfoPrev->m_Team != Team) |
336 | continue; |
337 | |
338 | PrevDDTeam = GameClient()->m_Teams.Team(ClientId: pInfoPrev->m_ClientId); |
339 | break; |
340 | } |
341 | } |
342 | |
343 | CUIRect RowAndSpacing, Row; |
344 | Scoreboard.HSplitTop(Cut: LineHeight + Spacing, pTop: &RowAndSpacing, pBottom: &Scoreboard); |
345 | RowAndSpacing.HSplitTop(Cut: LineHeight, pTop: &Row, pBottom: nullptr); |
346 | |
347 | // team background |
348 | if(DDTeam != TEAM_FLOCK) |
349 | { |
350 | const ColorRGBA Color = GameClient()->GetDDTeamColor(DDTeam).WithAlpha(alpha: 0.5f); |
351 | int TeamRectCorners = 0; |
352 | if(PrevDDTeam != DDTeam) |
353 | TeamRectCorners |= IGraphics::CORNER_T; |
354 | if(NextDDTeam != DDTeam) |
355 | TeamRectCorners |= IGraphics::CORNER_B; |
356 | RowAndSpacing.Draw(Color, Corners: TeamRectCorners, Rounding: RoundRadius); |
357 | |
358 | if(NextDDTeam != DDTeam) |
359 | { |
360 | const float TeamFontSize = FontSize / 1.5f; |
361 | if(NumPlayers > 8) |
362 | { |
363 | if(DDTeam == TEAM_SUPER) |
364 | str_copy(dst&: aBuf, src: Localize(pStr: "Super" )); |
365 | else |
366 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d" , DDTeam); |
367 | TextRender()->Text(x: Row.x, y: Row.y + Row.h / 2.0f - TeamFontSize / 2.0f, Size: TeamFontSize, pText: aBuf); |
368 | } |
369 | else |
370 | { |
371 | if(DDTeam == TEAM_SUPER) |
372 | str_copy(dst&: aBuf, src: Localize(pStr: "Super" )); |
373 | else |
374 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Team %d" ), DDTeam); |
375 | TextRender()->Text(x: Row.x + Row.w / 2.0f - TextRender()->TextWidth(Size: TeamFontSize, pText: aBuf) / 2.0f + 10.0f, y: Row.y + Row.h, Size: TeamFontSize, pText: aBuf); |
376 | } |
377 | } |
378 | } |
379 | PrevDDTeam = DDTeam; |
380 | |
381 | // background so it's easy to find the local player or the followed one in spectator mode |
382 | if((!GameClient()->m_Snap.m_SpecInfo.m_Active && pInfo->m_Local) || |
383 | (GameClient()->m_Snap.m_SpecInfo.m_SpectatorId == SPEC_FREEVIEW && pInfo->m_Local) || |
384 | (GameClient()->m_Snap.m_SpecInfo.m_Active && pInfo->m_ClientId == GameClient()->m_Snap.m_SpecInfo.m_SpectatorId)) |
385 | { |
386 | CUIRect Highlight; |
387 | Row.VMargin(Cut: 10.0f, pOtherRect: &Highlight); |
388 | Highlight.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: RoundRadius); |
389 | } |
390 | |
391 | // score |
392 | if(TimeScore) |
393 | { |
394 | if(pInfo->m_Score == -9999) |
395 | { |
396 | aBuf[0] = '\0'; |
397 | } |
398 | else |
399 | { |
400 | str_time(centisecs: (int64_t)absolute(a: pInfo->m_Score) * 100, format: TIME_HOURS, buffer: aBuf, buffer_size: sizeof(aBuf)); |
401 | } |
402 | } |
403 | else |
404 | { |
405 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d" , clamp(val: pInfo->m_Score, lo: -999, hi: 99999)); |
406 | } |
407 | TextRender()->Text(x: ScoreOffset + ScoreLength - TextRender()->TextWidth(Size: FontSize, pText: aBuf), y: Row.y + (Row.h - FontSize) / 2.0f, Size: FontSize, pText: aBuf); |
408 | |
409 | // CTF flag |
410 | if(pGameInfoObj && (pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS) && |
411 | pGameDataObj && (pGameDataObj->m_FlagCarrierRed == pInfo->m_ClientId || pGameDataObj->m_FlagCarrierBlue == pInfo->m_ClientId)) |
412 | { |
413 | Graphics()->BlendNormal(); |
414 | Graphics()->TextureSet(Texture: pGameDataObj->m_FlagCarrierBlue == pInfo->m_ClientId ? GameClient()->m_GameSkin.m_SpriteFlagBlue : GameClient()->m_GameSkin.m_SpriteFlagRed); |
415 | Graphics()->QuadsBegin(); |
416 | Graphics()->QuadsSetSubset(TopLeftU: 1.0f, TopLeftV: 0.0f, BottomRightU: 0.0f, BottomRightV: 1.0f); |
417 | IGraphics::CQuadItem QuadItem(TeeOffset, Row.y - 5.0f - Spacing / 2.0f, Row.h / 2.0f, Row.h); |
418 | Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1); |
419 | Graphics()->QuadsEnd(); |
420 | } |
421 | |
422 | const CGameClient::CClientData &ClientData = GameClient()->m_aClients[pInfo->m_ClientId]; |
423 | |
424 | // skin |
425 | { |
426 | CTeeRenderInfo TeeInfo = ClientData.m_RenderInfo; |
427 | TeeInfo.m_Size *= TeeSizeMod; |
428 | vec2 OffsetToMid; |
429 | CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid); |
430 | const vec2 TeeRenderPos = vec2(TeeOffset + TeeLength / 2, Row.y + Row.h / 2.0f + OffsetToMid.y); |
431 | RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &TeeInfo, Emote: EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos); |
432 | } |
433 | |
434 | // name |
435 | { |
436 | CTextCursor Cursor; |
437 | TextRender()->SetCursor(pCursor: &Cursor, x: NameOffset, y: Row.y + (Row.h - FontSize) / 2.0f, FontSize, Flags: TEXTFLAG_RENDER | TEXTFLAG_ELLIPSIS_AT_END); |
438 | Cursor.m_LineWidth = NameLength; |
439 | if(ClientData.m_AuthLevel) |
440 | { |
441 | TextRender()->TextColor(rgb: color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClAuthedPlayerColor))); |
442 | } |
443 | if(g_Config.m_ClShowIds) |
444 | { |
445 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s%d: %s" , pInfo->m_ClientId < 10 ? " " : "" , pInfo->m_ClientId, ClientData.m_aName); |
446 | TextRender()->TextEx(pCursor: &Cursor, pText: aBuf); |
447 | } |
448 | else |
449 | { |
450 | TextRender()->TextEx(pCursor: &Cursor, pText: ClientData.m_aName); |
451 | } |
452 | } |
453 | |
454 | // clan |
455 | { |
456 | if(str_comp(a: ClientData.m_aClan, b: GameClient()->m_aClients[GameClient()->m_aLocalIds[g_Config.m_ClDummy]].m_aClan) == 0) |
457 | { |
458 | TextRender()->TextColor(rgb: color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClSameClanColor))); |
459 | } |
460 | else |
461 | { |
462 | TextRender()->TextColor(rgb: TextRender()->DefaultTextColor()); |
463 | } |
464 | CTextCursor Cursor; |
465 | TextRender()->SetCursor(pCursor: &Cursor, x: ClanOffset + (ClanLength - minimum(a: TextRender()->TextWidth(Size: FontSize, pText: ClientData.m_aClan), b: ClanLength)) / 2.0f, y: Row.y + (Row.h - FontSize) / 2.0f, FontSize, Flags: TEXTFLAG_RENDER | TEXTFLAG_ELLIPSIS_AT_END); |
466 | Cursor.m_LineWidth = ClanLength; |
467 | TextRender()->TextEx(pCursor: &Cursor, pText: ClientData.m_aClan); |
468 | } |
469 | |
470 | // country flag |
471 | GameClient()->m_CountryFlags.Render(CountryCode: ClientData.m_Country, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f), |
472 | x: CountryOffset, y: Row.y + (Spacing + TeeSizeMod * 5.0f) / 2.0f, w: CountryLength, h: Row.h - Spacing - TeeSizeMod * 5.0f); |
473 | |
474 | // ping |
475 | if(g_Config.m_ClEnablePingColor) |
476 | { |
477 | TextRender()->TextColor(rgb: color_cast<ColorRGBA>(hsl: ColorHSLA((300.0f - clamp(val: pInfo->m_Latency, lo: 0, hi: 300)) / 1000.0f, 1.0f, 0.5f))); |
478 | } |
479 | else |
480 | { |
481 | TextRender()->TextColor(rgb: TextRender()->DefaultTextColor()); |
482 | } |
483 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d" , clamp(val: pInfo->m_Latency, lo: 0, hi: 999)); |
484 | TextRender()->Text(x: PingOffset + PingLength - TextRender()->TextWidth(Size: FontSize, pText: aBuf), y: Row.y + (Row.h - FontSize) / 2.0f, Size: FontSize, pText: aBuf); |
485 | TextRender()->TextColor(rgb: TextRender()->DefaultTextColor()); |
486 | |
487 | if(CountRendered == CountEnd) |
488 | break; |
489 | } |
490 | } |
491 | |
492 | void CScoreboard::RenderRecordingNotification(float x) |
493 | { |
494 | char aBuf[512] = "" ; |
495 | |
496 | const auto &&AppendRecorderInfo = [&](int Recorder, const char *pName) { |
497 | if(GameClient()->DemoRecorder(Recorder)->IsRecording()) |
498 | { |
499 | char aTime[32]; |
500 | str_time(centisecs: (int64_t)GameClient()->DemoRecorder(Recorder)->Length() * 100, format: TIME_HOURS, buffer: aTime, buffer_size: sizeof(aTime)); |
501 | str_append(dst&: aBuf, src: pName); |
502 | str_append(dst&: aBuf, src: " " ); |
503 | str_append(dst&: aBuf, src: aTime); |
504 | str_append(dst&: aBuf, src: " " ); |
505 | } |
506 | }; |
507 | |
508 | AppendRecorderInfo(RECORDER_MANUAL, Localize(pStr: "Manual" )); |
509 | AppendRecorderInfo(RECORDER_RACE, Localize(pStr: "Race" )); |
510 | AppendRecorderInfo(RECORDER_AUTO, Localize(pStr: "Auto" )); |
511 | AppendRecorderInfo(RECORDER_REPLAYS, Localize(pStr: "Replay" )); |
512 | |
513 | if(aBuf[0] == '\0') |
514 | return; |
515 | |
516 | const float FontSize = 20.0f; |
517 | |
518 | CUIRect Rect = {.x: x, .y: 0.0f, .w: TextRender()->TextWidth(Size: FontSize, pText: aBuf) + 60.0f, .h: 50.0f}; |
519 | Rect.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), Corners: IGraphics::CORNER_B, Rounding: 15.0f); |
520 | Rect.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &Rect); |
521 | Rect.VSplitRight(Cut: 10.0f, pLeft: &Rect, pRight: nullptr); |
522 | |
523 | CUIRect Circle; |
524 | Rect.VSplitLeft(Cut: 20.0f, pLeft: &Circle, pRight: &Rect); |
525 | Circle.HMargin(Cut: (Circle.h - Circle.w) / 2.0f, pOtherRect: &Circle); |
526 | Circle.Draw(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f), Corners: IGraphics::CORNER_ALL, Rounding: Circle.h / 2.0f); |
527 | |
528 | Rect.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &Rect); |
529 | Ui()->DoLabel(pRect: &Rect, pText: aBuf, Size: FontSize, Align: TEXTALIGN_ML); |
530 | } |
531 | |
532 | void CScoreboard::OnRender() |
533 | { |
534 | if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) |
535 | return; |
536 | |
537 | if(!Active()) |
538 | return; |
539 | |
540 | // if the score board is active, then we should clear the motd message as well |
541 | if(GameClient()->m_Motd.IsActive()) |
542 | GameClient()->m_Motd.Clear(); |
543 | |
544 | const float Height = 400.0f * 3.0f; |
545 | const float Width = Height * Graphics()->ScreenAspect(); |
546 | Graphics()->MapScreen(TopLeftX: 0, TopLeftY: 0, BottomRightX: Width, BottomRightY: Height); |
547 | |
548 | const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj; |
549 | const bool Teams = pGameInfoObj && (pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS); |
550 | const int NumPlayers = maximum(a: GameClient()->m_Snap.m_aTeamSize[TEAM_RED], b: GameClient()->m_Snap.m_aTeamSize[TEAM_BLUE]); |
551 | |
552 | const float ScoreboardSmallWidth = 750.0f + 20.0f; |
553 | const float ScoreboardWidth = !Teams && NumPlayers <= 16 ? ScoreboardSmallWidth : 1500.0f; |
554 | const float TitleHeight = 60.0f; |
555 | |
556 | CUIRect Scoreboard = {.x: (Width - ScoreboardWidth) / 2.0f, .y: 150.0f, .w: ScoreboardWidth, .h: 710.0f + TitleHeight}; |
557 | |
558 | if(Teams) |
559 | { |
560 | const char *pRedTeamName = GetTeamName(Team: TEAM_RED); |
561 | const char *pBlueTeamName = GetTeamName(Team: TEAM_BLUE); |
562 | |
563 | // Game over title |
564 | const CNetObj_GameData *pGameDataObj = GameClient()->m_Snap.m_pGameDataObj; |
565 | if((pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER) && pGameDataObj) |
566 | { |
567 | char aTitle[256]; |
568 | if(pGameDataObj->m_TeamscoreRed > pGameDataObj->m_TeamscoreBlue) |
569 | { |
570 | TextRender()->TextColor(rgb: ColorRGBA(0.975f, 0.17f, 0.17f, 1.0f)); |
571 | str_format(buffer: aTitle, buffer_size: sizeof(aTitle), format: Localize(pStr: "%s wins!" ), pRedTeamName); |
572 | } |
573 | else if(pGameDataObj->m_TeamscoreBlue > pGameDataObj->m_TeamscoreRed) |
574 | { |
575 | TextRender()->TextColor(rgb: ColorRGBA(0.17f, 0.46f, 0.975f, 1.0f)); |
576 | str_format(buffer: aTitle, buffer_size: sizeof(aTitle), format: Localize(pStr: "%s wins!" ), pBlueTeamName); |
577 | } |
578 | else |
579 | { |
580 | TextRender()->TextColor(rgb: ColorRGBA(0.91f, 0.78f, 0.33f, 1.0f)); |
581 | str_copy(dst&: aTitle, src: Localize(pStr: "Draw!" )); |
582 | } |
583 | |
584 | const float TitleFontSize = 72.0f; |
585 | CUIRect GameOverTitle = {.x: Scoreboard.x, .y: Scoreboard.y - TitleFontSize - 12.0f, .w: Scoreboard.w, .h: TitleFontSize}; |
586 | Ui()->DoLabel(pRect: &GameOverTitle, pText: aTitle, Size: TitleFontSize, Align: TEXTALIGN_MC); |
587 | TextRender()->TextColor(rgb: TextRender()->DefaultTextColor()); |
588 | } |
589 | |
590 | CUIRect RedScoreboard, BlueScoreboard, RedTitle, BlueTitle; |
591 | Scoreboard.VSplitMid(pLeft: &RedScoreboard, pRight: &BlueScoreboard, Spacing: 15.0f); |
592 | RedScoreboard.HSplitTop(Cut: TitleHeight, pTop: &RedTitle, pBottom: &RedScoreboard); |
593 | BlueScoreboard.HSplitTop(Cut: TitleHeight, pTop: &BlueTitle, pBottom: &BlueScoreboard); |
594 | |
595 | RedTitle.Draw(Color: ColorRGBA(0.975f, 0.17f, 0.17f, 0.5f), Corners: IGraphics::CORNER_T, Rounding: 15.0f); |
596 | BlueTitle.Draw(Color: ColorRGBA(0.17f, 0.46f, 0.975f, 0.5f), Corners: IGraphics::CORNER_T, Rounding: 15.0f); |
597 | RedScoreboard.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_B, Rounding: 15.0f); |
598 | BlueScoreboard.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_B, Rounding: 15.0f); |
599 | |
600 | RenderTitle(TitleBar: RedTitle, Team: TEAM_RED, pTitle: pRedTeamName); |
601 | RenderTitle(TitleBar: BlueTitle, Team: TEAM_BLUE, pTitle: pBlueTeamName); |
602 | RenderScoreboard(Scoreboard: RedScoreboard, Team: TEAM_RED, CountStart: 0, CountEnd: NumPlayers); |
603 | RenderScoreboard(Scoreboard: BlueScoreboard, Team: TEAM_BLUE, CountStart: 0, CountEnd: NumPlayers); |
604 | } |
605 | else |
606 | { |
607 | Scoreboard.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 15.0f); |
608 | |
609 | const char *pTitle; |
610 | if(pGameInfoObj && (pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER)) |
611 | { |
612 | pTitle = Localize(pStr: "Game over" ); |
613 | } |
614 | else |
615 | { |
616 | pTitle = Client()->GetCurrentMap(); |
617 | } |
618 | |
619 | CUIRect Title; |
620 | Scoreboard.HSplitTop(Cut: TitleHeight, pTop: &Title, pBottom: &Scoreboard); |
621 | RenderTitle(TitleBar: Title, Team: TEAM_RED, pTitle); |
622 | |
623 | if(NumPlayers <= 16) |
624 | { |
625 | RenderScoreboard(Scoreboard, Team: TEAM_RED, CountStart: 0, CountEnd: NumPlayers); |
626 | } |
627 | else |
628 | { |
629 | int PlayersPerSide; |
630 | if(NumPlayers <= 24) |
631 | PlayersPerSide = 12; |
632 | else if(NumPlayers <= 32) |
633 | PlayersPerSide = 16; |
634 | else if(NumPlayers <= 48) |
635 | PlayersPerSide = 24; |
636 | else |
637 | PlayersPerSide = 32; |
638 | |
639 | CUIRect LeftScoreboard, RightScoreboard; |
640 | Scoreboard.VSplitMid(pLeft: &LeftScoreboard, pRight: &RightScoreboard); |
641 | RenderScoreboard(Scoreboard: LeftScoreboard, Team: TEAM_RED, CountStart: 0, CountEnd: PlayersPerSide); |
642 | RenderScoreboard(Scoreboard: RightScoreboard, Team: TEAM_RED, CountStart: PlayersPerSide, CountEnd: 2 * PlayersPerSide); |
643 | } |
644 | } |
645 | |
646 | CUIRect Spectators = {.x: (Width - ScoreboardSmallWidth) / 2.0f, .y: Scoreboard.y + Scoreboard.h + 10.0f, .w: ScoreboardSmallWidth, .h: 200.0f}; |
647 | if(pGameInfoObj && (pGameInfoObj->m_ScoreLimit || pGameInfoObj->m_TimeLimit || (pGameInfoObj->m_RoundNum && pGameInfoObj->m_RoundCurrent))) |
648 | { |
649 | CUIRect Goals; |
650 | Spectators.HSplitTop(Cut: 50.0f, pTop: &Goals, pBottom: &Spectators); |
651 | Spectators.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &Spectators); |
652 | RenderGoals(Goals); |
653 | } |
654 | RenderSpectators(Spectators); |
655 | |
656 | RenderRecordingNotification(x: (Width / 7) * 4 + 20); |
657 | } |
658 | |
659 | bool CScoreboard::Active() const |
660 | { |
661 | // if statboard is active don't show scoreboard |
662 | if(GameClient()->m_Statboard.IsActive()) |
663 | return false; |
664 | |
665 | if(m_Active) |
666 | return true; |
667 | |
668 | if(GameClient()->m_Snap.m_pLocalInfo && !GameClient()->m_Snap.m_SpecInfo.m_Active) |
669 | { |
670 | // we are not a spectator, check if we are dead |
671 | if(!GameClient()->m_Snap.m_pLocalCharacter && g_Config.m_ClScoreboardOnDeath) |
672 | return true; |
673 | } |
674 | |
675 | // if the game is over |
676 | const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj; |
677 | if(pGameInfoObj && pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER) |
678 | return true; |
679 | |
680 | return false; |
681 | } |
682 | |
683 | const char *CScoreboard::GetTeamName(int Team) const |
684 | { |
685 | dbg_assert(Team == TEAM_RED || Team == TEAM_BLUE, "Team invalid" ); |
686 | |
687 | int ClanPlayers = 0; |
688 | const char *pClanName = nullptr; |
689 | const char *pDefaultTeamName = Team == TEAM_RED ? Localize(pStr: "Red team" ) : Localize(pStr: "Blue team" ); |
690 | for(const CNetObj_PlayerInfo *pInfo : GameClient()->m_Snap.m_apInfoByScore) |
691 | { |
692 | if(!pInfo || pInfo->m_Team != Team) |
693 | continue; |
694 | |
695 | if(!pClanName) |
696 | { |
697 | pClanName = GameClient()->m_aClients[pInfo->m_ClientId].m_aClan; |
698 | ClanPlayers++; |
699 | } |
700 | else |
701 | { |
702 | if(str_comp(a: GameClient()->m_aClients[pInfo->m_ClientId].m_aClan, b: pClanName) == 0) |
703 | ClanPlayers++; |
704 | else |
705 | return pDefaultTeamName; |
706 | } |
707 | } |
708 | |
709 | if(ClanPlayers > 1 && pClanName[0] != '\0') |
710 | return pClanName; |
711 | else |
712 | return pDefaultTeamName; |
713 | } |
714 | |