From 84a5e888a06e317309358910c76cd2548a92c6e5 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Mon, 9 Mar 2026 18:36:04 +0300 Subject: [PATCH] Added pickup boxes --- resources/button_take.png | 3 + resources/button_take_disabled.png | 3 + resources/button_take_pressed.png | 3 + resources/config/ui.json | 21 ++++- src/MenuManager.cpp | 11 ++- src/MenuManager.h | 1 + src/Space.cpp | 76 ++++++++++++++++++- src/Space.h | 1 + src/network/ClientState.h | 1 + src/network/LocalClient.cpp | 114 ++++++++++++++++++++++++++++ src/network/LocalClient.h | 5 ++ src/network/NetworkInterface.h | 13 ++++ src/network/WebSocketClientBase.cpp | 51 +++++++++++++ src/network/WebSocketClientBase.h | 4 + 14 files changed, 300 insertions(+), 7 deletions(-) create mode 100644 resources/button_take.png create mode 100644 resources/button_take_disabled.png create mode 100644 resources/button_take_pressed.png diff --git a/resources/button_take.png b/resources/button_take.png new file mode 100644 index 0000000..847af37 --- /dev/null +++ b/resources/button_take.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:41a37af9ee1a8ad10466e4b84ec8067ce1d5a1be39534c020fafcca47f187b93 +size 15512 diff --git a/resources/button_take_disabled.png b/resources/button_take_disabled.png new file mode 100644 index 0000000..b3eb843 --- /dev/null +++ b/resources/button_take_disabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be03ea373477852cde848215befb76e8a048089122ea603151c29b15bff1d736 +size 11520 diff --git a/resources/button_take_pressed.png b/resources/button_take_pressed.png new file mode 100644 index 0000000..8031be0 --- /dev/null +++ b/resources/button_take_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f24f7239e1820b0f43a8250c48e116f0d405ee3d936589608f308b85d21993b9 +size 27390 diff --git a/resources/config/ui.json b/resources/config/ui.json index 65d8225..dcd4df6 100644 --- a/resources/config/ui.json +++ b/resources/config/ui.json @@ -64,7 +64,7 @@ "vertical_gravity": "bottom", "textures": { "normal": "resources/button_minus.png", - "hover": "resources/button_minus_pressed.png", + "hover": "resources/button_minus.png", "pressed": "resources/button_minus_pressed.png", "disabled" : "resources/button_minus_disabled.png" } @@ -81,10 +81,27 @@ "vertical_gravity": "bottom", "textures": { "normal": "resources/button_plus.png", - "hover": "resources/button_plus_pressed.png", + "hover": "resources/button_plus.png", "pressed": "resources/button_plus_pressed.png", "disabled" : "resources/button_plus_disabled.png" } + }, + { + "type": "Button", + "name": "takeButton", + "x": -20, + "y": 320, + "width": 150, + "height": 150, + "border" : 20, + "horizontal_gravity": "right", + "vertical_gravity": "bottom", + "textures": { + "normal": "resources/button_take.png", + "hover": "resources/button_take.png", + "pressed": "resources/button_take_pressed.png", + "disabled" : "resources/button_take_disabled.png" + } } ] } diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index 6b8c2f4..bf928ad 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -136,6 +136,7 @@ namespace ZL { uiManager.replaceRoot(gameplayRoot); uiManager.findButton("minusButton")->state = ButtonState::Disabled; + if (auto btn = uiManager.findButton("takeButton")) btn->state = ButtonState::Disabled; auto velocityTv = uiManager.findTextView("velocityText"); @@ -144,8 +145,6 @@ namespace ZL { velocityTv->rect.y = static_cast(Environment::height) - velocityTv->rect.h - 10.0f; } - //uiManager.startAnimationOnNode("backgroundNode", "bgScroll"); - uiManager.setButtonPressCallback("shootButton", [this](const std::string&) { if (onFirePressed) onFirePressed(); }); @@ -180,13 +179,19 @@ namespace ZL { } if (onVelocityChanged) onVelocityChanged(newVel); }); + + uiManager.setButtonPressCallback("takeButton", [this](const std::string&) { + if (onTakeButtonPressed) onTakeButtonPressed(); + }); + + /* uiManager.setSliderCallback("velocitySlider", [this](const std::string&, float value) { int newVel = static_cast(roundf(value * 10)); if (newVel > 2) newVel = 2; if (newVel != Environment::shipState.selectedVelocity) { if (onVelocityChanged) onVelocityChanged(newVel); } - }); + });*/ } // ── State: GameOver ────────────────────────────────────────────────────── diff --git a/src/MenuManager.h b/src/MenuManager.h index 083492b..1a52b62 100644 --- a/src/MenuManager.h +++ b/src/MenuManager.h @@ -70,6 +70,7 @@ namespace ZL { std::function onRestartPressed; std::function onVelocityChanged; std::function onFirePressed; + std::function onTakeButtonPressed; std::function onSingleplayerPressed; std::function onMultiplayerPressed; }; diff --git a/src/Space.cpp b/src/Space.cpp index 3869c84..98f953f 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -317,6 +317,26 @@ namespace ZL firePressed = true; }; + 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(i); + } + } + if (bestIdx >= 0) { + networkClient->Send("BOX_PICKUP:" + std::to_string(bestIdx)); + } + }; + 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); @@ -395,7 +415,7 @@ namespace ZL boxLabels.clear(); boxLabels.reserve(boxCoordsArr.size()); for (size_t i = 0; i < boxCoordsArr.size(); ++i) { - boxLabels.push_back("Box " + std::to_string(i + 1)); + boxLabels.push_back("Box " + std::to_string(i)); } if (!cfgLoaded) @@ -658,6 +678,11 @@ namespace ZL 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; } } @@ -1776,6 +1801,24 @@ namespace ZL std::string velocityStr = "Velocity: " + std::to_string(static_cast(Environment::shipState.velocity)); menuManager.uiManager.setText("velocityText", velocityStr); } + + bool canPickup = false; + if (Environment::shipState.shipType == 1) { + 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; + } } } @@ -1890,7 +1933,7 @@ namespace ZL if (d.killerId == localId) { this->playerScore += 1; std::cout << "Client: Increased local score to " << this->playerScore << std::endl; - + } } } @@ -1952,6 +1995,35 @@ namespace ZL } } } + + 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) diff --git a/src/Space.h b/src/Space.h index 05395a3..f94d839 100644 --- a/src/Space.h +++ b/src/Space.h @@ -113,6 +113,7 @@ namespace ZL { const uint64_t explosionDurationMs = 500; bool serverBoxesApplied = false; + bool nearPickupBox = false; static constexpr float MAX_DIST_SQ = 10000.f * 10000.f; static constexpr float FADE_START = 6000.f; diff --git a/src/network/ClientState.h b/src/network/ClientState.h index d78903d..12c1c32 100644 --- a/src/network/ClientState.h +++ b/src/network/ClientState.h @@ -34,6 +34,7 @@ constexpr float PROJECTILE_LIFE = 15000.f; //ms const float projectileHitRadius = 1.5f * 5; const float boxCollisionRadius = 2.0f * 5; const float shipCollisionRadius = 15.0f * 5; +const float BOX_PICKUP_RADIUS = shipCollisionRadius * 5; const float npcCollisionRadius = 5.0f * 5; uint32_t fnv1a_hash(const std::string& data); diff --git a/src/network/LocalClient.cpp b/src/network/LocalClient.cpp index 4125593..f1e9cab 100644 --- a/src/network/LocalClient.cpp +++ b/src/network/LocalClient.cpp @@ -274,6 +274,24 @@ namespace ZL { std::cout << "LocalClient: Box " << boxIdx << " destroyed by projectile from player " << projectiles[projIdx].shooterId << std::endl; + // Respawn box + { + std::random_device rd2; + std::mt19937 gen2(rd2()); + std::uniform_real_distribution angleDist(0.f, static_cast(M_PI * 2.0)); + Eigen::Vector3f newPos = generateRespawnBoxPos(static_cast(boxIdx)); + Eigen::Vector3f axis = Eigen::Vector3f::Random().normalized(); + Eigen::Matrix3f newRot = Eigen::AngleAxisf(angleDist(gen2), axis).toRotationMatrix(); + serverBoxes[boxIdx].position = newPos; + serverBoxes[boxIdx].rotation = newRot; + serverBoxes[boxIdx].destroyed = false; + BoxRespawnInfo respawn; + respawn.boxIndex = static_cast(boxIdx); + respawn.position = newPos; + respawn.rotation = newRot; + pendingBoxRespawns.push_back(respawn); + } + if (std::find(projIndicesToRemove.begin(), projIndicesToRemove.end(), (int)projIdx) == projIndicesToRemove.end()) { projIndicesToRemove.push_back(static_cast(projIdx)); @@ -352,11 +370,62 @@ namespace ZL { std::cout << "LocalClient: Box " << bi << " destroyed by ship collision with player " << GetClientId() << std::endl; + + // Respawn box + { + std::random_device rd2; + std::mt19937 gen2(rd2()); + std::uniform_real_distribution angleDist(0.f, static_cast(M_PI * 2.0)); + Eigen::Vector3f newPos = generateRespawnBoxPos(static_cast(bi)); + Eigen::Vector3f axis = Eigen::Vector3f::Random().normalized(); + Eigen::Matrix3f newRot = Eigen::AngleAxisf(angleDist(gen2), axis).toRotationMatrix(); + serverBoxes[bi].position = newPos; + serverBoxes[bi].rotation = newRot; + serverBoxes[bi].destroyed = false; + BoxRespawnInfo respawn; + respawn.boxIndex = static_cast(bi); + respawn.position = newPos; + respawn.rotation = newRot; + pendingBoxRespawns.push_back(respawn); + } } } } } + Eigen::Vector3f LocalClient::generateRespawnBoxPos(int skipIdx) { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution dist(-1000.f, 1000.f); + + for (int attempt = 0; attempt < 500; ++attempt) { + Eigen::Vector3f cand(dist(gen), dist(gen), dist(gen)); + bool safe = true; + for (int i = 0; i < (int)serverBoxes.size(); ++i) { + if (i == skipIdx) continue; + if (serverBoxes[i].destroyed) continue; + if ((cand - serverBoxes[i].position).squaredNorm() < 9.f) { + safe = false; + break; + } + } + if (safe) return cand; + } + return Eigen::Vector3f(dist(gen), dist(gen), dist(gen)); + } + + std::vector LocalClient::getPendingBoxPickups() { + auto result = pendingBoxPickups; + pendingBoxPickups.clear(); + return result; + } + + std::vector LocalClient::getPendingBoxRespawns() { + auto result = pendingBoxRespawns; + pendingBoxRespawns.clear(); + return result; + } + void LocalClient::Send(const std::string& message) { auto parts = [](const std::string& s, char delimiter) { std::vector tokens; @@ -372,6 +441,51 @@ namespace ZL { std::string type = parts[0]; + if (type == "BOX_PICKUP") { + if (parts.size() < 2) return; + if (!hasLocalPlayerState || localPlayerState.shipType != 1) return; + + int boxIdx = -1; + try { boxIdx = std::stoi(parts[1]); } catch (...) { return; } + + if (boxIdx < 0 || boxIdx >= (int)serverBoxes.size()) return; + if (serverBoxes[boxIdx].destroyed) return; + + Eigen::Vector3f boxWorld = serverBoxes[boxIdx].position + Eigen::Vector3f(0.f, 0.f, 45000.f); + float distSq = (localPlayerState.position - boxWorld).squaredNorm(); + if (distSq > BOX_PICKUP_RADIUS * BOX_PICKUP_RADIUS) return; + + serverBoxes[boxIdx].destroyed = true; + + BoxPickedUpInfo pickup; + pickup.boxIndex = boxIdx; + pickup.pickedUpBy = GetClientId(); + pendingBoxPickups.push_back(pickup); + + std::cout << "LocalClient: Box " << boxIdx << " picked up by player " << GetClientId() << "\n"; + + // Respawn box at new position + { + std::random_device rd2; + std::mt19937 gen2(rd2()); + std::uniform_real_distribution angleDist(0.f, static_cast(M_PI * 2.0)); + Eigen::Vector3f newPos = generateRespawnBoxPos(boxIdx); + Eigen::Vector3f axis = Eigen::Vector3f::Random().normalized(); + Eigen::Matrix3f newRot = Eigen::AngleAxisf(angleDist(gen2), axis).toRotationMatrix(); + serverBoxes[boxIdx].position = newPos; + serverBoxes[boxIdx].rotation = newRot; + serverBoxes[boxIdx].destroyed = false; + + BoxRespawnInfo respawn; + respawn.boxIndex = boxIdx; + respawn.position = newPos; + respawn.rotation = newRot; + pendingBoxRespawns.push_back(respawn); + std::cout << "LocalClient: Box " << boxIdx << " respawned after pickup\n"; + } + return; + } + if (type == "FIRE") { if (parts.size() < 10) return; diff --git a/src/network/LocalClient.h b/src/network/LocalClient.h index 438dede..ce683fd 100644 --- a/src/network/LocalClient.h +++ b/src/network/LocalClient.h @@ -42,6 +42,8 @@ namespace ZL { std::vector pendingProjectiles; std::vector pendingDeaths; std::vector pendingBoxDestructions; + std::vector pendingBoxPickups; + std::vector pendingBoxRespawns; std::vector pendingRespawns; uint64_t lastUpdateMs = 0; @@ -56,6 +58,7 @@ namespace ZL { void initializeNPCs(); void updateNPCs(); Eigen::Vector3f generateRandomPosition(); + Eigen::Vector3f generateRespawnBoxPos(int skipIdx); public: void Connect(const std::string& host, uint16_t port) override; @@ -79,6 +82,8 @@ namespace ZL { } std::vector getPendingBoxDestructions() override; + std::vector getPendingBoxPickups() override; + std::vector getPendingBoxRespawns() override; void setLocalPlayerState(const ClientState& state) { localPlayerState = state; diff --git a/src/network/NetworkInterface.h b/src/network/NetworkInterface.h index a5dd5c9..ba60170 100644 --- a/src/network/NetworkInterface.h +++ b/src/network/NetworkInterface.h @@ -30,6 +30,17 @@ namespace ZL { int destroyedBy = -1; }; + struct BoxPickedUpInfo { + int boxIndex = -1; + int pickedUpBy = -1; + }; + + struct BoxRespawnInfo { + int boxIndex = -1; + Eigen::Vector3f position = Eigen::Vector3f::Zero(); + Eigen::Matrix3f rotation = Eigen::Matrix3f::Identity(); + }; + class INetworkClient { public: virtual ~INetworkClient() = default; @@ -50,6 +61,8 @@ namespace ZL { virtual std::vector getPendingRespawns() = 0; virtual int GetClientId() const { return -1; } virtual std::vector getPendingBoxDestructions() = 0; + virtual std::vector getPendingBoxPickups() { return {}; } + virtual std::vector getPendingBoxRespawns() { return {}; } virtual int64_t getTimeOffset() const { return 0; } virtual std::vector getPendingDisconnects() { return {}; } diff --git a/src/network/WebSocketClientBase.cpp b/src/network/WebSocketClientBase.cpp index 6be0c77..7c37d3f 100644 --- a/src/network/WebSocketClientBase.cpp +++ b/src/network/WebSocketClientBase.cpp @@ -129,6 +129,45 @@ namespace ZL { return; } + if (msg.rfind("BOX_PICKED_UP:", 0) == 0) { + if (parts.size() >= 3) { + try { + BoxPickedUpInfo pickup; + pickup.boxIndex = std::stoi(parts[1]); + pickup.pickedUpBy = std::stoi(parts[2]); + pendingBoxPickups_.push_back(pickup); + std::cout << "Client: Received BOX_PICKED_UP box=" << pickup.boxIndex + << " by player " << pickup.pickedUpBy << std::endl; + } + catch (...) {} + } + return; + } + + if (msg.rfind("BOX_RESPAWN:", 0) == 0) { + if (parts.size() >= 9) { + try { + BoxRespawnInfo respawn; + respawn.boxIndex = std::stoi(parts[1]); + float px = std::stof(parts[2]); + float py = std::stof(parts[3]); + float pz = std::stof(parts[4]); + Eigen::Quaternionf q( + std::stof(parts[5]), + std::stof(parts[6]), + std::stof(parts[7]), + std::stof(parts[8]) + ); + respawn.position = Eigen::Vector3f(px, py, pz); + respawn.rotation = q.toRotationMatrix(); + pendingBoxRespawns_.push_back(respawn); + std::cout << "Client: Received BOX_RESPAWN box=" << respawn.boxIndex << std::endl; + } + catch (...) {} + } + return; + } + if (msg.rfind("BOX_DESTROYED:", 0) == 0) { //auto parts = split(msg, ':'); if (parts.size() >= 7) { @@ -369,6 +408,18 @@ namespace ZL { return copy; } + std::vector WebSocketClientBase::getPendingBoxPickups() { + std::vector copy; + copy.swap(pendingBoxPickups_); + return copy; + } + + std::vector WebSocketClientBase::getPendingBoxRespawns() { + std::vector copy; + copy.swap(pendingBoxRespawns_); + return copy; + } + std::vector WebSocketClientBase::getPendingDisconnects() { std::vector copy; copy.swap(pendingDisconnects_); diff --git a/src/network/WebSocketClientBase.h b/src/network/WebSocketClientBase.h index f74c9b5..a324a70 100644 --- a/src/network/WebSocketClientBase.h +++ b/src/network/WebSocketClientBase.h @@ -20,6 +20,8 @@ namespace ZL { std::vector pendingDeaths_; std::vector pendingRespawns_; std::vector pendingBoxDestructions_; + std::vector pendingBoxPickups_; + std::vector pendingBoxRespawns_; std::vector pendingDisconnects_; int clientId = -1; int64_t timeOffset = 0; @@ -50,6 +52,8 @@ namespace ZL { std::vector getPendingDeaths() override; std::vector getPendingRespawns() override; std::vector getPendingBoxDestructions() override; + std::vector getPendingBoxPickups() override; + std::vector getPendingBoxRespawns() override; std::vector getPendingDisconnects() override; std::vector getPendingSpawns(); int getClientId() const { return clientId; }