Compare commits

...

11 Commits

Author SHA1 Message Date
Vladislav Khorev
5e7c050c68 Merge branch 'spark' 2026-03-10 11:39:02 +03:00
Vladislav Khorev
c88784a669 Added inverse, fixing server bug 2026-03-10 11:38:16 +03:00
Vlad
181132e092 merge 2026-03-10 14:36:35 +06:00
Vlad
6fe10eacfc Merge remote-tracking branch 'origin/main' into spark 2026-03-10 14:26:51 +06:00
Vlad
9e692c9ec3 added enter nicknamw 2026-03-10 14:25:00 +06:00
Vladislav Khorev
785b96ce82 Working on list of players 2026-03-09 23:04:48 +03:00
Vladislav Khorev
a8ded217df Fixing ui bugs 2026-03-09 20:52:16 +03:00
Vladislav Khorev
3e8dba36a4 fixing minor bugs, fixing velocity not changed after death, fixing server crash 2026-03-09 19:27:20 +03:00
Vladislav Khorev
84a5e888a0 Added pickup boxes 2026-03-09 18:36:04 +03:00
Vladislav Khorev
86a9f38c3b Some UI changes 2026-03-09 13:50:59 +03:00
Vladislav Khorev
24aa7007bb Added trails to other ships toos 2026-03-08 21:27:45 +03:00
39 changed files with 1484 additions and 152 deletions

View File

@ -8,7 +8,7 @@
body, html { body, html {
margin: 0; padding: 0; width: 100%; height: 100%; margin: 0; padding: 0; width: 100%; height: 100%;
overflow: hidden; background-color: #000; overflow: hidden; background-color: #000;
position: fixed; /* Предотвращает pull-to-refresh на Android */ position: fixed;
} }
#canvas { #canvas {
display: block; display: block;
@ -17,13 +17,65 @@
width: 100vw; height: 100vh; width: 100vw; height: 100vh;
border: none; border: none;
} }
#fs-button {
position: absolute;
top: 10px; right: 10px;
padding: 10px;
z-index: 10;
background: rgba(255,255,255,0.3);
color: white; border: 1px solid white;
cursor: pointer;
font-family: sans-serif;
border-radius: 5px;
}
#status { color: white; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } #status { color: white; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
/* Nick modal */
#nickOverlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.85);
z-index: 9999;
}
#nickBox {
background: #111;
border: 1px solid #444;
padding: 24px;
width: 320px;
box-shadow: 0 8px 24px rgba(0,0,0,0.6);
text-align: center;
}
#nickBox h2 { margin: 0 0 12px 0; font-size: 18px; color: #eee; }
#nickBox input[type="text"] {
width: 100%;
padding: 10px;
font-size: 16px;
box-sizing: border-box;
margin-bottom: 12px;
border: 1px solid #333;
background: #000;
color: #fff;
}
#nickBox button {
padding: 10px 16px;
font-size: 16px;
background: #2a9fd6;
border: none;
color: #fff;
cursor: pointer;
}
#nickSkip { margin-left: 8px; background: #666; }
</style> </style>
</head> </head>
<body> <body>
<button id="fs-button">Fullscreen</button>
<div id="status">Downloading...</div> <div id="status">Downloading...</div>
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas> <canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
<!--
<script> <script>
var statusElement = document.getElementById("status"); var statusElement = document.getElementById("status");
var canvas = document.getElementById("canvas"); var canvas = document.getElementById("canvas");
@ -36,7 +88,119 @@
} }
}; };
// Обработка ориентации document.getElementById('fs-button').addEventListener('click', function() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(e => {
console.error(`Error attempting to enable full-screen mode: ${e.message}`);
});
} else {
document.exitFullscreen();
}
});
window.addEventListener("orientationchange", function() {
setTimeout(() => {
window.dispatchEvent(new Event('resize'));
}, 200);
});
</script>
<script async src="space-game001.js"></script>-->
<div id="nickOverlay" style="display:none;">
<div id="nickBox">
<h2>Enter your nickname</h2>
<input id="nickInput" type="text" maxlength="32" placeholder="Player" />
<div>
<button id="nickSubmit">Start</button>
</div>
</div>
</div>
<script>
// Utility: подготовить глобальный Module до загрузки Emscripten-скрипта
function prepareModuleEnvironment() {
window.Module = window.Module || {};
var canvasEl = document.getElementById('canvas');
// Устанавливаем canvas для Emscripten, чтобы createContext не падал
window.Module.canvas = canvasEl;
// Подготовим заглушку setStatus, если ещё нет
window.Module.setStatus = window.Module.setStatus || function (text) {
var statusElement = document.getElementById("status");
statusElement.innerHTML = text;
statusElement.style.display = text ? 'block' : 'none';
};
}
// Show overlay only if no nickname saved.
function loadGameScript() {
var s = document.createElement('script');
s.src = 'space-game001.js';
s.async = true;
document.body.appendChild(s);
}
function showNickOverlay() {
var overlay = document.getElementById('nickOverlay');
overlay.style.display = 'flex';
var input = document.getElementById('nickInput');
input.focus();
}
function hideNickOverlay() {
var overlay = document.getElementById('nickOverlay');
overlay.style.display = 'none';
}
function saveNickAndStart(nick) {
try {
if (!nick || nick.trim() === '') nick = 'Player';
localStorage.setItem('spacegame_nick', nick);
} catch (e) {
console.warn('localStorage not available', e);
}
hideNickOverlay();
// перед загрузкой скрипта гарантируем, что Module.canvas задан
prepareModuleEnvironment();
loadGameScript();
}
document.addEventListener('DOMContentLoaded', function() {
// Готовим Module сразу — даже если откроется модалка, поле canvas будет доступно для скрипта (если он загружается позже)
prepareModuleEnvironment();
var stored = null;
try {
stored = localStorage.getItem('spacegame_nick');
} catch (e) {
console.warn('localStorage not available', e);
}
if (stored && stored.trim() !== '') {
// Nick is present — start immediately
loadGameScript();
} else {
// Show modal to request nickname before loading WASM
showNickOverlay();
var submit = document.getElementById('nickSubmit');
var skip = document.getElementById('nickSkip');
var input = document.getElementById('nickInput');
submit.addEventListener('click', function() {
saveNickAndStart(input.value);
});
skip.addEventListener('click', function() {
saveNickAndStart('Player');
});
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
saveNickAndStart(input.value);
}
});
}
});
window.addEventListener("orientationchange", function() { window.addEventListener("orientationchange", function() {
// Chrome на Android обновляет innerWidth/Height не мгновенно. // Chrome на Android обновляет innerWidth/Height не мгновенно.
// Ждем завершения анимации поворота. // Ждем завершения анимации поворота.
@ -47,6 +211,6 @@
}); });
</script> </script>
<script async src="space-game001.js"></script>
</body> </body>
</html> </html>

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

