| 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 | |
| 20 | bool 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 | |
| 33 | void 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 | |
| 85 | void 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 | |
| 98 | void 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 | |
| 107 | void 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 | |
| 116 | void 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 | |
| 125 | void CSpectator::ConSpectateClosest(IConsole::IResult *pResult, void *pUserData) |
| 126 | { |
| 127 | CSpectator *pSelf = (CSpectator *)pUserData; |
| 128 | pSelf->SpectateClosest(); |
| 129 | } |
| 130 | |
| 131 | void 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 | |
| 142 | CSpectator::CSpectator() |
| 143 | { |
| 144 | m_SelectorMouse = vec2(0.0f, 0.0f); |
| 145 | OnReset(); |
| 146 | } |
| 147 | |
| 148 | void 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 | |
| 158 | bool 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 | |
| 168 | bool 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 | |
| 204 | void CSpectator::OnRelease() |
| 205 | { |
| 206 | OnReset(); |
| 207 | } |
| 208 | |
| 209 | void 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 | |
| 591 | void CSpectator::OnReset() |
| 592 | { |
| 593 | m_WasActive = false; |
| 594 | m_Active = false; |
| 595 | m_SelectedSpectatorId = NO_SELECTION; |
| 596 | } |
| 597 | |
| 598 | void 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 | |
| 633 | void 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 | |