Files
spacetris/src/states/OptionsState.cpp
2025-12-25 19:41:19 +01:00

288 lines
9.6 KiB
C++

#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 <SDL3/SDL.h>
#include <algorithm>
#include <cctype>
#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<float>(logicalVP.w);
float winH = static_cast<float>(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<float>(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<float>(valueW) - 16.0f)
: (row.x + (row.w - static_cast<float>(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<int>(m_selectedField);
int total = static_cast<int>(Field::Back) + 1;
idx = (idx + delta + total) % total;
m_selectedField = static_cast<Field>(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();
}