/** * @file gmenu.cpp * * Implementation of the in-game navigation and interaction. */ #include "gmenu.h" #include "DiabloUI/ui_flags.hpp" #include "control.h" #include "controls/axis_direction.h" #include "controls/controller_motion.h" #include "engine.h" #include "engine/clx_sprite.hpp" #include "engine/load_cel.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/text_render.hpp" #include "miniwin/misc_msg.h" #include "options.h" #include "stores.h" #include "utils/language.h" #include "utils/stdcompat/algorithm.hpp" #include "utils/stdcompat/optional.hpp" #include "utils/ui_fwd.h" namespace devilution { namespace { // Width of the slider menu item, including the label. constexpr int SliderItemWidth = 490; // Horizontal dimensions of the slider value constexpr int SliderValueBoxLeft = 16 + SliderItemWidth / 2; constexpr int SliderValueBoxWidth = 287; constexpr int SliderValueBorderWidth = 2; constexpr int SliderValueLeft = SliderValueBoxLeft + SliderValueBorderWidth; constexpr int SliderValueWidth = SliderValueBoxWidth - 2 * SliderValueBorderWidth; constexpr int SliderValueHeight = 29; constexpr int SliderValuePaddingTop = 10; constexpr int SliderMarkerWidth = 27; constexpr int SliderFillMin = SliderMarkerWidth / 2; constexpr int SliderFillMax = SliderValueWidth - SliderMarkerWidth / 2 - 1; constexpr int GMenuTop = 117; constexpr int GMenuItemHeight = 45; OptionalOwnedClxSpriteList optbar_cel; OptionalOwnedClxSpriteList PentSpin_cel; OptionalOwnedClxSpriteList option_cel; OptionalOwnedClxSpriteList sgpLogo; bool isDraggingSlider; TMenuItem *sgpCurrItem; int LogoAnim_tick; uint8_t LogoAnim_frame; void (*gmenu_current_option)(); int sgCurrentMenuIdx; void GmenuUpDown(bool isDown) { if (sgpCurrItem == nullptr) { return; } isDraggingSlider = false; int i = sgCurrentMenuIdx; if (sgCurrentMenuIdx != 0) { while (i != 0) { i--; if (isDown) { sgpCurrItem++; if (sgpCurrItem->fnMenu == nullptr) sgpCurrItem = &sgpCurrentMenu[0]; } else { if (sgpCurrItem == sgpCurrentMenu) sgpCurrItem = &sgpCurrentMenu[sgCurrentMenuIdx]; sgpCurrItem--; } if (sgpCurrItem->enabled()) { if (i != 0) PlaySFX(IS_TITLEMOV); return; } } } } void GmenuLeftRight(bool isRight) { if (!sgpCurrItem->isSlider()) return; uint16_t step = sgpCurrItem->sliderStep(); if (isRight) { if (step == sgpCurrItem->sliderSteps()) return; step++; } else { if (step == 0) return; step--; } sgpCurrItem->setSliderStep(step); sgpCurrItem->fnMenu(false); } int GmenuGetLineWidth(TMenuItem *pItem) { if (pItem->isSlider()) return SliderItemWidth; return GetLineWidth(_(pItem->pszStr), GameFont46, 2); } void GmenuDrawMenuItem(const Surface &out, TMenuItem *pItem, int y) { int w = GmenuGetLineWidth(pItem); if (pItem->isSlider()) { int uiPositionX = GetUIRectangle().position.x; ClxDraw(out, { SliderValueBoxLeft + uiPositionX, y + 40 }, (*optbar_cel)[0]); const uint16_t step = pItem->dwFlags & 0xFFF; const uint16_t steps = std::max(pItem->sliderSteps(), 2); const uint16_t pos = SliderFillMin + step * (SliderFillMax - SliderFillMin) / steps; SDL_Rect rect = MakeSdlRect(SliderValueLeft + uiPositionX, y + SliderValuePaddingTop, pos, SliderValueHeight); SDL_FillRect(out.surface, &rect, 205); ClxDraw(out, { SliderValueLeft + pos - SliderMarkerWidth / 2 + uiPositionX, y + SliderValuePaddingTop + SliderValueHeight - 1 }, (*option_cel)[0]); } int x = (gnScreenWidth - w) / 2; UiFlags style = pItem->enabled() ? UiFlags::ColorGold : UiFlags::ColorBlack; DrawString(out, _(pItem->pszStr), Point { x, y }, style | UiFlags::FontSize46, 2); if (pItem == sgpCurrItem) { const ClxSprite sprite = (*PentSpin_cel)[PentSpn2Spin()]; ClxDraw(out, { x - 54, y + 51 }, sprite); ClxDraw(out, { x + 4 + w, y + 51 }, sprite); } } void GameMenuMove() { static AxisDirectionRepeater repeater; const AxisDirection moveDir = repeater.Get(GetLeftStickOrDpadDirection(false)); if (moveDir.x != AxisDirectionX_NONE) GmenuLeftRight(moveDir.x == AxisDirectionX_RIGHT); if (moveDir.y != AxisDirectionY_NONE) GmenuUpDown(moveDir.y == AxisDirectionY_DOWN); } bool GmenuMouseIsOverSlider() { int uiPositionX = GetUIRectangle().position.x; if (MousePosition.x < SliderValueLeft + uiPositionX) { return false; } if (MousePosition.x >= SliderValueLeft + SliderValueWidth + uiPositionX) { return false; } return true; } int GmenuGetSliderFill() { return clamp(MousePosition.x - SliderValueLeft - GetUIRectangle().position.x, SliderFillMin, SliderFillMax); } } // namespace TMenuItem *sgpCurrentMenu; void gmenu_draw_pause(const Surface &out) { if (leveltype != DTYPE_TOWN) RedBack(out); if (sgpCurrentMenu == nullptr) { LightTableIndex = 0; DrawString(out, _("Pause"), { { 0, 0 }, { gnScreenWidth, GetMainPanel().position.y } }, UiFlags::FontSize46 | UiFlags::ColorGold | UiFlags::AlignCenter | UiFlags::VerticalCenter, 2); } } void FreeGMenu() { sgpLogo = std::nullopt; PentSpin_cel = std::nullopt; option_cel = std::nullopt; optbar_cel = std::nullopt; } void gmenu_init_menu() { LogoAnim_frame = 0; sgpCurrentMenu = nullptr; sgpCurrItem = nullptr; gmenu_current_option = nullptr; sgCurrentMenuIdx = 0; isDraggingSlider = false; if (HeadlessMode) return; if (gbIsHellfire) sgpLogo = LoadCel("data\\hf_logo3", 430); else sgpLogo = LoadCel("data\\diabsmal", 296); PentSpin_cel = LoadCel("data\\pentspin", 48); option_cel = LoadCel("data\\option", SliderMarkerWidth); optbar_cel = LoadCel("data\\optbar", SliderValueBoxWidth); } bool gmenu_is_active() { return sgpCurrentMenu != nullptr; } void gmenu_set_items(TMenuItem *pItem, void (*gmFunc)()) { PauseMode = 0; isDraggingSlider = false; sgpCurrentMenu = pItem; gmenu_current_option = gmFunc; if (gmenu_current_option != nullptr) { gmenu_current_option(); } sgCurrentMenuIdx = 0; if (sgpCurrentMenu != nullptr) { for (int i = 0; sgpCurrentMenu[i].fnMenu != nullptr; i++) { sgCurrentMenuIdx++; } } // BUGFIX: OOB access when sgCurrentMenuIdx is 0; should be set to NULL instead. (fixed) sgpCurrItem = sgCurrentMenuIdx > 0 ? &sgpCurrentMenu[sgCurrentMenuIdx - 1] : nullptr; GmenuUpDown(true); if (sgpCurrentMenu == nullptr) SaveOptions(); } void gmenu_draw(const Surface &out) { if (sgpCurrentMenu != nullptr) { GameMenuMove(); if (gmenu_current_option != nullptr) gmenu_current_option(); if (gbIsHellfire) { const uint32_t ticks = SDL_GetTicks(); if ((int)(ticks - LogoAnim_tick) > 25) { ++LogoAnim_frame; if (LogoAnim_frame >= 16) LogoAnim_frame = 0; LogoAnim_tick = ticks; } } int uiPositionY = GetUIRectangle().position.y; const ClxSprite sprite = (*sgpLogo)[LogoAnim_frame]; ClxDraw(out, { (gnScreenWidth - sprite.width()) / 2, 102 + uiPositionY }, sprite); int y = 110 + uiPositionY; TMenuItem *i = sgpCurrentMenu; if (sgpCurrentMenu->fnMenu != nullptr) { while (i->fnMenu != nullptr) { GmenuDrawMenuItem(out, i, y); i++; y += GMenuItemHeight; } } } } bool gmenu_presskeys(SDL_Keycode vkey) { if (sgpCurrentMenu == nullptr) return false; switch (vkey) { case SDLK_KP_ENTER: case SDLK_RETURN: if (sgpCurrItem->enabled()) { PlaySFX(IS_TITLEMOV); sgpCurrItem->fnMenu(true); } break; case SDLK_ESCAPE: PlaySFX(IS_TITLEMOV); gmenu_set_items(nullptr, nullptr); break; case SDLK_SPACE: return false; case SDLK_LEFT: GmenuLeftRight(false); break; case SDLK_RIGHT: GmenuLeftRight(true); break; case SDLK_UP: GmenuUpDown(false); break; case SDLK_DOWN: GmenuUpDown(true); break; default: break; } return true; } bool gmenu_on_mouse_move() { if (!isDraggingSlider) return false; const uint16_t step = sgpCurrItem->sliderSteps() * (GmenuGetSliderFill() - SliderFillMin) / (SliderFillMax - SliderFillMin); sgpCurrItem->setSliderStep(step); sgpCurrItem->fnMenu(false); return true; } bool gmenu_left_mouse(bool isDown) { if (!isDown) { if (isDraggingSlider) { isDraggingSlider = false; return true; } return false; } if (sgpCurrentMenu == nullptr) { return false; } const Point uiPosition = GetUIRectangle().position; if (MousePosition.y >= GetMainPanel().position.y) { return false; } if (MousePosition.y - (GMenuTop + uiPosition.y) < 0) { return true; } int i = (MousePosition.y - (GMenuTop + uiPosition.y)) / GMenuItemHeight; if (i >= sgCurrentMenuIdx) { return true; } TMenuItem *pItem = &sgpCurrentMenu[i]; if (!pItem->enabled()) { return true; } int w = GmenuGetLineWidth(pItem); uint16_t screenWidth = GetScreenWidth(); if (MousePosition.x < screenWidth / 2 - w / 2) { return true; } if (MousePosition.x > screenWidth / 2 + w / 2) { return true; } sgpCurrItem = pItem; PlaySFX(IS_TITLEMOV); if (pItem->isSlider()) { isDraggingSlider = GmenuMouseIsOverSlider(); gmenu_on_mouse_move(); } else { sgpCurrItem->fnMenu(true); } return true; } void gmenu_slider_set(TMenuItem *pItem, int min, int max, int value) { assert(pItem); uint16_t nSteps = std::max(pItem->sliderSteps(), 2); pItem->setSliderStep(((max - min - 1) / 2 + (value - min) * nSteps) / (max - min)); } int gmenu_slider_get(TMenuItem *pItem, int min, int max) { uint16_t step = pItem->sliderStep(); uint16_t steps = std::max(pItem->sliderSteps(), 2); return min + (step * (max - min) + (steps - 1) / 2) / steps; } void gmenu_slider_steps(TMenuItem *pItem, int steps) { pItem->dwFlags &= 0xFF000FFF; pItem->setSliderSteps(steps); } } // namespace devilution