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 @@ + + + + + + + + + + + + + + +