Compare commits
10 Commits
5cca2413fe
...
e8f1786a2a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8f1786a2a | ||
|
|
e30f8a4c70 | ||
|
|
f8642efa23 | ||
| 5da5a754fe | |||
| fb1b0a1c2e | |||
|
|
74c2f786a1 | ||
|
|
5216965496 | ||
|
|
ad7294ceea | ||
|
|
3893038d9a | ||
|
|
cb2e8318e7 |
21
resources/config/crosshair_config.json
Normal file
21
resources/config/crosshair_config.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"enabled": true,
|
||||
|
||||
"referenceResolution": [1280, 720],
|
||||
|
||||
"color": [1.0, 1.0, 1.0],
|
||||
"cl_crosshairalpha": 1.0,
|
||||
"cl_crosshairthickness": 2.0,
|
||||
|
||||
"centerGapPx": 10.0,
|
||||
|
||||
"top": {
|
||||
"lengthPx": 14.0,
|
||||
"angleDeg": 90.0
|
||||
},
|
||||
|
||||
"arms": [
|
||||
{ "lengthPx": 20.0, "angleDeg": 210.0 },
|
||||
{ "lengthPx": 20.0, "angleDeg": 330.0 }
|
||||
]
|
||||
}
|
||||
@ -3,102 +3,155 @@
|
||||
"type": "LinearLayout",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"orientation": "vertical",
|
||||
"spacing": 20,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"children": [
|
||||
{
|
||||
"type": "TextView",
|
||||
"name": "titleText",
|
||||
"x": 300,
|
||||
"y": 100,
|
||||
"width": 1320,
|
||||
"height": 100,
|
||||
"text": "Multiplayer",
|
||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
||||
"fontSize": 72,
|
||||
"color": [1, 1, 1, 1],
|
||||
"centered": true
|
||||
"type": "Button",
|
||||
"name": "langButton",
|
||||
"x": 1100,
|
||||
"y": 580,
|
||||
"width": 142,
|
||||
"height": 96,
|
||||
"textures": {
|
||||
"normal": "resources/main_menu/lang.png",
|
||||
"hover": "resources/main_menu/lang.png",
|
||||
"pressed": "resources/main_menu/lang.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "TextView",
|
||||
"name": "serverLabel",
|
||||
"x": 400,
|
||||
"y": 250,
|
||||
"width": 1120,
|
||||
"height": 50,
|
||||
"text": "Enter server name or IP:",
|
||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
||||
"fontSize": 32,
|
||||
"color": [1, 1, 1, 1],
|
||||
"centered": false
|
||||
"type": "Button",
|
||||
"name": "titleBtn",
|
||||
"x": 512,
|
||||
"y": 500,
|
||||
"width": 254,
|
||||
"height": 35,
|
||||
"textures": {
|
||||
"normal": "resources/multiplayer_menu/title.png",
|
||||
"hover": "resources/multiplayer_menu/title.png",
|
||||
"pressed": "resources/multiplayer_menu/title.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "subtitle",
|
||||
"x": 596.5,
|
||||
"y": 470,
|
||||
"width": 87,
|
||||
"height": 11,
|
||||
"textures": {
|
||||
"normal": "resources/multiplayer_menu/JoinServer.png",
|
||||
"hover": "resources/multiplayer_menu/JoinServer.png",
|
||||
"pressed": "resources/multiplayer_menu/JoinServer.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "subtitleBtn",
|
||||
"x": 450,
|
||||
"y": 445,
|
||||
"width": 94,
|
||||
"height": 9,
|
||||
"textures": {
|
||||
"normal": "resources/multiplayer_menu/ServerName.png",
|
||||
"hover": "resources/multiplayer_menu/ServerName.png",
|
||||
"pressed": "resources/multiplayer_menu/ServerName.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "TextField",
|
||||
"name": "serverInputField",
|
||||
"x": 400,
|
||||
"y": 320,
|
||||
"width": 1120,
|
||||
"height": 60,
|
||||
"x": 449,
|
||||
"y": 390,
|
||||
"width": 382,
|
||||
"height": 56,
|
||||
"placeholder": "Enter server name or IP",
|
||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
||||
"fontSize": 28,
|
||||
"fontSize": 16,
|
||||
"maxLength": 256,
|
||||
"color": [1, 1, 1, 1],
|
||||
"placeholderColor": [0.6, 0.6, 0.6, 1],
|
||||
"backgroundColor": [0.15, 0.15, 0.15, 1],
|
||||
"borderColor": [0.7, 0.7, 0.7, 1]
|
||||
"color": [122, 156, 198, 1],
|
||||
"placeholderColor": [122, 156, 198, 1],
|
||||
"backgroundColor": [15, 29, 51, 1],
|
||||
"borderColor": [15, 29, 51, 1]
|
||||
},
|
||||
{
|
||||
"type": "LinearLayout",
|
||||
"x": 400,
|
||||
"y": 450,
|
||||
"width": 1120,
|
||||
"height": 80,
|
||||
"orientation": "horizontal",
|
||||
"spacing": 30,
|
||||
"children": [
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "connectButton",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 530,
|
||||
"height": 80,
|
||||
"x": 449,
|
||||
"y": 350,
|
||||
"width": 382,
|
||||
"height": 56,
|
||||
"textures": {
|
||||
"normal": "resources/main_menu/single.png",
|
||||
"hover": "resources/main_menu/single.png",
|
||||
"pressed": "resources/main_menu/single.png"
|
||||
"normal": "resources/multiplayer_menu/Filledbuttons.png",
|
||||
"hover": "resources/multiplayer_menu/Filledbuttons.png",
|
||||
"pressed": "resources/multiplayer_menu/Filledbuttons.png"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "backButton",
|
||||
"x": 449,
|
||||
"y": 280,
|
||||
"width": 382,
|
||||
"height": 56,
|
||||
"textures": {
|
||||
"normal": "resources/multiplayer_menu/Backbutton.png",
|
||||
"hover": "resources/multiplayer_menu/Backbutton.png",
|
||||
"pressed": "resources/multiplayer_menu/Backbutton.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "backButton",
|
||||
"x": 590,
|
||||
"y": 0,
|
||||
"width": 530,
|
||||
"height": 80,
|
||||
"name": "AvailableServers",
|
||||
"x": 450,
|
||||
"y": 240,
|
||||
"width": 139,
|
||||
"height": 9,
|
||||
"textures": {
|
||||
"normal": "resources/main_menu/exit.png",
|
||||
"hover": "resources/main_menu/exit.png",
|
||||
"pressed": "resources/main_menu/exit.png"
|
||||
"normal": "resources/multiplayer_menu/AvailableServers.png",
|
||||
"hover": "resources/multiplayer_menu/AvailableServers.png",
|
||||
"pressed": "resources/multiplayer_menu/AvailableServers.png"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "TextView",
|
||||
"name": "statusText",
|
||||
"x": 400,
|
||||
"y": 580,
|
||||
"width": 1120,
|
||||
"height": 50,
|
||||
"text": "Ready to connect",
|
||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
||||
"fontSize": 24,
|
||||
"color": [0.8, 0.8, 0.8, 1],
|
||||
"centered": false
|
||||
"type": "Button",
|
||||
"name": "SerButton",
|
||||
"x": 436.5,
|
||||
"y": 170,
|
||||
"width": 407,
|
||||
"height": 62,
|
||||
"textures": {
|
||||
"normal": "resources/multiplayer_menu/Button.png",
|
||||
"hover": "resources/multiplayer_menu/Button.png",
|
||||
"pressed": "resources/multiplayer_menu/Button.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "SerButton2",
|
||||
"x": 436.5,
|
||||
"y": 88,
|
||||
"width": 407,
|
||||
"height": 62,
|
||||
"textures": {
|
||||
"normal": "resources/multiplayer_menu/Button2.png",
|
||||
"hover": "resources/multiplayer_menu/Button2.png",
|
||||
"pressed": "resources/multiplayer_menu/Button2.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "SerButton3",
|
||||
"x": 436.5,
|
||||
"y": 6,
|
||||
"width": 407,
|
||||
"height": 62,
|
||||
"textures": {
|
||||
"normal": "resources/multiplayer_menu/Button3.png",
|
||||
"hover": "resources/multiplayer_menu/Button3.png",
|
||||
"pressed": "resources/multiplayer_menu/Button3.png"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
64
resources/config/ship_selection_menu.json
Normal file
64
resources/config/ship_selection_menu.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"root": {
|
||||
"name": "shipSelectionRoot",
|
||||
"type": "node",
|
||||
"children": [
|
||||
|
||||
{
|
||||
"type": "TextField",
|
||||
"name": "nicknameInput",
|
||||
"x": 400,
|
||||
"y": 150,
|
||||
"width": 400,
|
||||
"height": 50,
|
||||
"placeholder": "Enter your nickname",
|
||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
||||
"fontSize": 16,
|
||||
"maxLength": 256,
|
||||
"color": [122, 156, 198, 1],
|
||||
"placeholderColor": [122, 156, 198, 1],
|
||||
"backgroundColor": [15, 29, 51, 1],
|
||||
"borderColor": [15, 29, 51, 1]
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "spaceshipButton",
|
||||
"x": 300,
|
||||
"y": 320,
|
||||
"width": 200,
|
||||
"height": 80,
|
||||
"textures": {
|
||||
"normal": "resources/multiplayer_menu/JoinServer.png",
|
||||
"hover": "resources/multiplayer_menu/JoinServer.png",
|
||||
"pressed": "resources/multiplayer_menu/JoinServer.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "cargoshipButton",
|
||||
"x": 700,
|
||||
"y": 320,
|
||||
"width": 200,
|
||||
"height": 80,
|
||||
"textures": {
|
||||
"normal": "resources/multiplayer_menu/JoinServer.png",
|
||||
"hover": "resources/multiplayer_menu/JoinServer.png",
|
||||
"pressed": "resources/multiplayer_menu/JoinServer.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "backButton",
|
||||
"x": 449,
|
||||
"y": 280,
|
||||
"width": 382,
|
||||
"height": 56,
|
||||
"textures": {
|
||||
"normal": "resources/multiplayer_menu/Backbutton.png",
|
||||
"hover": "resources/multiplayer_menu/Backbutton.png",
|
||||
"pressed": "resources/multiplayer_menu/Backbutton.png"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
resources/multiplayer_menu/AvailableServers.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/AvailableServers.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/Backbutton.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/Backbutton.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/Button.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/Button.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/Button2.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/Button2.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/Button3.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/Button3.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/Filledbuttons.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/Filledbuttons.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/JoinServer.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/JoinServer.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/ServerName.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/ServerName.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/title.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/title.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -93,6 +93,9 @@ class Session : public std::enable_shared_from_this<Session> {
|
||||
public:
|
||||
ClientStateInterval timedClientStates;
|
||||
|
||||
std::string nickname = "Player";
|
||||
int shipType = 0;
|
||||
|
||||
explicit Session(tcp::socket&& socket, int id)
|
||||
: ws_(std::move(socket)), id_(id) {
|
||||
}
|
||||
@ -253,7 +256,6 @@ private:
|
||||
|
||||
void process_message(const std::string& msg) {
|
||||
if (!IsMessageValid(msg)) {
|
||||
// Логируем попытку подмены и просто выходим из обработки
|
||||
std::cout << "[Security] Invalid packet hash. Dropping message: " << msg << std::endl;
|
||||
return;
|
||||
}
|
||||
@ -266,7 +268,40 @@ private:
|
||||
|
||||
std::string type = parts[0];
|
||||
|
||||
if (type == "UPD") {
|
||||
if (type == "JOIN") {
|
||||
std::string nick = "Player";
|
||||
int sType = 0;
|
||||
if (parts.size() >= 2) nick = parts[1];
|
||||
if (parts.size() >= 3) {
|
||||
try { sType = std::stoi(parts[2]); }
|
||||
catch (...) { sType = 0; }
|
||||
}
|
||||
|
||||
this->nickname = nick;
|
||||
this->shipType = sType;
|
||||
|
||||
std::cout << "Server: Player " << id_ << " joined as [" << nick << "] shipType=" << sType << std::endl;
|
||||
|
||||
std::string info = "PLAYERINFO:" + std::to_string(id_) + ":" + nick + ":" + std::to_string(sType);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
||||
for (auto& session : g_sessions) {
|
||||
if (session->get_id() == this->id_) continue;
|
||||
session->send_message(info);
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
||||
for (auto& session : g_sessions) {
|
||||
if (session->get_id() == this->id_) continue;
|
||||
std::string otherInfo = "PLAYERINFO:" + std::to_string(session->get_id()) + ":" + session->nickname + ":" + std::to_string(session->shipType);
|
||||
// Отправляем именно новому клиенту
|
||||
this->send_message(otherInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type == "UPD") {
|
||||
{
|
||||
std::lock_guard<std::mutex> gd(g_dead_mutex);
|
||||
if (g_dead_players.find(id_) != g_dead_players.end()) {
|
||||
@ -284,20 +319,48 @@ private:
|
||||
};
|
||||
receivedState.lastUpdateServerTime = uptime_timepoint;
|
||||
receivedState.handle_full_sync(parts, 2);
|
||||
receivedState.nickname = this->nickname;
|
||||
receivedState.shipType = this->shipType;
|
||||
timedClientStates.add_state(receivedState);
|
||||
|
||||
retranslateMessage(cleanMessage);
|
||||
}
|
||||
else if (parts[0] == "RESPAWN") {
|
||||
else if (type == "RESPAWN") {
|
||||
{
|
||||
std::lock_guard<std::mutex> gd(g_dead_mutex);
|
||||
g_dead_players.erase(id_);
|
||||
}
|
||||
|
||||
{
|
||||
auto now_tp = std::chrono::system_clock::now();
|
||||
uint64_t now_ms = static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(now_tp.time_since_epoch()).count());
|
||||
|
||||
ClientState st;
|
||||
st.id = id_;
|
||||
st.position = Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
|
||||
st.rotation = Eigen::Matrix3f::Identity();
|
||||
st.currentAngularVelocity = Eigen::Vector3f::Zero();
|
||||
st.velocity = 0.0f;
|
||||
st.selectedVelocity = 0;
|
||||
st.discreteMag = 0.0f;
|
||||
st.discreteAngle = -1;
|
||||
st.lastUpdateServerTime = now_tp;
|
||||
st.nickname = this->nickname;
|
||||
st.shipType = this->shipType;
|
||||
|
||||
timedClientStates.add_state(st);
|
||||
|
||||
std::string respawnMsg = "RESPAWN_ACK:" + std::to_string(id_);
|
||||
broadcastToAll(respawnMsg);
|
||||
|
||||
std::cout << "Server: Player " << id_ << " respawned\n";
|
||||
std::string playerInfo = "PLAYERINFO:" + std::to_string(id_) + ":" + st.nickname + ":" + std::to_string(st.shipType);
|
||||
broadcastToAll(playerInfo);
|
||||
|
||||
std::string eventMsg = "EVENT:" + std::to_string(id_) + ":UPD:" + std::to_string(now_ms) + ":" + st.formPingMessageContent();
|
||||
broadcastToAll(eventMsg);
|
||||
|
||||
std::cout << "Server: Player " << id_ << " respawned, broadcasted RESPAWN_ACK, PLAYERINFO and initial UPD\n";
|
||||
}
|
||||
}
|
||||
else if (parts[0] == "FIRE") {
|
||||
if (parts.size() < 10) return;
|
||||
@ -344,8 +407,8 @@ private:
|
||||
Eigen::Vector3f(1.5f, 0.9f - 6.f, 5.0f)
|
||||
};
|
||||
|
||||
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>((
|
||||
std::chrono::system_clock::now().time_since_epoch())).count();
|
||||
|
||||
std::lock_guard<std::mutex> pl(g_projectiles_mutex);
|
||||
for (int i = 0; i < std::min(shotCount, (int)localOffsets.size()); ++i) {
|
||||
|
||||
40
src/Game.cpp
40
src/Game.cpp
@ -24,6 +24,7 @@
|
||||
#endif
|
||||
|
||||
#include "network/LocalClient.h"
|
||||
#include "network/ClientState.h"
|
||||
|
||||
|
||||
namespace ZL
|
||||
@ -103,14 +104,30 @@ namespace ZL
|
||||
|
||||
menuManager.setupMenu();
|
||||
|
||||
menuManager.onSingleplayerPressed = [this]() {
|
||||
menuManager.onSingleplayerPressed = [this](const std::string& nickname, int shipType) {
|
||||
Environment::shipState.nickname = nickname;
|
||||
Environment::shipState.shipType = shipType;
|
||||
|
||||
networkClient = std::make_unique<LocalClient>();
|
||||
networkClient->Connect("", 0);
|
||||
|
||||
#ifndef NETWORK
|
||||
auto localClient = dynamic_cast<ZL::LocalClient*>(networkClient.get());
|
||||
if (localClient) {
|
||||
ZL::ClientState st = Environment::shipState;
|
||||
st.id = localClient->GetClientId();
|
||||
localClient->setLocalPlayerState(st);
|
||||
}
|
||||
#endif
|
||||
lastTickCount = 0;
|
||||
spaceGameStarted = 1;
|
||||
};
|
||||
|
||||
menuManager.onMultiplayerPressed = [this](const std::string& nickname, int shipType) {
|
||||
Environment::shipState.nickname = nickname;
|
||||
Environment::shipState.shipType = shipType;
|
||||
|
||||
menuManager.onMultiplayerPressed = [this]() {
|
||||
networkClient = std::make_unique<LocalClient>();
|
||||
#ifdef NETWORK
|
||||
#ifdef EMSCRIPTEN
|
||||
networkClient = std::make_unique<WebSocketClientEmscripten>();
|
||||
@ -119,7 +136,26 @@ namespace ZL
|
||||
networkClient = std::make_unique<WebSocketClient>(taskManager.getIOContext());
|
||||
networkClient->Connect("localhost", 8081);
|
||||
#endif
|
||||
#else
|
||||
networkClient->Connect("", 0);
|
||||
#endif
|
||||
|
||||
#ifndef NETWORK
|
||||
auto localClient = dynamic_cast<ZL::LocalClient*>(networkClient.get());
|
||||
if (localClient) {
|
||||
ZL::ClientState st = Environment::shipState;
|
||||
st.id = localClient->GetClientId();
|
||||
localClient->setLocalPlayerState(st);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
if (networkClient) {
|
||||
std::string joinMsg = std::string("JOIN:") + nickname + ":" + std::to_string(shipType);
|
||||
networkClient->Send(joinMsg);
|
||||
std::cerr << "Sent JOIN: " << joinMsg << std::endl;
|
||||
}
|
||||
|
||||
lastTickCount = 0;
|
||||
spaceGameStarted = 1;
|
||||
};
|
||||
|
||||
@ -21,6 +21,7 @@ namespace ZL {
|
||||
|
||||
gameOverSavedRoot = loadUiFromFile("resources/config/game_over.json", renderer, CONST_ZIP_FILE);
|
||||
|
||||
auto shipSelectionRoot = loadUiFromFile("resources/config/ship_selection_menu.json", renderer, CONST_ZIP_FILE);
|
||||
std::function<void()> loadGameplayUI;
|
||||
loadGameplayUI = [this]() {
|
||||
uiManager.replaceRoot(uiSavedRoot);
|
||||
@ -114,15 +115,72 @@ namespace ZL {
|
||||
});
|
||||
};
|
||||
|
||||
uiManager.setButtonCallback("singleButton", [loadGameplayUI, this](const std::string& name) {
|
||||
std::cerr << "Single button pressed: " << name << " -> load gameplay UI\n";
|
||||
uiManager.setButtonCallback("singleButton", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) {
|
||||
std::cerr << "Single button pressed: " << name << " -> open ship selection UI\n";
|
||||
if (!shipSelectionRoot) {
|
||||
std::cerr << "Failed to load ship selection UI\n";
|
||||
return;
|
||||
}
|
||||
if (uiManager.pushMenuFromSavedRoot(shipSelectionRoot)) {
|
||||
uiManager.setButtonCallback("spaceshipButton", [this, loadGameplayUI](const std::string& btnName) {
|
||||
std::string nick = uiManager.getTextFieldValue("nicknameInput");
|
||||
if (nick.empty()) nick = "Player";
|
||||
int shipType = 0;
|
||||
uiManager.popMenu();
|
||||
loadGameplayUI();
|
||||
onSingleplayerPressed();
|
||||
if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType);
|
||||
});
|
||||
uiManager.setButtonCallback("multiplayerButton", [loadGameplayUI, this](const std::string& name) {
|
||||
std::cerr << "Multiplayer button pressed: " << name << " -> load gameplay UI\n";
|
||||
|
||||
uiManager.setButtonCallback("cargoshipButton", [this, loadGameplayUI](const std::string& btnName) {
|
||||
std::string nick = uiManager.getTextFieldValue("nicknameInput");
|
||||
if (nick.empty()) nick = "Player";
|
||||
int shipType = 1;
|
||||
uiManager.popMenu();
|
||||
loadGameplayUI();
|
||||
onMultiplayerPressed();
|
||||
if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType);
|
||||
});
|
||||
|
||||
uiManager.setButtonCallback("backButton", [this](const std::string& btnName) {
|
||||
uiManager.popMenu();
|
||||
});
|
||||
}
|
||||
else {
|
||||
std::cerr << "Failed to push ship selection menu\n";
|
||||
}
|
||||
});
|
||||
|
||||
uiManager.setButtonCallback("multiplayerButton", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) {
|
||||
std::cerr << "Multiplayer button pressed: " << name << " -> open ship selection UI\n";
|
||||
if (!shipSelectionRoot) {
|
||||
std::cerr << "Failed to load ship selection UI\n";
|
||||
return;
|
||||
}
|
||||
if (uiManager.pushMenuFromSavedRoot(shipSelectionRoot)) {
|
||||
uiManager.setButtonCallback("spaceshipButton", [this, loadGameplayUI](const std::string& btnName) {
|
||||
std::string nick = uiManager.getTextFieldValue("nicknameInput");
|
||||
if (nick.empty()) nick = "Player";
|
||||
int shipType = 0;
|
||||
uiManager.popMenu();
|
||||
loadGameplayUI();
|
||||
if (onMultiplayerPressed) onMultiplayerPressed(nick, shipType);
|
||||
});
|
||||
|
||||
uiManager.setButtonCallback("cargoshipButton", [this, loadGameplayUI](const std::string& btnName) {
|
||||
std::string nick = uiManager.getTextFieldValue("nicknameInput");
|
||||
if (nick.empty()) nick = "Player";
|
||||
int shipType = 1;
|
||||
uiManager.popMenu();
|
||||
loadGameplayUI();
|
||||
if (onMultiplayerPressed) onMultiplayerPressed(nick, shipType);
|
||||
});
|
||||
|
||||
uiManager.setButtonCallback("backButton", [this](const std::string& btnName) {
|
||||
uiManager.popMenu();
|
||||
});
|
||||
}
|
||||
else {
|
||||
std::cerr << "Failed to push ship selection menu\n";
|
||||
}
|
||||
});
|
||||
|
||||
uiManager.setButtonCallback("multiplayerButton2", [this](const std::string& name) {
|
||||
@ -135,7 +193,6 @@ namespace ZL {
|
||||
|
||||
if (uiManager.pushMenuFromSavedRoot(multiplayerSavedRoot)) {
|
||||
|
||||
// Callback для кнопки подключения
|
||||
uiManager.setButtonCallback("connectButton", [this](const std::string& buttonName) {
|
||||
std::string serverAddress = uiManager.getTextFieldValue("serverInputField");
|
||||
|
||||
@ -147,16 +204,12 @@ namespace ZL {
|
||||
uiManager.setText("statusText", "Connecting to " + serverAddress + "...");
|
||||
std::cerr << "Connecting to server: " << serverAddress << std::endl;
|
||||
|
||||
// Здесь добавить вашу логику подключения к серверу
|
||||
// connectToServer(serverAddress);
|
||||
});
|
||||
|
||||
// Callback для кнопки назад
|
||||
uiManager.setButtonCallback("backButton", [this](const std::string& buttonName) {
|
||||
uiManager.popMenu();
|
||||
});
|
||||
|
||||
// Callback для отслеживания ввода текста
|
||||
uiManager.setTextFieldCallback("serverInputField",
|
||||
[this](const std::string& fieldName, const std::string& newText) {
|
||||
std::cout << "Server input field changed to: " << newText << std::endl;
|
||||
|
||||
@ -34,8 +34,8 @@ namespace ZL {
|
||||
std::function<void(float)> onVelocityChanged;
|
||||
std::function<void()> onFirePressed;
|
||||
|
||||
std::function<void()> onSingleplayerPressed;
|
||||
std::function<void()> onMultiplayerPressed;
|
||||
std::function<void(const std::string&, int)> onSingleplayerPressed;
|
||||
std::function<void(const std::string&, int)> onMultiplayerPressed;
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
386
src/Space.cpp
386
src/Space.cpp
@ -267,6 +267,16 @@ namespace ZL
|
||||
Environment::zoom = DEFAULT_ZOOM;
|
||||
Environment::tapDownHold = false;
|
||||
|
||||
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";
|
||||
};
|
||||
|
||||
@ -310,6 +320,21 @@ namespace ZL
|
||||
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{ 1.2, 0, -5 });
|
||||
cargo.AssignFrom(cargoBase);
|
||||
cargo.RefreshVBO();
|
||||
|
||||
|
||||
//Boxes
|
||||
boxTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/box/box.png", CONST_ZIP_FILE));
|
||||
@ -336,6 +361,19 @@ namespace ZL
|
||||
throw std::runtime_error("Failed to load spark emitter config file!");
|
||||
}
|
||||
|
||||
crosshairCfgLoaded = loadCrosshairConfig("resources/config/crosshair_config.json");
|
||||
std::cerr << "[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";
|
||||
@ -439,9 +477,15 @@ namespace ZL
|
||||
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);
|
||||
}
|
||||
}
|
||||
renderer.PopMatrix();
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
@ -566,6 +610,7 @@ namespace ZL
|
||||
drawBoxesLabels();
|
||||
drawShip();
|
||||
|
||||
drawCrosshair();
|
||||
drawTargetHud();
|
||||
CheckGlError();
|
||||
}
|
||||
@ -588,8 +633,8 @@ namespace ZL
|
||||
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
// Биндим текстуру корабля один раз для всех удаленных игроков (оптимизация батчинга)
|
||||
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
|
||||
// Биндим текстуру корабля один раз для <EFBFBD>?сех правильных игроков
|
||||
// ?????????: ?????? ???????? ?????????? ?????? ????? ? ??????????? ?? ClientState.shipType
|
||||
|
||||
// Если сервер прислал коробки, применяем их однократно вместо локальной генерации
|
||||
if (!serverBoxesApplied && networkClient) {
|
||||
@ -633,7 +678,14 @@ namespace ZL
|
||||
// 3. Поворот врага
|
||||
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();
|
||||
}
|
||||
|
||||
@ -649,8 +701,6 @@ namespace ZL
|
||||
{
|
||||
if (!textRenderer) return;
|
||||
|
||||
//#ifdef NETWORK
|
||||
// 2D поверх 3D
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
@ -660,18 +710,12 @@ namespace ZL
|
||||
if (deadRemotePlayers.count(id)) continue;
|
||||
|
||||
const ClientState& st = remotePlayer;
|
||||
// Позиция корабля в мире
|
||||
Vector3f shipWorld = st.position;
|
||||
|
||||
float distSq = (Environment::shipState.position - shipWorld).squaredNorm();
|
||||
/*if (distSq > MAX_DIST_SQ) // дальность прорисовки никнейма
|
||||
continue;*/
|
||||
float dist = sqrt(distSq);
|
||||
float alpha = 1.0f; // постоянная видимость
|
||||
/*float alpha = std::clamp(1.f - (dist - FADE_START) / FADE_RANGE, 0.f, 1.f); // дальность прорисовки никнейма
|
||||
if (alpha < 0.01f)
|
||||
continue; */
|
||||
Vector3f labelWorld = shipWorld + Vector3f{ 0.f, -4.f, 0.f }; // регулировка высоты
|
||||
float alpha = 1.0f;
|
||||
Vector3f labelWorld = shipWorld + Vector3f{ 0.f, -4.f, 0.f };
|
||||
float sx, sy, depth;
|
||||
if (!worldToScreen(labelWorld, sx, sy, depth))
|
||||
continue;
|
||||
@ -679,18 +723,245 @@ namespace ZL
|
||||
float uiX = sx, uiY = sy;
|
||||
float scale = std::clamp(BASE_SCALE / (dist * PERSPECTIVE_K + 1.f), MIN_SCALE, MAX_SCALE);
|
||||
|
||||
// Дефолтный лейбл
|
||||
std::string label = "Player (" + std::to_string(st.id) + ") " + std::to_string((int)dist) + "m";
|
||||
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";
|
||||
|
||||
// TODO: nickname sync
|
||||
|
||||
textRenderer->drawText(label, uiX + 1.f, uiY + 1.f, scale, true, { 0.f, 0.f, 0.f, alpha }); // color param
|
||||
textRenderer->drawText(label, uiX + 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);
|
||||
//#endif
|
||||
}
|
||||
|
||||
// хелпер прицела: добавляет повернутую 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::width &&
|
||||
crosshairLastH == Environment::height &&
|
||||
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::width;
|
||||
crosshairLastH = Environment::height;
|
||||
crosshairLastAlpha = crosshairCfg.alpha;
|
||||
crosshairLastThickness = crosshairCfg.thicknessPx;
|
||||
crosshairLastGap = crosshairCfg.gapPx;
|
||||
crosshairLastScaleMul = crosshairCfg.scaleMul;
|
||||
|
||||
float cx = Environment::width * 0.5f;
|
||||
float cy = Environment::height * 0.5f;
|
||||
|
||||
// масштаб от reference (стандартно: по высоте)
|
||||
float scale = (crosshairCfg.refH > 0) ? (Environment::height / (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((float)Environment::width, (float)Environment::height, 0.f, 1.f);
|
||||
renderer.PushMatrix();
|
||||
renderer.LoadIdentity();
|
||||
|
||||
renderer.EnableVertexAttribArray("vPosition");
|
||||
renderer.EnableVertexAttribArray("vColor");
|
||||
|
||||
Eigen::Vector4f uColor(crosshairCfg.color.x(), crosshairCfg.color.y(), crosshairCfg.color.z(), crosshairCfg.alpha);
|
||||
renderer.RenderUniform4fv("uColor", uColor.data());
|
||||
|
||||
renderer.DrawVertexRenderStruct(crosshairMesh);
|
||||
|
||||
renderer.DisableVertexAttribArray("vPosition");
|
||||
renderer.DisableVertexAttribArray("vColor");
|
||||
|
||||
renderer.PopMatrix();
|
||||
renderer.PopProjectionMatrix();
|
||||
renderer.shaderManager.PopShader();
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
}
|
||||
|
||||
int Space::pickTargetId() const
|
||||
@ -863,41 +1134,38 @@ namespace ZL
|
||||
Vector3f shooterVel = ForwardFromRotation(Environment::shipState.rotation) * Environment::shipState.velocity;
|
||||
Vector3f targetVel = ForwardFromRotation(st.rotation) * st.velocity;
|
||||
|
||||
// условие "если враг не движется — круг не рисуем"
|
||||
const float minTargetSpeed = 0.5f; // подобрать (в твоих единицах)
|
||||
bool targetMoving = (targetVel.norm() > minTargetSpeed);
|
||||
|
||||
// альфа круга
|
||||
float leadAlpha = targetMoving ? 1.0f : 0.5f;
|
||||
|
||||
Vector3f leadWorld = shipWorld;
|
||||
bool haveLead = false;
|
||||
|
||||
//if (targetMoving) {
|
||||
// float tLead = 0.0f;
|
||||
// if (SolveLeadInterceptTime(shooterPos, shooterVel, shipWorld, targetVel, projectileSpeed, tLead)) {
|
||||
// // ограничим случаи, чтобы круг не улетал далеко
|
||||
// if (tLead > 0.0f && tLead < 8.0f) {
|
||||
// // подобрать максимум (сек)
|
||||
// leadWorld = shipWorld + targetVel * tLead;
|
||||
// haveLead = true;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
if (targetMoving) {
|
||||
float tLead = 0.0f;
|
||||
// чтобы круг не улетал далеко: максимум 4 секунды (подстроить под игру)
|
||||
float distToTarget = (Environment::shipState.position - shipWorld).norm();
|
||||
float maxLeadTime = std::clamp((distToTarget / projectileSpeed) * 1.2f, 0.05f, 4.0f);
|
||||
|
||||
const float leadMaxDist = 2500.0f; // максимум
|
||||
float allowedDist = min(distToTarget, leadMaxDist);
|
||||
|
||||
// + небольшой запас 10–20% чтобы не моргало на границе
|
||||
const float maxLeadTime = (allowedDist / projectileSpeed) * 1.2f;
|
||||
|
||||
if (SolveLeadInterceptTime(shooterPos, shooterVel, shipWorld, targetVel, projectileSpeed, tLead)) {
|
||||
if (tLead > 0.0f && tLead < maxLeadTime) {
|
||||
leadWorld = shipWorld + targetVel * tLead;
|
||||
if (!targetMoving) {
|
||||
// Цель стоит: рисуем lead прямо на ней, но полупрозрачный
|
||||
leadWorld = shipWorld;
|
||||
haveLead = true;
|
||||
}
|
||||
else {
|
||||
float tLead = 0.0f;
|
||||
|
||||
// 1) Пытаемся “правильное” решение перехвата
|
||||
bool ok = SolveLeadInterceptTime(shooterPos, shooterVel, shipWorld, targetVel, projectileSpeed, tLead);
|
||||
|
||||
// 2) Если решения нет / оно плохое — fallback (чтобы круг не пропадал при пролёте "вбок")
|
||||
// Это ключевое изменение: lead всегда будет.
|
||||
if (!ok || !(tLead > 0.0f) || tLead > maxLeadTime) {
|
||||
tLead = std::clamp(distToTarget / projectileSpeed, 0.05f, maxLeadTime);
|
||||
}
|
||||
|
||||
leadWorld = shipWorld + targetVel * tLead;
|
||||
haveLead = true;
|
||||
}
|
||||
|
||||
// 2) проекция
|
||||
@ -973,7 +1241,10 @@ namespace ZL
|
||||
|
||||
renderer.EnableVertexAttribArray("vPosition");
|
||||
|
||||
// рисуем кружок упреждения (только если есть решение)
|
||||
Eigen::Vector4f hudColor = enemyColor;
|
||||
renderer.RenderUniform4fv("uColor", hudColor.data());
|
||||
|
||||
|
||||
if (haveLead) {
|
||||
float leadNdcX, leadNdcY, leadNdcZ, leadClipW;
|
||||
if (projectToNDC(leadWorld, leadNdcX, leadNdcY, leadNdcZ, leadClipW) && leadClipW > 0.0f) {
|
||||
@ -988,28 +1259,30 @@ namespace ZL
|
||||
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);
|
||||
|
||||
renderer.RenderUniform4fv("uColor", hudColor.data());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderer.EnableVertexAttribArray("vPosition");
|
||||
|
||||
// верх-лево: горизонт + вертикаль
|
||||
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);
|
||||
|
||||
@ -1027,12 +1300,9 @@ namespace ZL
|
||||
return;
|
||||
}
|
||||
|
||||
// 6) Если цель offscreen: рисуем стрелку на краю
|
||||
// dir: куда “смотреть” в NDC
|
||||
float dirX = ndcX;
|
||||
float dirY = ndcY;
|
||||
|
||||
// если позади камеры — разворачиваем направление
|
||||
if (behind) {
|
||||
dirX = -dirX;
|
||||
dirY = -dirY;
|
||||
@ -1043,7 +1313,6 @@ namespace ZL
|
||||
dirX /= len;
|
||||
dirY /= len;
|
||||
|
||||
// пересечение луча с прямоугольником [-1..1] с отступом
|
||||
float marginNdc = 0.08f;
|
||||
float maxX = 1.0f - marginNdc;
|
||||
float maxY = 1.0f - marginNdc;
|
||||
@ -1058,16 +1327,13 @@ namespace ZL
|
||||
float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::width;
|
||||
float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::height;
|
||||
|
||||
// лёгкая анимация “зова”: смещение по направлению
|
||||
float bob = std::sin(t * 6.0f) * 6.0f;
|
||||
edgeX += dirX * bob;
|
||||
edgeY += dirY * bob;
|
||||
|
||||
// стрелка как треугольник + маленький “хвост”
|
||||
float arrowLen = 26.0f;
|
||||
float arrowWid = 14.0f;
|
||||
|
||||
// перпендикуляр
|
||||
float px = -dirY;
|
||||
float py = dirX;
|
||||
|
||||
@ -1106,23 +1372,18 @@ namespace ZL
|
||||
renderer.PushMatrix();
|
||||
renderer.LoadIdentity();
|
||||
|
||||
// треугольник-стрелка
|
||||
drawTri(tip, left, right);
|
||||
|
||||
// “хвост” (короткая черта)
|
||||
float tailLen = 14.0f;
|
||||
float tailX = edgeX - dirX * 6.0f;
|
||||
float tailY = edgeY - dirY * 6.0f;
|
||||
// хвост рисуем как тонкий прямоугольник, ориентированный примерно по направлению:
|
||||
// (упрощение: горизонт/вертикаль не поворачиваем, но выглядит ок. Хочешь — сделаем поворот матрицей)
|
||||
|
||||
drawBar(tailX, tailY, max(thickness, tailLen), thickness);
|
||||
|
||||
renderer.PopMatrix();
|
||||
renderer.PopProjectionMatrix();
|
||||
renderer.shaderManager.PopShader();
|
||||
|
||||
// дистанция рядом со стрелкой
|
||||
// (у тебя ещё будет “статично под прицелом” — это просто другой TextView / drawText)
|
||||
{
|
||||
std::string d = std::to_string((int)dist) + "m";
|
||||
float tx = edgeX + px * 18.0f;
|
||||
@ -1260,6 +1521,10 @@ namespace ZL
|
||||
|
||||
for (auto const& [id, remotePlayer] : latestRemotePlayers) {
|
||||
|
||||
if (networkClient && id == networkClient->GetClientId()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!remotePlayer.canFetchClientStateAtTime(nowRoundedWithDelay))
|
||||
{
|
||||
continue;
|
||||
@ -1268,7 +1533,6 @@ namespace ZL
|
||||
ClientState playerState = remotePlayer.fetchClientStateAtTime(nowRoundedWithDelay);
|
||||
|
||||
remotePlayerStates[id] = playerState;
|
||||
|
||||
}
|
||||
|
||||
for (auto& p : projectiles) {
|
||||
|
||||
41
src/Space.h
41
src/Space.h
@ -78,6 +78,9 @@ namespace ZL {
|
||||
VertexDataStruct spaceshipBase;
|
||||
VertexRenderStruct spaceship;
|
||||
|
||||
std::shared_ptr<Texture> cargoTexture;
|
||||
VertexDataStruct cargoBase;
|
||||
VertexRenderStruct cargo;
|
||||
|
||||
VertexRenderStruct cubemap;
|
||||
|
||||
@ -133,9 +136,45 @@ namespace ZL {
|
||||
|
||||
// helpers
|
||||
void drawTargetHud(); // рисует рамку или стрелку
|
||||
int pickTargetId() const; // выбирает цель (пока: ближайший живой удаленный игрок)
|
||||
int pickTargetId() const; // ???????? ???? (????: ????????? ????? ????????? ?????)
|
||||
|
||||
void clearTextRendererCache();
|
||||
|
||||
// Crosshair HUD
|
||||
struct CrosshairConfig {
|
||||
bool enabled = true;
|
||||
int refW = 1280;
|
||||
int refH = 720;
|
||||
|
||||
float scaleMul = 1.0f;
|
||||
|
||||
Eigen::Vector3f color = { 1.f, 1.f, 1.f };
|
||||
float alpha = 1.0f; // cl_crosshairalpha
|
||||
float thicknessPx = 2.0f; // cl_crosshairthickness
|
||||
float gapPx = 10.0f;
|
||||
|
||||
float topLenPx = 14.0f;
|
||||
float topAngleDeg = 90.0f;
|
||||
|
||||
struct Arm { float lenPx; float angleDeg; };
|
||||
std::vector<Arm> arms;
|
||||
};
|
||||
|
||||
CrosshairConfig crosshairCfg;
|
||||
bool crosshairCfgLoaded = false;
|
||||
|
||||
// кеш геометрии
|
||||
VertexRenderStruct crosshairMesh;
|
||||
bool crosshairMeshValid = false;
|
||||
int crosshairLastW = 0, crosshairLastH = 0;
|
||||
float crosshairLastAlpha = -1.0f;
|
||||
float crosshairLastThickness = -1.0f;
|
||||
float crosshairLastGap = -1.0f;
|
||||
float crosshairLastScaleMul = -1.0f;
|
||||
|
||||
bool loadCrosshairConfig(const std::string& path);
|
||||
void rebuildCrosshairMeshIfNeeded();
|
||||
void drawCrosshair();
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -906,24 +906,30 @@ namespace ZL {
|
||||
}
|
||||
|
||||
void UiManager::onMouseUp(int x, int y) {
|
||||
std::vector<std::shared_ptr<UiButton>> clicked;
|
||||
|
||||
for (auto& b : buttons) {
|
||||
if (!b) continue;
|
||||
bool contains = b->rect.contains((float)x, (float)y);
|
||||
|
||||
if (b->state == ButtonState::Pressed) {
|
||||
if (contains && pressedButton == b) {
|
||||
if (b->onClick) {
|
||||
b->onClick(b->name);
|
||||
}
|
||||
clicked.push_back(b);
|
||||
}
|
||||
b->state = contains ? ButtonState::Hover : ButtonState::Normal;
|
||||
}
|
||||
}
|
||||
pressedButton.reset();
|
||||
|
||||
if (pressedSlider) {
|
||||
pressedSlider.reset();
|
||||
for (auto& b : clicked) {
|
||||
if (b->onClick) {
|
||||
b->onClick(b->name);
|
||||
}
|
||||
}
|
||||
|
||||
pressedButton.reset();
|
||||
if (pressedSlider) pressedSlider.reset();
|
||||
}
|
||||
|
||||
void UiManager::onKeyPress(unsigned char key) {
|
||||
if (!focusedTextField) return;
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
#define _USE_MATH_DEFINES
|
||||
#include <math.h>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
|
||||
using std::min;
|
||||
@ -18,7 +19,7 @@ constexpr float ROTATION_SENSITIVITY = 0.002f;
|
||||
|
||||
constexpr float PLANET_RADIUS = 20000.f;
|
||||
constexpr float PLANET_ALIGN_ZONE = 1.05f;
|
||||
constexpr float PLANET_ANGULAR_ACCEL = 0.01f; // Подбери под динамику
|
||||
constexpr float PLANET_ANGULAR_ACCEL = 0.01f; // ??????? ??? ????????
|
||||
constexpr float PLANET_MAX_ANGULAR_VELOCITY = 10.f;
|
||||
constexpr float PITCH_LIMIT = static_cast<float>(M_PI) / 9.f;//18.0f;
|
||||
|
||||
@ -38,7 +39,9 @@ struct ClientState {
|
||||
float discreteMag = 0;
|
||||
int discreteAngle = -1;
|
||||
|
||||
// Для расчета лага
|
||||
std::string nickname = "Player";
|
||||
int shipType = 0;
|
||||
// ??? ??????? ????
|
||||
std::chrono::system_clock::time_point lastUpdateServerTime;
|
||||
|
||||
void simulate_physics(size_t delta);
|
||||
|
||||
@ -79,6 +79,10 @@ namespace ZL {
|
||||
|
||||
void LocalClient::initializeNPCs() {
|
||||
npcs.clear();
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<int> typeDistrib(0, 1); // 0 = default ship, 1 = cargo
|
||||
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
LocalNPC npc;
|
||||
npc.id = 100 + i;
|
||||
@ -91,6 +95,11 @@ namespace ZL {
|
||||
npc.currentState.discreteAngle = -1;
|
||||
npc.currentState.currentAngularVelocity = Eigen::Vector3f::Zero();
|
||||
|
||||
// random
|
||||
int shipType = typeDistrib(gen);
|
||||
npc.shipType = shipType;
|
||||
npc.currentState.shipType = shipType;
|
||||
|
||||
npc.targetPosition = generateRandomPosition();
|
||||
npc.lastStateUpdateMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
|
||||
@ -31,6 +31,7 @@ namespace ZL {
|
||||
Eigen::Vector3f targetPosition;
|
||||
uint64_t lastStateUpdateMs = 0;
|
||||
bool destroyed = false;
|
||||
int shipType = 0;
|
||||
};
|
||||
|
||||
class LocalClient : public INetworkClient {
|
||||
|
||||
@ -79,7 +79,6 @@ namespace ZL {
|
||||
try {
|
||||
int respawnedPlayerId = std::stoi(parts[1]);
|
||||
pendingRespawns_.push_back(respawnedPlayerId);
|
||||
remotePlayers.erase(respawnedPlayerId);
|
||||
std::cout << "Client: Received RESPAWN_ACK for player " << respawnedPlayerId << std::endl;
|
||||
}
|
||||
catch (...) {}
|
||||
@ -202,6 +201,11 @@ namespace ZL {
|
||||
|
||||
{
|
||||
auto& rp = remotePlayers[remoteId];
|
||||
if (!rp.timedStates.empty()) {
|
||||
const ClientState& last = rp.timedStates.back();
|
||||
remoteState.nickname = last.nickname;
|
||||
remoteState.shipType = last.shipType;
|
||||
}
|
||||
rp.add_state(remoteState);
|
||||
}
|
||||
}
|
||||
@ -218,7 +222,7 @@ namespace ZL {
|
||||
if (playerParts.size() < 15) return; // ID + 14 полей ClientState
|
||||
|
||||
int rId = std::stoi(playerParts[0]);
|
||||
if (rId == clientId) return; // Свое состояние игрок знает лучше всех (Client Side Prediction)
|
||||
if (rId == clientId) return; // Свое состояние игрок знает лучше всех, (Client Side Prediction)
|
||||
|
||||
ClientState remoteState;
|
||||
remoteState.id = rId;
|
||||
@ -230,6 +234,40 @@ namespace ZL {
|
||||
remotePlayers[rId].add_state(remoteState);
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.rfind("PLAYERINFO:", 0) == 0) {
|
||||
if (parts.size() >= 4) {
|
||||
try {
|
||||
int pid = std::stoi(parts[1]);
|
||||
if (pid == clientId) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string nick = parts[2];
|
||||
int st = std::stoi(parts[3]);
|
||||
|
||||
auto it = remotePlayers.find(pid);
|
||||
if (it != remotePlayers.end() && !it->second.timedStates.empty()) {
|
||||
auto& states = it->second.timedStates;
|
||||
states.back().nickname = nick;
|
||||
states.back().shipType = st;
|
||||
}
|
||||
else {
|
||||
ClientState cs;
|
||||
cs.id = pid;
|
||||
cs.nickname = nick;
|
||||
cs.shipType = st;
|
||||
cs.lastUpdateServerTime = std::chrono::system_clock::now();
|
||||
remotePlayers[pid].add_state(cs);
|
||||
}
|
||||
|
||||
std::cout << "Client: PLAYERINFO received. id=" << pid << " nick=" << nick << " shipType=" << st << std::endl;
|
||||
}
|
||||
catch (...) {
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::string WebSocketClientBase::SignMessage(const std::string& msg) {
|
||||
|
||||
@ -7,8 +7,8 @@
|
||||
namespace ZL {
|
||||
void WebSocketClientEmscripten::Connect(const std::string& host, uint16_t port) {
|
||||
// Формируем URL. Обратите внимание, что в Web часто лучше использовать ws://localhost
|
||||
//std::string url = "ws://" + host + ":" + std::to_string(port);
|
||||
std::string url = "wss://api.spacegame.fishrungames.com";
|
||||
std::string url = "ws://" + host + ":" + std::to_string(port);
|
||||
//std::string url = "wss://api.spacegame.fishrungames.com";
|
||||
|
||||
EmscriptenWebSocketCreateAttributes attr = {
|
||||
url.c_str(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user