From 24aa7007bb21e32f879cfa150e5c70fb06766ff2 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Sun, 8 Mar 2026 21:27:45 +0300 Subject: [PATCH 1/6] Added trails to other ships toos --- resources/config/spark_config.json | 2 +- resources/config/spark_config_cargo.json | 2 +- src/Game.cpp | 40 +++--- src/Space.cpp | 148 ++++++++++++++--------- src/Space.h | 5 + src/SparkEmitter.cpp | 16 +-- src/render/Renderer.cpp | 3 +- 7 files changed, 128 insertions(+), 88 deletions(-) diff --git a/resources/config/spark_config.json b/resources/config/spark_config.json index 2329820..2a46f11 100644 --- a/resources/config/spark_config.json +++ b/resources/config/spark_config.json @@ -1,5 +1,5 @@ { - "emissionRate": 0.4, + "emissionRate": 1.2, "maxParticles": 400, "particleSize": 0.3, "biasX": 0.3, diff --git a/resources/config/spark_config_cargo.json b/resources/config/spark_config_cargo.json index 36816c7..c853da0 100644 --- a/resources/config/spark_config_cargo.json +++ b/resources/config/spark_config_cargo.json @@ -1,5 +1,5 @@ { - "emissionRate": 0.4, + "emissionRate": 1.2, "maxParticles": 400, "particleSize": 0.3, "biasX": 0.3, diff --git a/src/Game.cpp b/src/Game.cpp index c55e31a..6936e59 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -41,6 +41,10 @@ namespace ZL const char* CONST_ZIP_FILE = ""; #endif + float x = 0; + float y = 0; + float z = 0; + #ifdef EMSCRIPTEN Game* Game::s_instance = nullptr; @@ -420,22 +424,6 @@ namespace ZL 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) { static const float zoomstep = 2.0f; if (event.wheel.y > 0) { @@ -465,7 +453,25 @@ namespace ZL if (event.type == SDL_KEYUP) { if (event.key.keysym.sym == SDLK_a) { - //Environment::shipState.position = { 9466.15820, 1046.00159, 18531.2090 }; + x = x + 0.2f; + } + if (event.key.keysym.sym == SDLK_q) { + x = x - 0.2f; + } + if (event.key.keysym.sym == SDLK_s) { + y = y + 0.2f; + } + if (event.key.keysym.sym == SDLK_w) { + y = y - 0.2f; + } + if (event.key.keysym.sym == SDLK_d) { + z = z + 0.2f; + } + if (event.key.keysym.sym == SDLK_e) { + z = z - 0.2f; + } + if (event.key.keysym.sym == SDLK_r) { + std::cout << "Camera position: x=" << x << " y=" << y << " z=" << z << std::endl; } } #endif diff --git a/src/Space.cpp b/src/Space.cpp index c3f3b67..c7cfc3d 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -33,6 +33,8 @@ namespace ZL extern const char* CONST_ZIP_FILE; extern float x; + extern float y; + extern float z; Eigen::Quaternionf generateRandomQuaternion(std::mt19937& gen) { @@ -507,17 +509,7 @@ namespace ZL } - renderer.PushMatrix(); - 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(); + drawShipSparkEmitters(); } renderer.PopMatrix(); @@ -591,16 +583,7 @@ namespace ZL glViewport(0, 0, Environment::width, Environment::height); - // Готовим данные всех эмиттеров (CPU + VBO upload) до начала отрисовки, - // чтобы draw() делал только GPU-вызовы без пауз между кораблём и частицами. - sparkEmitter.prepareForDraw(true); - sparkEmitterCargo.prepareForDraw(true); - explosionEmitter.prepareForDraw(false); - for (const auto& p : projectiles) { - if (p && p->isActive()) { - p->projectileEmitter.prepareForDraw(true); - } - } + prepareSparkEmittersForDraw(); CheckGlError(); @@ -1431,6 +1414,89 @@ namespace ZL targetWasVisible = false; } + void Space::updateSparkEmitters(float deltaMs) + { + // Local ship + SparkEmitter* sparkEmitterPtr; + if (Environment::shipState.shipType == 1) { + sparkEmitterPtr = &sparkEmitterCargo; + static std::vector 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 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 remEmitPts(2); + if (playerState.shipType == 1) { + remEmitPts[0] = playerState.position + playerState.rotation * Vector3f{ 0.0f, -0.4f+2.8f, 8.4f }; + remEmitPts[1] = playerState.position + playerState.rotation * Vector3f{ 0.0f, -0.4f+1.5f, 8.4f }; + } else { + remEmitPts[0] = playerState.position + playerState.rotation * Vector3f{ -0.9f, -0.2,5.6 }; + remEmitPts[1] = playerState.position + playerState.rotation * Vector3f{ 0.9f,-0.2,5.6 }; + } + remEmitter.setEmissionPoints(remEmitPts); + remEmitter.setIsActive(playerState.velocity > 0.1f); + remEmitter.update(deltaMs); + } + } + + void Space::prepareSparkEmittersForDraw() + { + sparkEmitter.prepareForDraw(true); + sparkEmitterCargo.prepareForDraw(true); + for (auto& [id, emitter] : remoteShipSparkEmitters) { + if (!deadRemotePlayers.count(id)) emitter.prepareForDraw(true); + } + explosionEmitter.prepareForDraw(false); + for (const auto& p : projectiles) { + if (p && p->isActive()) { + p->projectileEmitter.prepareForDraw(true); + } + } + } + + void Space::drawShipSparkEmitters() + { + renderer.PushMatrix(); + renderer.RotateMatrix(Environment::inverseShipMatrix); + renderer.TranslateMatrix(-Environment::shipState.position); + if (Environment::shipState.shipType == 1) { + sparkEmitterCargo.draw(renderer, Environment::zoom, Environment::width, Environment::height); + } else { + sparkEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); + } + + for (auto& [id, emitter] : remoteShipSparkEmitters) { + if (!deadRemotePlayers.count(id)) { + + renderer.PushMatrix(); + renderer.LoadIdentity(); + renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom }); + renderer.RotateMatrix(Environment::inverseShipMatrix); + renderer.TranslateMatrix(-Environment::shipState.position); + emitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); + renderer.PopMatrix(); + } + } + renderer.PopMatrix(); + } + void Space::processTickCount(int64_t newTickCount, int64_t delta) { auto now_ms = newTickCount; @@ -1522,7 +1588,7 @@ namespace ZL std::string msg = "UPD:" + std::to_string(now_ms) + ":" + Environment::shipState.formPingMessageContent(); networkClient->Send(msg); - std::cout << "Sending: " << msg << std::endl; + //std::cout << "Sending: " << msg << std::endl; } long long leftoverDelta = delta; @@ -1545,41 +1611,6 @@ namespace ZL } - //-------------- - - SparkEmitter* sparkEmitterPtr; - - if (Environment::shipState.shipType == 1) { - sparkEmitterPtr = &sparkEmitterCargo; - static std::vector 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 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(delta)); - - auto latestRemotePlayers = networkClient->getRemotePlayers(); std::chrono::system_clock::time_point nowRoundedWithDelay{ std::chrono::milliseconds(newTickCount - CLIENT_DELAY) }; @@ -1601,6 +1632,8 @@ namespace ZL remotePlayerStates[id] = playerState; } + updateSparkEmitters(static_cast(delta)); + for (auto& p : projectiles) { if (p && p->isActive()) { p->update(static_cast(delta), renderer); @@ -1875,6 +1908,7 @@ namespace ZL for (int pid : disconnects) { remotePlayerStates.erase(pid); deadRemotePlayers.erase(pid); + remoteShipSparkEmitters.erase(pid); if (trackedTargetId == pid) { trackedTargetId = -1; targetAcquireAnim = 0.f; diff --git a/src/Space.h b/src/Space.h index ee7ae12..05395a3 100644 --- a/src/Space.h +++ b/src/Space.h @@ -66,6 +66,7 @@ namespace ZL { std::unique_ptr textRenderer; std::unordered_map remotePlayerStates; + std::unordered_map remoteShipSparkEmitters; float newShipVelocity = 0; @@ -144,6 +145,10 @@ namespace ZL { void resetPlayerState(); void clearTextRendererCache(); + void updateSparkEmitters(float deltaMs); + void prepareSparkEmittersForDraw(); + void drawShipSparkEmitters(); + // Crosshair HUD struct CrosshairConfig { bool enabled = true; diff --git a/src/SparkEmitter.cpp b/src/SparkEmitter.cpp index 1115abf..4baa1dc 100644 --- a/src/SparkEmitter.cpp +++ b/src/SparkEmitter.cpp @@ -26,12 +26,12 @@ namespace ZL { sparkQuad.data = VertexDataStruct(); } - SparkEmitter::SparkEmitter(const SparkEmitter& copyFrom) + SparkEmitter::SparkEmitter(const SparkEmitter& copyFrom) : particles(copyFrom.particles), emissionPoints(copyFrom.emissionPoints), lastEmissionTime(copyFrom.lastEmissionTime), emissionRate(copyFrom.emissionRate), isActive(copyFrom.isActive), drawPositions(copyFrom.drawPositions), - drawTexCoords(copyFrom.drawTexCoords), drawDataDirty(copyFrom.drawDataDirty), - sparkQuad(copyFrom.sparkQuad), texture(copyFrom.texture), + drawTexCoords(copyFrom.drawTexCoords), drawDataDirty(true), + texture(copyFrom.texture), maxParticles(copyFrom.maxParticles), particleSize(copyFrom.particleSize), biasX(copyFrom.biasX), speedRange(copyFrom.speedRange), zSpeedRange(copyFrom.zSpeedRange), @@ -40,6 +40,8 @@ namespace ZL { shaderProgramName(copyFrom.shaderProgramName), 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!"); } - //prepareDrawData(withRotation); - if (drawPositions.empty()) { return; } - /* - sparkQuad.data.PositionData = drawPositions; - sparkQuad.data.TexCoordData = drawTexCoords; - sparkQuad.RefreshVBO(); - */ + renderer.shaderManager.PushShader(shaderProgramName); renderer.RenderUniform1i(textureUniformName, 0); renderer.SetMatrix(); diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp index 003e71d..f50aec0 100644 --- a/src/render/Renderer.cpp +++ b/src/render/Renderer.cpp @@ -617,6 +617,7 @@ namespace ZL { { throw std::runtime_error("Modelview matrix stack overflow!!!!"); } + SetMatrix(); } void Renderer::LoadIdentity() @@ -922,8 +923,6 @@ namespace ZL { glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.positionVBO->getBuffer()); VertexAttribPointer3fv(vPosition, 0, NULL); - //EnableVertexAttribArray(vPosition); - glDrawArrays(GL_TRIANGLES, 0, static_cast(VertexRenderStruct.data.PositionData.size())); } From 86a9f38c3bf2cf6fd9a44f0f01ef60a5821d317e Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Mon, 9 Mar 2026 13:50:59 +0300 Subject: [PATCH 2/6] Some UI changes --- resources/button_minus_disabled.png | 3 ++ resources/button_plus_disabled.png | 3 ++ resources/config/main_menu.json | 10 ++---- resources/config/ship_selection_menu.json | 7 ++++ resources/config/ui.json | 6 ++-- resources/connection_failed.png | 4 +-- resources/loading.png | 4 +-- resources/main_menu/about.png | 3 ++ resources/main_menu/about_hover.png | 3 ++ resources/main_menu/about_pressed.png | 3 ++ resources/main_menu/title.png | 4 +-- resources/select_your_ship.png | 3 ++ src/Game.cpp | 43 ++++++++++------------- src/MenuManager.cpp | 23 +++++++++++- src/Space.cpp | 7 ++++ 15 files changed, 86 insertions(+), 40 deletions(-) create mode 100644 resources/button_minus_disabled.png create mode 100644 resources/button_plus_disabled.png create mode 100644 resources/main_menu/about.png create mode 100644 resources/main_menu/about_hover.png create mode 100644 resources/main_menu/about_pressed.png create mode 100644 resources/select_your_ship.png diff --git a/resources/button_minus_disabled.png b/resources/button_minus_disabled.png new file mode 100644 index 0000000..6a990e9 --- /dev/null +++ b/resources/button_minus_disabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9667686ef6f83297b38a9b06d893fce5f1128ed8610283c32989a6bd37e9327 +size 11294 diff --git a/resources/button_plus_disabled.png b/resources/button_plus_disabled.png new file mode 100644 index 0000000..e7cf3c7 --- /dev/null +++ b/resources/button_plus_disabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08e52cc78b6e2fbade9edd79ab7f394118fdcd6ca38ea7f2d68b25852e327acd +size 9563 diff --git a/resources/config/main_menu.json b/resources/config/main_menu.json index e97adb4..c103fb2 100644 --- a/resources/config/main_menu.json +++ b/resources/config/main_menu.json @@ -13,7 +13,7 @@ { "type": "StaticImage", "name": "titleBtn", - "width": 254, + "width": 434, "height": 35, "texture": "resources/main_menu/title.png" }, @@ -25,15 +25,11 @@ "texture": "resources/main_menu/line.png" }, { - "type": "Button", + "type": "StaticImage", "name": "subtitleBtn", "width": 144, "height": 11, - "textures": { - "normal": "resources/main_menu/subtitle.png", - "hover": "resources/main_menu/subtitle.png", - "pressed": "resources/main_menu/subtitle.png" - } + "texture": "resources/main_menu/subtitle.png" }, { "type": "Button", diff --git a/resources/config/ship_selection_menu.json b/resources/config/ship_selection_menu.json index 1a305de..bba2052 100644 --- a/resources/config/ship_selection_menu.json +++ b/resources/config/ship_selection_menu.json @@ -10,6 +10,13 @@ "width": "match_parent", "height": "match_parent", "children": [ + { + "type": "StaticImage", + "name": "titleBtn", + "width": 266, + "height": 66, + "texture": "resources/select_your_ship.png" + }, { "type": "LinearLayout", "orientation": "horizontal", diff --git a/resources/config/ui.json b/resources/config/ui.json index b7971a7..65d8225 100644 --- a/resources/config/ui.json +++ b/resources/config/ui.json @@ -65,7 +65,8 @@ "textures": { "normal": "resources/button_minus.png", "hover": "resources/button_minus_pressed.png", - "pressed": "resources/button_minus_pressed.png" + "pressed": "resources/button_minus_pressed.png", + "disabled" : "resources/button_minus_disabled.png" } }, { @@ -81,7 +82,8 @@ "textures": { "normal": "resources/button_plus.png", "hover": "resources/button_plus_pressed.png", - "pressed": "resources/button_plus_pressed.png" + "pressed": "resources/button_plus_pressed.png", + "disabled" : "resources/button_plus_disabled.png" } } ] diff --git a/resources/connection_failed.png b/resources/connection_failed.png index 52515b1..c1d108c 100644 --- a/resources/connection_failed.png +++ b/resources/connection_failed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:801e00e096b469e1081cd805e83e682b858ded21ab0d3963bf7633fb22a76a4e -size 124627 +oid sha256:529240c1211bd455015a6dbc581d4b624711183b60f736b45a7729cdb4c6277f +size 125781 diff --git a/resources/loading.png b/resources/loading.png index 76fa26a..a31d7fa 100644 --- a/resources/loading.png +++ b/resources/loading.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb69fc16814ff0db507965e95df225f1316917b802998e8517242b8ef534e3ea -size 5861 +oid sha256:f5c05b59ffb6170102aef205bafaf95fa88abb76ec549722d408600154c2b4f0 +size 13592 diff --git a/resources/main_menu/about.png b/resources/main_menu/about.png new file mode 100644 index 0000000..da06378 --- /dev/null +++ b/resources/main_menu/about.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97ce17108ceabc0c0c8f8853f910322d98ee21a3d85dff44409ccc00c370ebf2 +size 1027 diff --git a/resources/main_menu/about_hover.png b/resources/main_menu/about_hover.png new file mode 100644 index 0000000..dac27c9 --- /dev/null +++ b/resources/main_menu/about_hover.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0675bd5e392e2635cb896215e023a9785e765507449b716273c826e17edeff73 +size 17670 diff --git a/resources/main_menu/about_pressed.png b/resources/main_menu/about_pressed.png new file mode 100644 index 0000000..714d290 --- /dev/null +++ b/resources/main_menu/about_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a395d9bada999ce26f7379543c708e0bbe7b56c3fc7989deac8675c669d1b28 +size 1003 diff --git a/resources/main_menu/title.png b/resources/main_menu/title.png index 70058a0..c20c4d7 100644 --- a/resources/main_menu/title.png +++ b/resources/main_menu/title.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cfd73ee7e067e0298f461ea0a01f1e2fc4b093cf2d22c665654f030867463b7 -size 7191 +oid sha256:32a5288d6024ea21d8b9e53608ec182516e30845de638457a23498649cda9832 +size 12534 diff --git a/resources/select_your_ship.png b/resources/select_your_ship.png new file mode 100644 index 0000000..14f21d6 --- /dev/null +++ b/resources/select_your_ship.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c962f6f711cb6b4b243c3c62334282b7565ca643bca99f0efd7049ef6e012217 +size 17024 diff --git a/src/Game.cpp b/src/Game.cpp index 6936e59..b58236e 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -110,7 +110,20 @@ namespace ZL loadingTexture = std::make_unique(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE)); #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(); #ifdef EMSCRIPTEN @@ -268,12 +281,12 @@ namespace ZL renderer.shaderManager.PushShader(defaultShaderName); renderer.RenderUniform1i(textureUniformName, 0); - //float width = Environment::projectionWidth; - //float height = Environment::projectionHeight; + float width = Environment::projectionWidth; + float height = Environment::projectionHeight; renderer.PushProjectionMatrix( - 0, 1, - 0, 1, + -width * 0.5f, width*0.5f, + -height * 0.5f, height * 0.5f, -10, 10); renderer.PushMatrix(); @@ -335,7 +348,7 @@ namespace ZL //SDL_GL_MakeCurrent(ZL::Environment::window, glContext); 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); //processTickCount(); @@ -452,24 +465,6 @@ namespace ZL } if (event.type == SDL_KEYUP) { - if (event.key.keysym.sym == SDLK_a) { - x = x + 0.2f; - } - if (event.key.keysym.sym == SDLK_q) { - x = x - 0.2f; - } - if (event.key.keysym.sym == SDLK_s) { - y = y + 0.2f; - } - if (event.key.keysym.sym == SDLK_w) { - y = y - 0.2f; - } - if (event.key.keysym.sym == SDLK_d) { - z = z + 0.2f; - } - if (event.key.keysym.sym == SDLK_e) { - z = z - 0.2f; - } if (event.key.keysym.sym == SDLK_r) { std::cout << "Camera position: x=" << x << " y=" << y << " z=" << z << std::endl; } diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index e862e49..6b8c2f4 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -135,13 +135,16 @@ namespace ZL { state = GameState::Gameplay; uiManager.replaceRoot(gameplayRoot); + uiManager.findButton("minusButton")->state = ButtonState::Disabled; + + auto velocityTv = uiManager.findTextView("velocityText"); if (velocityTv) { velocityTv->rect.x = 10.0f; velocityTv->rect.y = static_cast(Environment::height) - velocityTv->rect.h - 10.0f; } - uiManager.startAnimationOnNode("backgroundNode", "bgScroll"); + //uiManager.startAnimationOnNode("backgroundNode", "bgScroll"); uiManager.setButtonPressCallback("shootButton", [this](const std::string&) { if (onFirePressed) onFirePressed(); @@ -152,11 +155,29 @@ namespace ZL { uiManager.setButtonPressCallback("plusButton", [this](const std::string&) { int newVel = Environment::shipState.selectedVelocity + 1; 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); }); uiManager.setButtonPressCallback("minusButton", [this](const std::string&) { int newVel = Environment::shipState.selectedVelocity - 1; 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); }); uiManager.setSliderCallback("velocitySlider", [this](const std::string&, float value) { diff --git a/src/Space.cpp b/src/Space.cpp index c7cfc3d..3869c84 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -264,11 +264,17 @@ namespace ZL explosionEmitter.setEmissionPoints(std::vector()); Environment::shipState.position = Vector3f{ 0, 0, 45000.f }; Environment::shipState.velocity = 0.0f; + Environment::shipState.selectedVelocity = 0; Environment::shipState.rotation = Eigen::Matrix3f::Identity(); Environment::inverseShipMatrix = Eigen::Matrix3f::Identity(); Environment::zoom = DEFAULT_ZOOM; Environment::tapDownHold = false; playerScore = 0; + if (menuManager.uiManager.findButton("minusButton")) + { + menuManager.uiManager.findButton("minusButton")->state = ButtonState::Disabled; + } + } void Space::setup() { @@ -1669,6 +1675,7 @@ namespace ZL shipAlive = false; gameOver = true; + Environment::shipState.selectedVelocity = 0; Environment::shipState.velocity = 0.0f; showExplosion = true; From 84a5e888a06e317309358910c76cd2548a92c6e5 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Mon, 9 Mar 2026 18:36:04 +0300 Subject: [PATCH 3/6] Added pickup boxes --- resources/button_take.png | 3 + resources/button_take_disabled.png | 3 + resources/button_take_pressed.png | 3 + resources/config/ui.json | 21 ++++- src/MenuManager.cpp | 11 ++- src/MenuManager.h | 1 + src/Space.cpp | 76 ++++++++++++++++++- src/Space.h | 1 + src/network/ClientState.h | 1 + src/network/LocalClient.cpp | 114 ++++++++++++++++++++++++++++ src/network/LocalClient.h | 5 ++ src/network/NetworkInterface.h | 13 ++++ src/network/WebSocketClientBase.cpp | 51 +++++++++++++ src/network/WebSocketClientBase.h | 4 + 14 files changed, 300 insertions(+), 7 deletions(-) create mode 100644 resources/button_take.png create mode 100644 resources/button_take_disabled.png create mode 100644 resources/button_take_pressed.png diff --git a/resources/button_take.png b/resources/button_take.png new file mode 100644 index 0000000..847af37 --- /dev/null +++ b/resources/button_take.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:41a37af9ee1a8ad10466e4b84ec8067ce1d5a1be39534c020fafcca47f187b93 +size 15512 diff --git a/resources/button_take_disabled.png b/resources/button_take_disabled.png new file mode 100644 index 0000000..b3eb843 --- /dev/null +++ b/resources/button_take_disabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be03ea373477852cde848215befb76e8a048089122ea603151c29b15bff1d736 +size 11520 diff --git a/resources/button_take_pressed.png b/resources/button_take_pressed.png new file mode 100644 index 0000000..8031be0 --- /dev/null +++ b/resources/button_take_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f24f7239e1820b0f43a8250c48e116f0d405ee3d936589608f308b85d21993b9 +size 27390 diff --git a/resources/config/ui.json b/resources/config/ui.json index 65d8225..dcd4df6 100644 --- a/resources/config/ui.json +++ b/resources/config/ui.json @@ -64,7 +64,7 @@ "vertical_gravity": "bottom", "textures": { "normal": "resources/button_minus.png", - "hover": "resources/button_minus_pressed.png", + "hover": "resources/button_minus.png", "pressed": "resources/button_minus_pressed.png", "disabled" : "resources/button_minus_disabled.png" } @@ -81,10 +81,27 @@ "vertical_gravity": "bottom", "textures": { "normal": "resources/button_plus.png", - "hover": "resources/button_plus_pressed.png", + "hover": "resources/button_plus.png", "pressed": "resources/button_plus_pressed.png", "disabled" : "resources/button_plus_disabled.png" } + }, + { + "type": "Button", + "name": "takeButton", + "x": -20, + "y": 320, + "width": 150, + "height": 150, + "border" : 20, + "horizontal_gravity": "right", + "vertical_gravity": "bottom", + "textures": { + "normal": "resources/button_take.png", + "hover": "resources/button_take.png", + "pressed": "resources/button_take_pressed.png", + "disabled" : "resources/button_take_disabled.png" + } } ] } diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index 6b8c2f4..bf928ad 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -136,6 +136,7 @@ namespace ZL { uiManager.replaceRoot(gameplayRoot); uiManager.findButton("minusButton")->state = ButtonState::Disabled; + if (auto btn = uiManager.findButton("takeButton")) btn->state = ButtonState::Disabled; auto velocityTv = uiManager.findTextView("velocityText"); @@ -144,8 +145,6 @@ namespace ZL { velocityTv->rect.y = static_cast(Environment::height) - velocityTv->rect.h - 10.0f; } - //uiManager.startAnimationOnNode("backgroundNode", "bgScroll"); - uiManager.setButtonPressCallback("shootButton", [this](const std::string&) { if (onFirePressed) onFirePressed(); }); @@ -180,13 +179,19 @@ namespace ZL { } if (onVelocityChanged) onVelocityChanged(newVel); }); + + uiManager.setButtonPressCallback("takeButton", [this](const std::string&) { + if (onTakeButtonPressed) onTakeButtonPressed(); + }); + + /* uiManager.setSliderCallback("velocitySlider", [this](const std::string&, float value) { int newVel = static_cast(roundf(value * 10)); if (newVel > 2) newVel = 2; if (newVel != Environment::shipState.selectedVelocity) { if (onVelocityChanged) onVelocityChanged(newVel); } - }); + });*/ } // ── State: GameOver ────────────────────────────────────────────────────── diff --git a/src/MenuManager.h b/src/MenuManager.h index 083492b..1a52b62 100644 --- a/src/MenuManager.h +++ b/src/MenuManager.h @@ -70,6 +70,7 @@ namespace ZL { std::function onRestartPressed; std::function onVelocityChanged; std::function onFirePressed; + std::function onTakeButtonPressed; std::function onSingleplayerPressed; std::function onMultiplayerPressed; }; diff --git a/src/Space.cpp b/src/Space.cpp index 3869c84..98f953f 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -317,6 +317,26 @@ namespace ZL firePressed = true; }; + menuManager.onTakeButtonPressed = [this]() { + if (Environment::shipState.shipType != 1) return; + if (!networkClient) return; + + int bestIdx = -1; + float bestDistSq = BOX_PICKUP_RADIUS * BOX_PICKUP_RADIUS; + for (size_t i = 0; i < boxCoordsArr.size(); ++i) { + if (i >= boxAlive.size() || !boxAlive[i]) continue; + Vector3f boxWorld = boxCoordsArr[i].pos + Vector3f{ 0.f, 0.f, 45000.f }; + float distSq = (Environment::shipState.position - boxWorld).squaredNorm(); + if (distSq <= bestDistSq) { + bestDistSq = distSq; + bestIdx = static_cast(i); + } + } + if (bestIdx >= 0) { + networkClient->Send("BOX_PICKUP:" + std::to_string(bestIdx)); + } + }; + bool cfgLoaded = sparkEmitter.loadFromJsonFile("resources/config/spark_config.json", renderer, CONST_ZIP_FILE); bool cfgLoaded2 = sparkEmitterCargo.loadFromJsonFile("resources/config/spark_config_cargo.json", renderer, CONST_ZIP_FILE); sparkEmitter.setIsActive(false); @@ -395,7 +415,7 @@ namespace ZL boxLabels.clear(); boxLabels.reserve(boxCoordsArr.size()); for (size_t i = 0; i < boxCoordsArr.size(); ++i) { - boxLabels.push_back("Box " + std::to_string(i + 1)); + boxLabels.push_back("Box " + std::to_string(i)); } if (!cfgLoaded) @@ -658,6 +678,11 @@ namespace ZL for (size_t i = 0; i < n; ++i) { if (destroyedFlags[i]) boxAlive[i] = false; // destroyed => не рисуем } + boxLabels.clear(); + boxLabels.resize(boxCoordsArr.size()); + for (size_t i = 0; i < boxCoordsArr.size(); ++i) { + boxLabels[i] = "Box " + std::to_string(i); + } serverBoxesApplied = true; } } @@ -1776,6 +1801,24 @@ namespace ZL std::string velocityStr = "Velocity: " + std::to_string(static_cast(Environment::shipState.velocity)); menuManager.uiManager.setText("velocityText", velocityStr); } + + bool canPickup = false; + if (Environment::shipState.shipType == 1) { + for (size_t i = 0; i < boxCoordsArr.size(); ++i) { + if (i >= boxAlive.size() || !boxAlive[i]) continue; + Vector3f boxWorld = boxCoordsArr[i].pos + Vector3f{ 0.f, 0.f, 45000.f }; + float distSq = (Environment::shipState.position - boxWorld).squaredNorm(); + if (distSq <= BOX_PICKUP_RADIUS * BOX_PICKUP_RADIUS) { + canPickup = true; + break; + } + } + } + if (canPickup != nearPickupBox) { + nearPickupBox = canPickup; + if (auto btn = menuManager.uiManager.findButton("takeButton")) + btn->state = canPickup ? ButtonState::Normal : ButtonState::Disabled; + } } } @@ -1890,7 +1933,7 @@ namespace ZL if (d.killerId == localId) { this->playerScore += 1; std::cout << "Client: Increased local score to " << this->playerScore << std::endl; - + } } } @@ -1952,6 +1995,35 @@ namespace ZL } } } + + auto boxPickups = networkClient->getPendingBoxPickups(); + for (const auto& pickup : boxPickups) { + int idx = pickup.boxIndex; + if (idx >= 0 && idx < (int)boxCoordsArr.size() && idx < (int)boxAlive.size()) { + if (boxAlive[idx]) { + boxAlive[idx] = false; + boxRenderArr[idx].data.PositionData.clear(); + boxRenderArr[idx].vao.reset(); + boxRenderArr[idx].positionVBO.reset(); + boxRenderArr[idx].texCoordVBO.reset(); + std::cout << "Client: Box " << idx << " picked up by player " << pickup.pickedUpBy << "\n"; + } + } + } + + + auto boxRespawns = networkClient->getPendingBoxRespawns(); + for (const auto& respawn : boxRespawns) { + int idx = respawn.boxIndex; + if (idx >= 0 && idx < (int)boxCoordsArr.size()) { + boxCoordsArr[idx].pos = respawn.position; + boxCoordsArr[idx].m = respawn.rotation; + boxAlive[idx] = true; + boxRenderArr[idx].AssignFrom(boxBase); + boxRenderArr[idx].RefreshVBO(); + std::cout << "Client: Box " << idx << " respawned" << std::endl; + } + } } void Space::handleDown(int mx, int my) diff --git a/src/Space.h b/src/Space.h index 05395a3..f94d839 100644 --- a/src/Space.h +++ b/src/Space.h @@ -113,6 +113,7 @@ namespace ZL { const uint64_t explosionDurationMs = 500; bool serverBoxesApplied = false; + bool nearPickupBox = false; static constexpr float MAX_DIST_SQ = 10000.f * 10000.f; static constexpr float FADE_START = 6000.f; diff --git a/src/network/ClientState.h b/src/network/ClientState.h index d78903d..12c1c32 100644 --- a/src/network/ClientState.h +++ b/src/network/ClientState.h @@ -34,6 +34,7 @@ constexpr float PROJECTILE_LIFE = 15000.f; //ms const float projectileHitRadius = 1.5f * 5; const float boxCollisionRadius = 2.0f * 5; const float shipCollisionRadius = 15.0f * 5; +const float BOX_PICKUP_RADIUS = shipCollisionRadius * 5; const float npcCollisionRadius = 5.0f * 5; uint32_t fnv1a_hash(const std::string& data); diff --git a/src/network/LocalClient.cpp b/src/network/LocalClient.cpp index 4125593..f1e9cab 100644 --- a/src/network/LocalClient.cpp +++ b/src/network/LocalClient.cpp @@ -274,6 +274,24 @@ namespace ZL { std::cout << "LocalClient: Box " << boxIdx << " destroyed by projectile from player " << projectiles[projIdx].shooterId << std::endl; + // Respawn box + { + std::random_device rd2; + std::mt19937 gen2(rd2()); + std::uniform_real_distribution angleDist(0.f, static_cast(M_PI * 2.0)); + Eigen::Vector3f newPos = generateRespawnBoxPos(static_cast(boxIdx)); + Eigen::Vector3f axis = Eigen::Vector3f::Random().normalized(); + Eigen::Matrix3f newRot = Eigen::AngleAxisf(angleDist(gen2), axis).toRotationMatrix(); + serverBoxes[boxIdx].position = newPos; + serverBoxes[boxIdx].rotation = newRot; + serverBoxes[boxIdx].destroyed = false; + BoxRespawnInfo respawn; + respawn.boxIndex = static_cast(boxIdx); + respawn.position = newPos; + respawn.rotation = newRot; + pendingBoxRespawns.push_back(respawn); + } + if (std::find(projIndicesToRemove.begin(), projIndicesToRemove.end(), (int)projIdx) == projIndicesToRemove.end()) { projIndicesToRemove.push_back(static_cast(projIdx)); @@ -352,11 +370,62 @@ namespace ZL { std::cout << "LocalClient: Box " << bi << " destroyed by ship collision with player " << GetClientId() << std::endl; + + // Respawn box + { + std::random_device rd2; + std::mt19937 gen2(rd2()); + std::uniform_real_distribution angleDist(0.f, static_cast(M_PI * 2.0)); + Eigen::Vector3f newPos = generateRespawnBoxPos(static_cast(bi)); + Eigen::Vector3f axis = Eigen::Vector3f::Random().normalized(); + Eigen::Matrix3f newRot = Eigen::AngleAxisf(angleDist(gen2), axis).toRotationMatrix(); + serverBoxes[bi].position = newPos; + serverBoxes[bi].rotation = newRot; + serverBoxes[bi].destroyed = false; + BoxRespawnInfo respawn; + respawn.boxIndex = static_cast(bi); + respawn.position = newPos; + respawn.rotation = newRot; + pendingBoxRespawns.push_back(respawn); + } } } } } + Eigen::Vector3f LocalClient::generateRespawnBoxPos(int skipIdx) { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution dist(-1000.f, 1000.f); + + for (int attempt = 0; attempt < 500; ++attempt) { + Eigen::Vector3f cand(dist(gen), dist(gen), dist(gen)); + bool safe = true; + for (int i = 0; i < (int)serverBoxes.size(); ++i) { + if (i == skipIdx) continue; + if (serverBoxes[i].destroyed) continue; + if ((cand - serverBoxes[i].position).squaredNorm() < 9.f) { + safe = false; + break; + } + } + if (safe) return cand; + } + return Eigen::Vector3f(dist(gen), dist(gen), dist(gen)); + } + + std::vector LocalClient::getPendingBoxPickups() { + auto result = pendingBoxPickups; + pendingBoxPickups.clear(); + return result; + } + + std::vector LocalClient::getPendingBoxRespawns() { + auto result = pendingBoxRespawns; + pendingBoxRespawns.clear(); + return result; + } + void LocalClient::Send(const std::string& message) { auto parts = [](const std::string& s, char delimiter) { std::vector tokens; @@ -372,6 +441,51 @@ namespace ZL { std::string type = parts[0]; + if (type == "BOX_PICKUP") { + if (parts.size() < 2) return; + if (!hasLocalPlayerState || localPlayerState.shipType != 1) return; + + int boxIdx = -1; + try { boxIdx = std::stoi(parts[1]); } catch (...) { return; } + + if (boxIdx < 0 || boxIdx >= (int)serverBoxes.size()) return; + if (serverBoxes[boxIdx].destroyed) return; + + Eigen::Vector3f boxWorld = serverBoxes[boxIdx].position + Eigen::Vector3f(0.f, 0.f, 45000.f); + float distSq = (localPlayerState.position - boxWorld).squaredNorm(); + if (distSq > BOX_PICKUP_RADIUS * BOX_PICKUP_RADIUS) return; + + serverBoxes[boxIdx].destroyed = true; + + BoxPickedUpInfo pickup; + pickup.boxIndex = boxIdx; + pickup.pickedUpBy = GetClientId(); + pendingBoxPickups.push_back(pickup); + + std::cout << "LocalClient: Box " << boxIdx << " picked up by player " << GetClientId() << "\n"; + + // Respawn box at new position + { + std::random_device rd2; + std::mt19937 gen2(rd2()); + std::uniform_real_distribution angleDist(0.f, static_cast(M_PI * 2.0)); + Eigen::Vector3f newPos = generateRespawnBoxPos(boxIdx); + Eigen::Vector3f axis = Eigen::Vector3f::Random().normalized(); + Eigen::Matrix3f newRot = Eigen::AngleAxisf(angleDist(gen2), axis).toRotationMatrix(); + serverBoxes[boxIdx].position = newPos; + serverBoxes[boxIdx].rotation = newRot; + serverBoxes[boxIdx].destroyed = false; + + BoxRespawnInfo respawn; + respawn.boxIndex = boxIdx; + respawn.position = newPos; + respawn.rotation = newRot; + pendingBoxRespawns.push_back(respawn); + std::cout << "LocalClient: Box " << boxIdx << " respawned after pickup\n"; + } + return; + } + if (type == "FIRE") { if (parts.size() < 10) return; diff --git a/src/network/LocalClient.h b/src/network/LocalClient.h index 438dede..ce683fd 100644 --- a/src/network/LocalClient.h +++ b/src/network/LocalClient.h @@ -42,6 +42,8 @@ namespace ZL { std::vector pendingProjectiles; std::vector pendingDeaths; std::vector pendingBoxDestructions; + std::vector pendingBoxPickups; + std::vector pendingBoxRespawns; std::vector pendingRespawns; uint64_t lastUpdateMs = 0; @@ -56,6 +58,7 @@ namespace ZL { void initializeNPCs(); void updateNPCs(); Eigen::Vector3f generateRandomPosition(); + Eigen::Vector3f generateRespawnBoxPos(int skipIdx); public: void Connect(const std::string& host, uint16_t port) override; @@ -79,6 +82,8 @@ namespace ZL { } std::vector getPendingBoxDestructions() override; + std::vector getPendingBoxPickups() override; + std::vector getPendingBoxRespawns() override; void setLocalPlayerState(const ClientState& state) { localPlayerState = state; diff --git a/src/network/NetworkInterface.h b/src/network/NetworkInterface.h index a5dd5c9..ba60170 100644 --- a/src/network/NetworkInterface.h +++ b/src/network/NetworkInterface.h @@ -30,6 +30,17 @@ namespace ZL { int destroyedBy = -1; }; + struct BoxPickedUpInfo { + int boxIndex = -1; + int pickedUpBy = -1; + }; + + struct BoxRespawnInfo { + int boxIndex = -1; + Eigen::Vector3f position = Eigen::Vector3f::Zero(); + Eigen::Matrix3f rotation = Eigen::Matrix3f::Identity(); + }; + class INetworkClient { public: virtual ~INetworkClient() = default; @@ -50,6 +61,8 @@ namespace ZL { virtual std::vector getPendingRespawns() = 0; virtual int GetClientId() const { return -1; } virtual std::vector getPendingBoxDestructions() = 0; + virtual std::vector getPendingBoxPickups() { return {}; } + virtual std::vector getPendingBoxRespawns() { return {}; } virtual int64_t getTimeOffset() const { return 0; } virtual std::vector getPendingDisconnects() { return {}; } diff --git a/src/network/WebSocketClientBase.cpp b/src/network/WebSocketClientBase.cpp index 6be0c77..7c37d3f 100644 --- a/src/network/WebSocketClientBase.cpp +++ b/src/network/WebSocketClientBase.cpp @@ -129,6 +129,45 @@ namespace ZL { return; } + if (msg.rfind("BOX_PICKED_UP:", 0) == 0) { + if (parts.size() >= 3) { + try { + BoxPickedUpInfo pickup; + pickup.boxIndex = std::stoi(parts[1]); + pickup.pickedUpBy = std::stoi(parts[2]); + pendingBoxPickups_.push_back(pickup); + std::cout << "Client: Received BOX_PICKED_UP box=" << pickup.boxIndex + << " by player " << pickup.pickedUpBy << std::endl; + } + catch (...) {} + } + return; + } + + if (msg.rfind("BOX_RESPAWN:", 0) == 0) { + if (parts.size() >= 9) { + try { + BoxRespawnInfo respawn; + respawn.boxIndex = std::stoi(parts[1]); + float px = std::stof(parts[2]); + float py = std::stof(parts[3]); + float pz = std::stof(parts[4]); + Eigen::Quaternionf q( + std::stof(parts[5]), + std::stof(parts[6]), + std::stof(parts[7]), + std::stof(parts[8]) + ); + respawn.position = Eigen::Vector3f(px, py, pz); + respawn.rotation = q.toRotationMatrix(); + pendingBoxRespawns_.push_back(respawn); + std::cout << "Client: Received BOX_RESPAWN box=" << respawn.boxIndex << std::endl; + } + catch (...) {} + } + return; + } + if (msg.rfind("BOX_DESTROYED:", 0) == 0) { //auto parts = split(msg, ':'); if (parts.size() >= 7) { @@ -369,6 +408,18 @@ namespace ZL { return copy; } + std::vector WebSocketClientBase::getPendingBoxPickups() { + std::vector copy; + copy.swap(pendingBoxPickups_); + return copy; + } + + std::vector WebSocketClientBase::getPendingBoxRespawns() { + std::vector copy; + copy.swap(pendingBoxRespawns_); + return copy; + } + std::vector WebSocketClientBase::getPendingDisconnects() { std::vector copy; copy.swap(pendingDisconnects_); diff --git a/src/network/WebSocketClientBase.h b/src/network/WebSocketClientBase.h index f74c9b5..a324a70 100644 --- a/src/network/WebSocketClientBase.h +++ b/src/network/WebSocketClientBase.h @@ -20,6 +20,8 @@ namespace ZL { std::vector pendingDeaths_; std::vector pendingRespawns_; std::vector pendingBoxDestructions_; + std::vector pendingBoxPickups_; + std::vector pendingBoxRespawns_; std::vector pendingDisconnects_; int clientId = -1; int64_t timeOffset = 0; @@ -50,6 +52,8 @@ namespace ZL { std::vector getPendingDeaths() override; std::vector getPendingRespawns() override; std::vector getPendingBoxDestructions() override; + std::vector getPendingBoxPickups() override; + std::vector getPendingBoxRespawns() override; std::vector getPendingDisconnects() override; std::vector getPendingSpawns(); int getClientId() const { return clientId; } From 3e8dba36a481c5358ccd0f130d15b6c6ca9d69d9 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Mon, 9 Mar 2026 19:27:20 +0300 Subject: [PATCH 4/6] fixing minor bugs, fixing velocity not changed after death, fixing server crash --- server/server.cpp | 127 +++++++++++++++++++++++++++++++++++++- server/server.h | 2 + src/Space.cpp | 20 +++++- src/network/ClientState.h | 10 +-- 4 files changed, 150 insertions(+), 9 deletions(-) diff --git a/server/server.cpp b/server/server.cpp index 2f1da7d..b72487e 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -31,8 +31,8 @@ bool Session::is_timed_out(std::chrono::system_clock::time_point now) const { } void Session::force_disconnect() { - ws_.async_close(websocket::close_code::normal, - [self = shared_from_this()](beast::error_code) {}); + beast::error_code ec; + ws_.next_layer().socket().close(ec); } int Session::get_id() const { return id_; } @@ -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"; } } + 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 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 angleDist(0.f, static_cast(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") { 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 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) { static thread_local std::mt19937 rng{ std::random_device{}() }; @@ -608,6 +688,8 @@ void Server::update_world() { + std::vector boxesToRespawn; + // --- Tick: box-projectile collisions --- { std::lock_guard bm(g_boxes_mutex); @@ -632,6 +714,7 @@ void Server::update_world() { } for (const auto& [boxIdx, projIdx] : boxProjectileCollisions) { + if (g_serverBoxes[boxIdx].destroyed) continue; g_serverBoxes[boxIdx].destroyed = true; 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); } + boxesToRespawn.push_back(static_cast(boxIdx)); + std::cout << "Server: Box " << boxIdx << " destroyed by projectile from player " << g_projectiles[projIdx].shooterId << std::endl; } @@ -690,6 +775,8 @@ void Server::update_world() { g_boxDestructions.push_back(destruction); } + boxesToRespawn.push_back(static_cast(bi)); + std::cout << "Server: Box " << bi << " destroyed by ship collision with player " << session->get_id() << std::endl; break; @@ -732,6 +819,41 @@ void Server::update_world() { 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 angleDist(0.f, static_cast(M_PI * 2.0)); + std::vector respawnMsgs; + { + std::lock_guard 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 --- timer.expires_after(std::chrono::milliseconds(50)); timer.async_wait([this](const boost::system::error_code& ec) { @@ -820,6 +942,7 @@ int main() { Server server(acceptor, ioc); + server.init(); server.accept(); std::cout << "Server started on port 8081...\n"; diff --git a/server/server.h b/server/server.h index 485f35c..a53ee50 100644 --- a/server/server.h +++ b/server/server.h @@ -135,6 +135,8 @@ public: void createProjectile(int id, Eigen::Vector3f pos, Eigen::Quaternionf dir, float velocity); void update_world(); Eigen::Vector3f PickSafeSpawnPos(int forPlayerId); + // Caller must hold g_boxes_mutex + Eigen::Vector3f PickSafeBoxPos(int skipIdx); void init(); void accept(); }; diff --git a/src/Space.cpp b/src/Space.cpp index 98f953f..cde5e60 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -265,6 +265,7 @@ namespace ZL Environment::shipState.position = Vector3f{ 0, 0, 45000.f }; Environment::shipState.velocity = 0.0f; Environment::shipState.selectedVelocity = 0; + newShipVelocity = 0; Environment::shipState.rotation = Eigen::Matrix3f::Identity(); Environment::inverseShipMatrix = Eigen::Matrix3f::Identity(); Environment::zoom = DEFAULT_ZOOM; @@ -274,7 +275,21 @@ namespace ZL { 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() { @@ -1702,6 +1717,7 @@ namespace ZL gameOver = true; Environment::shipState.selectedVelocity = 0; Environment::shipState.velocity = 0.0f; + newShipVelocity = 0; showExplosion = true; explosionEmitter.setUseWorldSpace(true); @@ -1803,7 +1819,7 @@ namespace ZL } bool canPickup = false; - if (Environment::shipState.shipType == 1) { + 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 }; diff --git a/src/network/ClientState.h b/src/network/ClientState.h index 12c1c32..fadb13e 100644 --- a/src/network/ClientState.h +++ b/src/network/ClientState.h @@ -31,11 +31,11 @@ constexpr long long PLAYER_TIMEOUT_MS = 10000; //ms — disconnect if no UPD rec constexpr float PROJECTILE_VELOCITY = 600.f; constexpr float PROJECTILE_LIFE = 15000.f; //ms -const float projectileHitRadius = 1.5f * 5; -const float boxCollisionRadius = 2.0f * 5; -const float shipCollisionRadius = 15.0f * 5; -const float BOX_PICKUP_RADIUS = shipCollisionRadius * 5; -const float npcCollisionRadius = 5.0f * 5; +const float projectileHitRadius = 1.5f * 4; +const float boxCollisionRadius = 2.0f * 4; +const float shipCollisionRadius = 15.0f * 3; +const float BOX_PICKUP_RADIUS = shipCollisionRadius * 3; +const float npcCollisionRadius = 5.0f * 3; uint32_t fnv1a_hash(const std::string& data); From a8ded217dfd342838a56e668093d7e4fd5fc0ebf Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Mon, 9 Mar 2026 20:52:16 +0300 Subject: [PATCH 5/6] Fixing ui bugs --- proj-web/space-game001plain.html | 29 ++++++++++++++++++++++++----- resources/config/ui.json | 19 ++++++++++++------- src/MenuManager.cpp | 3 ++- src/Space.cpp | 7 +++++++ src/Space.h | 1 + 5 files changed, 46 insertions(+), 13 deletions(-) diff --git a/proj-web/space-game001plain.html b/proj-web/space-game001plain.html index d3c1953..aae27f4 100644 --- a/proj-web/space-game001plain.html +++ b/proj-web/space-game001plain.html @@ -8,7 +8,7 @@ body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background-color: #000; - position: fixed; /* Предотвращает pull-to-refresh на Android */ + position: fixed; } #canvas { display: block; @@ -17,10 +17,23 @@ width: 100vw; height: 100vh; 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%); } +
Downloading...
@@ -36,12 +49,18 @@ } }; - // Обработка ориентации + 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() { - // Chrome на Android обновляет innerWidth/Height не мгновенно. - // Ждем завершения анимации поворота. setTimeout(() => { - // В Emscripten это вызовет ваш onWindowResized в C++ window.dispatchEvent(new Event('resize')); }, 200); }); diff --git a/resources/config/ui.json b/resources/config/ui.json index dcd4df6..c5ebf78 100644 --- a/resources/config/ui.json +++ b/resources/config/ui.json @@ -8,16 +8,21 @@ "children": [ { "type": "TextView", - "name": "velocityText", - "x": 10, - "y": 10, - "width": 200, + "name": "gameScoreText", + "x": 0, + "y": 30, + "width": 80, "height": 40, "horizontal_gravity": "left", "vertical_gravity": "top", - "text": "Velocity: 0", - "fontSize": 24, - "color": [1.0, 1.0, 1.0, 1.0], + "text": "Score: 0", + "fontSize": 36, + "color": [ + 0, + 217, + 255, + 1 + ], "centered": false }, { diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index bf928ad..a634803 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -139,11 +139,12 @@ namespace ZL { if (auto btn = uiManager.findButton("takeButton")) btn->state = ButtonState::Disabled; + /* auto velocityTv = uiManager.findTextView("velocityText"); if (velocityTv) { velocityTv->rect.x = 10.0f; velocityTv->rect.y = static_cast(Environment::height) - velocityTv->rect.h - 10.0f; - } + }*/ uiManager.setButtonPressCallback("shootButton", [this](const std::string&) { if (onFirePressed) onFirePressed(); diff --git a/src/Space.cpp b/src/Space.cpp index cde5e60..6bfb7d4 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -349,6 +349,7 @@ namespace ZL } if (bestIdx >= 0) { networkClient->Send("BOX_PICKUP:" + std::to_string(bestIdx)); + this->playerScore += 1; } }; @@ -1837,6 +1838,11 @@ namespace ZL } } + if (playerScore != prevPlayerScore) + { + prevPlayerScore = playerScore; + menuManager.uiManager.setText("gameScoreText", "Score: " + std::to_string(playerScore)); + } } void Space::fireProjectiles() { @@ -1845,6 +1851,7 @@ namespace ZL Vector3f{ 1.5f, 0.9f - 6.f, 5.0f } }; + const float projectileSpeed = PROJECTILE_VELOCITY; const float lifeMs = PROJECTILE_LIFE; const float size = 0.5f; diff --git a/src/Space.h b/src/Space.h index f94d839..a7006c5 100644 --- a/src/Space.h +++ b/src/Space.h @@ -126,6 +126,7 @@ namespace ZL { std::unordered_set deadRemotePlayers; int playerScore = 0; + int prevPlayerScore = 0; bool wasConnectedToServer = false; static constexpr float TARGET_MAX_DIST = 50000.0f; From 785b96ce82d3344239d6bb8e86dff64e0c006d12 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Mon, 9 Mar 2026 23:04:48 +0300 Subject: [PATCH 6/6] Working on list of players --- resources/black.png | 3 + resources/blue_transparent.png | 3 + resources/button_players.png | 3 + resources/config/ui.json | 20 ++- src/MenuManager.cpp | 5 + src/MenuManager.h | 1 + src/Space.cpp | 189 ++++++++++++++++++++++- src/Space.h | 10 ++ src/UiManager.cpp | 264 ++++++++++++++++++++++++++++++++- src/UiManager.h | 59 +++++++- 10 files changed, 544 insertions(+), 13 deletions(-) create mode 100644 resources/black.png create mode 100644 resources/blue_transparent.png create mode 100644 resources/button_players.png diff --git a/resources/black.png b/resources/black.png new file mode 100644 index 0000000..603e239 --- /dev/null +++ b/resources/black.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5a9276602359b1ed4341487185ed8b5f4c4dfa86d9efb06cc09f748a50c8560 +size 353 diff --git a/resources/blue_transparent.png b/resources/blue_transparent.png new file mode 100644 index 0000000..c4d0889 --- /dev/null +++ b/resources/blue_transparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f459ece1aea9b5514f5c170720968f0817e7596f01c483a02f274180f6e4143 +size 364 diff --git a/resources/button_players.png b/resources/button_players.png new file mode 100644 index 0000000..31bdb5e --- /dev/null +++ b/resources/button_players.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43058b1c47b3a29d364c77642cfa0c83ccc719e260418fa89bf0dd501b45fbf7 +size 22899 diff --git a/resources/config/ui.json b/resources/config/ui.json index c5ebf78..1dc861d 100644 --- a/resources/config/ui.json +++ b/resources/config/ui.json @@ -11,8 +11,8 @@ "name": "gameScoreText", "x": 0, "y": 30, - "width": 80, - "height": 40, + "width": 200, + "height": 60, "horizontal_gravity": "left", "vertical_gravity": "top", "text": "Score: 0", @@ -25,6 +25,22 @@ ], "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": "shootButton", diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index a634803..955324e 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -185,6 +185,11 @@ namespace ZL { if (onTakeButtonPressed) onTakeButtonPressed(); }); + uiManager.setButtonCallback("showPlayersButton", [this](const std::string&) { + if (onShowPlayersPressed) onShowPlayersPressed(); + }); + + /* uiManager.setSliderCallback("velocitySlider", [this](const std::string&, float value) { int newVel = static_cast(roundf(value * 10)); diff --git a/src/MenuManager.h b/src/MenuManager.h index 1a52b62..ec7844c 100644 --- a/src/MenuManager.h +++ b/src/MenuManager.h @@ -73,6 +73,7 @@ namespace ZL { std::function onTakeButtonPressed; std::function onSingleplayerPressed; std::function onMultiplayerPressed; + std::function onShowPlayersPressed; }; } // namespace ZL \ No newline at end of file diff --git a/src/Space.cpp b/src/Space.cpp index 6bfb7d4..5b865fc 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -332,6 +332,10 @@ namespace ZL firePressed = true; }; + menuManager.onShowPlayersPressed = [this]() { + buildAndShowPlayerList(); + }; + menuManager.onTakeButtonPressed = [this]() { if (Environment::shipState.shipType != 1) return; if (!networkClient) return; @@ -1001,6 +1005,16 @@ namespace ZL int Space::pickTargetId() const { + // Use manually selected target if it's still alive and in range + if (manualTrackedTargetId >= 0) { + auto it = remotePlayerStates.find(manualTrackedTargetId); + if (it != remotePlayerStates.end() && !deadRemotePlayers.count(manualTrackedTargetId)) { + float d2 = (Environment::shipState.position - it->second.position).squaredNorm(); + if (d2 <= TARGET_MAX_DIST_SQ) return manualTrackedTargetId; + } + // Target no longer valid — fall through to auto-pick + } + int bestId = -1; float bestDistSq = 1e30f; @@ -1008,8 +1022,8 @@ namespace ZL if (deadRemotePlayers.count(id)) continue; 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) { bestDistSq = d2; @@ -1728,6 +1742,7 @@ namespace ZL std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n"; + clearPlayerListIfVisible(); menuManager.showGameOver(this->playerScore); } else { @@ -1803,6 +1818,7 @@ namespace ZL planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty; } + clearPlayerListIfVisible(); menuManager.showGameOver(this->playerScore); } } @@ -1885,6 +1901,7 @@ namespace ZL gameOver = true; Environment::shipState.velocity = 0.0f; std::cout << "Client: Lost connection to server\n"; + clearPlayerListIfVisible(); menuManager.showConnectionLost(); } @@ -1923,6 +1940,7 @@ namespace ZL } } } + // Обработка событий смерти, присланных сервером auto deaths = networkClient->getPendingDeaths(); if (!deaths.empty()) { @@ -1947,10 +1965,12 @@ namespace ZL shipAlive = false; gameOver = true; Environment::shipState.velocity = 0.0f; + clearPlayerListIfVisible(); menuManager.showGameOver(this->playerScore); } else { deadRemotePlayers.insert(d.targetId); + if (d.targetId == manualTrackedTargetId) manualTrackedTargetId = -1; std::cout << "Marked remote player " << d.targetId << " as dead" << std::endl; } if (d.killerId == localId) { @@ -1959,6 +1979,7 @@ namespace ZL } } + rebuildPlayerListIfVisible(); } auto respawns = networkClient->getPendingRespawns(); @@ -1976,6 +1997,7 @@ namespace ZL std::cout << "Client: Remote player " << respawnId << " respawned, removed from dead list" << std::endl; } } + rebuildPlayerListIfVisible(); auto disconnects = networkClient->getPendingDisconnects(); for (int pid : disconnects) { @@ -1986,9 +2008,11 @@ namespace ZL trackedTargetId = -1; targetAcquireAnim = 0.f; } + if (pid == manualTrackedTargetId) manualTrackedTargetId = -1; std::cout << "Client: Remote player " << pid << " left the game, removed from scene\n"; } + rebuildPlayerListIfVisible(); auto boxDestructions = networkClient->getPendingBoxDestructions(); if (!boxDestructions.empty()) { std::cout << "Game: Received " << boxDestructions.size() << " box destruction events" << std::endl; @@ -2051,13 +2075,15 @@ namespace ZL void Space::handleDown(int mx, int my) { - Environment::tapDownHold = true; + if (playerListVisible) return; - Environment::tapDownStartPos(0) = mx; - Environment::tapDownStartPos(1) = my; + Environment::tapDownHold = true; - Environment::tapDownCurrentPos(0) = mx; - Environment::tapDownCurrentPos(1) = my; + Environment::tapDownStartPos(0) = mx; + Environment::tapDownStartPos(1) = my; + + Environment::tapDownCurrentPos(0) = mx; + Environment::tapDownCurrentPos(1) = my; } void Space::handleUp(int mx, int my) @@ -2068,6 +2094,8 @@ namespace ZL void Space::handleMotion(int mx, int my) { + if (playerListVisible) return; + if (Environment::tapDownHold) { Environment::tapDownCurrentPos(0) = mx; Environment::tapDownCurrentPos(1) = my; @@ -2105,4 +2133,151 @@ namespace ZL }*/ + + std::shared_ptr Space::buildPlayerListRoot() + { + const float btnW = 400; + const float btnH = 50.0f; + + // Collect alive remote players + std::vector> 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(); + 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(); + 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(); + btnNode->name = "playerBtn_" + std::to_string(pid); + btnNode->layoutType = LayoutType::Frame; + btnNode->width = btnW; + btnNode->height = btnH; + + + auto tb = std::make_shared(); + 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(); + 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(CreateTextureDataFromPng("resources/black.png", "")); + + btnNode->textButton = tb; + /*auto button = std::make_shared(); + button->name = "Hello"; + button->texNormal = std::make_unique(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(); + backdropNode->name = "playerListBackdrop"; + backdropNode->layoutType = LayoutType::Frame; + backdropNode->width = -1.0f; + backdropNode->height = -1.0f; + auto backdropTb = std::make_shared(); + backdropTb->name = "playerListBackdrop"; + backdropNode->textButton = backdropTb; + + + /* + auto backgroundNode = std::make_shared(); + 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(); + backdropImage->name = "playerListBackgroundImage"; + backdropImage->texture = std::make_unique(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 diff --git a/src/Space.h b/src/Space.h index a7006c5..ca45251 100644 --- a/src/Space.h +++ b/src/Space.h @@ -129,6 +129,9 @@ namespace ZL { int prevPlayerScore = 0; bool wasConnectedToServer = false; + bool playerListVisible = false; + int manualTrackedTargetId = -1; + static constexpr float TARGET_MAX_DIST = 50000.0f; static constexpr float TARGET_MAX_DIST_SQ = TARGET_MAX_DIST * TARGET_MAX_DIST; @@ -147,6 +150,13 @@ namespace ZL { void resetPlayerState(); void clearTextRendererCache(); + // Player list overlay + void buildAndShowPlayerList(); + void closePlayerList(); + void rebuildPlayerListIfVisible(); + void clearPlayerListIfVisible(); + std::shared_ptr buildPlayerListRoot(); + void updateSparkEmitters(float deltaMs); void prepareSparkEmittersForDraw(); void drawShipSparkEmitters(); diff --git a/src/UiManager.cpp b/src/UiManager.cpp index b04a861..734ddd6 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -72,6 +72,65 @@ namespace ZL { 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* 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() { trackMesh.data.PositionData.clear(); trackMesh.data.TexCoordData.clear(); @@ -393,6 +452,49 @@ namespace ZL { node->textField = tf; } + if (typeStr == "TextButton") { + auto tb = std::make_shared(); + 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 { + if (!t.contains(key) || !t[key].is_string()) return nullptr; + std::string path = t[key].get(); + try { + auto data = CreateTextureDataFromPng(path.c_str(), zipFile.c_str()); + return std::make_shared(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(); + if (j.contains("fontPath")) tb->fontPath = j["fontPath"].get(); + if (j.contains("fontSize")) tb->fontSize = j["fontSize"].get(); + if (j.contains("textCentered")) tb->textCentered = j["textCentered"].get(); + 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(); + } + + tb->textRenderer = std::make_unique(); + 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()) { for (auto it = j["animations"].begin(); it != j["animations"].end(); ++it) { std::string animName = it.key(); @@ -536,6 +638,7 @@ namespace ZL { root->localY // finalLocalY ); buttons.clear(); + textButtons.clear(); sliders.clear(); textViews.clear(); textFields.clear(); @@ -547,6 +650,9 @@ namespace ZL { for (auto& b : buttons) { b->buildMesh(); } + for (auto& tb : textButtons) { + tb->buildMesh(); + } for (auto& s : sliders) { s->buildTrackMesh(); s->buildKnobMesh(); @@ -694,11 +800,14 @@ namespace ZL { // 1. Обновляем кнопку if (node->button) { node->button->rect = node->screenRect; - // Если у кнопки есть анимационные смещения, они учитываются внутри buildMesh - // или при рендеринге через Uniform-переменные матрицы модели. node->button->buildMesh(); } + if (node->textButton) { + node->textButton->rect = node->screenRect; + node->textButton->buildMesh(); + } + // 2. Обновляем слайдер if (node->slider) { node->slider->rect = node->screenRect; @@ -744,6 +853,9 @@ namespace ZL { if (node->button) { buttons.push_back(node->button); } + if (node->textButton) { + textButtons.push_back(node->textButton); + } if (node->slider) { sliders.push_back(node->slider); } @@ -862,10 +974,13 @@ namespace ZL { MenuState prev; prev.root = root; prev.buttons = buttons; + prev.textButtons = textButtons; prev.sliders = sliders; + prev.textViews = textViews; prev.textFields = textFields; prev.staticImages = staticImages; prev.pressedButtons = pressedButtons; + prev.pressedTextButtons = pressedTextButtons; prev.pressedSliders = pressedSliders; prev.focusedTextField = focusedTextField; prev.path = ""; @@ -884,6 +999,14 @@ namespace ZL { 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); menuStack.push_back(std::move(prev)); @@ -913,10 +1036,13 @@ namespace ZL { root = s.root; buttons = s.buttons; + textButtons = s.textButtons; sliders = s.sliders; + textViews = s.textViews; textFields = s.textFields; staticImages = s.staticImages; pressedButtons = s.pressedButtons; + pressedTextButtons = s.pressedTextButtons; pressedSliders = s.pressedSliders; focusedTextField = s.focusedTextField; @@ -931,6 +1057,15 @@ namespace ZL { 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) { if (sl) { @@ -956,6 +1091,9 @@ namespace ZL { for (const auto& b : buttons) { b->draw(renderer); } + for (const auto& tb : textButtons) { + tb->draw(renderer); + } for (const auto& s : sliders) { s->draw(renderer); } @@ -1007,6 +1145,12 @@ namespace ZL { node->button->animScaleX = act.origScaleX; 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.elapsedMs = 0.0f; act.stepStarted = false; @@ -1028,12 +1172,20 @@ namespace ZL { node->button->animOffsetX = step.toX; node->button->animOffsetY = step.toY; } + if (node->textButton) { + node->textButton->animOffsetX = step.toX; + node->textButton->animOffsetY = step.toY; + } } else if (step.type == "scale") { if (node->button) { node->button->animScaleX = step.toX; node->button->animScaleY = step.toY; } + if (node->textButton) { + node->textButton->animScaleX = step.toX; + node->textButton->animScaleY = step.toY; + } } act.stepIndex++; act.elapsedMs = 0.0f; @@ -1048,6 +1200,12 @@ namespace ZL { act.origScaleX = node->button->animScaleX; 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 { act.origOffsetX = act.origOffsetY = 0.0f; act.origScaleX = act.origScaleY = 1.0f; @@ -1064,6 +1222,12 @@ namespace ZL { act.startScaleX = node->button->animScaleX; 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 { act.startOffsetX = act.startOffsetY = 0.0f; act.startScaleX = act.startScaleY = 1.0f; @@ -1089,6 +1253,10 @@ namespace ZL { node->button->animOffsetX = nx; node->button->animOffsetY = ny; } + if (node->textButton) { + node->textButton->animOffsetX = nx; + node->textButton->animOffsetY = ny; + } } else if (step.type == "scale") { float sx = act.startScaleX + (act.endScaleX - act.startScaleX) * te; @@ -1097,6 +1265,10 @@ namespace ZL { node->button->animScaleX = sx; node->button->animScaleY = sy; } + if (node->textButton) { + node->textButton->animScaleX = sx; + node->textButton->animScaleY = sy; + } } else if (step.type == "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); @@ -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) { if (s->rect.contains((float)x, (float)y)) { pressedSliders[fingerId] = s; @@ -1212,6 +1407,7 @@ namespace ZL { void UiManager::onTouchUp(int64_t fingerId, int x, int y) { std::vector> clicked; + std::vector> clickedText; auto btnIt = pressedButtons.find(fingerId); if (btnIt != pressedButtons.end()) { @@ -1229,6 +1425,21 @@ namespace ZL { 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); for (auto& b : clicked) { @@ -1236,6 +1447,11 @@ namespace ZL { b->onClick(b->name); } } + for (auto& tb : clickedText) { + if (tb->onClick) { + tb->onClick(tb->name); + } + } } void UiManager::onKeyPress(unsigned char key) { @@ -1287,6 +1503,12 @@ namespace ZL { aa.origScaleX = node->button->animScaleX; 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 }); if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second; nodeActiveAnims[node].push_back(std::move(aa)); @@ -1337,6 +1559,12 @@ namespace ZL { aa.origScaleX = n->button->animScaleX; 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 }); if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second; nodeActiveAnims[n].push_back(std::move(aa)); @@ -1374,4 +1602,36 @@ namespace ZL { return true; } + std::shared_ptr 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 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 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 \ No newline at end of file diff --git a/src/UiManager.h b/src/UiManager.h index e8f8c69..a237771 100644 --- a/src/UiManager.h +++ b/src/UiManager.h @@ -125,6 +125,42 @@ namespace ZL { 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 texNormal; + std::shared_ptr texHover; + std::shared_ptr texPressed; + std::shared_ptr 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 color = { 1.f, 1.f, 1.f, 1.f }; + bool textCentered = true; + + std::unique_ptr textRenderer; + + std::function onClick; + std::function 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 { std::string name; UiRect rect; @@ -197,6 +233,7 @@ namespace ZL { // Компоненты (только один из них обычно активен для ноды) std::shared_ptr button; + std::shared_ptr textButton; std::shared_ptr slider; std::shared_ptr textView; std::shared_ptr textField; @@ -249,12 +286,12 @@ namespace ZL { // Returns true if any finger is currently interacting with UI 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 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() { @@ -268,6 +305,14 @@ namespace ZL { 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 findButton(const std::string& name); @@ -275,6 +320,11 @@ namespace ZL { bool setButtonCallback(const std::string& name, std::function cb); bool setButtonPressCallback(const std::string& name, std::function cb); + std::shared_ptr findTextButton(const std::string& name); + bool setTextButtonCallback(const std::string& name, std::function cb); + bool setTextButtonPressCallback(const std::string& name, std::function 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, const std::string& trackPath, const std::string& knobPath, float initialValue = 0.0f, bool vertical = true); @@ -333,6 +383,7 @@ namespace ZL { std::shared_ptr root; std::vector> buttons; + std::vector> textButtons; std::vector> sliders; std::vector> textViews; std::vector> textFields; @@ -343,16 +394,20 @@ namespace ZL { // Per-finger tracking for multi-touch support std::map> pressedButtons; + std::map> pressedTextButtons; std::map> pressedSliders; std::shared_ptr focusedTextField; struct MenuState { std::shared_ptr root; std::vector> buttons; + std::vector> textButtons; std::vector> sliders; + std::vector> textViews; std::vector> textFields; std::vector> staticImages; std::map> pressedButtons; + std::map> pressedTextButtons; std::map> pressedSliders; std::shared_ptr focusedTextField; std::string path;