From 785b96ce82d3344239d6bb8e86dff64e0c006d12 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Mon, 9 Mar 2026 23:04:48 +0300 Subject: [PATCH] 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;