space-game001/src/Space.cpp
2026-03-09 23:04:48 +03:00

2284 lines
74 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "Space.h"
#include "AnimatedModel.h"
#include "BoneAnimatedModel.h"
#include "planet/PlanetData.h"
#include "utils/Utils.h"
#include "render/OpenGlExtensions.h"
#include <iostream>
#include "render/TextureManager.h"
#include "TextModel.h"
#include <random>
#include <cmath>
#include <algorithm>
#include <functional>
#ifdef __ANDROID__
#include <android/log.h>
#endif
#ifdef NETWORK
#ifdef EMSCRIPTEN
#include "network/WebSocketClientEmscripten.h"
#else
#include "network/WebSocketClient.h"
#endif
#else
#include "network/LocalClient.h"
#endif
#include "GameConstants.h"
namespace ZL
{
extern const char* CONST_ZIP_FILE;
extern float x;
extern float y;
extern float z;
Eigen::Quaternionf generateRandomQuaternion(std::mt19937& gen)
{
std::normal_distribution<> distrib(0.0, 1.0);
Eigen::Quaternionf randomQuat = {
(float)distrib(gen),
(float)distrib(gen),
(float)distrib(gen),
(float)distrib(gen)
};
return randomQuat.normalized();
}
std::vector<BoxCoords> generateRandomBoxCoords(int N)
{
const float MIN_DISTANCE = 3.0f;
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
const float MIN_COORD = -100.0f;
const float MAX_COORD = 100.0f;
const int MAX_ATTEMPTS = 1000;
std::vector<BoxCoords> boxCoordsArr;
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> distrib(MIN_COORD, MAX_COORD);
int generatedCount = 0;
while (generatedCount < N)
{
bool accepted = false;
int attempts = 0;
while (!accepted && attempts < MAX_ATTEMPTS)
{
Vector3f newPos(
(float)distrib(gen),
(float)distrib(gen),
(float)distrib(gen)
);
accepted = true;
for (const auto& existingBox : boxCoordsArr)
{
Vector3f diff = newPos - existingBox.pos;
float distanceSquared = diff.squaredNorm();
if (distanceSquared < MIN_DISTANCE_SQUARED)
{
accepted = false;
break;
}
}
if (accepted)
{
Eigen::Quaternionf randomQuat = generateRandomQuaternion(gen);
Matrix3f randomMatrix = randomQuat.toRotationMatrix();
boxCoordsArr.emplace_back(BoxCoords{ newPos, randomMatrix });
generatedCount++;
}
attempts++;
}
if (!accepted) {
break;
}
}
return boxCoordsArr;
}
static Eigen::Matrix4f makeViewMatrix_FromYourCamera()
{
Eigen::Matrix4f Tz = Eigen::Matrix4f::Identity();
Tz(2, 3) = -1.0f * ZL::Environment::zoom;
Eigen::Matrix4f R = Eigen::Matrix4f::Identity();
R.block<3, 3>(0, 0) = ZL::Environment::inverseShipMatrix;
Eigen::Matrix4f Tship = Eigen::Matrix4f::Identity();
Tship(0, 3) = -ZL::Environment::shipState.position.x();
Tship(1, 3) = -ZL::Environment::shipState.position.y();
Tship(2, 3) = -ZL::Environment::shipState.position.z();
return Tz * R * Tship;
}
static Eigen::Matrix4f makePerspective(float fovyRadians, float aspect, float zNear, float zFar)
{
// Стандартная перспектива
float f = 1.0f / std::tan(fovyRadians * 0.5f);
Eigen::Matrix4f P = Eigen::Matrix4f::Zero();
P(0, 0) = f / aspect;
P(1, 1) = f;
P(2, 2) = (zFar + zNear) / (zNear - zFar);
P(2, 3) = (2.0f * zFar * zNear) / (zNear - zFar);
P(3, 2) = -1.0f;
return P;
}
bool worldToScreen(const Vector3f& world, float& outX, float& outY, float& outDepth)
{
// Матрицы должны совпасть с drawBoxes/drawShip по смыслу
float aspect = static_cast<float>(Environment::width) / static_cast<float>(Environment::height);
Eigen::Matrix4f V = makeViewMatrix_FromYourCamera();
Eigen::Matrix4f P = makePerspective(1.0f / 1.5f, aspect, Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
Eigen::Vector4f w(world.x(), world.y(), world.z(), 1.0f);
Eigen::Vector4f clip = P * V * w;
if (clip.w() <= 0.0001f) return false; // позади камеры
Eigen::Vector3f ndc = clip.head<3>() / clip.w(); // [-1..1]
outDepth = ndc.z();
// В пределах экрана?
// (можно оставить, можно клампить)
float sx = (ndc.x() * 0.5f + 0.5f) * Environment::projectionWidth;
float sy = (ndc.y() * 0.5f + 0.5f) * Environment::projectionHeight;
outX = sx;
outY = sy;
// Можно отсеять те, что вне:
if (sx < -200 || sx > Environment::projectionWidth + 200) return false;
if (sy < -200 || sy > Environment::projectionHeight + 200) return false;
return true;
}
bool projectToNDC(const Vector3f& world, float& ndcX, float& ndcY, float& ndcZ, float& clipW)
{
float aspect = static_cast<float>(Environment::width) / static_cast<float>(Environment::height);
Eigen::Matrix4f V = makeViewMatrix_FromYourCamera();
Eigen::Matrix4f P = makePerspective(1.0f / 1.5f, aspect, Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
Eigen::Vector4f w(world.x(), world.y(), world.z(), 1.0f);
Eigen::Vector4f clip = P * V * w;
clipW = clip.w();
if (std::abs(clipW) < 1e-6f) return false;
Eigen::Vector3f ndc = clip.head<3>() / clipW;
ndcX = ndc.x();
ndcY = ndc.y();
ndcZ = ndc.z();
return true;
}
void Space::drawBoxesLabels()
{
if (!textRenderer) return;
// Текст рисуем как 2D поверх всего 3D, но ДО drawUI или после — как хочешь.
// Чтобы подписи были поверх — делай после drawBoxes и до drawUI (как мы и вставили).
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
for (size_t i = 0; i < boxCoordsArr.size(); ++i)
{
if (i >= boxAlive.size() || !boxAlive[i]) continue;
if (i >= boxLabels.size()) continue;
// ВАЖНО: твои боксы рисуются с Translate({0,0,45000}) + pos
Vector3f boxWorld = boxCoordsArr[i].pos + Vector3f{ 0.0f, 0.0f, 45000.0f };
// Чуть выше бокса по Y (или по Z — как нравится)
Vector3f labelWorld = boxWorld + Vector3f{ 0.0f, 2.2f, 0.0f };
float sx, sy, depth;
if (!worldToScreen(labelWorld, sx, sy, depth)) continue;
// В твоей UI-системе Y обычно перевёрнут (ты делаешь uiY = height - my).
// Наш worldToScreen отдаёт Y в системе "низ=0, верх=height" (NDC->screen).
// Чтобы совпало с твоей UI-логикой, перевернём:
float uiX = sx;
float uiY = sy; // если окажется вверх ногами — замени на (Environment::height - sy)
float dist = (Environment::shipState.position - boxWorld).norm();
float scaleRaw = 120.0f / (dist + 1.0f);
float scale = std::round(scaleRaw * 10.0f) / 10.0f; // округление до 0.1
scale = std::clamp(scale, 0.6f, 1.2f);
textRenderer->drawText(boxLabels[i], uiX, uiY, scale, /*centered*/true);
}
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
}
Space::Space(Renderer& iRenderer, TaskManager& iTaskManager, MainThreadHandler& iMainThreadHandler, std::unique_ptr<INetworkClient>& iNetworkClient, MenuManager& iMenuManager)
: renderer(iRenderer),
taskManager(iTaskManager),
mainThreadHandler(iMainThreadHandler),
planetObject(renderer, taskManager, mainThreadHandler),
networkClient(iNetworkClient),
menuManager(iMenuManager)
{
projectiles.reserve(maxProjectiles);
for (int i = 0; i < maxProjectiles; ++i) {
projectiles.emplace_back(std::make_unique<Projectile>());
}
}
Space::~Space() {
}
void Space::resetPlayerState()
{
shipAlive = true;
gameOver = false;
showExplosion = false;
explosionEmitter.setEmissionPoints(std::vector<Vector3f>());
Environment::shipState.position = Vector3f{ 0, 0, 45000.f };
Environment::shipState.velocity = 0.0f;
Environment::shipState.selectedVelocity = 0;
newShipVelocity = 0;
Environment::shipState.rotation = Eigen::Matrix3f::Identity();
Environment::inverseShipMatrix = Eigen::Matrix3f::Identity();
Environment::zoom = DEFAULT_ZOOM;
Environment::tapDownHold = false;
playerScore = 0;
if (menuManager.uiManager.findButton("minusButton"))
{
menuManager.uiManager.findButton("minusButton")->state = ButtonState::Disabled;
}
if (menuManager.uiManager.findButton("plusButton"))
{
menuManager.uiManager.findButton("plusButton")->state = ButtonState::Normal;
}
if (Environment::shipState.shipType == 0)
{
if (menuManager.uiManager.findButton("shootButton"))
{
menuManager.uiManager.findButton("shootButton")->state = ButtonState::Normal;
}
if (menuManager.uiManager.findButton("shootButton2"))
{
menuManager.uiManager.findButton("shootButton2")->state = ButtonState::Normal;
}
}
}
void Space::setup() {
menuManager.onRestartPressed = [this]() {
resetPlayerState();
if (networkClient) {
try {
networkClient->Send(std::string("RESPAWN"));
std::cout << "Client: Sent RESPAWN to server\n";
}
catch (...) {
std::cerr << "Client: Failed to send RESPAWN\n";
}
}
std::cerr << "Game restarted\n";
};
menuManager.onVelocityChanged = [this](float newVelocity) {
newShipVelocity = newVelocity;
if (Environment::shipState.shipType == 0)
{
if (newVelocity > 2)
{
this->menuManager.uiManager.findButton("shootButton")->state = ButtonState::Disabled;
this->menuManager.uiManager.findButton("shootButton2")->state = ButtonState::Disabled;
}
else
{
this->menuManager.uiManager.findButton("shootButton")->state = ButtonState::Normal;
this->menuManager.uiManager.findButton("shootButton2")->state = ButtonState::Normal;
}
}
};
menuManager.onFirePressed = [this]() {
firePressed = true;
};
menuManager.onShowPlayersPressed = [this]() {
buildAndShowPlayerList();
};
menuManager.onTakeButtonPressed = [this]() {
if (Environment::shipState.shipType != 1) return;
if (!networkClient) return;
int bestIdx = -1;
float bestDistSq = BOX_PICKUP_RADIUS * BOX_PICKUP_RADIUS;
for (size_t i = 0; i < boxCoordsArr.size(); ++i) {
if (i >= boxAlive.size() || !boxAlive[i]) continue;
Vector3f boxWorld = boxCoordsArr[i].pos + Vector3f{ 0.f, 0.f, 45000.f };
float distSq = (Environment::shipState.position - boxWorld).squaredNorm();
if (distSq <= bestDistSq) {
bestDistSq = distSq;
bestIdx = static_cast<int>(i);
}
}
if (bestIdx >= 0) {
networkClient->Send("BOX_PICKUP:" + std::to_string(bestIdx));
this->playerScore += 1;
}
};
bool cfgLoaded = sparkEmitter.loadFromJsonFile("resources/config/spark_config.json", renderer, CONST_ZIP_FILE);
bool cfgLoaded2 = sparkEmitterCargo.loadFromJsonFile("resources/config/spark_config_cargo.json", renderer, CONST_ZIP_FILE);
sparkEmitter.setIsActive(false);
sparkEmitterCargo.setIsActive(false);
bool projCfgLoaded = projectileEmitter.loadFromJsonFile("resources/config/spark_projectile_config.json", renderer, CONST_ZIP_FILE);
bool explosionCfgLoaded = explosionEmitter.loadFromJsonFile("resources/config/explosion_config.json", renderer, CONST_ZIP_FILE);
explosionEmitter.setEmissionPoints(std::vector<Vector3f>());
//projectileEmitter.setEmissionPoints({ Vector3f{0,0,45000} });
//projectileEmitter.setUseWorldSpace(true);
cubemapTexture = std::make_shared<Texture>(
std::array<TextureDataStruct, 6>{
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE)
});
cubemap.data = ZL::CreateCubemap(500);
cubemap.RefreshVBO();
//Load texture
spaceshipTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/MainCharacter_Base_color_sRGB.png", CONST_ZIP_FILE));
spaceshipBase = LoadFromTextFile02("resources/spaceshipnew001.txt", CONST_ZIP_FILE);
spaceshipBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())).toRotationMatrix());// QuatFromRotateAroundY(M_PI / 2.0).toRotationMatrix());
spaceshipBase.Move(Vector3f{ 1.2, 0, -5 });
spaceshipBase.Scale(0.4f);
spaceship.AssignFrom(spaceshipBase);
spaceship.RefreshVBO();
// Load cargo
cargoTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/Cargo_Base_color_sRGB.png", CONST_ZIP_FILE));
cargoBase = LoadFromTextFile02("resources/cargoship001.txt", CONST_ZIP_FILE);
auto quat = Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitZ()));
auto rotMatrix = quat.toRotationMatrix();
cargoBase.RotateByMatrix(rotMatrix);
auto quat2 = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI * 0.5, Eigen::Vector3f::UnitY()));
auto rotMatrix2 = quat2.toRotationMatrix();
cargoBase.RotateByMatrix(rotMatrix2);
//cargoBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())).toRotationMatrix());
cargoBase.Move(Vector3f{ 0, 0, -5 });
cargo.AssignFrom(cargoBase);
cargo.RefreshVBO();
//projectileTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/spark2.png", CONST_ZIP_FILE));
//Boxes
boxTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/box/box.png", CONST_ZIP_FILE));
boxBase = LoadFromTextFile02("resources/box/box.txt", CONST_ZIP_FILE);
boxCoordsArr = generateRandomBoxCoords(50);
boxRenderArr.resize(boxCoordsArr.size());
for (int i = 0; i < boxCoordsArr.size(); i++)
{
boxRenderArr[i].AssignFrom(boxBase);
boxRenderArr[i].RefreshVBO();
}
boxAlive.resize(boxCoordsArr.size(), true);
#ifdef NETWORK
std::fill(boxAlive.begin(), boxAlive.end(), false);
serverBoxesApplied = false;
#endif
ZL::CheckGlError();
boxLabels.clear();
boxLabels.reserve(boxCoordsArr.size());
for (size_t i = 0; i < boxCoordsArr.size(); ++i) {
boxLabels.push_back("Box " + std::to_string(i));
}
if (!cfgLoaded)
{
throw std::runtime_error("Failed to load spark emitter config file!");
}
crosshairCfgLoaded = loadCrosshairConfig("resources/config/crosshair_config.json");
std::cout << "[Crosshair] loaded=" << crosshairCfgLoaded
<< " enabled=" << crosshairCfg.enabled
<< " w=" << Environment::width << " h=" << Environment::height
<< " alpha=" << crosshairCfg.alpha
<< " thickness=" << crosshairCfg.thicknessPx
<< " gap=" << crosshairCfg.gapPx << "\n";
if (!crosshairCfgLoaded) {
std::cerr << "Failed to load crosshair_config.json, using defaults\n";
}
textRenderer = std::make_unique<ZL::TextRenderer>();
if (!textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32, CONST_ZIP_FILE)) {
std::cerr << "Failed to init TextRenderer\n";
}
ZL::CheckGlError();
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
planetObject.init();
}
void Space::drawCubemap(float skyPercent)
{
static const std::string envSkyShaderName = "env_sky";
static const std::string skyPercentUniformName = "skyPercent";
renderer.shaderManager.PushShader(envSkyShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.RenderUniform1f(skyPercentUniformName, skyPercent);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.RotateMatrix(Environment::inverseShipMatrix);
Vector3f worldLightDir = Vector3f(1.0f, -1.0f, -1.0f).normalized();
Matrix3f viewMatrix = Environment::inverseShipMatrix;
Vector3f viewLightDir = (viewMatrix * worldLightDir).normalized();
// Передаем вектор НА источник света
Vector3f lightToSource = -viewLightDir;
renderer.RenderUniform3fv("uLightDirView", lightToSource.data());
// 2. Базовый цвет атмосферы (голубой)
Vector3f skyColor = { 0.0f, 0.5f, 1.0f };
renderer.RenderUniform3fv("uSkyColor", skyColor.data());
// 1. Вектор направления от центра планеты к игроку (в мировых координатах)
// Предполагаем, что планета в (0,0,0). Если нет, то (shipPosition - planetCenter)
Vector3f playerDirWorld = Environment::shipState.position.normalized();
// 2. Направление света в мировом пространстве
//Vector3f worldLightDir = Vector3f(1.0f, -1.0f, -1.0f).normalized();
// 3. Считаем глобальную освещенность для игрока (насколько он на свету)
// Это одно число для всего кадра
float playerLightFactor = playerDirWorld.dot(-worldLightDir);
// Ограничиваем и делаем переход мягче
playerLightFactor = std::clamp((playerLightFactor + 0.2f) / 1.2f, 0.0f, 1.0f);
renderer.RenderUniform1f("uPlayerLightFactor", playerLightFactor);
renderer.RenderUniform1f("skyPercent", skyPercent);
CheckGlError();
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture->getTexID());
renderer.DrawVertexRenderStruct(cubemap);
CheckGlError();
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
CheckGlError();
}
void Space::drawShip()
{
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.PushMatrix();
renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset
if (shipAlive) {
if (Environment::shipState.shipType == 1 && cargoTexture) {
glBindTexture(GL_TEXTURE_2D, cargoTexture->getTexID());
renderer.DrawVertexRenderStruct(cargo);
}
else {
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
renderer.DrawVertexRenderStruct(spaceship);
}
drawShipSparkEmitters();
}
renderer.PopMatrix();
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipState.position);
for (const auto& p : projectiles) {
if (p && p->isActive()) {
p->projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
}
}
renderer.PopMatrix();
glDisable(GL_BLEND);
renderer.shaderManager.PopShader();
if (showExplosion) {
explosionEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height, false);
}
glDisable(GL_BLEND);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
CheckGlError();
}
void Space::drawBoxes()
{
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
for (int i = 0; i < boxCoordsArr.size(); i++)
{
if (!boxAlive[i]) continue;
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipState.position);
renderer.TranslateMatrix({ 0.f, 0.f, 45000.f });
renderer.TranslateMatrix(boxCoordsArr[i].pos);
renderer.RotateMatrix(boxCoordsArr[i].m);
glBindTexture(GL_TEXTURE_2D, boxTexture->getTexID());
renderer.DrawVertexRenderStruct(boxRenderArr[i]);
renderer.PopMatrix();
}
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
CheckGlError();
}
void Space::drawScene() {
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
glViewport(0, 0, Environment::width, Environment::height);
prepareSparkEmittersForDraw();
CheckGlError();
float skyPercent = 0.0;
float distance = planetObject.distanceToPlanetSurface(Environment::shipState.position);
if (distance > 1500.f)
{
skyPercent = 0.0f;
}
else if (distance < 800.f)
{
skyPercent = 1.0f;
}
else
{
skyPercent = (1500.f - distance) / (1500.f - 800.f);
}
drawCubemap(skyPercent);
planetObject.draw(renderer);
if (planetObject.distanceToPlanetSurface(Environment::shipState.position) > 100.f)
{
glClear(GL_DEPTH_BUFFER_BIT);
}
drawRemoteShips();
drawRemoteShipsLabels();
drawBoxes();
drawBoxesLabels();
drawShip();
drawCrosshair();
drawTargetHud();
CheckGlError();
}
void Space::drawRemoteShips() {
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
if (!serverBoxesApplied && networkClient) {
auto sboxes = networkClient->getServerBoxes();
auto destroyedFlags = networkClient->getServerBoxDestroyedFlags();
if (!sboxes.empty()) {
boxCoordsArr.clear();
boxCoordsArr.resize(sboxes.size());
for (size_t i = 0; i < sboxes.size(); ++i) {
BoxCoords bc;
bc.pos = sboxes[i].first;
bc.m = sboxes[i].second;
boxCoordsArr[i] = bc;
}
boxRenderArr.resize(boxCoordsArr.size());
for (int i = 0; i < (int)boxCoordsArr.size(); ++i) {
boxRenderArr[i].AssignFrom(boxBase);
boxRenderArr[i].RefreshVBO();
}
boxAlive.assign(boxCoordsArr.size(), true);
size_t n = (std::min)(destroyedFlags.size(), boxAlive.size());
for (size_t i = 0; i < n; ++i) {
if (destroyedFlags[i]) boxAlive[i] = false; // destroyed => не рисуем
}
boxLabels.clear();
boxLabels.resize(boxCoordsArr.size());
for (size_t i = 0; i < boxCoordsArr.size(); ++i) {
boxLabels[i] = "Box " + std::to_string(i);
}
serverBoxesApplied = true;
}
}
for (auto const& [id, remotePlayer] : remotePlayerStates) {
const ClientState& playerState = remotePlayer;
if (deadRemotePlayers.count(id)) continue;
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipState.position);
Eigen::Vector3f relativePos = playerState.position;// -Environment::shipPosition;
renderer.TranslateMatrix(relativePos);
renderer.RotateMatrix(playerState.rotation);
if (playerState.shipType == 1 && cargoTexture) {
glBindTexture(GL_TEXTURE_2D, cargoTexture->getTexID());
renderer.DrawVertexRenderStruct(cargo);
}
else {
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
renderer.DrawVertexRenderStruct(spaceship);
}
renderer.PopMatrix();
}
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
CheckGlError();
}
void Space::drawRemoteShipsLabels()
{
if (!textRenderer) return;
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
for (auto const& [id, remotePlayer] : remotePlayerStates)
{
if (deadRemotePlayers.count(id)) continue;
const ClientState& st = remotePlayer;
Vector3f shipWorld = st.position;
float distSq = (Environment::shipState.position - shipWorld).squaredNorm();
float dist = sqrt(distSq);
float alpha = 1.0f;
Vector3f labelWorld = shipWorld + Vector3f{ 0.f, -4.f, 0.f };
float sx, sy, depth;
if (!worldToScreen(labelWorld, sx, sy, depth))
continue;
float uiX = sx, uiY = sy;
float scale = std::clamp(BASE_SCALE / (dist * PERSPECTIVE_K + 1.f), MIN_SCALE, MAX_SCALE);
std::string displayName;
if (!st.nickname.empty() && st.nickname != "Player") {
displayName = st.nickname;
}
else {
displayName = "Player (" + std::to_string(st.id) + ")";
}
std::string label = displayName + " " + std::to_string((int)dist) + "m";
textRenderer->drawText(label, uiX + 1.f, uiY + 1.f, scale, true, { 0.f, 0.f, 0.f, alpha });
textRenderer->drawText(label, uiX, uiY, scale, true, { 1.f, 1.f, 1.f, alpha });
}
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
}
// хелпер прицела: добавляет повернутую 2D-линию в меш прицела
static void AppendRotatedRect2D(
VertexDataStruct& out,
float cx, float cy,
float length, float thickness,
float angleRad,
float z,
const Eigen::Vector3f& rgb)
{
// прямоугольник вдоль локальной оси +X: [-L/2..+L/2] и [-T/2..+T/2]
float hl = length * 0.5f;
float ht = thickness * 0.5f;
Eigen::Vector2f p0(-hl, -ht);
Eigen::Vector2f p1(-hl, +ht);
Eigen::Vector2f p2(+hl, +ht);
Eigen::Vector2f p3(+hl, -ht);
float c = std::cos(angleRad);
float s = std::sin(angleRad);
auto rot = [&](const Eigen::Vector2f& p) -> Vector3f {
float rx = p.x() * c - p.y() * s;
float ry = p.x() * s + p.y() * c;
return Vector3f(cx + rx, cy + ry, z);
};
Vector3f v0 = rot(p0);
Vector3f v1 = rot(p1);
Vector3f v2 = rot(p2);
Vector3f v3 = rot(p3);
// 2 треугольника
out.PositionData.push_back(v0);
out.PositionData.push_back(v1);
out.PositionData.push_back(v2);
out.PositionData.push_back(v2);
out.PositionData.push_back(v3);
out.PositionData.push_back(v0);
for (int i = 0; i < 6; ++i) out.ColorData.push_back(rgb);
}
bool Space::loadCrosshairConfig(const std::string& path)
{
using json = nlohmann::json;
std::string content;
try {
if (std::string(CONST_ZIP_FILE).empty()) content = readTextFile(path);
else {
auto buf = readFileFromZIP(path, CONST_ZIP_FILE);
if (buf.empty()) return false;
content.assign(buf.begin(), buf.end());
}
json j = json::parse(content);
if (j.contains("enabled")) crosshairCfg.enabled = j["enabled"].get<bool>();
if (j.contains("referenceResolution") && j["referenceResolution"].is_array() && j["referenceResolution"].size() == 2) {
crosshairCfg.refW = j["referenceResolution"][0].get<int>();
crosshairCfg.refH = j["referenceResolution"][1].get<int>();
}
if (j.contains("scale")) crosshairCfg.scaleMul = j["scale"].get<float>();
crosshairCfg.scaleMul = std::clamp(crosshairCfg.scaleMul, 0.1f, 3.0f);
if (j.contains("color") && j["color"].is_array() && j["color"].size() == 3) {
crosshairCfg.color = Eigen::Vector3f(
j["color"][0].get<float>(),
j["color"][1].get<float>(),
j["color"][2].get<float>()
);
}
if (j.contains("cl_crosshairalpha")) crosshairCfg.alpha = j["cl_crosshairalpha"].get<float>();
if (j.contains("cl_crosshairthickness")) crosshairCfg.thicknessPx = j["cl_crosshairthickness"].get<float>();
if (j.contains("centerGapPx")) crosshairCfg.gapPx = j["centerGapPx"].get<float>();
if (j.contains("top") && j["top"].is_object()) {
auto t = j["top"];
if (t.contains("lengthPx")) crosshairCfg.topLenPx = t["lengthPx"].get<float>();
if (t.contains("angleDeg")) crosshairCfg.topAngleDeg = t["angleDeg"].get<float>();
}
crosshairCfg.arms.clear();
if (j.contains("arms") && j["arms"].is_array()) {
for (auto& a : j["arms"]) {
CrosshairConfig::Arm arm;
arm.lenPx = a.value("lengthPx", 20.0f);
arm.angleDeg = a.value("angleDeg", 210.0f);
crosshairCfg.arms.push_back(arm);
}
}
else {
// дефолт
crosshairCfg.arms.push_back({ 20.0f, 210.0f });
crosshairCfg.arms.push_back({ 20.0f, 330.0f });
}
// clamp
crosshairCfg.alpha = std::clamp(crosshairCfg.alpha, 0.0f, 1.0f);
crosshairCfg.thicknessPx = max(0.5f, crosshairCfg.thicknessPx);
crosshairCfg.gapPx = max(0.0f, crosshairCfg.gapPx);
crosshairMeshValid = false; // пересобрать
return true;
}
catch (...) {
return false;
}
}
// пересобирает mesh прицела при изменениях/ресайзе
void Space::rebuildCrosshairMeshIfNeeded()
{
if (!crosshairCfg.enabled) return;
// если ничего не изменилось — не трогаем VBO
if (crosshairMeshValid &&
crosshairLastW == Environment::projectionWidth &&
crosshairLastH == Environment::projectionHeight &&
std::abs(crosshairLastAlpha - crosshairCfg.alpha) < 1e-6f &&
std::abs(crosshairLastThickness - crosshairCfg.thicknessPx) < 1e-6f &&
std::abs(crosshairLastGap - crosshairCfg.gapPx) < 1e-6f &&
std::abs(crosshairLastScaleMul - crosshairCfg.scaleMul) < 1e-6f)
{
return;
}
crosshairLastW = Environment::projectionWidth;
crosshairLastH = Environment::projectionHeight;
crosshairLastAlpha = crosshairCfg.alpha;
crosshairLastThickness = crosshairCfg.thicknessPx;
crosshairLastGap = crosshairCfg.gapPx;
crosshairLastScaleMul = crosshairCfg.scaleMul;
float cx = Environment::projectionWidth * 0.5f;
float cy = Environment::projectionHeight * 0.5f;
// масштаб от reference (стандартно: по высоте)
float scale = (crosshairCfg.refH > 0) ? (Environment::projectionHeight / (float)crosshairCfg.refH) : 1.0f;
scale *= crosshairCfg.scaleMul;
float thickness = crosshairCfg.thicknessPx * scale;
float gap = crosshairCfg.gapPx * scale;
VertexDataStruct v;
v.PositionData.reserve(6 * (1 + (int)crosshairCfg.arms.size()));
v.ColorData.reserve(6 * (1 + (int)crosshairCfg.arms.size()));
const float z = 0.0f;
const Eigen::Vector3f rgb = crosshairCfg.color;
auto deg2rad = [](float d) { return d * 3.1415926535f / 180.0f; };
// TOP (короткая палочка сверху)
{
float len = crosshairCfg.topLenPx * scale;
float ang = deg2rad(crosshairCfg.topAngleDeg);
// сдвигаем сегмент от центра на gap + len/2 по направлению
float dx = std::cos(ang);
float dy = std::sin(ang);
float mx = cx + dx * (gap + len * 0.5f);
float my = cy + dy * (gap + len * 0.5f);
AppendRotatedRect2D(v, mx, my, len, thickness, ang, z, rgb);
}
// ARMS (2 луча вниз-влево и вниз-вправо)
for (auto& a : crosshairCfg.arms)
{
float len = a.lenPx * scale;
float ang = deg2rad(a.angleDeg);
float dx = std::cos(ang);
float dy = std::sin(ang);
float mx = cx + dx * (gap + len * 0.5f);
float my = cy + dy * (gap + len * 0.5f);
AppendRotatedRect2D(v, mx, my, len, thickness, ang, z, rgb);
}
crosshairMesh.AssignFrom(v);
crosshairMesh.RefreshVBO();
crosshairMeshValid = true;
}
void Space::drawCrosshair()
{
if (!crosshairCfg.enabled) return;
rebuildCrosshairMeshIfNeeded();
if (!crosshairMeshValid) return;
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
renderer.shaderManager.PushShader("defaultColor");
renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
renderer.PushMatrix();
renderer.LoadIdentity();
Eigen::Vector4f uColor(crosshairCfg.color.x(), crosshairCfg.color.y(), crosshairCfg.color.z(), crosshairCfg.alpha);
renderer.RenderUniform4fv("uColor", uColor.data());
renderer.DrawVertexRenderStruct(crosshairMesh);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
}
int Space::pickTargetId() const
{
// Use manually selected target if it's still alive and in range
if (manualTrackedTargetId >= 0) {
auto it = remotePlayerStates.find(manualTrackedTargetId);
if (it != remotePlayerStates.end() && !deadRemotePlayers.count(manualTrackedTargetId)) {
float d2 = (Environment::shipState.position - it->second.position).squaredNorm();
if (d2 <= TARGET_MAX_DIST_SQ) return manualTrackedTargetId;
}
// Target no longer valid — fall through to auto-pick
}
int bestId = -1;
float bestDistSq = 1e30f;
for (auto const& [id, st] : remotePlayerStates) {
if (deadRemotePlayers.count(id)) continue;
float d2 = (Environment::shipState.position - st.position).squaredNorm();
if (d2 > TARGET_MAX_DIST_SQ) continue;
if (d2 < bestDistSq) {
bestDistSq = d2;
bestId = id;
}
}
return bestId;
}
static Vector3f ForwardFromRotation(const Matrix3f& rot)
{
Vector3f localForward(0, 0, -1);
Vector3f worldForward = rot * localForward;
float len = worldForward.norm();
if (len > 1e-6f) worldForward /= len;
return worldForward;
}
static bool SolveLeadInterceptTime(
const Vector3f& shooterPos,
const Vector3f& shooterVel,
const Vector3f& targetPos,
const Vector3f& targetVel,
float projectileSpeed, // muzzle speed (например 60)
float& outT)
{
Vector3f r = targetPos - shooterPos;
Vector3f v = targetVel - shooterVel;
float S = projectileSpeed;
float a = v.dot(v) - S * S;
float b = 2.0f * r.dot(v);
float c = r.dot(r);
// Если a почти 0 -> линейный случай
if (std::abs(a) < 1e-6f) {
if (std::abs(b) < 1e-6f) return false; // нет решения
float t = -c / b;
if (t > 0.0f) { outT = t; return true; }
return false;
}
float disc = b * b - 4.0f * a * c;
if (disc < 0.0f) return false;
float sqrtDisc = std::sqrt(disc);
float t1 = (-b - sqrtDisc) / (2.0f * a);
float t2 = (-b + sqrtDisc) / (2.0f * a);
float t = 1e30f;
if (t1 > 0.0f) t = min(t, t1);
if (t2 > 0.0f) t = min(t, t2);
if (t >= 1e29f) return false;
outT = t;
return true;
}
static VertexDataStruct MakeRing2D(
float cx, float cy,
float innerR, float outerR,
float z,
int segments,
const Eigen::Vector4f& rgba)
{
VertexDataStruct v;
v.PositionData.reserve(segments * 6);
v.ColorData.reserve(segments * 6);
Vector3f rgb(rgba.x(), rgba.y(), rgba.z());
const float twoPi = 6.28318530718f;
for (int i = 0; i < segments; ++i) {
float a0 = twoPi * (float)i / (float)segments;
float a1 = twoPi * (float)(i + 1) / (float)segments;
float c0 = std::cos(a0), s0 = std::sin(a0);
float c1 = std::cos(a1), s1 = std::sin(a1);
Vector3f p0i(cx + innerR * c0, cy + innerR * s0, z);
Vector3f p0o(cx + outerR * c0, cy + outerR * s0, z);
Vector3f p1i(cx + innerR * c1, cy + innerR * s1, z);
Vector3f p1o(cx + outerR * c1, cy + outerR * s1, z);
// два треугольника (p0i,p0o,p1o) и (p0i,p1o,p1i)
v.PositionData.push_back(p0i);
v.PositionData.push_back(p0o);
v.PositionData.push_back(p1o);
v.PositionData.push_back(p0i);
v.PositionData.push_back(p1o);
v.PositionData.push_back(p1i);
for (int k = 0; k < 6; ++k) v.ColorData.push_back(rgb);
}
return v;
}
static VertexDataStruct MakeColoredRect2D(float cx, float cy, float hw, float hh, float z,
const Eigen::Vector4f& rgba)
{
VertexDataStruct v;
// 2 triangles
Vector3f p1{ cx - hw, cy - hh, z };
Vector3f p2{ cx - hw, cy + hh, z };
Vector3f p3{ cx + hw, cy + hh, z };
Vector3f p4{ cx + hw, cy - hh, z };
v.PositionData = { p1, p2, p3, p3, p4, p1 };
// defaultColor shader likely uses vColor (vec3), но нам нужен alpha.
// У тебя в Renderer есть RenderUniform4fv, но шейдер может брать vColor.
// Поэтому: сделаем ColorData vec3, а alpha будем задавать uniform'ом отдельно.
Vector3f rgb{ rgba.x(), rgba.y(), rgba.z() };
v.ColorData = { rgb, rgb, rgb, rgb, rgb, rgb };
// defaultColor vertex shader expects vNormal and vTexCoord; provide valid values
// so WebGL/GLSL doesn't get NaN from normalize(vec3(0,0,0)).
const Vector3f n{ 0.f, 0.f, 1.f };
v.NormalData = { n, n, n, n, n, n };
const Vector2f uv{ 0.f, 0.f };
v.TexCoordData = { uv, uv, uv, uv, uv, uv };
return v;
}
void Space::drawTargetHud()
{
if (!textRenderer) return;
// 1) выбираем цель
int targetIdNow = pickTargetId();
if (targetIdNow < 0) {
trackedTargetId = -1;
targetAcquireAnim = 0.f;
targetWasVisible = false;
return;
}
// если цель сменилась — сброс анимации “схлопывания”
if (trackedTargetId != targetIdNow) {
trackedTargetId = targetIdNow;
targetAcquireAnim = 0.0f;
targetWasVisible = false;
}
const ClientState& st = remotePlayerStates.at(trackedTargetId);
Vector3f shipWorld = st.position;
// Lead Indicator
// скорость пули (как в fireProjectiles)
const float projectileSpeed = PROJECTILE_VELOCITY;
// позиция вылета
Vector3f shooterPos = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.0f, 0.9f - 6.0f, 5.0f };
// скорость цели в мире (вектор)
// Vector3f shooterVel = ForwardFromRotation(Environment::shipState.rotation) * Environment::shipState.velocity;
float shooterSpeed = std::abs(Environment::shipState.velocity);
// В нашей физике линейная скорость корабля всегда направлена по его forward (-Z)
// Когда игрок наводится на lead indicator, forward (и скорость) становятся сонаправлены с выстрелом
// поэтому эффективная скорость снаряда в мире ≈ muzzle + shipSpeed.
const float effectiveProjectileSpeed = projectileSpeed + shooterSpeed;
Vector3f shooterVel = Vector3f::Zero(); // скорость уже учтена в effectiveProjectileSpeed
Vector3f targetVel = ForwardFromRotation(st.rotation) * st.velocity;
// ВАЖНО: remote state берется на now_ms - CLIENT_DELAY
// Значит shipWorld - это позиция ~0.5 сек назад.
// Для корректного lead нужно предсказать положение цели на сейчас
const float clientDelaySec = (float)CLIENT_DELAY / 1000.0f;
Vector3f targetPosNow = shipWorld + targetVel * clientDelaySec;
const float minTargetSpeed = 0.5f; // подобрать (в твоих единицах)
bool targetMoving = (targetVel.norm() > minTargetSpeed);
// альфа круга
float leadAlpha = targetMoving ? 1.0f : 0.5f;
Vector3f leadWorld = targetPosNow;
bool haveLead = false;
// Дистанцию лучше считать от реальной точки вылета
float distToTarget = (shooterPos - targetPosNow).norm();
// Максимальное время перехвата ограничиваем жизнью пули
const float projectileLifeSec = (float)PROJECTILE_LIFE / 1000.0f;
float maxLeadTime = std::clamp((distToTarget / effectiveProjectileSpeed) * 1.25f, 0.01f, projectileLifeSec * 0.98f);
if (!targetMoving) {
// Цель стоит: рисуем lead прямо на ней, но полупрозрачный
leadWorld = targetPosNow;
haveLead = true;
}
else {
float tLead = 0.0f;
// 1) Пытаемся “правильное” решение перехвата
bool ok = SolveLeadInterceptTime(shooterPos, shooterVel, targetPosNow, targetVel, effectiveProjectileSpeed, tLead);
// 2) Если решения нет / оно плохое — fallback (чтобы круг не пропадал при пролёте "вбок")
// Это ключевое изменение: lead всегда будет.
if (!ok || !(tLead > 0.0f) || tLead > maxLeadTime) {
tLead = std::clamp(distToTarget / effectiveProjectileSpeed, 0.05f, maxLeadTime);
}
leadWorld = targetPosNow + targetVel * tLead;
haveLead = true;
}
// Проекция цели (для рамок/стрелки)
float ndcX, ndcY, ndcZ, clipW;
if (!projectToNDC(shipWorld, ndcX, ndcY, ndcZ, clipW)) return;
bool behind = (clipW <= 0.0f);
bool onScreen = (!behind &&
ndcX >= -1.0f && ndcX <= 1.0f &&
ndcY >= -1.0f && ndcY <= 1.0f);
float dist = (Environment::shipState.position - shipWorld).norm();
float t = static_cast<float>(SDL_GetTicks64()) * 0.001f;
// Проекция Lead
float leadNdcX = 0.f, leadNdcY = 0.f, leadNdcZ = 0.f, leadClipW = 0.f;
bool leadOnScreen = false;
if (haveLead) {
if (projectToNDC(leadWorld, leadNdcX, leadNdcY, leadNdcZ, leadClipW) && leadClipW > 0.0f) {
leadOnScreen =
(leadNdcX >= -1.0f && leadNdcX <= 1.0f &&
leadNdcY >= -1.0f && leadNdcY <= 1.0f);
}
}
// Настройки HUD стилизация
Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный
float thickness = 2.0f; // толщина линий (px)
float z = 0.0f; // 2D слой
auto drawLeadRing2D = [&](float lx, float ly)
{
float distLead = (Environment::shipState.position - leadWorld).norm();
float r = 30.0f / (distLead * 0.01f + 1.0f);
r = std::clamp(r, 6.0f, 18.0f);
float thicknessPx = 2.5f;
float innerR = max(1.0f, r - thicknessPx);
float outerR = r + thicknessPx;
Eigen::Vector4f leadColor = enemyColor;
leadColor.w() = leadAlpha;
renderer.RenderUniform4fv("uColor", leadColor.data());
VertexDataStruct ring = MakeRing2D(lx, ly, innerR, outerR, 0.0f, 32, enemyColor);
hudTempMesh.AssignFrom(ring);
renderer.DrawVertexRenderStruct(hudTempMesh);
// вернуть цвет HUD обратно
renderer.RenderUniform4fv("uColor", enemyColor.data());
};
// Цель в кадре: рамки
if (onScreen)
{
// перевод NDC -> экран (в пикселях)
float sx = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth;
float sy = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight;
// анимация “снаружи внутрь”
// targetAcquireAnim растёт к 1, быстро (похоже на захват)
float dt = 1.0f / 60.0f; // у тебя нет dt в draw, берём константу, выглядит норм
targetAcquireAnim = min(1.0f, targetAcquireAnim + dt * 6.5f);
// базовый размер рамки в зависимости от дистанции (как у лейблов)
float size = 220.0f / (dist * 0.01f + 1.0f); // подстройка
size = std::clamp(size, 35.0f, 120.0f); // min/max
// “схлопывание”: сначала больше, потом ближе к кораблю
// expand 1.6 -> 1.0
float expand = 1.6f - 0.6f * targetAcquireAnim;
float half = size * expand;
float cornerLen = max(10.0f, half * 0.35f);
// точки углов
float left = sx - half;
float right = sx + half;
float bottom = sy - half;
float top = sy + half;
// рисуем 8 тонких прямоугольников (2 на угол)
auto drawBar = [&](float cx, float cy, float w, float h)
{
VertexDataStruct v = MakeColoredRect2D(cx, cy, w * 0.5f, h * 0.5f, z, enemyColor);
hudTempMesh.AssignFrom(v);
renderer.DrawVertexRenderStruct(hudTempMesh);
};
// включаем 2D режим
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glClear(GL_DEPTH_BUFFER_BIT);
renderer.shaderManager.PushShader("defaultColor");
renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.RenderUniform4fv("uColor", enemyColor.data());
// рамки
drawBar(left + cornerLen * 0.5f, top, cornerLen, thickness);
drawBar(left, top - cornerLen * 0.5f, thickness, cornerLen);
drawBar(right - cornerLen * 0.5f, top, cornerLen, thickness);
drawBar(right, top - cornerLen * 0.5f, thickness, cornerLen);
drawBar(left + cornerLen * 0.5f, bottom, cornerLen, thickness);
drawBar(left, bottom + cornerLen * 0.5f, thickness, cornerLen);
drawBar(right - cornerLen * 0.5f, bottom, cornerLen, thickness);
drawBar(right, bottom + cornerLen * 0.5f, thickness, cornerLen);
// LEAD — независимо от рамок: если его точка на экране, рисуем
if (haveLead && leadOnScreen) {
float lx = (leadNdcX * 0.5f + 0.5f) * Environment::projectionWidth;
float ly = (leadNdcY * 0.5f + 0.5f) * Environment::projectionHeight;
drawLeadRing2D(lx, ly);
}
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
targetWasVisible = true;
return;
}
// Цель вне экрана: стрелка
float dirX = ndcX;
float dirY = ndcY;
if (behind) {
dirX = -dirX;
dirY = -dirY;
}
float len = std::sqrt(dirX * dirX + dirY * dirY);
if (len < 1e-5f) return;
dirX /= len;
dirY /= len;
float marginNdc = 0.08f;
float maxX = 1.0f - marginNdc;
float maxY = 1.0f - marginNdc;
float tx = (std::abs(dirX) < 1e-6f) ? 1e9f : (maxX / std::abs(dirX));
float ty = (std::abs(dirY) < 1e-6f) ? 1e9f : (maxY / std::abs(dirY));
float k = min(tx, ty);
float edgeNdcX = dirX * k;
float edgeNdcY = dirY * k;
float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::projectionWidth;
float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::projectionHeight;
float bob = std::sin(t * 6.0f) * 6.0f;
edgeX += dirX * bob;
edgeY += dirY * bob;
float arrowLen = 26.0f;
float arrowWid = 14.0f;
float px = -dirY;
float py = dirX;
Vector3f tip{ edgeX + dirX * arrowLen, edgeY + dirY * arrowLen, z };
Vector3f left{ edgeX + px * (arrowWid * 0.5f), edgeY + py * (arrowWid * 0.5f), z };
Vector3f right{ edgeX - px * (arrowWid * 0.5f), edgeY - py * (arrowWid * 0.5f), z };
auto drawTri = [&](const Vector3f& a, const Vector3f& b, const Vector3f& c)
{
VertexDataStruct v;
v.PositionData = { a, b, c };
Vector3f rgb{ enemyColor.x(), enemyColor.y(), enemyColor.z() };
v.ColorData = { rgb, rgb, rgb };
// defaultColor vertex shader expects vNormal and vTexCoord (avoids NaN on WebGL).
const Vector3f n{ 0.f, 0.f, 1.f };
v.NormalData = { n, n, n };
const Vector2f uv{ 0.f, 0.f };
v.TexCoordData = { uv, uv, uv };
hudTempMesh.AssignFrom(v);
renderer.DrawVertexRenderStruct(hudTempMesh);
};
auto drawBar = [&](float cx, float cy, float w, float h)
{
VertexDataStruct v = MakeColoredRect2D(cx, cy, w * 0.5f, h * 0.5f, z, enemyColor);
hudTempMesh.AssignFrom(v);
renderer.DrawVertexRenderStruct(hudTempMesh);
};
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
renderer.shaderManager.PushShader("defaultColor");
renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.RenderUniform4fv("uColor", enemyColor.data());
// стрелка
drawTri(tip, left, right);
float tailLen = 14.0f;
float tailX = edgeX - dirX * 6.0f;
float tailY = edgeY - dirY * 6.0f;
drawBar(tailX, tailY, max(thickness, tailLen), thickness);
// LEAD — рисуем даже когда цель вне экрана (если lead точка на экране)
if (haveLead && leadOnScreen) {
float lx = (leadNdcX * 0.5f + 0.5f) * Environment::projectionWidth;
float ly = (leadNdcY * 0.5f + 0.5f) * Environment::projectionHeight;
drawLeadRing2D(lx, ly);
}
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
// дистанция около стрелки
{
std::string d = std::to_string((int)dist) + "m";
float tx = edgeX + px * 18.0f;
float ty = edgeY + py * 18.0f;
textRenderer->drawText(d, tx, ty, 0.6f, true, { 1.f, 0.f, 0.f, 1.f });
}
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
targetWasVisible = false;
}
void Space::updateSparkEmitters(float deltaMs)
{
// Local ship
SparkEmitter* sparkEmitterPtr;
if (Environment::shipState.shipType == 1) {
sparkEmitterPtr = &sparkEmitterCargo;
static std::vector<Vector3f> emissionPoints = { Vector3f(0, 0, 0), Vector3f(0, 0, 0) };
emissionPoints[0] = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.0, 2.8, -6.5 + 16.0 };
emissionPoints[1] = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.0, 1.5, -6.5 + 16.0 };
sparkEmitterPtr->setEmissionPoints(emissionPoints);
}
else {
sparkEmitterPtr = &sparkEmitter;
static std::vector<Vector3f> emissionPoints = { Vector3f(0, 0, 0), Vector3f(0, 0, 0) };
emissionPoints[0] = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ -0.9, 1.4 - 1.0, -8.5 + 16.0 };
emissionPoints[1] = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.9, 1.4 - 1.0, -8.5 + 16.0 };
sparkEmitterPtr->setEmissionPoints(emissionPoints);
}
sparkEmitterPtr->setIsActive(Environment::shipState.velocity > 0.1f);
sparkEmitterPtr->update(deltaMs);
// Remote ships
for (auto const& [id, playerState] : remotePlayerStates) {
if (deadRemotePlayers.count(id)) continue;
if (!remoteShipSparkEmitters.count(id)) {
remoteShipSparkEmitters.emplace(id, playerState.shipType == 1 ? sparkEmitterCargo : sparkEmitter);
}
auto& remEmitter = remoteShipSparkEmitters.at(id);
std::vector<Vector3f> remEmitPts(2);
if (playerState.shipType == 1) {
remEmitPts[0] = playerState.position + playerState.rotation * Vector3f{ 0.0f, -0.4f+2.8f, 8.4f };
remEmitPts[1] = playerState.position + playerState.rotation * Vector3f{ 0.0f, -0.4f+1.5f, 8.4f };
} else {
remEmitPts[0] = playerState.position + playerState.rotation * Vector3f{ -0.9f, -0.2,5.6 };
remEmitPts[1] = playerState.position + playerState.rotation * Vector3f{ 0.9f,-0.2,5.6 };
}
remEmitter.setEmissionPoints(remEmitPts);
remEmitter.setIsActive(playerState.velocity > 0.1f);
remEmitter.update(deltaMs);
}
}
void Space::prepareSparkEmittersForDraw()
{
sparkEmitter.prepareForDraw(true);
sparkEmitterCargo.prepareForDraw(true);
for (auto& [id, emitter] : remoteShipSparkEmitters) {
if (!deadRemotePlayers.count(id)) emitter.prepareForDraw(true);
}
explosionEmitter.prepareForDraw(false);
for (const auto& p : projectiles) {
if (p && p->isActive()) {
p->projectileEmitter.prepareForDraw(true);
}
}
}
void Space::drawShipSparkEmitters()
{
renderer.PushMatrix();
renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipState.position);
if (Environment::shipState.shipType == 1) {
sparkEmitterCargo.draw(renderer, Environment::zoom, Environment::width, Environment::height);
} else {
sparkEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
}
for (auto& [id, emitter] : remoteShipSparkEmitters) {
if (!deadRemotePlayers.count(id)) {
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipState.position);
emitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
renderer.PopMatrix();
}
}
renderer.PopMatrix();
}
void Space::processTickCount(int64_t newTickCount, int64_t delta) {
auto now_ms = newTickCount;
if (firePressed)
{
firePressed = false;
if (now_ms - lastProjectileFireTime >= static_cast<uint64_t>(projectileCooldownMs)) {
lastProjectileFireTime = now_ms;
const float projectileSpeed = PROJECTILE_VELOCITY;
this->fireProjectiles();
Eigen::Vector3f localForward = { 0, 0, -1 };
Eigen::Vector3f worldForward = (Environment::shipState.rotation * localForward).normalized();
Eigen::Vector3f centerPos = Environment::shipState.position +
Environment::shipState.rotation * Vector3f{ 0, 0.9f - 6.0f, 5.0f };
Eigen::Quaternionf q(Environment::shipState.rotation);
float speedToSend = projectileSpeed + Environment::shipState.velocity;
int shotCount = 2;
std::string fireMsg = "FIRE:" +
std::to_string(now_ms) + ":" +
std::to_string(centerPos.x()) + ":" +
std::to_string(centerPos.y()) + ":" +
std::to_string(centerPos.z()) + ":" +
std::to_string(q.w()) + ":" +
std::to_string(q.x()) + ":" +
std::to_string(q.y()) + ":" +
std::to_string(q.z()) + ":" +
std::to_string(speedToSend) + ":" +
std::to_string(shotCount);
networkClient->Send(fireMsg);
}
}
//Handle input:
if (newShipVelocity != Environment::shipState.selectedVelocity)
{
Environment::shipState.selectedVelocity = newShipVelocity;
std::string msg = "UPD:" + std::to_string(now_ms) + ":" + Environment::shipState.formPingMessageContent();
networkClient->Send(msg);
}
float discreteMag;
int discreteAngle;
if (Environment::tapDownHold) {
float diffx = Environment::tapDownCurrentPos(0) - Environment::tapDownStartPos(0);
float diffy = Environment::tapDownCurrentPos(1) - Environment::tapDownStartPos(1);
float rawMag = sqrtf(diffx * diffx + diffy * diffy);
float maxRadius = 200.0f; // Максимальный вынос джойстика
if (rawMag > 10.0f) { // Мертвая зона
// 1. Дискретизируем отклонение (0.0 - 1.0 с шагом 0.1)
float normalizedMag = min(rawMag / maxRadius, 1.0f);
discreteMag = std::round(normalizedMag * 10.0f) / 10.0f;
// 2. Дискретизируем угол (0-359 градусов)
// atan2 возвращает радианы, переводим в градусы
float radians = atan2f(diffy, diffx);
discreteAngle = static_cast<int>(radians * 180.0f / M_PI);
if (discreteAngle < 0) discreteAngle += 360;
}
else
{
discreteAngle = -1;
discreteMag = 0.0f;
}
}
else
{
discreteAngle = -1;
discreteMag = 0.0f;
}
if (discreteAngle != Environment::shipState.discreteAngle || discreteMag != Environment::shipState.discreteMag) {
Environment::shipState.discreteAngle = discreteAngle;
Environment::shipState.discreteMag = discreteMag;
std::string msg = "UPD:" + std::to_string(now_ms) + ":" + Environment::shipState.formPingMessageContent();
networkClient->Send(msg);
//std::cout << "Sending: " << msg << std::endl;
}
long long leftoverDelta = delta;
while (leftoverDelta > 0)
{
long long miniDelta = 50;
Environment::shipState.simulate_physics(miniDelta);
leftoverDelta -= miniDelta;
}
Environment::inverseShipMatrix = Environment::shipState.rotation.inverse();
static float pingTimer = 0.0f;
pingTimer += delta;
if (pingTimer >= 1000.0f) {
std::string pingMsg = "UPD:" + std::to_string(now_ms) + ":" + Environment::shipState.formPingMessageContent();
networkClient->Send(pingMsg);
std::cout << "Sending: " << pingMsg << std::endl;
pingTimer = 0.0f;
}
auto latestRemotePlayers = networkClient->getRemotePlayers();
std::chrono::system_clock::time_point nowRoundedWithDelay{ std::chrono::milliseconds(newTickCount - CLIENT_DELAY) };
for (auto const& [id, remotePlayer] : latestRemotePlayers) {
if (networkClient && id == networkClient->GetClientId()) {
continue;
}
if (!remotePlayer.canFetchClientStateAtTime(nowRoundedWithDelay))
{
continue;
}
ClientState playerState = remotePlayer.fetchClientStateAtTime(nowRoundedWithDelay);
remotePlayerStates[id] = playerState;
}
updateSparkEmitters(static_cast<float>(delta));
for (auto& p : projectiles) {
if (p && p->isActive()) {
p->update(static_cast<float>(delta), renderer);
}
}
for (const auto& p : projectiles) {
if (p && p->isActive()) {
Vector3f worldPos = p->getPosition();
p->projectileEmitter.resetEmissionPoints({ worldPos });
p->projectileEmitter.update(static_cast<float>(delta));
}
}
explosionEmitter.update(static_cast<float>(delta));
if (showExplosion) {
uint64_t now = SDL_GetTicks64();
if (lastExplosionTime != 0 && now - lastExplosionTime >= explosionDurationMs) {
showExplosion = false;
explosionEmitter.setEmissionPoints(std::vector<Vector3f>());
explosionEmitter.setUseWorldSpace(false);
}
}
if (shipAlive) {
float distToSurface = planetObject.distanceToPlanetSurface(Environment::shipState.position);
if (distToSurface <= 0.0f) {
Vector3f dir = (Environment::shipState.position - PlanetData::PLANET_CENTER_OFFSET).normalized();
Vector3f collisionPoint = PlanetData::PLANET_CENTER_OFFSET + dir * PlanetData::PLANET_RADIUS;
Environment::shipState.position = PlanetData::PLANET_CENTER_OFFSET + dir * (PlanetData::PLANET_RADIUS + shipCollisionRadius + 0.1f);
shipAlive = false;
gameOver = true;
Environment::shipState.selectedVelocity = 0;
Environment::shipState.velocity = 0.0f;
newShipVelocity = 0;
showExplosion = true;
explosionEmitter.setUseWorldSpace(true);
explosionEmitter.setEmissionPoints(std::vector<Vector3f>{ collisionPoint });
explosionEmitter.emit();
lastExplosionTime = SDL_GetTicks64();
std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n";
clearPlayerListIfVisible();
menuManager.showGameOver(this->playerScore);
}
else {
bool stoneCollided = false;
int collidedTriIdx = -1;
Vector3f collidedStonePos = Vector3f{ 0.0f, 0.0f, 0.0f };
float collidedStoneRadius = 0.0f;
for (int triIdx : planetObject.triangleIndicesToDraw) {
if (triIdx < 0 || triIdx >= static_cast<int>(planetObject.planetStones.allInstances.size()))
continue;
if (planetObject.planetStones.statuses.size() <= static_cast<size_t>(triIdx))
continue;
if (planetObject.planetStones.statuses[triIdx] != ChunkStatus::Live)
continue;
const auto& instances = planetObject.planetStones.allInstances[triIdx];
for (const auto& inst : instances) {
Vector3f stoneWorld = inst.position;
Vector3f diff = Environment::shipState.position - stoneWorld;
float maxScale = (std::max)({ inst.scale(0), inst.scale(1), inst.scale(2) });
float stoneRadius = StoneParams::BASE_SCALE * maxScale * 0.9f;
float thresh = shipCollisionRadius + stoneRadius;
if (diff.squaredNorm() <= thresh * thresh) {
stoneCollided = true;
collidedTriIdx = triIdx;
collidedStonePos = stoneWorld;
collidedStoneRadius = stoneRadius;
break;
}
}
if (stoneCollided) break;
}
if (stoneCollided) {
Vector3f away = (Environment::shipState.position - collidedStonePos);
if (away.squaredNorm() <= 1e-6f) {
away = Vector3f{ 0.0f, 1.0f, 0.0f };
}
away.normalize();
Environment::shipState.position = collidedStonePos + away * (collidedStoneRadius + shipCollisionRadius + 0.1f);
shipAlive = false;
gameOver = true;
Environment::shipState.velocity = 0.0f;
showExplosion = true;
explosionEmitter.setUseWorldSpace(true);
explosionEmitter.setEmissionPoints(std::vector<Vector3f>{ collidedStonePos });
explosionEmitter.emit();
lastExplosionTime = SDL_GetTicks64();
std::cerr << "GAME OVER: collision with stone on triangle " << collidedTriIdx << std::endl;
if (collidedTriIdx >= 0 && collidedTriIdx < static_cast<int>(planetObject.stonesToRender.size())) {
planetObject.stonesToRender[collidedTriIdx].data.PositionData.clear();
planetObject.stonesToRender[collidedTriIdx].vao.reset();
planetObject.stonesToRender[collidedTriIdx].positionVBO.reset();
planetObject.stonesToRender[collidedTriIdx].normalVBO.reset();
planetObject.stonesToRender[collidedTriIdx].tangentVBO.reset();
planetObject.stonesToRender[collidedTriIdx].binormalVBO.reset();
planetObject.stonesToRender[collidedTriIdx].colorVBO.reset();
planetObject.stonesToRender[collidedTriIdx].texCoordVBO.reset();
}
if (collidedTriIdx >= 0 && collidedTriIdx < static_cast<int>(planetObject.planetStones.statuses.size())) {
planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty;
}
clearPlayerListIfVisible();
menuManager.showGameOver(this->playerScore);
}
}
}
planetObject.update(static_cast<float>(delta));
// update velocity text
if (shipAlive && !gameOver) {
auto velocityTv = menuManager.uiManager.findTextView("velocityText");
if (velocityTv) {
std::string velocityStr = "Velocity: " + std::to_string(static_cast<int>(Environment::shipState.velocity));
menuManager.uiManager.setText("velocityText", velocityStr);
}
bool canPickup = false;
if (Environment::shipState.shipType == 1 && Environment::shipState.velocity < 0.1f) {
for (size_t i = 0; i < boxCoordsArr.size(); ++i) {
if (i >= boxAlive.size() || !boxAlive[i]) continue;
Vector3f boxWorld = boxCoordsArr[i].pos + Vector3f{ 0.f, 0.f, 45000.f };
float distSq = (Environment::shipState.position - boxWorld).squaredNorm();
if (distSq <= BOX_PICKUP_RADIUS * BOX_PICKUP_RADIUS) {
canPickup = true;
break;
}
}
}
if (canPickup != nearPickupBox) {
nearPickupBox = canPickup;
if (auto btn = menuManager.uiManager.findButton("takeButton"))
btn->state = canPickup ? ButtonState::Normal : ButtonState::Disabled;
}
}
if (playerScore != prevPlayerScore)
{
prevPlayerScore = playerScore;
menuManager.uiManager.setText("gameScoreText", "Score: " + std::to_string(playerScore));
}
}
void Space::fireProjectiles() {
std::vector<Vector3f> localOffsets = {
Vector3f{ -1.5f, 0.9f - 6.f, 5.0f },
Vector3f{ 1.5f, 0.9f - 6.f, 5.0f }
};
const float projectileSpeed = PROJECTILE_VELOCITY;
const float lifeMs = PROJECTILE_LIFE;
const float size = 0.5f;
Vector3f localForward = { 0,0,-1 };
Vector3f worldForward = (Environment::shipState.rotation * localForward).normalized();
for (const auto& lo : localOffsets) {
Vector3f worldPos = Environment::shipState.position + Environment::shipState.rotation * lo;
Vector3f worldVel = worldForward * (projectileSpeed + Environment::shipState.velocity);
for (auto& p : projectiles) {
if (!p->isActive()) {
p->init(worldPos, worldVel, lifeMs, size, projectileTexture, renderer);
p->projectileEmitter = SparkEmitter(projectileEmitter);
break;
}
}
}
}
void Space::update() {
if (networkClient) {
if (networkClient->IsConnected()) {
wasConnectedToServer = true;
}
else if (wasConnectedToServer && shipAlive && !gameOver) {
wasConnectedToServer = false;
shipAlive = false;
gameOver = true;
Environment::shipState.velocity = 0.0f;
std::cout << "Client: Lost connection to server\n";
clearPlayerListIfVisible();
menuManager.showConnectionLost();
}
auto pending = networkClient->getPendingProjectiles();
if (!pending.empty()) {
const float projectileSpeed = PROJECTILE_VELOCITY;
const float lifeMs = PROJECTILE_LIFE;
const float size = 0.5f;
for (const auto& pi : pending) {
const std::vector<Vector3f> localOffsets = {
Vector3f{ -1.5f, 0.9f - 6.0f, 5.0f },
Vector3f{ 1.5f, 0.9f - 6.0f, 5.0f }
};
Vector3f localForward = { 0, 0, -1 };
Vector3f worldForward = pi.rotation * localForward;
float len = worldForward.norm();
if (len <= 1e-6f) {
continue;
}
worldForward /= len;
Vector3f baseVel = worldForward * pi.velocity;
for (const auto& off : localOffsets) {
Vector3f shotPos = pi.position + (pi.rotation * off);
for (auto& p : projectiles) {
if (!p->isActive()) {
p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer);
p->projectileEmitter = SparkEmitter(projectileEmitter);
break;
}
}
}
}
}
// Обработка событий смерти, присланных сервером
auto deaths = networkClient->getPendingDeaths();
if (!deaths.empty()) {
int localId = networkClient->GetClientId();
std::cout << "Client: Received " << deaths.size() << " death events" << std::endl;
for (const auto& d : deaths) {
std::cout << "Client: Processing death - target=" << d.targetId
<< ", killer=" << d.killerId << ", pos=("
<< d.position.x() << ", " << d.position.y() << ", " << d.position.z() << ")" << std::endl;
showExplosion = true;
explosionEmitter.setUseWorldSpace(true);
explosionEmitter.setEmissionPoints(std::vector<Vector3f>{ d.position });
explosionEmitter.emit();
lastExplosionTime = SDL_GetTicks64();
std::cout << "Client: Explosion emitted at (" << d.position.x() << ", "
<< d.position.y() << ", " << d.position.z() << ")" << std::endl;
if (d.targetId == localId) {
std::cout << "Client: Local ship destroyed!" << std::endl;
shipAlive = false;
gameOver = true;
Environment::shipState.velocity = 0.0f;
clearPlayerListIfVisible();
menuManager.showGameOver(this->playerScore);
}
else {
deadRemotePlayers.insert(d.targetId);
if (d.targetId == manualTrackedTargetId) manualTrackedTargetId = -1;
std::cout << "Marked remote player " << d.targetId << " as dead" << std::endl;
}
if (d.killerId == localId) {
this->playerScore += 1;
std::cout << "Client: Increased local score to " << this->playerScore << std::endl;
}
}
rebuildPlayerListIfVisible();
}
auto respawns = networkClient->getPendingRespawns();
if (!respawns.empty()) {
for (const auto& respawnId : respawns) {
deadRemotePlayers.erase(respawnId);
auto it = remotePlayerStates.find(respawnId);
if (it != remotePlayerStates.end()) {
it->second.position = Vector3f{ 0.f, 0.f, 45000.f };
it->second.velocity = 0.0f;
it->second.rotation = Eigen::Matrix3f::Identity();
}
std::cout << "Client: Remote player " << respawnId << " respawned, removed from dead list" << std::endl;
}
}
rebuildPlayerListIfVisible();
auto disconnects = networkClient->getPendingDisconnects();
for (int pid : disconnects) {
remotePlayerStates.erase(pid);
deadRemotePlayers.erase(pid);
remoteShipSparkEmitters.erase(pid);
if (trackedTargetId == pid) {
trackedTargetId = -1;
targetAcquireAnim = 0.f;
}
if (pid == manualTrackedTargetId) manualTrackedTargetId = -1;
std::cout << "Client: Remote player " << pid << " left the game, removed from scene\n";
}
rebuildPlayerListIfVisible();
auto boxDestructions = networkClient->getPendingBoxDestructions();
if (!boxDestructions.empty()) {
std::cout << "Game: Received " << boxDestructions.size() << " box destruction events" << std::endl;
for (const auto& destruction : boxDestructions) {
int idx = destruction.boxIndex;
if (idx >= 0 && idx < (int)boxCoordsArr.size()) {
if (boxAlive[idx]) {
boxAlive[idx] = false;
boxRenderArr[idx].data.PositionData.clear();
boxRenderArr[idx].vao.reset();
boxRenderArr[idx].positionVBO.reset();
boxRenderArr[idx].texCoordVBO.reset();
showExplosion = true;
explosionEmitter.setUseWorldSpace(true);
explosionEmitter.setEmissionPoints(std::vector<Vector3f>{ destruction.position });
explosionEmitter.emit();
lastExplosionTime = SDL_GetTicks64();
std::cout << "Game: Box " << idx << " destroyed by player "
<< destruction.destroyedBy << std::endl;
}
}
}
}
}
auto boxPickups = networkClient->getPendingBoxPickups();
for (const auto& pickup : boxPickups) {
int idx = pickup.boxIndex;
if (idx >= 0 && idx < (int)boxCoordsArr.size() && idx < (int)boxAlive.size()) {
if (boxAlive[idx]) {
boxAlive[idx] = false;
boxRenderArr[idx].data.PositionData.clear();
boxRenderArr[idx].vao.reset();
boxRenderArr[idx].positionVBO.reset();
boxRenderArr[idx].texCoordVBO.reset();
std::cout << "Client: Box " << idx << " picked up by player " << pickup.pickedUpBy << "\n";
}
}
}
auto boxRespawns = networkClient->getPendingBoxRespawns();
for (const auto& respawn : boxRespawns) {
int idx = respawn.boxIndex;
if (idx >= 0 && idx < (int)boxCoordsArr.size()) {
boxCoordsArr[idx].pos = respawn.position;
boxCoordsArr[idx].m = respawn.rotation;
boxAlive[idx] = true;
boxRenderArr[idx].AssignFrom(boxBase);
boxRenderArr[idx].RefreshVBO();
std::cout << "Client: Box " << idx << " respawned" << std::endl;
}
}
}
void Space::handleDown(int mx, int my)
{
if (playerListVisible) return;
Environment::tapDownHold = true;
Environment::tapDownStartPos(0) = mx;
Environment::tapDownStartPos(1) = my;
Environment::tapDownCurrentPos(0) = mx;
Environment::tapDownCurrentPos(1) = my;
}
void Space::handleUp(int mx, int my)
{
Environment::tapDownHold = false;
}
void Space::handleMotion(int mx, int my)
{
if (playerListVisible) return;
if (Environment::tapDownHold) {
Environment::tapDownCurrentPos(0) = mx;
Environment::tapDownCurrentPos(1) = my;
}
}
void Space::clearTextRendererCache()
{
if (textRenderer) {
textRenderer->ClearCache();
}
}
/*
std::string Space::formPingMessageContent()
{
Eigen::Quaternionf q(Environment::shipMatrix);
std::string pingMsg = std::to_string(Environment::shipPosition.x()) + ":"
+ std::to_string(Environment::shipPosition.y()) + ":"
+ std::to_string(Environment::shipPosition.z()) + ":"
+ std::to_string(q.w()) + ":"
+ std::to_string(q.x()) + ":"
+ std::to_string(q.y()) + ":"
+ std::to_string(q.z()) + ":"
+ std::to_string(Environment::currentAngularVelocity.x()) + ":"
+ std::to_string(Environment::currentAngularVelocity.y()) + ":"
+ std::to_string(Environment::currentAngularVelocity.z()) + ":"
+ std::to_string(Environment::shipVelocity) + ":"
+ std::to_string(Environment::shipSelectedVelocity) + ":"
+ std::to_string(Environment::lastSentMagnitude) + ":" // Используем те же static переменные из блока ROT
+ std::to_string(Environment::lastSentAngle);
return pingMsg;
}*/
std::shared_ptr<UiNode> Space::buildPlayerListRoot()
{
const float btnW = 400;
const float btnH = 50.0f;
// Collect alive remote players
std::vector<std::pair<int, std::string>> players;
for (auto& kv : remotePlayerStates) {
if (!deadRemotePlayers.count(kv.first))
players.push_back({ kv.first, kv.second.nickname });
}
// Root: FrameLayout match_parent x match_parent
auto root = std::make_shared<UiNode>();
root->name = "playerListRoot";
root->layoutType = LayoutType::Frame;
root->width = -1.0f; // match_parent
root->height = -1.0f;
// List container: LinearLayout vertical, centered
float listH = btnH * (float)players.size();
auto listNode = std::make_shared<UiNode>();
listNode->name = "playerList";
listNode->layoutType = LayoutType::Linear;
listNode->orientation = Orientation::Vertical;
listNode->width = btnW;
listNode->height = listH;
listNode->layoutSettings.hGravity = HorizontalGravity::Center;
listNode->layoutSettings.vGravity = VerticalGravity::Center;
for (auto& [pid, nick] : players) {
auto btnNode = std::make_shared<UiNode>();
btnNode->name = "playerBtn_" + std::to_string(pid);
btnNode->layoutType = LayoutType::Frame;
btnNode->width = btnW;
btnNode->height = btnH;
auto tb = std::make_shared<UiTextButton>();
tb->name = btnNode->name;
tb->text = nick;
tb->fontSize = 20;
tb->color = { 1.f, 1.f, 1.f, 1.f };
tb->textCentered = true;
tb->textRenderer = std::make_unique<TextRenderer>();
if (!tb->textRenderer->init(renderer, tb->fontPath, tb->fontSize, CONST_ZIP_FILE)) {
std::cerr << "Failed to init TextRenderer for TextField: " << tb->name << std::endl;
}
//tb->texNormal = std::make_unique<Texture>(CreateTextureDataFromPng("resources/black.png", ""));
btnNode->textButton = tb;
/*auto button = std::make_shared<UiButton>();
button->name = "Hello";
button->texNormal = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", ""));
btnNode->button = button;*/
listNode->children.push_back(btnNode);
}
// Backdrop: invisible full-screen TextButton — placed LAST so player buttons get priority
auto backdropNode = std::make_shared<UiNode>();
backdropNode->name = "playerListBackdrop";
backdropNode->layoutType = LayoutType::Frame;
backdropNode->width = -1.0f;
backdropNode->height = -1.0f;
auto backdropTb = std::make_shared<UiTextButton>();
backdropTb->name = "playerListBackdrop";
backdropNode->textButton = backdropTb;
/*
auto backgroundNode = std::make_shared<UiNode>();
backgroundNode->name = "playerListBackground";
backgroundNode->layoutType = LayoutType::Frame;
backgroundNode->width = btnW;
backgroundNode->height = listH;
backgroundNode->layoutSettings.hGravity = HorizontalGravity::Center;
backgroundNode->layoutSettings.vGravity = VerticalGravity::Center;
auto backdropImage = std::make_shared<UiStaticImage>();
backdropImage->name = "playerListBackgroundImage";
backdropImage->texture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/blue_transparent.png", ""));
backgroundNode->staticImage = backdropImage;
*/
root->children.push_back(listNode);
root->children.push_back(backdropNode);
//root->children.push_back(backgroundNode);
return root;
}
void Space::buildAndShowPlayerList()
{
auto listRoot = buildPlayerListRoot();
menuManager.uiManager.pushMenuFromSavedRoot(listRoot);
menuManager.uiManager.updateAllLayouts();
playerListVisible = true;
for (auto& kv : remotePlayerStates) {
if (deadRemotePlayers.count(kv.first)) continue;
int pid = kv.first;
std::string btnName = "playerBtn_" + std::to_string(pid);
menuManager.uiManager.setTextButtonCallback(btnName, [this, pid](const std::string&) {
manualTrackedTargetId = pid;
closePlayerList();
});
}
menuManager.uiManager.setTextButtonCallback("playerListBackdrop", [this](const std::string&) {
closePlayerList();
});
}
void Space::closePlayerList()
{
menuManager.uiManager.popMenu();
menuManager.uiManager.updateAllLayouts();
playerListVisible = false;
}
void Space::rebuildPlayerListIfVisible()
{
if (!playerListVisible) return;
auto listRoot = buildPlayerListRoot();
menuManager.uiManager.replaceRoot(listRoot);
for (auto& kv : remotePlayerStates) {
if (deadRemotePlayers.count(kv.first)) continue;
int pid = kv.first;
std::string btnName = "playerBtn_" + std::to_string(pid);
menuManager.uiManager.setTextButtonCallback(btnName, [this, pid](const std::string&) {
manualTrackedTargetId = pid;
closePlayerList();
});
}
menuManager.uiManager.setTextButtonCallback("playerListBackdrop", [this](const std::string&) {
closePlayerList();
});
}
void Space::clearPlayerListIfVisible()
{
if (!playerListVisible) return;
menuManager.uiManager.clearMenuStack();
playerListVisible = false;
}
} // namespace ZL