From dc1fe62dded40110fefb3f26f86e8b0e70b614b9 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Fri, 27 Aug 2021 20:07:50 +0200 Subject: [PATCH 1/8] Overhaul magic effects to work with onApply and onEnd events --- CHANGELOG.md | 9 + apps/essimporter/converter.hpp | 4 +- apps/openmw/CMakeLists.txt | 4 +- apps/openmw/mwbase/mechanicsmanager.hpp | 2 - apps/openmw/mwbase/world.hpp | 4 +- apps/openmw/mwgui/charactercreation.cpp | 16 - apps/openmw/mwgui/container.cpp | 2 +- apps/openmw/mwgui/jailscreen.cpp | 5 +- apps/openmw/mwgui/spellbuyingwindow.cpp | 8 +- apps/openmw/mwgui/spellcreationdialog.cpp | 4 +- apps/openmw/mwgui/spellicons.cpp | 54 +- apps/openmw/mwgui/spellicons.hpp | 14 - apps/openmw/mwgui/spellmodel.cpp | 3 +- apps/openmw/mwmechanics/activespells.cpp | 569 +++++---- apps/openmw/mwmechanics/activespells.hpp | 129 ++- apps/openmw/mwmechanics/actors.cpp | 824 ++----------- apps/openmw/mwmechanics/actors.hpp | 7 +- apps/openmw/mwmechanics/aicombataction.cpp | 12 +- apps/openmw/mwmechanics/creaturestats.cpp | 44 +- apps/openmw/mwmechanics/creaturestats.hpp | 13 +- apps/openmw/mwmechanics/disease.hpp | 7 +- apps/openmw/mwmechanics/linkedeffects.cpp | 75 -- apps/openmw/mwmechanics/linkedeffects.hpp | 32 - apps/openmw/mwmechanics/magiceffects.cpp | 12 +- apps/openmw/mwmechanics/magiceffects.hpp | 10 - .../mwmechanics/mechanicsmanagerimp.cpp | 22 +- .../mwmechanics/mechanicsmanagerimp.hpp | 2 - apps/openmw/mwmechanics/spellabsorption.cpp | 45 +- apps/openmw/mwmechanics/spellcasting.cpp | 401 ++----- apps/openmw/mwmechanics/spellcasting.hpp | 13 +- apps/openmw/mwmechanics/spelleffects.cpp | 1032 +++++++++++++++++ apps/openmw/mwmechanics/spelleffects.hpp | 20 + apps/openmw/mwmechanics/spelllist.hpp | 6 - apps/openmw/mwmechanics/spellpriority.cpp | 22 +- apps/openmw/mwmechanics/spells.cpp | 234 +--- apps/openmw/mwmechanics/spells.hpp | 26 +- apps/openmw/mwmechanics/summoning.cpp | 112 +- apps/openmw/mwmechanics/summoning.hpp | 23 +- apps/openmw/mwmechanics/tickableeffects.cpp | 216 ---- apps/openmw/mwmechanics/tickableeffects.hpp | 20 - apps/openmw/mwrender/npcanimation.cpp | 28 - apps/openmw/mwrender/npcanimation.hpp | 1 - apps/openmw/mwscript/miscextensions.cpp | 10 +- apps/openmw/mwscript/statsextensions.cpp | 18 +- apps/openmw/mwworld/cellstore.cpp | 8 + apps/openmw/mwworld/esmloader.cpp | 203 ++++ apps/openmw/mwworld/esmloader.hpp | 5 + apps/openmw/mwworld/inventorystore.cpp | 271 +---- apps/openmw/mwworld/inventorystore.hpp | 43 +- apps/openmw/mwworld/player.cpp | 11 +- apps/openmw/mwworld/projectilemanager.cpp | 10 +- apps/openmw/mwworld/projectilemanager.hpp | 3 +- apps/openmw/mwworld/worldimp.cpp | 78 +- apps/openmw/mwworld/worldimp.hpp | 4 +- components/esm/activespells.cpp | 97 +- components/esm/activespells.hpp | 35 +- components/esm/creaturestats.cpp | 52 +- components/esm/creaturestats.hpp | 1 + components/esm/magiceffects.cpp | 18 +- components/esm/magiceffects.hpp | 4 +- components/esm/projectilestate.cpp | 5 + components/esm/projectilestate.hpp | 1 + components/esm/savedgame.cpp | 2 +- components/esm/spellstate.cpp | 69 +- components/esm/spellstate.hpp | 8 +- 65 files changed, 2334 insertions(+), 2708 deletions(-) delete mode 100644 apps/openmw/mwmechanics/linkedeffects.cpp delete mode 100644 apps/openmw/mwmechanics/linkedeffects.hpp create mode 100644 apps/openmw/mwmechanics/spelleffects.cpp create mode 100644 apps/openmw/mwmechanics/spelleffects.hpp delete mode 100644 apps/openmw/mwmechanics/tickableeffects.cpp delete mode 100644 apps/openmw/mwmechanics/tickableeffects.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dc9510f6e..ffe687a04c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ 0.48.0 ------ + Bug #1751: Birthsign abilities increase modified attribute values instead of base ones Bug #3246: ESSImporter: Most NPCs are dead on save load Bug #3514: Editing a reference's position after loading an esp file makes the reference disappear Bug #3737: Scripts from The Underground 2 .esp do not play (all patched versions) @@ -16,13 +17,18 @@ Bug #5453: Magic effect VFX are offset for creatures Bug #5483: AutoCalc flag is not used to calculate spells cost Bug #5508: Engine binary links to Qt without using it + Bug #5596: Effects in constant spells should not be merged + Bug #5621: Drained stats cannot be restored Bug #5755: Active grid object paging - disappearing textures Bug #5788: Texture editing parses the selected indexes wrongly + Bug #5801: A multi-effect spell with the intervention effects and recall always favors Almsivi intervention Bug #5842: GetDisposition adds temporary disposition change from different actors + Bug #5863: GetEffect should return true after the player has teleported Bug #6037: Morrowind Content Language Cannot be Set to English in OpenMW Launcher Bug #6051: NaN water height in ESM file is not handled gracefully Bug #6066: addtopic "return" does not work from within script. No errors thrown Bug #6067: esp loader fails in for certain subrecord orders + Bug #6087: Bound items added directly to the inventory disappear if their corresponding spell effect ends Bug #6101: Disarming trapped unlocked owned objects isn't considered a crime Bug #6107: Fatigue is incorrectly recalculated when fortify effect is applied or removed Bug #6115: Showmap overzealous matching @@ -36,6 +42,7 @@ Bug #6174: Spellmaking and Enchanting sliders differences from vanilla Bug #6184: Command and Calm and Demoralize and Frenzy and Rally magic effects inconsistencies with vanilla Bug #6197: Infinite Casting Loop + Bug #6223: Some Constant Effect Bound Items inconsistencies Bug #6273: Respawning NPCs rotation is inconsistent Bug #6282: Laura craft doesn't follow the player character Bug #6283: Avis Dorsey follows you after her death @@ -45,8 +52,10 @@ Feature #2554: Modifying an object triggers the instances table to scroll to the corresponding record Feature #2780: A way to see current OpenMW version in the console Feature #3616: Allow Zoom levels on the World Map + Feature #4297: Implement APPLIED_ONCE flag for magic effects Feature #4595: Unique object identifier Feature #4737: Handle instance move from one cell to another + Feature #5198: Implement "Magic effect expired" event Feature #5489: MCP: Telekinesis fix for activators Feature #5996: Support Lua scripts in OpenMW Feature #6017: Separate persistent and temporary cell references when saving diff --git a/apps/essimporter/converter.hpp b/apps/essimporter/converter.hpp index 46e4426dc5..81b9711bbf 100644 --- a/apps/essimporter/converter.hpp +++ b/apps/essimporter/converter.hpp @@ -124,11 +124,9 @@ public: { mContext->mPlayer.mObject.mCreatureStats.mLevel = npc.mNpdt.mLevel; mContext->mPlayerBase = npc; - ESM::SpellState::SpellParams empty; // FIXME: player start spells and birthsign spells aren't listed here, // need to fix openmw to account for this - for (const auto & spell : npc.mSpells.mList) - mContext->mPlayer.mObject.mCreatureStats.mSpells.mSpells[spell] = empty; + mContext->mPlayer.mObject.mCreatureStats.mSpells.mSpells = npc.mSpells.mList; // Clear the list now that we've written it, this prevents issues cropping up with // ensureCustomData() in OpenMW tripping over no longer existing spells, where an error would be fatal. diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 82a91699a9..7ad9856a77 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -92,8 +92,8 @@ add_openmw_dir (mwmechanics drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor aibreathe aicast aiescort aiface aiactivate aicombat recharge repair enchanting pathfinding pathgrid security spellcasting spellresistance disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning - character actors objects aistate trading weaponpriority spellpriority weapontype spellutil tickableeffects - spellabsorption linkedeffects + character actors objects aistate trading weaponpriority spellpriority weapontype spellutil + spellabsorption spelleffects ) add_openmw_dir (mwstate diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index b638ecade9..ccc60afc27 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -277,8 +277,6 @@ namespace MWBase virtual float getAngleToPlayer(const MWWorld::Ptr& ptr) const = 0; virtual MWMechanics::GreetingState getGreetingState(const MWWorld::Ptr& ptr) const = 0; virtual bool isTurningToPlayer(const MWWorld::Ptr& ptr) const = 0; - - virtual void restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) = 0; }; } diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 1dbb2b96f6..e9fc6e5c7a 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -539,7 +539,7 @@ namespace MWBase virtual void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) = 0; - virtual void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) = 0; + virtual void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot) = 0; virtual void launchProjectile (MWWorld::Ptr& actor, MWWorld::Ptr& projectile, const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr& bow, float speed, float attackStrength) = 0; virtual void updateProjectilesCasters() = 0; @@ -591,7 +591,7 @@ namespace MWBase virtual void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const MWWorld::Ptr& ignore, ESM::RangeType rangeType, const std::string& id, - const std::string& sourceName, const bool fromProjectile=false) = 0; + const std::string& sourceName, const bool fromProjectile=false, int slot = 0) = 0; virtual void activate (const MWWorld::Ptr& object, const MWWorld::Ptr& actor) = 0; diff --git a/apps/openmw/mwgui/charactercreation.cpp b/apps/openmw/mwgui/charactercreation.cpp index 827f87c7d6..43da1ef83f 100644 --- a/apps/openmw/mwgui/charactercreation.cpp +++ b/apps/openmw/mwgui/charactercreation.cpp @@ -72,13 +72,6 @@ namespace return {question, {r2, r1, r0}, sound}; } } - - void updatePlayerHealth() - { - MWWorld::Ptr player = MWMechanics::getPlayer(); - MWMechanics::NpcStats& npcStats = player.getClass().getNpcStats(player); - npcStats.updateHealth(); - } } namespace MWGui @@ -372,8 +365,6 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeDialog(mPickClassDialog); mPickClassDialog = nullptr; } - - updatePlayerHealth(); } void CharacterCreation::onPickClassDialogDone(WindowBase* parWindow) @@ -448,8 +439,6 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeDialog(mRaceDialog); mRaceDialog = nullptr; } - - updatePlayerHealth(); } void CharacterCreation::onRaceDialogBack() @@ -477,8 +466,6 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeDialog(mBirthSignDialog); mBirthSignDialog = nullptr; } - - updatePlayerHealth(); } void CharacterCreation::onBirthSignDialogDone(WindowBase* parWindow) @@ -527,7 +514,6 @@ namespace MWGui // Do not delete dialog, so that choices are remembered in case we want to go back and adjust them later mCreateClassDialog->setVisible(false); } - updatePlayerHealth(); } void CharacterCreation::onCreateClassDialogDone(WindowBase* parWindow) @@ -719,8 +705,6 @@ namespace MWGui MWBase::Environment::get().getWorld()->getStore().get().find(mGenerateClass); mPlayerClass = *klass; - - updatePlayerHealth(); } void CharacterCreation::onGenerateClassBack() diff --git a/apps/openmw/mwgui/container.cpp b/apps/openmw/mwgui/container.cpp index b649e41b0f..df490943d2 100644 --- a/apps/openmw/mwgui/container.cpp +++ b/apps/openmw/mwgui/container.cpp @@ -262,7 +262,7 @@ namespace MWGui } // Clean up summoned creatures as well - std::map& creatureMap = creatureStats.getSummonedCreatureMap(); + auto& creatureMap = creatureStats.getSummonedCreatureMap(); for (const auto& creature : creatureMap) MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(ptr, creature.second); creatureMap.clear(); diff --git a/apps/openmw/mwgui/jailscreen.cpp b/apps/openmw/mwgui/jailscreen.cpp index cc793073e3..a029fe54b6 100644 --- a/apps/openmw/mwgui/jailscreen.cpp +++ b/apps/openmw/mwgui/jailscreen.cpp @@ -82,10 +82,7 @@ namespace MWGui MWBase::Environment::get().getWorld()->advanceTime(mDays * 24); // We should not worsen corprus when in prison - for (auto& spell : player.getClass().getCreatureStats(player).getCorprusSpells()) - { - spell.second.mNextWorsening += mDays * 24; - } + player.getClass().getCreatureStats(player).getActiveSpells().skipWorsenings(mDays * 24); std::set skills; for (int day=0; day spellsToSort; - for (MWMechanics::Spells::TIterator iter = merchantSpells.begin(); iter!=merchantSpells.end(); ++iter) + for (const ESM::Spell* spell : merchantSpells) { - const ESM::Spell* spell = iter->first; - if (spell->mData.mType!=ESM::Spell::ST_Spell) continue; // don't try to sell diseases, curses or powers @@ -115,10 +113,10 @@ namespace MWGui continue; } - if (playerHasSpell(iter->first->mId)) + if (playerHasSpell(spell->mId)) continue; - spellsToSort.push_back(iter->first); + spellsToSort.push_back(spell); } std::stable_sort(spellsToSort.begin(), spellsToSort.end(), sortSpells); diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 01653d9e6f..8c0ea865a8 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -520,10 +520,8 @@ namespace MWGui std::vector knownEffects; - for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - const ESM::Spell* spell = it->first; - // only normal spells count if (spell->mData.mType != ESM::Spell::ST_Spell) continue; diff --git a/apps/openmw/mwgui/spellicons.cpp b/apps/openmw/mwgui/spellicons.cpp index 405abfbae7..4a86503f46 100644 --- a/apps/openmw/mwgui/spellicons.cpp +++ b/apps/openmw/mwgui/spellicons.cpp @@ -24,50 +24,33 @@ namespace MWGui { - - void EffectSourceVisitor::visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime, float totalTime) - { - MagicEffectInfo newEffectSource; - newEffectSource.mKey = key; - newEffectSource.mMagnitude = static_cast(magnitude); - newEffectSource.mPermanent = mIsPermanent; - newEffectSource.mRemainingTime = remainingTime; - newEffectSource.mSource = sourceName; - newEffectSource.mTotalTime = totalTime; - - mEffectSources[key.mId].push_back(newEffectSource); - } - - void SpellIcons::updateWidgets(MyGUI::Widget *parent, bool adjustSize) { - // TODO: Tracking add/remove/expire would be better than force updating every frame - MWWorld::Ptr player = MWMechanics::getPlayer(); const MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); - - EffectSourceVisitor visitor; - - // permanent item enchantments & permanent spells - visitor.mIsPermanent = true; - MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - store.visitEffectSources(visitor); - stats.getSpells().visitEffectSources(visitor); - - // now add lasting effects - visitor.mIsPermanent = false; - stats.getActiveSpells().visitEffectSources(visitor); - - std::map >& effects = visitor.mEffectSources; + std::map> effects; + for(const auto& params : stats.getActiveSpells()) + { + for(const auto& effect : params.getEffects()) + { + if(!effect.mMagnitude) + continue; + MagicEffectInfo newEffectSource; + newEffectSource.mKey = MWMechanics::EffectKey(effect.mEffectId, effect.mArg); + newEffectSource.mMagnitude = static_cast(effect.mMagnitude); + newEffectSource.mPermanent = effect.mDuration == -1.f; + newEffectSource.mRemainingTime = effect.mTimeLeft; + newEffectSource.mSource = params.getDisplayName(); + newEffectSource.mTotalTime = effect.mDuration; + effects[effect.mEffectId].push_back(newEffectSource); + } + } int w=2; - for (auto& effectInfoPair : effects) + for (const auto& [effectId, effectInfos] : effects) { - const int effectId = effectInfoPair.first; const ESM::MagicEffect* effect = MWBase::Environment::get().getWorld ()->getStore ().get().find(effectId); @@ -78,7 +61,6 @@ namespace MWGui static const float fadeTime = MWBase::Environment::get().getWorld()->getStore().get().find("fMagicStartIconBlink")->mValue.getFloat(); - std::vector& effectInfos = effectInfoPair.second; bool addNewLine = false; for (const MagicEffectInfo& effectInfo : effectInfos) { diff --git a/apps/openmw/mwgui/spellicons.hpp b/apps/openmw/mwgui/spellicons.hpp index b6aa49e69e..9825162a33 100644 --- a/apps/openmw/mwgui/spellicons.hpp +++ b/apps/openmw/mwgui/spellicons.hpp @@ -37,20 +37,6 @@ namespace MWGui bool mPermanent; // the effect is permanent }; - class EffectSourceVisitor : public MWMechanics::EffectSourceVisitor - { - public: - bool mIsPermanent; - - std::map > mEffectSources; - - virtual ~EffectSourceVisitor() {} - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override; - }; - class SpellIcons { public: diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp index fe18b0f082..61ea9ce93a 100644 --- a/apps/openmw/mwgui/spellmodel.cpp +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -92,9 +92,8 @@ namespace MWGui std::string filter = Misc::StringUtils::lowerCaseUtf8(mFilter); - for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - const ESM::Spell* spell = it->first; if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) continue; diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index 319d797257..6ad6ef369e 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -5,347 +5,442 @@ #include +#include "creaturestats.hpp" +#include "spellcasting.hpp" +#include "spelleffects.hpp" + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/inventorystore.hpp" -namespace MWMechanics +namespace { - void ActiveSpells::update(float duration) const + bool merge(std::vector& present, const std::vector& queued) { - bool rebuild = false; - - // Erase no longer active spells and effects - if (duration > 0) + // Can't merge if we already have an effect with the same effect index + auto problem = std::find_if(queued.begin(), queued.end(), [&] (const auto& qEffect) { - TContainer::iterator iter (mSpells.begin()); - while (iter!=mSpells.end()) - { - if (!timeToExpire (iter)) - { - mSpells.erase (iter++); - rebuild = true; - } - else - { - bool interrupt = false; - std::vector& effects = iter->second.mEffects; - for (std::vector::iterator effectIt = effects.begin(); effectIt != effects.end();) - { - if (effectIt->mTimeLeft <= 0) - { - rebuild = true; - - // Note: it we expire a Corprus effect, we should remove the whole spell. - if (effectIt->mEffectId == ESM::MagicEffect::Corprus) - { - iter = mSpells.erase (iter); - interrupt = true; - break; - } - - effectIt = effects.erase(effectIt); - } - else - { - effectIt->mTimeLeft -= duration; - ++effectIt; - } - } - - if (!interrupt) - ++iter; - } - } - } - - if (mSpellsChanged) - { - mSpellsChanged = false; - rebuild = true; - } - - if (rebuild) - rebuildEffects(); + return std::find_if(present.begin(), present.end(), [&] (const auto& pEffect) { return pEffect.mEffectIndex == qEffect.mEffectIndex; }) != present.end(); + }); + if(problem != queued.end()) + return false; + present.insert(present.end(), queued.begin(), queued.end()); + return true; } - void ActiveSpells::rebuildEffects() const + void addEffects(std::vector& effects, const ESM::EffectList& list, bool ignoreResistances = false) { - mEffects = MagicEffects(); - - for (TIterator iter (begin()); iter!=end(); ++iter) + int currentEffectIndex = 0; + for(const auto& enam : list.mList) { - const std::vector& effects = iter->second.mEffects; - - for (std::vector::const_iterator effectIt = effects.begin(); effectIt != effects.end(); ++effectIt) - { - if (effectIt->mTimeLeft > 0) - mEffects.add(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), MWMechanics::EffectParam(effectIt->mMagnitude)); - } + ESM::ActiveEffect effect; + effect.mEffectId = enam.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mMagnitude = 0.f; + effect.mMinMagnitude = enam.mMagnMin; + effect.mMaxMagnitude = enam.mMagnMax; + effect.mEffectIndex = currentEffectIndex++; + effect.mFlags = ESM::ActiveEffect::Flag_None; + if(ignoreResistances) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Resistances; + effect.mDuration = -1; + effect.mTimeLeft = -1; + effects.emplace_back(effect); } } +} - ActiveSpells::ActiveSpells() - : mSpellsChanged (false) - {} +namespace MWMechanics +{ + ActiveSpells::IterationGuard::IterationGuard(ActiveSpells& spells) : mActiveSpells(spells) + { + mActiveSpells.mIterating = true; + } - const MagicEffects& ActiveSpells::getMagicEffects() const + ActiveSpells::IterationGuard::~IterationGuard() { - update(0.f); - return mEffects; + mActiveSpells.mIterating = false; } - ActiveSpells::TIterator ActiveSpells::begin() const + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const CastSpell& cast, const MWWorld::Ptr& caster) + : mId(cast.mId), mDisplayName(cast.mSourceName), mCasterActorId(-1), mSlot(cast.mSlot), mType(cast.mType), mWorsenings(-1) { - return mSpells.begin(); + if(!caster.isEmpty() && caster.getClass().isActor()) + mCasterActorId = caster.getClass().getCreatureStats(caster).getActorId(); } - ActiveSpells::TIterator ActiveSpells::end() const + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances) + : mId(spell->mId), mDisplayName(spell->mName), mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()), mSlot(0) + , mType(spell->mData.mType == ESM::Spell::ST_Ability ? ESM::ActiveSpells::Type_Ability : ESM::ActiveSpells::Type_Permanent), mWorsenings(-1) { - return mSpells.end(); + assert(spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power); + addEffects(mEffects, spell->mEffects, ignoreResistances); } - double ActiveSpells::timeToExpire (const TIterator& iterator) const + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, int slotIndex, const MWWorld::Ptr& actor) + : mId(item.getCellRef().getRefId()), mDisplayName(item.getClass().getName(item)), mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) + , mSlot(slotIndex), mType(ESM::ActiveSpells::Type_Enchantment), mWorsenings(-1) { - const std::vector& effects = iterator->second.mEffects; + assert(enchantment->mData.mType == ESM::Enchantment::ConstantEffect); + addEffects(mEffects, enchantment->mEffects); + } - float duration = 0; + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ESM::ActiveSpells::ActiveSpellParams& params) + : mId(params.mId), mEffects(params.mEffects), mDisplayName(params.mDisplayName), mCasterActorId(params.mCasterActorId) + , mSlot(params.mItem.isSet() ? params.mItem.mIndex : 0) + , mType(params.mType), mWorsenings(params.mWorsenings), mNextWorsening({params.mNextWorsening}) + {} - for (std::vector::const_iterator iter (effects.begin()); - iter!=effects.end(); ++iter) + ESM::ActiveSpells::ActiveSpellParams ActiveSpells::ActiveSpellParams::toEsm() const + { + ESM::ActiveSpells::ActiveSpellParams params; + params.mId = mId; + params.mEffects = mEffects; + params.mDisplayName = mDisplayName; + params.mCasterActorId = mCasterActorId; + params.mItem.unset(); + if(mSlot) { - if (iter->mTimeLeft > duration) - duration = iter->mTimeLeft; + // Note that we're storing the inventory slot as a RefNum instead of an int as a matter of future proofing + // mSlot needs to be replaced with a RefNum once inventory items get persistent RefNum (#4508 #6148) + params.mItem = { static_cast(mSlot), 0 }; } + params.mType = mType; + params.mWorsenings = mWorsenings; + params.mNextWorsening = mNextWorsening.toEsm(); + return params; + } - if (duration < 0) - return 0; - - return duration; + void ActiveSpells::ActiveSpellParams::worsen() + { + ++mWorsenings; + if(!mWorsenings) + mNextWorsening = MWBase::Environment::get().getWorld()->getTimeStamp(); + mNextWorsening += CorprusStats::sWorseningPeriod; } - bool ActiveSpells::isSpellActive(const std::string& id) const + bool ActiveSpells::ActiveSpellParams::shouldWorsen() const { - for (TContainer::iterator iter = mSpells.begin(); iter != mSpells.end(); ++iter) - { - if (Misc::StringUtils::ciEqual(iter->first, id)) - return true; - } - return false; + return mWorsenings >= 0 && MWBase::Environment::get().getWorld()->getTimeStamp() >= mNextWorsening; } - const ActiveSpells::TContainer& ActiveSpells::getActiveSpells() const + void ActiveSpells::ActiveSpellParams::resetWorsenings() { - return mSpells; + mWorsenings = -1; } - void ActiveSpells::addSpell(const std::string &id, bool stack, const std::vector& effects, - const std::string &displayName, int casterActorId) + void ActiveSpells::update(const MWWorld::Ptr& ptr, float duration) { - TContainer::iterator it(mSpells.find(id)); + const auto& creatureStats = ptr.getClass().getCreatureStats(ptr); + assert(&creatureStats.getActiveSpells() == this); + IterationGuard guard{*this}; + // Erase no longer active spells and effects + for(auto spellIt = mSpells.begin(); spellIt != mSpells.end();) + { + if(spellIt->mType != ESM::ActiveSpells::Type_Temporary && spellIt->mType != ESM::ActiveSpells::Type_Consumable) + { + ++spellIt; + continue; + } + bool removedSpell = false; + for(auto effectIt = spellIt->mEffects.begin(); effectIt != spellIt->mEffects.end();) + { + if(effectIt->mFlags & ESM::ActiveEffect::Flag_Remove && effectIt->mTimeLeft <= 0.f) + { + auto effect = *effectIt; + effectIt = spellIt->mEffects.erase(effectIt); + onMagicEffectRemoved(ptr, *spellIt, effect); + removedSpell = applyPurges(ptr, &spellIt, &effectIt); + if(removedSpell) + break; + } + else + { + ++effectIt; + } + } + if(removedSpell) + continue; + if(spellIt->mEffects.empty()) + spellIt = mSpells.erase(spellIt); + else + ++spellIt; + } - ActiveSpellParams params; - params.mEffects = effects; - params.mDisplayName = displayName; - params.mCasterActorId = casterActorId; + for(const auto& spell : mQueue) + addToSpells(ptr, spell); + mQueue.clear(); - if (it == end() || stack) + // Vanilla only does this on cell change I think + const auto& spells = creatureStats.getSpells(); + for(const ESM::Spell* spell : spells) { - mSpells.insert(std::make_pair(id, params)); + if(spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power && !isSpellActive(spell->mId)) + mSpells.emplace_back(ActiveSpellParams{spell, ptr}); } - else + + if(ptr.getClass().hasInventoryStore(ptr) && !(creatureStats.isDead() && !creatureStats.isDeathAnimationFinished())) { - // addSpell() is called with effects for a range. - // but a spell may have effects with different ranges (e.g. Touch & Target) - // so, if we see new effects for same spell assume additional - // spell effects and add to existing effects of spell - mergeEffects(params.mEffects, it->second.mEffects); - it->second = params; + auto& store = ptr.getClass().getInventoryStore(ptr); + if(store.getInvListener() != nullptr) + { + bool playNonLooping = !store.isFirstEquip(); + const auto world = MWBase::Environment::get().getWorld(); + for(int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) + { + auto slot = store.getSlot(slotIndex); + if(slot == store.end()) + continue; + const auto& enchantmentId = slot->getClass().getEnchantment(*slot); + if(enchantmentId.empty()) + continue; + const ESM::Enchantment* enchantment = world->getStore().get().find(enchantmentId); + if(enchantment->mData.mType != ESM::Enchantment::ConstantEffect) + continue; + if(std::find_if(mSpells.begin(), mSpells.end(), [&] (const ActiveSpellParams& params) + { + return params.mSlot == slotIndex && params.mType == ESM::ActiveSpells::Type_Enchantment && params.mId == slot->getCellRef().getRefId(); + }) != mSpells.end()) + continue; + ActiveSpellParams params(*slot, enchantment, slotIndex, ptr); + mSpells.emplace_back(params); + for(const auto& effect : params.mEffects) + MWMechanics::playEffects(ptr, *world->getStore().get().find(effect.mEffectId), playNonLooping); + } + } } - mSpellsChanged = true; - } - - void ActiveSpells::mergeEffects(std::vector& addTo, const std::vector& from) - { - for (std::vector::const_iterator effect(from.begin()); effect != from.end(); ++effect) + // Update effects + for(auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { - // if effect is not in addTo, add it - bool missing = true; - for (std::vector::const_iterator iter(addTo.begin()); iter != addTo.end(); ++iter) + const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spellIt->mCasterActorId); //Maybe make this search outside active grid? + bool removedSpell = false; + for(auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();) { - if ((effect->mEffectId == iter->mEffectId) && (effect->mArg == iter->mArg)) - { - missing = false; + bool remove = applyMagicEffect(ptr, caster, *spellIt, *it, duration); + if(remove) + it = spellIt->mEffects.erase(it); + else + ++it; + removedSpell = applyPurges(ptr, &spellIt, &it); + if(removedSpell) break; - } } - if (missing) + if(removedSpell) + continue; + + bool remove = false; + if(spellIt->mType == ESM::ActiveSpells::Type_Ability || spellIt->mType == ESM::ActiveSpells::Type_Permanent) + remove = !spells.hasSpell(spellIt->mId); + else if(spellIt->mType == ESM::ActiveSpells::Type_Enchantment) { - addTo.push_back(*effect); + const auto& store = ptr.getClass().getInventoryStore(ptr); + auto slot = store.getSlot(spellIt->mSlot); + remove = slot == store.end() || slot->getCellRef().getRefId() != spellIt->mId; } + if(remove) + { + auto params = *spellIt; + spellIt = mSpells.erase(spellIt); + for(const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); + applyPurges(ptr, &spellIt); + continue; + } + ++spellIt; } } - void ActiveSpells::removeEffects(const std::string &id) + void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell) { - for (TContainer::iterator spell = mSpells.begin(); spell != mSpells.end(); ++spell) + if(spell.mType != ESM::ActiveSpells::Type_Consumable) { - if (spell->first == id) + auto found = std::find_if(mSpells.begin(), mSpells.end(), [&] (const auto& existing) + { + return spell.mId == existing.mId && spell.mCasterActorId == existing.mCasterActorId && spell.mSlot == existing.mSlot; + }); + if(found != mSpells.end()) { - spell->second.mEffects.clear(); - mSpellsChanged = true; + if(merge(found->mEffects, spell.mEffects)) + return; + auto params = *found; + mSpells.erase(found); + for(const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); } } + mSpells.emplace_back(spell); } - void ActiveSpells::visitEffectSources(EffectSourceVisitor &visitor) const + ActiveSpells::ActiveSpells() : mIterating(false) + {} + + ActiveSpells::TIterator ActiveSpells::begin() const { - for (TContainer::const_iterator it = begin(); it != end(); ++it) - { - for (std::vector::const_iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end(); ++effectIt) - { - std::string name = it->second.mDisplayName; + return mSpells.begin(); + } - float magnitude = effectIt->mMagnitude; - if (magnitude) - visitor.visit(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), effectIt->mEffectIndex, name, it->first, it->second.mCasterActorId, magnitude, effectIt->mTimeLeft, effectIt->mDuration); - } - } + ActiveSpells::TIterator ActiveSpells::end() const + { + return mSpells.end(); } - void ActiveSpells::purgeAll(float chance, bool spellOnly) + bool ActiveSpells::isSpellActive(const std::string& id) const { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ) + return std::find_if(mSpells.begin(), mSpells.end(), [&] (const auto& spell) { - const std::string spellId = it->first; + return Misc::StringUtils::ciEqual(spell.mId, id); + }) != mSpells.end(); + } - // if spellOnly is true, dispell only spells. Leave potions, enchanted items etc. - if (spellOnly) - { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId); - if (!spell || spell->mData.mType != ESM::Spell::ST_Spell) - { - ++it; - continue; - } - } + void ActiveSpells::addSpell(const ActiveSpellParams& params) + { + mQueue.emplace_back(params); + } - if (Misc::Rng::roll0to99() < chance) - mSpells.erase(it++); - else - ++it; - } - mSpellsChanged = true; + void ActiveSpells::addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor) + { + mQueue.emplace_back(ActiveSpellParams{spell, actor, true}); } - void ActiveSpells::purgeEffect(short effectId) + void ActiveSpells::purge(ParamsPredicate predicate, const MWWorld::Ptr& ptr) { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) + assert(&ptr.getClass().getCreatureStats(ptr).getActiveSpells() == this); + mPurges.emplace(predicate); + if(!mIterating) { - for (std::vector::iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end();) - { - if (effectIt->mEffectId == effectId) - effectIt = it->second.mEffects.erase(effectIt); - else - ++effectIt; - } + IterationGuard guard{*this}; + applyPurges(ptr); } - mSpellsChanged = true; } - void ActiveSpells::purgeEffect(short effectId, const std::string& sourceId, int effectIndex) + void ActiveSpells::purge(EffectPredicate predicate, const MWWorld::Ptr& ptr) { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) + assert(&ptr.getClass().getCreatureStats(ptr).getActiveSpells() == this); + mPurges.emplace(predicate); + if(!mIterating) { - for (std::vector::iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end();) - { - if (effectIt->mEffectId == effectId && it->first == sourceId && (effectIndex < 0 || effectIndex == effectIt->mEffectIndex)) - effectIt = it->second.mEffects.erase(effectIt); - else - ++effectIt; - } + IterationGuard guard{*this}; + applyPurges(ptr); } - mSpellsChanged = true; } - void ActiveSpells::purge(int casterActorId) + bool ActiveSpells::applyPurges(const MWWorld::Ptr& ptr, std::list::iterator* currentSpell, std::vector::iterator* currentEffect) { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) + bool removedCurrentSpell = false; + while(!mPurges.empty()) { - for (std::vector::iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end();) + auto predicate = mPurges.front(); + mPurges.pop(); + for(auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { - if (it->second.mCasterActorId == casterActorId) - effectIt = it->second.mEffects.erase(effectIt); - else - ++effectIt; + bool isCurrentSpell = currentSpell && *currentSpell == spellIt; + std::visit([&] (auto&& variant) + { + using T = std::decay_t; + if constexpr (std::is_same_v) + { + if(variant(*spellIt)) + { + auto params = *spellIt; + spellIt = mSpells.erase(spellIt); + if(isCurrentSpell) + { + *currentSpell = spellIt; + removedCurrentSpell = true; + } + for(const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); + } + else + ++spellIt; + } + else + { + static_assert(std::is_same_v, "Non-exhaustive visitor"); + for(auto effectIt = spellIt->mEffects.begin(); effectIt != spellIt->mEffects.end();) + { + if(variant(*spellIt, *effectIt)) + { + auto effect = *effectIt; + if(isCurrentSpell && currentEffect) + { + auto distance = std::distance(spellIt->mEffects.begin(), *currentEffect); + if(effectIt <= *currentEffect) + distance--; + effectIt = spellIt->mEffects.erase(effectIt); + *currentEffect = spellIt->mEffects.begin() + distance; + } + else + effectIt = spellIt->mEffects.erase(effectIt); + onMagicEffectRemoved(ptr, *spellIt, effect); + } + else + ++effectIt; + } + ++spellIt; + } + }, predicate); } } - mSpellsChanged = true; + return removedCurrentSpell; } - void ActiveSpells::purgeCorprusDisease() + void ActiveSpells::removeEffects(const MWWorld::Ptr& ptr, const std::string &id) { - for (TContainer::iterator iter = mSpells.begin(); iter!=mSpells.end();) + purge([=] (const ActiveSpellParams& params) { - bool hasCorprusEffect = false; - for (std::vector::iterator effectIt = iter->second.mEffects.begin(); - effectIt != iter->second.mEffects.end();++effectIt) - { - if (effectIt->mEffectId == ESM::MagicEffect::Corprus) - { - hasCorprusEffect = true; - break; - } - } - - if (hasCorprusEffect) - { - mSpells.erase(iter++); - mSpellsChanged = true; - } - else - ++iter; - } + return params.mId == id; + }, ptr); } - void ActiveSpells::clear() + void ActiveSpells::purgeEffect(const MWWorld::Ptr& ptr, short effectId) { - mSpells.clear(); - mSpellsChanged = true; + purge([=] (const ActiveSpellParams&, const ESM::ActiveEffect& effect) + { + return effect.mEffectId == effectId; + }, ptr); } - void ActiveSpells::writeState(ESM::ActiveSpells &state) const + void ActiveSpells::purge(const MWWorld::Ptr& ptr, int casterActorId) { - for (TContainer::const_iterator it = mSpells.begin(); it != mSpells.end(); ++it) + purge([=] (const ActiveSpellParams& params) { - // Stupid copying of almost identical structures. ESM::TimeStamp <-> MWWorld::TimeStamp - ESM::ActiveSpells::ActiveSpellParams params; - params.mEffects = it->second.mEffects; - params.mCasterActorId = it->second.mCasterActorId; - params.mDisplayName = it->second.mDisplayName; + return params.mCasterActorId == casterActorId; + }, ptr); + } - state.mSpells.insert (std::make_pair(it->first, params)); - } + void ActiveSpells::clear(const MWWorld::Ptr& ptr) + { + mQueue.clear(); + purge([] (const ActiveSpellParams& params) { return true; }, ptr); } - void ActiveSpells::readState(const ESM::ActiveSpells &state) + void ActiveSpells::skipWorsenings(double hours) { - for (ESM::ActiveSpells::TContainer::const_iterator it = state.mSpells.begin(); it != state.mSpells.end(); ++it) + for(auto& spell : mSpells) { - // Stupid copying of almost identical structures. ESM::TimeStamp <-> MWWorld::TimeStamp - ActiveSpellParams params; - params.mEffects = it->second.mEffects; - params.mCasterActorId = it->second.mCasterActorId; - params.mDisplayName = it->second.mDisplayName; - - mSpells.insert (std::make_pair(it->first, params)); - mSpellsChanged = true; + if(spell.mWorsenings >= 0) + spell.mNextWorsening += hours; } } + + void ActiveSpells::writeState(ESM::ActiveSpells &state) const + { + for(const auto& spell : mSpells) + state.mSpells.emplace_back(spell.toEsm()); + for(const auto& spell : mQueue) + state.mQueue.emplace_back(spell.toEsm()); + } + + void ActiveSpells::readState(const ESM::ActiveSpells &state) + { + for(const ESM::ActiveSpells::ActiveSpellParams& spell : state.mSpells) + mSpells.emplace_back(ActiveSpellParams{spell}); + for(const ESM::ActiveSpells::ActiveSpellParams& spell : state.mQueue) + mQueue.emplace_back(ActiveSpellParams{spell}); + } } diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 8bc29aa444..0538d7a8b7 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -1,40 +1,83 @@ #ifndef GAME_MWMECHANICS_ACTIVESPELLS_H #define GAME_MWMECHANICS_ACTIVESPELLS_H -#include -#include +#include +#include +#include #include +#include +#include #include #include "../mwworld/timestamp.hpp" +#include "../mwworld/ptr.hpp" #include "magiceffects.hpp" +#include "spellcasting.hpp" + +namespace ESM +{ + struct Enchantment; + struct Spell; +} namespace MWMechanics { /// \brief Lasting spell effects /// - /// \note The name of this class is slightly misleading, since it also handels lasting potion + /// \note The name of this class is slightly misleading, since it also handles lasting potion /// effects. class ActiveSpells { public: - typedef ESM::ActiveEffect ActiveEffect; - - struct ActiveSpellParams + using ActiveEffect = ESM::ActiveEffect; + class ActiveSpellParams { - std::vector mEffects; - MWWorld::TimeStamp mTimeStamp; - std::string mDisplayName; + std::string mId; + std::vector mEffects; + std::string mDisplayName; + int mCasterActorId; + int mSlot; + ESM::ActiveSpells::EffectType mType; + int mWorsenings; + MWWorld::TimeStamp mNextWorsening; + + ActiveSpellParams(const ESM::ActiveSpells::ActiveSpellParams& params); + + ActiveSpellParams(const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances = false); + + ActiveSpellParams(const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, int slotIndex, const MWWorld::Ptr& actor); + + ESM::ActiveSpells::ActiveSpellParams toEsm() const; + + friend class ActiveSpells; + public: + ActiveSpellParams(const CastSpell& cast, const MWWorld::Ptr& caster); + + const std::string& getId() const { return mId; } + + const std::vector& getEffects() const { return mEffects; } + std::vector& getEffects() { return mEffects; } - // The caster that inflicted this spell on us - int mCasterActorId; + ESM::ActiveSpells::EffectType getType() const { return mType; } + + int getCasterActorId() const { return mCasterActorId; } + + int getWorsenings() const { return mWorsenings; } + + const std::string& getDisplayName() const { return mDisplayName; } + + // Increments worsenings count and sets the next timestamp + void worsen(); + + bool shouldWorsen() const; + + void resetWorsenings(); }; - typedef std::multimap TContainer; - typedef TContainer::const_iterator TIterator; + typedef std::list::const_iterator TIterator; void readState (const ESM::ActiveSpells& state); void writeState (ESM::ActiveSpells& state) const; @@ -43,24 +86,29 @@ namespace MWMechanics TIterator end() const; - void update(float duration) const; + void update(const MWWorld::Ptr& ptr, float duration); private: + using ParamsPredicate = std::function; + using EffectPredicate = std::function; + using Predicate = std::variant; - mutable TContainer mSpells; - mutable MagicEffects mEffects; - mutable bool mSpellsChanged; + struct IterationGuard + { + ActiveSpells& mActiveSpells; - void rebuildEffects() const; + IterationGuard(ActiveSpells& spells); + ~IterationGuard(); + }; - /// Add any effects that are in "from" and not in "addTo" to "addTo" - void mergeEffects(std::vector& addTo, const std::vector& from); + std::list mSpells; + std::vector mQueue; + std::queue mPurges; + bool mIterating; - double timeToExpire (const TIterator& iterator) const; - ///< Returns time (in in-game hours) until the spell pointed to by \a iterator - /// expires. + void addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell); - const TContainer& getActiveSpells() const; + bool applyPurges(const MWWorld::Ptr& ptr, std::list::iterator* currentSpell = nullptr, std::vector::iterator* currentEffect = nullptr); public: @@ -70,40 +118,31 @@ namespace MWMechanics /// /// \brief addSpell /// \param id ID for stacking purposes. - /// \param stack If false, the spell is not added if one with the same ID exists already. - /// \param effects - /// \param displayName Name for display in magic menu. /// - void addSpell (const std::string& id, bool stack, const std::vector& effects, - const std::string& displayName, int casterActorId); + void addSpell (const ActiveSpellParams& params); + + /// Bypasses resistances + void addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor); /// Removes the active effects from this spell/potion/.. with \a id - void removeEffects (const std::string& id); + void removeEffects (const MWWorld::Ptr& ptr, const std::string& id); /// Remove all active effects with this effect id - void purgeEffect (short effectId); - - /// Remove all active effects with this effect id and source id - void purgeEffect (short effectId, const std::string& sourceId, int effectIndex=-1); + void purgeEffect (const MWWorld::Ptr& ptr, short effectId); - /// Remove all active effects, if roll succeeds (for each effect) - void purgeAll(float chance, bool spellOnly = false); + void purge(EffectPredicate predicate, const MWWorld::Ptr& ptr); + void purge(ParamsPredicate predicate, const MWWorld::Ptr& ptr); - /// Remove all effects with CASTER_LINKED flag that were cast by \a casterActorId - void purge (int casterActorId); + /// Remove all effects that were cast by \a casterActorId + void purge (const MWWorld::Ptr& ptr, int casterActorId); /// Remove all spells - void clear(); + void clear(const MWWorld::Ptr& ptr); bool isSpellActive (const std::string& id) const; ///< case insensitive - void purgeCorprusDisease(); - - const MagicEffects& getMagicEffects() const; - - void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor) const; - + void skipWorsenings(double hours); }; } diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 542f185854..b1aa246385 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -44,7 +44,6 @@ #include "actor.hpp" #include "summoning.hpp" #include "actorutil.hpp" -#include "tickableeffects.hpp" namespace { @@ -55,67 +54,26 @@ bool isConscious(const MWWorld::Ptr& ptr) return !stats.isDead() && !stats.getKnockedDown(); } -int getBoundItemSlot (const std::string& itemId) +bool isCommanded(const MWWorld::Ptr& actor) { - static std::map boundItemsMap; - if (boundItemsMap.empty()) + const auto& stats = actor.getClass().getCreatureStats(actor); + for(const auto& params : stats.getActiveSpells()) { - const auto& store = MWBase::Environment::get().getWorld()->getStore().get(); - - std::string boundId = store.find("sMagicBoundBootsID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Boots; - - boundId = store.find("sMagicBoundCuirassID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Cuirass; - - boundId = store.find("sMagicBoundLeftGauntletID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_LeftGauntlet; - - boundId = store.find("sMagicBoundRightGauntletID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_RightGauntlet; - - boundId = store.find("sMagicBoundHelmID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Helmet; - - boundId = store.find("sMagicBoundShieldID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_CarriedLeft; + for(const auto& effect : params.getEffects()) + { + if(((effect.mEffectId == ESM::MagicEffect::CommandHumanoid && actor.getClass().isNpc()) + || (effect.mEffectId == ESM::MagicEffect::CommandCreature && !actor.getClass().isNpc())) + && effect.mMagnitude >= stats.getLevel()) + return true; + } } - - int slot = MWWorld::InventoryStore::Slot_CarriedRight; - std::map::iterator it = boundItemsMap.find(itemId); - if (it != boundItemsMap.end()) - slot = it->second; - - return slot; + return false; } -class CheckActorCommanded : public MWMechanics::EffectSourceVisitor -{ - MWWorld::Ptr mActor; -public: - bool mCommanded; - - CheckActorCommanded(const MWWorld::Ptr& actor) - : mActor(actor) - , mCommanded(false){} - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (((key.mId == ESM::MagicEffect::CommandHumanoid && mActor.getClass().isNpc()) - || (key.mId == ESM::MagicEffect::CommandCreature && mActor.getTypeName() == typeid(ESM::Creature).name())) - && magnitude >= mActor.getClass().getCreatureStats(mActor).getLevel()) - mCommanded = true; - } -}; - // Check for command effects having ended and remove package if necessary void adjustCommandedActor (const MWWorld::Ptr& actor) { - CheckActorCommanded check(actor); MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); - stats.getActiveSpells().visitEffectSources(check); bool hasCommandPackage = false; @@ -130,7 +88,7 @@ void adjustCommandedActor (const MWWorld::Ptr& actor) } } - if (!check.mCommanded && hasCommandPackage) + if (!isCommanded(actor) && hasCommandPackage) stats.getAiSequence().erase(it); } @@ -169,129 +127,43 @@ void forEachFollowingPackage(MWMechanics::Actors::PtrActorMap& actors, const MWW } } -} - -namespace MWMechanics +float getStuntedMagickaDuration(const MWWorld::Ptr& actor) { - static const int GREETING_SHOULD_START = 4; // how many updates should pass before NPC can greet player - static const int GREETING_SHOULD_END = 20; // how many updates should pass before NPC stops turning to player - static const int GREETING_COOLDOWN = 40; // how many updates should pass before NPC can continue movement - static const float DECELERATE_DISTANCE = 512.f; - - namespace + float remainingTime = 0.f; + for(const auto& params : actor.getClass().getCreatureStats(actor).getActiveSpells()) { - float getTimeToDestination(const AiPackage& package, const osg::Vec3f& position, float speed, float duration, const osg::Vec3f& halfExtents) + for(const auto& effect : params.getEffects()) { - const auto distanceToNextPathPoint = (package.getNextPathPoint(package.getDestination()) - position).length(); - return (distanceToNextPathPoint - package.getNextPathPointTolerance(speed, duration, halfExtents)) / speed; - } - } - - class GetStuntedMagickaDuration : public MWMechanics::EffectSourceVisitor - { - public: - float mRemainingTime; - - GetStuntedMagickaDuration(const MWWorld::Ptr& actor) - : mRemainingTime(0.f){} - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (mRemainingTime == -1) return; - - if (key.mId == ESM::MagicEffect::StuntedMagicka) + if(effect.mEffectId == ESM::MagicEffect::StuntedMagicka) { - if (totalTime == -1) - { - mRemainingTime = -1; - return; - } - - if (remainingTime > mRemainingTime) - mRemainingTime = remainingTime; + if(effect.mDuration == -1.f) + return -1.f; + remainingTime = std::max(remainingTime, effect.mTimeLeft); } } - }; - - class GetCurrentMagnitudes : public MWMechanics::EffectSourceVisitor - { - std::string mSpellId; - - public: - GetCurrentMagnitudes(const std::string& spellId) - : mSpellId(spellId) - { - } - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (magnitude <= 0) - return; - - if (sourceId != mSpellId) - return; - - mMagnitudes.emplace_back(key, magnitude); - } - - std::vector> mMagnitudes; - }; - - class GetCorprusSpells : public MWMechanics::EffectSourceVisitor - { - - public: - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (key.mId != ESM::MagicEffect::Corprus) - return; - - mSpells.push_back(sourceId); - } - - std::vector mSpells; - }; - - class SoulTrap : public MWMechanics::EffectSourceVisitor - { - MWWorld::Ptr mCreature; - MWWorld::Ptr mActor; - bool mTrapped; - public: - SoulTrap(const MWWorld::Ptr& trappedCreature) - : mCreature(trappedCreature) - , mTrapped(false) - { - } - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (mTrapped) - return; - if (key.mId != ESM::MagicEffect::Soultrap) - return; - if (magnitude <= 0) - return; - - MWBase::World* world = MWBase::Environment::get().getWorld(); + } + return remainingTime; +} - MWWorld::Ptr caster = world->searchPtrViaActorId(casterActorId); +void soulTrap(const MWWorld::Ptr& creature) +{ + const auto& stats = creature.getClass().getCreatureStats(creature); + if(!stats.getMagicEffects().get(ESM::MagicEffect::Soultrap).getMagnitude()) + return; + int creatureSoulValue = creature.get()->mBase->mData.mSoul; + if (creatureSoulValue == 0) + return; + MWBase::World* world = MWBase::Environment::get().getWorld(); + static const float fSoulgemMult = world->getStore().get().find("fSoulgemMult")->mValue.getFloat(); + for(const auto& params : stats.getActiveSpells()) + { + for(const auto& effect : params.getEffects()) + { + if(effect.mEffectId != ESM::MagicEffect::Soultrap || effect.mMagnitude <= 0.f) + continue; + MWWorld::Ptr caster = world->searchPtrViaActorId(params.getCasterActorId()); if (caster.isEmpty() || !caster.getClass().isActor()) - return; - - static const float fSoulgemMult = world->getStore().get().find("fSoulgemMult")->mValue.getFloat(); - - int creatureSoulValue = mCreature.get()->mBase->mData.mSoul; - if (creatureSoulValue == 0) - return; + continue; // Use the smallest soulgem that is large enough to hold the soul MWWorld::ContainerStore& container = caster.getClass().getContainerStore(caster); @@ -316,126 +188,50 @@ namespace MWMechanics } if (gem == container.end()) - return; + continue; // Set the soul on just one of the gems, not the whole stack gem->getContainerStore()->unstack(*gem, caster); - gem->getCellRef().setSoul(mCreature.getCellRef().getRefId()); + gem->getCellRef().setSoul(creature.getCellRef().getRefId()); // Restack the gem with other gems with the same soul gem->getContainerStore()->restack(*gem); - mTrapped = true; - - if (caster == getPlayer()) + if (caster == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sSoultrapSuccess}"); - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() + const ESM::Static* fx = world->getStore().get() .search("VFX_Soul_Trap"); if (fx) - MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, - "", mCreature.getRefData().getPosition().asVec3()); + world->spawnEffect("meshes\\" + fx->mModel, "", creature.getRefData().getPosition().asVec3()); - MWBase::Environment::get().getSoundManager()->playSound3D( - mCreature.getRefData().getPosition().asVec3(), "conjuration hit", 1.f, 1.f - ); + MWBase::Environment::get().getSoundManager()->playSound3D(creature.getRefData().getPosition().asVec3(), "conjuration hit", 1.f, 1.f); + return; //remove to get vanilla behaviour } - }; - - void Actors::addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) - { - MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); - int slot = getBoundItemSlot(itemId); - - if (actor.getClass().getContainerStore(actor).count(itemId) != 0) - return; - - MWWorld::ContainerStoreIterator prevItem = store.getSlot(slot); - - MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1, actor); - MWWorld::ActionEquip action(boundPtr); - action.execute(actor); - - if (actor != MWMechanics::getPlayer()) - return; - - MWWorld::Ptr newItem; - auto it = store.getSlot(slot); - // Equip can fail because beast races cannot equip boots/helmets - if(it != store.end()) - newItem = *it; - - if (newItem.isEmpty() || boundPtr != newItem) - return; - - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - - // change draw state only if the item is in player's right hand - if (slot == MWWorld::InventoryStore::Slot_CarriedRight) - player.setDrawState(MWMechanics::DrawState_Weapon); - - if (prevItem != store.end()) - player.setPreviousItem(itemId, prevItem->getCellRef().getRefId()); } +} +} - void Actors::removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) - { - MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); - int slot = getBoundItemSlot(itemId); - - MWWorld::ContainerStoreIterator currentItem = store.getSlot(slot); - - bool wasEquipped = currentItem != store.end() && Misc::StringUtils::ciEqual(currentItem->getCellRef().getRefId(), itemId); - - if (actor != MWMechanics::getPlayer()) - { - store.remove(itemId, 1, actor); - - // Equip a replacement - if (!wasEquipped) - return; - - std::string type = currentItem->getTypeName(); - if (type != typeid(ESM::Weapon).name() && type != typeid(ESM::Armor).name() && type != typeid(ESM::Clothing).name()) - return; - - if (actor.getClass().getCreatureStats(actor).isDead()) - return; - - if (!actor.getClass().hasInventoryStore(actor)) - return; - - if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) - return; - - actor.getClass().getInventoryStore(actor).autoEquip(actor); - - return; - } - - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - std::string prevItemId = player.getPreviousItem(itemId); - player.erasePreviousItem(itemId); +namespace MWMechanics +{ + static const int GREETING_SHOULD_START = 4; // how many updates should pass before NPC can greet player + static const int GREETING_SHOULD_END = 20; // how many updates should pass before NPC stops turning to player + static const int GREETING_COOLDOWN = 40; // how many updates should pass before NPC can continue movement + static const float DECELERATE_DISTANCE = 512.f; - if (!prevItemId.empty()) + namespace + { + float getTimeToDestination(const AiPackage& package, const osg::Vec3f& position, float speed, float duration, const osg::Vec3f& halfExtents) { - // Find previous item (or its replacement) by id. - // we should equip previous item only if expired bound item was equipped. - MWWorld::Ptr item = store.findReplacement(prevItemId); - if (!item.isEmpty() && wasEquipped) - { - MWWorld::ActionEquip action(item); - action.execute(actor); - } + const auto distanceToNextPathPoint = (package.getNextPathPoint(package.getDestination()) - position).length(); + return (distanceToNextPathPoint - package.getNextPathPointTolerance(speed, duration, halfExtents)) / speed; } - - store.remove(itemId, 1, actor); } void Actors::updateActor (const MWWorld::Ptr& ptr, float duration) { // magic effects - adjustMagicEffects (ptr); + adjustMagicEffects (ptr, duration); if (ptr.getClass().getCreatureStats(ptr).needToRecalcDynamicStats()) calculateDynamicStats (ptr); @@ -793,23 +589,61 @@ namespace MWMechanics } } - void Actors::adjustMagicEffects (const MWWorld::Ptr& creature) + void Actors::adjustMagicEffects (const MWWorld::Ptr& creature, float duration) { - CreatureStats& creatureStats = creature.getClass().getCreatureStats (creature); - if (creatureStats.isDeathAnimationFinished()) - return; + CreatureStats& creatureStats = creature.getClass().getCreatureStats (creature); + bool wasDead = creatureStats.isDead(); - MagicEffects now = creatureStats.getSpells().getMagicEffects(); + creatureStats.getActiveSpells().update(creature, duration); - if (creature.getClass().hasInventoryStore(creature)) + if (!wasDead && creatureStats.isDead()) { - MWWorld::InventoryStore& store = creature.getClass().getInventoryStore (creature); - now += store.getMagicEffects(); + // The actor was killed by a magic effect. Figure out if the player was responsible for it. + const ActiveSpells& spells = creatureStats.getActiveSpells(); + MWWorld::Ptr player = getPlayer(); + std::set playerFollowers; + getActorsSidingWith(player, playerFollowers); + + for (ActiveSpells::TIterator it = spells.begin(); it != spells.end(); ++it) + { + bool actorKilled = false; + + const ActiveSpells::ActiveSpellParams& spell = *it; + MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spell.getCasterActorId()); + for (const auto& effect : spell.getEffects()) + { + static const std::array damageEffects{ + ESM::MagicEffect::FireDamage, ESM::MagicEffect::ShockDamage, ESM::MagicEffect::FrostDamage, ESM::MagicEffect::Poison, + ESM::MagicEffect::SunDamage, ESM::MagicEffect::DamageHealth, ESM::MagicEffect::AbsorbHealth + }; + + bool isDamageEffect = std::find(damageEffects.begin(), damageEffects.end(), effect.mEffectId) != damageEffects.end(); + + if (isDamageEffect) + { + if (caster.getClass().isNpc() && caster.getClass().getNpcStats(caster).isWerewolf()) + caster.getClass().getNpcStats(caster).addWerewolfKill(); + if (caster == player || playerFollowers.find(caster) != playerFollowers.end()) + { + MWBase::Environment::get().getMechanicsManager()->actorKilled(creature, player); + actorKilled = true; + break; + } + } + } + + if (actorKilled) + break; + } } - now += creatureStats.getActiveSpells().getMagicEffects(); + // updateSummons assumes the actor belongs to a cell. + // This assumption isn't always valid for the player character. + if (!creature.isInCell()) + return; - creatureStats.modifyMagicEffects(now); + if (!creatureStats.getSummonedCreatureMap().empty() || !creatureStats.getSummonedCreatureGraveyard().empty()) + updateSummons(creature, mTimerDisposeSummonsCorpses == 0.f); } void Actors::calculateDynamicStats (const MWWorld::Ptr& ptr) @@ -857,22 +691,18 @@ namespace MWMechanics if (stunted) { // Stunted Magicka effect should be taken into account. - GetStuntedMagickaDuration visitor(ptr); - stats.getActiveSpells().visitEffectSources(visitor); - stats.getSpells().visitEffectSources(visitor); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).visitEffectSources(visitor); + float remainingTime = getStuntedMagickaDuration(ptr); // Take a maximum remaining duration of Stunted Magicka effects (-1 is a constant one) in game hours. - if (visitor.mRemainingTime > 0) + if (remainingTime > 0) { double timeScale = MWBase::Environment::get().getWorld()->getTimeScaleFactor(); if(timeScale == 0.0) timeScale = 1; - restoreHours = std::max(0.0, hours - visitor.mRemainingTime * timeScale / 3600.f); + restoreHours = std::max(0.0, hours - remainingTime * timeScale / 3600.f); } - else if (visitor.mRemainingTime == -1) + else if (remainingTime == -1) restoreHours = 0; } @@ -933,398 +763,12 @@ namespace MWMechanics stats.setFatigue (fatigue); } - class ExpiryVisitor : public EffectSourceVisitor - { - private: - MWWorld::Ptr mActor; - float mDuration; - - public: - ExpiryVisitor(const MWWorld::Ptr& actor, float duration) - : mActor(actor), mDuration(duration) - { - } - - void visit (MWMechanics::EffectKey key, int /*effectIndex*/, - const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, - float magnitude, float remainingTime = -1, float /*totalTime*/ = -1) override - { - if (magnitude > 0 && remainingTime > 0 && remainingTime < mDuration) - { - CreatureStats& creatureStats = mActor.getClass().getCreatureStats(mActor); - if (effectTick(creatureStats, mActor, key, magnitude * remainingTime)) - creatureStats.getMagicEffects().add(key, -magnitude); - } - } - }; - - void Actors::applyCureEffects(const MWWorld::Ptr& actor) - { - CreatureStats &creatureStats = actor.getClass().getCreatureStats(actor); - const MagicEffects &effects = creatureStats.getMagicEffects(); - - if (effects.get(ESM::MagicEffect::CurePoison).getModifier() > 0) - { - creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Poison); - creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Poison); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Poison); - } - if (effects.get(ESM::MagicEffect::CureParalyzation).getModifier() > 0) - { - creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Paralyze); - creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Paralyze); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Paralyze); - } - if (effects.get(ESM::MagicEffect::CureCommonDisease).getModifier() > 0) - { - creatureStats.getSpells().purgeCommonDisease(); - } - if (effects.get(ESM::MagicEffect::CureBlightDisease).getModifier() > 0) - { - creatureStats.getSpells().purgeBlightDisease(); - } - if (effects.get(ESM::MagicEffect::CureCorprusDisease).getModifier() > 0) - { - creatureStats.getActiveSpells().purgeCorprusDisease(); - creatureStats.getSpells().purgeCorprusDisease(); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Corprus, true); - } - if (effects.get(ESM::MagicEffect::RemoveCurse).getModifier() > 0) - { - creatureStats.getSpells().purgeCurses(); - } - } - void Actors::calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration) { CreatureStats &creatureStats = ptr.getClass().getCreatureStats(ptr); - const MagicEffects &effects = creatureStats.getMagicEffects(); - bool godmode = ptr == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - - applyCureEffects(ptr); - - bool wasDead = creatureStats.isDead(); - - if (duration > 0) - { - // Apply correct magnitude for tickable effects that have just expired, - // in case duration > remaining time of effect. - // One case where this will happen is when the player uses the rest/wait command - // while there is a tickable effect active that should expire before the end of the rest/wait. - ExpiryVisitor visitor(ptr, duration); - creatureStats.getActiveSpells().visitEffectSources(visitor); - - for (MagicEffects::Collection::const_iterator it = effects.begin(); it != effects.end(); ++it) - { - // tickable effects (i.e. effects having a lasting impact after expiry) - effectTick(creatureStats, ptr, it->first, it->second.getMagnitude() * duration); - - // instant effects are already applied on spell impact in spellcasting.cpp, but may also come from permanent abilities - if (it->second.getMagnitude() > 0) - { - CastSpell cast(ptr, ptr); - if (cast.applyInstantEffect(ptr, ptr, it->first, it->second.getMagnitude())) - { - creatureStats.getSpells().purgeEffect(it->first.mId); - creatureStats.getActiveSpells().purgeEffect(it->first.mId); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).purgeEffect(it->first.mId); - } - } - } - } - - // purge levitate effect if levitation is disabled - // check only modifier, because base value can be setted from SetFlying console command. - if (MWBase::Environment::get().getWorld()->isLevitationEnabled() == false && effects.get(ESM::MagicEffect::Levitate).getModifier() > 0) - { - creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Levitate); - creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Levitate); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).purgeEffect(ESM::MagicEffect::Levitate); - - if (ptr == getPlayer()) - { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sLevitateDisabled}"); - } - } - - // dynamic stats - for (int i = 0; i < 3; ++i) - { - DynamicStat stat = creatureStats.getDynamic(i); - float fortify = effects.get(ESM::MagicEffect::FortifyHealth + i).getMagnitude(); - float drain = 0.f; - if (!godmode) - drain = effects.get(ESM::MagicEffect::DrainHealth + i).getMagnitude(); - stat.setCurrentModifier(fortify - drain, - // Magicka can be decreased below zero due to a fortify effect wearing off - // Fatigue can be decreased below zero meaning the actor will be knocked out - i == 1 || i == 2); - - creatureStats.setDynamic(i, stat); - } - - // attributes - for(int i = 0;i < ESM::Attribute::Length;++i) - { - AttributeValue stat = creatureStats.getAttribute(i); - float fortify = effects.get(EffectKey(ESM::MagicEffect::FortifyAttribute, i)).getMagnitude(); - float drain = 0.f, absorb = 0.f; - if (!godmode) - { - drain = effects.get(EffectKey(ESM::MagicEffect::DrainAttribute, i)).getMagnitude(); - absorb = effects.get(EffectKey(ESM::MagicEffect::AbsorbAttribute, i)).getMagnitude(); - } - stat.setModifier(static_cast(fortify - drain - absorb)); - - creatureStats.setAttribute(i, stat); - } if (creatureStats.needToRecalcDynamicStats()) calculateDynamicStats(ptr); - - if (ptr == getPlayer()) - { - GetCorprusSpells getCorprusSpellsVisitor; - creatureStats.getSpells().visitEffectSources(getCorprusSpellsVisitor); - creatureStats.getActiveSpells().visitEffectSources(getCorprusSpellsVisitor); - ptr.getClass().getInventoryStore(ptr).visitEffectSources(getCorprusSpellsVisitor); - std::vector corprusSpells = getCorprusSpellsVisitor.mSpells; - std::vector corprusSpellsToRemove; - - for (auto it = creatureStats.getCorprusSpells().begin(); it != creatureStats.getCorprusSpells().end(); ++it) - { - if(std::find(corprusSpells.begin(), corprusSpells.end(), it->first) == corprusSpells.end()) - { - // Corprus effect expired, remove entry and restore stats. - MWBase::Environment::get().getMechanicsManager()->restoreStatsAfterCorprus(ptr, it->first); - corprusSpellsToRemove.push_back(it->first); - corprusSpells.erase(std::remove(corprusSpells.begin(), corprusSpells.end(), it->first), corprusSpells.end()); - continue; - } - - corprusSpells.erase(std::remove(corprusSpells.begin(), corprusSpells.end(), it->first), corprusSpells.end()); - - if (MWBase::Environment::get().getWorld()->getTimeStamp() >= it->second.mNextWorsening) - { - it->second.mNextWorsening += CorprusStats::sWorseningPeriod; - GetCurrentMagnitudes getMagnitudesVisitor (it->first); - creatureStats.getSpells().visitEffectSources(getMagnitudesVisitor); - creatureStats.getActiveSpells().visitEffectSources(getMagnitudesVisitor); - ptr.getClass().getInventoryStore(ptr).visitEffectSources(getMagnitudesVisitor); - for (auto& effectMagnitude : getMagnitudesVisitor.mMagnitudes) - { - if (effectMagnitude.first.mId == ESM::MagicEffect::FortifyAttribute) - { - AttributeValue attr = creatureStats.getAttribute(effectMagnitude.first.mArg); - attr.damage(-effectMagnitude.second); - creatureStats.setAttribute(effectMagnitude.first.mArg, attr); - it->second.mWorsenings[effectMagnitude.first.mArg] = 0; - } - else if (effectMagnitude.first.mId == ESM::MagicEffect::DrainAttribute) - { - AttributeValue attr = creatureStats.getAttribute(effectMagnitude.first.mArg); - int currentDamage = attr.getDamage(); - if (currentDamage >= 0) - it->second.mWorsenings[effectMagnitude.first.mArg] = std::min(it->second.mWorsenings[effectMagnitude.first.mArg], currentDamage); - - it->second.mWorsenings[effectMagnitude.first.mArg] += effectMagnitude.second; - attr.damage(effectMagnitude.second); - creatureStats.setAttribute(effectMagnitude.first.mArg, attr); - } - } - - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); - } - } - - for (std::string& oldCorprusSpell : corprusSpellsToRemove) - { - creatureStats.removeCorprusSpell(oldCorprusSpell); - } - - for (std::string& newCorprusSpell : corprusSpells) - { - CorprusStats corprus; - for (int i=0; igetTimeStamp() + CorprusStats::sWorseningPeriod; - - creatureStats.addCorprusSpell(newCorprusSpell, corprus); - } - } - - // AI setting modifiers - int creature = !ptr.getClass().isNpc(); - Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Fight); - stat.setModifier(static_cast(effects.get(ESM::MagicEffect::FrenzyHumanoid + creature).getMagnitude() - - effects.get(ESM::MagicEffect::CalmHumanoid+creature).getMagnitude())); - creatureStats.setAiSetting(CreatureStats::AI_Fight, stat); - - stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); - stat.setModifier(static_cast(effects.get(ESM::MagicEffect::DemoralizeHumanoid + creature).getMagnitude() - - effects.get(ESM::MagicEffect::RallyHumanoid+creature).getMagnitude())); - creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); - if (creature && ptr.get()->mBase->mData.mType == ESM::Creature::Undead) - { - stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); - stat.setModifier(static_cast(effects.get(ESM::MagicEffect::TurnUndead).getMagnitude())); - creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); - } - - if (!wasDead && creatureStats.isDead()) - { - // The actor was killed by a magic effect. Figure out if the player was responsible for it. - const ActiveSpells& spells = creatureStats.getActiveSpells(); - MWWorld::Ptr player = getPlayer(); - std::set playerFollowers; - getActorsSidingWith(player, playerFollowers); - - for (ActiveSpells::TIterator it = spells.begin(); it != spells.end(); ++it) - { - bool actorKilled = false; - - const ActiveSpells::ActiveSpellParams& spell = it->second; - MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spell.mCasterActorId); - for (std::vector::const_iterator effectIt = spell.mEffects.begin(); - effectIt != spell.mEffects.end(); ++effectIt) - { - int effectId = effectIt->mEffectId; - bool isDamageEffect = false; - - int damageEffects[] = { - ESM::MagicEffect::FireDamage, ESM::MagicEffect::ShockDamage, ESM::MagicEffect::FrostDamage, ESM::MagicEffect::Poison, - ESM::MagicEffect::SunDamage, ESM::MagicEffect::DamageHealth, ESM::MagicEffect::AbsorbHealth - }; - - for (unsigned int i=0; iactorKilled(ptr, player); - actorKilled = true; - break; - } - } - } - - if (actorKilled) - break; - } - } - - // TODO: dirty flag for magic effects to avoid some unnecessary work below? - - // any value of calm > 0 will stop the actor from fighting - if ((effects.get(ESM::MagicEffect::CalmHumanoid).getMagnitude() > 0 && ptr.getClass().isNpc()) - || (effects.get(ESM::MagicEffect::CalmCreature).getMagnitude() > 0 && !ptr.getClass().isNpc())) - creatureStats.getAiSequence().stopCombat(); - - // Update bound effects - // Note: in vanilla MW multiple bound items of the same type can be created by different spells. - // As these extra copies are kinda useless this may or may not be important. - static std::map boundItemsMap; - if (boundItemsMap.empty()) - { - boundItemsMap[ESM::MagicEffect::BoundBattleAxe] = "sMagicBoundBattleAxeID"; - boundItemsMap[ESM::MagicEffect::BoundBoots] = "sMagicBoundBootsID"; - boundItemsMap[ESM::MagicEffect::BoundCuirass] = "sMagicBoundCuirassID"; - boundItemsMap[ESM::MagicEffect::BoundDagger] = "sMagicBoundDaggerID"; - boundItemsMap[ESM::MagicEffect::BoundGloves] = "sMagicBoundLeftGauntletID"; // Note: needs RightGauntlet variant too (see below) - boundItemsMap[ESM::MagicEffect::BoundHelm] = "sMagicBoundHelmID"; - boundItemsMap[ESM::MagicEffect::BoundLongbow] = "sMagicBoundLongbowID"; - boundItemsMap[ESM::MagicEffect::BoundLongsword] = "sMagicBoundLongswordID"; - boundItemsMap[ESM::MagicEffect::BoundMace] = "sMagicBoundMaceID"; - boundItemsMap[ESM::MagicEffect::BoundShield] = "sMagicBoundShieldID"; - boundItemsMap[ESM::MagicEffect::BoundSpear] = "sMagicBoundSpearID"; - } - - if(ptr.getClass().hasInventoryStore(ptr)) - { - for (const auto& [effect, itemGmst] : boundItemsMap) - { - bool found = creatureStats.mBoundItems.find(effect) != creatureStats.mBoundItems.end(); - float magnitude = effects.get(effect).getMagnitude(); - if (found != (magnitude > 0)) - { - if (magnitude > 0) - creatureStats.mBoundItems.insert(effect); - else - creatureStats.mBoundItems.erase(effect); - - std::string item = MWBase::Environment::get().getWorld()->getStore().get().find( - itemGmst)->mValue.getString(); - - magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr); - - if (effect == ESM::MagicEffect::BoundGloves) - { - item = MWBase::Environment::get().getWorld()->getStore().get().find( - "sMagicBoundRightGauntletID")->mValue.getString(); - magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr); - } - } - } - } - - // Summoned creature update visitor assumes the actor belongs to a cell. - // This assumption isn't always valid for the player character. - if (!ptr.isInCell()) - return; - - bool hasSummonEffect = false; - for (MagicEffects::Collection::const_iterator it = effects.begin(); it != effects.end(); ++it) - { - if (isSummoningEffect(it->first.mId)) - { - hasSummonEffect = true; - break; - } - } - - if (!creatureStats.getSummonedCreatureMap().empty() || !creatureStats.getSummonedCreatureGraveyard().empty() || hasSummonEffect) - { - UpdateSummonedCreatures updateSummonedCreatures(ptr); - creatureStats.getActiveSpells().visitEffectSources(updateSummonedCreatures); - creatureStats.getSpells().visitEffectSources(updateSummonedCreatures); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).visitEffectSources(updateSummonedCreatures); - updateSummonedCreatures.process(mTimerDisposeSummonsCorpses == 0.f); - } - } - - void Actors::calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration) - { - NpcStats &npcStats = ptr.getClass().getNpcStats(ptr); - const MagicEffects &effects = npcStats.getMagicEffects(); - bool godmode = ptr == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - - // skills - for(int i = 0;i < ESM::Skill::Length;++i) - { - SkillValue& skill = npcStats.getSkill(i); - float fortify = effects.get(EffectKey(ESM::MagicEffect::FortifySkill, i)).getMagnitude(); - float drain = 0.f, absorb = 0.f; - if (!godmode) - { - drain = effects.get(EffectKey(ESM::MagicEffect::DrainSkill, i)).getMagnitude(); - absorb = effects.get(EffectKey(ESM::MagicEffect::AbsorbSkill, i)).getMagnitude(); - } - skill.setModifier(static_cast(fortify - drain - absorb)); - } } bool Actors::isAttackPreparing(const MWWorld::Ptr& ptr) @@ -1979,8 +1423,6 @@ namespace MWMechanics player.getClass().getCreatureStats(player).setHitAttemptActorId(-1); } - iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); - const Misc::TimerStatus engageCombatTimerStatus = iter->second->updateEngageCombatTimer(duration); // For dead actors we need to update looping spell particles @@ -1988,7 +1430,7 @@ namespace MWMechanics { // They can be added during the death animation if (!iter->first.getClass().getCreatureStats(iter->first).isDeathAnimationFinished()) - adjustMagicEffects(iter->first); + adjustMagicEffects(iter->first, duration); ctrl->updateContinuousVfx(); } else @@ -2083,8 +1525,6 @@ namespace MWMechanics if (inProcessingRange) updateDrowning(iter->first, duration, ctrl->isKnockedOut(), isPlayer); - calculateNpcStatModifiers(iter->first, duration); - if (timerUpdateEquippedLight == 0) updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches); } @@ -2251,10 +1691,7 @@ namespace MWMechanics // Apply soultrap if (iter->first.getTypeName() == typeid(ESM::Creature).name()) - { - SoulTrap soulTrap (iter->first); - stats.getActiveSpells().visitEffectSources(soulTrap); - } + soulTrap(iter->first); // Magic effects will be reset later, and the magic effect that could kill the actor // needs to be determined now @@ -2270,30 +1707,27 @@ namespace MWMechanics // Reset magic effects and recalculate derived effects // One case where we need this is to make sure bound items are removed upon death - stats.modifyMagicEffects(MWMechanics::MagicEffects()); - stats.getActiveSpells().clear(); + float vampirism = stats.getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude(); + stats.getActiveSpells().clear(iter->first); // Make sure spell effects are removed purgeSpellEffects(stats.getActorId()); // Reset dynamic stats, attributes and skills calculateCreatureStatModifiers(iter->first, 0); - if (iter->first.getClass().isNpc()) - calculateNpcStatModifiers(iter->first, 0); + stats.getMagicEffects().add(ESM::MagicEffect::Vampirism, vampirism); if (isPlayer) { //player's death animation is over MWBase::Environment::get().getStateManager()->askLoadRecent(); + // Play Death Music if it was the player dying + MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Death.mp3"); } else { // NPC death animation is over, disable actor collision MWBase::Environment::get().getWorld()->enableActorCollision(iter->first, false); } - - // Play Death Music if it was the player dying - if(iter->first == getPlayer()) - MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Death.mp3"); } } } @@ -2313,7 +1747,7 @@ namespace MWMechanics // Remove the summoned creature's summoned creatures as well MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - std::map& creatureMap = stats.getSummonedCreatureMap(); + auto& creatureMap = stats.getSummonedCreatureMap(); for (const auto& creature : creatureMap) cleanupSummonedCreature(stats, creature.second); creatureMap.clear(); @@ -2334,7 +1768,7 @@ namespace MWMechanics for (PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) { MWMechanics::ActiveSpells& spells = iter->first.getClass().getCreatureStats(iter->first).getActiveSpells(); - spells.purge(casterActorId); + spells.purge(iter->first, casterActorId); } } @@ -2352,7 +1786,7 @@ namespace MWMechanics { if (iter->first.getClass().getCreatureStats(iter->first).isDead()) { - iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); + adjustMagicEffects (iter->first, duration); continue; } @@ -2363,15 +1797,11 @@ namespace MWMechanics (playerPos - iter->first.getRefData().getPosition().asVec3()).length2() > mActorsProcessingRange*mActorsProcessingRange) continue; - adjustMagicEffects (iter->first); + adjustMagicEffects (iter->first, duration); if (iter->first.getClass().getCreatureStats(iter->first).needToRecalcDynamicStats()) calculateDynamicStats (iter->first); calculateCreatureStatModifiers (iter->first, duration); - if (iter->first.getClass().isNpc()) - calculateNpcStatModifiers(iter->first, duration); - - iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(iter->first); if (animation) @@ -2760,10 +2190,8 @@ namespace MWMechanics void Actors::updateMagicEffects(const MWWorld::Ptr &ptr) { - adjustMagicEffects(ptr); + adjustMagicEffects(ptr, 0.f); calculateCreatureStatModifiers(ptr, 0.f); - if (ptr.getClass().isNpc()) - calculateNpcStatModifiers(ptr, 0.f); } bool Actors::isReadyToBlock(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 171b45e270..3efe28204d 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -41,15 +41,11 @@ namespace MWMechanics { std::map mDeathCount; - void addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); - void removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); - - void adjustMagicEffects (const MWWorld::Ptr& creature); + void adjustMagicEffects (const MWWorld::Ptr& creature, float duration); void calculateDynamicStats (const MWWorld::Ptr& ptr); void calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration); - void calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration); void calculateRestoration (const MWWorld::Ptr& ptr, float duration); @@ -208,7 +204,6 @@ namespace MWMechanics private: void updateVisibility (const MWWorld::Ptr& ptr, CharacterController* ctrl); - void applyCureEffects (const MWWorld::Ptr& actor); PtrActorMap mActors; float mTimerDisposeSummonsCorpses; diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index c26454aab5..e6176c869c 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -208,14 +208,14 @@ namespace MWMechanics } } - for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - float rating = rateSpell(it->first, actor, enemy); + float rating = rateSpell(spell, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; - bestAction.reset(new ActionSpell(it->first->mId)); - antiFleeRating = vanillaRateSpell(it->first, actor, enemy); + bestAction.reset(new ActionSpell(spell->mId)); + antiFleeRating = vanillaRateSpell(spell, actor, enemy); } } @@ -266,9 +266,9 @@ namespace MWMechanics } } - for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - float rating = rateSpell(it->first, actor, enemy); + float rating = rateSpell(spell, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index 8d99664f27..8ca6e8b766 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -545,20 +545,12 @@ namespace MWMechanics mAiSequence.writeState(state.mAiSequence); mMagicEffects.writeState(state.mMagicEffects); - state.mSummonedCreatureMap = mSummonedCreatures; + state.mSummonedCreatures = mSummonedCreatures; state.mSummonGraveyard = mSummonGraveyard; state.mHasAiSettings = true; for (int i=0; i<4; ++i) mAiSettings[i].writeState (state.mAiSettings[i]); - - for (auto it = mCorprusSpells.begin(); it != mCorprusSpells.end(); ++it) - { - for (int i=0; ifirst].mWorsenings[i] = mCorprusSpells.at(it->first).mWorsenings[i]; - - state.mCorprusSpells[it->first].mNextWorsening = mCorprusSpells.at(it->first).mNextWorsening.toEsm(); - } } void CreatureStats::readState (const ESM::CreatureStats& state) @@ -618,7 +610,7 @@ namespace MWMechanics // We can't use mActiveSpells::getMagicEffects here because it doesn't include expired effects auto spell = std::find_if(mActiveSpells.begin(), mActiveSpells.end(), [&] (const auto& spell) { - const auto& effects = spell.second.mEffects; + const auto& effects = spell.getEffects(); return std::find_if(effects.begin(), effects.end(), [&] (const auto& effect) { return effect.mEffectId == effectId; @@ -629,21 +621,12 @@ namespace MWMechanics } } - mSummonedCreatures = state.mSummonedCreatureMap; + mSummonedCreatures = state.mSummonedCreatures; mSummonGraveyard = state.mSummonGraveyard; if (state.mHasAiSettings) for (int i=0; i<4; ++i) mAiSettings[i].readState(state.mAiSettings[i]); - - mCorprusSpells.clear(); - for (auto it = state.mCorprusSpells.begin(); it != state.mCorprusSpells.end(); ++it) - { - for (int i=0; ifirst].mWorsenings[i] = state.mCorprusSpells.at(it->first).mWorsenings[i]; - - mCorprusSpells[it->first].mNextWorsening = MWWorld::TimeStamp(state.mCorprusSpells.at(it->first).mNextWorsening); - } } void CreatureStats::setLastRestockTime(MWWorld::TimeStamp tradeTime) @@ -710,7 +693,7 @@ namespace MWMechanics return mTimeOfDeath; } - std::map& CreatureStats::getSummonedCreatureMap() + std::multimap& CreatureStats::getSummonedCreatureMap() { return mSummonedCreatures; } @@ -719,23 +702,4 @@ namespace MWMechanics { return mSummonGraveyard; } - - std::map &CreatureStats::getCorprusSpells() - { - return mCorprusSpells; - } - - void CreatureStats::addCorprusSpell(const std::string& sourceId, CorprusStats& stats) - { - mCorprusSpells[sourceId] = stats; - } - - void CreatureStats::removeCorprusSpell(const std::string& sourceId) - { - auto corprusIt = mCorprusSpells.find(sourceId); - if (corprusIt != mCorprusSpells.end()) - { - mCorprusSpells.erase(corprusIt); - } - } } diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp index 06d526cc5a..d234d14486 100644 --- a/apps/openmw/mwmechanics/creaturestats.hpp +++ b/apps/openmw/mwmechanics/creaturestats.hpp @@ -1,6 +1,7 @@ #ifndef GAME_MWMECHANICS_CREATURESTATS_H #define GAME_MWMECHANICS_CREATURESTATS_H +#include #include #include #include @@ -87,14 +88,12 @@ namespace MWMechanics float mSideMovementAngle; private: - std::map mSummonedCreatures; // + std::multimap mSummonedCreatures; // // Contains ActorIds of summoned creatures with an expired lifetime that have not been deleted yet. // This may be necessary when the creature is in an inactive cell. std::vector mSummonGraveyard; - std::map mCorprusSpells; - protected: int mLevel; @@ -236,7 +235,7 @@ namespace MWMechanics void setBlock(bool value); bool getBlock() const; - std::map& getSummonedCreatureMap(); // + std::multimap& getSummonedCreatureMap(); // std::vector& getSummonedCreatureGraveyard(); // ActorIds enum Flag @@ -297,12 +296,6 @@ namespace MWMechanics static void cleanup(); - std::map & getCorprusSpells(); - - void addCorprusSpell(const std::string& sourceId, CorprusStats& stats); - - void removeCorprusSpell(const std::string& sourceId); - float getSideMovementAngle() const { return mSideMovementAngle; } void setSideMovementAngle(float angle) { mSideMovementAngle = angle; } }; diff --git a/apps/openmw/mwmechanics/disease.hpp b/apps/openmw/mwmechanics/disease.hpp index 5d4bfb682f..426a66ecf2 100644 --- a/apps/openmw/mwmechanics/disease.hpp +++ b/apps/openmw/mwmechanics/disease.hpp @@ -30,12 +30,11 @@ namespace MWMechanics MWBase::Environment::get().getWorld()->getStore().get().find( "fDiseaseXferChance")->mValue.getFloat(); - MagicEffects& actorEffects = actor.getClass().getCreatureStats(actor).getMagicEffects(); + const MagicEffects& actorEffects = actor.getClass().getCreatureStats(actor).getMagicEffects(); Spells& spells = carrier.getClass().getCreatureStats(carrier).getSpells(); - for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - const ESM::Spell* spell = it->first; if (actor.getClass().getCreatureStats(actor).getSpells().hasSpell(spell->mId)) continue; @@ -56,7 +55,7 @@ namespace MWMechanics if (Misc::Rng::rollDice(10000) < x) { // Contracted disease! - actor.getClass().getCreatureStats(actor).getSpells().add(it->first); + actor.getClass().getCreatureStats(actor).getSpells().add(spell); MWBase::Environment::get().getWorld()->applyLoopingParticles(actor); std::string msg = "sMagicContractDisease"; diff --git a/apps/openmw/mwmechanics/linkedeffects.cpp b/apps/openmw/mwmechanics/linkedeffects.cpp deleted file mode 100644 index b0defac7d2..0000000000 --- a/apps/openmw/mwmechanics/linkedeffects.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "linkedeffects.hpp" - -#include -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "../mwrender/animation.hpp" - -#include "../mwworld/class.hpp" -#include "../mwworld/esmstore.hpp" - -#include "creaturestats.hpp" - -namespace MWMechanics -{ - - bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects) - { - if (caster.isEmpty() || caster == target || !target.getClass().isActor()) - return false; - - bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; - bool isUnreflectable = magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable; - if (!isHarmful || isUnreflectable) - return false; - - float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude(); - if (Misc::Rng::roll0to99() >= reflect) - return false; - - const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Reflect"); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation && !reflectStatic->mModel.empty()) - animation->addEffect("meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, std::string()); - reflectedEffects.mList.emplace_back(effect); - return true; - } - - void absorbStat(const ESM::ENAMstruct& effect, const ESM::ActiveEffect& appliedEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, bool reflected, const std::string& source) - { - if (caster.isEmpty() || caster == target) - return; - - if (!target.getClass().isActor() || !caster.getClass().isActor()) - return; - - // Make sure callers don't do something weird - if (effect.mEffectID < ESM::MagicEffect::AbsorbAttribute || effect.mEffectID > ESM::MagicEffect::AbsorbSkill) - throw std::runtime_error("invalid absorb stat effect"); - - if (appliedEffect.mMagnitude == 0) - return; - - std::vector absorbEffects; - ActiveSpells::ActiveEffect absorbEffect = appliedEffect; - absorbEffect.mMagnitude *= -1; - absorbEffect.mEffectIndex = appliedEffect.mEffectIndex; - absorbEffects.emplace_back(absorbEffect); - - // Morrowind negates reflected Absorb spells so the original caster won't be harmed. - if (reflected && Settings::Manager::getBool("classic reflected absorb spells behavior", "Game")) - { - target.getClass().getCreatureStats(target).getActiveSpells().addSpell(std::string(), true, - absorbEffects, source, caster.getClass().getCreatureStats(caster).getActorId()); - return; - } - - caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(std::string(), true, - absorbEffects, source, target.getClass().getCreatureStats(target).getActorId()); - } -} diff --git a/apps/openmw/mwmechanics/linkedeffects.hpp b/apps/openmw/mwmechanics/linkedeffects.hpp deleted file mode 100644 index a6dea2a3a2..0000000000 --- a/apps/openmw/mwmechanics/linkedeffects.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef MWMECHANICS_LINKEDEFFECTS_H -#define MWMECHANICS_LINKEDEFFECTS_H - -#include - -namespace ESM -{ - struct ActiveEffect; - struct EffectList; - struct ENAMstruct; - struct MagicEffect; - struct Spell; -} - -namespace MWWorld -{ - class Ptr; -} - -namespace MWMechanics -{ - - // Try to reflect a spell effect. If it's reflected, it's also put into the passed reflected effects list. - bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects); - - // Try to absorb a stat (skill, attribute, etc.) from the target and transfer it to the caster. - void absorbStat(const ESM::ENAMstruct& effect, const ESM::ActiveEffect& appliedEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, bool reflected, const std::string& source); -} - -#endif diff --git a/apps/openmw/mwmechanics/magiceffects.cpp b/apps/openmw/mwmechanics/magiceffects.cpp index 1a1a44f638..e75361628b 100644 --- a/apps/openmw/mwmechanics/magiceffects.cpp +++ b/apps/openmw/mwmechanics/magiceffects.cpp @@ -193,22 +193,22 @@ namespace MWMechanics void MagicEffects::writeState(ESM::MagicEffects &state) const { - // Don't need to save Modifiers, they are recalculated every frame anyway. - for (Collection::const_iterator iter (begin()); iter!=end(); ++iter) + for (const auto& [key, params] : mCollection) { - if (iter->second.getBase() != 0) + if (params.getBase() != 0 || params.getModifier() != 0.f) { // Don't worry about mArg, never used by magic effect script instructions - state.mEffects.insert(std::make_pair(iter->first.mId, iter->second.getBase())); + state.mEffects[key.mId] = {params.getBase(), params.getModifier()}; } } } void MagicEffects::readState(const ESM::MagicEffects &state) { - for (std::map::const_iterator it = state.mEffects.begin(); it != state.mEffects.end(); ++it) + for (const auto& [key, params] : state.mEffects) { - mCollection[EffectKey(it->first)].setBase(it->second); + mCollection[EffectKey(key)].setBase(params.first); + mCollection[EffectKey(key)].setModifier(params.second); } } } diff --git a/apps/openmw/mwmechanics/magiceffects.hpp b/apps/openmw/mwmechanics/magiceffects.hpp index 12735a87fc..50f4dab050 100644 --- a/apps/openmw/mwmechanics/magiceffects.hpp +++ b/apps/openmw/mwmechanics/magiceffects.hpp @@ -69,16 +69,6 @@ namespace MWMechanics return param -= right; } - // Used by effect management classes (ActiveSpells, InventoryStore, Spells) to list active effect sources for GUI display - struct EffectSourceVisitor - { - virtual ~EffectSourceVisitor() { } - - virtual void visit (EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) = 0; - }; - /// \brief Effects currently affecting a NPC or creature class MagicEffects { diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 24601d79c8..fb8b3fa2b0 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -84,7 +84,7 @@ namespace MWMechanics // reset creatureStats.setLevel(player->mNpdt.mLevel); creatureStats.getSpells().clear(true); - creatureStats.modifyMagicEffects(MagicEffects()); + creatureStats.getActiveSpells().clear(ptr); for (int i=0; i<27; ++i) npcStats.getSkill (i).setBase (player->mNpdt.mSkills[i]); @@ -213,6 +213,7 @@ namespace MWMechanics int attributes[ESM::Attribute::Length]; for (int i=0; i selectedSpells = autoCalcPlayerSpells(skills, attributes, race); @@ -221,6 +222,7 @@ namespace MWMechanics // forced update and current value adjustments mActors.updateActor (ptr, 0); + mActors.updateActor (ptr, 0); for (int i=0; i<3; ++i) { @@ -282,24 +284,6 @@ namespace MWMechanics mObjects.dropObjects(cellStore); } - void MechanicsManager::restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) - { - auto& stats = actor.getClass().getCreatureStats (actor); - auto& corprusSpells = stats.getCorprusSpells(); - - auto corprusIt = corprusSpells.find(sourceId); - - if (corprusIt != corprusSpells.end()) - { - for (int i = 0; i < ESM::Attribute::Length; ++i) - { - MWMechanics::AttributeValue attr = stats.getAttribute(i); - attr.restore(corprusIt->second.mWorsenings[i]); - actor.getClass().getCreatureStats(actor).setAttribute(i, attr); - } - } - } - void MechanicsManager::update(float duration, bool paused) { // Note: we should do it here since game mechanics and world updates use these values diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 0aaeb23435..206b5c5420 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -230,8 +230,6 @@ namespace MWMechanics GreetingState getGreetingState(const MWWorld::Ptr& ptr) const override; bool isTurningToPlayer(const MWWorld::Ptr& ptr) const override; - void restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) override; - private: bool canCommitCrimeAgainst(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker); bool canReportCrime(const MWWorld::Ptr &actor, const MWWorld::Ptr &victim, std::set &playerFollowers); diff --git a/apps/openmw/mwmechanics/spellabsorption.cpp b/apps/openmw/mwmechanics/spellabsorption.cpp index cb80921b33..82531132ca 100644 --- a/apps/openmw/mwmechanics/spellabsorption.cpp +++ b/apps/openmw/mwmechanics/spellabsorption.cpp @@ -16,33 +16,30 @@ namespace MWMechanics { - - class GetAbsorptionProbability : public MWMechanics::EffectSourceVisitor + float getProbability(const MWMechanics::ActiveSpells& activeSpells) { - public: - float mProbability{0.f}; - - GetAbsorptionProbability() = default; - - void visit (MWMechanics::EffectKey key, int /*effectIndex*/, - const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, - float magnitude, float /*remainingTime*/, float /*totalTime*/) override + float probability = 0.f; + for(const auto& params : activeSpells) { - if (key.mId == ESM::MagicEffect::SpellAbsorption) + for(const auto& effect : params.getEffects()) { - if (mProbability == 0.f) - mProbability = magnitude / 100; - else + if(effect.mEffectId == ESM::MagicEffect::SpellAbsorption) { - // If there are different sources of SpellAbsorption effect, multiply failing probability for all effects. - // Real absorption probability will be the (1 - total fail chance) in this case. - float failProbability = 1.f - mProbability; - failProbability *= 1.f - magnitude / 100; - mProbability = 1.f - failProbability; + if(probability == 0.f) + probability = effect.mMagnitude / 100; + else + { + // If there are different sources of SpellAbsorption effect, multiply failing probability for all effects. + // Real absorption probability will be the (1 - total fail chance) in this case. + float failProbability = 1.f - probability; + failProbability *= 1.f - effect.mMagnitude / 100; + probability = 1.f - failProbability; + } } } } - }; + return static_cast(probability * 100); + } bool absorbSpell (const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target) { @@ -53,13 +50,7 @@ namespace MWMechanics if (stats.getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).getMagnitude() <= 0.f) return false; - GetAbsorptionProbability check; - stats.getActiveSpells().visitEffectSources(check); - stats.getSpells().visitEffectSources(check); - if (target.getClass().hasInventoryStore(target)) - target.getClass().getInventoryStore(target).visitEffectSources(check); - - int chance = check.mProbability * 100; + int chance = getProbability(stats.getActiveSpells()); if (Misc::Rng::roll0to99() >= chance) return false; diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 502e56edf7..53fbe69ab3 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -22,14 +22,38 @@ #include "actorutil.hpp" #include "aifollow.hpp" #include "creaturestats.hpp" -#include "linkedeffects.hpp" #include "spellabsorption.hpp" -#include "spellresistance.hpp" +#include "spelleffects.hpp" #include "spellutil.hpp" #include "summoning.hpp" -#include "tickableeffects.hpp" #include "weapontype.hpp" +namespace +{ + bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, + const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects) + { + if (caster.isEmpty() || caster == target || !target.getClass().isActor()) + return false; + + bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; + bool isUnreflectable = magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable; + if (!isHarmful || isUnreflectable) + return false; + + float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude(); + if (Misc::Rng::roll0to99() >= reflect) + return false; + + const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Reflect"); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); + if (animation && !reflectStatic->mModel.empty()) + animation->addEffect("meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, std::string()); + reflectedEffects.mList.emplace_back(effect); + return true; + } +} + namespace MWMechanics { CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target, const bool fromProjectile, const bool manualSpell) @@ -54,7 +78,7 @@ namespace MWMechanics (mTarget.getRefData().getPosition().asVec3() + offset) - (mCaster.getRefData().getPosition().asVec3()); - MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection); + MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection, mSlot); } void CastSpell::inflict(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, @@ -100,20 +124,13 @@ namespace MWMechanics } ESM::EffectList reflectedEffects; - std::vector appliedLastingEffects; - - // HACK: cache target's magic effects here, and add any applied effects to it. Use the cached effects for determining resistance. - // This is required for Weakness effects in a spell to apply to any subsequent effects in the spell. - // Otherwise, they'd only apply after the whole spell was added. - MagicEffects targetEffects; - if (targetIsActor) - targetEffects += target.getClass().getCreatureStats(target).getMagicEffects(); + ActiveSpells::ActiveSpellParams params(*this, caster); bool castByPlayer = (!caster.isEmpty() && caster == getPlayer()); - ActiveSpells targetSpells; + const ActiveSpells* targetSpells = nullptr; if (targetIsActor) - targetSpells = target.getClass().getCreatureStats(target).getActiveSpells(); + targetSpells = &target.getClass().getCreatureStats(target).getActiveSpells(); bool canCastAnEffect = false; // For bound equipment.If this remains false // throughout the iteration of this spell's @@ -134,7 +151,7 @@ namespace MWMechanics effectIt->mEffectID); // Re-casting a bound equipment effect has no effect if the spell is still active - if (magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable && targetSpells.isSpellActive(mId)) + if (magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable && targetSpells && targetSpells->isSpellActive(mId)) { if (effectIt == (effects.mList.end() - 1) && !canCastAnEffect && castByPlayer) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCannotRecast}"); @@ -163,307 +180,70 @@ namespace MWMechanics if (!reflected && reflectEffect(*effectIt, magicEffect, caster, target, reflectedEffects)) continue; - // Try resisting. - float magnitudeMult = getEffectMultiplier(effectIt->mEffectID, target, caster, spell, &targetEffects); - if (magnitudeMult == 0) + ActiveSpells::ActiveEffect effect; + effect.mEffectId = effectIt->mEffectID; + effect.mArg = MWMechanics::EffectKey(*effectIt).mArg; + effect.mMagnitude = 0.f; + effect.mMinMagnitude = effectIt->mMagnMin; + effect.mMaxMagnitude = effectIt->mMagnMax; + effect.mTimeLeft = 0.f; + effect.mEffectIndex = currentEffectIndex; + effect.mFlags = ESM::ActiveEffect::Flag_None; + + // Avoid applying harmful effects to the player in god mode + if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful) { - // Fully resisted, show message - if (target == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); - else if (castByPlayer) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); + effect.mMinMagnitude = 0; + effect.mMaxMagnitude = 0; } - else - { - float magnitude = effectIt->mMagnMin + Misc::Rng::rollDice(effectIt->mMagnMax - effectIt->mMagnMin + 1); - magnitude *= magnitudeMult; - if (!target.getClass().isActor()) - { - // non-actor objects have no list of active magic effects, so have to apply instantly - if (!applyInstantEffect(target, caster, EffectKey(*effectIt), magnitude)) - continue; - } - else // target.getClass().isActor() == true - { - ActiveSpells::ActiveEffect effect; - effect.mEffectId = effectIt->mEffectID; - effect.mArg = MWMechanics::EffectKey(*effectIt).mArg; - effect.mMagnitude = magnitude; - effect.mTimeLeft = 0.f; - effect.mEffectIndex = currentEffectIndex; - - // Avoid applying absorb effects if the caster is the target - // We still need the spell to be added - if (caster == target - && effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute - && effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill) - { - effect.mMagnitude = 0; - } + bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); + effect.mDuration = hasDuration ? static_cast(effectIt->mDuration) : 1.f; - // Avoid applying harmful effects to the player in god mode - if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful) - { - effect.mMagnitude = 0; - } + bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; + if (!appliedOnce) + effect.mDuration = std::max(1.f, effect.mDuration); - bool effectAffectsHealth = isHarmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth; - if (castByPlayer && target != caster && !target.getClass().getCreatureStats(target).isDead() && effectAffectsHealth) - { - // If player is attempting to cast a harmful spell on or is healing a living target, show the target's HP bar. - MWBase::Environment::get().getWindowManager()->setEnemy(target); - } - - bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); - effect.mDuration = hasDuration ? static_cast(effectIt->mDuration) : 1.f; + effect.mTimeLeft = effect.mDuration; - bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; - if (!appliedOnce) - effect.mDuration = std::max(1.f, effect.mDuration); + // add to list of active effects, to apply in next frame + params.getEffects().emplace_back(effect); - if (effect.mDuration == 0) - { - // We still should add effect to list to allow GetSpellEffects to detect this spell - appliedLastingEffects.push_back(effect); - - // duration 0 means apply full magnitude instantly - bool wasDead = target.getClass().getCreatureStats(target).isDead(); - effectTick(target.getClass().getCreatureStats(target), target, EffectKey(*effectIt), effect.mMagnitude); - bool isDead = target.getClass().getCreatureStats(target).isDead(); - - if (!wasDead && isDead) - MWBase::Environment::get().getMechanicsManager()->actorKilled(target, caster); - } - else - { - effect.mTimeLeft = effect.mDuration; - - targetEffects.add(MWMechanics::EffectKey(*effectIt), MWMechanics::EffectParam(effect.mMagnitude)); - - // add to list of active effects, to apply in next frame - appliedLastingEffects.push_back(effect); - - // Unequip all items, if a spell with the ExtraSpell effect was casted - if (effectIt->mEffectID == ESM::MagicEffect::ExtraSpell && target.getClass().hasInventoryStore(target)) - { - MWWorld::InventoryStore& store = target.getClass().getInventoryStore(target); - store.unequipAll(target); - } - - // Command spells should have their effect, including taking the target out of combat, each time the spell successfully affects the target - if (((effectIt->mEffectID == ESM::MagicEffect::CommandHumanoid && target.getClass().isNpc()) - || (effectIt->mEffectID == ESM::MagicEffect::CommandCreature && target.getTypeName() == typeid(ESM::Creature).name())) - && !caster.isEmpty() && caster.getClass().isActor() && target != getPlayer() && effect.mMagnitude >= target.getClass().getCreatureStats(target).getLevel()) - { - MWMechanics::AiFollow package(caster, true); - target.getClass().getCreatureStats(target).getAiSequence().stack(package, target); - } - - // For absorb effects, also apply the effect to the caster - but with a negative - // magnitude, since we're transferring stats from the target to the caster - if (effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute && effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill) - absorbStat(*effectIt, effect, caster, target, reflected, mSourceName); - } - } - - // Re-casting a summon effect will remove the creature from previous castings of that effect. - if (isSummoningEffect(effectIt->mEffectID) && targetIsActor) - { - CreatureStats& targetStats = target.getClass().getCreatureStats(target); - ESM::SummonKey key(effectIt->mEffectID, mId, currentEffectIndex); - auto findCreature = targetStats.getSummonedCreatureMap().find(key); - if (findCreature != targetStats.getSummonedCreatureMap().end()) - { - MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(target, findCreature->second); - targetStats.getSummonedCreatureMap().erase(findCreature); - } - } - - if (target.getClass().isActor() || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) - { - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; + bool effectAffectsHealth = isHarmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth; + if (castByPlayer && target != caster && targetIsActor && effectAffectsHealth) + { + // If player is attempting to cast a harmful spell on or is healing a living target, show the target's HP bar. + MWBase::Environment::get().getWindowManager()->setEnemy(target); + } - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(!magicEffect->mHitSound.empty()) - sndMgr->playSound3D(target, magicEffect->mHitSound, 1.0f, 1.0f); - else - sndMgr->playSound3D(target, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f); - - // Add VFX - const ESM::Static* castStatic; - if (!magicEffect->mHit.empty()) - castStatic = MWBase::Environment::get().getWorld()->getStore().get().find (magicEffect->mHit); - else - castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_DefaultHit"); - - bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; - // Note: in case of non actor, a free effect should be fine as well - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(target); - if (anim && !castStatic->mModel.empty()) - anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", magicEffect->mParticle); - } + if (targetIsActor || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) + { + playEffects(target, *magicEffect); } } if (!exploded) - MWBase::Environment::get().getWorld()->explodeSpell(mHitPosition, effects, caster, target, range, mId, mSourceName, mFromProjectile); + MWBase::Environment::get().getWorld()->explodeSpell(mHitPosition, effects, caster, target, range, mId, mSourceName, mFromProjectile, mSlot); if (!target.isEmpty()) { if (!reflectedEffects.mList.empty()) inflict(caster, target, reflectedEffects, range, true, exploded); - if (!appliedLastingEffects.empty()) - { - int casterActorId = -1; - if (!caster.isEmpty() && caster.getClass().isActor()) - casterActorId = caster.getClass().getCreatureStats(caster).getActorId(); - target.getClass().getCreatureStats(target).getActiveSpells().addSpell(mId, mStack, appliedLastingEffects, - mSourceName, casterActorId); - } - } - } - - bool CastSpell::applyInstantEffect(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, const MWMechanics::EffectKey& effect, float magnitude) - { - short effectId = effect.mId; - if (target.getClass().canLock(target)) - { - if (effectId == ESM::MagicEffect::Lock) - { - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::MagicEffect *magiceffect = store.get().find(effectId); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation) - animation->addSpellCastGlow(magiceffect); - if (target.getCellRef().getLockLevel() < magnitude) //If the door is not already locked to a higher value, lock it to spell magnitude - { - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicLockSuccess}"); - target.getCellRef().lock(static_cast(magnitude)); - } - return true; - } - else if (effectId == ESM::MagicEffect::Open) + if (!params.getEffects().empty()) { - if (!caster.isEmpty()) - { - MWBase::Environment::get().getMechanicsManager()->unlockAttempted(getPlayer(), target); - // Use the player instead of the caster for vanilla crime compatibility - } - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::MagicEffect *magiceffect = store.get().find(effectId); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation) - animation->addSpellCastGlow(magiceffect); - if (target.getCellRef().getLockLevel() <= magnitude) - { - if (target.getCellRef().getLockLevel() > 0) - { - MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock", 1.f, 1.f); - - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicOpenSuccess}"); - } - target.getCellRef().unlock(); - } + if(targetIsActor) + target.getClass().getCreatureStats(target).getActiveSpells().addSpell(params); else { - MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock Fail", 1.f, 1.f); + // Apply effects instantly. We can ignore effect deletion since the entire params object gets deleted afterwards anyway + for(auto& effect : params.getEffects()) + applyMagicEffect(target, caster, params, effect, 0.f); } - - return true; - } - } - else if (target.getClass().isActor() && effectId == ESM::MagicEffect::Dispel) - { - target.getClass().getCreatureStats(target).getActiveSpells().purgeAll(magnitude, true); - return true; - } - else if(target.getClass().isActor() && effectId >= ESM::MagicEffect::CalmHumanoid && effectId <= ESM::MagicEffect::RallyCreature) - { - // Treat X Humanoid spells on creatures and X Creature spells on NPCs as instant effects and remove their VFX - bool affectsCreatures = (effectId - ESM::MagicEffect::CalmHumanoid) & 1; - if(affectsCreatures == target.getClass().isNpc()) - { - MWBase::Environment::get().getWorld()->getAnimation(target)->removeEffect(effectId); - return true; - } - } - else if(target.getClass().isActor() && effectId == ESM::MagicEffect::TurnUndead) - { - // Diverge from vanilla by giving scripts a chance to detect Turn Undead on non-undead, but still remove the effect and VFX - if(target.getClass().isNpc() || target.get()->mBase->mData.mType != ESM::Creature::Undead) - { - MWBase::Environment::get().getWorld()->getAnimation(target)->removeEffect(effectId); - return true; } } - else if (target.getClass().isActor() && target == getPlayer()) - { - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mCaster); - bool teleportingEnabled = MWBase::Environment::get().getWorld()->isTeleportingEnabled(); - - if (effectId == ESM::MagicEffect::DivineIntervention || effectId == ESM::MagicEffect::AlmsiviIntervention) - { - if (!teleportingEnabled) - { - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); - return true; - } - std::string marker = (effectId == ESM::MagicEffect::DivineIntervention) ? "divinemarker" : "templemarker"; - MWBase::Environment::get().getWorld()->teleportToClosestMarker(target, marker); - anim->removeEffect(effectId); - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() - .search("VFX_Summon_end"); - if (fx) - anim->addEffect("meshes\\" + fx->mModel, -1); - return true; - } - else if (effectId == ESM::MagicEffect::Mark) - { - if (teleportingEnabled) - { - MWBase::Environment::get().getWorld()->getPlayer().markPosition( - target.getCell(), target.getRefData().getPosition()); - } - else if (caster == getPlayer()) - { - MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); - } - return true; - } - else if (effectId == ESM::MagicEffect::Recall) - { - if (!teleportingEnabled) - { - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); - return true; - } - - MWWorld::CellStore* markedCell = nullptr; - ESM::Position markedPosition; - - MWBase::Environment::get().getWorld()->getPlayer().getMarkedPosition(markedCell, markedPosition); - if (markedCell) - { - MWWorld::ActionTeleport action(markedCell->isExterior() ? "" : markedCell->getCell()->mName, - markedPosition, false); - action.execute(target); - anim->removeEffect(effectId); - } - return true; - } - } - return false; } - bool CastSpell::cast(const std::string &id) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); @@ -479,7 +259,7 @@ namespace MWMechanics throw std::runtime_error("ID type cannot be casted"); } - bool CastSpell::cast(const MWWorld::Ptr &item, bool launchProjectile) + bool CastSpell::cast(const MWWorld::Ptr &item, int slot, bool launchProjectile) { std::string enchantmentName = item.getClass().getEnchantment(item); if (enchantmentName.empty()) @@ -490,7 +270,7 @@ namespace MWMechanics const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(enchantmentName); - mStack = false; + mSlot = slot; bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); bool isProjectile = false; @@ -570,7 +350,7 @@ namespace MWMechanics { mSourceName = potion->mName; mId = potion->mId; - mStack = true; + mType = ESM::ActiveSpells::Type_Consumable; inflict(mCaster, mCaster, potion->mEffects, ESM::RT_Self); @@ -581,7 +361,6 @@ namespace MWMechanics { mSourceName = spell->mName; mId = spell->mId; - mStack = false; const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); @@ -656,7 +435,7 @@ namespace MWMechanics bool CastSpell::cast (const ESM::Ingredient* ingredient) { mId = ingredient->mId; - mStack = true; + mType = ESM::ActiveSpells::Type_Consumable; mSourceName = ingredient->mName; ESM::ENAMstruct effect; @@ -799,4 +578,36 @@ namespace MWMechanics sndMgr->playSound3D(mCaster, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f); } } + + void playEffects(const MWWorld::Ptr& target, const ESM::MagicEffect& magicEffect, bool playNonLooping) + { + if (playNonLooping) + { + static const std::string schools[] = { + "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" + }; + + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + if(!magicEffect.mHitSound.empty()) + sndMgr->playSound3D(target, magicEffect.mHitSound, 1.0f, 1.0f); + else + sndMgr->playSound3D(target, schools[magicEffect.mData.mSchool]+" hit", 1.0f, 1.0f); + } + + // Add VFX + const ESM::Static* castStatic; + if (!magicEffect.mHit.empty()) + castStatic = MWBase::Environment::get().getWorld()->getStore().get().find (magicEffect.mHit); + else + castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_DefaultHit"); + + bool loop = (magicEffect.mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; + MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(target); + if(anim && !castStatic->mModel.empty()) + { + // Don't play particle VFX unless the effect is new or it should be looping. + if (playNonLooping || loop) + anim->addEffect("meshes\\" + castStatic->mModel, magicEffect.mIndex, loop, "", magicEffect.mParticle); + } + } } diff --git a/apps/openmw/mwmechanics/spellcasting.hpp b/apps/openmw/mwmechanics/spellcasting.hpp index cc525d2db6..a21ea33e0b 100644 --- a/apps/openmw/mwmechanics/spellcasting.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -1,6 +1,7 @@ #ifndef MWMECHANICS_SPELLCASTING_H #define MWMECHANICS_SPELLCASTING_H +#include #include #include "../mwworld/ptr.hpp" @@ -11,6 +12,7 @@ namespace ESM struct Ingredient; struct Potion; struct EffectList; + struct MagicEffect; } namespace MWMechanics @@ -26,13 +28,14 @@ namespace MWMechanics void playSpellCastingEffects(const std::vector& effects); public: - bool mStack{false}; std::string mId; // ID of spell, potion, item etc std::string mSourceName; // Display name for spell, potion, etc osg::Vec3f mHitPosition{0,0,0}; // Used for spawning area orb bool mAlwaysSucceed{false}; // Always succeed spells casted by NPCs/creatures regardless of their chance (default: false) bool mFromProjectile; // True if spell is cast by enchantment of some projectile (arrow, bolt or thrown weapon) bool mManualSpell; // True if spell is casted from script and ignores some checks (mana level, success chance, etc.) + int mSlot{0}; + ESM::ActiveSpells::EffectType mType{ESM::ActiveSpells::Type_Temporary}; public: CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile=false, const bool manualSpell=false); @@ -41,7 +44,7 @@ namespace MWMechanics /// @note mCaster must be an actor /// @param launchProjectile If set to false, "on target" effects are directly applied instead of being launched as projectile originating from the caster. - bool cast (const MWWorld::Ptr& item, bool launchProjectile=true); + bool cast (const MWWorld::Ptr& item, int slot, bool launchProjectile=true); /// @note mCaster must be an NPC bool cast (const ESM::Ingredient* ingredient); @@ -60,11 +63,9 @@ namespace MWMechanics /// @note \a caster can be any type of object, or even an empty object. void inflict (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const ESM::EffectList& effects, ESM::RangeType range, bool reflected=false, bool exploded=false); - - /// @note \a caster can be any type of object, or even an empty object. - /// @return was the target suitable for the effect? - bool applyInstantEffect (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const MWMechanics::EffectKey& effect, float magnitude); }; + + void playEffects(const MWWorld::Ptr& target, const ESM::MagicEffect& magicEffect, bool playNonLooping = true); } #endif diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp new file mode 100644 index 0000000000..895e908bd2 --- /dev/null +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -0,0 +1,1032 @@ +#include "spelleffects.hpp" + +#include + +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/soundmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" + +#include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/aifollow.hpp" +#include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/spellresistance.hpp" +#include "../mwmechanics/summoning.hpp" + +#include "../mwrender/animation.hpp" + +#include "../mwworld/actionequip.hpp" +#include "../mwworld/actionteleport.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/player.hpp" + +namespace +{ + float roll(const ESM::ActiveEffect& effect) + { + if(effect.mMinMagnitude == effect.mMaxMagnitude) + return effect.mMinMagnitude; + return effect.mMinMagnitude + Misc::Rng::rollDice(effect.mMaxMagnitude - effect.mMinMagnitude + 1); + } + + void modifyAiSetting(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, ESM::MagicEffect::Effects creatureEffect, MWMechanics::CreatureStats::AiSetting setting, float magnitude, bool& invalid) + { + if(target == MWMechanics::getPlayer() || (effect.mEffectId == creatureEffect) == target.getClass().isNpc()) + invalid = true; + else + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto stat = creatureStats.getAiSetting(setting); + stat.setModifier(static_cast(stat.getModifier() - magnitude)); + creatureStats.setAiSetting(setting, stat); + } + } + + void adjustDynamicStat(const MWWorld::Ptr& target, int index, float magnitude, bool allowDecreaseBelowZero = false, bool allowIncreaseAboveModified = false) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto stat = creatureStats.getDynamic(index); + stat.setCurrent(stat.getCurrent() + magnitude, allowDecreaseBelowZero, allowIncreaseAboveModified); + creatureStats.setDynamic(index, stat); + } + + void modDynamicStat(const MWWorld::Ptr& target, int index, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto stat = creatureStats.getDynamic(index); + float current = stat.getCurrent(); + stat.setModified(stat.getModified() - magnitude, 0); + stat.setCurrentModified(stat.getCurrentModified() - magnitude); + stat.setCurrent(current - magnitude); + creatureStats.setDynamic(index, stat); + } + + void damageAttribute(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto attr = creatureStats.getAttribute(effect.mArg); + attr.damage(magnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + + void restoreAttribute(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto attr = creatureStats.getAttribute(effect.mArg); + attr.restore(magnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + + void fortifyAttribute(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto attr = creatureStats.getAttribute(effect.mArg); + attr.setModifier(attr.getModifier() + magnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + + void damageSkill(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.damage(magnitude); + } + + void restoreSkill(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.restore(magnitude); + } + + void fortifySkill(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.setModifier(skill.getModifier() + magnitude); + } + + bool disintegrateSlot(const MWWorld::Ptr& ptr, int slot, float disintegrate) + { + MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); + MWWorld::ContainerStoreIterator item = inv.getSlot(slot); + + if (item != inv.end() && (item.getType() == MWWorld::ContainerStore::Type_Armor || item.getType() == MWWorld::ContainerStore::Type_Weapon)) + { + if (!item->getClass().hasItemHealth(*item)) + return false; + int charge = item->getClass().getItemHealth(*item); + if (charge == 0) + return false; + + // Store remainder of disintegrate amount (automatically subtracted if > 1) + item->getCellRef().applyChargeRemainderToBeSubtracted(disintegrate - std::floor(disintegrate)); + + charge = item->getClass().getItemHealth(*item); + charge -= std::min(static_cast(disintegrate), charge); + item->getCellRef().setCharge(charge); + + if (charge == 0) + { + // Will unequip the broken item and try to find a replacement + if (ptr != MWMechanics::getPlayer()) + inv.autoEquip(ptr); + else + inv.unequipItem(*item, ptr); + } + + return true; + } + + return false; + } + + int getBoundItemSlot(const MWWorld::Ptr& boundPtr) + { + const auto [slots, _] = boundPtr.getClass().getEquipmentSlots(boundPtr); + if(!slots.empty()) + return slots[0]; + return -1; + } + + void addBoundItem(const std::string& itemId, const MWWorld::Ptr& actor) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1, actor); + + int slot = getBoundItemSlot(boundPtr); + auto prevItem = slot >= 0 ? store.getSlot(slot) : store.end(); + + MWWorld::ActionEquip action(boundPtr); + action.execute(actor); + + if (actor != MWMechanics::getPlayer()) + return; + + MWWorld::Ptr newItem; + auto it = slot >= 0 ? store.getSlot(slot) : store.end(); + // Equip can fail because beast races cannot equip boots/helmets + if(it != store.end()) + newItem = *it; + + if (newItem.isEmpty() || boundPtr != newItem) + return; + + MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); + + // change draw state only if the item is in player's right hand + if (slot == MWWorld::InventoryStore::Slot_CarriedRight) + player.setDrawState(MWMechanics::DrawState_Weapon); + + if (prevItem != store.end()) + player.setPreviousItem(itemId, prevItem->getCellRef().getRefId()); + } + + void removeBoundItem(const std::string& itemId, const MWWorld::Ptr& actor) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + auto item = std::find_if(store.begin(), store.end(), [&] (const auto& it) + { + return Misc::StringUtils::ciEqual(it.getCellRef().getRefId(), itemId); + }); + if(item == store.end()) + return; + int slot = getBoundItemSlot(*item); + + auto currentItem = store.getSlot(slot); + + bool wasEquipped = currentItem != store.end() && Misc::StringUtils::ciEqual(currentItem->getCellRef().getRefId(), itemId); + + if (actor != MWMechanics::getPlayer()) + { + store.remove(itemId, 1, actor); + + // Equip a replacement + if (!wasEquipped) + return; + + std::string type = currentItem->getTypeName(); + if (type != typeid(ESM::Weapon).name() && type != typeid(ESM::Armor).name() && type != typeid(ESM::Clothing).name()) + return; + + if (actor.getClass().getCreatureStats(actor).isDead()) + return; + + if (!actor.getClass().hasInventoryStore(actor)) + return; + + if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) + return; + + actor.getClass().getInventoryStore(actor).autoEquip(actor); + + return; + } + + MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); + std::string prevItemId = player.getPreviousItem(itemId); + player.erasePreviousItem(itemId); + + if (!prevItemId.empty() && wasEquipped) + { + // Find previous item (or its replacement) by id. + // we should equip previous item only if expired bound item was equipped. + MWWorld::Ptr prevItem = store.findReplacement(prevItemId); + if (!prevItem.isEmpty()) + { + MWWorld::ActionEquip action(prevItem); + action.execute(actor); + } + } + + store.remove(itemId, 1, actor); + } + + bool isCorprusEffect(const MWMechanics::ActiveSpells::ActiveEffect& effect, bool harmfulOnly = false) + { + if(effect.mFlags & ESM::ActiveEffect::Flag_Applied && effect.mEffectId != ESM::MagicEffect::Corprus) + { + const auto* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectId); + if(magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce && (!harmfulOnly || magicEffect->mData.mFlags & ESM::MagicEffect::Flags::Harmful)) + return true; + } + return false; + } + + static const std::map sBoundItemsMap{ + {ESM::MagicEffect::BoundBattleAxe, "sMagicBoundBattleAxeID"}, + {ESM::MagicEffect::BoundBoots, "sMagicBoundBootsID"}, + {ESM::MagicEffect::BoundCuirass, "sMagicBoundCuirassID"}, + {ESM::MagicEffect::BoundDagger, "sMagicBoundDaggerID"}, + {ESM::MagicEffect::BoundGloves, "sMagicBoundLeftGauntletID"}, + {ESM::MagicEffect::BoundHelm, "sMagicBoundHelmID"}, + {ESM::MagicEffect::BoundLongbow, "sMagicBoundLongbowID"}, + {ESM::MagicEffect::BoundLongsword, "sMagicBoundLongswordID"}, + {ESM::MagicEffect::BoundMace, "sMagicBoundMaceID"}, + {ESM::MagicEffect::BoundShield, "sMagicBoundShieldID"}, + {ESM::MagicEffect::BoundSpear, "sMagicBoundSpearID"} + }; +} + +namespace MWMechanics +{ + +void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, bool& invalid, bool& receivedMagicDamage) +{ + const auto world = MWBase::Environment::get().getWorld(); + bool godmode = target == getPlayer() && world->getGodModeState(); + switch(effect.mEffectId) + { + case ESM::MagicEffect::CureCommonDisease: + target.getClass().getCreatureStats(target).getSpells().purgeCommonDisease(); + break; + case ESM::MagicEffect::CureBlightDisease: + target.getClass().getCreatureStats(target).getSpells().purgeBlightDisease(); + break; + case ESM::MagicEffect::RemoveCurse: + target.getClass().getCreatureStats(target).getSpells().purgeCurses(); + break; + case ESM::MagicEffect::CureCorprusDisease: + target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect(target, ESM::MagicEffect::Corprus); + break; + case ESM::MagicEffect::CurePoison: + target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect(target, ESM::MagicEffect::Poison); + break; + case ESM::MagicEffect::CureParalyzation: + target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect(target, ESM::MagicEffect::Paralyze); + break; + case ESM::MagicEffect::Dispel: + // Dispel removes entire spells at once + target.getClass().getCreatureStats(target).getActiveSpells().purge([magnitude=effect.mMagnitude] (const ActiveSpells::ActiveSpellParams& params) + { + if(params.getType() == ESM::ActiveSpells::Type_Temporary) + { + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(params.getId()); + if(spell && spell->mData.mType == ESM::Spell::ST_Spell) + return Misc::Rng::roll0to99() < magnitude; + } + return false; + }, target); + break; + case ESM::MagicEffect::AlmsiviIntervention: + case ESM::MagicEffect::DivineIntervention: + if (target != getPlayer()) + invalid = true; + else if (world->isTeleportingEnabled()) + { + auto marker = (effect.mEffectId == ESM::MagicEffect::DivineIntervention) ? "divinemarker" : "templemarker"; + world->teleportToClosestMarker(target, marker); + if(!caster.isEmpty()) + { + MWRender::Animation* anim = world->getAnimation(caster); + anim->removeEffect(effect.mEffectId); + const ESM::Static* fx = world->getStore().get().search("VFX_Summon_end"); + if (fx) + anim->addEffect("meshes\\" + fx->mModel, -1); + } + } + else if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + break; + case ESM::MagicEffect::Mark: + if (target != getPlayer()) + invalid = true; + else if (world->isTeleportingEnabled()) + world->getPlayer().markPosition(target.getCell(), target.getRefData().getPosition()); + else if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + break; + case ESM::MagicEffect::Recall: + if (target != getPlayer()) + invalid = true; + else if (world->isTeleportingEnabled()) + { + MWWorld::CellStore* markedCell = nullptr; + ESM::Position markedPosition; + + world->getPlayer().getMarkedPosition(markedCell, markedPosition); + if (markedCell) + { + MWWorld::ActionTeleport action(markedCell->isExterior() ? "" : markedCell->getCell()->mName, markedPosition, false); + action.execute(target); + if(!caster.isEmpty()) + { + MWRender::Animation* anim = world->getAnimation(caster); + anim->removeEffect(effect.mEffectId); + } + } + } + else if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + break; + case ESM::MagicEffect::CommandCreature: + case ESM::MagicEffect::CommandHumanoid: + if(caster.isEmpty() || !caster.getClass().isActor() || target == getPlayer() || (effect.mEffectId == ESM::MagicEffect::CommandCreature) == target.getClass().isNpc()) + invalid = true; + else if(effect.mMagnitude >= target.getClass().getCreatureStats(target).getLevel()) + { + MWMechanics::AiFollow package(caster, true); + target.getClass().getCreatureStats(target).getAiSequence().stack(package, target); + } + break; + case ESM::MagicEffect::ExtraSpell: + if(target.getClass().hasInventoryStore(target)) + { + auto& store = target.getClass().getInventoryStore(target); + store.unequipAll(target); + } + else + invalid = true; + break; + case ESM::MagicEffect::TurnUndead: + if(target.getClass().isNpc() || target.get()->mBase->mData.mType != ESM::Creature::Undead) + invalid = true; + else + { + auto& creatureStats = target.getClass().getCreatureStats(target); + Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); + stat.setModifier(static_cast(stat.getModifier() + effect.mMagnitude)); + creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); + } + break; + case ESM::MagicEffect::FrenzyCreature: + case ESM::MagicEffect::FrenzyHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::FrenzyCreature, CreatureStats::AI_Fight, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::CalmCreature: + case ESM::MagicEffect::CalmHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::CalmCreature, CreatureStats::AI_Fight, effect.mMagnitude, invalid); + if(!invalid && effect.mMagnitude > 0) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + creatureStats.getAiSequence().stopCombat(); + } + break; + case ESM::MagicEffect::DemoralizeCreature: + case ESM::MagicEffect::DemoralizeHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::DemoralizeCreature, CreatureStats::AI_Flee, effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::RallyCreature: + case ESM::MagicEffect::RallyHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::RallyCreature, CreatureStats::AI_Flee, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::SummonScamp: + case ESM::MagicEffect::SummonClannfear: + case ESM::MagicEffect::SummonDaedroth: + case ESM::MagicEffect::SummonDremora: + case ESM::MagicEffect::SummonAncestralGhost: + case ESM::MagicEffect::SummonSkeletalMinion: + case ESM::MagicEffect::SummonBonewalker: + case ESM::MagicEffect::SummonGreaterBonewalker: + case ESM::MagicEffect::SummonBonelord: + case ESM::MagicEffect::SummonWingedTwilight: + case ESM::MagicEffect::SummonHunger: + case ESM::MagicEffect::SummonGoldenSaint: + case ESM::MagicEffect::SummonFlameAtronach: + case ESM::MagicEffect::SummonFrostAtronach: + case ESM::MagicEffect::SummonStormAtronach: + case ESM::MagicEffect::SummonCenturionSphere: + case ESM::MagicEffect::SummonFabricant: + case ESM::MagicEffect::SummonWolf: + case ESM::MagicEffect::SummonBear: + case ESM::MagicEffect::SummonBonewolf: + case ESM::MagicEffect::SummonCreature04: + case ESM::MagicEffect::SummonCreature05: + if(!target.isInCell()) + invalid = true; + else + effect.mArg = summonCreature(effect.mEffectId, target); + break; + case ESM::MagicEffect::BoundGloves: + if(!target.getClass().hasInventoryStore(target)) + { + invalid = true; + break; + } + addBoundItem(world->getStore().get().find("sMagicBoundRightGauntletID")->mValue.getString(), target); + // left gauntlet added below + case ESM::MagicEffect::BoundDagger: + case ESM::MagicEffect::BoundLongsword: + case ESM::MagicEffect::BoundMace: + case ESM::MagicEffect::BoundBattleAxe: + case ESM::MagicEffect::BoundSpear: + case ESM::MagicEffect::BoundLongbow: + case ESM::MagicEffect::BoundCuirass: + case ESM::MagicEffect::BoundHelm: + case ESM::MagicEffect::BoundBoots: + case ESM::MagicEffect::BoundShield: + if(!target.getClass().hasInventoryStore(target)) + invalid = true; + else + { + const std::string& item = sBoundItemsMap.at(effect.mEffectId); + addBoundItem(world->getStore().get().find(item)->mValue.getString(), target); + } + break; + case ESM::MagicEffect::FireDamage: + case ESM::MagicEffect::ShockDamage: + case ESM::MagicEffect::FrostDamage: + case ESM::MagicEffect::DamageHealth: + case ESM::MagicEffect::Poison: + case ESM::MagicEffect::DamageMagicka: + case ESM::MagicEffect::DamageFatigue: + if(!godmode) + { + int index = 0; + if(effect.mEffectId == ESM::MagicEffect::DamageMagicka) + index = 1; + else if(effect.mEffectId == ESM::MagicEffect::DamageFatigue) + index = 2; + // Damage "Dynamic" abilities reduce the base value + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + modDynamicStat(target, index, effect.mMagnitude); + else + { + static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game"); + adjustDynamicStat(target, index, -effect.mMagnitude, index == 2 && uncappedDamageFatigue); + if(index == 0) + receivedMagicDamage = true; + } + } + break; + case ESM::MagicEffect::DamageAttribute: + if(!godmode) + damageAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::DamageSkill: + if(!target.getClass().isNpc()) + invalid = true; + else if(!godmode) + { + // Damage Skill abilities reduce base skill :todd: + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + auto& npcStats = target.getClass().getNpcStats(target); + SkillValue& skill = npcStats.getSkill(effect.mArg); + // Damage Skill abilities reduce base skill :todd: + skill.setBase(std::max(skill.getBase() - effect.mMagnitude, 0.f)); + } + else + damageSkill(target, effect, effect.mMagnitude); + } + break; + case ESM::MagicEffect::RestoreAttribute: + restoreAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::RestoreSkill: + if(!target.getClass().isNpc()) + invalid = true; + else + restoreSkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::RestoreHealth: + case ESM::MagicEffect::RestoreMagicka: + case ESM::MagicEffect::RestoreFatigue: + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::RestoreHealth, effect.mMagnitude); + break; + case ESM::MagicEffect::SunDamage: + { + // isInCell shouldn't be needed, but updateActor called during game start + if (!target.isInCell() || !target.getCell()->isExterior() || godmode) + break; + float time = world->getTimeStamp().getHour(); + float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13))); + float damageScale = 1.f - timeDiff / 7.f; + // When cloudy, the sun damage effect is halved + static float fMagicSunBlockedMult = world->getStore().get().find("fMagicSunBlockedMult")->mValue.getFloat(); + + int weather = world->getCurrentWeather(); + if (weather > 1) + damageScale *= fMagicSunBlockedMult; + float damage = effect.mMagnitude * damageScale; + adjustDynamicStat(target, 0, -damage); + if (damage > 0.f) + receivedMagicDamage = true; + } + break; + case ESM::MagicEffect::DrainHealth: + case ESM::MagicEffect::DrainMagicka: + case ESM::MagicEffect::DrainFatigue: + if(!godmode) + { + static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game"); + int index = effect.mEffectId - ESM::MagicEffect::DrainHealth; + adjustDynamicStat(target, index, -effect.mMagnitude, uncappedDamageFatigue && index == 2); + if(index == 0) + receivedMagicDamage = true; + } + break; + case ESM::MagicEffect::FortifyHealth: + case ESM::MagicEffect::FortifyMagicka: + case ESM::MagicEffect::FortifyFatigue: + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, effect.mMagnitude); + else + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, effect.mMagnitude, false, true); + break; + case ESM::MagicEffect::DrainAttribute: + if(!godmode) + damageAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyAttribute: + // Abilities affect base stats, but not for drain + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + AttributeValue attr = creatureStats.getAttribute(effect.mArg); + attr.setBase(attr.getBase() + effect.mMagnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + else + fortifyAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::DrainSkill: + if(!target.getClass().isNpc()) + invalid = true; + else if(!godmode) + damageSkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifySkill: + if(!target.getClass().isNpc()) + invalid = true; + else if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + // Abilities affect base stats, but not for drain + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.setBase(skill.getBase() + effect.mMagnitude); + } + else + fortifySkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyMaximumMagicka: + target.getClass().getCreatureStats(target).setNeedRecalcDynamicStats(true); + break; + case ESM::MagicEffect::AbsorbHealth: + case ESM::MagicEffect::AbsorbMagicka: + case ESM::MagicEffect::AbsorbFatigue: + if(!godmode) + { + int index = effect.mEffectId - ESM::MagicEffect::AbsorbHealth; + adjustDynamicStat(target, index, -effect.mMagnitude); + if(!caster.isEmpty()) + adjustDynamicStat(caster, index, effect.mMagnitude); + if(index == 0) + receivedMagicDamage = true; + } + break; + case ESM::MagicEffect::AbsorbAttribute: + if(!godmode) + { + damageAttribute(target, effect, effect.mMagnitude); + if(!caster.isEmpty()) + fortifyAttribute(caster, effect, effect.mMagnitude); + } + break; + case ESM::MagicEffect::AbsorbSkill: + if(!target.getClass().isNpc()) + invalid = true; + else if(!godmode) + { + damageSkill(target, effect, effect.mMagnitude); + if(!caster.isEmpty()) + fortifySkill(caster, effect, effect.mMagnitude); + } + break; + case ESM::MagicEffect::DisintegrateArmor: + { + if (!target.getClass().hasInventoryStore(target)) + { + invalid = true; + break; + } + if (godmode) + break; + static const std::array priorities + { + MWWorld::InventoryStore::Slot_CarriedLeft, + MWWorld::InventoryStore::Slot_Cuirass, + MWWorld::InventoryStore::Slot_LeftPauldron, + MWWorld::InventoryStore::Slot_RightPauldron, + MWWorld::InventoryStore::Slot_LeftGauntlet, + MWWorld::InventoryStore::Slot_RightGauntlet, + MWWorld::InventoryStore::Slot_Helmet, + MWWorld::InventoryStore::Slot_Greaves, + MWWorld::InventoryStore::Slot_Boots + }; + for (const int priority : priorities) + { + if (disintegrateSlot(target, priority, effect.mMagnitude)) + break; + } + break; + } + case ESM::MagicEffect::DisintegrateWeapon: + if (!target.getClass().hasInventoryStore(target)) + { + invalid = true; + break; + } + if (!godmode) + disintegrateSlot(target, MWWorld::InventoryStore::Slot_CarriedRight, effect.mMagnitude); + break; + } +} + +bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt) +{ + const auto world = MWBase::Environment::get().getWorld(); + bool invalid = false; + bool receivedMagicDamage = false; + if(effect.mEffectId == ESM::MagicEffect::Corprus && spellParams.shouldWorsen()) + { + spellParams.worsen(); + for(auto& otherEffect : spellParams.getEffects()) + { + if(isCorprusEffect(otherEffect)) + applyMagicEffect(target, caster, spellParams, otherEffect, invalid, receivedMagicDamage); + } + if(target == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); + return false; + } + else if(effect.mEffectId == ESM::MagicEffect::Levitate && !world->isLevitationEnabled()) + { + if(target == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox ("#{sLevitateDisabled}"); + return true; + } + const auto* magicEffect = world->getStore().get().find(effect.mEffectId); + if(effect.mFlags & ESM::ActiveEffect::Flag_Applied && magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce) + { + effect.mTimeLeft -= dt; + return false; + } + if(effect.mEffectId == ESM::MagicEffect::Lock) + { + if(target.getClass().canLock(target)) + { + MWRender::Animation* animation = world->getAnimation(target); + if(animation) + animation->addSpellCastGlow(magicEffect); + int magnitude = static_cast(roll(effect)); + if(target.getCellRef().getLockLevel() < magnitude) //If the door is not already locked to a higher value, lock it to spell magnitude + { + if(caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicLockSuccess}"); + target.getCellRef().lock(magnitude); + } + } + else + invalid = true; + } + else if(effect.mEffectId == ESM::MagicEffect::Open) + { + if(target.getClass().canLock(target)) + { + // Use the player instead of the caster for vanilla crime compatibility + MWBase::Environment::get().getMechanicsManager()->unlockAttempted(getPlayer(), target); + + MWRender::Animation* animation = world->getAnimation(target); + if(animation) + animation->addSpellCastGlow(magicEffect); + int magnitude = static_cast(roll(effect)); + if(target.getCellRef().getLockLevel() <= magnitude) + { + if(target.getCellRef().getLockLevel() > 0) + { + MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock", 1.f, 1.f); + + if(caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicOpenSuccess}"); + } + target.getCellRef().unlock(); + } + else + { + MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock Fail", 1.f, 1.f); + } + } + else + invalid = true; + } + else if(!target.getClass().isActor()) + { + invalid = true; + } + else + { + auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); + if(spellParams.getType() != ESM::ActiveSpells::Type_Ability && !(effect.mFlags & (ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Ignore_Resistances))) + { + const ESM::Spell* spell = nullptr; + if(spellParams.getType() == ESM::ActiveSpells::Type_Temporary) + spell = world->getStore().get().search(spellParams.getId()); + float magnitudeMult = getEffectMultiplier(effect.mEffectId, target, caster, spell, &magnitudes); + if (magnitudeMult == 0) + { + // Fully resisted, show message + if (target == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); + else if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); + return true; + } + effect.mMinMagnitude *= magnitudeMult; + effect.mMaxMagnitude *= magnitudeMult; + } + float oldMagnitude = 0.f; + if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) + oldMagnitude = effect.mMagnitude; + //Note that there's an early out for Flag_Applied AppliedOnce effects so we don't have to exclude them here + effect.mMagnitude = roll(effect); + if(!(magicEffect->mData.mFlags & (ESM::MagicEffect::Flags::NoMagnitude | ESM::MagicEffect::Flags::AppliedOnce))) + { + if(effect.mDuration != 0) + { + float mult = dt; + if(spellParams.getType() == ESM::ActiveSpells::Type_Consumable || spellParams.getType() == ESM::ActiveSpells::Type_Temporary) + mult = std::min(effect.mTimeLeft, dt); + effect.mMagnitude *= mult; + } + if(effect.mMagnitude == 0) + { + effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; + effect.mTimeLeft -= dt; + return false; + } + } + if(effect.mEffectId == ESM::MagicEffect::Corprus) + spellParams.worsen(); + else + applyMagicEffect(target, caster, spellParams, effect, invalid, receivedMagicDamage); + magnitudes.add(EffectKey(effect.mEffectId, effect.mArg), EffectParam(effect.mMagnitude - oldMagnitude)); + } + effect.mTimeLeft -= dt; + if(invalid) + { + effect.mTimeLeft = 0; + effect.mFlags |= ESM::ActiveEffect::Flag_Remove; + auto anim = world->getAnimation(target); + if(anim) + anim->removeEffect(effect.mEffectId); + } + else + effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; + if (receivedMagicDamage && target == getPlayer()) + MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); + return false; +} + +void removeMagicEffect(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spellParams, const ESM::ActiveEffect& effect) +{ + const auto world = MWBase::Environment::get().getWorld(); + auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); + bool invalid; + switch(effect.mEffectId) + { + case ESM::MagicEffect::CommandCreature: + case ESM::MagicEffect::CommandHumanoid: + if(magnitudes.get(effect.mEffectId).getMagnitude() <= 0.f) + { + auto& seq = target.getClass().getCreatureStats(target).getAiSequence(); + auto it = std::find_if(seq.begin(), seq.end(), [&](const auto& package) + { + return package->getTypeId() == MWMechanics::AiPackageTypeId::Follow && static_cast(package.get())->isCommanded(); + }); + if(it != seq.end()) + seq.erase(it); + } + break; + case ESM::MagicEffect::ExtraSpell: + if(magnitudes.get(effect.mEffectId).getMagnitude() <= 0.f) + target.getClass().getInventoryStore(target).autoEquip(target); + break; + case ESM::MagicEffect::TurnUndead: + { + auto& creatureStats = target.getClass().getCreatureStats(target); + Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); + stat.setModifier(static_cast(stat.getModifier() - effect.mMagnitude)); + creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); + } + break; + case ESM::MagicEffect::FrenzyCreature: + case ESM::MagicEffect::FrenzyHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::FrenzyCreature, CreatureStats::AI_Fight, effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::CalmCreature: + case ESM::MagicEffect::CalmHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::CalmCreature, CreatureStats::AI_Fight, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::DemoralizeCreature: + case ESM::MagicEffect::DemoralizeHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::DemoralizeCreature, CreatureStats::AI_Flee, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::RallyCreature: + case ESM::MagicEffect::RallyHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::RallyCreature, CreatureStats::AI_Flee, effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::SummonScamp: + case ESM::MagicEffect::SummonClannfear: + case ESM::MagicEffect::SummonDaedroth: + case ESM::MagicEffect::SummonDremora: + case ESM::MagicEffect::SummonAncestralGhost: + case ESM::MagicEffect::SummonSkeletalMinion: + case ESM::MagicEffect::SummonBonewalker: + case ESM::MagicEffect::SummonGreaterBonewalker: + case ESM::MagicEffect::SummonBonelord: + case ESM::MagicEffect::SummonWingedTwilight: + case ESM::MagicEffect::SummonHunger: + case ESM::MagicEffect::SummonGoldenSaint: + case ESM::MagicEffect::SummonFlameAtronach: + case ESM::MagicEffect::SummonFrostAtronach: + case ESM::MagicEffect::SummonStormAtronach: + case ESM::MagicEffect::SummonCenturionSphere: + case ESM::MagicEffect::SummonFabricant: + case ESM::MagicEffect::SummonWolf: + case ESM::MagicEffect::SummonBear: + case ESM::MagicEffect::SummonBonewolf: + case ESM::MagicEffect::SummonCreature04: + case ESM::MagicEffect::SummonCreature05: + { + if(effect.mArg != -1) + MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(target, effect.mArg); + auto& summons = target.getClass().getCreatureStats(target).getSummonedCreatureMap(); + auto [begin, end] = summons.equal_range(effect.mEffectId); + for(auto it = begin; it != end; ++it) + { + if(it->second == effect.mArg) + { + summons.erase(it); + break; + } + } + } + break; + case ESM::MagicEffect::BoundGloves: + removeBoundItem(world->getStore().get().find("sMagicBoundRightGauntletID")->mValue.getString(), target); + case ESM::MagicEffect::BoundDagger: + case ESM::MagicEffect::BoundLongsword: + case ESM::MagicEffect::BoundMace: + case ESM::MagicEffect::BoundBattleAxe: + case ESM::MagicEffect::BoundSpear: + case ESM::MagicEffect::BoundLongbow: + case ESM::MagicEffect::BoundCuirass: + case ESM::MagicEffect::BoundHelm: + case ESM::MagicEffect::BoundBoots: + case ESM::MagicEffect::BoundShield: + { + const std::string& item = sBoundItemsMap.at(effect.mEffectId); + removeBoundItem(world->getStore().get().find(item)->mValue.getString(), target); + } + break; + case ESM::MagicEffect::DrainHealth: + case ESM::MagicEffect::DrainMagicka: + case ESM::MagicEffect::DrainFatigue: + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::DrainHealth, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyHealth: + case ESM::MagicEffect::FortifyMagicka: + case ESM::MagicEffect::FortifyFatigue: + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, -effect.mMagnitude); + else + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, -effect.mMagnitude, true); + break; + case ESM::MagicEffect::DrainAttribute: + restoreAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyAttribute: + // Abilities affect base stats, but not for drain + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + AttributeValue attr = creatureStats.getAttribute(effect.mArg); + attr.setBase(attr.getBase() - effect.mMagnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + else + fortifyAttribute(target, effect, -effect.mMagnitude); + break; + case ESM::MagicEffect::DrainSkill: + restoreSkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifySkill: + // Abilities affect base stats, but not for drain + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.setBase(skill.getBase() - effect.mMagnitude); + } + else + fortifySkill(target, effect, -effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyMaximumMagicka: + target.getClass().getCreatureStats(target).setNeedRecalcDynamicStats(true); + break; + case ESM::MagicEffect::AbsorbAttribute: + { + const auto caster = world->searchPtrViaActorId(spellParams.getCasterActorId()); + restoreAttribute(target, effect, effect.mMagnitude); + if(!caster.isEmpty()) + fortifyAttribute(caster, effect, -effect.mMagnitude); + } + break; + case ESM::MagicEffect::AbsorbSkill: + { + const auto caster = world->searchPtrViaActorId(spellParams.getCasterActorId()); + restoreSkill(target, effect, effect.mMagnitude); + if(!caster.isEmpty()) + fortifySkill(caster, effect, -effect.mMagnitude); + } + break; + case ESM::MagicEffect::Corprus: + { + int worsenings = spellParams.getWorsenings(); + spellParams.resetWorsenings(); + if(worsenings > 0) + { + for(const auto& otherEffect : spellParams.getEffects()) + { + if(isCorprusEffect(otherEffect, true)) + { + for(int i = 0; i < worsenings; i++) + removeMagicEffect(target, spellParams, otherEffect); + } + } + } + //Note that we remove the effects, but keep the params + target.getClass().getCreatureStats(target).getActiveSpells().purge([&spellParams] (const ActiveSpells::ActiveSpellParams& params, const auto&) + { + return &spellParams == ¶ms; + }, target); + } + break; + } +} + +void onMagicEffectRemoved(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spellParams, const ESM::ActiveEffect& effect) +{ + if(!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) + return; + const auto world = MWBase::Environment::get().getWorld(); + auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); + const auto* magicEffect = world->getStore().get().find(effect.mEffectId); + if(magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce) + magnitudes.add(EffectKey(effect.mEffectId, effect.mArg), EffectParam(-effect.mMinMagnitude)); + removeMagicEffect(target, spellParams, effect); + auto anim = world->getAnimation(target); + if(anim) + anim->removeEffect(effect.mEffectId); +} + +} \ No newline at end of file diff --git a/apps/openmw/mwmechanics/spelleffects.hpp b/apps/openmw/mwmechanics/spelleffects.hpp new file mode 100644 index 0000000000..a9e7b066f3 --- /dev/null +++ b/apps/openmw/mwmechanics/spelleffects.hpp @@ -0,0 +1,20 @@ +#ifndef GAME_MWMECHANICS_SPELLEFFECTS_H +#define GAME_MWMECHANICS_SPELLEFFECTS_H + +#include "activespells.hpp" + +#include "../mwworld/ptr.hpp" + +// These functions should probably be split up into separate Lua functions for each magic effect when magic is dehardcoded. +// That way ESM::MGEF could point to two Lua scripts for each effect. Needs discussion. + +namespace MWMechanics +{ + // Applies a tick of a single effect. Returns true if the effect should be removed immediately + bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt); + + // Undoes permanent effects created by ESM::MagicEffect::AppliedOnce + void onMagicEffectRemoved(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spell, const ESM::ActiveEffect& effect); +} + +#endif diff --git a/apps/openmw/mwmechanics/spelllist.hpp b/apps/openmw/mwmechanics/spelllist.hpp index 5920949d65..f5759fd7ee 100644 --- a/apps/openmw/mwmechanics/spelllist.hpp +++ b/apps/openmw/mwmechanics/spelllist.hpp @@ -16,12 +16,6 @@ namespace ESM namespace MWMechanics { - struct SpellParams - { - std::map mEffectRands; // - std::set mPurgedEffects; // indices of purged effects - }; - class Spells; /// Multiple instances of the same actor share the same spell list in Morrowind. diff --git a/apps/openmw/mwmechanics/spellpriority.cpp b/apps/openmw/mwmechanics/spellpriority.cpp index bd9c5f7cb3..974d297f10 100644 --- a/apps/openmw/mwmechanics/spellpriority.cpp +++ b/apps/openmw/mwmechanics/spellpriority.cpp @@ -31,21 +31,20 @@ namespace // if the effect filter is not specified, take in account only spells effects. Leave potions, enchanted items etc. if (effectFilter == -1) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->getId()); if (!spell || spell->mData.mType != ESM::Spell::ST_Spell) continue; } - const MWMechanics::ActiveSpells::ActiveSpellParams& params = it->second; - for (std::vector::const_iterator effectIt = params.mEffects.begin(); - effectIt != params.mEffects.end(); ++effectIt) + const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it; + for (const auto& effect : params.getEffects()) { - int effectId = effectIt->mEffectId; + int effectId = effect.mEffectId; if (effectFilter != -1 && effectId != effectFilter) continue; const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectId); - if (effectIt->mDuration <= 3) // Don't attempt to dispel if effect runs out shortly anyway + if (effect.mDuration <= 3) // Don't attempt to dispel if effect runs out shortly anyway continue; if (negative && magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) @@ -64,15 +63,14 @@ namespace const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells(); for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it) { - if (it->first != spellId) + if (it->getId() != spellId) continue; - const MWMechanics::ActiveSpells::ActiveSpellParams& params = it->second; - for (std::vector::const_iterator effectIt = params.mEffects.begin(); - effectIt != params.mEffects.end(); ++effectIt) + const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it; + for (const auto& effect : params.getEffects()) { - if (effectIt->mDuration > duration) - duration = effectIt->mDuration; + if (effect.mDuration > duration) + duration = effect.mDuration; } } return duration; diff --git a/apps/openmw/mwmechanics/spells.cpp b/apps/openmw/mwmechanics/spells.cpp index b871376003..743eacfe2c 100644 --- a/apps/openmw/mwmechanics/spells.cpp +++ b/apps/openmw/mwmechanics/spells.cpp @@ -20,72 +20,33 @@ namespace MWMechanics { Spells::Spells() - : mSpellsChanged(false) { } Spells::Spells(const Spells& spells) : mSpellList(spells.mSpellList), mSpells(spells.mSpells), - mSelectedSpell(spells.mSelectedSpell), mUsedPowers(spells.mUsedPowers), - mSpellsChanged(spells.mSpellsChanged), mEffects(spells.mEffects), mSourcedEffects(spells.mSourcedEffects) + mSelectedSpell(spells.mSelectedSpell), mUsedPowers(spells.mUsedPowers) { if(mSpellList) mSpellList->addListener(this); } Spells::Spells(Spells&& spells) : mSpellList(std::move(spells.mSpellList)), mSpells(std::move(spells.mSpells)), - mSelectedSpell(std::move(spells.mSelectedSpell)), mUsedPowers(std::move(spells.mUsedPowers)), - mSpellsChanged(std::move(spells.mSpellsChanged)), mEffects(std::move(spells.mEffects)), - mSourcedEffects(std::move(spells.mSourcedEffects)) + mSelectedSpell(std::move(spells.mSelectedSpell)), mUsedPowers(std::move(spells.mUsedPowers)) { if (mSpellList) mSpellList->updateListener(&spells, this); } - std::map::const_iterator Spells::begin() const + std::vector::const_iterator Spells::begin() const { return mSpells.begin(); } - std::map::const_iterator Spells::end() const + std::vector::const_iterator Spells::end() const { return mSpells.end(); } - void Spells::rebuildEffects() const - { - mEffects = MagicEffects(); - mSourcedEffects.clear(); - - for (const auto& iter : mSpells) - { - const ESM::Spell *spell = iter.first; - - if (spell->mData.mType==ESM::Spell::ST_Ability || spell->mData.mType==ESM::Spell::ST_Blight || - spell->mData.mType==ESM::Spell::ST_Disease || spell->mData.mType==ESM::Spell::ST_Curse) - { - int i=0; - for (const auto& effect : spell->mEffects.mList) - { - if (iter.second.mPurgedEffects.find(i) != iter.second.mPurgedEffects.end()) - { - ++i; - continue; // effect was purged - } - - float random = 1.f; - if (iter.second.mEffectRands.find(i) != iter.second.mEffectRands.end()) - random = iter.second.mEffectRands.at(i); - - float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * random; - mEffects.add (effect, magnitude); - mSourcedEffects[spell].add(MWMechanics::EffectKey(effect), magnitude); - - ++i; - } - } - } - } - bool Spells::hasSpell(const std::string &spell) const { return hasSpell(SpellList::getSpell(spell)); @@ -93,7 +54,7 @@ namespace MWMechanics bool Spells::hasSpell(const ESM::Spell *spell) const { - return mSpells.find(spell) != mSpells.end(); + return std::find(mSpells.begin(), mSpells.end(), spell) != mSpells.end(); } void Spells::add (const ESM::Spell* spell) @@ -108,29 +69,8 @@ namespace MWMechanics void Spells::addSpell(const ESM::Spell* spell) { - if (mSpells.find (spell)==mSpells.end()) - { - std::map random; - - // Determine the random magnitudes (unless this is a castable spell, in which case - // they will be determined when the spell is cast) - if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) - { - for (unsigned int i=0; imEffects.mList.size();++i) - { - if (spell->mEffects.mList[i].mMagnMin != spell->mEffects.mList[i].mMagnMax) - { - int delta = spell->mEffects.mList[i].mMagnMax - spell->mEffects.mList[i].mMagnMin; - random[i] = Misc::Rng::rollDice(delta + 1) / static_cast(delta); - } - } - } - - SpellParams params; - params.mEffectRands = random; - mSpells.emplace(spell, params); - mSpellsChanged = true; - } + if (!hasSpell(spell)) + mSpells.emplace_back(spell); } void Spells::remove (const std::string& spellId) @@ -145,27 +85,14 @@ namespace MWMechanics void Spells::removeSpell(const ESM::Spell* spell) { - const auto it = mSpells.find(spell); + const auto it = std::find(mSpells.begin(), mSpells.end(), spell); if(it != mSpells.end()) - { mSpells.erase(it); - mSpellsChanged = true; - } - } - - MagicEffects Spells::getMagicEffects() const - { - if (mSpellsChanged) { - rebuildEffects(); - mSpellsChanged = false; - } - return mEffects; } void Spells::removeAllSpells() { mSpells.clear(); - mSpellsChanged = true; } void Spells::clear(bool modifyBase) @@ -185,26 +112,10 @@ namespace MWMechanics return mSelectedSpell; } - bool Spells::isSpellActive(const std::string &id) const - { - if (id.empty()) - return false; - - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(id); - if (spell && hasSpell(spell)) - { - auto type = spell->mData.mType; - return (type==ESM::Spell::ST_Ability || type==ESM::Spell::ST_Blight || type==ESM::Spell::ST_Disease || type==ESM::Spell::ST_Curse); - } - - return false; - } - bool Spells::hasDisease(const ESM::Spell::SpellType type) const { - for (const auto& iter : mSpells) + for (const auto spell : mSpells) { - const ESM::Spell *spell = iter.first; if (spell->mData.mType == type) return true; } @@ -227,12 +138,11 @@ namespace MWMechanics std::vector purged; for (auto iter = mSpells.begin(); iter!=mSpells.end();) { - const ESM::Spell *spell = iter->first; + const ESM::Spell *spell = *iter; if (filter(spell)) { mSpells.erase(iter++); purged.push_back(spell->mId); - mSpellsChanged = true; } else ++iter; @@ -261,43 +171,6 @@ namespace MWMechanics purge([](auto spell) { return spell->mData.mType == ESM::Spell::ST_Curse; }); } - void Spells::removeEffects(const std::string &id) - { - if (isSpellActive(id)) - { - for (auto& spell : mSpells) - { - if (spell.first == SpellList::getSpell(id)) - { - for (long unsigned int i = 0; i != spell.first->mEffects.mList.size(); i++) - { - spell.second.mPurgedEffects.insert(i); - } - } - } - - mSpellsChanged = true; - } - } - - void Spells::visitEffectSources(EffectSourceVisitor &visitor) const - { - if (mSpellsChanged) { - rebuildEffects(); - mSpellsChanged = false; - } - - for (const auto& it : mSourcedEffects) - { - const ESM::Spell * spell = it.first; - for (const auto& effectIt : it.second) - { - // FIXME: since Spells merges effects with the same ID, there is no sense to use multiple effects with same ID here - visitor.visit(effectIt.first, -1, spell->mName, spell->mId, -1, effectIt.second.getMagnitude()); - } - } - } - bool Spells::hasCorprusEffect(const ESM::Spell *spell) { for (const auto& effectIt : spell->mEffects.mList) @@ -310,46 +183,6 @@ namespace MWMechanics return false; } - void Spells::purgeEffect(int effectId) - { - for (auto& spellIt : mSpells) - { - int i = 0; - for (auto& effectIt : spellIt.first->mEffects.mList) - { - if (effectIt.mEffectID == effectId) - { - spellIt.second.mPurgedEffects.insert(i); - mSpellsChanged = true; - } - ++i; - } - } - } - - void Spells::purgeEffect(int effectId, const std::string & sourceId) - { - // Effect source may be not a spell - const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(sourceId); - if (spell == nullptr) - return; - - auto spellIt = mSpells.find(spell); - if (spellIt == mSpells.end()) - return; - - int index = 0; - for (auto& effectIt : spellIt->first->mEffects.mList) - { - if (effectIt.mEffectID == effectId) - { - spellIt->second.mPurgedEffects.insert(index); - mSpellsChanged = true; - } - ++index; - } - } - bool Spells::canUsePower(const ESM::Spell* spell) const { const auto it = mUsedPowers.find(spell); @@ -365,17 +198,16 @@ namespace MWMechanics { const auto& baseSpells = mSpellList->getSpells(); - for (ESM::SpellState::TContainer::const_iterator it = state.mSpells.begin(); it != state.mSpells.end(); ++it) + for (const std::string& id : state.mSpells) { // Discard spells that are no longer available due to changed content files - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(id); if (spell) { - mSpells[spell].mEffectRands = it->second.mEffectRands; - mSpells[spell].mPurgedEffects = it->second.mPurgedEffects; + mSpells.emplace_back(spell); - if (it->first == state.mSelectedSpell) - mSelectedSpell = it->first; + if (id == state.mSelectedSpell) + mSelectedSpell = id; } } // Add spells from the base record @@ -394,31 +226,6 @@ namespace MWMechanics mUsedPowers[spell] = MWWorld::TimeStamp(it->second); } - for (std::map::const_iterator it = state.mCorprusSpells.begin(); it != state.mCorprusSpells.end(); ++it) - { - const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); - if (!spell) - continue; - - CorprusStats stats; - - int worsening = state.mCorprusSpells.at(it->first).mWorsenings; - - for (int i=0; imEffects.mList) - { - if (effect.mEffectID == ESM::MagicEffect::DrainAttribute) - stats.mWorsenings[effect.mAttribute] = worsening; - } - stats.mNextWorsening = MWWorld::TimeStamp(state.mCorprusSpells.at(it->first).mNextWorsening); - - creatureStats->addCorprusSpell(it->first, stats); - } - - mSpellsChanged = true; - // Permanent effects are used only to keep the custom magnitude of corprus spells effects (after cure too), and only in old saves. Convert data to the new approach. for (std::map >::const_iterator it = state.mPermanentSpellEffects.begin(); it != state.mPermanentSpellEffects.end(); ++it) @@ -457,16 +264,13 @@ namespace MWMechanics void Spells::writeState(ESM::SpellState &state) const { const auto& baseSpells = mSpellList->getSpells(); - for (const auto& it : mSpells) + for (const auto spell : mSpells) { // Don't save spells and powers stored in the base record - if((it.first->mData.mType != ESM::Spell::ST_Spell && it.first->mData.mType != ESM::Spell::ST_Power) || - std::find(baseSpells.begin(), baseSpells.end(), it.first->mId) == baseSpells.end()) + if((spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power) || + std::find(baseSpells.begin(), baseSpells.end(), spell->mId) == baseSpells.end()) { - ESM::SpellState::SpellParams params; - params.mEffectRands = it.second.mEffectRands; - params.mPurgedEffects = it.second.mPurgedEffects; - state.mSpells.emplace(it.first->mId, params); + state.mSpells.emplace_back(spell->mId); } } diff --git a/apps/openmw/mwmechanics/spells.hpp b/apps/openmw/mwmechanics/spells.hpp index 9737b72cd0..29f505d369 100644 --- a/apps/openmw/mwmechanics/spells.hpp +++ b/apps/openmw/mwmechanics/spells.hpp @@ -29,18 +29,13 @@ namespace MWMechanics class Spells { std::shared_ptr mSpellList; - std::map mSpells; + std::vector mSpells; // Note: this is the spell that's about to be cast, *not* the spell selected in the GUI (which may be different) std::string mSelectedSpell; std::map mUsedPowers; - mutable bool mSpellsChanged; - mutable MagicEffects mEffects; - mutable std::map mSourcedEffects; - void rebuildEffects() const; - bool hasDisease(const ESM::Spell::SpellType type) const; using SpellFilter = bool (*)(const ESM::Spell*); @@ -52,8 +47,6 @@ namespace MWMechanics friend class SpellList; public: - using TIterator = std::map::const_iterator; - Spells(); Spells(const Spells&); @@ -64,9 +57,6 @@ namespace MWMechanics static bool hasCorprusEffect(const ESM::Spell *spell); - void purgeEffect(int effectId); - void purgeEffect(int effectId, const std::string & sourceId); - bool canUsePower (const ESM::Spell* spell) const; void usePower (const ESM::Spell* spell); @@ -75,9 +65,9 @@ namespace MWMechanics void purgeCorprusDisease(); void purgeCurses(); - TIterator begin() const; + std::vector::const_iterator begin() const; - TIterator end() const; + std::vector::const_iterator end() const; bool hasSpell(const std::string& spell) const; bool hasSpell(const ESM::Spell* spell) const; @@ -92,9 +82,6 @@ namespace MWMechanics ///< If the spell to be removed is the selected spell, the selected spell will be changed to /// no spell (empty string). - MagicEffects getMagicEffects() const; - ///< Return sum of magic effects resulting from abilities, blights, deseases and curses. - void clear(bool modifyBase = false); ///< Remove all spells of al types. @@ -104,17 +91,10 @@ namespace MWMechanics const std::string getSelectedSpell() const; ///< May return an empty string. - bool isSpellActive(const std::string& id) const; - ///< Are we under the effects of the given spell ID? - bool hasCommonDisease() const; bool hasBlightDisease() const; - void removeEffects(const std::string& id); - - void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor) const; - void readState (const ESM::SpellState& state, CreatureStats* creatureStats); void writeState (ESM::SpellState& state) const; diff --git a/apps/openmw/mwmechanics/summoning.cpp b/apps/openmw/mwmechanics/summoning.cpp index d90545fc60..953db077fb 100644 --- a/apps/openmw/mwmechanics/summoning.cpp +++ b/apps/openmw/mwmechanics/summoning.cpp @@ -60,90 +60,60 @@ namespace MWMechanics return std::string(); } - UpdateSummonedCreatures::UpdateSummonedCreatures(const MWWorld::Ptr &actor) - : mActor(actor) + int summonCreature(int effectId, const MWWorld::Ptr& summoner) { - } - - void UpdateSummonedCreatures::visit(EffectKey key, int effectIndex, const std::string &sourceName, const std::string &sourceId, int casterActorId, float magnitude, float remainingTime, float totalTime) - { - if (isSummoningEffect(key.mId) && magnitude > 0) + std::string creatureID = getSummonedCreature(effectId); + int creatureActorId = -1; + if (!creatureID.empty()) { - mActiveEffects.insert(ESM::SummonKey(key.mId, sourceId, effectIndex)); - } - } + try + { + auto world = MWBase::Environment::get().getWorld(); + MWWorld::ManualRef ref(world->getStore(), creatureID, 1); - void UpdateSummonedCreatures::process(bool cleanup) - { - MWMechanics::CreatureStats& creatureStats = mActor.getClass().getCreatureStats(mActor); - std::map& creatureMap = creatureStats.getSummonedCreatureMap(); + MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr()); - for (std::set::iterator it = mActiveEffects.begin(); it != mActiveEffects.end(); ++it) - { - bool found = creatureMap.find(*it) != creatureMap.end(); - if (!found) - { - std::string creatureID = getSummonedCreature(it->mEffectId); - if (!creatureID.empty()) + // Make the summoned creature follow its master and help in fights + AiFollow package(summoner); + summonedCreatureStats.getAiSequence().stack(package, ref.getPtr()); + creatureActorId = summonedCreatureStats.getActorId(); + + MWWorld::Ptr placed = world->safePlaceObject(ref.getPtr(), summoner, summoner.getCell(), 0, 120.f); + + MWRender::Animation* anim = world->getAnimation(placed); + if (anim) { - int creatureActorId = -1; - try - { - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), creatureID, 1); - - MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr()); - - // Make the summoned creature follow its master and help in fights - AiFollow package(mActor); - summonedCreatureStats.getAiSequence().stack(package, ref.getPtr()); - creatureActorId = summonedCreatureStats.getActorId(); - - MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(), mActor, mActor.getCell(), 0, 120.f); - - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(placed); - if (anim) - { - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() - .search("VFX_Summon_Start"); - if (fx) - anim->addEffect("meshes\\" + fx->mModel, -1, false); - } - } - catch (std::exception& e) - { - Log(Debug::Error) << "Failed to spawn summoned creature: " << e.what(); - // still insert into creatureMap so we don't try to spawn again every frame, that would spam the warning log - } - - creatureMap.emplace(*it, creatureActorId); + const ESM::Static* fx = world->getStore().get().search("VFX_Summon_Start"); + if (fx) + anim->addEffect("meshes\\" + fx->mModel, -1, false); } } - } - - // Update summon effects - for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) - { - bool found = mActiveEffects.find(it->first) != mActiveEffects.end(); - if (!found) + catch (std::exception& e) { - // Effect has ended - MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(mActor, it->second); - creatureMap.erase(it++); - continue; + Log(Debug::Error) << "Failed to spawn summoned creature: " << e.what(); + // still insert into creatureMap so we don't try to spawn again every frame, that would spam the warning log } - ++it; + + summoner.getClass().getCreatureStats(summoner).getSummonedCreatureMap().emplace(effectId, creatureActorId); } + return creatureActorId; + } + + void updateSummons(const MWWorld::Ptr& summoner, bool cleanup) + { + MWMechanics::CreatureStats& creatureStats = summoner.getClass().getCreatureStats(summoner); + auto& creatureMap = creatureStats.getSummonedCreatureMap(); std::vector graveyard = creatureStats.getSummonedCreatureGraveyard(); creatureStats.getSummonedCreatureGraveyard().clear(); for (const int creature : graveyard) - MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(mActor, creature); + MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(summoner, creature); if (!cleanup) return; - for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) + for (auto it = creatureMap.begin(); it != creatureMap.end(); ) { if(it->second == -1) { @@ -155,7 +125,7 @@ namespace MWMechanics if (!ptr.isEmpty() && ptr.getClass().getCreatureStats(ptr).isDead() && ptr.getClass().getCreatureStats(ptr).isDeathAnimationFinished()) { // Purge the magic effect so a new creature can be summoned if desired - purgeSummonEffect(mActor, *it); + purgeSummonEffect(summoner, *it); creatureMap.erase(it++); } else @@ -163,13 +133,13 @@ namespace MWMechanics } } - void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon) + void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon) { auto& creatureStats = summoner.getClass().getCreatureStats(summoner); - creatureStats.getActiveSpells().purgeEffect(summon.first.mEffectId, summon.first.mSourceId, summon.first.mEffectIndex); - creatureStats.getSpells().purgeEffect(summon.first.mEffectId, summon.first.mSourceId); - if (summoner.getClass().hasInventoryStore(summoner)) - summoner.getClass().getInventoryStore(summoner).purgeEffect(summon.first.mEffectId, summon.first.mSourceId, false, summon.first.mEffectIndex); + creatureStats.getActiveSpells().purge([summon] (const auto& spell, const auto& effect) + { + return effect.mEffectId == summon.first && effect.mArg == summon.second; + }, summoner); MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(summoner, summon.second); } diff --git a/apps/openmw/mwmechanics/summoning.hpp b/apps/openmw/mwmechanics/summoning.hpp index 3c3e18a96b..3186eef986 100644 --- a/apps/openmw/mwmechanics/summoning.hpp +++ b/apps/openmw/mwmechanics/summoning.hpp @@ -11,32 +11,15 @@ namespace MWMechanics { - class CreatureStats; - bool isSummoningEffect(int effectId); std::string getSummonedCreature(int effectId); - void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon); - - struct UpdateSummonedCreatures : public EffectSourceVisitor - { - UpdateSummonedCreatures(const MWWorld::Ptr& actor); - virtual ~UpdateSummonedCreatures() = default; - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override; - - /// To call after all effect sources have been visited - void process(bool cleanup); - - private: - MWWorld::Ptr mActor; + void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon); - std::set mActiveEffects; - }; + int summonCreature(int effectId, const MWWorld::Ptr& summoner); + void updateSummons(const MWWorld::Ptr& summoner, bool cleanup); } #endif diff --git a/apps/openmw/mwmechanics/tickableeffects.cpp b/apps/openmw/mwmechanics/tickableeffects.cpp deleted file mode 100644 index 5056179f8f..0000000000 --- a/apps/openmw/mwmechanics/tickableeffects.cpp +++ /dev/null @@ -1,216 +0,0 @@ -#include "tickableeffects.hpp" - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" -#include "../mwbase/world.hpp" - -#include "../mwworld/cellstore.hpp" -#include "../mwworld/class.hpp" -#include "../mwworld/containerstore.hpp" -#include "../mwworld/esmstore.hpp" -#include "../mwworld/inventorystore.hpp" - -#include "actorutil.hpp" -#include "npcstats.hpp" - -namespace MWMechanics -{ - void adjustDynamicStat(CreatureStats& creatureStats, int index, float magnitude, bool allowDecreaseBelowZero = false) - { - DynamicStat stat = creatureStats.getDynamic(index); - stat.setCurrent(stat.getCurrent() + magnitude, allowDecreaseBelowZero); - creatureStats.setDynamic(index, stat); - } - - bool disintegrateSlot (const MWWorld::Ptr& ptr, int slot, float disintegrate) - { - if (!ptr.getClass().hasInventoryStore(ptr)) - return false; - - MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); - MWWorld::ContainerStoreIterator item = inv.getSlot(slot); - - if (item != inv.end() && (item.getType() == MWWorld::ContainerStore::Type_Armor || item.getType() == MWWorld::ContainerStore::Type_Weapon)) - { - if (!item->getClass().hasItemHealth(*item)) - return false; - int charge = item->getClass().getItemHealth(*item); - if (charge == 0) - return false; - - // Store remainder of disintegrate amount (automatically subtracted if > 1) - item->getCellRef().applyChargeRemainderToBeSubtracted(disintegrate - std::floor(disintegrate)); - - charge = item->getClass().getItemHealth(*item); - charge -= std::min(static_cast(disintegrate), charge); - item->getCellRef().setCharge(charge); - - if (charge == 0) - { - // Will unequip the broken item and try to find a replacement - if (ptr != getPlayer()) - inv.autoEquip(ptr); - else - inv.unequipItem(*item, ptr); - } - - return true; - } - - return false; - } - - bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey &effectKey, float magnitude) - { - if (magnitude == 0.f) - return false; - - bool receivedMagicDamage = false; - bool godmode = actor == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - - switch (effectKey.mId) - { - case ESM::MagicEffect::DamageAttribute: - { - if (godmode) - break; - AttributeValue attr = creatureStats.getAttribute(effectKey.mArg); - attr.damage(magnitude); - creatureStats.setAttribute(effectKey.mArg, attr); - break; - } - case ESM::MagicEffect::RestoreAttribute: - { - AttributeValue attr = creatureStats.getAttribute(effectKey.mArg); - attr.restore(magnitude); - creatureStats.setAttribute(effectKey.mArg, attr); - break; - } - case ESM::MagicEffect::RestoreHealth: - case ESM::MagicEffect::RestoreMagicka: - case ESM::MagicEffect::RestoreFatigue: - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::RestoreHealth, magnitude); - break; - case ESM::MagicEffect::DamageHealth: - if (godmode) - break; - receivedMagicDamage = true; - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::DamageHealth, -magnitude); - break; - - case ESM::MagicEffect::DamageMagicka: - case ESM::MagicEffect::DamageFatigue: - { - if (godmode) - break; - int index = effectKey.mId-ESM::MagicEffect::DamageHealth; - static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game"); - adjustDynamicStat(creatureStats, index, -magnitude, index == 2 && uncappedDamageFatigue); - break; - } - case ESM::MagicEffect::AbsorbHealth: - if (!godmode || magnitude <= 0) - { - if (magnitude > 0.f) - receivedMagicDamage = true; - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude); - } - break; - - case ESM::MagicEffect::AbsorbMagicka: - case ESM::MagicEffect::AbsorbFatigue: - if (!godmode || magnitude <= 0) - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude); - break; - - case ESM::MagicEffect::DisintegrateArmor: - { - if (godmode) - break; - static const std::array priorities - { - MWWorld::InventoryStore::Slot_CarriedLeft, - MWWorld::InventoryStore::Slot_Cuirass, - MWWorld::InventoryStore::Slot_LeftPauldron, - MWWorld::InventoryStore::Slot_RightPauldron, - MWWorld::InventoryStore::Slot_LeftGauntlet, - MWWorld::InventoryStore::Slot_RightGauntlet, - MWWorld::InventoryStore::Slot_Helmet, - MWWorld::InventoryStore::Slot_Greaves, - MWWorld::InventoryStore::Slot_Boots - }; - for (const int priority : priorities) - { - if (disintegrateSlot(actor, priority, magnitude)) - break; - } - - break; - } - case ESM::MagicEffect::DisintegrateWeapon: - if (!godmode) - disintegrateSlot(actor, MWWorld::InventoryStore::Slot_CarriedRight, magnitude); - break; - - case ESM::MagicEffect::SunDamage: - { - // isInCell shouldn't be needed, but updateActor called during game start - if (!actor.isInCell() || !actor.getCell()->isExterior() || godmode) - break; - float time = MWBase::Environment::get().getWorld()->getTimeStamp().getHour(); - float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13))); - float damageScale = 1.f - timeDiff / 7.f; - // When cloudy, the sun damage effect is halved - static float fMagicSunBlockedMult = MWBase::Environment::get().getWorld()->getStore().get().find( - "fMagicSunBlockedMult")->mValue.getFloat(); - - int weather = MWBase::Environment::get().getWorld()->getCurrentWeather(); - if (weather > 1) - damageScale *= fMagicSunBlockedMult; - - adjustDynamicStat(creatureStats, 0, -magnitude * damageScale); - if (magnitude * damageScale > 0.f) - receivedMagicDamage = true; - - break; - } - - case ESM::MagicEffect::FireDamage: - case ESM::MagicEffect::ShockDamage: - case ESM::MagicEffect::FrostDamage: - case ESM::MagicEffect::Poison: - { - if (godmode) - break; - adjustDynamicStat(creatureStats, 0, -magnitude); - receivedMagicDamage = true; - break; - } - - case ESM::MagicEffect::DamageSkill: - case ESM::MagicEffect::RestoreSkill: - { - if (!actor.getClass().isNpc()) - break; - if (godmode && effectKey.mId == ESM::MagicEffect::DamageSkill) - break; - NpcStats &npcStats = actor.getClass().getNpcStats(actor); - SkillValue& skill = npcStats.getSkill(effectKey.mArg); - if (effectKey.mId == ESM::MagicEffect::RestoreSkill) - skill.restore(magnitude); - else - skill.damage(magnitude); - break; - } - - default: - return false; - } - - if (receivedMagicDamage && actor == getPlayer()) - MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); - return true; - } -} diff --git a/apps/openmw/mwmechanics/tickableeffects.hpp b/apps/openmw/mwmechanics/tickableeffects.hpp deleted file mode 100644 index ccd42ca19b..0000000000 --- a/apps/openmw/mwmechanics/tickableeffects.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef MWMECHANICS_TICKABLEEFFECTS_H -#define MWMECHANICS_TICKABLEEFFECTS_H - -namespace MWWorld -{ - class Ptr; -} - -namespace MWMechanics -{ - class CreatureStats; - struct EffectKey; - - /// Apply a magic effect that is applied in tick intervals until its remaining time ends or it is removed - /// Note: this function works in loop, so magic effects should not be removed here to avoid iterator invalidation. - /// @return Was the effect a tickable effect with a magnitude? - bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey& effectKey, float magnitude); -} - -#endif diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 7fc488020c..bcb2d36f5d 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -1101,34 +1101,6 @@ Resource::ResourceSystem* NpcAnimation::getResourceSystem() return mResourceSystem; } -void NpcAnimation::permanentEffectAdded(const ESM::MagicEffect *magicEffect, bool isNew) -{ - // During first auto equip, we don't play any sounds. - // Basically we don't want sounds when the actor is first loaded, - // the items should appear as if they'd always been equipped. - if (isNew) - { - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; - - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(!magicEffect->mHitSound.empty()) - sndMgr->playSound3D(mPtr, magicEffect->mHitSound, 1.0f, 1.0f); - else - sndMgr->playSound3D(mPtr, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f); - } - - if (!magicEffect->mHit.empty()) - { - const ESM::Static* castStatic = MWBase::Environment::get().getWorld()->getStore().get().find (magicEffect->mHit); - bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; - // Don't play particle VFX unless the effect is new or it should be looping. - if (isNew || loop) - addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", magicEffect->mParticle); - } -} - void NpcAnimation::enableHeadAnimation(bool enable) { mHeadAnimationTime->setEnabled(enable); diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index 768ac01622..9f7a186c5e 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -31,7 +31,6 @@ class NpcAnimation : public ActorAnimation, public WeaponAnimation, public MWWor { public: void equipmentChanged() override; - void permanentEffectAdded(const ESM::MagicEffect *magicEffect, bool isNew) override; public: typedef std::map PartBoneMap; diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 149f9f3f3f..3642f68dc2 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -563,13 +563,7 @@ namespace MWScript const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - MWMechanics::MagicEffects effects = stats.getSpells().getMagicEffects(); - effects += stats.getActiveSpells().getMagicEffects(); - if (ptr.getClass().hasInventoryStore(ptr) && !stats.isDeathAnimationFinished()) - { - MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); - effects += store.getMagicEffects(); - } + const MWMechanics::MagicEffects& effects = stats.getMagicEffects(); for (const auto& activeEffect : effects) { @@ -821,7 +815,7 @@ namespace MWScript } const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - runtime.push(stats.getActiveSpells().isSpellActive(id) || stats.getSpells().isSpellActive(id)); + runtime.push(stats.getActiveSpells().isSpellActive(id)); } }; diff --git a/apps/openmw/mwscript/statsextensions.cpp b/apps/openmw/mwscript/statsextensions.cpp index fe2df68473..ccad186a0d 100644 --- a/apps/openmw/mwscript/statsextensions.cpp +++ b/apps/openmw/mwscript/statsextensions.cpp @@ -473,6 +473,8 @@ namespace MWScript ESM::Spell::SpellType type = static_cast(spell->mData.mType); if (type != ESM::Spell::ST_Spell && type != ESM::Spell::ST_Power) { + // Add spell effect to *this actor's* queue immediately + creatureStats.getActiveSpells().addSpell(spell, ptr); // Apply looping particles immediately for constant effects MWBase::Environment::get().getWorld()->applyLoopingParticles(ptr); } @@ -492,17 +494,6 @@ namespace MWScript runtime.pop(); MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); - // The spell may have an instant effect which must be handled before the spell's removal. - for (const auto& effect : creatureStats.getSpells().getMagicEffects()) - { - if (effect.second.getMagnitude() <= 0) - continue; - MWMechanics::CastSpell cast(ptr, ptr); - if (cast.applyInstantEffect(ptr, ptr, effect.first, effect.second.getMagnitude())) - creatureStats.getSpells().purgeEffect(effect.first.mId); - } - - MWBase::Environment::get().getMechanicsManager()->restoreStatsAfterCorprus(ptr, id); creatureStats.getSpells().remove (id); MWBase::WindowManager *wm = MWBase::Environment::get().getWindowManager(); @@ -527,8 +518,7 @@ namespace MWScript std::string spellid = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); - ptr.getClass().getCreatureStats (ptr).getActiveSpells().removeEffects(spellid); - ptr.getClass().getCreatureStats (ptr).getSpells().removeEffects(spellid); + ptr.getClass().getCreatureStats (ptr).getActiveSpells().removeEffects(ptr, spellid); } }; @@ -544,7 +534,7 @@ namespace MWScript Interpreter::Type_Integer effectId = runtime[0].mInteger; runtime.pop(); - ptr.getClass().getCreatureStats (ptr).getActiveSpells().purgeEffect(effectId); + ptr.getClass().getCreatureStats (ptr).getActiveSpells().purgeEffect(ptr, effectId); } }; diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 3d82a30c29..b2ac511509 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -26,6 +26,7 @@ #include "../mwmechanics/recharge.hpp" #include "ptr.hpp" +#include "esmloader.hpp" #include "esmstore.hpp" #include "class.hpp" #include "containerstore.hpp" @@ -176,6 +177,13 @@ namespace if (state.mVersion < 15) fixRestocking(record, state); + if (state.mVersion < 17) + { + if constexpr (std::is_same_v) + MWWorld::convertMagicEffects(state.mCreatureStats, state.mInventory); + else if constexpr (std::is_same_v) + MWWorld::convertMagicEffects(state.mCreatureStats, state.mInventory, &state.mNpcStats); + } if (state.mRef.mRefNum.hasContentFile()) { diff --git a/apps/openmw/mwworld/esmloader.cpp b/apps/openmw/mwworld/esmloader.cpp index b12d646e70..a01128fe36 100644 --- a/apps/openmw/mwworld/esmloader.cpp +++ b/apps/openmw/mwworld/esmloader.cpp @@ -2,6 +2,26 @@ #include "esmstore.hpp" #include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwmechanics/magiceffects.hpp" + +namespace +{ + template + void getEnchantedItem(const std::string& id, std::string& enchantment, std::string& itemName) + { + const T* item = MWBase::Environment::get().getWorld()->getStore().get().search(id); + if(item) + { + enchantment = item->mEnchant; + itemName = item->mName; + } + } +} namespace MWWorld { @@ -28,4 +48,187 @@ void EsmLoader::load(const boost::filesystem::path& filepath, int& index) mStore.load(mEsm[index], &mListener); } + void convertMagicEffects(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, ESM::NpcStats* npcStats) + { + const auto& store = MWBase::Environment::get().getWorld()->getStore(); + // Convert corprus to format 10 + for (const auto& [id, oldStats] : creatureStats.mSpells.mCorprusSpells) + { + const ESM::Spell* spell = store.get().search(id); + if (!spell) + continue; + + ESM::CreatureStats::CorprusStats stats; + stats.mNextWorsening = oldStats.mNextWorsening; + for (int i=0; imEffects.mList) + { + if (effect.mEffectID == ESM::MagicEffect::DrainAttribute) + stats.mWorsenings[effect.mAttribute] = oldStats.mWorsenings; + } + creatureStats.mCorprusSpells[id] = stats; + } + // Convert to format 17 + for(const auto& [id, oldParams] : creatureStats.mSpells.mSpellParams) + { + const ESM::Spell* spell = store.get().search(id); + if (!spell || spell->mData.mType == ESM::Spell::ST_Spell || spell->mData.mType == ESM::Spell::ST_Power) + continue; + ESM::ActiveSpells::ActiveSpellParams params; + params.mId = id; + params.mDisplayName = spell->mName; + params.mItem.unset(); + params.mCasterActorId = creatureStats.mActorId; + if(spell->mData.mType == ESM::Spell::ST_Ability) + params.mType = ESM::ActiveSpells::Type_Ability; + else + params.mType = ESM::ActiveSpells::Type_Permanent; + params.mWorsenings = -1; + int effectIndex = 0; + for(const auto& enam : spell->mEffects.mList) + { + if(oldParams.mPurgedEffects.find(effectIndex) == oldParams.mPurgedEffects.end()) + { + ESM::ActiveEffect effect; + effect.mEffectId = enam.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mDuration = -1; + effect.mTimeLeft = -1; + effect.mEffectIndex = effectIndex; + auto rand = oldParams.mEffectRands.find(effectIndex); + if(rand != oldParams.mEffectRands.end()) + { + float magnitude = (enam.mMagnMax - enam.mMagnMin) * rand->second + enam.mMagnMin; + effect.mMagnitude = magnitude; + effect.mMinMagnitude = magnitude; + effect.mMaxMagnitude = magnitude; + // Prevent recalculation of resistances + effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances; + } + else + { + effect.mMagnitude = 0.f; + effect.mMinMagnitude = enam.mMagnMin; + effect.mMaxMagnitude = enam.mMagnMax; + effect.mFlags = ESM::ActiveEffect::Flag_None; + } + params.mEffects.emplace_back(effect); + } + effectIndex++; + } + creatureStats.mActiveSpells.mSpells.emplace_back(params); + } + std::multimap equippedItems; + for(std::size_t i = 0; i < inventory.mItems.size(); ++i) + { + const ESM::ObjectState& item = inventory.mItems[i]; + auto slot = inventory.mEquipmentSlots.find(i); + if(slot != inventory.mEquipmentSlots.end()) + equippedItems.emplace(item.mRef.mRefID, slot->second); + } + for(const auto& [id, oldMagnitudes] : inventory.mPermanentMagicEffectMagnitudes) + { + std::string eId; + std::string name; + switch(store.find(id)) + { + case ESM::REC_ARMO: + getEnchantedItem(id, eId, name); + break; + case ESM::REC_CLOT: + getEnchantedItem(id, eId, name); + break; + case ESM::REC_WEAP: + getEnchantedItem(id, eId, name); + break; + } + if(eId.empty()) + continue; + const ESM::Enchantment* enchantment = store.get().search(eId); + if(!enchantment) + continue; + ESM::ActiveSpells::ActiveSpellParams params; + params.mId = id; + params.mDisplayName = name; + params.mCasterActorId = creatureStats.mActorId; + params.mType = ESM::ActiveSpells::Type_Enchantment; + params.mWorsenings = -1; + for(std::size_t effectIndex = 0; effectIndex < oldMagnitudes.size() && effectIndex < enchantment->mEffects.mList.size(); ++effectIndex) + { + const auto& enam = enchantment->mEffects.mList[effectIndex]; + auto [random, multiplier] = oldMagnitudes[effectIndex]; + float magnitude = (enam.mMagnMax - enam.mMagnMin) * random + enam.mMagnMin; + magnitude *= multiplier; + if(magnitude <= 0) + continue; + ESM::ActiveEffect effect; + effect.mEffectId = enam.mEffectID; + effect.mMagnitude = magnitude; + effect.mMinMagnitude = magnitude; + effect.mMaxMagnitude = magnitude; + effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mDuration = -1; + effect.mTimeLeft = -1; + effect.mEffectIndex = static_cast(effectIndex); + // Prevent recalculation of resistances + effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances; + params.mEffects.emplace_back(effect); + } + auto [begin, end] = equippedItems.equal_range(id); + for(auto it = begin; it != end; ++it) + { + params.mItem = { static_cast(it->second), 0 }; + creatureStats.mActiveSpells.mSpells.emplace_back(params); + } + } + for(const auto& spell : creatureStats.mCorprusSpells) + { + auto it = std::find_if(creatureStats.mActiveSpells.mSpells.begin(), creatureStats.mActiveSpells.mSpells.end(), [&] (const auto& params) { return params.mId == spell.first; }); + if(it != creatureStats.mActiveSpells.mSpells.end()) + { + it->mNextWorsening = spell.second.mNextWorsening; + int worsenings = 0; + for(int i = 0; i < ESM::Attribute::Length; ++i) + worsenings = std::max(spell.second.mWorsenings[i], worsenings); + it->mWorsenings = worsenings; + } + } + for(const auto& [key, actorId] : creatureStats.mSummonedCreatureMap) + { + if(actorId == -1) + continue; + for(auto& params : creatureStats.mActiveSpells.mSpells) + { + if(params.mId == key.mSourceId) + { + bool found = false; + for(auto& effect : params.mEffects) + { + if(effect.mEffectId == key.mEffectId && effect.mEffectIndex == key.mEffectIndex) + { + effect.mArg = actorId; + found = true; + break; + } + } + if(found) + break; + } + } + } + // Reset modifiers that were previously recalculated each frame + for(std::size_t i = 0; i < ESM::Attribute::Length; ++i) + creatureStats.mAttributes[i].mMod = 0.f; + for(std::size_t i = 0; i < 3; ++i) + creatureStats.mDynamic[i].mMod = 0.f; + for(std::size_t i = 0; i < 4; ++i) + creatureStats.mAiSettings[i].mMod = 0.f; + if(npcStats) + { + for(std::size_t i = 0; i < ESM::Skill::Length; ++i) + npcStats->mSkills[i].mMod = 0.f; + } + } } /* namespace MWWorld */ diff --git a/apps/openmw/mwworld/esmloader.hpp b/apps/openmw/mwworld/esmloader.hpp index 506105bebb..50631603de 100644 --- a/apps/openmw/mwworld/esmloader.hpp +++ b/apps/openmw/mwworld/esmloader.hpp @@ -13,6 +13,9 @@ namespace ToUTF8 namespace ESM { class ESMReader; + struct CreatureStats; + struct InventoryState; + struct NpcStats; } namespace MWWorld @@ -33,6 +36,8 @@ struct EsmLoader : public ContentLoader ToUTF8::Utf8Encoder* mEncoder; }; +void convertMagicEffects(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, ESM::NpcStats* npcStats = nullptr); + } /* namespace MWWorld */ #endif // ESMLOADER_HPP diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index 0b8af4463c..49e60af1fd 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -108,11 +108,9 @@ MWWorld::InventoryStore::InventoryStore() MWWorld::InventoryStore::InventoryStore (const InventoryStore& store) : ContainerStore (store) - , mMagicEffects(store.mMagicEffects) , mInventoryListener(store.mInventoryListener) , mUpdatesEnabled(store.mUpdatesEnabled) , mFirstAutoEquip(store.mFirstAutoEquip) - , mPermanentMagicEffectMagnitudes(store.mPermanentMagicEffectMagnitudes) , mSelectedEnchantItem(end()) { copySlots (store); @@ -125,9 +123,7 @@ MWWorld::InventoryStore& MWWorld::InventoryStore::operator= (const InventoryStor mListener = store.mListener; mInventoryListener = store.mInventoryListener; - mMagicEffects = store.mMagicEffects; mFirstAutoEquip = store.mFirstAutoEquip; - mPermanentMagicEffectMagnitudes = store.mPermanentMagicEffectMagnitudes; mRechargingItemsUpToDate = false; ContainerStore::operator= (store); mSlots.clear(); @@ -186,8 +182,6 @@ void MWWorld::InventoryStore::equip (int slot, const ContainerStoreIterator& ite flagAsModified(); fireEquipmentChangedEvent(actor); - - updateMagicEffects(actor); } void MWWorld::InventoryStore::unequipAll(const MWWorld::Ptr& actor) @@ -199,7 +193,6 @@ void MWWorld::InventoryStore::unequipAll(const MWWorld::Ptr& actor) mUpdatesEnabled = true; fireEquipmentChangedEvent(actor); - updateMagicEffects(actor); } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getSlot (int slot) @@ -554,7 +547,6 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) { mSlots.swap (slots_); fireEquipmentChangedEvent(actor); - updateMagicEffects(actor); flagAsModified(); } } @@ -567,129 +559,6 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getPreferredShield(cons return slots[Slot_CarriedLeft]; } -const MWMechanics::MagicEffects& MWWorld::InventoryStore::getMagicEffects() const -{ - return mMagicEffects; -} - -void MWWorld::InventoryStore::updateMagicEffects(const Ptr& actor) -{ - // To avoid excessive updates during auto-equip - if (!mUpdatesEnabled) - return; - - // Delay update until the listener is set up - if (!mInventoryListener) - return; - - mMagicEffects = MWMechanics::MagicEffects(); - - const auto& stats = actor.getClass().getCreatureStats(actor); - if (stats.isDead() && stats.isDeathAnimationFinished()) - return; - - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter==end()) - continue; - - std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter); - - if (!enchantmentId.empty()) - { - const ESM::Enchantment& enchantment = - *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); - - if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) - continue; - - std::vector params; - - bool existed = (mPermanentMagicEffectMagnitudes.find((**iter).getCellRef().getRefId()) != mPermanentMagicEffectMagnitudes.end()); - if (!existed) - { - params.resize(enchantment.mEffects.mList.size()); - - int i=0; - for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) - { - int delta = effect.mMagnMax - effect.mMagnMin; - // Roll some dice, one for each effect - if (delta) - params[i].mRandom = Misc::Rng::rollDice(delta + 1) / static_cast(delta); - // Try resisting each effect - params[i].mMultiplier = MWMechanics::getEffectMultiplier(effect.mEffectID, actor, actor); - ++i; - } - - // Note that using the RefID as a key here is not entirely correct. - // Consider equipping the same item twice (e.g. a ring) - // However, permanent enchantments with a random magnitude are kind of an exploit anyway, - // so it doesn't really matter if both items will get the same magnitude. *Extreme* edge case. - mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()] = params; - } - else - params = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()]; - - int i=0; - for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) - { - const ESM::MagicEffect *magicEffect = - MWBase::Environment::get().getWorld()->getStore().get().find ( - effect.mEffectID); - - // Fully resisted or can't be applied to target? - if (params[i].mMultiplier == 0 || !MWMechanics::checkEffectTarget(effect.mEffectID, actor, actor, actor == MWMechanics::getPlayer())) - { - i++; - continue; - } - - float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * params[i].mRandom; - magnitude *= params[i].mMultiplier; - - if (!existed) - { - // During first auto equip, we don't play any sounds. - // Basically we don't want sounds when the actor is first loaded, - // the items should appear as if they'd always been equipped. - mInventoryListener->permanentEffectAdded(magicEffect, !mFirstAutoEquip); - } - - if (magnitude) - mMagicEffects.add (effect, magnitude); - - i++; - } - } - } - - // Now drop expired effects - for (TEffectMagnitudes::iterator it = mPermanentMagicEffectMagnitudes.begin(); - it != mPermanentMagicEffectMagnitudes.end();) - { - bool found = false; - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter == end()) - continue; - if ((**iter).getCellRef().getRefId() == it->first) - { - found = true; - } - } - if (!found) - mPermanentMagicEffectMagnitudes.erase(it++); - else - ++it; - } - - // Magic effects are normally not updated when paused, but we need this to make resistances work immediately after equipping - MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(actor); - - mFirstAutoEquip = false; -} - bool MWWorld::InventoryStore::stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) const { bool canStack = MWWorld::ContainerStore::stacks(ptr1, ptr2); @@ -800,7 +669,6 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipSlot(int slot, c if (applyUpdates) { fireEquipmentChangedEvent(actor); - updateMagicEffects(actor); } return retval; @@ -848,7 +716,7 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipItemQuantity(con return unstack(item, actor, item.getRefData().getCount() - count); } -MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() +MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() const { return mInventoryListener; } @@ -856,7 +724,6 @@ MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() void MWWorld::InventoryStore::setInvListener(InventoryStoreListener *listener, const Ptr& actor) { mInventoryListener = listener; - updateMagicEffects(actor); } void MWWorld::InventoryStore::fireEquipmentChangedEvent(const Ptr& actor) @@ -875,105 +742,6 @@ void MWWorld::InventoryStore::fireEquipmentChangedEvent(const Ptr& actor) */ } -void MWWorld::InventoryStore::visitEffectSources(MWMechanics::EffectSourceVisitor &visitor) -{ - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter==end()) - continue; - - std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter); - if (enchantmentId.empty()) - continue; - - const ESM::Enchantment& enchantment = - *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); - - if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) - continue; - - if (mPermanentMagicEffectMagnitudes.find((**iter).getCellRef().getRefId()) == mPermanentMagicEffectMagnitudes.end()) - continue; - - int i=0; - for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) - { - i++; - // Don't get spell icon display information for enchantments that weren't actually applied - if (mMagicEffects.get(MWMechanics::EffectKey(effect)).getMagnitude() == 0) - continue; - const EffectParams& params = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()][i-1]; - float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * params.mRandom; - magnitude *= params.mMultiplier; - if (magnitude > 0) - visitor.visit(MWMechanics::EffectKey(effect), i-1, (**iter).getClass().getName(**iter), (**iter).getCellRef().getRefId(), -1, magnitude); - } - } -} - -void MWWorld::InventoryStore::purgeEffect(short effectId, bool wholeSpell) -{ - for (TSlots::const_iterator it = mSlots.begin(); it != mSlots.end(); ++it) - { - if (*it != end()) - purgeEffect(effectId, (*it)->getCellRef().getRefId(), wholeSpell); - } -} - -void MWWorld::InventoryStore::purgeEffect(short effectId, const std::string &sourceId, bool wholeSpell, int effectIndex) -{ - TEffectMagnitudes::iterator effectMagnitudeIt = mPermanentMagicEffectMagnitudes.find(sourceId); - if (effectMagnitudeIt == mPermanentMagicEffectMagnitudes.end()) - return; - - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter==end()) - continue; - - if ((*iter)->getCellRef().getRefId() != sourceId) - continue; - - std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter); - - if (!enchantmentId.empty()) - { - const ESM::Enchantment& enchantment = - *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); - - if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) - continue; - - std::vector& params = effectMagnitudeIt->second; - - int i=0; - for (std::vector::const_iterator effectIt (enchantment.mEffects.mList.begin()); - effectIt!=enchantment.mEffects.mList.end(); ++effectIt, ++i) - { - if (effectIt->mEffectID != effectId) - continue; - - if (effectIndex >= 0 && effectIndex != i) - continue; - - if (wholeSpell) - { - mPermanentMagicEffectMagnitudes.erase(sourceId); - return; - } - - float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * params[i].mRandom; - magnitude *= params[i].mMultiplier; - - if (magnitude) - mMagicEffects.add (*effectIt, -magnitude); - - params[i].mMultiplier = 0; - } - } - } -} - void MWWorld::InventoryStore::clear() { mSlots.clear(); @@ -991,38 +759,9 @@ bool MWWorld::InventoryStore::isEquipped(const MWWorld::ConstPtr &item) return false; } -void MWWorld::InventoryStore::writeState(ESM::InventoryState &state) const -{ - MWWorld::ContainerStore::writeState(state); - - for (TEffectMagnitudes::const_iterator it = mPermanentMagicEffectMagnitudes.begin(); it != mPermanentMagicEffectMagnitudes.end(); ++it) - { - std::vector > params; - for (std::vector::const_iterator pIt = it->second.begin(); pIt != it->second.end(); ++pIt) - { - params.emplace_back(pIt->mRandom, pIt->mMultiplier); - } - - state.mPermanentMagicEffectMagnitudes[it->first] = params; - } -} - -void MWWorld::InventoryStore::readState(const ESM::InventoryState &state) +bool MWWorld::InventoryStore::isFirstEquip() { - MWWorld::ContainerStore::readState(state); - - for (ESM::InventoryState::TEffectMagnitudes::const_iterator it = state.mPermanentMagicEffectMagnitudes.begin(); - it != state.mPermanentMagicEffectMagnitudes.end(); ++it) - { - std::vector params; - for (std::vector >::const_iterator pIt = it->second.begin(); pIt != it->second.end(); ++pIt) - { - EffectParams p; - p.mRandom = pIt->first; - p.mMultiplier = pIt->second; - params.push_back(p); - } - - mPermanentMagicEffectMagnitudes[it->first] = params; - } + bool first = mFirstAutoEquip; + mFirstAutoEquip = false; + return first; } diff --git a/apps/openmw/mwworld/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index 32dc0d2e91..01c53d7028 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -25,15 +25,6 @@ namespace MWWorld */ virtual void equipmentChanged () {} - /** - * @param effect - * @param isNew Is this effect new (e.g. the item for it was just now manually equipped) - * or was it loaded from a savegame / initial game state? \n - * If it isn't new, non-looping VFX should not be played. - * @param playSound Play effect sound? - */ - virtual void permanentEffectAdded (const ESM::MagicEffect *magicEffect, bool isNew) {} - virtual ~InventoryStoreListener() = default; }; @@ -68,8 +59,6 @@ namespace MWWorld private: - MWMechanics::MagicEffects mMagicEffects; - InventoryStoreListener* mInventoryListener; // Enables updates of magic effects and actor model whenever items are equipped or unequipped. @@ -78,19 +67,6 @@ namespace MWWorld bool mFirstAutoEquip; - // Vanilla allows permanent effects with a random magnitude, so it needs to be stored here. - // We also need this to only play sounds and particle effects when the item is equipped, rather than on every update. - struct EffectParams - { - // Modifier to scale between min and max magnitude - float mRandom; - // Multiplier for when an effect was fully or partially resisted - float mMultiplier; - }; - - typedef std::map > TEffectMagnitudes; - TEffectMagnitudes mPermanentMagicEffectMagnitudes; - typedef std::vector TSlots; TSlots mSlots; @@ -106,8 +82,6 @@ namespace MWWorld void initSlots (TSlots& slots_); - void updateMagicEffects(const Ptr& actor); - void fireEquipmentChangedEvent(const Ptr& actor); void storeEquipmentState (const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const override; @@ -161,9 +135,6 @@ namespace MWWorld void autoEquip (const MWWorld::Ptr& actor); ///< Auto equip items according to stats and item value. - const MWMechanics::MagicEffects& getMagicEffects() const; - ///< Return magic effects from worn items. - bool stacks (const ConstPtr& ptr1, const ConstPtr& ptr2) const override; ///< @return true if the two specified objects can stack with each other @@ -198,22 +169,12 @@ namespace MWWorld void setInvListener (InventoryStoreListener* listener, const Ptr& actor); ///< Set a listener for various events, see \a InventoryStoreListener - InventoryStoreListener* getInvListener(); - - void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor); - - void purgeEffect (short effectId, bool wholeSpell = false); - ///< Remove a magic effect - - void purgeEffect (short effectId, const std::string& sourceId, bool wholeSpell = false, int effectIndex=-1); - ///< Remove a magic effect + InventoryStoreListener* getInvListener() const; void clear() override; ///< Empty container. - void writeState (ESM::InventoryState& state) const override; - - void readState (const ESM::InventoryState& state) override; + bool isFirstEquip(); }; } diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp index d8e2fb2f0d..caa0600f7c 100644 --- a/apps/openmw/mwworld/player.cpp +++ b/apps/openmw/mwworld/player.cpp @@ -22,9 +22,10 @@ #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellutil.hpp" +#include "cellstore.hpp" #include "class.hpp" +#include "esmloader.hpp" #include "ptr.hpp" -#include "cellstore.hpp" namespace MWWorld { @@ -381,6 +382,14 @@ namespace MWWorld // this is the one object we can not silently drop. throw std::runtime_error ("invalid player state record (object state)"); } + if (reader.getFormat() < 17) + { + convertMagicEffects(player.mObject.mCreatureStats, player.mObject.mInventory, &player.mObject.mNpcStats); + for(std::size_t i = 0; i < ESM::Attribute::Length; ++i) + player.mSaveAttributes[i].mMod = 0.f; + for(std::size_t i = 0; i < ESM::Skill::Length; ++i) + player.mSaveSkills[i].mMod = 0.f; + } if (!player.mObject.mEnabled) { diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index 25e7f0c7de..3b7be64ad1 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -256,7 +256,7 @@ namespace MWWorld state.mEffectAnimationTime->addTime(duration); } - void ProjectileManager::launchMagicBolt(const std::string &spellId, const Ptr &caster, const osg::Vec3f& fallbackDirection) + void ProjectileManager::launchMagicBolt(const std::string &spellId, const Ptr &caster, const osg::Vec3f& fallbackDirection, int slot) { osg::Vec3f pos = caster.getRefData().getPosition().asVec3(); if (caster.getClass().isActor()) @@ -278,6 +278,7 @@ namespace MWWorld MagicBoltState state; state.mSpellId = spellId; state.mCasterHandle = caster; + state.mSlot = slot; if (caster.getClass().isActor()) state.mActorId = caster.getClass().getCreatureStats(caster).getActorId(); else @@ -545,10 +546,10 @@ namespace MWWorld cast.mHitPosition = pos; cast.mId = magicBoltState.mSpellId; cast.mSourceName = magicBoltState.mSourceName; - cast.mStack = false; + cast.mSlot = magicBoltState.mSlot; cast.inflict(target, caster, magicBoltState.mEffects, ESM::RT_Target, false, true); - MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, target, ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName); + MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, target, ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName, false, magicBoltState.mSlot); magicBoltState.mToDelete = true; } @@ -628,7 +629,7 @@ namespace MWWorld state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition())); state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude())); state.mActorId = it->mActorId; - + state.mSlot = it->mSlot; state.mSpellId = it->mSpellId; state.mSpeed = it->mSpeed; @@ -684,6 +685,7 @@ namespace MWWorld state.mSpellId = esm.mSpellId; state.mActorId = esm.mActorId; state.mToDelete = false; + state.mSlot = esm.mSlot; std::string texture; try diff --git a/apps/openmw/mwworld/projectilemanager.hpp b/apps/openmw/mwworld/projectilemanager.hpp index 4dc250dc5f..f889250e1d 100644 --- a/apps/openmw/mwworld/projectilemanager.hpp +++ b/apps/openmw/mwworld/projectilemanager.hpp @@ -49,7 +49,7 @@ namespace MWWorld MWRender::RenderingManager* rendering, MWPhysics::PhysicsSystem* physics); /// If caster is an actor, the actor's facing orientation is used. Otherwise fallbackDirection is used. - void launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection); + void launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot); void launchProjectile (const MWWorld::Ptr& actor, const MWWorld::ConstPtr& projectile, const osg::Vec3f& pos, const osg::Quat& orient, const MWWorld::Ptr& bow, float speed, float attackStrength); @@ -107,6 +107,7 @@ namespace MWWorld ESM::EffectList mEffects; float mSpeed; + int mSlot; std::vector mSounds; std::set mSoundIds; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index da5daed295..ea455a31cf 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1862,7 +1862,7 @@ namespace MWWorld mRendering->getCamera()->setSneakOffset(0.f); int blind = 0; - auto& magicEffects = player.getClass().getCreatureStats(player).getMagicEffects(); + const auto& magicEffects = player.getClass().getCreatureStats(player).getMagicEffects(); if (!mGodMode) blind = static_cast(magicEffects.get(ESM::MagicEffect::Blind).getMagnitude()); MWBase::Environment::get().getWindowManager()->setBlindness(std::max(0, std::min(100, blind))); @@ -3109,7 +3109,20 @@ namespace MWWorld { MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); if (inv.getSelectedEnchantItem() != inv.end()) - cast.cast(*inv.getSelectedEnchantItem()); + { + const auto& itemPtr = *inv.getSelectedEnchantItem(); + auto [slots, _] = itemPtr.getClass().getEquipmentSlots(itemPtr); + int slot = 0; + for(std::size_t i = 0; i < slots.size(); ++i) + { + if(inv.getSlot(slots[i]) == inv.getSelectedEnchantItem()) + { + slot = slots[i]; + break; + } + } + cast.cast(itemPtr, slot); + } } } @@ -3144,9 +3157,9 @@ namespace MWWorld mProjectileManager->launchProjectile(actor, projectile, worldPos, orient, bow, speed, attackStrength); } - void World::launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) + void World::launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot) { - mProjectileManager->launchMagicBolt(spellId, caster, fallbackDirection); + mProjectileManager->launchMagicBolt(spellId, caster, fallbackDirection, slot); } void World::updateProjectilesCasters() @@ -3154,46 +3167,24 @@ namespace MWWorld mProjectileManager->updateCasters(); } - class ApplyLoopingParticlesVisitor : public MWMechanics::EffectSourceVisitor - { - private: - MWWorld::Ptr mActor; - - public: - ApplyLoopingParticlesVisitor(const MWWorld::Ptr& actor) - : mActor(actor) - { - } - - void visit (MWMechanics::EffectKey key, int /*effectIndex*/, - const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, - float /*magnitude*/, float /*remainingTime*/ = -1, float /*totalTime*/ = -1) override - { - const ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - const auto magicEffect = store.get().find(key.mId); - if ((magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) == 0) - return; - const ESM::Static* castStatic; - if (!magicEffect->mHit.empty()) - castStatic = store.get().find (magicEffect->mHit); - else - castStatic = store.get().find ("VFX_DefaultHit"); - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mActor); - if (anim && !castStatic->mModel.empty()) - anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, /*loop*/true, "", magicEffect->mParticle); - } - }; - void World::applyLoopingParticles(const MWWorld::Ptr& ptr) { const MWWorld::Class &cls = ptr.getClass(); if (cls.isActor()) { - ApplyLoopingParticlesVisitor visitor(ptr); - cls.getCreatureStats(ptr).getActiveSpells().visitEffectSources(visitor); - cls.getCreatureStats(ptr).getSpells().visitEffectSources(visitor); - if (cls.hasInventoryStore(ptr)) - cls.getInventoryStore(ptr).visitEffectSources(visitor); + std::set playing; + for(const auto& params : cls.getCreatureStats(ptr).getActiveSpells()) + { + for(const auto& effect : params.getEffects()) + { + if(playing.insert(effect.mEffectId).second) + { + const auto magicEffect = getStore().get().find(effect.mEffectId); + if(magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) + MWMechanics::playEffects(ptr, *magicEffect, false); + } + } + } } } @@ -3204,10 +3195,7 @@ namespace MWWorld void World::breakInvisibility(const Ptr &actor) { - actor.getClass().getCreatureStats(actor).getSpells().purgeEffect(ESM::MagicEffect::Invisibility); - actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(ESM::MagicEffect::Invisibility); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Invisibility); + actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(actor, ESM::MagicEffect::Invisibility); // Normally updated once per frame, but here it is kinda important to do it right away. MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(actor); @@ -3700,7 +3688,7 @@ namespace MWWorld } void World::explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const Ptr& caster, const Ptr& ignore, ESM::RangeType rangeType, - const std::string& id, const std::string& sourceName, const bool fromProjectile) + const std::string& id, const std::string& sourceName, const bool fromProjectile, int slot) { std::map > toApply; for (const ESM::ENAMstruct& effectInfo : effects.mList) @@ -3778,7 +3766,7 @@ namespace MWWorld cast.mHitPosition = origin; cast.mId = id; cast.mSourceName = sourceName; - cast.mStack = false; + cast.mSlot = slot; ESM::EffectList effectsToApply; effectsToApply.mList = applyPair.second; cast.inflict(applyPair.first, caster, effectsToApply, rangeType, false, true); diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 32fc9b101a..9ead6926c2 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -636,7 +636,7 @@ namespace MWWorld */ void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) override; - void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) override; + void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot) override; void launchProjectile (MWWorld::Ptr& actor, MWWorld::Ptr& projectile, const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr& bow, float speed, float attackStrength) override; void updateProjectilesCasters() override; @@ -681,7 +681,7 @@ namespace MWWorld void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const MWWorld::Ptr& ignore, ESM::RangeType rangeType, const std::string& id, const std::string& sourceName, - const bool fromProjectile=false) override; + const bool fromProjectile=false, int slot = 0) override; void activate (const MWWorld::Ptr& object, const MWWorld::Ptr& actor) override; diff --git a/components/esm/activespells.cpp b/components/esm/activespells.cpp index 4017a4933e..22f862b6e4 100644 --- a/components/esm/activespells.cpp +++ b/components/esm/activespells.cpp @@ -3,44 +3,67 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -namespace ESM +namespace { - - void ActiveSpells::save(ESMWriter &esm) const + void save(ESM::ESMWriter& esm, const std::vector& spells, const std::string& tag) { - for (TContainer::const_iterator it = mSpells.begin(); it != mSpells.end(); ++it) + for (const auto& params : spells) { - esm.writeHNString ("ID__", it->first); - - const ActiveSpellParams& params = it->second; + esm.writeHNString (tag, params.mId); esm.writeHNT ("CAST", params.mCasterActorId); esm.writeHNString ("DISP", params.mDisplayName); + esm.writeHNT ("TYPE", params.mType); + if(params.mItem.isSet()) + params.mItem.save(esm, true, "ITEM"); + if(params.mWorsenings >= 0) + { + esm.writeHNT ("WORS", params.mWorsenings); + esm.writeHNT ("TIME", params.mNextWorsening); + } - for (std::vector::const_iterator effectIt = params.mEffects.begin(); effectIt != params.mEffects.end(); ++effectIt) + for (auto effect : params.mEffects) { - esm.writeHNT ("MGEF", effectIt->mEffectId); - if (effectIt->mArg != -1) - esm.writeHNT ("ARG_", effectIt->mArg); - esm.writeHNT ("MAGN", effectIt->mMagnitude); - esm.writeHNT ("DURA", effectIt->mDuration); - esm.writeHNT ("EIND", effectIt->mEffectIndex); - esm.writeHNT ("LEFT", effectIt->mTimeLeft); + esm.writeHNT ("MGEF", effect.mEffectId); + if (effect.mArg != -1) + esm.writeHNT ("ARG_", effect.mArg); + esm.writeHNT ("MAGN", effect.mMagnitude); + esm.writeHNT ("MAGN", effect.mMinMagnitude); + esm.writeHNT ("MAGN", effect.mMaxMagnitude); + esm.writeHNT ("DURA", effect.mDuration); + esm.writeHNT ("EIND", effect.mEffectIndex); + esm.writeHNT ("LEFT", effect.mTimeLeft); + esm.writeHNT ("FLAG", effect.mFlags); } } } - void ActiveSpells::load(ESMReader &esm) + void load(ESM::ESMReader& esm, std::vector& spells, const char* tag) { int format = esm.getFormat(); - while (esm.isNextSub("ID__")) + while (esm.isNextSub(tag)) { - std::string spellId = esm.getHString(); - - ActiveSpellParams params; + ESM::ActiveSpells::ActiveSpellParams params; + params.mId = esm.getHString(); esm.getHNT (params.mCasterActorId, "CAST"); params.mDisplayName = esm.getHNString ("DISP"); + params.mItem.unset(); + if (format < 17) + params.mType = ESM::ActiveSpells::Type_Temporary; + else + { + esm.getHNT (params.mType, "TYPE"); + if(esm.peekNextSub("ITEM")) + params.mItem.load(esm, true, "ITEM"); + } + if(esm.isNextSub("WORS")) + { + esm.getHT(params.mWorsenings); + esm.getHNT(params.mNextWorsening, "TIME"); + } + else + params.mWorsenings = -1; // spell casting timestamp, no longer used if (esm.isNextSub("TIME")) @@ -48,11 +71,21 @@ namespace ESM while (esm.isNextSub("MGEF")) { - ActiveEffect effect; + ESM::ActiveEffect effect; esm.getHT(effect.mEffectId); effect.mArg = -1; esm.getHNOT(effect.mArg, "ARG_"); esm.getHNT (effect.mMagnitude, "MAGN"); + if (format < 17) + { + effect.mMinMagnitude = effect.mMagnitude; + effect.mMaxMagnitude = effect.mMagnitude; + } + else + { + esm.getHNT (effect.mMinMagnitude, "MAGN"); + esm.getHNT (effect.mMaxMagnitude, "MAGN"); + } esm.getHNT (effect.mDuration, "DURA"); effect.mEffectIndex = -1; esm.getHNOT (effect.mEffectIndex, "EIND"); @@ -60,10 +93,30 @@ namespace ESM effect.mTimeLeft = effect.mDuration; else esm.getHNT (effect.mTimeLeft, "LEFT"); + if (format < 17) + effect.mFlags = ESM::ActiveEffect::Flag_None; + else + esm.getHNT (effect.mFlags, "FLAG"); params.mEffects.push_back(effect); } - mSpells.insert(std::make_pair(spellId, params)); + spells.emplace_back(params); } } } + +namespace ESM +{ + + void ActiveSpells::save(ESMWriter &esm) const + { + ::save(esm, mSpells, "ID__"); + ::save(esm, mQueue, "QID_"); + } + + void ActiveSpells::load(ESMReader &esm) + { + ::load(esm, mSpells, "ID__"); + ::load(esm, mQueue, "QID_"); + } +} diff --git a/components/esm/activespells.hpp b/components/esm/activespells.hpp index 1b7f8b319c..8b5f1f1946 100644 --- a/components/esm/activespells.hpp +++ b/components/esm/activespells.hpp @@ -1,11 +1,12 @@ #ifndef OPENMW_ESM_ACTIVESPELLS_H #define OPENMW_ESM_ACTIVESPELLS_H -#include "effectlist.hpp" +#include "cellref.hpp" #include "defs.hpp" +#include "effectlist.hpp" #include -#include +#include namespace ESM { @@ -14,29 +15,53 @@ namespace ESM // Parameters of an effect concerning lasting effects. // Note we are not using ENAMstruct since the magnitude may be modified by magic resistance, etc. - // It could also be a negative magnitude, in case of inversing an effect, e.g. Absorb spell causes damage on target, but heals the caster. struct ActiveEffect { + enum Flags + { + Flag_None = 0, + Flag_Applied = 1 << 0, + Flag_Remove = 1 << 1, + Flag_Ignore_Resistances = 1 << 2 + }; + int mEffectId; float mMagnitude; + float mMinMagnitude; + float mMaxMagnitude; int mArg; // skill or attribute float mDuration; float mTimeLeft; int mEffectIndex; + int mFlags; }; // format 0, saved games only struct ActiveSpells { + enum EffectType + { + Type_Temporary, + Type_Ability, + Type_Enchantment, + Type_Permanent, + Type_Consumable + }; + struct ActiveSpellParams { + std::string mId; std::vector mEffects; std::string mDisplayName; int mCasterActorId; + RefNum mItem; + EffectType mType; + int mWorsenings; + TimeStamp mNextWorsening; }; - typedef std::multimap TContainer; - TContainer mSpells; + std::vector mSpells; + std::vector mQueue; void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/creaturestats.cpp b/components/esm/creaturestats.cpp index cb383992c6..d5030a6580 100644 --- a/components/esm/creaturestats.cpp +++ b/components/esm/creaturestats.cpp @@ -110,16 +110,31 @@ void ESM::CreatureStats::load (ESMReader &esm) mAiSequence.load(esm); mMagicEffects.load(esm); - while (esm.isNextSub("SUMM")) + if (esm.getFormat() < 17) { - int magicEffect; - esm.getHT(magicEffect); - std::string source = esm.getHNOString("SOUR"); - int effectIndex = -1; - esm.getHNOT (effectIndex, "EIND"); - int actorId; - esm.getHNT (actorId, "ACID"); - mSummonedCreatureMap[SummonKey(magicEffect, source, effectIndex)] = actorId; + while (esm.isNextSub("SUMM")) + { + int magicEffect; + esm.getHT(magicEffect); + std::string source = esm.getHNOString("SOUR"); + int effectIndex = -1; + esm.getHNOT (effectIndex, "EIND"); + int actorId; + esm.getHNT (actorId, "ACID"); + mSummonedCreatureMap[SummonKey(magicEffect, source, effectIndex)] = actorId; + mSummonedCreatures.emplace(magicEffect, actorId); + } + } + else + { + while (esm.isNextSub("SUMM")) + { + int magicEffect; + esm.getHT(magicEffect); + int actorId; + esm.getHNT (actorId, "ACID"); + mSummonedCreatures.emplace(magicEffect, actorId); + } } while (esm.isNextSub("GRAV")) @@ -214,14 +229,10 @@ void ESM::CreatureStats::save (ESMWriter &esm) const mAiSequence.save(esm); mMagicEffects.save(esm); - for (const auto& summon : mSummonedCreatureMap) + for (const auto& [effectId, actorId] : mSummonedCreatures) { - esm.writeHNT ("SUMM", summon.first.mEffectId); - esm.writeHNString ("SOUR", summon.first.mSourceId); - int effectIndex = summon.first.mEffectIndex; - if (effectIndex != -1) - esm.writeHNT ("EIND", effectIndex); - esm.writeHNT ("ACID", summon.second); + esm.writeHNT ("SUMM", effectId); + esm.writeHNT ("ACID", actorId); } for (int key : mSummonGraveyard) @@ -235,15 +246,6 @@ void ESM::CreatureStats::save (ESMWriter &esm) const for (int i=0; i<4; ++i) mAiSettings[i].save(esm); } - - for (const auto& corprusSpell : mCorprusSpells) - { - esm.writeHNString("CORP", corprusSpell.first); - - const CorprusStats & stats = corprusSpell.second; - esm.writeHNT("WORS", stats.mWorsenings); - esm.writeHNT("TIME", stats.mNextWorsening); - } } void ESM::CreatureStats::blank() diff --git a/components/esm/creaturestats.hpp b/components/esm/creaturestats.hpp index 13bc50008c..651b126d0e 100644 --- a/components/esm/creaturestats.hpp +++ b/components/esm/creaturestats.hpp @@ -40,6 +40,7 @@ namespace ESM StatState mAiSettings[4]; std::map mSummonedCreatureMap; + std::multimap mSummonedCreatures; std::vector mSummonGraveyard; ESM::TimeStamp mTradeTime; diff --git a/components/esm/magiceffects.cpp b/components/esm/magiceffects.cpp index 898e7e4b18..a1f943a93d 100644 --- a/components/esm/magiceffects.cpp +++ b/components/esm/magiceffects.cpp @@ -8,10 +8,11 @@ namespace ESM void MagicEffects::save(ESMWriter &esm) const { - for (std::map::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it) + for (const auto& [key, params] : mEffects) { - esm.writeHNT("EFID", it->first); - esm.writeHNT("BASE", it->second); + esm.writeHNT("EFID", key); + esm.writeHNT("BASE", params.first); + esm.writeHNT("MODI", params.second); } } @@ -19,10 +20,15 @@ void MagicEffects::load(ESMReader &esm) { while (esm.isNextSub("EFID")) { - int id, base; + int id; + std::pair params; esm.getHT(id); - esm.getHNT(base, "BASE"); - mEffects.insert(std::make_pair(id, base)); + esm.getHNT(params.first, "BASE"); + if(esm.getFormat() < 17) + params.second = 0.f; + else + esm.getHNT(params.second, "MODI"); + mEffects.emplace(id, params); } } diff --git a/components/esm/magiceffects.hpp b/components/esm/magiceffects.hpp index 5b8b0c924a..4b54692c5f 100644 --- a/components/esm/magiceffects.hpp +++ b/components/esm/magiceffects.hpp @@ -12,8 +12,8 @@ namespace ESM // format 0, saved games only struct MagicEffects { - // - std::map mEffects; + // + std::map> mEffects; void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/projectilestate.cpp b/components/esm/projectilestate.cpp index 8ade9d5b2e..3421c19526 100644 --- a/components/esm/projectilestate.cpp +++ b/components/esm/projectilestate.cpp @@ -28,6 +28,7 @@ namespace ESM esm.writeHNString ("SPEL", mSpellId); esm.writeHNT ("SPED", mSpeed); + esm.writeHNT ("SLOT", mSlot); } void MagicBoltState::load(ESMReader &esm) @@ -39,6 +40,10 @@ namespace ESM esm.skipHSub(); ESM::EffectList().load(esm); // for backwards compatibility esm.getHNT (mSpeed, "SPED"); + if(esm.getFormat() < 17) + mSlot = 0; + else + esm.getHNT(mSlot, "SLOT"); if (esm.isNextSub("STCK")) // for backwards compatibility esm.skipHSub(); if (esm.isNextSub("SOUN")) // for backwards compatibility diff --git a/components/esm/projectilestate.hpp b/components/esm/projectilestate.hpp index 67ec89bb6d..84292813ce 100644 --- a/components/esm/projectilestate.hpp +++ b/components/esm/projectilestate.hpp @@ -32,6 +32,7 @@ namespace ESM { std::string mSpellId; float mSpeed; + int mSlot; void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/savedgame.cpp b/components/esm/savedgame.cpp index 3f8bf10c56..8a98a63419 100644 --- a/components/esm/savedgame.cpp +++ b/components/esm/savedgame.cpp @@ -4,7 +4,7 @@ #include "esmwriter.hpp" unsigned int ESM::SavedGame::sRecordId = ESM::REC_SAVE; -int ESM::SavedGame::sCurrentFormat = 16; +int ESM::SavedGame::sCurrentFormat = 17; void ESM::SavedGame::load (ESMReader &esm) { diff --git a/components/esm/spellstate.cpp b/components/esm/spellstate.cpp index 2eb1e78679..b1ddb6523c 100644 --- a/components/esm/spellstate.cpp +++ b/components/esm/spellstate.cpp @@ -8,29 +8,38 @@ namespace ESM void SpellState::load(ESMReader &esm) { - while (esm.isNextSub("SPEL")) + if(esm.getFormat() < 17) { - std::string id = esm.getHString(); - - SpellParams state; - while (esm.isNextSub("INDX")) + while (esm.isNextSub("SPEL")) { - int index; - esm.getHT(index); + std::string id = esm.getHString(); - float magnitude; - esm.getHNT(magnitude, "RAND"); + SpellParams state; + while (esm.isNextSub("INDX")) + { + int index; + esm.getHT(index); - state.mEffectRands[index] = magnitude; - } + float magnitude; + esm.getHNT(magnitude, "RAND"); - while (esm.isNextSub("PURG")) { - int index; - esm.getHT(index); - state.mPurgedEffects.insert(index); - } + state.mEffectRands[index] = magnitude; + } - mSpells[id] = state; + while (esm.isNextSub("PURG")) { + int index; + esm.getHT(index); + state.mPurgedEffects.insert(index); + } + + mSpellParams[id] = state; + mSpells.emplace_back(id); + } + } + else + { + while (esm.isNextSub("SPEL")) + mSpells.emplace_back(esm.getHString()); } // Obsolete @@ -88,30 +97,8 @@ namespace ESM void SpellState::save(ESMWriter &esm) const { - for (TContainer::const_iterator it = mSpells.begin(); it != mSpells.end(); ++it) - { - esm.writeHNString("SPEL", it->first); - - const std::map& random = it->second.mEffectRands; - for (std::map::const_iterator rIt = random.begin(); rIt != random.end(); ++rIt) - { - esm.writeHNT("INDX", rIt->first); - esm.writeHNT("RAND", rIt->second); - } - - const std::set& purges = it->second.mPurgedEffects; - for (std::set::const_iterator pIt = purges.begin(); pIt != purges.end(); ++pIt) - esm.writeHNT("PURG", *pIt); - } - - for (std::map::const_iterator it = mCorprusSpells.begin(); it != mCorprusSpells.end(); ++it) - { - esm.writeHNString("CORP", it->first); - - const CorprusStats & stats = it->second; - esm.writeHNT("WORS", stats.mWorsenings); - esm.writeHNT("TIME", stats.mNextWorsening); - } + for (const std::string& spell : mSpells) + esm.writeHNString("SPEL", spell); for (std::map::const_iterator it = mUsedPowers.begin(); it != mUsedPowers.end(); ++it) { diff --git a/components/esm/spellstate.hpp b/components/esm/spellstate.hpp index 55c57611a2..e7067dae8c 100644 --- a/components/esm/spellstate.hpp +++ b/components/esm/spellstate.hpp @@ -31,13 +31,13 @@ namespace ESM struct SpellParams { - std::map mEffectRands; - std::set mPurgedEffects; + std::map mEffectRands; // + std::set mPurgedEffects; // indices of purged effects }; - typedef std::map TContainer; - TContainer mSpells; + std::vector mSpells; // FIXME: obsolete, used only for old saves + std::map mSpellParams; std::map > mPermanentSpellEffects; std::map mCorprusSpells; From 161e042e2ac72c84280bdb7005190fc3b53b2cad Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sat, 28 Aug 2021 11:06:47 +0200 Subject: [PATCH 2/8] Prevent iterator invalidation when cleaning up summons --- apps/openmw/mwgui/container.cpp | 3 ++- apps/openmw/mwmechanics/summoning.cpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwgui/container.cpp b/apps/openmw/mwgui/container.cpp index df490943d2..de771051ef 100644 --- a/apps/openmw/mwgui/container.cpp +++ b/apps/openmw/mwgui/container.cpp @@ -277,8 +277,9 @@ namespace MWGui auto it = std::find_if(summons.begin(), summons.end(), [&] (const auto& entry) { return entry.second == creatureStats.getActorId(); }); if(it != summons.end()) { - MWMechanics::purgeSummonEffect(summoner, *it); + auto summon = *it; summons.erase(it); + MWMechanics::purgeSummonEffect(summoner, summon); break; } } diff --git a/apps/openmw/mwmechanics/summoning.cpp b/apps/openmw/mwmechanics/summoning.cpp index 953db077fb..c1923d4337 100644 --- a/apps/openmw/mwmechanics/summoning.cpp +++ b/apps/openmw/mwmechanics/summoning.cpp @@ -125,8 +125,9 @@ namespace MWMechanics if (!ptr.isEmpty() && ptr.getClass().getCreatureStats(ptr).isDead() && ptr.getClass().getCreatureStats(ptr).isDeathAnimationFinished()) { // Purge the magic effect so a new creature can be summoned if desired - purgeSummonEffect(summoner, *it); + auto summon = *it; creatureMap.erase(it++); + purgeSummonEffect(summoner, summon); } else ++it; From b8e4f1875197621ce4587d8f42081b260ab90ddd Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sat, 28 Aug 2021 14:36:30 +0200 Subject: [PATCH 3/8] Clear temporary effects before unloading actors to prevent absorb effects becoming permanent --- CHANGELOG.md | 1 + apps/openmw/mwbase/mechanicsmanager.hpp | 2 +- apps/openmw/mwbase/world.hpp | 2 +- apps/openmw/mwmechanics/actors.cpp | 15 +++++++++++++-- apps/openmw/mwmechanics/actors.hpp | 2 +- apps/openmw/mwmechanics/mechanicsmanagerimp.cpp | 6 +++--- apps/openmw/mwmechanics/mechanicsmanagerimp.hpp | 2 +- apps/openmw/mwworld/actionteleport.cpp | 4 ++-- apps/openmw/mwworld/scene.cpp | 4 ++-- apps/openmw/mwworld/scene.hpp | 2 +- apps/openmw/mwworld/worldimp.cpp | 8 ++++---- apps/openmw/mwworld/worldimp.hpp | 2 +- 12 files changed, 31 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffe687a04c..34e7829274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ Feature #4595: Unique object identifier Feature #4737: Handle instance move from one cell to another Feature #5198: Implement "Magic effect expired" event + Feature #5454: Clear active spells from actor when he disappears from scene Feature #5489: MCP: Telekinesis fix for activators Feature #5996: Support Lua scripts in OpenMW Feature #6017: Separate persistent and temporary cell references when saving diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index ccc60afc27..6bedbb5b4d 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -58,7 +58,7 @@ namespace MWBase virtual void add (const MWWorld::Ptr& ptr) = 0; ///< Register an object for management - virtual void remove (const MWWorld::Ptr& ptr) = 0; + virtual void remove (const MWWorld::Ptr& ptr, bool keepActive) = 0; ///< Deregister an object for management virtual void updateCell(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) = 0; diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index e9fc6e5c7a..22fe42c5ed 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -286,7 +286,7 @@ namespace MWBase virtual MWWorld::Ptr moveObject (const MWWorld::Ptr& ptr, const osg::Vec3f& position, bool movePhysics=true, bool moveToActive=false) = 0; ///< @return an updated Ptr in case the Ptr's cell changes - virtual MWWorld::Ptr moveObject(const MWWorld::Ptr &ptr, MWWorld::CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true) = 0; + virtual MWWorld::Ptr moveObject(const MWWorld::Ptr &ptr, MWWorld::CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true, bool keepActive=false) = 0; ///< @return an updated Ptr virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, const osg::Vec3f& vec, bool moveToActive, bool ignoreCollisions) = 0; diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index b1aa246385..d71a8f53ee 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -210,6 +210,14 @@ void soulTrap(const MWWorld::Ptr& creature) } } } + +void removeTemporaryEffects(const MWWorld::Ptr& ptr) +{ + ptr.getClass().getCreatureStats(ptr).getActiveSpells().purge([] (const auto& spell) + { + return spell.getType() == ESM::ActiveSpells::Type_Consumable || spell.getType() == ESM::ActiveSpells::Type_Temporary; + }, ptr); +} } namespace MWMechanics @@ -1050,7 +1058,7 @@ namespace MWMechanics void Actors::addActor (const MWWorld::Ptr& ptr, bool updateImmediately) { - removeActor(ptr); + removeActor(ptr, true); MWRender::Animation *anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (!anim) @@ -1098,11 +1106,13 @@ namespace MWMechanics ctrl->setVisibility(visibilityRatio); } - void Actors::removeActor (const MWWorld::Ptr& ptr) + void Actors::removeActor (const MWWorld::Ptr& ptr, bool keepActive) { PtrActorMap::iterator iter = mActors.find(ptr); if(iter != mActors.end()) { + if(!keepActive) + removeTemporaryEffects(iter->first); delete iter->second; mActors.erase(iter); } @@ -1167,6 +1177,7 @@ namespace MWMechanics { if((iter->first.isInCell() && iter->first.getCell()==cellStore) && iter->first != ignore) { + removeTemporaryEffects(iter->first); delete iter->second; mActors.erase(iter++); } diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 3efe28204d..9950a591ab 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -90,7 +90,7 @@ namespace MWMechanics /// /// \note Dead actors are ignored. - void removeActor (const MWWorld::Ptr& ptr); + void removeActor (const MWWorld::Ptr& ptr, bool keepActive); ///< Deregister an actor for stats management /// /// \note Ignored, if \a ptr is not a registered actor. diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index fb8b3fa2b0..5e18e737df 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -259,11 +259,11 @@ namespace MWMechanics mActors.castSpell(ptr, spellId, manualSpell); } - void MechanicsManager::remove(const MWWorld::Ptr& ptr) + void MechanicsManager::remove(const MWWorld::Ptr& ptr, bool keepActive) { if(ptr == MWBase::Environment::get().getWindowManager()->getWatchedActor()) MWBase::Environment::get().getWindowManager()->watchActor(MWWorld::Ptr()); - mActors.removeActor(ptr); + mActors.removeActor(ptr, keepActive); mObjects.removeObject(ptr); } @@ -317,7 +317,7 @@ namespace MWMechanics // HACK? The player has been changed, so a new Animation object may // have been made for them. Make sure they're properly updated. - mActors.removeActor(ptr); + mActors.removeActor(ptr, true); mActors.addActor(ptr, true); } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 206b5c5420..06da2fde51 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -45,7 +45,7 @@ namespace MWMechanics void add (const MWWorld::Ptr& ptr) override; ///< Register an object for management - void remove (const MWWorld::Ptr& ptr) override; + void remove (const MWWorld::Ptr& ptr, bool keepActive) override; ///< Deregister an object for management void updateCell(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) override; diff --git a/apps/openmw/mwworld/actionteleport.cpp b/apps/openmw/mwworld/actionteleport.cpp index fcfafc5f4f..56274eb9a0 100644 --- a/apps/openmw/mwworld/actionteleport.cpp +++ b/apps/openmw/mwworld/actionteleport.cpp @@ -55,10 +55,10 @@ namespace MWWorld int cellY; world->positionToIndex(mPosition.pos[0],mPosition.pos[1],cellX,cellY); world->moveObject(actor,world->getExterior(cellX,cellY), - mPosition.asVec3()); + mPosition.asVec3(), true, true); } else - world->moveObject(actor,world->getInterior(mCellName),mPosition.asVec3()); + world->moveObject(actor,world->getInterior(mCellName),mPosition.asVec3(), true, true); } } diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index 559ab4aef4..57a5bd18f7 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -1010,9 +1010,9 @@ namespace MWWorld } } - void Scene::removeObjectFromScene (const Ptr& ptr) + void Scene::removeObjectFromScene (const Ptr& ptr, bool keepActive) { - MWBase::Environment::get().getMechanicsManager()->remove (ptr); + MWBase::Environment::get().getMechanicsManager()->remove (ptr, keepActive); MWBase::Environment::get().getSoundManager()->stopSound3D (ptr); MWBase::Environment::get().getLuaManager()->objectRemovedFromScene(ptr); if (const auto object = mPhysics->getObject(ptr)) diff --git a/apps/openmw/mwworld/scene.hpp b/apps/openmw/mwworld/scene.hpp index 3f5d7bf2f5..86cb1ee5b7 100644 --- a/apps/openmw/mwworld/scene.hpp +++ b/apps/openmw/mwworld/scene.hpp @@ -161,7 +161,7 @@ namespace MWWorld void addObjectToScene (const Ptr& ptr); ///< Add an object that already exists in the world model to the scene. - void removeObjectFromScene (const Ptr& ptr); + void removeObjectFromScene (const Ptr& ptr, bool keepActive = false); ///< Remove an object from the scene, but not from the world model. void removeFromPagedRefs(const Ptr &ptr); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index ea455a31cf..b9b8af0987 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1114,7 +1114,7 @@ namespace MWWorld } } - MWWorld::Ptr World::moveObject(const Ptr &ptr, CellStore* newCell, const osg::Vec3f& position, bool movePhysics) + MWWorld::Ptr World::moveObject(const Ptr &ptr, CellStore* newCell, const osg::Vec3f& position, bool movePhysics, bool keepActive) { ESM::Position pos = ptr.getRefData().getPosition(); std::memcpy(pos.pos, &position, sizeof(osg::Vec3f)); @@ -1171,7 +1171,7 @@ namespace MWWorld } else if (!newCellActive && currCellActive) { - mWorldScene->removeObjectFromScene(ptr); + mWorldScene->removeObjectFromScene(ptr, keepActive); mLocalScripts.remove(ptr); removeContainerScripts (ptr); haveToMove = false; @@ -2433,7 +2433,7 @@ namespace MWWorld else { // Remove the old CharacterController - MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr()); + MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr(), true); mNavigator->removeAgent(getPathfindingHalfExtents(getPlayerConstPtr())); mPhysics->remove(getPlayerPtr()); mRendering->removePlayer(getPlayerPtr()); @@ -2449,7 +2449,7 @@ namespace MWWorld void World::renderPlayer() { - MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr()); + MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr(), true); MWWorld::Ptr player = getPlayerPtr(); diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 9ead6926c2..d6b7eb3fbf 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -373,7 +373,7 @@ namespace MWWorld MWWorld::Ptr moveObject (const Ptr& ptr, const osg::Vec3f& position, bool movePhysics=true, bool moveToActive=false) override; ///< @return an updated Ptr in case the Ptr's cell changes - MWWorld::Ptr moveObject (const Ptr& ptr, CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true) override; + MWWorld::Ptr moveObject (const Ptr& ptr, CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true, bool keepActive=false) override; ///< @return an updated Ptr MWWorld::Ptr moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec, bool moveToActive, bool ignoreCollisions) override; From 43074347e82066c3601edf195e959a0e14d7f9f9 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Tue, 31 Aug 2021 19:59:55 +0200 Subject: [PATCH 4/8] Prevent spell duplication --- apps/openmw/mwmechanics/spells.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwmechanics/spells.cpp b/apps/openmw/mwmechanics/spells.cpp index 743eacfe2c..6520ae3ab3 100644 --- a/apps/openmw/mwmechanics/spells.cpp +++ b/apps/openmw/mwmechanics/spells.cpp @@ -141,7 +141,7 @@ namespace MWMechanics const ESM::Spell *spell = *iter; if (filter(spell)) { - mSpells.erase(iter++); + iter = mSpells.erase(iter); purged.push_back(spell->mId); } else @@ -204,7 +204,7 @@ namespace MWMechanics const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(id); if (spell) { - mSpells.emplace_back(spell); + addSpell(spell); if (id == state.mSelectedSpell) mSelectedSpell = id; From 63a9203dde6cd4cccc64fa81fb33ae6b3af1a4e5 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Tue, 31 Aug 2021 20:07:30 +0200 Subject: [PATCH 5/8] Fix icon magnitude --- apps/openmw/mwgui/spellicons.cpp | 2 +- apps/openmw/mwmechanics/spelleffects.cpp | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwgui/spellicons.cpp b/apps/openmw/mwgui/spellicons.cpp index 4a86503f46..0673446fe7 100644 --- a/apps/openmw/mwgui/spellicons.cpp +++ b/apps/openmw/mwgui/spellicons.cpp @@ -34,7 +34,7 @@ namespace MWGui { for(const auto& effect : params.getEffects()) { - if(!effect.mMagnitude) + if(!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) continue; MagicEffectInfo newEffectSource; newEffectSource.mKey = MWMechanics::EffectKey(effect.mEffectId, effect.mArg); diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 895e908bd2..96416f00c0 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -705,10 +705,15 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac return true; } const auto* magicEffect = world->getStore().get().find(effect.mEffectId); - if(effect.mFlags & ESM::ActiveEffect::Flag_Applied && magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce) + if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) { - effect.mTimeLeft -= dt; - return false; + if(magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce) + { + effect.mTimeLeft -= dt; + return false; + } + else if(!dt) + return false; } if(effect.mEffectId == ESM::MagicEffect::Lock) { @@ -786,8 +791,9 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac float oldMagnitude = 0.f; if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) oldMagnitude = effect.mMagnitude; + float magnitude = roll(effect); //Note that there's an early out for Flag_Applied AppliedOnce effects so we don't have to exclude them here - effect.mMagnitude = roll(effect); + effect.mMagnitude = magnitude; if(!(magicEffect->mData.mFlags & (ESM::MagicEffect::Flags::NoMagnitude | ESM::MagicEffect::Flags::AppliedOnce))) { if(effect.mDuration != 0) @@ -808,6 +814,7 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac spellParams.worsen(); else applyMagicEffect(target, caster, spellParams, effect, invalid, receivedMagicDamage); + effect.mMagnitude = magnitude; magnitudes.add(EffectKey(effect.mEffectId, effect.mArg), EffectParam(effect.mMagnitude - oldMagnitude)); } effect.mTimeLeft -= dt; From 46ce2ff10cab2ad3a41306b254ca69e68b25aed7 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Tue, 31 Aug 2021 20:12:11 +0200 Subject: [PATCH 6/8] Add #4414 to the changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34e7829274..0550725bc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ Feature #2780: A way to see current OpenMW version in the console Feature #3616: Allow Zoom levels on the World Map Feature #4297: Implement APPLIED_ONCE flag for magic effects + Feature #4414: Handle duration of EXTRA SPELL magic effect Feature #4595: Unique object identifier Feature #4737: Handle instance move from one cell to another Feature #5198: Implement "Magic effect expired" event From 4abcb0d7b960c79e5990e5893654512a22f9f20e Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Wed, 8 Sep 2021 19:25:22 +0200 Subject: [PATCH 7/8] Remove applied magnitude instead of min magnitude --- apps/openmw/mwmechanics/spelleffects.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 96416f00c0..24816b9ed1 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -1029,7 +1029,7 @@ void onMagicEffectRemoved(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellP auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); const auto* magicEffect = world->getStore().get().find(effect.mEffectId); if(magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce) - magnitudes.add(EffectKey(effect.mEffectId, effect.mArg), EffectParam(-effect.mMinMagnitude)); + magnitudes.add(EffectKey(effect.mEffectId, effect.mArg), EffectParam(-effect.mMagnitude)); removeMagicEffect(target, spellParams, effect); auto anim = world->getAnimation(target); if(anim) From e4994054ecabbd5eb5199e62ec84ff82ad57f258 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Tue, 21 Sep 2021 18:07:39 +0200 Subject: [PATCH 8/8] Add #5207 to the changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0550725bc8..25df64c65c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Bug #4744: Invisible particles must still be processed Bug #5100: Persuasion doesn't always clamp the resulting disposition Bug #5120: Scripted object spawning updates physics system + Bug #5207: Loose summons can be present in scene Bug #5379: Wandering NPCs falling through cantons Bug #5453: Magic effect VFX are offset for creatures Bug #5483: AutoCalc flag is not used to calculate spells cost