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