From 4af20c65e675b7e8294e6fdf4954f2830ec53487 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Fri, 6 Mar 2026 21:52:00 +0300 Subject: [PATCH] Make menu to state machine --- resources/config/connecting.json | 22 ++ resources/config/connection_failed.json | 52 +++ resources/config/connection_lost.json | 52 +++ resources/config/main_menu.json | 24 +- resources/connecting.png | 3 + resources/connection_failed.png | 3 + resources/connection_lost.png | 3 + src/Game.cpp | 19 +- src/Game.h | 2 - src/MenuManager.cpp | 455 +++++++++--------------- src/MenuManager.h | 53 ++- src/Space.cpp | 2 +- src/UiManager.cpp | 107 ++++++ src/UiManager.h | 18 + 14 files changed, 480 insertions(+), 335 deletions(-) create mode 100644 resources/config/connecting.json create mode 100644 resources/config/connection_failed.json create mode 100644 resources/config/connection_lost.json create mode 100644 resources/connecting.png create mode 100644 resources/connection_failed.png create mode 100644 resources/connection_lost.png diff --git a/resources/config/connecting.json b/resources/config/connecting.json new file mode 100644 index 0000000..b305d6e --- /dev/null +++ b/resources/config/connecting.json @@ -0,0 +1,22 @@ +{ + "root": { + "type": "FrameLayout", + "x": 0, + "y": 0, + "width": "match_parent", + "height": "match_parent", + "children": [ + { + "type": "StaticImage", + "name": "connecting", + "x" : 0, + "y" : 0, + "width": 488, + "height": 154, + "horizontal_gravity": "center", + "vertical_gravity": "center", + "texture": "resources/connecting.png" + } + ] + } +} \ No newline at end of file diff --git a/resources/config/connection_failed.json b/resources/config/connection_failed.json new file mode 100644 index 0000000..1ea3df9 --- /dev/null +++ b/resources/config/connection_failed.json @@ -0,0 +1,52 @@ +{ + "root": { + "type": "FrameLayout", + "x": 0, + "y": 0, + "width": "match_parent", + "height": "match_parent", + "children": [ + { + "type": "StaticImage", + "name": "connectionFailed", + "x" : 0, + "y" : 0, + "width": 488, + "height": 308, + "horizontal_gravity": "center", + "vertical_gravity": "center", + "texture": "resources/connection_failed.png" + }, + { + "type": "Button", + "name": "connectionFailedReconnectButton", + "width": 382, + "height": 56, + "x" : 0, + "y" : -20, + "horizontal_gravity": "center", + "vertical_gravity": "center", + "textures": { + "normal": "resources/game_over/Filledbuttons.png", + "hover": "resources/game_over/Variant5.png", + "pressed": "resources/game_over/Variant6.png" + } + }, + { + "type": "Button", + "name": "connectionFailedGoBack", + "width": 382, + "height": 56, + "x" : 0, + "y" : -86, + "horizontal_gravity": "center", + "vertical_gravity": "center", + "textures": { + "normal": "resources/game_over/Secondarybutton.png", + "hover": "resources/game_over/Secondarybutton.png", + "pressed": "resources/game_over/Secondarybutton.png" + } + } + ] + } +} \ No newline at end of file diff --git a/resources/config/connection_lost.json b/resources/config/connection_lost.json new file mode 100644 index 0000000..fd84d8a --- /dev/null +++ b/resources/config/connection_lost.json @@ -0,0 +1,52 @@ +{ + "root": { + "type": "FrameLayout", + "x": 0, + "y": 0, + "width": "match_parent", + "height": "match_parent", + "children": [ + { + "type": "StaticImage", + "name": "connectionLost", + "x" : 0, + "y" : 0, + "width": 488, + "height": 308, + "horizontal_gravity": "center", + "vertical_gravity": "center", + "texture": "resources/connection_lost.png" + }, + { + "type": "Button", + "name": "reconnectButton", + "width": 382, + "height": 56, + "x" : 0, + "y" : -20, + "horizontal_gravity": "center", + "vertical_gravity": "center", + "textures": { + "normal": "resources/game_over/Filledbuttons.png", + "hover": "resources/game_over/Variant5.png", + "pressed": "resources/game_over/Variant6.png" + } + }, + { + "type": "Button", + "name": "exitServerButton", + "width": 382, + "height": 56, + "x" : 0, + "y" : -86, + "horizontal_gravity": "center", + "vertical_gravity": "center", + "textures": { + "normal": "resources/game_over/Secondarybutton.png", + "hover": "resources/game_over/Secondarybutton.png", + "pressed": "resources/game_over/Secondarybutton.png" + } + } + ] + } +} \ No newline at end of file diff --git a/resources/config/main_menu.json b/resources/config/main_menu.json index 3d1c80b..e97adb4 100644 --- a/resources/config/main_menu.json +++ b/resources/config/main_menu.json @@ -11,26 +11,18 @@ "height": "match_parent", "children": [ { - "type": "Button", + "type": "StaticImage", "name": "titleBtn", "width": 254, "height": 35, - "textures": { - "normal": "resources/main_menu/title.png", - "hover": "resources/main_menu/title.png", - "pressed": "resources/main_menu/title.png" - } + "texture": "resources/main_menu/title.png" }, { - "type": "Button", + "type": "StaticImage", "name": "underlineBtn", "width": 168, "height": 44, - "textures": { - "normal": "resources/main_menu/line.png", - "hover": "resources/main_menu/line.png", - "pressed": "resources/main_menu/line.png" - } + "texture": "resources/main_menu/line.png" }, { "type": "Button", @@ -66,15 +58,11 @@ } }, { - "type": "Button", + "type": "StaticImage", "name": "versionLabel", "width": 81, "height": 9, - "textures": { - "normal": "resources/main_menu/version.png", - "hover": "resources/main_menu/version.png", - "pressed": "resources/main_menu/version.png" - } + "texture": "resources/main_menu/version.png" } ] } diff --git a/resources/connecting.png b/resources/connecting.png new file mode 100644 index 0000000..f88597c --- /dev/null +++ b/resources/connecting.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c7d62c0ae0956dfd50dbf6616b9162c2a985c3d36287e2bc8b696639ffe4a19 +size 87183 diff --git a/resources/connection_failed.png b/resources/connection_failed.png new file mode 100644 index 0000000..52515b1 --- /dev/null +++ b/resources/connection_failed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:801e00e096b469e1081cd805e83e682b858ded21ab0d3963bf7633fb22a76a4e +size 124627 diff --git a/resources/connection_lost.png b/resources/connection_lost.png new file mode 100644 index 0000000..52515b1 --- /dev/null +++ b/resources/connection_lost.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:801e00e096b469e1081cd805e83e682b858ded21ab0d3963bf7633fb22a76a4e +size 124627 diff --git a/src/Game.cpp b/src/Game.cpp index 4aa8d80..abf01e1 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -150,7 +150,6 @@ namespace ZL Environment::shipState.nickname = nickname; Environment::shipState.shipType = shipType; - if (Environment::shipState.shipType == 1) { menuManager.uiManager.findButton("shootButton")->state = ButtonState::Disabled; @@ -171,14 +170,12 @@ namespace ZL networkClient->Connect("", 0); lastTickCount = 0; - spaceGameStarted = 1; }; menuManager.onMultiplayerPressed = [this](const std::string& nickname, int shipType) { Environment::shipState.nickname = nickname; Environment::shipState.shipType = shipType; - #ifdef EMSCRIPTEN networkClient = std::make_unique(); networkClient->Connect("localhost", 8081); @@ -186,7 +183,7 @@ namespace ZL networkClient = std::make_unique(taskManager.getIOContext()); networkClient->Connect("localhost", 8081); #endif - + if (networkClient) { std::string joinMsg = std::string("JOIN:") + nickname + ":" + std::to_string(shipType); networkClient->Send(joinMsg); @@ -195,12 +192,10 @@ namespace ZL space.boxCoordsArr.clear(); space.boxRenderArr.clear(); - //space.boxLabels.clear(); space.boxAlive.clear(); space.serverBoxesApplied = false; lastTickCount = 0; - spaceGameStarted = 1; }; @@ -252,7 +247,7 @@ namespace ZL else { - if (spaceGameStarted) { + if (menuManager.shouldRenderSpace()) { space.drawScene(); } else @@ -336,7 +331,7 @@ namespace ZL //throw std::runtime_error("Synchronization is lost"); } - if (spaceGameStarted) { + if (menuManager.shouldRenderSpace()) { space.processTickCount(newTickCount, delta); } menuManager.uiManager.update(static_cast(delta)); @@ -503,7 +498,7 @@ namespace ZL } mainThreadHandler.processMainThreadTasks(); - if (spaceGameStarted) { + if (menuManager.shouldRenderSpace()) { space.update(); } } @@ -529,7 +524,7 @@ namespace ZL }(); if (!menuManager.uiManager.isUiInteraction()) { - if (spaceGameStarted) { + if (menuManager.shouldRenderSpace()) { space.handleDown(mx, my); } } @@ -544,7 +539,7 @@ namespace ZL menuManager.uiManager.onMouseUp(uiX, uiY); if (!menuManager.uiManager.isUiInteraction()) { - if (spaceGameStarted) { + if (menuManager.shouldRenderSpace()) { space.handleUp(mx, my); } } @@ -559,7 +554,7 @@ namespace ZL menuManager.uiManager.onMouseMove(uiX, uiY); if (!menuManager.uiManager.isUiInteraction()) { - if (spaceGameStarted) { + if (menuManager.shouldRenderSpace()) { space.handleMotion(mx, my); } } diff --git a/src/Game.h b/src/Game.h index 208fa1e..123e963 100644 --- a/src/Game.h +++ b/src/Game.h @@ -71,8 +71,6 @@ namespace ZL { MenuManager menuManager; Space space; - - int spaceGameStarted = 0; }; diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index fcd47f5..8d16e17 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -1,5 +1,5 @@ -#include "MenuManager.h" - +#include "MenuManager.h" +#include namespace ZL { @@ -10,294 +10,177 @@ namespace ZL { void MenuManager::setupMenu() { + mainMenuRoot = loadUiFromFile("resources/config/main_menu.json", renderer, CONST_ZIP_FILE); + shipSelectionRoot = loadUiFromFile("resources/config/ship_selection_menu.json", renderer, CONST_ZIP_FILE); + gameplayRoot = loadUiFromFile("resources/config/ui.json", renderer, CONST_ZIP_FILE); + gameOverRoot = loadUiFromFile("resources/config/game_over.json", renderer, CONST_ZIP_FILE); + connectionLostRoot = loadUiFromFile("resources/config/connection_lost.json", renderer, CONST_ZIP_FILE); - uiManager.loadFromFile("resources/config/main_menu.json", renderer, CONST_ZIP_FILE); - - uiSavedRoot = loadUiFromFile("resources/config/ui.json", renderer, CONST_ZIP_FILE); - - settingsSavedRoot = loadUiFromFile("resources/config/settings.json", renderer, CONST_ZIP_FILE); - - multiplayerSavedRoot = loadUiFromFile("resources/config/multiplayer_menu.json", renderer, CONST_ZIP_FILE); - - gameOverSavedRoot = loadUiFromFile("resources/config/game_over.json", renderer, CONST_ZIP_FILE); - - auto shipSelectionRoot = loadUiFromFile("resources/config/ship_selection_menu.json", renderer, CONST_ZIP_FILE); - std::function loadGameplayUI; - loadGameplayUI = [this]() { - uiManager.replaceRoot(uiSavedRoot); - - auto velocityTv = uiManager.findTextView("velocityText"); - if (velocityTv) { - velocityTv->rect.x = 10.0f; - velocityTv->rect.y = static_cast(Environment::height) - velocityTv->rect.h - 10.0f; - } - else { - std::cerr << "Failed to find velocityText in UI" << std::endl; - } - - uiManager.startAnimationOnNode("backgroundNode", "bgScroll"); - static bool isExitButtonAnimating = false; - uiManager.setAnimationCallback("settingsButton", "buttonsExit", [this]() { - std::cerr << "Settings button animation finished -> ??????? ? ?????????" << std::endl; - if (uiManager.pushMenuFromSavedRoot(settingsSavedRoot)) { - uiManager.setButtonCallback("Opt1", [this](const std::string& n) { - std::cerr << "Opt1 pressed: " << n << std::endl; - }); - uiManager.setButtonCallback("Opt2", [this](const std::string& n) { - std::cerr << "Opt2 pressed: " << n << std::endl; - }); - uiManager.setButtonCallback("backButton", [this](const std::string& n) { - uiManager.stopAllAnimations(); - uiManager.popMenu(); - }); - } - else { - std::cerr << "Failed to open settings menu after animations" << std::endl; - } - }); - - uiManager.setAnimationCallback("exitButton", "bgScroll", [this]() { - std::cerr << "Exit button bgScroll animation finished" << std::endl; - g_exitBgAnimating = false; - }); - - uiManager.setButtonCallback("playButton", [this](const std::string& name) { - std::cerr << "Play button pressed: " << name << std::endl; - }); - - uiManager.setButtonCallback("settingsButton", [this](const std::string& name) { - std::cerr << "Settings button pressed: " << name << std::endl; - uiManager.startAnimationOnNode("playButton", "buttonsExit"); - uiManager.startAnimationOnNode("settingsButton", "buttonsExit"); - uiManager.startAnimationOnNode("exitButton", "buttonsExit"); - }); - - uiManager.setButtonCallback("exitButton", [this](const std::string& name) { - std::cerr << "Exit button pressed: " << name << std::endl; - - if (!g_exitBgAnimating) { - std::cerr << "start repeat anim bgScroll on exitButton" << std::endl; - g_exitBgAnimating = true; - uiManager.startAnimationOnNode("exitButton", "bgScroll"); - } - else { - std::cerr << "stop repeat anim bgScroll on exitButton" << std::endl; - g_exitBgAnimating = false; - uiManager.stopAnimationOnNode("exitButton", "bgScroll"); - - auto exitButton = uiManager.findButton("exitButton"); - if (exitButton) { - exitButton->animOffsetX = 0.0f; - exitButton->animOffsetY = 0.0f; - exitButton->animScaleX = 1.0f; - exitButton->animScaleY = 1.0f; - exitButton->buildMesh(); - } - } - }); - - - uiManager.setButtonCallback("shootButton", [this](const std::string& name) { - onFirePressed(); - }); - uiManager.setButtonCallback("shootButton2", [this](const std::string& name) { - onFirePressed(); - }); - uiManager.setButtonCallback("plusButton", [this](const std::string& name) { - int newVel = Environment::shipState.selectedVelocity+1; - if (newVel > 4) - { - newVel = 4; - } - onVelocityChanged(newVel); - }); - uiManager.setButtonCallback("minusButton", [this](const std::string& name) { - int newVel = Environment::shipState.selectedVelocity-1; - if (newVel < 0) - { - newVel = 0; - } - onVelocityChanged(newVel); - }); - uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) { - - int newVel = roundf(value * 10); - if (newVel > 2) - { - newVel = 2; - } - - if (newVel != Environment::shipState.selectedVelocity) { - onVelocityChanged(newVel); - } - }); - }; - - uiManager.setButtonCallback("singleButton", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) { - std::cerr << "Single button pressed: " << name << " -> open ship selection UI\n"; - if (!shipSelectionRoot) { - std::cerr << "Failed to load ship selection UI\n"; - return; - } - if (uiManager.pushMenuFromSavedRoot(shipSelectionRoot)) { - uiManager.setButtonCallback("spaceshipButton", [this, loadGameplayUI](const std::string& btnName) { - std::string nick = uiManager.getTextFieldValue("nicknameInput"); - if (nick.empty()) nick = "Player"; - int shipType = 0; - uiManager.popMenu(); - loadGameplayUI(); - if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType); - }); - - uiManager.setButtonCallback("cargoshipButton", [this, loadGameplayUI](const std::string& btnName) { - std::string nick = uiManager.getTextFieldValue("nicknameInput"); - if (nick.empty()) nick = "Player"; - int shipType = 1; - uiManager.popMenu(); - loadGameplayUI(); - if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType); - }); - - uiManager.setButtonCallback("backButton", [this](const std::string& btnName) { - uiManager.popMenu(); - }); - } - else { - std::cerr << "Failed to push ship selection menu\n"; - } - }); - - uiManager.setButtonCallback("multiplayerButton", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) { - std::cerr << "Multiplayer button pressed: " << name << " -> open ship selection UI\n"; - if (!shipSelectionRoot) { - std::cerr << "Failed to load ship selection UI\n"; - return; - } - if (uiManager.pushMenuFromSavedRoot(shipSelectionRoot)) { - uiManager.setButtonCallback("spaceshipButton", [this, loadGameplayUI](const std::string& btnName) { - std::string nick = uiManager.getTextFieldValue("nicknameInput"); - if (nick.empty()) nick = "Player"; - int shipType = 0; - uiManager.popMenu(); - loadGameplayUI(); - if (onMultiplayerPressed) onMultiplayerPressed(nick, shipType); - }); - - uiManager.setButtonCallback("cargoshipButton", [this, loadGameplayUI](const std::string& btnName) { - std::string nick = uiManager.getTextFieldValue("nicknameInput"); - if (nick.empty()) nick = "Player"; - int shipType = 1; - uiManager.popMenu(); - loadGameplayUI(); - if (onMultiplayerPressed) onMultiplayerPressed(nick, shipType); - }); - - uiManager.setButtonCallback("backButton", [this](const std::string& btnName) { - uiManager.popMenu(); - }); - } - else { - std::cerr << "Failed to push ship selection menu\n"; - } - }); - - /*uiManager.setButtonCallback("multiplayerButton2", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) { - std::cerr << "Multiplayer button pressed → opening multiplayer menu\n"; - - uiManager.startAnimationOnNode("playButton", "buttonsExit"); - uiManager.startAnimationOnNode("settingsButton", "buttonsExit"); - uiManager.startAnimationOnNode("multiplayerButton", "buttonsExit"); - uiManager.startAnimationOnNode("exitButton", "buttonsExit"); - - if (uiManager.pushMenuFromSavedRoot(multiplayerSavedRoot)) { - - uiManager.setButtonCallback("connectButton", [this](const std::string& buttonName) { - std::string serverAddress = uiManager.getTextFieldValue("serverInputField"); - - if (serverAddress.empty()) { - uiManager.setText("statusText", "Please enter server address"); - return; - } - - uiManager.setText("statusText", "Connecting to " + serverAddress + "..."); - std::cerr << "Connecting to server: " << serverAddress << std::endl; - - }); - - uiManager.setButtonCallback("backButton", [this](const std::string& buttonName) { - uiManager.popMenu(); - }); - - uiManager.setTextFieldCallback("serverInputField", - [this](const std::string& fieldName, const std::string& newText) { - std::cout << "Server input field changed to: " << newText << std::endl; - }); - - std::cerr << "Multiplayer menu loaded successfully\n"; - } - else { - std::cerr << "Failed to load multiplayer menu\n"; - } - std::cerr << "Single button pressed: " << name << " -> open ship selection UI\n"; - if (!shipSelectionRoot) { - std::cerr << "Failed to load ship selection UI\n"; - return; - } - if (uiManager.pushMenuFromSavedRoot(shipSelectionRoot)) { - uiManager.setButtonCallback("spaceshipButton", [this, loadGameplayUI](const std::string& btnName) { - std::string nick = uiManager.getTextFieldValue("nicknameInput"); - if (nick.empty()) nick = "Player"; - int shipType = 0; - uiManager.popMenu(); - loadGameplayUI(); - if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType); - }); - - uiManager.setButtonCallback("cargoshipButton", [this, loadGameplayUI](const std::string& btnName) { - std::string nick = uiManager.getTextFieldValue("nicknameInput"); - if (nick.empty()) nick = "Player"; - int shipType = 1; - uiManager.popMenu(); - loadGameplayUI(); - if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType); - }); - - uiManager.setButtonCallback("backButton", [this](const std::string& btnName) { - uiManager.popMenu(); - }); - } - else { - std::cerr << "Failed to push ship selection menu\n"; - } - }); - uiManager.setButtonCallback("exitButton", [](const std::string& name) { - std::cerr << "Exit from main menu pressed: " << name << " -> exiting\n"; - Environment::exitGameLoop = true; - });*/ + enterMainMenu(); } + bool MenuManager::shouldRenderSpace() const + { + return state == GameState::Gameplay + || state == GameState::GameOver + || state == GameState::ConnectionLost; + } + + // ── State: MainMenu ────────────────────────────────────────────────────── + + void MenuManager::enterMainMenu() + { + state = GameState::MainMenu; + uiManager.replaceRoot(mainMenuRoot); + + uiManager.setButtonCallback("singleButton", [this](const std::string&) { + enterShipSelectionSingle(); + }); + + uiManager.setButtonCallback("multiplayerButton", [this](const std::string&) { + enterShipSelectionMulti(); + }); + } + + // ── State: ShipSelectionSingle ─────────────────────────────────────────── + + void MenuManager::enterShipSelectionSingle() + { + state = GameState::ShipSelectionSingle; + uiManager.replaceRoot(shipSelectionRoot); + + uiManager.setButtonCallback("spaceshipButton", [this](const std::string&) { + std::string nick = uiManager.getTextFieldValue("nicknameInput"); + if (nick.empty()) nick = "Player"; + enterGameplay(); + if (onSingleplayerPressed) onSingleplayerPressed(nick, 0); + }); + + uiManager.setButtonCallback("cargoshipButton", [this](const std::string&) { + std::string nick = uiManager.getTextFieldValue("nicknameInput"); + if (nick.empty()) nick = "Player"; + enterGameplay(); + if (onSingleplayerPressed) onSingleplayerPressed(nick, 1); + }); + + uiManager.setButtonCallback("backButton", [this](const std::string&) { + enterMainMenu(); + }); + } + + // ── State: ShipSelectionMulti ───────────────────────────────────────────── + + void MenuManager::enterShipSelectionMulti() + { + state = GameState::ShipSelectionMulti; + uiManager.replaceRoot(shipSelectionRoot); + + uiManager.setButtonCallback("spaceshipButton", [this](const std::string&) { + std::string nick = uiManager.getTextFieldValue("nicknameInput"); + if (nick.empty()) nick = "Player"; + enterGameplay(); + if (onMultiplayerPressed) onMultiplayerPressed(nick, 0); + }); + + uiManager.setButtonCallback("cargoshipButton", [this](const std::string&) { + std::string nick = uiManager.getTextFieldValue("nicknameInput"); + if (nick.empty()) nick = "Player"; + enterGameplay(); + if (onMultiplayerPressed) onMultiplayerPressed(nick, 1); + }); + + uiManager.setButtonCallback("backButton", [this](const std::string&) { + enterMainMenu(); + }); + } + + // ── State: Gameplay ────────────────────────────────────────────────────── + + void MenuManager::enterGameplay() + { + state = GameState::Gameplay; + uiManager.replaceRoot(gameplayRoot); + + 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.setButtonCallback("shootButton", [this](const std::string&) { + if (onFirePressed) onFirePressed(); + }); + uiManager.setButtonCallback("shootButton2", [this](const std::string&) { + if (onFirePressed) onFirePressed(); + }); + uiManager.setButtonCallback("plusButton", [this](const std::string&) { + int newVel = Environment::shipState.selectedVelocity + 1; + if (newVel > 4) newVel = 4; + if (onVelocityChanged) onVelocityChanged(newVel); + }); + uiManager.setButtonCallback("minusButton", [this](const std::string&) { + int newVel = Environment::shipState.selectedVelocity - 1; + if (newVel < 0) newVel = 0; + if (onVelocityChanged) onVelocityChanged(newVel); + }); + 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 ────────────────────────────────────────────────────── + + void MenuManager::enterGameOver(int score) + { + state = GameState::GameOver; + uiManager.replaceRoot(gameOverRoot); + + uiManager.setText("scoreText", "Score: " + std::to_string(score)); + + uiManager.setButtonCallback("restartButton", [this](const std::string&) { + if (onRestartPressed) onRestartPressed(); + enterGameplay(); + }); + uiManager.setButtonCallback("gameOverExitButton", [this](const std::string&) { + Environment::exitGameLoop = true; + }); + } + + // ── State: ConnectionLost ───────────────────────────────────────────────── + + void MenuManager::enterConnectionLost() + { + state = GameState::ConnectionLost; + uiManager.replaceRoot(connectionLostRoot); + + uiManager.setButtonCallback("reconnectButton", [this](const std::string&) { + // TODO: reconnect logic + }); + uiManager.setButtonCallback("exitServerButton", [this](const std::string&) { + Environment::exitGameLoop = true; + }); + } + + // ── Public event API ────────────────────────────────────────────────────── + void MenuManager::showGameOver(int score) { - if (!uiGameOverShown) { - if (uiManager.pushMenuFromSavedRoot(gameOverSavedRoot)) { - uiManager.setText("scoreText", std::string("Score: ") + std::to_string(score)); - - uiManager.setButtonCallback("restartButton", [this](const std::string& name) { - uiManager.setText("scoreText", ""); - uiGameOverShown = false; - uiManager.popMenu(); - if (onRestartPressed) onRestartPressed(); - }); - - uiManager.setButtonCallback("gameOverExitButton", [this](const std::string& name) { - Environment::exitGameLoop = true; - }); - - uiGameOverShown = true; - } - else { - std::cerr << "Failed to load game_over.json\n"; - } + if (state == GameState::Gameplay) { + enterGameOver(score); } } -} + + void MenuManager::showConnectionLost() + { + if (state == GameState::Gameplay) { + enterConnectionLost(); + } + } + +} // namespace ZL \ No newline at end of file diff --git a/src/MenuManager.h b/src/MenuManager.h index 88757be..877226d 100644 --- a/src/MenuManager.h +++ b/src/MenuManager.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "render/Renderer.h" #include "Environment.h" #include "render/TextureManager.h" @@ -7,37 +7,58 @@ namespace ZL { extern const char* CONST_ZIP_FILE; - //extern bool g_exitBgAnimating; - class MenuManager - { + enum class GameState { + MainMenu, + ShipSelectionSingle, + ShipSelectionMulti, + Gameplay, + GameOver, + ConnectionLost + }; + + class MenuManager { protected: Renderer& renderer; - std::shared_ptr uiSavedRoot; - std::shared_ptr gameOverSavedRoot; - std::shared_ptr settingsSavedRoot; - std::shared_ptr multiplayerSavedRoot; + + // Pre-loaded UI roots (loaded once in setupMenu) + std::shared_ptr mainMenuRoot; + std::shared_ptr shipSelectionRoot; + std::shared_ptr gameplayRoot; + std::shared_ptr gameOverRoot; + std::shared_ptr connectionLostRoot; + + GameState state = GameState::MainMenu; + + // State transition methods + void enterMainMenu(); + void enterShipSelectionSingle(); + void enterShipSelectionMulti(); + void enterGameplay(); + void enterGameOver(int score); + void enterConnectionLost(); public: - bool uiGameOverShown = false; - bool g_exitBgAnimating = false; - UiManager uiManager; MenuManager(Renderer& iRenderer); void setupMenu(); - //void showGameOver(); - void showGameOver(int score); + // Returns true for states where Space should render and run (Gameplay, GameOver, ConnectionLost) + bool shouldRenderSpace() const; + GameState getState() const { return state; } + // Called by game events + void showGameOver(int score); + void showConnectionLost(); + + // Callbacks set by Game/Space std::function onRestartPressed; std::function onVelocityChanged; std::function onFirePressed; - std::function onSingleplayerPressed; std::function onMultiplayerPressed; - }; -}; +} // namespace ZL \ No newline at end of file diff --git a/src/Space.cpp b/src/Space.cpp index b079041..e884170 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -1831,7 +1831,7 @@ namespace ZL gameOver = true; Environment::shipState.velocity = 0.0f; std::cout << "Client: Lost connection to server\n"; - menuManager.showGameOver(this->playerScore); + menuManager.showConnectionLost(); } auto pending = networkClient->getPendingProjectiles(); diff --git a/src/UiManager.cpp b/src/UiManager.cpp index 8a7bcf9..cf059c6 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -169,6 +169,54 @@ namespace ZL { renderer.DisableVertexAttribArray(vTexCoordName); } + void UiStaticImage::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 UiStaticImage::draw(Renderer& renderer) const { + if (!texture) return; + + static const std::string vPositionName = "vPosition"; + static const std::string vTexCoordName = "vTexCoord"; + static const std::string textureUniformName = "Texture"; + + renderer.RenderUniform1i(textureUniformName, 0); + renderer.EnableVertexAttribArray(vPositionName); + renderer.EnableVertexAttribArray(vTexCoordName); + + glBindTexture(GL_TEXTURE_2D, texture->getTexID()); + renderer.DrawVertexRenderStruct(mesh); + + renderer.DisableVertexAttribArray(vPositionName); + renderer.DisableVertexAttribArray(vTexCoordName); + } + void UiTextField::draw(Renderer& renderer) const { if (textRenderer) { float textX = rect.x + 10.0f; @@ -249,6 +297,7 @@ namespace ZL { if (j.contains("horizontal_gravity")) { std::string hg = j["horizontal_gravity"].get(); if (hg == "right") node->layoutSettings.hGravity = HorizontalGravity::Right; + else if (hg == "center") node->layoutSettings.hGravity = HorizontalGravity::Center; else node->layoutSettings.hGravity = HorizontalGravity::Left; } @@ -256,6 +305,7 @@ namespace ZL { if (j.contains("vertical_gravity")) { std::string vg = j["vertical_gravity"].get(); if (vg == "bottom") node->layoutSettings.vGravity = VerticalGravity::Bottom; + else if (vg == "center") node->layoutSettings.vGravity = VerticalGravity::Center; else node->layoutSettings.vGravity = VerticalGravity::Top; } @@ -403,6 +453,32 @@ namespace ZL { } } + if (typeStr == "StaticImage") { + auto img = std::make_shared(); + img->name = node->name; + img->rect = initialRect; + + std::string texPath; + if (j.contains("texture") && j["texture"].is_string()) { + texPath = j["texture"].get(); + } + else if (j.contains("textures") && j["textures"].is_object() && j["textures"].contains("normal")) { + texPath = j["textures"]["normal"].get(); + } + + if (!texPath.empty()) { + try { + auto data = CreateTextureDataFromPng(texPath.c_str(), zipFile.c_str()); + img->texture = std::make_shared(data); + } + catch (const std::exception& e) { + std::cerr << "UiManager: failed load texture for StaticImage '" << img->name << "' : " << e.what() << std::endl; + } + } + + node->staticImage = img; + } + if (typeStr == "TextView") { auto tv = std::make_shared(); @@ -492,6 +568,7 @@ namespace ZL { sliders.clear(); textViews.clear(); textFields.clear(); + staticImages.clear(); collectButtonsAndSliders(root); nodeActiveAnims.clear(); @@ -503,6 +580,9 @@ namespace ZL { s->buildTrackMesh(); s->buildKnobMesh(); } + for (auto& img : staticImages) { + img->buildMesh(); + } } void UiManager::loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) { @@ -618,9 +698,15 @@ namespace ZL { if (child->layoutSettings.hGravity == HorizontalGravity::Right) { fLX = currentW - childW - child->localX; } + else if (child->layoutSettings.hGravity == HorizontalGravity::Center) { + fLX = (currentW - childW) / 2.0f + child->localX; + } if (child->layoutSettings.vGravity == VerticalGravity::Top) { fLY = currentH - childH - child->localY; } + else if (child->layoutSettings.vGravity == VerticalGravity::Center) { + fLY = (currentH - childH) / 2.0f + child->localY; + } // Передаем рассчитанные fLX, fLY в рекурсию layoutNode(child, node->screenRect.x, node->screenRect.y, currentW, currentH, fLX, fLY); @@ -661,6 +747,12 @@ namespace ZL { node->textField->rect = node->screenRect; // Аналогично для курсора и фонового меша } + + // 5. Обновляем статическое изображение + if (node->staticImage) { + node->staticImage->rect = node->screenRect; + node->staticImage->buildMesh(); + } } void UiManager::updateAllLayouts() { @@ -690,6 +782,9 @@ namespace ZL { if (node->textField) { textFields.push_back(node->textField); } + if (node->staticImage) { + staticImages.push_back(node->staticImage); + } for (auto& c : node->children) collectButtonsAndSliders(c); } @@ -788,6 +883,7 @@ namespace ZL { prev.buttons = buttons; prev.sliders = sliders; prev.textFields = textFields; + prev.staticImages = staticImages; prev.pressedButton = pressedButton; prev.pressedSlider = pressedSlider; prev.focusedTextField = focusedTextField; @@ -838,6 +934,7 @@ namespace ZL { buttons = s.buttons; sliders = s.sliders; textFields = s.textFields; + staticImages = s.staticImages; pressedButton = s.pressedButton; pressedSlider = s.pressedSlider; focusedTextField = s.focusedTextField; @@ -872,6 +969,9 @@ namespace ZL { renderer.PushMatrix(); renderer.LoadIdentity(); + for (const auto& img : staticImages) { + img->draw(renderer); + } for (const auto& b : buttons) { b->draw(renderer); } @@ -1261,6 +1361,13 @@ namespace ZL { return true; } + std::shared_ptr UiManager::findStaticImage(const std::string& name) { + for (auto& img : staticImages) { + if (img->name == name) return img; + } + return nullptr; + } + std::shared_ptr UiManager::findTextView(const std::string& name) { for (auto& tv : textViews) { if (tv->name == name) return tv; diff --git a/src/UiManager.h b/src/UiManager.h index a03fa85..ccc2986 100644 --- a/src/UiManager.h +++ b/src/UiManager.h @@ -60,11 +60,13 @@ namespace ZL { enum class HorizontalGravity { Left, + Center, Right }; enum class VerticalGravity { Bottom, // Обычно в OpenGL Y растет вверх, так что низ - это 0 + Center, Top }; @@ -159,6 +161,17 @@ namespace ZL { void draw(Renderer& renderer) const; }; + struct UiStaticImage { + std::string name; + UiRect rect; + std::shared_ptr texture; + + VertexRenderStruct mesh; + + void buildMesh(); + void draw(Renderer& renderer) const; + }; + struct UiNode { std::string name; LayoutType layoutType = LayoutType::Frame; @@ -185,6 +198,7 @@ namespace ZL { std::shared_ptr slider; std::shared_ptr textView; std::shared_ptr textField; + std::shared_ptr staticImage; // Анимации struct AnimStep { @@ -256,6 +270,8 @@ namespace ZL { bool setTextFieldCallback(const std::string& name, std::function cb); std::string getTextFieldValue(const std::string& name); + std::shared_ptr findStaticImage(const std::string& name); + bool pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = ""); bool pushMenuFromSavedRoot(std::shared_ptr newRoot); bool popMenu(); @@ -301,6 +317,7 @@ namespace ZL { std::vector> sliders; std::vector> textViews; std::vector> textFields; + std::vector> staticImages; std::map, std::vector> nodeActiveAnims; std::map, std::function> animCallbacks; // key: (nodeName, animName) @@ -314,6 +331,7 @@ namespace ZL { std::vector> buttons; std::vector> sliders; std::vector> textFields; + std::vector> staticImages; std::shared_ptr pressedButton; std::shared_ptr pressedSlider; std::shared_ptr focusedTextField;