added buttons to main state
This commit is contained in:
@ -50,6 +50,7 @@ add_executable(tetris
|
|||||||
# State implementations (new)
|
# State implementations (new)
|
||||||
src/states/LoadingState.cpp
|
src/states/LoadingState.cpp
|
||||||
src/states/MenuState.cpp
|
src/states/MenuState.cpp
|
||||||
|
src/states/OptionsState.cpp
|
||||||
src/states/LevelSelectorState.cpp
|
src/states/LevelSelectorState.cpp
|
||||||
src/states/PlayingState.cpp
|
src/states/PlayingState.cpp
|
||||||
)
|
)
|
||||||
@ -144,6 +145,7 @@ add_executable(tetris_refactored
|
|||||||
# State implementations
|
# State implementations
|
||||||
src/states/LoadingState.cpp
|
src/states/LoadingState.cpp
|
||||||
src/states/MenuState.cpp
|
src/states/MenuState.cpp
|
||||||
|
src/states/OptionsState.cpp
|
||||||
src/states/LevelSelectorState.cpp
|
src/states/LevelSelectorState.cpp
|
||||||
src/states/PlayingState.cpp
|
src/states/PlayingState.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
#include "../../states/State.h"
|
#include "../../states/State.h"
|
||||||
#include "../../states/LoadingState.h"
|
#include "../../states/LoadingState.h"
|
||||||
#include "../../states/MenuState.h"
|
#include "../../states/MenuState.h"
|
||||||
|
#include "../../states/OptionsState.h"
|
||||||
#include "../../states/LevelSelectorState.h"
|
#include "../../states/LevelSelectorState.h"
|
||||||
#include "../../states/PlayingState.h"
|
#include "../../states/PlayingState.h"
|
||||||
#include "../assets/AssetManager.h"
|
#include "../assets/AssetManager.h"
|
||||||
@ -285,6 +286,7 @@ bool ApplicationManager::initializeManagers() {
|
|||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize RenderManager");
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize RenderManager");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
m_isFullscreen = m_renderManager->isFullscreen();
|
||||||
|
|
||||||
// Create InputManager
|
// Create InputManager
|
||||||
m_inputManager = std::make_unique<InputManager>();
|
m_inputManager = std::make_unique<InputManager>();
|
||||||
@ -330,6 +332,7 @@ bool ApplicationManager::initializeManagers() {
|
|||||||
if (m_renderManager) {
|
if (m_renderManager) {
|
||||||
bool fs = m_renderManager->isFullscreen();
|
bool fs = m_renderManager->isFullscreen();
|
||||||
m_renderManager->setFullscreen(!fs);
|
m_renderManager->setFullscreen(!fs);
|
||||||
|
m_isFullscreen = m_renderManager->isFullscreen();
|
||||||
}
|
}
|
||||||
// Don’t also forward Alt+Enter as an Enter keypress to states (prevents accidental "Start")
|
// Don’t also forward Alt+Enter as an Enter keypress to states (prevents accidental "Start")
|
||||||
// Don't also forward Alt+Enter as an Enter keypress to states (prevents accidental "Start")
|
// Don't also forward Alt+Enter as an Enter keypress to states (prevents accidental "Start")
|
||||||
@ -588,10 +591,30 @@ bool ApplicationManager::initializeGame() {
|
|||||||
m_stateContext.showSettingsPopup = &m_showSettingsPopup;
|
m_stateContext.showSettingsPopup = &m_showSettingsPopup;
|
||||||
m_stateContext.showExitConfirmPopup = &m_showExitConfirmPopup;
|
m_stateContext.showExitConfirmPopup = &m_showExitConfirmPopup;
|
||||||
m_stateContext.exitPopupSelectedButton = &m_exitPopupSelectedButton;
|
m_stateContext.exitPopupSelectedButton = &m_exitPopupSelectedButton;
|
||||||
|
m_stateContext.playerName = &m_playerName;
|
||||||
|
m_stateContext.fullscreenFlag = &m_isFullscreen;
|
||||||
|
m_stateContext.applyFullscreen = [this](bool enable) {
|
||||||
|
if (m_renderManager) {
|
||||||
|
m_renderManager->setFullscreen(enable);
|
||||||
|
m_isFullscreen = m_renderManager->isFullscreen();
|
||||||
|
} else {
|
||||||
|
m_isFullscreen = enable;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
m_stateContext.queryFullscreen = [this]() -> bool {
|
||||||
|
if (m_renderManager) {
|
||||||
|
return m_renderManager->isFullscreen();
|
||||||
|
}
|
||||||
|
return m_isFullscreen;
|
||||||
|
};
|
||||||
|
m_stateContext.requestQuit = [this]() {
|
||||||
|
requestShutdown();
|
||||||
|
};
|
||||||
|
|
||||||
// Create state instances
|
// Create state instances
|
||||||
m_loadingState = std::make_unique<LoadingState>(m_stateContext);
|
m_loadingState = std::make_unique<LoadingState>(m_stateContext);
|
||||||
m_menuState = std::make_unique<MenuState>(m_stateContext);
|
m_menuState = std::make_unique<MenuState>(m_stateContext);
|
||||||
|
m_optionsState = std::make_unique<OptionsState>(m_stateContext);
|
||||||
m_levelSelectorState = std::make_unique<LevelSelectorState>(m_stateContext);
|
m_levelSelectorState = std::make_unique<LevelSelectorState>(m_stateContext);
|
||||||
m_playingState = std::make_unique<PlayingState>(m_stateContext);
|
m_playingState = std::make_unique<PlayingState>(m_stateContext);
|
||||||
|
|
||||||
@ -605,6 +628,10 @@ bool ApplicationManager::initializeGame() {
|
|||||||
m_stateManager->registerOnEnter(AppState::Menu, [this](){ if (m_menuState) m_menuState->onEnter(); });
|
m_stateManager->registerOnEnter(AppState::Menu, [this](){ if (m_menuState) m_menuState->onEnter(); });
|
||||||
m_stateManager->registerOnExit(AppState::Menu, [this](){ if (m_menuState) m_menuState->onExit(); });
|
m_stateManager->registerOnExit(AppState::Menu, [this](){ if (m_menuState) m_menuState->onExit(); });
|
||||||
|
|
||||||
|
m_stateManager->registerEventHandler(AppState::Options, [this](const SDL_Event& e){ if (m_optionsState) m_optionsState->handleEvent(e); });
|
||||||
|
m_stateManager->registerOnEnter(AppState::Options, [this](){ if (m_optionsState) m_optionsState->onEnter(); });
|
||||||
|
m_stateManager->registerOnExit(AppState::Options, [this](){ if (m_optionsState) m_optionsState->onExit(); });
|
||||||
|
|
||||||
m_stateManager->registerEventHandler(AppState::LevelSelector, [this](const SDL_Event& e){ if (m_levelSelectorState) m_levelSelectorState->handleEvent(e); });
|
m_stateManager->registerEventHandler(AppState::LevelSelector, [this](const SDL_Event& e){ if (m_levelSelectorState) m_levelSelectorState->handleEvent(e); });
|
||||||
m_stateManager->registerOnEnter(AppState::LevelSelector, [this](){ if (m_levelSelectorState) m_levelSelectorState->onEnter(); });
|
m_stateManager->registerOnEnter(AppState::LevelSelector, [this](){ if (m_levelSelectorState) m_levelSelectorState->onEnter(); });
|
||||||
m_stateManager->registerOnExit(AppState::LevelSelector, [this](){ if (m_levelSelectorState) m_levelSelectorState->onExit(); });
|
m_stateManager->registerOnExit(AppState::LevelSelector, [this](){ if (m_levelSelectorState) m_levelSelectorState->onExit(); });
|
||||||
@ -720,6 +747,32 @@ void ApplicationManager::setupStateHandlers() {
|
|||||||
SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f);
|
SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
m_stateManager->registerRenderHandler(AppState::Options,
|
||||||
|
[this](RenderManager& renderer) {
|
||||||
|
renderer.clear(0, 0, 20, 255);
|
||||||
|
int winW = 0, winH = 0;
|
||||||
|
if (m_renderManager) m_renderManager->getWindowSize(winW, winH);
|
||||||
|
SDL_Texture* background = m_assetManager->getTexture("background");
|
||||||
|
if (background && winW > 0 && winH > 0) {
|
||||||
|
SDL_FRect bgRect = { 0, 0, (float)winW, (float)winH };
|
||||||
|
renderer.renderTexture(background, nullptr, &bgRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Rect logicalVP = {0,0,0,0};
|
||||||
|
float logicalScale = 1.0f;
|
||||||
|
if (m_renderManager) {
|
||||||
|
logicalVP = m_renderManager->getLogicalViewport();
|
||||||
|
logicalScale = m_renderManager->getLogicalScale();
|
||||||
|
}
|
||||||
|
SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP);
|
||||||
|
SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale);
|
||||||
|
if (m_optionsState) {
|
||||||
|
m_optionsState->render(renderer.getSDLRenderer(), logicalScale, logicalVP);
|
||||||
|
}
|
||||||
|
SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr);
|
||||||
|
SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f);
|
||||||
|
});
|
||||||
|
|
||||||
// LevelSelector State render: draw background full-screen, then delegate to LevelSelectorState::render
|
// LevelSelector State render: draw background full-screen, then delegate to LevelSelectorState::render
|
||||||
m_stateManager->registerRenderHandler(AppState::LevelSelector,
|
m_stateManager->registerRenderHandler(AppState::LevelSelector,
|
||||||
[this](RenderManager& renderer) {
|
[this](RenderManager& renderer) {
|
||||||
|
|||||||
@ -21,6 +21,7 @@ class LineEffect;
|
|||||||
// Forward declare state classes (top-level, defined under src/states)
|
// Forward declare state classes (top-level, defined under src/states)
|
||||||
class LoadingState;
|
class LoadingState;
|
||||||
class MenuState;
|
class MenuState;
|
||||||
|
class OptionsState;
|
||||||
class LevelSelectorState;
|
class LevelSelectorState;
|
||||||
class PlayingState;
|
class PlayingState;
|
||||||
|
|
||||||
@ -95,6 +96,8 @@ private:
|
|||||||
bool m_showSettingsPopup = false;
|
bool m_showSettingsPopup = false;
|
||||||
bool m_showExitConfirmPopup = false;
|
bool m_showExitConfirmPopup = false;
|
||||||
int m_exitPopupSelectedButton = 1; // 0 = YES, 1 = NO
|
int m_exitPopupSelectedButton = 1; // 0 = YES, 1 = NO
|
||||||
|
bool m_isFullscreen = false;
|
||||||
|
std::string m_playerName = "PLAYER";
|
||||||
uint64_t m_loadStartTicks = 0;
|
uint64_t m_loadStartTicks = 0;
|
||||||
bool m_musicStarted = false;
|
bool m_musicStarted = false;
|
||||||
bool m_musicLoaded = false;
|
bool m_musicLoaded = false;
|
||||||
@ -120,6 +123,7 @@ private:
|
|||||||
// State objects (mirror main.cpp pattern)
|
// State objects (mirror main.cpp pattern)
|
||||||
std::unique_ptr<LoadingState> m_loadingState;
|
std::unique_ptr<LoadingState> m_loadingState;
|
||||||
std::unique_ptr<MenuState> m_menuState;
|
std::unique_ptr<MenuState> m_menuState;
|
||||||
|
std::unique_ptr<OptionsState> m_optionsState;
|
||||||
std::unique_ptr<LevelSelectorState> m_levelSelectorState;
|
std::unique_ptr<LevelSelectorState> m_levelSelectorState;
|
||||||
std::unique_ptr<PlayingState> m_playingState;
|
std::unique_ptr<PlayingState> m_playingState;
|
||||||
// Application state
|
// Application state
|
||||||
|
|||||||
@ -170,6 +170,7 @@ const char* StateManager::getStateName(AppState state) const {
|
|||||||
switch (state) {
|
switch (state) {
|
||||||
case AppState::Loading: return "Loading";
|
case AppState::Loading: return "Loading";
|
||||||
case AppState::Menu: return "Menu";
|
case AppState::Menu: return "Menu";
|
||||||
|
case AppState::Options: return "Options";
|
||||||
case AppState::LevelSelector: return "LevelSelector";
|
case AppState::LevelSelector: return "LevelSelector";
|
||||||
case AppState::Playing: return "Playing";
|
case AppState::Playing: return "Playing";
|
||||||
case AppState::LevelSelect: return "LevelSelect";
|
case AppState::LevelSelect: return "LevelSelect";
|
||||||
|
|||||||
@ -13,6 +13,7 @@ class RenderManager;
|
|||||||
enum class AppState {
|
enum class AppState {
|
||||||
Loading,
|
Loading,
|
||||||
Menu,
|
Menu,
|
||||||
|
Options,
|
||||||
LevelSelector,
|
LevelSelector,
|
||||||
Playing,
|
Playing,
|
||||||
LevelSelect,
|
LevelSelect,
|
||||||
|
|||||||
88
src/main.cpp
88
src/main.cpp
@ -26,6 +26,7 @@
|
|||||||
#include "states/State.h"
|
#include "states/State.h"
|
||||||
#include "states/LoadingState.h"
|
#include "states/LoadingState.h"
|
||||||
#include "states/MenuState.h"
|
#include "states/MenuState.h"
|
||||||
|
#include "states/OptionsState.h"
|
||||||
#include "states/LevelSelectorState.h"
|
#include "states/LevelSelectorState.h"
|
||||||
#include "states/PlayingState.h"
|
#include "states/PlayingState.h"
|
||||||
#include "audio/MenuWrappers.h"
|
#include "audio/MenuWrappers.h"
|
||||||
@ -652,10 +653,23 @@ int main(int, char **)
|
|||||||
ctx.showSettingsPopup = &showSettingsPopup;
|
ctx.showSettingsPopup = &showSettingsPopup;
|
||||||
ctx.showExitConfirmPopup = &showExitConfirmPopup;
|
ctx.showExitConfirmPopup = &showExitConfirmPopup;
|
||||||
ctx.exitPopupSelectedButton = &exitPopupSelectedButton;
|
ctx.exitPopupSelectedButton = &exitPopupSelectedButton;
|
||||||
|
ctx.playerName = &playerName;
|
||||||
|
ctx.fullscreenFlag = &isFullscreen;
|
||||||
|
ctx.applyFullscreen = [window, &isFullscreen](bool enable) {
|
||||||
|
SDL_SetWindowFullscreen(window, enable ? SDL_WINDOW_FULLSCREEN : 0);
|
||||||
|
isFullscreen = enable;
|
||||||
|
};
|
||||||
|
ctx.queryFullscreen = [window]() -> bool {
|
||||||
|
return (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0;
|
||||||
|
};
|
||||||
|
ctx.requestQuit = [&running]() {
|
||||||
|
running = false;
|
||||||
|
};
|
||||||
|
|
||||||
// Instantiate state objects
|
// Instantiate state objects
|
||||||
auto loadingState = std::make_unique<LoadingState>(ctx);
|
auto loadingState = std::make_unique<LoadingState>(ctx);
|
||||||
auto menuState = std::make_unique<MenuState>(ctx);
|
auto menuState = std::make_unique<MenuState>(ctx);
|
||||||
|
auto optionsState = std::make_unique<OptionsState>(ctx);
|
||||||
auto levelSelectorState = std::make_unique<LevelSelectorState>(ctx);
|
auto levelSelectorState = std::make_unique<LevelSelectorState>(ctx);
|
||||||
auto playingState = std::make_unique<PlayingState>(ctx);
|
auto playingState = std::make_unique<PlayingState>(ctx);
|
||||||
|
|
||||||
@ -668,6 +682,10 @@ int main(int, char **)
|
|||||||
stateMgr.registerOnEnter(AppState::Menu, [&](){ menuState->onEnter(); });
|
stateMgr.registerOnEnter(AppState::Menu, [&](){ menuState->onEnter(); });
|
||||||
stateMgr.registerOnExit(AppState::Menu, [&](){ menuState->onExit(); });
|
stateMgr.registerOnExit(AppState::Menu, [&](){ menuState->onExit(); });
|
||||||
|
|
||||||
|
stateMgr.registerHandler(AppState::Options, [&](const SDL_Event& e){ optionsState->handleEvent(e); });
|
||||||
|
stateMgr.registerOnEnter(AppState::Options, [&](){ optionsState->onEnter(); });
|
||||||
|
stateMgr.registerOnExit(AppState::Options, [&](){ optionsState->onExit(); });
|
||||||
|
|
||||||
stateMgr.registerHandler(AppState::LevelSelector, [&](const SDL_Event& e){ levelSelectorState->handleEvent(e); });
|
stateMgr.registerHandler(AppState::LevelSelector, [&](const SDL_Event& e){ levelSelectorState->handleEvent(e); });
|
||||||
stateMgr.registerOnEnter(AppState::LevelSelector, [&](){ levelSelectorState->onEnter(); });
|
stateMgr.registerOnEnter(AppState::LevelSelector, [&](){ levelSelectorState->onEnter(); });
|
||||||
stateMgr.registerOnExit(AppState::LevelSelector, [&](){ levelSelectorState->onExit(); });
|
stateMgr.registerOnExit(AppState::LevelSelector, [&](){ levelSelectorState->onExit(); });
|
||||||
@ -792,21 +810,30 @@ int main(int, char **)
|
|||||||
float btnCX = LOGICAL_W * 0.5f + contentOffsetX;
|
float btnCX = LOGICAL_W * 0.5f + contentOffsetX;
|
||||||
const float btnYOffset = 40.0f; // must match MenuState offset
|
const float btnYOffset = 40.0f; // must match MenuState offset
|
||||||
float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset;
|
float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset;
|
||||||
SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
|
float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f;
|
||||||
SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
|
std::array<SDL_FRect, 4> buttonRects{};
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
float center = btnCX + (static_cast<float>(i) - 1.5f) * spacing;
|
||||||
|
buttonRects[i] = SDL_FRect{center - btnW / 2.0f, btnCY - btnH / 2.0f, btnW, btnH};
|
||||||
|
}
|
||||||
|
|
||||||
if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h)
|
auto pointInRect = [&](const SDL_FRect& r) {
|
||||||
{
|
return lx >= r.x && lx <= r.x + r.w && ly >= r.y && ly <= r.y + r.h;
|
||||||
// Reset the game first with the chosen start level so HUD and
|
};
|
||||||
// Playing state see the correct 0-based level immediately.
|
|
||||||
|
if (pointInRect(buttonRects[0])) {
|
||||||
game.reset(startLevelSelection);
|
game.reset(startLevelSelection);
|
||||||
state = AppState::Playing;
|
state = AppState::Playing;
|
||||||
stateMgr.setState(state);
|
stateMgr.setState(state);
|
||||||
}
|
} else if (pointInRect(buttonRects[1])) {
|
||||||
else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h)
|
|
||||||
{
|
|
||||||
state = AppState::LevelSelector;
|
state = AppState::LevelSelector;
|
||||||
stateMgr.setState(state);
|
stateMgr.setState(state);
|
||||||
|
} else if (pointInRect(buttonRects[2])) {
|
||||||
|
state = AppState::Options;
|
||||||
|
stateMgr.setState(state);
|
||||||
|
} else if (pointInRect(buttonRects[3])) {
|
||||||
|
showExitConfirmPopup = true;
|
||||||
|
exitPopupSelectedButton = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings button (gear icon area - top right)
|
// Settings button (gear icon area - top right)
|
||||||
@ -865,6 +892,32 @@ int main(int, char **)
|
|||||||
game.setPaused(false);
|
game.setPaused(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (state == AppState::Menu && showExitConfirmPopup) {
|
||||||
|
float contentW = LOGICAL_W * logicalScale;
|
||||||
|
float contentH = LOGICAL_H * logicalScale;
|
||||||
|
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
|
||||||
|
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
|
||||||
|
float popupW = 420.0f;
|
||||||
|
float popupH = 230.0f;
|
||||||
|
float popupX = (LOGICAL_W - popupW) * 0.5f + contentOffsetX;
|
||||||
|
float popupY = (LOGICAL_H - popupH) * 0.5f + contentOffsetY;
|
||||||
|
float btnW = 140.0f;
|
||||||
|
float btnH = 50.0f;
|
||||||
|
float yesX = popupX + popupW * 0.3f - btnW / 2.0f;
|
||||||
|
float noX = popupX + popupW * 0.7f - btnW / 2.0f;
|
||||||
|
float btnY = popupY + popupH - btnH - 30.0f;
|
||||||
|
bool insidePopup = lx >= popupX && lx <= popupX + popupW && ly >= popupY && ly <= popupY + popupH;
|
||||||
|
if (insidePopup) {
|
||||||
|
if (lx >= yesX && lx <= yesX + btnW && ly >= btnY && ly <= btnY + btnH) {
|
||||||
|
showExitConfirmPopup = false;
|
||||||
|
running = false;
|
||||||
|
} else if (lx >= noX && lx <= noX + btnW && ly >= btnY && ly <= btnY + btnH) {
|
||||||
|
showExitConfirmPopup = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showExitConfirmPopup = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (e.type == SDL_EVENT_MOUSE_MOTION)
|
else if (e.type == SDL_EVENT_MOUSE_MOTION)
|
||||||
@ -886,15 +939,16 @@ int main(int, char **)
|
|||||||
float btnCX = LOGICAL_W * 0.5f + contentOffsetX;
|
float btnCX = LOGICAL_W * 0.5f + contentOffsetX;
|
||||||
const float btnYOffset = 40.0f; // must match MenuState offset
|
const float btnYOffset = 40.0f; // must match MenuState offset
|
||||||
float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset;
|
float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset;
|
||||||
SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
|
float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f;
|
||||||
SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
|
|
||||||
|
|
||||||
// Check menu button hovers (no level popup to handle anymore)
|
|
||||||
hoveredButton = -1;
|
hoveredButton = -1;
|
||||||
if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h)
|
for (int i = 0; i < 4; ++i) {
|
||||||
hoveredButton = 0;
|
float center = btnCX + (static_cast<float>(i) - 1.5f) * spacing;
|
||||||
else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h)
|
SDL_FRect rect{center - btnW / 2.0f, btnCY - btnH / 2.0f, btnW, btnH};
|
||||||
hoveredButton = 1;
|
if (lx >= rect.x && lx <= rect.x + rect.w && ly >= rect.y && ly <= rect.y + rect.h) {
|
||||||
|
hoveredButton = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@
|
|||||||
#include "states/State.h"
|
#include "states/State.h"
|
||||||
#include "states/LoadingState.h"
|
#include "states/LoadingState.h"
|
||||||
#include "states/MenuState.h"
|
#include "states/MenuState.h"
|
||||||
|
#include "states/OptionsState.h"
|
||||||
#include "states/LevelSelectorState.h"
|
#include "states/LevelSelectorState.h"
|
||||||
#include "states/PlayingState.h"
|
#include "states/PlayingState.h"
|
||||||
#include "audio/MenuWrappers.h"
|
#include "audio/MenuWrappers.h"
|
||||||
@ -279,6 +280,7 @@ static bool showExitConfirmPopup = false;
|
|||||||
static int exitPopupSelectedButton = 1; // 0 = YES, 1 = NO
|
static int exitPopupSelectedButton = 1; // 0 = YES, 1 = NO
|
||||||
static bool musicEnabled = true;
|
static bool musicEnabled = true;
|
||||||
static int hoveredButton = -1; // -1 = none, 0 = play, 1 = level, 2 = settings
|
static int hoveredButton = -1; // -1 = none, 0 = play, 1 = level, 2 = settings
|
||||||
|
static std::string playerName = "PLAYER";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Tetris Block Fireworks for intro animation (block particles)
|
// Tetris Block Fireworks for intro animation (block particles)
|
||||||
@ -646,10 +648,23 @@ int main(int, char **)
|
|||||||
ctx.showSettingsPopup = &showSettingsPopup;
|
ctx.showSettingsPopup = &showSettingsPopup;
|
||||||
ctx.showExitConfirmPopup = &showExitConfirmPopup;
|
ctx.showExitConfirmPopup = &showExitConfirmPopup;
|
||||||
ctx.exitPopupSelectedButton = &exitPopupSelectedButton;
|
ctx.exitPopupSelectedButton = &exitPopupSelectedButton;
|
||||||
|
ctx.playerName = &playerName;
|
||||||
|
ctx.fullscreenFlag = &isFullscreen;
|
||||||
|
ctx.applyFullscreen = [window, &isFullscreen](bool enable) {
|
||||||
|
SDL_SetWindowFullscreen(window, enable ? SDL_WINDOW_FULLSCREEN : 0);
|
||||||
|
isFullscreen = enable;
|
||||||
|
};
|
||||||
|
ctx.queryFullscreen = [window]() -> bool {
|
||||||
|
return (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0;
|
||||||
|
};
|
||||||
|
ctx.requestQuit = [&running]() {
|
||||||
|
running = false;
|
||||||
|
};
|
||||||
|
|
||||||
// Instantiate state objects
|
// Instantiate state objects
|
||||||
auto loadingState = std::make_unique<LoadingState>(ctx);
|
auto loadingState = std::make_unique<LoadingState>(ctx);
|
||||||
auto menuState = std::make_unique<MenuState>(ctx);
|
auto menuState = std::make_unique<MenuState>(ctx);
|
||||||
|
auto optionsState = std::make_unique<OptionsState>(ctx);
|
||||||
auto levelSelectorState = std::make_unique<LevelSelectorState>(ctx);
|
auto levelSelectorState = std::make_unique<LevelSelectorState>(ctx);
|
||||||
auto playingState = std::make_unique<PlayingState>(ctx);
|
auto playingState = std::make_unique<PlayingState>(ctx);
|
||||||
|
|
||||||
@ -662,6 +677,10 @@ int main(int, char **)
|
|||||||
stateMgr.registerOnEnter(AppState::Menu, [&](){ menuState->onEnter(); });
|
stateMgr.registerOnEnter(AppState::Menu, [&](){ menuState->onEnter(); });
|
||||||
stateMgr.registerOnExit(AppState::Menu, [&](){ menuState->onExit(); });
|
stateMgr.registerOnExit(AppState::Menu, [&](){ menuState->onExit(); });
|
||||||
|
|
||||||
|
stateMgr.registerHandler(AppState::Options, [&](const SDL_Event& e){ optionsState->handleEvent(e); });
|
||||||
|
stateMgr.registerOnEnter(AppState::Options, [&](){ optionsState->onEnter(); });
|
||||||
|
stateMgr.registerOnExit(AppState::Options, [&](){ optionsState->onExit(); });
|
||||||
|
|
||||||
stateMgr.registerHandler(AppState::LevelSelector, [&](const SDL_Event& e){ levelSelectorState->handleEvent(e); });
|
stateMgr.registerHandler(AppState::LevelSelector, [&](const SDL_Event& e){ levelSelectorState->handleEvent(e); });
|
||||||
stateMgr.registerOnEnter(AppState::LevelSelector, [&](){ levelSelectorState->onEnter(); });
|
stateMgr.registerOnEnter(AppState::LevelSelector, [&](){ levelSelectorState->onEnter(); });
|
||||||
stateMgr.registerOnExit(AppState::LevelSelector, [&](){ levelSelectorState->onExit(); });
|
stateMgr.registerOnExit(AppState::LevelSelector, [&](){ levelSelectorState->onExit(); });
|
||||||
@ -773,21 +792,30 @@ int main(int, char **)
|
|||||||
float btnCX = LOGICAL_W * 0.5f + contentOffsetX;
|
float btnCX = LOGICAL_W * 0.5f + contentOffsetX;
|
||||||
const float btnYOffset = 40.0f; // must match MenuState offset
|
const float btnYOffset = 40.0f; // must match MenuState offset
|
||||||
float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset;
|
float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset;
|
||||||
SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
|
float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f;
|
||||||
SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
|
std::array<SDL_FRect, 4> buttonRects{};
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
float center = btnCX + (static_cast<float>(i) - 1.5f) * spacing;
|
||||||
|
buttonRects[i] = SDL_FRect{center - btnW / 2.0f, btnCY - btnH / 2.0f, btnW, btnH};
|
||||||
|
}
|
||||||
|
|
||||||
if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h)
|
auto pointInRect = [&](const SDL_FRect& r) {
|
||||||
{
|
return lx >= r.x && lx <= r.x + r.w && ly >= r.y && ly <= r.y + r.h;
|
||||||
// Reset the game first with the chosen start level so HUD and
|
};
|
||||||
// Playing state see the correct 0-based level immediately.
|
|
||||||
|
if (pointInRect(buttonRects[0])) {
|
||||||
game.reset(startLevelSelection);
|
game.reset(startLevelSelection);
|
||||||
state = AppState::Playing;
|
state = AppState::Playing;
|
||||||
stateMgr.setState(state);
|
stateMgr.setState(state);
|
||||||
}
|
} else if (pointInRect(buttonRects[1])) {
|
||||||
else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h)
|
|
||||||
{
|
|
||||||
state = AppState::LevelSelector;
|
state = AppState::LevelSelector;
|
||||||
stateMgr.setState(state);
|
stateMgr.setState(state);
|
||||||
|
} else if (pointInRect(buttonRects[2])) {
|
||||||
|
state = AppState::Options;
|
||||||
|
stateMgr.setState(state);
|
||||||
|
} else if (pointInRect(buttonRects[3])) {
|
||||||
|
showExitConfirmPopup = true;
|
||||||
|
exitPopupSelectedButton = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings button (gear icon area - top right)
|
// Settings button (gear icon area - top right)
|
||||||
@ -846,6 +874,32 @@ int main(int, char **)
|
|||||||
game.setPaused(false);
|
game.setPaused(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (state == AppState::Menu && showExitConfirmPopup) {
|
||||||
|
float contentW = LOGICAL_W * logicalScale;
|
||||||
|
float contentH = LOGICAL_H * logicalScale;
|
||||||
|
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
|
||||||
|
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
|
||||||
|
float popupW = 420.0f;
|
||||||
|
float popupH = 230.0f;
|
||||||
|
float popupX = (LOGICAL_W - popupW) * 0.5f + contentOffsetX;
|
||||||
|
float popupY = (LOGICAL_H - popupH) * 0.5f + contentOffsetY;
|
||||||
|
float btnW = 140.0f;
|
||||||
|
float btnH = 50.0f;
|
||||||
|
float yesX = popupX + popupW * 0.3f - btnW / 2.0f;
|
||||||
|
float noX = popupX + popupW * 0.7f - btnW / 2.0f;
|
||||||
|
float btnY = popupY + popupH - btnH - 30.0f;
|
||||||
|
bool insidePopup = lx >= popupX && lx <= popupX + popupW && ly >= popupY && ly <= popupY + popupH;
|
||||||
|
if (insidePopup) {
|
||||||
|
if (lx >= yesX && lx <= yesX + btnW && ly >= btnY && ly <= btnY + btnH) {
|
||||||
|
showExitConfirmPopup = false;
|
||||||
|
running = false;
|
||||||
|
} else if (lx >= noX && lx <= noX + btnW && ly >= btnY && ly <= btnY + btnH) {
|
||||||
|
showExitConfirmPopup = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showExitConfirmPopup = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (e.type == SDL_EVENT_MOUSE_MOTION)
|
else if (e.type == SDL_EVENT_MOUSE_MOTION)
|
||||||
@ -867,15 +921,16 @@ int main(int, char **)
|
|||||||
float btnCX = LOGICAL_W * 0.5f + contentOffsetX;
|
float btnCX = LOGICAL_W * 0.5f + contentOffsetX;
|
||||||
const float btnYOffset = 40.0f; // must match MenuState offset
|
const float btnYOffset = 40.0f; // must match MenuState offset
|
||||||
float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset;
|
float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset;
|
||||||
SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
|
float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f;
|
||||||
SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
|
|
||||||
|
|
||||||
// Check menu button hovers (no level popup to handle anymore)
|
|
||||||
hoveredButton = -1;
|
hoveredButton = -1;
|
||||||
if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h)
|
for (int i = 0; i < 4; ++i) {
|
||||||
hoveredButton = 0;
|
float center = btnCX + (static_cast<float>(i) - 1.5f) * spacing;
|
||||||
else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h)
|
SDL_FRect rect{center - btnW / 2.0f, btnCY - btnH / 2.0f, btnW, btnH};
|
||||||
hoveredButton = 1;
|
if (lx >= rect.x && lx <= rect.x + rect.w && ly >= rect.y && ly <= rect.y + rect.h) {
|
||||||
|
hoveredButton = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
// Use dynamic logical dimensions from GlobalState instead of hardcoded values
|
// Use dynamic logical dimensions from GlobalState instead of hardcoded values
|
||||||
@ -22,38 +23,115 @@ MenuState::MenuState(StateContext& ctx) : State(ctx) {}
|
|||||||
|
|
||||||
void MenuState::onEnter() {
|
void MenuState::onEnter() {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "MenuState::onEnter called");
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "MenuState::onEnter called");
|
||||||
|
if (ctx.showExitConfirmPopup) {
|
||||||
|
*ctx.showExitConfirmPopup = false;
|
||||||
|
}
|
||||||
|
if (ctx.exitPopupSelectedButton) {
|
||||||
|
*ctx.exitPopupSelectedButton = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuState::onExit() {
|
void MenuState::onExit() {
|
||||||
|
if (ctx.showExitConfirmPopup) {
|
||||||
|
*ctx.showExitConfirmPopup = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuState::handleEvent(const SDL_Event& e) {
|
void MenuState::handleEvent(const SDL_Event& e) {
|
||||||
// Keyboard navigation for menu buttons
|
// Keyboard navigation for menu buttons
|
||||||
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
|
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
|
||||||
|
auto setExitSelection = [&](int value) {
|
||||||
|
if (ctx.exitPopupSelectedButton) {
|
||||||
|
*ctx.exitPopupSelectedButton = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
auto getExitSelection = [&]() -> int {
|
||||||
|
return ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1;
|
||||||
|
};
|
||||||
|
auto isExitPromptVisible = [&]() -> bool {
|
||||||
|
return ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup;
|
||||||
|
};
|
||||||
|
auto setExitPrompt = [&](bool visible) {
|
||||||
|
if (ctx.showExitConfirmPopup) {
|
||||||
|
*ctx.showExitConfirmPopup = visible;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isExitPromptVisible()) {
|
||||||
|
switch (e.key.scancode) {
|
||||||
|
case SDL_SCANCODE_LEFT:
|
||||||
|
case SDL_SCANCODE_UP:
|
||||||
|
setExitSelection(0);
|
||||||
|
return;
|
||||||
|
case SDL_SCANCODE_RIGHT:
|
||||||
|
case SDL_SCANCODE_DOWN:
|
||||||
|
setExitSelection(1);
|
||||||
|
return;
|
||||||
|
case SDL_SCANCODE_RETURN:
|
||||||
|
case SDL_SCANCODE_KP_ENTER:
|
||||||
|
case SDL_SCANCODE_SPACE:
|
||||||
|
if (getExitSelection() == 0) {
|
||||||
|
setExitPrompt(false);
|
||||||
|
if (ctx.requestQuit) {
|
||||||
|
ctx.requestQuit();
|
||||||
|
} else {
|
||||||
|
SDL_Event quit{};
|
||||||
|
quit.type = SDL_EVENT_QUIT;
|
||||||
|
SDL_PushEvent(&quit);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setExitPrompt(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case SDL_SCANCODE_ESCAPE:
|
||||||
|
setExitPrompt(false);
|
||||||
|
setExitSelection(1);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (e.key.scancode) {
|
switch (e.key.scancode) {
|
||||||
case SDL_SCANCODE_LEFT:
|
case SDL_SCANCODE_LEFT:
|
||||||
case SDL_SCANCODE_UP:
|
case SDL_SCANCODE_UP:
|
||||||
selectedButton = 0; // PLAY
|
{
|
||||||
|
const int total = 4;
|
||||||
|
selectedButton = (selectedButton + total - 1) % total;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case SDL_SCANCODE_RIGHT:
|
case SDL_SCANCODE_RIGHT:
|
||||||
case SDL_SCANCODE_DOWN:
|
case SDL_SCANCODE_DOWN:
|
||||||
selectedButton = 1; // LEVEL
|
{
|
||||||
|
const int total = 4;
|
||||||
|
selectedButton = (selectedButton + 1) % total;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case SDL_SCANCODE_RETURN:
|
case SDL_SCANCODE_RETURN:
|
||||||
case SDL_SCANCODE_KP_ENTER:
|
case SDL_SCANCODE_KP_ENTER:
|
||||||
case SDL_SCANCODE_SPACE:
|
case SDL_SCANCODE_SPACE:
|
||||||
// Activate selected button
|
if (!ctx.stateManager) {
|
||||||
if (selectedButton == 0) {
|
break;
|
||||||
// PLAY button - transition to Playing state
|
|
||||||
if (ctx.stateManager) {
|
|
||||||
ctx.stateManager->setState(AppState::Playing);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// LEVEL button - transition to LevelSelector state
|
|
||||||
if (ctx.stateManager) {
|
|
||||||
ctx.stateManager->setState(AppState::LevelSelector);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
switch (selectedButton) {
|
||||||
|
case 0:
|
||||||
|
ctx.stateManager->setState(AppState::Playing);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
ctx.stateManager->setState(AppState::LevelSelector);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
ctx.stateManager->setState(AppState::Options);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
setExitPrompt(true);
|
||||||
|
setExitSelection(1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_ESCAPE:
|
||||||
|
setExitPrompt(true);
|
||||||
|
setExitSelection(1);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@ -204,32 +282,88 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
|||||||
char levelBtnText[32];
|
char levelBtnText[32];
|
||||||
int startLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0;
|
int startLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0;
|
||||||
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
|
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
|
||||||
// Draw simple styled buttons (replicating menu_drawMenuButton)
|
|
||||||
auto drawMenuButtonLocal = [&](SDL_Renderer* r, FontAtlas& font, float cx, float cy, float w, float h, const std::string& label, SDL_Color bg, SDL_Color border, bool selected){
|
auto drawMenuButtonLocal = [&](SDL_Renderer* r, FontAtlas& font, float cx, float cy, float w, float h, const std::string& label, SDL_Color bg, SDL_Color border, bool selected){
|
||||||
float x = cx - w/2; float y = cy - h/2;
|
float x = cx - w/2; float y = cy - h/2;
|
||||||
|
|
||||||
// If selected, draw a glow effect
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
SDL_SetRenderDrawColor(r, 255, 220, 0, 100);
|
SDL_SetRenderDrawColor(r, 255, 220, 0, 110);
|
||||||
SDL_FRect glow{ x-10, y-10, w+20, h+20 };
|
SDL_FRect glow{ x-10, y-10, w+20, h+20 };
|
||||||
SDL_RenderFillRect(r, &glow);
|
SDL_RenderFillRect(r, &glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_SetRenderDrawColor(r, border.r, border.g, border.b, border.a);
|
SDL_SetRenderDrawColor(r, border.r, border.g, border.b, border.a);
|
||||||
SDL_FRect br{ x-6, y-6, w+12, h+12 }; SDL_RenderFillRect(r, &br);
|
SDL_FRect br{ x-6, y-6, w+12, h+12 }; SDL_RenderFillRect(r, &br);
|
||||||
SDL_SetRenderDrawColor(r, 255,255,255,255); SDL_FRect br2{ x-4, y-4, w+8, h+8 }; SDL_RenderFillRect(r, &br2);
|
SDL_SetRenderDrawColor(r, 255,255,255,255); SDL_FRect br2{ x-4, y-4, w+8, h+8 }; SDL_RenderFillRect(r, &br2);
|
||||||
SDL_SetRenderDrawColor(r, bg.r, bg.g, bg.b, bg.a); SDL_FRect br3{ x, y, w, h }; SDL_RenderFillRect(r, &br3);
|
SDL_SetRenderDrawColor(r, bg.r, bg.g, bg.b, bg.a); SDL_FRect br3{ x, y, w, h }; SDL_RenderFillRect(r, &br3);
|
||||||
float textScale = 1.6f; float approxCharW = 12.0f * textScale; float textW = label.length() * approxCharW; float tx = x + (w - textW) / 2.0f; float ty = y + (h - 20.0f * textScale) / 2.0f;
|
float textScale = 1.5f; float approxCharW = 12.0f * textScale; float textW = label.length() * approxCharW; float tx = x + (w - textW) / 2.0f; float ty = y + (h - 20.0f * textScale) / 2.0f;
|
||||||
font.draw(r, tx+2, ty+2, label, textScale, SDL_Color{0,0,0,180});
|
font.draw(r, tx+2, ty+2, label, textScale, SDL_Color{0,0,0,200});
|
||||||
font.draw(r, tx, ty, label, textScale, SDL_Color{255,255,255,255});
|
font.draw(r, tx, ty, label, textScale, SDL_Color{255,255,255,255});
|
||||||
};
|
};
|
||||||
drawMenuButtonLocal(renderer, *ctx.pixelFont, btnX - btnW * 0.6f, btnY, btnW, btnH, std::string("PLAY"), SDL_Color{60,180,80,255}, SDL_Color{30,120,40,255}, selectedButton == 0);
|
|
||||||
{
|
struct MenuButtonDef {
|
||||||
|
SDL_Color bg;
|
||||||
|
SDL_Color border;
|
||||||
|
std::string label;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::array<MenuButtonDef, 4> buttons = {
|
||||||
|
MenuButtonDef{ SDL_Color{60,180,80,255}, SDL_Color{30,120,40,255}, "PLAY" },
|
||||||
|
MenuButtonDef{ SDL_Color{40,140,240,255}, SDL_Color{20,100,200,255}, levelBtnText },
|
||||||
|
MenuButtonDef{ SDL_Color{130,80,210,255}, SDL_Color{90,40,170,255}, "OPTIONS" },
|
||||||
|
MenuButtonDef{ SDL_Color{200,70,70,255}, SDL_Color{150,40,40,255}, "EXIT" }
|
||||||
|
};
|
||||||
|
|
||||||
|
float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f;
|
||||||
|
for (size_t i = 0; i < buttons.size(); ++i) {
|
||||||
|
float offset = (static_cast<float>(i) - 1.5f) * spacing;
|
||||||
|
float cx = btnX + offset;
|
||||||
|
drawMenuButtonLocal(renderer, *ctx.pixelFont, cx, btnY, btnW, btnH, buttons[i].label, buttons[i].bg, buttons[i].border, selectedButton == static_cast<int>(i));
|
||||||
}
|
}
|
||||||
drawMenuButtonLocal(renderer, *ctx.pixelFont, btnX + btnW * 0.6f, btnY, btnW, btnH, std::string(levelBtnText), SDL_Color{40,140,240,255}, SDL_Color{20,100,200,255}, selectedButton == 1);
|
}
|
||||||
{
|
|
||||||
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render after draw LEVEL button\n"); fclose(f); }
|
if (ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup) {
|
||||||
|
int selection = ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1;
|
||||||
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||||
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 150);
|
||||||
|
SDL_FRect overlay{contentOffsetX, contentOffsetY, LOGICAL_W, LOGICAL_H};
|
||||||
|
SDL_RenderFillRect(renderer, &overlay);
|
||||||
|
|
||||||
|
float popupW = 420.0f;
|
||||||
|
float popupH = 230.0f;
|
||||||
|
float popupX = (LOGICAL_W - popupW) * 0.5f + contentOffsetX;
|
||||||
|
float popupY = (LOGICAL_H - popupH) * 0.5f + contentOffsetY;
|
||||||
|
|
||||||
|
SDL_SetRenderDrawColor(renderer, 20, 30, 50, 240);
|
||||||
|
SDL_FRect popup{popupX, popupY, popupW, popupH};
|
||||||
|
SDL_RenderFillRect(renderer, &popup);
|
||||||
|
SDL_SetRenderDrawColor(renderer, 90, 140, 220, 255);
|
||||||
|
SDL_RenderRect(renderer, &popup);
|
||||||
|
|
||||||
|
FontAtlas* titleFont = ctx.font ? ctx.font : ctx.pixelFont;
|
||||||
|
if (titleFont) {
|
||||||
|
titleFont->draw(renderer, popupX + 40.0f, popupY + 30.0f, "EXIT GAME?", 1.8f, SDL_Color{255, 230, 140, 255});
|
||||||
|
titleFont->draw(renderer, popupX + 40.0f, popupY + 80.0f, "Are you sure you want to quit?", 1.1f, SDL_Color{200, 210, 230, 255});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto drawChoice = [&](const char* label, float cx, int idx) {
|
||||||
|
float btnW2 = 140.0f;
|
||||||
|
float btnH2 = 50.0f;
|
||||||
|
float x = cx - btnW2 / 2.0f;
|
||||||
|
float y = popupY + popupH - btnH2 - 30.0f;
|
||||||
|
bool selected = (selection == idx);
|
||||||
|
SDL_Color bg = selected ? SDL_Color{220, 180, 60, 255} : SDL_Color{80, 110, 160, 255};
|
||||||
|
SDL_Color border = selected ? SDL_Color{255, 220, 120, 255} : SDL_Color{40, 60, 100, 255};
|
||||||
|
SDL_SetRenderDrawColor(renderer, border.r, border.g, border.b, border.a);
|
||||||
|
SDL_FRect br{ x-4, y-4, btnW2+8, btnH2+8 };
|
||||||
|
SDL_RenderFillRect(renderer, &br);
|
||||||
|
SDL_SetRenderDrawColor(renderer, bg.r, bg.g, bg.b, bg.a);
|
||||||
|
SDL_FRect body{ x, y, btnW2, btnH2 };
|
||||||
|
SDL_RenderFillRect(renderer, &body);
|
||||||
|
if (titleFont) {
|
||||||
|
titleFont->draw(renderer, x + 20.0f, y + 10.0f, label, 1.2f, SDL_Color{15, 20, 35, 255});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
drawChoice("YES", popupX + popupW * 0.3f, 0);
|
||||||
|
drawChoice("NO", popupX + popupW * 0.7f, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Popups (settings only - level popup is now a separate state)
|
// Popups (settings only - level popup is now a separate state)
|
||||||
|
|||||||
@ -12,5 +12,5 @@ public:
|
|||||||
void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) override;
|
void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int selectedButton = 0; // 0 = PLAY, 1 = LEVEL
|
int selectedButton = 0; // 0 = PLAY, 1 = LEVEL, 2 = OPTIONS, 3 = EXIT
|
||||||
};
|
};
|
||||||
|
|||||||
238
src/states/OptionsState.cpp
Normal file
238
src/states/OptionsState.cpp
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
#include "OptionsState.h"
|
||||||
|
#include "../core/state/StateManager.h"
|
||||||
|
#include "../graphics/ui/Font.h"
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
OptionsState::OptionsState(StateContext& ctx) : State(ctx) {}
|
||||||
|
|
||||||
|
void OptionsState::onEnter() {
|
||||||
|
m_selectedField = Field::PlayerName;
|
||||||
|
m_cursorTimer = 0.0;
|
||||||
|
m_cursorVisible = true;
|
||||||
|
if (SDL_Window* focusWin = SDL_GetKeyboardFocus()) {
|
||||||
|
SDL_StartTextInput(focusWin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionsState::onExit() {
|
||||||
|
if (SDL_Window* focusWin = SDL_GetKeyboardFocus()) {
|
||||||
|
SDL_StopTextInput(focusWin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionsState::handleEvent(const SDL_Event& e) {
|
||||||
|
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
|
||||||
|
switch (e.key.scancode) {
|
||||||
|
case SDL_SCANCODE_ESCAPE:
|
||||||
|
exitToMenu();
|
||||||
|
return;
|
||||||
|
case SDL_SCANCODE_UP:
|
||||||
|
case SDL_SCANCODE_W:
|
||||||
|
moveSelection(-1);
|
||||||
|
return;
|
||||||
|
case SDL_SCANCODE_DOWN:
|
||||||
|
case SDL_SCANCODE_S:
|
||||||
|
moveSelection(1);
|
||||||
|
return;
|
||||||
|
case SDL_SCANCODE_RETURN:
|
||||||
|
case SDL_SCANCODE_KP_ENTER:
|
||||||
|
case SDL_SCANCODE_SPACE:
|
||||||
|
activateSelection();
|
||||||
|
return;
|
||||||
|
case SDL_SCANCODE_LEFT:
|
||||||
|
case SDL_SCANCODE_RIGHT:
|
||||||
|
if (m_selectedField == Field::Fullscreen) {
|
||||||
|
toggleFullscreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_selectedField == Field::PlayerName) {
|
||||||
|
handleNameInput(e);
|
||||||
|
}
|
||||||
|
} else if (e.type == SDL_EVENT_TEXT_INPUT && m_selectedField == Field::PlayerName) {
|
||||||
|
handleNameInput(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionsState::update(double frameMs) {
|
||||||
|
m_cursorTimer += frameMs;
|
||||||
|
if (m_cursorTimer >= 450.0) {
|
||||||
|
m_cursorTimer = 0.0;
|
||||||
|
m_cursorVisible = !m_cursorVisible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionsState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {
|
||||||
|
if (!renderer) return;
|
||||||
|
|
||||||
|
const float LOGICAL_W = 1200.0f;
|
||||||
|
const float LOGICAL_H = 1000.0f;
|
||||||
|
|
||||||
|
float winW = static_cast<float>(logicalVP.w);
|
||||||
|
float winH = static_cast<float>(logicalVP.h);
|
||||||
|
float contentW = LOGICAL_W * logicalScale;
|
||||||
|
float contentH = LOGICAL_H * logicalScale;
|
||||||
|
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
|
||||||
|
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
|
||||||
|
|
||||||
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||||
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 140);
|
||||||
|
SDL_FRect dim{contentOffsetX, contentOffsetY, LOGICAL_W, LOGICAL_H};
|
||||||
|
SDL_RenderFillRect(renderer, &dim);
|
||||||
|
|
||||||
|
const float panelW = 560.0f;
|
||||||
|
const float panelH = 420.0f;
|
||||||
|
SDL_FRect panel{
|
||||||
|
(LOGICAL_W - panelW) * 0.5f + contentOffsetX,
|
||||||
|
(LOGICAL_H - panelH) * 0.5f + contentOffsetY,
|
||||||
|
panelW,
|
||||||
|
panelH
|
||||||
|
};
|
||||||
|
|
||||||
|
SDL_SetRenderDrawColor(renderer, 15, 20, 34, 230);
|
||||||
|
SDL_RenderFillRect(renderer, &panel);
|
||||||
|
SDL_SetRenderDrawColor(renderer, 70, 110, 190, 255);
|
||||||
|
SDL_RenderRect(renderer, &panel);
|
||||||
|
|
||||||
|
FontAtlas* titleFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
|
||||||
|
FontAtlas* bodyFont = ctx.font ? ctx.font : ctx.pixelFont;
|
||||||
|
|
||||||
|
auto drawText = [&](FontAtlas* font, float x, float y, const std::string& text, float scale, SDL_Color color) {
|
||||||
|
if (!font) return;
|
||||||
|
font->draw(renderer, x, y, text, scale, color);
|
||||||
|
};
|
||||||
|
|
||||||
|
drawText(titleFont, panel.x + 24.0f, panel.y + 24.0f, "OPTIONS", 2.0f, {255, 230, 120, 255});
|
||||||
|
|
||||||
|
auto drawField = [&](Field field, float y, const std::string& label, const std::string& value) {
|
||||||
|
bool selected = (field == m_selectedField);
|
||||||
|
SDL_FRect row{panel.x + 20.0f, y - 10.0f, panel.w - 40.0f, 70.0f};
|
||||||
|
SDL_SetRenderDrawColor(renderer, selected ? 40 : 24, selected ? 80 : 36, selected ? 120 : 48, 220);
|
||||||
|
SDL_RenderFillRect(renderer, &row);
|
||||||
|
SDL_SetRenderDrawColor(renderer, 80, 120, 200, 255);
|
||||||
|
SDL_RenderRect(renderer, &row);
|
||||||
|
|
||||||
|
drawText(bodyFont, row.x + 18.0f, row.y + 12.0f, label, 1.4f, {200, 220, 255, 255});
|
||||||
|
drawText(bodyFont, row.x + 18.0f, row.y + 36.0f, value, 1.6f, {255, 255, 255, 255});
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string nameDisplay = playerName();
|
||||||
|
if (nameDisplay.empty()) {
|
||||||
|
nameDisplay = "<ENTER NAME>";
|
||||||
|
}
|
||||||
|
if (m_selectedField == Field::PlayerName && m_cursorVisible) {
|
||||||
|
nameDisplay.push_back('_');
|
||||||
|
}
|
||||||
|
|
||||||
|
drawField(Field::PlayerName, panel.y + 90.0f, "PLAYER NAME", nameDisplay);
|
||||||
|
|
||||||
|
std::string fullscreenValue = isFullscreen() ? "ON" : "OFF";
|
||||||
|
drawField(Field::Fullscreen, panel.y + 180.0f, "FULLSCREEN", fullscreenValue);
|
||||||
|
|
||||||
|
drawField(Field::Back, panel.y + 270.0f, "BACK", "RETURN TO MENU");
|
||||||
|
|
||||||
|
drawText(bodyFont, panel.x + 24.0f, panel.y + panel.h - 50.0f,
|
||||||
|
"ARROWS = NAV ENTER = SELECT ESC = MENU", 1.1f, {190, 200, 215, 255});
|
||||||
|
drawText(bodyFont, panel.x + 24.0f, panel.y + panel.h - 26.0f,
|
||||||
|
"LETTERS/NUMBERS TYPE INTO NAME FIELD", 1.0f, {150, 160, 180, 255});
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionsState::moveSelection(int delta) {
|
||||||
|
int idx = static_cast<int>(m_selectedField);
|
||||||
|
int total = 3;
|
||||||
|
idx = (idx + delta + total) % total;
|
||||||
|
m_selectedField = static_cast<Field>(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionsState::activateSelection() {
|
||||||
|
switch (m_selectedField) {
|
||||||
|
case Field::PlayerName:
|
||||||
|
// Nothing to do; typing is always enabled
|
||||||
|
break;
|
||||||
|
case Field::Fullscreen:
|
||||||
|
toggleFullscreen();
|
||||||
|
break;
|
||||||
|
case Field::Back:
|
||||||
|
exitToMenu();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionsState::handleNameInput(const SDL_Event& e) {
|
||||||
|
if (!ctx.playerName) return;
|
||||||
|
|
||||||
|
if (e.type == SDL_EVENT_KEY_DOWN) {
|
||||||
|
if (e.key.scancode == SDL_SCANCODE_BACKSPACE) {
|
||||||
|
removeCharacter();
|
||||||
|
} else if (e.key.scancode == SDL_SCANCODE_SPACE) {
|
||||||
|
addCharacter(' ');
|
||||||
|
} else {
|
||||||
|
SDL_Keymod mods = SDL_GetModState();
|
||||||
|
SDL_Keycode keycode = SDL_GetKeyFromScancode(e.key.scancode, mods, true);
|
||||||
|
bool shift = (mods & SDL_KMOD_SHIFT) != 0;
|
||||||
|
char c = static_cast<char>(keycode);
|
||||||
|
if (keycode >= 'a' && keycode <= 'z') {
|
||||||
|
c = shift ? static_cast<char>(std::toupper(c)) : static_cast<char>(std::toupper(c));
|
||||||
|
addCharacter(c);
|
||||||
|
} else if (keycode >= '0' && keycode <= '9') {
|
||||||
|
addCharacter(static_cast<char>(keycode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (e.type == SDL_EVENT_TEXT_INPUT) {
|
||||||
|
const char* text = e.text.text;
|
||||||
|
while (*text) {
|
||||||
|
unsigned char c = static_cast<unsigned char>(*text);
|
||||||
|
if (std::isalnum(c) || c == ' ') {
|
||||||
|
addCharacter(static_cast<char>(std::toupper(c)));
|
||||||
|
}
|
||||||
|
++text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionsState::addCharacter(char c) {
|
||||||
|
if (!ctx.playerName) return;
|
||||||
|
if (c == '\0') return;
|
||||||
|
if (c == ' ' && ctx.playerName->empty()) return;
|
||||||
|
if (ctx.playerName->size() >= MAX_NAME_LENGTH) return;
|
||||||
|
|
||||||
|
ctx.playerName->push_back(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionsState::removeCharacter() {
|
||||||
|
if (!ctx.playerName || ctx.playerName->empty()) return;
|
||||||
|
ctx.playerName->pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionsState::toggleFullscreen() {
|
||||||
|
bool nextState = !isFullscreen();
|
||||||
|
if (ctx.applyFullscreen) {
|
||||||
|
ctx.applyFullscreen(nextState);
|
||||||
|
}
|
||||||
|
if (ctx.fullscreenFlag) {
|
||||||
|
*ctx.fullscreenFlag = nextState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionsState::exitToMenu() {
|
||||||
|
if (ctx.stateManager) {
|
||||||
|
ctx.stateManager->setState(AppState::Menu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& OptionsState::playerName() const {
|
||||||
|
static std::string empty;
|
||||||
|
return ctx.playerName ? *ctx.playerName : empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OptionsState::isFullscreen() const {
|
||||||
|
if (ctx.queryFullscreen) {
|
||||||
|
return ctx.queryFullscreen();
|
||||||
|
}
|
||||||
|
return ctx.fullscreenFlag ? *ctx.fullscreenFlag : false;
|
||||||
|
}
|
||||||
35
src/states/OptionsState.h
Normal file
35
src/states/OptionsState.h
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "State.h"
|
||||||
|
|
||||||
|
class OptionsState : public State {
|
||||||
|
public:
|
||||||
|
explicit OptionsState(StateContext& ctx);
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void handleEvent(const SDL_Event& e) override;
|
||||||
|
void update(double frameMs) override;
|
||||||
|
void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class Field : int {
|
||||||
|
PlayerName = 0,
|
||||||
|
Fullscreen = 1,
|
||||||
|
Back = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr int MAX_NAME_LENGTH = 12;
|
||||||
|
Field m_selectedField = Field::PlayerName;
|
||||||
|
double m_cursorTimer = 0.0;
|
||||||
|
bool m_cursorVisible = true;
|
||||||
|
|
||||||
|
void moveSelection(int delta);
|
||||||
|
void activateSelection();
|
||||||
|
void handleNameInput(const SDL_Event& e);
|
||||||
|
void addCharacter(char c);
|
||||||
|
void removeCharacter();
|
||||||
|
void toggleFullscreen();
|
||||||
|
void exitToMenu();
|
||||||
|
const std::string& playerName() const;
|
||||||
|
bool isFullscreen() const;
|
||||||
|
};
|
||||||
@ -3,6 +3,8 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
// Forward declarations for frequently used types
|
// Forward declarations for frequently used types
|
||||||
class Game;
|
class Game;
|
||||||
@ -48,6 +50,11 @@ struct StateContext {
|
|||||||
bool* showSettingsPopup = nullptr;
|
bool* showSettingsPopup = nullptr;
|
||||||
bool* showExitConfirmPopup = nullptr; // If true, show "Exit game?" confirmation while playing
|
bool* showExitConfirmPopup = nullptr; // If true, show "Exit game?" confirmation while playing
|
||||||
int* exitPopupSelectedButton = nullptr; // 0 = YES, 1 = NO (default)
|
int* exitPopupSelectedButton = nullptr; // 0 = YES, 1 = NO (default)
|
||||||
|
std::string* playerName = nullptr; // Shared player name buffer for highscores/options
|
||||||
|
bool* fullscreenFlag = nullptr; // Tracks current fullscreen state when available
|
||||||
|
std::function<void(bool)> applyFullscreen; // Allows states to request fullscreen changes
|
||||||
|
std::function<bool()> queryFullscreen; // Optional callback if fullscreenFlag is not reliable
|
||||||
|
std::function<void()> requestQuit; // Allows menu/option states to close the app gracefully
|
||||||
// Pointer to the application's StateManager so states can request transitions
|
// Pointer to the application's StateManager so states can request transitions
|
||||||
StateManager* stateManager = nullptr;
|
StateManager* stateManager = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user