From 2dbe30ed5c790b0aed862a3dd90dca1e3d076c80 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 23 May 2024 17:15:56 +0200 Subject: [PATCH] Update effects upon applying them --- apps/openmw/mwmechanics/activespells.cpp | 258 +++++++++++++---------- apps/openmw/mwmechanics/activespells.hpp | 8 +- apps/openmw/mwmechanics/spellcasting.cpp | 4 - apps/openmw/mwmechanics/spelleffects.cpp | 6 +- 4 files changed, 156 insertions(+), 120 deletions(-) diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index 270faf8598..f9b7ec57ea 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -73,6 +73,20 @@ namespace namespace MWMechanics { + struct ActiveSpells::UpdateContext + { + bool mUpdatedEnemy = false; + bool mUpdatedHitOverlay = false; + bool mUpdateSpellWindow = false; + bool mPlayNonLooping = false; + bool mUpdate; + + UpdateContext(bool update) + : mUpdate(update) + { + } + }; + ActiveSpells::IterationGuard::IterationGuard(ActiveSpells& spells) : mActiveSpells(spells) { @@ -256,8 +270,9 @@ namespace MWMechanics ++spellIt; } + UpdateContext context(duration > 0.f); for (const auto& spell : mQueue) - addToSpells(ptr, spell); + addToSpells(ptr, spell, context); mQueue.clear(); // Vanilla only does this on cell change I think @@ -267,20 +282,17 @@ namespace MWMechanics if (spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power && !isSpellActive(spell->mId)) { - mSpells.emplace_back(ActiveSpellParams{ spell, ptr, true }); - mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); + initParams(ptr, ActiveSpellParams{ spell, ptr, true }, context); } } - bool updateSpellWindow = false; - bool playNonLooping = false; if (ptr.getClass().hasInventoryStore(ptr) && !(creatureStats.isDead() && !creatureStats.isDeathAnimationFinished())) { auto& store = ptr.getClass().getInventoryStore(ptr); if (store.getInvListener() != nullptr) { - playNonLooping = !store.isFirstEquip(); + context.mPlayNonLooping = !store.isFirstEquip(); const auto world = MWBase::Environment::get().getWorld(); for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) { @@ -306,117 +318,18 @@ namespace MWMechanics // invisibility manually purgeEffect(ptr, ESM::MagicEffect::Invisibility); applyPurges(ptr); - ActiveSpellParams& params = mSpells.emplace_back(ActiveSpellParams{ *slot, enchantment, ptr }); - params.setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); - updateSpellWindow = true; + ActiveSpellParams* params = initParams(ptr, ActiveSpellParams{ *slot, enchantment, ptr }, context); + if (params) + context.mUpdateSpellWindow = true; } } } const MWWorld::Ptr player = MWMechanics::getPlayer(); - bool updatedHitOverlay = false; - bool updatedEnemy = false; // Update effects for (auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { - const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId( - spellIt->mCasterActorId); // Maybe make this search outside active grid? - bool removedSpell = false; - std::optional reflected; - for (auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();) - { - auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration, playNonLooping); - if (result.mType == MagicApplicationResult::Type::REFLECTED) - { - if (!reflected) - { - if (Settings::game().mClassicReflectedAbsorbSpellsBehavior) - reflected = { *spellIt, caster }; - else - reflected = { *spellIt, ptr }; - } - auto& reflectedEffect = reflected->mEffects.emplace_back(*it); - reflectedEffect.mFlags - = ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; - it = spellIt->mEffects.erase(it); - } - else if (result.mType == MagicApplicationResult::Type::REMOVED) - it = spellIt->mEffects.erase(it); - else - { - ++it; - if (!updatedEnemy && result.mShowHealth && caster == player && ptr != player) - { - MWBase::Environment::get().getWindowManager()->setEnemy(ptr); - updatedEnemy = true; - } - if (!updatedHitOverlay && result.mShowHit && ptr == player) - { - MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); - updatedHitOverlay = true; - } - } - removedSpell = applyPurges(ptr, &spellIt, &it); - if (removedSpell) - break; - } - if (reflected) - { - const ESM::Static* reflectStatic = MWBase::Environment::get().getESMStore()->get().find( - ESM::RefId::stringRefId("VFX_Reflect")); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); - if (animation && !reflectStatic->mModel.empty()) - { - const VFS::Path::Normalized reflectStaticModel - = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(reflectStatic->mModel)); - animation->addEffect( - reflectStaticModel, ESM::MagicEffect::indexToName(ESM::MagicEffect::Reflect), false); - } - caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected); - } - if (removedSpell) - continue; - - bool remove = false; - if (spellIt->hasFlag(ESM::ActiveSpells::Flag_SpellStore)) - { - try - { - remove = !spells.hasSpell(spellIt->mSourceSpellId); - } - catch (const std::runtime_error& e) - { - remove = true; - Log(Debug::Error) << "Removing active effect: " << e.what(); - } - } - else if (spellIt->hasFlag(ESM::ActiveSpells::Flag_Equipment)) - { - // Remove effects tied to equipment that has been unequipped - const auto& store = ptr.getClass().getInventoryStore(ptr); - remove = true; - for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) - { - auto slot = store.getSlot(slotIndex); - if (slot != store.end() && slot->getCellRef().getRefNum().isSet() - && slot->getCellRef().getRefNum() == spellIt->mItem) - { - remove = false; - break; - } - } - } - if (remove) - { - auto params = *spellIt; - spellIt = mSpells.erase(spellIt); - for (const auto& effect : params.mEffects) - onMagicEffectRemoved(ptr, params, effect); - applyPurges(ptr, &spellIt); - updateSpellWindow = true; - continue; - } - ++spellIt; + updateActiveSpell(ptr, duration, spellIt, context); } if (Settings::game().mClassicCalmSpellsBehavior) @@ -427,7 +340,7 @@ namespace MWMechanics creatureStats.getAiSequence().stopCombat(); } - if (ptr == player && updateSpellWindow) + if (ptr == player && context.mUpdateSpellWindow) { // Something happened with the spell list -- possibly while the game is paused, // so we want to make the spell window get the memo. @@ -436,7 +349,125 @@ namespace MWMechanics } } - void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell) + bool ActiveSpells::updateActiveSpell( + const MWWorld::Ptr& ptr, float duration, Collection::iterator& spellIt, UpdateContext& context) + { + const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId( + spellIt->mCasterActorId); // Maybe make this search outside active grid? + bool removedSpell = false; + std::optional reflected; + for (auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();) + { + auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration, context.mPlayNonLooping); + if (result.mType == MagicApplicationResult::Type::REFLECTED) + { + if (!reflected) + { + if (Settings::game().mClassicReflectedAbsorbSpellsBehavior) + reflected = { *spellIt, caster }; + else + reflected = { *spellIt, ptr }; + } + auto& reflectedEffect = reflected->mEffects.emplace_back(*it); + reflectedEffect.mFlags + = ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; + it = spellIt->mEffects.erase(it); + } + else if (result.mType == MagicApplicationResult::Type::REMOVED) + it = spellIt->mEffects.erase(it); + else + { + const MWWorld::Ptr player = MWMechanics::getPlayer(); + ++it; + if (!context.mUpdatedEnemy && result.mShowHealth && caster == player && ptr != player) + { + MWBase::Environment::get().getWindowManager()->setEnemy(ptr); + context.mUpdatedEnemy = true; + } + if (!context.mUpdatedHitOverlay && result.mShowHit && ptr == player) + { + MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); + context.mUpdatedHitOverlay = true; + } + } + removedSpell = applyPurges(ptr, &spellIt, &it); + if (removedSpell) + break; + } + if (reflected) + { + const ESM::Static* reflectStatic = MWBase::Environment::get().getESMStore()->get().find( + ESM::RefId::stringRefId("VFX_Reflect")); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); + if (animation && !reflectStatic->mModel.empty()) + { + const VFS::Path::Normalized reflectStaticModel + = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(reflectStatic->mModel)); + animation->addEffect( + reflectStaticModel, ESM::MagicEffect::indexToName(ESM::MagicEffect::Reflect), false); + } + caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected); + } + if (removedSpell) + return true; + + bool remove = false; + if (spellIt->hasFlag(ESM::ActiveSpells::Flag_SpellStore)) + { + try + { + auto& spells = ptr.getClass().getCreatureStats(ptr).getSpells(); + remove = !spells.hasSpell(spellIt->mSourceSpellId); + } + catch (const std::runtime_error& e) + { + remove = true; + Log(Debug::Error) << "Removing active effect: " << e.what(); + } + } + else if (spellIt->hasFlag(ESM::ActiveSpells::Flag_Equipment)) + { + // Remove effects tied to equipment that has been unequipped + const auto& store = ptr.getClass().getInventoryStore(ptr); + remove = true; + for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) + { + auto slot = store.getSlot(slotIndex); + if (slot != store.end() && slot->getCellRef().getRefNum().isSet() + && slot->getCellRef().getRefNum() == spellIt->mItem) + { + remove = false; + break; + } + } + } + if (remove) + { + auto params = *spellIt; + spellIt = mSpells.erase(spellIt); + for (const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); + applyPurges(ptr, &spellIt); + context.mUpdateSpellWindow = true; + return true; + } + ++spellIt; + return false; + } + + ActiveSpells::ActiveSpellParams* ActiveSpells::initParams( + const MWWorld::Ptr& ptr, const ActiveSpellParams& params, UpdateContext& context) + { + mSpells.emplace_back(params).setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); + auto it = mSpells.end(); + --it; + // We instantly apply the effect with a duration of 0 so continuous effects can be purged before truly applying + if (context.mUpdate && updateActiveSpell(ptr, 0.f, it, context)) + return nullptr; + return &*it; + } + + void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell, UpdateContext& context) { if (!spell.hasFlag(ESM::ActiveSpells::Flag_Stackable)) { @@ -454,8 +485,7 @@ namespace MWMechanics onMagicEffectRemoved(ptr, params, effect); } } - mSpells.emplace_back(spell); - mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); + initParams(ptr, spell, context); } ActiveSpells::ActiveSpells() @@ -608,6 +638,8 @@ namespace MWMechanics { purge( [=](const ActiveSpellParams&, const ESM::ActiveEffect& effect) { + if (!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) + return false; if (effectArg.empty()) return effect.mEffectId == effectId; return effect.mEffectId == effectId && effect.getSkillOrAttribute() == effectArg; diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 3e4dafdb26..465e5aa456 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -116,17 +116,23 @@ namespace MWMechanics IterationGuard(ActiveSpells& spells); ~IterationGuard(); }; + struct UpdateContext; std::list mSpells; std::vector mQueue; std::queue mPurges; bool mIterating; - void addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell); + void addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell, UpdateContext& context); bool applyPurges(const MWWorld::Ptr& ptr, std::list::iterator* currentSpell = nullptr, std::vector::iterator* currentEffect = nullptr); + bool updateActiveSpell( + const MWWorld::Ptr& ptr, float duration, Collection::iterator& spellIt, UpdateContext& context); + + ActiveSpellParams* initParams(const MWWorld::Ptr& ptr, const ActiveSpellParams& params, UpdateContext& context); + public: ActiveSpells(); diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 59e7e29a38..bf9d6aa025 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -215,10 +215,6 @@ namespace MWMechanics bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); effect.mDuration = hasDuration ? static_cast(enam.mData.mDuration) : 1.f; - bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; - if (!appliedOnce) - effect.mDuration = std::max(1.f, effect.mDuration); - effect.mTimeLeft = effect.mDuration; // add to list of active effects, to apply in next frame diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 99e5a09481..822c394352 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -1011,11 +1011,13 @@ namespace MWMechanics else { // Morrowind.exe doesn't apply magic effects while the menu is open, we do because we like to see stats - // updated instantly. We don't want to teleport instantly though + // updated instantly. We don't want to teleport instantly though. Nor do we want to force players to drink + // invisibility potions in the "right" order if (!dt && (effect.mEffectId == ESM::MagicEffect::Recall || effect.mEffectId == ESM::MagicEffect::DivineIntervention - || effect.mEffectId == ESM::MagicEffect::AlmsiviIntervention)) + || effect.mEffectId == ESM::MagicEffect::AlmsiviIntervention + || effect.mEffectId == ESM::MagicEffect::Invisibility)) return { MagicApplicationResult::Type::APPLIED, receivedMagicDamage, affectedHealth }; auto& stats = target.getClass().getCreatureStats(target); auto& magnitudes = stats.getMagicEffects();