Added pickup boxes

This commit is contained in:
Vladislav Khorev 2026-03-09 18:36:04 +03:00
parent 86a9f38c3b
commit 84a5e888a0
14 changed files with 300 additions and 7 deletions

BIN
resources/button_take.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/button_take_disabled.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/button_take_pressed.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -64,7 +64,7 @@
"vertical_gravity": "bottom", "vertical_gravity": "bottom",
"textures": { "textures": {
"normal": "resources/button_minus.png", "normal": "resources/button_minus.png",
"hover": "resources/button_minus_pressed.png", "hover": "resources/button_minus.png",
"pressed": "resources/button_minus_pressed.png", "pressed": "resources/button_minus_pressed.png",
"disabled" : "resources/button_minus_disabled.png" "disabled" : "resources/button_minus_disabled.png"
} }
@ -81,10 +81,27 @@
"vertical_gravity": "bottom", "vertical_gravity": "bottom",
"textures": { "textures": {
"normal": "resources/button_plus.png", "normal": "resources/button_plus.png",
"hover": "resources/button_plus_pressed.png", "hover": "resources/button_plus.png",
"pressed": "resources/button_plus_pressed.png", "pressed": "resources/button_plus_pressed.png",
"disabled" : "resources/button_plus_disabled.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"
}
} }
] ]
} }

View File

@ -136,6 +136,7 @@ namespace ZL {
uiManager.replaceRoot(gameplayRoot); uiManager.replaceRoot(gameplayRoot);
uiManager.findButton("minusButton")->state = ButtonState::Disabled; uiManager.findButton("minusButton")->state = ButtonState::Disabled;
if (auto btn = uiManager.findButton("takeButton")) btn->state = ButtonState::Disabled;
auto velocityTv = uiManager.findTextView("velocityText"); auto velocityTv = uiManager.findTextView("velocityText");
@ -144,8 +145,6 @@ namespace ZL {
velocityTv->rect.y = static_cast<float>(Environment::height) - velocityTv->rect.h - 10.0f; velocityTv->rect.y = static_cast<float>(Environment::height) - velocityTv->rect.h - 10.0f;
} }
//uiManager.startAnimationOnNode("backgroundNode", "bgScroll");
uiManager.setButtonPressCallback("shootButton", [this](const std::string&) { uiManager.setButtonPressCallback("shootButton", [this](const std::string&) {
if (onFirePressed) onFirePressed(); if (onFirePressed) onFirePressed();
}); });
@ -180,13 +179,19 @@ namespace ZL {
} }
if (onVelocityChanged) onVelocityChanged(newVel); if (onVelocityChanged) onVelocityChanged(newVel);
}); });
uiManager.setButtonPressCallback("takeButton", [this](const std::string&) {
if (onTakeButtonPressed) onTakeButtonPressed();
});
/*
uiManager.setSliderCallback("velocitySlider", [this](const std::string&, float value) { uiManager.setSliderCallback("velocitySlider", [this](const std::string&, float value) {
int newVel = static_cast<int>(roundf(value * 10)); int newVel = static_cast<int>(roundf(value * 10));
if (newVel > 2) newVel = 2; if (newVel > 2) newVel = 2;
if (newVel != Environment::shipState.selectedVelocity) { if (newVel != Environment::shipState.selectedVelocity) {
if (onVelocityChanged) onVelocityChanged(newVel); if (onVelocityChanged) onVelocityChanged(newVel);
} }
}); });*/
} }
// ── State: GameOver ────────────────────────────────────────────────────── // ── State: GameOver ──────────────────────────────────────────────────────

View File

@ -70,6 +70,7 @@ namespace ZL {
std::function<void()> onRestartPressed; std::function<void()> onRestartPressed;
std::function<void(float)> onVelocityChanged; std::function<void(float)> onVelocityChanged;
std::function<void()> onFirePressed; std::function<void()> onFirePressed;
std::function<void()> onTakeButtonPressed;
std::function<void(const std::string&, int)> onSingleplayerPressed; std::function<void(const std::string&, int)> onSingleplayerPressed;
std::function<void(const std::string&, int)> onMultiplayerPressed; std::function<void(const std::string&, int)> onMultiplayerPressed;
}; };

View File

