/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #include #include #include #include #include #include #include #include #include #include "camera.h" #include "spectator.h" #include bool CSpectator::CanChangeSpectator() { // Don't change SpectatorID when not spectating return m_pClient->m_Snap.m_SpecInfo.m_Active; } void CSpectator::SpectateNext(bool Reverse) { int CurIndex = -1; const CNetObj_PlayerInfo **paPlayerInfos = m_pClient->m_Snap.m_paInfoByDDTeamName; // m_SpectatorID may be uninitialized if m_Active is false if(m_pClient->m_Snap.m_SpecInfo.m_Active) { for(int i = 0; i < MAX_CLIENTS; i++) { if(paPlayerInfos[i] && paPlayerInfos[i]->m_ClientID == m_pClient->m_Snap.m_SpecInfo.m_SpectatorID) { CurIndex = i; break; } } } int Start; if(CurIndex != -1) { if(Reverse) Start = CurIndex - 1; else Start = CurIndex + 1; } else { if(Reverse) Start = -1; else Start = 0; } int Increment = Reverse ? -1 : 1; for(int i = 0; i < MAX_CLIENTS; i++) { int PlayerIndex = (Start + i * Increment) % MAX_CLIENTS; // % in C++ takes the sign of the dividend, not divisor if(PlayerIndex < 0) PlayerIndex += MAX_CLIENTS; const CNetObj_PlayerInfo *pPlayerInfo = paPlayerInfos[PlayerIndex]; if(pPlayerInfo && pPlayerInfo->m_Team != TEAM_SPECTATORS) { Spectate(pPlayerInfo->m_ClientID); break; } } } void CSpectator::ConKeySpectator(IConsole::IResult *pResult, void *pUserData) { CSpectator *pSelf = (CSpectator *)pUserData; if(pSelf->m_pClient->m_Snap.m_SpecInfo.m_Active || pSelf->Client()->State() == IClient::STATE_DEMOPLAYBACK) pSelf->m_Active = pResult->GetInteger(0) != 0; else pSelf->m_Active = false; } void CSpectator::ConSpectate(IConsole::IResult *pResult, void *pUserData) { CSpectator *pSelf = (CSpectator *)pUserData; if(!pSelf->CanChangeSpectator()) return; pSelf->Spectate(pResult->GetInteger(0)); } void CSpectator::ConSpectateNext(IConsole::IResult *pResult, void *pUserData) { CSpectator *pSelf = (CSpectator *)pUserData; if(!pSelf->CanChangeSpectator()) return; pSelf->SpectateNext(false); } void CSpectator::ConSpectatePrevious(IConsole::IResult *pResult, void *pUserData) { CSpectator *pSelf = (CSpectator *)pUserData; if(!pSelf->CanChangeSpectator()) return; pSelf->SpectateNext(true); } void CSpectator::ConSpectateClosest(IConsole::IResult *pResult, void *pUserData) { CSpectator *pSelf = (CSpectator *)pUserData; if(!pSelf->CanChangeSpectator()) return; CGameClient::CSnapState &Snap = pSelf->m_pClient->m_Snap; int SpectatorID = Snap.m_SpecInfo.m_SpectatorID; int NewSpectatorID = -1; vec2 CurPosition(pSelf->m_pClient->m_Camera.m_Center); if(SpectatorID != SPEC_FREEVIEW) { const CNetObj_Character &CurCharacter = Snap.m_aCharacters[SpectatorID].m_Cur; CurPosition.x = CurCharacter.m_X; CurPosition.y = CurCharacter.m_Y; } int ClosestDistance = INT_MAX; for(int i = 0; i < MAX_CLIENTS; i++) { if(i == SpectatorID || !Snap.m_paPlayerInfos[i] || Snap.m_paPlayerInfos[i]->m_Team == TEAM_SPECTATORS || (SpectatorID == SPEC_FREEVIEW && i == Snap.m_LocalClientID)) continue; const CNetObj_Character &MaybeClosestCharacter = Snap.m_aCharacters[i].m_Cur; int Distance = distance(CurPosition, vec2(MaybeClosestCharacter.m_X, MaybeClosestCharacter.m_Y)); if(NewSpectatorID == -1 || Distance < ClosestDistance) { NewSpectatorID = i; ClosestDistance = Distance; } } if(NewSpectatorID > -1) pSelf->Spectate(NewSpectatorID); } CSpectator::CSpectator() { OnReset(); m_OldMouseX = m_OldMouseY = 0.0f; } void CSpectator::OnConsoleInit() { Console()->Register("+spectate", "", CFGFLAG_CLIENT, ConKeySpectator, this, "Open spectator mode selector"); Console()->Register("spectate", "i[spectator-id]", CFGFLAG_CLIENT, ConSpectate, this, "Switch spectator mode"); Console()->Register("spectate_next", "", CFGFLAG_CLIENT, ConSpectateNext, this, "Spectate the next player"); Console()->Register("spectate_previous", "", CFGFLAG_CLIENT, ConSpectatePrevious, this, "Spectate the previous player"); Console()->Register("spectate_closest", "", CFGFLAG_CLIENT, ConSpectateClosest, this, "Spectate the closest player"); } bool CSpectator::OnMouseMove(float x, float y) { if(!m_Active) return false; UI()->ConvertMouseMove(&x, &y); m_SelectorMouse += vec2(x, y); return true; } void CSpectator::OnRelease() { OnReset(); } void CSpectator::OnRender() { if(!m_Active) { if(m_WasActive) { if(m_SelectedSpectatorID != NO_SELECTION) Spectate(m_SelectedSpectatorID); m_WasActive = false; } return; } if(!m_pClient->m_Snap.m_SpecInfo.m_Active && Client()->State() != IClient::STATE_DEMOPLAYBACK) { m_Active = false; m_WasActive = false; return; } m_WasActive = true; m_SelectedSpectatorID = NO_SELECTION; // draw background float Width = 400 * 3.0f * Graphics()->ScreenAspect(); float Height = 400 * 3.0f; float ObjWidth = 300.0f; float FontSize = 20.0f; float BigFontSize = 20.0f; float StartY = -190.0f; float LineHeight = 60.0f; float TeeSizeMod = 1.0f; float RoundRadius = 30.0f; bool Selected = false; int TotalPlayers = 0; int PerLine = 8; float BoxMove = -10.0f; float BoxOffset = 0.0f; for(auto &pInfo : m_pClient->m_Snap.m_paInfoByDDTeamName) { if(!pInfo || pInfo->m_Team == TEAM_SPECTATORS) continue; ++TotalPlayers; } if(TotalPlayers > 32) { FontSize = 18.0f; LineHeight = 30.0f; TeeSizeMod = 0.7f; PerLine = 16; RoundRadius = 10.0f; BoxMove = 3.0f; BoxOffset = 6.0f; } if(TotalPlayers > 16) { ObjWidth = 600.0f; } Graphics()->MapScreen(0, 0, Width, Height); Graphics()->BlendNormal(); Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(0.0f, 0.0f, 0.0f, 0.3f); RenderTools()->DrawRoundRect(Width / 2.0f - ObjWidth, Height / 2.0f - 300.0f, ObjWidth * 2, 600.0f, 20.0f); Graphics()->QuadsEnd(); // clamp mouse position to selector area m_SelectorMouse.x = clamp(m_SelectorMouse.x, -(ObjWidth - 20.0f), ObjWidth - 20.0f); m_SelectorMouse.y = clamp(m_SelectorMouse.y, -280.0f, 280.0f); // draw selections if((Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_DemoSpecID == SPEC_FREEVIEW) || m_pClient->m_Snap.m_SpecInfo.m_SpectatorID == SPEC_FREEVIEW) { Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.25f); RenderTools()->DrawRoundRect(Width / 2.0f - (ObjWidth - 20.0f), Height / 2.0f - 280.0f, 270.0f, 60.0f, 20.0f); Graphics()->QuadsEnd(); } if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_DemoSpecID == SPEC_FOLLOW) { Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.25f); RenderTools()->DrawRoundRect(Width / 2.0f - (ObjWidth - 310.0f), Height / 2.0f - 280.0f, 270.0f, 60.0f, 20.0f); Graphics()->QuadsEnd(); } if(m_SelectorMouse.x >= -(ObjWidth - 20.0f) && m_SelectorMouse.x <= -(ObjWidth - 290 + 10.0f) && m_SelectorMouse.y >= -280.0f && m_SelectorMouse.y <= -220.0f) { m_SelectedSpectatorID = SPEC_FREEVIEW; Selected = true; } TextRender()->TextColor(1.0f, 1.0f, 1.0f, Selected ? 1.0f : 0.5f); TextRender()->Text(0, Width / 2.0f - (ObjWidth - 60.0f), Height / 2.0f - 280.f + (60.f - BigFontSize) / 2.f, BigFontSize, Localize("Free-View"), -1.0f); if(Client()->State() == IClient::STATE_DEMOPLAYBACK) { Selected = false; if(m_SelectorMouse.x > -(ObjWidth - 290.0f) && m_SelectorMouse.x <= -(ObjWidth - 590.0f) && m_SelectorMouse.y >= -280.0f && m_SelectorMouse.y <= -220.0f) { m_SelectedSpectatorID = SPEC_FOLLOW; Selected = true; } TextRender()->TextColor(1.0f, 1.0f, 1.0f, Selected ? 1.0f : 0.5f); TextRender()->Text(0, Width / 2.0f - (ObjWidth - 350.0f), Height / 2.0f - 280.0f + (60.f - BigFontSize) / 2.f, BigFontSize, Localize("Follow"), -1.0f); } float x = -(ObjWidth - 35.0f), y = StartY; int OldDDTeam = -1; for(int i = 0, Count = 0; i < MAX_CLIENTS; ++i) { if(!m_pClient->m_Snap.m_paInfoByDDTeamName[i] || m_pClient->m_Snap.m_paInfoByDDTeamName[i]->m_Team == TEAM_SPECTATORS) continue; ++Count; if(Count == PerLine + 1 || (Count > PerLine + 1 && (Count - 1) % PerLine == 0)) { x += 290.0f; y = StartY; } const CNetObj_PlayerInfo *pInfo = m_pClient->m_Snap.m_paInfoByDDTeamName[i]; int DDTeam = m_pClient->m_Teams.Team(pInfo->m_ClientID); int NextDDTeam = 0; for(int j = i + 1; j < MAX_CLIENTS; j++) { const CNetObj_PlayerInfo *pInfo2 = m_pClient->m_Snap.m_paInfoByDDTeamName[j]; if(!pInfo2 || pInfo2->m_Team == TEAM_SPECTATORS) continue; NextDDTeam = m_pClient->m_Teams.Team(pInfo2->m_ClientID); break; } if(OldDDTeam == -1) { for(int j = i - 1; j >= 0; j--) { const CNetObj_PlayerInfo *pInfo2 = m_pClient->m_Snap.m_paInfoByDDTeamName[j]; if(!pInfo2 || pInfo2->m_Team == TEAM_SPECTATORS) continue; OldDDTeam = m_pClient->m_Teams.Team(pInfo2->m_ClientID); break; } } if(DDTeam != TEAM_FLOCK) { Graphics()->TextureClear(); Graphics()->QuadsBegin(); ColorRGBA rgb = color_cast(ColorHSLA(DDTeam / 64.0f, 1.0f, 0.5f, 0.5f)); Graphics()->SetColor(rgb); int Corners = 0; if(OldDDTeam != DDTeam) Corners |= CUI::CORNER_TL | CUI::CORNER_TR; if(NextDDTeam != DDTeam) Corners |= CUI::CORNER_BL | CUI::CORNER_BR; RenderTools()->DrawRoundRectExt(Width / 2.0f + x - 10.0f + BoxOffset, Height / 2.0f + y + BoxMove, 270.0f - BoxOffset, LineHeight, RoundRadius, Corners); Graphics()->QuadsEnd(); } OldDDTeam = DDTeam; if((Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_DemoSpecID == m_pClient->m_Snap.m_paInfoByDDTeamName[i]->m_ClientID) || (Client()->State() != IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_SpecInfo.m_SpectatorID == m_pClient->m_Snap.m_paInfoByDDTeamName[i]->m_ClientID)) { Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.25f); RenderTools()->DrawRoundRect(Width / 2.0f + x - 10.0f + BoxOffset, Height / 2.0f + y + BoxMove, 270.0f - BoxOffset, LineHeight, RoundRadius); Graphics()->QuadsEnd(); } Selected = false; if(m_SelectorMouse.x >= x - 10.0f && m_SelectorMouse.x < x + 260.0f && m_SelectorMouse.y >= y - (LineHeight / 6.0f) && m_SelectorMouse.y < y + (LineHeight * 5.0f / 6.0f)) { m_SelectedSpectatorID = m_pClient->m_Snap.m_paInfoByDDTeamName[i]->m_ClientID; Selected = true; } float TeeAlpha; if(Client()->State() == IClient::STATE_DEMOPLAYBACK && !m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_paInfoByDDTeamName[i]->m_ClientID].m_Active) { TextRender()->TextColor(1.0f, 1.0f, 1.0f, 0.25f); TeeAlpha = 0.5f; } else { TextRender()->TextColor(1.0f, 1.0f, 1.0f, Selected ? 1.0f : 0.5f); TeeAlpha = 1.0f; } TextRender()->Text(0, Width / 2.0f + x + 50.0f, Height / 2.0f + y + BoxMove + (LineHeight - FontSize) / 2.f, FontSize, m_pClient->m_aClients[m_pClient->m_Snap.m_paInfoByDDTeamName[i]->m_ClientID].m_aName, 220.0f); // flag if(m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS && m_pClient->m_Snap.m_pGameDataObj && (m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierRed == m_pClient->m_Snap.m_paInfoByDDTeamName[i]->m_ClientID || m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierBlue == m_pClient->m_Snap.m_paInfoByDDTeamName[i]->m_ClientID)) { Graphics()->BlendNormal(); if(m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierBlue == m_pClient->m_Snap.m_paInfoByDDTeamName[i]->m_ClientID) Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteFlagBlue); else Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteFlagRed); Graphics()->QuadsBegin(); Graphics()->QuadsSetSubset(1, 0, 0, 1); float Size = LineHeight; IGraphics::CQuadItem QuadItem(Width / 2.0f + x - LineHeight / 5.0f, Height / 2.0f + y - LineHeight / 3.0f, Size / 2.0f, Size); Graphics()->QuadsDrawTL(&QuadItem, 1); Graphics()->QuadsEnd(); } CTeeRenderInfo TeeInfo = m_pClient->m_aClients[m_pClient->m_Snap.m_paInfoByDDTeamName[i]->m_ClientID].m_RenderInfo; TeeInfo.m_Size *= TeeSizeMod; CAnimState *pIdleState = CAnimState::GetIdle(); vec2 OffsetToMid; RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid); vec2 TeeRenderPos(Width / 2.0f + x + 20.0f, Height / 2.0f + y + BoxMove + LineHeight / 2.0f + OffsetToMid.y); RenderTools()->RenderTee(pIdleState, &TeeInfo, EMOTE_NORMAL, vec2(1.0f, 0.0f), TeeRenderPos, TeeAlpha); if(m_pClient->m_aClients[m_pClient->m_Snap.m_paInfoByDDTeamName[i]->m_ClientID].m_Friend) { ColorRGBA rgb = color_cast(ColorHSLA(g_Config.m_ClMessageFriendColor)); TextRender()->TextColor(rgb.WithAlpha(1.f)); TextRender()->Text(0, Width / 2.0f + x - TeeInfo.m_Size / 2.0f, Height / 2.0f + y + BoxMove + (LineHeight - FontSize) / 2.f, FontSize, "♥", 220.0f); TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); } y += LineHeight; } TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); RenderTools()->RenderCursor(m_SelectorMouse + vec2(Width, Height) / 2, 48.0f); } void CSpectator::OnReset() { m_WasActive = false; m_Active = false; m_SelectedSpectatorID = NO_SELECTION; } void CSpectator::Spectate(int SpectatorID) { if(Client()->State() == IClient::STATE_DEMOPLAYBACK) { m_pClient->m_DemoSpecID = clamp(SpectatorID, (int)SPEC_FOLLOW, MAX_CLIENTS - 1); return; } if(m_pClient->m_Snap.m_SpecInfo.m_SpectatorID == SpectatorID) return; CNetMsg_Cl_SetSpectatorMode Msg; Msg.m_SpectatorID = SpectatorID; Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL); }