From 5c4e1252e9ac0d18fdb13a614f4de34aea5c5193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Peltier?= Date: Fri, 2 Apr 2021 19:47:21 +0200 Subject: [PATCH] Handle AutoCalc flag when getting spell cost Fixes #5483 This only applies to "base game" spells. When adding an AutoCalc spell with TES:CS its cost is computed and stored inside game files. This stored cost was being used by OpenMW and the actual cost was never recomputed at runtime whereas Morrowind.exe discards the stored cost. While this worked fine in vanilla, mods can update AutoCalc spell effects without ever updating the stored cost. The formula was mostly there already but there was a few differences, namely a 1 second offset in duration. --- AUTHORS.md | 1 + CHANGELOG.md | 1 + apps/openmw/mwgui/spellcreationdialog.cpp | 2 +- apps/openmw/mwgui/spellmodel.cpp | 2 +- apps/openmw/mwgui/tooltips.cpp | 2 +- apps/openmw/mwmechanics/autocalcspell.cpp | 33 ++++++++++------- apps/openmw/mwmechanics/spellabsorption.cpp | 2 +- apps/openmw/mwmechanics/spellcasting.cpp | 2 +- apps/openmw/mwmechanics/spellutil.cpp | 39 ++++++++++++++++++--- apps/openmw/mwmechanics/spellutil.hpp | 8 ++++- apps/openmw/mwworld/worldimp.cpp | 6 ++-- 11 files changed, 72 insertions(+), 26 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 75302908ea..22a18506e8 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -118,6 +118,7 @@ Programmers Kurnevsky Evgeny (kurnevsky) Lars Söderberg (Lazaroth) lazydev + Léo Peltier Leon Krieg (lkrieg) Leon Saunders (emoose) logzero diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e4a5e9024..d4ffe9a630 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Bug #3737: Scripts from The Underground 2 .esp do not play (all patched versions) Bug #3846: Strings starting with "-" fail to compile if not enclosed in quotes + Bug #5483: AutoCalc flag is not used to calculate spells cost 0.47.0 ------ diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 5a5dec60f7..01653d9e6f 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -456,7 +456,7 @@ namespace MWGui for (const ESM::ENAMstruct& effect : mEffects) { - y += std::max(1.f, MWMechanics::calcEffectCost(effect)); + y += std::max(1.f, MWMechanics::calcEffectCost(effect, nullptr, MWMechanics::EffectCostMethod::PlayerSpell)); if (effect.mRange == ESM::RT_Target) y *= 1.5; diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp index 78b9171e59..fe18b0f082 100644 --- a/apps/openmw/mwgui/spellmodel.cpp +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -109,7 +109,7 @@ namespace MWGui if (spell->mData.mType == ESM::Spell::ST_Spell) { newSpell.mType = Spell::Type_Spell; - std::string cost = std::to_string(spell->mData.mCost); + std::string cost = std::to_string(MWMechanics::calcSpellCost(*spell)); std::string chance = std::to_string(int(MWMechanics::getSpellSuccessChance(spell, mActor))); newSpell.mCostColumn = cost + "/" + chance; } diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index a821d0106b..84d8b65592 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -251,7 +251,7 @@ namespace MWGui } std::string cost = focus->getUserString("SpellCost"); if (cost != "" && cost != "0") - info.text += MWGui::ToolTips::getValueString(spell->mData.mCost, "#{sCastCost}"); + info.text += MWGui::ToolTips::getValueString(MWMechanics::calcSpellCost(*spell), "#{sCastCost}"); info.effects = effects; tooltipSize = createToolTip(info); } diff --git a/apps/openmw/mwmechanics/autocalcspell.cpp b/apps/openmw/mwmechanics/autocalcspell.cpp index 662cfe473b..d82b8505f1 100644 --- a/apps/openmw/mwmechanics/autocalcspell.cpp +++ b/apps/openmw/mwmechanics/autocalcspell.cpp @@ -68,7 +68,8 @@ namespace MWMechanics if (!(spell.mData.mFlags & ESM::Spell::F_Autocalc)) continue; static const int iAutoSpellTimesCanCast = gmst.find("iAutoSpellTimesCanCast")->mValue.getInteger(); - if (baseMagicka < iAutoSpellTimesCanCast * spell.mData.mCost) + int spellCost = MWMechanics::calcSpellCost(spell); + if (baseMagicka < iAutoSpellTimesCanCast * spellCost) continue; if (race && race->mPowers.exists(spell.mId)) @@ -83,7 +84,7 @@ namespace MWMechanics assert(school >= 0 && school < 6); SchoolCaps& cap = schoolCaps[school]; - if (cap.mReachedLimit && spell.mData.mCost <= cap.mMinCost) + if (cap.mReachedLimit && spellCost <= cap.mMinCost) continue; static const float fAutoSpellChance = gmst.find("fAutoSpellChance")->mValue.getFloat(); @@ -102,6 +103,7 @@ namespace MWMechanics for (const std::string& testSpellName : selectedSpells) { const ESM::Spell* testSpell = spells.find(testSpellName); + int testSpellCost = MWMechanics::calcSpellCost(*testSpell); //int testSchool; //float dummySkillTerm; @@ -115,9 +117,9 @@ namespace MWMechanics // 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 && - testSpell->mData.mCost < cap.mMinCost) + testSpellCost < cap.mMinCost) { - cap.mMinCost = testSpell->mData.mCost; + cap.mMinCost = testSpellCost; cap.mWeakestSpell = testSpell->mId; } } @@ -128,10 +130,10 @@ namespace MWMechanics if (cap.mCount == cap.mLimit) cap.mReachedLimit = true; - if (spell.mData.mCost < cap.mMinCost) + if (spellCost < cap.mMinCost) { cap.mWeakestSpell = spell.mId; - cap.mMinCost = spell.mData.mCost; + cap.mMinCost = spellCost; } } } @@ -159,11 +161,13 @@ namespace MWMechanics continue; if (!(spell.mData.mFlags & ESM::Spell::F_PCStart)) continue; - if (reachedLimit && spell.mData.mCost <= minCost) + + 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 < spell.mData.mCost) + if (baseMagicka < spellCost) continue; static const float fAutoPCSpellChance = esmStore.get().find("fAutoPCSpellChance")->mValue.getFloat(); @@ -185,19 +189,20 @@ namespace MWMechanics for (const std::string& testSpellName : selectedSpells) { const ESM::Spell* testSpell = esmStore.get().find(testSpellName); - if (testSpell->mData.mCost < minCost) + int testSpellCost = MWMechanics::calcSpellCost(*testSpell); + if (testSpellCost < minCost) { - minCost = testSpell->mData.mCost; + minCost = testSpellCost; weakestSpell = testSpell; } } } else { - if (spell.mData.mCost < minCost) + if (spellCost < minCost) { weakestSpell = &spell; - minCost = weakestSpell->mData.mCost; + minCost = MWMechanics::calcSpellCost(*weakestSpell); } static const unsigned int iAutoPCSpellMax = esmStore.get().find("iAutoPCSpellMax")->mValue.getInteger(); if (selectedSpells.size() == iAutoPCSpellMax) @@ -291,7 +296,9 @@ namespace MWMechanics else calcWeakestSchool(spell, actorSkills, effectiveSchool, skillTerm); // Note effectiveSchool is unused after this - float castChance = skillTerm - spell->mData.mCost + 0.2f * actorAttributes[ESM::Attribute::Willpower] + 0.1f * actorAttributes[ESM::Attribute::Luck]; + float castChance = skillTerm - MWMechanics::calcSpellCost(*spell) + + 0.2f * actorAttributes[ESM::Attribute::Willpower] + + 0.1f * actorAttributes[ESM::Attribute::Luck]; return castChance; } } diff --git a/apps/openmw/mwmechanics/spellabsorption.cpp b/apps/openmw/mwmechanics/spellabsorption.cpp index bab290fdab..cb80921b33 100644 --- a/apps/openmw/mwmechanics/spellabsorption.cpp +++ b/apps/openmw/mwmechanics/spellabsorption.cpp @@ -72,7 +72,7 @@ namespace MWMechanics int spellCost = 0; if (spell) { - spellCost = spell->mData.mCost; + spellCost = MWMechanics::calcSpellCost(*spell); } else { diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 5a367b5020..47225bc6f7 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -584,7 +584,7 @@ namespace MWMechanics DynamicStat fatigue = stats.getFatigue(); const float normalizedEncumbrance = mCaster.getClass().getNormalizedEncumbrance(mCaster); - float fatigueLoss = spell->mData.mCost * (fFatigueSpellBase + normalizedEncumbrance * fFatigueSpellMult); + float fatigueLoss = MWMechanics::calcSpellCost(*spell) * (fFatigueSpellBase + normalizedEncumbrance * fFatigueSpellMult); fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); stats.setFatigue(fatigue); diff --git a/apps/openmw/mwmechanics/spellutil.cpp b/apps/openmw/mwmechanics/spellutil.cpp index 0c667e680a..18a1c0004e 100644 --- a/apps/openmw/mwmechanics/spellutil.cpp +++ b/apps/openmw/mwmechanics/spellutil.cpp @@ -24,7 +24,7 @@ namespace MWMechanics return schoolSkillArray.at(school); } - float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect) + float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, const EffectCostMethod method) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); if (!magicEffect) @@ -39,14 +39,43 @@ namespace MWMechanics duration = std::max(1, duration); static const float fEffectCostMult = store.get().find("fEffectCostMult")->mValue.getFloat(); + int durationOffset = 0; + int minArea = 0; + if (method == EffectCostMethod::PlayerSpell) { + durationOffset = 1; + minArea = 1; + } + 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 *= durationOffset + duration; + x += 0.05 * std::max(minArea, effect.mArea) * magicEffect->mData.mBaseCost; return x * fEffectCostMult; } + int calcSpellCost (const ESM::Spell& spell) + { + if (!(spell.mData.mFlags & ESM::Spell::F_Autocalc)) + return spell.mData.mCost; + + float cost = 0; + + for (const ESM::ENAMstruct& effect : spell.mEffects.mList) + { + float effectCost = std::max(0.f, MWMechanics::calcEffectCost(effect)); + + // This is applied to the whole spell cost for each effect when + // creating spells, but is only applied on the effect itself in TES:CS. + if (effect.mRange == ESM::RT_Target) + effectCost *= 1.5; + + cost += effectCost; + } + + return std::round(cost); + } + int getEffectiveEnchantmentCastCost(float castCost, const MWWorld::Ptr &actor) { /* @@ -97,7 +126,7 @@ namespace MWMechanics float actorWillpower = stats.getAttribute(ESM::Attribute::Willpower).getModified(); float actorLuck = stats.getAttribute(ESM::Attribute::Luck).getModified(); - float castChance = (lowestSkill - spell->mData.mCost + 0.2f * actorWillpower + 0.1f * actorLuck); + float castChance = (lowestSkill - calcSpellCost(*spell) + 0.2f * actorWillpower + 0.1f * actorLuck); return castChance; } @@ -123,7 +152,7 @@ namespace MWMechanics if (spell->mData.mType != ESM::Spell::ST_Spell) return 100; - if (checkMagicka && spell->mData.mCost > 0 && stats.getMagicka().getCurrent() < spell->mData.mCost) + if (checkMagicka && calcSpellCost(*spell) > 0 && stats.getMagicka().getCurrent() < calcSpellCost(*spell)) return 0; if (spell->mData.mFlags & ESM::Spell::F_Always) diff --git a/apps/openmw/mwmechanics/spellutil.hpp b/apps/openmw/mwmechanics/spellutil.hpp index 865a9126e7..81f39b6dda 100644 --- a/apps/openmw/mwmechanics/spellutil.hpp +++ b/apps/openmw/mwmechanics/spellutil.hpp @@ -19,7 +19,13 @@ namespace MWMechanics { ESM::Skill::SkillEnum spellSchoolToSkill(int school); - float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect = nullptr); + enum class EffectCostMethod { + GameSpell, + PlayerSpell, + }; + + float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect = nullptr, const EffectCostMethod method = EffectCostMethod::GameSpell); + int calcSpellCost (const ESM::Spell& spell); int getEffectiveEnchantmentCastCost (float castCost, const MWWorld::Ptr& actor); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 45a7dd8a74..611e535c88 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -40,6 +40,7 @@ #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellcasting.hpp" +#include "../mwmechanics/spellutil.hpp" #include "../mwmechanics/levelledlist.hpp" #include "../mwmechanics/combat.hpp" #include "../mwmechanics/aiavoiddoor.hpp" //Used to tell actors to avoid doors @@ -3017,11 +3018,12 @@ namespace MWWorld if (!selectedSpell.empty()) { const ESM::Spell* spell = mStore.get().find(selectedSpell); + int spellCost = MWMechanics::calcSpellCost(*spell); // Check mana bool godmode = (isPlayer && mGodMode); MWMechanics::DynamicStat magicka = stats.getMagicka(); - if (spell->mData.mCost > 0 && magicka.getCurrent() < spell->mData.mCost && !godmode) + if (spellCost > 0 && magicka.getCurrent() < spellCost && !godmode) { message = "#{sMagicInsufficientSP}"; fail = true; @@ -3037,7 +3039,7 @@ namespace MWWorld // Reduce mana if (!fail && !godmode) { - magicka.setCurrent(magicka.getCurrent() - spell->mData.mCost); + magicka.setCurrent(magicka.getCurrent() - spellCost); stats.setMagicka(magicka); } }