#include "autocalcspell.hpp" #include #include #include #include #include #include #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "spellutil.hpp" namespace MWMechanics { struct SchoolCaps { int mCount; int mLimit; bool mReachedLimit; int mMinCost; ESM::RefId mWeakestSpell; }; std::vector autoCalcNpcSpells(const std::map& actorSkills, const std::map& actorAttributes, const ESM::Race* race) { const MWWorld::Store& gmst = MWBase::Environment::get().getESMStore()->get(); static const float fNPCbaseMagickaMult = gmst.find("fNPCbaseMagickaMult")->mValue.getFloat(); float baseMagicka = fNPCbaseMagickaMult * actorAttributes.at(ESM::Attribute::Intelligence).getBase(); static const std::string schools[] = { "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" }; static int iAutoSpellSchoolMax[6]; static bool init = false; if (!init) { for (int i = 0; i < 6; ++i) { const std::string& gmstName = "iAutoSpell" + schools[i] + "Max"; iAutoSpellSchoolMax[i] = gmst.find(gmstName)->mValue.getInteger(); } init = true; } std::map schoolCaps; for (int i = 0; i < 6; ++i) { SchoolCaps caps; caps.mCount = 0; caps.mLimit = iAutoSpellSchoolMax[i]; caps.mReachedLimit = iAutoSpellSchoolMax[i] <= 0; caps.mMinCost = std::numeric_limits::max(); caps.mWeakestSpell = ESM::RefId(); schoolCaps[i] = caps; } std::vector selectedSpells; const MWWorld::Store& spells = MWBase::Environment::get().getESMStore()->get(); // Note: the algorithm heavily depends on the traversal order of the spells. For vanilla-compatible results the // Store must preserve the record ordering as it was in the content files. for (const ESM::Spell& spell : spells) { if (spell.mData.mType != ESM::Spell::ST_Spell) continue; if (!(spell.mData.mFlags & ESM::Spell::F_Autocalc)) continue; static const int iAutoSpellTimesCanCast = gmst.find("iAutoSpellTimesCanCast")->mValue.getInteger(); int spellCost = MWMechanics::calcSpellCost(spell); if (baseMagicka < iAutoSpellTimesCanCast * spellCost) continue; if (race && race->mPowers.exists(spell.mId)) continue; if (!attrSkillCheck(&spell, actorSkills, actorAttributes)) continue; int school; float skillTerm; calcWeakestSchool(&spell, actorSkills, school, skillTerm); assert(school >= 0 && school < 6); SchoolCaps& cap = schoolCaps[school]; if (cap.mReachedLimit && spellCost <= cap.mMinCost) continue; static const float fAutoSpellChance = gmst.find("fAutoSpellChance")->mValue.getFloat(); if (calcAutoCastChance(&spell, actorSkills, actorAttributes, school) < fAutoSpellChance) continue; selectedSpells.push_back(spell.mId); if (cap.mReachedLimit) { auto found = std::find(selectedSpells.begin(), selectedSpells.end(), cap.mWeakestSpell); if (found != selectedSpells.end()) selectedSpells.erase(found); cap.mMinCost = std::numeric_limits::max(); for (const ESM::RefId& testSpellName : selectedSpells) { const ESM::Spell* testSpell = spells.find(testSpellName); int testSpellCost = MWMechanics::calcSpellCost(*testSpell); // int testSchool; // float dummySkillTerm; // calcWeakestSchool(testSpell, actorSkills, testSchool, dummySkillTerm); // Note: if there are multiple spells with the same cost, we pick the first one we found. // So the algorithm depends on the iteration order of the outer loop. if ( // There is a huge bug here. It is not checked that weakestSpell is of the correct school. // As result multiple SchoolCaps could have the same mWeakestSpell. Erasing the weakest spell // would then fail if another school already erased it, and so the number of spells would often // exceed the sum of limits. This bug cannot be fixed without significantly changing the results // of the spell autocalc, which will not have been playtested. // testSchool == school && testSpellCost < cap.mMinCost) { cap.mMinCost = testSpellCost; cap.mWeakestSpell = testSpell->mId; } } } else { cap.mCount += 1; if (cap.mCount == cap.mLimit) cap.mReachedLimit = true; if (spellCost < cap.mMinCost) { cap.mWeakestSpell = spell.mId; cap.mMinCost = spellCost; } } } return selectedSpells; } std::vector autoCalcPlayerSpells(const std::map& actorSkills, const std::map& actorAttributes, const ESM::Race* race) { const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); static const float fPCbaseMagickaMult = esmStore.get().find("fPCbaseMagickaMult")->mValue.getFloat(); float baseMagicka = fPCbaseMagickaMult * actorAttributes.at(ESM::Attribute::Intelligence).getBase(); bool reachedLimit = false; const ESM::Spell* weakestSpell = nullptr; int minCost = std::numeric_limits::max(); std::vector selectedSpells; const MWWorld::Store& spells = esmStore.get(); for (const ESM::Spell& spell : spells) { if (spell.mData.mType != ESM::Spell::ST_Spell) continue; if (!(spell.mData.mFlags & ESM::Spell::F_PCStart)) continue; int spellCost = MWMechanics::calcSpellCost(spell); if (reachedLimit && spellCost <= minCost) continue; if (race && std::find(race->mPowers.mList.begin(), race->mPowers.mList.end(), spell.mId) != race->mPowers.mList.end()) continue; if (baseMagicka < spellCost) continue; static const float fAutoPCSpellChance = esmStore.get().find("fAutoPCSpellChance")->mValue.getFloat(); if (calcAutoCastChance(&spell, actorSkills, actorAttributes, -1) < fAutoPCSpellChance) continue; if (!attrSkillCheck(&spell, actorSkills, actorAttributes)) continue; selectedSpells.push_back(spell.mId); if (reachedLimit) { std::vector::iterator it = std::find(selectedSpells.begin(), selectedSpells.end(), weakestSpell->mId); if (it != selectedSpells.end()) selectedSpells.erase(it); minCost = std::numeric_limits::max(); for (const ESM::RefId& testSpellName : selectedSpells) { const ESM::Spell* testSpell = esmStore.get().find(testSpellName); int testSpellCost = MWMechanics::calcSpellCost(*testSpell); if (testSpellCost < minCost) { minCost = testSpellCost; weakestSpell = testSpell; } } } else { if (spellCost < minCost) { weakestSpell = &spell; minCost = MWMechanics::calcSpellCost(*weakestSpell); } static const unsigned int iAutoPCSpellMax = esmStore.get().find("iAutoPCSpellMax")->mValue.getInteger(); if (selectedSpells.size() == iAutoPCSpellMax) reachedLimit = true; } } return selectedSpells; } bool attrSkillCheck(const ESM::Spell* spell, const std::map& actorSkills, const std::map& actorAttributes) { for (const auto& spellEffect : spell->mEffects.mList) { const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getESMStore()->get().find(spellEffect.mEffectID); static const int iAutoSpellAttSkillMin = MWBase::Environment::get() .getESMStore() ->get() .find("iAutoSpellAttSkillMin") ->mValue.getInteger(); if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill)) { ESM::RefId skill = ESM::Skill::indexToRefId(spellEffect.mSkill); auto found = actorSkills.find(skill); if (found == actorSkills.end() || found->second.getBase() < iAutoSpellAttSkillMin) return false; } if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute)) { auto found = actorAttributes.find(ESM::Attribute::AttributeID(spellEffect.mAttribute)); if (found == actorAttributes.end() || found->second.getBase() < iAutoSpellAttSkillMin) return false; } } return true; } void calcWeakestSchool(const ESM::Spell* spell, const std::map& actorSkills, int& effectiveSchool, float& skillTerm) { // Morrowind for some reason uses a formula slightly different from magicka cost calculation float minChance = std::numeric_limits::max(); for (const ESM::ENAMstruct& effect : spell->mEffects.mList) { const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getESMStore()->get().find(effect.mEffectID); int minMagn = 1; int maxMagn = 1; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) { minMagn = effect.mMagnMin; maxMagn = effect.mMagnMax; } int duration = 0; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) duration = effect.mDuration; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce)) duration = std::max(1, duration); static const float fEffectCostMult = MWBase::Environment::get() .getESMStore() ->get() .find("fEffectCostMult") ->mValue.getFloat(); float x = 0.5 * (std::max(1, minMagn) + std::max(1, maxMagn)); x *= 0.1 * magicEffect->mData.mBaseCost; x *= 1 + duration; x += 0.05 * std::max(1, effect.mArea) * magicEffect->mData.mBaseCost; x *= fEffectCostMult; if (effect.mRange == ESM::RT_Target) x *= 1.5f; float s = 0.f; ESM::RefId skill = spellSchoolToSkill(magicEffect->mData.mSchool); auto found = actorSkills.find(skill); if (found != actorSkills.end()) s = 2.f * found->second.getBase(); if (s - x < minChance) { minChance = s - x; effectiveSchool = magicEffect->mData.mSchool; skillTerm = s; } } } float calcAutoCastChance(const ESM::Spell* spell, const std::map& actorSkills, const std::map& actorAttributes, int effectiveSchool) { if (spell->mData.mType != ESM::Spell::ST_Spell) return 100.f; if (spell->mData.mFlags & ESM::Spell::F_Always) return 100.f; float skillTerm = 0; if (effectiveSchool != -1) { ESM::RefId skill = spellSchoolToSkill(effectiveSchool); auto found = actorSkills.find(skill); if (found != actorSkills.end()) skillTerm = 2.f * found->second.getBase(); } else calcWeakestSchool( spell, actorSkills, effectiveSchool, skillTerm); // Note effectiveSchool is unused after this float castChance = skillTerm - MWMechanics::calcSpellCost(*spell) + 0.2f * actorAttributes.at(ESM::Attribute::Willpower).getBase() + 0.1f * actorAttributes.at(ESM::Attribute::Luck).getBase(); return castChance; } }