From e1b5dd97b861b86142de63faa0dce672a6511755 Mon Sep 17 00:00:00 2001 From: fredzio Date: Mon, 6 May 2019 06:27:38 +0200 Subject: [PATCH] Add a filter in the alchemy window. A button allow to switch between ingredient name and magic effect. Switching reset the filter. The default filter can be set in the layout file. The player can show only ingredients whose either name or effect match the filter Only effect that are known to the player (via alchemy skill) are taken into account --- AUTHORS.md | 1 + CHANGELOG.md | 1 + apps/openmw/mwgui/alchemywindow.cpp | 107 +++++++++++++++++++++- apps/openmw/mwgui/alchemywindow.hpp | 15 +++ apps/openmw/mwgui/sortfilteritemmodel.cpp | 44 ++++++++- apps/openmw/mwgui/sortfilteritemmodel.hpp | 2 + apps/openmw/mwmechanics/alchemy.cpp | 34 +++++++ apps/openmw/mwmechanics/alchemy.hpp | 2 + files/mygui/openmw_alchemy_window.layout | 18 +++- 9 files changed, 218 insertions(+), 6 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 1ec909d10..3cbe9b44b 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -74,6 +74,7 @@ Programmers Fil Krynicki (filkry) Finbar Crago (finbar-crago) Florian Weber (Florianjw) + Frédéric Chardon (fr3dz10) Gaëtan Dezeiraud (Brouilles) Gašper Sedej Gijsbert ter Horst (Ghostbird) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32255262e..f20ca90cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -264,6 +264,7 @@ Feature #5219: Impelement TestCells console command Feature #5224: Handle NiKeyframeController for NiTriShape Feature #5304: Morrowind-style bump-mapping + Feature #5314: Ingredient filter in the alchemy window Task #4686: Upgrade media decoder to a more current FFmpeg API Task #4695: Optimize Distant Terrain memory consumption Task #4789: Optimize cell transitions diff --git a/apps/openmw/mwgui/alchemywindow.cpp b/apps/openmw/mwgui/alchemywindow.cpp index ad66735bf..8dc44059f 100644 --- a/apps/openmw/mwgui/alchemywindow.cpp +++ b/apps/openmw/mwgui/alchemywindow.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -17,6 +18,7 @@ #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" +#include #include #include "inventoryitemmodel.hpp" @@ -29,6 +31,7 @@ namespace MWGui { AlchemyWindow::AlchemyWindow() : WindowBase("openmw_alchemy_window.layout") + , mModel(nullptr) , mSortModel(nullptr) , mAlchemy(new MWMechanics::Alchemy()) , mApparatus (4) @@ -50,6 +53,8 @@ namespace MWGui getWidget(mDecreaseButton, "DecreaseButton"); getWidget(mNameEdit, "NameEdit"); getWidget(mItemView, "ItemView"); + getWidget(mFilterValue, "FilterValue"); + getWidget(mFilterType, "FilterType"); mBrewCountEdit->eventValueChanged += MyGUI::newDelegate(this, &AlchemyWindow::onCountValueChanged); mBrewCountEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &AlchemyWindow::onAccept); @@ -72,6 +77,9 @@ namespace MWGui mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onCancelButtonClicked); mNameEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &AlchemyWindow::onAccept); + mFilterValue->eventComboChangePosition += MyGUI::newDelegate(this, &AlchemyWindow::onFilterChanged); + mFilterValue->eventEditTextChange += MyGUI::newDelegate(this, &AlchemyWindow::onFilterEdited); + mFilterType->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::switchFilterType); center(); } @@ -136,16 +144,110 @@ namespace MWGui removeIngredient(mIngredients[i]); } + updateFilters(); update(); } + void AlchemyWindow::initFilter() + { + auto const& wm = MWBase::Environment::get().getWindowManager(); + auto const ingredient = wm->getGameSettingString("sIngredients", "Ingredients"); + auto const effect = wm->getGameSettingString("sMagicEffects", "Magic Effects"); + + if (mFilterType->getCaption() == ingredient) + mCurrentFilter = FilterType::ByName; + else + mCurrentFilter = FilterType::ByEffect; + updateFilters(); + mFilterValue->clearIndexSelected(); + updateFilters(); + } + + void AlchemyWindow::switchFilterType(MyGUI::Widget* _sender) + { + auto const& wm = MWBase::Environment::get().getWindowManager(); + auto const ingredient = wm->getGameSettingString("sIngredients", "Ingredients"); + auto const effect = wm->getGameSettingString("sMagicEffects", "Magic Effects"); + auto *button = _sender->castType(); + + if (button->getCaption() == ingredient) + { + button->setCaption(effect); + mCurrentFilter = FilterType::ByEffect; + } + else + { + button->setCaption(ingredient); + mCurrentFilter = FilterType::ByName; + } + mSortModel->setNameFilter({}); + mSortModel->setEffectFilter({}); + mFilterValue->clearIndexSelected(); + updateFilters(); + mItemView->update(); + } + + void AlchemyWindow::updateFilters() + { + std::set itemNames, itemEffects; + for (size_t i = 0; i < mModel->getItemCount(); ++i) + { + auto const& base = mModel->getItem(i).mBase; + if (base.getTypeName() != typeid(ESM::Ingredient).name()) + continue; + + itemNames.insert(base.getClass().getName(base)); + + MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); + auto const alchemySkill = player.getClass().getSkill(player, ESM::Skill::Alchemy); + + auto const effects = MWMechanics::Alchemy::effectsDescription(base, alchemySkill); + itemEffects.insert(effects.begin(), effects.end()); + } + + mFilterValue->removeAllItems(); + auto const addItems = [&](auto const& container) + { + for (auto const& item : container) + mFilterValue->addItem(item); + }; + switch (mCurrentFilter) + { + case FilterType::ByName: addItems(itemNames); break; + case FilterType::ByEffect: addItems(itemEffects); break; + } + } + + void AlchemyWindow::applyFilter(const std::string& filter) + { + switch (mCurrentFilter) + { + case FilterType::ByName: mSortModel->setNameFilter(filter); break; + case FilterType::ByEffect: mSortModel->setEffectFilter(filter); break; + } + mItemView->update(); + } + + void AlchemyWindow::onFilterChanged(MyGUI::ComboBox* _sender, size_t _index) + { + // ignore spurious event fired when one edit the content after selection. + // onFilterEdited will handle it. + if (_index != MyGUI::ITEM_NONE) + applyFilter(_sender->getItemNameAt(_index)); + } + + void AlchemyWindow::onFilterEdited(MyGUI::EditBox* _sender) + { + applyFilter(_sender->getCaption()); + } + void AlchemyWindow::onOpen() { mAlchemy->clear(); mAlchemy->setAlchemist (MWMechanics::getPlayer()); - InventoryItemModel* model = new InventoryItemModel(MWMechanics::getPlayer()); - mSortModel = new SortFilterItemModel(model); + mModel = new InventoryItemModel(MWMechanics::getPlayer()); + mSortModel = new SortFilterItemModel(mModel); mSortModel->setFilter(SortFilterItemModel::Filter_OnlyIngredients); mItemView->setModel (mSortModel); mItemView->resetScrollBars(); @@ -167,6 +269,7 @@ namespace MWGui } update(); + initFilter(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mNameEdit); } diff --git a/apps/openmw/mwgui/alchemywindow.hpp b/apps/openmw/mwgui/alchemywindow.hpp index a3f1cb52e..82dd0a243 100644 --- a/apps/openmw/mwgui/alchemywindow.hpp +++ b/apps/openmw/mwgui/alchemywindow.hpp @@ -5,7 +5,9 @@ #include #include +#include +#include #include #include "windowbase.hpp" @@ -19,6 +21,7 @@ namespace MWGui { class ItemView; class ItemWidget; + class InventoryItemModel; class SortFilterItemModel; class AlchemyWindow : public WindowBase @@ -36,8 +39,11 @@ namespace MWGui static const float sCountChangeInterval; // in seconds std::string mSuggestedPotionName; + enum class FilterType { ByName, ByEffect }; + FilterType mCurrentFilter; ItemView* mItemView; + InventoryItemModel* mModel; SortFilterItemModel* mSortModel; MyGUI::Button* mCreateButton; @@ -47,6 +53,8 @@ namespace MWGui MyGUI::Button* mIncreaseButton; MyGUI::Button* mDecreaseButton; + Gui::AutoSizedButton* mFilterType; + MyGUI::ComboBox* mFilterValue; MyGUI::EditBox* mNameEdit; Gui::NumericEditBox* mBrewCountEdit; @@ -60,6 +68,13 @@ namespace MWGui void onCountValueChanged(int value); void onRepeatClick(MyGUI::Widget* widget, MyGUI::ControllerItem* controller); + void applyFilter(const std::string& filter); + void initFilter(); + void onFilterChanged(MyGUI::ComboBox* _sender, size_t _index); + void onFilterEdited(MyGUI::EditBox* _sender); + void switchFilterType(MyGUI::Widget* _sender); + void updateFilters(); + void addRepeatController(MyGUI::Widget* widget); void onIncreaseButtonTriggered(); diff --git a/apps/openmw/mwgui/sortfilteritemmodel.cpp b/apps/openmw/mwgui/sortfilteritemmodel.cpp index 88ae5fd1b..615e2dfc9 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.cpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.cpp @@ -23,6 +23,8 @@ #include "../mwworld/nullaction.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwmechanics/alchemy.hpp" + namespace { bool compareType(const std::string& type1, const std::string& type2) @@ -151,6 +153,8 @@ namespace MWGui : mCategory(Category_All) , mFilter(0) , mSortByType(true) + , mNameFilter("") + , mEffectFilter("") { mSourceModel = sourceModel; } @@ -199,8 +203,39 @@ namespace MWGui if (!(category & mCategory)) return false; - if ((mFilter & Filter_OnlyIngredients) && base.getTypeName() != typeid(ESM::Ingredient).name()) - return false; + if (mFilter & Filter_OnlyIngredients) + { + if (base.getTypeName() != typeid(ESM::Ingredient).name()) + return false; + + if (!mNameFilter.empty() && !mEffectFilter.empty()) + throw std::logic_error("name and magic effect filter are mutually exclusive"); + + if (!mNameFilter.empty()) + { + const auto itemName = Misc::StringUtils::lowerCase(base.getClass().getName(base)); + return itemName.find(mNameFilter) != std::string::npos; + } + + if (!mEffectFilter.empty()) + { + MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); + const auto alchemySkill = player.getClass().getSkill(player, ESM::Skill::Alchemy); + + const auto effects = MWMechanics::Alchemy::effectsDescription(base, alchemySkill); + + for (const auto& effect : effects) + { + const auto ciEffect = Misc::StringUtils::lowerCase(effect); + + if (ciEffect.find(mEffectFilter) != std::string::npos) + return true; + } + return false; + } + return true; + } + if ((mFilter & Filter_OnlyEnchanted) && !(item.mFlags & ItemStack::Flag_Enchanted)) return false; if ((mFilter & Filter_OnlyChargedSoulstones) && (base.getTypeName() != typeid(ESM::Miscellaneous).name() @@ -286,6 +321,11 @@ namespace MWGui mNameFilter = Misc::StringUtils::lowerCase(filter); } + void SortFilterItemModel::setEffectFilter (const std::string& filter) + { + mEffectFilter = Misc::StringUtils::lowerCase(filter); + } + void SortFilterItemModel::update() { mSourceModel->update(); diff --git a/apps/openmw/mwgui/sortfilteritemmodel.hpp b/apps/openmw/mwgui/sortfilteritemmodel.hpp index 6e400ddc9..3e616875e 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.hpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.hpp @@ -26,6 +26,7 @@ namespace MWGui void setCategory (int category); void setFilter (int filter); void setNameFilter (const std::string& filter); + void setEffectFilter (const std::string& filter); /// Use ItemStack::Type for sorting? void setSortByType(bool sort) { mSortByType = sort; } @@ -60,6 +61,7 @@ namespace MWGui bool mSortByType; std::string mNameFilter; // filter by item name + std::string mEffectFilter; // filter by magic effect }; } diff --git a/apps/openmw/mwmechanics/alchemy.cpp b/apps/openmw/mwmechanics/alchemy.cpp index 87ee47b49..b490db436 100644 --- a/apps/openmw/mwmechanics/alchemy.cpp +++ b/apps/openmw/mwmechanics/alchemy.cpp @@ -554,3 +554,37 @@ std::string MWMechanics::Alchemy::suggestPotionName() return MWBase::Environment::get().getWorld()->getStore().get().find( ESM::MagicEffect::effectIdToString(effectId))->mValue.getString(); } + +std::vector MWMechanics::Alchemy::effectsDescription (const MWWorld::ConstPtr &ptr, const int alchemySkill) +{ + std::vector effects; + + const auto& item = ptr.get()->mBase; + const auto& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + const static auto fWortChanceValue = gmst.find("fWortChanceValue")->mValue.getFloat(); + const auto& data = item->mData; + + for (auto i = 0; i < 4; ++i) + { + const auto effectID = data.mEffectID[i]; + const auto skillID = data.mSkills[i]; + const auto attributeID = data.mAttributes[i]; + + if (alchemySkill < fWortChanceValue * (i + 1)) + break; + + if (effectID != -1) + { + std::string effect = gmst.find(ESM::MagicEffect::effectIdToString(effectID))->mValue.getString(); + + if (skillID != -1) + effect += " " + gmst.find(ESM::Skill::sSkillNameIds[skillID])->mValue.getString(); + else if (attributeID != -1) + effect += " " + gmst.find(ESM::Attribute::sGmstAttributeIds[attributeID])->mValue.getString(); + + effects.push_back(effect); + + } + } + return effects; +} diff --git a/apps/openmw/mwmechanics/alchemy.hpp b/apps/openmw/mwmechanics/alchemy.hpp index 9f9f0b21c..d23f978ea 100644 --- a/apps/openmw/mwmechanics/alchemy.hpp +++ b/apps/openmw/mwmechanics/alchemy.hpp @@ -131,6 +131,8 @@ namespace MWMechanics ///< Try to create potions from the ingredients, place them in the inventory of the alchemist and /// adjust the skills of the alchemist accordingly. /// \param name must not be an empty string, or Result_NoName is returned + + static std::vector effectsDescription (const MWWorld::ConstPtr &ptr, const int alchemySKill); }; } diff --git a/files/mygui/openmw_alchemy_window.layout b/files/mygui/openmw_alchemy_window.layout index 714872fc3..8e1082952 100644 --- a/files/mygui/openmw_alchemy_window.layout +++ b/files/mygui/openmw_alchemy_window.layout @@ -57,8 +57,7 @@ - - + @@ -71,6 +70,21 @@ + + + + + + + + + + + + + + +