diff --git a/CMake/Assets.cmake b/CMake/Assets.cmake index 0d98f2dc561..bc409fbed57 100644 --- a/CMake/Assets.cmake +++ b/CMake/Assets.cmake @@ -62,6 +62,7 @@ set(devilutionx_assets data/resistance.clx data/stash.clx data/stashnavbtns.clx + data/store.clx data/talkbutton.clx data/xpbar.clx fonts/12-00.clx diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index f291841044c..bdb354c074b 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -166,6 +166,7 @@ set(libdevilutionx_SRCS qol/autopickup.cpp qol/chatlog.cpp qol/floatingnumbers.cpp + qol/guistore.cpp qol/itemlabels.cpp qol/monhealthbar.cpp qol/stash.cpp diff --git a/Source/control.cpp b/Source/control.cpp index 320b1b2926c..720f32cd968 100644 --- a/Source/control.cpp +++ b/Source/control.cpp @@ -44,6 +44,7 @@ #include "panels/spell_icons.hpp" #include "panels/spell_list.hpp" #include "playerdat.hpp" +#include "qol/guistore.h" #include "qol/stash.h" #include "qol/xpbar.h" #include "stores.h" @@ -107,7 +108,7 @@ const Rectangle &GetRightPanel() } bool IsLeftPanelOpen() { - return CharFlag || QuestLogIsOpen || IsStashOpen; + return CharFlag || QuestLogIsOpen || IsStashOpen || IsStoreOpen; } bool IsRightPanelOpen() { @@ -686,7 +687,7 @@ bool IsLevelUpButtonVisible() if (ControlMode == ControlTypes::VirtualGamepad) { return false; } - if (ActiveStore != TalkID::None || IsStashOpen) { + if (IsPlayerInStore() || IsStashOpen || IsStoreOpen) { return false; } if (QuestLogIsOpen && GetLeftPanel().contains(GetMainPanel().position + Displacement { 0, -74 })) { @@ -768,6 +769,7 @@ void OpenCharPanel() QuestLogIsOpen = false; CloseGoldWithdraw(); CloseStash(); + CloseStore(); CharFlag = true; } @@ -815,6 +817,7 @@ Point GetPanelPosition(UiPanels panel, Point offset) case UiPanels::Quest: case UiPanels::Character: case UiPanels::Stash: + case UiPanels::Store: return GetLeftPanel().position + displacement; case UiPanels::Spell: case UiPanels::Inventory: @@ -1166,6 +1169,7 @@ void CheckMainPanelButtonUp() CloseCharPanel(); CloseGoldWithdraw(); CloseStash(); + CloseStore(); if (!QuestLogIsOpen) StartQuestlog(); else @@ -1183,6 +1187,7 @@ void CheckMainPanelButtonUp() SpellbookFlag = false; CloseGoldWithdraw(); CloseStash(); + CloseStore(); invflag = !invflag; CloseGoldDrop(); break; @@ -1230,7 +1235,7 @@ void FreeControlPan() void DrawInfoBox(const Surface &out) { DrawPanelBox(out, { 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) { + if (!MainPanelFlag && !trigflag && pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell && pcursstoreitem == StoreStruct::EmptyCell && !SpellSelectFlag && pcurs != CURSOR_HOURGLASS) { InfoString = StringOrView {}; InfoColor = UiFlags::ColorWhite; } diff --git a/Source/controls/game_controls.cpp b/Source/controls/game_controls.cpp index 9c6cfd3bb56..3fe55c7b885 100644 --- a/Source/controls/game_controls.cpp +++ b/Source/controls/game_controls.cpp @@ -14,6 +14,7 @@ #include "gmenu.h" #include "options.h" #include "panels/spell_list.hpp" +#include "qol/guistore.h" #include "qol/stash.h" #include "stores.h" @@ -134,7 +135,7 @@ bool GetGameAction(const SDL_Event &event, ControllerButtonEvent ctrlEvent, Game if (ControllerActionHeld == GameActionType_NONE) { ControllerActionHeld = GameActionType_PRIMARY_ACTION; } - } else if (sgpCurrentMenu != nullptr || ActiveStore != TalkID::None || QuestLogIsOpen) { + } else if (sgpCurrentMenu != nullptr || IsPlayerInStore() || QuestLogIsOpen) { *action = GameActionSendKey { SDLK_RETURN, false }; } else { *action = GameActionSendKey { SDLK_SPACE, false }; @@ -171,12 +172,12 @@ bool GetGameAction(const SDL_Event &event, ControllerButtonEvent ctrlEvent, Game return true; } if (VirtualGamepadState.healthButton.isHeld && VirtualGamepadState.healthButton.didStateChange) { - if (!QuestLogIsOpen && !SpellbookFlag && ActiveStore == TalkID::None) + if (!QuestLogIsOpen && !SpellbookFlag && !IsPlayerInStore()) *action = GameAction(GameActionType_USE_HEALTH_POTION); return true; } if (VirtualGamepadState.manaButton.isHeld && VirtualGamepadState.manaButton.didStateChange) { - if (!QuestLogIsOpen && !SpellbookFlag && ActiveStore == TalkID::None) + if (!QuestLogIsOpen && !SpellbookFlag && !IsPlayerInStore()) *action = GameAction(GameActionType_USE_MANA_POTION); return true; } @@ -196,7 +197,7 @@ bool GetGameAction(const SDL_Event &event, ControllerButtonEvent ctrlEvent, Game SDL_Keycode translation = SDLK_UNKNOWN; - if (gmenu_is_active() || ActiveStore != TalkID::None) + if (gmenu_is_active() || IsPlayerInStore()) translation = TranslateControllerButtonToGameMenuKey(ctrlEvent.button); else if (inGameMenu) translation = TranslateControllerButtonToMenuKey(ctrlEvent.button); @@ -231,6 +232,22 @@ void PressControllerButton(ControllerButton button) } } + if (IsStoreOpen) { + switch (button) { + case ControllerButton_BUTTON_BACK: + // GUISTORE: Special action? + return; + case ControllerButton_BUTTON_LEFTSHOULDER: + // GUISTORE: Previous tab + return; + case ControllerButton_BUTTON_RIGHTSHOULDER: + // GUISTORE: Next tab + return; + default: + return; + } + } + if (PadHotspellMenuActive) { auto quickSpellAction = [](size_t slot) { if (SpellSelectFlag) { diff --git a/Source/controls/plrctrls.cpp b/Source/controls/plrctrls.cpp index 65982b7d55f..e2add59551b 100644 --- a/Source/controls/plrctrls.cpp +++ b/Source/controls/plrctrls.cpp @@ -34,6 +34,7 @@ #include "panels/spell_list.hpp" #include "panels/ui_panels.hpp" #include "qol/chatlog.h" +#include "qol/guistore.h" #include "qol/stash.h" #include "stores.h" #include "towners.h" @@ -65,7 +66,7 @@ quest_id pcursquest = Q_INVALID; */ bool InGameMenu() { - return ActiveStore != TalkID::None + return IsPlayerInStore() || HelpFlag || ChatLogFlag || ChatFlag @@ -79,8 +80,10 @@ namespace { int Slot = SLOTXY_INV_FIRST; Point ActiveStashSlot = InvalidStashPoint; +Point ActiveStoreSlot = InvalidStorePoint; int PreviousInventoryColumn = -1; bool BeltReturnsToStash = false; +bool BeltReturnsToStore = false; const Direction FaceDir[3][3] = { // NONE UP DOWN @@ -719,6 +722,20 @@ Point FindFirstStashSlotOnItem(StashStruct::StashCell itemInvId) return InvalidStashPoint; } +// GUISTORE: Correct grid dimensions +Point FindFirstStoreSlotOnItem(StoreStruct::StoreCell itemInvId) +{ + if (itemInvId == StoreStruct::EmptyCell) + return InvalidStorePoint; + + for (WorldTilePosition point : PointsInRectangle(WorldTileRectangle { { 0, 0 }, { 10, 10 } })) { + if (Store.GetItemIdAtPosition(point) == itemInvId) + return point; + } + + return InvalidStorePoint; +} + /** * Reset cursor position based on the current slot. */ @@ -776,6 +793,23 @@ Point FindClosestStashSlot(Point mousePos) return bestSlot; } +// GUISTORE: Change to correct grid dimensions +Point FindClosestStoreSlot(Point mousePos) +{ + int shortestDistance = std::numeric_limits::max(); + Point bestSlot = {}; + + for (Point point : PointsInRectangle(Rectangle { { 0, 0 }, Size { 10, 10 } })) { + int distance = mousePos.ManhattanDistance(GetStashSlotCoord(point)); + if (distance < shortestDistance) { + shortestDistance = distance; + bestSlot = point; + } + } + + return bestSlot; +} + /** * @brief Figures out where on the body to move when on the first row */ @@ -1179,6 +1213,125 @@ void StashMove(AxisDirection dir) FocusOnInventory(); } +void GUIStoreMove(AxisDirection dir) +{ + static AxisDirectionRepeater repeater(/*min_interval_ms=*/150); + dir = repeater.Get(dir); + if (dir.x == AxisDirectionX_NONE && dir.y == AxisDirectionY_NONE) + return; + + if (Slot < 0 && ActiveStoreSlot == InvalidStorePoint) { + int invSlot = FindClosestInventorySlot(MousePosition); + Point invSlotCoord = GetSlotCoord(invSlot); + int invDistance = MousePosition.ManhattanDistance(invSlotCoord); + + Point storeSlot = FindClosestStoreSlot(MousePosition); + Point storeSlotCoord = GetStoreSlotCoord(storeSlot); + int storeDistance = MousePosition.ManhattanDistance(storeSlotCoord); + + if (invDistance < storeDistance) { + BeltReturnsToStore = false; + InventoryMove(dir); + return; + } + + ActiveStoreSlot = storeSlot; + } + + Item &holdItem = MyPlayer->HoldItem; + Size itemSize = holdItem.isEmpty() ? Size { 1, 1 } : GetInventorySize(holdItem); + + // Jump from belt to store + if (BeltReturnsToStore && Slot >= SLOTXY_BELT_FIRST && Slot <= SLOTXY_BELT_LAST) { + if (dir.y == AxisDirectionY_UP) { + int beltSlot = Slot - SLOTXY_BELT_FIRST; + InvalidateInventorySlot(); + ActiveStoreSlot = { 2 + beltSlot, 10 - itemSize.height }; + dir.y = AxisDirectionY_NONE; + } + } + + // Jump from general inventory to store + if (Slot >= SLOTXY_INV_FIRST && Slot <= SLOTXY_INV_LAST) { + int firstSlot = Slot; + if (MyPlayer->HoldItem.isEmpty()) { + int8_t itemId = GetItemIdOnSlot(Slot); + if (itemId != 0) { + firstSlot = FindFirstSlotOnItem(itemId); + } + } + if (IsAnyOf(firstSlot, SLOTXY_INV_ROW1_FIRST, SLOTXY_INV_ROW2_FIRST, SLOTXY_INV_ROW3_FIRST, SLOTXY_INV_ROW4_FIRST)) { + if (dir.x == AxisDirectionX_LEFT) { + Point slotCoord = GetSlotCoord(Slot); + InvalidateInventorySlot(); + ActiveStoreSlot = FindClosestStoreSlot(slotCoord) - Displacement { itemSize.width - 1, 0 }; + dir.x = AxisDirectionX_NONE; + } + } + } + + bool isHeadSlot = SLOTXY_HEAD == Slot; + bool isLeftHandSlot = SLOTXY_HAND_LEFT == Slot; + bool isLeftRingSlot = Slot == SLOTXY_RING_LEFT; + if (isHeadSlot || isLeftHandSlot || isLeftRingSlot) { + if (dir.x == AxisDirectionX_LEFT) { + Point slotCoord = GetSlotCoord(Slot); + InvalidateInventorySlot(); + ActiveStoreSlot = FindClosestStoreSlot(slotCoord) - Displacement { itemSize.width - 1, 0 }; + dir.x = AxisDirectionX_NONE; + } + } + + if (Slot >= 0) { + InventoryMove(dir); + return; + } + + if (dir.x == AxisDirectionX_LEFT) { + if (ActiveStoreSlot.x > 0) + ActiveStoreSlot.x--; + } else if (dir.x == AxisDirectionX_RIGHT) { + if (ActiveStoreSlot.x < 10 - itemSize.width) { + ActiveStoreSlot.x++; + } else { + Point storeSlotCoord = GetStoreSlotCoord(ActiveStoreSlot); + Point rightPanelCoord = { GetRightPanel().position.x, storeSlotCoord.y }; + Slot = FindClosestInventorySlot(rightPanelCoord); + ActiveStoreSlot = InvalidStorePoint; + BeltReturnsToStore = false; + } + } + if (dir.y == AxisDirectionY_UP) { + if (ActiveStoreSlot.y > 0) + ActiveStoreSlot.y--; + } else if (dir.y == AxisDirectionY_DOWN) { + if (ActiveStoreSlot.y < 10 - itemSize.height) { + ActiveStoreSlot.y++; + } else if ((holdItem.isEmpty() || CanBePlacedOnBelt(*MyPlayer, holdItem)) && ActiveStoreSlot.x > 1) { + int beltSlot = ActiveStoreSlot.x - 2; + Slot = SLOTXY_BELT_FIRST + beltSlot; + ActiveStoreSlot = InvalidStorePoint; + BeltReturnsToStore = true; + } + } + + if (Slot >= 0) { + ResetInvCursorPosition(); + return; + } + + if (ActiveStoreSlot != InvalidStorePoint) { + Point mousePos = GetStoreSlotCoord(ActiveStoreSlot); + // Store coordinates are all the top left of the cell, so we need to shift the mouse to the center of the held item + // or the center of the cell if we have a hand cursor (itemSize will be 1x1 here so we can use the same calculation) + mousePos += Displacement { itemSize.width * INV_SLOT_HALF_SIZE_PX, itemSize.height * INV_SLOT_HALF_SIZE_PX }; + SetCursorPos(mousePos); + return; + } + + FocusOnInventory(); +} + void HotSpellMove(AxisDirection dir) { static AxisDirectionRepeater repeater; @@ -1332,6 +1485,9 @@ HandleLeftStickOrDPadFn GetLeftStickOrDPadGameUIHandler() if (IsStashOpen) { return &StashMove; } + if (IsStoreOpen) { + return &GUIStoreMove; + } if (invflag) { return &CheckInventoryMove; } @@ -1347,7 +1503,7 @@ HandleLeftStickOrDPadFn GetLeftStickOrDPadGameUIHandler() if (QuestLogIsOpen) { return &QuestLogMove; } - if (ActiveStore != TalkID::None) { + if (IsPlayerInStore()) { return &StoreMove; } return nullptr; @@ -1741,7 +1897,7 @@ void plrctrls_after_check_curs_move() if (ControllerActionHeld != GameActionType_NONE && IsNoneOf(LastMouseButtonAction, MouseActionType::None, MouseActionType::Attack, MouseActionType::Spell)) { InvalidateTargets(); - if (pcursmonst == -1 && ObjectUnderCursor == nullptr && pcursitem == -1 && pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell && PlayerUnderCursor == nullptr) { + if (pcursmonst == -1 && ObjectUnderCursor == nullptr && pcursitem == -1 && pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell && pcursstoreitem == StoreStruct::EmptyCell && PlayerUnderCursor == nullptr) { FindTrigger(); } return; @@ -1884,6 +2040,45 @@ void PerformPrimaryAction() mousePos.y += ((cursorSizeInCells.height) * InventorySlotSizeInPixels.height) / 2; } SetCursorPos(mousePos); + } else if (IsStoreOpen && GetLeftPanel().contains(MousePosition)) { // GUISTORE: Revise this + Point storeSlot = (ActiveStoreSlot != InvalidStorePoint) ? ActiveStoreSlot : FindClosestStoreSlot(MousePosition); + + Size cursorSizeInCells = MyPlayer->HoldItem.isEmpty() ? Size { 1, 1 } : GetInventorySize(MyPlayer->HoldItem); + + // Find any item occupying a slot that is currently under the cursor + StoreStruct::StoreCell itemUnderCursor = [](Point storeSlot, Size cursorSizeInCells) -> StoreStruct::StoreCell { + if (storeSlot == InvalidStorePoint) + return StoreStruct::EmptyCell; + for (Point slotUnderCursor : PointsInRectangle(Rectangle { storeSlot, cursorSizeInCells })) { + if (slotUnderCursor.x >= 10 || slotUnderCursor.y >= 10) // GUISTORE: FIX THIS + continue; + StoreStruct::StoreCell itemId = Store.GetItemIdAtPosition(slotUnderCursor); + if (itemId != StoreStruct::EmptyCell) + return itemId; + } + return StoreStruct::EmptyCell; + }(storeSlot, cursorSizeInCells); + + Point jumpSlot = itemUnderCursor == StoreStruct::EmptyCell ? storeSlot : FindFirstStoreSlotOnItem(itemUnderCursor); + CheckStoreItem(MousePosition); + + Point mousePos = GetStoreSlotCoord(jumpSlot); + ActiveStoreSlot = jumpSlot; + if (MyPlayer->HoldItem.isEmpty()) { + // For inventory cut/paste we can combine the cases where we swap or simply paste items. Because store movement is always cell based (there's no fast + // movement over large items) it looks better if we offset the hand cursor to the bottom right cell of the item we just placed. + ActiveStoreSlot += Displacement { cursorSizeInCells - 1 }; // shift the active store slot coordinates to account for items larger than 1x1 + // Then we displace the mouse position to the bottom right corner of the item, then shift it back half a cell to center it. + // Could also be written as (cursorSize - 1) * InventorySlotSize + HalfInventorySlotSize, same thing in the end. + mousePos += Displacement { cursorSizeInCells } * Displacement { InventorySlotSizeInPixels } - Displacement { InventorySlotSizeInPixels } / 2; + } else { + // If we've picked up an item then use the same logic as the inventory so that the cursor is offset to the center of where the old item location was + // (in this case jumpSlot was the top left cell of where it used to be in the grid, and we need to update the cursor size since we're now holding the item) + cursorSizeInCells = GetInventorySize(MyPlayer->HoldItem); + mousePos.x += ((cursorSizeInCells.width) * InventorySlotSizeInPixels.width) / 2; + mousePos.y += ((cursorSizeInCells.height) * InventorySlotSizeInPixels.height) / 2; + } + SetCursorPos(mousePos); } return; } @@ -2082,6 +2277,12 @@ void PerformSecondaryAction() } else if (pcursinvitem != -1) { TransferItemToStash(myPlayer, pcursinvitem); } + //} else if (IsStoreOpen) { + // if (pcursstoreitem != StoreStruct::EmptyCell) { + // GUISTORE: Buy item + //} else if (pcursinvitem != -1) { + // GUISTORE: Sell item + //} } else { CtrlUseInvItem(); } diff --git a/Source/controls/touch/event_handlers.cpp b/Source/controls/touch/event_handlers.cpp index 5e2b3906790..41009663412 100644 --- a/Source/controls/touch/event_handlers.cpp +++ b/Source/controls/touch/event_handlers.cpp @@ -10,6 +10,7 @@ #include "inv.h" #include "panels/spell_book.hpp" #include "panels/spell_list.hpp" +#include "qol/guistore.h" #include "qol/stash.h" #include "stores.h" #include "utils/ui_fwd.h" @@ -35,7 +36,7 @@ void SimulateMouseMovement(const SDL_Event &event) bool isInMainPanel = GetMainPanel().contains(position); bool isInLeftPanel = GetLeftPanel().contains(position); bool isInRightPanel = GetRightPanel().contains(position); - if (IsStashOpen) { + if (IsStashOpen || IsStoreOpen) { if (!SpellSelectFlag && !isInMainPanel && !isInLeftPanel && !isInRightPanel) return; } else if (invflag) { @@ -63,10 +64,10 @@ bool HandleGameMenuInteraction(const SDL_Event &event) bool HandleStoreInteraction(const SDL_Event &event) { - if (ActiveStore == TalkID::None) + if (!IsPlayerInStore()) return false; if (event.type == SDL_FINGERDOWN) - CheckStoreBtn(); + CheckStoreButton(); return true; } @@ -129,6 +130,18 @@ void HandleStashPanelInteraction(const SDL_Event &event) } } +void HandleStorePanelInteraction(const SDL_Event &event) +{ + if (!IsStoreOpen || !MyPlayer->HoldItem.isEmpty()) + return; + + if (event.type != SDL_FINGERUP) { + CheckGUIStoreButtonPress(MousePosition); + } else { + CheckGUIStoreButtonRelease(MousePosition); + } +} + } // namespace void HandleTouchEvent(const SDL_Event &event) @@ -158,6 +171,7 @@ void HandleTouchEvent(const SDL_Event &event) HandleBottomPanelInteraction(event); HandleCharacterPanelInteraction(event); HandleStashPanelInteraction(event); + HandleStorePanelInteraction(event); } bool VirtualGamepadEventHandler::Handle(const SDL_Event &event) diff --git a/Source/controls/touch/renderers.cpp b/Source/controls/touch/renderers.cpp index 6bbe710c4d8..f45cbe87bb3 100644 --- a/Source/controls/touch/renderers.cpp +++ b/Source/controls/touch/renderers.cpp @@ -12,6 +12,7 @@ #include "levels/gendung.h" #include "minitext.h" #include "panels/ui_panels.hpp" +#include "qol/guistore.h" #include "qol/stash.h" #include "stores.h" #include "towners.h" @@ -430,7 +431,7 @@ VirtualGamepadButtonType PrimaryActionButtonRenderer::GetButtonType() VirtualGamepadButtonType PrimaryActionButtonRenderer::GetTownButtonType() { - if (ActiveStore != TalkID::None || pcursmonst != -1) + if (IsPlayerInStore() || pcursmonst != -1) return GetTalkButtonType(virtualPadButton->isHeld); return GetBlankButtonType(virtualPadButton->isHeld); } @@ -447,7 +448,7 @@ VirtualGamepadButtonType PrimaryActionButtonRenderer::GetDungeonButtonType() VirtualGamepadButtonType PrimaryActionButtonRenderer::GetInventoryButtonType() { - if (pcursinvitem != -1 || pcursstashitem != StashStruct::EmptyCell || pcurs > CURSOR_HAND) + if (pcursinvitem != -1 || pcursstashitem != StashStruct::EmptyCell || pcursstoreitem != StoreStruct::EmptyCell || pcurs > CURSOR_HAND) return GetItemButtonType(virtualPadButton->isHeld); return GetBlankButtonType(virtualPadButton->isHeld); } diff --git a/Source/cursor.cpp b/Source/cursor.cpp index 08f8ae84590..ba73a7c505c 100644 --- a/Source/cursor.cpp +++ b/Source/cursor.cpp @@ -29,6 +29,7 @@ #include "levels/trigs.h" #include "missiles.h" #include "options.h" +#include "qol/guistore.h" #include "qol/itemlabels.h" #include "qol/stash.h" #include "towners.h" @@ -402,6 +403,8 @@ int pcursmonst = -1; int8_t pcursinvitem; /** StashItem value */ uint16_t pcursstashitem; +/** StoreItem value */ +uint16_t pcursstoreitem; /** Current highlighted item */ int8_t pcursitem; /** Current highlighted object */ @@ -596,6 +599,7 @@ void InitLevelCursor() ObjectUnderCursor = nullptr; pcursitem = -1; pcursstashitem = StashStruct::EmptyCell; + pcursstoreitem = StoreStruct::EmptyCell; PlayerUnderCursor = nullptr; ClearCursor(); } @@ -762,7 +766,7 @@ bool CheckMouseHold(const Point currentTile) if ((sgbMouseDown != CLICK_NONE || ControllerActionHeld != GameActionType_NONE) && IsNoneOf(LastMouseButtonAction, MouseActionType::None, MouseActionType::Attack, MouseActionType::Spell)) { InvalidateTargets(); - if (pcursmonst == -1 && ObjectUnderCursor == nullptr && pcursitem == -1 && pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell && PlayerUnderCursor == nullptr) { + if (pcursmonst == -1 && ObjectUnderCursor == nullptr && pcursitem == -1 && pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell && pcursstoreitem == StoreStruct::EmptyCell && PlayerUnderCursor == nullptr) { cursPosition = currentTile; DisplayTriggerInfo(); } @@ -782,6 +786,7 @@ void ResetCursorInfo() } pcursinvitem = -1; pcursstashitem = StashStruct::EmptyCell; + pcursstoreitem = StoreStruct::EmptyCell; PlayerUnderCursor = nullptr; ShowUniqueItemInfoBox = false; MainPanelFlag = false; @@ -816,6 +821,9 @@ bool CheckPanelsAndFlags(Rectangle mainPanel) if (IsStashOpen && GetLeftPanel().contains(MousePosition)) { pcursstashitem = CheckStashHLight(MousePosition); } + if (IsStoreOpen && GetLeftPanel().contains(MousePosition)) { + pcursstoreitem = CheckStoreHLight(MousePosition); + } if (SpellbookFlag && GetRightPanel().contains(MousePosition)) { return true; } diff --git a/Source/cursor.h b/Source/cursor.h index 0e459c43cd9..6760f16c35d 100644 --- a/Source/cursor.h +++ b/Source/cursor.h @@ -43,6 +43,7 @@ enum cursor_id : uint8_t { extern int pcursmonst; extern int8_t pcursinvitem; extern uint16_t pcursstashitem; +extern uint16_t pcursstoreitem; extern int8_t pcursitem; struct Object; // Defined in objects.h diff --git a/Source/diablo.cpp b/Source/diablo.cpp index e50b62433bd..9ed6ce9c90b 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -74,6 +74,7 @@ #include "plrmsg.h" #include "qol/chatlog.h" #include "qol/floatingnumbers.h" +#include "qol/guistore.h" #include "qol/itemlabels.h" #include "qol/monhealthbar.h" #include "qol/stash.h" @@ -245,6 +246,10 @@ void LeftMouseCmd(bool bShift) if (leveltype == DTYPE_TOWN) { CloseGoldWithdraw(); CloseStash(); + if (IsStoreOpen) { + CloseStore(); + return; + } if (pcursitem != -1 && pcurs == CURSOR_HAND) NetSendCmdLocParam1(true, invflag ? CMD_GOTOGETITEM : CMD_GOTOAGETITEM, cursPosition, pcursitem); if (pcursmonst != -1) @@ -353,8 +358,13 @@ void LeftMouseDown(uint16_t modState) return; } - if (ActiveStore != TalkID::None) { - CheckStoreBtn(); + if (IsPlayerInStore()) { + CheckStoreButton(); + return; + } + + if (GUIConfirmFlag) { + CheckGUIConfirm(); return; } @@ -377,6 +387,9 @@ void LeftMouseDown(uint16_t modState) if (!IsWithdrawGoldOpen) CheckStashItem(MousePosition, isShiftHeld, isCtrlHeld); CheckStashButtonPress(MousePosition); + } else if (IsStoreOpen && GetLeftPanel().contains(MousePosition)) { + CheckStoreItem(MousePosition, isShiftHeld, isCtrlHeld); + CheckGUIStoreButtonPress(MousePosition); } else if (SpellbookFlag && GetRightPanel().contains(MousePosition)) { CheckSBook(); } else if (!MyPlayer->HoldItem.isEmpty()) { @@ -399,6 +412,8 @@ void LeftMouseDown(uint16_t modState) CheckInvScrn(isShiftHeld, isCtrlHeld); CheckMainPanelButton(); CheckStashButtonPress(MousePosition); + if (IsStoreOpen) + CheckGUIStoreButtonPress(MousePosition); if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) NewCursor(CURSOR_HAND); } @@ -411,14 +426,16 @@ void LeftMouseUp(uint16_t modState) if (MainPanelButtonDown) CheckMainPanelButtonUp(); CheckStashButtonRelease(MousePosition); + if (IsStoreOpen) + CheckGUIStoreButtonRelease(MousePosition); if (CharPanelButtonActive) { const bool isShiftHeld = (modState & KMOD_SHIFT) != 0; ReleaseChrBtns(isShiftHeld); } if (LevelButtonDown) CheckLevelButtonUp(); - if (ActiveStore != TalkID::None) - ReleaseStoreBtn(); + if (IsPlayerInStore()) + ReleaseStoreButton(); } void RightMouseDown(bool isShiftHeld) @@ -439,7 +456,9 @@ void RightMouseDown(bool isShiftHeld) doom_close(); return; } - if (ActiveStore != TalkID::None) + if (IsPlayerInStore()) + return; + if (IsStoreOpen) return; if (SpellSelectFlag) { SetSpell(); @@ -576,7 +595,7 @@ void PressKey(SDL_Keycode vkey, uint16_t modState) if ((modState & KMOD_ALT) != 0) { sgOptions.Graphics.fullscreen.SetValue(!IsFullScreen()); SaveOptions(); - } else if (ActiveStore != TalkID::None) { + } else if (IsPlayerInStore() || GUIConfirmFlag) { StoreEnter(); } else if (QuestLogIsOpen) { QuestlogEnter(); @@ -585,7 +604,7 @@ void PressKey(SDL_Keycode vkey, uint16_t modState) } return; case SDLK_UP: - if (ActiveStore != TalkID::None) { + if (IsPlayerInStore() || GUIConfirmFlag) { StoreUp(); } else if (QuestLogIsOpen) { QuestlogUp(); @@ -597,10 +616,12 @@ void PressKey(SDL_Keycode vkey, uint16_t modState) AutomapUp(); } else if (IsStashOpen) { Stash.PreviousPage(); + } else if (IsStoreOpen) { + Store.PreviousPage(); } return; case SDLK_DOWN: - if (ActiveStore != TalkID::None) { + if (IsPlayerInStore() || GUIConfirmFlag) { StoreDown(); } else if (QuestLogIsOpen) { QuestlogDown(); @@ -612,17 +633,19 @@ void PressKey(SDL_Keycode vkey, uint16_t modState) AutomapDown(); } else if (IsStashOpen) { Stash.NextPage(); + } else if (IsStoreOpen) { + Store.NextPage(); } return; case SDLK_PAGEUP: - if (ActiveStore != TalkID::None) { + if (IsPlayerInStore()) { StorePrior(); } else if (ChatLogFlag) { ChatLogScrollTop(); } return; case SDLK_PAGEDOWN: - if (ActiveStore != TalkID::None) { + if (IsPlayerInStore()) { StoreNext(); } else if (ChatLogFlag) { ChatLogScrollBottom(); @@ -643,7 +666,7 @@ void PressKey(SDL_Keycode vkey, uint16_t modState) void HandleMouseButtonDown(Uint8 button, uint16_t modState) { - if (ActiveStore != TalkID::None && (button == SDL_BUTTON_X1 + if (IsPlayerInStore() && (button == SDL_BUTTON_X1 #if !SDL_VERSION_ATLEAST(2, 0, 0) || button == 8 #endif @@ -752,7 +775,7 @@ void GameEventHandler(const SDL_Event &event, uint16_t modState) #if SDL_VERSION_ATLEAST(2, 0, 0) case SDL_MOUSEWHEEL: if (event.wheel.y > 0) { // Up - if (ActiveStore != TalkID::None) { + if (IsPlayerInStore() || GUIConfirmFlag) { StoreUp(); } else if (QuestLogIsOpen) { QuestlogUp(); @@ -762,11 +785,13 @@ void GameEventHandler(const SDL_Event &event, uint16_t modState) ChatLogScrollUp(); } else if (IsStashOpen) { Stash.PreviousPage(); + } else if (IsStoreOpen) { + Store.PreviousPage(); } else { sgOptions.Keymapper.KeyPressed(MouseScrollUpButton); } } else if (event.wheel.y < 0) { // down - if (ActiveStore != TalkID::None) { + if (IsPlayerInStore() || GUIConfirmFlag) { StoreDown(); } else if (QuestLogIsOpen) { QuestlogDown(); @@ -776,6 +801,8 @@ void GameEventHandler(const SDL_Event &event, uint16_t modState) ChatLogScrollDown(); } else if (IsStashOpen) { Stash.NextPage(); + } else if (IsStoreOpen) { + Store.NextPage(); } else { sgOptions.Keymapper.KeyPressed(MouseScrollDownButton); } @@ -1492,7 +1519,7 @@ void HelpKeyPressed() { if (HelpFlag) { HelpFlag = false; - } else if (ActiveStore != TalkID::None) { + } else if (IsPlayerInStore() || IsStoreOpen) { InfoString = StringOrView {}; AddInfoBoxString(_("No help available")); /// BUGFIX: message isn't displayed AddInfoBoxString(_("while in stores")); @@ -1516,7 +1543,7 @@ void HelpKeyPressed() void InventoryKeyPressed() { - if (ActiveStore != TalkID::None) + if (IsPlayerInStore() || IsStoreOpen) return; invflag = !invflag; if (!IsLeftPanelOpen() && CanPanelsCoverView()) { @@ -1533,11 +1560,12 @@ void InventoryKeyPressed() SpellbookFlag = false; CloseGoldWithdraw(); CloseStash(); + CloseStore(); } void CharacterSheetKeyPressed() { - if (ActiveStore != TalkID::None) + if (IsPlayerInStore() || IsStoreOpen) return; if (!IsRightPanelOpen() && CanPanelsCoverView()) { if (CharFlag) { // We are closing the character sheet @@ -1555,7 +1583,7 @@ void CharacterSheetKeyPressed() void QuestLogKeyPressed() { - if (ActiveStore != TalkID::None) + if (IsPlayerInStore() || IsStoreOpen) return; if (!QuestLogIsOpen) { StartQuestlog(); @@ -1576,11 +1604,12 @@ void QuestLogKeyPressed() CloseCharPanel(); CloseGoldWithdraw(); CloseStash(); + CloseStore(); } void DisplaySpellsKeyPressed() { - if (ActiveStore != TalkID::None) + if (IsPlayerInStore() || IsStoreOpen) return; CloseCharPanel(); QuestLogIsOpen = false; @@ -1596,7 +1625,7 @@ void DisplaySpellsKeyPressed() void SpellBookKeyPressed() { - if (ActiveStore != TalkID::None) + if (IsPlayerInStore() || IsStoreOpen) return; SpellbookFlag = !SpellbookFlag; if (!IsLeftPanelOpen() && CanPanelsCoverView()) { @@ -1658,7 +1687,7 @@ bool CanPlayerTakeAction() bool CanAutomapBeToggledOff() { // check if every window is closed - if yes, automap can be toggled off - if (!QuestLogIsOpen && !IsWithdrawGoldOpen && !IsStashOpen && !CharFlag + if (!QuestLogIsOpen && !IsWithdrawGoldOpen && !IsStashOpen && !IsStoreOpen && !CharFlag && !SpellbookFlag && !invflag && !isGameMenuOpen && !qtextflag && !SpellSelectFlag && !ChatLogFlag && !HelpFlag) return true; @@ -1761,7 +1790,7 @@ void InitKeymapActions() SDLK_F3, [] { gamemenu_load_game(false); }, nullptr, - [&]() { return !gbIsMultiplayer && gbValidSaveFile && ActiveStore == TalkID::None && IsGameRunning(); }); + [&]() { return !gbIsMultiplayer && gbValidSaveFile && !IsPlayerInStore() && !IsStoreOpen && IsGameRunning(); }); #ifndef NOEXIT sgOptions.Keymapper.AddAction( "QuitGame", @@ -1941,6 +1970,7 @@ void InitKeymapActions() PROJECT_NAME, PROJECT_VERSION), UiFlags::ColorWhite); + IsStoreOpen = true; }, nullptr, CanPlayerTakeAction); @@ -2328,7 +2358,7 @@ void InitPadmapActions() ControllerButton_NONE, [] { gamemenu_load_game(false); }, nullptr, - [&]() { return !gbIsMultiplayer && gbValidSaveFile && ActiveStore == TalkID::None && IsGameRunning(); }); + [&]() { return !gbIsMultiplayer && gbValidSaveFile && !IsPlayerInStore() && !IsStoreOpen && IsGameRunning(); }); sgOptions.Padmapper.AddAction( "Item Highlighting", N_("Item highlighting"), @@ -2471,6 +2501,7 @@ void FreeGameMem() FreeObjectGFX(); FreeTownerGFX(); FreeStashGFX(); + FreeStoreGFX(); #ifndef USE_SDL1 DeactivateVirtualGamepad(); FreeVirtualGamepadGFX(); @@ -2778,7 +2809,7 @@ bool PressEscKey() rv = true; } - if (ActiveStore != TalkID::None) { + if (IsPlayerInStore()) { StoreESC(); rv = true; } @@ -3005,6 +3036,7 @@ void LoadGameLevel(bool firstflag, lvl_entry lvldir) InitTowners(); InitStash(); + InitStore(); // GUISTORE: Do toggle check InitItems(); InitMissiles(); IncProgress(); diff --git a/Source/engine/demomode.cpp b/Source/engine/demomode.cpp index ca9d34ab62d..8ff50d2dc72 100644 --- a/Source/engine/demomode.cpp +++ b/Source/engine/demomode.cpp @@ -127,6 +127,7 @@ struct { bool showItemLabels = false; bool autoRefillBelt = false; bool disableCripplingShrines = false; + bool useGUIStores = false; uint8_t numHealPotionPickup = 0; uint8_t numFullHealPotionPickup = 0; uint8_t numManaPotionPickup = 0; @@ -166,6 +167,7 @@ void ReadSettings(FILE *in, uint8_t version) // NOLINT(readability-identifier-le DemoSettings.showItemLabels = ReadByte(in) != 0; DemoSettings.autoRefillBelt = ReadByte(in) != 0; DemoSettings.disableCripplingShrines = ReadByte(in) != 0; + DemoSettings.useGUIStores = ReadByte(in) != 0; DemoSettings.numHealPotionPickup = ReadByte(in); DemoSettings.numFullHealPotionPickup = ReadByte(in); DemoSettings.numManaPotionPickup = ReadByte(in); @@ -194,7 +196,8 @@ void ReadSettings(FILE *in, uint8_t version) // NOLINT(readability-identifier-le { _("Randomize Quests"), DemoSettings.randomizeQuests }, { _("Show Item Labels"), DemoSettings.showItemLabels }, { _("Auto Refill Belt"), DemoSettings.autoRefillBelt }, - { _("Disable Crippling Shrines"), DemoSettings.disableCripplingShrines } }) { + { _("Disable Crippling Shrines"), DemoSettings.disableCripplingShrines }, + { _("Use GUI Stores"), DemoSettings.useGUIStores } }) { fmt::format_to(std::back_inserter(message), "\n{}={:d}", key, value); } for (const auto &[key, value] : std::initializer_list> { @@ -230,6 +233,7 @@ void WriteSettings(FILE *out) WriteByte(out, static_cast(*sgOptions.Gameplay.showItemLabels)); WriteByte(out, static_cast(*sgOptions.Gameplay.autoRefillBelt)); WriteByte(out, static_cast(*sgOptions.Gameplay.disableCripplingShrines)); + WriteByte(out, static_cast(*sgOptions.Gameplay.useGUIStores)); WriteByte(out, *sgOptions.Gameplay.numHealPotionPickup); WriteByte(out, *sgOptions.Gameplay.numFullHealPotionPickup); WriteByte(out, *sgOptions.Gameplay.numManaPotionPickup); @@ -617,6 +621,7 @@ void OverrideOptions() sgOptions.Gameplay.showItemLabels.SetValue(DemoSettings.showItemLabels); sgOptions.Gameplay.autoRefillBelt.SetValue(DemoSettings.autoRefillBelt); sgOptions.Gameplay.disableCripplingShrines.SetValue(DemoSettings.disableCripplingShrines); + sgOptions.Gameplay.useGUIStores.SetValue(DemoSettings.useGUIStores); sgOptions.Gameplay.numHealPotionPickup.SetValue(DemoSettings.numHealPotionPickup); sgOptions.Gameplay.numFullHealPotionPickup.SetValue(DemoSettings.numFullHealPotionPickup); sgOptions.Gameplay.numManaPotionPickup.SetValue(DemoSettings.numManaPotionPickup); diff --git a/Source/engine/render/scrollrt.cpp b/Source/engine/render/scrollrt.cpp index 52f997ebfcd..438dad765cc 100644 --- a/Source/engine/render/scrollrt.cpp +++ b/Source/engine/render/scrollrt.cpp @@ -45,6 +45,7 @@ #include "plrmsg.h" #include "qol/chatlog.h" #include "qol/floatingnumbers.h" +#include "qol/guistore.h" #include "qol/itemlabels.h" #include "qol/monhealthbar.h" #include "qol/stash.h" @@ -613,7 +614,7 @@ void DrawItem(const Surface &out, int8_t itemIndex, Point targetBufferPosition, const Item &item = Items[itemIndex]; const ClxSprite sprite = item.AnimInfo.currentSprite(); const Point position = targetBufferPosition + item.getRenderingOffset(sprite); - if (ActiveStore == TalkID::None && (itemIndex == pcursitem || AutoMapShowItems)) { + if (!IsPlayerInStore() && (itemIndex == pcursitem || AutoMapShowItems)) { ClxDrawOutlineSkipColorZero(out, GetOutlineColor(item, false), position, sprite); } ClxDrawLight(out, position, sprite, lightTableIndex); @@ -1197,8 +1198,10 @@ void DrawView(const Surface &out, Point startPosition) DrawMonsterHealthBar(out); DrawFloatingNumbers(out, startPosition, offset); - if (ActiveStore != TalkID::None && !qtextflag) - DrawSText(out); + if (IsPlayerInStore() && !qtextflag) + DrawStore(out); + if (GUIConfirmFlag) + DrawGUIConfirm(out); if (invflag) { DrawInv(out); } else if (SpellbookFlag) { @@ -1213,7 +1216,11 @@ void DrawView(const Surface &out, Point startPosition) DrawQuestLog(out); } else if (IsStashOpen) { DrawStash(out); + } else if (IsStoreOpen) { + DrawGUIStore(out); } + if (GUIConfirmFlag) + DrawGUIConfirm(out); DrawLevelButton(out); if (ShowUniqueItemInfoBox) { DrawUniqueInfo(out); diff --git a/Source/inv.cpp b/Source/inv.cpp index 02384c43bc4..f9b7dcce300 100644 --- a/Source/inv.cpp +++ b/Source/inv.cpp @@ -29,6 +29,7 @@ #include "panels/ui_panels.hpp" #include "player.h" #include "plrmsg.h" +#include "qol/guistore.h" #include "qol/stash.h" #include "stores.h" #include "towners.h" @@ -1575,6 +1576,8 @@ void CheckInvItem(bool isShiftHeld, bool isCtrlHeld) CheckInvPaste(*MyPlayer, MousePosition); } else if (IsStashOpen && isCtrlHeld) { TransferItemToStash(*MyPlayer, pcursinvitem); + //} else if (IsStoreOpen && isCtrlHeld) { + // GUISTORE: Sell item } else { CheckInvCut(*MyPlayer, MousePosition, isShiftHeld, isCtrlHeld); } @@ -2007,7 +2010,7 @@ bool UseInvItem(int cii) return true; if (pcurs != CURSOR_HAND) return true; - if (ActiveStore != TalkID::None) + if (IsPlayerInStore()) return true; if (cii < INVITEM_INV_FIRST) return false; @@ -2170,6 +2173,31 @@ void CloseStash() IsStashOpen = false; } +void CloseStore() +{ + if (!IsStoreOpen) + return; + + Player &myPlayer = *MyPlayer; + if (!myPlayer.HoldItem.isEmpty()) { + std::optional itemTile = FindAdjacentPositionForItem(myPlayer.position.future, myPlayer._pdir); + if (itemTile) { + NetSendCmdPItem(true, CMD_PUTITEM, *itemTile, myPlayer.HoldItem); + } else { + if (!AutoPlaceItemInBelt(myPlayer, myPlayer.HoldItem, true, true) + && !AutoPlaceItemInInventory(myPlayer, myPlayer.HoldItem, true, true)) { + app_fatal(_("No room for item")); + } + PlaySFX(ItemInvSnds[ItemCAnimTbl[myPlayer.HoldItem._iCurs]]); + } + myPlayer.HoldItem.clear(); + NewCursor(CURSOR_HAND); + } + + IsStoreOpen = false; + StartStore(TalkID::MainMenu); +} + void DoTelekinesis() { if (ObjectUnderCursor != nullptr && !ObjectUnderCursor->IsDisabled()) diff --git a/Source/inv.h b/Source/inv.h index 68c0b063f00..9f47638b909 100644 --- a/Source/inv.h +++ b/Source/inv.h @@ -101,6 +101,7 @@ using ItemFunc = void (*)(Item &); void CloseInventory(); void CloseStash(); +void CloseStore(); void FreeInvGFX(); void InitInv(); diff --git a/Source/items.cpp b/Source/items.cpp index c66085443d1..f51f65b7a5b 100644 --- a/Source/items.cpp +++ b/Source/items.cpp @@ -37,6 +37,7 @@ #include "panels/ui_panels.hpp" #include "player.h" #include "playerdat.hpp" +#include "qol/guistore.h" #include "qol/stash.h" #include "spells.h" #include "stores.h" @@ -321,7 +322,7 @@ SfxID ItemDropSnds[] = { SfxID::ItemLeatherFlip, }; /** Maps from Griswold premium item number to a quality level delta as added to the base quality level. */ -int premiumlvladd[] = { +int itemLevelAdd[] = { // clang-format off -1, -1, @@ -332,7 +333,7 @@ int premiumlvladd[] = { // clang-format on }; /** Maps from Griswold premium item number to a quality level delta as added to the base quality level. */ -int premiumLvlAddHellfire[] = { +int itemLevelAddHf[] = { // clang-format off -1, -1, @@ -1727,11 +1728,16 @@ void PrintItemOil(char iDidx) Point DrawUniqueInfoWindow(const Surface &out) { const bool isInStash = IsStashOpen && GetLeftPanel().contains(MousePosition); + const bool isInStore = IsStoreOpen && GetLeftPanel().contains(MousePosition); int panelX, panelY; if (isInStash) { ClxDraw(out, GetPanelPosition(UiPanels::Stash, { 24 + SidePanelSize.width, 327 }), (*pSTextBoxCels)[0]); panelX = GetLeftPanel().position.x + SidePanelSize.width + 27; panelY = GetLeftPanel().position.y + 28; + } else if (isInStore) { + ClxDraw(out, GetPanelPosition(UiPanels::Store, { 24 + SidePanelSize.width, 327 }), (*pSTextBoxCels)[0]); + panelX = GetLeftPanel().position.x + SidePanelSize.width + 27; + panelY = GetLeftPanel().position.y + 28; } else { ClxDraw(out, GetPanelPosition(UiPanels::Inventory, { 24 - SidePanelSize.width, 327 }), (*pSTextBoxCels)[0]); panelX = GetRightPanel().position.x - SidePanelSize.width + 27; @@ -1748,7 +1754,7 @@ Point DrawUniqueInfoWindow(const Surface &out) DrawHalfTransparentRectTo(out, panelX, panelY, 265, 297); } - return isInStash ? leftInfoPos : rightInfoPos; + return isInStash || isInStore ? leftInfoPos : rightInfoPos; } void printItemMiscKBM(const Item &item, const bool isOil, const bool isCastOnTarget) @@ -1891,17 +1897,31 @@ _item_indexes RndSmithItem(const Player &player, int lvl) return RndVendorItem(player, 0, lvl); } -void SortVendor(Item *itemList) +// FIXME: Move to stores.cpp +void SortVendor(std::vector &itemList, size_t startIndex = 0) { - int count = 1; - while (!itemList[count].isEmpty()) - count++; + // Boundary check + if (startIndex >= itemList.size()) { + return; // No valid range to sort + } + + // Find the first empty item slot + auto firstEmpty = std::find_if(itemList.begin() + startIndex, itemList.end(), [](const Item &item) { + return item.isEmpty(); + }); + + // Return early if no items to sort + if (firstEmpty == itemList.begin() + startIndex) { + return; // No items to sort + } + // Comparison function based on IDidx auto cmp = [](const Item &a, const Item &b) { return a.IDidx < b.IDidx; }; - std::sort(itemList, itemList + count, cmp); + // Sort the non-empty items + std::sort(itemList.begin() + startIndex, firstEmpty, cmp); } bool PremiumItemOk(const Player &player, const ItemData &item) @@ -2896,6 +2916,9 @@ void CalcPlrInv(Player &player, bool loadgfx) if (IsStashOpen) { // If stash is open, ensure the items are displayed correctly Stash.RefreshItemStatFlags(); + } else if (IsStoreOpen) { + // If store is open, ensure the items are displayed correctly + Store.RefreshItemStatFlags(); } } } @@ -4274,6 +4297,8 @@ void UseItem(Player &player, item_misc_id mid, SpellID spellID, int spellFrom) } if (IsStashOpen) { Stash.RefreshItemStatFlags(); + } else if (IsStoreOpen) { + Store.RefreshItemStatFlags(); } } RedrawComponent(PanelDrawComponent::Mana); @@ -4366,16 +4391,20 @@ void SpawnSmith(int lvl) constexpr int PinnedItemCount = 0; int maxValue = 140000; - int maxItems = 19; + int maxItems = NumSmithBasicItems; + if (gbIsHellfire) { maxValue = 200000; - maxItems = 24; + maxItems = NumSmithBasicItemsHf; } int iCnt = RandomIntBetween(10, maxItems); - for (int i = 0; i < iCnt; i++) { - Item &newItem = SmithItems[i]; + // Ensure we have enough items in the vector + while (Blacksmith.basicItems.size() < iCnt) { + Item newItem; + + // Generate a new item with a value under maxValue do { newItem = {}; newItem._iSeed = AdvanceRndSeed(); @@ -4386,42 +4415,50 @@ void SpawnSmith(int lvl) newItem._iCreateInfo = lvl | CF_SMITH; newItem._iIdentified = true; + + // Add the newly generated item to the vector + Blacksmith.basicItems.push_back(newItem); } - for (int i = iCnt; i < SMITH_ITEMS; i++) - SmithItems[i].clear(); - SortVendor(SmithItems + PinnedItemCount); + // If the vector has more items than needed, erase the excess + if (Blacksmith.basicItems.size() > iCnt) { + Blacksmith.basicItems.erase(Blacksmith.basicItems.begin() + iCnt, Blacksmith.basicItems.end()); + } + + SortVendor(Blacksmith.basicItems, PinnedItemCount); } void SpawnPremium(const Player &player) { int lvl = player.getCharacterLevel(); - int maxItems = gbIsHellfire ? SMITH_PREMIUM_ITEMS : 6; - if (PremiumItemCount < maxItems) { - for (int i = 0; i < maxItems; i++) { - if (PremiumItems[i].isEmpty()) { - int plvl = PremiumItemLevel + (gbIsHellfire ? premiumLvlAddHellfire[i] : premiumlvladd[i]); - SpawnOnePremium(PremiumItems[i], plvl, player); - } - } - PremiumItemCount = maxItems; + int maxItems = gbIsHellfire ? NumSmithItemsHf : NumSmithItems; + + // Fill empty slots or add new premium items until we reach maxItems + while (Blacksmith.items.size() < maxItems) { + int plvl = Blacksmith.itemLevel + (gbIsHellfire ? itemLevelAddHf[Blacksmith.items.size()] : itemLevelAdd[Blacksmith.items.size()]); + Item newItem; + SpawnOnePremium(newItem, plvl, player); + Blacksmith.items.push_back(newItem); // Add new premium item } - while (PremiumItemLevel < lvl) { - PremiumItemLevel++; + + // Increase the item level as the player's level increases + while (Blacksmith.itemLevel < lvl) { + Blacksmith.itemLevel++; + if (gbIsHellfire) { - // Discard first 3 items and shift next 10 - std::move(&PremiumItems[3], &PremiumItems[12] + 1, &PremiumItems[0]); - SpawnOnePremium(PremiumItems[10], PremiumItemLevel + premiumLvlAddHellfire[10], player); - PremiumItems[11] = PremiumItems[13]; - SpawnOnePremium(PremiumItems[12], PremiumItemLevel + premiumLvlAddHellfire[12], player); - PremiumItems[13] = PremiumItems[14]; - SpawnOnePremium(PremiumItems[14], PremiumItemLevel + premiumLvlAddHellfire[14], player); + // Remove the first 3 items + Blacksmith.items.erase(Blacksmith.items.begin(), Blacksmith.items.begin() + 3); } else { - // Discard first 2 items and shift next 3 - std::move(&PremiumItems[2], &PremiumItems[4] + 1, &PremiumItems[0]); - SpawnOnePremium(PremiumItems[3], PremiumItemLevel + premiumlvladd[3], player); - PremiumItems[4] = PremiumItems[5]; - SpawnOnePremium(PremiumItems[5], PremiumItemLevel + premiumlvladd[5], player); + // Remove the first 2 items + Blacksmith.items.erase(Blacksmith.items.begin(), Blacksmith.items.begin() + 2); + } + + // Continue adding new items if needed after removing the old ones + while (Blacksmith.items.size() < maxItems) { + int plvl = Blacksmith.itemLevel + (gbIsHellfire ? itemLevelAddHf[Blacksmith.items.size()] : itemLevelAdd[Blacksmith.items.size()]); + Item newItem; + SpawnOnePremium(newItem, plvl, player); + Blacksmith.items.push_back(newItem); // Add new premium item } } } @@ -4435,62 +4472,71 @@ void SpawnWitch(int lvl) int bookCount = 0; const int pinnedBookCount = gbIsHellfire ? RandomIntLessThan(MaxPinnedBookCount) : 0; - const int itemCount = RandomIntBetween(10, gbIsHellfire ? 24 : 17); + const int itemCount = RandomIntBetween(10, gbIsHellfire ? NumWitchItemsHf : NumWitchItems); const int maxValue = gbIsHellfire ? 200000 : 140000; - for (int i = 0; i < WITCH_ITEMS; i++) { - Item &item = WitchItems[i]; - item = {}; + // Ensure the vector has enough space for the new items + Witch.items.reserve(itemCount); + for (int i = 0; i < itemCount; ++i) { + Item newItem = {}; + + // Handle pinned items (Mana, Full Mana, Portal) if (i < PinnedItemCount) { - item._iSeed = AdvanceRndSeed(); - GetItemAttrs(item, PinnedItemTypes[i], 1); - item._iCreateInfo = lvl; - item._iStatFlag = true; - continue; + newItem._iSeed = AdvanceRndSeed(); + GetItemAttrs(newItem, PinnedItemTypes[i], 1); + newItem._iCreateInfo = lvl; + newItem._iStatFlag = true; } - - if (gbIsHellfire) { - if (i < PinnedItemCount + MaxPinnedBookCount && bookCount < pinnedBookCount) { - _item_indexes bookType = PinnedBookTypes[i - PinnedItemCount]; - if (lvl >= AllItemsList[bookType].iMinMLvl) { - item._iSeed = AdvanceRndSeed(); - SetRndSeed(item._iSeed); - DiscardRandomValues(1); - GetItemAttrs(item, bookType, lvl); - item._iCreateInfo = lvl | CF_WITCH; - item._iIdentified = true; - bookCount++; - continue; - } + // Handle pinned books in Hellfire + else if (gbIsHellfire && i < PinnedItemCount + MaxPinnedBookCount && bookCount < pinnedBookCount) { + _item_indexes bookType = PinnedBookTypes[i - PinnedItemCount]; + if (lvl >= AllItemsList[bookType].iMinMLvl) { + newItem._iSeed = AdvanceRndSeed(); + SetRndSeed(newItem._iSeed); + DiscardRandomValues(1); + GetItemAttrs(newItem, bookType, lvl); + newItem._iCreateInfo = lvl | CF_WITCH; + newItem._iIdentified = true; + bookCount++; + } else { + continue; // Skip if the level is too low } } - - if (i >= itemCount) { - item.clear(); - continue; + // Handle regular items + else { + do { + newItem = {}; + newItem._iSeed = AdvanceRndSeed(); + SetRndSeed(newItem._iSeed); + _item_indexes itemData = RndWitchItem(*MyPlayer, lvl); + GetItemAttrs(newItem, itemData, lvl); + + int maxlvl = -1; + if (GenerateRnd(100) <= 5) + maxlvl = 2 * lvl; + if (maxlvl == -1 && newItem._iMiscId == IMISC_STAFF) + maxlvl = 2 * lvl; + if (maxlvl != -1) + GetItemBonus(*MyPlayer, newItem, maxlvl / 2, maxlvl, true, true); + + } while (newItem._iIvalue > maxValue); + + newItem._iCreateInfo = lvl | CF_WITCH; + newItem._iIdentified = true; } - do { - item = {}; - item._iSeed = AdvanceRndSeed(); - SetRndSeed(item._iSeed); - _item_indexes itemData = RndWitchItem(*MyPlayer, lvl); - GetItemAttrs(item, itemData, lvl); - int maxlvl = -1; - if (GenerateRnd(100) <= 5) - maxlvl = 2 * lvl; - if (maxlvl == -1 && item._iMiscId == IMISC_STAFF) - maxlvl = 2 * lvl; - if (maxlvl != -1) - GetItemBonus(*MyPlayer, item, maxlvl / 2, maxlvl, true, true); - } while (item._iIvalue > maxValue); + // Add the newly generated item to the vector + Witch.items.push_back(std::move(newItem)); + } - item._iCreateInfo = lvl | CF_WITCH; - item._iIdentified = true; + // Remove any excess items beyond itemCount if the vector contains more + if (Witch.items.size() > itemCount) { + Witch.items.erase(Witch.items.begin() + itemCount, Witch.items.end()); } - SortVendor(WitchItems + PinnedItemCount); + // Sort the vendor's inventory, keeping pinned items in place + SortVendor(Witch.items, PinnedItemCount); } void SpawnBoy(int lvl) @@ -4509,19 +4555,22 @@ void SpawnBoy(int lvl) dexterity += dexterity / 5; magic += magic / 5; - if (BoyItemLevel >= (lvl / 2) && !BoyItem.isEmpty()) + if (Boy.itemLevel >= (lvl / 2) && !Boy.items.empty()) return; + + Item newItem; + do { keepgoing = false; - BoyItem = {}; - BoyItem._iSeed = AdvanceRndSeed(); - SetRndSeed(BoyItem._iSeed); + newItem = {}; + newItem._iSeed = AdvanceRndSeed(); + SetRndSeed(newItem._iSeed); _item_indexes itype = RndBoyItem(*MyPlayer, lvl); - GetItemAttrs(BoyItem, itype, lvl); - GetItemBonus(*MyPlayer, BoyItem, lvl, 2 * lvl, true, true); + GetItemAttrs(newItem, itype, lvl); + GetItemBonus(*MyPlayer, newItem, lvl, 2 * lvl, true, true); if (!gbIsHellfire) { - if (BoyItem._iIvalue > 90000) { + if (newItem._iIvalue > 90000) { keepgoing = true; // prevent breaking the do/while loop too early by failing hellfire's condition in while continue; } @@ -4530,7 +4579,7 @@ void SpawnBoy(int lvl) ivalue = 0; - ItemType itemType = BoyItem._itype; + ItemType itemType = newItem._itype; switch (itemType) { case ItemType::LightArmor: @@ -4596,49 +4645,56 @@ void SpawnBoy(int lvl) } } while (keepgoing || (( - BoyItem._iIvalue > 200000 - || BoyItem._iMinStr > strength - || BoyItem._iMinMag > magic - || BoyItem._iMinDex > dexterity - || BoyItem._iIvalue < ivalue) + newItem._iIvalue > 200000 + || newItem._iMinStr > strength + || newItem._iMinMag > magic + || newItem._iMinDex > dexterity + || newItem._iIvalue < ivalue) && count < 250)); - BoyItem._iCreateInfo = lvl | CF_BOY; - BoyItem._iIdentified = true; - BoyItemLevel = lvl / 2; + + newItem._iCreateInfo = lvl | CF_BOY; + newItem._iIdentified = true; + Boy.itemLevel = lvl / 2; + + Boy.items.push_back(newItem); } void SpawnHealer(int lvl) { constexpr size_t PinnedItemCount = 2; constexpr std::array<_item_indexes, PinnedItemCount + 1> PinnedItemTypes = { IDI_HEAL, IDI_FULLHEAL, IDI_RESURRECT }; - const auto itemCount = static_cast(RandomIntBetween(10, gbIsHellfire ? 19 : 17)); + const size_t itemCount = static_cast(RandomIntBetween(10, gbIsHellfire ? NumHealerItemsHf : NumHealerItems)); - for (size_t i = 0; i < sizeof(HealerItems) / sizeof(HealerItems[0]); ++i) { - Item &item = HealerItems[i]; - item = {}; + // Reserve space if necessary to optimize performance + Healer.items.reserve(itemCount); + + for (size_t i = 0; i < itemCount; ++i) { + Item newItem = {}; if (i < PinnedItemCount || (gbIsMultiplayer && i == PinnedItemCount)) { - item._iSeed = AdvanceRndSeed(); - GetItemAttrs(item, PinnedItemTypes[i], 1); - item._iCreateInfo = lvl; - item._iStatFlag = true; - continue; + newItem._iSeed = AdvanceRndSeed(); + GetItemAttrs(newItem, PinnedItemTypes[i], 1); + newItem._iCreateInfo = lvl; + newItem._iStatFlag = true; + } else { + newItem._iSeed = AdvanceRndSeed(); + SetRndSeed(newItem._iSeed); + _item_indexes itype = RndHealerItem(*MyPlayer, lvl); + GetItemAttrs(newItem, itype, lvl); + newItem._iCreateInfo = lvl | CF_HEALER; + newItem._iIdentified = true; } - if (i >= itemCount) { - item.clear(); - continue; - } + Healer.items.push_back(std::move(newItem)); + } - item._iSeed = AdvanceRndSeed(); - SetRndSeed(item._iSeed); - _item_indexes itype = RndHealerItem(*MyPlayer, lvl); - GetItemAttrs(item, itype, lvl); - item._iCreateInfo = lvl | CF_HEALER; - item._iIdentified = true; + // Remove any excess items if vector contains more than itemCount + if (Healer.items.size() > itemCount) { + Healer.items.erase(Healer.items.begin() + itemCount, Healer.items.end()); } - SortVendor(HealerItems + PinnedItemCount); + // Sort the vendor's items + SortVendor(Healer.items, PinnedItemCount); } void MakeGoldStack(Item &goldItem, int value) diff --git a/Source/loadsave.cpp b/Source/loadsave.cpp index df2f2453925..6d44d98e22e 100644 --- a/Source/loadsave.cpp +++ b/Source/loadsave.cpp @@ -865,9 +865,20 @@ void LoadItem(LoadHelper &file, Item &item) GetItemFrm(item); } -void LoadPremium(LoadHelper &file, int i) +void LoadPremiumItems(LoadHelper &file) { - LoadAndValidateItemData(file, PremiumItems[i]); + // Resize the vector to the expected number of items in the save file + Blacksmith.items.resize(giNumberOfSmithPremiumItems); + + for (int i = 0; i < giNumberOfSmithPremiumItems; ++i) { + LoadAndValidateItemData(file, Blacksmith.items[i]); + } + + // Remove any empty/null items from the vector after loading + Blacksmith.items.erase(std::remove_if(Blacksmith.items.begin(), Blacksmith.items.end(), [](const Item &item) { + return item._itype == ItemType::None; + }), + Blacksmith.items.end()); } void LoadQuest(LoadHelper *file, int i) @@ -2523,11 +2534,11 @@ void LoadGame(bool firstflag) memset(dLight, 0, sizeof(dLight)); } - PremiumItemCount = file.NextBE(); - PremiumItemLevel = file.NextBE(); + file.Skip(4); // Blacksmith.itemCount + Blacksmith.itemLevel = file.NextBE(); + + LoadPremiumItems(file); - for (int i = 0; i < giNumberOfSmithPremiumItems; i++) - LoadPremium(file, i); if (gbIsHellfire && !gbIsHellfireSaveGame) SpawnPremium(myPlayer); @@ -2786,11 +2797,21 @@ void SaveGameData(SaveWriter &saveWriter) } } - file.WriteBE(PremiumItemCount); - file.WriteBE(PremiumItemLevel); + file.WriteBE(Blacksmith.items.size()); + file.WriteBE(Blacksmith.itemLevel); - for (int i = 0; i < giNumberOfSmithPremiumItems; i++) - SaveItem(file, PremiumItems[i]); + // Save Smith premium items with a fixed count + for (int i = 0; i < giNumberOfSmithPremiumItems; ++i) { + if (i < Blacksmith.items.size()) { + // Save the item from the vector + SaveItem(file, Blacksmith.items[i]); + } else { + // Save an empty item if the vector has fewer items + Item emptyItem; + emptyItem.clear(); // Make the item null + SaveItem(file, emptyItem); + } + } file.WriteLE(AutomapActive ? 1 : 0); file.WriteBE(AutoMapScale); diff --git a/Source/options.cpp b/Source/options.cpp index 5275e359528..11ac8101dce 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -1070,6 +1070,7 @@ GameplayOptions::GameplayOptions() , showMonsterType("Show Monster Type", OptionEntryFlags::None, N_("Show Monster Type"), N_("Hovering over a monster will display the type of monster in the description box in the UI."), false) , showItemLabels("Show Item Labels", OptionEntryFlags::None, N_("Show Item Labels"), N_("Show labels for items on the ground when enabled."), false) , autoRefillBelt("Auto Refill Belt", OptionEntryFlags::None, N_("Auto Refill Belt"), N_("Refill belt from inventory when belt item is consumed."), false) + , useGUIStores("Use GUI Stores", OptionEntryFlags::None, N_("Use GUI Stores"), N_("Use GUI stores instead of the classic stores."), false) , disableCripplingShrines("Disable Crippling Shrines", OptionEntryFlags::None, N_("Disable Crippling Shrines"), N_("When enabled Cauldrons, Fascinating Shrines, Goat Shrines, Ornate Shrines, Sacred Shrines and Murphy's Shrines are not able to be clicked on and labeled as disabled."), false) , quickCast("Quick Cast", OptionEntryFlags::None, N_("Quick Cast"), N_("Spell hotkeys instantly cast the spell, rather than switching the readied spell."), false) , numHealPotionPickup("Heal Potion Pickup", OptionEntryFlags::None, N_("Heal Potion Pickup"), N_("Number of Healing potions to pick up automatically."), 0, { 0, 1, 2, 4, 8, 16 }) @@ -1127,6 +1128,7 @@ std::vector GameplayOptions::GetEntries() &numFullRejuPotionPickup, &autoPickupInTown, &disableCripplingShrines, + &useGUIStores, &adriaRefillsMana, &grabInput, &pauseOnFocusLoss, diff --git a/Source/options.h b/Source/options.h index 3c91257354b..36eff394c0c 100644 --- a/Source/options.h +++ b/Source/options.h @@ -582,6 +582,8 @@ struct GameplayOptions : OptionCategoryBase { OptionEntryBoolean autoRefillBelt; /** @brief Locally disable clicking on shrines which permanently cripple character. */ OptionEntryBoolean disableCripplingShrines; + /** @brief Use GUI based towner stores instead of list based towner stores for trading items. */ + OptionEntryBoolean useGUIStores; /** @brief Spell hotkeys instantly cast the spell. */ OptionEntryBoolean quickCast; /** @brief Number of Healing potions to pick up automatically */ diff --git a/Source/panels/ui_panels.hpp b/Source/panels/ui_panels.hpp index 7054eb79eff..9884ed13494 100644 --- a/Source/panels/ui_panels.hpp +++ b/Source/panels/ui_panels.hpp @@ -11,6 +11,7 @@ enum class UiPanels : uint8_t { Spell, Inventory, Stash, + Store, }; } // namespace devilution diff --git a/Source/qol/chatlog.cpp b/Source/qol/chatlog.cpp index 4fde2956aac..4146c006685 100644 --- a/Source/qol/chatlog.cpp +++ b/Source/qol/chatlog.cpp @@ -96,7 +96,7 @@ void ToggleChatLog() if (ChatLogFlag) { ChatLogFlag = false; } else { - ActiveStore = TalkID::None; + ExitStore(); CloseInventory(); CloseCharPanel(); SpellbookFlag = false; diff --git a/Source/qol/guistore.cpp b/Source/qol/guistore.cpp new file mode 100644 index 00000000000..ff5eac663b4 --- /dev/null +++ b/Source/qol/guistore.cpp @@ -0,0 +1,572 @@ +#include "qol/guistore.h" + +#include +#include +#include + +#include + +#include "DiabloUI/text_input.hpp" +#include "control.h" +#include "controls/plrctrls.h" +#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" +#include "engine/size.hpp" +#include "hwcursor.hpp" +#include "inv.h" +#include "minitext.h" +#include "utils/format_int.hpp" +#include "utils/language.h" +#include "utils/str_cat.hpp" +#include "utils/utf8.hpp" + +namespace devilution { + +bool IsStoreOpen; +StoreStruct Store; +bool GUIConfirmFlag; +uint16_t GUITempItemId; + +namespace { + +constexpr unsigned CountStorePages = 3; +constexpr unsigned LastStorePage = CountStorePages - 1; + +constexpr Size ButtonSize { 71, 19 }; +/** Contains mappings for the buttons in the store */ +constexpr Rectangle StoreButtonRect[] = { + // clang-format off + { { 49, 13 }, ButtonSize }, // Armor + { { 127, 13 }, ButtonSize }, // Weapons + { { 205, 13 }, ButtonSize }, // Misc + { { 236, 318 }, ButtonSize }, // Repair/Recharge/Identify + // clang-format on +}; + +constexpr Size StoreGridSize { 10, 9 }; +constexpr PointsInRectangle StoreGridRange { { { 0, 0 }, StoreGridSize } }; + +OptionalOwnedClxSpriteList StorePanelArt; +/** + * @param page The store page index. + * @param position Position to add the item to. + * @param storeListIndex The item's StoreList index + * @param itemSize Size of item + */ +void AddItemToStoreGrid(unsigned page, Point position, uint16_t storeListIndex, Size itemSize) +{ + for (Point point : PointsInRectangle(Rectangle { position, itemSize })) { + Store.storeGrids[page][point.x][point.y] = storeListIndex + 1; + } +} + +std::optional FindTargetSlotUnderItemCursor(Point cursorPosition, Size itemSize) +{ + for (auto point : StoreGridRange) { + Rectangle cell { + GetStoreSlotCoord(point), + InventorySlotSizeInPixels + 1 + }; + + if (cell.contains(cursorPosition)) { + // When trying to paste into the store 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 store would otherwise put it out of bounds) + point.y = std::clamp(point.y - hotPixelCellOffset.deltaY, 0, StoreGridSize.height - itemSize.height); + point.x = std::clamp(point.x - hotPixelCellOffset.deltaX, 0, StoreGridSize.width - itemSize.width); + return point; + } + } + + return {}; +} + +// GUISTORE: Borrow from filter function +bool IsItemAllowedInStore(const Item &item) +{ + return item._iMiscId != IMISC_ARENAPOT && IsNoneOf(item._iClass, ICLASS_GOLD, ICLASS_QUEST, ICLASS_NONE); +} + +void CheckStorePaste(Point cursorPosition) +{ + Player &player = *MyPlayer; + + if (!IsItemAllowedInStore(player.HoldItem)) + return; + + const Size itemSize = GetInventorySize(player.HoldItem); + + std::optional targetSlot = FindTargetSlotUnderItemCursor(cursorPosition, itemSize); + if (!targetSlot) + return; + + PlaySFX(ItemInvSnds[ItemCAnimTbl[ICURS_GOLD_SMALL]]); + + Item &itemToSell = player.HoldItem; + + int price = GetItemSellValue(itemToSell); + + // Add the gold to the player's inventory + AddGoldToInventory(*MyPlayer, price); + MyPlayer->_pGold += price; + + player.HoldItem.clear(); + + NewCursor(player.HoldItem); +} + +} // namespace + +Point GetStoreSlotCoord(Point slot) +{ + constexpr int StoreNextCell = INV_SLOT_SIZE_PX + 1; // spacing between each cell + + return GetPanelPosition(UiPanels::Store, slot * StoreNextCell + Displacement { 17, 43 }); +} + +void FreeStoreGFX() +{ + StorePanelArt = std::nullopt; +} + +void InitStore() +{ + if (!HeadlessMode) { + StorePanelArt = LoadClx("data\\store.clx"); + } +} + +void StoreStruct::BuyItem() +{ + Item &item = Store.storeList[GUITempItemId]; + + // Calculate the number of pinned items based on the current Towner + int numPinnedItems = 0; + switch (TownerId) { + case TOWN_HEALER: + numPinnedItems = !gbIsMultiplayer ? NumHealerPinnedItems : NumHealerPinnedItemsMp; + break; + case TOWN_WITCH: + numPinnedItems = NumWitchPinnedItems; + break; + } + + // If the item is a pinned item (infinite supply), generate a new seed for it + if (GUITempItemId < numPinnedItems) { + item._iSeed = AdvanceRndSeed(); + } + + // Non-magical items are unidentified + if (item._iMagical == ITEM_QUALITY_NORMAL) { + item._iIdentified = false; + } + + // Deduct player's gold and add the item to their inventory + TakePlrsMoney(item._iIvalue); + GiveItemToPlayer(item, true); + + // If the item is not pinned, remove it from the store grid and list + if (GUITempItemId >= numPinnedItems) { + Store.RemoveStoreItem(GUITempItemId); // Remove item from store + } + + // Handle blacksmith's premium items (replace purchased item with a new one) + if (TownerId == TOWN_SMITH) { + SpawnPremium(*MyPlayer); + } + + // Recalculate the player's inventory after the purchase + CalcPlrInv(*MyPlayer, true); + + PlaySFX(ItemInvSnds[ItemCAnimTbl[item._iCurs]]); +} + +int StoreButtonPressed = -1; + +void CheckGUIStoreButtonRelease(Point mousePosition) +{ + if (StoreButtonPressed == -1) + return; + + Rectangle storeButton = StoreButtonRect[StoreButtonPressed]; + storeButton.position = GetPanelPosition(UiPanels::Store, storeButton.position); + if (storeButton.contains(mousePosition)) { + switch (StoreButtonPressed) { + case 0: + Store.SetPage(0); + break; + case 1: + Store.SetPage(1); + break; + case 2: + Store.SetPage(2); + break; + case 3: + // GUISTORE: Repair/Recharge/Identify + break; + } + } + + StoreButtonPressed = -1; +} + +void CheckGUIStoreButtonPress(Point mousePosition) +{ + Rectangle storeButton; + + for (int i = 0; i < 5; i++) { + storeButton = StoreButtonRect[i]; + storeButton.position = GetPanelPosition(UiPanels::Store, storeButton.position); + if (storeButton.contains(mousePosition)) { + StoreButtonPressed = i; + return; + } + } + + StoreButtonPressed = -1; +} + +void DrawGUIStore(const Surface &out) +{ + RenderClxSprite(out, (*StorePanelArt)[0], GetPanelPosition(UiPanels::Store)); + + if (StoreButtonPressed != -1) { + Point storeButton = GetPanelPosition(UiPanels::Store, StoreButtonRect[StoreButtonPressed].position); + // RenderClxSprite(out, (*StoreNavButtonArt)[StoreButtonPressed], storeButton); + } + + constexpr Displacement offset { 0, INV_SLOT_SIZE_PX - 1 }; + + for (auto slot : StoreGridRange) { + StoreStruct::StoreCell itemId = Store.GetItemIdAtPosition(slot); + if (itemId == StoreStruct::EmptyCell) { + continue; // No item in the given slot + } + Item &item = Store.storeList[itemId]; + InvDrawSlotBack(out, GetStoreSlotCoord(slot) + offset, InventorySlotSizeInPixels, item._iMagical); + } + + for (auto slot : StoreGridRange) { + StoreStruct::StoreCell itemId = Store.GetItemIdAtPosition(slot); + if (itemId == StoreStruct::EmptyCell) { + continue; // No item in the given slot + } + + Item &item = Store.storeList[itemId]; + if (item.position != slot) { + continue; // Not the first slot of the item + } + + int frame = item._iCurs + CURSOR_FIRSTITEM; + + const Point position = GetStoreSlotCoord(item.position) + offset; + const ClxSprite sprite = GetInvItemSprite(frame); + + if (pcursstoreitem == itemId) { + uint8_t color = GetOutlineColor(item, true); + ClxDrawOutline(out, color, position, sprite); + } + + DrawItem(item, out, position, sprite); + } + + Point position = GetPanelPosition(UiPanels::Store); + UiFlags style = UiFlags::VerticalCenter | UiFlags::ColorWhite; + + DrawString(out, _("Armor"), { position + Displacement { 50, 14 }, { 69, 17 } }, + { .flags = UiFlags::AlignCenter | style }); + DrawString(out, _("Weapons"), { position + Displacement { 128, 14 }, { 69, 17 } }, + { .flags = UiFlags::AlignCenter | style }); + DrawString(out, _("Misc"), { position + Displacement { 206, 14 }, { 69, 17 } }, + { .flags = UiFlags::AlignCenter | style }); + + DrawString(out, _("Gold"), { position + Displacement { 24, 320 }, { 164, 14 } }, + { .flags = style }); + DrawString(out, FormatInteger(GetTotalPlayerGold()), { position + Displacement { 24, 320 }, { 164, 14 } }, + { .flags = UiFlags::AlignRight | style }); +} + +void CheckStoreItem(Point mousePosition, bool isShiftHeld, bool isCtrlHeld) +{ + // If the player is holding an item (from inventory), allow them to sell it (paste it into the store) + if (!MyPlayer->HoldItem.isEmpty()) { + CheckStorePaste(mousePosition); // This handles selling an item to the store + } else { + // The player is not holding an item, so they are attempting to buy an item + if (pcursstoreitem != StoreStruct::EmptyCell) { + if (!CanPlayerAfford) { + GUIConfirmFlag = true; + StartStore(TalkID::NoMoney); + return; + } + if (false) { // GUISTORE: No room! + GUIConfirmFlag = true; + StartStore(TalkID::NoRoom); + return; + } + GUITempItemId = pcursstoreitem; + GUIConfirmFlag = true; + StartStore(TalkID::Confirm); + } + } +} + +uint16_t CheckStoreHLight(Point mousePosition) +{ + Point slot = InvalidStorePoint; + for (auto point : StoreGridRange) { + Rectangle cell { + GetStoreSlotCoord(point), + InventorySlotSizeInPixels + 1 + }; + + if (cell.contains(mousePosition)) { + slot = point; + break; + } + } + + if (slot == InvalidStorePoint) + return -1; + + InfoColor = UiFlags::ColorWhite; + + StoreStruct::StoreCell itemId = Store.GetItemIdAtPosition(slot); + if (itemId == StoreStruct::EmptyCell) { + return -1; + } + + Item &item = Store.storeList[itemId]; + if (item.isEmpty()) { + return -1; + } + + InfoColor = item.getTextColor(); + InfoString = item.getName(); + if (item._iIdentified) { + PrintItemDetails(item); + } else { + PrintItemDur(item); + } + + return itemId; +} + +void StoreStruct::RemoveStoreItem(StoreStruct::StoreCell iv) +{ + // Iterate through storeGrid and remove every reference to the item + for (auto &row : Store.GetCurrentGrid()) { + for (StoreStruct::StoreCell &itemId : row) { + if (itemId - 1 == iv) { + itemId = 0; + } + } + } + + if (storeList.empty()) { + return; + } + + Item &removedItem = storeList[iv]; + bool itemFound = false; + + // Check if the item exists in the towner's basicItems or items vectors and remove it + TownerStore *towner = townerStores[TownerId]; + auto removeMatchingItem = [&removedItem](std::vector &itemVector) { + for (auto it = itemVector.begin(); it != itemVector.end(); ++it) { + if (it->_iSeed == removedItem._iSeed && it->_iCreateInfo == removedItem._iCreateInfo) { + itemVector.erase(it); + return true; + } + } + return false; + }; + + itemFound = removeMatchingItem(towner->basicItems); + if (!itemFound) { + removeMatchingItem(towner->items); + } + + // If the item at the end of store array isn't the one we removed, we need to swap its position in the array with the removed item + StoreStruct::StoreCell lastItemIndex = static_cast(storeList.size() - 1); + if (lastItemIndex != iv) { + storeList[iv] = storeList[lastItemIndex]; + + for (auto &[_, grid] : Store.storeGrids) { + for (auto &row : grid) { + for (StoreStruct::StoreCell &itemId : row) { + if (itemId == lastItemIndex + 1) { + itemId = iv + 1; + } + } + } + } + } + + storeList.pop_back(); + Store.dirty = true; +} + +void StoreStruct::SetPage(unsigned newPage) +{ + page = std::min(newPage, LastStorePage); + dirty = true; +} + +void StoreStruct::NextPage(unsigned offset) +{ + if (page <= LastStorePage) { + page += std::min(offset, LastStorePage - page); + } else { + page = LastStorePage; + } + dirty = true; +} + +void StoreStruct::PreviousPage(unsigned offset) +{ + if (page <= LastStorePage) { + page -= std::min(offset, page); + } else { + page = LastStorePage; + } + dirty = true; +} + +void StoreStruct::RefreshItemStatFlags() +{ + for (auto &item : Store.storeList) { + item.updateRequiredStatsCacheForPlayer(*MyPlayer); + } +} + +bool AutoSellItemToStore(Player &player, const Item &item, bool persistItem) +{ + if (!IsItemAllowedInStore(item)) + return false; + + 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 < CountStorePages; pageCounter++) { + unsigned pageIndex = Store.GetPage() + pageCounter; + // Wrap around if needed + if (pageIndex >= CountStorePages) + pageIndex -= CountStorePages; + // Search all possible position in store grid + for (auto storePosition : 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 { storePosition, itemSize })) { + uint16_t iv = Store.storeGrids[pageIndex][itemPoint.x][itemPoint.y]; + if (iv != 0) { + isSpaceFree = false; + break; + } + } + if (!isSpaceFree) + continue; + if (persistItem) { + Store.storeList.push_back(item); + uint16_t storeIndex = static_cast(Store.storeList.size() - 1); + Store.storeList[storeIndex].position = storePosition + Displacement { 0, itemSize.height - 1 }; + AddItemToStoreGrid(pageIndex, storePosition, storeIndex, itemSize); + Store.dirty = true; + } + return true; + } + } + + return false; +} + +void PopulateStoreGrid() +{ + // Get the current towner store (determined by global TownerId) + TownerStore *towner = townerStores[TownerId]; + + // Clear the store grids to start fresh + Store.storeGrids.clear(); + Store.storeList.clear(); + + // Function to add an item to the store grid for the given page + auto addItemToGrid = [](unsigned pageIndex, const Item &item) { + Size itemSize = GetInventorySize(item); + + // Search for a position in the page's grid + for (auto storePosition : PointsInRectangle(Rectangle { { 0, 0 }, Size { 10 - (itemSize.width - 1), 10 - (itemSize.height - 1) } })) { + // Check if all needed slots for the item are free + bool isSpaceFree = true; + for (auto itemPoint : PointsInRectangle(Rectangle { storePosition, itemSize })) { + if (Store.storeGrids[pageIndex][itemPoint.x][itemPoint.y] != 0) { + isSpaceFree = false; // Slot is occupied + break; + } + } + + if (!isSpaceFree) + continue; + + // Place the item in the grid if space is available + Store.storeList.push_back(item); + uint16_t storeIndex = static_cast(Store.storeList.size() - 1); + + // Set the item's position in the grid + Store.storeList[storeIndex].position = storePosition + Displacement { 0, itemSize.height - 1 }; + + // Mark the grid cells as occupied by this item + AddItemToStoreGrid(pageIndex, storePosition, storeIndex, itemSize); + + Store.dirty = true; + return; + } + }; + + // Determine the item vector to use based on the TalkID + const std::vector &itemsToProcess = (ActiveStore == TalkID::BasicBuy) ? towner->basicItems : towner->items; + + // Iterate through the selected item vector and place them in the correct page + for (const Item &item : itemsToProcess) { + // Determine the page based on the item class + unsigned pageIndex; + switch (item._iClass) { + case ICLASS_ARMOR: + pageIndex = 0; // Armor tab + break; + case ICLASS_WEAPON: + pageIndex = 1; // Weapon tab + break; + case ICLASS_MISC: + pageIndex = 2; // Misc tab + break; + default: + continue; // Skip items that don't belong in any of these categories + } + + // Add the item to the correct grid page + addItemToGrid(pageIndex, item); + } + + Store.RefreshItemStatFlags(); +} + +} // namespace devilution diff --git a/Source/qol/guistore.h b/Source/qol/guistore.h new file mode 100644 index 00000000000..e860d138302 --- /dev/null +++ b/Source/qol/guistore.h @@ -0,0 +1,100 @@ +/** + * @file qol/guistore.h + * + * Interface of the GUI Store. + */ +#pragma once + +#include +#include + +#include + +#include "engine/point.hpp" +#include "items.h" +#include "stores.h" + +namespace devilution { + +extern bool GUIConfirmFlag; +extern uint16_t GUITempItemId; + +class StoreStruct { +public: + using StoreCell = uint16_t; + using StoreGrid = std::array, 10>; + static constexpr StoreCell EmptyCell = -1; + + void RemoveStoreItem(StoreCell iv); + ankerl::unordered_dense::map storeGrids; + std::vector storeList; + bool dirty = false; + + unsigned GetPage() const + { + return page; + } + + StoreGrid &GetCurrentGrid() + { + return storeGrids[GetPage()]; + } + + /** + * @brief Returns the 0-based index of the item at the specified position, or EmptyCell if no item occupies that slot + * @param gridPosition x,y coordinate of the current store page + * @return a value which can be used to index into storeList or StoreStruct::EmptyCell + */ + StoreCell GetItemIdAtPosition(Point gridPosition) + { + // Because StoreCell is an unsigned type we can let this underflow + return GetCurrentGrid()[gridPosition.x][gridPosition.y] - 1; + } + + bool IsItemAtPosition(Point gridPosition) + { + return GetItemIdAtPosition(gridPosition) != EmptyCell; + } + + void SetPage(unsigned newPage); + void NextPage(unsigned offset = 1); + void PreviousPage(unsigned offset = 1); + + /** @brief Updates _iStatFlag for all store items. */ + void RefreshItemStatFlags(); + void BuyItem(); + +private: + /** Current Page */ + unsigned page; +}; + +constexpr Point InvalidStorePoint { -1, -1 }; + +extern bool IsStoreOpen; +extern StoreStruct Store; + +Point GetStoreSlotCoord(Point slot); +void InitStore(); +void FreeStoreGFX(); +/** + * @brief Render the inventory panel to the given buffer. + */ +void DrawGUIStore(const Surface &out); +void CheckStoreItem(Point mousePosition, bool isShiftHeld = false, bool isCtrlHeld = false); +uint16_t CheckStoreHLight(Point mousePosition); +void CheckGUIStoreButtonRelease(Point mousePosition); +void CheckGUIStoreButtonPress(Point mousePosition); + +/** + * @brief Checks whether the given item can be placed on the specified player's store. + * If 'persistItem' is 'True', the item is also placed in the inventory. + * @param player The player to check. + * @param item The item to be checked. + * @param persistItem Pass 'True' to actually place the item in the inventory. The default is 'False'. + * @return 'True' in case the item can be placed on the player's inventory and 'False' otherwise. + */ +bool AutoSellItemToStore(Player &player, const Item &item, bool persistItem); +void PopulateStoreGrid(); + +} // namespace devilution diff --git a/Source/qol/itemlabels.cpp b/Source/qol/itemlabels.cpp index 6d0d78394f0..f02ba3ca24f 100644 --- a/Source/qol/itemlabels.cpp +++ b/Source/qol/itemlabels.cpp @@ -98,7 +98,7 @@ void ResetItemlabelHighlighted() bool IsHighlightingLabelsEnabled() { - return ActiveStore == TalkID::None && highlightKeyPressed != *sgOptions.Gameplay.showItemLabels; + return !IsPlayerInStore() && highlightKeyPressed != *sgOptions.Gameplay.showItemLabels; } void AddItemToLabelQueue(int id, Point position) @@ -193,7 +193,7 @@ void DrawItemNameLabels(const Surface &out) if (!gmenu_is_active() && PauseMode == 0 && !MyPlayerIsDead - && ActiveStore == TalkID::None + && !IsPlayerInStore() && IsMouseOverGameArea() && LastMouseButtonAction == MouseActionType::None) { isLabelHighlighted = true; @@ -201,7 +201,7 @@ void DrawItemNameLabels(const Surface &out) pcursitem = label.id; } } - if (pcursitem == label.id && ActiveStore == TalkID::None) + if (pcursitem == label.id && !IsPlayerInStore()) FillRect(clippedOut, label.pos.x, label.pos.y, label.width, labelHeight, PAL8_BLUE + 6); else DrawHalfTransparentRectTo(clippedOut, label.pos.x, label.pos.y, label.width, labelHeight); diff --git a/Source/qol/stash.cpp b/Source/qol/stash.cpp index c4a5320871f..71274a97d67 100644 --- a/Source/qol/stash.cpp +++ b/Source/qol/stash.cpp @@ -462,7 +462,7 @@ bool UseStashItem(uint16_t c) return true; if (pcurs != CURSOR_HAND) return true; - if (ActiveStore != TalkID::None) + if (IsPlayerInStore()) return true; Item *item = &Stash.stashList[c]; diff --git a/Source/stores.cpp b/Source/stores.cpp index 588eec54595..2bac5b72c95 100644 --- a/Source/stores.cpp +++ b/Source/stores.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -15,16 +16,13 @@ #include "cursor.h" #include "engine/backbuffer_state.hpp" #include "engine/load_cel.hpp" -#include "engine/random.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/text_render.hpp" #include "engine/trn.hpp" #include "init.h" #include "minitext.h" -#include "options.h" #include "panels/info_box.hpp" -#include "qol/stash.h" -#include "towners.h" +#include "qol/guistore.h" #include "utils/format_int.hpp" #include "utils/language.h" #include "utils/str_cat.hpp" @@ -32,38 +30,46 @@ namespace devilution { -TalkID ActiveStore; +TownerStore Blacksmith("Griswold", TalkID::BasicBuy, TalkID::Buy, TalkID::Sell, TalkID::Repair, ResourceType::Invalid); +TownerStore Healer("Pepin", TalkID::Invalid, TalkID::Buy, TalkID::Invalid, TalkID::Invalid, ResourceType::Life); +TownerStore Witch("Adria", TalkID::Invalid, TalkID::Buy, TalkID::Sell, TalkID::Recharge, ResourceType::Mana); +TownerStore Boy("Wirt", TalkID::Invalid, TalkID::Buy, TalkID::Invalid, TalkID::Invalid, ResourceType::Invalid); +TownerStore Storyteller("Cain", TalkID::Invalid, TalkID::Invalid, TalkID::Invalid, TalkID::Identify, ResourceType::Invalid); +TownerStore Barmaid("Gillian", TalkID::Invalid, TalkID::Invalid, TalkID::Invalid, TalkID::Stash, ResourceType::Invalid); +TownerStore Tavern("Ogden", TalkID::Invalid, TalkID::Invalid, TalkID::Invalid, TalkID::Invalid, ResourceType::Invalid); +TownerStore Drunk("Farnham", TalkID::Invalid, TalkID::Invalid, TalkID::Invalid, TalkID::Invalid, ResourceType::Invalid); +TownerStore CowFarmer("Cow Farmer", TalkID::Invalid, TalkID::Invalid, TalkID::Invalid, TalkID::Invalid, ResourceType::Invalid); +TownerStore Farmer("Lester", TalkID::Invalid, TalkID::Invalid, TalkID::Invalid, TalkID::Invalid, ResourceType::Invalid); -int CurrentItemIndex; -int8_t PlayerItemIndexes[48]; -Item PlayerItems[48]; +TalkID ActiveStore; // The current store screen +_talker_id TownerId; // The current towner being interacted with -Item SmithItems[SMITH_ITEMS]; -int PremiumItemCount; -int PremiumItemLevel; -Item PremiumItems[SMITH_PREMIUM_ITEMS]; +std::vector playerItems; -Item HealerItems[20]; +std::unordered_map<_talker_id, TownerStore *> townerStores; -Item WitchItems[WITCH_ITEMS]; - -int BoyItemLevel; -Item BoyItem; +Item TempItem; // Temporary item used to hold the item being traded namespace { -/** The current towner being interacted with */ -_talker_id TownerId; +constexpr int PaddingTop = 32; + +const int SingleLineSpace = 1; +const int DoubleLineSpace = 2; +const int TripleLineSpace = 3; + +constexpr int MainMenuDividerLine = 5; +constexpr int BuySellMenuDividerLine = 3; +constexpr int ItemLineSpace = 4; +constexpr int ConfirmLine = 18; +constexpr int GUIConfirmLine = 3; // GUISTORE: Move? -/** Is the current dialog full size */ -bool IsTextFullSize; +constexpr int WirtDialogueDrawLine = 12; -/** Number of text lines in the current dialog */ -int NumTextLines; -/** Remember currently selected text line from TextLine while displaying a dialog */ -int OldTextLine; -/** Currently selected text line from TextLine */ -int CurrentTextLine; +bool IsTextFullSize; // Is the current dialog full size +int NumTextLines; // Number of text lines in the current dialog +int OldTextLine; // Remember currently selected text line from TextLine while displaying a dialog +int CurrentTextLine; // Currently selected text line from TextLine struct STextStruct { enum Type : uint8_t { @@ -97,47 +103,103 @@ struct STextStruct { } }; -/** Text lines */ -STextStruct TextLine[STORE_LINES]; - -/** Whether to render the player's gold amount in the top left */ -bool RenderGold; - -/** Does the current panel have a scrollbar */ -bool HasScrollbar; -/** Remember last scroll position */ -int OldScrollPos; -/** Scroll position */ -int ScrollPos; -/** Next scroll position */ -int NextScrollPos; -/** Previous scroll position */ -int PreviousScrollPos; -/** Countdown for the push state of the scroll up button */ -int8_t CountdownScrollUp; -/** Countdown for the push state of the scroll down button */ -int8_t CountdownScrollDown; - -/** Remember current store while displaying a dialog */ -TalkID OldActiveStore; - -/** Temporary item used to hold the item being traded */ -Item TempItem; - -/** Maps from towner IDs to NPC names. */ -const char *const TownerNames[] = { - N_("Griswold"), - N_("Pepin"), - "", - N_("Ogden"), - N_("Cain"), - N_("Farnham"), - N_("Adria"), - N_("Gillian"), - N_("Wirt"), +std::array TextLine; // Text lines + +bool RenderGold; // Whether to render the player's gold amount in the top left +int OldScrollPos; // Remember last scroll position +int ScrollPos; // Scroll position +int NextScrollPos; // Next scroll position +int PreviousScrollPos; // Previous scroll position +int8_t CountdownScrollUp; // Countdown for the push state of the scroll up button +int8_t CountdownScrollDown; // Countdown for the push state of the scroll down button + +TalkID OldActiveStore; // Remember current store while displaying a dialog + +std::vector> LineActionMappings; +int CurrentMenuDrawLine; + +const std::string SmithMenuHeader = "Welcome to the\n\nBlacksmith's shop"; + +const StoreMenuOption SmithMenuOptions[] = { + { TalkID::Gossip, fmt::format("Talk to {:s}", Blacksmith.name) }, + { TalkID::BasicBuy, "Buy basic items" }, + { TalkID::Buy, "Buy premium items" }, + { TalkID::Sell, "Sell items" }, + { TalkID::Repair, "Repair items" }, + { TalkID::Exit, "Leave the shop" } +}; + +const std::string HealerMenuHeader = "Welcome to the\n\nHealer's home"; + +const StoreMenuOption HealerMenuOptions[] = { + { TalkID::Gossip, fmt::format("Talk to {:s}", Healer.name) }, + { TalkID::Buy, "Buy items" }, + { TalkID::Exit, "Leave Healer's home" } +}; + +const std::string BoyMenuHeader = "Wirt the Peg-legged boy"; + +const StoreMenuOption BoyMenuOptions[] = { + { TalkID::Gossip, fmt::format("Talk to {:s}", Boy.name) }, + { TalkID::Buy, "What have you got?" }, + { TalkID::Exit, "Say goodbye" } }; -constexpr int PaddingTop = 32; +const std::string WitchMenuHeader = "Welcome to the\n\nWitch's shack"; + +const StoreMenuOption WitchMenuOptions[] = { + { TalkID::Gossip, fmt::format("Talk to {:s}", Witch.name) }, + { TalkID::Buy, "Buy items" }, + { TalkID::Sell, "Sell items" }, + { TalkID::Recharge, "Recharge staves" }, + { TalkID::Exit, "Leave the shack" } +}; + +const std::string TavernMenuHeader = "Welcome to the\n\nRising Sun"; + +const StoreMenuOption TavernMenuOptions[] = { + { TalkID::Gossip, fmt::format("Talk to {:s}", Tavern.name) }, + { TalkID::Exit, "Leave the tavern" } +}; + +const std::string BarmaidMenuHeader = "Gillian"; + +const StoreMenuOption BarmaidMenuOptions[] = { + { TalkID::Gossip, fmt::format("Talk to {:s}", Barmaid.name) }, + { TalkID::Stash, "Access Stash" }, + { TalkID::Exit, "Say goodbye" } +}; + +const std::string DrunkMenuHeader = "Farnham the Drunk"; + +const StoreMenuOption DrunkMenuOptions[] = { + { TalkID::Gossip, fmt::format("Talk to {:s}", Drunk.name) }, + { TalkID::Exit, "Say goodbye" } +}; + +const std::string StorytellerMenuHeader = "The Town Elder"; + +const StoreMenuOption StorytellerMenuOptions[] = { + { TalkID::Gossip, fmt::format("Talk to {:s}", Storyteller.name) }, + { TalkID::Identify, "Identify an item" }, + { TalkID::Exit, "Say goodbye" } +}; + +const TownerLine TownerLines[] = { + { SmithMenuHeader, SmithMenuOptions, sizeof(SmithMenuOptions) / sizeof(StoreMenuOption) }, + { HealerMenuHeader, HealerMenuOptions, sizeof(HealerMenuOptions) / sizeof(StoreMenuOption) }, + {}, + { TavernMenuHeader, TavernMenuOptions, sizeof(TavernMenuOptions) / sizeof(StoreMenuOption) }, + { StorytellerMenuHeader, StorytellerMenuOptions, sizeof(StorytellerMenuOptions) / sizeof(StoreMenuOption) }, + { DrunkMenuHeader, DrunkMenuOptions, sizeof(DrunkMenuOptions) / sizeof(StoreMenuOption) }, + { WitchMenuHeader, WitchMenuOptions, sizeof(WitchMenuOptions) / sizeof(StoreMenuOption) }, + { BarmaidMenuHeader, BarmaidMenuOptions, sizeof(BarmaidMenuOptions) / sizeof(StoreMenuOption) }, + { BoyMenuHeader, BoyMenuOptions, sizeof(BoyMenuOptions) / sizeof(StoreMenuOption) }, + {}, + {}, + {}, + {}, +}; // For most languages, line height is always 12. // This includes blank lines and divider line. @@ -150,6 +212,58 @@ constexpr int SmallTextHeight = 12; constexpr int LargeLineHeight = SmallLineHeight + 1; constexpr int LargeTextHeight = 18; +void InitializeTownerStores() +{ + townerStores[TOWN_SMITH] = &Blacksmith; + townerStores[TOWN_HEALER] = &Healer; + townerStores[TOWN_WITCH] = &Witch; + townerStores[TOWN_PEGBOY] = &Boy; + townerStores[TOWN_STORY] = &Storyteller; + townerStores[TOWN_BMAID] = &Barmaid; + townerStores[TOWN_TAVERN] = &Tavern; + townerStores[TOWN_DRUNK] = &Drunk; + + if (gbIsHellfire) { + townerStores[TOWN_COWFARM] = &CowFarmer; + townerStores[TOWN_FARMER] = &Farmer; + } +} + +void SetActiveStore(TalkID talkId) +{ + OldActiveStore = ActiveStore; + ActiveStore = talkId; +} + +int GetItemCount(TalkID talkId) +{ + TownerStore *towner = townerStores[TownerId]; + + if (towner != nullptr) { + switch (talkId) { + case TalkID::BasicBuy: + return towner->basicItems.size(); + case TalkID::Buy: + return towner->items.size(); + } + } + + return playerItems.size(); +} + +bool HasScrollbar() +{ + if (!IsAnyOf(ActiveStore, TalkID::BasicBuy, TalkID::Buy, TalkID::Sell, TalkID::Repair, TalkID::Recharge, TalkID::Identify)) + return false; + + int itemCount = GetItemCount(ActiveStore); + + if (itemCount <= ItemLineSpace) + return false; + + return true; +} + /** * The line index with the Back / Leave button. * This is a special button that is always the last line. @@ -159,9 +273,13 @@ constexpr int LargeTextHeight = 18; int BackButtonLine() { if (IsSmallFontTall()) { - return HasScrollbar ? 21 : 20; + if (HasScrollbar()) { + GUIConfirmFlag ? 3 : 21; + } else { + GUIConfirmFlag ? 2 : 20; + } } - return 22; + return GUIConfirmFlag ? 4 : 22; } int LineHeight() @@ -178,7 +296,7 @@ void CalculateLineHeights() { TextLine[0].y = 0; if (IsSmallFontTall()) { - for (int i = 1; i < STORE_LINES; ++i) { + for (int i = 1; i < NumStoreLines; ++i) { // Space out consecutive text lines, unless they are both selectable (never the case currently). if (TextLine[i].hasText() && TextLine[i - 1].hasText() && !(TextLine[i].isSelectable() && TextLine[i - 1].isSelectable())) { TextLine[i].y = TextLine[i - 1].y + LargeTextHeight; @@ -187,21 +305,22 @@ void CalculateLineHeights() } } } else { - for (int i = 1; i < STORE_LINES; ++i) { + for (int i = 1; i < NumStoreLines; ++i) { TextLine[i].y = i * SmallLineHeight; } } } -void DrawSTextBack(const Surface &out) +void DrawTextUI(const Surface &out) { const Point uiPosition = GetUIRectangle().position; ClxDraw(out, { uiPosition.x + 320 + 24, 327 + uiPosition.y }, (*pSTextBoxCels)[0]); DrawHalfTransparentRectTo(out, uiPosition.x + 347, uiPosition.y + 28, 265, 297); } -void DrawSSlider(const Surface &out, int y1, int y2) +void DrawScrollbar(const Surface &out, int y1, int y2) { + int itemCount = GetItemCount(ActiveStore); const Point uiPosition = GetUIRectangle().position; int yd1 = y1 * 12 + 44 + uiPosition.y; int yd2 = y2 * 12 + 44 + uiPosition.y; @@ -222,14 +341,12 @@ void DrawSSlider(const Surface &out, int y1, int y2) yd3 = OldTextLine; else yd3 = CurrentTextLine; - if (CurrentItemIndex > 1) - yd3 = 1000 * (ScrollPos + ((yd3 - PreviousScrollPos) / 4)) / (CurrentItemIndex - 1) * (y2 * 12 - y1 * 12 - 24) / 1000; - else - yd3 = 0; + + yd3 = 1000 * (ScrollPos + ((yd3 - PreviousScrollPos) / 4)) / (itemCount - 1) * ((y2 * 12) - (y1 * 12) - 24) / 1000; ClxDraw(out, { uiPosition.x + 601, (y1 + 1) * 12 + 44 + uiPosition.y + yd3 }, (*pSTextSlidCels)[12]); } -void AddSLine(size_t y) +void SetLineAsDivider(size_t y) { TextLine[y]._sx = 0; TextLine[y]._syoff = 0; @@ -240,12 +357,12 @@ void AddSLine(size_t y) TextLine[y].cursIndent = false; } -void AddSTextVal(size_t y, int val) +void SetLineValue(size_t y, int val) { TextLine[y]._sval = val; } -void AddSText(uint8_t x, size_t y, std::string_view text, UiFlags flags, bool sel, int cursId = -1, bool cursIndent = false) +void SetLineText(uint8_t x, size_t y, std::string_view text, UiFlags flags, bool sel, int cursId = -1, bool cursIndent = false) { TextLine[y]._sx = x; TextLine[y]._syoff = 0; @@ -257,22 +374,22 @@ void AddSText(uint8_t x, size_t y, std::string_view text, UiFlags flags, bool se TextLine[y].cursIndent = cursIndent; } -void AddOptionsBackButton() +void SetLineAsOptionsBackButton() { const int line = BackButtonLine(); - AddSText(0, line, _("Back"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); + SetLineText(0, line, _("Back"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); TextLine[line]._syoff = IsSmallFontTall() ? 0 : 6; } -void AddItemListBackButton(bool selectable = false) +void AddItemListBackButton(TalkID talkId, bool selectable = false) { const int line = BackButtonLine(); - std::string_view text = _("Back"); + std::string_view text = (TownerId == TOWN_PEGBOY && talkId == TalkID::Buy) ? _("Leave") : _("Back"); if (!selectable && IsSmallFontTall()) { - AddSText(0, line, text, UiFlags::ColorWhite | UiFlags::AlignRight, selectable); + SetLineText(0, line, text, UiFlags::ColorWhite | UiFlags::AlignRight, selectable); } else { - AddSLine(line - 1); - AddSText(0, line, text, UiFlags::ColorWhite | UiFlags::AlignCenter, selectable); + SetLineAsDivider(line - 1); + SetLineText(0, line, text, UiFlags::ColorWhite | UiFlags::AlignCenter, selectable); TextLine[line]._syoff = 6; } } @@ -299,7 +416,7 @@ void PrintStoreItem(const Item &item, int l, UiFlags flags, bool cursIndent = fa productLine.append(fmt::format(fmt::runtime(_("Charges: {:d}/{:d}")), item._iCharges, item._iMaxCharges)); } if (!productLine.empty()) { - AddSText(40, l, productLine, flags, false, -1, cursIndent); + SetLineText(40, l, productLine, flags, false, -1, cursIndent); l++; productLine.clear(); } @@ -330,1087 +447,699 @@ void PrintStoreItem(const Item &item, int l, UiFlags flags, bool cursIndent = fa if (dex != 0) productLine.append(fmt::format(fmt::runtime(_(" {:d} Dex")), dex)); } - AddSText(40, l++, productLine, flags, false, -1, cursIndent); + SetLineText(40, l++, productLine, flags, false, -1, cursIndent); } -bool StoreAutoPlace(Item &item, bool persistItem) +void SetupScreenElements(TalkID talkId) { - Player &player = *MyPlayer; + IsTextFullSize = true; + RenderGold = true; + ScrollPos = 0; - if (AutoEquipEnabled(player, item) && AutoEquip(player, item, persistItem, true)) { - return true; - } + SetLineAsDivider(BuySellMenuDividerLine); + AddItemListBackButton(talkId, /*selectable=*/true); - if (AutoPlaceItemInBelt(player, item, persistItem, true)) { - return true; - } + const UiFlags flags = UiFlags::ColorWhitegold; + const int itemCount = GetItemCount(talkId); - return AutoPlaceItemInInventory(player, item, persistItem, true); -} + switch (talkId) { + case TalkID::BasicBuy: + case TalkID::Buy: { + if (itemCount == 0) { + SetLineText(20, 1, _("I have nothing for sale."), UiFlags::ColorWhitegold, false); + return; + } -void ScrollVendorStore(Item *itemData, int storeLimit, int idx, int selling = true) -{ - ClearSText(5, 21); - PreviousScrollPos = 5; + ScrollPos = 0; + NumTextLines = std::max(itemCount - ItemLineSpace, 0); // FIXME: Why is this different?? - for (int l = 5; l < 20 && idx < storeLimit; l += 4) { - const Item &item = itemData[idx]; - if (!item.isEmpty()) { - UiFlags itemColor = item.getTextColorWithStatCheck(); - AddSText(20, l, item.getName(), itemColor, true, item._iCurs, true); - AddSTextVal(l, item._iIdentified ? item._iIvalue : item._ivalue); - PrintStoreItem(item, l + 1, itemColor, true); - NextScrollPos = l; + if (itemCount == 1) { + SetLineText(20, 1, _("I have this item for sale:"), flags, false); } else { - l -= 4; + SetLineText(20, 1, _("I have these items for sale:"), flags, false); + } + } break; + case TalkID::Sell: { + if (itemCount == 0) { + SetLineText(20, 1, _("You have nothing I want."), UiFlags::ColorWhitegold, false); + return; } - idx++; - } - if (selling) { - if (CurrentTextLine != -1 && !TextLine[CurrentTextLine].isSelectable() && CurrentTextLine != BackButtonLine()) - CurrentTextLine = NextScrollPos; - } else { - NumTextLines = std::max(static_cast(storeLimit) - 4, 0); - } -} -void StartSmith() -{ - IsTextFullSize = false; - HasScrollbar = false; - AddSText(0, 1, _("Welcome to the"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 3, _("Blacksmith's shop"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 7, _("Would you like to:"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 10, _("Talk to Griswold"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); - AddSText(0, 12, _("Buy basic items"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSText(0, 14, _("Buy premium items"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSText(0, 16, _("Sell items"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSText(0, 18, _("Repair items"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSText(0, 20, _("Leave the shop"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSLine(5); - CurrentItemIndex = 20; -} + ScrollPos = 0; + NumTextLines = itemCount; -void ScrollSmithBuy(int idx) -{ - ScrollVendorStore(SmithItems, static_cast(std::size(SmithItems)), idx); -} + SetLineText(20, 1, _("Which item is for sale?"), UiFlags::ColorWhitegold, false); + } break; + case TalkID::Repair: { + if (itemCount == 0) { + SetLineText(20, 1, _("You have nothing to repair."), UiFlags::ColorWhitegold, false); + return; + } -uint32_t TotalPlayerGold() -{ - return MyPlayer->_pGold + Stash.gold; -} + ScrollPos = 0; + NumTextLines = itemCount; + SetLineText(20, 1, _("Repair which item?"), UiFlags::ColorWhitegold, false); + } break; + case TalkID::Recharge: { + if (itemCount == 0) { + SetLineText(20, 1, _("You have nothing to recharge."), UiFlags::ColorWhitegold, false); + return; + } -// TODO: Change `_iIvalue` to be unsigned instead of passing `int` here. -bool PlayerCanAfford(int price) -{ - return TotalPlayerGold() >= static_cast(price); + ScrollPos = 0; + NumTextLines = itemCount; + SetLineText(20, 1, _("Recharge which item?"), UiFlags::ColorWhitegold, false); + } break; + case TalkID::Identify: { + if (itemCount == 0) { + SetLineText(20, 1, _("You have nothing to identify."), UiFlags::ColorWhitegold, false); + return; + } + + ScrollPos = 0; + NumTextLines = itemCount; + + SetLineText(20, 1, _("Identify which item?"), UiFlags::ColorWhitegold, false); + } break; + } } -void StartSmithBuy() +void SetupErrorScreen(TalkID talkId) { - IsTextFullSize = true; - HasScrollbar = true; - ScrollPos = 0; + SetupScreenElements(OldActiveStore); + ClearTextLines(5, 23); - RenderGold = true; - AddSText(20, 1, _("I have these items for sale:"), UiFlags::ColorWhitegold, false); - AddSLine(3); - ScrollSmithBuy(ScrollPos); - AddItemListBackButton(); - - CurrentItemIndex = 0; - for (Item &item : SmithItems) { - if (item.isEmpty()) - continue; + std::string_view text; - item._iStatFlag = MyPlayer->CanUseItem(item); - CurrentItemIndex++; + switch (talkId) { + case TalkID::NoMoney: + IsTextFullSize = true; + RenderGold = true; + text = _("You do not have enough gold"); + break; + case TalkID::NoRoom: + text = _("You do not have enough room in inventory"); + break; } - NumTextLines = std::max(CurrentItemIndex - 4, 0); + SetLineText(0, 14, text, UiFlags::ColorWhite | UiFlags::AlignCenter, true); } -void ScrollSmithPremiumBuy(int boughtitems) +void SetupConfirmScreen() { - int idx = 0; - for (; boughtitems != 0; idx++) { - if (!PremiumItems[idx].isEmpty()) - boughtitems--; + SetupScreenElements(OldActiveStore); + ClearTextLines(5, 23); + + int goldAmountDisplay; + std::string_view prompt; + + switch (OldActiveStore) { + case TalkID::BasicBuy: + case TalkID::Buy: { + goldAmountDisplay = GetItemBuyValue(TempItem); + if (TownerId == TOWN_PEGBOY) + prompt = _("Do we have a deal?"); + else + prompt = _("Are you sure you want to buy this item?"); + } break; + case TalkID::Sell: + goldAmountDisplay = GetItemSellValue(TempItem); + prompt = _("Are you sure you want to sell this item?"); + break; + case TalkID::Repair: + goldAmountDisplay = GetItemRepairCost(TempItem); + prompt = _("Are you sure you want to repair this item?"); + break; + case TalkID::Recharge: + goldAmountDisplay = GetItemRechargeCost(TempItem); + prompt = _("Are you sure you want to recharge this item?"); + break; + case TalkID::Identify: + goldAmountDisplay = GetItemIdentifyCost(); + prompt = _("Are you sure you want to identify this item?"); + break; + default: + app_fatal(StrCat("Unknown store dialog ", static_cast(OldActiveStore))); } - ScrollVendorStore(PremiumItems, static_cast(std::size(PremiumItems)), idx); + UiFlags itemColor = TempItem.getTextColorWithStatCheck(); + + SetLineText(20, 8, TempItem.getName(), itemColor, false); + SetLineValue(8, goldAmountDisplay); + PrintStoreItem(TempItem, 9, itemColor); + SetLineText(0, ConfirmLine - TripleLineSpace, prompt, UiFlags::ColorWhite | UiFlags::AlignCenter, false); + SetLineText(0, ConfirmLine, _("Yes"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); + SetLineText(0, ConfirmLine + DoubleLineSpace, _("No"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); } -bool StartSmithPremiumBuy() +void SetupGUIConfirmScreen() { - CurrentItemIndex = 0; - for (Item &item : PremiumItems) { - if (item.isEmpty()) - continue; + ClearTextLines(0, NumStoreLines); - item._iStatFlag = MyPlayer->CanUseItem(item); - CurrentItemIndex++; - } - if (CurrentItemIndex == 0) { - StartStore(TalkID::Smith); - CurrentTextLine = 14; - return false; - } + Item &item = Store.storeList[GUITempItemId]; - IsTextFullSize = true; - HasScrollbar = true; - ScrollPos = 0; + // Line 1: "Buy" + SetLineText(0, 0, _("Buy"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - RenderGold = true; - AddSText(20, 1, _("I have these premium items for sale:"), UiFlags::ColorWhitegold, false); - AddSLine(3); - AddItemListBackButton(); + // Line 2: Item name + SetLineText(0, 1, item._iIName, UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - NumTextLines = std::max(CurrentItemIndex - 4, 0); + // Line 3: Gold cost + SetLineText(0, 2, fmt::format(fmt::runtime(_("Gold: {:d}")), item._iIvalue), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - ScrollSmithPremiumBuy(ScrollPos); + // Line 4: "Yes" option (selectable) + SetLineText(0, 3, _("Yes"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, true); - return true; + // Line 5: "No" option (selectable) + SetLineText(0, 4, _("No"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, true); + + // Set cursor to "Yes" by default + CurrentTextLine = 3; } -bool SmithSellOk(int i) +void SetupGossipScreen() { - Item *pI; + int la; + TownerStore *towner = townerStores[TownerId]; - if (i >= 0) { - pI = &MyPlayer->InvList[i]; - } else { - pI = &MyPlayer->SpdList[-(i + 1)]; + IsTextFullSize = false; + + SetLineText(0, 2, fmt::format(fmt::runtime(_("Talk to {:s}")), towner->name), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); + SetLineAsDivider(5); + if (gbIsSpawn) { + SetLineText(0, 10, fmt::format(fmt::runtime(_("Talking to {:s}")), towner->name), UiFlags::ColorWhite | UiFlags::AlignCenter, false); + + SetLineText(0, 12, _("is not available"), UiFlags::ColorWhite | UiFlags::AlignCenter, false); + SetLineText(0, 14, _("in the shareware"), UiFlags::ColorWhite | UiFlags::AlignCenter, false); + SetLineText(0, 16, _("version"), UiFlags::ColorWhite | UiFlags::AlignCenter, false); + SetLineAsOptionsBackButton(); + return; } - if (pI->isEmpty()) - return false; + int sn = 0; + for (auto &quest : Quests) { + if (quest._qactive == QUEST_ACTIVE && QuestDialogTable[TownerId][quest._qidx] != TEXT_NONE && quest._qlog) + sn++; + } - if (pI->_iMiscId > IMISC_OILFIRST && pI->_iMiscId < IMISC_OILLAST) - return true; + if (sn > 6) { + sn = 14 - (sn / 2); + la = 1; + } else { + sn = 15 - sn; + la = 2; + } - if (pI->_itype == ItemType::Misc) - return false; - if (pI->_itype == ItemType::Gold) - return false; - if (pI->_itype == ItemType::Staff && (!gbIsHellfire || IsValidSpell(pI->_iSpell))) - return false; - if (pI->_iClass == ICLASS_QUEST) - return false; - if (pI->IDidx == IDI_LAZSTAFF) - return false; + int sn2 = sn - 2; - return true; + for (auto &quest : Quests) { + if (quest._qactive == QUEST_ACTIVE && QuestDialogTable[TownerId][quest._qidx] != TEXT_NONE && quest._qlog) { + SetLineText(0, sn, _(QuestsData[quest._qidx]._qlstr), UiFlags::ColorWhite | UiFlags::AlignCenter, true); + sn += la; + } + } + SetLineText(0, sn2, _("Gossip"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); + SetLineAsOptionsBackButton(); } -void ScrollSmithSell(int idx) +void SetMenuHeader(const std::string &header) { - ScrollVendorStore(PlayerItems, CurrentItemIndex, idx, false); -} + // Check if the header contains "\n\n", which indicates a two-line header + std::string::size_type pos = header.find("\n\n"); -void StartSmithSell() -{ - IsTextFullSize = true; - bool sellOk = false; - CurrentItemIndex = 0; + if (pos != std::string::npos) { + // Split the header into two parts for a two-line header + std::string header1 = header.substr(0, pos); + std::string header2 = header.substr(pos + 2); - for (auto &item : PlayerItems) { - item.clear(); + // Set the headers on lines 1 and 3 + SetLineText(0, 1, header1, UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); + SetLineText(0, 3, header2, UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); + } else { + // If there's no "\n\n", treat it as a single-line header + SetLineText(0, 2, header, UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); } +} - const Player &myPlayer = *MyPlayer; - - for (int8_t i = 0; i < myPlayer._pNumInv; i++) { - if (CurrentItemIndex >= 48) - break; - if (SmithSellOk(i)) { - sellOk = true; - PlayerItems[CurrentItemIndex] = myPlayer.InvList[i]; +void SetMenuText(const TownerLine &townerInfo) +{ + const UiFlags flags = UiFlags::ColorWhitegold | UiFlags::AlignCenter; - if (PlayerItems[CurrentItemIndex]._iMagical != ITEM_QUALITY_NORMAL && PlayerItems[CurrentItemIndex]._iIdentified) - PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._iIvalue; + int startLine = MainMenuDividerLine + SingleLineSpace; - PlayerItems[CurrentItemIndex]._ivalue = std::max(PlayerItems[CurrentItemIndex]._ivalue / 4, 1); - PlayerItems[CurrentItemIndex]._iIvalue = PlayerItems[CurrentItemIndex]._ivalue; - PlayerItemIndexes[CurrentItemIndex] = i; - CurrentItemIndex++; - } + if (TownerId != TOWN_PEGBOY) { + CurrentMenuDrawLine = townerInfo.numOptions > 5 ? startLine + SingleLineSpace : startLine + TripleLineSpace; + SetLineText(0, CurrentMenuDrawLine, _("Would you like to:"), flags, false); + CurrentMenuDrawLine += TripleLineSpace; + } else if (!Boy.items.empty()) { + CurrentMenuDrawLine = WirtDialogueDrawLine; + SetLineText(0, CurrentMenuDrawLine, _("I have something for sale,"), flags, false); + CurrentMenuDrawLine += DoubleLineSpace; + SetLineText(0, CurrentMenuDrawLine, _("but it will cost 50 gold"), flags, false); + CurrentMenuDrawLine += DoubleLineSpace; + SetLineText(0, CurrentMenuDrawLine, _("just to take a look. "), flags, false); + CurrentMenuDrawLine = WirtDialogueDrawLine - (DoubleLineSpace * 2); // Needed to draw first Wirt menu option far away enough from dialogue lines. + } else { + CurrentMenuDrawLine = startLine + (TripleLineSpace * 2); } +} - for (int i = 0; i < MaxBeltItems; i++) { - if (CurrentItemIndex >= 48) - break; - if (SmithSellOk(-(i + 1))) { - sellOk = true; - PlayerItems[CurrentItemIndex] = myPlayer.SpdList[i]; - - if (PlayerItems[CurrentItemIndex]._iMagical != ITEM_QUALITY_NORMAL && PlayerItems[CurrentItemIndex]._iIdentified) - PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._iIvalue; +void SetMenuOption(TalkID action, const std::string_view &text) +{ + UiFlags flags = (action == TalkID::Gossip) ? UiFlags::ColorBlue | UiFlags::AlignCenter : UiFlags::ColorWhite | UiFlags::AlignCenter; - PlayerItems[CurrentItemIndex]._ivalue = std::max(PlayerItems[CurrentItemIndex]._ivalue / 4, 1); - PlayerItems[CurrentItemIndex]._iIvalue = PlayerItems[CurrentItemIndex]._ivalue; - PlayerItemIndexes[CurrentItemIndex] = -(i + 1); - CurrentItemIndex++; - } + // Set leave option as the last menu option, trying for line 18 if there's room, otherwise line 20. + if (action == TalkID::Exit) { + CurrentMenuDrawLine = CurrentMenuDrawLine < 18 ? 18 : 20; } - if (!sellOk) { - HasScrollbar = false; + SetLineText(0, CurrentMenuDrawLine, text, flags, true); - RenderGold = true; - AddSText(20, 1, _("You have nothing I want."), UiFlags::ColorWhitegold, false); - AddSLine(3); - AddItemListBackButton(/*selectable=*/true); - return; - } + // Update the vector to map the current line to the action + LineActionMappings.push_back({ CurrentMenuDrawLine, action }); - HasScrollbar = true; - ScrollPos = 0; - NumTextLines = myPlayer._pNumInv; + CurrentMenuDrawLine += DoubleLineSpace; - RenderGold = true; - AddSText(20, 1, _("Which item is for sale?"), UiFlags::ColorWhitegold, false); - AddSLine(3); - ScrollSmithSell(ScrollPos); - AddItemListBackButton(); + if (TownerId == TOWN_PEGBOY && !Boy.items.empty() && CurrentMenuDrawLine == (WirtDialogueDrawLine - DoubleLineSpace)) { + CurrentMenuDrawLine = WirtDialogueDrawLine + (TripleLineSpace * 2); + } } -bool SmithRepairOk(int i) +// FIXME: Put in anonymous namespace +void RestoreResource() { - const Player &myPlayer = *MyPlayer; - const Item &item = myPlayer.InvList[i]; + int *resource = nullptr; + int *maxResource = nullptr; + int *baseResource = nullptr; + int *baseMaxResource = nullptr; + PanelDrawComponent component; + TownerStore *towner = townerStores[TownerId]; - if (item.isEmpty()) - return false; - if (item._itype == ItemType::Misc) - return false; - if (item._itype == ItemType::Gold) - return false; - if (item._iDurability == item._iMaxDur) - return false; - if (item._iMaxDur == DUR_INDESTRUCTIBLE) - return false; + switch (towner->resourceType) { + case ResourceType::Life: + resource = &MyPlayer->_pHitPoints; + maxResource = &MyPlayer->_pMaxHP; + baseResource = &MyPlayer->_pHPBase; + baseMaxResource = &MyPlayer->_pMaxHPBase; + component = PanelDrawComponent::Health; + break; + case ResourceType::Mana: + if (!*sgOptions.Gameplay.adriaRefillsMana) + return; + resource = &MyPlayer->_pMana; + maxResource = &MyPlayer->_pMaxMana; + baseResource = &MyPlayer->_pManaBase; + baseMaxResource = &MyPlayer->_pMaxManaBase; + component = PanelDrawComponent::Mana; + break; + default: + return; + } - return true; + if (*resource == *maxResource) + return; + + PlaySFX(SfxID::CastHealing); + *resource = *maxResource; + *baseResource = *baseMaxResource; + RedrawComponent(component); } -void StartSmithRepair() +void SetupMainMenuScreen() { - IsTextFullSize = true; - CurrentItemIndex = 0; + RestoreResource(); - for (auto &item : PlayerItems) { - item.clear(); - } + IsTextFullSize = false; - Player &myPlayer = *MyPlayer; + const TownerLine &lines = TownerLines[TownerId]; - auto &helmet = myPlayer.InvBody[INVLOC_HEAD]; - if (!helmet.isEmpty() && helmet._iDurability != helmet._iMaxDur) { - AddStoreHoldRepair(&helmet, -1); - } + SetMenuHeader(lines.menuHeader); + SetLineAsDivider(MainMenuDividerLine); + SetMenuText(lines); - auto &armor = myPlayer.InvBody[INVLOC_CHEST]; - if (!armor.isEmpty() && armor._iDurability != armor._iMaxDur) { - AddStoreHoldRepair(&armor, -2); - } + LineActionMappings.clear(); - auto &leftHand = myPlayer.InvBody[INVLOC_HAND_LEFT]; - if (!leftHand.isEmpty() && leftHand._iDurability != leftHand._iMaxDur) { - AddStoreHoldRepair(&leftHand, -3); + for (size_t i = 0; i < lines.numOptions; i++) { + const StoreMenuOption &option = lines.menuOptions[i]; + if (TownerId == TOWN_PEGBOY && option.action == TalkID::Buy && Boy.items.empty()) + continue; + SetMenuOption(option.action, option.text); } +} - auto &rightHand = myPlayer.InvBody[INVLOC_HAND_RIGHT]; - if (!rightHand.isEmpty() && rightHand._iDurability != rightHand._iMaxDur) { - AddStoreHoldRepair(&rightHand, -4); - } +void BuildPlayerItemsVector() +{ + playerItems.clear(); - for (int i = 0; i < myPlayer._pNumInv; i++) { - if (CurrentItemIndex >= 48) - break; - if (SmithRepairOk(i)) { - AddStoreHoldRepair(&myPlayer.InvList[i], i); - } + // Add body items + for (int8_t i = 0; i < SLOTXY_EQUIPPED_LAST; i++) { + if (MyPlayer->InvBody[i].isEmpty()) + continue; + playerItems.push_back({ &MyPlayer->InvBody[i], ItemLocation::Body, i }); } - if (CurrentItemIndex == 0) { - HasScrollbar = false; + // Add inventory items + for (int8_t i = 0; i < MyPlayer->_pNumInv; i++) { + if (MyPlayer->InvList[i].isEmpty()) + continue; + playerItems.push_back({ &MyPlayer->InvList[i], ItemLocation::Inventory, i }); + } - RenderGold = true; - AddSText(20, 1, _("You have nothing to repair."), UiFlags::ColorWhitegold, false); - AddSLine(3); - AddItemListBackButton(/*selectable=*/true); - return; + // Add belt items + for (int i = 0; i < MaxBeltItems; i++) { + if (MyPlayer->SpdList[i].isEmpty()) + continue; + playerItems.push_back({ &MyPlayer->SpdList[i], ItemLocation::Belt, i }); } +} - HasScrollbar = true; - ScrollPos = 0; - NumTextLines = myPlayer._pNumInv; +void FilterSellableItems(TalkID talkId) +{ + playerItems.erase(std::remove_if(playerItems.begin(), playerItems.end(), + [talkId](const IndexedItem &indexedItem) { + Item *pI = indexedItem.itemPtr; - RenderGold = true; - AddSText(20, 1, _("Repair which item?"), UiFlags::ColorWhitegold, false); - AddSLine(3); + // Cannot sell equipped items + if (indexedItem.location == ItemLocation::Body) + return true; // Remove this item - ScrollSmithSell(ScrollPos); - AddItemListBackButton(); -} + // Common conditions for both Smith and Witch + if (pI->_itype == ItemType::Gold || pI->_iClass == ICLASS_QUEST || pI->IDidx == IDI_LAZSTAFF) + return true; // Remove this item -void FillManaPlayer() -{ - if (!*sgOptions.Gameplay.adriaRefillsMana) - return; + switch (TownerId) { + case TOWN_SMITH: + if (pI->_iMiscId > IMISC_OILFIRST && pI->_iMiscId < IMISC_OILLAST) + return false; // Keep this item + if (pI->_itype == ItemType::Misc || (pI->_itype == ItemType::Staff && (!gbIsHellfire || IsValidSpell(pI->_iSpell)))) + return true; // Remove this item + return false; // Keep this item - Player &myPlayer = *MyPlayer; + case TOWN_WITCH: + if (pI->_itype == ItemType::Misc && (pI->_iMiscId > 29 && pI->_iMiscId < 41)) + return true; // Remove this item + if (pI->_itype == ItemType::Staff && (!gbIsHellfire || IsValidSpell(pI->_iSpell))) + return false; // Keep this item + return pI->_itype != ItemType::Misc; // Keep if it's not Misc - if (myPlayer._pMana != myPlayer._pMaxMana) { - PlaySFX(SfxID::CastHealing); - } - myPlayer._pMana = myPlayer._pMaxMana; - myPlayer._pManaBase = myPlayer._pMaxManaBase; - RedrawComponent(PanelDrawComponent::Mana); + default: + return true; // Remove this item for unsupported TalkID + } + }), + playerItems.end()); } -void StartWitch() +void FilterRepairableItems() { - FillManaPlayer(); - IsTextFullSize = false; - HasScrollbar = false; - AddSText(0, 2, _("Witch's shack"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 9, _("Would you like to:"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 12, _("Talk to Adria"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); - AddSText(0, 14, _("Buy items"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSText(0, 16, _("Sell items"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSText(0, 18, _("Recharge staves"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSText(0, 20, _("Leave the shack"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSLine(5); - CurrentItemIndex = 20; + // Filter playerItems in place to only include items that can be repaired + playerItems.erase(std::remove_if(playerItems.begin(), playerItems.end(), + [](const IndexedItem &indexedItem) { + const Item &itemPtr = *indexedItem.itemPtr; + return itemPtr._iDurability == itemPtr._iMaxDur || itemPtr._iMaxDur == DUR_INDESTRUCTIBLE; + }), + playerItems.end()); } -void ScrollWitchBuy(int idx) +void FilterRechargeableItems() { - ScrollVendorStore(WitchItems, static_cast(std::size(WitchItems)), idx); + // Filter playerItems to include only items that can be recharged + playerItems.erase(std::remove_if(playerItems.begin(), playerItems.end(), + [](const IndexedItem &indexedItem) { + const Item &itemPtr = *indexedItem.itemPtr; + return itemPtr._iCharges == itemPtr._iMaxCharges || (itemPtr._itype != ItemType::Staff && itemPtr._iMiscId != IMISC_UNIQUE && itemPtr._iMiscId != IMISC_STAFF); + }), + playerItems.end()); } -void WitchBookLevel(Item &bookItem) +void FilterIdentifiableItems() { - if (bookItem._iMiscId != IMISC_BOOK) - return; - bookItem._iMinMag = GetSpellData(bookItem._iSpell).minInt; - uint8_t spellLevel = MyPlayer->_pSplLvl[static_cast(bookItem._iSpell)]; - while (spellLevel > 0) { - bookItem._iMinMag += 20 * bookItem._iMinMag / 100; - spellLevel--; - if (bookItem._iMinMag + 20 * bookItem._iMinMag / 100 > 255) { - bookItem._iMinMag = 255; - spellLevel = 0; - } - } + // Filter playerItems to include only items that can be identified + playerItems.erase(std::remove_if(playerItems.begin(), playerItems.end(), + [](const IndexedItem &indexedItem) { + const Item &itemPtr = *indexedItem.itemPtr; + return itemPtr._iMagical == ITEM_QUALITY_NORMAL || itemPtr._iIdentified; + }), + playerItems.end()); } -void StartWitchBuy() +void FilterPlayerItemsForAction(TalkID talkId) { - IsTextFullSize = true; - HasScrollbar = true; - ScrollPos = 0; - NumTextLines = 20; + BuildPlayerItemsVector(); - RenderGold = true; - AddSText(20, 1, _("I have these items for sale:"), UiFlags::ColorWhitegold, false); - AddSLine(3); - ScrollWitchBuy(ScrollPos); - AddItemListBackButton(); - - CurrentItemIndex = 0; - for (Item &item : WitchItems) { - if (item.isEmpty()) - continue; - - WitchBookLevel(item); - item._iStatFlag = MyPlayer->CanUseItem(item); - CurrentItemIndex++; + switch (talkId) { + case TalkID::Sell: + // Filter items for selling + FilterSellableItems(talkId); + break; + case TalkID::Repair: + // Filter items for repairing + FilterRepairableItems(); + break; + case TalkID::Recharge: + // Filter items for recharging + FilterRechargeableItems(); + break; + case TalkID::Identify: + // Filter items for identifying + FilterIdentifiableItems(); + break; } - NumTextLines = std::max(CurrentItemIndex - 4, 0); } -bool WitchSellOk(int i) +void SetupTownerItemList(TalkID talkId, std::vector &items, int idx, bool selling /*= true*/) { - Item *pI; + ClearTextLines(5, 21); + PreviousScrollPos = 5; - bool rv = false; + int startLine = (TownerId == TOWN_PEGBOY) ? 10 : 5; + for (int l = startLine; l < 20 && idx < items.size(); l += 4) { + const Item &item = items[idx]; + int price = GetItemBuyValue(item); + UiFlags itemColor = item.getTextColorWithStatCheck(); - if (i >= 0) - pI = &MyPlayer->InvList[i]; - else - pI = &MyPlayer->SpdList[-(i + 1)]; + SetLineText(20, l, item.getName(), itemColor, true, item._iCurs, true); + SetLineValue(l, price); + PrintStoreItem(item, l + 1, itemColor, true); + NextScrollPos = l; + idx++; + } - if (pI->_itype == ItemType::Misc) - rv = true; - if (pI->_iMiscId > 29 && pI->_iMiscId < 41) - rv = false; - if (pI->_iClass == ICLASS_QUEST) - rv = false; - if (pI->_itype == ItemType::Staff && (!gbIsHellfire || IsValidSpell(pI->_iSpell))) - rv = true; - if (pI->IDidx >= IDI_FIRSTQUEST && pI->IDidx <= IDI_LASTQUEST) - rv = false; - if (pI->IDidx == IDI_LAZSTAFF) - rv = false; - return rv; -} - -void StartWitchSell() -{ - IsTextFullSize = true; - bool sellok = false; - CurrentItemIndex = 0; - - for (auto &item : PlayerItems) { - item.clear(); - } - - const Player &myPlayer = *MyPlayer; - - for (int i = 0; i < myPlayer._pNumInv; i++) { - if (CurrentItemIndex >= 48) - break; - if (WitchSellOk(i)) { - sellok = true; - PlayerItems[CurrentItemIndex] = myPlayer.InvList[i]; - - if (PlayerItems[CurrentItemIndex]._iMagical != ITEM_QUALITY_NORMAL && PlayerItems[CurrentItemIndex]._iIdentified) - PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._iIvalue; - - PlayerItems[CurrentItemIndex]._ivalue = std::max(PlayerItems[CurrentItemIndex]._ivalue / 4, 1); - PlayerItems[CurrentItemIndex]._iIvalue = PlayerItems[CurrentItemIndex]._ivalue; - PlayerItemIndexes[CurrentItemIndex] = i; - CurrentItemIndex++; - } - } - - for (int i = 0; i < MaxBeltItems; i++) { - if (CurrentItemIndex >= 48) - break; - if (!myPlayer.SpdList[i].isEmpty() && WitchSellOk(-(i + 1))) { - sellok = true; - PlayerItems[CurrentItemIndex] = myPlayer.SpdList[i]; - - if (PlayerItems[CurrentItemIndex]._iMagical != ITEM_QUALITY_NORMAL && PlayerItems[CurrentItemIndex]._iIdentified) - PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._iIvalue; - - PlayerItems[CurrentItemIndex]._ivalue = std::max(PlayerItems[CurrentItemIndex]._ivalue / 4, 1); - PlayerItems[CurrentItemIndex]._iIvalue = PlayerItems[CurrentItemIndex]._ivalue; - PlayerItemIndexes[CurrentItemIndex] = -(i + 1); - CurrentItemIndex++; - } - } - - if (!sellok) { - HasScrollbar = false; - - RenderGold = true; - AddSText(20, 1, _("You have nothing I want."), UiFlags::ColorWhitegold, false); - - AddSLine(3); - AddItemListBackButton(/*selectable=*/true); - return; - } - - HasScrollbar = true; - ScrollPos = 0; - NumTextLines = myPlayer._pNumInv; - - RenderGold = true; - AddSText(20, 1, _("Which item is for sale?"), UiFlags::ColorWhitegold, false); - AddSLine(3); - ScrollSmithSell(ScrollPos); - AddItemListBackButton(); -} - -bool WitchRechargeOk(int i) -{ - const auto &item = MyPlayer->InvList[i]; - - if (item._itype == ItemType::Staff && item._iCharges != item._iMaxCharges) { - return true; - } - - if ((item._iMiscId == IMISC_UNIQUE || item._iMiscId == IMISC_STAFF) && item._iCharges < item._iMaxCharges) { - return true; + if (selling) { + if (CurrentTextLine != -1 && !TextLine[CurrentTextLine].isSelectable() && CurrentTextLine != BackButtonLine()) + CurrentTextLine = NextScrollPos; + } else { + NumTextLines = std::max(static_cast(items.size()) - ItemLineSpace, 0); } - - return false; -} - -void AddStoreHoldRecharge(Item itm, int8_t i) -{ - PlayerItems[CurrentItemIndex] = itm; - PlayerItems[CurrentItemIndex]._ivalue += GetSpellData(itm._iSpell).staffCost(); - PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._ivalue * (PlayerItems[CurrentItemIndex]._iMaxCharges - PlayerItems[CurrentItemIndex]._iCharges) / (PlayerItems[CurrentItemIndex]._iMaxCharges * 2); - PlayerItems[CurrentItemIndex]._iIvalue = PlayerItems[CurrentItemIndex]._ivalue; - PlayerItemIndexes[CurrentItemIndex] = i; - CurrentItemIndex++; } -void StartWitchRecharge() +void SetupPlayerItemList(TalkID talkId, std::vector &items, int idx, bool selling /*= true*/) { - IsTextFullSize = true; - bool rechargeok = false; - CurrentItemIndex = 0; - - for (auto &item : PlayerItems) { - item.clear(); - } - - const Player &myPlayer = *MyPlayer; - const auto &leftHand = myPlayer.InvBody[INVLOC_HAND_LEFT]; + ClearTextLines(5, 21); + PreviousScrollPos = 5; - if ((leftHand._itype == ItemType::Staff || leftHand._iMiscId == IMISC_UNIQUE) && leftHand._iCharges != leftHand._iMaxCharges) { - rechargeok = true; - AddStoreHoldRecharge(leftHand, -1); - } + int goldAmountDisplay; - for (int i = 0; i < myPlayer._pNumInv; i++) { - if (CurrentItemIndex >= 48) + for (int l = 5; l < 20 && idx < items.size(); l += 4) { + const Item &item = *items[idx].itemPtr; + UiFlags itemColor = item.getTextColorWithStatCheck(); + SetLineText(20, l, item.getName(), itemColor, true, item._iCurs, true); + switch (talkId) { + case TalkID::Sell: + goldAmountDisplay = GetItemSellValue(item); + break; + case TalkID::Repair: + goldAmountDisplay = GetItemRepairCost(item); + break; + case TalkID::Recharge: + goldAmountDisplay = GetItemRechargeCost(item); + break; + case TalkID::Identify: + goldAmountDisplay = GetItemIdentifyCost(); break; - if (WitchRechargeOk(i)) { - rechargeok = true; - AddStoreHoldRecharge(myPlayer.InvList[i], i); } + SetLineValue(l, goldAmountDisplay); + PrintStoreItem(item, l + 1, itemColor, true); + NextScrollPos = l; + idx++; } - if (!rechargeok) { - HasScrollbar = false; - - RenderGold = true; - AddSText(20, 1, _("You have nothing to recharge."), UiFlags::ColorWhitegold, false); - AddSLine(3); - AddItemListBackButton(/*selectable=*/true); - return; + if (selling) { + if (CurrentTextLine != -1 && !TextLine[CurrentTextLine].isSelectable() && CurrentTextLine != BackButtonLine()) + CurrentTextLine = NextScrollPos; + } else { + NumTextLines = std::max(static_cast(items.size()) - ItemLineSpace, 0); } - - HasScrollbar = true; - ScrollPos = 0; - NumTextLines = myPlayer._pNumInv; - - RenderGold = true; - AddSText(20, 1, _("Recharge which item?"), UiFlags::ColorWhitegold, false); - AddSLine(3); - ScrollSmithSell(ScrollPos); - AddItemListBackButton(); -} - -void StoreNoMoney() -{ - StartStore(OldActiveStore); - HasScrollbar = false; - IsTextFullSize = true; - RenderGold = true; - ClearSText(5, 23); - AddSText(0, 14, _("You do not have enough gold"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); } -void StoreNoRoom() +void SetupItemList(TalkID talkId) { - StartStore(OldActiveStore); - HasScrollbar = false; - ClearSText(5, 23); - AddSText(0, 14, _("You do not have enough room in inventory"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); -} + TownerStore *towner = townerStores[TownerId]; -void StoreConfirm(Item &item) -{ - StartStore(OldActiveStore); - HasScrollbar = false; - ClearSText(5, 23); - - UiFlags itemColor = item.getTextColorWithStatCheck(); - AddSText(20, 8, item.getName(), itemColor, false); - AddSTextVal(8, item._iIvalue); - PrintStoreItem(item, 9, itemColor); - - std::string_view prompt; - - switch (OldActiveStore) { - case TalkID::BoyBuy: - prompt = _("Do we have a deal?"); - break; - case TalkID::StorytellerIdentify: - prompt = _("Are you sure you want to identify this item?"); - break; - case TalkID::HealerBuy: - case TalkID::SmithPremiumBuy: - case TalkID::WitchBuy: - case TalkID::SmithBuy: - prompt = _("Are you sure you want to buy this item?"); - break; - case TalkID::WitchRecharge: - prompt = _("Are you sure you want to recharge this item?"); + switch (talkId) { + case TalkID::BasicBuy: + SetupTownerItemList(talkId, towner->basicItems, ScrollPos, true); break; - case TalkID::SmithSell: - case TalkID::WitchSell: - prompt = _("Are you sure you want to sell this item?"); + case TalkID::Buy: + SetupTownerItemList(talkId, towner->items, ScrollPos, true); break; - case TalkID::SmithRepair: - prompt = _("Are you sure you want to repair this item?"); + case TalkID::Sell: + case TalkID::Repair: + case TalkID::Recharge: + case TalkID::Identify: + SetupPlayerItemList(talkId, playerItems, ScrollPos, false); break; - default: - app_fatal(StrCat("Unknown store dialog ", static_cast(OldActiveStore))); - } - AddSText(0, 15, prompt, UiFlags::ColorWhite | UiFlags::AlignCenter, false); - AddSText(0, 18, _("Yes"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSText(0, 20, _("No"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); -} - -void StartBoy() -{ - IsTextFullSize = false; - HasScrollbar = false; - AddSText(0, 2, _("Wirt the Peg-legged boy"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSLine(5); - if (!BoyItem.isEmpty()) { - AddSText(0, 8, _("Talk to Wirt"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); - AddSText(0, 12, _("I have something for sale,"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 14, _("but it will cost 50 gold"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 16, _("just to take a look. "), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 18, _("What have you got?"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSText(0, 20, _("Say goodbye"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - } else { - AddSText(0, 12, _("Talk to Wirt"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); - AddSText(0, 18, _("Say goodbye"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); } } -void SStartBoyBuy() +void UpdateBookMinMagic(Item &bookItem) { - IsTextFullSize = true; - HasScrollbar = false; - - RenderGold = true; - AddSText(20, 1, _("I have this item for sale:"), UiFlags::ColorWhitegold, false); - AddSLine(3); - - BoyItem._iStatFlag = MyPlayer->CanUseItem(BoyItem); - UiFlags itemColor = BoyItem.getTextColorWithStatCheck(); - AddSText(20, 10, BoyItem.getName(), itemColor, true, BoyItem._iCurs, true); - if (gbIsHellfire) - AddSTextVal(10, BoyItem._iIvalue - (BoyItem._iIvalue / 4)); - else - AddSTextVal(10, BoyItem._iIvalue + (BoyItem._iIvalue / 2)); - PrintStoreItem(BoyItem, 11, itemColor, true); - - { - // Add a Leave button. Unlike the other item list back buttons, - // this one has different text and different layout in LargerSmallFont locales. - const int line = BackButtonLine(); - AddSLine(line - 1); - AddSText(0, line, _("Leave"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - TextLine[line]._syoff = 6; - } -} - -void HealPlayer() -{ - Player &myPlayer = *MyPlayer; - - if (myPlayer._pHitPoints != myPlayer._pMaxHP) { - PlaySFX(SfxID::CastHealing); + if (bookItem._iMiscId != IMISC_BOOK) + return; + bookItem._iMinMag = GetSpellData(bookItem._iSpell).minInt; + uint8_t spellLevel = MyPlayer->_pSplLvl[static_cast(bookItem._iSpell)]; + while (spellLevel > 0) { + bookItem._iMinMag += 20 * bookItem._iMinMag / 100; + spellLevel--; + if (bookItem._iMinMag + 20 * bookItem._iMinMag / 100 > 255) { + bookItem._iMinMag = 255; + spellLevel = 0; + } } - myPlayer._pHitPoints = myPlayer._pMaxHP; - myPlayer._pHPBase = myPlayer._pMaxHPBase; - RedrawComponent(PanelDrawComponent::Health); } -void StartHealer() +// FIXME: Move to anonymous namespace +static void UpdateItemStatFlag(Item &item) { - HealPlayer(); - IsTextFullSize = false; - HasScrollbar = false; - AddSText(0, 1, _("Welcome to the"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 3, _("Healer's home"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 9, _("Would you like to:"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 12, _("Talk to Pepin"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); - AddSText(0, 14, _("Buy items"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSText(0, 18, _("Leave Healer's home"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSLine(5); - CurrentItemIndex = 20; + item._iStatFlag = MyPlayer->CanUseItem(item); } -void ScrollHealerBuy(int idx) +void UpdateItemStatFlags(TalkID talkId) { - ScrollVendorStore(HealerItems, static_cast(std::size(HealerItems)), idx); -} + TownerStore *towner = townerStores[TownerId]; -void StartHealerBuy() -{ - IsTextFullSize = true; - HasScrollbar = true; - ScrollPos = 0; - - RenderGold = true; - AddSText(20, 1, _("I have these items for sale:"), UiFlags::ColorWhitegold, false); - AddSLine(3); - - ScrollHealerBuy(ScrollPos); - AddItemListBackButton(); - - CurrentItemIndex = 0; - for (Item &item : HealerItems) { - if (item.isEmpty()) - continue; - - item._iStatFlag = MyPlayer->CanUseItem(item); - CurrentItemIndex++; + switch (talkId) { + case TalkID::BasicBuy: + for (Item &item : towner->basicItems) + UpdateItemStatFlag(item); + break; + case TalkID::Buy: + for (Item &item : towner->items) + UpdateItemStatFlag(item); + break; } - - NumTextLines = std::max(CurrentItemIndex - 4, 0); } -void StartStoryteller() +void SetupIdentifyResultScreen() { - IsTextFullSize = false; - HasScrollbar = false; - AddSText(0, 2, _("The Town Elder"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 9, _("Would you like to:"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 12, _("Talk to Cain"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); - AddSText(0, 14, _("Identify an item"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSText(0, 18, _("Say goodbye"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSLine(5); -} + SetupScreenElements(OldActiveStore); + ClearTextLines(5, 23); -bool IdItemOk(Item *i) -{ - if (i->isEmpty()) { - return false; - } - if (i->_iMagical == ITEM_QUALITY_NORMAL) { - return false; - } - return !i->_iIdentified; -} + UiFlags itemColor = TempItem.getTextColorWithStatCheck(); -void AddStoreHoldId(Item itm, int8_t i) -{ - PlayerItems[CurrentItemIndex] = itm; - PlayerItems[CurrentItemIndex]._ivalue = 100; - PlayerItems[CurrentItemIndex]._iIvalue = 100; - PlayerItemIndexes[CurrentItemIndex] = i; - CurrentItemIndex++; + SetLineText(0, 7, _("This item is:"), UiFlags::ColorWhite | UiFlags::AlignCenter, false); + SetLineText(20, 11, TempItem.getName(), itemColor, false); + PrintStoreItem(TempItem, 12, itemColor); + SetLineText(0, 18, _("Done"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); } -void StartStorytellerIdentify() +int GetLineForAction(TalkID action) { - bool idok = false; - IsTextFullSize = true; - CurrentItemIndex = 0; - - for (auto &item : PlayerItems) { - item.clear(); - } - - Player &myPlayer = *MyPlayer; - - auto &helmet = myPlayer.InvBody[INVLOC_HEAD]; - if (IdItemOk(&helmet)) { - idok = true; - AddStoreHoldId(helmet, -1); - } - - auto &armor = myPlayer.InvBody[INVLOC_CHEST]; - if (IdItemOk(&armor)) { - idok = true; - AddStoreHoldId(armor, -2); - } - - auto &leftHand = myPlayer.InvBody[INVLOC_HAND_LEFT]; - if (IdItemOk(&leftHand)) { - idok = true; - AddStoreHoldId(leftHand, -3); - } - - auto &rightHand = myPlayer.InvBody[INVLOC_HAND_RIGHT]; - if (IdItemOk(&rightHand)) { - idok = true; - AddStoreHoldId(rightHand, -4); - } - - auto &leftRing = myPlayer.InvBody[INVLOC_RING_LEFT]; - if (IdItemOk(&leftRing)) { - idok = true; - AddStoreHoldId(leftRing, -5); - } - - auto &rightRing = myPlayer.InvBody[INVLOC_RING_RIGHT]; - if (IdItemOk(&rightRing)) { - idok = true; - AddStoreHoldId(rightRing, -6); - } - - auto &amulet = myPlayer.InvBody[INVLOC_AMULET]; - if (IdItemOk(&amulet)) { - idok = true; - AddStoreHoldId(amulet, -7); - } - - for (int i = 0; i < myPlayer._pNumInv; i++) { - if (CurrentItemIndex >= 48) - break; - auto &item = myPlayer.InvList[i]; - if (IdItemOk(&item)) { - idok = true; - AddStoreHoldId(item, i); - } - } - - if (!idok) { - HasScrollbar = false; - - RenderGold = true; - AddSText(20, 1, _("You have nothing to identify."), UiFlags::ColorWhitegold, false); - AddSLine(3); - AddItemListBackButton(/*selectable=*/true); - return; - } - - HasScrollbar = true; - ScrollPos = 0; - NumTextLines = myPlayer._pNumInv; - - RenderGold = true; - AddSText(20, 1, _("Identify which item?"), UiFlags::ColorWhitegold, false); - AddSLine(3); - - ScrollSmithSell(ScrollPos); - AddItemListBackButton(); + auto it = std::find_if(LineActionMappings.begin(), LineActionMappings.end(), + [action](const std::pair &pair) { + return pair.second == action; + }); + return (it != LineActionMappings.end()) ? it->first : -1; } -void StartStorytellerIdentifyShow(Item &item) +TalkID GetActionForLine(int line) { - StartStore(OldActiveStore); - HasScrollbar = false; - ClearSText(5, 23); - - UiFlags itemColor = item.getTextColorWithStatCheck(); - - AddSText(0, 7, _("This item is:"), UiFlags::ColorWhite | UiFlags::AlignCenter, false); - AddSText(20, 11, item.getName(), itemColor, false); - PrintStoreItem(item, 12, itemColor); - AddSText(0, 18, _("Done"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); + auto it = std::find_if(LineActionMappings.begin(), LineActionMappings.end(), + [line](const std::pair &pair) { + return pair.first == line; + }); + return (it != LineActionMappings.end()) ? it->second : TalkID::Invalid; } -void StartTalk() +void MainMenuEnter() { - int la; + TalkID selectedAction = GetActionForLine(CurrentTextLine); + TownerStore *towner = townerStores[TownerId]; - IsTextFullSize = false; - HasScrollbar = false; - AddSText(0, 2, fmt::format(fmt::runtime(_("Talk to {:s}")), _(TownerNames[TownerId])), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSLine(5); - if (gbIsSpawn) { - AddSText(0, 10, fmt::format(fmt::runtime(_("Talking to {:s}")), _(TownerNames[TownerId])), UiFlags::ColorWhite | UiFlags::AlignCenter, false); - AddSText(0, 12, _("is not available"), UiFlags::ColorWhite | UiFlags::AlignCenter, false); - AddSText(0, 14, _("in the shareware"), UiFlags::ColorWhite | UiFlags::AlignCenter, false); - AddSText(0, 16, _("version"), UiFlags::ColorWhite | UiFlags::AlignCenter, false); - AddOptionsBackButton(); + switch (selectedAction) { + case TalkID::Exit: + ExitStore(); return; - } - - int sn = 0; - for (auto &quest : Quests) { - if (quest._qactive == QUEST_ACTIVE && QuestDialogTable[TownerId][quest._qidx] != TEXT_NONE && quest._qlog) - sn++; - } - - if (sn > 6) { - sn = 14 - (sn / 2); - la = 1; - } else { - sn = 15 - sn; - la = 2; - } - - int sn2 = sn - 2; - - for (auto &quest : Quests) { - if (quest._qactive == QUEST_ACTIVE && QuestDialogTable[TownerId][quest._qidx] != TEXT_NONE && quest._qlog) { - AddSText(0, sn, _(QuestsData[quest._qidx]._qlstr), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - sn += la; + case TalkID::Gossip: + OldTextLine = CurrentTextLine; + break; + case TalkID::Buy: + if (TownerId == TOWN_PEGBOY) { + if (!CanPlayerAfford(50)) { + // OldActiveStore is TalkID::Buy at this point, and we need to override and set "most recent" store to the main menu + OldActiveStore = TalkID::MainMenu; + selectedAction = TalkID::NoMoney; + } else { + TakePlrsMoney(50); + } } + break; + case TalkID::Stash: + ExitStore(); + IsStashOpen = true; + Stash.RefreshItemStatFlags(); + invflag = true; + if (ControlMode != ControlTypes::KeyboardAndMouse) { + if (pcurs == CURSOR_DISARM) + NewCursor(CURSOR_HAND); + FocusOnInventory(); + } + return; } - AddSText(0, sn2, _("Gossip"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); - AddOptionsBackButton(); -} -void StartTavern() -{ - IsTextFullSize = false; - HasScrollbar = false; - AddSText(0, 1, _("Welcome to the"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 3, _("Rising Sun"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 9, _("Would you like to:"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 12, _("Talk to Ogden"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); - AddSText(0, 18, _("Leave the tavern"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSLine(5); - CurrentItemIndex = 20; + StartStore(selectedAction); } -void StartBarmaid() +int GetItemIndex() { - IsTextFullSize = false; - HasScrollbar = false; - AddSText(0, 2, _("Gillian"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 9, _("Would you like to:"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 12, _("Talk to Gillian"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); - AddSText(0, 14, _("Access Storage"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSText(0, 18, _("Say goodbye"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSLine(5); - CurrentItemIndex = 20; + return OldScrollPos + ((OldTextLine - PreviousScrollPos) / ItemLineSpace); } -void StartDrunk() +bool ReturnToMainMenu() { - IsTextFullSize = false; - HasScrollbar = false; - AddSText(0, 2, _("Farnham the Drunk"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 9, _("Would you like to:"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); - AddSText(0, 12, _("Talk to Farnham"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); - AddSText(0, 18, _("Say Goodbye"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); - AddSLine(5); - CurrentItemIndex = 20; -} - -void SmithEnter() -{ - switch (CurrentTextLine) { - case 10: - TownerId = TOWN_SMITH; - OldTextLine = 10; - OldActiveStore = TalkID::Smith; - StartStore(TalkID::Gossip); - break; - case 12: - StartStore(TalkID::SmithBuy); - break; - case 14: - StartStore(TalkID::SmithPremiumBuy); - break; - case 16: - StartStore(TalkID::SmithSell); - break; - case 18: - StartStore(TalkID::SmithRepair); - break; - case 20: - ActiveStore = TalkID::None; - break; + if (CurrentTextLine == BackButtonLine()) { + StartStore(TalkID::MainMenu); + return true; } -} -/** - * @brief Purchases an item from the smith. - */ -void SmithBuyItem(Item &item) -{ - TakePlrsMoney(item._iIvalue); - if (item._iMagical == ITEM_QUALITY_NORMAL) - item._iIdentified = false; - StoreAutoPlace(item, true); - int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); - if (idx == SMITH_ITEMS - 1) { - SmithItems[SMITH_ITEMS - 1].clear(); - } else { - for (; !SmithItems[idx + 1].isEmpty(); idx++) { - SmithItems[idx] = std::move(SmithItems[idx + 1]); - } - SmithItems[idx].clear(); - } - CalcPlrInv(*MyPlayer, true); + return false; } -void SmithBuyEnter() +void BuyEnter() { - if (CurrentTextLine == BackButtonLine()) { - StartStore(TalkID::Smith); - CurrentTextLine = 12; + if (ReturnToMainMenu()) return; - } OldTextLine = CurrentTextLine; OldScrollPos = ScrollPos; - OldActiveStore = TalkID::SmithBuy; - - int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); - if (!PlayerCanAfford(SmithItems[idx]._iIvalue)) { - StartStore(TalkID::NoMoney); - return; - } - if (!StoreAutoPlace(SmithItems[idx], false)) { - StartStore(TalkID::NoRoom); - return; - } + int idx = GetItemIndex(); - TempItem = SmithItems[idx]; - StartStore(TalkID::Confirm); -} + // Boy displays his item in the 2nd slot instead of the 1st, so we need to adjust the index + if (TownerId == TOWN_PEGBOY) + idx--; -/** - * @brief Purchases a premium item from the smith. - */ -void SmithBuyPItem(Item &item) -{ - TakePlrsMoney(item._iIvalue); - if (item._iMagical == ITEM_QUALITY_NORMAL) - item._iIdentified = false; - StoreAutoPlace(item, true); - - int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); - int xx = 0; - for (int i = 0; idx >= 0; i++) { - if (!PremiumItems[i].isEmpty()) { - idx--; - xx = i; - } - } + TownerStore *towner = townerStores[TownerId]; + Item &item = (ActiveStore == TalkID::BasicBuy) ? towner->basicItems[idx] : towner->items[idx]; + int cost = GetItemBuyValue(item); - PremiumItems[xx].clear(); - PremiumItemCount--; - SpawnPremium(*MyPlayer); -} - -void SmithPremiumBuyEnter() -{ - if (CurrentTextLine == BackButtonLine()) { - StartStore(TalkID::Smith); - CurrentTextLine = 14; - return; - } - - OldActiveStore = TalkID::SmithPremiumBuy; - OldTextLine = CurrentTextLine; - OldScrollPos = ScrollPos; - - int xx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); - int idx = 0; - for (int i = 0; xx >= 0; i++) { - if (!PremiumItems[i].isEmpty()) { - xx--; - idx = i; - } - } - - if (!PlayerCanAfford(PremiumItems[idx]._iIvalue)) { + if (!CanPlayerAfford(cost)) { StartStore(TalkID::NoMoney); - return; - } - - if (!StoreAutoPlace(PremiumItems[idx], false)) { + } else if (!GiveItemToPlayer(item, false)) { StartStore(TalkID::NoRoom); - return; + } else { + TempItem = item; + StartStore(TalkID::Confirm); } - - TempItem = PremiumItems[idx]; - StartStore(TalkID::Confirm); } bool StoreGoldFit(Item &item) @@ -1430,422 +1159,252 @@ bool StoreGoldFit(Item &item) /** * @brief Sells an item from the player's inventory or belt. */ -void StoreSellItem() -{ - Player &myPlayer = *MyPlayer; - - int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); - if (PlayerItemIndexes[idx] >= 0) - myPlayer.RemoveInvItem(PlayerItemIndexes[idx]); - else - myPlayer.RemoveSpdBarItem(-(PlayerItemIndexes[idx] + 1)); - - int cost = PlayerItems[idx]._iIvalue; - CurrentItemIndex--; - if (idx != CurrentItemIndex) { - while (idx < CurrentItemIndex) { - PlayerItems[idx] = PlayerItems[idx + 1]; - PlayerItemIndexes[idx] = PlayerItemIndexes[idx + 1]; - idx++; - } - } - - AddGoldToInventory(myPlayer, cost); - - myPlayer._pGold += cost; -} - -void SmithSellEnter() +void SellItem() { - if (CurrentTextLine == BackButtonLine()) { - StartStore(TalkID::Smith); - CurrentTextLine = 16; - return; - } - - OldTextLine = CurrentTextLine; - OldActiveStore = TalkID::SmithSell; - OldScrollPos = ScrollPos; + int idx = GetItemIndex(); - int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); + IndexedItem &itemToSell = playerItems[idx]; - if (!StoreGoldFit(PlayerItems[idx])) { - StartStore(TalkID::NoRoom); - return; + // Remove the sold item from the player's inventory or belt + if (itemToSell.location == ItemLocation::Inventory) { + MyPlayer->RemoveInvItem(itemToSell.index); + } else if (itemToSell.location == ItemLocation::Belt) { + MyPlayer->RemoveSpdBarItem(itemToSell.index); } - TempItem = PlayerItems[idx]; - StartStore(TalkID::Confirm); -} + int price = GetItemSellValue(*itemToSell.itemPtr); -/** - * @brief Repairs an item in the player's inventory or body in the smith. - */ -void SmithRepairItem(int price) -{ - int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); - PlayerItems[idx]._iDurability = PlayerItems[idx]._iMaxDur; - - int8_t i = PlayerItemIndexes[idx]; - - Player &myPlayer = *MyPlayer; - - if (i < 0) { - if (i == -1) - myPlayer.InvBody[INVLOC_HEAD]._iDurability = myPlayer.InvBody[INVLOC_HEAD]._iMaxDur; - if (i == -2) - myPlayer.InvBody[INVLOC_CHEST]._iDurability = myPlayer.InvBody[INVLOC_CHEST]._iMaxDur; - if (i == -3) - myPlayer.InvBody[INVLOC_HAND_LEFT]._iDurability = myPlayer.InvBody[INVLOC_HAND_LEFT]._iMaxDur; - if (i == -4) - myPlayer.InvBody[INVLOC_HAND_RIGHT]._iDurability = myPlayer.InvBody[INVLOC_HAND_RIGHT]._iMaxDur; - TakePlrsMoney(price); - return; - } + // Remove the sold item from the playerItems vector + playerItems.erase(playerItems.begin() + idx); - myPlayer.InvList[i]._iDurability = myPlayer.InvList[i]._iMaxDur; - TakePlrsMoney(price); + // Add the gold to the player's inventory + AddGoldToInventory(*MyPlayer, price); + MyPlayer->_pGold += price; } -void SmithRepairEnter() +void SellEnter() { - if (CurrentTextLine == BackButtonLine()) { - StartStore(TalkID::Smith); - CurrentTextLine = 18; + if (ReturnToMainMenu()) return; - } - OldActiveStore = TalkID::SmithRepair; OldTextLine = CurrentTextLine; OldScrollPos = ScrollPos; - int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); + int idx = GetItemIndex(); - if (!PlayerCanAfford(PlayerItems[idx]._iIvalue)) { - StartStore(TalkID::NoMoney); + // Check if there's enough room for the gold that will be earned from selling the item + if (!StoreGoldFit(*playerItems[idx].itemPtr)) { + StartStore(TalkID::NoRoom); return; } - TempItem = PlayerItems[idx]; - StartStore(TalkID::Confirm); -} + // Store the item to be sold temporarily + // FIXME: Clean up call chain flow, so we no longer need TempItem global + TempItem = *playerItems[idx].itemPtr; -void WitchEnter() -{ - switch (CurrentTextLine) { - case 12: - OldTextLine = 12; - TownerId = TOWN_WITCH; - OldActiveStore = TalkID::Witch; - StartStore(TalkID::Gossip); - break; - case 14: - StartStore(TalkID::WitchBuy); - break; - case 16: - StartStore(TalkID::WitchSell); - break; - case 18: - StartStore(TalkID::WitchRecharge); - break; - case 20: - ActiveStore = TalkID::None; - break; - } + // Proceed to the confirmation store screen + StartStore(TalkID::Confirm); } /** - * @brief Purchases an item from the witch. + * @brief Repairs an item in the player's inventory or body in the smith. */ -void WitchBuyItem(Item &item) +void RepairItem() { - int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); + int idx = GetItemIndex(); - if (idx < 3) - item._iSeed = AdvanceRndSeed(); + // Get a reference to the IndexedItem at the calculated index + IndexedItem &indexedItem = playerItems[idx]; - TakePlrsMoney(item._iIvalue); - StoreAutoPlace(item, true); + // Repair the item by setting its durability to the maximum + indexedItem.itemPtr->_iDurability = indexedItem.itemPtr->_iMaxDur; - if (idx >= 3) { - if (idx == WITCH_ITEMS - 1) { - WitchItems[WITCH_ITEMS - 1].clear(); - } else { - for (; !WitchItems[idx + 1].isEmpty(); idx++) { - WitchItems[idx] = std::move(WitchItems[idx + 1]); - } - WitchItems[idx].clear(); - } - } + // Deduct the repair cost from the player's money + TakePlrsMoney(GetItemRepairCost(*indexedItem.itemPtr)); + // Update the player's inventory CalcPlrInv(*MyPlayer, true); } -void WitchBuyEnter() +void RepairEnter() { - if (CurrentTextLine == BackButtonLine()) { - StartStore(TalkID::Witch); - CurrentTextLine = 14; + if (ReturnToMainMenu()) return; - } OldTextLine = CurrentTextLine; OldScrollPos = ScrollPos; - OldActiveStore = TalkID::WitchBuy; - int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); + int idx = GetItemIndex(); - if (!PlayerCanAfford(WitchItems[idx]._iIvalue)) { + // Check if the player can afford the repair cost + if (!CanPlayerAfford(GetItemRepairCost(*playerItems[idx].itemPtr))) { StartStore(TalkID::NoMoney); return; } - if (!StoreAutoPlace(WitchItems[idx], false)) { - StartStore(TalkID::NoRoom); - return; - } - - TempItem = WitchItems[idx]; - StartStore(TalkID::Confirm); -} - -void WitchSellEnter() -{ - if (CurrentTextLine == BackButtonLine()) { - StartStore(TalkID::Witch); - CurrentTextLine = 16; - return; - } - - OldTextLine = CurrentTextLine; - OldActiveStore = TalkID::WitchSell; - OldScrollPos = ScrollPos; - - int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); + // Temporarily store the item being repaired + TempItem = *playerItems[idx].itemPtr; - if (!StoreGoldFit(PlayerItems[idx])) { - StartStore(TalkID::NoRoom); - return; - } - - TempItem = PlayerItems[idx]; + // Proceed to the confirmation screen StartStore(TalkID::Confirm); } /** - * @brief Recharges an item in the player's inventory or body in the witch. + * @brief Purchases an item. */ -void WitchRechargeItem(int price) +void BuyItem(Item &item) { - int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); - PlayerItems[idx]._iCharges = PlayerItems[idx]._iMaxCharges; + // Get the index of the purchased item + int idx = GetItemIndex(); - Player &myPlayer = *MyPlayer; + // Boy displays his item in the 2nd slot instead of the 1st, so we need to adjust the index + if (TownerId == TOWN_PEGBOY) + idx--; - int8_t i = PlayerItemIndexes[idx]; - if (i < 0) { - myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges = myPlayer.InvBody[INVLOC_HAND_LEFT]._iMaxCharges; - NetSendCmdChItem(true, INVLOC_HAND_LEFT); - } else { - myPlayer.InvList[i]._iCharges = myPlayer.InvList[i]._iMaxCharges; - NetSyncInvItem(myPlayer, i); - } - - TakePlrsMoney(price); - CalcPlrInv(myPlayer, true); -} + int numPinnedItems = 0; -void WitchRechargeEnter() -{ - if (CurrentTextLine == BackButtonLine()) { - StartStore(TalkID::Witch); - CurrentTextLine = 18; - return; + switch (TownerId) { + case TOWN_HEALER: + numPinnedItems = !gbIsMultiplayer ? NumHealerPinnedItems : NumHealerPinnedItemsMp; + break; + case TOWN_WITCH: + numPinnedItems = NumWitchPinnedItems; + break; } - OldActiveStore = TalkID::WitchRecharge; - OldTextLine = CurrentTextLine; - OldScrollPos = ScrollPos; + // If the item is one of the pinned items, generate a new seed for it + if (idx < numPinnedItems) { + item._iSeed = AdvanceRndSeed(); + } - int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); + // Non-magical items are unidentified + if (item._iMagical == ITEM_QUALITY_NORMAL) + item._iIdentified = false; - if (!PlayerCanAfford(PlayerItems[idx]._iIvalue)) { - StartStore(TalkID::NoMoney); - return; - } + // Deduct the player's gold and give the item to the player + TakePlrsMoney(item._iIvalue); + GiveItemToPlayer(item, true); - TempItem = PlayerItems[idx]; - StartStore(TalkID::Confirm); -} + TownerStore *towner = townerStores[TownerId]; -void BoyEnter() -{ - if (!BoyItem.isEmpty() && CurrentTextLine == 18) { - if (!PlayerCanAfford(50)) { - OldActiveStore = TalkID::Boy; - OldTextLine = 18; - OldScrollPos = ScrollPos; - StartStore(TalkID::NoMoney); + // If the purchased item is not a pinned item, remove it from the store + if (idx >= numPinnedItems) { + if (OldActiveStore == TalkID::BasicBuy) { + towner->basicItems.erase(towner->basicItems.begin() + idx); } else { - TakePlrsMoney(50); - StartStore(TalkID::BoyBuy); + towner->items.erase(towner->items.begin() + idx); } - return; } - if ((CurrentTextLine != 8 && !BoyItem.isEmpty()) || (CurrentTextLine != 12 && BoyItem.isEmpty())) { - ActiveStore = TalkID::None; - return; + // Blacksmith replaces the item with a new one + if (TownerId == TOWN_SMITH) { + SpawnPremium(*MyPlayer); } - TownerId = TOWN_PEGBOY; - OldActiveStore = TalkID::Boy; - OldTextLine = CurrentTextLine; - StartStore(TalkID::Gossip); -} + // Boy returns to main menu instead of item list + if (TownerId == TOWN_PEGBOY) { + OldActiveStore = TalkID::MainMenu; + OldTextLine = CurrentTextLine; // FIXME: May need to adjust this! + } -void BoyBuyItem(Item &item) -{ - TakePlrsMoney(item._iIvalue); - StoreAutoPlace(item, true); - BoyItem.clear(); - OldActiveStore = TalkID::Boy; + // Recalculate the player's inventory CalcPlrInv(*MyPlayer, true); - OldTextLine = 12; } /** - * @brief Purchases an item from the healer. + * @brief Recharges an item in the player's inventory or body in the witch. */ -void HealerBuyItem(Item &item) +void RechargeItem() { - int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); - if (!gbIsMultiplayer) { - if (idx < 2) - item._iSeed = AdvanceRndSeed(); - } else { - if (idx < 3) - item._iSeed = AdvanceRndSeed(); - } - - TakePlrsMoney(item._iIvalue); - if (item._iMagical == ITEM_QUALITY_NORMAL) - item._iIdentified = false; - StoreAutoPlace(item, true); + int idx = GetItemIndex(); - if (!gbIsMultiplayer) { - if (idx < 2) - return; - } else { - if (idx < 3) - return; - } - idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); - if (idx == 19) { - HealerItems[19].clear(); + // Get a reference to the IndexedItem at the calculated index + IndexedItem &indexedItem = playerItems[idx]; + + // Recharge the item by setting its charges to the maximum + indexedItem.itemPtr->_iCharges = indexedItem.itemPtr->_iMaxCharges; + + // Send network commands for synchronization + if (indexedItem.location == ItemLocation::Body) { + NetSendCmdChItem(true, indexedItem.index); } else { - for (; !HealerItems[idx + 1].isEmpty(); idx++) { - HealerItems[idx] = std::move(HealerItems[idx + 1]); - } - HealerItems[idx].clear(); + NetSyncInvItem(*MyPlayer, indexedItem.index); } + + // Deduct the recharge cost from the player's money + TakePlrsMoney(GetItemRechargeCost(*indexedItem.itemPtr)); + + // Recalculate and update the player's inventory CalcPlrInv(*MyPlayer, true); } -void BoyBuyEnter() +void RechargeEnter() { - if (CurrentTextLine != 10) { - ActiveStore = TalkID::None; + if (ReturnToMainMenu()) { return; } - OldActiveStore = TalkID::BoyBuy; + OldTextLine = CurrentTextLine; OldScrollPos = ScrollPos; - OldTextLine = 10; - int price = BoyItem._iIvalue; - if (gbIsHellfire) - price -= BoyItem._iIvalue / 4; - else - price += BoyItem._iIvalue / 2; - if (!PlayerCanAfford(price)) { - StartStore(TalkID::NoMoney); - return; - } + int idx = GetItemIndex(); - if (!StoreAutoPlace(BoyItem, false)) { - StartStore(TalkID::NoRoom); + // Check if the player can afford the recharge cost + if (!CanPlayerAfford(GetItemRechargeCost(*playerItems[idx].itemPtr))) { + StartStore(TalkID::NoMoney); return; } - TempItem = BoyItem; - TempItem._iIvalue = price; + // Store the item temporarily for the confirmation screen + TempItem = *playerItems[idx].itemPtr; StartStore(TalkID::Confirm); } -void StorytellerIdentifyItem(Item &item) -{ - Player &myPlayer = *MyPlayer; - - int8_t idx = PlayerItemIndexes[((OldTextLine - PreviousScrollPos) / 4) + OldScrollPos]; - if (idx < 0) { - if (idx == -1) - myPlayer.InvBody[INVLOC_HEAD]._iIdentified = true; - if (idx == -2) - myPlayer.InvBody[INVLOC_CHEST]._iIdentified = true; - if (idx == -3) - myPlayer.InvBody[INVLOC_HAND_LEFT]._iIdentified = true; - if (idx == -4) - myPlayer.InvBody[INVLOC_HAND_RIGHT]._iIdentified = true; - if (idx == -5) - myPlayer.InvBody[INVLOC_RING_LEFT]._iIdentified = true; - if (idx == -6) - myPlayer.InvBody[INVLOC_RING_RIGHT]._iIdentified = true; - if (idx == -7) - myPlayer.InvBody[INVLOC_AMULET]._iIdentified = true; - } else { - myPlayer.InvList[idx]._iIdentified = true; - } - item._iIdentified = true; - TakePlrsMoney(item._iIvalue); - CalcPlrInv(myPlayer, true); +/** + * @brief Identifies an item in the player's inventory or body. + */ +void IdentifyItem() +{ + int idx = GetItemIndex(); + + // Get a reference to the IndexedItem at the calculated index + IndexedItem &indexedItem = playerItems[idx]; + + // Mark the item as identified + indexedItem.itemPtr->_iIdentified = true; + + // Deduct the identification cost from the player's money + TakePlrsMoney(GetItemIdentifyCost()); + + // Update the player's inventory + CalcPlrInv(*MyPlayer, true); } void ConfirmEnter(Item &item) { - if (CurrentTextLine == 18) { + if (CurrentTextLine == (GUIConfirmFlag ? GUIConfirmLine : ConfirmLine)) { switch (OldActiveStore) { - case TalkID::SmithBuy: - SmithBuyItem(item); - break; - case TalkID::SmithSell: - case TalkID::WitchSell: - StoreSellItem(); - break; - case TalkID::SmithRepair: - SmithRepairItem(item._iIvalue); - break; - case TalkID::WitchBuy: - WitchBuyItem(item); + case TalkID::BasicBuy: + case TalkID::Buy: + if (IsStoreOpen) { + Store.BuyItem(); + } else { + BuyItem(item); + } break; - case TalkID::WitchRecharge: - WitchRechargeItem(item._iIvalue); + case TalkID::Sell: + SellItem(); break; - case TalkID::BoyBuy: - BoyBuyItem(item); + case TalkID::Repair: + RepairItem(); break; - case TalkID::HealerBuy: - HealerBuyItem(item); + case TalkID::Recharge: + RechargeItem(); break; - case TalkID::StorytellerIdentify: - StorytellerIdentifyItem(item); - StartStore(TalkID::StorytellerIdentifyShow); + case TalkID::Identify: + IdentifyItem(); + StartStore(TalkID::IdentifyShow); return; - case TalkID::SmithPremiumBuy: - SmithBuyPItem(item); - break; - default: - break; } } @@ -1862,90 +1421,25 @@ void ConfirmEnter(Item &item) } } -void HealerEnter() -{ - switch (CurrentTextLine) { - case 12: - OldTextLine = 12; - TownerId = TOWN_HEALER; - OldActiveStore = TalkID::Healer; - StartStore(TalkID::Gossip); - break; - case 14: - StartStore(TalkID::HealerBuy); - break; - case 18: - ActiveStore = TalkID::None; - break; - } -} - -void HealerBuyEnter() -{ - if (CurrentTextLine == BackButtonLine()) { - StartStore(TalkID::Healer); - CurrentTextLine = 14; - return; - } - - OldTextLine = CurrentTextLine; - OldScrollPos = ScrollPos; - OldActiveStore = TalkID::HealerBuy; - - int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); - - if (!PlayerCanAfford(HealerItems[idx]._iIvalue)) { - StartStore(TalkID::NoMoney); - return; - } - - if (!StoreAutoPlace(HealerItems[idx], false)) { - StartStore(TalkID::NoRoom); - return; - } - - TempItem = HealerItems[idx]; - StartStore(TalkID::Confirm); -} - -void StorytellerEnter() -{ - switch (CurrentTextLine) { - case 12: - OldTextLine = 12; - TownerId = TOWN_STORY; - OldActiveStore = TalkID::Storyteller; - StartStore(TalkID::Gossip); - break; - case 14: - StartStore(TalkID::StorytellerIdentify); - break; - case 18: - ActiveStore = TalkID::None; - break; - } -} - -void StorytellerIdentifyEnter() +void IdentifyEnter() { - if (CurrentTextLine == BackButtonLine()) { - StartStore(TalkID::Storyteller); - CurrentTextLine = 14; + if (ReturnToMainMenu()) { return; } - OldActiveStore = TalkID::StorytellerIdentify; OldTextLine = CurrentTextLine; OldScrollPos = ScrollPos; - int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); + int idx = GetItemIndex(); - if (!PlayerCanAfford(PlayerItems[idx]._iIvalue)) { + // Check if the player can afford the identification cost + if (!CanPlayerAfford(GetItemIdentifyCost())) { StartStore(TalkID::NoMoney); return; } - TempItem = PlayerItems[idx]; + // Store the item temporarily for the confirmation screen + TempItem = *playerItems[idx].itemPtr; StartStore(TalkID::Confirm); } @@ -1987,62 +1481,6 @@ void TalkEnter() } } -void TavernEnter() -{ - switch (CurrentTextLine) { - case 12: - OldTextLine = 12; - TownerId = TOWN_TAVERN; - OldActiveStore = TalkID::Tavern; - StartStore(TalkID::Gossip); - break; - case 18: - ActiveStore = TalkID::None; - break; - } -} - -void BarmaidEnter() -{ - switch (CurrentTextLine) { - case 12: - OldTextLine = 12; - TownerId = TOWN_BMAID; - OldActiveStore = TalkID::Barmaid; - StartStore(TalkID::Gossip); - break; - case 14: - ActiveStore = TalkID::None; - IsStashOpen = true; - Stash.RefreshItemStatFlags(); - invflag = true; - if (ControlMode != ControlTypes::KeyboardAndMouse) { - if (pcurs == CURSOR_DISARM) - NewCursor(CURSOR_HAND); - FocusOnInventory(); - } - break; - case 18: - ActiveStore = TalkID::None; - break; - } -} - -void DrunkEnter() -{ - switch (CurrentTextLine) { - case 12: - OldTextLine = 12; - TownerId = TOWN_DRUNK; - OldActiveStore = TalkID::Drunk; - StartStore(TalkID::Gossip); - break; - case 18: - ActiveStore = TalkID::None; - break; - } -} - int TakeGold(Player &player, int cost, bool skipMaxPiles) { for (int i = 0; i < player._pNumInv; i++) { @@ -2083,54 +1521,25 @@ void DrawSelector(const Surface &out, const Rectangle &rect, std::string_view te } // namespace -void AddStoreHoldRepair(Item *itm, int8_t i) -{ - Item *item; - int v; - - item = &PlayerItems[CurrentItemIndex]; - PlayerItems[CurrentItemIndex] = *itm; - - int due = item->_iMaxDur - item->_iDurability; - if (item->_iMagical != ITEM_QUALITY_NORMAL && item->_iIdentified) { - v = 30 * item->_iIvalue * due / (item->_iMaxDur * 100 * 2); - if (v == 0) - return; - } else { - v = item->_ivalue * due / (item->_iMaxDur * 2); - v = std::max(v, 1); - } - item->_iIvalue = v; - item->_ivalue = v; - PlayerItemIndexes[CurrentItemIndex] = i; - CurrentItemIndex++; -} - void InitStores() { - ClearSText(0, STORE_LINES); - ActiveStore = TalkID::None; + int numSmithItems = gbIsHellfire ? NumSmithItemsHf : NumSmithItems; + ClearTextLines(0, NumStoreLines); + ExitStore(); IsTextFullSize = false; - HasScrollbar = false; - PremiumItemCount = 0; - PremiumItemLevel = 1; + Blacksmith.itemLevel = 1; + Boy.itemLevel = 0; - for (auto &premiumitem : PremiumItems) - premiumitem.clear(); - - BoyItem.clear(); - BoyItemLevel = 0; + InitializeTownerStores(); } void SetupTownStores() { - Player &myPlayer = *MyPlayer; - - int l = myPlayer.getCharacterLevel() / 2; + int l = MyPlayer->getCharacterLevel() / 2; if (!gbIsMultiplayer) { l = 0; for (int i = 0; i < NUMLEVELS; i++) { - if (myPlayer._pLvlVisited[i]) + if (MyPlayer->_pLvlVisited[i]) l = i; } } @@ -2139,8 +1548,8 @@ void SetupTownStores() SpawnSmith(l); SpawnWitch(l); SpawnHealer(l); - SpawnBoy(myPlayer.getCharacterLevel()); - SpawnPremium(myPlayer); + SpawnBoy(MyPlayer->getCharacterLevel()); + SpawnPremium(*MyPlayer); } void FreeStoreMem() @@ -2148,13 +1557,18 @@ void FreeStoreMem() if (*sgOptions.Gameplay.showItemGraphicsInStores) { FreeHalfSizeItemSprites(); } - ActiveStore = TalkID::None; + ExitStore(); for (STextStruct &entry : TextLine) { entry.text.clear(); entry.text.shrink_to_fit(); } } +void ExitStore() +{ + SetActiveStore(TalkID::Exit); +} + void PrintSString(const Surface &out, int margin, int line, std::string_view text, UiFlags flags, int price, int cursId, bool cursIndent) { const Point uiPosition = GetUIRectangle().position; @@ -2166,7 +1580,7 @@ void PrintSString(const Surface &out, int margin, int line, std::string_view tex const int sy = uiPosition.y + PaddingTop + TextLine[line].y + TextLine[line]._syoff; int width = IsTextFullSize ? 575 : 255; - if (HasScrollbar && line >= 4 && line <= 20) { + if (HasScrollbar() && line >= 4 && line <= 20) { width -= 9; // Space for the selector } width -= margin * 2; @@ -2234,9 +1648,9 @@ void DrawSTextHelp() IsTextFullSize = true; } -void ClearSText(int s, int e) +void ClearTextLines(int start, int end) { - for (int i = s; i < e; i++) { + for (int i = start; i < end; i++) { TextLine[i]._sx = 0; TextLine[i]._syoff = 0; TextLine[i].text.clear(); @@ -2247,156 +1661,99 @@ void ClearSText(int s, int e) } } -void StartStore(TalkID s) +void StartStore(TalkID store /*= TalkID::MainMenu*/) { + SetActiveStore(store); if (*sgOptions.Gameplay.showItemGraphicsInStores) { CreateHalfSizeItemSprites(); } SpellbookFlag = false; - CloseInventory(); + if (!IsStoreOpen) + CloseInventory(); CloseCharPanel(); RenderGold = false; QuestLogIsOpen = false; CloseGoldDrop(); - ClearSText(0, STORE_LINES); - ReleaseStoreBtn(); - switch (s) { - case TalkID::Smith: - StartSmith(); + ClearTextLines(0, NumStoreLines); + ReleaseStoreButton(); + + switch (store) { + case TalkID::MainMenu: + SetupMainMenuScreen(); break; - case TalkID::SmithBuy: { - bool hasAnyItems = false; - for (int i = 0; !SmithItems[i].isEmpty(); i++) { - hasAnyItems = true; - break; - } - if (hasAnyItems) - StartSmithBuy(); - else { - ActiveStore = TalkID::SmithBuy; - OldTextLine = 12; - StoreESC(); - return; + case TalkID::Gossip: + SetupGossipScreen(); + break; + case TalkID::BasicBuy: + case TalkID::Buy: + if (*sgOptions.Gameplay.useGUIStores) { + IsStoreOpen = true; + PopulateStoreGrid(); + invflag = true; + if (ControlMode != ControlTypes::KeyboardAndMouse) { + if (pcurs == CURSOR_DISARM) + NewCursor(CURSOR_HAND); + FocusOnInventory(); + } + } else { + SetupScreenElements(store); + SetupItemList(store); + UpdateItemStatFlags(store); } break; - } - case TalkID::SmithSell: - StartSmithSell(); - break; - case TalkID::SmithRepair: - StartSmithRepair(); - break; - case TalkID::Witch: - StartWitch(); + case TalkID::Sell: + case TalkID::Repair: + case TalkID::Recharge: + case TalkID::Identify: + SetupScreenElements(store); + FilterPlayerItemsForAction(store); + SetupItemList(store); break; - case TalkID::WitchBuy: - if (CurrentItemIndex > 0) - StartWitchBuy(); - break; - case TalkID::WitchSell: - StartWitchSell(); - break; - case TalkID::WitchRecharge: - StartWitchRecharge(); + case TalkID::IdentifyShow: + SetupIdentifyResultScreen(); break; case TalkID::NoMoney: - StoreNoMoney(); - break; case TalkID::NoRoom: - StoreNoRoom(); + SetupErrorScreen(store); break; case TalkID::Confirm: - StoreConfirm(TempItem); - break; - case TalkID::Boy: - StartBoy(); - break; - case TalkID::BoyBuy: - SStartBoyBuy(); - break; - case TalkID::Healer: - StartHealer(); - break; - case TalkID::Storyteller: - StartStoryteller(); - break; - case TalkID::HealerBuy: - if (CurrentItemIndex > 0) - StartHealerBuy(); - break; - case TalkID::StorytellerIdentify: - StartStorytellerIdentify(); - break; - case TalkID::SmithPremiumBuy: - if (!StartSmithPremiumBuy()) - return; - break; - case TalkID::Gossip: - StartTalk(); - break; - case TalkID::StorytellerIdentifyShow: - StartStorytellerIdentifyShow(TempItem); - break; - case TalkID::Tavern: - StartTavern(); - break; - case TalkID::Drunk: - StartDrunk(); - break; - case TalkID::Barmaid: - StartBarmaid(); + if (IsStoreOpen) + SetupGUIConfirmScreen(); + else + SetupConfirmScreen(); break; - case TalkID::None: + case TalkID::Exit: break; } CurrentTextLine = -1; - for (int i = 0; i < STORE_LINES; i++) { - if (TextLine[i].isSelectable()) { - CurrentTextLine = i; - break; + + if (store == TalkID::MainMenu && IsNoneOf(OldActiveStore, TalkID::Exit, TalkID::Invalid)) { + CurrentTextLine = GetLineForAction(OldActiveStore); + } else { // Set currently selected line to the first selectable line + for (int i = 0; i < NumStoreLines; i++) { + if (TextLine[i].isSelectable()) { + CurrentTextLine = i; + break; + } } } - - ActiveStore = s; } -void DrawSText(const Surface &out) +void DrawStore(const Surface &out) { if (!IsTextFullSize) - DrawSTextBack(out); + DrawTextUI(out); else DrawQTextBack(out); - if (HasScrollbar) { - switch (ActiveStore) { - case TalkID::SmithBuy: - ScrollSmithBuy(ScrollPos); - break; - case TalkID::SmithSell: - case TalkID::SmithRepair: - case TalkID::WitchSell: - case TalkID::WitchRecharge: - case TalkID::StorytellerIdentify: - ScrollSmithSell(ScrollPos); - break; - case TalkID::WitchBuy: - ScrollWitchBuy(ScrollPos); - break; - case TalkID::HealerBuy: - ScrollHealerBuy(ScrollPos); - break; - case TalkID::SmithPremiumBuy: - ScrollSmithPremiumBuy(ScrollPos); - break; - default: - break; - } + if (GetItemCount(ActiveStore) > 0) { + SetupItemList(ActiveStore); // FIXME: Can't figure out why this needs to be done here, yet in other places? } CalculateLineHeights(); const Point uiPosition = GetUIRectangle().position; - for (int i = 0; i < STORE_LINES; i++) { + for (int i = 0; i < NumStoreLines; i++) { if (TextLine[i].isDivider()) DrawSLine(out, uiPosition.y + PaddingTop + TextLine[i].y + TextHeight() / 2); else if (TextLine[i].hasText()) @@ -2404,11 +1761,52 @@ void DrawSText(const Surface &out) } if (RenderGold) { - PrintSString(out, 28, 1, fmt::format(fmt::runtime(_("Your gold: {:s}")), FormatInteger(TotalPlayerGold())).c_str(), UiFlags::ColorWhitegold | UiFlags::AlignRight); + PrintSString(out, 28, 1, fmt::format(fmt::runtime(_("Your gold: {:s}")), FormatInteger(GetTotalPlayerGold())).c_str(), UiFlags::ColorWhitegold | UiFlags::AlignRight); + } + + if (HasScrollbar()) + DrawScrollbar(out, 4, 20); +} + +void PrintGUIConfirmString(const Surface &out, const Point &position, int line, std::string_view text, UiFlags flags) +{ + // Calculate the vertical position for each line + const int yOffset = line * TextHeight(); // Adjust line height as needed for each line + + const Rectangle rect { position + Displacement { 0, yOffset }, { 180, 20 } }; // Adjust width and height based on your text + + // Draw the text string in the confirmation box + DrawString(out, text, rect, { .flags = flags }); + + // If the current line is selected, draw the selector around it + if (CurrentTextLine == line) { + DrawSelector(out, rect, text, flags); } +} + +void DrawGUIConfirm(const Surface &out) +{ + Item &item = Store.storeList[GUITempItemId]; + constexpr Displacement offset { 0, INV_SLOT_SIZE_PX - 1 }; + const Point position = GetStoreSlotCoord(item.position) + offset; + + // Define the size and position of the transparent black box + constexpr int boxWidth = 220; + int boxHeight = 20 + TextHeight() * 5; + const Rectangle boxRect { position, { boxWidth, boxHeight } }; + + // Draw the half-transparent rectangle (black box) + DrawHalfTransparentRectTo(out, boxRect.position.x, boxRect.position.y, boxWidth, boxHeight); + DrawHalfTransparentRectTo(out, boxRect.position.x, boxRect.position.y, boxWidth, boxHeight); - if (HasScrollbar) - DrawSSlider(out, 4, 20); + // Draw each line of the prompt using the new PrintGUIConfirmString function + const Point textStartPos = position + Displacement { 10, 10 }; + + PrintGUIConfirmString(out, textStartPos, 0, _("Buy"), UiFlags::ColorWhitegold | UiFlags::AlignCenter); + PrintGUIConfirmString(out, textStartPos, 1, item._iIName, UiFlags::ColorWhitegold | UiFlags::AlignCenter); + PrintGUIConfirmString(out, textStartPos, 2, fmt::format(fmt::runtime(_("Gold: {:d}")), item._iIvalue), UiFlags::ColorWhitegold | UiFlags::AlignCenter); + PrintGUIConfirmString(out, textStartPos, 3, _("Yes"), UiFlags::ColorWhite | UiFlags::AlignCenter); + PrintGUIConfirmString(out, textStartPos, 4, _("No"), UiFlags::ColorWhite | UiFlags::AlignCenter); } void StoreESC() @@ -2421,69 +1819,31 @@ void StoreESC() } switch (ActiveStore) { - case TalkID::Smith: - case TalkID::Witch: - case TalkID::Boy: - case TalkID::BoyBuy: - case TalkID::Healer: - case TalkID::Storyteller: - case TalkID::Tavern: - case TalkID::Drunk: - case TalkID::Barmaid: - ActiveStore = TalkID::None; - break; + case TalkID::MainMenu: + ExitStore(); + return; case TalkID::Gossip: - StartStore(OldActiveStore); + case TalkID::BasicBuy: + case TalkID::Buy: + case TalkID::Sell: + case TalkID::Repair: + case TalkID::Recharge: + case TalkID::Identify: + StartStore(TalkID::MainMenu); CurrentTextLine = OldTextLine; - break; - case TalkID::SmithBuy: - StartStore(TalkID::Smith); - CurrentTextLine = 12; - break; - case TalkID::SmithPremiumBuy: - StartStore(TalkID::Smith); - CurrentTextLine = 14; - break; - case TalkID::SmithSell: - StartStore(TalkID::Smith); - CurrentTextLine = 16; - break; - case TalkID::SmithRepair: - StartStore(TalkID::Smith); - CurrentTextLine = 18; - break; - case TalkID::WitchBuy: - StartStore(TalkID::Witch); - CurrentTextLine = 14; - break; - case TalkID::WitchSell: - StartStore(TalkID::Witch); - CurrentTextLine = 16; - break; - case TalkID::WitchRecharge: - StartStore(TalkID::Witch); - CurrentTextLine = 18; - break; - case TalkID::HealerBuy: - StartStore(TalkID::Healer); - CurrentTextLine = 14; - break; - case TalkID::StorytellerIdentify: - StartStore(TalkID::Storyteller); - CurrentTextLine = 14; - break; - case TalkID::StorytellerIdentifyShow: - StartStore(TalkID::StorytellerIdentify); - break; + return; + case TalkID::IdentifyShow: + StartStore(TalkID::Identify); + return; case TalkID::NoMoney: case TalkID::NoRoom: case TalkID::Confirm: StartStore(OldActiveStore); CurrentTextLine = OldTextLine; ScrollPos = OldScrollPos; - break; - case TalkID::None: - break; + return; + case TalkID::Exit: // FIXME: This should never happen!!! Right?? + return; } } @@ -2494,7 +1854,7 @@ void StoreUp() return; } - if (HasScrollbar) { + if (HasScrollbar()) { if (CurrentTextLine == PreviousScrollPos) { if (ScrollPos != 0) ScrollPos--; @@ -2504,7 +1864,7 @@ void StoreUp() CurrentTextLine--; while (!TextLine[CurrentTextLine].isSelectable()) { if (CurrentTextLine == 0) - CurrentTextLine = STORE_LINES - 1; + CurrentTextLine = NumStoreLines - 1; else CurrentTextLine--; } @@ -2512,13 +1872,13 @@ void StoreUp() } if (CurrentTextLine == 0) - CurrentTextLine = STORE_LINES - 1; + CurrentTextLine = NumStoreLines - 1; else CurrentTextLine--; while (!TextLine[CurrentTextLine].isSelectable()) { if (CurrentTextLine == 0) - CurrentTextLine = STORE_LINES - 1; + CurrentTextLine = NumStoreLines - 1; else CurrentTextLine--; } @@ -2531,7 +1891,7 @@ void StoreDown() return; } - if (HasScrollbar) { + if (HasScrollbar()) { if (CurrentTextLine == NextScrollPos) { if (ScrollPos < NumTextLines) ScrollPos++; @@ -2540,7 +1900,7 @@ void StoreDown() CurrentTextLine++; while (!TextLine[CurrentTextLine].isSelectable()) { - if (CurrentTextLine == STORE_LINES - 1) + if (CurrentTextLine == NumStoreLines - 1) CurrentTextLine = 0; else CurrentTextLine++; @@ -2548,13 +1908,13 @@ void StoreDown() return; } - if (CurrentTextLine == STORE_LINES - 1) + if (CurrentTextLine == NumStoreLines - 1) CurrentTextLine = 0; else CurrentTextLine++; while (!TextLine[CurrentTextLine].isSelectable()) { - if (CurrentTextLine == STORE_LINES - 1) + if (CurrentTextLine == NumStoreLines - 1) CurrentTextLine = 0; else CurrentTextLine++; @@ -2564,7 +1924,7 @@ void StoreDown() void StorePrior() { PlaySFX(SfxID::MenuMove); - if (CurrentTextLine != -1 && HasScrollbar) { + if (CurrentTextLine != -1 && HasScrollbar()) { if (CurrentTextLine == PreviousScrollPos) { ScrollPos = std::max(ScrollPos - 4, 0); } else { @@ -2576,7 +1936,7 @@ void StorePrior() void StoreNext() { PlaySFX(SfxID::MenuMove); - if (CurrentTextLine != -1 && HasScrollbar) { + if (CurrentTextLine != -1 && HasScrollbar()) { if (CurrentTextLine == NextScrollPos) { if (ScrollPos < NumTextLines) ScrollPos += 4; @@ -2590,13 +1950,11 @@ void StoreNext() void TakePlrsMoney(int cost) { - Player &myPlayer = *MyPlayer; - - myPlayer._pGold -= std::min(cost, myPlayer._pGold); + MyPlayer->_pGold -= std::min(cost, MyPlayer->_pGold); - cost = TakeGold(myPlayer, cost, true); + cost = TakeGold(*MyPlayer, cost, true); if (cost != 0) { - cost = TakeGold(myPlayer, cost, false); + cost = TakeGold(*MyPlayer, cost, false); } Stash.gold -= cost; @@ -2614,33 +1972,23 @@ void StoreEnter() } PlaySFX(SfxID::MenuSelect); + switch (ActiveStore) { - case TalkID::Smith: - SmithEnter(); - break; - case TalkID::SmithPremiumBuy: - SmithPremiumBuyEnter(); + case TalkID::MainMenu: + MainMenuEnter(); break; - case TalkID::SmithBuy: - SmithBuyEnter(); + case TalkID::BasicBuy: + case TalkID::Buy: + BuyEnter(); break; - case TalkID::SmithSell: - SmithSellEnter(); + case TalkID::Sell: + SellEnter(); break; - case TalkID::SmithRepair: - SmithRepairEnter(); + case TalkID::Repair: + RepairEnter(); break; - case TalkID::Witch: - WitchEnter(); - break; - case TalkID::WitchBuy: - WitchBuyEnter(); - break; - case TalkID::WitchSell: - WitchSellEnter(); - break; - case TalkID::WitchRecharge: - WitchRechargeEnter(); + case TalkID::Recharge: + RechargeEnter(); break; case TalkID::NoMoney: case TalkID::NoRoom: @@ -2651,45 +1999,21 @@ void StoreEnter() case TalkID::Confirm: ConfirmEnter(TempItem); break; - case TalkID::Boy: - BoyEnter(); - break; - case TalkID::BoyBuy: - BoyBuyEnter(); - break; - case TalkID::Healer: - HealerEnter(); - break; - case TalkID::Storyteller: - StorytellerEnter(); - break; - case TalkID::HealerBuy: - HealerBuyEnter(); - break; - case TalkID::StorytellerIdentify: - StorytellerIdentifyEnter(); + case TalkID::Identify: + IdentifyEnter(); break; case TalkID::Gossip: TalkEnter(); break; - case TalkID::StorytellerIdentifyShow: - StartStore(TalkID::StorytellerIdentify); + case TalkID::IdentifyShow: + StartStore(TalkID::Identify); break; - case TalkID::Drunk: - DrunkEnter(); - break; - case TalkID::Tavern: - TavernEnter(); - break; - case TalkID::Barmaid: - BarmaidEnter(); - break; - case TalkID::None: + case TalkID::Exit: // FIXME: Do we even need this? break; } } -void CheckStoreBtn() +void CheckStoreButton() { const Point uiPosition = GetUIRectangle().position; const Rectangle windowRect { { uiPosition.x + 344, uiPosition.y + PaddingTop - 7 }, { 271, 303 } }; @@ -2697,12 +2021,12 @@ void CheckStoreBtn() if (!IsTextFullSize) { if (!windowRect.contains(MousePosition)) { - while (ActiveStore != TalkID::None) + while (ActiveStore != TalkID::Exit) StoreESC(); } } else { if (!windowRectFull.contains(MousePosition)) { - while (ActiveStore != TalkID::None) + while (ActiveStore != TalkID::Exit) StoreESC(); } } @@ -2714,7 +2038,7 @@ void CheckStoreBtn() } else if (CurrentTextLine != -1) { const int relativeY = MousePosition.y - (uiPosition.y + PaddingTop); - if (HasScrollbar && MousePosition.x > 600 + uiPosition.x) { + if (HasScrollbar() && MousePosition.x > 600 + uiPosition.x) { // Scroll bar is always measured in terms of the small line height. int y = relativeY / SmallLineHeight; if (y == 4) { @@ -2739,7 +2063,7 @@ void CheckStoreBtn() int y = relativeY / LineHeight(); // Large small fonts draw beyond LineHeight. Check if the click was on the overflow text. - if (IsSmallFontTall() && y > 0 && y < STORE_LINES + if (IsSmallFontTall() && y > 0 && y < NumStoreLines && TextLine[y - 1].hasText() && !TextLine[y].hasText() && relativeY < TextLine[y - 1].y + LargeTextHeight) { --y; @@ -2748,14 +2072,14 @@ void CheckStoreBtn() if (y >= 5) { if (y >= BackButtonLine() + 1) y = BackButtonLine(); - if (HasScrollbar && y <= 20 && !TextLine[y].isSelectable()) { + if (GetItemCount(ActiveStore) > 0 && y <= 20 && !TextLine[y].isSelectable()) { if (TextLine[y - 2].isSelectable()) { y -= 2; } else if (TextLine[y - 1].isSelectable()) { y--; } } - if (TextLine[y].isSelectable() || (HasScrollbar && y == BackButtonLine())) { + if (TextLine[y].isSelectable() || (GetItemCount(ActiveStore) > 0 && y == BackButtonLine())) { CurrentTextLine = y; StoreEnter(); } @@ -2763,10 +2087,139 @@ void CheckStoreBtn() } } -void ReleaseStoreBtn() +void CheckGUIConfirm() +{ + // Define the item and get its position in the store + Item &item = Store.storeList[GUITempItemId]; + constexpr Displacement offset { 0, INV_SLOT_SIZE_PX - 1 }; + const Point position = GetStoreSlotCoord(item.position) + offset; + + // Define the size and position of the confirmation dialog box + constexpr int boxWidth = 240; + int boxHeight = 20 + TextHeight() * 5; + const Rectangle boxRect { position, { boxWidth, boxHeight } }; + + // Check if the mouse click is outside the confirmation dialog + if (!boxRect.contains(MousePosition)) { + GUIConfirmFlag = false; // Close the confirmation dialog + return; + } + + // Calculate the relative Y position of the mouse click within the dialog box + const int relativeY = MousePosition.y - position.y - 10; + + // Calculate the height of each line (based on the store's font/text height system) + const int lineHeight = TextHeight(); // Use the existing function for consistent line height + + // Determine which line was clicked (line 3 is "Yes", line 4 is "No") + int lineClicked = relativeY / lineHeight; + + // Large small fonts draw beyond LineHeight. Check if the click was on the overflow text. + if (IsSmallFontTall() && lineClicked > 0 && lineClicked < NumStoreLines + && TextLine[lineClicked - 1].hasText() && !TextLine[lineClicked].hasText() + && relativeY < TextLine[lineClicked - 1].y + LargeTextHeight) { + --lineClicked; + } + + if (lineClicked >= 3) { + if (lineClicked >= BackButtonLine() + 1) + lineClicked = BackButtonLine(); + if (lineClicked <= 5 && !TextLine[lineClicked].isSelectable()) { + if (TextLine[lineClicked - 2].isSelectable()) { + lineClicked -= 2; + } else if (TextLine[lineClicked - 1].isSelectable()) { + lineClicked--; + } + } + if (TextLine[lineClicked].isSelectable() || lineClicked == BackButtonLine()) { + CurrentTextLine = lineClicked; + StoreEnter(); + } + } + + // Reset the confirmation flag after the action + GUIConfirmFlag = false; +} + +void ReleaseStoreButton() { CountdownScrollUp = -1; CountdownScrollDown = -1; } +bool IsPlayerInStore() +{ + if (IsStoreOpen) + return false; // Player currently doesn't have the text based store active. + return ActiveStore != TalkID::Exit; +} + +int GetItemBuyValue(const Item &item) +{ + int price = item._iIdentified ? item._iIvalue : item._ivalue; + + if (TownerId == TOWN_PEGBOY) { + price = gbIsHellfire ? price - (price / 4) : price + (price / 2); + } + + return price; +} + +int GetItemSellValue(const Item &item) +{ + int price = item._iIdentified ? item._iIvalue : item._ivalue; + + return price / 4; +} + +int GetItemRepairCost(const Item &item) +{ + int dur = item._iMaxDur - item._iDurability; + int repairCost = 0; + + if (item._iMagical != ITEM_QUALITY_NORMAL && item._iIdentified) { + repairCost = 30 * item._iIvalue * dur / (item._iMaxDur * 100 * 2); + } else { + repairCost = std::max(item._ivalue * dur / (item._iMaxDur * 2), 1); + } + + return repairCost; +} + +int GetItemRechargeCost(const Item &item) +{ + int rechargeCost = GetSpellData(item._iSpell).staffCost(); + rechargeCost = (rechargeCost * (item._iMaxCharges - item._iCharges)) / (item._iMaxCharges * 2); + return rechargeCost; +} + +int GetItemIdentifyCost() +{ + return 100; +} + +bool GiveItemToPlayer(Item &item, bool persistItem) +{ + + if (AutoEquipEnabled(*MyPlayer, item) && AutoEquip(*MyPlayer, item, persistItem, true)) { + return true; + } + + if (AutoPlaceItemInBelt(*MyPlayer, item, persistItem, true)) { + return true; + } + + return AutoPlaceItemInInventory(*MyPlayer, item, persistItem, true); +} + +uint32_t GetTotalPlayerGold() +{ + return MyPlayer->_pGold + Stash.gold; +} + +bool CanPlayerAfford(uint32_t price) +{ + return GetTotalPlayerGold() >= price; +} + } // namespace devilution diff --git a/Source/stores.h b/Source/stores.h index cea33714bbc..60e6de43784 100644 --- a/Source/stores.h +++ b/Source/stores.h @@ -12,88 +12,136 @@ #include "control.h" #include "engine.h" #include "engine/clx_sprite.hpp" +#include "engine/random.hpp" +#include "options.h" +#include "qol/stash.h" +#include "towners.h" #include "utils/attributes.h" namespace devilution { -#define WITCH_ITEMS 25 -#define SMITH_ITEMS 25 -#define SMITH_PREMIUM_ITEMS 15 -#define STORE_LINES 104 +/** @brief Number of player items that display in stores (Inventory slots and belt slots) */ +const int NumPlayerItems = (NUM_XY_SLOTS - (SLOTXY_EQUIPPED_LAST + 1)); + +constexpr int NumSmithBasicItems = 19; +constexpr int NumSmithBasicItemsHf = 24; + +constexpr int NumSmithItems = 6; +constexpr int NumSmithItemsHf = 15; + +constexpr int NumHealerItems = 17; +constexpr int NumHealerItemsHf = 19; +constexpr int NumHealerPinnedItems = 2; +constexpr int NumHealerPinnedItemsMp = 3; + +constexpr int NumWitchItems = 17; +constexpr int NumWitchItemsHf = 24; +constexpr int NumWitchPinnedItems = 3; + +constexpr int NumBoyItems = 1; + +constexpr int NumStoreLines = 104; + +extern _talker_id TownerId; + +extern Item TempItem; // Temporary item used to hold the item being traded enum class TalkID : uint8_t { - None, - Smith, - SmithBuy, - SmithSell, - SmithRepair, - Witch, - WitchBuy, - WitchSell, - WitchRecharge, + Exit, + MainMenu, + BasicBuy, + Buy, + Sell, + Repair, + Recharge, + Identify, + IdentifyShow, + Stash, NoMoney, NoRoom, Confirm, - Boy, - BoyBuy, - Healer, - Storyteller, - HealerBuy, - StorytellerIdentify, - SmithPremiumBuy, Gossip, - StorytellerIdentifyShow, - Tavern, - Drunk, - Barmaid, + Invalid, +}; + +enum class ItemLocation { + Inventory, + Belt, + Body }; -/** Currently active store */ -extern TalkID ActiveStore; +struct StoreMenuOption { + TalkID action; + std::string text; +}; -/** Current index into PlayerItemIndexes/PlayerItems */ -extern DVL_API_FOR_TEST int CurrentItemIndex; -/** Map of inventory items being presented in the store */ -extern int8_t PlayerItemIndexes[48]; -/** Copies of the players items as presented in the store */ -extern DVL_API_FOR_TEST Item PlayerItems[48]; +struct TownerLine { + const std::string menuHeader; + const StoreMenuOption *menuOptions; + size_t numOptions; +}; -/** Items sold by Griswold */ -extern Item SmithItems[SMITH_ITEMS]; -/** Number of premium items for sale by Griswold */ -extern int PremiumItemCount; -/** Base level of current premium items sold by Griswold */ -extern int PremiumItemLevel; -/** Premium items sold by Griswold */ -extern Item PremiumItems[SMITH_PREMIUM_ITEMS]; +struct IndexedItem { + Item *itemPtr; // Pointer to the original item + ItemLocation location; // Location in the player's inventory (Inventory, Belt, or Body) + int index; // Index in the corresponding array +}; -/** Items sold by Pepin */ -extern Item HealerItems[20]; +enum class ResourceType { + Life, + Mana, + Invalid, +}; -/** Items sold by Adria */ -extern Item WitchItems[WITCH_ITEMS]; +extern TalkID ActiveStore; // Currently active store +extern DVL_API_FOR_TEST std::vector playerItems; // Pointers to player items, coupled with necessary information + +class TownerStore { +public: + TownerStore(std::string name, TalkID buyBasic, TalkID buy, TalkID sell, TalkID special, ResourceType resource) + : name(name) + , buyBasic(buyBasic) + , buy(buy) + , sell(sell) + , special(special) + , resourceType(resource) + { + } + + std::string name; + std::vector basicItems; // Used for the blacksmith store that only displays non-magical items + std::vector items; + uint8_t itemLevel; + + TalkID buyBasic; + TalkID buy; + TalkID sell; + TalkID special; + ResourceType resourceType; // Resource type to restore for stores that restore player's resources +}; -/** Current level of the item sold by Wirt */ -extern int BoyItemLevel; -/** Current item sold by Wirt */ -extern Item BoyItem; +extern TownerStore Blacksmith; +extern TownerStore Healer; +extern TownerStore Witch; +extern TownerStore Boy; +extern TownerStore Storyteller; +extern TownerStore Barmaid; -void AddStoreHoldRepair(Item *itm, int8_t i); +extern std::unordered_map<_talker_id, TownerStore *> townerStores; -/** Clears premium items sold by Griswold and Wirt. */ +/* Clears premium items sold by Griswold and Wirt. */ void InitStores(); - -/** Spawns items sold by vendors, including premium items sold by Griswold and Wirt. */ +/* Spawns items sold by vendors, including premium items sold by Griswold and Wirt. */ void SetupTownStores(); - void FreeStoreMem(); - +void ExitStore(); void PrintSString(const Surface &out, int margin, int line, std::string_view text, UiFlags flags, int price = 0, int cursId = -1, bool cursIndent = false); void DrawSLine(const Surface &out, int sy); void DrawSTextHelp(); -void ClearSText(int s, int e); -void StartStore(TalkID s); -void DrawSText(const Surface &out); +void ClearTextLines(int start, int end); +void StartStore(TalkID s = TalkID::MainMenu); +void DrawStore(const Surface &out); +void DrawGUIConfirm(const Surface &out); void StoreESC(); void StoreUp(); void StoreDown(); @@ -101,7 +149,17 @@ void StorePrior(); void StoreNext(); void TakePlrsMoney(int cost); void StoreEnter(); -void CheckStoreBtn(); -void ReleaseStoreBtn(); +void CheckStoreButton(); +void CheckGUIConfirm(); +void ReleaseStoreButton(); +bool IsPlayerInStore(); +int GetItemBuyValue(const Item &item); +int GetItemSellValue(const Item &item); +int GetItemRepairCost(const Item &item); +int GetItemRechargeCost(const Item &item); +int GetItemIdentifyCost(); +bool GiveItemToPlayer(Item &item, bool persistItem); +uint32_t GetTotalPlayerGold(); +bool CanPlayerAfford(uint32_t price); } // namespace devilution diff --git a/Source/towners.cpp b/Source/towners.cpp index b89786a185e..56d1085fe13 100644 --- a/Source/towners.cpp +++ b/Source/towners.cpp @@ -340,7 +340,8 @@ void TalkToBarOwner(Player &player, Towner &barOwner) } TownerTalk(TEXT_OGDEN1); - StartStore(TalkID::Tavern); + TownerId = TOWN_TAVERN; + StartStore(); } void TalkToDeadguy(Player &player, Towner & /*deadguy*/) @@ -408,7 +409,8 @@ void TalkToBlackSmith(Player &player, Towner &blackSmith) } TownerTalk(TEXT_GRISWOLD1); - StartStore(TalkID::Smith); + TownerId = TOWN_SMITH; + StartStore(); } void TalkToWitch(Player &player, Towner & /*witch*/) @@ -458,7 +460,8 @@ void TalkToWitch(Player &player, Towner & /*witch*/) } TownerTalk(TEXT_ADRIA1); - StartStore(TalkID::Witch); + TownerId = TOWN_WITCH; + StartStore(); } void TalkToBarmaid(Player &player, Towner & /*barmaid*/) @@ -473,13 +476,15 @@ void TalkToBarmaid(Player &player, Towner & /*barmaid*/) } TownerTalk(TEXT_GILLIAN1); - StartStore(TalkID::Barmaid); + TownerId = TOWN_BMAID; + StartStore(); } void TalkToDrunk(Player & /*player*/, Towner & /*drunk*/) { TownerTalk(TEXT_FARNHAM1); - StartStore(TalkID::Drunk); + TownerId = TOWN_DRUNK; + StartStore(); } void TalkToHealer(Player &player, Towner &healer) @@ -517,13 +522,15 @@ void TalkToHealer(Player &player, Towner &healer) } TownerTalk(TEXT_PEPIN1); - StartStore(TalkID::Healer); + TownerId = TOWN_HEALER; + StartStore(); } void TalkToBoy(Player & /*player*/, Towner & /*boy*/) { TownerTalk(TEXT_WIRT1); - StartStore(TalkID::Boy); + TownerId = TOWN_PEGBOY; + StartStore(); } void TalkToStoryteller(Player &player, Towner & /*storyteller*/) @@ -559,7 +566,8 @@ void TalkToStoryteller(Player &player, Towner & /*storyteller*/) } TownerTalk(TEXT_STORY1); - StartStore(TalkID::Storyteller); + TownerId = TOWN_STORY; + StartStore(); } void TalkToCow(Player &player, Towner &cow) diff --git a/Source/track.cpp b/Source/track.cpp index 9ec9fca6c0a..244b239bce0 100644 --- a/Source/track.cpp +++ b/Source/track.cpp @@ -66,7 +66,7 @@ void RepeatMouseAction() if (sgbMouseDown == CLICK_NONE && ControllerActionHeld == GameActionType_NONE) return; - if (ActiveStore != TalkID::None) + if (IsPlayerInStore()) return; if (LastMouseButtonAction == MouseActionType::None) diff --git a/assets/data/store.clx b/assets/data/store.clx new file mode 100644 index 00000000000..8d192e96b4b Binary files /dev/null and b/assets/data/store.clx differ diff --git a/test/fixtures/memory_map/game.txt b/test/fixtures/memory_map/game.txt index a725eef9f42..f1c98aac768 100644 --- a/test/fixtures/memory_map/game.txt +++ b/test/fixtures/memory_map/game.txt @@ -47,8 +47,8 @@ M_DL 12544 8 dLight M_DL 12544 8 dPreLight M_DL 1600 8 AutomapView M_DL 12544 8 dMissile -R 32 PremiumItemCount -R 32 PremiumItemLevel +R 32 numPremiumItems +R 32 premiumItemLevel C_DA 6 item PremiumItems C_HF 15 item PremiumItems R 8 AutomapActive diff --git a/test/stores_test.cpp b/test/stores_test.cpp index 0e4d60e29e9..e85b0b2f2d7 100644 --- a/test/stores_test.cpp +++ b/test/stores_test.cpp @@ -6,69 +6,95 @@ using namespace devilution; namespace { -TEST(Stores, AddStoreHoldRepair_magic) +// Helper function to reset the playerItems vector before each test +void ResetPlayerItems() { - Item *item; - - item = &PlayerItems[0]; - - item->_iMaxDur = 60; - item->_iDurability = item->_iMaxDur; - item->_iMagical = ITEM_QUALITY_MAGIC; - item->_iIdentified = true; - item->_ivalue = 2000; - item->_iIvalue = 19000; - - for (int i = 1; i < item->_iMaxDur; i++) { - item->_ivalue = 2000; - item->_iIvalue = 19000; - item->_iDurability = i; - CurrentItemIndex = 0; - AddStoreHoldRepair(item, 0); - EXPECT_EQ(1, CurrentItemIndex); - EXPECT_EQ(95 * (item->_iMaxDur - i) / 2, item->_ivalue); - } - - item->_iDurability = 59; - CurrentItemIndex = 0; - item->_ivalue = 500; - item->_iIvalue = 30; // To cheap to repair - AddStoreHoldRepair(item, 0); - EXPECT_EQ(0, CurrentItemIndex); - EXPECT_EQ(30, item->_iIvalue); - EXPECT_EQ(500, item->_ivalue); + playerItems.clear(); } -TEST(Stores, AddStoreHoldRepair_normal) +// This is a direct copy of FilterRepairableItems logic for testing purposes +void Test_FilterRepairableItems() { - Item *item; - - item = &PlayerItems[0]; - - item->_iMaxDur = 20; - item->_iDurability = item->_iMaxDur; - item->_iMagical = ITEM_QUALITY_NORMAL; - item->_iIdentified = true; - item->_ivalue = 2000; - item->_iIvalue = item->_ivalue; - - for (int i = 1; i < item->_iMaxDur; i++) { - item->_ivalue = 2000; - item->_iIvalue = item->_ivalue; - item->_iDurability = i; - CurrentItemIndex = 0; - AddStoreHoldRepair(item, 0); - EXPECT_EQ(1, CurrentItemIndex); - EXPECT_EQ(50 * (item->_iMaxDur - i), item->_ivalue); - } - - item->_iDurability = 19; - CurrentItemIndex = 0; - item->_ivalue = 10; // less than 1 per dur - item->_iIvalue = item->_ivalue; - AddStoreHoldRepair(item, 0); - EXPECT_EQ(1, CurrentItemIndex); - EXPECT_EQ(1, item->_ivalue); - EXPECT_EQ(1, item->_iIvalue); + playerItems.erase(std::remove_if(playerItems.begin(), playerItems.end(), + [](const IndexedItem &indexedItem) { + const Item &itemPtr = *indexedItem.itemPtr; + return itemPtr._iDurability == itemPtr._iMaxDur || itemPtr._iMaxDur == DUR_INDESTRUCTIBLE; + }), + playerItems.end()); } + +TEST(Stores, FilterRepairableItems_magic) +{ + // Reset playerItems before starting the test + ResetPlayerItems(); + + // Create a magic item with durability and add it to the player's inventory + Item magicItem; + magicItem._iMaxDur = 60; + magicItem._iDurability = magicItem._iMaxDur - 1; + magicItem._iMagical = ITEM_QUALITY_MAGIC; + magicItem._iIdentified = true; + magicItem._ivalue = 2000; + magicItem._iIvalue = 19000; + + // Add the item to the player's inventory + playerItems.push_back({ &magicItem, ItemLocation::Inventory, 0 }); + + // Call the filtering function to remove non-repairable items + Test_FilterRepairableItems(); + + // Check that the playerItems vector contains the magic item and its values are correct + ASSERT_EQ(playerItems.size(), 1); + EXPECT_EQ(playerItems[0].itemPtr->_ivalue, 2000); // Item's value should not change + EXPECT_EQ(playerItems[0].itemPtr->_iDurability, 59); // Durability should match +} + +TEST(Stores, FilterRepairableItems_normal) +{ + // Reset playerItems before starting the test + ResetPlayerItems(); + + // Create a normal item with durability and add it to the player's inventory + Item normalItem; + normalItem._iMaxDur = 20; + normalItem._iDurability = normalItem._iMaxDur - 1; + normalItem._iMagical = ITEM_QUALITY_NORMAL; + normalItem._iIdentified = true; + normalItem._ivalue = 2000; + + // Add the item to the player's inventory + playerItems.push_back({ &normalItem, ItemLocation::Inventory, 0 }); + + // Call the filtering function to remove non-repairable items + Test_FilterRepairableItems(); + + // Check that the playerItems vector contains the normal item and its values are correct + ASSERT_EQ(playerItems.size(), 1); + EXPECT_EQ(playerItems[0].itemPtr->_ivalue, 2000); // Item's value should not change + EXPECT_EQ(playerItems[0].itemPtr->_iDurability, 19); // Durability should match +} + +TEST(Stores, FilterRepairableItems_no_repair) +{ + // Reset playerItems before starting the test + ResetPlayerItems(); + + // Create an item that cannot be repaired (already at max durability) + Item indestructibleItem; + indestructibleItem._iMaxDur = DUR_INDESTRUCTIBLE; // Indestructible item + indestructibleItem._iDurability = 100; + indestructibleItem._iMagical = ITEM_QUALITY_MAGIC; + indestructibleItem._iIdentified = true; + indestructibleItem._ivalue = 5000; + + // Add the item to the player's inventory + playerItems.push_back({ &indestructibleItem, ItemLocation::Inventory, 0 }); + + // Call the filtering function to remove non-repairable items + Test_FilterRepairableItems(); + + // Check that the playerItems vector is empty since the item is indestructible + ASSERT_EQ(playerItems.size(), 0); +} + } // namespace