diff --git a/CMake/Assets.cmake b/CMake/Assets.cmake index e4731a299..139882adc 100644 --- a/CMake/Assets.cmake +++ b/CMake/Assets.cmake @@ -173,6 +173,7 @@ set(devilutionx_assets nlevels/l5data/uberroom.dun ui_art/diablo.pal ui_art/hellfire.pal + ui_art/bnconnbgw.clx ui_art/creditsw.clx ui_art/dvl_but_sml.clx ui_art/dvl_lrpopup.clx diff --git a/Packaging/resources/assets/ui_art/bnconnbgw.clx b/Packaging/resources/assets/ui_art/bnconnbgw.clx new file mode 100644 index 000000000..f4d3584f7 Binary files /dev/null and b/Packaging/resources/assets/ui_art/bnconnbgw.clx differ diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 7a9969d6e..a7323689e 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -73,6 +73,11 @@ set(libdevilutionx_SRCS DiabloUI/diabloui.cpp DiabloUI/dialogs.cpp DiabloUI/hero/selhero.cpp + DiabloUI/hub/chat.cpp + DiabloUI/hub/create.cpp + DiabloUI/hub/friends.cpp + DiabloUI/hub/hub.cpp + DiabloUI/hub/join.cpp DiabloUI/mainmenu.cpp DiabloUI/multi/selconn.cpp DiabloUI/multi/selgame.cpp diff --git a/Source/DiabloUI/button.cpp b/Source/DiabloUI/button.cpp index 3a702dbed..0af3e4f65 100644 --- a/Source/DiabloUI/button.cpp +++ b/Source/DiabloUI/button.cpp @@ -13,6 +13,8 @@ namespace devilution { namespace { OptionalOwnedClxSpriteList ButtonSprites; +OptionalOwnedClxSpriteList ButtonXsSprites; +OptionalOwnedClxSpriteList ButtonBnSprites; } // namespace @@ -22,11 +24,21 @@ void LoadDialogButtonGraphics() if (!ButtonSprites) { ButtonSprites = LoadPcxSpriteList("ui_art\\but_sml", 15); } + ButtonXsSprites = LoadOptionalClx("ui_art\\but_xsm.clx"); + if (!ButtonXsSprites) { + ButtonXsSprites = LoadPcxSpriteList("ui_art\\but_xsm", -35); + } + ButtonBnSprites = LoadOptionalClx("ui_art\\bnbuttns.clx"); + if (!ButtonBnSprites) { + ButtonBnSprites = LoadPcxSpriteList("ui_art\\bnbuttns", -71); + } } void FreeDialogButtonGraphics() { ButtonSprites = std::nullopt; + ButtonXsSprites = std::nullopt; + ButtonBnSprites = std::nullopt; } ClxSprite ButtonSprite(bool pressed) diff --git a/Source/DiabloUI/diabloui.cpp b/Source/DiabloUI/diabloui.cpp index c2fb5c826..5fe0f06de 100644 --- a/Source/DiabloUI/diabloui.cpp +++ b/Source/DiabloUI/diabloui.cpp @@ -818,7 +818,11 @@ void Render(const UiImageClx &uiImage) if (uiImage.isCentered()) { x += GetCenterOffset(sprite.width(), uiImage.m_rect.w); } - RenderClxSprite(Surface(DiabloUiSurface()), sprite, { x, uiImage.m_rect.y }); + const Surface &out = Surface(DiabloUiSurface()); + if (uiImage.m_rect.w != 0) + RenderClxSprite(out.subregion(x, 0, uiImage.m_rect.w, out.h()), sprite, { 0, uiImage.m_rect.y }); + else + RenderClxSprite(out, sprite, { x, uiImage.m_rect.y }); } void Render(const UiImageAnimatedClx &uiImage) @@ -863,7 +867,7 @@ void Render(const UiScrollbar &uiSb) { const int bgY = uiSb.m_rect.y + uiSb.m_arrow[0].height(); const int bgH = DownArrowRect(uiSb).y - bgY; - const Surface backgroundOut = out.subregion(uiSb.m_rect.x, bgY, ScrollBarBgWidth, bgH); + const Surface backgroundOut = out.subregion(uiSb.m_rect.x, bgY, ScrollBarWidth, bgH); int y = 0; while (y < bgH) { RenderClxSprite(backgroundOut, uiSb.m_bg, { 0, y }); @@ -874,13 +878,13 @@ void Render(const UiScrollbar &uiSb) // Arrows: { const SDL_Rect rect = UpArrowRect(uiSb); - const auto frame = static_cast(scrollBarState.upArrowPressed ? ScrollBarArrowFrame_UP_ACTIVE : ScrollBarArrowFrame_UP); - RenderClxSprite(out.subregion(rect.x, 0, ScrollBarArrowWidth, out.h()), uiSb.m_arrow[frame], { 0, rect.y }); + const uint16_t frame = scrollBarState.upArrowPressed ? ScrollBarArrowFrame_UP_ACTIVE : ScrollBarArrowFrame_UP; + RenderClxSprite(out.subregion(rect.x, 0, ScrollBarWidth, out.h()), uiSb.m_arrow[frame], { 0, rect.y }); } { const SDL_Rect rect = DownArrowRect(uiSb); - const auto frame = static_cast(scrollBarState.downArrowPressed ? ScrollBarArrowFrame_DOWN_ACTIVE : ScrollBarArrowFrame_DOWN); - RenderClxSprite(out.subregion(rect.x, 0, ScrollBarArrowWidth, out.h()), uiSb.m_arrow[frame], { 0, rect.y }); + const uint16_t frame = scrollBarState.downArrowPressed ? ScrollBarArrowFrame_DOWN_ACTIVE : ScrollBarArrowFrame_DOWN; + RenderClxSprite(out.subregion(rect.x, 0, ScrollBarWidth, out.h()), uiSb.m_arrow[frame], { 0, rect.y }); } // Thumb: diff --git a/Source/DiabloUI/diabloui.h b/Source/DiabloUI/diabloui.h index 5c5e99412..e0dab36dd 100644 --- a/Source/DiabloUI/diabloui.h +++ b/Source/DiabloUI/diabloui.h @@ -89,6 +89,7 @@ bool UiCreditsDialog(); bool UiSupportDialog(); bool UiMainMenuDialog(const char *name, _mainmenu_selections *pdwResult, int attractTimeOut); bool UiProgressDialog(int (*fnfunc)()); +bool UiHubMain(); bool UiSelectGame(GameData *gameData, int *playerId); bool UiSelectProvider(GameData *gameData); void UiFadeIn(); diff --git a/Source/DiabloUI/hero/selhero.cpp b/Source/DiabloUI/hero/selhero.cpp index 844625bf7..6bd1d6880 100644 --- a/Source/DiabloUI/hero/selhero.cpp +++ b/Source/DiabloUI/hero/selhero.cpp @@ -8,6 +8,7 @@ #include "DiabloUI/diabloui.h" #include "DiabloUI/dialogs.h" +#include "DiabloUI/hub/hub.h" #include "DiabloUI/multi/selgame.h" #include "DiabloUI/scrollbar.h" #include "DiabloUI/selok.h" @@ -336,6 +337,7 @@ void SelheroLoadSelect(int value) } if (!selhero_isMultiPlayer) { + // TODO fix this :( // This is part of a dangerous hack to enable difficulty selection in single-player. // FIXME: Dialogs should not refer to each other's variables. @@ -350,7 +352,7 @@ void SelheroLoadSelect(int value) selhero_isSavegame = false; SelheroFree(); - LoadBackgroundArt("ui_art\\selgame"); + LoadBackgroundArt("ui_art\\bnconnbg"); selgame_GameSelection_Select(0); } diff --git a/Source/DiabloUI/hub/chat.cpp b/Source/DiabloUI/hub/chat.cpp new file mode 100644 index 000000000..40f23161d --- /dev/null +++ b/Source/DiabloUI/hub/chat.cpp @@ -0,0 +1,141 @@ +#include "DiabloUI/hub/chat.h" + +#include "DiabloUI/hub/hub.h" +#include "DiabloUI/scrollbar.h" +#include "engine/load_pcx.hpp" + +namespace devilution { + +namespace { + +char ChatMessage[129]; + +OptionalOwnedClxSpriteList MainButton; +std::vector PlayerList; + +struct ColoredText { + std::string text; + UiFlags color; +}; + +struct MultiColoredText { + std::string text; + std::vector colors; + int offset = 0; +}; + +std::vector ChatLogLines; + +void BuildFixtures() +{ + PlayerList.emplace_back(PlayerInfo { "KPhoenix", HeroClass::Warrior, 35, 2, GameIdDiabloFull, 50 }); + PlayerList.emplace_back(PlayerInfo { "AJenbo", HeroClass::Rogue, 23, 1, GameIdHellfireFull, 320 }); + PlayerList.emplace_back(PlayerInfo { "FireIceTalon", HeroClass::Sorcerer, 50, 3, GameIdDiabloFull, 150 }); + PlayerList.emplace_back(PlayerInfo { "glebm", HeroClass::Monk, 12, 0, GameIdHellfireFull, 1400 }); + PlayerList.emplace_back(PlayerInfo { "qndel", HeroClass::Warrior, 1, 0, GameIdDiabloSpawn, 450 }); + + ChatLogLines.emplace_back(MultiColoredText { "{0}", { { "KPhoenix has appeared on the network", UiFlags::ColorUiGreen } } }); + ChatLogLines.emplace_back(MultiColoredText { "{0} {1}", { { "", UiFlags::ColorYellow }, { "Hello", UiFlags::ColorDialogWhite } } }); + ChatLogLines.emplace_back(MultiColoredText { "{0} {1}", { { "", UiFlags::ColorBlue }, { "Ready?", UiFlags::ColorUiSilverDark } } }); + ChatLogLines.emplace_back(MultiColoredText { "{0}", { { "FireIceTalon started a new game", UiFlags::ColorRed } } }); +} + +void hubmain_Free() +{ + PlayerList.clear(); + ChatLogLines.clear(); +} + +void DialogActionOK() +{ +} + +} // namespace + +void hubmain_Init() +{ + Layout = LoadPcx("ui_art\\chat_bkg", /*transparentColor=*/0); + MainButton = LoadPcxSpriteList("ui_art\\bnbuttns", -71); + + BuildFixtures(); +} + +void UiHubInitPlayerList() +{ + const Point uiPosition = GetUIRectangle().position + Displacement { 460, 200 }; + int yOffset = 0; + + for (const PlayerInfo &player : PlayerList) { + UiHubPlacePlayerIcon({ uiPosition.x, uiPosition.y + yOffset }, player.gameMode, player); + + const SDL_Rect rect3 = MakeSdlRect(uiPosition.x + 30, uiPosition.y + yOffset - 6, 89, 20); + vecHubMainDialog.push_back(std::make_unique(player.name.data(), rect3, UiFlags::FontSizeDialog | UiFlags::ColorYellow, -1)); + + UiHubPlaceLatencyMeter(player.latency, { uiPosition.x + 121, uiPosition.y + yOffset + 2 }); + + yOffset += 16; + } +} + +void hubmain_GameSelection_Init() +{ + const Point uiPosition = GetUIRectangle().position; + + const SDL_Rect rect2 = MakeSdlRect(uiPosition.x + 445, uiPosition.y + 148, 179, 28); + vecHubMainDialog.push_back(std::make_unique(_("Global Chat").data(), rect2, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite | UiFlags::AlignCenter | UiFlags::VerticalCenter, -1)); + + vecHubMainDialog.push_back(std::make_unique((*MainButton)[0], MakeSdlRect(uiPosition.x + 10, uiPosition.y + 140, 85, 71))); + const SDL_Rect rect3 = MakeSdlRect(uiPosition.x + 10, uiPosition.y + 140 + 43, 85, 71); + vecHubMainDialog.push_back(std::make_unique(_("Friends").data(), rect3, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite | UiFlags::AlignCenter, -1)); + vecHubMainDialog.push_back(std::make_unique((*MainButton)[5], MakeSdlRect(uiPosition.x + 10, uiPosition.y + 227, 85, 71))); + const SDL_Rect rect4 = MakeSdlRect(uiPosition.x + 10, uiPosition.y + 227 + 43, 85, 71); + vecHubMainDialog.push_back(std::make_unique(_("Create").data(), rect4, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite | UiFlags::AlignCenter, -1)); + vecHubMainDialog.push_back(std::make_unique((*MainButton)[10], MakeSdlRect(uiPosition.x + 10, uiPosition.y + 315, 85, 71))); + const SDL_Rect rect5 = MakeSdlRect(uiPosition.x + 10, uiPosition.y + 315 + 43, 85, 71); + vecHubMainDialog.push_back(std::make_unique(_("Join").data(), rect5, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite | UiFlags::AlignCenter, -1)); + vecHubMainDialog.push_back(std::make_unique((*MainButton)[15], MakeSdlRect(uiPosition.x + 10, uiPosition.y + 402, 85, 71))); + const SDL_Rect rect6 = MakeSdlRect(uiPosition.x + 10, uiPosition.y + 402 + 43, 85, 71); + vecHubMainDialog.push_back(std::make_unique(_("Quit").data(), rect6, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite | UiFlags::AlignCenter, -1)); + + UiHubInitPlayerList(); + + const SDL_Rect rectScrollbar1 = MakeSdlRect(uiPosition.x + 402, uiPosition.y + 158, 17, 251); + vecHubMainDialog.push_back(std::make_unique((*ArtScrollBarBackground)[0], (*ArtScrollBarThumb)[0], *ArtScrollBarArrow, rectScrollbar1)); + + const SDL_Rect rectScrollbar2 = MakeSdlRect(uiPosition.x + 602, uiPosition.y + 199, 17, 209); + vecHubMainDialog.push_back(std::make_unique((*ArtScrollBarBackground)[0], (*ArtScrollBarThumb)[0], *ArtScrollBarArrow, rectScrollbar2)); + + const SDL_Rect rect7 = MakeSdlRect(uiPosition.x + 264 + 188, uiPosition.y + 335 + 103, 85, 35); + vecHubMainDialog.push_back(std::make_unique(_("Send"), &DialogActionOK, rect7)); + + const SDL_Rect rect8 = MakeSdlRect(uiPosition.x + 264 + 283, uiPosition.y + 335 + 103, 85, 35); + vecHubMainDialog.push_back(std::make_unique(_("Whisper"), &DialogActionOK, rect8)); + + const SDL_Rect rect9 = MakeSdlRect(uiPosition.x + 117, uiPosition.y + 433, 306, 30); + vecHubMainDialog.push_back(std::make_unique(_("Enter chat message"), ChatMessage, 128, false, rect9, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite)); +} + +void DrawChat() +{ + const Point uiPosition = GetUIRectangle().position + Displacement { 116, 161 }; + + const Surface &out = Surface(DiabloUiSurface()); + int displayLines = ChatLogLines.size(); + displayLines = std::min(100, displayLines); + int SkipLines = 0; + int lineHeight = 22; + for (int i = 0; i < displayLines; i++) { + if (i + SkipLines >= ChatLogLines.size()) + break; + MultiColoredText &text = ChatLogLines[ChatLogLines.size() - (i + SkipLines + 1)]; + const string_view line = text.text; + + std::vector args; + for (auto &x : text.colors) { + args.emplace_back(DrawStringFormatArg { x.text, x.color }); + } + DrawStringWithColors(out, line, args, { { (uiPosition.x + text.offset), uiPosition.y + i * lineHeight }, { 280 - text.offset * 2, lineHeight } }, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite, /*spacing=*/1, lineHeight); + } +} + +} // namespace devilution diff --git a/Source/DiabloUI/hub/chat.h b/Source/DiabloUI/hub/chat.h new file mode 100644 index 000000000..7ed12b0ea --- /dev/null +++ b/Source/DiabloUI/hub/chat.h @@ -0,0 +1,9 @@ +#pragma once + +namespace devilution { + +void hubmain_Init(); +void hubmain_GameSelection_Init(); +void DrawChat(); + +} // namespace devilution diff --git a/Source/DiabloUI/hub/create.cpp b/Source/DiabloUI/hub/create.cpp new file mode 100644 index 000000000..d42663ece --- /dev/null +++ b/Source/DiabloUI/hub/create.cpp @@ -0,0 +1,51 @@ +#include "DiabloUI/hub/create.h" + +#include "DiabloUI/hub/hub.h" +#include "engine/load_pcx.hpp" + +namespace devilution { + +namespace { + +OptionalOwnedClxSpriteList CreateButton; + +void DialogActionOK() +{ +} + +} // namespace + +void HubLoadCreate() +{ + Layout = LoadPcx("ui_art\\creat_bg", /*transparentColor=*/0); + CreateButton = LoadPcxSpriteList("ui_art\\diffbtns", -68); +} + +void HubInitCreate() +{ + const Point uiPosition = GetUIRectangle().position; + + const SDL_Rect rect0 = MakeSdlRect(uiPosition.x + 17, uiPosition.y + 154, 274, 26); + vecHubMainDialog.push_back(std::make_unique(_("Difficulty").data(), rect0, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite | UiFlags::AlignCenter, -1)); + + vecHubMainDialog.push_back(std::make_unique((*CreateButton)[1], MakeSdlRect(uiPosition.x + 115, uiPosition.y + 187, 85, 68))); + const SDL_Rect rect1 = MakeSdlRect(uiPosition.x + 115, uiPosition.y + 227, 85, 26); + vecHubMainDialog.push_back(std::make_unique(_("Normal").data(), rect1, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite | UiFlags::AlignCenter, -2)); + vecHubMainDialog.push_back(std::make_unique((*CreateButton)[2], MakeSdlRect(uiPosition.x + 62, uiPosition.y + 263, 85, 68))); + const SDL_Rect rect2 = MakeSdlRect(uiPosition.x + 62, uiPosition.y + 303, 85, 26); + vecHubMainDialog.push_back(std::make_unique(_("Nightmare").data(), rect2, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite | UiFlags::AlignCenter, -2)); + vecHubMainDialog.push_back(std::make_unique((*CreateButton)[4], MakeSdlRect(uiPosition.x + 167, uiPosition.y + 263, 85, 68))); + const SDL_Rect rect3 = MakeSdlRect(uiPosition.x + 167, uiPosition.y + 303, 85, 26); + vecHubMainDialog.push_back(std::make_unique(_("Hell").data(), rect3, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite | UiFlags::AlignCenter, -2)); + + const SDL_Rect rect4 = MakeSdlRect(uiPosition.x + 26, uiPosition.y + 340, 262, 110); + vecHubMainDialog.push_back(std::make_unique(_("Normal Difficulty\nThis is where a starting\ncharacter should begin the quest\nto defeat Diablo.").data(), rect4, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite, -1, 19)); + + const SDL_Rect rect7 = MakeSdlRect(uiPosition.x + 264 + 188, uiPosition.y + 335 + 103, 85, 35); + vecHubMainDialog.push_back(std::make_unique(_("OK"), &DialogActionOK, rect7)); + + const SDL_Rect rect8 = MakeSdlRect(uiPosition.x + 264 + 283, uiPosition.y + 335 + 103, 85, 35); + vecHubMainDialog.push_back(std::make_unique(_("Cancel"), &DialogActionOK, rect8)); +} + +} // namespace devilution diff --git a/Source/DiabloUI/hub/create.h b/Source/DiabloUI/hub/create.h new file mode 100644 index 000000000..cb9bafa73 --- /dev/null +++ b/Source/DiabloUI/hub/create.h @@ -0,0 +1,8 @@ +#pragma once + +namespace devilution { + +void HubLoadCreate(); +void HubInitCreate(); + +} // namespace devilution diff --git a/Source/DiabloUI/hub/friends.cpp b/Source/DiabloUI/hub/friends.cpp new file mode 100644 index 000000000..e84c3542b --- /dev/null +++ b/Source/DiabloUI/hub/friends.cpp @@ -0,0 +1,22 @@ +#include "DiabloUI/hub/friends.h" + +#include "DiabloUI/hub/hub.h" +#include "engine/load_pcx.hpp" + +namespace devilution { + +namespace { + +} // namespace + +void HubLoadFriends() +{ + Layout = LoadPcx("ui_art\\bnselchn", /*transparentColor=*/0); +} + +void HubInitFriends() +{ + const Point uiPosition = GetUIRectangle().position; +} + +} // namespace devilution diff --git a/Source/DiabloUI/hub/friends.h b/Source/DiabloUI/hub/friends.h new file mode 100644 index 000000000..2ef641329 --- /dev/null +++ b/Source/DiabloUI/hub/friends.h @@ -0,0 +1,8 @@ +#pragma once + +namespace devilution { + +void HubLoadFriends(); +void HubInitFriends(); + +} // namespace devilution diff --git a/Source/DiabloUI/hub/hub.cpp b/Source/DiabloUI/hub/hub.cpp new file mode 100644 index 000000000..e6ad30ba0 --- /dev/null +++ b/Source/DiabloUI/hub/hub.cpp @@ -0,0 +1,221 @@ +#include "DiabloUI/hub/hub.h" + +#include + +#include "DiabloUI/button.h" +#include "DiabloUI/diabloui.h" +#include "DiabloUI/dialogs.h" +#include "DiabloUI/hero/selhero.h" +#include "DiabloUI/hub/chat.h" +#include "DiabloUI/hub/create.h" +#include "DiabloUI/hub/friends.h" +#include "DiabloUI/hub/join.h" +#include "DiabloUI/scrollbar.h" +#include "DiabloUI/selok.h" +#include "config.h" +#include "control.h" +#include "engine/assets.hpp" +#include "engine/load_clx.hpp" +#include "menu.h" +#include "options.h" +#include "storm/storm_net.hpp" +#include "utils/language.h" +#include "utils/str_cat.hpp" +#include "utils/utf8.hpp" + +namespace devilution { + +bool hubmain_endMenu; +std::vector> vecHubMainDialog; +OptionalOwnedClxSpriteList Layout; + +namespace { + +enum class HubPanels : uint8_t { + Chat, + Friends, + Create, + Join, +}; + +HubPanels HubPanel = HubPanels::Join; + +OptionalOwnedClxSpriteList PlayerIcons; +OptionalOwnedClxSpriteList PlayerSpawnIcon; +OptionalOwnedClxSpriteList PlayerLevelFont; +OptionalOwnedClxSpriteList LagGreen; +OptionalOwnedClxSpriteList LagYellow; +OptionalOwnedClxSpriteList LagRed; + +void LoadHubPlayerGraphics() +{ + PlayerIcons = LoadPcxSpriteList("ui_art\\heroport", -14); + PlayerSpawnIcon = LoadPcx("ui_art\\spwnport"); + PlayerLevelFont = LoadPcxSpriteList("ui_art\\heronum", 10); + LagGreen = LoadPcx("ui_art\\greenlag"); + LagYellow = LoadPcx("ui_art\\yellolag"); + LagRed = LoadPcx("ui_art\\redlag"); +} + +std::vector> vecHubBackground; + +void hub_Init() +{ + LoadDialogButtonGraphics(); + LoadHubScrollBar(); + + LoadBackgroundArt("ui_art\\bnconnbg"); + ArtBackgroundWidescreen = LoadOptionalClx("ui_art\\bnconnbgw.clx"); + + uint8_t transparentColor = 250; + AssetRef ref = FindAsset("ui_art\\xsmlogo"); + if (ref.ok() && ref.size() == 167723) + transparentColor = 32; + ArtLogo = LoadPcxSpriteList("ui_art\\xsmlogo", /*numFrames=*/15, transparentColor); + + switch (HubPanel) { + case HubPanels::Chat: + hubmain_Init(); + break; + case HubPanels::Friends: + break; + case HubPanels::Create: + HubLoadCreate(); + break; + case HubPanels::Join: + HubLoadJoin(); + break; + } + + LoadHubPlayerGraphics(); +} + +void hubmain_Free() +{ + FreeDialogButtonGraphics(); + ArtBackground = std::nullopt; + ArtBackgroundWidescreen = std::nullopt; + Layout = std::nullopt; + vecHubMainDialog.clear(); +} + +bool IsKnownHeroType(uint32_t gameMode, HeroClass heroClass) +{ + if (gameMode != GameIdDiabloFull && gameMode != GameIdHellfireFull) + return false; + + return heroClass == HeroClass::Warrior + || heroClass == HeroClass::Rogue + || heroClass == HeroClass::Sorcerer; +} + +} // namespace + +void LoadHubScrollBar() +{ + ScrollBarWidth = 17; + ScrollBarArrowFrame_UP_ACTIVE = 2; + ScrollBarArrowFrame_UP = 0; + ScrollBarArrowFrame_DOWN_ACTIVE = 3; + ScrollBarArrowFrame_DOWN = 1; + ArtScrollBarArrow = LoadPcxSpriteList("ui_art\\scrlarrw", 4); + ArtScrollBarBackground = LoadPcx("ui_art\\scrlbar"); + ArtScrollBarThumb = LoadPcx("ui_art\\scrlthmb"); +} + +void UiHubPlacePlayerIcon(Point position, uint32_t gameMode, const PlayerInfo &player) +{ + ClxSprite sprite = (*PlayerSpawnIcon)[0]; + if (IsKnownHeroType(gameMode, player.heroClass)) { + int level = player.diabloKillLevel * 3; + sprite = (*PlayerIcons)[level + static_cast(player.heroClass)]; + } + const SDL_Rect rect0 = MakeSdlRect(position.x, position.y, 0, 0); + vecHubMainDialog.push_back(std::make_unique(sprite, rect0)); + + const SDL_Rect rect1 = MakeSdlRect(position.x + 20, position.y + 5, 0, 0); + vecHubMainDialog.push_back(std::make_unique((*PlayerLevelFont)[player.level % 10], rect1)); + if (player.level > 9) { + const SDL_Rect rect2 = MakeSdlRect(position.x + 14, position.y + 5, 0, 0); + vecHubMainDialog.push_back(std::make_unique((*PlayerLevelFont)[player.level / 10], rect2)); + } +} + +void UiHubPlaceLatencyMeter(int latency, Point position) +{ + int bars = latency / 50 + 1; + bars = std::min(bars, 6); + + ClxSprite lagSprite = (*LagRed)[0]; + if (bars <= 2) + lagSprite = (*LagGreen)[0]; + else if (bars <= 4) + lagSprite = (*LagYellow)[0]; + + const SDL_Rect rect4 = MakeSdlRect(position.x, position.y, 3 * bars, 11); + vecHubMainDialog.push_back(std::make_unique(lagSprite, rect4)); +} + +void hubmain_GameSelection_Focus(int value) +{ +} + +void hubmain_Diff_Select(int value) +{ +} + +void hubmain_GameSelection_Esc() +{ + UiInitList_clear(); + hubmain_endMenu = true; +} + +void hub_GameSelection_Init() +{ + const Point uiPosition = GetUIRectangle().position; + + const SDL_Rect rect0 = MakeSdlRect(0, uiPosition.y, 0, 0); + vecHubBackground.push_back(std::make_unique((*ArtBackground)[0], rect0, UiFlags::AlignCenter)); + vecHubBackground.push_back(std::make_unique((*ArtBackgroundWidescreen)[0], rect0, UiFlags::AlignCenter)); + + vecHubBackground.push_back(std::make_unique(*ArtLogo, MakeSdlRect(uiPosition.x, uiPosition.y, 0, 0))); + + const SDL_Rect rect1 = MakeSdlRect(uiPosition.x, uiPosition.y + (*ArtBackground)[0].height() - (*Layout)[0].height(), (*Layout)[0].width(), (*Layout)[0].height()); + vecHubBackground.push_back(std::make_unique((*Layout)[0], rect1, UiFlags::AlignCenter)); + + switch (HubPanel) { + case HubPanels::Chat: + hubmain_GameSelection_Init(); + break; + case HubPanels::Friends: + break; + case HubPanels::Create: + HubInitCreate(); + break; + case HubPanels::Join: + HubInitJoin(); + break; + } + + UiInitList(hubmain_GameSelection_Focus, hubmain_Diff_Select, hubmain_GameSelection_Esc, vecHubMainDialog, true); +} + +bool UiHubMain() +{ + hub_Init(); + hub_GameSelection_Init(); + + hubmain_endMenu = false; + + while (!hubmain_endMenu) { + UiClearScreen(); + UiRenderItems(vecHubBackground); + if (HubPanel == HubPanels::Chat) + DrawChat(); + UiPollAndRender(); + } + hubmain_Free(); + + return true; +} +} // namespace devilution diff --git a/Source/DiabloUI/hub/hub.h b/Source/DiabloUI/hub/hub.h new file mode 100644 index 000000000..6b1e65e34 --- /dev/null +++ b/Source/DiabloUI/hub/hub.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "DiabloUI/diabloui.h" +#include "engine/point.hpp" + +namespace devilution { + +struct PlayerInfo { + std::string name; + HeroClass heroClass; + uint8_t level; + uint8_t diabloKillLevel; + uint32_t gameMode; + int latency; +}; + +extern std::vector> vecHubMainDialog; +extern OptionalOwnedClxSpriteList Layout; + +void selgame_GameSelection_Init(); +void selgame_GameSelection_Focus(int value); +void selgame_GameSelection_Esc(); +void selgame_Diff_Select(int value); +void LoadHubScrollBar(); +void UiHubPlacePlayerIcon(Point position, uint32_t gameMode, const PlayerInfo &player); +void UiHubPlaceLatencyMeter(int latency, Point position); + +} // namespace devilution diff --git a/Source/DiabloUI/hub/join.cpp b/Source/DiabloUI/hub/join.cpp new file mode 100644 index 000000000..e78640818 --- /dev/null +++ b/Source/DiabloUI/hub/join.cpp @@ -0,0 +1,135 @@ +#include "DiabloUI/hub/join.h" + +#include "DiabloUI/hub/hub.h" +#include "DiabloUI/scrollbar.h" +#include "engine/load_pcx.hpp" + +namespace devilution { + +namespace { + +void DialogActionOK() +{ +} + +std::vector PlayerList; + +std::string relativeTime; +std::string difficultyString; +std::string GetDifficultyString(int difficulty) +{ + constexpr std::array DifficultyStrs = { N_("Normal"), N_("Nightmare"), N_("Hell") }; + const string_view difficultyStr = _(DifficultyStrs[difficulty]); + return fmt::format(fmt::runtime(_(/* TRANSLATORS: "Nightmare Difficulty" */ "{:s} Difficulty")), difficultyStr); +} + +char GameNameInput[32]; +char GamePasswordInput[32]; + +} // namespace + +void HubLoadJoin() +{ + Layout = LoadPcx("ui_art\\bnjoinbg", /*transparentColor=*/0); +} + +void HubInitJoin() +{ + const Point uiPosition = GetUIRectangle().position; + + const SDL_Rect rect0 = MakeSdlRect(uiPosition.x + 17, uiPosition.y + 150, 274, 26); + vecHubMainDialog.push_back(std::make_unique(_("Matching Public Games").data(), rect0, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite | UiFlags::AlignCenter, -1)); + + const SDL_Rect rect1 = MakeSdlRect(uiPosition.x + 312, uiPosition.y + 150, 311, 26); + vecHubMainDialog.push_back(std::make_unique(_("Join Game").data(), rect1, UiFlags::FontSizeDialog | UiFlags::ColorYellow | UiFlags::AlignCenter, -1)); + + const SDL_Rect rect2 = MakeSdlRect(uiPosition.x + 326, uiPosition.y + 185, 274, 26 * 2); + vecHubMainDialog.push_back(std::make_unique(_("To Join a game, enter the game\ninfomration below.").data(), rect2, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite, -1)); + + const SDL_Rect rect3 = MakeSdlRect(uiPosition.x + 326, uiPosition.y + 249, 86, 26); + vecHubMainDialog.push_back(std::make_unique(_("Name:").data(), rect3, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite | UiFlags::AlignRight, -1)); + const SDL_Rect rect4 = MakeSdlRect(uiPosition.x + 421, uiPosition.y + 250, 192, 29); + vecHubMainDialog.push_back(std::make_unique(_("Enter game name"), GameNameInput, 32, false, rect4, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite)); + + const SDL_Rect rect5 = MakeSdlRect(uiPosition.x + 326, uiPosition.y + 294, 86, 26); + vecHubMainDialog.push_back(std::make_unique(_("Password:").data(), rect5, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite | UiFlags::AlignRight, -1)); + const SDL_Rect rect6 = MakeSdlRect(uiPosition.x + 421, uiPosition.y + 295, 192, 29); + vecHubMainDialog.push_back(std::make_unique(_("Enter game password"), GamePasswordInput, 32, false, rect6, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite)); + + const SDL_Rect rectScrollbar = MakeSdlRect(uiPosition.x + 269, uiPosition.y + 189, 17, 274); + vecHubMainDialog.push_back(std::make_unique((*ArtScrollBarBackground)[0], (*ArtScrollBarThumb)[0], *ArtScrollBarArrow, rectScrollbar)); + + UiHubPlaceLatencyMeter(75, { uiPosition.x + 245, uiPosition.y + 195 }); + + const SDL_Rect rect7 = MakeSdlRect(uiPosition.x + 27, uiPosition.y + 186, 218, 26); + vecHubMainDialog.push_back(std::make_unique("CWEJZ", rect7, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite, -1)); + + const Point detailsPosition = uiPosition + Displacement { 326, 342 }; + int yOffset = 0; + + difficultyString = GetDifficultyString(1); + const SDL_Rect rect8 = MakeSdlRect(detailsPosition.x, detailsPosition.y + yOffset - 6, 149, 18); + vecHubMainDialog.push_back(std::make_unique(difficultyString.data(), rect8, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite, -1)); + yOffset += 16; + + int seconds = 76 * 60; + int hours = seconds / 3600; + int minutes = (seconds % 3600) / 60; + relativeTime = fmt::format(fmt::runtime(ngettext("{:d} minute", "{:d} minutes", minutes)), minutes); + if (hours > 0) { + if (minutes > 0) { + relativeTime = fmt::format(fmt::runtime(ngettext("{:d} minute", "{:d} minutes", minutes)), minutes); + relativeTime = fmt::format(fmt::runtime(ngettext( + /* TRANSLATORS: {:s} the translated minuts (3 minuts).*/ + "Time: {:d} hour and {:s}", + "Time: {:d} hours and {:s}", + hours)), + hours, relativeTime); + } else { + relativeTime = fmt::format(fmt::runtime(ngettext("Time: {:d} hour", "Time: {:d} hours", hours)), hours); + } + } else { + relativeTime = fmt::format(fmt::runtime(_("Time: {:s}")), relativeTime); + } + const SDL_Rect rect9 = MakeSdlRect(detailsPosition.x, detailsPosition.y + yOffset - 6, 149, 18); + vecHubMainDialog.push_back(std::make_unique(relativeTime.data(), rect9, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite, -1)); + yOffset += 16; + + const SDL_Rect rect10 = MakeSdlRect(detailsPosition.x, detailsPosition.y + yOffset - 6, 51, 18); + vecHubMainDialog.push_back(std::make_unique(_("Players:").data(), rect10, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite, -1)); + + PlayerList.emplace_back(PlayerInfo { "KPhoenix", HeroClass::Warrior, 35, 2, GameIdDiabloFull, 0 }); + PlayerList.emplace_back(PlayerInfo { "AJenbo", HeroClass::Rogue, 23, 1, GameIdDiabloFull, 0 }); + PlayerList.emplace_back(PlayerInfo { "glebm", HeroClass::Sorcerer, 19, 0, GameIdDiabloFull, 0 }); + + for (auto &player : PlayerList) { + UiHubPlacePlayerIcon({ detailsPosition.x + 52, detailsPosition.y + yOffset }, GameIdDiabloFull, player); + const SDL_Rect rect = MakeSdlRect(detailsPosition.x + 52 + 30, detailsPosition.y + yOffset - 6, 149 - 30, 18); + vecHubMainDialog.push_back(std::make_unique(player.name.data(), rect, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite, -1)); + + yOffset += 16; + } + + const Point detailsPosition2 = detailsPosition + Displacement { 184, 0 }; + yOffset = 0; + + const SDL_Rect rect11 = MakeSdlRect(detailsPosition2.x, detailsPosition2.y + yOffset - 6, 149, 18); + vecHubMainDialog.push_back(std::make_unique(_("Run in Town").data(), rect11, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite, -1)); + yOffset += 16; + + const SDL_Rect rect12 = MakeSdlRect(detailsPosition2.x, detailsPosition2.y + yOffset - 6, 149, 18); + vecHubMainDialog.push_back(std::make_unique(_("Theo Quest").data(), rect12, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite, -1)); + yOffset += 16; + + const SDL_Rect rect13 = MakeSdlRect(detailsPosition2.x, detailsPosition2.y + yOffset - 6, 149, 18); + vecHubMainDialog.push_back(std::make_unique(_("Friendly Fire").data(), rect13, UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite, -1)); + yOffset += 16; + + const SDL_Rect rect14 = MakeSdlRect(uiPosition.x + 264 + 188, uiPosition.y + 335 + 103, 85, 35); + vecHubMainDialog.push_back(std::make_unique(_("OK"), &DialogActionOK, rect14)); + + const SDL_Rect rect15 = MakeSdlRect(uiPosition.x + 264 + 283, uiPosition.y + 335 + 103, 85, 35); + vecHubMainDialog.push_back(std::make_unique(_("Cancel"), &DialogActionOK, rect15)); +} + +} // namespace devilution diff --git a/Source/DiabloUI/hub/join.h b/Source/DiabloUI/hub/join.h new file mode 100644 index 000000000..872f2750f --- /dev/null +++ b/Source/DiabloUI/hub/join.h @@ -0,0 +1,8 @@ +#pragma once + +namespace devilution { + +void HubLoadJoin(); +void HubInitJoin(); + +} // namespace devilution diff --git a/Source/DiabloUI/scrollbar.cpp b/Source/DiabloUI/scrollbar.cpp index 7ca5d587b..d80518738 100644 --- a/Source/DiabloUI/scrollbar.cpp +++ b/Source/DiabloUI/scrollbar.cpp @@ -7,9 +7,19 @@ namespace devilution { OptionalOwnedClxSpriteList ArtScrollBarBackground; OptionalOwnedClxSpriteList ArtScrollBarThumb; OptionalOwnedClxSpriteList ArtScrollBarArrow; +Uint16 ScrollBarWidth; +uint16_t ScrollBarArrowFrame_UP_ACTIVE; +uint16_t ScrollBarArrowFrame_UP; +uint16_t ScrollBarArrowFrame_DOWN_ACTIVE; +uint16_t ScrollBarArrowFrame_DOWN; void LoadScrollBar() { + ScrollBarWidth = 25; + ScrollBarArrowFrame_UP_ACTIVE = 0; + ScrollBarArrowFrame_UP = 1; + ScrollBarArrowFrame_DOWN_ACTIVE = 2; + ScrollBarArrowFrame_DOWN = 3; ArtScrollBarBackground = LoadPcx("ui_art\\sb_bg"); ArtScrollBarThumb = LoadPcx("ui_art\\sb_thumb"); ArtScrollBarArrow = LoadPcxSpriteList("ui_art\\sb_arrow", 4); diff --git a/Source/DiabloUI/scrollbar.h b/Source/DiabloUI/scrollbar.h index 1bfddcfac..de2da1a58 100644 --- a/Source/DiabloUI/scrollbar.h +++ b/Source/DiabloUI/scrollbar.h @@ -11,23 +11,19 @@ namespace devilution { extern OptionalOwnedClxSpriteList ArtScrollBarBackground; extern OptionalOwnedClxSpriteList ArtScrollBarThumb; extern OptionalOwnedClxSpriteList ArtScrollBarArrow; -constexpr Uint16 ScrollBarBgWidth = 25; +extern Uint16 ScrollBarWidth; -enum ScrollBarArrowFrame : uint8_t { - ScrollBarArrowFrame_UP_ACTIVE, - ScrollBarArrowFrame_UP, - ScrollBarArrowFrame_DOWN_ACTIVE, - ScrollBarArrowFrame_DOWN, -}; - -constexpr Uint16 ScrollBarArrowWidth = 25; +extern uint16_t ScrollBarArrowFrame_UP_ACTIVE; +extern uint16_t ScrollBarArrowFrame_UP; +extern uint16_t ScrollBarArrowFrame_DOWN_ACTIVE; +extern uint16_t ScrollBarArrowFrame_DOWN; inline SDL_Rect UpArrowRect(const UiScrollbar &bar) { return MakeSdlRect( bar.m_rect.x, bar.m_rect.y, - ScrollBarArrowWidth, + ScrollBarWidth, bar.m_arrow[0].height()); } @@ -36,7 +32,7 @@ inline SDL_Rect DownArrowRect(const UiScrollbar &bar) return MakeSdlRect( bar.m_rect.x, bar.m_rect.y + bar.m_rect.h - bar.m_arrow[0].height(), - ScrollBarArrowWidth, + ScrollBarWidth, bar.m_arrow[0].height()); } @@ -50,7 +46,7 @@ inline SDL_Rect BarRect(const UiScrollbar &bar) return MakeSdlRect( bar.m_rect.x, bar.m_rect.y + bar.m_arrow[0].height(), - ScrollBarArrowWidth, + ScrollBarWidth, BarHeight(bar)); } diff --git a/Source/DiabloUI/ui_flags.hpp b/Source/DiabloUI/ui_flags.hpp index 9c3c9d33a..f9d9811a3 100644 --- a/Source/DiabloUI/ui_flags.hpp +++ b/Source/DiabloUI/ui_flags.hpp @@ -21,6 +21,7 @@ enum class UiFlags : uint32_t { ColorUiSilver = 1 << 7, ColorUiGoldDark = 1 << 8, ColorUiSilverDark = 1 << 9, + ColorUiGreen = 1 << 6, ColorDialogWhite = 1 << 10, ColorYellow = 1 << 11, ColorGold = 1 << 12, diff --git a/Source/multi.cpp b/Source/multi.cpp index 9d8498b26..31a58fe86 100644 --- a/Source/multi.cpp +++ b/Source/multi.cpp @@ -440,6 +440,8 @@ bool InitSingle(GameData *gameData) return true; } +extern int provider; + bool InitMulti(GameData *gameData) { Players.resize(MAX_PLRS); @@ -452,8 +454,13 @@ bool InitMulti(GameData *gameData) } RegisterNetEventHandlers(); - if (UiSelectGame(gameData, &playerId)) - break; + if (true) { + if (UiHubMain()) + break; + } else { + if (UiSelectGame(gameData, &playerId)) + break; + } gbSelectProvider = true; }