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.

740 lines
20 KiB

4 years ago
#include "qol/stash.h"
#include <algorithm>
#include <cstdint>
4 years ago
#include <utility>
#ifdef USE_SDL3
#include <SDL3/SDL_keyboard.h>
#else
#include <SDL.h>
#endif
#include <fmt/format.h>
#include "DiabloUI/text_input.hpp"
4 years ago
#include "control.h"
#include "controls/plrctrls.h"
#include "cursor.h"
#include "engine/clx_sprite.hpp"
#include "engine/load_clx.hpp"
4 years ago
#include "engine/rectangle.hpp"
#include "engine/render/clx_render.hpp"
4 years ago
#include "engine/render/text_render.hpp"
#include "engine/size.hpp"
#include "headless_mode.hpp"
4 years ago
#include "hwcursor.hpp"
#include "inv.h"
4 years ago
#include "minitext.h"
#include "stores.h"
#include "utils/display.h"
#include "utils/format_int.hpp"
4 years ago
#include "utils/language.h"
#include "utils/sdl_compat.h"
#include "utils/str_cat.hpp"
4 years ago
#include "utils/utf8.hpp"
namespace devilution {
bool IsStashOpen;
StashStruct Stash;
bool IsWithdrawGoldOpen;
namespace {
constexpr unsigned CountStashPages = 100;
constexpr unsigned LastStashPage = CountStashPages - 1;
char GoldWithdrawText[21];
TextInputCursorState GoldWithdrawCursor;
std::optional<NumberInputState> GoldWithdrawInputState;
4 years ago
constexpr Size ButtonSize { 27, 16 };
4 years ago
/** Contains mappings for the buttons in the stash (2 navigation buttons, withdraw gold buttons, 2 navigation buttons) */
constexpr Rectangle StashButtonRect[] = {
// clang-format off
{ { 19, 19 }, ButtonSize }, // 10 left
{ { 56, 19 }, ButtonSize }, // 1 left
{ { 93, 19 }, ButtonSize }, // withdraw gold
{ { 242, 19 }, ButtonSize }, // 1 right
{ { 279, 19 }, ButtonSize } // 10 right
4 years ago
// clang-format on
};
OptionalOwnedClxSpriteList StashPanelArt;
OptionalOwnedClxSpriteList StashNavButtonArt;
4 years ago
/**
* @param page The stash page index.
* @param position Position to add the item to.
4 years ago
* @param stashListIndex The item's StashList index
* @param itemSize Size of item
*/
void AddItemToStashGrid(unsigned page, Point position, uint16_t stashListIndex, Size itemSize)
4 years ago
{
for (const Point point : PointsInRectangle(Rectangle { position, itemSize })) {
Stash.stashGrids[page][point.x][point.y] = stashListIndex + 1;
4 years ago
}
}
std::optional<Point> FindTargetSlotUnderItemCursor(Point cursorPosition, Size itemSize)
4 years ago
{
for (auto point : StashGridRange) {
const Rectangle cell {
4 years ago
GetStashSlotCoord(point),
InventorySlotSizeInPixels + 1
4 years ago
};
if (cell.contains(cursorPosition)) {
// When trying to paste into the stash we need to determine the top left cell of the nearest area that could fit the item, not the slot under the center/hot pixel.
if (itemSize.height <= 1 && itemSize.width <= 1) {
// top left cell of a 1x1 item is the same cell as the hot pixel, no work to do
return point;
}
// Otherwise work out how far the central cell is from the top-left cell
Displacement hotPixelCellOffset = { (itemSize.width - 1) / 2, (itemSize.height - 1) / 2 };
// For even dimension items we need to work out if the cursor is in the left/right (or top/bottom) half of the central cell and adjust the offset so the item lands in the area most covered by the cursor.
if (itemSize.width % 2 == 0 && cell.contains(cursorPosition + Displacement { INV_SLOT_HALF_SIZE_PX, 0 })) {
// hot pixel was in the left half of the cell, so we want to increase the offset to preference the column to the left
hotPixelCellOffset.deltaX++;
}
if (itemSize.height % 2 == 0 && cell.contains(cursorPosition + Displacement { 0, INV_SLOT_HALF_SIZE_PX })) {
// hot pixel was in the top half of the cell, so we want to increase the offset to preference the row above
hotPixelCellOffset.deltaY++;
}
// Then work out the top left cell of the nearest area that could fit this item (as pasting on the edge of the stash would otherwise put it out of bounds)
point.y = std::clamp(point.y - hotPixelCellOffset.deltaY, 0, StashGridSize.height - itemSize.height);
point.x = std::clamp(point.x - hotPixelCellOffset.deltaX, 0, StashGridSize.width - itemSize.width);
4 years ago
return point;
}
}
return {};
4 years ago
}
bool IsItemAllowedInStash(const Item &item)
{
return item._iMiscId != IMISC_ARENAPOT;
}
4 years ago
void CheckStashPaste(Point cursorPosition)
{
Player &player = *MyPlayer;
4 years ago
if (!IsItemAllowedInStash(player.HoldItem))
return;
4 years ago
if (player.HoldItem._itype == ItemType::Gold) {
3 years ago
if (Stash.gold > std::numeric_limits<int>::max() - player.HoldItem._ivalue)
return;
4 years ago
Stash.gold += player.HoldItem._ivalue;
player.HoldItem.clear();
PlaySFX(SfxID::ItemGold);
4 years ago
Stash.dirty = true;
NewCursor(CURSOR_HAND);
return;
}
const Size itemSize = GetInventorySize(player.HoldItem);
std::optional<Point> targetSlot = FindTargetSlotUnderItemCursor(cursorPosition, itemSize);
if (!targetSlot)
4 years ago
return;
const Point firstSlot = *targetSlot;
4 years ago
// Check that no more than 1 item is replaced by the move
StashStruct::StashCell stashIndex = StashStruct::EmptyCell;
for (const Point point : PointsInRectangle(Rectangle { firstSlot, itemSize })) {
const StashStruct::StashCell iv = Stash.GetItemIdAtPosition(point);
if (iv == StashStruct::EmptyCell || stashIndex == iv)
4 years ago
continue;
if (stashIndex == StashStruct::EmptyCell) {
stashIndex = iv; // Found first item
4 years ago
continue;
}
return; // Found a second item
}
PlaySFX(ItemInvSnds[ItemCAnimTbl[player.HoldItem._iCurs]]);
// Need to set the item anchor position to the bottom left so drawing code functions correctly.
4 years ago
player.HoldItem.position = firstSlot + Displacement { 0, itemSize.height - 1 };
if (stashIndex == StashStruct::EmptyCell) {
Stash.stashList.emplace_back(player.HoldItem.pop());
// stashList will have at most 10 000 items, up to 65 535 are supported with uint16_t indexes
stashIndex = static_cast<uint16_t>(Stash.stashList.size() - 1);
4 years ago
} else {
// swap the held item and whatever was in the stash at this position
std::swap(Stash.stashList[stashIndex], player.HoldItem);
// then clear the space occupied by the old item
for (auto &row : Stash.GetCurrentGrid()) {
4 years ago
for (auto &itemId : row) {
if (itemId - 1 == stashIndex)
4 years ago
itemId = 0;
}
}
}
// Finally mark the area now occupied by the pasted item in the current page/grid.
AddItemToStashGrid(Stash.GetPage(), firstSlot, stashIndex, itemSize);
4 years ago
Stash.dirty = true;
NewCursor(player.HoldItem);
4 years ago
}
void CheckStashCut(Point cursorPosition, bool automaticMove)
4 years ago
{
Player &player = *MyPlayer;
4 years ago
CloseGoldWithdraw();
4 years ago
Point slot = InvalidStashPoint;
for (auto point : StashGridRange) {
const Rectangle cell {
4 years ago
GetStashSlotCoord(point),
InventorySlotSizeInPixels + 1
4 years ago
};
// check which inventory rectangle the mouse is in, if any
if (cell.contains(cursorPosition)) {
4 years ago
slot = point;
break;
}
}
if (slot == InvalidStashPoint) {
return;
}
Item &holdItem = player.HoldItem;
holdItem.clear();
4 years ago
bool automaticallyMoved = false;
const bool automaticallyEquipped = false;
4 years ago
const StashStruct::StashCell iv = Stash.GetItemIdAtPosition(slot);
if (iv != StashStruct::EmptyCell) {
4 years ago
holdItem = Stash.stashList[iv];
if (automaticMove) {
if (CanBePlacedOnBelt(player, holdItem)) {
automaticallyMoved = AutoPlaceItemInBelt(player, holdItem, true, true);
4 years ago
} else {
automaticallyMoved = AutoEquip(player, holdItem, true, true);
4 years ago
}
}
if (!automaticMove || automaticallyMoved) {
Stash.RemoveStashItem(iv);
}
}
if (!holdItem.isEmpty()) {
CalcPlrInv(player, true);
holdItem._iStatFlag = player.CanUseItem(holdItem);
4 years ago
if (automaticallyEquipped) {
PlaySFX(ItemInvSnds[ItemCAnimTbl[holdItem._iCurs]]);
} else if (!automaticMove || automaticallyMoved) {
PlaySFX(SfxID::GrabItem);
4 years ago
}
if (automaticMove) {
if (!automaticallyMoved) {
if (CanBePlacedOnBelt(player, holdItem)) {
4 years ago
player.SaySpecific(HeroSpeech::IHaveNoRoom);
} else {
player.SaySpecific(HeroSpeech::ICantDoThat);
}
}
holdItem.clear();
4 years ago
} else {
NewCursor(holdItem);
4 years ago
}
}
}
void WithdrawGold(Player &player, int amount)
{
AddGoldToInventory(player, amount);
4 years ago
Stash.gold -= amount;
Stash.dirty = true;
}
} // namespace
Point GetStashSlotCoord(Point slot)
{
constexpr int StashNextCell = INV_SLOT_SIZE_PX + 1; // spacing between each cell
return GetPanelPosition(UiPanels::Stash, slot * StashNextCell + Displacement { 17, 48 });
}
void FreeStashGFX()
{
StashNavButtonArt = std::nullopt;
StashPanelArt = std::nullopt;
4 years ago
}
void InitStash()
{
if (!HeadlessMode) {
StashPanelArt = LoadClx("data\\stash.clx");
StashNavButtonArt = LoadClx("data\\stashnavbtns.clx");
}
4 years ago
}
void TransferItemToInventory(Player &player, uint16_t itemId)
{
if (itemId == StashStruct::EmptyCell) {
return;
}
const Item &item = Stash.stashList[itemId];
if (item.isEmpty()) {
return;
}
if (!AutoPlaceItemInInventory(player, item)) {
player.SaySpecific(HeroSpeech::IHaveNoRoom);
return;
}
PlaySFX(ItemInvSnds[ItemCAnimTbl[item._iCurs]]);
Stash.RemoveStashItem(itemId);
}
4 years ago
int StashButtonPressed = -1;
void CheckStashButtonRelease(Point mousePosition)
{
if (StashButtonPressed == -1)
return;
Rectangle stashButton = StashButtonRect[StashButtonPressed];
stashButton.position = GetPanelPosition(UiPanels::Stash, stashButton.position);
if (stashButton.contains(mousePosition)) {
4 years ago
switch (StashButtonPressed) {
case 0:
Stash.PreviousPage(10);
4 years ago
break;
case 1:
Stash.PreviousPage();
4 years ago
break;
case 2:
StartGoldWithdraw();
break;
case 3:
Stash.NextPage();
4 years ago
break;
case 4:
Stash.NextPage(10);
4 years ago
break;
}
}
StashButtonPressed = -1;
}
void CheckStashButtonPress(Point mousePosition)
{
Rectangle stashButton;
for (int i = 0; i < 5; i++) {
stashButton = StashButtonRect[i];
stashButton.position = GetPanelPosition(UiPanels::Stash, stashButton.position);
if (stashButton.contains(mousePosition)) {
4 years ago
StashButtonPressed = i;
return;
}
}
StashButtonPressed = -1;
}
void DrawStash(const Surface &out)
{
RenderClxSprite(out, (*StashPanelArt)[0], GetPanelPosition(UiPanels::Stash));
4 years ago
if (StashButtonPressed != -1) {
const Point stashButton = GetPanelPosition(UiPanels::Stash, StashButtonRect[StashButtonPressed].position);
RenderClxSprite(out, (*StashNavButtonArt)[StashButtonPressed], stashButton);
4 years ago
}
constexpr Displacement offset { 0, INV_SLOT_SIZE_PX - 1 };
for (auto slot : StashGridRange) {
const StashStruct::StashCell itemId = Stash.GetItemIdAtPosition(slot);
if (itemId == StashStruct::EmptyCell) {
continue; // No item in the given slot
4 years ago
}
const Item &item = Stash.stashList[itemId];
InvDrawSlotBack(out, GetStashSlotCoord(slot) + offset, InventorySlotSizeInPixels, item._iMagical);
4 years ago
}
for (auto slot : StashGridRange) {
const StashStruct::StashCell itemId = Stash.GetItemIdAtPosition(slot);
if (itemId == StashStruct::EmptyCell) {
4 years ago
continue; // No item in the given slot
}
const Item &item = Stash.stashList[itemId];
if (item.position != slot) {
4 years ago
continue; // Not the first slot of the item
}
const int frame = item._iCurs + CURSOR_FIRSTITEM;
4 years ago
const Point position = GetStashSlotCoord(item.position) + offset;
const ClxSprite sprite = GetInvItemSprite(frame);
4 years ago
if (pcursstashitem == itemId) {
const uint8_t color = GetOutlineColor(item, true);
ClxDrawOutline(out, color, position, sprite);
4 years ago
}
DrawItem(item, out, position, sprite);
4 years ago
}
const Point position = GetPanelPosition(UiPanels::Stash);
const UiFlags style = UiFlags::VerticalCenter | UiFlags::ColorWhite;
const int textboxHeight = 13;
4 years ago
DrawString(out, StrCat(Stash.GetPage() + 1), { position + Displacement { 132, 0 }, { 57, textboxHeight } },
{ .flags = UiFlags::AlignCenter | style });
DrawString(out, FormatInteger(Stash.gold), { position + Displacement { 122, 19 }, { 107, textboxHeight } },
{ .flags = UiFlags::AlignRight | style });
4 years ago
}
void CheckStashItem(Point mousePosition, bool isShiftHeld, bool isCtrlHeld)
{
if (!MyPlayer->HoldItem.isEmpty()) {
4 years ago
CheckStashPaste(mousePosition);
} else if (isCtrlHeld) {
TransferItemToInventory(*MyPlayer, pcursstashitem);
4 years ago
} else {
CheckStashCut(mousePosition, isShiftHeld);
4 years ago
}
}
uint16_t CheckStashHLight(Point mousePosition)
{
Point slot = InvalidStashPoint;
for (auto point : StashGridRange) {
const Rectangle cell {
4 years ago
GetStashSlotCoord(point),
InventorySlotSizeInPixels + 1
4 years ago
};
if (cell.contains(mousePosition)) {
4 years ago
slot = point;
break;
}
}
if (slot == InvalidStashPoint)
return -1;
InfoColor = UiFlags::ColorWhite;
const StashStruct::StashCell itemId = Stash.GetItemIdAtPosition(slot);
if (itemId == StashStruct::EmptyCell) {
4 years ago
return -1;
}
4 years ago
const Item &item = Stash.stashList[itemId];
if (item.isEmpty()) {
4 years ago
return -1;
}
4 years ago
InfoColor = item.getTextColor();
InfoString = item.getName();
FloatingInfoString = item.getName();
4 years ago
if (item._iIdentified) {
PrintItemDetails(item);
} else {
PrintItemDur(item);
}
return itemId;
4 years ago
}
bool UseStashItem(uint16_t c)
{
if (MyPlayer->_pInvincible && MyPlayer->hasNoLife())
4 years ago
return true;
if (pcurs != CURSOR_HAND)
return true;
if (IsPlayerInStore())
4 years ago
return true;
Item *item = &Stash.stashList[c];
constexpr int SpeechDelay = 10;
if (item->IDidx == IDI_MUSHROOM) {
MyPlayer->Say(HeroSpeech::NowThatsOneBigMushroom, SpeechDelay);
return true;
}
if (item->IDidx == IDI_FUNGALTM) {
PlaySFX(SfxID::ItemBook);
4 years ago
MyPlayer->Say(HeroSpeech::ThatDidntDoAnything, SpeechDelay);
return true;
}
if (!item->isUsable())
4 years ago
return false;
if (!MyPlayer->CanUseItem(*item)) {
MyPlayer->Say(HeroSpeech::ICantUseThisYet);
return true;
}
CloseGoldWithdraw();
4 years ago
if (item->isScroll()) {
4 years ago
return true;
}
if (item->_iMiscId > IMISC_RUNEFIRST && item->_iMiscId < IMISC_RUNELAST && leveltype == DTYPE_TOWN) {
4 years ago
return true;
}
if (item->_iMiscId == IMISC_BOOK)
PlaySFX(SfxID::ReadBook);
4 years ago
else
PlaySFX(ItemInvSnds[ItemCAnimTbl[item->_iCurs]]);
UseItem(*MyPlayer, item->_iMiscId, item->_iSpell, -1);
4 years ago
if (Stash.stashList[c]._iMiscId == IMISC_MAPOFDOOM)
return true;
if (Stash.stashList[c]._iMiscId == IMISC_NOTE) {
InitQTextMsg(TEXT_BOOK9);
CloseInventory();
return true;
}
Stash.RemoveStashItem(c);
return true;
}
void StashStruct::RemoveStashItem(StashStruct::StashCell iv)
4 years ago
{
// Iterate through stashGrid and remove every reference to item
for (auto &row : Stash.GetCurrentGrid()) {
for (StashStruct::StashCell &itemId : row) {
4 years ago
if (itemId - 1 == iv) {
itemId = 0;
}
}
}
if (stashList.empty()) {
return;
}
// If the item at the end of stash array isn't the one we removed, we need to swap its position in the array with the removed item
const StashStruct::StashCell lastItemIndex = static_cast<StashStruct::StashCell>(stashList.size() - 1);
4 years ago
if (lastItemIndex != iv) {
stashList[iv] = stashList[lastItemIndex];
for (auto &[_, grid] : Stash.stashGrids) {
4 years ago
for (auto &row : grid) {
for (StashStruct::StashCell &itemId : row) {
4 years ago
if (itemId == lastItemIndex + 1) {
itemId = iv + 1;
}
}
}
}
}
stashList.pop_back();
Stash.dirty = true;
}
void StashStruct::SetPage(unsigned newPage)
{
page = std::min(newPage, LastStashPage);
dirty = true;
}
void StashStruct::NextPage(unsigned offset)
{
if (page <= LastStashPage) {
page += std::min(offset, LastStashPage - page);
} else {
page = LastStashPage;
}
dirty = true;
}
void StashStruct::PreviousPage(unsigned offset)
{
if (page <= LastStashPage) {
page -= std::min(offset, page);
} else {
page = LastStashPage;
}
dirty = true;
}
void StashStruct::RefreshItemStatFlags()
{
for (auto &item : Stash.stashList) {
item.updateRequiredStatsCacheForPlayer(*MyPlayer);
}
}
void StartGoldWithdraw()
{
CloseGoldDrop();
if (ChatFlag)
ResetChat();
const Point start = GetPanelPosition(UiPanels::Stash, { 67, 128 });
SDL_Rect rect = MakeSdlRect(start.x, start.y, 180, 20);
SDL_SetTextInputArea(ghMainWnd, &rect, /*cursor=*/0);
IsWithdrawGoldOpen = true;
GoldWithdrawText[0] = '\0';
GoldWithdrawInputState.emplace(NumberInputState::Options {
.textOptions {
.value = GoldWithdrawText,
.cursor = &GoldWithdrawCursor,
.maxLength = sizeof(GoldWithdrawText) - 1,
},
.min = 0,
.max = std::min(RoomForGold(), Stash.gold),
});
SDLC_StartTextInput(ghMainWnd);
}
void WithdrawGoldKeyPress(SDL_Keycode vkey)
4 years ago
{
Player &myPlayer = *MyPlayer;
4 years ago
if (myPlayer.hasNoLife()) {
4 years ago
CloseGoldWithdraw();
return;
}
switch (vkey) {
case SDLK_RETURN:
case SDLK_KP_ENTER:
if (const int value = GoldWithdrawInputState->value(); value != 0) {
WithdrawGold(myPlayer, value);
PlaySFX(SfxID::ItemGold);
4 years ago
}
CloseGoldWithdraw();
break;
case SDLK_ESCAPE:
4 years ago
CloseGoldWithdraw();
break;
default:
break;
4 years ago
}
}
void DrawGoldWithdraw(const Surface &out)
4 years ago
{
if (!IsWithdrawGoldOpen) {
return;
}
const std::string_view amountText = GoldWithdrawText;
const TextInputCursorState &cursor = GoldWithdrawCursor;
4 years ago
const int dialogX = 30;
ClxDraw(out, GetPanelPosition(UiPanels::Stash, { dialogX, 178 }), (*GoldBoxBuffer)[0]);
4 years ago
// Pre-wrap the string at spaces, otherwise DrawString would hard wrap in the middle of words
const std::string wrapped = WordWrapString(_("How many gold pieces do you want to withdraw?"), 200);
4 years ago
// 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::Stash, { dialogX + 31, 75 }), { 200, 50 } },
{ .flags = UiFlags::ColorWhitegold | UiFlags::AlignCenter, .lineHeight = 17 });
4 years ago
// 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::Stash, { dialogX + 37, 128 }),
{
.flags = UiFlags::ColorWhite | UiFlags::PentaCursor,
.cursorPosition = static_cast<int>(cursor.position),
.highlightRange = { static_cast<int>(cursor.selection.begin), static_cast<int>(cursor.selection.end) },
});
4 years ago
}
void CloseGoldWithdraw()
{
if (!IsWithdrawGoldOpen)
return;
SDLC_StopTextInput(ghMainWnd);
IsWithdrawGoldOpen = false;
GoldWithdrawInputState = std::nullopt;
4 years ago
}
bool HandleGoldWithdrawTextInputEvent(const SDL_Event &event)
4 years ago
{
return HandleNumberInputEvent(event, *GoldWithdrawInputState);
4 years ago
}
bool AutoPlaceItemInStash(Player &player, const Item &item, bool persistItem)
{
if (!IsItemAllowedInStash(item))
return false;
if (item._itype == ItemType::Gold) {
3 years ago
if (Stash.gold > std::numeric_limits<int>::max() - item._ivalue)
return false;
if (persistItem) {
Stash.gold += item._ivalue;
Stash.dirty = true;
}
return true;
}
const Size itemSize = GetInventorySize(item);
// Try to add the item to the current active page and if it's not possible move forward
for (unsigned pageCounter = 0; pageCounter < CountStashPages; pageCounter++) {
unsigned pageIndex = Stash.GetPage() + pageCounter;
// Wrap around if needed
if (pageIndex >= CountStashPages)
pageIndex -= CountStashPages;
// Search all possible position in stash grid
for (auto stashPosition : PointsInRectangle(Rectangle { { 0, 0 }, Size { 10 - (itemSize.width - 1), 10 - (itemSize.height - 1) } })) {
// Check that all needed slots are free
bool isSpaceFree = true;
for (auto itemPoint : PointsInRectangle(Rectangle { stashPosition, itemSize })) {
const uint16_t iv = Stash.stashGrids[pageIndex][itemPoint.x][itemPoint.y];
if (iv != 0) {
isSpaceFree = false;
break;
}
}
if (!isSpaceFree)
continue;
if (persistItem) {
Stash.stashList.push_back(item);
const uint16_t stashIndex = static_cast<uint16_t>(Stash.stashList.size() - 1);
Stash.stashList[stashIndex].position = stashPosition + Displacement { 0, itemSize.height - 1 };
AddItemToStashGrid(pageIndex, stashPosition, stashIndex, itemSize);
Stash.dirty = true;
}
return true;
}
}
return false;
}
4 years ago
} // namespace devilution