#include "enchanting.hpp" #include #include #include #include #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "actorutil.hpp" #include "creaturestats.hpp" #include "spellutil.hpp" #include "weapontype.hpp" namespace MWMechanics { Enchanting::Enchanting() : mCastStyle(ESM::Enchantment::CastOnce) , mSelfEnchanting(false) , mObjectType(0) , mWeaponType(-1) { } void Enchanting::setOldItem(const MWWorld::Ptr& oldItem) { mOldItemPtr = oldItem; mWeaponType = -1; mObjectType = 0; if (!itemEmpty()) { mObjectType = mOldItemPtr.getType(); if (mObjectType == ESM::Weapon::sRecordId) mWeaponType = mOldItemPtr.get()->mBase->mData.mType; } } void Enchanting::setNewItemName(const std::string& s) { mNewItemName = s; } void Enchanting::setEffect(const ESM::EffectList& effectList) { mEffectList = effectList; } int Enchanting::getCastStyle() const { return mCastStyle; } void Enchanting::setSoulGem(const MWWorld::Ptr& soulGem) { mSoulGemPtr = soulGem; } bool Enchanting::create() { const MWWorld::Ptr& player = getPlayer(); MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); ESM::Enchantment enchantment; enchantment.mData.mFlags = 0; enchantment.mData.mType = mCastStyle; enchantment.mData.mCost = getBaseCastCost(); enchantment.mRecordFlags = 0; store.remove(mSoulGemPtr, 1); // Exception for Azura Star, new one will be added after enchanting auto azurasStarId = ESM::RefId::stringRefId("Misc_SoulGem_Azura"); if (mSoulGemPtr.get()->mBase->mId == azurasStarId) store.add(azurasStarId, 1); if (mSelfEnchanting) { auto& prng = MWBase::Environment::get().getWorld()->getPrng(); if (getEnchantChance() <= (Misc::Rng::roll0to99(prng))) return false; mEnchanter.getClass().skillUsageSucceeded( mEnchanter, ESM::Skill::Enchant, ESM::Skill::Enchant_CreateMagicItem); } enchantment.mEffects = mEffectList; int count = getEnchantItemsCount(); if (mCastStyle == ESM::Enchantment::ConstantEffect) enchantment.mData.mCharge = 0; else enchantment.mData.mCharge = getGemCharge() / count; // Try to find a dynamic enchantment with the same stats, create a new one if not found. const ESM::Enchantment* enchantmentPtr = getRecord(enchantment); if (enchantmentPtr == nullptr) enchantmentPtr = MWBase::Environment::get().getESMStore()->insert(enchantment); // Apply the enchantment const ESM::RefId& newItemId = mOldItemPtr.getClass().applyEnchantment(mOldItemPtr, enchantmentPtr->mId, getGemCharge(), mNewItemName); if (!mSelfEnchanting) payForEnchantment(count); // Add the new item to player inventory and remove the old one store.remove(mOldItemPtr, count); store.add(newItemId, count); return true; } void Enchanting::nextCastStyle() { if (itemEmpty()) return; const bool powerfulSoul = getGemCharge() >= MWBase::Environment::get() .getESMStore() ->get() .find("iSoulAmountForConstantEffect") ->mValue.getInteger(); if ((mObjectType == ESM::Armor::sRecordId) || (mObjectType == ESM::Clothing::sRecordId)) { // Armor or Clothing switch (mCastStyle) { case ESM::Enchantment::WhenUsed: if (powerfulSoul) mCastStyle = ESM::Enchantment::ConstantEffect; return; default: // takes care of Constant effect too mCastStyle = ESM::Enchantment::WhenUsed; return; } } else if (mWeaponType != -1) { // Weapon ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass; switch (mCastStyle) { case ESM::Enchantment::WhenStrikes: if (weapclass == ESM::WeaponType::Melee || weapclass == ESM::WeaponType::Ranged) mCastStyle = ESM::Enchantment::WhenUsed; return; case ESM::Enchantment::WhenUsed: if (powerfulSoul && weapclass != ESM::WeaponType::Ammo && weapclass != ESM::WeaponType::Thrown) mCastStyle = ESM::Enchantment::ConstantEffect; else if (weapclass != ESM::WeaponType::Ranged) mCastStyle = ESM::Enchantment::WhenStrikes; return; default: // takes care of Constant effect too mCastStyle = ESM::Enchantment::WhenUsed; if (weapclass != ESM::WeaponType::Ranged) mCastStyle = ESM::Enchantment::WhenStrikes; return; } } else if (mObjectType == ESM::Book::sRecordId) { // Scroll or Book mCastStyle = ESM::Enchantment::CastOnce; return; } // Fail case mCastStyle = ESM::Enchantment::CastOnce; } /* * Vanilla enchant cost formula: * * Touch/Self: (min + max) * baseCost * 0.025 * duration + area * baseCost * 0.025 * Target: 1.5 * ((min + max) * baseCost * 0.025 * duration + area * baseCost * 0.025) * Constant eff: (min + max) * baseCost * 2.5 + area * baseCost * 0.025 * * For multiple effects - cost of each effect is multiplied by number of effects that follows +1. * * Note: Minimal value inside formula for 'min' and 'max' is 1. So in vanilla: * (0 + 0) == (1 + 0) == (1 + 1) => 2 or (2 + 0) == (1 + 2) => 3 * * Formula on UESPWiki is not entirely correct. */ float Enchanting::getEnchantPoints(bool precise) const { if (mEffectList.mList.empty()) // No effects added, cost = 0 return 0; const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); const float fEffectCostMult = store.get().find("fEffectCostMult")->mValue.getFloat(); const float fEnchantmentConstantDurationMult = store.get().find("fEnchantmentConstantDurationMult")->mValue.getFloat(); float enchantmentCost = 0.f; float cost = 0.f; for (const ESM::IndexedENAMstruct& effect : mEffectList.mList) { float baseCost = (store.get().find(effect.mData.mEffectID))->mData.mBaseCost; int magMin = std::max(1, effect.mData.mMagnMin); int magMax = std::max(1, effect.mData.mMagnMax); int area = std::max(1, effect.mData.mArea); float duration = static_cast(effect.mData.mDuration); if (mCastStyle == ESM::Enchantment::ConstantEffect) duration = fEnchantmentConstantDurationMult; cost += ((magMin + magMax) * duration + area) * baseCost * fEffectCostMult * 0.05f; cost = std::max(1.f, cost); if (effect.mData.mRange == ESM::RT_Target) cost *= 1.5f; enchantmentCost += precise ? cost : std::floor(cost); } return enchantmentCost; } const ESM::Enchantment* Enchanting::getRecord(const ESM::Enchantment& toFind) const { const MWWorld::Store& enchantments = MWBase::Environment::get().getESMStore()->get(); MWWorld::Store::iterator iter(enchantments.begin()); iter += (enchantments.getSize() - enchantments.getDynamicSize()); for (; iter != enchantments.end(); ++iter) { if (iter->mEffects.mList.size() != toFind.mEffects.mList.size()) continue; if (iter->mData.mFlags != toFind.mData.mFlags || iter->mData.mType != toFind.mData.mType || iter->mData.mCost != toFind.mData.mCost || iter->mData.mCharge != toFind.mData.mCharge) continue; // Don't choose an ID that came from the content files, would have unintended side effects if (!enchantments.isDynamic(iter->mId)) continue; bool mismatch = false; for (int i = 0; i < static_cast(iter->mEffects.mList.size()); ++i) { if (iter->mEffects.mList[i] != toFind.mEffects.mList[i]) { mismatch = true; break; } } if (!mismatch) return &(*iter); } return nullptr; } int Enchanting::getBaseCastCost() const { if (mCastStyle == ESM::Enchantment::ConstantEffect) return 0; return static_cast(getEnchantPoints(false)); } int Enchanting::getEffectiveCastCost() const { int baseCost = getBaseCastCost(); MWWorld::Ptr player = getPlayer(); return getEffectiveEnchantmentCastCost(static_cast(baseCost), player); } int Enchanting::getEnchantPrice(int count) const { if (mEnchanter.isEmpty()) return 0; float priceMultipler = MWBase::Environment::get() .getESMStore() ->get() .find("fEnchantmentValueMult") ->mValue.getFloat(); int price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer( mEnchanter, static_cast(getEnchantPoints() * priceMultipler), true); price *= count * getTypeMultiplier(); return std::max(1, price); } int Enchanting::getGemCharge() const { const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); if (soulEmpty()) return 0; if (mSoulGemPtr.getCellRef().getSoul().empty()) return 0; const ESM::Creature* soul = store.get().search(mSoulGemPtr.getCellRef().getSoul()); if (soul) return soul->mData.mSoul; else return 0; } int Enchanting::getMaxEnchantValue() const { if (itemEmpty()) return 0; const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); return static_cast(mOldItemPtr.getClass().getEnchantmentPoints(mOldItemPtr) * store.get().find("fEnchantmentMult")->mValue.getFloat()); } bool Enchanting::soulEmpty() const { return mSoulGemPtr.isEmpty(); } bool Enchanting::itemEmpty() const { return mOldItemPtr.isEmpty(); } void Enchanting::setSelfEnchanting(bool selfEnchanting) { mSelfEnchanting = selfEnchanting; } void Enchanting::setEnchanter(const MWWorld::Ptr& enchanter) { mEnchanter = enchanter; // Reset cast style mCastStyle = ESM::Enchantment::CastOnce; } int Enchanting::getEnchantChance() const { const CreatureStats& stats = mEnchanter.getClass().getCreatureStats(mEnchanter); const MWWorld::Store& gmst = MWBase::Environment::get().getESMStore()->get(); const float a = static_cast(mEnchanter.getClass().getSkill(mEnchanter, ESM::Skill::Enchant)); const float b = static_cast(stats.getAttribute(ESM::Attribute::Intelligence).getModified()); const float c = static_cast(stats.getAttribute(ESM::Attribute::Luck).getModified()); const float fEnchantmentChanceMult = gmst.find("fEnchantmentChanceMult")->mValue.getFloat(); const float fEnchantmentConstantChanceMult = gmst.find("fEnchantmentConstantChanceMult")->mValue.getFloat(); float x = (a - getEnchantPoints() * fEnchantmentChanceMult * getTypeMultiplier() * getEnchantItemsCount() + 0.2f * b + 0.1f * c) * stats.getFatigueTerm(); if (mCastStyle == ESM::Enchantment::ConstantEffect) x *= fEnchantmentConstantChanceMult; return static_cast(x); } int Enchanting::getEnchantItemsCount() const { int count = 1; float enchantPoints = getEnchantPoints(); if (mWeaponType != -1 && enchantPoints > 0) { ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass; if (weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo) { MWWorld::Ptr player = getPlayer(); count = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId()); count = std::clamp( getGemCharge() * Settings::game().mProjectilesEnchantMultiplier / enchantPoints, 1, count); } } return count; } float Enchanting::getTypeMultiplier() const { if (Settings::game().mProjectilesEnchantMultiplier > 0 && mWeaponType != -1 && getEnchantPoints() > 0) { ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass; if (weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo) return 0.125f; } return 1.f; } void Enchanting::payForEnchantment(int count) const { const MWWorld::Ptr& player = getPlayer(); MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); int price = getEnchantPrice(count); store.remove(MWWorld::ContainerStore::sGoldId, price); // add gold to NPC trading gold pool CreatureStats& enchanterStats = mEnchanter.getClass().getCreatureStats(mEnchanter); enchanterStats.setGoldPool(enchanterStats.getGoldPool() + price); } }