osb/source/frontend/StarInventory.cpp
Kae 888cde79ef feat: middle click objects in the inventory to open their interface
makes carrying around shop objects easier

might need to restrict the allowed interaction types more, as some may break due to the source entity being the player
2024-03-27 16:00:13 +11:00

482 lines
17 KiB
C++

#include "StarInventory.hpp"
#include "StarGuiReader.hpp"
#include "StarItemTooltip.hpp"
#include "StarSimpleTooltip.hpp"
#include "StarRoot.hpp"
#include "StarUniverseClient.hpp"
#include "StarItemGridWidget.hpp"
#include "StarButtonWidget.hpp"
#include "StarPortraitWidget.hpp"
#include "StarPaneManager.hpp"
#include "StarLabelWidget.hpp"
#include "StarImageWidget.hpp"
#include "StarPlayerInventory.hpp"
#include "StarPlayerCompanions.hpp"
#include "StarWorldClient.hpp"
#include "StarAssets.hpp"
#include "StarItem.hpp"
#include "StarMainInterface.hpp"
#include "StarMerchantInterface.hpp"
#include "StarJsonExtra.hpp"
#include "StarStatistics.hpp"
#include "StarAugmentItem.hpp"
#include "StarObjectItem.hpp"
#include "StarInteractionTypes.hpp"
namespace Star {
InventoryPane::InventoryPane(MainInterface* parent, PlayerPtr player, ContainerInteractorPtr containerInteractor) {
m_parent = parent;
m_player = std::move(player);
m_containerInteractor = std::move(containerInteractor);
GuiReader invWindowReader;
auto config = Root::singleton().assets()->json("/interface/windowconfig/playerinventory.config");
auto leftClickCallback = [this](String const& bagType, Widget* widget) {
auto itemGrid = convert<ItemGridWidget>(widget);
InventorySlot inventorySlot = BagSlot(bagType, itemGrid->selectedIndex());
auto inventory = m_player->inventory();
if (context()->shiftHeld()) {
if (auto sourceItem = itemGrid->selectedItem()) {
if (auto activeMerchantPane = m_parent->activeMerchantPane()) {
auto remainder = activeMerchantPane->addItems(inventory->takeSlot(inventorySlot));
if (remainder && !remainder->empty())
inventory->setItem(inventorySlot, remainder);
} else if (m_containerInteractor->containerOpen()) {
inventory->takeSlot(inventorySlot);
m_containerInteractor->addToContainer(sourceItem);
m_containerSource = inventorySlot;
m_expectingSwap = true;
}
}
} else {
inventory->shiftSwap(inventorySlot);
}
};
auto rightClickCallback = [this](InventorySlot slot) {
auto inventory = m_player->inventory();
if (ItemPtr slotItem = inventory->itemsAt(slot)) {
auto swapItem = inventory->swapSlotItem();
if (!swapItem || swapItem->empty() || swapItem->couldStack(slotItem)) {
uint64_t count = swapItem ? swapItem->couldStack(slotItem) : slotItem->maxStack();
if (context()->shiftHeld())
count = max(1, min<int>(count, slotItem->count() / 2));
else
count = 1;
if (auto taken = slotItem->take(count)) {
if (swapItem)
swapItem->stackWith(taken);
else
inventory->setSwapSlotItem(taken);
}
} else if (auto augment = as<AugmentItem>(swapItem)) {
if (auto augmented = augment->applyTo(slotItem))
inventory->setItem(slot, augmented);
}
}
else if (auto swapSlot = inventory->swapSlotItem()) {
if (auto es = slot.ptr<EquipmentSlot>()) {
if (inventory->itemAllowedAsEquipment(swapSlot, *es))
inventory->setItem(slot, swapSlot->take(1));
} else if (slot.is<TrashSlot>()) {
inventory->setItem(slot, swapSlot->take(1));
} else if (auto bs = slot.ptr<BagSlot>()) {
if (inventory->itemAllowedInBag(swapSlot, bs->first))
inventory->setItem(slot, swapSlot->take(1));
}
}
};
auto bagGridCallback = [rightClickCallback](String const& bagType, Widget* widget) {
auto slot = BagSlot(bagType, convert<ItemGridWidget>(widget)->selectedIndex());
rightClickCallback(slot);
};
auto middleClickCallback = [this](String const& bagType, Widget* widget) {
if (!m_player->inWorld())
return;
auto itemGrid = convert<ItemGridWidget>(widget);
InventorySlot inventorySlot = BagSlot(bagType, itemGrid->selectedIndex());
auto inventory = m_player->inventory();
if (auto sourceItem = as<ObjectItem>(itemGrid->selectedItem())) {
if (auto actionTypeName = sourceItem->instanceValue("interactAction")) {
auto actionType = InteractActionTypeNames.getLeft(actionTypeName.toString());
if (actionType >= InteractActionType::OpenCraftingInterface && actionType <= InteractActionType::ScriptPane) {
auto actionData = sourceItem->instanceValue("interactData", Json());
if (actionData.isType(Json::Type::Object))
actionData = actionData.set("openWithInventory", false);
InteractAction action(actionType, m_player->entityId(), actionData);
m_player->interact(action);
}
}
}
};
Json itemBagConfig = config.get("bagConfig");
auto bagOrder = itemBagConfig.toObject().keys().sorted([&itemBagConfig](String const& a, String const& b) {
return itemBagConfig.get(a).getInt("order", 0) < itemBagConfig.get(b).getInt("order", 0);
});
for (auto name : bagOrder) {
auto itemGrid = itemBagConfig.get(name).getString("itemGrid");
invWindowReader.registerCallback(itemGrid, bind(leftClickCallback, name, _1));
invWindowReader.registerCallback(strf("{}.right", itemGrid), bind(bagGridCallback, name, _1));
invWindowReader.registerCallback(strf("{}.middle", itemGrid), bind(middleClickCallback, name, _1));
}
invWindowReader.registerCallback("close", [=](Widget*) {
dismiss();
});
invWindowReader.registerCallback("sort", [=](Widget*) {
m_player->inventory()->condenseBagStacks(m_selectedTab);
m_player->inventory()->sortBag(m_selectedTab);
// Don't show sorted items as new items
m_itemGrids[m_selectedTab]->updateItemState();
m_itemGrids[m_selectedTab]->clearChangedSlots();
});
invWindowReader.registerCallback("gridModeSelector", [=](Widget* widget) {
auto selected = convert<ButtonWidget>(widget)->data().toString();
selectTab(m_tabButtonData.keyOf(selected));
});
auto registerSlotCallbacks = [&](String name, InventorySlot slot) {
invWindowReader.registerCallback(name, [=](Widget* paneObj) {
if (as<ItemSlotWidget>(paneObj))
m_player->inventory()->shiftSwap(slot);
else
throw GuiException("Invalid object type, expected ItemSlotWidget");
});
invWindowReader.registerCallback(name + ".right", [=](Widget* paneObj) {
if (as<ItemSlotWidget>(paneObj))
rightClickCallback(slot);
else
throw GuiException("Invalid object type, expected ItemSlotWidget");
});
};
for (auto const p : EquipmentSlotNames)
registerSlotCallbacks(p.second, p.first);
registerSlotCallbacks("trash", TrashSlot());
invWindowReader.construct(config.get("paneLayout"), this);
m_trashSlot = fetchChild<ItemSlotWidget>("trash");
m_trashBurn = GameTimer(config.get("trashBurnTimeout").toFloat());
m_disabledTechOverlays.append(fetchChild<ImageWidget>("techHeadDisabled"));
m_disabledTechOverlays.append(fetchChild<ImageWidget>("techBodyDisabled"));
m_disabledTechOverlays.append(fetchChild<ImageWidget>("techLegsDisabled"));
for (auto const p : EquipmentSlotNames) {
if (auto itemSlot = fetchChild<ItemSlotWidget>(p.second))
itemSlot->setItem(m_player->inventory()->itemsAt(p.first));
}
for (auto name : bagOrder) {
auto itemTab = itemBagConfig.get(name);
m_itemGrids[name] = fetchChild<ItemGridWidget>(itemTab.getString("itemGrid"));
m_itemGrids[name]->setItemBag(m_player->inventory()->bagContents(name));
m_itemGrids[name]->hide();
m_newItemMarkers[name] = fetchChild<Widget>(itemTab.getString("newItemMarker"));
m_tabButtonData[name] = itemTab.getString("tabButtonData");
}
selectTab(bagOrder[0]);
auto centralPortrait = fetchChild<PortraitWidget>("portrait");
centralPortrait->setEntity(m_player);
auto portrait = make_shared<PortraitWidget>(m_player, PortraitMode::Bust);
portrait->setIconMode();
setTitle(portrait, m_player->name(), config.getString("subtitle"));
m_expectingSwap = false;
if (auto item = m_player->inventory()->swapSlotItem())
m_currentSwapSlotItem = item->descriptor();
m_pickUpSounds = jsonToStringList(config.get("sounds").get("pickup"));
m_putDownSounds = jsonToStringList(config.get("sounds").get("putdown"));
m_someUpSounds = jsonToStringList(config.get("sounds").get("someup"));
m_someDownSounds = jsonToStringList(config.get("sounds").get("somedown"));
}
void InventoryPane::displayed() {
Pane::displayed();
m_expectingSwap = false;
for (auto grid : m_itemGrids)
grid.second->updateItemState();
m_itemGrids[m_selectedTab]->indicateChangedSlots();
}
PanePtr InventoryPane::createTooltip(Vec2I const& screenPosition) {
ItemPtr item;
if (auto child = getChildAt(screenPosition)) {
if (auto itemSlot = as<ItemSlotWidget>(child)) {
item = itemSlot->item();
if (!item) {
auto widgetData = itemSlot->data();
if (widgetData && widgetData.type() == Json::Type::Object) {
if (auto text = widgetData.optString("tooltipText"))
return SimpleTooltipBuilder::buildTooltip(*text);
}
}
}
if (auto itemGrid = as<ItemGridWidget>(child))
item = itemGrid->itemAt(screenPosition);
}
if (item)
return ItemTooltipBuilder::buildItemTooltip(item, m_player);
auto techDatabase = Root::singleton().techDatabase();
for (auto const& p : TechTypeNames) {
if (auto techIcon = fetchChild<ImageWidget>(strf("tech{}", p.second))) {
if (techIcon->screenBoundRect().contains(screenPosition)) {
if (auto techModule = m_player->techs()->equippedTechs().maybe(p.first))
return SimpleTooltipBuilder::buildTooltip(techDatabase->tech(*techModule).description);
}
}
}
return {};
}
bool InventoryPane::giveContainerResult(ContainerResult result) {
if (!m_expectingSwap)
return false;
for (auto& item : result) {
auto inv = m_player->inventory();
m_player->triggerPickupEvents(item);
auto remainder = inv->stackWith(m_containerSource, item);
if (remainder && !remainder->empty())
m_player->giveItem(remainder);
}
m_expectingSwap = false;
return true;
}
void InventoryPane::updateItems() {
for (auto& p : m_itemGrids)
p.second->updateItemState();
}
bool InventoryPane::containsNewItems() const {
for (auto& p : m_itemGrids) {
if (p.second->slotsChanged())
return true;
}
return false;
}
void InventoryPane::clearChangedSlots() {
for (auto& p : m_itemGrids) {
p.second->updateItemState();
p.second->clearChangedSlots();
}
}
void InventoryPane::update(float dt) {
auto inventory = m_player->inventory();
auto context = Widget::context();
HashSet<ItemPtr> customBarItems;
for (uint8_t i = 0; i < inventory->customBarIndexes(); ++i) {
if (auto primarySlot = inventory->customBarPrimarySlot(i)) {
if (auto primaryItem = inventory->itemsAt(*primarySlot))
customBarItems.add(primaryItem);
}
if (auto secondarySlot = inventory->customBarSecondarySlot(i)) {
if (auto secondaryItem = inventory->itemsAt(*secondarySlot))
customBarItems.add(secondaryItem);
}
}
m_trashSlot->setItem(inventory->itemsAt(TrashSlot()));
m_trashSlot->showLinkIndicator(customBarItems.contains(m_trashSlot->item()));
if (auto trashItem = m_trashSlot->item()) {
if (m_trashBurn.tick(dt) && trashItem->count() > 0) {
m_player->statistics()->recordEvent("trashItem", JsonObject{
{"itemName", trashItem->name()},
{"count", trashItem->count()},
{"category", trashItem->category()}
});
trashItem->take(trashItem->count());
}
} else {
m_trashBurn.reset();
}
m_trashSlot->setProgress(m_trashBurn.timer / m_trashBurn.time);
for (auto const& p : EquipmentSlotNames) {
if (auto itemSlot = fetchChild<ItemSlotWidget>(p.second)) {
itemSlot->setItem(inventory->itemsAt(p.first));
itemSlot->showLinkIndicator(customBarItems.contains(itemSlot->item()));
}
}
auto techDatabase = Root::singleton().techDatabase();
for (auto const& p : TechTypeNames) {
if (auto techIcon = fetchChild<ImageWidget>(strf("tech{}", p.second))) {
if (auto techModule = m_player->techs()->equippedTechs().maybe(p.first))
techIcon->setImage(techDatabase->tech(*techModule).icon);
else
techIcon->setImage("");
}
}
if (ItemPtr swapSlot = inventory->swapSlotItem()) {
if (!PlayerInventory::itemAllowedInBag(swapSlot, m_selectedTab)) {
for (auto& pair : m_itemGrids) {
if (pair.first != m_selectedTab && PlayerInventory::itemAllowedInBag(swapSlot, pair.first)) {
selectTab(pair.first);
break;
}
}
}
}
for (auto p : m_itemGrids) {
p.second->updateItemState();
for (size_t i = 0; i < p.second->itemSlots(); ++i) {
auto itemWidget = p.second->itemWidgetAt(i);
itemWidget->showLinkIndicator(customBarItems.contains(itemWidget->item()));
}
}
m_itemGrids[m_selectedTab]->clearChangedSlots();
for (auto& pair : m_newItemMarkers) {
if (m_itemGrids[pair.first]->slotsChanged())
pair.second->show();
else
pair.second->hide();
}
for (auto& techOverlay : m_disabledTechOverlays)
techOverlay->setVisibility(m_player->techOverridden());
auto healthLabel = fetchChild<LabelWidget>("healthtext");
healthLabel->setText(toString(m_player->maxHealth()));
auto energyLabel = fetchChild<LabelWidget>("energytext");
energyLabel->setText(toString(m_player->maxEnergy()));
auto weaponLabel = fetchChild<LabelWidget>("weapontext");
weaponLabel->setText(strf("{}%", ceil(m_player->powerMultiplier() * 100)));
auto defenseLabel = fetchChild<LabelWidget>("defensetext");
if (m_player->protection() == 0)
defenseLabel->setText("--");
else
defenseLabel->setText(toString(ceil(m_player->protection())));
auto moneyLabel = fetchChild<LabelWidget>("lblMoney");
moneyLabel->setText(toString(m_player->currency("money")));
if (m_player->currency("essence") > 0) {
fetchChild<ImageWidget>("imgEssenceIcon")->show();
auto essenceLabel = fetchChild<LabelWidget>("lblEssence");
essenceLabel->show();
essenceLabel->setText(toString(m_player->currency("essence")));
} else {
fetchChild<ImageWidget>("imgEssenceIcon")->hide();
fetchChild<LabelWidget>("lblEssence")->hide();
}
auto config = Root::singleton().assets()->json("/interface/windowconfig/playerinventory.config");
auto pets = m_player->companions()->getCompanions("pets");
if (pets.size() > 0) {
auto pet = pets.first();
auto companionImage = fetchChild<ImageWidget>("companionSlot");
companionImage->setVisibility(true);
companionImage->setDrawables(pet->portrait());
auto nameLabel = fetchChild<LabelWidget>("companionName");
if (auto name = pet->name()) {
nameLabel->setText(pet->name()->toUpper());
} else {
nameLabel->setText(config.getString("defaultPetNameLabel"));
}
auto attackLabel = fetchChild<LabelWidget>("companionAttackStat");
if (auto attack = pet->stat("attack")) {
attackLabel->setText(strf("{:.0f}", *attack));
} else {
attackLabel->setText("");
}
auto defenseLabel = fetchChild<LabelWidget>("companionDefenseStat");
if (auto defense = pet->stat("defense")) {
defenseLabel->setText(strf("{:.0f}", *defense));
} else {
defenseLabel->setText("");
}
if (containsChild("companionHealthBar")) {
auto healthBar = fetchChild<ProgressWidget>("companionHealthBar");
Maybe<float> health = pet->resource("health");
Maybe<float> healthMax = pet->resourceMax("health");
if (health && healthMax) {
healthBar->setCurrentProgressLevel(*health);
healthBar->setMaxProgressLevel(*healthMax);
} else {
healthBar->setCurrentProgressLevel(0);
healthBar->setMaxProgressLevel(1);
}
}
} else {
fetchChild<ImageWidget>("companionSlot")->setVisibility(false);
fetchChild<LabelWidget>("companionName")->setText(config.getString("defaultPetNameLabel"));
fetchChild<LabelWidget>("companionAttackStat")->setText("");
fetchChild<LabelWidget>("companionDefenseStat")->setText("");
if (containsChild("companionHealthBar")) {
auto healthBar = fetchChild<ProgressWidget>("companionHealthBar");
healthBar->setCurrentProgressLevel(0);
healthBar->setMaxProgressLevel(1);
}
}
if (auto item = inventory->swapSlotItem()) {
if (!m_currentSwapSlotItem || !item->matches(*m_currentSwapSlotItem, true))
context->playAudio(RandomSource().randFrom(m_pickUpSounds));
else if (item->count() > m_currentSwapSlotItem->count())
context->playAudio(RandomSource().randFrom(m_someUpSounds));
else if (item->count() < m_currentSwapSlotItem->count())
context->playAudio(RandomSource().randFrom(m_someDownSounds));
m_currentSwapSlotItem = item->descriptor();
} else {
if (m_currentSwapSlotItem)
context->playAudio(RandomSource().randFrom(m_putDownSounds));
m_currentSwapSlotItem = {};
}
m_title = m_player->name();
Pane::update(dt);
}
void InventoryPane::selectTab(String const& selected) {
for (auto grid : m_itemGrids)
grid.second->hide();
m_selectedTab = selected;
m_itemGrids[m_selectedTab]->show();
m_itemGrids[m_selectedTab]->indicateChangedSlots();
auto tabs = fetchChild<ButtonGroupWidget>("gridModeSelector");
for (auto button : tabs->buttons())
if (button->data().toString().equalsIgnoreCase(m_tabButtonData[selected]))
tabs->select(tabs->id(button));
}
}