From 5b57696acfab2aacf7affd316e0fb620c2529f55 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Sun, 22 Feb 2026 19:15:25 +0300 Subject: [PATCH] Refactoring major --- src/Game.cpp | 1560 +------------------------------------------------ src/Game.h | 88 +-- src/Space.cpp | 1482 ++++++++++++++++++++++++++++++++++++++++++++++ src/Space.h | 31 +- 4 files changed, 1508 insertions(+), 1653 deletions(-) diff --git a/src/Game.cpp b/src/Game.cpp index 5600e8b..743dc93 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -40,220 +40,14 @@ namespace ZL float x = 0; - 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 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 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 Game::worldToScreen(const Vector3f& world, float& outX, float& outY, float& outDepth) const - { - // Матрицы должны совпасть с drawBoxes/drawShip по смыслу - float aspect = static_cast(Environment::width) / static_cast(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::width; - float sy = (ndc.y() * 0.5f + 0.5f) * Environment::height; - - outX = sx; - outY = sy; - - // Можно отсеять те, что вне: - if (sx < -200 || sx > Environment::width + 200) return false; - if (sy < -200 || sy > Environment::height + 200) return false; - - return true; - } - - bool Game::projectToNDC(const Vector3f& world, float& ndcX, float& ndcY, float& ndcZ, float& clipW) const - { - float aspect = static_cast(Environment::width) / static_cast(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 Game::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); - } - Game::Game() : window(nullptr) , glContext(nullptr) , newTickCount(0) , lastTickCount(0) - , planetObject(renderer, taskManager, mainThreadHandler) , menuManager(renderer) + , space(renderer, taskManager, mainThreadHandler, networkClient, menuManager) { - projectiles.reserve(maxProjectiles); - for (int i = 0; i < maxProjectiles; ++i) { - projectiles.emplace_back(std::make_unique()); - } } Game::~Game() { @@ -272,9 +66,6 @@ namespace ZL ZL::BindOpenGlFunctions(); ZL::CheckGlError(); - //#ifndef SIMPLIFIED - - #ifdef EMSCRIPTEN renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE); @@ -295,135 +86,10 @@ namespace ZL renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/planet_land.vertex", "resources/shaders/planet_land_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("spark", "resources/shaders/spark.vertex", "resources/shaders/spark_desktop.fragment", CONST_ZIP_FILE); #endif - /*#else - renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE); - renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/default_env.vertex", "resources/shaders/default_env_web.fragment", CONST_ZIP_FILE); - renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE); - renderer.shaderManager.AddShaderFromFiles("planetBake", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE); - renderer.shaderManager.AddShaderFromFiles("planetStone", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE); - renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE); - #endif*/ - bool cfgLoaded = sparkEmitter.loadFromJsonFile("resources/config/spark_config.json", renderer, CONST_ZIP_FILE); - 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()); - projectileEmitter.setEmissionPoints(std::vector()); - - menuManager.onRestartPressed = [this]() { - this->shipAlive = true; - this->gameOver = false; - this->showExplosion = false; - this->explosionEmitter.setEmissionPoints(std::vector()); - Environment::shipState.position = Vector3f{ 0, 0, 45000.f }; - Environment::shipState.velocity = 0.0f; - Environment::shipState.rotation = Eigen::Matrix3f::Identity(); - Environment::inverseShipMatrix = Eigen::Matrix3f::Identity(); - Environment::zoom = DEFAULT_ZOOM; - Environment::tapDownHold = false; - - std::cerr << "Game restarted\n"; - }; - - menuManager.onVelocityChanged = [this](float newVelocity) { - newShipVelocity = newVelocity; - //Environment::shipState.velocity = newVelocity; - //std::cerr << "Ship velocity changed: " << newVelocity << "\n"; - }; menuManager.setupMenu(); - - - - cubemapTexture = std::make_shared( - std::array{ - CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), - CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), - CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), - CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), - CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), - CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE) - }); - - - cubemap.data = ZL::CreateCubemap(500); - cubemap.RefreshVBO(); - - - - //Load texture - - //spaceshipTexture = std::make_unique(CreateTextureDataFromPng("resources/DefaultMaterial_BaseColor_shine.png", CONST_ZIP_FILE)); - //spaceshipBase = LoadFromTextFile02("resources/spaceship006.txt", CONST_ZIP_FILE); - //spaceshipBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI / 2.0, Eigen::Vector3f::UnitY())).toRotationMatrix());// QuatFromRotateAroundY(M_PI / 2.0).toRotationMatrix()); - - //spaceshipTexture = std::make_unique(CreateTextureDataFromPng("./resources/cap_D.png", CONST_ZIP_FILE)); - //spaceshipBase = LoadFromTextFile02("./resources/spaceship006x.txt", CONST_ZIP_FILE); - //spaceshipBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI / 2.0, Eigen::Vector3f::UnitY())).toRotationMatrix());// QuatFromRotateAroundY(M_PI / 2.0).toRotationMatrix()); - - - spaceshipTexture = std::make_unique(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 }); - - /* - spaceshipTexture = std::make_unique(CreateTextureDataFromPng("resources/Cargo_Base_color_sRGB.png", CONST_ZIP_FILE)); - spaceshipBase = LoadFromTextFile02("resources/cargoship001.txt", CONST_ZIP_FILE); - - auto quat = Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI*0.5, Eigen::Vector3f::UnitZ())); - auto rotMatrix = quat.toRotationMatrix(); - spaceshipBase.RotateByMatrix(rotMatrix); - - auto quat2 = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI*0.5, Eigen::Vector3f::UnitY())); - auto rotMatrix2 = quat2.toRotationMatrix(); - spaceshipBase.RotateByMatrix(rotMatrix2); - - //spaceshipBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())).toRotationMatrix());// QuatFromRotateAroundY(M_PI / 2.0).toRotationMatrix()); - */ - spaceship.AssignFrom(spaceshipBase); - spaceship.RefreshVBO(); - - - //Boxes - boxTexture = std::make_unique(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].data = CreateBaseConvexPolyhedron(1999); - boxRenderArr[i].RefreshVBO(); - } - - boxAlive.resize(boxCoordsArr.size(), true); - 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 + 1)); - } - - if (!cfgLoaded) - { - throw std::runtime_error("Failed to load spark emitter config file!"); - } - renderer.InitOpenGL(); - - // TextRenderer создаём/инициализируем ПОСЛЕ инициализации OpenGL - textRenderer = std::make_unique(); - 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(); + space.setup(); #ifdef NETWORK #ifdef EMSCRIPTEN @@ -440,179 +106,6 @@ namespace ZL } - void Game::drawCubemap(float skyPercent) - { - static const std::string defaultShaderName = "default"; - static const std::string envShaderName = "env_sky"; - static const std::string vPositionName = "vPosition"; - static const std::string vTexCoordName = "vTexCoord"; - static const std::string textureUniformName = "Texture"; - static const std::string skyPercentUniformName = "skyPercent"; - - renderer.shaderManager.PushShader(envShaderName); - renderer.RenderUniform1i(textureUniformName, 0); - renderer.RenderUniform1f(skyPercentUniformName, skyPercent); - renderer.EnableVertexAttribArray(vPositionName); - renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, - static_cast(Environment::width) / static_cast(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.DisableVertexAttribArray(vPositionName); - - renderer.shaderManager.PopShader(); - CheckGlError(); - } - - void Game::drawShip() - { - static const std::string defaultShaderName = "default"; - static const std::string envShaderName = "env"; - static const std::string vPositionName = "vPosition"; - static const std::string vTexCoordName = "vTexCoord"; - static const std::string textureUniformName = "Texture"; - - renderer.shaderManager.PushShader(defaultShaderName); - renderer.RenderUniform1i(textureUniformName, 0); - renderer.EnableVertexAttribArray(vPositionName); - renderer.EnableVertexAttribArray(vTexCoordName); - - renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, - static_cast(Environment::width) / static_cast(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 - - if (shipAlive) { - glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID()); - renderer.DrawVertexRenderStruct(spaceship); - } - renderer.PopMatrix(); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - for (const auto& p : projectiles) { - if (p && p->isActive()) { - p->draw(renderer); - } - } - - projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); - - if (shipAlive) { - renderer.PushMatrix(); - renderer.TranslateMatrix({ 0, 0, 16 }); - renderer.TranslateMatrix({ 0, -6.f, 0 }); - sparkEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); - renderer.PopMatrix(); - } - - if (showExplosion) { - explosionEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); - } - - //glBindTexture(GL_TEXTURE_2D, basePlatformTexture->getTexID()); - //renderer.DrawVertexRenderStruct(basePlatform); - - glDisable(GL_BLEND); - renderer.PopMatrix(); - renderer.PopProjectionMatrix(); - renderer.DisableVertexAttribArray(vPositionName); - renderer.DisableVertexAttribArray(vTexCoordName); - - renderer.shaderManager.PopShader(); - CheckGlError(); - } - - void Game::drawBoxes() - { - static const std::string defaultShaderName = "default"; - static const std::string envShaderName = "env"; - static const std::string vPositionName = "vPosition"; - static const std::string vTexCoordName = "vTexCoord"; - static const std::string textureUniformName = "Texture"; - - renderer.shaderManager.PushShader(defaultShaderName); - renderer.RenderUniform1i(textureUniformName, 0); - renderer.EnableVertexAttribArray(vPositionName); - renderer.EnableVertexAttribArray(vTexCoordName); - - renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, - static_cast(Environment::width) / static_cast(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()); - //glBindTexture(GL_TEXTURE_2D, rockTexture->getTexID()); - renderer.DrawVertexRenderStruct(boxRenderArr[i]); - - renderer.PopMatrix(); - } - renderer.PopProjectionMatrix(); - renderer.DisableVertexAttribArray(vPositionName); - renderer.DisableVertexAttribArray(vTexCoordName); - - renderer.shaderManager.PopShader(); - CheckGlError(); - } void Game::drawUI() { @@ -639,469 +132,22 @@ namespace ZL } void Game::drawScene() { - static const std::string defaultShaderName = "default"; - static const std::string envShaderName = "env"; - static const std::string vPositionName = "vPosition"; - static const std::string vTexCoordName = "vTexCoord"; - static const std::string textureUniformName = "Texture"; - - 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); - - 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(); + space.drawScene(); drawUI(); - drawTargetHud(); CheckGlError(); } - void Game::drawRemoteShips() { - // Используем те же константы имен для шейдеров, что и в drawShip - static const std::string defaultShaderName = "default"; - static const std::string vPositionName = "vPosition"; - static const std::string vTexCoordName = "vTexCoord"; - static const std::string textureUniformName = "Texture"; - - // Активируем шейдер и текстуру (предполагаем, что меш у всех одинаковый) - renderer.shaderManager.PushShader(defaultShaderName); - renderer.RenderUniform1i(textureUniformName, 0); - - renderer.EnableVertexAttribArray(vPositionName); - renderer.EnableVertexAttribArray(vTexCoordName); - - renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, - static_cast(Environment::width) / static_cast(Environment::height), - Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); - - // Биндим текстуру корабля один раз для всех удаленных игроков (оптимизация батчинга) - glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID()); - - /*auto now = std::chrono::system_clock::now(); - - //Apply server delay: - now -= std::chrono::milliseconds(CLIENT_DELAY); - - latestRemotePlayers = networkClient->getRemotePlayers(); - */ - // Если сервер прислал коробки, применяем их однократно вместо локальной генерации - if (!serverBoxesApplied && networkClient) { - auto sboxes = networkClient->getServerBoxes(); - if (!sboxes.empty()) { - boxCoordsArr.clear(); - for (auto& b : sboxes) { - BoxCoords bc; - bc.pos = b.first; - bc.m = b.second; - boxCoordsArr.push_back(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); - serverBoxesApplied = true; - } - } - - // Итерируемся по актуальным данным из extrapolateRemotePlayers - 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.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset - renderer.RotateMatrix(Environment::inverseShipMatrix); - renderer.TranslateMatrix(-Environment::shipState.position); - - - Eigen::Vector3f relativePos = playerState.position;// -Environment::shipPosition; - renderer.TranslateMatrix(relativePos); - - // 3. Поворот врага - renderer.RotateMatrix(playerState.rotation); - - renderer.DrawVertexRenderStruct(spaceship); - renderer.PopMatrix(); - } - - renderer.PopProjectionMatrix(); - renderer.DisableVertexAttribArray(vPositionName); - renderer.DisableVertexAttribArray(vTexCoordName); - renderer.shaderManager.PopShader(); - - CheckGlError(); - } - - void Game::drawRemoteShipsLabels() - { - if (!textRenderer) return; - -//#ifdef NETWORK - // 2D поверх 3D - glDisable(GL_DEPTH_TEST); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - // Берем удаленных игроков - //latestRemotePlayers = networkClient->getRemotePlayers(); - - //auto now = std::chrono::system_clock::now(); - //now -= std::chrono::milliseconds(CLIENT_DELAY); - - for (auto const& [id, remotePlayer] : remotePlayerStates) - { - if (deadRemotePlayers.count(id)) continue; - - //<<<<<<< HEAD - const ClientState& st = remotePlayer; - // Позиция корабля в мире - Vector3f shipWorld = st.position; - - float distSq = (Environment::shipState.position - shipWorld).squaredNorm(); - /*if (distSq > MAX_DIST_SQ) // дальность прорисовки никнейма - continue;*/ - float dist = sqrt(distSq); - float alpha = 1.0f; // постоянная видимость - /*float alpha = std::clamp(1.f - (dist - FADE_START) / FADE_RANGE, 0.f, 1.f); // дальность прорисовки никнейма - if (alpha < 0.01f) - continue; */ - 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 label = "Player (" + std::to_string(st.id) + ") " + std::to_string((int)dist) + "m"; - - // TODO: nickname sync - - textRenderer->drawText(label, uiX + 1.f, uiY + 1.f, scale, true, { 0.f, 0.f, 0.f, alpha }); // color param - textRenderer->drawText(label, uiX, uiY, scale, true, { 1.f, 1.f, 1.f, alpha }); - } - - glDisable(GL_BLEND); - glEnable(GL_DEPTH_TEST); -//#endif - } - int64_t Game::getSyncTimeMs() { int64_t localNow = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); - - //std::cout << "getSyncTimeMs localNow = " << localNow << std::endl; - //std::cout << "getSyncTimeMs getTimeOffset = " << networkClient->getTimeOffset() << std::endl; - // Добавляем смещение, полученное от сервера return localNow + networkClient->getTimeOffset(); // Нужно добавить геттер в интерфейс } - int Game::pickTargetId() const - { - int bestId = -1; - constexpr float INF_F = 1e30f; - float bestDistSq = INF_F; - - for (auto const& [id, st] : remotePlayerStates) { - if (deadRemotePlayers.count(id)) continue; - - float d2 = (Environment::shipState.position - st.position).squaredNorm(); - if (d2 < bestDistSq) { - bestDistSq = d2; - bestId = id; - } - } - return bestId; - } - - 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 uColor, если есть. - // Если в defaultColor нет uniform uColor — тогда alpha будет 1.0. - // Для совместимости: кладём RGB, alpha будем задавать uniform'ом отдельно. - Vector3f rgb{ rgba.x(), rgba.y(), rgba.z() }; - v.ColorData = { rgb, rgb, rgb, rgb, rgb, rgb }; - return v; - } - - void Game::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; - - // 2) проекция - float ndcX, ndcY, ndcZ, clipW; - if (!projectToNDC(shipWorld, ndcX, ndcY, ndcZ, clipW)) return; - - // behind camera? - bool behind = (clipW <= 0.0f); - - // on-screen check (NDC) - bool onScreen = (!behind && - ndcX >= -1.0f && ndcX <= 1.0f && - ndcY >= -1.0f && ndcY <= 1.0f); - - // 3) расстояние - float dist = (Environment::shipState.position - shipWorld).norm(); - - // time for arrow bob - float t = static_cast(SDL_GetTicks64()) * 0.001f; - - // 4) Настройки стиля (как X3) - Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный - float thickness = 2.0f; // толщина линий (px) - float z = 0.0f; // 2D слой - - // 5) Если цель в кадре: рисуем скобки - if (onScreen) - { - // перевод NDC -> экран (в пикселях) - float sx = (ndcX * 0.5f + 0.5f) * Environment::width; - float sy = (ndcY * 0.5f + 0.5f) * Environment::height; - - // анимация “снаружи внутрь” - // 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); - - renderer.shaderManager.PushShader("defaultColor"); - renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f); - renderer.PushMatrix(); - renderer.LoadIdentity(); - - // верх-лево: горизонт + вертикаль - 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); - - renderer.PopMatrix(); - renderer.PopProjectionMatrix(); - renderer.shaderManager.PopShader(); - - glDisable(GL_BLEND); - glEnable(GL_DEPTH_TEST); - - targetWasVisible = true; - return; - } - - // 6) Если цель offscreen: рисуем стрелку на краю - // dir: куда “смотреть” в NDC - 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; - - // пересечение луча с прямоугольником [-1..1] с отступом - 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::width; - float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::height; - - // лёгкая анимация “зова”: смещение по направлению - 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 }; - 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((float)Environment::width, (float)Environment::height, 0.f, 1.f); - renderer.PushMatrix(); - renderer.LoadIdentity(); - - // треугольник-стрелка - 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); - - renderer.PopMatrix(); - renderer.PopProjectionMatrix(); - renderer.shaderManager.PopShader(); - - // дистанция рядом со стрелкой - // (у тебя ещё будет “статично под прицелом” — это просто другой TextView / drawText) - { - 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 Game::processTickCount() { if (lastTickCount == 0) { - //lastTickCount = SDL_GetTicks64(); lastTickCount = getSyncTimeMs(); lastTickCount = (lastTickCount / 50) * 50; @@ -1109,7 +155,6 @@ namespace ZL return; } - //newTickCount = SDL_GetTicks64(); newTickCount = getSyncTimeMs(); newTickCount = (newTickCount / 50) * 50; @@ -1122,380 +167,13 @@ namespace ZL //throw std::runtime_error("Synchronization is lost"); } - auto now_ms = newTickCount; - - //std::cout << "processTickCount = " << now_ms << std::endl; - - sparkEmitter.update(static_cast(delta)); - planetObject.update(static_cast(delta)); - - if (firePressed) - { - firePressed = false; - if (now_ms - lastProjectileFireTime >= static_cast(projectileCooldownMs)) { - lastProjectileFireTime = now_ms; - const float projectileSpeed = 250.0f; - - 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, 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(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 (!remotePlayer.canFetchClientStateAtTime(nowRoundedWithDelay)) - { - continue; - } - - ClientState playerState = remotePlayer.fetchClientStateAtTime(nowRoundedWithDelay); - - remotePlayerStates[id] = playerState; - - } - - for (auto& p : projectiles) { - if (p && p->isActive()) { - p->update(static_cast(delta), renderer); - } - } - - std::vector projCameraPoints; - for (const auto& p : projectiles) { - if (p && p->isActive()) { - Vector3f worldPos = p->getPosition(); - Vector3f rel = worldPos - Environment::shipState.position; - Vector3f camPos = Environment::inverseShipMatrix * rel; - projCameraPoints.push_back(camPos); - } - } - if (!projCameraPoints.empty()) { - projectileEmitter.setEmissionPoints(projCameraPoints); - projectileEmitter.emit(); - } - else { - projectileEmitter.setEmissionPoints(std::vector()); - } - - std::vector shipCameraPoints; - for (const auto& lp : shipLocalEmissionPoints) { - Vector3f adjusted = lp + Vector3f{ 0.0f, -Environment::zoom * 0.03f, 0.0f }; - shipCameraPoints.push_back(adjusted); - } - if (!shipCameraPoints.empty()) { - sparkEmitter.setEmissionPoints(shipCameraPoints); - } - - sparkEmitter.update(static_cast(delta)); - projectileEmitter.update(static_cast(delta)); - - explosionEmitter.update(static_cast(delta)); - if (showExplosion) { - uint64_t now = SDL_GetTicks64(); - if (lastExplosionTime != 0 && now - lastExplosionTime >= explosionDurationMs) { - showExplosion = false; - explosionEmitter.setEmissionPoints(std::vector()); - 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.velocity = 0.0f; - showExplosion = true; - - explosionEmitter.setUseWorldSpace(true); - explosionEmitter.setEmissionPoints(std::vector{ collisionPoint }); - explosionEmitter.emit(); - lastExplosionTime = SDL_GetTicks64(); - - std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n"; - - menuManager.showGameOver(); - } - 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(planetObject.planetStones.allInstances.size())) - continue; - - if (planetObject.planetStones.statuses.size() <= static_cast(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.velocity = 0.0f; - showExplosion = true; - - explosionEmitter.setUseWorldSpace(true); - explosionEmitter.setEmissionPoints(std::vector{ collidedStonePos }); - explosionEmitter.emit(); - lastExplosionTime = SDL_GetTicks64(); - - std::cerr << "GAME OVER: collision with stone on triangle " << collidedTriIdx << std::endl; - - if (collidedTriIdx >= 0 && collidedTriIdx < static_cast(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(planetObject.planetStones.statuses.size())) { - planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty; - } - - menuManager.showGameOver(); - } - } - } - - /*for (int i = 0; i < boxCoordsArr.size(); ++i) { - if (!boxAlive[i]) continue; - Vector3f boxWorld = boxCoordsArr[i].pos + Vector3f{ 0.0f, 0.0f, 45000.0f }; - Vector3f diff = Environment::shipState.position - boxWorld; - float thresh = shipCollisionRadius + boxCollisionRadius; - if (diff.squaredNorm() <= thresh * thresh) { - boxAlive[i] = false; - - boxRenderArr[i].data.PositionData.clear(); - boxRenderArr[i].vao.reset(); - boxRenderArr[i].positionVBO.reset(); - boxRenderArr[i].texCoordVBO.reset(); - showExplosion = true; - - Vector3f rel = boxWorld - Environment::shipState.position; - Vector3f camPos = Environment::inverseShipMatrix * rel; - explosionEmitter.setUseWorldSpace(true); - explosionEmitter.setEmissionPoints(std::vector{ boxWorld }); - explosionEmitter.emit(); - lastExplosionTime = SDL_GetTicks64(); - - std::cerr << "Box destroyed at index " << i << std::endl; - } - } - - const float projectileHitRadius = 1.5f; - for (auto& p : projectiles) { - if (!p || !p->isActive()) continue; - Vector3f ppos = p->getPosition(); - Vector3f projInBoxSpace = Environment::inverseShipMatrix * (ppos - Environment::shipState.position); - for (int i = 0; i < boxCoordsArr.size(); ++i) { - if (!boxAlive[i]) continue; - Vector3f boxWorld = boxCoordsArr[i].pos + Vector3f{ 0.0f, 6.0f, 45000.0f }; - Vector3f dd = ppos - boxWorld; - float thresh = boxCollisionRadius + projectileHitRadius; - if (dd.squaredNorm() <= thresh * thresh) { - boxAlive[i] = false; - boxRenderArr[i].data.PositionData.clear(); - boxRenderArr[i].vao.reset(); - boxRenderArr[i].positionVBO.reset(); - boxRenderArr[i].texCoordVBO.reset(); - - showExplosion = true; - explosionEmitter.setUseWorldSpace(true); - explosionEmitter.setEmissionPoints(std::vector{ boxWorld }); - explosionEmitter.emit(); - lastExplosionTime = SDL_GetTicks64(); - - p->deactivate(); - std::cerr << "Box destroyed by projectile at index " << i << std::endl; - break; - } - } - }*/ - - // update velocity text - if (shipAlive && !gameOver) { - auto velocityTv = menuManager.uiManager.findTextView("velocityText"); - if (velocityTv) { - std::string velocityStr = "Velocity: " + std::to_string(static_cast(Environment::shipState.velocity)); - menuManager.uiManager.setText("velocityText", velocityStr); - } - } + space.processTickCount(newTickCount, delta); menuManager.uiManager.update(static_cast(delta)); lastTickCount = newTickCount; } } - void Game::fireProjectiles() { - std::vector localOffsets = { - Vector3f{ -1.5f, 0.9f - 6.f, 5.0f }, - Vector3f{ 1.5f, 0.9f - 6.f, 5.0f } - }; - - const float projectileSpeed = 60.0f; - const float lifeMs = 5000.0f; - 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); - break; - } - } - } - } - void Game::render() { SDL_GL_MakeCurrent(ZL::Environment::window, glContext); ZL::CheckGlError(); @@ -1520,7 +198,7 @@ namespace ZL // Обновляем размеры и сбрасываем кеш текстов, т.к. меши хранятся в пикселях Environment::width = event.window.data1; Environment::height = event.window.data2; - if (textRenderer) textRenderer->ClearCache(); + space.clearTextRendererCache(); } #endif #ifdef __ANDROID__ @@ -1615,243 +293,23 @@ namespace ZL } mainThreadHandler.processMainThreadTasks(); - if (networkClient) { - auto pending = networkClient->getPendingProjectiles(); - if (!pending.empty()) { - const float projectileSpeed = 60.0f; - const float lifeMs = 5000.0f; - const float size = 0.5f; - for (const auto& pi : pending) { - const std::vector localOffsets = { - Vector3f{ -1.5f, 0.9f, 5.0f }, - Vector3f{ 1.5f, 0.9f, 5.0f } - //Vector3f{}, - //Vector3f{} - }; - - 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); - break; - } - } - } - } - /* - auto remotePlayersSnapshot = networkClient->getRemotePlayers(); - for (const auto& pi : pending) { - Eigen::Vector3f dir = pi.direction; - float len = dir.norm(); - if (len <= 1e-6f) continue; - dir /= len; - - Eigen::Matrix3f shooterRot = Eigen::Matrix3f::Identity(); - float shooterVel = 0.0f; - auto it = remotePlayersSnapshot.find(pi.shooterId); - if (it != remotePlayersSnapshot.end()) { - std::chrono::system_clock::time_point pktTime{ std::chrono::milliseconds(pi.clientTime) }; - if (it->second.canFetchClientStateAtTime(pktTime)) { - ClientState shooterState = it->second.fetchClientStateAtTime(pktTime); - shooterRot = shooterState.rotation; - shooterVel = shooterState.velocity; - } - } - - float speedWithOwner = projectileSpeed + shooterVel; - Eigen::Vector3f baseVel = dir * speedWithOwner; - - int shotCount = 2; - - std::vector localOffsets = { - {-1.5f, 0.9f, 5.0f}, - { 1.5f, 0.9f, 5.0f} - }; - - for (int i = 0; i < shotCount; ++i) { - Eigen::Vector3f rotatedOffset = shooterRot * localOffsets[i]; - Eigen::Vector3f shotPos = pi.position + rotatedOffset; - - for (auto& p : projectiles) { - if (!p->isActive()) { - p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer); - 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{ 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; - menuManager.showGameOver(); - } - else { - deadRemotePlayers.insert(d.targetId); - std::cout << "Marked remote player " << d.targetId << " as dead" << std::endl; - } - } - } - - 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; - } - } - - 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{ destruction.position }); - explosionEmitter.emit(); - lastExplosionTime = SDL_GetTicks64(); - - std::cout << "Game: Box " << idx << " destroyed by player " - << destruction.destroyedBy << std::endl; - } - } - } - } - } + space.update(); } void Game::handleDown(int mx, int my) { - int uiX = mx; - int uiY = Environment::height - my; - - menuManager.uiManager.onMouseDown(uiX, uiY); - - bool uiHandled = false; - - for (const auto& button : menuManager.uiManager.findButton("") ? std::vector>{} : std::vector>{}) { - (void)button; - } - - auto pressedSlider = [&]() -> std::shared_ptr { - for (const auto& slider : menuManager.uiManager.findSlider("") ? std::vector>{} : std::vector>{}) { - (void)slider; - } - return nullptr; - }(); - - if (!menuManager.uiManager.isUiInteraction()) { - Environment::tapDownHold = true; - - Environment::tapDownStartPos(0) = mx; - Environment::tapDownStartPos(1) = my; - - Environment::tapDownCurrentPos(0) = mx; - Environment::tapDownCurrentPos(1) = my; - } + space.handleDown(mx, my); } void Game::handleUp(int mx, int my) { - int uiX = mx; - int uiY = Environment::height - my; - - menuManager.uiManager.onMouseUp(uiX, uiY); - - if (!menuManager.uiManager.isUiInteraction()) { - Environment::tapDownHold = false; - } + space.handleUp(mx, my); } void Game::handleMotion(int mx, int my) { - int uiX = mx; - int uiY = Environment::height - my; - - menuManager.uiManager.onMouseMove(uiX, uiY); - - if (Environment::tapDownHold && !menuManager.uiManager.isUiInteraction()) { - Environment::tapDownCurrentPos(0) = mx; - Environment::tapDownCurrentPos(1) = my; - } + space.handleMotion(mx, my); } - /* - std::string Game::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; - }*/ - } // namespace ZL diff --git a/src/Game.h b/src/Game.h index 84e3701..057eb17 100644 --- a/src/Game.h +++ b/src/Game.h @@ -35,113 +35,27 @@ namespace ZL { Renderer renderer; TaskManager taskManager; MainThreadHandler mainThreadHandler; - std::unique_ptr networkClient; - - private: int64_t getSyncTimeMs(); void processTickCount(); void drawScene(); - void drawCubemap(float skyPercent); - void drawShip(); - void drawBoxes(); - void drawBoxesLabels(); void drawUI(); - void drawRemoteShips(); - void drawRemoteShipsLabels(); - void fireProjectiles(); - - bool worldToScreen(const Vector3f& world, float& outX, float& outY, float& outDepth) const; - void handleDown(int mx, int my); void handleUp(int mx, int my); void handleMotion(int mx, int my); SDL_Window* window; SDL_GLContext glContext; - - int64_t newTickCount; int64_t lastTickCount; - std::vector boxCoordsArr; - std::vector boxRenderArr; - - std::vector boxLabels; - std::unique_ptr textRenderer; - - //std::unordered_map latestRemotePlayers; - std::unordered_map remotePlayerStates; - - float newShipVelocity = 0; - static const size_t CONST_TIMER_INTERVAL = 10; static const size_t CONST_MAX_TIME_INTERVAL = 1000; - std::shared_ptr sparkTexture; - std::shared_ptr spaceshipTexture; - std::shared_ptr cubemapTexture; - VertexDataStruct spaceshipBase; - VertexRenderStruct spaceship; - - - VertexRenderStruct cubemap; - - std::shared_ptr boxTexture; - VertexDataStruct boxBase; - - SparkEmitter sparkEmitter; - SparkEmitter projectileEmitter; - SparkEmitter explosionEmitter; - PlanetObject planetObject; - MenuManager menuManager; - - std::vector> projectiles; - std::shared_ptr projectileTexture; - float projectileCooldownMs = 500.0f; - int64_t lastProjectileFireTime = 0; - int maxProjectiles = 32; - std::vector shipLocalEmissionPoints; - - - bool shipAlive = true; - bool gameOver = false; - std::vector boxAlive; - float shipCollisionRadius = 15.0f; - float boxCollisionRadius = 2.0f; - //bool uiGameOverShown = false; - bool showExplosion = false; - uint64_t lastExplosionTime = 0; - const uint64_t explosionDurationMs = 500; - - bool serverBoxesApplied = false; - - static constexpr float MAX_DIST_SQ = 10000.f * 10000.f; - static constexpr float FADE_START = 6000.f; - static constexpr float FADE_RANGE = 4000.f; - static constexpr float BASE_SCALE = 140.f; - static constexpr float PERSPECTIVE_K = 0.05f; // Tune - static constexpr float MIN_SCALE = 0.4f; - static constexpr float MAX_SCALE = 0.8f; - static constexpr float CLOSE_DIST = 600.0f; - - std::unordered_set deadRemotePlayers; - - // --- Target HUD (brackets + offscreen arrow) --- - int trackedTargetId = -1; - bool targetWasVisible = false; - float targetAcquireAnim = 0.0f; // 0..1 схлопывание (0 = далеко, 1 = на месте) - - // временный меш для HUD (будем перезаливать VBO маленькими порциями) - VertexRenderStruct hudTempMesh; - - // helpers - bool projectToNDC(const Vector3f& world, float& ndcX, float& ndcY, float& ndcZ, float& clipW) const; - void drawTargetHud(); // рисует рамку или стрелку - int pickTargetId() const; // выбирает цель (пока: ближайший живой удаленный игрок) + Space space; }; diff --git a/src/Space.cpp b/src/Space.cpp index e69de29..073b1d5 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -0,0 +1,1482 @@ +#include "Space.h" +#include "AnimatedModel.h" +#include "BoneAnimatedModel.h" +#include "planet/PlanetData.h" +#include "utils/Utils.h" +#include "render/OpenGlExtensions.h" +#include +#include "render/TextureManager.h" +#include "TextModel.h" +#include +#include +#include +#include +#ifdef __ANDROID__ +#include +#endif + +#ifdef NETWORK +#ifdef EMSCRIPTEN +#include "network/WebSocketClientEmscripten.h" +#else +#include "network/WebSocketClient.h" +#endif +#else +#include "network/LocalClient.h" +#endif + +namespace ZL +{ + + extern const char* CONST_ZIP_FILE; + + extern bool g_exitBgAnimating; + + extern bool firePressed; + + extern float x; + + 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 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 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 Space::worldToScreen(const Vector3f& world, float& outX, float& outY, float& outDepth) const + { + // Матрицы должны совпасть с drawBoxes/drawShip по смыслу + float aspect = static_cast(Environment::width) / static_cast(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::width; + float sy = (ndc.y() * 0.5f + 0.5f) * Environment::height; + + outX = sx; + outY = sy; + + // Можно отсеять те, что вне: + if (sx < -200 || sx > Environment::width + 200) return false; + if (sy < -200 || sy > Environment::height + 200) return false; + + return true; + } + + bool Space::projectToNDC(const Vector3f& world, float& ndcX, float& ndcY, float& ndcZ, float& clipW) const + { + float aspect = static_cast(Environment::width) / static_cast(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, 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()); + } + } + + Space::~Space() { + } + + void Space::setup() { + + + menuManager.onRestartPressed = [this]() { + this->shipAlive = true; + this->gameOver = false; + this->showExplosion = false; + this->explosionEmitter.setEmissionPoints(std::vector()); + Environment::shipState.position = Vector3f{ 0, 0, 45000.f }; + Environment::shipState.velocity = 0.0f; + Environment::shipState.rotation = Eigen::Matrix3f::Identity(); + Environment::inverseShipMatrix = Eigen::Matrix3f::Identity(); + Environment::zoom = DEFAULT_ZOOM; + Environment::tapDownHold = false; + + std::cerr << "Game restarted\n"; + }; + + menuManager.onVelocityChanged = [this](float newVelocity) { + newShipVelocity = newVelocity; + }; + + bool cfgLoaded = sparkEmitter.loadFromJsonFile("resources/config/spark_config.json", renderer, CONST_ZIP_FILE); + 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()); + projectileEmitter.setEmissionPoints(std::vector()); + + cubemapTexture = std::make_shared( + std::array{ + CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), + CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), + CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), + CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), + CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), + CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE) + }); + + + cubemap.data = ZL::CreateCubemap(500); + cubemap.RefreshVBO(); + + + + //Load texture + spaceshipTexture = std::make_unique(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 }); + + spaceship.AssignFrom(spaceshipBase); + spaceship.RefreshVBO(); + + + //Boxes + boxTexture = std::make_unique(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); + 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 + 1)); + } + + if (!cfgLoaded) + { + throw std::runtime_error("Failed to load spark emitter config file!"); + } + + textRenderer = std::make_unique(); + 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 defaultShaderName = "default"; + static const std::string envShaderName = "env_sky"; + static const std::string vPositionName = "vPosition"; + static const std::string vTexCoordName = "vTexCoord"; + static const std::string textureUniformName = "Texture"; + static const std::string skyPercentUniformName = "skyPercent"; + + renderer.shaderManager.PushShader(envShaderName); + renderer.RenderUniform1i(textureUniformName, 0); + renderer.RenderUniform1f(skyPercentUniformName, skyPercent); + renderer.EnableVertexAttribArray(vPositionName); + renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, + static_cast(Environment::width) / static_cast(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.DisableVertexAttribArray(vPositionName); + + renderer.shaderManager.PopShader(); + CheckGlError(); + } + + void Space::drawShip() + { + static const std::string defaultShaderName = "default"; + static const std::string envShaderName = "env"; + static const std::string vPositionName = "vPosition"; + static const std::string vTexCoordName = "vTexCoord"; + static const std::string textureUniformName = "Texture"; + + renderer.shaderManager.PushShader(defaultShaderName); + renderer.RenderUniform1i(textureUniformName, 0); + renderer.EnableVertexAttribArray(vPositionName); + renderer.EnableVertexAttribArray(vTexCoordName); + + renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, + static_cast(Environment::width) / static_cast(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 + + if (shipAlive) { + glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID()); + renderer.DrawVertexRenderStruct(spaceship); + } + renderer.PopMatrix(); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + for (const auto& p : projectiles) { + if (p && p->isActive()) { + p->draw(renderer); + } + } + + projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); + + if (shipAlive) { + renderer.PushMatrix(); + renderer.TranslateMatrix({ 0, 0, 16 }); + renderer.TranslateMatrix({ 0, -6.f, 0 }); + sparkEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); + renderer.PopMatrix(); + } + + if (showExplosion) { + explosionEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); + } + + //glBindTexture(GL_TEXTURE_2D, basePlatformTexture->getTexID()); + //renderer.DrawVertexRenderStruct(basePlatform); + + glDisable(GL_BLEND); + renderer.PopMatrix(); + renderer.PopProjectionMatrix(); + renderer.DisableVertexAttribArray(vPositionName); + renderer.DisableVertexAttribArray(vTexCoordName); + + renderer.shaderManager.PopShader(); + CheckGlError(); + } + + void Space::drawBoxes() + { + static const std::string defaultShaderName = "default"; + static const std::string envShaderName = "env"; + static const std::string vPositionName = "vPosition"; + static const std::string vTexCoordName = "vTexCoord"; + static const std::string textureUniformName = "Texture"; + + renderer.shaderManager.PushShader(defaultShaderName); + renderer.RenderUniform1i(textureUniformName, 0); + renderer.EnableVertexAttribArray(vPositionName); + renderer.EnableVertexAttribArray(vTexCoordName); + + renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, + static_cast(Environment::width) / static_cast(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()); + //glBindTexture(GL_TEXTURE_2D, rockTexture->getTexID()); + renderer.DrawVertexRenderStruct(boxRenderArr[i]); + + renderer.PopMatrix(); + } + renderer.PopProjectionMatrix(); + renderer.DisableVertexAttribArray(vPositionName); + renderer.DisableVertexAttribArray(vTexCoordName); + + renderer.shaderManager.PopShader(); + CheckGlError(); + } + + void Space::drawScene() { + static const std::string defaultShaderName = "default"; + static const std::string envShaderName = "env"; + static const std::string vPositionName = "vPosition"; + static const std::string vTexCoordName = "vTexCoord"; + static const std::string textureUniformName = "Texture"; + + 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); + + 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(); + + //drawUI(); + drawTargetHud(); + CheckGlError(); + } + + void Space::drawRemoteShips() { + // Используем те же константы имен для шейдеров, что и в drawShip + static const std::string defaultShaderName = "default"; + static const std::string vPositionName = "vPosition"; + static const std::string vTexCoordName = "vTexCoord"; + static const std::string textureUniformName = "Texture"; + + // Активируем шейдер и текстуру (предполагаем, что меш у всех одинаковый) + renderer.shaderManager.PushShader(defaultShaderName); + renderer.RenderUniform1i(textureUniformName, 0); + + renderer.EnableVertexAttribArray(vPositionName); + renderer.EnableVertexAttribArray(vTexCoordName); + + renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, + static_cast(Environment::width) / static_cast(Environment::height), + Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); + + // Биндим текстуру корабля один раз для всех удаленных игроков (оптимизация батчинга) + glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID()); + + // Если сервер прислал коробки, применяем их однократно вместо локальной генерации + if (!serverBoxesApplied && networkClient) { + auto sboxes = networkClient->getServerBoxes(); + if (!sboxes.empty()) { + boxCoordsArr.clear(); + for (auto& b : sboxes) { + BoxCoords bc; + bc.pos = b.first; + bc.m = b.second; + boxCoordsArr.push_back(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); + serverBoxesApplied = true; + } + } + + // Итерируемся по актуальным данным из extrapolateRemotePlayers + 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.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset + renderer.RotateMatrix(Environment::inverseShipMatrix); + renderer.TranslateMatrix(-Environment::shipState.position); + + + Eigen::Vector3f relativePos = playerState.position;// -Environment::shipPosition; + renderer.TranslateMatrix(relativePos); + + // 3. Поворот врага + renderer.RotateMatrix(playerState.rotation); + + renderer.DrawVertexRenderStruct(spaceship); + renderer.PopMatrix(); + } + + renderer.PopProjectionMatrix(); + renderer.DisableVertexAttribArray(vPositionName); + renderer.DisableVertexAttribArray(vTexCoordName); + renderer.shaderManager.PopShader(); + + CheckGlError(); + } + + void Space::drawRemoteShipsLabels() + { + if (!textRenderer) return; + + //#ifdef NETWORK + // 2D поверх 3D + 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(); + /*if (distSq > MAX_DIST_SQ) // дальность прорисовки никнейма + continue;*/ + float dist = sqrt(distSq); + float alpha = 1.0f; // постоянная видимость + /*float alpha = std::clamp(1.f - (dist - FADE_START) / FADE_RANGE, 0.f, 1.f); // дальность прорисовки никнейма + if (alpha < 0.01f) + continue; */ + 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 label = "Player (" + std::to_string(st.id) + ") " + std::to_string((int)dist) + "m"; + + // TODO: nickname sync + + textRenderer->drawText(label, uiX + 1.f, uiY + 1.f, scale, true, { 0.f, 0.f, 0.f, alpha }); // color param + textRenderer->drawText(label, uiX, uiY, scale, true, { 1.f, 1.f, 1.f, alpha }); + } + + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + //#endif + } + + int Space::pickTargetId() const + { + int bestId = -1; + constexpr float INF_F = 1e30f; + float bestDistSq = INF_F; + + for (auto const& [id, st] : remotePlayerStates) { + if (deadRemotePlayers.count(id)) continue; + + float d2 = (Environment::shipState.position - st.position).squaredNorm(); + if (d2 < bestDistSq) { + bestDistSq = d2; + bestId = id; + } + } + return bestId; + } + + 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 uColor, если есть. + // Если в defaultColor нет uniform uColor — тогда alpha будет 1.0. + // Для совместимости: кладём RGB, alpha будем задавать uniform'ом отдельно. + Vector3f rgb{ rgba.x(), rgba.y(), rgba.z() }; + v.ColorData = { rgb, rgb, rgb, rgb, rgb, rgb }; + 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; + + // 2) проекция + float ndcX, ndcY, ndcZ, clipW; + if (!projectToNDC(shipWorld, ndcX, ndcY, ndcZ, clipW)) return; + + // behind camera? + bool behind = (clipW <= 0.0f); + + // on-screen check (NDC) + bool onScreen = (!behind && + ndcX >= -1.0f && ndcX <= 1.0f && + ndcY >= -1.0f && ndcY <= 1.0f); + + // 3) расстояние + float dist = (Environment::shipState.position - shipWorld).norm(); + + // time for arrow bob + float t = static_cast(SDL_GetTicks64()) * 0.001f; + + // 4) Настройки стиля (как X3) + Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный + float thickness = 2.0f; // толщина линий (px) + float z = 0.0f; // 2D слой + + // 5) Если цель в кадре: рисуем скобки + if (onScreen) + { + // перевод NDC -> экран (в пикселях) + float sx = (ndcX * 0.5f + 0.5f) * Environment::width; + float sy = (ndcY * 0.5f + 0.5f) * Environment::height; + + // анимация “снаружи внутрь” + // 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); + + renderer.shaderManager.PushShader("defaultColor"); + renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f); + renderer.PushMatrix(); + renderer.LoadIdentity(); + + // верх-лево: горизонт + вертикаль + 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); + + renderer.PopMatrix(); + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); + + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + + targetWasVisible = true; + return; + } + + // 6) Если цель offscreen: рисуем стрелку на краю + // dir: куда “смотреть” в NDC + 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; + + // пересечение луча с прямоугольником [-1..1] с отступом + 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::width; + float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::height; + + // лёгкая анимация “зова”: смещение по направлению + 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 }; + 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((float)Environment::width, (float)Environment::height, 0.f, 1.f); + renderer.PushMatrix(); + renderer.LoadIdentity(); + + // треугольник-стрелка + 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); + + renderer.PopMatrix(); + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); + + // дистанция рядом со стрелкой + // (у тебя ещё будет “статично под прицелом” — это просто другой TextView / drawText) + { + 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::processTickCount(int64_t newTickCount, int64_t delta) { + + auto now_ms = newTickCount; + + sparkEmitter.update(static_cast(delta)); + planetObject.update(static_cast(delta)); + + if (firePressed) + { + firePressed = false; + if (now_ms - lastProjectileFireTime >= static_cast(projectileCooldownMs)) { + lastProjectileFireTime = now_ms; + const float projectileSpeed = 250.0f; + + 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, 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(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 (!remotePlayer.canFetchClientStateAtTime(nowRoundedWithDelay)) + { + continue; + } + + ClientState playerState = remotePlayer.fetchClientStateAtTime(nowRoundedWithDelay); + + remotePlayerStates[id] = playerState; + + } + + for (auto& p : projectiles) { + if (p && p->isActive()) { + p->update(static_cast(delta), renderer); + } + } + + std::vector projCameraPoints; + for (const auto& p : projectiles) { + if (p && p->isActive()) { + Vector3f worldPos = p->getPosition(); + Vector3f rel = worldPos - Environment::shipState.position; + Vector3f camPos = Environment::inverseShipMatrix * rel; + projCameraPoints.push_back(camPos); + } + } + if (!projCameraPoints.empty()) { + projectileEmitter.setEmissionPoints(projCameraPoints); + projectileEmitter.emit(); + } + else { + projectileEmitter.setEmissionPoints(std::vector()); + } + + std::vector shipCameraPoints; + for (const auto& lp : shipLocalEmissionPoints) { + Vector3f adjusted = lp + Vector3f{ 0.0f, -Environment::zoom * 0.03f, 0.0f }; + shipCameraPoints.push_back(adjusted); + } + if (!shipCameraPoints.empty()) { + sparkEmitter.setEmissionPoints(shipCameraPoints); + } + + sparkEmitter.update(static_cast(delta)); + projectileEmitter.update(static_cast(delta)); + + explosionEmitter.update(static_cast(delta)); + if (showExplosion) { + uint64_t now = SDL_GetTicks64(); + if (lastExplosionTime != 0 && now - lastExplosionTime >= explosionDurationMs) { + showExplosion = false; + explosionEmitter.setEmissionPoints(std::vector()); + 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.velocity = 0.0f; + showExplosion = true; + + explosionEmitter.setUseWorldSpace(true); + explosionEmitter.setEmissionPoints(std::vector{ collisionPoint }); + explosionEmitter.emit(); + lastExplosionTime = SDL_GetTicks64(); + + std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n"; + + menuManager.showGameOver(); + } + 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(planetObject.planetStones.allInstances.size())) + continue; + + if (planetObject.planetStones.statuses.size() <= static_cast(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.velocity = 0.0f; + showExplosion = true; + + explosionEmitter.setUseWorldSpace(true); + explosionEmitter.setEmissionPoints(std::vector{ collidedStonePos }); + explosionEmitter.emit(); + lastExplosionTime = SDL_GetTicks64(); + + std::cerr << "GAME OVER: collision with stone on triangle " << collidedTriIdx << std::endl; + + if (collidedTriIdx >= 0 && collidedTriIdx < static_cast(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(planetObject.planetStones.statuses.size())) { + planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty; + } + + menuManager.showGameOver(); + } + } + } + + // update velocity text + + if (shipAlive && !gameOver) { + auto velocityTv = menuManager.uiManager.findTextView("velocityText"); + if (velocityTv) { + std::string velocityStr = "Velocity: " + std::to_string(static_cast(Environment::shipState.velocity)); + menuManager.uiManager.setText("velocityText", velocityStr); + } + } + + } + + void Space::fireProjectiles() { + std::vector localOffsets = { + Vector3f{ -1.5f, 0.9f - 6.f, 5.0f }, + Vector3f{ 1.5f, 0.9f - 6.f, 5.0f } + }; + + const float projectileSpeed = 60.0f; + const float lifeMs = 5000.0f; + 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); + break; + } + } + } + } + + + void Space::update() { + if (networkClient) { + auto pending = networkClient->getPendingProjectiles(); + if (!pending.empty()) { + const float projectileSpeed = 60.0f; + const float lifeMs = 5000.0f; + const float size = 0.5f; + for (const auto& pi : pending) { + const std::vector localOffsets = { + Vector3f{ -1.5f, 0.9f, 5.0f }, + Vector3f{ 1.5f, 0.9f, 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); + 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{ 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; + menuManager.showGameOver(); + } + else { + deadRemotePlayers.insert(d.targetId); + std::cout << "Marked remote player " << d.targetId << " as dead" << std::endl; + } + } + } + + 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; + } + } + + 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{ destruction.position }); + explosionEmitter.emit(); + lastExplosionTime = SDL_GetTicks64(); + + std::cout << "Game: Box " << idx << " destroyed by player " + << destruction.destroyedBy << std::endl; + } + } + } + } + } + } + + void Space::handleDown(int mx, int my) + { + int uiX = mx; + int uiY = Environment::height - my; + + menuManager.uiManager.onMouseDown(uiX, uiY); + + bool uiHandled = false; + + for (const auto& button : menuManager.uiManager.findButton("") ? std::vector>{} : std::vector>{}) { + (void)button; + } + + auto pressedSlider = [&]() -> std::shared_ptr { + for (const auto& slider : menuManager.uiManager.findSlider("") ? std::vector>{} : std::vector>{}) { + (void)slider; + } + return nullptr; + }(); + + if (!menuManager.uiManager.isUiInteraction()) { + Environment::tapDownHold = true; + + Environment::tapDownStartPos(0) = mx; + Environment::tapDownStartPos(1) = my; + + Environment::tapDownCurrentPos(0) = mx; + Environment::tapDownCurrentPos(1) = my; + } + } + + void Space::handleUp(int mx, int my) + { + int uiX = mx; + int uiY = Environment::height - my; + + menuManager.uiManager.onMouseUp(uiX, uiY); + + if (!menuManager.uiManager.isUiInteraction()) { + Environment::tapDownHold = false; + } + } + + void Space::handleMotion(int mx, int my) + { + int uiX = mx; + int uiY = Environment::height - my; + + menuManager.uiManager.onMouseMove(uiX, uiY); + + if (Environment::tapDownHold && !menuManager.uiManager.isUiInteraction()) { + 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; + }*/ + + +} // namespace ZL diff --git a/src/Space.h b/src/Space.h index f0d56d1..993d3e4 100644 --- a/src/Space.h +++ b/src/Space.h @@ -27,31 +27,32 @@ namespace ZL { Matrix3f m; }; - /* + class Space { public: - Space(Renderer& iRenderer, TaskManager& iTaskManager, MainThreadHandler& iMainThreadHandler); + Space(Renderer& iRenderer, TaskManager& iTaskManager, MainThreadHandler& iMainThreadHandler, std::unique_ptr& iNetworkClient, MenuManager& iMenuManager); ~Space(); void setup(); void update(); - void render(); bool shouldExit() const { return Environment::exitGameLoop; } Renderer& renderer; TaskManager& taskManager; MainThreadHandler& mainThreadHandler; + std::unique_ptr& networkClient; + MenuManager& menuManager; - private: - int64_t getSyncTimeMs(); - void processTickCount(); + + public: + void processTickCount(int64_t newTickCount, int64_t delta); void drawScene(); void drawCubemap(float skyPercent); void drawShip(); void drawBoxes(); void drawBoxesLabels(); - void drawUI(); + //void drawUI(); void drawRemoteShips(); void drawRemoteShipsLabels(); void fireProjectiles(); @@ -62,13 +63,13 @@ namespace ZL { void handleUp(int mx, int my); void handleMotion(int mx, int my); - SDL_Window* window; - SDL_GLContext glContext; + //SDL_Window* window; + //SDL_GLContext glContext; - int64_t newTickCount; - int64_t lastTickCount; + //int64_t newTickCount; + //int64_t lastTickCount; std::vector boxCoordsArr; std::vector boxRenderArr; @@ -76,7 +77,6 @@ namespace ZL { std::vector boxLabels; std::unique_ptr textRenderer; - //std::unordered_map latestRemotePlayers; std::unordered_map remotePlayerStates; float newShipVelocity = 0; @@ -101,7 +101,7 @@ namespace ZL { SparkEmitter explosionEmitter; PlanetObject planetObject; - MenuManager menuManager; + //MenuManager menuManager; std::vector> projectiles; std::shared_ptr projectileTexture; @@ -116,7 +116,6 @@ namespace ZL { std::vector boxAlive; float shipCollisionRadius = 15.0f; float boxCollisionRadius = 2.0f; - //bool uiGameOverShown = false; bool showExplosion = false; uint64_t lastExplosionTime = 0; const uint64_t explosionDurationMs = 500; @@ -146,7 +145,9 @@ namespace ZL { bool projectToNDC(const Vector3f& world, float& ndcX, float& ndcY, float& ndcZ, float& clipW) const; void drawTargetHud(); // рисует рамку или стрелку int pickTargetId() const; // выбирает цель (пока: ближайший живой удаленный игрок) - };*/ + + void clearTextRendererCache(); + }; } // namespace ZL \ No newline at end of file