diff --git a/docs/options-window.md b/docs/options-window.md index 1648e12a34..50dd142c00 100644 --- a/docs/options-window.md +++ b/docs/options-window.md @@ -37,6 +37,9 @@ What was added on top of that: centered dialogs remain capped at 2 times their reference size. Text grows with the active layout up to the dock scale, shrinking only when needed to fit an existing control. +- **Classic bottom HUD** is an opt-in UI setting. It keeps the original 640-wide + bar uniformly scaled and centered on wide windows; leave it off to use the + responsive three-band layout. The change applies immediately. ## What it stores in `config.ini` @@ -52,6 +55,8 @@ window writes to these sections: `SavePassword`, `EncryptedUsername`, `EncryptedPassword`); see "Remembering login credentials" below. - `[ConnectionSettings]` +- `[UI]` - locale, font, and `BottomHudClassic` (`0` by default; `1` enables the + classic centered bottom bar). - `[Camera]` - orbital wheel-zoom radius (`Zoom`). **New in this PR**, the only key the camera rework added. diff --git a/src/Localization/Game.en.resx b/src/Localization/Game.en.resx index cae014ca44..cc38b0944e 100644 --- a/src/Localization/Game.en.resx +++ b/src/Localization/Game.en.resx @@ -1433,6 +1433,9 @@ Windowed mode + + Classic bottom HUD + Close legacy_id=388,1002 diff --git a/src/source/App/Platform/Windows/Winmain.cpp b/src/source/App/Platform/Windows/Winmain.cpp index 0b577b9b53..460067edea 100644 --- a/src/source/App/Platform/Windows/Winmain.cpp +++ b/src/source/App/Platform/Windows/Winmain.cpp @@ -327,6 +327,11 @@ void MuReapplyVSyncPreference() g_hasPendingVSyncPreference = true; } +void MuApplyBottomHudLayoutPreference() +{ + UI::Scaling::SetBottomHudClassic(GameConfig::GetInstance().GetBottomHudClassic()); +} + BOOL GetFileNameOfFilePath(wchar_t* lpszFile, wchar_t* lpszPath) { auto iFind = (int)'\\'; @@ -1916,6 +1921,7 @@ int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int nC // Load game settings from INI file first GameConfig::GetInstance().Load(); + MuApplyBottomHudLayoutPreference(); // Check if animation task pool should be enabled (disabled by default) { diff --git a/src/source/Data/GameConfig/GameConfig.cpp b/src/source/Data/GameConfig/GameConfig.cpp index dd505befc8..07ebcf6520 100644 --- a/src/source/Data/GameConfig/GameConfig.cpp +++ b/src/source/Data/GameConfig/GameConfig.cpp @@ -79,6 +79,7 @@ void GameConfig::Load() m_uiLocale = ReadString(CfgSectionUI, CfgKeyUILocale, CfgDefaultUILocale); m_fontSelection = ReadString(CfgSectionUI, CfgKeyFont, CfgDefaultFont); + m_bottomHudClassic = ReadBool(CfgSectionUI, CfgKeyBottomHudClassic, CfgDefaultBottomHudClassic); m_zoom = ReadInt(CfgSectionCamera, CfgKeyZoom, CfgDefaultZoom); m_sortParticleDraws = ReadBool(CfgSectionRender, CfgKeySortParticleDraws, CfgDefaultSortParticleDraws); @@ -124,6 +125,7 @@ void GameConfig::Save() WriteString(CfgSectionUI, CfgKeyUILocale, m_uiLocale); WriteString(CfgSectionUI, CfgKeyFont, m_fontSelection); + WriteBool(CfgSectionUI, CfgKeyBottomHudClassic, m_bottomHudClassic); WriteInt(CfgSectionCamera, CfgKeyZoom, m_zoom); WriteBool(CfgSectionRender, CfgKeyVSync, m_vsyncEnabled); @@ -226,6 +228,11 @@ void GameConfig::SetFontSelection(const std::wstring& font) m_fontSelection = font; } +void GameConfig::SetBottomHudClassic(bool classic) +{ + m_bottomHudClassic = classic; +} + void GameConfig::SetEncryptedUsername(const std::wstring& encryptedUsername) { m_encryptedUsername = encryptedUsername; diff --git a/src/source/Data/GameConfig/GameConfig.h b/src/source/Data/GameConfig/GameConfig.h index 2510f305e7..c9af27226d 100644 --- a/src/source/Data/GameConfig/GameConfig.h +++ b/src/source/Data/GameConfig/GameConfig.h @@ -67,6 +67,11 @@ class GameConfig // UI font family name (GDI face name). Empty = platform default. std::wstring GetFontSelection() const { return m_fontSelection; } void SetFontSelection(const std::wstring& font); + bool GetBottomHudClassic() const + { + return m_bottomHudClassic; + } + void SetBottomHudClassic(bool classic); // Chat commands - the favourites and the named templates of the command // window. They belong to the installation, not to a character. @@ -127,6 +132,7 @@ class GameConfig std::wstring m_uiLocale; std::wstring m_fontSelection; + bool m_bottomHudClassic; int m_zoom; bool m_sortParticleDraws; diff --git a/src/source/Data/GameConfig/GameConfigConstants.h b/src/source/Data/GameConfig/GameConfigConstants.h index 8f90c1333a..cc039c5a01 100644 --- a/src/source/Data/GameConfig/GameConfigConstants.h +++ b/src/source/Data/GameConfig/GameConfigConstants.h @@ -36,8 +36,8 @@ namespace CfgKeys // UI inline constexpr wchar_t CfgKeyUILocale[] = L"Locale"; - inline constexpr wchar_t CfgKeyFont[] = L"Font"; - + inline constexpr wchar_t CfgKeyFont[] = L"Font"; + inline constexpr wchar_t CfgKeyBottomHudClassic[] = L"BottomHudClassic"; // Camera inline constexpr wchar_t CfgKeyZoom[] = L"Zoom"; @@ -75,6 +75,8 @@ namespace CfgDefaults // Windows, fontconfig "sans-serif" on Linux), so the look is unchanged until // the user picks a font. Any value is passed through as the GDI face name. inline constexpr wchar_t CfgDefaultFont[] = L""; + // Keep the responsive three-band layout unless the player opts in. + inline constexpr bool CfgDefaultBottomHudClassic = false; // Legacy config compatibility only. SDL GPU ignores this key and default. inline constexpr bool CfgDefaultCoreProfile = true; diff --git a/src/source/UI/NewUI/Options/NewUIOptionWindow.cpp b/src/source/UI/NewUI/Options/NewUIOptionWindow.cpp index 814daced38..cf56c7334c 100644 --- a/src/source/UI/NewUI/Options/NewUIOptionWindow.cpp +++ b/src/source/UI/NewUI/Options/NewUIOptionWindow.cpp @@ -9,6 +9,7 @@ #include "Audio/DSPlaySound.h" #include "Data/GameConfig/GameConfig.h" #include "Data/GameConfig/GameConfigConstants.h" +#include "UI/Scaling/UITransform.h" #include "Audio/AudioPlayer.h" #include #include @@ -182,10 +183,16 @@ namespace constexpr int FONT_LABEL_Y_LOCAL = 244; constexpr int FONT_COMBO_X_LOCAL = 22; constexpr int FONT_COMBO_Y_LOCAL = 257; - constexpr int FONT_COMBO_WIDTH = 148; - constexpr int FONT_COMBO_HEIGHT = 16; + constexpr int FONT_COMBO_WIDTH = 148; + constexpr int FONT_COMBO_HEIGHT = 16; constexpr int FONT_COMBO_MAX_VISIBLE = 5; -} + + constexpr int CLASSIC_BOTTOM_HUD_CHECKBOX_Y_LOCAL = 395; + constexpr int CLASSIC_BOTTOM_HUD_LABEL_Y_LOCAL = 400; + constexpr int OPTION_CLOSE_Y_LOCAL = 427; + constexpr int OPTION_WINDOW_HEIGHT = 458; + constexpr int OPTION_FRAME_SLAT_COUNT = 35; +} // namespace ////////////////////////////////////////////////////////////////////// // Construction/Destruction @@ -206,6 +213,7 @@ SEASON3B::CNewUIOptionWindow::CNewUIOptionWindow() m_bRenderAllEffects = true; m_iResolutionIndex = 0; m_bWindowedMode = (g_bUseWindowMode == TRUE); + m_bBottomHudClassic = GameConfig::GetInstance().GetBottomHudClassic(); m_iLanguageIndex = FindCurrentLanguageIndex(); m_iFontIndex = FindCurrentFontIndex(); } @@ -299,7 +307,7 @@ void SEASON3B::CNewUIOptionWindow::SetButtonInfo() { m_BtnClose.ChangeTextBackColor(RGBA(255, 255, 255, 0)); m_BtnClose.ChangeButtonImgState(true, IMAGE_OPTION_BTN_CLOSE, true); - m_BtnClose.ChangeButtonInfo(m_Pos.x + 68, m_Pos.y + 388, 54, 30); + m_BtnClose.ChangeButtonInfo(m_Pos.x + 68, m_Pos.y + OPTION_CLOSE_Y_LOCAL, 54, 30); m_BtnClose.ChangeImgColor(BUTTON_STATE_UP, RGBA(255, 255, 255, 255)); m_BtnClose.ChangeImgColor(BUTTON_STATE_DOWN, RGBA(255, 255, 255, 255)); } @@ -392,12 +400,14 @@ bool SEASON3B::CNewUIOptionWindow::UpdateMouseEvent() return false; } - bool oldWindowedMode = m_bWindowedMode; + const bool oldWindowedMode = m_bWindowedMode; + const bool oldBottomHudClassic = m_bBottomHudClassic; HandleCheckboxInputs(); if (m_bWindowedMode != oldWindowedMode) ApplyWindowModeToggle(); - + if (m_bBottomHudClassic != oldBottomHudClassic) + ApplyBottomHudClassic(); if (HandleVolumeSlider(m_iVolumeLevel, 104)) OnSoundVolumeChanged(); @@ -408,7 +418,7 @@ bool SEASON3B::CNewUIOptionWindow::UpdateMouseEvent() // Combo box already processed at the top. Just consume clicks inside the // option window itself so they don't fall through to the world. - if (CheckMouseIn(m_Pos.x, m_Pos.y, 190, 419)) + if (CheckMouseIn(m_Pos.x, m_Pos.y, 190, OPTION_WINDOW_HEIGHT)) return false; return true; @@ -418,11 +428,9 @@ void SEASON3B::CNewUIOptionWindow::HandleCheckboxInputs() { struct Checkbox { int yLocal; bool* target; }; const Checkbox boxes[] = { - { 43, &m_bAutoAttack }, - { 65, &m_bWhisperSound }, - { 155, &m_bSlideHelp }, - { 238, &m_bRenderAllEffects }, - { 356, &m_bWindowedMode }, + {43, &m_bAutoAttack}, {65, &m_bWhisperSound}, + {155, &m_bSlideHelp}, {238, &m_bRenderAllEffects}, + {356, &m_bWindowedMode}, {CLASSIC_BOTTOM_HUD_CHECKBOX_Y_LOCAL, &m_bBottomHudClassic}, }; constexpr int CHECKBOX_X_LOCAL = 150; @@ -567,6 +575,7 @@ void SEASON3B::CNewUIOptionWindow::OpenningProcess() m_FontCombo.SetSelectedIndex(m_iFontIndex); m_FontCombo.Close(); m_bWindowedMode = (g_bUseWindowMode == TRUE); + m_bBottomHudClassic = GameConfig::GetInstance().GetBottomHudClassic(); } void SEASON3B::CNewUIOptionWindow::ClosingProcess() @@ -616,9 +625,8 @@ void SEASON3B::CNewUIOptionWindow::RenderFrame() x = m_Pos.x; y = m_Pos.y; // Frame is composed of: 64px top + N*10px middle slats + 45px bottom. The - // slat count is tuned so the frame reaches the Close button (Y 388) plus the - // bottom border, after the Font/Language/Resolution/Windowed rows. - constexpr int SLAT_COUNT = 30; + // slat count reaches the Close button after the UI settings rows. + constexpr int SLAT_COUNT = OPTION_FRAME_SLAT_COUNT; constexpr float FRAME_HEIGHT = 64.f + SLAT_COUNT * 10.f + 45.f; RenderImage(IMAGE_OPTION_FRAME_BACK, x, y, 190.f, FRAME_HEIGHT); RenderImage(IMAGE_OPTION_FRAME_UP, x, y, 190.f, 64.f); @@ -695,6 +703,10 @@ void SEASON3B::CNewUIOptionWindow::RenderContents() y += 39.f; RenderImage(IMAGE_OPTION_POINT, x, y, 10.f, 10.f); // Windowed Mode g_pRenderText->RenderText(m_Pos.x + 40, m_Pos.y + 361, I18N::Game::WindowedMode); + + y += 39.f; + RenderImage(IMAGE_OPTION_POINT, x, y, 10.f, 10.f); // Classic Bottom HUD + g_pRenderText->RenderText(m_Pos.x + 40, m_Pos.y + CLASSIC_BOTTOM_HUD_LABEL_Y_LOCAL, I18N::Game::ClassicBottomHUD); } void SEASON3B::CNewUIOptionWindow::RenderButtons() @@ -772,6 +784,16 @@ void SEASON3B::CNewUIOptionWindow::RenderButtons() RenderImage(IMAGE_OPTION_BTN_CHECK, m_Pos.x + 150, m_Pos.y + 356, 15, 15, 0, 15.f); } + if (m_bBottomHudClassic) + { + RenderImage(IMAGE_OPTION_BTN_CHECK, m_Pos.x + 150, m_Pos.y + CLASSIC_BOTTOM_HUD_CHECKBOX_Y_LOCAL, 15, 15, 0, 0); + } + else + { + RenderImage(IMAGE_OPTION_BTN_CHECK, m_Pos.x + 150, m_Pos.y + CLASSIC_BOTTOM_HUD_CHECKBOX_Y_LOCAL, 15, 15, 0, + 15.f); + } + // Combo boxes drawn last so their expanded dropdowns sit on top of // anything else in the window. Within the combo pair, render the // closed one(s) first and any open dropdown last - otherwise a combo @@ -966,3 +988,10 @@ void SEASON3B::CNewUIOptionWindow::ApplyWindowModeToggle() // toggle again next frame; the user must release and click again. g_pNewKeyInput->SetKeyState(VK_LBUTTON, SEASON3B::CNewKeyInput::KEY_NONE); } + +void SEASON3B::CNewUIOptionWindow::ApplyBottomHudClassic() +{ + UI::Scaling::SetBottomHudClassic(m_bBottomHudClassic); + GameConfig::GetInstance().SetBottomHudClassic(m_bBottomHudClassic); + GameConfig::GetInstance().Save(); +} diff --git a/src/source/UI/NewUI/Options/NewUIOptionWindow.h b/src/source/UI/NewUI/Options/NewUIOptionWindow.h index ebc807f735..a757fe327a 100644 --- a/src/source/UI/NewUI/Options/NewUIOptionWindow.h +++ b/src/source/UI/NewUI/Options/NewUIOptionWindow.h @@ -114,6 +114,7 @@ namespace SEASON3B bool m_bRenderAllEffects; int m_iResolutionIndex; bool m_bWindowedMode; + bool m_bBottomHudClassic; int m_iLanguageIndex; int m_iFontIndex; @@ -134,6 +135,7 @@ namespace SEASON3B void InitResolutionCombo(); void SyncResolutionComboToWindow(); void ApplyWindowModeToggle(); + void ApplyBottomHudClassic(); void ApplyLanguage(); int FindCurrentLanguageIndex(); diff --git a/src/source/UI/Scaling/UITransform.cpp b/src/source/UI/Scaling/UITransform.cpp index 3384ab4238..3cb819f9a5 100644 --- a/src/source/UI/Scaling/UITransform.cpp +++ b/src/source/UI/Scaling/UITransform.cpp @@ -55,6 +55,13 @@ UI::Scaling::Transform BottomHudTransform(int windowWidth, int windowHeight, flo return {scale, scale, offsetX, static_cast(windowHeight) - kReferenceHeight * scale, scale}; } +UI::Scaling::Transform BottomHudClassicTransform(int windowWidth, int windowHeight) +{ + const float scale = UI::Scaling::BottomHudScale(windowWidth, windowHeight); + const float offsetX = (static_cast(windowWidth) - kReferenceWidth * scale) * 0.5f; + return BottomHudTransform(windowWidth, windowHeight, offsetX); +} + bool ContainsLogicalRect(const UI::Scaling::Transform& transform, float windowX, float windowY, float left, float top, float right, float bottom) { @@ -82,11 +89,14 @@ float CappedUniformScale(int windowWidth, int windowHeight, float maximumScale) UI::Scaling::Transform DockTransform(int windowWidth, int windowHeight) { const float scale = CappedUniformScale(windowWidth, windowHeight, kMaximumDockScale); - const float offsetY = static_cast(RoundedBottomHudTop(windowWidth, windowHeight)) - - UI::Scaling::DockLogicalBottom * scale; + const float offsetY = + static_cast(RoundedBottomHudTop(windowWidth, windowHeight)) - UI::Scaling::DockLogicalBottom * scale; return {scale, scale, 0.0f, offsetY, scale}; } -} + +bool g_bottomHudClassic = false; + +} // namespace UI::Scaling::Transform UI::Scaling::ScreenOverlayTransform(int windowWidth, int windowHeight) { @@ -133,13 +143,27 @@ float UI::Scaling::BottomHudScale(int windowWidth, int windowHeight) kMaximumHudScale * contentScale); } +void UI::Scaling::SetBottomHudClassic(bool classic) +{ + g_bottomHudClassic = classic; +} + +bool UI::Scaling::IsBottomHudClassic() +{ + return g_bottomHudClassic; +} + UI::Scaling::Transform UI::Scaling::BottomHudLeftTransform(int windowWidth, int windowHeight) { + if (g_bottomHudClassic) + return BottomHudClassicTransform(windowWidth, windowHeight); return BottomHudTransform(windowWidth, windowHeight, 0.0f); } UI::Scaling::Transform UI::Scaling::BottomHudCenterTransform(int windowWidth, int windowHeight) { + if (g_bottomHudClassic) + return BottomHudClassicTransform(windowWidth, windowHeight); const float scale = BottomHudScale(windowWidth, windowHeight); return BottomHudTransform(windowWidth, windowHeight, static_cast(windowWidth) * 0.5f - 320.0f * scale); @@ -147,6 +171,8 @@ UI::Scaling::Transform UI::Scaling::BottomHudCenterTransform(int windowWidth, in UI::Scaling::Transform UI::Scaling::BottomHudRightTransform(int windowWidth, int windowHeight) { + if (g_bottomHudClassic) + return BottomHudClassicTransform(windowWidth, windowHeight); const float scale = BottomHudScale(windowWidth, windowHeight); return BottomHudTransform(windowWidth, windowHeight, static_cast(windowWidth) - kReferenceWidth * scale); @@ -154,6 +180,8 @@ UI::Scaling::Transform UI::Scaling::BottomHudRightTransform(int windowWidth, int UI::Scaling::Transform UI::Scaling::BottomHudExperienceTransform(int windowWidth, int windowHeight) { + if (g_bottomHudClassic) + return BottomHudClassicTransform(windowWidth, windowHeight); Transform transform = BottomHudLeftTransform(windowWidth, windowHeight); transform.scaleX = static_cast(windowWidth) / kReferenceWidth; return transform; diff --git a/src/source/UI/Scaling/UITransform.h b/src/source/UI/Scaling/UITransform.h index fe095d307b..62c635e1e6 100644 --- a/src/source/UI/Scaling/UITransform.h +++ b/src/source/UI/Scaling/UITransform.h @@ -69,6 +69,8 @@ namespace UI::Scaling Transform LegacyUiTransform(int windowWidth, int windowHeight); Transform PanelTransform(int windowWidth, int windowHeight); float BottomHudScale(int windowWidth, int windowHeight); + void SetBottomHudClassic(bool classic); + bool IsBottomHudClassic(); Transform BottomHudLeftTransform(int windowWidth, int windowHeight); Transform BottomHudCenterTransform(int windowWidth, int windowHeight); Transform BottomHudRightTransform(int windowWidth, int windowHeight); diff --git a/tests/ui/test_ui_scaling.cpp b/tests/ui/test_ui_scaling.cpp index 0af1ff621a..cd79a2fdf6 100644 --- a/tests/ui/test_ui_scaling.cpp +++ b/tests/ui/test_ui_scaling.cpp @@ -615,6 +615,30 @@ TEST_CASE("bottom HUD regional transforms round trip window positions [ui][scali CHECK(UI::Scaling::LogicalY(center, UI::Scaling::PositionY(center, 450.0f)) == doctest::Approx(450.0f)); } +TEST_CASE("classic bottom HUD centers the whole bar and keeps the rail its width [ui][scaling]") +{ + CHECK_FALSE(CfgDefaults::CfgDefaultBottomHudClassic); + CHECK_FALSE(UI::Scaling::IsBottomHudClassic()); + UI::Scaling::SetBottomHudClassic(true); + const auto left = UI::Scaling::BottomHudLeftTransform(1920, 1080); + const auto center = UI::Scaling::BottomHudCenterTransform(1920, 1080); + const auto right = UI::Scaling::BottomHudRightTransform(1920, 1080); + const auto experience = UI::Scaling::BottomHudExperienceTransform(1920, 1080); + UI::Scaling::SetBottomHudClassic(false); + CHECK_FALSE(UI::Scaling::IsBottomHudClassic()); + + CHECK(left.scaleX == doctest::Approx(2.0f)); + CHECK(left.offsetX == doctest::Approx(320.0f)); + CHECK(center.offsetX == doctest::Approx(left.offsetX)); + CHECK(right.offsetX == doctest::Approx(left.offsetX)); + CHECK(UI::Scaling::PositionX(left, 0.0f) == doctest::Approx(320.0f)); + CHECK(UI::Scaling::PositionX(right, 640.0f) == doctest::Approx(1600.0f)); + CHECK(UI::Scaling::PositionX(center, 320.0f) == doctest::Approx(960.0f)); + CHECK(experience.scaleX == doctest::Approx(2.0f)); + CHECK(UI::Scaling::PositionX(experience, 640.0f) == doctest::Approx(1600.0f)); + CHECK(UI::Scaling::PositionY(experience, 480.0f) == doctest::Approx(1080.0f)); +} + TEST_CASE("experience transform spans the window with HUD vertical scale [ui][scaling]") { const auto experience = UI::Scaling::BottomHudExperienceTransform(1920, 1200);