@ -13,7 +13,7 @@
{ {
"type": "StaticImage", "type": "StaticImage",
"name": "titleBtn", "name": "titleBtn",
"width": 254, "width": 434,
"height": 35, "height": 35,
"texture": "resources/main_menu/title.png" "texture": "resources/main_menu/title.png"
}, },
@ -25,15 +25,11 @@
"texture": "resources/main_menu/line.png" "texture": "resources/main_menu/line.png"
}, },
{ {
"type": "Button", "type": "StaticImage",
"name": "subtitleBtn", "name": "subtitleBtn",
"width": 144, "width": 144,
"height": 11, "height": 11,
"textures": { "texture": "resources/main_menu/subtitle.png"
"normal": "resources/main_menu/subtitle.png",
"hover": "resources/main_menu/subtitle.png",
"pressed": "resources/main_menu/subtitle.png"
}
}, },
{ {
"type": "Button", "type": "Button",

View File

@ -10,6 +10,13 @@
"width": "match_parent", "width": "match_parent",
"height": "match_parent", "height": "match_parent",
"children": [ "children": [
{
"type": "StaticImage",
"name": "titleBtn",
"width": 266,
"height": 66,
"texture": "resources/select_your_ship.png"
},
{ {
"type": "LinearLayout", "type": "LinearLayout",
"orientation": "horizontal", "orientation": "horizontal",

View File

@ -1,5 +1,5 @@
{ {
"emissionRate": 0.4, "emissionRate": 1.2,
"maxParticles": 400, "maxParticles": 400,
"particleSize": 0.3, "particleSize": 0.3,
"biasX": 0.3, "biasX": 0.3,

View File

@ -1,5 +1,5 @@
{ {
"emissionRate": 0.4, "emissionRate": 1.2,
"maxParticles": 400, "maxParticles": 400,
"particleSize": 0.3, "particleSize": 0.3,
"biasX": 0.3, "biasX": 0.3,

View File

@ -8,18 +8,55 @@
"children": [ "children": [
{ {
"type": "TextView", "type": "TextView",
"name": "velocityText", "name": "gameScoreText",
"x": 10, "x": 0,
"y": 10, "y": 30,
"width": 200, "width": 200,
"height": 40, "height": 60,
"horizontal_gravity": "left", "horizontal_gravity": "left",
"vertical_gravity": "top", "vertical_gravity": "top",
"text": "Velocity: 0", "text": "Score: 0",
"fontSize": 24, "fontSize": 36,
"color": [1.0, 1.0, 1.0, 1.0], "color": [
0,
217,
255,
1
],
"centered": false "centered": false
}, },
{
"type": "Button",
"name": "showPlayersButton",
"x": 0,
"y": 100,
"width": 150,
"height": 150,
"horizontal_gravity": "left",
"vertical_gravity": "top",
"textures": {
"normal": "resources/button_players.png",
"hover": "resources/button_players.png",
"pressed": "resources/button_players.png",
"disabled": "resources/button_players.png"
}
},
{
"type": "Button",
"name": "inverseMouseButton",
"x": 0,
"y": 100,
"width": 150,
"height": 150,
"horizontal_gravity": "right",
"vertical_gravity": "top",
"textures": {
"normal": "resources/fire.png",
"hover": "resources/fire.png",
"pressed": "resources/fire2.png",
"disabled": "resources/fire.png"
}
},
{ {
"type": "Button", "type": "Button",
"name": "shootButton", "name": "shootButton",
@ -64,8 +101,9 @@
"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"
} }
}, },
{ {
@ -80,8 +118,26 @@
"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"
}
},
{
"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"
} }
} }
] ]

BIN
resources/connection_failed.png (Stored with Git LFS)

Binary file not shown.

BIN
resources/loading.png (Stored with Git LFS)

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

BIN
resources/main_menu/title.png (Stored with Git LFS)

Binary file not shown.

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

Binary file not shown.

View File

@ -31,8 +31,8 @@ bool Session::is_timed_out(std::chrono::system_clock::time_point now) const {
} }
void Session::force_disconnect() { void Session::force_disconnect() {
ws_.async_close(websocket::close_code::normal, beast::error_code ec;
[self = shared_from_this()](beast::error_code) {}); ws_.next_layer().socket().close(ec);
} }
int Session::get_id() const { return id_; } int Session::get_id() const { return id_; }
@ -125,8 +125,6 @@ void Session::sendBoxesToClient() {
void Session::init() void Session::init()
{ {
sendBoxesToClient();
auto timer = std::make_shared<net::steady_timer>(ws_.get_executor()); auto timer = std::make_shared<net::steady_timer>(ws_.get_executor());
timer->expires_after(std::chrono::milliseconds(100)); timer->expires_after(std::chrono::milliseconds(100));
timer->async_wait([self = shared_from_this(), timer](const boost::system::error_code& ec) { timer->async_wait([self = shared_from_this(), timer](const boost::system::error_code& ec) {
@ -135,6 +133,8 @@ void Session::init()
uint64_t now_ms = static_cast<uint64_t>( uint64_t now_ms = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(now_tp.time_since_epoch()).count()); std::chrono::duration_cast<std::chrono::milliseconds>(now_tp.time_since_epoch()).count());
self->sendBoxesToClient();
self->send_message("ID:" + std::to_string(self->id_) + ":" + std::to_string(now_ms)); self->send_message("ID:" + std::to_string(self->id_) + ":" + std::to_string(now_ms));
self->do_read(); self->do_read();
} }
@ -346,6 +346,64 @@ void Session::process_message(const std::string& msg) {
std::cout << "Server: Player " << id_ << " respawned, broadcasted RESPAWN_ACK, PLAYERINFO and initial UPD\n"; std::cout << "Server: Player " << id_ << " respawned, broadcasted RESPAWN_ACK, PLAYERINFO and initial UPD\n";
} }
} }
else if (parts[0] == "BOX_PICKUP") {
if (parts.size() < 2) return;
if (this->shipType != 1) {
std::cout << "Server: Player " << id_ << " tried BOX_PICKUP but is not a cargo ship\n";
return;
}
int boxIdx = -1;
try { boxIdx = std::stoi(parts[1]); } catch (...) { return; }
std::lock_guard<std::mutex> bm(server_.g_boxes_mutex);
if (boxIdx < 0 || boxIdx >= (int)server_.g_serverBoxes.size()) return;
if (server_.g_serverBoxes[boxIdx].destroyed) return;
if (timedClientStates.timedStates.empty()) return;
const ClientState& playerState = timedClientStates.timedStates.back();
Eigen::Vector3f boxWorld = server_.g_serverBoxes[boxIdx].position + kWorldOffset;
float distSq = (playerState.position - boxWorld).squaredNorm();
if (distSq > BOX_PICKUP_RADIUS * BOX_PICKUP_RADIUS) {
std::cout << "Server: Player " << id_ << " too far to pick up box " << boxIdx << "\n";
return;
}
server_.g_serverBoxes[boxIdx].destroyed = true;
std::string pickedUpMsg = "BOX_PICKED_UP:" + std::to_string(boxIdx) + ":" + std::to_string(id_);
server_.broadcastToAll(pickedUpMsg);
std::cout << "Server: Box " << boxIdx << " picked up by player " << id_ << "\n";
// Respawn box
{
static thread_local std::mt19937 rng{ std::random_device{}() };
static thread_local std::uniform_real_distribution<float> angleDist(0.f, static_cast<float>(M_PI * 2.0));
Eigen::Vector3f newPos = server_.PickSafeBoxPos(boxIdx);
Eigen::Vector3f axis = Eigen::Vector3f::Random().normalized();
Eigen::Matrix3f newRot = Eigen::AngleAxisf(angleDist(rng), axis).toRotationMatrix();
server_.g_serverBoxes[boxIdx].position = newPos;
server_.g_serverBoxes[boxIdx].rotation = newRot;
server_.g_serverBoxes[boxIdx].destroyed = false;
Eigen::Quaternionf q(newRot);
std::string respawnMsg = "BOX_RESPAWN:" +
std::to_string(boxIdx) + ":" +
std::to_string(newPos.x()) + ":" +
std::to_string(newPos.y()) + ":" +
std::to_string(newPos.z()) + ":" +
std::to_string(q.w()) + ":" +
std::to_string(q.x()) + ":" +
std::to_string(q.y()) + ":" +
std::to_string(q.z());
server_.broadcastToAll(respawnMsg);
std::cout << "Server: Box " << boxIdx << " respawned after pickup\n";
}
}
else if (parts[0] == "FIRE") { else if (parts[0] == "FIRE") {
if (parts.size() < 10) return; if (parts.size() < 10) return;
@ -377,6 +435,28 @@ void Session::process_message(const std::string& msg) {
} }
} }
Eigen::Vector3f Server::PickSafeBoxPos(int skipIdx)
{
// Assumes g_boxes_mutex is already held by the caller
static thread_local std::mt19937 rng{ std::random_device{}() };
std::uniform_real_distribution<float> dist(-1000.f, 1000.f);
for (int attempt = 0; attempt < 500; ++attempt) {
Eigen::Vector3f cand(dist(rng), dist(rng), dist(rng));
bool safe = true;
for (int i = 0; i < (int)g_serverBoxes.size(); ++i) {
if (i == skipIdx) continue;
if (g_serverBoxes[i].destroyed) continue;
if ((cand - g_serverBoxes[i].position).squaredNorm() < 9.f) {
safe = false;
break;
}
}
if (safe) return cand;
}
return Eigen::Vector3f(dist(rng), dist(rng), dist(rng));
}
Eigen::Vector3f Server::PickSafeSpawnPos(int forPlayerId) Eigen::Vector3f Server::PickSafeSpawnPos(int forPlayerId)
{ {
static thread_local std::mt19937 rng{ std::random_device{}() }; static thread_local std::mt19937 rng{ std::random_device{}() };
@ -608,6 +688,8 @@ void Server::update_world() {
std::vector<int> boxesToRespawn;
// --- Tick: box-projectile collisions --- // --- Tick: box-projectile collisions ---
{ {
std::lock_guard<std::mutex> bm(g_boxes_mutex); std::lock_guard<std::mutex> bm(g_boxes_mutex);
@ -632,6 +714,7 @@ void Server::update_world() {
} }
for (const auto& [boxIdx, projIdx] : boxProjectileCollisions) { for (const auto& [boxIdx, projIdx] : boxProjectileCollisions) {
if (g_serverBoxes[boxIdx].destroyed) continue;
g_serverBoxes[boxIdx].destroyed = true; g_serverBoxes[boxIdx].destroyed = true;
Eigen::Vector3f boxWorld = g_serverBoxes[boxIdx].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f); Eigen::Vector3f boxWorld = g_serverBoxes[boxIdx].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
@ -647,6 +730,8 @@ void Server::update_world() {
g_boxDestructions.push_back(destruction); g_boxDestructions.push_back(destruction);
} }
boxesToRespawn.push_back(static_cast<int>(boxIdx));
std::cout << "Server: Box " << boxIdx << " destroyed by projectile from player " std::cout << "Server: Box " << boxIdx << " destroyed by projectile from player "
<< g_projectiles[projIdx].shooterId << std::endl; << g_projectiles[projIdx].shooterId << std::endl;
} }
@ -690,6 +775,8 @@ void Server::update_world() {
g_boxDestructions.push_back(destruction); g_boxDestructions.push_back(destruction);
} }
boxesToRespawn.push_back(static_cast<int>(bi));
std::cout << "Server: Box " << bi << " destroyed by ship collision with player " std::cout << "Server: Box " << bi << " destroyed by ship collision with player "
<< session->get_id() << std::endl; << session->get_id() << std::endl;
break; break;
@ -732,6 +819,41 @@ void Server::update_world() {
g_boxDestructions.clear(); g_boxDestructions.clear();
} }
// --- Respawn destroyed boxes ---
if (!boxesToRespawn.empty()) {
static thread_local std::mt19937 rng{ std::random_device{}() };
static thread_local std::uniform_real_distribution<float> angleDist(0.f, static_cast<float>(M_PI * 2.0));
std::vector<std::string> respawnMsgs;
{
std::lock_guard<std::mutex> bm(g_boxes_mutex);
for (int idx : boxesToRespawn) {
if (idx < 0 || idx >= (int)g_serverBoxes.size()) continue;
Eigen::Vector3f newPos = PickSafeBoxPos(idx);
Eigen::Vector3f axis = Eigen::Vector3f::Random().normalized();
Eigen::Matrix3f newRot = Eigen::AngleAxisf(angleDist(rng), axis).toRotationMatrix();
g_serverBoxes[idx].position = newPos;
g_serverBoxes[idx].rotation = newRot;
g_serverBoxes[idx].destroyed = false;
Eigen::Quaternionf q(newRot);
std::string respawnMsg = "BOX_RESPAWN:" +
std::to_string(idx) + ":" +
std::to_string(newPos.x()) + ":" +
std::to_string(newPos.y()) + ":" +
std::to_string(newPos.z()) + ":" +
std::to_string(q.w()) + ":" +
std::to_string(q.x()) + ":" +
std::to_string(q.y()) + ":" +
std::to_string(q.z());
respawnMsgs.push_back(respawnMsg);
std::cout << "Server: Box " << idx << " respawned" << std::endl;
}
}
for (const auto& msg : respawnMsgs) {
broadcastToAll(msg);
}
}
// --- Schedule next tick in 50ms --- // --- Schedule next tick in 50ms ---
timer.expires_after(std::chrono::milliseconds(50)); timer.expires_after(std::chrono::milliseconds(50));
timer.async_wait([this](const boost::system::error_code& ec) { timer.async_wait([this](const boost::system::error_code& ec) {
@ -820,6 +942,7 @@ int main() {
Server server(acceptor, ioc); Server server(acceptor, ioc);
server.init();
server.accept(); server.accept();
std::cout << "Server started on port 8081...\n"; std::cout << "Server started on port 8081...\n";

View File

@ -135,6 +135,8 @@ public:
void createProjectile(int id, Eigen::Vector3f pos, Eigen::Quaternionf dir, float velocity); void createProjectile(int id, Eigen::Vector3f pos, Eigen::Quaternionf dir, float velocity);
void update_world(); void update_world();
Eigen::Vector3f PickSafeSpawnPos(int forPlayerId); Eigen::Vector3f PickSafeSpawnPos(int forPlayerId);
// Caller must hold g_boxes_mutex
Eigen::Vector3f PickSafeBoxPos(int skipIdx);
void init(); void init();
void accept(); void accept();
}; };

View File

@ -41,6 +41,10 @@ namespace ZL
const char* CONST_ZIP_FILE = ""; const char* CONST_ZIP_FILE = "";
#endif #endif
float x = 0;
float y = 0;
float z = 0;
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
Game* Game::s_instance = nullptr; Game* Game::s_instance = nullptr;
@ -106,7 +110,20 @@ namespace ZL
loadingTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE)); loadingTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE));
#endif #endif
loadingMesh.data = CreateRect2D({ 0.5f, 0.5f }, { 0.5f, 0.5f }, 3); float minDimension;
float width = Environment::projectionWidth;
float height = Environment::projectionHeight;
if (width >= height)
{
minDimension = height;
}
else
{
minDimension = width;
}
loadingMesh.data = CreateRect2D({ 0.0f, 0.0f }, { minDimension*0.5f, minDimension*0.5f }, 3);
loadingMesh.RefreshVBO(); loadingMesh.RefreshVBO();
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
@ -264,12 +281,12 @@ namespace ZL
renderer.shaderManager.PushShader(defaultShaderName); renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1i(textureUniformName, 0);
//float width = Environment::projectionWidth; float width = Environment::projectionWidth;
//float height = Environment::projectionHeight; float height = Environment::projectionHeight;
renderer.PushProjectionMatrix( renderer.PushProjectionMatrix(
0, 1, -width * 0.5f, width*0.5f,
0, 1, -height * 0.5f, height * 0.5f,
-10, 10); -10, 10);
renderer.PushMatrix(); renderer.PushMatrix();
@ -331,7 +348,7 @@ namespace ZL
//SDL_GL_MakeCurrent(ZL::Environment::window, glContext); //SDL_GL_MakeCurrent(ZL::Environment::window, glContext);
ZL::CheckGlError(); ZL::CheckGlError();
glClearColor(0.0f, 0.0f, 1.0f, 1.0f); glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//processTickCount(); //processTickCount();
@ -420,22 +437,6 @@ namespace ZL
handleMotion(ZL::UiManager::MOUSE_FINGER_ID, mx, my); handleMotion(ZL::UiManager::MOUSE_FINGER_ID, mx, my);
} }
/*if (event.type == SDL_MOUSEBUTTONDOWN) {
int mx = event.button.x;
int my = event.button.y;
handleDown(mx, my);
}
if (event.type == SDL_MOUSEBUTTONUP) {
int mx = event.button.x;
int my = event.button.y;
handleUp(mx, my);
}
if (event.type == SDL_MOUSEMOTION) {
int mx = event.motion.x;
int my = event.motion.y;
handleMotion(mx, my);
}*/
if (event.type == SDL_MOUSEWHEEL) { if (event.type == SDL_MOUSEWHEEL) {
static const float zoomstep = 2.0f; static const float zoomstep = 2.0f;
if (event.wheel.y > 0) { if (event.wheel.y > 0) {
@ -464,8 +465,8 @@ namespace ZL
} }
if (event.type == SDL_KEYUP) { if (event.type == SDL_KEYUP) {
if (event.key.keysym.sym == SDLK_a) { if (event.key.keysym.sym == SDLK_r) {
//Environment::shipState.position = { 9466.15820, 1046.00159, 18531.2090 }; std::cout << "Camera position: x=" << x << " y=" << y << " z=" << z << std::endl;
} }
} }
#endif #endif

View File

@ -1,8 +1,15 @@
#include "MenuManager.h" #include "MenuManager.h"
#include <iostream> #include <iostream>
#ifdef EMSCRIPTEN
#include <emscripten.h>
#include <cstdlib>
#endif
namespace ZL { namespace ZL {
extern bool inverseVertical;
MenuManager::MenuManager(Renderer& iRenderer) : MenuManager::MenuManager(Renderer& iRenderer) :
renderer(iRenderer) renderer(iRenderer)
{ {
@ -53,15 +60,39 @@ namespace ZL {
state = GameState::ShipSelectionSingle; state = GameState::ShipSelectionSingle;
uiManager.replaceRoot(shipSelectionRoot); uiManager.replaceRoot(shipSelectionRoot);
uiManager.setButtonCallback("spaceshipButton", [this](const std::string&) { std::string initialNick;
#ifdef EMSCRIPTEN
char* savedNickC = emscripten_run_script_string("localStorage.getItem('spacegame_nick') || ''");
if (savedNickC) {
initialNick = savedNickC;
free(savedNickC);
}
#endif
auto tf = uiManager.findTextField("nicknameInput");
if (tf) {
if (!initialNick.empty()) tf->text = initialNick;
#ifdef EMSCRIPTEN
uiManager.setTextFieldCallback("nicknameInput", [](const std::string&, const std::string& value) {
EM_ASM_({
try { localStorage.setItem('spacegame_nick', UTF8ToString($0)); } catch(e) {}
}, value.c_str());
});
#endif
}
uiManager.setButtonCallback("spaceshipButton", [this, initialNick](const std::string&) {
std::string nick = uiManager.getTextFieldValue("nicknameInput"); std::string nick = uiManager.getTextFieldValue("nicknameInput");
if (nick.empty()) nick = initialNick;
if (nick.empty()) nick = "Player"; if (nick.empty()) nick = "Player";
enterGameplay(); enterGameplay();
if (onSingleplayerPressed) onSingleplayerPressed(nick, 0); if (onSingleplayerPressed) onSingleplayerPressed(nick, 0);
}); });
uiManager.setButtonCallback("cargoshipButton", [this](const std::string&) { uiManager.setButtonCallback("cargoshipButton", [this, initialNick](const std::string&) {
std::string nick = uiManager.getTextFieldValue("nicknameInput"); std::string nick = uiManager.getTextFieldValue("nicknameInput");
if (nick.empty()) nick = initialNick;
if (nick.empty()) nick = "Player"; if (nick.empty()) nick = "Player";
enterGameplay(); enterGameplay();
if (onSingleplayerPressed) onSingleplayerPressed(nick, 1); if (onSingleplayerPressed) onSingleplayerPressed(nick, 1);
@ -79,8 +110,31 @@ namespace ZL {
state = GameState::ShipSelectionMulti; state = GameState::ShipSelectionMulti;
uiManager.replaceRoot(shipSelectionRoot); uiManager.replaceRoot(shipSelectionRoot);
uiManager.setButtonCallback("spaceshipButton", [this](const std::string&) { std::string initialNick;
#ifdef EMSCRIPTEN
char* savedNickC = emscripten_run_script_string("localStorage.getItem('spacegame_nick') || ''");
if (savedNickC) {
initialNick = savedNickC;
free(savedNickC);
}
#endif
auto tf = uiManager.findTextField("nicknameInput");
if (tf) {
if (!initialNick.empty()) tf->text = initialNick;
#ifdef EMSCRIPTEN
uiManager.setTextFieldCallback("nicknameInput", [](const std::string&, const std::string& value) {
EM_ASM_({
try { localStorage.setItem('spacegame_nick', UTF8ToString($0)); } catch(e) {}
}, value.c_str());
});
#endif
}
uiManager.setButtonCallback("spaceshipButton", [this, initialNick](const std::string&) {
std::string nick = uiManager.getTextFieldValue("nicknameInput"); std::string nick = uiManager.getTextFieldValue("nicknameInput");
if (nick.empty()) nick = initialNick;
if (nick.empty()) nick = "Player"; if (nick.empty()) nick = "Player";
pendingMultiNick = nick; pendingMultiNick = nick;
pendingMultiShipType = 0; pendingMultiShipType = 0;
@ -88,8 +142,9 @@ namespace ZL {
if (onMultiplayerPressed) onMultiplayerPressed(nick, 0); if (onMultiplayerPressed) onMultiplayerPressed(nick, 0);
}); });
uiManager.setButtonCallback("cargoshipButton", [this](const std::string&) { uiManager.setButtonCallback("cargoshipButton", [this, initialNick](const std::string&) {
std::string nick = uiManager.getTextFieldValue("nicknameInput"); std::string nick = uiManager.getTextFieldValue("nicknameInput");
if (nick.empty()) nick = initialNick;
if (nick.empty()) nick = "Player"; if (nick.empty()) nick = "Player";
pendingMultiNick = nick; pendingMultiNick = nick;
pendingMultiShipType = 1; pendingMultiShipType = 1;
@ -135,13 +190,16 @@ namespace ZL {
state = GameState::Gameplay; state = GameState::Gameplay;
uiManager.replaceRoot(gameplayRoot); uiManager.replaceRoot(gameplayRoot);
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");
if (velocityTv) { if (velocityTv) {
velocityTv->rect.x = 10.0f; velocityTv->rect.x = 10.0f;
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();
@ -152,20 +210,57 @@ namespace ZL {
uiManager.setButtonPressCallback("plusButton", [this](const std::string&) { uiManager.setButtonPressCallback("plusButton", [this](const std::string&) {
int newVel = Environment::shipState.selectedVelocity + 1; int newVel = Environment::shipState.selectedVelocity + 1;
if (newVel > 4) newVel = 4; if (newVel > 4) newVel = 4;
uiManager.findButton("minusButton")->state = ButtonState::Normal;
if (newVel == 4)
{
uiManager.findButton("plusButton")->state = ButtonState::Disabled;
}
else
{
uiManager.findButton("plusButton")->state = ButtonState::Normal;
}
if (onVelocityChanged) onVelocityChanged(newVel); if (onVelocityChanged) onVelocityChanged(newVel);
}); });
uiManager.setButtonPressCallback("minusButton", [this](const std::string&) { uiManager.setButtonPressCallback("minusButton", [this](const std::string&) {
int newVel = Environment::shipState.selectedVelocity - 1; int newVel = Environment::shipState.selectedVelocity - 1;
if (newVel < 0) newVel = 0; if (newVel < 0) newVel = 0;
uiManager.findButton("plusButton")->state = ButtonState::Normal;
if (newVel == 0)
{
uiManager.findButton("minusButton")->state = ButtonState::Disabled;
}
else
{
uiManager.findButton("minusButton")->state = ButtonState::Normal;
}
if (onVelocityChanged) onVelocityChanged(newVel); if (onVelocityChanged) onVelocityChanged(newVel);
}); });
uiManager.setButtonPressCallback("takeButton", [this](const std::string&) {
if (onTakeButtonPressed) onTakeButtonPressed();
});
uiManager.setButtonCallback("showPlayersButton", [this](const std::string&) {
if (onShowPlayersPressed) onShowPlayersPressed();
});
//inverseMouseButton
uiManager.setButtonPressCallback("inverseMouseButton", [this](const std::string&) {
inverseVertical = !inverseVertical;
std::cout << "Inverse mouse: " << (inverseVertical ? "ON" : "OFF") << std::endl;
});
/*
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,8 +70,10 @@ 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;
std::function<void()> onShowPlayersPressed;
}; };
} // namespace ZL } // namespace ZL

View File

@ -33,6 +33,10 @@ namespace ZL
extern const char* CONST_ZIP_FILE; extern const char* CONST_ZIP_FILE;
extern float x; extern float x;
extern float y;
extern float z;
bool inverseVertical = true;
Eigen::Quaternionf generateRandomQuaternion(std::mt19937& gen) Eigen::Quaternionf generateRandomQuaternion(std::mt19937& gen)
{ {
@ -262,11 +266,32 @@ namespace ZL
explosionEmitter.setEmissionPoints(std::vector<Vector3f>()); explosionEmitter.setEmissionPoints(std::vector<Vector3f>());
Environment::shipState.position = Vector3f{ 0, 0, 45000.f }; Environment::shipState.position = Vector3f{ 0, 0, 45000.f };
Environment::shipState.velocity = 0.0f; Environment::shipState.velocity = 0.0f;
Environment::shipState.selectedVelocity = 0;
newShipVelocity = 0;
Environment::shipState.rotation = Eigen::Matrix3f::Identity(); Environment::shipState.rotation = Eigen::Matrix3f::Identity();
Environment::inverseShipMatrix = Eigen::Matrix3f::Identity(); Environment::inverseShipMatrix = Eigen::Matrix3f::Identity();
Environment::zoom = DEFAULT_ZOOM; Environment::zoom = DEFAULT_ZOOM;
Environment::tapDownHold = false; Environment::tapDownHold = false;
playerScore = 0; 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() { void Space::setup() {
@ -309,6 +334,31 @@ namespace ZL
firePressed = true; 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 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);
@ -387,7 +437,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)
@ -507,17 +557,7 @@ namespace ZL
} }
renderer.PushMatrix(); drawShipSparkEmitters();
renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(- /*0.5 * */ Environment::shipState.position);
std::cout << "Ship pos draw: " << Environment::shipState.position.transpose() << "\n";
if (Environment::shipState.shipType == 1) {
sparkEmitterCargo.draw(renderer, Environment::zoom, Environment::width, Environment::height);
}
else {
sparkEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
}
renderer.PopMatrix();
} }
renderer.PopMatrix(); renderer.PopMatrix();
@ -591,16 +631,7 @@ namespace ZL
glViewport(0, 0, Environment::width, Environment::height); glViewport(0, 0, Environment::width, Environment::height);
// Готовим данные всех эмиттеров (CPU + VBO upload) до начала отрисовки, prepareSparkEmittersForDraw();
// чтобы draw() делал только GPU-вызовы без пауз между кораблём и частицами.
sparkEmitter.prepareForDraw(true);
sparkEmitterCargo.prepareForDraw(true);
explosionEmitter.prepareForDraw(false);
for (const auto& p : projectiles) {
if (p && p->isActive()) {
p->projectileEmitter.prepareForDraw(true);
}
}
CheckGlError(); CheckGlError();
@ -669,6 +700,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;
} }
} }
@ -971,6 +1007,16 @@ namespace ZL
int Space::pickTargetId() const 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; int bestId = -1;
float bestDistSq = 1e30f; float bestDistSq = 1e30f;
@ -978,8 +1024,8 @@ namespace ZL
if (deadRemotePlayers.count(id)) continue; if (deadRemotePlayers.count(id)) continue;
float d2 = (Environment::shipState.position - st.position).squaredNorm(); float d2 = (Environment::shipState.position - st.position).squaredNorm();
if (d2 > TARGET_MAX_DIST_SQ) continue; // слишком далеко if (d2 > TARGET_MAX_DIST_SQ) continue;
if (d2 < bestDistSq) { if (d2 < bestDistSq) {
bestDistSq = d2; bestDistSq = d2;
@ -1431,6 +1477,89 @@ namespace ZL
targetWasVisible = false; 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) { void Space::processTickCount(int64_t newTickCount, int64_t delta) {
auto now_ms = newTickCount; auto now_ms = newTickCount;
@ -1522,7 +1651,7 @@ namespace ZL
std::string msg = "UPD:" + std::to_string(now_ms) + ":" + Environment::shipState.formPingMessageContent(); std::string msg = "UPD:" + std::to_string(now_ms) + ":" + Environment::shipState.formPingMessageContent();
networkClient->Send(msg); networkClient->Send(msg);
std::cout << "Sending: " << msg << std::endl; //std::cout << "Sending: " << msg << std::endl;
} }
long long leftoverDelta = delta; long long leftoverDelta = delta;
@ -1545,41 +1674,6 @@ namespace ZL
} }
//--------------
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->setEmissionPoints({ /*0.5* */Environment::shipState.position });
//sparkEmitterPtr->setEmissionPoints({ Vector3f(0, 0, 0) });
std::cout << "Ship pos empo: " << Environment::shipState.position.transpose() << "\n";
}
if (Environment::shipState.velocity > 0.1f)
{
sparkEmitterPtr->setIsActive(true);
}
else
{
sparkEmitterPtr->setIsActive(false);
}
sparkEmitterPtr->update(static_cast<float>(delta));
auto latestRemotePlayers = networkClient->getRemotePlayers(); auto latestRemotePlayers = networkClient->getRemotePlayers();
std::chrono::system_clock::time_point nowRoundedWithDelay{ std::chrono::milliseconds(newTickCount - CLIENT_DELAY) }; std::chrono::system_clock::time_point nowRoundedWithDelay{ std::chrono::milliseconds(newTickCount - CLIENT_DELAY) };
@ -1601,6 +1695,8 @@ namespace ZL
remotePlayerStates[id] = playerState; remotePlayerStates[id] = playerState;
} }
updateSparkEmitters(static_cast<float>(delta));
for (auto& p : projectiles) { for (auto& p : projectiles) {
if (p && p->isActive()) { if (p && p->isActive()) {
p->update(static_cast<float>(delta), renderer); p->update(static_cast<float>(delta), renderer);
@ -1636,7 +1732,9 @@ namespace ZL
shipAlive = false; shipAlive = false;
gameOver = true; gameOver = true;
Environment::shipState.selectedVelocity = 0;
Environment::shipState.velocity = 0.0f; Environment::shipState.velocity = 0.0f;
newShipVelocity = 0;
showExplosion = true; showExplosion = true;
explosionEmitter.setUseWorldSpace(true); explosionEmitter.setUseWorldSpace(true);
@ -1646,6 +1744,7 @@ namespace ZL
std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n"; std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n";
clearPlayerListIfVisible();
menuManager.showGameOver(this->playerScore); menuManager.showGameOver(this->playerScore);
} }
else { else {
@ -1721,6 +1820,7 @@ namespace ZL
planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty; planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty;
} }
clearPlayerListIfVisible();
menuManager.showGameOver(this->playerScore); menuManager.showGameOver(this->playerScore);
} }
} }
@ -1736,8 +1836,31 @@ 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 && 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() { void Space::fireProjectiles() {
@ -1746,6 +1869,7 @@ namespace ZL
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 projectileSpeed = PROJECTILE_VELOCITY;
const float lifeMs = PROJECTILE_LIFE; const float lifeMs = PROJECTILE_LIFE;
const float size = 0.5f; const float size = 0.5f;
@ -1779,6 +1903,7 @@ namespace ZL
gameOver = true; gameOver = true;
Environment::shipState.velocity = 0.0f; Environment::shipState.velocity = 0.0f;
std::cout << "Client: Lost connection to server\n"; std::cout << "Client: Lost connection to server\n";
clearPlayerListIfVisible();
menuManager.showConnectionLost(); menuManager.showConnectionLost();
} }
@ -1817,6 +1942,7 @@ namespace ZL
} }
} }
} }
// Обработка событий смерти, присланных сервером // Обработка событий смерти, присланных сервером
auto deaths = networkClient->getPendingDeaths(); auto deaths = networkClient->getPendingDeaths();
if (!deaths.empty()) { if (!deaths.empty()) {
@ -1841,18 +1967,21 @@ namespace ZL
shipAlive = false; shipAlive = false;
gameOver = true; gameOver = true;
Environment::shipState.velocity = 0.0f; Environment::shipState.velocity = 0.0f;
clearPlayerListIfVisible();
menuManager.showGameOver(this->playerScore); menuManager.showGameOver(this->playerScore);
} }
else { else {
deadRemotePlayers.insert(d.targetId); deadRemotePlayers.insert(d.targetId);
if (d.targetId == manualTrackedTargetId) manualTrackedTargetId = -1;
std::cout << "Marked remote player " << d.targetId << " as dead" << std::endl; std::cout << "Marked remote player " << d.targetId << " as dead" << std::endl;
} }
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;
} }
} }
rebuildPlayerListIfVisible();
} }
auto respawns = networkClient->getPendingRespawns(); auto respawns = networkClient->getPendingRespawns();
@ -1870,18 +1999,22 @@ namespace ZL
std::cout << "Client: Remote player " << respawnId << " respawned, removed from dead list" << std::endl; std::cout << "Client: Remote player " << respawnId << " respawned, removed from dead list" << std::endl;
} }
} }
rebuildPlayerListIfVisible();
auto disconnects = networkClient->getPendingDisconnects(); auto disconnects = networkClient->getPendingDisconnects();
for (int pid : disconnects) { for (int pid : disconnects) {
remotePlayerStates.erase(pid); remotePlayerStates.erase(pid);
deadRemotePlayers.erase(pid); deadRemotePlayers.erase(pid);
remoteShipSparkEmitters.erase(pid);
if (trackedTargetId == pid) { if (trackedTargetId == pid) {
trackedTargetId = -1; trackedTargetId = -1;
targetAcquireAnim = 0.f; targetAcquireAnim = 0.f;
} }
if (pid == manualTrackedTargetId) manualTrackedTargetId = -1;
std::cout << "Client: Remote player " << pid << " left the game, removed from scene\n"; std::cout << "Client: Remote player " << pid << " left the game, removed from scene\n";
} }
rebuildPlayerListIfVisible();
auto boxDestructions = networkClient->getPendingBoxDestructions(); auto boxDestructions = networkClient->getPendingBoxDestructions();
if (!boxDestructions.empty()) { if (!boxDestructions.empty()) {
std::cout << "Game: Received " << boxDestructions.size() << " box destruction events" << std::endl; std::cout << "Game: Received " << boxDestructions.size() << " box destruction events" << std::endl;
@ -1911,17 +2044,59 @@ 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)
{ {
Environment::tapDownHold = true; if (playerListVisible) return;
Environment::tapDownHold = true;
if (inverseVertical)
{
Environment::tapDownStartPos(0) = mx; Environment::tapDownStartPos(0) = mx;
Environment::tapDownStartPos(1) = my; Environment::tapDownStartPos(1) = my;
Environment::tapDownCurrentPos(0) = mx; Environment::tapDownCurrentPos(0) = mx;
Environment::tapDownCurrentPos(1) = my; Environment::tapDownCurrentPos(1) = my;
}
else
{
Environment::tapDownStartPos(0) = mx;
Environment::tapDownStartPos(1) = -my;
Environment::tapDownCurrentPos(0) = mx;
Environment::tapDownCurrentPos(1) = -my;
}
} }
void Space::handleUp(int mx, int my) void Space::handleUp(int mx, int my)
@ -1932,9 +2107,22 @@ namespace ZL
void Space::handleMotion(int mx, int my) void Space::handleMotion(int mx, int my)
{ {
if (Environment::tapDownHold) { if (playerListVisible) return;
Environment::tapDownCurrentPos(0) = mx;
Environment::tapDownCurrentPos(1) = my;
if (inverseVertical)
{
if (Environment::tapDownHold) {
Environment::tapDownCurrentPos(0) = mx;
Environment::tapDownCurrentPos(1) = my;
}
}
else
{
if (Environment::tapDownHold) {
Environment::tapDownCurrentPos(0) = mx;
Environment::tapDownCurrentPos(1) = -my;
}
} }
} }
@ -1969,4 +2157,151 @@ namespace ZL
}*/ }*/
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 } // namespace ZL

