// --- Includes --- #include "service_menu.h" #include "MenuOption.h" #include "screen.h" #include #include #include "text.h" #include "resource.h" #include "section.h" #include "audio.h" #include // --- Singleton --- ServiceMenu *ServiceMenu::instance_ = nullptr; // Inicializa la instancia singleton void ServiceMenu::init() { ServiceMenu::instance_ = new ServiceMenu(); } // Libera la instancia singleton void ServiceMenu::destroy() { delete ServiceMenu::instance_; } // Devuelve la instancia singleton ServiceMenu *ServiceMenu::get() { return ServiceMenu::instance_; } // --- Constructor y setup --- // Constructor: inicializa textos, mensaje UI y estado inicial ServiceMenu::ServiceMenu() : element_text_(Resource::get()->getText("04b_25_flat")), title_text_(Resource::get()->getText("04b_25_flat_2x")), current_settings_group_(SettingsGroup::MAIN), previous_settings_group_(current_settings_group_) { restart_message_ui_ = std::make_unique( element_text_, Lang::getText("[SERVICE_MENU] NEED_RESTART_MESSAGE"), title_color_); reset(); } // Alterna la visibilidad del menú de servicio. // Si se desactiva, reinicia el menú al estado inicial. void ServiceMenu::toggle() { enabled_ = !enabled_; if (!enabled_) { reset(); } } // Inicializa todas las opciones del menú void ServiceMenu::initializeOptions() { options_.clear(); // --- Video --- options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] FULLSCREEN"), SettingsGroup::VIDEO, &Options::video.fullscreen)); options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] WINDOW_SIZE"), SettingsGroup::VIDEO, &Options::window.size, 1, Options::window.max_size, 1)); options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] SHADERS"), SettingsGroup::VIDEO, &Options::video.shaders)); options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] VSYNC"), SettingsGroup::VIDEO, &Options::video.v_sync)); options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] INTEGER_SCALE"), SettingsGroup::VIDEO, &Options::video.integer_scale)); // --- Audio --- options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] AUDIO"), SettingsGroup::AUDIO, &Options::audio.enabled)); options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] MAIN_VOLUME"), SettingsGroup::AUDIO, &Options::audio.volume, 0, 100, 5)); options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] MUSIC_VOLUME"), SettingsGroup::AUDIO, &Options::audio.music.volume, 0, 100, 5)); options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] SFX_VOLUME"), SettingsGroup::AUDIO, &Options::audio.sound.volume, 0, 100, 5)); // --- Settings --- options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] AUTOFIRE"), SettingsGroup::SETTINGS, &Options::settings.autofire)); options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] LANGUAGE"), SettingsGroup::SETTINGS, std::vector{Lang::getText("[SERVICE_MENU] LANG_ES"), Lang::getText("[SERVICE_MENU] LANG_BA"), Lang::getText("[SERVICE_MENU] LANG_EN")}, []() { return Lang::getNameFromCode(Options::pending_changes.new_language); }, [](const std::string &val) { Options::pending_changes.new_language = Lang::getCodeFromName(val); Options::checkPendingChanges(); })); options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] DIFFICULTY"), SettingsGroup::SETTINGS, std::vector{Lang::getText("[SERVICE_MENU] EASY"), Lang::getText("[SERVICE_MENU] NORMAL"), Lang::getText("[SERVICE_MENU] HARD")}, []() { return Options::getDifficultyNameFromCode(Options::pending_changes.new_difficulty); }, [](const std::string &val) { Options::pending_changes.new_difficulty = Options::getDifficultyCodeFromName(val); Options::checkPendingChanges(); })); options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] ENABLE_SHUTDOWN"), SettingsGroup::SETTINGS, &Options::settings.shutdown_enabled)); // --- System --- options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] RESET"), SettingsGroup::SYSTEM, [this]() { Section::name = Section::Name::RESET; toggle(); })); options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] QUIT"), SettingsGroup::SYSTEM, []() { Section::name = Section::Name::QUIT; Section::options = Section::Options::NONE; })); options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] SHUTDOWN"), SettingsGroup::SYSTEM, []() { Section::name = Section::Name::QUIT; Section::options = Section::Options::SHUTDOWN; }, !Options::settings.shutdown_enabled)); // --- Main Menu --- options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] VIDEO"), SettingsGroup::MAIN, SettingsGroup::VIDEO)); options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] AUDIO"), SettingsGroup::MAIN, SettingsGroup::AUDIO)); options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] SETTINGS"), SettingsGroup::MAIN, SettingsGroup::SETTINGS)); options_.push_back(std::make_unique(Lang::getText("[SERVICE_MENU] SYSTEM"), SettingsGroup::MAIN, SettingsGroup::SYSTEM)); precalculateMenuWidths(); } // Reinicia el menú al estado inicial void ServiceMenu::reset() { selected_ = 0; previous_settings_group_ = current_settings_group_ = SettingsGroup::MAIN; title_ = settingsGroupToString(current_settings_group_); initializeOptions(); updateMenu(current_settings_group_); setAnchors(); } // --- Métodos de navegación y selección --- // Sube el selector una posición en la lista de opciones void ServiceMenu::setSelectorUp() { if (display_options_.empty()) return; selected_ = (selected_ > 0) ? selected_ - 1 : display_options_.size() - 1; playMenuSound(); } // Baja el selector una posición en la lista de opciones void ServiceMenu::setSelectorDown() { if (display_options_.empty()) return; selected_ = (selected_ + 1) % display_options_.size(); playMenuSound(); } // Ajusta el valor de la opción seleccionada si es ajustable void ServiceMenu::adjustOption(bool adjust_up) { if (display_options_.empty() || selected_ >= display_options_.size()) return; auto &selected_option = display_options_.at(selected_); if (selected_option->getBehavior() == MenuOption::Behavior::ADJUST) { selected_option->adjustValue(adjust_up); option_pairs_ = getOptionPairs(current_settings_group_); applySettings(current_settings_group_); playMenuSound(); } } // Ejecuta la acción de la opción seleccionada o navega a un submenú void ServiceMenu::selectOption() { if (display_options_.empty() || selected_ >= display_options_.size()) return; // Guarda la selección en el menú principal if (current_settings_group_ == SettingsGroup::MAIN) main_menu_selected_ = selected_; auto &selected_option = display_options_.at(selected_); // Si es una carpeta, navega al subgrupo correspondiente if (auto folder = dynamic_cast(selected_option)) { previous_settings_group_ = current_settings_group_; current_settings_group_ = folder->getTargetGroup(); title_ = settingsGroupToString(current_settings_group_); updateMenu(current_settings_group_); selected_ = 0; setOptionsPosition(); playMenuSound(); return; } // Si es una acción, ejecútala if (selected_option->getBehavior() == MenuOption::Behavior::SELECT) { selected_option->executeAction(); } } // Vuelve al grupo de opciones anterior o cierra el menú si está en el principal void ServiceMenu::moveBack() { if (current_settings_group_ == SettingsGroup::MAIN) { enabled_ = false; return; } else { if (previous_settings_group_ == SettingsGroup::MAIN) selected_ = main_menu_selected_; else selected_ = 0; current_settings_group_ = previous_settings_group_; updateMenu(current_settings_group_); setOptionsPosition(); playMenuSound(); } } // --- Métodos de renderizado --- // Dibuja el menú de servicio y el mensaje de reinicio si corresponde void ServiceMenu::render() { if (!enabled_) return; // --- Fondo y marco del menú --- if (aspect_ == Aspect::ASPECT1) { SDL_FRect shadowRect = {rect_.x + 5, rect_.y + 5, rect_.w, rect_.h}; SDL_SetRenderDrawColor(Screen::get()->getRenderer(), 0, 0, 0, 64); SDL_RenderFillRect(Screen::get()->getRenderer(), &shadowRect); } const Uint8 ALPHA = aspect_ == Aspect::ASPECT1 ? 255 : 255; SDL_SetRenderDrawColor(Screen::get()->getRenderer(), bg_color_.r, bg_color_.g, bg_color_.b, ALPHA); SDL_RenderFillRect(Screen::get()->getRenderer(), &rect_); SDL_SetRenderDrawColor(Screen::get()->getRenderer(), title_color_.r, title_color_.g, title_color_.b, 255); SDL_RenderRect(Screen::get()->getRenderer(), &rect_); // --- Mensaje de reinicio (animado) --- restart_message_ui_->render(); // --- Título del menú --- int y = rect_.y + title_padding_; title_text_->writeDX(TEXT_COLOR | TEXT_CENTER, param.game.game_area.center_x, y, title_, -4, title_color_); // --- Línea separadora --- y = rect_.y + upper_height_; SDL_SetRenderDrawColor(Screen::get()->getRenderer(), title_color_.r, title_color_.g, title_color_.b, 255); SDL_RenderLine(Screen::get()->getRenderer(), rect_.x + OPTIONS_HORIZONTAL_PADDING_, y, rect_.x + rect_.w - OPTIONS_HORIZONTAL_PADDING_, y); // --- Opciones del menú --- y = options_y_; for (size_t i = 0; i < option_pairs_.size(); ++i) { if (getGroupAlignment(current_settings_group_) == GroupAlignment::LEFT) { // Opción alineada a la izquierda element_text_->writeColored(rect_.x + OPTIONS_HORIZONTAL_PADDING_, y, option_pairs_.at(i).first, i == selected_ ? selected_color_ : text_color_, -2); const int X = rect_.x + rect_.w - OPTIONS_HORIZONTAL_PADDING_ - element_text_->lenght(std::string(option_pairs_.at(i).second), -2); element_text_->writeColored(X, y, std::string(option_pairs_.at(i).second), i == selected_ ? selected_color_ : text_color_, -2); } else { // Opción centrada element_text_->writeDX(TEXT_CENTER | TEXT_COLOR, rect_.x + rect_.w / 2, y, option_pairs_.at(i).first, -2, i == selected_ ? selected_color_ : text_color_); } y += options_height_ + options_padding_; } } // --- Métodos de actualización y animación --- // Actualiza el estado del menú y la animación del mensaje de reinicio void ServiceMenu::update() { if (resizing_) { updateResizeAnimation(); return; } if (enabled_) { updateCounter(); selected_color_ = getSelectedColor(); } // Detecta cambios en el estado de pending_changes y muestra/oculta el mensaje animado bool now_pending = Options::pending_changes.has_pending_changes; if (now_pending != last_pending_changes_) { if (now_pending) { const float MSG_X = param.game.game_area.center_x; const float MSG_Y = rect_.y + RESET_TEXT_POS_Y_; restart_message_ui_->show(MSG_X, MSG_Y); } else { restart_message_ui_->hide(); } last_pending_changes_ = now_pending; } // Actualiza la animación del mensaje restart_message_ui_->update(); } // --- Métodos de layout y animación del menú --- // Calcula y establece los anclajes y dimensiones del menú void ServiceMenu::setAnchors() { const size_t MAX_ENTRIES = findLargestGroupSize(); options_height_ = element_text_->getCharacterSize(); options_padding_ = 5; title_height_ = title_text_->getCharacterSize(); title_padding_ = title_height_ / 2; upper_height_ = (title_padding_ * 2) + title_height_; lower_padding_ = (options_padding_ * 3); lower_height_ = ((MAX_ENTRIES > 0 ? MAX_ENTRIES - 1 : 0) * (options_height_ + options_padding_)) + options_height_ + (lower_padding_ * 2); width_ = 240; height_ = upper_height_ + lower_height_; rect_ = {(param.game.width - width_) / 2.0f, (param.game.height - height_) / 2.0f, static_cast(width_), static_cast(height_)}; setOptionsPosition(); } // Calcula la posición vertical de las opciones void ServiceMenu::setOptionsPosition() { resize(); SDL_FRect new_rect = {(param.game.width - width_) / 2.0f, (param.game.height - height_) / 2.0f, static_cast(width_), static_cast(height_)}; options_y_ = new_rect.y + upper_height_ + lower_padding_; } // Redimensiona el menú y prepara la animación si es necesario void ServiceMenu::resize() { int menu_width = getMenuWidthForGroup(current_settings_group_); width_ = menu_width; lower_height_ = ((display_options_.size() > 0 ? display_options_.size() - 1 : 0) * (options_height_ + options_padding_)) + options_height_ + (lower_padding_ * 2); height_ = upper_height_ + lower_height_; SDL_FRect new_rect = {(param.game.width - width_) / 2.0f, (param.game.height - height_) / 2.0f, static_cast(width_), static_cast(height_)}; if (rect_.x != new_rect.x || rect_.y != new_rect.y || rect_.w != new_rect.w || rect_.h != new_rect.h) { rect_anim_from_ = rect_; rect_anim_to_ = new_rect; resize_anim_step_ = 0; resizing_ = true; } else { rect_ = new_rect; resizing_ = false; } } // Actualiza la animación de resize del menú y la posición del mensaje de reinicio void ServiceMenu::updateResizeAnimation() { if (!resizing_) return; ++resize_anim_step_; float t = static_cast(resize_anim_step_) / resize_anim_steps_; if (t >= 1.0f) { rect_ = rect_anim_to_; resizing_ = false; restart_message_ui_->setBaseY(rect_.y + RESET_TEXT_POS_Y_); return; } float ease = 1 - (1 - t) * (1 - t); rect_.x = rect_anim_from_.x + (rect_anim_to_.x - rect_anim_from_.x) * ease; rect_.y = rect_anim_from_.y + (rect_anim_to_.y - rect_anim_from_.y) * ease; rect_.w = rect_anim_from_.w + (rect_anim_to_.w - rect_anim_from_.w) * ease; rect_.h = rect_anim_from_.h + (rect_anim_to_.h - rect_anim_from_.h) * ease; } // --- Métodos utilitarios --- // Actualiza el contador interno para animaciones de color void ServiceMenu::updateCounter() { static Uint64 lastUpdate = SDL_GetTicks(); Uint64 currentTicks = SDL_GetTicks(); if (currentTicks - lastUpdate >= 50) { counter_++; lastUpdate = currentTicks; } } // Devuelve el color animado del elemento seleccionado Color ServiceMenu::getSelectedColor() const { static std::array colors = {Color(0xFF, 0xFB, 0x8A), Color(0xFF, 0xE4, 0x5D), Color(0xFF, 0xD1, 0x3C), Color(0xFF, 0xBF, 0x23), Color(0xFF, 0xAA, 0x12), Color(0xE6, 0x9A, 0x08), Color(0xE6, 0x9A, 0x08), Color(0xFF, 0xAA, 0x12), Color(0xFF, 0xBF, 0x23), Color(0xFF, 0xD1, 0x3C), Color(0xFF, 0xE4, 0x5D), Color(0xFF, 0xFB, 0x8A)}; return colors.at(counter_ % colors.size()); } // Reproduce el sonido de navegación del menú void ServiceMenu::playMenuSound() { Audio::get()->playSound(MENU_SOUND_); } // Actualiza el menú según el grupo seleccionado void ServiceMenu::updateMenu(SettingsGroup group) { title_ = settingsGroupToString(group); AdjustListValues(); option_pairs_ = getOptionPairs(group); display_options_ = getOptionsByGroup(group); resize(); } // Devuelve el tamaño del grupo con más opciones int ServiceMenu::findLargestGroupSize() const { std::unordered_map group_counts; for (const auto &option : options_) ++group_counts[option->getGroup()]; int max_size = 0; for (const auto &pair : group_counts) if (pair.second > max_size) max_size = pair.second; return max_size; } // Devuelve la alineación de las opciones para el grupo dado ServiceMenu::GroupAlignment ServiceMenu::getGroupAlignment(SettingsGroup group) const { switch (group) { case SettingsGroup::VIDEO: case SettingsGroup::AUDIO: case SettingsGroup::SETTINGS: return GroupAlignment::LEFT; default: return GroupAlignment::CENTERED; } } // Devuelve el nombre del grupo como string para el título std::string ServiceMenu::settingsGroupToString(SettingsGroup group) const { switch (group) { case SettingsGroup::MAIN: return Lang::getText("[SERVICE_MENU] TITLE"); case SettingsGroup::VIDEO: return Lang::getText("[SERVICE_MENU] VIDEO"); case SettingsGroup::AUDIO: return Lang::getText("[SERVICE_MENU] AUDIO"); case SettingsGroup::SETTINGS: return Lang::getText("[SERVICE_MENU] SETTINGS"); case SettingsGroup::SYSTEM: return Lang::getText("[SERVICE_MENU] SYSTEM"); default: return Lang::getText("[SERVICE_MENU] TITLE"); } } // Devuelve las opciones del grupo como pares (nombre, valor) ServiceMenu::OptionPairs ServiceMenu::getOptionPairs(SettingsGroup group) const { OptionPairs option_pairs; for (const auto &option : options_) { if (option->getGroup() == group && !option->isHidden()) { option_pairs.emplace_back(option->getCaption(), option->getValueAsString()); } } return option_pairs; } // Devuelve las opciones del grupo como un vector de punteros std::vector ServiceMenu::getOptionsByGroup(SettingsGroup group) { std::vector filtered_options; for (auto &option : options_) { if (option->getGroup() == group && !option->isHidden()) { filtered_options.push_back(option.get()); } } return filtered_options; } // Busca una opción por su caption MenuOption *ServiceMenu::getOptionByCaption(const std::string &caption) { for (auto &option : options_) { if (option->getCaption() == caption) { return option.get(); } } return nullptr; } // Aplica la configuración del grupo seleccionado void ServiceMenu::applySettings(SettingsGroup group) { if (group == SettingsGroup::VIDEO) Screen::get()->applySettings(); if (group == SettingsGroup::AUDIO) Audio::get()->applySettings(); if (group == SettingsGroup::SETTINGS) { auto option = getOptionByCaption(Lang::getText("[SERVICE_MENU] SHUTDOWN")); if (option) { option->setHidden(!Options::settings.shutdown_enabled); display_options_ = getOptionsByGroup(group); } } } // Sincroniza los valores de las opciones tipo lista void ServiceMenu::AdjustListValues() { for (auto &option : options_) { if (auto list_option = dynamic_cast(option.get())) { list_option->sync(); } } } // --- Métodos de cálculo de ancho de menú --- // Calcula el ancho óptimo para cada grupo de menú void ServiceMenu::precalculateMenuWidths() { for (int &w : group_menu_widths_) w = MIN_WIDTH_; for (int group = 0; group < 5; ++group) { SettingsGroup sg = static_cast(group); int max_option_width = 0; int max_value_width = 0; for (const auto &option : options_) { if (option->getGroup() != sg) continue; max_option_width = std::max(max_option_width, element_text_->lenght(option->getCaption(), -2)); max_value_width = std::max(max_value_width, option->getMaxValueWidth(element_text_.get())); } if (getGroupAlignment(sg) == GroupAlignment::LEFT) { size_t total_width = max_option_width + MIN_GAP_OPTION_VALUE_ + max_value_width + (OPTIONS_HORIZONTAL_PADDING_ * 2); group_menu_widths_[group] = std::max(static_cast(MIN_WIDTH_), static_cast(total_width)); } else { size_t total_width = max_option_width + (OPTIONS_HORIZONTAL_PADDING_ * 2); group_menu_widths_[group] = std::max(static_cast(MIN_WIDTH_), static_cast(total_width)); } } } // Devuelve el ancho precalculado para un grupo de menú int ServiceMenu::getMenuWidthForGroup(SettingsGroup group) const { return group_menu_widths_[static_cast(group)]; }