diff --git a/src/graphics/GameRenderer.cpp b/src/graphics/GameRenderer.cpp index a661ee4..f37a617 100644 --- a/src/graphics/GameRenderer.cpp +++ b/src/graphics/GameRenderer.cpp @@ -3,6 +3,7 @@ #include "../graphics/Font.h" #include "../gameplay/LineEffect.h" #include +#include #include #include @@ -421,60 +422,88 @@ void GameRenderer::renderPlayingState( pixelFont->draw(renderer, logicalW * 0.5f - 120, logicalH * 0.5f + 30, "Press P to resume", 0.8f, {200, 200, 220, 255}); } - // Exit confirmation popup if (showExitConfirmPopup) { - float popupW = 420.0f, popupH = 180.0f; - float popupX = (logicalW - popupW) * 0.5f; - float popupY = (logicalH - popupH) * 0.5f; - - // Dim entire window (do not change viewport/scales here) + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 200); SDL_FRect fullWin{0.f, 0.f, winW, winH}; SDL_RenderFillRect(renderer, &fullWin); - // Draw popup box in logical coords with content offsets - drawRectWithOffset(popupX - 4.0f, popupY - 4.0f, popupW + 8.0f, popupH + 8.0f, {60, 70, 90, 255}); - drawRectWithOffset(popupX, popupY, popupW, popupH, {20, 22, 28, 240}); + const float panelW = 640.0f; + const float panelH = 320.0f; + SDL_FRect panel{ + (logicalW - panelW) * 0.5f + contentOffsetX, + (logicalH - panelH) * 0.5f + contentOffsetY, + panelW, + panelH + }; - // Text content (measure to perfectly center) - const std::string title = "Exit game?"; - const std::string line1 = "Are you sure you want to"; - const std::string line2 = "leave the current game?"; + SDL_FRect shadow{panel.x + 6.0f, panel.y + 10.0f, panel.w, panel.h}; + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 140); + SDL_RenderFillRect(renderer, &shadow); + for (int i = 0; i < 5; ++i) { + SDL_FRect glow{panel.x - float(i * 2), panel.y - float(i * 2), panel.w + float(i * 4), panel.h + float(i * 4)}; + SDL_SetRenderDrawColor(renderer, 0, 180, 255, Uint8(44 - i * 7)); + SDL_RenderRect(renderer, &glow); + } - int wTitle=0,hTitle=0; pixelFont->measure(title, 1.6f, wTitle, hTitle); - int wL1=0,hL1=0; pixelFont->measure(line1, 0.9f, wL1, hL1); - int wL2=0,hL2=0; pixelFont->measure(line2, 0.9f, wL2, hL2); + SDL_SetRenderDrawColor(renderer, 18, 30, 52, 255); + SDL_RenderFillRect(renderer, &panel); + SDL_SetRenderDrawColor(renderer, 70, 120, 210, 255); + SDL_RenderRect(renderer, &panel); - float titleX = popupX + (popupW - (float)wTitle) * 0.5f + contentOffsetX; - float l1X = popupX + (popupW - (float)wL1) * 0.5f + contentOffsetX; - float l2X = popupX + (popupW - (float)wL2) * 0.5f + contentOffsetX; + SDL_FRect inner{panel.x + 24.0f, panel.y + 98.0f, panel.w - 48.0f, panel.h - 146.0f}; + SDL_SetRenderDrawColor(renderer, 16, 24, 40, 235); + SDL_RenderFillRect(renderer, &inner); + SDL_SetRenderDrawColor(renderer, 40, 80, 140, 235); + SDL_RenderRect(renderer, &inner); - pixelFont->draw(renderer, titleX, popupY + contentOffsetY + 20.0f, title, 1.6f, {255, 220, 0, 255}); - pixelFont->draw(renderer, l1X, popupY + contentOffsetY + 60.0f, line1, 0.9f, {220, 220, 230, 255}); - pixelFont->draw(renderer, l2X, popupY + contentOffsetY + 84.0f, line2, 0.9f, {220, 220, 230, 255}); + const std::string title = "EXIT GAME?"; + int titleW = 0, titleH = 0; + const float titleScale = 1.8f; + pixelFont->measure(title, titleScale, titleW, titleH); + pixelFont->draw(renderer, panel.x + (panel.w - titleW) * 0.5f, panel.y + 30.0f, title, titleScale, {255, 230, 140, 255}); - // Buttons - float btnW = 140.0f, btnH = 46.0f; - float yesX = popupX + popupW * 0.25f - btnW * 0.5f; - float noX = popupX + popupW * 0.75f - btnW * 0.5f; - float btnY = popupY + popupH - 60.0f; + std::array lines = { + "Are you sure you want to quit?", + "Current progress will be lost." + }; + float lineY = inner.y + 22.0f; + const float lineScale = 1.05f; + for (const auto& line : lines) { + int lineW = 0, lineH = 0; + pixelFont->measure(line, lineScale, lineW, lineH); + float textX = panel.x + (panel.w - lineW) * 0.5f; + pixelFont->draw(renderer, textX, lineY, line, lineScale, SDL_Color{210, 220, 240, 255}); + lineY += lineH + 10.0f; + } - // YES button - drawRectWithOffset(yesX - 2.0f, btnY - 2.0f, btnW + 4.0f, btnH + 4.0f, {100, 120, 140, 255}); - drawRectWithOffset(yesX, btnY, btnW, btnH, {200, 60, 60, 255}); - const std::string yes = "YES"; - int wYes=0,hYes=0; pixelFont->measure(yes, 1.0f, wYes, hYes); - pixelFont->draw(renderer, yesX + (btnW - (float)wYes) * 0.5f + contentOffsetX, - btnY + (btnH - (float)hYes) * 0.5f + contentOffsetY, - yes, 1.0f, {255, 255, 255, 255}); + const float horizontalPad = 28.0f; + const float buttonGap = 32.0f; + const float buttonH = 66.0f; + float buttonW = (inner.w - horizontalPad * 2.0f - buttonGap) * 0.5f; + float buttonY = inner.y + inner.h - buttonH - 24.0f; - // NO button - drawRectWithOffset(noX - 2.0f, btnY - 2.0f, btnW + 4.0f, btnH + 4.0f, {100, 120, 140, 255}); - drawRectWithOffset(noX, btnY, btnW, btnH, {80, 140, 80, 255}); - const std::string no = "NO"; - int wNo=0,hNo=0; pixelFont->measure(no, 1.0f, wNo, hNo); - pixelFont->draw(renderer, noX + (btnW - (float)wNo) * 0.5f + contentOffsetX, - btnY + (btnH - (float)hNo) * 0.5f + contentOffsetY, - no, 1.0f, {255, 255, 255, 255}); + auto drawButton = [&](float x, const char* label, SDL_Color base) { + SDL_FRect btn{x, buttonY, buttonW, buttonH}; + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 120); + SDL_FRect btnShadow{btn.x + 4.0f, btn.y + 6.0f, btn.w, btn.h}; + SDL_RenderFillRect(renderer, &btnShadow); + SDL_SetRenderDrawColor(renderer, base.r, base.g, base.b, base.a); + SDL_RenderFillRect(renderer, &btn); + SDL_SetRenderDrawColor(renderer, 90, 130, 200, 255); + SDL_RenderRect(renderer, &btn); + + int textW = 0, textH = 0; + const float labelScale = 1.4f; + pixelFont->measure(label, labelScale, textW, textH); + float textX = btn.x + (btn.w - textW) * 0.5f; + float textY = btn.y + (btn.h - textH) * 0.5f; + pixelFont->draw(renderer, textX, textY, label, labelScale, SDL_Color{255, 255, 255, 255}); + }; + + float yesX = inner.x + horizontalPad; + float noX = yesX + buttonW + buttonGap; + drawButton(yesX, "YES", SDL_Color{185, 70, 70, 255}); + drawButton(noX, "NO", SDL_Color{60, 95, 150, 255}); } } diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index 137c88c..6abc259 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -3,6 +3,7 @@ #include "../ui/Font.h" #include "../../gameplay/effects/LineEffect.h" #include +#include #include #include @@ -422,68 +423,96 @@ void GameRenderer::renderPlayingState( pixelFont->draw(renderer, logicalW * 0.5f - 120, logicalH * 0.5f + 30, "Press P to resume", 0.8f, {200, 200, 220, 255}); } - // Exit confirmation popup + // Exit confirmation popup styled like other retro panels if (showExitConfirmPopup) { - float popupW = 420.0f, popupH = 180.0f; - float popupX = (logicalW - popupW) * 0.5f; - float popupY = (logicalH - popupH) * 0.5f; - - // Dim entire window (do not change viewport/scales here) + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 200); SDL_FRect fullWin{0.f, 0.f, winW, winH}; SDL_RenderFillRect(renderer, &fullWin); - // Draw popup box in logical coords with content offsets - drawRectWithOffset(popupX - 4.0f, popupY - 4.0f, popupW + 8.0f, popupH + 8.0f, {60, 70, 90, 255}); - drawRectWithOffset(popupX, popupY, popupW, popupH, {20, 22, 28, 240}); + const float panelW = 640.0f; + const float panelH = 320.0f; + SDL_FRect panel{ + (logicalW - panelW) * 0.5f + contentOffsetX, + (logicalH - panelH) * 0.5f + contentOffsetY, + panelW, + panelH + }; - // Text content (measure to perfectly center) - const std::string title = "Exit game?"; - const std::string line1 = "Are you sure you want to"; - const std::string line2 = "leave the current game?"; + SDL_FRect shadow{panel.x + 6.0f, panel.y + 10.0f, panel.w, panel.h}; + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 140); + SDL_RenderFillRect(renderer, &shadow); - int wTitle=0,hTitle=0; pixelFont->measure(title, 1.6f, wTitle, hTitle); - int wL1=0,hL1=0; pixelFont->measure(line1, 0.9f, wL1, hL1); - int wL2=0,hL2=0; pixelFont->measure(line2, 0.9f, wL2, hL2); - - float titleX = popupX + (popupW - (float)wTitle) * 0.5f + contentOffsetX; - float l1X = popupX + (popupW - (float)wL1) * 0.5f + contentOffsetX; - float l2X = popupX + (popupW - (float)wL2) * 0.5f + contentOffsetX; - - pixelFont->draw(renderer, titleX, popupY + contentOffsetY + 20.0f, title, 1.6f, {255, 220, 0, 255}); - pixelFont->draw(renderer, l1X, popupY + contentOffsetY + 60.0f, line1, 0.9f, {220, 220, 230, 255}); - pixelFont->draw(renderer, l2X, popupY + contentOffsetY + 84.0f, line2, 0.9f, {220, 220, 230, 255}); - - // Buttons - float btnW = 140.0f, btnH = 46.0f; - float yesX = popupX + popupW * 0.25f - btnW * 0.5f; - float noX = popupX + popupW * 0.75f - btnW * 0.5f; - float btnY = popupY + popupH - 60.0f; - - // YES button - if (exitPopupSelectedButton == 0) { - // Draw glow for selected YES button - drawRectWithOffset(yesX - 6.0f, btnY - 6.0f, btnW + 12.0f, btnH + 12.0f, {255, 220, 0, 100}); + for (int i = 0; i < 5; ++i) { + SDL_FRect glow{panel.x - float(i * 2), panel.y - float(i * 2), panel.w + float(i * 4), panel.h + float(i * 4)}; + SDL_SetRenderDrawColor(renderer, 0, 180, 255, Uint8(44 - i * 7)); + SDL_RenderRect(renderer, &glow); } - drawRectWithOffset(yesX - 2.0f, btnY - 2.0f, btnW + 4.0f, btnH + 4.0f, {100, 120, 140, 255}); - drawRectWithOffset(yesX, btnY, btnW, btnH, {200, 60, 60, 255}); - const std::string yes = "YES"; - int wYes=0,hYes=0; pixelFont->measure(yes, 1.0f, wYes, hYes); - pixelFont->draw(renderer, yesX + (btnW - (float)wYes) * 0.5f + contentOffsetX, - btnY + (btnH - (float)hYes) * 0.5f + contentOffsetY, - yes, 1.0f, {255, 255, 255, 255}); - // NO button - if (exitPopupSelectedButton == 1) { - // Draw glow for selected NO button - drawRectWithOffset(noX - 6.0f, btnY - 6.0f, btnW + 12.0f, btnH + 12.0f, {255, 220, 0, 100}); + SDL_SetRenderDrawColor(renderer, 18, 30, 52, 255); + SDL_RenderFillRect(renderer, &panel); + SDL_SetRenderDrawColor(renderer, 70, 120, 210, 255); + SDL_RenderRect(renderer, &panel); + + SDL_FRect inner{panel.x + 24.0f, panel.y + 98.0f, panel.w - 48.0f, panel.h - 146.0f}; + SDL_SetRenderDrawColor(renderer, 16, 24, 40, 235); + SDL_RenderFillRect(renderer, &inner); + SDL_SetRenderDrawColor(renderer, 40, 80, 140, 235); + SDL_RenderRect(renderer, &inner); + + const std::string title = "EXIT GAME?"; + int titleW = 0, titleH = 0; + const float titleScale = 1.8f; + pixelFont->measure(title, titleScale, titleW, titleH); + pixelFont->draw(renderer, panel.x + (panel.w - titleW) * 0.5f, panel.y + 30.0f, title, titleScale, {255, 230, 140, 255}); + + std::array lines = { + "Are you sure you want to quit?", + "Current progress will be lost." + }; + float lineY = inner.y + 22.0f; + const float lineScale = 1.05f; + for (const auto& line : lines) { + int lineW = 0, lineH = 0; + pixelFont->measure(line, lineScale, lineW, lineH); + float textX = panel.x + (panel.w - lineW) * 0.5f; + pixelFont->draw(renderer, textX, lineY, line, lineScale, SDL_Color{210, 220, 240, 255}); + lineY += lineH + 10.0f; } - drawRectWithOffset(noX - 2.0f, btnY - 2.0f, btnW + 4.0f, btnH + 4.0f, {100, 120, 140, 255}); - drawRectWithOffset(noX, btnY, btnW, btnH, {80, 140, 80, 255}); - const std::string no = "NO"; - int wNo=0,hNo=0; pixelFont->measure(no, 1.0f, wNo, hNo); - pixelFont->draw(renderer, noX + (btnW - (float)wNo) * 0.5f + contentOffsetX, - btnY + (btnH - (float)hNo) * 0.5f + contentOffsetY, - no, 1.0f, {255, 255, 255, 255}); + + const float horizontalPad = 28.0f; + const float buttonGap = 32.0f; + const float buttonH = 66.0f; + float buttonW = (inner.w - horizontalPad * 2.0f - buttonGap) * 0.5f; + float buttonY = inner.y + inner.h - buttonH - 24.0f; + + auto drawButton = [&](int idx, float x, const char* label) { + bool selected = (exitPopupSelectedButton == idx); + SDL_Color base = (idx == 0) ? SDL_Color{185, 70, 70, 255} : SDL_Color{60, 95, 150, 255}; + SDL_Color body = selected ? SDL_Color{Uint8(std::min(255, base.r + 35)), Uint8(std::min(255, base.g + 35)), Uint8(std::min(255, base.b + 35)), 255} : base; + SDL_Color border = selected ? SDL_Color{255, 220, 120, 255} : SDL_Color{80, 110, 160, 255}; + + SDL_FRect btn{x, buttonY, buttonW, buttonH}; + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 120); + SDL_FRect btnShadow{btn.x + 4.0f, btn.y + 6.0f, btn.w, btn.h}; + SDL_RenderFillRect(renderer, &btnShadow); + SDL_SetRenderDrawColor(renderer, body.r, body.g, body.b, body.a); + SDL_RenderFillRect(renderer, &btn); + SDL_SetRenderDrawColor(renderer, border.r, border.g, border.b, border.a); + SDL_RenderRect(renderer, &btn); + + int textW = 0, textH = 0; + const float labelScale = 1.4f; + pixelFont->measure(label, labelScale, textW, textH); + float textX = btn.x + (btn.w - textW) * 0.5f; + float textY = btn.y + (btn.h - textH) * 0.5f; + SDL_Color textColor = selected ? SDL_Color{255, 255, 255, 255} : SDL_Color{230, 235, 250, 255}; + pixelFont->draw(renderer, textX, textY, label, labelScale, textColor); + }; + + float yesX = inner.x + horizontalPad; + float noX = yesX + buttonW + buttonGap; + drawButton(0, yesX, "YES"); + drawButton(1, noX, "NO"); } } diff --git a/src/main.cpp b/src/main.cpp index 5561d37..f773d13 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1167,6 +1167,9 @@ int main(int, char **) case AppState::Menu: menuState->update(frameMs); break; + case AppState::Options: + optionsState->update(frameMs); + break; case AppState::LevelSelector: levelSelectorState->update(frameMs); break; @@ -1240,7 +1243,7 @@ int main(int, char **) } else if (state == AppState::Loading) { // Use 3D starfield for loading screen (full screen) starfield3D.draw(renderer); - } else if (state == AppState::Menu || state == AppState::LevelSelector) { + } else if (state == AppState::Menu || state == AppState::LevelSelector || state == AppState::Options) { // Use static background for menu, stretched to window; no starfield on sides if (backgroundTex) { SDL_FRect fullRect = { 0, 0, (float)winW, (float)winH }; @@ -1350,6 +1353,9 @@ int main(int, char **) // Delegate full menu rendering to MenuState object now menuState->render(renderer, logicalScale, logicalVP); break; + case AppState::Options: + optionsState->render(renderer, logicalScale, logicalVP); + break; case AppState::LevelSelector: // Delegate level selector rendering to LevelSelectorState levelSelectorState->render(renderer, logicalScale, logicalVP); diff --git a/src/main_dist.cpp b/src/main_dist.cpp index 4771989..7cfebfc 100644 --- a/src/main_dist.cpp +++ b/src/main_dist.cpp @@ -1181,7 +1181,7 @@ int main(int, char **) } else if (state == AppState::Loading) { // Use 3D starfield for loading screen (full screen) starfield3D.draw(renderer); - } else if (state == AppState::Menu || state == AppState::LevelSelector) { + } else if (state == AppState::Menu || state == AppState::LevelSelector || state == AppState::Options) { // Use static background for menu, stretched to window; no starfield on sides if (backgroundTex) { SDL_FRect fullRect = { 0, 0, (float)winW, (float)winH }; diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index 6f55c26..457ce10 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -268,8 +268,9 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi // Draw bottom action buttons with responsive sizing (reduced to match main mouse hit-test) // Use the contentW calculated at the top with content offsets bool isSmall = (contentW < 700.0f); - float btnW = isSmall ? (LOGICAL_W * 0.4f) : 300.0f; - float btnH = isSmall ? 60.0f : 70.0f; + float btnW = isSmall ? (LOGICAL_W * 0.32f) : (LOGICAL_W * 0.18f); + btnW = std::clamp(btnW, 180.0f, 260.0f); // keep buttons from consuming entire row + float btnH = isSmall ? 56.0f : 64.0f; float btnX = LOGICAL_W * 0.5f + contentOffsetX; // Move buttons down by 40px to match original layout (user requested 30-50px) const float btnYOffset = 40.0f; @@ -294,9 +295,13 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi 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, bg.r, bg.g, bg.b, bg.a); SDL_FRect br3{ x, y, w, h }; SDL_RenderFillRect(r, &br3); - 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,200}); - font.draw(r, tx, ty, label, textScale, SDL_Color{255,255,255,255}); + float textScale = 1.5f; + int textW = 0, textH = 0; + font.measure(label, textScale, textW, textH); + float tx = x + (w - static_cast(textW)) * 0.5f; + float ty = y + (h - static_cast(textH)) * 0.5f; + font.draw(r, tx + 2.0f, ty + 2.0f, label, textScale, SDL_Color{0, 0, 0, 200}); + font.draw(r, tx, ty, label, textScale, SDL_Color{255, 255, 255, 255}); }; struct MenuButtonDef { @@ -312,7 +317,7 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi MenuButtonDef{ SDL_Color{200,70,70,255}, SDL_Color{150,40,40,255}, "EXIT" } }; - float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f; + float spacing = isSmall ? btnW * 1.2f : btnW * 1.15f; for (size_t i = 0; i < buttons.size(); ++i) { float offset = (static_cast(i) - 1.5f) * spacing; float cx = btnX + offset; @@ -327,43 +332,89 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi 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; + const float panelW = 640.0f; + const float panelH = 320.0f; + SDL_FRect panel{ + (LOGICAL_W - panelW) * 0.5f + contentOffsetX, + (LOGICAL_H - panelH) * 0.5f + contentOffsetY, + panelW, + panelH + }; - 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}); + SDL_FRect shadow{panel.x + 6.0f, panel.y + 10.0f, panel.w, panel.h}; + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 140); + SDL_RenderFillRect(renderer, &shadow); + for (int i = 0; i < 5; ++i) { + SDL_FRect glow{panel.x - float(i * 2), panel.y - float(i * 2), panel.w + float(i * 4), panel.h + float(i * 4)}; + SDL_SetRenderDrawColor(renderer, 0, 180, 255, Uint8(44 - i * 7)); + SDL_RenderRect(renderer, &glow); } - 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; + SDL_SetRenderDrawColor(renderer, 18, 30, 52, 255); + SDL_RenderFillRect(renderer, &panel); + SDL_SetRenderDrawColor(renderer, 70, 120, 210, 255); + SDL_RenderRect(renderer, &panel); + + SDL_FRect inner{panel.x + 24.0f, panel.y + 98.0f, panel.w - 48.0f, panel.h - 146.0f}; + SDL_SetRenderDrawColor(renderer, 16, 24, 40, 235); + SDL_RenderFillRect(renderer, &inner); + SDL_SetRenderDrawColor(renderer, 40, 80, 140, 235); + SDL_RenderRect(renderer, &inner); + + FontAtlas* retroFont = ctx.pixelFont ? ctx.pixelFont : ctx.font; + if (retroFont) { + const float titleScale = 1.9f; + const char* title = "EXIT GAME?"; + int titleW = 0, titleH = 0; + retroFont->measure(title, titleScale, titleW, titleH); + float titleX = panel.x + (panel.w - static_cast(titleW)) * 0.5f; + retroFont->draw(renderer, titleX, panel.y + 30.0f, title, titleScale, SDL_Color{255, 230, 140, 255}); + + const float bodyScale = 1.05f; + const char* line = "Are you sure you want to quit?"; + int bodyW = 0, bodyH = 0; + retroFont->measure(line, bodyScale, bodyW, bodyH); + float bodyX = panel.x + (panel.w - static_cast(bodyW)) * 0.5f; + retroFont->draw(renderer, bodyX, inner.y + 18.0f, line, bodyScale, SDL_Color{210, 220, 240, 255}); + } + + const float horizontalPad = 28.0f; + const float buttonGap = 32.0f; + const float buttonH = 66.0f; + float buttonW = (inner.w - horizontalPad * 2.0f - buttonGap) * 0.5f; + float buttonY = inner.y + inner.h - buttonH - 24.0f; + + auto drawChoice = [&](int idx, float x, const char* label) { 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_Color base = (idx == 0) ? SDL_Color{185, 70, 70, 255} : SDL_Color{60, 95, 150, 255}; + SDL_Color body = selected ? SDL_Color{Uint8(std::min(255, base.r + 35)), Uint8(std::min(255, base.g + 35)), Uint8(std::min(255, base.b + 35)), 255} : base; + SDL_Color border = selected ? SDL_Color{255, 220, 120, 255} : SDL_Color{80, 110, 160, 255}; + + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 120); + SDL_FRect shadowRect{x + 4.0f, buttonY + 6.0f, buttonW, buttonH}; + SDL_RenderFillRect(renderer, &shadowRect); + + SDL_FRect bodyRect{x, buttonY, buttonW, buttonH}; + SDL_SetRenderDrawColor(renderer, body.r, body.g, body.b, body.a); + SDL_RenderFillRect(renderer, &bodyRect); 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}); + SDL_RenderRect(renderer, &bodyRect); + + if (retroFont) { + const float labelScale = 1.4f; + int textW = 0, textH = 0; + retroFont->measure(label, labelScale, textW, textH); + float textX = bodyRect.x + (bodyRect.w - static_cast(textW)) * 0.5f; + float textY = bodyRect.y + (bodyRect.h - static_cast(textH)) * 0.5f; + SDL_Color textColor = selected ? SDL_Color{255, 255, 255, 255} : SDL_Color{230, 235, 250, 255}; + retroFont->draw(renderer, textX, textY, label, labelScale, textColor); } }; - drawChoice("YES", popupX + popupW * 0.3f, 0); - drawChoice("NO", popupX + popupW * 0.7f, 1); + + float yesX = inner.x + horizontalPad; + float noX = yesX + buttonW + buttonGap; + drawChoice(0, yesX, "YES"); + drawChoice(1, noX, "NO"); } // Popups (settings only - level popup is now a separate state) diff --git a/src/states/OptionsState.cpp b/src/states/OptionsState.cpp index 67de3c7..0b4a657 100644 --- a/src/states/OptionsState.cpp +++ b/src/states/OptionsState.cpp @@ -1,13 +1,16 @@ #include "OptionsState.h" #include "../core/state/StateManager.h" #include "../graphics/ui/Font.h" +#include "../audio/Audio.h" +#include "../audio/SoundEffect.h" #include +#include #include OptionsState::OptionsState(StateContext& ctx) : State(ctx) {} void OptionsState::onEnter() { - m_selectedField = Field::PlayerName; + m_selectedField = Field::Fullscreen; m_cursorTimer = 0.0; m_cursorVisible = true; if (SDL_Window* focusWin = SDL_GetKeyboardFocus()) { @@ -40,22 +43,28 @@ void OptionsState::handleEvent(const SDL_Event& e) { case SDL_SCANCODE_SPACE: activateSelection(); return; + case SDL_SCANCODE_F: + toggleFullscreen(); + return; case SDL_SCANCODE_LEFT: case SDL_SCANCODE_RIGHT: if (m_selectedField == Field::Fullscreen) { toggleFullscreen(); return; } + if (m_selectedField == Field::Music) { + toggleMusic(); + return; + } + if (m_selectedField == Field::SoundFx) { + toggleSoundFx(); + 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); } } @@ -80,12 +89,29 @@ void OptionsState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l 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); + SDL_Texture* logoTexture = ctx.logoSmallTex ? ctx.logoSmallTex : ctx.logoTex; + if (logoTexture) { + float texW = 0.0f; + float texH = 0.0f; + if (logoTexture == ctx.logoSmallTex && ctx.logoSmallW > 0 && ctx.logoSmallH > 0) { + texW = static_cast(ctx.logoSmallW); + texH = static_cast(ctx.logoSmallH); + } else { + SDL_GetTextureSize(logoTexture, &texW, &texH); + } + if (texW > 0.0f && texH > 0.0f) { + float maxWidth = LOGICAL_W * 0.6f; + float scale = std::min(1.0f, maxWidth / texW); + float dw = texW * scale; + float dh = texH * scale; + float logoX = (LOGICAL_W - dw) * 0.5f + contentOffsetX; + float logoY = LOGICAL_H * 0.05f + contentOffsetY; + SDL_FRect dst{logoX, logoY, dw, dh}; + SDL_RenderTexture(renderer, logoTexture, nullptr, &dst); + } + } - const float panelW = 560.0f; + const float panelW = 520.0f; const float panelH = 420.0f; SDL_FRect panel{ (LOGICAL_W - panelW) * 0.5f + contentOffsetX, @@ -94,121 +120,111 @@ void OptionsState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l panelH }; - SDL_SetRenderDrawColor(renderer, 15, 20, 34, 230); + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + + // Panel styling similar to level selector + SDL_FRect shadow{panel.x + 6.0f, panel.y + 10.0f, panel.w, panel.h}; + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 120); + SDL_RenderFillRect(renderer, &shadow); + + for (int i = 0; i < 5; ++i) { + SDL_FRect glow{panel.x - float(i * 2), panel.y - float(i * 2), panel.w + float(i * 4), panel.h + float(i * 4)}; + SDL_SetRenderDrawColor(renderer, 0, 180, 255, Uint8(42 - i * 8)); + SDL_RenderRect(renderer, &glow); + } + + SDL_SetRenderDrawColor(renderer, 18, 30, 52, 255); SDL_RenderFillRect(renderer, &panel); - SDL_SetRenderDrawColor(renderer, 70, 110, 190, 255); + SDL_SetRenderDrawColor(renderer, 70, 120, 210, 255); SDL_RenderRect(renderer, &panel); - FontAtlas* titleFont = ctx.pixelFont ? ctx.pixelFont : ctx.font; - FontAtlas* bodyFont = ctx.font ? ctx.font : ctx.pixelFont; + FontAtlas* retroFont = ctx.pixelFont ? ctx.pixelFont : ctx.font; - 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); - }; + if (!logoTexture && retroFont) { + retroFont->draw(renderer, panel.x + 24.0f, panel.y + 24.0f, "OPTIONS", 2.0f, {255, 230, 120, 255}); + } - drawText(titleFont, panel.x + 24.0f, panel.y + 24.0f, "OPTIONS", 2.0f, {255, 230, 120, 255}); + SDL_FRect inner{panel.x + 20.0f, panel.y + 80.0f, panel.w - 40.0f, panel.h - 120.0f}; + SDL_SetRenderDrawColor(renderer, 16, 24, 40, 235); + SDL_RenderFillRect(renderer, &inner); + SDL_SetRenderDrawColor(renderer, 40, 80, 140, 240); + SDL_RenderRect(renderer, &inner); + + constexpr int rowCount = 4; + const float rowHeight = 60.0f; + const float spacing = std::max(0.0f, (inner.h - rowHeight * rowCount) / (rowCount + 1)); 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_FRect row{inner.x + 12.0f, y, inner.w - 24.0f, rowHeight}; + + SDL_SetRenderDrawColor(renderer, selected ? 55 : 28, selected ? 90 : 40, selected ? 140 : 60, 235); SDL_RenderFillRect(renderer, &row); - SDL_SetRenderDrawColor(renderer, 80, 120, 200, 255); + SDL_SetRenderDrawColor(renderer, selected ? 180 : 90, selected ? 210 : 120, 255, 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}); + if (retroFont) { + SDL_Color labelColor = selected ? SDL_Color{255, 220, 120, 255} : SDL_Color{210, 200, 190, 255}; + SDL_Color valueColor = selected ? SDL_Color{255, 255, 255, 255} : SDL_Color{215, 230, 250, 255}; + + if (!label.empty()) { + float labelScale = 1.2f; + int labelW = 0; + int labelH = 0; + retroFont->measure(label, labelScale, labelW, labelH); + float labelY = row.y + (row.h - static_cast(labelH)) * 0.5f; + retroFont->draw(renderer, row.x + 16.0f, labelY, label, labelScale, labelColor); + } + + int valueW = 0, valueH = 0; + float valueScale = (field == Field::Back) ? 1.3f : 1.5f; + retroFont->measure(value, valueScale, valueW, valueH); + bool rightAlign = (field == Field::Fullscreen || field == Field::Music || field == Field::SoundFx); + float valX = rightAlign + ? (row.x + row.w - static_cast(valueW) - 16.0f) + : (row.x + (row.w - static_cast(valueW)) * 0.5f); + float valY = row.y + (row.h - valueH) * 0.5f; + retroFont->draw(renderer, valX, valY, value, valueScale, valueColor); + } }; - std::string nameDisplay = playerName(); - if (nameDisplay.empty()) { - nameDisplay = ""; - } - if (m_selectedField == Field::PlayerName && m_cursorVisible) { - nameDisplay.push_back('_'); - } + float rowY = inner.y + spacing; - drawField(Field::PlayerName, panel.y + 90.0f, "PLAYER NAME", nameDisplay); + drawField(Field::Fullscreen, rowY, "FULLSCREEN", isFullscreen() ? "ON" : "OFF"); + rowY += rowHeight + spacing; + drawField(Field::Music, rowY, "MUSIC", isMusicEnabled() ? "ON" : "OFF"); + rowY += rowHeight + spacing; + drawField(Field::SoundFx, rowY, "SOUND FX", isSoundFxEnabled() ? "ON" : "OFF"); + rowY += rowHeight + spacing; + drawField(Field::Back, rowY, "", "RETURN TO MENU"); - 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)retroFont; // footer removed for cleaner layout } void OptionsState::moveSelection(int delta) { int idx = static_cast(m_selectedField); - int total = 3; + int total = static_cast(Field::Back) + 1; idx = (idx + delta + total) % total; m_selectedField = static_cast(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::Music: + toggleMusic(); + break; + case Field::SoundFx: + toggleSoundFx(); + 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(keycode); - if (keycode >= 'a' && keycode <= 'z') { - c = shift ? static_cast(std::toupper(c)) : static_cast(std::toupper(c)); - addCharacter(c); - } else if (keycode >= '0' && keycode <= '9') { - addCharacter(static_cast(keycode)); - } - } - } else if (e.type == SDL_EVENT_TEXT_INPUT) { - const char* text = e.text.text; - while (*text) { - unsigned char c = static_cast(*text); - if (std::isalnum(c) || c == ' ') { - addCharacter(static_cast(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) { @@ -219,20 +235,38 @@ void OptionsState::toggleFullscreen() { } } +void OptionsState::toggleMusic() { + Audio::instance().toggleMute(); + if (ctx.musicEnabled) { + *ctx.musicEnabled = !*ctx.musicEnabled; + } +} + +void OptionsState::toggleSoundFx() { + bool next = !SoundEffectManager::instance().isEnabled(); + SoundEffectManager::instance().setEnabled(next); +} + 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; } + +bool OptionsState::isMusicEnabled() const { + if (ctx.musicEnabled) { + return *ctx.musicEnabled; + } + return true; +} + +bool OptionsState::isSoundFxEnabled() const { + return SoundEffectManager::instance().isEnabled(); +} diff --git a/src/states/OptionsState.h b/src/states/OptionsState.h index f6285b4..58885e3 100644 --- a/src/states/OptionsState.h +++ b/src/states/OptionsState.h @@ -13,23 +13,24 @@ public: private: enum class Field : int { - PlayerName = 0, - Fullscreen = 1, - Back = 2 + Fullscreen = 0, + Music = 1, + SoundFx = 2, + Back = 3 }; static constexpr int MAX_NAME_LENGTH = 12; - Field m_selectedField = Field::PlayerName; + Field m_selectedField = Field::Fullscreen; 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 toggleMusic(); + void toggleSoundFx(); void exitToMenu(); - const std::string& playerName() const; bool isFullscreen() const; + bool isMusicEnabled() const; + bool isSoundFxEnabled() const; };