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 "players.h"
5
6#include <base/color.h>
7#include <base/dbg.h>
8#include <base/math.h>
9
10#include <engine/client/enums.h>
11#include <engine/demo.h>
12#include <engine/graphics.h>
13#include <engine/shared/config.h>
14
15#include <generated/client_data.h>
16#include <generated/client_data7.h>
17#include <generated/protocol.h>
18
19#include <game/client/animstate.h>
20#include <game/client/components/controls.h>
21#include <game/client/components/effects.h>
22#include <game/client/components/flow.h>
23#include <game/client/components/skins.h>
24#include <game/client/components/sounds.h>
25#include <game/client/gameclient.h>
26#include <game/collision.h>
27#include <game/gamecore.h>
28#include <game/mapitems.h>
29
30static float CalculateHandAngle(vec2 Dir, float AngleOffset)
31{
32 const float Angle = angle(a: Dir);
33 if(Dir.x < 0.0f)
34 {
35 return Angle - AngleOffset;
36 }
37 else
38 {
39 return Angle + AngleOffset;
40 }
41}
42
43static vec2 CalculateHandPosition(vec2 CenterPos, vec2 Dir, vec2 PostRotOffset)
44{
45 vec2 DirY = vec2(-Dir.y, Dir.x);
46 if(Dir.x < 0.0f)
47 {
48 DirY = -DirY;
49 }
50 return CenterPos + Dir + Dir * PostRotOffset.x + DirY * PostRotOffset.y;
51}
52
53void CPlayers::RenderHand(const CTeeRenderInfo *pInfo, vec2 CenterPos, vec2 Dir, float AngleOffset, vec2 PostRotOffset, float Alpha)
54{
55 const vec2 HandPos = CalculateHandPosition(CenterPos, Dir, PostRotOffset);
56 const float HandAngle = CalculateHandAngle(Dir, AngleOffset);
57 if(pInfo->m_aSixup[g_Config.m_ClDummy].PartTexture(Part: protocol7::SKINPART_HANDS).IsValid())
58 {
59 RenderHand7(pInfo, HandPos, HandAngle, Alpha);
60 }
61 else
62 {
63 RenderHand6(pInfo, HandPos, HandAngle, Alpha);
64 }
65}
66
67void CPlayers::RenderHand7(const CTeeRenderInfo *pInfo, vec2 HandPos, float HandAngle, float Alpha)
68{
69 // in-game hand size is 15 when tee size is 64
70 const float BaseSize = 15.0f * (pInfo->m_Size / 64.0f);
71 IGraphics::CQuadItem QuadOutline(HandPos.x, HandPos.y, 2 * BaseSize, 2 * BaseSize);
72 IGraphics::CQuadItem QuadHand = QuadOutline;
73
74 Graphics()->TextureSet(Texture: pInfo->m_aSixup[g_Config.m_ClDummy].PartTexture(Part: protocol7::SKINPART_HANDS));
75 Graphics()->QuadsBegin();
76 Graphics()->SetColor(pInfo->m_aSixup[g_Config.m_ClDummy].m_aColors[protocol7::SKINPART_HANDS].WithAlpha(alpha: Alpha));
77 Graphics()->QuadsSetRotation(Angle: HandAngle);
78 Graphics()->SelectSprite7(Id: client_data7::SPRITE_TEE_HAND_OUTLINE);
79 Graphics()->QuadsDraw(pArray: &QuadOutline, Num: 1);
80 Graphics()->SelectSprite7(Id: client_data7::SPRITE_TEE_HAND);
81 Graphics()->QuadsDraw(pArray: &QuadHand, Num: 1);
82 Graphics()->QuadsEnd();
83}
84
85void CPlayers::RenderHand6(const CTeeRenderInfo *pInfo, vec2 HandPos, float HandAngle, float Alpha)
86{
87 const CSkin::CSkinTextures *pSkinTextures = pInfo->m_CustomColoredSkin ? &pInfo->m_ColorableRenderSkin : &pInfo->m_OriginalRenderSkin;
88
89 Graphics()->SetColor(pInfo->m_ColorBody.WithAlpha(alpha: Alpha));
90 Graphics()->QuadsSetRotation(Angle: HandAngle);
91 Graphics()->TextureSet(Texture: pSkinTextures->m_HandsOutline);
92 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_WeaponEmoteQuadContainerIndex, QuadOffset: NUM_WEAPONS * 2, X: HandPos.x, Y: HandPos.y);
93 Graphics()->TextureSet(Texture: pSkinTextures->m_Hands);
94 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_WeaponEmoteQuadContainerIndex, QuadOffset: NUM_WEAPONS * 2 + 1, X: HandPos.x, Y: HandPos.y);
95}
96
97float CPlayers::GetPlayerTargetAngle(
98 const CNetObj_Character *pPrevChar,
99 const CNetObj_Character *pPlayerChar,
100 int ClientId,
101 float Intra)
102{
103 if(GameClient()->PredictDummy() && GameClient()->m_aLocalIds[!g_Config.m_ClDummy] == ClientId)
104 {
105 const CNetObj_PlayerInput &Input = g_Config.m_ClDummyHammer ? GameClient()->m_HammerInput : GameClient()->m_DummyInput;
106 return angle(a: vec2(Input.m_TargetX, Input.m_TargetY));
107 }
108
109 // with dummy copy, use the same angle as local player
110 if((GameClient()->m_Snap.m_LocalClientId == ClientId || (GameClient()->PredictDummy() && g_Config.m_ClDummyCopyMoves && GameClient()->m_aLocalIds[!g_Config.m_ClDummy] == ClientId)) &&
111 !GameClient()->m_Snap.m_SpecInfo.m_Active && Client()->State() != IClient::STATE_DEMOPLAYBACK)
112 {
113 // calculate what would be sent to the server from our current input
114 vec2 Direction = normalize(v: vec2((int)GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy].x, (int)GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy].y));
115
116 // fix direction if mouse is exactly in the center
117 if(Direction == vec2(0.0f, 0.0f))
118 Direction = vec2(1.0f, 0.0f);
119
120 return angle(a: Direction);
121 }
122
123 // using unpredicted angle when rendering other players in-game
124 if(ClientId >= 0)
125 Intra = Client()->IntraGameTick(Conn: g_Config.m_ClDummy);
126
127 if(ClientId >= 0 && GameClient()->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo)
128 {
129 const CNetObj_DDNetCharacter *pExtendedData = &GameClient()->m_Snap.m_aCharacters[ClientId].m_ExtendedData;
130 const CNetObj_DDNetCharacter *pPrevExtendedData = GameClient()->m_Snap.m_aCharacters[ClientId].m_pPrevExtendedData;
131 if(pPrevExtendedData)
132 {
133 float MixX = mix(a: (float)pPrevExtendedData->m_TargetX, b: (float)pExtendedData->m_TargetX, amount: Intra);
134 float MixY = mix(a: (float)pPrevExtendedData->m_TargetY, b: (float)pExtendedData->m_TargetY, amount: Intra);
135 return angle(a: vec2(MixX, MixY));
136 }
137 else
138 {
139 return angle(a: vec2(pExtendedData->m_TargetX, pExtendedData->m_TargetY));
140 }
141 }
142 else
143 {
144 // If the player moves their weapon through top, then change
145 // the end angle by 2*Pi, so that the mix function will use the
146 // short path and not the long one.
147 if(pPlayerChar->m_Angle > (256.0f * pi) && pPrevChar->m_Angle < 0)
148 {
149 return mix(a: (float)pPrevChar->m_Angle, b: (float)(pPlayerChar->m_Angle - 256.0f * 2 * pi), amount: Intra) / 256.0f;
150 }
151 else if(pPlayerChar->m_Angle < 0 && pPrevChar->m_Angle > (256.0f * pi))
152 {
153 return mix(a: (float)pPrevChar->m_Angle, b: (float)(pPlayerChar->m_Angle + 256.0f * 2 * pi), amount: Intra) / 256.0f;
154 }
155 else
156 {
157 return mix(a: (float)pPrevChar->m_Angle, b: (float)pPlayerChar->m_Angle, amount: Intra) / 256.0f;
158 }
159 }
160}
161
162void CPlayers::RenderHookCollLine(
163 const CScreenRect &ScreenRect,
164 const CNetObj_Character *pPrevChar,
165 const CNetObj_Character *pPlayerChar,
166 int ClientId)
167{
168 CNetObj_Character Prev;
169 CNetObj_Character Player;
170 Prev = *pPrevChar;
171 Player = *pPlayerChar;
172
173 dbg_assert(in_range(ClientId, MAX_CLIENTS - 1), "invalid client id (%d)", ClientId);
174
175 if(!GameClient()->m_GameInfo.m_AllowHookColl)
176 return;
177
178 bool Local = GameClient()->m_Snap.m_LocalClientId == ClientId;
179
180#if defined(CONF_VIDEORECORDER)
181 if(IVideo::Current() && !g_Config.m_ClVideoShowHookCollOther && !Local)
182 return;
183#endif
184
185 bool Aim = (Player.m_PlayerFlags & PLAYERFLAG_AIM);
186 if(!Client()->ServerCapAnyPlayerFlag())
187 {
188 for(int i = 0; i < NUM_DUMMIES; i++)
189 {
190 if(ClientId == GameClient()->m_aLocalIds[i])
191 {
192 Aim = GameClient()->m_Controls.m_aShowHookColl[i];
193 break;
194 }
195 }
196 }
197
198 if(GameClient()->PredictDummy() && g_Config.m_ClDummyCopyMoves && GameClient()->m_aLocalIds[!g_Config.m_ClDummy] == ClientId)
199 Aim = false; // don't use unpredicted with copy moves
200
201 bool AlwaysRenderHookColl = (Local ? g_Config.m_ClShowHookCollOwn : g_Config.m_ClShowHookCollOther) == 2;
202 bool RenderHookCollPlayer = Aim && (Local ? g_Config.m_ClShowHookCollOwn : g_Config.m_ClShowHookCollOther) > 0;
203 if(Local && Client()->State() != IClient::STATE_DEMOPLAYBACK)
204 RenderHookCollPlayer = GameClient()->m_Controls.m_aShowHookColl[g_Config.m_ClDummy] && g_Config.m_ClShowHookCollOwn > 0;
205
206 if(GameClient()->PredictDummy() && g_Config.m_ClDummyCopyMoves &&
207 GameClient()->m_aLocalIds[!g_Config.m_ClDummy] == ClientId && GameClient()->m_Controls.m_aShowHookColl[g_Config.m_ClDummy] &&
208 Client()->State() != IClient::STATE_DEMOPLAYBACK)
209 {
210 RenderHookCollPlayer = g_Config.m_ClShowHookCollOther > 0;
211 }
212
213 if(!AlwaysRenderHookColl && !RenderHookCollPlayer)
214 return;
215
216 float Intra = GameClient()->m_aClients[ClientId].m_IsPredicted ? Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy) : Client()->IntraGameTick(Conn: g_Config.m_ClDummy);
217 float Angle = GetPlayerTargetAngle(pPrevChar: &Prev, pPlayerChar: &Player, ClientId, Intra);
218
219 vec2 Position = GameClient()->m_aClients[ClientId].m_RenderPos;
220 vec2 Direction = direction(angle: Angle);
221
222 // When the other player isn't predicted, we don't know their tunes.
223 // Use our own tunes instead. This is wrong, but a good heuristic.
224 const CCharacterCore &PlayerCore = GameClient()->m_aClients[ClientId].m_IsPredicted ? GameClient()->m_aClients[ClientId].m_Predicted : GameClient()->m_aClients[GameClient()->m_aLocalIds[g_Config.m_ClDummy]].m_Predicted;
225 float HookLength = PlayerCore.m_Tuning.m_HookLength;
226 float HookFireSpeed = PlayerCore.m_Tuning.m_HookFireSpeed;
227
228 // Check, if the player is outside the screen-rect
229 // If the map contains hook teleports, we are out of luck since we don't know if it will enter the screen at any point.
230 if(!Collision()->HasHookTeleIns())
231 {
232 const float MaxHookReach = HookLength + HookFireSpeed;
233
234 if(Position.x < ScreenRect.m_TopLeft.x - (Direction.x >= 0 ? MaxHookReach : 0) ||
235 Position.x > ScreenRect.m_BottomRight.x + (Direction.x <= 0 ? MaxHookReach : 0) ||
236 Position.y < ScreenRect.m_TopLeft.y - (Direction.y >= 0 ? MaxHookReach : 0) ||
237 Position.y > ScreenRect.m_BottomRight.y + (Direction.y <= 0 ? MaxHookReach : 0))
238 {
239 return;
240 }
241 }
242
243 static constexpr float HOOK_START_DISTANCE = CCharacterCore::PhysicalSize() * 1.5f;
244
245 // janky physics
246 if(HookLength < HOOK_START_DISTANCE || HookFireSpeed <= 0.0f)
247 return;
248
249 vec2 QuantizedDirection = Direction;
250 vec2 StartOffset = Direction * HOOK_START_DISTANCE;
251 vec2 BasePos = Position;
252 vec2 LineStartPos = BasePos + StartOffset;
253 vec2 SegmentStartPos = LineStartPos;
254
255 ColorRGBA HookCollColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClHookCollColorNoColl));
256 std::vector<IGraphics::CLineItem> vLineSegments;
257
258 const int MaxHookTicks = 5 * Client()->GameTickSpeed(); // calculating above 5 seconds is very expensive and unlikely to happen
259
260 auto AddHookPlayerSegment = [&](const vec2 &StartPos, const vec2 &EndPos, const vec2 &HookablePlayerPosition, const vec2 &HitPos) {
261 HookCollColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClHookCollColorTeeColl));
262
263 // stop hookline at player circle so it looks better
264 vec2 aIntersections[2];
265 int NumIntersections = intersect_line_circle(LineStart: StartPos, LineEnd: EndPos, CircleCenter: HookablePlayerPosition, Radius: CCharacterCore::PhysicalSize() * 1.45f / 2.0f, aIntersections);
266 if(NumIntersections == 2)
267 {
268 if(distance(a: Position, b: aIntersections[0]) < distance(a: Position, b: aIntersections[1]))
269 vLineSegments.emplace_back(args: StartPos, args&: aIntersections[0]);
270 else
271 vLineSegments.emplace_back(args: StartPos, args&: aIntersections[1]);
272 }
273 else if(NumIntersections == 1)
274 {
275 vLineSegments.emplace_back(args: StartPos, args&: aIntersections[0]);
276 }
277 else
278 {
279 vLineSegments.emplace_back(args: StartPos, args: HitPos);
280 }
281 };
282
283 // simulate the hook into the future
284 int HookTick;
285 bool HookEnteredTelehook = false;
286 std::optional<IGraphics::CLineItem> HookTipLineSegment;
287 for(HookTick = 0; HookTick < MaxHookTicks; ++HookTick)
288 {
289 int Tele;
290 vec2 HitPos, IntersectedPlayerPosition;
291 vec2 SegmentEndPos = SegmentStartPos + QuantizedDirection * HookFireSpeed;
292
293 // check if a hook would enter retracting state in this tick
294 if(distance(a: BasePos, b: SegmentEndPos) > HookLength)
295 {
296 // check if the retracting hook hits a player
297 if(!HookEnteredTelehook)
298 {
299 vec2 RetractingHookEndPos = BasePos + normalize(v: SegmentEndPos - BasePos) * HookLength;
300 // you can't hook a player, if the hook is behind solids, however you miss the solids as well
301 int Hit = Collision()->IntersectLineTeleHook(Pos0: SegmentStartPos, Pos1: RetractingHookEndPos, pOutCollision: &HitPos, pOutBeforeCollision: nullptr, pTeleNr: &Tele);
302
303 if(GameClient()->IntersectCharacter(HookPos: SegmentStartPos, NewPos: HitPos, NewPos2&: RetractingHookEndPos, OwnId: ClientId, pPlayerPosition: &IntersectedPlayerPosition) != -1)
304 {
305 AddHookPlayerSegment(LineStartPos, SegmentEndPos, IntersectedPlayerPosition, RetractingHookEndPos);
306 break;
307 }
308
309 // Retracting hooks don't go through hook teleporters
310 if(Hit && Hit != TILE_TELEINHOOK)
311 {
312 // The hook misses the player, but also misses the solid
313 vLineSegments.emplace_back(args&: LineStartPos, args&: SegmentStartPos);
314
315 // The player hook misses due to a solid
316 HookTipLineSegment = IGraphics::CLineItem(SegmentStartPos, HitPos);
317 break;
318 }
319
320 // we are missing the player, the solid hookline stopped already, but we want this extra line segment
321 // the player-hooking-hook is only longer, if we didn't go through a tele hook
322 HookTipLineSegment = IGraphics::CLineItem(SegmentStartPos, RetractingHookEndPos);
323 }
324
325 // the line is too long here, and the hook retracts, use old position
326 vLineSegments.emplace_back(args&: LineStartPos, args&: SegmentStartPos);
327 break;
328 }
329
330 // check for map collisions
331 int Hit = Collision()->IntersectLineTeleHook(Pos0: SegmentStartPos, Pos1: SegmentEndPos, pOutCollision: &HitPos, pOutBeforeCollision: nullptr, pTeleNr: &Tele);
332
333 // check if we intersect a player
334 if(GameClient()->IntersectCharacter(HookPos: SegmentStartPos, NewPos: HitPos, NewPos2&: SegmentEndPos, OwnId: ClientId, pPlayerPosition: &IntersectedPlayerPosition) != -1)
335 {
336 AddHookPlayerSegment(LineStartPos, HitPos, IntersectedPlayerPosition, SegmentEndPos);
337 break;
338 }
339
340 // we hit nothing, continue calculating segments
341 if(!Hit)
342 {
343 SegmentStartPos = SegmentEndPos;
344 SegmentStartPos.x = round_to_int(f: SegmentStartPos.x);
345 SegmentStartPos.y = round_to_int(f: SegmentStartPos.y);
346
347 // direction is always the same after the first tick quantization
348 if(HookTick == 0)
349 {
350 QuantizedDirection.x = round_to_int(f: QuantizedDirection.x * 256.0f) / 256.0f;
351 QuantizedDirection.y = round_to_int(f: QuantizedDirection.y * 256.0f) / 256.0f;
352 }
353 continue;
354 }
355
356 // we hit a solid / hook stopper
357 if(Hit != TILE_TELEINHOOK)
358 {
359 if(Hit != TILE_NOHOOK)
360 HookCollColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClHookCollColorHookableColl));
361 vLineSegments.emplace_back(args&: LineStartPos, args&: HitPos);
362 break;
363 }
364
365 // we are hitting TILE_TELEINHOOK
366 vLineSegments.emplace_back(args&: LineStartPos, args&: HitPos);
367 HookEnteredTelehook = true;
368
369 // check tele outs
370 const std::vector<vec2> &vTeleOuts = Collision()->TeleOuts(Number: Tele - 1);
371 if(vTeleOuts.empty())
372 {
373 // the hook gets stuck, this is a feature or a bug
374 HookCollColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClHookCollColorHookableColl));
375 break;
376 }
377 else if(vTeleOuts.size() > 1)
378 {
379 // we don't know which teleout the hook takes, just invert the color
380 HookCollColor = color_invert(col: color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClHookCollColorTeeColl)));
381 break;
382 }
383
384 // go through one teleout, update positions and continue
385 BasePos = vTeleOuts[0];
386 LineStartPos = BasePos; // make the line start in the teleporter to prevent a gap
387 SegmentStartPos = BasePos + Direction * HOOK_START_DISTANCE;
388 SegmentStartPos.x = round_to_int(f: SegmentStartPos.x);
389 SegmentStartPos.y = round_to_int(f: SegmentStartPos.y);
390
391 // direction is always the same after the first tick quantization
392 if(HookTick == 0)
393 {
394 QuantizedDirection.x = round_to_int(f: QuantizedDirection.x * 256.0f) / 256.0f;
395 QuantizedDirection.y = round_to_int(f: QuantizedDirection.y * 256.0f) / 256.0f;
396 }
397 }
398
399 // The hook line is too expensive to calculate and didn't hit anything before, just set a straight line
400 if(HookTick >= MaxHookTicks && vLineSegments.empty())
401 {
402 // we simply don't know if we hit anything or not
403 HookCollColor = color_invert(col: color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClHookCollColorTeeColl)));
404 vLineSegments.emplace_back(args&: LineStartPos, args: BasePos + QuantizedDirection * HookLength);
405 }
406
407 // add a line from the player to the start position to prevent a visual gap
408 vLineSegments.emplace_back(args&: Position, args: Position + StartOffset);
409
410 if(AlwaysRenderHookColl && RenderHookCollPlayer)
411 {
412 // invert the hook coll colors when using cl_show_hook_coll_always and +showhookcoll is pressed
413 HookCollColor = color_invert(col: HookCollColor);
414 }
415
416 // Render hook coll line
417 const int HookCollSize = Local ? g_Config.m_ClHookCollSize : g_Config.m_ClHookCollSizeOther;
418
419 float Alpha = GameClient()->IsOtherTeam(ClientId) ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.0f;
420 Alpha *= (float)g_Config.m_ClHookCollAlpha / 100;
421 if(Alpha <= 0.0f)
422 return;
423 ColorRGBA HookCollTipColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClHookCollTipColor, true));
424
425 Graphics()->TextureClear();
426 if(HookCollSize > 0)
427 {
428 std::vector<IGraphics::CFreeformItem> vLineQuadSegments;
429 vLineQuadSegments.reserve(n: vLineSegments.size());
430
431 float LineWidth = 0.5f + (float)(HookCollSize - 1) * 0.25f;
432 const vec2 PerpToAngle = normalize(v: vec2(Direction.y, -Direction.x)) * GameClient()->m_Camera.m_Zoom;
433
434 auto ConvertLineSegments = [&](const IGraphics::CLineItem &LineSegment) {
435 vec2 DrawInitPos(LineSegment.m_X0, LineSegment.m_Y0);
436 vec2 DrawFinishPos(LineSegment.m_X1, LineSegment.m_Y1);
437 vec2 Pos0 = DrawFinishPos + PerpToAngle * -LineWidth;
438 vec2 Pos1 = DrawFinishPos + PerpToAngle * LineWidth;
439 vec2 Pos2 = DrawInitPos + PerpToAngle * -LineWidth;
440 vec2 Pos3 = DrawInitPos + PerpToAngle * LineWidth;
441 vLineQuadSegments.emplace_back(args&: Pos0.x, args&: Pos0.y, args&: Pos1.x, args&: Pos1.y, args&: Pos2.x, args&: Pos2.y, args&: Pos3.x, args&: Pos3.y);
442 };
443
444 for(const auto &LineSegment : vLineSegments)
445 {
446 ConvertLineSegments(LineSegment);
447 }
448
449 vLineSegments.clear();
450
451 Graphics()->QuadsBegin();
452 Graphics()->SetColor(HookCollColor.WithAlpha(alpha: Alpha));
453 Graphics()->QuadsDrawFreeform(pArray: vLineQuadSegments.data(), Num: vLineQuadSegments.size());
454 if(HookTipLineSegment.has_value() && HookCollTipColor.a > 0.0f)
455 {
456 vLineQuadSegments.clear();
457 ConvertLineSegments(HookTipLineSegment.value());
458 Graphics()->SetColor(HookCollTipColor.WithMultipliedAlpha(alpha: Alpha));
459 Graphics()->QuadsDrawFreeform(pArray: vLineQuadSegments.data(), Num: vLineQuadSegments.size());
460 }
461 Graphics()->QuadsEnd();
462 }
463 else
464 {
465 Graphics()->LinesBegin();
466 Graphics()->SetColor(HookCollColor.WithAlpha(alpha: Alpha));
467 Graphics()->LinesDraw(pArray: vLineSegments.data(), Num: vLineSegments.size());
468 if(HookTipLineSegment.has_value() && HookCollTipColor.a > 0.0f)
469 {
470 Graphics()->SetColor(HookCollTipColor.WithMultipliedAlpha(alpha: Alpha));
471 Graphics()->LinesDraw(pArray: &HookTipLineSegment.value(), Num: 1);
472 }
473 Graphics()->LinesEnd();
474 }
475}
476
477void CPlayers::RenderHook(
478 const CScreenRect &ScreenRect,
479 const CNetObj_Character *pPrevChar,
480 const CNetObj_Character *pPlayerChar,
481 const CTeeRenderInfo *pRenderInfo,
482 int ClientId,
483 float Intra)
484{
485 if(pPlayerChar->m_HookState <= 0)
486 return;
487
488 CNetObj_Character Prev;
489 CNetObj_Character Player;
490 Prev = *pPrevChar;
491 Player = *pPlayerChar;
492
493 CTeeRenderInfo RenderInfo = *pRenderInfo;
494
495 // don't render hooks to not active character cores
496 if(pPlayerChar->m_HookedPlayer != -1 && !GameClient()->m_Snap.m_aCharacters[pPlayerChar->m_HookedPlayer].m_Active)
497 return;
498
499 if(ClientId >= 0)
500 Intra = GameClient()->m_aClients[ClientId].m_IsPredicted ? Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy) : Client()->IntraGameTick(Conn: g_Config.m_ClDummy);
501
502 bool OtherTeam = GameClient()->IsOtherTeam(ClientId);
503 float Alpha = (OtherTeam || ClientId < 0) ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.0f;
504 if(ClientId == -2) // ghost
505 Alpha = g_Config.m_ClRaceGhostAlpha / 100.0f;
506
507 RenderInfo.m_Size = 64.0f;
508
509 vec2 Position;
510 if(in_range(a: ClientId, upper: MAX_CLIENTS - 1))
511 Position = GameClient()->m_aClients[ClientId].m_RenderPos;
512 else
513 Position = mix(a: vec2(Prev.m_X, Prev.m_Y), b: vec2(Player.m_X, Player.m_Y), amount: Intra);
514
515 // draw hook
516 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
517 if(ClientId < 0)
518 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.5f);
519
520 vec2 Pos = Position;
521 vec2 HookPos;
522
523 if(in_range(a: pPlayerChar->m_HookedPlayer, upper: MAX_CLIENTS - 1))
524 HookPos = GameClient()->m_aClients[pPlayerChar->m_HookedPlayer].m_RenderPos;
525 else
526 HookPos = mix(a: vec2(Prev.m_HookX, Prev.m_HookY), b: vec2(Player.m_HookX, Player.m_HookY), amount: Intra);
527
528 if((Pos.x < ScreenRect.m_TopLeft.x && HookPos.x < ScreenRect.m_TopLeft.x) ||
529 (Pos.x > ScreenRect.m_BottomRight.x && HookPos.x > ScreenRect.m_BottomRight.x) ||
530 (Pos.y < ScreenRect.m_TopLeft.y && HookPos.y < ScreenRect.m_TopLeft.y) ||
531 (Pos.y > ScreenRect.m_BottomRight.y && HookPos.y > ScreenRect.m_BottomRight.y))
532 return;
533
534 float d = distance(a: Pos, b: HookPos);
535 vec2 Dir = normalize(v: Pos - HookPos);
536
537 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpriteHookHead);
538 Graphics()->QuadsSetRotation(Angle: angle(a: Dir) + pi);
539 // render head
540 int QuadOffset = NUM_WEAPONS * 2 + 2;
541 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: Alpha);
542 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_WeaponEmoteQuadContainerIndex, QuadOffset, X: HookPos.x, Y: HookPos.y);
543
544 // render chain
545 ++QuadOffset;
546 static IGraphics::SRenderSpriteInfo s_aHookChainRenderInfo[1024];
547 int HookChainCount = 0;
548 for(float f = 24; f < d && HookChainCount < 1024; f += 24, ++HookChainCount)
549 {
550 vec2 p = HookPos + Dir * f;
551 s_aHookChainRenderInfo[HookChainCount].m_Pos[0] = p.x;
552 s_aHookChainRenderInfo[HookChainCount].m_Pos[1] = p.y;
553 s_aHookChainRenderInfo[HookChainCount].m_Scale = 1;
554 s_aHookChainRenderInfo[HookChainCount].m_Rotation = angle(a: Dir) + pi;
555 }
556 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpriteHookChain);
557 Graphics()->RenderQuadContainerAsSpriteMultiple(ContainerIndex: m_WeaponEmoteQuadContainerIndex, QuadOffset, DrawCount: HookChainCount, pRenderInfo: s_aHookChainRenderInfo);
558
559 Graphics()->QuadsSetRotation(Angle: 0);
560 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
561
562 RenderHand(pInfo: &RenderInfo, CenterPos: Position, Dir: normalize(v: HookPos - Pos), AngleOffset: -pi / 2, PostRotOffset: vec2(20, 0), Alpha);
563}
564
565void CPlayers::RenderPlayer(
566 const CScreenRect &ScreenRect,
567 const CNetObj_Character *pPrevChar,
568 const CNetObj_Character *pPlayerChar,
569 const CTeeRenderInfo *pRenderInfo,
570 int ClientId,
571 float Intra)
572{
573 CNetObj_Character Prev;
574 CNetObj_Character Player;
575 Prev = *pPrevChar;
576 Player = *pPlayerChar;
577
578 vec2 Position;
579 if(in_range(a: ClientId, upper: MAX_CLIENTS - 1))
580 Position = GameClient()->m_aClients[ClientId].m_RenderPos;
581 else
582 Position = mix(a: vec2(Prev.m_X, Prev.m_Y), b: vec2(Player.m_X, Player.m_Y), amount: Intra);
583
584 if(!ScreenRect.Inside(Position))
585 return;
586
587 CTeeRenderInfo RenderInfo = *pRenderInfo;
588
589 const bool Paused = GameClient()->IsWorldPaused() || GameClient()->IsDemoPlaybackPaused();
590 bool Local = GameClient()->m_Snap.m_LocalClientId == ClientId;
591 bool OtherTeam = GameClient()->IsOtherTeam(ClientId);
592 float Alpha = (OtherTeam || ClientId < 0) ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.0f;
593 if(ClientId == -2) // ghost
594 Alpha = g_Config.m_ClRaceGhostAlpha / 100.0f;
595 // TODO: snd_game_volume_others
596 const float Volume = 1.0f;
597
598 // set size
599 RenderInfo.m_Size = 64.0f;
600
601 if(ClientId >= 0)
602 Intra = GameClient()->m_aClients[ClientId].m_IsPredicted ? Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy) : Client()->IntraGameTick(Conn: g_Config.m_ClDummy);
603
604 static float s_LastGameTickTime = Client()->GameTickTime(Conn: g_Config.m_ClDummy);
605 static float s_LastPredIntraTick = Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy);
606 if(!Paused)
607 {
608 s_LastGameTickTime = Client()->GameTickTime(Conn: g_Config.m_ClDummy);
609 s_LastPredIntraTick = Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy);
610 }
611
612 bool PredictLocalWeapons = false;
613 float AttackTime = (Client()->PrevGameTick(Conn: g_Config.m_ClDummy) - Player.m_AttackTick) / (float)Client()->GameTickSpeed() + Client()->GameTickTime(Conn: g_Config.m_ClDummy);
614 float LastAttackTime = (Client()->PrevGameTick(Conn: g_Config.m_ClDummy) - Player.m_AttackTick) / (float)Client()->GameTickSpeed() + s_LastGameTickTime;
615 if(ClientId >= 0 && GameClient()->m_aClients[ClientId].m_IsPredictedLocal && GameClient()->AntiPingGunfire())
616 {
617 PredictLocalWeapons = true;
618 AttackTime = (Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy) + (Client()->PredGameTick(Conn: g_Config.m_ClDummy) - 1 - Player.m_AttackTick)) / (float)Client()->GameTickSpeed();
619 LastAttackTime = (s_LastPredIntraTick + (Client()->PredGameTick(Conn: g_Config.m_ClDummy) - 1 - Player.m_AttackTick)) / (float)Client()->GameTickSpeed();
620 }
621 float AttackTicksPassed = AttackTime * (float)Client()->GameTickSpeed();
622
623 float Angle = GetPlayerTargetAngle(pPrevChar: &Prev, pPlayerChar: &Player, ClientId, Intra);
624
625 vec2 Direction = direction(angle: Angle);
626 vec2 Vel = mix(a: vec2(Prev.m_VelX / 256.0f, Prev.m_VelY / 256.0f), b: vec2(Player.m_VelX / 256.0f, Player.m_VelY / 256.0f), amount: Intra);
627
628 GameClient()->m_Flow.Add(Pos: Position, Vel: Vel * 100.0f, Size: 10.0f);
629
630 RenderInfo.m_GotAirJump = Player.m_Jumped & 2 ? false : true;
631
632 RenderInfo.m_FeetFlipped = false;
633
634 bool Stationary = Player.m_VelX <= 1 && Player.m_VelX >= -1;
635 bool InAir = !Collision()->CheckPoint(x: Player.m_X, y: Player.m_Y + 16);
636 bool Running = Player.m_VelX >= 5000 || Player.m_VelX <= -5000;
637 bool WantOtherDir = (Player.m_Direction == -1 && Vel.x > 0) || (Player.m_Direction == 1 && Vel.x < 0);
638 bool Inactive = ClientId >= 0 && (GameClient()->m_aClients[ClientId].m_Afk || GameClient()->m_aClients[ClientId].m_Paused);
639
640 // evaluate animation
641 float WalkTime = std::fmod(x: Position.x, y: 100.0f) / 100.0f;
642 float RunTime = std::fmod(x: Position.x, y: 200.0f) / 200.0f;
643
644 // Don't do a moon walk outside the left border
645 if(WalkTime < 0.0f)
646 WalkTime += 1.0f;
647 if(RunTime < 0.0f)
648 RunTime += 1.0f;
649
650 CAnimState State;
651 State.Set(pAnim: &g_pData->m_aAnimations[ANIM_BASE], Time: 0.0f);
652
653 if(InAir)
654 {
655 State.Add(pAnim: &g_pData->m_aAnimations[ANIM_INAIR], Time: 0.0f, Amount: 1.0f); // TODO: some sort of time here
656 }
657 else if(Stationary)
658 {
659 if(Inactive)
660 {
661 State.Add(pAnim: Direction.x < 0.0f ? &g_pData->m_aAnimations[ANIM_SIT_LEFT] : &g_pData->m_aAnimations[ANIM_SIT_RIGHT], Time: 0.0f, Amount: 1.0f); // TODO: some sort of time here
662 RenderInfo.m_FeetFlipped = true;
663 }
664 else
665 {
666 State.Add(pAnim: &g_pData->m_aAnimations[ANIM_IDLE], Time: 0.0f, Amount: 1.0f); // TODO: some sort of time here
667 }
668 }
669 else if(!WantOtherDir)
670 {
671 if(Running)
672 State.Add(pAnim: Player.m_VelX < 0 ? &g_pData->m_aAnimations[ANIM_RUN_LEFT] : &g_pData->m_aAnimations[ANIM_RUN_RIGHT], Time: RunTime, Amount: 1.0f);
673 else
674 State.Add(pAnim: &g_pData->m_aAnimations[ANIM_WALK], Time: WalkTime, Amount: 1.0f);
675 }
676
677 const float HammerAnimationTimeScale = 5.0f;
678 if(Player.m_Weapon == WEAPON_HAMMER)
679 State.Add(pAnim: &g_pData->m_aAnimations[ANIM_HAMMER_SWING], Time: std::clamp(val: LastAttackTime * HammerAnimationTimeScale, lo: 0.0f, hi: 1.0f), Amount: 1.0f);
680 if(Player.m_Weapon == WEAPON_NINJA)
681 State.Add(pAnim: &g_pData->m_aAnimations[ANIM_NINJA_SWING], Time: std::clamp(val: LastAttackTime * 2.0f, lo: 0.0f, hi: 1.0f), Amount: 1.0f);
682
683 // do skidding
684 if(!InAir && WantOtherDir && length(a: Vel * 50) > 500.0f)
685 GameClient()->m_Effects.SkidTrail(Pos: Position, Vel, Direction: Player.m_Direction, Alpha, Volume);
686
687 // draw gun
688 if(Player.m_Weapon >= 0)
689 {
690 if(!(RenderInfo.m_TeeRenderFlags & TEE_NO_WEAPON))
691 {
692 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: Alpha);
693
694 // normal weapons
695 int CurrentWeapon = std::clamp(val: Player.m_Weapon, lo: 0, hi: NUM_WEAPONS - 1);
696 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_aSpriteWeapons[CurrentWeapon]);
697 int QuadOffset = CurrentWeapon * 2 + (Direction.x < 0.0f ? 1 : 0);
698
699 float Recoil = 0.0f;
700 vec2 WeaponPosition;
701 bool IsSit = Inactive && !InAir && Stationary;
702
703 if(Player.m_Weapon == WEAPON_HAMMER)
704 {
705 // static position for hammer
706 WeaponPosition = Position + vec2(State.GetAttach()->m_X, State.GetAttach()->m_Y);
707 WeaponPosition.y += g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsety;
708 if(Direction.x < 0.0f)
709 WeaponPosition.x -= g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsetx;
710 if(IsSit)
711 WeaponPosition.y += 3.0f;
712
713 // if active and attack is under way, bash stuffs
714 if(!Inactive || LastAttackTime * HammerAnimationTimeScale < 1.0f)
715 {
716 if(Direction.x < 0)
717 Graphics()->QuadsSetRotation(Angle: -pi / 2.0f - State.GetAttach()->m_Angle * pi * 2.0f);
718 else
719 Graphics()->QuadsSetRotation(Angle: -pi / 2.0f + State.GetAttach()->m_Angle * pi * 2.0f);
720 }
721 else
722 {
723 Graphics()->QuadsSetRotation(Angle: Direction.x < 0.0f ? 100.0f : 500.0f);
724 }
725
726 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_WeaponEmoteQuadContainerIndex, QuadOffset, X: WeaponPosition.x, Y: WeaponPosition.y);
727 }
728 else if(Player.m_Weapon == WEAPON_NINJA)
729 {
730 WeaponPosition = Position;
731 WeaponPosition.y += g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsety;
732 if(IsSit)
733 WeaponPosition.y += 3.0f;
734
735 if(Direction.x < 0.0f)
736 {
737 Graphics()->QuadsSetRotation(Angle: -pi / 2 - State.GetAttach()->m_Angle * pi * 2.0f);
738 WeaponPosition.x -= g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsetx;
739 GameClient()->m_Effects.PowerupShine(Pos: WeaponPosition + vec2(32.0f, 0.0f), Size: vec2(32.0f, 12.0f), Alpha);
740 }
741 else
742 {
743 Graphics()->QuadsSetRotation(Angle: -pi / 2 + State.GetAttach()->m_Angle * pi * 2.0f);
744 GameClient()->m_Effects.PowerupShine(Pos: WeaponPosition - vec2(32.0f, 0.0f), Size: vec2(32.0f, 12.0f), Alpha);
745 }
746 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_WeaponEmoteQuadContainerIndex, QuadOffset, X: WeaponPosition.x, Y: WeaponPosition.y);
747
748 // HADOKEN
749 if(AttackTime <= 1.0f / 6.0f && g_pData->m_Weapons.m_aId[CurrentWeapon].m_NumSpriteMuzzles)
750 {
751 int IteX = rand() % g_pData->m_Weapons.m_aId[CurrentWeapon].m_NumSpriteMuzzles;
752 static int s_LastIteX = IteX;
753 if(Paused)
754 {
755 IteX = s_LastIteX;
756 }
757 else
758 {
759 s_LastIteX = IteX;
760 }
761 if(g_pData->m_Weapons.m_aId[CurrentWeapon].m_aSpriteMuzzles[IteX])
762 {
763 vec2 HadokenDirection;
764 if(PredictLocalWeapons || ClientId < 0)
765 HadokenDirection = vec2(pPlayerChar->m_X, pPlayerChar->m_Y) - vec2(pPrevChar->m_X, pPrevChar->m_Y);
766 else
767 HadokenDirection = vec2(GameClient()->m_Snap.m_aCharacters[ClientId].m_Cur.m_X, GameClient()->m_Snap.m_aCharacters[ClientId].m_Cur.m_Y) - vec2(GameClient()->m_Snap.m_aCharacters[ClientId].m_Prev.m_X, GameClient()->m_Snap.m_aCharacters[ClientId].m_Prev.m_Y);
768 float HadokenAngle = 0.0f;
769 if(absolute(a: HadokenDirection.x) > 0.0001f || absolute(a: HadokenDirection.y) > 0.0001f)
770 {
771 HadokenDirection = normalize(v: HadokenDirection);
772 HadokenAngle = angle(a: HadokenDirection);
773 }
774 else
775 {
776 HadokenDirection = vec2(1.0f, 0.0f);
777 }
778 Graphics()->QuadsSetRotation(Angle: HadokenAngle);
779 QuadOffset = IteX * 2;
780 WeaponPosition = Position;
781 float OffsetX = g_pData->m_Weapons.m_aId[CurrentWeapon].m_Muzzleoffsetx;
782 WeaponPosition -= HadokenDirection * OffsetX;
783 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_aaSpriteWeaponsMuzzles[CurrentWeapon][IteX]);
784 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_aWeaponSpriteMuzzleQuadContainerIndex[CurrentWeapon], QuadOffset, X: WeaponPosition.x, Y: WeaponPosition.y);
785 }
786 }
787 }
788 else
789 {
790 // TODO: should be an animation
791 Recoil = 0.0f;
792 float a = AttackTicksPassed / 5.0f;
793 if(a < 1.0f)
794 Recoil = std::sin(x: a * pi);
795 WeaponPosition = Position + Direction * g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsetx - Direction * Recoil * 10.0f;
796 WeaponPosition.y += g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsety;
797 if(IsSit)
798 WeaponPosition.y += 3.0f;
799 if(Player.m_Weapon == WEAPON_GUN && g_Config.m_ClOldGunPosition)
800 WeaponPosition.y -= 8.0f;
801 Graphics()->QuadsSetRotation(Angle: State.GetAttach()->m_Angle * pi * 2.0f + Angle);
802 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_WeaponEmoteQuadContainerIndex, QuadOffset, X: WeaponPosition.x, Y: WeaponPosition.y);
803 }
804
805 if(Player.m_Weapon == WEAPON_GUN || Player.m_Weapon == WEAPON_SHOTGUN)
806 {
807 // check if we're firing stuff
808 if(g_pData->m_Weapons.m_aId[CurrentWeapon].m_NumSpriteMuzzles) // prev.attackticks)
809 {
810 float AlphaMuzzle = 0.0f;
811 if(AttackTicksPassed < g_pData->m_Weapons.m_aId[CurrentWeapon].m_Muzzleduration + 3.0f)
812 {
813 float t = AttackTicksPassed / g_pData->m_Weapons.m_aId[CurrentWeapon].m_Muzzleduration;
814 AlphaMuzzle = mix(a: 2.0f, b: 0.0f, amount: std::clamp(val: t, lo: 0.0f, hi: 1.0f));
815 }
816
817 int IteX = rand() % g_pData->m_Weapons.m_aId[CurrentWeapon].m_NumSpriteMuzzles;
818 static int s_LastIteX = IteX;
819 if(Paused)
820 {
821 IteX = s_LastIteX;
822 }
823 else
824 {
825 s_LastIteX = IteX;
826 }
827 if(AlphaMuzzle > 0.0f && g_pData->m_Weapons.m_aId[CurrentWeapon].m_aSpriteMuzzles[IteX])
828 {
829 float OffsetY = -g_pData->m_Weapons.m_aId[CurrentWeapon].m_Muzzleoffsety;
830 QuadOffset = IteX * 2 + (Direction.x < 0.0f ? 1 : 0);
831 if(Direction.x < 0.0f)
832 OffsetY = -OffsetY;
833
834 vec2 DirectionY(-Direction.y, Direction.x);
835 vec2 MuzzlePos = WeaponPosition + Direction * g_pData->m_Weapons.m_aId[CurrentWeapon].m_Muzzleoffsetx + DirectionY * OffsetY;
836 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_aaSpriteWeaponsMuzzles[CurrentWeapon][IteX]);
837 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_aWeaponSpriteMuzzleQuadContainerIndex[CurrentWeapon], QuadOffset, X: MuzzlePos.x, Y: MuzzlePos.y);
838 }
839 }
840 }
841 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
842 Graphics()->QuadsSetRotation(Angle: 0.0f);
843
844 switch(Player.m_Weapon)
845 {
846 case WEAPON_GUN: RenderHand(pInfo: &RenderInfo, CenterPos: WeaponPosition, Dir: Direction, AngleOffset: -3.0f * pi / 4.0f, PostRotOffset: vec2(-15.0f, 4.0f), Alpha); break;
847 case WEAPON_SHOTGUN: RenderHand(pInfo: &RenderInfo, CenterPos: WeaponPosition, Dir: Direction, AngleOffset: -pi / 2.0f, PostRotOffset: vec2(-5.0f, 4.0f), Alpha); break;
848 case WEAPON_GRENADE: RenderHand(pInfo: &RenderInfo, CenterPos: WeaponPosition, Dir: Direction, AngleOffset: -pi / 2.0f, PostRotOffset: vec2(-4.0f, 7.0f), Alpha); break;
849 }
850 }
851 }
852
853 // render the "shadow" tee
854 if(g_Config.m_ClUnpredictedShadow == 3 || (Local && g_Config.m_ClUnpredictedShadow == 1) || (!Local && g_Config.m_ClUnpredictedShadow == 2))
855 {
856 vec2 ShadowPosition = Position;
857 if(ClientId >= 0)
858 ShadowPosition = mix(
859 a: vec2(GameClient()->m_Snap.m_aCharacters[ClientId].m_Prev.m_X, GameClient()->m_Snap.m_aCharacters[ClientId].m_Prev.m_Y),
860 b: vec2(GameClient()->m_Snap.m_aCharacters[ClientId].m_Cur.m_X, GameClient()->m_Snap.m_aCharacters[ClientId].m_Cur.m_Y),
861 amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
862
863 RenderTools()->RenderTee(pAnim: &State, pInfo: &RenderInfo, Emote: Player.m_Emote, Dir: Direction, Pos: ShadowPosition, Alpha: g_Config.m_ClUnpredictedShadowAlpha / 100.f); // render ghost
864 }
865
866 RenderTools()->RenderTee(pAnim: &State, pInfo: &RenderInfo, Emote: Player.m_Emote, Dir: Direction, Pos: Position, Alpha);
867
868 float TeeAnimScale, TeeBaseSize;
869 CRenderTools::GetRenderTeeAnimScaleAndBaseSize(pInfo: &RenderInfo, AnimScale&: TeeAnimScale, BaseSize&: TeeBaseSize);
870 vec2 BodyPos = Position + vec2(State.GetBody()->m_X, State.GetBody()->m_Y) * TeeAnimScale;
871 if(RenderInfo.m_TeeRenderFlags & TEE_EFFECT_FROZEN)
872 {
873 GameClient()->m_Effects.FreezingFlakes(Pos: BodyPos, Size: vec2(32, 32), Alpha);
874 }
875 if(RenderInfo.m_TeeRenderFlags & TEE_EFFECT_SPARKLE)
876 {
877 GameClient()->m_Effects.SparkleTrail(Pos: BodyPos, Alpha);
878 }
879
880 if(ClientId < 0)
881 return;
882
883 int QuadOffsetToEmoticon = NUM_WEAPONS * 2 + 2 + 2;
884 if((Player.m_PlayerFlags & PLAYERFLAG_CHATTING) && !GameClient()->m_aClients[ClientId].m_Afk)
885 {
886 int CurEmoticon = (SPRITE_DOTDOT - SPRITE_OOP);
887 Graphics()->TextureSet(Texture: GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[CurEmoticon]);
888 int QuadOffset = QuadOffsetToEmoticon + CurEmoticon;
889 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: Alpha);
890 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_WeaponEmoteQuadContainerIndex, QuadOffset, X: Position.x + 24.f, Y: Position.y - 40.f);
891
892 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
893 Graphics()->QuadsSetRotation(Angle: 0);
894 }
895
896 if(g_Config.m_ClAfkEmote && GameClient()->m_aClients[ClientId].m_Afk && ClientId != GameClient()->m_aLocalIds[!g_Config.m_ClDummy])
897 {
898 int CurEmoticon = (SPRITE_ZZZ - SPRITE_OOP);
899 Graphics()->TextureSet(Texture: GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[CurEmoticon]);
900 int QuadOffset = QuadOffsetToEmoticon + CurEmoticon;
901 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: Alpha);
902 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_WeaponEmoteQuadContainerIndex, QuadOffset, X: Position.x + 24.f, Y: Position.y - 40.f);
903
904 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
905 Graphics()->QuadsSetRotation(Angle: 0);
906 }
907
908 if(g_Config.m_ClShowEmotes && !GameClient()->m_aClients[ClientId].m_EmoticonIgnore && GameClient()->m_aClients[ClientId].m_EmoticonStartTick != -1)
909 {
910 float SinceStart = (Client()->GameTick(Conn: g_Config.m_ClDummy) - GameClient()->m_aClients[ClientId].m_EmoticonStartTick) + (Client()->IntraGameTickSincePrev(Conn: g_Config.m_ClDummy) - GameClient()->m_aClients[ClientId].m_EmoticonStartFraction);
911 float FromEnd = (2 * Client()->GameTickSpeed()) - SinceStart;
912
913 if(0 <= SinceStart && FromEnd > 0)
914 {
915 float a = 1;
916
917 if(FromEnd < Client()->GameTickSpeed() / 5)
918 a = FromEnd / (Client()->GameTickSpeed() / 5.0f);
919
920 float h = 1;
921 if(SinceStart < Client()->GameTickSpeed() / 10)
922 h = SinceStart / (Client()->GameTickSpeed() / 10.0f);
923
924 float Wiggle = 0;
925 if(SinceStart < Client()->GameTickSpeed() / 5)
926 Wiggle = SinceStart / (Client()->GameTickSpeed() / 5.0f);
927
928 float WiggleAngle = std::sin(x: 5 * Wiggle);
929
930 Graphics()->QuadsSetRotation(Angle: pi / 6 * WiggleAngle);
931
932 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: a * Alpha);
933 // client_datas::emoticon is an offset from the first emoticon
934 int QuadOffset = QuadOffsetToEmoticon + GameClient()->m_aClients[ClientId].m_Emoticon;
935 Graphics()->TextureSet(Texture: GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[GameClient()->m_aClients[ClientId].m_Emoticon]);
936 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_WeaponEmoteQuadContainerIndex, QuadOffset, X: Position.x, Y: Position.y - 23.f - 32.f * h, ScaleX: 1.f, ScaleY: (64.f * h) / 64.f);
937
938 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
939 Graphics()->QuadsSetRotation(Angle: 0);
940 }
941 }
942}
943
944inline bool CPlayers::IsPlayerInfoAvailable(int ClientId) const
945{
946 return GameClient()->m_Snap.m_aCharacters[ClientId].m_Active &&
947 GameClient()->m_Snap.m_apPrevPlayerInfos[ClientId] != nullptr &&
948 GameClient()->m_Snap.m_apPlayerInfos[ClientId] != nullptr;
949}
950
951void CPlayers::OnRender()
952{
953 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
954 return;
955
956 // update render info for ninja
957 CTeeRenderInfo aRenderInfo[MAX_CLIENTS];
958 const bool IsTeamPlay = GameClient()->IsTeamPlay();
959 for(int i = 0; i < MAX_CLIENTS; ++i)
960 {
961 aRenderInfo[i] = GameClient()->m_aClients[i].m_RenderInfo;
962 aRenderInfo[i].m_TeeRenderFlags = 0;
963
964 // predict freeze skin only for local players
965 bool Frozen = false;
966 if(i == GameClient()->m_aLocalIds[0] || i == GameClient()->m_aLocalIds[1])
967 {
968 if(GameClient()->m_aClients[i].m_Predicted.m_FreezeEnd != 0)
969 aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_FROZEN | TEE_NO_WEAPON;
970 if(GameClient()->m_aClients[i].m_Predicted.m_LiveFrozen)
971 aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_FROZEN;
972 if(GameClient()->m_aClients[i].m_Predicted.m_Invincible)
973 aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_SPARKLE;
974
975 Frozen = GameClient()->m_aClients[i].m_Predicted.m_FreezeEnd != 0;
976 }
977 else
978 {
979 if(GameClient()->m_aClients[i].m_FreezeEnd != 0)
980 aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_FROZEN | TEE_NO_WEAPON;
981 if(GameClient()->m_aClients[i].m_LiveFrozen)
982 aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_FROZEN;
983 if(GameClient()->m_aClients[i].m_Invincible)
984 aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_SPARKLE;
985
986 Frozen = GameClient()->m_Snap.m_aCharacters[i].m_HasExtendedData && GameClient()->m_Snap.m_aCharacters[i].m_ExtendedData.m_FreezeEnd != 0;
987 }
988
989 if((GameClient()->m_aClients[i].m_RenderCur.m_Weapon == WEAPON_NINJA || (Frozen && !GameClient()->m_GameInfo.m_NoSkinChangeForFrozen)) && g_Config.m_ClShowNinja)
990 {
991 // change the skin for the player to the ninja
992 aRenderInfo[i].m_aSixup[g_Config.m_ClDummy].Reset();
993 aRenderInfo[i].ApplySkin(TeeRenderInfo: NinjaTeeRenderInfo()->TeeRenderInfo());
994 aRenderInfo[i].m_CustomColoredSkin = IsTeamPlay;
995 if(!IsTeamPlay)
996 {
997 aRenderInfo[i].m_ColorBody = ColorRGBA(1, 1, 1);
998 aRenderInfo[i].m_ColorFeet = ColorRGBA(1, 1, 1);
999 }
1000 }
1001 }
1002
1003 // get screen edges to avoid rendering offscreen
1004 CScreenRect ScreenRect = Graphics()->GetScreen();
1005 // expand the edges to prevent popping in/out onscreen
1006 //
1007 // it is assumed that the tee, all its weapons, and emotes fit into a 200x200 box centered on the tee
1008 // this may need to be changed or calculated differently in the future
1009 constexpr float PlayerBorderBuffer = 100.0f;
1010 ScreenRect.Expand(Size: PlayerBorderBuffer);
1011
1012 // render everyone else's hook, then our own
1013 const int LocalClientId = GameClient()->m_Snap.m_LocalClientId;
1014 for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++)
1015 {
1016 if(ClientId == LocalClientId || !IsPlayerInfoAvailable(ClientId))
1017 {
1018 continue;
1019 }
1020 RenderHook(ScreenRect, pPrevChar: &GameClient()->m_aClients[ClientId].m_RenderPrev, pPlayerChar: &GameClient()->m_aClients[ClientId].m_RenderCur, pRenderInfo: &aRenderInfo[ClientId], ClientId);
1021 }
1022 if(LocalClientId != -1 && IsPlayerInfoAvailable(ClientId: LocalClientId))
1023 {
1024 const CGameClient::CClientData *pLocalClientData = &GameClient()->m_aClients[LocalClientId];
1025 RenderHook(ScreenRect, pPrevChar: &pLocalClientData->m_RenderPrev, pPlayerChar: &pLocalClientData->m_RenderCur, pRenderInfo: &aRenderInfo[LocalClientId], ClientId: LocalClientId);
1026 }
1027
1028 // render spectating players
1029 for(const auto &Client : GameClient()->m_aClients)
1030 {
1031 if(!Client.m_SpecCharPresent)
1032 {
1033 continue;
1034 }
1035
1036 const int ClientId = Client.ClientId();
1037 float Alpha = (GameClient()->IsOtherTeam(ClientId) || ClientId < 0) ? g_Config.m_ClShowOthersAlpha / 100.f : 1.f;
1038 if(ClientId == -2) // ghost
1039 {
1040 Alpha = g_Config.m_ClRaceGhostAlpha / 100.f;
1041 }
1042 if(!ScreenRect.Inside(Position: Client.m_SpecChar))
1043 continue;
1044 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &SpectatorTeeRenderInfo()->TeeRenderInfo(), Emote: EMOTE_BLINK, Dir: vec2(1, 0), Pos: Client.m_SpecChar, Alpha);
1045 }
1046
1047 // render everyone else's tee, then either our own or the tee we are spectating.
1048 const int RenderLastId = (GameClient()->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW && GameClient()->m_Snap.m_SpecInfo.m_Active) ? GameClient()->m_Snap.m_SpecInfo.m_SpectatorId : LocalClientId;
1049
1050 for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++)
1051 {
1052 if(ClientId == RenderLastId || !IsPlayerInfoAvailable(ClientId))
1053 {
1054 continue;
1055 }
1056
1057 RenderHookCollLine(ScreenRect, pPrevChar: &GameClient()->m_aClients[ClientId].m_RenderPrev, pPlayerChar: &GameClient()->m_aClients[ClientId].m_RenderCur, ClientId);
1058 RenderPlayer(ScreenRect, pPrevChar: &GameClient()->m_aClients[ClientId].m_RenderPrev, pPlayerChar: &GameClient()->m_aClients[ClientId].m_RenderCur, pRenderInfo: &aRenderInfo[ClientId], ClientId);
1059 }
1060 if(RenderLastId != -1 && IsPlayerInfoAvailable(ClientId: RenderLastId))
1061 {
1062 const CGameClient::CClientData *pClientData = &GameClient()->m_aClients[RenderLastId];
1063 RenderHookCollLine(ScreenRect, pPrevChar: &pClientData->m_RenderPrev, pPlayerChar: &pClientData->m_RenderCur, ClientId: RenderLastId);
1064 RenderPlayer(ScreenRect, pPrevChar: &pClientData->m_RenderPrev, pPlayerChar: &pClientData->m_RenderCur, pRenderInfo: &aRenderInfo[RenderLastId], ClientId: RenderLastId);
1065 }
1066}
1067
1068void CPlayers::CreateNinjaTeeRenderInfo()
1069{
1070 CTeeRenderInfo NinjaTeeRenderInfo;
1071 NinjaTeeRenderInfo.m_Size = 64.0f;
1072 CSkinDescriptor NinjaSkinDescriptor;
1073 NinjaSkinDescriptor.m_Flags |= CSkinDescriptor::FLAG_SIX;
1074 str_copy(dst&: NinjaSkinDescriptor.m_aSkinName, src: "x_ninja");
1075 m_pNinjaTeeRenderInfo = GameClient()->CreateManagedTeeRenderInfo(TeeRenderInfo: NinjaTeeRenderInfo, SkinDescriptor: NinjaSkinDescriptor);
1076}
1077
1078void CPlayers::CreateSpectatorTeeRenderInfo()
1079{
1080 CTeeRenderInfo SpectatorTeeRenderInfo;
1081 SpectatorTeeRenderInfo.m_Size = 64.0f;
1082 CSkinDescriptor SpectatorSkinDescriptor;
1083 SpectatorSkinDescriptor.m_Flags |= CSkinDescriptor::FLAG_SIX;
1084 str_copy(dst&: SpectatorSkinDescriptor.m_aSkinName, src: "x_spec");
1085 m_pSpectatorTeeRenderInfo = GameClient()->CreateManagedTeeRenderInfo(TeeRenderInfo: SpectatorTeeRenderInfo, SkinDescriptor: SpectatorSkinDescriptor);
1086}
1087
1088void CPlayers::OnInit()
1089{
1090 m_WeaponEmoteQuadContainerIndex = Graphics()->CreateQuadContainer(AutomaticUpload: false);
1091
1092 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: 1.f);
1093
1094 for(int i = 0; i < NUM_WEAPONS; ++i)
1095 {
1096 float ScaleX, ScaleY;
1097 Graphics()->GetSpriteScale(pSprite: g_pData->m_Weapons.m_aId[i].m_pSpriteBody, ScaleX, ScaleY);
1098 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
1099 Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_WeaponEmoteQuadContainerIndex, Width: g_pData->m_Weapons.m_aId[i].m_VisualSize * ScaleX, Height: g_pData->m_Weapons.m_aId[i].m_VisualSize * ScaleY);
1100 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 1, BottomRightU: 1, BottomRightV: 0);
1101 Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_WeaponEmoteQuadContainerIndex, Width: g_pData->m_Weapons.m_aId[i].m_VisualSize * ScaleX, Height: g_pData->m_Weapons.m_aId[i].m_VisualSize * ScaleY);
1102 }
1103 float ScaleX, ScaleY;
1104
1105 // at the end the hand
1106 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
1107 Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_WeaponEmoteQuadContainerIndex, Size: 20.f);
1108 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
1109 Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_WeaponEmoteQuadContainerIndex, Size: 20.f);
1110
1111 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
1112 Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_WeaponEmoteQuadContainerIndex, X: -12.f, Y: -8.f, Width: 24.f, Height: 16.f);
1113 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
1114 Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_WeaponEmoteQuadContainerIndex, X: -12.f, Y: -8.f, Width: 24.f, Height: 16.f);
1115
1116 for(int i = 0; i < NUM_EMOTICONS; ++i)
1117 {
1118 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
1119 Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_WeaponEmoteQuadContainerIndex, Size: 64.f);
1120 }
1121 Graphics()->QuadContainerUpload(ContainerIndex: m_WeaponEmoteQuadContainerIndex);
1122
1123 for(int i = 0; i < NUM_WEAPONS; ++i)
1124 {
1125 m_aWeaponSpriteMuzzleQuadContainerIndex[i] = Graphics()->CreateQuadContainer(AutomaticUpload: false);
1126 for(int n = 0; n < g_pData->m_Weapons.m_aId[i].m_NumSpriteMuzzles; ++n)
1127 {
1128 if(g_pData->m_Weapons.m_aId[i].m_aSpriteMuzzles[n])
1129 {
1130 if(i == WEAPON_GUN || i == WEAPON_SHOTGUN)
1131 {
1132 // TODO: hardcoded for now to get the same particle size as before
1133 Graphics()->GetSpriteScaleImpl(Width: 96, Height: 64, ScaleX, ScaleY);
1134 }
1135 else
1136 {
1137 Graphics()->GetSpriteScale(pSprite: g_pData->m_Weapons.m_aId[i].m_aSpriteMuzzles[n], ScaleX, ScaleY);
1138 }
1139 }
1140
1141 float SWidth = (g_pData->m_Weapons.m_aId[i].m_VisualSize * ScaleX) * (4.0f / 3.0f);
1142 float SHeight = g_pData->m_Weapons.m_aId[i].m_VisualSize * ScaleY;
1143
1144 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
1145 if(WEAPON_NINJA == i)
1146 Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_aWeaponSpriteMuzzleQuadContainerIndex[i], Width: 160.f * ScaleX, Height: 160.f * ScaleY);
1147 else
1148 Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_aWeaponSpriteMuzzleQuadContainerIndex[i], Width: SWidth, Height: SHeight);
1149
1150 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 1, BottomRightU: 1, BottomRightV: 0);
1151 if(WEAPON_NINJA == i)
1152 Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_aWeaponSpriteMuzzleQuadContainerIndex[i], Width: 160.f * ScaleX, Height: 160.f * ScaleY);
1153 else
1154 Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_aWeaponSpriteMuzzleQuadContainerIndex[i], Width: SWidth, Height: SHeight);
1155 }
1156 Graphics()->QuadContainerUpload(ContainerIndex: m_aWeaponSpriteMuzzleQuadContainerIndex[i]);
1157 }
1158
1159 Graphics()->QuadsSetSubset(TopLeftU: 0.f, TopLeftV: 0.f, BottomRightU: 1.f, BottomRightV: 1.f);
1160 Graphics()->QuadsSetRotation(Angle: 0.f);
1161
1162 CreateNinjaTeeRenderInfo();
1163 CreateSpectatorTeeRenderInfo();
1164}
1165