#include "Space.h"
#include "AnimatedModel.h"
#include "BoneAnimatedModel.h"
#include "planet/PlanetData.h"
#include "utils/Utils.h"
#include "render/OpenGlExtensions.h"
#include <iostream>
#include "render/TextureManager.h"
#include "TextModel.h"
#include <random>
#include <cmath>
#include <algorithm>
#include <functional>
#ifdef __ANDROID__
#include <android/log.h>
#endif

#ifdef NETWORK
#ifdef EMSCRIPTEN
#include "network/WebSocketClientEmscripten.h"
#else
#include "network/WebSocketClient.h"
#endif
#else
#include "network/LocalClient.h"
#endif

#include "GameConstants.h"

namespace ZL
{

	extern const char* CONST_ZIP_FILE;

	extern float x;
	extern float y;
	extern float z;

	bool inverseVertical = true;

	Eigen::Quaternionf generateRandomQuaternion(std::mt19937& gen)
	{

		std::normal_distribution<> distrib(0.0, 1.0);

		Eigen::Quaternionf randomQuat = {
			(float)distrib(gen),
			(float)distrib(gen),
			(float)distrib(gen),
			(float)distrib(gen)
		};

		return randomQuat.normalized();
	}

	std::vector<BoxCoords> generateRandomBoxCoords(int N)
	{
		const float MIN_DISTANCE = 3.0f;
		const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
		const float MIN_COORD = -100.0f;
		const float MAX_COORD = 100.0f;
		const int MAX_ATTEMPTS = 1000;
		std::vector<BoxCoords> boxCoordsArr;

		std::random_device rd;
		std::mt19937 gen(rd());

		std::uniform_real_distribution<> distrib(MIN_COORD, MAX_COORD);

		int generatedCount = 0;

		while (generatedCount < N)
		{
			bool accepted = false;
			int attempts = 0;

			while (!accepted && attempts < MAX_ATTEMPTS)
			{
				Vector3f newPos(
					(float)distrib(gen),
					(float)distrib(gen),
					(float)distrib(gen)
				);

				accepted = true;
				for (const auto& existingBox : boxCoordsArr)
				{

					Vector3f diff = newPos - existingBox.pos;

					float distanceSquared = diff.squaredNorm();

					if (distanceSquared < MIN_DISTANCE_SQUARED)
					{
						accepted = false;
						break;
					}
				}

				if (accepted)
				{
					Eigen::Quaternionf randomQuat = generateRandomQuaternion(gen);

					Matrix3f randomMatrix = randomQuat.toRotationMatrix();

					boxCoordsArr.emplace_back(BoxCoords{ newPos, randomMatrix });
					generatedCount++;
				}
				attempts++;
			}

			if (!accepted) {
				break;
			}
		}

		return boxCoordsArr;
	}

	static Eigen::Matrix4f makeViewMatrix_FromYourCamera()
	{
		Eigen::Matrix4f Tz = Eigen::Matrix4f::Identity();
		Tz(2, 3) = -1.0f * ZL::Environment::zoom;

		Eigen::Matrix4f R = Eigen::Matrix4f::Identity();
		R.block<3, 3>(0, 0) = ZL::Environment::inverseShipMatrix;

		Eigen::Matrix4f Tship = Eigen::Matrix4f::Identity();
		Tship(0, 3) = -ZL::Environment::shipState.position.x();
		Tship(1, 3) = -ZL::Environment::shipState.position.y();
		Tship(2, 3) = -ZL::Environment::shipState.position.z();

		return Tz * R * Tship;
	}

	static Eigen::Matrix4f makePerspective(float fovyRadians, float aspect, float zNear, float zFar)
	{
		// Стандартная перспектива
		float f = 1.0f / std::tan(fovyRadians * 0.5f);

		Eigen::Matrix4f P = Eigen::Matrix4f::Zero();
		P(0, 0) = f / aspect;
		P(1, 1) = f;
		P(2, 2) = (zFar + zNear) / (zNear - zFar);
		P(2, 3) = (2.0f * zFar * zNear) / (zNear - zFar);
		P(3, 2) = -1.0f;
		return P;
	}

	bool worldToScreen(const Vector3f& world, float& outX, float& outY, float& outDepth)
	{
		// Матрицы должны совпасть с drawBoxes/drawShip по смыслу
		float aspect = static_cast<float>(Environment::width) / static_cast<float>(Environment::height);

		Eigen::Matrix4f V = makeViewMatrix_FromYourCamera();
		Eigen::Matrix4f P = makePerspective(1.0f / 1.5f, aspect, Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);

		Eigen::Vector4f w(world.x(), world.y(), world.z(), 1.0f);
		Eigen::Vector4f clip = P * V * w;

		if (clip.w() <= 0.0001f) return false; // позади камеры

		Eigen::Vector3f ndc = clip.head<3>() / clip.w(); // [-1..1]
		outDepth = ndc.z();

		// В пределах экрана?
		// (можно оставить, можно клампить)
		float sx = (ndc.x() * 0.5f + 0.5f) * Environment::projectionWidth;
		float sy = (ndc.y() * 0.5f + 0.5f) * Environment::projectionHeight;

		outX = sx;
		outY = sy;

		// Можно отсеять те, что вне:
		if (sx < -200 || sx > Environment::projectionWidth + 200) return false;
		if (sy < -200 || sy > Environment::projectionHeight + 200) return false;

		return true;
	}

	bool projectToNDC(const Vector3f& world, float& ndcX, float& ndcY, float& ndcZ, float& clipW)
	{
		float aspect = static_cast<float>(Environment::width) / static_cast<float>(Environment::height);
		Eigen::Matrix4f V = makeViewMatrix_FromYourCamera();
		Eigen::Matrix4f P = makePerspective(1.0f / 1.5f, aspect, Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);

		Eigen::Vector4f w(world.x(), world.y(), world.z(), 1.0f);
		Eigen::Vector4f clip = P * V * w;

		clipW = clip.w();
		if (std::abs(clipW) < 1e-6f) return false;

		Eigen::Vector3f ndc = clip.head<3>() / clipW;
		ndcX = ndc.x();
		ndcY = ndc.y();
		ndcZ = ndc.z();
		return true;
	}

	void Space::drawBoxesLabels()
	{
		if (!textRenderer) return;

		// Текст рисуем как 2D поверх всего 3D, но ДО drawUI или после — как хочешь.
		// Чтобы подписи были поверх — делай после drawBoxes и до drawUI (как мы и вставили).

		glDisable(GL_DEPTH_TEST);
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

		for (size_t i = 0; i < boxCoordsArr.size(); ++i)
		{
			if (i >= boxAlive.size() || !boxAlive[i]) continue;
			if (i >= boxLabels.size()) continue;

			// ВАЖНО: твои боксы рисуются с Translate({0,0,45000}) + pos
			Vector3f boxWorld = boxCoordsArr[i].pos + Vector3f{ 0.0f, 0.0f, 45000.0f };

			// Чуть выше бокса по Y (или по Z — как нравится)
			Vector3f labelWorld = boxWorld + Vector3f{ 0.0f, 2.2f, 0.0f };

			float sx, sy, depth;
			if (!worldToScreen(labelWorld, sx, sy, depth)) continue;

			// В твоей UI-системе Y обычно перевёрнут (ты делаешь uiY = height - my).
			// Наш worldToScreen отдаёт Y в системе "низ=0, верх=height" (NDC->screen).
			// Чтобы совпало с твоей UI-логикой, перевернём:
			float uiX = sx;
			float uiY = sy; // если окажется вверх ногами — замени на (Environment::height - sy)

			float dist = (Environment::shipState.position - boxWorld).norm();
			float scaleRaw = 120.0f / (dist + 1.0f);
			float scale = std::round(scaleRaw * 10.0f) / 10.0f; // округление до 0.1
			scale = std::clamp(scale, 0.6f, 1.2f);

			textRenderer->drawText(boxLabels[i], uiX, uiY, scale, /*centered*/true);
		}

		glDisable(GL_BLEND);
		glEnable(GL_DEPTH_TEST);
	}


	Space::Space(Renderer& iRenderer, TaskManager& iTaskManager, MainThreadHandler& iMainThreadHandler, std::unique_ptr<INetworkClient>& iNetworkClient, MenuManager& iMenuManager)
		: renderer(iRenderer),
		taskManager(iTaskManager),
		mainThreadHandler(iMainThreadHandler),
		planetObject(renderer, taskManager, mainThreadHandler),
		networkClient(iNetworkClient),
		menuManager(iMenuManager)
	{
		projectiles.reserve(maxProjectiles);
		for (int i = 0; i < maxProjectiles; ++i) {
			projectiles.emplace_back(std::make_unique<Projectile>());
		}
	}

	Space::~Space() {
	}

	void Space::resetPlayerState()
	{
		shipAlive = true;
		gameOver = false;
		showExplosion = false;
		explosionEmitter.setEmissionPoints(std::vector<Vector3f>());
		Environment::shipState.position = Vector3f{ 0, 0, 45000.f };
		Environment::shipState.velocity = 0.0f;
		Environment::shipState.selectedVelocity = 0;
		newShipVelocity = 0;
		Environment::shipState.rotation = Eigen::Matrix3f::Identity();
		Environment::inverseShipMatrix = Eigen::Matrix3f::Identity();
		Environment::zoom = DEFAULT_ZOOM;
		Environment::tapDownHold = false;
		playerScore = 0;
		
	}

	void Space::updateShowPlayersButtonState()
	{
		bool hasAlivePlayers = false;
		for (auto const& [id, _] : remotePlayerStates) {
			if (!deadRemotePlayers.count(id)) {
				hasAlivePlayers = true;
				break;
			}
		}
		if (hasAlivePlayers == showPlayersButtonEnabled) return;
		showPlayersButtonEnabled = hasAlivePlayers;
		auto btn = menuManager.uiManager.findButton("showPlayersButton");
		if (!btn) return;
		btn->state = hasAlivePlayers ? ButtonState::Normal : ButtonState::Disabled;
	}


