From df5625a1e385a94e8e7ab992288cdeada58b23bc Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 6 Jul 2025 13:36:34 +0300 Subject: [PATCH] Unify creature/NPC armor autoequip --- apps/openmw/mwworld/inventorystore.cpp | 162 +++++++++++-------------- apps/openmw/mwworld/inventorystore.hpp | 1 - 2 files changed, 70 insertions(+), 93 deletions(-) diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index f48f4e6e31..5f7e135847 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -378,110 +378,111 @@ void MWWorld::InventoryStore::autoEquipWeapon(TSlots& slots_) void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_) { - // Only NPCs can wear armor for now. - // For creatures we equip only shields. const Ptr& actor = getPtr(); - if (!actor.getClass().isNpc()) + + // Creatures only want shields and don't benefit from armor rating or unarmored skill + const MWWorld::Class& actorCls = actor.getClass(); + const bool actorIsNpc = actorCls.isNpc(); + + int equipmentTypes = ContainerStore::Type_Armor; + float unarmoredRating = 0.f; + if (actorIsNpc) { - autoEquipShield(slots_); - return; + equipmentTypes |= ContainerStore::Type_Clothing; + const auto& store = MWBase::Environment::get().getESMStore()->get(); + const float fUnarmoredBase1 = store.find("fUnarmoredBase1")->mValue.getFloat(); + const float fUnarmoredBase2 = store.find("fUnarmoredBase2")->mValue.getFloat(); + const float unarmoredSkill = actorCls.getSkill(actor, ESM::Skill::Unarmored); + unarmoredRating = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill); + unarmoredRating = std::max(unarmoredRating, 0.f); } - const MWWorld::Store& store = MWBase::Environment::get().getESMStore()->get(); - - static float fUnarmoredBase1 = store.find("fUnarmoredBase1")->mValue.getFloat(); - static float fUnarmoredBase2 = store.find("fUnarmoredBase2")->mValue.getFloat(); - - float unarmoredSkill = actor.getClass().getSkill(actor, ESM::Skill::Unarmored); - float unarmoredRating = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill); - - for (ContainerStoreIterator iter(begin(ContainerStore::Type_Clothing | ContainerStore::Type_Armor)); iter != end(); - ++iter) + for (ContainerStoreIterator iter(begin(equipmentTypes)); iter != end(); ++iter) { Ptr test = *iter; + const MWWorld::Class& testCls = test.getClass(); + const bool isArmor = iter.getType() == ContainerStore::Type_Armor; - switch (test.getClass().canBeEquipped(test, actor).first) + // Discard armor that is worse than unarmored for NPCs and non-shields for creatures + if (isArmor) { - case 0: - continue; - default: - break; + if (actorIsNpc) + { + if (testCls.getEffectiveArmorRating(test, actor) <= unarmoredRating) + continue; + } + else + { + if (test.get()->mBase->mData.mType != ESM::Armor::Shield) + continue; + } } - if (iter.getType() == ContainerStore::Type_Armor - && test.getClass().getEffectiveArmorRating(test, actor) <= std::max(unarmoredRating, 0.f)) - { + // Don't equip the item if it cannot be equipped + if (testCls.canBeEquipped(test, actor).first == 0) continue; - } - std::pair, bool> itemsSlots = iter->getClass().getEquipmentSlots(*iter); + const auto [itemSlots, canStack] = testCls.getEquipmentSlots(test); // checking if current item pointed by iter can be equipped - for (int slot : itemsSlots.first) + for (const int slot : itemSlots) { - // if true then it means slot is equipped already // check if slot may require swapping if current item is more valuable if (slots_.at(slot) != end()) { Ptr old = *slots_.at(slot); + const MWWorld::Class& oldCls = old.getClass(); + unsigned int oldType = old.getType(); - if (iter.getType() == ContainerStore::Type_Armor) + if (!isArmor) { - if (old.getType() == ESM::Armor::sRecordId) - { - if (old.get()->mBase->mData.mType < test.get()->mBase->mData.mType) - continue; + // Armor should replace clothing and weapons, but clothing should only replace clothing + if (oldType != ESM::Clothing::sRecordId) + continue; - if (old.get()->mBase->mData.mType == test.get()->mBase->mData.mType) - { - if (old.getClass().getEffectiveArmorRating(old, actor) - >= test.getClass().getEffectiveArmorRating(test, actor)) - // old armor had better armor rating - continue; - } - } - // suitable armor should replace already equipped clothing - } - else if (iter.getType() == ContainerStore::Type_Clothing) - { - // if left ring is equipped + // If the left ring slot is filled, don't swap if the right ring is cheaper if (slot == Slot_LeftRing) { - // if there is a place for right ring dont swap it if (slots_.at(Slot_RightRing) == end()) - { continue; - } - else // if right ring is equipped too - { - Ptr rightRing = *slots_.at(Slot_RightRing); - // we want to swap cheaper ring only if both are equipped - if (old.getClass().getValue(old) >= rightRing.getClass().getValue(rightRing)) + Ptr rightRing = *slots_.at(Slot_RightRing); + if (rightRing.getClass().getValue(rightRing) <= oldCls.getValue(old)) + continue; + } + + if (testCls.getValue(test) <= oldCls.getValue(old)) + continue; + } + else if (oldType == ESM::Armor::sRecordId) + { + const int32_t oldArmorType = old.get()->mBase->mData.mType; + const int32_t newArmorType = test.get()->mBase->mData.mType; + if (oldArmorType == newArmorType) + { + // For NPCs, compare armor rating; for creatures, compare condition + if (actorIsNpc) + { + const float rating = testCls.getEffectiveArmorRating(test, actor); + const float oldRating = oldCls.getEffectiveArmorRating(old, actor); + if (rating <= oldRating) + continue; + } + else + { + if (testCls.getItemHealth(test) <= oldCls.getItemHealth(old)) continue; } } - - if (old.getType() == ESM::Clothing::sRecordId) - { - // check value - if (old.getClass().getValue(old) >= test.getClass().getValue(test)) - // old clothing was more valuable - continue; - } - else - // suitable clothing should NOT replace already equipped armor + else if (oldArmorType < newArmorType) continue; } } - if (!itemsSlots.second) // if itemsSlots.second is true, item can stay stacked when equipped + // unstack the item if required + if (!canStack && test.getCellRef().getCount() > 1) { - // unstack item pointed to by iterator if required - if (iter->getCellRef().getCount() > 1) - { - unstack(*iter); - } + unstack(test); } // if we are here it means item can be equipped or swapped @@ -491,27 +492,6 @@ void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_) } } -void MWWorld::InventoryStore::autoEquipShield(TSlots& slots_) -{ - for (ContainerStoreIterator iter(begin(ContainerStore::Type_Armor)); iter != end(); ++iter) - { - if (iter->get()->mBase->mData.mType != ESM::Armor::Shield) - continue; - if (iter->getClass().canBeEquipped(*iter, getPtr()).first != 1) - continue; - std::pair, bool> shieldSlots = iter->getClass().getEquipmentSlots(*iter); - int slot = shieldSlots.first[0]; - const ContainerStoreIterator& shield = slots_[slot]; - if (shield != end() && shield.getType() == Type_Armor - && shield->get()->mBase->mData.mType == ESM::Armor::Shield) - { - if (shield->getClass().getItemHealth(*shield) >= iter->getClass().getItemHealth(*iter)) - continue; - } - slots_[slot] = iter; - } -} - void MWWorld::InventoryStore::autoEquip() { TSlots slots_; @@ -522,8 +502,6 @@ void MWWorld::InventoryStore::autoEquip() // Autoequip clothing, armor and weapons. // Equipping lights is handled in Actors::updateEquippedLight based on environment light. - // Note: creatures ignore equipment armor rating and only equip shields - // Use custom logic for them - select shield based on its health instead of armor rating autoEquipWeapon(slots_); autoEquipArmor(slots_); diff --git a/apps/openmw/mwworld/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index 0af6ee2b28..4e8f56f616 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -69,7 +69,6 @@ namespace MWWorld void autoEquipWeapon(TSlots& slots_); void autoEquipArmor(TSlots& slots_); - void autoEquipShield(TSlots& slots_); // selected magic item (for using enchantments of type "Cast once" or "Cast when used") ContainerStoreIterator mSelectedEnchantItem;