#include "OptionsState.h" #include "../core/state/StateManager.h" #include "../graphics/ui/Font.h" #include "../audio/Audio.h" #include "../audio/AudioManager.h" #include "../audio/SoundEffect.h" #include #include #include #include "../core/Settings.h" #include "../graphics/renderers/UIRenderer.h" OptionsState::OptionsState(StateContext& ctx) : State(ctx) {} void OptionsState::onEnter() { m_selectedField = Field::Fullscreen; 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_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; } if (m_selectedField == Field::SmoothScroll) { toggleSmoothScroll(); return; } break; default: break; } } } 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(logicalVP.w); float winH = static_cast(logicalVP.h); float contentOffsetX = 0.0f; float contentOffsetY = 0.0f; UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W, LOGICAL_H, logicalScale, contentOffsetX, contentOffsetY); SDL_Texture* logoTexture = ctx.logoSmallTex ? ctx.logoSmallTex : ctx.logoTex; int logoW = 0, logoH = 0; if (logoTexture == ctx.logoSmallTex && ctx.logoSmallW > 0) { logoW = ctx.logoSmallW; logoH = ctx.logoSmallH; } UIRenderer::drawLogo(renderer, logoTexture, LOGICAL_W, LOGICAL_H, contentOffsetX, contentOffsetY, logoW, logoH); const float panelW = 520.0f; const float panelH = 420.0f; SDL_FRect panel{ (LOGICAL_W - panelW) * 0.5f + contentOffsetX, (LOGICAL_H - panelH) * 0.5f + contentOffsetY, panelW, panelH }; UIRenderer::drawSciFiPanel(renderer, panel); // For options/settings we prefer the secondary (Exo2) font for longer descriptions. FontAtlas* retroFont = ctx.font ? ctx.font : ctx.pixelFont; if (!logoTexture && retroFont) { retroFont->draw(renderer, 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 = 5; 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{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, selected ? 180 : 90, selected ? 210 : 120, 255, 255); SDL_RenderRect(renderer, &row); 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 || field == Field::SmoothScroll); 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); } }; float rowY = inner.y + spacing; 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::SmoothScroll, rowY, "SMOOTH SCROLL", isSmoothScrollEnabled() ? "ON" : "OFF"); rowY += rowHeight + spacing; drawField(Field::Back, rowY, "", "RETURN TO MENU"); (void)retroFont; // footer removed for cleaner layout } void OptionsState::moveSelection(int delta) { int idx = static_cast(m_selectedField); 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::Fullscreen: toggleFullscreen(); break; case Field::Music: toggleMusic(); break; case Field::SoundFx: toggleSoundFx(); break; case Field::SmoothScroll: toggleSmoothScroll(); break; case Field::Back: exitToMenu(); break; } } void OptionsState::toggleFullscreen() { bool nextState = !isFullscreen(); if (ctx.applyFullscreen) { ctx.applyFullscreen(nextState); } if (ctx.fullscreenFlag) { *ctx.fullscreenFlag = nextState; } // Save setting Settings::instance().setFullscreen(nextState); Settings::instance().save(); } void OptionsState::toggleMusic() { if (auto sys = AudioManager::get()) sys->toggleMute(); // If muted, music is disabled. If not muted, music is enabled. // Note: Audio::instance().isMuted() returns true if muted. // But Audio class doesn't expose isMuted directly in header usually? // Let's assume toggleMute toggles internal state. // We can track it via ctx.musicEnabled if it's synced. bool enabled = true; if (ctx.musicEnabled) { *ctx.musicEnabled = !*ctx.musicEnabled; enabled = *ctx.musicEnabled; } // Save setting Settings::instance().setMusicEnabled(enabled); Settings::instance().save(); } void OptionsState::toggleSoundFx() { bool next = !SoundEffectManager::instance().isEnabled(); SoundEffectManager::instance().setEnabled(next); // Save setting Settings::instance().setSoundEnabled(next); Settings::instance().save(); } void OptionsState::toggleSmoothScroll() { bool next = !Settings::instance().isSmoothScrollEnabled(); Settings::instance().setSmoothScrollEnabled(next); Settings::instance().save(); } void OptionsState::exitToMenu() { // Try a graceful fade transition if available, but always ensure we // return to the Menu state so the UI is responsive to the user's action. if (ctx.requestFadeTransition) { ctx.requestFadeTransition(AppState::Menu); } if (ctx.stateManager) { ctx.stateManager->setState(AppState::Menu); } } 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(); } bool OptionsState::isSmoothScrollEnabled() const { return Settings::instance().isSmoothScrollEnabled(); }