@ -317,6 +317,26 @@ namespace ZL
firePressed = true; 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<int>(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 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); bool cfgLoaded2 = sparkEmitterCargo.loadFromJsonFile("resources/config/spark_config_cargo.json", renderer, CONST_ZIP_FILE);
sparkEmitter.setIsActive(false); sparkEmitter.setIsActive(false);
@ -395,7 +415,7 @@ namespace ZL
boxLabels.clear(); boxLabels.clear();
boxLabels.reserve(boxCoordsArr.size()); boxLabels.reserve(boxCoordsArr.size());
for (size_t i = 0; i < boxCoordsArr.size(); ++i) { 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) if (!cfgLoaded)
@ -658,6 +678,11 @@ namespace ZL
for (size_t i = 0; i < n; ++i) { for (size_t i = 0; i < n; ++i) {
if (destroyedFlags[i]) boxAlive[i] = false; // destroyed => не рисуем 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; serverBoxesApplied = true;
} }
} }
@ -1776,6 +1801,24 @@ namespace ZL
std::string velocityStr = "Velocity: " + std::to_string(static_cast<int>(Environment::shipState.velocity)); std::string velocityStr = "Velocity: " + std::to_string(static_cast<int>(Environment::shipState.velocity));
menuManager.uiManager.setText("velocityText", velocityStr); 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) { if (d.killerId == localId) {
this->playerScore += 1; this->playerScore += 1;
std::cout << "Client: Increased local score to " << this->playerScore << std::endl; 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) void Space::handleDown(int mx, int my)

View File

@ -113,6 +113,7 @@ namespace ZL {
const uint64_t explosionDurationMs = 500; const uint64_t explosionDurationMs = 500;
bool serverBoxesApplied = false; bool serverBoxesApplied = false;
bool nearPickupBox = false;
static constexpr float MAX_DIST_SQ = 10000.f * 10000.f; static constexpr float MAX_DIST_SQ = 10000.f * 10000.f;
static constexpr float FADE_START = 6000.f; static constexpr float FADE_START = 6000.f;

View File

@ -34,6 +34,7 @@ constexpr float PROJECTILE_LIFE = 15000.f; //ms
const float projectileHitRadius = 1.5f * 5; const float projectileHitRadius = 1.5f * 5;
const float boxCollisionRadius = 2.0f * 5; const float boxCollisionRadius = 2.0f * 5;
const float shipCollisionRadius = 15.0f * 5; const float shipCollisionRadius = 15.0f * 5;
const float BOX_PICKUP_RADIUS = shipCollisionRadius * 5;
const float npcCollisionRadius = 5.0f * 5; const float npcCollisionRadius = 5.0f * 5;
uint32_t fnv1a_hash(const std::string& data); uint32_t fnv1a_hash(const std::string& data);

View File

@ -274,6 +274,24 @@ namespace ZL {
std::cout << "LocalClient: Box " << boxIdx << " destroyed by projectile from player " std::cout << "LocalClient: Box " << boxIdx << " destroyed by projectile from player "
<< projectiles[projIdx].shooterId << std::endl; << projectiles[projIdx].shooterId << std::endl;
// Respawn box
{
std::random_device rd2;
std::mt19937 gen2(rd2());
std::uniform_real_distribution<float> angleDist(0.f, static_cast<float>(M_PI * 2.0));
Eigen::Vector3f newPos = generateRespawnBoxPos(static_cast<int>(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<int>(boxIdx);
respawn.position = newPos;
respawn.rotation = newRot;
pendingBoxRespawns.push_back(respawn);
}
if (std::find(projIndicesToRemove.begin(), projIndicesToRemove.end(), (int)projIdx) if (std::find(projIndicesToRemove.begin(), projIndicesToRemove.end(), (int)projIdx)
== projIndicesToRemove.end()) { == projIndicesToRemove.end()) {
projIndicesToRemove.push_back(static_cast<int>(projIdx)); projIndicesToRemove.push_back(static_cast<int>(projIdx));
@ -352,11 +370,62 @@ namespace ZL {
std::cout << "LocalClient: Box " << bi << " destroyed by ship collision with player " std::cout << "LocalClient: Box " << bi << " destroyed by ship collision with player "
<< GetClientId() << std::endl; << GetClientId() << std::endl;
// Respawn box
{
std::random_device rd2;
std::mt19937 gen2(rd2());
std::uniform_real_distribution<float> angleDist(0.f, static_cast<float>(M_PI * 2.0));
Eigen::Vector3f newPos = generateRespawnBoxPos(static_cast<int>(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<int>(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<float> 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<BoxPickedUpInfo> LocalClient::getPendingBoxPickups() {
auto result = pendingBoxPickups;
pendingBoxPickups.clear();
return result;
}
std::vector<BoxRespawnInfo> LocalClient::getPendingBoxRespawns() {
auto result = pendingBoxRespawns;
pendingBoxRespawns.clear();
return result;
}
void LocalClient::Send(const std::string& message) { void LocalClient::Send(const std::string& message) {
auto parts = [](const std::string& s, char delimiter) { auto parts = [](const std::string& s, char delimiter) {
std::vector<std::string> tokens; std::vector<std::string> tokens;
@ -372,6 +441,51 @@ namespace ZL {
std::string type = parts[0]; 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<float> angleDist(0.f, static_cast<float>(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 (type == "FIRE") {
if (parts.size() < 10) return; if (parts.size() < 10) return;

View File

@ -42,6 +42,8 @@ namespace ZL {
std::vector<ProjectileInfo> pendingProjectiles; std::vector<ProjectileInfo> pendingProjectiles;
std::vector<DeathInfo> pendingDeaths; std::vector<DeathInfo> pendingDeaths;
std::vector<BoxDestroyedInfo> pendingBoxDestructions; std::vector<BoxDestroyedInfo> pendingBoxDestructions;
std::vector<BoxPickedUpInfo> pendingBoxPickups;
std::vector<BoxRespawnInfo> pendingBoxRespawns;
std::vector<int> pendingRespawns; std::vector<int> pendingRespawns;
uint64_t lastUpdateMs = 0; uint64_t lastUpdateMs = 0;
@ -56,6 +58,7 @@ namespace ZL {
void initializeNPCs(); void initializeNPCs();
void updateNPCs(); void updateNPCs();
Eigen::Vector3f generateRandomPosition(); Eigen::Vector3f generateRandomPosition();
Eigen::Vector3f generateRespawnBoxPos(int skipIdx);
public: public:
void Connect(const std::string& host, uint16_t port) override; void Connect(const std::string& host, uint16_t port) override;
@ -79,6 +82,8 @@ namespace ZL {
} }
std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override; std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override;
std::vector<BoxPickedUpInfo> getPendingBoxPickups() override;
std::vector<BoxRespawnInfo> getPendingBoxRespawns() override;
void setLocalPlayerState(const ClientState& state) { void setLocalPlayerState(const ClientState& state) {
localPlayerState = state; localPlayerState = state;

View File

@ -30,6 +30,17 @@ namespace ZL {
int destroyedBy = -1; 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 { class INetworkClient {
public: public:
virtual ~INetworkClient() = default; virtual ~INetworkClient() = default;
@ -50,6 +61,8 @@ namespace ZL {
virtual std::vector<int> getPendingRespawns() = 0; virtual std::vector<int> getPendingRespawns() = 0;
virtual int GetClientId() const { return -1; } virtual int GetClientId() const { return -1; }
virtual std::vector<BoxDestroyedInfo> getPendingBoxDestructions() = 0; virtual std::vector<BoxDestroyedInfo> getPendingBoxDestructions() = 0;
virtual std::vector<BoxPickedUpInfo> getPendingBoxPickups() { return {}; }
virtual std::vector<BoxRespawnInfo> getPendingBoxRespawns() { return {}; }
virtual int64_t getTimeOffset() const { return 0; } virtual int64_t getTimeOffset() const { return 0; }
virtual std::vector<int> getPendingDisconnects() { return {}; } virtual std::vector<int> getPendingDisconnects() { return {}; }

View File

@ -129,6 +129,45 @@ namespace ZL {
return; 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) { if (msg.rfind("BOX_DESTROYED:", 0) == 0) {
//auto parts = split(msg, ':'); //auto parts = split(msg, ':');
if (parts.size() >= 7) { if (parts.size() >= 7) {
@ -369,6 +408,18 @@ namespace ZL {
return copy; return copy;
} }
std::vector<BoxPickedUpInfo> WebSocketClientBase::getPendingBoxPickups() {
std::vector<BoxPickedUpInfo> copy;
copy.swap(pendingBoxPickups_);
return copy;
}
std::vector<BoxRespawnInfo> WebSocketClientBase::getPendingBoxRespawns() {
std::vector<BoxRespawnInfo> copy;
copy.swap(pendingBoxRespawns_);
return copy;
}
std::vector<int> WebSocketClientBase::getPendingDisconnects() { std::vector<int> WebSocketClientBase::getPendingDisconnects() {
std::vector<int> copy; std::vector<int> copy;
copy.swap(pendingDisconnects_); copy.swap(pendingDisconnects_);

View File

@ -20,6 +20,8 @@ namespace ZL {
std::vector<DeathInfo> pendingDeaths_; std::vector<DeathInfo> pendingDeaths_;
std::vector<int> pendingRespawns_; std::vector<int> pendingRespawns_;
std::vector<BoxDestroyedInfo> pendingBoxDestructions_; std::vector<BoxDestroyedInfo> pendingBoxDestructions_;
std::vector<BoxPickedUpInfo> pendingBoxPickups_;
std::vector<BoxRespawnInfo> pendingBoxRespawns_;
std::vector<int> pendingDisconnects_; std::vector<int> pendingDisconnects_;
int clientId = -1; int clientId = -1;
int64_t timeOffset = 0; int64_t timeOffset = 0;
@ -50,6 +52,8 @@ namespace ZL {
std::vector<DeathInfo> getPendingDeaths() override; std::vector<DeathInfo> getPendingDeaths() override;
std::vector<int> getPendingRespawns() override; std::vector<int> getPendingRespawns() override;
std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override; std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override;
std::vector<BoxPickedUpInfo> getPendingBoxPickups() override;
std::vector<BoxRespawnInfo> getPendingBoxRespawns() override;
std::vector<int> getPendingDisconnects() override; std::vector<int> getPendingDisconnects() override;
std::vector<ClientState> getPendingSpawns(); std::vector<ClientState> getPendingSpawns();
int getClientId() const { return clientId; } int getClientId() const { return clientId; }