diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 164dd97cf..b711ec798 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -6,7 +6,6 @@ set(libdevilutionx_SRCS appfat.cpp automap.cpp capture.cpp - control.cpp cursor.cpp dead.cpp debug.cpp @@ -38,6 +37,13 @@ set(libdevilutionx_SRCS towners.cpp track.cpp + control/control_chat.cpp + control/control_chat_commands.cpp + control/control_flasks.cpp + control/control_gold.cpp + control/control_infobox.cpp + control/control_panel.cpp + controls/axis_direction.cpp controls/controller_motion.cpp controls/controller.cpp diff --git a/Source/automap.cpp b/Source/automap.cpp index 342c019dc..745e520fb 100644 --- a/Source/automap.cpp +++ b/Source/automap.cpp @@ -10,7 +10,7 @@ #include -#include "control.h" +#include "control/control.hpp" #include "engine/load_file.hpp" #include "engine/palette.h" #include "engine/render/automap_render.hpp" diff --git a/Source/control.cpp b/Source/control.cpp deleted file mode 100644 index f082171e1..000000000 --- a/Source/control.cpp +++ /dev/null @@ -1,2108 +0,0 @@ -/** - * @file control.cpp - * - * Implementation of the character and main control panels - */ -#include "control.h" - -#include -#include -#include -#include -#include -#include -#include - -#ifdef USE_SDL3 -#include -#include -#include -#include -#else -#include - -#ifdef USE_SDL1 -#include "utils/sdl2_to_1_2_backports.h" -#endif -#endif - -#include - -#include "DiabloUI/text_input.hpp" -#include "automap.h" -#include "controls/control_mode.hpp" -#include "controls/modifier_hints.h" -#include "controls/plrctrls.h" -#include "cursor.h" -#include "diablo_msg.hpp" -#include "engine/backbuffer_state.hpp" -#include "engine/clx_sprite.hpp" -#include "engine/load_cel.hpp" -#include "engine/render/clx_render.hpp" -#include "engine/render/primitive_render.hpp" -#include "engine/render/text_render.hpp" -#include "engine/trn.hpp" -#include "gamemenu.h" -#include "headless_mode.hpp" -#include "inv.h" -#include "inv_iterators.hpp" -#include "levels/setmaps.h" -#include "levels/trigs.h" -#include "lighting.h" -#include "minitext.h" -#include "missiles.h" -#include "options.h" -#include "panels/charpanel.hpp" -#include "panels/console.hpp" -#include "panels/mainpanel.hpp" -#include "panels/partypanel.hpp" -#include "panels/spell_book.hpp" -#include "panels/spell_icons.hpp" -#include "panels/spell_list.hpp" -#include "pfile.h" -#include "playerdat.hpp" -#include "qol/stash.h" -#include "qol/xpbar.h" -#include "quick_messages.hpp" -#include "stores.h" -#include "storm/storm_net.hpp" -#include "towners.h" -#include "utils/algorithm/container.hpp" -#include "utils/format_int.hpp" -#include "utils/language.h" -#include "utils/log.hpp" -#include "utils/parse_int.hpp" -#include "utils/screen_reader.hpp" -#include "utils/sdl_compat.h" -#include "utils/sdl_geometry.h" -#include "utils/sdl_ptrs.h" -#include "utils/status_macros.hpp" -#include "utils/str_case.hpp" -#include "utils/str_cat.hpp" -#include "utils/str_split.hpp" -#include "utils/string_or_view.hpp" -#include "utils/utf8.hpp" - -#ifdef _DEBUG -#include "debug.h" -#endif - -namespace devilution { - -bool DropGoldFlag; -TextInputCursorState GoldDropCursor; -char GoldDropText[21]; -namespace { -int8_t GoldDropInvIndex; -std::optional GoldDropInputState; -} // namespace - -bool CharPanelButton[4]; -bool LevelButtonDown; -bool CharPanelButtonActive; -UiFlags InfoColor; -int SpellbookTab; -bool ChatFlag; -bool SpellbookFlag; -bool CharFlag; -StringOrView InfoString; -StringOrView FloatingInfoString; -bool MainPanelFlag; -bool MainPanelButtonDown; -bool SpellSelectFlag; -Rectangle MainPanel; -Rectangle LeftPanel; -Rectangle RightPanel; -std::optional BottomBuffer; -OptionalOwnedClxSpriteList GoldBoxBuffer; - -const Rectangle &GetMainPanel() -{ - return MainPanel; -} -const Rectangle &GetLeftPanel() -{ - return LeftPanel; -} -const Rectangle &GetRightPanel() -{ - return RightPanel; -} -bool IsLeftPanelOpen() -{ - return CharFlag || QuestLogIsOpen || IsStashOpen; -} -bool IsRightPanelOpen() -{ - return invflag || SpellbookFlag; -} - -constexpr Size IncrementAttributeButtonSize { 41, 22 }; -/** Maps from attribute_id to the rectangle on screen used for attribute increment buttons. */ -Rectangle CharPanelButtonRect[4] = { - { { 137, 138 }, IncrementAttributeButtonSize }, - { { 137, 166 }, IncrementAttributeButtonSize }, - { { 137, 195 }, IncrementAttributeButtonSize }, - { { 137, 223 }, IncrementAttributeButtonSize } -}; - -constexpr Size WidePanelButtonSize { 71, 20 }; -constexpr Size PanelButtonSize { 33, 32 }; -/** Positions of panel buttons. */ -Rectangle MainPanelButtonRect[8] = { - // clang-format off - { { 9, 9 }, WidePanelButtonSize }, // char button - { { 9, 35 }, WidePanelButtonSize }, // quests button - { { 9, 75 }, WidePanelButtonSize }, // map button - { { 9, 101 }, WidePanelButtonSize }, // menu button - { { 560, 9 }, WidePanelButtonSize }, // inv button - { { 560, 35 }, WidePanelButtonSize }, // spells button - { { 87, 91 }, PanelButtonSize }, // chat button - { { 527, 91 }, PanelButtonSize }, // friendly fire button - // clang-format on -}; - -Rectangle LevelButtonRect = { { 40, -39 }, { 41, 22 } }; - -constexpr int BeltItems = 8; -constexpr Size BeltSize { (INV_SLOT_SIZE_PX + 1) * BeltItems, INV_SLOT_SIZE_PX }; -Rectangle BeltRect { { 205, 5 }, BeltSize }; - -Rectangle SpellButtonRect { { 565, 64 }, { 56, 56 } }; - -Rectangle FlaskTopRect { { 11, 3 }, { 62, 13 } }; -Rectangle FlaskBottomRect { { 0, 16 }, { 88, 69 } }; - -int MuteButtons = 3; -int MuteButtonPadding = 2; -Rectangle MuteButtonRect { { 172, 69 }, { 61, 16 } }; - -namespace { - -std::optional pLifeBuff; -std::optional pManaBuff; -OptionalOwnedClxSpriteList talkButtons; -OptionalOwnedClxSpriteList pDurIcons; -OptionalOwnedClxSpriteList multiButtons; -OptionalOwnedClxSpriteList pMainPanelButtons; - -enum panel_button_id : uint8_t { - PanelButtonCharinfo, - PanelButtonFirst = PanelButtonCharinfo, - PanelButtonQlog, - PanelButtonAutomap, - PanelButtonMainmenu, - PanelButtonInventory, - PanelButtonSpellbook, - PanelButtonSendmsg, - PanelButtonFriendly, - PanelButtonLast = PanelButtonFriendly, -}; - -bool MainPanelButtons[PanelButtonLast + 1]; -int TotalSpMainPanelButtons = 6; -int TotalMpMainPanelButtons = 8; -char TalkSave[8][MAX_SEND_STR_LEN]; -uint8_t TalkSaveIndex; -uint8_t NextTalkSave; -char TalkMessage[MAX_SEND_STR_LEN]; -bool TalkButtonsDown[3]; -int sgbPlrTalkTbl; -bool WhisperList[MAX_PLRS]; -int PanelPaddingHeight = 16; - -TextInputCursorState ChatCursor; -std::optional ChatInputState; - -/** Maps from panel_button_id to hotkey name. */ -const char *const PanBtnHotKey[8] = { "'c'", "'q'", N_("Tab"), N_("Esc"), "'i'", "'b'", N_("Enter"), nullptr }; -/** Maps from panel_button_id to panel button description. */ -const char *const PanBtnStr[8] = { - N_("Character Information"), - N_("Quests log"), - N_("Automap"), - N_("Main Menu"), - N_("Inventory"), - N_("Spell book"), - N_("Send Message"), - "" // Player attack -}; - -/** - * Draws the dome of the flask that protrudes above the panel top line. - * It draws a rectangle of fixed width 59 and height 'h' from the source buffer - * into the target buffer. - * @param out The target buffer. - * @param celBuf Buffer of the flask cel. - * @param targetPosition Target buffer coordinate. - */ -void DrawFlaskAbovePanel(const Surface &out, const Surface &celBuf, Point targetPosition) -{ - out.BlitFromSkipColorIndexZero(celBuf, MakeSdlRect(0, 0, celBuf.w(), celBuf.h()), targetPosition); -} - -/** - * @brief Draws the part of the life/mana flasks protruding above the bottom panel - * @see DrawFlaskLower() - * @param out The display region to draw to - * @param sourceBuffer A sprite representing the appropriate background/empty flask style - * @param offset X coordinate offset for where the flask should be drawn - * @param fillPer How full the flask is (a value from 0 to 81) - */ -void DrawFlaskUpper(const Surface &out, const Surface &sourceBuffer, int offset, int fillPer) -{ - const Rectangle &rect = FlaskTopRect; - const int emptyRows = std::clamp(81 - fillPer, 0, rect.size.height); - const int filledRows = rect.size.height - emptyRows; - - // Draw the empty part of the flask - DrawFlaskAbovePanel(out, - sourceBuffer.subregion(rect.position.x, rect.position.y, rect.size.width, rect.size.height), - GetMainPanel().position + Displacement { offset, -rect.size.height }); - - // Draw the filled part of the flask over the empty part - if (filledRows > 0) { - DrawFlaskAbovePanel(out, - BottomBuffer->subregion(offset, rect.position.y + emptyRows, rect.size.width, filledRows), - GetMainPanel().position + Displacement { offset, -rect.size.height + emptyRows }); - } -} - -/** - * Draws a section of the empty flask cel on top of the panel to create the illusion - * of the flask getting empty. This function takes a cel and draws a - * horizontal stripe of height (max-min) onto the given buffer. - * @param out Target buffer. - * @param celBuf Buffer of the flask cel. - * @param targetPosition Target buffer coordinate. - */ -void DrawFlaskOnPanel(const Surface &out, const Surface &celBuf, Point targetPosition) -{ - out.BlitFrom(celBuf, MakeSdlRect(0, 0, celBuf.w(), celBuf.h()), targetPosition); -} - -/** - * @brief Draws the part of the life/mana flasks inside the bottom panel - * @see DrawFlaskUpper() - * @param out The display region to draw to - * @param sourceBuffer A sprite representing the appropriate background/empty flask style - * @param offset X coordinate offset for where the flask should be drawn - * @param fillPer How full the flask is (a value from 0 to 80) - * @param drawFilledPortion Indicates whether to draw the filled portion of the flask - */ -void DrawFlaskLower(const Surface &out, const Surface &sourceBuffer, int offset, int fillPer, bool drawFilledPortion) -{ - const Rectangle &rect = FlaskBottomRect; - const int filledRows = std::clamp(fillPer, 0, rect.size.height); - const int emptyRows = rect.size.height - filledRows; - - // Draw the empty part of the flask - if (emptyRows > 0) { - DrawFlaskOnPanel(out, - sourceBuffer.subregion(rect.position.x, rect.position.y, rect.size.width, emptyRows), - GetMainPanel().position + Displacement { offset, 0 }); - } - - // Draw the filled part of the flask - if (drawFilledPortion && filledRows > 0) { - DrawFlaskOnPanel(out, - BottomBuffer->subregion(offset, rect.position.y + emptyRows, rect.size.width, filledRows), - GetMainPanel().position + Displacement { offset, emptyRows }); - } -} - -void SetMainPanelButtonDown(int btnId) -{ - MainPanelButtons[btnId] = true; - RedrawComponent(PanelDrawComponent::ControlButtons); - MainPanelButtonDown = true; -} - -void SetMainPanelButtonUp() -{ - RedrawComponent(PanelDrawComponent::ControlButtons); - MainPanelButtonDown = false; -} - -void SetPanelObjectPosition(UiPanels panel, Rectangle &button) -{ - button.position = GetPanelPosition(panel, button.position); -} - -void PrintInfo(const Surface &out) -{ - if (ChatFlag) - return; - - const int space[] = { 18, 12, 6, 3, 0 }; - Rectangle infoBox = InfoBoxRect; - - SetPanelObjectPosition(UiPanels::Main, infoBox); - - const auto newLineCount = static_cast(c_count(InfoString.str(), '\n')); - const int spaceIndex = std::min(4, newLineCount); - const int spacing = space[spaceIndex]; - const int lineHeight = 12 + spacing; - - // Adjusting the line height to add spacing between lines - // will also add additional space beneath the last line - // which throws off the vertical centering - infoBox.position.y += spacing / 2; - - SpeakText(InfoString); - - DrawString(out, InfoString, infoBox, - { - .flags = InfoColor | UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::KerningFitSpacing, - .spacing = 2, - .lineHeight = lineHeight, - }); -} - -Rectangle GetFloatingInfoRect(const int lineHeight, const int textSpacing) -{ - // Calculate the width and height of the floating info box - const std::string txt = std::string(FloatingInfoString); - - auto lines = SplitByChar(txt, '\n'); - const GameFontTables font = GameFont12; - int maxW = 0; - - for (const auto &line : lines) { - const int w = GetLineWidth(line, font, textSpacing, nullptr); - maxW = std::max(maxW, w); - } - - const auto lineCount = 1 + static_cast(c_count(FloatingInfoString.str(), '\n')); - const int totalH = lineCount * lineHeight; - - const Player &player = *InspectPlayer; - - // 1) Equipment (Rect position) - if (pcursinvitem >= INVITEM_HEAD && pcursinvitem < INVITEM_INV_FIRST) { - const int slot = pcursinvitem - INVITEM_HEAD; - static constexpr Point equipLocal[] = { - { 133, 59 }, - { 48, 205 }, - { 249, 205 }, - { 205, 60 }, - { 17, 160 }, - { 248, 160 }, - { 133, 160 }, - }; - - Point itemPosition = equipLocal[slot]; - auto &item = player.InvBody[slot]; - const Size frame = GetInvItemSize(item._iCurs + CURSOR_FIRSTITEM); - - if (slot == INVLOC_HAND_LEFT) { - itemPosition.x += frame.width == InventorySlotSizeInPixels.width - ? InventorySlotSizeInPixels.width - : 0; - itemPosition.y += frame.height == 3 * InventorySlotSizeInPixels.height - ? 0 - : -InventorySlotSizeInPixels.height; - } else if (slot == INVLOC_HAND_RIGHT) { - itemPosition.x += frame.width == InventorySlotSizeInPixels.width - ? (InventorySlotSizeInPixels.width - 1) - : 1; - itemPosition.y += frame.height == 3 * InventorySlotSizeInPixels.height - ? 0 - : -InventorySlotSizeInPixels.height; - } - - itemPosition.y++; // Align position to bottom left of the item graphic - itemPosition.x += frame.width / 2; // Align position to center of the item graphic - itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box - - const Point screen = GetPanelPosition(UiPanels::Inventory, itemPosition); - - return { { screen.x, screen.y }, { maxW, totalH } }; - } - - // 2) Inventory grid (Rect position) - if (pcursinvitem >= INVITEM_INV_FIRST && pcursinvitem < INVITEM_INV_FIRST + InventoryGridCells) { - const int itemIdx = pcursinvitem - INVITEM_INV_FIRST; - - for (int j = 0; j < InventoryGridCells; ++j) { - if (player.InvGrid[j] > 0 && player.InvGrid[j] - 1 == itemIdx) { - const Item &it = player.InvList[itemIdx]; - Point itemPosition = InvRect[j + SLOTXY_INV_FIRST].position; - - itemPosition.x += GetInventorySize(it).width * InventorySlotSizeInPixels.width / 2; // Align position to center of the item graphic - itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box - - const Point screen = GetPanelPosition(UiPanels::Inventory, itemPosition); - - return { { screen.x, screen.y }, { maxW, totalH } }; - } - } - } - - // 3) Belt (Rect position) - if (pcursinvitem >= INVITEM_BELT_FIRST && pcursinvitem < INVITEM_BELT_FIRST + MaxBeltItems) { - const int itemIdx = pcursinvitem - INVITEM_BELT_FIRST; - for (int i = 0; i < MaxBeltItems; ++i) { - if (player.SpdList[i].isEmpty()) - continue; - if (i != itemIdx) - continue; - - const Item &item = player.SpdList[i]; - Point itemPosition = InvRect[i + SLOTXY_BELT_FIRST].position; - - itemPosition.x += GetInventorySize(item).width * InventorySlotSizeInPixels.width / 2; // Align position to center of the item graphic - itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box - - const Point screen = GetMainPanel().position + Displacement { itemPosition.x, itemPosition.y }; - - return { { screen.x, screen.y }, { maxW, totalH } }; - } - } - - // 4) Stash (Rect position) - if (pcursstashitem != StashStruct::EmptyCell) { - for (auto slot : StashGridRange) { - auto itemId = Stash.GetItemIdAtPosition(slot); - if (itemId == StashStruct::EmptyCell) - continue; - if (itemId != pcursstashitem) - continue; - - const Item &item = Stash.stashList[itemId]; - Point itemPosition = GetStashSlotCoord(slot); - const Size itemGridSize = GetInventorySize(item); - - itemPosition.y += itemGridSize.height * (InventorySlotSizeInPixels.height + 1) - 1; // Align position to bottom left of the item graphic - itemPosition.x += itemGridSize.width * InventorySlotSizeInPixels.width / 2; // Align position to center of the item graphic - itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box - - return { { itemPosition.x, itemPosition.y }, { maxW, totalH } }; - } - } - - return { { 0, 0 }, { 0, 0 } }; -} - -int GetHoverSpriteHeight() -{ - if (pcursinvitem >= INVITEM_HEAD && pcursinvitem < INVITEM_INV_FIRST) { - auto &it = (*InspectPlayer).InvBody[pcursinvitem - INVITEM_HEAD]; - return GetInvItemSize(it._iCurs + CURSOR_FIRSTITEM).height + 1; - } - if (pcursinvitem >= INVITEM_INV_FIRST - && pcursinvitem < INVITEM_INV_FIRST + InventoryGridCells) { - const int idx = pcursinvitem - INVITEM_INV_FIRST; - auto &it = (*InspectPlayer).InvList[idx]; - return GetInventorySize(it).height * (InventorySlotSizeInPixels.height + 1) - - InventorySlotSizeInPixels.height; - } - if (pcursinvitem >= INVITEM_BELT_FIRST - && pcursinvitem < INVITEM_BELT_FIRST + MaxBeltItems) { - const int idx = pcursinvitem - INVITEM_BELT_FIRST; - auto &it = (*InspectPlayer).SpdList[idx]; - return GetInventorySize(it).height * (InventorySlotSizeInPixels.height + 1) - - InventorySlotSizeInPixels.height - 1; - } - if (pcursstashitem != StashStruct::EmptyCell) { - auto &it = Stash.stashList[pcursstashitem]; - return GetInventorySize(it).height * (InventorySlotSizeInPixels.height + 1); - } - return InventorySlotSizeInPixels.height; -} - -int ClampAboveOrBelow(int anchorY, int spriteH, int boxH, int pad, int linePad) -{ - const int yAbove = anchorY - spriteH - boxH - pad; - const int yBelow = anchorY + linePad / 2 + pad; - return (yAbove >= 0) ? yAbove : yBelow; -} - -void PrintFloatingInfo(const Surface &out) -{ - if (ChatFlag) - return; - if (FloatingInfoString.empty()) - return; - - const int verticalSpacing = 3; - const int lineHeight = 12 + verticalSpacing; - const int textSpacing = 2; - const int hPadding = 5; - const int vPadding = 4; - - Rectangle floatingInfoBox = GetFloatingInfoRect(lineHeight, textSpacing); - - // Prevent the floating info box from going off-screen horizontally - floatingInfoBox.position.x = std::clamp(floatingInfoBox.position.x, hPadding, GetScreenWidth() - (floatingInfoBox.size.width + hPadding)); - - const int spriteH = GetHoverSpriteHeight(); - const int anchorY = floatingInfoBox.position.y; - - // Prevent the floating info box from going off-screen vertically - floatingInfoBox.position.y = ClampAboveOrBelow(anchorY, spriteH, floatingInfoBox.size.height, vPadding, verticalSpacing); - - SpeakText(FloatingInfoString); - - for (int i = 0; i < 3; i++) - DrawHalfTransparentRectTo(out, floatingInfoBox.position.x - hPadding, floatingInfoBox.position.y - vPadding, floatingInfoBox.size.width + hPadding * 2, floatingInfoBox.size.height + vPadding * 2); - DrawHalfTransparentVerticalLine(out, { floatingInfoBox.position.x - hPadding - 1, floatingInfoBox.position.y - vPadding - 1 }, floatingInfoBox.size.height + (vPadding * 2) + 2, PAL16_GRAY + 10); - DrawHalfTransparentVerticalLine(out, { floatingInfoBox.position.x + hPadding + floatingInfoBox.size.width, floatingInfoBox.position.y - vPadding - 1 }, floatingInfoBox.size.height + (vPadding * 2) + 2, PAL16_GRAY + 10); - DrawHalfTransparentHorizontalLine(out, { floatingInfoBox.position.x - hPadding, floatingInfoBox.position.y - vPadding - 1 }, floatingInfoBox.size.width + (hPadding * 2), PAL16_GRAY + 10); - DrawHalfTransparentHorizontalLine(out, { floatingInfoBox.position.x - hPadding, floatingInfoBox.position.y + vPadding + floatingInfoBox.size.height }, floatingInfoBox.size.width + (hPadding * 2), PAL16_GRAY + 10); - - DrawString(out, FloatingInfoString, floatingInfoBox, - { - .flags = InfoColor | UiFlags::AlignCenter | UiFlags::VerticalCenter, - .spacing = textSpacing, - .lineHeight = lineHeight, - }); -} - -int CapStatPointsToAdd(int remainingStatPoints, const Player &player, CharacterAttribute attribute) -{ - const int pointsToReachCap = player.GetMaximumAttributeValue(attribute) - player.GetBaseAttributeValue(attribute); - - return std::min(remainingStatPoints, pointsToReachCap); -} - -int DrawDurIcon4Item(const Surface &out, Item &pItem, int x, int c) -{ - const int durabilityThresholdGold = 5; - const int durabilityThresholdRed = 2; - - if (pItem.isEmpty()) - return x; - if (pItem._iDurability > durabilityThresholdGold) - return x; - if (c == 0) { - switch (pItem._itype) { - case ItemType::Sword: - c = 1; - break; - case ItemType::Axe: - c = 5; - break; - case ItemType::Bow: - c = 6; - break; - case ItemType::Mace: - c = 4; - break; - case ItemType::Staff: - c = 7; - break; - case ItemType::Shield: - default: - c = 0; - break; - } - } - - // Calculate how much of the icon should be gold and red - const int height = (*pDurIcons)[c].height(); // Height of durability icon CEL - int partition = 0; - if (pItem._iDurability > durabilityThresholdRed) { - const int current = pItem._iDurability - durabilityThresholdRed; - partition = (height * current) / (durabilityThresholdGold - durabilityThresholdRed); - } - - // Draw icon - const int y = -17 + GetMainPanel().position.y; - if (partition > 0) { - const Surface stenciledBuffer = out.subregionY(y - partition, partition); - ClxDraw(stenciledBuffer, { x, partition }, (*pDurIcons)[c + 8]); // Gold icon - } - if (partition != height) { - const Surface stenciledBuffer = out.subregionY(y - height, height - partition); - ClxDraw(stenciledBuffer, { x, height }, (*pDurIcons)[c]); // Red icon - } - - return x - (*pDurIcons)[c].height() - 8; // Add in spacing for the next durability icon -} - -struct TextCmdItem { - const std::string text; - const std::string description; - const std::string requiredParameter; - std::string (*actionProc)(const std::string_view); -}; - -extern std::vector TextCmdList; - -std::string TextCmdHelp(const std::string_view parameter) -{ - if (parameter.empty()) { - std::string ret; - StrAppend(ret, _("Available Commands:")); - for (const TextCmdItem &textCmd : TextCmdList) { - StrAppend(ret, " ", _(textCmd.text)); - } - return ret; - } - auto textCmdIterator = c_find_if(TextCmdList, [&](const TextCmdItem &elem) { return elem.text == parameter; }); - if (textCmdIterator == TextCmdList.end()) - return StrCat(_("Command "), parameter, _(" is unknown.")); - auto &textCmdItem = *textCmdIterator; - if (textCmdItem.requiredParameter.empty()) - return StrCat(_("Description: "), _(textCmdItem.description), _("\nParameters: No additional parameter needed.")); - return StrCat(_("Description: "), _(textCmdItem.description), _("\nParameters: "), _(textCmdItem.requiredParameter)); -} - -void AppendArenaOverview(std::string &ret) -{ - for (int arena = SL_FIRST_ARENA; arena <= SL_LAST; arena++) { - StrAppend(ret, "\n", arena - SL_FIRST_ARENA + 1, " (", QuestLevelNames[arena], ")"); - } -} - -std::string TextCmdArena(const std::string_view parameter) -{ - std::string ret; - if (!gbIsMultiplayer) { - StrAppend(ret, _("Arenas are only supported in multiplayer.")); - return ret; - } - - if (parameter.empty()) { - StrAppend(ret, _("What arena do you want to visit?")); - AppendArenaOverview(ret); - return ret; - } - - const ParseIntResult parsedParam = ParseInt(parameter, /*min=*/0); - const _setlevels arenaLevel = parsedParam.has_value() ? static_cast<_setlevels>(parsedParam.value() - 1 + SL_FIRST_ARENA) : _setlevels::SL_NONE; - if (!IsArenaLevel(arenaLevel)) { - StrAppend(ret, _("Invalid arena-number. Valid numbers are:")); - AppendArenaOverview(ret); - return ret; - } - - if (!MyPlayer->isOnLevel(0) && !MyPlayer->isOnArenaLevel()) { - StrAppend(ret, _("To enter a arena, you need to be in town or another arena.")); - return ret; - } - - setlvltype = GetArenaLevelType(arenaLevel); - StartNewLvl(*MyPlayer, WM_DIABSETLVL, arenaLevel); - return ret; -} - -std::string TextCmdArenaPot(const std::string_view parameter) -{ - std::string ret; - if (!gbIsMultiplayer) { - StrAppend(ret, _("Arenas are only supported in multiplayer.")); - return ret; - } - const int numPots = ParseInt(parameter, /*min=*/1).value_or(1); - - Player &myPlayer = *MyPlayer; - - for (int potNumber = numPots; potNumber > 0; potNumber--) { - Item item {}; - InitializeItem(item, IDI_ARENAPOT); - GenerateNewSeed(item); - item.updateRequiredStatsCacheForPlayer(myPlayer); - - if (!AutoPlaceItemInBelt(myPlayer, item, true, true) && !AutoPlaceItemInInventory(myPlayer, item, true)) { - break; // inventory is full - } - } - - return ret; -} - -std::string TextCmdInspect(const std::string_view parameter) -{ - std::string ret; - if (!gbIsMultiplayer) { - StrAppend(ret, _("Inspecting only supported in multiplayer.")); - return ret; - } - - if (parameter.empty()) { - StrAppend(ret, _("Stopped inspecting players.")); - InspectPlayer = MyPlayer; - return ret; - } - - const std::string param = AsciiStrToLower(parameter); - auto it = c_find_if(Players, [¶m](const Player &player) { - return AsciiStrToLower(player._pName) == param; - }); - if (it == Players.end()) { - it = c_find_if(Players, [¶m](const Player &player) { - return AsciiStrToLower(player._pName).find(param) != std::string::npos; - }); - } - if (it == Players.end()) { - StrAppend(ret, _("No players found with such a name")); - return ret; - } - - Player &player = *it; - InspectPlayer = &player; - StrAppend(ret, _("Inspecting player: ")); - StrAppend(ret, player._pName); - OpenCharPanel(); - if (!SpellbookFlag) - invflag = true; - RedrawEverything(); - return ret; -} - -bool IsQuestEnabled(const Quest &quest) -{ - switch (quest._qidx) { - case Q_FARMER: - return gbIsHellfire && !sgGameInitInfo.bCowQuest; - case Q_JERSEY: - return gbIsHellfire && sgGameInitInfo.bCowQuest; - case Q_GIRL: - return gbIsHellfire && sgGameInitInfo.bTheoQuest; - case Q_CORNSTN: - return gbIsHellfire && !gbIsMultiplayer; - case Q_GRAVE: - case Q_DEFILER: - case Q_NAKRUL: - return gbIsHellfire; - case Q_TRADER: - return false; - default: - return quest._qactive != QUEST_NOTAVAIL; - } -} - -std::string TextCmdLevelSeed(const std::string_view parameter) -{ - const std::string_view levelType = setlevel ? "set level" : "dungeon level"; - - char gameId[] = { - static_cast((sgGameInitInfo.programid >> 24) & 0xFF), - static_cast((sgGameInitInfo.programid >> 16) & 0xFF), - static_cast((sgGameInitInfo.programid >> 8) & 0xFF), - static_cast(sgGameInitInfo.programid & 0xFF), - '\0' - }; - - const std::string_view mode = gbIsMultiplayer ? "MP" : "SP"; - const std::string_view questPool = UseMultiplayerQuests() ? "MP" : "Full"; - - uint32_t questFlags = 0; - for (const Quest &quest : Quests) { - questFlags <<= 1; - if (IsQuestEnabled(quest)) - questFlags |= 1; - } - - return StrCat( - "Seedinfo for ", levelType, " ", currlevel, "\n", - "seed: ", DungeonSeeds[currlevel], "\n", -#ifdef _DEBUG - "Mid1: ", glMid1Seed[currlevel], "\n", - "Mid2: ", glMid2Seed[currlevel], "\n", - "Mid3: ", glMid3Seed[currlevel], "\n", - "End: ", glEndSeed[currlevel], "\n", -#endif - "\n", - gameId, " ", mode, "\n", - questPool, " quests: ", questFlags, "\n", - "Storybook: ", DungeonSeeds[16]); -} - -std::string TextCmdPing(const std::string_view parameter) -{ - std::string ret; - const std::string param = AsciiStrToLower(parameter); - auto it = c_find_if(Players, [¶m](const Player &player) { - return AsciiStrToLower(player._pName) == param; - }); - if (it == Players.end()) { - it = c_find_if(Players, [¶m](const Player &player) { - return AsciiStrToLower(player._pName).find(param) != std::string::npos; - }); - } - if (it == Players.end()) { - StrAppend(ret, _("No players found with such a name")); - return ret; - } - - Player &player = *it; - DvlNetLatencies latencies = DvlNet_GetLatencies(player.getId()); - - StrAppend(ret, fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} means: Character Name */ "Latency statistics for {:s}:")), player.name())); - - StrAppend(ret, "\n", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Echo latency: {:d} ms")), latencies.echoLatency)); - - if (latencies.providerLatency) { - if (latencies.isRelayed && *latencies.isRelayed) { - StrAppend(ret, "\n", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms (Relayed)")), *latencies.providerLatency)); - } else { - StrAppend(ret, "\n", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms")), *latencies.providerLatency)); - } - } - - return ret; -} - -std::vector TextCmdList = { - { "/help", N_("Prints help overview or help for a specific command."), N_("[command]"), &TextCmdHelp }, - { "/arena", N_("Enter a PvP Arena."), N_(""), &TextCmdArena }, - { "/arenapot", N_("Gives Arena Potions."), N_(""), &TextCmdArenaPot }, - { "/inspect", N_("Inspects stats and equipment of another player."), N_(""), &TextCmdInspect }, - { "/seedinfo", N_("Show seed infos for current level."), "", &TextCmdLevelSeed }, - { "/ping", N_("Show latency statistics for another player."), N_(""), &TextCmdPing }, -}; - -bool CheckChatCommand(const std::string_view text) -{ - if (text.size() < 1 || text[0] != '/') - return false; - - auto textCmdIterator = c_find_if(TextCmdList, [&](const TextCmdItem &elem) { return text.find(elem.text) == 0 && (text.length() == elem.text.length() || text[elem.text.length()] == ' '); }); - if (textCmdIterator == TextCmdList.end()) { - InitDiabloMsg(StrCat(_("Command "), "\"", text, "\"", _(" is unknown."))); - return true; - } - - const TextCmdItem &textCmd = *textCmdIterator; - std::string_view parameter = ""; - if (text.length() > (textCmd.text.length() + 1)) - parameter = text.substr(textCmd.text.length() + 1); - const std::string result = textCmd.actionProc(parameter); - if (result != "") - InitDiabloMsg(result); - return true; -} - -void ResetChatMessage() -{ - if (CheckChatCommand(TalkMessage)) - return; - - uint32_t pmask = 0; - - for (size_t i = 0; i < Players.size(); i++) { - if (WhisperList[i]) - pmask |= 1 << i; - } - - NetSendCmdString(pmask, TalkMessage); -} - -void ControlPressEnter() -{ - if (TalkMessage[0] != 0) { - ResetChatMessage(); - uint8_t i = 0; - for (; i < 8; i++) { - if (strcmp(TalkSave[i], TalkMessage) == 0) - break; - } - if (i >= 8) { - strcpy(TalkSave[NextTalkSave], TalkMessage); - NextTalkSave++; - NextTalkSave &= 7; - } else { - uint8_t talkSave = NextTalkSave - 1; - talkSave &= 7; - if (i != talkSave) { - strcpy(TalkSave[i], TalkSave[talkSave]); - *BufCopy(TalkSave[talkSave], ChatInputState->value()) = '\0'; - } - } - TalkMessage[0] = '\0'; - TalkSaveIndex = NextTalkSave; - } - ResetChat(); -} - -void ControlUpDown(int v) -{ - for (int i = 0; i < 8; i++) { - TalkSaveIndex = (v + TalkSaveIndex) & 7; - if (TalkSave[TalkSaveIndex][0] != 0) { - ChatInputState->assign(TalkSave[TalkSaveIndex]); - return; - } - } -} - -void RemoveGold(Player &player, int goldIndex, int amount) -{ - const int gi = goldIndex - INVITEM_INV_FIRST; - player.InvList[gi]._ivalue -= amount; - if (player.InvList[gi]._ivalue > 0) { - SetPlrHandGoldCurs(player.InvList[gi]); - NetSyncInvItem(player, gi); - } else { - player.RemoveInvItem(gi); - } - - MakeGoldStack(player.HoldItem, amount); - NewCursor(player.HoldItem); - - player._pGold = CalculateGold(player); -} - -bool IsLevelUpButtonVisible() -{ - if (SpellSelectFlag || CharFlag || MyPlayer->_pStatPts == 0) { - return false; - } - if (ControlMode == ControlTypes::VirtualGamepad) { - return false; - } - if (IsPlayerInStore() || IsStashOpen) { - return false; - } - if (QuestLogIsOpen && GetLeftPanel().contains(GetMainPanel().position + Displacement { 0, -74 })) { - return false; - } - - return true; -} - -} // namespace - -void CalculatePanelAreas() -{ - constexpr Size MainPanelSize { 640, 128 }; - - MainPanel = { - { (gnScreenWidth - MainPanelSize.width) / 2, gnScreenHeight - MainPanelSize.height }, - MainPanelSize - }; - LeftPanel = { - { 0, 0 }, - SidePanelSize - }; - RightPanel = { - { 0, 0 }, - SidePanelSize - }; - - if (ControlMode == ControlTypes::VirtualGamepad) { - LeftPanel.position.x = gnScreenWidth / 2 - LeftPanel.size.width; - } else { - if (gnScreenWidth - LeftPanel.size.width - RightPanel.size.width > MainPanel.size.width) { - LeftPanel.position.x = (gnScreenWidth - LeftPanel.size.width - RightPanel.size.width - MainPanel.size.width) / 2; - } - } - LeftPanel.position.y = (gnScreenHeight - LeftPanel.size.height - MainPanel.size.height) / 2; - - if (ControlMode == ControlTypes::VirtualGamepad) { - RightPanel.position.x = gnScreenWidth / 2; - } else { - RightPanel.position.x = gnScreenWidth - RightPanel.size.width - LeftPanel.position.x; - } - RightPanel.position.y = LeftPanel.position.y; - - gnViewportHeight = gnScreenHeight; - if (gnScreenWidth <= MainPanel.size.width) { - // Part of the screen is fully obscured by the UI - gnViewportHeight -= MainPanel.size.height; - } -} - -bool IsChatAvailable() -{ - return gbIsMultiplayer; -} - -void FocusOnCharInfo() -{ - const Player &myPlayer = *MyPlayer; - - if (invflag || myPlayer._pStatPts <= 0) - return; - - // Find the first incrementable stat. - int stat = -1; - for (auto attribute : enum_values()) { - if (myPlayer.GetBaseAttributeValue(attribute) >= myPlayer.GetMaximumAttributeValue(attribute)) - continue; - stat = static_cast(attribute); - } - if (stat == -1) - return; - - SetCursorPos(CharPanelButtonRect[stat].Center()); -} - -void OpenCharPanel() -{ - QuestLogIsOpen = false; - CloseGoldWithdraw(); - CloseStash(); - CharFlag = true; -} - -void CloseCharPanel() -{ - CharFlag = false; - if (IsInspectingPlayer()) { - InspectPlayer = MyPlayer; - RedrawEverything(); - - if (InspectingFromPartyPanel) - InspectingFromPartyPanel = false; - else - InitDiabloMsg(_("Stopped inspecting players.")); - } -} - -void ToggleCharPanel() -{ - if (CharFlag) - CloseCharPanel(); - else - OpenCharPanel(); -} - -void AddInfoBoxString(std::string_view str, bool floatingBox /*= false*/) -{ - StringOrView &infoString = floatingBox ? FloatingInfoString : InfoString; - - if (infoString.empty()) - infoString = str; - else - infoString = StrCat(infoString, "\n", str); -} - -void AddInfoBoxString(std::string &&str, bool floatingBox /*= false*/) -{ - StringOrView &infoString = floatingBox ? FloatingInfoString : InfoString; - - if (infoString.empty()) - infoString = std::move(str); - else - infoString = StrCat(infoString, "\n", str); -} - -Point GetPanelPosition(UiPanels panel, Point offset) -{ - const Displacement displacement { offset.x, offset.y }; - - switch (panel) { - case UiPanels::Main: - return GetMainPanel().position + displacement; - case UiPanels::Quest: - case UiPanels::Character: - case UiPanels::Stash: - return GetLeftPanel().position + displacement; - case UiPanels::Spell: - case UiPanels::Inventory: - return GetRightPanel().position + displacement; - default: - return GetMainPanel().position + displacement; - } -} - -void DrawPanelBox(const Surface &out, SDL_Rect srcRect, Point targetPosition) -{ - out.BlitFrom(*BottomBuffer, srcRect, targetPosition); -} - -void DrawLifeFlaskUpper(const Surface &out) -{ - constexpr int LifeFlaskUpperOffset = 107; - DrawFlaskUpper(out, *pLifeBuff, LifeFlaskUpperOffset, MyPlayer->_pHPPer); -} - -void DrawManaFlaskUpper(const Surface &out) -{ - constexpr int ManaFlaskUpperOffset = 475; - DrawFlaskUpper(out, *pManaBuff, ManaFlaskUpperOffset, MyPlayer->_pManaPer); -} - -void DrawLifeFlaskLower(const Surface &out, bool drawFilledPortion) -{ - constexpr int LifeFlaskLowerOffset = 96; - DrawFlaskLower(out, *pLifeBuff, LifeFlaskLowerOffset, MyPlayer->_pHPPer, drawFilledPortion); -} - -void DrawManaFlaskLower(const Surface &out, bool drawFilledPortion) -{ - constexpr int ManaFlaskLowerOffset = 464; - DrawFlaskLower(out, *pManaBuff, ManaFlaskLowerOffset, MyPlayer->_pManaPer, drawFilledPortion); -} - -void DrawFlaskValues(const Surface &out, Point pos, int currValue, int maxValue) -{ - const UiFlags color = (currValue > 0 ? (currValue == maxValue ? UiFlags::ColorGold : UiFlags::ColorWhite) : UiFlags::ColorRed); - - auto drawStringWithShadow = [out, color](std::string_view text, Point pos) { - DrawString(out, text, pos + Displacement { -1, -1 }, - { .flags = UiFlags::ColorBlack | UiFlags::KerningFitSpacing, .spacing = 0 }); - DrawString(out, text, pos, - { .flags = color | UiFlags::KerningFitSpacing, .spacing = 0 }); - }; - - const std::string currText = StrCat(currValue); - drawStringWithShadow(currText, pos - Displacement { GetLineWidth(currText, GameFont12) + 1, 0 }); - drawStringWithShadow("/", pos); - drawStringWithShadow(StrCat(maxValue), pos + Displacement { GetLineWidth("/", GameFont12) + 1, 0 }); -} - -void UpdateLifeManaPercent() -{ - MyPlayer->UpdateManaPercentage(); - MyPlayer->UpdateHitPointPercentage(); -} - -tl::expected InitMainPanel() -{ - if (!HeadlessMode) { - BottomBuffer.emplace(GetMainPanel().size.width, (GetMainPanel().size.height + PanelPaddingHeight) * (IsChatAvailable() ? 2 : 1)); - pManaBuff.emplace(88, 88); - pLifeBuff.emplace(88, 88); - - RETURN_IF_ERROR(LoadPartyPanel()); - RETURN_IF_ERROR(LoadCharPanel()); - RETURN_IF_ERROR(LoadLargeSpellIcons()); - { - ASSIGN_OR_RETURN(const OwnedClxSpriteList sprite, LoadCelWithStatus("ctrlpan\\panel8", GetMainPanel().size.width)); - ClxDraw(*BottomBuffer, { 0, (GetMainPanel().size.height + PanelPaddingHeight) - 1 }, sprite[0]); - } - { - const Point bulbsPosition { 0, 87 }; - ASSIGN_OR_RETURN(const OwnedClxSpriteList statusPanel, LoadCelWithStatus("ctrlpan\\p8bulbs", 88)); - ClxDraw(*pLifeBuff, bulbsPosition, statusPanel[0]); - ClxDraw(*pManaBuff, bulbsPosition, statusPanel[1]); - } - } - ChatFlag = false; - ChatInputState = std::nullopt; - if (IsChatAvailable()) { - if (!HeadlessMode) { - { - ASSIGN_OR_RETURN(const OwnedClxSpriteList sprite, LoadCelWithStatus("ctrlpan\\talkpanl", GetMainPanel().size.width)); - ClxDraw(*BottomBuffer, { 0, (GetMainPanel().size.height + PanelPaddingHeight) * 2 - 1 }, sprite[0]); - } - multiButtons = LoadCel("ctrlpan\\p8but2", 33); - talkButtons = LoadCel("ctrlpan\\talkbutt", 61); - } - sgbPlrTalkTbl = 0; - TalkMessage[0] = '\0'; - for (bool &whisper : WhisperList) - whisper = true; - for (bool &talkButtonDown : TalkButtonsDown) - talkButtonDown = false; - } - MainPanelFlag = false; - LevelButtonDown = false; - if (!HeadlessMode) { - RETURN_IF_ERROR(LoadMainPanel()); - ASSIGN_OR_RETURN(pMainPanelButtons, LoadCelWithStatus("ctrlpan\\panel8bu", 71)); - - static const uint16_t CharButtonsFrameWidths[9] { 95, 41, 41, 41, 41, 41, 41, 41, 41 }; - ASSIGN_OR_RETURN(pChrButtons, LoadCelWithStatus("data\\charbut", CharButtonsFrameWidths)); - } - ResetMainPanelButtons(); - if (!HeadlessMode) - pDurIcons = LoadCel("items\\duricons", 32); - for (bool &buttonEnabled : CharPanelButton) - buttonEnabled = false; - CharPanelButtonActive = false; - InfoString = StringOrView {}; - FloatingInfoString = StringOrView {}; - RedrawComponent(PanelDrawComponent::Health); - RedrawComponent(PanelDrawComponent::Mana); - CloseCharPanel(); - SpellSelectFlag = false; - SpellbookTab = 0; - SpellbookFlag = false; - - if (!HeadlessMode) { - InitSpellBook(); - ASSIGN_OR_RETURN(pQLogCel, LoadCelWithStatus("data\\quest", static_cast(SidePanelSize.width))); - ASSIGN_OR_RETURN(GoldBoxBuffer, LoadCelWithStatus("ctrlpan\\golddrop", 261)); - } - CloseGoldDrop(); - CalculatePanelAreas(); - - if (!HeadlessMode) - InitModifierHints(); - - return {}; -} - -void DrawMainPanel(const Surface &out) -{ - DrawPanelBox(out, MakeSdlRect(0, sgbPlrTalkTbl + PanelPaddingHeight, GetMainPanel().size.width, GetMainPanel().size.height), GetMainPanel().position); - DrawInfoBox(out); -} - -void DrawMainPanelButtons(const Surface &out) -{ - const Point mainPanelPosition = GetMainPanel().position; - - for (int i = 0; i < TotalSpMainPanelButtons; i++) { - if (!MainPanelButtons[i]) { - DrawPanelBox(out, MakeSdlRect(MainPanelButtonRect[i].position.x, MainPanelButtonRect[i].position.y + PanelPaddingHeight, MainPanelButtonRect[i].size.width, MainPanelButtonRect[i].size.height + 1), mainPanelPosition + Displacement { MainPanelButtonRect[i].position.x, MainPanelButtonRect[i].position.y }); - } else { - const Point position = mainPanelPosition + Displacement { MainPanelButtonRect[i].position.x, MainPanelButtonRect[i].position.y }; - RenderClxSprite(out, (*pMainPanelButtons)[i], position); - RenderClxSprite(out, (*PanelButtonDown)[i], position + Displacement { 4, 0 }); - } - } - - if (IsChatAvailable()) { - RenderClxSprite(out, (*multiButtons)[MainPanelButtons[PanelButtonSendmsg] ? 1 : 0], mainPanelPosition + Displacement { MainPanelButtonRect[PanelButtonSendmsg].position.x, MainPanelButtonRect[PanelButtonSendmsg].position.y }); - - const Point friendlyButtonPosition = mainPanelPosition + Displacement { MainPanelButtonRect[PanelButtonFriendly].position.x, MainPanelButtonRect[PanelButtonFriendly].position.y }; - - if (MyPlayer->friendlyMode) - RenderClxSprite(out, (*multiButtons)[MainPanelButtons[PanelButtonFriendly] ? 3 : 2], friendlyButtonPosition); - else - RenderClxSprite(out, (*multiButtons)[MainPanelButtons[PanelButtonFriendly] ? 5 : 4], friendlyButtonPosition); - } -} - -void ResetMainPanelButtons() -{ - for (bool &panelButton : MainPanelButtons) - panelButton = false; - SetMainPanelButtonUp(); -} - -void CheckMainPanelButton() -{ - const int totalButtons = IsChatAvailable() ? TotalMpMainPanelButtons : TotalSpMainPanelButtons; - - for (int i = 0; i < totalButtons; i++) { - Rectangle button = MainPanelButtonRect[i]; - - SetPanelObjectPosition(UiPanels::Main, button); - - if (button.contains(MousePosition)) { - SetMainPanelButtonDown(i); - } - } - - Rectangle spellSelectButton = SpellButtonRect; - - SetPanelObjectPosition(UiPanels::Main, spellSelectButton); - - if (!SpellSelectFlag && spellSelectButton.contains(MousePosition)) { - if ((SDL_GetModState() & SDL_KMOD_SHIFT) != 0) { - Player &myPlayer = *MyPlayer; - myPlayer._pRSpell = SpellID::Invalid; - myPlayer._pRSplType = SpellType::Invalid; - RedrawEverything(); - return; - } - DoSpeedBook(); - gamemenu_off(); - } -} - -void CheckMainPanelButtonDead() -{ - Rectangle menuButton = MainPanelButtonRect[PanelButtonMainmenu]; - - SetPanelObjectPosition(UiPanels::Main, menuButton); - - if (menuButton.contains(MousePosition)) { - SetMainPanelButtonDown(PanelButtonMainmenu); - return; - } - - Rectangle chatButton = MainPanelButtonRect[PanelButtonSendmsg]; - - SetPanelObjectPosition(UiPanels::Main, chatButton); - - if (chatButton.contains(MousePosition)) { - SetMainPanelButtonDown(PanelButtonSendmsg); - } -} - -void DoAutoMap() -{ - if (!AutomapActive) - StartAutomap(); - else - AutomapActive = false; -} - -void CycleAutomapType() -{ - if (!AutomapActive) { - StartAutomap(); - return; - } - const AutomapType newType { static_cast>( - (static_cast(GetAutomapType()) + 1) % enum_size::value) }; - SetAutomapType(newType); - if (newType == AutomapType::FIRST) { - AutomapActive = false; - } -} - -void CheckPanelInfo() -{ - MainPanelFlag = false; - InfoString = StringOrView {}; - FloatingInfoString = StringOrView {}; - - const int totalButtons = IsChatAvailable() ? TotalMpMainPanelButtons : TotalSpMainPanelButtons; - - for (int i = 0; i < totalButtons; i++) { - Rectangle button = MainPanelButtonRect[i]; - - SetPanelObjectPosition(UiPanels::Main, button); - - if (button.contains(MousePosition)) { - if (i != 7) { - InfoString = _(PanBtnStr[i]); - } else { - if (MyPlayer->friendlyMode) - InfoString = _("Player friendly"); - else - InfoString = _("Player attack"); - } - if (PanBtnHotKey[i] != nullptr) { - AddInfoBoxString(fmt::format(fmt::runtime(_("Hotkey: {:s}")), _(PanBtnHotKey[i]))); - } - InfoColor = UiFlags::ColorWhite; - MainPanelFlag = true; - } - } - - Rectangle spellSelectButton = SpellButtonRect; - - SetPanelObjectPosition(UiPanels::Main, spellSelectButton); - - if (!SpellSelectFlag && spellSelectButton.contains(MousePosition)) { - InfoString = _("Select current spell button"); - InfoColor = UiFlags::ColorWhite; - MainPanelFlag = true; - AddInfoBoxString(_("Hotkey: 's'")); - const Player &myPlayer = *MyPlayer; - const SpellID spellId = myPlayer._pRSpell; - if (IsValidSpell(spellId)) { - switch (myPlayer._pRSplType) { - case SpellType::Skill: - AddInfoBoxString(fmt::format(fmt::runtime(_("{:s} Skill")), pgettext("spell", GetSpellData(spellId).sNameText))); - break; - case SpellType::Spell: { - AddInfoBoxString(fmt::format(fmt::runtime(_("{:s} Spell")), pgettext("spell", GetSpellData(spellId).sNameText))); - const int spellLevel = myPlayer.GetSpellLevel(spellId); - AddInfoBoxString(spellLevel == 0 ? _("Spell Level 0 - Unusable") : fmt::format(fmt::runtime(_("Spell Level {:d}")), spellLevel)); - } break; - case SpellType::Scroll: { - AddInfoBoxString(fmt::format(fmt::runtime(_("Scroll of {:s}")), pgettext("spell", GetSpellData(spellId).sNameText))); - const int scrollCount = c_count_if(InventoryAndBeltPlayerItemsRange { myPlayer }, [spellId](const Item &item) { - return item.isScrollOf(spellId); - }); - AddInfoBoxString(fmt::format(fmt::runtime(ngettext("{:d} Scroll", "{:d} Scrolls", scrollCount)), scrollCount)); - } break; - case SpellType::Charges: - AddInfoBoxString(fmt::format(fmt::runtime(_("Staff of {:s}")), pgettext("spell", GetSpellData(spellId).sNameText))); - AddInfoBoxString(fmt::format(fmt::runtime(ngettext("{:d} Charge", "{:d} Charges", myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges)), myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges)); - break; - case SpellType::Invalid: - break; - } - } - } - - Rectangle belt = BeltRect; - - SetPanelObjectPosition(UiPanels::Main, belt); - - if (belt.contains(MousePosition)) - pcursinvitem = CheckInvHLight(); - - if (CheckXPBarInfo()) - MainPanelFlag = true; -} - -void CheckMainPanelButtonUp() -{ - bool gamemenuOff = true; - - SetMainPanelButtonUp(); - - for (int i = PanelButtonFirst; i <= PanelButtonLast; i++) { - if (!MainPanelButtons[i]) - continue; - - MainPanelButtons[i] = false; - - Rectangle button = MainPanelButtonRect[i]; - - SetPanelObjectPosition(UiPanels::Main, button); - - if (!button.contains(MousePosition)) - continue; - - switch (i) { - case PanelButtonCharinfo: - ToggleCharPanel(); - break; - case PanelButtonQlog: - CloseCharPanel(); - CloseGoldWithdraw(); - CloseStash(); - if (!QuestLogIsOpen) - StartQuestlog(); - else - QuestLogIsOpen = false; - break; - case PanelButtonAutomap: - DoAutoMap(); - break; - case PanelButtonMainmenu: - if (MyPlayerIsDead) { - if (!gbIsMultiplayer) { - if (gbValidSaveFile) - gamemenu_load_game(false); - else - gamemenu_exit_game(false); - } else { - NetSendCmd(true, CMD_RETOWN); - } - break; - } else if (MyPlayer->hasNoLife()) { - break; - } - qtextflag = false; - gamemenu_handle_previous(); - gamemenuOff = false; - break; - case PanelButtonInventory: - SpellbookFlag = false; - CloseGoldWithdraw(); - CloseStash(); - invflag = !invflag; - CloseGoldDrop(); - break; - case PanelButtonSpellbook: - CloseInventory(); - CloseGoldDrop(); - SpellbookFlag = !SpellbookFlag; - break; - case PanelButtonSendmsg: - if (ChatFlag) - ResetChat(); - else - TypeChatMessage(); - break; - case PanelButtonFriendly: - // Toggle friendly Mode - NetSendCmd(true, CMD_FRIENDLYMODE); - break; - } - } - - if (gamemenuOff) - gamemenu_off(); -} - -void FreeControlPan() -{ - BottomBuffer = std::nullopt; - pManaBuff = std::nullopt; - pLifeBuff = std::nullopt; - FreeLargeSpellIcons(); - FreeSpellBook(); - pMainPanelButtons = std::nullopt; - multiButtons = std::nullopt; - talkButtons = std::nullopt; - pChrButtons = std::nullopt; - pDurIcons = std::nullopt; - pQLogCel = std::nullopt; - GoldBoxBuffer = std::nullopt; - FreeMainPanel(); - FreePartyPanel(); - FreeCharPanel(); - FreeModifierHints(); -} - -void DrawInfoBox(const Surface &out) -{ - DrawPanelBox(out, MakeSdlRect(InfoBoxRect.position.x, InfoBoxRect.position.y + PanelPaddingHeight, InfoBoxRect.size.width, InfoBoxRect.size.height), GetMainPanel().position + Displacement { InfoBoxRect.position.x, InfoBoxRect.position.y }); - if (!MainPanelFlag && !trigflag && pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell && !SpellSelectFlag && pcurs != CURSOR_HOURGLASS) { - InfoString = StringOrView {}; - InfoColor = UiFlags::ColorWhite; - } - const Player &myPlayer = *MyPlayer; - if (SpellSelectFlag || trigflag || pcurs == CURSOR_HOURGLASS) { - InfoColor = UiFlags::ColorWhite; - } else if (!myPlayer.HoldItem.isEmpty()) { - if (myPlayer.HoldItem._itype == ItemType::Gold) { - const int nGold = myPlayer.HoldItem._ivalue; - InfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)); - } else if (!myPlayer.CanUseItem(myPlayer.HoldItem)) { - InfoString = _("Requirements not met"); - } else { - InfoString = myPlayer.HoldItem.getName(); - InfoColor = myPlayer.HoldItem.getTextColor(); - } - } else { - if (pcursitem != -1) - GetItemStr(Items[pcursitem]); - else if (ObjectUnderCursor != nullptr) - GetObjectStr(*ObjectUnderCursor); - if (pcursmonst != -1) { - if (leveltype != DTYPE_TOWN) { - const Monster &monster = Monsters[pcursmonst]; - InfoColor = UiFlags::ColorWhite; - InfoString = monster.name(); - if (monster.isUnique()) { - InfoColor = UiFlags::ColorWhitegold; - PrintUniqueHistory(); - } else { - PrintMonstHistory(monster.type().type); - } - } else if (pcursitem == -1) { - InfoString = std::string_view(Towners[pcursmonst].name); - } - } - if (PlayerUnderCursor != nullptr) { - InfoColor = UiFlags::ColorWhitegold; - const auto &target = *PlayerUnderCursor; - InfoString = std::string_view(target._pName); - AddInfoBoxString(fmt::format(fmt::runtime(_("{:s}, Level: {:d}")), target.getClassName(), target.getCharacterLevel())); - AddInfoBoxString(fmt::format(fmt::runtime(_("Hit Points {:d} of {:d}")), target._pHitPoints >> 6, target._pMaxHP >> 6)); - } - if (PortraitIdUnderCursor != -1) { - InfoColor = UiFlags::ColorWhitegold; - auto &target = Players[PortraitIdUnderCursor]; - InfoString = std::string_view(target._pName); - AddInfoBoxString(_("Right click to inspect")); - } - } - if (!InfoString.empty()) - PrintInfo(out); -} - -void DrawFloatingInfoBox(const Surface &out) -{ - if (pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell) { - FloatingInfoString = StringOrView {}; - InfoColor = UiFlags::ColorWhite; - } - - if (!FloatingInfoString.empty()) - PrintFloatingInfo(out); -} - -void CheckLevelButton() -{ - if (!IsLevelUpButtonVisible()) { - return; - } - - Rectangle button = LevelButtonRect; - - SetPanelObjectPosition(UiPanels::Main, button); - - if (!LevelButtonDown && button.contains(MousePosition)) - LevelButtonDown = true; -} - -void CheckLevelButtonUp() -{ - Rectangle button = LevelButtonRect; - - SetPanelObjectPosition(UiPanels::Main, button); - - if (button.contains(MousePosition)) { - OpenCharPanel(); - } - LevelButtonDown = false; -} - -void DrawLevelButton(const Surface &out) -{ - if (IsLevelUpButtonVisible()) { - const int nCel = LevelButtonDown ? 2 : 1; - DrawString(out, _("Level Up"), { GetMainPanel().position + Displacement { 0, LevelButtonRect.position.y - 23 }, { 120, 0 } }, - { .flags = UiFlags::ColorWhite | UiFlags::AlignCenter | UiFlags::KerningFitSpacing }); - RenderClxSprite(out, (*pChrButtons)[nCel], GetMainPanel().position + Displacement { LevelButtonRect.position.x, LevelButtonRect.position.y }); - } -} - -void CheckChrBtns() -{ - const Player &myPlayer = *MyPlayer; - - if (CharPanelButtonActive || myPlayer._pStatPts == 0) - return; - - for (auto attribute : enum_values()) { - if (myPlayer.GetBaseAttributeValue(attribute) >= myPlayer.GetMaximumAttributeValue(attribute)) - continue; - auto buttonId = static_cast(attribute); - Rectangle button = CharPanelButtonRect[buttonId]; - SetPanelObjectPosition(UiPanels::Character, button); - if (button.contains(MousePosition)) { - CharPanelButton[buttonId] = true; - CharPanelButtonActive = true; - } - } -} - -void ReleaseChrBtns(bool addAllStatPoints) -{ - CharPanelButtonActive = false; - for (auto attribute : enum_values()) { - auto buttonId = static_cast(attribute); - if (!CharPanelButton[buttonId]) - continue; - - CharPanelButton[buttonId] = false; - Rectangle button = CharPanelButtonRect[buttonId]; - SetPanelObjectPosition(UiPanels::Character, button); - if (button.contains(MousePosition)) { - Player &myPlayer = *MyPlayer; - int statPointsToAdd = 1; - if (addAllStatPoints) - statPointsToAdd = CapStatPointsToAdd(myPlayer._pStatPts, myPlayer, attribute); - switch (attribute) { - case CharacterAttribute::Strength: - NetSendCmdParam1(true, CMD_ADDSTR, statPointsToAdd); - myPlayer._pStatPts -= statPointsToAdd; - break; - case CharacterAttribute::Magic: - NetSendCmdParam1(true, CMD_ADDMAG, statPointsToAdd); - myPlayer._pStatPts -= statPointsToAdd; - break; - case CharacterAttribute::Dexterity: - NetSendCmdParam1(true, CMD_ADDDEX, statPointsToAdd); - myPlayer._pStatPts -= statPointsToAdd; - break; - case CharacterAttribute::Vitality: - NetSendCmdParam1(true, CMD_ADDVIT, statPointsToAdd); - myPlayer._pStatPts -= statPointsToAdd; - break; - } - } - } -} - -void DrawDurIcon(const Surface &out) -{ - const bool hasRoomBetweenPanels = RightPanel.position.x - (LeftPanel.position.x + LeftPanel.size.width) >= 16 + (32 + 8 + 32 + 8 + 32 + 8 + 32) + 16; - const bool hasRoomUnderPanels = MainPanel.position.y - (RightPanel.position.y + RightPanel.size.height) >= 16 + 32 + 16; - - if (!hasRoomBetweenPanels && !hasRoomUnderPanels) { - if (IsLeftPanelOpen() && IsRightPanelOpen()) - return; - } - - int x = MainPanel.position.x + MainPanel.size.width - 32 - 16; - if (!hasRoomUnderPanels) { - if (IsRightPanelOpen() && MainPanel.position.x + MainPanel.size.width > RightPanel.position.x) - x -= MainPanel.position.x + MainPanel.size.width - RightPanel.position.x; - } - - Player &myPlayer = *MyPlayer; - x = DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_HEAD], x, 3); - x = DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_CHEST], x, 2); - x = DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_HAND_LEFT], x, 0); - DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_HAND_RIGHT], x, 0); -} - -void RedBack(const Surface &out) -{ - uint8_t *dst = out.begin(); - uint8_t *tbl = GetPauseTRN(); - for (int h = gnViewportHeight; h != 0; h--, dst += out.pitch() - gnScreenWidth) { - for (int w = gnScreenWidth; w != 0; w--) { - if (leveltype != DTYPE_HELL || *dst >= 32) - *dst = tbl[*dst]; - dst++; - } - } -} - -void DrawDeathText(const Surface &out) -{ - const TextRenderOptions largeTextOptions { - .flags = UiFlags::FontSize42 | UiFlags::ColorGold | UiFlags::AlignCenter | UiFlags::VerticalCenter, - .spacing = 2 - }; - const TextRenderOptions smallTextOptions { - .flags = UiFlags::FontSize30 | UiFlags::ColorGold | UiFlags::AlignCenter | UiFlags::VerticalCenter, - .spacing = 2 - }; - std::string text; - const int verticalPadding = 42; - Point linePosition { 0, gnScreenHeight / 2 - (verticalPadding * 2) }; - - text = _("You have died"); - DrawString(out, text, linePosition, largeTextOptions); - linePosition.y += verticalPadding; - - std::string buttonText; - - switch (ControlMode) { - case ControlTypes::KeyboardAndMouse: - buttonText = _("ESC"); - break; - case ControlTypes::Gamepad: - buttonText = ToString(GamepadType, ControllerButton_BUTTON_START); - break; - case ControlTypes::VirtualGamepad: - buttonText = _("Menu Button"); - break; - default: - break; - } - - if (!gbIsMultiplayer) { - if (gbValidSaveFile) - text = fmt::format(fmt::runtime(_("Press {} to load last save.")), buttonText); - else - text = fmt::format(fmt::runtime(_("Press {} to return to Main Menu.")), buttonText); - - } else { - text = fmt::format(fmt::runtime(_("Press {} to restart in town.")), buttonText); - } - DrawString(out, text, linePosition, smallTextOptions); -} - -void DrawGoldSplit(const Surface &out) -{ - const int dialogX = 30; - - ClxDraw(out, GetPanelPosition(UiPanels::Inventory, { dialogX, 178 }), (*GoldBoxBuffer)[0]); - - const std::string_view amountText = GoldDropText; - const TextInputCursorState &cursor = GoldDropCursor; - const int max = GetGoldDropMax(); - - const std::string description = fmt::format( - fmt::runtime(ngettext( - /* TRANSLATORS: {:s} is a number with separators. Dialog is shown when splitting a stash of Gold.*/ - "You have {:s} gold piece. How many do you want to remove?", - "You have {:s} gold pieces. How many do you want to remove?", - max)), - FormatInteger(max)); - - // Pre-wrap the string at spaces, otherwise DrawString would hard wrap in the middle of words - const std::string wrapped = WordWrapString(description, 200); - - // The split gold dialog is roughly 4 lines high, but we need at least one line for the player to input an amount. - // Using a clipping region 50 units high (approx 3 lines with a lineheight of 17) to ensure there is enough room left - // for the text entered by the player. - DrawString(out, wrapped, { GetPanelPosition(UiPanels::Inventory, { dialogX + 31, 75 }), { 200, 50 } }, - { .flags = UiFlags::ColorWhitegold | UiFlags::AlignCenter, .lineHeight = 17 }); - - // Even a ten digit amount of gold only takes up about half a line. There's no need to wrap or clip text here so we - // use the Point form of DrawString. - DrawString(out, amountText, GetPanelPosition(UiPanels::Inventory, { dialogX + 37, 128 }), - { - .flags = UiFlags::ColorWhite | UiFlags::PentaCursor, - .cursorPosition = static_cast(cursor.position), - .highlightRange = { static_cast(cursor.selection.begin), static_cast(cursor.selection.end) }, - }); -} - -void control_drop_gold(SDL_Keycode vkey) -{ - Player &myPlayer = *MyPlayer; - - if (myPlayer.hasNoLife()) { - CloseGoldDrop(); - return; - } - - switch (vkey) { - case SDLK_RETURN: - case SDLK_KP_ENTER: - if (const int value = GoldDropInputState->value(); value != 0) { - RemoveGold(myPlayer, GoldDropInvIndex, value); - } - CloseGoldDrop(); - break; - case SDLK_ESCAPE: - CloseGoldDrop(); - break; - default: - break; - } -} - -void DrawChatBox(const Surface &out) -{ - if (!ChatFlag) - return; - - const Point mainPanelPosition = GetMainPanel().position; - - DrawPanelBox(out, MakeSdlRect(175, sgbPlrTalkTbl + 20, 294, 5), mainPanelPosition + Displacement { 175, 4 }); - int off = 0; - for (int i = 293; i > 283; off++, i--) { - DrawPanelBox(out, MakeSdlRect((off / 2) + 175, sgbPlrTalkTbl + off + 25, i, 1), mainPanelPosition + Displacement { (off / 2) + 175, off + 9 }); - } - DrawPanelBox(out, MakeSdlRect(185, sgbPlrTalkTbl + 35, 274, 30), mainPanelPosition + Displacement { 185, 19 }); - DrawPanelBox(out, MakeSdlRect(180, sgbPlrTalkTbl + 65, 284, 5), mainPanelPosition + Displacement { 180, 49 }); - for (int i = 0; i < 10; i++) { - DrawPanelBox(out, MakeSdlRect(180, sgbPlrTalkTbl + i + 70, i + 284, 1), mainPanelPosition + Displacement { 180, i + 54 }); - } - DrawPanelBox(out, MakeSdlRect(170, sgbPlrTalkTbl + 80, 310, 55), mainPanelPosition + Displacement { 170, 64 }); - - int x = mainPanelPosition.x + 200; - const int y = mainPanelPosition.y + 10; - - const uint32_t len = DrawString(out, TalkMessage, { { x, y }, { 250, 39 } }, - { - .flags = UiFlags::ColorWhite | UiFlags::PentaCursor, - .lineHeight = 13, - .cursorPosition = static_cast(ChatCursor.position), - .highlightRange = { static_cast(ChatCursor.selection.begin), static_cast(ChatCursor.selection.end) }, - }); - ChatInputState->truncate(len); - - x += 46; - int talkBtn = 0; - for (size_t i = 0; i < Players.size(); i++) { - Player &player = Players[i]; - if (&player == MyPlayer) - continue; - - const UiFlags color = player.friendlyMode ? UiFlags::ColorWhitegold : UiFlags::ColorRed; - const Point talkPanPosition = mainPanelPosition + Displacement { 172, 84 + 18 * talkBtn }; - if (WhisperList[i]) { - // the normal (unpressed) voice button is pre-rendered on the panel, only need to draw over it when the button is held - if (TalkButtonsDown[talkBtn]) { - const unsigned spriteIndex = talkBtn == 0 ? 2 : 3; // the first button sprite includes a tip from the devils wing so is different to the rest. - ClxDraw(out, talkPanPosition, (*talkButtons)[spriteIndex]); - - // Draw the translated string over the top of the default (english) button. This graphic is inset to avoid overlapping the wingtip, letting - // the first button be treated the same as the other two further down the panel. - RenderClxSprite(out, (*TalkButton)[2], talkPanPosition + Displacement { 4, -15 }); - } - } else { - unsigned spriteIndex = talkBtn == 0 ? 0 : 1; // the first button sprite includes a tip from the devils wing so is different to the rest. - if (TalkButtonsDown[talkBtn]) - spriteIndex += 4; // held button sprites are at index 4 and 5 (with and without wingtip respectively) - ClxDraw(out, talkPanPosition, (*talkButtons)[spriteIndex]); - - // Draw the translated string over the top of the default (english) button. This graphic is inset to avoid overlapping the wingtip, letting - // the first button be treated the same as the other two further down the panel. - RenderClxSprite(out, (*TalkButton)[TalkButtonsDown[talkBtn] ? 1 : 0], talkPanPosition + Displacement { 4, -15 }); - } - if (player.plractive) { - DrawString(out, player._pName, { { x, y + 60 + talkBtn * 18 }, { 204, 0 } }, { .flags = color }); - } - - talkBtn++; - } -} - -bool CheckMuteButton() -{ - if (!ChatFlag) - return false; - - Rectangle buttons = MuteButtonRect; - - SetPanelObjectPosition(UiPanels::Main, buttons); - - buttons.size.height = (MuteButtons * buttons.size.height) + ((MuteButtons - 1) * MuteButtonPadding); - - if (!buttons.contains(MousePosition)) - return false; - - for (bool &talkButtonDown : TalkButtonsDown) { - talkButtonDown = false; - } - - const Point mainPanelPosition = GetMainPanel().position; - - TalkButtonsDown[(MousePosition.y - (69 + mainPanelPosition.y)) / 18] = true; - - return true; -} - -void CheckMuteButtonUp() -{ - if (!ChatFlag) - return; - - for (bool &talkButtonDown : TalkButtonsDown) - talkButtonDown = false; - - Rectangle buttons = MuteButtonRect; - - SetPanelObjectPosition(UiPanels::Main, buttons); - - buttons.size.height = (MuteButtons * buttons.size.height) + ((MuteButtons - 1) * MuteButtonPadding); - - if (!buttons.contains(MousePosition)) - return; - - int off = (MousePosition.y - buttons.position.y) / (MuteButtonRect.size.height + MuteButtonPadding); - - size_t playerId = 0; - for (; playerId < Players.size() && off != -1; ++playerId) { - if (playerId != MyPlayerId) - off--; - } - if (playerId > 0 && playerId <= Players.size()) - WhisperList[playerId - 1] = !WhisperList[playerId - 1]; -} - -void TypeChatMessage() -{ - if (!IsChatAvailable()) - return; - - ChatFlag = true; - TalkMessage[0] = '\0'; - ChatInputState.emplace(TextInputState::Options { - .value = TalkMessage, - .cursor = &ChatCursor, - .maxLength = sizeof(TalkMessage) - 1 }); - for (bool &talkButtonDown : TalkButtonsDown) { - talkButtonDown = false; - } - sgbPlrTalkTbl = GetMainPanel().size.height + PanelPaddingHeight; - RedrawEverything(); - TalkSaveIndex = NextTalkSave; - - SDL_Rect rect = MakeSdlRect(GetMainPanel().position.x + 200, GetMainPanel().position.y + 22, 0, 27); - SDL_SetTextInputArea(ghMainWnd, &rect, /*cursor=*/0); - SDLC_StartTextInput(ghMainWnd); -} - -void ResetChat() -{ - ChatFlag = false; - SDLC_StopTextInput(ghMainWnd); - ChatCursor = {}; - ChatInputState = std::nullopt; - sgbPlrTalkTbl = 0; - RedrawEverything(); -} - -bool IsChatActive() -{ - if (!IsChatAvailable()) - return false; - - if (!ChatFlag) - return false; - - return true; -} - -template -bool HandleInputEvent(const SDL_Event &event, std::optional &inputState) -{ - if (!inputState) { - return false; // No input state to handle - } - - if constexpr (std::is_same_v) { - return HandleTextInputEvent(event, *inputState); - } else if constexpr (std::is_same_v) { - return HandleNumberInputEvent(event, *inputState); - } - - return false; // Unknown input state type -} - -bool HandleTalkTextInputEvent(const SDL_Event &event) -{ - return HandleInputEvent(event, ChatInputState); -} - -bool CheckKeypress(SDL_Keycode vkey) -{ - if (!IsChatAvailable()) - return false; - if (!ChatFlag) - return false; - - switch (vkey) { - case SDLK_ESCAPE: - ResetChat(); - return true; - case SDLK_RETURN: - case SDLK_KP_ENTER: - ControlPressEnter(); - return true; - case SDLK_DOWN: - ControlUpDown(1); - return true; - case SDLK_UP: - ControlUpDown(-1); - return true; - default: - return vkey >= SDLK_SPACE && vkey <= SDLK_Z; - } -} - -void DiabloHotkeyMsg(uint32_t dwMsg) -{ - assert(dwMsg < QuickMessages.size()); - -#ifdef _DEBUG - constexpr std::string_view LuaPrefix = "/lua "; - for (const std::string &msg : GetOptions().Chat.szHotKeyMsgs[dwMsg]) { - if (!msg.starts_with(LuaPrefix)) continue; - InitConsole(); - RunInConsole(std::string_view(msg).substr(LuaPrefix.size())); - } -#endif - - if (!IsChatAvailable()) { - return; - } - - for (const std::string &msg : GetOptions().Chat.szHotKeyMsgs[dwMsg]) { -#ifdef _DEBUG - if (msg.starts_with(LuaPrefix)) continue; -#endif - char charMsg[MAX_SEND_STR_LEN]; - CopyUtf8(charMsg, msg, sizeof(charMsg)); - NetSendCmdString(0xFFFFFF, charMsg); - } -} - -void OpenGoldDrop(int8_t invIndex, int max) -{ - DropGoldFlag = true; - GoldDropInvIndex = invIndex; - GoldDropText[0] = '\0'; - GoldDropInputState.emplace(NumberInputState::Options { - .textOptions { - .value = GoldDropText, - .cursor = &GoldDropCursor, - .maxLength = sizeof(GoldDropText) - 1, - }, - .min = 0, - .max = max, - }); - SDLC_StartTextInput(ghMainWnd); -} - -void CloseGoldDrop() -{ - if (!DropGoldFlag) - return; - SDLC_StopTextInput(ghMainWnd); - DropGoldFlag = false; - GoldDropInputState = std::nullopt; - GoldDropInvIndex = 0; -} - -int GetGoldDropMax() -{ - return GoldDropInputState->max(); -} - -bool HandleGoldDropTextInputEvent(const SDL_Event &event) -{ - return HandleInputEvent(event, GoldDropInputState); -} - -} // namespace devilution diff --git a/Source/control.h b/Source/control/control.hpp similarity index 91% rename from Source/control.h rename to Source/control/control.hpp index e7d78e837..dcee869db 100644 --- a/Source/control.h +++ b/Source/control/control.hpp @@ -1,8 +1,3 @@ -/** - * @file control.h - * - * Interface of the character and main control panels - */ #pragma once #include @@ -45,34 +40,37 @@ constexpr Size SidePanelSize { 320, 352 }; constexpr Rectangle InfoBoxRect = { { 177, 46 }, { 288, 64 } }; -extern bool DropGoldFlag; -extern TextInputCursorState GoldDropCursor; -extern char GoldDropText[21]; - extern bool CharPanelButton[4]; -extern bool LevelButtonDown; extern bool CharPanelButtonActive; -extern UiFlags InfoColor; + extern int SpellbookTab; -extern bool ChatFlag; -extern bool SpellbookFlag; -extern bool CharFlag; + +extern UiFlags InfoColor; + extern StringOrView InfoString; extern StringOrView FloatingInfoString; -extern bool MainPanelFlag; + +extern Rectangle MainPanelButtonRect[8]; +extern Rectangle CharPanelButtonRect[4]; + extern bool MainPanelButtonDown; -extern bool SpellSelectFlag; -const Rectangle &GetMainPanel(); -const Rectangle &GetLeftPanel(); -const Rectangle &GetRightPanel(); -bool IsLeftPanelOpen(); -bool IsRightPanelOpen(); +extern bool LevelButtonDown; + extern std::optional BottomBuffer; extern OptionalOwnedClxSpriteList GoldBoxBuffer; -extern Rectangle MainPanelButtonRect[8]; +extern bool MainPanelFlag; +extern bool ChatFlag; +extern bool SpellbookFlag; +extern bool CharFlag; +extern bool SpellSelectFlag; + +[[nodiscard]] const Rectangle &GetMainPanel(); +[[nodiscard]] const Rectangle &GetLeftPanel(); +[[nodiscard]] const Rectangle &GetRightPanel(); +bool IsLeftPanelOpen(); +bool IsRightPanelOpen(); void CalculatePanelAreas(); -bool IsChatAvailable(); /** * @brief Moves the mouse to the first attribute "+" button. @@ -85,7 +83,7 @@ void ToggleCharPanel(); /** * @brief Check if the UI can cover the game area entirely */ -inline bool CanPanelsCoverView() +[[nodiscard]] inline bool CanPanelsCoverView() { const Rectangle &mainPanel = GetMainPanel(); return GetScreenWidth() <= mainPanel.size.width && GetScreenHeight() <= SidePanelSize.height + mainPanel.size.height; @@ -96,46 +94,6 @@ void AddInfoBoxString(std::string &&str, bool floatingBox = false); void DrawPanelBox(const Surface &out, SDL_Rect srcRect, Point targetPosition); Point GetPanelPosition(UiPanels panel, Point offset = { 0, 0 }); -/** - * Draws the top dome of the life flask (that part that protrudes out of the control panel). - * The empty flask cel is drawn from the top of the flask to the fill level (there is always a 2 pixel "air gap") and - * the filled flask cel is drawn from that level to the top of the control panel if required. - */ -void DrawLifeFlaskUpper(const Surface &out); - -/** - * Controls the drawing of the area of the life flask within the control panel. - * First sets the fill amount then draws the empty flask cel portion then the filled - * flask portion. - */ -void DrawLifeFlaskLower(const Surface &out, bool drawFilledPortion); - -/** - * Draws the top dome of the mana flask (that part that protrudes out of the control panel). - * The empty flask cel is drawn from the top of the flask to the fill level (there is always a 2 pixel "air gap") and - * the filled flask cel is drawn from that level to the top of the control panel if required. - */ -void DrawManaFlaskUpper(const Surface &out); - -/** - * Controls the drawing of the area of the mana flask within the control panel. - */ -void DrawManaFlaskLower(const Surface &out, bool drawFilledPortion); - -/** - * Controls drawing of current / max values (health, mana) within the control panel. - */ -void DrawFlaskValues(const Surface &out, Point pos, int currValue, int maxValue); - -/** - * @brief calls on the active player object to update HP/Mana percentage variables - * - * This is used to ensure that DrawFlaskAbovePanel routines display an accurate representation of the players health/mana - * - * @see Player::UpdateHitPointPercentage() and Player::UpdateManaPercentage() - */ -void UpdateLifeManaPercent(); - tl::expected InitMainPanel(); void DrawMainPanel(const Surface &out); @@ -186,21 +144,66 @@ void DrawDurIcon(const Surface &out); void RedBack(const Surface &out); void DrawDeathText(const Surface &out); void DrawSpellBook(const Surface &out); -void DrawGoldSplit(const Surface &out); -void control_drop_gold(SDL_Keycode vkey); + +extern Rectangle CharPanelButtonRect[4]; + +bool CheckKeypress(SDL_Keycode vkey); +void DiabloHotkeyMsg(uint32_t dwMsg); void DrawChatBox(const Surface &out); bool CheckMuteButton(); void CheckMuteButtonUp(); void TypeChatMessage(); void ResetChat(); bool IsChatActive(); +bool IsChatAvailable(); bool HandleTalkTextInputEvent(const SDL_Event &event); -bool CheckKeypress(SDL_Keycode vkey); -void DiabloHotkeyMsg(uint32_t dwMsg); + +/** + * Draws the top dome of the life flask (that part that protrudes out of the control panel). + * The empty flask cel is drawn from the top of the flask to the fill level (there is always a 2 pixel "air gap") and + * the filled flask cel is drawn from that level to the top of the control panel if required. + */ +void DrawLifeFlaskUpper(const Surface &out); + +/** + * Controls the drawing of the area of the life flask within the control panel. + * First sets the fill amount then draws the empty flask cel portion then the filled + * flask portion. + */ +void DrawLifeFlaskLower(const Surface &out, bool drawFilledPortion); + +/** + * Draws the top dome of the mana flask (that part that protrudes out of the control panel). + * The empty flask cel is drawn from the top of the flask to the fill level (there is always a 2 pixel "air gap") and + * the filled flask cel is drawn from that level to the top of the control panel if required. + */ +void DrawManaFlaskUpper(const Surface &out); + +/** + * Controls the drawing of the area of the mana flask within the control panel. + */ +void DrawManaFlaskLower(const Surface &out, bool drawFilledPortion); + +/** + * Controls drawing of current / max values (health, mana) within the control panel. + */ +void DrawFlaskValues(const Surface &out, Point pos, int currValue, int maxValue); + +/** + * @brief calls on the active player object to update HP/Mana percentage variables + * + * This is used to ensure that DrawFlaskAbovePanel routines display an accurate representation of the players health/mana + * + * @see Player::UpdateHitPointPercentage() and Player::UpdateManaPercentage() + */ +void UpdateLifeManaPercent(); + +extern bool DropGoldFlag; + +void DrawGoldSplit(const Surface &out); +void control_drop_gold(SDL_Keycode vkey); void OpenGoldDrop(int8_t invIndex, int max); void CloseGoldDrop(); -int GetGoldDropMax(); bool HandleGoldDropTextInputEvent(const SDL_Event &event); -extern Rectangle CharPanelButtonRect[4]; } // namespace devilution diff --git a/Source/control/control_chat.cpp b/Source/control/control_chat.cpp new file mode 100644 index 000000000..c4b16b961 --- /dev/null +++ b/Source/control/control_chat.cpp @@ -0,0 +1,319 @@ +#include "control_chat.hpp" +#include "control.hpp" +#include "control_panel.hpp" + +#include "control/control_chat_commands.hpp" +#include "engine/backbuffer_state.hpp" +#include "engine/render/clx_render.hpp" +#include "options.h" +#include "panels/console.hpp" +#include "panels/mainpanel.hpp" +#include "quick_messages.hpp" +#include "utils/display.h" +#include "utils/sdl_compat.h" +#include "utils/str_cat.hpp" + +namespace devilution { + +std::optional ChatInputState; +char TalkMessage[MAX_SEND_STR_LEN]; +bool TalkButtonsDown[3]; +int sgbPlrTalkTbl; +bool WhisperList[MAX_PLRS]; +OptionalOwnedClxSpriteList talkButtons; + +namespace { + +char TalkSave[8][MAX_SEND_STR_LEN]; +uint8_t TalkSaveIndex; +uint8_t NextTalkSave; +TextInputCursorState ChatCursor; + +int MuteButtons = 3; +int MuteButtonPadding = 2; +Rectangle MuteButtonRect { { 172, 69 }, { 61, 16 } }; + +void ResetChatMessage() +{ + if (CheckChatCommand(TalkMessage)) + return; + + uint32_t pmask = 0; + + for (size_t i = 0; i < Players.size(); i++) { + if (WhisperList[i]) + pmask |= 1 << i; + } + + NetSendCmdString(pmask, TalkMessage); +} + +void ControlPressEnter() +{ + if (TalkMessage[0] != 0) { + ResetChatMessage(); + uint8_t i = 0; + for (; i < 8; i++) { + if (strcmp(TalkSave[i], TalkMessage) == 0) + break; + } + if (i >= 8) { + strcpy(TalkSave[NextTalkSave], TalkMessage); + NextTalkSave++; + NextTalkSave &= 7; + } else { + uint8_t talkSave = NextTalkSave - 1; + talkSave &= 7; + if (i != talkSave) { + strcpy(TalkSave[i], TalkSave[talkSave]); + *BufCopy(TalkSave[talkSave], ChatInputState->value()) = '\0'; + } + } + TalkMessage[0] = '\0'; + TalkSaveIndex = NextTalkSave; + } + ResetChat(); +} + +void ControlUpDown(int v) +{ + for (int i = 0; i < 8; i++) { + TalkSaveIndex = (v + TalkSaveIndex) & 7; + if (TalkSave[TalkSaveIndex][0] != 0) { + ChatInputState->assign(TalkSave[TalkSaveIndex]); + return; + } + } +} + +} // namespace + +void DrawChatBox(const Surface &out) +{ + if (!ChatFlag) + return; + + const Point mainPanelPosition = GetMainPanel().position; + + DrawPanelBox(out, MakeSdlRect(175, sgbPlrTalkTbl + 20, 294, 5), mainPanelPosition + Displacement { 175, 4 }); + int off = 0; + for (int i = 293; i > 283; off++, i--) { + DrawPanelBox(out, MakeSdlRect((off / 2) + 175, sgbPlrTalkTbl + off + 25, i, 1), mainPanelPosition + Displacement { (off / 2) + 175, off + 9 }); + } + DrawPanelBox(out, MakeSdlRect(185, sgbPlrTalkTbl + 35, 274, 30), mainPanelPosition + Displacement { 185, 19 }); + DrawPanelBox(out, MakeSdlRect(180, sgbPlrTalkTbl + 65, 284, 5), mainPanelPosition + Displacement { 180, 49 }); + for (int i = 0; i < 10; i++) { + DrawPanelBox(out, MakeSdlRect(180, sgbPlrTalkTbl + i + 70, i + 284, 1), mainPanelPosition + Displacement { 180, i + 54 }); + } + DrawPanelBox(out, MakeSdlRect(170, sgbPlrTalkTbl + 80, 310, 55), mainPanelPosition + Displacement { 170, 64 }); + + int x = mainPanelPosition.x + 200; + const int y = mainPanelPosition.y + 10; + + const uint32_t len = DrawString(out, TalkMessage, { { x, y }, { 250, 39 } }, + { + .flags = UiFlags::ColorWhite | UiFlags::PentaCursor, + .lineHeight = 13, + .cursorPosition = static_cast(ChatCursor.position), + .highlightRange = { static_cast(ChatCursor.selection.begin), static_cast(ChatCursor.selection.end) }, + }); + ChatInputState->truncate(len); + + x += 46; + int talkBtn = 0; + for (size_t i = 0; i < Players.size(); i++) { + Player &player = Players[i]; + if (&player == MyPlayer) + continue; + + const UiFlags color = player.friendlyMode ? UiFlags::ColorWhitegold : UiFlags::ColorRed; + const Point talkPanPosition = mainPanelPosition + Displacement { 172, 84 + 18 * talkBtn }; + if (WhisperList[i]) { + // the normal (unpressed) voice button is pre-rendered on the panel, only need to draw over it when the button is held + if (TalkButtonsDown[talkBtn]) { + const unsigned spriteIndex = talkBtn == 0 ? 2 : 3; // the first button sprite includes a tip from the devils wing so is different to the rest. + ClxDraw(out, talkPanPosition, (*talkButtons)[spriteIndex]); + + // Draw the translated string over the top of the default (english) button. This graphic is inset to avoid overlapping the wingtip, letting + // the first button be treated the same as the other two further down the panel. + RenderClxSprite(out, (*TalkButton)[2], talkPanPosition + Displacement { 4, -15 }); + } + } else { + unsigned spriteIndex = talkBtn == 0 ? 0 : 1; // the first button sprite includes a tip from the devils wing so is different to the rest. + if (TalkButtonsDown[talkBtn]) + spriteIndex += 4; // held button sprites are at index 4 and 5 (with and without wingtip respectively) + ClxDraw(out, talkPanPosition, (*talkButtons)[spriteIndex]); + + // Draw the translated string over the top of the default (english) button. This graphic is inset to avoid overlapping the wingtip, letting + // the first button be treated the same as the other two further down the panel. + RenderClxSprite(out, (*TalkButton)[TalkButtonsDown[talkBtn] ? 1 : 0], talkPanPosition + Displacement { 4, -15 }); + } + if (player.plractive) { + DrawString(out, player._pName, { { x, y + 60 + talkBtn * 18 }, { 204, 0 } }, { .flags = color }); + } + + talkBtn++; + } +} + +bool CheckMuteButton() +{ + if (!ChatFlag) + return false; + + Rectangle buttons = MuteButtonRect; + + SetPanelObjectPosition(UiPanels::Main, buttons); + + buttons.size.height = (MuteButtons * buttons.size.height) + ((MuteButtons - 1) * MuteButtonPadding); + + if (!buttons.contains(MousePosition)) + return false; + + for (bool &talkButtonDown : TalkButtonsDown) { + talkButtonDown = false; + } + + const Point mainPanelPosition = GetMainPanel().position; + + TalkButtonsDown[(MousePosition.y - (69 + mainPanelPosition.y)) / 18] = true; + + return true; +} + +void CheckMuteButtonUp() +{ + if (!ChatFlag) + return; + + for (bool &talkButtonDown : TalkButtonsDown) + talkButtonDown = false; + + Rectangle buttons = MuteButtonRect; + + SetPanelObjectPosition(UiPanels::Main, buttons); + + buttons.size.height = (MuteButtons * buttons.size.height) + ((MuteButtons - 1) * MuteButtonPadding); + + if (!buttons.contains(MousePosition)) + return; + + int off = (MousePosition.y - buttons.position.y) / (MuteButtonRect.size.height + MuteButtonPadding); + + size_t playerId = 0; + for (; playerId < Players.size() && off != -1; ++playerId) { + if (playerId != MyPlayerId) + off--; + } + if (playerId > 0 && playerId <= Players.size()) + WhisperList[playerId - 1] = !WhisperList[playerId - 1]; +} + +void TypeChatMessage() +{ + if (!IsChatAvailable()) + return; + + ChatFlag = true; + TalkMessage[0] = '\0'; + ChatInputState.emplace(TextInputState::Options { + .value = TalkMessage, + .cursor = &ChatCursor, + .maxLength = sizeof(TalkMessage) - 1 }); + for (bool &talkButtonDown : TalkButtonsDown) { + talkButtonDown = false; + } + sgbPlrTalkTbl = GetMainPanel().size.height + PanelPaddingHeight; + RedrawEverything(); + TalkSaveIndex = NextTalkSave; + + SDL_Rect rect = MakeSdlRect(GetMainPanel().position.x + 200, GetMainPanel().position.y + 22, 0, 27); + SDL_SetTextInputArea(ghMainWnd, &rect, /*cursor=*/0); + SDLC_StartTextInput(ghMainWnd); +} + +void ResetChat() +{ + ChatFlag = false; + SDLC_StopTextInput(ghMainWnd); + ChatCursor = {}; + ChatInputState = std::nullopt; + sgbPlrTalkTbl = 0; + RedrawEverything(); +} + +bool IsChatActive() +{ + if (!IsChatAvailable()) + return false; + + if (!ChatFlag) + return false; + + return true; +} + +bool CheckKeypress(SDL_Keycode vkey) +{ + if (!IsChatAvailable()) + return false; + if (!ChatFlag) + return false; + + switch (vkey) { + case SDLK_ESCAPE: + ResetChat(); + return true; + case SDLK_RETURN: + case SDLK_KP_ENTER: + ControlPressEnter(); + return true; + case SDLK_DOWN: + ControlUpDown(1); + return true; + case SDLK_UP: + ControlUpDown(-1); + return true; + default: + return vkey >= SDLK_SPACE && vkey <= SDLK_Z; + } +} + +void DiabloHotkeyMsg(uint32_t dwMsg) +{ + assert(dwMsg < QuickMessages.size()); + +#ifdef _DEBUG + constexpr std::string_view LuaPrefix = "/lua "; + for (const std::string &msg : GetOptions().Chat.szHotKeyMsgs[dwMsg]) { + if (!msg.starts_with(LuaPrefix)) continue; + InitConsole(); + RunInConsole(std::string_view(msg).substr(LuaPrefix.size())); + } +#endif + + if (!IsChatAvailable()) { + return; + } + + for (const std::string &msg : GetOptions().Chat.szHotKeyMsgs[dwMsg]) { +#ifdef _DEBUG + if (msg.starts_with(LuaPrefix)) continue; +#endif + char charMsg[MAX_SEND_STR_LEN]; + CopyUtf8(charMsg, msg, sizeof(charMsg)); + NetSendCmdString(0xFFFFFF, charMsg); + } +} + +bool IsChatAvailable() +{ + return gbIsMultiplayer; +} + +bool HandleTalkTextInputEvent(const SDL_Event &event) +{ + return HandleInputEvent(event, ChatInputState); +} + +} // namespace devilution diff --git a/Source/control/control_chat.hpp b/Source/control/control_chat.hpp new file mode 100644 index 000000000..b21aa0fdb --- /dev/null +++ b/Source/control/control_chat.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +#include "DiabloUI/text_input.hpp" +#include "engine/clx_sprite.hpp" +#include "msg.h" +#include "multi.h" + +#ifdef USE_SDL3 +#include +#include +#include +#include +#else +#include + +#ifdef USE_SDL1 +#include "utils/sdl2_to_1_2_backports.h" +#endif +#endif + +namespace devilution { + +extern OptionalOwnedClxSpriteList talkButtons; +extern std::optional ChatInputState; +extern char TalkMessage[MAX_SEND_STR_LEN]; +extern bool TalkButtonsDown[3]; +extern int sgbPlrTalkTbl; +extern bool WhisperList[MAX_PLRS]; + +bool CheckChatCommand(std::string_view text); + +template +bool HandleInputEvent(const SDL_Event &event, std::optional &inputState) +{ + if (!inputState) { + return false; // No input state to handle + } + + if constexpr (std::is_same_v) { + return HandleTextInputEvent(event, *inputState); + } else if constexpr (std::is_same_v) { + return HandleNumberInputEvent(event, *inputState); + } + + return false; // Unknown input state type +} + +} // namespace devilution diff --git a/Source/control/control_chat_commands.cpp b/Source/control/control_chat_commands.cpp new file mode 100644 index 000000000..ec57de005 --- /dev/null +++ b/Source/control/control_chat_commands.cpp @@ -0,0 +1,280 @@ +#include "control_chat_commands.hpp" +#include "control.hpp" + +#include "diablo_msg.hpp" +#include "engine/backbuffer_state.hpp" +#include "inv.h" +#include "levels/setmaps.h" +#include "storm/storm_net.hpp" +#include "utils/algorithm/container.hpp" +#include "utils/log.hpp" +#include "utils/parse_int.hpp" +#include "utils/str_case.hpp" +#include "utils/str_cat.hpp" + +#ifdef _DEBUG +#include "debug.h" +#endif + +namespace devilution { + +namespace { + +struct TextCmdItem { + const std::string text; + const std::string description; + const std::string requiredParameter; + std::string (*actionProc)(const std::string_view); +}; + +extern std::vector TextCmdList; + +std::string TextCmdHelp(const std::string_view parameter) +{ + if (parameter.empty()) { + std::string ret; + StrAppend(ret, _("Available Commands:")); + for (const TextCmdItem &textCmd : TextCmdList) { + StrAppend(ret, " ", _(textCmd.text)); + } + return ret; + } + auto textCmdIterator = c_find_if(TextCmdList, [&](const TextCmdItem &elem) { return elem.text == parameter; }); + if (textCmdIterator == TextCmdList.end()) + return StrCat(_("Command "), parameter, _(" is unknown.")); + auto &textCmdItem = *textCmdIterator; + if (textCmdItem.requiredParameter.empty()) + return StrCat(_("Description: "), _(textCmdItem.description), _("\nParameters: No additional parameter needed.")); + return StrCat(_("Description: "), _(textCmdItem.description), _("\nParameters: "), _(textCmdItem.requiredParameter)); +} + +void AppendArenaOverview(std::string &ret) +{ + for (int arena = SL_FIRST_ARENA; arena <= SL_LAST; arena++) { + StrAppend(ret, "\n", arena - SL_FIRST_ARENA + 1, " (", QuestLevelNames[arena], ")"); + } +} + +std::string TextCmdArena(const std::string_view parameter) +{ + std::string ret; + if (!gbIsMultiplayer) { + StrAppend(ret, _("Arenas are only supported in multiplayer.")); + return ret; + } + + if (parameter.empty()) { + StrAppend(ret, _("What arena do you want to visit?")); + AppendArenaOverview(ret); + return ret; + } + + const ParseIntResult parsedParam = ParseInt(parameter, /*min=*/0); + const _setlevels arenaLevel = parsedParam.has_value() ? static_cast<_setlevels>(parsedParam.value() - 1 + SL_FIRST_ARENA) : _setlevels::SL_NONE; + if (!IsArenaLevel(arenaLevel)) { + StrAppend(ret, _("Invalid arena-number. Valid numbers are:")); + AppendArenaOverview(ret); + return ret; + } + + if (!MyPlayer->isOnLevel(0) && !MyPlayer->isOnArenaLevel()) { + StrAppend(ret, _("To enter a arena, you need to be in town or another arena.")); + return ret; + } + + setlvltype = GetArenaLevelType(arenaLevel); + StartNewLvl(*MyPlayer, WM_DIABSETLVL, arenaLevel); + return ret; +} + +std::string TextCmdArenaPot(const std::string_view parameter) +{ + std::string ret; + if (!gbIsMultiplayer) { + StrAppend(ret, _("Arenas are only supported in multiplayer.")); + return ret; + } + const int numPots = ParseInt(parameter, /*min=*/1).value_or(1); + + Player &myPlayer = *MyPlayer; + + for (int potNumber = numPots; potNumber > 0; potNumber--) { + Item item {}; + InitializeItem(item, IDI_ARENAPOT); + GenerateNewSeed(item); + item.updateRequiredStatsCacheForPlayer(myPlayer); + + if (!AutoPlaceItemInBelt(myPlayer, item, true, true) && !AutoPlaceItemInInventory(myPlayer, item, true)) { + break; // inventory is full + } + } + + return ret; +} + +std::string TextCmdInspect(const std::string_view parameter) +{ + std::string ret; + if (!gbIsMultiplayer) { + StrAppend(ret, _("Inspecting only supported in multiplayer.")); + return ret; + } + + if (parameter.empty()) { + StrAppend(ret, _("Stopped inspecting players.")); + InspectPlayer = MyPlayer; + return ret; + } + + const std::string param = AsciiStrToLower(parameter); + auto it = c_find_if(Players, [¶m](const Player &player) { + return AsciiStrToLower(player._pName) == param; + }); + if (it == Players.end()) { + it = c_find_if(Players, [¶m](const Player &player) { + return AsciiStrToLower(player._pName).find(param) != std::string::npos; + }); + } + if (it == Players.end()) { + StrAppend(ret, _("No players found with such a name")); + return ret; + } + + Player &player = *it; + InspectPlayer = &player; + StrAppend(ret, _("Inspecting player: ")); + StrAppend(ret, player._pName); + OpenCharPanel(); + if (!SpellbookFlag) + invflag = true; + RedrawEverything(); + return ret; +} + +bool IsQuestEnabled(const Quest &quest) +{ + switch (quest._qidx) { + case Q_FARMER: + return gbIsHellfire && !sgGameInitInfo.bCowQuest; + case Q_JERSEY: + return gbIsHellfire && sgGameInitInfo.bCowQuest; + case Q_GIRL: + return gbIsHellfire && sgGameInitInfo.bTheoQuest; + case Q_CORNSTN: + return gbIsHellfire && !gbIsMultiplayer; + case Q_GRAVE: + case Q_DEFILER: + case Q_NAKRUL: + return gbIsHellfire; + case Q_TRADER: + return false; + default: + return quest._qactive != QUEST_NOTAVAIL; + } +} + +std::string TextCmdLevelSeed(const std::string_view parameter) +{ + const std::string_view levelType = setlevel ? "set level" : "dungeon level"; + + char gameId[] = { + static_cast((sgGameInitInfo.programid >> 24) & 0xFF), + static_cast((sgGameInitInfo.programid >> 16) & 0xFF), + static_cast((sgGameInitInfo.programid >> 8) & 0xFF), + static_cast(sgGameInitInfo.programid & 0xFF), + '\0' + }; + + const std::string_view mode = gbIsMultiplayer ? "MP" : "SP"; + const std::string_view questPool = UseMultiplayerQuests() ? "MP" : "Full"; + + uint32_t questFlags = 0; + for (const Quest &quest : Quests) { + questFlags <<= 1; + if (IsQuestEnabled(quest)) + questFlags |= 1; + } + + return StrCat( + "Seedinfo for ", levelType, " ", currlevel, "\n", + "seed: ", DungeonSeeds[currlevel], "\n", +#ifdef _DEBUG + "Mid1: ", glMid1Seed[currlevel], "\n", + "Mid2: ", glMid2Seed[currlevel], "\n", + "Mid3: ", glMid3Seed[currlevel], "\n", + "End: ", glEndSeed[currlevel], "\n", +#endif + "\n", + gameId, " ", mode, "\n", + questPool, " quests: ", questFlags, "\n", + "Storybook: ", DungeonSeeds[16]); +} + +std::string TextCmdPing(const std::string_view parameter) +{ + std::string ret; + const std::string param = AsciiStrToLower(parameter); + auto it = c_find_if(Players, [¶m](const Player &player) { + return AsciiStrToLower(player._pName) == param; + }); + if (it == Players.end()) { + it = c_find_if(Players, [¶m](const Player &player) { + return AsciiStrToLower(player._pName).find(param) != std::string::npos; + }); + } + if (it == Players.end()) { + StrAppend(ret, _("No players found with such a name")); + return ret; + } + + Player &player = *it; + DvlNetLatencies latencies = DvlNet_GetLatencies(player.getId()); + + StrAppend(ret, fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} means: Character Name */ "Latency statistics for {:s}:")), player.name())); + + StrAppend(ret, "\n", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Echo latency: {:d} ms")), latencies.echoLatency)); + + if (latencies.providerLatency) { + if (latencies.isRelayed && *latencies.isRelayed) { + StrAppend(ret, "\n", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms (Relayed)")), *latencies.providerLatency)); + } else { + StrAppend(ret, "\n", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms")), *latencies.providerLatency)); + } + } + + return ret; +} + +std::vector TextCmdList = { + { "/help", N_("Prints help overview or help for a specific command."), N_("[command]"), &TextCmdHelp }, + { "/arena", N_("Enter a PvP Arena."), N_(""), &TextCmdArena }, + { "/arenapot", N_("Gives Arena Potions."), N_(""), &TextCmdArenaPot }, + { "/inspect", N_("Inspects stats and equipment of another player."), N_(""), &TextCmdInspect }, + { "/seedinfo", N_("Show seed infos for current level."), "", &TextCmdLevelSeed }, + { "/ping", N_("Show latency statistics for another player."), N_(""), &TextCmdPing }, +}; + +} // namespace + +bool CheckChatCommand(const std::string_view text) +{ + if (text.size() < 1 || text[0] != '/') + return false; + + auto textCmdIterator = c_find_if(TextCmdList, [&](const TextCmdItem &elem) { return text.find(elem.text) == 0 && (text.length() == elem.text.length() || text[elem.text.length()] == ' '); }); + if (textCmdIterator == TextCmdList.end()) { + InitDiabloMsg(StrCat(_("Command "), "\"", text, "\"", _(" is unknown."))); + return true; + } + + const TextCmdItem &textCmd = *textCmdIterator; + std::string_view parameter = ""; + if (text.length() > (textCmd.text.length() + 1)) + parameter = text.substr(textCmd.text.length() + 1); + const std::string result = textCmd.actionProc(parameter); + if (result != "") + InitDiabloMsg(result); + return true; +} + +} // namespace devilution diff --git a/Source/control/control_chat_commands.hpp b/Source/control/control_chat_commands.hpp new file mode 100644 index 000000000..599ebcf31 --- /dev/null +++ b/Source/control/control_chat_commands.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace devilution { + +bool CheckChatCommand(std::string_view text); + +} // namespace devilution diff --git a/Source/control/control_flasks.cpp b/Source/control/control_flasks.cpp new file mode 100644 index 000000000..ad5449f70 --- /dev/null +++ b/Source/control/control_flasks.cpp @@ -0,0 +1,149 @@ +#include "control_flasks.hpp" +#include "control.hpp" + +#include "engine/surface.hpp" +#include "utils/str_cat.hpp" + +namespace devilution { + +std::optional pLifeBuff; +std::optional pManaBuff; + +namespace { + +Rectangle FlaskTopRect { { 11, 3 }, { 62, 13 } }; +Rectangle FlaskBottomRect { { 0, 16 }, { 88, 69 } }; + +/** + * Draws the dome of the flask that protrudes above the panel top line. + * It draws a rectangle of fixed width 59 and height 'h' from the source buffer + * into the target buffer. + * @param out The target buffer. + * @param celBuf Buffer of the flask cel. + * @param targetPosition Target buffer coordinate. + */ +void DrawFlaskAbovePanel(const Surface &out, const Surface &celBuf, Point targetPosition) +{ + out.BlitFromSkipColorIndexZero(celBuf, MakeSdlRect(0, 0, celBuf.w(), celBuf.h()), targetPosition); +} + +/** + * @brief Draws the part of the life/mana flasks protruding above the bottom panel + * @see DrawFlaskLower() + * @param out The display region to draw to + * @param sourceBuffer A sprite representing the appropriate background/empty flask style + * @param offset X coordinate offset for where the flask should be drawn + * @param fillPer How full the flask is (a value from 0 to 81) + */ +void DrawFlaskUpper(const Surface &out, const Surface &sourceBuffer, int offset, int fillPer) +{ + const Rectangle &rect = FlaskTopRect; + const int emptyRows = std::clamp(81 - fillPer, 0, rect.size.height); + const int filledRows = rect.size.height - emptyRows; + + // Draw the empty part of the flask + DrawFlaskAbovePanel(out, + sourceBuffer.subregion(rect.position.x, rect.position.y, rect.size.width, rect.size.height), + GetMainPanel().position + Displacement { offset, -rect.size.height }); + + // Draw the filled part of the flask over the empty part + if (filledRows > 0) { + DrawFlaskAbovePanel(out, + BottomBuffer->subregion(offset, rect.position.y + emptyRows, rect.size.width, filledRows), + GetMainPanel().position + Displacement { offset, -rect.size.height + emptyRows }); + } +} + +/** + * Draws a section of the empty flask cel on top of the panel to create the illusion + * of the flask getting empty. This function takes a cel and draws a + * horizontal stripe of height (max-min) onto the given buffer. + * @param out Target buffer. + * @param celBuf Buffer of the flask cel. + * @param targetPosition Target buffer coordinate. + */ +void DrawFlaskOnPanel(const Surface &out, const Surface &celBuf, Point targetPosition) +{ + out.BlitFrom(celBuf, MakeSdlRect(0, 0, celBuf.w(), celBuf.h()), targetPosition); +} + +/** + * @brief Draws the part of the life/mana flasks inside the bottom panel + * @see DrawFlaskUpper() + * @param out The display region to draw to + * @param sourceBuffer A sprite representing the appropriate background/empty flask style + * @param offset X coordinate offset for where the flask should be drawn + * @param fillPer How full the flask is (a value from 0 to 80) + * @param drawFilledPortion Indicates whether to draw the filled portion of the flask + */ +void DrawFlaskLower(const Surface &out, const Surface &sourceBuffer, int offset, int fillPer, bool drawFilledPortion) +{ + const Rectangle &rect = FlaskBottomRect; + const int filledRows = std::clamp(fillPer, 0, rect.size.height); + const int emptyRows = rect.size.height - filledRows; + + // Draw the empty part of the flask + if (emptyRows > 0) { + DrawFlaskOnPanel(out, + sourceBuffer.subregion(rect.position.x, rect.position.y, rect.size.width, emptyRows), + GetMainPanel().position + Displacement { offset, 0 }); + } + + // Draw the filled part of the flask + if (drawFilledPortion && filledRows > 0) { + DrawFlaskOnPanel(out, + BottomBuffer->subregion(offset, rect.position.y + emptyRows, rect.size.width, filledRows), + GetMainPanel().position + Displacement { offset, emptyRows }); + } +} + +} // namespace + +void DrawLifeFlaskUpper(const Surface &out) +{ + constexpr int LifeFlaskUpperOffset = 107; + DrawFlaskUpper(out, *pLifeBuff, LifeFlaskUpperOffset, MyPlayer->_pHPPer); +} + +void DrawManaFlaskUpper(const Surface &out) +{ + constexpr int ManaFlaskUpperOffset = 475; + DrawFlaskUpper(out, *pManaBuff, ManaFlaskUpperOffset, MyPlayer->_pManaPer); +} + +void DrawLifeFlaskLower(const Surface &out, bool drawFilledPortion) +{ + constexpr int LifeFlaskLowerOffset = 96; + DrawFlaskLower(out, *pLifeBuff, LifeFlaskLowerOffset, MyPlayer->_pHPPer, drawFilledPortion); +} + +void DrawManaFlaskLower(const Surface &out, bool drawFilledPortion) +{ + constexpr int ManaFlaskLowerOffset = 464; + DrawFlaskLower(out, *pManaBuff, ManaFlaskLowerOffset, MyPlayer->_pManaPer, drawFilledPortion); +} + +void DrawFlaskValues(const Surface &out, Point pos, int currValue, int maxValue) +{ + const UiFlags color = (currValue > 0 ? (currValue == maxValue ? UiFlags::ColorGold : UiFlags::ColorWhite) : UiFlags::ColorRed); + + auto drawStringWithShadow = [out, color](std::string_view text, Point pos) { + DrawString(out, text, pos + Displacement { -1, -1 }, + { .flags = UiFlags::ColorBlack | UiFlags::KerningFitSpacing, .spacing = 0 }); + DrawString(out, text, pos, + { .flags = color | UiFlags::KerningFitSpacing, .spacing = 0 }); + }; + + const std::string currText = StrCat(currValue); + drawStringWithShadow(currText, pos - Displacement { GetLineWidth(currText, GameFont12) + 1, 0 }); + drawStringWithShadow("/", pos); + drawStringWithShadow(StrCat(maxValue), pos + Displacement { GetLineWidth("/", GameFont12) + 1, 0 }); +} + +void UpdateLifeManaPercent() +{ + MyPlayer->UpdateManaPercentage(); + MyPlayer->UpdateHitPointPercentage(); +} + +} // namespace devilution diff --git a/Source/control/control_flasks.hpp b/Source/control/control_flasks.hpp new file mode 100644 index 000000000..3cc97f834 --- /dev/null +++ b/Source/control/control_flasks.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +#include "engine/surface.hpp" + +namespace devilution { + +extern std::optional pLifeBuff; +extern std::optional pManaBuff; + +} // namespace devilution diff --git a/Source/control/control_gold.cpp b/Source/control/control_gold.cpp new file mode 100644 index 000000000..8cdfb12e4 --- /dev/null +++ b/Source/control/control_gold.cpp @@ -0,0 +1,141 @@ +#include "control.hpp" +#include "control_chat.hpp" + +#include "DiabloUI/text_input.hpp" +#include "engine/render/clx_render.hpp" +#include "inv.h" +#include "utils/display.h" +#include "utils/format_int.hpp" +#include "utils/log.hpp" +#include "utils/sdl_compat.h" + +namespace devilution { + +bool DropGoldFlag; +TextInputCursorState GoldDropCursor; +char GoldDropText[21]; + +namespace { + +int8_t GoldDropInvIndex; +std::optional GoldDropInputState; + +void RemoveGold(Player &player, int goldIndex, int amount) +{ + const int gi = goldIndex - INVITEM_INV_FIRST; + player.InvList[gi]._ivalue -= amount; + if (player.InvList[gi]._ivalue > 0) { + SetPlrHandGoldCurs(player.InvList[gi]); + NetSyncInvItem(player, gi); + } else { + player.RemoveInvItem(gi); + } + + MakeGoldStack(player.HoldItem, amount); + NewCursor(player.HoldItem); + + player._pGold = CalculateGold(player); +} + +int GetGoldDropMax() +{ + return GoldDropInputState->max(); +} + +} // namespace + +void DrawGoldSplit(const Surface &out) +{ + const int dialogX = 30; + + ClxDraw(out, GetPanelPosition(UiPanels::Inventory, { dialogX, 178 }), (*GoldBoxBuffer)[0]); + + const std::string_view amountText = GoldDropText; + const TextInputCursorState &cursor = GoldDropCursor; + const int max = GetGoldDropMax(); + + const std::string description = fmt::format( + fmt::runtime(ngettext( + /* TRANSLATORS: {:s} is a number with separators. Dialog is shown when splitting a stash of Gold.*/ + "You have {:s} gold piece. How many do you want to remove?", + "You have {:s} gold pieces. How many do you want to remove?", + max)), + FormatInteger(max)); + + // Pre-wrap the string at spaces, otherwise DrawString would hard wrap in the middle of words + const std::string wrapped = WordWrapString(description, 200); + + // The split gold dialog is roughly 4 lines high, but we need at least one line for the player to input an amount. + // Using a clipping region 50 units high (approx 3 lines with a lineheight of 17) to ensure there is enough room left + // for the text entered by the player. + DrawString(out, wrapped, { GetPanelPosition(UiPanels::Inventory, { dialogX + 31, 75 }), { 200, 50 } }, + { .flags = UiFlags::ColorWhitegold | UiFlags::AlignCenter, .lineHeight = 17 }); + + // Even a ten digit amount of gold only takes up about half a line. There's no need to wrap or clip text here so we + // use the Point form of DrawString. + DrawString(out, amountText, GetPanelPosition(UiPanels::Inventory, { dialogX + 37, 128 }), + { + .flags = UiFlags::ColorWhite | UiFlags::PentaCursor, + .cursorPosition = static_cast(cursor.position), + .highlightRange = { static_cast(cursor.selection.begin), static_cast(cursor.selection.end) }, + }); +} + +void control_drop_gold(SDL_Keycode vkey) +{ + Player &myPlayer = *MyPlayer; + + if (myPlayer.hasNoLife()) { + CloseGoldDrop(); + return; + } + + switch (vkey) { + case SDLK_RETURN: + case SDLK_KP_ENTER: + if (const int value = GoldDropInputState->value(); value != 0) { + RemoveGold(myPlayer, GoldDropInvIndex, value); + } + CloseGoldDrop(); + break; + case SDLK_ESCAPE: + CloseGoldDrop(); + break; + default: + break; + } +} + +void OpenGoldDrop(int8_t invIndex, int max) +{ + DropGoldFlag = true; + GoldDropInvIndex = invIndex; + GoldDropText[0] = '\0'; + GoldDropInputState.emplace(NumberInputState::Options { + .textOptions { + .value = GoldDropText, + .cursor = &GoldDropCursor, + .maxLength = sizeof(GoldDropText) - 1, + }, + .min = 0, + .max = max, + }); + SDLC_StartTextInput(ghMainWnd); +} + +void CloseGoldDrop() +{ + if (!DropGoldFlag) + return; + SDLC_StopTextInput(ghMainWnd); + DropGoldFlag = false; + GoldDropInputState = std::nullopt; + GoldDropInvIndex = 0; +} + +bool HandleGoldDropTextInputEvent(const SDL_Event &event) +{ + return HandleInputEvent(event, GoldDropInputState); +} + +} // namespace devilution diff --git a/Source/control/control_infobox.cpp b/Source/control/control_infobox.cpp new file mode 100644 index 000000000..30469e973 --- /dev/null +++ b/Source/control/control_infobox.cpp @@ -0,0 +1,425 @@ +#include "control.hpp" +#include "control_panel.hpp" + +#include "engine/render/primitive_render.hpp" +#include "inv.h" +#include "levels/trigs.h" +#include "panels/partypanel.hpp" +#include "qol/stash.h" +#include "qol/xpbar.h" +#include "towners.h" +#include "utils/algorithm/container.hpp" +#include "utils/format_int.hpp" +#include "utils/log.hpp" +#include "utils/screen_reader.hpp" +#include "utils/str_cat.hpp" +#include "utils/str_split.hpp" + +namespace devilution { + +StringOrView InfoString; +StringOrView FloatingInfoString; + +namespace { + +void PrintInfo(const Surface &out) +{ + if (ChatFlag) + return; + + const int space[] = { 18, 12, 6, 3, 0 }; + Rectangle infoBox = InfoBoxRect; + + SetPanelObjectPosition(UiPanels::Main, infoBox); + + const auto newLineCount = static_cast(c_count(InfoString.str(), '\n')); + const int spaceIndex = std::min(4, newLineCount); + const int spacing = space[spaceIndex]; + const int lineHeight = 12 + spacing; + + // Adjusting the line height to add spacing between lines + // will also add additional space beneath the last line + // which throws off the vertical centering + infoBox.position.y += spacing / 2; + + SpeakText(InfoString); + + DrawString(out, InfoString, infoBox, + { + .flags = InfoColor | UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::KerningFitSpacing, + .spacing = 2, + .lineHeight = lineHeight, + }); +} + +Rectangle GetFloatingInfoRect(const int lineHeight, const int textSpacing) +{ + // Calculate the width and height of the floating info box + const std::string txt = std::string(FloatingInfoString); + + auto lines = SplitByChar(txt, '\n'); + const GameFontTables font = GameFont12; + int maxW = 0; + + for (const auto &line : lines) { + const int w = GetLineWidth(line, font, textSpacing, nullptr); + maxW = std::max(maxW, w); + } + + const auto lineCount = 1 + static_cast(c_count(FloatingInfoString.str(), '\n')); + const int totalH = lineCount * lineHeight; + + const Player &player = *InspectPlayer; + + // 1) Equipment (Rect position) + if (pcursinvitem >= INVITEM_HEAD && pcursinvitem < INVITEM_INV_FIRST) { + const int slot = pcursinvitem - INVITEM_HEAD; + static constexpr Point equipLocal[] = { + { 133, 59 }, + { 48, 205 }, + { 249, 205 }, + { 205, 60 }, + { 17, 160 }, + { 248, 160 }, + { 133, 160 }, + }; + + Point itemPosition = equipLocal[slot]; + auto &item = player.InvBody[slot]; + const Size frame = GetInvItemSize(item._iCurs + CURSOR_FIRSTITEM); + + if (slot == INVLOC_HAND_LEFT) { + itemPosition.x += frame.width == InventorySlotSizeInPixels.width + ? InventorySlotSizeInPixels.width + : 0; + itemPosition.y += frame.height == 3 * InventorySlotSizeInPixels.height + ? 0 + : -InventorySlotSizeInPixels.height; + } else if (slot == INVLOC_HAND_RIGHT) { + itemPosition.x += frame.width == InventorySlotSizeInPixels.width + ? (InventorySlotSizeInPixels.width - 1) + : 1; + itemPosition.y += frame.height == 3 * InventorySlotSizeInPixels.height + ? 0 + : -InventorySlotSizeInPixels.height; + } + + itemPosition.y++; // Align position to bottom left of the item graphic + itemPosition.x += frame.width / 2; // Align position to center of the item graphic + itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box + + const Point screen = GetPanelPosition(UiPanels::Inventory, itemPosition); + + return { { screen.x, screen.y }, { maxW, totalH } }; + } + + // 2) Inventory grid (Rect position) + if (pcursinvitem >= INVITEM_INV_FIRST && pcursinvitem < INVITEM_INV_FIRST + InventoryGridCells) { + const int itemIdx = pcursinvitem - INVITEM_INV_FIRST; + + for (int j = 0; j < InventoryGridCells; ++j) { + if (player.InvGrid[j] > 0 && player.InvGrid[j] - 1 == itemIdx) { + const Item &it = player.InvList[itemIdx]; + Point itemPosition = InvRect[j + SLOTXY_INV_FIRST].position; + + itemPosition.x += GetInventorySize(it).width * InventorySlotSizeInPixels.width / 2; // Align position to center of the item graphic + itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box + + const Point screen = GetPanelPosition(UiPanels::Inventory, itemPosition); + + return { { screen.x, screen.y }, { maxW, totalH } }; + } + } + } + + // 3) Belt (Rect position) + if (pcursinvitem >= INVITEM_BELT_FIRST && pcursinvitem < INVITEM_BELT_FIRST + MaxBeltItems) { + const int itemIdx = pcursinvitem - INVITEM_BELT_FIRST; + for (int i = 0; i < MaxBeltItems; ++i) { + if (player.SpdList[i].isEmpty()) + continue; + if (i != itemIdx) + continue; + + const Item &item = player.SpdList[i]; + Point itemPosition = InvRect[i + SLOTXY_BELT_FIRST].position; + + itemPosition.x += GetInventorySize(item).width * InventorySlotSizeInPixels.width / 2; // Align position to center of the item graphic + itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box + + const Point screen = GetMainPanel().position + Displacement { itemPosition.x, itemPosition.y }; + + return { { screen.x, screen.y }, { maxW, totalH } }; + } + } + + // 4) Stash (Rect position) + if (pcursstashitem != StashStruct::EmptyCell) { + for (auto slot : StashGridRange) { + auto itemId = Stash.GetItemIdAtPosition(slot); + if (itemId == StashStruct::EmptyCell) + continue; + if (itemId != pcursstashitem) + continue; + + const Item &item = Stash.stashList[itemId]; + Point itemPosition = GetStashSlotCoord(slot); + const Size itemGridSize = GetInventorySize(item); + + itemPosition.y += itemGridSize.height * (InventorySlotSizeInPixels.height + 1) - 1; // Align position to bottom left of the item graphic + itemPosition.x += itemGridSize.width * InventorySlotSizeInPixels.width / 2; // Align position to center of the item graphic + itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box + + return { { itemPosition.x, itemPosition.y }, { maxW, totalH } }; + } + } + + return { { 0, 0 }, { 0, 0 } }; +} + +int GetHoverSpriteHeight() +{ + if (pcursinvitem >= INVITEM_HEAD && pcursinvitem < INVITEM_INV_FIRST) { + auto &it = (*InspectPlayer).InvBody[pcursinvitem - INVITEM_HEAD]; + return GetInvItemSize(it._iCurs + CURSOR_FIRSTITEM).height + 1; + } + if (pcursinvitem >= INVITEM_INV_FIRST + && pcursinvitem < INVITEM_INV_FIRST + InventoryGridCells) { + const int idx = pcursinvitem - INVITEM_INV_FIRST; + auto &it = (*InspectPlayer).InvList[idx]; + return GetInventorySize(it).height * (InventorySlotSizeInPixels.height + 1) + - InventorySlotSizeInPixels.height; + } + if (pcursinvitem >= INVITEM_BELT_FIRST + && pcursinvitem < INVITEM_BELT_FIRST + MaxBeltItems) { + const int idx = pcursinvitem - INVITEM_BELT_FIRST; + auto &it = (*InspectPlayer).SpdList[idx]; + return GetInventorySize(it).height * (InventorySlotSizeInPixels.height + 1) + - InventorySlotSizeInPixels.height - 1; + } + if (pcursstashitem != StashStruct::EmptyCell) { + auto &it = Stash.stashList[pcursstashitem]; + return GetInventorySize(it).height * (InventorySlotSizeInPixels.height + 1); + } + return InventorySlotSizeInPixels.height; +} + +int ClampAboveOrBelow(int anchorY, int spriteH, int boxH, int pad, int linePad) +{ + const int yAbove = anchorY - spriteH - boxH - pad; + const int yBelow = anchorY + linePad / 2 + pad; + return (yAbove >= 0) ? yAbove : yBelow; +} + +void PrintFloatingInfo(const Surface &out) +{ + if (ChatFlag) + return; + if (FloatingInfoString.empty()) + return; + + const int verticalSpacing = 3; + const int lineHeight = 12 + verticalSpacing; + const int textSpacing = 2; + const int hPadding = 5; + const int vPadding = 4; + + Rectangle floatingInfoBox = GetFloatingInfoRect(lineHeight, textSpacing); + + // Prevent the floating info box from going off-screen horizontally + floatingInfoBox.position.x = std::clamp(floatingInfoBox.position.x, hPadding, GetScreenWidth() - (floatingInfoBox.size.width + hPadding)); + + const int spriteH = GetHoverSpriteHeight(); + const int anchorY = floatingInfoBox.position.y; + + // Prevent the floating info box from going off-screen vertically + floatingInfoBox.position.y = ClampAboveOrBelow(anchorY, spriteH, floatingInfoBox.size.height, vPadding, verticalSpacing); + + SpeakText(FloatingInfoString); + + for (int i = 0; i < 3; i++) + DrawHalfTransparentRectTo(out, floatingInfoBox.position.x - hPadding, floatingInfoBox.position.y - vPadding, floatingInfoBox.size.width + hPadding * 2, floatingInfoBox.size.height + vPadding * 2); + DrawHalfTransparentVerticalLine(out, { floatingInfoBox.position.x - hPadding - 1, floatingInfoBox.position.y - vPadding - 1 }, floatingInfoBox.size.height + (vPadding * 2) + 2, PAL16_GRAY + 10); + DrawHalfTransparentVerticalLine(out, { floatingInfoBox.position.x + hPadding + floatingInfoBox.size.width, floatingInfoBox.position.y - vPadding - 1 }, floatingInfoBox.size.height + (vPadding * 2) + 2, PAL16_GRAY + 10); + DrawHalfTransparentHorizontalLine(out, { floatingInfoBox.position.x - hPadding, floatingInfoBox.position.y - vPadding - 1 }, floatingInfoBox.size.width + (hPadding * 2), PAL16_GRAY + 10); + DrawHalfTransparentHorizontalLine(out, { floatingInfoBox.position.x - hPadding, floatingInfoBox.position.y + vPadding + floatingInfoBox.size.height }, floatingInfoBox.size.width + (hPadding * 2), PAL16_GRAY + 10); + + DrawString(out, FloatingInfoString, floatingInfoBox, + { + .flags = InfoColor | UiFlags::AlignCenter | UiFlags::VerticalCenter, + .spacing = textSpacing, + .lineHeight = lineHeight, + }); +} + +} // namespace + +void AddInfoBoxString(std::string_view str, bool floatingBox /*= false*/) +{ + StringOrView &infoString = floatingBox ? FloatingInfoString : InfoString; + + if (infoString.empty()) + infoString = str; + else + infoString = StrCat(infoString, "\n", str); +} + +void AddInfoBoxString(std::string &&str, bool floatingBox /*= false*/) +{ + StringOrView &infoString = floatingBox ? FloatingInfoString : InfoString; + + if (infoString.empty()) + infoString = std::move(str); + else + infoString = StrCat(infoString, "\n", str); +} + +void CheckPanelInfo() +{ + MainPanelFlag = false; + InfoString = StringOrView {}; + FloatingInfoString = StringOrView {}; + + const int totalButtons = IsChatAvailable() ? TotalMpMainPanelButtons : TotalSpMainPanelButtons; + + for (int i = 0; i < totalButtons; i++) { + Rectangle button = MainPanelButtonRect[i]; + + SetPanelObjectPosition(UiPanels::Main, button); + + if (button.contains(MousePosition)) { + if (i != 7) { + InfoString = _(PanBtnStr[i]); + } else { + if (MyPlayer->friendlyMode) + InfoString = _("Player friendly"); + else + InfoString = _("Player attack"); + } + if (PanBtnHotKey[i] != nullptr) { + AddInfoBoxString(fmt::format(fmt::runtime(_("Hotkey: {:s}")), _(PanBtnHotKey[i]))); + } + InfoColor = UiFlags::ColorWhite; + MainPanelFlag = true; + } + } + + Rectangle spellSelectButton = SpellButtonRect; + + SetPanelObjectPosition(UiPanels::Main, spellSelectButton); + + if (!SpellSelectFlag && spellSelectButton.contains(MousePosition)) { + InfoString = _("Select current spell button"); + InfoColor = UiFlags::ColorWhite; + MainPanelFlag = true; + AddInfoBoxString(_("Hotkey: 's'")); + const Player &myPlayer = *MyPlayer; + const SpellID spellId = myPlayer._pRSpell; + if (IsValidSpell(spellId)) { + switch (myPlayer._pRSplType) { + case SpellType::Skill: + AddInfoBoxString(fmt::format(fmt::runtime(_("{:s} Skill")), pgettext("spell", GetSpellData(spellId).sNameText))); + break; + case SpellType::Spell: { + AddInfoBoxString(fmt::format(fmt::runtime(_("{:s} Spell")), pgettext("spell", GetSpellData(spellId).sNameText))); + const int spellLevel = myPlayer.GetSpellLevel(spellId); + AddInfoBoxString(spellLevel == 0 ? _("Spell Level 0 - Unusable") : fmt::format(fmt::runtime(_("Spell Level {:d}")), spellLevel)); + } break; + case SpellType::Scroll: { + AddInfoBoxString(fmt::format(fmt::runtime(_("Scroll of {:s}")), pgettext("spell", GetSpellData(spellId).sNameText))); + const int scrollCount = c_count_if(InventoryAndBeltPlayerItemsRange { myPlayer }, [spellId](const Item &item) { + return item.isScrollOf(spellId); + }); + AddInfoBoxString(fmt::format(fmt::runtime(ngettext("{:d} Scroll", "{:d} Scrolls", scrollCount)), scrollCount)); + } break; + case SpellType::Charges: + AddInfoBoxString(fmt::format(fmt::runtime(_("Staff of {:s}")), pgettext("spell", GetSpellData(spellId).sNameText))); + AddInfoBoxString(fmt::format(fmt::runtime(ngettext("{:d} Charge", "{:d} Charges", myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges)), myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges)); + break; + case SpellType::Invalid: + break; + } + } + } + + Rectangle belt = BeltRect; + + SetPanelObjectPosition(UiPanels::Main, belt); + + if (belt.contains(MousePosition)) + pcursinvitem = CheckInvHLight(); + + if (CheckXPBarInfo()) + MainPanelFlag = true; +} + +void DrawInfoBox(const Surface &out) +{ + DrawPanelBox(out, MakeSdlRect(InfoBoxRect.position.x, InfoBoxRect.position.y + PanelPaddingHeight, InfoBoxRect.size.width, InfoBoxRect.size.height), GetMainPanel().position + Displacement { InfoBoxRect.position.x, InfoBoxRect.position.y }); + if (!MainPanelFlag && !trigflag && pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell && !SpellSelectFlag && pcurs != CURSOR_HOURGLASS) { + InfoString = StringOrView {}; + InfoColor = UiFlags::ColorWhite; + } + const Player &myPlayer = *MyPlayer; + if (SpellSelectFlag || trigflag || pcurs == CURSOR_HOURGLASS) { + InfoColor = UiFlags::ColorWhite; + } else if (!myPlayer.HoldItem.isEmpty()) { + if (myPlayer.HoldItem._itype == ItemType::Gold) { + const int nGold = myPlayer.HoldItem._ivalue; + InfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)); + } else if (!myPlayer.CanUseItem(myPlayer.HoldItem)) { + InfoString = _("Requirements not met"); + } else { + InfoString = myPlayer.HoldItem.getName(); + InfoColor = myPlayer.HoldItem.getTextColor(); + } + } else { + if (pcursitem != -1) + GetItemStr(Items[pcursitem]); + else if (ObjectUnderCursor != nullptr) + GetObjectStr(*ObjectUnderCursor); + if (pcursmonst != -1) { + if (leveltype != DTYPE_TOWN) { + const Monster &monster = Monsters[pcursmonst]; + InfoColor = UiFlags::ColorWhite; + InfoString = monster.name(); + if (monster.isUnique()) { + InfoColor = UiFlags::ColorWhitegold; + PrintUniqueHistory(); + } else { + PrintMonstHistory(monster.type().type); + } + } else if (pcursitem == -1) { + InfoString = std::string_view(Towners[pcursmonst].name); + } + } + if (PlayerUnderCursor != nullptr) { + InfoColor = UiFlags::ColorWhitegold; + const auto &target = *PlayerUnderCursor; + InfoString = std::string_view(target._pName); + AddInfoBoxString(fmt::format(fmt::runtime(_("{:s}, Level: {:d}")), target.getClassName(), target.getCharacterLevel())); + AddInfoBoxString(fmt::format(fmt::runtime(_("Hit Points {:d} of {:d}")), target._pHitPoints >> 6, target._pMaxHP >> 6)); + } + if (PortraitIdUnderCursor != -1) { + InfoColor = UiFlags::ColorWhitegold; + auto &target = Players[PortraitIdUnderCursor]; + InfoString = std::string_view(target._pName); + AddInfoBoxString(_("Right click to inspect")); + } + } + if (!InfoString.empty()) + PrintInfo(out); +} + +void DrawFloatingInfoBox(const Surface &out) +{ + if (pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell) { + FloatingInfoString = StringOrView {}; + InfoColor = UiFlags::ColorWhite; + } + + if (!FloatingInfoString.empty()) + PrintFloatingInfo(out); +} + +} // namespace devilution diff --git a/Source/control/control_panel.cpp b/Source/control/control_panel.cpp new file mode 100644 index 000000000..721c0cea7 --- /dev/null +++ b/Source/control/control_panel.cpp @@ -0,0 +1,824 @@ +#include "control_panel.hpp" +#include "control.hpp" +#include "control_chat.hpp" +#include "control_flasks.hpp" + +#include "automap.h" +#include "controls/control_mode.hpp" +#include "controls/modifier_hints.h" +#include "diablo_msg.hpp" +#include "engine/backbuffer_state.hpp" +#include "engine/load_cel.hpp" +#include "engine/render/clx_render.hpp" +#include "engine/trn.hpp" +#include "gamemenu.h" +#include "headless_mode.hpp" +#include "minitext.h" +#include "options.h" +#include "panels/charpanel.hpp" +#include "panels/mainpanel.hpp" +#include "panels/partypanel.hpp" +#include "panels/spell_book.hpp" +#include "panels/spell_icons.hpp" +#include "panels/spell_list.hpp" +#include "pfile.h" +#include "qol/stash.h" +#include "stores.h" +#include "utils/sdl_compat.h" + +namespace devilution { + +bool CharPanelButton[4]; +bool LevelButtonDown; +bool CharPanelButtonActive; +UiFlags InfoColor; +int SpellbookTab; +bool ChatFlag; +bool SpellbookFlag; +bool CharFlag; +bool MainPanelFlag; +bool MainPanelButtonDown; +bool SpellSelectFlag; +Rectangle MainPanel; +Rectangle LeftPanel; +Rectangle RightPanel; +std::optional BottomBuffer; +OptionalOwnedClxSpriteList GoldBoxBuffer; + +const Rectangle &GetMainPanel() +{ + return MainPanel; +} +const Rectangle &GetLeftPanel() +{ + return LeftPanel; +} +const Rectangle &GetRightPanel() +{ + return RightPanel; +} +bool IsLeftPanelOpen() +{ + return CharFlag || QuestLogIsOpen || IsStashOpen; +} +bool IsRightPanelOpen() +{ + return invflag || SpellbookFlag; +} + +constexpr Size IncrementAttributeButtonSize { 41, 22 }; +/** Maps from attribute_id to the rectangle on screen used for attribute increment buttons. */ +Rectangle CharPanelButtonRect[4] = { + { { 137, 138 }, IncrementAttributeButtonSize }, + { { 137, 166 }, IncrementAttributeButtonSize }, + { { 137, 195 }, IncrementAttributeButtonSize }, + { { 137, 223 }, IncrementAttributeButtonSize } +}; + +constexpr Size WidePanelButtonSize { 71, 20 }; +constexpr Size PanelButtonSize { 33, 32 }; +/** Positions of panel buttons. */ +Rectangle MainPanelButtonRect[8] = { + // clang-format off + { { 9, 9 }, WidePanelButtonSize }, // char button + { { 9, 35 }, WidePanelButtonSize }, // quests button + { { 9, 75 }, WidePanelButtonSize }, // map button + { { 9, 101 }, WidePanelButtonSize }, // menu button + { { 560, 9 }, WidePanelButtonSize }, // inv button + { { 560, 35 }, WidePanelButtonSize }, // spells button + { { 87, 91 }, PanelButtonSize }, // chat button + { { 527, 91 }, PanelButtonSize }, // friendly fire button + // clang-format on +}; + +Rectangle LevelButtonRect = { { 40, -39 }, { 41, 22 } }; + +constexpr int BeltItems = 8; +constexpr Size BeltSize { (INV_SLOT_SIZE_PX + 1) * BeltItems, INV_SLOT_SIZE_PX }; +Rectangle BeltRect { { 205, 5 }, BeltSize }; + +Rectangle SpellButtonRect { { 565, 64 }, { 56, 56 } }; + +int PanelPaddingHeight = 16; + +/** Maps from panel_button_id to panel button description. */ +const char *const PanBtnStr[8] = { + N_("Character Information"), + N_("Quests log"), + N_("Automap"), + N_("Main Menu"), + N_("Inventory"), + N_("Spell book"), + N_("Send Message"), + "" // Player attack +}; + +/** Maps from panel_button_id to hotkey name. */ +const char *const PanBtnHotKey[8] = { "'c'", "'q'", N_("Tab"), N_("Esc"), "'i'", "'b'", N_("Enter"), nullptr }; + +int TotalSpMainPanelButtons = 6; +int TotalMpMainPanelButtons = 8; + +namespace { + +OptionalOwnedClxSpriteList pDurIcons; +OptionalOwnedClxSpriteList multiButtons; +OptionalOwnedClxSpriteList pMainPanelButtons; + +enum panel_button_id : uint8_t { + PanelButtonCharinfo, + PanelButtonFirst = PanelButtonCharinfo, + PanelButtonQlog, + PanelButtonAutomap, + PanelButtonMainmenu, + PanelButtonInventory, + PanelButtonSpellbook, + PanelButtonSendmsg, + PanelButtonFriendly, + PanelButtonLast = PanelButtonFriendly, +}; + +bool MainPanelButtons[PanelButtonLast + 1]; + +void SetMainPanelButtonDown(int btnId) +{ + MainPanelButtons[btnId] = true; + RedrawComponent(PanelDrawComponent::ControlButtons); + MainPanelButtonDown = true; +} + +void SetMainPanelButtonUp() +{ + RedrawComponent(PanelDrawComponent::ControlButtons); + MainPanelButtonDown = false; +} + +int CapStatPointsToAdd(int remainingStatPoints, const Player &player, CharacterAttribute attribute) +{ + const int pointsToReachCap = player.GetMaximumAttributeValue(attribute) - player.GetBaseAttributeValue(attribute); + + return std::min(remainingStatPoints, pointsToReachCap); +} + +int DrawDurIcon4Item(const Surface &out, Item &pItem, int x, int c) +{ + const int durabilityThresholdGold = 5; + const int durabilityThresholdRed = 2; + + if (pItem.isEmpty()) + return x; + if (pItem._iDurability > durabilityThresholdGold) + return x; + if (c == 0) { + switch (pItem._itype) { + case ItemType::Sword: + c = 1; + break; + case ItemType::Axe: + c = 5; + break; + case ItemType::Bow: + c = 6; + break; + case ItemType::Mace: + c = 4; + break; + case ItemType::Staff: + c = 7; + break; + case ItemType::Shield: + default: + c = 0; + break; + } + } + + // Calculate how much of the icon should be gold and red + const int height = (*pDurIcons)[c].height(); // Height of durability icon CEL + int partition = 0; + if (pItem._iDurability > durabilityThresholdRed) { + const int current = pItem._iDurability - durabilityThresholdRed; + partition = (height * current) / (durabilityThresholdGold - durabilityThresholdRed); + } + + // Draw icon + const int y = -17 + GetMainPanel().position.y; + if (partition > 0) { + const Surface stenciledBuffer = out.subregionY(y - partition, partition); + ClxDraw(stenciledBuffer, { x, partition }, (*pDurIcons)[c + 8]); // Gold icon + } + if (partition != height) { + const Surface stenciledBuffer = out.subregionY(y - height, height - partition); + ClxDraw(stenciledBuffer, { x, height }, (*pDurIcons)[c]); // Red icon + } + + return x - (*pDurIcons)[c].height() - 8; // Add in spacing for the next durability icon +} + +bool IsLevelUpButtonVisible() +{ + if (SpellSelectFlag || CharFlag || MyPlayer->_pStatPts == 0) { + return false; + } + if (ControlMode == ControlTypes::VirtualGamepad) { + return false; + } + if (IsPlayerInStore() || IsStashOpen) { + return false; + } + if (QuestLogIsOpen && GetLeftPanel().contains(GetMainPanel().position + Displacement { 0, -74 })) { + return false; + } + + return true; +} + +} // namespace + +void CalculatePanelAreas() +{ + constexpr Size MainPanelSize { 640, 128 }; + + MainPanel = { + { (gnScreenWidth - MainPanelSize.width) / 2, gnScreenHeight - MainPanelSize.height }, + MainPanelSize + }; + LeftPanel = { + { 0, 0 }, + SidePanelSize + }; + RightPanel = { + { 0, 0 }, + SidePanelSize + }; + + if (ControlMode == ControlTypes::VirtualGamepad) { + LeftPanel.position.x = gnScreenWidth / 2 - LeftPanel.size.width; + } else { + if (gnScreenWidth - LeftPanel.size.width - RightPanel.size.width > MainPanel.size.width) { + LeftPanel.position.x = (gnScreenWidth - LeftPanel.size.width - RightPanel.size.width - MainPanel.size.width) / 2; + } + } + LeftPanel.position.y = (gnScreenHeight - LeftPanel.size.height - MainPanel.size.height) / 2; + + if (ControlMode == ControlTypes::VirtualGamepad) { + RightPanel.position.x = gnScreenWidth / 2; + } else { + RightPanel.position.x = gnScreenWidth - RightPanel.size.width - LeftPanel.position.x; + } + RightPanel.position.y = LeftPanel.position.y; + + gnViewportHeight = gnScreenHeight; + if (gnScreenWidth <= MainPanel.size.width) { + // Part of the screen is fully obscured by the UI + gnViewportHeight -= MainPanel.size.height; + } +} + +void FocusOnCharInfo() +{ + const Player &myPlayer = *MyPlayer; + + if (invflag || myPlayer._pStatPts <= 0) + return; + + // Find the first incrementable stat. + int stat = -1; + for (auto attribute : enum_values()) { + if (myPlayer.GetBaseAttributeValue(attribute) >= myPlayer.GetMaximumAttributeValue(attribute)) + continue; + stat = static_cast(attribute); + } + if (stat == -1) + return; + + SetCursorPos(CharPanelButtonRect[stat].Center()); +} + +void OpenCharPanel() +{ + QuestLogIsOpen = false; + CloseGoldWithdraw(); + CloseStash(); + CharFlag = true; +} + +void CloseCharPanel() +{ + CharFlag = false; + if (IsInspectingPlayer()) { + InspectPlayer = MyPlayer; + RedrawEverything(); + + if (InspectingFromPartyPanel) + InspectingFromPartyPanel = false; + else + InitDiabloMsg(_("Stopped inspecting players.")); + } +} + +void ToggleCharPanel() +{ + if (CharFlag) + CloseCharPanel(); + else + OpenCharPanel(); +} + +Point GetPanelPosition(UiPanels panel, Point offset) +{ + const Displacement displacement { offset.x, offset.y }; + + switch (panel) { + case UiPanels::Main: + return GetMainPanel().position + displacement; + case UiPanels::Quest: + case UiPanels::Character: + case UiPanels::Stash: + return GetLeftPanel().position + displacement; + case UiPanels::Spell: + case UiPanels::Inventory: + return GetRightPanel().position + displacement; + default: + return GetMainPanel().position + displacement; + } +} + +void DrawPanelBox(const Surface &out, SDL_Rect srcRect, Point targetPosition) +{ + out.BlitFrom(*BottomBuffer, srcRect, targetPosition); +} + +tl::expected InitMainPanel() +{ + if (!HeadlessMode) { + BottomBuffer.emplace(GetMainPanel().size.width, (GetMainPanel().size.height + PanelPaddingHeight) * (IsChatAvailable() ? 2 : 1)); + pManaBuff.emplace(88, 88); + pLifeBuff.emplace(88, 88); + + RETURN_IF_ERROR(LoadPartyPanel()); + RETURN_IF_ERROR(LoadCharPanel()); + RETURN_IF_ERROR(LoadLargeSpellIcons()); + { + ASSIGN_OR_RETURN(const OwnedClxSpriteList sprite, LoadCelWithStatus("ctrlpan\\panel8", GetMainPanel().size.width)); + ClxDraw(*BottomBuffer, { 0, (GetMainPanel().size.height + PanelPaddingHeight) - 1 }, sprite[0]); + } + { + const Point bulbsPosition { 0, 87 }; + ASSIGN_OR_RETURN(const OwnedClxSpriteList statusPanel, LoadCelWithStatus("ctrlpan\\p8bulbs", 88)); + ClxDraw(*pLifeBuff, bulbsPosition, statusPanel[0]); + ClxDraw(*pManaBuff, bulbsPosition, statusPanel[1]); + } + } + ChatFlag = false; + ChatInputState = std::nullopt; + if (IsChatAvailable()) { + if (!HeadlessMode) { + { + ASSIGN_OR_RETURN(const OwnedClxSpriteList sprite, LoadCelWithStatus("ctrlpan\\talkpanl", GetMainPanel().size.width)); + ClxDraw(*BottomBuffer, { 0, (GetMainPanel().size.height + PanelPaddingHeight) * 2 - 1 }, sprite[0]); + } + multiButtons = LoadCel("ctrlpan\\p8but2", 33); + talkButtons = LoadCel("ctrlpan\\talkbutt", 61); + } + sgbPlrTalkTbl = 0; + TalkMessage[0] = '\0'; + for (bool &whisper : WhisperList) + whisper = true; + for (bool &talkButtonDown : TalkButtonsDown) + talkButtonDown = false; + } + MainPanelFlag = false; + LevelButtonDown = false; + if (!HeadlessMode) { + RETURN_IF_ERROR(LoadMainPanel()); + ASSIGN_OR_RETURN(pMainPanelButtons, LoadCelWithStatus("ctrlpan\\panel8bu", 71)); + + static const uint16_t CharButtonsFrameWidths[9] { 95, 41, 41, 41, 41, 41, 41, 41, 41 }; + ASSIGN_OR_RETURN(pChrButtons, LoadCelWithStatus("data\\charbut", CharButtonsFrameWidths)); + } + ResetMainPanelButtons(); + if (!HeadlessMode) + pDurIcons = LoadCel("items\\duricons", 32); + for (bool &buttonEnabled : CharPanelButton) + buttonEnabled = false; + CharPanelButtonActive = false; + InfoString = StringOrView {}; + FloatingInfoString = StringOrView {}; + RedrawComponent(PanelDrawComponent::Health); + RedrawComponent(PanelDrawComponent::Mana); + CloseCharPanel(); + SpellSelectFlag = false; + SpellbookTab = 0; + SpellbookFlag = false; + + if (!HeadlessMode) { + InitSpellBook(); + ASSIGN_OR_RETURN(pQLogCel, LoadCelWithStatus("data\\quest", static_cast(SidePanelSize.width))); + ASSIGN_OR_RETURN(GoldBoxBuffer, LoadCelWithStatus("ctrlpan\\golddrop", 261)); + } + CloseGoldDrop(); + CalculatePanelAreas(); + + if (!HeadlessMode) + InitModifierHints(); + + return {}; +} + +void DrawMainPanel(const Surface &out) +{ + DrawPanelBox(out, MakeSdlRect(0, sgbPlrTalkTbl + PanelPaddingHeight, GetMainPanel().size.width, GetMainPanel().size.height), GetMainPanel().position); + DrawInfoBox(out); +} + +void DrawMainPanelButtons(const Surface &out) +{ + const Point mainPanelPosition = GetMainPanel().position; + + for (int i = 0; i < TotalSpMainPanelButtons; i++) { + if (!MainPanelButtons[i]) { + DrawPanelBox(out, MakeSdlRect(MainPanelButtonRect[i].position.x, MainPanelButtonRect[i].position.y + PanelPaddingHeight, MainPanelButtonRect[i].size.width, MainPanelButtonRect[i].size.height + 1), mainPanelPosition + Displacement { MainPanelButtonRect[i].position.x, MainPanelButtonRect[i].position.y }); + } else { + const Point position = mainPanelPosition + Displacement { MainPanelButtonRect[i].position.x, MainPanelButtonRect[i].position.y }; + RenderClxSprite(out, (*pMainPanelButtons)[i], position); + RenderClxSprite(out, (*PanelButtonDown)[i], position + Displacement { 4, 0 }); + } + } + + if (IsChatAvailable()) { + RenderClxSprite(out, (*multiButtons)[MainPanelButtons[PanelButtonSendmsg] ? 1 : 0], mainPanelPosition + Displacement { MainPanelButtonRect[PanelButtonSendmsg].position.x, MainPanelButtonRect[PanelButtonSendmsg].position.y }); + + const Point friendlyButtonPosition = mainPanelPosition + Displacement { MainPanelButtonRect[PanelButtonFriendly].position.x, MainPanelButtonRect[PanelButtonFriendly].position.y }; + + if (MyPlayer->friendlyMode) + RenderClxSprite(out, (*multiButtons)[MainPanelButtons[PanelButtonFriendly] ? 3 : 2], friendlyButtonPosition); + else + RenderClxSprite(out, (*multiButtons)[MainPanelButtons[PanelButtonFriendly] ? 5 : 4], friendlyButtonPosition); + } +} + +void ResetMainPanelButtons() +{ + for (bool &panelButton : MainPanelButtons) + panelButton = false; + SetMainPanelButtonUp(); +} + +void CheckMainPanelButton() +{ + const int totalButtons = IsChatAvailable() ? TotalMpMainPanelButtons : TotalSpMainPanelButtons; + + for (int i = 0; i < totalButtons; i++) { + Rectangle button = MainPanelButtonRect[i]; + + SetPanelObjectPosition(UiPanels::Main, button); + + if (button.contains(MousePosition)) { + SetMainPanelButtonDown(i); + } + } + + Rectangle spellSelectButton = SpellButtonRect; + + SetPanelObjectPosition(UiPanels::Main, spellSelectButton); + + if (!SpellSelectFlag && spellSelectButton.contains(MousePosition)) { + if ((SDL_GetModState() & SDL_KMOD_SHIFT) != 0) { + Player &myPlayer = *MyPlayer; + myPlayer._pRSpell = SpellID::Invalid; + myPlayer._pRSplType = SpellType::Invalid; + RedrawEverything(); + return; + } + DoSpeedBook(); + gamemenu_off(); + } +} + +void CheckMainPanelButtonDead() +{ + Rectangle menuButton = MainPanelButtonRect[PanelButtonMainmenu]; + + SetPanelObjectPosition(UiPanels::Main, menuButton); + + if (menuButton.contains(MousePosition)) { + SetMainPanelButtonDown(PanelButtonMainmenu); + return; + } + + Rectangle chatButton = MainPanelButtonRect[PanelButtonSendmsg]; + + SetPanelObjectPosition(UiPanels::Main, chatButton); + + if (chatButton.contains(MousePosition)) { + SetMainPanelButtonDown(PanelButtonSendmsg); + } +} + +void DoAutoMap() +{ + if (!AutomapActive) + StartAutomap(); + else + AutomapActive = false; +} + +void CycleAutomapType() +{ + if (!AutomapActive) { + StartAutomap(); + return; + } + const AutomapType newType { static_cast>( + (static_cast(GetAutomapType()) + 1) % enum_size::value) }; + SetAutomapType(newType); + if (newType == AutomapType::FIRST) { + AutomapActive = false; + } +} + +void CheckMainPanelButtonUp() +{ + bool gamemenuOff = true; + + SetMainPanelButtonUp(); + + for (int i = PanelButtonFirst; i <= PanelButtonLast; i++) { + if (!MainPanelButtons[i]) + continue; + + MainPanelButtons[i] = false; + + Rectangle button = MainPanelButtonRect[i]; + + SetPanelObjectPosition(UiPanels::Main, button); + + if (!button.contains(MousePosition)) + continue; + + switch (i) { + case PanelButtonCharinfo: + ToggleCharPanel(); + break; + case PanelButtonQlog: + CloseCharPanel(); + CloseGoldWithdraw(); + CloseStash(); + if (!QuestLogIsOpen) + StartQuestlog(); + else + QuestLogIsOpen = false; + break; + case PanelButtonAutomap: + DoAutoMap(); + break; + case PanelButtonMainmenu: + if (MyPlayerIsDead) { + if (!gbIsMultiplayer) { + if (gbValidSaveFile) + gamemenu_load_game(false); + else + gamemenu_exit_game(false); + } else { + NetSendCmd(true, CMD_RETOWN); + } + break; + } else if (MyPlayer->hasNoLife()) { + break; + } + qtextflag = false; + gamemenu_handle_previous(); + gamemenuOff = false; + break; + case PanelButtonInventory: + SpellbookFlag = false; + CloseGoldWithdraw(); + CloseStash(); + invflag = !invflag; + CloseGoldDrop(); + break; + case PanelButtonSpellbook: + CloseInventory(); + CloseGoldDrop(); + SpellbookFlag = !SpellbookFlag; + break; + case PanelButtonSendmsg: + if (ChatFlag) + ResetChat(); + else + TypeChatMessage(); + break; + case PanelButtonFriendly: + // Toggle friendly Mode + NetSendCmd(true, CMD_FRIENDLYMODE); + break; + } + } + + if (gamemenuOff) + gamemenu_off(); +} + +void FreeControlPan() +{ + BottomBuffer = std::nullopt; + pManaBuff = std::nullopt; + pLifeBuff = std::nullopt; + FreeLargeSpellIcons(); + FreeSpellBook(); + pMainPanelButtons = std::nullopt; + multiButtons = std::nullopt; + talkButtons = std::nullopt; + pChrButtons = std::nullopt; + pDurIcons = std::nullopt; + pQLogCel = std::nullopt; + GoldBoxBuffer = std::nullopt; + FreeMainPanel(); + FreePartyPanel(); + FreeCharPanel(); + FreeModifierHints(); +} + +void CheckLevelButton() +{ + if (!IsLevelUpButtonVisible()) { + return; + } + + Rectangle button = LevelButtonRect; + + SetPanelObjectPosition(UiPanels::Main, button); + + if (!LevelButtonDown && button.contains(MousePosition)) + LevelButtonDown = true; +} + +void CheckLevelButtonUp() +{ + Rectangle button = LevelButtonRect; + + SetPanelObjectPosition(UiPanels::Main, button); + + if (button.contains(MousePosition)) { + OpenCharPanel(); + } + LevelButtonDown = false; +} + +void DrawLevelButton(const Surface &out) +{ + if (IsLevelUpButtonVisible()) { + const int nCel = LevelButtonDown ? 2 : 1; + DrawString(out, _("Level Up"), { GetMainPanel().position + Displacement { 0, LevelButtonRect.position.y - 23 }, { 120, 0 } }, + { .flags = UiFlags::ColorWhite | UiFlags::AlignCenter | UiFlags::KerningFitSpacing }); + RenderClxSprite(out, (*pChrButtons)[nCel], GetMainPanel().position + Displacement { LevelButtonRect.position.x, LevelButtonRect.position.y }); + } +} + +void CheckChrBtns() +{ + const Player &myPlayer = *MyPlayer; + + if (CharPanelButtonActive || myPlayer._pStatPts == 0) + return; + + for (auto attribute : enum_values()) { + if (myPlayer.GetBaseAttributeValue(attribute) >= myPlayer.GetMaximumAttributeValue(attribute)) + continue; + auto buttonId = static_cast(attribute); + Rectangle button = CharPanelButtonRect[buttonId]; + SetPanelObjectPosition(UiPanels::Character, button); + if (button.contains(MousePosition)) { + CharPanelButton[buttonId] = true; + CharPanelButtonActive = true; + } + } +} + +void ReleaseChrBtns(bool addAllStatPoints) +{ + CharPanelButtonActive = false; + for (auto attribute : enum_values()) { + auto buttonId = static_cast(attribute); + if (!CharPanelButton[buttonId]) + continue; + + CharPanelButton[buttonId] = false; + Rectangle button = CharPanelButtonRect[buttonId]; + SetPanelObjectPosition(UiPanels::Character, button); + if (button.contains(MousePosition)) { + Player &myPlayer = *MyPlayer; + int statPointsToAdd = 1; + if (addAllStatPoints) + statPointsToAdd = CapStatPointsToAdd(myPlayer._pStatPts, myPlayer, attribute); + switch (attribute) { + case CharacterAttribute::Strength: + NetSendCmdParam1(true, CMD_ADDSTR, statPointsToAdd); + myPlayer._pStatPts -= statPointsToAdd; + break; + case CharacterAttribute::Magic: + NetSendCmdParam1(true, CMD_ADDMAG, statPointsToAdd); + myPlayer._pStatPts -= statPointsToAdd; + break; + case CharacterAttribute::Dexterity: + NetSendCmdParam1(true, CMD_ADDDEX, statPointsToAdd); + myPlayer._pStatPts -= statPointsToAdd; + break; + case CharacterAttribute::Vitality: + NetSendCmdParam1(true, CMD_ADDVIT, statPointsToAdd); + myPlayer._pStatPts -= statPointsToAdd; + break; + } + } + } +} + +void DrawDurIcon(const Surface &out) +{ + const bool hasRoomBetweenPanels = RightPanel.position.x - (LeftPanel.position.x + LeftPanel.size.width) >= 16 + (32 + 8 + 32 + 8 + 32 + 8 + 32) + 16; + const bool hasRoomUnderPanels = MainPanel.position.y - (RightPanel.position.y + RightPanel.size.height) >= 16 + 32 + 16; + + if (!hasRoomBetweenPanels && !hasRoomUnderPanels) { + if (IsLeftPanelOpen() && IsRightPanelOpen()) + return; + } + + int x = MainPanel.position.x + MainPanel.size.width - 32 - 16; + if (!hasRoomUnderPanels) { + if (IsRightPanelOpen() && MainPanel.position.x + MainPanel.size.width > RightPanel.position.x) + x -= MainPanel.position.x + MainPanel.size.width - RightPanel.position.x; + } + + Player &myPlayer = *MyPlayer; + x = DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_HEAD], x, 3); + x = DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_CHEST], x, 2); + x = DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_HAND_LEFT], x, 0); + DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_HAND_RIGHT], x, 0); +} + +void RedBack(const Surface &out) +{ + uint8_t *dst = out.begin(); + uint8_t *tbl = GetPauseTRN(); + for (int h = gnViewportHeight; h != 0; h--, dst += out.pitch() - gnScreenWidth) { + for (int w = gnScreenWidth; w != 0; w--) { + if (leveltype != DTYPE_HELL || *dst >= 32) + *dst = tbl[*dst]; + dst++; + } + } +} + +void DrawDeathText(const Surface &out) +{ + const TextRenderOptions largeTextOptions { + .flags = UiFlags::FontSize42 | UiFlags::ColorGold | UiFlags::AlignCenter | UiFlags::VerticalCenter, + .spacing = 2 + }; + const TextRenderOptions smallTextOptions { + .flags = UiFlags::FontSize30 | UiFlags::ColorGold | UiFlags::AlignCenter | UiFlags::VerticalCenter, + .spacing = 2 + }; + std::string text; + const int verticalPadding = 42; + Point linePosition { 0, gnScreenHeight / 2 - (verticalPadding * 2) }; + + text = _("You have died"); + DrawString(out, text, linePosition, largeTextOptions); + linePosition.y += verticalPadding; + + std::string buttonText; + + switch (ControlMode) { + case ControlTypes::KeyboardAndMouse: + buttonText = _("ESC"); + break; + case ControlTypes::Gamepad: + buttonText = ToString(GamepadType, ControllerButton_BUTTON_START); + break; + case ControlTypes::VirtualGamepad: + buttonText = _("Menu Button"); + break; + default: + break; + } + + if (!gbIsMultiplayer) { + if (gbValidSaveFile) + text = fmt::format(fmt::runtime(_("Press {} to load last save.")), buttonText); + else + text = fmt::format(fmt::runtime(_("Press {} to return to Main Menu.")), buttonText); + + } else { + text = fmt::format(fmt::runtime(_("Press {} to restart in town.")), buttonText); + } + DrawString(out, text, linePosition, smallTextOptions); +} + +void SetPanelObjectPosition(UiPanels panel, Rectangle &button) +{ + button.position = GetPanelPosition(panel, button.position); +} + +} // namespace devilution diff --git a/Source/control/control_panel.hpp b/Source/control/control_panel.hpp new file mode 100644 index 000000000..120b3ecd4 --- /dev/null +++ b/Source/control/control_panel.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "engine/rectangle.hpp" +#include "panels/ui_panels.hpp" + +namespace devilution { + +extern int TotalSpMainPanelButtons; +extern int TotalMpMainPanelButtons; +extern int PanelPaddingHeight; +extern const char *const PanBtnStr[8]; +extern const char *const PanBtnHotKey[8]; +extern Rectangle SpellButtonRect; +extern Rectangle BeltRect; + +void SetPanelObjectPosition(UiPanels panel, Rectangle &button); + +} // namespace devilution diff --git a/Source/controls/controller_motion.cpp b/Source/controls/controller_motion.cpp index 5b8b4cb90..692c25057 100644 --- a/Source/controls/controller_motion.cpp +++ b/Source/controls/controller_motion.cpp @@ -9,7 +9,7 @@ #include #endif -#include "control.h" +#include "control/control.hpp" #include "controls/control_mode.hpp" #include "controls/controller.h" #ifndef USE_SDL1 diff --git a/Source/controls/keymapper.cpp b/Source/controls/keymapper.cpp index 8a7b344cf..b13b74168 100644 --- a/Source/controls/keymapper.cpp +++ b/Source/controls/keymapper.cpp @@ -12,7 +12,7 @@ #endif #endif -#include "control.h" +#include "control/control.hpp" #include "options.h" #include "utils/is_of.hpp" diff --git a/Source/controls/modifier_hints.cpp b/Source/controls/modifier_hints.cpp index 881b3adbc..729655916 100644 --- a/Source/controls/modifier_hints.cpp +++ b/Source/controls/modifier_hints.cpp @@ -4,7 +4,7 @@ #include #include "DiabloUI/ui_flags.hpp" -#include "control.h" +#include "control/control.hpp" #include "controls/controller_motion.h" #include "controls/game_controls.h" #include "controls/plrctrls.h" diff --git a/Source/controls/plrctrls.cpp b/Source/controls/plrctrls.cpp index 37d4c1c70..4ab25c8ea 100644 --- a/Source/controls/plrctrls.cpp +++ b/Source/controls/plrctrls.cpp @@ -18,7 +18,7 @@ #endif #include "automap.h" -#include "control.h" +#include "control/control.hpp" #include "controls/controller_motion.h" #ifndef USE_SDL1 #include "controls/devices/game_controller.h" diff --git a/Source/controls/touch/event_handlers.cpp b/Source/controls/touch/event_handlers.cpp index 30e205e13..f6c3e435d 100644 --- a/Source/controls/touch/event_handlers.cpp +++ b/Source/controls/touch/event_handlers.cpp @@ -7,7 +7,7 @@ #include #endif -#include "control.h" +#include "control/control.hpp" #include "controls/plrctrls.h" #include "cursor.h" #include "diablo.h" diff --git a/Source/controls/touch/gamepad.cpp b/Source/controls/touch/gamepad.cpp index 257e6b4da..57facd7b2 100644 --- a/Source/controls/touch/gamepad.cpp +++ b/Source/controls/touch/gamepad.cpp @@ -6,7 +6,7 @@ #include #endif -#include "control.h" +#include "control/control.hpp" #include "controls/touch/event_handlers.h" #include "controls/touch/gamepad.h" #include "quests.h" diff --git a/Source/controls/touch/renderers.cpp b/Source/controls/touch/renderers.cpp index 56384c017..29b01cdad 100644 --- a/Source/controls/touch/renderers.cpp +++ b/Source/controls/touch/renderers.cpp @@ -6,7 +6,7 @@ #include #endif -#include "control.h" +#include "control/control.hpp" #include "cursor.h" #include "diablo.h" #include "doom.h" diff --git a/Source/cursor.cpp b/Source/cursor.cpp index fa4cef28a..0fa280b6c 100644 --- a/Source/cursor.cpp +++ b/Source/cursor.cpp @@ -21,7 +21,7 @@ #include #include "DiabloUI/diabloui.h" -#include "control.h" +#include "control/control.hpp" #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "doom.h" diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 4f51b7a76..cd095aa90 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -27,7 +27,7 @@ #include "appfat.h" #include "automap.h" #include "capture.h" -#include "control.h" +#include "control/control.hpp" #include "cursor.h" #include "dead.h" #ifdef _DEBUG diff --git a/Source/doom.cpp b/Source/doom.cpp index b665c649f..2fa8f93dd 100644 --- a/Source/doom.cpp +++ b/Source/doom.cpp @@ -7,7 +7,7 @@ #include -#include "control.h" +#include "control/control.hpp" #include "engine/clx_sprite.hpp" #include "engine/load_cel.hpp" #include "engine/render/clx_render.hpp" diff --git a/Source/gmenu.cpp b/Source/gmenu.cpp index 7c7e76afd..6e1c5230d 100644 --- a/Source/gmenu.cpp +++ b/Source/gmenu.cpp @@ -20,7 +20,7 @@ #include "DiabloUI/ui_flags.hpp" #include "appfat.h" -#include "control.h" +#include "control/control.hpp" #include "controls/axis_direction.h" #include "controls/controller_motion.h" #include "engine/clx_sprite.hpp" diff --git a/Source/interfac.cpp b/Source/interfac.cpp index 2a8efb408..0ca8a58b2 100644 --- a/Source/interfac.cpp +++ b/Source/interfac.cpp @@ -22,7 +22,7 @@ #include -#include "control.h" +#include "control/control.hpp" #include "controls/input.h" #include "engine/clx_sprite.hpp" #include "engine/dx.h" diff --git a/Source/items.cpp b/Source/items.cpp index 5a5876d07..82c0ebb23 100644 --- a/Source/items.cpp +++ b/Source/items.cpp @@ -28,7 +28,7 @@ #include #include "DiabloUI/ui_flags.hpp" -#include "control.h" +#include "control/control.hpp" #include "controls/control_mode.hpp" #include "controls/controller_buttons.h" #include "cursor.h" diff --git a/Source/levels/trigs.cpp b/Source/levels/trigs.cpp index 141564231..c3b88ae25 100644 --- a/Source/levels/trigs.cpp +++ b/Source/levels/trigs.cpp @@ -10,7 +10,7 @@ #include -#include "control.h" +#include "control/control.hpp" #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "cursor.h" diff --git a/Source/loadsave.cpp b/Source/loadsave.cpp index 75959092e..86b8d099f 100644 --- a/Source/loadsave.cpp +++ b/Source/loadsave.cpp @@ -17,7 +17,7 @@ #include "automap.h" #include "codec.h" -#include "control.h" +#include "control/control.hpp" #include "cursor.h" #include "dead.h" #include "doom.h" diff --git a/Source/minitext.cpp b/Source/minitext.cpp index 1b25b0db2..fdca168b5 100644 --- a/Source/minitext.cpp +++ b/Source/minitext.cpp @@ -10,7 +10,7 @@ #include #include "DiabloUI/ui_flags.hpp" -#include "control.h" +#include "control/control.hpp" #include "engine/clx_sprite.hpp" #include "engine/dx.h" #include "engine/load_cel.hpp" diff --git a/Source/missiles.cpp b/Source/missiles.cpp index f4733157f..adf087d61 100644 --- a/Source/missiles.cpp +++ b/Source/missiles.cpp @@ -19,7 +19,7 @@ #include #include "appfat.h" -#include "control.h" +#include "control/control.hpp" #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "crawl.hpp" diff --git a/Source/monster.cpp b/Source/monster.cpp index 5d586cfe6..0f7791578 100644 --- a/Source/monster.cpp +++ b/Source/monster.cpp @@ -34,7 +34,7 @@ #include #include "automap.h" -#include "control.h" +#include "control/control.hpp" #include "crawl.hpp" #include "cursor.h" #include "dead.h" diff --git a/Source/msg.cpp b/Source/msg.cpp index 0cb56dde6..bd98a5066 100644 --- a/Source/msg.cpp +++ b/Source/msg.cpp @@ -28,7 +28,7 @@ #include "DiabloUI/diabloui.h" #include "automap.h" #include "config.h" -#include "control.h" +#include "control/control.hpp" #include "dead.h" #include "engine/backbuffer_state.hpp" #include "engine/random.hpp" diff --git a/Source/panels/charpanel.cpp b/Source/panels/charpanel.cpp index 30ee9c222..75ee4f76d 100644 --- a/Source/panels/charpanel.cpp +++ b/Source/panels/charpanel.cpp @@ -9,7 +9,7 @@ #include #include -#include "control.h" +#include "control/control.hpp" #include "engine/load_clx.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/text_render.hpp" diff --git a/Source/panels/console.cpp b/Source/panels/console.cpp index 2b6ceabe5..b14982bb8 100644 --- a/Source/panels/console.cpp +++ b/Source/panels/console.cpp @@ -22,7 +22,7 @@ #endif #include "DiabloUI/text_input.hpp" -#include "control.h" +#include "control/control.hpp" #include "engine/assets.hpp" #include "engine/displacement.hpp" #include "engine/dx.h" diff --git a/Source/panels/mainpanel.cpp b/Source/panels/mainpanel.cpp index 935fde6d1..99f8549d0 100644 --- a/Source/panels/mainpanel.cpp +++ b/Source/panels/mainpanel.cpp @@ -6,7 +6,7 @@ #include -#include "control.h" +#include "control/control.hpp" #include "engine/clx_sprite.hpp" #include "engine/load_clx.hpp" #include "engine/render/clx_render.hpp" diff --git a/Source/panels/partypanel.cpp b/Source/panels/partypanel.cpp index 2a9f49596..5eab18802 100644 --- a/Source/panels/partypanel.cpp +++ b/Source/panels/partypanel.cpp @@ -4,7 +4,7 @@ #include #include "automap.h" -#include "control.h" +#include "control/control.hpp" #include "engine/backbuffer_state.hpp" #include "engine/clx_sprite.hpp" #include "engine/load_cel.hpp" diff --git a/Source/panels/spell_book.cpp b/Source/panels/spell_book.cpp index a9b7d18b1..e5ef1aea5 100644 --- a/Source/panels/spell_book.cpp +++ b/Source/panels/spell_book.cpp @@ -7,7 +7,7 @@ #include #include -#include "control.h" +#include "control/control.hpp" #include "engine/backbuffer_state.hpp" #include "engine/clx_sprite.hpp" #include "engine/load_cel.hpp" diff --git a/Source/panels/spell_list.cpp b/Source/panels/spell_list.cpp index 153ae048d..cb50b0341 100644 --- a/Source/panels/spell_list.cpp +++ b/Source/panels/spell_list.cpp @@ -4,7 +4,7 @@ #include -#include "control.h" +#include "control/control.hpp" #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "engine/backbuffer_state.hpp" diff --git a/Source/player.cpp b/Source/player.cpp index 42cc9bb8a..894b8335d 100644 --- a/Source/player.cpp +++ b/Source/player.cpp @@ -16,7 +16,7 @@ #include -#include "control.h" +#include "control/control.hpp" #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "cursor.h" diff --git a/Source/plrmsg.cpp b/Source/plrmsg.cpp index 9d45ac84e..f879d675a 100644 --- a/Source/plrmsg.cpp +++ b/Source/plrmsg.cpp @@ -17,7 +17,7 @@ #include -#include "control.h" +#include "control/control.hpp" #include "engine/render/primitive_render.hpp" #include "engine/render/text_render.hpp" #include "inv.h" diff --git a/Source/qol/chatlog.cpp b/Source/qol/chatlog.cpp index a2d1dbb03..c73a25df5 100644 --- a/Source/qol/chatlog.cpp +++ b/Source/qol/chatlog.cpp @@ -14,7 +14,7 @@ #include "DiabloUI/ui_flags.hpp" #include "automap.h" #include "chatlog.h" -#include "control.h" +#include "control/control.hpp" #include "diablo_msg.hpp" #include "doom.h" #include "engine/render/text_render.hpp" diff --git a/Source/qol/itemlabels.cpp b/Source/qol/itemlabels.cpp index 6f73c8167..838a76baf 100644 --- a/Source/qol/itemlabels.cpp +++ b/Source/qol/itemlabels.cpp @@ -9,7 +9,7 @@ #include -#include "control.h" +#include "control/control.hpp" #include "cursor.h" #include "engine/point.hpp" #include "engine/render/clx_render.hpp" diff --git a/Source/qol/monhealthbar.cpp b/Source/qol/monhealthbar.cpp index 456f3a8ca..1be073a89 100644 --- a/Source/qol/monhealthbar.cpp +++ b/Source/qol/monhealthbar.cpp @@ -9,7 +9,7 @@ #include -#include "control.h" +#include "control/control.hpp" #include "cursor.h" #include "engine/clx_sprite.hpp" #include "engine/load_clx.hpp" diff --git a/Source/qol/stash.cpp b/Source/qol/stash.cpp index b7c395508..2f7d427ff 100644 --- a/Source/qol/stash.cpp +++ b/Source/qol/stash.cpp @@ -13,7 +13,7 @@ #include #include "DiabloUI/text_input.hpp" -#include "control.h" +#include "control/control.hpp" #include "controls/plrctrls.h" #include "cursor.h" #include "engine/clx_sprite.hpp" diff --git a/Source/qol/xpbar.cpp b/Source/qol/xpbar.cpp index 271dda89d..da0bba575 100644 --- a/Source/qol/xpbar.cpp +++ b/Source/qol/xpbar.cpp @@ -10,7 +10,7 @@ #include -#include "control.h" +#include "control/control.hpp" #include "engine/clx_sprite.hpp" #include "engine/load_clx.hpp" #include "engine/point.hpp" diff --git a/Source/quests.cpp b/Source/quests.cpp index c5442d4cc..4d5d56920 100644 --- a/Source/quests.cpp +++ b/Source/quests.cpp @@ -10,7 +10,7 @@ #include #include "DiabloUI/ui_flags.hpp" -#include "control.h" +#include "control/control.hpp" #include "cursor.h" #include "data/file.hpp" #include "data/record_reader.hpp" diff --git a/Source/spells.cpp b/Source/spells.cpp index 9304f5b85..476e2013b 100644 --- a/Source/spells.cpp +++ b/Source/spells.cpp @@ -5,7 +5,7 @@ */ #include "spells.h" -#include "control.h" +#include "control/control.hpp" #include "cursor.h" #ifdef _DEBUG #include "debug.h" diff --git a/Source/stores.h b/Source/stores.h index a6471f4b0..fc38e00db 100644 --- a/Source/stores.h +++ b/Source/stores.h @@ -9,7 +9,7 @@ #include #include "DiabloUI/ui_flags.hpp" -#include "control.h" +#include "control/control.hpp" #include "engine/clx_sprite.hpp" #include "engine/surface.hpp" #include "game_mode.hpp" diff --git a/Source/utils/display.cpp b/Source/utils/display.cpp index 1a39ca532..80d0aad11 100644 --- a/Source/utils/display.cpp +++ b/Source/utils/display.cpp @@ -35,7 +35,7 @@ #include "DiabloUI/diabloui.h" #include "config.h" -#include "control.h" +#include "control/control.hpp" #include "controls/controller.h" #ifndef USE_SDL1 #include "controls/devices/game_controller.h" diff --git a/test/scrollrt_test.cpp b/test/scrollrt_test.cpp index f85ac40e9..941b9a4b1 100644 --- a/test/scrollrt_test.cpp +++ b/test/scrollrt_test.cpp @@ -1,6 +1,6 @@ #include -#include "control.h" +#include "control/control.hpp" #include "diablo.h" #include "engine/render/scrollrt.h" #include "options.h"