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
4#include <limits>
5
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/render.h>
14#include <game/localization.h>
15
16#include "camera.h"
17#include "spectator.h"
18
19#include <game/client/gameclient.h>
20
21bool CSpectator::CanChangeSpectator()
22{
23 // Don't change SpectatorId when not spectating
24 return m_pClient->m_Snap.m_SpecInfo.m_Active;
25}
26
27void CSpectator::SpectateNext(bool Reverse)
28{
29 int CurIndex = -1;
30 const CNetObj_PlayerInfo **paPlayerInfos = m_pClient->m_Snap.m_apInfoByDDTeamName;
31
32 // m_SpectatorId may be uninitialized if m_Active is false
33 if(m_pClient->m_Snap.m_SpecInfo.m_Active)
34 {
35 for(int i = 0; i < MAX_CLIENTS; i++)
36 {
37 if(paPlayerInfos[i] && paPlayerInfos[i]->m_ClientId == m_pClient->m_Snap.m_SpecInfo.m_SpectatorId)
38 {
39 CurIndex = i;
40 break;
41 }
42 }
43 }
44
45 int Start;
46 if(CurIndex != -1)
47 {
48 if(Reverse)
49 Start = CurIndex - 1;
50 else
51 Start = CurIndex + 1;
52 }
53 else
54 {
55 if(Reverse)
56 Start = -1;
57 else
58 Start = 0;
59 }
60
61 int Increment = Reverse ? -1 : 1;
62
63 for(int i = 0; i < MAX_CLIENTS; i++)
64 {
65 int PlayerIndex = (Start + i * Increment) % MAX_CLIENTS;
66 // % in C++ takes the sign of the dividend, not divisor
67 if(PlayerIndex < 0)
68 PlayerIndex += MAX_CLIENTS;
69
70 const CNetObj_PlayerInfo *pPlayerInfo = paPlayerInfos[PlayerIndex];
71 if(pPlayerInfo && pPlayerInfo->m_Team != TEAM_SPECTATORS)
72 {
73 Spectate(SpectatorId: pPlayerInfo->m_ClientId);
74 break;
75 }
76 }
77}
78
79void CSpectator::ConKeySpectator(IConsole::IResult *pResult, void *pUserData)
80{
81 CSpectator *pSelf = (CSpectator *)pUserData;
82
83 if(pSelf->m_pClient->m_Snap.m_SpecInfo.m_Active || pSelf->Client()->State() == IClient::STATE_DEMOPLAYBACK)
84 pSelf->m_Active = pResult->GetInteger(Index: 0) != 0;
85 else
86 pSelf->m_Active = false;
87}
88
89void CSpectator::ConSpectate(IConsole::IResult *pResult, void *pUserData)
90{
91 CSpectator *pSelf = (CSpectator *)pUserData;
92 if(!pSelf->CanChangeSpectator())
93 return;
94
95 pSelf->Spectate(SpectatorId: pResult->GetInteger(Index: 0));
96}
97
98void CSpectator::ConSpectateNext(IConsole::IResult *pResult, void *pUserData)
99{
100 CSpectator *pSelf = (CSpectator *)pUserData;
101 if(!pSelf->CanChangeSpectator())
102 return;
103
104 pSelf->SpectateNext(Reverse: false);
105}
106
107void CSpectator::ConSpectatePrevious(IConsole::IResult *pResult, void *pUserData)
108{
109 CSpectator *pSelf = (CSpectator *)pUserData;
110 if(!pSelf->CanChangeSpectator())
111 return;
112
113 pSelf->SpectateNext(Reverse: true);
114}
115
116void CSpectator::ConSpectateClosest(IConsole::IResult *pResult, void *pUserData)
117{
118 CSpectator *pSelf = (CSpectator *)pUserData;
119 if(!pSelf->CanChangeSpectator())
120 return;
121
122 const CGameClient::CSnapState &Snap = pSelf->m_pClient->m_Snap;
123 int SpectatorId = Snap.m_SpecInfo.m_SpectatorId;
124
125 int NewSpectatorId = -1;
126
127 vec2 CurPosition(pSelf->m_pClient->m_Camera.m_Center);
128 if(SpectatorId != SPEC_FREEVIEW)
129 {
130 const CNetObj_Character &CurCharacter = Snap.m_aCharacters[SpectatorId].m_Cur;
131 CurPosition.x = CurCharacter.m_X;
132 CurPosition.y = CurCharacter.m_Y;
133 }
134
135 int ClosestDistance = std::numeric_limits<int>::max();
136 for(int i = 0; i < MAX_CLIENTS; i++)
137 {
138 if(i == SpectatorId || !Snap.m_apPlayerInfos[i] || Snap.m_apPlayerInfos[i]->m_Team == TEAM_SPECTATORS || (SpectatorId == SPEC_FREEVIEW && i == Snap.m_LocalClientId))
139 continue;
140 const CNetObj_Character &MaybeClosestCharacter = Snap.m_aCharacters[i].m_Cur;
141 int Distance = distance(a: CurPosition, b: vec2(MaybeClosestCharacter.m_X, MaybeClosestCharacter.m_Y));
142 if(NewSpectatorId == -1 || Distance < ClosestDistance)
143 {
144 NewSpectatorId = i;
145 ClosestDistance = Distance;
146 }
147 }
148 if(NewSpectatorId > -1)
149 pSelf->Spectate(SpectatorId: NewSpectatorId);
150}
151
152void CSpectator::ConMultiView(IConsole::IResult *pResult, void *pUserData)
153{
154 CSpectator *pSelf = (CSpectator *)pUserData;
155 int Input = pResult->GetInteger(Index: 0);
156
157 if(Input == -1)
158 std::fill(first: std::begin(arr&: pSelf->GameClient()->m_aMultiViewId), last: std::end(arr&: pSelf->GameClient()->m_aMultiViewId), value: false); // remove everyone from multiview
159 else if(Input < MAX_CLIENTS && Input >= 0)
160 pSelf->GameClient()->m_aMultiViewId[Input] = !pSelf->GameClient()->m_aMultiViewId[Input]; // activate or deactivate one player from multiview
161}
162
163CSpectator::CSpectator()
164{
165 m_SelectorMouse = vec2(0.0f, 0.0f);
166 OnReset();
167 m_OldMouseX = m_OldMouseY = 0.0f;
168}
169
170void CSpectator::OnConsoleInit()
171{
172 Console()->Register(pName: "+spectate", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKeySpectator, pUser: this, pHelp: "Open spectator mode selector");
173 Console()->Register(pName: "spectate", pParams: "i[spectator-id]", Flags: CFGFLAG_CLIENT, pfnFunc: ConSpectate, pUser: this, pHelp: "Switch spectator mode");
174 Console()->Register(pName: "spectate_next", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConSpectateNext, pUser: this, pHelp: "Spectate the next player");
175 Console()->Register(pName: "spectate_previous", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConSpectatePrevious, pUser: this, pHelp: "Spectate the previous player");
176 Console()->Register(pName: "spectate_closest", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConSpectateClosest, pUser: this, pHelp: "Spectate the closest player");
177 Console()->Register(pName: "spectate_multiview", pParams: "i[id]", Flags: CFGFLAG_CLIENT, pfnFunc: ConMultiView, pUser: this, pHelp: "Add/remove Client-IDs to spectate them exclusively (-1 to reset)");
178}
179
180bool CSpectator::OnCursorMove(float x, float y, IInput::ECursorType CursorType)
181{
182 if(!m_Active)
183 return false;
184
185 Ui()->ConvertMouseMove(pX: &x, pY: &y, CursorType);
186 m_SelectorMouse += vec2(x, y);
187 return true;
188}
189
190void CSpectator::OnRelease()
191{
192 OnReset();
193}
194
195void CSpectator::OnRender()
196{
197 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
198 return;
199
200 if(!GameClient()->m_MultiViewActivated && m_MultiViewActivateDelay != 0.0f)
201 {
202 if(m_MultiViewActivateDelay <= Client()->LocalTime())
203 {
204 m_MultiViewActivateDelay = 0.0f;
205 GameClient()->m_MultiViewActivated = true;
206 }
207 }
208
209 if(!m_Active)
210 {
211 // closing the spectator menu
212 if(m_WasActive)
213 {
214 if(m_SelectedSpectatorId != NO_SELECTION)
215 {
216 if(m_SelectedSpectatorId == MULTI_VIEW)
217 GameClient()->m_MultiViewActivated = true;
218 else if(m_SelectedSpectatorId == SPEC_FREEVIEW || m_SelectedSpectatorId == SPEC_FOLLOW)
219 GameClient()->m_MultiViewActivated = false;
220
221 if(!GameClient()->m_MultiViewActivated)
222 Spectate(SpectatorId: m_SelectedSpectatorId);
223
224 if(GameClient()->m_MultiViewActivated && m_SelectedSpectatorId != MULTI_VIEW && m_pClient->m_Teams.Team(ClientId: m_SelectedSpectatorId) != GameClient()->m_MultiViewTeam)
225 {
226 GameClient()->ResetMultiView();
227 Spectate(SpectatorId: m_SelectedSpectatorId);
228 m_MultiViewActivateDelay = Client()->LocalTime() + 0.3f;
229 }
230 }
231 m_WasActive = false;
232 }
233 return;
234 }
235
236 if(m_SelectedSpectatorId != NO_SELECTION)
237 {
238 // clicking a component
239 if(m_Clicked)
240 {
241 if(!GameClient()->m_MultiViewActivated)
242 Spectate(SpectatorId: m_SelectedSpectatorId);
243
244 if(m_SelectedSpectatorId == MULTI_VIEW)
245 GameClient()->m_MultiViewActivated = true;
246 else if(m_SelectedSpectatorId == SPEC_FREEVIEW || m_SelectedSpectatorId == SPEC_FOLLOW)
247 GameClient()->m_MultiViewActivated = false;
248
249 if(!GameClient()->m_MultiViewActivated && m_SelectedSpectatorId >= 0 && m_SelectedSpectatorId < MAX_CLIENTS)
250 m_Clicked = false;
251 }
252 }
253
254 if(!m_pClient->m_Snap.m_SpecInfo.m_Active && Client()->State() != IClient::STATE_DEMOPLAYBACK)
255 {
256 m_Active = false;
257 m_WasActive = false;
258 return;
259 }
260
261 m_WasActive = true;
262 m_SelectedSpectatorId = NO_SELECTION;
263
264 // draw background
265 float Width = 400 * 3.0f * Graphics()->ScreenAspect();
266 float Height = 400 * 3.0f;
267 float ObjWidth = 300.0f;
268 float FontSize = 20.0f;
269 float BigFontSize = 20.0f;
270 float StartY = -190.0f;
271 float LineHeight = 60.0f;
272 float TeeSizeMod = 1.0f;
273 float RoundRadius = 30.0f;
274 bool Selected = false;
275 bool MultiViewSelected = false;
276 int TotalPlayers = 0;
277 int PerLine = 8;
278 float BoxMove = -10.0f;
279 float BoxOffset = 0.0f;
280
281 for(const auto &pInfo : m_pClient->m_Snap.m_apInfoByDDTeamName)
282 {
283 if(!pInfo || pInfo->m_Team == TEAM_SPECTATORS)
284 continue;
285
286 ++TotalPlayers;
287 }
288
289 if(TotalPlayers > 32)
290 {
291 FontSize = 18.0f;
292 LineHeight = 30.0f;
293 TeeSizeMod = 0.7f;
294 PerLine = 16;
295 RoundRadius = 10.0f;
296 BoxMove = 3.0f;
297 BoxOffset = 6.0f;
298 }
299 if(TotalPlayers > 16)
300 {
301 ObjWidth = 600.0f;
302 }
303
304 Graphics()->MapScreen(TopLeftX: 0, TopLeftY: 0, BottomRightX: Width, BottomRightY: Height);
305
306 Graphics()->DrawRect(x: Width / 2.0f - ObjWidth, y: Height / 2.0f - 300.0f, w: ObjWidth * 2, h: 600.0f, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f), Corners: IGraphics::CORNER_ALL, Rounding: 20.0f);
307
308 // clamp mouse position to selector area
309 m_SelectorMouse.x = clamp(val: m_SelectorMouse.x, lo: -(ObjWidth - 20.0f), hi: ObjWidth - 20.0f);
310 m_SelectorMouse.y = clamp(val: m_SelectorMouse.y, lo: -280.0f, hi: 280.0f);
311
312 // draw selections
313 if((Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_DemoSpecId == SPEC_FREEVIEW) ||
314 (Client()->State() != IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_SpecInfo.m_SpectatorId == SPEC_FREEVIEW))
315 {
316 Graphics()->DrawRect(x: Width / 2.0f - (ObjWidth - 20.0f), y: Height / 2.0f - 280.0f, w: ((ObjWidth * 2.0f) / 3.0f) - 40.0f, h: 60.0f, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 20.0f);
317 }
318
319 if(GameClient()->m_MultiViewActivated)
320 {
321 Graphics()->DrawRect(x: Width / 2.0f - (ObjWidth - 20.0f) + (ObjWidth * 2.0f / 3.0f), y: Height / 2.0f - 280.0f, w: ((ObjWidth * 2.0f) / 3.0f) - 40.0f, h: 60.0f, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 20.0f);
322 }
323
324 if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_LocalClientId >= 0 && m_pClient->m_DemoSpecId == SPEC_FOLLOW)
325 {
326 Graphics()->DrawRect(x: Width / 2.0f - (ObjWidth - 20.0f) + (ObjWidth * 2.0f * 2.0f / 3.0f), y: Height / 2.0f - 280.0f, w: ((ObjWidth * 2.0f) / 3.0f) - 40.0f, h: 60.0f, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 20.0f);
327 }
328
329 if(m_SelectorMouse.x >= -(ObjWidth - 20.0f) && m_SelectorMouse.x <= -(ObjWidth - 20.0f) + ((ObjWidth * 2.0f) / 3.0f) - 40.0f &&
330 m_SelectorMouse.y >= -280.0f && m_SelectorMouse.y <= -220.0f)
331 {
332 m_SelectedSpectatorId = SPEC_FREEVIEW;
333 Selected = true;
334 }
335 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: Selected ? 1.0f : 0.5f);
336 TextRender()->Text(x: Width / 2.0f - (ObjWidth - 40.0f), y: Height / 2.0f - 280.f + (60.f - BigFontSize) / 2.f, Size: BigFontSize, pText: Localize(pStr: "Free-View"), LineWidth: -1.0f);
337
338 if(m_SelectorMouse.x >= -(ObjWidth - 20.0f) + (ObjWidth * 2.0f / 3.0f) && m_SelectorMouse.x <= -(ObjWidth - 20.0f) + (ObjWidth * 2.0f / 3.0f) + ((ObjWidth * 2.0f) / 3.0f) - 40.0f &&
339 m_SelectorMouse.y >= -280.0f && m_SelectorMouse.y <= -220.0f)
340 {
341 m_SelectedSpectatorId = MULTI_VIEW;
342 MultiViewSelected = true;
343 }
344 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: MultiViewSelected ? 1.0f : 0.5f);
345 TextRender()->Text(x: Width / 2.0f - (ObjWidth - 40.0f) + (ObjWidth * 2.0f / 3.0f), y: Height / 2.0f - 280.f + (60.f - BigFontSize) / 2.f, Size: BigFontSize, pText: Localize(pStr: "Multi-View"), LineWidth: -1.0f);
346
347 if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_LocalClientId >= 0)
348 {
349 Selected = false;
350 if(m_SelectorMouse.x >= -(ObjWidth - 20.0f) + (ObjWidth * 2.0f * 2.0f / 3.0f) && m_SelectorMouse.x <= -(ObjWidth - 20.0f) + (ObjWidth * 2.0f * 2.0f / 3.0f) + ((ObjWidth * 2.0f) / 3.0f) - 40.0f &&
351 m_SelectorMouse.y >= -280.0f && m_SelectorMouse.y <= -220.0f)
352 {
353 m_SelectedSpectatorId = SPEC_FOLLOW;
354 Selected = true;
355 }
356 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: Selected ? 1.0f : 0.5f);
357 TextRender()->Text(x: Width / 2.0f - (ObjWidth - 40.0f) + (ObjWidth * 2.0f * 2.0f / 3.0f), y: Height / 2.0f - 280.0f + (60.f - BigFontSize) / 2.f, Size: BigFontSize, pText: Localize(pStr: "Follow"), LineWidth: -1.0f);
358 }
359
360 float x = -(ObjWidth - 35.0f), y = StartY;
361
362 int OldDDTeam = -1;
363
364 for(int i = 0, Count = 0; i < MAX_CLIENTS; ++i)
365 {
366 if(!m_pClient->m_Snap.m_apInfoByDDTeamName[i] || m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_Team == TEAM_SPECTATORS)
367 continue;
368
369 ++Count;
370
371 if(Count == PerLine + 1 || (Count > PerLine + 1 && (Count - 1) % PerLine == 0))
372 {
373 x += 290.0f;
374 y = StartY;
375 }
376
377 const CNetObj_PlayerInfo *pInfo = m_pClient->m_Snap.m_apInfoByDDTeamName[i];
378 int DDTeam = m_pClient->m_Teams.Team(ClientId: pInfo->m_ClientId);
379 int NextDDTeam = 0;
380
381 for(int j = i + 1; j < MAX_CLIENTS; j++)
382 {
383 const CNetObj_PlayerInfo *pInfo2 = m_pClient->m_Snap.m_apInfoByDDTeamName[j];
384
385 if(!pInfo2 || pInfo2->m_Team == TEAM_SPECTATORS)
386 continue;
387
388 NextDDTeam = m_pClient->m_Teams.Team(ClientId: pInfo2->m_ClientId);
389 break;
390 }
391
392 if(OldDDTeam == -1)
393 {
394 for(int j = i - 1; j >= 0; j--)
395 {
396 const CNetObj_PlayerInfo *pInfo2 = m_pClient->m_Snap.m_apInfoByDDTeamName[j];
397
398 if(!pInfo2 || pInfo2->m_Team == TEAM_SPECTATORS)
399 continue;
400
401 OldDDTeam = m_pClient->m_Teams.Team(ClientId: pInfo2->m_ClientId);
402 break;
403 }
404 }
405
406 if(DDTeam != TEAM_FLOCK)
407 {
408 const ColorRGBA Color = m_pClient->GetDDTeamColor(DDTeam).WithAlpha(alpha: 0.5f);
409 int Corners = 0;
410 if(OldDDTeam != DDTeam)
411 Corners |= IGraphics::CORNER_TL | IGraphics::CORNER_TR;
412 if(NextDDTeam != DDTeam)
413 Corners |= IGraphics::CORNER_BL | IGraphics::CORNER_BR;
414 Graphics()->DrawRect(x: Width / 2.0f + x - 10.0f + BoxOffset, y: Height / 2.0f + y + BoxMove, w: 270.0f - BoxOffset, h: LineHeight, Color, Corners, Rounding: RoundRadius);
415 }
416
417 OldDDTeam = DDTeam;
418
419 if((Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_DemoSpecId == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId) || (Client()->State() != IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_SpecInfo.m_SpectatorId == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId))
420 {
421 Graphics()->DrawRect(x: Width / 2.0f + x - 10.0f + BoxOffset, y: Height / 2.0f + y + BoxMove, w: 270.0f - BoxOffset, h: LineHeight, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: RoundRadius);
422 }
423
424 Selected = false;
425 if(m_SelectorMouse.x >= x - 10.0f && m_SelectorMouse.x < x + 260.0f &&
426 m_SelectorMouse.y >= y - (LineHeight / 6.0f) && m_SelectorMouse.y < y + (LineHeight * 5.0f / 6.0f))
427 {
428 m_SelectedSpectatorId = m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId;
429 Selected = true;
430 if(GameClient()->m_MultiViewActivated && m_Clicked)
431 {
432 if(GameClient()->m_MultiViewTeam == DDTeam)
433 {
434 GameClient()->m_aMultiViewId[m_SelectedSpectatorId] = !GameClient()->m_aMultiViewId[m_SelectedSpectatorId];
435 if(!GameClient()->m_aMultiViewId[m_pClient->m_Snap.m_SpecInfo.m_SpectatorId])
436 {
437 int NewClientId = GameClient()->FindFirstMultiViewId();
438 if(NewClientId < MAX_CLIENTS && NewClientId >= 0)
439 {
440 GameClient()->CleanMultiViewId(ClientId: NewClientId);
441 GameClient()->m_aMultiViewId[NewClientId] = true;
442 Spectate(SpectatorId: NewClientId);
443 }
444 }
445 }
446 else
447 {
448 GameClient()->ResetMultiView();
449 Spectate(SpectatorId: m_SelectedSpectatorId);
450 m_MultiViewActivateDelay = Client()->LocalTime() + 0.3f;
451 }
452 m_Clicked = false;
453 }
454 }
455 float TeeAlpha;
456 if(Client()->State() == IClient::STATE_DEMOPLAYBACK &&
457 !m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId].m_Active)
458 {
459 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.25f);
460 TeeAlpha = 0.5f;
461 }
462 else
463 {
464 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: Selected ? 1.0f : 0.5f);
465 TeeAlpha = 1.0f;
466 }
467 TextRender()->Text(x: Width / 2.0f + x + 50.0f, y: Height / 2.0f + y + BoxMove + (LineHeight - FontSize) / 2.f, Size: FontSize, pText: m_pClient->m_aClients[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId].m_aName, LineWidth: 220.0f);
468
469 if(GameClient()->m_MultiViewActivated)
470 {
471 if(GameClient()->m_aMultiViewId[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId])
472 {
473 TextRender()->TextColor(r: 0.1f, g: 1.0f, b: 0.1f, a: Selected ? 1.0f : 0.5f);
474 TextRender()->Text(x: Width / 2.0f + x + 50.0f + 180.0f, y: Height / 2.0f + y + BoxMove + (LineHeight - FontSize) / 2.f, Size: FontSize - 3, pText: "⬤", LineWidth: 220.0f);
475 }
476 else if(GameClient()->m_MultiViewTeam == DDTeam)
477 {
478 TextRender()->TextColor(r: 1.0f, g: 0.1f, b: 0.1f, a: Selected ? 1.0f : 0.5f);
479 TextRender()->Text(x: Width / 2.0f + x + 50.0f + 180.0f, y: Height / 2.0f + y + BoxMove + (LineHeight - FontSize) / 2.f, Size: FontSize - 3, pText: "◯", LineWidth: 220.0f);
480 }
481 }
482
483 // flag
484 if(m_pClient->m_Snap.m_pGameInfoObj && (m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS) &&
485 m_pClient->m_Snap.m_pGameDataObj && (m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierRed == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId || m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierBlue == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId))
486 {
487 Graphics()->BlendNormal();
488 if(m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierBlue == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId)
489 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpriteFlagBlue);
490 else
491 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpriteFlagRed);
492
493 Graphics()->QuadsBegin();
494 Graphics()->QuadsSetSubset(TopLeftU: 1, TopLeftV: 0, BottomRightU: 0, BottomRightV: 1);
495
496 float Size = LineHeight;
497 IGraphics::CQuadItem QuadItem(Width / 2.0f + x - LineHeight / 5.0f, Height / 2.0f + y - LineHeight / 3.0f, Size / 2.0f, Size);
498 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
499 Graphics()->QuadsEnd();
500 }
501
502 CTeeRenderInfo TeeInfo = m_pClient->m_aClients[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId].m_RenderInfo;
503 TeeInfo.m_Size *= TeeSizeMod;
504
505 const CAnimState *pIdleState = CAnimState::GetIdle();
506 vec2 OffsetToMid;
507 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
508 vec2 TeeRenderPos(Width / 2.0f + x + 20.0f, Height / 2.0f + y + BoxMove + LineHeight / 2.0f + OffsetToMid.y);
509
510 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &TeeInfo, Emote: EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos, Alpha: TeeAlpha);
511
512 if(m_pClient->m_aClients[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId].m_Friend)
513 {
514 ColorRGBA rgb = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClMessageFriendColor));
515 TextRender()->TextColor(rgb: rgb.WithAlpha(alpha: 1.f));
516 TextRender()->Text(x: Width / 2.0f + x - TeeInfo.m_Size / 2.0f, y: Height / 2.0f + y + BoxMove + (LineHeight - FontSize) / 2.f, Size: FontSize, pText: "♥", LineWidth: 220.0f);
517 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
518 }
519
520 y += LineHeight;
521 }
522 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
523
524 RenderTools()->RenderCursor(Center: m_SelectorMouse + vec2(Width, Height) / 2, Size: 48.0f);
525}
526
527void CSpectator::OnReset()
528{
529 m_WasActive = false;
530 m_Active = false;
531 m_SelectedSpectatorId = NO_SELECTION;
532}
533
534void CSpectator::Spectate(int SpectatorId)
535{
536 if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
537 {
538 m_pClient->m_DemoSpecId = clamp(val: SpectatorId, lo: (int)SPEC_FOLLOW, hi: MAX_CLIENTS - 1);
539 // The tick must be rendered for the spectator mode to be updated, so we do it manually when demo playback is paused
540 if(DemoPlayer()->BaseInfo()->m_Paused)
541 GameClient()->m_Menus.DemoSeekTick(TickOffset: IDemoPlayer::TICK_CURRENT);
542 return;
543 }
544
545 if(m_pClient->m_Snap.m_SpecInfo.m_SpectatorId == SpectatorId)
546 return;
547
548 CNetMsg_Cl_SetSpectatorMode Msg;
549 Msg.m_SpectatorId = SpectatorId;
550 Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL);
551}
552
553void CSpectator::SpectateClosest()
554{
555 ConSpectateClosest(NULL, pUserData: this);
556}
557
558bool CSpectator::OnInput(const IInput::CEvent &Event)
559{
560 if(m_Active && Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_MOUSE_1)
561 {
562 m_Clicked = true;
563 return true;
564 }
565 else if(Event.m_Flags & IInput::FLAG_RELEASE && Event.m_Key == KEY_MOUSE_1)
566 {
567 m_Clicked = false;
568 return false;
569 }
570 return false;
571}
572