Retro exit modal styling and shortcuts

This commit is contained in:
2025-11-22 19:56:07 +01:00
parent aed1cb62e7
commit c0bee9296a
7 changed files with 389 additions and 239 deletions

View File

@ -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 <SDL3/SDL.h>
#include <algorithm>
#include <cctype>
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<float>(ctx.logoSmallW);
texH = static_cast<float>(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<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);
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);
}
};
std::string nameDisplay = playerName();
if (nameDisplay.empty()) {
nameDisplay = "<ENTER NAME>";
}
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<int>(m_selectedField);
int total = 3;
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::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<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) {
@ -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();
}