From 12434056a59119f654404d9f5d2573c251c9abf1 Mon Sep 17 00:00:00 2001 From: Eric Robinson Date: Tue, 17 Sep 2024 18:00:51 -0400 Subject: [PATCH 1/3] Cleanup stores --- Source/control.cpp | 2 +- Source/controls/game_controls.cpp | 8 +- Source/controls/plrctrls.cpp | 4 +- Source/controls/touch/event_handlers.cpp | 4 +- Source/controls/touch/renderers.cpp | 2 +- Source/diablo.cpp | 44 +- Source/engine/render/scrollrt.cpp | 6 +- Source/inv.cpp | 2 +- Source/items.cpp | 279 ++- Source/loadsave.cpp | 41 +- Source/qol/chatlog.cpp | 2 +- Source/qol/itemlabels.cpp | 6 +- Source/qol/stash.cpp | 2 +- Source/stores.cpp | 2739 ++++++++-------------- Source/stores.h | 162 +- Source/towners.cpp | 24 +- Source/track.cpp | 2 +- test/fixtures/memory_map/game.txt | 4 +- test/stores_test.cpp | 146 +- 19 files changed, 1465 insertions(+), 2014 deletions(-) diff --git a/Source/control.cpp b/Source/control.cpp index 320b1b2926c..c807f46b677 100644 --- a/Source/control.cpp +++ b/Source/control.cpp @@ -686,7 +686,7 @@ bool IsLevelUpButtonVisible() if (ControlMode == ControlTypes::VirtualGamepad) { return false; } - if (ActiveStore != TalkID::None || IsStashOpen) { + if (IsPlayerInStore() || IsStashOpen) { return false; } if (QuestLogIsOpen && GetLeftPanel().contains(GetMainPanel().position + Displacement { 0, -74 })) { diff --git a/Source/controls/game_controls.cpp b/Source/controls/game_controls.cpp index 9c6cfd3bb56..8117a3212b7 100644 --- a/Source/controls/game_controls.cpp +++ b/Source/controls/game_controls.cpp @@ -134,7 +134,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 +171,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 +196,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); diff --git a/Source/controls/plrctrls.cpp b/Source/controls/plrctrls.cpp index 65982b7d55f..8c65c969c4f 100644 --- a/Source/controls/plrctrls.cpp +++ b/Source/controls/plrctrls.cpp @@ -65,7 +65,7 @@ quest_id pcursquest = Q_INVALID; */ bool InGameMenu() { - return ActiveStore != TalkID::None + return IsPlayerInStore() || HelpFlag || ChatLogFlag || ChatFlag @@ -1347,7 +1347,7 @@ HandleLeftStickOrDPadFn GetLeftStickOrDPadGameUIHandler() if (QuestLogIsOpen) { return &QuestLogMove; } - if (ActiveStore != TalkID::None) { + if (IsPlayerInStore()) { return &StoreMove; } return nullptr; diff --git a/Source/controls/touch/event_handlers.cpp b/Source/controls/touch/event_handlers.cpp index 5e2b3906790..7e93a129ed6 100644 --- a/Source/controls/touch/event_handlers.cpp +++ b/Source/controls/touch/event_handlers.cpp @@ -63,10 +63,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; } diff --git a/Source/controls/touch/renderers.cpp b/Source/controls/touch/renderers.cpp index 6bbe710c4d8..378aac33a3b 100644 --- a/Source/controls/touch/renderers.cpp +++ b/Source/controls/touch/renderers.cpp @@ -430,7 +430,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); } diff --git a/Source/diablo.cpp b/Source/diablo.cpp index e50b62433bd..72bacfa4023 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -353,8 +353,8 @@ void LeftMouseDown(uint16_t modState) return; } - if (ActiveStore != TalkID::None) { - CheckStoreBtn(); + if (IsPlayerInStore()) { + CheckStoreButton(); return; } @@ -417,8 +417,8 @@ void LeftMouseUp(uint16_t modState) } if (LevelButtonDown) CheckLevelButtonUp(); - if (ActiveStore != TalkID::None) - ReleaseStoreBtn(); + if (IsPlayerInStore()) + ReleaseStoreButton(); } void RightMouseDown(bool isShiftHeld) @@ -439,7 +439,7 @@ void RightMouseDown(bool isShiftHeld) doom_close(); return; } - if (ActiveStore != TalkID::None) + if (IsPlayerInStore()) return; if (SpellSelectFlag) { SetSpell(); @@ -576,7 +576,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()) { StoreEnter(); } else if (QuestLogIsOpen) { QuestlogEnter(); @@ -585,7 +585,7 @@ void PressKey(SDL_Keycode vkey, uint16_t modState) } return; case SDLK_UP: - if (ActiveStore != TalkID::None) { + if (IsPlayerInStore()) { StoreUp(); } else if (QuestLogIsOpen) { QuestlogUp(); @@ -600,7 +600,7 @@ void PressKey(SDL_Keycode vkey, uint16_t modState) } return; case SDLK_DOWN: - if (ActiveStore != TalkID::None) { + if (IsPlayerInStore()) { StoreDown(); } else if (QuestLogIsOpen) { QuestlogDown(); @@ -615,14 +615,14 @@ void PressKey(SDL_Keycode vkey, uint16_t modState) } 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 +643,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 +752,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()) { StoreUp(); } else if (QuestLogIsOpen) { QuestlogUp(); @@ -766,7 +766,7 @@ void GameEventHandler(const SDL_Event &event, uint16_t modState) sgOptions.Keymapper.KeyPressed(MouseScrollUpButton); } } else if (event.wheel.y < 0) { // down - if (ActiveStore != TalkID::None) { + if (IsPlayerInStore()) { StoreDown(); } else if (QuestLogIsOpen) { QuestlogDown(); @@ -1492,7 +1492,7 @@ void HelpKeyPressed() { if (HelpFlag) { HelpFlag = false; - } else if (ActiveStore != TalkID::None) { + } else if (IsPlayerInStore()) { InfoString = StringOrView {}; AddInfoBoxString(_("No help available")); /// BUGFIX: message isn't displayed AddInfoBoxString(_("while in stores")); @@ -1516,7 +1516,7 @@ void HelpKeyPressed() void InventoryKeyPressed() { - if (ActiveStore != TalkID::None) + if (IsPlayerInStore()) return; invflag = !invflag; if (!IsLeftPanelOpen() && CanPanelsCoverView()) { @@ -1537,7 +1537,7 @@ void InventoryKeyPressed() void CharacterSheetKeyPressed() { - if (ActiveStore != TalkID::None) + if (IsPlayerInStore()) return; if (!IsRightPanelOpen() && CanPanelsCoverView()) { if (CharFlag) { // We are closing the character sheet @@ -1555,7 +1555,7 @@ void CharacterSheetKeyPressed() void QuestLogKeyPressed() { - if (ActiveStore != TalkID::None) + if (IsPlayerInStore()) return; if (!QuestLogIsOpen) { StartQuestlog(); @@ -1580,7 +1580,7 @@ void QuestLogKeyPressed() void DisplaySpellsKeyPressed() { - if (ActiveStore != TalkID::None) + if (IsPlayerInStore()) return; CloseCharPanel(); QuestLogIsOpen = false; @@ -1596,7 +1596,7 @@ void DisplaySpellsKeyPressed() void SpellBookKeyPressed() { - if (ActiveStore != TalkID::None) + if (IsPlayerInStore()) return; SpellbookFlag = !SpellbookFlag; if (!IsLeftPanelOpen() && CanPanelsCoverView()) { @@ -1761,7 +1761,7 @@ void InitKeymapActions() SDLK_F3, [] { gamemenu_load_game(false); }, nullptr, - [&]() { return !gbIsMultiplayer && gbValidSaveFile && ActiveStore == TalkID::None && IsGameRunning(); }); + [&]() { return !gbIsMultiplayer && gbValidSaveFile && !IsPlayerInStore() && IsGameRunning(); }); #ifndef NOEXIT sgOptions.Keymapper.AddAction( "QuitGame", @@ -2328,7 +2328,7 @@ void InitPadmapActions() ControllerButton_NONE, [] { gamemenu_load_game(false); }, nullptr, - [&]() { return !gbIsMultiplayer && gbValidSaveFile && ActiveStore == TalkID::None && IsGameRunning(); }); + [&]() { return !gbIsMultiplayer && gbValidSaveFile && !IsPlayerInStore() && IsGameRunning(); }); sgOptions.Padmapper.AddAction( "Item Highlighting", N_("Item highlighting"), @@ -2778,7 +2778,7 @@ bool PressEscKey() rv = true; } - if (ActiveStore != TalkID::None) { + if (IsPlayerInStore()) { StoreESC(); rv = true; } diff --git a/Source/engine/render/scrollrt.cpp b/Source/engine/render/scrollrt.cpp index 52f997ebfcd..ed9fe9c08b5 100644 --- a/Source/engine/render/scrollrt.cpp +++ b/Source/engine/render/scrollrt.cpp @@ -613,7 +613,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 +1197,8 @@ 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 (invflag) { DrawInv(out); } else if (SpellbookFlag) { diff --git a/Source/inv.cpp b/Source/inv.cpp index 02384c43bc4..fd35f679358 100644 --- a/Source/inv.cpp +++ b/Source/inv.cpp @@ -2007,7 +2007,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; diff --git a/Source/items.cpp b/Source/items.cpp index c66085443d1..f52f9e82b0e 100644 --- a/Source/items.cpp +++ b/Source/items.cpp @@ -321,7 +321,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 +332,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, @@ -1891,17 +1891,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) @@ -4366,16 +4380,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 +4404,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); + } + + // 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()); } - for (int i = iCnt; i < SMITH_ITEMS; i++) - SmithItems[i].clear(); - SortVendor(SmithItems + PinnedItemCount); + 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 +4461,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 +4544,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 +4568,7 @@ void SpawnBoy(int lvl) ivalue = 0; - ItemType itemType = BoyItem._itype; + ItemType itemType = newItem._itype; switch (itemType) { case ItemType::LightArmor: @@ -4596,49 +4634,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..47e855713ea 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.Skip(4); // Blacksmith.itemCount + 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/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/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..94e464bdc0e 100644 --- a/Source/stores.cpp +++ b/Source/stores.cpp @@ -15,16 +15,12 @@ #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 "utils/format_int.hpp" #include "utils/language.h" #include "utils/str_cat.hpp" @@ -32,38 +28,41 @@ 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]; - -Item WitchItems[WITCH_ITEMS]; +namespace { -int BoyItemLevel; -Item BoyItem; +constexpr int PaddingTop = 32; -namespace { +const int SingleLineSpace = 1; +const int DoubleLineSpace = 2; +const int TripleLineSpace = 3; -/** The current towner being interacted with */ -_talker_id TownerId; +constexpr int MainMenuDividerLine = 5; +constexpr int BuySellMenuDividerLine = 3; +constexpr int ItemLineSpace = 4; +constexpr int ConfirmLine = 18; -/** 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 +96,105 @@ 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 + +Item TempItem; // Temporary item used to hold the item being traded + +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" } }; -constexpr int PaddingTop = 32; +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" } +}; + +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 +207,60 @@ constexpr int SmallTextHeight = 12; constexpr int LargeLineHeight = SmallLineHeight + 1; constexpr int LargeTextHeight = 18; +std::unordered_map<_talker_id, TownerStore *> townerStores; + +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,7 +270,7 @@ constexpr int LargeTextHeight = 18; int BackButtonLine() { if (IsSmallFontTall()) { - return HasScrollbar ? 21 : 20; + return HasScrollbar() ? 21 : 20; } return 22; } @@ -178,7 +289,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 +298,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 +334,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 +350,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 +367,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 +409,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,1522 +440,1003 @@ 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) +bool GiveItemToPlayer(Item &item, bool persistItem) { - Player &player = *MyPlayer; - if (AutoEquipEnabled(player, item) && AutoEquip(player, item, persistItem, true)) { + if (AutoEquipEnabled(*MyPlayer, item) && AutoEquip(*MyPlayer, item, persistItem, true)) { return true; } - if (AutoPlaceItemInBelt(player, item, persistItem, true)) { + if (AutoPlaceItemInBelt(*MyPlayer, item, persistItem, true)) { return true; } - return AutoPlaceItemInInventory(player, item, persistItem, true); + return AutoPlaceItemInInventory(*MyPlayer, item, persistItem, true); } -void ScrollVendorStore(Item *itemData, int storeLimit, int idx, int selling = true) +void SetupScreenElements(TalkID talkId) { - ClearSText(5, 21); - PreviousScrollPos = 5; + IsTextFullSize = true; + RenderGold = true; + ScrollPos = 0; - 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; - } else { - l -= 4; + SetLineAsDivider(BuySellMenuDividerLine); + AddItemListBackButton(talkId, /*selectable=*/true); + + const UiFlags flags = UiFlags::ColorWhitegold; + const int itemCount = GetItemCount(talkId); + + switch (talkId) { + case TalkID::BasicBuy: + case TalkID::Buy: { + if (itemCount == 0) { + SetLineText(20, 1, _("I have nothing for sale."), 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 = std::max(itemCount - ItemLineSpace, 0); // FIXME: Why is this different?? -void ScrollSmithBuy(int idx) -{ - ScrollVendorStore(SmithItems, static_cast(std::size(SmithItems)), idx); -} + if (itemCount == 1) { + SetLineText(20, 1, _("I have this item for sale:"), flags, false); + } else { + 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; + } -uint32_t TotalPlayerGold() -{ - return MyPlayer->_pGold + Stash.gold; -} + ScrollPos = 0; + NumTextLines = itemCount; -// TODO: Change `_iIvalue` to be unsigned instead of passing `int` here. -bool PlayerCanAfford(int price) -{ - return TotalPlayerGold() >= static_cast(price); -} + 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; + } -void StartSmithBuy() -{ - IsTextFullSize = true; - HasScrollbar = true; - ScrollPos = 0; + 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; + } - 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; + 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; + } - item._iStatFlag = MyPlayer->CanUseItem(item); - CurrentItemIndex++; - } + ScrollPos = 0; + NumTextLines = itemCount; - NumTextLines = std::max(CurrentItemIndex - 4, 0); + SetLineText(20, 1, _("Identify which item?"), UiFlags::ColorWhitegold, false); + } break; + } } -void ScrollSmithPremiumBuy(int boughtitems) +void SetupErrorScreen(TalkID talkId) { - int idx = 0; - for (; boughtitems != 0; idx++) { - if (!PremiumItems[idx].isEmpty()) - boughtitems--; + SetupScreenElements(OldActiveStore); + ClearTextLines(5, 23); + + std::string_view text; + + 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; } - ScrollVendorStore(PremiumItems, static_cast(std::size(PremiumItems)), idx); + SetLineText(0, 14, text, UiFlags::ColorWhite | UiFlags::AlignCenter, true); } -bool StartSmithPremiumBuy() +int GetItemBuyValue(const Item &item) { - CurrentItemIndex = 0; - for (Item &item : PremiumItems) { - if (item.isEmpty()) - continue; + int price = item._iIdentified ? item._iIvalue : item._ivalue; - item._iStatFlag = MyPlayer->CanUseItem(item); - CurrentItemIndex++; - } - if (CurrentItemIndex == 0) { - StartStore(TalkID::Smith); - CurrentTextLine = 14; - return false; + if (TownerId == TOWN_PEGBOY) { + price = gbIsHellfire ? price - (price / 4) : price + (price / 2); } - IsTextFullSize = true; - HasScrollbar = true; - ScrollPos = 0; - - RenderGold = true; - AddSText(20, 1, _("I have these premium items for sale:"), UiFlags::ColorWhitegold, false); - AddSLine(3); - AddItemListBackButton(); - - NumTextLines = std::max(CurrentItemIndex - 4, 0); + return price; +} - ScrollSmithPremiumBuy(ScrollPos); +int GetItemSellValue(const Item &item) +{ + int price = item._iIdentified ? item._iIvalue : item._ivalue; - return true; + return price / 4; } -bool SmithSellOk(int i) +int GetItemRepairCost(const Item &item) { - Item *pI; + int dur = item._iMaxDur - item._iDurability; + int repairCost = 0; - if (i >= 0) { - pI = &MyPlayer->InvList[i]; + if (item._iMagical != ITEM_QUALITY_NORMAL && item._iIdentified) { + repairCost = 30 * item._iIvalue * dur / (item._iMaxDur * 100 * 2); } else { - pI = &MyPlayer->SpdList[-(i + 1)]; + repairCost = std::max(item._ivalue * dur / (item._iMaxDur * 2), 1); } - if (pI->isEmpty()) - return false; - - if (pI->_iMiscId > IMISC_OILFIRST && pI->_iMiscId < IMISC_OILLAST) - return true; - - 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; + return repairCost; +} - return true; +int GetItemRechargeCost(const Item &item) +{ + int rechargeCost = GetSpellData(item._iSpell).staffCost(); + rechargeCost = (rechargeCost * (item._iMaxCharges - item._iCharges)) / (item._iMaxCharges * 2); + return rechargeCost; } -void ScrollSmithSell(int idx) +int GetItemIdentifyCost() { - ScrollVendorStore(PlayerItems, CurrentItemIndex, idx, false); + return 100; } -void StartSmithSell() +void SetupConfirmScreen() { - IsTextFullSize = true; - bool sellOk = false; - CurrentItemIndex = 0; + SetupScreenElements(OldActiveStore); + ClearTextLines(5, 23); - for (auto &item : PlayerItems) { - item.clear(); - } + int goldAmountDisplay; + std::string_view prompt; - const Player &myPlayer = *MyPlayer; + 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))); + } - for (int8_t i = 0; i < myPlayer._pNumInv; i++) { - if (CurrentItemIndex >= 48) - break; - if (SmithSellOk(i)) { - sellOk = true; - PlayerItems[CurrentItemIndex] = myPlayer.InvList[i]; + UiFlags itemColor = TempItem.getTextColorWithStatCheck(); - if (PlayerItems[CurrentItemIndex]._iMagical != ITEM_QUALITY_NORMAL && PlayerItems[CurrentItemIndex]._iIdentified) - PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._iIvalue; + 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); +} - PlayerItems[CurrentItemIndex]._ivalue = std::max(PlayerItems[CurrentItemIndex]._ivalue / 4, 1); - PlayerItems[CurrentItemIndex]._iIvalue = PlayerItems[CurrentItemIndex]._ivalue; - PlayerItemIndexes[CurrentItemIndex] = i; - CurrentItemIndex++; - } - } +void SetupGossipScreen() +{ + int la; + TownerStore *towner = townerStores[TownerId]; - for (int i = 0; i < MaxBeltItems; i++) { - if (CurrentItemIndex >= 48) - break; - if (SmithSellOk(-(i + 1))) { - sellOk = true; - PlayerItems[CurrentItemIndex] = myPlayer.SpdList[i]; + IsTextFullSize = false; - if (PlayerItems[CurrentItemIndex]._iMagical != ITEM_QUALITY_NORMAL && PlayerItems[CurrentItemIndex]._iIdentified) - PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._iIvalue; + 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); - PlayerItems[CurrentItemIndex]._ivalue = std::max(PlayerItems[CurrentItemIndex]._ivalue / 4, 1); - PlayerItems[CurrentItemIndex]._iIvalue = PlayerItems[CurrentItemIndex]._ivalue; - PlayerItemIndexes[CurrentItemIndex] = -(i + 1); - CurrentItemIndex++; - } + 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 (!sellOk) { - HasScrollbar = false; + int sn = 0; + for (auto &quest : Quests) { + if (quest._qactive == QUEST_ACTIVE && QuestDialogTable[TownerId][quest._qidx] != TEXT_NONE && quest._qlog) + sn++; + } - RenderGold = true; - AddSText(20, 1, _("You have nothing I want."), UiFlags::ColorWhitegold, false); - AddSLine(3); - AddItemListBackButton(/*selectable=*/true); - return; + if (sn > 6) { + sn = 14 - (sn / 2); + la = 1; + } else { + sn = 15 - sn; + la = 2; } - HasScrollbar = true; - ScrollPos = 0; - NumTextLines = myPlayer._pNumInv; + int sn2 = sn - 2; - RenderGold = true; - AddSText(20, 1, _("Which item is for sale?"), UiFlags::ColorWhitegold, false); - AddSLine(3); - ScrollSmithSell(ScrollPos); - AddItemListBackButton(); + 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(); } -bool SmithRepairOk(int i) +void SetMenuHeader(const std::string &header) { - const Player &myPlayer = *MyPlayer; - const Item &item = myPlayer.InvList[i]; + // Check if the header contains "\n\n", which indicates a two-line header + std::string::size_type pos = header.find("\n\n"); - 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; + 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); - return true; + // 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); + } } -void StartSmithRepair() +void SetMenuText(const TownerLine &townerInfo) { - IsTextFullSize = true; - CurrentItemIndex = 0; + const UiFlags flags = UiFlags::ColorWhitegold | UiFlags::AlignCenter; + + int startLine = MainMenuDividerLine + SingleLineSpace; - for (auto &item : PlayerItems) { - item.clear(); + 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); } +} - Player &myPlayer = *MyPlayer; +void SetMenuOption(TalkID action, const std::string_view &text) +{ + UiFlags flags = (action == TalkID::Gossip) ? UiFlags::ColorBlue | UiFlags::AlignCenter : UiFlags::ColorWhite | UiFlags::AlignCenter; - auto &helmet = myPlayer.InvBody[INVLOC_HEAD]; - if (!helmet.isEmpty() && helmet._iDurability != helmet._iMaxDur) { - AddStoreHoldRepair(&helmet, -1); + // 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; } - auto &armor = myPlayer.InvBody[INVLOC_CHEST]; - if (!armor.isEmpty() && armor._iDurability != armor._iMaxDur) { - AddStoreHoldRepair(&armor, -2); - } + SetLineText(0, CurrentMenuDrawLine, text, flags, true); - auto &leftHand = myPlayer.InvBody[INVLOC_HAND_LEFT]; - if (!leftHand.isEmpty() && leftHand._iDurability != leftHand._iMaxDur) { - AddStoreHoldRepair(&leftHand, -3); - } + // Update the vector to map the current line to the action + LineActionMappings.push_back({ CurrentMenuDrawLine, action }); - auto &rightHand = myPlayer.InvBody[INVLOC_HAND_RIGHT]; - if (!rightHand.isEmpty() && rightHand._iDurability != rightHand._iMaxDur) { - AddStoreHoldRepair(&rightHand, -4); - } + CurrentMenuDrawLine += DoubleLineSpace; - for (int i = 0; i < myPlayer._pNumInv; i++) { - if (CurrentItemIndex >= 48) - break; - if (SmithRepairOk(i)) { - AddStoreHoldRepair(&myPlayer.InvList[i], i); - } + if (TownerId == TOWN_PEGBOY && !Boy.items.empty() && CurrentMenuDrawLine == (WirtDialogueDrawLine - DoubleLineSpace)) { + CurrentMenuDrawLine = WirtDialogueDrawLine + (TripleLineSpace * 2); } +} - if (CurrentItemIndex == 0) { - HasScrollbar = false; +// FIXME: Put in anonymous namespace +void RestoreResource() +{ + int *resource = nullptr; + int *maxResource = nullptr; + int *baseResource = nullptr; + int *baseMaxResource = nullptr; + PanelDrawComponent component; + TownerStore *towner = townerStores[TownerId]; - RenderGold = true; - AddSText(20, 1, _("You have nothing to repair."), UiFlags::ColorWhitegold, false); - AddSLine(3); - AddItemListBackButton(/*selectable=*/true); + 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; } - HasScrollbar = true; - ScrollPos = 0; - NumTextLines = myPlayer._pNumInv; - - RenderGold = true; - AddSText(20, 1, _("Repair which item?"), UiFlags::ColorWhitegold, false); - AddSLine(3); + if (*resource == *maxResource) + return; - ScrollSmithSell(ScrollPos); - AddItemListBackButton(); + PlaySFX(SfxID::CastHealing); + *resource = *maxResource; + *baseResource = *baseMaxResource; + RedrawComponent(component); } -void FillManaPlayer() +void SetupMainMenuScreen() { - if (!*sgOptions.Gameplay.adriaRefillsMana) - return; + RestoreResource(); - Player &myPlayer = *MyPlayer; + IsTextFullSize = false; - if (myPlayer._pMana != myPlayer._pMaxMana) { - PlaySFX(SfxID::CastHealing); - } - myPlayer._pMana = myPlayer._pMaxMana; - myPlayer._pManaBase = myPlayer._pMaxManaBase; - RedrawComponent(PanelDrawComponent::Mana); -} + const TownerLine &lines = TownerLines[TownerId]; -void StartWitch() -{ - 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; -} + SetMenuHeader(lines.menuHeader); + SetLineAsDivider(MainMenuDividerLine); + SetMenuText(lines); -void ScrollWitchBuy(int idx) -{ - ScrollVendorStore(WitchItems, static_cast(std::size(WitchItems)), idx); -} + LineActionMappings.clear(); -void WitchBookLevel(Item &bookItem) -{ - 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; - } + 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); } } -void StartWitchBuy() +void BuildPlayerItemsVector() { - IsTextFullSize = true; - HasScrollbar = true; - ScrollPos = 0; - NumTextLines = 20; + playerItems.clear(); - 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()) + // 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 }); + } + + // 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 }); + } - WitchBookLevel(item); - item._iStatFlag = MyPlayer->CanUseItem(item); - CurrentItemIndex++; + // 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 }); } - NumTextLines = std::max(CurrentItemIndex - 4, 0); } -bool WitchSellOk(int i) +void FilterSellableItems(TalkID talkId) { - Item *pI; + playerItems.erase(std::remove_if(playerItems.begin(), playerItems.end(), + [talkId](const IndexedItem &indexedItem) { + Item *pI = indexedItem.itemPtr; - bool rv = false; + // Cannot sell equipped items + if (indexedItem.location == ItemLocation::Body) + return true; // Remove this item - if (i >= 0) - pI = &MyPlayer->InvList[i]; - else - pI = &MyPlayer->SpdList[-(i + 1)]; + // 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 + + 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 - 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; + 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 + + default: + return true; // Remove this item for unsupported TalkID + } + }), + playerItems.end()); } -void StartWitchSell() +void FilterRepairableItems() { - IsTextFullSize = true; - bool sellok = false; - CurrentItemIndex = 0; - - for (auto &item : PlayerItems) { - item.clear(); - } + // 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()); +} - const Player &myPlayer = *MyPlayer; +void FilterRechargeableItems() +{ + // 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()); +} - for (int i = 0; i < myPlayer._pNumInv; i++) { - if (CurrentItemIndex >= 48) - break; - if (WitchSellOk(i)) { - sellok = true; - PlayerItems[CurrentItemIndex] = myPlayer.InvList[i]; +void FilterIdentifiableItems() +{ + // 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()); +} - if (PlayerItems[CurrentItemIndex]._iMagical != ITEM_QUALITY_NORMAL && PlayerItems[CurrentItemIndex]._iIdentified) - PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._iIvalue; +void FilterPlayerItemsForAction(TalkID talkId) +{ + BuildPlayerItemsVector(); - 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; + 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; } - - 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) +void SetupTownerItemList(TalkID talkId, std::vector &items, int idx, bool selling /*= true*/) { - const auto &item = MyPlayer->InvList[i]; + ClearTextLines(5, 21); + PreviousScrollPos = 5; - if (item._itype == ItemType::Staff && item._iCharges != item._iMaxCharges) { - return true; - } + 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 ((item._iMiscId == IMISC_UNIQUE || item._iMiscId == IMISC_STAFF) && item._iCharges < item._iMaxCharges) { - return true; + SetLineText(20, l, item.getName(), itemColor, true, item._iCurs, true); + SetLineValue(l, price); + PrintStoreItem(item, l + 1, itemColor, true); + NextScrollPos = l; + idx++; } - 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++; + if (selling) { + if (CurrentTextLine != -1 && !TextLine[CurrentTextLine].isSelectable() && CurrentTextLine != BackButtonLine()) + CurrentTextLine = NextScrollPos; + } else { + NumTextLines = std::max(static_cast(items.size()) - ItemLineSpace, 0); + } } -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() -{ - StartStore(OldActiveStore); - HasScrollbar = false; - ClearSText(5, 23); - AddSText(0, 14, _("You do not have enough room in inventory"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); } -void StoreConfirm(Item &item) +void SetupItemList(TalkID talkId) { - 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; + TownerStore *towner = townerStores[TownerId]; - switch (OldActiveStore) { - case TalkID::BoyBuy: - prompt = _("Do we have a deal?"); + switch (talkId) { + case TalkID::BasicBuy: + SetupTownerItemList(talkId, towner->basicItems, ScrollPos, true); break; - case TalkID::StorytellerIdentify: - prompt = _("Are you sure you want to identify this item?"); + case TalkID::Buy: + SetupTownerItemList(talkId, towner->items, ScrollPos, true); break; - case TalkID::HealerBuy: - case TalkID::SmithPremiumBuy: - case TalkID::WitchBuy: - case TalkID::SmithBuy: - prompt = _("Are you sure you want to buy this item?"); + case TalkID::Sell: + case TalkID::Repair: + case TalkID::Recharge: + case TalkID::Identify: + SetupPlayerItemList(talkId, playerItems, ScrollPos, false); break; - case TalkID::WitchRecharge: - prompt = _("Are you sure you want to recharge this item?"); - break; - case TalkID::SmithSell: - case TalkID::WitchSell: - prompt = _("Are you sure you want to sell this item?"); - break; - case TalkID::SmithRepair: - prompt = _("Are you sure you want to repair this item?"); - 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() +void UpdateBookMinMagic(Item &bookItem) { - 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); + 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; + } } } -void SStartBoyBuy() +// FIXME: Move to anonymous namespace +static void UpdateItemStatFlag(Item &item) { - 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; - } + item._iStatFlag = MyPlayer->CanUseItem(item); } -void HealPlayer() +void UpdateItemStatFlags(TalkID talkId) { - Player &myPlayer = *MyPlayer; + TownerStore *towner = townerStores[TownerId]; - if (myPlayer._pHitPoints != myPlayer._pMaxHP) { - PlaySFX(SfxID::CastHealing); + switch (talkId) { + case TalkID::BasicBuy: + for (Item &item : towner->basicItems) + UpdateItemStatFlag(item); + break; + case TalkID::Buy: + for (Item &item : towner->items) + UpdateItemStatFlag(item); + break; } - myPlayer._pHitPoints = myPlayer._pMaxHP; - myPlayer._pHPBase = myPlayer._pMaxHPBase; - RedrawComponent(PanelDrawComponent::Health); } -void StartHealer() +uint32_t GetTotalPlayerGold() { - 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; + return MyPlayer->_pGold + Stash.gold; } -void ScrollHealerBuy(int idx) +bool CanPlayerAfford(uint32_t price) { - ScrollVendorStore(HealerItems, static_cast(std::size(HealerItems)), idx); + return GetTotalPlayerGold() >= price; } -void StartHealerBuy() +void SetupIdentifyResultScreen() { - 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); - - ScrollHealerBuy(ScrollPos); - AddItemListBackButton(); - - CurrentItemIndex = 0; - for (Item &item : HealerItems) { - if (item.isEmpty()) - continue; - - item._iStatFlag = MyPlayer->CanUseItem(item); - CurrentItemIndex++; - } + UiFlags itemColor = TempItem.getTextColorWithStatCheck(); - NumTextLines = std::max(CurrentItemIndex - 4, 0); + 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 StartStoryteller() +int GetLineForAction(TalkID action) { - 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); + 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; } -bool IdItemOk(Item *i) +TalkID GetActionForLine(int line) { - if (i->isEmpty()) { - return false; - } - if (i->_iMagical == ITEM_QUALITY_NORMAL) { - return false; - } - return !i->_iIdentified; + 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 AddStoreHoldId(Item itm, int8_t i) +void MainMenuEnter() { - PlayerItems[CurrentItemIndex] = itm; - PlayerItems[CurrentItemIndex]._ivalue = 100; - PlayerItems[CurrentItemIndex]._iIvalue = 100; - PlayerItemIndexes[CurrentItemIndex] = i; - CurrentItemIndex++; -} - -void StartStorytellerIdentify() -{ - bool idok = false; - IsTextFullSize = true; - CurrentItemIndex = 0; - - for (auto &item : PlayerItems) { - item.clear(); - } - - Player &myPlayer = *MyPlayer; + TalkID selectedAction = GetActionForLine(CurrentTextLine); + TownerStore *towner = townerStores[TownerId]; - 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(); -} - -void StartStorytellerIdentifyShow(Item &item) -{ - 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); -} - -void StartTalk() -{ - int la; - - 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); + } } - } - 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; -} - -void StartBarmaid() -{ - 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; -} - -void StartDrunk() -{ - 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; - } -} - -/** - * @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]); + case TalkID::Stash: + ExitStore(); + IsStashOpen = true; + Stash.RefreshItemStatFlags(); + invflag = true; + if (ControlMode != ControlTypes::KeyboardAndMouse) { + if (pcurs == CURSOR_DISARM) + NewCursor(CURSOR_HAND); + FocusOnInventory(); } - SmithItems[idx].clear(); - } - CalcPlrInv(*MyPlayer, true); -} - -void SmithBuyEnter() -{ - if (CurrentTextLine == BackButtonLine()) { - StartStore(TalkID::Smith); - CurrentTextLine = 12; - 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; - } - - TempItem = SmithItems[idx]; - StartStore(TalkID::Confirm); + StartStore(selectedAction); } -/** - * @brief Purchases a premium item from the smith. - */ -void SmithBuyPItem(Item &item) +int GetItemIndex() { - 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; - } - } - - PremiumItems[xx].clear(); - PremiumItemCount--; - SpawnPremium(*MyPlayer); + return OldScrollPos + ((OldTextLine - PreviousScrollPos) / ItemLineSpace); } -void SmithPremiumBuyEnter() +bool ReturnToMainMenu() { 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)) { - StartStore(TalkID::NoMoney); - return; - } - - if (!StoreAutoPlace(PremiumItems[idx], false)) { - StartStore(TalkID::NoRoom); - return; - } - - TempItem = PremiumItems[idx]; - StartStore(TalkID::Confirm); -} - -bool StoreGoldFit(Item &item) -{ - int cost = item._iIvalue; - - Size itemSize = GetInventorySize(item); - int itemRoomForGold = itemSize.width * itemSize.height * MaxGold; - - if (cost <= itemRoomForGold) { + StartStore(TalkID::MainMenu); return true; } - return cost <= itemRoomForGold + RoomForGold(); -} - -/** - * @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; + return false; } -void SmithSellEnter() +void BuyEnter() { - if (CurrentTextLine == BackButtonLine()) { - StartStore(TalkID::Smith); - CurrentTextLine = 16; + if (ReturnToMainMenu()) return; - } OldTextLine = CurrentTextLine; - OldActiveStore = TalkID::SmithSell; OldScrollPos = ScrollPos; - int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); - - if (!StoreGoldFit(PlayerItems[idx])) { - StartStore(TalkID::NoRoom); - return; - } - - TempItem = PlayerItems[idx]; - StartStore(TalkID::Confirm); -} - -/** - * @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; - } - - myPlayer.InvList[i]._iDurability = myPlayer.InvList[i]._iMaxDur; - TakePlrsMoney(price); -} - -void SmithRepairEnter() -{ - if (CurrentTextLine == BackButtonLine()) { - StartStore(TalkID::Smith); - CurrentTextLine = 18; - return; - } + int idx = GetItemIndex(); - OldActiveStore = TalkID::SmithRepair; - OldTextLine = CurrentTextLine; - OldScrollPos = ScrollPos; + // Boy displays his item in the 2nd slot instead of the 1st, so we need to adjust the index + if (TownerId == TOWN_PEGBOY) + idx--; - int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); + TownerStore *towner = townerStores[TownerId]; + Item &item = (ActiveStore == TalkID::BasicBuy) ? towner->basicItems[idx] : towner->items[idx]; + int cost = GetItemBuyValue(item); - if (!PlayerCanAfford(PlayerItems[idx]._iIvalue)) { + if (!CanPlayerAfford(cost)) { StartStore(TalkID::NoMoney); - return; - } - - TempItem = PlayerItems[idx]; - StartStore(TalkID::Confirm); -} - -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; + } else if (!GiveItemToPlayer(item, false)) { + StartStore(TalkID::NoRoom); + } else { + TempItem = item; + StartStore(TalkID::Confirm); } } -/** - * @brief Purchases an item from the witch. - */ -void WitchBuyItem(Item &item) +bool StoreGoldFit(Item &item) { - int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); - - if (idx < 3) - item._iSeed = AdvanceRndSeed(); + int cost = item._iIvalue; - TakePlrsMoney(item._iIvalue); - StoreAutoPlace(item, true); + Size itemSize = GetInventorySize(item); + int itemRoomForGold = itemSize.width * itemSize.height * MaxGold; - 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(); - } + if (cost <= itemRoomForGold) { + return true; } - CalcPlrInv(*MyPlayer, true); + return cost <= itemRoomForGold + RoomForGold(); } -void WitchBuyEnter() +/** + * @brief Sells an item from the player's inventory or belt. + */ +void SellItem() { - if (CurrentTextLine == BackButtonLine()) { - StartStore(TalkID::Witch); - CurrentTextLine = 14; - return; - } - - OldTextLine = CurrentTextLine; - OldScrollPos = ScrollPos; - OldActiveStore = TalkID::WitchBuy; + int idx = GetItemIndex(); - int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); + IndexedItem &itemToSell = playerItems[idx]; - if (!PlayerCanAfford(WitchItems[idx]._iIvalue)) { - StartStore(TalkID::NoMoney); - 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); } - if (!StoreAutoPlace(WitchItems[idx], false)) { - StartStore(TalkID::NoRoom); - return; - } + int price = GetItemSellValue(*itemToSell.itemPtr); - TempItem = WitchItems[idx]; - StartStore(TalkID::Confirm); + // Remove the sold item from the playerItems vector + playerItems.erase(playerItems.begin() + idx); + + // Add the gold to the player's inventory + AddGoldToInventory(*MyPlayer, price); + MyPlayer->_pGold += price; } -void WitchSellEnter() +void SellEnter() { - if (CurrentTextLine == BackButtonLine()) { - StartStore(TalkID::Witch); - CurrentTextLine = 16; + if (ReturnToMainMenu()) return; - } OldTextLine = CurrentTextLine; - OldActiveStore = TalkID::WitchSell; OldScrollPos = ScrollPos; - int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); + int idx = GetItemIndex(); - if (!StoreGoldFit(PlayerItems[idx])) { + // 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]; + // Store the item to be sold temporarily + // FIXME: Clean up call chain flow, so we no longer need TempItem global + TempItem = *playerItems[idx].itemPtr; + + // Proceed to the confirmation store screen StartStore(TalkID::Confirm); } /** - * @brief Recharges an item in the player's inventory or body in the witch. + * @brief Repairs an item in the player's inventory or body in the smith. */ -void WitchRechargeItem(int price) +void RepairItem() { - int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); - PlayerItems[idx]._iCharges = PlayerItems[idx]._iMaxCharges; + int idx = GetItemIndex(); - Player &myPlayer = *MyPlayer; + // Get a reference to the IndexedItem at the calculated index + IndexedItem &indexedItem = playerItems[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); - } + // Repair the item by setting its durability to the maximum + indexedItem.itemPtr->_iDurability = indexedItem.itemPtr->_iMaxDur; - TakePlrsMoney(price); - CalcPlrInv(myPlayer, true); + // Deduct the repair cost from the player's money + TakePlrsMoney(GetItemRepairCost(*indexedItem.itemPtr)); + + // Update the player's inventory + CalcPlrInv(*MyPlayer, true); } -void WitchRechargeEnter() +void RepairEnter() { - if (CurrentTextLine == BackButtonLine()) { - StartStore(TalkID::Witch); - CurrentTextLine = 18; + if (ReturnToMainMenu()) return; - } - OldActiveStore = TalkID::WitchRecharge; 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 repair cost + if (!CanPlayerAfford(GetItemRepairCost(*playerItems[idx].itemPtr))) { StartStore(TalkID::NoMoney); return; } - TempItem = PlayerItems[idx]; + // Temporarily store the item being repaired + TempItem = *playerItems[idx].itemPtr; + + // Proceed to the confirmation screen StartStore(TalkID::Confirm); } -void BoyEnter() +/** + * @brief Purchases an item from the witch. + */ +void BuyItem(Item &item) { - if (!BoyItem.isEmpty() && CurrentTextLine == 18) { - if (!PlayerCanAfford(50)) { - OldActiveStore = TalkID::Boy; - OldTextLine = 18; - OldScrollPos = ScrollPos; - StartStore(TalkID::NoMoney); + // Get the index of the purchased item + int idx = GetItemIndex(); + + // Boy displays his item in the 2nd slot instead of the 1st, so we need to adjust the index + if (TownerId == TOWN_PEGBOY) + idx--; + + int numPinnedItems = 0; + + switch (TownerId) { + case TOWN_HEALER: + numPinnedItems = !gbIsMultiplayer ? NumHealerPinnedItems : NumHealerPinnedItemsMp; + break; + case TOWN_WITCH: + numPinnedItems = NumWitchPinnedItems; + break; + } + + // If the item is one of the pinned items, generate a new seed for it + if (idx < numPinnedItems) { + item._iSeed = AdvanceRndSeed(); + } + + // Non-magical items are unidentified + if (item._iMagical == ITEM_QUALITY_NORMAL) + item._iIdentified = false; + + // Deduct the player's gold and give the item to the player + TakePlrsMoney(item._iIvalue); + GiveItemToPlayer(item, true); + + TownerStore *towner = townerStores[TownerId]; + + // 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(); - } + int idx = GetItemIndex(); - TakePlrsMoney(item._iIvalue); - if (item._iMagical == ITEM_QUALITY_NORMAL) - item._iIdentified = false; - StoreAutoPlace(item, true); + // Get a reference to the IndexedItem at the calculated index + IndexedItem &indexedItem = playerItems[idx]; - if (!gbIsMultiplayer) { - if (idx < 2) - return; - } else { - if (idx < 3) - return; - } - idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); - if (idx == 19) { - HealerItems[19].clear(); + // 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 == 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: + 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 +1453,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 +1513,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 +1553,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 +1580,8 @@ void SetupTownStores() SpawnSmith(l); SpawnWitch(l); SpawnHealer(l); - SpawnBoy(myPlayer.getCharacterLevel()); - SpawnPremium(myPlayer); + SpawnBoy(MyPlayer->getCharacterLevel()); + SpawnPremium(*MyPlayer); } void FreeStoreMem() @@ -2148,13 +1589,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 +1612,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 +1680,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(); @@ -2245,10 +1691,13 @@ void ClearSText(int s, int e) TextLine[i].type = STextStruct::Label; TextLine[i]._sval = 0; } + + // std::fill(storeLineMapping.begin(), storeLineMapping.end(), TalkID::None); } -void StartStore(TalkID s) +void StartStore(TalkID store /*= TalkID::MainMenu*/) { + SetActiveStore(store); if (*sgOptions.Gameplay.showItemGraphicsInStores) { CreateHalfSizeItemSprites(); } @@ -2258,145 +1707,72 @@ void StartStore(TalkID s) RenderGold = false; QuestLogIsOpen = false; CloseGoldDrop(); - ClearSText(0, STORE_LINES); - ReleaseStoreBtn(); - switch (s) { - case TalkID::Smith: - StartSmith(); - 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; - } - break; - } - case TalkID::SmithSell: - StartSmithSell(); - break; - case TalkID::SmithRepair: - StartSmithRepair(); + ClearTextLines(0, NumStoreLines); + ReleaseStoreButton(); + + switch (store) { + case TalkID::MainMenu: + SetupMainMenuScreen(); break; - case TalkID::Witch: - StartWitch(); + case TalkID::Gossip: + SetupGossipScreen(); break; - case TalkID::WitchBuy: - if (CurrentItemIndex > 0) - StartWitchBuy(); + case TalkID::BasicBuy: + case TalkID::Buy: + SetupScreenElements(store); + SetupItemList(store); + UpdateItemStatFlags(store); break; - case TalkID::WitchSell: - StartWitchSell(); + case TalkID::Sell: + case TalkID::Repair: + case TalkID::Recharge: + case TalkID::Identify: + SetupScreenElements(store); + FilterPlayerItemsForAction(store); + SetupItemList(store); 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(); + 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 +1780,11 @@ 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) - DrawSSlider(out, 4, 20); + if (HasScrollbar()) + DrawScrollbar(out, 4, 20); } void StoreESC() @@ -2421,69 +1797,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 +1832,7 @@ void StoreUp() return; } - if (HasScrollbar) { + if (HasScrollbar()) { if (CurrentTextLine == PreviousScrollPos) { if (ScrollPos != 0) ScrollPos--; @@ -2504,7 +1842,7 @@ void StoreUp() CurrentTextLine--; while (!TextLine[CurrentTextLine].isSelectable()) { if (CurrentTextLine == 0) - CurrentTextLine = STORE_LINES - 1; + CurrentTextLine = NumStoreLines - 1; else CurrentTextLine--; } @@ -2512,13 +1850,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 +1869,7 @@ void StoreDown() return; } - if (HasScrollbar) { + if (HasScrollbar()) { if (CurrentTextLine == NextScrollPos) { if (ScrollPos < NumTextLines) ScrollPos++; @@ -2540,7 +1878,7 @@ void StoreDown() CurrentTextLine++; while (!TextLine[CurrentTextLine].isSelectable()) { - if (CurrentTextLine == STORE_LINES - 1) + if (CurrentTextLine == NumStoreLines - 1) CurrentTextLine = 0; else CurrentTextLine++; @@ -2548,13 +1886,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 +1902,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 +1914,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 +1928,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 +1950,23 @@ void StoreEnter() } PlaySFX(SfxID::MenuSelect); + switch (ActiveStore) { - case TalkID::Smith: - SmithEnter(); - break; - case TalkID::SmithPremiumBuy: - SmithPremiumBuyEnter(); - break; - case TalkID::SmithBuy: - SmithBuyEnter(); - break; - case TalkID::SmithSell: - SmithSellEnter(); + case TalkID::MainMenu: + MainMenuEnter(); break; - case TalkID::SmithRepair: - SmithRepairEnter(); + case TalkID::BasicBuy: + case TalkID::Buy: + BuyEnter(); break; - case TalkID::Witch: - WitchEnter(); + case TalkID::Sell: + SellEnter(); break; - case TalkID::WitchBuy: - WitchBuyEnter(); + case TalkID::Repair: + RepairEnter(); break; - case TalkID::WitchSell: - WitchSellEnter(); - break; - case TalkID::WitchRecharge: - WitchRechargeEnter(); + case TalkID::Recharge: + RechargeEnter(); break; case TalkID::NoMoney: case TalkID::NoRoom: @@ -2651,45 +1977,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); - break; - case TalkID::Drunk: - DrunkEnter(); - break; - case TalkID::Tavern: - TavernEnter(); + case TalkID::IdentifyShow: + StartStore(TalkID::Identify); 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 +1999,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 +2016,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 +2041,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 +2050,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 +2065,15 @@ void CheckStoreBtn() } } -void ReleaseStoreBtn() +void ReleaseStoreButton() { CountdownScrollUp = -1; CountdownScrollDown = -1; } +bool IsPlayerInStore() +{ + return ActiveStore != TalkID::Exit; +} + } // namespace devilution diff --git a/Source/stores.h b/Source/stores.h index cea33714bbc..1814d56f4fc 100644 --- a/Source/stores.h +++ b/Source/stores.h @@ -12,88 +12,131 @@ #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; 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, }; -/** Currently active store */ -extern TalkID ActiveStore; +enum class ItemLocation { + Inventory, + Belt, + Body +}; -/** 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 StoreMenuOption { + TalkID action; + std::string text; +}; -/** 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 TownerLine { + const std::string menuHeader; + const StoreMenuOption *menuOptions; + size_t numOptions; +}; -/** Items sold by Pepin */ -extern Item HealerItems[20]; +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 Adria */ -extern Item WitchItems[WITCH_ITEMS]; +enum class ResourceType { + Life, + Mana, + Invalid, +}; -/** Current level of the item sold by Wirt */ -extern int BoyItemLevel; -/** Current item sold by Wirt */ -extern Item BoyItem; +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 +}; -void AddStoreHoldRepair(Item *itm, int8_t i); +extern TownerStore Blacksmith; +extern TownerStore Healer; +extern TownerStore Witch; +extern TownerStore Boy; +extern TownerStore Storyteller; +extern TownerStore Barmaid; -/** 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 StoreESC(); void StoreUp(); void StoreDown(); @@ -101,7 +144,8 @@ void StorePrior(); void StoreNext(); void TakePlrsMoney(int cost); void StoreEnter(); -void CheckStoreBtn(); -void ReleaseStoreBtn(); +void CheckStoreButton(); +void ReleaseStoreButton(); +bool IsPlayerInStore(); } // 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/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 From ccb9294a580f72aac5f21065ad4a9fb3df952fb6 Mon Sep 17 00:00:00 2001 From: Eric Robinson Date: Thu, 10 Oct 2024 00:10:05 -0400 Subject: [PATCH 2/3] Add missing include --- Source/stores.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/stores.cpp b/Source/stores.cpp index 94e464bdc0e..4dc041e2290 100644 --- a/Source/stores.cpp +++ b/Source/stores.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include From 7043ea42bbc311c9e7cf98e964a440343cd73cfc Mon Sep 17 00:00:00 2001 From: Eric Robinson Date: Thu, 10 Oct 2024 00:27:31 -0400 Subject: [PATCH 3/3] Write item count to save file --- Source/loadsave.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/loadsave.cpp b/Source/loadsave.cpp index 47e855713ea..6d44d98e22e 100644 --- a/Source/loadsave.cpp +++ b/Source/loadsave.cpp @@ -2797,7 +2797,7 @@ void SaveGameData(SaveWriter &saveWriter) } } - file.Skip(4); // Blacksmith.itemCount + file.WriteBE(Blacksmith.items.size()); file.WriteBE(Blacksmith.itemLevel); // Save Smith premium items with a fixed count