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#include <engine/graphics.h>
4#include <engine/shared/config.h>
5#include <engine/textrender.h>
6
7#include <game/generated/client_data.h>
8#include <game/generated/protocol.h>
9
10#include <game/client/gameclient.h>
11#include <game/client/prediction/entities/character.h>
12
13#include "camera.h"
14#include "controls.h"
15#include "nameplates.h"
16
17void CNamePlates::RenderNameplate(
18 const CNetObj_Character *pPrevChar,
19 const CNetObj_Character *pPlayerChar,
20 const CNetObj_PlayerInfo *pPlayerInfo)
21{
22 int ClientId = pPlayerInfo->m_ClientId;
23
24 vec2 Position;
25 if(ClientId >= 0 && ClientId < MAX_CLIENTS)
26 Position = m_pClient->m_aClients[ClientId].m_RenderPos;
27 else
28 Position = mix(a: vec2(pPrevChar->m_X, pPrevChar->m_Y), b: vec2(pPlayerChar->m_X, pPlayerChar->m_Y), amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
29
30 RenderNameplatePos(Position, pPlayerInfo, Alpha: 1.0f);
31}
32
33void CNamePlates::RenderNameplatePos(vec2 Position, const CNetObj_PlayerInfo *pPlayerInfo, float Alpha, bool ForceAlpha)
34{
35 int ClientId = pPlayerInfo->m_ClientId;
36
37 bool OtherTeam = m_pClient->IsOtherTeam(ClientId);
38
39 float FontSize = 18.0f + 20.0f * g_Config.m_ClNameplatesSize / 100.0f;
40 float FontSizeClan = 18.0f + 20.0f * g_Config.m_ClNameplatesClanSize / 100.0f;
41
42 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_NO_FIRST_CHARACTER_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_LAST_CHARACTER_ADVANCE);
43 float YOffset = Position.y - 38;
44 ColorRGBA rgb = ColorRGBA(1.0f, 1.0f, 1.0f);
45
46 // render players' key presses
47 int ShowDirection = g_Config.m_ClShowDirection;
48#if defined(CONF_VIDEORECORDER)
49 if(IVideo::Current())
50 ShowDirection = g_Config.m_ClVideoShowDirection;
51#endif
52 if((ShowDirection && ShowDirection != 3 && !pPlayerInfo->m_Local) || (ShowDirection >= 2 && pPlayerInfo->m_Local) || (ShowDirection == 3 && Client()->DummyConnected() && Client()->State() != IClient::STATE_DEMOPLAYBACK && ClientId == m_pClient->m_aLocalIds[!g_Config.m_ClDummy]))
53 {
54 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
55 Graphics()->QuadsSetRotation(Angle: 0);
56
57 const float ShowDirectionImgSize = 22.0f;
58 YOffset -= ShowDirectionImgSize;
59 vec2 ShowDirectionPos = vec2(Position.x - 11.0f, YOffset);
60
61 bool DirLeft = m_pClient->m_Snap.m_aCharacters[pPlayerInfo->m_ClientId].m_Cur.m_Direction == -1;
62 bool DirRight = m_pClient->m_Snap.m_aCharacters[pPlayerInfo->m_ClientId].m_Cur.m_Direction == 1;
63 bool Jump = m_pClient->m_Snap.m_aCharacters[pPlayerInfo->m_ClientId].m_Cur.m_Jumped & 1;
64
65 if(pPlayerInfo->m_Local && Client()->State() != IClient::STATE_DEMOPLAYBACK)
66 {
67 DirLeft = m_pClient->m_Controls.m_aInputData[g_Config.m_ClDummy].m_Direction == -1;
68 DirRight = m_pClient->m_Controls.m_aInputData[g_Config.m_ClDummy].m_Direction == 1;
69 Jump = m_pClient->m_Controls.m_aInputData[g_Config.m_ClDummy].m_Jump == 1;
70 }
71 if(Client()->DummyConnected() && Client()->State() != IClient::STATE_DEMOPLAYBACK && pPlayerInfo->m_ClientId == m_pClient->m_aLocalIds[!g_Config.m_ClDummy])
72 {
73 DirLeft = m_pClient->m_Controls.m_aInputData[!g_Config.m_ClDummy].m_Direction == -1;
74 DirRight = m_pClient->m_Controls.m_aInputData[!g_Config.m_ClDummy].m_Direction == 1;
75 Jump = m_pClient->m_Controls.m_aInputData[!g_Config.m_ClDummy].m_Jump == 1;
76 }
77 if(DirLeft)
78 {
79 Graphics()->TextureSet(Texture: g_pData->m_aImages[IMAGE_ARROW].m_Id);
80 Graphics()->QuadsSetRotation(Angle: pi);
81 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_DirectionQuadContainerIndex, QuadOffset: 0, X: ShowDirectionPos.x - 30.f, Y: ShowDirectionPos.y);
82 }
83 else if(DirRight)
84 {
85 Graphics()->TextureSet(Texture: g_pData->m_aImages[IMAGE_ARROW].m_Id);
86 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_DirectionQuadContainerIndex, QuadOffset: 0, X: ShowDirectionPos.x + 30.f, Y: ShowDirectionPos.y);
87 }
88 if(Jump)
89 {
90 Graphics()->TextureSet(Texture: g_pData->m_aImages[IMAGE_ARROW].m_Id);
91 Graphics()->QuadsSetRotation(Angle: pi * 3 / 2);
92 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_DirectionQuadContainerIndex, QuadOffset: 0, X: ShowDirectionPos.x, Y: ShowDirectionPos.y);
93 }
94 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
95 Graphics()->QuadsSetRotation(Angle: 0);
96 }
97
98 // render name plate
99 if((!pPlayerInfo->m_Local || g_Config.m_ClNameplatesOwn) && g_Config.m_ClNameplates)
100 {
101 float a = 1;
102 if(g_Config.m_ClNameplatesAlways == 0)
103 a = clamp(val: 1 - std::pow(x: distance(a: m_pClient->m_Controls.m_aTargetPos[g_Config.m_ClDummy], b: Position) / 200.0f, y: 16.0f), lo: 0.0f, hi: 1.0f);
104
105 const char *pName = m_pClient->m_aClients[pPlayerInfo->m_ClientId].m_aName;
106 if(str_comp(a: pName, b: m_aNamePlates[ClientId].m_aName) != 0 || FontSize != m_aNamePlates[ClientId].m_NameTextFontSize)
107 {
108 str_copy(dst&: m_aNamePlates[ClientId].m_aName, src: pName);
109 m_aNamePlates[ClientId].m_NameTextFontSize = FontSize;
110
111 CTextCursor Cursor;
112 TextRender()->SetCursor(pCursor: &Cursor, x: 0, y: 0, FontSize, Flags: TEXTFLAG_RENDER);
113 Cursor.m_LineWidth = -1;
114
115 // create nameplates at standard zoom
116 float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
117 Graphics()->GetScreen(pTopLeftX: &ScreenX0, pTopLeftY: &ScreenY0, pBottomRightX: &ScreenX1, pBottomRightY: &ScreenY1);
118 RenderTools()->MapScreenToInterface(CenterX: m_pClient->m_Camera.m_Center.x, CenterY: m_pClient->m_Camera.m_Center.y);
119
120 m_aNamePlates[ClientId].m_NameTextWidth = TextRender()->TextWidth(Size: FontSize, pText: pName, StrLength: -1, LineWidth: -1.0f);
121
122 TextRender()->RecreateTextContainer(TextContainerIndex&: m_aNamePlates[ClientId].m_NameTextContainerIndex, pCursor: &Cursor, pText: pName);
123 Graphics()->MapScreen(TopLeftX: ScreenX0, TopLeftY: ScreenY0, BottomRightX: ScreenX1, BottomRightY: ScreenY1);
124 }
125
126 if(g_Config.m_ClNameplatesClan)
127 {
128 const char *pClan = m_pClient->m_aClients[ClientId].m_aClan;
129 if(str_comp(a: pClan, b: m_aNamePlates[ClientId].m_aClanName) != 0 || FontSizeClan != m_aNamePlates[ClientId].m_ClanNameTextFontSize)
130 {
131 str_copy(dst&: m_aNamePlates[ClientId].m_aClanName, src: pClan);
132 m_aNamePlates[ClientId].m_ClanNameTextFontSize = FontSizeClan;
133
134 CTextCursor Cursor;
135 TextRender()->SetCursor(pCursor: &Cursor, x: 0, y: 0, FontSize: FontSizeClan, Flags: TEXTFLAG_RENDER);
136 Cursor.m_LineWidth = -1;
137
138 // create nameplates at standard zoom
139 float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
140 Graphics()->GetScreen(pTopLeftX: &ScreenX0, pTopLeftY: &ScreenY0, pBottomRightX: &ScreenX1, pBottomRightY: &ScreenY1);
141 RenderTools()->MapScreenToInterface(CenterX: m_pClient->m_Camera.m_Center.x, CenterY: m_pClient->m_Camera.m_Center.y);
142
143 m_aNamePlates[ClientId].m_ClanNameTextWidth = TextRender()->TextWidth(Size: FontSizeClan, pText: pClan, StrLength: -1, LineWidth: -1.0f);
144
145 TextRender()->RecreateTextContainer(TextContainerIndex&: m_aNamePlates[ClientId].m_ClanNameTextContainerIndex, pCursor: &Cursor, pText: pClan);
146 Graphics()->MapScreen(TopLeftX: ScreenX0, TopLeftY: ScreenY0, BottomRightX: ScreenX1, BottomRightY: ScreenY1);
147 }
148 }
149
150 float tw = m_aNamePlates[ClientId].m_NameTextWidth;
151 if(g_Config.m_ClNameplatesTeamcolors && m_pClient->m_Teams.Team(ClientId))
152 rgb = m_pClient->GetDDTeamColor(DDTeam: m_pClient->m_Teams.Team(ClientId), Lightness: 0.75f);
153
154 ColorRGBA TColor;
155 ColorRGBA TOutlineColor;
156
157 if(OtherTeam && !ForceAlpha)
158 {
159 TOutlineColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.2f * g_Config.m_ClShowOthersAlpha / 100.0f);
160 TColor = ColorRGBA(rgb.r, rgb.g, rgb.b, g_Config.m_ClShowOthersAlpha / 100.0f);
161 }
162 else
163 {
164 TOutlineColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f * a);
165 TColor = ColorRGBA(rgb.r, rgb.g, rgb.b, a);
166 }
167 if(g_Config.m_ClNameplatesTeamcolors && m_pClient->m_Snap.m_pGameInfoObj && m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS)
168 {
169 if(m_pClient->m_aClients[ClientId].m_Team == TEAM_RED)
170 TColor = ColorRGBA(1.0f, 0.5f, 0.5f, a);
171 else if(m_pClient->m_aClients[ClientId].m_Team == TEAM_BLUE)
172 TColor = ColorRGBA(0.7f, 0.7f, 1.0f, a);
173 }
174
175 TOutlineColor.a *= Alpha;
176 TColor.a *= Alpha;
177
178 if(m_aNamePlates[ClientId].m_NameTextContainerIndex.Valid())
179 {
180 YOffset -= FontSize;
181 TextRender()->RenderTextContainer(TextContainerIndex: m_aNamePlates[ClientId].m_NameTextContainerIndex, TextColor: TColor, TextOutlineColor: TOutlineColor, X: Position.x - tw / 2.0f, Y: YOffset);
182 }
183
184 if(g_Config.m_ClNameplatesClan)
185 {
186 YOffset -= FontSizeClan;
187 if(m_aNamePlates[ClientId].m_ClanNameTextContainerIndex.Valid())
188 TextRender()->RenderTextContainer(TextContainerIndex: m_aNamePlates[ClientId].m_ClanNameTextContainerIndex, TextColor: TColor, TextOutlineColor: TOutlineColor, X: Position.x - m_aNamePlates[ClientId].m_ClanNameTextWidth / 2.0f, Y: YOffset);
189 }
190
191 if(g_Config.m_ClNameplatesFriendMark && m_pClient->m_aClients[ClientId].m_Friend)
192 {
193 YOffset -= FontSize;
194 char aFriendMark[] = "♥";
195
196 ColorRGBA Color;
197
198 if(OtherTeam && !ForceAlpha)
199 Color = ColorRGBA(1.0f, 0.0f, 0.0f, g_Config.m_ClShowOthersAlpha / 100.0f);
200 else
201 Color = ColorRGBA(1.0f, 0.0f, 0.0f, a);
202
203 Color.a *= Alpha;
204
205 TextRender()->TextColor(rgb: Color);
206 float XOffSet = TextRender()->TextWidth(Size: FontSize, pText: aFriendMark, StrLength: -1, LineWidth: -1.0f) / 2.0f;
207 TextRender()->Text(x: Position.x - XOffSet, y: YOffset, Size: FontSize, pText: aFriendMark, LineWidth: -1.0f);
208 }
209
210 if(g_Config.m_Debug || g_Config.m_ClNameplatesIds) // render client id when in debug as well
211 {
212 YOffset -= FontSize;
213 char aBuf[128];
214 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pPlayerInfo->m_ClientId);
215 float XOffset = TextRender()->TextWidth(Size: FontSize, pText: aBuf, StrLength: -1, LineWidth: -1.0f) / 2.0f;
216 TextRender()->TextColor(rgb);
217 TextRender()->Text(x: Position.x - XOffset, y: YOffset, Size: FontSize, pText: aBuf, LineWidth: -1.0f);
218 }
219 }
220
221 if((g_Config.m_Debug || g_Config.m_ClNameplatesStrong) && g_Config.m_ClNameplates)
222 {
223 bool Following = (m_pClient->m_Snap.m_SpecInfo.m_Active && !GameClient()->m_MultiViewActivated && m_pClient->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW);
224 if(m_pClient->m_Snap.m_LocalClientId != -1 || Following)
225 {
226 int SelectedId = Following ? m_pClient->m_Snap.m_SpecInfo.m_SpectatorId : m_pClient->m_Snap.m_LocalClientId;
227 const CGameClient::CSnapState::CCharacterInfo &Selected = m_pClient->m_Snap.m_aCharacters[SelectedId];
228 const CGameClient::CSnapState::CCharacterInfo &Other = m_pClient->m_Snap.m_aCharacters[ClientId];
229 if(Selected.m_HasExtendedData && Other.m_HasExtendedData)
230 {
231 if(SelectedId == ClientId)
232 TextRender()->TextColor(rgb);
233 else
234 {
235 float ScaleX, ScaleY;
236 const float StrongWeakImgSize = 40.0f;
237 Graphics()->TextureClear();
238 Graphics()->TextureSet(Texture: g_pData->m_aImages[IMAGE_STRONGWEAK].m_Id);
239 Graphics()->QuadsBegin();
240 ColorRGBA StrongWeakStatusColor;
241 int StrongWeakSpriteId;
242 if(Selected.m_ExtendedData.m_StrongWeakId > Other.m_ExtendedData.m_StrongWeakId)
243 {
244 StrongWeakStatusColor = color_cast<ColorRGBA>(hsl: ColorHSLA(6401973));
245 StrongWeakSpriteId = SPRITE_HOOK_STRONG;
246 }
247 else
248 {
249 StrongWeakStatusColor = color_cast<ColorRGBA>(hsl: ColorHSLA(41131));
250 StrongWeakSpriteId = SPRITE_HOOK_WEAK;
251 }
252
253 float ClampedAlpha = 1;
254 if(g_Config.m_ClNameplatesAlways == 0)
255 ClampedAlpha = clamp(val: 1 - std::pow(x: distance(a: m_pClient->m_Controls.m_aTargetPos[g_Config.m_ClDummy], b: Position) / 200.0f, y: 16.0f), lo: 0.0f, hi: 1.0f);
256
257 if(OtherTeam && !ForceAlpha)
258 StrongWeakStatusColor.a = g_Config.m_ClShowOthersAlpha / 100.0f;
259 else
260 StrongWeakStatusColor.a = ClampedAlpha;
261
262 StrongWeakStatusColor.a *= Alpha;
263 Graphics()->SetColor(StrongWeakStatusColor);
264 RenderTools()->SelectSprite(Id: StrongWeakSpriteId);
265 RenderTools()->GetSpriteScale(Id: StrongWeakSpriteId, ScaleX, ScaleY);
266 TextRender()->TextColor(rgb: StrongWeakStatusColor);
267
268 YOffset -= StrongWeakImgSize * ScaleY;
269 RenderTools()->DrawSprite(x: Position.x, y: YOffset + (StrongWeakImgSize / 2.0f) * ScaleY, Size: StrongWeakImgSize);
270 Graphics()->QuadsEnd();
271 }
272 if(g_Config.m_Debug || g_Config.m_ClNameplatesStrong == 2)
273 {
274 YOffset -= FontSize;
275 char aBuf[12];
276 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", Other.m_ExtendedData.m_StrongWeakId);
277 float XOffset = TextRender()->TextWidth(Size: FontSize, pText: aBuf, StrLength: -1, LineWidth: -1.0f) / 2.0f;
278 TextRender()->Text(x: Position.x - XOffset, y: YOffset, Size: FontSize, pText: aBuf, LineWidth: -1.0f);
279 }
280 }
281 }
282 }
283
284 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
285 TextRender()->TextOutlineColor(rgb: TextRender()->DefaultTextOutlineColor());
286
287 TextRender()->SetRenderFlags(0);
288}
289
290void CNamePlates::OnRender()
291{
292 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
293 return;
294
295 int ShowDirection = g_Config.m_ClShowDirection;
296#if defined(CONF_VIDEORECORDER)
297 if(IVideo::Current())
298 ShowDirection = g_Config.m_ClVideoShowDirection;
299#endif
300 if(!g_Config.m_ClNameplates && ShowDirection == 0)
301 return;
302
303 // get screen edges to avoid rendering offscreen
304 float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
305 Graphics()->GetScreen(pTopLeftX: &ScreenX0, pTopLeftY: &ScreenY0, pBottomRightX: &ScreenX1, pBottomRightY: &ScreenY1);
306 // expand the edges to prevent popping in/out onscreen
307 //
308 // it is assumed that the nameplate and all its components fit into a 800x800 box placed directly above the tee
309 // this may need to be changed or calculated differently in the future
310 ScreenX0 -= 400;
311 ScreenX1 += 400;
312 //ScreenY0 -= 0;
313 ScreenY1 += 800;
314
315 for(int i = 0; i < MAX_CLIENTS; i++)
316 {
317 const CNetObj_PlayerInfo *pInfo = m_pClient->m_Snap.m_apPlayerInfos[i];
318 if(!pInfo)
319 {
320 continue;
321 }
322
323 vec2 *pRenderPos;
324 if(m_pClient->m_aClients[i].m_SpecCharPresent)
325 {
326 // Each player can also have a spec char whose nameplate is displayed independently
327 pRenderPos = &m_pClient->m_aClients[i].m_SpecChar;
328 // don't render offscreen
329 if(!(pRenderPos->x < ScreenX0) && !(pRenderPos->x > ScreenX1) && !(pRenderPos->y < ScreenY0) && !(pRenderPos->y > ScreenY1))
330 {
331 RenderNameplatePos(Position: m_pClient->m_aClients[i].m_SpecChar, pPlayerInfo: pInfo, Alpha: 0.4f, ForceAlpha: true);
332 }
333 }
334 if(m_pClient->m_Snap.m_aCharacters[i].m_Active)
335 {
336 // Only render nameplates for active characters
337 pRenderPos = &m_pClient->m_aClients[i].m_RenderPos;
338 // don't render offscreen
339 if(!(pRenderPos->x < ScreenX0) && !(pRenderPos->x > ScreenX1) && !(pRenderPos->y < ScreenY0) && !(pRenderPos->y > ScreenY1))
340 {
341 RenderNameplate(
342 pPrevChar: &m_pClient->m_Snap.m_aCharacters[i].m_Prev,
343 pPlayerChar: &m_pClient->m_Snap.m_aCharacters[i].m_Cur,
344 pPlayerInfo: pInfo);
345 }
346 }
347 }
348}
349
350void CNamePlates::ResetNamePlates()
351{
352 for(auto &NamePlate : m_aNamePlates)
353 {
354 TextRender()->DeleteTextContainer(TextContainerIndex&: NamePlate.m_NameTextContainerIndex);
355 TextRender()->DeleteTextContainer(TextContainerIndex&: NamePlate.m_ClanNameTextContainerIndex);
356
357 NamePlate.Reset();
358 }
359}
360
361void CNamePlates::OnWindowResize()
362{
363 ResetNamePlates();
364}
365
366void CNamePlates::OnInit()
367{
368 ResetNamePlates();
369
370 // Quad for the direction arrows above the player
371 m_DirectionQuadContainerIndex = Graphics()->CreateQuadContainer(AutomaticUpload: false);
372 RenderTools()->QuadContainerAddSprite(QuadContainerIndex: m_DirectionQuadContainerIndex, x: 0.f, y: 0.f, Size: 22.f);
373 Graphics()->QuadContainerUpload(ContainerIndex: m_DirectionQuadContainerIndex);
374}
375