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 CSpectator::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 > 96)
285 {
286 FontSize = 15.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 > 64)
295 {
296 FontSize = 16.0f;
297 LineHeight = 19.0f;
298 TeeSizeMod = 0.45f;
299 PerLine = 24;
300 RoundRadius = 6.0f;
301 BoxMove = 3.0f;
302 BoxOffset = 6.0f;
303 }
304 else if(TotalPlayers > 32)
305 {
306 FontSize = 18.0f;
307 LineHeight = 30.0f;
308 TeeSizeMod = 0.7f;
309 PerLine = 16;
310 RoundRadius = 10.0f;
311 BoxMove = 3.0f;
312 BoxOffset = 6.0f;
313 }
314 if(TotalPlayers > 16)
315 {
316 ObjWidth = 600.0f;
317 }
318
319 const vec2 ScreenSize = vec2(Width, Height);
320 const vec2 ScreenCenter = ScreenSize / 2.0f;
321 CUIRect SpectatorRect = {.x: Width / 2.0f - ObjWidth, .y: Height / 2.0f - 300.0f, .w: ObjWidth * 2.0f, .h: 600.0f};
322 CUIRect SpectatorMouseRect;
323 SpectatorRect.Margin(Cut: 20.0f, pOtherRect: &SpectatorMouseRect);
324
325 const bool WasTouchPressed = m_TouchState.m_AnyPressed;
326 Ui()->UpdateTouchState(State&: m_TouchState);
327 if(m_TouchState.m_AnyPressed)
328 {
329 const vec2 TouchPos = (m_TouchState.m_PrimaryPosition - vec2(0.5f, 0.5f)) * ScreenSize;
330 if(SpectatorMouseRect.Inside(Point: ScreenCenter + TouchPos))
331 {
332 m_SelectorMouse = TouchPos;
333 }
334 }
335 else if(WasTouchPressed)
336 {
337 const vec2 TouchPos = (m_TouchState.m_PrimaryPosition - vec2(0.5f, 0.5f)) * ScreenSize;
338 if(!SpectatorRect.Inside(Point: ScreenCenter + TouchPos))
339 {
340 OnRelease();
341 return;
342 }
343 }
344
345 Graphics()->MapScreenToSize(Width, Height);
346
347 SpectatorRect.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f), Corners: IGraphics::CORNER_ALL, Rounding: 20.0f);
348
349 // clamp mouse position to selector area
350 m_SelectorMouse.x = std::clamp(val: m_SelectorMouse.x, lo: -(ObjWidth - 20.0f), hi: ObjWidth - 20.0f);
351 m_SelectorMouse.y = std::clamp(val: m_SelectorMouse.y, lo: -280.0f, hi: 280.0f);
352
353 const bool MousePressed = Input()->KeyPress(Key: KEY_MOUSE_1) || m_TouchState.m_PrimaryPressed;
354
355 // draw selections
356 if((Client()->State() == IClient::STATE_DEMOPLAYBACK && GameClient()->m_DemoSpecId == SPEC_FREEVIEW) ||
357 (Client()->State() != IClient::STATE_DEMOPLAYBACK && GameClient()->m_Snap.m_SpecInfo.m_SpectatorId == SPEC_FREEVIEW))
358 {
359 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);
360 }
361
362 if(GameClient()->m_MultiViewActivated)
363 {
364 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);
365 }
366
367 if(Client()->State() == IClient::STATE_DEMOPLAYBACK && GameClient()->m_Snap.m_LocalClientId >= 0 && GameClient()->m_DemoSpecId == SPEC_FOLLOW)
368 {
369 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);
370 }
371
372 bool FreeViewSelected = false;
373 if(m_SelectorMouse.x >= -(ObjWidth - 20.0f) && m_SelectorMouse.x <= -(ObjWidth - 20.0f) + ((ObjWidth * 2.0f) / 3.0f) - 40.0f &&
374 m_SelectorMouse.y >= -280.0f && m_SelectorMouse.y <= -220.0f)
375 {
376 m_SelectedSpectatorId = SPEC_FREEVIEW;
377 FreeViewSelected = true;
378 if(MousePressed)
379 {
380 GameClient()->m_MultiViewActivated = false;
381 Spectate(SpectatorId: m_SelectedSpectatorId);
382 }
383 }
384 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: FreeViewSelected ? 1.0f : 0.5f);
385 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);
386
387 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 &&
388 m_SelectorMouse.y >= -280.0f && m_SelectorMouse.y <= -220.0f)
389 {
390 m_SelectedSpectatorId = MULTI_VIEW;
391 MultiViewSelected = true;
392 if(MousePressed)
393 {
394 GameClient()->m_MultiViewActivated = true;
395 }
396 }
397 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: MultiViewSelected ? 1.0f : 0.5f);
398 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);
399
400 if(Client()->State() == IClient::STATE_DEMOPLAYBACK && GameClient()->m_Snap.m_LocalClientId >= 0)
401 {
402 bool FollowSelected = false;
403 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 &&
404 m_SelectorMouse.y >= -280.0f && m_SelectorMouse.y <= -220.0f)
405 {
406 m_SelectedSpectatorId = SPEC_FOLLOW;
407 FollowSelected = true;
408 if(MousePressed)
409 {
410 GameClient()->m_MultiViewActivated = false;
411 Spectate(SpectatorId: m_SelectedSpectatorId);
412 }
413 }
414 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: FollowSelected ? 1.0f : 0.5f);
415 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);
416 }
417
418 float x = -(ObjWidth - 35.0f), y = StartY;
419
420 int OldDDTeam = -1;
421
422 for(int i = 0, Count = 0; i < MAX_CLIENTS; ++i)
423 {
424 if(!GameClient()->m_Snap.m_apInfoByDDTeamName[i] || GameClient()->m_Snap.m_apInfoByDDTeamName[i]->m_Team == TEAM_SPECTATORS)
425 continue;
426
427 ++Count;
428
429 if(Count == PerLine + 1 || (Count > PerLine + 1 && (Count - 1) % PerLine == 0))
430 {
431 x += 290.0f;
432 y = StartY;
433 }
434
435 const CNetObj_PlayerInfo *pInfo = GameClient()->m_Snap.m_apInfoByDDTeamName[i];
436 int DDTeam = GameClient()->m_Teams.Team(ClientId: pInfo->m_ClientId);
437 int NextDDTeam = 0;
438
439 for(int j = i + 1; j < MAX_CLIENTS; j++)
440 {
441 const CNetObj_PlayerInfo *pInfo2 = GameClient()->m_Snap.m_apInfoByDDTeamName[j];
442
443 if(!pInfo2 || pInfo2->m_Team == TEAM_SPECTATORS)
444 continue;
445
446 NextDDTeam = GameClient()->m_Teams.Team(ClientId: pInfo2->m_ClientId);
447 break;
448 }
449
450 if(OldDDTeam == -1)
451 {
452 for(int j = i - 1; j >= 0; j--)
453 {
454 const CNetObj_PlayerInfo *pInfo2 = GameClient()->m_Snap.m_apInfoByDDTeamName[j];
455
456 if(!pInfo2 || pInfo2->m_Team == TEAM_SPECTATORS)
457 continue;
458
459 OldDDTeam = GameClient()->m_Teams.Team(ClientId: pInfo2->m_ClientId);
460 break;
461 }
462 }
463
464 if(DDTeam != TEAM_FLOCK)
465 {
466 const ColorRGBA Color = GameClient()->GetDDTeamColor(DDTeam).WithAlpha(alpha: 0.5f);
467 int Corners = 0;
468 if(OldDDTeam != DDTeam)
469 Corners |= IGraphics::CORNER_TL | IGraphics::CORNER_TR;
470 if(NextDDTeam != DDTeam)
471 Corners |= IGraphics::CORNER_BL | IGraphics::CORNER_BR;
472 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);
473 }
474
475 OldDDTeam = DDTeam;
476
477 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))
478 {
479 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);
480 }
481
482 bool PlayerSelected = false;
483 if(m_SelectorMouse.x >= x - 10.0f && m_SelectorMouse.x < x + 260.0f &&
484 m_SelectorMouse.y >= y - (LineHeight / 6.0f) && m_SelectorMouse.y < y + (LineHeight * 5.0f / 6.0f))
485 {
486 m_SelectedSpectatorId = GameClient()->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId;
487 PlayerSelected = true;
488 if(MousePressed)
489 {
490 if(GameClient()->m_MultiViewActivated)
491 {
492 if(GameClient()->m_MultiViewTeam == DDTeam)
493 {
494 GameClient()->m_aMultiViewId[m_SelectedSpectatorId] = !GameClient()->m_aMultiViewId[m_SelectedSpectatorId];
495 if(!GameClient()->m_aMultiViewId[GameClient()->m_Snap.m_SpecInfo.m_SpectatorId])
496 {
497 int NewClientId = GameClient()->FindFirstMultiViewId();
498 if(NewClientId < MAX_CLIENTS && NewClientId >= 0)
499 {
500 GameClient()->CleanMultiViewId(ClientId: NewClientId);
501 GameClient()->m_aMultiViewId[NewClientId] = true;
502 Spectate(SpectatorId: NewClientId);
503 }
504 }
505 }
506 else
507 {
508 GameClient()->ResetMultiView();
509 Spectate(SpectatorId: m_SelectedSpectatorId);
510 m_MultiViewActivateDelay = Client()->LocalTime() + 0.3f;
511 }
512 }
513 else
514 {
515 Spectate(SpectatorId: m_SelectedSpectatorId);
516 }
517 }
518 }
519 float TeeAlpha;
520 if(Client()->State() == IClient::STATE_DEMOPLAYBACK &&
521 !GameClient()->m_Snap.m_aCharacters[GameClient()->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId].m_Active)
522 {
523 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.25f);
524 TeeAlpha = 0.5f;
525 }
526 else
527 {
528 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: PlayerSelected ? 1.0f : 0.5f);
529 TeeAlpha = 1.0f;
530 }
531 CTextCursor NameCursor;
532 NameCursor.SetPosition(vec2(Width / 2.0f + x + 50.0f, Height / 2.0f + y + BoxMove + (LineHeight - FontSize) / 2.f));
533 NameCursor.m_FontSize = FontSize;
534 NameCursor.m_Flags |= TEXTFLAG_ELLIPSIS_AT_END;
535 NameCursor.m_LineWidth = 180.0f;
536 if(g_Config.m_ClShowIds)
537 {
538 char aClientId[16];
539 GameClient()->FormatClientId(ClientId: GameClient()->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId, aClientId, Format: EClientIdFormat::INDENT_AUTO);
540 TextRender()->TextEx(pCursor: &NameCursor, pText: aClientId);
541 }
542 TextRender()->TextEx(pCursor: &NameCursor, pText: GameClient()->m_aClients[GameClient()->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId].m_aName);
543
544 if(GameClient()->m_MultiViewActivated)
545 {
546 if(GameClient()->m_aMultiViewId[GameClient()->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId])
547 {
548 TextRender()->TextColor(r: 0.1f, g: 1.0f, b: 0.1f, a: PlayerSelected ? 1.0f : 0.5f);
549 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);
550 }
551 else if(GameClient()->m_MultiViewTeam == DDTeam)
552 {
553 TextRender()->TextColor(r: 1.0f, g: 0.1f, b: 0.1f, a: PlayerSelected ? 1.0f : 0.5f);
554 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);
555 }
556 }
557
558 // flag
559 if(GameClient()->m_Snap.m_pGameInfoObj && (GameClient()->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS) &&
560 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))
561 {
562 if(GameClient()->m_Snap.m_pGameDataObj->m_FlagCarrierBlue == GameClient()->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId)
563 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpriteFlagBlue);
564 else
565 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpriteFlagRed);
566
567 Graphics()->QuadsBegin();
568 Graphics()->QuadsSetSubset(TopLeftU: 1, TopLeftV: 0, BottomRightU: 0, BottomRightV: 1);
569
570 float Size = LineHeight;
571 IGraphics::CQuadItem QuadItem(Width / 2.0f + x - LineHeight / 5.0f, Height / 2.0f + y - LineHeight / 3.0f, Size / 2.0f, Size);
572 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
573 Graphics()->QuadsEnd();
574 }
575
576 CTeeRenderInfo TeeInfo = GameClient()->m_aClients[GameClient()->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId].m_RenderInfo;
577 TeeInfo.m_Size *= TeeSizeMod;
578
579 const CAnimState *pIdleState = CAnimState::GetIdle();
580 vec2 OffsetToMid;
581 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
582 vec2 TeeRenderPos(Width / 2.0f + x + 20.0f, Height / 2.0f + y + BoxMove + LineHeight / 2.0f + OffsetToMid.y);
583
584 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &TeeInfo, Emote: EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos, Alpha: TeeAlpha);
585
586 if(GameClient()->m_aClients[GameClient()->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId].m_Friend)
587 {
588 TextRender()->TextColor(Color: color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClMessageFriendColor)));
589 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);
590 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
591 }
592
593 y += LineHeight;
594 }
595 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
596
597 RenderTools()->RenderCursor(Center: ScreenCenter + m_SelectorMouse, Size: 48.0f);
598}
599
600void CSpectator::OnReset()
601{
602 m_WasActive = false;
603 m_Active = false;
604 m_SelectedSpectatorId = NO_SELECTION;
605}
606
607void CSpectator::Spectate(int SpectatorId)
608{
609 if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
610 {
611 GameClient()->m_DemoSpecId = std::clamp(val: SpectatorId, lo: (int)SPEC_FOLLOW, hi: MAX_CLIENTS - 1);
612 // The tick must be rendered for the spectator mode to be updated, so we do it manually when demo playback is paused
613 // TODO: https://github.com/ddnet/ddnet/issues/11681
614 if(DemoPlayer()->BaseInfo()->m_Paused)
615 GameClient()->m_Menus.DemoSeekTick(TickOffset: IDemoPlayer::TICK_CURRENT);
616 return;
617 }
618
619 if(GameClient()->m_Snap.m_SpecInfo.m_SpectatorId == SpectatorId)
620 return;
621
622 if(Client()->IsSixup())
623 {
624 protocol7::CNetMsg_Cl_SetSpectatorMode Msg;
625 if(SpectatorId == SPEC_FREEVIEW)
626 {
627 Msg.m_SpecMode = protocol7::SPEC_FREEVIEW;
628 Msg.m_SpectatorId = -1;
629 }
630 else
631 {
632 Msg.m_SpecMode = protocol7::SPEC_PLAYER;
633 Msg.m_SpectatorId = SpectatorId;
634 }
635 Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL, NoTranslate: true);
636 return;
637 }
638 CNetMsg_Cl_SetSpectatorMode Msg;
639 Msg.m_SpectatorId = SpectatorId;
640 Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL);
641}
642
643void CSpectator::SpectateClosest()
644{
645 if(!CanChangeSpectatorId())
646 return;
647
648 const CGameClient::CSnapState &Snap = GameClient()->m_Snap;
649 int SpectatorId = Snap.m_SpecInfo.m_SpectatorId;
650
651 int NewSpectatorId = -1;
652
653 vec2 CurPosition = GameClient()->m_Camera.m_Center;
654 if(SpectatorId != SPEC_FREEVIEW)
655 {
656 const CNetObj_Character &CurCharacter = Snap.m_aCharacters[SpectatorId].m_Cur;
657 CurPosition = vec2(CurCharacter.m_X, CurCharacter.m_Y);
658 }
659
660 int ClosestDistance = std::numeric_limits<int>::max();
661 for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++)
662 {
663 if(ClientId == SpectatorId || !Snap.m_aCharacters[ClientId].m_Active || !Snap.m_apPlayerInfos[ClientId] || Snap.m_apPlayerInfos[ClientId]->m_Team == TEAM_SPECTATORS)
664 continue;
665
666 if(Client()->State() != IClient::STATE_DEMOPLAYBACK && ClientId == Snap.m_LocalClientId)
667 continue;
668
669 const CNetObj_Character &MaybeClosestCharacter = Snap.m_aCharacters[ClientId].m_Cur;
670 int Distance = distance(a: CurPosition, b: vec2(MaybeClosestCharacter.m_X, MaybeClosestCharacter.m_Y));
671 if(NewSpectatorId == -1 || Distance < ClosestDistance)
672 {
673 NewSpectatorId = ClientId;
674 ClosestDistance = Distance;
675 }
676 }
677 if(NewSpectatorId > -1)
678 Spectate(SpectatorId: NewSpectatorId);
679}
680