View File

@ -66,6 +66,7 @@ namespace ZL {
std::unique_ptr<TextRenderer> textRenderer; std::unique_ptr<TextRenderer> textRenderer;
std::unordered_map<int, ClientState> remotePlayerStates; std::unordered_map<int, ClientState> remotePlayerStates;
std::unordered_map<int, SparkEmitter> remoteShipSparkEmitters;
float newShipVelocity = 0; float newShipVelocity = 0;
@ -112,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;
@ -124,8 +126,12 @@ namespace ZL {
std::unordered_set<int> deadRemotePlayers; std::unordered_set<int> deadRemotePlayers;
int playerScore = 0; int playerScore = 0;
int prevPlayerScore = 0;
bool wasConnectedToServer = false; bool wasConnectedToServer = false;
bool playerListVisible = false;
int manualTrackedTargetId = -1;
static constexpr float TARGET_MAX_DIST = 50000.0f; static constexpr float TARGET_MAX_DIST = 50000.0f;
static constexpr float TARGET_MAX_DIST_SQ = TARGET_MAX_DIST * TARGET_MAX_DIST; static constexpr float TARGET_MAX_DIST_SQ = TARGET_MAX_DIST * TARGET_MAX_DIST;
@ -144,6 +150,17 @@ namespace ZL {
void resetPlayerState(); void resetPlayerState();
void clearTextRendererCache(); void clearTextRendererCache();
// Player list overlay
void buildAndShowPlayerList();
void closePlayerList();
void rebuildPlayerListIfVisible();
void clearPlayerListIfVisible();
std::shared_ptr<UiNode> buildPlayerListRoot();
void updateSparkEmitters(float deltaMs);
void prepareSparkEmittersForDraw();
void drawShipSparkEmitters();
// Crosshair HUD // Crosshair HUD
struct CrosshairConfig { struct CrosshairConfig {
bool enabled = true; bool enabled = true;

View File

@ -26,12 +26,12 @@ namespace ZL {
sparkQuad.data = VertexDataStruct(); sparkQuad.data = VertexDataStruct();
} }
SparkEmitter::SparkEmitter(const SparkEmitter& copyFrom) SparkEmitter::SparkEmitter(const SparkEmitter& copyFrom)
: particles(copyFrom.particles), emissionPoints(copyFrom.emissionPoints), : particles(copyFrom.particles), emissionPoints(copyFrom.emissionPoints),
lastEmissionTime(copyFrom.lastEmissionTime), emissionRate(copyFrom.emissionRate), lastEmissionTime(copyFrom.lastEmissionTime), emissionRate(copyFrom.emissionRate),
isActive(copyFrom.isActive), drawPositions(copyFrom.drawPositions), isActive(copyFrom.isActive), drawPositions(copyFrom.drawPositions),
drawTexCoords(copyFrom.drawTexCoords), drawDataDirty(copyFrom.drawDataDirty), drawTexCoords(copyFrom.drawTexCoords), drawDataDirty(true),
sparkQuad(copyFrom.sparkQuad), texture(copyFrom.texture), texture(copyFrom.texture),
maxParticles(copyFrom.maxParticles), particleSize(copyFrom.particleSize), maxParticles(copyFrom.maxParticles), particleSize(copyFrom.particleSize),
biasX(copyFrom.biasX), speedRange(copyFrom.speedRange), biasX(copyFrom.biasX), speedRange(copyFrom.speedRange),
zSpeedRange(copyFrom.zSpeedRange), zSpeedRange(copyFrom.zSpeedRange),
@ -40,6 +40,8 @@ namespace ZL {
shaderProgramName(copyFrom.shaderProgramName), shaderProgramName(copyFrom.shaderProgramName),
configured(copyFrom.configured), useWorldSpace(copyFrom.useWorldSpace) configured(copyFrom.configured), useWorldSpace(copyFrom.useWorldSpace)
{ {
// Each copy gets its own GPU buffers; only copy CPU-side data
sparkQuad.data = copyFrom.sparkQuad.data;
} }
@ -198,16 +200,10 @@ namespace ZL {
throw std::runtime_error("Failed to load spark emitter config file 2!"); throw std::runtime_error("Failed to load spark emitter config file 2!");
} }
//prepareDrawData(withRotation);
if (drawPositions.empty()) { if (drawPositions.empty()) {
return; return;
} }
/*
sparkQuad.data.PositionData = drawPositions;
sparkQuad.data.TexCoordData = drawTexCoords;
sparkQuad.RefreshVBO();
*/
renderer.shaderManager.PushShader(shaderProgramName); renderer.shaderManager.PushShader(shaderProgramName);
renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1i(textureUniformName, 0);
renderer.SetMatrix(); renderer.SetMatrix();

View File

@ -72,6 +72,65 @@ namespace ZL {
renderer.PopMatrix(); renderer.PopMatrix();
} }
void UiTextButton::buildMesh() {
mesh.data.PositionData.clear();
mesh.data.TexCoordData.clear();
float x0 = rect.x;
float y0 = rect.y;
float x1 = rect.x + rect.w;
float y1 = rect.y + rect.h;
mesh.data.PositionData.push_back({ x0, y0, 0 });
mesh.data.TexCoordData.push_back({ 0, 0 });
mesh.data.PositionData.push_back({ x0, y1, 0 });
mesh.data.TexCoordData.push_back({ 0, 1 });
mesh.data.PositionData.push_back({ x1, y1, 0 });
mesh.data.TexCoordData.push_back({ 1, 1 });
mesh.data.PositionData.push_back({ x0, y0, 0 });
mesh.data.TexCoordData.push_back({ 0, 0 });
mesh.data.PositionData.push_back({ x1, y1, 0 });
mesh.data.TexCoordData.push_back({ 1, 1 });
mesh.data.PositionData.push_back({ x1, y0, 0 });
mesh.data.TexCoordData.push_back({ 1, 0 });
mesh.RefreshVBO();
}
void UiTextButton::draw(Renderer& renderer) const {
renderer.PushMatrix();
renderer.TranslateMatrix({ animOffsetX, animOffsetY, 0.0f });
renderer.ScaleMatrix({ animScaleX, animScaleY, 1.0f });
// Draw background texture (optional)
const std::shared_ptr<Texture>* tex = nullptr;
switch (state) {
case ButtonState::Normal: if (texNormal) tex = &texNormal; break;
case ButtonState::Hover: tex = texHover ? &texHover : (texNormal ? &texNormal : nullptr); break;
case ButtonState::Pressed: tex = texPressed ? &texPressed : (texNormal ? &texNormal : nullptr); break;
case ButtonState::Disabled: tex = texDisabled ? &texDisabled : (texNormal ? &texNormal : nullptr); break;
}
if (tex && *tex) {
renderer.RenderUniform1i(textureUniformName, 0);
glBindTexture(GL_TEXTURE_2D, (*tex)->getTexID());
renderer.DrawVertexRenderStruct(mesh);
}
renderer.PopMatrix();
// Draw text on top (uses absolute coords, add anim offset manually)
if (textRenderer && !text.empty()) {
float cx = rect.x + rect.w / 2.0f + animOffsetX;
float cy = rect.y + rect.h / 2.0f + animOffsetY;
textRenderer->drawText(text, cx, cy, 1.0f, textCentered, color);
}
}
void UiSlider::buildTrackMesh() { void UiSlider::buildTrackMesh() {
trackMesh.data.PositionData.clear(); trackMesh.data.PositionData.clear();
trackMesh.data.TexCoordData.clear(); trackMesh.data.TexCoordData.clear();
@ -393,6 +452,49 @@ namespace ZL {
node->textField = tf; node->textField = tf;
} }
if (typeStr == "TextButton") {
auto tb = std::make_shared<UiTextButton>();
tb->name = node->name;
tb->rect = initialRect;
tb->border = j.value("border", 0.0f);
// Textures are optional
if (j.contains("textures") && j["textures"].is_object()) {
auto t = j["textures"];
auto loadTex = [&](const std::string& key) -> std::shared_ptr<Texture> {
if (!t.contains(key) || !t[key].is_string()) return nullptr;
std::string path = t[key].get<std::string>();
try {
auto data = CreateTextureDataFromPng(path.c_str(), zipFile.c_str());
return std::make_shared<Texture>(data);
}
catch (const std::exception& e) {
std::cerr << "UiManager: TextButton '" << tb->name << "' failed to load texture " << path << ": " << e.what() << std::endl;
return nullptr;
}
};
tb->texNormal = loadTex("normal");
tb->texHover = loadTex("hover");
tb->texPressed = loadTex("pressed");
tb->texDisabled = loadTex("disabled");
}
if (j.contains("text")) tb->text = j["text"].get<std::string>();
if (j.contains("fontPath")) tb->fontPath = j["fontPath"].get<std::string>();
if (j.contains("fontSize")) tb->fontSize = j["fontSize"].get<int>();
if (j.contains("textCentered")) tb->textCentered = j["textCentered"].get<bool>();
if (j.contains("color") && j["color"].is_array() && j["color"].size() == 4) {
for (int i = 0; i < 4; ++i) tb->color[i] = j["color"][i].get<float>();
}
tb->textRenderer = std::make_unique<TextRenderer>();
if (!tb->textRenderer->init(renderer, tb->fontPath, tb->fontSize, zipFile)) {
std::cerr << "UiManager: Failed to init TextRenderer for TextButton: " << tb->name << std::endl;
}
node->textButton = tb;
}
if (j.contains("animations") && j["animations"].is_object()) { if (j.contains("animations") && j["animations"].is_object()) {
for (auto it = j["animations"].begin(); it != j["animations"].end(); ++it) { for (auto it = j["animations"].begin(); it != j["animations"].end(); ++it) {
std::string animName = it.key(); std::string animName = it.key();
@ -536,6 +638,7 @@ namespace ZL {
root->localY // finalLocalY root->localY // finalLocalY
); );
buttons.clear(); buttons.clear();
textButtons.clear();
sliders.clear(); sliders.clear();
textViews.clear(); textViews.clear();
textFields.clear(); textFields.clear();
@ -547,6 +650,9 @@ namespace ZL {
for (auto& b : buttons) { for (auto& b : buttons) {
b->buildMesh(); b->buildMesh();
} }
for (auto& tb : textButtons) {
tb->buildMesh();
}
for (auto& s : sliders) { for (auto& s : sliders) {
s->buildTrackMesh(); s->buildTrackMesh();
s->buildKnobMesh(); s->buildKnobMesh();
@ -694,11 +800,14 @@ namespace ZL {
// 1. Обновляем кнопку // 1. Обновляем кнопку
if (node->button) { if (node->button) {
node->button->rect = node->screenRect; node->button->rect = node->screenRect;
// Если у кнопки есть анимационные смещения, они учитываются внутри buildMesh
// или при рендеринге через Uniform-переменные матрицы модели.
node->button->buildMesh(); node->button->buildMesh();
} }
if (node->textButton) {
node->textButton->rect = node->screenRect;
node->textButton->buildMesh();
}
// 2. Обновляем слайдер // 2. Обновляем слайдер
if (node->slider) { if (node->slider) {
node->slider->rect = node->screenRect; node->slider->rect = node->screenRect;
@ -744,6 +853,9 @@ namespace ZL {
if (node->button) { if (node->button) {
buttons.push_back(node->button); buttons.push_back(node->button);
} }
if (node->textButton) {
textButtons.push_back(node->textButton);
}
if (node->slider) { if (node->slider) {
sliders.push_back(node->slider); sliders.push_back(node->slider);
} }
@ -862,10 +974,13 @@ namespace ZL {
MenuState prev; MenuState prev;
prev.root = root; prev.root = root;
prev.buttons = buttons; prev.buttons = buttons;
prev.textButtons = textButtons;
prev.sliders = sliders; prev.sliders = sliders;
prev.textViews = textViews;
prev.textFields = textFields; prev.textFields = textFields;
prev.staticImages = staticImages; prev.staticImages = staticImages;
prev.pressedButtons = pressedButtons; prev.pressedButtons = pressedButtons;
prev.pressedTextButtons = pressedTextButtons;
prev.pressedSliders = pressedSliders; prev.pressedSliders = pressedSliders;
prev.focusedTextField = focusedTextField; prev.focusedTextField = focusedTextField;
prev.path = ""; prev.path = "";
@ -884,6 +999,14 @@ namespace ZL {
b->animScaleY = 1.0f; b->animScaleY = 1.0f;
} }
} }
for (auto& tb : textButtons) {
if (tb) {
tb->animOffsetX = 0.0f;
tb->animOffsetY = 0.0f;
tb->animScaleX = 1.0f;
tb->animScaleY = 1.0f;
}
}
replaceRoot(newRoot); replaceRoot(newRoot);
menuStack.push_back(std::move(prev)); menuStack.push_back(std::move(prev));
@ -913,10 +1036,13 @@ namespace ZL {
root = s.root; root = s.root;
buttons = s.buttons; buttons = s.buttons;
textButtons = s.textButtons;
sliders = s.sliders; sliders = s.sliders;
textViews = s.textViews;
textFields = s.textFields; textFields = s.textFields;
staticImages = s.staticImages; staticImages = s.staticImages;
pressedButtons = s.pressedButtons; pressedButtons = s.pressedButtons;
pressedTextButtons = s.pressedTextButtons;
pressedSliders = s.pressedSliders; pressedSliders = s.pressedSliders;
focusedTextField = s.focusedTextField; focusedTextField = s.focusedTextField;
@ -931,6 +1057,15 @@ namespace ZL {
b->buildMesh(); b->buildMesh();
} }
} }
for (auto& tb : textButtons) {
if (tb) {
tb->animOffsetX = 0.0f;
tb->animOffsetY = 0.0f;
tb->animScaleX = 1.0f;
tb->animScaleY = 1.0f;
tb->buildMesh();
}
}
for (auto& sl : sliders) { for (auto& sl : sliders) {
if (sl) { if (sl) {
@ -956,6 +1091,9 @@ namespace ZL {
for (const auto& b : buttons) { for (const auto& b : buttons) {
b->draw(renderer); b->draw(renderer);
} }
for (const auto& tb : textButtons) {
tb->draw(renderer);
}
for (const auto& s : sliders) { for (const auto& s : sliders) {
s->draw(renderer); s->draw(renderer);
} }
@ -1007,6 +1145,12 @@ namespace ZL {
node->button->animScaleX = act.origScaleX; node->button->animScaleX = act.origScaleX;
node->button->animScaleY = act.origScaleY; node->button->animScaleY = act.origScaleY;
} }
if (node->textButton) {
node->textButton->animOffsetX = act.origOffsetX;
node->textButton->animOffsetY = act.origOffsetY;
node->textButton->animScaleX = act.origScaleX;
node->textButton->animScaleY = act.origScaleY;
}
act.stepIndex = 0; act.stepIndex = 0;
act.elapsedMs = 0.0f; act.elapsedMs = 0.0f;
act.stepStarted = false; act.stepStarted = false;
@ -1028,12 +1172,20 @@ namespace ZL {
node->button->animOffsetX = step.toX; node->button->animOffsetX = step.toX;
node->button->animOffsetY = step.toY; node->button->animOffsetY = step.toY;
} }
if (node->textButton) {
node->textButton->animOffsetX = step.toX;
node->textButton->animOffsetY = step.toY;
}
} }
else if (step.type == "scale") { else if (step.type == "scale") {
if (node->button) { if (node->button) {
node->button->animScaleX = step.toX; node->button->animScaleX = step.toX;
node->button->animScaleY = step.toY; node->button->animScaleY = step.toY;
} }
if (node->textButton) {
node->textButton->animScaleX = step.toX;
node->textButton->animScaleY = step.toY;
}
} }
act.stepIndex++; act.stepIndex++;
act.elapsedMs = 0.0f; act.elapsedMs = 0.0f;
@ -1048,6 +1200,12 @@ namespace ZL {
act.origScaleX = node->button->animScaleX; act.origScaleX = node->button->animScaleX;
act.origScaleY = node->button->animScaleY; act.origScaleY = node->button->animScaleY;
} }
else if (node->textButton) {
act.origOffsetX = node->textButton->animOffsetX;
act.origOffsetY = node->textButton->animOffsetY;
act.origScaleX = node->textButton->animScaleX;
act.origScaleY = node->textButton->animScaleY;
}
else { else {
act.origOffsetX = act.origOffsetY = 0.0f; act.origOffsetX = act.origOffsetY = 0.0f;
act.origScaleX = act.origScaleY = 1.0f; act.origScaleX = act.origScaleY = 1.0f;
@ -1064,6 +1222,12 @@ namespace ZL {
act.startScaleX = node->button->animScaleX; act.startScaleX = node->button->animScaleX;
act.startScaleY = node->button->animScaleY; act.startScaleY = node->button->animScaleY;
} }
else if (node->textButton) {
act.startOffsetX = node->textButton->animOffsetX;
act.startOffsetY = node->textButton->animOffsetY;
act.startScaleX = node->textButton->animScaleX;
act.startScaleY = node->textButton->animScaleY;
}
else { else {
act.startOffsetX = act.startOffsetY = 0.0f; act.startOffsetX = act.startOffsetY = 0.0f;
act.startScaleX = act.startScaleY = 1.0f; act.startScaleX = act.startScaleY = 1.0f;
@ -1089,6 +1253,10 @@ namespace ZL {
node->button->animOffsetX = nx; node->button->animOffsetX = nx;
node->button->animOffsetY = ny; node->button->animOffsetY = ny;
} }
if (node->textButton) {
node->textButton->animOffsetX = nx;
node->textButton->animOffsetY = ny;
}
} }
else if (step.type == "scale") { else if (step.type == "scale") {
float sx = act.startScaleX + (act.endScaleX - act.startScaleX) * te; float sx = act.startScaleX + (act.endScaleX - act.startScaleX) * te;
@ -1097,6 +1265,10 @@ namespace ZL {
node->button->animScaleX = sx; node->button->animScaleX = sx;
node->button->animScaleY = sy; node->button->animScaleY = sy;
} }
if (node->textButton) {
node->textButton->animScaleX = sx;
node->textButton->animScaleY = sy;
}
} }
else if (step.type == "wait") { else if (step.type == "wait") {
//wait //wait
@ -1146,6 +1318,17 @@ namespace ZL {
} }
} }
} }
for (auto& tb : textButtons) {
if (tb->state != ButtonState::Disabled)
{
if (tb->rect.containsConsideringBorder((float)x, (float)y, tb->border)) {
if (tb->state != ButtonState::Pressed) tb->state = ButtonState::Hover;
}
else {
if (tb->state != ButtonState::Pressed) tb->state = ButtonState::Normal;
}
}
}
} }
auto it = pressedSliders.find(fingerId); auto it = pressedSliders.find(fingerId);
@ -1180,6 +1363,18 @@ namespace ZL {
} }
} }
for (auto& tb : textButtons) {
if (tb->state != ButtonState::Disabled)
{
if (tb->rect.containsConsideringBorder((float)x, (float)y, tb->border)) {
tb->state = ButtonState::Pressed;
pressedTextButtons[fingerId] = tb;
if (tb->onPress) tb->onPress(tb->name);
break;
}
}
}
for (auto& s : sliders) { for (auto& s : sliders) {
if (s->rect.contains((float)x, (float)y)) { if (s->rect.contains((float)x, (float)y)) {
pressedSliders[fingerId] = s; pressedSliders[fingerId] = s;
@ -1212,6 +1407,7 @@ namespace ZL {
void UiManager::onTouchUp(int64_t fingerId, int x, int y) { void UiManager::onTouchUp(int64_t fingerId, int x, int y) {
std::vector<std::shared_ptr<UiButton>> clicked; std::vector<std::shared_ptr<UiButton>> clicked;
std::vector<std::shared_ptr<UiTextButton>> clickedText;
auto btnIt = pressedButtons.find(fingerId); auto btnIt = pressedButtons.find(fingerId);
if (btnIt != pressedButtons.end()) { if (btnIt != pressedButtons.end()) {
@ -1229,6 +1425,21 @@ namespace ZL {
pressedButtons.erase(btnIt); pressedButtons.erase(btnIt);
} }
auto tbIt = pressedTextButtons.find(fingerId);
if (tbIt != pressedTextButtons.end()) {
auto tb = tbIt->second;
if (tb) {
bool contains = tb->rect.contains((float)x, (float)y);
if (tb->state == ButtonState::Pressed) {
if (contains) {
clickedText.push_back(tb);
}
tb->state = (contains && fingerId == MOUSE_FINGER_ID) ? ButtonState::Hover : ButtonState::Normal;
}
}
pressedTextButtons.erase(tbIt);
}
pressedSliders.erase(fingerId); pressedSliders.erase(fingerId);
for (auto& b : clicked) { for (auto& b : clicked) {
@ -1236,6 +1447,11 @@ namespace ZL {
b->onClick(b->name); b->onClick(b->name);
} }
} }
for (auto& tb : clickedText) {
if (tb->onClick) {
tb->onClick(tb->name);
}
}
} }
void UiManager::onKeyPress(unsigned char key) { void UiManager::onKeyPress(unsigned char key) {
@ -1287,6 +1503,12 @@ namespace ZL {
aa.origScaleX = node->button->animScaleX; aa.origScaleX = node->button->animScaleX;
aa.origScaleY = node->button->animScaleY; aa.origScaleY = node->button->animScaleY;
} }
else if (node->textButton) {
aa.origOffsetX = node->textButton->animOffsetX;
aa.origOffsetY = node->textButton->animOffsetY;
aa.origScaleX = node->textButton->animScaleX;
aa.origScaleY = node->textButton->animScaleY;
}
auto cbIt = animCallbacks.find({ nodeName, animName }); auto cbIt = animCallbacks.find({ nodeName, animName });
if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second; if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second;
nodeActiveAnims[node].push_back(std::move(aa)); nodeActiveAnims[node].push_back(std::move(aa));
@ -1337,6 +1559,12 @@ namespace ZL {
aa.origScaleX = n->button->animScaleX; aa.origScaleX = n->button->animScaleX;
aa.origScaleY = n->button->animScaleY; aa.origScaleY = n->button->animScaleY;
} }
else if (n->textButton) {
aa.origOffsetX = n->textButton->animOffsetX;
aa.origOffsetY = n->textButton->animOffsetY;
aa.origScaleX = n->textButton->animScaleX;
aa.origScaleY = n->textButton->animScaleY;
}
auto cbIt = animCallbacks.find({ n->name, animName }); auto cbIt = animCallbacks.find({ n->name, animName });
if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second; if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second;
nodeActiveAnims[n].push_back(std::move(aa)); nodeActiveAnims[n].push_back(std::move(aa));
@ -1374,4 +1602,36 @@ namespace ZL {
return true; return true;
} }
std::shared_ptr<UiTextButton> UiManager::findTextButton(const std::string& name) {
for (auto& tb : textButtons) if (tb->name == name) return tb;
return nullptr;
}
bool UiManager::setTextButtonCallback(const std::string& name, std::function<void(const std::string&)> cb) {
auto tb = findTextButton(name);
if (!tb) {
std::cerr << "UiManager: setTextButtonCallback failed, textButton not found: " << name << std::endl;
return false;
}
tb->onClick = std::move(cb);
return true;
}
bool UiManager::setTextButtonPressCallback(const std::string& name, std::function<void(const std::string&)> cb) {
auto tb = findTextButton(name);
if (!tb) {
std::cerr << "UiManager: setTextButtonPressCallback failed, textButton not found: " << name << std::endl;
return false;
}
tb->onPress = std::move(cb);
return true;
}
bool UiManager::setTextButtonText(const std::string& name, const std::string& newText) {
auto tb = findTextButton(name);
if (!tb) return false;
tb->text = newText;
return true;
}
} // namespace ZL } // namespace ZL

View File

@ -125,6 +125,42 @@ namespace ZL {
void draw(Renderer& renderer) const; void draw(Renderer& renderer) const;
}; };
struct UiTextButton {
std::string name;
UiRect rect;
float border = 0;
// Textures are optional — button can be text-only
std::shared_ptr<Texture> texNormal;
std::shared_ptr<Texture> texHover;
std::shared_ptr<Texture> texPressed;
std::shared_ptr<Texture> texDisabled;
ButtonState state = ButtonState::Normal;
VertexRenderStruct mesh;
// Text drawn on top of the button
std::string text;
std::string fontPath = "resources/fonts/DroidSans.ttf";
int fontSize = 32;
std::array<float, 4> color = { 1.f, 1.f, 1.f, 1.f };
bool textCentered = true;
std::unique_ptr<TextRenderer> textRenderer;
std::function<void(const std::string&)> onClick;
std::function<void(const std::string&)> onPress;
// Animation runtime
float animOffsetX = 0.0f;
float animOffsetY = 0.0f;
float animScaleX = 1.0f;
float animScaleY = 1.0f;
void buildMesh();
void draw(Renderer& renderer) const;
};
struct UiTextView { struct UiTextView {
std::string name; std::string name;
UiRect rect; UiRect rect;
@ -197,6 +233,7 @@ namespace ZL {
// Компоненты (только один из них обычно активен для ноды) // Компоненты (только один из них обычно активен для ноды)
std::shared_ptr<UiButton> button; std::shared_ptr<UiButton> button;
std::shared_ptr<UiTextButton> textButton;
std::shared_ptr<UiSlider> slider; std::shared_ptr<UiSlider> slider;
std::shared_ptr<UiTextView> textView; std::shared_ptr<UiTextView> textView;
std::shared_ptr<UiTextField> textField; std::shared_ptr<UiTextField> textField;
@ -249,12 +286,12 @@ namespace ZL {
// Returns true if any finger is currently interacting with UI // Returns true if any finger is currently interacting with UI
bool isUiInteraction() const { bool isUiInteraction() const {
return !pressedButtons.empty() || !pressedSliders.empty() || focusedTextField != nullptr; return !pressedButtons.empty() || !pressedTextButtons.empty() || !pressedSliders.empty() || focusedTextField != nullptr;
} }
// Returns true if this specific finger is currently interacting with UI // Returns true if this specific finger is currently interacting with UI
bool isUiInteractionForFinger(int64_t fingerId) const { bool isUiInteractionForFinger(int64_t fingerId) const {
return pressedButtons.count(fingerId) > 0 || pressedSliders.count(fingerId) > 0 || focusedTextField != nullptr; return pressedButtons.count(fingerId) > 0 || pressedTextButtons.count(fingerId) > 0 || pressedSliders.count(fingerId) > 0 || focusedTextField != nullptr;
} }
void stopAllAnimations() { void stopAllAnimations() {
@ -268,6 +305,14 @@ namespace ZL {
b->animScaleY = 1.0f; b->animScaleY = 1.0f;
} }
} }
for (auto& tb : textButtons) {
if (tb) {
tb->animOffsetX = 0.0f;
tb->animOffsetY = 0.0f;
tb->animScaleX = 1.0f;
tb->animScaleY = 1.0f;
}
}
} }
std::shared_ptr<UiButton> findButton(const std::string& name); std::shared_ptr<UiButton> findButton(const std::string& name);
@ -275,6 +320,11 @@ namespace ZL {
bool setButtonCallback(const std::string& name, std::function<void(const std::string&)> cb); bool setButtonCallback(const std::string& name, std::function<void(const std::string&)> cb);
bool setButtonPressCallback(const std::string& name, std::function<void(const std::string&)> cb); bool setButtonPressCallback(const std::string& name, std::function<void(const std::string&)> cb);
std::shared_ptr<UiTextButton> findTextButton(const std::string& name);
bool setTextButtonCallback(const std::string& name, std::function<void(const std::string&)> cb);
bool setTextButtonPressCallback(const std::string& name, std::function<void(const std::string&)> cb);
bool setTextButtonText(const std::string& name, const std::string& newText);
bool addSlider(const std::string& name, const UiRect& rect, Renderer& renderer, const std::string& zipFile, bool addSlider(const std::string& name, const UiRect& rect, Renderer& renderer, const std::string& zipFile,
const std::string& trackPath, const std::string& knobPath, float initialValue = 0.0f, bool vertical = true); const std::string& trackPath, const std::string& knobPath, float initialValue = 0.0f, bool vertical = true);
@ -333,6 +383,7 @@ namespace ZL {
std::shared_ptr<UiNode> root; std::shared_ptr<UiNode> root;
std::vector<std::shared_ptr<UiButton>> buttons; std::vector<std::shared_ptr<UiButton>> buttons;
std::vector<std::shared_ptr<UiTextButton>> textButtons;
std::vector<std::shared_ptr<UiSlider>> sliders; std::vector<std::shared_ptr<UiSlider>> sliders;
std::vector<std::shared_ptr<UiTextView>> textViews; std::vector<std::shared_ptr<UiTextView>> textViews;
std::vector<std::shared_ptr<UiTextField>> textFields; std::vector<std::shared_ptr<UiTextField>> textFields;
@ -343,16 +394,20 @@ namespace ZL {
// Per-finger tracking for multi-touch support // Per-finger tracking for multi-touch support
std::map<int64_t, std::shared_ptr<UiButton>> pressedButtons; std::map<int64_t, std::shared_ptr<UiButton>> pressedButtons;
std::map<int64_t, std::shared_ptr<UiTextButton>> pressedTextButtons;
std::map<int64_t, std::shared_ptr<UiSlider>> pressedSliders; std::map<int64_t, std::shared_ptr<UiSlider>> pressedSliders;
std::shared_ptr<UiTextField> focusedTextField; std::shared_ptr<UiTextField> focusedTextField;
struct MenuState { struct MenuState {
std::shared_ptr<UiNode> root; std::shared_ptr<UiNode> root;
std::vector<std::shared_ptr<UiButton>> buttons; std::vector<std::shared_ptr<UiButton>> buttons;
std::vector<std::shared_ptr<UiTextButton>> textButtons;
std::vector<std::shared_ptr<UiSlider>> sliders; std::vector<std::shared_ptr<UiSlider>> sliders;
std::vector<std::shared_ptr<UiTextView>> textViews;
std::vector<std::shared_ptr<UiTextField>> textFields; std::vector<std::shared_ptr<UiTextField>> textFields;
std::vector<std::shared_ptr<UiStaticImage>> staticImages; std::vector<std::shared_ptr<UiStaticImage>> staticImages;
std::map<int64_t, std::shared_ptr<UiButton>> pressedButtons; std::map<int64_t, std::shared_ptr<UiButton>> pressedButtons;
std::map<int64_t, std::shared_ptr<UiTextButton>> pressedTextButtons;
std::map<int64_t, std::shared_ptr<UiSlider>> pressedSliders; std::map<int64_t, std::shared_ptr<UiSlider>> pressedSliders;
std::shared_ptr<UiTextField> focusedTextField; std::shared_ptr<UiTextField> focusedTextField;
std::string path; std::string path;

View File

@ -31,10 +31,11 @@ constexpr long long PLAYER_TIMEOUT_MS = 10000; //ms — disconnect if no UPD rec
constexpr float PROJECTILE_VELOCITY = 600.f; constexpr float PROJECTILE_VELOCITY = 600.f;
constexpr float PROJECTILE_LIFE = 15000.f; //ms constexpr float PROJECTILE_LIFE = 15000.f; //ms
const float projectileHitRadius = 1.5f * 5; const float projectileHitRadius = 1.5f * 4;
const float boxCollisionRadius = 2.0f * 5; const float boxCollisionRadius = 2.0f * 4;
const float shipCollisionRadius = 15.0f * 5; const float shipCollisionRadius = 15.0f * 3;
const float npcCollisionRadius = 5.0f * 5; const float BOX_PICKUP_RADIUS = shipCollisionRadius * 3;
const float npcCollisionRadius = 5.0f * 3;
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; }

View File

@ -9,7 +9,7 @@ namespace ZL {
// Формируем URL. Обратите внимание, что в Web часто лучше использовать ws://localhost // Формируем URL. Обратите внимание, что в Web часто лучше использовать ws://localhost
//std::string url = "ws://" + host + ":" + std::to_string(port); //std::string url = "ws://" + host + ":" + std::to_string(port);
std::string url = "wss://api.spacegame.fishrungames.com"; std::string url = "wss://api.spacegame.fishrungames.com";
//std::string url = "ws://localhost:8081";
EmscriptenWebSocketCreateAttributes attr = { EmscriptenWebSocketCreateAttributes attr = {
url.c_str(), url.c_str(),
nullptr, nullptr,

View File

@ -617,6 +617,7 @@ namespace ZL {
{ {
throw std::runtime_error("Modelview matrix stack overflow!!!!"); throw std::runtime_error("Modelview matrix stack overflow!!!!");
} }
SetMatrix();
} }
void Renderer::LoadIdentity() void Renderer::LoadIdentity()
@ -922,8 +923,6 @@ namespace ZL {
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.positionVBO->getBuffer()); glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.positionVBO->getBuffer());
VertexAttribPointer3fv(vPosition, 0, NULL); VertexAttribPointer3fv(vPosition, 0, NULL);
//EnableVertexAttribArray(vPosition);
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(VertexRenderStruct.data.PositionData.size())); glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(VertexRenderStruct.data.PositionData.size()));
} }