	void Space::setup() {

		menuManager.onRestartPressed = [this]() {
			resetPlayerState();

			if (networkClient) {
				try {
					networkClient->Send(std::string("RESPAWN"));
					std::cout << "Client: Sent RESPAWN to server\n";
				}
				catch (...) {
					std::cerr << "Client: Failed to send RESPAWN\n";
				}
			}
			std::cerr << "Game restarted\n";
			};

		menuManager.onVelocityChanged = [this](float newVelocity) {
			newShipVelocity = newVelocity;
			if (Environment::shipState.shipType == 0)
			{
				if (newVelocity > 2)
				{
					this->menuManager.uiManager.findButton("shootButton")->state = ButtonState::Disabled;
					this->menuManager.uiManager.findButton("shootButton2")->state = ButtonState::Disabled;

				}
				else
				{
					this->menuManager.uiManager.findButton("shootButton")->state = ButtonState::Normal;
					this->menuManager.uiManager.findButton("shootButton2")->state = ButtonState::Normal;
				}
			}
			};

		menuManager.onFirePressed = [this]() {
			firePressed = true;
			};

		menuManager.onShowPlayersPressed = [this]() {
			buildAndShowPlayerList();
			};

		menuManager.onTakeButtonPressed = [this]() {
			if (Environment::shipState.shipType != 1) return;
			if (!networkClient) return;

			int bestIdx = -1;
			float bestDistSq = BOX_PICKUP_RADIUS * BOX_PICKUP_RADIUS;
			for (size_t i = 0; i < boxCoordsArr.size(); ++i) {
				if (i >= boxAlive.size() || !boxAlive[i]) continue;
				Vector3f boxWorld = boxCoordsArr[i].pos + Vector3f{ 0.f, 0.f, 45000.f };
				float distSq = (Environment::shipState.position - boxWorld).squaredNorm();
				if (distSq <= bestDistSq) {
					bestDistSq = distSq;
					bestIdx = static_cast<int>(i);
				}
			}
			if (bestIdx >= 0) {
				networkClient->Send("BOX_PICKUP:" + std::to_string(bestIdx));
				this->playerScore += 1;
			}
		};

		menuManager.forceSetupSpaceUICallback = [this]() {
			this->nearPickupBox = false;
			this->showPlayersButtonEnabled = false;

			if (menuManager.uiManager.findButton("minusButton"))
			{
				menuManager.uiManager.findButton("minusButton")->state = ButtonState::Disabled;
			}
			if (menuManager.uiManager.findButton("plusButton"))
			{
				menuManager.uiManager.findButton("plusButton")->state = ButtonState::Normal;
			}
			if (Environment::shipState.shipType == 0)
			{
				if (menuManager.uiManager.findButton("shootButton"))
				{
					menuManager.uiManager.findButton("shootButton")->state = ButtonState::Normal;
				}
				if (menuManager.uiManager.findButton("shootButton2"))
				{
					menuManager.uiManager.findButton("shootButton2")->state = ButtonState::Normal;
				}
			}

		};

		bool cfgLoaded = sparkEmitter.loadFromJsonFile("resources/config/spark_config.json", renderer, CONST_ZIP_FILE);
		bool cfgLoaded2 = sparkEmitterCargo.loadFromJsonFile("resources/config/spark_config_cargo.json", renderer, CONST_ZIP_FILE);
		sparkEmitter.setIsActive(false);
		sparkEmitterCargo.setIsActive(false);
		
		bool projCfgLoaded = projectileEmitter.loadFromJsonFile("resources/config/spark_projectile_config.json", renderer, CONST_ZIP_FILE);
		bool explosionCfgLoaded = explosionEmitter.loadFromJsonFile("resources/config/explosion_config.json", renderer, CONST_ZIP_FILE);
		explosionEmitter.setEmissionPoints(std::vector<Vector3f>());
		//projectileEmitter.setEmissionPoints({ Vector3f{0,0,45000} });
		//projectileEmitter.setUseWorldSpace(true);


		cubemapTexture = std::make_shared<Texture>(
			std::array<TextureDataStruct, 6>{
			CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
				CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
				CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
				CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
				CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
				CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE)
		});


		cubemap.data = ZL::CreateCubemap(500);
		cubemap.RefreshVBO();



		//Load texture
		spaceshipTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/MainCharacter_Base_color_sRGB.png", CONST_ZIP_FILE));
		spaceshipBase = LoadFromTextFile02("resources/spaceshipnew001.txt", CONST_ZIP_FILE);
		spaceshipBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())).toRotationMatrix());// QuatFromRotateAroundY(M_PI / 2.0).toRotationMatrix());


		spaceshipBase.Move(Vector3f{ 1.2, 0, -5 });
		spaceshipBase.Scale(0.4f);

		spaceship.AssignFrom(spaceshipBase);
		spaceship.RefreshVBO();

		// Load cargo
		cargoTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/Cargo_Base_color_sRGB.png", CONST_ZIP_FILE));
		cargoBase = LoadFromTextFile02("resources/cargoship001.txt", CONST_ZIP_FILE);
		auto quat = Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitZ()));
		auto rotMatrix = quat.toRotationMatrix();
		cargoBase.RotateByMatrix(rotMatrix);

		auto quat2 = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI * 0.5, Eigen::Vector3f::UnitY()));
		auto rotMatrix2 = quat2.toRotationMatrix();
		cargoBase.RotateByMatrix(rotMatrix2);
		//cargoBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())).toRotationMatrix());
		cargoBase.Move(Vector3f{ 0, 0, -5 });
		cargo.AssignFrom(cargoBase);
		cargo.RefreshVBO();

		//projectileTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/spark2.png", CONST_ZIP_FILE));

		//Boxes
		boxTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/box/box.png", CONST_ZIP_FILE));
		boxBase = LoadFromTextFile02("resources/box/box.txt", CONST_ZIP_FILE);

		boxCoordsArr = generateRandomBoxCoords(50);
		boxRenderArr.resize(boxCoordsArr.size());
		for (int i = 0; i < boxCoordsArr.size(); i++)
		{
			boxRenderArr[i].AssignFrom(boxBase);
			boxRenderArr[i].RefreshVBO();
		}

		boxAlive.resize(boxCoordsArr.size(), true);
#ifdef NETWORK
		std::fill(boxAlive.begin(), boxAlive.end(), false);
		serverBoxesApplied = false;
