#include "StarMerchantInterface.hpp" #include "StarJsonExtra.hpp" #include "StarGuiReader.hpp" #include "StarLexicalCast.hpp" #include "StarRoot.hpp" #include "StarItemTooltip.hpp" #include "StarPlayer.hpp" #include "StarWorldClient.hpp" #include "StarButtonWidget.hpp" #include "StarLabelWidget.hpp" #include "StarTextBoxWidget.hpp" #include "StarImageWidget.hpp" #include "StarItemGridWidget.hpp" #include "StarListWidget.hpp" #include "StarTabSet.hpp" #include "StarAssets.hpp" #include "StarItemDatabase.hpp" #include "StarPlayerInventory.hpp" #include "StarItemBag.hpp" #include "StarQuestManager.hpp" namespace Star { MerchantPane::MerchantPane( WorldClientPtr worldClient, PlayerPtr player, Json const& settings, EntityId sourceEntityId) { m_worldClient = move(worldClient); m_player = move(player); m_sourceEntityId = sourceEntityId; auto assets = Root::singleton().assets(); auto baseConfig = settings.get("config", "/interface/windowconfig/merchant.config"); m_settings = jsonMerge(assets->fetchJson(baseConfig), settings); m_refreshTimer = GameTimer(assets->json("/merchant.config:autoRefreshRate").toFloat()); m_buyFactor = m_settings.getFloat("buyFactor", assets->json("/merchant.config:defaultBuyFactor").toFloat()); m_sellFactor = m_settings.getFloat("sellFactor", assets->json("/merchant.config:defaultSellFactor").toFloat()); m_itemBag = make_shared(m_settings.getUInt("sellContainerSize")); GuiReader reader; reader.registerCallback("spinCount.up", [=](Widget*) { if (m_selectedIndex != NPos) { if (m_buyCount < maxBuyCount()) m_buyCount++; else m_buyCount = 1; } else { m_buyCount = 0; } countChanged(); }); reader.registerCallback("spinCount.down", [=](Widget*) { if (m_selectedIndex != NPos) { if (m_buyCount > 1) m_buyCount--; else m_buyCount = std::max(maxBuyCount(), 1); } else { m_buyCount = 0; } countChanged(); }); reader.registerCallback("countChanged", [=](Widget*) { countChanged(); }); reader.registerCallback("parseCountText", [=](Widget*) { countTextChanged(); }); reader.registerCallback("buy", [=](Widget*) { buy(); }); reader.registerCallback("sell", [=](Widget*) { sell(); }); reader.registerCallback("close", [=](Widget*) { dismiss(); }); reader.registerCallback("itemGrid", [=](Widget*) { swapSlot(); updateSellTotal(); }); Json paneLayout = m_settings.get("paneLayout"); paneLayout = jsonMerge(paneLayout, m_settings.get("paneLayoutOverride", {})); reader.construct(paneLayout, this); m_tabSet = findChild("buySellTabs"); m_tabSet->setCallback([this](Widget*) { auto bgResult = getBG(); if (m_tabSet->selectedTab() == 0) bgResult.body = m_settings.getString("buyBody"); else bgResult.body = m_settings.getString("sellBody"); setBG(bgResult); }); m_itemGuiList = findChild("itemList"); m_countTextBox = findChild("tbCount"); m_buyTotalLabel = findChild("lblBuyTotal"); m_buyButton = findChild("btnBuy"); m_sellTotalLabel = findChild("lblSellTotal"); m_sellButton = findChild("btnSell"); m_itemGrid = findChild("itemGrid"); m_itemGrid->setItemBag(m_itemBag); buildItemList(); updateSelection(); updateSellTotal(); } void MerchantPane::displayed() { Pane::displayed(); } void MerchantPane::dismissed() { Pane::dismissed(); for (auto unsold : m_itemBag->takeAll()) m_player->giveItem(unsold); m_worldClient->sendEntityMessage(m_sourceEntityId, "onMerchantClosed"); } PanePtr MerchantPane::createTooltip(Vec2I const& screenPosition) { if (m_tabSet->selectedTab() == 0) { for (size_t i = 0; i < m_itemGuiList->numChildren(); ++i) { auto entry = m_itemGuiList->itemAt(i); if (entry->getChildAt(screenPosition)) { auto itemConfig = m_itemList.get(i); ItemPtr item = Root::singleton().itemDatabase()->item(ItemDescriptor(itemConfig.get("item"))); return ItemTooltipBuilder::buildItemTooltip(item, m_player); } } } else { if (auto item = m_itemGrid->itemAt(screenPosition)) return ItemTooltipBuilder::buildItemTooltip(item, m_player); } return {}; } void MerchantPane::update(float dt) { Pane::update(dt); if (!m_worldClient->playerCanReachEntity(m_sourceEntityId)) dismiss(); if (m_refreshTimer.wrapTick()) { for (size_t i = 0; i < m_itemList.size(); ++i) { auto itemConfig = m_itemList.get(i); auto itemWidget = m_itemGuiList->itemAt(i); setupWidget(itemWidget, itemConfig); } updateBuyTotal(); } updateSelection(); m_itemGrid->updateAllItemSlots(); } EntityId MerchantPane::sourceEntityId() const { return m_sourceEntityId; } ItemPtr MerchantPane::addItems(ItemPtr const& items) { if (m_tabSet->selectedTab() == 1) { auto remainder = m_itemBag->addItems(items); updateSellTotal(); return remainder; } else { return items; } } void MerchantPane::swapSlot() { ItemPtr source = m_player->inventory()->swapSlotItem(); auto inv = m_player->inventory(); if (context()->shiftHeld()) { if (m_itemGrid->selectedItem()) { auto remainder = inv->addItems(m_itemBag->takeItems(m_itemGrid->selectedIndex())); if (remainder && !remainder->empty()) m_itemBag->setItem(m_itemGrid->selectedIndex(), remainder); } } else { if (auto heldItem = m_player->inventory()->swapSlotItem()) inv->setSwapSlotItem(m_itemBag->swapItems(m_itemGrid->selectedIndex(), heldItem)); else inv->setSwapSlotItem(m_itemBag->takeItems(m_itemGrid->selectedIndex())); } } void MerchantPane::buildItemList() { m_itemGuiList->clear(); m_itemList = m_settings.getArray("items"); auto itemDatabase = Root::singleton().itemDatabase(); filter(m_itemList, [&](Json const& itemConfig) { if (!itemDatabase->hasItem(ItemDescriptor(itemConfig.get("item")).name())) return false; if (auto prerequisite = itemConfig.optString("prerequisiteQuest")) { if (!m_player->questManager()->hasCompleted(*prerequisite)) return false; } if (auto quests = itemConfig.optArray("exclusiveQuests")) { for (auto quest : *quests) { if (m_player->questManager()->hasQuest(quest.toString())) return false; } } if (auto prerequisite = itemConfig.optUInt("prerequisiteShipLevel")) { if (m_player->shipUpgrades().shipLevel < *prerequisite) return false; } if (auto maxLevel = itemConfig.optUInt("maxShipLevel")) { if (m_player->shipUpgrades().shipLevel > *maxLevel) return false; } return true; }); for (auto itemConfig : m_itemList) { auto widget = m_itemGuiList->addItem(); setupWidget(widget, itemConfig); } } void MerchantPane::setupWidget(WidgetPtr const& widget, Json const& itemConfig) { auto& root = Root::singleton(); auto assets = root.assets(); ItemPtr item = root.itemDatabase()->item(ItemDescriptor(itemConfig.get("item"))); String name = item->friendlyName(); if (item->count() > 1) name = strf("{} (x{})", name, item->count()); auto itemName = widget->fetchChild("itemName"); itemName->setText(name); unsigned price = ceil(itemConfig.getInt("price", item->price()) * m_buyFactor); widget->setLabel("priceLabel", toString(price)); widget->setData(price); bool unavailable = price > m_player->currency("money"); auto unavailableoverlay = widget->fetchChild("unavailableoverlay"); if (unavailable) { itemName->setColor(Color::Gray); unavailableoverlay->show(); } else { itemName->setColor(Color::White); unavailableoverlay->hide(); } widget->fetchChild("itemIcon")->setItem(item); widget->show(); } void MerchantPane::updateSelection() { if (m_selectedIndex != m_itemGuiList->selectedItem()) { m_selectedIndex = m_itemGuiList->selectedItem(); if (m_selectedIndex != NPos) { auto itemConfig = m_itemList.get(m_selectedIndex); m_selectedItem = Root::singleton().itemDatabase()->item(ItemDescriptor(itemConfig.get("item"))); findChild("spinCount.up")->enable(); findChild("spinCount.down")->enable(); m_countTextBox->setColor(Color::White); m_buyCount = 1; } else { findChild("spinCount.up")->disable(); findChild("spinCount.down")->disable(); m_countTextBox->setColor(Color::Gray); m_buyCount = 0; } countChanged(); } } void MerchantPane::updateBuyTotal() { if (auto selected = m_itemGuiList->selectedWidget()) m_buyTotal = selected->data().toUInt() * m_buyCount; else m_buyTotal = 0; m_buyTotalLabel->setText(toString(m_buyTotal)); if (m_selectedIndex != NPos && m_buyCount > 0) m_buyButton->enable(); else m_buyButton->disable(); if (m_buyTotal > (int)m_player->inventory()->currency("money")) { m_buyTotalLabel->setColor(Color::Red); m_buyButton->disable(); } else { m_buyTotalLabel->setColor(Color::White); } } void MerchantPane::buy() { if (m_buyTotal > 0 && m_player->inventory()->consumeCurrency("money", m_buyTotal)) { auto countRemaining = m_buyCount; while (countRemaining > 0) { auto buyItem = m_selectedItem->clone(); buyItem->setCount(m_selectedItem->count() * countRemaining); countRemaining -= buyItem->count(); m_player->giveItem(buyItem); } auto reportItem = m_selectedItem->clone(); reportItem->setCount(reportItem->count() * m_buyCount, true); auto buySummary = JsonObject{{"item", reportItem->descriptor().toJson()}, {"total", m_buyTotal}}; m_worldClient->sendEntityMessage(m_sourceEntityId, "onBuy", {buySummary}); auto& guiContext = GuiContext::singleton(); guiContext.playAudio(Root::singleton().assets()->json("/merchant.config:buySound").toString()); buildItemList(); updateBuyTotal(); } } void MerchantPane::updateSellTotal() { m_sellTotal = 0; for (auto item : m_itemBag->items()) { if (item) m_sellTotal += round(item->price() * m_sellFactor); } m_sellTotalLabel->setText(toString(m_sellTotal)); if (m_sellTotal > 0) m_sellButton->enable(); else m_sellButton->disable(); } void MerchantPane::sell() { if (m_sellTotal > 0) { auto sellSummary = JsonObject{{"items", m_itemBag->toJson()}, {"total", m_sellTotal}}; m_worldClient->sendEntityMessage(m_sourceEntityId, "onSell", {sellSummary}); m_player->inventory()->addCurrency("money", m_sellTotal); m_itemBag->clearItems(); updateSellTotal(); auto& guiContext = GuiContext::singleton(); guiContext.playAudio(Root::singleton().assets()->json("/merchant.config:sellSound").toString()); } } int MerchantPane::maxBuyCount() { if (auto selected = m_itemGuiList->selectedWidget()) { auto assets = Root::singleton().assets(); auto unitPrice = selected->data().toUInt(); if (unitPrice == 0) return 1000; return min(1000, (int)floor(m_player->currency("money") / unitPrice)); } else { return 0; } } void MerchantPane::countChanged() { m_countTextBox->setText(strf("x{}", m_buyCount)); updateBuyTotal(); } void MerchantPane::countTextChanged() { if (m_selectedIndex == NPos) { m_buyCount = 0; countChanged(); } else { try { auto countString = m_countTextBox->getText().replace("x", ""); if (countString.size()) { m_buyCount = clamp(lexicalCast(countString), 1, maxBuyCount()); countChanged(); } } catch (BadLexicalCast const&) { m_buyCount = 1; countChanged(); } } } }