/* (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 "players.h"

#include <base/color.h>
#include <base/math.h>

#include <engine/client/enums.h>
#include <engine/demo.h>
#include <engine/graphics.h>
#include <engine/shared/config.h>

#include <generated/client_data.h>
#include <generated/client_data7.h>
#include <generated/protocol.h>

#include <game/client/animstate.h>
#include <game/client/components/controls.h>
#include <game/client/components/effects.h>
#include <game/client/components/flow.h>
#include <game/client/components/skins.h>
#include <game/client/components/sounds.h>
#include <game/client/gameclient.h>
#include <game/gamecore.h>
#include <game/mapitems.h>

// TClient
#include <game/client/components/tclient/rainbow.h>
#include <game/client/prediction/entities/character.h>

static float CalculateHandAngle(vec2 Dir, float AngleOffset)
{
	const float Angle = angle(Dir);
	if(Dir.x < 0.0f)
	{
		return Angle - AngleOffset;
	}
	else
	{
		return Angle + AngleOffset;
	}
}

static vec2 CalculateHandPosition(vec2 CenterPos, vec2 Dir, vec2 PostRotOffset)
{
	vec2 DirY = vec2(-Dir.y, Dir.x);
	if(Dir.x < 0.0f)
	{
		DirY = -DirY;
	}
	return CenterPos + Dir + Dir * PostRotOffset.x + DirY * PostRotOffset.y;
}

void CPlayers::RenderHand(const CTeeRenderInfo *pInfo, vec2 CenterPos, vec2 Dir, float AngleOffset, vec2 PostRotOffset, float Alpha)
{
	const vec2 HandPos = CalculateHandPosition(CenterPos, Dir, PostRotOffset);
	const float HandAngle = CalculateHandAngle(Dir, AngleOffset);
	if(pInfo->m_aSixup[g_Config.m_ClDummy].PartTexture(protocol7::SKINPART_HANDS).IsValid())
	{
		RenderHand7(pInfo, HandPos, HandAngle, Alpha);
	}
	else
	{
		RenderHand6(pInfo, HandPos, HandAngle, Alpha);
	}
}

void CPlayers::RenderHand7(const CTeeRenderInfo *pInfo, vec2 HandPos, float HandAngle, float Alpha)
{
	// in-game hand size is 15 when tee size is 64
	const float BaseSize = 15.0f * (pInfo->m_Size / 64.0f);
	IGraphics::CQuadItem QuadOutline(HandPos.x, HandPos.y, 2 * BaseSize, 2 * BaseSize);
	IGraphics::CQuadItem QuadHand = QuadOutline;

	Graphics()->TextureSet(pInfo->m_aSixup[g_Config.m_ClDummy].PartTexture(protocol7::SKINPART_HANDS));
	Graphics()->QuadsBegin();
	Graphics()->SetColor(pInfo->m_aSixup[g_Config.m_ClDummy].m_aColors[protocol7::SKINPART_HANDS].WithAlpha(Alpha));
	Graphics()->QuadsSetRotation(HandAngle);
	Graphics()->SelectSprite7(client_data7::SPRITE_TEE_HAND_OUTLINE);
	Graphics()->QuadsDraw(&QuadOutline, 1);
	Graphics()->SelectSprite7(client_data7::SPRITE_TEE_HAND);
	Graphics()->QuadsDraw(&QuadHand, 1);
	Graphics()->QuadsEnd();
}

void CPlayers::RenderHand6(const CTeeRenderInfo *pInfo, vec2 HandPos, float HandAngle, float Alpha)
{
	const CSkin::CSkinTextures *pSkinTextures = pInfo->m_CustomColoredSkin ? &pInfo->m_ColorableRenderSkin : &pInfo->m_OriginalRenderSkin;

	if(!g_Config.m_TcRainbowTees) // TClient
		Graphics()->SetColor(pInfo->m_ColorBody.WithAlpha(Alpha));
	Graphics()->QuadsSetRotation(HandAngle);
	Graphics()->TextureSet(pSkinTextures->m_HandsOutline);
	Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, NUM_WEAPONS * 2, HandPos.x, HandPos.y);
	Graphics()->TextureSet(pSkinTextures->m_Hands);
	Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, NUM_WEAPONS * 2 + 1, HandPos.x, HandPos.y);
}

float CPlayers::GetPlayerTargetAngle(
	const CNetObj_Character *pPrevChar,
	const CNetObj_Character *pPlayerChar,
	int ClientId,
	float Intra)
{
	if(GameClient()->m_Snap.m_LocalClientId == ClientId && !GameClient()->m_Snap.m_SpecInfo.m_Active && Client()->State() != IClient::STATE_DEMOPLAYBACK)
	{
		// TClient
		vec2 Direction = GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy];
		if(g_Config.m_TcScaleMouseDistance)
		{
			const int MaxDistance = g_Config.m_ClDyncam ? g_Config.m_ClDyncamMaxDistance : g_Config.m_ClMouseMaxDistance;
			if(MaxDistance > 5 && MaxDistance < 1000) // Don't scale if angle bind or reduces precision
				Direction *= 1000.0f / (float)MaxDistance;
		}
		Direction.x = (int)Direction.x;
		Direction.y = (int)Direction.y;

		return angle(Direction);
	}

	// using unpredicted angle when rendering other players in-game
	if(ClientId >= 0)
		Intra = Client()->IntraGameTick(g_Config.m_ClDummy);

	if(ClientId >= 0 && GameClient()->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo)
	{
		const CNetObj_DDNetCharacter *pExtendedData = &GameClient()->m_Snap.m_aCharacters[ClientId].m_ExtendedData;
		const CNetObj_DDNetCharacter *pPrevExtendedData = GameClient()->m_Snap.m_aCharacters[ClientId].m_pPrevExtendedData;
		if(pPrevExtendedData)
		{
			float MixX = mix((float)pPrevExtendedData->m_TargetX, (float)pExtendedData->m_TargetX, Intra);
			float MixY = mix((float)pPrevExtendedData->m_TargetY, (float)pExtendedData->m_TargetY, Intra);
			return angle(vec2(MixX, MixY));
		}
		else
		{
			return angle(vec2(pExtendedData->m_TargetX, pExtendedData->m_TargetY));
		}
	}
	else
	{
		// If the player moves their weapon through top, then change
		// the end angle by 2*Pi, so that the mix function will use the
		// short path and not the long one.
		if(pPlayerChar->m_Angle > (256.0f * pi) && pPrevChar->m_Angle < 0)
		{
			return mix((float)pPrevChar->m_Angle, (float)(pPlayerChar->m_Angle - 256.0f * 2 * pi), Intra) / 256.0f;
		}
		else if(pPlayerChar->m_Angle < 0 && pPrevChar->m_Angle > (256.0f * pi))
		{
			return mix((float)pPrevChar->m_Angle, (float)(pPlayerChar->m_Angle + 256.0f * 2 * pi), Intra) / 256.0f;
		}
		else
		{
			return mix((float)pPrevChar->m_Angle, (float)pPlayerChar->m_Angle, Intra) / 256.0f;
		}
	}
}

void CPlayers::RenderHookCollLine(
	const CNetObj_Character *pPrevChar,
	const CNetObj_Character *pPlayerChar,
	int ClientId)
{
	// TClient
	if(ClientId >= 0 && GameClient()->m_aClients[ClientId].m_IsVolleyBall)
		return;

	CNetObj_Character Prev;
	CNetObj_Character Player;
	Prev = *pPrevChar;
	Player = *pPlayerChar;

	dbg_assert(in_range(ClientId, MAX_CLIENTS - 1), "invalid client id (%d)", ClientId);

	bool Local = GameClient()->m_Snap.m_LocalClientId == ClientId;

	float Intra = GameClient()->m_aClients[ClientId].m_IsPredicted ? Client()->PredIntraGameTick(g_Config.m_ClDummy) : Client()->IntraGameTick(g_Config.m_ClDummy);
	float Angle = GetPlayerTargetAngle(&Prev, &Player, ClientId, Intra);

	vec2 Direction = direction(Angle);
	vec2 Position = GameClient()->m_aClients[ClientId].m_RenderPos;

	bool Aim = (Player.m_PlayerFlags & PLAYERFLAG_AIM);
	if(!Client()->ServerCapAnyPlayerFlag())
	{
		for(int i = 0; i < NUM_DUMMIES; i++)
		{
			if(ClientId == GameClient()->m_aLocalIds[i])
			{
				Aim = GameClient()->m_Controls.m_aShowHookColl[i];
				break;
			}
		}
	}

#if defined(CONF_VIDEORECORDER)
	if(IVideo::Current() && !g_Config.m_ClVideoShowHookCollOther && !Local)
		return;
#endif

	bool AlwaysRenderHookColl = GameClient()->m_GameInfo.m_AllowHookColl && (Local ? g_Config.m_ClShowHookCollOwn : g_Config.m_ClShowHookCollOther) == 2;
	bool RenderHookCollPlayer = Aim && (Local ? g_Config.m_ClShowHookCollOwn : g_Config.m_ClShowHookCollOther) > 0;
	if(Local && GameClient()->m_GameInfo.m_AllowHookColl && Client()->State() != IClient::STATE_DEMOPLAYBACK)
		RenderHookCollPlayer = GameClient()->m_Controls.m_aShowHookColl[g_Config.m_ClDummy] && g_Config.m_ClShowHookCollOwn > 0;
	if(!AlwaysRenderHookColl && !RenderHookCollPlayer && (!(Local || GameClient()->m_GameInfo.m_AllowHookColl) || g_Config.m_ClHookCollAlwaysAlpha <= 0))
		return;

	static constexpr float HOOK_START_DISTANCE = CCharacterCore::PhysicalSize() * 1.5f;
	float HookLength = (float)GameClient()->m_aClients[ClientId].m_Predicted.m_Tuning.m_HookLength;
	float HookFireSpeed = (float)GameClient()->m_aClients[ClientId].m_Predicted.m_Tuning.m_HookFireSpeed;

	// janky physics
	if(HookLength < HOOK_START_DISTANCE || HookFireSpeed <= 0.0f)
		return;

	vec2 QuantizedDirection = Direction;
	vec2 StartOffset = Direction * HOOK_START_DISTANCE;
	vec2 BasePos = Position;
	vec2 LineStartPos = BasePos + StartOffset;
	vec2 SegmentStartPos = LineStartPos;

	ColorRGBA HookCollColor = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClHookCollColorNoColl));
	std::vector<IGraphics::CLineItem> vLineSegments;

	const int MaxHookTicks = 5 * Client()->GameTickSpeed(); // calculating above 5 seconds is very expensive and unlikely to happen

	// simulate the hook into the future
	int HookTick;
	bool HookEnteredTelehook = false;
	for(HookTick = 0; HookTick < MaxHookTicks; ++HookTick)
	{
		int Tele;
		vec2 HitPos;
		vec2 SegmentEndPos = SegmentStartPos + QuantizedDirection * HookFireSpeed;

		// check if a hook would enter retracting state in this tick
		if(distance(BasePos, SegmentEndPos) > HookLength)
		{
			// check if the retracting hook hits a player
			if(!HookEnteredTelehook)
			{
				vec2 RetractingHookEndPos = BasePos + normalize(SegmentEndPos - BasePos) * HookLength;
				if(GameClient()->IntersectCharacter(SegmentStartPos, RetractingHookEndPos, HitPos, ClientId) != -1)
				{
					HookCollColor = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClHookCollColorTeeColl));
					vLineSegments.emplace_back(LineStartPos, HitPos);
					break;
				}
			}

			// the line is too long here, and the hook retracts, use old position
			vLineSegments.emplace_back(LineStartPos, SegmentStartPos);
			break;
		}

		// check for map collisions
		int Hit = Collision()->IntersectLineTeleHook(SegmentStartPos, SegmentEndPos, &HitPos, nullptr, &Tele);

		// check if we intersect a player
		if(GameClient()->IntersectCharacter(SegmentStartPos, HitPos, SegmentEndPos, ClientId) != -1)
		{
			HookCollColor = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClHookCollColorTeeColl));
			vLineSegments.emplace_back(LineStartPos, SegmentEndPos);
			break;
		}

		// we hit nothing, continue calculating segments
		if(!Hit)
		{
			SegmentStartPos = SegmentEndPos;
			SegmentStartPos.x = round_to_int(SegmentStartPos.x);
			SegmentStartPos.y = round_to_int(SegmentStartPos.y);

			// direction is always the same after the first tick quantization
			if(HookTick == 0)
			{
				QuantizedDirection.x = round_to_int(QuantizedDirection.x * 256.0f) / 256.0f;
				QuantizedDirection.y = round_to_int(QuantizedDirection.y * 256.0f) / 256.0f;
			}
			continue;
		}

		// we hit a solid / hook stopper
		if(Hit != TILE_TELEINHOOK)
		{
			if(Hit != TILE_NOHOOK)
				HookCollColor = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClHookCollColorHookableColl));
			vLineSegments.emplace_back(LineStartPos, HitPos);
			break;
		}

		// we are hitting TILE_TELEINHOOK
		vLineSegments.emplace_back(LineStartPos, HitPos);
		HookEnteredTelehook = true;

		// check tele outs
		const std::vector<vec2> &vTeleOuts = Collision()->TeleOuts(Tele - 1);
		if(vTeleOuts.empty())
		{
			// the hook gets stuck, this is a feature or a bug
			HookCollColor = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClHookCollColorHookableColl));
			break;
		}
		else if(vTeleOuts.size() > 1)
		{
			// we don't know which teleout the hook takes, just invert the color
			HookCollColor = color_invert(color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClHookCollColorTeeColl)));
			break;
		}

		// go through one teleout, update positions and continue
		BasePos = vTeleOuts[0];
		LineStartPos = BasePos; // make the line start in the teleporter to prevent a gap
		SegmentStartPos = BasePos + Direction * HOOK_START_DISTANCE;
		SegmentStartPos.x = round_to_int(SegmentStartPos.x);
		SegmentStartPos.y = round_to_int(SegmentStartPos.y);

		// direction is always the same after the first tick quantization
		if(HookTick == 0)
		{
			QuantizedDirection.x = round_to_int(QuantizedDirection.x * 256.0f) / 256.0f;
			QuantizedDirection.y = round_to_int(QuantizedDirection.y * 256.0f) / 256.0f;
		}
	}

	// The hook line is too expensive to calculate and didn't hit anything before, just set a straight line
	if(HookTick >= MaxHookTicks && vLineSegments.empty())
	{
		// we simply don't know if we hit anything or not
		HookCollColor = color_invert(color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClHookCollColorTeeColl)));
		vLineSegments.emplace_back(LineStartPos, BasePos + QuantizedDirection * HookLength);
	}

	// add a line from the player to the start position to prevent a visual gap
	vLineSegments.emplace_back(Position, Position + StartOffset);

	if(AlwaysRenderHookColl && RenderHookCollPlayer)
	{
		// invert the hook coll colors when using cl_show_hook_coll_always and +showhookcoll is pressed
		HookCollColor = color_invert(HookCollColor);
	}

	// Render hook coll line
	const int HookCollSize = Local ? g_Config.m_ClHookCollSize : g_Config.m_ClHookCollSizeOther;

	float Alpha = GameClient()->IsOtherTeam(ClientId) ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.0f;
	Alpha *= (float)g_Config.m_ClHookCollAlpha / 100;
	if(!AlwaysRenderHookColl && !RenderHookCollPlayer)
		Alpha *= (float)g_Config.m_ClHookCollAlwaysAlpha / 100.0f;
	if(Alpha <= 0.0f)
		return;

	Graphics()->TextureClear();
	if(HookCollSize > 0)
	{
		std::vector<IGraphics::CFreeformItem> vLineQuadSegments;
		vLineQuadSegments.reserve(vLineSegments.size());

		float LineWidth = 0.5f + (float)(HookCollSize - 1) * 0.25f;
		const vec2 PerpToAngle = normalize(vec2(Direction.y, -Direction.x)) * GameClient()->m_Camera.m_Zoom;

		for(const auto &LineSegment : vLineSegments)
		{
			vec2 DrawInitPos(LineSegment.m_X0, LineSegment.m_Y0);
			vec2 DrawFinishPos(LineSegment.m_X1, LineSegment.m_Y1);
			vec2 Pos0 = DrawFinishPos + PerpToAngle * -LineWidth;
			vec2 Pos1 = DrawFinishPos + PerpToAngle * LineWidth;
			vec2 Pos2 = DrawInitPos + PerpToAngle * -LineWidth;
			vec2 Pos3 = DrawInitPos + PerpToAngle * LineWidth;
			vLineQuadSegments.emplace_back(Pos0.x, Pos0.y, Pos1.x, Pos1.y, Pos2.x, Pos2.y, Pos3.x, Pos3.y);
		}
		Graphics()->QuadsBegin();
		Graphics()->SetColor(HookCollColor.WithAlpha(Alpha));
		Graphics()->QuadsDrawFreeform(vLineQuadSegments.data(), vLineQuadSegments.size());
		Graphics()->QuadsEnd();
	}
	else
	{
		Graphics()->LinesBegin();
		Graphics()->SetColor(HookCollColor.WithAlpha(Alpha));
		Graphics()->LinesDraw(vLineSegments.data(), vLineSegments.size());
		Graphics()->LinesEnd();
	}
}

void CPlayers::RenderHook(
	const CNetObj_Character *pPrevChar,
	const CNetObj_Character *pPlayerChar,
	const CTeeRenderInfo *pRenderInfo,
	int ClientId,
	float Intra)
{
	if(pPrevChar->m_HookState <= 0 || pPlayerChar->m_HookState <= 0)
		return;

	CNetObj_Character Prev;
	CNetObj_Character Player;
	Prev = *pPrevChar;
	Player = *pPlayerChar;

	CTeeRenderInfo RenderInfo = *pRenderInfo;

	// don't render hooks to not active character cores
	if(pPlayerChar->m_HookedPlayer != -1 && !GameClient()->m_Snap.m_aCharacters[pPlayerChar->m_HookedPlayer].m_Active)
		return;

	if(ClientId >= 0)
		Intra = GameClient()->m_aClients[ClientId].m_IsPredicted ? Client()->PredIntraGameTick(g_Config.m_ClDummy) : Client()->IntraGameTick(g_Config.m_ClDummy);

	bool OtherTeam = GameClient()->IsOtherTeam(ClientId);
	float Alpha = (OtherTeam || ClientId < 0) ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.0f;
	if(ClientId == -2) // ghost
		Alpha = g_Config.m_ClRaceGhostAlpha / 100.0f;

	RenderInfo.m_Size = 64.0f;

	vec2 Position;
	if(in_range(ClientId, MAX_CLIENTS - 1))
		Position = GameClient()->m_aClients[ClientId].m_RenderPos;
	else
		Position = mix(vec2(Prev.m_X, Prev.m_Y), vec2(Player.m_X, Player.m_Y), Intra);

	// draw hook
	Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
	if(ClientId < 0)
		Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.5f);

	vec2 Pos = Position;
	vec2 HookPos;

	if(in_range(pPlayerChar->m_HookedPlayer, MAX_CLIENTS - 1))
	{
		HookPos = GameClient()->m_aClients[pPlayerChar->m_HookedPlayer].m_RenderPos;
		if(g_Config.m_TcSwapGhosts && Client()->State() != IClient::STATE_DEMOPLAYBACK && GameClient()->m_Snap.m_LocalClientId == ClientId)
		{
			HookPos = GameClient()->GetSmoothPos(pPlayerChar->m_HookedPlayer);
		}
	}
	else
		HookPos = mix(vec2(Prev.m_HookX, Prev.m_HookY), vec2(Player.m_HookX, Player.m_HookY), Intra);

	float d = distance(Pos, HookPos);
	vec2 Dir = normalize(Pos - HookPos);

	Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteHookHead);
	Graphics()->QuadsSetRotation(angle(Dir) + pi);
	// render head
	int QuadOffset = NUM_WEAPONS * 2 + 2;
	Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha);

	// TClient
	bool Local = GameClient()->m_Snap.m_LocalClientId == ClientId;
	bool DontOthers = !g_Config.m_TcRainbowOthers && !Local;
	if(g_Config.m_TcRainbowHook && !DontOthers)
		Graphics()->SetColor(GameClient()->m_Rainbow.m_RainbowColor.WithAlpha(Alpha));

	Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, HookPos.x, HookPos.y);

	// render chain
	++QuadOffset;
	static IGraphics::SRenderSpriteInfo s_aHookChainRenderInfo[1024];
	int HookChainCount = 0;
	for(float f = 24; f < d && HookChainCount < 1024; f += 24, ++HookChainCount)
	{
		vec2 p = HookPos + Dir * f;
		s_aHookChainRenderInfo[HookChainCount].m_Pos[0] = p.x;
		s_aHookChainRenderInfo[HookChainCount].m_Pos[1] = p.y;
		s_aHookChainRenderInfo[HookChainCount].m_Scale = 1;
		s_aHookChainRenderInfo[HookChainCount].m_Rotation = angle(Dir) + pi;
	}
	Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteHookChain);
	Graphics()->RenderQuadContainerAsSpriteMultiple(m_WeaponEmoteQuadContainerIndex, QuadOffset, HookChainCount, s_aHookChainRenderInfo);

	Graphics()->QuadsSetRotation(0);
	Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);

	if(g_Config.m_TcRainbowHook && !DontOthers)
		Graphics()->SetColor(GameClient()->m_Rainbow.m_RainbowColor.WithAlpha(Alpha));

	RenderHand(&RenderInfo, Position, normalize(HookPos - Pos), -pi / 2, vec2(20, 0), Alpha);
}

void CPlayers::RenderWeapon(
	const CNetObj_Character *pPrevChar,
	const CNetObj_Character *pPlayerChar,
	const CTeeRenderInfo *pRenderInfo,
	int ClientId,
	float Intra)
{
	// TClient
	if(GameClient()->m_aClients[ClientId].m_DeepFrozen && (g_Config.m_TcVolleyBallBetterBall == 2 || (g_Config.m_TcVolleyBallBetterBall == 1 && str_startswith_nocase(Client()->GetCurrentMap(), "volleyball"))))
		return;

	CNetObj_Character Prev;
	CNetObj_Character Player;
	Prev = *pPrevChar;
	Player = *pPlayerChar;

	CTeeRenderInfo RenderInfo = *pRenderInfo;

	bool Local = GameClient()->m_Snap.m_LocalClientId == ClientId;
	bool OtherTeam = GameClient()->IsOtherTeam(ClientId);
	// float Alpha = (OtherTeam || ClientId < 0) ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.0f;
	bool Spec = GameClient()->m_Snap.m_SpecInfo.m_Active;

	RenderTools()->m_LocalTeeRender = Local; // TClient

	float Alpha = 1.0f;
	if(OtherTeam || ClientId < 0)
		Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
	else if(g_Config.m_TcShowOthersGhosts && !Local && !Spec)
		Alpha = g_Config.m_TcPredGhostsAlpha / 100.0f;

	if(!OtherTeam && g_Config.m_TcShowOthersGhosts && !Local && g_Config.m_TcUnpredOthersInFreeze && Client()->m_IsLocalFrozen && !Spec)
		Alpha = 1.0f;

	if(ClientId == -2) // ghost
		Alpha = g_Config.m_ClRaceGhostAlpha / 100.0f;

	// set size
	RenderInfo.m_Size = 64.0f;

	if(ClientId >= 0)
		Intra = GameClient()->m_aClients[ClientId].m_IsPredicted ? Client()->PredIntraGameTick(g_Config.m_ClDummy) : Client()->IntraGameTick(g_Config.m_ClDummy);

	static float s_LastGameTickTime = Client()->GameTickTime(g_Config.m_ClDummy);
	static float s_LastPredIntraTick = Client()->PredIntraGameTick(g_Config.m_ClDummy);
	if(GameClient()->m_Snap.m_pGameInfoObj && !(GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED))
	{
		s_LastGameTickTime = Client()->GameTickTime(g_Config.m_ClDummy);
		s_LastPredIntraTick = Client()->PredIntraGameTick(g_Config.m_ClDummy);
	}

	bool PredictLocalWeapons = false;
	float AttackTime = (Client()->PrevGameTick(g_Config.m_ClDummy) - Player.m_AttackTick) / (float)Client()->GameTickSpeed() + Client()->GameTickTime(g_Config.m_ClDummy);
	float LastAttackTime = (Client()->PrevGameTick(g_Config.m_ClDummy) - Player.m_AttackTick) / (float)Client()->GameTickSpeed() + s_LastGameTickTime;
	if(ClientId >= 0 && GameClient()->m_aClients[ClientId].m_IsPredictedLocal && GameClient()->AntiPingGunfire())
	{
		PredictLocalWeapons = true;
		AttackTime = (Client()->PredIntraGameTick(g_Config.m_ClDummy) + (Client()->PredGameTick(g_Config.m_ClDummy) - 1 - Player.m_AttackTick)) / (float)Client()->GameTickSpeed();
		LastAttackTime = (s_LastPredIntraTick + (Client()->PredGameTick(g_Config.m_ClDummy) - 1 - Player.m_AttackTick)) / (float)Client()->GameTickSpeed();
	}
	float AttackTicksPassed = AttackTime * (float)Client()->GameTickSpeed();

	float Angle = GetPlayerTargetAngle(&Prev, &Player, ClientId, Intra);

	vec2 Direction = direction(Angle);
	vec2 Position;
	if(in_range(ClientId, MAX_CLIENTS - 1))
		Position = GameClient()->m_aClients[ClientId].m_RenderPos;
	else
		Position = mix(vec2(Prev.m_X, Prev.m_Y), vec2(Player.m_X, Player.m_Y), Intra);
	vec2 Vel = mix(vec2(Prev.m_VelX / 256.0f, Prev.m_VelY / 256.0f), vec2(Player.m_VelX / 256.0f, Player.m_VelY / 256.0f), Intra);

	// TClient
	if(g_Config.m_TcSwapGhosts && g_Config.m_TcShowOthersGhosts && !Local && Client()->State() != IClient::STATE_DEMOPLAYBACK && ClientId >= 0)
		Position = mix(
			vec2(GameClient()->m_Snap.m_aCharacters[ClientId].m_Prev.m_X, GameClient()->m_Snap.m_aCharacters[ClientId].m_Prev.m_Y),
			vec2(GameClient()->m_Snap.m_aCharacters[ClientId].m_Cur.m_X, GameClient()->m_Snap.m_aCharacters[ClientId].m_Cur.m_Y),
			Client()->IntraGameTick(g_Config.m_ClDummy));

	// TClient
	if(ClientId >= 0 && GameClient()->m_aClients[ClientId].m_IsVolleyBall)
		return;
	if(g_Config.m_TcFakeCtfFlags > 0)
		GameClient()->m_TClient.RenderCtfFlag(Position, Alpha);

	RenderInfo.m_GotAirJump = Player.m_Jumped & 2 ? false : true;

	RenderInfo.m_FeetFlipped = false;

	bool Stationary = Player.m_VelX <= 1 && Player.m_VelX >= -1;
	bool InAir = !Collision()->CheckPoint(Player.m_X, Player.m_Y + 16);
	if(g_Config.m_TcAntiPingImproved && !Local)
		InAir = !Collision()->CheckPoint(Position.x, Position.y + 16);
	bool Running = Player.m_VelX >= 5000 || Player.m_VelX <= -5000;
	bool WantOtherDir = (Player.m_Direction == -1 && Vel.x > 0) || (Player.m_Direction == 1 && Vel.x < 0);
	bool Inactive = ClientId >= 0 && (GameClient()->m_aClients[ClientId].m_Afk || GameClient()->m_aClients[ClientId].m_Paused);

	// evaluate animation
	float WalkTime = std::fmod(Position.x, 100.0f) / 100.0f;
	float RunTime = std::fmod(Position.x, 200.0f) / 200.0f;

	// Don't do a moon walk outside the left border
	if(WalkTime < 0.0f)
		WalkTime += 1.0f;
	if(RunTime < 0.0f)
		RunTime += 1.0f;

	CAnimState State;
	State.Set(&g_pData->m_aAnimations[ANIM_BASE], 0.0f);

	if(InAir)
		State.Add(&g_pData->m_aAnimations[ANIM_INAIR], 0.0f, 1.0f); // TODO: some sort of time here
	else if(Stationary)
	{
		if(Inactive)
		{
			State.Add(Direction.x < 0.0f ? &g_pData->m_aAnimations[ANIM_SIT_LEFT] : &g_pData->m_aAnimations[ANIM_SIT_RIGHT], 0.0f, 1.0f); // TODO: some sort of time here
			RenderInfo.m_FeetFlipped = true;
		}
		else
			State.Add(&g_pData->m_aAnimations[ANIM_IDLE], 0.0f, 1.0f); // TODO: some sort of time here
	}
	else if(!WantOtherDir)
	{
		if(Running)
			State.Add(Player.m_VelX < 0 ? &g_pData->m_aAnimations[ANIM_RUN_LEFT] : &g_pData->m_aAnimations[ANIM_RUN_RIGHT], RunTime, 1.0f);
		else
			State.Add(&g_pData->m_aAnimations[ANIM_WALK], WalkTime, 1.0f);
	}

	if(Player.m_Weapon == WEAPON_HAMMER)
		State.Add(&g_pData->m_aAnimations[ANIM_HAMMER_SWING], std::clamp(LastAttackTime * 5.0f, 0.0f, 1.0f), 1.0f);
	if(Player.m_Weapon == WEAPON_NINJA)
		State.Add(&g_pData->m_aAnimations[ANIM_NINJA_SWING], std::clamp(LastAttackTime * 2.0f, 0.0f, 1.0f), 1.0f);

	// draw gun
	if(Player.m_Weapon >= 0)
	{
		if(!(RenderInfo.m_TeeRenderFlags & TEE_NO_WEAPON))
		{
			Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha);

			// normal weapons
			int CurrentWeapon = std::clamp(Player.m_Weapon, 0, NUM_WEAPONS - 1);
			Graphics()->TextureSet(GameClient()->m_GameSkin.m_aSpriteWeapons[CurrentWeapon]);
			int QuadOffset = CurrentWeapon * 2 + (Direction.x < 0.0f ? 1 : 0);

			// TClient
			const bool DontOthers = !g_Config.m_TcRainbowOthers && !Local;
			if(g_Config.m_TcRainbowWeapon && !DontOthers)
				Graphics()->SetColor(GameClient()->m_Rainbow.m_RainbowColor.WithAlpha(Alpha));

			float Recoil = 0.0f;
			vec2 WeaponPosition;
			bool IsSit = Inactive && !InAir && Stationary;

			if(Player.m_Weapon == WEAPON_HAMMER)
			{
				// TODO: Make this less intrusive
				switch(g_Config.m_TcHammerRotatesWithCursor)
				{
				case 0:
				{
					// static position for hammer
					WeaponPosition = Position + vec2(State.GetAttach()->m_X, State.GetAttach()->m_Y);
					WeaponPosition.y += g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsety;
					if(Direction.x < 0)
						WeaponPosition.x -= g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsetx;
					if(IsSit)
						WeaponPosition.y += 3.0f;

					// if active and attack is under way, bash stuffs
					if(!Inactive || LastAttackTime < GameClient()->m_aClients[ClientId].m_Predicted.m_Tuning.GetWeaponFireDelay(Player.m_Weapon))
					{
						if(Direction.x < 0)
							Graphics()->QuadsSetRotation(-pi / 2 - State.GetAttach()->m_Angle * pi * 2);
						else
							Graphics()->QuadsSetRotation(-pi / 2 + State.GetAttach()->m_Angle * pi * 2);
					}
					else
						Graphics()->QuadsSetRotation(Direction.x < 0 ? 100.0f : 500.0f);

					Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, WeaponPosition.x, WeaponPosition.y);
					break;
				}
				case 1:
				{
					WeaponPosition = Position + vec2(State.GetAttach()->m_X, State.GetAttach()->m_Y);
					WeaponPosition.y += g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsety;
					if(Direction.x < 0.0f)
						WeaponPosition.x -= g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsetx;
					if(IsSit)
						WeaponPosition.y += 3.0f;

					// set rotation
					float QuadsRotation = -pi / 2.0f;
					QuadsRotation += State.GetAttach()->m_Angle * (Direction.x < 0 ? -1 : 1) * pi * 2;
					QuadsRotation += Angle;
					if(Direction.x < 0.0f)
						QuadsRotation += pi;

					Graphics()->QuadsSetRotation(QuadsRotation);
					Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, WeaponPosition.x, WeaponPosition.y);
					break;
				}
				case 2:
				{
					// TODO: should be an animation
					Recoil = 0;
					float a = AttackTicksPassed / 5.0f;
					if(a < 1)
						Recoil = std::sin(a * pi);
					WeaponPosition = Position - Direction * (Recoil * 10.0f - 5.0f);
					if(IsSit)
						WeaponPosition.y += 3.0f;

					Graphics()->QuadsSetRotation(Angle + 2 * pi);
					Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, WeaponPosition.x, WeaponPosition.y);
					RenderHand(&RenderInfo,
						Position + Direction * g_pData->m_Weapons.m_aId[WEAPON_GUN].m_Offsetx - Direction * Recoil * 10.0f + vec2(0.0f, g_pData->m_Weapons.m_aId[WEAPON_GUN].m_Offsety),
						Direction, -3 * pi / 4, vec2(-15, 4), Alpha);
					break;
				}
				break;
				}
			}
			else if(Player.m_Weapon == WEAPON_NINJA)
			{
				WeaponPosition = Position;
				WeaponPosition.y += g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsety;
				if(IsSit)
					WeaponPosition.y += 3.0f;

				if(Direction.x < 0.0f)
				{
					Graphics()->QuadsSetRotation(-pi / 2 - State.GetAttach()->m_Angle * pi * 2.0f);
					WeaponPosition.x -= g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsetx;
					GameClient()->m_Effects.PowerupShine(WeaponPosition + vec2(32.0f, 0.0f), vec2(32.0f, 12.0f), Alpha);
				}
				else
				{
					Graphics()->QuadsSetRotation(-pi / 2 + State.GetAttach()->m_Angle * pi * 2.0f);
					GameClient()->m_Effects.PowerupShine(WeaponPosition - vec2(32.0f, 0.0f), vec2(32.0f, 12.0f), Alpha);
				}
				Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, WeaponPosition.x, WeaponPosition.y);

				// HADOKEN
				if(AttackTime <= 1.0f / 6.0f && g_pData->m_Weapons.m_aId[CurrentWeapon].m_NumSpriteMuzzles)
				{
					int IteX = rand() % g_pData->m_Weapons.m_aId[CurrentWeapon].m_NumSpriteMuzzles;
					static int s_LastIteX = IteX;
					if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
					{
						const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo();
						if(pInfo->m_Paused)
							IteX = s_LastIteX;
						else
							s_LastIteX = IteX;
					}
					else
					{
						if(GameClient()->m_Snap.m_pGameInfoObj && GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED)
							IteX = s_LastIteX;
						else
							s_LastIteX = IteX;
					}
					if(g_pData->m_Weapons.m_aId[CurrentWeapon].m_aSpriteMuzzles[IteX])
					{
						vec2 HadokenDirection;
						if(PredictLocalWeapons || ClientId < 0)
							HadokenDirection = vec2(pPlayerChar->m_X, pPlayerChar->m_Y) - vec2(pPrevChar->m_X, pPrevChar->m_Y);
						else
							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);
						float HadokenAngle = 0.0f;
						if(absolute(HadokenDirection.x) > 0.0001f || absolute(HadokenDirection.y) > 0.0001f)
						{
							HadokenDirection = normalize(HadokenDirection);
							HadokenAngle = angle(HadokenDirection);
						}
						else
						{
							HadokenDirection = vec2(1.0f, 0.0f);
						}
						Graphics()->QuadsSetRotation(HadokenAngle);
						QuadOffset = IteX * 2;
						WeaponPosition = Position;
						float OffsetX = g_pData->m_Weapons.m_aId[CurrentWeapon].m_Muzzleoffsetx;
						WeaponPosition -= HadokenDirection * OffsetX;
						Graphics()->TextureSet(GameClient()->m_GameSkin.m_aaSpriteWeaponsMuzzles[CurrentWeapon][IteX]);
						Graphics()->RenderQuadContainerAsSprite(m_aWeaponSpriteMuzzleQuadContainerIndex[CurrentWeapon], QuadOffset, WeaponPosition.x, WeaponPosition.y);
					}
				}
			}
			else
			{
				// TODO: should be an animation
				Recoil = 0.0f;
				float a = AttackTicksPassed / 5.0f;
				if(a < 1.0f)
					Recoil = std::sin(a * pi);
				WeaponPosition = Position + Direction * g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsetx - Direction * Recoil * 10.0f;
				WeaponPosition.y += g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsety;
				if(IsSit)
					WeaponPosition.y += 3.0f;
				if(Player.m_Weapon == WEAPON_GUN && g_Config.m_ClOldGunPosition)
					WeaponPosition.y -= 8.0f;
				Graphics()->QuadsSetRotation(State.GetAttach()->m_Angle * pi * 2.0f + Angle);
				Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, WeaponPosition.x, WeaponPosition.y);
			}

			if(Player.m_Weapon == WEAPON_GUN || Player.m_Weapon == WEAPON_SHOTGUN)
			{
				// check if we're firing stuff
				if(g_pData->m_Weapons.m_aId[CurrentWeapon].m_NumSpriteMuzzles) // prev.attackticks)
				{
					float AlphaMuzzle = 0.0f;
					if(AttackTicksPassed < g_pData->m_Weapons.m_aId[CurrentWeapon].m_Muzzleduration + 3.0f)
					{
						float t = AttackTicksPassed / g_pData->m_Weapons.m_aId[CurrentWeapon].m_Muzzleduration;
						AlphaMuzzle = mix(2.0f, 0.0f, minimum(1.0f, maximum(0.0f, t)));
					}

					int IteX = rand() % g_pData->m_Weapons.m_aId[CurrentWeapon].m_NumSpriteMuzzles;
					static int s_LastIteX = IteX;
					if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
					{
						const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo();
						if(pInfo->m_Paused)
							IteX = s_LastIteX;
						else
							s_LastIteX = IteX;
					}
					else
					{
						if(GameClient()->m_Snap.m_pGameInfoObj && GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED)
							IteX = s_LastIteX;
						else
							s_LastIteX = IteX;
					}
					if(AlphaMuzzle > 0.0f && g_pData->m_Weapons.m_aId[CurrentWeapon].m_aSpriteMuzzles[IteX])
					{
						float OffsetY = -g_pData->m_Weapons.m_aId[CurrentWeapon].m_Muzzleoffsety;
						QuadOffset = IteX * 2 + (Direction.x < 0.0f ? 1 : 0);
						if(Direction.x < 0.0f)
							OffsetY = -OffsetY;

						vec2 DirectionY(-Direction.y, Direction.x);
						vec2 MuzzlePos = WeaponPosition + Direction * g_pData->m_Weapons.m_aId[CurrentWeapon].m_Muzzleoffsetx + DirectionY * OffsetY;
						Graphics()->TextureSet(GameClient()->m_GameSkin.m_aaSpriteWeaponsMuzzles[CurrentWeapon][IteX]);
						Graphics()->RenderQuadContainerAsSprite(m_aWeaponSpriteMuzzleQuadContainerIndex[CurrentWeapon], QuadOffset, MuzzlePos.x, MuzzlePos.y);
					}
				}
			}
			Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
			Graphics()->QuadsSetRotation(0.0f);

			switch(Player.m_Weapon)
			{
			case WEAPON_GUN: RenderHand(&RenderInfo, WeaponPosition, Direction, -3.0f * pi / 4.0f, vec2(-15.0f, 4.0f), Alpha); break;
			case WEAPON_SHOTGUN: RenderHand(&RenderInfo, WeaponPosition, Direction, -pi / 2.0f, vec2(-5.0f, 4.0f), Alpha); break;
			case WEAPON_GRENADE: RenderHand(&RenderInfo, WeaponPosition, Direction, -pi / 2.0f, vec2(-4.0f, 7.0f), Alpha); break;
			}
		}
	}
}

void CPlayers::RenderPlayer(
	const CNetObj_Character *pPrevChar,
	const CNetObj_Character *pPlayerChar,
	const CTeeRenderInfo *pRenderInfo,
	int ClientId,
	float Intra)
{
	CNetObj_Character Prev;
	CNetObj_Character Player;
	Prev = *pPrevChar;
	Player = *pPlayerChar;

	CTeeRenderInfo RenderInfo = *pRenderInfo;

	bool Local = GameClient()->m_Snap.m_LocalClientId == ClientId;
	bool OtherTeam = GameClient()->IsOtherTeam(ClientId);
	// float Alpha = (OtherTeam || ClientId < 0) ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.0f;
	bool Spec = GameClient()->m_Snap.m_SpecInfo.m_Active;

	RenderTools()->m_LocalTeeRender = Local; // TClient

	float Alpha = 1.0f;
	if(OtherTeam || ClientId < 0)
		Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
	else if(g_Config.m_TcShowOthersGhosts && !Local && !Spec)
		Alpha = g_Config.m_TcPredGhostsAlpha / 100.0f;

	if(!OtherTeam && g_Config.m_TcShowOthersGhosts && !Local && g_Config.m_TcUnpredOthersInFreeze && Client()->m_IsLocalFrozen && !Spec)
		Alpha = 1.0f;

	if(ClientId == -2) // ghost
		Alpha = g_Config.m_ClRaceGhostAlpha / 100.0f;
	// TODO: snd_game_volume_others
	const float Volume = 1.0f;

	// set size
	RenderInfo.m_Size = 64.0f;

	if(ClientId >= 0)
		Intra = GameClient()->m_aClients[ClientId].m_IsPredicted ? Client()->PredIntraGameTick(g_Config.m_ClDummy) : Client()->IntraGameTick(g_Config.m_ClDummy);

	static float s_LastGameTickTime = Client()->GameTickTime(g_Config.m_ClDummy);
	static float s_LastPredIntraTick = Client()->PredIntraGameTick(g_Config.m_ClDummy);
	if(GameClient()->m_Snap.m_pGameInfoObj && !(GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED))
	{
		s_LastGameTickTime = Client()->GameTickTime(g_Config.m_ClDummy);
		s_LastPredIntraTick = Client()->PredIntraGameTick(g_Config.m_ClDummy);
	}

	bool PredictLocalWeapons = false;
	float AttackTime = (Client()->PrevGameTick(g_Config.m_ClDummy) - Player.m_AttackTick) / (float)Client()->GameTickSpeed() + Client()->GameTickTime(g_Config.m_ClDummy);
	float LastAttackTime = (Client()->PrevGameTick(g_Config.m_ClDummy) - Player.m_AttackTick) / (float)Client()->GameTickSpeed() + s_LastGameTickTime;
	if(ClientId >= 0 && GameClient()->m_aClients[ClientId].m_IsPredictedLocal && GameClient()->AntiPingGunfire())
	{
		PredictLocalWeapons = true;
		AttackTime = (Client()->PredIntraGameTick(g_Config.m_ClDummy) + (Client()->PredGameTick(g_Config.m_ClDummy) - 1 - Player.m_AttackTick)) / (float)Client()->GameTickSpeed();
		LastAttackTime = (s_LastPredIntraTick + (Client()->PredGameTick(g_Config.m_ClDummy) - 1 - Player.m_AttackTick)) / (float)Client()->GameTickSpeed();
	}
	float AttackTicksPassed = AttackTime * (float)Client()->GameTickSpeed();

	float Angle = GetPlayerTargetAngle(&Prev, &Player, ClientId, Intra);

	vec2 Direction = direction(Angle);
	vec2 Position;
	if(in_range(ClientId, MAX_CLIENTS - 1))
		Position = GameClient()->m_aClients[ClientId].m_RenderPos;
	else
		Position = mix(vec2(Prev.m_X, Prev.m_Y), vec2(Player.m_X, Player.m_Y), Intra);
	vec2 Vel = mix(vec2(Prev.m_VelX / 256.0f, Prev.m_VelY / 256.0f), vec2(Player.m_VelX / 256.0f, Player.m_VelY / 256.0f), Intra);

	// TClient
	if(g_Config.m_TcSwapGhosts && g_Config.m_TcShowOthersGhosts && !Local && Client()->State() != IClient::STATE_DEMOPLAYBACK && ClientId >= 0)
		Position = mix(
			vec2(GameClient()->m_Snap.m_aCharacters[ClientId].m_Prev.m_X, GameClient()->m_Snap.m_aCharacters[ClientId].m_Prev.m_Y),
			vec2(GameClient()->m_Snap.m_aCharacters[ClientId].m_Cur.m_X, GameClient()->m_Snap.m_aCharacters[ClientId].m_Cur.m_Y),
			Client()->IntraGameTick(g_Config.m_ClDummy));

	GameClient()->m_Flow.Add(Position, Vel * 100.0f, 10.0f);

	// TClient
	if(GameClient()->m_aClients[ClientId].m_DeepFrozen && (g_Config.m_TcVolleyBallBetterBall == 2 || (g_Config.m_TcVolleyBallBetterBall == 1 && str_startswith_nocase(Client()->GetCurrentMap(), "volleyball"))))
	{
		// Update
		const float Delta = Client()->IntraGameTickSincePrev(g_Config.m_ClDummy);
		auto &ClientData = GameClient()->m_aClients[ClientId];
		ClientData.m_VolleyBallAngle += Vel.x * Delta / 48.0f;
		if(ClientData.m_VolleyBallAngle < 0.0f)
			ClientData.m_VolleyBallAngle += 2.0f * pi;
		else if(ClientData.m_VolleyBallAngle > 2.0f * pi)
			ClientData.m_VolleyBallAngle -= 2.0f * pi;
		// Render
		const CSkin *pSkin = GameClient()->m_Skins.Find(g_Config.m_TcVolleyBallBetterBallSkin);
		if(!pSkin)
			pSkin = GameClient()->m_Skins.Find("x_ninja");
		if(!pSkin)
			pSkin = GameClient()->m_Skins.Find("default");
		if(!pSkin)
			return;
		const float Size = pRenderInfo->m_Size * 1.2f;
		Graphics()->TextureSet(pSkin->m_OriginalSkin.m_BodyOutline);
		Graphics()->QuadsBegin();
		Graphics()->SetColor(ColorRGBA(1.0f, 1.0f, 1.0f, Alpha));
		IEngineGraphics::CQuadItem QuadOutline{Position.x, Position.y, Size, Size};
		Graphics()->QuadsSetRotation(ClientData.m_VolleyBallAngle);
		Graphics()->QuadsDraw(&QuadOutline, 1);
		Graphics()->QuadsEnd();
		Graphics()->TextureSet(pSkin->m_OriginalSkin.m_Body);
		Graphics()->QuadsBegin();
		Graphics()->SetColor(ColorRGBA(1.0f, 1.0f, 1.0f, Alpha));
		Graphics()->QuadsSetRotation(ClientData.m_VolleyBallAngle);
		IEngineGraphics::CQuadItem Quad{Position.x, Position.y, Size, Size};
		Graphics()->QuadsDraw(&Quad, 1);
		Graphics()->QuadsEnd();
		Graphics()->SetColor(ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
		return;
	}

	RenderInfo.m_GotAirJump = Player.m_Jumped & 2 ? false : true;

	RenderInfo.m_FeetFlipped = false;

	bool Stationary = Player.m_VelX <= 1 && Player.m_VelX >= -1;
	bool InAir = !Collision()->CheckPoint(Player.m_X, Player.m_Y + 16);
	if(g_Config.m_TcAntiPingImproved && !Local)
		InAir = !Collision()->CheckPoint(Position.x, Position.y + 16);
	bool Running = Player.m_VelX >= 5000 || Player.m_VelX <= -5000;
	bool WantOtherDir = (Player.m_Direction == -1 && Vel.x > 0) || (Player.m_Direction == 1 && Vel.x < 0);
	bool Inactive = ClientId >= 0 && (GameClient()->m_aClients[ClientId].m_Afk || GameClient()->m_aClients[ClientId].m_Paused);

	// evaluate animation
	float WalkTime = std::fmod(Position.x, 100.0f) / 100.0f;
	float RunTime = std::fmod(Position.x, 200.0f) / 200.0f;

	// Don't do a moon walk outside the left border
	if(WalkTime < 0)
		WalkTime += 1;
	if(RunTime < 0)
		RunTime += 1;

	CAnimState State;
	State.Set(&g_pData->m_aAnimations[ANIM_BASE], 0);

	if(InAir)
		State.Add(&g_pData->m_aAnimations[ANIM_INAIR], 0, 1.0f); // TODO: some sort of time here
	else if(Stationary)
	{
		if(Inactive)
		{
			State.Add(Direction.x < 0 ? &g_pData->m_aAnimations[ANIM_SIT_LEFT] : &g_pData->m_aAnimations[ANIM_SIT_RIGHT], 0, 1.0f); // TODO: some sort of time here
			RenderInfo.m_FeetFlipped = true;
		}
		else
			State.Add(&g_pData->m_aAnimations[ANIM_IDLE], 0, 1.0f); // TODO: some sort of time here
	}
	else if(!WantOtherDir)
	{
		if(Running)
			State.Add(Player.m_VelX < 0 ? &g_pData->m_aAnimations[ANIM_RUN_LEFT] : &g_pData->m_aAnimations[ANIM_RUN_RIGHT], RunTime, 1.0f);
		else
			State.Add(&g_pData->m_aAnimations[ANIM_WALK], WalkTime, 1.0f);
	}

	if(Player.m_Weapon == WEAPON_HAMMER)
		State.Add(&g_pData->m_aAnimations[ANIM_HAMMER_SWING], std::clamp(LastAttackTime * 5.0f, 0.0f, 1.0f), 1.0f);
	if(Player.m_Weapon == WEAPON_NINJA)
		State.Add(&g_pData->m_aAnimations[ANIM_NINJA_SWING], std::clamp(LastAttackTime * 2.0f, 0.0f, 1.0f), 1.0f);

	// do skidding
	if(!InAir && WantOtherDir && length(Vel * 50) > 500.0f)
		GameClient()->m_Effects.SkidTrail(Position, Vel, Player.m_Direction, Alpha, Volume);

	// render the "shadow" tee
	if(Local && ((g_Config.m_Debug && g_Config.m_ClUnpredictedShadow >= 0) || g_Config.m_ClUnpredictedShadow == 1))
	{
		vec2 ShadowPosition = Position;
		if(ClientId >= 0)
			ShadowPosition = mix(
				vec2(GameClient()->m_Snap.m_aCharacters[ClientId].m_Prev.m_X, GameClient()->m_Snap.m_aCharacters[ClientId].m_Prev.m_Y),
				vec2(GameClient()->m_Snap.m_aCharacters[ClientId].m_Cur.m_X, GameClient()->m_Snap.m_aCharacters[ClientId].m_Cur.m_Y),
				Client()->IntraGameTick(g_Config.m_ClDummy));

		RenderTools()->RenderTee(&State, &RenderInfo, Player.m_Emote, Direction, ShadowPosition, 0.5f); // render ghost
	}

	RenderTools()->RenderTee(&State, &RenderInfo, Player.m_Emote, Direction, Position, Alpha);

	float TeeAnimScale, TeeBaseSize;
	CRenderTools::GetRenderTeeAnimScaleAndBaseSize(&RenderInfo, TeeAnimScale, TeeBaseSize);
	vec2 BodyPos = Position + vec2(State.GetBody()->m_X, State.GetBody()->m_Y) * TeeAnimScale;
	if(RenderInfo.m_TeeRenderFlags & (TEE_EFFECT_FROZEN | TEE_EFFECT_FROZEN_LIVE))
	{
		GameClient()->m_Effects.FreezingFlakes(BodyPos, vec2(32, 32), Alpha);
	}
	if(RenderInfo.m_TeeRenderFlags & TEE_EFFECT_SPARKLE)
	{
		GameClient()->m_Effects.SparkleTrail(BodyPos, Alpha);
	}

	if(ClientId < 0)
		return;

	int QuadOffsetToEmoticon = NUM_WEAPONS * 2 + 2 + 2;
	if((Player.m_PlayerFlags & PLAYERFLAG_CHATTING) && !GameClient()->m_aClients[ClientId].m_Afk)
	{
		int CurEmoticon = (SPRITE_DOTDOT - SPRITE_OOP);
		Graphics()->TextureSet(GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[CurEmoticon]);
		int QuadOffset = QuadOffsetToEmoticon + CurEmoticon;
		Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha);
		Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, Position.x + 24.f, Position.y - 40.f);

		Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
		Graphics()->QuadsSetRotation(0);
	}

	if(g_Config.m_ClAfkEmote && GameClient()->m_aClients[ClientId].m_Afk && ClientId != GameClient()->m_aLocalIds[!g_Config.m_ClDummy])
	{
		int CurEmoticon = (SPRITE_ZZZ - SPRITE_OOP);
		Graphics()->TextureSet(GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[CurEmoticon]);
		int QuadOffset = QuadOffsetToEmoticon + CurEmoticon;
		Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha);
		Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, Position.x + 24.f, Position.y - 40.f);

		Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
		Graphics()->QuadsSetRotation(0);
	}

	if(g_Config.m_ClShowEmotes && !GameClient()->m_aClients[ClientId].m_EmoticonIgnore && GameClient()->m_aClients[ClientId].m_EmoticonStartTick != -1)
	{
		float SinceStart = (Client()->GameTick(g_Config.m_ClDummy) - GameClient()->m_aClients[ClientId].m_EmoticonStartTick) + (Client()->IntraGameTickSincePrev(g_Config.m_ClDummy) - GameClient()->m_aClients[ClientId].m_EmoticonStartFraction);
		float FromEnd = (2 * Client()->GameTickSpeed()) - SinceStart;

		if(0 <= SinceStart && FromEnd > 0)
		{
			float a = 1;

			if(FromEnd < Client()->GameTickSpeed() / 5)
				a = FromEnd / (Client()->GameTickSpeed() / 5.0f);

			float h = 1;
			if(SinceStart < Client()->GameTickSpeed() / 10)
				h = SinceStart / (Client()->GameTickSpeed() / 10.0f);

			float Wiggle = 0;
			if(SinceStart < Client()->GameTickSpeed() / 5)
				Wiggle = SinceStart / (Client()->GameTickSpeed() / 5.0f);

			float WiggleAngle = std::sin(5 * Wiggle);

			Graphics()->QuadsSetRotation(pi / 6 * WiggleAngle);

			Graphics()->SetColor(1.0f, 1.0f, 1.0f, a * Alpha);
			// client_datas::emoticon is an offset from the first emoticon
			int QuadOffset = QuadOffsetToEmoticon + GameClient()->m_aClients[ClientId].m_Emoticon;
			Graphics()->TextureSet(GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[GameClient()->m_aClients[ClientId].m_Emoticon]);
			Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, Position.x, Position.y - 23.f - 32.f * h, 1.f, (64.f * h) / 64.f);

			Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
			Graphics()->QuadsSetRotation(0);
		}
	}
}

// TClient: TODO remove this
void CPlayers::RenderPlayerGhost(
	const CNetObj_Character *pPrevChar,
	const CNetObj_Character *pPlayerChar,
	const CTeeRenderInfo *pRenderInfo,
	int ClientId,
	float Intra)
{
	CNetObj_Character Prev;
	CNetObj_Character Player;
	Prev = *pPrevChar;
	Player = *pPlayerChar;

	CTeeRenderInfo RenderInfo = *pRenderInfo;

	bool Local = GameClient()->m_Snap.m_LocalClientId == ClientId;
	bool OtherTeam = GameClient()->IsOtherTeam(ClientId);
	float Alpha = 1.0f;

	RenderTools()->m_LocalTeeRender = Local; // TClient

	bool FrozenSwappingHide = (GameClient()->m_aClients[ClientId].m_FreezeEnd > 0) && g_Config.m_TcHideFrozenGhosts && g_Config.m_TcSwapGhosts;

	if(OtherTeam || ClientId < 0)
		Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
	else
		Alpha = g_Config.m_TcUnpredGhostsAlpha / 100.0f;

	if(!OtherTeam && FrozenSwappingHide)
		Alpha = 1.0f;

	// set size
	RenderInfo.m_Size = 64.0f;

	float IntraTick = Intra;
	if(ClientId >= 0)
		IntraTick = GameClient()->m_aClients[ClientId].m_IsPredicted ? Client()->PredIntraGameTick(g_Config.m_ClDummy) : Client()->IntraGameTick(g_Config.m_ClDummy);

	static float s_LastGameTickTime = Client()->GameTickTime(g_Config.m_ClDummy);
	static float s_LastPredIntraTick = Client()->PredIntraGameTick(g_Config.m_ClDummy);
	if(GameClient()->m_Snap.m_pGameInfoObj && !(GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED))
	{
		s_LastGameTickTime = Client()->GameTickTime(g_Config.m_ClDummy);
		s_LastPredIntraTick = Client()->PredIntraGameTick(g_Config.m_ClDummy);
	}

	bool PredictLocalWeapons = false;
	float AttackTime = (Client()->PrevGameTick(g_Config.m_ClDummy) - Player.m_AttackTick) / (float)SERVER_TICK_SPEED + Client()->GameTickTime(g_Config.m_ClDummy);
	float LastAttackTime = (Client()->PrevGameTick(g_Config.m_ClDummy) - Player.m_AttackTick) / (float)SERVER_TICK_SPEED + s_LastGameTickTime;
	if(ClientId >= 0 && GameClient()->m_aClients[ClientId].m_IsPredictedLocal && GameClient()->AntiPingGunfire())
	{
		PredictLocalWeapons = true;
		AttackTime = (Client()->PredIntraGameTick(g_Config.m_ClDummy) + (Client()->PredGameTick(g_Config.m_ClDummy) - 1 - Player.m_AttackTick)) / (float)SERVER_TICK_SPEED;
		LastAttackTime = (s_LastPredIntraTick + (Client()->PredGameTick(g_Config.m_ClDummy) - 1 - Player.m_AttackTick)) / (float)SERVER_TICK_SPEED;
	}
	float AttackTicksPassed = AttackTime * (float)SERVER_TICK_SPEED;

	float Angle;
	if(Local && Client()->State() != IClient::STATE_DEMOPLAYBACK)
	{
		// just use the direct input if it's the local player we are rendering
		vec2 Pos = GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy];
		if(g_Config.m_TcScaleMouseDistance)
		{
			const int MaxDistance = g_Config.m_ClDyncam ? g_Config.m_ClDyncamMaxDistance : g_Config.m_ClMouseMaxDistance;
			if(MaxDistance > 5 && MaxDistance < 1000) // Don't scale if angle bind or reduces precision
				Pos *= 1000.0f / (float)MaxDistance;
		}
		Pos.x = (int)Pos.x;
		Pos.y = (int)Pos.y;
		Angle = angle(Pos);
	}
	else
	{
		Angle = GetPlayerTargetAngle(&Prev, &Player, ClientId, IntraTick);
	}

	vec2 Direction = direction(Angle);
	vec2 Position;
	if(in_range(ClientId, MAX_CLIENTS - 1))
		Position = GameClient()->m_aClients[ClientId].m_RenderPos;
	else
		Position = mix(vec2(Prev.m_X, Prev.m_Y), vec2(Player.m_X, Player.m_Y), IntraTick);

	if(g_Config.m_TcSwapGhosts)
	{
		Position = GameClient()->GetSmoothPos(ClientId);
	}
	else
	{
		if(ClientId >= 0)
			Position = mix(
				vec2(GameClient()->m_Snap.m_aCharacters[ClientId].m_Prev.m_X, GameClient()->m_Snap.m_aCharacters[ClientId].m_Prev.m_Y),
				vec2(GameClient()->m_Snap.m_aCharacters[ClientId].m_Cur.m_X, GameClient()->m_Snap.m_aCharacters[ClientId].m_Cur.m_Y),
				Client()->IntraGameTick(g_Config.m_ClDummy));
	}

	if(g_Config.m_TcRenderGhostAsCircle && !FrozenSwappingHide)
	{
		Graphics()->TextureClear();
		Graphics()->QuadsBegin();
		Graphics()->SetColor(RenderInfo.m_ColorBody.r, RenderInfo.m_ColorBody.g, RenderInfo.m_ColorBody.b, Alpha);
		Graphics()->DrawCircle(Position.x, Position.y, 22.0f, 24);
		Graphics()->QuadsEnd();
		return;
	}

	vec2 Vel = mix(vec2(Prev.m_VelX / 256.0f, Prev.m_VelY / 256.0f), vec2(Player.m_VelX / 256.0f, Player.m_VelY / 256.0f), IntraTick);

	GameClient()->m_Flow.Add(Position, Vel * 100.0f, 10.0f);

	RenderInfo.m_GotAirJump = Player.m_Jumped & 2 ? false : true;

	RenderInfo.m_FeetFlipped = false;

	bool Stationary = Player.m_VelX <= 1 && Player.m_VelX >= -1;
	bool InAir = !Collision()->CheckPoint(Player.m_X, Player.m_Y + 16);
	bool Running = Player.m_VelX >= 5000 || Player.m_VelX <= -5000;
	bool WantOtherDir = (Player.m_Direction == -1 && Vel.x > 0) || (Player.m_Direction == 1 && Vel.x < 0);
	bool Inactive = GameClient()->m_aClients[ClientId].m_Afk || GameClient()->m_aClients[ClientId].m_Paused;

	// evaluate animation
	float WalkTime = std::fmod(Position.x, 100.0f) / 100.0f;
	float RunTime = std::fmod(Position.x, 200.0f) / 200.0f;

	// Don't do a moon walk outside the left border
	if(WalkTime < 0)
		WalkTime += 1;
	if(RunTime < 0)
		RunTime += 1;

	CAnimState State;
	State.Set(&g_pData->m_aAnimations[ANIM_BASE], 0);

	if(InAir)
		State.Add(&g_pData->m_aAnimations[ANIM_INAIR], 0, 1.0f); // TODO: some sort of time here
	else if(Stationary)
	{
		if(Inactive)
		{
			State.Add(Direction.x < 0 ? &g_pData->m_aAnimations[ANIM_SIT_LEFT] : &g_pData->m_aAnimations[ANIM_SIT_RIGHT], 0, 1.0f); // TODO: some sort of time here
			RenderInfo.m_FeetFlipped = true;
		}
		else
			State.Add(&g_pData->m_aAnimations[ANIM_IDLE], 0, 1.0f); // TODO: some sort of time here
	}
	else if(!WantOtherDir)
	{
		if(Running)
			State.Add(Player.m_VelX < 0 ? &g_pData->m_aAnimations[ANIM_RUN_LEFT] : &g_pData->m_aAnimations[ANIM_RUN_RIGHT], RunTime, 1.0f);
		else
			State.Add(&g_pData->m_aAnimations[ANIM_WALK], WalkTime, 1.0f);
	}

	if(Player.m_Weapon == WEAPON_HAMMER)
		State.Add(&g_pData->m_aAnimations[ANIM_HAMMER_SWING], std::clamp(LastAttackTime * 5.0f, 0.0f, 1.0f), 1.0f);
	if(Player.m_Weapon == WEAPON_NINJA)
		State.Add(&g_pData->m_aAnimations[ANIM_NINJA_SWING], std::clamp(LastAttackTime * 2.0f, 0.0f, 1.0f), 1.0f);

	// do skidding
	if(!InAir && WantOtherDir && length(Vel * 50) > 500.0f)
		GameClient()->m_Effects.SkidTrail(Position, Vel, Player.m_Direction, Alpha, 1.0f);

	// draw gun
	{
		if(!(RenderInfo.m_TeeRenderFlags & TEE_NO_WEAPON))
		{
			Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
			Graphics()->QuadsSetRotation(State.GetAttach()->m_Angle * pi * 2 + Angle);

			if(ClientId < 0)
				Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.5f);

			// normal weapons
			int CurrentWeapon = std::clamp(Player.m_Weapon, 0, NUM_WEAPONS - 1);
			Graphics()->TextureSet(GameClient()->m_GameSkin.m_aSpriteWeapons[CurrentWeapon]);
			int QuadOffset = CurrentWeapon * 2 + (Direction.x < 0 ? 1 : 0);

			Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha);

			vec2 Dir = Direction;
			float Recoil = 0.0f;
			vec2 WeaponPosition;
			bool IsSit = Inactive && !InAir && Stationary;

			if(Player.m_Weapon == WEAPON_HAMMER)
			{
				// static position for hammer
				WeaponPosition = Position + vec2(State.GetAttach()->m_X, State.GetAttach()->m_Y);
				WeaponPosition.y += g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsety;
				if(Direction.x < 0)
					WeaponPosition.x -= g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsetx;
				if(IsSit)
					WeaponPosition.y += 3.0f;

				// if active and attack is under way, bash stuffs
				if(!Inactive || LastAttackTime < GameClient()->m_aTuning[g_Config.m_ClDummy].GetWeaponFireDelay(Player.m_Weapon))
				{
					if(Direction.x < 0)
						Graphics()->QuadsSetRotation(-pi / 2 - State.GetAttach()->m_Angle * pi * 2);
					else
						Graphics()->QuadsSetRotation(-pi / 2 + State.GetAttach()->m_Angle * pi * 2);
				}
				else
					Graphics()->QuadsSetRotation(Direction.x < 0 ? 100.0f : 500.0f);

				Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, WeaponPosition.x, WeaponPosition.y);
			}
			else if(Player.m_Weapon == WEAPON_NINJA)
			{
				WeaponPosition = Position;
				WeaponPosition.y += g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsety;
				if(IsSit)
					WeaponPosition.y += 3.0f;

				if(Direction.x < 0)
				{
					Graphics()->QuadsSetRotation(-pi / 2 - State.GetAttach()->m_Angle * pi * 2);
					WeaponPosition.x -= g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsetx;
					GameClient()->m_Effects.PowerupShine(WeaponPosition + vec2(32, 0), vec2(32, 12), Alpha);
				}
				else
				{
					Graphics()->QuadsSetRotation(-pi / 2 + State.GetAttach()->m_Angle * pi * 2);
					GameClient()->m_Effects.PowerupShine(WeaponPosition - vec2(32, 0), vec2(32, 12), Alpha);
				}
				Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, WeaponPosition.x, WeaponPosition.y);

				// HADOKEN
				if(AttackTime <= 1 / 6.f && g_pData->m_Weapons.m_aId[CurrentWeapon].m_NumSpriteMuzzles)
				{
					int IteX = rand() % g_pData->m_Weapons.m_aId[CurrentWeapon].m_NumSpriteMuzzles;
					static int s_LastIteX = IteX;
					if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
					{
						const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo();
						if(pInfo->m_Paused)
							IteX = s_LastIteX;
						else
							s_LastIteX = IteX;
					}
					else
					{
						if(GameClient()->m_Snap.m_pGameInfoObj && GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED)
							IteX = s_LastIteX;
						else
							s_LastIteX = IteX;
					}
					if(g_pData->m_Weapons.m_aId[CurrentWeapon].m_aSpriteMuzzles[IteX])
					{
						if(PredictLocalWeapons)
							Dir = vec2(pPlayerChar->m_X, pPlayerChar->m_Y) - vec2(pPrevChar->m_X, pPrevChar->m_Y);
						else
							Dir = 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);
						float HadOkenAngle = 0;
						if(absolute(Dir.x) > 0.0001f || absolute(Dir.y) > 0.0001f)
						{
							Dir = normalize(Dir);
							HadOkenAngle = angle(Dir);
						}
						else
						{
							Dir = vec2(1, 0);
						}
						Graphics()->QuadsSetRotation(HadOkenAngle);
						QuadOffset = IteX * 2;
						vec2 DirY(-Dir.y, Dir.x);
						WeaponPosition = Position;
						float OffsetX = g_pData->m_Weapons.m_aId[CurrentWeapon].m_Muzzleoffsetx;
						WeaponPosition -= Dir * OffsetX;
						Graphics()->TextureSet(GameClient()->m_GameSkin.m_aaSpriteWeaponsMuzzles[CurrentWeapon][IteX]);
						Graphics()->RenderQuadContainerAsSprite(m_aWeaponSpriteMuzzleQuadContainerIndex[CurrentWeapon], QuadOffset, WeaponPosition.x, WeaponPosition.y);
					}
				}
			}
			else
			{
				// TODO: should be an animation
				Recoil = 0;
				float a = AttackTicksPassed / 5.0f;
				if(a < 1)
					Recoil = std::sin(a * pi);
				WeaponPosition = Position + Dir * g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsetx - Dir * Recoil * 10.0f;
				WeaponPosition.y += g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsety;
				if(IsSit)
					WeaponPosition.y += 3.0f;
				if(Player.m_Weapon == WEAPON_GUN && g_Config.m_ClOldGunPosition)
					WeaponPosition.y -= 8;
				Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, WeaponPosition.x, WeaponPosition.y);
			}

			if(Player.m_Weapon == WEAPON_GUN || Player.m_Weapon == WEAPON_SHOTGUN)
			{
				// check if we're firing stuff
				if(g_pData->m_Weapons.m_aId[CurrentWeapon].m_NumSpriteMuzzles) // prev.attackticks)
				{
					float AlphaMuzzle = 0.0f;
					if(AttackTicksPassed < g_pData->m_Weapons.m_aId[CurrentWeapon].m_Muzzleduration + 3)
					{
						float t = AttackTicksPassed / g_pData->m_Weapons.m_aId[CurrentWeapon].m_Muzzleduration;
						AlphaMuzzle = mix(2.0f, 0.0f, minimum(1.0f, maximum(0.0f, t)));
					}

					int IteX = rand() % g_pData->m_Weapons.m_aId[CurrentWeapon].m_NumSpriteMuzzles;
					static int s_LastIteX = IteX;
					if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
					{
						const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo();
						if(pInfo->m_Paused)
							IteX = s_LastIteX;
						else
							s_LastIteX = IteX;
					}
					else
					{
						if(GameClient()->m_Snap.m_pGameInfoObj && GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED)
							IteX = s_LastIteX;
						else
							s_LastIteX = IteX;
					}
					if(AlphaMuzzle > 0.0f && g_pData->m_Weapons.m_aId[CurrentWeapon].m_aSpriteMuzzles[IteX])
					{
						float OffsetY = -g_pData->m_Weapons.m_aId[CurrentWeapon].m_Muzzleoffsety;
						QuadOffset = IteX * 2 + (Direction.x < 0 ? 1 : 0);
						if(Direction.x < 0)
							OffsetY = -OffsetY;

						vec2 DirY(-Dir.y, Dir.x);
						vec2 MuzzlePos = WeaponPosition + Dir * g_pData->m_Weapons.m_aId[CurrentWeapon].m_Muzzleoffsetx + DirY * OffsetY;
						Graphics()->TextureSet(GameClient()->m_GameSkin.m_aaSpriteWeaponsMuzzles[CurrentWeapon][IteX]);
						Graphics()->RenderQuadContainerAsSprite(m_aWeaponSpriteMuzzleQuadContainerIndex[CurrentWeapon], QuadOffset, MuzzlePos.x, MuzzlePos.y);
					}
				}
			}
			Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
			Graphics()->QuadsSetRotation(0);

			switch(Player.m_Weapon)
			{
			case WEAPON_GUN: RenderHand(&RenderInfo, WeaponPosition, Direction, -3 * pi / 4, vec2(-15, 4), Alpha); break;
			case WEAPON_SHOTGUN: RenderHand(&RenderInfo, WeaponPosition, Direction, -pi / 2, vec2(-5, 4), Alpha); break;
			case WEAPON_GRENADE: RenderHand(&RenderInfo, WeaponPosition, Direction, -pi / 2, vec2(-4, 7), Alpha); break;
			}
		}
	}

	RenderTools()->RenderTee(&State, &RenderInfo, Player.m_Emote, Direction, Position, Alpha);

	float TeeAnimScale, TeeBaseSize;
	CRenderTools::GetRenderTeeAnimScaleAndBaseSize(&RenderInfo, TeeAnimScale, TeeBaseSize);
	vec2 BodyPos = Position + vec2(State.GetBody()->m_X, State.GetBody()->m_Y) * TeeAnimScale;
	if(RenderInfo.m_TeeRenderFlags & (TEE_EFFECT_FROZEN | TEE_EFFECT_FROZEN_LIVE))
	{
		GameClient()->m_Effects.FreezingFlakes(BodyPos, vec2(32, 32), Alpha);
	}

	int QuadOffsetToEmoticon = NUM_WEAPONS * 2 + 2 + 2;
	if((Player.m_PlayerFlags & PLAYERFLAG_CHATTING) && !GameClient()->m_aClients[ClientId].m_Afk)
	{
		int CurEmoticon = (SPRITE_DOTDOT - SPRITE_OOP);
		Graphics()->TextureSet(GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[CurEmoticon]);
		int QuadOffset = QuadOffsetToEmoticon + CurEmoticon;
		Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha);
		Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, Position.x + 24.f, Position.y - 40.f);

		Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
		Graphics()->QuadsSetRotation(0);
	}

	if(ClientId < 0)
		return;

	if(g_Config.m_ClAfkEmote && GameClient()->m_aClients[ClientId].m_Afk && !(Client()->DummyConnected() && ClientId == GameClient()->m_aLocalIds[!g_Config.m_ClDummy]))
	{
		int CurEmoticon = (SPRITE_ZZZ - SPRITE_OOP);
		Graphics()->TextureSet(GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[CurEmoticon]);
		int QuadOffset = QuadOffsetToEmoticon + CurEmoticon;
		Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha);
		Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, Position.x + 24.f, Position.y - 40.f);

		Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
		Graphics()->QuadsSetRotation(0);
	}

	if(g_Config.m_ClShowEmotes && !GameClient()->m_aClients[ClientId].m_EmoticonIgnore && GameClient()->m_aClients[ClientId].m_EmoticonStartTick != -1)
	{
		float SinceStart = (Client()->GameTick(g_Config.m_ClDummy) - GameClient()->m_aClients[ClientId].m_EmoticonStartTick) + (Client()->IntraGameTickSincePrev(g_Config.m_ClDummy) - GameClient()->m_aClients[ClientId].m_EmoticonStartFraction);
		float FromEnd = (2 * Client()->GameTickSpeed()) - SinceStart;

		if(0 <= SinceStart && FromEnd > 0)
		{
			float a = 1;

			if(FromEnd < Client()->GameTickSpeed() / 5)
				a = FromEnd / (Client()->GameTickSpeed() / 5.0f);

			float h = 1;
			if(SinceStart < Client()->GameTickSpeed() / 10)
				h = SinceStart / (Client()->GameTickSpeed() / 10.0f);

			float Wiggle = 0;
			if(SinceStart < Client()->GameTickSpeed() / 5)
				Wiggle = SinceStart / (Client()->GameTickSpeed() / 5.0f);

			float WiggleAngle = std::sin(5 * Wiggle);

			Graphics()->QuadsSetRotation(pi / 6 * WiggleAngle);

			Graphics()->SetColor(1.0f, 1.0f, 1.0f, a * Alpha);
			// client_datas::emoticon is an offset from the first emoticon
			int QuadOffset = QuadOffsetToEmoticon + GameClient()->m_aClients[ClientId].m_Emoticon;
			Graphics()->TextureSet(GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[GameClient()->m_aClients[ClientId].m_Emoticon]);
			Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, Position.x, Position.y - 23.f - 32.f * h, 1.f, (64.f * h) / 64.f);

			Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
			Graphics()->QuadsSetRotation(0);
		}
	}
}
inline bool CPlayers::IsPlayerInfoAvailable(int ClientId) const
{
	return GameClient()->m_Snap.m_aCharacters[ClientId].m_Active &&
	       GameClient()->m_Snap.m_apPrevPlayerInfos[ClientId] != nullptr &&
	       GameClient()->m_Snap.m_apPlayerInfos[ClientId] != nullptr;
}

void CPlayers::OnRender()
{
	if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
		return;

	// update render info for ninja
	CTeeRenderInfo aRenderInfo[MAX_CLIENTS];
	const bool IsTeamPlay = GameClient()->IsTeamPlay();
	for(int i = 0; i < MAX_CLIENTS; ++i)
	{
		aRenderInfo[i] = GameClient()->m_aClients[i].m_RenderInfo;
		aRenderInfo[i].m_TeeRenderFlags = 0;

		// predict freeze skin only for local players
		bool Frozen = false;
		if(i == GameClient()->m_aLocalIds[0] || i == GameClient()->m_aLocalIds[1])
		{
			if(GameClient()->m_aClients[i].m_Predicted.m_FreezeEnd != 0)
				aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_FROZEN | TEE_NO_WEAPON;
			if(GameClient()->m_aClients[i].m_Predicted.m_DeepFrozen)
				aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_FROZEN_DEEP;
			if(GameClient()->m_aClients[i].m_Predicted.m_LiveFrozen)
				aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_FROZEN_LIVE;
			if(GameClient()->m_aClients[i].m_Predicted.m_Invincible)
				aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_SPARKLE;

			Frozen = GameClient()->m_aClients[i].m_Predicted.m_FreezeEnd != 0;
		}
		else
		{
			if(GameClient()->m_aClients[i].m_FreezeEnd != 0)
				aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_FROZEN | TEE_NO_WEAPON;
			if(GameClient()->m_aClients[i].m_DeepFrozen)
				aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_FROZEN_DEEP;
			if(GameClient()->m_aClients[i].m_LiveFrozen)
				aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_FROZEN_LIVE;
			if(GameClient()->m_aClients[i].m_Invincible)
				aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_SPARKLE;

			Frozen = GameClient()->m_Snap.m_aCharacters[i].m_HasExtendedData && GameClient()->m_Snap.m_aCharacters[i].m_ExtendedData.m_FreezeEnd != 0;
		}
		bool FakeNinja = false;
		// ddnet-insta sets GAMEINFOFLAG_ENTITIES_FNG but not GAMEINFOFLAG_PREDICT_FNG
		// just assume anything with GAMEINFOFLAG_ENTITIES_FNG won't have ninja...
		if(GameClient()->m_GameInfo.m_EntitiesFNG)
		{
			if(g_Config.m_ClFNGDeepFrozenToFrozen && (aRenderInfo[i].m_TeeRenderFlags & TEE_EFFECT_FROZEN_DEEP))
				aRenderInfo[i].m_TeeRenderFlags = (aRenderInfo[i].m_TeeRenderFlags & ~TEE_EFFECT_FROZEN_DEEP) | TEE_EFFECT_FROZEN, Frozen = true;
			if(g_Config.m_ClFNGNinjaToFrozen && GameClient()->m_aClients[i].m_RenderCur.m_Weapon == WEAPON_NINJA)
				aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_FROZEN | TEE_NO_WEAPON, Frozen = true, FakeNinja = true;
		}

		// TClient
		if(g_Config.m_TcFreezeKatana > 0 && GameClient()->m_aClients[i].m_Predicted.m_FreezeEnd != 0)
		{
			GameClient()->m_aClients[i].m_RenderCur.m_Weapon = WEAPON_NINJA;
			aRenderInfo[i].m_TeeRenderFlags &= ~TEE_NO_WEAPON;
		}

		std::shared_ptr<CManagedTeeRenderInfo> SpecialSkin = nullptr;
		if(GameClient()->m_aClients[i].m_LiveFrozen && !GameClient()->m_GameInfo.m_NoSkinChangeForFrozen && g_Config.m_ClLiveFreezeSkin[0] != '\0')
			SpecialSkin = LiveFrozenTeeRenderInfo() ? LiveFrozenTeeRenderInfo() : nullptr;
		if(!FakeNinja && GameClient()->m_aClients[i].m_RenderCur.m_Weapon == WEAPON_NINJA && g_Config.m_ClShowNinja && g_Config.m_ClNinjaSkin[0] != '\0')
			SpecialSkin = NinjaTeeRenderInfo();
		if(Frozen && !GameClient()->m_GameInfo.m_NoSkinChangeForFrozen && g_Config.m_ClFreezeSkin[0] != '\0')
			SpecialSkin = FrozenTeeRenderInfo() ? FrozenTeeRenderInfo() : NinjaTeeRenderInfo();
		if(GameClient()->m_aClients[i].m_DeepFrozen && !GameClient()->m_GameInfo.m_NoSkinChangeForFrozen && g_Config.m_ClDeepFreezeSkin[0] != '\0')
			SpecialSkin = DeepFrozenTeeRenderInfo() ? DeepFrozenTeeRenderInfo() : NinjaTeeRenderInfo();
		if(SpecialSkin)
		{
			// change the skin for the player to the ninja
			aRenderInfo[i].m_aSixup[g_Config.m_ClDummy].Reset();
			aRenderInfo[i].ApplySkin(SpecialSkin->TeeRenderInfo());
			aRenderInfo[i].m_CustomColoredSkin = IsTeamPlay;
			if(!IsTeamPlay)
			{
				aRenderInfo[i].m_ColorBody = ColorRGBA(1, 1, 1);
				aRenderInfo[i].m_ColorFeet = ColorRGBA(1, 1, 1);

				if(g_Config.m_TcColorFreeze && Frozen)
				{
					aRenderInfo[i].m_CustomColoredSkin = GameClient()->m_aClients[i].m_RenderInfo.m_CustomColoredSkin;
					aRenderInfo[i].m_ColorFeet = g_Config.m_TcColorFreezeFeet ? GameClient()->m_aClients[i].m_RenderInfo.m_ColorFeet : ColorRGBA(1, 1, 1);
					float Darken = (g_Config.m_TcColorFreezeDarken / 100.0f) * 0.5f + 0.5f;
					aRenderInfo[i].m_ColorBody = GameClient()->m_aClients[i].m_RenderInfo.m_ColorBody;
					aRenderInfo[i].m_ColorBody = ColorRGBA(aRenderInfo[i].m_ColorBody.r * Darken, aRenderInfo[i].m_ColorBody.g * Darken, aRenderInfo[i].m_ColorBody.b * Darken, 1.0);
				}
			}
		}
	}

	// get screen edges to avoid rendering offscreen
	float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
	Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1);
	// expand the edges to prevent popping in/out onscreen
	//
	// it is assumed that the tee, all its weapons, and emotes fit into a 200x200 box centered on the tee
	// this may need to be changed or calculated differently in the future
	float BorderBuffer = 100;
	ScreenX0 -= BorderBuffer;
	ScreenX1 += BorderBuffer;
	ScreenY0 -= BorderBuffer;
	ScreenY1 += BorderBuffer;

	// render everyone else's hook, then our own
	const int LocalClientId = GameClient()->m_Snap.m_LocalClientId;
	for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++)
	{
		if(ClientId == LocalClientId || !IsPlayerInfoAvailable(ClientId))
		{
			continue;
		}
		RenderHook(&GameClient()->m_aClients[ClientId].m_RenderPrev, &GameClient()->m_aClients[ClientId].m_RenderCur, &aRenderInfo[ClientId], ClientId);
	}
	if(LocalClientId != -1 && IsPlayerInfoAvailable(LocalClientId))
	{
		const CGameClient::CClientData *pLocalClientData = &GameClient()->m_aClients[LocalClientId];
		RenderHook(&pLocalClientData->m_RenderPrev, &pLocalClientData->m_RenderCur, &aRenderInfo[LocalClientId], LocalClientId);
	}
	// render everyone else's weapon, then our own
	for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++)
	{
		if(ClientId == LocalClientId || !GameClient()->m_Snap.m_aCharacters[ClientId].m_Active || !IsPlayerInfoAvailable(ClientId))
		{
			continue;
		}
		RenderHookCollLine(&GameClient()->m_aClients[ClientId].m_RenderPrev, &GameClient()->m_aClients[ClientId].m_RenderCur, ClientId);
		if(in_range(GameClient()->m_aClients[ClientId].m_RenderPos.x, ScreenX0, ScreenX1) && in_range(GameClient()->m_aClients[ClientId].m_RenderPos.y, ScreenY0, ScreenY1))
			RenderWeapon(&GameClient()->m_aClients[ClientId].m_RenderPrev, &GameClient()->m_aClients[ClientId].m_RenderCur, &aRenderInfo[ClientId], ClientId);
	}
	if(LocalClientId != -1 && GameClient()->m_Snap.m_aCharacters[LocalClientId].m_Active && IsPlayerInfoAvailable(LocalClientId))
	{
		const CGameClient::CClientData *pLocalClientData = &GameClient()->m_aClients[LocalClientId];
		RenderHookCollLine(&pLocalClientData->m_RenderPrev, &pLocalClientData->m_RenderCur, LocalClientId);
		RenderWeapon(&pLocalClientData->m_RenderPrev, &pLocalClientData->m_RenderCur, &aRenderInfo[LocalClientId], LocalClientId);
	}

	// render spectating players
	for(const auto &Client : GameClient()->m_aClients)
	{
		if(!Client.m_SpecCharPresent)
		{
			continue;
		}

		const int ClientId = Client.ClientId();
		float Alpha = (GameClient()->IsOtherTeam(ClientId) || ClientId < 0) ? g_Config.m_ClShowOthersAlpha / 100.f : 1.f;
		if(ClientId == -2) // ghost
		{
			Alpha = g_Config.m_ClRaceGhostAlpha / 100.f;
		}
		RenderTools()->RenderTee(CAnimState::GetIdle(), &SpectatorTeeRenderInfo()->TeeRenderInfo(), EMOTE_BLINK, vec2(1, 0), Client.m_SpecChar, Alpha);
	}

	// render everyone else's tee, then either our own or the tee we are spectating.
	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;

	for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++)
	{
		if(ClientId == RenderLastId || !IsPlayerInfoAvailable(ClientId))
		{
			continue;
		}

		if(!in_range(GameClient()->m_aClients[ClientId].m_RenderPos.x, ScreenX0, ScreenX1) || !in_range(GameClient()->m_aClients[ClientId].m_RenderPos.y, ScreenY0, ScreenY1))
		{
			if(!(g_Config.m_TcShowOthersGhosts && g_Config.m_TcSwapGhosts))
				continue;
		}

		bool Frozen = (GameClient()->m_aClients[ClientId].m_FreezeEnd > 0) && g_Config.m_TcHideFrozenGhosts;
		bool RenderGhost = true;
		if(g_Config.m_TcHideFrozenGhosts && Frozen && g_Config.m_TcShowOthersGhosts)
		{
			if(!g_Config.m_TcSwapGhosts)
				RenderGhost = false;
		}
		if(g_Config.m_TcUnpredOthersInFreeze && Client()->m_IsLocalFrozen && g_Config.m_TcShowOthersGhosts)
		{
			RenderGhost = false;
		}

		bool Spec = GameClient()->m_Snap.m_SpecInfo.m_Active;

		// If we are frozen and hiding frozen ghosts and not swapping render only the regular player
		if(RenderGhost && g_Config.m_TcShowOthersGhosts && !Spec && Client()->State() != IClient::STATE_DEMOPLAYBACK)
			RenderPlayerGhost(&GameClient()->m_aClients[ClientId].m_RenderPrev, &GameClient()->m_aClients[ClientId].m_RenderCur, &aRenderInfo[ClientId], ClientId);

		RenderPlayer(&GameClient()->m_aClients[ClientId].m_RenderPrev, &GameClient()->m_aClients[ClientId].m_RenderCur, &aRenderInfo[ClientId], ClientId);
	}
	if(RenderLastId != -1 && IsPlayerInfoAvailable(RenderLastId))
	{
		const CGameClient::CClientData *pClientData = &GameClient()->m_aClients[RenderLastId];
		RenderPlayer(&pClientData->m_RenderPrev, &pClientData->m_RenderCur, &aRenderInfo[RenderLastId], RenderLastId);
	}
}

void CPlayers::CreateNinjaTeeRenderInfo()
{
	CTeeRenderInfo NinjaTeeRenderInfo;
	NinjaTeeRenderInfo.m_Size = 64.0f;
	CSkinDescriptor NinjaSkinDescriptor;
	NinjaSkinDescriptor.m_Flags |= CSkinDescriptor::FLAG_SIX;
	str_copy(NinjaSkinDescriptor.m_aSkinName, g_Config.m_ClNinjaSkin[0] != '\0' ? g_Config.m_ClNinjaSkin : "x_ninja");
	m_pNinjaTeeRenderInfo = GameClient()->CreateManagedTeeRenderInfo(NinjaTeeRenderInfo, NinjaSkinDescriptor);
}
void CPlayers::CreateFrozenTeeRenderInfos()
{
	CTeeRenderInfo FrozenTeeRenderInfo;
	FrozenTeeRenderInfo.m_Size = 64.0f;
	if(g_Config.m_ClFreezeSkin[0] != '\0')
	{
		CSkinDescriptor FrozenSkinDescriptor;
		FrozenSkinDescriptor.m_Flags |= CSkinDescriptor::FLAG_SIX;
		str_copy(FrozenSkinDescriptor.m_aSkinName, g_Config.m_ClFreezeSkin);
		m_pFrozenTeeRenderInfo = GameClient()->CreateManagedTeeRenderInfo(FrozenTeeRenderInfo, FrozenSkinDescriptor);
	}
	else
		m_pFrozenTeeRenderInfo = nullptr;
	if(g_Config.m_ClDeepFreezeSkin[0] != '\0')
	{
		CSkinDescriptor FrozenSkinDescriptor;
		FrozenSkinDescriptor.m_Flags |= CSkinDescriptor::FLAG_SIX;
		str_copy(FrozenSkinDescriptor.m_aSkinName, g_Config.m_ClDeepFreezeSkin);
		m_pDeepFrozenTeeRenderInfo = GameClient()->CreateManagedTeeRenderInfo(FrozenTeeRenderInfo, FrozenSkinDescriptor);
	}
	else
		m_pDeepFrozenTeeRenderInfo = nullptr;
	if(g_Config.m_ClLiveFreezeSkin[0] != '\0')
	{
		CSkinDescriptor FrozenSkinDescriptor;
		FrozenSkinDescriptor.m_Flags |= CSkinDescriptor::FLAG_SIX;
		str_copy(FrozenSkinDescriptor.m_aSkinName, g_Config.m_ClLiveFreezeSkin);
		m_pLiveFrozenTeeRenderInfo = GameClient()->CreateManagedTeeRenderInfo(FrozenTeeRenderInfo, FrozenSkinDescriptor);
	}
	else
		m_pLiveFrozenTeeRenderInfo = nullptr;
}

void CPlayers::CreateSpectatorTeeRenderInfo()
{
	CTeeRenderInfo SpectatorTeeRenderInfo;
	SpectatorTeeRenderInfo.m_Size = 64.0f;
	CSkinDescriptor SpectatorSkinDescriptor;
	SpectatorSkinDescriptor.m_Flags |= CSkinDescriptor::FLAG_SIX;
	str_copy(SpectatorSkinDescriptor.m_aSkinName, g_Config.m_ClSpecSkin[0] != '\0' ? g_Config.m_ClSpecSkin : "x_spec");
	m_pSpectatorTeeRenderInfo = GameClient()->CreateManagedTeeRenderInfo(SpectatorTeeRenderInfo, SpectatorSkinDescriptor);
}

void CPlayers::OnInit()
{
	m_WeaponEmoteQuadContainerIndex = Graphics()->CreateQuadContainer(false);

	Graphics()->SetColor(1.f, 1.f, 1.f, 1.f);

	for(int i = 0; i < NUM_WEAPONS; ++i)
	{
		float ScaleX, ScaleY;
		Graphics()->GetSpriteScale(g_pData->m_Weapons.m_aId[i].m_pSpriteBody, ScaleX, ScaleY);
		Graphics()->QuadsSetSubset(0, 0, 1, 1);
		Graphics()->QuadContainerAddSprite(m_WeaponEmoteQuadContainerIndex, g_pData->m_Weapons.m_aId[i].m_VisualSize * ScaleX, g_pData->m_Weapons.m_aId[i].m_VisualSize * ScaleY);
		Graphics()->QuadsSetSubset(0, 1, 1, 0);
		Graphics()->QuadContainerAddSprite(m_WeaponEmoteQuadContainerIndex, g_pData->m_Weapons.m_aId[i].m_VisualSize * ScaleX, g_pData->m_Weapons.m_aId[i].m_VisualSize * ScaleY);
	}
	float ScaleX, ScaleY;

	// at the end the hand
	Graphics()->QuadsSetSubset(0, 0, 1, 1);
	Graphics()->QuadContainerAddSprite(m_WeaponEmoteQuadContainerIndex, 20.f);
	Graphics()->QuadsSetSubset(0, 0, 1, 1);
	Graphics()->QuadContainerAddSprite(m_WeaponEmoteQuadContainerIndex, 20.f);

	Graphics()->QuadsSetSubset(0, 0, 1, 1);
	Graphics()->QuadContainerAddSprite(m_WeaponEmoteQuadContainerIndex, -12.f, -8.f, 24.f, 16.f);
	Graphics()->QuadsSetSubset(0, 0, 1, 1);
	Graphics()->QuadContainerAddSprite(m_WeaponEmoteQuadContainerIndex, -12.f, -8.f, 24.f, 16.f);

	for(int i = 0; i < NUM_EMOTICONS; ++i)
	{
		Graphics()->QuadsSetSubset(0, 0, 1, 1);
		Graphics()->QuadContainerAddSprite(m_WeaponEmoteQuadContainerIndex, 64.f);
	}
	Graphics()->QuadContainerUpload(m_WeaponEmoteQuadContainerIndex);

	for(int i = 0; i < NUM_WEAPONS; ++i)
	{
		m_aWeaponSpriteMuzzleQuadContainerIndex[i] = Graphics()->CreateQuadContainer(false);
		for(int n = 0; n < g_pData->m_Weapons.m_aId[i].m_NumSpriteMuzzles; ++n)
		{
			if(g_pData->m_Weapons.m_aId[i].m_aSpriteMuzzles[n])
			{
				if(i == WEAPON_GUN || i == WEAPON_SHOTGUN)
				{
					// TODO: hardcoded for now to get the same particle size as before
					Graphics()->GetSpriteScaleImpl(96, 64, ScaleX, ScaleY);
				}
				else
					Graphics()->GetSpriteScale(g_pData->m_Weapons.m_aId[i].m_aSpriteMuzzles[n], ScaleX, ScaleY);
			}

			float SWidth = (g_pData->m_Weapons.m_aId[i].m_VisualSize * ScaleX) * (4.0f / 3.0f);
			float SHeight = g_pData->m_Weapons.m_aId[i].m_VisualSize * ScaleY;

			Graphics()->QuadsSetSubset(0, 0, 1, 1);
			if(WEAPON_NINJA == i)
				Graphics()->QuadContainerAddSprite(m_aWeaponSpriteMuzzleQuadContainerIndex[i], 160.f * ScaleX, 160.f * ScaleY);
			else
				Graphics()->QuadContainerAddSprite(m_aWeaponSpriteMuzzleQuadContainerIndex[i], SWidth, SHeight);

			Graphics()->QuadsSetSubset(0, 1, 1, 0);
			if(WEAPON_NINJA == i)
				Graphics()->QuadContainerAddSprite(m_aWeaponSpriteMuzzleQuadContainerIndex[i], 160.f * ScaleX, 160.f * ScaleY);
			else
				Graphics()->QuadContainerAddSprite(m_aWeaponSpriteMuzzleQuadContainerIndex[i], SWidth, SHeight);
		}
		Graphics()->QuadContainerUpload(m_aWeaponSpriteMuzzleQuadContainerIndex[i]);
	}

	Graphics()->QuadsSetSubset(0.f, 0.f, 1.f, 1.f);
	Graphics()->QuadsSetRotation(0.f);

	CreateNinjaTeeRenderInfo();
	CreateFrozenTeeRenderInfos();
	CreateSpectatorTeeRenderInfo();
}
