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

2803 lines
69 KiB

/**
* @file stores.cpp
*
* Implementation of functionality for stores and towner dialogs.
*/
#include "stores.h"
#include <algorithm>
#include <cstdint>
#include <string_view>
#include <fmt/format.h>
#include "controls/control_mode.hpp"
#include "controls/plrctrls.h"
#include "cursor.h"
#include "engine/backbuffer_state.hpp"
#include "engine/random.hpp"
#include "engine/render/clx_render.hpp"
#include "engine/render/primitive_render.hpp"
#include "engine/render/text_render.hpp"
#include "engine/trn.hpp"
#include "game_mode.hpp"
#include "lua/lua_event.hpp"
#include "minitext.h"
#include "multi.h"
#include "options.h"
#include "panels/info_box.hpp"
#include "qol/stash.h"
#include "tables/townerdat.hpp"
#include "towners.h"
#include "utils/format_int.hpp"
#include "utils/language.h"
#include "utils/str_cat.hpp"
#include "utils/utf8.hpp"
namespace devilution {
TalkID ActiveStore;
int CurrentItemIndex;
int8_t PlayerItemIndexes[48];
Item PlayerItems[48];
StaticVector<Item, NumSmithBasicItemsHf> SmithItems;
int PremiumItemCount;
int PremiumItemLevel;
StaticVector<Item, NumSmithItemsHf> PremiumItems;
StaticVector<Item, NumHealerItemsHf> HealerItems;
StaticVector<Item, NumWitchItemsHf> WitchItems;
int BoyItemLevel;
Item BoyItem;
namespace {
/** The current towner being interacted with */
_talker_id TownerId;
/** Is the current dialog full size */
bool IsTextFullSize;
/** 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;
struct STextStruct {
enum Type : uint8_t {
Label,
Divider,
Selectable,
};
std::string text;
int _sval;
int y;
UiFlags flags;
Type type;
uint8_t _sx;
uint8_t _syoff;
int cursId;
bool cursIndent;
[[nodiscard]] bool isDivider() const
{
return type == Divider;
}
[[nodiscard]] bool isSelectable() const
{
return type == Selectable;
}
[[nodiscard]] bool hasText() const
{
return !text.empty();
}
};
/** Text lines */
STextStruct TextLine[NumStoreLines];
/** 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"),
};
constexpr int PaddingTop = 32;
// For most languages, line height is always 12.
// This includes blank lines and divider line.
constexpr int SmallLineHeight = 12;
constexpr int SmallTextHeight = 12;
// For larger small fonts (Chinese and Japanese), text lines are
// taller and overflow.
// We space out blank lines a bit more to give space to 3-line store items.
constexpr int LargeLineHeight = SmallLineHeight + 1;
constexpr int LargeTextHeight = 18;
/**
* The line index with the Back / Leave button.
* This is a special button that is always the last line.
*
* For lists with a scrollbar, it is not selectable (mouse-only).
*/
int BackButtonLine()
{
if (IsSmallFontTall()) {
return HasScrollbar ? 21 : 20;
}
return 22;
}
int LineHeight()
{
return IsSmallFontTall() ? LargeLineHeight : SmallLineHeight;
}
int TextHeight()
{
return IsSmallFontTall() ? LargeTextHeight : SmallTextHeight;
}
void CalculateLineHeights()
{
TextLine[0].y = 0;
if (IsSmallFontTall()) {
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;
} else {
TextLine[i].y = i * LargeLineHeight;
}
}
} else {
for (int i = 1; i < NumStoreLines; ++i) {
TextLine[i].y = i * SmallLineHeight;
}
}
}
void DrawSTextBack(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)
{
const Point uiPosition = GetUIRectangle().position;
int yd1 = y1 * 12 + 44 + uiPosition.y;
const int yd2 = y2 * 12 + 44 + uiPosition.y;
if (CountdownScrollUp != -1)
ClxDraw(out, { uiPosition.x + 601, yd1 }, (*pSTextSlidCels)[11]);
else
ClxDraw(out, { uiPosition.x + 601, yd1 }, (*pSTextSlidCels)[9]);
if (CountdownScrollDown != -1)
ClxDraw(out, { uiPosition.x + 601, yd2 }, (*pSTextSlidCels)[10]);
else
ClxDraw(out, { uiPosition.x + 601, yd2 }, (*pSTextSlidCels)[8]);
yd1 += 12;
int yd3 = yd1;
for (; yd3 < yd2; yd3 += 12) {
ClxDraw(out, { uiPosition.x + 601, yd3 }, (*pSTextSlidCels)[13]);
}
if (CurrentTextLine == BackButtonLine())
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;
ClxDraw(out, { uiPosition.x + 601, (y1 + 1) * 12 + 44 + uiPosition.y + yd3 }, (*pSTextSlidCels)[12]);
}
void AddSLine(size_t y)
{
TextLine[y]._sx = 0;
TextLine[y]._syoff = 0;
TextLine[y].text.clear();
TextLine[y].text.shrink_to_fit();
TextLine[y].type = STextStruct::Divider;
TextLine[y].cursId = -1;
TextLine[y].cursIndent = false;
}
void AddSTextVal(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)
{
TextLine[y]._sx = x;
TextLine[y]._syoff = 0;
TextLine[y].text.clear();
TextLine[y].text.append(text);
TextLine[y].flags = flags;
TextLine[y].type = sel ? STextStruct::Selectable : STextStruct::Label;
TextLine[y].cursId = cursId;
TextLine[y].cursIndent = cursIndent;
}
void AddOptionsBackButton()
{
const int line = BackButtonLine();
AddSText(0, line, _("Back"), UiFlags::ColorWhite | UiFlags::AlignCenter, true);
TextLine[line]._syoff = IsSmallFontTall() ? 0 : 6;
}
void AddItemListBackButton(bool selectable = false)
{
const int line = BackButtonLine();
const std::string_view text = _("Back");
if (!selectable && IsSmallFontTall()) {
AddSText(0, line, text, UiFlags::ColorWhite | UiFlags::AlignRight, selectable);
} else {
AddSLine(line - 1);
AddSText(0, line, text, UiFlags::ColorWhite | UiFlags::AlignCenter, selectable);
TextLine[line]._syoff = 6;
}
}
void PrintStoreItem(const Item &item, int l, UiFlags flags, bool cursIndent = false)
{
std::string productLine;
if (item._iIdentified) {
if (item._iMagical != ITEM_QUALITY_UNIQUE) {
if (item._iPrePower != -1) {
productLine.append(PrintItemPower(item._iPrePower, item));
}
}
if (item._iSufPower != -1) {
if (!productLine.empty())
productLine.append(_(", "));
productLine.append(PrintItemPower(item._iSufPower, item));
}
}
if (item._iMiscId == IMISC_STAFF && item._iMaxCharges != 0) {
if (!productLine.empty())
productLine.append(_(", "));
productLine.append(fmt::format(fmt::runtime(_("Charges: {:d}/{:d}")), item._iCharges, item._iMaxCharges));
}
if (!productLine.empty()) {
AddSText(40, l, productLine, flags, false, -1, cursIndent);
l++;
productLine.clear();
}
if (item._itype != ItemType::Misc) {
if (item._iClass == ICLASS_WEAPON)
productLine = fmt::format(fmt::runtime(_("Damage: {:d}-{:d} ")), item._iMinDam, item._iMaxDam);
else if (item._iClass == ICLASS_ARMOR)
productLine = fmt::format(fmt::runtime(_("Armor: {:d} ")), item._iAC);
if (item._iMaxDur != DUR_INDESTRUCTIBLE && item._iMaxDur != 0)
productLine += fmt::format(fmt::runtime(_("Dur: {:d}/{:d}")), item._iDurability, item._iMaxDur);
else
productLine.append(_("Indestructible"));
}
int8_t str = item._iMinStr;
uint8_t mag = item._iMinMag;
int8_t dex = item._iMinDex;
if (str != 0 || mag != 0 || dex != 0) {
if (!productLine.empty())
productLine.append(_(", "));
productLine.append(_("Required:"));
if (str != 0)
productLine.append(fmt::format(fmt::runtime(_(" {:d} Str")), str));
if (mag != 0)
productLine.append(fmt::format(fmt::runtime(_(" {:d} Mag")), mag));
if (dex != 0)
productLine.append(fmt::format(fmt::runtime(_(" {:d} Dex")), dex));
}
AddSText(40, l++, productLine, flags, false, -1, cursIndent);
}
bool StoreAutoPlace(Item &item, bool persistItem)
{
Player &player = *MyPlayer;
if (AutoEquipEnabled(player, item) && AutoEquip(player, item, persistItem, true)) {
return true;
}
if (AutoPlaceItemInBelt(player, item, persistItem, true)) {
return true;
}
if (persistItem) {
return AutoPlaceItemInInventory(player, item, true);
}
return CanFitItemInInventory(player, item);
}
void ScrollVendorStore(std::span<Item> itemData, int storeLimit, int idx, int selling = true)
{
ClearSText(5, 21);
PreviousScrollPos = 5;
for (int l = 5; l < 20 && idx < storeLimit; l += 4) {
const Item &item = itemData[idx];
const 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;
idx++;
}
if (selling) {
if (CurrentTextLine != -1 && !TextLine[CurrentTextLine].isSelectable() && CurrentTextLine != BackButtonLine())
CurrentTextLine = NextScrollPos;
} else {
NumTextLines = std::max(static_cast<int>(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;
}
void ScrollSmithBuy(int idx)
{
ScrollVendorStore(SmithItems, static_cast<int>(SmithItems.size()), idx);
}
uint32_t TotalPlayerGold()
{
return MyPlayer->_pGold + Stash.gold;
}
// TODO: Change `_iIvalue` to be unsigned instead of passing `int` here.
bool PlayerCanAfford(int price)
{
return TotalPlayerGold() >= static_cast<uint32_t>(price);
}
void StartSmithBuy()
{
IsTextFullSize = true;
HasScrollbar = true;
ScrollPos = 0;
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) {
item._iStatFlag = MyPlayer->CanUseItem(item);
CurrentItemIndex++;
}
NumTextLines = std::max(CurrentItemIndex - 4, 0);
}
void ScrollSmithPremiumBuy(int boughtitems)
{
int idx = 0;
for (; boughtitems != 0; idx++) {
if (!PremiumItems[idx].isEmpty())
boughtitems--;
}
ScrollVendorStore(PremiumItems, static_cast<int>(PremiumItems.size()), idx);
}
bool StartSmithPremiumBuy()
{
CurrentItemIndex = 0;
for (Item &item : PremiumItems) {
item._iStatFlag = MyPlayer->CanUseItem(item);
CurrentItemIndex++;
}
if (CurrentItemIndex == 0) {
StartStore(TalkID::Smith);
CurrentTextLine = 14;
return false;
}
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);
ScrollSmithPremiumBuy(ScrollPos);
return true;
}
bool SmithSellOk(int i)
{
Item *pI;
if (i >= 0) {
pI = &MyPlayer->InvList[i];
} else {
pI = &MyPlayer->SpdList[-(i + 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 true;
}
void ScrollSmithSell(int idx)
{
ScrollVendorStore(PlayerItems, CurrentItemIndex, idx, false);
}
void StartSmithSell()
{
IsTextFullSize = true;
bool sellOk = false;
CurrentItemIndex = 0;
for (auto &item : PlayerItems) {
item.clear();
}
const Player &myPlayer = *MyPlayer;
for (int8_t i = 0; i < myPlayer._pNumInv; i++) {
if (CurrentItemIndex >= 48)
break;
if (SmithSellOk(i)) {
sellOk = true;
PlayerItems[CurrentItemIndex] = myPlayer.InvList[i];
if (PlayerItems[CurrentItemIndex]._iMagical != ITEM_QUALITY_NORMAL && PlayerItems[CurrentItemIndex]._iIdentified)
PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._iIvalue;
PlayerItems[CurrentItemIndex]._ivalue = std::max(PlayerItems[CurrentItemIndex]._ivalue / 4, 1);
PlayerItems[CurrentItemIndex]._iIvalue = PlayerItems[CurrentItemIndex]._ivalue;
PlayerItemIndexes[CurrentItemIndex] = i;
CurrentItemIndex++;
}
}
for (int i = 0; i < MaxBeltItems; i++) {
if (CurrentItemIndex >= 48)
break;
if (SmithSellOk(-(i + 1))) {
sellOk = true;
PlayerItems[CurrentItemIndex] = myPlayer.SpdList[i];
if (PlayerItems[CurrentItemIndex]._iMagical != ITEM_QUALITY_NORMAL && PlayerItems[CurrentItemIndex]._iIdentified)
PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._iIvalue;
PlayerItems[CurrentItemIndex]._ivalue = std::max(PlayerItems[CurrentItemIndex]._ivalue / 4, 1);
PlayerItems[CurrentItemIndex]._iIvalue = PlayerItems[CurrentItemIndex]._ivalue;
PlayerItemIndexes[CurrentItemIndex] = -(i + 1);
CurrentItemIndex++;
}
}
if (!sellOk) {
HasScrollbar = false;
RenderGold = true;
AddSText(20, 1, _("You have nothing I want."), UiFlags::ColorWhitegold, false);
AddSLine(3);
AddItemListBackButton(/*selectable=*/true);
return;
}
HasScrollbar = true;
ScrollPos = 0;
NumTextLines = myPlayer._pNumInv;
RenderGold = true;
AddSText(20, 1, _("Which item is for sale?"), UiFlags::ColorWhitegold, false);
AddSLine(3);
ScrollSmithSell(ScrollPos);
AddItemListBackButton();
}
bool SmithRepairOk(int i)
{
const Player &myPlayer = *MyPlayer;
const Item &item = myPlayer.InvList[i];
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;
return true;
}
void StartSmithRepair()
{
IsTextFullSize = true;
CurrentItemIndex = 0;
for (auto &item : PlayerItems) {
item.clear();
}
Player &myPlayer = *MyPlayer;
auto &helmet = myPlayer.InvBody[INVLOC_HEAD];
if (!helmet.isEmpty() && helmet._iDurability != helmet._iMaxDur) {
AddStoreHoldRepair(&helmet, -1);
}
auto &armor = myPlayer.InvBody[INVLOC_CHEST];
if (!armor.isEmpty() && armor._iDurability != armor._iMaxDur) {
AddStoreHoldRepair(&armor, -2);
}
auto &leftHand = myPlayer.InvBody[INVLOC_HAND_LEFT];
if (!leftHand.isEmpty() && leftHand._iDurability != leftHand._iMaxDur) {
AddStoreHoldRepair(&leftHand, -3);
}
auto &rightHand = myPlayer.InvBody[INVLOC_HAND_RIGHT];
if (!rightHand.isEmpty() && rightHand._iDurability != rightHand._iMaxDur) {
AddStoreHoldRepair(&rightHand, -4);
}
for (int i = 0; i < myPlayer._pNumInv; i++) {
if (CurrentItemIndex >= 48)
break;
if (SmithRepairOk(i)) {
AddStoreHoldRepair(&myPlayer.InvList[i], i);
}
}
if (CurrentItemIndex == 0) {
HasScrollbar = false;
RenderGold = true;
AddSText(20, 1, _("You have nothing to repair."), UiFlags::ColorWhitegold, false);
AddSLine(3);
AddItemListBackButton(/*selectable=*/true);
return;
}
HasScrollbar = true;
ScrollPos = 0;
NumTextLines = myPlayer._pNumInv;
RenderGold = true;
AddSText(20, 1, _("Repair which item?"), UiFlags::ColorWhitegold, false);
AddSLine(3);
ScrollSmithSell(ScrollPos);
AddItemListBackButton();
}
void StartWitch()
{
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;
}
void ScrollWitchBuy(int idx)
{
ScrollVendorStore(WitchItems, static_cast<int>(WitchItems.size()), idx);
}
void WitchBookLevel(Item &bookItem)
{
if (bookItem._iMiscId != IMISC_BOOK)
return;
bookItem._iMinMag = GetSpellData(bookItem._iSpell).minInt;
uint8_t spellLevel = MyPlayer->_pSplLvl[static_cast<int8_t>(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 StartWitchBuy()
{
IsTextFullSize = true;
HasScrollbar = true;
ScrollPos = 0;
NumTextLines = 20;
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) {
WitchBookLevel(item);
item._iStatFlag = MyPlayer->CanUseItem(item);
CurrentItemIndex++;
}
NumTextLines = std::max(CurrentItemIndex - 4, 0);
}
bool WitchSellOk(int i)
{
Item *pI;
bool rv = false;
if (i >= 0)
pI = &MyPlayer->InvList[i];
else
pI = &MyPlayer->SpdList[-(i + 1)];
if (pI->_itype == ItemType::Misc)
rv = true;
if (pI->_iMiscId > 29 && pI->_iMiscId < 41)
rv = false;
if (pI->_iClass == ICLASS_QUEST)
rv = false;
if (pI->_itype == ItemType::Staff && (!gbIsHellfire || IsValidSpell(pI->_iSpell)))
rv = true;
if (pI->IDidx >= IDI_FIRSTQUEST && pI->IDidx <= IDI_LASTQUEST)
rv = false;
if (pI->IDidx == IDI_LAZSTAFF)
rv = false;
return rv;
}
void StartWitchSell()
{
IsTextFullSize = true;
bool sellok = false;
CurrentItemIndex = 0;
for (auto &item : PlayerItems) {
item.clear();
}
const Player &myPlayer = *MyPlayer;
for (int i = 0; i < myPlayer._pNumInv; i++) {
if (CurrentItemIndex >= 48)
break;
if (WitchSellOk(i)) {
sellok = true;
PlayerItems[CurrentItemIndex] = myPlayer.InvList[i];
if (PlayerItems[CurrentItemIndex]._iMagical != ITEM_QUALITY_NORMAL && PlayerItems[CurrentItemIndex]._iIdentified)
PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._iIvalue;
PlayerItems[CurrentItemIndex]._ivalue = std::max(PlayerItems[CurrentItemIndex]._ivalue / 4, 1);
PlayerItems[CurrentItemIndex]._iIvalue = PlayerItems[CurrentItemIndex]._ivalue;
PlayerItemIndexes[CurrentItemIndex] = i;
CurrentItemIndex++;
}
}
for (int i = 0; i < MaxBeltItems; i++) {
if (CurrentItemIndex >= 48)
break;
if (!myPlayer.SpdList[i].isEmpty() && WitchSellOk(-(i + 1))) {
sellok = true;
PlayerItems[CurrentItemIndex] = myPlayer.SpdList[i];
if (PlayerItems[CurrentItemIndex]._iMagical != ITEM_QUALITY_NORMAL && PlayerItems[CurrentItemIndex]._iIdentified)
PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._iIvalue;
PlayerItems[CurrentItemIndex]._ivalue = std::max(PlayerItems[CurrentItemIndex]._ivalue / 4, 1);
PlayerItems[CurrentItemIndex]._iIvalue = PlayerItems[CurrentItemIndex]._ivalue;
PlayerItemIndexes[CurrentItemIndex] = -(i + 1);
CurrentItemIndex++;
}
}
if (!sellok) {
HasScrollbar = false;
RenderGold = true;
AddSText(20, 1, _("You have nothing I want."), UiFlags::ColorWhitegold, false);
AddSLine(3);
AddItemListBackButton(/*selectable=*/true);
return;
}
HasScrollbar = true;
ScrollPos = 0;
NumTextLines = myPlayer._pNumInv;
RenderGold = true;
AddSText(20, 1, _("Which item is for sale?"), UiFlags::ColorWhitegold, false);
AddSLine(3);
ScrollSmithSell(ScrollPos);
AddItemListBackButton();
}
bool WitchRechargeOk(int i)
{
const auto &item = MyPlayer->InvList[i];
if (item._itype == ItemType::Staff && item._iCharges != item._iMaxCharges) {
return true;
}
if ((item._iMiscId == IMISC_UNIQUE || item._iMiscId == IMISC_STAFF) && item._iCharges < item._iMaxCharges) {
return true;
}
return false;
}
void AddStoreHoldRecharge(Item itm, int8_t i)
{
PlayerItems[CurrentItemIndex] = itm;
PlayerItems[CurrentItemIndex]._ivalue += GetSpellData(itm._iSpell).staffCost();
PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._ivalue * (PlayerItems[CurrentItemIndex]._iMaxCharges - PlayerItems[CurrentItemIndex]._iCharges) / (PlayerItems[CurrentItemIndex]._iMaxCharges * 2);
PlayerItems[CurrentItemIndex]._iIvalue = PlayerItems[CurrentItemIndex]._ivalue;
PlayerItemIndexes[CurrentItemIndex] = i;
CurrentItemIndex++;
}
void StartWitchRecharge()
{
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];
if ((leftHand._itype == ItemType::Staff || leftHand._iMiscId == IMISC_UNIQUE) && leftHand._iCharges != leftHand._iMaxCharges) {
rechargeok = true;
AddStoreHoldRecharge(leftHand, -1);
}
for (int i = 0; i < myPlayer._pNumInv; i++) {
if (CurrentItemIndex >= 48)
break;
if (WitchRechargeOk(i)) {
rechargeok = true;
AddStoreHoldRecharge(myPlayer.InvList[i], i);
}
}
if (!rechargeok) {
HasScrollbar = false;
RenderGold = true;
AddSText(20, 1, _("You have nothing to recharge."), UiFlags::ColorWhitegold, false);
AddSLine(3);
AddItemListBackButton(/*selectable=*/true);
return;
}
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)
{
StartStore(OldActiveStore);
HasScrollbar = false;
ClearSText(5, 23);
if (OldActiveStore == TalkID::StorytellerIdentifyAll) {
AddSText(0, 10, _("Identify all items?"), UiFlags::ColorWhite | UiFlags::AlignCenter, false);
AddSText(0, 12, fmt::format(fmt::runtime(_("Cost: {:s} gold")), FormatInteger(item._iIvalue)), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false);
AddSText(0, 15, _("Are you sure you want to identify all items?"), UiFlags::ColorWhite | UiFlags::AlignCenter, false);
AddSText(0, 18, _("Yes"), UiFlags::ColorWhite | UiFlags::AlignCenter, true);
AddSText(0, 20, _("No"), UiFlags::ColorWhite | UiFlags::AlignCenter, true);
return;
}
const UiFlags itemColor = item.getTextColorWithStatCheck();
AddSText(20, 8, item.getName(), itemColor, false);
AddSTextVal(8, item._iIvalue);
PrintStoreItem(item, 9, itemColor);
std::string_view prompt;
switch (OldActiveStore) {
case TalkID::BoyBuy:
prompt = _("Do we have a deal?");
break;
case TalkID::StorytellerIdentify:
prompt = _("Are you sure you want to identify this item?");
break;
case TalkID::StorytellerIdentifyAll:
prompt = _("Are you sure you want to identify all items?");
break;
case TalkID::HealerBuy:
case TalkID::SmithPremiumBuy:
case TalkID::WitchBuy:
case TalkID::SmithBuy:
prompt = _("Are you sure you want to buy this item?");
break;
case TalkID::WitchRecharge:
prompt = _("Are you sure you want to recharge this item?");
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<int>(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 RestoreStoreFromOldState()
{
if (OldActiveStore == TalkID::StorytellerIdentifyAll) {
StartStore(TalkID::Storyteller);
CurrentTextLine = 16;
ScrollPos = 0;
return;
}
StartStore(OldActiveStore);
CurrentTextLine = OldTextLine;
ScrollPos = OldScrollPos;
}
void StartBoy()
{
IsTextFullSize = false;
HasScrollbar = false;
AddSText(0, 2, _("Wirt the Peg-legged boy"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false);
AddSLine(5);
if (!BoyItem.isEmpty()) {
AddSText(0, 8, _("Talk to Wirt"), UiFlags::ColorBlue | UiFlags::AlignCenter, true);
AddSText(0, 12, _("I have something for sale,"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false);
AddSText(0, 14, _("but it will cost 50 gold"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false);
AddSText(0, 16, _("just to take a look. "), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false);
AddSText(0, 18, _("What have you got?"), UiFlags::ColorWhite | UiFlags::AlignCenter, true);
AddSText(0, 20, _("Say goodbye"), UiFlags::ColorWhite | UiFlags::AlignCenter, true);
} else {
AddSText(0, 12, _("Talk to Wirt"), UiFlags::ColorBlue | UiFlags::AlignCenter, true);
AddSText(0, 18, _("Say goodbye"), UiFlags::ColorWhite | UiFlags::AlignCenter, true);
}
}
void SStartBoyBuy()
{
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);
const UiFlags itemColor = BoyItem.getTextColorWithStatCheck();
AddSText(20, 10, BoyItem.getName(), itemColor, true, BoyItem._iCurs, true);
if (gbIsHellfire)
AddSTextVal(10, BoyItem._iIvalue - (BoyItem._iIvalue / 4));
else
AddSTextVal(10, BoyItem._iIvalue + (BoyItem._iIvalue / 2));
PrintStoreItem(BoyItem, 11, itemColor, true);
{
// Add a Leave button. Unlike the other item list back buttons,
// this one has different text and different layout in LargerSmallFont locales.
const int line = BackButtonLine();
AddSLine(line - 1);
AddSText(0, line, _("Leave"), UiFlags::ColorWhite | UiFlags::AlignCenter, true);
TextLine[line]._syoff = 6;
}
}
void HealPlayer()
{
Player &myPlayer = *MyPlayer;
if (myPlayer._pHitPoints != myPlayer._pMaxHP) {
PlaySFX(SfxID::CastHealing);
}
myPlayer._pHitPoints = myPlayer._pMaxHP;
myPlayer._pHPBase = myPlayer._pMaxHPBase;
RedrawComponent(PanelDrawComponent::Health);
}
void StartHealer()
{
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;
}
void ScrollHealerBuy(int idx)
{
ScrollVendorStore(HealerItems, static_cast<int>(HealerItems.size()), idx);
}
void StartHealerBuy()
{
IsTextFullSize = true;
HasScrollbar = true;
ScrollPos = 0;
RenderGold = true;
AddSText(20, 1, _("I have these items for sale:"), UiFlags::ColorWhitegold, false);
AddSLine(3);
ScrollHealerBuy(ScrollPos);
AddItemListBackButton();
CurrentItemIndex = 0;
for (Item &item : HealerItems) {
item._iStatFlag = MyPlayer->CanUseItem(item);
CurrentItemIndex++;
}
NumTextLines = std::max(CurrentItemIndex - 4, 0);
}
void StartStoryteller()
{
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, 16, _("Identify all items"), UiFlags::ColorWhite | UiFlags::AlignCenter, true);
AddSText(0, 18, _("Say goodbye"), UiFlags::ColorWhite | UiFlags::AlignCenter, true);
AddSLine(5);
}
bool IdItemOk(Item *i)
{
return IsItemIdentifiableByStoryteller(*i);
}
void AddStoreHoldId(Item itm, int8_t i)
{
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;
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);
const 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 StorytellerIdentifyAllItems()
{
Player &myPlayer = *MyPlayer;
TakePlrsMoney(CountIdentifiablePlayerItems(myPlayer) * 100);
IdentifyPlayerItems(myPlayer);
}
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();
return;
}
int sn = 0;
for (auto &quest : Quests) {
if (quest._qactive == QUEST_ACTIVE && GetTownerQuestDialog(TownerId, quest._qidx) != TEXT_NONE && quest._qlog)
sn++;
}
if (sn > 6) {
sn = 14 - (sn / 2);
la = 1;
} else {
sn = 15 - sn;
la = 2;
}
const int sn2 = sn - 2;
for (auto &quest : Quests) {
if (quest._qactive == QUEST_ACTIVE && GetTownerQuestDialog(TownerId, quest._qidx) != TEXT_NONE && quest._qlog) {
AddSText(0, sn, _(QuestsData[quest._qidx]._qlstr), UiFlags::ColorWhite | UiFlags::AlignCenter, true);
sn += la;
}
}
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);
SmithItems.erase(SmithItems.begin() + idx);
CalcPlrInv(*MyPlayer, true);
}
void SmithBuyEnter()
{
if (CurrentTextLine == BackButtonLine()) {
StartStore(TalkID::Smith);
CurrentTextLine = 12;
return;
}
OldTextLine = CurrentTextLine;
OldScrollPos = ScrollPos;
OldActiveStore = TalkID::SmithBuy;
const 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);
}
/**
* @brief Purchases a premium item from the smith.
*/
void SmithBuyPItem(Item &item)
{
TakePlrsMoney(item._iIvalue);
if (item._iMagical == ITEM_QUALITY_NORMAL)
item._iIdentified = false;
StoreAutoPlace(item, true);
int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4);
ReplacePremium(*MyPlayer, idx);
}
void SmithPremiumBuyEnter()
{
if (CurrentTextLine == BackButtonLine()) {
StartStore(TalkID::Smith);
CurrentTextLine = 14;
return;
}
OldActiveStore = TalkID::SmithPremiumBuy;
OldTextLine = CurrentTextLine;
OldScrollPos = ScrollPos;
int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4);
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)
{
const int cost = item._iIvalue;
const Size itemSize = GetInventorySize(item);
const int itemRoomForGold = itemSize.width * itemSize.height * MaxGold;
if (cost <= itemRoomForGold) {
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));
const int cost = PlayerItems[idx]._iIvalue;
CurrentItemIndex--;
if (idx != CurrentItemIndex) {
while (idx < CurrentItemIndex) {
PlayerItems[idx] = PlayerItems[idx + 1];
PlayerItemIndexes[idx] = PlayerItemIndexes[idx + 1];
idx++;
}
}
AddGoldToInventory(myPlayer, cost);
myPlayer._pGold += cost;
}
void SmithSellEnter()
{
if (CurrentTextLine == BackButtonLine()) {
StartStore(TalkID::Smith);
CurrentTextLine = 16;
return;
}
OldTextLine = CurrentTextLine;
OldActiveStore = TalkID::SmithSell;
OldScrollPos = ScrollPos;
const 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)
{
const int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4);
PlayerItems[idx]._iDurability = PlayerItems[idx]._iMaxDur;
const 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;
}
OldActiveStore = TalkID::SmithRepair;
OldTextLine = CurrentTextLine;
OldScrollPos = ScrollPos;
const int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4);
if (!PlayerCanAfford(PlayerItems[idx]._iIvalue)) {
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;
}
}
/**
* @brief Purchases an item from the witch.
*/
void WitchBuyItem(Item &item)
{
int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4);
if (idx < 3)
item._iSeed = AdvanceRndSeed();
TakePlrsMoney(item._iIvalue);
StoreAutoPlace(item, true);
if (idx >= 3) {
WitchItems.erase(WitchItems.begin() + idx);
}
CalcPlrInv(*MyPlayer, true);
}
void WitchBuyEnter()
{
if (CurrentTextLine == BackButtonLine()) {
StartStore(TalkID::Witch);
CurrentTextLine = 14;
return;
}
OldTextLine = CurrentTextLine;
OldScrollPos = ScrollPos;
OldActiveStore = TalkID::WitchBuy;
const int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4);
if (!PlayerCanAfford(WitchItems[idx]._iIvalue)) {
StartStore(TalkID::NoMoney);
return;
}
if (!StoreAutoPlace(WitchItems[idx], false)) {
StartStore(TalkID::NoRoom);
return;
}
TempItem = WitchItems[idx];
StartStore(TalkID::Confirm);
}
void WitchSellEnter()
{
if (CurrentTextLine == BackButtonLine()) {
StartStore(TalkID::Witch);
CurrentTextLine = 16;
return;
}
OldTextLine = CurrentTextLine;
OldActiveStore = TalkID::WitchSell;
OldScrollPos = ScrollPos;
const int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4);
if (!StoreGoldFit(PlayerItems[idx])) {
StartStore(TalkID::NoRoom);
return;
}
TempItem = PlayerItems[idx];
StartStore(TalkID::Confirm);
}
/**
* @brief Recharges an item in the player's inventory or body in the witch.
*/
void WitchRechargeItem(int price)
{
const int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4);
PlayerItems[idx]._iCharges = PlayerItems[idx]._iMaxCharges;
Player &myPlayer = *MyPlayer;
const int8_t i = PlayerItemIndexes[idx];
if (i < 0) {
myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges = myPlayer.InvBody[INVLOC_HAND_LEFT]._iMaxCharges;
NetSendCmdChItem(true, INVLOC_HAND_LEFT);
} else {
myPlayer.InvList[i]._iCharges = myPlayer.InvList[i]._iMaxCharges;
NetSyncInvItem(myPlayer, i);
}
TakePlrsMoney(price);
CalcPlrInv(myPlayer, true);
}
void WitchRechargeEnter()
{
if (CurrentTextLine == BackButtonLine()) {
StartStore(TalkID::Witch);
CurrentTextLine = 18;
return;
}
OldActiveStore = TalkID::WitchRecharge;
OldTextLine = CurrentTextLine;
OldScrollPos = ScrollPos;
const int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4);
if (!PlayerCanAfford(PlayerItems[idx]._iIvalue)) {
StartStore(TalkID::NoMoney);
return;
}
TempItem = PlayerItems[idx];
StartStore(TalkID::Confirm);
}
void BoyEnter()
{
if (!BoyItem.isEmpty() && CurrentTextLine == 18) {
if (!PlayerCanAfford(50)) {
OldActiveStore = TalkID::Boy;
OldTextLine = 18;
OldScrollPos = ScrollPos;
StartStore(TalkID::NoMoney);
} else {
TakePlrsMoney(50);
StartStore(TalkID::BoyBuy);
}
return;
}
if ((CurrentTextLine != 8 && !BoyItem.isEmpty()) || (CurrentTextLine != 12 && BoyItem.isEmpty())) {
ActiveStore = TalkID::None;
return;
}
TownerId = TOWN_PEGBOY;
OldActiveStore = TalkID::Boy;
OldTextLine = CurrentTextLine;
StartStore(TalkID::Gossip);
}
void BoyBuyItem(Item &item, int itemPrice)
{
TakePlrsMoney(itemPrice);
StoreAutoPlace(item, true);
item.clear();
OldActiveStore = TalkID::Boy;
CalcPlrInv(*MyPlayer, true);
OldTextLine = 12;
}
/**
* @brief Purchases an item from the healer.
*/
void HealerBuyItem(Item &item)
{
int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4);
if (!gbIsMultiplayer) {
if (idx < 2)
item._iSeed = AdvanceRndSeed();
} else {
if (idx < 3)
item._iSeed = AdvanceRndSeed();
}
TakePlrsMoney(item._iIvalue);
if (item._iMagical == ITEM_QUALITY_NORMAL)
item._iIdentified = false;
StoreAutoPlace(item, true);
if (!gbIsMultiplayer) {
if (idx < 2)
return;
} else {
if (idx < 3)
return;
}
idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4);
HealerItems.erase(HealerItems.begin() + idx);
CalcPlrInv(*MyPlayer, true);
}
void BoyBuyEnter()
{
if (CurrentTextLine != 10) {
ActiveStore = TalkID::None;
return;
}
OldActiveStore = TalkID::BoyBuy;
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;
}
if (!StoreAutoPlace(BoyItem, false)) {
StartStore(TalkID::NoRoom);
return;
}
TempItem = BoyItem;
TempItem._iIvalue = price;
StartStore(TalkID::Confirm);
}
void StorytellerIdentifyItem(Item &item)
{
Player &myPlayer = *MyPlayer;
const 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);
}
void ConfirmEnter(Item &item)
{
if (CurrentTextLine == 18) {
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);
break;
case TalkID::WitchRecharge:
WitchRechargeItem(item._iIvalue);
break;
case TalkID::BoyBuy:
BoyBuyItem(BoyItem, item._iIvalue);
break;
case TalkID::HealerBuy:
HealerBuyItem(item);
break;
case TalkID::StorytellerIdentify:
StorytellerIdentifyItem(item);
StartStore(TalkID::StorytellerIdentifyShow);
return;
case TalkID::StorytellerIdentifyAll:
StorytellerIdentifyAllItems();
StartStore(TalkID::Storyteller);
CurrentTextLine = 16;
return;
case TalkID::SmithPremiumBuy:
SmithBuyPItem(item);
break;
default:
break;
}
}
StartStore(OldActiveStore);
if (CurrentTextLine == BackButtonLine())
return;
if (OldActiveStore == TalkID::StorytellerIdentifyAll) {
CurrentTextLine = 16;
ScrollPos = 0;
return;
}
CurrentTextLine = OldTextLine;
ScrollPos = std::min(OldScrollPos, NumTextLines);
while (CurrentTextLine != -1 && !TextLine[CurrentTextLine].isSelectable()) {
CurrentTextLine--;
}
}
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;
const 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 16:
OldActiveStore = TalkID::StorytellerIdentifyAll;
OldTextLine = 16;
OldScrollPos = 0;
TempItem.clear();
TempItem._iIvalue = CountIdentifiablePlayerItems(*MyPlayer) * 100;
if (TempItem._iIvalue == 0) {
StartStore(TalkID::StorytellerIdentify);
return;
}
if (!PlayerCanAfford(TempItem._iIvalue)) {
StartStore(TalkID::NoMoney);
return;
}
StartStore(TalkID::Confirm);
return;
case 18:
ActiveStore = TalkID::None;
break;
}
}
void StorytellerIdentifyEnter()
{
if (CurrentTextLine == BackButtonLine()) {
StartStore(TalkID::Storyteller);
CurrentTextLine = 14;
return;
}
OldActiveStore = TalkID::StorytellerIdentify;
OldTextLine = CurrentTextLine;
OldScrollPos = ScrollPos;
const int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4);
if (!PlayerCanAfford(PlayerItems[idx]._iIvalue)) {
StartStore(TalkID::NoMoney);
return;
}
TempItem = PlayerItems[idx];
StartStore(TalkID::Confirm);
}
void TalkEnter()
{
if (CurrentTextLine == BackButtonLine()) {
StartStore(OldActiveStore);
CurrentTextLine = OldTextLine;
return;
}
int sn = 0;
for (auto &quest : Quests) {
if (quest._qactive == QUEST_ACTIVE && GetTownerQuestDialog(TownerId, quest._qidx) != TEXT_NONE && quest._qlog)
sn++;
}
int la = 2;
if (sn > 6) {
sn = 14 - (sn / 2);
la = 1;
} else {
sn = 15 - sn;
}
if (CurrentTextLine == sn - 2) {
Towner *target = GetTowner(TownerId);
assert(target != nullptr);
InitQTextMsg(target->gossip);
return;
}
for (auto &quest : Quests) {
if (quest._qactive == QUEST_ACTIVE && GetTownerQuestDialog(TownerId, quest._qidx) != TEXT_NONE && quest._qlog) {
if (sn == CurrentTextLine) {
InitQTextMsg(GetTownerQuestDialog(TownerId, quest._qidx));
}
sn += la;
}
}
}
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++) {
auto &item = player.InvList[i];
if (item._itype != ItemType::Gold || (skipMaxPiles && item._ivalue == MaxGold))
continue;
if (cost < item._ivalue) {
item._ivalue -= cost;
SetPlrHandGoldCurs(player.InvList[i]);
return 0;
}
cost -= item._ivalue;
player.RemoveInvItem(i);
i = -1;
}
return cost;
}
void DrawSelector(const Surface &out, const Rectangle &rect, std::string_view text, UiFlags flags)
{
const int lineWidth = GetLineWidth(text);
int x1 = rect.position.x - 20;
if (HasAnyOf(flags, UiFlags::AlignCenter))
x1 += (rect.size.width - lineWidth) / 2;
ClxDraw(out, { x1, rect.position.y + 13 }, (*pSPentSpn2Cels)[PentSpn2Spin()]);
int x2 = rect.position.x + rect.size.width + 5;
if (HasAnyOf(flags, UiFlags::AlignCenter))
x2 = rect.position.x + (rect.size.width - lineWidth) / 2 + lineWidth + 5;
ClxDraw(out, { x2, rect.position.y + 13 }, (*pSPentSpn2Cels)[PentSpn2Spin()]);
}
} // namespace
void AddStoreHoldRepair(Item *itm, int8_t i)
{
Item *item;
int v;
item = &PlayerItems[CurrentItemIndex];
PlayerItems[CurrentItemIndex] = *itm;
const 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, NumStoreLines);
ActiveStore = TalkID::None;
IsTextFullSize = false;
HasScrollbar = false;
PremiumItemCount = 0;
PremiumItemLevel = 1;
SmithItems.clear();
WitchItems.clear();
HealerItems.clear();
PremiumItems.clear();
BoyItem.clear();
BoyItemLevel = 0;
}
void SetupTownStores()
{
const Player &myPlayer = *MyPlayer;
int l = myPlayer.getCharacterLevel() / 2;
if (!gbIsMultiplayer) {
l = 0;
for (int i = 0; i < NUMLEVELS; i++) {
if (myPlayer._pLvlVisited[i])
l = i;
}
}
l = std::clamp(l + 2, 6, 16);
SpawnSmith(l);
SpawnWitch(l);
SpawnHealer(l);
SpawnBoy(myPlayer.getCharacterLevel());
SpawnPremium(myPlayer);
}
void FreeStoreMem()
{
if (*GetOptions().Gameplay.showItemGraphicsInStores) {
FreeHalfSizeItemSprites();
}
ActiveStore = TalkID::None;
for (STextStruct &entry : TextLine) {
entry.text.clear();
entry.text.shrink_to_fit();
}
}
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;
int sx = uiPosition.x + 32 + margin;
if (!IsTextFullSize) {
sx += 320;
}
const int sy = uiPosition.y + PaddingTop + TextLine[line].y + TextLine[line]._syoff;
int width = IsTextFullSize ? 575 : 255;
if (HasScrollbar && line >= 4 && line <= 20) {
width -= 9; // Space for the selector
}
width -= margin * 2;
const Rectangle rect { { sx, sy }, { width, 0 } };
// Space reserved for item graphic is based on the size of 2x3 cursor sprites
constexpr int CursWidth = INV_SLOT_SIZE_PX * 2;
constexpr int HalfCursWidth = CursWidth / 2;
if (*GetOptions().Gameplay.showItemGraphicsInStores && cursId >= 0) {
const Size size = GetInvItemSize(static_cast<int>(CURSOR_FIRSTITEM) + cursId);
const bool useHalfSize = size.width > INV_SLOT_SIZE_PX || size.height > INV_SLOT_SIZE_PX;
const bool useRed = HasAnyOf(flags, UiFlags::ColorRed);
const ClxSprite sprite = useHalfSize
? (useRed ? GetHalfSizeItemSpriteRed(cursId) : GetHalfSizeItemSprite(cursId))
: GetInvItemSprite(static_cast<int>(CURSOR_FIRSTITEM) + cursId);
const Point position {
rect.position.x + (HalfCursWidth - sprite.width()) / 2,
rect.position.y + (TextHeight() * 3 + sprite.height()) / 2
};
if (useHalfSize || !useRed) {
ClxDraw(out, position, sprite);
} else {
ClxDrawTRN(out, position, sprite, GetInfravisionTRN());
}
}
if (*GetOptions().Gameplay.showItemGraphicsInStores && cursIndent) {
const Rectangle textRect { { rect.position.x + HalfCursWidth + 8, rect.position.y }, { rect.size.width - HalfCursWidth + 8, rect.size.height } };
DrawString(out, text, textRect, { .flags = flags });
} else {
DrawString(out, text, rect, { .flags = flags });
}
if (price > 0)
DrawString(out, FormatInteger(price), rect, { .flags = flags | UiFlags::AlignRight });
if (CurrentTextLine == line) {
DrawSelector(out, rect, text, flags);
}
}
void DrawSLine(const Surface &out, int sy)
{
const Point uiPosition = GetUIRectangle().position;
int sx = 26;
int width = 587;
if (!IsTextFullSize) {
sx += SidePanelSize.width;
width -= SidePanelSize.width;
}
uint8_t *src = out.at(uiPosition.x + sx, uiPosition.y + 25);
uint8_t *dst = out.at(uiPosition.x + sx, sy);
for (int i = 0; i < 3; i++, src += out.pitch(), dst += out.pitch())
memcpy(dst, src, width);
}
void DrawSTextHelp()
{
CurrentTextLine = -1;
IsTextFullSize = true;
}
void ClearSText(int s, int e)
{
for (int i = s; i < e; i++) {
TextLine[i]._sx = 0;
TextLine[i]._syoff = 0;
TextLine[i].text.clear();
TextLine[i].text.shrink_to_fit();
TextLine[i].flags = UiFlags::None;
TextLine[i].type = STextStruct::Label;
TextLine[i]._sval = 0;
}
}
void StartStore(TalkID s)
{
if (*GetOptions().Gameplay.showItemGraphicsInStores) {
CreateHalfSizeItemSprites();
}
SpellbookFlag = false;
CloseInventory();
CloseCharPanel();
RenderGold = false;
QuestLogIsOpen = false;
CloseGoldDrop();
ClearSText(0, NumStoreLines);
ReleaseStoreBtn();
// Fire StoreOpened Lua event for main store entries
switch (s) {
case TalkID::Smith:
lua::StoreOpened("griswold");
break;
case TalkID::Witch:
lua::StoreOpened("adria");
break;
case TalkID::Boy:
lua::StoreOpened("wirt");
break;
case TalkID::Healer:
lua::StoreOpened("pepin");
break;
case TalkID::Storyteller:
lua::StoreOpened("cain");
break;
case TalkID::Tavern:
lua::StoreOpened("ogden");
break;
case TalkID::Drunk:
lua::StoreOpened("farnham");
break;
case TalkID::Barmaid:
lua::StoreOpened("gillian");
break;
default:
break;
}
switch (s) {
case TalkID::Smith:
StartSmith();
break;
case TalkID::SmithBuy: {
if (!SmithItems.empty())
StartSmithBuy();
else {
ActiveStore = TalkID::SmithBuy;
OldTextLine = 12;
StoreESC();
return;
}
break;
}
case TalkID::SmithSell:
StartSmithSell();
break;
case TalkID::SmithRepair:
StartSmithRepair();
break;
case TalkID::Witch:
StartWitch();
break;
case TalkID::WitchBuy:
if (CurrentItemIndex > 0)
StartWitchBuy();
break;
case TalkID::WitchSell:
StartWitchSell();
break;
case TalkID::WitchRecharge:
StartWitchRecharge();
break;
case TalkID::NoMoney:
StoreNoMoney();
break;
case TalkID::NoRoom:
StoreNoRoom();
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::StorytellerIdentifyAll:
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();
break;
case TalkID::None:
break;
}
CurrentTextLine = -1;
for (int i = 0; i < NumStoreLines; i++) {
if (TextLine[i].isSelectable()) {
CurrentTextLine = i;
break;
}
}
ActiveStore = s;
}
void DrawSText(const Surface &out)
{
if (!IsTextFullSize)
DrawSTextBack(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;
}
}
CalculateLineHeights();
const Point uiPosition = GetUIRectangle().position;
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())
PrintSString(out, TextLine[i]._sx, i, TextLine[i].text, TextLine[i].flags, TextLine[i]._sval, TextLine[i].cursId, TextLine[i].cursIndent);
}
if (RenderGold) {
PrintSString(out, 28, 1, fmt::format(fmt::runtime(_("Your gold: {:s}")), FormatInteger(TotalPlayerGold())).c_str(), UiFlags::ColorWhitegold | UiFlags::AlignRight);
}
if (HasScrollbar)
DrawSSlider(out, 4, 20);
}
void StoreESC()
{
if (qtextflag) {
qtextflag = false;
if (leveltype == DTYPE_TOWN)
stream_stop();
return;
}
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::Gossip:
StartStore(OldActiveStore);
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::StorytellerIdentifyAll:
StartStore(TalkID::Storyteller);
CurrentTextLine = 16;
break;
case TalkID::StorytellerIdentifyShow:
StartStore(TalkID::StorytellerIdentify);
break;
case TalkID::NoMoney:
case TalkID::NoRoom:
case TalkID::Confirm:
RestoreStoreFromOldState();
break;
case TalkID::None:
break;
}
}
void StoreUp()
{
PlaySFX(SfxID::MenuMove);
if (CurrentTextLine == -1) {
return;
}
if (HasScrollbar) {
if (CurrentTextLine == PreviousScrollPos) {
if (ScrollPos != 0)
ScrollPos--;
return;
}
CurrentTextLine--;
while (!TextLine[CurrentTextLine].isSelectable()) {
if (CurrentTextLine == 0)
CurrentTextLine = NumStoreLines - 1;
else
CurrentTextLine--;
}
return;
}
if (CurrentTextLine == 0)
CurrentTextLine = NumStoreLines - 1;
else
CurrentTextLine--;
while (!TextLine[CurrentTextLine].isSelectable()) {
if (CurrentTextLine == 0)
CurrentTextLine = NumStoreLines - 1;
else
CurrentTextLine--;
}
}
void StoreDown()
{
PlaySFX(SfxID::MenuMove);
if (CurrentTextLine == -1) {
return;
}
if (HasScrollbar) {
if (CurrentTextLine == NextScrollPos) {
if (ScrollPos < NumTextLines)
ScrollPos++;
return;
}
CurrentTextLine++;
while (!TextLine[CurrentTextLine].isSelectable()) {
if (CurrentTextLine == NumStoreLines - 1)
CurrentTextLine = 0;
else
CurrentTextLine++;
}
return;
}
if (CurrentTextLine == NumStoreLines - 1)
CurrentTextLine = 0;
else
CurrentTextLine++;
while (!TextLine[CurrentTextLine].isSelectable()) {
if (CurrentTextLine == NumStoreLines - 1)
CurrentTextLine = 0;
else
CurrentTextLine++;
}
}
void StorePrior()
{
PlaySFX(SfxID::MenuMove);
if (CurrentTextLine != -1 && HasScrollbar) {
if (CurrentTextLine == PreviousScrollPos) {
ScrollPos = std::max(ScrollPos - 4, 0);
} else {
CurrentTextLine = PreviousScrollPos;
}
}
}
void StoreNext()
{
PlaySFX(SfxID::MenuMove);
if (CurrentTextLine != -1 && HasScrollbar) {
if (CurrentTextLine == NextScrollPos) {
if (ScrollPos < NumTextLines)
ScrollPos += 4;
if (ScrollPos > NumTextLines)
ScrollPos = NumTextLines;
} else {
CurrentTextLine = NextScrollPos;
}
}
}
void TakePlrsMoney(int cost)
{
Player &myPlayer = *MyPlayer;
myPlayer._pGold -= std::min(cost, myPlayer._pGold);
cost = TakeGold(myPlayer, cost, true);
if (cost != 0) {
cost = TakeGold(myPlayer, cost, false);
}
Stash.gold -= cost;
Stash.dirty = true;
}
void StoreEnter()
{
if (qtextflag) {
qtextflag = false;
if (leveltype == DTYPE_TOWN)
stream_stop();
return;
}
PlaySFX(SfxID::MenuSelect);
switch (ActiveStore) {
case TalkID::Smith:
SmithEnter();
break;
case TalkID::SmithPremiumBuy:
SmithPremiumBuyEnter();
break;
case TalkID::SmithBuy:
SmithBuyEnter();
break;
case TalkID::SmithSell:
SmithSellEnter();
break;
case TalkID::SmithRepair:
SmithRepairEnter();
break;
case TalkID::Witch:
WitchEnter();
break;
case TalkID::WitchBuy:
WitchBuyEnter();
break;
case TalkID::WitchSell:
WitchSellEnter();
break;
case TalkID::WitchRecharge:
WitchRechargeEnter();
break;
case TalkID::NoMoney:
case TalkID::NoRoom:
RestoreStoreFromOldState();
break;
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();
break;
case TalkID::StorytellerIdentifyAll:
StartStore(TalkID::Confirm);
break;
case TalkID::Gossip:
TalkEnter();
break;
case TalkID::StorytellerIdentifyShow:
StartStore(TalkID::StorytellerIdentify);
break;
case TalkID::Drunk:
DrunkEnter();
break;
case TalkID::Tavern:
TavernEnter();
break;
case TalkID::Barmaid:
BarmaidEnter();
break;
case TalkID::None:
break;
}
}
void CheckStoreBtn()
{
const Point uiPosition = GetUIRectangle().position;
const Rectangle windowRect { { uiPosition.x + 344, uiPosition.y + PaddingTop - 7 }, { 271, 303 } };
const Rectangle windowRectFull { { uiPosition.x + 24, uiPosition.y + PaddingTop - 7 }, { 591, 303 } };
if (!IsTextFullSize) {
if (!windowRect.contains(MousePosition)) {
while (IsPlayerInStore())
StoreESC();
}
} else {
if (!windowRectFull.contains(MousePosition)) {
while (IsPlayerInStore())
StoreESC();
}
}
if (qtextflag) {
qtextflag = false;
if (leveltype == DTYPE_TOWN)
stream_stop();
} else if (CurrentTextLine != -1) {
const int relativeY = MousePosition.y - (uiPosition.y + PaddingTop);
if (HasScrollbar && MousePosition.x > 600 + uiPosition.x) {
// Scroll bar is always measured in terms of the small line height.
const int y = relativeY / SmallLineHeight;
if (y == 4) {
if (CountdownScrollUp <= 0) {
StoreUp();
CountdownScrollUp = 10;
} else {
CountdownScrollUp--;
}
}
if (y == 20) {
if (CountdownScrollDown <= 0) {
StoreDown();
CountdownScrollDown = 10;
} else {
CountdownScrollDown--;
}
}
return;
}
int y = relativeY / LineHeight();
// Large small fonts draw beyond LineHeight. Check if the click was on the overflow text.
if (IsSmallFontTall() && y > 0 && y < NumStoreLines
&& TextLine[y - 1].hasText() && !TextLine[y].hasText()
&& relativeY < TextLine[y - 1].y + LargeTextHeight) {
--y;
}
if (y >= 5) {
if (y >= BackButtonLine() + 1)
y = BackButtonLine();
if (HasScrollbar && 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())) {
CurrentTextLine = y;
StoreEnter();
}
}
}
}
void ReleaseStoreBtn()
{
CountdownScrollUp = -1;
CountdownScrollDown = -1;
}
bool IsPlayerInStore()
{
return ActiveStore != TalkID::None;
}
} // namespace devilution