diff --git a/Source/control.cpp b/Source/control.cpp index 5db62b582..2f2ea3fb2 100644 --- a/Source/control.cpp +++ b/Source/control.cpp @@ -26,6 +26,7 @@ #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" @@ -62,6 +63,7 @@ #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" @@ -88,6 +90,7 @@ bool ChatFlag; bool SpellbookFlag; bool CharFlag; StringOrView InfoString; +StringOrView FloatingInfoString; bool MainPanelFlag; bool MainPanelButtonDown; bool SpellSelectFlag; @@ -340,6 +343,209 @@ void PrintInfo(const Surface &out) }); } +Rectangle GetFloatingInfoRect(const int lineHeight, const int textSpacing) +{ + // Calculate the width and height of the floating info box + std::string txt = std::string(FloatingInfoString); + + auto lines = SplitByChar(txt, '\n'); + const GameFontTables font = GameFont12; + int maxW = 0; + + for (const auto &line : lines) { + int w = GetLineWidth(line, font, textSpacing, nullptr); + maxW = std::max(maxW, w); + } + + const auto lineCount = 1 + static_cast(c_count(FloatingInfoString.str(), '\n')); + int totalH = lineCount * lineHeight; + + Player &player = *InspectPlayer; + + // 1) Equipment (Rect position) + if (pcursinvitem >= INVITEM_HEAD && pcursinvitem < INVITEM_INV_FIRST) { + 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]; + 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 + + 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) { + int itemIdx = pcursinvitem - INVITEM_INV_FIRST; + + for (int j = 0; j < InventoryGridCells; ++j) { + if (player.InvGrid[j] > 0 && player.InvGrid[j] - 1 == itemIdx) { + 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 + + 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) { + int itemIdx = pcursinvitem - INVITEM_BELT_FIRST; + for (int i = 0; i < MaxBeltItems; ++i) { + if (player.SpdList[i].isEmpty()) + continue; + if (i != itemIdx) + continue; + + 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 + + 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; + + Item &item = Stash.stashList[itemId]; + Point itemPosition = GetStashSlotCoord(slot); + 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) { + 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) { + 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) +{ + int yAbove = anchorY - spriteH - boxH - pad; + 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)); + + int spriteH = GetHoverSpriteHeight(); + int anchorY = floatingInfoBox.position.y; + int boxH = floatingInfoBox.size.height; + int yAbove = anchorY - spriteH - boxH - vPadding; + int yBelow = anchorY + verticalSpacing / 2 + vPadding; + + // 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) { int pointsToReachCap = player.GetMaximumAttributeValue(attribute) - player.GetBaseAttributeValue(attribute); @@ -802,20 +1008,24 @@ void ToggleCharPanel() OpenCharPanel(); } -void AddInfoBoxString(std::string_view str) +void AddInfoBoxString(std::string_view str, bool floatingBox /*= false*/) { - if (InfoString.empty()) - InfoString = str; + StringOrView &infoString = floatingBox ? FloatingInfoString : InfoString; + + if (infoString.empty()) + infoString = str; else - InfoString = StrCat(InfoString, "\n", str); + infoString = StrCat(infoString, "\n", str); } -void AddInfoBoxString(std::string &&str) +void AddInfoBoxString(std::string &&str, bool floatingBox /*= false*/) { - if (InfoString.empty()) - InfoString = std::move(str); + StringOrView &infoString = floatingBox ? FloatingInfoString : InfoString; + + if (infoString.empty()) + infoString = std::move(str); else - InfoString = StrCat(InfoString, "\n", str); + infoString = StrCat(infoString, "\n", str); } Point GetPanelPosition(UiPanels panel, Point offset) @@ -943,6 +1153,7 @@ tl::expected InitMainPanel() buttonEnabled = false; CharPanelButtonActive = false; InfoString = StringOrView {}; + FloatingInfoString = StringOrView {}; RedrawComponent(PanelDrawComponent::Health); RedrawComponent(PanelDrawComponent::Mana); CloseCharPanel(); @@ -1080,6 +1291,7 @@ void CheckPanelInfo() { MainPanelFlag = false; InfoString = StringOrView {}; + FloatingInfoString = StringOrView {}; int totalButtons = IsChatAvailable() ? TotalMpMainPanelButtons : TotalSpMainPanelButtons; @@ -1307,6 +1519,17 @@ void DrawInfoBox(const Surface &out) 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()) { diff --git a/Source/control.h b/Source/control.h index bfe84b76f..3e2b82e8d 100644 --- a/Source/control.h +++ b/Source/control.h @@ -51,6 +51,7 @@ extern bool ChatFlag; extern bool SpellbookFlag; extern bool CharFlag; extern StringOrView InfoString; +extern StringOrView FloatingInfoString; extern bool MainPanelFlag; extern bool MainPanelButtonDown; extern bool SpellSelectFlag; @@ -83,8 +84,8 @@ inline bool CanPanelsCoverView() return GetScreenWidth() <= mainPanel.size.width && GetScreenHeight() <= SidePanelSize.height + mainPanel.size.height; } -void AddInfoBoxString(std::string_view str); -void AddInfoBoxString(std::string &&str); +void AddInfoBoxString(std::string_view str, bool floatingBox = false); +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 }); @@ -168,6 +169,7 @@ void FreeControlPan(); * Sets a string to be drawn in the info box and then draws it. */ void DrawInfoBox(const Surface &out); +void DrawFloatingInfoBox(const Surface &out); void CheckLevelButton(); void CheckLevelButtonUp(); void DrawLevelButton(const Surface &out); diff --git a/Source/engine/render/primitive_render.cpp b/Source/engine/render/primitive_render.cpp index ef7cd7a28..1b5f8df5b 100644 --- a/Source/engine/render/primitive_render.cpp +++ b/Source/engine/render/primitive_render.cpp @@ -127,6 +127,35 @@ void UnsafeDrawVerticalLine(const Surface &out, Point from, int height, std::uin } } +void DrawHalfTransparentHorizontalLine(const Surface &out, Point from, int width, uint8_t colorIndex) +{ + // completely off-bounds? + if (from.y < 0 || from.y >= out.h() || width <= 0 || from.x >= out.w() || from.x + width <= 0) + return; + + int x0 = std::max(0, from.x); + int x1 = std::min(out.w(), from.x + width); + + for (int x = x0; x < x1; ++x) { + SetHalfTransparentPixel(out, { x, from.y }, colorIndex); + } +} + +// Draw a half-transparent vertical line of `height` pixels starting at `from`. +void DrawHalfTransparentVerticalLine(const Surface &out, Point from, int height, uint8_t colorIndex) +{ + // completely off-bounds? + if (from.x < 0 || from.x >= out.w() || height <= 0 || from.y >= out.h() || from.y + height <= 0) + return; + + int y0 = std::max(0, from.y); + int y1 = std::min(out.h(), from.y + height); + + for (int y = y0; y < y1; ++y) { + SetHalfTransparentPixel(out, { from.x, y }, colorIndex); + } +} + void DrawHalfTransparentRectTo(const Surface &out, int sx, int sy, int width, int height) { if (sx + width < 0) diff --git a/Source/engine/render/primitive_render.hpp b/Source/engine/render/primitive_render.hpp index 24d874e5f..b22857000 100644 --- a/Source/engine/render/primitive_render.hpp +++ b/Source/engine/render/primitive_render.hpp @@ -38,6 +38,9 @@ void DrawVerticalLine(const Surface &out, Point from, int height, std::uint8_t c /** Same as DrawVerticalLine but without bounds clipping. */ void UnsafeDrawVerticalLine(const Surface &out, Point from, int height, std::uint8_t colorIndex); +void DrawHalfTransparentHorizontalLine(const Surface &out, Point from, int width, uint8_t colorIndex); +void DrawHalfTransparentVerticalLine(const Surface &out, Point from, int width, uint8_t colorIndex); + /** * Draws a half-transparent rectangle by palette blending with black. * diff --git a/Source/engine/render/scrollrt.cpp b/Source/engine/render/scrollrt.cpp index b5d442090..89fc831d5 100644 --- a/Source/engine/render/scrollrt.cpp +++ b/Source/engine/render/scrollrt.cpp @@ -1782,6 +1782,8 @@ void DrawAndBlit() DrawFlaskValues(out, { mainPanel.position.x + mainPanel.size.width - 138, mainPanel.position.y + 28 }, (HasAnyOf(InspectPlayer->_pIFlags, ItemSpecialEffect::NoMana) || (MyPlayer->_pMana >> 6) <= 0) ? 0 : MyPlayer->_pMana >> 6, HasAnyOf(InspectPlayer->_pIFlags, ItemSpecialEffect::NoMana) ? 0 : MyPlayer->_pMaxMana >> 6); + if (*GetOptions().Gameplay.floatingInfoBox) + DrawFloatingInfoBox(out); DrawCursor(out); diff --git a/Source/inv.cpp b/Source/inv.cpp index 5b9a461c5..c8326ad0a 100644 --- a/Source/inv.cpp +++ b/Source/inv.cpp @@ -1984,9 +1984,11 @@ int8_t CheckInvHLight() if (pi->_itype == ItemType::Gold) { int nGold = pi->_ivalue; InfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)); + FloatingInfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)); } else { InfoColor = pi->getTextColor(); InfoString = pi->getName(); + FloatingInfoString = pi->getName(); if (pi->_iIdentified) { PrintItemDetails(*pi); } else { diff --git a/Source/items.cpp b/Source/items.cpp index c4746fecb..e0792205e 100644 --- a/Source/items.cpp +++ b/Source/items.cpp @@ -1597,90 +1597,90 @@ void PrintItemOil(char iDidx) { switch (iDidx) { case IMISC_OILACC: - AddInfoBoxString(_("increases a weapon's")); - AddInfoBoxString(_("chance to hit")); + AddInfoBoxString(_("increases a weapon's"), true); + AddInfoBoxString(_("chance to hit"), true); break; case IMISC_OILMAST: - AddInfoBoxString(_("greatly increases a")); - AddInfoBoxString(_("weapon's chance to hit")); + AddInfoBoxString(_("greatly increases a"), true); + AddInfoBoxString(_("weapon's chance to hit"), true); break; case IMISC_OILSHARP: - AddInfoBoxString(_("increases a weapon's")); - AddInfoBoxString(_("damage potential")); + AddInfoBoxString(_("increases a weapon's"), true); + AddInfoBoxString(_("damage potential"), true); break; case IMISC_OILDEATH: - AddInfoBoxString(_("greatly increases a weapon's")); - AddInfoBoxString(_("damage potential - not bows")); + AddInfoBoxString(_("greatly increases a weapon's"), true); + AddInfoBoxString(_("damage potential - not bows"), true); break; case IMISC_OILSKILL: - AddInfoBoxString(_("reduces attributes needed")); - AddInfoBoxString(_("to use armor or weapons")); + AddInfoBoxString(_("reduces attributes needed"), true); + AddInfoBoxString(_("to use armor or weapons"), true); break; case IMISC_OILBSMTH: - AddInfoBoxString(/*xgettext:no-c-format*/ _("restores 20% of an")); - AddInfoBoxString(_("item's durability")); + AddInfoBoxString(/*xgettext:no-c-format*/ _("restores 20% of an"), true); + AddInfoBoxString(_("item's durability"), true); break; case IMISC_OILFORT: - AddInfoBoxString(_("increases an item's")); - AddInfoBoxString(_("current and max durability")); + AddInfoBoxString(_("increases an item's"), true); + AddInfoBoxString(_("current and max durability"), true); break; case IMISC_OILPERM: - AddInfoBoxString(_("makes an item indestructible")); + AddInfoBoxString(_("makes an item indestructible"), true); break; case IMISC_OILHARD: - AddInfoBoxString(_("increases the armor class")); - AddInfoBoxString(_("of armor and shields")); + AddInfoBoxString(_("increases the armor class"), true); + AddInfoBoxString(_("of armor and shields"), true); break; case IMISC_OILIMP: - AddInfoBoxString(_("greatly increases the armor")); - AddInfoBoxString(_("class of armor and shields")); + AddInfoBoxString(_("greatly increases the armor"), true); + AddInfoBoxString(_("class of armor and shields"), true); break; case IMISC_RUNEF: - AddInfoBoxString(_("sets fire trap")); + AddInfoBoxString(_("sets fire trap"), true); break; case IMISC_RUNEL: case IMISC_GR_RUNEL: - AddInfoBoxString(_("sets lightning trap")); + AddInfoBoxString(_("sets lightning trap"), true); break; case IMISC_GR_RUNEF: - AddInfoBoxString(_("sets fire trap")); + AddInfoBoxString(_("sets fire trap"), true); break; case IMISC_RUNES: - AddInfoBoxString(_("sets petrification trap")); + AddInfoBoxString(_("sets petrification trap"), true); break; case IMISC_FULLHEAL: - AddInfoBoxString(_("restore all life")); + AddInfoBoxString(_("restore all life"), true); break; case IMISC_HEAL: - AddInfoBoxString(_("restore some life")); + AddInfoBoxString(_("restore some life"), true); break; case IMISC_MANA: - AddInfoBoxString(_("restore some mana")); + AddInfoBoxString(_("restore some mana"), true); break; case IMISC_FULLMANA: - AddInfoBoxString(_("restore all mana")); + AddInfoBoxString(_("restore all mana"), true); break; case IMISC_ELIXSTR: - AddInfoBoxString(_("increase strength")); + AddInfoBoxString(_("increase strength"), true); break; case IMISC_ELIXMAG: - AddInfoBoxString(_("increase magic")); + AddInfoBoxString(_("increase magic"), true); break; case IMISC_ELIXDEX: - AddInfoBoxString(_("increase dexterity")); + AddInfoBoxString(_("increase dexterity"), true); break; case IMISC_ELIXVIT: - AddInfoBoxString(_("increase vitality")); + AddInfoBoxString(_("increase vitality"), true); break; case IMISC_REJUV: - AddInfoBoxString(_("restore some life and mana")); + AddInfoBoxString(_("restore some life and mana"), true); break; case IMISC_FULLREJUV: - AddInfoBoxString(_("restore all life and mana")); + AddInfoBoxString(_("restore all life and mana"), true); break; case IMISC_ARENAPOT: - AddInfoBoxString(_("restore all life and mana")); - AddInfoBoxString(_("(works only in arenas)")); + AddInfoBoxString(_("restore all life and mana"), true); + AddInfoBoxString(_("(works only in arenas)"), true); break; } } @@ -1715,32 +1715,32 @@ Point DrawUniqueInfoWindow(const Surface &out) void printItemMiscKBM(const Item &item, const bool isOil, const bool isCastOnTarget) { if (item._iMiscId == IMISC_MAPOFDOOM) { - AddInfoBoxString(_("Right-click to view")); + AddInfoBoxString(_("Right-click to view"), true); } else if (isOil) { PrintItemOil(item._iMiscId); - AddInfoBoxString(_("Right-click to use")); + AddInfoBoxString(_("Right-click to use"), true); } else if (isCastOnTarget) { - AddInfoBoxString(_("Right-click to read, then\nleft-click to target")); + AddInfoBoxString(_("Right-click to read, then\nleft-click to target"), true); } else if (IsAnyOf(item._iMiscId, IMISC_BOOK, IMISC_NOTE, IMISC_SCROLL, IMISC_SCROLLT)) { - AddInfoBoxString(_("Right-click to read")); + AddInfoBoxString(_("Right-click to read"), true); } } void printItemMiscGenericGamepad(const Item &item, const bool isOil, bool isCastOnTarget) { if (item._iMiscId == IMISC_MAPOFDOOM) { - AddInfoBoxString(_("Activate to view")); + AddInfoBoxString(_("Activate to view"), true); } else if (isOil) { PrintItemOil(item._iMiscId); if (!invflag) { - AddInfoBoxString(_("Open inventory to use")); + AddInfoBoxString(_("Open inventory to use"), true); } else { - AddInfoBoxString(_("Activate to use")); + AddInfoBoxString(_("Activate to use"), true); } } else if (isCastOnTarget) { - AddInfoBoxString(_("Select from spell book, then\ncast spell to read")); + AddInfoBoxString(_("Select from spell book, then\ncast spell to read"), true); } else if (IsAnyOf(item._iMiscId, IMISC_BOOK, IMISC_NOTE, IMISC_SCROLL, IMISC_SCROLLT)) { - AddInfoBoxString(_("Activate to read")); + AddInfoBoxString(_("Activate to read"), true); } } @@ -1754,29 +1754,29 @@ void printItemMiscGamepad(const Item &item, bool isOil, bool isCastOnTarget) const std::string_view castButton = GetOptions().Padmapper.InputNameForAction("SpellAction"); if (item._iMiscId == IMISC_MAPOFDOOM) { - AddInfoBoxString(fmt::format(fmt::runtime(_("{} to view")), activateButton)); + AddInfoBoxString(fmt::format(fmt::runtime(_("{} to view")), activateButton), true); } else if (isOil) { PrintItemOil(item._iMiscId); if (!invflag) { - AddInfoBoxString(_("Open inventory to use")); + AddInfoBoxString(_("Open inventory to use"), true); } else { - AddInfoBoxString(fmt::format(fmt::runtime(_("{} to use")), activateButton)); + AddInfoBoxString(fmt::format(fmt::runtime(_("{} to use")), activateButton), true); } } else if (isCastOnTarget) { - AddInfoBoxString(fmt::format(fmt::runtime(_("Select from spell book,\nthen {} to read")), castButton)); + AddInfoBoxString(fmt::format(fmt::runtime(_("Select from spell book,\nthen {} to read")), castButton), true); } else if (IsAnyOf(item._iMiscId, IMISC_BOOK, IMISC_NOTE, IMISC_SCROLL, IMISC_SCROLLT)) { - AddInfoBoxString(fmt::format(fmt::runtime(_("{} to read")), activateButton)); + AddInfoBoxString(fmt::format(fmt::runtime(_("{} to read")), activateButton), true); } } void PrintItemMisc(const Item &item) { if (item._iMiscId == IMISC_EAR) { - AddInfoBoxString(fmt::format(fmt::runtime(pgettext("player", "Level: {:d}")), item._ivalue)); + AddInfoBoxString(fmt::format(fmt::runtime(pgettext("player", "Level: {:d}")), item._ivalue), true); return; } if (item._iMiscId == IMISC_AURIC) { - AddInfoBoxString(_("Doubles gold capacity")); + AddInfoBoxString(_("Doubles gold capacity"), true); return; } const bool isOil = (item._iMiscId >= IMISC_USEFIRST && item._iMiscId <= IMISC_USELAST) @@ -1816,7 +1816,7 @@ void PrintItemInfo(const Item &item) text.append(fmt::format(fmt::runtime(_(" {:d} Mag")), mag)); if (dex != 0) text.append(fmt::format(fmt::runtime(_(" {:d} Dex")), dex)); - AddInfoBoxString(text); + AddInfoBoxString(text, true); } } @@ -4059,33 +4059,33 @@ void PrintItemDetails(const Item &item) if (item._iClass == ICLASS_WEAPON) { if (item._iMinDam == item._iMaxDam) { if (item._iMaxDur == DUR_INDESTRUCTIBLE) - AddInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d} Indestructible")), item._iMinDam)); + AddInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d} Indestructible")), item._iMinDam), true); else - AddInfoBoxString(fmt::format(fmt::runtime(_(/* TRANSLATORS: Dur: is durability */ "damage: {:d} Dur: {:d}/{:d}")), item._iMinDam, item._iDurability, item._iMaxDur)); + AddInfoBoxString(fmt::format(fmt::runtime(_(/* TRANSLATORS: Dur: is durability */ "damage: {:d} Dur: {:d}/{:d}")), item._iMinDam, item._iDurability, item._iMaxDur), true); } else { if (item._iMaxDur == DUR_INDESTRUCTIBLE) - AddInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d}-{:d} Indestructible")), item._iMinDam, item._iMaxDam)); + AddInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d}-{:d} Indestructible")), item._iMinDam, item._iMaxDam), true); else - AddInfoBoxString(fmt::format(fmt::runtime(_(/* TRANSLATORS: Dur: is durability */ "damage: {:d}-{:d} Dur: {:d}/{:d}")), item._iMinDam, item._iMaxDam, item._iDurability, item._iMaxDur)); + AddInfoBoxString(fmt::format(fmt::runtime(_(/* TRANSLATORS: Dur: is durability */ "damage: {:d}-{:d} Dur: {:d}/{:d}")), item._iMinDam, item._iMaxDam, item._iDurability, item._iMaxDur), true); } } if (item._iClass == ICLASS_ARMOR) { if (item._iMaxDur == DUR_INDESTRUCTIBLE) - AddInfoBoxString(fmt::format(fmt::runtime(_("armor: {:d} Indestructible")), item._iAC)); + AddInfoBoxString(fmt::format(fmt::runtime(_("armor: {:d} Indestructible")), item._iAC), true); else - AddInfoBoxString(fmt::format(fmt::runtime(_(/* TRANSLATORS: Dur: is durability */ "armor: {:d} Dur: {:d}/{:d}")), item._iAC, item._iDurability, item._iMaxDur)); + AddInfoBoxString(fmt::format(fmt::runtime(_(/* TRANSLATORS: Dur: is durability */ "armor: {:d} Dur: {:d}/{:d}")), item._iAC, item._iDurability, item._iMaxDur), true); } if (item._iMiscId == IMISC_STAFF && item._iMaxCharges != 0) { - AddInfoBoxString(fmt::format(fmt::runtime(_("Charges: {:d}/{:d}")), item._iCharges, item._iMaxCharges)); + AddInfoBoxString(fmt::format(fmt::runtime(_("Charges: {:d}/{:d}")), item._iCharges, item._iMaxCharges), true); } if (item._iPrePower != -1) { - AddInfoBoxString(PrintItemPower(item._iPrePower, item)); + AddInfoBoxString(PrintItemPower(item._iPrePower, item), true); } if (item._iSufPower != -1) { - AddInfoBoxString(PrintItemPower(item._iSufPower, item)); + AddInfoBoxString(PrintItemPower(item._iSufPower, item), true); } if (item._iMagical == ITEM_QUALITY_UNIQUE) { - AddInfoBoxString(_("unique item")); + AddInfoBoxString(_("unique item"), true); ShowUniqueItemInfoBox = true; curruitem = item; } @@ -4100,34 +4100,34 @@ void PrintItemDur(const Item &item) if (item._iClass == ICLASS_WEAPON) { if (item._iMinDam == item._iMaxDam) { if (item._iMaxDur == DUR_INDESTRUCTIBLE) - AddInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d} Indestructible")), item._iMinDam)); + AddInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d} Indestructible")), item._iMinDam), true); else - AddInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d} Dur: {:d}/{:d}")), item._iMinDam, item._iDurability, item._iMaxDur)); + AddInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d} Dur: {:d}/{:d}")), item._iMinDam, item._iDurability, item._iMaxDur), true); } else { if (item._iMaxDur == DUR_INDESTRUCTIBLE) - AddInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d}-{:d} Indestructible")), item._iMinDam, item._iMaxDam)); + AddInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d}-{:d} Indestructible")), item._iMinDam, item._iMaxDam), true); else - AddInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d}-{:d} Dur: {:d}/{:d}")), item._iMinDam, item._iMaxDam, item._iDurability, item._iMaxDur)); + AddInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d}-{:d} Dur: {:d}/{:d}")), item._iMinDam, item._iMaxDam, item._iDurability, item._iMaxDur), true); } if (item._iMiscId == IMISC_STAFF && item._iMaxCharges > 0) { - AddInfoBoxString(fmt::format(fmt::runtime(_("Charges: {:d}/{:d}")), item._iCharges, item._iMaxCharges)); + AddInfoBoxString(fmt::format(fmt::runtime(_("Charges: {:d}/{:d}")), item._iCharges, item._iMaxCharges), true); } if (item._iMagical != ITEM_QUALITY_NORMAL) - AddInfoBoxString(_("Not Identified")); + AddInfoBoxString(_("Not Identified"), true); } if (item._iClass == ICLASS_ARMOR) { if (item._iMaxDur == DUR_INDESTRUCTIBLE) - AddInfoBoxString(fmt::format(fmt::runtime(_("armor: {:d} Indestructible")), item._iAC)); + AddInfoBoxString(fmt::format(fmt::runtime(_("armor: {:d} Indestructible")), item._iAC), true); else - AddInfoBoxString(fmt::format(fmt::runtime(_("armor: {:d} Dur: {:d}/{:d}")), item._iAC, item._iDurability, item._iMaxDur)); + AddInfoBoxString(fmt::format(fmt::runtime(_("armor: {:d} Dur: {:d}/{:d}")), item._iAC, item._iDurability, item._iMaxDur), true); if (item._iMagical != ITEM_QUALITY_NORMAL) - AddInfoBoxString(_("Not Identified")); + AddInfoBoxString(_("Not Identified"), true); if (item._iMiscId == IMISC_STAFF && item._iMaxCharges > 0) { - AddInfoBoxString(fmt::format(fmt::runtime(_("Charges: {:d}/{:d}")), item._iCharges, item._iMaxCharges)); + AddInfoBoxString(fmt::format(fmt::runtime(_("Charges: {:d}/{:d}")), item._iCharges, item._iMaxCharges), true); } } if (IsAnyOf(item._itype, ItemType::Ring, ItemType::Amulet)) - AddInfoBoxString(_("Not Identified")); + AddInfoBoxString(_("Not Identified"), true); PrintItemInfo(item); } diff --git a/Source/options.cpp b/Source/options.cpp index 86f9f76fb..7f4a181a6 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -787,6 +787,7 @@ GameplayOptions::GameplayOptions() , showHealthValues("Show health values", OptionEntryFlags::None, N_("Show health values"), N_("Displays current / max health value on health globe."), false) , showManaValues("Show mana values", OptionEntryFlags::None, N_("Show mana values"), N_("Displays current / max mana value on mana globe."), false) , enemyHealthBar("Enemy Health Bar", OptionEntryFlags::None, N_("Enemy Health Bar"), N_("Enemy Health Bar is displayed at the top of the screen."), false) + , floatingInfoBox("Floating Item Info Box", OptionEntryFlags::None, N_("Floating Item Info Box"), N_("Displays item info in a floating box when hovering over an item."), false) , autoGoldPickup("Auto Gold Pickup", OptionEntryFlags::None, N_("Auto Gold Pickup"), N_("Gold is automatically collected when in close proximity to the player."), false) , autoElixirPickup("Auto Elixir Pickup", OptionEntryFlags::None, N_("Auto Elixir Pickup"), N_("Elixirs are automatically collected when in close proximity to the player."), false) , autoOilPickup("Auto Oil Pickup", OptionEntryFlags::OnlyHellfire, N_("Auto Oil Pickup"), N_("Oils are automatically collected when in close proximity to the player."), false) @@ -837,6 +838,7 @@ std::vector GameplayOptions::GetEntries() &showHealthValues, &showManaValues, &enemyHealthBar, + &floatingInfoBox, &showMonsterType, &showItemLabels, &enableFloatingNumbers, diff --git a/Source/options.h b/Source/options.h index 62c969809..7f7f03bbb 100644 --- a/Source/options.h +++ b/Source/options.h @@ -580,6 +580,8 @@ struct GameplayOptions : OptionCategoryBase { OptionEntryBoolean showManaValues; /** @brief Show enemy health at the top of the screen. */ OptionEntryBoolean enemyHealthBar; + /** @brief Displays item info in a floating box when hovering over an ite. */ + OptionEntryBoolean floatingInfoBox; /** @brief Automatically pick up gold when walking over it. */ OptionEntryBoolean autoGoldPickup; /** @brief Auto-pickup elixirs */ diff --git a/Source/qol/stash.cpp b/Source/qol/stash.cpp index 8f6df4fc2..ab0d51e1d 100644 --- a/Source/qol/stash.cpp +++ b/Source/qol/stash.cpp @@ -12,7 +12,6 @@ #include "cursor.h" #include "engine/clx_sprite.hpp" #include "engine/load_clx.hpp" -#include "engine/points_in_rectangle_range.hpp" #include "engine/rectangle.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/text_render.hpp" @@ -54,9 +53,6 @@ constexpr Rectangle StashButtonRect[] = { // clang-format on }; -constexpr Size StashGridSize { 10, 10 }; -constexpr PointsInRectangle StashGridRange { { { 0, 0 }, StashGridSize } }; - OptionalOwnedClxSpriteList StashPanelArt; OptionalOwnedClxSpriteList StashNavButtonArt; @@ -448,6 +444,7 @@ uint16_t CheckStashHLight(Point mousePosition) InfoColor = item.getTextColor(); InfoString = item.getName(); + FloatingInfoString = item.getName(); if (item._iIdentified) { PrintItemDetails(item); } else { diff --git a/Source/qol/stash.h b/Source/qol/stash.h index 5172c01aa..4e5038f05 100644 --- a/Source/qol/stash.h +++ b/Source/qol/stash.h @@ -11,6 +11,7 @@ #include #include "engine/point.hpp" +#include "engine/points_in_rectangle_range.hpp" #include "items.h" namespace devilution { @@ -73,6 +74,9 @@ extern StashStruct Stash; extern bool IsWithdrawGoldOpen; extern int WithdrawGoldValue; +inline constexpr Size StashGridSize { 10, 10 }; +inline constexpr PointsInRectangle StashGridRange { { { 0, 0 }, StashGridSize } }; + Point GetStashSlotCoord(Point slot); void InitStash(); void FreeStashGFX();