#endif
		ZL::CheckGlError();
		boxLabels.clear();
		boxLabels.reserve(boxCoordsArr.size());
		for (size_t i = 0; i < boxCoordsArr.size(); ++i) {
			boxLabels.push_back("Box " + std::to_string(i));
		}

		if (!cfgLoaded)
		{
			throw std::runtime_error("Failed to load spark emitter config file!");
		}

		crosshairCfgLoaded = loadCrosshairConfig("resources/config/crosshair_config.json");
		std::cout << "[Crosshair] loaded=" << crosshairCfgLoaded
			<< " enabled=" << crosshairCfg.enabled
			<< " w=" << Environment::width << " h=" << Environment::height
			<< " alpha=" << crosshairCfg.alpha
			<< " thickness=" << crosshairCfg.thicknessPx
			<< " gap=" << crosshairCfg.gapPx << "\n";
		if (!crosshairCfgLoaded) {
			std::cerr << "Failed to load crosshair_config.json, using defaults\n";
		}



		textRenderer = std::make_unique<ZL::TextRenderer>();
		if (!textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32, CONST_ZIP_FILE)) {
			std::cerr << "Failed to init TextRenderer\n";
		}
		ZL::CheckGlError();

		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

		planetObject.init();
	}


	void Space::drawCubemap(float skyPercent)
	{
		static const std::string envSkyShaderName = "env_sky";
		static const std::string skyPercentUniformName = "skyPercent";

		renderer.shaderManager.PushShader(envSkyShaderName);
		renderer.RenderUniform1i(textureUniformName, 0);
		renderer.RenderUniform1f(skyPercentUniformName, skyPercent);
		
		renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
			static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
			Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
		renderer.PushMatrix();
		renderer.LoadIdentity();
		renderer.RotateMatrix(Environment::inverseShipMatrix);


		Vector3f worldLightDir = Vector3f(1.0f, -1.0f, -1.0f).normalized();
		Matrix3f viewMatrix = Environment::inverseShipMatrix;
		Vector3f viewLightDir = (viewMatrix * worldLightDir).normalized();


		// Передаем вектор НА источник света
		Vector3f lightToSource = -viewLightDir;
		renderer.RenderUniform3fv("uLightDirView", lightToSource.data());

		// 2. Базовый цвет атмосферы (голубой)
		Vector3f skyColor = { 0.0f, 0.5f, 1.0f };
		renderer.RenderUniform3fv("uSkyColor", skyColor.data());

		// 1. Вектор направления от центра планеты к игроку (в мировых координатах)
		// Предполагаем, что планета в (0,0,0). Если нет, то (shipPosition - planetCenter)
		Vector3f playerDirWorld = Environment::shipState.position.normalized();

		// 2. Направление света в мировом пространстве
		//Vector3f worldLightDir = Vector3f(1.0f, -1.0f, -1.0f).normalized();

		// 3. Считаем глобальную освещенность для игрока (насколько он на свету)
		// Это одно число для всего кадра
		float playerLightFactor = playerDirWorld.dot(-worldLightDir);
		// Ограничиваем и делаем переход мягче
		playerLightFactor = std::clamp((playerLightFactor + 0.2f) / 1.2f, 0.0f, 1.0f);

		renderer.RenderUniform1f("uPlayerLightFactor", playerLightFactor);
		renderer.RenderUniform1f("skyPercent", skyPercent);

		CheckGlError();

		glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture->getTexID());
		renderer.DrawVertexRenderStruct(cubemap);

		CheckGlError();


		renderer.PopMatrix();
		renderer.PopProjectionMatrix();
		renderer.shaderManager.PopShader();
		CheckGlError();
	}

	void Space::drawShip()
	{
		renderer.shaderManager.PushShader(defaultShaderName);
		renderer.RenderUniform1i(textureUniformName, 0);

		renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
			static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
			Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
		renderer.PushMatrix();

		renderer.LoadIdentity();
		renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
		renderer.PushMatrix();
		renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset

		//renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI * 150 / 180.f, Eigen::Vector3f::UnitY())).toRotationMatrix());
		if (shipAlive) {
			if (Environment::shipState.shipType == 1 && cargoTexture) {
				glBindTexture(GL_TEXTURE_2D, cargoTexture->getTexID());
				renderer.DrawVertexRenderStruct(cargo);
			}
			else {
				glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
				renderer.DrawVertexRenderStruct(spaceship);
			}


			drawShipSparkEmitters();
		}

		renderer.PopMatrix();
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

		renderer.shaderManager.PushShader(defaultShaderName);
		renderer.RenderUniform1i(textureUniformName, 0);
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE);
		renderer.PushMatrix();
		renderer.LoadIdentity();
		renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
		renderer.RotateMatrix(Environment::inverseShipMatrix);
		renderer.TranslateMatrix(-Environment::shipState.position);
		for (const auto& p : projectiles) {
			if (p && p->isActive()) {
				p->projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
			}
		}
		renderer.PopMatrix();
		glDisable(GL_BLEND);

		renderer.shaderManager.PopShader();

		if (showExplosion) {
			explosionEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height, false);
		}

		glDisable(GL_BLEND);
		renderer.PopMatrix();
		renderer.PopProjectionMatrix();
		renderer.shaderManager.PopShader();
		CheckGlError();
	}

	void Space::drawBoxes()
	{
		renderer.shaderManager.PushShader(defaultShaderName);
		renderer.RenderUniform1i(textureUniformName, 0);
		renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
			static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
			Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);

		for (int i = 0; i < boxCoordsArr.size(); i++)
		{
			if (!boxAlive[i]) continue;
			renderer.PushMatrix();

			renderer.LoadIdentity();
			renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
			renderer.RotateMatrix(Environment::inverseShipMatrix);
			renderer.TranslateMatrix(-Environment::shipState.position);
			renderer.TranslateMatrix({ 0.f, 0.f, 45000.f });
			renderer.TranslateMatrix(boxCoordsArr[i].pos);
			renderer.RotateMatrix(boxCoordsArr[i].m);

			glBindTexture(GL_TEXTURE_2D, boxTexture->getTexID());
			renderer.DrawVertexRenderStruct(boxRenderArr[i]);

			renderer.PopMatrix();
		}
		renderer.PopProjectionMatrix();
		renderer.shaderManager.PopShader();
		CheckGlError();
	}

	void Space::drawScene() {
		glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
		glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);

		glViewport(0, 0, Environment::width, Environment::height);

		prepareSparkEmittersForDraw();

		CheckGlError();

		float skyPercent = 0.0;
		float distance = planetObject.distanceToPlanetSurface(Environment::shipState.position);
		if (distance > 1500.f)
		{
			skyPercent = 0.0f;
		}
		else if (distance < 800.f)
		{
			skyPercent = 1.0f;
		}
		else
		{
			skyPercent = (1500.f - distance) / (1500.f - 800.f);
		}


		drawCubemap(skyPercent);
		planetObject.draw(renderer);
		if (planetObject.distanceToPlanetSurface(Environment::shipState.position) > 100.f)
		{
			glClear(GL_DEPTH_BUFFER_BIT);
		}

		drawRemoteShips();
		drawRemoteShipsLabels();
		drawBoxes();
		drawBoxesLabels();
		drawShip();

		drawCrosshair();
		drawTargetHud();
		CheckGlError();
	}

	void Space::drawRemoteShips() {
		renderer.shaderManager.PushShader(defaultShaderName);
		renderer.RenderUniform1i(textureUniformName, 0);

		renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
			static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
			Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);

		if (!serverBoxesApplied && networkClient) {
			auto sboxes = networkClient->getServerBoxes();
			auto destroyedFlags = networkClient->getServerBoxDestroyedFlags();
			if (!sboxes.empty()) {
				boxCoordsArr.clear();
				boxCoordsArr.resize(sboxes.size());
				for (size_t i = 0; i < sboxes.size(); ++i) {
					BoxCoords bc;
					bc.pos = sboxes[i].first;
					bc.m = sboxes[i].second;
					boxCoordsArr[i] = bc;
				}
				boxRenderArr.resize(boxCoordsArr.size());
				for (int i = 0; i < (int)boxCoordsArr.size(); ++i) {
					boxRenderArr[i].AssignFrom(boxBase);
					boxRenderArr[i].RefreshVBO();
				}
				boxAlive.assign(boxCoordsArr.size(), true);

				size_t n = (std::min)(destroyedFlags.size(), boxAlive.size());
				for (size_t i = 0; i < n; ++i) {
					if (destroyedFlags[i]) boxAlive[i] = false; // destroyed => не рисуем
				}
				boxLabels.clear();
				boxLabels.resize(boxCoordsArr.size());
				for (size_t i = 0; i < boxCoordsArr.size(); ++i) {
					boxLabels[i] = "Box " + std::to_string(i);
				}
				serverBoxesApplied = true;
			}
		}

		for (auto const& [id, remotePlayer] : remotePlayerStates) {

			const ClientState& playerState = remotePlayer;
			if (deadRemotePlayers.count(id)) continue;

			renderer.PushMatrix();
			renderer.LoadIdentity();

			renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
			renderer.RotateMatrix(Environment::inverseShipMatrix);
			renderer.TranslateMatrix(-Environment::shipState.position);


			Eigen::Vector3f relativePos = playerState.position;// -Environment::shipPosition;
			renderer.TranslateMatrix(relativePos);

			renderer.RotateMatrix(playerState.rotation);

			if (playerState.shipType == 1 && cargoTexture) {
				glBindTexture(GL_TEXTURE_2D, cargoTexture->getTexID());
				renderer.DrawVertexRenderStruct(cargo);
			}
			else {
				glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
				renderer.DrawVertexRenderStruct(spaceship);
			}
			renderer.PopMatrix();
		}

		renderer.PopProjectionMatrix();
		renderer.shaderManager.PopShader();

		CheckGlError();
	}

	void Space::drawRemoteShipsLabels()
	{
		if (!textRenderer) return;

		glDisable(GL_DEPTH_TEST);
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

		for (auto const& [id, remotePlayer] : remotePlayerStates)
		{
			if (deadRemotePlayers.count(id)) continue;

			const ClientState& st = remotePlayer;
			Vector3f shipWorld = st.position;

			float distSq = (Environment::shipState.position - shipWorld).squaredNorm();
			float dist = sqrt(distSq);
			float alpha = 1.0f;
			Vector3f labelWorld = shipWorld + Vector3f{ 0.f, -4.f, 0.f };
			float sx, sy, depth;
			if (!worldToScreen(labelWorld, sx, sy, depth))
				continue;

			float uiX = sx, uiY = sy;
			float scale = std::clamp(BASE_SCALE / (dist * PERSPECTIVE_K + 1.f), MIN_SCALE, MAX_SCALE);

			std::string displayName;
			if (!st.nickname.empty() && st.nickname != "Player") {
				displayName = st.nickname;
			}
			else {
				displayName = "Player (" + std::to_string(st.id) + ")";
			}
			std::string label = displayName + " " + std::to_string((int)dist) + "m";

			textRenderer->drawText(label, uiX + 1.f, uiY + 1.f, scale, true, { 0.f, 0.f, 0.f, alpha });
			textRenderer->drawText(label, uiX, uiY, scale, true, { 1.f, 1.f, 1.f, alpha });
		}

		glDisable(GL_BLEND);
		glEnable(GL_DEPTH_TEST);
	}

	// хелпер прицела: добавляет повернутую 2D-линию в меш прицела
	static void AppendRotatedRect2D(
		VertexDataStruct& out,
		float cx, float cy,
		float length, float thickness,
		float angleRad,
		float z,
		const Eigen::Vector3f& rgb)
	{
		// прямоугольник вдоль локальной оси +X: [-L/2..+L/2] и [-T/2..+T/2]
		float hl = length * 0.5f;
		float ht = thickness * 0.5f;

		Eigen::Vector2f p0(-hl, -ht);
		Eigen::Vector2f p1(-hl, +ht);
		Eigen::Vector2f p2(+hl, +ht);
		Eigen::Vector2f p3(+hl, -ht);

		float c = std::cos(angleRad);
		float s = std::sin(angleRad);

		auto rot = [&](const Eigen::Vector2f& p) -> Vector3f {
			float rx = p.x() * c - p.y() * s;
			float ry = p.x() * s + p.y() * c;
			return Vector3f(cx + rx, cy + ry, z);
			};

		Vector3f v0 = rot(p0);
		Vector3f v1 = rot(p1);
		Vector3f v2 = rot(p2);
		Vector3f v3 = rot(p3);

		// 2 треугольника
		out.PositionData.push_back(v0);
		out.PositionData.push_back(v1);
		out.PositionData.push_back(v2);
		out.PositionData.push_back(v2);
		out.PositionData.push_back(v3);
		out.PositionData.push_back(v0);

		for (int i = 0; i < 6; ++i) out.ColorData.push_back(rgb);
	}

	bool Space::loadCrosshairConfig(const std::string& path)
	{
		using json = nlohmann::json;

		std::string content;
		try {
			if (std::string(CONST_ZIP_FILE).empty()) content = readTextFile(path);
			else {
				auto buf = readFileFromZIP(path, CONST_ZIP_FILE);
				if (buf.empty()) return false;
				content.assign(buf.begin(), buf.end());
			}
			json j = json::parse(content);

			if (j.contains("enabled")) crosshairCfg.enabled = j["enabled"].get<bool>();

			if (j.contains("referenceResolution") && j["referenceResolution"].is_array() && j["referenceResolution"].size() == 2) {
				crosshairCfg.refW = j["referenceResolution"][0].get<int>();
				crosshairCfg.refH = j["referenceResolution"][1].get<int>();
			}

			if (j.contains("scale")) crosshairCfg.scaleMul = j["scale"].get<float>();
			crosshairCfg.scaleMul = std::clamp(crosshairCfg.scaleMul, 0.1f, 3.0f);

			if (j.contains("color") && j["color"].is_array() && j["color"].size() == 3) {
				crosshairCfg.color = Eigen::Vector3f(
					j["color"][0].get<float>(),
					j["color"][1].get<float>(),
					j["color"][2].get<float>()
				);
			}

			if (j.contains("cl_crosshairalpha")) crosshairCfg.alpha = j["cl_crosshairalpha"].get<float>();
			if (j.contains("cl_crosshairthickness")) crosshairCfg.thicknessPx = j["cl_crosshairthickness"].get<float>();
			if (j.contains("centerGapPx")) crosshairCfg.gapPx = j["centerGapPx"].get<float>();

			if (j.contains("top") && j["top"].is_object()) {
				auto t = j["top"];
				if (t.contains("lengthPx")) crosshairCfg.topLenPx = t["lengthPx"].get<float>();
				if (t.contains("angleDeg")) crosshairCfg.topAngleDeg = t["angleDeg"].get<float>();
			}

			crosshairCfg.arms.clear();
			if (j.contains("arms") && j["arms"].is_array()) {
				for (auto& a : j["arms"]) {
					CrosshairConfig::Arm arm;
					arm.lenPx = a.value("lengthPx", 20.0f);
					arm.angleDeg = a.value("angleDeg", 210.0f);
					crosshairCfg.arms.push_back(arm);
				}
			}
			else {
				// дефолт
				crosshairCfg.arms.push_back({ 20.0f, 210.0f });
				crosshairCfg.arms.push_back({ 20.0f, 330.0f });
			}

			// clamp
			crosshairCfg.alpha = std::clamp(crosshairCfg.alpha, 0.0f, 1.0f);
			crosshairCfg.thicknessPx = max(0.5f, crosshairCfg.thicknessPx);
			crosshairCfg.gapPx = max(0.0f, crosshairCfg.gapPx);

			crosshairMeshValid = false; // пересобрать
			return true;
		}
		catch (...) {
			return false;
		}
	}

	// пересобирает mesh прицела при изменениях/ресайзе
	void Space::rebuildCrosshairMeshIfNeeded()
	{
		if (!crosshairCfg.enabled) return;

		// если ничего не изменилось — не трогаем VBO
		if (crosshairMeshValid &&
			crosshairLastW == Environment::projectionWidth &&
			crosshairLastH == Environment::projectionHeight &&
			std::abs(crosshairLastAlpha - crosshairCfg.alpha) < 1e-6f &&
			std::abs(crosshairLastThickness - crosshairCfg.thicknessPx) < 1e-6f &&
			std::abs(crosshairLastGap - crosshairCfg.gapPx) < 1e-6f &&
			std::abs(crosshairLastScaleMul - crosshairCfg.scaleMul) < 1e-6f)
		{
			return;
		}

		crosshairLastW = Environment::projectionWidth;
		crosshairLastH = Environment::projectionHeight;
		crosshairLastAlpha = crosshairCfg.alpha;
		crosshairLastThickness = crosshairCfg.thicknessPx;
		crosshairLastGap = crosshairCfg.gapPx;
		crosshairLastScaleMul = crosshairCfg.scaleMul;

		float cx = Environment::projectionWidth * 0.5f;
		float cy = Environment::projectionHeight * 0.5f;

		// масштаб от reference (стандартно: по высоте)
		float scale = (crosshairCfg.refH > 0) ? (Environment::projectionHeight / (float)crosshairCfg.refH) : 1.0f;
		scale *= crosshairCfg.scaleMul;

		float thickness = crosshairCfg.thicknessPx * scale;
		float gap = crosshairCfg.gapPx * scale;

		VertexDataStruct v;
		v.PositionData.reserve(6 * (1 + (int)crosshairCfg.arms.size()));
		v.ColorData.reserve(6 * (1 + (int)crosshairCfg.arms.size()));

		const float z = 0.0f;
		const Eigen::Vector3f rgb = crosshairCfg.color;

		auto deg2rad = [](float d) { return d * 3.1415926535f / 180.0f; };

		// TOP (короткая палочка сверху)
		{
			float len = crosshairCfg.topLenPx * scale;
			float ang = deg2rad(crosshairCfg.topAngleDeg);

			// сдвигаем сегмент от центра на gap + len/2 по направлению
			float dx = std::cos(ang);
			float dy = std::sin(ang);
			float mx = cx + dx * (gap + len * 0.5f);
			float my = cy + dy * (gap + len * 0.5f);

			AppendRotatedRect2D(v, mx, my, len, thickness, ang, z, rgb);
		}

		// ARMS (2 луча вниз-влево и вниз-вправо)
		for (auto& a : crosshairCfg.arms)
		{
			float len = a.lenPx * scale;
			float ang = deg2rad(a.angleDeg);

			float dx = std::cos(ang);
			float dy = std::sin(ang);
			float mx = cx + dx * (gap + len * 0.5f);
			float my = cy + dy * (gap + len * 0.5f);

			AppendRotatedRect2D(v, mx, my, len, thickness, ang, z, rgb);
		}

		crosshairMesh.AssignFrom(v);
		crosshairMesh.RefreshVBO();
		crosshairMeshValid = true;
	}

	void Space::drawCrosshair()
	{
		if (!crosshairCfg.enabled) return;

		rebuildCrosshairMeshIfNeeded();
		if (!crosshairMeshValid) return;

		glDisable(GL_DEPTH_TEST);
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

		renderer.shaderManager.PushShader("defaultColor");
		renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
		renderer.PushMatrix();
		renderer.LoadIdentity();

		Eigen::Vector4f uColor(crosshairCfg.color.x(), crosshairCfg.color.y(), crosshairCfg.color.z(), crosshairCfg.alpha);
		renderer.RenderUniform4fv("uColor", uColor.data());

		renderer.DrawVertexRenderStruct(crosshairMesh);

		renderer.PopMatrix();
		renderer.PopProjectionMatrix();
		renderer.shaderManager.PopShader();

		glDisable(GL_BLEND);
		glEnable(GL_DEPTH_TEST);
	}

	int Space::pickTargetId() const
	{
		// Use manually selected target if it's still alive and in range
		if (manualTrackedTargetId >= 0) {
			auto it = remotePlayerStates.find(manualTrackedTargetId);
			if (it != remotePlayerStates.end() && !deadRemotePlayers.count(manualTrackedTargetId)) {
				float d2 = (Environment::shipState.position - it->second.position).squaredNorm();
				if (d2 <= TARGET_MAX_DIST_SQ) return manualTrackedTargetId;
			}
			// Target no longer valid — fall through to auto-pick
		}

		int bestId = -1;
		float bestDistSq = 1e30f;

		for (auto const& [id, st] : remotePlayerStates) {
			if (deadRemotePlayers.count(id)) continue;

			float d2 = (Environment::shipState.position - st.position).squaredNorm();

			if (d2 > TARGET_MAX_DIST_SQ) continue;

			if (d2 < bestDistSq) {
				bestDistSq = d2;
				bestId = id;
			}
		}
		return bestId;
	}

	static Vector3f ForwardFromRotation(const Matrix3f& rot)
	{
		Vector3f localForward(0, 0, -1);
		Vector3f worldForward = rot * localForward;
		float len = worldForward.norm();
		if (len > 1e-6f) worldForward /= len;
		return worldForward;
	}

	static bool SolveLeadInterceptTime(
		const Vector3f& shooterPos,
		const Vector3f& shooterVel,
		const Vector3f& targetPos,
		const Vector3f& targetVel,
		float projectileSpeed,   // muzzle speed (например 60)
		float& outT)
	{
		Vector3f r = targetPos - shooterPos;
		Vector3f v = targetVel - shooterVel;
		float S = projectileSpeed;

		float a = v.dot(v) - S * S;
		float b = 2.0f * r.dot(v);
		float c = r.dot(r);

		// Если a почти 0 -> линейный случай
		if (std::abs(a) < 1e-6f) {
			if (std::abs(b) < 1e-6f) return false; // нет решения
			float t = -c / b;
			if (t > 0.0f) { outT = t; return true; }
			return false;
		}

		float disc = b * b - 4.0f * a * c;
		if (disc < 0.0f) return false;

		float sqrtDisc = std::sqrt(disc);
		float t1 = (-b - sqrtDisc) / (2.0f * a);
		float t2 = (-b + sqrtDisc) / (2.0f * a);

		float t = 1e30f;
		if (t1 > 0.0f) t = min(t, t1);
		if (t2 > 0.0f) t = min(t, t2);

		if (t >= 1e29f) return false;
		outT = t;
		return true;
	}

	static VertexDataStruct MakeRing2D(
		float cx, float cy,
		float innerR, float outerR,
		float z,
		int segments,
		const Eigen::Vector4f& rgba)
	{
		VertexDataStruct v;
		v.PositionData.reserve(segments * 6);
		v.ColorData.reserve(segments * 6);

		Vector3f rgb(rgba.x(), rgba.y(), rgba.z());

		const float twoPi = 6.28318530718f;
		for (int i = 0; i < segments; ++i) {
			float a0 = twoPi * (float)i / (float)segments;
			float a1 = twoPi * (float)(i + 1) / (float)segments;

			float c0 = std::cos(a0), s0 = std::sin(a0);
			float c1 = std::cos(a1), s1 = std::sin(a1);

			Vector3f p0i(cx + innerR * c0, cy + innerR * s0, z);
			Vector3f p0o(cx + outerR * c0, cy + outerR * s0, z);
			Vector3f p1i(cx + innerR * c1, cy + innerR * s1, z);
			Vector3f p1o(cx + outerR * c1, cy + outerR * s1, z);

			// два треугольника (p0i,p0o,p1o) и (p0i,p1o,p1i)
			v.PositionData.push_back(p0i);
			v.PositionData.push_back(p0o);
			v.PositionData.push_back(p1o);

			v.PositionData.push_back(p0i);
			v.PositionData.push_back(p1o);
			v.PositionData.push_back(p1i);

			for (int k = 0; k < 6; ++k) v.ColorData.push_back(rgb);
		}
		return v;
	}

	static VertexDataStruct MakeColoredRect2D(float cx, float cy, float hw, float hh, float z,
		const Eigen::Vector4f& rgba)
	{
		VertexDataStruct v;
		// 2 triangles
		Vector3f p1{ cx - hw, cy - hh, z };
		Vector3f p2{ cx - hw, cy + hh, z };
		Vector3f p3{ cx + hw, cy + hh, z };
		Vector3f p4{ cx + hw, cy - hh, z };

		v.PositionData = { p1, p2, p3,  p3, p4, p1 };

		// defaultColor shader likely uses vColor (vec3), но нам нужен alpha.
		// У тебя в Renderer есть RenderUniform4fv, но шейдер может брать vColor.
		// Поэтому: сделаем ColorData vec3, а alpha будем задавать uniform'ом отдельно.
		Vector3f rgb{ rgba.x(), rgba.y(), rgba.z() };
		v.ColorData = { rgb, rgb, rgb, rgb, rgb, rgb };

		// defaultColor vertex shader expects vNormal and vTexCoord; provide valid values
		// so WebGL/GLSL doesn't get NaN from normalize(vec3(0,0,0)).
		const Vector3f n{ 0.f, 0.f, 1.f };
		v.NormalData = { n, n, n, n, n, n };
		const Vector2f uv{ 0.f, 0.f };
		v.TexCoordData = { uv, uv, uv, uv, uv, uv };
		return v;
	}

	void Space::drawTargetHud()
	{
		if (!textRenderer) return;

		// 1) выбираем цель
		int targetIdNow = pickTargetId();
		if (targetIdNow < 0) {
			trackedTargetId = -1;
			targetAcquireAnim = 0.f;
			targetWasVisible = false;
			return;
		}

		// если цель сменилась — сброс анимации “схлопывания”
		if (trackedTargetId != targetIdNow) {
			trackedTargetId = targetIdNow;
			targetAcquireAnim = 0.0f;
			targetWasVisible = false;
		}

		const ClientState& st = remotePlayerStates.at(trackedTargetId);
		Vector3f shipWorld = st.position;

		// Lead Indicator
		// скорость пули (как в fireProjectiles)
		const float projectileSpeed = PROJECTILE_VELOCITY;

		// позиция вылета 
		Vector3f shooterPos = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.0f, 0.9f - 6.0f, 5.0f };

		// скорость цели в мире (вектор)
		// Vector3f shooterVel = ForwardFromRotation(Environment::shipState.rotation) * Environment::shipState.velocity;
		
		float shooterSpeed = std::abs(Environment::shipState.velocity);
		// В нашей физике линейная скорость корабля всегда направлена по его forward (-Z)
		// Когда игрок наводится на lead indicator, forward (и скорость) становятся сонаправлены с выстрелом
		// поэтому эффективная скорость снаряда в мире ≈ muzzle + shipSpeed.
		const float effectiveProjectileSpeed = projectileSpeed + shooterSpeed;
		Vector3f shooterVel = Vector3f::Zero(); // скорость уже учтена в effectiveProjectileSpeed
		Vector3f targetVel = ForwardFromRotation(st.rotation) * st.velocity;

		// ВАЖНО: remote state берется на now_ms - CLIENT_DELAY
		// Значит shipWorld - это позиция ~0.5 сек назад.
		// Для корректного lead нужно предсказать положение цели на сейчас
		const float clientDelaySec = (float)CLIENT_DELAY / 1000.0f;
		Vector3f targetPosNow = shipWorld + targetVel * clientDelaySec;

		const float minTargetSpeed = 0.5f; // подобрать (в твоих единицах)
		bool targetMoving = (targetVel.norm() > minTargetSpeed);
		
		// альфа круга
		float leadAlpha = targetMoving ? 1.0f : 0.5f;

		Vector3f leadWorld = targetPosNow;
		bool haveLead = false;

		// Дистанцию лучше считать от реальной точки вылета
		float distToTarget = (shooterPos - targetPosNow).norm();
		// Максимальное время перехвата ограничиваем жизнью пули
		const float projectileLifeSec = (float)PROJECTILE_LIFE / 1000.0f;
		float maxLeadTime = std::clamp((distToTarget / effectiveProjectileSpeed) * 1.25f, 0.01f, projectileLifeSec * 0.98f);

		if (!targetMoving) {
			// Цель стоит: рисуем lead прямо на ней, но полупрозрачный
			leadWorld = targetPosNow;
			haveLead = true;
		}
		else {
			float tLead = 0.0f;

			// 1) Пытаемся “правильное” решение перехвата
			bool ok = SolveLeadInterceptTime(shooterPos, shooterVel, targetPosNow, targetVel, effectiveProjectileSpeed, tLead);

			// 2) Если решения нет / оно плохое — fallback (чтобы круг не пропадал при пролёте "вбок")
			//    Это ключевое изменение: lead всегда будет.
			if (!ok || !(tLead > 0.0f) || tLead > maxLeadTime) {
				tLead = std::clamp(distToTarget / effectiveProjectileSpeed, 0.05f, maxLeadTime);
			}

			leadWorld = targetPosNow + targetVel * tLead;
			haveLead = true;
		}

		// Проекция цели (для рамок/стрелки)
		float ndcX, ndcY, ndcZ, clipW;
		if (!projectToNDC(shipWorld, ndcX, ndcY, ndcZ, clipW)) return;

		bool behind = (clipW <= 0.0f);

		bool onScreen = (!behind &&
			ndcX >= -1.0f && ndcX <= 1.0f &&
			ndcY >= -1.0f && ndcY <= 1.0f);

		float dist = (Environment::shipState.position - shipWorld).norm();
		float t = static_cast<float>(SDL_GetTicks64()) * 0.001f;

		// Проекция Lead
		float leadNdcX = 0.f, leadNdcY = 0.f, leadNdcZ = 0.f, leadClipW = 0.f;
		bool leadOnScreen = false;

		if (haveLead) {
			if (projectToNDC(leadWorld, leadNdcX, leadNdcY, leadNdcZ, leadClipW) && leadClipW > 0.0f) {
				leadOnScreen =
					(leadNdcX >= -1.0f && leadNdcX <= 1.0f &&
					 leadNdcY >= -1.0f && leadNdcY <= 1.0f);
			}
		}

		// Настройки HUD стилизация
		Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный
		float thickness = 2.0f;                        // толщина линий (px)
		float z = 0.0f;                                // 2D слой

		auto drawLeadRing2D = [&](float lx, float ly)
			{
				float distLead = (Environment::shipState.position - leadWorld).norm();
				float r = 30.0f / (distLead * 0.01f + 1.0f);
				r = std::clamp(r, 6.0f, 18.0f);

				float thicknessPx = 2.5f;
				float innerR = max(1.0f, r - thicknessPx);
				float outerR = r + thicknessPx;

				Eigen::Vector4f leadColor = enemyColor;
				leadColor.w() = leadAlpha;

				renderer.RenderUniform4fv("uColor", leadColor.data());
				VertexDataStruct ring = MakeRing2D(lx, ly, innerR, outerR, 0.0f, 32, enemyColor);
				hudTempMesh.AssignFrom(ring);
				renderer.DrawVertexRenderStruct(hudTempMesh);

				// вернуть цвет HUD обратно
				renderer.RenderUniform4fv("uColor", enemyColor.data());
			};


		// Цель в кадре: рамки
		if (onScreen)
		{
			// перевод NDC -> экран (в пикселях)
			float sx = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth;
			float sy = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight;

			// анимация “снаружи внутрь”
			// targetAcquireAnim растёт к 1, быстро (похоже на захват)
			float dt = 1.0f / 60.0f; // у тебя нет dt в draw, берём константу, выглядит норм
			targetAcquireAnim = min(1.0f, targetAcquireAnim + dt * 6.5f);

			// базовый размер рамки в зависимости от дистанции (как у лейблов)
			float size = 220.0f / (dist * 0.01f + 1.0f);     // подстройка
			size = std::clamp(size, 35.0f, 120.0f);          // min/max

			// “схлопывание”: сначала больше, потом ближе к кораблю
			// expand 1.6 -> 1.0
			float expand = 1.6f - 0.6f * targetAcquireAnim;

			float half = size * expand;
			float cornerLen = max(10.0f, half * 0.35f);

			// точки углов
			float left = sx - half;
			float right = sx + half;
			float bottom = sy - half;
			float top = sy + half;

			// рисуем 8 тонких прямоугольников (2 на угол)
			auto drawBar = [&](float cx, float cy, float w, float h)
			{
				VertexDataStruct v = MakeColoredRect2D(cx, cy, w * 0.5f, h * 0.5f, z, enemyColor);
				hudTempMesh.AssignFrom(v);
				renderer.DrawVertexRenderStruct(hudTempMesh);
			};

			// включаем 2D режим
			glDisable(GL_DEPTH_TEST);
			glEnable(GL_BLEND);
			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
			glClear(GL_DEPTH_BUFFER_BIT);

			renderer.shaderManager.PushShader("defaultColor");
			renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
			renderer.PushMatrix();
			renderer.LoadIdentity();

			renderer.RenderUniform4fv("uColor", enemyColor.data());

			// рамки
			drawBar(left + cornerLen * 0.5f, top, cornerLen, thickness);
			drawBar(left, top - cornerLen * 0.5f, thickness, cornerLen);

			drawBar(right - cornerLen * 0.5f, top, cornerLen, thickness);
			drawBar(right, top - cornerLen * 0.5f, thickness, cornerLen);

			drawBar(left + cornerLen * 0.5f, bottom, cornerLen, thickness);
			drawBar(left, bottom + cornerLen * 0.5f, thickness, cornerLen);

			drawBar(right - cornerLen * 0.5f, bottom, cornerLen, thickness);
			drawBar(right, bottom + cornerLen * 0.5f, thickness, cornerLen);

			// LEAD — независимо от рамок: если его точка на экране, рисуем
			if (haveLead && leadOnScreen) {
				float lx = (leadNdcX * 0.5f + 0.5f) * Environment::projectionWidth;
				float ly = (leadNdcY * 0.5f + 0.5f) * Environment::projectionHeight;
				drawLeadRing2D(lx, ly);
			}

			renderer.PopMatrix();
			renderer.PopProjectionMatrix();
			renderer.shaderManager.PopShader();

			glDisable(GL_BLEND);
			glEnable(GL_DEPTH_TEST);

			targetWasVisible = true;
			return;
		}


		// Цель вне экрана: стрелка
		float dirX = ndcX;
		float dirY = ndcY;

		if (behind) {
			dirX = -dirX;
			dirY = -dirY;
		}

		float len = std::sqrt(dirX * dirX + dirY * dirY);
		if (len < 1e-5f) return;
		dirX /= len;
		dirY /= len;

		float marginNdc = 0.08f;
		float maxX = 1.0f - marginNdc;
		float maxY = 1.0f - marginNdc;

		float tx = (std::abs(dirX) < 1e-6f) ? 1e9f : (maxX / std::abs(dirX));
		float ty = (std::abs(dirY) < 1e-6f) ? 1e9f : (maxY / std::abs(dirY));
		float k = min(tx, ty);

		float edgeNdcX = dirX * k;
		float edgeNdcY = dirY * k;

		float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::projectionWidth;
		float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::projectionHeight;

		float bob = std::sin(t * 6.0f) * 6.0f;
		edgeX += dirX * bob;
		edgeY += dirY * bob;

		float arrowLen = 26.0f;
		float arrowWid = 14.0f;

		float px = -dirY;
		float py = dirX;

		Vector3f tip{ edgeX + dirX * arrowLen, edgeY + dirY * arrowLen, z };
		Vector3f left{ edgeX + px * (arrowWid * 0.5f), edgeY + py * (arrowWid * 0.5f), z };
		Vector3f right{ edgeX - px * (arrowWid * 0.5f), edgeY - py * (arrowWid * 0.5f), z };

		auto drawTri = [&](const Vector3f& a, const Vector3f& b, const Vector3f& c)
		{
			VertexDataStruct v;
			v.PositionData = { a, b, c };
			Vector3f rgb{ enemyColor.x(), enemyColor.y(), enemyColor.z() };
			v.ColorData = { rgb, rgb, rgb };
			// defaultColor vertex shader expects vNormal and vTexCoord (avoids NaN on WebGL).
			const Vector3f n{ 0.f, 0.f, 1.f };
			v.NormalData = { n, n, n };
			const Vector2f uv{ 0.f, 0.f };
			v.TexCoordData = { uv, uv, uv };
			hudTempMesh.AssignFrom(v);
			renderer.DrawVertexRenderStruct(hudTempMesh);
		};

		auto drawBar = [&](float cx, float cy, float w, float h)
		{
			VertexDataStruct v = MakeColoredRect2D(cx, cy, w * 0.5f, h * 0.5f, z, enemyColor);
			hudTempMesh.AssignFrom(v);
			renderer.DrawVertexRenderStruct(hudTempMesh);
		};

		glDisable(GL_DEPTH_TEST);
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

		renderer.shaderManager.PushShader("defaultColor");
		renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
		renderer.PushMatrix();
		renderer.LoadIdentity();

		renderer.RenderUniform4fv("uColor", enemyColor.data());

		// стрелка
		drawTri(tip, left, right);

		float tailLen = 14.0f;
		float tailX = edgeX - dirX * 6.0f;
		float tailY = edgeY - dirY * 6.0f;
		drawBar(tailX, tailY, max(thickness, tailLen), thickness);

		// LEAD — рисуем даже когда цель вне экрана (если lead точка на экране)
		if (haveLead && leadOnScreen) {
			float lx = (leadNdcX * 0.5f + 0.5f) * Environment::projectionWidth;
			float ly = (leadNdcY * 0.5f + 0.5f) * Environment::projectionHeight;
			drawLeadRing2D(lx, ly);
		}

		renderer.PopMatrix();
		renderer.PopProjectionMatrix();
		renderer.shaderManager.PopShader();

		// дистанция около стрелки
		{
			std::string d = std::to_string((int)dist) + "m";
			float tx = edgeX + px * 18.0f;
			float ty = edgeY + py * 18.0f;
			textRenderer->drawText(d, tx, ty, 0.6f, true, { 1.f, 0.f, 0.f, 1.f });
		}

		glDisable(GL_BLEND);
		glEnable(GL_DEPTH_TEST);

		targetWasVisible = false;
	}

	void Space::updateSparkEmitters(float deltaMs)
	{
		// Local ship
		SparkEmitter* sparkEmitterPtr;
		if (Environment::shipState.shipType == 1) {
			sparkEmitterPtr = &sparkEmitterCargo;
			static std::vector<Vector3f> emissionPoints = { Vector3f(0, 0, 0), Vector3f(0, 0, 0) };
			emissionPoints[0] = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.0, 2.8, -6.5 + 16.0 };
			emissionPoints[1] = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.0, 1.5, -6.5 + 16.0 };
			sparkEmitterPtr->setEmissionPoints(emissionPoints);
		}
		else {
			sparkEmitterPtr = &sparkEmitter;
			static std::vector<Vector3f> emissionPoints = { Vector3f(0, 0, 0), Vector3f(0, 0, 0) };
			emissionPoints[0] = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ -0.9, 1.4 - 1.0, -8.5 + 16.0 };
			emissionPoints[1] = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.9, 1.4 - 1.0, -8.5 + 16.0 };
			sparkEmitterPtr->setEmissionPoints(emissionPoints);
		}
		sparkEmitterPtr->setIsActive(Environment::shipState.velocity > 0.1f);
		sparkEmitterPtr->update(deltaMs);

		// Remote ships
		for (auto const& [id, playerState] : remotePlayerStates) {
			if (deadRemotePlayers.count(id)) continue;
			if (!remoteShipSparkEmitters.count(id)) {
				remoteShipSparkEmitters.emplace(id, playerState.shipType == 1 ? sparkEmitterCargo : sparkEmitter);
			}
			auto& remEmitter = remoteShipSparkEmitters.at(id);
			std::vector<Vector3f> remEmitPts(2);
			if (playerState.shipType == 1) {
				remEmitPts[0] = playerState.position + playerState.rotation * Vector3f{ 0.0f, -0.4f+2.8f, 8.4f };
				remEmitPts[1] = playerState.position + playerState.rotation * Vector3f{ 0.0f, -0.4f+1.5f, 8.4f };
			} else {
				remEmitPts[0] = playerState.position + playerState.rotation * Vector3f{ -0.9f, -0.2,5.6 };
				remEmitPts[1] = playerState.position + playerState.rotation * Vector3f{ 0.9f,-0.2,5.6 };
			}
			remEmitter.setEmissionPoints(remEmitPts);
			remEmitter.setIsActive(playerState.velocity > 0.1f);
			remEmitter.update(deltaMs);
		}
	}

	void Space::prepareSparkEmittersForDraw()
	{
		sparkEmitter.prepareForDraw(true);
		sparkEmitterCargo.prepareForDraw(true);
		for (auto& [id, emitter] : remoteShipSparkEmitters) {
			if (!deadRemotePlayers.count(id)) emitter.prepareForDraw(true);
		}
		explosionEmitter.prepareForDraw(false);
		for (const auto& p : projectiles) {
			if (p && p->isActive()) {
				p->projectileEmitter.prepareForDraw(true);
			}
		}
	}

	void Space::drawShipSparkEmitters()
	{
		renderer.PushMatrix();
		renderer.RotateMatrix(Environment::inverseShipMatrix);
		renderer.TranslateMatrix(-Environment::shipState.position);
		if (Environment::shipState.shipType == 1) {
			sparkEmitterCargo.draw(renderer, Environment::zoom, Environment::width, Environment::height);
		} else {
			sparkEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
		}

		for (auto& [id, emitter] : remoteShipSparkEmitters) {
			if (!deadRemotePlayers.count(id)) {

				renderer.PushMatrix();
				renderer.LoadIdentity();
				renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
				renderer.RotateMatrix(Environment::inverseShipMatrix);
				renderer.TranslateMatrix(-Environment::shipState.position);
				emitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
				renderer.PopMatrix();
			}
		}
		renderer.PopMatrix();
	}

	void Space::processTickCount(int64_t newTickCount, int64_t delta) {

		auto now_ms = newTickCount;

		if (firePressed)
		{
			firePressed = false;
			if (now_ms - lastProjectileFireTime >= static_cast<uint64_t>(projectileCooldownMs)) {
				lastProjectileFireTime = now_ms;
				const float projectileSpeed = PROJECTILE_VELOCITY;

				this->fireProjectiles();

				Eigen::Vector3f localForward = { 0, 0, -1 };
				Eigen::Vector3f worldForward = (Environment::shipState.rotation * localForward).normalized();

				Eigen::Vector3f centerPos = Environment::shipState.position +
					Environment::shipState.rotation * Vector3f{ 0, 0.9f - 6.0f, 5.0f };

				Eigen::Quaternionf q(Environment::shipState.rotation);
				float speedToSend = projectileSpeed + Environment::shipState.velocity;
				int shotCount = 2;

				std::string fireMsg = "FIRE:" +
					std::to_string(now_ms) + ":" +
					std::to_string(centerPos.x()) + ":" +
					std::to_string(centerPos.y()) + ":" +
					std::to_string(centerPos.z()) + ":" +
					std::to_string(q.w()) + ":" +
					std::to_string(q.x()) + ":" +
					std::to_string(q.y()) + ":" +
					std::to_string(q.z()) + ":" +
					std::to_string(speedToSend) + ":" +
					std::to_string(shotCount);

				networkClient->Send(fireMsg);
			}
		}


		//Handle input:

		if (newShipVelocity != Environment::shipState.selectedVelocity)
		{
			Environment::shipState.selectedVelocity = newShipVelocity;

			std::string msg = "UPD:" + std::to_string(now_ms) + ":" + Environment::shipState.formPingMessageContent();
			networkClient->Send(msg);
		}

		float discreteMag;
		int discreteAngle;

		if (Environment::tapDownHold) {
			float diffx = Environment::tapDownCurrentPos(0) - Environment::tapDownStartPos(0);
			float diffy = Environment::tapDownCurrentPos(1) - Environment::tapDownStartPos(1);

			float rawMag = sqrtf(diffx * diffx + diffy * diffy);
			float maxRadius = 200.0f; // Максимальный вынос джойстика

			if (rawMag > 10.0f) { // Мертвая зона
				// 1. Дискретизируем отклонение (0.0 - 1.0 с шагом 0.1)
				float normalizedMag = min(rawMag / maxRadius, 1.0f);
				discreteMag = std::round(normalizedMag * 10.0f) / 10.0f;

				// 2. Дискретизируем угол (0-359 градусов)
				// atan2 возвращает радианы, переводим в градусы
				float radians = atan2f(diffy, diffx);
				discreteAngle = static_cast<int>(radians * 180.0f / M_PI);
				if (discreteAngle < 0) discreteAngle += 360;

			}
			else
			{
				discreteAngle = -1;
				discreteMag = 0.0f;
			}
		}
		else
		{
			discreteAngle = -1;
			discreteMag = 0.0f;
		}


		if (discreteAngle != Environment::shipState.discreteAngle || discreteMag != Environment::shipState.discreteMag) {
			Environment::shipState.discreteAngle = discreteAngle;
			Environment::shipState.discreteMag = discreteMag;

			std::string msg = "UPD:" + std::to_string(now_ms) + ":" + Environment::shipState.formPingMessageContent();
			networkClient->Send(msg);
			//std::cout << "Sending: " << msg << std::endl;
		}

		long long leftoverDelta = delta;
		while (leftoverDelta > 0)
		{
			long long miniDelta = 50;
			Environment::shipState.simulate_physics(miniDelta);
			leftoverDelta -= miniDelta;
		}
		Environment::inverseShipMatrix = Environment::shipState.rotation.inverse();

		static float pingTimer = 0.0f;
		pingTimer += delta;
		if (pingTimer >= 1000.0f) {
			std::string pingMsg = "UPD:" + std::to_string(now_ms) + ":" + Environment::shipState.formPingMessageContent();

			networkClient->Send(pingMsg);
			//std::cout << "Sending: " << pingMsg << std::endl;
			pingTimer = 0.0f;
		}


		auto latestRemotePlayers = networkClient->getRemotePlayers();

		std::chrono::system_clock::time_point nowRoundedWithDelay{ std::chrono::milliseconds(newTickCount - CLIENT_DELAY) };


		for (auto const& [id, remotePlayer] : latestRemotePlayers) {

			if (networkClient && id == networkClient->GetClientId()) {
				continue;
			}

			if (!remotePlayer.canFetchClientStateAtTime(nowRoundedWithDelay))
			{
				continue;
			}

			ClientState playerState = remotePlayer.fetchClientStateAtTime(nowRoundedWithDelay);

			remotePlayerStates[id] = playerState;
		}

		updateShowPlayersButtonState();
		updateSparkEmitters(static_cast<float>(delta));

		for (auto& p : projectiles) {
			if (p && p->isActive()) {
				p->update(static_cast<float>(delta), renderer);
			}
		}

		
		for (const auto& p : projectiles) {
			if (p && p->isActive()) {
				Vector3f worldPos = p->getPosition();
				p->projectileEmitter.resetEmissionPoints({ worldPos });
				p->projectileEmitter.update(static_cast<float>(delta));
			}
		}


		explosionEmitter.update(static_cast<float>(delta));
		if (showExplosion) {
			uint64_t now = SDL_GetTicks64();
			if (lastExplosionTime != 0 && now - lastExplosionTime >= explosionDurationMs) {
				showExplosion = false;
				explosionEmitter.setEmissionPoints(std::vector<Vector3f>());
				explosionEmitter.setUseWorldSpace(false);
			}
		}
		if (shipAlive) {
			float distToSurface = planetObject.distanceToPlanetSurface(Environment::shipState.position);
			if (distToSurface <= 0.0f) {

				Vector3f dir = (Environment::shipState.position - PlanetData::PLANET_CENTER_OFFSET).normalized();
				Vector3f collisionPoint = PlanetData::PLANET_CENTER_OFFSET + dir * PlanetData::PLANET_RADIUS;
				Environment::shipState.position = PlanetData::PLANET_CENTER_OFFSET + dir * (PlanetData::PLANET_RADIUS + shipCollisionRadius + 0.1f);

				shipAlive = false;
				gameOver = true;
				Environment::shipState.selectedVelocity = 0;
				Environment::shipState.velocity = 0.0f;
				newShipVelocity = 0;
				showExplosion = true;

				explosionEmitter.setUseWorldSpace(true);
				explosionEmitter.setEmissionPoints(std::vector<Vector3f>{ collisionPoint });
				explosionEmitter.emit();
				lastExplosionTime = SDL_GetTicks64();

				std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n";

				clearPlayerListIfVisible();
				menuManager.showGameOver(this->playerScore);
			}
			else {
				bool stoneCollided = false;
				int collidedTriIdx = -1;
				Vector3f collidedStonePos = Vector3f{ 0.0f, 0.0f, 0.0f };
				float collidedStoneRadius = 0.0f;

				for (int triIdx : planetObject.triangleIndicesToDraw) {
					if (triIdx < 0 || triIdx >= static_cast<int>(planetObject.planetStones.allInstances.size()))
						continue;

					if (planetObject.planetStones.statuses.size() <= static_cast<size_t>(triIdx))
						continue;

					if (planetObject.planetStones.statuses[triIdx] != ChunkStatus::Live)
						continue;

					const auto& instances = planetObject.planetStones.allInstances[triIdx];
					for (const auto& inst : instances) {

						Vector3f stoneWorld = inst.position;
						Vector3f diff = Environment::shipState.position - stoneWorld;

						float maxScale = (std::max)({ inst.scale(0), inst.scale(1), inst.scale(2) });
						float stoneRadius = StoneParams::BASE_SCALE * maxScale * 0.9f;
						float thresh = shipCollisionRadius + stoneRadius;

						if (diff.squaredNorm() <= thresh * thresh) {
							stoneCollided = true;
							collidedTriIdx = triIdx;
							collidedStonePos = stoneWorld;
							collidedStoneRadius = stoneRadius;
							break;
						}
					}

					if (stoneCollided) break;
				}

				if (stoneCollided) {
					Vector3f away = (Environment::shipState.position - collidedStonePos);
					if (away.squaredNorm() <= 1e-6f) {
						away = Vector3f{ 0.0f, 1.0f, 0.0f };
					}
					away.normalize();

					Environment::shipState.position = collidedStonePos + away * (collidedStoneRadius + shipCollisionRadius + 0.1f);

					shipAlive = false;
					gameOver = true;
					Environment::shipState.selectedVelocity = 0;
					Environment::shipState.velocity = 0.0f;
					newShipVelocity = 0;
					showExplosion = true;

					explosionEmitter.setUseWorldSpace(true);
					explosionEmitter.setEmissionPoints(std::vector<Vector3f>{ collidedStonePos });
					explosionEmitter.emit();
					lastExplosionTime = SDL_GetTicks64();

					std::cerr << "GAME OVER: collision with stone on triangle " << collidedTriIdx << std::endl;

					if (collidedTriIdx >= 0 && collidedTriIdx < static_cast<int>(planetObject.stonesToRender.size())) {
						planetObject.stonesToRender[collidedTriIdx].data.PositionData.clear();
						planetObject.stonesToRender[collidedTriIdx].vao.reset();
						planetObject.stonesToRender[collidedTriIdx].positionVBO.reset();
						planetObject.stonesToRender[collidedTriIdx].normalVBO.reset();
						planetObject.stonesToRender[collidedTriIdx].tangentVBO.reset();
						planetObject.stonesToRender[collidedTriIdx].binormalVBO.reset();
						planetObject.stonesToRender[collidedTriIdx].colorVBO.reset();
						planetObject.stonesToRender[collidedTriIdx].texCoordVBO.reset();
					}
					if (collidedTriIdx >= 0 && collidedTriIdx < static_cast<int>(planetObject.planetStones.statuses.size())) {
						planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty;
					}

					clearPlayerListIfVisible();
					menuManager.showGameOver(this->playerScore);
				}
			}
		}

		planetObject.update(static_cast<float>(delta));

		// update velocity text

		if (shipAlive && !gameOver) {
			auto velocityTv = menuManager.uiManager.findTextView("velocityText");
			if (velocityTv) {
				std::string velocityStr = "Velocity: " + std::to_string(static_cast<int>(Environment::shipState.velocity));
				menuManager.uiManager.setText("velocityText", velocityStr);
			}

			bool canPickup = false;
			if (Environment::shipState.shipType == 1 && Environment::shipState.velocity < 0.1f) {
				for (size_t i = 0; i < boxCoordsArr.size(); ++i) {
					if (i >= boxAlive.size() || !boxAlive[i]) continue;
					Vector3f boxWorld = boxCoordsArr[i].pos + Vector3f{ 0.f, 0.f, 45000.f };
					float distSq = (Environment::shipState.position - boxWorld).squaredNorm();
					if (distSq <= BOX_PICKUP_RADIUS * BOX_PICKUP_RADIUS) {
						canPickup = true;
						break;
					}
				}
			}
			if (canPickup != nearPickupBox) {
				nearPickupBox = canPickup;
				if (auto btn = menuManager.uiManager.findButton("takeButton"))
					btn->state = canPickup ? ButtonState::Normal : ButtonState::Disabled;
			}
		}

		if (playerScore != prevPlayerScore)
		{
			prevPlayerScore = playerScore;
			menuManager.uiManager.setText("gameScoreText", "Score: " + std::to_string(playerScore));
		}
	}

	void Space::fireProjectiles() {
		std::vector<Vector3f> localOffsets = {
			Vector3f{ -1.5f, 0.9f - 6.f, 5.0f },
			Vector3f{ 1.5f, 0.9f - 6.f, 5.0f }
		};


		const float projectileSpeed = PROJECTILE_VELOCITY;
		const float lifeMs = PROJECTILE_LIFE;
		const float size = 0.5f;

		Vector3f localForward = { 0,0,-1 };
		Vector3f worldForward = (Environment::shipState.rotation * localForward).normalized();

		for (const auto& lo : localOffsets) {
			Vector3f worldPos = Environment::shipState.position + Environment::shipState.rotation * lo;
			Vector3f worldVel = worldForward * (projectileSpeed + Environment::shipState.velocity);

			for (auto& p : projectiles) {
				if (!p->isActive()) {
					p->init(worldPos, worldVel, lifeMs, size, projectileTexture, renderer);
					p->projectileEmitter = SparkEmitter(projectileEmitter);
					break;
				}
			}
		}
	}


	void Space::update() {
		if (networkClient) {
			if (networkClient->IsConnected()) {
				wasConnectedToServer = true;
			}
			else if (wasConnectedToServer && shipAlive && !gameOver) {
				wasConnectedToServer = false;
				shipAlive = false;
				gameOver = true;
				Environment::shipState.velocity = 0.0f;
				std::cout << "Client: Lost connection to server\n";
				clearPlayerListIfVisible();
				menuManager.showConnectionLost();
			}

			auto pending = networkClient->getPendingProjectiles();
			if (!pending.empty()) {
				const float projectileSpeed = PROJECTILE_VELOCITY;
				const float lifeMs = PROJECTILE_LIFE;
				const float size = 0.5f;
				for (const auto& pi : pending) {
					const std::vector<Vector3f> localOffsets = {
						Vector3f{ -1.5f, 0.9f - 6.0f, 5.0f },
						Vector3f{  1.5f, 0.9f - 6.0f, 5.0f }
					};

					Vector3f localForward = { 0, 0, -1 };
					Vector3f worldForward = pi.rotation * localForward;

					float len = worldForward.norm();
					if (len <= 1e-6f) {
						continue;
					}
					worldForward /= len;

					Vector3f baseVel = worldForward * pi.velocity;

					for (const auto& off : localOffsets) {
						Vector3f shotPos = pi.position + (pi.rotation * off);

						for (auto& p : projectiles) {
							if (!p->isActive()) {
								p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer);
								p->projectileEmitter = SparkEmitter(projectileEmitter);
								break;
							}
						}
					}
				}
			}

			// Обработка событий смерти, присланных сервером
			auto deaths = networkClient->getPendingDeaths();
			if (!deaths.empty()) {
				int localId = networkClient->GetClientId();
				std::cout << "Client: Received " << deaths.size() << " death events" << std::endl;

				for (const auto& d : deaths) {
					std::cout << "Client: Processing death - target=" << d.targetId
						<< ", killer=" << d.killerId << ", pos=("
						<< d.position.x() << ", " << d.position.y() << ", " << d.position.z() << ")" << std::endl;

					showExplosion = true;
					explosionEmitter.setUseWorldSpace(true);
					explosionEmitter.setEmissionPoints(std::vector<Vector3f>{ d.position });
					explosionEmitter.emit();
					lastExplosionTime = SDL_GetTicks64();
					std::cout << "Client: Explosion emitted at (" << d.position.x() << ", "
						<< d.position.y() << ", " << d.position.z() << ")" << std::endl;

					if (d.targetId == localId) {
						std::cout << "Client: Local ship destroyed!" << std::endl;
						shipAlive = false;
						gameOver = true;
						Environment::shipState.velocity = 0.0f;
						clearPlayerListIfVisible();
						menuManager.showGameOver(this->playerScore);
					}
					else {
						deadRemotePlayers.insert(d.targetId);
						if (d.targetId == manualTrackedTargetId) manualTrackedTargetId = -1;
						std::cout << "Marked remote player " << d.targetId << " as dead" << std::endl;
					}
					if (d.killerId == localId) {
						this->playerScore += 1;
						std::cout << "Client: Increased local score to " << this->playerScore << std::endl;

					}
				}
				rebuildPlayerListIfVisible();
			}

			auto respawns = networkClient->getPendingRespawns();
			if (!respawns.empty()) {
				for (const auto& respawnId : respawns) {
					deadRemotePlayers.erase(respawnId);

					auto it = remotePlayerStates.find(respawnId);
					if (it != remotePlayerStates.end()) {
						it->second.position = Vector3f{ 0.f, 0.f, 45000.f };
						it->second.velocity = 0.0f;
						it->second.rotation = Eigen::Matrix3f::Identity();
					}

					std::cout << "Client: Remote player " << respawnId << " respawned, removed from dead list" << std::endl;
				}
			}
			rebuildPlayerListIfVisible();

			auto disconnects = networkClient->getPendingDisconnects();
			for (int pid : disconnects) {
				remotePlayerStates.erase(pid);
				deadRemotePlayers.erase(pid);
				remoteShipSparkEmitters.erase(pid);
				if (trackedTargetId == pid) {
					trackedTargetId = -1;
					targetAcquireAnim = 0.f;
				}
				if (pid == manualTrackedTargetId) manualTrackedTargetId = -1;
				std::cout << "Client: Remote player " << pid << " left the game, removed from scene\n";
			}

			rebuildPlayerListIfVisible();
			updateShowPlayersButtonState();
			auto boxDestructions = networkClient->getPendingBoxDestructions();
			if (!boxDestructions.empty()) {
				std::cout << "Game: Received " << boxDestructions.size() << " box destruction events" << std::endl;

				for (const auto& destruction : boxDestructions) {
					int idx = destruction.boxIndex;

					if (idx >= 0 && idx < (int)boxCoordsArr.size()) {
						if (boxAlive[idx]) {
							boxAlive[idx] = false;

							boxRenderArr[idx].data.PositionData.clear();
							boxRenderArr[idx].vao.reset();
							boxRenderArr[idx].positionVBO.reset();
							boxRenderArr[idx].texCoordVBO.reset();

							showExplosion = true;
							explosionEmitter.setUseWorldSpace(true);
							explosionEmitter.setEmissionPoints(std::vector<Vector3f>{ destruction.position });
							explosionEmitter.emit();
							lastExplosionTime = SDL_GetTicks64();

							std::cout << "Game: Box " << idx << " destroyed by player "
								<< destruction.destroyedBy << std::endl;
						}
					}
				}
			}
		}

		auto boxPickups = networkClient->getPendingBoxPickups();
		for (const auto& pickup : boxPickups) {
			int idx = pickup.boxIndex;
			if (idx >= 0 && idx < (int)boxCoordsArr.size() && idx < (int)boxAlive.size()) {
				if (boxAlive[idx]) {
					boxAlive[idx] = false;
					boxRenderArr[idx].data.PositionData.clear();
					boxRenderArr[idx].vao.reset();
					boxRenderArr[idx].positionVBO.reset();
					boxRenderArr[idx].texCoordVBO.reset();
					std::cout << "Client: Box " << idx << " picked up by player " << pickup.pickedUpBy << "\n";
				}
			}
		}


		auto boxRespawns = networkClient->getPendingBoxRespawns();
		for (const auto& respawn : boxRespawns) {
			int idx = respawn.boxIndex;
			if (idx >= 0 && idx < (int)boxCoordsArr.size()) {
				boxCoordsArr[idx].pos = respawn.position;
				boxCoordsArr[idx].m = respawn.rotation;
				boxAlive[idx] = true;
				boxRenderArr[idx].AssignFrom(boxBase);
				boxRenderArr[idx].RefreshVBO();
				std::cout << "Client: Box " << idx << " respawned" << std::endl;
			}
		}
	}

	void Space::handleDown(int mx, int my)
	{
		if (playerListVisible) return;

		Environment::tapDownHold = true;

		if (inverseVertical)
		{
			Environment::tapDownStartPos(0) = mx;
			Environment::tapDownStartPos(1) = my;

			Environment::tapDownCurrentPos(0) = mx;
			Environment::tapDownCurrentPos(1) = my;
		}
		else
		{
			Environment::tapDownStartPos(0) = mx;
			Environment::tapDownStartPos(1) = -my;

			Environment::tapDownCurrentPos(0) = mx;
			Environment::tapDownCurrentPos(1) = -my;
		}
	}

	void Space::handleUp(int mx, int my)
	{
		Environment::tapDownHold = false;
		
	}

	void Space::handleMotion(int mx, int my)
	{
		if (playerListVisible) return;


		if (inverseVertical)
		{
			if (Environment::tapDownHold) {
				Environment::tapDownCurrentPos(0) = mx;
				Environment::tapDownCurrentPos(1) = my;
			}
		}
		else
		{
			if (Environment::tapDownHold) {
				Environment::tapDownCurrentPos(0) = mx;
				Environment::tapDownCurrentPos(1) = -my;
			}
		}
	}

	void Space::clearTextRendererCache()
	{
		if (textRenderer) {
			textRenderer->ClearCache();
		}
	}

	/*
	std::string Space::formPingMessageContent()
	{
		Eigen::Quaternionf q(Environment::shipMatrix);

		std::string pingMsg = std::to_string(Environment::shipPosition.x()) + ":"
			+ std::to_string(Environment::shipPosition.y()) + ":"
			+ std::to_string(Environment::shipPosition.z()) + ":"
			+ std::to_string(q.w()) + ":"
			+ std::to_string(q.x()) + ":"
			+ std::to_string(q.y()) + ":"
			+ std::to_string(q.z()) + ":"
			+ std::to_string(Environment::currentAngularVelocity.x()) + ":"
			+ std::to_string(Environment::currentAngularVelocity.y()) + ":"
			+ std::to_string(Environment::currentAngularVelocity.z()) + ":"
			+ std::to_string(Environment::shipVelocity) + ":"
			+ std::to_string(Environment::shipSelectedVelocity) + ":"
			+ std::to_string(Environment::lastSentMagnitude) + ":" // Используем те же static переменные из блока ROT
			+ std::to_string(Environment::lastSentAngle);

		return pingMsg;
	}*/



	std::shared_ptr<UiNode> Space::buildPlayerListRoot()
	{
		const float btnW = 250;
		const float btnH = 40.0f;

		// Collect alive remote players
		std::vector<std::pair<int, std::string>> players;
		for (auto& kv : remotePlayerStates) {
			if (!deadRemotePlayers.count(kv.first))
				players.push_back({ kv.first, kv.second.nickname });
		}

		// Root: FrameLayout match_parent x match_parent
		auto root = std::make_shared<UiNode>();
		root->name = "playerListRoot";
		root->layoutType = LayoutType::Frame;
		root->width = -1.0f;  // match_parent
		root->height = -1.0f;

		// List container: LinearLayout vertical, centered
		float listH = btnH * (float)(players.size()+1);
		auto listNode = std::make_shared<UiNode>();
		listNode->name = "playerList";
		listNode->layoutType = LayoutType::Linear;
		listNode->orientation = Orientation::Vertical;
		listNode->width = btnW;
		listNode->height = listH;
		listNode->layoutSettings.hGravity = HorizontalGravity::Center;
		listNode->layoutSettings.vGravity = VerticalGravity::Center;

		auto titleNode = std::make_shared<UiNode>();
		titleNode->name = "player_list_title";
		titleNode->layoutType = LayoutType::Frame;
		titleNode->width = btnW;
		titleNode->height = btnH;
		auto titleImage = std::make_shared<UiButton>();
		titleImage->name = "player_list_title";
		titleImage->texNormal = std::make_unique<Texture>(CreateTextureDataFromPng("resources/players_list_title.png", CONST_ZIP_FILE));
		titleImage->texPressed = titleImage->texNormal;
		titleImage->texHover = titleImage->texNormal;
		titleNode->button = titleImage;

		listNode->children.push_back(titleNode);

		for (auto& [pid, nick] : players) {
			auto btnNode = std::make_shared<UiNode>();
			btnNode->name = "playerBtn_" + std::to_string(pid);
			btnNode->layoutType = LayoutType::Frame;
			btnNode->width = btnW;
			btnNode->height = btnH;

			
			auto tb = std::make_shared<UiTextButton>();
			tb->name = btnNode->name;
			tb->text = nick;
			tb->fontSize = 20;
			tb->color = { 1.f, 1.f, 1.f, 1.f };
			tb->textCentered = true;
			tb->textRenderer = std::make_unique<TextRenderer>();
			if (!tb->textRenderer->init(renderer, tb->fontPath, tb->fontSize, CONST_ZIP_FILE)) {
				std::cerr << "Failed to init TextRenderer for TextField: " << tb->name << std::endl;
			}
			tb->texNormal = std::make_unique<Texture>(CreateTextureDataFromPng("resources/player_under.png", CONST_ZIP_FILE));

			btnNode->textButton = tb;
			/*auto button = std::make_shared<UiButton>();
			button->name = "Hello";
			button->texNormal = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", ""));
			btnNode->button = button;*/
			listNode->children.push_back(btnNode);
		}

		// Backdrop: invisible full-screen TextButton — placed LAST so player buttons get priority
		auto backdropNode = std::make_shared<UiNode>();
		backdropNode->name = "playerListBackdrop";
		backdropNode->layoutType = LayoutType::Frame;
		backdropNode->width = -1.0f;
		backdropNode->height = -1.0f;
		auto backdropTb = std::make_shared<UiTextButton>();
		backdropTb->name = "playerListBackdrop";
		backdropNode->textButton = backdropTb;


		/*
		auto backgroundNode = std::make_shared<UiNode>();
		backgroundNode->name = "playerListBackground";
		backgroundNode->layoutType = LayoutType::Frame;
		backgroundNode->width = btnW;
		backgroundNode->height = listH;
		backgroundNode->layoutSettings.hGravity = HorizontalGravity::Center;
		backgroundNode->layoutSettings.vGravity = VerticalGravity::Center;
		auto backdropImage = std::make_shared<UiStaticImage>();
		backdropImage->name = "playerListBackgroundImage";
		backdropImage->texture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/blue_transparent.png", ""));
		backgroundNode->staticImage = backdropImage;
		*/
		root->children.push_back(listNode);
		root->children.push_back(backdropNode);
		//root->children.push_back(backgroundNode);
		return root;
	}

	void Space::buildAndShowPlayerList()
	{
		auto listRoot = buildPlayerListRoot();
		menuManager.uiManager.pushMenuFromSavedRoot(listRoot);
		menuManager.uiManager.updateAllLayouts();
		playerListVisible = true;

		for (auto& kv : remotePlayerStates) {
			if (deadRemotePlayers.count(kv.first)) continue;
			int pid = kv.first;
			std::string btnName = "playerBtn_" + std::to_string(pid);
			menuManager.uiManager.setTextButtonCallback(btnName, [this, pid](const std::string&) {
				manualTrackedTargetId = pid;
				closePlayerList();
			});
		}

		menuManager.uiManager.setTextButtonCallback("playerListBackdrop", [this](const std::string&) {
			closePlayerList();
		});
		menuManager.uiManager.setTextButtonCallback("player_list_title", [this](const std::string&) {
			closePlayerList();
			});
	}

	void Space::closePlayerList()
	{
		menuManager.uiManager.popMenu();
		menuManager.uiManager.updateAllLayouts();
		playerListVisible = false;
	}

	void Space::rebuildPlayerListIfVisible()
	{
		if (!playerListVisible) return;

		auto listRoot = buildPlayerListRoot();
		menuManager.uiManager.replaceRoot(listRoot);

		for (auto& kv : remotePlayerStates) {
			if (deadRemotePlayers.count(kv.first)) continue;
			int pid = kv.first;
			std::string btnName = "playerBtn_" + std::to_string(pid);
			menuManager.uiManager.setTextButtonCallback(btnName, [this, pid](const std::string&) {
				manualTrackedTargetId = pid;
				closePlayerList();
			});
		}

		menuManager.uiManager.setTextButtonCallback("playerListBackdrop", [this](const std::string&) {
			closePlayerList();
		});
	}

	void Space::clearPlayerListIfVisible()
	{
		if (!playerListVisible) return;
		menuManager.uiManager.clearMenuStack();
		playerListVisible = false;
	}

}  // namespace ZL
