You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

455 lines
16 KiB

#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 {
std::string LastSpokenInfoString;
std::string LastSpokenFloatingInfoString;
[[nodiscard]] bool ShouldSpeakInfoBox()
{
// Suppress hover-based dungeon announcements; those are noisy for keyboard/screen-reader play.
return pcursitem == -1 && ObjectUnderCursor == nullptr && pcursmonst == -1 && PlayerUnderCursor == nullptr && PortraitIdUnderCursor == -1;
}
void SpeakIfChanged(const StringOrView &text, std::string &lastSpoken)
{
if (text.empty()) {
lastSpoken.clear();
return;
}
const std::string_view current = text.str();
if (current == lastSpoken)
return;
lastSpoken.assign(current);
SpeakText(current, /*force=*/true);
}
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<int>(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;
if (ShouldSpeakInfoBox())
SpeakIfChanged(InfoString, LastSpokenInfoString);
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<int>(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);
SpeakIfChanged(FloatingInfoString, LastSpokenFloatingInfoString);
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()) {
LastSpokenInfoString.clear();
} else {
PrintInfo(out);
}
}
void DrawFloatingInfoBox(const Surface &out)
{
if (pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell) {
FloatingInfoString = StringOrView {};
InfoColor = UiFlags::ColorWhite;
LastSpokenFloatingInfoString.clear();
}
if (!FloatingInfoString.empty())
PrintFloatingInfo(out);
}
} // namespace devilution